Compare commits

..

1 commit

Author SHA1 Message Date
Orion Henry
684cd7a46c insert query caching 2022-06-11 15:43:33 +02:00
500 changed files with 23817 additions and 63002 deletions

View file

@ -14,7 +14,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.67.0
toolchain: 1.60.0
default: true
components: rustfmt
- uses: Swatinem/rust-cache@v1
@ -28,7 +28,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.67.0
toolchain: 1.60.0
default: true
components: clippy
- uses: Swatinem/rust-cache@v1
@ -42,14 +42,10 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.67.0
toolchain: 1.60.0
default: true
- uses: Swatinem/rust-cache@v1
- name: Build rust docs
run: ./scripts/ci/rust-docs
shell: bash
- name: Install doxygen
run: sudo apt-get install -y doxygen
- run: ./scripts/ci/docs
shell: bash
cargo-deny:
@ -64,50 +60,23 @@ jobs:
- uses: actions/checkout@v2
- uses: EmbarkStudios/cargo-deny-action@v1
with:
arguments: '--manifest-path ./rust/Cargo.toml'
command: check ${{ matrix.checks }}
wasm_tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install wasm-bindgen-cli
run: cargo install wasm-bindgen-cli wasm-opt
- name: Install wasm32 target
run: rustup target add wasm32-unknown-unknown
- name: Install wasm-pack
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
- name: run tests
run: ./scripts/ci/wasm_tests
deno_tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: denoland/setup-deno@v1
with:
deno-version: v1.x
- name: Install wasm-bindgen-cli
run: cargo install wasm-bindgen-cli wasm-opt
- name: Install wasm32 target
run: rustup target add wasm32-unknown-unknown
- name: run tests
run: ./scripts/ci/deno_tests
js_fmt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: install
run: yarn global add prettier
- name: format
run: prettier -c javascript/.prettierrc javascript
js_tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install wasm-bindgen-cli
run: cargo install wasm-bindgen-cli wasm-opt
- name: Install wasm32 target
run: rustup target add wasm32-unknown-unknown
- name: Install wasm-pack
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
- name: run tests
run: ./scripts/ci/js_tests
@ -118,7 +87,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: nightly-2023-01-26
toolchain: 1.60.0
default: true
- uses: Swatinem/rust-cache@v1
- name: Install CMocka
@ -127,8 +96,6 @@ jobs:
uses: jwlawson/actions-setup-cmake@v1.12
with:
cmake-version: latest
- name: Install rust-src
run: rustup component add rust-src
- name: Build and test C bindings
run: ./scripts/ci/cmake-build Release Static
shell: bash
@ -138,7 +105,9 @@ jobs:
strategy:
matrix:
toolchain:
- 1.67.0
- 1.60.0
- nightly
continue-on-error: ${{ matrix.toolchain == 'nightly' }}
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
@ -157,7 +126,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.67.0
toolchain: 1.60.0
default: true
- uses: Swatinem/rust-cache@v1
- run: ./scripts/ci/build-test
@ -170,8 +139,9 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.67.0
toolchain: 1.60.0
default: true
- uses: Swatinem/rust-cache@v1
- run: ./scripts/ci/build-test
shell: bash

View file

@ -30,16 +30,32 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: clean
args: --manifest-path ./rust/Cargo.toml --doc
args: --doc
- name: Build Rust docs
uses: actions-rs/cargo@v1
with:
command: doc
args: --manifest-path ./rust/Cargo.toml --workspace --all-features --no-deps
args: --workspace --all-features --no-deps
- name: Move Rust docs
run: mkdir -p docs && mv rust/target/doc/* docs/.
run: mkdir -p docs && mv target/doc/* docs/.
shell: bash
- name: Install doxygen
run: sudo apt-get install -y doxygen
shell: bash
- name: Install cmocka
run: sudo apt-get install -y libcmocka-dev
shell: bash
- name: Build C docs
run: ./scripts/ci/cmake-docs
shell: bash
- name: Move C docs
run: mkdir -p docs/automerge-c && mv automerge-c/build/src/html/* docs/automerge-c/.
shell: bash
- name: Configure root page

View file

@ -1,214 +0,0 @@
name: Release
on:
push:
branches:
- main
jobs:
check_if_wasm_version_upgraded:
name: Check if WASM version has been upgraded
runs-on: ubuntu-latest
outputs:
wasm_version: ${{ steps.version-updated.outputs.current-package-version }}
wasm_has_updated: ${{ steps.version-updated.outputs.has-updated }}
steps:
- uses: JiPaix/package-json-updated-action@v1.0.5
id: version-updated
with:
path: rust/automerge-wasm/package.json
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
publish-wasm:
name: Publish WASM package
runs-on: ubuntu-latest
needs:
- check_if_wasm_version_upgraded
# We create release only if the version in the package.json has been upgraded
if: needs.check_if_wasm_version_upgraded.outputs.wasm_has_updated == 'true'
steps:
- uses: actions/setup-node@v3
with:
node-version: '16.x'
registry-url: 'https://registry.npmjs.org'
- uses: denoland/setup-deno@v1
- uses: actions/checkout@v3
with:
fetch-depth: 0
ref: ${{ github.ref }}
- name: Get rid of local github workflows
run: rm -r .github/workflows
- name: Remove tmp_branch if it exists
run: git push origin :tmp_branch || true
- run: git checkout -b tmp_branch
- name: Install wasm-bindgen-cli
run: cargo install wasm-bindgen-cli wasm-opt
- name: Install wasm32 target
run: rustup target add wasm32-unknown-unknown
- name: run wasm js tests
id: wasm_js_tests
run: ./scripts/ci/wasm_tests
- name: run wasm deno tests
id: wasm_deno_tests
run: ./scripts/ci/deno_tests
- name: build release
id: build_release
run: |
npm --prefix $GITHUB_WORKSPACE/rust/automerge-wasm run release
- name: Collate deno release files
if: steps.wasm_js_tests.outcome == 'success' && steps.wasm_deno_tests.outcome == 'success'
run: |
mkdir $GITHUB_WORKSPACE/deno_wasm_dist
cp $GITHUB_WORKSPACE/rust/automerge-wasm/deno/* $GITHUB_WORKSPACE/deno_wasm_dist
cp $GITHUB_WORKSPACE/rust/automerge-wasm/index.d.ts $GITHUB_WORKSPACE/deno_wasm_dist
cp $GITHUB_WORKSPACE/rust/automerge-wasm/README.md $GITHUB_WORKSPACE/deno_wasm_dist
cp $GITHUB_WORKSPACE/rust/automerge-wasm/LICENSE $GITHUB_WORKSPACE/deno_wasm_dist
sed -i '1i /// <reference types="./index.d.ts" />' $GITHUB_WORKSPACE/deno_wasm_dist/automerge_wasm.js
- name: Create npm release
if: steps.wasm_js_tests.outcome == 'success' && steps.wasm_deno_tests.outcome == 'success'
run: |
if [ "$(npm --prefix $GITHUB_WORKSPACE/rust/automerge-wasm show . version)" = "$VERSION" ]; then
echo "This version is already published"
exit 0
fi
EXTRA_ARGS="--access public"
if [[ $VERSION == *"alpha."* ]] || [[ $VERSION == *"beta."* ]] || [[ $VERSION == *"rc."* ]]; then
echo "Is pre-release version"
EXTRA_ARGS="$EXTRA_ARGS --tag next"
fi
if [ "$NODE_AUTH_TOKEN" = "" ]; then
echo "Can't publish on NPM, You need a NPM_TOKEN secret."
false
fi
npm publish $GITHUB_WORKSPACE/rust/automerge-wasm $EXTRA_ARGS
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
VERSION: ${{ needs.check_if_wasm_version_upgraded.outputs.wasm_version }}
- name: Commit wasm deno release files
run: |
git config --global user.name "actions"
git config --global user.email actions@github.com
git add $GITHUB_WORKSPACE/deno_wasm_dist
git commit -am "Add deno release files"
git push origin tmp_branch
- name: Tag wasm release
if: steps.wasm_js_tests.outcome == 'success' && steps.wasm_deno_tests.outcome == 'success'
uses: softprops/action-gh-release@v1
with:
name: Automerge Wasm v${{ needs.check_if_wasm_version_upgraded.outputs.wasm_version }}
tag_name: js/automerge-wasm-${{ needs.check_if_wasm_version_upgraded.outputs.wasm_version }}
target_commitish: tmp_branch
generate_release_notes: false
draft: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Remove tmp_branch
run: git push origin :tmp_branch
check_if_js_version_upgraded:
name: Check if JS version has been upgraded
runs-on: ubuntu-latest
outputs:
js_version: ${{ steps.version-updated.outputs.current-package-version }}
js_has_updated: ${{ steps.version-updated.outputs.has-updated }}
steps:
- uses: JiPaix/package-json-updated-action@v1.0.5
id: version-updated
with:
path: javascript/package.json
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
publish-js:
name: Publish JS package
runs-on: ubuntu-latest
needs:
- check_if_js_version_upgraded
- check_if_wasm_version_upgraded
- publish-wasm
# We create release only if the version in the package.json has been upgraded and after the WASM release
if: |
(always() && ! cancelled()) &&
(needs.publish-wasm.result == 'success' || needs.publish-wasm.result == 'skipped') &&
needs.check_if_js_version_upgraded.outputs.js_has_updated == 'true'
steps:
- uses: actions/setup-node@v3
with:
node-version: '16.x'
registry-url: 'https://registry.npmjs.org'
- uses: denoland/setup-deno@v1
- uses: actions/checkout@v3
with:
fetch-depth: 0
ref: ${{ github.ref }}
- name: Get rid of local github workflows
run: rm -r .github/workflows
- name: Remove js_tmp_branch if it exists
run: git push origin :js_tmp_branch || true
- run: git checkout -b js_tmp_branch
- name: check js formatting
run: |
yarn global add prettier
prettier -c javascript/.prettierrc javascript
- name: run js tests
id: js_tests
run: |
cargo install wasm-bindgen-cli wasm-opt
rustup target add wasm32-unknown-unknown
./scripts/ci/js_tests
- name: build js release
id: build_release
run: |
npm --prefix $GITHUB_WORKSPACE/javascript run build
- name: build js deno release
id: build_deno_release
run: |
VERSION=$WASM_VERSION npm --prefix $GITHUB_WORKSPACE/javascript run deno:build
env:
WASM_VERSION: ${{ needs.check_if_wasm_version_upgraded.outputs.wasm_version }}
- name: run deno tests
id: deno_tests
run: |
npm --prefix $GITHUB_WORKSPACE/javascript run deno:test
- name: Collate deno release files
if: steps.js_tests.outcome == 'success' && steps.deno_tests.outcome == 'success'
run: |
mkdir $GITHUB_WORKSPACE/deno_js_dist
cp $GITHUB_WORKSPACE/javascript/deno_dist/* $GITHUB_WORKSPACE/deno_js_dist
- name: Create npm release
if: steps.js_tests.outcome == 'success' && steps.deno_tests.outcome == 'success'
run: |
if [ "$(npm --prefix $GITHUB_WORKSPACE/javascript show . version)" = "$VERSION" ]; then
echo "This version is already published"
exit 0
fi
EXTRA_ARGS="--access public"
if [[ $VERSION == *"alpha."* ]] || [[ $VERSION == *"beta."* ]] || [[ $VERSION == *"rc."* ]]; then
echo "Is pre-release version"
EXTRA_ARGS="$EXTRA_ARGS --tag next"
fi
if [ "$NODE_AUTH_TOKEN" = "" ]; then
echo "Can't publish on NPM, You need a NPM_TOKEN secret."
false
fi
npm publish $GITHUB_WORKSPACE/javascript $EXTRA_ARGS
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
VERSION: ${{ needs.check_if_js_version_upgraded.outputs.js_version }}
- name: Commit js deno release files
run: |
git config --global user.name "actions"
git config --global user.email actions@github.com
git add $GITHUB_WORKSPACE/deno_js_dist
git commit -am "Add deno js release files"
git push origin js_tmp_branch
- name: Tag JS release
if: steps.js_tests.outcome == 'success' && steps.deno_tests.outcome == 'success'
uses: softprops/action-gh-release@v1
with:
name: Automerge v${{ needs.check_if_js_version_upgraded.outputs.js_version }}
tag_name: js/automerge-${{ needs.check_if_js_version_upgraded.outputs.js_version }}
target_commitish: js_tmp_branch
generate_release_notes: false
draft: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Remove js_tmp_branch
run: git push origin :js_tmp_branch

3
.gitignore vendored
View file

@ -1,6 +1,5 @@
/target
/.direnv
perf.*
/Cargo.lock
build/
.vim/*
/target

View file

@ -3,15 +3,14 @@ members = [
"automerge",
"automerge-c",
"automerge-cli",
"automerge-test",
"automerge-wasm",
"edit-trace",
]
resolver = "2"
[profile.release]
debug = true
lto = true
codegen-units = 1
opt-level = 3
[profile.bench]
debug = true

13
Makefile Normal file
View file

@ -0,0 +1,13 @@
rust:
cd automerge && cargo test
wasm:
cd automerge-wasm && yarn
cd automerge-wasm && yarn build
cd automerge-wasm && yarn test
cd automerge-wasm && yarn link
js: wasm
cd automerge-js && yarn
cd automerge-js && yarn link "automerge-wasm"
cd automerge-js && yarn test

199
README.md
View file

@ -1,147 +1,110 @@
# Automerge
# Automerge RS
<img src='./img/sign.svg' width='500' alt='Automerge logo' />
[![homepage](https://img.shields.io/badge/homepage-published-informational)](https://automerge.org/)
[![main docs](https://img.shields.io/badge/docs-main-informational)](https://automerge.org/automerge-rs/automerge/)
[![ci](https://github.com/automerge/automerge-rs/actions/workflows/ci.yaml/badge.svg)](https://github.com/automerge/automerge-rs/actions/workflows/ci.yaml)
[![docs](https://github.com/automerge/automerge-rs/actions/workflows/docs.yaml/badge.svg)](https://github.com/automerge/automerge-rs/actions/workflows/docs.yaml)
Automerge is a library which provides fast implementations of several different
CRDTs, a compact compression format for these CRDTs, and a sync protocol for
efficiently transmitting those changes over the network. The objective of the
project is to support [local-first](https://www.inkandswitch.com/local-first/) applications in the same way that relational
databases support server applications - by providing mechanisms for persistence
which allow application developers to avoid thinking about hard distributed
computing problems. Automerge aims to be PostgreSQL for your local-first app.
This is a rust implementation of the [Automerge](https://github.com/automerge/automerge) file format and network protocol.
If you're looking for documentation on the JavaScript implementation take a look
at https://automerge.org/docs/hello/. There are other implementations in both
Rust and C, but they are earlier and don't have documentation yet. You can find
them in `rust/automerge` and `rust/automerge-c` if you are comfortable
reading the code and tests to figure out how to use them.
If you're familiar with CRDTs and interested in the design of Automerge in
particular take a look at https://automerge.org/docs/how-it-works/backend/
Finally, if you want to talk to us about this project please [join the
Slack](https://join.slack.com/t/automerge/shared_invite/zt-e4p3760n-kKh7r3KRH1YwwNfiZM8ktw)
If you are looking for the origional `automerge-rs` project that can be used as a wasm backend to the javascript implementation, it can be found [here](https://github.com/automerge/automerge-rs/tree/automerge-1.0).
## Status
This project is formed of a core Rust implementation which is exposed via FFI in
javascript+WASM, C, and soon other languages. Alex
([@alexjg](https://github.com/alexjg/)]) is working full time on maintaining
automerge, other members of Ink and Switch are also contributing time and there
are several other maintainers. The focus is currently on shipping the new JS
package. We expect to be iterating the API and adding new features over the next
six months so there will likely be several major version bumps in all packages
in that time.
This project has 4 components:
In general we try and respect semver.
1. _automerge_ - a rust implementation of the library. This project is the most mature and being used in a handful of small applications.
2. _automerge-wasm_ - a js/wasm interface to the underlying rust library. This api is generally mature and in use in a handful of projects as well.
3. _automerge-js_ - this is a javascript library using the wasm interface to export the same public api of the primary automerge project. Currently this project passes all of automerge's tests but has not been used in any real project or packaged as an NPM. Alpha testers welcome.
4. _automerge-c_ - this is a c library intended to be an ffi integration point for all other languages. It is currently a work in progress and not yet ready for any testing.
### JavaScript
## How?
A stable release of the javascript package is currently available as
`@automerge/automerge@2.0.0` where. pre-release verisions of the `2.0.1` are
available as `2.0.1-alpha.n`. `2.0.1*` packages are also available for Deno at
https://deno.land/x/automerge
The current iteration of automerge-rs is complicated to work with because it
adopts the frontend/backend split architecture of the JS implementation. This
architecture was necessary due to basic operations on the automerge opset being
too slow to perform on the UI thread. Recently @orionz has been able to improve
the performance to the point where the split is no longer necessary. This means
we can adopt a much simpler mutable API.
### Rust
The architecture is now built around the `OpTree`. This is a data structure
which supports efficiently inserting new operations and realising values of
existing operations. Most interactions with the `OpTree` are in the form of
implementations of `TreeQuery` - a trait which can be used to traverse the
optree and producing state of some kind. User facing operations are exposed on
an `Automerge` object, under the covers these operations typically instantiate
some `TreeQuery` and run it over the `OpTree`.
The rust codebase is currently oriented around producing a performant backend
for the Javascript wrapper and as such the API for Rust code is low level and
not well documented. We will be returning to this over the next few months but
for now you will need to be comfortable reading the tests and asking questions
to figure out how to use it. If you are looking to build rust applications which
use automerge you may want to look into
[autosurgeon](https://github.com/alexjg/autosurgeon)
## Development
## Repository Organisation
Please feel free to open issues and pull requests.
- `./rust` - the rust rust implementation and also the Rust components of
platform specific wrappers (e.g. `automerge-wasm` for the WASM API or
`automerge-c` for the C FFI bindings)
- `./javascript` - The javascript library which uses `automerge-wasm`
internally but presents a more idiomatic javascript interface
- `./scripts` - scripts which are useful to maintenance of the repository.
This includes the scripts which are run in CI.
- `./img` - static assets for use in `.md` files
### Running CI
## Building
The steps CI will run are all defined in `./scripts/ci`. Obviously CI will run
everything when you submit a PR, but if you want to run everything locally
before you push you can run `./scripts/ci/run` to run everything.
To build this codebase you will need:
### Running the JS tests
- `rust`
- `node`
- `yarn`
- `cmake`
- `cmocka`
You will need to have [node](https://nodejs.org/en/), [yarn](https://yarnpkg.com/getting-started/install), [rust](https://rustup.rs/) and [wasm-pack](https://rustwasm.github.io/wasm-pack/installer/) installed.
You will also need to install the following with `cargo install`
To build and test the rust library:
- `wasm-bindgen-cli`
- `wasm-opt`
- `cargo-deny`
And ensure you have added the `wasm32-unknown-unknown` target for rust cross-compilation.
The various subprojects (the rust code, the wrapper projects) have their own
build instructions, but to run the tests that will be run in CI you can run
`./scripts/ci/run`.
### For macOS
These instructions worked to build locally on macOS 13.1 (arm64) as of
Nov 29th 2022.
```bash
# clone the repo
git clone https://github.com/automerge/automerge-rs
cd automerge-rs
# install rustup
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# install homebrew
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# install cmake, node, cmocka
brew install cmake node cmocka
# install yarn
npm install --global yarn
# install javascript dependencies
yarn --cwd ./javascript
# install rust dependencies
cargo install wasm-bindgen-cli wasm-opt cargo-deny
# get nightly rust to produce optimized automerge-c builds
rustup toolchain install nightly
rustup component add rust-src --toolchain nightly
# add wasm target in addition to current architecture
rustup target add wasm32-unknown-unknown
# Run ci script
./scripts/ci/run
```shell
$ cd automerge
$ cargo test
```
If your build fails to find `cmocka.h` you may need to teach it about homebrew's
installation location:
To build and test the wasm library:
```
export CPATH=/opt/homebrew/include
export LIBRARY_PATH=/opt/homebrew/lib
./scripts/ci/run
```shell
## setup
$ cd automerge-wasm
$ yarn
## building or testing
$ yarn build
$ yarn test
## without this the js library wont automatically use changes
$ yarn link
## cutting a release or doing benchmarking
$ yarn release
```
## Contributing
To test the js library. This is where most of the tests reside.
Please try and split your changes up into relatively independent commits which
change one subsystem at a time and add good commit messages which describe what
the change is and why you're making it (err on the side of longer commit
messages). `git blame` should give future maintainers a good idea of why
something is the way it is.
```shell
## setup
$ cd automerge-js
$ yarn
$ yarn link "automerge-wasm"
## testing
$ yarn test
```
And finally, to build and test the C bindings with CMake:
```shell
## setup
$ cd automerge-c
$ mkdir -p build
$ cd build
$ cmake -S .. -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF
## building and testing
$ cmake --build .
```
To add debugging symbols, replace `Release` with `Debug`.
To build a shared library instead of a static one, replace `OFF` with `ON`.
The C bindings can be built and tested on any platform for which CMake is
available but the steps for doing so vary across platforms and are too numerous
to list here.
## Benchmarking
The `edit-trace` folder has the main code for running the edit trace benchmarking.

32
TODO.md Normal file
View file

@ -0,0 +1,32 @@
### next steps:
1. C API
2. port rust command line tool
3. fast load
### ergonomics:
1. value() -> () or something that into's a value
### automerge:
1. single pass (fast) load
2. micro-patches / bare bones observation API / fully hydrated documents
### future:
1. handle columns with unknown data in and out
2. branches with different indexes
### Peritext
1. add mark / remove mark -- type, start/end elemid (inclusive,exclusive)
2. track any formatting ops that start or end on a character
3. ops right before the character, ops right after that character
4. query a single character - character, plus marks that start or end on that character
what is its current formatting,
what are the ops that include that in their span,
None = same as last time, Set( bold, italic ),
keep these on index
5. op probably belongs with the start character - possible packed at the beginning or end of the list
### maybe:
1. tables
### no:
1. cursors

3
automerge-c/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
automerge
automerge.h
automerge.o

137
automerge-c/CMakeLists.txt Normal file
View file

@ -0,0 +1,137 @@
cmake_minimum_required(VERSION 3.18 FATAL_ERROR)
set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
# Parse the library name, project name and project version out of Cargo's TOML file.
set(CARGO_LIB_SECTION OFF)
set(LIBRARY_NAME "")
set(CARGO_PKG_SECTION OFF)
set(CARGO_PKG_NAME "")
set(CARGO_PKG_VERSION "")
file(READ Cargo.toml TOML_STRING)
string(REPLACE ";" "\\\\;" TOML_STRING "${TOML_STRING}")
string(REPLACE "\n" ";" TOML_LINES "${TOML_STRING}")
foreach(TOML_LINE IN ITEMS ${TOML_LINES})
string(REGEX MATCH "^\\[(lib|package)\\]$" _ ${TOML_LINE})
if(CMAKE_MATCH_1 STREQUAL "lib")
set(CARGO_LIB_SECTION ON)
set(CARGO_PKG_SECTION OFF)
elseif(CMAKE_MATCH_1 STREQUAL "package")
set(CARGO_LIB_SECTION OFF)
set(CARGO_PKG_SECTION ON)
endif()
string(REGEX MATCH "^name += +\"([^\"]+)\"$" _ ${TOML_LINE})
if(CMAKE_MATCH_1 AND (CARGO_LIB_SECTION AND NOT CARGO_PKG_SECTION))
set(LIBRARY_NAME "${CMAKE_MATCH_1}")
elseif(CMAKE_MATCH_1 AND (NOT CARGO_LIB_SECTION AND CARGO_PKG_SECTION))
set(CARGO_PKG_NAME "${CMAKE_MATCH_1}")
endif()
string(REGEX MATCH "^version += +\"([^\"]+)\"$" _ ${TOML_LINE})
if(CMAKE_MATCH_1 AND CARGO_PKG_SECTION)
set(CARGO_PKG_VERSION "${CMAKE_MATCH_1}")
endif()
if(LIBRARY_NAME AND (CARGO_PKG_NAME AND CARGO_PKG_VERSION))
break()
endif()
endforeach()
project(${CARGO_PKG_NAME} VERSION ${CARGO_PKG_VERSION} LANGUAGES C DESCRIPTION "C bindings for the Automerge Rust backend.")
include(CTest)
option(BUILD_SHARED_LIBS "Enable the choice of a shared or static library.")
include(CMakePackageConfigHelpers)
include(GNUInstallDirs)
string(MAKE_C_IDENTIFIER ${PROJECT_NAME} SYMBOL_PREFIX)
string(TOUPPER ${SYMBOL_PREFIX} SYMBOL_PREFIX)
set(CARGO_TARGET_DIR "${CMAKE_CURRENT_BINARY_DIR}/Cargo/target")
add_subdirectory(src)
# Generate and install the configuration header.
math(EXPR INTEGER_PROJECT_VERSION_MAJOR "${PROJECT_VERSION_MAJOR} * 100000")
math(EXPR INTEGER_PROJECT_VERSION_MINOR "${PROJECT_VERSION_MINOR} * 100")
math(EXPR INTEGER_PROJECT_VERSION_PATCH "${PROJECT_VERSION_PATCH}")
math(EXPR INTEGER_PROJECT_VERSION "${INTEGER_PROJECT_VERSION_MAJOR} + ${INTEGER_PROJECT_VERSION_MINOR} + ${INTEGER_PROJECT_VERSION_PATCH}")
configure_file(
${CMAKE_MODULE_PATH}/config.h.in
config.h
@ONLY
NEWLINE_STYLE LF
)
install(
FILES ${CMAKE_BINARY_DIR}/config.h
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}
)
if(BUILD_TESTING)
add_subdirectory(test)
enable_testing()
endif()
add_subdirectory(examples EXCLUDE_FROM_ALL)
# Generate and install .cmake files
set(PROJECT_CONFIG_NAME "${PROJECT_NAME}-config")
set(PROJECT_CONFIG_VERSION_NAME "${PROJECT_CONFIG_NAME}-version")
write_basic_package_version_file(
${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_CONFIG_VERSION_NAME}.cmake
VERSION ${PROJECT_VERSION}
COMPATIBILITY ExactVersion
)
# The namespace label starts with the title-cased library name.
string(SUBSTRING ${LIBRARY_NAME} 0 1 NS_FIRST)
string(SUBSTRING ${LIBRARY_NAME} 1 -1 NS_REST)
string(TOUPPER ${NS_FIRST} NS_FIRST)
string(TOLOWER ${NS_REST} NS_REST)
string(CONCAT NAMESPACE ${NS_FIRST} ${NS_REST} "::")
# \note CMake doesn't automate the exporting of an imported library's targets
# so the package configuration script must do it.
configure_package_config_file(
${CMAKE_MODULE_PATH}/${PROJECT_CONFIG_NAME}.cmake.in
${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_CONFIG_NAME}.cmake
INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}
)
install(
FILES
${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_CONFIG_NAME}.cmake
${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_CONFIG_VERSION_NAME}.cmake
DESTINATION
${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}
)

View file

@ -7,8 +7,8 @@ license = "MIT"
rust-version = "1.57.0"
[lib]
name = "automerge_core"
crate-type = ["staticlib"]
name = "automerge"
crate-type = ["cdylib", "staticlib"]
bench = false
doc = false
@ -19,4 +19,4 @@ libc = "^0.2"
smol_str = "^0.1.21"
[build-dependencies]
cbindgen = "^0.24"
cbindgen = "^0.20"

30
automerge-c/Makefile Normal file
View file

@ -0,0 +1,30 @@
CC=gcc
CFLAGS=-I.
DEPS=automerge.h
LIBS=-lpthread -ldl -lm
LDIR=../target/release
LIB=../target/release/libautomerge.a
DEBUG_LIB=../target/debug/libautomerge.a
all: $(DEBUG_LIB) automerge
debug: LDIR=../target/debug
debug: automerge $(DEBUG_LIB)
automerge: automerge.o $(LDIR)/libautomerge.a
$(CC) -o $@ automerge.o $(LDIR)/libautomerge.a $(LIBS) -L$(LDIR)
$(DEBUG_LIB): src/*.rs
cargo build
$(LIB): src/*.rs
cargo build --release
%.o: %.c $(DEPS)
$(CC) -c -o $@ $< $(CFLAGS)
.PHONY: clean
clean:
rm -f *.o automerge $(LIB) $(DEBUG_LIB)

95
automerge-c/README.md Normal file
View file

@ -0,0 +1,95 @@
## Methods we need to support
### Basic management
1. `AMcreate()`
1. `AMclone(doc)`
1. `AMfree(doc)`
1. `AMconfig(doc, key, val)` // set actor
1. `actor = get_actor(doc)`
### Transactions
1. `AMpendingOps(doc)`
1. `AMcommit(doc, message, time)`
1. `AMrollback(doc)`
### Write
1. `AMset{Map|List}(doc, obj, prop, value)`
1. `AMinsert(doc, obj, index, value)`
1. `AMpush(doc, obj, value)`
1. `AMdel{Map|List}(doc, obj, prop)`
1. `AMinc{Map|List}(doc, obj, prop, value)`
1. `AMspliceText(doc, obj, start, num_del, text)`
### Read
1. `AMkeys(doc, obj, heads)`
1. `AMlength(doc, obj, heads)`
1. `AMvalues(doc, obj, heads)`
1. `AMtext(doc, obj, heads)`
### Sync
1. `AMgenerateSyncMessage(doc, state)`
1. `AMreceiveSyncMessage(doc, state, message)`
1. `AMinitSyncState()`
### Save / Load
1. `AMload(data)`
1. `AMloadIncremental(doc, data)`
1. `AMsave(doc)`
1. `AMsaveIncremental(doc)`
### Low Level Access
1. `AMapplyChanges(doc, changes)`
1. `AMgetChanges(doc, deps)`
1. `AMgetChangesAdded(doc1, doc2)`
1. `AMgetHeads(doc)`
1. `AMgetLastLocalChange(doc)`
1. `AMgetMissingDeps(doc, heads)`
### Encode/Decode
1. `AMencodeChange(change)`
1. `AMdecodeChange(change)`
1. `AMencodeSyncMessage(change)`
1. `AMdecodeSyncMessage(change)`
1. `AMencodeSyncState(change)`
1. `AMdecodeSyncState(change)`
## Open Question - Memory management
Most of these calls return one or more items of arbitrary length. Doing memory management in C is tricky. This is my proposed solution...
###
```
// returns 1 or zero opids
n = automerge_set(doc, "_root", "hello", datatype, value);
if (n) {
automerge_pop(doc, &obj, len);
}
// returns n values
n = automerge_values(doc, "_root", "hello");
for (i = 0; i<n ;i ++) {
automerge_pop_value(doc, &value, &datatype, len);
}
```
There would be one pop method per object type. Users allocs and frees the buffers. Multiple return values would result in multiple pops. Too small buffers would error and allow retry.
### Formats
Actors - We could do (bytes,len) or a hex encoded string?.
ObjIds - We could do flat bytes of the ExId struct but lets do human readable strings for now - the struct would be faster but opque
Heads - Might as well make it a flat buffer `(n, hash, hash, ...)`
Changes - Put them all in a flat concatenated buffer
Encode/Decode - to json strings?

36
automerge-c/automerge.c Normal file
View file

@ -0,0 +1,36 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include "automerge.h"
#define MAX_BUFF_SIZE 4096
int main() {
int n = 0;
int data_type = 0;
char buff[MAX_BUFF_SIZE];
char obj[MAX_BUFF_SIZE];
AMresult* res = NULL;
printf("begin\n");
AMdoc* doc = AMcreate();
printf("AMconfig()...");
AMconfig(doc, "actor", "aabbcc");
printf("pass!\n");
printf("AMmapSetStr()...\n");
res = AMmapSetStr(doc, NULL, "string", "hello world");
if (AMresultStatus(res) != AM_STATUS_COMMAND_OK)
{
printf("AMmapSet() failed: %s\n", AMerrorMessage(res));
return 1;
}
AMclear(res);
printf("pass!\n");
AMdestroy(doc);
printf("end\n");
}

25
automerge-c/build.rs Normal file
View file

@ -0,0 +1,25 @@
extern crate cbindgen;
use std::{env, path::PathBuf};
fn main() {
let crate_dir = PathBuf::from(
env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR env var is not defined"),
);
let config = cbindgen::Config::from_file("cbindgen.toml")
.expect("Unable to find cbindgen.toml configuration file");
// let mut config: cbindgen::Config = Default::default();
// config.language = cbindgen::Language::C;
if let Ok(writer) = cbindgen::generate_with_config(&crate_dir, config) {
writer.write_to_file(crate_dir.join("automerge.h"));
// Also write the generated header into the target directory when
// specified (necessary for an out-of-source build a la CMake).
if let Ok(target_dir) = env::var("CARGO_TARGET_DIR") {
writer.write_to_file(PathBuf::from(target_dir).join("automerge.h"));
}
}
}

View file

@ -1,7 +1,7 @@
after_includes = """\n
/**
* \\defgroup enumerations Public Enumerations
* Symbolic names for integer constants.
Symbolic names for integer constants.
*/
/**
@ -10,25 +10,16 @@ after_includes = """\n
* \\brief The root object of a document.
*/
#define AM_ROOT NULL
/**
* \\memberof AMdoc
* \\def AM_CHANGE_HASH_SIZE
* \\brief The count of bytes in a change hash.
*/
#define AM_CHANGE_HASH_SIZE 32
"""
autogen_warning = """
/**
* \\file
* \\brief All constants, functions and types in the core Automerge C API.
*
* \\warning This file is auto-generated by cbindgen.
*/
"""
autogen_warning = "/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */"
documentation = true
documentation_style = "doxy"
include_guard = "AUTOMERGE_C_H"
header = """
/** \\file
* All constants, functions and types in the Automerge library's C API.
*/
"""
include_guard = "automerge_h"
includes = []
language = "C"
line_length = 140
@ -45,4 +36,4 @@ prefix_with_name = true
rename_variants = "ScreamingSnakeCase"
[export]
item_types = ["constants", "enums", "functions", "opaque", "structs", "typedefs"]
item_types = ["enums", "structs", "opaque", "constants", "functions"]

View file

@ -0,0 +1,14 @@
#ifndef @SYMBOL_PREFIX@_CONFIG_INCLUDED
#define @SYMBOL_PREFIX@_CONFIG_INCLUDED
/* This header is auto-generated by CMake. */
#define @SYMBOL_PREFIX@_VERSION @INTEGER_PROJECT_VERSION@
#define @SYMBOL_PREFIX@_MAJOR_VERSION (@SYMBOL_PREFIX@_VERSION / 100000)
#define @SYMBOL_PREFIX@_MINOR_VERSION ((@SYMBOL_PREFIX@_VERSION / 100) % 1000)
#define @SYMBOL_PREFIX@_PATCH_VERSION (@SYMBOL_PREFIX@_VERSION % 100)
#endif /* @SYMBOL_PREFIX@_CONFIG_INCLUDED */

View file

@ -1,6 +1,4 @@
# This CMake script is used to perform string substitutions within a generated
# file.
cmake_minimum_required(VERSION 3.23 FATAL_ERROR)
cmake_minimum_required(VERSION 3.18 FATAL_ERROR)
if(NOT DEFINED MATCH_REGEX)
message(FATAL_ERROR "Variable \"MATCH_REGEX\" is not defined.")

View file

@ -1,6 +1,4 @@
# This CMake script is used to force Cargo to regenerate the header file for the
# core bindings after the out-of-source build directory has been cleaned.
cmake_minimum_required(VERSION 3.23 FATAL_ERROR)
cmake_minimum_required(VERSION 3.18 FATAL_ERROR)
if(NOT DEFINED CONDITION)
message(FATAL_ERROR "Variable \"CONDITION\" is not defined.")

View file

@ -1,39 +1,41 @@
cmake_minimum_required(VERSION 3.18 FATAL_ERROR)
add_executable(
${LIBRARY_NAME}_quickstart
example_quickstart
quickstart.c
)
set_target_properties(${LIBRARY_NAME}_quickstart PROPERTIES LINKER_LANGUAGE C)
set_target_properties(example_quickstart PROPERTIES LINKER_LANGUAGE C)
# \note An imported library's INTERFACE_INCLUDE_DIRECTORIES property can't
# contain a non-existent path so its build-time include directory
# must be specified for all of its dependent targets instead.
target_include_directories(
${LIBRARY_NAME}_quickstart
PRIVATE "$<BUILD_INTERFACE:${CBINDGEN_INCLUDEDIR}>"
example_quickstart
PRIVATE "$<BUILD_INTERFACE:${CARGO_TARGET_DIR}>"
)
target_link_libraries(${LIBRARY_NAME}_quickstart PRIVATE ${LIBRARY_NAME})
target_link_libraries(example_quickstart PRIVATE ${LIBRARY_NAME})
add_dependencies(${LIBRARY_NAME}_quickstart ${BINDINGS_NAME}_artifacts)
add_dependencies(example_quickstart ${LIBRARY_NAME}_artifacts)
if(BUILD_SHARED_LIBS AND WIN32)
add_custom_command(
TARGET ${LIBRARY_NAME}_quickstart
TARGET example_quickstart
POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CARGO_CURRENT_BINARY_DIR}/${CMAKE_SHARED_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_${CMAKE_BUILD_TYPE}_POSTFIX}${CMAKE_SHARED_LIBRARY_SUFFIX}
${CMAKE_BINARY_DIR}
${CMAKE_CURRENT_BINARY_DIR}
COMMENT "Copying the DLL built by Cargo into the examples directory..."
VERBATIM
)
endif()
add_custom_command(
TARGET ${LIBRARY_NAME}_quickstart
TARGET example_quickstart
POST_BUILD
COMMAND
${LIBRARY_NAME}_quickstart
example_quickstart
COMMENT
"Running the example quickstart..."
VERBATIM

View file

@ -5,5 +5,5 @@
```shell
cmake -E make_directory automerge-c/build
cmake -S automerge-c -B automerge-c/build
cmake --build automerge-c/build --target automerge_quickstart
cmake --build automerge-c/build --target example_quickstart
```

View file

@ -0,0 +1,157 @@
#include <stdio.h>
#include <stdlib.h>
#include <automerge.h>
AMvalue test(AMresult*, AMvalueVariant const);
/*
* Based on https://automerge.github.io/docs/quickstart
*/
int main(int argc, char** argv) {
AMresult* const doc1_result = AMcreate();
AMdoc* const doc1 = AMresultValue(doc1_result).doc;
if (doc1 == NULL) {
fprintf(stderr, "`AMcreate()` failure.");
exit(EXIT_FAILURE);
}
AMresult* const cards_result = AMmapPutObject(doc1, AM_ROOT, "cards", AM_OBJ_TYPE_LIST);
AMvalue value = test(cards_result, AM_VALUE_OBJ_ID);
AMobjId const* const cards = value.obj_id;
AMresult* const card1_result = AMlistPutObject(doc1, cards, 0, true, AM_OBJ_TYPE_MAP);
value = test(card1_result, AM_VALUE_OBJ_ID);
AMobjId const* const card1 = value.obj_id;
AMresult* result = AMmapPutStr(doc1, card1, "title", "Rewrite everything in Clojure");
test(result, AM_VALUE_VOID);
AMfree(result);
result = AMmapPutBool(doc1, card1, "done", false);
test(result, AM_VALUE_VOID);
AMfree(result);
AMresult* const card2_result = AMlistPutObject(doc1, cards, 0, true, AM_OBJ_TYPE_MAP);
value = test(card2_result, AM_VALUE_OBJ_ID);
AMobjId const* const card2 = value.obj_id;
result = AMmapPutStr(doc1, card2, "title", "Rewrite everything in Haskell");
test(result, AM_VALUE_VOID);
AMfree(result);
result = AMmapPutBool(doc1, card2, "done", false);
test(result, AM_VALUE_VOID);
AMfree(result);
AMfree(card2_result);
result = AMcommit(doc1, "Add card", NULL);
test(result, AM_VALUE_CHANGE_HASHES);
AMfree(result);
AMresult* doc2_result = AMcreate();
AMdoc* doc2 = AMresultValue(doc2_result).doc;
if (doc2 == NULL) {
fprintf(stderr, "`AMcreate()` failure.");
AMfree(card1_result);
AMfree(cards_result);
AMfree(doc1_result);
exit(EXIT_FAILURE);
}
result = AMmerge(doc2, doc1);
test(result, AM_VALUE_CHANGE_HASHES);
AMfree(result);
AMfree(doc2_result);
AMresult* const save_result = AMsave(doc1);
value = test(save_result, AM_VALUE_BYTES);
AMbyteSpan binary = value.bytes;
doc2_result = AMload(binary.src, binary.count);
doc2 = AMresultValue(doc2_result).doc;
AMfree(save_result);
if (doc2 == NULL) {
fprintf(stderr, "`AMload()` failure.");
AMfree(card1_result);
AMfree(cards_result);
AMfree(doc1_result);
exit(EXIT_FAILURE);
}
result = AMmapPutBool(doc1, card1, "done", true);
test(result, AM_VALUE_VOID);
AMfree(result);
result = AMcommit(doc1, "Mark card as done", NULL);
test(result, AM_VALUE_CHANGE_HASHES);
AMfree(result);
AMfree(card1_result);
result = AMlistDelete(doc2, cards, 0);
test(result, AM_VALUE_VOID);
AMfree(result);
result = AMcommit(doc2, "Delete card", NULL);
test(result, AM_VALUE_CHANGE_HASHES);
AMfree(result);
result = AMmerge(doc1, doc2);
test(result, AM_VALUE_CHANGE_HASHES);
AMfree(result);
AMfree(doc2_result);
result = AMgetChanges(doc1, NULL);
value = test(result, AM_VALUE_CHANGES);
AMchange const* change = NULL;
while ((change = AMchangesNext(&value.changes, 1)) != NULL) {
size_t const size = AMobjSizeAt(doc1, cards, change);
printf("%s %ld\n", AMchangeMessage(change), size);
}
AMfree(result);
AMfree(cards_result);
AMfree(doc1_result);
}
/**
* \brief Extracts a value with the given discriminant from the given result
* or writes a message to `stderr`, frees the given result and
* terminates the program.
*
.* \param[in] result A pointer to an `AMresult` struct.
* \param[in] discriminant An `AMvalueVariant` enum tag.
* \return An `AMvalue` struct.
* \pre \p result must be a valid address.
*/
AMvalue test(AMresult* result, AMvalueVariant const discriminant) {
static char prelude[64];
if (result == NULL) {
fprintf(stderr, "NULL `AMresult` struct pointer.");
exit(EXIT_FAILURE);
}
AMstatus const status = AMresultStatus(result);
if (status != AM_STATUS_OK) {
switch (status) {
case AM_STATUS_ERROR: sprintf(prelude, "Error"); break;
case AM_STATUS_INVALID_RESULT: sprintf(prelude, "Invalid result"); break;
default: sprintf(prelude, "Unknown `AMstatus` tag %d", status);
}
fprintf(stderr, "%s; %s.", prelude, AMerrorMessage(result));
AMfree(result);
exit(EXIT_FAILURE);
}
AMvalue const value = AMresultValue(result);
if (value.tag != discriminant) {
char const* label = NULL;
switch (value.tag) {
case AM_VALUE_ACTOR_ID: label = "AM_VALUE_ACTOR_ID"; break;
case AM_VALUE_BOOLEAN: label = "AM_VALUE_BOOLEAN"; break;
case AM_VALUE_BYTES: label = "AM_VALUE_BYTES"; break;
case AM_VALUE_CHANGE_HASHES: label = "AM_VALUE_CHANGE_HASHES"; break;
case AM_VALUE_CHANGES: label = "AM_VALUE_CHANGES"; break;
case AM_VALUE_COUNTER: label = "AM_VALUE_COUNTER"; break;
case AM_VALUE_F64: label = "AM_VALUE_F64"; break;
case AM_VALUE_INT: label = "AM_VALUE_INT"; break;
case AM_VALUE_VOID: label = "AM_VALUE_VOID"; break;
case AM_VALUE_NULL: label = "AM_VALUE_NULL"; break;
case AM_VALUE_OBJ_ID: label = "AM_VALUE_OBJ_ID"; break;
case AM_VALUE_STR: label = "AM_VALUE_STR"; break;
case AM_VALUE_TIMESTAMP: label = "AM_VALUE_TIMESTAMP"; break;
case AM_VALUE_UINT: label = "AM_VALUE_UINT"; break;
default: label = "<unknown>";
}
fprintf(stderr, "Unexpected `AMvalueVariant` tag `%s` (%d).", label, value.tag);
AMfree(result);
exit(EXIT_FAILURE);
}
return value;
}

View file

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1,237 @@
cmake_minimum_required(VERSION 3.18 FATAL_ERROR)
find_program (
CARGO_CMD
"cargo"
PATHS "$ENV{CARGO_HOME}/bin"
DOC "The Cargo command"
)
if(NOT CARGO_CMD)
message(FATAL_ERROR "Cargo (Rust package manager) not found! Install it and/or set the CARGO_HOME environment variable.")
endif()
string(TOLOWER "${CMAKE_BUILD_TYPE}" BUILD_TYPE_LOWER)
if(BUILD_TYPE_LOWER STREQUAL debug)
set(CARGO_BUILD_TYPE "debug")
set(CARGO_FLAG "")
else()
set(CARGO_BUILD_TYPE "release")
set(CARGO_FLAG "--release")
endif()
set(CARGO_CURRENT_BINARY_DIR "${CARGO_TARGET_DIR}/${CARGO_BUILD_TYPE}")
set(
CARGO_OUTPUT
${CARGO_TARGET_DIR}/${LIBRARY_NAME}.h
${CARGO_CURRENT_BINARY_DIR}/${CMAKE_SHARED_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_SHARED_LIBRARY_SUFFIX}
${CARGO_CURRENT_BINARY_DIR}/${CMAKE_STATIC_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_STATIC_LIBRARY_SUFFIX}
)
if(WIN32)
# \note The basename of an import library output by Cargo is the filename
# of its corresponding shared library.
list(APPEND CARGO_OUTPUT ${CARGO_CURRENT_BINARY_DIR}/${CMAKE_SHARED_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_SHARED_LIBRARY_SUFFIX}${CMAKE_STATIC_LIBRARY_SUFFIX})
endif()
add_custom_command(
OUTPUT
${CARGO_OUTPUT}
COMMAND
# \note cbindgen won't regenerate its output header file after it's
# been removed but it will after its configuration file has been
# updated.
${CMAKE_COMMAND} -DCONDITION=NOT_EXISTS -P ${CMAKE_SOURCE_DIR}/cmake/file_touch.cmake -- ${CARGO_TARGET_DIR}/${LIBRARY_NAME}.h ${CMAKE_SOURCE_DIR}/cbindgen.toml
COMMAND
${CMAKE_COMMAND} -E env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} ${CARGO_CMD} build ${CARGO_FLAG}
MAIN_DEPENDENCY
lib.rs
DEPENDS
byte_span.rs
change_hashes.rs
change.rs
changes.rs
doc.rs
doc/list.rs
doc/map.rs
doc/utils.rs
obj.rs
result.rs
sync.rs
sync/have.rs
sync/haves.rs
sync/message.rs
sync/state.rs
${CMAKE_SOURCE_DIR}/build.rs
${CMAKE_SOURCE_DIR}/Cargo.toml
${CMAKE_SOURCE_DIR}/cbindgen.toml
WORKING_DIRECTORY
${CMAKE_SOURCE_DIR}
COMMENT
"Producing the library artifacts with Cargo..."
VERBATIM
)
add_custom_target(
${LIBRARY_NAME}_artifacts
DEPENDS ${CARGO_OUTPUT}
)
# \note cbindgen's naming behavior isn't fully configurable and it ignores
# `const fn` calls (https://github.com/eqrion/cbindgen/issues/252).
add_custom_command(
TARGET ${LIBRARY_NAME}_artifacts
POST_BUILD
COMMAND
# Compensate for cbindgen's variant struct naming.
${CMAKE_COMMAND} -DMATCH_REGEX=AM\([^_]+_[^_]+\)_Body -DREPLACE_EXPR=AM\\1 -P ${CMAKE_SOURCE_DIR}/cmake/file_regex_replace.cmake -- ${CARGO_TARGET_DIR}/${LIBRARY_NAME}.h
COMMAND
# Compensate for cbindgen's union tag enum type naming.
${CMAKE_COMMAND} -DMATCH_REGEX=AM\([^_]+\)_Tag -DREPLACE_EXPR=AM\\1Variant -P ${CMAKE_SOURCE_DIR}/cmake/file_regex_replace.cmake -- ${CARGO_TARGET_DIR}/${LIBRARY_NAME}.h
COMMAND
# Compensate for cbindgen's translation of consecutive uppercase letters to "ScreamingSnakeCase".
${CMAKE_COMMAND} -DMATCH_REGEX=A_M\([^_]+\)_ -DREPLACE_EXPR=AM_\\1_ -P ${CMAKE_SOURCE_DIR}/cmake/file_regex_replace.cmake -- ${CARGO_TARGET_DIR}/${LIBRARY_NAME}.h
COMMAND
# Compensate for cbindgen ignoring `std:mem::size_of<T>()` calls.
${CMAKE_COMMAND} -DMATCH_REGEX=USIZE_ -DREPLACE_EXPR=\+${CMAKE_SIZEOF_VOID_P} -P ${CMAKE_SOURCE_DIR}/cmake/file_regex_replace.cmake -- ${CARGO_TARGET_DIR}/${LIBRARY_NAME}.h
WORKING_DIRECTORY
${CMAKE_SOURCE_DIR}
COMMENT
"Compensating for cbindgen deficits..."
VERBATIM
)
if(BUILD_SHARED_LIBS)
if(WIN32)
set(LIBRARY_DESTINATION "${CMAKE_INSTALL_BINDIR}")
else()
set(LIBRARY_DESTINATION "${CMAKE_INSTALL_LIBDIR}")
endif()
set(LIBRARY_DEFINE_SYMBOL "${SYMBOL_PREFIX}_EXPORTS")
# \note The basename of an import library output by Cargo is the filename
# of its corresponding shared library.
set(LIBRARY_IMPLIB "${CARGO_CURRENT_BINARY_DIR}/${CMAKE_SHARED_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_SHARED_LIBRARY_SUFFIX}${CMAKE_STATIC_LIBRARY_SUFFIX}")
set(LIBRARY_LOCATION "${CARGO_CURRENT_BINARY_DIR}/${CMAKE_SHARED_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_SHARED_LIBRARY_SUFFIX}")
set(LIBRARY_NO_SONAME "${WIN32}")
set(LIBRARY_SONAME "${CMAKE_SHARED_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_${CMAKE_BUILD_TYPE}_POSTFIX}${CMAKE_SHARED_LIBRARY_SUFFIX}")
set(LIBRARY_TYPE "SHARED")
else()
set(LIBRARY_DEFINE_SYMBOL "")
set(LIBRARY_DESTINATION "${CMAKE_INSTALL_LIBDIR}")
set(LIBRARY_IMPLIB "")
set(LIBRARY_LOCATION "${CARGO_CURRENT_BINARY_DIR}/${CMAKE_STATIC_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_STATIC_LIBRARY_SUFFIX}")
set(LIBRARY_NO_SONAME "TRUE")
set(LIBRARY_SONAME "")
set(LIBRARY_TYPE "STATIC")
endif()
add_library(${LIBRARY_NAME} ${LIBRARY_TYPE} IMPORTED GLOBAL)
set_target_properties(
${LIBRARY_NAME}
PROPERTIES
# \note Cargo writes a debug build into a nested directory instead of
# decorating its name.
DEBUG_POSTFIX ""
DEFINE_SYMBOL "${LIBRARY_DEFINE_SYMBOL}"
IMPORTED_IMPLIB "${LIBRARY_IMPLIB}"
IMPORTED_LOCATION "${LIBRARY_LOCATION}"
IMPORTED_NO_SONAME "${LIBRARY_NO_SONAME}"
IMPORTED_SONAME "${LIBRARY_SONAME}"
LINKER_LANGUAGE C
PUBLIC_HEADER "${CARGO_TARGET_DIR}/${LIBRARY_NAME}.h"
SOVERSION "${PROJECT_VERSION_MAJOR}"
VERSION "${PROJECT_VERSION}"
# \note Cargo exports all of the symbols automatically.
WINDOWS_EXPORT_ALL_SYMBOLS "TRUE"
)
target_compile_definitions(${LIBRARY_NAME} INTERFACE $<TARGET_PROPERTY:${LIBRARY_NAME},DEFINE_SYMBOL>)
target_include_directories(
${LIBRARY_NAME}
INTERFACE
"$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}>"
)
set(CMAKE_THREAD_PREFER_PTHREAD TRUE)
set(THREADS_PREFER_PTHREAD_FLAG TRUE)
find_package(Threads REQUIRED)
set(LIBRARY_DEPENDENCIES Threads::Threads ${CMAKE_DL_LIBS})
if(WIN32)
list(APPEND LIBRARY_DEPENDENCIES Bcrypt userenv ws2_32)
else()
list(APPEND LIBRARY_DEPENDENCIES m)
endif()
target_link_libraries(${LIBRARY_NAME} INTERFACE ${LIBRARY_DEPENDENCIES})
install(
FILES $<TARGET_PROPERTY:${LIBRARY_NAME},IMPORTED_IMPLIB>
TYPE LIB
# \note The basename of an import library output by Cargo is the filename
# of its corresponding shared library.
RENAME "${CMAKE_STATIC_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_${CMAKE_BUILD_TYPE}_POSTFIX}${CMAKE_STATIC_LIBRARY_SUFFIX}"
OPTIONAL
)
set(LIBRARY_FILE_NAME "${CMAKE_${LIBRARY_TYPE}_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_${CMAKE_BUILD_TYPE}_POSTFIX}${CMAKE_${LIBRARY_TYPE}_LIBRARY_SUFFIX}")
install(
FILES $<TARGET_PROPERTY:${LIBRARY_NAME},IMPORTED_LOCATION>
RENAME "${LIBRARY_FILE_NAME}"
DESTINATION ${LIBRARY_DESTINATION}
)
install(
FILES $<TARGET_PROPERTY:${LIBRARY_NAME},PUBLIC_HEADER>
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}
)
find_package(Doxygen OPTIONAL_COMPONENTS dot)
if(DOXYGEN_FOUND)
set(DOXYGEN_GENERATE_LATEX YES)
set(DOXYGEN_PDF_HYPERLINKS YES)
set(DOXYGEN_PROJECT_LOGO "${CMAKE_SOURCE_DIR}/img/brandmark.png")
set(DOXYGEN_SORT_BRIEF_DOCS YES)
set(DOXYGEN_USE_MDFILE_AS_MAINPAGE "${CMAKE_SOURCE_DIR}/README.md")
doxygen_add_docs(
${LIBRARY_NAME}_docs
"${CARGO_TARGET_DIR}/${LIBRARY_NAME}.h"
"${CMAKE_SOURCE_DIR}/README.md"
USE_STAMP_FILE
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMENT "Producing documentation with Doxygen..."
)
# \note A Doxygen input file isn't a file-level dependency so the Doxygen
# command must instead depend upon a target that outputs the file or
# it will just output an error message when it can't be found.
add_dependencies(${LIBRARY_NAME}_docs ${LIBRARY_NAME}_artifacts)
endif()

View file

@ -0,0 +1,62 @@
use automerge as am;
/// \struct AMbyteSpan
/// \brief A contiguous sequence of bytes.
///
#[repr(C)]
pub struct AMbyteSpan {
/// A pointer to an array of bytes.
/// \warning \p src is only valid until the `AMfree()` function is
/// called on the `AMresult` struct hosting the array of bytes to
/// which it points.
src: *const u8,
/// The number of bytes in the array.
count: usize,
}
impl Default for AMbyteSpan {
fn default() -> Self {
Self {
src: std::ptr::null(),
count: 0,
}
}
}
impl From<&am::ActorId> for AMbyteSpan {
fn from(actor: &am::ActorId) -> Self {
let slice = actor.to_bytes();
Self {
src: slice.as_ptr(),
count: slice.len(),
}
}
}
impl From<&mut am::ActorId> for AMbyteSpan {
fn from(actor: &mut am::ActorId) -> Self {
let slice = actor.to_bytes();
Self {
src: slice.as_ptr(),
count: slice.len(),
}
}
}
impl From<&am::ChangeHash> for AMbyteSpan {
fn from(change_hash: &am::ChangeHash) -> Self {
Self {
src: change_hash.0.as_ptr(),
count: change_hash.0.len(),
}
}
}
impl From<&[u8]> for AMbyteSpan {
fn from(slice: &[u8]) -> Self {
Self {
src: slice.as_ptr(),
count: slice.len(),
}
}
}

View file

@ -1,55 +1,32 @@
use automerge as am;
use std::cell::RefCell;
use std::ffi::CString;
use std::os::raw::c_char;
use crate::byte_span::AMbyteSpan;
use crate::change_hashes::AMchangeHashes;
use crate::result::{to_result, AMresult};
macro_rules! to_change {
($handle:expr) => {{
let handle = $handle.as_ref();
match handle {
Some(b) => b,
None => return AMresult::error("Invalid `AMchange*`").into(),
}
}};
}
/// \struct AMchange
/// \installed_headerfile
/// \brief A group of operations performed by an actor.
#[derive(Eq, PartialEq)]
pub struct AMchange {
body: *mut am::Change,
change_hash: RefCell<Option<am::ChangeHash>>,
c_message: Option<CString>,
}
impl AMchange {
pub fn new(change: &mut am::Change) -> Self {
let c_message = match change.message() {
Some(c_message) => CString::new(c_message).ok(),
None => None,
};
Self {
body: change,
change_hash: Default::default(),
c_message,
}
}
pub fn message(&self) -> AMbyteSpan {
if let Some(message) = unsafe { (*self.body).message() } {
return message.as_str().as_bytes().into();
}
Default::default()
}
pub fn hash(&self) -> AMbyteSpan {
let mut change_hash = self.change_hash.borrow_mut();
if let Some(change_hash) = change_hash.as_ref() {
change_hash.into()
} else {
let hash = unsafe { (*self.body).hash() };
let ptr = change_hash.insert(hash);
AMbyteSpan {
src: ptr.0.as_ptr(),
count: hash.as_ref().len(),
}
}
pub fn c_message(&self) -> Option<&CString> {
self.c_message.as_ref()
}
}
@ -66,38 +43,36 @@ impl AsRef<am::Change> for AMchange {
}
/// \memberof AMchange
/// \brief Gets the first referenced actor identifier in a change.
/// \brief Gets the first referenced actor ID in a change.
///
/// \param[in] change A pointer to an `AMchange` struct.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_ACTOR_ID` item.
/// \pre \p change `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \return An actor ID as an `AMbyteSpan` struct.
/// \pre \p change must be a valid address.
/// \internal
///
/// # Safety
/// change must be a valid pointer to an AMchange
/// change must be a pointer to a valid AMchange
#[no_mangle]
pub unsafe extern "C" fn AMchangeActorId(change: *const AMchange) -> *mut AMresult {
let change = to_change!(change);
to_result(Ok::<am::ActorId, am::AutomergeError>(
change.as_ref().actor_id().clone(),
))
pub unsafe extern "C" fn AMchangeActorId(change: *const AMchange) -> AMbyteSpan {
match change.as_ref() {
Some(change) => change.as_ref().actor_id().into(),
None => AMbyteSpan::default(),
}
}
/// \memberof AMchange
/// \brief Compresses the raw bytes of a change.
///
/// \param[in] change A pointer to an `AMchange` struct.
/// \pre \p change `!= NULL`
/// \pre \p change must be a valid address.
/// \internal
///
/// # Safety
/// change must be a valid pointer to an AMchange
/// change must be a pointer to a valid AMchange
#[no_mangle]
pub unsafe extern "C" fn AMchangeCompress(change: *mut AMchange) {
if let Some(change) = change.as_mut() {
let _ = change.as_mut().bytes();
change.as_mut().compress();
};
}
@ -105,20 +80,18 @@ pub unsafe extern "C" fn AMchangeCompress(change: *mut AMchange) {
/// \brief Gets the dependencies of a change.
///
/// \param[in] change A pointer to an `AMchange` struct.
/// \return A pointer to an `AMresult` struct with `AM_VAL_TYPE_CHANGE_HASH` items.
/// \pre \p change `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \return A pointer to an `AMchangeHashes` struct or `NULL`.
/// \pre \p change must be a valid address.
/// \internal
///
/// # Safety
/// change must be a valid pointer to an AMchange
/// change must be a pointer to a valid AMchange
#[no_mangle]
pub unsafe extern "C" fn AMchangeDeps(change: *const AMchange) -> *mut AMresult {
to_result(match change.as_ref() {
Some(change) => change.as_ref().deps(),
None => Default::default(),
})
pub unsafe extern "C" fn AMchangeDeps(change: *const AMchange) -> AMchangeHashes {
match change.as_ref() {
Some(change) => AMchangeHashes::new(&change.as_ref().deps),
None => AMchangeHashes::default(),
}
}
/// \memberof AMchange
@ -126,57 +99,59 @@ pub unsafe extern "C" fn AMchangeDeps(change: *const AMchange) -> *mut AMresult
///
/// \param[in] change A pointer to an `AMchange` struct.
/// \return An `AMbyteSpan` struct.
/// \pre \p change `!= NULL`
/// \pre \p change must be a valid address.
/// \internal
///
/// # Safety
/// change must be a valid pointer to an AMchange
/// change must be a pointer to a valid AMchange
#[no_mangle]
pub unsafe extern "C" fn AMchangeExtraBytes(change: *const AMchange) -> AMbyteSpan {
if let Some(change) = change.as_ref() {
change.as_ref().extra_bytes().into()
} else {
Default::default()
AMbyteSpan::default()
}
}
/// \memberof AMchange
/// \brief Allocates a new change and initializes it from an array of bytes value.
/// \brief Loads a sequence of bytes into a change.
///
/// \param[in] src A pointer to an array of bytes.
/// \param[in] count The count of bytes to load from the array pointed to by
/// \p src.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_CHANGE` item.
/// \pre \p src `!= NULL`
/// \pre `sizeof(`\p src `) > 0`
/// \pre \p count `<= sizeof(`\p src `)`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \param[in] count The number of bytes in \p src to load.
/// \return A pointer to an `AMresult` struct containing an `AMchange` struct.
/// \pre \p src must be a valid address.
/// \pre `0 <=` \p count `<=` length of \p src.
/// \warning To avoid a memory leak, the returned `AMresult` struct must be
/// deallocated with `AMfree()`.
/// \internal
///
/// # Safety
/// src must be a byte array of length `>= count`
#[no_mangle]
pub unsafe extern "C" fn AMchangeFromBytes(src: *const u8, count: usize) -> *mut AMresult {
let data = std::slice::from_raw_parts(src, count);
to_result(am::Change::from_bytes(data.to_vec()))
let mut data = Vec::new();
data.extend_from_slice(std::slice::from_raw_parts(src, count));
to_result(am::Change::from_bytes(data))
}
/// \memberof AMchange
/// \brief Gets the hash of a change.
///
/// \param[in] change A pointer to an `AMchange` struct.
/// \return An `AMbyteSpan` struct for a change hash.
/// \pre \p change `!= NULL`
/// \return A change hash as an `AMbyteSpan` struct.
/// \pre \p change must be a valid address.
/// \internal
///
/// # Safety
/// change must be a valid pointer to an AMchange
/// change must be a pointer to a valid AMchange
#[no_mangle]
pub unsafe extern "C" fn AMchangeHash(change: *const AMchange) -> AMbyteSpan {
match change.as_ref() {
Some(change) => change.hash(),
None => Default::default(),
Some(change) => {
let hash: &am::ChangeHash = &change.as_ref().hash;
hash.into()
}
None => AMbyteSpan::default(),
}
}
@ -184,12 +159,12 @@ pub unsafe extern "C" fn AMchangeHash(change: *const AMchange) -> AMbyteSpan {
/// \brief Tests the emptiness of a change.
///
/// \param[in] change A pointer to an `AMchange` struct.
/// \return `true` if \p change is empty, `false` otherwise.
/// \pre \p change `!= NULL`
/// \return A boolean.
/// \pre \p change must be a valid address.
/// \internal
///
/// # Safety
/// change must be a valid pointer to an AMchange
/// change must be a pointer to a valid AMchange
#[no_mangle]
pub unsafe extern "C" fn AMchangeIsEmpty(change: *const AMchange) -> bool {
if let Some(change) = change.as_ref() {
@ -199,41 +174,16 @@ pub unsafe extern "C" fn AMchangeIsEmpty(change: *const AMchange) -> bool {
}
}
/// \memberof AMchange
/// \brief Loads a document into a sequence of changes.
///
/// \param[in] src A pointer to an array of bytes.
/// \param[in] count The count of bytes to load from the array pointed to by
/// \p src.
/// \return A pointer to an `AMresult` struct with `AM_VAL_TYPE_CHANGE` items.
/// \pre \p src `!= NULL`
/// \pre `sizeof(`\p src `) > 0`
/// \pre \p count `<= sizeof(`\p src `)`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \internal
///
/// # Safety
/// src must be a byte array of length `>= count`
#[no_mangle]
pub unsafe extern "C" fn AMchangeLoadDocument(src: *const u8, count: usize) -> *mut AMresult {
let data = std::slice::from_raw_parts(src, count);
to_result::<Result<Vec<am::Change>, _>>(
am::Automerge::load(data)
.and_then(|d| d.get_changes(&[]).map(|c| c.into_iter().cloned().collect())),
)
}
/// \memberof AMchange
/// \brief Gets the maximum operation index of a change.
///
/// \param[in] change A pointer to an `AMchange` struct.
/// \return A 64-bit unsigned integer.
/// \pre \p change `!= NULL`
/// \pre \p change must be a valid address.
/// \internal
///
/// # Safety
/// change must be a valid pointer to an AMchange
/// change must be a pointer to a valid AMchange
#[no_mangle]
pub unsafe extern "C" fn AMchangeMaxOp(change: *const AMchange) -> u64 {
if let Some(change) = change.as_ref() {
@ -247,18 +197,20 @@ pub unsafe extern "C" fn AMchangeMaxOp(change: *const AMchange) -> u64 {
/// \brief Gets the message of a change.
///
/// \param[in] change A pointer to an `AMchange` struct.
/// \return An `AMbyteSpan` struct for a UTF-8 string.
/// \pre \p change `!= NULL`
/// \return A UTF-8 string or `NULL`.
/// \pre \p change must be a valid address.
/// \internal
///
/// # Safety
/// change must be a valid pointer to an AMchange
/// change must be a pointer to a valid AMchange
#[no_mangle]
pub unsafe extern "C" fn AMchangeMessage(change: *const AMchange) -> AMbyteSpan {
pub unsafe extern "C" fn AMchangeMessage(change: *const AMchange) -> *const c_char {
if let Some(change) = change.as_ref() {
return change.message();
};
Default::default()
if let Some(c_message) = change.c_message() {
return c_message.as_ptr();
}
}
std::ptr::null::<c_char>()
}
/// \memberof AMchange
@ -266,15 +218,15 @@ pub unsafe extern "C" fn AMchangeMessage(change: *const AMchange) -> AMbyteSpan
///
/// \param[in] change A pointer to an `AMchange` struct.
/// \return A 64-bit unsigned integer.
/// \pre \p change `!= NULL`
/// \pre \p change must be a valid address.
/// \internal
///
/// # Safety
/// change must be a valid pointer to an AMchange
/// change must be a pointer to a valid AMchange
#[no_mangle]
pub unsafe extern "C" fn AMchangeSeq(change: *const AMchange) -> u64 {
if let Some(change) = change.as_ref() {
change.as_ref().seq()
change.as_ref().seq
} else {
u64::MAX
}
@ -285,33 +237,34 @@ pub unsafe extern "C" fn AMchangeSeq(change: *const AMchange) -> u64 {
///
/// \param[in] change A pointer to an `AMchange` struct.
/// \return A 64-bit unsigned integer.
/// \pre \p change `!= NULL`
/// \pre \p change must be a valid address.
/// \internal
///
/// # Safety
/// change must be a valid pointer to an AMchange
/// change must be a pointer to a valid AMchange
#[no_mangle]
pub unsafe extern "C" fn AMchangeSize(change: *const AMchange) -> usize {
if let Some(change) = change.as_ref() {
return change.as_ref().len();
}
change.as_ref().len()
} else {
0
}
}
/// \memberof AMchange
/// \brief Gets the start operation index of a change.
///
/// \param[in] change A pointer to an `AMchange` struct.
/// \return A 64-bit unsigned integer.
/// \pre \p change `!= NULL`
/// \pre \p change must be a valid address.
/// \internal
///
/// # Safety
/// change must be a valid pointer to an AMchange
/// change must be a pointer to a valid AMchange
#[no_mangle]
pub unsafe extern "C" fn AMchangeStartOp(change: *const AMchange) -> u64 {
if let Some(change) = change.as_ref() {
u64::from(change.as_ref().start_op())
u64::from(change.as_ref().start_op)
} else {
u64::MAX
}
@ -322,15 +275,15 @@ pub unsafe extern "C" fn AMchangeStartOp(change: *const AMchange) -> u64 {
///
/// \param[in] change A pointer to an `AMchange` struct.
/// \return A 64-bit signed integer.
/// \pre \p change `!= NULL`
/// \pre \p change must be a valid address.
/// \internal
///
/// # Safety
/// change must be a valid pointer to an AMchange
/// change must be a pointer to a valid AMchange
#[no_mangle]
pub unsafe extern "C" fn AMchangeTime(change: *const AMchange) -> i64 {
if let Some(change) = change.as_ref() {
change.as_ref().timestamp()
change.as_ref().time
} else {
i64::MAX
}
@ -340,17 +293,39 @@ pub unsafe extern "C" fn AMchangeTime(change: *const AMchange) -> i64 {
/// \brief Gets the raw bytes of a change.
///
/// \param[in] change A pointer to an `AMchange` struct.
/// \return An `AMbyteSpan` struct for an array of bytes.
/// \pre \p change `!= NULL`
/// \return An `AMbyteSpan` struct.
/// \pre \p change must be a valid address.
/// \internal
///
/// # Safety
/// change must be a valid pointer to an AMchange
/// change must be a pointer to a valid AMchange
#[no_mangle]
pub unsafe extern "C" fn AMchangeRawBytes(change: *const AMchange) -> AMbyteSpan {
if let Some(change) = change.as_ref() {
change.as_ref().raw_bytes().into()
} else {
Default::default()
AMbyteSpan::default()
}
}
/// \memberof AMchange
/// \brief Loads a document into a sequence of changes.
///
/// \param[in] src A pointer to an array of bytes.
/// \param[in] count The number of bytes in \p src to load.
/// \return A pointer to an `AMresult` struct containing a sequence of
/// `AMchange` structs.
/// \pre \p src must be a valid address.
/// \pre `0 <=` \p count `<=` length of \p src.
/// \warning To avoid a memory leak, the returned `AMresult` struct must be
/// deallocated with `AMfree()`.
/// \internal
///
/// # Safety
/// src must be a byte array of length `>= count`
#[no_mangle]
pub unsafe extern "C" fn AMchangeLoadDocument(src: *const u8, count: usize) -> *mut AMresult {
let mut data = Vec::new();
data.extend_from_slice(std::slice::from_raw_parts(src, count));
to_result(am::Change::load_document(&data))
}

View file

@ -0,0 +1,304 @@
use automerge as am;
use std::cmp::Ordering;
use std::ffi::c_void;
use std::mem::size_of;
use crate::byte_span::AMbyteSpan;
#[repr(C)]
struct Detail {
len: usize,
offset: isize,
ptr: *const c_void,
}
/// \note cbindgen won't propagate the value of a `std::mem::size_of<T>()` call
/// (https://github.com/eqrion/cbindgen/issues/252) but it will
/// propagate the name of a constant initialized from it so if the
/// constant's name is a symbolic representation of the value it can be
/// converted into a number by post-processing the header it generated.
pub const USIZE_USIZE_USIZE_: usize = size_of::<Detail>();
impl Detail {
fn new(change_hashes: &[am::ChangeHash], offset: isize) -> Self {
Self {
len: change_hashes.len(),
offset,
ptr: change_hashes.as_ptr() as *const c_void,
}
}
pub fn advance(&mut self, n: isize) {
if n != 0 && !self.is_stopped() {
let n = if self.offset < 0 { -n } else { n };
let len = self.len as isize;
self.offset = std::cmp::max(-(len + 1), std::cmp::min(self.offset + n, len));
};
}
pub fn get_index(&self) -> usize {
(self.offset
+ if self.offset < 0 {
self.len as isize
} else {
0
}) as usize
}
pub fn next(&mut self, n: isize) -> Option<&am::ChangeHash> {
if self.is_stopped() {
return None;
}
let slice: &[am::ChangeHash] =
unsafe { std::slice::from_raw_parts(self.ptr as *const am::ChangeHash, self.len) };
let value = &slice[self.get_index()];
self.advance(n);
Some(value)
}
pub fn is_stopped(&self) -> bool {
let len = self.len as isize;
self.offset < -len || self.offset == len
}
pub fn prev(&mut self, n: isize) -> Option<&am::ChangeHash> {
self.advance(n);
if self.is_stopped() {
return None;
}
let slice: &[am::ChangeHash] =
unsafe { std::slice::from_raw_parts(self.ptr as *const am::ChangeHash, self.len) };
Some(&slice[self.get_index()])
}
pub fn reversed(&self) -> Self {
Self {
len: self.len,
offset: -(self.offset + 1),
ptr: self.ptr,
}
}
}
impl From<Detail> for [u8; USIZE_USIZE_USIZE_] {
fn from(detail: Detail) -> Self {
unsafe {
std::slice::from_raw_parts((&detail as *const Detail) as *const u8, USIZE_USIZE_USIZE_)
.try_into()
.unwrap()
}
}
}
/// \struct AMchangeHashes
/// \brief A random-access iterator over a sequence of change hashes.
#[repr(C)]
pub struct AMchangeHashes {
/// Reserved.
detail: [u8; USIZE_USIZE_USIZE_],
}
impl AMchangeHashes {
pub fn new(change_hashes: &[am::ChangeHash]) -> Self {
Self {
detail: Detail::new(change_hashes, 0).into(),
}
}
pub fn advance(&mut self, n: isize) {
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
detail.advance(n);
}
pub fn len(&self) -> usize {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
detail.len
}
pub fn next(&mut self, n: isize) -> Option<&am::ChangeHash> {
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
detail.next(n)
}
pub fn prev(&mut self, n: isize) -> Option<&am::ChangeHash> {
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
detail.prev(n)
}
pub fn reversed(&self) -> Self {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
Self {
detail: detail.reversed().into(),
}
}
}
impl AsRef<[am::ChangeHash]> for AMchangeHashes {
fn as_ref(&self) -> &[am::ChangeHash] {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
unsafe { std::slice::from_raw_parts(detail.ptr as *const am::ChangeHash, detail.len) }
}
}
impl Default for AMchangeHashes {
fn default() -> Self {
Self {
detail: [0; USIZE_USIZE_USIZE_],
}
}
}
/// \memberof AMchangeHashes
/// \brief Advances an iterator over a sequence of change hashes by at most
/// \p |n| positions where the sign of \p n is relative to the
/// iterator's direction.
///
/// \param[in] change_hashes A pointer to an `AMchangeHashes` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \pre \p change_hashes must be a valid address.
/// \internal
///
/// #Safety
/// change_hashes must be a pointer to a valid AMchangeHashes
#[no_mangle]
pub unsafe extern "C" fn AMchangeHashesAdvance(change_hashes: *mut AMchangeHashes, n: isize) {
if let Some(change_hashes) = change_hashes.as_mut() {
change_hashes.advance(n);
};
}
/// \memberof AMchangeHashes
/// \brief Compares the sequences of change hashes underlying a pair of
/// iterators.
///
/// \param[in] change_hashes1 A pointer to an `AMchangeHashes` struct.
/// \param[in] change_hashes2 A pointer to an `AMchangeHashes` struct.
/// \return `-1` if \p change_hashes1 `<` \p change_hashes2, `0` if
/// \p change_hashes1 `==` \p change_hashes2 and `1` if
/// \p change_hashes1 `>` \p change_hashes2.
/// \pre \p change_hashes1 must be a valid address.
/// \pre \p change_hashes2 must be a valid address.
/// \internal
///
/// #Safety
/// change_hashes1 must be a pointer to a valid AMchangeHashes
/// change_hashes2 must be a pointer to a valid AMchangeHashes
#[no_mangle]
pub unsafe extern "C" fn AMchangeHashesCmp(
change_hashes1: *const AMchangeHashes,
change_hashes2: *const AMchangeHashes,
) -> isize {
match (change_hashes1.as_ref(), change_hashes2.as_ref()) {
(Some(change_hashes1), Some(change_hashes2)) => {
match change_hashes1.as_ref().cmp(change_hashes2.as_ref()) {
Ordering::Less => -1,
Ordering::Equal => 0,
Ordering::Greater => 1,
}
}
(None, Some(_)) => -1,
(Some(_), None) => 1,
(None, None) => 0,
}
}
/// \memberof AMchangeHashes
/// \brief Gets the change hash at the current position of an iterator over a
/// sequence of change hashes and then advances it by at most \p |n|
/// positions where the sign of \p n is relative to the iterator's
/// direction.
///
/// \param[in] change_hashes A pointer to an `AMchangeHashes` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \return An `AMbyteSpan` struct with `.src == NULL` when \p change_hashes
/// was previously advanced past its forward/reverse limit.
/// \pre \p change_hashes must be a valid address.
/// \internal
///
/// #Safety
/// change_hashes must be a pointer to a valid AMchangeHashes
#[no_mangle]
pub unsafe extern "C" fn AMchangeHashesNext(
change_hashes: *mut AMchangeHashes,
n: isize,
) -> AMbyteSpan {
if let Some(change_hashes) = change_hashes.as_mut() {
if let Some(change_hash) = change_hashes.next(n) {
return change_hash.into();
}
}
AMbyteSpan::default()
}
/// \memberof AMchangeHashes
/// \brief Advances an iterator over a sequence of change hashes by at most
/// \p |n| positions where the sign of \p n is relative to the
/// iterator's direction and then gets the change hash at its new
/// position.
///
/// \param[in] change_hashes A pointer to an `AMchangeHashes` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \return An `AMbyteSpan` struct with `.src == NULL` when \p change_hashes is
/// presently advanced past its forward/reverse limit.
/// \pre \p change_hashes must be a valid address.
/// \internal
///
/// #Safety
/// change_hashes must be a pointer to a valid AMchangeHashes
#[no_mangle]
pub unsafe extern "C" fn AMchangeHashesPrev(
change_hashes: *mut AMchangeHashes,
n: isize,
) -> AMbyteSpan {
if let Some(change_hashes) = change_hashes.as_mut() {
if let Some(change_hash) = change_hashes.prev(n) {
return change_hash.into();
}
}
AMbyteSpan::default()
}
/// \memberof AMchangeHashes
/// \brief Gets the size of the sequence of change hashes underlying an
/// iterator.
///
/// \param[in] change_hashes A pointer to an `AMchangeHashes` struct.
/// \return The count of values in \p change_hashes.
/// \pre \p change_hashes must be a valid address.
/// \internal
///
/// #Safety
/// change_hashes must be a pointer to a valid AMchangeHashes
#[no_mangle]
pub unsafe extern "C" fn AMchangeHashesSize(change_hashes: *const AMchangeHashes) -> usize {
if let Some(change_hashes) = change_hashes.as_ref() {
change_hashes.len()
} else {
0
}
}
/// \memberof AMchangeHashes
/// \brief Creates an iterator over the same sequence of change hashes as the
/// given one but with the opposite position and direction.
///
/// \param[in] change_hashes A pointer to an `AMchangeHashes` struct.
/// \return An `AMchangeHashes` struct
/// \pre \p change_hashes must be a valid address.
/// \internal
///
/// #Safety
/// change_hashes must be a pointer to a valid AMchangeHashes
#[no_mangle]
pub unsafe extern "C" fn AMchangeHashesReversed(
change_hashes: *const AMchangeHashes,
) -> AMchangeHashes {
if let Some(change_hashes) = change_hashes.as_ref() {
change_hashes.reversed()
} else {
AMchangeHashes::default()
}
}

306
automerge-c/src/changes.rs Normal file
View file

@ -0,0 +1,306 @@
use automerge as am;
use std::collections::BTreeMap;
use std::ffi::c_void;
use std::mem::size_of;
use crate::change::AMchange;
#[repr(C)]
struct Detail {
len: usize,
offset: isize,
ptr: *const c_void,
storage: *mut c_void,
}
/// \note cbindgen won't propagate the value of a `std::mem::size_of<T>()` call
/// (https://github.com/eqrion/cbindgen/issues/252) but it will
/// propagate the name of a constant initialized from it so if the
/// constant's name is a symbolic representation of the value it can be
/// converted into a number by post-processing the header it generated.
pub const USIZE_USIZE_USIZE_USIZE_: usize = size_of::<Detail>();
impl Detail {
fn new(changes: &[am::Change], offset: isize, storage: &mut BTreeMap<usize, AMchange>) -> Self {
let storage: *mut BTreeMap<usize, AMchange> = storage;
Self {
len: changes.len(),
offset,
ptr: changes.as_ptr() as *const c_void,
storage: storage as *mut c_void,
}
}
pub fn advance(&mut self, n: isize) {
if n != 0 && !self.is_stopped() {
let n = if self.offset < 0 { -n } else { n };
let len = self.len as isize;
self.offset = std::cmp::max(-(len + 1), std::cmp::min(self.offset + n, len));
};
}
pub fn get_index(&self) -> usize {
(self.offset
+ if self.offset < 0 {
self.len as isize
} else {
0
}) as usize
}
pub fn next(&mut self, n: isize) -> Option<*const AMchange> {
if self.is_stopped() {
return None;
}
let slice: &mut [am::Change] =
unsafe { std::slice::from_raw_parts_mut(self.ptr as *mut am::Change, self.len) };
let storage = unsafe { &mut *(self.storage as *mut BTreeMap<usize, AMchange>) };
let index = self.get_index();
let value = match storage.get_mut(&index) {
Some(value) => value,
None => {
storage.insert(index, AMchange::new(&mut slice[index]));
storage.get_mut(&index).unwrap()
}
};
self.advance(n);
Some(value)
}
pub fn is_stopped(&self) -> bool {
let len = self.len as isize;
self.offset < -len || self.offset == len
}
pub fn prev(&mut self, n: isize) -> Option<*const AMchange> {
self.advance(n);
if self.is_stopped() {
return None;
}
let slice: &mut [am::Change] =
unsafe { std::slice::from_raw_parts_mut(self.ptr as *mut am::Change, self.len) };
let storage = unsafe { &mut *(self.storage as *mut BTreeMap<usize, AMchange>) };
let index = self.get_index();
Some(match storage.get_mut(&index) {
Some(value) => value,
None => {
storage.insert(index, AMchange::new(&mut slice[index]));
storage.get_mut(&index).unwrap()
}
})
}
pub fn reversed(&self) -> Self {
Self {
len: self.len,
offset: -(self.offset + 1),
ptr: self.ptr,
storage: self.storage,
}
}
}
impl From<Detail> for [u8; USIZE_USIZE_USIZE_USIZE_] {
fn from(detail: Detail) -> Self {
unsafe {
std::slice::from_raw_parts(
(&detail as *const Detail) as *const u8,
USIZE_USIZE_USIZE_USIZE_,
)
.try_into()
.unwrap()
}
}
}
/// \struct AMchanges
/// \brief A random-access iterator over a sequence of changes.
#[repr(C)]
pub struct AMchanges {
/// Reserved.
detail: [u8; USIZE_USIZE_USIZE_USIZE_],
}
impl AMchanges {
pub fn new(changes: &[am::Change], storage: &mut BTreeMap<usize, AMchange>) -> Self {
Self {
detail: Detail::new(changes, 0, storage).into(),
}
}
pub fn advance(&mut self, n: isize) {
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
detail.advance(n);
}
pub fn len(&self) -> usize {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
detail.len
}
pub fn next(&mut self, n: isize) -> Option<*const AMchange> {
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
detail.next(n)
}
pub fn prev(&mut self, n: isize) -> Option<*const AMchange> {
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
detail.prev(n)
}
pub fn reversed(&self) -> Self {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
Self {
detail: detail.reversed().into(),
}
}
}
impl AsRef<[am::Change]> for AMchanges {
fn as_ref(&self) -> &[am::Change] {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
unsafe { std::slice::from_raw_parts(detail.ptr as *const am::Change, detail.len) }
}
}
impl Default for AMchanges {
fn default() -> Self {
Self {
detail: [0; USIZE_USIZE_USIZE_USIZE_],
}
}
}
/// \memberof AMchanges
/// \brief Advances an iterator over a sequence of changes by at most \p |n|
/// positions where the sign of \p n is relative to the iterator's
/// direction.
///
/// \param[in] changes A pointer to an `AMchanges` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \pre \p changes must be a valid address.
/// \internal
///
/// #Safety
/// changes must be a pointer to a valid AMchanges
#[no_mangle]
pub unsafe extern "C" fn AMchangesAdvance(changes: *mut AMchanges, n: isize) {
if let Some(changes) = changes.as_mut() {
changes.advance(n);
};
}
/// \memberof AMchanges
/// \brief Tests the equality of two sequences of changes underlying a pair
/// of iterators.
///
/// \param[in] changes1 A pointer to an `AMchanges` struct.
/// \param[in] changes2 A pointer to an `AMchanges` struct.
/// \return `true` if \p changes1 `==` \p changes2 and `false` otherwise.
/// \pre \p changes1 must be a valid address.
/// \pre \p changes2 must be a valid address.
/// \internal
///
/// #Safety
/// changes1 must be a pointer to a valid AMchanges
/// changes2 must be a pointer to a valid AMchanges
#[no_mangle]
pub unsafe extern "C" fn AMchangesEqual(
changes1: *const AMchanges,
changes2: *const AMchanges,
) -> bool {
match (changes1.as_ref(), changes2.as_ref()) {
(Some(changes1), Some(changes2)) => changes1.as_ref() == changes2.as_ref(),
(None, Some(_)) | (Some(_), None) | (None, None) => false,
}
}
/// \memberof AMchanges
/// \brief Gets the change at the current position of an iterator over a
/// sequence of changes and then advances it by at most \p |n| positions
/// where the sign of \p n is relative to the iterator's direction.
///
/// \param[in] changes A pointer to an `AMchanges` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \return A pointer to an `AMchange` struct that's `NULL` when \p changes was
/// previously advanced past its forward/reverse limit.
/// \pre \p changes must be a valid address.
/// \internal
///
/// #Safety
/// changes must be a pointer to a valid AMchanges
#[no_mangle]
pub unsafe extern "C" fn AMchangesNext(changes: *mut AMchanges, n: isize) -> *const AMchange {
if let Some(changes) = changes.as_mut() {
if let Some(change) = changes.next(n) {
return change;
}
}
std::ptr::null()
}
/// \memberof AMchanges
/// \brief Advances an iterator over a sequence of changes by at most \p |n|
/// positions where the sign of \p n is relative to the iterator's
/// direction and then gets the change at its new position.
///
/// \param[in] changes A pointer to an `AMchanges` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \return A pointer to an `AMchange` struct that's `NULL` when \p changes is
/// presently advanced past its forward/reverse limit.
/// \pre \p changes must be a valid address.
/// \internal
///
/// #Safety
/// changes must be a pointer to a valid AMchanges
#[no_mangle]
pub unsafe extern "C" fn AMchangesPrev(changes: *mut AMchanges, n: isize) -> *const AMchange {
if let Some(changes) = changes.as_mut() {
if let Some(change) = changes.prev(n) {
return change;
}
}
std::ptr::null()
}
/// \memberof AMchanges
/// \brief Gets the size of the sequence of changes underlying an iterator.
///
/// \param[in] changes A pointer to an `AMchanges` struct.
/// \return The count of values in \p changes.
/// \pre \p changes must be a valid address.
/// \internal
///
/// #Safety
/// changes must be a pointer to a valid AMchanges
#[no_mangle]
pub unsafe extern "C" fn AMchangesSize(changes: *const AMchanges) -> usize {
if let Some(changes) = changes.as_ref() {
changes.len()
} else {
0
}
}
/// \memberof AMchanges
/// \brief Creates an iterator over the same sequence of changes as the given
/// one but with the opposite position and direction.
///
/// \param[in] changes A pointer to an `AMchanges` struct.
/// \return An `AMchanges` struct.
/// \pre \p changes must be a valid address.
/// \internal
///
/// #Safety
/// changes must be a pointer to a valid AMchanges
#[no_mangle]
pub unsafe extern "C" fn AMchangesReversed(changes: *const AMchanges) -> AMchanges {
if let Some(changes) = changes.as_ref() {
changes.reversed()
} else {
AMchanges::default()
}
}

659
automerge-c/src/doc.rs Normal file
View file

@ -0,0 +1,659 @@
use automerge as am;
use automerge::transaction::{CommitOptions, Transactable};
use smol_str::SmolStr;
use std::borrow::Cow;
use std::ops::{Deref, DerefMut};
use std::os::raw::c_char;
use crate::change::AMchange;
use crate::change_hashes::AMchangeHashes;
use crate::obj::AMobjId;
use crate::result::{to_result, AMresult};
use crate::sync::{to_sync_message, AMsyncMessage, AMsyncState};
mod list;
mod map;
mod utils;
use crate::changes::AMchanges;
use crate::doc::utils::to_str;
use crate::doc::utils::{to_doc, to_obj_id};
macro_rules! to_changes {
($handle:expr) => {{
let handle = $handle.as_ref();
match handle {
Some(b) => b,
None => return AMresult::err("Invalid AMchanges pointer").into(),
}
}};
}
macro_rules! to_sync_state_mut {
($handle:expr) => {{
let handle = $handle.as_mut();
match handle {
Some(b) => b,
None => return AMresult::err("Invalid AMsyncState pointer").into(),
}
}};
}
/// \struct AMdoc
/// \brief A JSON-like CRDT.
#[derive(Clone)]
pub struct AMdoc(am::AutoCommit);
impl AMdoc {
pub fn new(body: am::AutoCommit) -> Self {
Self(body)
}
}
impl AsRef<am::AutoCommit> for AMdoc {
fn as_ref(&self) -> &am::AutoCommit {
&self.0
}
}
impl Deref for AMdoc {
type Target = am::AutoCommit;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for AMdoc {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
/// \memberof AMdoc
/// \brief Applies a sequence of changes to a document.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] changes A pointer to an `AMchanges` struct.
/// \pre \p doc must be a valid address.
/// \pre \p changes must be a valid address.
/// \return A pointer to an `AMresult` struct containing a void.
/// \warning To avoid a memory leak, the returned `AMresult` struct must be
/// deallocated with `AMfree()`.
/// \internal
///
/// # Safety
/// doc must be a pointer to a valid AMdoc
/// changes must be a pointer to a valid AMchanges.
#[no_mangle]
pub unsafe extern "C" fn AMapplyChanges(
doc: *mut AMdoc,
changes: *const AMchanges,
) -> *mut AMresult {
let doc = to_doc!(doc);
let changes = to_changes!(changes);
to_result(doc.apply_changes(changes.as_ref().to_vec()))
}
/// \memberof AMdoc
/// \brief Allocates a new document and initializes it with defaults.
///
/// \return A pointer to an `AMresult` struct containing a pointer to an
/// `AMdoc` struct.
/// \warning To avoid a memory leak, the returned `AMresult` struct must be
/// deallocated with `AMfree()`.
#[no_mangle]
pub extern "C" fn AMcreate() -> *mut AMresult {
to_result(am::AutoCommit::new())
}
/// \memberof AMdoc
/// \brief Commits the current operations on a document with an optional
/// message and/or time override as seconds since the epoch.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] message A UTF-8 string or `NULL`.
/// \param[in] time A pointer to a `time_t` value or `NULL`.
/// \return A pointer to an `AMresult` struct containing a change hash as an
/// `AMbyteSpan` struct.
/// \pre \p doc must be a valid address.
/// \warning To avoid a memory leak, the returned `AMresult` struct must be
/// deallocated with `AMfree()`.
/// \internal
///
/// # Safety
/// doc must be a pointer to a valid AMdoc
#[no_mangle]
pub unsafe extern "C" fn AMcommit(
doc: *mut AMdoc,
message: *const c_char,
time: *const libc::time_t,
) -> *mut AMresult {
let doc = to_doc!(doc);
let mut options = CommitOptions::default();
if !message.is_null() {
options.set_message(to_str(message));
}
if let Some(time) = time.as_ref() {
options.set_time(*time);
}
to_result(doc.commit_with::<()>(options))
}
/// \memberof AMdoc
/// \brief Allocates storage for a document and initializes it by duplicating
/// the given document.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \return A pointer to an `AMresult` struct containing a pointer to an
/// `AMdoc` struct.
/// \pre \p doc must be a valid address.
/// \warning To avoid a memory leak, the returned `AMresult` struct must be
/// deallocated with `AMfree()`.
/// \internal
///
/// # Safety
/// doc must be a pointer to a valid AMdoc
#[no_mangle]
pub unsafe extern "C" fn AMdup(doc: *mut AMdoc) -> *mut AMresult {
let doc = to_doc!(doc);
to_result(doc.as_ref().clone())
}
/// \memberof AMdoc
/// \brief Tests the equality of two documents after closing their respective
/// transactions.
///
/// \param[in] doc1 An `AMdoc` struct.
/// \param[in] doc2 An `AMdoc` struct.
/// \return `true` if \p doc1 `==` \p doc2 and `false` otherwise.
/// \pre \p doc1 must be a valid address.
/// \pre \p doc2 must be a valid address.
/// \internal
///
/// #Safety
/// doc1 must be a pointer to a valid AMdoc
/// doc2 must be a pointer to a valid AMdoc
#[no_mangle]
pub unsafe extern "C" fn AMequal(doc1: *mut AMdoc, doc2: *mut AMdoc) -> bool {
match (doc1.as_mut(), doc2.as_mut()) {
(Some(doc1), Some(doc2)) => doc1.document().get_heads() == doc2.document().get_heads(),
(None, Some(_)) | (Some(_), None) | (None, None) => false,
}
}
/// \memberof AMdoc
/// \brief Generates a synchronization message for a peer based upon the given
/// synchronization state.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] sync_state A pointer to an `AMsyncState` struct.
/// \return A pointer to an `AMresult` struct containing either a pointer to an
/// `AMsyncMessage` struct or a void.
/// \pre \p doc must b e a valid address.
/// \pre \p sync_state must be a valid address.
/// \warning To avoid a memory leak, the returned `AMresult` struct must be
/// deallocated with `AMfree()`.
/// \internal
///
/// # Safety
/// doc must be a pointer to a valid AMdoc
/// sync_state must be a pointer to a valid AMsyncState
#[no_mangle]
pub unsafe extern "C" fn AMgenerateSyncMessage(
doc: *mut AMdoc,
sync_state: *mut AMsyncState,
) -> *mut AMresult {
let doc = to_doc!(doc);
let sync_state = to_sync_state_mut!(sync_state);
to_result(doc.generate_sync_message(sync_state.as_mut()))
}
/// \memberof AMdoc
/// \brief Gets an `AMdoc` struct's actor ID value as an array of bytes.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \return A pointer to an `AMresult` struct containing an actor ID as an
/// `AMbyteSpan` struct.
/// \pre \p doc must be a valid address.
/// \warning To avoid a memory leak, the returned `AMresult` struct must be
/// deallocated with `AMfree()`.
/// \internal
///
/// # Safety
/// doc must be a pointer to a valid AMdoc
#[no_mangle]
pub unsafe extern "C" fn AMgetActor(doc: *mut AMdoc) -> *mut AMresult {
let doc = to_doc!(doc);
to_result(Ok(doc.get_actor().clone()))
}
/// \memberof AMdoc
/// \brief Gets an `AMdoc` struct's actor ID value as a hexadecimal string.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \return A pointer to an `AMresult` struct containing a `char const*`.
/// \pre \p doc must be a valid address.
/// \warning To avoid a memory leak, the returned `AMresult` struct must be
/// deallocated with `AMfree()`.
/// \internal
///
/// # Safety
/// doc must be a pointer to a valid AMdoc
#[no_mangle]
pub unsafe extern "C" fn AMgetActorHex(doc: *mut AMdoc) -> *mut AMresult {
let doc = to_doc!(doc);
let hex_str = doc.get_actor().to_hex_string();
let value = am::Value::Scalar(Cow::Owned(am::ScalarValue::Str(SmolStr::new(hex_str))));
to_result(Ok(value))
}
/// \memberof AMdoc
/// \brief Gets the changes added to a document by their respective hashes.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] have_deps A pointer to an `AMchangeHashes` struct or `NULL`.
/// \return A pointer to an `AMresult` struct containing an `AMchanges` struct.
/// \pre \p doc must be a valid address.
/// \warning To avoid a memory leak, the returned `AMresult` struct must be
/// deallocated with `AMfree()`.
/// \internal
///
/// # Safety
/// doc must be a pointer to a valid AMdoc
#[no_mangle]
pub unsafe extern "C" fn AMgetChanges(
doc: *mut AMdoc,
have_deps: *const AMchangeHashes,
) -> *mut AMresult {
let doc = to_doc!(doc);
let empty_deps = Vec::<am::ChangeHash>::new();
let have_deps = match have_deps.as_ref() {
Some(have_deps) => have_deps.as_ref(),
None => &empty_deps,
};
to_result(doc.get_changes(have_deps))
}
/// \memberof AMdoc
/// \brief Gets the changes added to a second document that weren't added to
/// a first document.
///
/// \param[in] doc1 An `AMdoc` struct.
/// \param[in] doc2 An `AMdoc` struct.
/// \return A pointer to an `AMresult` struct containing an `AMchanges` struct.
/// \pre \p doc1 must be a valid address.
/// \pre \p doc2 must be a valid address.
/// \warning To avoid a memory leak, the returned `AMresult` struct must be
/// deallocated with `AMfree()`.
/// \internal
///
/// # Safety
/// doc1 must be a pointer to a valid AMdoc
/// doc2 must be a pointer to a valid AMdoc
#[no_mangle]
pub unsafe extern "C" fn AMgetChangesAdded(doc1: *mut AMdoc, doc2: *mut AMdoc) -> *mut AMresult {
let doc1 = to_doc!(doc1);
let doc2 = to_doc!(doc2);
to_result(doc1.get_changes_added(doc2))
}
/// \memberof AMdoc
/// \brief Gets the current heads of a document.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \return A pointer to an `AMresult` struct containing an `AMchangeHashes`
/// struct.
/// \pre \p doc must be a valid address.
/// \warning To avoid a memory leak, the returned `AMresult` struct must be
/// deallocated with `AMfree()`.
/// \internal
///
/// # Safety
/// doc must be a pointer to a valid AMdoc
#[no_mangle]
pub unsafe extern "C" fn AMgetHeads(doc: *mut AMdoc) -> *mut AMresult {
let doc = to_doc!(doc);
to_result(Ok(doc.get_heads()))
}
/// \memberof AMdoc
/// \brief Gets the hashes of the changes in a document that aren't transitive
/// dependencies of the given hashes of changes.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] heads A pointer to an `AMchangeHashes` struct or `NULL`.
/// \return A pointer to an `AMresult` struct containing an `AMchangeHashes`
/// struct.
/// \pre \p doc must be a valid address.
/// \warning To avoid a memory leak, the returned `AMresult` struct must be
/// deallocated with `AMfree()`.
/// \internal
///
/// # Safety
/// doc must be a pointer to a valid AMdoc
#[no_mangle]
pub unsafe extern "C" fn AMgetMissingDeps(
doc: *mut AMdoc,
heads: *const AMchangeHashes,
) -> *mut AMresult {
let doc = to_doc!(doc);
let empty_heads = Vec::<am::ChangeHash>::new();
let heads = match heads.as_ref() {
Some(heads) => heads.as_ref(),
None => &empty_heads,
};
to_result(doc.get_missing_deps(heads))
}
/// \memberof AMdoc
/// \brief Gets the last change made to a document.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \return A pointer to an `AMresult` struct containing either an `AMchange`
/// struct or a void.
/// \pre \p doc must be a valid address.
/// \warning To avoid a memory leak, the returned `AMresult` struct must be
/// deallocated with `AMfree()`.
/// \internal
///
/// # Safety
/// doc must be a pointer to a valid AMdoc
#[no_mangle]
pub unsafe extern "C" fn AMgetLastLocalChange(doc: *mut AMdoc) -> *mut AMresult {
let doc = to_doc!(doc);
to_result(doc.get_last_local_change())
}
/// \memberof AMdoc
/// \brief Allocates storage for a document and initializes it with the compact
/// form of an incremental save.
///
/// \param[in] src A pointer to an array of bytes.
/// \param[in] count The number of bytes in \p src to load.
/// \return A pointer to an `AMresult` struct containing a pointer to an
/// `AMdoc` struct.
/// \pre \p src must be a valid address.
/// \pre `0 <=` \p count `<=` length of \p src.
/// \warning To avoid a memory leak, the returned `AMresult` struct must be
/// deallocated with `AMfree()`.
/// \internal
///
/// # Safety
/// src must be a byte array of length `>= count`
#[no_mangle]
pub unsafe extern "C" fn AMload(src: *const u8, count: usize) -> *mut AMresult {
let mut data = Vec::new();
data.extend_from_slice(std::slice::from_raw_parts(src, count));
to_result(am::AutoCommit::load(&data))
}
/// \memberof AMdoc
/// \brief Loads the compact form of an incremental save into a document.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] src A pointer to an array of bytes.
/// \param[in] count The number of bytes in \p src to load.
/// \return A pointer to an `AMresult` struct containing the number of
/// operations loaded from \p src.
/// \pre \p doc must be a valid address.
/// \pre \p src must be a valid address.
/// \pre `0 <=` \p count `<=` length of \p src.
/// \warning To avoid a memory leak, the returned `AMresult` struct must be
/// deallocated with `AMfree()`.
/// \internal
///
/// # Safety
/// doc must be a pointer to a valid AMdoc
/// src must be a byte array of length `>= count`
#[no_mangle]
pub unsafe extern "C" fn AMloadIncremental(
doc: *mut AMdoc,
src: *const u8,
count: usize,
) -> *mut AMresult {
let doc = to_doc!(doc);
let mut data = Vec::new();
data.extend_from_slice(std::slice::from_raw_parts(src, count));
to_result(doc.load_incremental(&data))
}
/// \memberof AMdoc
/// \brief Applies all of the changes in \p src which are not in \p dest to
/// \p dest.
///
/// \param[in] dest A pointer to an `AMdoc` struct.
/// \param[in] src A pointer to an `AMdoc` struct.
/// \return A pointer to an `AMresult` struct containing an `AMchangeHashes`
/// struct.
/// \pre \p dest must be a valid address.
/// \pre \p src must be a valid address.
/// \warning To avoid a memory leak, the returned `AMresult` struct must be
/// deallocated with `AMfree()`.
/// \internal
///
/// # Safety
/// dest must be a pointer to a valid AMdoc
/// src must be a pointer to a valid AMdoc
#[no_mangle]
pub unsafe extern "C" fn AMmerge(dest: *mut AMdoc, src: *mut AMdoc) -> *mut AMresult {
let dest = to_doc!(dest);
to_result(dest.merge(to_doc!(src)))
}
/// \memberof AMdoc
/// \brief Gets the size of an object.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `NULL`.
/// \return The count of values in the object identified by \p obj_id.
/// \pre \p doc must be a valid address.
/// \internal
///
/// # Safety
/// doc must be a pointer to a valid AMdoc
/// obj_id must be a pointer to a valid AMobjId or NULL
#[no_mangle]
pub unsafe extern "C" fn AMobjSize(doc: *const AMdoc, obj_id: *const AMobjId) -> usize {
if let Some(doc) = doc.as_ref() {
doc.length(to_obj_id!(obj_id))
} else {
0
}
}
/// \memberof AMdoc
/// \brief Gets the historical size of an object.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `NULL`.
/// \param[in] change A pointer to an `AMchange` struct or `NULL`.
/// \return The count of values in the object identified by \p obj_id at
/// \p change.
/// \pre \p doc must be a valid address.
/// \internal
///
/// # Safety
/// doc must be a pointer to a valid AMdoc
/// obj_id must be a pointer to a valid AMobjId or NULL
/// change must be a pointer to a valid AMchange or NULL
#[no_mangle]
pub unsafe extern "C" fn AMobjSizeAt(
doc: *const AMdoc,
obj_id: *const AMobjId,
change: *const AMchange,
) -> usize {
if let Some(doc) = doc.as_ref() {
if let Some(change) = change.as_ref() {
let change: &am::Change = change.as_ref();
let change_hashes = vec![change.hash];
return doc.length_at(to_obj_id!(obj_id), &change_hashes);
}
};
0
}
/// \memberof AMdoc
/// \brief Gets the number of pending operations added during a document's
/// current transaction.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \return The count of pending operations for \p doc.
/// \pre \p doc must be a valid address.
/// \internal
///
/// # Safety
/// doc must be a pointer to a valid AMdoc
#[no_mangle]
pub unsafe extern "C" fn AMpendingOps(doc: *mut AMdoc) -> usize {
if let Some(doc) = doc.as_mut() {
doc.pending_ops()
} else {
0
}
}
/// \memberof AMdoc
/// \brief Receives a synchronization message from a peer based upon a given
/// synchronization state.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] sync_state A pointer to an `AMsyncState` struct.
/// \param[in] sync_message A pointer to an `AMsyncMessage` struct.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc must be a valid address.
/// \pre \p sync_state must be a valid address.
/// \pre \p sync_message must be a valid address.
/// \internal
///
/// # Safety
/// doc must be a pointer to a valid AMdoc
/// sync_state must be a pointer to a valid AMsyncState
/// sync_message must be a pointer to a valid AMsyncMessage
#[no_mangle]
pub unsafe extern "C" fn AMreceiveSyncMessage(
doc: *mut AMdoc,
sync_state: *mut AMsyncState,
sync_message: *const AMsyncMessage,
) -> *mut AMresult {
let doc = to_doc!(doc);
let sync_state = to_sync_state_mut!(sync_state);
let sync_message = to_sync_message!(sync_message);
to_result(doc.receive_sync_message(sync_state.as_mut(), sync_message.as_ref().clone()))
}
/// \memberof AMdoc
/// \brief Cancels the pending operations added during a document's current
/// transaction and gets the number of cancellations.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \return The count of pending operations for \p doc that were cancelled.
/// \pre \p doc must be a valid address.
/// \internal
///
/// # Safety
/// doc must be a pointer to a valid AMdoc
#[no_mangle]
pub unsafe extern "C" fn AMrollback(doc: *mut AMdoc) -> usize {
if let Some(doc) = doc.as_mut() {
doc.rollback()
} else {
0
}
}
/// \memberof AMdoc
/// \brief Saves the entirety of a document into a compact form.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \return A pointer to an `AMresult` struct containing an array of bytes as
/// an `AMbyteSpan` struct.
/// \pre \p doc must be a valid address.
/// \warning To avoid a memory leak, the returned `AMresult` struct must be
/// deallocated with `AMfree()`.
/// \internal
///
/// # Safety
/// doc must be a pointer to a valid AMdoc
#[no_mangle]
pub unsafe extern "C" fn AMsave(doc: *mut AMdoc) -> *mut AMresult {
let doc = to_doc!(doc);
to_result(Ok(doc.save()))
}
/// \memberof AMdoc
/// \brief Saves the changes to a document since its last save into a compact
/// form.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \return A pointer to an `AMresult` struct containing an array of bytes as
/// an `AMbyteSpan` struct.
/// \pre \p doc must be a valid address.
/// \warning To avoid a memory leak, the returned `AMresult` struct must be
/// deallocated with `AMfree()`.
/// \internal
///
/// # Safety
/// doc must be a pointer to a valid AMdoc
#[no_mangle]
pub unsafe extern "C" fn AMsaveIncremental(doc: *mut AMdoc) -> *mut AMresult {
let doc = to_doc!(doc);
to_result(Ok(doc.save_incremental()))
}
/// \memberof AMdoc
/// \brief Puts a sequence of bytes as the actor ID value of a document.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] value A pointer to a contiguous sequence of bytes.
/// \param[in] count The number of bytes to copy from \p value.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc must be a valid address.
/// \pre \p value must be a valid address.
/// \pre `0 <=` \p count `<=` length of \p value.
/// \warning To avoid a memory leak, the returned `AMresult` struct must be
/// deallocated with `AMfree()`.
/// \internal
///
/// # Safety
/// doc must be a pointer to a valid AMdoc
/// value must be a byte array of length `>= count`
#[no_mangle]
pub unsafe extern "C" fn AMsetActor(
doc: *mut AMdoc,
value: *const u8,
count: usize,
) -> *mut AMresult {
let doc = to_doc!(doc);
let slice = std::slice::from_raw_parts(value, count);
doc.set_actor(am::ActorId::from(slice));
to_result(Ok(()))
}
/// \memberof AMdoc
/// \brief Puts a hexadecimal string as the actor ID value of a document.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] hex_str A string of hexadecimal characters.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc must be a valid address.
/// \pre \p hex_str must be a valid address.
/// \warning To avoid a memory leak, the returned `AMresult` struct must be
/// deallocated with `AMfree()`.
/// \internal
///
/// # Safety
/// doc must be a pointer to a valid AMdoc
/// hex_str must be a null-terminated array of `c_char`
#[no_mangle]
pub unsafe extern "C" fn AMsetActorHex(doc: *mut AMdoc, hex_str: *const c_char) -> *mut AMresult {
let doc = to_doc!(doc);
let slice = std::slice::from_raw_parts(hex_str as *const u8, libc::strlen(hex_str));
to_result(match hex::decode(slice) {
Ok(vec) => {
doc.set_actor(vec.into());
Ok(())
}
Err(error) => Err(am::AutomergeError::HexDecode(error)),
})
}

460
automerge-c/src/doc/list.rs Normal file
View file

@ -0,0 +1,460 @@
use automerge as am;
use automerge::transaction::Transactable;
use std::os::raw::c_char;
use crate::doc::{to_doc, to_obj_id, to_str, AMdoc};
use crate::obj::{AMobjId, AMobjType};
use crate::result::{to_result, AMresult};
/// \memberof AMdoc
/// \brief Deletes an index in a list object.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `NULL`.
/// \param[in] index An index in the list object identified by \p obj_id.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc must be a valid address.
/// \pre `0 <=` \p index `<=` length of the list object identified by \p obj_id.
/// \warning To avoid a memory leak, the returned `AMresult` struct must be
/// deallocated with `AMfree()`.
/// \internal
///
/// # Safety
/// doc must be a pointer to a valid AMdoc
/// obj_id must be a pointer to a valid AMobjId or NULL
#[no_mangle]
pub unsafe extern "C" fn AMlistDelete(
doc: *mut AMdoc,
obj_id: *const AMobjId,
index: usize,
) -> *mut AMresult {
let doc = to_doc!(doc);
to_result(doc.delete(to_obj_id!(obj_id), index))
}
/// \memberof AMdoc
/// \brief Gets the value at an index in a list object.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `NULL`.
/// \param[in] index An index within the list object identified by \p obj_id.
/// \return A pointer to an `AMresult` struct.
/// \pre \p doc must be a valid address.
/// \pre `0 <=` \p index `<=` length of the list object identified by \p obj_id.
/// \warning To avoid a memory leak, the returned `AMresult` struct must be
/// deallocated with `AMfree()`.
/// \internal
///
/// # Safety
/// doc must be a pointer to a valid AMdoc
/// obj_id must be a pointer to a valid AMobjId or NULL
#[no_mangle]
pub unsafe extern "C" fn AMlistGet(
doc: *mut AMdoc,
obj_id: *const AMobjId,
index: usize,
) -> *mut AMresult {
let doc = to_doc!(doc);
to_result(doc.get(to_obj_id!(obj_id), index))
}
/// \memberof AMdoc
/// \brief Increments a counter at an index in a list object by the given
/// value.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `NULL`.
/// \param[in] index An index in the list object identified by \p obj_id.
/// \param[in] value A 64-bit signed integer.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc must be a valid address.
/// \pre `0 <=` \p index `<=` length of the list object identified by \p obj_id.
/// \warning To avoid a memory leak, the returned `AMresult` struct must be
/// deallocated with `AMfree()`.
/// \internal
///
/// # Safety
/// doc must be a pointer to a valid AMdoc
/// obj_id must be a pointer to a valid AMobjId or NULL
#[no_mangle]
pub unsafe extern "C" fn AMlistIncrement(
doc: *mut AMdoc,
obj_id: *const AMobjId,
index: usize,
value: i64,
) -> *mut AMresult {
let doc = to_doc!(doc);
to_result(doc.increment(to_obj_id!(obj_id), index, value))
}
/// \memberof AMdoc
/// \brief Puts a boolean as the value at an index in a list object.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `NULL`.
/// \param[in] index An index in the list object identified by \p obj_id.
/// \param[in] insert A flag to insert \p value before \p index instead of
/// writing \p value over \p index.
/// \param[in] value A boolean.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc must be a valid address.
/// \warning To avoid a memory leak, the returned `AMresult` struct must be
/// deallocated with `AMfree()`.
/// \internal
///
/// # Safety
/// doc must be a pointer to a valid AMdoc
/// obj_id must be a pointer to a valid AMobjId or NULL
#[no_mangle]
pub unsafe extern "C" fn AMlistPutBool(
doc: *mut AMdoc,
obj_id: *const AMobjId,
index: usize,
insert: bool,
value: bool,
) -> *mut AMresult {
let doc = to_doc!(doc);
let obj_id = to_obj_id!(obj_id);
let value = am::ScalarValue::Boolean(value);
to_result(if insert {
doc.insert(obj_id, index, value)
} else {
doc.put(obj_id, index, value)
})
}
/// \memberof AMdoc
/// \brief Puts a sequence of bytes as the value at an index in a list object.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `NULL`.
/// \param[in] index An index in the list object identified by \p obj_id.
/// \param[in] insert A flag to insert \p value before \p index instead of
/// writing \p value over \p index.
/// \param[in] value A pointer to an array of bytes.
/// \param[in] count The number of bytes to copy from \p value.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc must be a valid address.
/// \pre `0 <=` \p index `<=` length of the list object identified by \p obj_id.
/// \pre \p value must be a valid address.
/// \pre `0 <=` \p count `<=` length of \p value.
/// \warning To avoid a memory leak, the returned `AMresult` struct must be
/// deallocated with `AMfree()`.
/// \internal
///
/// # Safety
/// doc must be a pointer to a valid AMdoc
/// obj_id must be a pointer to a valid AMobjId or NULL
/// value must be a byte array of length `>= count`
#[no_mangle]
pub unsafe extern "C" fn AMlistPutBytes(
doc: *mut AMdoc,
obj_id: *const AMobjId,
index: usize,
insert: bool,
value: *const u8,
count: usize,
) -> *mut AMresult {
let doc = to_doc!(doc);
let obj_id = to_obj_id!(obj_id);
let mut vec = Vec::new();
vec.extend_from_slice(std::slice::from_raw_parts(value, count));
to_result(if insert {
doc.insert(obj_id, index, vec)
} else {
doc.put(obj_id, index, vec)
})
}
/// \memberof AMdoc
/// \brief Puts a CRDT counter as the value at an index in a list object.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `NULL`.
/// \param[in] index An index in the list object identified by \p obj_id.
/// \param[in] insert A flag to insert \p value before \p index instead of
/// writing \p value over \p index.
/// \param[in] value A 64-bit signed integer.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc must be a valid address.
/// \pre `0 <=` \p index `<=` length of the list object identified by \p obj_id.
/// \warning To avoid a memory leak, the returned `AMresult` struct must be
/// deallocated with `AMfree()`.
/// \internal
///
/// # Safety
/// doc must be a pointer to a valid AMdoc
/// obj_id must be a pointer to a valid AMobjId or NULL
#[no_mangle]
pub unsafe extern "C" fn AMlistPutCounter(
doc: *mut AMdoc,
obj_id: *const AMobjId,
index: usize,
insert: bool,
value: i64,
) -> *mut AMresult {
let doc = to_doc!(doc);
let obj_id = to_obj_id!(obj_id);
let value = am::ScalarValue::Counter(value.into());
to_result(if insert {
doc.insert(obj_id, index, value)
} else {
doc.put(obj_id, index, value)
})
}
/// \memberof AMdoc
/// \brief Puts a float as the value at an index in a list object.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `NULL`.
/// \param[in] index An index in the list object identified by \p obj_id.
/// \param[in] insert A flag to insert \p value before \p index instead of
/// writing \p value over \p index.
/// \param[in] value A 64-bit float.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc must be a valid address.
/// \pre `0 <=` \p index `<=` length of the list object identified by \p obj_id.
/// \warning To avoid a memory leak, the returned `AMresult` struct must be
/// deallocated with `AMfree()`.
/// \internal
///
/// # Safety
/// doc must be a pointer to a valid AMdoc
/// obj_id must be a pointer to a valid AMobjId or NULL
#[no_mangle]
pub unsafe extern "C" fn AMlistPutF64(
doc: *mut AMdoc,
obj_id: *const AMobjId,
index: usize,
insert: bool,
value: f64,
) -> *mut AMresult {
let doc = to_doc!(doc);
let obj_id = to_obj_id!(obj_id);
to_result(if insert {
doc.insert(obj_id, index, value)
} else {
doc.put(obj_id, index, value)
})
}
/// \memberof AMdoc
/// \brief Puts a signed integer as the value at an index in a list object.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `NULL`.
/// \param[in] index An index in the list object identified by \p obj_id.
/// \param[in] insert A flag to insert \p value before \p index instead of
/// writing \p value over \p index.
/// \param[in] value A 64-bit signed integer.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc must be a valid address.
/// \pre `0 <=` \p index `<=` length of the list object identified by \p obj_id.
/// \warning To avoid a memory leak, the returned `AMresult` struct must be
/// deallocated with `AMfree()`.
/// \internal
///
/// # Safety
/// doc must be a pointer to a valid AMdoc
/// obj_id must be a pointer to a valid AMobjId or NULL
#[no_mangle]
pub unsafe extern "C" fn AMlistPutInt(
doc: *mut AMdoc,
obj_id: *const AMobjId,
index: usize,
insert: bool,
value: i64,
) -> *mut AMresult {
let doc = to_doc!(doc);
let obj_id = to_obj_id!(obj_id);
to_result(if insert {
doc.insert(obj_id, index, value)
} else {
doc.put(obj_id, index, value)
})
}
/// \memberof AMdoc
/// \brief Puts null as the value at an index in a list object.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `NULL`.
/// \param[in] index An index in the list object identified by \p obj_id.
/// \param[in] insert A flag to insert \p value before \p index instead of
/// writing \p value over \p index.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc must be a valid address.
/// \pre `0 <=` \p index `<=` length of the list object identified by \p obj_id.
/// \warning To avoid a memory leak, the returned `AMresult` struct must be
/// deallocated with `AMfree()`.
/// \internal
///
/// # Safety
/// doc must be a pointer to a valid AMdoc
/// obj_id must be a pointer to a valid AMobjId or NULL
#[no_mangle]
pub unsafe extern "C" fn AMlistPutNull(
doc: *mut AMdoc,
obj_id: *const AMobjId,
index: usize,
insert: bool,
) -> *mut AMresult {
let doc = to_doc!(doc);
let obj_id = to_obj_id!(obj_id);
let value = ();
to_result(if insert {
doc.insert(obj_id, index, value)
} else {
doc.put(obj_id, index, value)
})
}
/// \memberof AMdoc
/// \brief Puts an empty object as the value at an index in a list object.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `NULL`.
/// \param[in] index An index in the list object identified by \p obj_id.
/// \param[in] insert A flag to insert \p value before \p index instead of
/// writing \p value over \p index.
/// \param[in] obj_type An `AMobjIdType` enum tag.
/// \return A pointer to an `AMresult` struct containing a pointer to an `AMobjId` struct.
/// \pre \p doc must be a valid address.
/// \pre `0 <=` \p index `<=` length of the list object identified by \p obj_id.
/// \warning To avoid a memory leak, the returned `AMresult` struct must be
/// deallocated with `AMfree()`.
/// \internal
///
/// # Safety
/// doc must be a pointer to a valid AMdoc
/// obj_id must be a pointer to a valid AMobjId or NULL
#[no_mangle]
pub unsafe extern "C" fn AMlistPutObject(
doc: *mut AMdoc,
obj_id: *const AMobjId,
index: usize,
insert: bool,
obj_type: AMobjType,
) -> *mut AMresult {
let doc = to_doc!(doc);
let obj_id = to_obj_id!(obj_id);
let value = obj_type.into();
to_result(if insert {
doc.insert_object(obj_id, index, value)
} else {
doc.put_object(&obj_id, index, value)
})
}
/// \memberof AMdoc
/// \brief Puts a UTF-8 string as the value at an index in a list object.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `NULL`.
/// \param[in] index An index in the list object identified by \p obj_id.
/// \param[in] insert A flag to insert \p value before \p index instead of
/// writing \p value over \p index.
/// \param[in] value A UTF-8 string.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc must be a valid address.
/// \pre `0 <=` \p index `<=` length of the list object identified by \p obj_id.
/// \pre \p value must be a valid address.
/// \warning To avoid a memory leak, the returned `AMresult` struct must be
/// deallocated with `AMfree()`.
/// \internal
///
/// # Safety
/// doc must be a pointer to a valid AMdoc
/// obj_id must be a pointer to a valid AMobjId or NULL
/// value must be a null-terminated array of `c_char`
#[no_mangle]
pub unsafe extern "C" fn AMlistPutStr(
doc: *mut AMdoc,
obj_id: *const AMobjId,
index: usize,
insert: bool,
value: *const c_char,
) -> *mut AMresult {
let doc = to_doc!(doc);
let obj_id = to_obj_id!(obj_id);
let value = to_str(value);
to_result(if insert {
doc.insert(obj_id, index, value)
} else {
doc.put(obj_id, index, value)
})
}
/// \memberof AMdoc
/// \brief Puts a Lamport timestamp as the value at an index in a list object.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `NULL`.
/// \param[in] index An index in the list object identified by \p obj_id.
/// \param[in] insert A flag to insert \p value before \p index instead of
/// writing \p value over \p index.
/// \param[in] value A 64-bit signed integer.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc must be a valid address.
/// \pre `0 <=` \p index `<=` length of the list object identified by \p obj_id.
/// \warning To avoid a memory leak, the returned `AMresult` struct must be
/// deallocated with `AMfree()`.
/// \internal
///
/// # Safety
/// doc must be a pointer to a valid AMdoc
/// obj_id must be a pointer to a valid AMobjId or NULL
#[no_mangle]
pub unsafe extern "C" fn AMlistPutTimestamp(
doc: *mut AMdoc,
obj_id: *const AMobjId,
index: usize,
insert: bool,
value: i64,
) -> *mut AMresult {
let doc = to_doc!(doc);
let obj_id = to_obj_id!(obj_id);
let value = am::ScalarValue::Timestamp(value);
to_result(if insert {
doc.insert(obj_id, index, value)
} else {
doc.put(obj_id, index, value)
})
}
/// \memberof AMdoc
/// \brief Puts an unsigned integer as the value at an index in a list object.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `NULL`.
/// \param[in] index An index in the list object identified by \p obj_id.
/// \param[in] insert A flag to insert \p value before \p index instead of
/// writing \p value over \p index.
/// \param[in] value A 64-bit unsigned integer.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc must be a valid address.
/// \pre `0 <=` \p index `<=` length of the list object identified by \p obj_id.
/// \warning To avoid a memory leak, the returned `AMresult` struct must be
/// deallocated with `AMfree()`.
/// \internal
///
/// # Safety
/// doc must be a pointer to a valid AMdoc
/// obj_id must be a pointer to a valid AMobjId or NULL
#[no_mangle]
pub unsafe extern "C" fn AMlistPutUint(
doc: *mut AMdoc,
obj_id: *const AMobjId,
index: usize,
insert: bool,
value: u64,
) -> *mut AMresult {
let doc = to_doc!(doc);
let obj_id = to_obj_id!(obj_id);
to_result(if insert {
doc.insert(obj_id, index, value)
} else {
doc.put(obj_id, index, value)
})
}

396
automerge-c/src/doc/map.rs Normal file
View file

@ -0,0 +1,396 @@
use automerge as am;
use automerge::transaction::Transactable;
use std::os::raw::c_char;
use crate::doc::utils::to_str;
use crate::doc::{to_doc, to_obj_id, AMdoc};
use crate::obj::{AMobjId, AMobjType};
use crate::result::{to_result, AMresult};
/// \memberof AMdoc
/// \brief Deletes a key in a map object.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `NULL`.
/// \param[in] key A UTF-8 string key for the map object identified by \p obj_id.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc must be a valid address.
/// \pre \p key must be a valid address.
/// \warning To avoid a memory leak, the returned `AMresult` struct must be
/// deallocated with `AMfree()`.
/// \internal
///
/// # Safety
/// doc must be a pointer to a valid AMdoc
/// obj_id must be a pointer to a valid AMobjId or NULL
/// key must be a c string of the map key to be used
#[no_mangle]
pub unsafe extern "C" fn AMmapDelete(
doc: *mut AMdoc,
obj_id: *const AMobjId,
key: *const c_char,
) -> *mut AMresult {
let doc = to_doc!(doc);
to_result(doc.delete(to_obj_id!(obj_id), to_str(key)))
}
/// \memberof AMdoc
/// \brief Gets the value for a key in a map object.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `NULL`.
/// \param[in] key A UTF-8 string key for the map object identified by \p obj_id.
/// \return A pointer to an `AMresult` struct.
/// \pre \p doc must be a valid address.
/// \pre \p key must be a valid address.
/// \warning To avoid a memory leak, the returned `AMresult` struct must be
/// deallocated with `AMfree()`.
/// \internal
///
/// # Safety
/// doc must be a pointer to a valid AMdoc
/// obj_id must be a pointer to a valid AMobjId or NULL
/// key must be a c string of the map key to be used
#[no_mangle]
pub unsafe extern "C" fn AMmapGet(
doc: *mut AMdoc,
obj_id: *const AMobjId,
key: *const c_char,
) -> *mut AMresult {
let doc = to_doc!(doc);
to_result(doc.get(to_obj_id!(obj_id), to_str(key)))
}
/// \memberof AMdoc
/// \brief Increments a counter for a key in a map object by the given value.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `NULL`.
/// \param[in] key A UTF-8 string key for the map object identified by \p obj_id.
/// \param[in] value A 64-bit signed integer.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc must be a valid address.
/// \pre \p key must be a valid address.
/// \warning To avoid a memory leak, the returned `AMresult` struct must be
/// deallocated with `AMfree()`.
/// \internal
///
/// # Safety
/// doc must be a pointer to a valid AMdoc
/// obj_id must be a pointer to a valid AMobjId or NULL
/// key must be a c string of the map key to be used
#[no_mangle]
pub unsafe extern "C" fn AMmapIncrement(
doc: *mut AMdoc,
obj_id: *const AMobjId,
key: *const c_char,
value: i64,
) -> *mut AMresult {
let doc = to_doc!(doc);
to_result(doc.increment(to_obj_id!(obj_id), to_str(key), value))
}
/// \memberof AMdoc
/// \brief Puts a boolean as the value of a key in a map object.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `NULL`.
/// \param[in] key A UTF-8 string key for the map object identified by \p obj_id.
/// \param[in] value A boolean.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc must be a valid address.
/// \pre \p key must be a valid address.
/// \warning To avoid a memory leak, the returned `AMresult` struct must be
/// deallocated with `AMfree()`.
/// \internal
///
/// # Safety
/// doc must be a pointer to a valid AMdoc
/// obj_id must be a pointer to a valid AMobjId or NULL
/// key must be a c string of the map key to be used
#[no_mangle]
pub unsafe extern "C" fn AMmapPutBool(
doc: *mut AMdoc,
obj_id: *const AMobjId,
key: *const c_char,
value: bool,
) -> *mut AMresult {
let doc = to_doc!(doc);
to_result(doc.put(to_obj_id!(obj_id), to_str(key), value))
}
/// \memberof AMdoc
/// \brief Puts a sequence of bytes as the value of a key in a map object.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `NULL`.
/// \param[in] key A UTF-8 string key for the map object identified by \p obj_id.
/// \param[in] value A pointer to an array of bytes.
/// \param[in] count The number of bytes to copy from \p value.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc must be a valid address.
/// \pre \p key must be a valid address.
/// \pre \p value must be a valid address.
/// \pre `0 <=` \p count `<=` length of \p value.
/// \warning To avoid a memory leak, the returned `AMresult` struct must be
/// deallocated with `AMfree()`.
/// \internal
///
/// # Safety
/// doc must be a pointer to a valid AMdoc
/// obj_id must be a pointer to a valid AMobjId or NULL
/// key must be a c string of the map key to be used
/// value must be a byte array of length `>= count`
#[no_mangle]
pub unsafe extern "C" fn AMmapPutBytes(
doc: *mut AMdoc,
obj_id: *const AMobjId,
key: *const c_char,
value: *const u8,
count: usize,
) -> *mut AMresult {
let doc = to_doc!(doc);
let mut vec = Vec::new();
vec.extend_from_slice(std::slice::from_raw_parts(value, count));
to_result(doc.put(to_obj_id!(obj_id), to_str(key), vec))
}
/// \memberof AMdoc
/// \brief Puts a CRDT counter as the value of a key in a map object.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `NULL`.
/// \param[in] key A UTF-8 string key for the map object identified by \p obj_id.
/// \param[in] value A 64-bit signed integer.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc must be a valid address.
/// \pre \p key must be a valid address.
/// \warning To avoid a memory leak, the returned `AMresult` struct must be
/// deallocated with `AMfree()`.
/// \internal
///
/// # Safety
/// doc must be a pointer to a valid AMdoc
/// obj_id must be a pointer to a valid AMobjId or NULL
/// key must be a c string of the map key to be used
#[no_mangle]
pub unsafe extern "C" fn AMmapPutCounter(
doc: *mut AMdoc,
obj_id: *const AMobjId,
key: *const c_char,
value: i64,
) -> *mut AMresult {
let doc = to_doc!(doc);
to_result(doc.put(
to_obj_id!(obj_id),
to_str(key),
am::ScalarValue::Counter(value.into()),
))
}
/// \memberof AMdoc
/// \brief Puts null as the value of a key in a map object.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `NULL`.
/// \param[in] key A UTF-8 string key for the map object identified by \p obj_id.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc must be a valid address.
/// \pre \p key must be a valid address.
/// \warning To avoid a memory leak, the returned `AMresult` struct must be
/// deallocated with `AMfree()`.
/// \internal
///
/// # Safety
/// doc must be a pointer to a valid AMdoc
/// obj_id must be a pointer to a valid AMobjId or NULL
/// key must be a c string of the map key to be used
#[no_mangle]
pub unsafe extern "C" fn AMmapPutNull(
doc: *mut AMdoc,
obj_id: *const AMobjId,
key: *const c_char,
) -> *mut AMresult {
let doc = to_doc!(doc);
to_result(doc.put(to_obj_id!(obj_id), to_str(key), ()))
}
/// \memberof AMdoc
/// \brief Puts an empty object as the value of a key in a map object.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `NULL`.
/// \param[in] key A UTF-8 string key for the map object identified by \p obj_id.
/// \param[in] obj_type An `AMobjIdType` enum tag.
/// \return A pointer to an `AMresult` struct containing a pointer to an `AMobjId` struct.
/// \pre \p doc must be a valid address.
/// \pre \p key must be a valid address.
/// \warning To avoid a memory leak, the returned `AMresult` struct must be
/// deallocated with `AMfree()`.
/// \internal
///
/// # Safety
/// doc must be a pointer to a valid AMdoc
/// obj_id must be a pointer to a valid AMobjId or NULL
/// key must be a c string of the map key to be used
#[no_mangle]
pub unsafe extern "C" fn AMmapPutObject(
doc: *mut AMdoc,
obj_id: *const AMobjId,
key: *const c_char,
obj_type: AMobjType,
) -> *mut AMresult {
let doc = to_doc!(doc);
to_result(doc.put_object(to_obj_id!(obj_id), to_str(key), obj_type.into()))
}
/// \memberof AMdoc
/// \brief Puts a float as the value of a key in a map object.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `NULL`.
/// \param[in] key A UTF-8 string key for the map object identified by \p obj_id.
/// \param[in] value A 64-bit float.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc must be a valid address.
/// \pre \p key must be a valid address.
/// \warning To avoid a memory leak, the returned `AMresult` struct must be
/// deallocated with `AMfree()`.
/// \internal
///
/// # Safety
/// doc must be a pointer to a valid AMdoc
/// obj_id must be a pointer to a valid AMobjId or NULL
/// key must be a c string of the map key to be used
#[no_mangle]
pub unsafe extern "C" fn AMmapPutF64(
doc: *mut AMdoc,
obj_id: *const AMobjId,
key: *const c_char,
value: f64,
) -> *mut AMresult {
let doc = to_doc!(doc);
to_result(doc.put(to_obj_id!(obj_id), to_str(key), value))
}
/// \memberof AMdoc
/// \brief Puts a signed integer as the value of a key in a map object.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `NULL`.
/// \param[in] key A UTF-8 string key for the map object identified by \p obj_id.
/// \param[in] value A 64-bit signed integer.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc must be a valid address.
/// \pre \p key must be a valid address.
/// \warning To avoid a memory leak, the returned `AMresult` struct must be
/// deallocated with `AMfree()`.
/// \internal
///
/// # Safety
/// doc must be a pointer to a valid AMdoc
/// obj_id must be a pointer to a valid AMobjId or NULL
/// key must be a c string of the map key to be used
#[no_mangle]
pub unsafe extern "C" fn AMmapPutInt(
doc: *mut AMdoc,
obj_id: *const AMobjId,
key: *const c_char,
value: i64,
) -> *mut AMresult {
let doc = to_doc!(doc);
to_result(doc.put(to_obj_id!(obj_id), to_str(key), value))
}
/// \memberof AMdoc
/// \brief Puts a UTF-8 string as the value of a key in a map object.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `NULL`.
/// \param[in] key A UTF-8 string key for the map object identified by \p obj_id.
/// \param[in] value A UTF-8 string.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc must be a valid address.
/// \pre \p key must be a valid address.
/// \pre \p value must be a valid address.
/// \warning To avoid a memory leak, the returned `AMresult` struct must be
/// deallocated with `AMfree()`.
/// \internal
///
/// # Safety
/// doc must be a pointer to a valid AMdoc
/// obj_id must be a pointer to a valid AMobjId or NULL
/// key must be a c string of the map key to be used
/// value must be a null-terminated array of `c_char`
#[no_mangle]
pub unsafe extern "C" fn AMmapPutStr(
doc: *mut AMdoc,
obj_id: *const AMobjId,
key: *const c_char,
value: *const c_char,
) -> *mut AMresult {
let doc = to_doc!(doc);
to_result(doc.put(to_obj_id!(obj_id), to_str(key), to_str(value)))
}
/// \memberof AMdoc
/// \brief Puts a Lamport timestamp as the value of a key in a map object.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `NULL`.
/// \param[in] key A UTF-8 string key for the map object identified by \p obj_id.
/// \param[in] value A 64-bit signed integer.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc must be a valid address.
/// \pre \p key must be a valid address.
/// \warning To avoid a memory leak, the returned `AMresult` struct must be
/// deallocated with `AMfree()`.
/// \internal
///
/// # Safety
/// doc must be a pointer to a valid AMdoc
/// obj_id must be a pointer to a valid AMobjId or NULL
/// key must be a c string of the map key to be used
#[no_mangle]
pub unsafe extern "C" fn AMmapPutTimestamp(
doc: *mut AMdoc,
obj_id: *const AMobjId,
key: *const c_char,
value: i64,
) -> *mut AMresult {
let doc = to_doc!(doc);
to_result(doc.put(
to_obj_id!(obj_id),
to_str(key),
am::ScalarValue::Timestamp(value),
))
}
/// \memberof AMdoc
/// \brief Puts an unsigned integer as the value of a key in a map object.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `NULL`.
/// \param[in] key A UTF-8 string key for the map object identified by \p obj_id.
/// \param[in] value A 64-bit unsigned integer.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc must be a valid address.
/// \pre \p key must be a valid address.
/// \warning To avoid a memory leak, the returned `AMresult` struct must be
/// deallocated with `AMfree()`.
/// \internal
///
/// # Safety
/// doc must be a pointer to a valid AMdoc
/// obj_id must be a pointer to a valid AMobjId or NULL
/// key must be a c string of the map key to be used
#[no_mangle]
pub unsafe extern "C" fn AMmapPutUint(
doc: *mut AMdoc,
obj_id: *const AMobjId,
key: *const c_char,
value: u64,
) -> *mut AMresult {
let doc = to_doc!(doc);
to_result(doc.put(to_obj_id!(obj_id), to_str(key), value))
}

View file

@ -0,0 +1,29 @@
use std::ffi::CStr;
use std::os::raw::c_char;
macro_rules! to_doc {
($handle:expr) => {{
let handle = $handle.as_mut();
match handle {
Some(b) => b,
None => return AMresult::err("Invalid AMdoc pointer").into(),
}
}};
}
pub(crate) use to_doc;
macro_rules! to_obj_id {
($handle:expr) => {{
match $handle.as_ref() {
Some(obj_id) => obj_id,
None => &automerge::ROOT,
}
}};
}
pub(crate) use to_obj_id;
pub(crate) unsafe fn to_str(c: *const c_char) -> String {
CStr::from_ptr(c).to_string_lossy().to_string()
}

8
automerge-c/src/lib.rs Normal file
View file

@ -0,0 +1,8 @@
mod byte_span;
mod change;
mod change_hashes;
mod changes;
mod doc;
mod obj;
mod result;
mod sync;

49
automerge-c/src/obj.rs Normal file
View file

@ -0,0 +1,49 @@
use automerge as am;
use std::ops::Deref;
/// \struct AMobjId
/// \brief An object's unique identifier.
pub struct AMobjId(am::ObjId);
impl AMobjId {
pub fn new(obj_id: am::ObjId) -> Self {
Self(obj_id)
}
}
impl AsRef<am::ObjId> for AMobjId {
fn as_ref(&self) -> &am::ObjId {
&self.0
}
}
impl Deref for AMobjId {
type Target = am::ObjId;
fn deref(&self) -> &Self::Target {
&self.0
}
}
/// \ingroup enumerations
/// \enum AMobjType
/// \brief The type of an object value.
#[repr(u8)]
pub enum AMobjType {
/// A list.
List = 1,
/// A key-value map.
Map,
/// A list of Unicode graphemes.
Text,
}
impl From<AMobjType> for am::ObjType {
fn from(o: AMobjType) -> Self {
match o {
AMobjType::Map => am::ObjType::Map,
AMobjType::List => am::ObjType::List,
AMobjType::Text => am::ObjType::Text,
}
}
}

497
automerge-c/src/result.rs Normal file
View file

@ -0,0 +1,497 @@
use automerge as am;
use std::collections::BTreeMap;
use std::ffi::CString;
use std::os::raw::c_char;
use crate::byte_span::AMbyteSpan;
use crate::change::AMchange;
use crate::change_hashes::AMchangeHashes;
use crate::changes::AMchanges;
use crate::doc::AMdoc;
use crate::obj::AMobjId;
use crate::sync::{AMsyncMessage, AMsyncState};
/// \struct AMvalue
/// \brief A discriminated union of value type variants for a result.
///
/// \enum AMvalueVariant
/// \brief A value type discriminant.
///
/// \var AMvalue::tag
/// The variant discriminator of an `AMvalue` struct.
///
/// \var AMvalue::actor_id
/// An actor ID as an `AMbyteSpan` struct.
///
/// \var AMvalue::boolean
/// A boolean.
///
/// \var AMvalue::bytes
/// A sequence of bytes as an `AMbyteSpan` struct.
///
/// \var AMvalue::change_hashes
/// A sequence of change hashes as an `AMchangeHashes` struct.
///
/// \var AMvalue::changes
/// A sequence of changes as an `AMchanges` struct.
///
/// \var AMvalue::counter
/// A CRDT counter.
///
/// \var AMvalue::f64
/// A 64-bit float.
///
/// \var AMvalue::int_
/// A 64-bit signed integer.
///
/// \var AMvalue::obj_id
/// An object identifier.
///
/// \var AMvalue::str
/// A UTF-8 string.
///
/// \var AMvalue::timestamp
/// A Lamport timestamp.
///
/// \var AMvalue::uint
/// A 64-bit unsigned integer.
#[repr(C)]
pub enum AMvalue<'a> {
/// An actor ID variant.
ActorId(AMbyteSpan),
/// A boolean variant.
Boolean(bool),
/// A byte array variant.
Bytes(AMbyteSpan),
/// A change hashes variant.
ChangeHashes(AMchangeHashes),
/// A changes variant.
Changes(AMchanges),
/// A CRDT counter variant.
Counter(i64),
/// A document variant.
Doc(*mut AMdoc),
/// A 64-bit float variant.
F64(f64),
/// A 64-bit signed integer variant.
Int(i64),
/*
/// A keys variant.
Keys(_),
*/
/// A null variant.
Null,
/// An object identifier variant.
ObjId(&'a AMobjId),
/// A UTF-8 string variant.
Str(*const libc::c_char),
/// A Lamport timestamp variant.
Timestamp(i64),
/*
/// A transaction variant.
Transaction(_),
*/
/// A 64-bit unsigned integer variant.
Uint(u64),
/// A synchronization message variant.
SyncMessage(&'a AMsyncMessage),
/// A synchronization state variant.
SyncState(&'a mut AMsyncState),
/// A void variant.
Void,
}
/// \struct AMresult
/// \brief A discriminated union of result variants.
pub enum AMresult {
ActorId(am::ActorId),
ChangeHashes(Vec<am::ChangeHash>),
Changes(Vec<am::Change>, BTreeMap<usize, AMchange>),
Doc(Box<AMdoc>),
Error(CString),
ObjId(AMobjId),
Value(am::Value<'static>, Option<CString>),
SyncMessage(AMsyncMessage),
SyncState(AMsyncState),
Void,
}
impl AMresult {
pub(crate) fn err(s: &str) -> Self {
AMresult::Error(CString::new(s).unwrap())
}
}
impl From<am::AutoCommit> for AMresult {
fn from(auto_commit: am::AutoCommit) -> Self {
AMresult::Doc(Box::new(AMdoc::new(auto_commit)))
}
}
impl From<am::ChangeHash> for AMresult {
fn from(change_hash: am::ChangeHash) -> Self {
AMresult::ChangeHashes(vec![change_hash])
}
}
impl From<am::sync::State> for AMresult {
fn from(state: am::sync::State) -> Self {
AMresult::SyncState(AMsyncState::new(state))
}
}
impl From<AMresult> for *mut AMresult {
fn from(b: AMresult) -> Self {
Box::into_raw(Box::new(b))
}
}
impl From<Option<&am::Change>> for AMresult {
fn from(maybe: Option<&am::Change>) -> Self {
match maybe {
Some(change) => AMresult::Changes(vec![change.clone()], BTreeMap::new()),
None => AMresult::Void,
}
}
}
impl From<Option<am::sync::Message>> for AMresult {
fn from(maybe: Option<am::sync::Message>) -> Self {
match maybe {
Some(message) => AMresult::SyncMessage(AMsyncMessage::new(message)),
None => AMresult::Void,
}
}
}
impl From<Result<(), am::AutomergeError>> for AMresult {
fn from(maybe: Result<(), am::AutomergeError>) -> Self {
match maybe {
Ok(()) => AMresult::Void,
Err(e) => AMresult::err(&e.to_string()),
}
}
}
impl From<Result<am::ActorId, am::AutomergeError>> for AMresult {
fn from(maybe: Result<am::ActorId, am::AutomergeError>) -> Self {
match maybe {
Ok(actor_id) => AMresult::ActorId(actor_id),
Err(e) => AMresult::err(&e.to_string()),
}
}
}
impl From<Result<am::AutoCommit, am::AutomergeError>> for AMresult {
fn from(maybe: Result<am::AutoCommit, am::AutomergeError>) -> Self {
match maybe {
Ok(auto_commit) => AMresult::Doc(Box::new(AMdoc::new(auto_commit))),
Err(e) => AMresult::err(&e.to_string()),
}
}
}
impl From<Result<am::Change, am::DecodingError>> for AMresult {
fn from(maybe: Result<am::Change, am::DecodingError>) -> Self {
match maybe {
Ok(change) => AMresult::Changes(vec![change], BTreeMap::new()),
Err(e) => AMresult::err(&e.to_string()),
}
}
}
impl From<Result<am::ObjId, am::AutomergeError>> for AMresult {
fn from(maybe: Result<am::ObjId, am::AutomergeError>) -> Self {
match maybe {
Ok(obj_id) => AMresult::ObjId(AMobjId::new(obj_id)),
Err(e) => AMresult::err(&e.to_string()),
}
}
}
impl From<Result<am::sync::Message, am::DecodingError>> for AMresult {
fn from(maybe: Result<am::sync::Message, am::DecodingError>) -> Self {
match maybe {
Ok(message) => AMresult::SyncMessage(AMsyncMessage::new(message)),
Err(e) => AMresult::err(&e.to_string()),
}
}
}
impl From<Result<am::sync::State, am::DecodingError>> for AMresult {
fn from(maybe: Result<am::sync::State, am::DecodingError>) -> Self {
match maybe {
Ok(state) => AMresult::SyncState(AMsyncState::new(state)),
Err(e) => AMresult::err(&e.to_string()),
}
}
}
impl From<Result<am::Value<'static>, am::AutomergeError>> for AMresult {
fn from(maybe: Result<am::Value<'static>, am::AutomergeError>) -> Self {
match maybe {
Ok(value) => AMresult::Value(value, None),
Err(e) => AMresult::err(&e.to_string()),
}
}
}
impl From<Result<Option<(am::Value<'static>, am::ObjId)>, am::AutomergeError>> for AMresult {
fn from(maybe: Result<Option<(am::Value<'static>, am::ObjId)>, am::AutomergeError>) -> Self {
match maybe {
// \todo Ensure that it's alright to ignore the `am::ObjId` value.
Ok(Some((value, _))) => AMresult::Value(value, None),
Ok(None) => AMresult::Void,
Err(e) => AMresult::err(&e.to_string()),
}
}
}
impl From<Result<usize, am::AutomergeError>> for AMresult {
fn from(maybe: Result<usize, am::AutomergeError>) -> Self {
match maybe {
Ok(size) => AMresult::Value(am::Value::uint(size as u64), None),
Err(e) => AMresult::err(&e.to_string()),
}
}
}
impl From<Result<Vec<am::Change>, am::AutomergeError>> for AMresult {
fn from(maybe: Result<Vec<am::Change>, am::AutomergeError>) -> Self {
match maybe {
Ok(changes) => AMresult::Changes(changes, BTreeMap::new()),
Err(e) => AMresult::err(&e.to_string()),
}
}
}
impl From<Result<Vec<&am::Change>, am::AutomergeError>> for AMresult {
fn from(maybe: Result<Vec<&am::Change>, am::AutomergeError>) -> Self {
match maybe {
Ok(changes) => {
let changes: Vec<am::Change> =
changes.iter().map(|&change| change.clone()).collect();
AMresult::Changes(changes, BTreeMap::new())
}
Err(e) => AMresult::err(&e.to_string()),
}
}
}
impl From<Result<Vec<am::ChangeHash>, am::AutomergeError>> for AMresult {
fn from(maybe: Result<Vec<am::ChangeHash>, am::AutomergeError>) -> Self {
match maybe {
Ok(change_hashes) => AMresult::ChangeHashes(change_hashes),
Err(e) => AMresult::err(&e.to_string()),
}
}
}
impl From<Result<Vec<u8>, am::AutomergeError>> for AMresult {
fn from(maybe: Result<Vec<u8>, am::AutomergeError>) -> Self {
match maybe {
Ok(bytes) => AMresult::Value(am::Value::bytes(bytes), None),
Err(e) => AMresult::err(&e.to_string()),
}
}
}
impl From<Vec<&am::Change>> for AMresult {
fn from(changes: Vec<&am::Change>) -> Self {
let changes: Vec<am::Change> = changes.iter().map(|&change| change.clone()).collect();
AMresult::Changes(changes, BTreeMap::new())
}
}
impl From<Vec<am::ChangeHash>> for AMresult {
fn from(change_hashes: Vec<am::ChangeHash>) -> Self {
AMresult::ChangeHashes(change_hashes)
}
}
impl From<Vec<u8>> for AMresult {
fn from(bytes: Vec<u8>) -> Self {
AMresult::Value(am::Value::bytes(bytes), None)
}
}
pub fn to_result<R: Into<AMresult>>(r: R) -> *mut AMresult {
(r.into()).into()
}
/// \ingroup enumerations
/// \enum AMstatus
/// \brief The status of an API call.
#[derive(Debug)]
#[repr(u8)]
pub enum AMstatus {
/// Success.
/// \note This tag is unalphabetized so that `0` indicates success.
Ok,
/// Failure due to an error.
Error,
/// Failure due to an invalid result.
InvalidResult,
}
/// \memberof AMresult
/// \brief Gets a result's error message string.
///
/// \param[in] result A pointer to an `AMresult` struct.
/// \return A UTF-8 string value or `NULL`.
/// \pre \p result must be a valid address.
/// \internal
///
/// # Safety
/// result must be a pointer to a valid AMresult
#[no_mangle]
pub unsafe extern "C" fn AMerrorMessage(result: *mut AMresult) -> *const c_char {
match result.as_mut() {
Some(AMresult::Error(s)) => s.as_ptr(),
_ => std::ptr::null::<c_char>(),
}
}
/// \memberof AMresult
/// \brief Deallocates the storage for a result.
///
/// \param[in] result A pointer to an `AMresult` struct.
/// \pre \p result must be a valid address.
/// \internal
///
/// # Safety
/// result must be a pointer to a valid AMresult
#[no_mangle]
pub unsafe extern "C" fn AMfree(result: *mut AMresult) {
if !result.is_null() {
let result: AMresult = *Box::from_raw(result);
drop(result)
}
}
/// \memberof AMresult
/// \brief Gets the size of a result's value.
///
/// \param[in] result A pointer to an `AMresult` struct.
/// \return The count of values in \p result.
/// \pre \p result must be a valid address.
/// \internal
///
/// # Safety
/// result must be a pointer to a valid AMresult
#[no_mangle]
pub unsafe extern "C" fn AMresultSize(result: *mut AMresult) -> usize {
if let Some(result) = result.as_mut() {
match result {
AMresult::Error(_) | AMresult::Void => 0,
AMresult::ActorId(_)
| AMresult::Doc(_)
| AMresult::ObjId(_)
| AMresult::SyncMessage(_)
| AMresult::SyncState(_)
| AMresult::Value(_, _) => 1,
AMresult::ChangeHashes(change_hashes) => change_hashes.len(),
AMresult::Changes(changes, _) => changes.len(),
}
} else {
0
}
}
/// \memberof AMresult
/// \brief Gets the status code of a result.
///
/// \param[in] result A pointer to an `AMresult` struct.
/// \return An `AMstatus` enum tag.
/// \pre \p result must be a valid address.
/// \internal
///
/// # Safety
/// result must be a pointer to a valid AMresult
#[no_mangle]
pub unsafe extern "C" fn AMresultStatus(result: *mut AMresult) -> AMstatus {
match result.as_mut() {
Some(AMresult::Error(_)) => AMstatus::Error,
None => AMstatus::InvalidResult,
_ => AMstatus::Ok,
}
}
/// \memberof AMresult
/// \brief Gets a result's value.
///
/// \param[in] result A pointer to an `AMresult` struct.
/// \return An `AMvalue` struct.
/// \pre \p result must be a valid address.
/// \internal
///
/// # Safety
/// result must be a pointer to a valid AMresult
#[no_mangle]
pub unsafe extern "C" fn AMresultValue<'a>(result: *mut AMresult) -> AMvalue<'a> {
let mut content = AMvalue::Void;
if let Some(result) = result.as_mut() {
match result {
AMresult::ActorId(actor_id) => {
content = AMvalue::ActorId(actor_id.into());
}
AMresult::ChangeHashes(change_hashes) => {
content = AMvalue::ChangeHashes(AMchangeHashes::new(change_hashes));
}
AMresult::Changes(changes, storage) => {
content = AMvalue::Changes(AMchanges::new(changes, storage));
}
AMresult::Doc(doc) => content = AMvalue::Doc(&mut **doc),
AMresult::Error(_) => {}
AMresult::ObjId(obj_id) => {
content = AMvalue::ObjId(obj_id);
}
AMresult::Value(value, hosted_str) => {
match value {
am::Value::Scalar(scalar) => match scalar.as_ref() {
am::ScalarValue::Boolean(flag) => {
content = AMvalue::Boolean(*flag);
}
am::ScalarValue::Bytes(bytes) => {
content = AMvalue::Bytes(bytes.as_slice().into());
}
am::ScalarValue::Counter(counter) => {
content = AMvalue::Counter(counter.into());
}
am::ScalarValue::F64(float) => {
content = AMvalue::F64(*float);
}
am::ScalarValue::Int(int) => {
content = AMvalue::Int(*int);
}
am::ScalarValue::Null => {
content = AMvalue::Null;
}
am::ScalarValue::Str(smol_str) => {
*hosted_str = CString::new(smol_str.to_string()).ok();
if let Some(c_str) = hosted_str {
content = AMvalue::Str(c_str.as_ptr());
}
}
am::ScalarValue::Timestamp(timestamp) => {
content = AMvalue::Timestamp(*timestamp);
}
am::ScalarValue::Uint(uint) => {
content = AMvalue::Uint(*uint);
}
},
// \todo Confirm that an object variant should be ignored
// when there's no object ID variant.
am::Value::Object(_) => {}
}
}
AMresult::SyncMessage(sync_message) => {
content = AMvalue::SyncMessage(sync_message);
}
AMresult::SyncState(sync_state) => {
content = AMvalue::SyncState(sync_state);
}
AMresult::Void => {}
}
};
content
}

View file

@ -1,7 +1,7 @@
mod have;
mod haves;
mod message;
mod state;
pub(crate) use have::AMsyncHave;
pub(crate) use message::{to_sync_message, AMsyncMessage};
pub(crate) use state::AMsyncState;

View file

@ -0,0 +1,40 @@
use automerge as am;
use crate::change_hashes::AMchangeHashes;
/// \struct AMsyncHave
/// \brief A summary of the changes that the sender of a synchronization
/// message already has.
#[derive(Clone)]
pub struct AMsyncHave(*const am::sync::Have);
impl AMsyncHave {
pub fn new(have: &am::sync::Have) -> Self {
Self(have)
}
}
impl AsRef<am::sync::Have> for AMsyncHave {
fn as_ref(&self) -> &am::sync::Have {
unsafe { &*self.0 }
}
}
/// \memberof AMsyncHave
/// \brief Gets the heads of the sender.
///
/// \param[in] sync_have A pointer to an `AMsyncHave` struct.
/// \return An `AMchangeHashes` struct.
/// \pre \p sync_have must be a valid address.
/// \internal
///
/// # Safety
/// sync_have must be a pointer to a valid AMsyncHave
#[no_mangle]
pub unsafe extern "C" fn AMsyncHaveLastSync(sync_have: *const AMsyncHave) -> AMchangeHashes {
if let Some(sync_have) = sync_have.as_ref() {
AMchangeHashes::new(&sync_have.as_ref().last_sync)
} else {
AMchangeHashes::default()
}
}

View file

@ -0,0 +1,320 @@
use automerge as am;
use std::collections::BTreeMap;
use std::ffi::c_void;
use std::mem::size_of;
use crate::sync::have::AMsyncHave;
#[repr(C)]
struct Detail {
len: usize,
offset: isize,
ptr: *const c_void,
storage: *mut c_void,
}
/// \note cbindgen won't propagate the value of a `std::mem::size_of<T>()` call
/// (https://github.com/eqrion/cbindgen/issues/252) but it will
/// propagate the name of a constant initialized from it so if the
/// constant's name is a symbolic representation of the value it can be
/// converted into a number by post-processing the header it generated.
pub const USIZE_USIZE_USIZE_USIZE_: usize = size_of::<Detail>();
impl Detail {
fn new(
haves: &[am::sync::Have],
offset: isize,
storage: &mut BTreeMap<usize, AMsyncHave>,
) -> Self {
let storage: *mut BTreeMap<usize, AMsyncHave> = storage;
Self {
len: haves.len(),
offset,
ptr: haves.as_ptr() as *const c_void,
storage: storage as *mut c_void,
}
}
pub fn advance(&mut self, n: isize) {
if n != 0 && !self.is_stopped() {
let n = if self.offset < 0 { -n } else { n };
let len = self.len as isize;
self.offset = std::cmp::max(-(len + 1), std::cmp::min(self.offset + n, len));
};
}
pub fn get_index(&self) -> usize {
(self.offset
+ if self.offset < 0 {
self.len as isize
} else {
0
}) as usize
}
pub fn next(&mut self, n: isize) -> Option<*const AMsyncHave> {
if self.is_stopped() {
return None;
}
let slice: &[am::sync::Have] =
unsafe { std::slice::from_raw_parts(self.ptr as *const am::sync::Have, self.len) };
let storage = unsafe { &mut *(self.storage as *mut BTreeMap<usize, AMsyncHave>) };
let index = self.get_index();
let value = match storage.get_mut(&index) {
Some(value) => value,
None => {
storage.insert(index, AMsyncHave::new(&slice[index]));
storage.get_mut(&index).unwrap()
}
};
self.advance(n);
Some(value)
}
pub fn is_stopped(&self) -> bool {
let len = self.len as isize;
self.offset < -len || self.offset == len
}
pub fn prev(&mut self, n: isize) -> Option<*const AMsyncHave> {
self.advance(n);
if self.is_stopped() {
return None;
}
let slice: &[am::sync::Have] =
unsafe { std::slice::from_raw_parts(self.ptr as *const am::sync::Have, self.len) };
let storage = unsafe { &mut *(self.storage as *mut BTreeMap<usize, AMsyncHave>) };
let index = self.get_index();
Some(match storage.get_mut(&index) {
Some(value) => value,
None => {
storage.insert(index, AMsyncHave::new(&slice[index]));
storage.get_mut(&index).unwrap()
}
})
}
pub fn reversed(&self) -> Self {
Self {
len: self.len,
offset: -(self.offset + 1),
ptr: self.ptr,
storage: self.storage,
}
}
}
impl From<Detail> for [u8; USIZE_USIZE_USIZE_USIZE_] {
fn from(detail: Detail) -> Self {
unsafe {
std::slice::from_raw_parts(
(&detail as *const Detail) as *const u8,
USIZE_USIZE_USIZE_USIZE_,
)
.try_into()
.unwrap()
}
}
}
/// \struct AMsyncHaves
/// \brief A random-access iterator over a sequence of synchronization haves.
#[repr(C)]
pub struct AMsyncHaves {
/// Reserved.
detail: [u8; USIZE_USIZE_USIZE_USIZE_],
}
impl AMsyncHaves {
pub fn new(haves: &[am::sync::Have], storage: &mut BTreeMap<usize, AMsyncHave>) -> Self {
Self {
detail: Detail::new(haves, 0, storage).into(),
}
}
pub fn advance(&mut self, n: isize) {
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
detail.advance(n);
}
pub fn len(&self) -> usize {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
detail.len
}
pub fn next(&mut self, n: isize) -> Option<*const AMsyncHave> {
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
detail.next(n)
}
pub fn prev(&mut self, n: isize) -> Option<*const AMsyncHave> {
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
detail.prev(n)
}
pub fn reversed(&self) -> Self {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
Self {
detail: detail.reversed().into(),
}
}
}
impl AsRef<[am::sync::Have]> for AMsyncHaves {
fn as_ref(&self) -> &[am::sync::Have] {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
unsafe { std::slice::from_raw_parts(detail.ptr as *const am::sync::Have, detail.len) }
}
}
impl Default for AMsyncHaves {
fn default() -> Self {
Self {
detail: [0; USIZE_USIZE_USIZE_USIZE_],
}
}
}
/// \memberof AMsyncHaves
/// \brief Advances an iterator over a sequence of synchronization haves by at
/// most \p |n| positions where the sign of \p n is relative to the
/// iterator's direction.
///
/// \param[in] sync_haves A pointer to an `AMsyncHaves` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \pre \p sync_haves must be a valid address.
/// \internal
///
/// #Safety
/// sync_haves must be a pointer to a valid AMsyncHaves
#[no_mangle]
pub unsafe extern "C" fn AMsyncHavesAdvance(sync_haves: *mut AMsyncHaves, n: isize) {
if let Some(sync_haves) = sync_haves.as_mut() {
sync_haves.advance(n);
};
}
/// \memberof AMsyncHaves
/// \brief Tests the equality of two sequences of synchronization haves
/// underlying a pair of iterators.
///
/// \param[in] sync_haves1 A pointer to an `AMsyncHaves` struct.
/// \param[in] sync_haves2 A pointer to an `AMsyncHaves` struct.
/// \return `true` if \p sync_haves1 `==` \p sync_haves2 and `false` otherwise.
/// \pre \p sync_haves1 must be a valid address.
/// \pre \p sync_haves2 must be a valid address.
/// \internal
///
/// #Safety
/// sync_haves1 must be a pointer to a valid AMsyncHaves
/// sync_haves2 must be a pointer to a valid AMsyncHaves
#[no_mangle]
pub unsafe extern "C" fn AMsyncHavesEqual(
sync_haves1: *const AMsyncHaves,
sync_haves2: *const AMsyncHaves,
) -> bool {
match (sync_haves1.as_ref(), sync_haves2.as_ref()) {
(Some(sync_haves1), Some(sync_haves2)) => sync_haves1.as_ref() == sync_haves2.as_ref(),
(None, Some(_)) | (Some(_), None) | (None, None) => false,
}
}
/// \memberof AMsyncHaves
/// \brief Gets the synchronization have at the current position of an iterator
/// over a sequence of synchronization haves and then advances it by at
/// most \p |n| positions where the sign of \p n is relative to the
/// iterator's direction.
///
/// \param[in] sync_haves A pointer to an `AMsyncHaves` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \return A pointer to an `AMsyncHave` struct that's `NULL` when
/// \p sync_haves was previously advanced past its forward/reverse
/// limit.
/// \pre \p sync_haves must be a valid address.
/// \internal
///
/// #Safety
/// sync_haves must be a pointer to a valid AMsyncHaves
#[no_mangle]
pub unsafe extern "C" fn AMsyncHavesNext(
sync_haves: *mut AMsyncHaves,
n: isize,
) -> *const AMsyncHave {
if let Some(sync_haves) = sync_haves.as_mut() {
if let Some(sync_have) = sync_haves.next(n) {
return sync_have;
}
}
std::ptr::null()
}
/// \memberof AMsyncHaves
/// \brief Advances an iterator over a sequence of synchronization haves by at
/// most \p |n| positions where the sign of \p n is relative to the
/// iterator's direction and then gets the synchronization have at its
/// new position.
///
/// \param[in] sync_haves A pointer to an `AMsyncHaves` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \return A pointer to an `AMsyncHave` struct that's `NULL` when
/// \p sync_haves is presently advanced past its forward/reverse limit.
/// \pre \p sync_haves must be a valid address.
/// \internal
///
/// #Safety
/// sync_haves must be a pointer to a valid AMsyncHaves
#[no_mangle]
pub unsafe extern "C" fn AMsyncHavesPrev(
sync_haves: *mut AMsyncHaves,
n: isize,
) -> *const AMsyncHave {
if let Some(sync_haves) = sync_haves.as_mut() {
if let Some(sync_have) = sync_haves.prev(n) {
return sync_have;
}
}
std::ptr::null()
}
/// \memberof AMsyncHaves
/// \brief Gets the size of the sequence of synchronization haves underlying an
/// iterator.
///
/// \param[in] sync_haves A pointer to an `AMsyncHaves` struct.
/// \return The count of values in \p sync_haves.
/// \pre \p sync_haves must be a valid address.
/// \internal
///
/// #Safety
/// sync_haves must be a pointer to a valid AMsyncHaves
#[no_mangle]
pub unsafe extern "C" fn AMsyncHavesSize(sync_haves: *const AMsyncHaves) -> usize {
if let Some(sync_haves) = sync_haves.as_ref() {
sync_haves.len()
} else {
0
}
}
/// \memberof AMsyncHaves
/// \brief Creates an iterator over the same sequence of synchronization haves
/// as the given one but with the opposite position and direction.
///
/// \param[in] sync_haves A pointer to an `AMsyncHaves` struct.
/// \return An `AMsyncHaves` struct
/// \pre \p sync_haves must be a valid address.
/// \internal
///
/// #Safety
/// sync_haves must be a pointer to a valid AMsyncHaves
#[no_mangle]
pub unsafe extern "C" fn AMsyncHavesReversed(sync_haves: *const AMsyncHaves) -> AMsyncHaves {
if let Some(sync_haves) = sync_haves.as_ref() {
sync_haves.reversed()
} else {
AMsyncHaves::default()
}
}

View file

@ -0,0 +1,170 @@
use automerge as am;
use std::cell::RefCell;
use std::collections::BTreeMap;
use crate::change::AMchange;
use crate::change_hashes::AMchangeHashes;
use crate::changes::AMchanges;
use crate::result::{to_result, AMresult};
use crate::sync::have::AMsyncHave;
use crate::sync::haves::AMsyncHaves;
macro_rules! to_sync_message {
($handle:expr) => {{
let handle = $handle.as_ref();
match handle {
Some(b) => b,
None => return AMresult::err("Invalid AMsyncMessage pointer").into(),
}
}};
}
pub(crate) use to_sync_message;
/// \struct AMsyncMessage
/// \brief A synchronization message for a peer.
pub struct AMsyncMessage {
body: am::sync::Message,
changes_storage: RefCell<BTreeMap<usize, AMchange>>,
haves_storage: RefCell<BTreeMap<usize, AMsyncHave>>,
}
impl AMsyncMessage {
pub fn new(message: am::sync::Message) -> Self {
Self {
body: message,
changes_storage: RefCell::new(BTreeMap::new()),
haves_storage: RefCell::new(BTreeMap::new()),
}
}
}
impl AsRef<am::sync::Message> for AMsyncMessage {
fn as_ref(&self) -> &am::sync::Message {
&self.body
}
}
/// \memberof AMsyncMessage
/// \brief Gets the changes for the recipient to apply.
///
/// \param[in] sync_message A pointer to an `AMsyncMessage` struct.
/// \return An `AMchanges` struct.
/// \pre \p sync_message must be a valid address.
/// \internal
///
/// # Safety
/// sync_message must be a pointer to a valid AMsyncMessage
#[no_mangle]
pub unsafe extern "C" fn AMsyncMessageChanges(sync_message: *const AMsyncMessage) -> AMchanges {
if let Some(sync_message) = sync_message.as_ref() {
AMchanges::new(
&sync_message.body.changes,
&mut sync_message.changes_storage.borrow_mut(),
)
} else {
AMchanges::default()
}
}
/// \memberof AMsyncMessage
/// \brief Decodes a sequence of bytes into a synchronization message.
///
/// \param[in] src A pointer to an array of bytes.
/// \param[in] count The number of bytes in \p src to decode.
/// \return A pointer to an `AMresult` struct containing an `AMsyncMessage`
/// struct.
/// \pre \p src must be a valid address.
/// \pre `0 <=` \p count `<=` length of \p src.
/// \warning To avoid a memory leak, the returned `AMresult` struct must be
/// deallocated with `AMfree()`.
/// \internal
///
/// # Safety
/// src must be a byte array of length `>= count`
#[no_mangle]
pub unsafe extern "C" fn AMsyncMessageDecode(src: *const u8, count: usize) -> *mut AMresult {
let mut data = Vec::new();
data.extend_from_slice(std::slice::from_raw_parts(src, count));
to_result(am::sync::Message::decode(&data))
}
/// \memberof AMsyncMessage
/// \brief Encodes a synchronization message as a sequence of bytes.
///
/// \param[in] sync_message A pointer to an `AMsyncMessage` struct.
/// \return A pointer to an `AMresult` struct containing an array of bytes as
/// an `AMbyteSpan` struct.
/// \pre \p sync_message must be a valid address.
/// \warning To avoid a memory leak, the returned `AMresult` struct must be
/// deallocated with `AMfree()`.
/// \internal
///
/// # Safety
/// sync_message must be a pointer to a valid AMsyncMessage
#[no_mangle]
pub unsafe extern "C" fn AMsyncMessageEncode(sync_message: *const AMsyncMessage) -> *mut AMresult {
let sync_message = to_sync_message!(sync_message);
to_result(sync_message.as_ref().clone().encode())
}
/// \memberof AMsyncMessage
/// \brief Gets a summary of the changes that the sender already has.
///
/// \param[in] sync_message A pointer to an `AMsyncMessage` struct.
/// \return An `AMhaves` struct.
/// \pre \p sync_message must be a valid address.
/// \internal
///
/// # Safety
/// sync_message must be a pointer to a valid AMsyncMessage
#[no_mangle]
pub unsafe extern "C" fn AMsyncMessageHaves(sync_message: *const AMsyncMessage) -> AMsyncHaves {
if let Some(sync_message) = sync_message.as_ref() {
AMsyncHaves::new(
&sync_message.as_ref().have,
&mut sync_message.haves_storage.borrow_mut(),
)
} else {
AMsyncHaves::default()
}
}
/// \memberof AMsyncMessage
/// \brief Gets the heads of the sender.
///
/// \param[in] sync_message A pointer to an `AMsyncMessage` struct.
/// \return An `AMchangeHashes` struct.
/// \pre \p sync_message must be a valid address.
/// \internal
///
/// # Safety
/// sync_message must be a pointer to a valid AMsyncMessage
#[no_mangle]
pub unsafe extern "C" fn AMsyncMessageHeads(sync_message: *const AMsyncMessage) -> AMchangeHashes {
if let Some(sync_message) = sync_message.as_ref() {
AMchangeHashes::new(&sync_message.as_ref().heads)
} else {
AMchangeHashes::default()
}
}
/// \memberof AMsyncMessage
/// \brief Gets the hashes of any changes that are being explicitly requested
/// by the recipient.
///
/// \param[in] sync_message A pointer to an `AMsyncMessage` struct.
/// \return An `AMchangeHashes` struct.
/// \pre \p sync_message must be a valid address.
/// \internal
///
/// # Safety
/// sync_message must be a pointer to a valid AMsyncMessage
#[no_mangle]
pub unsafe extern "C" fn AMsyncMessageNeeds(sync_message: *const AMsyncMessage) -> AMchangeHashes {
if let Some(sync_message) = sync_message.as_ref() {
AMchangeHashes::new(&sync_message.as_ref().need)
} else {
AMchangeHashes::default()
}
}

View file

@ -2,15 +2,17 @@ use automerge as am;
use std::cell::RefCell;
use std::collections::BTreeMap;
use crate::change_hashes::AMchangeHashes;
use crate::result::{to_result, AMresult};
use crate::sync::have::AMsyncHave;
use crate::sync::haves::AMsyncHaves;
macro_rules! to_sync_state {
($handle:expr) => {{
let handle = $handle.as_ref();
match handle {
Some(b) => b,
None => return AMresult::error("Invalid `AMsyncState*`").into(),
None => return AMresult::err("Invalid AMsyncState pointer").into(),
}
}};
}
@ -18,9 +20,7 @@ macro_rules! to_sync_state {
pub(crate) use to_sync_state;
/// \struct AMsyncState
/// \installed_headerfile
/// \brief The state of synchronization with a peer.
#[derive(Eq, PartialEq)]
pub struct AMsyncState {
body: am::sync::State,
their_haves_storage: RefCell<BTreeMap<usize, AMsyncHave>>,
@ -54,39 +54,40 @@ impl From<AMsyncState> for *mut AMsyncState {
}
/// \memberof AMsyncState
/// \brief Decodes an array of bytes into a synchronization state.
/// \brief Decodes a sequence of bytes into a synchronization state.
///
/// \param[in] src A pointer to an array of bytes.
/// \param[in] count The count of bytes to decode from the array pointed to by
/// \p src.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_SYNC_STATE` item.
/// \pre \p src `!= NULL`
/// \pre `sizeof(`\p src `) > 0`
/// \pre \p count `<= sizeof(`\p src `)`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \param[in] count The number of bytes in \p src to decode.
/// \return A pointer to an `AMresult` struct containing an `AMsyncState`
/// struct.
/// \pre \p src must be a valid address.
/// \pre `0 <=` \p count `<=` length of \p src.
/// \warning To avoid a memory leak, the returned `AMresult` struct must be
/// deallocated with `AMfree()`.
/// \internal
///
/// # Safety
/// src must be a byte array of length `>= count`
#[no_mangle]
pub unsafe extern "C" fn AMsyncStateDecode(src: *const u8, count: usize) -> *mut AMresult {
let data = std::slice::from_raw_parts(src, count);
to_result(am::sync::State::decode(data))
let mut data = Vec::new();
data.extend_from_slice(std::slice::from_raw_parts(src, count));
to_result(am::sync::State::decode(&data))
}
/// \memberof AMsyncState
/// \brief Encodes a synchronization state as an array of bytes.
/// \brief Encodes a synchronizaton state as a sequence of bytes.
///
/// \param[in] sync_state A pointer to an `AMsyncState` struct.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_BYTE_SPAN` item.
/// \pre \p sync_state `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \return A pointer to an `AMresult` struct containing an array of bytes as
/// an `AMbyteSpan` struct.
/// \pre \p sync_state must be a valid address.
/// \warning To avoid a memory leak, the returned `AMresult` struct must be
/// deallocated with `AMfree()`.
/// \internal
///
/// # Safety
/// sync_state must be a valid pointer to an AMsyncState
/// sync_state must be a pointer to a valid AMsyncState
#[no_mangle]
pub unsafe extern "C" fn AMsyncStateEncode(sync_state: *const AMsyncState) -> *mut AMresult {
let sync_state = to_sync_state!(sync_state);
@ -99,14 +100,13 @@ pub unsafe extern "C" fn AMsyncStateEncode(sync_state: *const AMsyncState) -> *m
/// \param[in] sync_state1 A pointer to an `AMsyncState` struct.
/// \param[in] sync_state2 A pointer to an `AMsyncState` struct.
/// \return `true` if \p sync_state1 `==` \p sync_state2 and `false` otherwise.
/// \pre \p sync_state1 `!= NULL`
/// \pre \p sync_state2 `!= NULL`
/// \post `!(`\p sync_state1 `&&` \p sync_state2 `) -> false`
/// \pre \p sync_state1 must be a valid address.
/// \pre \p sync_state2 must be a valid address.
/// \internal
///
/// #Safety
/// sync_state1 must be a valid pointer to an AMsyncState
/// sync_state2 must be a valid pointer to an AMsyncState
/// sync_state1 must be a pointer to a valid AMsyncState
/// sync_state2 must be a pointer to a valid AMsyncState
#[no_mangle]
pub unsafe extern "C" fn AMsyncStateEqual(
sync_state1: *const AMsyncState,
@ -114,17 +114,18 @@ pub unsafe extern "C" fn AMsyncStateEqual(
) -> bool {
match (sync_state1.as_ref(), sync_state2.as_ref()) {
(Some(sync_state1), Some(sync_state2)) => sync_state1.as_ref() == sync_state2.as_ref(),
(None, None) | (None, Some(_)) | (Some(_), None) => false,
(None, Some(_)) | (Some(_), None) | (None, None) => false,
}
}
/// \memberof AMsyncState
/// \brief Allocates a new synchronization state and initializes it from
/// default values.
/// \brief Allocates a new synchronization state and initializes it with
/// defaults.
///
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_SYNC_STATE` item.
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \return A pointer to an `AMresult` struct containing a pointer to an
/// `AMsyncState` struct.
/// \warning To avoid a memory leak, the returned `AMresult` struct must be
/// deallocated with `AMfree()`.
#[no_mangle]
pub extern "C" fn AMsyncStateInit() -> *mut AMresult {
to_result(am::sync::State::new())
@ -134,36 +135,40 @@ pub extern "C" fn AMsyncStateInit() -> *mut AMresult {
/// \brief Gets the heads that are shared by both peers.
///
/// \param[in] sync_state A pointer to an `AMsyncState` struct.
/// \return A pointer to an `AMresult` struct with `AM_VAL_TYPE_CHANGE_HASH` items.
/// \pre \p sync_state `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \return An `AMchangeHashes` struct.
/// \pre \p sync_state must be a valid address.
/// \internal
///
/// # Safety
/// sync_state must be a valid pointer to an AMsyncState
/// sync_state must be a pointer to a valid AMsyncState
#[no_mangle]
pub unsafe extern "C" fn AMsyncStateSharedHeads(sync_state: *const AMsyncState) -> *mut AMresult {
let sync_state = to_sync_state!(sync_state);
to_result(sync_state.as_ref().shared_heads.as_slice())
pub unsafe extern "C" fn AMsyncStateSharedHeads(sync_state: *const AMsyncState) -> AMchangeHashes {
if let Some(sync_state) = sync_state.as_ref() {
AMchangeHashes::new(&sync_state.as_ref().shared_heads)
} else {
AMchangeHashes::default()
}
}
/// \memberof AMsyncState
/// \brief Gets the heads that were last sent by this peer.
///
/// \param[in] sync_state A pointer to an `AMsyncState` struct.
/// \return A pointer to an `AMresult` struct with `AM_VAL_TYPE_CHANGE_HASH` items.
/// \pre \p sync_state `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \return An `AMchangeHashes` struct.
/// \pre \p sync_state must be a valid address.
/// \internal
///
/// # Safety
/// sync_state must be a valid pointer to an AMsyncState
/// sync_state must be a pointer to a valid AMsyncState
#[no_mangle]
pub unsafe extern "C" fn AMsyncStateLastSentHeads(sync_state: *const AMsyncState) -> *mut AMresult {
let sync_state = to_sync_state!(sync_state);
to_result(sync_state.as_ref().last_sent_heads.as_slice())
pub unsafe extern "C" fn AMsyncStateLastSentHeads(
sync_state: *const AMsyncState,
) -> AMchangeHashes {
if let Some(sync_state) = sync_state.as_ref() {
AMchangeHashes::new(&sync_state.as_ref().last_sent_heads)
} else {
AMchangeHashes::default()
}
}
/// \memberof AMsyncState
@ -171,30 +176,28 @@ pub unsafe extern "C" fn AMsyncStateLastSentHeads(sync_state: *const AMsyncState
///
/// \param[in] sync_state A pointer to an `AMsyncState` struct.
/// \param[out] has_value A pointer to a boolean flag that is set to `true` if
/// the returned `AMitems` struct is relevant, `false` otherwise.
/// \return A pointer to an `AMresult` struct with `AM_VAL_TYPE_SYNC_HAVE` items.
/// \pre \p sync_state `!= NULL`
/// \pre \p has_value `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
//// \internal
/// the returned `AMhaves` struct is relevant, `false` otherwise.
/// \return An `AMhaves` struct.
/// \pre \p sync_state must be a valid address.
/// \pre \p has_value must be a valid address.
/// \internal
///
/// # Safety
/// sync_state must be a valid pointer to an AMsyncState
/// has_value must be a valid pointer to a bool.
/// sync_state must be a pointer to a valid AMsyncState
/// has_value must be a pointer to a valid bool.
#[no_mangle]
pub unsafe extern "C" fn AMsyncStateTheirHaves(
sync_state: *const AMsyncState,
has_value: *mut bool,
) -> *mut AMresult {
) -> AMsyncHaves {
if let Some(sync_state) = sync_state.as_ref() {
if let Some(haves) = &sync_state.as_ref().their_have {
*has_value = true;
return to_result(haves.as_slice());
}
return AMsyncHaves::new(haves, &mut sync_state.their_haves_storage.borrow_mut());
};
};
*has_value = false;
to_result(Vec::<am::sync::Have>::new())
AMsyncHaves::default()
}
/// \memberof AMsyncState
@ -202,31 +205,29 @@ pub unsafe extern "C" fn AMsyncStateTheirHaves(
///
/// \param[in] sync_state A pointer to an `AMsyncState` struct.
/// \param[out] has_value A pointer to a boolean flag that is set to `true` if
/// the returned `AMitems` struct is relevant, `false`
/// the returned `AMchangeHashes` struct is relevant, `false`
/// otherwise.
/// \return A pointer to an `AMresult` struct with `AM_VAL_TYPE_CHANGE_HASH` items.
/// \pre \p sync_state `!= NULL`
/// \pre \p has_value `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \return An `AMchangeHashes` struct.
/// \pre \p sync_state must be a valid address.
/// \pre \p has_value must be a valid address.
/// \internal
///
/// # Safety
/// sync_state must be a valid pointer to an AMsyncState
/// has_value must be a valid pointer to a bool
/// sync_state must be a pointer to a valid AMsyncState
/// has_value must be a pointer to a valid bool.
#[no_mangle]
pub unsafe extern "C" fn AMsyncStateTheirHeads(
sync_state: *const AMsyncState,
has_value: *mut bool,
) -> *mut AMresult {
) -> AMchangeHashes {
if let Some(sync_state) = sync_state.as_ref() {
if let Some(change_hashes) = &sync_state.as_ref().their_heads {
*has_value = true;
return to_result(change_hashes.as_slice());
return AMchangeHashes::new(change_hashes);
}
};
*has_value = false;
to_result(Vec::<am::ChangeHash>::new())
AMchangeHashes::default()
}
/// \memberof AMsyncState
@ -234,29 +235,27 @@ pub unsafe extern "C" fn AMsyncStateTheirHeads(
///
/// \param[in] sync_state A pointer to an `AMsyncState` struct.
/// \param[out] has_value A pointer to a boolean flag that is set to `true` if
/// the returned `AMitems` struct is relevant, `false`
/// the returned `AMchangeHashes` struct is relevant, `false`
/// otherwise.
/// \return A pointer to an `AMresult` struct with `AM_VAL_TYPE_CHANGE_HASH` items.
/// \pre \p sync_state `!= NULL`
/// \pre \p has_value `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \return An `AMchangeHashes` struct.
/// \pre \p sync_state must be a valid address.
/// \pre \p has_value must be a valid address.
/// \internal
///
/// # Safety
/// sync_state must be a valid pointer to an AMsyncState
/// has_value must be a valid pointer to a bool
/// sync_state must be a pointer to a valid AMsyncState
/// has_value must be a pointer to a valid bool.
#[no_mangle]
pub unsafe extern "C" fn AMsyncStateTheirNeeds(
sync_state: *const AMsyncState,
has_value: *mut bool,
) -> *mut AMresult {
) -> AMchangeHashes {
if let Some(sync_state) = sync_state.as_ref() {
if let Some(change_hashes) = &sync_state.as_ref().their_need {
*has_value = true;
return to_result(change_hashes.as_slice());
return AMchangeHashes::new(change_hashes);
}
};
*has_value = false;
to_result(Vec::<am::ChangeHash>::new())
AMchangeHashes::default()
}

View file

@ -0,0 +1,52 @@
cmake_minimum_required(VERSION 3.18 FATAL_ERROR)
find_package(cmocka REQUIRED)
add_executable(
test_${LIBRARY_NAME}
group_state.c
doc_tests.c
list_tests.c
map_tests.c
macro_utils.c
main.c
sync_tests.c
)
set_target_properties(test_${LIBRARY_NAME} PROPERTIES LINKER_LANGUAGE C)
# \note An imported library's INTERFACE_INCLUDE_DIRECTORIES property can't
# contain a non-existent path so its build-time include directory
# must be specified for all of its dependent targets instead.
target_include_directories(
test_${LIBRARY_NAME}
PRIVATE "$<BUILD_INTERFACE:${CARGO_TARGET_DIR}>"
)
target_link_libraries(test_${LIBRARY_NAME} PRIVATE cmocka ${LIBRARY_NAME})
add_dependencies(test_${LIBRARY_NAME} ${LIBRARY_NAME}_artifacts)
if(BUILD_SHARED_LIBS AND WIN32)
add_custom_command(
TARGET test_${LIBRARY_NAME}
POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CARGO_CURRENT_BINARY_DIR}/${CMAKE_SHARED_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_${CMAKE_BUILD_TYPE}_POSTFIX}${CMAKE_SHARED_LIBRARY_SUFFIX}
${CMAKE_CURRENT_BINARY_DIR}
COMMENT "Copying the DLL built by Cargo into the test directory..."
VERBATIM
)
endif()
add_test(NAME test_${LIBRARY_NAME} COMMAND test_${LIBRARY_NAME})
add_custom_command(
TARGET test_${LIBRARY_NAME}
POST_BUILD
COMMAND
${CMAKE_CTEST_COMMAND} --config $<CONFIG> --output-on-failure
COMMENT
"Running the test(s)..."
VERBATIM
)

View file

@ -0,0 +1,110 @@
#include <setjmp.h>
#include <stdarg.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* third-party */
#include <cmocka.h>
/* local */
#include "group_state.h"
typedef struct {
GroupState* group_state;
char const* actor_id_str;
uint8_t* actor_id_bytes;
size_t actor_id_size;
} TestState;
static void hex_to_bytes(char const* hex_str, uint8_t* bytes, size_t const count) {
unsigned int byte;
char const* next = hex_str;
for (size_t index = 0; *next && index != count; next += 2, ++index) {
if (sscanf(next, "%02x", &byte) == 1) {
bytes[index] = (uint8_t)byte;
}
}
}
static int setup(void** state) {
TestState* test_state = calloc(1, sizeof(TestState));
group_setup((void**)&test_state->group_state);
test_state->actor_id_str = "000102030405060708090a0b0c0d0e0f";
test_state->actor_id_size = strlen(test_state->actor_id_str) / 2;
test_state->actor_id_bytes = malloc(test_state->actor_id_size);
hex_to_bytes(test_state->actor_id_str, test_state->actor_id_bytes, test_state->actor_id_size);
*state = test_state;
return 0;
}
static int teardown(void** state) {
TestState* test_state = *state;
group_teardown((void**)&test_state->group_state);
free(test_state->actor_id_bytes);
free(test_state);
return 0;
}
static void test_AMputActor(void **state) {
TestState* test_state = *state;
GroupState* group_state = test_state->group_state;
AMresult* res = AMsetActor(
group_state->doc,
test_state->actor_id_bytes,
test_state->actor_id_size
);
if (AMresultStatus(res) != AM_STATUS_OK) {
fail_msg("%s", AMerrorMessage(res));
}
assert_int_equal(AMresultSize(res), 0);
AMvalue value = AMresultValue(res);
assert_int_equal(value.tag, AM_VALUE_VOID);
AMfree(res);
res = AMgetActor(group_state->doc);
if (AMresultStatus(res) != AM_STATUS_OK) {
fail_msg("%s", AMerrorMessage(res));
}
assert_int_equal(AMresultSize(res), 1);
value = AMresultValue(res);
assert_int_equal(value.tag, AM_VALUE_ACTOR_ID);
assert_int_equal(value.actor_id.count, test_state->actor_id_size);
assert_memory_equal(value.actor_id.src, test_state->actor_id_bytes, value.actor_id.count);
AMfree(res);
}
static void test_AMputActorHex(void **state) {
TestState* test_state = *state;
GroupState* group_state = test_state->group_state;
AMresult* res = AMsetActorHex(
group_state->doc,
test_state->actor_id_str
);
if (AMresultStatus(res) != AM_STATUS_OK) {
fail_msg("%s", AMerrorMessage(res));
}
assert_int_equal(AMresultSize(res), 0);
AMvalue value = AMresultValue(res);
assert_int_equal(value.tag, AM_VALUE_VOID);
AMfree(res);
res = AMgetActorHex(group_state->doc);
if (AMresultStatus(res) != AM_STATUS_OK) {
fail_msg("%s", AMerrorMessage(res));
}
assert_int_equal(AMresultSize(res), 1);
value = AMresultValue(res);
assert_int_equal(value.tag, AM_VALUE_STR);
assert_int_equal(strlen(value.str), test_state->actor_id_size * 2);
assert_string_equal(value.str, test_state->actor_id_str);
AMfree(res);
}
int run_AMdoc_property_tests(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test_setup_teardown(test_AMputActor, setup, teardown),
cmocka_unit_test_setup_teardown(test_AMputActorHex, setup, teardown),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}

View file

@ -0,0 +1,235 @@
#include <float.h>
#include <limits.h>
#include <setjmp.h>
#include <stdarg.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h>
/* third-party */
#include <cmocka.h>
/* local */
#include "group_state.h"
#include "macro_utils.h"
#define test_AMlistPut(suffix, mode) test_AMlistPut ## suffix ## _ ## mode
#define static_void_test_AMlistPut(suffix, mode, member, scalar_value) \
static void test_AMlistPut ## suffix ## _ ## mode(void **state) { \
GroupState* group_state = *state; \
AMresult* res = AMlistPut ## suffix( \
group_state->doc, AM_ROOT, 0, !strcmp(#mode, "insert"), scalar_value \
); \
if (AMresultStatus(res) != AM_STATUS_OK) { \
fail_msg("%s", AMerrorMessage(res)); \
} \
assert_int_equal(AMresultSize(res), 0); \
AMvalue value = AMresultValue(res); \
assert_int_equal(value.tag, AM_VALUE_VOID); \
AMfree(res); \
res = AMlistGet(group_state->doc, AM_ROOT, 0); \
if (AMresultStatus(res) != AM_STATUS_OK) { \
fail_msg("%s", AMerrorMessage(res)); \
} \
assert_int_equal(AMresultSize(res), 1); \
value = AMresultValue(res); \
assert_int_equal(value.tag, AMvalue_discriminant(#suffix)); \
assert_true(value.member == scalar_value); \
AMfree(res); \
}
#define test_AMlistPutBytes(mode) test_AMlistPutBytes ## _ ## mode
#define static_void_test_AMlistPutBytes(mode, bytes_value) \
static void test_AMlistPutBytes_ ## mode(void **state) { \
static size_t const BYTES_SIZE = sizeof(bytes_value) / sizeof(uint8_t); \
\
GroupState* group_state = *state; \
AMresult* res = AMlistPutBytes( \
group_state->doc, \
AM_ROOT, \
0, \
!strcmp(#mode, "insert"), \
bytes_value, \
BYTES_SIZE \
); \
if (AMresultStatus(res) != AM_STATUS_OK) { \
fail_msg("%s", AMerrorMessage(res)); \
} \
assert_int_equal(AMresultSize(res), 0); \
AMvalue value = AMresultValue(res); \
assert_int_equal(value.tag, AM_VALUE_VOID); \
AMfree(res); \
res = AMlistGet(group_state->doc, AM_ROOT, 0); \
if (AMresultStatus(res) != AM_STATUS_OK) { \
fail_msg("%s", AMerrorMessage(res)); \
} \
assert_int_equal(AMresultSize(res), 1); \
value = AMresultValue(res); \
assert_int_equal(value.tag, AM_VALUE_BYTES); \
assert_int_equal(value.bytes.count, BYTES_SIZE); \
assert_memory_equal(value.bytes.src, bytes_value, BYTES_SIZE); \
AMfree(res); \
}
#define test_AMlistPutNull(mode) test_AMlistPutNull_ ## mode
#define static_void_test_AMlistPutNull(mode) \
static void test_AMlistPutNull_ ## mode(void **state) { \
GroupState* group_state = *state; \
AMresult* res = AMlistPutNull( \
group_state->doc, AM_ROOT, 0, !strcmp(#mode, "insert")); \
if (AMresultStatus(res) != AM_STATUS_OK) { \
fail_msg("%s", AMerrorMessage(res)); \
} \
assert_int_equal(AMresultSize(res), 0); \
AMvalue value = AMresultValue(res); \
assert_int_equal(value.tag, AM_VALUE_VOID); \
AMfree(res); \
res = AMlistGet(group_state->doc, AM_ROOT, 0); \
if (AMresultStatus(res) != AM_STATUS_OK) { \
fail_msg("%s", AMerrorMessage(res)); \
} \
assert_int_equal(AMresultSize(res), 1); \
value = AMresultValue(res); \
assert_int_equal(value.tag, AM_VALUE_NULL); \
AMfree(res); \
}
#define test_AMlistPutObject(label, mode) test_AMlistPutObject_ ## label ## _ ## mode
#define static_void_test_AMlistPutObject(label, mode) \
static void test_AMlistPutObject_ ## label ## _ ## mode(void **state) { \
GroupState* group_state = *state; \
AMresult* res = AMlistPutObject( \
group_state->doc, \
AM_ROOT, \
0, \
!strcmp(#mode, "insert"), \
AMobjType_tag(#label) \
); \
if (AMresultStatus(res) != AM_STATUS_OK) { \
fail_msg("%s", AMerrorMessage(res)); \
} \
assert_int_equal(AMresultSize(res), 1); \
AMvalue value = AMresultValue(res); \
assert_int_equal(value.tag, AM_VALUE_OBJ_ID); \
assert_non_null(value.obj_id); \
assert_int_equal(AMobjSize(group_state->doc, value.obj_id), 0); \
AMfree(res); \
}
#define test_AMlistPutStr(mode) test_AMlistPutStr ## _ ## mode
#define static_void_test_AMlistPutStr(mode, str_value) \
static void test_AMlistPutStr_ ## mode(void **state) { \
static size_t const STR_LEN = strlen(str_value); \
\
GroupState* group_state = *state; \
AMresult* res = AMlistPutStr( \
group_state->doc, \
AM_ROOT, \
0, \
!strcmp(#mode, "insert"), \
str_value \
); \
if (AMresultStatus(res) != AM_STATUS_OK) { \
fail_msg("%s", AMerrorMessage(res)); \
} \
assert_int_equal(AMresultSize(res), 0); \
AMvalue value = AMresultValue(res); \
assert_int_equal(value.tag, AM_VALUE_VOID); \
AMfree(res); \
res = AMlistGet(group_state->doc, AM_ROOT, 0); \
if (AMresultStatus(res) != AM_STATUS_OK) { \
fail_msg("%s", AMerrorMessage(res)); \
} \
assert_int_equal(AMresultSize(res), 1); \
value = AMresultValue(res); \
assert_int_equal(value.tag, AM_VALUE_STR); \
assert_int_equal(strlen(value.str), STR_LEN); \
assert_memory_equal(value.str, str_value, STR_LEN + 1); \
AMfree(res); \
}
static_void_test_AMlistPut(Bool, insert, boolean, true)
static_void_test_AMlistPut(Bool, update, boolean, true)
static uint8_t const BYTES_VALUE[] = {INT8_MIN, INT8_MAX / 2, INT8_MAX};
static_void_test_AMlistPutBytes(insert, BYTES_VALUE)
static_void_test_AMlistPutBytes(update, BYTES_VALUE)
static_void_test_AMlistPut(Counter, insert, counter, INT64_MAX)
static_void_test_AMlistPut(Counter, update, counter, INT64_MAX)
static_void_test_AMlistPut(F64, insert, f64, DBL_MAX)
static_void_test_AMlistPut(F64, update, f64, DBL_MAX)
static_void_test_AMlistPut(Int, insert, int_, INT64_MAX)
static_void_test_AMlistPut(Int, update, int_, INT64_MAX)
static_void_test_AMlistPutNull(insert)
static_void_test_AMlistPutNull(update)
static_void_test_AMlistPutObject(List, insert)
static_void_test_AMlistPutObject(List, update)
static_void_test_AMlistPutObject(Map, insert)
static_void_test_AMlistPutObject(Map, update)
static_void_test_AMlistPutObject(Text, insert)
static_void_test_AMlistPutObject(Text, update)
static_void_test_AMlistPutStr(insert, "Hello, world!")
static_void_test_AMlistPutStr(update, "Hello, world!")
static_void_test_AMlistPut(Timestamp, insert, timestamp, INT64_MAX)
static_void_test_AMlistPut(Timestamp, update, timestamp, INT64_MAX)
static_void_test_AMlistPut(Uint, insert, uint, UINT64_MAX)
static_void_test_AMlistPut(Uint, update, uint, UINT64_MAX)
int run_AMlistPut_tests(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_AMlistPut(Bool, insert)),
cmocka_unit_test(test_AMlistPut(Bool, update)),
cmocka_unit_test(test_AMlistPutBytes(insert)),
cmocka_unit_test(test_AMlistPutBytes(update)),
cmocka_unit_test(test_AMlistPut(Counter, insert)),
cmocka_unit_test(test_AMlistPut(Counter, update)),
cmocka_unit_test(test_AMlistPut(F64, insert)),
cmocka_unit_test(test_AMlistPut(F64, update)),
cmocka_unit_test(test_AMlistPut(Int, insert)),
cmocka_unit_test(test_AMlistPut(Int, update)),
cmocka_unit_test(test_AMlistPutNull(insert)),
cmocka_unit_test(test_AMlistPutNull(update)),
cmocka_unit_test(test_AMlistPutObject(List, insert)),
cmocka_unit_test(test_AMlistPutObject(List, update)),
cmocka_unit_test(test_AMlistPutObject(Map, insert)),
cmocka_unit_test(test_AMlistPutObject(Map, update)),
cmocka_unit_test(test_AMlistPutObject(Text, insert)),
cmocka_unit_test(test_AMlistPutObject(Text, update)),
cmocka_unit_test(test_AMlistPutStr(insert)),
cmocka_unit_test(test_AMlistPutStr(update)),
cmocka_unit_test(test_AMlistPut(Timestamp, insert)),
cmocka_unit_test(test_AMlistPut(Timestamp, update)),
cmocka_unit_test(test_AMlistPut(Uint, insert)),
cmocka_unit_test(test_AMlistPut(Uint, update)),
};
return cmocka_run_group_tests(tests, group_setup, group_teardown);
}

View file

@ -0,0 +1,187 @@
#include <float.h>
#include <limits.h>
#include <setjmp.h>
#include <stdarg.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h>
/* third-party */
#include <cmocka.h>
/* local */
#include "group_state.h"
#include "macro_utils.h"
#define test_AMmapPut(suffix) test_AMmapPut ## suffix
#define static_void_test_AMmapPut(suffix, member, scalar_value) \
static void test_AMmapPut ## suffix(void **state) { \
GroupState* group_state = *state; \
AMresult* res = AMmapPut ## suffix( \
group_state->doc, \
AM_ROOT, \
#suffix, \
scalar_value \
); \
if (AMresultStatus(res) != AM_STATUS_OK) { \
fail_msg("%s", AMerrorMessage(res)); \
} \
assert_int_equal(AMresultSize(res), 0); \
AMvalue value = AMresultValue(res); \
assert_int_equal(value.tag, AM_VALUE_VOID); \
AMfree(res); \
res = AMmapGet(group_state->doc, AM_ROOT, #suffix); \
if (AMresultStatus(res) != AM_STATUS_OK) { \
fail_msg("%s", AMerrorMessage(res)); \
} \
assert_int_equal(AMresultSize(res), 1); \
value = AMresultValue(res); \
assert_int_equal(value.tag, AMvalue_discriminant(#suffix)); \
assert_true(value.member == scalar_value); \
AMfree(res); \
}
#define test_AMmapPutObject(label) test_AMmapPutObject_ ## label
#define static_void_test_AMmapPutObject(label) \
static void test_AMmapPutObject_ ## label(void **state) { \
GroupState* group_state = *state; \
AMresult* res = AMmapPutObject( \
group_state->doc, \
AM_ROOT, \
#label, \
AMobjType_tag(#label) \
); \
if (AMresultStatus(res) != AM_STATUS_OK) { \
fail_msg("%s", AMerrorMessage(res)); \
} \
assert_int_equal(AMresultSize(res), 1); \
AMvalue value = AMresultValue(res); \
assert_int_equal(value.tag, AM_VALUE_OBJ_ID); \
assert_non_null(value.obj_id); \
assert_int_equal(AMobjSize(group_state->doc, value.obj_id), 0); \
AMfree(res); \
}
static_void_test_AMmapPut(Bool, boolean, true)
static void test_AMmapPutBytes(void **state) {
static char const* const KEY = "Bytes";
static uint8_t const BYTES_VALUE[] = {INT8_MIN, INT8_MAX / 2, INT8_MAX};
static size_t const BYTES_SIZE = sizeof(BYTES_VALUE) / sizeof(uint8_t);
GroupState* group_state = *state;
AMresult* res = AMmapPutBytes(
group_state->doc,
AM_ROOT,
KEY,
BYTES_VALUE,
BYTES_SIZE
);
if (AMresultStatus(res) != AM_STATUS_OK) {
fail_msg("%s", AMerrorMessage(res));
}
assert_int_equal(AMresultSize(res), 0);
AMvalue value = AMresultValue(res);
assert_int_equal(value.tag, AM_VALUE_VOID);
AMfree(res);
res = AMmapGet(group_state->doc, AM_ROOT, KEY);
if (AMresultStatus(res) != AM_STATUS_OK) {
fail_msg("%s", AMerrorMessage(res));
}
assert_int_equal(AMresultSize(res), 1);
value = AMresultValue(res);
assert_int_equal(value.tag, AM_VALUE_BYTES);
assert_int_equal(value.bytes.count, BYTES_SIZE);
assert_memory_equal(value.bytes.src, BYTES_VALUE, BYTES_SIZE);
AMfree(res);
}
static_void_test_AMmapPut(Counter, counter, INT64_MAX)
static_void_test_AMmapPut(F64, f64, DBL_MAX)
static_void_test_AMmapPut(Int, int_, INT64_MAX)
static void test_AMmapPutNull(void **state) {
static char const* const KEY = "Null";
GroupState* group_state = *state;
AMresult* res = AMmapPutNull(group_state->doc, AM_ROOT, KEY);
if (AMresultStatus(res) != AM_STATUS_OK) {
fail_msg("%s", AMerrorMessage(res));
}
assert_int_equal(AMresultSize(res), 0);
AMvalue value = AMresultValue(res);
assert_int_equal(value.tag, AM_VALUE_VOID);
AMfree(res);
res = AMmapGet(group_state->doc, AM_ROOT, KEY);
if (AMresultStatus(res) != AM_STATUS_OK) {
fail_msg("%s", AMerrorMessage(res));
}
assert_int_equal(AMresultSize(res), 1);
value = AMresultValue(res);
assert_int_equal(value.tag, AM_VALUE_NULL);
AMfree(res);
}
static_void_test_AMmapPutObject(List)
static_void_test_AMmapPutObject(Map)
static_void_test_AMmapPutObject(Text)
static void test_AMmapPutStr(void **state) {
static char const* const KEY = "Str";
static char const* const STR_VALUE = "Hello, world!";
size_t const STR_LEN = strlen(STR_VALUE);
GroupState* group_state = *state;
AMresult* res = AMmapPutStr(
group_state->doc,
AM_ROOT,
KEY,
STR_VALUE
);
if (AMresultStatus(res) != AM_STATUS_OK) {
fail_msg("%s", AMerrorMessage(res));
}
assert_int_equal(AMresultSize(res), 0);
AMvalue value = AMresultValue(res);
assert_int_equal(value.tag, AM_VALUE_VOID);
AMfree(res);
res = AMmapGet(group_state->doc, AM_ROOT, KEY);
if (AMresultStatus(res) != AM_STATUS_OK) {
fail_msg("%s", AMerrorMessage(res));
}
assert_int_equal(AMresultSize(res), 1);
value = AMresultValue(res);
assert_int_equal(value.tag, AM_VALUE_STR);
assert_int_equal(strlen(value.str), STR_LEN);
assert_memory_equal(value.str, STR_VALUE, STR_LEN + 1);
AMfree(res);
}
static_void_test_AMmapPut(Timestamp, timestamp, INT64_MAX)
static_void_test_AMmapPut(Uint, uint, UINT64_MAX)
int run_AMmapPut_tests(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_AMmapPut(Bool)),
cmocka_unit_test(test_AMmapPutBytes),
cmocka_unit_test(test_AMmapPut(Counter)),
cmocka_unit_test(test_AMmapPut(F64)),
cmocka_unit_test(test_AMmapPut(Int)),
cmocka_unit_test(test_AMmapPutNull),
cmocka_unit_test(test_AMmapPutObject(List)),
cmocka_unit_test(test_AMmapPutObject(Map)),
cmocka_unit_test(test_AMmapPutObject(Text)),
cmocka_unit_test(test_AMmapPutStr),
cmocka_unit_test(test_AMmapPut(Timestamp)),
cmocka_unit_test(test_AMmapPut(Uint)),
};
return cmocka_run_group_tests(tests, group_setup, group_teardown);
}

View file

@ -0,0 +1,110 @@
#include <setjmp.h>
#include <stdarg.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* third-party */
#include <cmocka.h>
/* local */
#include "group_state.h"
typedef struct {
GroupState* group_state;
char const* actor_id_str;
uint8_t* actor_id_bytes;
size_t actor_id_size;
} TestState;
static void hex_to_bytes(char const* hex_str, uint8_t* bytes, size_t const count) {
unsigned int byte;
char const* next = hex_str;
for (size_t index = 0; *next && index != count; next += 2, ++index) {
if (sscanf(next, "%02x", &byte) == 1) {
bytes[index] = (uint8_t)byte;
}
}
}
static int setup(void** state) {
TestState* test_state = calloc(1, sizeof(TestState));
group_setup((void**)&test_state->group_state);
test_state->actor_id_str = "000102030405060708090a0b0c0d0e0f";
test_state->actor_id_size = strlen(test_state->actor_id_str) / 2;
test_state->actor_id_bytes = malloc(test_state->actor_id_size);
hex_to_bytes(test_state->actor_id_str, test_state->actor_id_bytes, test_state->actor_id_size);
*state = test_state;
return 0;
}
static int teardown(void** state) {
TestState* test_state = *state;
group_teardown((void**)&test_state->group_state);
free(test_state->actor_id_bytes);
free(test_state);
return 0;
}
static void test_AMputActor(void **state) {
TestState* test_state = *state;
GroupState* group_state = test_state->group_state;
AMresult* res = AMsetActor(
group_state->doc,
test_state->actor_id_bytes,
test_state->actor_id_size
);
if (AMresultStatus(res) != AM_STATUS_OK) {
fail_msg("%s", AMerrorMessage(res));
}
assert_int_equal(AMresultSize(res), 0);
AMvalue value = AMresultValue(res);
assert_int_equal(value.tag, AM_VALUE_VOID);
AMfree(res);
res = AMgetActor(group_state->doc);
if (AMresultStatus(res) != AM_STATUS_OK) {
fail_msg("%s", AMerrorMessage(res));
}
assert_int_equal(AMresultSize(res), 1);
value = AMresultValue(res);
assert_int_equal(value.tag, AM_VALUE_ACTOR_ID);
assert_int_equal(value.actor_id.count, test_state->actor_id_size);
assert_memory_equal(value.actor_id.src, test_state->actor_id_bytes, value.actor_id.count);
AMfree(res);
}
static void test_AMputActorHex(void **state) {
TestState* test_state = *state;
GroupState* group_state = test_state->group_state;
AMresult* res = AMsetActorHex(
group_state->doc,
test_state->actor_id_str
);
if (AMresultStatus(res) != AM_STATUS_OK) {
fail_msg("%s", AMerrorMessage(res));
}
assert_int_equal(AMresultSize(res), 0);
AMvalue value = AMresultValue(res);
assert_int_equal(value.tag, AM_VALUE_VOID);
AMfree(res);
res = AMgetActorHex(group_state->doc);
if (AMresultStatus(res) != AM_STATUS_OK) {
fail_msg("%s", AMerrorMessage(res));
}
assert_int_equal(AMresultSize(res), 1);
value = AMresultValue(res);
assert_int_equal(value.tag, AM_VALUE_STR);
assert_int_equal(strlen(value.str), test_state->actor_id_size * 2);
assert_string_equal(value.str, test_state->actor_id_str);
AMfree(res);
}
int run_doc_tests(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test_setup_teardown(test_AMputActor, setup, teardown),
cmocka_unit_test_setup_teardown(test_AMputActorHex, setup, teardown),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}

View file

@ -0,0 +1,19 @@
#include <stdlib.h>
/* local */
#include "group_state.h"
int group_setup(void** state) {
GroupState* group_state = calloc(1, sizeof(GroupState));
group_state->doc_result = AMcreate();
group_state->doc = AMresultValue(group_state->doc_result).doc;
*state = group_state;
return 0;
}
int group_teardown(void** state) {
GroupState* group_state = *state;
AMfree(group_state->doc_result);
free(group_state);
return 0;
}

View file

@ -0,0 +1,16 @@
#ifndef GROUP_STATE_INCLUDED
#define GROUP_STATE_INCLUDED
/* local */
#include "automerge.h"
typedef struct {
AMresult* doc_result;
AMdoc* doc;
} GroupState;
int group_setup(void** state);
int group_teardown(void** state);
#endif

View file

@ -0,0 +1,272 @@
#include <float.h>
#include <limits.h>
#include <setjmp.h>
#include <stdarg.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h>
/* third-party */
#include <cmocka.h>
/* local */
#include "group_state.h"
#include "macro_utils.h"
static void test_AMlistIncrement(void** state) {
GroupState* group_state = *state;
AMresult* res = AMlistPutCounter(group_state->doc, AM_ROOT, 0, true, 0);
if (AMresultStatus(res) != AM_STATUS_OK) {
fail_msg("%s", AMerrorMessage(res));
}
assert_int_equal(AMresultSize(res), 0);
assert_int_equal(AMresultValue(res).tag, AM_VALUE_VOID);
AMfree(res);
res = AMlistGet(group_state->doc, AM_ROOT, 0);
if (AMresultStatus(res) != AM_STATUS_OK) {
fail_msg("%s", AMerrorMessage(res));
}
assert_int_equal(AMresultSize(res), 1);
AMvalue value = AMresultValue(res);
assert_int_equal(value.tag, AM_VALUE_COUNTER);
assert_int_equal(value.counter, 0);
AMfree(res);
res = AMlistIncrement(group_state->doc, AM_ROOT, 0, 3);
if (AMresultStatus(res) != AM_STATUS_OK) {
fail_msg("%s", AMerrorMessage(res));
}
assert_int_equal(AMresultSize(res), 0);
assert_int_equal(AMresultValue(res).tag, AM_VALUE_VOID);
AMfree(res);
res = AMlistGet(group_state->doc, AM_ROOT, 0);
if (AMresultStatus(res) != AM_STATUS_OK) {
fail_msg("%s", AMerrorMessage(res));
}
assert_int_equal(AMresultSize(res), 1);
value = AMresultValue(res);
assert_int_equal(value.tag, AM_VALUE_COUNTER);
assert_int_equal(value.counter, 3);
AMfree(res);
}
#define test_AMlistPut(suffix, mode) test_AMlistPut ## suffix ## _ ## mode
#define static_void_test_AMlistPut(suffix, mode, member, scalar_value) \
static void test_AMlistPut ## suffix ## _ ## mode(void **state) { \
GroupState* group_state = *state; \
AMresult* res = AMlistPut ## suffix( \
group_state->doc, AM_ROOT, 0, !strcmp(#mode, "insert"), scalar_value \
); \
if (AMresultStatus(res) != AM_STATUS_OK) { \
fail_msg("%s", AMerrorMessage(res)); \
} \
assert_int_equal(AMresultSize(res), 0); \
AMvalue value = AMresultValue(res); \
assert_int_equal(value.tag, AM_VALUE_VOID); \
AMfree(res); \
res = AMlistGet(group_state->doc, AM_ROOT, 0); \
if (AMresultStatus(res) != AM_STATUS_OK) { \
fail_msg("%s", AMerrorMessage(res)); \
} \
assert_int_equal(AMresultSize(res), 1); \
value = AMresultValue(res); \
assert_int_equal(value.tag, AMvalue_discriminant(#suffix)); \
assert_true(value.member == scalar_value); \
AMfree(res); \
}
#define test_AMlistPutBytes(mode) test_AMlistPutBytes ## _ ## mode
#define static_void_test_AMlistPutBytes(mode, bytes_value) \
static void test_AMlistPutBytes_ ## mode(void **state) { \
static size_t const BYTES_SIZE = sizeof(bytes_value) / sizeof(uint8_t); \
\
GroupState* group_state = *state; \
AMresult* res = AMlistPutBytes( \
group_state->doc, \
AM_ROOT, \
0, \
!strcmp(#mode, "insert"), \
bytes_value, \
BYTES_SIZE \
); \
if (AMresultStatus(res) != AM_STATUS_OK) { \
fail_msg("%s", AMerrorMessage(res)); \
} \
assert_int_equal(AMresultSize(res), 0); \
AMvalue value = AMresultValue(res); \
assert_int_equal(value.tag, AM_VALUE_VOID); \
AMfree(res); \
res = AMlistGet(group_state->doc, AM_ROOT, 0); \
if (AMresultStatus(res) != AM_STATUS_OK) { \
fail_msg("%s", AMerrorMessage(res)); \
} \
assert_int_equal(AMresultSize(res), 1); \
value = AMresultValue(res); \
assert_int_equal(value.tag, AM_VALUE_BYTES); \
assert_int_equal(value.bytes.count, BYTES_SIZE); \
assert_memory_equal(value.bytes.src, bytes_value, BYTES_SIZE); \
AMfree(res); \
}
#define test_AMlistPutNull(mode) test_AMlistPutNull_ ## mode
#define static_void_test_AMlistPutNull(mode) \
static void test_AMlistPutNull_ ## mode(void **state) { \
GroupState* group_state = *state; \
AMresult* res = AMlistPutNull( \
group_state->doc, AM_ROOT, 0, !strcmp(#mode, "insert")); \
if (AMresultStatus(res) != AM_STATUS_OK) { \
fail_msg("%s", AMerrorMessage(res)); \
} \
assert_int_equal(AMresultSize(res), 0); \
AMvalue value = AMresultValue(res); \
assert_int_equal(value.tag, AM_VALUE_VOID); \
AMfree(res); \
res = AMlistGet(group_state->doc, AM_ROOT, 0); \
if (AMresultStatus(res) != AM_STATUS_OK) { \
fail_msg("%s", AMerrorMessage(res)); \
} \
assert_int_equal(AMresultSize(res), 1); \
value = AMresultValue(res); \
assert_int_equal(value.tag, AM_VALUE_NULL); \
AMfree(res); \
}
#define test_AMlistPutObject(label, mode) test_AMlistPutObject_ ## label ## _ ## mode
#define static_void_test_AMlistPutObject(label, mode) \
static void test_AMlistPutObject_ ## label ## _ ## mode(void **state) { \
GroupState* group_state = *state; \
AMresult* res = AMlistPutObject( \
group_state->doc, \
AM_ROOT, \
0, \
!strcmp(#mode, "insert"), \
AMobjType_tag(#label) \
); \
if (AMresultStatus(res) != AM_STATUS_OK) { \
fail_msg("%s", AMerrorMessage(res)); \
} \
assert_int_equal(AMresultSize(res), 1); \
AMvalue value = AMresultValue(res); \
assert_int_equal(value.tag, AM_VALUE_OBJ_ID); \
assert_non_null(value.obj_id); \
assert_int_equal(AMobjSize(group_state->doc, value.obj_id), 0); \
AMfree(res); \
}
#define test_AMlistPutStr(mode) test_AMlistPutStr ## _ ## mode
#define static_void_test_AMlistPutStr(mode, str_value) \
static void test_AMlistPutStr_ ## mode(void **state) { \
static size_t const STR_LEN = strlen(str_value); \
\
GroupState* group_state = *state; \
AMresult* res = AMlistPutStr( \
group_state->doc, \
AM_ROOT, \
0, \
!strcmp(#mode, "insert"), \
str_value \
); \
if (AMresultStatus(res) != AM_STATUS_OK) { \
fail_msg("%s", AMerrorMessage(res)); \
} \
assert_int_equal(AMresultSize(res), 0); \
AMvalue value = AMresultValue(res); \
assert_int_equal(value.tag, AM_VALUE_VOID); \
AMfree(res); \
res = AMlistGet(group_state->doc, AM_ROOT, 0); \
if (AMresultStatus(res) != AM_STATUS_OK) { \
fail_msg("%s", AMerrorMessage(res)); \
} \
assert_int_equal(AMresultSize(res), 1); \
value = AMresultValue(res); \
assert_int_equal(value.tag, AM_VALUE_STR); \
assert_int_equal(strlen(value.str), STR_LEN); \
assert_memory_equal(value.str, str_value, STR_LEN + 1); \
AMfree(res); \
}
static_void_test_AMlistPut(Bool, insert, boolean, true)
static_void_test_AMlistPut(Bool, update, boolean, true)
static uint8_t const BYTES_VALUE[] = {INT8_MIN, INT8_MAX / 2, INT8_MAX};
static_void_test_AMlistPutBytes(insert, BYTES_VALUE)
static_void_test_AMlistPutBytes(update, BYTES_VALUE)
static_void_test_AMlistPut(Counter, insert, counter, INT64_MAX)
static_void_test_AMlistPut(Counter, update, counter, INT64_MAX)
static_void_test_AMlistPut(F64, insert, f64, DBL_MAX)
static_void_test_AMlistPut(F64, update, f64, DBL_MAX)
static_void_test_AMlistPut(Int, insert, int_, INT64_MAX)
static_void_test_AMlistPut(Int, update, int_, INT64_MAX)
static_void_test_AMlistPutNull(insert)
static_void_test_AMlistPutNull(update)
static_void_test_AMlistPutObject(List, insert)
static_void_test_AMlistPutObject(List, update)
static_void_test_AMlistPutObject(Map, insert)
static_void_test_AMlistPutObject(Map, update)
static_void_test_AMlistPutObject(Text, insert)
static_void_test_AMlistPutObject(Text, update)
static_void_test_AMlistPutStr(insert, "Hello, world!")
static_void_test_AMlistPutStr(update, "Hello, world!")
static_void_test_AMlistPut(Timestamp, insert, timestamp, INT64_MAX)
static_void_test_AMlistPut(Timestamp, update, timestamp, INT64_MAX)
static_void_test_AMlistPut(Uint, insert, uint, UINT64_MAX)
static_void_test_AMlistPut(Uint, update, uint, UINT64_MAX)
int run_list_tests(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_AMlistIncrement),
cmocka_unit_test(test_AMlistPut(Bool, insert)),
cmocka_unit_test(test_AMlistPut(Bool, update)),
cmocka_unit_test(test_AMlistPutBytes(insert)),
cmocka_unit_test(test_AMlistPutBytes(update)),
cmocka_unit_test(test_AMlistPut(Counter, insert)),
cmocka_unit_test(test_AMlistPut(Counter, update)),
cmocka_unit_test(test_AMlistPut(F64, insert)),
cmocka_unit_test(test_AMlistPut(F64, update)),
cmocka_unit_test(test_AMlistPut(Int, insert)),
cmocka_unit_test(test_AMlistPut(Int, update)),
cmocka_unit_test(test_AMlistPutNull(insert)),
cmocka_unit_test(test_AMlistPutNull(update)),
cmocka_unit_test(test_AMlistPutObject(List, insert)),
cmocka_unit_test(test_AMlistPutObject(List, update)),
cmocka_unit_test(test_AMlistPutObject(Map, insert)),
cmocka_unit_test(test_AMlistPutObject(Map, update)),
cmocka_unit_test(test_AMlistPutObject(Text, insert)),
cmocka_unit_test(test_AMlistPutObject(Text, update)),
cmocka_unit_test(test_AMlistPutStr(insert)),
cmocka_unit_test(test_AMlistPutStr(update)),
cmocka_unit_test(test_AMlistPut(Timestamp, insert)),
cmocka_unit_test(test_AMlistPut(Timestamp, update)),
cmocka_unit_test(test_AMlistPut(Uint, insert)),
cmocka_unit_test(test_AMlistPut(Uint, update)),
};
return cmocka_run_group_tests(tests, group_setup, group_teardown);
}

View file

@ -0,0 +1,24 @@
#include <string.h>
/* local */
#include "macro_utils.h"
AMvalueVariant AMvalue_discriminant(char const* suffix) {
if (!strcmp(suffix, "Bool")) return AM_VALUE_BOOLEAN;
else if (!strcmp(suffix, "Bytes")) return AM_VALUE_BYTES;
else if (!strcmp(suffix, "Counter")) return AM_VALUE_COUNTER;
else if (!strcmp(suffix, "F64")) return AM_VALUE_F64;
else if (!strcmp(suffix, "Int")) return AM_VALUE_INT;
else if (!strcmp(suffix, "Null")) return AM_VALUE_NULL;
else if (!strcmp(suffix, "Str")) return AM_VALUE_STR;
else if (!strcmp(suffix, "Timestamp")) return AM_VALUE_TIMESTAMP;
else if (!strcmp(suffix, "Uint")) return AM_VALUE_UINT;
else return AM_VALUE_VOID;
}
AMobjType AMobjType_tag(char const* obj_type_label) {
if (!strcmp(obj_type_label, "List")) return AM_OBJ_TYPE_LIST;
else if (!strcmp(obj_type_label, "Map")) return AM_OBJ_TYPE_MAP;
else if (!strcmp(obj_type_label, "Text")) return AM_OBJ_TYPE_TEXT;
else return 0;
}

View file

@ -0,0 +1,24 @@
#ifndef MACRO_UTILS_INCLUDED
#define MACRO_UTILS_INCLUDED
/* local */
#include "automerge.h"
/**
* \brief Gets the result value discriminant corresponding to a function name
* suffix.
*
* \param[in] suffix A string.
* \return An `AMvalue` struct discriminant.
*/
AMvalueVariant AMvalue_discriminant(char const* suffix);
/**
* \brief Gets the object type tag corresponding to an object type label.
*
* \param[in] obj_type_label A string.
* \return An `AMobjType` enum tag.
*/
AMobjType AMobjType_tag(char const* obj_type_label);
#endif

24
automerge-c/test/main.c Normal file
View file

@ -0,0 +1,24 @@
#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <stdint.h>
/* third-party */
#include <cmocka.h>
extern int run_doc_tests(void);
extern int run_list_tests(void);
extern int run_map_tests(void);
extern int run_sync_tests(void);
int main(void) {
return (
run_doc_tests() +
run_list_tests() +
run_map_tests() +
run_sync_tests()
);
}

View file

@ -0,0 +1,224 @@
#include <float.h>
#include <limits.h>
#include <setjmp.h>
#include <stdarg.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h>
/* third-party */
#include <cmocka.h>
/* local */
#include "group_state.h"
#include "macro_utils.h"
static void test_AMmapIncrement(void** state) {
GroupState* group_state = *state;
AMresult* res = AMmapPutCounter(group_state->doc, AM_ROOT, "Counter", 0);
if (AMresultStatus(res) != AM_STATUS_OK) {
fail_msg("%s", AMerrorMessage(res));
}
assert_int_equal(AMresultSize(res), 0);
assert_int_equal(AMresultValue(res).tag, AM_VALUE_VOID);
AMfree(res);
res = AMmapGet(group_state->doc, AM_ROOT, "Counter");
if (AMresultStatus(res) != AM_STATUS_OK) {
fail_msg("%s", AMerrorMessage(res));
}
assert_int_equal(AMresultSize(res), 1);
AMvalue value = AMresultValue(res);
assert_int_equal(value.tag, AM_VALUE_COUNTER);
assert_int_equal(value.counter, 0);
AMfree(res);
res = AMmapIncrement(group_state->doc, AM_ROOT, "Counter", 3);
if (AMresultStatus(res) != AM_STATUS_OK) {
fail_msg("%s", AMerrorMessage(res));
}
assert_int_equal(AMresultSize(res), 0);
assert_int_equal(AMresultValue(res).tag, AM_VALUE_VOID);
AMfree(res);
res = AMmapGet(group_state->doc, AM_ROOT, "Counter");
if (AMresultStatus(res) != AM_STATUS_OK) {
fail_msg("%s", AMerrorMessage(res));
}
assert_int_equal(AMresultSize(res), 1);
value = AMresultValue(res);
assert_int_equal(value.tag, AM_VALUE_COUNTER);
assert_int_equal(value.counter, 3);
AMfree(res);
}
#define test_AMmapPut(suffix) test_AMmapPut ## suffix
#define static_void_test_AMmapPut(suffix, member, scalar_value) \
static void test_AMmapPut ## suffix(void **state) { \
GroupState* group_state = *state; \
AMresult* res = AMmapPut ## suffix( \
group_state->doc, \
AM_ROOT, \
#suffix, \
scalar_value \
); \
if (AMresultStatus(res) != AM_STATUS_OK) { \
fail_msg("%s", AMerrorMessage(res)); \
} \
assert_int_equal(AMresultSize(res), 0); \
AMvalue value = AMresultValue(res); \
assert_int_equal(value.tag, AM_VALUE_VOID); \
AMfree(res); \
res = AMmapGet(group_state->doc, AM_ROOT, #suffix); \
if (AMresultStatus(res) != AM_STATUS_OK) { \
fail_msg("%s", AMerrorMessage(res)); \
} \
assert_int_equal(AMresultSize(res), 1); \
value = AMresultValue(res); \
assert_int_equal(value.tag, AMvalue_discriminant(#suffix)); \
assert_true(value.member == scalar_value); \
AMfree(res); \
}
#define test_AMmapPutObject(label) test_AMmapPutObject_ ## label
#define static_void_test_AMmapPutObject(label) \
static void test_AMmapPutObject_ ## label(void **state) { \
GroupState* group_state = *state; \
AMresult* res = AMmapPutObject( \
group_state->doc, \
AM_ROOT, \
#label, \
AMobjType_tag(#label) \
); \
if (AMresultStatus(res) != AM_STATUS_OK) { \
fail_msg("%s", AMerrorMessage(res)); \
} \
assert_int_equal(AMresultSize(res), 1); \
AMvalue value = AMresultValue(res); \
assert_int_equal(value.tag, AM_VALUE_OBJ_ID); \
assert_non_null(value.obj_id); \
assert_int_equal(AMobjSize(group_state->doc, value.obj_id), 0); \
AMfree(res); \
}
static_void_test_AMmapPut(Bool, boolean, true)
static void test_AMmapPutBytes(void **state) {
static char const* const KEY = "Bytes";
static uint8_t const BYTES_VALUE[] = {INT8_MIN, INT8_MAX / 2, INT8_MAX};
static size_t const BYTES_SIZE = sizeof(BYTES_VALUE) / sizeof(uint8_t);
GroupState* group_state = *state;
AMresult* res = AMmapPutBytes(
group_state->doc,
AM_ROOT,
KEY,
BYTES_VALUE,
BYTES_SIZE
);
if (AMresultStatus(res) != AM_STATUS_OK) {
fail_msg("%s", AMerrorMessage(res));
}
assert_int_equal(AMresultSize(res), 0);
AMvalue value = AMresultValue(res);
assert_int_equal(value.tag, AM_VALUE_VOID);
AMfree(res);
res = AMmapGet(group_state->doc, AM_ROOT, KEY);
if (AMresultStatus(res) != AM_STATUS_OK) {
fail_msg("%s", AMerrorMessage(res));
}
assert_int_equal(AMresultSize(res), 1);
value = AMresultValue(res);
assert_int_equal(value.tag, AM_VALUE_BYTES);
assert_int_equal(value.bytes.count, BYTES_SIZE);
assert_memory_equal(value.bytes.src, BYTES_VALUE, BYTES_SIZE);
AMfree(res);
}
static_void_test_AMmapPut(Counter, counter, INT64_MAX)
static_void_test_AMmapPut(F64, f64, DBL_MAX)
static_void_test_AMmapPut(Int, int_, INT64_MAX)
static void test_AMmapPutNull(void **state) {
static char const* const KEY = "Null";
GroupState* group_state = *state;
AMresult* res = AMmapPutNull(group_state->doc, AM_ROOT, KEY);
if (AMresultStatus(res) != AM_STATUS_OK) {
fail_msg("%s", AMerrorMessage(res));
}
assert_int_equal(AMresultSize(res), 0);
AMvalue value = AMresultValue(res);
assert_int_equal(value.tag, AM_VALUE_VOID);
AMfree(res);
res = AMmapGet(group_state->doc, AM_ROOT, KEY);
if (AMresultStatus(res) != AM_STATUS_OK) {
fail_msg("%s", AMerrorMessage(res));
}
assert_int_equal(AMresultSize(res), 1);
value = AMresultValue(res);
assert_int_equal(value.tag, AM_VALUE_NULL);
AMfree(res);
}
static_void_test_AMmapPutObject(List)
static_void_test_AMmapPutObject(Map)
static_void_test_AMmapPutObject(Text)
static void test_AMmapPutStr(void **state) {
static char const* const KEY = "Str";
static char const* const STR_VALUE = "Hello, world!";
size_t const STR_LEN = strlen(STR_VALUE);
GroupState* group_state = *state;
AMresult* res = AMmapPutStr(
group_state->doc,
AM_ROOT,
KEY,
STR_VALUE
);
if (AMresultStatus(res) != AM_STATUS_OK) {
fail_msg("%s", AMerrorMessage(res));
}
assert_int_equal(AMresultSize(res), 0);
AMvalue value = AMresultValue(res);
assert_int_equal(value.tag, AM_VALUE_VOID);
AMfree(res);
res = AMmapGet(group_state->doc, AM_ROOT, KEY);
if (AMresultStatus(res) != AM_STATUS_OK) {
fail_msg("%s", AMerrorMessage(res));
}
assert_int_equal(AMresultSize(res), 1);
value = AMresultValue(res);
assert_int_equal(value.tag, AM_VALUE_STR);
assert_int_equal(strlen(value.str), STR_LEN);
assert_memory_equal(value.str, STR_VALUE, STR_LEN + 1);
AMfree(res);
}
static_void_test_AMmapPut(Timestamp, timestamp, INT64_MAX)
static_void_test_AMmapPut(Uint, uint, UINT64_MAX)
int run_map_tests(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_AMmapIncrement),
cmocka_unit_test(test_AMmapPut(Bool)),
cmocka_unit_test(test_AMmapPutBytes),
cmocka_unit_test(test_AMmapPut(Counter)),
cmocka_unit_test(test_AMmapPut(F64)),
cmocka_unit_test(test_AMmapPut(Int)),
cmocka_unit_test(test_AMmapPutNull),
cmocka_unit_test(test_AMmapPutObject(List)),
cmocka_unit_test(test_AMmapPutObject(Map)),
cmocka_unit_test(test_AMmapPutObject(Text)),
cmocka_unit_test(test_AMmapPutStr),
cmocka_unit_test(test_AMmapPut(Timestamp)),
cmocka_unit_test(test_AMmapPut(Uint)),
};
return cmocka_run_group_tests(tests, group_setup, group_teardown);
}

File diff suppressed because it is too large Load diff

857
automerge-cli/Cargo.lock generated Normal file
View file

@ -0,0 +1,857 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "adler"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "ansi_term"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
dependencies = [
"winapi",
]
[[package]]
name = "anyhow"
version = "1.0.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "159bb86af3a200e19a068f4224eae4c8bb2d0fa054c7e5d1cacd5cef95e684cd"
[[package]]
name = "atty"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
"hermit-abi",
"libc",
"winapi",
]
[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "automerge"
version = "0.1.0"
dependencies = [
"flate2",
"fxhash",
"hex",
"itertools",
"js-sys",
"leb128",
"nonzero_ext",
"rand",
"serde",
"sha2",
"smol_str",
"thiserror",
"tinyvec",
"tracing",
"unicode-segmentation",
"uuid",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "automerge-cli"
version = "0.1.0"
dependencies = [
"anyhow",
"atty",
"automerge",
"clap",
"colored_json",
"combine",
"duct",
"maplit",
"serde_json",
"thiserror",
"tracing-subscriber",
]
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "block-buffer"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324"
dependencies = [
"generic-array",
]
[[package]]
name = "bumpalo"
version = "3.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899"
[[package]]
name = "byteorder"
version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]]
name = "bytes"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clap"
version = "3.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ced1892c55c910c1219e98d6fc8d71f6bddba7905866ce740066d8bfea859312"
dependencies = [
"atty",
"bitflags",
"clap_derive",
"indexmap",
"lazy_static",
"os_str_bytes",
"strsim",
"termcolor",
"textwrap",
]
[[package]]
name = "clap_derive"
version = "3.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da95d038ede1a964ce99f49cbe27a7fb538d1da595e4b4f70b8c8f338d17bf16"
dependencies = [
"heck",
"proc-macro-error",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "colored_json"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd32eb54d016e203b7c2600e3a7802c75843a92e38ccc4869aefeca21771a64"
dependencies = [
"ansi_term",
"atty",
"libc",
"serde",
"serde_json",
]
[[package]]
name = "combine"
version = "4.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50b727aacc797f9fc28e355d21f34709ac4fc9adecfe470ad07b8f4464f53062"
dependencies = [
"bytes",
"memchr",
]
[[package]]
name = "cpufeatures"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469"
dependencies = [
"libc",
]
[[package]]
name = "crc32fast"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
dependencies = [
"cfg-if",
]
[[package]]
name = "crypto-common"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "digest"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506"
dependencies = [
"block-buffer",
"crypto-common",
]
[[package]]
name = "duct"
version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fc6a0a59ed0888e0041cf708e66357b7ae1a82f1c67247e1f93b5e0818f7d8d"
dependencies = [
"libc",
"once_cell",
"os_pipe",
"shared_child",
]
[[package]]
name = "either"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
[[package]]
name = "flate2"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e6988e897c1c9c485f43b47a529cef42fde0547f9d8d41a7062518f1d8fc53f"
dependencies = [
"cfg-if",
"crc32fast",
"libc",
"miniz_oxide",
]
[[package]]
name = "fxhash"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
dependencies = [
"byteorder",
]
[[package]]
name = "generic-array"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "getrandom"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d39cd93900197114fa1fcb7ae84ca742095eed9442088988ae74fa744e930e77"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi",
"wasm-bindgen",
]
[[package]]
name = "hashbrown"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
[[package]]
name = "heck"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
[[package]]
name = "hermit-abi"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
dependencies = [
"libc",
]
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "indexmap"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223"
dependencies = [
"autocfg",
"hashbrown",
]
[[package]]
name = "itertools"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35"
[[package]]
name = "js-sys"
version = "0.3.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a38fc24e30fd564ce974c02bf1d337caddff65be6cc4735a1f7eab22a7440f04"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[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.119"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bf2e165bb3457c8e098ea76f3e3bc9db55f87aa90d52d0e6be741470916aaa4"
[[package]]
name = "log"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
dependencies = [
"cfg-if",
]
[[package]]
name = "maplit"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
[[package]]
name = "memchr"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
[[package]]
name = "miniz_oxide"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b"
dependencies = [
"adler",
"autocfg",
]
[[package]]
name = "nonzero_ext"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44a1290799eababa63ea60af0cbc3f03363e328e58f32fb0294798ed3e85f444"
[[package]]
name = "once_cell"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5"
[[package]]
name = "os_pipe"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb233f06c2307e1f5ce2ecad9f8121cffbbee2c95428f44ea85222e460d0d213"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "os_str_bytes"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64"
dependencies = [
"memchr",
]
[[package]]
name = "pin-project-lite"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c"
[[package]]
name = "ppv-lite86"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
[[package]]
name = "proc-macro-error"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [
"proc-macro-error-attr",
"proc-macro2",
"quote",
"syn",
"version_check",
]
[[package]]
name = "proc-macro-error-attr"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [
"proc-macro2",
"quote",
"version_check",
]
[[package]]
name = "proc-macro2"
version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029"
dependencies = [
"unicode-xid",
]
[[package]]
name = "quote"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145"
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.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
dependencies = [
"getrandom",
]
[[package]]
name = "ryu"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f"
[[package]]
name = "serde"
version = "1.0.136"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.136"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "sha2"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "sharded-slab"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31"
dependencies = [
"lazy_static",
]
[[package]]
name = "shared_child"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6be9f7d5565b1483af3e72975e2dee33879b3b86bd48c0929fccf6585d79e65a"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "smallvec"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83"
[[package]]
name = "smol_str"
version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61d15c83e300cce35b7c8cd39ff567c1ef42dde6d4a1a38dbdbf9a59902261bd"
dependencies = [
"serde",
]
[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "syn"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b"
dependencies = [
"proc-macro2",
"quote",
"unicode-xid",
]
[[package]]
name = "termcolor"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
dependencies = [
"winapi-util",
]
[[package]]
name = "textwrap"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb"
[[package]]
name = "thiserror"
version = "1.0.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "thread_local"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180"
dependencies = [
"once_cell",
]
[[package]]
name = "tinyvec"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "tracing"
version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6c650a8ef0cd2dd93736f033d21cbd1224c5a967aa0c258d00fcf7dafef9b9f"
dependencies = [
"cfg-if",
"log",
"pin-project-lite",
"tracing-attributes",
"tracing-core",
]
[[package]]
name = "tracing-attributes"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8276d9a4a3a558d7b7ad5303ad50b53d58264641b82914b7ada36bd762e7a716"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tracing-core"
version = "0.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03cfcb51380632a72d3111cb8d3447a8d908e577d31beeac006f836383d29a23"
dependencies = [
"lazy_static",
"valuable",
]
[[package]]
name = "tracing-log"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6923477a48e41c1951f1999ef8bb5a3023eb723ceadafe78ffb65dc366761e3"
dependencies = [
"lazy_static",
"log",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e0ab7bdc962035a87fba73f3acca9b8a8d0034c2e6f60b84aeaaddddc155dce"
dependencies = [
"ansi_term",
"sharded-slab",
"smallvec",
"thread_local",
"tracing-core",
"tracing-log",
]
[[package]]
name = "typenum"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
[[package]]
name = "unicode-segmentation"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99"
[[package]]
name = "unicode-xid"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
[[package]]
name = "uuid"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
dependencies = [
"getrandom",
"serde",
]
[[package]]
name = "valuable"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]]
name = "version_check"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "wasi"
version = "0.10.2+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
[[package]]
name = "wasm-bindgen"
version = "0.2.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25f1af7423d8588a3d840681122e72e6a24ddbcb3f0ec385cac0d12d24256c06"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b21c0df030f5a177f3cba22e9bc4322695ec43e7257d865302900290bcdedca"
dependencies = [
"bumpalo",
"lazy_static",
"log",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f4203d69e40a52ee523b2529a773d5ffc1dc0071801c87b3d270b471b80ed01"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa8a30d46208db204854cadbb5d4baf5fcf8071ba5bf48190c3e59937962ebc"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d958d035c4438e28c70e4321a2911302f10135ce78a9c7834c0cab4123d06a2"
[[package]]
name = "web-sys"
version = "0.3.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c060b319f29dd25724f09a2ba1418f142f539b2be99fbf4d2d5a8f7330afb8eb"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
dependencies = [
"winapi",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"

View file

@ -13,18 +13,17 @@ bench = false
doc = false
[dependencies]
clap = {version = "~4", features = ["derive"]}
clap = {version = "~3.1", features = ["derive"]}
serde_json = "^1.0"
anyhow = "1.0"
atty = "^0.2"
thiserror = "^1.0"
combine = "^4.5"
maplit = "^1.0"
colored_json = "^2.1"
tracing-subscriber = "~0.3"
automerge = { path = "../automerge" }
is-terminal = "0.4.1"
termcolor = "1.1.3"
serde = "1.0.150"
[dev-dependencies]
duct = "^0.13"

View file

@ -1,8 +1,6 @@
use automerge as am;
use thiserror::Error;
use crate::{color_json::print_colored_json, SkipVerifyFlag};
#[derive(Error, Debug)]
pub enum ExamineError {
#[error("Error reading change file: {:?}", source)]
@ -22,18 +20,16 @@ pub enum ExamineError {
},
}
pub(crate) fn examine(
pub fn examine(
mut input: impl std::io::Read,
mut output: impl std::io::Write,
skip: SkipVerifyFlag,
is_tty: bool,
) -> Result<(), ExamineError> {
let mut buf: Vec<u8> = Vec::new();
input
.read_to_end(&mut buf)
.map_err(|e| ExamineError::ReadingChanges { source: e })?;
let doc = skip
.load(&buf)
let doc = am::Automerge::load(&buf)
.map_err(|e| ExamineError::ApplyingInitialChanges { source: e })?;
let uncompressed_changes: Vec<_> = doc
.get_changes(&[])
@ -43,7 +39,7 @@ pub(crate) fn examine(
.collect();
if is_tty {
let json_changes = serde_json::to_value(uncompressed_changes).unwrap();
print_colored_json(&json_changes).unwrap();
colored_json::write_colored_json(&json_changes, &mut output).unwrap();
writeln!(output).unwrap();
} else {
let json_changes = serde_json::to_string_pretty(&uncompressed_changes).unwrap();

View file

@ -1,8 +1,5 @@
use anyhow::Result;
use automerge as am;
use automerge::ReadDoc;
use crate::{color_json::print_colored_json, SkipVerifyFlag};
pub(crate) fn map_to_json(doc: &am::Automerge, obj: &am::ObjId) -> serde_json::Value {
let keys = doc.keys(obj);
@ -31,7 +28,7 @@ fn list_to_json(doc: &am::Automerge, obj: &am::ObjId) -> serde_json::Value {
let len = doc.length(obj);
let mut array = Vec::new();
for i in 0..len {
let val = doc.get(obj, i);
let val = doc.get(obj, i as usize);
match val {
Ok(Some((am::Value::Object(o), exid)))
if o == am::ObjType::Map || o == am::ObjType::Table =>
@ -53,13 +50,11 @@ fn list_to_json(doc: &am::Automerge, obj: &am::ObjId) -> serde_json::Value {
fn scalar_to_json(val: &am::ScalarValue) -> serde_json::Value {
match val {
am::ScalarValue::Str(s) => serde_json::Value::String(s.to_string()),
am::ScalarValue::Bytes(b) | am::ScalarValue::Unknown { bytes: b, .. } => {
serde_json::Value::Array(
am::ScalarValue::Bytes(b) => serde_json::Value::Array(
b.iter()
.map(|byte| serde_json::Value::Number((*byte).into()))
.collect(),
)
}
),
am::ScalarValue::Int(n) => serde_json::Value::Number((*n).into()),
am::ScalarValue::Uint(n) => serde_json::Value::Number((*n).into()),
am::ScalarValue::F64(n) => serde_json::Number::from_f64(*n)
@ -72,23 +67,22 @@ fn scalar_to_json(val: &am::ScalarValue) -> serde_json::Value {
}
}
fn get_state_json(input_data: Vec<u8>, skip: SkipVerifyFlag) -> Result<serde_json::Value> {
let doc = skip.load(&input_data).unwrap(); // FIXME
fn get_state_json(input_data: Vec<u8>) -> Result<serde_json::Value> {
let doc = am::Automerge::load(&input_data).unwrap(); // FIXME
Ok(map_to_json(&doc, &am::ObjId::Root))
}
pub(crate) fn export_json(
pub fn export_json(
mut changes_reader: impl std::io::Read,
mut writer: impl std::io::Write,
skip: SkipVerifyFlag,
is_tty: bool,
) -> Result<()> {
let mut input_data = vec![];
changes_reader.read_to_end(&mut input_data)?;
let state_json = get_state_json(input_data, skip)?;
let state_json = get_state_json(input_data)?;
if is_tty {
print_colored_json(&state_json).unwrap();
colored_json::write_colored_json(&state_json, &mut writer).unwrap();
writeln!(writer).unwrap();
} else {
writeln!(
@ -107,10 +101,7 @@ mod tests {
#[test]
fn cli_export_with_empty_input() {
assert_eq!(
get_state_json(vec![], Default::default()).unwrap(),
serde_json::json!({})
)
assert_eq!(get_state_json(vec![]).unwrap(), serde_json::json!({}))
}
#[test]
@ -124,7 +115,7 @@ mod tests {
let mut backend = initialize_from_json(&initial_state_json).unwrap();
let change_bytes = backend.save();
assert_eq!(
get_state_json(change_bytes, Default::default()).unwrap(),
get_state_json(change_bytes).unwrap(),
serde_json::json!({"sparrows": 15.0})
)
}
@ -151,7 +142,7 @@ mod tests {
*/
let change_bytes = backend.save();
assert_eq!(
get_state_json(change_bytes, Default::default()).unwrap(),
get_state_json(change_bytes).unwrap(),
serde_json::json!({
"birds": {
"wrens": 3.0,

View file

@ -1,15 +1,10 @@
use std::{fs::File, path::PathBuf, str::FromStr};
use anyhow::{anyhow, Result};
use clap::{
builder::{BoolishValueParser, TypedValueParser, ValueParserFactory},
Parser,
};
use is_terminal::IsTerminal;
use clap::Parser;
mod color_json;
//mod change;
mod examine;
mod examine_sync;
mod export;
mod import;
mod merge;
@ -21,50 +16,12 @@ struct Opts {
cmd: Command,
}
#[derive(clap::ValueEnum, Clone, Debug)]
#[derive(Debug)]
enum ExportFormat {
Json,
Toml,
}
#[derive(Copy, Clone, Default, Debug)]
pub(crate) struct SkipVerifyFlag(bool);
impl SkipVerifyFlag {
fn load(&self, buf: &[u8]) -> Result<automerge::Automerge, automerge::AutomergeError> {
if self.0 {
automerge::Automerge::load(buf)
} else {
automerge::Automerge::load_unverified_heads(buf)
}
}
}
#[derive(Clone)]
struct SkipVerifyFlagParser;
impl ValueParserFactory for SkipVerifyFlag {
type Parser = SkipVerifyFlagParser;
fn value_parser() -> Self::Parser {
SkipVerifyFlagParser
}
}
impl TypedValueParser for SkipVerifyFlagParser {
type Value = SkipVerifyFlag;
fn parse_ref(
&self,
cmd: &clap::Command,
arg: Option<&clap::Arg>,
value: &std::ffi::OsStr,
) -> Result<Self::Value, clap::Error> {
BoolishValueParser::new()
.parse_ref(cmd, arg, value)
.map(SkipVerifyFlag)
}
}
impl FromStr for ExportFormat {
type Err = anyhow::Error;
@ -86,15 +43,12 @@ enum Command {
format: ExportFormat,
/// Path that contains Automerge changes
#[clap(parse(from_os_str))]
changes_file: Option<PathBuf>,
/// The file to write to. If omitted assumes stdout
#[clap(long("out"), short('o'))]
#[clap(parse(from_os_str), long("out"), short('o'))]
output_file: Option<PathBuf>,
/// Whether to verify the head hashes of a compressed document
#[clap(long, action = clap::ArgAction::SetFalse)]
skip_verifying_heads: SkipVerifyFlag,
},
Import {
@ -102,37 +56,69 @@ enum Command {
#[clap(long, short, default_value = "json")]
format: ExportFormat,
#[clap(parse(from_os_str))]
input_file: Option<PathBuf>,
/// Path to write Automerge changes to
#[clap(long("out"), short('o'))]
#[clap(parse(from_os_str), long("out"), short('o'))]
changes_file: Option<PathBuf>,
},
/// Read an automerge document and print a JSON representation of the changes in it to stdout
Examine {
/// Read an automerge document from a file or stdin, perform a change on it and write a new
/// document to stdout or the specified output file.
Change {
/// The change script to perform. Change scripts have the form <command> <path> [<JSON value>].
/// The possible commands are 'set', 'insert', 'delete', and 'increment'.
///
/// Paths look like this: $["mapkey"][0]. They always lways start with a '$', then each
/// subsequent segment of the path is either a string in double quotes to index a key in a
/// map, or an integer index to address an array element.
///
/// Examples
///
/// ## set
///
/// > automerge change 'set $["someobject"] {"items": []}' somefile
///
/// ## insert
///
/// > automerge change 'insert $["someobject"]["items"][0] "item1"' somefile
///
/// ## increment
///
/// > automerge change 'increment $["mycounter"]'
///
/// ## delete
///
/// > automerge change 'delete $["someobject"]["items"]' somefile
script: String,
/// The file to change, if omitted will assume stdin
#[clap(parse(from_os_str))]
input_file: Option<PathBuf>,
skip_verifying_heads: SkipVerifyFlag,
/// Path to write Automerge changes to, if omitted will write to stdout
#[clap(parse(from_os_str), long("out"), short('o'))]
output_file: Option<PathBuf>,
},
/// Read an automerge sync messaage and print a JSON representation of it
ExamineSync { input_file: Option<PathBuf> },
/// Read an automerge document and print a JSON representation of the changes in it to stdout
Examine { input_file: Option<PathBuf> },
/// Read one or more automerge documents and output a merged, compacted version of them
Merge {
/// The file to write to. If omitted assumes stdout
#[clap(long("out"), short('o'))]
#[clap(parse(from_os_str), long("out"), short('o'))]
output_file: Option<PathBuf>,
/// The file(s) to compact. If empty assumes stdin
input: Vec<PathBuf>,
},
}
fn open_file_or_stdin(maybe_path: Option<PathBuf>) -> Result<Box<dyn std::io::Read>> {
if std::io::stdin().is_terminal() {
if atty::is(atty::Stream::Stdin) {
if let Some(path) = maybe_path {
Ok(Box::new(File::open(path).unwrap()))
Ok(Box::new(File::open(&path).unwrap()))
} else {
Err(anyhow!(
"Must provide file path if not providing input via stdin"
@ -144,9 +130,9 @@ fn open_file_or_stdin(maybe_path: Option<PathBuf>) -> Result<Box<dyn std::io::Re
}
fn create_file_or_stdout(maybe_path: Option<PathBuf>) -> Result<Box<dyn std::io::Write>> {
if std::io::stdout().is_terminal() {
if atty::is(atty::Stream::Stdout) {
if let Some(path) = maybe_path {
Ok(Box::new(File::create(path).unwrap()))
Ok(Box::new(File::create(&path).unwrap()))
} else {
Err(anyhow!("Must provide file path if not piping to stdout"))
}
@ -163,22 +149,16 @@ fn main() -> Result<()> {
changes_file,
format,
output_file,
skip_verifying_heads,
} => {
let output: Box<dyn std::io::Write> = if let Some(output_file) = output_file {
Box::new(File::create(output_file)?)
Box::new(File::create(&output_file)?)
} else {
Box::new(std::io::stdout())
};
match format {
ExportFormat::Json => {
let mut in_buffer = open_file_or_stdin(changes_file)?;
export::export_json(
&mut in_buffer,
output,
skip_verifying_heads,
std::io::stdout().is_terminal(),
)
export::export_json(&mut in_buffer, output, atty::is(atty::Stream::Stdout))
}
ExportFormat::Toml => unimplemented!(),
}
@ -195,30 +175,23 @@ fn main() -> Result<()> {
}
ExportFormat::Toml => unimplemented!(),
},
Command::Examine {
input_file,
skip_verifying_heads,
Command::Change { ..
//input_file,
//output_file,
//script,
} => {
unimplemented!()
/*
let in_buffer = open_file_or_stdin(input_file)?;
let mut out_buffer = create_file_or_stdout(output_file)?;
change::change(in_buffer, &mut out_buffer, script.as_str())
.map_err(|e| anyhow::format_err!("Unable to make changes: {:?}", e))
*/
}
Command::Examine { input_file } => {
let in_buffer = open_file_or_stdin(input_file)?;
let out_buffer = std::io::stdout();
match examine::examine(
in_buffer,
out_buffer,
skip_verifying_heads,
std::io::stdout().is_terminal(),
) {
Ok(()) => {}
Err(e) => {
eprintln!("Error: {:?}", e);
}
}
Ok(())
}
Command::ExamineSync { input_file } => {
let in_buffer = open_file_or_stdin(input_file)?;
let out_buffer = std::io::stdout();
match examine_sync::examine_sync(in_buffer, out_buffer, std::io::stdout().is_terminal())
{
match examine::examine(in_buffer, out_buffer, atty::is(atty::Stream::Stdout)) {
Ok(()) => {}
Err(e) => {
eprintln!("Error: {:?}", e);

2
automerge-js/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/node_modules
/yarn.lock

18
automerge-js/package.json Normal file
View file

@ -0,0 +1,18 @@
{
"name": "automerge-js",
"version": "0.1.0",
"main": "src/index.js",
"license": "MIT",
"scripts": {
"test": "mocha --bail --full-trace"
},
"devDependencies": {
"mocha": "^9.1.1"
},
"dependencies": {
"automerge-wasm": "file:../automerge-wasm",
"fast-sha256": "^1.3.0",
"pako": "^2.0.4",
"uuid": "^8.3"
}
}

View file

@ -0,0 +1,18 @@
// Properties of the document root object
//const OPTIONS = Symbol('_options') // object containing options passed to init()
//const CACHE = Symbol('_cache') // map from objectId to immutable object
const STATE = Symbol('_state') // object containing metadata about current state (e.g. sequence numbers)
const HEADS = Symbol('_heads') // object containing metadata about current state (e.g. sequence numbers)
const OBJECT_ID = Symbol('_objectId') // object containing metadata about current state (e.g. sequence numbers)
const READ_ONLY = Symbol('_readOnly') // object containing metadata about current state (e.g. sequence numbers)
const FROZEN = Symbol('_frozen') // object containing metadata about current state (e.g. sequence numbers)
// Properties of all Automerge objects
//const OBJECT_ID = Symbol('_objectId') // the object ID of the current object (string)
//const CONFLICTS = Symbol('_conflicts') // map or list (depending on object type) of conflicts
//const CHANGE = Symbol('_change') // the context object on proxy objects used in change callback
//const ELEM_IDS = Symbol('_elemIds') // list containing the element ID of each list element
module.exports = {
STATE, HEADS, OBJECT_ID, READ_ONLY, FROZEN
}

View file

@ -1,16 +1,12 @@
import { Automerge, type ObjID, type Prop } from "@automerge/automerge-wasm"
import { COUNTER } from "./constants"
/**
* The most basic CRDT: an integer value that can be changed only by
* incrementing and decrementing. Since addition of integers is commutative,
* the value trivially converges.
*/
export class Counter {
value: number
constructor(value?: number) {
class Counter {
constructor(value) {
this.value = value || 0
Reflect.defineProperty(this, COUNTER, { value: true })
Object.freeze(this)
}
/**
@ -21,7 +17,7 @@ export class Counter {
* concatenating it with another string, as in `x + ''`.
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/valueOf
*/
valueOf(): number {
valueOf() {
return this.value
}
@ -30,7 +26,7 @@ export class Counter {
* this method is called e.g. when you do `['value: ', x].join('')` or when
* you use string interpolation: `value: ${x}`.
*/
toString(): string {
toString() {
return this.valueOf().toString()
}
@ -38,7 +34,7 @@ export class Counter {
* Returns the counter value, so that a JSON serialization of an Automerge
* document represents the counter simply as an integer.
*/
toJSON(): number {
toJSON() {
return this.value
}
}
@ -48,31 +44,12 @@ export class Counter {
* callback.
*/
class WriteableCounter extends Counter {
context: Automerge
path: Prop[]
objectId: ObjID
key: Prop
constructor(
value: number,
context: Automerge,
path: Prop[],
objectId: ObjID,
key: Prop
) {
super(value)
this.context = context
this.path = path
this.objectId = objectId
this.key = key
}
/**
* Increases the value of the counter by `delta`. If `delta` is not given,
* increases the value of the counter by 1.
*/
increment(delta: number): number {
delta = typeof delta === "number" ? delta : 1
increment(delta) {
delta = typeof delta === 'number' ? delta : 1
this.context.increment(this.objectId, this.key, delta)
this.value += delta
return this.value
@ -82,8 +59,8 @@ class WriteableCounter extends Counter {
* Decreases the value of the counter by `delta`. If `delta` is not given,
* decreases the value of the counter by 1.
*/
decrement(delta: number): number {
return this.increment(typeof delta === "number" ? -delta : -1)
decrement(delta) {
return this.increment(typeof delta === 'number' ? -delta : -1)
}
}
@ -94,14 +71,14 @@ class WriteableCounter extends Counter {
* the property name (key in map, or index in list) where the counter is
* located.
*/
export function getWriteableCounter(
value: number,
context: Automerge,
path: Prop[],
objectId: ObjID,
key: Prop
): WriteableCounter {
return new WriteableCounter(value, context, path, objectId, key)
function getWriteableCounter(value, context, path, objectId, key) {
const instance = Object.create(WriteableCounter.prototype)
instance.value = value
instance.context = context
instance.path = path
instance.objectId = objectId
instance.key = key
return instance
}
//module.exports = { Counter, getWriteableCounter }
module.exports = { Counter, getWriteableCounter }

372
automerge-js/src/index.js Normal file
View file

@ -0,0 +1,372 @@
const AutomergeWASM = require("automerge-wasm")
const uuid = require('./uuid')
let { rootProxy, listProxy, textProxy, mapProxy } = require("./proxies")
let { Counter } = require("./counter")
let { Text } = require("./text")
let { Int, Uint, Float64 } = require("./numbers")
let { STATE, HEADS, OBJECT_ID, READ_ONLY, FROZEN } = require("./constants")
function init(actor) {
if (typeof actor != 'string') {
actor = null
}
const state = AutomergeWASM.create(actor)
return rootProxy(state, true);
}
function clone(doc) {
const state = doc[STATE].clone()
return rootProxy(state, true);
}
function free(doc) {
return doc[STATE].free()
}
function from(data, actor) {
let doc1 = init(actor)
let doc2 = change(doc1, (d) => Object.assign(d, data))
return doc2
}
function change(doc, options, callback) {
if (callback === undefined) {
// FIXME implement options
callback = options
options = {}
}
if (typeof options === "string") {
options = { message: options }
}
if (doc === undefined || doc[STATE] === undefined || doc[OBJECT_ID] !== "_root") {
throw new RangeError("must be the document root");
}
if (doc[FROZEN] === true) {
throw new RangeError("Attempting to use an outdated Automerge document")
}
if (!!doc[HEADS] === true) {
throw new RangeError("Attempting to change an out of date document");
}
if (doc[READ_ONLY] === false) {
throw new RangeError("Calls to Automerge.change cannot be nested")
}
const state = doc[STATE]
const heads = state.getHeads()
try {
doc[HEADS] = heads
doc[FROZEN] = true
let root = rootProxy(state);
callback(root)
if (state.pendingOps() === 0) {
doc[FROZEN] = false
doc[HEADS] = undefined
return doc
} else {
state.commit(options.message, options.time)
return rootProxy(state, true);
}
} catch (e) {
//console.log("ERROR: ",e)
doc[FROZEN] = false
doc[HEADS] = undefined
state.rollback()
throw e
}
}
function emptyChange(doc, options) {
if (options === undefined) {
options = {}
}
if (typeof options === "string") {
options = { message: options }
}
if (doc === undefined || doc[STATE] === undefined || doc[OBJECT_ID] !== "_root") {
throw new RangeError("must be the document root");
}
if (doc[FROZEN] === true) {
throw new RangeError("Attempting to use an outdated Automerge document")
}
if (doc[READ_ONLY] === false) {
throw new RangeError("Calls to Automerge.change cannot be nested")
}
const state = doc[STATE]
state.commit(options.message, options.time)
return rootProxy(state, true);
}
function load(data, actor) {
const state = AutomergeWASM.load(data, actor)
return rootProxy(state, true);
}
function save(doc) {
const state = doc[STATE]
return state.save()
}
function merge(local, remote) {
if (local[HEADS] === true) {
throw new RangeError("Attempting to change an out of date document");
}
const localState = local[STATE]
const heads = localState.getHeads()
const remoteState = remote[STATE]
const changes = localState.getChangesAdded(remoteState)
localState.applyChanges(changes)
local[HEADS] = heads
return rootProxy(localState, true)
}
function getActorId(doc) {
const state = doc[STATE]
return state.getActorId()
}
function conflictAt(context, objectId, prop) {
let values = context.getAll(objectId, prop)
if (values.length <= 1) {
return
}
let result = {}
for (const conflict of values) {
const datatype = conflict[0]
const value = conflict[1]
switch (datatype) {
case "map":
result[value] = mapProxy(context, value, [ prop ], true)
break;
case "list":
result[value] = listProxy(context, value, [ prop ], true)
break;
case "text":
result[value] = textProxy(context, value, [ prop ], true)
break;
//case "table":
//case "cursor":
case "str":
case "uint":
case "int":
case "f64":
case "boolean":
case "bytes":
case "null":
result[conflict[2]] = value
break;
case "counter":
result[conflict[2]] = new Counter(value)
break;
case "timestamp":
result[conflict[2]] = new Date(value)
break;
default:
throw RangeError(`datatype ${datatype} unimplemented`)
}
}
return result
}
function getConflicts(doc, prop) {
const state = doc[STATE]
const objectId = doc[OBJECT_ID]
return conflictAt(state, objectId, prop)
}
function getLastLocalChange(doc) {
const state = doc[STATE]
try {
return state.getLastLocalChange()
} catch (e) {
return
}
}
function getObjectId(doc) {
return doc[OBJECT_ID]
}
function getChanges(oldState, newState) {
const o = oldState[STATE]
const n = newState[STATE]
const heads = oldState[HEADS]
return n.getChanges(heads || o.getHeads())
}
function getAllChanges(doc) {
const state = doc[STATE]
return state.getChanges([])
}
function applyChanges(doc, changes) {
if (doc === undefined || doc[STATE] === undefined || doc[OBJECT_ID] !== "_root") {
throw new RangeError("must be the document root");
}
if (doc[FROZEN] === true) {
throw new RangeError("Attempting to use an outdated Automerge document")
}
if (doc[READ_ONLY] === false) {
throw new RangeError("Calls to Automerge.change cannot be nested")
}
const state = doc[STATE]
const heads = state.getHeads()
state.applyChanges(changes)
doc[HEADS] = heads
return [rootProxy(state, true)];
}
function getHistory(doc) {
const actor = getActorId(doc)
const history = getAllChanges(doc)
return history.map((change, index) => ({
get change () {
return decodeChange(change)
},
get snapshot () {
const [state] = applyChanges(init(), history.slice(0, index + 1))
return state
}
})
)
}
function equals() {
if (!isObject(val1) || !isObject(val2)) return val1 === val2
const keys1 = Object.keys(val1).sort(), keys2 = Object.keys(val2).sort()
if (keys1.length !== keys2.length) return false
for (let i = 0; i < keys1.length; i++) {
if (keys1[i] !== keys2[i]) return false
if (!equals(val1[keys1[i]], val2[keys2[i]])) return false
}
return true
}
function encodeSyncMessage(msg) {
return AutomergeWASM.encodeSyncMessage(msg)
}
function decodeSyncMessage(msg) {
return AutomergeWASM.decodeSyncMessage(msg)
}
function encodeSyncState(state) {
return AutomergeWASM.encodeSyncState(AutomergeWASM.importSyncState(state))
}
function decodeSyncState(state) {
return AutomergeWASM.exportSyncState(AutomergeWASM.decodeSyncState(state))
}
function generateSyncMessage(doc, inState) {
const state = doc[STATE]
const syncState = AutomergeWASM.importSyncState(inState)
const message = state.generateSyncMessage(syncState)
const outState = AutomergeWASM.exportSyncState(syncState)
return [ outState, message ]
}
function receiveSyncMessage(doc, inState, message) {
const syncState = AutomergeWASM.importSyncState(inState)
if (doc === undefined || doc[STATE] === undefined || doc[OBJECT_ID] !== "_root") {
throw new RangeError("must be the document root");
}
if (doc[FROZEN] === true) {
throw new RangeError("Attempting to use an outdated Automerge document")
}
if (!!doc[HEADS] === true) {
throw new RangeError("Attempting to change an out of date document");
}
if (doc[READ_ONLY] === false) {
throw new RangeError("Calls to Automerge.change cannot be nested")
}
const state = doc[STATE]
const heads = state.getHeads()
state.receiveSyncMessage(syncState, message)
const outState = AutomergeWASM.exportSyncState(syncState)
doc[HEADS] = heads
return [rootProxy(state, true), outState, null];
}
function initSyncState() {
return AutomergeWASM.exportSyncState(AutomergeWASM.initSyncState(change))
}
function encodeChange(change) {
return AutomergeWASM.encodeChange(change)
}
function decodeChange(data) {
return AutomergeWASM.decodeChange(data)
}
function encodeSyncMessage(change) {
return AutomergeWASM.encodeSyncMessage(change)
}
function decodeSyncMessage(data) {
return AutomergeWASM.decodeSyncMessage(data)
}
function getMissingDeps(doc, heads) {
const state = doc[STATE]
return state.getMissingDeps(heads)
}
function getHeads(doc) {
const state = doc[STATE]
return doc[HEADS] || state.getHeads()
}
function dump(doc) {
const state = doc[STATE]
state.dump()
}
function toJS(doc) {
if (typeof doc === "object") {
if (doc instanceof Uint8Array) {
return doc
}
if (doc === null) {
return doc
}
if (doc instanceof Array) {
return doc.map((a) => toJS(a))
}
if (doc instanceof Text) {
return doc.map((a) => toJS(a))
}
let tmp = {}
for (index in doc) {
tmp[index] = toJS(doc[index])
}
return tmp
} else {
return doc
}
}
module.exports = {
init, from, change, emptyChange, clone, free,
load, save, merge, getChanges, getAllChanges, applyChanges,
getLastLocalChange, getObjectId, getActorId, getConflicts,
encodeChange, decodeChange, equals, getHistory, getHeads, uuid,
generateSyncMessage, receiveSyncMessage, initSyncState,
decodeSyncMessage, encodeSyncMessage, decodeSyncState, encodeSyncState,
getMissingDeps,
dump, Text, Counter, Int, Uint, Float64, toJS,
}
// depricated
// Frontend, setDefaultBackend, Backend
// more...
/*
for (let name of ['getObjectId', 'getObjectById',
'setActorId',
'Text', 'Table', 'Counter', 'Observable' ]) {
module.exports[name] = Frontend[name]
}
*/

View file

@ -0,0 +1,33 @@
// Convience classes to allow users to stricly specify the number type they want
class Int {
constructor(value) {
if (!(Number.isInteger(value) && value <= Number.MAX_SAFE_INTEGER && value >= Number.MIN_SAFE_INTEGER)) {
throw new RangeError(`Value ${value} cannot be a uint`)
}
this.value = value
Object.freeze(this)
}
}
class Uint {
constructor(value) {
if (!(Number.isInteger(value) && value <= Number.MAX_SAFE_INTEGER && value >= 0)) {
throw new RangeError(`Value ${value} cannot be a uint`)
}
this.value = value
Object.freeze(this)
}
}
class Float64 {
constructor(value) {
if (typeof value !== 'number') {
throw new RangeError(`Value ${value} cannot be a float64`)
}
this.value = value || 0.0
Object.freeze(this)
}
}
module.exports = { Int, Uint, Float64 }

617
automerge-js/src/proxies.js Normal file
View file

@ -0,0 +1,617 @@
const AutomergeWASM = require("automerge-wasm")
const { Int, Uint, Float64 } = require("./numbers");
const { Counter, getWriteableCounter } = require("./counter");
const { Text } = require("./text");
const { STATE, HEADS, FROZEN, OBJECT_ID, READ_ONLY } = require("./constants")
function parseListIndex(key) {
if (typeof key === 'string' && /^[0-9]+$/.test(key)) key = parseInt(key, 10)
if (typeof key !== 'number') {
// throw new TypeError('A list index must be a number, but you passed ' + JSON.stringify(key))
return key
}
if (key < 0 || isNaN(key) || key === Infinity || key === -Infinity) {
throw new RangeError('A list index must be positive, but you passed ' + key)
}
return key
}
function valueAt(target, prop) {
const { context, objectId, path, readonly, heads} = target
let value = context.get(objectId, prop, heads)
if (value === undefined) {
return
}
const datatype = value[0]
const val = value[1]
switch (datatype) {
case undefined: return;
case "map": return mapProxy(context, val, [ ... path, prop ], readonly, heads);
case "list": return listProxy(context, val, [ ... path, prop ], readonly, heads);
case "text": return textProxy(context, val, [ ... path, prop ], readonly, heads);
//case "table":
//case "cursor":
case "str": return val;
case "uint": return val;
case "int": return val;
case "f64": return val;
case "boolean": return val;
case "null": return null;
case "bytes": return val;
case "timestamp": return val;
case "counter": {
if (readonly) {
return new Counter(val);
} else {
return getWriteableCounter(val, context, path, objectId, prop)
}
}
default:
throw RangeError(`datatype ${datatype} unimplemented`)
}
}
function import_value(value) {
switch (typeof value) {
case 'object':
if (value == null) {
return [ null, "null"]
} else if (value instanceof Uint) {
return [ value.value, "uint" ]
} else if (value instanceof Int) {
return [ value.value, "int" ]
} else if (value instanceof Float64) {
return [ value.value, "f64" ]
} else if (value instanceof Counter) {
return [ value.value, "counter" ]
} else if (value instanceof Date) {
return [ value.getTime(), "timestamp" ]
} else if (value instanceof Uint8Array) {
return [ value, "bytes" ]
} else if (value instanceof Array) {
return [ value, "list" ]
} else if (value instanceof Text) {
return [ value, "text" ]
} else if (value[OBJECT_ID]) {
throw new RangeError('Cannot create a reference to an existing document object')
} else {
return [ value, "map" ]
}
break;
case 'boolean':
return [ value, "boolean" ]
case 'number':
if (Number.isInteger(value)) {
return [ value, "int" ]
} else {
return [ value, "f64" ]
}
break;
case 'string':
return [ value ]
break;
default:
throw new RangeError(`Unsupported type of value: ${typeof value}`)
}
}
const MapHandler = {
get (target, key) {
const { context, objectId, path, readonly, frozen, heads, cache } = target
if (key === Symbol.toStringTag) { return target[Symbol.toStringTag] }
if (key === OBJECT_ID) return objectId
if (key === READ_ONLY) return readonly
if (key === FROZEN) return frozen
if (key === HEADS) return heads
if (key === STATE) return context;
if (!cache[key]) {
cache[key] = valueAt(target, key)
}
return cache[key]
},
set (target, key, val) {
let { context, objectId, path, readonly, frozen} = target
target.cache = {} // reset cache on set
if (val && val[OBJECT_ID]) {
throw new RangeError('Cannot create a reference to an existing document object')
}
if (key === FROZEN) {
target.frozen = val
return
}
if (key === HEADS) {
target.heads = val
return
}
let [ value, datatype ] = import_value(val)
if (frozen) {
throw new RangeError("Attempting to use an outdated Automerge document")
}
if (readonly) {
throw new RangeError(`Object property "${key}" cannot be modified`)
}
switch (datatype) {
case "list":
const list = context.putObject(objectId, key, [])
const proxyList = listProxy(context, list, [ ... path, key ], readonly );
for (let i = 0; i < value.length; i++) {
proxyList[i] = value[i]
}
break;
case "text":
const text = context.putObject(objectId, key, "", "text")
const proxyText = textProxy(context, text, [ ... path, key ], readonly );
for (let i = 0; i < value.length; i++) {
proxyText[i] = value.get(i)
}
break;
case "map":
const map = context.putObject(objectId, key, {})
const proxyMap = mapProxy(context, map, [ ... path, key ], readonly );
for (const key in value) {
proxyMap[key] = value[key]
}
break;
default:
context.put(objectId, key, value, datatype)
}
return true
},
deleteProperty (target, key) {
const { context, objectId, path, readonly, frozen } = target
target.cache = {} // reset cache on delete
if (readonly) {
throw new RangeError(`Object property "${key}" cannot be modified`)
}
context.delete(objectId, key)
return true
},
has (target, key) {
const value = this.get(target, key)
return value !== undefined
},
getOwnPropertyDescriptor (target, key) {
const { context, objectId } = target
const value = this.get(target, key)
if (typeof value !== 'undefined') {
return {
configurable: true, enumerable: true, value
}
}
},
ownKeys (target) {
const { context, objectId, heads} = target
return context.keys(objectId, heads)
},
}
const ListHandler = {
get (target, index) {
const {context, objectId, path, readonly, frozen, heads } = target
index = parseListIndex(index)
if (index === Symbol.hasInstance) { return (instance) => { return [].has(instance) } }
if (index === Symbol.toStringTag) { return target[Symbol.toStringTag] }
if (index === OBJECT_ID) return objectId
if (index === READ_ONLY) return readonly
if (index === FROZEN) return frozen
if (index === HEADS) return heads
if (index === STATE) return context;
if (index === 'length') return context.length(objectId, heads);
if (index === Symbol.iterator) {
let i = 0;
return function *() {
// FIXME - ugly
let value = valueAt(target, i)
while (value !== undefined) {
yield value
i += 1
value = valueAt(target, i)
}
}
}
if (typeof index === 'number') {
return valueAt(target, index)
} else {
return listMethods(target)[index]
}
},
set (target, index, val) {
let {context, objectId, path, readonly, frozen } = target
index = parseListIndex(index)
if (val && val[OBJECT_ID]) {
throw new RangeError('Cannot create a reference to an existing document object')
}
if (index === FROZEN) {
target.frozen = val
return
}
if (index === HEADS) {
target.heads = val
return
}
if (typeof index == "string") {
throw new RangeError('list index must be a number')
}
const [ value, datatype] = import_value(val)
if (frozen) {
throw new RangeError("Attempting to use an outdated Automerge document")
}
if (readonly) {
throw new RangeError(`Object property "${index}" cannot be modified`)
}
switch (datatype) {
case "list":
let list
if (index >= context.length(objectId)) {
list = context.insertObject(objectId, index, [])
} else {
list = context.putObject(objectId, index, [])
}
const proxyList = listProxy(context, list, [ ... path, index ], readonly);
proxyList.splice(0,0,...value)
break;
case "text":
let text
if (index >= context.length(objectId)) {
text = context.insertObject(objectId, index, "", "text")
} else {
text = context.putObject(objectId, index, "", "text")
}
const proxyText = textProxy(context, text, [ ... path, index ], readonly);
proxyText.splice(0,0,...value)
break;
case "map":
let map
if (index >= context.length(objectId)) {
map = context.insertObject(objectId, index, {})
} else {
map = context.putObject(objectId, index, {})
}
const proxyMap = mapProxy(context, map, [ ... path, index ], readonly);
for (const key in value) {
proxyMap[key] = value[key]
}
break;
default:
if (index >= context.length(objectId)) {
context.insert(objectId, index, value, datatype)
} else {
context.put(objectId, index, value, datatype)
}
}
return true
},
deleteProperty (target, index) {
const {context, objectId} = target
index = parseListIndex(index)
if (context.get(objectId, index)[0] == "counter") {
throw new TypeError('Unsupported operation: deleting a counter from a list')
}
context.delete(objectId, index)
return true
},
has (target, index) {
const {context, objectId, heads} = target
index = parseListIndex(index)
if (typeof index === 'number') {
return index < context.length(objectId, heads)
}
return index === 'length'
},
getOwnPropertyDescriptor (target, index) {
const {context, objectId, path, readonly, frozen, heads} = target
if (index === 'length') return {writable: true, value: context.length(objectId, heads) }
if (index === OBJECT_ID) return {configurable: false, enumerable: false, value: objectId}
index = parseListIndex(index)
let value = valueAt(target, index)
return { configurable: true, enumerable: true, value }
},
getPrototypeOf(target) { return Object.getPrototypeOf([]) },
ownKeys (target) {
const {context, objectId, heads } = target
let keys = []
// uncommenting this causes assert.deepEqual() to fail when comparing to a pojo array
// but not uncommenting it causes for (i in list) {} to not enumerate values properly
//for (let i = 0; i < target.context.length(objectId, heads); i++) { keys.push(i.toString()) }
keys.push("length");
return keys
}
}
const TextHandler = Object.assign({}, ListHandler, {
get (target, index) {
// FIXME this is a one line change from ListHandler.get()
const {context, objectId, path, readonly, frozen, heads } = target
index = parseListIndex(index)
if (index === Symbol.toStringTag) { return target[Symbol.toStringTag] }
if (index === Symbol.hasInstance) { return (instance) => { return [].has(instance) } }
if (index === OBJECT_ID) return objectId
if (index === READ_ONLY) return readonly
if (index === FROZEN) return frozen
if (index === HEADS) return heads
if (index === STATE) return context;
if (index === 'length') return context.length(objectId, heads);
if (index === Symbol.iterator) {
let i = 0;
return function *() {
let value = valueAt(target, i)
while (value !== undefined) {
yield value
i += 1
value = valueAt(target, i)
}
}
}
if (typeof index === 'number') {
return valueAt(target, index)
} else {
return textMethods(target)[index] || listMethods(target)[index]
}
},
getPrototypeOf(target) {
return Object.getPrototypeOf(new Text())
},
})
function mapProxy(context, objectId, path, readonly, heads) {
return new Proxy({context, objectId, path, readonly: !!readonly, frozen: false, heads, cache: {}}, MapHandler)
}
function listProxy(context, objectId, path, readonly, heads) {
let target = []
Object.assign(target, {context, objectId, path, readonly: !!readonly, frozen: false, heads, cache: {}})
return new Proxy(target, ListHandler)
}
function textProxy(context, objectId, path, readonly, heads) {
let target = []
Object.assign(target, {context, objectId, path, readonly: !!readonly, frozen: false, heads, cache: {}})
return new Proxy(target, TextHandler)
}
function rootProxy(context, readonly) {
return mapProxy(context, "_root", [], readonly)
}
function listMethods(target) {
const {context, objectId, path, readonly, frozen, heads} = target
const methods = {
deleteAt(index, numDelete) {
if (typeof numDelete === 'number') {
context.splice(objectId, index, numDelete)
} else {
context.delete(objectId, index)
}
return this
},
fill(val, start, end) {
// FIXME
let list = context.getObject(objectId)
let [value, datatype] = valueAt(target, index)
for (let index = parseListIndex(start || 0); index < parseListIndex(end || list.length); index++) {
context.put(objectId, index, value, datatype)
}
return this
},
indexOf(o, start = 0) {
// FIXME
const id = o[OBJECT_ID]
if (id) {
const list = context.getObject(objectId)
for (let index = start; index < list.length; index++) {
if (list[index][OBJECT_ID] === id) {
return index
}
}
return -1
} else {
return context.indexOf(objectId, o, start)
}
},
insertAt(index, ...values) {
this.splice(index, 0, ...values)
return this
},
pop() {
let length = context.length(objectId)
if (length == 0) {
return undefined
}
let last = valueAt(target, length - 1)
context.delete(objectId, length - 1)
return last
},
push(...values) {
let len = context.length(objectId)
this.splice(len, 0, ...values)
return context.length(objectId)
},
shift() {
if (context.length(objectId) == 0) return
const first = valueAt(target, 0)
context.delete(objectId, 0)
return first
},
splice(index, del, ...vals) {
index = parseListIndex(index)
del = parseListIndex(del)
for (let val of vals) {
if (val && val[OBJECT_ID]) {
throw new RangeError('Cannot create a reference to an existing document object')
}
}
if (frozen) {
throw new RangeError("Attempting to use an outdated Automerge document")
}
if (readonly) {
throw new RangeError("Sequence object cannot be modified outside of a change block")
}
let result = []
for (let i = 0; i < del; i++) {
let value = valueAt(target, index)
result.push(value)
context.delete(objectId, index)
}
const values = vals.map((val) => import_value(val))
for (let [value,datatype] of values) {
switch (datatype) {
case "list":
const list = context.insertObject(objectId, index, [])
const proxyList = listProxy(context, list, [ ... path, index ], readonly);
proxyList.splice(0,0,...value)
break;
case "text":
const text = context.insertObject(objectId, index, "", "text")
const proxyText = textProxy(context, text, [ ... path, index ], readonly);
proxyText.splice(0,0,...value)
break;
case "map":
const map = context.insertObject(objectId, index, {})
const proxyMap = mapProxy(context, map, [ ... path, index ], readonly);
for (const key in value) {
proxyMap[key] = value[key]
}
break;
default:
context.insert(objectId, index, value, datatype)
}
index += 1
}
return result
},
unshift(...values) {
this.splice(0, 0, ...values)
return context.length(objectId)
},
entries() {
let i = 0;
const iterator = {
next: () => {
let value = valueAt(target, i)
if (value === undefined) {
return { value: undefined, done: true }
} else {
return { value: [ i, value ], done: false }
}
}
}
return iterator
},
keys() {
let i = 0;
let len = context.length(objectId, heads)
const iterator = {
next: () => {
let value = undefined
if (i < len) { value = i; i++ }
return { value, done: true }
}
}
return iterator
},
values() {
let i = 0;
const iterator = {
next: () => {
let value = valueAt(target, i)
if (value === undefined) {
return { value: undefined, done: true }
} else {
return { value, done: false }
}
}
}
return iterator
}
}
// Read-only methods that can delegate to the JavaScript built-in implementations
// FIXME - super slow
for (let method of ['concat', 'every', 'filter', 'find', 'findIndex', 'forEach', 'includes',
'join', 'lastIndexOf', 'map', 'reduce', 'reduceRight',
'slice', 'some', 'toLocaleString', 'toString']) {
methods[method] = (...args) => {
const list = []
while (true) {
let value = valueAt(target, list.length)
if (value == undefined) {
break
}
list.push(value)
}
return list[method](...args)
}
}
return methods
}
function textMethods(target) {
const {context, objectId, path, readonly, frozen, heads } = target
const methods = {
set (index, value) {
return this[index] = value
},
get (index) {
return this[index]
},
toString () {
return context.text(objectId, heads).replace(//g,'')
},
toSpans () {
let spans = []
let chars = ''
let length = this.length
for (let i = 0; i < length; i++) {
const value = this[i]
if (typeof value === 'string') {
chars += value
} else {
if (chars.length > 0) {
spans.push(chars)
chars = ''
}
spans.push(value)
}
}
if (chars.length > 0) {
spans.push(chars)
}
return spans
},
toJSON () {
return this.toString()
}
}
return methods
}
module.exports = { rootProxy, textProxy, listProxy, mapProxy, MapHandler, ListHandler, TextHandler }

View file

@ -16,15 +16,11 @@
* last sync to disk), and we fall back to sending the entire document in this case.
*/
const Backend = null //require('./backend')
const {
hexStringToBytes,
bytesToHexString,
Encoder,
Decoder,
} = require("./encoding")
const { decodeChangeMeta } = require("./columnar")
const { copyObject } = require("./common")
//const Backend = require('./backend')
const Backend = {} //require('./backend')
const { hexStringToBytes, bytesToHexString, Encoder, Decoder } = require('./encoding')
const { decodeChangeMeta } = require('./columnar')
const { copyObject } = require('../src/common')
const HASH_SIZE = 32 // 256 bits = 32 bytes
const MESSAGE_TYPE_SYNC = 0x42 // first byte of a sync message, for identification
@ -33,8 +29,7 @@ const PEER_STATE_TYPE = 0x43 // first byte of an encoded peer state, for identif
// These constants correspond to a 1% false positive rate. The values can be changed without
// breaking compatibility of the network protocol, since the parameters used for a particular
// Bloom filter are encoded in the wire format.
const BITS_PER_ENTRY = 10,
NUM_PROBES = 7
const BITS_PER_ENTRY = 10, NUM_PROBES = 7
/**
* A Bloom filter implementation that can be serialised to a byte array for transmission
@ -48,9 +43,7 @@ class BloomFilter {
this.numEntries = arg.length
this.numBitsPerEntry = BITS_PER_ENTRY
this.numProbes = NUM_PROBES
this.bits = new Uint8Array(
Math.ceil((this.numEntries * this.numBitsPerEntry) / 8)
)
this.bits = new Uint8Array(Math.ceil(this.numEntries * this.numBitsPerEntry / 8))
for (let hash of arg) this.addHash(hash)
} else if (arg instanceof Uint8Array) {
if (arg.byteLength === 0) {
@ -63,12 +56,10 @@ class BloomFilter {
this.numEntries = decoder.readUint32()
this.numBitsPerEntry = decoder.readUint32()
this.numProbes = decoder.readUint32()
this.bits = decoder.readRawBytes(
Math.ceil((this.numEntries * this.numBitsPerEntry) / 8)
)
this.bits = decoder.readRawBytes(Math.ceil(this.numEntries * this.numBitsPerEntry / 8))
}
} else {
throw new TypeError("invalid argument")
throw new TypeError('invalid argument')
}
}
@ -96,32 +87,12 @@ class BloomFilter {
* http://www.ccis.northeastern.edu/home/pete/pub/bloom-filters-verification.pdf
*/
getProbes(hash) {
const hashBytes = hexStringToBytes(hash),
modulo = 8 * this.bits.byteLength
if (hashBytes.byteLength !== 32)
throw new RangeError(`Not a 256-bit hash: ${hash}`)
const hashBytes = hexStringToBytes(hash), modulo = 8 * this.bits.byteLength
if (hashBytes.byteLength !== 32) throw new RangeError(`Not a 256-bit hash: ${hash}`)
// on the next three lines, the right shift means interpret value as unsigned
let x =
((hashBytes[0] |
(hashBytes[1] << 8) |
(hashBytes[2] << 16) |
(hashBytes[3] << 24)) >>>
0) %
modulo
let y =
((hashBytes[4] |
(hashBytes[5] << 8) |
(hashBytes[6] << 16) |
(hashBytes[7] << 24)) >>>
0) %
modulo
let z =
((hashBytes[8] |
(hashBytes[9] << 8) |
(hashBytes[10] << 16) |
(hashBytes[11] << 24)) >>>
0) %
modulo
let x = ((hashBytes[0] | hashBytes[1] << 8 | hashBytes[2] << 16 | hashBytes[3] << 24) >>> 0) % modulo
let y = ((hashBytes[4] | hashBytes[5] << 8 | hashBytes[6] << 16 | hashBytes[7] << 24) >>> 0) % modulo
let z = ((hashBytes[8] | hashBytes[9] << 8 | hashBytes[10] << 16 | hashBytes[11] << 24) >>> 0) % modulo
const probes = [x]
for (let i = 1; i < this.numProbes; i++) {
x = (x + y) % modulo
@ -158,14 +129,12 @@ class BloomFilter {
* Encodes a sorted array of SHA-256 hashes (as hexadecimal strings) into a byte array.
*/
function encodeHashes(encoder, hashes) {
if (!Array.isArray(hashes)) throw new TypeError("hashes must be an array")
if (!Array.isArray(hashes)) throw new TypeError('hashes must be an array')
encoder.appendUint32(hashes.length)
for (let i = 0; i < hashes.length; i++) {
if (i > 0 && hashes[i - 1] >= hashes[i])
throw new RangeError("hashes must be sorted")
if (i > 0 && hashes[i - 1] >= hashes[i]) throw new RangeError('hashes must be sorted')
const bytes = hexStringToBytes(hashes[i])
if (bytes.byteLength !== HASH_SIZE)
throw new TypeError("heads hashes must be 256 bits")
if (bytes.byteLength !== HASH_SIZE) throw new TypeError('heads hashes must be 256 bits')
encoder.appendRawBytes(bytes)
}
}
@ -175,8 +144,7 @@ function encodeHashes(encoder, hashes) {
* array of hex strings.
*/
function decodeHashes(decoder) {
let length = decoder.readUint32(),
hashes = []
let length = decoder.readUint32(), hashes = []
for (let i = 0; i < length; i++) {
hashes.push(bytesToHexString(decoder.readRawBytes(HASH_SIZE)))
}
@ -278,26 +246,20 @@ function makeBloomFilter(backend, lastSync) {
*/
function getChangesToSend(backend, have, need) {
if (have.length === 0) {
return need
.map(hash => Backend.getChangeByHash(backend, hash))
.filter(change => change !== undefined)
return need.map(hash => Backend.getChangeByHash(backend, hash)).filter(change => change !== undefined)
}
let lastSyncHashes = {},
bloomFilters = []
let lastSyncHashes = {}, bloomFilters = []
for (let h of have) {
for (let hash of h.lastSync) lastSyncHashes[hash] = true
bloomFilters.push(new BloomFilter(h.bloom))
}
// Get all changes that were added since the last sync
const changes = Backend.getChanges(backend, Object.keys(lastSyncHashes)).map(
change => decodeChangeMeta(change, true)
)
const changes = Backend.getChanges(backend, Object.keys(lastSyncHashes))
.map(change => decodeChangeMeta(change, true))
let changeHashes = {},
dependents = {},
hashesToSend = {}
let changeHashes = {}, dependents = {}, hashesToSend = {}
for (let change of changes) {
changeHashes[change.hash] = true
@ -331,8 +293,7 @@ function getChangesToSend(backend, have, need) {
let changesToSend = []
for (let hash of need) {
hashesToSend[hash] = true
if (!changeHashes[hash]) {
// Change is not among those returned by getMissingChanges()?
if (!changeHashes[hash]) { // Change is not among those returned by getMissingChanges()?
const change = Backend.getChangeByHash(backend, hash)
if (change) changesToSend.push(change)
}
@ -357,7 +318,7 @@ function initSyncState() {
}
function compareArrays(a, b) {
return a.length === b.length && a.every((v, i) => v === b[i])
return (a.length === b.length) && a.every((v, i) => v === b[i])
}
/**
@ -369,19 +330,10 @@ function generateSyncMessage(backend, syncState) {
throw new Error("generateSyncMessage called with no Automerge document")
}
if (!syncState) {
throw new Error(
"generateSyncMessage requires a syncState, which can be created with initSyncState()"
)
throw new Error("generateSyncMessage requires a syncState, which can be created with initSyncState()")
}
let {
sharedHeads,
lastSentHeads,
theirHeads,
theirNeed,
theirHave,
sentHashes,
} = syncState
let { sharedHeads, lastSentHeads, theirHeads, theirNeed, theirHave, sentHashes } = syncState
const ourHeads = Backend.getHeads(backend)
// Hashes to explicitly request from the remote peer: any missing dependencies of unapplied
@ -405,28 +357,18 @@ function generateSyncMessage(backend, syncState) {
const lastSync = theirHave[0].lastSync
if (!lastSync.every(hash => Backend.getChangeByHash(backend, hash))) {
// we need to queue them to send us a fresh sync message, the one they sent is uninteligible so we don't know what they need
const resetMsg = {
heads: ourHeads,
need: [],
have: [{ lastSync: [], bloom: new Uint8Array(0) }],
changes: [],
}
const resetMsg = {heads: ourHeads, need: [], have: [{ lastSync: [], bloom: new Uint8Array(0) }], changes: []}
return [syncState, encodeSyncMessage(resetMsg)]
}
}
// XXX: we should limit ourselves to only sending a subset of all the messages, probably limited by a total message size
// these changes should ideally be RLE encoded but we haven't implemented that yet.
let changesToSend =
Array.isArray(theirHave) && Array.isArray(theirNeed)
? getChangesToSend(backend, theirHave, theirNeed)
: []
let changesToSend = Array.isArray(theirHave) && Array.isArray(theirNeed) ? getChangesToSend(backend, theirHave, theirNeed) : []
// If the heads are equal, we're in sync and don't need to do anything further
const headsUnchanged =
Array.isArray(lastSentHeads) && compareArrays(ourHeads, lastSentHeads)
const headsEqual =
Array.isArray(theirHeads) && compareArrays(ourHeads, theirHeads)
const headsUnchanged = Array.isArray(lastSentHeads) && compareArrays(ourHeads, lastSentHeads)
const headsEqual = Array.isArray(theirHeads) && compareArrays(ourHeads, theirHeads)
if (headsUnchanged && headsEqual && changesToSend.length === 0) {
// no need to send a sync message if we know we're synced!
return [syncState, null]
@ -434,19 +376,12 @@ function generateSyncMessage(backend, syncState) {
// TODO: this recomputes the SHA-256 hash of each change; we should restructure this to avoid the
// unnecessary recomputation
changesToSend = changesToSend.filter(
change => !sentHashes[decodeChangeMeta(change, true).hash]
)
changesToSend = changesToSend.filter(change => !sentHashes[decodeChangeMeta(change, true).hash])
// Regular response to a sync message: send any changes that the other node
// doesn't have. We leave the "have" field empty because the previous message
// generated by `syncStart` already indicated what changes we have.
const syncMessage = {
heads: ourHeads,
have: ourHave,
need: ourNeed,
changes: changesToSend,
}
const syncMessage = {heads: ourHeads, have: ourHave, need: ourNeed, changes: changesToSend}
if (changesToSend.length > 0) {
sentHashes = copyObject(sentHashes)
for (const change of changesToSend) {
@ -454,10 +389,7 @@ function generateSyncMessage(backend, syncState) {
}
}
syncState = Object.assign({}, syncState, {
lastSentHeads: ourHeads,
sentHashes,
})
syncState = Object.assign({}, syncState, {lastSentHeads: ourHeads, sentHashes})
return [syncState, encodeSyncMessage(syncMessage)]
}
@ -475,14 +407,13 @@ function generateSyncMessage(backend, syncState) {
* another peer, that means that peer had those changes, and therefore we now both know about them.
*/
function advanceHeads(myOldHeads, myNewHeads, ourOldSharedHeads) {
const newHeads = myNewHeads.filter(head => !myOldHeads.includes(head))
const commonHeads = ourOldSharedHeads.filter(head =>
myNewHeads.includes(head)
)
const newHeads = myNewHeads.filter((head) => !myOldHeads.includes(head))
const commonHeads = ourOldSharedHeads.filter((head) => myNewHeads.includes(head))
const advancedHeads = [...new Set([...newHeads, ...commonHeads])].sort()
return advancedHeads
}
/**
* Given a backend, a message message and the state of our peer, apply any changes, update what
* we believe about the peer, and (if there were applied changes) produce a patch for the frontend
@ -492,13 +423,10 @@ function receiveSyncMessage(backend, oldSyncState, binaryMessage) {
throw new Error("generateSyncMessage called with no Automerge document")
}
if (!oldSyncState) {
throw new Error(
"generateSyncMessage requires a syncState, which can be created with initSyncState()"
)
throw new Error("generateSyncMessage requires a syncState, which can be created with initSyncState()")
}
let { sharedHeads, lastSentHeads, sentHashes } = oldSyncState,
patch = null
let { sharedHeads, lastSentHeads, sentHashes } = oldSyncState, patch = null
const message = decodeSyncMessage(binaryMessage)
const beforeHeads = Backend.getHeads(backend)
@ -507,27 +435,18 @@ function receiveSyncMessage(backend, oldSyncState, binaryMessage) {
// changes without applying them. The set of changes may also be incomplete if the sender decided
// to break a large set of changes into chunks.
if (message.changes.length > 0) {
;[backend, patch] = Backend.applyChanges(backend, message.changes)
sharedHeads = advanceHeads(
beforeHeads,
Backend.getHeads(backend),
sharedHeads
)
[backend, patch] = Backend.applyChanges(backend, message.changes)
sharedHeads = advanceHeads(beforeHeads, Backend.getHeads(backend), sharedHeads)
}
// If heads are equal, indicate we don't need to send a response message
if (
message.changes.length === 0 &&
compareArrays(message.heads, beforeHeads)
) {
if (message.changes.length === 0 && compareArrays(message.heads, beforeHeads)) {
lastSentHeads = message.heads
}
// If all of the remote heads are known to us, that means either our heads are equal, or we are
// ahead of the remote peer. In this case, take the remote heads to be our shared heads.
const knownHeads = message.heads.filter(head =>
Backend.getChangeByHash(backend, head)
)
const knownHeads = message.heads.filter(head => Backend.getChangeByHash(backend, head))
if (knownHeads.length === message.heads.length) {
sharedHeads = message.heads
// If the remote peer has lost all its data, reset our state to perform a full resync
@ -549,18 +468,14 @@ function receiveSyncMessage(backend, oldSyncState, binaryMessage) {
theirHave: message.have, // the information we need to calculate the changes they need
theirHeads: message.heads,
theirNeed: message.need,
sentHashes,
sentHashes
}
return [backend, syncState, patch]
}
module.exports = {
receiveSyncMessage,
generateSyncMessage,
encodeSyncMessage,
decodeSyncMessage,
initSyncState,
encodeSyncState,
decodeSyncState,
BloomFilter, // BloomFilter is a private API, exported only for testing purposes
receiveSyncMessage, generateSyncMessage,
encodeSyncMessage, decodeSyncMessage,
initSyncState, encodeSyncState, decodeSyncState,
BloomFilter // BloomFilter is a private API, exported only for testing purposes
}

132
automerge-js/src/text.js Normal file
View file

@ -0,0 +1,132 @@
const { OBJECT_ID } = require('./constants')
const { isObject } = require('../src/common')
class Text {
constructor (text) {
const instance = Object.create(Text.prototype)
if (typeof text === 'string') {
instance.elems = [...text]
} else if (Array.isArray(text)) {
instance.elems = text
} else if (text === undefined) {
instance.elems = []
} else {
throw new TypeError(`Unsupported initial value for Text: ${text}`)
}
return instance
}
get length () {
return this.elems.length
}
get (index) {
return this.elems[index]
}
getElemId (index) {
return undefined
}
/**
* Iterates over the text elements character by character, including any
* inline objects.
*/
[Symbol.iterator] () {
let elems = this.elems, index = -1
return {
next () {
index += 1
if (index < elems.length) {
return {done: false, value: elems[index]}
} else {
return {done: true}
}
}
}
}
/**
* Returns the content of the Text object as a simple string, ignoring any
* non-character elements.
*/
toString() {
// Concatting to a string is faster than creating an array and then
// .join()ing for small (<100KB) arrays.
// https://jsperf.com/join-vs-loop-w-type-test
let str = ''
for (const elem of this.elems) {
if (typeof elem === 'string') str += elem
}
return str
}
/**
* Returns the content of the Text object as a sequence of strings,
* interleaved with non-character elements.
*
* For example, the value ['a', 'b', {x: 3}, 'c', 'd'] has spans:
* => ['ab', {x: 3}, 'cd']
*/
toSpans() {
let spans = []
let chars = ''
for (const elem of this.elems) {
if (typeof elem === 'string') {
chars += elem
} else {
if (chars.length > 0) {
spans.push(chars)
chars = ''
}
spans.push(elem)
}
}
if (chars.length > 0) {
spans.push(chars)
}
return spans
}
/**
* Returns the content of the Text object as a simple string, so that the
* JSON serialization of an Automerge document represents text nicely.
*/
toJSON() {
return this.toString()
}
/**
* Updates the list item at position `index` to a new value `value`.
*/
set (index, value) {
this.elems[index] = value
}
/**
* Inserts new list items `values` starting at position `index`.
*/
insertAt(index, ...values) {
this.elems.splice(index, 0, ... values)
}
/**
* Deletes `numDelete` list items starting at position `index`.
* if `numDelete` is not given, one item is deleted.
*/
deleteAt(index, numDelete = 1) {
this.elems.splice(index, numDelete)
}
}
// Read-only methods that can delegate to the JavaScript built-in array
for (let method of ['concat', 'every', 'filter', 'find', 'findIndex', 'forEach', 'includes',
'indexOf', 'join', 'lastIndexOf', 'map', 'reduce', 'reduceRight',
'slice', 'some', 'toLocaleString']) {
Text.prototype[method] = function (...args) {
const array = [...this]
return array[method](...args)
}
}
module.exports = { Text }

16
automerge-js/src/uuid.js Normal file
View file

@ -0,0 +1,16 @@
const { v4: uuid } = require('uuid')
function defaultFactory() {
return uuid().replace(/-/g, '')
}
let factory = defaultFactory
function makeUuid() {
return factory()
}
makeUuid.setFactory = newFactory => { factory = newFactory }
makeUuid.reset = () => { factory = defaultFactory }
module.exports = makeUuid

View file

@ -0,0 +1,164 @@
const assert = require('assert')
const util = require('util')
const Automerge = require('..')
describe('Automerge', () => {
describe('basics', () => {
it('should init clone and free', () => {
let doc1 = Automerge.init()
let doc2 = Automerge.clone(doc1);
})
it('handle basic set and read on root object', () => {
let doc1 = Automerge.init()
let doc2 = Automerge.change(doc1, (d) => {
d.hello = "world"
d.big = "little"
d.zip = "zop"
d.app = "dap"
assert.deepEqual(d, { hello: "world", big: "little", zip: "zop", app: "dap" })
})
assert.deepEqual(doc2, { hello: "world", big: "little", zip: "zop", app: "dap" })
})
it('handle basic sets over many changes', () => {
let doc1 = Automerge.init()
let timestamp = new Date();
let counter = new Automerge.Counter(100);
let bytes = new Uint8Array([10,11,12]);
let doc2 = Automerge.change(doc1, (d) => {
d.hello = "world"
})
let doc3 = Automerge.change(doc2, (d) => {
d.counter1 = counter
})
let doc4 = Automerge.change(doc3, (d) => {
d.timestamp1 = timestamp
})
let doc5 = Automerge.change(doc4, (d) => {
d.app = null
})
let doc6 = Automerge.change(doc5, (d) => {
d.bytes1 = bytes
})
let doc7 = Automerge.change(doc6, (d) => {
d.uint = new Automerge.Uint(1)
d.int = new Automerge.Int(-1)
d.float64 = new Automerge.Float64(5.5)
d.number1 = 100
d.number2 = -45.67
d.true = true
d.false = false
})
assert.deepEqual(doc7, { hello: "world", true: true, false: false, int: -1, uint: 1, float64: 5.5, number1: 100, number2: -45.67, counter1: counter, timestamp1: timestamp, bytes1: bytes, app: null })
let changes = Automerge.getAllChanges(doc7)
let t1 = Automerge.init()
;let [t2] = Automerge.applyChanges(t1, changes)
assert.deepEqual(doc7,t2)
})
it('handle overwrites to values', () => {
let doc1 = Automerge.init()
let doc2 = Automerge.change(doc1, (d) => {
d.hello = "world1"
})
let doc3 = Automerge.change(doc2, (d) => {
d.hello = "world2"
})
let doc4 = Automerge.change(doc3, (d) => {
d.hello = "world3"
})
let doc5 = Automerge.change(doc4, (d) => {
d.hello = "world4"
})
assert.deepEqual(doc5, { hello: "world4" } )
})
it('handle set with object value', () => {
let doc1 = Automerge.init()
let doc2 = Automerge.change(doc1, (d) => {
d.subobj = { hello: "world", subsubobj: { zip: "zop" } }
})
assert.deepEqual(doc2, { subobj: { hello: "world", subsubobj: { zip: "zop" } } })
})
it('handle simple list creation', () => {
let doc1 = Automerge.init()
let doc2 = Automerge.change(doc1, (d) => d.list = [])
assert.deepEqual(doc2, { list: []})
})
it('handle simple lists', () => {
let doc1 = Automerge.init()
let doc2 = Automerge.change(doc1, (d) => {
d.list = [ 1, 2, 3 ]
})
assert.deepEqual(doc2.list.length, 3)
assert.deepEqual(doc2.list[0], 1)
assert.deepEqual(doc2.list[1], 2)
assert.deepEqual(doc2.list[2], 3)
assert.deepEqual(doc2, { list: [1,2,3] })
// assert.deepStrictEqual(Automerge.toJS(doc2), { list: [1,2,3] })
let doc3 = Automerge.change(doc2, (d) => {
d.list[1] = "a"
})
assert.deepEqual(doc3.list.length, 3)
assert.deepEqual(doc3.list[0], 1)
assert.deepEqual(doc3.list[1], "a")
assert.deepEqual(doc3.list[2], 3)
assert.deepEqual(doc3, { list: [1,"a",3] })
})
it('handle simple lists', () => {
let doc1 = Automerge.init()
let doc2 = Automerge.change(doc1, (d) => {
d.list = [ 1, 2, 3 ]
})
let changes = Automerge.getChanges(doc1, doc2)
let docB1 = Automerge.init()
;let [docB2] = Automerge.applyChanges(docB1, changes)
assert.deepEqual(docB2, doc2);
})
it('handle text', () => {
let doc1 = Automerge.init()
let tmp = new Automerge.Text("hello")
let doc2 = Automerge.change(doc1, (d) => {
d.list = new Automerge.Text("hello")
d.list.insertAt(2,"Z")
})
let changes = Automerge.getChanges(doc1, doc2)
let docB1 = Automerge.init()
;let [docB2] = Automerge.applyChanges(docB1, changes)
assert.deepEqual(docB2, doc2);
})
it('have many list methods', () => {
let doc1 = Automerge.from({ list: [1,2,3] })
assert.deepEqual(doc1, { list: [1,2,3] });
let doc2 = Automerge.change(doc1, (d) => {
d.list.splice(1,1,9,10)
})
assert.deepEqual(doc2, { list: [1,9,10,3] });
let doc3 = Automerge.change(doc2, (d) => {
d.list.push(11,12)
})
assert.deepEqual(doc3, { list: [1,9,10,3,11,12] });
let doc4 = Automerge.change(doc3, (d) => {
d.list.unshift(2,2)
})
assert.deepEqual(doc4, { list: [2,2,1,9,10,3,11,12] });
let doc5 = Automerge.change(doc4, (d) => {
d.list.shift()
})
assert.deepEqual(doc5, { list: [2,1,9,10,3,11,12] });
let doc6 = Automerge.change(doc5, (d) => {
d.list.insertAt(3,100,101)
})
assert.deepEqual(doc6, { list: [2,1,9,100,101,10,3,11,12] });
})
})
})

View file

@ -0,0 +1,97 @@
const assert = require('assert')
const { checkEncoded } = require('./helpers')
const Automerge = require('..')
const { encodeChange, decodeChange } = Automerge
describe('change encoding', () => {
it('should encode text edits', () => {
/*
const change1 = {actor: 'aaaa', seq: 1, startOp: 1, time: 9, message: '', deps: [], ops: [
{action: 'makeText', obj: '_root', key: 'text', insert: false, pred: []},
{action: 'set', obj: '1@aaaa', elemId: '_head', insert: true, value: 'h', pred: []},
{action: 'del', obj: '1@aaaa', elemId: '2@aaaa', insert: false, pred: ['2@aaaa']},
{action: 'set', obj: '1@aaaa', elemId: '_head', insert: true, value: 'H', pred: []},
{action: 'set', obj: '1@aaaa', elemId: '4@aaaa', insert: true, value: 'i', pred: []}
]}
*/
const change1 = {actor: 'aaaa', seq: 1, startOp: 1, time: 9, message: null, deps: [], ops: [
{action: 'makeText', obj: '_root', key: 'text', pred: []},
{action: 'set', obj: '1@aaaa', elemId: '_head', insert: true, value: 'h', pred: []},
{action: 'del', obj: '1@aaaa', elemId: '2@aaaa', pred: ['2@aaaa']},
{action: 'set', obj: '1@aaaa', elemId: '_head', insert: true, value: 'H', pred: []},
{action: 'set', obj: '1@aaaa', elemId: '4@aaaa', insert: true, value: 'i', pred: []}
]}
checkEncoded(encodeChange(change1), [
0x85, 0x6f, 0x4a, 0x83, // magic bytes
0xe2, 0xbd, 0xfb, 0xf5, // checksum
1, 94, 0, 2, 0xaa, 0xaa, // chunkType: change, length, deps, actor 'aaaa'
1, 1, 9, 0, 0, // seq, startOp, time, message, actor list
12, 0x01, 4, 0x02, 4, // column count, objActor, objCtr
0x11, 8, 0x13, 7, 0x15, 8, // keyActor, keyCtr, keyStr
0x34, 4, 0x42, 6, // insert, action
0x56, 6, 0x57, 3, // valLen, valRaw
0x70, 6, 0x71, 2, 0x73, 2, // predNum, predActor, predCtr
0, 1, 4, 0, // objActor column: null, 0, 0, 0, 0
0, 1, 4, 1, // objCtr column: null, 1, 1, 1, 1
0, 2, 0x7f, 0, 0, 1, 0x7f, 0, // keyActor column: null, null, 0, null, 0
0, 1, 0x7c, 0, 2, 0x7e, 4, // keyCtr column: null, 0, 2, 0, 4
0x7f, 4, 0x74, 0x65, 0x78, 0x74, 0, 4, // keyStr column: 'text', null, null, null, null
1, 1, 1, 2, // insert column: false, true, false, true, true
0x7d, 4, 1, 3, 2, 1, // action column: makeText, set, del, set, set
0x7d, 0, 0x16, 0, 2, 0x16, // valLen column: 0, 0x16, 0, 0x16, 0x16
0x68, 0x48, 0x69, // valRaw column: 'h', 'H', 'i'
2, 0, 0x7f, 1, 2, 0, // predNum column: 0, 0, 1, 0, 0
0x7f, 0, // predActor column: 0
0x7f, 2 // predCtr column: 2
])
const decoded = decodeChange(encodeChange(change1))
assert.deepStrictEqual(decoded, Object.assign({hash: decoded.hash}, change1))
})
// FIXME - skipping this b/c it was never implemented in the rust impl and isnt trivial
/*
it.skip('should require strict ordering of preds', () => {
const change = new Uint8Array([
133, 111, 74, 131, 31, 229, 112, 44, 1, 105, 1, 58, 30, 190, 100, 253, 180, 180, 66, 49, 126,
81, 142, 10, 3, 35, 140, 189, 231, 34, 145, 57, 66, 23, 224, 149, 64, 97, 88, 140, 168, 194,
229, 4, 244, 209, 58, 138, 67, 140, 1, 152, 236, 250, 2, 0, 1, 4, 55, 234, 66, 242, 8, 21, 11,
52, 1, 66, 2, 86, 3, 87, 10, 112, 2, 113, 3, 115, 4, 127, 9, 99, 111, 109, 109, 111, 110, 86,
97, 114, 1, 127, 1, 127, 166, 1, 52, 48, 57, 49, 52, 57, 52, 53, 56, 50, 127, 2, 126, 0, 1,
126, 139, 1, 0
])
assert.throws(() => { decodeChange(change) }, /operation IDs are not in ascending order/)
})
*/
describe('with trailing bytes', () => {
let change = new Uint8Array([
0x85, 0x6f, 0x4a, 0x83, // magic bytes
0xb2, 0x98, 0x9e, 0xa9, // checksum
1, 61, 0, 2, 0x12, 0x34, // chunkType: change, length, deps, actor '1234'
1, 1, 252, 250, 220, 255, 5, // seq, startOp, time
14, 73, 110, 105, 116, 105, 97, 108, 105, 122, 97, 116, 105, 111, 110, // message: 'Initialization'
0, 6, // actor list, column count
0x15, 3, 0x34, 1, 0x42, 2, // keyStr, insert, action
0x56, 2, 0x57, 1, 0x70, 2, // valLen, valRaw, predNum
0x7f, 1, 0x78, // keyStr: 'x'
1, // insert: false
0x7f, 1, // action: set
0x7f, 19, // valLen: 1 byte of type uint
1, // valRaw: 1
0x7f, 0, // predNum: 0
0, 1, 2, 3, 4, 5, 6, 7, 8, 9 // 10 trailing bytes
])
it('should allow decoding and re-encoding', () => {
// NOTE: This calls the JavaScript encoding and decoding functions, even when the WebAssembly
// backend is loaded. Should the wasm backend export its own functions for testing?
checkEncoded(change, encodeChange(decodeChange(change)))
})
it('should be preserved in document encoding', () => {
const [doc] = Automerge.applyChanges(Automerge.init(), [change])
const [reconstructed] = Automerge.getAllChanges(Automerge.load(Automerge.save(doc)))
checkEncoded(change, reconstructed)
})
})
})

View file

@ -1,21 +1,16 @@
import * as assert from "assert"
import { Encoder } from "./legacy/encoding"
const assert = require('assert')
const { Encoder } = require('../src/encoding')
// Assertion that succeeds if the first argument deepStrictEquals at least one of the
// subsequent arguments (but we don't care which one)
export function assertEqualsOneOf(actual, ...expected) {
function assertEqualsOneOf(actual, ...expected) {
assert(expected.length > 0)
for (let i = 0; i < expected.length; i++) {
try {
assert.deepStrictEqual(actual, expected[i])
return // if we get here without an exception, that means success
} catch (e) {
if (e instanceof assert.AssertionError) {
if (!e.name.match(/^AssertionError/) || i === expected.length - 1)
throw e
} else {
throw e
}
if (!e.name.match(/^AssertionError/) || i === expected.length - 1) throw e
}
}
}
@ -24,13 +19,14 @@ export function assertEqualsOneOf(actual, ...expected) {
* Asserts that the byte array maintained by `encoder` contains the same byte
* sequence as the array `bytes`.
*/
export function checkEncoded(encoder, bytes, detail?) {
const encoded = encoder instanceof Encoder ? encoder.buffer : encoder
function checkEncoded(encoder, bytes, detail) {
const encoded = (encoder instanceof Encoder) ? encoder.buffer : encoder
const expected = new Uint8Array(bytes)
const message =
(detail ? `${detail}: ` : "") + `${encoded} expected to equal ${expected}`
const message = (detail ? `${detail}: ` : '') + `${encoded} expected to equal ${expected}`
assert(encoded.byteLength === expected.byteLength, message)
for (let i = 0; i < encoded.byteLength; i++) {
assert(encoded[i] === expected[i], message)
}
}
module.exports = { assertEqualsOneOf, checkEncoded }

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,697 @@
const assert = require('assert')
const Automerge = require('..')
const { assertEqualsOneOf } = require('./helpers')
function attributeStateToAttributes(accumulatedAttributes) {
const attributes = {}
Object.entries(accumulatedAttributes).forEach(([key, values]) => {
if (values.length && values[0] !== null) {
attributes[key] = values[0]
}
})
return attributes
}
function isEquivalent(a, b) {
const aProps = Object.getOwnPropertyNames(a)
const bProps = Object.getOwnPropertyNames(b)
if (aProps.length != bProps.length) {
return false
}
for (let i = 0; i < aProps.length; i++) {
const propName = aProps[i]
if (a[propName] !== b[propName]) {
return false
}
}
return true
}
function isControlMarker(pseudoCharacter) {
return typeof pseudoCharacter === 'object' && pseudoCharacter.attributes
}
function opFrom(text, attributes) {
let op = { insert: text }
if (Object.keys(attributes).length > 0) {
op.attributes = attributes
}
return op
}
function accumulateAttributes(span, accumulatedAttributes) {
Object.entries(span).forEach(([key, value]) => {
if (!accumulatedAttributes[key]) {
accumulatedAttributes[key] = []
}
if (value === null) {
if (accumulatedAttributes[key].length === 0 || accumulatedAttributes[key] === null) {
accumulatedAttributes[key].unshift(null)
} else {
accumulatedAttributes[key].shift()
}
} else {
if (accumulatedAttributes[key][0] === null) {
accumulatedAttributes[key].shift()
} else {
accumulatedAttributes[key].unshift(value)
}
}
})
return accumulatedAttributes
}
function automergeTextToDeltaDoc(text) {
let ops = []
let controlState = {}
let currentString = ""
let attributes = {}
text.toSpans().forEach((span) => {
if (isControlMarker(span)) {
controlState = accumulateAttributes(span.attributes, controlState)
} else {
let next = attributeStateToAttributes(controlState)
// if the next span has the same calculated attributes as the current span
// don't bother outputting it as a separate span, just let it ride
if (typeof span === 'string' && isEquivalent(next, attributes)) {
currentString = currentString + span
return
}
if (currentString) {
ops.push(opFrom(currentString, attributes))
}
// If we've got a string, we might be able to concatenate it to another
// same-attributed-string, so remember it and go to the next iteration.
if (typeof span === 'string') {
currentString = span
attributes = next
} else {
// otherwise we have an embed "character" and should output it immediately.
// embeds are always one-"character" in length.
ops.push(opFrom(span, next))
currentString = ''
attributes = {}
}
}
})
// at the end, flush any accumulated string out
if (currentString) {
ops.push(opFrom(currentString, attributes))
}
return ops
}
function inverseAttributes(attributes) {
let invertedAttributes = {}
Object.keys(attributes).forEach((key) => {
invertedAttributes[key] = null
})
return invertedAttributes
}
function applyDeleteOp(text, offset, op) {
let length = op.delete
while (length > 0) {
if (isControlMarker(text.get(offset))) {
offset += 1
} else {
// we need to not delete control characters, but we do delete embed characters
text.deleteAt(offset, 1)
length -= 1
}
}
return [text, offset]
}
function applyRetainOp(text, offset, op) {
let length = op.retain
if (op.attributes) {
text.insertAt(offset, { attributes: op.attributes })
offset += 1
}
while (length > 0) {
const char = text.get(offset)
offset += 1
if (!isControlMarker(char)) {
length -= 1
}
}
if (op.attributes) {
text.insertAt(offset, { attributes: inverseAttributes(op.attributes) })
offset += 1
}
return [text, offset]
}
function applyInsertOp(text, offset, op) {
let originalOffset = offset
if (typeof op.insert === 'string') {
text.insertAt(offset, ...op.insert.split(''))
offset += op.insert.length
} else {
// we have an embed or something similar
text.insertAt(offset, op.insert)
offset += 1
}
if (op.attributes) {
text.insertAt(originalOffset, { attributes: op.attributes })
offset += 1
}
if (op.attributes) {
text.insertAt(offset, { attributes: inverseAttributes(op.attributes) })
offset += 1
}
return [text, offset]
}
// XXX: uhhhhh, why can't I pass in text?
function applyDeltaDocToAutomergeText(delta, doc) {
let offset = 0
delta.forEach(op => {
if (op.retain) {
[, offset] = applyRetainOp(doc.text, offset, op)
} else if (op.delete) {
[, offset] = applyDeleteOp(doc.text, offset, op)
} else if (op.insert) {
[, offset] = applyInsertOp(doc.text, offset, op)
}
})
}
describe('Automerge.Text', () => {
let s1, s2
beforeEach(() => {
s1 = Automerge.change(Automerge.init(), doc => doc.text = new Automerge.Text())
s2 = Automerge.merge(Automerge.init(), s1)
})
it('should support insertion', () => {
s1 = Automerge.change(s1, doc => doc.text.insertAt(0, 'a'))
assert.strictEqual(s1.text.length, 1)
assert.strictEqual(s1.text.get(0), 'a')
assert.strictEqual(s1.text.toString(), 'a')
//assert.strictEqual(s1.text.getElemId(0), `2@${Automerge.getActorId(s1)}`)
})
it('should support deletion', () => {
s1 = Automerge.change(s1, doc => doc.text.insertAt(0, 'a', 'b', 'c'))
s1 = Automerge.change(s1, doc => doc.text.deleteAt(1, 1))
assert.strictEqual(s1.text.length, 2)
assert.strictEqual(s1.text.get(0), 'a')
assert.strictEqual(s1.text.get(1), 'c')
assert.strictEqual(s1.text.toString(), 'ac')
})
it("should support implicit and explicit deletion", () => {
s1 = Automerge.change(s1, doc => doc.text.insertAt(0, "a", "b", "c"))
s1 = Automerge.change(s1, doc => doc.text.deleteAt(1))
s1 = Automerge.change(s1, doc => doc.text.deleteAt(1, 0))
assert.strictEqual(s1.text.length, 2)
assert.strictEqual(s1.text.get(0), "a")
assert.strictEqual(s1.text.get(1), "c")
assert.strictEqual(s1.text.toString(), "ac")
})
it('should handle concurrent insertion', () => {
s1 = Automerge.change(s1, doc => doc.text.insertAt(0, 'a', 'b', 'c'))
s2 = Automerge.change(s2, doc => doc.text.insertAt(0, 'x', 'y', 'z'))
s1 = Automerge.merge(s1, s2)
assert.strictEqual(s1.text.length, 6)
assertEqualsOneOf(s1.text.toString(), 'abcxyz', 'xyzabc')
assertEqualsOneOf(s1.text.join(''), 'abcxyz', 'xyzabc')
})
it('should handle text and other ops in the same change', () => {
s1 = Automerge.change(s1, doc => {
doc.foo = 'bar'
doc.text.insertAt(0, 'a')
})
assert.strictEqual(s1.foo, 'bar')
assert.strictEqual(s1.text.toString(), 'a')
assert.strictEqual(s1.text.join(''), 'a')
})
it('should serialize to JSON as a simple string', () => {
s1 = Automerge.change(s1, doc => doc.text.insertAt(0, 'a', '"', 'b'))
assert.strictEqual(JSON.stringify(s1), '{"text":"a\\"b"}')
})
it('should allow modification before an object is assigned to a document', () => {
s1 = Automerge.change(Automerge.init(), doc => {
const text = new Automerge.Text()
text.insertAt(0, 'a', 'b', 'c', 'd')
text.deleteAt(2)
doc.text = text
assert.strictEqual(doc.text.toString(), 'abd')
assert.strictEqual(doc.text.join(''), 'abd')
})
assert.strictEqual(s1.text.toString(), 'abd')
assert.strictEqual(s1.text.join(''), 'abd')
})
it('should allow modification after an object is assigned to a document', () => {
s1 = Automerge.change(Automerge.init(), doc => {
const text = new Automerge.Text()
doc.text = text
doc.text.insertAt(0, 'a', 'b', 'c', 'd')
doc.text.deleteAt(2)
assert.strictEqual(doc.text.toString(), 'abd')
assert.strictEqual(doc.text.join(''), 'abd')
})
assert.strictEqual(s1.text.join(''), 'abd')
})
it('should not allow modification outside of a change callback', () => {
assert.throws(() => s1.text.insertAt(0, 'a'), /object cannot be modified outside of a change block/)
})
describe('with initial value', () => {
it('should accept a string as initial value', () => {
let s1 = Automerge.change(Automerge.init(), doc => doc.text = new Automerge.Text('init'))
assert.strictEqual(s1.text.length, 4)
assert.strictEqual(s1.text.get(0), 'i')
assert.strictEqual(s1.text.get(1), 'n')
assert.strictEqual(s1.text.get(2), 'i')
assert.strictEqual(s1.text.get(3), 't')
assert.strictEqual(s1.text.toString(), 'init')
})
it('should accept an array as initial value', () => {
let s1 = Automerge.change(Automerge.init(), doc => doc.text = new Automerge.Text(['i', 'n', 'i', 't']))
assert.strictEqual(s1.text.length, 4)
assert.strictEqual(s1.text.get(0), 'i')
assert.strictEqual(s1.text.get(1), 'n')
assert.strictEqual(s1.text.get(2), 'i')
assert.strictEqual(s1.text.get(3), 't')
assert.strictEqual(s1.text.toString(), 'init')
})
it('should initialize text in Automerge.from()', () => {
let s1 = Automerge.from({text: new Automerge.Text('init')})
assert.strictEqual(s1.text.length, 4)
assert.strictEqual(s1.text.get(0), 'i')
assert.strictEqual(s1.text.get(1), 'n')
assert.strictEqual(s1.text.get(2), 'i')
assert.strictEqual(s1.text.get(3), 't')
assert.strictEqual(s1.text.toString(), 'init')
})
it('should encode the initial value as a change', () => {
const s1 = Automerge.from({text: new Automerge.Text('init')})
const changes = Automerge.getAllChanges(s1)
assert.strictEqual(changes.length, 1)
const [s2] = Automerge.applyChanges(Automerge.init(), changes)
assert.strictEqual(s2.text instanceof Automerge.Text, true)
assert.strictEqual(s2.text.toString(), 'init')
assert.strictEqual(s2.text.join(''), 'init')
})
it('should allow immediate access to the value', () => {
Automerge.change(Automerge.init(), doc => {
const text = new Automerge.Text('init')
assert.strictEqual(text.length, 4)
assert.strictEqual(text.get(0), 'i')
assert.strictEqual(text.toString(), 'init')
doc.text = text
assert.strictEqual(doc.text.length, 4)
assert.strictEqual(doc.text.get(0), 'i')
assert.strictEqual(doc.text.toString(), 'init')
})
})
it('should allow pre-assignment modification of the initial value', () => {
let s1 = Automerge.change(Automerge.init(), doc => {
const text = new Automerge.Text('init')
text.deleteAt(3)
assert.strictEqual(text.join(''), 'ini')
doc.text = text
assert.strictEqual(doc.text.join(''), 'ini')
assert.strictEqual(doc.text.toString(), 'ini')
})
assert.strictEqual(s1.text.toString(), 'ini')
assert.strictEqual(s1.text.join(''), 'ini')
})
it('should allow post-assignment modification of the initial value', () => {
let s1 = Automerge.change(Automerge.init(), doc => {
const text = new Automerge.Text('init')
doc.text = text
doc.text.deleteAt(0)
doc.text.insertAt(0, 'I')
assert.strictEqual(doc.text.join(''), 'Init')
assert.strictEqual(doc.text.toString(), 'Init')
})
assert.strictEqual(s1.text.join(''), 'Init')
assert.strictEqual(s1.text.toString(), 'Init')
})
})
describe('non-textual control characters', () => {
let s1
beforeEach(() => {
s1 = Automerge.change(Automerge.init(), doc => {
doc.text = new Automerge.Text()
doc.text.insertAt(0, 'a')
doc.text.insertAt(1, { attribute: 'bold' })
})
})
it('should allow fetching non-textual characters', () => {
assert.deepEqual(s1.text.get(1), { attribute: 'bold' })
//assert.strictEqual(s1.text.getElemId(1), `3@${Automerge.getActorId(s1)}`)
})
it('should include control characters in string length', () => {
assert.strictEqual(s1.text.length, 2)
assert.strictEqual(s1.text.get(0), 'a')
})
it('should exclude control characters from toString()', () => {
assert.strictEqual(s1.text.toString(), 'a')
})
it('should allow control characters to be updated', () => {
const s2 = Automerge.change(s1, doc => doc.text.get(1).attribute = 'italic')
const s3 = Automerge.load(Automerge.save(s2))
assert.strictEqual(s1.text.get(1).attribute, 'bold')
assert.strictEqual(s2.text.get(1).attribute, 'italic')
assert.strictEqual(s3.text.get(1).attribute, 'italic')
})
describe('spans interface to Text', () => {
it('should return a simple string as a single span', () => {
let s1 = Automerge.change(Automerge.init(), doc => {
doc.text = new Automerge.Text('hello world')
})
assert.deepEqual(s1.text.toSpans(), ['hello world'])
})
it('should return an empty string as an empty array', () => {
let s1 = Automerge.change(Automerge.init(), doc => {
doc.text = new Automerge.Text()
})
assert.deepEqual(s1.text.toSpans(), [])
})
it('should split a span at a control character', () => {
let s1 = Automerge.change(Automerge.init(), doc => {
doc.text = new Automerge.Text('hello world')
doc.text.insertAt(5, { attributes: { bold: true } })
})
assert.deepEqual(s1.text.toSpans(),
['hello', { attributes: { bold: true } }, ' world'])
})
it('should allow consecutive control characters', () => {
let s1 = Automerge.change(Automerge.init(), doc => {
doc.text = new Automerge.Text('hello world')
doc.text.insertAt(5, { attributes: { bold: true } })
doc.text.insertAt(6, { attributes: { italic: true } })
})
assert.deepEqual(s1.text.toSpans(),
['hello',
{ attributes: { bold: true } },
{ attributes: { italic: true } },
' world'
])
})
it('should allow non-consecutive control characters', () => {
let s1 = Automerge.change(Automerge.init(), doc => {
doc.text = new Automerge.Text('hello world')
doc.text.insertAt(5, { attributes: { bold: true } })
doc.text.insertAt(12, { attributes: { italic: true } })
})
assert.deepEqual(s1.text.toSpans(),
['hello',
{ attributes: { bold: true } },
' world',
{ attributes: { italic: true } }
])
})
it('should be convertable into a Quill delta', () => {
let s1 = Automerge.change(Automerge.init(), doc => {
doc.text = new Automerge.Text('Gandalf the Grey')
doc.text.insertAt(0, { attributes: { bold: true } })
doc.text.insertAt(7 + 1, { attributes: { bold: null } })
doc.text.insertAt(12 + 2, { attributes: { color: '#cccccc' } })
})
let deltaDoc = automergeTextToDeltaDoc(s1.text)
// From https://quilljs.com/docs/delta/
let expectedDoc = [
{ insert: 'Gandalf', attributes: { bold: true } },
{ insert: ' the ' },
{ insert: 'Grey', attributes: { color: '#cccccc' } }
]
assert.deepEqual(deltaDoc, expectedDoc)
})
it('should support embeds', () => {
let s1 = Automerge.change(Automerge.init(), doc => {
doc.text = new Automerge.Text('')
doc.text.insertAt(0, { attributes: { link: 'https://quilljs.com' } })
doc.text.insertAt(1, {
image: 'https://quilljs.com/assets/images/icon.png'
})
doc.text.insertAt(2, { attributes: { link: null } })
})
let deltaDoc = automergeTextToDeltaDoc(s1.text)
// From https://quilljs.com/docs/delta/
let expectedDoc = [{
// An image link
insert: {
image: 'https://quilljs.com/assets/images/icon.png'
},
attributes: {
link: 'https://quilljs.com'
}
}]
assert.deepEqual(deltaDoc, expectedDoc)
})
it('should handle concurrent overlapping spans', () => {
let s1 = Automerge.change(Automerge.init(), doc => {
doc.text = new Automerge.Text('Gandalf the Grey')
})
let s2 = Automerge.merge(Automerge.init(), s1)
let s3 = Automerge.change(s1, doc => {
doc.text.insertAt(8, { attributes: { bold: true } })
doc.text.insertAt(16 + 1, { attributes: { bold: null } })
})
let s4 = Automerge.change(s2, doc => {
doc.text.insertAt(0, { attributes: { bold: true } })
doc.text.insertAt(11 + 1, { attributes: { bold: null } })
})
let merged = Automerge.merge(s3, s4)
let deltaDoc = automergeTextToDeltaDoc(merged.text)
// From https://quilljs.com/docs/delta/
let expectedDoc = [
{ insert: 'Gandalf the Grey', attributes: { bold: true } },
]
assert.deepEqual(deltaDoc, expectedDoc)
})
it('should handle debolding spans', () => {
let s1 = Automerge.change(Automerge.init(), doc => {
doc.text = new Automerge.Text('Gandalf the Grey')
})
let s2 = Automerge.merge(Automerge.init(), s1)
let s3 = Automerge.change(s1, doc => {
doc.text.insertAt(0, { attributes: { bold: true } })
doc.text.insertAt(16 + 1, { attributes: { bold: null } })
})
let s4 = Automerge.change(s2, doc => {
doc.text.insertAt(8, { attributes: { bold: null } })
doc.text.insertAt(11 + 1, { attributes: { bold: true } })
})
let merged = Automerge.merge(s3, s4)
let deltaDoc = automergeTextToDeltaDoc(merged.text)
// From https://quilljs.com/docs/delta/
let expectedDoc = [
{ insert: 'Gandalf ', attributes: { bold: true } },
{ insert: 'the' },
{ insert: ' Grey', attributes: { bold: true } },
]
assert.deepEqual(deltaDoc, expectedDoc)
})
// xxx: how would this work for colors?
it('should handle destyling across destyled spans', () => {
let s1 = Automerge.change(Automerge.init(), doc => {
doc.text = new Automerge.Text('Gandalf the Grey')
})
let s2 = Automerge.merge(Automerge.init(), s1)
let s3 = Automerge.change(s1, doc => {
doc.text.insertAt(0, { attributes: { bold: true } })
doc.text.insertAt(16 + 1, { attributes: { bold: null } })
})
let s4 = Automerge.change(s2, doc => {
doc.text.insertAt(8, { attributes: { bold: null } })
doc.text.insertAt(11 + 1, { attributes: { bold: true } })
})
let merged = Automerge.merge(s3, s4)
let final = Automerge.change(merged, doc => {
doc.text.insertAt(3 + 1, { attributes: { bold: null } })
doc.text.insertAt(doc.text.length, { attributes: { bold: true } })
})
let deltaDoc = automergeTextToDeltaDoc(final.text)
// From https://quilljs.com/docs/delta/
let expectedDoc = [
{ insert: 'Gan', attributes: { bold: true } },
{ insert: 'dalf the Grey' },
]
assert.deepEqual(deltaDoc, expectedDoc)
})
it('should apply an insert', () => {
let s1 = Automerge.change(Automerge.init(), doc => {
doc.text = new Automerge.Text('Hello world')
})
const delta = [
{ retain: 6 },
{ insert: 'reader' },
{ delete: 5 }
]
let s2 = Automerge.change(s1, doc => {
applyDeltaDocToAutomergeText(delta, doc)
})
assert.strictEqual(s2.text.join(''), 'Hello reader')
})
it('should apply an insert with control characters', () => {
let s1 = Automerge.change(Automerge.init(), doc => {
doc.text = new Automerge.Text('Hello world')
})
const delta = [
{ retain: 6 },
{ insert: 'reader', attributes: { bold: true } },
{ delete: 5 },
{ insert: '!' }
]
let s2 = Automerge.change(s1, doc => {
applyDeltaDocToAutomergeText(delta, doc)
})
assert.strictEqual(s2.text.toString(), 'Hello reader!')
assert.deepEqual(s2.text.toSpans(), [
"Hello ",
{ attributes: { bold: true } },
"reader",
{ attributes: { bold: null } },
"!"
])
})
it('should account for control characters in retain/delete lengths', () => {
let s1 = Automerge.change(Automerge.init(), doc => {
doc.text = new Automerge.Text('Hello world')
doc.text.insertAt(4, { attributes: { color: '#ccc' } })
doc.text.insertAt(10, { attributes: { color: '#f00' } })
})
const delta = [
{ retain: 6 },
{ insert: 'reader', attributes: { bold: true } },
{ delete: 5 },
{ insert: '!' }
]
let s2 = Automerge.change(s1, doc => {
applyDeltaDocToAutomergeText(delta, doc)
})
assert.strictEqual(s2.text.toString(), 'Hello reader!')
assert.deepEqual(s2.text.toSpans(), [
"Hell",
{ attributes: { color: '#ccc'} },
"o ",
{ attributes: { bold: true } },
"reader",
{ attributes: { bold: null } },
{ attributes: { color: '#f00'} },
"!"
])
})
it('should support embeds', () => {
let s1 = Automerge.change(Automerge.init(), doc => {
doc.text = new Automerge.Text('')
})
let deltaDoc = [{
// An image link
insert: {
image: 'https://quilljs.com/assets/images/icon.png'
},
attributes: {
link: 'https://quilljs.com'
}
}]
let s2 = Automerge.change(s1, doc => {
applyDeltaDocToAutomergeText(deltaDoc, doc)
})
assert.deepEqual(s2.text.toSpans(), [
{ attributes: { link: 'https://quilljs.com' } },
{ image: 'https://quilljs.com/assets/images/icon.png'},
{ attributes: { link: null } },
])
})
})
})
it('should support unicode when creating text', () => {
s1 = Automerge.from({
text: new Automerge.Text('🐦')
})
assert.strictEqual(s1.text.get(0), '🐦')
})
})

View file

@ -0,0 +1,32 @@
const assert = require('assert')
const Automerge = require('..')
const uuid = Automerge.uuid
describe('uuid', () => {
afterEach(() => {
uuid.reset()
})
describe('default implementation', () => {
it('generates unique values', () => {
assert.notEqual(uuid(), uuid())
})
})
describe('custom implementation', () => {
let counter
function customUuid() {
return `custom-uuid-${counter++}`
}
before(() => uuid.setFactory(customUuid))
beforeEach(() => counter = 0)
it('invokes the custom factory', () => {
assert.equal(uuid(), 'custom-uuid-0')
assert.equal(uuid(), 'custom-uuid-1')
})
})
})

View file

@ -1,6 +1,5 @@
/node_modules
/bundler
/nodejs
/deno
/dev
/target
Cargo.lock
yarn.lock

View file

@ -28,16 +28,14 @@ serde = "^1.0"
serde_json = "^1.0"
rand = { version = "^0.8.4" }
getrandom = { version = "^0.2.2", features=["js"] }
uuid = { version = "^1.2.1", features=["v4", "js", "serde"] }
serde-wasm-bindgen = "0.4.3"
uuid = { version = "^0.8.2", features=["v4", "wasm-bindgen", "serde"] }
serde-wasm-bindgen = "0.1.3"
serde_bytes = "0.11.5"
hex = "^0.4.3"
regex = "^1.5"
itertools = "^0.10.3"
thiserror = "^1.0.16"
[dependencies.wasm-bindgen]
version = "^0.2.83"
version = "^0.2"
#features = ["std"]
features = ["serde-serialize", "std"]
@ -57,6 +55,5 @@ features = ["console"]
[dev-dependencies]
futures = "^0.1"
proptest = { version = "^1.0.0", default-features = false, features = ["std"] }
wasm-bindgen-futures = "^0.4"
wasm-bindgen-test = "^0.3"

View file

@ -18,6 +18,34 @@ An Object id uniquely identifies a Map, List or Text object within a document.
Heads refers to a set of hashes that uniquely identifies a point in time in a document's history. Heads are useful for comparing documents state or retrieving past states from the document.
### Using the Library and Creating a Document
This is a rust/wasm package and will work in a node or web environment. Node is able to load wasm synchronously but a web environment is not. The default import of the package is a function that returns a promise that resolves once the wasm is loaded.
This creates a document in node. The memory allocated is handled by wasm and isn't managed by the javascript garbage collector and thus needs to be manually freed.
```javascript
import { create } from "automerge-wasm"
let doc = create()
doc.free()
```
While this will work in both node and in a web context
```javascript
import init, { create } from "automerge-wasm"
init().then(_ => {
let doc = create()
doc.free()
})
```
The examples below will assume a node context for brevity.
### Automerge Scalar Types
Automerge has many scalar types. Methods like `put()` and `insert()` take an optional data type parameter. Normally the type can be inferred but in some cases, such as telling the difference between int, uint and a counter, it cannot.
@ -25,7 +53,7 @@ Automerge has many scalar types. Methods like `put()` and `insert()` take an op
These are puts without a data type
```javascript
import { create } from "@automerge/automerge-wasm"
import { create } from "automerge-wasm"
let doc = create()
doc.put("/", "prop1", 100) // int
@ -35,6 +63,7 @@ These are puts without a data type
doc.put("/", "prop5", new Uint8Array([1,2,3]))
doc.put("/", "prop6", true)
doc.put("/", "prop7", null)
doc.free()
```
Put's with a data type and examples of all the supported data types.
@ -42,7 +71,7 @@ Put's with a data type and examples of all the supported data types.
While int vs uint vs f64 matters little in javascript, Automerge is a cross platform library where these distinctions matter.
```javascript
import { create } from "@automerge/automerge-wasm"
import { create } from "automerge-wasm"
let doc = create()
doc.put("/", "prop1", 100, "int")
@ -55,6 +84,7 @@ While int vs uint vs f64 matters little in javascript, Automerge is a cross plat
doc.put("/", "prop8", new Uint8Array([1,2,3]), "bytes")
doc.put("/", "prop9", true, "boolean")
doc.put("/", "prop10", null, "null")
doc.free()
```
### Automerge Object Types
@ -62,7 +92,7 @@ While int vs uint vs f64 matters little in javascript, Automerge is a cross plat
Automerge WASM supports 3 object types. Maps, lists, and text. Maps are key value stores where the values can be any scalar type or any object type. Lists are numerically indexed sets of data that can hold any scalar or any object type.
```javascript
import { create } from "@automerge/automerge-wasm"
import { create } from "automerge-wasm"
let doc = create()
@ -81,12 +111,14 @@ Automerge WASM supports 3 object types. Maps, lists, and text. Maps are key va
// text is initialized with a string
let notes = doc.putObject("/", "notes", "Hello world!")
doc.free()
```
You can access objects by passing the object id as the first parameter for a call.
```javascript
import { create } from "@automerge/automerge-wasm"
import { create } from "automerge-wasm"
let doc = create()
@ -99,10 +131,7 @@ You can access objects by passing the object id as the first parameter for a cal
// get the id then use it
// get returns a single simple javascript value or undefined
// getWithType returns an Array of the datatype plus basic type or null
let id = doc.getWithType("/", "config")
let id = doc.get("/", "config")
if (id && id[0] === 'map') {
doc.put(id[1], "align", "right")
}
@ -110,6 +139,8 @@ You can access objects by passing the object id as the first parameter for a cal
// use a path instead
doc.put("/config", "align", "right")
doc.free()
```
Using the id directly is always faster (as it prevents the path to id conversion internally) so it is preferred for performance critical code.
@ -131,6 +162,7 @@ Maps are key/value stores. The root object is always a map. The keys are alway
doc.keys(mymap) // returns ["bytes","foo","sub"]
doc.materialize("_root") // returns { mymap: { bytes: new Uint8Array([1,2,3]), foo: "bar", sub: {}}}
doc.free()
```
### Lists
@ -150,11 +182,12 @@ Lists are index addressable sets of values. These values can be any scalar or o
doc.materialize(items) // returns [ "bat", [1,2], { hello : "world" }, true, "bag", "brick"]
doc.length(items) // returns 6
doc.free()
```
### Text
Text is a specialized list type intended for modifying a text document. The primary way to interact with a text document is via the `splice()` method. Spliced strings will be indexable by character (important to note for platforms that index by graphmeme cluster).
Text is a specialized list type intended for modifying a text document. The primary way to interact with a text document is via the `splice()` method. Spliced strings will be indexable by character (important to note for platforms that index by graphmeme cluster). Non text can be inserted into a text document and will be represented with the unicode object replacement character.
```javascript
let doc = create("aaaaaa")
@ -162,6 +195,13 @@ Text is a specialized list type intended for modifying a text document. The pri
doc.splice(notes, 6, 5, "everyone")
doc.text(notes) // returns "Hello everyone"
let obj = doc.insertObject(notes, 6, { hi: "there" })
doc.text(notes) // returns "Hello \ufffceveryone"
doc.get(notes, 6) // returns ["map", obj]
doc.get(obj, "hi") // returns ["str", "there"]
doc.free()
```
### Tables
@ -177,8 +217,8 @@ When querying maps use the `get()` method with the object in question and the pr
doc1.put("_root", "key1", "val1")
let key2 = doc1.putObject("_root", "key2", [])
doc1.get("_root", "key1") // returns "val1"
doc1.getWithType("_root", "key2") // returns ["list", "2@aabbcc"]
doc1.get("_root", "key1") // returns ["str", "val1"]
doc1.get("_root", "key2") // returns ["list", "2@aabbcc"]
doc1.keys("_root") // returns ["key1", "key2"]
let doc2 = doc1.fork("ffaaff")
@ -189,8 +229,9 @@ When querying maps use the `get()` method with the object in question and the pr
doc1.merge(doc2)
doc1.get("_root","key3") // returns "doc2val"
doc1.get("_root","key3") // returns ["str", "doc2val"]
doc1.getAll("_root","key3") // returns [[ "str", "doc1val"], ["str", "doc2val"]]
doc1.free(); doc2.free()
```
### Counters
@ -212,6 +253,8 @@ Counters are 64 bit ints that support the increment operation. Frequently diffe
doc1.merge(doc2)
doc1.materialize("_root") // returns { number: 10, total: 33 }
doc1.free(); doc2.free()
```
### Transactions
@ -223,7 +266,7 @@ Generally speaking you don't need to think about transactions when using Automer
doc.put("_root", "key", "val1")
doc.get("_root", "key") // returns "val1"
doc.get("_root", "key") // returns ["str","val1"]
doc.pendingOps() // returns 1
doc.rollback()
@ -237,8 +280,10 @@ Generally speaking you don't need to think about transactions when using Automer
doc.commit("test commit 1")
doc.get("_root", "key") // returns "val2"
doc.get("_root", "key") // returns ["str","val2"]
doc.pendingOps() // returns 0
doc.free()
```
### Viewing Old Versions of the Document
@ -256,10 +301,12 @@ All query functions can take an optional argument of `heads` which allow you to
doc.put("_root", "key", "val3")
doc.get("_root","key") // returns "val3"
doc.get("_root","key",heads2) // returns "val2"
doc.get("_root","key",heads1) // returns "val1"
doc.get("_root","key",[]) // returns undefined
doc.get("_root","key") // returns ["str","val3"]
doc.get("_root","key",heads2) // returns ["str","val2"]
doc.get("_root","key",heads1) // returns ["str","val1"]
doc.get("_root","key",[]) // returns null
doc.free()
```
This works for `get()`, `getAll()`, `keys()`, `length()`, `text()`, and `materialize()`
@ -285,6 +332,8 @@ The `merge()` command applies all changes in the argument doc into the calling d
doc1.materialize("_root") // returns { key1: "val1", key2: "val2", key3: "val3" }
doc2.materialize("_root") // returns { key1: "val1", key3: "val3" }
doc1.free(); doc2.free()
```
Note that calling `a.merge(a)` will produce an unrecoverable error from the wasm-bindgen layer which (as of this writing) there is no workaround for.
@ -298,7 +347,7 @@ If you wish to incrementally update a saved Automerge doc you can call `saveIncr
The `load()` function takes a `Uint8Array()` of bytes produced in this way and constitutes a new document. The `loadIncremental()` method is available if you wish to consume the result of a `saveIncremental()` with an already instanciated document.
```javascript
import { create, load } from "@automerge/automerge-wasm"
import { create, load } from "automerge-wasm"
let doc1 = create()
@ -330,12 +379,14 @@ The `load()` function takes a `Uint8Array()` of bytes produced in this way and c
doc2.materialize("_root") // returns { key1: "value1", key2: "value2" }
doc3.materialize("_root") // returns { key1: "value1", key2: "value2" }
doc4.materialize("_root") // returns { key1: "value1", key2: "value2" }
doc1.free(); doc2.free(); doc3.free(); doc4.free()
```
One interesting feature of automerge binary saves is that they can be concatenated together in any order and can still be loaded into a coherent merged document.
```javascript
import { load } from "@automerge/automerge-wasm"
import { load } from "automerge-wasm"
import * as fs from "fs"
let file1 = fs.readFileSync("automerge_save_1");
@ -355,7 +406,7 @@ When syncing a document the `generateSyncMessage()` and `receiveSyncMessage()` m
A very simple sync implementation might look like this.
```javascript
import { encodeSyncState, decodeSyncState, initSyncState } from "@automerge/automerge-wasm"
import { encodeSyncState, decodeSyncState, initSyncState } from "automerge-wasm"
let states = {}
@ -403,7 +454,7 @@ Actors are ids that need to be unique to each process writing to a document. Th
Methods that create new documents will generate random actors automatically - if you wish to supply your own it is always taken as an optional argument. This is true for the following functions.
```javascript
import { create, load } from "@automerge/automerge-wasm"
import { create, load } from "automerge-wasm"
let doc1 = create() // random actorid
let doc2 = create("aabbccdd")
@ -413,6 +464,8 @@ Methods that create new documents will generate random actors automatically - if
let doc6 = load(doc4.save(), "00aabb11")
let actor = doc1.getActor()
doc1.free(); doc2.free(); doc3.free(); doc4.free(); doc5.free(); doc6.free()
```
### Glossary: Object Id's
@ -435,35 +488,7 @@ Object Ids uniquely identify an object within a document. They are represented
doc.put(o1v2, "x", "y") // modifying the new "o1" object
assert.deepEqual(doc.materialize("_root"), { "o1": { x: "y" }, "o2": {} })
```
### Appendix: Building
The following steps should allow you to build the package
```
$ rustup target add wasm32-unknown-unknown
$ cargo install wasm-bindgen-cli
$ cargo install wasm-opt
$ yarn
$ yarn release
$ yarn pack
```
### Appendix: WASM and Memory Allocation
Allocated memory in rust will be freed automatically on platforms that support `FinalizationRegistry`.
This is currently supported in [all major browsers and nodejs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/FinalizationRegistry).
On unsupported platforms you can free memory explicitly.
```javascript
import { create, initSyncState } from "@automerge/automerge-wasm"
let doc = create()
let sync = initSyncState()
doc.free()
sync.free()
```

View file

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View file

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View file

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Some files were not shown because too many files have changed in this diff Show more