From 2a41ffca2d10cc899bd1c9793ca7abf29ffdd2fa Mon Sep 17 00:00:00 2001 From: Leon Zhao Date: Tue, 31 Dec 2024 16:21:35 +0800 Subject: [PATCH] feat: basic api --- .github/workflows/CI.yml | 181 +++++ .gitignore | 72 ++ Cargo.lock | 1197 +++++++++++++++++++++++++++++++++ Cargo.toml | 17 + README.md | 1 + example/main.py | 5 + pyproject.toml | 13 + src/container.rs | 37 + src/container/counter.rs | 9 + src/container/list.rs | 9 + src/container/map.rs | 14 + src/container/movable_list.rs | 14 + src/container/text.rs | 14 + src/container/tree.rs | 14 + src/convert.rs | 523 ++++++++++++++ src/doc.rs | 857 +++++++++++++++++++++++ src/err.rs | 17 + src/event.rs | 440 ++++++++++++ src/lib.rs | 20 + src/value.rs | 182 +++++ src/version.rs | 118 ++++ 21 files changed, 3754 insertions(+) create mode 100644 .github/workflows/CI.yml create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 example/main.py create mode 100644 pyproject.toml create mode 100644 src/container.rs create mode 100644 src/container/counter.rs create mode 100644 src/container/list.rs create mode 100644 src/container/map.rs create mode 100644 src/container/movable_list.rs create mode 100644 src/container/text.rs create mode 100644 src/container/tree.rs create mode 100644 src/convert.rs create mode 100644 src/doc.rs create mode 100644 src/err.rs create mode 100644 src/event.rs create mode 100644 src/lib.rs create mode 100644 src/value.rs create mode 100644 src/version.rs diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml new file mode 100644 index 0000000..5391614 --- /dev/null +++ b/.github/workflows/CI.yml @@ -0,0 +1,181 @@ +# This file is autogenerated by maturin v1.8.0 +# To update, run +# +# maturin generate-ci github +# +name: CI + +on: + push: + branches: + - main + - master + tags: + - '*' + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + linux: + runs-on: ${{ matrix.platform.runner }} + strategy: + matrix: + platform: + - runner: ubuntu-22.04 + target: x86_64 + - runner: ubuntu-22.04 + target: x86 + - runner: ubuntu-22.04 + target: aarch64 + - runner: ubuntu-22.04 + target: armv7 + - runner: ubuntu-22.04 + target: s390x + - runner: ubuntu-22.04 + target: ppc64le + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.x + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.platform.target }} + args: --release --out dist --find-interpreter + sccache: 'true' + manylinux: auto + - name: Upload wheels + uses: actions/upload-artifact@v4 + with: + name: wheels-linux-${{ matrix.platform.target }} + path: dist + + musllinux: + runs-on: ${{ matrix.platform.runner }} + strategy: + matrix: + platform: + - runner: ubuntu-22.04 + target: x86_64 + - runner: ubuntu-22.04 + target: x86 + - runner: ubuntu-22.04 + target: aarch64 + - runner: ubuntu-22.04 + target: armv7 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.x + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.platform.target }} + args: --release --out dist --find-interpreter + sccache: 'true' + manylinux: musllinux_1_2 + - name: Upload wheels + uses: actions/upload-artifact@v4 + with: + name: wheels-musllinux-${{ matrix.platform.target }} + path: dist + + windows: + runs-on: ${{ matrix.platform.runner }} + strategy: + matrix: + platform: + - runner: windows-latest + target: x64 + - runner: windows-latest + target: x86 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.x + architecture: ${{ matrix.platform.target }} + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.platform.target }} + args: --release --out dist --find-interpreter + sccache: 'true' + - name: Upload wheels + uses: actions/upload-artifact@v4 + with: + name: wheels-windows-${{ matrix.platform.target }} + path: dist + + macos: + runs-on: ${{ matrix.platform.runner }} + strategy: + matrix: + platform: + - runner: macos-13 + target: x86_64 + - runner: macos-14 + target: aarch64 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.x + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.platform.target }} + args: --release --out dist --find-interpreter + sccache: 'true' + - name: Upload wheels + uses: actions/upload-artifact@v4 + with: + name: wheels-macos-${{ matrix.platform.target }} + path: dist + + sdist: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Build sdist + uses: PyO3/maturin-action@v1 + with: + command: sdist + args: --out dist + - name: Upload sdist + uses: actions/upload-artifact@v4 + with: + name: wheels-sdist + path: dist + + release: + name: Release + runs-on: ubuntu-latest + if: ${{ startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' }} + needs: [linux, musllinux, windows, macos, sdist] + permissions: + # Use to sign the release artifacts + id-token: write + # Used to upload release artifacts + contents: write + # Used to generate artifact attestation + attestations: write + steps: + - uses: actions/download-artifact@v4 + - name: Generate artifact attestation + uses: actions/attest-build-provenance@v1 + with: + subject-path: 'wheels-*/*' + - name: Publish to PyPI + if: ${{ startsWith(github.ref, 'refs/tags/') }} + uses: PyO3/maturin-action@v1 + env: + MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }} + with: + command: upload + args: --non-interactive --skip-existing wheels-*/* diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c8f0442 --- /dev/null +++ b/.gitignore @@ -0,0 +1,72 @@ +/target + +# Byte-compiled / optimized / DLL files +__pycache__/ +.pytest_cache/ +*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +.venv/ +env/ +bin/ +build/ +develop-eggs/ +dist/ +eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +include/ +man/ +venv/ +*.egg-info/ +.installed.cfg +*.egg + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt +pip-selfcheck.json + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.cache +nosetests.xml +coverage.xml + +# Translations +*.mo + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# Rope +.ropeproject + +# Django stuff: +*.log +*.pot + +.DS_Store + +# Sphinx documentation +docs/_build/ + +# PyCharm +.idea/ + +# VSCode +.vscode/ + +# Pyenv +.python-version diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..5e4d0b1 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1197 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "append-only-bytes" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac436601d6bdde674a0d7fb593e829ffe7b3387c351b356dd20e2d40f5bf3ee5" + +[[package]] +name = "arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "arref" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ccd462b64c3c72f1be8305905a85d85403d768e8690c9b8bd3b9009a5761679" + +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "bitmaps" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2" +dependencies = [ + "typenum", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cobs" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15" + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.93", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.93", +] + +[[package]] +name = "derive_arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.93", +] + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "ensure-cov" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33753185802e107b8fa907192af1f0eca13b1fb33327a59266d650fef29b2b4e" + +[[package]] +name = "enum-as-inner" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9720bba047d567ffc8a3cba48bf19126600e249ab7f128e9233e6376976a116" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "enum-as-inner" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.93", +] + +[[package]] +name = "enum_dispatch" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd" +dependencies = [ + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.93", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "generic-btree" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "210507e6dec78bb1304e52a174bd99efdd83894219bf20d656a066a0ce2fedc5" +dependencies = [ + "arref", + "fxhash", + "heapless 0.7.17", + "itertools 0.11.0", + "loro-thunderdome", + "proc-macro2", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32 0.2.1", + "rustc_version", + "serde", + "spin", + "stable_deref_trait", +] + +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32 0.3.1", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "im" +version = "15.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0acd33ff0285af998aaf9b57342af478078f53492322fafc47450e09397e0e9" +dependencies = [ + "bitmaps", + "rand_core", + "rand_xoshiro", + "serde", + "sized-chunks", + "typenum", + "version_check", +] + +[[package]] +name = "indoc" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" + +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" + +[[package]] +name = "leb128" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" + +[[package]] +name = "libc" +version = "0.2.169" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "loro" +version = "1.1.0" +source = "git+https://github.com/loro-dev/loro.git?tag=loro-crdt%401.2.5#5a85e6e5d2309c5460471ae62d30cb0cf02685a3" +dependencies = [ + "enum-as-inner 0.6.1", + "fxhash", + "generic-btree", + "loro-common", + "loro-delta", + "loro-internal", + "loro-kv-store", + "tracing", +] + +[[package]] +name = "loro-common" +version = "1.1.0" +source = "git+https://github.com/loro-dev/loro.git?tag=loro-crdt%401.2.5#5a85e6e5d2309c5460471ae62d30cb0cf02685a3" +dependencies = [ + "arbitrary", + "enum-as-inner 0.6.1", + "fxhash", + "leb128", + "loro-rle", + "nonmax", + "serde", + "serde_columnar", + "serde_json", + "thiserror", +] + +[[package]] +name = "loro-delta" +version = "1.1.0" +source = "git+https://github.com/loro-dev/loro.git?tag=loro-crdt%401.2.5#5a85e6e5d2309c5460471ae62d30cb0cf02685a3" +dependencies = [ + "arrayvec", + "enum-as-inner 0.5.1", + "generic-btree", + "heapless 0.8.0", +] + +[[package]] +name = "loro-internal" +version = "1.1.0" +source = "git+https://github.com/loro-dev/loro.git?tag=loro-crdt%401.2.5#5a85e6e5d2309c5460471ae62d30cb0cf02685a3" +dependencies = [ + "append-only-bytes", + "arref", + "bytes", + "either", + "ensure-cov", + "enum-as-inner 0.6.1", + "enum_dispatch", + "fxhash", + "generic-btree", + "getrandom", + "im", + "itertools 0.12.1", + "leb128", + "loro-common", + "loro-delta", + "loro-kv-store", + "loro-rle", + "loro_fractional_index", + "md5", + "nonmax", + "num", + "num-traits", + "once_cell", + "postcard", + "pretty_assertions", + "rand", + "serde", + "serde_columnar", + "serde_json", + "smallvec", + "thiserror", + "tracing", + "xxhash-rust", +] + +[[package]] +name = "loro-kv-store" +version = "1.1.0" +source = "git+https://github.com/loro-dev/loro.git?tag=loro-crdt%401.2.5#5a85e6e5d2309c5460471ae62d30cb0cf02685a3" +dependencies = [ + "bytes", + "ensure-cov", + "fxhash", + "loro-common", + "lz4_flex", + "once_cell", + "quick_cache", + "tracing", + "xxhash-rust", +] + +[[package]] +name = "loro-rle" +version = "1.1.0" +source = "git+https://github.com/loro-dev/loro.git?tag=loro-crdt%401.2.5#5a85e6e5d2309c5460471ae62d30cb0cf02685a3" +dependencies = [ + "append-only-bytes", + "num", + "smallvec", +] + +[[package]] +name = "loro-thunderdome" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f3d053a135388e6b1df14e8af1212af5064746e9b87a06a345a7a779ee9695a" + +[[package]] +name = "loro_fractional_index" +version = "1.1.0" +source = "git+https://github.com/loro-dev/loro.git?tag=loro-crdt%401.2.5#5a85e6e5d2309c5460471ae62d30cb0cf02685a3" +dependencies = [ + "once_cell", + "rand", + "serde", +] + +[[package]] +name = "loro_py" +version = "1.2.5-alpha.0" +dependencies = [ + "fxhash", + "loro", + "pyo3", +] + +[[package]] +name = "lz4_flex" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75761162ae2b0e580d7e7c390558127e5f01b4194debd6221fd8c207fc80e3f5" +dependencies = [ + "twox-hash", +] + +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "nonmax" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "610a5acd306ec67f907abe5567859a3c693fb9886eb1f012ab8f2a47bef3db51" + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" + +[[package]] +name = "portable-atomic" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" + +[[package]] +name = "postcard" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170a2601f67cc9dba8edd8c4870b15f71a6a2dc196daec8c83f72b59dff628a8" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "heapless 0.7.17", + "serde", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + +[[package]] +name = "proc-macro2" +version = "1.0.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pyo3" +version = "0.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e484fd2c8b4cb67ab05a318f1fd6fa8f199fcc30819f08f07d200809dba26c15" +dependencies = [ + "cfg-if", + "indoc", + "libc", + "memoffset", + "once_cell", + "portable-atomic", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-build-config" +version = "0.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc0e0469a84f208e20044b98965e1561028180219e35352a2afaf2b942beff3b" +dependencies = [ + "once_cell", + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb1547a7f9966f6f1a0f0227564a9945fe36b90da5a93b3933fc3dc03fae372d" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb6da8ec6fa5cedd1626c886fc8749bdcbb09424a86461eb8cdf096b7c33257" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn 2.0.93", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38a385202ff5a92791168b1136afae5059d3ac118457bb7bc304c197c2d33e7d" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "pyo3-build-config", + "quote", + "syn 2.0.93", +] + +[[package]] +name = "quick_cache" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d7c94f8935a9df96bb6380e8592c70edf497a643f94bd23b2f76b399385dbf4" +dependencies = [ + "ahash", + "equivalent", + "hashbrown", + "parking_lot", +] + +[[package]] +name = "quote" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_xoshiro" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" +dependencies = [ + "rand_core", +] + +[[package]] +name = "redox_syscall" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" + +[[package]] +name = "serde" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_columnar" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d4e3c0e46450edf7da174b610b9143eb8ca22059ace5016741fc9e20b88d1e7" +dependencies = [ + "itertools 0.11.0", + "postcard", + "serde", + "serde_columnar_derive", + "thiserror", +] + +[[package]] +name = "serde_columnar_derive" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42c5d47942b2a7e76118b697fc0f94516a5d8366a3c0fee8d0e2b713e952e306" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.93", +] + +[[package]] +name = "serde_derive" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.93", +] + +[[package]] +name = "serde_json" +version = "1.0.134" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d00f4175c42ee48b15416f6193a959ba3a0d67fc699a0db9ad12df9f83991c7d" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "sized-chunks" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d69225bde7a69b235da73377861095455d298f2b970996eec25ddbb42b3d1e" +dependencies = [ + "bitmaps", + "typenum", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +dependencies = [ + "serde", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c786062daee0d6db1132800e623df74274a0a87322d8e183338e01b3d98d058" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.93", +] + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.93", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", +] + +[[package]] +name = "twox-hash" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" +dependencies = [ + "cfg-if", + "static_assertions", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-ident" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" + +[[package]] +name = "unindent" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "xxhash-rust" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.93", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..8103051 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "loro_py" +version = "1.2.5-alpha.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +name = "loro" +crate-type = ["cdylib"] + +[dependencies] +loro = { git = "https://github.com/loro-dev/loro.git", tag = "loro-crdt@1.2.5", features = [ + "counter", + "jsonpath", +] } +fxhash = "0.2.1" +pyo3 = { version = "0.23.3", features = ["extension-module"] } diff --git a/README.md b/README.md new file mode 100644 index 0000000..9e47e68 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# loro-py diff --git a/example/main.py b/example/main.py new file mode 100644 index 0000000..0cf0365 --- /dev/null +++ b/example/main.py @@ -0,0 +1,5 @@ +from loro import LoroDoc + +if __name__ == "__main__": + doc = LoroDoc() + text = doc.get_text("text") diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..4184b7b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,13 @@ +[build-system] +requires = ["maturin>=1.8,<2.0"] +build-backend = "maturin" + +[project] +name = "loro" +requires-python = ">=3.8" +classifiers = [ + "Programming Language :: Rust", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", +] +dynamic = ["version"] diff --git a/src/container.rs b/src/container.rs new file mode 100644 index 0000000..7bd65f3 --- /dev/null +++ b/src/container.rs @@ -0,0 +1,37 @@ +use pyo3::prelude::*; + +mod counter; +mod list; +mod map; +mod movable_list; +mod text; +mod tree; + +pub use counter::LoroCounter; +pub use list::LoroList; +pub use map::LoroMap; +pub use movable_list::LoroMovableList; +pub use text::LoroText; +pub use tree::LoroTree; + +#[pyclass(frozen)] +#[derive(Debug, Clone)] +pub enum Container { + List(LoroList), + Map(LoroMap), + MovableList(LoroMovableList), + Text(LoroText), + Tree(LoroTree), + Counter(LoroCounter), +} + +pub fn register_class(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/src/container/counter.rs b/src/container/counter.rs new file mode 100644 index 0000000..165a63c --- /dev/null +++ b/src/container/counter.rs @@ -0,0 +1,9 @@ +use loro::LoroCounter as LoroCounterInner; +use pyo3::prelude::*; + +#[pyclass(frozen)] +#[derive(Debug, Clone)] +pub struct LoroCounter(pub LoroCounterInner); + +#[pymethods] +impl LoroCounter {} diff --git a/src/container/list.rs b/src/container/list.rs new file mode 100644 index 0000000..1f205e0 --- /dev/null +++ b/src/container/list.rs @@ -0,0 +1,9 @@ +use loro::LoroList as LoroListInner; +use pyo3::prelude::*; + +#[pyclass(frozen)] +#[derive(Debug, Clone)] +pub struct LoroList(pub LoroListInner); + +#[pymethods] +impl LoroList {} diff --git a/src/container/map.rs b/src/container/map.rs new file mode 100644 index 0000000..d38a8be --- /dev/null +++ b/src/container/map.rs @@ -0,0 +1,14 @@ +use loro::LoroMap as LoroMapInner; +use pyo3::prelude::*; + +#[pyclass(frozen)] +#[derive(Debug, Clone)] +pub struct LoroMap(pub LoroMapInner); + +#[pymethods] +impl LoroMap { + #[new] + pub fn new() -> Self { + Self(LoroMapInner::new()) + } +} diff --git a/src/container/movable_list.rs b/src/container/movable_list.rs new file mode 100644 index 0000000..ed31519 --- /dev/null +++ b/src/container/movable_list.rs @@ -0,0 +1,14 @@ +use loro::{LoroMovableList as LoroMovableListInner, LoroValue}; +use pyo3::prelude::*; + +#[pyclass(frozen)] +#[derive(Debug, Clone)] +pub struct LoroMovableList(pub LoroMovableListInner); + +#[pymethods] +impl LoroMovableList { + #[new] + pub fn new() -> Self { + Self(LoroMovableListInner::new()) + } +} diff --git a/src/container/text.rs b/src/container/text.rs new file mode 100644 index 0000000..fc16d57 --- /dev/null +++ b/src/container/text.rs @@ -0,0 +1,14 @@ +use loro::LoroText as LoroTextInner; +use pyo3::prelude::*; + +#[pyclass(frozen)] +#[derive(Debug, Clone)] +pub struct LoroText(pub LoroTextInner); + +#[pymethods] +impl LoroText { + #[new] + pub fn new() -> Self { + Self(LoroTextInner::new()) + } +} diff --git a/src/container/tree.rs b/src/container/tree.rs new file mode 100644 index 0000000..dc7b043 --- /dev/null +++ b/src/container/tree.rs @@ -0,0 +1,14 @@ +use loro::LoroTree as LoroTreeInner; +use pyo3::prelude::*; + +#[pyclass(frozen)] +#[derive(Debug, Clone)] +pub struct LoroTree(pub LoroTreeInner); + +#[pymethods] +impl LoroTree { + #[new] + pub fn new() -> Self { + Self(LoroTreeInner::new()) + } +} diff --git a/src/convert.rs b/src/convert.rs new file mode 100644 index 0000000..639ded5 --- /dev/null +++ b/src/convert.rs @@ -0,0 +1,523 @@ +use std::{collections::HashMap, sync::Arc}; + +use fxhash::FxHashMap; +use pyo3::{ + exceptions::PyTypeError, + prelude::*, + types::{PyBytes, PyDict, PyList, PyMapping, PyString}, + PyObject, PyResult, Python, +}; + +use crate::{ + container::{Container, LoroList, LoroMap, LoroMovableList, LoroText, LoroTree}, + event::{ + ContainerDiff, Diff, EventTriggerKind, Index, ListDiffItem, MapDelta, PathItem, TextDelta, + TreeDiff, TreeDiffItem, TreeExternalDiff, + }, + value::{ + ContainerID, ContainerType, LoroBinaryValue, LoroListValue, LoroMapValue, LoroStringValue, + LoroValue, TreeID, TreeParentId, ValueOrContainer, ID, + }, +}; + +impl From for loro::ID { + fn from(value: ID) -> Self { + Self { + peer: value.peer, + counter: value.counter, + } + } +} + +impl From for ID { + fn from(value: loro::ID) -> Self { + Self { + peer: value.peer, + counter: value.counter, + } + } +} +impl From<&LoroValue> for loro::LoroValue { + fn from(value: &LoroValue) -> Self { + match value { + LoroValue::Null {} => loro::LoroValue::Null, + LoroValue::Bool(b) => loro::LoroValue::Bool(*b), + LoroValue::Double(d) => loro::LoroValue::Double(*d), + LoroValue::I64(i) => loro::LoroValue::I64(*i), + LoroValue::Binary(b) => { + loro::LoroValue::Binary(loro::LoroBinaryValue::from((*b.0).clone())) + } + LoroValue::String(s) => { + loro::LoroValue::String(loro::LoroStringValue::from((*s.0).clone())) + } + LoroValue::List(l) => loro::LoroValue::List(loro::LoroListValue::from( + (*l.0) + .clone() + .into_iter() + .map(|v| (&v).into()) + .collect::>(), + )), + LoroValue::Map(m) => loro::LoroValue::Map(loro::LoroMapValue::from( + (*m.0) + .clone() + .into_iter() + .map(|(k, v)| (k, (&v).into())) + .collect::>(), + )), + LoroValue::Container(c) => loro::LoroValue::Container(c.into()), + } + } +} + +impl From for LoroValue { + fn from(value: loro::LoroValue) -> Self { + match value { + loro::LoroValue::Null => LoroValue::Null {}, + loro::LoroValue::Bool(b) => LoroValue::Bool(b), + loro::LoroValue::Double(d) => LoroValue::Double(d), + loro::LoroValue::I64(i) => LoroValue::I64(i), + loro::LoroValue::Binary(b) => LoroValue::Binary(LoroBinaryValue(Arc::new(b.to_vec()))), + loro::LoroValue::String(s) => { + LoroValue::String(LoroStringValue(Arc::new(s.to_string()))) + } + loro::LoroValue::List(l) => LoroValue::List(LoroListValue(Arc::new( + l.iter().map(|v| v.clone().into()).collect::>(), + ))), + loro::LoroValue::Map(m) => LoroValue::Map(LoroMapValue(Arc::new( + m.iter() + .map(|(k, v)| (k.to_string(), v.clone().into())) + .collect::>(), + ))), + loro::LoroValue::Container(c) => LoroValue::Container(ContainerID::from(c)), + } + } +} + +pub fn pyobject_to_container_id( + py: Python<'_>, + obj: PyObject, + ty: ContainerType, +) -> PyResult { + if let Ok(value) = obj.downcast_bound::(py) { + return Ok(loro::ContainerID::new_root(value.to_str()?, ty.into())); + } + if let Ok(value) = obj.downcast_bound::(py) { + return Ok(loro::ContainerID::from(value.get())); + } + + Err(PyTypeError::new_err("Invalid ContainerID")) +} + +pub fn pyobject_to_loro_value(py: Python<'_>, obj: &PyObject) -> PyResult { + if obj.is_none(py) { + return Ok(loro::LoroValue::Null); + } + if let Ok(value) = obj.downcast_bound::(py) { + return Ok(value.get().into()); + } + if let Ok(value) = obj.downcast_bound::(py) { + return Ok(loro::LoroValue::Binary(loro::LoroBinaryValue::from( + value.as_bytes().to_vec(), + ))); + } + if let Ok(value) = obj.downcast_bound::(py) { + return Ok(loro::LoroValue::String(loro::LoroStringValue::from( + value.to_string(), + ))); + } + if let Ok(value) = obj.downcast_bound::(py) { + let mut list = Vec::with_capacity(value.len()); + for item in value.iter() { + list.push(pyobject_to_loro_value(py, &item.unbind())?); + } + return Ok(loro::LoroValue::List(loro::LoroListValue::from(list))); + } + if let Ok(value) = obj.downcast_bound::(py) { + let mut map = FxHashMap::default(); + for (key, value) in value.iter() { + if key.downcast::().is_ok() { + map.insert( + key.to_string(), + pyobject_to_loro_value(py, &value.unbind())?, + ); + } else { + return Err(PyTypeError::new_err( + "only dict with string keys is supported for converting to LoroValue", + )); + } + } + return Ok(loro::LoroValue::Map(loro::LoroMapValue::from(map))); + } + if let Ok(value) = obj.downcast_bound::(py) { + let mut map = FxHashMap::default(); + for key in value.keys()? { + if key.downcast::().is_ok() { + map.insert( + key.to_string(), + pyobject_to_loro_value(py, &value.get_item(key).unwrap().unbind())?, + ); + } else { + return Err(PyTypeError::new_err( + "only dict with string keys is supported for converting to LoroValue", + )); + } + } + return Ok(loro::LoroValue::Map(loro::LoroMapValue::from(map))); + } + if let Ok(value) = obj.downcast_bound::(py) { + return Ok(loro::LoroValue::Container(value.get().clone().into())); + } + Err(PyTypeError::new_err("Invalid LoroValue")) +} + +impl From for loro::ContainerType { + fn from(value: ContainerType) -> loro::ContainerType { + match value { + ContainerType::Text {} => loro::ContainerType::Text, + ContainerType::Map {} => loro::ContainerType::Map, + ContainerType::List {} => loro::ContainerType::List, + ContainerType::MovableList {} => loro::ContainerType::MovableList, + ContainerType::Tree {} => loro::ContainerType::Tree, + ContainerType::Counter {} => loro::ContainerType::Counter, + ContainerType::Unknown { kind } => loro::ContainerType::Unknown(kind), + } + } +} +impl From for ContainerType { + fn from(value: loro::ContainerType) -> ContainerType { + match value { + loro::ContainerType::Text => ContainerType::Text {}, + loro::ContainerType::Map => ContainerType::Map {}, + loro::ContainerType::List => ContainerType::List {}, + loro::ContainerType::MovableList => ContainerType::MovableList {}, + loro::ContainerType::Tree => ContainerType::Tree {}, + loro::ContainerType::Counter => ContainerType::Counter {}, + loro::ContainerType::Unknown(kind) => ContainerType::Unknown { kind }, + } + } +} +impl From for loro::ContainerID { + fn from(value: ContainerID) -> loro::ContainerID { + match value { + ContainerID::Root { + name, + container_type, + } => loro::ContainerID::Root { + name: name.into(), + container_type: container_type.into(), + }, + ContainerID::Normal { + peer, + counter, + container_type, + } => loro::ContainerID::Normal { + peer, + counter, + container_type: container_type.into(), + }, + } + } +} + +impl From<&ContainerID> for loro::ContainerID { + fn from(value: &ContainerID) -> loro::ContainerID { + match value { + ContainerID::Root { + name, + container_type, + } => loro::ContainerID::Root { + name: name.clone().into(), + container_type: (*container_type).into(), + }, + ContainerID::Normal { + peer, + counter, + container_type, + } => loro::ContainerID::Normal { + peer: *peer, + counter: *counter, + container_type: (*container_type).into(), + }, + } + } +} + +impl From for ContainerID { + fn from(value: loro::ContainerID) -> ContainerID { + match value { + loro::ContainerID::Root { + name, + container_type, + } => ContainerID::Root { + name: name.to_string(), + container_type: container_type.into(), + }, + loro::ContainerID::Normal { + peer, + counter, + container_type, + } => ContainerID::Normal { + peer, + counter, + container_type: container_type.into(), + }, + } + } +} + +impl From<&loro::ContainerID> for ContainerID { + fn from(value: &loro::ContainerID) -> ContainerID { + match value { + loro::ContainerID::Root { + name, + container_type, + } => ContainerID::Root { + name: name.to_string(), + container_type: (*container_type).into(), + }, + loro::ContainerID::Normal { + peer, + counter, + container_type, + } => ContainerID::Normal { + peer: *peer, + counter: *counter, + container_type: (*container_type).into(), + }, + } + } +} + +impl From for loro::TreeID { + fn from(value: TreeID) -> Self { + Self { + peer: value.peer, + counter: value.counter, + } + } +} + +impl From for TreeID { + fn from(value: loro::TreeID) -> Self { + Self { + peer: value.peer, + counter: value.counter, + } + } +} + +impl From for loro::TreeParentId { + fn from(value: TreeParentId) -> Self { + match value { + TreeParentId::Node { id } => loro::TreeParentId::Node(id.into()), + TreeParentId::Root {} => loro::TreeParentId::Root, + TreeParentId::Deleted {} => loro::TreeParentId::Deleted, + TreeParentId::Unexist {} => loro::TreeParentId::Unexist, + } + } +} + +impl From for TreeParentId { + fn from(value: loro::TreeParentId) -> Self { + match value { + loro::TreeParentId::Node(id) => TreeParentId::Node { id: id.into() }, + loro::TreeParentId::Root => TreeParentId::Root {}, + loro::TreeParentId::Deleted => TreeParentId::Deleted {}, + loro::TreeParentId::Unexist => TreeParentId::Unexist {}, + } + } +} + +impl<'a> From<&loro::event::ContainerDiff<'a>> for ContainerDiff { + fn from(value: &loro::event::ContainerDiff<'a>) -> Self { + Self { + target: value.target.into(), + path: value + .path + .iter() + .map(|(id, index)| PathItem { + container: id.into(), + index: index.into(), + }) + .collect(), + is_unknown: value.is_unknown, + diff: (&value.diff).into(), + } + } +} + +impl From<&loro::Index> for Index { + fn from(value: &loro::Index) -> Self { + match value { + loro::Index::Key(key) => Index::Key { + key: key.to_string(), + }, + loro::Index::Seq(index) => Index::Seq { + index: *index as u32, + }, + loro::Index::Node(target) => Index::Node { + target: (*target).into(), + }, + } + } +} + +impl From for loro::Index { + fn from(value: Index) -> loro::Index { + match value { + Index::Key { key } => loro::Index::Key(key.into()), + Index::Seq { index } => loro::Index::Seq(index as usize), + Index::Node { target } => loro::Index::Node(target.into()), + } + } +} + +impl From<&loro::event::Diff<'_>> for Diff { + fn from(value: &loro::event::Diff) -> Self { + match value { + loro::event::Diff::List(l) => { + let mut ans = Vec::with_capacity(l.len()); + for item in l.iter() { + match item { + loro::event::ListDiffItem::Insert { insert, is_move } => { + let mut new_insert = Vec::with_capacity(insert.len()); + for v in insert.iter() { + new_insert.push(v.clone().into()); + } + ans.push(ListDiffItem::Insert { + insert: new_insert, + is_move: *is_move, + }); + } + loro::event::ListDiffItem::Delete { delete } => { + ans.push(ListDiffItem::Delete { + delete: *delete as u32, + }); + } + loro::event::ListDiffItem::Retain { retain } => { + ans.push(ListDiffItem::Retain { + retain: *retain as u32, + }); + } + } + } + Diff::List(ans) + } + loro::event::Diff::Text(t) => { + let mut ans = Vec::new(); + for item in t.iter() { + match item { + loro::TextDelta::Retain { retain, attributes } => { + ans.push(TextDelta::Retain { + retain: *retain as u32, + attributes: attributes.as_ref().map(|a| { + a.iter() + .map(|(k, v)| (k.to_string(), v.clone().into())) + .collect() + }), + }); + } + loro::TextDelta::Insert { insert, attributes } => { + ans.push(TextDelta::Insert { + insert: insert.to_string(), + attributes: attributes.as_ref().map(|a| { + a.iter() + .map(|(k, v)| (k.to_string(), v.clone().into())) + .collect() + }), + }); + } + loro::TextDelta::Delete { delete } => { + ans.push(TextDelta::Delete { + delete: *delete as u32, + }); + } + } + } + Diff::Text(ans) + } + loro::event::Diff::Map(m) => { + let mut updated = HashMap::new(); + for (key, value) in m.updated.iter() { + updated.insert(key.to_string(), value.as_ref().map(|v| v.clone().into())); + } + + Diff::Map(MapDelta { updated }) + } + loro::event::Diff::Tree(t) => { + let mut diff = Vec::new(); + for item in t.iter() { + diff.push(TreeDiffItem { + target: item.target.into(), + action: match &item.action { + loro::TreeExternalDiff::Create { + parent, + index, + position, + } => TreeExternalDiff::Create { + parent: (*parent).into(), + index: *index as u32, + fractional_index: position.to_string(), + }, + loro::TreeExternalDiff::Move { + parent, + index, + position, + old_parent, + old_index, + } => TreeExternalDiff::Move { + parent: (*parent).into(), + index: *index as u32, + fractional_index: position.to_string(), + old_parent: (*old_parent).into(), + old_index: *old_index as u32, + }, + loro::TreeExternalDiff::Delete { + old_parent, + old_index, + } => TreeExternalDiff::Delete { + old_parent: (*old_parent).into(), + old_index: *old_index as u32, + }, + }, + }); + } + Diff::Tree(TreeDiff { diff }) + } + loro::event::Diff::Counter(c) => Diff::Counter(*c), + loro::event::Diff::Unknown => Diff::Unknown {}, + } + } +} +impl From for EventTriggerKind { + fn from(kind: loro::EventTriggerKind) -> Self { + match kind { + loro::EventTriggerKind::Local => Self::Local, + loro::EventTriggerKind::Import => Self::Import, + loro::EventTriggerKind::Checkout => Self::Checkout, + } + } +} + +impl From for ValueOrContainer { + fn from(value: loro::ValueOrContainer) -> Self { + match value { + loro::ValueOrContainer::Value(v) => ValueOrContainer::Value(v.into()), + loro::ValueOrContainer::Container(c) => ValueOrContainer::Container(c.into()), + } + } +} + +impl From for Container { + fn from(value: loro::Container) -> Self { + match value { + loro::Container::List(c) => Container::List(LoroList(c)), + loro::Container::Map(c) => Container::Map(LoroMap(c)), + loro::Container::MovableList(c) => Container::MovableList(LoroMovableList(c)), + loro::Container::Text(c) => Container::Text(LoroText(c)), + loro::Container::Tree(c) => Container::Tree(LoroTree(c)), + loro::Container::Counter(c) => todo!(), + loro::Container::Unknown(c) => todo!(), + } + } +} diff --git a/src/doc.rs b/src/doc.rs new file mode 100644 index 0000000..e1ed9fe --- /dev/null +++ b/src/doc.rs @@ -0,0 +1,857 @@ +use loro::{LoroDoc as LoroDocInner, PeerID, Timestamp}; +use pyo3::{prelude::*, types::PyBytes}; +use std::{fmt::Display, sync::Arc}; + +use crate::{ + container::{LoroCounter, LoroList, LoroMap, LoroMovableList, LoroText, LoroTree}, + convert::pyobject_to_container_id, + err::PyLoroResult, + event::{DiffEvent, Subscription}, + value::{ContainerID, ContainerType, LoroValue, Ordering}, + version::{Frontiers, VersionRange, VersionVector}, +}; + +pub fn register_class(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} + +#[pyclass(frozen)] +pub struct LoroDoc { + doc: LoroDocInner, +} + +impl Default for LoroDoc { + fn default() -> Self { + let doc = LoroDocInner::new(); + Self { doc } + } +} + +#[pymethods] +impl LoroDoc { + /// Create a new `LoroDoc` instance. + #[new] + pub fn new() -> Self { + Self::default() + } + + /// Duplicate the document with a different PeerID + /// + /// The time complexity and space complexity of this operation are both O(n), + /// + /// When called in detached mode, it will fork at the current state frontiers. + /// It will have the same effect as `fork_at(&self.state_frontiers())`. + #[inline] + pub fn fork(&self) -> Self { + let doc = self.doc.fork(); + Self { doc } + } + + /// Fork the document at the given frontiers. + /// + /// The created doc will only contain the history before the specified frontiers. + pub fn fork_at(&self, frontiers: &Frontiers) -> Self { + let new_doc = self.doc.fork_at(&frontiers.into()); + Self { doc: new_doc } + } + + /// Get the configurations of the document. + #[inline] + pub fn config(&self) -> Configure { + self.doc.config().clone().into() + } + + /// Get `Change` at the given id. + /// + /// `Change` is a grouped continuous operations that share the same id, timestamp, commit message. + /// + /// - The id of the `Change` is the id of its first op. + /// - The second op's id is `{ peer: change.id.peer, counter: change.id.counter + 1 }` + /// + /// The same applies on `Lamport`: + /// + /// - The lamport of the `Change` is the lamport of its first op. + /// - The second op's lamport is `change.lamport + 1` + /// + /// The length of the `Change` is how many operations it contains + // TODO: + // pub fn get_change(&self, id: ID) -> Option { + // let change = self.doc.oplog().try_lock().unwrap().get_change_at(id)?; + // Some(ChangeMeta::from_change(&change)) + // } + + /// Decodes the metadata for an imported blob from the provided bytes. + // TODO: + // #[classmethod] + // pub fn decode_import_blob_meta( + // cls: &PyType, + // bytes: &[u8], + // check_checksum: bool, + // ) -> LoroResult { + // LoroDocInner::decode_import_blob_meta(bytes, check_checksum) + // } + + /// Set whether to record the timestamp of each change. Default is `false`. + /// + /// If enabled, the Unix timestamp will be recorded for each change automatically. + /// + /// You can set each timestamp manually when committing a change. + /// + /// NOTE: Timestamps are forced to be in ascending order. + /// If you commit a new change with a timestamp that is less than the existing one, + /// the largest existing timestamp will be used instead. + pub fn set_record_timestamp(&self, record: bool) { + self.doc.set_record_timestamp(record); + } + + /// Enables editing in detached mode, which is disabled by default. + /// + /// The doc enter detached mode after calling `detach` or checking out a non-latest version. + /// + /// # Important Notes: + /// + /// - This mode uses a different PeerID for each checkout. + /// - Ensure no concurrent operations share the same PeerID if set manually. + /// - Importing does not affect the document's state or version; changes are + /// recorded in the [OpLog] only. Call `checkout` to apply changes. + #[inline] + pub fn set_detached_editing(&self, enable: bool) { + self.doc.set_detached_editing(enable); + } + + /// Whether editing the doc in detached mode is allowed, which is disabled by + /// default. + /// + /// The doc enter detached mode after calling `detach` or checking out a non-latest version. + /// + /// # Important Notes: + /// + /// - This mode uses a different PeerID for each checkout. + /// - Ensure no concurrent operations share the same PeerID if set manually. + /// - Importing does not affect the document's state or version; changes are + /// recorded in the [OpLog] only. Call `checkout` to apply changes. + #[inline] + pub fn is_detached_editing_enabled(&self) -> bool { + self.doc.is_detached_editing_enabled() + } + + /// Set the interval of mergeable changes, in seconds. + /// + /// If two continuous local changes are within the interval, they will be merged into one change. + /// The default value is 1000 seconds. + #[inline] + pub fn set_change_merge_interval(&self, interval: i64) { + self.doc.set_change_merge_interval(interval); + } + + /// Set the rich text format configuration of the document. + /// + /// You need to config it if you use rich text `mark` method. + /// Specifically, you need to config the `expand` property of each style. + /// + /// Expand is used to specify the behavior of expanding when new text is inserted at the + /// beginning or end of the style. + // TODO: + // #[inline] + // pub fn config_text_style(&self, text_style: StyleConfigMap) { + // self.doc.config_text_style(text_style) + // } + + /// Attach the document state to the latest known version. + /// + /// > The document becomes detached during a `checkout` operation. + /// > Being `detached` implies that the `DocState` is not synchronized with the latest version of the `OpLog`. + /// > In a detached state, the document is not editable, and any `import` operations will be + /// > recorded in the `OpLog` without being applied to the `DocState`. + #[inline] + pub fn attach(&self) { + self.doc.attach() + } + + /// Checkout the `DocState` to a specific version. + /// + /// The document becomes detached during a `checkout` operation. + /// Being `detached` implies that the `DocState` is not synchronized with the latest version of the `OpLog`. + /// In a detached state, the document is not editable, and any `import` operations will be + /// recorded in the `OpLog` without being applied to the `DocState`. + /// + /// You should call `attach` to attach the `DocState` to the latest version of `OpLog`. + #[inline] + pub fn checkout(&self, frontiers: &Frontiers) -> PyLoroResult<()> { + self.doc.checkout(&frontiers.into())?; + Ok(()) + } + + /// Checkout the `DocState` to the latest version. + /// + /// > The document becomes detached during a `checkout` operation. + /// > Being `detached` implies that the `DocState` is not synchronized with the latest version of the `OpLog`. + /// > In a detached state, the document is not editable, and any `import` operations will be + /// > recorded in the `OpLog` without being applied to the `DocState`. + /// + /// This has the same effect as `attach`. + #[inline] + pub fn checkout_to_latest(&self) { + self.doc.checkout_to_latest() + } + + // TODO: + + // /// Compare the frontiers with the current OpLog's version. + // /// + // /// If `other` contains any version that's not contained in the current OpLog, return [Ordering::Less]. + #[inline] + pub fn cmp_with_frontiers(&self, other: &Frontiers) -> Ordering { + self.doc.cmp_with_frontiers(&other.into()).into() + } + + // /// Compare two frontiers. + // /// + // /// If the frontiers are not included in the document, return [`FrontiersNotIncluded`]. + // #[inline] + // pub fn cmp_frontiers( + // &self, + // a: &Frontiers, + // b: &Frontiers, + // ) -> Result, FrontiersNotIncluded> { + // self.doc.cmp_frontiers(a, b) + // } + + // /// Force the document enter the detached mode. + // /// + // /// In this mode, when you importing new updates, the [loro_internal::DocState] will not be changed. + // /// + // /// Learn more at https://loro.dev/docs/advanced/doc_state_and_oplog#attacheddetached-status + #[inline] + pub fn detach(&self) { + self.doc.detach() + } + + // /// Import a batch of updates/snapshot. + // /// + // /// The data can be in arbitrary order. The import result will be the same. + #[inline] + pub fn import_batch(&self, bytes: Vec>) -> PyLoroResult { + let vec_bytes: Vec> = bytes.into_iter().map(|b| b.as_bytes().to_vec()).collect(); + let status = self.doc.import_batch(&vec_bytes)?; + Ok(ImportStatus::from(status)) + } + + /// Get a [LoroMovableList] by container id. + /// + /// If the provided id is string, it will be converted into a root container id with the name of the string. + #[inline] + pub fn get_movable_list(&self, py: Python, obj: PyObject) -> PyResult { + let container_id = pyobject_to_container_id(py, obj, ContainerType::MovableList {})?; + Ok(LoroMovableList(self.doc.get_movable_list(container_id))) + } + + /// Get a [LoroList] by container id. + /// + /// If the provided id is string, it will be converted into a root container id with the name of the string. + #[inline] + pub fn get_list(&self, py: Python, obj: PyObject) -> PyResult { + let container_id = pyobject_to_container_id(py, obj, ContainerType::List {})?; + Ok(LoroList(self.doc.get_list(container_id))) + } + + /// Get a [LoroMap] by container id. + /// + /// If the provided id is string, it will be converted into a root container id with the name of the string. + #[inline] + pub fn get_map(&self, py: Python, obj: PyObject) -> PyResult { + let container_id = pyobject_to_container_id(py, obj, ContainerType::Map {})?; + Ok(LoroMap(self.doc.get_map(container_id))) + } + + /// Get a [LoroText] by container id. + /// + /// If the provided id is string, it will be converted into a root container id with the name of the string. + #[inline] + pub fn get_text(&self, py: Python, obj: PyObject) -> PyResult { + let container_id = pyobject_to_container_id(py, obj, ContainerType::Text {})?; + Ok(LoroText(self.doc.get_text(container_id))) + } + + /// Get a [LoroTree] by container id. + /// + /// If the provided id is string, it will be converted into a root container id with the name of the string. + #[inline] + pub fn get_tree(&self, py: Python, obj: PyObject) -> PyResult { + let container_id = pyobject_to_container_id(py, obj, ContainerType::Tree {})?; + Ok(LoroTree(self.doc.get_tree(container_id))) + } + + /// Get a [LoroCounter] by container id. + /// + /// If the provided id is string, it will be converted into a root container id with the name of the string. + #[inline] + pub fn get_counter(&self, py: Python, obj: PyObject) -> PyResult { + let container_id = pyobject_to_container_id(py, obj, ContainerType::Counter {})?; + Ok(LoroCounter(self.doc.get_counter(container_id))) + } + + /// Commit the cumulative auto commit transaction. + /// + /// There is a transaction behind every operation. + /// The events will be emitted after a transaction is committed. A transaction is committed when: + /// + /// - `doc.commit()` is called. + /// - `doc.export(mode)` is called. + /// - `doc.import(data)` is called. + /// - `doc.checkout(version)` is called. + #[inline] + pub fn commit(&self) { + self.doc.commit() + } + + /// Commit the cumulative auto commit transaction with custom configure. + /// + /// There is a transaction behind every operation. + /// It will automatically commit when users invoke export or import. + /// The event will be sent after a transaction is committed + #[inline] + pub fn commit_with(&self, options: &CommitOptions) { + self.doc.commit_with(options.into()) + } + + /// Set commit message for the current uncommitted changes + pub fn set_next_commit_message(&self, msg: &str) { + self.doc.set_next_commit_message(msg) + } + + /// Whether the document is in detached mode, where the [loro_internal::DocState] is not + /// synchronized with the latest version of the [loro_internal::OpLog]. + #[inline] + pub fn is_detached(&self) -> bool { + self.doc.is_detached() + } + + /// Import updates/snapshot exported by [`LoroDoc::export_snapshot`] or [`LoroDoc::export_from`]. + #[inline] + pub fn import(&self, bytes: &[u8]) -> PyLoroResult { + let status = self.doc.import(bytes)?; + Ok(ImportStatus::from(status)) + } + + /// Import updates/snapshot exported by [`LoroDoc::export_snapshot`] or [`LoroDoc::export_from`]. + /// + /// It marks the import with a custom `origin` string. It can be used to track the import source + /// in the generated events. + #[inline] + pub fn import_with(&self, bytes: &[u8], origin: &str) -> PyLoroResult { + let status = self.doc.import_with(bytes, origin)?; + Ok(ImportStatus::from(status)) + } + + // /// Import the json schema updates. + // /// + // /// only supports backward compatibility but not forward compatibility. + // #[inline] + // pub fn import_json_updates>( + // &self, + // json: T, + // ) -> Result { + // self.doc.import_json_updates(json) + // } + + // /// Export the current state with json-string format of the document. + // #[inline] + // pub fn export_json_updates( + // &self, + // start_vv: &VersionVector, + // end_vv: &VersionVector, + // ) -> JsonSchema { + // self.doc.export_json_updates(start_vv, end_vv) + // } + + // /// Export all the ops not included in the given `VersionVector` + // #[deprecated( + // since = "1.0.0", + // note = "Use `export` with `ExportMode::Updates` instead" + // )] + // #[inline] + // pub fn export_from(&self, vv: &VersionVector) -> Vec { + // self.doc.export_from(vv) + // } + + // /// Export the current state and history of the document. + // #[deprecated( + // since = "1.0.0", + // note = "Use `export` with `ExportMode::Snapshot` instead" + // )] + // #[inline] + // pub fn export_snapshot(&self) -> Vec { + // self.doc.export_snapshot().unwrap() + // } + + /// Convert `Frontiers` into `VersionVector` + #[inline] + pub fn frontiers_to_vv(&self, frontiers: &Frontiers) -> Option { + self.doc + .frontiers_to_vv(&frontiers.into()) + .map(|vv| vv.into()) + } + + // /// Minimize the frontiers by removing the unnecessary entries. + // pub fn minimize_frontiers(&self, frontiers: &Frontiers) -> Result { + // self.with_oplog(|oplog| shrink_frontiers(frontiers, oplog.dag())) + // } + + /// Convert `VersionVector` into `Frontiers` + #[inline] + pub fn vv_to_frontiers(&self, vv: &VersionVector) -> Frontiers { + self.doc.vv_to_frontiers(&(vv.into())).into() + } + + // /// Access the `OpLog`. + // /// + // /// NOTE: Please be ware that the API in `OpLog` is unstable + // #[inline] + // pub fn with_oplog(&self, f: impl FnOnce(&OpLog) -> R) -> R { + // let oplog = self.doc.oplog().try_lock().unwrap(); + // f(&oplog) + // } + + // /// Access the `DocState`. + // /// + // /// NOTE: Please be ware that the API in `DocState` is unstable + // #[inline] + // pub fn with_state(&self, f: impl FnOnce(&mut DocState) -> R) -> R { + // let mut state = self.doc.app_state().try_lock().unwrap(); + // f(&mut state) + // } + + /// Get the `VersionVector` version of `OpLog` + #[inline] + pub fn oplog_vv(&self) -> VersionVector { + self.doc.oplog_vv().into() + } + + /// Get the `VersionVector` version of `DocState` + #[inline] + pub fn state_vv(&self) -> VersionVector { + self.doc.state_vv().into() + } + + /// The doc only contains the history since this version + /// + /// This is empty if the doc is not shallow. + /// + /// The ops included by the shallow history start version vector are not in the doc. + #[inline] + pub fn shallow_since_vv(&self) -> VersionVector { + loro::VersionVector::from_im_vv(&self.doc.shallow_since_vv()).into() + } + + /// The doc only contains the history since this version + /// + /// This is empty if the doc is not shallow. + /// + /// The ops included by the shallow history start frontiers are not in the doc. + #[inline] + pub fn shallow_since_frontiers(&self) -> Frontiers { + self.doc.shallow_since_frontiers().into() + } + + /// Get the total number of operations in the `OpLog` + #[inline] + pub fn len_ops(&self) -> usize { + self.doc.len_ops() + } + + /// Get the total number of changes in the `OpLog` + #[inline] + pub fn len_changes(&self) -> usize { + self.doc.len_changes() + } + + /// Get the shallow value of the document. + #[inline] + pub fn get_value(&self) -> LoroValue { + self.doc.get_value().into() + } + + /// Get the entire state of the current DocState + #[inline] + pub fn get_deep_value(&self) -> LoroValue { + self.doc.get_deep_value().into() + } + + /// Get the entire state of the current DocState with container id + pub fn get_deep_value_with_id(&self) -> LoroValue { + self.doc.get_deep_value_with_id().into() + } + + /// Get the `Frontiers` version of `OpLog` + #[inline] + pub fn oplog_frontiers(&self) -> Frontiers { + self.doc.oplog_frontiers().into() + } + + /// Get the `Frontiers` version of `DocState` + /// + /// Learn more about [`Frontiers`](https://loro.dev/docs/advanced/version_deep_dive) + #[inline] + pub fn state_frontiers(&self) -> Frontiers { + self.doc.state_frontiers().into() + } + + /// Get the PeerID + #[getter] + #[inline] + pub fn peer_id(&self) -> PeerID { + self.doc.peer_id() + } + + /// Change the PeerID + /// + /// NOTE: You need to make sure there is no chance two peer have the same PeerID. + /// If it happens, the document will be corrupted. + #[inline] + pub fn set_peer_id(&self, peer: PeerID) -> PyLoroResult<()> { + self.doc.set_peer_id(peer)?; + Ok(()) + } + + /// Subscribe the events of a container. + /// + /// The callback will be invoked after a transaction that change the container. + /// Returns a subscription that can be used to unsubscribe. + /// + /// The events will be emitted after a transaction is committed. A transaction is committed when: + /// + /// - `doc.commit()` is called. + /// - `doc.export(mode)` is called. + /// - `doc.import(data)` is called. + /// - `doc.checkout(version)` is called. + /// + /// # Example + /// + /// ``` + /// # use loro::LoroDoc; + /// # use std::sync::{atomic::AtomicBool, Arc}; + /// # use loro::{event::DiffEvent, LoroResult, TextDelta}; + /// # + /// let doc = LoroDoc::new(); + /// let text = doc.get_text("text"); + /// let ran = Arc::new(AtomicBool::new(false)); + /// let ran2 = ran.clone(); + /// let sub = doc.subscribe( + /// &text.id(), + /// Arc::new(move |event| { + /// assert!(event.triggered_by.is_local()); + /// for event in event.events { + /// let delta = event.diff.as_text().unwrap(); + /// let d = TextDelta::Insert { + /// insert: "123".into(), + /// attributes: Default::default(), + /// }; + /// assert_eq!(delta, &vec![d]); + /// ran2.store(true, std::sync::atomic::Ordering::Relaxed); + /// } + /// }), + /// ); + /// text.insert(0, "123").unwrap(); + /// doc.commit(); + /// assert!(ran.load(std::sync::atomic::Ordering::Relaxed)); + /// // unsubscribe + /// sub.unsubscribe(); + /// ``` + #[inline] + pub fn subscribe(&self, container_id: &ContainerID, callback: Py) -> Subscription { + let subscription = self.doc.subscribe( + &container_id.into(), + Arc::new(move |e| { + Python::with_gil(|py| { + callback.call1(py, (DiffEvent::from(e),)).unwrap(); + }); + }), + ); + Subscription::new(subscription) + } + + // /// Subscribe all the events. + // /// + // /// The callback will be invoked when any part of the [loro_internal::DocState] is changed. + // /// Returns a subscription that can be used to unsubscribe. + // /// + // /// The events will be emitted after a transaction is committed. A transaction is committed when: + // /// + // /// - `doc.commit()` is called. + // /// - `doc.export(mode)` is called. + // /// - `doc.import(data)` is called. + // /// - `doc.checkout(version)` is called. + // #[inline] + // pub fn subscribe_root(&self, callback: Subscriber) -> Subscription { + // // self.doc.subscribe_root(callback) + // self.doc.subscribe_root(Arc::new(move |e| { + // callback(DiffEvent::from(e)); + // })) + // } + + // /// Subscribe the local update of the document. + // pub fn subscribe_local_update(&self, callback: LocalUpdateCallback) -> Subscription { + // self.doc.subscribe_local_update(callback) + // } + + // /// Subscribe the peer id change of the document. + // pub fn subscribe_peer_id_change(&self, callback: PeerIdUpdateCallback) -> Subscription { + // self.doc.subscribe_peer_id_change(callback) + // } + + // /// Estimate the size of the document states in memory. + // #[inline] + // pub fn log_estimate_size(&self) { + // self.doc.log_estimated_size(); + // } + + // /// Check the correctness of the document state by comparing it with the state + // /// calculated by applying all the history. + // #[inline] + // pub fn check_state_correctness_slow(&self) { + // self.doc.check_state_diff_calc_consistency_slow() + // } + + // /// Get the handler by the path. + // #[inline] + // pub fn get_by_path(&self, path: &[Index]) -> Option { + // self.doc.get_by_path(path).map(ValueOrContainer::from) + // } + + // /// Get the handler by the string path. + // #[inline] + // pub fn get_by_str_path(&self, path: &str) -> Option { + // self.doc.get_by_str_path(path).map(ValueOrContainer::from) + // } + + // /// Get the absolute position of the given cursor. + // /// + // /// # Example + // /// + // /// ``` + // /// # use loro::{LoroDoc, ToJson}; + // /// let doc = LoroDoc::new(); + // /// let text = &doc.get_text("text"); + // /// text.insert(0, "01234").unwrap(); + // /// let pos = text.get_cursor(5, Default::default()).unwrap(); + // /// assert_eq!(doc.get_cursor_pos(&pos).unwrap().current.pos, 5); + // /// text.insert(0, "01234").unwrap(); + // /// assert_eq!(doc.get_cursor_pos(&pos).unwrap().current.pos, 10); + // /// text.delete(0, 10).unwrap(); + // /// assert_eq!(doc.get_cursor_pos(&pos).unwrap().current.pos, 0); + // /// text.insert(0, "01234").unwrap(); + // /// assert_eq!(doc.get_cursor_pos(&pos).unwrap().current.pos, 5); + // /// ``` + // #[inline] + // pub fn get_cursor_pos( + // &self, + // cursor: &Cursor, + // ) -> Result { + // self.doc.query_pos(cursor) + // } + + // /// Get the inner LoroDoc ref. + // // #[inline] + // // pub fn inner(&self) -> &InnerLoroDoc { + // // &self.doc + // // } + + // /// Whether the history cache is built. + // #[inline] + // pub fn has_history_cache(&self) -> bool { + // self.doc.has_history_cache() + // } + + // /// Free the history cache that is used for making checkout faster. + // /// + // /// If you use checkout that switching to an old/concurrent version, the history cache will be built. + // /// You can free it by calling this method. + // #[inline] + // pub fn free_history_cache(&self) { + // self.doc.free_history_cache() + // } + + // /// Free the cached diff calculator that is used for checkout. + // #[inline] + // pub fn free_diff_calculator(&self) { + // self.doc.free_diff_calculator() + // } + + // /// Encoded all ops and history cache to bytes and store them in the kv store. + // /// + // /// This will free up the memory that used by parsed ops + // #[inline] + // pub fn compact_change_store(&self) { + // self.doc.compact_change_store() + // } + + // /// Export the document in the given mode. + // pub fn export(&self, mode: ExportMode) -> Result, LoroEncodeError> { + // self.doc.export(mode) + // } + + // /// Analyze the container info of the doc + // /// + // /// This is used for development and debugging. It can be slow. + // // TODO: + // // pub fn analyze(&self) -> DocAnalysis { + // // self.doc.analyze() + // // } + + // /// Get the path from the root to the container + // pub fn get_path_to_container(&self, id: &ContainerID) -> Option> { + // self.doc.get_path_to_container(id) + // } + + // /// Evaluate a JSONPath expression on the document and return matching values or handlers. + // /// + // /// This method allows querying the document structure using JSONPath syntax. + // /// It returns a vector of `ValueOrHandler` which can represent either primitive values + // /// or container handlers, depending on what the JSONPath expression matches. + // /// + // /// # Arguments + // /// + // /// * `path` - A string slice containing the JSONPath expression to evaluate. + // /// + // /// # Returns + // /// + // /// A `Result` containing either: + // /// - `Ok(Vec)`: A vector of matching values or handlers. + // /// - `Err(String)`: An error message if the JSONPath expression is invalid or evaluation fails. + // /// + // /// # Example + // /// + // /// ``` + // /// # use loro::{LoroDoc, ToJson}; + // /// + // /// let doc = LoroDoc::new(); + // /// let map = doc.get_map("users"); + // /// map.insert("alice", 30).unwrap(); + // /// map.insert("bob", 25).unwrap(); + // /// + // /// let result = doc.jsonpath("$.users.alice").unwrap(); + // /// assert_eq!(result.len(), 1); + // /// assert_eq!(result[0].as_value().unwrap().to_json_value(), serde_json::json!(30)); + // /// ``` + // #[inline] + // pub fn jsonpath(&self, path: &str) -> Result, JsonPathError> { + // self.doc.jsonpath(path).map(|vec| { + // vec.into_iter() + // .map(|v| match v { + // ValueOrHandler::Value(v) => ValueOrContainer::Value(v), + // ValueOrHandler::Handler(h) => ValueOrContainer::Container(h.into()), + // }) + // .collect() + // }) + // } + + // /// Get the number of operations in the pending transaction. + // /// + // /// The pending transaction is the one that is not committed yet. It will be committed + // /// after calling `doc.commit()`, `doc.export(mode)` or `doc.checkout(version)`. + // pub fn get_pending_txn_len(&self) -> usize { + // self.doc.get_pending_txn_len() + // } + + // /// Traverses the ancestors of the Change containing the given ID, including itself. + // /// + // /// This method visits all ancestors in causal order, from the latest to the oldest, + // /// based on their Lamport timestamps. + // /// + // /// # Arguments + // /// + // /// * `ids` - The IDs of the Change to start the traversal from. + // /// * `f` - A mutable function that is called for each ancestor. It can return `ControlFlow::Break(())` to stop the traversal. + // pub fn travel_change_ancestors( + // &self, + // ids: &[ID], + // f: &mut dyn FnMut(ChangeMeta) -> ControlFlow<()>, + // ) -> Result<(), ChangeTravelError> { + // self.doc.travel_change_ancestors(ids, f) + // } + + // /// Check if the doc contains the full history. + // pub fn is_shallow(&self) -> bool { + // self.doc.is_shallow() + // } + + // /// Gets container IDs modified in the given ID range. + // /// + // /// **NOTE:** This method will implicitly commit. + // /// + // /// This method can be used in conjunction with `doc.travel_change_ancestors()` to traverse + // /// the history and identify all changes that affected specific containers. + // /// + // /// # Arguments + // /// + // /// * `id` - The starting ID of the change range + // /// * `len` - The length of the change range to check + // pub fn get_changed_containers_in(&self, id: ID, len: usize) -> FxHashSet { + // self.doc.get_changed_containers_in(id, len) + // } +} + +#[pyclass(frozen)] +pub struct Configure(pub loro::Configure); + +impl From for Configure { + fn from(value: loro::Configure) -> Self { + Self(value) + } +} + +// TODO: Implement the methods for Configure + +#[pyclass(get_all, set_all, str)] +#[derive(Debug)] +pub struct ImportStatus { + pub success: VersionRange, + pub pending: Option, +} + +impl From for ImportStatus { + fn from(value: loro::ImportStatus) -> Self { + let a = value.success; + Self { + success: a.into(), + pending: value.pending.map(|x| x.into()), + } + } +} + +impl Display for ImportStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +#[pyclass(get_all, set_all, str)] +#[derive(Debug)] +pub struct CommitOptions { + pub origin: Option, + pub immediate_renew: bool, + pub timestamp: Option, + pub commit_msg: Option, +} + +impl From<&CommitOptions> for loro::CommitOptions { + fn from(value: &CommitOptions) -> Self { + loro::CommitOptions { + origin: value.origin.clone().map(|x| x.into()), + immediate_renew: value.immediate_renew, + timestamp: value.timestamp, + commit_msg: value.commit_msg.clone().map(|x| x.into()), + } + } +} + +impl Display for CommitOptions { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} diff --git a/src/err.rs b/src/err.rs new file mode 100644 index 0000000..e3cd4c3 --- /dev/null +++ b/src/err.rs @@ -0,0 +1,17 @@ +use pyo3::{exceptions::PyValueError, PyErr}; + +pub struct PyLoroError(loro::LoroError); + +pub type PyLoroResult = Result; + +impl From for PyLoroError { + fn from(other: loro::LoroError) -> Self { + Self(other) + } +} + +impl From for PyErr { + fn from(value: PyLoroError) -> Self { + PyValueError::new_err(value.0.to_string()) + } +} diff --git a/src/event.rs b/src/event.rs new file mode 100644 index 0000000..ae28026 --- /dev/null +++ b/src/event.rs @@ -0,0 +1,440 @@ +use crate::value::{ContainerID, LoroValue, TreeID, TreeParentId, ValueOrContainer}; +use pyo3::prelude::*; +use std::collections::HashMap; +use std::fmt; + +pub fn register_class(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} + +#[pyclass(str, get_all, set_all)] +#[derive(Debug)] +pub struct DiffEvent { + /// How the event is triggered. + pub triggered_by: EventTriggerKind, + /// The origin of the event. + pub origin: String, + /// The current receiver of the event. + pub current_target: Option, + /// The diffs of the event. + pub events: Vec, +} + +impl fmt::Display for DiffEvent { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "DiffEvent(triggered_by={}, origin='{}', current_target={}, events={})", + self.triggered_by, + self.origin, + self.current_target + .as_ref() + .map_or("None".to_string(), |v| format!("{}", v)), + self.events + .iter() + .map(|e| format!("{}", e)) + .collect::>() + .join(", ") + ) + } +} + +impl From> for DiffEvent { + fn from(diff_event: loro::event::DiffEvent) -> Self { + Self { + triggered_by: diff_event.triggered_by.into(), + origin: diff_event.origin.to_string(), + current_target: diff_event.current_target.map(|v| v.into()), + events: diff_event.events.iter().map(ContainerDiff::from).collect(), + } + } +} + +/// The kind of the event trigger. +#[pyclass(eq, eq_int)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum EventTriggerKind { + /// The event is triggered by a local transaction. + Local, + /// The event is triggered by importing + Import, + /// The event is triggered by checkout + Checkout, +} + +impl fmt::Display for EventTriggerKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + EventTriggerKind::Local => write!(f, "Local"), + EventTriggerKind::Import => write!(f, "Import"), + EventTriggerKind::Checkout => write!(f, "Checkout"), + } + } +} + +#[pyclass(str, get_all, set_all)] +#[derive(Debug, Clone)] +pub struct PathItem { + pub container: ContainerID, + pub index: Index, +} + +impl fmt::Display for PathItem { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "PathItem(container={}, index={})", + self.container, self.index + ) + } +} + +#[pyclass(str, get_all, set_all)] +#[derive(Debug, Clone)] +/// A diff of a container. +pub struct ContainerDiff { + /// The target container id of the diff. + pub target: ContainerID, + /// The path of the diff. + pub path: Vec, + /// Whether the diff is from unknown container. + pub is_unknown: bool, + /// The diff + pub diff: Diff, +} + +impl fmt::Display for ContainerDiff { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "ContainerDiff(target={}, path=[{}], is_unknown={}, diff={})", + self.target, + self.path + .iter() + .map(|p| format!("{}", p)) + .collect::>() + .join(", "), + self.is_unknown, + self.diff + ) + } +} + +#[pyclass(str)] +#[derive(Debug, Clone)] +pub enum Index { + Key { key: String }, + Seq { index: u32 }, + Node { target: TreeID }, +} + +impl fmt::Display for Index { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Index::Key { key } => write!(f, "Key(key='{}')", key), + Index::Seq { index } => write!(f, "Seq(index={})", index), + Index::Node { target } => write!(f, "Node(target={})", target), + } + } +} + +#[pyclass] +#[derive(Debug, Clone)] +pub enum Diff { + /// A list diff. + List(Vec), + /// A text diff. + Text(Vec), + /// A map diff. + Map(MapDelta), + /// A tree diff. + Tree(TreeDiff), + /// A counter diff. + Counter(f64), + /// An unknown diff. + Unknown {}, +} + +impl fmt::Display for Diff { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Diff::List(diff) => write!( + f, + "List([{}])", + diff.iter() + .map(|d| format!("{}", d)) + .collect::>() + .join(", ") + ), + Diff::Text(diff) => write!( + f, + "Text([{}])", + diff.iter() + .map(|d| format!("{}", d)) + .collect::>() + .join(", ") + ), + Diff::Map(diff) => write!(f, "Map({})", diff), + Diff::Tree(diff) => write!(f, "Tree({})", diff), + Diff::Counter(diff) => write!(f, "Counter({})", diff), + Diff::Unknown {} => write!(f, "Unknown()"), + } + } +} + +#[pyclass(str, get_all, set_all)] +#[derive(Debug, Clone)] +pub enum TextDelta { + Retain { + retain: u32, + attributes: Option>, + }, + Insert { + insert: String, + attributes: Option>, + }, + Delete { + delete: u32, + }, +} + +impl fmt::Display for TextDelta { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + TextDelta::Retain { retain, attributes } => { + write!( + f, + "Retain(retain={}, attributes={})", + retain, + attributes.as_ref().map_or("None".to_string(), |a| format!( + "{{{}}}", + a.iter() + .map(|(k, v)| format!("'{}': {}", k, v)) + .collect::>() + .join(", ") + )) + ) + } + TextDelta::Insert { insert, attributes } => { + write!( + f, + "Insert(insert='{}', attributes={})", + insert, + attributes.as_ref().map_or("None".to_string(), |a| format!( + "{{{}}}", + a.iter() + .map(|(k, v)| format!("'{}': {}", k, v)) + .collect::>() + .join(", ") + )) + ) + } + TextDelta::Delete { delete } => { + write!(f, "Delete(delete={})", delete) + } + } + } +} + +#[pyclass] +#[derive(Debug, Clone)] +pub enum ListDiffItem { + /// Insert a new element into the list. + Insert { + /// The new elements to insert. + insert: Vec, + /// Whether the new elements are created by moving + is_move: bool, + }, + /// Delete n elements from the list at the current index. + Delete { + /// The number of elements to delete. + delete: u32, + }, + /// Retain n elements in the list. + /// + /// This is used to keep the current index unchanged. + Retain { + /// The number of elements to retain. + retain: u32, + }, +} + +impl fmt::Display for ListDiffItem { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ListDiffItem::Insert { insert, is_move } => { + write!( + f, + "Insert(insert=[{}], is_move={})", + insert + .iter() + .map(|v| format!("{}", v)) + .collect::>() + .join(", "), + is_move + ) + } + ListDiffItem::Delete { delete } => { + write!(f, "Delete(delete={})", delete) + } + ListDiffItem::Retain { retain } => { + write!(f, "Retain(retain={})", retain) + } + } + } +} + +#[pyclass(str, get_all, set_all)] +#[derive(Debug, Clone)] +pub struct MapDelta { + /// All the updated keys and their new values. + pub updated: HashMap>, +} + +impl fmt::Display for MapDelta { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "MapDelta(updated={{{}}})", + self.updated + .iter() + .map(|(k, v)| format!( + "'{}': {}", + k, + v.as_ref().map_or("None".to_string(), |v| format!("{}", v)) + )) + .collect::>() + .join(", ") + ) + } +} + +#[pyclass(str, get_all, set_all)] +#[derive(Debug, Clone)] +pub struct TreeDiff { + pub diff: Vec, +} + +impl fmt::Display for TreeDiff { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "TreeDiff(diff=[{}])", + self.diff + .iter() + .map(|d| format!("{}", d)) + .collect::>() + .join(", ") + ) + } +} + +#[pyclass(str, get_all, set_all)] +#[derive(Debug, Clone)] +pub struct TreeDiffItem { + pub target: TreeID, + pub action: TreeExternalDiff, +} + +impl fmt::Display for TreeDiffItem { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "TreeDiffItem(target={}, action={})", + self.target, self.action + ) + } +} + +#[pyclass(str, get_all, set_all)] +#[derive(Debug, Clone)] +pub enum TreeExternalDiff { + Create { + parent: TreeParentId, + index: u32, + fractional_index: String, + }, + Move { + parent: TreeParentId, + index: u32, + fractional_index: String, + old_parent: TreeParentId, + old_index: u32, + }, + Delete { + old_parent: TreeParentId, + old_index: u32, + }, +} + +impl fmt::Display for TreeExternalDiff { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + TreeExternalDiff::Create { + parent, + index, + fractional_index, + } => { + write!( + f, + "Create(parent={}, index={}, fractional_index='{}')", + parent, index, fractional_index + ) + } + TreeExternalDiff::Move { + parent, + index, + fractional_index, + old_parent, + old_index, + } => { + write!( + f, + "Move(parent={}, index={}, fractional_index='{}', old_parent={}, old_index={})", + parent, index, fractional_index, old_parent, old_index + ) + } + TreeExternalDiff::Delete { + old_parent, + old_index, + } => { + write!( + f, + "Delete(old_parent={}, old_index={})", + old_parent, old_index + ) + } + } + } +} + +#[pyclass] +pub struct Subscription(Option); + +impl Subscription { + pub fn new(subscription: loro::Subscription) -> Self { + Self(Some(subscription)) + } + + pub fn __call__(mut slf: PyRefMut) -> PyResult<()> { + // 使用 take() 获取所有权 + if let Some(subscription) = std::mem::take(&mut slf.0) { + subscription.unsubscribe(); + } + Ok(()) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..372ec1c --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,20 @@ +use pyo3::prelude::*; + +mod container; +mod convert; +mod doc; +mod err; +mod event; +mod value; +mod version; + +/// A Python module implemented in Rust. +#[pymodule] +fn loro(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + doc::register_class(m)?; + container::register_class(m)?; + event::register_class(m)?; + value::register_class(m)?; + version::register_class(m)?; + Ok(()) +} diff --git a/src/value.rs b/src/value.rs new file mode 100644 index 0000000..9ac7ec4 --- /dev/null +++ b/src/value.rs @@ -0,0 +1,182 @@ +use std::fmt::Display; +use std::sync::Arc; + +use fxhash::FxHashMap; +use loro::{Counter, PeerID}; +use pyo3::prelude::*; + +use crate::container::Container; + +pub fn register_class(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} + +#[pyclass(eq, str, get_all, set_all)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct ID { + pub peer: u64, + pub counter: i32, +} + +impl Display for ID { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +#[pymethods] +impl ID { + #[new] + pub fn new(peer: u64, counter: i32) -> Self { + Self { peer, counter } + } +} + +#[pyclass(eq, hash, str)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ContainerType { + Text {}, + Map {}, + List {}, + MovableList {}, + Tree {}, + Counter {}, + Unknown { kind: u8 }, +} + +impl Display for ContainerType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +#[pyclass(eq, str, hash)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum ContainerID { + Root { + name: String, + container_type: ContainerType, + }, + Normal { + peer: u64, + counter: i32, + container_type: ContainerType, + }, +} + +impl Display for ContainerID { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +#[pyclass(eq, str)] +#[derive(Debug, Clone, PartialEq)] +pub enum LoroValue { + Null {}, + Bool(bool), + Double(f64), + I64(i64), + Binary(LoroBinaryValue), + String(LoroStringValue), + List(LoroListValue), + Map(LoroMapValue), + Container(ContainerID), +} + +impl Display for LoroValue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +#[pyclass] +#[derive(Default, Debug, PartialEq, Clone)] +pub struct LoroBinaryValue(pub Arc>); +#[pyclass] +#[derive(Default, Debug, PartialEq, Clone)] +pub struct LoroStringValue(pub Arc); +#[pyclass] +#[derive(Default, Debug, PartialEq, Clone)] +pub struct LoroListValue(pub Arc>); + +#[pyclass] +#[derive(Default, Debug, PartialEq, Clone)] +pub struct LoroMapValue(pub Arc>); + +#[pyclass(eq, str, eq_int)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Ordering { + Less, + Equal, + Greater, +} + +impl Display for Ordering { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +impl From for Ordering { + fn from(value: std::cmp::Ordering) -> Self { + match value { + std::cmp::Ordering::Less => Ordering::Less, + std::cmp::Ordering::Equal => Ordering::Equal, + std::cmp::Ordering::Greater => Ordering::Greater, + } + } +} + +#[pyclass(eq, str, get_all, set_all)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct TreeID { + pub peer: PeerID, + pub counter: Counter, +} + +impl Display for TreeID { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +#[pyclass(eq, str)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum TreeParentId { + Node { id: TreeID }, + Root {}, + Deleted {}, + Unexist {}, +} + +impl Display for TreeParentId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +#[pyclass] +#[derive(Debug, Clone)] +pub enum ValueOrContainer { + Value(LoroValue), + Container(Container), +} + +impl Display for ValueOrContainer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} diff --git a/src/version.rs b/src/version.rs new file mode 100644 index 0000000..cfe4a5f --- /dev/null +++ b/src/version.rs @@ -0,0 +1,118 @@ +use crate::{err::PyLoroResult, value::ID}; +use pyo3::{prelude::*, types::PyType}; +use std::{fmt::Display, sync::RwLock}; + +pub fn register_class(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} + +#[pyclass(str)] +#[derive(Clone, Default)] +pub struct Frontiers(loro::Frontiers); + +impl Display for Frontiers { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self.0) + } +} + +#[pymethods] +impl Frontiers { + #[new] + pub fn new() -> Self { + Self::default() + } + + #[classmethod] + pub fn from_id(_cls: &Bound<'_, PyType>, id: ID) -> Self { + Self(loro::Frontiers::from(loro::ID::from(id))) + } + + #[classmethod] + pub fn from_ids(_cls: &Bound<'_, PyType>, ids: Vec) -> Self { + Self(loro::Frontiers::from( + ids.into_iter().map(loro::ID::from).collect::>(), + )) + } + + pub fn encode(&self) -> Vec { + self.0.encode() + } + + #[classmethod] + pub fn decode(_cls: &Bound<'_, PyType>, bytes: &[u8]) -> PyLoroResult { + let ans = Self(loro::Frontiers::decode(bytes)?); + Ok(ans) + } +} + +impl From for loro::Frontiers { + fn from(value: Frontiers) -> Self { + value.0 + } +} + +impl From for Frontiers { + fn from(value: loro::Frontiers) -> Self { + Self(value) + } +} + +impl From<&Frontiers> for loro::Frontiers { + fn from(value: &Frontiers) -> Self { + value.0.clone() + } +} + +#[pyclass(str)] +#[derive(Clone, Debug, PartialEq, Eq, Default)] +pub struct VersionRange(loro::VersionRange); + +impl Display for VersionRange { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self.0) + } +} + +impl From for loro::VersionRange { + fn from(value: VersionRange) -> Self { + value.0 + } +} + +impl From for VersionRange { + fn from(value: loro::VersionRange) -> Self { + Self(value) + } +} + +#[pyclass(frozen, str)] +#[derive(Debug)] +pub struct VersionVector(RwLock); + +impl Default for VersionVector { + fn default() -> Self { + Self(RwLock::new(loro::VersionVector::new())) + } +} + +impl Display for VersionVector { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self.0.read().unwrap()) + } +} + +impl From<&VersionVector> for loro::VersionVector { + fn from(value: &VersionVector) -> Self { + value.0.read().unwrap().clone() + } +} + +impl From for VersionVector { + fn from(value: loro::VersionVector) -> Self { + Self(RwLock::new(value)) + } +}