Jump To …

content.js

var _ = require("underscore");

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.

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;
      }
    },
    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.

        processor = _(this.type.split(";")[0]
          .split(/\+|\//)).detect(function(type) {
            return Content.processors[type];
          });
        return Content.processors[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() { 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: identity });

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); }});

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;