1 /// 2 module dpq.serialisers.composite; 3 4 import dpq.serialisation; 5 import dpq.meta; 6 import dpq.attributes; 7 import dpq.exception; 8 import dpq.value; 9 import dpq.connection; 10 11 import std..string : format, join; 12 import std.typecons : Nullable, Typedef; 13 import std.traits; 14 import std.array : Appender; 15 import std.bitmanip; 16 import std.conv : to; 17 import std.datetime : SysTime; 18 19 import libpq.libpq; 20 /** 21 The default serialiser for any composite type (structs and classes) 22 23 For custom types, the data representation is the following 24 25 First 4 bytes are an int representing the number of members 26 After that, the members are listed in the following way: 27 - OID 28 - length 29 - value 30 31 Example: (bytes, decimal) 32 [0 0 0 2 , 0 0 0 23 , 0 0 0 4 , 0 0 0 1 , 0 0 0 23 , 0 0 0 4 , 0 0 0 2] 33 will represent a struct with two members, both OID 23, length 4, with values 1 and 2 34 */ 35 struct CompositeTypeSerialiser 36 { 37 /** 38 Only accepts structs and classes, will fail on Nullable or Typedef types 39 which should be taken care of by toBytes function. 40 */ 41 static bool isSupportedType(T)() 42 { 43 return 44 is(T == class) || is(T == struct) && 45 !is(T == SysTime) && 46 !isInstanceOf!(Typedef, T); 47 } 48 49 static void enforceSupportedType(T)() 50 { 51 static assert ( 52 isSupportedType!T, 53 "'%s' is not supported by CompositeTypeSerialiser".format(T.stringof)); 54 } 55 56 static Nullable!(ubyte[]) serialise(T)(T val) 57 { 58 enforceSupportedType!T; 59 60 alias RT = Nullable!(ubyte[]); 61 62 if (isAnyNull(val)) 63 return RT.init; 64 65 alias members = serialisableMembers!T; 66 ubyte[] data; 67 68 // The number of members of this type 69 data ~= nativeToBigEndian(cast(int) members.length); 70 71 foreach (mName; members) 72 { 73 auto member = __traits(getMember, val, mName); 74 // The member's actual type without any qualifiers and such 75 alias MT = RealType!(typeof(member)); 76 77 // Element's Oid 78 data ~= nativeToBigEndian(cast(int) SerialiserFor!MT.oidForType!MT); 79 80 auto bytes = toBytes(member); 81 82 // Null values have length of -1 83 if (bytes.isNull) 84 data ~= nativeToBigEndian(cast(int) -1); 85 else 86 { 87 // The element length and data itself 88 data ~= nativeToBigEndian(bytes.length.to!int); 89 data ~= bytes; 90 } 91 } 92 93 return RT(data); 94 } 95 96 static T deserialise(T)(const (ubyte)[] bytes) 97 { 98 enforceSupportedType!T; 99 100 alias members = serialisableMembers!T; 101 102 int length = bytes.read!int; 103 104 if (length != members.length) 105 throw new DPQException("Length for %s (%d) does not actual match number of members (%s)".format( 106 T.stringof, 107 length, 108 members.length 109 )); 110 111 T result; 112 foreach (mName; members) 113 { 114 auto member = __traits(getMember, result, mName); 115 alias OT = typeof(member); 116 alias MT = RealType!OT; 117 118 Oid oid = cast(Oid) bytes.read!int; 119 auto mLen = bytes.read!int; 120 121 // When a null value is encontered, leave the member to its init value 122 if (mLen == -1) 123 continue; 124 125 // Read the value 126 __traits(getMember, result, mName) = cast(OT) fromBytes!MT(bytes[0 .. mLen], mLen); 127 128 // "Consume" the bytes that were just read 129 bytes = bytes[mLen .. $]; 130 } 131 132 return result; 133 } 134 135 static void ensureExistence(T)(Connection conn) 136 { 137 alias members = serialisableMembers!T; 138 139 string typeName = SerialiserFor!T.nameForType!T; 140 if ((typeName in _customOids) != null) 141 return; 142 143 string escTypeName = conn.escapeIdentifier(typeName); 144 145 string[] columns; 146 147 foreach (mName; members) 148 { 149 enum member = "T." ~ mName; 150 151 alias MType = RealType!(typeof(mixin(member))); 152 alias serialiser = SerialiserFor!MType; 153 serialiser.ensureExistence!MType(conn); 154 155 string attrName = attributeName!(mixin(member)); 156 string escAttrName = conn.escapeIdentifier(attrName); 157 158 static if (hasUDA!(mixin(member), PGTypeAttribute)) 159 string attrType = getUDAs!(mixin(member), PGTypeAttribute)[0].type; 160 else 161 string attrType = serialiser.nameForType!MType; 162 163 columns ~= escAttrName ~ " " ~ attrType; 164 } 165 166 try 167 { 168 conn.exec("CREATE TYPE %s AS (%s)".format(escTypeName, columns.join(", "))); 169 } catch (DPQException e) {} // Horrible, but just means the type already exists 170 171 conn.addOidsFor(typeName); 172 } 173 174 static string nameForType(T)() 175 { 176 enforceSupportedType!T; 177 178 return relationName!(RealType!T); 179 } 180 181 private static Oid[string] _customOids; 182 static Oid oidForType(T)() 183 { 184 enforceSupportedType!T; 185 186 auto oid = nameForType!T in _customOids; 187 assert( 188 oid != null, 189 "Oid for type %s not found. Did you run ensureSchema?".format(T.stringof)); 190 191 return *oid; 192 } 193 194 static void addCustomOid(string typeName, Oid oid) 195 { 196 _customOids[typeName] = oid; 197 } 198 } 199 200 // Very basic tests 201 unittest 202 { 203 import std.stdio; 204 205 writeln(" * CompositeTypeSerialiser"); 206 207 struct Test2 208 { 209 int c = 3; 210 } 211 212 struct Test 213 { 214 int a = 1; 215 int b = 2; 216 217 // test nullable too 218 Nullable!Test2 ntest2; 219 Test2 test2; 220 } 221 222 // An OID must exist for types being serialised 223 CompositeTypeSerialiser.addCustomOid("test2", 999999); 224 225 Test t = Test(1, 2); 226 auto serialised = CompositeTypeSerialiser.serialise(t); 227 auto deserialised = CompositeTypeSerialiser.deserialise!Test(serialised); 228 229 // Why is this throwing AssertError??? 230 //assert(t == deserialised); 231 232 // The manual approach, I guess 233 assert(deserialised.a == t.a); 234 assert(deserialised.b == t.b); 235 assert(deserialised.test2.c == t.test2.c); 236 assert(deserialised.ntest2.isNull == t.ntest2.isNull); 237 }