1 module text.json.Encode;
2 
3 import meta.attributesOrNothing;
4 import meta.never;
5 import meta.SafeUnqual;
6 import std.json;
7 import std.traits;
8 import text.json.Json;
9 
10 /**
11  * Encodes an arbitrary type as a JSON string using introspection.
12  */
13 public string encode(T, alias transform = never)(const T value)
14 {
15     auto json = encodeJson!(T, transform)(value);
16 
17     return json.toJSON;
18 }
19 
20 /// ditto
21 public JSONValue encodeJson(T)(const T value)
22 {
23     return encodeJson!(T, transform)(value);
24 }
25 
26 public JSONValue encodeJson(T, alias transform, attributes...)(const T parameter)
27 in
28 {
29     static if (is(T == class))
30     {
31         assert(value !is null);
32     }
33 }
34 do
35 {
36     import boilerplate.util : formatNamed, optionallyRemoveTrailingUnderline, removeTrailingUnderline, udaIndex;
37     import std.algorithm : map;
38     import std.array : assocArray;
39     import std.format : format;
40     import std.meta : AliasSeq, anySatisfy, ApplyLeft;
41     import std.range : array;
42     import std.traits : fullyQualifiedName, isIterable, Unqual;
43     import std.typecons : Nullable, Tuple, tuple;
44 
45     static if (__traits(compiles, transform(parameter)))
46     {
47         auto transformedValue = transform(parameter);
48         static assert(!is(Unqual!(typeof(transformedValue)) == Unqual!T),
49             "transform must not return the same type as it takes!");
50 
51         return encodeJson!(typeof(transformedValue), transform)(transformedValue);
52     }
53     else
54     {
55         static assert(
56             !__traits(compiles, transform(Unqual!T.init)),
57             "transform() must take its parameter as const!");
58 
59         auto value = parameter;
60         alias Type = T;
61 
62         alias typeAttributes = attributesOrNothing!Type;
63 
64         static if (udaIndex!(Json.Encode, attributes) != -1)
65         {
66             alias encodeFunction = attributes[udaIndex!(Json.Encode, attributes)].EncodeFunction;
67             enum hasEncodeFunction = true;
68         }
69         else static if (udaIndex!(Json.Encode, typeAttributes) != -1)
70         {
71             alias encodeFunction = typeAttributes[udaIndex!(Json.Encode, typeAttributes)].EncodeFunction;
72             enum hasEncodeFunction = true;
73         }
74         else
75         {
76             enum hasEncodeFunction = false;
77         }
78 
79         static if (hasEncodeFunction)
80         {
81             static if (__traits(compiles, encodeFunction!(typeof(value), transform, attributes)))
82             {
83                 return encodeFunction!(typeof(value), transform, attributes)(value);
84             }
85             else
86             {
87                 return encodeFunction(value);
88             }
89         }
90         else static if (__traits(compiles, encodeValue(value)))
91         {
92             return encodeValue(value);
93         }
94         else static if (is(Type : V[string], V))
95         {
96             // TODO json encode of associative arrays with non-string keys
97             return JSONValue(value.byKeyValue.map!(pair => tuple!("key", "value")(
98                     pair.key,
99                     .encodeJson!(typeof(pair.value), transform, attributes)(pair.value)))
100                 .assocArray);
101         }
102         else static if (isIterable!Type)
103         {
104             return JSONValue(value.map!(a => .encodeJson!(typeof(a), transform, attributes)(a)).array);
105         }
106         else
107         {
108             JSONValue[string] members = null;
109 
110             static assert(
111                 __traits(hasMember, Type, "ConstructorInfo"),
112                 fullyQualifiedName!Type ~ " does not have a boilerplate constructor!");
113 
114             alias Info = Tuple!(string, "builderField", string, "constructorField");
115 
116             static foreach (string constructorField; Type.ConstructorInfo.fields)
117             {{
118                 enum builderField = optionallyRemoveTrailingUnderline!constructorField;
119 
120                 mixin(formatNamed!q{
121                     alias MemberType = SafeUnqual!(Type.ConstructorInfo.FieldInfo.%(constructorField).Type);
122 
123                     const MemberType memberValue = value.%(builderField);
124 
125                     static if (is(MemberType : Nullable!Arg, Arg))
126                     {
127                         bool includeMember = !memberValue.isNull;
128                         enum getMemberValue = "memberValue.get";
129                     }
130                     else
131                     {
132                         enum includeMember = true;
133                         enum getMemberValue = "memberValue";
134                     }
135 
136                     alias attributes = AliasSeq!(Type.ConstructorInfo.FieldInfo.%(constructorField).attributes);
137 
138                     if (includeMember)
139                     {
140                         static if (udaIndex!(Json, attributes) != -1)
141                         {
142                             enum name = attributes[udaIndex!(Json, attributes)].name;
143                         }
144                         else
145                         {
146                             enum name = constructorField.removeTrailingUnderline;
147                         }
148 
149                         auto finalMemberValue = mixin(getMemberValue);
150 
151                         enum sameField(string lhs, string rhs)
152                             = optionallyRemoveTrailingUnderline!lhs== optionallyRemoveTrailingUnderline!rhs;
153                         enum memberIsAliasedToThis = anySatisfy!(
154                             ApplyLeft!(sameField, constructorField),
155                             __traits(getAliasThis, T));
156 
157                         static if (memberIsAliasedToThis)
158                         {
159                             auto json = encodeJson!(typeof(finalMemberValue), transform, attributes)(finalMemberValue);
160 
161                             foreach (string key, newValue; json)
162                             {
163                                 // impossible as it would have caused compiletime errors on access
164                                 assert(key !in members,
165                                     format!"key collision: %s both in %s and member %s which is aliased to this"
166                                         (key, T.stringof, constructorField));
167 
168                                 members[key] = newValue;
169                             }
170                         }
171                         else
172                         {
173                             members[name] = encodeJson!(typeof(finalMemberValue), transform, attributes)
174                                 (finalMemberValue);
175                         }
176                     }
177                 }.values(Info(builderField, constructorField)));
178             }}
179 
180             return JSONValue(members);
181         }
182     }
183 }
184 
185 public JSONValue encodeJson(T : JSONValue, alias transform, attributes...)(const T parameter)
186 {
187     return parameter;
188 }
189 
190 private JSONValue encodeValue(T)(T value)
191 if (!is(T: Nullable!Arg, Arg))
192 {
193     import std.conv : to;
194     import text.xml.Convert : Convert;
195 
196     static if (is(T == enum))
197     {
198         return JSONValue(value.to!string);
199     }
200     else static if (
201         isBoolean!T || isIntegral!T || isFloatingPoint!T || isSomeString!T || is(T == typeof(null)))
202     {
203         return JSONValue(value);
204     }
205     else static if (__traits(compiles, Convert.toString(value)))
206     {
207         return JSONValue(Convert.toString(value));
208     }
209     else
210     {
211         static assert(false, "Cannot encode " ~ T.stringof ~ " as value");
212     }
213 }