1 // Written in the D programming language.
2 
3 /**
4  * Yajl Encoder
5  *
6  * Example:
7  * -----
8  * Encoder encoder;
9  * assert(encoder.encode(["foo":"bar"] == `{"foo":"bar"}`));
10  * -----
11  *
12  * with option:
13  * -----
14  * Encoder.Option opt;
15  * opt.beautify = true;
16  * opt.indentString = "  "; 
17  * Encoder encoder = Encoder(opt);
18  * assert(encoder.encode(["foo":"bar"] ==
19  * `{
20  *    "foo": "bar"
21  * }
22  * `));
23  * -----
24  *
25  * See_Also:
26  *  $(LINK2 http://lloyd.github.com/yajl/yajl-2.0.1/yajl__gen_8h.html, Yajl gen header)$(BR)
27  *
28  * Copyright: Copyright Masahiro Nakagawa 2013-.
29  * License:   <a href="http://www.boost.org/LICENSE_1_0.txt">Boost License 1.0</a>.
30  * Authors:   Masahiro Nakagawa
31  */
32 module yajl.encoder;
33 
34 import yajl.c.gen;
35 import yajl.common;
36 
37 import std.traits;
38 
39 
40 /**
41  * Encoder provides the method for serializing a D object into JSON format.
42  */
43 struct Encoder
44 {
45     //alias yajl_print_t PrintCallback;
46 
47     /// See: http://lloyd.github.io/yajl/yajl-2.1.0/yajl__gen_8h.html#a57c29080044a7231ac0cf1fead4de4b0
48     static struct Option
49     {
50         bool beautify;
51         bool validateUTF8;
52         bool escapeSolidus;
53         string indentString;
54         //PrintCallback printCallback; // TODO: Fix "killed by signal 11"
55     }
56 
57   private:
58     yajl_gen _gen;
59 
60   public:
61     /**
62      * Constructs an Encoder object with $(D_PARAM opt).
63      */
64     @trusted
65     this(ref Option opt)
66     {
67         initialize();
68         setEncoderConfig(_gen, opt);
69     }
70 
71     @trusted
72     ~this()
73     {
74         clear();
75     }
76 
77     /**
78      * Encodes an argument and returns the JSON.
79      */
80     @trusted
81     string encode(T)(auto ref T value)
82     {
83         initialize();
84 
85         yajlGenerate(_gen, value);
86 
87         ubyte* resultBuffer;
88         size_t resultLength;
89 
90         yajl_gen_get_buf(_gen, &resultBuffer, &resultLength);
91 
92         string result = cast(string)resultBuffer[0..resultLength].dup;
93 
94         // reset state and internal buffer for next encode
95         yajl_gen_reset(_gen, null);
96         yajl_gen_clear(_gen);
97 
98         return result;
99     }
100 
101   private:
102     void initialize()
103     {
104         if (_gen is null)
105             _gen = yajl_gen_alloc(&yajlAllocFuncs);
106     }
107 
108     void clear()
109     {
110         if (_gen !is null) {
111             yajl_gen_free(_gen);
112             _gen = null;
113         }
114     }
115 
116     @trusted
117     static void setEncoderConfig(yajl_gen gen, ref Encoder.Option opt)
118     {
119         import std.string : toStringz;
120 
121         if (opt.beautify)
122             yajl_gen_config(gen, yajl_gen_option.yajl_gen_beautify, 1);
123         if (opt.validateUTF8)
124             yajl_gen_config(gen, yajl_gen_option.yajl_gen_validate_utf8, 1);
125         if (opt.escapeSolidus)
126             yajl_gen_config(gen, yajl_gen_option.yajl_gen_escape_solidus, 1);
127         if (opt.indentString)
128             yajl_gen_config(gen, yajl_gen_option.yajl_gen_indent_string, toStringz(opt.indentString));
129         //if (opt.printCallback)
130         //    yajl_gen_config(gen, yajl_gen_option.yajl_gen_print_callback, opt.printCallback);
131     }
132 }
133 
134 unittest
135 {
136     static struct Handa
137     {
138         ulong id;
139         string name;
140         double height;
141     }
142 
143     Handa handa = Handa(1000, "shinobu", 169.5);
144 
145     {
146         Encoder encoder;
147         assert(encoder.encode(handa) == `{"id":1000,"name":"shinobu","height":169.5}`);
148     }
149     { // opt
150         Encoder.Option opt;
151         opt.beautify = true;
152         opt.indentString = "  ";
153         assert(Encoder(opt).encode(handa) == `{
154   "id": 1000,
155   "name": "shinobu",
156   "height": 169.5
157 }
158 `);
159     }
160 }
161 
162 unittest
163 {
164     static struct JSONNameTest
165     {
166         @JSONName("body") string _body;
167     }
168 
169     JSONNameTest jnt = JSONNameTest("body!");
170 
171     {
172         Encoder encoder;
173         assert(encoder.encode(jnt) == `{"body":"body!"}`);
174     }
175 }
176 
177 private:
178 
179 @trusted
180 void yajlGenerate(T)(yajl_gen gen, auto ref T value)
181 {
182     import std.conv : to;
183     import std.typecons : isTuple;
184 
185     static if (isBoolean!T)
186     {
187         checkStatus(yajl_gen_bool(gen, value ? 1 : 0));
188     }
189     else static if (isIntegral!T)
190     {
191         checkStatus(yajl_gen_integer(gen, value));
192     }
193     else static if (isFloatingPoint!T)
194     {
195         checkStatus(yajl_gen_double(gen, value));
196     }
197     else static if (isSomeString!T)
198     {
199         checkStatus(yajl_gen_string(gen, cast(const(ubyte)*)value.ptr, value.length));
200     }
201     else static if (isArray!T)
202     {
203         if (value is null) {
204             checkStatus(yajl_gen_null(gen));
205         } else {
206             checkStatus(yajl_gen_array_open(gen));
207             foreach (ref v; value)
208                 yajlGenerate(gen, v);
209             checkStatus(yajl_gen_array_close(gen));
210         }
211     }
212     else static if (isAssociativeArray!T)
213     {
214         if (value is null) {
215             checkStatus(yajl_gen_null(gen));
216         } else {
217             checkStatus(yajl_gen_map_open(gen));
218             foreach (k, ref v; value) {
219                 yajlGenerate(gen, to!string(k));
220                 yajlGenerate(gen, v);
221             }
222             checkStatus(yajl_gen_map_close(gen));
223         }
224     }
225     else static if (isTuple!T)
226     {
227         checkStatus(yajl_gen_array_open(gen));
228         foreach (i, Type; T.Types)
229             yajlGenerate(gen, value[i]);
230         checkStatus(yajl_gen_array_close(gen));
231     }
232     else static if (is(T == struct) || is(T == class))
233     {
234         static if (is(T == class))
235         {
236             if (value is null) {
237                 checkStatus(yajl_gen_null(gen));
238                 return;
239             }
240         }
241 
242         checkStatus(yajl_gen_map_open(gen));
243         foreach (i, ref v; value.tupleof) {
244             static if (isNullable!(typeof(v)))
245             {
246                 if (!v.isNull) {
247                     yajlGenerate(gen, getFieldName!(T, i));
248                     yajlGenerate(gen, v.get);
249                 }
250             }
251             else
252             {
253                 yajlGenerate(gen, getFieldName!(T, i));
254                 yajlGenerate(gen, v);
255             }
256         }
257         checkStatus(yajl_gen_map_close(gen));
258     }
259 
260     return;
261 }
262 
263 // status check is needed?
264 @safe
265 void checkStatus(yajl_gen_status status) pure
266 {
267     if (status != yajl_gen_status.yajl_gen_status_ok)
268         throw new YajlException(formatStatus(status));
269 }
270 
271 @safe
272 string formatStatus(in yajl_gen_status status) pure
273 {
274     final switch (status) {
275 	case yajl_gen_status.yajl_gen_status_ok:
276         return null;
277 	case yajl_gen_status.yajl_gen_keys_must_be_strings:
278         return "A map key is generated, a function other than yajl_gen_string was called";
279 	case yajl_gen_status.yajl_max_depth_exceeded:
280         return "YAJL's maximum generation depth was exceeded";
281 	case yajl_gen_status.yajl_gen_in_error_state:
282         return "A generator function was called while in an error state";
283 	case yajl_gen_status.yajl_gen_generation_complete:
284         return "A complete JSON document has been generated";
285 	case yajl_gen_status.yajl_gen_invalid_number:
286         return "Invalid floating point value (infinity or NaN)";
287 	case yajl_gen_status.yajl_gen_no_buf:
288         return "There is no internal buffer to get from print callback";
289     case yajl_gen_status.yajl_gen_invalid_string:
290         return "Returned invalid utf8 string from yajl_gen_string";
291     }
292 }