1 /******************************************************************************* 2 3 Serialization-related utilities 4 5 When passing instances accross threads, LocalRest often cannot use the 6 provided instance itself, as it would require it to be either `shared`, 7 a value type, or `immutable`. 8 The term instance here is used to mean an instance of a type, 9 that is either passed as parameter, and used as a return value. 10 11 Instead, we support serializing the parameter to one of three types: 12 `immutable(void)[]`, `immutable(ubyte)[]`, or `string`. 13 The first two are regular serialized target, and `string` can be used 14 for JSON, XML, etc. 15 16 A serializer can be a `template` or an aggregate, or even a function 17 returning an aggregate. No limitation is put on the type itself, 18 only on what operations should be supported. 19 20 For a type `T` to be a valid serializer, it should support 21 the following actions: 22 - `T.serialize()` should compile when passed on parameter. 23 - This one parameter can be any type that is either a parameter or a 24 return value of `API`'s methods. This is not checked explicitly, 25 but will lead to compilation error in LocalRest if not followed. 26 - `T.serialize(param)` should return an one of `string`, 27 `immutable(void)[]`, `immutable(ubyte)[]` 28 - `T.deserialize!QT` is a template function that returns an 29 instance of type `QT` (`QT` is potentially qualified, e.g. `const`). 30 - `T.deserialize!!T()` should accept one runtime non-default 31 parameter, of the type returned by `T.serialize`. 32 33 The default implementation is the `VibeJSONSerializer`. 34 Other utilities in this module are used to facilitate development 35 and diagnostics. 36 37 Author: Mathias 'Geod24' Lang 38 License: MIT (See LICENSE.txt) 39 Copyright: Copyright (c) 2020 Mathias Lang. All rights reserved. 40 41 *******************************************************************************/ 42 43 module geod24.Serialization; 44 45 import std.traits; 46 47 /******************************************************************************* 48 49 Serialize arbitrary data to / from JSON 50 51 This is the default serializer used by LocalRest, when none is provided. 52 It is a template so that LocalRest does not import Vibe.d if not used. 53 54 *******************************************************************************/ 55 56 public template VibeJSONSerializer () 57 { 58 import vibe.data.json; 59 60 public string serialize (T) (auto ref T value) @safe 61 { 62 return serializeToJsonString(value); 63 } 64 65 public QT deserialize (QT) (string data) @safe 66 { 67 return deserializeJson!(QT)(data); 68 } 69 } 70 71 /// 72 unittest 73 { 74 alias S = VibeJSONSerializer!(); 75 static assert(!serializerInvalidReason!(S).length, serializerInvalidReason!(S)); 76 static assert(is(SerializedT!(S) == string)); 77 } 78 79 /// Utility function to check if a serializer conforms to the requirements 80 /// Returns: An error message, or `null` if the serializer is conformant 81 public string serializerInvalidReason (alias S) () 82 { 83 // Give codegen a break 84 if (!__ctfe) return null; 85 86 static if (!is(typeof(() { alias X = S.serialize; }))) 87 return "`" ~ S.stringof ~ "` is missing a `serialize` method"; 88 else static if (!is(typeof(() { alias X = S.deserialize; }))) 89 return "`" ~ S.stringof ~ "` is missing a `deserialize` method"; 90 91 /// We need a type to deserialize to test this function 92 /// While we should only test with return types / parameter types, 93 /// pretty much anything that can't (de)serialize an `int` is broken. 94 95 else static if (!is(typeof(() { S.serialize(0); }))) 96 return "`" ~ S.stringof ~ ".serialize` is not callable using argument `int`"; 97 else static if (!isCallable!(S.deserialize!int)) 98 return "`" ~ S.stringof ~ ".deserialize` is not callable"; 99 100 // If the template has an error we want to be informative 101 else static if (!is(typeof(S.serialize(0)))) 102 return "`" ~ S.stringof ~ ".serialize(0)` does not return any value, does it compile?"; 103 104 // All accepted types convert to `immutable(void)[]` 105 else static if (!is(typeof(S.serialize(0)) : immutable(void)[])) 106 return "`" ~ S.stringof ~ ".serialize` return value should be " 107 ~ "`string`, `immutable(ubyte)[]`, or `immutable(void)[]`, not: `" 108 ~ typeof(S.serialize(0)).stringof ~"`"; 109 110 else 111 { 112 // Actual return type used 113 alias RT = SerializedT!S; 114 115 static if (!is(typeof(S.deserialize!int(RT.init)))) 116 return "`" ~ S.stringof ~ ".deserialize!int` does not accept serialized type: `" 117 ~ RT.stringof ~ "`"; 118 else 119 return null; 120 } 121 } 122 123 /// 124 unittest 125 { 126 template Nothing () {} 127 static assert(serializerInvalidReason!(Nothing!()).length); 128 static assert(serializerInvalidReason!(Object).length); 129 // Older frontend do not support passing basic type as template alias parameters 130 //static assert(serializerInvalidReason!(int).length); 131 132 // Note: We don't test the error message. Invert the condition to see them. 133 134 // Valid 135 static struct S1 136 { 137 static immutable(void)[] serialize (T) (T v) { return null; } 138 static QT deserialize (QT) (immutable(void)[] v) { return QT.init; } 139 } 140 static assert(!serializerInvalidReason!(S1).length, serializerInvalidReason!(S1)); 141 142 // Valid: Type conversions are performed (ubyte[] => void[]) 143 static struct S2 144 { 145 static immutable(ubyte)[] serialize (T) (T v) { return null; } 146 static QT deserialize (QT) (immutable(void)[] v) { return QT.init; } 147 } 148 static assert(!serializerInvalidReason!(S2).length, serializerInvalidReason!(S2)); 149 150 // Invalid: `void[]` =/> `ubyte[]` 151 static struct S3 152 { 153 static immutable(void)[] serialize (T) (T v) { return null; } 154 static QT deserialize (QT) (immutable(ubyte)[] v) { return QT.init; } 155 } 156 static assert(serializerInvalidReason!(S3).length, serializerInvalidReason!(S3)); 157 } 158 159 /******************************************************************************* 160 161 Returns: 162 The serialized type for a given serializer. 163 `immutable(string)` will match `string`, `immutable(ubyte[])` will match 164 `immutable(ubyte)[]`, and everything that can convert to 165 `immutable(void)[]` will match it. 166 167 *******************************************************************************/ 168 169 public template SerializedT (alias S) 170 { 171 static if (is(Unqual!(typeof(S.serialize(0))) == string)) 172 public alias SerializedT = string; 173 else static if (is(Unqual!(typeof(S.serialize(0))) == immutable(ubyte)[])) 174 public alias SerializedT = immutable(ubyte)[]; 175 // Catch-all convertion 176 else static if (is(typeof(S.serialize(0)) : immutable(void)[])) 177 public alias SerializedT = immutable(void)[]; 178 else static if (is(typeof(S.serialize(0)))) 179 static assert(0, "`" ~ typeof(S.serialize(0)).stringof ~ 180 "` is not a valid type for a serializer"); 181 else 182 static assert(0, "`" ~ S.stringof ~ ".serialize` is invalid or does not compile"); 183 } 184 185 /// 186 unittest 187 { 188 static struct S (RT) 189 { 190 public static RT serialize (T) (auto ref T value, uint opts = 42) @safe; 191 // Ignored because it doesn't take 1 non-default parameter 192 public static RT serialize (T) (T a, T b) @safe; 193 } 194 195 static assert(is(SerializedT!(S!string) == string)); 196 static assert(is(SerializedT!(S!(immutable(string))) == string)); 197 static assert(is(SerializedT!(S!(const(string))) == string)); 198 199 static assert(is(SerializedT!(S!(immutable(ubyte[]))) == immutable(ubyte)[])); 200 static assert(is(SerializedT!(S!(immutable(ubyte)[])) == immutable(ubyte)[])); 201 static assert(is(SerializedT!(S!(const(immutable(ubyte)[]))) == immutable(ubyte)[])); 202 203 static assert(is(SerializedT!(S!(immutable(void[]))) == immutable(void)[])); 204 static assert(is(SerializedT!(S!(immutable(void)[])) == immutable(void)[])); 205 static assert(is(SerializedT!(S!(const(immutable(void)[]))) == immutable(void)[])); 206 207 static struct Struct { uint v; } 208 static assert(is(SerializedT!(S!(immutable(Struct)[])) == immutable(void)[])); 209 210 static struct Struct2 { void[] data; alias data this; } 211 static assert(is(SerializedT!(S!(immutable(Struct2))) == immutable(void)[])); 212 213 static assert(!is(typeof(SerializedT!(S!(ubyte[]))))); 214 static assert(!is(typeof(SerializedT!(S!(void[]))))); 215 static assert(!is(typeof(SerializedT!(S!(char[]))))); 216 } 217 218 219 /// Type-erasing wrapper to handle serialized data 220 public union SerializedData 221 { 222 /// 223 immutable(void)[] void_; 224 /// 225 immutable(ubyte)[] ubyte_; 226 /// 227 immutable(char)[] string_; 228 229 /// 230 this (typeof(void_) arg) pure nothrow @nogc @trusted 231 { 232 // Note: Arrays have the same memory layout, so we don't care about 233 // the actual type here. 234 this.void_ = arg; 235 } 236 237 /// Helper to wrap `SerializedT` 238 public auto getS (alias Serializer) () const pure nothrow @nogc @trusted 239 { 240 static assert (!serializerInvalidReason!(Serializer).length, 241 serializerInvalidReason!Serializer); 242 243 return this.get!(SerializedT!Serializer); 244 } 245 246 /// Return an exact type (the type returned is always `immutable(ET)[]`) 247 public auto get (T) () const pure nothrow @nogc @trusted 248 { 249 static if (is(immutable(T) == immutable(string))) 250 return this.string_; 251 else static if (is(immutable(T) == immutable(ubyte[]))) 252 return this.ubyte_; 253 else 254 return this.void_; 255 } 256 } 257 258 static assert (SerializedData.sizeof == size_t.sizeof * 2, 259 "Size mismatch for `SerializedData`");