Typed structs for Loro CRDTs
rust crdt

initial commit

+3297
+1489
Cargo.lock
··· 1 + # This file is automatically @generated by Cargo. 2 + # It is not intended for manual editing. 3 + version = 4 4 + 5 + [[package]] 6 + name = "ahash" 7 + version = "0.8.12" 8 + source = "registry+https://github.com/rust-lang/crates.io-index" 9 + checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" 10 + dependencies = [ 11 + "cfg-if", 12 + "getrandom 0.3.4", 13 + "once_cell", 14 + "version_check", 15 + "zerocopy", 16 + ] 17 + 18 + [[package]] 19 + name = "aho-corasick" 20 + version = "1.1.4" 21 + source = "registry+https://github.com/rust-lang/crates.io-index" 22 + checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" 23 + dependencies = [ 24 + "memchr", 25 + ] 26 + 27 + [[package]] 28 + name = "append-only-bytes" 29 + version = "0.1.12" 30 + source = "registry+https://github.com/rust-lang/crates.io-index" 31 + checksum = "ac436601d6bdde674a0d7fb593e829ffe7b3387c351b356dd20e2d40f5bf3ee5" 32 + 33 + [[package]] 34 + name = "arbitrary" 35 + version = "1.4.2" 36 + source = "registry+https://github.com/rust-lang/crates.io-index" 37 + checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" 38 + dependencies = [ 39 + "derive_arbitrary", 40 + ] 41 + 42 + [[package]] 43 + name = "arrayvec" 44 + version = "0.7.6" 45 + source = "registry+https://github.com/rust-lang/crates.io-index" 46 + checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" 47 + 48 + [[package]] 49 + name = "arref" 50 + version = "0.1.0" 51 + source = "registry+https://github.com/rust-lang/crates.io-index" 52 + checksum = "2ccd462b64c3c72f1be8305905a85d85403d768e8690c9b8bd3b9009a5761679" 53 + 54 + [[package]] 55 + name = "atomic-polyfill" 56 + version = "1.0.3" 57 + source = "registry+https://github.com/rust-lang/crates.io-index" 58 + checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" 59 + dependencies = [ 60 + "critical-section", 61 + ] 62 + 63 + [[package]] 64 + name = "autocfg" 65 + version = "1.5.0" 66 + source = "registry+https://github.com/rust-lang/crates.io-index" 67 + checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" 68 + 69 + [[package]] 70 + name = "bitflags" 71 + version = "2.11.0" 72 + source = "registry+https://github.com/rust-lang/crates.io-index" 73 + checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" 74 + 75 + [[package]] 76 + name = "bitmaps" 77 + version = "2.1.0" 78 + source = "registry+https://github.com/rust-lang/crates.io-index" 79 + checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2" 80 + dependencies = [ 81 + "typenum", 82 + ] 83 + 84 + [[package]] 85 + name = "block-buffer" 86 + version = "0.10.4" 87 + source = "registry+https://github.com/rust-lang/crates.io-index" 88 + checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 89 + dependencies = [ 90 + "generic-array", 91 + ] 92 + 93 + [[package]] 94 + name = "bumpalo" 95 + version = "3.20.2" 96 + source = "registry+https://github.com/rust-lang/crates.io-index" 97 + checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" 98 + 99 + [[package]] 100 + name = "byteorder" 101 + version = "1.5.0" 102 + source = "registry+https://github.com/rust-lang/crates.io-index" 103 + checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 104 + 105 + [[package]] 106 + name = "bytes" 107 + version = "1.11.1" 108 + source = "registry+https://github.com/rust-lang/crates.io-index" 109 + checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" 110 + 111 + [[package]] 112 + name = "cc" 113 + version = "1.2.56" 114 + source = "registry+https://github.com/rust-lang/crates.io-index" 115 + checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" 116 + dependencies = [ 117 + "find-msvc-tools", 118 + "shlex", 119 + ] 120 + 121 + [[package]] 122 + name = "cfg-if" 123 + version = "1.0.4" 124 + source = "registry+https://github.com/rust-lang/crates.io-index" 125 + checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" 126 + 127 + [[package]] 128 + name = "cobs" 129 + version = "0.3.0" 130 + source = "registry+https://github.com/rust-lang/crates.io-index" 131 + checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" 132 + dependencies = [ 133 + "thiserror 2.0.18", 134 + ] 135 + 136 + [[package]] 137 + name = "cpufeatures" 138 + version = "0.2.17" 139 + source = "registry+https://github.com/rust-lang/crates.io-index" 140 + checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" 141 + dependencies = [ 142 + "libc", 143 + ] 144 + 145 + [[package]] 146 + name = "critical-section" 147 + version = "1.2.0" 148 + source = "registry+https://github.com/rust-lang/crates.io-index" 149 + checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" 150 + 151 + [[package]] 152 + name = "crypto-common" 153 + version = "0.1.7" 154 + source = "registry+https://github.com/rust-lang/crates.io-index" 155 + checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" 156 + dependencies = [ 157 + "generic-array", 158 + "typenum", 159 + ] 160 + 161 + [[package]] 162 + name = "darling" 163 + version = "0.20.11" 164 + source = "registry+https://github.com/rust-lang/crates.io-index" 165 + checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" 166 + dependencies = [ 167 + "darling_core", 168 + "darling_macro", 169 + ] 170 + 171 + [[package]] 172 + name = "darling_core" 173 + version = "0.20.11" 174 + source = "registry+https://github.com/rust-lang/crates.io-index" 175 + checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" 176 + dependencies = [ 177 + "fnv", 178 + "ident_case", 179 + "proc-macro2", 180 + "quote", 181 + "strsim", 182 + "syn 2.0.117", 183 + ] 184 + 185 + [[package]] 186 + name = "darling_macro" 187 + version = "0.20.11" 188 + source = "registry+https://github.com/rust-lang/crates.io-index" 189 + checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" 190 + dependencies = [ 191 + "darling_core", 192 + "quote", 193 + "syn 2.0.117", 194 + ] 195 + 196 + [[package]] 197 + name = "derive_arbitrary" 198 + version = "1.4.2" 199 + source = "registry+https://github.com/rust-lang/crates.io-index" 200 + checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" 201 + dependencies = [ 202 + "proc-macro2", 203 + "quote", 204 + "syn 2.0.117", 205 + ] 206 + 207 + [[package]] 208 + name = "diff" 209 + version = "0.1.13" 210 + source = "registry+https://github.com/rust-lang/crates.io-index" 211 + checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" 212 + 213 + [[package]] 214 + name = "digest" 215 + version = "0.10.7" 216 + source = "registry+https://github.com/rust-lang/crates.io-index" 217 + checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 218 + dependencies = [ 219 + "block-buffer", 220 + "crypto-common", 221 + ] 222 + 223 + [[package]] 224 + name = "either" 225 + version = "1.15.0" 226 + source = "registry+https://github.com/rust-lang/crates.io-index" 227 + checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 228 + 229 + [[package]] 230 + name = "embedded-io" 231 + version = "0.4.0" 232 + source = "registry+https://github.com/rust-lang/crates.io-index" 233 + checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" 234 + 235 + [[package]] 236 + name = "embedded-io" 237 + version = "0.6.1" 238 + source = "registry+https://github.com/rust-lang/crates.io-index" 239 + checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" 240 + 241 + [[package]] 242 + name = "ensure-cov" 243 + version = "0.1.0" 244 + source = "registry+https://github.com/rust-lang/crates.io-index" 245 + checksum = "33753185802e107b8fa907192af1f0eca13b1fb33327a59266d650fef29b2b4e" 246 + 247 + [[package]] 248 + name = "enum-as-inner" 249 + version = "0.5.1" 250 + source = "registry+https://github.com/rust-lang/crates.io-index" 251 + checksum = "c9720bba047d567ffc8a3cba48bf19126600e249ab7f128e9233e6376976a116" 252 + dependencies = [ 253 + "heck 0.4.1", 254 + "proc-macro2", 255 + "quote", 256 + "syn 1.0.109", 257 + ] 258 + 259 + [[package]] 260 + name = "enum-as-inner" 261 + version = "0.6.1" 262 + source = "registry+https://github.com/rust-lang/crates.io-index" 263 + checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" 264 + dependencies = [ 265 + "heck 0.5.0", 266 + "proc-macro2", 267 + "quote", 268 + "syn 2.0.117", 269 + ] 270 + 271 + [[package]] 272 + name = "enum_dispatch" 273 + version = "0.3.13" 274 + source = "registry+https://github.com/rust-lang/crates.io-index" 275 + checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd" 276 + dependencies = [ 277 + "once_cell", 278 + "proc-macro2", 279 + "quote", 280 + "syn 2.0.117", 281 + ] 282 + 283 + [[package]] 284 + name = "equivalent" 285 + version = "1.0.2" 286 + source = "registry+https://github.com/rust-lang/crates.io-index" 287 + checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 288 + 289 + [[package]] 290 + name = "find-msvc-tools" 291 + version = "0.1.9" 292 + source = "registry+https://github.com/rust-lang/crates.io-index" 293 + checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" 294 + 295 + [[package]] 296 + name = "fnv" 297 + version = "1.0.7" 298 + source = "registry+https://github.com/rust-lang/crates.io-index" 299 + checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 300 + 301 + [[package]] 302 + name = "generator" 303 + version = "0.8.8" 304 + source = "registry+https://github.com/rust-lang/crates.io-index" 305 + checksum = "52f04ae4152da20c76fe800fa48659201d5cf627c5149ca0b707b69d7eef6cf9" 306 + dependencies = [ 307 + "cc", 308 + "cfg-if", 309 + "libc", 310 + "log", 311 + "rustversion", 312 + "windows-link", 313 + "windows-result", 314 + ] 315 + 316 + [[package]] 317 + name = "generic-array" 318 + version = "0.14.7" 319 + source = "registry+https://github.com/rust-lang/crates.io-index" 320 + checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 321 + dependencies = [ 322 + "typenum", 323 + "version_check", 324 + ] 325 + 326 + [[package]] 327 + name = "generic-btree" 328 + version = "0.10.7" 329 + source = "registry+https://github.com/rust-lang/crates.io-index" 330 + checksum = "a0c1bce85c110ab718fd139e0cc89c51b63bd647b14a767e24bdfc77c83df79b" 331 + dependencies = [ 332 + "arref", 333 + "heapless 0.9.2", 334 + "itertools 0.11.0", 335 + "loro-thunderdome", 336 + "proc-macro2", 337 + "rustc-hash", 338 + ] 339 + 340 + [[package]] 341 + name = "getrandom" 342 + version = "0.2.17" 343 + source = "registry+https://github.com/rust-lang/crates.io-index" 344 + checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" 345 + dependencies = [ 346 + "cfg-if", 347 + "js-sys", 348 + "libc", 349 + "wasi", 350 + "wasm-bindgen", 351 + ] 352 + 353 + [[package]] 354 + name = "getrandom" 355 + version = "0.3.4" 356 + source = "registry+https://github.com/rust-lang/crates.io-index" 357 + checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" 358 + dependencies = [ 359 + "cfg-if", 360 + "libc", 361 + "r-efi", 362 + "wasip2", 363 + ] 364 + 365 + [[package]] 366 + name = "hash32" 367 + version = "0.2.1" 368 + source = "registry+https://github.com/rust-lang/crates.io-index" 369 + checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" 370 + dependencies = [ 371 + "byteorder", 372 + ] 373 + 374 + [[package]] 375 + name = "hash32" 376 + version = "0.3.1" 377 + source = "registry+https://github.com/rust-lang/crates.io-index" 378 + checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" 379 + dependencies = [ 380 + "byteorder", 381 + ] 382 + 383 + [[package]] 384 + name = "hashbrown" 385 + version = "0.16.1" 386 + source = "registry+https://github.com/rust-lang/crates.io-index" 387 + checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" 388 + 389 + [[package]] 390 + name = "heapless" 391 + version = "0.7.17" 392 + source = "registry+https://github.com/rust-lang/crates.io-index" 393 + checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" 394 + dependencies = [ 395 + "atomic-polyfill", 396 + "hash32 0.2.1", 397 + "rustc_version", 398 + "serde", 399 + "spin", 400 + "stable_deref_trait", 401 + ] 402 + 403 + [[package]] 404 + name = "heapless" 405 + version = "0.8.0" 406 + source = "registry+https://github.com/rust-lang/crates.io-index" 407 + checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" 408 + dependencies = [ 409 + "hash32 0.3.1", 410 + "stable_deref_trait", 411 + ] 412 + 413 + [[package]] 414 + name = "heapless" 415 + version = "0.9.2" 416 + source = "registry+https://github.com/rust-lang/crates.io-index" 417 + checksum = "2af2455f757db2b292a9b1768c4b70186d443bcb3b316252d6b540aec1cd89ed" 418 + dependencies = [ 419 + "hash32 0.3.1", 420 + "stable_deref_trait", 421 + ] 422 + 423 + [[package]] 424 + name = "heck" 425 + version = "0.4.1" 426 + source = "registry+https://github.com/rust-lang/crates.io-index" 427 + checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 428 + 429 + [[package]] 430 + name = "heck" 431 + version = "0.5.0" 432 + source = "registry+https://github.com/rust-lang/crates.io-index" 433 + checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 434 + 435 + [[package]] 436 + name = "ident_case" 437 + version = "1.0.1" 438 + source = "registry+https://github.com/rust-lang/crates.io-index" 439 + checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 440 + 441 + [[package]] 442 + name = "im" 443 + version = "15.1.0" 444 + source = "registry+https://github.com/rust-lang/crates.io-index" 445 + checksum = "d0acd33ff0285af998aaf9b57342af478078f53492322fafc47450e09397e0e9" 446 + dependencies = [ 447 + "bitmaps", 448 + "rand_core", 449 + "rand_xoshiro", 450 + "serde", 451 + "sized-chunks", 452 + "typenum", 453 + "version_check", 454 + ] 455 + 456 + [[package]] 457 + name = "itertools" 458 + version = "0.11.0" 459 + source = "registry+https://github.com/rust-lang/crates.io-index" 460 + checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" 461 + dependencies = [ 462 + "either", 463 + ] 464 + 465 + [[package]] 466 + name = "itertools" 467 + version = "0.12.1" 468 + source = "registry+https://github.com/rust-lang/crates.io-index" 469 + checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" 470 + dependencies = [ 471 + "either", 472 + ] 473 + 474 + [[package]] 475 + name = "itoa" 476 + version = "1.0.17" 477 + source = "registry+https://github.com/rust-lang/crates.io-index" 478 + checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" 479 + 480 + [[package]] 481 + name = "js-sys" 482 + version = "0.3.86" 483 + source = "registry+https://github.com/rust-lang/crates.io-index" 484 + checksum = "d36139f1c97c42c0c86a411910b04e48d4939a0376e6e0f989420cbdee0120e5" 485 + dependencies = [ 486 + "once_cell", 487 + "wasm-bindgen", 488 + ] 489 + 490 + [[package]] 491 + name = "lazy_static" 492 + version = "1.5.0" 493 + source = "registry+https://github.com/rust-lang/crates.io-index" 494 + checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 495 + 496 + [[package]] 497 + name = "leb128" 498 + version = "0.2.5" 499 + source = "registry+https://github.com/rust-lang/crates.io-index" 500 + checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" 501 + 502 + [[package]] 503 + name = "libc" 504 + version = "0.2.182" 505 + source = "registry+https://github.com/rust-lang/crates.io-index" 506 + checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" 507 + 508 + [[package]] 509 + name = "lock_api" 510 + version = "0.4.14" 511 + source = "registry+https://github.com/rust-lang/crates.io-index" 512 + checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" 513 + dependencies = [ 514 + "scopeguard", 515 + ] 516 + 517 + [[package]] 518 + name = "log" 519 + version = "0.4.29" 520 + source = "registry+https://github.com/rust-lang/crates.io-index" 521 + checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" 522 + 523 + [[package]] 524 + name = "loom" 525 + version = "0.7.2" 526 + source = "registry+https://github.com/rust-lang/crates.io-index" 527 + checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" 528 + dependencies = [ 529 + "cfg-if", 530 + "generator", 531 + "scoped-tls", 532 + "serde", 533 + "serde_json", 534 + "tracing", 535 + "tracing-subscriber", 536 + ] 537 + 538 + [[package]] 539 + name = "loro" 540 + version = "1.10.3" 541 + source = "registry+https://github.com/rust-lang/crates.io-index" 542 + checksum = "d75216d8f99725531a30f7b00901ee154a4f8a9b7f125bfe032e197d4c7ffb8c" 543 + dependencies = [ 544 + "enum-as-inner 0.6.1", 545 + "generic-btree", 546 + "loro-common", 547 + "loro-delta", 548 + "loro-internal", 549 + "loro-kv-store", 550 + "rustc-hash", 551 + "tracing", 552 + ] 553 + 554 + [[package]] 555 + name = "loro-common" 556 + version = "1.10.0" 557 + source = "registry+https://github.com/rust-lang/crates.io-index" 558 + checksum = "70363ea05a9c507fd9d58b65dc414bf515f636d69d8ab53e50ecbe8d27eef90c" 559 + dependencies = [ 560 + "arbitrary", 561 + "enum-as-inner 0.6.1", 562 + "leb128", 563 + "loro-rle", 564 + "nonmax", 565 + "rustc-hash", 566 + "serde", 567 + "serde_columnar", 568 + "serde_json", 569 + "thiserror 1.0.69", 570 + ] 571 + 572 + [[package]] 573 + name = "loro-delta" 574 + version = "1.9.1" 575 + source = "registry+https://github.com/rust-lang/crates.io-index" 576 + checksum = "8eafa788a72c1cbf0b7dc08a862cd7cc31b96d99c2ef749cdc94c2330f9494d3" 577 + dependencies = [ 578 + "arrayvec", 579 + "enum-as-inner 0.5.1", 580 + "generic-btree", 581 + "heapless 0.8.0", 582 + ] 583 + 584 + [[package]] 585 + name = "loro-internal" 586 + version = "1.10.3" 587 + source = "registry+https://github.com/rust-lang/crates.io-index" 588 + checksum = "f447044ec3d3ba572623859add3334bd87b84340ee5fdf00315bfee0e3ad3e3f" 589 + dependencies = [ 590 + "append-only-bytes", 591 + "arref", 592 + "bytes", 593 + "either", 594 + "ensure-cov", 595 + "enum-as-inner 0.6.1", 596 + "enum_dispatch", 597 + "generic-btree", 598 + "getrandom 0.2.17", 599 + "im", 600 + "itertools 0.12.1", 601 + "leb128", 602 + "loom", 603 + "loro-common", 604 + "loro-delta", 605 + "loro-kv-store", 606 + "loro-rle", 607 + "loro_fractional_index", 608 + "md5", 609 + "nonmax", 610 + "num", 611 + "num-traits", 612 + "once_cell", 613 + "parking_lot", 614 + "pest", 615 + "pest_derive", 616 + "postcard", 617 + "pretty_assertions", 618 + "rand", 619 + "rustc-hash", 620 + "serde", 621 + "serde_columnar", 622 + "serde_json", 623 + "smallvec", 624 + "thiserror 1.0.69", 625 + "thread_local", 626 + "tracing", 627 + "wasm-bindgen", 628 + "xxhash-rust", 629 + ] 630 + 631 + [[package]] 632 + name = "loro-kv-store" 633 + version = "1.10.0" 634 + source = "registry+https://github.com/rust-lang/crates.io-index" 635 + checksum = "78beebc933a33c26495c9a98f05b38bc0a4c0a337ecfbd3146ce1f9437eec71f" 636 + dependencies = [ 637 + "bytes", 638 + "ensure-cov", 639 + "loro-common", 640 + "lz4_flex", 641 + "once_cell", 642 + "quick_cache", 643 + "rustc-hash", 644 + "tracing", 645 + "xxhash-rust", 646 + ] 647 + 648 + [[package]] 649 + name = "loro-rle" 650 + version = "1.6.0" 651 + source = "registry+https://github.com/rust-lang/crates.io-index" 652 + checksum = "76400c3eea6bb39b013406acce964a8db39311534e308286c8d8721baba8ee20" 653 + dependencies = [ 654 + "append-only-bytes", 655 + "num", 656 + "smallvec", 657 + ] 658 + 659 + [[package]] 660 + name = "loro-thunderdome" 661 + version = "0.6.2" 662 + source = "registry+https://github.com/rust-lang/crates.io-index" 663 + checksum = "3f3d053a135388e6b1df14e8af1212af5064746e9b87a06a345a7a779ee9695a" 664 + 665 + [[package]] 666 + name = "loro_fractional_index" 667 + version = "1.6.0" 668 + source = "registry+https://github.com/rust-lang/crates.io-index" 669 + checksum = "427c8ea186958094052b971fe7e322a934b034c3bf62f0458ccea04fcd687ba1" 670 + dependencies = [ 671 + "once_cell", 672 + "rand", 673 + "serde", 674 + ] 675 + 676 + [[package]] 677 + name = "loroscope" 678 + version = "0.1.0" 679 + dependencies = [ 680 + "loro", 681 + "loroscope_macros", 682 + ] 683 + 684 + [[package]] 685 + name = "loroscope_macros" 686 + version = "0.1.0" 687 + dependencies = [ 688 + "proc-macro2", 689 + "quote", 690 + "syn 2.0.117", 691 + ] 692 + 693 + [[package]] 694 + name = "lz4_flex" 695 + version = "0.11.5" 696 + source = "registry+https://github.com/rust-lang/crates.io-index" 697 + checksum = "08ab2867e3eeeca90e844d1940eab391c9dc5228783db2ed999acbc0a9ed375a" 698 + dependencies = [ 699 + "twox-hash", 700 + ] 701 + 702 + [[package]] 703 + name = "matchers" 704 + version = "0.2.0" 705 + source = "registry+https://github.com/rust-lang/crates.io-index" 706 + checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" 707 + dependencies = [ 708 + "regex-automata", 709 + ] 710 + 711 + [[package]] 712 + name = "md5" 713 + version = "0.7.0" 714 + source = "registry+https://github.com/rust-lang/crates.io-index" 715 + checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" 716 + 717 + [[package]] 718 + name = "memchr" 719 + version = "2.8.0" 720 + source = "registry+https://github.com/rust-lang/crates.io-index" 721 + checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" 722 + 723 + [[package]] 724 + name = "nonmax" 725 + version = "0.5.5" 726 + source = "registry+https://github.com/rust-lang/crates.io-index" 727 + checksum = "610a5acd306ec67f907abe5567859a3c693fb9886eb1f012ab8f2a47bef3db51" 728 + 729 + [[package]] 730 + name = "nu-ansi-term" 731 + version = "0.50.3" 732 + source = "registry+https://github.com/rust-lang/crates.io-index" 733 + checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" 734 + dependencies = [ 735 + "windows-sys", 736 + ] 737 + 738 + [[package]] 739 + name = "num" 740 + version = "0.4.3" 741 + source = "registry+https://github.com/rust-lang/crates.io-index" 742 + checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" 743 + dependencies = [ 744 + "num-bigint", 745 + "num-complex", 746 + "num-integer", 747 + "num-iter", 748 + "num-rational", 749 + "num-traits", 750 + ] 751 + 752 + [[package]] 753 + name = "num-bigint" 754 + version = "0.4.6" 755 + source = "registry+https://github.com/rust-lang/crates.io-index" 756 + checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" 757 + dependencies = [ 758 + "num-integer", 759 + "num-traits", 760 + ] 761 + 762 + [[package]] 763 + name = "num-complex" 764 + version = "0.4.6" 765 + source = "registry+https://github.com/rust-lang/crates.io-index" 766 + checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" 767 + dependencies = [ 768 + "num-traits", 769 + ] 770 + 771 + [[package]] 772 + name = "num-integer" 773 + version = "0.1.46" 774 + source = "registry+https://github.com/rust-lang/crates.io-index" 775 + checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" 776 + dependencies = [ 777 + "num-traits", 778 + ] 779 + 780 + [[package]] 781 + name = "num-iter" 782 + version = "0.1.45" 783 + source = "registry+https://github.com/rust-lang/crates.io-index" 784 + checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" 785 + dependencies = [ 786 + "autocfg", 787 + "num-integer", 788 + "num-traits", 789 + ] 790 + 791 + [[package]] 792 + name = "num-rational" 793 + version = "0.4.2" 794 + source = "registry+https://github.com/rust-lang/crates.io-index" 795 + checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" 796 + dependencies = [ 797 + "num-bigint", 798 + "num-integer", 799 + "num-traits", 800 + ] 801 + 802 + [[package]] 803 + name = "num-traits" 804 + version = "0.2.19" 805 + source = "registry+https://github.com/rust-lang/crates.io-index" 806 + checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 807 + dependencies = [ 808 + "autocfg", 809 + ] 810 + 811 + [[package]] 812 + name = "once_cell" 813 + version = "1.21.3" 814 + source = "registry+https://github.com/rust-lang/crates.io-index" 815 + checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 816 + 817 + [[package]] 818 + name = "parking_lot" 819 + version = "0.12.5" 820 + source = "registry+https://github.com/rust-lang/crates.io-index" 821 + checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" 822 + dependencies = [ 823 + "lock_api", 824 + "parking_lot_core", 825 + ] 826 + 827 + [[package]] 828 + name = "parking_lot_core" 829 + version = "0.9.12" 830 + source = "registry+https://github.com/rust-lang/crates.io-index" 831 + checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" 832 + dependencies = [ 833 + "cfg-if", 834 + "libc", 835 + "redox_syscall", 836 + "smallvec", 837 + "windows-link", 838 + ] 839 + 840 + [[package]] 841 + name = "pest" 842 + version = "2.8.6" 843 + source = "registry+https://github.com/rust-lang/crates.io-index" 844 + checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" 845 + dependencies = [ 846 + "memchr", 847 + "ucd-trie", 848 + ] 849 + 850 + [[package]] 851 + name = "pest_derive" 852 + version = "2.8.6" 853 + source = "registry+https://github.com/rust-lang/crates.io-index" 854 + checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" 855 + dependencies = [ 856 + "pest", 857 + "pest_generator", 858 + ] 859 + 860 + [[package]] 861 + name = "pest_generator" 862 + version = "2.8.6" 863 + source = "registry+https://github.com/rust-lang/crates.io-index" 864 + checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" 865 + dependencies = [ 866 + "pest", 867 + "pest_meta", 868 + "proc-macro2", 869 + "quote", 870 + "syn 2.0.117", 871 + ] 872 + 873 + [[package]] 874 + name = "pest_meta" 875 + version = "2.8.6" 876 + source = "registry+https://github.com/rust-lang/crates.io-index" 877 + checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" 878 + dependencies = [ 879 + "pest", 880 + "sha2", 881 + ] 882 + 883 + [[package]] 884 + name = "pin-project-lite" 885 + version = "0.2.16" 886 + source = "registry+https://github.com/rust-lang/crates.io-index" 887 + checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 888 + 889 + [[package]] 890 + name = "postcard" 891 + version = "1.1.3" 892 + source = "registry+https://github.com/rust-lang/crates.io-index" 893 + checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" 894 + dependencies = [ 895 + "cobs", 896 + "embedded-io 0.4.0", 897 + "embedded-io 0.6.1", 898 + "heapless 0.7.17", 899 + "serde", 900 + ] 901 + 902 + [[package]] 903 + name = "ppv-lite86" 904 + version = "0.2.21" 905 + source = "registry+https://github.com/rust-lang/crates.io-index" 906 + checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" 907 + dependencies = [ 908 + "zerocopy", 909 + ] 910 + 911 + [[package]] 912 + name = "pretty_assertions" 913 + version = "1.4.1" 914 + source = "registry+https://github.com/rust-lang/crates.io-index" 915 + checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" 916 + dependencies = [ 917 + "diff", 918 + "yansi", 919 + ] 920 + 921 + [[package]] 922 + name = "proc-macro2" 923 + version = "1.0.106" 924 + source = "registry+https://github.com/rust-lang/crates.io-index" 925 + checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" 926 + dependencies = [ 927 + "unicode-ident", 928 + ] 929 + 930 + [[package]] 931 + name = "quick_cache" 932 + version = "0.6.18" 933 + source = "registry+https://github.com/rust-lang/crates.io-index" 934 + checksum = "7ada44a88ef953a3294f6eb55d2007ba44646015e18613d2f213016379203ef3" 935 + dependencies = [ 936 + "ahash", 937 + "equivalent", 938 + "hashbrown", 939 + "parking_lot", 940 + ] 941 + 942 + [[package]] 943 + name = "quote" 944 + version = "1.0.44" 945 + source = "registry+https://github.com/rust-lang/crates.io-index" 946 + checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" 947 + dependencies = [ 948 + "proc-macro2", 949 + ] 950 + 951 + [[package]] 952 + name = "r-efi" 953 + version = "5.3.0" 954 + source = "registry+https://github.com/rust-lang/crates.io-index" 955 + checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" 956 + 957 + [[package]] 958 + name = "rand" 959 + version = "0.8.5" 960 + source = "registry+https://github.com/rust-lang/crates.io-index" 961 + checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 962 + dependencies = [ 963 + "libc", 964 + "rand_chacha", 965 + "rand_core", 966 + ] 967 + 968 + [[package]] 969 + name = "rand_chacha" 970 + version = "0.3.1" 971 + source = "registry+https://github.com/rust-lang/crates.io-index" 972 + checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 973 + dependencies = [ 974 + "ppv-lite86", 975 + "rand_core", 976 + ] 977 + 978 + [[package]] 979 + name = "rand_core" 980 + version = "0.6.4" 981 + source = "registry+https://github.com/rust-lang/crates.io-index" 982 + checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 983 + dependencies = [ 984 + "getrandom 0.2.17", 985 + ] 986 + 987 + [[package]] 988 + name = "rand_xoshiro" 989 + version = "0.6.0" 990 + source = "registry+https://github.com/rust-lang/crates.io-index" 991 + checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" 992 + dependencies = [ 993 + "rand_core", 994 + ] 995 + 996 + [[package]] 997 + name = "redox_syscall" 998 + version = "0.5.18" 999 + source = "registry+https://github.com/rust-lang/crates.io-index" 1000 + checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" 1001 + dependencies = [ 1002 + "bitflags", 1003 + ] 1004 + 1005 + [[package]] 1006 + name = "regex-automata" 1007 + version = "0.4.14" 1008 + source = "registry+https://github.com/rust-lang/crates.io-index" 1009 + checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" 1010 + dependencies = [ 1011 + "aho-corasick", 1012 + "memchr", 1013 + "regex-syntax", 1014 + ] 1015 + 1016 + [[package]] 1017 + name = "regex-syntax" 1018 + version = "0.8.9" 1019 + source = "registry+https://github.com/rust-lang/crates.io-index" 1020 + checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" 1021 + 1022 + [[package]] 1023 + name = "rustc-hash" 1024 + version = "2.1.1" 1025 + source = "registry+https://github.com/rust-lang/crates.io-index" 1026 + checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" 1027 + 1028 + [[package]] 1029 + name = "rustc_version" 1030 + version = "0.4.1" 1031 + source = "registry+https://github.com/rust-lang/crates.io-index" 1032 + checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" 1033 + dependencies = [ 1034 + "semver", 1035 + ] 1036 + 1037 + [[package]] 1038 + name = "rustversion" 1039 + version = "1.0.22" 1040 + source = "registry+https://github.com/rust-lang/crates.io-index" 1041 + checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" 1042 + 1043 + [[package]] 1044 + name = "scoped-tls" 1045 + version = "1.0.1" 1046 + source = "registry+https://github.com/rust-lang/crates.io-index" 1047 + checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" 1048 + 1049 + [[package]] 1050 + name = "scopeguard" 1051 + version = "1.2.0" 1052 + source = "registry+https://github.com/rust-lang/crates.io-index" 1053 + checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1054 + 1055 + [[package]] 1056 + name = "semver" 1057 + version = "1.0.27" 1058 + source = "registry+https://github.com/rust-lang/crates.io-index" 1059 + checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" 1060 + 1061 + [[package]] 1062 + name = "serde" 1063 + version = "1.0.228" 1064 + source = "registry+https://github.com/rust-lang/crates.io-index" 1065 + checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" 1066 + dependencies = [ 1067 + "serde_core", 1068 + "serde_derive", 1069 + ] 1070 + 1071 + [[package]] 1072 + name = "serde_columnar" 1073 + version = "0.3.14" 1074 + source = "registry+https://github.com/rust-lang/crates.io-index" 1075 + checksum = "2a16e404f17b16d0273460350e29b02d76ba0d70f34afdc9a4fa034c97d6c6eb" 1076 + dependencies = [ 1077 + "itertools 0.11.0", 1078 + "postcard", 1079 + "serde", 1080 + "serde_columnar_derive", 1081 + "thiserror 1.0.69", 1082 + ] 1083 + 1084 + [[package]] 1085 + name = "serde_columnar_derive" 1086 + version = "0.3.7" 1087 + source = "registry+https://github.com/rust-lang/crates.io-index" 1088 + checksum = "45958fce4903f67e871fbf15ac78e289269b21ebd357d6fecacdba233629112e" 1089 + dependencies = [ 1090 + "darling", 1091 + "proc-macro2", 1092 + "quote", 1093 + "syn 2.0.117", 1094 + ] 1095 + 1096 + [[package]] 1097 + name = "serde_core" 1098 + version = "1.0.228" 1099 + source = "registry+https://github.com/rust-lang/crates.io-index" 1100 + checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" 1101 + dependencies = [ 1102 + "serde_derive", 1103 + ] 1104 + 1105 + [[package]] 1106 + name = "serde_derive" 1107 + version = "1.0.228" 1108 + source = "registry+https://github.com/rust-lang/crates.io-index" 1109 + checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" 1110 + dependencies = [ 1111 + "proc-macro2", 1112 + "quote", 1113 + "syn 2.0.117", 1114 + ] 1115 + 1116 + [[package]] 1117 + name = "serde_json" 1118 + version = "1.0.149" 1119 + source = "registry+https://github.com/rust-lang/crates.io-index" 1120 + checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" 1121 + dependencies = [ 1122 + "itoa", 1123 + "memchr", 1124 + "serde", 1125 + "serde_core", 1126 + "zmij", 1127 + ] 1128 + 1129 + [[package]] 1130 + name = "sha2" 1131 + version = "0.10.9" 1132 + source = "registry+https://github.com/rust-lang/crates.io-index" 1133 + checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" 1134 + dependencies = [ 1135 + "cfg-if", 1136 + "cpufeatures", 1137 + "digest", 1138 + ] 1139 + 1140 + [[package]] 1141 + name = "sharded-slab" 1142 + version = "0.1.7" 1143 + source = "registry+https://github.com/rust-lang/crates.io-index" 1144 + checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" 1145 + dependencies = [ 1146 + "lazy_static", 1147 + ] 1148 + 1149 + [[package]] 1150 + name = "shlex" 1151 + version = "1.3.0" 1152 + source = "registry+https://github.com/rust-lang/crates.io-index" 1153 + checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 1154 + 1155 + [[package]] 1156 + name = "sized-chunks" 1157 + version = "0.6.5" 1158 + source = "registry+https://github.com/rust-lang/crates.io-index" 1159 + checksum = "16d69225bde7a69b235da73377861095455d298f2b970996eec25ddbb42b3d1e" 1160 + dependencies = [ 1161 + "bitmaps", 1162 + "typenum", 1163 + ] 1164 + 1165 + [[package]] 1166 + name = "smallvec" 1167 + version = "1.15.1" 1168 + source = "registry+https://github.com/rust-lang/crates.io-index" 1169 + checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" 1170 + dependencies = [ 1171 + "serde", 1172 + ] 1173 + 1174 + [[package]] 1175 + name = "spin" 1176 + version = "0.9.8" 1177 + source = "registry+https://github.com/rust-lang/crates.io-index" 1178 + checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 1179 + dependencies = [ 1180 + "lock_api", 1181 + ] 1182 + 1183 + [[package]] 1184 + name = "stable_deref_trait" 1185 + version = "1.2.1" 1186 + source = "registry+https://github.com/rust-lang/crates.io-index" 1187 + checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" 1188 + 1189 + [[package]] 1190 + name = "strsim" 1191 + version = "0.11.1" 1192 + source = "registry+https://github.com/rust-lang/crates.io-index" 1193 + checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 1194 + 1195 + [[package]] 1196 + name = "syn" 1197 + version = "1.0.109" 1198 + source = "registry+https://github.com/rust-lang/crates.io-index" 1199 + checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 1200 + dependencies = [ 1201 + "proc-macro2", 1202 + "quote", 1203 + "unicode-ident", 1204 + ] 1205 + 1206 + [[package]] 1207 + name = "syn" 1208 + version = "2.0.117" 1209 + source = "registry+https://github.com/rust-lang/crates.io-index" 1210 + checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" 1211 + dependencies = [ 1212 + "proc-macro2", 1213 + "quote", 1214 + "unicode-ident", 1215 + ] 1216 + 1217 + [[package]] 1218 + name = "thiserror" 1219 + version = "1.0.69" 1220 + source = "registry+https://github.com/rust-lang/crates.io-index" 1221 + checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 1222 + dependencies = [ 1223 + "thiserror-impl 1.0.69", 1224 + ] 1225 + 1226 + [[package]] 1227 + name = "thiserror" 1228 + version = "2.0.18" 1229 + source = "registry+https://github.com/rust-lang/crates.io-index" 1230 + checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" 1231 + dependencies = [ 1232 + "thiserror-impl 2.0.18", 1233 + ] 1234 + 1235 + [[package]] 1236 + name = "thiserror-impl" 1237 + version = "1.0.69" 1238 + source = "registry+https://github.com/rust-lang/crates.io-index" 1239 + checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 1240 + dependencies = [ 1241 + "proc-macro2", 1242 + "quote", 1243 + "syn 2.0.117", 1244 + ] 1245 + 1246 + [[package]] 1247 + name = "thiserror-impl" 1248 + version = "2.0.18" 1249 + source = "registry+https://github.com/rust-lang/crates.io-index" 1250 + checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" 1251 + dependencies = [ 1252 + "proc-macro2", 1253 + "quote", 1254 + "syn 2.0.117", 1255 + ] 1256 + 1257 + [[package]] 1258 + name = "thread_local" 1259 + version = "1.1.9" 1260 + source = "registry+https://github.com/rust-lang/crates.io-index" 1261 + checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" 1262 + dependencies = [ 1263 + "cfg-if", 1264 + ] 1265 + 1266 + [[package]] 1267 + name = "tracing" 1268 + version = "0.1.44" 1269 + source = "registry+https://github.com/rust-lang/crates.io-index" 1270 + checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" 1271 + dependencies = [ 1272 + "pin-project-lite", 1273 + "tracing-attributes", 1274 + "tracing-core", 1275 + ] 1276 + 1277 + [[package]] 1278 + name = "tracing-attributes" 1279 + version = "0.1.31" 1280 + source = "registry+https://github.com/rust-lang/crates.io-index" 1281 + checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" 1282 + dependencies = [ 1283 + "proc-macro2", 1284 + "quote", 1285 + "syn 2.0.117", 1286 + ] 1287 + 1288 + [[package]] 1289 + name = "tracing-core" 1290 + version = "0.1.36" 1291 + source = "registry+https://github.com/rust-lang/crates.io-index" 1292 + checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" 1293 + dependencies = [ 1294 + "once_cell", 1295 + "valuable", 1296 + ] 1297 + 1298 + [[package]] 1299 + name = "tracing-log" 1300 + version = "0.2.0" 1301 + source = "registry+https://github.com/rust-lang/crates.io-index" 1302 + checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" 1303 + dependencies = [ 1304 + "log", 1305 + "once_cell", 1306 + "tracing-core", 1307 + ] 1308 + 1309 + [[package]] 1310 + name = "tracing-subscriber" 1311 + version = "0.3.22" 1312 + source = "registry+https://github.com/rust-lang/crates.io-index" 1313 + checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" 1314 + dependencies = [ 1315 + "matchers", 1316 + "nu-ansi-term", 1317 + "once_cell", 1318 + "regex-automata", 1319 + "sharded-slab", 1320 + "smallvec", 1321 + "thread_local", 1322 + "tracing", 1323 + "tracing-core", 1324 + "tracing-log", 1325 + ] 1326 + 1327 + [[package]] 1328 + name = "twox-hash" 1329 + version = "2.1.2" 1330 + source = "registry+https://github.com/rust-lang/crates.io-index" 1331 + checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" 1332 + 1333 + [[package]] 1334 + name = "typenum" 1335 + version = "1.19.0" 1336 + source = "registry+https://github.com/rust-lang/crates.io-index" 1337 + checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" 1338 + 1339 + [[package]] 1340 + name = "ucd-trie" 1341 + version = "0.1.7" 1342 + source = "registry+https://github.com/rust-lang/crates.io-index" 1343 + checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" 1344 + 1345 + [[package]] 1346 + name = "unicode-ident" 1347 + version = "1.0.24" 1348 + source = "registry+https://github.com/rust-lang/crates.io-index" 1349 + checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" 1350 + 1351 + [[package]] 1352 + name = "valuable" 1353 + version = "0.1.1" 1354 + source = "registry+https://github.com/rust-lang/crates.io-index" 1355 + checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" 1356 + 1357 + [[package]] 1358 + name = "version_check" 1359 + version = "0.9.5" 1360 + source = "registry+https://github.com/rust-lang/crates.io-index" 1361 + checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 1362 + 1363 + [[package]] 1364 + name = "wasi" 1365 + version = "0.11.1+wasi-snapshot-preview1" 1366 + source = "registry+https://github.com/rust-lang/crates.io-index" 1367 + checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" 1368 + 1369 + [[package]] 1370 + name = "wasip2" 1371 + version = "1.0.2+wasi-0.2.9" 1372 + source = "registry+https://github.com/rust-lang/crates.io-index" 1373 + checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" 1374 + dependencies = [ 1375 + "wit-bindgen", 1376 + ] 1377 + 1378 + [[package]] 1379 + name = "wasm-bindgen" 1380 + version = "0.2.109" 1381 + source = "registry+https://github.com/rust-lang/crates.io-index" 1382 + checksum = "9ff9c7baef35ac3c0e17d8bfc9ad75eb62f85a2f02bccc906699dadb0aa9c622" 1383 + dependencies = [ 1384 + "cfg-if", 1385 + "once_cell", 1386 + "rustversion", 1387 + "wasm-bindgen-macro", 1388 + "wasm-bindgen-shared", 1389 + ] 1390 + 1391 + [[package]] 1392 + name = "wasm-bindgen-macro" 1393 + version = "0.2.109" 1394 + source = "registry+https://github.com/rust-lang/crates.io-index" 1395 + checksum = "39455e84ad887a0bbc93c116d72403f1bb0a39e37dd6f235a43e2128a0c7f1fd" 1396 + dependencies = [ 1397 + "quote", 1398 + "wasm-bindgen-macro-support", 1399 + ] 1400 + 1401 + [[package]] 1402 + name = "wasm-bindgen-macro-support" 1403 + version = "0.2.109" 1404 + source = "registry+https://github.com/rust-lang/crates.io-index" 1405 + checksum = "dff4761f60b0b51fd13fec8764167b7bbcc34498ce3e52805fe1db6f2d56b6d6" 1406 + dependencies = [ 1407 + "bumpalo", 1408 + "proc-macro2", 1409 + "quote", 1410 + "syn 2.0.117", 1411 + "wasm-bindgen-shared", 1412 + ] 1413 + 1414 + [[package]] 1415 + name = "wasm-bindgen-shared" 1416 + version = "0.2.109" 1417 + source = "registry+https://github.com/rust-lang/crates.io-index" 1418 + checksum = "bc6a171c53d98021a93a474c4a4579d76ba97f9517d871bc12e27640f218b6dd" 1419 + dependencies = [ 1420 + "unicode-ident", 1421 + ] 1422 + 1423 + [[package]] 1424 + name = "windows-link" 1425 + version = "0.2.1" 1426 + source = "registry+https://github.com/rust-lang/crates.io-index" 1427 + checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" 1428 + 1429 + [[package]] 1430 + name = "windows-result" 1431 + version = "0.4.1" 1432 + source = "registry+https://github.com/rust-lang/crates.io-index" 1433 + checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" 1434 + dependencies = [ 1435 + "windows-link", 1436 + ] 1437 + 1438 + [[package]] 1439 + name = "windows-sys" 1440 + version = "0.61.2" 1441 + source = "registry+https://github.com/rust-lang/crates.io-index" 1442 + checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" 1443 + dependencies = [ 1444 + "windows-link", 1445 + ] 1446 + 1447 + [[package]] 1448 + name = "wit-bindgen" 1449 + version = "0.51.0" 1450 + source = "registry+https://github.com/rust-lang/crates.io-index" 1451 + checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" 1452 + 1453 + [[package]] 1454 + name = "xxhash-rust" 1455 + version = "0.8.15" 1456 + source = "registry+https://github.com/rust-lang/crates.io-index" 1457 + checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" 1458 + 1459 + [[package]] 1460 + name = "yansi" 1461 + version = "1.0.1" 1462 + source = "registry+https://github.com/rust-lang/crates.io-index" 1463 + checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" 1464 + 1465 + [[package]] 1466 + name = "zerocopy" 1467 + version = "0.8.39" 1468 + source = "registry+https://github.com/rust-lang/crates.io-index" 1469 + checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" 1470 + dependencies = [ 1471 + "zerocopy-derive", 1472 + ] 1473 + 1474 + [[package]] 1475 + name = "zerocopy-derive" 1476 + version = "0.8.39" 1477 + source = "registry+https://github.com/rust-lang/crates.io-index" 1478 + checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" 1479 + dependencies = [ 1480 + "proc-macro2", 1481 + "quote", 1482 + "syn 2.0.117", 1483 + ] 1484 + 1485 + [[package]] 1486 + name = "zmij" 1487 + version = "1.0.21" 1488 + source = "registry+https://github.com/rust-lang/crates.io-index" 1489 + checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
+3
Cargo.toml
··· 1 + [workspace] 2 + members = ["loroscope", "loroscope_macros"] 3 + resolver = "2"
+8
loroscope/Cargo.toml
··· 1 + [package] 2 + name = "loroscope" 3 + version = "0.1.0" 4 + edition = "2024" 5 + 6 + [dependencies] 7 + loro = { version = "1", features = ["counter"] } 8 + loroscope_macros = { path = "../loroscope_macros" }
+91
loroscope/src/lib.rs
··· 1 + //! Typed structs for [Loro](https://loro.dev/) CRDTs. 2 + //! 3 + //! [Loro](https://loro.dev/) is a CRDT library — it gives you conflict-free 4 + //! replicated containers (lists, maps, trees, rich text, etc.) with undo/redo, 5 + //! sync, and time travel. But working with it directly means string-keyed 6 + //! container lookups and manual [`LoroValue`](loro::LoroValue) matching. 7 + //! 8 + //! `#[loroscope]` generates typed accessors from a struct definition, so field 9 + //! access is checked at compile time. 10 + //! 11 + //! # Example 12 + //! 13 + //! ``` 14 + //! use loroscope::loroscope; 15 + //! 16 + //! #[loroscope] 17 + //! struct TodoItem { 18 + //! title: Text, 19 + //! done: bool, 20 + //! } 21 + //! 22 + //! #[loroscope] 23 + //! struct App { 24 + //! todos: List<TodoItem>, 25 + //! } 26 + //! 27 + //! let app = App::new(); 28 + //! 29 + //! let item = app.todos().push_new(); 30 + //! item.title().insert(0, "Buy milk").unwrap(); 31 + //! item.set_done(false); 32 + //! 33 + //! assert_eq!(app.todos().len(), 1); 34 + //! assert!(!app.todos().get(0).unwrap().done()); 35 + //! 36 + //! // The underlying LoroDoc is always available for export, import, 37 + //! // undo, subscriptions, etc. 38 + //! let _doc = app.doc(); 39 + //! ``` 40 + //! 41 + //! # Field types 42 + //! 43 + //! | Type | Accessor | 44 + //! |---|---| 45 + //! | `f64`, `i64`, `bool`, `String` | Getter + `set_` setter | 46 + //! | [`Text`], [`Counter`] | Getter returning the container | 47 + //! | [`List<T>`], [`Map<V>`], [`MovableList<T>`], [`Tree<T>`] | Getter returning a typed collection | 48 + //! | `LoroList`, `LoroMap`, `LoroText`, … | Getter returning the raw Loro container | 49 + //! | Any `#[loroscope]` struct | Getter returning a nested typed view | 50 + 51 + #![deny(missing_docs)] 52 + #![deny(unreachable_pub)] 53 + #![deny(clippy::unwrap_used)] 54 + #![deny(clippy::doc_markdown)] 55 + 56 + pub use loroscope_macros::loroscope; 57 + 58 + /// A rich-text container. Type alias for [`loro::LoroText`]. 59 + pub type Text = loro::LoroText; 60 + 61 + /// A CRDT counter. Type alias for [`loro::LoroCounter`]. 62 + pub type Counter = loro::LoroCounter; 63 + 64 + mod list; 65 + mod map; 66 + mod movable_list; 67 + mod tree; 68 + 69 + pub use list::List; 70 + pub use loro::{TreeID, TreeParentId}; 71 + pub use map::Map; 72 + pub use movable_list::MovableList; 73 + pub use tree::Tree; 74 + 75 + /// Trait implemented automatically by `#[loroscope]` structs. 76 + /// 77 + /// This allows structs to be used as elements in [`List<T>`], [`Map<V>`], 78 + /// and [`MovableList<T>`]. You do not need to implement this trait manually. 79 + pub trait FromLoroMap: Sized { 80 + /// Creates a typed view from a [`loro::LoroMap`]. 81 + fn from_loro_map(map: loro::LoroMap) -> Self; 82 + } 83 + 84 + #[doc(hidden)] 85 + #[allow(missing_docs)] 86 + pub mod __private { 87 + pub use loro::{ 88 + Container, ContainerTrait, LoroCounter, LoroDoc, LoroList, LoroMap, LoroMovableList, 89 + LoroText, LoroTree, LoroValue, ValueOrContainer, 90 + }; 91 + }
+159
loroscope/src/list.rs
··· 1 + use std::marker::PhantomData; 2 + 3 + use loro::{LoroList, LoroMap, LoroValue}; 4 + 5 + use crate::FromLoroMap; 6 + 7 + /// A typed list where every element is of type `T`. 8 + /// 9 + /// Use this as a field type in a `#[loroscope]` struct: 10 + /// 11 + /// ```ignore 12 + /// #[loroscope] 13 + /// struct Project { 14 + /// items: List<Item>, 15 + /// } 16 + /// ``` 17 + /// 18 + /// `T` can be a `#[loroscope]` struct, a primitive (`f64`, `i64`, `bool`, 19 + /// `String`), or a Loro container type (`LoroText`, `LoroCounter`, 20 + /// `LoroTree`). 21 + pub struct List<T> { 22 + list: LoroList, 23 + _phantom: PhantomData<T>, 24 + } 25 + 26 + impl<T> Clone for List<T> { 27 + fn clone(&self) -> Self { 28 + Self { 29 + list: self.list.clone(), 30 + _phantom: PhantomData, 31 + } 32 + } 33 + } 34 + 35 + impl<T> List<T> { 36 + /// Creates a typed view from a [`LoroList`]. 37 + pub fn new(list: LoroList) -> Self { 38 + Self { 39 + list, 40 + _phantom: PhantomData, 41 + } 42 + } 43 + 44 + /// Returns the number of elements. 45 + pub fn len(&self) -> usize { 46 + self.list.len() 47 + } 48 + 49 + /// Returns `true` if the list is empty. 50 + pub fn is_empty(&self) -> bool { 51 + self.list.len() == 0 52 + } 53 + 54 + /// Returns a reference to the underlying [`LoroList`]. 55 + pub fn raw(&self) -> &LoroList { 56 + &self.list 57 + } 58 + } 59 + 60 + // --- #[loroscope] structs ---------------------------------------------------- 61 + 62 + impl<T: FromLoroMap> List<T> { 63 + /// Returns the element at `index`, or `None` if out of bounds. 64 + pub fn get(&self, index: usize) -> Option<T> { 65 + self.list 66 + .get(index) 67 + .and_then(|v| v.into_container().ok()) 68 + .and_then(|c| c.into_map().ok()) 69 + .map(T::from_loro_map) 70 + } 71 + 72 + /// Returns an iterator over all elements. 73 + pub fn iter(&self) -> impl Iterator<Item = T> + '_ { 74 + (0..self.len()).map(|i| self.get(i).expect("index is within bounds")) 75 + } 76 + 77 + /// Appends a new element and returns it. 78 + pub fn push_new(&self) -> T { 79 + T::from_loro_map( 80 + self.list 81 + .push_container(LoroMap::new()) 82 + .expect("push to attached list"), 83 + ) 84 + } 85 + } 86 + 87 + // --- Primitives -------------------------------------------------------------- 88 + 89 + macro_rules! impl_primitive { 90 + ($ty:ty, $variant:pat => $expr:expr) => { 91 + impl List<$ty> { 92 + /// Returns the element at `index`, or `None` if out of bounds. 93 + pub fn get(&self, index: usize) -> Option<$ty> { 94 + self.list 95 + .get(index) 96 + .and_then(|v| v.into_value().ok()) 97 + .and_then(|v| match v { 98 + $variant => Some($expr), 99 + _ => None, 100 + }) 101 + } 102 + 103 + /// Returns an iterator over all elements. 104 + pub fn iter(&self) -> impl Iterator<Item = $ty> + '_ { 105 + (0..self.len()).map(|i| self.get(i).expect("index is within bounds")) 106 + } 107 + } 108 + }; 109 + } 110 + 111 + impl_primitive!(f64, LoroValue::Double(d) => d); 112 + impl_primitive!(i64, LoroValue::I64(i) => i); 113 + impl_primitive!(bool, LoroValue::Bool(b) => b); 114 + impl_primitive!(String, LoroValue::String(s) => s.to_string()); 115 + 116 + macro_rules! impl_push { 117 + ($($value_ty:ty => $param_ty:ty),* $(,)?) => {$( 118 + impl List<$value_ty> { 119 + /// Appends a value to the end of the list. 120 + pub fn push(&self, val: $param_ty) { 121 + self.list.push(val).expect("push to attached list"); 122 + } 123 + } 124 + )*}; 125 + } 126 + 127 + impl_push!(f64 => f64, i64 => i64, bool => bool, String => &str); 128 + 129 + // --- Loro containers --------------------------------------------------------- 130 + 131 + macro_rules! impl_container { 132 + ($ty:ty, $variant:ident) => { 133 + impl List<$ty> { 134 + /// Returns the container at `index`, or `None` if out of bounds. 135 + pub fn get(&self, index: usize) -> Option<$ty> { 136 + self.list 137 + .get(index) 138 + .and_then(|v| v.into_container().ok()) 139 + .and_then(|c| c.$variant().ok()) 140 + } 141 + 142 + /// Appends a new empty container and returns it. 143 + pub fn push_new(&self) -> $ty { 144 + self.list 145 + .push_container(<$ty>::new()) 146 + .expect("push to attached list") 147 + } 148 + 149 + /// Returns an iterator over all elements. 150 + pub fn iter(&self) -> impl Iterator<Item = $ty> + '_ { 151 + (0..self.len()).map(|i| self.get(i).expect("index is within bounds")) 152 + } 153 + } 154 + }; 155 + } 156 + 157 + impl_container!(loro::LoroText, into_text); 158 + impl_container!(loro::LoroCounter, into_counter); 159 + impl_container!(loro::LoroTree, into_tree);
+176
loroscope/src/map.rs
··· 1 + use std::marker::PhantomData; 2 + 3 + use loro::{LoroList, LoroMap, LoroMovableList, LoroTree, LoroValue}; 4 + 5 + use crate::FromLoroMap; 6 + 7 + /// A typed string-keyed map where every value is of type `V`. 8 + /// 9 + /// Use this as a field type in a `#[loroscope]` struct: 10 + /// 11 + /// ```ignore 12 + /// #[loroscope] 13 + /// struct Settings { 14 + /// values: Map<i64>, 15 + /// } 16 + /// ``` 17 + /// 18 + /// `V` can be a `#[loroscope]` struct, a primitive (`f64`, `i64`, `bool`, 19 + /// `String`), a Loro container type (`LoroText`, `LoroCounter`, `LoroTree`), 20 + /// or another collection ([`List<T>`](crate::List), 21 + /// [`MovableList<T>`](crate::MovableList), `Map<V>`). 22 + pub struct Map<V> { 23 + map: LoroMap, 24 + _phantom: PhantomData<V>, 25 + } 26 + 27 + impl<V> Clone for Map<V> { 28 + fn clone(&self) -> Self { 29 + Self { 30 + map: self.map.clone(), 31 + _phantom: PhantomData, 32 + } 33 + } 34 + } 35 + 36 + impl<V> Map<V> { 37 + /// Creates a typed view from a [`LoroMap`]. 38 + pub fn new(map: LoroMap) -> Self { 39 + Self { 40 + map, 41 + _phantom: PhantomData, 42 + } 43 + } 44 + 45 + /// Returns the number of entries. 46 + pub fn len(&self) -> usize { 47 + self.map.len() 48 + } 49 + 50 + /// Returns `true` if the map is empty. 51 + pub fn is_empty(&self) -> bool { 52 + self.map.len() == 0 53 + } 54 + 55 + /// Returns a reference to the underlying [`LoroMap`]. 56 + pub fn raw(&self) -> &LoroMap { 57 + &self.map 58 + } 59 + } 60 + 61 + // --- #[loroscope] structs ---------------------------------------------------- 62 + 63 + impl<V: FromLoroMap> Map<V> { 64 + /// Returns the value for `key`, or `None` if the key is missing. 65 + pub fn get(&self, key: &str) -> Option<V> { 66 + self.map 67 + .get(key) 68 + .and_then(|v| v.into_container().ok()) 69 + .and_then(|c| c.into_map().ok()) 70 + .map(V::from_loro_map) 71 + } 72 + 73 + /// Returns the value for `key`, creating it if it doesn't exist. 74 + pub fn get_or_create(&self, key: &str) -> V { 75 + V::from_loro_map( 76 + self.map 77 + .get_or_create_container(key, LoroMap::new()) 78 + .expect("create container on attached map"), 79 + ) 80 + } 81 + } 82 + 83 + // --- Primitives -------------------------------------------------------------- 84 + 85 + macro_rules! impl_primitive { 86 + ($ty:ty, $variant:pat => $expr:expr) => { 87 + impl Map<$ty> { 88 + /// Returns the value for `key`, or `None` if missing. 89 + pub fn get(&self, key: &str) -> Option<$ty> { 90 + self.map 91 + .get(key) 92 + .and_then(|v| v.into_value().ok()) 93 + .and_then(|v| match v { 94 + $variant => Some($expr), 95 + _ => None, 96 + }) 97 + } 98 + } 99 + }; 100 + } 101 + 102 + impl_primitive!(f64, LoroValue::Double(d) => d); 103 + impl_primitive!(i64, LoroValue::I64(i) => i); 104 + impl_primitive!(bool, LoroValue::Bool(b) => b); 105 + impl_primitive!(String, LoroValue::String(s) => s.to_string()); 106 + 107 + macro_rules! impl_insert { 108 + ($($value_ty:ty => $param_ty:ty),* $(,)?) => {$( 109 + impl Map<$value_ty> { 110 + /// Inserts a value for the given key, overwriting any previous value. 111 + pub fn insert(&self, key: &str, val: $param_ty) { 112 + self.map.insert(key, val).expect("insert into attached map"); 113 + } 114 + } 115 + )*}; 116 + } 117 + 118 + impl_insert!(f64 => f64, i64 => i64, bool => bool, String => &str); 119 + 120 + // --- Loro containers --------------------------------------------------------- 121 + 122 + macro_rules! impl_container { 123 + ($ty:ty, $variant:ident) => { 124 + impl Map<$ty> { 125 + /// Returns the container for `key`, or `None` if missing. 126 + pub fn get(&self, key: &str) -> Option<$ty> { 127 + self.map 128 + .get(key) 129 + .and_then(|v| v.into_container().ok()) 130 + .and_then(|c| c.$variant().ok()) 131 + } 132 + 133 + /// Returns the container for `key`, creating it if it doesn't exist. 134 + pub fn get_or_create(&self, key: &str) -> $ty { 135 + self.map 136 + .get_or_create_container(key, <$ty>::new()) 137 + .expect("create container on attached map") 138 + } 139 + } 140 + }; 141 + } 142 + 143 + impl_container!(loro::LoroText, into_text); 144 + impl_container!(loro::LoroCounter, into_counter); 145 + impl_container!(loro::LoroTree, into_tree); 146 + 147 + // --- Wrapper types ----------------------------------------------------------- 148 + 149 + macro_rules! impl_wrapper { 150 + ($wrapper:ident < $($param:ident),+ >, $loro_ty:ty, $variant:ident) => { 151 + impl<$($param),+> Map<crate::$wrapper<$($param),+>> { 152 + /// Returns the value for `key`, or `None` if missing. 153 + pub fn get(&self, key: &str) -> Option<crate::$wrapper<$($param),+>> { 154 + self.map 155 + .get(key) 156 + .and_then(|v| v.into_container().ok()) 157 + .and_then(|c| c.$variant().ok()) 158 + .map(crate::$wrapper::new) 159 + } 160 + 161 + /// Returns the value for `key`, creating it if it doesn't exist. 162 + pub fn get_or_create(&self, key: &str) -> crate::$wrapper<$($param),+> { 163 + crate::$wrapper::new( 164 + self.map 165 + .get_or_create_container(key, <$loro_ty>::new()) 166 + .expect("create container on attached map"), 167 + ) 168 + } 169 + } 170 + }; 171 + } 172 + 173 + impl_wrapper!(List<T>, LoroList, into_list); 174 + impl_wrapper!(MovableList<T>, LoroMovableList, into_movable_list); 175 + impl_wrapper!(Map<V>, LoroMap, into_map); 176 + impl_wrapper!(Tree<T>, LoroTree, into_tree);
+172
loroscope/src/movable_list.rs
··· 1 + use std::marker::PhantomData; 2 + 3 + use loro::{LoroMap, LoroMovableList, LoroValue}; 4 + 5 + use crate::FromLoroMap; 6 + 7 + /// A typed list that supports reordering and in-place updates. 8 + /// 9 + /// Unlike [`List`](crate::List), elements can be moved with [`mov`](Self::mov) 10 + /// and replaced with `set`. 11 + /// 12 + /// Use this as a field type in a `#[loroscope]` struct: 13 + /// 14 + /// ```ignore 15 + /// #[loroscope] 16 + /// struct Playlist { 17 + /// songs: MovableList<Song>, 18 + /// } 19 + /// ``` 20 + /// 21 + /// `T` can be a `#[loroscope]` struct, a primitive (`f64`, `i64`, `bool`, 22 + /// `String`), or a Loro container type (`LoroText`, `LoroCounter`, 23 + /// `LoroTree`). 24 + pub struct MovableList<T> { 25 + list: LoroMovableList, 26 + _phantom: PhantomData<T>, 27 + } 28 + 29 + impl<T> Clone for MovableList<T> { 30 + fn clone(&self) -> Self { 31 + Self { 32 + list: self.list.clone(), 33 + _phantom: PhantomData, 34 + } 35 + } 36 + } 37 + 38 + impl<T> MovableList<T> { 39 + /// Creates a typed view from a [`LoroMovableList`]. 40 + pub fn new(list: LoroMovableList) -> Self { 41 + Self { 42 + list, 43 + _phantom: PhantomData, 44 + } 45 + } 46 + 47 + /// Returns the number of elements. 48 + pub fn len(&self) -> usize { 49 + self.list.len() 50 + } 51 + 52 + /// Returns `true` if the list is empty. 53 + pub fn is_empty(&self) -> bool { 54 + self.list.len() == 0 55 + } 56 + 57 + /// Returns a reference to the underlying [`LoroMovableList`]. 58 + pub fn raw(&self) -> &LoroMovableList { 59 + &self.list 60 + } 61 + 62 + /// Moves the element at index `from` to index `to`. 63 + pub fn mov(&self, from: usize, to: usize) { 64 + self.list.mov(from, to).expect("mov on attached list"); 65 + } 66 + } 67 + 68 + // --- #[loroscope] structs ---------------------------------------------------- 69 + 70 + impl<T: FromLoroMap> MovableList<T> { 71 + /// Returns the element at `index`, or `None` if out of bounds. 72 + pub fn get(&self, index: usize) -> Option<T> { 73 + self.list 74 + .get(index) 75 + .and_then(|v| v.into_container().ok()) 76 + .and_then(|c| c.into_map().ok()) 77 + .map(T::from_loro_map) 78 + } 79 + 80 + /// Returns an iterator over all elements. 81 + pub fn iter(&self) -> impl Iterator<Item = T> + '_ { 82 + (0..self.len()).map(|i| self.get(i).expect("index is within bounds")) 83 + } 84 + 85 + /// Appends a new element and returns it. 86 + pub fn push_new(&self) -> T { 87 + T::from_loro_map( 88 + self.list 89 + .push_container(LoroMap::new()) 90 + .expect("push to attached list"), 91 + ) 92 + } 93 + } 94 + 95 + // --- Primitives -------------------------------------------------------------- 96 + 97 + macro_rules! impl_primitive { 98 + ($ty:ty, $variant:pat => $expr:expr) => { 99 + impl MovableList<$ty> { 100 + /// Returns the element at `index`, or `None` if out of bounds. 101 + pub fn get(&self, index: usize) -> Option<$ty> { 102 + self.list 103 + .get(index) 104 + .and_then(|v| v.into_value().ok()) 105 + .and_then(|v| match v { 106 + $variant => Some($expr), 107 + _ => None, 108 + }) 109 + } 110 + 111 + /// Returns an iterator over all elements. 112 + pub fn iter(&self) -> impl Iterator<Item = $ty> + '_ { 113 + (0..self.len()).map(|i| self.get(i).expect("index is within bounds")) 114 + } 115 + } 116 + }; 117 + } 118 + 119 + impl_primitive!(f64, LoroValue::Double(d) => d); 120 + impl_primitive!(i64, LoroValue::I64(i) => i); 121 + impl_primitive!(bool, LoroValue::Bool(b) => b); 122 + impl_primitive!(String, LoroValue::String(s) => s.to_string()); 123 + 124 + macro_rules! impl_push_set { 125 + ($($value_ty:ty => $param_ty:ty),* $(,)?) => {$( 126 + impl MovableList<$value_ty> { 127 + /// Appends a value to the end of the list. 128 + pub fn push(&self, val: $param_ty) { 129 + self.list.push(val).expect("push to attached list"); 130 + } 131 + 132 + /// Replaces the value at `index`. 133 + pub fn set(&self, index: usize, val: $param_ty) { 134 + self.list.set(index, val).expect("set on attached list"); 135 + } 136 + } 137 + )*}; 138 + } 139 + 140 + impl_push_set!(f64 => f64, i64 => i64, bool => bool, String => &str); 141 + 142 + // --- Loro containers --------------------------------------------------------- 143 + 144 + macro_rules! impl_container { 145 + ($ty:ty, $variant:ident) => { 146 + impl MovableList<$ty> { 147 + /// Returns the container at `index`, or `None` if out of bounds. 148 + pub fn get(&self, index: usize) -> Option<$ty> { 149 + self.list 150 + .get(index) 151 + .and_then(|v| v.into_container().ok()) 152 + .and_then(|c| c.$variant().ok()) 153 + } 154 + 155 + /// Appends a new empty container and returns it. 156 + pub fn push_new(&self) -> $ty { 157 + self.list 158 + .push_container(<$ty>::new()) 159 + .expect("push to attached list") 160 + } 161 + 162 + /// Returns an iterator over all elements. 163 + pub fn iter(&self) -> impl Iterator<Item = $ty> + '_ { 164 + (0..self.len()).map(|i| self.get(i).expect("index is within bounds")) 165 + } 166 + } 167 + }; 168 + } 169 + 170 + impl_container!(loro::LoroText, into_text); 171 + impl_container!(loro::LoroCounter, into_counter); 172 + impl_container!(loro::LoroTree, into_tree);
+192
loroscope/src/tree.rs
··· 1 + use std::marker::PhantomData; 2 + 3 + use loro::{LoroTree, TreeID, TreeParentId}; 4 + 5 + use crate::FromLoroMap; 6 + 7 + /// A typed tree (hierarchy) where each node's metadata is of type `T`. 8 + /// 9 + /// Use this as a field type in a `#[loroscope]` struct: 10 + /// 11 + /// ```ignore 12 + /// #[loroscope] 13 + /// struct FileNode { 14 + /// name: Text, 15 + /// size: i64, 16 + /// } 17 + /// 18 + /// #[loroscope] 19 + /// struct Project { 20 + /// files: Tree<FileNode>, 21 + /// } 22 + /// ``` 23 + /// 24 + /// Each tree node has a [`TreeID`] and an associated metadata map that is 25 + /// converted to `T` via [`FromLoroMap`]. Structural operations (move, delete, 26 + /// parent/child queries) are available on all `Tree<T>`, while typed metadata 27 + /// access (`get`, `create_root`, etc.) requires `T: FromLoroMap`. 28 + /// 29 + /// Fractional indexing is enabled by default so positional methods 30 + /// ([`mov_to`](Self::mov_to), [`mov_after`](Self::mov_after), 31 + /// [`mov_before`](Self::mov_before), [`create_at`](Self::create_at)) 32 + /// always work. 33 + pub struct Tree<T> { 34 + tree: LoroTree, 35 + _phantom: PhantomData<T>, 36 + } 37 + 38 + impl<T> Clone for Tree<T> { 39 + fn clone(&self) -> Self { 40 + Self { 41 + tree: self.tree.clone(), 42 + _phantom: PhantomData, 43 + } 44 + } 45 + } 46 + 47 + impl<T> Tree<T> { 48 + /// Creates a typed view from a [`LoroTree`]. 49 + /// 50 + /// Enables fractional indexing (jitter=0) so that positional operations 51 + /// are always available. 52 + pub fn new(tree: LoroTree) -> Self { 53 + tree.enable_fractional_index(0); 54 + Self { 55 + tree, 56 + _phantom: PhantomData, 57 + } 58 + } 59 + 60 + /// Returns `true` if the tree has no (non-deleted) root nodes. 61 + pub fn is_empty(&self) -> bool { 62 + self.tree.roots().is_empty() 63 + } 64 + 65 + /// Returns a reference to the underlying [`LoroTree`]. 66 + pub fn raw(&self) -> &LoroTree { 67 + &self.tree 68 + } 69 + 70 + /// Returns all root node IDs (excluding deleted nodes). 71 + pub fn roots(&self) -> Vec<TreeID> { 72 + self.tree.roots() 73 + } 74 + 75 + /// Returns the children of `parent`, or `None` if the node doesn't exist. 76 + pub fn children(&self, parent: TreeID) -> Option<Vec<TreeID>> { 77 + self.tree.children(parent) 78 + } 79 + 80 + /// Returns the number of children of `parent`, or `None` if the node 81 + /// doesn't exist. 82 + pub fn children_num(&self, parent: TreeID) -> Option<usize> { 83 + self.tree.children_num(parent) 84 + } 85 + 86 + /// Returns the parent of `target`, or `None` if the node doesn't exist. 87 + pub fn parent(&self, target: TreeID) -> Option<TreeParentId> { 88 + self.tree.parent(target) 89 + } 90 + 91 + /// Returns whether the tree contains `target` (including deleted nodes). 92 + pub fn contains(&self, target: TreeID) -> bool { 93 + self.tree.contains(target) 94 + } 95 + 96 + /// Marks a node as deleted. 97 + pub fn delete(&self, target: TreeID) { 98 + self.tree.delete(target).expect("delete on attached tree"); 99 + } 100 + 101 + /// Moves `target` to be a child of `parent`. 102 + pub fn mov(&self, target: TreeID, parent: TreeID) { 103 + self.tree.mov(target, parent).expect("mov on attached tree"); 104 + } 105 + 106 + /// Moves `target` to be a root node. 107 + pub fn mov_to_root(&self, target: TreeID) { 108 + self.tree 109 + .mov(target, TreeParentId::Root) 110 + .expect("mov to root on attached tree"); 111 + } 112 + 113 + /// Moves `target` to position `index` under `parent`. 114 + pub fn mov_to(&self, target: TreeID, parent: TreeID, index: usize) { 115 + self.tree 116 + .mov_to(target, parent, index) 117 + .expect("mov_to on attached tree"); 118 + } 119 + 120 + /// Moves `target` to the position immediately after `after`. 121 + pub fn mov_after(&self, target: TreeID, after: TreeID) { 122 + self.tree 123 + .mov_after(target, after) 124 + .expect("mov_after on attached tree"); 125 + } 126 + 127 + /// Moves `target` to the position immediately before `before`. 128 + pub fn mov_before(&self, target: TreeID, before: TreeID) { 129 + self.tree 130 + .mov_before(target, before) 131 + .expect("mov_before on attached tree"); 132 + } 133 + } 134 + 135 + // --- Typed metadata access (requires FromLoroMap) ---------------------------- 136 + 137 + impl<T: FromLoroMap> Tree<T> { 138 + /// Returns the typed metadata for `id`, or `None` if the node doesn't 139 + /// exist or has been deleted. 140 + pub fn get(&self, id: TreeID) -> Option<T> { 141 + // Only return data for nodes that are live (have a non-Deleted parent). 142 + let parent = self.tree.parent(id)?; 143 + if parent == TreeParentId::Deleted { 144 + return None; 145 + } 146 + self.tree.get_meta(id).ok().map(T::from_loro_map) 147 + } 148 + 149 + /// Creates a new root node and returns its ID together with the typed 150 + /// metadata view. 151 + pub fn create_root(&self) -> (TreeID, T) { 152 + let id = self 153 + .tree 154 + .create(TreeParentId::Root) 155 + .expect("create root on attached tree"); 156 + let meta = self 157 + .tree 158 + .get_meta(id) 159 + .expect("get_meta on just-created node"); 160 + (id, T::from_loro_map(meta)) 161 + } 162 + 163 + /// Creates a new child node under `parent` and returns its ID together 164 + /// with the typed metadata view. 165 + pub fn create_child(&self, parent: TreeID) -> (TreeID, T) { 166 + let id = self 167 + .tree 168 + .create(parent) 169 + .expect("create child on attached tree"); 170 + let meta = self 171 + .tree 172 + .get_meta(id) 173 + .expect("get_meta on just-created node"); 174 + (id, T::from_loro_map(meta)) 175 + } 176 + 177 + /// Creates a new child node at position `index` under `parent` and 178 + /// returns its ID together with the typed metadata view. 179 + /// 180 + /// Requires fractional indexing to be enabled (it is by default). 181 + pub fn create_at(&self, parent: TreeID, index: usize) -> (TreeID, T) { 182 + let id = self 183 + .tree 184 + .create_at(parent, index) 185 + .expect("create_at on attached tree"); 186 + let meta = self 187 + .tree 188 + .get_meta(id) 189 + .expect("get_meta on just-created node"); 190 + (id, T::from_loro_map(meta)) 191 + } 192 + }
+420
loroscope/tests/integration.rs
··· 1 + use loroscope::loroscope; 2 + 3 + #[loroscope] 4 + struct NpcStats { 5 + hp: i64, 6 + attack: i64, 7 + defense: i64, 8 + } 9 + 10 + #[loroscope] 11 + struct Npc { 12 + name: Text, 13 + x: f64, 14 + y: f64, 15 + hp: i64, 16 + visible: bool, 17 + stats: NpcStats, 18 + } 19 + 20 + #[loroscope] 21 + struct Project { 22 + npcs: List<Npc>, 23 + name: Text, 24 + settings: Map<i64>, 25 + } 26 + 27 + // ---- Test 1: Primitive fields ---- 28 + 29 + #[test] 30 + fn primitive_fields() { 31 + let project = Project::new(); 32 + 33 + let npc = project.npcs().push_new(); 34 + npc.set_x(42.0); 35 + npc.set_y(100.0); 36 + npc.set_hp(20); 37 + npc.set_visible(true); 38 + 39 + assert_eq!(npc.x(), 42.0); 40 + assert_eq!(npc.y(), 100.0); 41 + assert_eq!(npc.hp(), 20); 42 + assert!(npc.visible()); 43 + } 44 + 45 + // ---- Test 2: Nested structs ---- 46 + 47 + #[test] 48 + fn nested_structs() { 49 + let project = Project::new(); 50 + 51 + let npc = project.npcs().push_new(); 52 + let stats = npc.stats(); 53 + stats.set_hp(100); 54 + stats.set_attack(50); 55 + stats.set_defense(30); 56 + 57 + assert_eq!(stats.hp(), 100); 58 + assert_eq!(stats.attack(), 50); 59 + assert_eq!(stats.defense(), 30); 60 + } 61 + 62 + // ---- Test 3: Default values (unwrap_or_default) ---- 63 + 64 + #[test] 65 + fn default_values() { 66 + let project = Project::new(); 67 + 68 + let npc = project.npcs().push_new(); 69 + assert_eq!(npc.x(), 0.0); 70 + assert_eq!(npc.y(), 0.0); 71 + assert_eq!(npc.hp(), 0); 72 + assert!(!npc.visible()); 73 + } 74 + 75 + // ---- Test 4: Shared views ---- 76 + 77 + #[test] 78 + fn shared_views() { 79 + let project1 = Project::new(); 80 + let doc = project1.doc(); 81 + let project2 = Project::from(doc.get_map("Project")); 82 + 83 + let npc = project1.npcs().push_new(); 84 + npc.set_hp(42); 85 + doc.commit(); 86 + 87 + let npc2 = project2.npcs().get(0).unwrap(); 88 + assert_eq!(npc2.hp(), 42); 89 + } 90 + 91 + // ---- Test 5: Undo manager ---- 92 + 93 + #[test] 94 + fn undo_manager() { 95 + let project = Project::new(); 96 + let doc = project.doc(); 97 + 98 + let npc = project.npcs().push_new(); 99 + npc.set_hp(10); 100 + doc.commit(); 101 + 102 + let mut undo = loro::UndoManager::new(&doc); 103 + 104 + npc.set_hp(99); 105 + doc.commit(); 106 + assert_eq!(npc.hp(), 99); 107 + 108 + undo.undo().unwrap(); 109 + assert_eq!(npc.hp(), 10); 110 + } 111 + 112 + // ---- Test 6: List operations ---- 113 + 114 + #[test] 115 + fn list_operations() { 116 + let project = Project::new(); 117 + 118 + let npc1 = project.npcs().push_new(); 119 + npc1.set_hp(10); 120 + let npc2 = project.npcs().push_new(); 121 + npc2.set_hp(20); 122 + 123 + assert_eq!(project.npcs().len(), 2); 124 + assert!(!project.npcs().is_empty()); 125 + assert_eq!(project.npcs().get(0).unwrap().hp(), 10); 126 + assert_eq!(project.npcs().get(1).unwrap().hp(), 20); 127 + 128 + let hps: Vec<i64> = project.npcs().iter().map(|n: Npc| n.hp()).collect(); 129 + assert_eq!(hps, vec![10, 20]); 130 + } 131 + 132 + // ---- Test 7: Map operations ---- 133 + 134 + #[test] 135 + fn map_operations() { 136 + let project = Project::new(); 137 + 138 + project.settings().insert("volume", 80); 139 + project.settings().insert("brightness", 50); 140 + 141 + assert_eq!(project.settings().get("volume"), Some(80)); 142 + assert_eq!(project.settings().get("brightness"), Some(50)); 143 + assert_eq!(project.settings().get("nonexistent"), None); 144 + assert_eq!(project.settings().len(), 2); 145 + } 146 + 147 + // ---- Test 8: Raw Loro types ---- 148 + 149 + #[loroscope] 150 + struct RawExample { 151 + items: LoroList, 152 + metadata: LoroMap, 153 + notes: LoroText, 154 + } 155 + 156 + #[test] 157 + fn raw_loro_types() { 158 + let example = RawExample::new(); 159 + 160 + // LoroList — user handles types themselves 161 + example.items().push(42).unwrap(); 162 + example.items().push("hello").unwrap(); 163 + assert_eq!(example.items().len(), 2); 164 + 165 + // LoroMap — untyped key-value pairs 166 + example.metadata().insert("key", "value").unwrap(); 167 + assert_eq!(example.metadata().len(), 1); 168 + 169 + // LoroText 170 + example.notes().insert(0, "hello world").unwrap(); 171 + assert_eq!(example.notes().to_string(), "hello world"); 172 + } 173 + 174 + // ---- Test 9: raw() and doc() on generated structs ---- 175 + 176 + #[test] 177 + fn raw_and_doc_accessor() { 178 + let project = Project::new(); 179 + 180 + // All structs expose &LoroMap via raw() 181 + let _raw_map: &loro::LoroMap = project.raw(); 182 + 183 + // doc() returns the owning LoroDoc 184 + let _doc: loro::LoroDoc = project.doc(); 185 + 186 + // Nested struct also has raw() and doc() 187 + let npc = project.npcs().push_new(); 188 + let _raw_map: &loro::LoroMap = npc.raw(); 189 + let _doc: loro::LoroDoc = npc.doc(); 190 + 191 + // Can use raw() to do things the typed API doesn't cover 192 + npc.raw().insert("custom_field", 999).unwrap(); 193 + } 194 + 195 + // ---- Test 10: derive pass-through and custom Debug ---- 196 + 197 + #[loroscope] 198 + #[derive(Debug, Clone)] 199 + struct SimplePoint { 200 + x: f64, 201 + y: f64, 202 + } 203 + 204 + #[test] 205 + fn derive_debug() { 206 + let point = SimplePoint::new(); 207 + point.set_x(1.5); 208 + point.set_y(2.5); 209 + 210 + let debug_str = format!("{:?}", point); 211 + assert!(debug_str.contains("SimplePoint")); 212 + assert!(debug_str.contains("x: 1.5")); 213 + assert!(debug_str.contains("y: 2.5")); 214 + } 215 + 216 + #[test] 217 + fn derive_clone() { 218 + let point = SimplePoint::new(); 219 + let point2 = point.clone(); 220 + point.set_x(42.0); 221 + // Clone shares the same underlying CRDT, so both see the change 222 + assert_eq!(point2.x(), 42.0); 223 + } 224 + 225 + #[test] 226 + fn sync_export_import() { 227 + use loro::ExportMode; 228 + 229 + // Alice creates a document and adds an NPC. 230 + let alice = Project::new(); 231 + let alice_doc = alice.doc(); 232 + let npc = alice.npcs().push_new(); 233 + npc.set_hp(10); 234 + alice_doc.commit(); 235 + 236 + // Bob starts from Alice's document. 237 + let bob = Project::new(); 238 + let bob_doc = bob.doc(); 239 + bob_doc 240 + .import(&alice_doc.export(ExportMode::all_updates()).unwrap()) 241 + .unwrap(); 242 + 243 + // Both edit concurrently. 244 + let npc = alice.npcs().push_new(); 245 + npc.set_hp(20); 246 + alice_doc.commit(); 247 + 248 + let npc = bob.npcs().push_new(); 249 + npc.set_hp(30); 250 + bob_doc.commit(); 251 + 252 + // Sync — all 3 NPCs appear in both documents. 253 + bob_doc 254 + .import(&alice_doc.export(ExportMode::all_updates()).unwrap()) 255 + .unwrap(); 256 + alice_doc 257 + .import(&bob_doc.export(ExportMode::all_updates()).unwrap()) 258 + .unwrap(); 259 + 260 + assert_eq!(alice.npcs().len(), 3); 261 + assert_eq!(bob.npcs().len(), 3); 262 + } 263 + 264 + // ---- Tree tests ---- 265 + 266 + #[loroscope] 267 + struct FileNode { 268 + name: Text, 269 + size: i64, 270 + } 271 + 272 + #[loroscope] 273 + struct FileSystem { 274 + files: Tree<FileNode>, 275 + } 276 + 277 + #[test] 278 + fn tree_create_and_get() { 279 + let fs = FileSystem::new(); 280 + let tree = fs.files(); 281 + 282 + let (root_id, root) = tree.create_root(); 283 + root.name().insert(0, "root").unwrap(); 284 + root.set_size(100); 285 + 286 + let retrieved = tree.get(root_id).unwrap(); 287 + assert_eq!(retrieved.name().to_string(), "root"); 288 + assert_eq!(retrieved.size(), 100); 289 + } 290 + 291 + #[test] 292 + fn tree_structure_queries() { 293 + let fs = FileSystem::new(); 294 + let tree = fs.files(); 295 + 296 + let (root_id, root) = tree.create_root(); 297 + root.name().insert(0, "root").unwrap(); 298 + 299 + let (child1_id, child1) = tree.create_child(root_id); 300 + child1.name().insert(0, "child1").unwrap(); 301 + 302 + let (child2_id, child2) = tree.create_child(root_id); 303 + child2.name().insert(0, "child2").unwrap(); 304 + 305 + // roots 306 + assert_eq!(tree.roots(), vec![root_id]); 307 + assert!(!tree.is_empty()); 308 + 309 + // children 310 + let children = tree.children(root_id).unwrap(); 311 + assert_eq!(children.len(), 2); 312 + assert!(children.contains(&child1_id)); 313 + assert!(children.contains(&child2_id)); 314 + 315 + assert_eq!(tree.children_num(root_id), Some(2)); 316 + 317 + // parent 318 + assert_eq!( 319 + tree.parent(child1_id), 320 + Some(loroscope::TreeParentId::Node(root_id)) 321 + ); 322 + assert_eq!(tree.parent(root_id), Some(loroscope::TreeParentId::Root)); 323 + 324 + // contains 325 + assert!(tree.contains(root_id)); 326 + assert!(tree.contains(child1_id)); 327 + } 328 + 329 + #[test] 330 + fn tree_move_operations() { 331 + let fs = FileSystem::new(); 332 + let tree = fs.files(); 333 + 334 + let (root1_id, _) = tree.create_root(); 335 + let (root2_id, _) = tree.create_root(); 336 + let (child_id, _) = tree.create_child(root1_id); 337 + 338 + // Move child from root1 to root2 339 + tree.mov(child_id, root2_id); 340 + assert_eq!( 341 + tree.parent(child_id), 342 + Some(loroscope::TreeParentId::Node(root2_id)) 343 + ); 344 + assert_eq!(tree.children_num(root1_id), Some(0)); 345 + assert_eq!(tree.children_num(root2_id), Some(1)); 346 + 347 + // Move child to root 348 + tree.mov_to_root(child_id); 349 + assert_eq!(tree.parent(child_id), Some(loroscope::TreeParentId::Root)); 350 + assert_eq!(tree.roots().len(), 3); 351 + } 352 + 353 + #[test] 354 + fn tree_positional_operations() { 355 + let fs = FileSystem::new(); 356 + let tree = fs.files(); 357 + 358 + let (root_id, _) = tree.create_root(); 359 + let (a_id, a) = tree.create_child(root_id); 360 + a.name().insert(0, "a").unwrap(); 361 + let (b_id, b) = tree.create_child(root_id); 362 + b.name().insert(0, "b").unwrap(); 363 + 364 + // create_at inserts at position 0 (before both) 365 + let (c_id, c) = tree.create_at(root_id, 0); 366 + c.name().insert(0, "c").unwrap(); 367 + 368 + let children = tree.children(root_id).unwrap(); 369 + assert_eq!(children[0], c_id); 370 + 371 + // mov_to: move b to position 0 372 + tree.mov_to(b_id, root_id, 0); 373 + let children = tree.children(root_id).unwrap(); 374 + assert_eq!(children[0], b_id); 375 + 376 + // mov_after: move a after c 377 + tree.mov_after(a_id, c_id); 378 + let children = tree.children(root_id).unwrap(); 379 + let c_pos = children.iter().position(|id| *id == c_id).unwrap(); 380 + let a_pos = children.iter().position(|id| *id == a_id).unwrap(); 381 + assert_eq!(a_pos, c_pos + 1); 382 + 383 + // mov_before: move b before c 384 + tree.mov_before(b_id, c_id); 385 + let children = tree.children(root_id).unwrap(); 386 + let b_pos = children.iter().position(|id| *id == b_id).unwrap(); 387 + let c_pos = children.iter().position(|id| *id == c_id).unwrap(); 388 + assert_eq!(b_pos + 1, c_pos); 389 + } 390 + 391 + #[test] 392 + fn tree_delete() { 393 + let fs = FileSystem::new(); 394 + let tree = fs.files(); 395 + 396 + let (root_id, _) = tree.create_root(); 397 + let (child_id, _) = tree.create_child(root_id); 398 + 399 + tree.delete(child_id); 400 + 401 + // get returns None for deleted nodes 402 + assert!(tree.get(child_id).is_none()); 403 + 404 + // children excludes deleted nodes 405 + assert_eq!(tree.children_num(root_id), Some(0)); 406 + } 407 + 408 + #[test] 409 + fn tree_get_nonexistent() { 410 + let fs = FileSystem::new(); 411 + let tree = fs.files(); 412 + 413 + // Fabricate a TreeID that doesn't exist in this tree 414 + let fake_id = loroscope::TreeID { 415 + peer: 999, 416 + counter: 999, 417 + }; 418 + assert!(tree.get(fake_id).is_none()); 419 + assert!(!tree.contains(fake_id)); 420 + }
+12
loroscope_macros/Cargo.toml
··· 1 + [package] 2 + name = "loroscope_macros" 3 + version = "0.1.0" 4 + edition = "2024" 5 + 6 + [lib] 7 + proc-macro = true 8 + 9 + [dependencies] 10 + syn = { version = "2", features = ["full"] } 11 + quote = "1" 12 + proc-macro2 = "1"
+517
loroscope_macros/src/lib.rs
··· 1 + //! Proc-macro implementation for the `loroscope` crate. 2 + //! 3 + //! Depend on [`loroscope`](https://docs.rs/loroscope) rather than using this 4 + //! crate directly. 5 + 6 + #![deny(missing_docs)] 7 + #![deny(clippy::unwrap_used)] 8 + 9 + use proc_macro::TokenStream; 10 + use quote::quote; 11 + use syn::punctuated::Punctuated; 12 + use syn::{GenericArgument, Ident, ItemStruct, Meta, PathArguments, Type, parse_macro_input}; 13 + 14 + /// Defines a typed struct over a Loro CRDT document. 15 + /// 16 + /// # Example 17 + /// 18 + /// ```ignore 19 + /// use loroscope::loroscope; 20 + /// 21 + /// #[loroscope] 22 + /// struct Player { 23 + /// name: Text, 24 + /// score: i64, 25 + /// } 26 + /// 27 + /// let player = Player::new(); 28 + /// player.set_score(100); 29 + /// assert_eq!(player.score(), 100); 30 + /// ``` 31 + /// 32 + /// # Field types 33 + /// 34 + /// | Type | Accessor | 35 + /// |---|---| 36 + /// | `f64`, `i64`, `bool`, `String` | Getter + `set_` setter | 37 + /// | [`Text`], [`Counter`] | Getter returning the container | 38 + /// | [`List<T>`], [`Map<V>`], [`MovableList<T>`], [`Tree<T>`] | Getter returning a typed collection | 39 + /// | `LoroList`, `LoroMap`, `LoroText`, … | Getter returning the raw Loro container | 40 + /// | Any `#[loroscope]` struct | Getter returning a nested typed view | 41 + /// 42 + /// # Generated methods 43 + /// 44 + /// - `new()` — creates a new [`LoroDoc`](loro::LoroDoc) and returns 45 + /// a typed view into it. 46 + /// - `doc()` — returns the [`LoroDoc`](loro::LoroDoc) this struct belongs to. 47 + /// - `raw()` — returns a reference to the underlying [`LoroMap`](loro::LoroMap). 48 + /// 49 + /// # Derives 50 + /// 51 + /// Derive macros on the input struct are forwarded to the generated struct. 52 + /// `Debug` is handled specially: it prints the struct using its field 53 + /// accessors rather than exposing internals. 54 + /// 55 + /// [`Text`]: https://docs.rs/loroscope/latest/loroscope/type.Text.html 56 + /// [`Counter`]: https://docs.rs/loroscope/latest/loroscope/type.Counter.html 57 + /// [`Tree`]: https://docs.rs/loroscope/latest/loroscope/type.Tree.html 58 + /// [`List<T>`]: https://docs.rs/loroscope/latest/loroscope/struct.List.html 59 + /// [`Map<V>`]: https://docs.rs/loroscope/latest/loroscope/struct.Map.html 60 + /// [`MovableList<T>`]: https://docs.rs/loroscope/latest/loroscope/struct.MovableList.html 61 + #[proc_macro_attribute] 62 + pub fn loroscope(attr: TokenStream, item: TokenStream) -> TokenStream { 63 + if !attr.is_empty() { 64 + let ident = parse_macro_input!(attr as Ident); 65 + return syn::Error::new(ident.span(), "unexpected attribute argument") 66 + .to_compile_error() 67 + .into(); 68 + } 69 + 70 + let input = parse_macro_input!(item as ItemStruct); 71 + let struct_name = &input.ident; 72 + let struct_name_str = struct_name.to_string(); 73 + let vis = &input.vis; 74 + let map_key = struct_name.to_string(); 75 + 76 + // Separate derive macros from other attributes, pulling out Debug specially. 77 + let mut has_debug = false; 78 + let mut other_derives: Vec<syn::Path> = Vec::new(); 79 + let mut other_attrs: Vec<&syn::Attribute> = Vec::new(); 80 + 81 + for attr in &input.attrs { 82 + if attr.path().is_ident("derive") { 83 + if let Meta::List(meta_list) = &attr.meta 84 + && let Ok(paths) = meta_list 85 + .parse_args_with(Punctuated::<syn::Path, syn::token::Comma>::parse_terminated) 86 + { 87 + for path in paths { 88 + if path.is_ident("Debug") { 89 + has_debug = true; 90 + } else { 91 + other_derives.push(path); 92 + } 93 + } 94 + } 95 + } else { 96 + other_attrs.push(attr); 97 + } 98 + } 99 + 100 + let derive_attr = if other_derives.is_empty() { 101 + quote!() 102 + } else { 103 + quote!(#[derive(#(#other_derives),*)]) 104 + }; 105 + 106 + let fields = match &input.fields { 107 + syn::Fields::Named(f) => &f.named, 108 + _ => { 109 + return syn::Error::new_spanned(&input, "expected named fields") 110 + .to_compile_error() 111 + .into(); 112 + } 113 + }; 114 + 115 + let mut accessors = Vec::new(); 116 + 117 + for field in fields { 118 + let field_name = field.ident.as_ref().expect("named fields"); 119 + let field_name_str = field_name.to_string(); 120 + let field_ty = &field.ty; 121 + 122 + match categorize_type(field_ty) { 123 + TypeCategory::Primitive(prim) => { 124 + accessors.push(gen_primitive_getter(field_name, &field_name_str, &prim)); 125 + accessors.push(gen_primitive_setter(field_name, &field_name_str, &prim)); 126 + } 127 + TypeCategory::NonGenericContainer(container) => { 128 + accessors.push(gen_non_generic_container_getter( 129 + field_name, 130 + &field_name_str, 131 + &container, 132 + )); 133 + } 134 + TypeCategory::GenericContainer(container, type_args) => { 135 + accessors.push(gen_generic_container_getter( 136 + field_name, 137 + &field_name_str, 138 + &container, 139 + &type_args, 140 + )); 141 + } 142 + TypeCategory::NestedLoroscope => { 143 + accessors.push(gen_nested_getter(field_name, &field_name_str, field_ty)); 144 + } 145 + } 146 + } 147 + 148 + // Generate a custom Debug impl that prints as if the original fields exist. 149 + let debug_impl = if has_debug { 150 + let debug_fields: Vec<_> = fields 151 + .iter() 152 + .map(|field| { 153 + let name = field.ident.as_ref().expect("named fields"); 154 + let name_str = name.to_string(); 155 + quote! { .field(#name_str, &self.#name()) } 156 + }) 157 + .collect(); 158 + 159 + quote! { 160 + impl ::core::fmt::Debug for #struct_name { 161 + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { 162 + f.debug_struct(#struct_name_str) 163 + #(#debug_fields)* 164 + .finish() 165 + } 166 + } 167 + } 168 + } else { 169 + quote!() 170 + }; 171 + 172 + let output = quote! { 173 + #(#other_attrs)* 174 + #derive_attr 175 + #vis struct #struct_name { 176 + _map: ::loroscope::__private::LoroMap, 177 + } 178 + 179 + impl ::core::convert::From<::loroscope::__private::LoroMap> for #struct_name { 180 + fn from(map: ::loroscope::__private::LoroMap) -> Self { 181 + Self { _map: map } 182 + } 183 + } 184 + 185 + impl ::loroscope::FromLoroMap for #struct_name { 186 + fn from_loro_map(map: ::loroscope::__private::LoroMap) -> Self { 187 + Self { _map: map } 188 + } 189 + } 190 + 191 + #debug_impl 192 + 193 + impl #struct_name { 194 + /// Creates a new [`LoroDoc`](loro::LoroDoc) and returns a typed view into it. 195 + pub fn new() -> Self { 196 + let doc = ::loroscope::__private::LoroDoc::new(); 197 + let map = doc.get_map(#map_key); 198 + Self { _map: map } 199 + } 200 + 201 + /// Returns the [`LoroDoc`](loro::LoroDoc) this struct belongs to. 202 + pub fn doc(&self) -> ::loroscope::__private::LoroDoc { 203 + use ::loroscope::__private::ContainerTrait; 204 + self._map.doc().expect("loroscope container is not attached to a document") 205 + } 206 + 207 + /// Returns a reference to the underlying [`LoroMap`](loro::LoroMap). 208 + pub fn raw(&self) -> &::loroscope::__private::LoroMap { 209 + &self._map 210 + } 211 + 212 + #(#accessors)* 213 + } 214 + }; 215 + 216 + output.into() 217 + } 218 + 219 + // --------------------------------------------------------------------------- 220 + // Type categorization 221 + // --------------------------------------------------------------------------- 222 + 223 + enum TypeCategory { 224 + Primitive(PrimitiveKind), 225 + NonGenericContainer(NonGenericContainerKind), 226 + GenericContainer(GenericContainerKind, Vec<Type>), 227 + NestedLoroscope, 228 + } 229 + 230 + enum PrimitiveKind { 231 + F64, 232 + I64, 233 + Bool, 234 + String, 235 + } 236 + 237 + enum NonGenericContainerKind { 238 + Text, 239 + Counter, 240 + LoroTree, 241 + LoroList, 242 + LoroMap, 243 + LoroMovableList, 244 + } 245 + 246 + enum GenericContainerKind { 247 + List, 248 + Map, 249 + MovableList, 250 + Tree, 251 + } 252 + 253 + /// Known crate-level prefixes. If a multi-segment path has one of these as 254 + /// the segment immediately before the type name, we treat the final segment 255 + /// the same as a bare import. For any other prefix (e.g. `foo::Text`) we 256 + /// conservatively fall back to [`TypeCategory::NestedLoroscope`]. 257 + const KNOWN_PREFIXES: &[&str] = &["loroscope", "loro"]; 258 + 259 + fn categorize_type(ty: &Type) -> TypeCategory { 260 + if let Type::Path(type_path) = ty { 261 + let segments = &type_path.path.segments; 262 + let last_segment = match segments.last() { 263 + Some(s) => s, 264 + None => return TypeCategory::NestedLoroscope, 265 + }; 266 + 267 + // For multi-segment paths (e.g. `loroscope::Text`, `loro::LoroMap`), 268 + // only proceed if the parent segment is a known crate prefix. 269 + // Proc macros cannot do real name resolution, so this is a heuristic. 270 + if segments.len() > 1 { 271 + let parent = segments[segments.len() - 2].ident.to_string(); 272 + if !KNOWN_PREFIXES.contains(&parent.as_str()) { 273 + return TypeCategory::NestedLoroscope; 274 + } 275 + } 276 + 277 + let ident = last_segment.ident.to_string(); 278 + 279 + match ident.as_str() { 280 + "f64" => TypeCategory::Primitive(PrimitiveKind::F64), 281 + "i64" => TypeCategory::Primitive(PrimitiveKind::I64), 282 + "bool" => TypeCategory::Primitive(PrimitiveKind::Bool), 283 + "String" => TypeCategory::Primitive(PrimitiveKind::String), 284 + "Text" | "LoroText" => TypeCategory::NonGenericContainer(NonGenericContainerKind::Text), 285 + "Counter" | "LoroCounter" => { 286 + TypeCategory::NonGenericContainer(NonGenericContainerKind::Counter) 287 + } 288 + "LoroTree" => TypeCategory::NonGenericContainer(NonGenericContainerKind::LoroTree), 289 + "LoroList" => TypeCategory::NonGenericContainer(NonGenericContainerKind::LoroList), 290 + "LoroMap" => TypeCategory::NonGenericContainer(NonGenericContainerKind::LoroMap), 291 + "LoroMovableList" => { 292 + TypeCategory::NonGenericContainer(NonGenericContainerKind::LoroMovableList) 293 + } 294 + "List" | "Map" | "MovableList" | "Tree" => { 295 + let kind = match ident.as_str() { 296 + "List" => GenericContainerKind::List, 297 + "Map" => GenericContainerKind::Map, 298 + "MovableList" => GenericContainerKind::MovableList, 299 + "Tree" => GenericContainerKind::Tree, 300 + _ => unreachable!(), 301 + }; 302 + let type_args = extract_type_args(&last_segment.arguments); 303 + TypeCategory::GenericContainer(kind, type_args) 304 + } 305 + _ => TypeCategory::NestedLoroscope, 306 + } 307 + } else { 308 + TypeCategory::NestedLoroscope 309 + } 310 + } 311 + 312 + fn extract_type_args(args: &PathArguments) -> Vec<Type> { 313 + match args { 314 + PathArguments::AngleBracketed(ab) => ab 315 + .args 316 + .iter() 317 + .filter_map(|a| { 318 + if let GenericArgument::Type(t) = a { 319 + Some(t.clone()) 320 + } else { 321 + None 322 + } 323 + }) 324 + .collect(), 325 + _ => Vec::new(), 326 + } 327 + } 328 + 329 + // --------------------------------------------------------------------------- 330 + // Code generation — primitives 331 + // --------------------------------------------------------------------------- 332 + 333 + fn gen_primitive_getter( 334 + field_name: &Ident, 335 + field_name_str: &str, 336 + prim: &PrimitiveKind, 337 + ) -> proc_macro2::TokenStream { 338 + match prim { 339 + PrimitiveKind::F64 => quote! { 340 + pub fn #field_name(&self) -> f64 { 341 + self._map.get(#field_name_str) 342 + .and_then(|v| v.into_value().ok()) 343 + .and_then(|v| match v { 344 + ::loroscope::__private::LoroValue::Double(d) => Some(d), 345 + _ => None, 346 + }) 347 + .unwrap_or_default() 348 + } 349 + }, 350 + PrimitiveKind::I64 => quote! { 351 + pub fn #field_name(&self) -> i64 { 352 + self._map.get(#field_name_str) 353 + .and_then(|v| v.into_value().ok()) 354 + .and_then(|v| match v { 355 + ::loroscope::__private::LoroValue::I64(i) => Some(i), 356 + _ => None, 357 + }) 358 + .unwrap_or_default() 359 + } 360 + }, 361 + PrimitiveKind::Bool => quote! { 362 + pub fn #field_name(&self) -> bool { 363 + self._map.get(#field_name_str) 364 + .and_then(|v| v.into_value().ok()) 365 + .and_then(|v| match v { 366 + ::loroscope::__private::LoroValue::Bool(b) => Some(b), 367 + _ => None, 368 + }) 369 + .unwrap_or_default() 370 + } 371 + }, 372 + PrimitiveKind::String => quote! { 373 + pub fn #field_name(&self) -> String { 374 + self._map.get(#field_name_str) 375 + .and_then(|v| v.into_value().ok()) 376 + .and_then(|v| match v { 377 + ::loroscope::__private::LoroValue::String(s) => Some(s.to_string()), 378 + _ => None, 379 + }) 380 + .unwrap_or_default() 381 + } 382 + }, 383 + } 384 + } 385 + 386 + fn gen_primitive_setter( 387 + field_name: &Ident, 388 + field_name_str: &str, 389 + prim: &PrimitiveKind, 390 + ) -> proc_macro2::TokenStream { 391 + let setter_name = Ident::new(&format!("set_{}", field_name), field_name.span()); 392 + 393 + match prim { 394 + PrimitiveKind::String => quote! { 395 + pub fn #setter_name(&self, val: &str) { 396 + self._map.insert(#field_name_str, val).expect("insert into attached map"); 397 + } 398 + }, 399 + PrimitiveKind::F64 => quote! { 400 + pub fn #setter_name(&self, val: f64) { 401 + self._map.insert(#field_name_str, val).expect("insert into attached map"); 402 + } 403 + }, 404 + PrimitiveKind::I64 => quote! { 405 + pub fn #setter_name(&self, val: i64) { 406 + self._map.insert(#field_name_str, val).expect("insert into attached map"); 407 + } 408 + }, 409 + PrimitiveKind::Bool => quote! { 410 + pub fn #setter_name(&self, val: bool) { 411 + self._map.insert(#field_name_str, val).expect("insert into attached map"); 412 + } 413 + }, 414 + } 415 + } 416 + 417 + // --------------------------------------------------------------------------- 418 + // Code generation — non-generic containers 419 + // --------------------------------------------------------------------------- 420 + 421 + fn gen_non_generic_container_getter( 422 + field_name: &Ident, 423 + field_name_str: &str, 424 + container: &NonGenericContainerKind, 425 + ) -> proc_macro2::TokenStream { 426 + let (loro_type, loroscope_type) = match container { 427 + NonGenericContainerKind::Text => ( 428 + quote!(::loroscope::__private::LoroText), 429 + quote!(::loroscope::Text), 430 + ), 431 + NonGenericContainerKind::Counter => ( 432 + quote!(::loroscope::__private::LoroCounter), 433 + quote!(::loroscope::Counter), 434 + ), 435 + NonGenericContainerKind::LoroTree => ( 436 + quote!(::loroscope::__private::LoroTree), 437 + quote!(::loroscope::__private::LoroTree), 438 + ), 439 + NonGenericContainerKind::LoroList => ( 440 + quote!(::loroscope::__private::LoroList), 441 + quote!(::loroscope::__private::LoroList), 442 + ), 443 + NonGenericContainerKind::LoroMap => ( 444 + quote!(::loroscope::__private::LoroMap), 445 + quote!(::loroscope::__private::LoroMap), 446 + ), 447 + NonGenericContainerKind::LoroMovableList => ( 448 + quote!(::loroscope::__private::LoroMovableList), 449 + quote!(::loroscope::__private::LoroMovableList), 450 + ), 451 + }; 452 + 453 + quote! { 454 + pub fn #field_name(&self) -> #loroscope_type { 455 + self._map.get_or_create_container(#field_name_str, #loro_type::new()).expect("create container on attached map") 456 + } 457 + } 458 + } 459 + 460 + // --------------------------------------------------------------------------- 461 + // Code generation — generic containers 462 + // --------------------------------------------------------------------------- 463 + 464 + fn gen_generic_container_getter( 465 + field_name: &Ident, 466 + field_name_str: &str, 467 + container: &GenericContainerKind, 468 + type_args: &[Type], 469 + ) -> proc_macro2::TokenStream { 470 + let (loro_container, wrapper_path) = match container { 471 + GenericContainerKind::List => ( 472 + quote!(::loroscope::__private::LoroList), 473 + quote!(::loroscope::List), 474 + ), 475 + GenericContainerKind::Map => ( 476 + quote!(::loroscope::__private::LoroMap), 477 + quote!(::loroscope::Map), 478 + ), 479 + GenericContainerKind::MovableList => ( 480 + quote!(::loroscope::__private::LoroMovableList), 481 + quote!(::loroscope::MovableList), 482 + ), 483 + GenericContainerKind::Tree => ( 484 + quote!(::loroscope::__private::LoroTree), 485 + quote!(::loroscope::Tree), 486 + ), 487 + }; 488 + 489 + quote! { 490 + pub fn #field_name(&self) -> #wrapper_path<#(#type_args),*> { 491 + #wrapper_path::new( 492 + self._map.get_or_create_container(#field_name_str, #loro_container::new()).expect("create container on attached map") 493 + ) 494 + } 495 + } 496 + } 497 + 498 + // --------------------------------------------------------------------------- 499 + // Code generation — nested loroscope structs 500 + // --------------------------------------------------------------------------- 501 + 502 + fn gen_nested_getter( 503 + field_name: &Ident, 504 + field_name_str: &str, 505 + field_ty: &Type, 506 + ) -> proc_macro2::TokenStream { 507 + quote! { 508 + pub fn #field_name(&self) -> #field_ty { 509 + ::loroscope::FromLoroMap::from_loro_map( 510 + self._map.get_or_create_container( 511 + #field_name_str, 512 + ::loroscope::__private::LoroMap::new(), 513 + ).expect("create container on attached map") 514 + ) 515 + } 516 + } 517 + }
+58
readme.md
··· 1 + # loroscope 2 + 3 + Typed structs for [Loro](https://loro.dev/) CRDTs. 4 + 5 + [Loro](https://loro.dev/) is a CRDT library — it gives you conflict-free 6 + replicated containers (lists, maps, trees, rich text, etc.) with undo/redo, 7 + sync, and time travel. But working with it directly means string-keyed 8 + container lookups and manual `LoroValue` matching. 9 + 10 + `#[loroscope]` generates typed accessors from a struct definition, so field 11 + access is checked at compile time. 12 + 13 + ## Example 14 + 15 + ```rust 16 + use loroscope::loroscope; 17 + 18 + #[loroscope] 19 + struct TodoItem { 20 + title: Text, 21 + done: bool, 22 + } 23 + 24 + #[loroscope] 25 + struct App { 26 + todos: List<TodoItem>, 27 + } 28 + 29 + let app = App::new(); 30 + 31 + let item = app.todos().push_new(); 32 + item.title().insert(0, "Buy milk").unwrap(); 33 + item.set_done(false); 34 + 35 + assert_eq!(app.todos().len(), 1); 36 + assert!(!app.todos().get(0).unwrap().done()); 37 + 38 + // The underlying LoroDoc is always available for export, import, 39 + // undo, subscriptions, etc. 40 + let _doc = app.doc(); 41 + ``` 42 + 43 + ## Field types 44 + 45 + | Type | Accessor | 46 + |---|---| 47 + | `f64`, `i64`, `bool`, `String` | Getter + `set_` setter | 48 + | [`Text`], [`Counter`] | Getter returning the container | 49 + | [`List<T>`], [`Map<V>`], [`MovableList<T>`], [`Tree<T>`] | Getter returning a typed collection | 50 + | `LoroList`, `LoroMap`, `LoroText`, ... | Getter returning the raw Loro container | 51 + | Any `#[loroscope]` struct | Getter returning a nested typed view | 52 + 53 + [`Text`]: https://docs.rs/loroscope/latest/loroscope/type.Text.html 54 + [`Counter`]: https://docs.rs/loroscope/latest/loroscope/type.Counter.html 55 + [`List<T>`]: https://docs.rs/loroscope/latest/loroscope/struct.List.html 56 + [`Map<V>`]: https://docs.rs/loroscope/latest/loroscope/struct.Map.html 57 + [`MovableList<T>`]: https://docs.rs/loroscope/latest/loroscope/struct.MovableList.html 58 + [`Tree<T>`]: https://docs.rs/loroscope/latest/loroscope/struct.Tree.html