You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. // The purpose of the `Content` object is to abstract away the data conversions
  2. // to and from raw content entities as strings. For example, you want to be able
  3. // to pass in a Javascript object and have it be automatically converted into a
  4. // JSON string if the `content-type` is set to a JSON-based media type.
  5. // Conversely, you want to be able to transparently get back a Javascript object
  6. // in the response if the `content-type` is a JSON-based media-type.
  7. // One limitation of the current implementation is that it [assumes the `charset` is UTF-8](https://github.com/spire-io/shred/issues/5).
  8. // The `Content` constructor takes an options object, which *must* have either a
  9. // `body` or `data` property and *may* have a `type` property indicating the
  10. // media type. If there is no `type` attribute, a default will be inferred.
  11. var Content = function(options) {
  12. this.body = options.body;
  13. this.data = options.data;
  14. this.type = options.type;
  15. };
  16. Content.prototype = {
  17. // Treat `toString()` as asking for the `content.body`. That is, the raw content entity.
  18. //
  19. // toString: function() { return this.body; }
  20. //
  21. // Commented out, but I've forgotten why. :/
  22. };
  23. // `Content` objects have the following attributes:
  24. Object.defineProperties(Content.prototype,{
  25. // - **type**. Typically accessed as `content.type`, reflects the `content-type`
  26. // header associated with the request or response. If not passed as an options
  27. // to the constructor or set explicitly, it will infer the type the `data`
  28. // attribute, if possible, and, failing that, will default to `text/plain`.
  29. type: {
  30. get: function() {
  31. if (this._type) {
  32. return this._type;
  33. } else {
  34. if (this._data) {
  35. switch(typeof this._data) {
  36. case "string": return "text/plain";
  37. case "object": return "application/json";
  38. }
  39. }
  40. }
  41. return "text/plain";
  42. },
  43. set: function(value) {
  44. this._type = value;
  45. return this;
  46. },
  47. enumerable: true
  48. },
  49. // - **data**. Typically accessed as `content.data`, reflects the content entity
  50. // converted into Javascript data. This can be a string, if the `type` is, say,
  51. // `text/plain`, but can also be a Javascript object. The conversion applied is
  52. // based on the `processor` attribute. The `data` attribute can also be set
  53. // directly, in which case the conversion will be done the other way, to infer
  54. // the `body` attribute.
  55. data: {
  56. get: function() {
  57. if (this._body) {
  58. return this.processor.parser(this._body);
  59. } else {
  60. return this._data;
  61. }
  62. },
  63. set: function(data) {
  64. if (this._body&&data) Errors.setDataWithBody(this);
  65. this._data = data;
  66. return this;
  67. },
  68. enumerable: true
  69. },
  70. // - **body**. Typically accessed as `content.body`, reflects the content entity
  71. // as a UTF-8 string. It is the mirror of the `data` attribute. If you set the
  72. // `data` attribute, the `body` attribute will be inferred and vice-versa. If
  73. // you attempt to set both, an exception is raised.
  74. body: {
  75. get: function() {
  76. if (this._data) {
  77. return this.processor.stringify(this._data);
  78. } else {
  79. return this._body.toString();
  80. }
  81. },
  82. set: function(body) {
  83. if (this._data&&body) Errors.setBodyWithData(this);
  84. this._body = body;
  85. return this;
  86. },
  87. enumerable: true
  88. },
  89. // - **processor**. The functions that will be used to convert to/from `data` and
  90. // `body` attributes. You can add processors. The two that are built-in are for
  91. // `text/plain`, which is basically an identity transformation and
  92. // `application/json` and other JSON-based media types (including custom media
  93. // types with `+json`). You can add your own processors. See below.
  94. processor: {
  95. get: function() {
  96. var processor = Content.processors[this.type];
  97. if (processor) {
  98. return processor;
  99. } else {
  100. // Return the first processor that matches any part of the
  101. // content type. ex: application/vnd.foobar.baz+json will match json.
  102. var main = this.type.split(";")[0];
  103. var parts = main.split(/\+|\//);
  104. for (var i=0, l=parts.length; i < l; i++) {
  105. processor = Content.processors[parts[i]]
  106. }
  107. return processor || {parser:identity,stringify:toString};
  108. }
  109. },
  110. enumerable: true
  111. },
  112. // - **length**. Typically accessed as `content.length`, returns the length in
  113. // bytes of the raw content entity.
  114. length: {
  115. get: function() {
  116. if (typeof Buffer !== 'undefined') {
  117. return Buffer.byteLength(this.body);
  118. }
  119. return this.body.length;
  120. }
  121. }
  122. });
  123. Content.processors = {};
  124. // The `registerProcessor` function allows you to add your own processors to
  125. // convert content entities. Each processor consists of a Javascript object with
  126. // two properties:
  127. // - **parser**. The function used to parse a raw content entity and convert it
  128. // into a Javascript data type.
  129. // - **stringify**. The function used to convert a Javascript data type into a
  130. // raw content entity.
  131. Content.registerProcessor = function(types,processor) {
  132. // You can pass an array of types that will trigger this processor, or just one.
  133. // We determine the array via duck-typing here.
  134. if (types.forEach) {
  135. types.forEach(function(type) {
  136. Content.processors[type] = processor;
  137. });
  138. } else {
  139. // If you didn't pass an array, we just use what you pass in.
  140. Content.processors[types] = processor;
  141. }
  142. };
  143. // Register the identity processor, which is used for text-based media types.
  144. var identity = function(x) { return x; }
  145. , toString = function(x) { return x.toString(); }
  146. Content.registerProcessor(
  147. ["text/html","text/plain","text"],
  148. { parser: identity, stringify: toString });
  149. // Register the JSON processor, which is used for JSON-based media types.
  150. Content.registerProcessor(
  151. ["application/json; charset=utf-8","application/json","json"],
  152. {
  153. parser: function(string) {
  154. return JSON.parse(string);
  155. },
  156. stringify: function(data) {
  157. return JSON.stringify(data); }});
  158. var qs = require('querystring');
  159. // Register the post processor, which is used for JSON-based media types.
  160. Content.registerProcessor(
  161. ["application/x-www-form-urlencoded"],
  162. { parser : qs.parse, stringify : qs.stringify });
  163. // Error functions are defined separately here in an attempt to make the code
  164. // easier to read.
  165. var Errors = {
  166. setDataWithBody: function(object) {
  167. throw new Error("Attempt to set data attribute of a content object " +
  168. "when the body attributes was already set.");
  169. },
  170. setBodyWithData: function(object) {
  171. throw new Error("Attempt to set body attribute of a content object " +
  172. "when the data attributes was already set.");
  173. }
  174. }
  175. module.exports = Content;