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 }