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`");