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