1 /++ 2 Functions used to generate strings of statements describing the differences 3 (or delta) between two instances of a struct or class of the same type. 4 They can be either assignment statements or assert statements. 5 6 Example: 7 --- 8 struct Foo 9 { 10 string s; 11 int i; 12 bool b; 13 } 14 15 Foo altered; 16 17 altered.s = "some string"; 18 altered.i = 42; 19 altered.b = true; 20 21 Appender!(char[]) sink; 22 23 // Fill with delta between `Foo.init` and modified `altered` 24 sink.formatDeltaInto!(No.asserts)(Foo.init, altered); 25 26 assert(sink.data == 27 `s = "some string"; 28 i = 42; 29 b = true; 30 `); 31 sink.clear(); 32 33 // Do the same but prepend the name "altered" to the member names 34 sink.formatDeltaInto!(No.asserts)(Foo.init, altered, 0, "altered"); 35 36 assert(sink.data == 37 `altered.s = "some string"; 38 altered.i = 42; 39 altered.b = true; 40 `); 41 sink.clear(); 42 43 // Generate assert statements instead, for easy copy/pasting into unittest blocks 44 sink.formatDeltaInto!(Yes.asserts)(Foo.init, altered, 0, "altered"); 45 46 assert(sink.data == 47 `assert((altered.s == "some string"), altered.s); 48 assert((altered.i == 42), altered.i.to!string); 49 assert(altered.b, altered.b.to!string); 50 `); 51 --- 52 +/ 53 module lu.deltastrings; 54 55 private: 56 57 import std.range.primitives : isOutputRange; 58 import std.traits : isAggregateType; 59 import std.typecons : Flag, No, Yes; 60 61 public: 62 63 import lu.uda : Hidden; 64 65 //@safe: 66 67 68 // formatDeltaInto 69 /++ 70 Constructs statement lines for each changed field (or the delta) between two 71 instances of a struct and stores them into a passed output sink. 72 73 Example: 74 --- 75 struct Foo 76 { 77 string s; 78 int i; 79 bool b; 80 } 81 82 Foo altered; 83 84 altered.s = "some string"; 85 altered.i = 42; 86 altered.b = true; 87 88 Appender!(char[]) sink; 89 sink.formatDeltaInto!(No.asserts)(Foo.init, altered); 90 --- 91 92 Params: 93 asserts = Whether or not to build assert statements or assignment statements. 94 sink = Output buffer to write to. 95 before = Original struct object. 96 after = Changed struct object. 97 indents = The number of tabs to indent the lines with. 98 submember = The string name of a recursing symbol, if applicable. 99 +/ 100 void formatDeltaInto(Flag!"asserts" asserts = No.asserts, Sink, QualThing) 101 (auto ref Sink sink, 102 auto ref QualThing before, 103 auto ref QualThing after, 104 const uint indents = 0, 105 const string submember = string.init) 106 if (isOutputRange!(Sink, char[]) && isAggregateType!QualThing) 107 { 108 immutable prefix = submember.length ? (submember ~ '.') : string.init; 109 110 foreach (immutable i, ref member; after.tupleof) 111 { 112 import lu.uda : Hidden; 113 import std.traits : Unqual, hasUDA, isAggregateType, isArray, 114 isSomeFunction, isSomeString, isType; 115 116 alias T = Unqual!(typeof(member)); 117 enum memberstring = __traits(identifier, before.tupleof[i]); 118 119 static if (hasUDA!(after.tupleof[i], Hidden)) 120 { 121 // Member is annotated as Hidden; skip 122 continue; 123 } 124 else static if (isAggregateType!T) 125 { 126 // Recurse 127 sink.formatDeltaInto!asserts(before.tupleof[i], member, indents, prefix ~ memberstring); 128 } 129 else static if (!isType!member && !isSomeFunction!member && !__traits(isTemplate, member)) 130 { 131 if (after.tupleof[i] != before.tupleof[i]) 132 { 133 static if (isArray!T && !isSomeString!T) 134 { 135 import std.range : ElementEncodingType; 136 137 // TODO: Rewrite this to recurse 138 alias E = ElementEncodingType!T; 139 140 static if (isSomeString!E) 141 { 142 static if (asserts) 143 { 144 enum pattern = "%sassert((%s%s[%d] == \"%s\"), %2$s%3$s[%4$d]);\n"; 145 } 146 else 147 { 148 enum pattern = "%s%s%s[%d] = \"%s\";\n"; 149 } 150 } 151 else static if (is(E == char)) 152 { 153 static if (asserts) 154 { 155 enum pattern = "%sassert((%s%s[%d] == '%s'), %2$s%3$s[%4$d].to!string);\n"; 156 } 157 else 158 { 159 enum pattern = "%s%s%s[%d] = '%s';\n"; 160 } 161 } 162 else 163 { 164 static if (asserts) 165 { 166 enum pattern = "%sassert((%s%s[%d] == %s), %2$s%3$s[%4$d].to!string);\n"; 167 } 168 else 169 { 170 enum pattern = "%s%s%s[%d] = %s;\n"; 171 } 172 } 173 } 174 else static if (isSomeString!T) 175 { 176 static if (asserts) 177 { 178 enum pattern = "%sassert((%s%s == \"%s\"), %2$s%3$s);\n"; 179 } 180 else 181 { 182 enum pattern = "%s%s%s = \"%s\";\n"; 183 } 184 } 185 else static if (is(T == char)) 186 { 187 static if (asserts) 188 { 189 enum pattern = "%sassert((%s%s == '%s'), %2$s%3$s.to!string);\n"; 190 } 191 else 192 { 193 enum pattern = "%s%s%s = '%s';\n"; 194 } 195 } 196 else static if (is(T == enum)) 197 { 198 enum typename = Unqual!QualThing.stringof ~ '.' ~ T.stringof; 199 200 static if (asserts) 201 { 202 immutable pattern = "%sassert((%s%s == " ~ typename ~ ".%s), " ~ 203 "Enum!(" ~ typename ~ ").toString(%2$s%3$s));\n"; 204 } 205 else 206 { 207 immutable pattern = "%s%s%s = " ~ typename ~ ".%s;\n"; 208 } 209 } 210 else static if (is(T == bool)) 211 { 212 static if (asserts) 213 { 214 immutable pattern = member ? 215 "%sassert(%s%s);\n" : 216 "%sassert(!%s%s);\n"; 217 } 218 else 219 { 220 enum pattern = "%s%s%s = %s;\n"; 221 } 222 } 223 else 224 { 225 static if (asserts) 226 { 227 enum pattern = "%sassert((%s%s == %s), %2$s%3$s.to!string);\n"; 228 } 229 else 230 { 231 enum pattern = "%s%s%s = %s;\n"; 232 } 233 } 234 235 import std.format : formattedWrite; 236 import std.range : repeat; 237 import std.string : join; 238 239 immutable indentation = " ".repeat(indents).join; 240 241 static if (isSomeString!T) 242 { 243 import std.array : replace; 244 245 immutable escaped = member 246 .replace('\\', `\\`) 247 .replace('"', `\"`); 248 249 sink.formattedWrite(pattern, indentation, prefix, memberstring, escaped); 250 } 251 else static if (isArray!T) 252 { 253 foreach (n, val; member) 254 { 255 if (before.tupleof[i][n] == after.tupleof[i][n]) continue; 256 sink.formattedWrite(pattern, indentation, prefix, memberstring, n, member[n]); 257 } 258 } 259 else 260 { 261 sink.formattedWrite(pattern, indentation, prefix, memberstring, member); 262 } 263 } 264 } 265 else 266 { 267 static assert(0, "Cannot produce deltastrings for type `%s`" 268 .format(Unqual!QualThing.stringof)); 269 } 270 } 271 } 272 273 /// 274 unittest 275 { 276 import lu.uda : Hidden; 277 import std.array : Appender; 278 279 Appender!(char[]) sink; 280 sink.reserve(1024); 281 282 struct Server 283 { 284 string address; 285 ushort port; 286 bool connected; 287 } 288 289 struct Connection 290 { 291 enum State 292 { 293 unset, 294 disconnected, 295 connected, 296 } 297 298 State state; 299 string nickname; 300 @Hidden string user; 301 @Hidden string password; 302 Server server; 303 } 304 305 Connection conn; 306 307 with (conn) 308 { 309 state = Connection.State.connected; 310 nickname = "NICKNAME"; 311 user = "USER"; 312 password = "hunter2"; 313 server.address = "address.tld"; 314 server.port = 1337; 315 } 316 317 sink.formatDeltaInto!(No.asserts)(Connection.init, conn, 0, "conn"); 318 319 assert(sink.data == 320 `conn.state = Connection.State.connected; 321 conn.nickname = "NICKNAME"; 322 conn.server.address = "address.tld"; 323 conn.server.port = 1337; 324 `, '\n' ~ sink.data); 325 326 sink = typeof(sink).init; 327 328 sink.formatDeltaInto!(Yes.asserts)(Connection.init, conn, 0, "conn"); 329 330 assert(sink.data == 331 `assert((conn.state == Connection.State.connected), Enum!(Connection.State).toString(conn.state)); 332 assert((conn.nickname == "NICKNAME"), conn.nickname); 333 assert((conn.server.address == "address.tld"), conn.server.address); 334 assert((conn.server.port == 1337), conn.server.port.to!string); 335 `, '\n' ~ sink.data); 336 337 struct Foo 338 { 339 string s; 340 int i; 341 bool b; 342 char c; 343 } 344 345 Foo f1; 346 f1.s = "string"; 347 f1.i = 42; 348 f1.b = true; 349 f1.c = '$'; 350 351 Foo f2 = f1; 352 f2.s = "yarn"; 353 f2.b = false; 354 f2.c = '#'; 355 356 sink = typeof(sink).init; 357 358 sink.formatDeltaInto!(No.asserts)(f1, f2); 359 assert(sink.data == 360 `s = "yarn"; 361 b = false; 362 c = '#'; 363 `, '\n' ~ sink.data); 364 365 sink = typeof(sink).init; 366 367 sink.formatDeltaInto!(Yes.asserts)(f1, f2); 368 assert(sink.data == 369 `assert((s == "yarn"), s); 370 assert(!b); 371 assert((c == '#'), c.to!string); 372 `, '\n' ~ sink.data); 373 374 sink = typeof(sink).init; 375 376 { 377 struct S 378 { 379 int i; 380 } 381 382 class C 383 { 384 string s; 385 bool b; 386 S child; 387 } 388 389 C c1 = new C; 390 C c2 = new C; 391 392 c2.s = "harbl"; 393 c2.b = true; 394 c2.child.i = 42; 395 396 sink.formatDeltaInto!(No.asserts)(c1, c2); 397 assert(sink.data == 398 `s = "harbl"; 399 b = true; 400 child.i = 42; 401 `, '\n' ~ sink.data); 402 403 sink = typeof(sink).init; 404 405 sink.formatDeltaInto!(Yes.asserts)(c1, c2); 406 assert(sink.data == 407 `assert((s == "harbl"), s); 408 assert(b); 409 assert((child.i == 42), child.i.to!string); 410 `, '\n' ~ sink.data); 411 } 412 { 413 struct Blah 414 { 415 int[5] arr; 416 string[3] sarr; 417 char[2] carr; 418 } 419 420 Blah b1; 421 Blah b2; 422 b2.arr = [ 1, 0, 3, 0, 5 ]; 423 b2.sarr = [ "hello", string.init, "world" ]; 424 b2.carr = [ 'a', char.init ]; 425 426 sink = typeof(sink).init; 427 428 sink.formatDeltaInto(b1, b2); 429 assert(sink.data == 430 `arr[0] = 1; 431 arr[2] = 3; 432 arr[4] = 5; 433 sarr[0] = "hello"; 434 sarr[2] = "world"; 435 carr[0] = 'a'; 436 `); 437 438 sink = typeof(sink).init; 439 440 sink.formatDeltaInto!(Yes.asserts)(b1, b2); 441 assert(sink.data == 442 `assert((arr[0] == 1), arr[0].to!string); 443 assert((arr[2] == 3), arr[2].to!string); 444 assert((arr[4] == 5), arr[4].to!string); 445 assert((sarr[0] == "hello"), sarr[0]); 446 assert((sarr[2] == "world"), sarr[2]); 447 assert((carr[0] == 'a'), carr[0].to!string); 448 `); 449 } 450 }