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