1 /** 
2  * JSON-based provider
3  *
4  * You should use this when
5  * you want to fill up your
6  * config with values stored
7  * in a string containing
8  * JSON-encoded data
9  *
10  * Authors: Tristan Brice Velloza Kildaire (deavmi)
11  */
12 module hummus.providers.json;
13 
14 import hummus.provider : Provider;
15 import std.json : JSONValue, JSONType;
16 import niknaks.json : traverseTo;
17 
18 version(unittest)
19 {
20     import gogga.mixins;
21 }
22 
23 /**
24  * A provider which will look for
25  * JSON key-value pairs based on
26  * matching them to the names of the
27  * fields in the provided struct.
28  *
29  * Struct fields which are of
30  * a struct-type themselves are
31  * supported and are filled 
32  * whenever json such as `x.y`
33  * is encountered. This means `x`
34  * is some field in the "outer"
35  * struct. Then because we have
36  * `x.y`, `x` MUST be of a struct
37  * type. Then we access the field
38  * named `y` in this "inner" struct
39  */
40 public class JSONProvider : Provider
41 {
42     import std.json : parseJSON, JSONException;
43 
44     private JSONValue _j;
45 
46     /** 
47      * Constructs a new JSON provider
48      * with the given input JSON to
49      * parse
50      *
51      * Params:
52      *   json = the input JSON
53      * Throws:
54      *   JSONException when parsing
55      * fails
56      */
57     this(string json)
58     {
59         // todo: handle exceptions in non-library specific way
60         // OR require they parse in the JSONValue - that is
61         // library-dependent tho
62         this._j = parseJSON(json);
63     }
64 
65     /**
66      * Implementation
67      */
68     protected bool provideImpl(string n, ref string v)
69     {
70         // todo: check return value for nullity
71         JSONValue* f_node = traverseTo(n, &this._j);
72 
73         // todo: value conversion here
74         if(f_node is null)
75         {
76             return false;
77         }
78 
79         string s_out;
80         if(jsonNormal(f_node, s_out))
81         {
82             version(unittest)
83                 DEBUG("found JSON node toString(): ", s_out);
84 
85             v = s_out;
86             return true;
87         }
88         else
89         {
90             return false;
91         }
92     }
93 }
94 
95 private bool jsonNormal(JSONValue* i, ref string o)
96 {
97     auto t = i.type();
98     if(t == JSONType..string)
99     {
100         o = i.str();
101         return true;
102     }
103     else if(t == JSONType.ARRAY)
104     {
105         version(unittest)
106             DEBUG("'", i, "' is an array type, these are unsupported");
107         
108         return false;
109     }
110     // todo: disallow array types and object types
111     else
112     {
113         o = i.toString();
114         return true;
115     }
116 }
117 
118 private version(unittest)
119 {
120     import hummus.cfg : fieldsOf;
121     import std.stdio : writeln;
122 }
123 
124 unittest
125 {
126     struct Inner
127     {
128         int prop;
129         int k;
130     }
131 
132     struct Basic
133     {
134         string name;
135         ulong age;
136         Inner x;
137         string bad;
138     }
139 
140     auto cfg = Basic();
141     writeln("Before provisioning: ", cfg);
142 
143     // input json
144     string json = `
145     {
146         "name": "Tristan Brice Velloza Kildaire",
147         "age": 25,
148         "x": {
149             "prop": 2
150         },
151         "bad": ["", 2]
152     }
153     `;
154 
155     // create a new JSON provider with the
156     // input JSON
157     fieldsOf(cfg, new JSONProvider(json));
158 
159     assert(cfg.name == "Tristan Brice Velloza Kildaire");
160     assert(cfg.age == 25);
161     assert(cfg.x.prop == 2);
162 
163     // it is not present (in the JSON) hence it should
164     // never be set in our struct
165     assert(cfg.x.k == cfg.x.k.init);
166     assert(cfg.bad.length == 0);
167 }