1 module toml_foolery.encode.tomlify;
2 
3 import toml_foolery.attributes;
4 import toml_foolery.encode;
5 import toml_foolery.encode.types.array;
6 import toml_foolery.encode.types.boolean;
7 import toml_foolery.encode.types.datetime;
8 import toml_foolery.encode.types.enum_;
9 import toml_foolery.encode.types.floating_point;
10 import toml_foolery.encode.types.integer;
11 import toml_foolery.encode.types..string;
12 import toml_foolery.encode.types.table;
13 import toml_foolery.encode.util;
14 
15 import std.algorithm : map, any;
16 import std.array : join;
17 import std.range.primitives : ElementType;
18 import std.traits;
19 
20 version(unittest) import exceeds_expectations;
21 
22 /**
23  *  Encodes a struct of type T into a TOML string.
24  *
25  *  Each field in the struct will be an entry in the resulting TOML string. If a
26  *  field is itself a struct, then it will show up as a subtable in the TOML.
27  *
28  *  The struct may not contain any fields that are classes or pointers, because
29  *  circular references are currently not checked for. This restriction may be
30  *  lifted in the future.
31  *
32  *  Params:
33  *      object = The object to be converted into a TOML file.
34  *      T =      The type of the given object.
35  *
36  *  Returns:
37  *      A string containing TOML data representing the given object.
38  */
39 public string tomlify(T)(T object)
40 if(is(T == struct))
41 {
42     Appender!string buffer;
43 
44     static assert (
45         !hasDuplicateKeys!T,
46         "Struct " ~ T.stringof ~ "contains some duplicate key names."
47     );
48 
49     enum auto fieldNames = FieldNameTuple!T;
50     static foreach (fieldName; fieldNames)
51     {
52         tomlifyField(dFieldToTomlKey!(T, fieldName), __traits(getMember, object, fieldName), buffer, []);
53     }
54 
55     return buffer.data;
56 }
57 
58 /// A simple example of `tomlify` with an array of tables.
59 unittest
60 {
61     struct Forecast
62     {
63         struct Day
64         {
65             int min;
66             int max;
67         }
68 
69         struct Location
70         {
71             string name;
72             real lat;
73             real lon;
74         }
75 
76         string temperatureUnit;
77         Location location;
78         Day[] days;
79     }
80 
81     Forecast data = Forecast(
82         "℃",
83         Forecast.Location("Pedra Amarela", 38.76417, -9.436667),
84         [
85             Forecast.Day(18, 23),
86             Forecast.Day(15, 21)
87         ]
88     );
89 
90     string toml = tomlify(data);
91 
92     expectToEqualNoBlanks(toml, `
93 temperatureUnit = "℃"
94 
95 [location]
96 name = "Pedra Amarela"
97 lat = 38.76417
98 lon = -9.4366670
99 
100 [[days]]
101 min = 18
102 max = 23
103 
104 [[days]]
105 min = 15
106 max = 21
107 `
108     );
109 }
110 
111 /// A struct containing classes cannot be encoded.
112 unittest
113 {
114     class C {}
115 
116     struct S
117     {
118         C c;
119     }
120 
121     S s;
122     static assert(
123         !__traits(compiles, tomlify(s)),
124         "Should not be able to compile when struct contains a class field."
125     );
126 }
127 
128 /// A struct with a pointer to anything cannot be encoded.
129 unittest
130 {
131     struct S
132     {
133         int* i;
134     }
135 
136     S s;
137     static assert(
138         !__traits(compiles, tomlify(s)),
139         "Should not be able to compile when struct contains a pointer field."
140     );
141 }
142 
143 
144 /// Thrown by `tomlify` if given data cannot be encoded in a way that adheres to
145 /// the TOML spec.
146 public class TomlEncodingException : Exception
147 {
148     /// See `Exception.this()`
149     package this(string msg, string file = __FILE__, size_t line = __LINE__, Throwable nextInChain = null)
150     @nogc @safe pure nothrow
151     {
152         super(msg, file, line, nextInChain);
153     }
154 
155     /// ditto
156     package this(string msg, Throwable nextInChain, string file = __FILE__, size_t line = __LINE__)
157     @nogc @safe pure nothrow
158     {
159         super(msg, file, line, nextInChain);
160     }
161 }
162 
163 package void tomlifyField(K, V)(K key, V value, ref Appender!string buffer, immutable string[] parentTables)
164 if (makesTomlKey!K)
165 {
166     static if (makesTomlTable!V)
167     {
168         buffer.put('\n');
169         buffer.put('[');
170         string fullTableName = (parentTables ~ key).map!((e) => tomlifyKey(e)).join(".");
171         buffer.put(fullTableName);
172         buffer.put("]\n");
173         tomlifyValue(value, buffer, parentTables ~ key);
174     }
175     else static if (
176         isArray!V &&
177         is(ElementType!V == struct)
178     )
179     {
180         foreach (ElementType!V entry; value)
181         {
182             buffer.put('\n');
183             buffer.put("[[");
184             string fullTableName = (parentTables ~ key).map!((e) => tomlifyKey(e)).join(".");
185             buffer.put(fullTableName);
186             buffer.put("]]\n");
187             tomlifyValue(entry, buffer, parentTables ~ key);
188         }
189     }
190     else
191     {
192         static assert(
193             !is(V == class),
194             `Encoding classes into TOML is not yet supported.`
195         );
196 
197         static assert(
198             !isPointer!V,
199             `Encoding pointers into TOML is not yet supported.`
200         );
201 
202         buffer.put(tomlifyKey(key));
203         buffer.put(" = ");
204         tomlifyValue(value, buffer, parentTables);
205         buffer.put('\n');
206     }
207 }
208 
209 package(toml_foolery.encode) enum bool makesTomlKey(T) = (
210     isSomeString!T
211 );
212 
213 private string tomlifyKey(T)(T key)
214 if (makesTomlKey!T)
215 {
216     if (key == "" || key.any!((dchar e)
217     {
218         import std.ascii : isAlphaNum;
219         return !(isAlphaNum(e) || e == dchar('-') || e == dchar('_'));
220     }))
221     {
222         return '"' ~ key ~ '"';
223     }
224     else
225     {
226         return key;
227     }
228 }
229 
230 @("Ensure keys are legal")
231 unittest
232 {
233     expect(tomlifyKey(`hello_woRLD---`)).toEqual(`hello_woRLD---`);
234     expect(tomlifyKey(`hello world`)).toEqual(`"hello world"`);
235     expect(tomlifyKey(`012`)).toEqual(`012`);
236     expect(tomlifyKey(`kiː`)).toEqual(`"kiː"`);
237     expect(tomlifyKey(``)).toEqual(`""`);
238 }
239 
240 /// Encodes any value of type T.
241 package void tomlifyValue(T)(const T value, ref Appender!string buffer, immutable string[] parentTables)
242 {
243     tomlifyValueImpl(value, buffer, parentTables);
244 }
245 
246 package bool hasDuplicateKeys(S)()
247 {
248     bool[string] keys;
249 
250     static foreach (field; FieldNameTuple!S)
251     {
252         static if (hasUDA!(mixin("S." ~ field), TomlName))
253         {
254             if (getUDAs!(mixin("S." ~ field), TomlName)[0].tomlName in keys) return true;
255             keys[getUDAs!(mixin("S." ~ field), TomlName)[0].tomlName] = true;
256         }
257         else
258         {
259             if (field in keys) return true;
260             keys[field] = true;
261         }
262     }
263     return false;
264 }
265 
266 version(unittest)
267 {
268     /// Helper for testing.
269     package string _tomlifyValue(T)(const T value)
270     {
271         Appender!string buff;
272         tomlifyValue(value, buff, []);
273         return buff.data;
274     }
275 }