| |
| // The purpose of the `Content` object is to abstract away the data conversions |
| // to and from raw content entities as strings. For example, you want to be able |
| // to pass in a Javascript object and have it be automatically converted into a |
| // JSON string if the `content-type` is set to a JSON-based media type. |
| // Conversely, you want to be able to transparently get back a Javascript object |
| // in the response if the `content-type` is a JSON-based media-type. |
| |
| // One limitation of the current implementation is that it [assumes the `charset` is UTF-8](https://github.com/spire-io/shred/issues/5). |
| |
| // The `Content` constructor takes an options object, which *must* have either a |
| // `body` or `data` property and *may* have a `type` property indicating the |
| // media type. If there is no `type` attribute, a default will be inferred. |
| var Content = function(options) { |
| this.body = options.body; |
| this.data = options.data; |
| this.type = options.type; |
| }; |
| |
| Content.prototype = { |
| // Treat `toString()` as asking for the `content.body`. That is, the raw content entity. |
| // |
| // toString: function() { return this.body; } |
| // |
| // Commented out, but I've forgotten why. :/ |
| }; |
| |
| |
| // `Content` objects have the following attributes: |
| Object.defineProperties(Content.prototype,{ |
| |
| // - **type**. Typically accessed as `content.type`, reflects the `content-type` |
| // header associated with the request or response. If not passed as an options |
| // to the constructor or set explicitly, it will infer the type the `data` |
| // attribute, if possible, and, failing that, will default to `text/plain`. |
| type: { |
| get: function() { |
| if (this._type) { |
| return this._type; |
| } else { |
| if (this._data) { |
| switch(typeof this._data) { |
| case "string": return "text/plain"; |
| case "object": return "application/json"; |
| } |
| } |
| } |
| return "text/plain"; |
| }, |
| set: function(value) { |
| this._type = value; |
| return this; |
| }, |
| enumerable: true |
| }, |
| |
| // - **data**. Typically accessed as `content.data`, reflects the content entity |
| // converted into Javascript data. This can be a string, if the `type` is, say, |
| // `text/plain`, but can also be a Javascript object. The conversion applied is |
| // based on the `processor` attribute. The `data` attribute can also be set |
| // directly, in which case the conversion will be done the other way, to infer |
| // the `body` attribute. |
| data: { |
| get: function() { |
| if (this._body) { |
| return this.processor.parser(this._body); |
| } else { |
| return this._data; |
| } |
| }, |
| set: function(data) { |
| if (this._body&&data) Errors.setDataWithBody(this); |
| this._data = data; |
| return this; |
| }, |
| enumerable: true |
| }, |
| |
| // - **body**. Typically accessed as `content.body`, reflects the content entity |
| // as a UTF-8 string. It is the mirror of the `data` attribute. If you set the |
| // `data` attribute, the `body` attribute will be inferred and vice-versa. If |
| // you attempt to set both, an exception is raised. |
| body: { |
| get: function() { |
| if (this._data) { |
| return this.processor.stringify(this._data); |
| } else { |
| return this._body.toString(); |
| } |
| }, |
| set: function(body) { |
| if (this._data&&body) Errors.setBodyWithData(this); |
| this._body = body; |
| return this; |
| }, |
| enumerable: true |
| }, |
| |
| // - **processor**. The functions that will be used to convert to/from `data` and |
| // `body` attributes. You can add processors. The two that are built-in are for |
| // `text/plain`, which is basically an identity transformation and |
| // `application/json` and other JSON-based media types (including custom media |
| // types with `+json`). You can add your own processors. See below. |
| processor: { |
| get: function() { |
| var processor = Content.processors[this.type]; |
| if (processor) { |
| return processor; |
| } else { |
| // Return the first processor that matches any part of the |
| // content type. ex: application/vnd.foobar.baz+json will match json. |
| var main = this.type.split(";")[0]; |
| var parts = main.split(/\+|\//); |
| for (var i=0, l=parts.length; i < l; i++) { |
| processor = Content.processors[parts[i]] |
| } |
| return processor || {parser:identity,stringify:toString}; |
| } |
| }, |
| enumerable: true |
| }, |
| |
| // - **length**. Typically accessed as `content.length`, returns the length in |
| // bytes of the raw content entity. |
| length: { |
| get: function() { |
| if (typeof Buffer !== 'undefined') { |
| return Buffer.byteLength(this.body); |
| } |
| return this.body.length; |
| } |
| } |
| }); |
| |
| Content.processors = {}; |
| |
| // The `registerProcessor` function allows you to add your own processors to |
| // convert content entities. Each processor consists of a Javascript object with |
| // two properties: |
| // - **parser**. The function used to parse a raw content entity and convert it |
| // into a Javascript data type. |
| // - **stringify**. The function used to convert a Javascript data type into a |
| // raw content entity. |
| Content.registerProcessor = function(types,processor) { |
| |
| // You can pass an array of types that will trigger this processor, or just one. |
| // We determine the array via duck-typing here. |
| if (types.forEach) { |
| types.forEach(function(type) { |
| Content.processors[type] = processor; |
| }); |
| } else { |
| // If you didn't pass an array, we just use what you pass in. |
| Content.processors[types] = processor; |
| } |
| }; |
| |
| // Register the identity processor, which is used for text-based media types. |
| var identity = function(x) { return x; } |
| , toString = function(x) { return x.toString(); } |
| Content.registerProcessor( |
| ["text/html","text/plain","text"], |
| { parser: identity, stringify: toString }); |
| |
| // Register the JSON processor, which is used for JSON-based media types. |
| Content.registerProcessor( |
| ["application/json; charset=utf-8","application/json","json"], |
| { |
| parser: function(string) { |
| return JSON.parse(string); |
| }, |
| stringify: function(data) { |
| return JSON.stringify(data); }}); |
| |
| var qs = require('querystring'); |
| // Register the post processor, which is used for JSON-based media types. |
| Content.registerProcessor( |
| ["application/x-www-form-urlencoded"], |
| { parser : qs.parse, stringify : qs.stringify }); |
| |
| // Error functions are defined separately here in an attempt to make the code |
| // easier to read. |
| var Errors = { |
| setDataWithBody: function(object) { |
| throw new Error("Attempt to set data attribute of a content object " + |
| "when the body attributes was already set."); |
| }, |
| setBodyWithData: function(object) { |
| throw new Error("Attempt to set body attribute of a content object " + |
| "when the data attributes was already set."); |
| } |
| } |
| module.exports = Content; |