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 }