Compare commits

..

No commits in common. "main" and "rust/automerge-test@0.2.0" have entirely different histories.

142 changed files with 10064 additions and 11367 deletions

View file

@ -2,10 +2,10 @@ name: CI
on:
push:
branches:
- main
- main
pull_request:
branches:
- main
- main
jobs:
fmt:
runs-on: ubuntu-latest
@ -14,7 +14,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.67.0
toolchain: 1.66.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.66.0
default: true
components: clippy
- uses: Swatinem/rust-cache@v1
@ -42,7 +42,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.67.0
toolchain: 1.66.0
default: true
- uses: Swatinem/rust-cache@v1
- name: Build rust docs
@ -118,7 +118,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: nightly-2023-01-26
toolchain: 1.66.0
default: true
- uses: Swatinem/rust-cache@v1
- name: Install CMocka
@ -127,8 +127,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 +136,9 @@ jobs:
strategy:
matrix:
toolchain:
- 1.67.0
- 1.66.0
- nightly
continue-on-error: ${{ matrix.toolchain == 'nightly' }}
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
@ -157,7 +157,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.67.0
toolchain: 1.66.0
default: true
- uses: Swatinem/rust-cache@v1
- run: ./scripts/ci/build-test
@ -170,7 +170,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.67.0
toolchain: 1.66.0
default: true
- uses: Swatinem/rust-cache@v1
- run: ./scripts/ci/build-test

View file

@ -57,6 +57,7 @@ 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)
## Repository Organisation
- `./rust` - the rust rust implementation and also the Rust components of
@ -112,16 +113,9 @@ 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

View file

@ -54,7 +54,6 @@
nodejs
yarn
deno
# c deps
cmake

View file

@ -5845,9 +5845,9 @@ json-stable-stringify-without-jsonify@^1.0.1:
integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==
json5@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593"
integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==
version "1.0.1"
resolved "http://localhost:4873/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe"
integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==
dependencies:
minimist "^1.2.0"
@ -6165,9 +6165,9 @@ minimatch@^5.0.1:
brace-expansion "^2.0.1"
minimist@^1.2.0, minimist@^1.2.6:
version "1.2.7"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18"
integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==
version "1.2.6"
resolved "http://localhost:4873/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
mkdirp@~0.5.1:
version "0.5.6"

View file

@ -4,7 +4,7 @@
"Orion Henry <orion@inkandswitch.com>",
"Martin Kleppmann"
],
"version": "2.0.2",
"version": "2.0.1",
"description": "Javascript implementation of automerge, backed by @automerge/automerge-wasm",
"homepage": "https://github.com/automerge/automerge-rs/tree/main/wrappers/javascript",
"repository": "github:automerge/automerge-rs",
@ -47,7 +47,7 @@
"typescript": "^4.9.4"
},
"dependencies": {
"@automerge/automerge-wasm": "0.1.25",
"@automerge/automerge-wasm": "0.1.23",
"uuid": "^9.0.0"
}
}

View file

@ -26,7 +26,7 @@ import { Text } from "./text"
export { Text } from "./text"
import type {
API as WasmAPI,
API,
Actor as ActorId,
Prop,
ObjID,
@ -34,7 +34,7 @@ import type {
DecodedChange,
Heads,
MaterializeValue,
JsSyncState,
JsSyncState as SyncState,
SyncMessage,
DecodedSyncMessage,
} from "@automerge/automerge-wasm"
@ -46,17 +46,6 @@ export type {
IncPatch,
SyncMessage,
} from "@automerge/automerge-wasm"
/** @hidden **/
type API = WasmAPI
const SyncStateSymbol = Symbol("_syncstate")
/**
* An opaque type tracking the state of sync with a remote peer
*/
type SyncState = JsSyncState & { _opaque: typeof SyncStateSymbol }
import { ApiHandler, type ChangeToEncode, UseApi } from "./low_level"
import { Automerge } from "@automerge/automerge-wasm"
@ -305,7 +294,7 @@ export function from<T extends Record<string, unknown>>(
* @example A change with a message and a timestamp
*
* ```
* doc1 = automerge.change(doc1, {message: "add another value", time: 1640995200}, d => {
* doc1 = automerge.change(doc1, {message: "add another value", timestamp: 1640995200}, d => {
* d.key2 = "value2"
* })
* ```
@ -316,7 +305,7 @@ export function from<T extends Record<string, unknown>>(
* let patchCallback = patch => {
* patchedPath = patch.path
* }
* doc1 = automerge.change(doc1, {message, "add another value", time: 1640995200, patchCallback}, d => {
* doc1 = automerge.change(doc1, {message, "add another value", timestamp: 1640995200, patchCallback}, d => {
* d.key2 = "value2"
* })
* assert.equal(patchedPath, ["key2"])
@ -783,7 +772,7 @@ export function decodeSyncState(state: Uint8Array): SyncState {
const sync = ApiHandler.decodeSyncState(state)
const result = ApiHandler.exportSyncState(sync)
sync.free()
return result as SyncState
return result
}
/**
@ -804,7 +793,7 @@ export function generateSyncMessage<T>(
const state = _state(doc)
const syncState = ApiHandler.importSyncState(inState)
const message = state.handle.generateSyncMessage(syncState)
const outState = ApiHandler.exportSyncState(syncState) as SyncState
const outState = ApiHandler.exportSyncState(syncState)
return [outState, message]
}
@ -846,7 +835,7 @@ export function receiveSyncMessage<T>(
}
const heads = state.handle.getHeads()
state.handle.receiveSyncMessage(syncState, message)
const outSyncState = ApiHandler.exportSyncState(syncState) as SyncState
const outSyncState = ApiHandler.exportSyncState(syncState)
return [
progressDocument(doc, heads, opts.patchCallback || state.patchCallback),
outSyncState,
@ -863,7 +852,7 @@ export function receiveSyncMessage<T>(
* @group sync
*/
export function initSyncState(): SyncState {
return ApiHandler.exportSyncState(ApiHandler.initSyncState()) as SyncState
return ApiHandler.exportSyncState(ApiHandler.initSyncState())
}
/** @hidden */

View file

@ -58,22 +58,6 @@ describe("Automerge", () => {
})
})
it("should be able to insert and delete a large number of properties", () => {
let doc = Automerge.init<any>()
doc = Automerge.change(doc, doc => {
doc["k1"] = true
})
for (let idx = 1; idx <= 200; idx++) {
doc = Automerge.change(doc, doc => {
delete doc["k" + idx]
doc["k" + (idx + 1)] = true
assert(Object.keys(doc).length == 1)
})
}
})
it("can detect an automerge doc with isAutomerge()", () => {
const doc1 = Automerge.from({ sub: { object: true } })
assert(Automerge.isAutomerge(doc1))

View file

@ -1849,8 +1849,9 @@ describe("Automerge", () => {
})
assert.deepStrictEqual(patches, [
{ action: "put", path: ["birds"], value: [] },
{ action: "insert", path: ["birds", 0], values: ["", ""] },
{ action: "insert", path: ["birds", 0], values: [""] },
{ action: "splice", path: ["birds", 0, 0], value: "Goldfinch" },
{ action: "insert", path: ["birds", 1], values: [""] },
{ action: "splice", path: ["birds", 1, 0], value: "Chaffinch" },
])
})

View file

@ -10,8 +10,13 @@ members = [
resolver = "2"
[profile.release]
debug = true
lto = true
codegen-units = 1
opt-level = 3
[profile.bench]
debug = true
debug = true
[profile.release.package.automerge-wasm]
debug = false
opt-level = 3

View file

@ -1,250 +0,0 @@
---
Language: Cpp
# BasedOnStyle: Chromium
AccessModifierOffset: -1
AlignAfterOpenBracket: Align
AlignArrayOfStructures: None
AlignConsecutiveAssignments:
Enabled: false
AcrossEmptyLines: false
AcrossComments: false
AlignCompound: false
PadOperators: true
AlignConsecutiveBitFields:
Enabled: false
AcrossEmptyLines: false
AcrossComments: false
AlignCompound: false
PadOperators: false
AlignConsecutiveDeclarations:
Enabled: false
AcrossEmptyLines: false
AcrossComments: false
AlignCompound: false
PadOperators: false
AlignConsecutiveMacros:
Enabled: false
AcrossEmptyLines: false
AcrossComments: false
AlignCompound: false
PadOperators: false
AlignEscapedNewlines: Left
AlignOperands: Align
AlignTrailingComments: true
AllowAllArgumentsOnNextLine: true
AllowAllParametersOfDeclarationOnNextLine: false
AllowShortEnumsOnASingleLine: true
AllowShortBlocksOnASingleLine: Never
AllowShortCaseLabelsOnASingleLine: false
AllowShortFunctionsOnASingleLine: Inline
AllowShortLambdasOnASingleLine: All
AllowShortIfStatementsOnASingleLine: Never
AllowShortLoopsOnASingleLine: false
AlwaysBreakAfterDefinitionReturnType: None
AlwaysBreakAfterReturnType: None
AlwaysBreakBeforeMultilineStrings: true
AlwaysBreakTemplateDeclarations: Yes
AttributeMacros:
- __capability
BinPackArguments: true
BinPackParameters: false
BraceWrapping:
AfterCaseLabel: false
AfterClass: false
AfterControlStatement: Never
AfterEnum: false
AfterFunction: false
AfterNamespace: false
AfterObjCDeclaration: false
AfterStruct: false
AfterUnion: false
AfterExternBlock: false
BeforeCatch: false
BeforeElse: false
BeforeLambdaBody: false
BeforeWhile: false
IndentBraces: false
SplitEmptyFunction: true
SplitEmptyRecord: true
SplitEmptyNamespace: true
BreakBeforeBinaryOperators: None
BreakBeforeConceptDeclarations: Always
BreakBeforeBraces: Attach
BreakBeforeInheritanceComma: false
BreakInheritanceList: BeforeColon
BreakBeforeTernaryOperators: true
BreakConstructorInitializersBeforeComma: false
BreakConstructorInitializers: BeforeColon
BreakAfterJavaFieldAnnotations: false
BreakStringLiterals: true
ColumnLimit: 120
CommentPragmas: '^ IWYU pragma:'
QualifierAlignment: Leave
CompactNamespaces: false
ConstructorInitializerIndentWidth: 4
ContinuationIndentWidth: 4
Cpp11BracedListStyle: true
DeriveLineEnding: true
DerivePointerAlignment: false
DisableFormat: false
EmptyLineAfterAccessModifier: Never
EmptyLineBeforeAccessModifier: LogicalBlock
ExperimentalAutoDetectBinPacking: false
PackConstructorInitializers: NextLine
BasedOnStyle: ''
ConstructorInitializerAllOnOneLineOrOnePerLine: false
AllowAllConstructorInitializersOnNextLine: true
FixNamespaceComments: true
ForEachMacros:
- foreach
- Q_FOREACH
- BOOST_FOREACH
IfMacros:
- KJ_IF_MAYBE
IncludeBlocks: Preserve
IncludeCategories:
- Regex: '^<ext/.*\.h>'
Priority: 2
SortPriority: 0
CaseSensitive: false
- Regex: '^<.*\.h>'
Priority: 1
SortPriority: 0
CaseSensitive: false
- Regex: '^<.*'
Priority: 2
SortPriority: 0
CaseSensitive: false
- Regex: '.*'
Priority: 3
SortPriority: 0
CaseSensitive: false
IncludeIsMainRegex: '([-_](test|unittest))?$'
IncludeIsMainSourceRegex: ''
IndentAccessModifiers: false
IndentCaseLabels: true
IndentCaseBlocks: false
IndentGotoLabels: true
IndentPPDirectives: None
IndentExternBlock: AfterExternBlock
IndentRequiresClause: true
IndentWidth: 4
IndentWrappedFunctionNames: false
InsertBraces: false
InsertTrailingCommas: None
JavaScriptQuotes: Leave
JavaScriptWrapImports: true
KeepEmptyLinesAtTheStartOfBlocks: false
LambdaBodyIndentation: Signature
MacroBlockBegin: ''
MacroBlockEnd: ''
MaxEmptyLinesToKeep: 1
NamespaceIndentation: None
ObjCBinPackProtocolList: Never
ObjCBlockIndentWidth: 2
ObjCBreakBeforeNestedBlockParam: true
ObjCSpaceAfterProperty: false
ObjCSpaceBeforeProtocolList: true
PenaltyBreakAssignment: 2
PenaltyBreakBeforeFirstCallParameter: 1
PenaltyBreakComment: 300
PenaltyBreakFirstLessLess: 120
PenaltyBreakOpenParenthesis: 0
PenaltyBreakString: 1000
PenaltyBreakTemplateDeclaration: 10
PenaltyExcessCharacter: 1000000
PenaltyReturnTypeOnItsOwnLine: 200
PenaltyIndentedWhitespace: 0
PointerAlignment: Left
PPIndentWidth: -1
RawStringFormats:
- Language: Cpp
Delimiters:
- cc
- CC
- cpp
- Cpp
- CPP
- 'c++'
- 'C++'
CanonicalDelimiter: ''
BasedOnStyle: google
- Language: TextProto
Delimiters:
- pb
- PB
- proto
- PROTO
EnclosingFunctions:
- EqualsProto
- EquivToProto
- PARSE_PARTIAL_TEXT_PROTO
- PARSE_TEST_PROTO
- PARSE_TEXT_PROTO
- ParseTextOrDie
- ParseTextProtoOrDie
- ParseTestProto
- ParsePartialTestProto
CanonicalDelimiter: pb
BasedOnStyle: google
ReferenceAlignment: Pointer
ReflowComments: true
RemoveBracesLLVM: false
RequiresClausePosition: OwnLine
SeparateDefinitionBlocks: Leave
ShortNamespaceLines: 1
SortIncludes: CaseSensitive
SortJavaStaticImport: Before
SortUsingDeclarations: true
SpaceAfterCStyleCast: false
SpaceAfterLogicalNot: false
SpaceAfterTemplateKeyword: true
SpaceBeforeAssignmentOperators: true
SpaceBeforeCaseColon: false
SpaceBeforeCpp11BracedList: false
SpaceBeforeCtorInitializerColon: true
SpaceBeforeInheritanceColon: true
SpaceBeforeParens: ControlStatements
SpaceBeforeParensOptions:
AfterControlStatements: true
AfterForeachMacros: true
AfterFunctionDefinitionName: false
AfterFunctionDeclarationName: false
AfterIfMacros: true
AfterOverloadedOperator: false
AfterRequiresInClause: false
AfterRequiresInExpression: false
BeforeNonEmptyParentheses: false
SpaceAroundPointerQualifiers: Default
SpaceBeforeRangeBasedForLoopColon: true
SpaceInEmptyBlock: false
SpaceInEmptyParentheses: false
SpacesBeforeTrailingComments: 2
SpacesInAngles: Never
SpacesInConditionalStatement: false
SpacesInContainerLiterals: true
SpacesInCStyleCastParentheses: false
SpacesInLineCommentPrefix:
Minimum: 1
Maximum: -1
SpacesInParentheses: false
SpacesInSquareBrackets: false
SpaceBeforeSquareBrackets: false
BitFieldColonSpacing: Both
Standard: Auto
StatementAttributeLikeMacros:
- Q_EMIT
StatementMacros:
- Q_UNUSED
- QT_REQUIRE_VERSION
TabWidth: 8
UseCRLF: false
UseTab: Never
WhitespaceSensitiveMacros:
- STRINGIZE
- PP_STRINGIZE
- BOOST_PP_STRINGIZE
- NS_SWIFT_NAME
- CF_SWIFT_NAME
...

View file

@ -1,10 +1,10 @@
automerge
automerge.h
automerge.o
build/
CMakeCache.txt
*.cmake
CMakeFiles
CMakePresets.json
Makefile
DartConfiguration.tcl
out/
config.h
CMakeCache.txt
Cargo

View file

@ -1,297 +1,97 @@
cmake_minimum_required(VERSION 3.23 FATAL_ERROR)
cmake_minimum_required(VERSION 3.18 FATAL_ERROR)
project(automerge-c VERSION 0.1.0
LANGUAGES C
DESCRIPTION "C bindings for the Automerge Rust library.")
set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
set(LIBRARY_NAME "automerge")
# Parse the library name, project name and project version out of Cargo's TOML file.
set(CARGO_LIB_SECTION OFF)
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
set(LIBRARY_NAME "")
option(BUILD_SHARED_LIBS "Enable the choice of a shared or static library.")
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 0.0.1 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)
set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
string(MAKE_C_IDENTIFIER ${PROJECT_NAME} SYMBOL_PREFIX)
string(TOUPPER ${SYMBOL_PREFIX} SYMBOL_PREFIX)
set(CARGO_TARGET_DIR "${CMAKE_BINARY_DIR}/Cargo/target")
set(CARGO_TARGET_DIR "${CMAKE_CURRENT_BINARY_DIR}/Cargo/target")
set(CBINDGEN_INCLUDEDIR "${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_INCLUDEDIR}")
set(CBINDGEN_INCLUDEDIR "${CARGO_TARGET_DIR}/${CMAKE_INSTALL_INCLUDEDIR}")
set(CBINDGEN_TARGET_DIR "${CBINDGEN_INCLUDEDIR}/${PROJECT_NAME}")
find_program (
CARGO_CMD
"cargo"
PATHS "$ENV{CARGO_HOME}/bin"
DOC "The Cargo command"
)
add_subdirectory(src)
if(NOT CARGO_CMD)
message(FATAL_ERROR "Cargo (Rust package manager) not found! "
"Please install it and/or set the CARGO_HOME "
"environment variable to its path.")
endif()
string(TOLOWER "${CMAKE_BUILD_TYPE}" BUILD_TYPE_LOWER)
# In order to build with -Z build-std, we need to pass target explicitly.
# https://doc.rust-lang.org/cargo/reference/unstable.html#build-std
execute_process (
COMMAND rustc -vV
OUTPUT_VARIABLE RUSTC_VERSION
OUTPUT_STRIP_TRAILING_WHITESPACE
)
string(REGEX REPLACE ".*host: ([^ \n]*).*" "\\1"
CARGO_TARGET
${RUSTC_VERSION}
)
if(BUILD_TYPE_LOWER STREQUAL debug)
set(CARGO_BUILD_TYPE "debug")
set(CARGO_FLAG --target=${CARGO_TARGET})
else()
set(CARGO_BUILD_TYPE "release")
if (NOT RUSTC_VERSION MATCHES "nightly")
set(RUSTUP_TOOLCHAIN nightly)
endif()
set(RUSTFLAGS -C\ panic=abort)
set(CARGO_FLAG -Z build-std=std,panic_abort --release --target=${CARGO_TARGET})
endif()
set(CARGO_FEATURES "")
set(CARGO_BINARY_DIR "${CARGO_TARGET_DIR}/${CARGO_TARGET}/${CARGO_BUILD_TYPE}")
set(BINDINGS_NAME "${LIBRARY_NAME}_core")
configure_file(
${CMAKE_MODULE_PATH}/Cargo.toml.in
${CMAKE_SOURCE_DIR}/Cargo.toml
@ONLY
NEWLINE_STYLE LF
)
set(INCLUDE_GUARD_PREFIX "${SYMBOL_PREFIX}")
configure_file(
${CMAKE_MODULE_PATH}/cbindgen.toml.in
${CMAKE_SOURCE_DIR}/cbindgen.toml
@ONLY
NEWLINE_STYLE LF
)
set(CARGO_OUTPUT
${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h
${CARGO_BINARY_DIR}/${CMAKE_STATIC_LIBRARY_PREFIX}${BINDINGS_NAME}${CMAKE_STATIC_LIBRARY_SUFFIX}
)
# \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(
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 -- ${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h ${CMAKE_SOURCE_DIR}/cbindgen.toml
COMMAND
${CMAKE_COMMAND} -E env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} CBINDGEN_TARGET_DIR=${CBINDGEN_TARGET_DIR} RUSTUP_TOOLCHAIN=${RUSTUP_TOOLCHAIN} RUSTFLAGS=${RUSTFLAGS} ${CARGO_CMD} build ${CARGO_FLAG} ${CARGO_FEATURES}
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 -- ${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h
COMMAND
# Compensate for cbindgen ignoring `std:mem::size_of<usize>()` calls.
${CMAKE_COMMAND} -DMATCH_REGEX=USIZE_ -DREPLACE_EXPR=\+${CMAKE_SIZEOF_VOID_P} -P ${CMAKE_SOURCE_DIR}/cmake/file-regex-replace.cmake -- ${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h
MAIN_DEPENDENCY
src/lib.rs
DEPENDS
src/actor_id.rs
src/byte_span.rs
src/change.rs
src/doc.rs
src/doc/list.rs
src/doc/map.rs
src/doc/utils.rs
src/index.rs
src/item.rs
src/items.rs
src/obj.rs
src/result.rs
src/sync.rs
src/sync/have.rs
src/sync/message.rs
src/sync/state.rs
${CMAKE_SOURCE_DIR}/build.rs
${CMAKE_MODULE_PATH}/Cargo.toml.in
${CMAKE_MODULE_PATH}/cbindgen.toml.in
WORKING_DIRECTORY
${CMAKE_SOURCE_DIR}
COMMENT
"Producing the bindings' artifacts with Cargo..."
VERBATIM
)
add_custom_target(${BINDINGS_NAME}_artifacts ALL
DEPENDS ${CARGO_OUTPUT}
)
add_library(${BINDINGS_NAME} STATIC IMPORTED GLOBAL)
target_include_directories(${BINDINGS_NAME} INTERFACE "${CBINDGEN_INCLUDEDIR}")
set_target_properties(
${BINDINGS_NAME}
PROPERTIES
# \note Cargo writes a debug build into a nested directory instead of
# decorating its name.
DEBUG_POSTFIX ""
DEFINE_SYMBOL ""
IMPORTED_IMPLIB ""
IMPORTED_LOCATION "${CARGO_BINARY_DIR}/${CMAKE_STATIC_LIBRARY_PREFIX}${BINDINGS_NAME}${CMAKE_STATIC_LIBRARY_SUFFIX}"
IMPORTED_NO_SONAME "TRUE"
IMPORTED_SONAME ""
LINKER_LANGUAGE C
PUBLIC_HEADER "${CBINDGEN_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(${BINDINGS_NAME} INTERFACE $<TARGET_PROPERTY:${BINDINGS_NAME},DEFINE_SYMBOL>)
set(UTILS_SUBDIR "utils")
add_custom_command(
OUTPUT
${CBINDGEN_TARGET_DIR}/${UTILS_SUBDIR}/enum_string.h
${CMAKE_BINARY_DIR}/src/${UTILS_SUBDIR}/enum_string.c
COMMAND
${CMAKE_COMMAND} -DPROJECT_NAME=${PROJECT_NAME} -DLIBRARY_NAME=${LIBRARY_NAME} -DSUBDIR=${UTILS_SUBDIR} -P ${CMAKE_SOURCE_DIR}/cmake/enum-string-functions-gen.cmake -- ${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h ${CBINDGEN_TARGET_DIR}/${UTILS_SUBDIR}/enum_string.h ${CMAKE_BINARY_DIR}/src/${UTILS_SUBDIR}/enum_string.c
MAIN_DEPENDENCY
${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h
DEPENDS
${CMAKE_SOURCE_DIR}/cmake/enum-string-functions-gen.cmake
WORKING_DIRECTORY
${CMAKE_SOURCE_DIR}
COMMENT
"Generating the enum string functions with CMake..."
VERBATIM
)
add_custom_target(${LIBRARY_NAME}_utilities
DEPENDS ${CBINDGEN_TARGET_DIR}/${UTILS_SUBDIR}/enum_string.h
${CMAKE_BINARY_DIR}/src/${UTILS_SUBDIR}/enum_string.c
)
add_library(${LIBRARY_NAME})
target_compile_features(${LIBRARY_NAME} PRIVATE c_std_99)
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}
PUBLIC ${BINDINGS_NAME}
${LIBRARY_DEPENDENCIES}
)
# \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}
PUBLIC "$<BUILD_INTERFACE:${CBINDGEN_INCLUDEDIR};${CMAKE_SOURCE_DIR}/${CMAKE_INSTALL_INCLUDEDIR}>"
"$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>"
)
add_dependencies(${LIBRARY_NAME} ${BINDINGS_NAME}_artifacts)
# Generate the configuration header.
# 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}")
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
${CBINDGEN_TARGET_DIR}/config.h
config.h
@ONLY
NEWLINE_STYLE LF
)
target_sources(${LIBRARY_NAME}
PRIVATE
src/${UTILS_SUBDIR}/result.c
src/${UTILS_SUBDIR}/stack_callback_data.c
src/${UTILS_SUBDIR}/stack.c
src/${UTILS_SUBDIR}/string.c
${CMAKE_BINARY_DIR}/src/${UTILS_SUBDIR}/enum_string.c
PUBLIC
FILE_SET api TYPE HEADERS
BASE_DIRS
${CBINDGEN_INCLUDEDIR}
${CMAKE_SOURCE_DIR}/${CMAKE_INSTALL_INCLUDEDIR}
FILES
${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h
${CBINDGEN_TARGET_DIR}/${UTILS_SUBDIR}/enum_string.h
${CMAKE_SOURCE_DIR}/${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}/${UTILS_SUBDIR}/result.h
${CMAKE_SOURCE_DIR}/${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}/${UTILS_SUBDIR}/stack_callback_data.h
${CMAKE_SOURCE_DIR}/${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}/${UTILS_SUBDIR}/stack.h
${CMAKE_SOURCE_DIR}/${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}/${UTILS_SUBDIR}/string.h
INTERFACE
FILE_SET config TYPE HEADERS
BASE_DIRS
${CBINDGEN_INCLUDEDIR}
FILES
${CBINDGEN_TARGET_DIR}/config.h
)
install(
TARGETS ${LIBRARY_NAME}
EXPORT ${PROJECT_NAME}-config
FILE_SET api
FILE_SET config
)
# \note Install the Cargo-built core bindings to enable direct linkage.
install(
FILES $<TARGET_PROPERTY:${BINDINGS_NAME},IMPORTED_LOCATION>
DESTINATION ${CMAKE_INSTALL_LIBDIR}
)
install(EXPORT ${PROJECT_NAME}-config
FILE ${PROJECT_NAME}-config.cmake
NAMESPACE "${PROJECT_NAME}::"
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${LIB}
FILES ${CMAKE_BINARY_DIR}/config.h
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}
)
if(BUILD_TESTING)
@ -300,6 +100,42 @@ if(BUILD_TESTING)
enable_testing()
endif()
add_subdirectory(docs)
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

View file

@ -1,29 +1,22 @@
# Overview
automerge-c exposes an API to C that can either be used directly or as a basis
for other language bindings that have good support for calling into C functions.
automerge-c exposes a C API that can either be used directly or as the basis
for other language bindings that have good support for calling C functions.
# Building
# Installing
See the main README for instructions on getting your environment set up, then
you can use `./scripts/ci/cmake-build Release static` to build automerge-c.
See the main README for instructions on getting your environment set up and then
you can build the automerge-c library and install its constituent files within
a root directory of your choosing (e.g. "/usr/local") like so:
```shell
cmake -E make_directory automerge-c/build
cmake -S automerge-c -B automerge-c/build
cmake --build automerge-c/build
cmake --install automerge-c/build --prefix "/usr/local"
```
Installation is important because the name, location and structure of CMake's
out-of-source build subdirectory is subject to change based on the platform and
the release version; generated headers like `automerge-c/config.h` and
`automerge-c/utils/enum_string.h` are only sure to be found within their
installed locations.
It will output two files:
It's not obvious because they are versioned but the `Cargo.toml` and
`cbindgen.toml` configuration files are also generated in order to ensure that
the project name, project version and library name that they contain match those
specified within the top-level `CMakeLists.txt` file.
- ./build/Cargo/target/include/automerge-c/automerge.h
- ./build/Cargo/target/release/libautomerge.a
To use these in your application you must arrange for your C compiler to find
these files, either by moving them to the right location on your computer, or
by configuring the compiler to reference these directories.
- `export LDFLAGS=-L./build/Cargo/target/release -lautomerge`
- `export CFLAGS=-I./build/Cargo/target/include`
If you'd like to cross compile the library for different platforms you can do so
using [cross](https://github.com/cross-rs/cross). For example:
@ -32,176 +25,134 @@ using [cross](https://github.com/cross-rs/cross). For example:
This will output a shared library in the directory `rust/target/aarch64-unknown-linux-gnu/release/`.
You can replace `aarch64-unknown-linux-gnu` with any
[cross supported targets](https://github.com/cross-rs/cross#supported-targets).
The targets below are known to work, though other targets are expected to work
too:
You can replace `aarch64-unknown-linux-gnu` with any [cross supported targets](https://github.com/cross-rs/cross#supported-targets). The targets below are known to work, though other targets are expected to work too:
- `x86_64-apple-darwin`
- `aarch64-apple-darwin`
- `x86_64-unknown-linux-gnu`
- `aarch64-unknown-linux-gnu`
As a caveat, CMake generates the `automerge.h` header file in terms of the
processor architecture of the computer on which it was built so, for example,
don't use a header generated for a 64-bit processor if your target is a 32-bit
processor.
As a caveat, the header file is currently 32/64-bit dependant. You can re-use it
for all 64-bit architectures, but you must generate a specific header for 32-bit
targets.
# Usage
You can build and view the C API's HTML reference documentation like so:
```shell
cmake -E make_directory automerge-c/build
cmake -S automerge-c -B automerge-c/build
cmake --build automerge-c/build --target automerge_docs
firefox automerge-c/build/src/html/index.html
```
To get started quickly, look at the
For full reference, read through `automerge.h`, or to get started quickly look
at the
[examples](https://github.com/automerge/automerge-rs/tree/main/rust/automerge-c/examples).
Almost all operations in automerge-c act on an Automerge document
(`AMdoc` struct) which is structurally similar to a JSON document.
Almost all operations in automerge-c act on an AMdoc struct which you can get
from `AMcreate()` or `AMload()`. Operations on a given doc are not thread safe
so you must use a mutex or similar to avoid calling more than one function with
the same AMdoc pointer concurrently.
You can get a document by calling either `AMcreate()` or `AMload()`. Operations
on a given document are not thread-safe so you must use a mutex or similar to
avoid calling more than one function on the same one concurrently.
As with all functions that either allocate memory, or could fail if given
invalid input, `AMcreate()` returns an `AMresult`. The `AMresult` contains the
returned doc (or error message), and must be freed with `AMfree()` after you are
done to avoid leaking memory.
A C API function that could succeed or fail returns a result (`AMresult` struct)
containing a status code (`AMstatus` enum) and either a sequence of at least one
item (`AMitem` struct) or a read-only view onto a UTF-8 error message string
(`AMbyteSpan` struct).
An item contains up to three components: an index within its parent object
(`AMbyteSpan` struct or `size_t`), a unique identifier (`AMobjId` struct) and a
value.
The result of a successful function call that doesn't produce any values will
contain a single item that is void (`AM_VAL_TYPE_VOID`).
A returned result **must** be passed to `AMresultFree()` once the item(s) or
error message it contains is no longer needed in order to avoid a memory leak.
```
#include <stdio.h>
#include <stdlib.h>
#include <automerge-c/automerge.h>
#include <automerge-c/utils/string.h>
#include <stdio.h>
int main(int argc, char** argv) {
AMresult *docResult = AMcreate(NULL);
if (AMresultStatus(docResult) != AM_STATUS_OK) {
char* const err_msg = AMstrdup(AMresultError(docResult), NULL);
printf("failed to create doc: %s", err_msg);
free(err_msg);
printf("failed to create doc: %s", AMerrorMessage(docResult).src);
goto cleanup;
}
AMdoc *doc;
AMitemToDoc(AMresultItem(docResult), &doc);
AMdoc *doc = AMresultValue(docResult).doc;
// useful code goes here!
cleanup:
AMresultFree(docResult);
AMfree(docResult);
}
```
If you are writing an application in C, the `AMstackItem()`, `AMstackItems()`
and `AMstackResult()` functions enable the lifetimes of anonymous results to be
centrally managed and allow the same validation logic to be reused without
relying upon the `goto` statement (see examples/quickstart.c).
If you are writing code in C directly, you can use the `AMpush()` helper
function to reduce the boilerplate of error handling and freeing for you (see
examples/quickstart.c).
If you are wrapping automerge-c in another language, particularly one that has a
garbage collector, you can call the `AMresultFree()` function within a finalizer
to ensure that memory is reclaimed when it is no longer needed.
garbage collector, you can call `AMfree` within a finalizer to ensure that memory
is reclaimed when it is no longer needed.
Automerge documents consist of a mutable root which is always a map from string
keys to values. A value can be one of the following types:
An AMdoc wraps an automerge document which are very similar to JSON documents.
Automerge documents consist of a mutable root, which is always a map from string
keys to values. Values can have the following types:
- A number of type double / int64_t / uint64_t
- An explicit true / false / null
- An immutable UTF-8 string (`AMbyteSpan`).
- An immutable array of arbitrary bytes (`AMbyteSpan`).
- A mutable map from string keys to values.
- A mutable list of values.
- A mutable UTF-8 string.
- An explicit true / false / nul
- An immutable utf-8 string (AMbyteSpan)
- An immutable array of arbitrary bytes (AMbyteSpan)
- A mutable map from string keys to values (AMmap)
- A mutable list of values (AMlist)
- A mutable string (AMtext)
If you read from a location in the document with no value, an item with type
`AM_VAL_TYPE_VOID` will be returned, but you cannot write such a value
explicitly.
If you read from a location in the document with no value a value with
`.tag == AM_VALUE_VOID` will be returned, but you cannot write such a value explicitly.
Under the hood, automerge references a mutable object by its object identifier
where `AM_ROOT` signifies a document's root map object.
Under the hood, automerge references mutable objects by the internal object id,
and `AM_ROOT` is always the object id of the root value.
There are functions to put each type of value into either a map or a list, and
functions to read the current or a historical value from a map or a list. As (in general) collaborators
There is a function to put each type of value into either a map or a list, and a
function to read the current value from a list. As (in general) collaborators
may edit the document at any time, you cannot guarantee that the type of the
value at a given part of the document will stay the same. As a result, reading
from the document will return an `AMitem` struct that you can inspect to
determine the type of value that it contains.
value at a given part of the document will stay the same. As a result reading
from the document will return an `AMvalue` union that you can inspect to
determine its type.
Strings in automerge-c are represented using an `AMbyteSpan` which contains a
pointer and a length. Strings must be valid UTF-8 and may contain NUL (`0`)
characters.
For your convenience, you can call `AMstr()` to get the `AMbyteSpan` struct
equivalent of a null-terminated byte string or `AMstrdup()` to get the
representation of an `AMbyteSpan` struct as a null-terminated byte string
wherein its NUL characters have been removed/replaced as you choose.
pointer and a length. Strings must be valid utf-8 and may contain null bytes.
As a convenience you can use `AMstr()` to get the representation of a
null-terminated C string as an `AMbyteSpan`.
Putting all of that together, to read and write from the root of the document
you can do this:
```
#include <stdio.h>
#include <stdlib.h>
#include <automerge-c/automerge.h>
#include <automerge-c/utils/string.h>
#include <stdio.h>
int main(int argc, char** argv) {
// ...previous example...
AMdoc *doc;
AMitemToDoc(AMresultItem(docResult), &doc);
AMdoc *doc = AMresultValue(docResult).doc;
AMresult *putResult = AMmapPutStr(doc, AM_ROOT, AMstr("key"), AMstr("value"));
if (AMresultStatus(putResult) != AM_STATUS_OK) {
char* const err_msg = AMstrdup(AMresultError(putResult), NULL);
printf("failed to put: %s", err_msg);
free(err_msg);
printf("failed to put: %s", AMerrorMessage(putResult).src);
goto cleanup;
}
AMresult *getResult = AMmapGet(doc, AM_ROOT, AMstr("key"), NULL);
if (AMresultStatus(getResult) != AM_STATUS_OK) {
char* const err_msg = AMstrdup(AMresultError(putResult), NULL);
printf("failed to get: %s", err_msg);
free(err_msg);
printf("failed to get: %s", AMerrorMessage(getResult).src);
goto cleanup;
}
AMbyteSpan got;
if (AMitemToStr(AMresultItem(getResult), &got)) {
char* const c_str = AMstrdup(got, NULL);
printf("Got %zu-character string \"%s\"", got.count, c_str);
free(c_str);
} else {
AMvalue got = AMresultValue(getResult);
if (got.tag != AM_VALUE_STR) {
printf("expected to read a string!");
goto cleanup;
}
printf("Got %zu-character string `%s`", got.str.count, got.str.src);
cleanup:
AMresultFree(getResult);
AMresultFree(putResult);
AMresultFree(docResult);
AMfree(getResult);
AMfree(putResult);
AMfree(docResult);
}
```
Functions that do not return an `AMresult` (for example `AMitemKey()`) do
not allocate memory but rather reference memory that was previously
allocated. It's therefore important to keep the original `AMresult` alive (in
this case the one returned by `AMmapRange()`) until after you are finished with
the items that it contains. However, the memory for an individual `AMitem` can
be shared with a new `AMresult` by calling `AMitemResult()` on it. In other
words, a select group of items can be filtered out of a collection and only each
one's corresponding `AMresult` must be kept alive from that point forward; the
originating collection's `AMresult` can be safely freed.
Functions that do not return an `AMresult` (for example `AMmapItemValue()`) do
not allocate memory, but continue to reference memory that was previously
allocated. It's thus important to keep the original `AMresult` alive (in this
case the one returned by `AMmapRange()`) until after you are done with the return
values of these functions.
Beyond that, good luck!

View file

@ -1,7 +1,7 @@
after_includes = """\n
/**
* \\defgroup enumerations Public Enumerations
* Symbolic names for integer constants.
Symbolic names for integer constants.
*/
/**
@ -12,23 +12,21 @@ after_includes = """\n
#define AM_ROOT NULL
/**
* \\memberof AMdoc
* \\memberof AMchangeHash
* \\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

View file

@ -1,22 +0,0 @@
[package]
name = "@PROJECT_NAME@"
version = "@PROJECT_VERSION@"
authors = ["Orion Henry <orion.henry@gmail.com>", "Jason Kankiewicz <jason.kankiewicz@gmail.com>"]
edition = "2021"
license = "MIT"
rust-version = "1.57.0"
[lib]
name = "@BINDINGS_NAME@"
crate-type = ["staticlib"]
bench = false
doc = false
[dependencies]
@LIBRARY_NAME@ = { path = "../@LIBRARY_NAME@" }
hex = "^0.4.3"
libc = "^0.2"
smol_str = "^0.1.21"
[build-dependencies]
cbindgen = "^0.24"

View file

@ -1,48 +0,0 @@
after_includes = """\n
/**
* \\defgroup enumerations Public Enumerations
* Symbolic names for integer constants.
*/
/**
* \\memberof AMdoc
* \\def AM_ROOT
* \\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.
*/
"""
documentation = true
documentation_style = "doxy"
include_guard = "@INCLUDE_GUARD_PREFIX@_H"
includes = []
language = "C"
line_length = 140
no_includes = true
style = "both"
sys_includes = ["stdbool.h", "stddef.h", "stdint.h", "time.h"]
usize_is_size_t = true
[enum]
derive_const_casts = true
enum_class = true
must_use = "MUST_USE_ENUM"
prefix_with_name = true
rename_variants = "ScreamingSnakeCase"
[export]
item_types = ["constants", "enums", "functions", "opaque", "structs", "typedefs"]

View file

@ -1,35 +1,14 @@
#ifndef @INCLUDE_GUARD_PREFIX@_CONFIG_H
#define @INCLUDE_GUARD_PREFIX@_CONFIG_H
/**
* \file
* \brief Configuration pararameters defined by the build system.
*
* \warning This file is auto-generated by CMake.
*/
#ifndef @SYMBOL_PREFIX@_CONFIG_H
#define @SYMBOL_PREFIX@_CONFIG_H
/* This header is auto-generated by CMake. */
/**
* \def @SYMBOL_PREFIX@_VERSION
* \brief Denotes a semantic version of the form {MAJOR}{MINOR}{PATCH} as three,
* two-digit decimal numbers without leading zeros (e.g. 100 is 0.1.0).
*/
#define @SYMBOL_PREFIX@_VERSION @INTEGER_PROJECT_VERSION@
/**
* \def @SYMBOL_PREFIX@_MAJOR_VERSION
* \brief Denotes a semantic major version as a decimal number.
*/
#define @SYMBOL_PREFIX@_MAJOR_VERSION (@SYMBOL_PREFIX@_VERSION / 100000)
/**
* \def @SYMBOL_PREFIX@_MINOR_VERSION
* \brief Denotes a semantic minor version as a decimal number.
*/
#define @SYMBOL_PREFIX@_MINOR_VERSION ((@SYMBOL_PREFIX@_VERSION / 100) % 1000)
/**
* \def @SYMBOL_PREFIX@_PATCH_VERSION
* \brief Denotes a semantic patch version as a decimal number.
*/
#define @SYMBOL_PREFIX@_PATCH_VERSION (@SYMBOL_PREFIX@_VERSION % 100)
#endif /* @INCLUDE_GUARD_PREFIX@_CONFIG_H */
#endif /* @SYMBOL_PREFIX@_CONFIG_H */

View file

@ -1,183 +0,0 @@
# This CMake script is used to generate a header and a source file for utility
# functions that convert the tags of generated enum types into strings and
# strings into the tags of generated enum types.
cmake_minimum_required(VERSION 3.23 FATAL_ERROR)
# Seeks the starting line of the source enum's declaration.
macro(seek_enum_mode)
if (line MATCHES "^(typedef[ \t]+)?enum ")
string(REGEX REPLACE "^enum ([0-9a-zA-Z_]+).*$" "\\1" enum_name "${line}")
set(mode "read_tags")
endif()
endmacro()
# Scans the input for the current enum's tags.
macro(read_tags_mode)
if(line MATCHES "^}")
set(mode "generate")
elseif(line MATCHES "^[A-Z0-9_]+.*$")
string(REGEX REPLACE "^([A-Za-z0-9_]+).*$" "\\1" tmp "${line}")
list(APPEND enum_tags "${tmp}")
endif()
endmacro()
macro(write_header_file)
# Generate a to-string function declaration.
list(APPEND header_body
"/**\n"
" * \\ingroup enumerations\n"
" * \\brief Gets the string representation of an `${enum_name}` enum tag.\n"
" *\n"
" * \\param[in] tag An `${enum_name}` enum tag.\n"
" * \\return A null-terminated byte string.\n"
" */\n"
"char const* ${enum_name}ToString(${enum_name} const tag)\;\n"
"\n")
# Generate a from-string function declaration.
list(APPEND header_body
"/**\n"
" * \\ingroup enumerations\n"
" * \\brief Gets an `${enum_name}` enum tag from its string representation.\n"
" *\n"
" * \\param[out] dest An `${enum_name}` enum tag pointer.\n"
" * \\param[in] src A null-terminated byte string.\n"
" * \\return `true` if \\p src matches the string representation of an\n"
" * `${enum_name}` enum tag, `false` otherwise.\n"
" */\n"
"bool ${enum_name}FromString(${enum_name}* dest, char const* const src)\;\n"
"\n")
endmacro()
macro(write_source_file)
# Generate a to-string function implementation.
list(APPEND source_body
"char const* ${enum_name}ToString(${enum_name} const tag) {\n"
" switch (tag) {\n"
" default:\n"
" return \"???\"\;\n")
foreach(label IN LISTS enum_tags)
list(APPEND source_body
" case ${label}:\n"
" return \"${label}\"\;\n")
endforeach()
list(APPEND source_body
" }\n"
"}\n"
"\n")
# Generate a from-string function implementation.
list(APPEND source_body
"bool ${enum_name}FromString(${enum_name}* dest, char const* const src) {\n")
foreach(label IN LISTS enum_tags)
list(APPEND source_body
" if (!strcmp(src, \"${label}\")) {\n"
" *dest = ${label}\;\n"
" return true\;\n"
" }\n")
endforeach()
list(APPEND source_body
" return false\;\n"
"}\n"
"\n")
endmacro()
function(main)
set(header_body "")
# File header and includes.
list(APPEND header_body
"#ifndef ${include_guard}\n"
"#define ${include_guard}\n"
"/**\n"
" * \\file\n"
" * \\brief Utility functions for converting enum tags into null-terminated\n"
" * byte strings and vice versa.\n"
" *\n"
" * \\warning This file is auto-generated by CMake.\n"
" */\n"
"\n"
"#include <stdbool.h>\n"
"\n"
"#include <${library_include}>\n"
"\n")
set(source_body "")
# File includes.
list(APPEND source_body
"/** \\warning This file is auto-generated by CMake. */\n"
"\n"
"#include \"stdio.h\"\n"
"#include \"string.h\"\n"
"\n"
"#include <${header_include}>\n"
"\n")
set(enum_name "")
set(enum_tags "")
set(mode "seek_enum")
file(STRINGS "${input_path}" lines)
foreach(line IN LISTS lines)
string(REGEX REPLACE "^(.+)(//.*)?" "\\1" line "${line}")
string(STRIP "${line}" line)
if(mode STREQUAL "seek_enum")
seek_enum_mode()
elseif(mode STREQUAL "read_tags")
read_tags_mode()
else()
# The end of the enum declaration was reached.
if(NOT enum_name)
# The end of the file was reached.
return()
endif()
if(NOT enum_tags)
message(FATAL_ERROR "No tags found for `${enum_name}`.")
endif()
string(TOLOWER "${enum_name}" output_stem_prefix)
string(CONCAT output_stem "${output_stem_prefix}" "_string")
cmake_path(REPLACE_EXTENSION output_stem "h" OUTPUT_VARIABLE output_header_basename)
write_header_file()
write_source_file()
set(enum_name "")
set(enum_tags "")
set(mode "seek_enum")
endif()
endforeach()
# File footer.
list(APPEND header_body
"#endif /* ${include_guard} */\n")
message(STATUS "Generating header file \"${output_header_path}\"...")
file(WRITE "${output_header_path}" ${header_body})
message(STATUS "Generating source file \"${output_source_path}\"...")
file(WRITE "${output_source_path}" ${source_body})
endfunction()
if(NOT DEFINED PROJECT_NAME)
message(FATAL_ERROR "Variable PROJECT_NAME is not defined.")
elseif(NOT DEFINED LIBRARY_NAME)
message(FATAL_ERROR "Variable LIBRARY_NAME is not defined.")
elseif(NOT DEFINED SUBDIR)
message(FATAL_ERROR "Variable SUBDIR is not defined.")
elseif(${CMAKE_ARGC} LESS 9)
message(FATAL_ERROR "Too few arguments.")
elseif(${CMAKE_ARGC} GREATER 10)
message(FATAL_ERROR "Too many arguments.")
elseif(NOT EXISTS ${CMAKE_ARGV5})
message(FATAL_ERROR "Input header \"${CMAKE_ARGV7}\" not found.")
endif()
cmake_path(CONVERT "${CMAKE_ARGV7}" TO_CMAKE_PATH_LIST input_path NORMALIZE)
cmake_path(CONVERT "${CMAKE_ARGV8}" TO_CMAKE_PATH_LIST output_header_path NORMALIZE)
cmake_path(CONVERT "${CMAKE_ARGV9}" TO_CMAKE_PATH_LIST output_source_path NORMALIZE)
string(TOLOWER "${PROJECT_NAME}" project_root)
cmake_path(CONVERT "${SUBDIR}" TO_CMAKE_PATH_LIST project_subdir NORMALIZE)
string(TOLOWER "${project_subdir}" project_subdir)
string(TOLOWER "${LIBRARY_NAME}" library_stem)
cmake_path(REPLACE_EXTENSION library_stem "h" OUTPUT_VARIABLE library_basename)
string(JOIN "/" library_include "${project_root}" "${library_basename}")
string(TOUPPER "${PROJECT_NAME}" project_name_upper)
string(TOUPPER "${project_subdir}" include_guard_infix)
string(REGEX REPLACE "/" "_" include_guard_infix "${include_guard_infix}")
string(REGEX REPLACE "-" "_" include_guard_prefix "${project_name_upper}")
string(JOIN "_" include_guard_prefix "${include_guard_prefix}" "${include_guard_infix}")
string(JOIN "/" output_header_prefix "${project_root}" "${project_subdir}")
cmake_path(GET output_header_path STEM output_header_stem)
string(TOUPPER "${output_header_stem}" include_guard_stem)
string(JOIN "_" include_guard "${include_guard_prefix}" "${include_guard_stem}" "H")
cmake_path(GET output_header_path FILENAME output_header_basename)
string(JOIN "/" header_include "${output_header_prefix}" "${output_header_basename}")
main()

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,35 +0,0 @@
find_package(Doxygen OPTIONAL_COMPONENTS dot)
if(DOXYGEN_FOUND)
set(DOXYGEN_ALIASES "installed_headerfile=\\headerfile ${LIBRARY_NAME}.h <${PROJECT_NAME}/${LIBRARY_NAME}.h>")
set(DOXYGEN_GENERATE_LATEX YES)
set(DOXYGEN_PDF_HYPERLINKS YES)
set(DOXYGEN_PROJECT_LOGO "${CMAKE_CURRENT_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
"${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h"
"${CBINDGEN_TARGET_DIR}/config.h"
"${CBINDGEN_TARGET_DIR}/${UTILS_SUBDIR}/enum_string.h"
"${CMAKE_SOURCE_DIR}/${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}/${UTILS_SUBDIR}/result.h"
"${CMAKE_SOURCE_DIR}/${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}/${UTILS_SUBDIR}/stack_callback_data.h"
"${CMAKE_SOURCE_DIR}/${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}/${UTILS_SUBDIR}/stack.h"
"${CMAKE_SOURCE_DIR}/${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}/${UTILS_SUBDIR}/string.h"
"${CMAKE_SOURCE_DIR}/README.md"
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 either outputs the
# file or depends upon it also or it will just output an error message
# when it can't be found.
add_dependencies(${LIBRARY_NAME}_docs ${BINDINGS_NAME}_artifacts ${LIBRARY_NAME}_utilities)
endif()

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
example_quickstart
PRIVATE "$<BUILD_INTERFACE:${CBINDGEN_INCLUDEDIR}>"
)
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

@ -3,127 +3,152 @@
#include <string.h>
#include <automerge-c/automerge.h>
#include <automerge-c/utils/enum_string.h>
#include <automerge-c/utils/stack.h>
#include <automerge-c/utils/stack_callback_data.h>
#include <automerge-c/utils/string.h>
static bool abort_cb(AMstack**, void*);
static void abort_cb(AMresultStack**, uint8_t);
/**
* \brief Based on https://automerge.github.io/docs/quickstart
*/
int main(int argc, char** argv) {
AMstack* stack = NULL;
AMdoc* doc1;
AMitemToDoc(AMstackItem(&stack, AMcreate(NULL), abort_cb, AMexpect(AM_VAL_TYPE_DOC)), &doc1);
AMobjId const* const cards =
AMitemObjId(AMstackItem(&stack, AMmapPutObject(doc1, AM_ROOT, AMstr("cards"), AM_OBJ_TYPE_LIST), abort_cb,
AMexpect(AM_VAL_TYPE_OBJ_TYPE)));
AMobjId const* const card1 =
AMitemObjId(AMstackItem(&stack, AMlistPutObject(doc1, cards, SIZE_MAX, true, AM_OBJ_TYPE_MAP), abort_cb,
AMexpect(AM_VAL_TYPE_OBJ_TYPE)));
AMstackItem(NULL, AMmapPutStr(doc1, card1, AMstr("title"), AMstr("Rewrite everything in Clojure")), abort_cb,
AMexpect(AM_VAL_TYPE_VOID));
AMstackItem(NULL, AMmapPutBool(doc1, card1, AMstr("done"), false), abort_cb, AMexpect(AM_VAL_TYPE_VOID));
AMobjId const* const card2 =
AMitemObjId(AMstackItem(&stack, AMlistPutObject(doc1, cards, SIZE_MAX, true, AM_OBJ_TYPE_MAP), abort_cb,
AMexpect(AM_VAL_TYPE_OBJ_TYPE)));
AMstackItem(NULL, AMmapPutStr(doc1, card2, AMstr("title"), AMstr("Rewrite everything in Haskell")), abort_cb,
AMexpect(AM_VAL_TYPE_VOID));
AMstackItem(NULL, AMmapPutBool(doc1, card2, AMstr("done"), false), abort_cb, AMexpect(AM_VAL_TYPE_VOID));
AMstackItem(NULL, AMcommit(doc1, AMstr("Add card"), NULL), abort_cb, AMexpect(AM_VAL_TYPE_CHANGE_HASH));
AMresultStack* stack = NULL;
AMdoc* const doc1 = AMpush(&stack, AMcreate(NULL), AM_VALUE_DOC, abort_cb).doc;
AMobjId const* const cards = AMpush(&stack,
AMmapPutObject(doc1, AM_ROOT, AMstr("cards"), AM_OBJ_TYPE_LIST),
AM_VALUE_OBJ_ID,
abort_cb).obj_id;
AMobjId const* const card1 = AMpush(&stack,
AMlistPutObject(doc1, cards, SIZE_MAX, true, AM_OBJ_TYPE_MAP),
AM_VALUE_OBJ_ID,
abort_cb).obj_id;
AMfree(AMmapPutStr(doc1, card1, AMstr("title"), AMstr("Rewrite everything in Clojure")));
AMfree(AMmapPutBool(doc1, card1, AMstr("done"), false));
AMobjId const* const card2 = AMpush(&stack,
AMlistPutObject(doc1, cards, SIZE_MAX, true, AM_OBJ_TYPE_MAP),
AM_VALUE_OBJ_ID,
abort_cb).obj_id;
AMfree(AMmapPutStr(doc1, card2, AMstr("title"), AMstr("Rewrite everything in Haskell")));
AMfree(AMmapPutBool(doc1, card2, AMstr("done"), false));
AMfree(AMcommit(doc1, AMstr("Add card"), NULL));
AMdoc* doc2;
AMitemToDoc(AMstackItem(&stack, AMcreate(NULL), abort_cb, AMexpect(AM_VAL_TYPE_DOC)), &doc2);
AMstackItem(NULL, AMmerge(doc2, doc1), abort_cb, AMexpect(AM_VAL_TYPE_CHANGE_HASH));
AMdoc* doc2 = AMpush(&stack, AMcreate(NULL), AM_VALUE_DOC, abort_cb).doc;
AMfree(AMmerge(doc2, doc1));
AMbyteSpan binary;
AMitemToBytes(AMstackItem(&stack, AMsave(doc1), abort_cb, AMexpect(AM_VAL_TYPE_BYTES)), &binary);
AMitemToDoc(AMstackItem(&stack, AMload(binary.src, binary.count), abort_cb, AMexpect(AM_VAL_TYPE_DOC)), &doc2);
AMbyteSpan const binary = AMpush(&stack, AMsave(doc1), AM_VALUE_BYTES, abort_cb).bytes;
doc2 = AMpush(&stack, AMload(binary.src, binary.count), AM_VALUE_DOC, abort_cb).doc;
AMstackItem(NULL, AMmapPutBool(doc1, card1, AMstr("done"), true), abort_cb, AMexpect(AM_VAL_TYPE_VOID));
AMstackItem(NULL, AMcommit(doc1, AMstr("Mark card as done"), NULL), abort_cb, AMexpect(AM_VAL_TYPE_CHANGE_HASH));
AMfree(AMmapPutBool(doc1, card1, AMstr("done"), true));
AMfree(AMcommit(doc1, AMstr("Mark card as done"), NULL));
AMstackItem(NULL, AMlistDelete(doc2, cards, 0), abort_cb, AMexpect(AM_VAL_TYPE_VOID));
AMstackItem(NULL, AMcommit(doc2, AMstr("Delete card"), NULL), abort_cb, AMexpect(AM_VAL_TYPE_CHANGE_HASH));
AMfree(AMlistDelete(doc2, cards, 0));
AMfree(AMcommit(doc2, AMstr("Delete card"), NULL));
AMstackItem(NULL, AMmerge(doc1, doc2), abort_cb, AMexpect(AM_VAL_TYPE_CHANGE_HASH));
AMfree(AMmerge(doc1, doc2));
AMitems changes = AMstackItems(&stack, AMgetChanges(doc1, NULL), abort_cb, AMexpect(AM_VAL_TYPE_CHANGE));
AMitem* item = NULL;
while ((item = AMitemsNext(&changes, 1)) != NULL) {
AMchange const* change;
AMitemToChange(item, &change);
AMitems const heads = AMstackItems(&stack, AMitemFromChangeHash(AMchangeHash(change)), abort_cb,
AMexpect(AM_VAL_TYPE_CHANGE_HASH));
char* const c_msg = AMstrdup(AMchangeMessage(change), NULL);
printf("%s %zu\n", c_msg, AMobjSize(doc1, cards, &heads));
AMchanges changes = AMpush(&stack, AMgetChanges(doc1, NULL), AM_VALUE_CHANGES, abort_cb).changes;
AMchange const* change = NULL;
while ((change = AMchangesNext(&changes, 1)) != NULL) {
AMbyteSpan const change_hash = AMchangeHash(change);
AMchangeHashes const heads = AMpush(&stack,
AMchangeHashesInit(&change_hash, 1),
AM_VALUE_CHANGE_HASHES,
abort_cb).change_hashes;
AMbyteSpan const msg = AMchangeMessage(change);
char* const c_msg = calloc(1, msg.count + 1);
strncpy(c_msg, msg.src, msg.count);
printf("%s %ld\n", c_msg, AMobjSize(doc1, cards, &heads));
free(c_msg);
}
AMstackFree(&stack);
AMfreeStack(&stack);
}
static char const* discriminant_suffix(AMvalueVariant const);
/**
* \brief Examines the result at the top of the given stack and, if it's
* invalid, prints an error message to `stderr`, deallocates all results
* in the stack and exits.
* \brief Prints an error message to `stderr`, deallocates all results in the
* given stack and exits.
*
* \param[in,out] stack A pointer to a pointer to an `AMstack` struct.
* \param[in] data A pointer to an owned `AMstackCallbackData` struct or `NULL`.
* \return `true` if the top `AMresult` in \p stack is valid, `false` otherwise.
* \pre \p stack `!= NULL`.
* \param[in,out] stack A pointer to a pointer to an `AMresultStack` struct.
* \param[in] discriminant An `AMvalueVariant` enum tag.
* \pre \p stack` != NULL`.
* \post `*stack == NULL`.
*/
static bool abort_cb(AMstack** stack, void* data) {
static void abort_cb(AMresultStack** stack, uint8_t discriminant) {
static char buffer[512] = {0};
char const* suffix = NULL;
if (!stack) {
suffix = "Stack*";
} else if (!*stack) {
}
else if (!*stack) {
suffix = "Stack";
} else if (!(*stack)->result) {
}
else if (!(*stack)->result) {
suffix = "";
}
if (suffix) {
fprintf(stderr, "Null `AMresult%s*`.\n", suffix);
AMstackFree(stack);
fprintf(stderr, "Null `AMresult%s*`.", suffix);
AMfreeStack(stack);
exit(EXIT_FAILURE);
return false;
return;
}
AMstatus const status = AMresultStatus((*stack)->result);
switch (status) {
case AM_STATUS_ERROR:
strcpy(buffer, "Error");
break;
case AM_STATUS_INVALID_RESULT:
strcpy(buffer, "Invalid result");
break;
case AM_STATUS_OK:
break;
default:
sprintf(buffer, "Unknown `AMstatus` tag %d", status);
case AM_STATUS_ERROR: strcpy(buffer, "Error"); break;
case AM_STATUS_INVALID_RESULT: strcpy(buffer, "Invalid result"); break;
case AM_STATUS_OK: break;
default: sprintf(buffer, "Unknown `AMstatus` tag %d", status);
}
if (buffer[0]) {
char* const c_msg = AMstrdup(AMresultError((*stack)->result), NULL);
fprintf(stderr, "%s; %s.\n", buffer, c_msg);
AMbyteSpan const msg = AMerrorMessage((*stack)->result);
char* const c_msg = calloc(1, msg.count + 1);
strncpy(c_msg, msg.src, msg.count);
fprintf(stderr, "%s; %s.", buffer, c_msg);
free(c_msg);
AMstackFree(stack);
AMfreeStack(stack);
exit(EXIT_FAILURE);
return false;
return;
}
if (data) {
AMstackCallbackData* sc_data = (AMstackCallbackData*)data;
AMvalType const tag = AMitemValType(AMresultItem((*stack)->result));
if (tag != sc_data->bitmask) {
fprintf(stderr, "Unexpected tag `%s` (%d) instead of `%s` at %s:%d.\n", AMvalTypeToString(tag), tag,
AMvalTypeToString(sc_data->bitmask), sc_data->file, sc_data->line);
free(sc_data);
AMstackFree(stack);
exit(EXIT_FAILURE);
return false;
}
}
free(data);
return true;
AMvalue const value = AMresultValue((*stack)->result);
fprintf(stderr, "Unexpected tag `AM_VALUE_%s` (%d); expected `AM_VALUE_%s`.",
discriminant_suffix(value.tag),
value.tag,
discriminant_suffix(discriminant));
AMfreeStack(stack);
exit(EXIT_FAILURE);
}
/**
* \brief Gets the suffix for a discriminant's corresponding string
* representation.
*
* \param[in] discriminant An `AMvalueVariant` enum tag.
* \return A UTF-8 string.
*/
static char const* discriminant_suffix(AMvalueVariant const discriminant) {
char const* suffix = NULL;
switch (discriminant) {
case AM_VALUE_ACTOR_ID: suffix = "ACTOR_ID"; break;
case AM_VALUE_BOOLEAN: suffix = "BOOLEAN"; break;
case AM_VALUE_BYTES: suffix = "BYTES"; break;
case AM_VALUE_CHANGE_HASHES: suffix = "CHANGE_HASHES"; break;
case AM_VALUE_CHANGES: suffix = "CHANGES"; break;
case AM_VALUE_COUNTER: suffix = "COUNTER"; break;
case AM_VALUE_DOC: suffix = "DOC"; break;
case AM_VALUE_F64: suffix = "F64"; break;
case AM_VALUE_INT: suffix = "INT"; break;
case AM_VALUE_LIST_ITEMS: suffix = "LIST_ITEMS"; break;
case AM_VALUE_MAP_ITEMS: suffix = "MAP_ITEMS"; break;
case AM_VALUE_NULL: suffix = "NULL"; break;
case AM_VALUE_OBJ_ID: suffix = "OBJ_ID"; break;
case AM_VALUE_OBJ_ITEMS: suffix = "OBJ_ITEMS"; break;
case AM_VALUE_STR: suffix = "STR"; break;
case AM_VALUE_STRS: suffix = "STRINGS"; break;
case AM_VALUE_SYNC_MESSAGE: suffix = "SYNC_MESSAGE"; break;
case AM_VALUE_SYNC_STATE: suffix = "SYNC_STATE"; break;
case AM_VALUE_TIMESTAMP: suffix = "TIMESTAMP"; break;
case AM_VALUE_UINT: suffix = "UINT"; break;
case AM_VALUE_VOID: suffix = "VOID"; break;
default: suffix = "...";
}
return suffix;
}

View file

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Before After
Before After

View file

@ -1,30 +0,0 @@
#ifndef AUTOMERGE_C_UTILS_RESULT_H
#define AUTOMERGE_C_UTILS_RESULT_H
/**
* \file
* \brief Utility functions for use with `AMresult` structs.
*/
#include <stdarg.h>
#include <automerge-c/automerge.h>
/**
* \brief Transfers the items within an arbitrary list of results into a
* new result in their order of specification.
* \param[in] count The count of subsequent arguments.
* \param[in] ... A \p count list of arguments, each of which is a pointer to
* an `AMresult` struct whose items will be transferred out of it
* and which is subsequently freed.
* \return A pointer to an `AMresult` struct or `NULL`.
* \pre `𝑥 ` \p ... `, AMresultStatus(𝑥) == AM_STATUS_OK`
* \post `(𝑥 ` \p ... `, AMresultStatus(𝑥) != AM_STATUS_OK) -> NULL`
* \attention All `AMresult` struct pointer arguments are passed to
* `AMresultFree()` regardless of success; use `AMresultCat()`
* instead if you wish to pass them to `AMresultFree()` yourself.
* \warning The returned `AMresult` struct pointer must be passed to
* `AMresultFree()` in order to avoid a memory leak.
*/
AMresult* AMresultFrom(int count, ...);
#endif /* AUTOMERGE_C_UTILS_RESULT_H */

View file

@ -1,130 +0,0 @@
#ifndef AUTOMERGE_C_UTILS_STACK_H
#define AUTOMERGE_C_UTILS_STACK_H
/**
* \file
* \brief Utility data structures and functions for hiding `AMresult` structs,
* managing their lifetimes, and automatically applying custom
* validation logic to the `AMitem` structs that they contain.
*
* \note The `AMstack` struct and its related functions drastically reduce the
* need for boilerplate code and/or `goto` statement usage within a C
* application but a higher-level programming language offers even better
* ways to do the same things.
*/
#include <automerge-c/automerge.h>
/**
* \struct AMstack
* \brief A node in a singly-linked list of result pointers.
*/
typedef struct AMstack {
/** A result to be deallocated. */
AMresult* result;
/** The previous node in the singly-linked list or `NULL`. */
struct AMstack* prev;
} AMstack;
/**
* \memberof AMstack
* \brief The prototype of a function that examines the result at the top of
* the given stack in terms of some arbitrary data.
*
* \param[in,out] stack A pointer to a pointer to an `AMstack` struct.
* \param[in] data A pointer to arbitrary data or `NULL`.
* \return `true` if the top `AMresult` struct in \p stack is valid, `false`
* otherwise.
* \pre \p stack `!= NULL`.
*/
typedef bool (*AMstackCallback)(AMstack** stack, void* data);
/**
* \memberof AMstack
* \brief Deallocates the storage for a stack of results.
*
* \param[in,out] stack A pointer to a pointer to an `AMstack` struct.
* \pre \p stack `!= NULL`
* \post `*stack == NULL`
*/
void AMstackFree(AMstack** stack);
/**
* \memberof AMstack
* \brief Gets a result from the stack after removing it.
*
* \param[in,out] stack A pointer to a pointer to an `AMstack` struct.
* \param[in] result A pointer to the `AMresult` to be popped or `NULL` to
* select the top result in \p stack.
* \return A pointer to an `AMresult` struct or `NULL`.
* \pre \p stack `!= NULL`
* \warning The returned `AMresult` struct pointer must be passed to
* `AMresultFree()` in order to avoid a memory leak.
*/
AMresult* AMstackPop(AMstack** stack, AMresult const* result);
/**
* \memberof AMstack
* \brief Pushes the given result onto the given stack, calls the given
* callback with the given data to validate it and then either gets the
* result if it's valid or gets `NULL` instead.
*
* \param[in,out] stack A pointer to a pointer to an `AMstack` struct.
* \param[in] result A pointer to an `AMresult` struct.
* \param[in] callback A pointer to a function with the same signature as
* `AMstackCallback()` or `NULL`.
* \param[in] data A pointer to arbitrary data or `NULL` which is passed to
* \p callback.
* \return \p result or `NULL`.
* \warning If \p stack `== NULL` then \p result is deallocated in order to
* avoid a memory leak.
*/
AMresult* AMstackResult(AMstack** stack, AMresult* result, AMstackCallback callback, void* data);
/**
* \memberof AMstack
* \brief Pushes the given result onto the given stack, calls the given
* callback with the given data to validate it and then either gets the
* first item in the sequence of items within that result if it's valid
* or gets `NULL` instead.
*
* \param[in,out] stack A pointer to a pointer to an `AMstack` struct.
* \param[in] result A pointer to an `AMresult` struct.
* \param[in] callback A pointer to a function with the same signature as
* `AMstackCallback()` or `NULL`.
* \param[in] data A pointer to arbitrary data or `NULL` which is passed to
* \p callback.
* \return A pointer to an `AMitem` struct or `NULL`.
* \warning If \p stack `== NULL` then \p result is deallocated in order to
* avoid a memory leak.
*/
AMitem* AMstackItem(AMstack** stack, AMresult* result, AMstackCallback callback, void* data);
/**
* \memberof AMstack
* \brief Pushes the given result onto the given stack, calls the given
* callback with the given data to validate it and then either gets an
* `AMitems` struct over the sequence of items within that result if it's
* valid or gets an empty `AMitems` instead.
*
* \param[in,out] stack A pointer to a pointer to an `AMstack` struct.
* \param[in] result A pointer to an `AMresult` struct.
* \param[in] callback A pointer to a function with the same signature as
* `AMstackCallback()` or `NULL`.
* \param[in] data A pointer to arbitrary data or `NULL` which is passed to
* \p callback.
* \return An `AMitems` struct.
* \warning If \p stack `== NULL` then \p result is deallocated immediately
* in order to avoid a memory leak.
*/
AMitems AMstackItems(AMstack** stack, AMresult* result, AMstackCallback callback, void* data);
/**
* \memberof AMstack
* \brief Gets the count of results that have been pushed onto the stack.
*
* \param[in,out] stack A pointer to an `AMstack` struct.
* \return A 64-bit unsigned integer.
*/
size_t AMstackSize(AMstack const* const stack);
#endif /* AUTOMERGE_C_UTILS_STACK_H */

View file

@ -1,53 +0,0 @@
#ifndef AUTOMERGE_C_UTILS_PUSH_CALLBACK_DATA_H
#define AUTOMERGE_C_UTILS_PUSH_CALLBACK_DATA_H
/**
* \file
* \brief Utility data structures, functions and macros for supplying
* parameters to the custom validation logic applied to `AMitem`
* structs.
*/
#include <automerge-c/automerge.h>
/**
* \struct AMstackCallbackData
* \brief A data structure for passing the parameters of an item value test
* to an implementation of the `AMstackCallback` function prototype.
*/
typedef struct {
/** A bitmask of `AMvalType` tags. */
AMvalType bitmask;
/** A null-terminated file path string. */
char const* file;
/** The ordinal number of a line within a file. */
int line;
} AMstackCallbackData;
/**
* \memberof AMstackCallbackData
* \brief Allocates a new `AMstackCallbackData` struct and initializes its
* members from their corresponding arguments.
*
* \param[in] bitmask A bitmask of `AMvalType` tags.
* \param[in] file A null-terminated file path string.
* \param[in] line The ordinal number of a line within a file.
* \return A pointer to a disowned `AMstackCallbackData` struct.
* \warning The returned pointer must be passed to `free()` to avoid a memory
* leak.
*/
AMstackCallbackData* AMstackCallbackDataInit(AMvalType const bitmask, char const* const file, int const line);
/**
* \memberof AMstackCallbackData
* \def AMexpect
* \brief Allocates a new `AMstackCallbackData` struct and initializes it from
* an `AMvalueType` bitmask.
*
* \param[in] bitmask A bitmask of `AMvalType` tags.
* \return A pointer to a disowned `AMstackCallbackData` struct.
* \warning The returned pointer must be passed to `free()` to avoid a memory
* leak.
*/
#define AMexpect(bitmask) AMstackCallbackDataInit(bitmask, __FILE__, __LINE__)
#endif /* AUTOMERGE_C_UTILS_PUSH_CALLBACK_DATA_H */

View file

@ -1,29 +0,0 @@
#ifndef AUTOMERGE_C_UTILS_STRING_H
#define AUTOMERGE_C_UTILS_STRING_H
/**
* \file
* \brief Utility functions for use with `AMbyteSpan` structs that provide
* UTF-8 string views.
*/
#include <automerge-c/automerge.h>
/**
* \memberof AMbyteSpan
* \brief Returns a pointer to a null-terminated byte string which is a
* duplicate of the given UTF-8 string view except for the substitution
* of its NUL (0) characters with the specified null-terminated byte
* string.
*
* \param[in] str A UTF-8 string view as an `AMbyteSpan` struct.
* \param[in] nul A null-terminated byte string to substitute for NUL characters
* or `NULL` to substitute `"\\0"` for NUL characters.
* \return A disowned null-terminated byte string.
* \pre \p str.src `!= NULL`
* \pre \p str.count `<= sizeof(`\p str.src `)`
* \warning The returned pointer must be passed to `free()` to avoid a memory
* leak.
*/
char* AMstrdup(AMbyteSpan const str, char const* nul);
#endif /* AUTOMERGE_C_UTILS_STRING_H */

View file

@ -0,0 +1,250 @@
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_FEATURES "")
set(CARGO_CURRENT_BINARY_DIR "${CARGO_TARGET_DIR}/${CARGO_BUILD_TYPE}")
set(
CARGO_OUTPUT
${CBINDGEN_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 -- ${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h ${CMAKE_SOURCE_DIR}/cbindgen.toml
COMMAND
${CMAKE_COMMAND} -E env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} CBINDGEN_TARGET_DIR=${CBINDGEN_TARGET_DIR} ${CARGO_CMD} build ${CARGO_FLAG} ${CARGO_FEATURES}
MAIN_DEPENDENCY
lib.rs
DEPENDS
actor_id.rs
byte_span.rs
change_hashes.rs
change.rs
changes.rs
doc.rs
doc/list.rs
doc/list/item.rs
doc/list/items.rs
doc/map.rs
doc/map/item.rs
doc/map/items.rs
doc/utils.rs
obj.rs
obj/item.rs
obj/items.rs
result.rs
result_stack.rs
strs.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 ALL
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 -- ${CBINDGEN_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 -- ${CBINDGEN_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 -- ${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h
COMMAND
# Compensate for cbindgen ignoring `std:mem::size_of<usize>()` calls.
${CMAKE_COMMAND} -DMATCH_REGEX=USIZE_ -DREPLACE_EXPR=\+${CMAKE_SIZEOF_VOID_P} -P ${CMAKE_SOURCE_DIR}/cmake/file_regex_replace.cmake -- ${CBINDGEN_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 "${CBINDGEN_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_ALIASES "installed_headerfile=\\headerfile ${LIBRARY_NAME}.h <${PROJECT_NAME}/${LIBRARY_NAME}.h>")
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
"${CBINDGEN_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

@ -1,5 +1,4 @@
use automerge as am;
use libc::c_int;
use std::cell::RefCell;
use std::cmp::Ordering;
use std::str::FromStr;
@ -12,7 +11,7 @@ macro_rules! to_actor_id {
let handle = $handle.as_ref();
match handle {
Some(b) => b,
None => return AMresult::error("Invalid `AMactorId*`").into(),
None => return AMresult::err("Invalid AMactorId pointer").into(),
}
}};
}
@ -58,11 +57,11 @@ impl AsRef<am::ActorId> for AMactorId {
}
/// \memberof AMactorId
/// \brief Gets the value of an actor identifier as an array of bytes.
/// \brief Gets the value of an actor identifier as a sequence of bytes.
///
/// \param[in] actor_id A pointer to an `AMactorId` struct.
/// \return An `AMbyteSpan` struct for an array of bytes.
/// \pre \p actor_id `!= NULL`
/// \pre \p actor_id `!= NULL`.
/// \return An `AMbyteSpan` struct.
/// \internal
///
/// # Safety
@ -83,8 +82,8 @@ pub unsafe extern "C" fn AMactorIdBytes(actor_id: *const AMactorId) -> AMbyteSpa
/// \return `-1` if \p actor_id1 `<` \p actor_id2, `0` if
/// \p actor_id1 `==` \p actor_id2 and `1` if
/// \p actor_id1 `>` \p actor_id2.
/// \pre \p actor_id1 `!= NULL`
/// \pre \p actor_id2 `!= NULL`
/// \pre \p actor_id1 `!= NULL`.
/// \pre \p actor_id2 `!= NULL`.
/// \internal
///
/// #Safety
@ -94,7 +93,7 @@ pub unsafe extern "C" fn AMactorIdBytes(actor_id: *const AMactorId) -> AMbyteSpa
pub unsafe extern "C" fn AMactorIdCmp(
actor_id1: *const AMactorId,
actor_id2: *const AMactorId,
) -> c_int {
) -> isize {
match (actor_id1.as_ref(), actor_id2.as_ref()) {
(Some(actor_id1), Some(actor_id2)) => match actor_id1.as_ref().cmp(actor_id2.as_ref()) {
Ordering::Less => -1,
@ -102,69 +101,65 @@ pub unsafe extern "C" fn AMactorIdCmp(
Ordering::Greater => 1,
},
(None, Some(_)) => -1,
(None, None) => 0,
(Some(_), None) => 1,
(None, None) => 0,
}
}
/// \memberof AMactorId
/// \brief Allocates a new actor identifier and initializes it from a random
/// UUID value.
/// \brief Allocates a new actor identifier and initializes it with a random
/// UUID.
///
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_ACTOR_ID` 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
/// `AMactorId` struct.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
#[no_mangle]
pub unsafe extern "C" fn AMactorIdInit() -> *mut AMresult {
to_result(Ok::<am::ActorId, am::AutomergeError>(am::ActorId::random()))
}
/// \memberof AMactorId
/// \brief Allocates a new actor identifier and initializes it from an array of
/// bytes value.
/// \brief Allocates a new actor identifier and initializes it from a sequence
/// of bytes.
///
/// \param[in] src A pointer to an array of bytes.
/// \param[in] count The count of bytes to copy from the array pointed to by
/// \p src.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_ACTOR_ID` 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] src A pointer to a contiguous sequence of bytes.
/// \param[in] count The number of bytes to copy from \p src.
/// \pre `0 <` \p count `<= sizeof(`\p src`)`.
/// \return A pointer to an `AMresult` struct containing a pointer to an
/// `AMactorId` struct.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
/// src must be a byte array of length `>= count`
/// src must be a byte array of size `>= count`
#[no_mangle]
pub unsafe extern "C" fn AMactorIdFromBytes(src: *const u8, count: usize) -> *mut AMresult {
if !src.is_null() {
let value = std::slice::from_raw_parts(src, count);
to_result(Ok::<am::ActorId, am::InvalidActorId>(am::ActorId::from(
value,
)))
} else {
AMresult::error("Invalid uint8_t*").into()
}
pub unsafe extern "C" fn AMactorIdInitBytes(src: *const u8, count: usize) -> *mut AMresult {
let slice = std::slice::from_raw_parts(src, count);
to_result(Ok::<am::ActorId, am::InvalidActorId>(am::ActorId::from(
slice,
)))
}
/// \memberof AMactorId
/// \brief Allocates a new actor identifier and initializes it from a
/// hexadecimal UTF-8 string view value.
/// hexadecimal string.
///
/// \param[in] value A UTF-8 string view as an `AMbyteSpan` struct.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_ACTOR_ID` item.
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \param[in] hex_str A UTF-8 string view as an `AMbyteSpan` struct.
/// \return A pointer to an `AMresult` struct containing a pointer to an
/// `AMactorId` struct.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
/// hex_str must be a valid pointer to an AMbyteSpan
#[no_mangle]
pub unsafe extern "C" fn AMactorIdFromStr(value: AMbyteSpan) -> *mut AMresult {
pub unsafe extern "C" fn AMactorIdInitStr(hex_str: AMbyteSpan) -> *mut AMresult {
use am::AutomergeError::InvalidActorId;
to_result(match (&value).try_into() {
to_result(match (&hex_str).try_into() {
Ok(s) => match am::ActorId::from_str(s) {
Ok(actor_id) => Ok(actor_id),
Err(_) => Err(InvalidActorId(String::from(s))),
@ -174,12 +169,11 @@ pub unsafe extern "C" fn AMactorIdFromStr(value: AMbyteSpan) -> *mut AMresult {
}
/// \memberof AMactorId
/// \brief Gets the value of an actor identifier as a UTF-8 hexadecimal string
/// view.
/// \brief Gets the value of an actor identifier as a hexadecimal string.
///
/// \param[in] actor_id A pointer to an `AMactorId` struct.
/// \pre \p actor_id `!= NULL`.
/// \return A UTF-8 string view as an `AMbyteSpan` struct.
/// \pre \p actor_id `!= NULL`
/// \internal
///
/// # Safety

View file

@ -1,17 +1,14 @@
use automerge as am;
use std::cmp::Ordering;
use libc::strlen;
use std::convert::TryFrom;
use std::os::raw::c_char;
use libc::{c_int, strlen};
use smol_str::SmolStr;
macro_rules! to_str {
($byte_span:expr) => {{
let result: Result<&str, am::AutomergeError> = (&$byte_span).try_into();
($span:expr) => {{
let result: Result<&str, am::AutomergeError> = (&$span).try_into();
match result {
Ok(s) => s,
Err(e) => return AMresult::error(&e.to_string()).into(),
Err(e) => return AMresult::err(&e.to_string()).into(),
}
}};
}
@ -20,17 +17,16 @@ pub(crate) use to_str;
/// \struct AMbyteSpan
/// \installed_headerfile
/// \brief A view onto an array of bytes.
/// \brief A view onto a contiguous sequence of bytes.
#[repr(C)]
pub struct AMbyteSpan {
/// A pointer to the first byte of an array of bytes.
/// \warning \p src is only valid until the array of bytes to which it
/// points is freed.
/// \note If the `AMbyteSpan` came from within an `AMitem` struct then
/// \p src will be freed when the pointer to the `AMresult` struct
/// containing the `AMitem` struct is passed to `AMresultFree()`.
/// A pointer to an array of bytes.
/// \attention <b>NEVER CALL `free()` ON \p src!</b>
/// \warning \p src is only valid until the `AMfree()` function is called
/// on the `AMresult` struct that stores the array of bytes to
/// which it points.
pub src: *const u8,
/// The count of bytes in the array.
/// The number of bytes in the array.
pub count: usize,
}
@ -56,7 +52,9 @@ impl PartialEq for AMbyteSpan {
} else if self.src == other.src {
return true;
}
<&[u8]>::from(self) == <&[u8]>::from(other)
let slice = unsafe { std::slice::from_raw_parts(self.src, self.count) };
let other_slice = unsafe { std::slice::from_raw_parts(other.src, other.count) };
slice == other_slice
}
}
@ -74,15 +72,10 @@ impl From<&am::ActorId> for AMbyteSpan {
impl From<&mut am::ActorId> for AMbyteSpan {
fn from(actor: &mut am::ActorId) -> Self {
actor.as_ref().into()
}
}
impl From<&am::ChangeHash> for AMbyteSpan {
fn from(change_hash: &am::ChangeHash) -> Self {
let slice = actor.to_bytes();
Self {
src: change_hash.0.as_ptr(),
count: change_hash.0.len(),
src: slice.as_ptr(),
count: slice.len(),
}
}
}
@ -100,9 +93,12 @@ impl From<*const c_char> for AMbyteSpan {
}
}
impl From<&SmolStr> for AMbyteSpan {
fn from(smol_str: &SmolStr) -> Self {
smol_str.as_bytes().into()
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(),
}
}
}
@ -115,39 +111,13 @@ impl From<&[u8]> for AMbyteSpan {
}
}
impl From<&AMbyteSpan> for &[u8] {
fn from(byte_span: &AMbyteSpan) -> Self {
unsafe { std::slice::from_raw_parts(byte_span.src, byte_span.count) }
}
}
impl From<&AMbyteSpan> for Vec<u8> {
fn from(byte_span: &AMbyteSpan) -> Self {
<&[u8]>::from(byte_span).to_vec()
}
}
impl TryFrom<&AMbyteSpan> for am::ChangeHash {
type Error = am::AutomergeError;
fn try_from(byte_span: &AMbyteSpan) -> Result<Self, Self::Error> {
use am::AutomergeError::InvalidChangeHashBytes;
let slice: &[u8] = byte_span.into();
match slice.try_into() {
Ok(change_hash) => Ok(change_hash),
Err(e) => Err(InvalidChangeHashBytes(e)),
}
}
}
impl TryFrom<&AMbyteSpan> for &str {
type Error = am::AutomergeError;
fn try_from(byte_span: &AMbyteSpan) -> Result<Self, Self::Error> {
fn try_from(span: &AMbyteSpan) -> Result<Self, Self::Error> {
use am::AutomergeError::InvalidCharacter;
let slice = byte_span.into();
let slice = unsafe { std::slice::from_raw_parts(span.src, span.count) };
match std::str::from_utf8(slice) {
Ok(str_) => Ok(str_),
Err(e) => Err(InvalidCharacter(e.valid_up_to())),
@ -155,69 +125,17 @@ impl TryFrom<&AMbyteSpan> for &str {
}
}
/// \memberof AMbyteSpan
/// \brief Creates a view onto an array of bytes.
/// \brief Creates an AMbyteSpan from a pointer + length
///
/// \param[in] src A pointer to an array of bytes or `NULL`.
/// \param[in] count The count of bytes to view from the array pointed to by
/// \p src.
/// \return An `AMbyteSpan` struct.
/// \pre \p count `<= sizeof(`\p src `)`
/// \post `(`\p src `== NULL) -> (AMbyteSpan){NULL, 0}`
/// \param[in] src A pointer to a span of bytes
/// \param[in] count The number of bytes in the span
/// \return An `AMbyteSpan` struct
/// \internal
///
/// #Safety
/// src must be a byte array of length `>= count` or `std::ptr::null()`
/// AMbytes does not retain the underlying storage, so you must discard the
/// return value before freeing the bytes.
#[no_mangle]
pub unsafe extern "C" fn AMbytes(src: *const u8, count: usize) -> AMbyteSpan {
AMbyteSpan {
src,
count: if src.is_null() { 0 } else { count },
}
}
/// \memberof AMbyteSpan
/// \brief Creates a view onto a C string.
///
/// \param[in] c_str A null-terminated byte string or `NULL`.
/// \return An `AMbyteSpan` struct.
/// \pre Each byte in \p c_str encodes one UTF-8 character.
/// \internal
///
/// #Safety
/// c_str must be a null-terminated array of `std::os::raw::c_char` or `std::ptr::null()`.
#[no_mangle]
pub unsafe extern "C" fn AMstr(c_str: *const c_char) -> AMbyteSpan {
c_str.into()
}
/// \memberof AMbyteSpan
/// \brief Compares two UTF-8 string views lexicographically.
///
/// \param[in] lhs A UTF-8 string view as an `AMbyteSpan` struct.
/// \param[in] rhs A UTF-8 string view as an `AMbyteSpan` struct.
/// \return Negative value if \p lhs appears before \p rhs in lexicographical order.
/// Zero if \p lhs and \p rhs compare equal.
/// Positive value if \p lhs appears after \p rhs in lexicographical order.
/// \pre \p lhs.src `!= NULL`
/// \pre \p lhs.count `<= sizeof(`\p lhs.src `)`
/// \pre \p rhs.src `!= NULL`
/// \pre \p rhs.count `<= sizeof(`\p rhs.src `)`
/// \internal
///
/// #Safety
/// lhs.src must be a byte array of length >= lhs.count
/// rhs.src must be a a byte array of length >= rhs.count
#[no_mangle]
pub unsafe extern "C" fn AMstrCmp(lhs: AMbyteSpan, rhs: AMbyteSpan) -> c_int {
match (<&str>::try_from(&lhs), <&str>::try_from(&rhs)) {
(Ok(lhs), Ok(rhs)) => match lhs.cmp(rhs) {
Ordering::Less => -1,
Ordering::Equal => 0,
Ordering::Greater => 1,
},
(Err(_), Ok(_)) => -1,
(Err(_), Err(_)) => 0,
(Ok(_), Err(_)) => 1,
}
AMbyteSpan { src, count }
}

View file

@ -2,6 +2,7 @@ use automerge as am;
use std::cell::RefCell;
use crate::byte_span::AMbyteSpan;
use crate::change_hashes::AMchangeHashes;
use crate::result::{to_result, AMresult};
macro_rules! to_change {
@ -9,7 +10,7 @@ macro_rules! to_change {
let handle = $handle.as_ref();
match handle {
Some(b) => b,
None => return AMresult::error("Invalid `AMchange*`").into(),
None => return AMresult::err("Invalid AMchange pointer").into(),
}
}};
}
@ -20,14 +21,14 @@ macro_rules! to_change {
#[derive(Eq, PartialEq)]
pub struct AMchange {
body: *mut am::Change,
change_hash: RefCell<Option<am::ChangeHash>>,
changehash: RefCell<Option<am::ChangeHash>>,
}
impl AMchange {
pub fn new(change: &mut am::Change) -> Self {
Self {
body: change,
change_hash: Default::default(),
changehash: Default::default(),
}
}
@ -39,12 +40,12 @@ impl AMchange {
}
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()
let mut changehash = self.changehash.borrow_mut();
if let Some(changehash) = changehash.as_ref() {
changehash.into()
} else {
let hash = unsafe { (*self.body).hash() };
let ptr = change_hash.insert(hash);
let ptr = changehash.insert(hash);
AMbyteSpan {
src: ptr.0.as_ptr(),
count: hash.as_ref().len(),
@ -69,10 +70,11 @@ impl AsRef<am::Change> for AMchange {
/// \brief Gets the first referenced actor identifier 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.
/// \pre \p change `!= NULL`.
/// \return A pointer to an `AMresult` struct containing a pointer to an
/// `AMactorId` struct.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
@ -88,8 +90,8 @@ pub unsafe extern "C" fn AMchangeActorId(change: *const AMchange) -> *mut AMresu
/// \memberof AMchange
/// \brief Compresses the raw bytes of a change.
///
/// \param[in] change A pointer to an `AMchange` struct.
/// \pre \p change `!= NULL`
/// \param[in,out] change A pointer to an `AMchange` struct.
/// \pre \p change `!= NULL`.
/// \internal
///
/// # Safety
@ -105,20 +107,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 `!= NULL`.
/// \internal
///
/// # Safety
/// change must be a valid pointer to an 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(),
pub unsafe extern "C" fn AMchangeDeps(change: *const AMchange) -> AMchangeHashes {
match change.as_ref() {
Some(change) => AMchangeHashes::new(change.as_ref().deps()),
None => Default::default(),
})
}
}
/// \memberof AMchange
@ -126,7 +126,7 @@ 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 `!= NULL`.
/// \internal
///
/// # Safety
@ -141,33 +141,32 @@ pub unsafe extern "C" fn AMchangeExtraBytes(change: *const AMchange) -> AMbyteSp
}
/// \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 `!= NULL`.
/// \pre `0 <` \p count `<= sizeof(`\p src`)`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
/// src must be a byte array of length `>= count`
/// src must be a byte array of size `>= 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 `!= NULL`.
/// \internal
///
/// # Safety
@ -184,8 +183,8 @@ 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 `!= NULL`.
/// \internal
///
/// # Safety
@ -199,37 +198,12 @@ 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 `!= NULL`.
/// \internal
///
/// # Safety
@ -247,8 +221,8 @@ 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 view as an `AMbyteSpan` struct.
/// \pre \p change `!= NULL`.
/// \internal
///
/// # Safety
@ -266,7 +240,7 @@ 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 `!= NULL`.
/// \internal
///
/// # Safety
@ -285,7 +259,7 @@ 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 `!= NULL`.
/// \internal
///
/// # Safety
@ -293,9 +267,10 @@ pub unsafe extern "C" fn AMchangeSeq(change: *const AMchange) -> u64 {
#[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
}
0
}
/// \memberof AMchange
@ -303,7 +278,7 @@ pub unsafe extern "C" fn AMchangeSize(change: *const AMchange) -> usize {
///
/// \param[in] change A pointer to an `AMchange` struct.
/// \return A 64-bit unsigned integer.
/// \pre \p change `!= NULL`
/// \pre \p change `!= NULL`.
/// \internal
///
/// # Safety
@ -322,7 +297,7 @@ 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 `!= NULL`.
/// \internal
///
/// # Safety
@ -340,8 +315,8 @@ 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 `!= NULL`.
/// \internal
///
/// # Safety
@ -354,3 +329,28 @@ pub unsafe extern "C" fn AMchangeRawBytes(change: *const AMchange) -> AMbyteSpan
Default::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 `!= NULL`.
/// \pre `0 <` \p count `<= sizeof(`\p src`)`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
/// src must be a byte array of size `>= 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::<Result<Vec<am::Change>, _>>(
am::Automerge::load(&data)
.and_then(|d| d.get_changes(&[]).map(|c| c.into_iter().cloned().collect())),
)
}

View file

@ -0,0 +1,400 @@
use automerge as am;
use std::cmp::Ordering;
use std::ffi::c_void;
use std::mem::size_of;
use crate::byte_span::AMbyteSpan;
use crate::result::{to_result, AMresult};
#[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 {
return;
}
let len = self.len as isize;
self.offset = if self.offset < 0 {
// It's reversed.
let unclipped = self.offset.checked_sub(n).unwrap_or(isize::MIN);
if unclipped >= 0 {
// Clip it to the forward stop.
len
} else {
std::cmp::min(std::cmp::max(-(len + 1), unclipped), -1)
}
} else {
let unclipped = self.offset.checked_add(n).unwrap_or(isize::MAX);
if unclipped < 0 {
// Clip it to the reverse stop.
-(len + 1)
} else {
std::cmp::max(0, std::cmp::min(unclipped, 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,
}
}
pub fn rewound(&self) -> Self {
Self {
len: self.len,
offset: if self.offset < 0 { -1 } else { 0 },
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
/// \installed_headerfile
/// \brief A random-access iterator over a sequence of change hashes.
#[repr(C)]
#[derive(Eq, PartialEq)]
pub struct AMchangeHashes {
/// An implementation detail that is intentionally opaque.
/// \warning Modifying \p detail will cause undefined behavior.
/// \note The actual size of \p detail will vary by platform, this is just
/// the one for the platform this documentation was built on.
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(),
}
}
pub fn rewound(&self) -> Self {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
Self {
detail: detail.rewound().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,out] 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 `!= NULL`.
/// \internal
///
/// #Safety
/// change_hashes must be a valid pointer to an 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 `!= NULL`.
/// \pre \p change_hashes2 `!= NULL`.
/// \internal
///
/// #Safety
/// change_hashes1 must be a valid pointer to an AMchangeHashes
/// change_hashes2 must be a valid pointer to an 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 Allocates an iterator over a sequence of change hashes and
/// initializes it from a sequence of byte spans.
///
/// \param[in] src A pointer to an array of `AMbyteSpan` structs.
/// \param[in] count The number of `AMbyteSpan` structs to copy from \p src.
/// \return A pointer to an `AMresult` struct containing an `AMchangeHashes`
/// struct.
/// \pre \p src `!= NULL`.
/// \pre `0 <` \p count `<= sizeof(`\p src`) / sizeof(AMbyteSpan)`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
/// src must be an AMbyteSpan array of size `>= count`
#[no_mangle]
pub unsafe extern "C" fn AMchangeHashesInit(src: *const AMbyteSpan, count: usize) -> *mut AMresult {
let mut change_hashes = Vec::<am::ChangeHash>::new();
for n in 0..count {
let byte_span = &*src.add(n);
let slice = std::slice::from_raw_parts(byte_span.src, byte_span.count);
match slice.try_into() {
Ok(change_hash) => {
change_hashes.push(change_hash);
}
Err(e) => {
return to_result(Err(e));
}
}
}
to_result(Ok::<Vec<am::ChangeHash>, am::InvalidChangeHashSlice>(
change_hashes,
))
}
/// \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,out] 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 `!= NULL`.
/// \internal
///
/// #Safety
/// change_hashes must be a valid pointer to an 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();
}
}
Default::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,out] 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 `!= NULL`.
/// \internal
///
/// #Safety
/// change_hashes must be a valid pointer to an 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();
}
}
Default::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 `!= NULL`.
/// \internal
///
/// #Safety
/// change_hashes must be a valid pointer to an 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 `!= NULL`.
/// \internal
///
/// #Safety
/// change_hashes must be a valid pointer to an 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 {
Default::default()
}
}
/// \memberof AMchangeHashes
/// \brief Creates an iterator at the starting position over the same sequence
/// of change hashes as the given one.
///
/// \param[in] change_hashes A pointer to an `AMchangeHashes` struct.
/// \return An `AMchangeHashes` struct
/// \pre \p change_hashes `!= NULL`.
/// \internal
///
/// #Safety
/// change_hashes must be a valid pointer to an AMchangeHashes
#[no_mangle]
pub unsafe extern "C" fn AMchangeHashesRewound(
change_hashes: *const AMchangeHashes,
) -> AMchangeHashes {
if let Some(change_hashes) = change_hashes.as_ref() {
change_hashes.rewound()
} else {
Default::default()
}
}

View file

@ -0,0 +1,399 @@
use automerge as am;
use std::collections::BTreeMap;
use std::ffi::c_void;
use std::mem::size_of;
use crate::byte_span::AMbyteSpan;
use crate::change::AMchange;
use crate::result::{to_result, AMresult};
#[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 {
return;
}
let len = self.len as isize;
self.offset = if self.offset < 0 {
// It's reversed.
let unclipped = self.offset.checked_sub(n).unwrap_or(isize::MIN);
if unclipped >= 0 {
// Clip it to the forward stop.
len
} else {
std::cmp::min(std::cmp::max(-(len + 1), unclipped), -1)
}
} else {
let unclipped = self.offset.checked_add(n).unwrap_or(isize::MAX);
if unclipped < 0 {
// Clip it to the reverse stop.
-(len + 1)
} else {
std::cmp::max(0, std::cmp::min(unclipped, 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,
}
}
pub fn rewound(&self) -> Self {
Self {
len: self.len,
offset: if self.offset < 0 { -1 } else { 0 },
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
/// \installed_headerfile
/// \brief A random-access iterator over a sequence of changes.
#[repr(C)]
#[derive(Eq, PartialEq)]
pub struct AMchanges {
/// An implementation detail that is intentionally opaque.
/// \warning Modifying \p detail will cause undefined behavior.
/// \note The actual size of \p detail will vary by platform, this is just
/// the one for the platform this documentation was built on.
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, &mut *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(),
}
}
pub fn rewound(&self) -> Self {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
Self {
detail: detail.rewound().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,out] 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 `!= NULL`.
/// \internal
///
/// #Safety
/// changes must be a valid pointer to an 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 `!= NULL`.
/// \pre \p changes2 `!= NULL`.
/// \internal
///
/// #Safety
/// changes1 must be a valid pointer to an AMchanges
/// changes2 must be a valid pointer to an 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 Allocates an iterator over a sequence of changes and initializes it
/// from a sequence of byte spans.
///
/// \param[in] src A pointer to an array of `AMbyteSpan` structs.
/// \param[in] count The number of `AMbyteSpan` structs to copy from \p src.
/// \return A pointer to an `AMresult` struct containing an `AMchanges` struct.
/// \pre \p src `!= NULL`.
/// \pre `0 <` \p count `<= sizeof(`\p src`) / sizeof(AMbyteSpan)`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
/// src must be an AMbyteSpan array of size `>= count`
#[no_mangle]
pub unsafe extern "C" fn AMchangesInit(src: *const AMbyteSpan, count: usize) -> *mut AMresult {
let mut changes = Vec::<am::Change>::new();
for n in 0..count {
let byte_span = &*src.add(n);
let slice = std::slice::from_raw_parts(byte_span.src, byte_span.count);
match slice.try_into() {
Ok(change) => {
changes.push(change);
}
Err(e) => {
return to_result(Err::<Vec<am::Change>, am::LoadChangeError>(e));
}
}
}
to_result(Ok::<Vec<am::Change>, am::LoadChangeError>(changes))
}
/// \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,out] 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 `!= NULL`.
/// \internal
///
/// #Safety
/// changes must be a valid pointer to an 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,out] 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 `!= NULL`.
/// \internal
///
/// #Safety
/// changes must be a valid pointer to an 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 `!= NULL`.
/// \internal
///
/// #Safety
/// changes must be a valid pointer to an 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 `!= NULL`.
/// \internal
///
/// #Safety
/// changes must be a valid pointer to an AMchanges
#[no_mangle]
pub unsafe extern "C" fn AMchangesReversed(changes: *const AMchanges) -> AMchanges {
if let Some(changes) = changes.as_ref() {
changes.reversed()
} else {
Default::default()
}
}
/// \memberof AMchanges
/// \brief Creates an iterator at the starting position over the same sequence
/// of changes as the given one.
///
/// \param[in] changes A pointer to an `AMchanges` struct.
/// \return An `AMchanges` struct
/// \pre \p changes `!= NULL`.
/// \internal
///
/// #Safety
/// changes must be a valid pointer to an AMchanges
#[no_mangle]
pub unsafe extern "C" fn AMchangesRewound(changes: *const AMchanges) -> AMchanges {
if let Some(changes) = changes.as_ref() {
changes.rewound()
} else {
Default::default()
}
}

View file

@ -6,23 +6,43 @@ use std::ops::{Deref, DerefMut};
use crate::actor_id::{to_actor_id, AMactorId};
use crate::byte_span::{to_str, AMbyteSpan};
use crate::items::AMitems;
use crate::change_hashes::AMchangeHashes;
use crate::obj::{to_obj_id, AMobjId, AMobjType};
use crate::result::{to_result, AMresult};
use crate::result::{to_result, AMresult, AMvalue};
use crate::sync::{to_sync_message, AMsyncMessage, AMsyncState};
pub mod list;
pub mod map;
pub mod utils;
use crate::doc::utils::{clamp, to_doc, to_doc_mut, to_items};
use crate::changes::AMchanges;
use crate::doc::utils::{to_doc, to_doc_mut};
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_index {
($index:expr, $len:expr, $param_name:expr) => {{
if $index > $len && $index != usize::MAX {
return AMresult::err(&format!("Invalid {} {}", $param_name, $index)).into();
}
std::cmp::min($index, $len)
}};
}
macro_rules! to_sync_state_mut {
($handle:expr) => {{
let handle = $handle.as_mut();
match handle {
Some(b) => b,
None => return AMresult::error("Invalid `AMsyncState*`").into(),
None => return AMresult::err("Invalid AMsyncState pointer").into(),
}
}};
}
@ -37,10 +57,6 @@ impl AMdoc {
pub fn new(auto_commit: am::AutoCommit) -> Self {
Self(auto_commit)
}
pub fn is_equal_to(&mut self, other: &mut Self) -> bool {
self.document().get_heads() == other.document().get_heads()
}
}
impl AsRef<am::AutoCommit> for AMdoc {
@ -66,38 +82,38 @@ impl DerefMut for AMdoc {
/// \memberof AMdoc
/// \brief Applies a sequence of changes to a document.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] items A pointer to an `AMitems` struct with `AM_VAL_TYPE_CHANGE`
/// items.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_VOID` item.
/// \pre \p doc `!= NULL`
/// \pre \p items `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] changes A pointer to an `AMchanges` struct.
/// \pre \p doc `!= NULL`.
/// \pre \p changes `!= NULL`.
/// \return A pointer to an `AMresult` struct containing a void.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// items must be a valid pointer to an AMitems.
/// changes must be a valid pointer to an AMchanges.
#[no_mangle]
pub unsafe extern "C" fn AMapplyChanges(doc: *mut AMdoc, items: *const AMitems) -> *mut AMresult {
pub unsafe extern "C" fn AMapplyChanges(
doc: *mut AMdoc,
changes: *const AMchanges,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let items = to_items!(items);
match Vec::<am::Change>::try_from(items) {
Ok(changes) => to_result(doc.apply_changes(changes)),
Err(e) => AMresult::error(&e.to_string()).into(),
}
let changes = to_changes!(changes);
to_result(doc.apply_changes(changes.as_ref().to_vec()))
}
/// \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 with an `AM_VAL_TYPE_DOC` item.
/// \pre \p doc `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \return A pointer to an `AMresult` struct containing a pointer to an
/// `AMdoc` struct.
/// \pre \p doc `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
@ -113,9 +129,10 @@ pub unsafe extern "C" fn AMclone(doc: *const AMdoc) -> *mut AMresult {
///
/// \param[in] actor_id A pointer to an `AMactorId` struct or `NULL` for a
/// random one.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_DOC` 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
/// `AMdoc` struct.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
@ -132,15 +149,15 @@ pub unsafe extern "C" fn AMcreate(actor_id: *const AMactorId) -> *mut AMresult {
/// \brief Commits the current operations on a document with an optional
/// message and/or *nix timestamp (milliseconds).
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] message A UTF-8 string view as an `AMbyteSpan` struct.
/// \param[in] timestamp A pointer to a 64-bit integer or `NULL`.
/// \return A pointer to an `AMresult` struct with one `AM_VAL_TYPE_CHANGE_HASH`
/// item if there were operations to commit or an `AM_VAL_TYPE_VOID` item
/// if there were no operations to commit.
/// \pre \p doc `!= 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 `AMchangeHashes`
/// with one element if there were operations to commit, or void if
/// there were no operations to commit.
/// \pre \p doc `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
@ -166,24 +183,24 @@ pub unsafe extern "C" fn AMcommit(
/// \brief Creates an empty change with an optional message and/or *nix
/// timestamp (milliseconds).
///
/// \details This is useful if you wish to create a "merge commit" which has as
/// its dependents the current heads of the document but you don't have
/// any operations to add to the document.
/// This is useful if you wish to create a "merge commit" which has as its
/// dependents the current heads of the document but you don't have any
/// operations to add to the document.
///
/// \note If there are outstanding uncommitted changes to the document
/// then two changes will be created: one for creating the outstanding
/// changes and one for the empty change. The empty change will always be
/// the latest change in the document after this call and the returned
/// hash will be the hash of that empty change.
/// then two changes will be created: one for creating the outstanding changes
/// and one for the empty change. The empty change will always be the
/// latest change in the document after this call and the returned hash will be
/// the hash of that empty change.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] message A UTF-8 string view as an `AMbyteSpan` struct.
/// \param[in] timestamp A pointer to a 64-bit integer or `NULL`.
/// \return A pointer to an `AMresult` struct with one `AM_VAL_TYPE_CHANGE_HASH`
/// item.
/// \pre \p doc `!= 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 `AMchangeHashes`
/// with one element.
/// \pre \p doc `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
@ -209,11 +226,11 @@ pub unsafe extern "C" fn AMemptyChange(
/// \brief Tests the equality of two documents after closing their respective
/// transactions.
///
/// \param[in] doc1 A pointer to an `AMdoc` struct.
/// \param[in] doc2 A pointer to an `AMdoc` struct.
/// \param[in,out] doc1 An `AMdoc` struct.
/// \param[in,out] doc2 An `AMdoc` struct.
/// \return `true` if \p doc1 `==` \p doc2 and `false` otherwise.
/// \pre \p doc1 `!= NULL`
/// \pre \p doc2 `!= NULL`
/// \pre \p doc1 `!= NULL`.
/// \pre \p doc2 `!= NULL`.
/// \internal
///
/// #Safety
@ -222,36 +239,33 @@ pub unsafe extern "C" fn AMemptyChange(
#[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.is_equal_to(doc2),
(None, None) | (None, Some(_)) | (Some(_), None) => false,
(Some(doc1), Some(doc2)) => doc1.document().get_heads() == doc2.document().get_heads(),
(None, Some(_)) | (Some(_), None) | (None, None) => false,
}
}
/// \memberof AMdoc
/// \brief Forks this document at its current or a historical point for use by
/// \brief Forks this document at the current or a historical point for use by
/// a different actor.
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] heads A pointer to an `AMitems` struct with `AM_VAL_TYPE_CHANGE_HASH`
/// items to select a historical point or `NULL` to select its
/// current point.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_VOID` item.
/// \pre \p doc `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] heads A pointer to an `AMchangeHashes` struct for a historical
/// point or `NULL` for the current point.
/// \return A pointer to an `AMresult` struct containing a pointer to an
/// `AMdoc` struct.
/// \pre \p doc `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// heads must be a valid pointer to an AMitems or std::ptr::null()
/// heads must be a valid pointer to an AMchangeHashes or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMfork(doc: *mut AMdoc, heads: *const AMitems) -> *mut AMresult {
pub unsafe extern "C" fn AMfork(doc: *mut AMdoc, heads: *const AMchangeHashes) -> *mut AMresult {
let doc = to_doc_mut!(doc);
match heads.as_ref() {
None => to_result(doc.fork()),
Some(heads) => match <Vec<am::ChangeHash>>::try_from(heads) {
Ok(heads) => to_result(doc.fork_at(&heads)),
Err(e) => AMresult::error(&e.to_string()).into(),
},
Some(heads) => to_result(doc.fork_at(heads.as_ref())),
}
}
@ -259,14 +273,14 @@ pub unsafe extern "C" fn AMfork(doc: *mut AMdoc, heads: *const AMitems) -> *mut
/// \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 with either an
/// `AM_VAL_TYPE_SYNC_MESSAGE` or `AM_VAL_TYPE_VOID` item.
/// \pre \p doc `!= NULL`
/// \pre \p sync_state `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in,out] 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 `!= NULL`.
/// \pre \p sync_state `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
@ -286,10 +300,11 @@ pub unsafe extern "C" fn AMgenerateSyncMessage(
/// \brief Gets a document's actor identifier.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_ACTOR_ID` item.
/// \pre \p doc `!= 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 a pointer to an
/// `AMactorId` struct.
/// \pre \p doc `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
@ -305,22 +320,20 @@ pub unsafe extern "C" fn AMgetActorId(doc: *const AMdoc) -> *mut AMresult {
/// \memberof AMdoc
/// \brief Gets the change added to a document by its respective hash.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] src A pointer to an array of bytes.
/// \param[in] count The count of bytes to copy from the array pointed to by
/// \p src.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_CHANGE` item.
/// \pre \p doc `!= NULL`
/// \pre \p src `!= NULL`
/// \pre `sizeof(`\p src') >= AM_CHANGE_HASH_SIZE`
/// \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.
/// \return A pointer to an `AMresult` struct containing an `AMchanges` struct.
/// \pre \p doc `!= NULL`.
/// \pre \p src `!= NULL`.
/// \pre \p count `>= AM_CHANGE_HASH_SIZE`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// src must be a byte array of length `>= automerge::types::HASH_SIZE`
/// src must be a byte array of size `>= automerge::types::HASH_SIZE`
#[no_mangle]
pub unsafe extern "C" fn AMgetChangeByHash(
doc: *mut AMdoc,
@ -331,48 +344,48 @@ pub unsafe extern "C" fn AMgetChangeByHash(
let slice = std::slice::from_raw_parts(src, count);
match slice.try_into() {
Ok(change_hash) => to_result(doc.get_change_by_hash(&change_hash)),
Err(e) => AMresult::error(&e.to_string()).into(),
Err(e) => AMresult::err(&e.to_string()).into(),
}
}
/// \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 `AMitems` struct with
/// `AM_VAL_TYPE_CHANGE_HASH` items or `NULL`.
/// \return A pointer to an `AMresult` struct with `AM_VAL_TYPE_CHANGE` items.
/// \pre \p doc `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \param[in,out] 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 `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
/// doc must be a valid pointer to an AMdoc
#[no_mangle]
pub unsafe extern "C" fn AMgetChanges(doc: *mut AMdoc, have_deps: *const AMitems) -> *mut AMresult {
pub unsafe extern "C" fn AMgetChanges(
doc: *mut AMdoc,
have_deps: *const AMchangeHashes,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let empty_deps = Vec::<am::ChangeHash>::new();
let have_deps = match have_deps.as_ref() {
Some(have_deps) => match Vec::<am::ChangeHash>::try_from(have_deps) {
Ok(change_hashes) => change_hashes,
Err(e) => return AMresult::error(&e.to_string()).into(),
},
None => Vec::<am::ChangeHash>::new(),
Some(have_deps) => have_deps.as_ref(),
None => &empty_deps,
};
to_result(doc.get_changes(&have_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 A pointer to an `AMdoc` struct.
/// \param[in] doc2 A pointer to an `AMdoc` struct.
/// \return A pointer to an `AMresult` struct with `AM_VAL_TYPE_CHANGE` items.
/// \pre \p doc1 `!= NULL`
/// \pre \p doc2 `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \param[in,out] doc1 An `AMdoc` struct.
/// \param[in,out] doc2 An `AMdoc` struct.
/// \return A pointer to an `AMresult` struct containing an `AMchanges` struct.
/// \pre \p doc1 `!= NULL`.
/// \pre \p doc2 `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
@ -388,11 +401,12 @@ pub unsafe extern "C" fn AMgetChangesAdded(doc1: *mut AMdoc, doc2: *mut AMdoc) -
/// \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 with `AM_VAL_TYPE_CHANGE_HASH` items.
/// \pre \p doc `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \return A pointer to an `AMresult` struct containing an `AMchangeHashes`
/// struct.
/// \pre \p doc `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
@ -409,42 +423,41 @@ pub unsafe extern "C" fn AMgetHeads(doc: *mut AMdoc) -> *mut AMresult {
/// \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 `AMitems` struct with `AM_VAL_TYPE_CHANGE_HASH`
/// items or `NULL`.
/// \return A pointer to an `AMresult` struct with `AM_VAL_TYPE_CHANGE_HASH` items.
/// \pre \p doc `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \param[in,out] 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 `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// heads must be a valid pointer to an AMitems or std::ptr::null()
/// heads must be a valid pointer to an AMchangeHashes or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMgetMissingDeps(doc: *mut AMdoc, heads: *const AMitems) -> *mut AMresult {
pub unsafe extern "C" fn AMgetMissingDeps(
doc: *mut AMdoc,
heads: *const AMchangeHashes,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let empty_heads = Vec::<am::ChangeHash>::new();
let heads = match heads.as_ref() {
None => Vec::<am::ChangeHash>::new(),
Some(heads) => match <Vec<am::ChangeHash>>::try_from(heads) {
Ok(heads) => heads,
Err(e) => {
return AMresult::error(&e.to_string()).into();
}
},
Some(heads) => heads.as_ref(),
None => &empty_heads,
};
to_result(doc.get_missing_deps(heads.as_slice()))
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
/// `AM_VAL_TYPE_CHANGE` or `AM_VAL_TYPE_VOID` item.
/// \pre \p doc `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \param[in,out] 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 `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
@ -460,33 +473,29 @@ pub unsafe extern "C" fn AMgetLastLocalChange(doc: *mut AMdoc) -> *mut AMresult
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] heads A pointer to an `AMitems` struct with `AM_VAL_TYPE_CHANGE_HASH`
/// items to select historical keys or `NULL` to select current
/// keys.
/// \return A pointer to an `AMresult` struct with `AM_VAL_TYPE_STR` items.
/// \pre \p doc `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \param[in] heads A pointer to an `AMchangeHashes` struct for historical
/// keys or `NULL` for current keys.
/// \return A pointer to an `AMresult` struct containing an `AMstrs` struct.
/// \pre \p doc `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// heads must be a valid pointer to an AMitems or std::ptr::null()
/// heads must be a valid pointer to an AMchangeHashes or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMkeys(
doc: *const AMdoc,
obj_id: *const AMobjId,
heads: *const AMitems,
heads: *const AMchangeHashes,
) -> *mut AMresult {
let doc = to_doc!(doc);
let obj_id = to_obj_id!(obj_id);
match heads.as_ref() {
None => to_result(doc.keys(obj_id)),
Some(heads) => match <Vec<am::ChangeHash>>::try_from(heads) {
Ok(heads) => to_result(doc.keys_at(obj_id, &heads)),
Err(e) => AMresult::error(&e.to_string()).into(),
},
Some(heads) => to_result(doc.keys_at(obj_id, heads.as_ref())),
}
}
@ -495,43 +504,42 @@ pub unsafe extern "C" fn AMkeys(
/// form of an incremental save.
///
/// \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_DOC` 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 a pointer to an
/// `AMdoc` struct.
/// \pre \p src `!= NULL`.
/// \pre `0 <` \p count `<= sizeof(`\p src`)`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
/// src must be a byte array of length `>= count`
/// src must be a byte array of size `>= count`
#[no_mangle]
pub unsafe extern "C" fn AMload(src: *const u8, count: usize) -> *mut AMresult {
let data = std::slice::from_raw_parts(src, count);
to_result(am::AutoCommit::load(data))
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,out] doc A pointer to an `AMdoc` struct.
/// \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_UINT` item.
/// \pre \p doc `!= NULL`
/// \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 the number of
/// operations loaded from \p src.
/// \pre \p doc `!= NULL`.
/// \pre \p src `!= NULL`.
/// \pre `0 <` \p count `<= sizeof(`\p src`)`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// src must be a byte array of length `>= count`
/// src must be a byte array of size `>= count`
#[no_mangle]
pub unsafe extern "C" fn AMloadIncremental(
doc: *mut AMdoc,
@ -539,21 +547,23 @@ pub unsafe extern "C" fn AMloadIncremental(
count: usize,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let data = std::slice::from_raw_parts(src, count);
to_result(doc.load_incremental(data))
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 with `AM_VAL_TYPE_CHANGE_HASH` items.
/// \pre \p dest `!= NULL`
/// \pre \p src `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \param[in,out] dest A pointer to an `AMdoc` struct.
/// \param[in,out] src A pointer to an `AMdoc` struct.
/// \return A pointer to an `AMresult` struct containing an `AMchangeHashes`
/// struct.
/// \pre \p dest `!= NULL`.
/// \pre \p src `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
@ -570,37 +580,31 @@ pub unsafe extern "C" fn AMmerge(dest: *mut AMdoc, src: *mut AMdoc) -> *mut AMre
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] heads A pointer to an `AMitems` struct with `AM_VAL_TYPE_CHANGE_HASH`
/// items to select a historical size or `NULL` to select its
/// current size.
/// \return The count of items in the object identified by \p obj_id.
/// \pre \p doc `!= NULL`
/// \param[in] heads A pointer to an `AMchangeHashes` struct for historical
/// size or `NULL` for current size.
/// \return A 64-bit unsigned integer.
/// \pre \p doc `!= NULL`.
/// \internal
///
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// heads must be a valid pointer to an AMitems or std::ptr::null()
/// heads must be a valid pointer to an AMchangeHashes or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMobjSize(
doc: *const AMdoc,
obj_id: *const AMobjId,
heads: *const AMitems,
heads: *const AMchangeHashes,
) -> usize {
if let Some(doc) = doc.as_ref() {
let obj_id = to_obj_id!(obj_id);
match heads.as_ref() {
None => {
return doc.length(obj_id);
}
Some(heads) => {
if let Ok(heads) = <Vec<am::ChangeHash>>::try_from(heads) {
return doc.length_at(obj_id, &heads);
}
}
None => doc.length(obj_id),
Some(heads) => doc.length_at(obj_id, heads.as_ref()),
}
} else {
0
}
0
}
/// \memberof AMdoc
@ -608,9 +612,8 @@ pub unsafe extern "C" fn AMobjSize(
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \return An `AMobjType` tag or `0`.
/// \pre \p doc `!= NULL`
/// \pre \p obj_id `!= NULL`
/// \return An `AMobjType`.
/// \pre \p doc `!= NULL`.
/// \internal
///
/// # Safety
@ -620,45 +623,44 @@ pub unsafe extern "C" fn AMobjSize(
pub unsafe extern "C" fn AMobjObjType(doc: *const AMdoc, obj_id: *const AMobjId) -> AMobjType {
if let Some(doc) = doc.as_ref() {
let obj_id = to_obj_id!(obj_id);
if let Ok(obj_type) = doc.object_type(obj_id) {
return (&obj_type).into();
match doc.object_type(obj_id) {
Err(_) => AMobjType::Void,
Ok(obj_type) => obj_type.into(),
}
} else {
AMobjType::Void
}
Default::default()
}
/// \memberof AMdoc
/// \brief Gets the current or historical items of an entire object.
/// \brief Gets the current or historical values of an object within its entire
/// range.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] heads A pointer to an `AMitems` struct with `AM_VAL_TYPE_CHANGE_HASH`
/// items to select its historical items or `NULL` to select
/// its current items.
/// \return A pointer to an `AMresult` struct with an `AMitems` struct.
/// \pre \p doc `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \param[in] heads A pointer to an `AMchangeHashes` struct for historical
/// items or `NULL` for current items.
/// \return A pointer to an `AMresult` struct containing an `AMobjItems` struct.
/// \pre \p doc `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// heads must be a valid pointer to an AMitems or std::ptr::null()
/// heads must be a valid pointer to an AMchangeHashes or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMobjItems(
pub unsafe extern "C" fn AMobjValues(
doc: *const AMdoc,
obj_id: *const AMobjId,
heads: *const AMitems,
heads: *const AMchangeHashes,
) -> *mut AMresult {
let doc = to_doc!(doc);
let obj_id = to_obj_id!(obj_id);
match heads.as_ref() {
None => to_result(doc.values(obj_id)),
Some(heads) => match <Vec<am::ChangeHash>>::try_from(heads) {
Ok(heads) => to_result(doc.values_at(obj_id, &heads)),
Err(e) => AMresult::error(&e.to_string()).into(),
},
Some(heads) => to_result(doc.values_at(obj_id, heads.as_ref())),
}
}
@ -668,7 +670,7 @@ pub unsafe extern "C" fn AMobjItems(
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \return The count of pending operations for \p doc.
/// \pre \p doc `!= NULL`
/// \pre \p doc `!= NULL`.
/// \internal
///
/// # Safety
@ -676,22 +678,23 @@ pub unsafe extern "C" fn AMobjItems(
#[no_mangle]
pub unsafe extern "C" fn AMpendingOps(doc: *const AMdoc) -> usize {
if let Some(doc) = doc.as_ref() {
return doc.pending_ops();
doc.pending_ops()
} else {
0
}
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,out] doc A pointer to an `AMdoc` struct.
/// \param[in,out] 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 with an `AM_VAL_TYPE_VOID` item.
/// \pre \p doc `!= NULL`
/// \pre \p sync_state `!= NULL`
/// \pre \p sync_message `!= NULL`
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc `!= NULL`.
/// \pre \p sync_state `!= NULL`.
/// \pre \p sync_message `!= NULL`.
/// \internal
///
/// # Safety
@ -717,9 +720,9 @@ pub unsafe extern "C" fn AMreceiveSyncMessage(
/// \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.
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \return The count of pending operations for \p doc that were cancelled.
/// \pre \p doc `!= NULL`
/// \pre \p doc `!= NULL`.
/// \internal
///
/// # Safety
@ -727,19 +730,21 @@ pub unsafe extern "C" fn AMreceiveSyncMessage(
#[no_mangle]
pub unsafe extern "C" fn AMrollback(doc: *mut AMdoc) -> usize {
if let Some(doc) = doc.as_mut() {
return doc.rollback();
doc.rollback()
} else {
0
}
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 with an `AM_VAL_TYPE_BYTES` item.
/// \pre \p doc `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \param[in,out] 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 `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
@ -754,11 +759,12 @@ pub unsafe extern "C" fn AMsave(doc: *mut AMdoc) -> *mut AMresult {
/// \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 with an `AM_VAL_TYPE_BYTES` item.
/// \pre \p doc `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \param[in,out] 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 `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
@ -772,13 +778,13 @@ pub unsafe extern "C" fn AMsaveIncremental(doc: *mut AMdoc) -> *mut AMresult {
/// \memberof AMdoc
/// \brief Puts the actor identifier of a document.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] actor_id A pointer to an `AMactorId` struct.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_VOID` item.
/// \pre \p doc `!= NULL`
/// \pre \p actor_id `!= 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 a void.
/// \pre \p doc `!= NULL`.
/// \pre \p actor_id `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
@ -799,65 +805,76 @@ pub unsafe extern "C" fn AMsetActorId(
/// \brief Splices values into and/or removes values from the identified object
/// at a given position within it.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] pos A position in the object identified by \p obj_id or
/// `SIZE_MAX` to indicate one past its end.
/// \param[in] del The number of values to delete or `SIZE_MAX` to indicate
/// \param[in] del The number of characters to delete or `SIZE_MAX` to indicate
/// all of them.
/// \param[in] values A copy of an `AMitems` struct from which values will be
/// spliced <b>starting at its current position</b>; call
/// `AMitemsRewound()` on a used `AMitems` first to ensure
/// that all of its values are spliced in. Pass `(AMitems){0}`
/// when zero values should be spliced in.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_VOID` item.
/// \pre \p doc `!= NULL`
/// \pre `0 <=` \p pos `<= AMobjSize(`\p obj_id `)` or \p pos `== SIZE_MAX`
/// \pre `0 <=` \p del `<= AMobjSize(`\p obj_id `)` or \p del `== SIZE_MAX`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \param[in] src A pointer to an array of `AMvalue` structs.
/// \param[in] count The number of `AMvalue` structs in \p src to load.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc `!= NULL`.
/// \pre `0 <=` \p pos `<= AMobjSize(`\p obj_id`)` or \p pos `== SIZE_MAX`.
/// \pre `0 <=` \p del `<= AMobjSize(`\p obj_id`)` or \p del `== SIZE_MAX`.
/// \pre `(`\p src `!= NULL and 1 <=` \p count `<= sizeof(`\p src`)/
/// sizeof(AMvalue)) or `\p src `== NULL or `\p count `== 0`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// values must be a valid pointer to an AMitems or std::ptr::null()
/// src must be an AMvalue array of size `>= count` or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMsplice(
doc: *mut AMdoc,
obj_id: *const AMobjId,
pos: usize,
del: usize,
values: AMitems,
src: *const AMvalue,
count: usize,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let obj_id = to_obj_id!(obj_id);
let len = doc.length(obj_id);
let pos = clamp!(pos, len, "pos");
let del = clamp!(del, len, "del");
match Vec::<am::ScalarValue>::try_from(&values) {
Ok(vals) => to_result(doc.splice(obj_id, pos, del, vals)),
Err(e) => AMresult::error(&e.to_string()).into(),
let pos = to_index!(pos, len, "pos");
let del = to_index!(del, len, "del");
let mut vals: Vec<am::ScalarValue> = vec![];
if !(src.is_null() || count == 0) {
let c_vals = std::slice::from_raw_parts(src, count);
for c_val in c_vals {
match c_val.try_into() {
Ok(s) => {
vals.push(s);
}
Err(e) => {
return AMresult::err(&e.to_string()).into();
}
}
}
}
to_result(doc.splice(obj_id, pos, del, vals))
}
/// \memberof AMdoc
/// \brief Splices characters into and/or removes characters from the
/// identified object at a given position within it.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] pos A position in the text object identified by \p obj_id or
/// `SIZE_MAX` to indicate one past its end.
/// \param[in] del The number of characters to delete or `SIZE_MAX` to indicate
/// all of them.
/// \param[in] text A UTF-8 string view as an `AMbyteSpan` struct.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_VOID` item.
/// \pre \p doc `!= NULL`
/// \pre `0 <=` \p pos `<= AMobjSize(`\p obj_id `)` or \p pos `== SIZE_MAX`
/// \pre `0 <=` \p del `<= AMobjSize(`\p obj_id `)` or \p del `== SIZE_MAX`
/// \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 void.
/// \pre \p doc `!= NULL`.
/// \pre `0 <=` \p pos `<= AMobjSize(`\p obj_id`)` or \p pos `== SIZE_MAX`.
/// \pre `0 <=` \p del `<= AMobjSize(`\p obj_id`)` or \p del `== SIZE_MAX`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
@ -874,8 +891,8 @@ pub unsafe extern "C" fn AMspliceText(
let doc = to_doc_mut!(doc);
let obj_id = to_obj_id!(obj_id);
let len = doc.length(obj_id);
let pos = clamp!(pos, len, "pos");
let del = clamp!(del, len, "del");
let pos = to_index!(pos, len, "pos");
let del = to_index!(del, len, "del");
to_result(doc.splice_text(obj_id, pos, del, to_str!(text)))
}
@ -884,32 +901,28 @@ pub unsafe extern "C" fn AMspliceText(
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] heads A pointer to an `AMitems` struct containing
/// `AM_VAL_TYPE_CHANGE_HASH` items to select a historical string
/// or `NULL` to select the current string.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_STR` item.
/// \pre \p doc `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \param[in] heads A pointer to an `AMchangeHashes` struct for historical
/// keys or `NULL` for current keys.
/// \return A pointer to an `AMresult` struct containing a UTF-8 string.
/// \pre \p doc `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// heads must be a valid pointer to an AMitems or std::ptr::null()
/// heads must be a valid pointer to an AMchangeHashes or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMtext(
doc: *const AMdoc,
obj_id: *const AMobjId,
heads: *const AMitems,
heads: *const AMchangeHashes,
) -> *mut AMresult {
let doc = to_doc!(doc);
let obj_id = to_obj_id!(obj_id);
match heads.as_ref() {
None => to_result(doc.text(obj_id)),
Some(heads) => match <Vec<am::ChangeHash>>::try_from(heads) {
Ok(heads) => to_result(doc.text_at(obj_id, &heads)),
Err(e) => AMresult::error(&e.to_string()).into(),
},
Some(heads) => to_result(doc.text_at(obj_id, heads.as_ref())),
}
}

View file

@ -3,44 +3,47 @@ use automerge::transaction::Transactable;
use automerge::ReadDoc;
use crate::byte_span::{to_str, AMbyteSpan};
use crate::doc::{to_doc, to_doc_mut, AMdoc};
use crate::items::AMitems;
use crate::obj::{to_obj_id, to_obj_type, AMobjId, AMobjType};
use crate::change_hashes::AMchangeHashes;
use crate::doc::{to_doc, to_doc_mut, to_obj_id, AMdoc};
use crate::obj::{to_obj_type, AMobjId, AMobjType};
use crate::result::{to_result, AMresult};
pub mod item;
pub mod items;
macro_rules! adjust {
($pos:expr, $insert:expr, $len:expr) => {{
($index:expr, $insert:expr, $len:expr) => {{
// An empty object can only be inserted into.
let insert = $insert || $len == 0;
let end = if insert { $len } else { $len - 1 };
if $pos > end && $pos != usize::MAX {
return AMresult::error(&format!("Invalid pos {}", $pos)).into();
if $index > end && $index != usize::MAX {
return AMresult::err(&format!("Invalid index {}", $index)).into();
}
(std::cmp::min($pos, end), insert)
(std::cmp::min($index, end), insert)
}};
}
macro_rules! to_range {
($begin:expr, $end:expr) => {{
if $begin > $end {
return AMresult::error(&format!("Invalid range [{}-{})", $begin, $end)).into();
return AMresult::err(&format!("Invalid range [{}-{})", $begin, $end)).into();
};
($begin..$end)
}};
}
/// \memberof AMdoc
/// \brief Deletes an item from a list object.
/// \brief Deletes an index in a list object.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] pos The position of an item within the list object identified by
/// \p obj_id or `SIZE_MAX` to indicate its last item.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_VOID` item.
/// \pre \p doc `!= NULL`
/// \pre `0 <=` \p pos `<= AMobjSize(`\p obj_id `)` or \p pos `== SIZE_MAX`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \param[in] index An index in the list object identified by \p obj_id or
/// `SIZE_MAX` to indicate its last index.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc `!= NULL`.
/// \pre `0 <=` \p index `<= AMobjSize(`\p obj_id`)` or \p index `== SIZE_MAX`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
@ -50,109 +53,101 @@ macro_rules! to_range {
pub unsafe extern "C" fn AMlistDelete(
doc: *mut AMdoc,
obj_id: *const AMobjId,
pos: usize,
index: usize,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let obj_id = to_obj_id!(obj_id);
let (pos, _) = adjust!(pos, false, doc.length(obj_id));
to_result(doc.delete(obj_id, pos))
let (index, _) = adjust!(index, false, doc.length(obj_id));
to_result(doc.delete(obj_id, index))
}
/// \memberof AMdoc
/// \brief Gets a current or historical item within a list object.
/// \brief Gets the current or historical 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 `AM_ROOT`.
/// \param[in] pos The position of an item within the list object identified by
/// \p obj_id or `SIZE_MAX` to indicate its last item.
/// \param[in] heads A pointer to an `AMitems` struct with `AM_VAL_TYPE_CHANGE_HASH`
/// items to select a historical item at \p pos or `NULL`
/// to select the current item at \p pos.
/// \return A pointer to an `AMresult` struct with an `AMitem` struct.
/// \pre \p doc `!= NULL`
/// \pre `0 <=` \p pos `<= AMobjSize(`\p obj_id `)` or \p pos `== SIZE_MAX`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \param[in] index An index in the list object identified by \p obj_id or
/// `SIZE_MAX` to indicate its last index.
/// \param[in] heads A pointer to an `AMchangeHashes` struct for a historical
/// value or `NULL` for the current value.
/// \return A pointer to an `AMresult` struct that doesn't contain a void.
/// \pre \p doc `!= NULL`.
/// \pre `0 <=` \p index `<= AMobjSize(`\p obj_id`)` or \p index `== SIZE_MAX`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// heads must be a valid pointer to an AMitems or std::ptr::null()
/// heads must be a valid pointer to an AMchangeHashes or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMlistGet(
doc: *const AMdoc,
obj_id: *const AMobjId,
pos: usize,
heads: *const AMitems,
index: usize,
heads: *const AMchangeHashes,
) -> *mut AMresult {
let doc = to_doc!(doc);
let obj_id = to_obj_id!(obj_id);
let (pos, _) = adjust!(pos, false, doc.length(obj_id));
match heads.as_ref() {
None => to_result(doc.get(obj_id, pos)),
Some(heads) => match <Vec<am::ChangeHash>>::try_from(heads) {
Ok(heads) => to_result(doc.get_at(obj_id, pos, &heads)),
Err(e) => AMresult::error(&e.to_string()).into(),
},
}
let (index, _) = adjust!(index, false, doc.length(obj_id));
to_result(match heads.as_ref() {
None => doc.get(obj_id, index),
Some(heads) => doc.get_at(obj_id, index, heads.as_ref()),
})
}
/// \memberof AMdoc
/// \brief Gets all of the historical items at a position within a list object
/// until its current one or a specific one.
/// \brief Gets all of the historical values at an index in a list object until
/// its current one or a specific one.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] pos The position of an item within the list object identified by
/// \p obj_id or `SIZE_MAX` to indicate its last item.
/// \param[in] heads A pointer to an `AMitems` struct with `AM_VAL_TYPE_CHANGE_HASH`
/// items to select a historical last item or `NULL` to select
/// the current last item.
/// \return A pointer to an `AMresult` struct with an `AMitems` struct.
/// \pre \p doc `!= NULL`
/// \pre `0 <=` \p pos `<= AMobjSize(`\p obj_id `)` or \p pos `== SIZE_MAX`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \param[in] index An index in the list object identified by \p obj_id or
/// `SIZE_MAX` to indicate its last index.
/// \param[in] heads A pointer to an `AMchangeHashes` struct for a historical
/// last value or `NULL` for the current last value.
/// \return A pointer to an `AMresult` struct containing an `AMobjItems` struct.
/// \pre \p doc `!= NULL`.
/// \pre `0 <=` \p index `<= AMobjSize(`\p obj_id`)` or \p index `== SIZE_MAX`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// heads must be a valid pointer to an AMitems or std::ptr::null()
/// heads must be a valid pointer to an AMchangeHashes or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMlistGetAll(
doc: *const AMdoc,
obj_id: *const AMobjId,
pos: usize,
heads: *const AMitems,
index: usize,
heads: *const AMchangeHashes,
) -> *mut AMresult {
let doc = to_doc!(doc);
let obj_id = to_obj_id!(obj_id);
let (pos, _) = adjust!(pos, false, doc.length(obj_id));
let (index, _) = adjust!(index, false, doc.length(obj_id));
match heads.as_ref() {
None => to_result(doc.get_all(obj_id, pos)),
Some(heads) => match <Vec<am::ChangeHash>>::try_from(heads) {
Ok(heads) => to_result(doc.get_all_at(obj_id, pos, &heads)),
Err(e) => AMresult::error(&e.to_string()).into(),
},
None => to_result(doc.get_all(obj_id, index)),
Some(heads) => to_result(doc.get_all_at(obj_id, index, heads.as_ref())),
}
}
/// \memberof AMdoc
/// \brief Increments a counter value in an item within a list object by the
/// given value.
/// \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,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] pos The position of an item within the list object identified by
/// \p obj_id or `SIZE_MAX` to indicate its last item.
/// \param[in] index An index in the list object identified by \p obj_id or
/// `SIZE_MAX` to indicate its last index.
/// \param[in] value A 64-bit signed integer.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_VOID` item.
/// \pre \p doc `!= NULL`
/// \pre `0 <=` \p pos `<= AMobjSize(`\p obj_id `)` or \p pos `== SIZE_MAX`
/// \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 void.
/// \pre \p doc `!= NULL`.
/// \pre `0 <=` \p index `<= AMobjSize(`\p obj_id`)` or \p index `== SIZE_MAX`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
@ -162,33 +157,32 @@ pub unsafe extern "C" fn AMlistGetAll(
pub unsafe extern "C" fn AMlistIncrement(
doc: *mut AMdoc,
obj_id: *const AMobjId,
pos: usize,
index: usize,
value: i64,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let obj_id = to_obj_id!(obj_id);
let (pos, _) = adjust!(pos, false, doc.length(obj_id));
to_result(doc.increment(obj_id, pos, value))
let (index, _) = adjust!(index, false, doc.length(obj_id));
to_result(doc.increment(obj_id, index, value))
}
/// \memberof AMdoc
/// \brief Puts a boolean value into an item within a list object.
/// \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,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] pos The position of an item within the list object identified by
/// \p obj_id or `SIZE_MAX` to indicate its last item if
/// \p insert `== false` or one past its last item if
/// \p insert `== true`.
/// \param[in] insert A flag for inserting a new item for \p value before
/// \p pos instead of putting \p value into the item at
/// \p pos.
/// \param[in] index An index in the list object identified by \p obj_id or
/// `SIZE_MAX` to indicate its last index if \p insert
/// `== false` or one past its last index if \p insert
/// `== true`.
/// \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 with an `AM_VAL_TYPE_VOID` item.
/// \pre \p doc `!= NULL`
/// \pre `0 <=` \p pos `<= AMobjSize(`\p obj_id `)` or \p pos `== SIZE_MAX`
/// \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 void.
/// \pre \p doc `!= NULL`.
/// \pre `0 <=` \p index `<= AMobjSize(`\p obj_id`)` or \p index `== SIZE_MAX`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
@ -198,85 +192,84 @@ pub unsafe extern "C" fn AMlistIncrement(
pub unsafe extern "C" fn AMlistPutBool(
doc: *mut AMdoc,
obj_id: *const AMobjId,
pos: usize,
index: usize,
insert: bool,
value: bool,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let obj_id = to_obj_id!(obj_id);
let (pos, insert) = adjust!(pos, insert, doc.length(obj_id));
let (index, insert) = adjust!(index, insert, doc.length(obj_id));
let value = am::ScalarValue::Boolean(value);
to_result(if insert {
doc.insert(obj_id, pos, value)
doc.insert(obj_id, index, value)
} else {
doc.put(obj_id, pos, value)
doc.put(obj_id, index, value)
})
}
/// \memberof AMdoc
/// \brief Puts an array of bytes value at a position within a list object.
/// \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,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] pos The position of an item within the list object identified by
/// \p obj_id or `SIZE_MAX` to indicate its last item if
/// \p insert `== false` or one past its last item if
/// \p insert `== true`.
/// \param[in] insert A flag for inserting a new item for \p value before
/// \p pos instead of putting \p value into the item at
/// \p pos.
/// \param[in] value A view onto the array of bytes to copy from as an
/// `AMbyteSpan` struct.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_VOID` item.
/// \pre \p doc `!= NULL`
/// \pre `0 <=` \p pos `<= AMobjSize(`\p obj_id `)` or \p pos `== SIZE_MAX`
/// \pre \p value.src `!= NULL`
/// \pre `0 <` \p value.count `<= sizeof(`\p value.src `)`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \param[in] index An index in the list object identified by \p obj_id or
/// `SIZE_MAX` to indicate its last index if \p insert
/// `== false` or one past its last index if \p insert
/// `== true`.
/// \param[in] insert A flag to insert \p src before \p index instead of
/// writing \p src over \p index.
/// \param[in] src A pointer to an array of bytes.
/// \param[in] count The number of bytes to copy from \p src.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc `!= NULL`.
/// \pre `0 <=` \p index `<= AMobjSize(`\p obj_id`)` or \p index `== SIZE_MAX`.
/// \pre \p src `!= NULL`.
/// \pre `0 <` \p count `<= sizeof(`\p src`)`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// value.src must be a byte array of length >= value.count
/// src must be a byte array of size `>= count`
#[no_mangle]
pub unsafe extern "C" fn AMlistPutBytes(
doc: *mut AMdoc,
obj_id: *const AMobjId,
pos: usize,
index: usize,
insert: bool,
value: AMbyteSpan,
val: AMbyteSpan,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let obj_id = to_obj_id!(obj_id);
let (pos, insert) = adjust!(pos, insert, doc.length(obj_id));
let value: Vec<u8> = (&value).into();
let (index, insert) = adjust!(index, insert, doc.length(obj_id));
let mut value = Vec::new();
value.extend_from_slice(std::slice::from_raw_parts(val.src, val.count));
to_result(if insert {
doc.insert(obj_id, pos, value)
doc.insert(obj_id, index, value)
} else {
doc.put(obj_id, pos, value)
doc.put(obj_id, index, value)
})
}
/// \memberof AMdoc
/// \brief Puts a CRDT counter value into an item within a list object.
/// \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,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] pos The position of an item within the list object identified by
/// \p obj_id or `SIZE_MAX` to indicate its last item if
/// \p insert `== false` or one past its last item if
/// \p insert `== true`.
/// \param[in] insert A flag for inserting a new item for \p value before
/// \p pos instead of putting \p value into the item at
/// \p pos.
/// \param[in] index An index in the list object identified by \p obj_id or
/// `SIZE_MAX` to indicate its last index if \p insert
/// `== false` or one past its last index if \p insert
/// `== true`.
/// \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 with an `AM_VAL_TYPE_VOID` item.
/// \pre \p doc `!= NULL`
/// \pre `0 <=` \p pos `<= AMobjSize(`\p obj_id `)` or \p pos `== SIZE_MAX`
/// \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 void.
/// \pre \p doc `!= NULL`.
/// \pre `0 <=` \p index `<= AMobjSize(`\p obj_id`)` or \p index `== SIZE_MAX`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
@ -286,39 +279,38 @@ pub unsafe extern "C" fn AMlistPutBytes(
pub unsafe extern "C" fn AMlistPutCounter(
doc: *mut AMdoc,
obj_id: *const AMobjId,
pos: usize,
index: usize,
insert: bool,
value: i64,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let obj_id = to_obj_id!(obj_id);
let (pos, insert) = adjust!(pos, insert, doc.length(obj_id));
let (index, insert) = adjust!(index, insert, doc.length(obj_id));
let value = am::ScalarValue::Counter(value.into());
to_result(if insert {
doc.insert(obj_id, pos, value)
doc.insert(obj_id, index, value)
} else {
doc.put(obj_id, pos, value)
doc.put(obj_id, index, value)
})
}
/// \memberof AMdoc
/// \brief Puts a float value into an item within a list object.
/// \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,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] pos The position of an item within the list object identified by
/// \p obj_id or `SIZE_MAX` to indicate its last item if
/// \p insert `== false` or one past its last item if
/// \p insert `== true`.
/// \param[in] insert A flag for inserting a new item for \p value before
/// \p pos instead of putting \p value into the item at
/// \p pos.
/// \param[in] index An index in the list object identified by \p obj_id or
/// `SIZE_MAX` to indicate its last index if \p insert
/// `== false` or one past its last index if \p insert
/// `== true`.
/// \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 with an `AM_VAL_TYPE_VOID` item.
/// \pre \p doc `!= NULL`
/// \pre `0 <=` \p pos `<= AMobjSize(`\p obj_id `)` or \p pos `== SIZE_MAX`
/// \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 void.
/// \pre \p doc `!= NULL`.
/// \pre `0 <=` \p index `<= AMobjSize(`\p obj_id`)` or \p index `== SIZE_MAX`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
@ -328,38 +320,37 @@ pub unsafe extern "C" fn AMlistPutCounter(
pub unsafe extern "C" fn AMlistPutF64(
doc: *mut AMdoc,
obj_id: *const AMobjId,
pos: usize,
index: usize,
insert: bool,
value: f64,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let obj_id = to_obj_id!(obj_id);
let (pos, insert) = adjust!(pos, insert, doc.length(obj_id));
let (index, insert) = adjust!(index, insert, doc.length(obj_id));
to_result(if insert {
doc.insert(obj_id, pos, value)
doc.insert(obj_id, index, value)
} else {
doc.put(obj_id, pos, value)
doc.put(obj_id, index, value)
})
}
/// \memberof AMdoc
/// \brief Puts a signed integer value into an item within a list object.
/// \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,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] pos The position of an item within the list object identified by
/// \p obj_id or `SIZE_MAX` to indicate its last item if
/// \p insert `== false` or one past its last item if
/// \p insert `== true`.
/// \param[in] insert A flag for inserting a new item for \p value before
/// \p pos instead of putting \p value into the item at
/// \p pos.
/// \param[in] index An index in the list object identified by \p obj_id or
/// `SIZE_MAX` to indicate its last index if \p insert
/// `== false` or one past its last index if \p insert
/// `== true`.
/// \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 with an `AM_VAL_TYPE_VOID` item.
/// \pre \p doc `!= NULL`
/// \pre `0 <=` \p pos `<= AMobjSize(`\p obj_id `)` or \p pos `== SIZE_MAX`
/// \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 void.
/// \pre \p doc `!= NULL`.
/// \pre `0 <=` \p index `<= AMobjSize(`\p obj_id`)` or \p index `== SIZE_MAX`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
@ -369,37 +360,36 @@ pub unsafe extern "C" fn AMlistPutF64(
pub unsafe extern "C" fn AMlistPutInt(
doc: *mut AMdoc,
obj_id: *const AMobjId,
pos: usize,
index: usize,
insert: bool,
value: i64,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let obj_id = to_obj_id!(obj_id);
let (pos, insert) = adjust!(pos, insert, doc.length(obj_id));
let (index, insert) = adjust!(index, insert, doc.length(obj_id));
to_result(if insert {
doc.insert(obj_id, pos, value)
doc.insert(obj_id, index, value)
} else {
doc.put(obj_id, pos, value)
doc.put(obj_id, index, value)
})
}
/// \memberof AMdoc
/// \brief Puts a null value into an item within a list object.
/// \brief Puts null as the value at an index in a list object.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] pos The position of an item within the list object identified by
/// \p obj_id or `SIZE_MAX` to indicate its last item if
/// \p insert `== false` or one past its last item if
/// \p insert `== true`.
/// \param[in] insert A flag for inserting a new item for \p value before
/// \p pos instead of putting \p value into the item at
/// \p pos.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_VOID` item.
/// \pre \p doc `!= NULL`
/// \pre `0 <=` \p pos `<= AMobjSize(`\p obj_id `)` or \p pos `== SIZE_MAX`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \param[in] index An index in the list object identified by \p obj_id or
/// `SIZE_MAX` to indicate its last index if \p insert
/// `== false` or one past its last index if \p insert
/// `== true`.
/// \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 `!= NULL`.
/// \pre `0 <=` \p index `<= AMobjSize(`\p obj_id`)` or \p index `== SIZE_MAX`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
@ -409,37 +399,38 @@ pub unsafe extern "C" fn AMlistPutInt(
pub unsafe extern "C" fn AMlistPutNull(
doc: *mut AMdoc,
obj_id: *const AMobjId,
pos: usize,
index: usize,
insert: bool,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let obj_id = to_obj_id!(obj_id);
let (pos, insert) = adjust!(pos, insert, doc.length(obj_id));
let (index, insert) = adjust!(index, insert, doc.length(obj_id));
to_result(if insert {
doc.insert(obj_id, pos, ())
doc.insert(obj_id, index, ())
} else {
doc.put(obj_id, pos, ())
doc.put(obj_id, index, ())
})
}
/// \memberof AMdoc
/// \brief Puts an empty object value into an item within a list object.
/// \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,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] pos The position of an item within the list object identified by
/// \p obj_id or `SIZE_MAX` to indicate its last item if
/// \p insert `== false` or one past its last item if
/// \p insert `== true`.
/// \param[in] insert A flag for inserting a new item for \p value before
/// \p pos instead of putting \p value into the item at
/// \p pos.
/// \param[in] index An index in the list object identified by \p obj_id or
/// `SIZE_MAX` to indicate its last index if \p insert
/// `== false` or one past its last index if \p insert
/// `== true`.
/// \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 with an `AM_VAL_TYPE_OBJ_TYPE` item.
/// \pre \p doc `!= NULL`
/// \pre `0 <=` \p pos `<= AMobjSize(`\p obj_id `)` or \p pos `== SIZE_MAX`
/// \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
/// `AMobjId` struct.
/// \pre \p doc `!= NULL`.
/// \pre `0 <=` \p index `<= AMobjSize(`\p obj_id`)` or \p index `== SIZE_MAX`.
/// \pre \p obj_type != `AM_OBJ_TYPE_VOID`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
@ -449,85 +440,82 @@ pub unsafe extern "C" fn AMlistPutNull(
pub unsafe extern "C" fn AMlistPutObject(
doc: *mut AMdoc,
obj_id: *const AMobjId,
pos: usize,
index: usize,
insert: bool,
obj_type: AMobjType,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let obj_id = to_obj_id!(obj_id);
let (pos, insert) = adjust!(pos, insert, doc.length(obj_id));
let obj_type = to_obj_type!(obj_type);
let (index, insert) = adjust!(index, insert, doc.length(obj_id));
let object = to_obj_type!(obj_type);
to_result(if insert {
(doc.insert_object(obj_id, pos, obj_type), obj_type)
doc.insert_object(obj_id, index, object)
} else {
(doc.put_object(obj_id, pos, obj_type), obj_type)
doc.put_object(obj_id, index, object)
})
}
/// \memberof AMdoc
/// \brief Puts a UTF-8 string value into an item within a list object.
/// \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,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] pos The position of an item within the list object identified by
/// \p obj_id or `SIZE_MAX` to indicate its last item if
/// \p insert `== false` or one past its last item if
/// \p insert `== true`.
/// \param[in] insert A flag for inserting a new item for \p value before
/// \p pos instead of putting \p value into the item at
/// \p pos.
/// \param[in] index An index in the list object identified by \p obj_id or
/// `SIZE_MAX` to indicate its last index if \p insert
/// `== false` or one past its last index if \p insert
/// `== true`.
/// \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 view as an `AMbyteSpan` struct.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_VOID` item.
/// \pre \p doc `!= NULL`
/// \pre `0 <=` \p pos `<= AMobjSize(`\p obj_id `)` or \p pos `== SIZE_MAX`
/// \pre \p value.src `!= NULL`
/// \pre `0 <` \p value.count `<= sizeof(`\p value.src `)`
/// \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 void.
/// \pre \p doc `!= NULL`.
/// \pre `0 <=` \p index `<= AMobjSize(`\p obj_id`)` or \p index `== SIZE_MAX`.
/// \pre \p value `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// value.src must be a byte array of length >= value.count
/// 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,
pos: usize,
index: usize,
insert: bool,
value: AMbyteSpan,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let obj_id = to_obj_id!(obj_id);
let (pos, insert) = adjust!(pos, insert, doc.length(obj_id));
let (index, insert) = adjust!(index, insert, doc.length(obj_id));
let value = to_str!(value);
to_result(if insert {
doc.insert(obj_id, pos, value)
doc.insert(obj_id, index, value)
} else {
doc.put(obj_id, pos, value)
doc.put(obj_id, index, value)
})
}
/// \memberof AMdoc
/// \brief Puts a *nix timestamp (milliseconds) value into an item within a
/// \brief Puts a *nix timestamp (milliseconds) as the value at an index in a
/// list object.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] pos The position of an item within the list object identified by
/// \p obj_id or `SIZE_MAX` to indicate its last item if
/// \p insert `== false` or one past its last item if
/// \p insert `== true`.
/// \param[in] insert A flag for inserting a new item for \p value before
/// \p pos instead of putting \p value into the item at
/// \p pos.
/// \param[in] index An index in the list object identified by \p obj_id or
/// `SIZE_MAX` to indicate its last index if \p insert
/// `== false` or one past its last index if \p insert
/// `== true`.
/// \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 with an `AM_VAL_TYPE_VOID` item.
/// \pre \p doc `!= NULL`
/// \pre `0 <=` \p pos `<= AMobjSize(`\p obj_id `)` or \p pos `== SIZE_MAX`
/// \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 void.
/// \pre \p doc `!= NULL`.
/// \pre `0 <=` \p index `<= AMobjSize(`\p obj_id`)` or \p index `== SIZE_MAX`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
@ -537,39 +525,38 @@ pub unsafe extern "C" fn AMlistPutStr(
pub unsafe extern "C" fn AMlistPutTimestamp(
doc: *mut AMdoc,
obj_id: *const AMobjId,
pos: usize,
index: usize,
insert: bool,
value: i64,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let obj_id = to_obj_id!(obj_id);
let (pos, insert) = adjust!(pos, insert, doc.length(obj_id));
let (index, insert) = adjust!(index, insert, doc.length(obj_id));
let value = am::ScalarValue::Timestamp(value);
to_result(if insert {
doc.insert(obj_id, pos, value)
doc.insert(obj_id, index, value)
} else {
doc.put(obj_id, pos, value)
doc.put(obj_id, index, value)
})
}
/// \memberof AMdoc
/// \brief Puts an unsigned integer value into an item within a list object.
/// \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,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] pos The position of an item within the list object identified by
/// \p obj_id or `SIZE_MAX` to indicate its last item if
/// \p insert `== false` or one past its last item if
/// \p insert `== true`.
/// \param[in] insert A flag for inserting a new item for \p value before
/// \p pos instead of putting \p value into the item at
/// \p pos.
/// \param[in] index An index in the list object identified by \p obj_id or
/// `SIZE_MAX` to indicate its last index if \p insert
/// `== false` or one past its last index if \p insert
/// `== true`.
/// \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 with an `AM_VAL_TYPE_VOID` item.
/// \pre \p doc `!= NULL`
/// \pre `0 <=` \p pos `<= AMobjSize(`\p obj_id `)` or \p pos `== SIZE_MAX`
/// \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 void.
/// \pre \p doc `!= NULL`.
/// \pre `0 <=` \p index `<= AMobjSize(`\p obj_id`)` or \p index `== SIZE_MAX`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
@ -579,58 +566,56 @@ pub unsafe extern "C" fn AMlistPutTimestamp(
pub unsafe extern "C" fn AMlistPutUint(
doc: *mut AMdoc,
obj_id: *const AMobjId,
pos: usize,
index: usize,
insert: bool,
value: u64,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let obj_id = to_obj_id!(obj_id);
let (pos, insert) = adjust!(pos, insert, doc.length(obj_id));
let (index, insert) = adjust!(index, insert, doc.length(obj_id));
to_result(if insert {
doc.insert(obj_id, pos, value)
doc.insert(obj_id, index, value)
} else {
doc.put(obj_id, pos, value)
doc.put(obj_id, index, value)
})
}
/// \memberof AMdoc
/// \brief Gets the current or historical items in the list object within the
/// given range.
/// \brief Gets the current or historical indices and values of the list object
/// within the given range.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] begin The first pos in a range of indices.
/// \param[in] end At least one past the last pos in a range of indices.
/// \param[in] heads A pointer to an `AMitems` struct with `AM_VAL_TYPE_CHANGE_HASH`
/// items to select historical items or `NULL` to select
/// current items.
/// \return A pointer to an `AMresult` struct with an `AMitems` struct.
/// \pre \p doc `!= NULL`
/// \pre \p begin `<=` \p end `<= SIZE_MAX`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \param[in] begin The first index in a range of indices.
/// \param[in] end At least one past the last index in a range of indices.
/// \param[in] heads A pointer to an `AMchangeHashes` struct for historical
/// indices and values or `NULL` for current indices and
/// values.
/// \return A pointer to an `AMresult` struct containing an `AMlistItems`
/// struct.
/// \pre \p doc `!= NULL`.
/// \pre \p begin `<=` \p end `<= SIZE_MAX`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// heads must be a valid pointer to an AMitems or std::ptr::null()
/// heads must be a valid pointer to an AMchangeHashes or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMlistRange(
doc: *const AMdoc,
obj_id: *const AMobjId,
begin: usize,
end: usize,
heads: *const AMitems,
heads: *const AMchangeHashes,
) -> *mut AMresult {
let doc = to_doc!(doc);
let obj_id = to_obj_id!(obj_id);
let range = to_range!(begin, end);
match heads.as_ref() {
None => to_result(doc.list_range(obj_id, range)),
Some(heads) => match <Vec<am::ChangeHash>>::try_from(heads) {
Ok(heads) => to_result(doc.list_range_at(obj_id, range, &heads)),
Err(e) => AMresult::error(&e.to_string()).into(),
},
Some(heads) => to_result(doc.list_range_at(obj_id, range, heads.as_ref())),
}
}

View file

@ -0,0 +1,97 @@
use automerge as am;
use crate::obj::AMobjId;
use crate::result::AMvalue;
/// \struct AMlistItem
/// \installed_headerfile
/// \brief An item in a list object.
pub struct AMlistItem {
/// The index of an item in a list object.
index: usize,
/// The object identifier of an item in a list object.
obj_id: AMobjId,
/// The value of an item in a list object.
value: am::Value<'static>,
}
impl AMlistItem {
pub fn new(index: usize, value: am::Value<'static>, obj_id: am::ObjId) -> Self {
Self {
index,
obj_id: AMobjId::new(obj_id),
value,
}
}
}
impl PartialEq for AMlistItem {
fn eq(&self, other: &Self) -> bool {
self.index == other.index && self.obj_id == other.obj_id && self.value == other.value
}
}
/*
impl From<&AMlistItem> for (usize, am::Value<'static>, am::ObjId) {
fn from(list_item: &AMlistItem) -> Self {
(list_item.index, list_item.value.0.clone(), list_item.obj_id.as_ref().clone())
}
}
*/
/// \memberof AMlistItem
/// \brief Gets the index of an item in a list object.
///
/// \param[in] list_item A pointer to an `AMlistItem` struct.
/// \return A 64-bit unsigned integer.
/// \pre \p list_item `!= NULL`.
/// \internal
///
/// # Safety
/// list_item must be a valid pointer to an AMlistItem
#[no_mangle]
pub unsafe extern "C" fn AMlistItemIndex(list_item: *const AMlistItem) -> usize {
if let Some(list_item) = list_item.as_ref() {
list_item.index
} else {
usize::MAX
}
}
/// \memberof AMlistItem
/// \brief Gets the object identifier of an item in a list object.
///
/// \param[in] list_item A pointer to an `AMlistItem` struct.
/// \return A pointer to an `AMobjId` struct.
/// \pre \p list_item `!= NULL`.
/// \internal
///
/// # Safety
/// list_item must be a valid pointer to an AMlistItem
#[no_mangle]
pub unsafe extern "C" fn AMlistItemObjId(list_item: *const AMlistItem) -> *const AMobjId {
if let Some(list_item) = list_item.as_ref() {
&list_item.obj_id
} else {
std::ptr::null()
}
}
/// \memberof AMlistItem
/// \brief Gets the value of an item in a list object.
///
/// \param[in] list_item A pointer to an `AMlistItem` struct.
/// \return An `AMvalue` struct.
/// \pre \p list_item `!= NULL`.
/// \internal
///
/// # Safety
/// list_item must be a valid pointer to an AMlistItem
#[no_mangle]
pub unsafe extern "C" fn AMlistItemValue<'a>(list_item: *const AMlistItem) -> AMvalue<'a> {
if let Some(list_item) = list_item.as_ref() {
(&list_item.value).into()
} else {
AMvalue::Void
}
}

View file

@ -0,0 +1,348 @@
use std::ffi::c_void;
use std::mem::size_of;
use crate::doc::list::item::AMlistItem;
#[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(list_items: &[AMlistItem], offset: isize) -> Self {
Self {
len: list_items.len(),
offset,
ptr: list_items.as_ptr() as *const c_void,
}
}
pub fn advance(&mut self, n: isize) {
if n == 0 {
return;
}
let len = self.len as isize;
self.offset = if self.offset < 0 {
// It's reversed.
let unclipped = self.offset.checked_sub(n).unwrap_or(isize::MIN);
if unclipped >= 0 {
// Clip it to the forward stop.
len
} else {
std::cmp::min(std::cmp::max(-(len + 1), unclipped), -1)
}
} else {
let unclipped = self.offset.checked_add(n).unwrap_or(isize::MAX);
if unclipped < 0 {
// Clip it to the reverse stop.
-(len + 1)
} else {
std::cmp::max(0, std::cmp::min(unclipped, 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<&AMlistItem> {
if self.is_stopped() {
return None;
}
let slice: &[AMlistItem] =
unsafe { std::slice::from_raw_parts(self.ptr as *const AMlistItem, 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<&AMlistItem> {
self.advance(-n);
if self.is_stopped() {
return None;
}
let slice: &[AMlistItem] =
unsafe { std::slice::from_raw_parts(self.ptr as *const AMlistItem, self.len) };
Some(&slice[self.get_index()])
}
pub fn reversed(&self) -> Self {
Self {
len: self.len,
offset: -(self.offset + 1),
ptr: self.ptr,
}
}
pub fn rewound(&self) -> Self {
Self {
len: self.len,
offset: if self.offset < 0 { -1 } else { 0 },
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 AMlistItems
/// \installed_headerfile
/// \brief A random-access iterator over a sequence of list object items.
#[repr(C)]
#[derive(Eq, PartialEq)]
pub struct AMlistItems {
/// An implementation detail that is intentionally opaque.
/// \warning Modifying \p detail will cause undefined behavior.
/// \note The actual size of \p detail will vary by platform, this is just
/// the one for the platform this documentation was built on.
detail: [u8; USIZE_USIZE_USIZE_],
}
impl AMlistItems {
pub fn new(list_items: &[AMlistItem]) -> Self {
Self {
detail: Detail::new(list_items, 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<&AMlistItem> {
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
detail.next(n)
}
pub fn prev(&mut self, n: isize) -> Option<&AMlistItem> {
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(),
}
}
pub fn rewound(&self) -> Self {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
Self {
detail: detail.rewound().into(),
}
}
}
impl AsRef<[AMlistItem]> for AMlistItems {
fn as_ref(&self) -> &[AMlistItem] {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
unsafe { std::slice::from_raw_parts(detail.ptr as *const AMlistItem, detail.len) }
}
}
impl Default for AMlistItems {
fn default() -> Self {
Self {
detail: [0; USIZE_USIZE_USIZE_],
}
}
}
/// \memberof AMlistItems
/// \brief Advances an iterator over a sequence of list object items by at most
/// \p |n| positions where the sign of \p n is relative to the
/// iterator's direction.
///
/// \param[in,out] list_items A pointer to an `AMlistItems` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \pre \p list_items `!= NULL`.
/// \internal
///
/// #Safety
/// list_items must be a valid pointer to an AMlistItems
#[no_mangle]
pub unsafe extern "C" fn AMlistItemsAdvance(list_items: *mut AMlistItems, n: isize) {
if let Some(list_items) = list_items.as_mut() {
list_items.advance(n);
};
}
/// \memberof AMlistItems
/// \brief Tests the equality of two sequences of list object items underlying
/// a pair of iterators.
///
/// \param[in] list_items1 A pointer to an `AMlistItems` struct.
/// \param[in] list_items2 A pointer to an `AMlistItems` struct.
/// \return `true` if \p list_items1 `==` \p list_items2 and `false` otherwise.
/// \pre \p list_items1 `!= NULL`.
/// \pre \p list_items2 `!= NULL`.
/// \internal
///
/// #Safety
/// list_items1 must be a valid pointer to an AMlistItems
/// list_items2 must be a valid pointer to an AMlistItems
#[no_mangle]
pub unsafe extern "C" fn AMlistItemsEqual(
list_items1: *const AMlistItems,
list_items2: *const AMlistItems,
) -> bool {
match (list_items1.as_ref(), list_items2.as_ref()) {
(Some(list_items1), Some(list_items2)) => list_items1.as_ref() == list_items2.as_ref(),
(None, Some(_)) | (Some(_), None) | (None, None) => false,
}
}
/// \memberof AMlistItems
/// \brief Gets the list object item at the current position of an iterator
/// over a sequence of list object items 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,out] list_items A pointer to an `AMlistItems` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \return A pointer to an `AMlistItem` struct that's `NULL` when
/// \p list_items was previously advanced past its forward/reverse
/// limit.
/// \pre \p list_items `!= NULL`.
/// \internal
///
/// #Safety
/// list_items must be a valid pointer to an AMlistItems
#[no_mangle]
pub unsafe extern "C" fn AMlistItemsNext(
list_items: *mut AMlistItems,
n: isize,
) -> *const AMlistItem {
if let Some(list_items) = list_items.as_mut() {
if let Some(list_item) = list_items.next(n) {
return list_item;
}
}
std::ptr::null()
}
/// \memberof AMlistItems
/// \brief Advances an iterator over a sequence of list object items by at most
/// \p |n| positions where the sign of \p n is relative to the
/// iterator's direction and then gets the list object item at its new
/// position.
///
/// \param[in,out] list_items A pointer to an `AMlistItems` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \return A pointer to an `AMlistItem` struct that's `NULL` when
/// \p list_items is presently advanced past its forward/reverse limit.
/// \pre \p list_items `!= NULL`.
/// \internal
///
/// #Safety
/// list_items must be a valid pointer to an AMlistItems
#[no_mangle]
pub unsafe extern "C" fn AMlistItemsPrev(
list_items: *mut AMlistItems,
n: isize,
) -> *const AMlistItem {
if let Some(list_items) = list_items.as_mut() {
if let Some(list_item) = list_items.prev(n) {
return list_item;
}
}
std::ptr::null()
}
/// \memberof AMlistItems
/// \brief Gets the size of the sequence of list object items underlying an
/// iterator.
///
/// \param[in] list_items A pointer to an `AMlistItems` struct.
/// \return The count of values in \p list_items.
/// \pre \p list_items `!= NULL`.
/// \internal
///
/// #Safety
/// list_items must be a valid pointer to an AMlistItems
#[no_mangle]
pub unsafe extern "C" fn AMlistItemsSize(list_items: *const AMlistItems) -> usize {
if let Some(list_items) = list_items.as_ref() {
list_items.len()
} else {
0
}
}
/// \memberof AMlistItems
/// \brief Creates an iterator over the same sequence of list object items as
/// the given one but with the opposite position and direction.
///
/// \param[in] list_items A pointer to an `AMlistItems` struct.
/// \return An `AMlistItems` struct
/// \pre \p list_items `!= NULL`.
/// \internal
///
/// #Safety
/// list_items must be a valid pointer to an AMlistItems
#[no_mangle]
pub unsafe extern "C" fn AMlistItemsReversed(list_items: *const AMlistItems) -> AMlistItems {
if let Some(list_items) = list_items.as_ref() {
list_items.reversed()
} else {
Default::default()
}
}
/// \memberof AMlistItems
/// \brief Creates an iterator at the starting position over the same sequence
/// of list object items as the given one.
///
/// \param[in] list_items A pointer to an `AMlistItems` struct.
/// \return An `AMlistItems` struct
/// \pre \p list_items `!= NULL`.
/// \internal
///
/// #Safety
/// list_items must be a valid pointer to an AMlistItems
#[no_mangle]
pub unsafe extern "C" fn AMlistItemsRewound(list_items: *const AMlistItems) -> AMlistItems {
if let Some(list_items) = list_items.as_ref() {
list_items.rewound()
} else {
Default::default()
}
}

View file

@ -3,29 +3,31 @@ use automerge::transaction::Transactable;
use automerge::ReadDoc;
use crate::byte_span::{to_str, AMbyteSpan};
use crate::doc::{to_doc, to_doc_mut, AMdoc};
use crate::items::AMitems;
use crate::obj::{to_obj_id, to_obj_type, AMobjId, AMobjType};
use crate::change_hashes::AMchangeHashes;
use crate::doc::{to_doc, to_doc_mut, to_obj_id, AMdoc};
use crate::obj::{to_obj_type, AMobjId, AMobjType};
use crate::result::{to_result, AMresult};
pub mod item;
pub mod items;
/// \memberof AMdoc
/// \brief Deletes an item from a map object.
/// \brief Deletes a key in a map object.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] key The UTF-8 string view key of an item within the map object
/// identified by \p obj_id as an `AMbyteSpan` struct.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_VOID` item.
/// \pre \p doc `!= NULL`
/// \pre \p key.src `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \param[in] key A UTF-8 string view key for the map object identified by
/// \p obj_id as an `AMbyteSpan` struct.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc `!= NULL`.
/// \pre \p key `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// key.src must be a byte array of length >= key.count
#[no_mangle]
pub unsafe extern "C" fn AMmapDelete(
doc: *mut AMdoc,
@ -38,107 +40,96 @@ pub unsafe extern "C" fn AMmapDelete(
}
/// \memberof AMdoc
/// \brief Gets a current or historical item within a map object.
/// \brief Gets the current or historical 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 `AM_ROOT`.
/// \param[in] key The UTF-8 string view key of an item within the map object
/// identified by \p obj_id as an `AMbyteSpan` struct.
/// \param[in] heads A pointer to an `AMitems` struct with `AM_VAL_TYPE_CHANGE_HASH`
/// items to select a historical item at \p key or `NULL`
/// to select the current item at \p key.
/// \return A pointer to an `AMresult` struct with an `AMitem` struct.
/// \pre \p doc `!= NULL`
/// \pre \p key.src `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \param[in] key A UTF-8 string view key for the map object identified by
/// \p obj_id as an `AMbyteSpan` struct.
/// \param[in] heads A pointer to an `AMchangeHashes` struct for a historical
/// value or `NULL` for the current value.
/// \return A pointer to an `AMresult` struct that doesn't contain a void.
/// \pre \p doc `!= NULL`.
/// \pre \p key `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// key.src must be a byte array of length >= key.count
/// heads must be a valid pointer to an AMitems or std::ptr::null()
/// heads must be a valid pointer to an AMchangeHashes or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMmapGet(
doc: *const AMdoc,
obj_id: *const AMobjId,
key: AMbyteSpan,
heads: *const AMitems,
heads: *const AMchangeHashes,
) -> *mut AMresult {
let doc = to_doc!(doc);
let obj_id = to_obj_id!(obj_id);
let key = to_str!(key);
match heads.as_ref() {
None => to_result(doc.get(obj_id, key)),
Some(heads) => match <Vec<am::ChangeHash>>::try_from(heads) {
Ok(heads) => to_result(doc.get_at(obj_id, key, &heads)),
Err(e) => AMresult::error(&e.to_string()).into(),
},
Some(heads) => to_result(doc.get_at(obj_id, key, heads.as_ref())),
}
}
/// \memberof AMdoc
/// \brief Gets all of the historical items at a key within a map object until
/// \brief Gets all of the historical values for a key in a map object until
/// its current one or a specific one.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] key The UTF-8 string view key of an item within the map object
/// identified by \p obj_id as an `AMbyteSpan` struct.
/// \param[in] heads A pointer to an `AMitems` struct with `AM_VAL_TYPE_CHANGE_HASH`
/// items to select a historical last item or `NULL` to
/// select the current last item.
/// \return A pointer to an `AMresult` struct with an `AMItems` struct.
/// \pre \p doc `!= NULL`
/// \pre \p key.src `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \param[in] key A UTF-8 string view key for the map object identified by
/// \p obj_id as an `AMbyteSpan` struct.
/// \param[in] heads A pointer to an `AMchangeHashes` struct for a historical
/// last value or `NULL` for the current last value.
/// \return A pointer to an `AMresult` struct containing an `AMobjItems` struct.
/// \pre \p doc `!= NULL`.
/// \pre \p key `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// key.src must be a byte array of length >= key.count
/// heads must be a valid pointer to an AMitems or std::ptr::null()
/// heads must be a valid pointer to an AMchangeHashes or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMmapGetAll(
doc: *const AMdoc,
obj_id: *const AMobjId,
key: AMbyteSpan,
heads: *const AMitems,
heads: *const AMchangeHashes,
) -> *mut AMresult {
let doc = to_doc!(doc);
let obj_id = to_obj_id!(obj_id);
let key = to_str!(key);
match heads.as_ref() {
None => to_result(doc.get_all(obj_id, key)),
Some(heads) => match <Vec<am::ChangeHash>>::try_from(heads) {
Ok(heads) => to_result(doc.get_all_at(obj_id, key, &heads)),
Err(e) => AMresult::error(&e.to_string()).into(),
},
Some(heads) => to_result(doc.get_all_at(obj_id, key, heads.as_ref())),
}
}
/// \memberof AMdoc
/// \brief Increments a counter at a key in a map object by the given value.
/// \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,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] key The UTF-8 string view key of an item within the map object
/// identified by \p obj_id as an `AMbyteSpan` struct.
/// \param[in] key A UTF-8 string view key for the map object identified by
/// \p obj_id as an `AMbyteSpan` struct.
/// \param[in] value A 64-bit signed integer.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_VOID` item.
/// \pre \p doc `!= NULL`
/// \pre \p key.src `!= 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 a void.
/// \pre \p doc `!= NULL`.
/// \pre \p key `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// key.src must be a byte array of length >= key.count
#[no_mangle]
pub unsafe extern "C" fn AMmapIncrement(
doc: *mut AMdoc,
@ -154,22 +145,21 @@ pub unsafe extern "C" fn AMmapIncrement(
/// \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,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] key The UTF-8 string view key of an item within the map object
/// identified by \p obj_id as an `AMbyteSpan` struct.
/// \param[in] key A UTF-8 string view key for the map object identified by
/// \p obj_id as an `AMbyteSpan` struct.
/// \param[in] value A boolean.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_VOID` item.
/// \pre \p doc `!= NULL`
/// \pre \p key.src `!= 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 a void.
/// \pre \p doc `!= NULL`.
/// \pre \p key `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// key.src must be a byte array of length >= key.count
#[no_mangle]
pub unsafe extern "C" fn AMmapPutBool(
doc: *mut AMdoc,
@ -183,58 +173,59 @@ pub unsafe extern "C" fn AMmapPutBool(
}
/// \memberof AMdoc
/// \brief Puts an array of bytes value at a key in a map object.
/// \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,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] key The UTF-8 string view key of an item within the map object
/// identified by \p obj_id as an `AMbyteSpan` struct.
/// \param[in] value A view onto an array of bytes as an `AMbyteSpan` struct.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_VOID` item.
/// \pre \p doc `!= NULL`
/// \pre \p key.src `!= NULL`
/// \pre \p value.src `!= NULL`
/// \pre `0 <` \p value.count `<= sizeof(`\p value.src `)`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \param[in] key A UTF-8 string view key for the map object identified by
/// \p obj_id as an `AMbyteSpan` struct.
/// \param[in] src A pointer to an array of bytes.
/// \param[in] count The number of bytes to copy from \p src.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc `!= NULL`.
/// \pre \p key `!= NULL`.
/// \pre \p src `!= NULL`.
/// \pre `0 <` \p count `<= sizeof(`\p src`)`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// key.src must be a byte array of length >= key.count
/// value.src must be a byte array of length >= value.count
/// src must be a byte array of size `>= count`
#[no_mangle]
pub unsafe extern "C" fn AMmapPutBytes(
doc: *mut AMdoc,
obj_id: *const AMobjId,
key: AMbyteSpan,
value: AMbyteSpan,
val: AMbyteSpan,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let key = to_str!(key);
to_result(doc.put(to_obj_id!(obj_id), key, Vec::<u8>::from(&value)))
let mut vec = Vec::new();
vec.extend_from_slice(std::slice::from_raw_parts(val.src, val.count));
to_result(doc.put(to_obj_id!(obj_id), 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,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] key A UTF-8 string view key for the map object identified by
/// \p obj_id as an `AMbyteSpan` struct.
/// \param[in] value A 64-bit signed integer.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_VOID` item.
/// \pre \p doc `!= NULL`
/// \pre \p key.src `!= 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 a void.
/// \pre \p doc `!= NULL`.
/// \pre \p key `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// key.src must be a byte array of length >= key.count
#[no_mangle]
pub unsafe extern "C" fn AMmapPutCounter(
doc: *mut AMdoc,
@ -254,21 +245,20 @@ pub unsafe extern "C" fn AMmapPutCounter(
/// \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,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] key A UTF-8 string view key for the map object identified by
/// \p obj_id as an `AMbyteSpan` struct.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_VOID` item.
/// \pre \p doc `!= NULL`
/// \pre \p key.src `!= 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 a void.
/// \pre \p doc `!= NULL`.
/// \pre \p key `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// key.src must be a byte array of length >= key.count
#[no_mangle]
pub unsafe extern "C" fn AMmapPutNull(
doc: *mut AMdoc,
@ -283,22 +273,23 @@ pub unsafe extern "C" fn AMmapPutNull(
/// \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,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] key A UTF-8 string view key for the map object identified by
/// \p obj_id as an `AMbyteSpan` struct.
/// \param[in] obj_type An `AMobjIdType` enum tag.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_OBJ_TYPE` item.
/// \pre \p doc `!= NULL`
/// \pre \p key.src `!= 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 a pointer to an
/// `AMobjId` struct.
/// \pre \p doc `!= NULL`.
/// \pre \p key `!= NULL`.
/// \pre \p obj_type != `AM_OBJ_TYPE_VOID`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// key.src must be a byte array of length >= key.count
#[no_mangle]
pub unsafe extern "C" fn AMmapPutObject(
doc: *mut AMdoc,
@ -308,29 +299,27 @@ pub unsafe extern "C" fn AMmapPutObject(
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let key = to_str!(key);
let obj_type = to_obj_type!(obj_type);
to_result((doc.put_object(to_obj_id!(obj_id), key, obj_type), obj_type))
to_result(doc.put_object(to_obj_id!(obj_id), key, to_obj_type!(obj_type)))
}
/// \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,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] key A UTF-8 string view key for the map object identified by
/// \p obj_id as an `AMbyteSpan` struct.
/// \param[in] value A 64-bit float.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_VOID` item.
/// \pre \p doc `!= NULL`
/// \pre \p key.src `!= 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 a void.
/// \pre \p doc `!= NULL`.
/// \pre \p key `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// key.src must be a byte array of length >= key.count
#[no_mangle]
pub unsafe extern "C" fn AMmapPutF64(
doc: *mut AMdoc,
@ -346,22 +335,21 @@ pub unsafe extern "C" fn AMmapPutF64(
/// \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,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] key A UTF-8 string view key for the map object identified by
/// \p obj_id as an `AMbyteSpan` struct.
/// \param[in] value A 64-bit signed integer.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_VOID` item.
/// \pre \p doc `!= NULL`
/// \pre \p key.src `!= 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 a void.
/// \pre \p doc `!= NULL`.
/// \pre \p key `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// key.src must be a byte array of length >= key.count
#[no_mangle]
pub unsafe extern "C" fn AMmapPutInt(
doc: *mut AMdoc,
@ -377,22 +365,21 @@ pub unsafe extern "C" fn AMmapPutInt(
/// \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,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] key A UTF-8 string view key for the map object identified by
/// \p obj_id as an `AMbyteSpan` struct.
/// \param[in] value A UTF-8 string view as an `AMbyteSpan` struct.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_VOID` item.
/// \pre \p doc `!= NULL`
/// \pre \p key.src `!= 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 a void.
/// \pre \p doc `!= NULL`.
/// \pre \p key `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// key.src must be a byte array of length >= key.count
#[no_mangle]
pub unsafe extern "C" fn AMmapPutStr(
doc: *mut AMdoc,
@ -408,22 +395,21 @@ pub unsafe extern "C" fn AMmapPutStr(
/// \brief Puts a *nix timestamp (milliseconds) as the value of a key in a map
/// object.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] key A UTF-8 string view key for the map object identified by
/// \p obj_id as an `AMbyteSpan` struct.
/// \param[in] value A 64-bit signed integer.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_VOID` item.
/// \pre \p doc `!= NULL`
/// \pre \p key.src `!= 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 a void.
/// \pre \p doc `!= NULL`.
/// \pre \p key `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// key.src must be a byte array of length >= key.count
#[no_mangle]
pub unsafe extern "C" fn AMmapPutTimestamp(
doc: *mut AMdoc,
@ -439,22 +425,21 @@ pub unsafe extern "C" fn AMmapPutTimestamp(
/// \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,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] key A UTF-8 string view key for the map object identified by
/// \p obj_id as an `AMbyteSpan` struct.
/// \param[in] value A 64-bit unsigned integer.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_VOID` item.
/// \pre \p doc `!= NULL`
/// \pre \p key.src `!= 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 a void.
/// \pre \p doc `!= NULL`.
/// \pre \p key `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// key.src must be a byte array of length >= key.count
#[no_mangle]
pub unsafe extern "C" fn AMmapPutUint(
doc: *mut AMdoc,
@ -468,82 +453,71 @@ pub unsafe extern "C" fn AMmapPutUint(
}
/// \memberof AMdoc
/// \brief Gets the current or historical items of the map object within the
/// given range.
/// \brief Gets the current or historical keys and values of the map object
/// within the given range.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] begin The first key in a subrange or `AMstr(NULL)` to indicate the
/// absolute first key.
/// \param[in] end The key one past the last key in a subrange or `AMstr(NULL)`
/// to indicate one past the absolute last key.
/// \param[in] heads A pointer to an `AMitems` struct with `AM_VAL_TYPE_CHANGE_HASH`
/// items to select historical items or `NULL` to select
/// current items.
/// \return A pointer to an `AMresult` struct with an `AMitems` struct.
/// \pre \p doc `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \param[in] end The key one past the last key in a subrange or `AMstr(NULL)` to
/// indicate one past the absolute last key.
/// \param[in] heads A pointer to an `AMchangeHashes` struct for historical
/// keys and values or `NULL` for current keys and values.
/// \return A pointer to an `AMresult` struct containing an `AMmapItems`
/// struct.
/// \pre \p doc `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// begin.src must be a byte array of length >= begin.count or std::ptr::null()
/// end.src must be a byte array of length >= end.count or std::ptr::null()
/// heads must be a valid pointer to an AMitems or std::ptr::null()
/// heads must be a valid pointer to an AMchangeHashes or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMmapRange(
doc: *const AMdoc,
obj_id: *const AMobjId,
begin: AMbyteSpan,
end: AMbyteSpan,
heads: *const AMitems,
heads: *const AMchangeHashes,
) -> *mut AMresult {
let doc = to_doc!(doc);
let obj_id = to_obj_id!(obj_id);
let heads = match heads.as_ref() {
None => None,
Some(heads) => match <Vec<am::ChangeHash>>::try_from(heads) {
Ok(heads) => Some(heads),
Err(e) => {
return AMresult::error(&e.to_string()).into();
}
},
};
match (begin.is_null(), end.is_null()) {
(false, false) => {
let (begin, end) = (to_str!(begin).to_string(), to_str!(end).to_string());
if begin > end {
return AMresult::error(&format!("Invalid range [{}-{})", begin, end)).into();
return AMresult::err(&format!("Invalid range [{}-{})", begin, end)).into();
};
let bounds = begin..end;
if let Some(heads) = heads {
to_result(doc.map_range_at(obj_id, bounds, &heads))
if let Some(heads) = heads.as_ref() {
to_result(doc.map_range_at(obj_id, bounds, heads.as_ref()))
} else {
to_result(doc.map_range(obj_id, bounds))
}
}
(false, true) => {
let bounds = to_str!(begin).to_string()..;
if let Some(heads) = heads {
to_result(doc.map_range_at(obj_id, bounds, &heads))
if let Some(heads) = heads.as_ref() {
to_result(doc.map_range_at(obj_id, bounds, heads.as_ref()))
} else {
to_result(doc.map_range(obj_id, bounds))
}
}
(true, false) => {
let bounds = ..to_str!(end).to_string();
if let Some(heads) = heads {
to_result(doc.map_range_at(obj_id, bounds, &heads))
if let Some(heads) = heads.as_ref() {
to_result(doc.map_range_at(obj_id, bounds, heads.as_ref()))
} else {
to_result(doc.map_range(obj_id, bounds))
}
}
(true, true) => {
let bounds = ..;
if let Some(heads) = heads {
to_result(doc.map_range_at(obj_id, bounds, &heads))
if let Some(heads) = heads.as_ref() {
to_result(doc.map_range_at(obj_id, bounds, heads.as_ref()))
} else {
to_result(doc.map_range(obj_id, bounds))
}

View file

@ -0,0 +1,98 @@
use automerge as am;
use crate::byte_span::AMbyteSpan;
use crate::obj::AMobjId;
use crate::result::AMvalue;
/// \struct AMmapItem
/// \installed_headerfile
/// \brief An item in a map object.
pub struct AMmapItem {
/// The key of an item in a map object.
key: String,
/// The object identifier of an item in a map object.
obj_id: AMobjId,
/// The value of an item in a map object.
value: am::Value<'static>,
}
impl AMmapItem {
pub fn new(key: &'static str, value: am::Value<'static>, obj_id: am::ObjId) -> Self {
Self {
key: key.to_string(),
obj_id: AMobjId::new(obj_id),
value,
}
}
}
impl PartialEq for AMmapItem {
fn eq(&self, other: &Self) -> bool {
self.key == other.key && self.obj_id == other.obj_id && self.value == other.value
}
}
/*
impl From<&AMmapItem> for (String, am::Value<'static>, am::ObjId) {
fn from(map_item: &AMmapItem) -> Self {
(map_item.key.into_string().unwrap(), map_item.value.0.clone(), map_item.obj_id.as_ref().clone())
}
}
*/
/// \memberof AMmapItem
/// \brief Gets the key of an item in a map object.
///
/// \param[in] map_item A pointer to an `AMmapItem` struct.
/// \return An `AMbyteSpan` view of a UTF-8 string.
/// \pre \p map_item `!= NULL`.
/// \internal
///
/// # Safety
/// map_item must be a valid pointer to an AMmapItem
#[no_mangle]
pub unsafe extern "C" fn AMmapItemKey(map_item: *const AMmapItem) -> AMbyteSpan {
if let Some(map_item) = map_item.as_ref() {
map_item.key.as_bytes().into()
} else {
Default::default()
}
}
/// \memberof AMmapItem
/// \brief Gets the object identifier of an item in a map object.
///
/// \param[in] map_item A pointer to an `AMmapItem` struct.
/// \return A pointer to an `AMobjId` struct.
/// \pre \p map_item `!= NULL`.
/// \internal
///
/// # Safety
/// map_item must be a valid pointer to an AMmapItem
#[no_mangle]
pub unsafe extern "C" fn AMmapItemObjId(map_item: *const AMmapItem) -> *const AMobjId {
if let Some(map_item) = map_item.as_ref() {
&map_item.obj_id
} else {
std::ptr::null()
}
}
/// \memberof AMmapItem
/// \brief Gets the value of an item in a map object.
///
/// \param[in] map_item A pointer to an `AMmapItem` struct.
/// \return An `AMvalue` struct.
/// \pre \p map_item `!= NULL`.
/// \internal
///
/// # Safety
/// map_item must be a valid pointer to an AMmapItem
#[no_mangle]
pub unsafe extern "C" fn AMmapItemValue<'a>(map_item: *const AMmapItem) -> AMvalue<'a> {
if let Some(map_item) = map_item.as_ref() {
(&map_item.value).into()
} else {
AMvalue::Void
}
}

View file

@ -0,0 +1,340 @@
use std::ffi::c_void;
use std::mem::size_of;
use crate::doc::map::item::AMmapItem;
#[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(map_items: &[AMmapItem], offset: isize) -> Self {
Self {
len: map_items.len(),
offset,
ptr: map_items.as_ptr() as *const c_void,
}
}
pub fn advance(&mut self, n: isize) {
if n == 0 {
return;
}
let len = self.len as isize;
self.offset = if self.offset < 0 {
// It's reversed.
let unclipped = self.offset.checked_sub(n).unwrap_or(isize::MIN);
if unclipped >= 0 {
// Clip it to the forward stop.
len
} else {
std::cmp::min(std::cmp::max(-(len + 1), unclipped), -1)
}
} else {
let unclipped = self.offset.checked_add(n).unwrap_or(isize::MAX);
if unclipped < 0 {
// Clip it to the reverse stop.
-(len + 1)
} else {
std::cmp::max(0, std::cmp::min(unclipped, 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<&AMmapItem> {
if self.is_stopped() {
return None;
}
let slice: &[AMmapItem] =
unsafe { std::slice::from_raw_parts(self.ptr as *const AMmapItem, 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<&AMmapItem> {
self.advance(-n);
if self.is_stopped() {
return None;
}
let slice: &[AMmapItem] =
unsafe { std::slice::from_raw_parts(self.ptr as *const AMmapItem, self.len) };
Some(&slice[self.get_index()])
}
pub fn reversed(&self) -> Self {
Self {
len: self.len,
offset: -(self.offset + 1),
ptr: self.ptr,
}
}
pub fn rewound(&self) -> Self {
Self {
len: self.len,
offset: if self.offset < 0 { -1 } else { 0 },
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 AMmapItems
/// \installed_headerfile
/// \brief A random-access iterator over a sequence of map object items.
#[repr(C)]
#[derive(Eq, PartialEq)]
pub struct AMmapItems {
/// An implementation detail that is intentionally opaque.
/// \warning Modifying \p detail will cause undefined behavior.
/// \note The actual size of \p detail will vary by platform, this is just
/// the one for the platform this documentation was built on.
detail: [u8; USIZE_USIZE_USIZE_],
}
impl AMmapItems {
pub fn new(map_items: &[AMmapItem]) -> Self {
Self {
detail: Detail::new(map_items, 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<&AMmapItem> {
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
detail.next(n)
}
pub fn prev(&mut self, n: isize) -> Option<&AMmapItem> {
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(),
}
}
pub fn rewound(&self) -> Self {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
Self {
detail: detail.rewound().into(),
}
}
}
impl AsRef<[AMmapItem]> for AMmapItems {
fn as_ref(&self) -> &[AMmapItem] {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
unsafe { std::slice::from_raw_parts(detail.ptr as *const AMmapItem, detail.len) }
}
}
impl Default for AMmapItems {
fn default() -> Self {
Self {
detail: [0; USIZE_USIZE_USIZE_],
}
}
}
/// \memberof AMmapItems
/// \brief Advances an iterator over a sequence of map object items by at most
/// \p |n| positions where the sign of \p n is relative to the
/// iterator's direction.
///
/// \param[in,out] map_items A pointer to an `AMmapItems` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \pre \p map_items `!= NULL`.
/// \internal
///
/// #Safety
/// map_items must be a valid pointer to an AMmapItems
#[no_mangle]
pub unsafe extern "C" fn AMmapItemsAdvance(map_items: *mut AMmapItems, n: isize) {
if let Some(map_items) = map_items.as_mut() {
map_items.advance(n);
};
}
/// \memberof AMmapItems
/// \brief Tests the equality of two sequences of map object items underlying
/// a pair of iterators.
///
/// \param[in] map_items1 A pointer to an `AMmapItems` struct.
/// \param[in] map_items2 A pointer to an `AMmapItems` struct.
/// \return `true` if \p map_items1 `==` \p map_items2 and `false` otherwise.
/// \pre \p map_items1 `!= NULL`.
/// \pre \p map_items2 `!= NULL`.
/// \internal
///
/// #Safety
/// map_items1 must be a valid pointer to an AMmapItems
/// map_items2 must be a valid pointer to an AMmapItems
#[no_mangle]
pub unsafe extern "C" fn AMmapItemsEqual(
map_items1: *const AMmapItems,
map_items2: *const AMmapItems,
) -> bool {
match (map_items1.as_ref(), map_items2.as_ref()) {
(Some(map_items1), Some(map_items2)) => map_items1.as_ref() == map_items2.as_ref(),
(None, Some(_)) | (Some(_), None) | (None, None) => false,
}
}
/// \memberof AMmapItems
/// \brief Gets the map object item at the current position of an iterator
/// over a sequence of map object items 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,out] map_items A pointer to an `AMmapItems` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \return A pointer to an `AMmapItem` struct that's `NULL` when \p map_items
/// was previously advanced past its forward/reverse limit.
/// \pre \p map_items `!= NULL`.
/// \internal
///
/// #Safety
/// map_items must be a valid pointer to an AMmapItems
#[no_mangle]
pub unsafe extern "C" fn AMmapItemsNext(map_items: *mut AMmapItems, n: isize) -> *const AMmapItem {
if let Some(map_items) = map_items.as_mut() {
if let Some(map_item) = map_items.next(n) {
return map_item;
}
}
std::ptr::null()
}
/// \memberof AMmapItems
/// \brief Advances an iterator over a sequence of map object items by at most
/// \p |n| positions where the sign of \p n is relative to the
/// iterator's direction and then gets the map object item at its new
/// position.
///
/// \param[in,out] map_items A pointer to an `AMmapItems` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \return A pointer to an `AMmapItem` struct that's `NULL` when \p map_items
/// is presently advanced past its forward/reverse limit.
/// \pre \p map_items `!= NULL`.
/// \internal
///
/// #Safety
/// map_items must be a valid pointer to an AMmapItems
#[no_mangle]
pub unsafe extern "C" fn AMmapItemsPrev(map_items: *mut AMmapItems, n: isize) -> *const AMmapItem {
if let Some(map_items) = map_items.as_mut() {
if let Some(map_item) = map_items.prev(n) {
return map_item;
}
}
std::ptr::null()
}
/// \memberof AMmapItems
/// \brief Gets the size of the sequence of map object items underlying an
/// iterator.
///
/// \param[in] map_items A pointer to an `AMmapItems` struct.
/// \return The count of values in \p map_items.
/// \pre \p map_items `!= NULL`.
/// \internal
///
/// #Safety
/// map_items must be a valid pointer to an AMmapItems
#[no_mangle]
pub unsafe extern "C" fn AMmapItemsSize(map_items: *const AMmapItems) -> usize {
if let Some(map_items) = map_items.as_ref() {
map_items.len()
} else {
0
}
}
/// \memberof AMmapItems
/// \brief Creates an iterator over the same sequence of map object items as
/// the given one but with the opposite position and direction.
///
/// \param[in] map_items A pointer to an `AMmapItems` struct.
/// \return An `AMmapItems` struct
/// \pre \p map_items `!= NULL`.
/// \internal
///
/// #Safety
/// map_items must be a valid pointer to an AMmapItems
#[no_mangle]
pub unsafe extern "C" fn AMmapItemsReversed(map_items: *const AMmapItems) -> AMmapItems {
if let Some(map_items) = map_items.as_ref() {
map_items.reversed()
} else {
Default::default()
}
}
/// \memberof AMmapItems
/// \brief Creates an iterator at the starting position over the same sequence of map object items as the given one.
///
/// \param[in] map_items A pointer to an `AMmapItems` struct.
/// \return An `AMmapItems` struct
/// \pre \p map_items `!= NULL`.
/// \internal
///
/// #Safety
/// map_items must be a valid pointer to an AMmapItems
#[no_mangle]
pub unsafe extern "C" fn AMmapItemsRewound(map_items: *const AMmapItems) -> AMmapItems {
if let Some(map_items) = map_items.as_ref() {
map_items.rewound()
} else {
Default::default()
}
}

View file

@ -1,20 +1,9 @@
macro_rules! clamp {
($index:expr, $len:expr, $param_name:expr) => {{
if $index > $len && $index != usize::MAX {
return AMresult::error(&format!("Invalid {} {}", $param_name, $index)).into();
}
std::cmp::min($index, $len)
}};
}
pub(crate) use clamp;
macro_rules! to_doc {
($handle:expr) => {{
let handle = $handle.as_ref();
match handle {
Some(b) => b,
None => return AMresult::error("Invalid `AMdoc*`").into(),
None => return AMresult::err("Invalid AMdoc pointer").into(),
}
}};
}
@ -26,21 +15,9 @@ macro_rules! to_doc_mut {
let handle = $handle.as_mut();
match handle {
Some(b) => b,
None => return AMresult::error("Invalid `AMdoc*`").into(),
None => return AMresult::err("Invalid AMdoc pointer").into(),
}
}};
}
pub(crate) use to_doc_mut;
macro_rules! to_items {
($handle:expr) => {{
let handle = $handle.as_ref();
match handle {
Some(b) => b,
None => return AMresult::error("Invalid `AMitems*`").into(),
}
}};
}
pub(crate) use to_items;

View file

@ -1,84 +0,0 @@
use automerge as am;
use std::any::type_name;
use smol_str::SmolStr;
use crate::byte_span::AMbyteSpan;
/// \struct AMindex
/// \installed_headerfile
/// \brief An item index.
#[derive(PartialEq)]
pub enum AMindex {
/// A UTF-8 string key variant.
Key(SmolStr),
/// A 64-bit unsigned integer position variant.
Pos(usize),
}
impl TryFrom<&AMindex> for AMbyteSpan {
type Error = am::AutomergeError;
fn try_from(item: &AMindex) -> Result<Self, Self::Error> {
use am::AutomergeError::InvalidValueType;
use AMindex::*;
if let Key(key) = item {
return Ok(key.into());
}
Err(InvalidValueType {
expected: type_name::<SmolStr>().to_string(),
unexpected: type_name::<usize>().to_string(),
})
}
}
impl TryFrom<&AMindex> for usize {
type Error = am::AutomergeError;
fn try_from(item: &AMindex) -> Result<Self, Self::Error> {
use am::AutomergeError::InvalidValueType;
use AMindex::*;
if let Pos(pos) = item {
return Ok(*pos);
}
Err(InvalidValueType {
expected: type_name::<usize>().to_string(),
unexpected: type_name::<SmolStr>().to_string(),
})
}
}
/// \ingroup enumerations
/// \enum AMidxType
/// \installed_headerfile
/// \brief The type of an item's index.
#[derive(PartialEq, Eq)]
#[repr(u8)]
pub enum AMidxType {
/// The default tag, not a type signifier.
Default = 0,
/// A UTF-8 string view key.
Key,
/// A 64-bit unsigned integer position.
Pos,
}
impl Default for AMidxType {
fn default() -> Self {
Self::Default
}
}
impl From<&AMindex> for AMidxType {
fn from(index: &AMindex) -> Self {
use AMindex::*;
match index {
Key(_) => Self::Key,
Pos(_) => Self::Pos,
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,401 +0,0 @@
use automerge as am;
use std::ffi::c_void;
use std::marker::PhantomData;
use std::mem::size_of;
use crate::item::AMitem;
use crate::result::AMresult;
#[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(items: &[AMitem], offset: isize) -> Self {
Self {
len: items.len(),
offset,
ptr: items.as_ptr() as *mut c_void,
}
}
pub fn advance(&mut self, n: isize) {
if n == 0 {
return;
}
let len = self.len as isize;
self.offset = if self.offset < 0 {
// It's reversed.
let unclipped = self.offset.checked_sub(n).unwrap_or(isize::MIN);
if unclipped >= 0 {
// Clip it to the forward stop.
len
} else {
std::cmp::min(std::cmp::max(-(len + 1), unclipped), -1)
}
} else {
let unclipped = self.offset.checked_add(n).unwrap_or(isize::MAX);
if unclipped < 0 {
// Clip it to the reverse stop.
-(len + 1)
} else {
std::cmp::max(0, std::cmp::min(unclipped, 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<&mut AMitem> {
if self.is_stopped() {
return None;
}
let slice: &mut [AMitem] =
unsafe { std::slice::from_raw_parts_mut(self.ptr as *mut AMitem, self.len) };
let value = &mut 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<&mut AMitem> {
self.advance(-n);
if self.is_stopped() {
return None;
}
let slice: &mut [AMitem] =
unsafe { std::slice::from_raw_parts_mut(self.ptr as *mut AMitem, self.len) };
Some(&mut slice[self.get_index()])
}
pub fn reversed(&self) -> Self {
Self {
len: self.len,
offset: -(self.offset + 1),
ptr: self.ptr,
}
}
pub fn rewound(&self) -> Self {
Self {
len: self.len,
offset: if self.offset < 0 { -1 } else { 0 },
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 AMitems
/// \installed_headerfile
/// \brief A random-access iterator over a sequence of `AMitem` structs.
#[repr(C)]
#[derive(Eq, PartialEq)]
pub struct AMitems<'a> {
/// An implementation detail that is intentionally opaque.
/// \warning Modifying \p detail will cause undefined behavior.
/// \note The actual size of \p detail will vary by platform, this is just
/// the one for the platform this documentation was built on.
detail: [u8; USIZE_USIZE_USIZE_],
phantom: PhantomData<&'a mut AMresult>,
}
impl<'a> AMitems<'a> {
pub fn new(items: &[AMitem]) -> Self {
Self {
detail: Detail::new(items, 0).into(),
phantom: PhantomData,
}
}
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<&mut AMitem> {
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
detail.next(n)
}
pub fn prev(&mut self, n: isize) -> Option<&mut AMitem> {
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(),
phantom: PhantomData,
}
}
pub fn rewound(&self) -> Self {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
Self {
detail: detail.rewound().into(),
phantom: PhantomData,
}
}
}
impl<'a> AsRef<[AMitem]> for AMitems<'a> {
fn as_ref(&self) -> &[AMitem] {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
unsafe { std::slice::from_raw_parts(detail.ptr as *const AMitem, detail.len) }
}
}
impl<'a> Default for AMitems<'a> {
fn default() -> Self {
Self {
detail: [0; USIZE_USIZE_USIZE_],
phantom: PhantomData,
}
}
}
impl TryFrom<&AMitems<'_>> for Vec<am::Change> {
type Error = am::AutomergeError;
fn try_from(items: &AMitems<'_>) -> Result<Self, Self::Error> {
let mut changes = Vec::<am::Change>::with_capacity(items.len());
for item in items.as_ref().iter() {
match <&am::Change>::try_from(item.as_ref()) {
Ok(change) => {
changes.push(change.clone());
}
Err(e) => {
return Err(e);
}
}
}
Ok(changes)
}
}
impl TryFrom<&AMitems<'_>> for Vec<am::ChangeHash> {
type Error = am::AutomergeError;
fn try_from(items: &AMitems<'_>) -> Result<Self, Self::Error> {
let mut change_hashes = Vec::<am::ChangeHash>::with_capacity(items.len());
for item in items.as_ref().iter() {
match <&am::ChangeHash>::try_from(item.as_ref()) {
Ok(change_hash) => {
change_hashes.push(*change_hash);
}
Err(e) => {
return Err(e);
}
}
}
Ok(change_hashes)
}
}
impl TryFrom<&AMitems<'_>> for Vec<am::ScalarValue> {
type Error = am::AutomergeError;
fn try_from(items: &AMitems<'_>) -> Result<Self, Self::Error> {
let mut scalars = Vec::<am::ScalarValue>::with_capacity(items.len());
for item in items.as_ref().iter() {
match <&am::ScalarValue>::try_from(item.as_ref()) {
Ok(scalar) => {
scalars.push(scalar.clone());
}
Err(e) => {
return Err(e);
}
}
}
Ok(scalars)
}
}
/// \memberof AMitems
/// \brief Advances an iterator over a sequence of object items by at most
/// \p |n| positions where the sign of \p n is relative to the
/// iterator's direction.
///
/// \param[in] items A pointer to an `AMitems` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \pre \p items `!= NULL`
/// \internal
///
/// #Safety
/// items must be a valid pointer to an AMitems
#[no_mangle]
pub unsafe extern "C" fn AMitemsAdvance(items: *mut AMitems, n: isize) {
if let Some(items) = items.as_mut() {
items.advance(n);
};
}
/// \memberof AMitems
/// \brief Tests the equality of two sequences of object items underlying a
/// pair of iterators.
///
/// \param[in] items1 A pointer to an `AMitems` struct.
/// \param[in] items2 A pointer to an `AMitems` struct.
/// \return `true` if \p items1 `==` \p items2 and `false` otherwise.
/// \pre \p items1 `!= NULL`
/// \pre \p items1 `!= NULL`
/// \post `!(`\p items1 `&&` \p items2 `) -> false`
/// \internal
///
/// #Safety
/// items1 must be a valid pointer to an AMitems
/// items2 must be a valid pointer to an AMitems
#[no_mangle]
pub unsafe extern "C" fn AMitemsEqual(items1: *const AMitems, items2: *const AMitems) -> bool {
match (items1.as_ref(), items2.as_ref()) {
(Some(items1), Some(items2)) => items1.as_ref() == items2.as_ref(),
(None, None) | (None, Some(_)) | (Some(_), None) => false,
}
}
/// \memberof AMitems
/// \brief Gets the object item at the current position of an iterator over a
/// sequence of object items 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] items A pointer to an `AMitems` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \return A pointer to an `AMitem` struct that's `NULL` when \p items
/// was previously advanced past its forward/reverse limit.
/// \pre \p items `!= NULL`
/// \internal
///
/// #Safety
/// items must be a valid pointer to an AMitems
#[no_mangle]
pub unsafe extern "C" fn AMitemsNext(items: *mut AMitems, n: isize) -> *mut AMitem {
if let Some(items) = items.as_mut() {
if let Some(item) = items.next(n) {
return item;
}
}
std::ptr::null_mut()
}
/// \memberof AMitems
/// \brief Advances an iterator over a sequence of object items by at most
/// \p |n| positions where the sign of \p n is relative to the
/// iterator's direction and then gets the object item at its new
/// position.
///
/// \param[in] items A pointer to an `AMitems` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \return A pointer to an `AMitem` struct that's `NULL` when \p items
/// is presently advanced past its forward/reverse limit.
/// \pre \p items `!= NULL`
/// \internal
///
/// #Safety
/// items must be a valid pointer to an AMitems
#[no_mangle]
pub unsafe extern "C" fn AMitemsPrev(items: *mut AMitems, n: isize) -> *mut AMitem {
if let Some(items) = items.as_mut() {
if let Some(obj_item) = items.prev(n) {
return obj_item;
}
}
std::ptr::null_mut()
}
/// \memberof AMitems
/// \brief Gets the size of the sequence underlying an iterator.
///
/// \param[in] items A pointer to an `AMitems` struct.
/// \return The count of items in \p items.
/// \pre \p items `!= NULL`
/// \internal
///
/// #Safety
/// items must be a valid pointer to an AMitems
#[no_mangle]
pub unsafe extern "C" fn AMitemsSize(items: *const AMitems) -> usize {
if let Some(items) = items.as_ref() {
return items.len();
}
0
}
/// \memberof AMitems
/// \brief Creates an iterator over the same sequence of items as the
/// given one but with the opposite position and direction.
///
/// \param[in] items A pointer to an `AMitems` struct.
/// \return An `AMitems` struct
/// \pre \p items `!= NULL`
/// \internal
///
/// #Safety
/// items must be a valid pointer to an AMitems
#[no_mangle]
pub unsafe extern "C" fn AMitemsReversed(items: *const AMitems) -> AMitems {
if let Some(items) = items.as_ref() {
return items.reversed();
}
Default::default()
}
/// \memberof AMitems
/// \brief Creates an iterator at the starting position over the same sequence
/// of items as the given one.
///
/// \param[in] items A pointer to an `AMitems` struct.
/// \return An `AMitems` struct
/// \pre \p items `!= NULL`
/// \internal
///
/// #Safety
/// items must be a valid pointer to an AMitems
#[no_mangle]
pub unsafe extern "C" fn AMitemsRewound(items: *const AMitems) -> AMitems {
if let Some(items) = items.as_ref() {
return items.rewound();
}
Default::default()
}

View file

@ -1,12 +1,11 @@
mod actor_id;
mod byte_span;
mod change;
mod change_hashes;
mod changes;
mod doc;
mod index;
mod item;
mod items;
mod obj;
mod result;
mod result_stack;
mod strs;
mod sync;
// include!(concat!(env!("OUT_DIR"), "/enum_string_functions.rs"));

View file

@ -1,10 +1,12 @@
use automerge as am;
use std::any::type_name;
use std::cell::RefCell;
use std::ops::Deref;
use crate::actor_id::AMactorId;
pub mod item;
pub mod items;
macro_rules! to_obj_id {
($handle:expr) => {{
match $handle.as_ref() {
@ -17,11 +19,12 @@ macro_rules! to_obj_id {
pub(crate) use to_obj_id;
macro_rules! to_obj_type {
($c_obj_type:expr) => {{
let result: Result<am::ObjType, am::AutomergeError> = (&$c_obj_type).try_into();
match result {
Ok(obj_type) => obj_type,
Err(e) => return AMresult::error(&e.to_string()).into(),
($am_obj_type:expr) => {{
match $am_obj_type {
AMobjType::Map => am::ObjType::Map,
AMobjType::List => am::ObjType::List,
AMobjType::Text => am::ObjType::Text,
AMobjType::Void => return AMresult::err("Invalid AMobjType value").into(),
}
}};
}
@ -76,11 +79,11 @@ impl Deref for AMobjId {
}
/// \memberof AMobjId
/// \brief Gets the actor identifier component of an object identifier.
/// \brief Gets the actor identifier of an object identifier.
///
/// \param[in] obj_id A pointer to an `AMobjId` struct.
/// \return A pointer to an `AMactorId` struct or `NULL`.
/// \pre \p obj_id `!= NULL`
/// \pre \p obj_id `!= NULL`.
/// \internal
///
/// # Safety
@ -94,11 +97,11 @@ pub unsafe extern "C" fn AMobjIdActorId(obj_id: *const AMobjId) -> *const AMacto
}
/// \memberof AMobjId
/// \brief Gets the counter component of an object identifier.
/// \brief Gets the counter of an object identifier.
///
/// \param[in] obj_id A pointer to an `AMobjId` struct.
/// \return A 64-bit unsigned integer.
/// \pre \p obj_id `!= NULL`
/// \pre \p obj_id `!= NULL`.
/// \internal
///
/// # Safety
@ -121,9 +124,8 @@ pub unsafe extern "C" fn AMobjIdCounter(obj_id: *const AMobjId) -> u64 {
/// \param[in] obj_id1 A pointer to an `AMobjId` struct.
/// \param[in] obj_id2 A pointer to an `AMobjId` struct.
/// \return `true` if \p obj_id1 `==` \p obj_id2 and `false` otherwise.
/// \pre \p obj_id1 `!= NULL`
/// \pre \p obj_id1 `!= NULL`
/// \post `!(`\p obj_id1 `&&` \p obj_id2 `) -> false`
/// \pre \p obj_id1 `!= NULL`.
/// \pre \p obj_id2 `!= NULL`.
/// \internal
///
/// #Safety
@ -133,28 +135,26 @@ pub unsafe extern "C" fn AMobjIdCounter(obj_id: *const AMobjId) -> u64 {
pub unsafe extern "C" fn AMobjIdEqual(obj_id1: *const AMobjId, obj_id2: *const AMobjId) -> bool {
match (obj_id1.as_ref(), obj_id2.as_ref()) {
(Some(obj_id1), Some(obj_id2)) => obj_id1 == obj_id2,
(None, None) | (None, Some(_)) | (Some(_), None) => false,
(None, Some(_)) | (Some(_), None) | (None, None) => false,
}
}
/// \memberof AMobjId
/// \brief Gets the index component of an object identifier.
/// \brief Gets the index of an object identifier.
///
/// \param[in] obj_id A pointer to an `AMobjId` struct.
/// \return A 64-bit unsigned integer.
/// \pre \p obj_id `!= NULL`
/// \pre \p obj_id `!= NULL`.
/// \internal
///
/// # Safety
/// obj_id must be a valid pointer to an AMobjId
#[no_mangle]
pub unsafe extern "C" fn AMobjIdIndex(obj_id: *const AMobjId) -> usize {
use am::ObjId::*;
if let Some(obj_id) = obj_id.as_ref() {
match obj_id.as_ref() {
Id(_, _, index) => *index,
Root => 0,
am::ObjId::Id(_, _, index) => *index,
am::ObjId::Root => 0,
}
} else {
usize::MAX
@ -163,54 +163,26 @@ pub unsafe extern "C" fn AMobjIdIndex(obj_id: *const AMobjId) -> usize {
/// \ingroup enumerations
/// \enum AMobjType
/// \installed_headerfile
/// \brief The type of an object value.
#[derive(PartialEq, Eq)]
#[repr(u8)]
pub enum AMobjType {
/// The default tag, not a type signifier.
Default = 0,
/// A void.
/// \note This tag is unalphabetized to evaluate as false.
Void = 0,
/// A list.
List = 1,
List,
/// A key-value map.
Map,
/// A list of Unicode graphemes.
Text,
}
impl Default for AMobjType {
fn default() -> Self {
Self::Default
}
}
impl From<&am::ObjType> for AMobjType {
fn from(o: &am::ObjType) -> Self {
use am::ObjType::*;
impl From<am::ObjType> for AMobjType {
fn from(o: am::ObjType) -> Self {
match o {
List => Self::List,
Map | Table => Self::Map,
Text => Self::Text,
}
}
}
impl TryFrom<&AMobjType> for am::ObjType {
type Error = am::AutomergeError;
fn try_from(c_obj_type: &AMobjType) -> Result<Self, Self::Error> {
use am::AutomergeError::InvalidValueType;
use AMobjType::*;
match c_obj_type {
List => Ok(Self::List),
Map => Ok(Self::Map),
Text => Ok(Self::Text),
_ => Err(InvalidValueType {
expected: type_name::<Self>().to_string(),
unexpected: type_name::<AMobjType>().to_string(),
}),
am::ObjType::Map | am::ObjType::Table => AMobjType::Map,
am::ObjType::List => AMobjType::List,
am::ObjType::Text => AMobjType::Text,
}
}
}

View file

@ -0,0 +1,73 @@
use automerge as am;
use crate::obj::AMobjId;
use crate::result::AMvalue;
/// \struct AMobjItem
/// \installed_headerfile
/// \brief An item in an object.
pub struct AMobjItem {
/// The object identifier of an item in an object.
obj_id: AMobjId,
/// The value of an item in an object.
value: am::Value<'static>,
}
impl AMobjItem {
pub fn new(value: am::Value<'static>, obj_id: am::ObjId) -> Self {
Self {
obj_id: AMobjId::new(obj_id),
value,
}
}
}
impl PartialEq for AMobjItem {
fn eq(&self, other: &Self) -> bool {
self.obj_id == other.obj_id && self.value == other.value
}
}
impl From<&AMobjItem> for (am::Value<'static>, am::ObjId) {
fn from(obj_item: &AMobjItem) -> Self {
(obj_item.value.clone(), obj_item.obj_id.as_ref().clone())
}
}
/// \memberof AMobjItem
/// \brief Gets the object identifier of an item in an object.
///
/// \param[in] obj_item A pointer to an `AMobjItem` struct.
/// \return A pointer to an `AMobjId` struct.
/// \pre \p obj_item `!= NULL`.
/// \internal
///
/// # Safety
/// obj_item must be a valid pointer to an AMobjItem
#[no_mangle]
pub unsafe extern "C" fn AMobjItemObjId(obj_item: *const AMobjItem) -> *const AMobjId {
if let Some(obj_item) = obj_item.as_ref() {
&obj_item.obj_id
} else {
std::ptr::null()
}
}
/// \memberof AMobjItem
/// \brief Gets the value of an item in an object.
///
/// \param[in] obj_item A pointer to an `AMobjItem` struct.
/// \return An `AMvalue` struct.
/// \pre \p obj_item `!= NULL`.
/// \internal
///
/// # Safety
/// obj_item must be a valid pointer to an AMobjItem
#[no_mangle]
pub unsafe extern "C" fn AMobjItemValue<'a>(obj_item: *const AMobjItem) -> AMvalue<'a> {
if let Some(obj_item) = obj_item.as_ref() {
(&obj_item.value).into()
} else {
AMvalue::Void
}
}

View file

@ -0,0 +1,341 @@
use std::ffi::c_void;
use std::mem::size_of;
use crate::obj::item::AMobjItem;
#[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(obj_items: &[AMobjItem], offset: isize) -> Self {
Self {
len: obj_items.len(),
offset,
ptr: obj_items.as_ptr() as *const c_void,
}
}
pub fn advance(&mut self, n: isize) {
if n == 0 {
return;
}
let len = self.len as isize;
self.offset = if self.offset < 0 {
// It's reversed.
let unclipped = self.offset.checked_sub(n).unwrap_or(isize::MIN);
if unclipped >= 0 {
// Clip it to the forward stop.
len
} else {
std::cmp::min(std::cmp::max(-(len + 1), unclipped), -1)
}
} else {
let unclipped = self.offset.checked_add(n).unwrap_or(isize::MAX);
if unclipped < 0 {
// Clip it to the reverse stop.
-(len + 1)
} else {
std::cmp::max(0, std::cmp::min(unclipped, 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<&AMobjItem> {
if self.is_stopped() {
return None;
}
let slice: &[AMobjItem] =
unsafe { std::slice::from_raw_parts(self.ptr as *const AMobjItem, 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<&AMobjItem> {
self.advance(-n);
if self.is_stopped() {
return None;
}
let slice: &[AMobjItem] =
unsafe { std::slice::from_raw_parts(self.ptr as *const AMobjItem, self.len) };
Some(&slice[self.get_index()])
}
pub fn reversed(&self) -> Self {
Self {
len: self.len,
offset: -(self.offset + 1),
ptr: self.ptr,
}
}
pub fn rewound(&self) -> Self {
Self {
len: self.len,
offset: if self.offset < 0 { -1 } else { 0 },
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 AMobjItems
/// \installed_headerfile
/// \brief A random-access iterator over a sequence of object items.
#[repr(C)]
#[derive(Eq, PartialEq)]
pub struct AMobjItems {
/// An implementation detail that is intentionally opaque.
/// \warning Modifying \p detail will cause undefined behavior.
/// \note The actual size of \p detail will vary by platform, this is just
/// the one for the platform this documentation was built on.
detail: [u8; USIZE_USIZE_USIZE_],
}
impl AMobjItems {
pub fn new(obj_items: &[AMobjItem]) -> Self {
Self {
detail: Detail::new(obj_items, 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<&AMobjItem> {
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
detail.next(n)
}
pub fn prev(&mut self, n: isize) -> Option<&AMobjItem> {
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(),
}
}
pub fn rewound(&self) -> Self {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
Self {
detail: detail.rewound().into(),
}
}
}
impl AsRef<[AMobjItem]> for AMobjItems {
fn as_ref(&self) -> &[AMobjItem] {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
unsafe { std::slice::from_raw_parts(detail.ptr as *const AMobjItem, detail.len) }
}
}
impl Default for AMobjItems {
fn default() -> Self {
Self {
detail: [0; USIZE_USIZE_USIZE_],
}
}
}
/// \memberof AMobjItems
/// \brief Advances an iterator over a sequence of object items by at most
/// \p |n| positions where the sign of \p n is relative to the
/// iterator's direction.
///
/// \param[in,out] obj_items A pointer to an `AMobjItems` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \pre \p obj_items `!= NULL`.
/// \internal
///
/// #Safety
/// obj_items must be a valid pointer to an AMobjItems
#[no_mangle]
pub unsafe extern "C" fn AMobjItemsAdvance(obj_items: *mut AMobjItems, n: isize) {
if let Some(obj_items) = obj_items.as_mut() {
obj_items.advance(n);
};
}
/// \memberof AMobjItems
/// \brief Tests the equality of two sequences of object items underlying a
/// pair of iterators.
///
/// \param[in] obj_items1 A pointer to an `AMobjItems` struct.
/// \param[in] obj_items2 A pointer to an `AMobjItems` struct.
/// \return `true` if \p obj_items1 `==` \p obj_items2 and `false` otherwise.
/// \pre \p obj_items1 `!= NULL`.
/// \pre \p obj_items2 `!= NULL`.
/// \internal
///
/// #Safety
/// obj_items1 must be a valid pointer to an AMobjItems
/// obj_items2 must be a valid pointer to an AMobjItems
#[no_mangle]
pub unsafe extern "C" fn AMobjItemsEqual(
obj_items1: *const AMobjItems,
obj_items2: *const AMobjItems,
) -> bool {
match (obj_items1.as_ref(), obj_items2.as_ref()) {
(Some(obj_items1), Some(obj_items2)) => obj_items1.as_ref() == obj_items2.as_ref(),
(None, Some(_)) | (Some(_), None) | (None, None) => false,
}
}
/// \memberof AMobjItems
/// \brief Gets the object item at the current position of an iterator over a
/// sequence of object items 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,out] obj_items A pointer to an `AMobjItems` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \return A pointer to an `AMobjItem` struct that's `NULL` when \p obj_items
/// was previously advanced past its forward/reverse limit.
/// \pre \p obj_items `!= NULL`.
/// \internal
///
/// #Safety
/// obj_items must be a valid pointer to an AMobjItems
#[no_mangle]
pub unsafe extern "C" fn AMobjItemsNext(obj_items: *mut AMobjItems, n: isize) -> *const AMobjItem {
if let Some(obj_items) = obj_items.as_mut() {
if let Some(obj_item) = obj_items.next(n) {
return obj_item;
}
}
std::ptr::null()
}
/// \memberof AMobjItems
/// \brief Advances an iterator over a sequence of object items by at most
/// \p |n| positions where the sign of \p n is relative to the
/// iterator's direction and then gets the object item at its new
/// position.
///
/// \param[in,out] obj_items A pointer to an `AMobjItems` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \return A pointer to an `AMobjItem` struct that's `NULL` when \p obj_items
/// is presently advanced past its forward/reverse limit.
/// \pre \p obj_items `!= NULL`.
/// \internal
///
/// #Safety
/// obj_items must be a valid pointer to an AMobjItems
#[no_mangle]
pub unsafe extern "C" fn AMobjItemsPrev(obj_items: *mut AMobjItems, n: isize) -> *const AMobjItem {
if let Some(obj_items) = obj_items.as_mut() {
if let Some(obj_item) = obj_items.prev(n) {
return obj_item;
}
}
std::ptr::null()
}
/// \memberof AMobjItems
/// \brief Gets the size of the sequence of object items underlying an
/// iterator.
///
/// \param[in] obj_items A pointer to an `AMobjItems` struct.
/// \return The count of values in \p obj_items.
/// \pre \p obj_items `!= NULL`.
/// \internal
///
/// #Safety
/// obj_items must be a valid pointer to an AMobjItems
#[no_mangle]
pub unsafe extern "C" fn AMobjItemsSize(obj_items: *const AMobjItems) -> usize {
if let Some(obj_items) = obj_items.as_ref() {
obj_items.len()
} else {
0
}
}
/// \memberof AMobjItems
/// \brief Creates an iterator over the same sequence of object items as the
/// given one but with the opposite position and direction.
///
/// \param[in] obj_items A pointer to an `AMobjItems` struct.
/// \return An `AMobjItems` struct
/// \pre \p obj_items `!= NULL`.
/// \internal
///
/// #Safety
/// obj_items must be a valid pointer to an AMobjItems
#[no_mangle]
pub unsafe extern "C" fn AMobjItemsReversed(obj_items: *const AMobjItems) -> AMobjItems {
if let Some(obj_items) = obj_items.as_ref() {
obj_items.reversed()
} else {
Default::default()
}
}
/// \memberof AMobjItems
/// \brief Creates an iterator at the starting position over the same sequence
/// of object items as the given one.
///
/// \param[in] obj_items A pointer to an `AMobjItems` struct.
/// \return An `AMobjItems` struct
/// \pre \p obj_items `!= NULL`.
/// \internal
///
/// #Safety
/// obj_items must be a valid pointer to an AMobjItems
#[no_mangle]
pub unsafe extern "C" fn AMobjItemsRewound(obj_items: *const AMobjItems) -> AMobjItems {
if let Some(obj_items) = obj_items.as_ref() {
obj_items.rewound()
} else {
Default::default()
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,156 @@
use crate::result::{AMfree, AMresult, AMresultStatus, AMresultValue, AMstatus, AMvalue};
/// \struct AMresultStack
/// \installed_headerfile
/// \brief A node in a singly-linked list of result pointers.
///
/// \note Using this data structure is purely optional because its only purpose
/// is to make memory management tolerable for direct usage of this API
/// in C, C++ and Objective-C.
#[repr(C)]
pub struct AMresultStack {
/// A result to be deallocated.
pub result: *mut AMresult,
/// The next node in the singly-linked list or `NULL`.
pub next: *mut AMresultStack,
}
impl AMresultStack {
pub fn new(result: *mut AMresult, next: *mut AMresultStack) -> Self {
Self { result, next }
}
}
/// \memberof AMresultStack
/// \brief Deallocates the storage for a stack of results.
///
/// \param[in,out] stack A pointer to a pointer to an `AMresultStack` struct.
/// \return The number of `AMresult` structs freed.
/// \pre \p stack `!= NULL`.
/// \post `*stack == NULL`.
/// \note Calling this function is purely optional because its only purpose is
/// to make memory management tolerable for direct usage of this API in
/// C, C++ and Objective-C.
/// \internal
///
/// # Safety
/// stack must be a valid AMresultStack pointer pointer
#[no_mangle]
pub unsafe extern "C" fn AMfreeStack(stack: *mut *mut AMresultStack) -> usize {
if stack.is_null() {
return 0;
}
let mut count: usize = 0;
while !(*stack).is_null() {
AMfree(AMpop(stack));
count += 1;
}
count
}
/// \memberof AMresultStack
/// \brief Gets the topmost result from the stack after removing it.
///
/// \param[in,out] stack A pointer to a pointer to an `AMresultStack` struct.
/// \return A pointer to an `AMresult` struct or `NULL`.
/// \pre \p stack `!= NULL`.
/// \post `*stack == NULL`.
/// \note Calling this function is purely optional because its only purpose is
/// to make memory management tolerable for direct usage of this API in
/// C, C++ and Objective-C.
/// \internal
///
/// # Safety
/// stack must be a valid AMresultStack pointer pointer
#[no_mangle]
pub unsafe extern "C" fn AMpop(stack: *mut *mut AMresultStack) -> *mut AMresult {
if stack.is_null() || (*stack).is_null() {
return std::ptr::null_mut();
}
let top = Box::from_raw(*stack);
*stack = top.next;
let result = top.result;
drop(top);
result
}
/// \memberof AMresultStack
/// \brief The prototype of a function to be called when a value matching the
/// given discriminant cannot be extracted from the result at the top of
/// the given stack.
///
/// \note Implementing this function is purely optional because its only purpose
/// is to make memory management tolerable for direct usage of this API
/// in C, C++ and Objective-C.
pub type AMpushCallback =
Option<extern "C" fn(stack: *mut *mut AMresultStack, discriminant: u8) -> ()>;
/// \memberof AMresultStack
/// \brief Pushes the given result onto the given stack and then either extracts
/// a value matching the given discriminant from that result or,
/// failing that, calls the given function and gets a void value instead.
///
/// \param[in,out] stack A pointer to a pointer to an `AMresultStack` struct.
/// \param[in] result A pointer to an `AMresult` struct.
/// \param[in] discriminant An `AMvalue` variant's corresponding enum tag.
/// \param[in] callback A pointer to a function with the same signature as
/// `AMpushCallback()` or `NULL`.
/// \return An `AMvalue` struct.
/// \pre \p stack `!= NULL`.
/// \pre \p result `!= NULL`.
/// \warning If \p stack `== NULL` then \p result is deallocated in order to
/// prevent a memory leak.
/// \note Calling this function is purely optional because its only purpose is
/// to make memory management tolerable for direct usage of this API in
/// C, C++ and Objective-C.
/// \internal
///
/// # Safety
/// stack must be a valid AMresultStack pointer pointer
/// result must be a valid AMresult pointer
#[no_mangle]
pub unsafe extern "C" fn AMpush<'a>(
stack: *mut *mut AMresultStack,
result: *mut AMresult,
discriminant: u8,
callback: AMpushCallback,
) -> AMvalue<'a> {
if stack.is_null() {
// There's no stack to push the result onto so it has to be freed in
// order to prevent a memory leak.
AMfree(result);
if let Some(callback) = callback {
callback(stack, discriminant);
}
return AMvalue::Void;
} else if result.is_null() {
if let Some(callback) = callback {
callback(stack, discriminant);
}
return AMvalue::Void;
}
// Always push the result onto the stack, even if it's wrong, so that the
// given callback can retrieve it.
let node = Box::new(AMresultStack::new(result, *stack));
let top = Box::into_raw(node);
*stack = top;
// Test that the result contains a value.
match AMresultStatus(result) {
AMstatus::Ok => {}
_ => {
if let Some(callback) = callback {
callback(stack, discriminant);
}
return AMvalue::Void;
}
}
// Test that the result's value matches the given discriminant.
let value = AMresultValue(result);
if discriminant != u8::from(&value) {
if let Some(callback) = callback {
callback(stack, discriminant);
}
return AMvalue::Void;
}
value
}

View file

@ -0,0 +1,359 @@
use std::cmp::Ordering;
use std::ffi::c_void;
use std::mem::size_of;
use std::os::raw::c_char;
use crate::byte_span::AMbyteSpan;
/// \brief Creates a string view from a C string.
///
/// \param[in] c_str A UTF-8 C string.
/// \return A UTF-8 string view as an `AMbyteSpan` struct.
/// \internal
///
/// #Safety
/// c_str must be a null-terminated array of `c_char`
#[no_mangle]
pub unsafe extern "C" fn AMstr(c_str: *const c_char) -> AMbyteSpan {
c_str.into()
}
#[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(strings: &[String], offset: isize) -> Self {
Self {
len: strings.len(),
offset,
ptr: strings.as_ptr() as *const c_void,
}
}
pub fn advance(&mut self, n: isize) {
if n == 0 {
return;
}
let len = self.len as isize;
self.offset = if self.offset < 0 {
// It's reversed.
let unclipped = self.offset.checked_sub(n).unwrap_or(isize::MIN);
if unclipped >= 0 {
// Clip it to the forward stop.
len
} else {
std::cmp::min(std::cmp::max(-(len + 1), unclipped), -1)
}
} else {
let unclipped = self.offset.checked_add(n).unwrap_or(isize::MAX);
if unclipped < 0 {
// Clip it to the reverse stop.
-(len + 1)
} else {
std::cmp::max(0, std::cmp::min(unclipped, 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<AMbyteSpan> {
if self.is_stopped() {
return None;
}
let slice: &[String] =
unsafe { std::slice::from_raw_parts(self.ptr as *const String, self.len) };
let value = slice[self.get_index()].as_bytes().into();
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<AMbyteSpan> {
self.advance(-n);
if self.is_stopped() {
return None;
}
let slice: &[String] =
unsafe { std::slice::from_raw_parts(self.ptr as *const String, self.len) };
Some(slice[self.get_index()].as_bytes().into())
}
pub fn reversed(&self) -> Self {
Self {
len: self.len,
offset: -(self.offset + 1),
ptr: self.ptr,
}
}
pub fn rewound(&self) -> Self {
Self {
len: self.len,
offset: if self.offset < 0 { -1 } else { 0 },
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 AMstrs
/// \installed_headerfile
/// \brief A random-access iterator over a sequence of UTF-8 strings.
#[repr(C)]
#[derive(Eq, PartialEq)]
pub struct AMstrs {
/// An implementation detail that is intentionally opaque.
/// \warning Modifying \p detail will cause undefined behavior.
/// \note The actual size of \p detail will vary by platform, this is just
/// the one for the platform this documentation was built on.
detail: [u8; USIZE_USIZE_USIZE_],
}
impl AMstrs {
pub fn new(strings: &[String]) -> Self {
Self {
detail: Detail::new(strings, 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<AMbyteSpan> {
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
detail.next(n)
}
pub fn prev(&mut self, n: isize) -> Option<AMbyteSpan> {
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(),
}
}
pub fn rewound(&self) -> Self {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
Self {
detail: detail.rewound().into(),
}
}
}
impl AsRef<[String]> for AMstrs {
fn as_ref(&self) -> &[String] {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
unsafe { std::slice::from_raw_parts(detail.ptr as *const String, detail.len) }
}
}
impl Default for AMstrs {
fn default() -> Self {
Self {
detail: [0; USIZE_USIZE_USIZE_],
}
}
}
/// \memberof AMstrs
/// \brief Advances an iterator over a sequence of UTF-8 strings by at most
/// \p |n| positions where the sign of \p n is relative to the
/// iterator's direction.
///
/// \param[in,out] strs A pointer to an `AMstrs` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \pre \p strs `!= NULL`.
/// \internal
///
/// #Safety
/// strs must be a valid pointer to an AMstrs
#[no_mangle]
pub unsafe extern "C" fn AMstrsAdvance(strs: *mut AMstrs, n: isize) {
if let Some(strs) = strs.as_mut() {
strs.advance(n);
};
}
/// \memberof AMstrs
/// \brief Compares the sequences of UTF-8 strings underlying a pair of
/// iterators.
///
/// \param[in] strs1 A pointer to an `AMstrs` struct.
/// \param[in] strs2 A pointer to an `AMstrs` struct.
/// \return `-1` if \p strs1 `<` \p strs2, `0` if
/// \p strs1 `==` \p strs2 and `1` if
/// \p strs1 `>` \p strs2.
/// \pre \p strs1 `!= NULL`.
/// \pre \p strs2 `!= NULL`.
/// \internal
///
/// #Safety
/// strs1 must be a valid pointer to an AMstrs
/// strs2 must be a valid pointer to an AMstrs
#[no_mangle]
pub unsafe extern "C" fn AMstrsCmp(strs1: *const AMstrs, strs2: *const AMstrs) -> isize {
match (strs1.as_ref(), strs2.as_ref()) {
(Some(strs1), Some(strs2)) => match strs1.as_ref().cmp(strs2.as_ref()) {
Ordering::Less => -1,
Ordering::Equal => 0,
Ordering::Greater => 1,
},
(None, Some(_)) => -1,
(Some(_), None) => 1,
(None, None) => 0,
}
}
/// \memberof AMstrs
/// \brief Gets the key at the current position of an iterator over a sequence
/// of UTF-8 strings 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,out] strs A pointer to an `AMstrs` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \return A UTF-8 string view as an `AMbyteSpan` struct that's `AMstr(NULL)`
/// when \p strs was previously advanced past its forward/reverse limit.
/// \pre \p strs `!= NULL`.
/// \internal
///
/// #Safety
/// strs must be a valid pointer to an AMstrs
#[no_mangle]
pub unsafe extern "C" fn AMstrsNext(strs: *mut AMstrs, n: isize) -> AMbyteSpan {
if let Some(strs) = strs.as_mut() {
if let Some(key) = strs.next(n) {
return key;
}
}
Default::default()
}
/// \memberof AMstrs
/// \brief Advances an iterator over a sequence of UTF-8 strings by at most
/// \p |n| positions where the sign of \p n is relative to the
/// iterator's direction and then gets the key at its new position.
///
/// \param[in,out] strs A pointer to an `AMstrs` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \return A UTF-8 string view as an `AMbyteSpan` struct that's `AMstr(NULL)`
/// when \p strs is presently advanced past its forward/reverse limit.
/// \pre \p strs `!= NULL`.
/// \internal
///
/// #Safety
/// strs must be a valid pointer to an AMstrs
#[no_mangle]
pub unsafe extern "C" fn AMstrsPrev(strs: *mut AMstrs, n: isize) -> AMbyteSpan {
if let Some(strs) = strs.as_mut() {
if let Some(key) = strs.prev(n) {
return key;
}
}
Default::default()
}
/// \memberof AMstrs
/// \brief Gets the size of the sequence of UTF-8 strings underlying an
/// iterator.
///
/// \param[in] strs A pointer to an `AMstrs` struct.
/// \return The count of values in \p strs.
/// \pre \p strs `!= NULL`.
/// \internal
///
/// #Safety
/// strs must be a valid pointer to an AMstrs
#[no_mangle]
pub unsafe extern "C" fn AMstrsSize(strs: *const AMstrs) -> usize {
if let Some(strs) = strs.as_ref() {
strs.len()
} else {
0
}
}
/// \memberof AMstrs
/// \brief Creates an iterator over the same sequence of UTF-8 strings as the
/// given one but with the opposite position and direction.
///
/// \param[in] strs A pointer to an `AMstrs` struct.
/// \return An `AMstrs` struct.
/// \pre \p strs `!= NULL`.
/// \internal
///
/// #Safety
/// strs must be a valid pointer to an AMstrs
#[no_mangle]
pub unsafe extern "C" fn AMstrsReversed(strs: *const AMstrs) -> AMstrs {
if let Some(strs) = strs.as_ref() {
strs.reversed()
} else {
AMstrs::default()
}
}
/// \memberof AMstrs
/// \brief Creates an iterator at the starting position over the same sequence
/// of UTF-8 strings as the given one.
///
/// \param[in] strs A pointer to an `AMstrs` struct.
/// \return An `AMstrs` struct
/// \pre \p strs `!= NULL`.
/// \internal
///
/// #Safety
/// strs must be a valid pointer to an AMstrs
#[no_mangle]
pub unsafe extern "C" fn AMstrsRewound(strs: *const AMstrs) -> AMstrs {
if let Some(strs) = strs.as_ref() {
strs.rewound()
} else {
Default::default()
}
}

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

@ -1,23 +1,23 @@
use automerge as am;
use crate::result::{to_result, AMresult};
use crate::change_hashes::AMchangeHashes;
/// \struct AMsyncHave
/// \installed_headerfile
/// \brief A summary of the changes that the sender of a synchronization
/// message already has.
#[derive(Clone, Eq, PartialEq)]
pub struct AMsyncHave(am::sync::Have);
pub struct AMsyncHave(*const am::sync::Have);
impl AMsyncHave {
pub fn new(have: am::sync::Have) -> Self {
pub fn new(have: &am::sync::Have) -> Self {
Self(have)
}
}
impl AsRef<am::sync::Have> for AMsyncHave {
fn as_ref(&self) -> &am::sync::Have {
&self.0
unsafe { &*self.0 }
}
}
@ -25,18 +25,17 @@ impl AsRef<am::sync::Have> for AMsyncHave {
/// \brief Gets the heads of the sender.
///
/// \param[in] sync_have A pointer to an `AMsyncHave` struct.
/// \return A pointer to an `AMresult` struct with `AM_VAL_TYPE_CHANGE_HASH` items.
/// \pre \p sync_have `!= 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_have `!= NULL`.
/// \internal
///
/// # Safety
/// sync_have must be a valid pointer to an AMsyncHave
#[no_mangle]
pub unsafe extern "C" fn AMsyncHaveLastSync(sync_have: *const AMsyncHave) -> *mut AMresult {
to_result(match sync_have.as_ref() {
Some(sync_have) => sync_have.as_ref().last_sync.as_slice(),
None => Default::default(),
})
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 {
Default::default()
}
}

View file

@ -0,0 +1,378 @@
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 {
return;
}
let len = self.len as isize;
self.offset = if self.offset < 0 {
// It's reversed.
let unclipped = self.offset.checked_sub(n).unwrap_or(isize::MIN);
if unclipped >= 0 {
// Clip it to the forward stop.
len
} else {
std::cmp::min(std::cmp::max(-(len + 1), unclipped), -1)
}
} else {
let unclipped = self.offset.checked_add(n).unwrap_or(isize::MAX);
if unclipped < 0 {
// Clip it to the reverse stop.
-(len + 1)
} else {
std::cmp::max(0, std::cmp::min(unclipped, 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,
}
}
pub fn rewound(&self) -> Self {
Self {
len: self.len,
offset: if self.offset < 0 { -1 } else { 0 },
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
/// \installed_headerfile
/// \brief A random-access iterator over a sequence of synchronization haves.
#[repr(C)]
#[derive(Eq, PartialEq)]
pub struct AMsyncHaves {
/// An implementation detail that is intentionally opaque.
/// \warning Modifying \p detail will cause undefined behavior.
/// \note The actual size of \p detail will vary by platform, this is just
/// the one for the platform this documentation was built on.
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(),
}
}
pub fn rewound(&self) -> Self {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
Self {
detail: detail.rewound().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,out] 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 `!= NULL`.
/// \internal
///
/// #Safety
/// sync_haves must be a valid pointer to an 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 `!= NULL`.
/// \pre \p sync_haves2 `!= NULL`.
/// \internal
///
/// #Safety
/// sync_haves1 must be a valid pointer to an AMsyncHaves
/// sync_haves2 must be a valid pointer to an 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,out] 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 `!= NULL`.
/// \internal
///
/// #Safety
/// sync_haves must be a valid pointer to an 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,out] 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 `!= NULL`.
/// \internal
///
/// #Safety
/// sync_haves must be a valid pointer to an 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 `!= NULL`.
/// \internal
///
/// #Safety
/// sync_haves must be a valid pointer to an 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 `!= NULL`.
/// \internal
///
/// #Safety
/// sync_haves must be a valid pointer to an 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 {
Default::default()
}
}
/// \memberof AMsyncHaves
/// \brief Creates an iterator at the starting position over the same sequence
/// of synchronization haves as the given one.
///
/// \param[in] sync_haves A pointer to an `AMsyncHaves` struct.
/// \return An `AMsyncHaves` struct
/// \pre \p sync_haves `!= NULL`.
/// \internal
///
/// #Safety
/// sync_haves must be a valid pointer to an AMsyncHaves
#[no_mangle]
pub unsafe extern "C" fn AMsyncHavesRewound(sync_haves: *const AMsyncHaves) -> AMsyncHaves {
if let Some(sync_haves) = sync_haves.as_ref() {
sync_haves.rewound()
} else {
Default::default()
}
}

View file

@ -3,15 +3,18 @@ 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::error("Invalid `AMsyncMessage*`").into(),
None => return AMresult::err("Invalid AMsyncMessage pointer").into(),
}
}};
}
@ -48,52 +51,55 @@ impl AsRef<am::sync::Message> for AMsyncMessage {
/// \brief Gets the changes for the recipient to apply.
///
/// \param[in] sync_message A pointer to an `AMsyncMessage` struct.
/// \return A pointer to an `AMresult` struct with `AM_VAL_TYPE_CHANGE` items.
/// \pre \p sync_message `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \return An `AMchanges` struct.
/// \pre \p sync_message `!= NULL`.
/// \internal
///
/// # Safety
/// sync_message must be a valid pointer to an AMsyncMessage
#[no_mangle]
pub unsafe extern "C" fn AMsyncMessageChanges(sync_message: *const AMsyncMessage) -> *mut AMresult {
to_result(match sync_message.as_ref() {
Some(sync_message) => sync_message.body.changes.as_slice(),
None => Default::default(),
})
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 {
Default::default()
}
}
/// \memberof AMsyncMessage
/// \brief Decodes an array of bytes into a synchronization message.
/// \brief Decodes a sequence of bytes into a synchronization message.
///
/// \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 `AM_VAL_TYPE_SYNC_MESSAGE` 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 `AMsyncMessage`
/// struct.
/// \pre \p src `!= NULL`.
/// \pre `0 <` \p count `<= sizeof(`\p src`)`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
/// src must be a byte array of length `>= count`
/// src must be a byte array of size `>= count`
#[no_mangle]
pub unsafe extern "C" fn AMsyncMessageDecode(src: *const u8, count: usize) -> *mut AMresult {
let data = std::slice::from_raw_parts(src, count);
to_result(am::sync::Message::decode(data))
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 an array of bytes.
/// \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 with an `AM_VAL_TYPE_BYTES` item.
/// \pre \p sync_message `!= 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_message `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
@ -108,40 +114,41 @@ pub unsafe extern "C" fn AMsyncMessageEncode(sync_message: *const AMsyncMessage)
/// \brief Gets a summary of the changes that the sender already has.
///
/// \param[in] sync_message A pointer to an `AMsyncMessage` struct.
/// \return A pointer to an `AMresult` struct with `AM_SYNC_HAVE` items.
/// \pre \p sync_message `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \return An `AMhaves` struct.
/// \pre \p sync_message `!= NULL`.
/// \internal
///
/// # Safety
/// sync_message must be a valid pointer to an AMsyncMessage
#[no_mangle]
pub unsafe extern "C" fn AMsyncMessageHaves(sync_message: *const AMsyncMessage) -> *mut AMresult {
to_result(match sync_message.as_ref() {
Some(sync_message) => sync_message.as_ref().have.as_slice(),
None => Default::default(),
})
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 {
Default::default()
}
}
/// \memberof AMsyncMessage
/// \brief Gets the heads of the sender.
///
/// \param[in] sync_message A pointer to an `AMsyncMessage` struct.
/// \return A pointer to an `AMresult` struct with `AM_VAL_TYPE_CHANGE_HASH` items.
/// \pre \p sync_message `!= 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_message `!= NULL`.
/// \internal
///
/// # Safety
/// sync_message must be a valid pointer to an AMsyncMessage
#[no_mangle]
pub unsafe extern "C" fn AMsyncMessageHeads(sync_message: *const AMsyncMessage) -> *mut AMresult {
to_result(match sync_message.as_ref() {
Some(sync_message) => sync_message.as_ref().heads.as_slice(),
None => Default::default(),
})
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 {
Default::default()
}
}
/// \memberof AMsyncMessage
@ -149,18 +156,17 @@ pub unsafe extern "C" fn AMsyncMessageHeads(sync_message: *const AMsyncMessage)
/// by the recipient.
///
/// \param[in] sync_message A pointer to an `AMsyncMessage` struct.
/// \return A pointer to an `AMresult` struct with `AM_VAL_TYPE_CHANGE_HASH` items.
/// \pre \p sync_message `!= 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_message `!= NULL`.
/// \internal
///
/// # Safety
/// sync_message must be a valid pointer to an AMsyncMessage
#[no_mangle]
pub unsafe extern "C" fn AMsyncMessageNeeds(sync_message: *const AMsyncMessage) -> *mut AMresult {
to_result(match sync_message.as_ref() {
Some(sync_message) => sync_message.as_ref().need.as_slice(),
None => Default::default(),
})
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 {
Default::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(),
}
}};
}
@ -54,35 +56,36 @@ 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 `!= NULL`.
/// \pre `0 <` \p count `<= sizeof(`\p src`)`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
/// src must be a byte array of length `>= count`
/// src must be a byte array of size `>= 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 `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
@ -99,9 +102,8 @@ 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 `!= NULL`.
/// \pre \p sync_state2 `!= NULL`.
/// \internal
///
/// #Safety
@ -114,17 +116,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 The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
#[no_mangle]
pub extern "C" fn AMsyncStateInit() -> *mut AMresult {
to_result(am::sync::State::new())
@ -134,36 +137,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 `!= NULL`.
/// \internal
///
/// # Safety
/// sync_state must be a valid pointer to an 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 {
Default::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 `!= NULL`.
/// \internal
///
/// # Safety
/// sync_state must be a valid pointer to an 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 {
Default::default()
}
}
/// \memberof AMsyncState
@ -171,13 +178,11 @@ 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 `!= NULL`.
/// \pre \p has_value `!= NULL`.
/// \internal
///
/// # Safety
/// sync_state must be a valid pointer to an AMsyncState
@ -186,15 +191,15 @@ pub unsafe extern "C" fn AMsyncStateLastSentHeads(sync_state: *const AMsyncState
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())
Default::default()
}
/// \memberof AMsyncState
@ -202,31 +207,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`
/// 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.
/// the returned `AMchangeHashes` struct is relevant, `false`
/// otherwise.
/// \return An `AMchangeHashes` struct.
/// \pre \p sync_state `!= NULL`.
/// \pre \p has_value `!= NULL`.
/// \internal
///
/// # Safety
/// sync_state must be a valid pointer to an AMsyncState
/// has_value must be a valid pointer to a bool
/// has_value must be a valid pointer to a 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())
Default::default()
}
/// \memberof AMsyncState
@ -234,29 +237,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`
/// 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.
/// the returned `AMchangeHashes` struct is relevant, `false`
/// otherwise.
/// \return An `AMchangeHashes` struct.
/// \pre \p sync_state `!= NULL`.
/// \pre \p has_value `!= NULL`.
/// \internal
///
/// # Safety
/// sync_state must be a valid pointer to an AMsyncState
/// has_value must be a valid pointer to a bool
/// has_value must be a valid pointer to a 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())
Default::default()
}

View file

@ -1,33 +0,0 @@
#include <stdarg.h>
#include <automerge-c/utils/result.h>
AMresult* AMresultFrom(int count, ...) {
AMresult* result = NULL;
bool is_ok = true;
va_list args;
va_start(args, count);
for (int i = 0; i != count; ++i) {
AMresult* src = va_arg(args, AMresult*);
AMresult* dest = result;
is_ok = (AMresultStatus(src) == AM_STATUS_OK);
if (is_ok) {
if (dest) {
result = AMresultCat(dest, src);
is_ok = (AMresultStatus(result) == AM_STATUS_OK);
AMresultFree(dest);
AMresultFree(src);
} else {
result = src;
}
} else {
AMresultFree(src);
}
}
va_end(args);
if (!is_ok) {
AMresultFree(result);
result = NULL;
}
return result;
}

View file

@ -1,106 +0,0 @@
#include <stdio.h>
#include <stdlib.h>
#include <automerge-c/utils/stack.h>
#include <automerge-c/utils/string.h>
void AMstackFree(AMstack** stack) {
if (stack) {
while (*stack) {
AMresultFree(AMstackPop(stack, NULL));
}
}
}
AMresult* AMstackPop(AMstack** stack, const AMresult* result) {
if (!stack) {
return NULL;
}
AMstack** prev = stack;
if (result) {
while (*prev && ((*prev)->result != result)) {
*prev = (*prev)->prev;
}
}
if (!*prev) {
return NULL;
}
AMstack* target = *prev;
*prev = target->prev;
AMresult* popped = target->result;
free(target);
return popped;
}
AMresult* AMstackResult(AMstack** stack, AMresult* result, AMstackCallback callback, void* data) {
if (!stack) {
if (callback) {
/* Create a local stack so that the callback can still examine the
* result. */
AMstack node = {.result = result, .prev = NULL};
AMstack* stack = &node;
callback(&stack, data);
} else {
/* \note There is no reason to call this function when both the
* stack and the callback are null. */
fprintf(stderr, "ERROR: NULL AMstackCallback!\n");
}
/* \note Nothing can be returned without a stack regardless of
* whether or not the callback validated the result. */
AMresultFree(result);
return NULL;
}
/* Always push the result onto the stack, even if it's null, so that the
* callback can examine it. */
AMstack* next = calloc(1, sizeof(AMstack));
*next = (AMstack){.result = result, .prev = *stack};
AMstack* top = next;
*stack = top;
if (callback) {
if (!callback(stack, data)) {
/* The result didn't pass the callback's examination. */
return NULL;
}
} else {
/* Report an obvious error. */
if (result) {
AMbyteSpan const err_msg = AMresultError(result);
if (err_msg.src && err_msg.count) {
/* \note The callback may be null because the result is supposed
* to be examined externally so return it despite an
* error. */
char* const cstr = AMstrdup(err_msg, NULL);
fprintf(stderr, "WARNING: %s.\n", cstr);
free(cstr);
}
} else {
/* \note There's no reason to call this function when both the
* result and the callback are null. */
fprintf(stderr, "ERROR: NULL AMresult*!\n");
return NULL;
}
}
return result;
}
AMitem* AMstackItem(AMstack** stack, AMresult* result, AMstackCallback callback, void* data) {
AMitems items = AMstackItems(stack, result, callback, data);
return AMitemsNext(&items, 1);
}
AMitems AMstackItems(AMstack** stack, AMresult* result, AMstackCallback callback, void* data) {
return (AMstackResult(stack, result, callback, data)) ? AMresultItems(result) : (AMitems){0};
}
size_t AMstackSize(AMstack const* const stack) {
if (!stack) {
return 0;
}
size_t count = 0;
AMstack const* prev = stack;
while (prev) {
++count;
prev = prev->prev;
}
return count;
}

View file

@ -1,9 +0,0 @@
#include <stdlib.h>
#include <automerge-c/utils/stack_callback_data.h>
AMstackCallbackData* AMstackCallbackDataInit(AMvalType const bitmask, char const* const file, int const line) {
AMstackCallbackData* data = malloc(sizeof(AMstackCallbackData));
*data = (AMstackCallbackData){.bitmask = bitmask, .file = file, .line = line};
return data;
}

View file

@ -1,46 +0,0 @@
#include <stdlib.h>
#include <string.h>
#include <automerge-c/utils/string.h>
char* AMstrdup(AMbyteSpan const str, char const* nul) {
if (!str.src) {
return NULL;
} else if (!str.count) {
return strdup("");
}
nul = (nul) ? nul : "\\0";
size_t const nul_len = strlen(nul);
char* dup = NULL;
size_t dup_len = 0;
char const* begin = str.src;
char const* end = begin;
for (size_t i = 0; i != str.count; ++i, ++end) {
if (!*end) {
size_t const len = end - begin;
size_t const alloc_len = dup_len + len + nul_len;
if (dup) {
dup = realloc(dup, alloc_len + 1);
} else {
dup = malloc(alloc_len + 1);
}
memcpy(dup + dup_len, begin, len);
memcpy(dup + dup_len + len, nul, nul_len);
dup[alloc_len] = '\0';
begin = end + 1;
dup_len = alloc_len;
}
}
if (begin != end) {
size_t const len = end - begin;
size_t const alloc_len = dup_len + len;
if (dup) {
dup = realloc(dup, alloc_len + 1);
} else {
dup = malloc(alloc_len + 1);
}
memcpy(dup + dup_len, begin, len);
dup[alloc_len] = '\0';
}
return dup;
}

View file

@ -1,51 +1,53 @@
find_package(cmocka CONFIG REQUIRED)
cmake_minimum_required(VERSION 3.18 FATAL_ERROR)
find_package(cmocka REQUIRED)
add_executable(
${LIBRARY_NAME}_test
test_${LIBRARY_NAME}
actor_id_tests.c
base_state.c
byte_span_tests.c
cmocka_utils.c
enum_string_tests.c
doc_state.c
doc_tests.c
item_tests.c
group_state.c
list_tests.c
macro_utils.c
main.c
map_tests.c
stack_utils.c
str_utils.c
ported_wasm/basic_tests.c
ported_wasm/suite.c
ported_wasm/sync_tests.c
)
set_target_properties(${LIBRARY_NAME}_test PROPERTIES LINKER_LANGUAGE C)
set_target_properties(test_${LIBRARY_NAME} PROPERTIES LINKER_LANGUAGE C)
if(WIN32)
set(CMOCKA "cmocka::cmocka")
else()
set(CMOCKA "cmocka")
endif()
# \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:${CBINDGEN_INCLUDEDIR}>"
)
target_link_libraries(${LIBRARY_NAME}_test PRIVATE ${CMOCKA} ${LIBRARY_NAME})
target_link_libraries(test_${LIBRARY_NAME} PRIVATE cmocka ${LIBRARY_NAME})
add_dependencies(${LIBRARY_NAME}_test ${BINDINGS_NAME}_artifacts)
add_dependencies(test_${LIBRARY_NAME} ${LIBRARY_NAME}_artifacts)
if(BUILD_SHARED_LIBS AND WIN32)
add_custom_command(
TARGET ${LIBRARY_NAME}_test
TARGET test_${LIBRARY_NAME}
POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:${LIBRARY_NAME}> $<TARGET_FILE_DIR:${LIBRARY_NAME}_test>
COMMENT "Copying the DLL into the tests directory..."
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 ${LIBRARY_NAME}_test COMMAND ${LIBRARY_NAME}_test)
add_test(NAME test_${LIBRARY_NAME} COMMAND test_${LIBRARY_NAME})
add_custom_command(
TARGET ${LIBRARY_NAME}_test
TARGET test_${LIBRARY_NAME}
POST_BUILD
COMMAND
${CMAKE_CTEST_COMMAND} --config $<CONFIG> --output-on-failure

View file

@ -14,126 +14,99 @@
#include "cmocka_utils.h"
#include "str_utils.h"
/**
* \brief State for a group of cmocka test cases.
*/
typedef struct {
/** An actor ID as an array of bytes. */
uint8_t* src;
/** The count of bytes in \p src. */
size_t count;
/** A stack of results. */
AMstack* stack;
/** An actor ID as a hexadecimal string. */
AMbyteSpan str;
} DocState;
size_t count;
} GroupState;
static int group_setup(void** state) {
DocState* doc_state = test_calloc(1, sizeof(DocState));
doc_state->str = AMstr("000102030405060708090a0b0c0d0e0f");
doc_state->count = doc_state->str.count / 2;
doc_state->src = test_calloc(doc_state->count, sizeof(uint8_t));
hex_to_bytes(doc_state->str.src, doc_state->src, doc_state->count);
*state = doc_state;
GroupState* group_state = test_calloc(1, sizeof(GroupState));
group_state->str.src = "000102030405060708090a0b0c0d0e0f";
group_state->str.count = strlen(group_state->str.src);
group_state->count = group_state->str.count / 2;
group_state->src = test_malloc(group_state->count);
hex_to_bytes(group_state->str.src, group_state->src, group_state->count);
*state = group_state;
return 0;
}
static int group_teardown(void** state) {
DocState* doc_state = *state;
test_free(doc_state->src);
AMstackFree(&doc_state->stack);
test_free(doc_state);
GroupState* group_state = *state;
test_free(group_state->src);
test_free(group_state);
return 0;
}
static void test_AMactorIdFromBytes(void** state) {
DocState* doc_state = *state;
AMstack** stack_ptr = &doc_state->stack;
/* Non-empty string. */
AMresult* result = AMstackResult(stack_ptr, AMactorIdFromBytes(doc_state->src, doc_state->count), NULL, NULL);
if (AMresultStatus(result) != AM_STATUS_OK) {
fail_msg_view("%s", AMresultError(result));
}
assert_int_equal(AMresultSize(result), 1);
AMitem* const item = AMresultItem(result);
assert_int_equal(AMitemValType(item), AM_VAL_TYPE_ACTOR_ID);
AMactorId const* actor_id;
assert_true(AMitemToActorId(item, &actor_id));
AMbyteSpan const bytes = AMactorIdBytes(actor_id);
assert_int_equal(bytes.count, doc_state->count);
assert_memory_equal(bytes.src, doc_state->src, bytes.count);
/* Empty array. */
/** \todo Find out if this is intentionally allowed. */
result = AMstackResult(stack_ptr, AMactorIdFromBytes(doc_state->src, 0), NULL, NULL);
if (AMresultStatus(result) != AM_STATUS_OK) {
fail_msg_view("%s", AMresultError(result));
}
/* NULL array. */
result = AMstackResult(stack_ptr, AMactorIdFromBytes(NULL, doc_state->count), NULL, NULL);
if (AMresultStatus(result) == AM_STATUS_OK) {
fail_msg("AMactorId from NULL.");
}
}
static void test_AMactorIdFromStr(void** state) {
DocState* doc_state = *state;
AMstack** stack_ptr = &doc_state->stack;
AMresult* result = AMstackResult(stack_ptr, AMactorIdFromStr(doc_state->str), NULL, NULL);
if (AMresultStatus(result) != AM_STATUS_OK) {
fail_msg_view("%s", AMresultError(result));
}
assert_int_equal(AMresultSize(result), 1);
AMitem* const item = AMresultItem(result);
assert_int_equal(AMitemValType(item), AM_VAL_TYPE_ACTOR_ID);
/* The hexadecimal string should've been decoded as identical bytes. */
AMactorId const* actor_id;
assert_true(AMitemToActorId(item, &actor_id));
AMbyteSpan const bytes = AMactorIdBytes(actor_id);
assert_int_equal(bytes.count, doc_state->count);
assert_memory_equal(bytes.src, doc_state->src, bytes.count);
/* The bytes should've been encoded as an identical hexadecimal string. */
assert_true(AMitemToActorId(item, &actor_id));
AMbyteSpan const str = AMactorIdStr(actor_id);
assert_int_equal(str.count, doc_state->str.count);
assert_memory_equal(str.src, doc_state->str.src, str.count);
}
static void test_AMactorIdInit(void** state) {
DocState* doc_state = *state;
AMstack** stack_ptr = &doc_state->stack;
static void test_AMactorIdInit() {
AMresult* prior_result = NULL;
AMbyteSpan prior_bytes = {NULL, 0};
AMbyteSpan prior_str = {NULL, 0};
AMresult* result = NULL;
for (size_t i = 0; i != 11; ++i) {
AMresult* result = AMstackResult(stack_ptr, AMactorIdInit(), NULL, NULL);
result = AMactorIdInit();
if (AMresultStatus(result) != AM_STATUS_OK) {
fail_msg_view("%s", AMresultError(result));
fail_msg_view("%s", AMerrorMessage(result));
}
assert_int_equal(AMresultSize(result), 1);
AMitem* const item = AMresultItem(result);
assert_int_equal(AMitemValType(item), AM_VAL_TYPE_ACTOR_ID);
AMactorId const* actor_id;
assert_true(AMitemToActorId(item, &actor_id));
AMbyteSpan const bytes = AMactorIdBytes(actor_id);
assert_true(AMitemToActorId(item, &actor_id));
AMbyteSpan const str = AMactorIdStr(actor_id);
AMvalue const value = AMresultValue(result);
assert_int_equal(value.tag, AM_VALUE_ACTOR_ID);
AMbyteSpan const bytes = AMactorIdBytes(value.actor_id);
AMbyteSpan const str = AMactorIdStr(value.actor_id);
if (prior_result) {
size_t const max_byte_count = fmax(bytes.count, prior_bytes.count);
assert_memory_not_equal(bytes.src, prior_bytes.src, max_byte_count);
size_t const max_char_count = fmax(str.count, prior_str.count);
assert_memory_not_equal(str.src, prior_str.src, max_char_count);
AMfree(prior_result);
}
prior_result = result;
prior_bytes = bytes;
prior_str = str;
}
AMfree(result);
}
static void test_AMactorIdInitBytes(void **state) {
GroupState* group_state = *state;
AMresult* const result = AMactorIdInitBytes(group_state->src, group_state->count);
if (AMresultStatus(result) != AM_STATUS_OK) {
fail_msg_view("%s", AMerrorMessage(result));
}
assert_int_equal(AMresultSize(result), 1);
AMvalue const value = AMresultValue(result);
assert_int_equal(value.tag, AM_VALUE_ACTOR_ID);
AMbyteSpan const bytes = AMactorIdBytes(value.actor_id);
assert_int_equal(bytes.count, group_state->count);
assert_memory_equal(bytes.src, group_state->src, bytes.count);
AMfree(result);
}
static void test_AMactorIdInitStr(void **state) {
GroupState* group_state = *state;
AMresult* const result = AMactorIdInitStr(group_state->str);
if (AMresultStatus(result) != AM_STATUS_OK) {
fail_msg_view("%s", AMerrorMessage(result));
}
assert_int_equal(AMresultSize(result), 1);
AMvalue const value = AMresultValue(result);
assert_int_equal(value.tag, AM_VALUE_ACTOR_ID);
/* The hexadecimal string should've been decoded as identical bytes. */
AMbyteSpan const bytes = AMactorIdBytes(value.actor_id);
assert_int_equal(bytes.count, group_state->count);
assert_memory_equal(bytes.src, group_state->src, bytes.count);
/* The bytes should've been encoded as an identical hexadecimal string. */
AMbyteSpan const str = AMactorIdStr(value.actor_id);
assert_int_equal(str.count, group_state->str.count);
assert_memory_equal(str.src, group_state->str.src, str.count);
AMfree(result);
}
int run_actor_id_tests(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_AMactorIdFromBytes),
cmocka_unit_test(test_AMactorIdFromStr),
cmocka_unit_test(test_AMactorIdInit),
cmocka_unit_test(test_AMactorIdInitBytes),
cmocka_unit_test(test_AMactorIdInitStr),
};
return cmocka_run_group_tests(tests, group_setup, group_teardown);

View file

@ -1,17 +0,0 @@
#include <stdlib.h>
/* local */
#include "base_state.h"
int setup_base(void** state) {
BaseState* base_state = calloc(1, sizeof(BaseState));
*state = base_state;
return 0;
}
int teardown_base(void** state) {
BaseState* base_state = *state;
AMstackFree(&base_state->stack);
free(base_state);
return 0;
}

View file

@ -1,39 +0,0 @@
#ifndef TESTS_BASE_STATE_H
#define TESTS_BASE_STATE_H
#include <stdint.h>
/* local */
#include <automerge-c/automerge.h>
#include <automerge-c/utils/stack.h>
/**
* \struct BaseState
* \brief The shared state for one or more cmocka test cases.
*/
typedef struct {
/** A stack of results. */
AMstack* stack;
} BaseState;
/**
* \memberof BaseState
* \brief Sets up the shared state for one or more cmocka test cases.
*
* \param[in,out] state A pointer to a pointer to a `BaseState` struct.
* \pre \p state `!= NULL`.
* \warning The `BaseState` struct returned through \p state must be
* passed to `teardown_base()` in order to avoid a memory leak.
*/
int setup_base(void** state);
/**
* \memberof BaseState
* \brief Tears down the shared state for one or more cmocka test cases.
*
* \param[in] state A pointer to a pointer to a `BaseState` struct.
* \pre \p state `!= NULL`.
*/
int teardown_base(void** state);
#endif /* TESTS_BASE_STATE_H */

View file

@ -1,119 +0,0 @@
#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 <automerge-c/automerge.h>
#include <automerge-c/utils/string.h>
static void test_AMbytes(void** state) {
static char const DATA[] = {0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf};
AMbyteSpan bytes = AMbytes(DATA, sizeof(DATA));
assert_int_equal(bytes.count, sizeof(DATA));
assert_memory_equal(bytes.src, DATA, bytes.count);
assert_ptr_equal(bytes.src, DATA);
/* Empty view */
bytes = AMbytes(DATA, 0);
assert_int_equal(bytes.count, 0);
assert_ptr_equal(bytes.src, DATA);
/* Invalid array */
bytes = AMbytes(NULL, SIZE_MAX);
assert_int_not_equal(bytes.count, SIZE_MAX);
assert_int_equal(bytes.count, 0);
assert_ptr_equal(bytes.src, NULL);
}
static void test_AMstr(void** state) {
AMbyteSpan str = AMstr("abcdefghijkl");
assert_int_equal(str.count, strlen("abcdefghijkl"));
assert_memory_equal(str.src, "abcdefghijkl", str.count);
/* Empty string */
static char const* const EMPTY = "";
str = AMstr(EMPTY);
assert_int_equal(str.count, 0);
assert_ptr_equal(str.src, EMPTY);
/* Invalid string */
str = AMstr(NULL);
assert_int_equal(str.count, 0);
assert_ptr_equal(str.src, NULL);
}
static void test_AMstrCmp(void** state) {
/* Length ordering */
assert_int_equal(AMstrCmp(AMstr("abcdef"), AMstr("abcdefghijkl")), -1);
assert_int_equal(AMstrCmp(AMstr("abcdefghijkl"), AMstr("abcdefghijkl")), 0);
assert_int_equal(AMstrCmp(AMstr("abcdefghijkl"), AMstr("abcdef")), 1);
/* Lexicographical ordering */
assert_int_equal(AMstrCmp(AMstr("abcdef"), AMstr("ghijkl")), -1);
assert_int_equal(AMstrCmp(AMstr("ghijkl"), AMstr("abcdef")), 1);
/* Case ordering */
assert_int_equal(AMstrCmp(AMstr("ABCDEFGHIJKL"), AMstr("abcdefghijkl")), -1);
assert_int_equal(AMstrCmp(AMstr("ABCDEFGHIJKL"), AMstr("ABCDEFGHIJKL")), 0);
assert_int_equal(AMstrCmp(AMstr("abcdefghijkl"), AMstr("ABCDEFGHIJKL")), 1);
assert_int_equal(AMstrCmp(AMstr("ABCDEFGHIJKL"), AMstr("abcdef")), -1);
assert_int_equal(AMstrCmp(AMstr("abcdef"), AMstr("ABCDEFGHIJKL")), 1);
assert_int_equal(AMstrCmp(AMstr("GHIJKL"), AMstr("abcdef")), -1);
assert_int_equal(AMstrCmp(AMstr("abcdef"), AMstr("GHIJKL")), 1);
/* NUL character inclusion */
static char const SRC[] = {'a', 'b', 'c', 'd', 'e', 'f', '\0', 'g', 'h', 'i', 'j', 'k', 'l'};
static AMbyteSpan const NUL_STR = {.src = SRC, .count = 13};
assert_int_equal(AMstrCmp(AMstr("abcdef"), NUL_STR), -1);
assert_int_equal(AMstrCmp(NUL_STR, NUL_STR), 0);
assert_int_equal(AMstrCmp(NUL_STR, AMstr("abcdef")), 1);
/* Empty string */
assert_int_equal(AMstrCmp(AMstr(""), AMstr("abcdefghijkl")), -1);
assert_int_equal(AMstrCmp(AMstr(""), AMstr("")), 0);
assert_int_equal(AMstrCmp(AMstr("abcdefghijkl"), AMstr("")), 1);
/* Invalid string */
assert_int_equal(AMstrCmp(AMstr(NULL), AMstr("abcdefghijkl")), -1);
assert_int_equal(AMstrCmp(AMstr(NULL), AMstr(NULL)), 0);
assert_int_equal(AMstrCmp(AMstr("abcdefghijkl"), AMstr(NULL)), 1);
}
static void test_AMstrdup(void** state) {
static char const SRC[] = {'a', 'b', 'c', '\0', 'd', 'e', 'f', '\0', 'g', 'h', 'i', '\0', 'j', 'k', 'l'};
static AMbyteSpan const NUL_STR = {.src = SRC, .count = 15};
/* Default substitution ("\\0") for NUL */
char* dup = AMstrdup(NUL_STR, NULL);
assert_int_equal(strlen(dup), 18);
assert_string_equal(dup, "abc\\0def\\0ghi\\0jkl");
free(dup);
/* Arbitrary substitution for NUL */
dup = AMstrdup(NUL_STR, ":-O");
assert_int_equal(strlen(dup), 21);
assert_string_equal(dup, "abc:-Odef:-Oghi:-Ojkl");
free(dup);
/* Empty substitution for NUL */
dup = AMstrdup(NUL_STR, "");
assert_int_equal(strlen(dup), 12);
assert_string_equal(dup, "abcdefghijkl");
free(dup);
/* Empty string */
dup = AMstrdup(AMstr(""), NULL);
assert_int_equal(strlen(dup), 0);
assert_string_equal(dup, "");
free(dup);
/* Invalid string */
assert_null(AMstrdup(AMstr(NULL), NULL));
}
int run_byte_span_tests(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_AMbytes),
cmocka_unit_test(test_AMstr),
cmocka_unit_test(test_AMstrCmp),
cmocka_unit_test(test_AMstrdup),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}

View file

@ -1,88 +0,0 @@
#include <setjmp.h>
#include <stdarg.h>
#include <stddef.h>
#include <stdlib.h>
/* third-party */
#include <automerge-c/utils/enum_string.h>
#include <automerge-c/utils/stack_callback_data.h>
#include <automerge-c/utils/string.h>
#include <cmocka.h>
/* local */
#include "cmocka_utils.h"
/**
* \brief Assert that the given expression is true and report failure in terms
* of a line number within a file.
*
* \param[in] c An expression.
* \param[in] file A file's full path string.
* \param[in] line A line number.
*/
#define assert_true_where(c, file, line) _assert_true(cast_ptr_to_largest_integral_type(c), #c, file, line)
/**
* \brief Assert that the given pointer is non-NULL and report failure in terms
* of a line number within a file.
*
* \param[in] c An expression.
* \param[in] file A file's full path string.
* \param[in] line A line number.
*/
#define assert_non_null_where(c, file, line) assert_true_where(c, file, line)
/**
* \brief Forces the test to fail immediately and quit, printing the reason in
* terms of a line number within a file.
*
* \param[in] msg A message string into which \p str is interpolated.
* \param[in] str An owned string.
* \param[in] file A file's full path string.
* \param[in] line A line number.
*/
#define fail_msg_where(msg, str, file, line) \
do { \
print_error("ERROR: " msg "\n", str); \
_fail(file, line); \
} while (0)
/**
* \brief Forces the test to fail immediately and quit, printing the reason in
* terms of a line number within a file.
*
* \param[in] msg A message string into which \p view.src is interpolated.
* \param[in] view A UTF-8 string view as an `AMbyteSpan` struct.
* \param[in] file A file's full path string.
* \param[in] line A line number.
*/
#define fail_msg_view_where(msg, view, file, line) \
do { \
char* const str = AMstrdup(view, NULL); \
print_error("ERROR: " msg "\n", str); \
free(str); \
_fail(file, line); \
} while (0)
bool cmocka_cb(AMstack** stack, void* data) {
assert_non_null(data);
AMstackCallbackData* const sc_data = (AMstackCallbackData*)data;
assert_non_null_where(stack, sc_data->file, sc_data->line);
assert_non_null_where(*stack, sc_data->file, sc_data->line);
assert_non_null_where((*stack)->result, sc_data->file, sc_data->line);
if (AMresultStatus((*stack)->result) != AM_STATUS_OK) {
fail_msg_view_where("%s", AMresultError((*stack)->result), sc_data->file, sc_data->line);
return false;
}
/* Test that the types of all item values are members of the mask. */
AMitems items = AMresultItems((*stack)->result);
AMitem* item = NULL;
while ((item = AMitemsNext(&items, 1)) != NULL) {
AMvalType const tag = AMitemValType(item);
if (!(tag & sc_data->bitmask)) {
fail_msg_where("Unexpected value type `%s`.", AMvalTypeToString(tag), sc_data->file, sc_data->line);
return false;
}
}
return true;
}

View file

@ -1,42 +1,22 @@
#ifndef TESTS_CMOCKA_UTILS_H
#define TESTS_CMOCKA_UTILS_H
#ifndef CMOCKA_UTILS_H
#define CMOCKA_UTILS_H
#include <stdlib.h>
#include <string.h>
/* third-party */
#include <automerge-c/utils/string.h>
#include <cmocka.h>
/* local */
#include "base_state.h"
/**
* \brief Forces the test to fail immediately and quit, printing the reason.
*
* \param[in] msg A message string into which \p view.src is interpolated.
* \param[in] view A UTF-8 string view as an `AMbyteSpan` struct.
* \param[in] view A string view as an `AMbyteSpan` struct.
*/
#define fail_msg_view(msg, view) \
do { \
char* const c_str = AMstrdup(view, NULL); \
print_error("ERROR: " msg "\n", c_str); \
free(c_str); \
fail(); \
} while (0)
#define fail_msg_view(msg, view) do { \
char* const c_str = test_calloc(1, view.count + 1); \
strncpy(c_str, view.src, view.count); \
print_error(msg, c_str); \
test_free(c_str); \
fail(); \
} while (0)
/**
* \brief Validates the top result in a stack based upon the parameters
* specified within the given data structure and reports violations
* using cmocka assertions.
*
* \param[in,out] stack A pointer to a pointer to an `AMstack` struct.
* \param[in] data A pointer to an owned `AMpushData` struct.
* \return `true` if the top `AMresult` struct in \p stack is valid, `false`
* otherwise.
* \pre \p stack `!= NULL`.
* \pre \p data `!= NULL`.
*/
bool cmocka_cb(AMstack** stack, void* data);
#endif /* TESTS_CMOCKA_UTILS_H */
#endif /* CMOCKA_UTILS_H */

View file

@ -1,27 +0,0 @@
#include <setjmp.h>
#include <stdarg.h>
#include <stdlib.h>
/* third-party */
#include <cmocka.h>
/* local */
#include <automerge-c/utils/stack_callback_data.h>
#include "cmocka_utils.h"
#include "doc_state.h"
int setup_doc(void** state) {
DocState* doc_state = test_calloc(1, sizeof(DocState));
setup_base((void**)&doc_state->base_state);
AMitemToDoc(AMstackItem(&doc_state->base_state->stack, AMcreate(NULL), cmocka_cb, AMexpect(AM_VAL_TYPE_DOC)),
&doc_state->doc);
*state = doc_state;
return 0;
}
int teardown_doc(void** state) {
DocState* doc_state = *state;
teardown_base((void**)&doc_state->base_state);
test_free(doc_state);
return 0;
}

View file

@ -1,17 +0,0 @@
#ifndef TESTS_DOC_STATE_H
#define TESTS_DOC_STATE_H
/* local */
#include <automerge-c/automerge.h>
#include "base_state.h"
typedef struct {
BaseState* base_state;
AMdoc* doc;
} DocState;
int setup_doc(void** state);
int teardown_doc(void** state);
#endif /* TESTS_DOC_STATE_H */

View file

@ -9,14 +9,12 @@
/* local */
#include <automerge-c/automerge.h>
#include <automerge-c/utils/stack_callback_data.h>
#include "base_state.h"
#include "cmocka_utils.h"
#include "doc_state.h"
#include "group_state.h"
#include "stack_utils.h"
#include "str_utils.h"
typedef struct {
DocState* doc_state;
GroupState* group_state;
AMbyteSpan actor_id_str;
uint8_t* actor_id_bytes;
size_t actor_id_size;
@ -24,7 +22,7 @@ typedef struct {
static int setup(void** state) {
TestState* test_state = test_calloc(1, sizeof(TestState));
setup_doc((void**)&test_state->doc_state);
group_setup((void**)&test_state->group_state);
test_state->actor_id_str.src = "000102030405060708090a0b0c0d0e0f";
test_state->actor_id_str.count = strlen(test_state->actor_id_str.src);
test_state->actor_id_size = test_state->actor_id_str.count / 2;
@ -36,195 +34,204 @@ static int setup(void** state) {
static int teardown(void** state) {
TestState* test_state = *state;
teardown_doc((void**)&test_state->doc_state);
group_teardown((void**)&test_state->group_state);
test_free(test_state->actor_id_bytes);
test_free(test_state);
return 0;
}
static void test_AMkeys_empty(void** state) {
TestState* test_state = *state;
AMstack** stack_ptr = &test_state->doc_state->base_state->stack;
AMdoc* doc;
assert_true(AMitemToDoc(AMstackItem(stack_ptr, AMcreate(NULL), cmocka_cb, AMexpect(AM_VAL_TYPE_DOC)), &doc));
AMitems forward = AMstackItems(stack_ptr, AMkeys(doc, AM_ROOT, NULL), cmocka_cb, AMexpect(AM_VAL_TYPE_VOID));
assert_int_equal(AMitemsSize(&forward), 0);
AMitems reverse = AMitemsReversed(&forward);
assert_int_equal(AMitemsSize(&reverse), 0);
assert_null(AMitemsNext(&forward, 1));
assert_null(AMitemsPrev(&forward, 1));
assert_null(AMitemsNext(&reverse, 1));
assert_null(AMitemsPrev(&reverse, 1));
static void test_AMkeys_empty() {
AMresultStack* stack = NULL;
AMdoc* const doc = AMpush(&stack, AMcreate(NULL), AM_VALUE_DOC, cmocka_cb).doc;
AMstrs forward = AMpush(&stack,
AMkeys(doc, AM_ROOT, NULL),
AM_VALUE_STRS,
cmocka_cb).strs;
assert_int_equal(AMstrsSize(&forward), 0);
AMstrs reverse = AMstrsReversed(&forward);
assert_int_equal(AMstrsSize(&reverse), 0);
assert_null(AMstrsNext(&forward, 1).src);
assert_null(AMstrsPrev(&forward, 1).src);
assert_null(AMstrsNext(&reverse, 1).src);
assert_null(AMstrsPrev(&reverse, 1).src);
AMfreeStack(&stack);
}
static void test_AMkeys_list(void** state) {
TestState* test_state = *state;
AMstack** stack_ptr = &test_state->doc_state->base_state->stack;
AMdoc* doc;
assert_true(AMitemToDoc(AMstackItem(stack_ptr, AMcreate(NULL), cmocka_cb, AMexpect(AM_VAL_TYPE_DOC)), &doc));
AMobjId const* const list =
AMitemObjId(AMstackItem(stack_ptr, AMmapPutObject(doc, AM_ROOT, AMstr("list"), AM_OBJ_TYPE_LIST), cmocka_cb,
AMexpect(AM_VAL_TYPE_OBJ_TYPE)));
AMstackItem(NULL, AMlistPutInt(doc, list, 0, true, 0), cmocka_cb, AMexpect(AM_VAL_TYPE_VOID));
AMstackItem(NULL, AMlistPutInt(doc, list, 1, true, 0), cmocka_cb, AMexpect(AM_VAL_TYPE_VOID));
AMstackItem(NULL, AMlistPutInt(doc, list, 2, true, 0), cmocka_cb, AMexpect(AM_VAL_TYPE_VOID));
AMitems forward = AMstackItems(stack_ptr, AMkeys(doc, list, NULL), cmocka_cb, AMexpect(AM_VAL_TYPE_STR));
assert_int_equal(AMitemsSize(&forward), 3);
AMitems reverse = AMitemsReversed(&forward);
assert_int_equal(AMitemsSize(&reverse), 3);
static void test_AMkeys_list() {
AMresultStack* stack = NULL;
AMdoc* const doc = AMpush(&stack, AMcreate(NULL), AM_VALUE_DOC, cmocka_cb).doc;
AMobjId const* const list = AMpush(
&stack,
AMmapPutObject(doc, AM_ROOT, AMstr("list"), AM_OBJ_TYPE_LIST),
AM_VALUE_OBJ_ID,
cmocka_cb).obj_id;
AMfree(AMlistPutInt(doc, list, 0, true, 0));
AMfree(AMlistPutInt(doc, list, 1, true, 0));
AMfree(AMlistPutInt(doc, list, 2, true, 0));
AMstrs forward = AMpush(&stack,
AMkeys(doc, list, NULL),
AM_VALUE_STRS,
cmocka_cb).strs;
assert_int_equal(AMstrsSize(&forward), 3);
AMstrs reverse = AMstrsReversed(&forward);
assert_int_equal(AMstrsSize(&reverse), 3);
/* Forward iterator forward. */
AMbyteSpan str;
assert_true(AMitemToStr(AMitemsNext(&forward, 1), &str));
AMbyteSpan str = AMstrsNext(&forward, 1);
assert_ptr_equal(strstr(str.src, "2@"), str.src);
assert_true(AMitemToStr(AMitemsNext(&forward, 1), &str));
str = AMstrsNext(&forward, 1);
assert_ptr_equal(strstr(str.src, "3@"), str.src);
assert_true(AMitemToStr(AMitemsNext(&forward, 1), &str));
str = AMstrsNext(&forward, 1);
assert_ptr_equal(strstr(str.src, "4@"), str.src);
assert_null(AMitemsNext(&forward, 1));
assert_null(AMstrsNext(&forward, 1).src);
// /* Forward iterator reverse. */
assert_true(AMitemToStr(AMitemsPrev(&forward, 1), &str));
str = AMstrsPrev(&forward, 1);
assert_ptr_equal(strstr(str.src, "4@"), str.src);
assert_true(AMitemToStr(AMitemsPrev(&forward, 1), &str));
str = AMstrsPrev(&forward, 1);
assert_ptr_equal(strstr(str.src, "3@"), str.src);
assert_true(AMitemToStr(AMitemsPrev(&forward, 1), &str));
str = AMstrsPrev(&forward, 1);
assert_ptr_equal(strstr(str.src, "2@"), str.src);
assert_null(AMitemsPrev(&forward, 1));
assert_null(AMstrsPrev(&forward, 1).src);
/* Reverse iterator forward. */
assert_true(AMitemToStr(AMitemsNext(&reverse, 1), &str));
str = AMstrsNext(&reverse, 1);
assert_ptr_equal(strstr(str.src, "4@"), str.src);
assert_true(AMitemToStr(AMitemsNext(&reverse, 1), &str));
str = AMstrsNext(&reverse, 1);
assert_ptr_equal(strstr(str.src, "3@"), str.src);
assert_true(AMitemToStr(AMitemsNext(&reverse, 1), &str));
str = AMstrsNext(&reverse, 1);
assert_ptr_equal(strstr(str.src, "2@"), str.src);
assert_null(AMitemsNext(&reverse, 1));
assert_null(AMstrsNext(&reverse, 1).src);
/* Reverse iterator reverse. */
assert_true(AMitemToStr(AMitemsPrev(&reverse, 1), &str));
str = AMstrsPrev(&reverse, 1);
assert_ptr_equal(strstr(str.src, "2@"), str.src);
assert_true(AMitemToStr(AMitemsPrev(&reverse, 1), &str));
str = AMstrsPrev(&reverse, 1);
assert_ptr_equal(strstr(str.src, "3@"), str.src);
assert_true(AMitemToStr(AMitemsPrev(&reverse, 1), &str));
str = AMstrsPrev(&reverse, 1);
assert_ptr_equal(strstr(str.src, "4@"), str.src);
assert_null(AMitemsPrev(&reverse, 1));
assert_null(AMstrsPrev(&reverse, 1).src);
AMfreeStack(&stack);
}
static void test_AMkeys_map(void** state) {
TestState* test_state = *state;
AMstack** stack_ptr = &test_state->doc_state->base_state->stack;
AMdoc* doc;
assert_true(AMitemToDoc(AMstackItem(stack_ptr, AMcreate(NULL), cmocka_cb, AMexpect(AM_VAL_TYPE_DOC)), &doc));
AMstackItem(NULL, AMmapPutInt(doc, AM_ROOT, AMstr("one"), 1), cmocka_cb, AMexpect(AM_VAL_TYPE_VOID));
AMstackItem(NULL, AMmapPutInt(doc, AM_ROOT, AMstr("two"), 2), cmocka_cb, AMexpect(AM_VAL_TYPE_VOID));
AMstackItem(NULL, AMmapPutInt(doc, AM_ROOT, AMstr("three"), 3), cmocka_cb, AMexpect(AM_VAL_TYPE_VOID));
AMitems forward = AMstackItems(stack_ptr, AMkeys(doc, AM_ROOT, NULL), cmocka_cb, AMexpect(AM_VAL_TYPE_STR));
assert_int_equal(AMitemsSize(&forward), 3);
AMitems reverse = AMitemsReversed(&forward);
assert_int_equal(AMitemsSize(&reverse), 3);
static void test_AMkeys_map() {
AMresultStack* stack = NULL;
AMdoc* const doc = AMpush(&stack, AMcreate(NULL), AM_VALUE_DOC, cmocka_cb).doc;
AMfree(AMmapPutInt(doc, AM_ROOT, AMstr("one"), 1));
AMfree(AMmapPutInt(doc, AM_ROOT, AMstr("two"), 2));
AMfree(AMmapPutInt(doc, AM_ROOT, AMstr("three"), 3));
AMstrs forward = AMpush(&stack,
AMkeys(doc, AM_ROOT, NULL),
AM_VALUE_STRS,
cmocka_cb).strs;
assert_int_equal(AMstrsSize(&forward), 3);
AMstrs reverse = AMstrsReversed(&forward);
assert_int_equal(AMstrsSize(&reverse), 3);
/* Forward iterator forward. */
AMbyteSpan str;
assert_true(AMitemToStr(AMitemsNext(&forward, 1), &str));
AMbyteSpan str = AMstrsNext(&forward, 1);
assert_int_equal(str.count, 3);
assert_memory_equal(str.src, "one", str.count);
assert_true(AMitemToStr(AMitemsNext(&forward, 1), &str));
str = AMstrsNext(&forward, 1);
assert_int_equal(str.count, 5);
assert_memory_equal(str.src, "three", str.count);
assert_true(AMitemToStr(AMitemsNext(&forward, 1), &str));
str = AMstrsNext(&forward, 1);
assert_int_equal(str.count, 3);
assert_memory_equal(str.src, "two", str.count);
assert_null(AMitemsNext(&forward, 1));
assert_null(AMstrsNext(&forward, 1).src);
/* Forward iterator reverse. */
assert_true(AMitemToStr(AMitemsPrev(&forward, 1), &str));
str = AMstrsPrev(&forward, 1);
assert_int_equal(str.count, 3);
assert_memory_equal(str.src, "two", str.count);
assert_true(AMitemToStr(AMitemsPrev(&forward, 1), &str));
str = AMstrsPrev(&forward, 1);
assert_int_equal(str.count, 5);
assert_memory_equal(str.src, "three", str.count);
assert_true(AMitemToStr(AMitemsPrev(&forward, 1), &str));
str = AMstrsPrev(&forward, 1);
assert_int_equal(str.count, 3);
assert_memory_equal(str.src, "one", str.count);
assert_null(AMitemsPrev(&forward, 1));
assert_null(AMstrsPrev(&forward, 1).src);
/* Reverse iterator forward. */
assert_true(AMitemToStr(AMitemsNext(&reverse, 1), &str));
str = AMstrsNext(&reverse, 1);
assert_int_equal(str.count, 3);
assert_memory_equal(str.src, "two", str.count);
assert_true(AMitemToStr(AMitemsNext(&reverse, 1), &str));
str = AMstrsNext(&reverse, 1);
assert_int_equal(str.count, 5);
assert_memory_equal(str.src, "three", str.count);
assert_true(AMitemToStr(AMitemsNext(&reverse, 1), &str));
str = AMstrsNext(&reverse, 1);
assert_int_equal(str.count, 3);
assert_memory_equal(str.src, "one", str.count);
assert_null(AMitemsNext(&reverse, 1));
assert_null(AMstrsNext(&reverse, 1).src);
/* Reverse iterator reverse. */
assert_true(AMitemToStr(AMitemsPrev(&reverse, 1), &str));
str = AMstrsPrev(&reverse, 1);
assert_int_equal(str.count, 3);
assert_memory_equal(str.src, "one", str.count);
assert_true(AMitemToStr(AMitemsPrev(&reverse, 1), &str));
str = AMstrsPrev(&reverse, 1);
assert_int_equal(str.count, 5);
assert_memory_equal(str.src, "three", str.count);
assert_true(AMitemToStr(AMitemsPrev(&reverse, 1), &str));
str = AMstrsPrev(&reverse, 1);
assert_int_equal(str.count, 3);
assert_memory_equal(str.src, "two", str.count);
assert_null(AMitemsPrev(&reverse, 1));
assert_null(AMstrsPrev(&reverse, 1).src);
AMfreeStack(&stack);
}
static void test_AMputActor_bytes(void** state) {
static void test_AMputActor_bytes(void **state) {
TestState* test_state = *state;
AMstack** stack_ptr = &test_state->doc_state->base_state->stack;
AMactorId const* actor_id;
assert_true(AMitemToActorId(
AMstackItem(stack_ptr, AMactorIdFromBytes(test_state->actor_id_bytes, test_state->actor_id_size), cmocka_cb,
AMexpect(AM_VAL_TYPE_ACTOR_ID)),
&actor_id));
AMstackItem(NULL, AMsetActorId(test_state->doc_state->doc, actor_id), cmocka_cb, AMexpect(AM_VAL_TYPE_VOID));
assert_true(AMitemToActorId(
AMstackItem(stack_ptr, AMgetActorId(test_state->doc_state->doc), cmocka_cb, AMexpect(AM_VAL_TYPE_ACTOR_ID)),
&actor_id));
AMactorId const* actor_id = AMpush(&test_state->group_state->stack,
AMactorIdInitBytes(
test_state->actor_id_bytes,
test_state->actor_id_size),
AM_VALUE_ACTOR_ID,
cmocka_cb).actor_id;
AMfree(AMsetActorId(test_state->group_state->doc, actor_id));
actor_id = AMpush(&test_state->group_state->stack,
AMgetActorId(test_state->group_state->doc),
AM_VALUE_ACTOR_ID,
cmocka_cb).actor_id;
AMbyteSpan const bytes = AMactorIdBytes(actor_id);
assert_int_equal(bytes.count, test_state->actor_id_size);
assert_memory_equal(bytes.src, test_state->actor_id_bytes, bytes.count);
}
static void test_AMputActor_str(void** state) {
static void test_AMputActor_str(void **state) {
TestState* test_state = *state;
AMstack** stack_ptr = &test_state->doc_state->base_state->stack;
AMactorId const* actor_id;
assert_true(AMitemToActorId(
AMstackItem(stack_ptr, AMactorIdFromStr(test_state->actor_id_str), cmocka_cb, AMexpect(AM_VAL_TYPE_ACTOR_ID)),
&actor_id));
AMstackItem(NULL, AMsetActorId(test_state->doc_state->doc, actor_id), cmocka_cb, AMexpect(AM_VAL_TYPE_VOID));
assert_true(AMitemToActorId(
AMstackItem(stack_ptr, AMgetActorId(test_state->doc_state->doc), cmocka_cb, AMexpect(AM_VAL_TYPE_ACTOR_ID)),
&actor_id));
AMactorId const* actor_id = AMpush(&test_state->group_state->stack,
AMactorIdInitStr(test_state->actor_id_str),
AM_VALUE_ACTOR_ID,
cmocka_cb).actor_id;
AMfree(AMsetActorId(test_state->group_state->doc, actor_id));
actor_id = AMpush(&test_state->group_state->stack,
AMgetActorId(test_state->group_state->doc),
AM_VALUE_ACTOR_ID,
cmocka_cb).actor_id;
AMbyteSpan const str = AMactorIdStr(actor_id);
assert_int_equal(str.count, test_state->actor_id_str.count);
assert_memory_equal(str.src, test_state->actor_id_str.src, str.count);
}
static void test_AMspliceText(void** state) {
TestState* test_state = *state;
AMstack** stack_ptr = &test_state->doc_state->base_state->stack;
AMdoc* doc;
assert_true(AMitemToDoc(AMstackItem(stack_ptr, AMcreate(NULL), cmocka_cb, AMexpect(AM_VAL_TYPE_DOC)), &doc));
AMobjId const* const text =
AMitemObjId(AMstackItem(stack_ptr, AMmapPutObject(doc, AM_ROOT, AMstr("text"), AM_OBJ_TYPE_TEXT), cmocka_cb,
AMexpect(AM_VAL_TYPE_OBJ_TYPE)));
AMstackItem(NULL, AMspliceText(doc, text, 0, 0, AMstr("one + ")), cmocka_cb, AMexpect(AM_VAL_TYPE_VOID));
AMstackItem(NULL, AMspliceText(doc, text, 4, 2, AMstr("two = ")), cmocka_cb, AMexpect(AM_VAL_TYPE_VOID));
AMstackItem(NULL, AMspliceText(doc, text, 8, 2, AMstr("three")), cmocka_cb, AMexpect(AM_VAL_TYPE_VOID));
AMbyteSpan str;
assert_true(
AMitemToStr(AMstackItem(stack_ptr, AMtext(doc, text, NULL), cmocka_cb, AMexpect(AM_VAL_TYPE_STR)), &str));
assert_int_equal(str.count, strlen("one two three"));
assert_memory_equal(str.src, "one two three", str.count);
static void test_AMspliceText() {
AMresultStack* stack = NULL;
AMdoc* const doc = AMpush(&stack, AMcreate(NULL), AM_VALUE_DOC, cmocka_cb).doc;
AMobjId const* const text = AMpush(&stack,
AMmapPutObject(doc, AM_ROOT, AMstr("text"), AM_OBJ_TYPE_TEXT),
AM_VALUE_OBJ_ID,
cmocka_cb).obj_id;
AMfree(AMspliceText(doc, text, 0, 0, AMstr("one + ")));
AMfree(AMspliceText(doc, text, 4, 2, AMstr("two = ")));
AMfree(AMspliceText(doc, text, 8, 2, AMstr("three")));
AMbyteSpan const str = AMpush(&stack,
AMtext(doc, text, NULL),
AM_VALUE_STR,
cmocka_cb).str;
static char const* const STR_VALUE = "one two three";
assert_int_equal(str.count, strlen(STR_VALUE));
assert_memory_equal(str.src, STR_VALUE, str.count);
AMfreeStack(&stack);
}
int run_doc_tests(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test_setup_teardown(test_AMkeys_empty, setup, teardown),
cmocka_unit_test_setup_teardown(test_AMkeys_list, setup, teardown),
cmocka_unit_test_setup_teardown(test_AMkeys_map, setup, teardown),
cmocka_unit_test(test_AMkeys_empty),
cmocka_unit_test(test_AMkeys_list),
cmocka_unit_test(test_AMkeys_map),
cmocka_unit_test_setup_teardown(test_AMputActor_bytes, setup, teardown),
cmocka_unit_test_setup_teardown(test_AMputActor_str, setup, teardown),
cmocka_unit_test_setup_teardown(test_AMspliceText, setup, teardown),
cmocka_unit_test(test_AMspliceText),
};
return cmocka_run_group_tests(tests, NULL, NULL);

View file

@ -1,148 +0,0 @@
#include <setjmp.h>
#include <stdarg.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
/* third-party */
#include <cmocka.h>
/* local */
#include <automerge-c/automerge.h>
#include <automerge-c/utils/enum_string.h>
#define assert_to_string(function, tag) assert_string_equal(function(tag), #tag)
#define assert_from_string(function, type, tag) \
do { \
type out; \
assert_true(function(&out, #tag)); \
assert_int_equal(out, tag); \
} while (0)
static void test_AMidxTypeToString(void** state) {
assert_to_string(AMidxTypeToString, AM_IDX_TYPE_DEFAULT);
assert_to_string(AMidxTypeToString, AM_IDX_TYPE_KEY);
assert_to_string(AMidxTypeToString, AM_IDX_TYPE_POS);
/* Zero tag */
assert_string_equal(AMidxTypeToString(0), "AM_IDX_TYPE_DEFAULT");
/* Invalid tag */
assert_string_equal(AMidxTypeToString(-1), "???");
}
static void test_AMidxTypeFromString(void** state) {
assert_from_string(AMidxTypeFromString, AMidxType, AM_IDX_TYPE_DEFAULT);
assert_from_string(AMidxTypeFromString, AMidxType, AM_IDX_TYPE_KEY);
assert_from_string(AMidxTypeFromString, AMidxType, AM_IDX_TYPE_POS);
/* Invalid tag */
AMidxType out = -1;
assert_false(AMidxTypeFromString(&out, "???"));
assert_int_equal(out, (AMidxType)-1);
}
static void test_AMobjTypeToString(void** state) {
assert_to_string(AMobjTypeToString, AM_OBJ_TYPE_DEFAULT);
assert_to_string(AMobjTypeToString, AM_OBJ_TYPE_LIST);
assert_to_string(AMobjTypeToString, AM_OBJ_TYPE_MAP);
assert_to_string(AMobjTypeToString, AM_OBJ_TYPE_TEXT);
/* Zero tag */
assert_string_equal(AMobjTypeToString(0), "AM_OBJ_TYPE_DEFAULT");
/* Invalid tag */
assert_string_equal(AMobjTypeToString(-1), "???");
}
static void test_AMobjTypeFromString(void** state) {
assert_from_string(AMobjTypeFromString, AMobjType, AM_OBJ_TYPE_DEFAULT);
assert_from_string(AMobjTypeFromString, AMobjType, AM_OBJ_TYPE_LIST);
assert_from_string(AMobjTypeFromString, AMobjType, AM_OBJ_TYPE_MAP);
assert_from_string(AMobjTypeFromString, AMobjType, AM_OBJ_TYPE_TEXT);
/* Invalid tag */
AMobjType out = -1;
assert_false(AMobjTypeFromString(&out, "???"));
assert_int_equal(out, (AMobjType)-1);
}
static void test_AMstatusToString(void** state) {
assert_to_string(AMstatusToString, AM_STATUS_ERROR);
assert_to_string(AMstatusToString, AM_STATUS_INVALID_RESULT);
assert_to_string(AMstatusToString, AM_STATUS_OK);
/* Zero tag */
assert_string_equal(AMstatusToString(0), "AM_STATUS_OK");
/* Invalid tag */
assert_string_equal(AMstatusToString(-1), "???");
}
static void test_AMstatusFromString(void** state) {
assert_from_string(AMstatusFromString, AMstatus, AM_STATUS_ERROR);
assert_from_string(AMstatusFromString, AMstatus, AM_STATUS_INVALID_RESULT);
assert_from_string(AMstatusFromString, AMstatus, AM_STATUS_OK);
/* Invalid tag */
AMstatus out = -1;
assert_false(AMstatusFromString(&out, "???"));
assert_int_equal(out, (AMstatus)-1);
}
static void test_AMvalTypeToString(void** state) {
assert_to_string(AMvalTypeToString, AM_VAL_TYPE_ACTOR_ID);
assert_to_string(AMvalTypeToString, AM_VAL_TYPE_BOOL);
assert_to_string(AMvalTypeToString, AM_VAL_TYPE_BYTES);
assert_to_string(AMvalTypeToString, AM_VAL_TYPE_CHANGE);
assert_to_string(AMvalTypeToString, AM_VAL_TYPE_CHANGE_HASH);
assert_to_string(AMvalTypeToString, AM_VAL_TYPE_COUNTER);
assert_to_string(AMvalTypeToString, AM_VAL_TYPE_DEFAULT);
assert_to_string(AMvalTypeToString, AM_VAL_TYPE_DOC);
assert_to_string(AMvalTypeToString, AM_VAL_TYPE_F64);
assert_to_string(AMvalTypeToString, AM_VAL_TYPE_INT);
assert_to_string(AMvalTypeToString, AM_VAL_TYPE_NULL);
assert_to_string(AMvalTypeToString, AM_VAL_TYPE_OBJ_TYPE);
assert_to_string(AMvalTypeToString, AM_VAL_TYPE_STR);
assert_to_string(AMvalTypeToString, AM_VAL_TYPE_SYNC_HAVE);
assert_to_string(AMvalTypeToString, AM_VAL_TYPE_SYNC_MESSAGE);
assert_to_string(AMvalTypeToString, AM_VAL_TYPE_SYNC_STATE);
assert_to_string(AMvalTypeToString, AM_VAL_TYPE_TIMESTAMP);
assert_to_string(AMvalTypeToString, AM_VAL_TYPE_UINT);
assert_to_string(AMvalTypeToString, AM_VAL_TYPE_UNKNOWN);
assert_to_string(AMvalTypeToString, AM_VAL_TYPE_VOID);
/* Zero tag */
assert_string_equal(AMvalTypeToString(0), "AM_VAL_TYPE_DEFAULT");
/* Invalid tag */
assert_string_equal(AMvalTypeToString(-1), "???");
}
static void test_AMvalTypeFromString(void** state) {
assert_from_string(AMvalTypeFromString, AMvalType, AM_VAL_TYPE_ACTOR_ID);
assert_from_string(AMvalTypeFromString, AMvalType, AM_VAL_TYPE_BOOL);
assert_from_string(AMvalTypeFromString, AMvalType, AM_VAL_TYPE_BYTES);
assert_from_string(AMvalTypeFromString, AMvalType, AM_VAL_TYPE_CHANGE);
assert_from_string(AMvalTypeFromString, AMvalType, AM_VAL_TYPE_CHANGE_HASH);
assert_from_string(AMvalTypeFromString, AMvalType, AM_VAL_TYPE_COUNTER);
assert_from_string(AMvalTypeFromString, AMvalType, AM_VAL_TYPE_DEFAULT);
assert_from_string(AMvalTypeFromString, AMvalType, AM_VAL_TYPE_DOC);
assert_from_string(AMvalTypeFromString, AMvalType, AM_VAL_TYPE_F64);
assert_from_string(AMvalTypeFromString, AMvalType, AM_VAL_TYPE_INT);
assert_from_string(AMvalTypeFromString, AMvalType, AM_VAL_TYPE_NULL);
assert_from_string(AMvalTypeFromString, AMvalType, AM_VAL_TYPE_OBJ_TYPE);
assert_from_string(AMvalTypeFromString, AMvalType, AM_VAL_TYPE_STR);
assert_from_string(AMvalTypeFromString, AMvalType, AM_VAL_TYPE_SYNC_HAVE);
assert_from_string(AMvalTypeFromString, AMvalType, AM_VAL_TYPE_SYNC_MESSAGE);
assert_from_string(AMvalTypeFromString, AMvalType, AM_VAL_TYPE_SYNC_STATE);
assert_from_string(AMvalTypeFromString, AMvalType, AM_VAL_TYPE_TIMESTAMP);
assert_from_string(AMvalTypeFromString, AMvalType, AM_VAL_TYPE_UINT);
assert_from_string(AMvalTypeFromString, AMvalType, AM_VAL_TYPE_UNKNOWN);
assert_from_string(AMvalTypeFromString, AMvalType, AM_VAL_TYPE_VOID);
/* Invalid tag */
AMvalType out = -1;
assert_false(AMvalTypeFromString(&out, "???"));
assert_int_equal(out, (AMvalType)-1);
}
int run_enum_string_tests(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_AMidxTypeToString), cmocka_unit_test(test_AMidxTypeFromString),
cmocka_unit_test(test_AMobjTypeToString), cmocka_unit_test(test_AMobjTypeFromString),
cmocka_unit_test(test_AMstatusToString), cmocka_unit_test(test_AMstatusFromString),
cmocka_unit_test(test_AMvalTypeToString), cmocka_unit_test(test_AMvalTypeFromString),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}

View file

@ -0,0 +1,27 @@
#include <setjmp.h>
#include <stdarg.h>
#include <stdlib.h>
/* third-party */
#include <cmocka.h>
/* local */
#include "group_state.h"
#include "stack_utils.h"
int group_setup(void** state) {
GroupState* group_state = test_calloc(1, sizeof(GroupState));
group_state->doc = AMpush(&group_state->stack,
AMcreate(NULL),
AM_VALUE_DOC,
cmocka_cb).doc;
*state = group_state;
return 0;
}
int group_teardown(void** state) {
GroupState* group_state = *state;
AMfreeStack(&group_state->stack);
test_free(group_state);
return 0;
}

View file

@ -0,0 +1,16 @@
#ifndef GROUP_STATE_H
#define GROUP_STATE_H
/* local */
#include <automerge-c/automerge.h>
typedef struct {
AMresultStack* stack;
AMdoc* doc;
} GroupState;
int group_setup(void** state);
int group_teardown(void** state);
#endif /* GROUP_STATE_H */

View file

@ -1,94 +0,0 @@
#include <setjmp.h>
#include <stdarg.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
/* third-party */
#include <cmocka.h>
/* local */
#include <automerge-c/automerge.h>
#include <automerge-c/utils/stack_callback_data.h>
#include "cmocka_utils.h"
#include "doc_state.h"
static void test_AMitemResult(void** state) {
enum { ITEM_COUNT = 1000 };
DocState* doc_state = *state;
AMstack** stack_ptr = &doc_state->base_state->stack;
/* Append the strings to a list so that they'll be in numerical order. */
AMobjId const* const list =
AMitemObjId(AMstackItem(stack_ptr, AMmapPutObject(doc_state->doc, AM_ROOT, AMstr("list"), AM_OBJ_TYPE_LIST),
cmocka_cb, AMexpect(AM_VAL_TYPE_OBJ_TYPE)));
for (size_t pos = 0; pos != ITEM_COUNT; ++pos) {
size_t const count = snprintf(NULL, 0, "%zu", pos);
char* const src = test_calloc(count + 1, sizeof(char));
assert_int_equal(sprintf(src, "%zu", pos), count);
AMstackItem(NULL, AMlistPutStr(doc_state->doc, list, pos, true, AMbytes(src, count)), cmocka_cb,
AMexpect(AM_VAL_TYPE_VOID));
test_free(src);
}
/* Get an item iterator. */
AMitems items = AMstackItems(stack_ptr, AMlistRange(doc_state->doc, list, 0, SIZE_MAX, NULL), cmocka_cb,
AMexpect(AM_VAL_TYPE_STR));
/* Get the item iterator's result so that it can be freed later. */
AMresult const* const items_result = (*stack_ptr)->result;
/* Iterate over all of the items and copy their pointers into an array. */
AMitem* item_ptrs[ITEM_COUNT] = {NULL};
AMitem* item = NULL;
for (size_t pos = 0; (item = AMitemsNext(&items, 1)) != NULL; ++pos) {
/* The item's reference count should be 1. */
assert_int_equal(AMitemRefCount(item), 1);
if (pos & 1) {
/* Create a redundant result for an odd item. */
AMitem* const new_item = AMstackItem(stack_ptr, AMitemResult(item), cmocka_cb, AMexpect(AM_VAL_TYPE_STR));
/* The item's old and new pointers will never match. */
assert_ptr_not_equal(new_item, item);
/* The item's reference count will have been incremented. */
assert_int_equal(AMitemRefCount(item), 2);
assert_int_equal(AMitemRefCount(new_item), 2);
/* The item's old and new indices should match. */
assert_int_equal(AMitemIdxType(item), AMitemIdxType(new_item));
assert_int_equal(AMitemIdxType(item), AM_IDX_TYPE_POS);
size_t pos, new_pos;
assert_true(AMitemPos(item, &pos));
assert_true(AMitemPos(new_item, &new_pos));
assert_int_equal(pos, new_pos);
/* The item's old and new object IDs should match. */
AMobjId const* const obj_id = AMitemObjId(item);
AMobjId const* const new_obj_id = AMitemObjId(new_item);
assert_true(AMobjIdEqual(obj_id, new_obj_id));
/* The item's old and new value types should match. */
assert_int_equal(AMitemValType(item), AMitemValType(new_item));
/* The item's old and new string values should match. */
AMbyteSpan str;
assert_true(AMitemToStr(item, &str));
AMbyteSpan new_str;
assert_true(AMitemToStr(new_item, &new_str));
assert_int_equal(str.count, new_str.count);
assert_memory_equal(str.src, new_str.src, new_str.count);
/* The item's old and new object IDs are one and the same. */
assert_ptr_equal(obj_id, new_obj_id);
/* The item's old and new string values are one and the same. */
assert_ptr_equal(str.src, new_str.src);
/* Save the item's new pointer. */
item_ptrs[pos] = new_item;
}
}
/* Free the item iterator's result. */
AMresultFree(AMstackPop(stack_ptr, items_result));
/* An odd item's reference count should be 1 again. */
for (size_t pos = 1; pos < ITEM_COUNT; pos += 2) {
assert_int_equal(AMitemRefCount(item_ptrs[pos]), 1);
}
}
int run_item_tests(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_AMitemResult),
};
return cmocka_run_group_tests(tests, setup_doc, teardown_doc);
}

View file

@ -11,417 +11,367 @@
/* local */
#include <automerge-c/automerge.h>
#include <automerge-c/utils/stack_callback_data.h>
#include "base_state.h"
#include "cmocka_utils.h"
#include "doc_state.h"
#include "group_state.h"
#include "macro_utils.h"
#include "stack_utils.h"
static void test_AMlistIncrement(void** state) {
DocState* doc_state = *state;
AMstack** stack_ptr = &doc_state->base_state->stack;
AMobjId const* const list =
AMitemObjId(AMstackItem(stack_ptr, AMmapPutObject(doc_state->doc, AM_ROOT, AMstr("list"), AM_OBJ_TYPE_LIST),
cmocka_cb, AMexpect(AM_VAL_TYPE_OBJ_TYPE)));
AMstackItem(NULL, AMlistPutCounter(doc_state->doc, list, 0, true, 0), cmocka_cb, AMexpect(AM_VAL_TYPE_VOID));
int64_t counter;
assert_true(AMitemToCounter(
AMstackItem(stack_ptr, AMlistGet(doc_state->doc, list, 0, NULL), cmocka_cb, AMexpect(AM_VAL_TYPE_COUNTER)),
&counter));
assert_int_equal(counter, 0);
AMresultFree(AMstackPop(stack_ptr, NULL));
AMstackItem(NULL, AMlistIncrement(doc_state->doc, list, 0, 3), cmocka_cb, AMexpect(AM_VAL_TYPE_VOID));
assert_true(AMitemToCounter(
AMstackItem(stack_ptr, AMlistGet(doc_state->doc, list, 0, NULL), cmocka_cb, AMexpect(AM_VAL_TYPE_COUNTER)),
&counter));
assert_int_equal(counter, 3);
AMresultFree(AMstackPop(stack_ptr, NULL));
GroupState* group_state = *state;
AMobjId const* const list = AMpush(
&group_state->stack,
AMmapPutObject(group_state->doc, AM_ROOT, AMstr("list"), AM_OBJ_TYPE_LIST),
AM_VALUE_OBJ_ID,
cmocka_cb).obj_id;
AMfree(AMlistPutCounter(group_state->doc, list, 0, true, 0));
assert_int_equal(AMpush(&group_state->stack,
AMlistGet(group_state->doc, list, 0, NULL),
AM_VALUE_COUNTER,
cmocka_cb).counter, 0);
AMfree(AMpop(&group_state->stack));
AMfree(AMlistIncrement(group_state->doc, list, 0, 3));
assert_int_equal(AMpush(&group_state->stack,
AMlistGet(group_state->doc, list, 0, NULL),
AM_VALUE_COUNTER,
cmocka_cb).counter, 3);
AMfree(AMpop(&group_state->stack));
}
#define test_AMlistPut(suffix, mode) test_AMlistPut##suffix##_##mode
#define test_AMlistPut(suffix, mode) test_AMlistPut ## suffix ## _ ## mode
#define static_void_test_AMlistPut(suffix, mode, type, scalar_value) \
static void test_AMlistPut##suffix##_##mode(void** state) { \
DocState* doc_state = *state; \
AMstack** stack_ptr = &doc_state->base_state->stack; \
AMobjId const* const list = AMitemObjId( \
AMstackItem(stack_ptr, AMmapPutObject(doc_state->doc, AM_ROOT, AMstr("list"), AM_OBJ_TYPE_LIST), \
cmocka_cb, AMexpect(AM_VAL_TYPE_OBJ_TYPE))); \
AMstackItem(NULL, AMlistPut##suffix(doc_state->doc, list, 0, !strcmp(#mode, "insert"), scalar_value), \
cmocka_cb, AMexpect(AM_VAL_TYPE_VOID)); \
type value; \
assert_true(AMitemTo##suffix(AMstackItem(stack_ptr, AMlistGet(doc_state->doc, list, 0, NULL), cmocka_cb, \
AMexpect(suffix_to_val_type(#suffix))), \
&value)); \
assert_true(value == scalar_value); \
AMresultFree(AMstackPop(stack_ptr, NULL)); \
}
#define static_void_test_AMlistPut(suffix, mode, member, scalar_value) \
static void test_AMlistPut ## suffix ## _ ## mode(void **state) { \
GroupState* group_state = *state; \
AMobjId const* const list = AMpush( \
&group_state->stack, \
AMmapPutObject(group_state->doc, AM_ROOT, AMstr("list"), AM_OBJ_TYPE_LIST),\
AM_VALUE_OBJ_ID, \
cmocka_cb).obj_id; \
AMfree(AMlistPut ## suffix(group_state->doc, \
list, \
0, \
!strcmp(#mode, "insert"), \
scalar_value)); \
assert_true(AMpush( \
&group_state->stack, \
AMlistGet(group_state->doc, list, 0, NULL), \
AMvalue_discriminant(#suffix), \
cmocka_cb).member == scalar_value); \
AMfree(AMpop(&group_state->stack)); \
}
#define test_AMlistPutBytes(mode) test_AMlistPutBytes##_##mode
#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); \
\
DocState* doc_state = *state; \
AMstack** stack_ptr = &doc_state->base_state->stack; \
AMobjId const* const list = AMitemObjId( \
AMstackItem(stack_ptr, AMmapPutObject(doc_state->doc, AM_ROOT, AMstr("list"), AM_OBJ_TYPE_LIST), \
cmocka_cb, AMexpect(AM_VAL_TYPE_OBJ_TYPE))); \
AMstackItem( \
NULL, AMlistPutBytes(doc_state->doc, list, 0, !strcmp(#mode, "insert"), AMbytes(bytes_value, BYTES_SIZE)), \
cmocka_cb, AMexpect(AM_VAL_TYPE_VOID)); \
AMbyteSpan bytes; \
assert_true(AMitemToBytes( \
AMstackItem(stack_ptr, AMlistGet(doc_state->doc, list, 0, NULL), cmocka_cb, AMexpect(AM_VAL_TYPE_BYTES)), \
&bytes)); \
assert_int_equal(bytes.count, BYTES_SIZE); \
assert_memory_equal(bytes.src, bytes_value, BYTES_SIZE); \
AMresultFree(AMstackPop(stack_ptr, NULL)); \
}
#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; \
AMobjId const* const list = AMpush( \
&group_state->stack, \
AMmapPutObject(group_state->doc, AM_ROOT, AMstr("list"), AM_OBJ_TYPE_LIST),\
AM_VALUE_OBJ_ID, \
cmocka_cb).obj_id; \
AMfree(AMlistPutBytes(group_state->doc, \
list, \
0, \
!strcmp(#mode, "insert"), \
AMbytes(bytes_value, BYTES_SIZE))); \
AMbyteSpan const bytes = AMpush( \
&group_state->stack, \
AMlistGet(group_state->doc, list, 0, NULL), \
AM_VALUE_BYTES, \
cmocka_cb).bytes; \
assert_int_equal(bytes.count, BYTES_SIZE); \
assert_memory_equal(bytes.src, bytes_value, BYTES_SIZE); \
AMfree(AMpop(&group_state->stack)); \
}
#define test_AMlistPutNull(mode) test_AMlistPutNull_##mode
#define test_AMlistPutNull(mode) test_AMlistPutNull_ ## mode
#define static_void_test_AMlistPutNull(mode) \
static void test_AMlistPutNull_##mode(void** state) { \
DocState* doc_state = *state; \
AMstack** stack_ptr = &doc_state->base_state->stack; \
AMobjId const* const list = AMitemObjId( \
AMstackItem(stack_ptr, AMmapPutObject(doc_state->doc, AM_ROOT, AMstr("list"), AM_OBJ_TYPE_LIST), \
cmocka_cb, AMexpect(AM_VAL_TYPE_OBJ_TYPE))); \
AMstackItem(NULL, AMlistPutNull(doc_state->doc, list, 0, !strcmp(#mode, "insert")), cmocka_cb, \
AMexpect(AM_VAL_TYPE_VOID)); \
AMresult* result = AMstackResult(stack_ptr, AMlistGet(doc_state->doc, list, 0, NULL), NULL, NULL); \
if (AMresultStatus(result) != AM_STATUS_OK) { \
fail_msg_view("%s", AMresultError(result)); \
} \
assert_int_equal(AMresultSize(result), 1); \
assert_int_equal(AMitemValType(AMresultItem(result)), AM_VAL_TYPE_NULL); \
AMresultFree(AMstackPop(stack_ptr, NULL)); \
}
#define static_void_test_AMlistPutNull(mode) \
static void test_AMlistPutNull_ ## mode(void **state) { \
GroupState* group_state = *state; \
AMobjId const* const list = AMpush( \
&group_state->stack, \
AMmapPutObject(group_state->doc, AM_ROOT, AMstr("list"), AM_OBJ_TYPE_LIST),\
AM_VALUE_OBJ_ID, \
cmocka_cb).obj_id; \
AMfree(AMlistPutNull(group_state->doc, \
list, \
0, \
!strcmp(#mode, "insert"))); \
AMresult* const result = AMlistGet(group_state->doc, list, 0, NULL); \
if (AMresultStatus(result) != AM_STATUS_OK) { \
fail_msg_view("%s", AMerrorMessage(result)); \
} \
assert_int_equal(AMresultSize(result), 1); \
assert_int_equal(AMresultValue(result).tag, AM_VALUE_NULL); \
AMfree(result); \
}
#define test_AMlistPutObject(label, mode) test_AMlistPutObject_##label##_##mode
#define test_AMlistPutObject(label, mode) test_AMlistPutObject_ ## label ## _ ## mode
#define static_void_test_AMlistPutObject(label, mode) \
static void test_AMlistPutObject_##label##_##mode(void** state) { \
DocState* doc_state = *state; \
AMstack** stack_ptr = &doc_state->base_state->stack; \
AMobjId const* const list = AMitemObjId( \
AMstackItem(stack_ptr, AMmapPutObject(doc_state->doc, AM_ROOT, AMstr("list"), AM_OBJ_TYPE_LIST), \
cmocka_cb, AMexpect(AM_VAL_TYPE_OBJ_TYPE))); \
AMobjType const obj_type = suffix_to_obj_type(#label); \
AMobjId const* const obj_id = AMitemObjId( \
AMstackItem(stack_ptr, AMlistPutObject(doc_state->doc, list, 0, !strcmp(#mode, "insert"), obj_type), \
cmocka_cb, AMexpect(AM_VAL_TYPE_OBJ_TYPE))); \
assert_non_null(obj_id); \
assert_int_equal(AMobjObjType(doc_state->doc, obj_id), obj_type); \
assert_int_equal(AMobjSize(doc_state->doc, obj_id, NULL), 0); \
AMresultFree(AMstackPop(stack_ptr, NULL)); \
}
#define static_void_test_AMlistPutObject(label, mode) \
static void test_AMlistPutObject_ ## label ## _ ## mode(void **state) { \
GroupState* group_state = *state; \
AMobjId const* const list = AMpush( \
&group_state->stack, \
AMmapPutObject(group_state->doc, AM_ROOT, AMstr("list"), AM_OBJ_TYPE_LIST),\
AM_VALUE_OBJ_ID, \
cmocka_cb).obj_id; \
AMobjType const obj_type = AMobjType_tag(#label); \
if (obj_type != AM_OBJ_TYPE_VOID) { \
AMobjId const* const obj_id = AMpush( \
&group_state->stack, \
AMlistPutObject(group_state->doc, \
list, \
0, \
!strcmp(#mode, "insert"), \
obj_type), \
AM_VALUE_OBJ_ID, \
cmocka_cb).obj_id; \
assert_non_null(obj_id); \
assert_int_equal(AMobjObjType(group_state->doc, obj_id), obj_type); \
assert_int_equal(AMobjSize(group_state->doc, obj_id, NULL), 0); \
} \
else { \
AMpush(&group_state->stack, \
AMlistPutObject(group_state->doc, \
list, \
0, \
!strcmp(#mode, "insert"), \
obj_type), \
AM_VALUE_VOID, \
NULL); \
assert_int_not_equal(AMresultStatus(group_state->stack->result), \
AM_STATUS_OK); \
} \
AMfree(AMpop(&group_state->stack)); \
}
#define test_AMlistPutStr(mode) test_AMlistPutStr##_##mode
#define test_AMlistPutStr(mode) test_AMlistPutStr ## _ ## mode
#define static_void_test_AMlistPutStr(mode, str_value) \
static void test_AMlistPutStr_##mode(void** state) { \
DocState* doc_state = *state; \
AMstack** stack_ptr = &doc_state->base_state->stack; \
AMobjId const* const list = AMitemObjId( \
AMstackItem(stack_ptr, AMmapPutObject(doc_state->doc, AM_ROOT, AMstr("list"), AM_OBJ_TYPE_LIST), \
cmocka_cb, AMexpect(AM_VAL_TYPE_OBJ_TYPE))); \
AMstackItem(NULL, AMlistPutStr(doc_state->doc, list, 0, !strcmp(#mode, "insert"), AMstr(str_value)), \
cmocka_cb, AMexpect(AM_VAL_TYPE_VOID)); \
AMbyteSpan str; \
assert_true(AMitemToStr( \
AMstackItem(stack_ptr, AMlistGet(doc_state->doc, list, 0, NULL), cmocka_cb, AMexpect(AM_VAL_TYPE_STR)), \
&str)); \
assert_int_equal(str.count, strlen(str_value)); \
assert_memory_equal(str.src, str_value, str.count); \
AMresultFree(AMstackPop(stack_ptr, NULL)); \
}
#define static_void_test_AMlistPutStr(mode, str_value) \
static void test_AMlistPutStr_ ## mode(void **state) { \
GroupState* group_state = *state; \
AMobjId const* const list = AMpush( \
&group_state->stack, \
AMmapPutObject(group_state->doc, AM_ROOT, AMstr("list"), AM_OBJ_TYPE_LIST),\
AM_VALUE_OBJ_ID, \
cmocka_cb).obj_id; \
AMfree(AMlistPutStr(group_state->doc, \
list, \
0, \
!strcmp(#mode, "insert"), \
AMstr(str_value))); \
AMbyteSpan const str = AMpush( \
&group_state->stack, \
AMlistGet(group_state->doc, list, 0, NULL), \
AM_VALUE_STR, \
cmocka_cb).str; \
assert_int_equal(str.count, strlen(str_value)); \
assert_memory_equal(str.src, str_value, str.count); \
AMfree(AMpop(&group_state->stack)); \
}
static_void_test_AMlistPut(Bool, insert, bool, true);
static_void_test_AMlistPut(Bool, insert, boolean, true)
static_void_test_AMlistPut(Bool, update, bool, 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(insert, BYTES_VALUE)
static_void_test_AMlistPutBytes(update, BYTES_VALUE);
static_void_test_AMlistPutBytes(update, BYTES_VALUE)
static_void_test_AMlistPut(Counter, insert, int64_t, INT64_MAX);
static_void_test_AMlistPut(Counter, insert, counter, INT64_MAX)
static_void_test_AMlistPut(Counter, update, int64_t, INT64_MAX);
static_void_test_AMlistPut(Counter, update, counter, INT64_MAX)
static_void_test_AMlistPut(F64, insert, double, DBL_MAX);
static_void_test_AMlistPut(F64, insert, f64, DBL_MAX)
static_void_test_AMlistPut(F64, update, double, DBL_MAX);
static_void_test_AMlistPut(F64, update, f64, DBL_MAX)
static_void_test_AMlistPut(Int, insert, int64_t, INT64_MAX);
static_void_test_AMlistPut(Int, insert, int_, INT64_MAX)
static_void_test_AMlistPut(Int, update, int64_t, INT64_MAX);
static_void_test_AMlistPut(Int, update, int_, INT64_MAX)
static_void_test_AMlistPutNull(insert);
static_void_test_AMlistPutNull(insert)
static_void_test_AMlistPutNull(update);
static_void_test_AMlistPutNull(update)
static_void_test_AMlistPutObject(List, insert);
static_void_test_AMlistPutObject(List, insert)
static_void_test_AMlistPutObject(List, update);
static_void_test_AMlistPutObject(List, update)
static_void_test_AMlistPutObject(Map, insert);
static_void_test_AMlistPutObject(Map, insert)
static_void_test_AMlistPutObject(Map, update);
static_void_test_AMlistPutObject(Map, update)
static_void_test_AMlistPutObject(Text, insert);
static_void_test_AMlistPutObject(Text, insert)
static_void_test_AMlistPutObject(Text, update);
static_void_test_AMlistPutObject(Text, update)
static_void_test_AMlistPutStr(insert,
"Hello, "
"world!");
static_void_test_AMlistPutObject(Void, insert)
static_void_test_AMlistPutStr(update,
"Hello,"
" world"
"!");
static_void_test_AMlistPutObject(Void, update)
static_void_test_AMlistPut(Timestamp, insert, int64_t, INT64_MAX);
static_void_test_AMlistPutStr(insert, "Hello, world!")
static_void_test_AMlistPut(Timestamp, update, int64_t, INT64_MAX);
static_void_test_AMlistPutStr(update, "Hello, world!")
static_void_test_AMlistPut(Uint, insert, uint64_t, UINT64_MAX);
static_void_test_AMlistPut(Timestamp, insert, timestamp, INT64_MAX)
static_void_test_AMlistPut(Uint, update, uint64_t, UINT64_MAX);
static_void_test_AMlistPut(Timestamp, update, timestamp, INT64_MAX)
static void test_get_range_values(void** state) {
BaseState* base_state = *state;
AMstack** stack_ptr = &base_state->stack;
AMdoc* doc1;
assert_true(AMitemToDoc(AMstackItem(stack_ptr, AMcreate(NULL), cmocka_cb, AMexpect(AM_VAL_TYPE_DOC)), &doc1));
AMobjId const* const list =
AMitemObjId(AMstackItem(stack_ptr, AMmapPutObject(doc1, AM_ROOT, AMstr("list"), AM_OBJ_TYPE_LIST), cmocka_cb,
AMexpect(AM_VAL_TYPE_OBJ_TYPE)));
static_void_test_AMlistPut(Uint, insert, uint, UINT64_MAX)
static_void_test_AMlistPut(Uint, update, uint, UINT64_MAX)
static void test_get_list_values(void** state) {
AMresultStack* stack = *state;
AMdoc* const doc1 = AMpush(&stack, AMcreate(NULL), AM_VALUE_DOC, cmocka_cb).doc;
AMobjId const* const list = AMpush(
&stack,
AMmapPutObject(doc1, AM_ROOT, AMstr("list"), AM_OBJ_TYPE_LIST),
AM_VALUE_OBJ_ID,
cmocka_cb).obj_id;
/* Insert elements. */
AMstackItem(NULL, AMlistPutStr(doc1, list, 0, true, AMstr("First")), cmocka_cb, AMexpect(AM_VAL_TYPE_VOID));
AMstackItem(NULL, AMlistPutStr(doc1, list, 0, true, AMstr("Second")), cmocka_cb, AMexpect(AM_VAL_TYPE_VOID));
AMstackItem(NULL, AMlistPutStr(doc1, list, 0, true, AMstr("Third")), cmocka_cb, AMexpect(AM_VAL_TYPE_VOID));
AMstackItem(NULL, AMlistPutStr(doc1, list, 0, true, AMstr("Fourth")), cmocka_cb, AMexpect(AM_VAL_TYPE_VOID));
AMstackItem(NULL, AMlistPutStr(doc1, list, 0, true, AMstr("Fifth")), cmocka_cb, AMexpect(AM_VAL_TYPE_VOID));
AMstackItem(NULL, AMlistPutStr(doc1, list, 0, true, AMstr("Sixth")), cmocka_cb, AMexpect(AM_VAL_TYPE_VOID));
AMstackItem(NULL, AMlistPutStr(doc1, list, 0, true, AMstr("Seventh")), cmocka_cb, AMexpect(AM_VAL_TYPE_VOID));
AMstackItem(NULL, AMlistPutStr(doc1, list, 0, true, AMstr("Eighth")), cmocka_cb, AMexpect(AM_VAL_TYPE_VOID));
AMstackItem(NULL, AMcommit(doc1, AMstr(NULL), NULL), cmocka_cb, AMexpect(AM_VAL_TYPE_CHANGE_HASH));
AMfree(AMlistPutStr(doc1, list, 0, true, AMstr("First")));
AMfree(AMlistPutStr(doc1, list, 0, true, AMstr("Second")));
AMfree(AMlistPutStr(doc1, list, 0, true, AMstr("Third")));
AMfree(AMlistPutStr(doc1, list, 0, true, AMstr("Fourth")));
AMfree(AMlistPutStr(doc1, list, 0, true, AMstr("Fifth")));
AMfree(AMlistPutStr(doc1, list, 0, true, AMstr("Sixth")));
AMfree(AMlistPutStr(doc1, list, 0, true, AMstr("Seventh")));
AMfree(AMlistPutStr(doc1, list, 0, true, AMstr("Eighth")));
AMfree(AMcommit(doc1, AMstr(NULL), NULL));
AMitems const v1 = AMstackItems(stack_ptr, AMgetHeads(doc1), cmocka_cb, AMexpect(AM_VAL_TYPE_CHANGE_HASH));
AMdoc* doc2;
assert_true(AMitemToDoc(AMstackItem(stack_ptr, AMfork(doc1, NULL), cmocka_cb, AMexpect(AM_VAL_TYPE_DOC)), &doc2));
AMchangeHashes const v1 = AMpush(&stack,
AMgetHeads(doc1),
AM_VALUE_CHANGE_HASHES,
cmocka_cb).change_hashes;
AMdoc* const doc2 = AMpush(&stack,
AMfork(doc1, NULL),
AM_VALUE_DOC,
cmocka_cb).doc;
AMstackItem(NULL, AMlistPutStr(doc1, list, 2, false, AMstr("Third V2")), cmocka_cb, AMexpect(AM_VAL_TYPE_VOID));
AMstackItem(NULL, AMcommit(doc1, AMstr(NULL), NULL), cmocka_cb, AMexpect(AM_VAL_TYPE_CHANGE_HASH));
AMfree(AMlistPutStr(doc1, list, 2, false, AMstr("Third V2")));
AMfree(AMcommit(doc1, AMstr(NULL), NULL));
AMstackItem(NULL, AMlistPutStr(doc2, list, 2, false, AMstr("Third V3")), cmocka_cb, AMexpect(AM_VAL_TYPE_VOID));
AMstackItem(NULL, AMcommit(doc2, AMstr(NULL), NULL), cmocka_cb, AMexpect(AM_VAL_TYPE_CHANGE_HASH));
AMfree(AMlistPutStr(doc2, list, 2, false, AMstr("Third V3")));
AMfree(AMcommit(doc2, AMstr(NULL), NULL));
AMstackItem(NULL, AMmerge(doc1, doc2), cmocka_cb, AMexpect(AM_VAL_TYPE_CHANGE_HASH));
AMfree(AMmerge(doc1, doc2));
/* Forward vs. reverse: complete current list range. */
AMitems range =
AMstackItems(stack_ptr, AMlistRange(doc1, list, 0, SIZE_MAX, NULL), cmocka_cb, AMexpect(AM_VAL_TYPE_STR));
size_t size = AMitemsSize(&range);
assert_int_equal(size, 8);
AMitems range_back = AMitemsReversed(&range);
assert_int_equal(AMitemsSize(&range_back), size);
size_t pos;
assert_true(AMitemPos(AMitemsNext(&range, 1), &pos));
assert_int_equal(pos, 0);
assert_true(AMitemPos(AMitemsNext(&range_back, 1), &pos));
assert_int_equal(pos, 7);
AMlistItems range = AMpush(&stack,
AMlistRange(doc1, list, 0, SIZE_MAX, NULL),
AM_VALUE_LIST_ITEMS,
cmocka_cb).list_items;
assert_int_equal(AMlistItemsSize(&range), 8);
AMitem *item1, *item_back1;
size_t count, middle = size / 2;
range = AMitemsRewound(&range);
range_back = AMitemsRewound(&range_back);
for (item1 = NULL, item_back1 = NULL, count = 0; item1 && item_back1;
item1 = AMitemsNext(&range, 1), item_back1 = AMitemsNext(&range_back, 1), ++count) {
size_t pos1, pos_back1;
assert_true(AMitemPos(item1, &pos1));
assert_true(AMitemPos(item_back1, &pos_back1));
if ((count == middle) && (middle & 1)) {
/* The iterators are crossing in the middle. */
assert_int_equal(pos1, pos_back1);
assert_true(AMitemEqual(item1, item_back1));
assert_true(AMobjIdEqual(AMitemObjId(item1), AMitemObjId(item_back1)));
} else {
assert_int_not_equal(pos1, pos_back1);
}
AMitem* item2 = AMstackItem(stack_ptr, AMlistGet(doc1, list, pos1, NULL), NULL, NULL);
AMitem* item_back2 = AMstackItem(stack_ptr, AMlistGet(doc1, list, pos_back1, NULL), NULL, NULL);
/** \note An item returned from an `AM...Get()` call doesn't include the
index used to retrieve it. */
assert_false(AMitemIdxType(item2));
assert_false(AMitemIdxType(item_back2));
assert_true(AMitemEqual(item1, item2));
assert_true(AMobjIdEqual(AMitemObjId(item1), AMitemObjId(item2)));
assert_true(AMitemEqual(item_back1, item_back2));
assert_true(AMobjIdEqual(AMitemObjId(item_back1), AMitemObjId(item_back2)));
AMresultFree(AMstackPop(stack_ptr, NULL));
AMlistItem const* list_item = NULL;
while ((list_item = AMlistItemsNext(&range, 1)) != NULL) {
AMvalue const val1 = AMlistItemValue(list_item);
AMresult* result = AMlistGet(doc1, list, AMlistItemIndex(list_item), NULL);
AMvalue const val2 = AMresultValue(result);
assert_true(AMvalueEqual(&val1, &val2));
assert_non_null(AMlistItemObjId(list_item));
AMfree(result);
}
/* Forward vs. reverse: partial current list range. */
range = AMstackItems(stack_ptr, AMlistRange(doc1, list, 1, 6, NULL), cmocka_cb, AMexpect(AM_VAL_TYPE_STR));
size = AMitemsSize(&range);
assert_int_equal(size, 5);
range_back = AMitemsReversed(&range);
assert_int_equal(AMitemsSize(&range_back), size);
assert_true(AMitemPos(AMitemsNext(&range, 1), &pos));
assert_int_equal(pos, 1);
assert_true(AMitemPos(AMitemsNext(&range_back, 1), &pos));
assert_int_equal(pos, 5);
range = AMpush(&stack,
AMlistRange(doc1, list, 3, 6, NULL),
AM_VALUE_LIST_ITEMS,
cmocka_cb).list_items;
AMlistItems range_back = AMlistItemsReversed(&range);
assert_int_equal(AMlistItemsSize(&range), 3);
assert_int_equal(AMlistItemIndex(AMlistItemsNext(&range, 1)), 3);
assert_int_equal(AMlistItemIndex(AMlistItemsNext(&range_back, 1)), 5);
middle = size / 2;
range = AMitemsRewound(&range);
range_back = AMitemsRewound(&range_back);
for (item1 = NULL, item_back1 = NULL, count = 0; item1 && item_back1;
item1 = AMitemsNext(&range, 1), item_back1 = AMitemsNext(&range_back, 1), ++count) {
size_t pos1, pos_back1;
assert_true(AMitemPos(item1, &pos1));
assert_true(AMitemPos(item_back1, &pos_back1));
if ((count == middle) && (middle & 1)) {
/* The iterators are crossing in the middle. */
assert_int_equal(pos1, pos_back1);
assert_true(AMitemEqual(item1, item_back1));
assert_true(AMobjIdEqual(AMitemObjId(item1), AMitemObjId(item_back1)));
} else {
assert_int_not_equal(pos1, pos_back1);
}
AMitem* item2 = AMstackItem(stack_ptr, AMlistGet(doc1, list, pos1, NULL), NULL, NULL);
AMitem* item_back2 = AMstackItem(stack_ptr, AMlistGet(doc1, list, pos_back1, NULL), NULL, NULL);
/** \note An item returned from an `AMlistGet()` call doesn't include
the index used to retrieve it. */
assert_int_equal(AMitemIdxType(item2), 0);
assert_int_equal(AMitemIdxType(item_back2), 0);
assert_true(AMitemEqual(item1, item2));
assert_true(AMobjIdEqual(AMitemObjId(item1), AMitemObjId(item2)));
assert_true(AMitemEqual(item_back1, item_back2));
assert_true(AMobjIdEqual(AMitemObjId(item_back1), AMitemObjId(item_back2)));
AMresultFree(AMstackPop(stack_ptr, NULL));
range = AMlistItemsRewound(&range);
while ((list_item = AMlistItemsNext(&range, 1)) != NULL) {
AMvalue const val1 = AMlistItemValue(list_item);
AMresult* result = AMlistGet(doc1, list, AMlistItemIndex(list_item), NULL);
AMvalue const val2 = AMresultValue(result);
assert_true(AMvalueEqual(&val1, &val2));
assert_non_null(AMlistItemObjId(list_item));
AMfree(result);
}
/* Forward vs. reverse: complete historical map range. */
range = AMstackItems(stack_ptr, AMlistRange(doc1, list, 0, SIZE_MAX, &v1), cmocka_cb, AMexpect(AM_VAL_TYPE_STR));
size = AMitemsSize(&range);
assert_int_equal(size, 8);
range_back = AMitemsReversed(&range);
assert_int_equal(AMitemsSize(&range_back), size);
assert_true(AMitemPos(AMitemsNext(&range, 1), &pos));
assert_int_equal(pos, 0);
assert_true(AMitemPos(AMitemsNext(&range_back, 1), &pos));
assert_int_equal(pos, 7);
middle = size / 2;
range = AMitemsRewound(&range);
range_back = AMitemsRewound(&range_back);
for (item1 = NULL, item_back1 = NULL, count = 0; item1 && item_back1;
item1 = AMitemsNext(&range, 1), item_back1 = AMitemsNext(&range_back, 1), ++count) {
size_t pos1, pos_back1;
assert_true(AMitemPos(item1, &pos1));
assert_true(AMitemPos(item_back1, &pos_back1));
if ((count == middle) && (middle & 1)) {
/* The iterators are crossing in the middle. */
assert_int_equal(pos1, pos_back1);
assert_true(AMitemEqual(item1, item_back1));
assert_true(AMobjIdEqual(AMitemObjId(item1), AMitemObjId(item_back1)));
} else {
assert_int_not_equal(pos1, pos_back1);
}
AMitem* item2 = AMstackItem(stack_ptr, AMlistGet(doc1, list, pos1, &v1), NULL, NULL);
AMitem* item_back2 = AMstackItem(stack_ptr, AMlistGet(doc1, list, pos_back1, &v1), NULL, NULL);
/** \note An item returned from an `AM...Get()` call doesn't include the
index used to retrieve it. */
assert_false(AMitemIdxType(item2));
assert_false(AMitemIdxType(item_back2));
assert_true(AMitemEqual(item1, item2));
assert_true(AMobjIdEqual(AMitemObjId(item1), AMitemObjId(item2)));
assert_true(AMitemEqual(item_back1, item_back2));
assert_true(AMobjIdEqual(AMitemObjId(item_back1), AMitemObjId(item_back2)));
AMresultFree(AMstackPop(stack_ptr, NULL));
range = AMpush(&stack,
AMlistRange(doc1, list, 0, SIZE_MAX, &v1),
AM_VALUE_LIST_ITEMS,
cmocka_cb).list_items;
assert_int_equal(AMlistItemsSize(&range), 8);
while ((list_item = AMlistItemsNext(&range, 1)) != NULL) {
AMvalue const val1 = AMlistItemValue(list_item);
AMresult* result = AMlistGet(doc1, list, AMlistItemIndex(list_item), &v1);
AMvalue const val2 = AMresultValue(result);
assert_true(AMvalueEqual(&val1, &val2));
assert_non_null(AMlistItemObjId(list_item));
AMfree(result);
}
/* Forward vs. reverse: partial historical map range. */
range = AMstackItems(stack_ptr, AMlistRange(doc1, list, 2, 7, &v1), cmocka_cb, AMexpect(AM_VAL_TYPE_STR));
size = AMitemsSize(&range);
assert_int_equal(size, 5);
range_back = AMitemsReversed(&range);
assert_int_equal(AMitemsSize(&range_back), size);
assert_true(AMitemPos(AMitemsNext(&range, 1), &pos));
assert_int_equal(pos, 2);
assert_true(AMitemPos(AMitemsNext(&range_back, 1), &pos));
assert_int_equal(pos, 6);
range = AMpush(&stack,
AMlistRange(doc1, list, 3, 6, &v1),
AM_VALUE_LIST_ITEMS,
cmocka_cb).list_items;
range_back = AMlistItemsReversed(&range);
assert_int_equal(AMlistItemsSize(&range), 3);
assert_int_equal(AMlistItemIndex(AMlistItemsNext(&range, 1)), 3);
assert_int_equal(AMlistItemIndex(AMlistItemsNext(&range_back, 1)), 5);
middle = size / 2;
range = AMitemsRewound(&range);
range_back = AMitemsRewound(&range_back);
for (item1 = NULL, item_back1 = NULL, count = 0; item1 && item_back1;
item1 = AMitemsNext(&range, 1), item_back1 = AMitemsNext(&range_back, 1), ++count) {
size_t pos1, pos_back1;
assert_true(AMitemPos(item1, &pos1));
assert_true(AMitemPos(item_back1, &pos_back1));
if ((count == middle) && (middle & 1)) {
/* The iterators are crossing in the middle. */
assert_int_equal(pos1, pos_back1);
assert_true(AMitemEqual(item1, item_back1));
assert_true(AMobjIdEqual(AMitemObjId(item1), AMitemObjId(item_back1)));
} else {
assert_int_not_equal(pos1, pos_back1);
}
AMitem* item2 = AMstackItem(stack_ptr, AMlistGet(doc1, list, pos1, &v1), NULL, NULL);
AMitem* item_back2 = AMstackItem(stack_ptr, AMlistGet(doc1, list, pos_back1, &v1), NULL, NULL);
/** \note An item returned from an `AM...Get()` call doesn't include the
index used to retrieve it. */
assert_false(AMitemIdxType(item2));
assert_false(AMitemIdxType(item_back2));
assert_true(AMitemEqual(item1, item2));
assert_true(AMobjIdEqual(AMitemObjId(item1), AMitemObjId(item2)));
assert_true(AMitemEqual(item_back1, item_back2));
assert_true(AMobjIdEqual(AMitemObjId(item_back1), AMitemObjId(item_back2)));
AMresultFree(AMstackPop(stack_ptr, NULL));
range = AMlistItemsRewound(&range);
while ((list_item = AMlistItemsNext(&range, 1)) != NULL) {
AMvalue const val1 = AMlistItemValue(list_item);
AMresult* result = AMlistGet(doc1, list, AMlistItemIndex(list_item), &v1);
AMvalue const val2 = AMresultValue(result);
assert_true(AMvalueEqual(&val1, &val2));
assert_non_null(AMlistItemObjId(list_item));
AMfree(result);
}
/* List range vs. object range: complete current. */
range = AMstackItems(stack_ptr, AMlistRange(doc1, list, 0, SIZE_MAX, NULL), cmocka_cb, AMexpect(AM_VAL_TYPE_STR));
AMitems obj_items = AMstackItems(stack_ptr, AMobjItems(doc1, list, NULL), cmocka_cb, AMexpect(AM_VAL_TYPE_STR));
assert_int_equal(AMitemsSize(&range), AMitemsSize(&obj_items));
AMitem *item, *obj_item;
for (item = NULL, obj_item = NULL; item && obj_item;
item = AMitemsNext(&range, 1), obj_item = AMitemsNext(&obj_items, 1)) {
/** \note Object iteration doesn't yield any item indices. */
assert_true(AMitemIdxType(item));
assert_false(AMitemIdxType(obj_item));
assert_true(AMitemEqual(item, obj_item));
assert_true(AMobjIdEqual(AMitemObjId(item), AMitemObjId(obj_item)));
range = AMpush(&stack,
AMlistRange(doc1, list, 0, SIZE_MAX, NULL),
AM_VALUE_LIST_ITEMS,
cmocka_cb).list_items;
AMobjItems values = AMpush(&stack,
AMobjValues(doc1, list, NULL),
AM_VALUE_OBJ_ITEMS,
cmocka_cb).obj_items;
assert_int_equal(AMlistItemsSize(&range), AMobjItemsSize(&values));
AMobjItem const* value = NULL;
while ((list_item = AMlistItemsNext(&range, 1)) != NULL &&
(value = AMobjItemsNext(&values, 1)) != NULL) {
AMvalue const val1 = AMlistItemValue(list_item);
AMvalue const val2 = AMobjItemValue(value);
assert_true(AMvalueEqual(&val1, &val2));
assert_true(AMobjIdEqual(AMlistItemObjId(list_item), AMobjItemObjId(value)));
}
/* List range vs. object range: complete historical. */
range = AMstackItems(stack_ptr, AMlistRange(doc1, list, 0, SIZE_MAX, &v1), cmocka_cb, AMexpect(AM_VAL_TYPE_STR));
obj_items = AMstackItems(stack_ptr, AMobjItems(doc1, list, &v1), cmocka_cb, AMexpect(AM_VAL_TYPE_STR));
assert_int_equal(AMitemsSize(&range), AMitemsSize(&obj_items));
for (item = NULL, obj_item = NULL; item && obj_item;
item = AMitemsNext(&range, 1), obj_item = AMitemsNext(&obj_items, 1)) {
/** \note Object iteration doesn't yield any item indices. */
assert_true(AMitemIdxType(item));
assert_false(AMitemIdxType(obj_item));
assert_true(AMitemEqual(item, obj_item));
assert_true(AMobjIdEqual(AMitemObjId(item), AMitemObjId(obj_item)));
range = AMpush(&stack,
AMlistRange(doc1, list, 0, SIZE_MAX, &v1),
AM_VALUE_LIST_ITEMS,
cmocka_cb).list_items;
values = AMpush(&stack,
AMobjValues(doc1, list, &v1),
AM_VALUE_OBJ_ITEMS,
cmocka_cb).obj_items;
assert_int_equal(AMlistItemsSize(&range), AMobjItemsSize(&values));
while ((list_item = AMlistItemsNext(&range, 1)) != NULL &&
(value = AMobjItemsNext(&values, 1)) != NULL) {
AMvalue const val1 = AMlistItemValue(list_item);
AMvalue const val2 = AMobjItemValue(value);
assert_true(AMvalueEqual(&val1, &val2));
assert_true(AMobjIdEqual(AMlistItemObjId(list_item), AMobjItemObjId(value)));
}
}
/**
* \brief A JavaScript application can introduce NUL (`\0`) characters into a
* list object's string value which will truncate it in a C application.
/** \brief A JavaScript application can introduce NUL (`\0`) characters into a
* list object's string value which will truncate it in a C application.
*/
static void test_get_NUL_string_value(void** state) {
/*
@ -431,52 +381,60 @@ static void test_get_NUL_string_value(void** state) {
doc[0] = 'o\0ps';
});
const bytes = Automerge.save(doc);
console.log("static uint8_t const SAVED_DOC[] = {" + Array.apply([],
bytes).join(", ") + "};");
console.log("static uint8_t const SAVED_DOC[] = {" + Array.apply([], bytes).join(", ") + "};");
*/
static uint8_t const OOPS_VALUE[] = {'o', '\0', 'p', 's'};
static size_t const OOPS_SIZE = sizeof(OOPS_VALUE) / sizeof(uint8_t);
static uint8_t const SAVED_DOC[] = {
133, 111, 74, 131, 224, 28, 197, 17, 0, 113, 1, 16, 246, 137, 63, 193, 255, 181, 76, 79, 129,
213, 133, 29, 214, 158, 164, 15, 1, 207, 184, 14, 57, 1, 194, 79, 247, 82, 160, 134, 227, 144,
5, 241, 136, 205, 238, 250, 251, 54, 34, 250, 210, 96, 204, 132, 153, 203, 110, 109, 6, 6, 1,
2, 3, 2, 19, 2, 35, 2, 64, 2, 86, 2, 8, 21, 3, 33, 2, 35, 2, 52, 1, 66,
2, 86, 2, 87, 4, 128, 1, 2, 127, 0, 127, 1, 127, 1, 127, 0, 127, 0, 127, 7, 127,
1, 48, 127, 0, 127, 1, 1, 127, 1, 127, 70, 111, 0, 112, 115, 127, 0, 0};
133, 111, 74, 131, 224, 28, 197, 17, 0, 113, 1, 16, 246, 137, 63, 193,
255, 181, 76, 79, 129, 213, 133, 29, 214, 158, 164, 15, 1, 207, 184,
14, 57, 1, 194, 79, 247, 82, 160, 134, 227, 144, 5, 241, 136, 205,
238, 250, 251, 54, 34, 250, 210, 96, 204, 132, 153, 203, 110, 109, 6,
6, 1, 2, 3, 2, 19, 2, 35, 2, 64, 2, 86, 2, 8, 21, 3, 33, 2, 35, 2, 52,
1, 66, 2, 86, 2, 87, 4, 128, 1, 2, 127, 0, 127, 1, 127, 1, 127, 0,
127, 0, 127, 7, 127, 1, 48, 127, 0, 127, 1, 1, 127, 1, 127, 70, 111,
0, 112, 115, 127, 0, 0};
static size_t const SAVED_DOC_SIZE = sizeof(SAVED_DOC) / sizeof(uint8_t);
BaseState* base_state = *state;
AMstack** stack_ptr = &base_state->stack;
AMdoc* doc;
assert_true(AMitemToDoc(
AMstackItem(stack_ptr, AMload(SAVED_DOC, SAVED_DOC_SIZE), cmocka_cb, AMexpect(AM_VAL_TYPE_DOC)), &doc));
AMbyteSpan str;
assert_true(AMitemToStr(
AMstackItem(stack_ptr, AMlistGet(doc, AM_ROOT, 0, NULL), cmocka_cb, AMexpect(AM_VAL_TYPE_STR)), &str));
AMresultStack* stack = *state;
AMdoc* const doc = AMpush(&stack,
AMload(SAVED_DOC, SAVED_DOC_SIZE),
AM_VALUE_DOC,
cmocka_cb).doc;
AMbyteSpan const str = AMpush(&stack,
AMlistGet(doc, AM_ROOT, 0, NULL),
AM_VALUE_STR,
cmocka_cb).str;
assert_int_not_equal(str.count, strlen(OOPS_VALUE));
assert_int_equal(str.count, OOPS_SIZE);
assert_memory_equal(str.src, OOPS_VALUE, str.count);
}
static void test_insert_at_index(void** state) {
BaseState* base_state = *state;
AMstack** stack_ptr = &base_state->stack;
AMdoc* doc;
assert_true(AMitemToDoc(AMstackItem(stack_ptr, AMcreate(NULL), cmocka_cb, AMexpect(AM_VAL_TYPE_DOC)), &doc));
AMobjId const* const list =
AMitemObjId(AMstackItem(stack_ptr, AMmapPutObject(doc, AM_ROOT, AMstr("list"), AM_OBJ_TYPE_LIST), cmocka_cb,
AMexpect(AM_VAL_TYPE_OBJ_TYPE)));
AMresultStack* stack = *state;
AMdoc* const doc = AMpush(&stack, AMcreate(NULL), AM_VALUE_DOC, cmocka_cb).doc;
AMobjId const* const list = AMpush(
&stack,
AMmapPutObject(doc, AM_ROOT, AMstr("list"), AM_OBJ_TYPE_LIST),
AM_VALUE_OBJ_ID,
cmocka_cb).obj_id;
/* Insert both at the same index. */
AMstackItem(NULL, AMlistPutUint(doc, list, 0, true, 0), cmocka_cb, AMexpect(AM_VAL_TYPE_VOID));
AMstackItem(NULL, AMlistPutUint(doc, list, 0, true, 1), cmocka_cb, AMexpect(AM_VAL_TYPE_VOID));
AMfree(AMlistPutUint(doc, list, 0, true, 0));
AMfree(AMlistPutUint(doc, list, 0, true, 1));
assert_int_equal(AMobjSize(doc, list, NULL), 2);
AMitems const keys = AMstackItems(stack_ptr, AMkeys(doc, list, NULL), cmocka_cb, AMexpect(AM_VAL_TYPE_STR));
assert_int_equal(AMitemsSize(&keys), 2);
AMitems const range =
AMstackItems(stack_ptr, AMlistRange(doc, list, 0, SIZE_MAX, NULL), cmocka_cb, AMexpect(AM_VAL_TYPE_UINT));
assert_int_equal(AMitemsSize(&range), 2);
AMstrs const keys = AMpush(&stack,
AMkeys(doc, list, NULL),
AM_VALUE_STRS,
cmocka_cb).strs;
assert_int_equal(AMstrsSize(&keys), 2);
AMlistItems const range = AMpush(&stack,
AMlistRange(doc, list, 0, SIZE_MAX, NULL),
AM_VALUE_LIST_ITEMS,
cmocka_cb).list_items;
assert_int_equal(AMlistItemsSize(&range), 2);
}
int run_list_tests(void) {
@ -500,16 +458,18 @@ int run_list_tests(void) {
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_AMlistPutObject(Void, insert)),
cmocka_unit_test(test_AMlistPutObject(Void, 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)),
cmocka_unit_test_setup_teardown(test_get_range_values, setup_base, teardown_base),
cmocka_unit_test_setup_teardown(test_get_NUL_string_value, setup_base, teardown_base),
cmocka_unit_test_setup_teardown(test_insert_at_index, setup_base, teardown_base),
cmocka_unit_test_setup_teardown(test_get_list_values, setup_stack, teardown_stack),
cmocka_unit_test_setup_teardown(test_get_NUL_string_value, setup_stack, teardown_stack),
cmocka_unit_test_setup_teardown(test_insert_at_index, setup_stack, teardown_stack),
};
return cmocka_run_group_tests(tests, setup_doc, teardown_doc);
return cmocka_run_group_tests(tests, group_setup, group_teardown);
}

View file

@ -3,36 +3,23 @@
/* local */
#include "macro_utils.h"
AMobjType suffix_to_obj_type(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 AM_OBJ_TYPE_DEFAULT;
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;
}
AMvalType suffix_to_val_type(char const* suffix) {
if (!strcmp(suffix, "Bool"))
return AM_VAL_TYPE_BOOL;
else if (!strcmp(suffix, "Bytes"))
return AM_VAL_TYPE_BYTES;
else if (!strcmp(suffix, "Counter"))
return AM_VAL_TYPE_COUNTER;
else if (!strcmp(suffix, "F64"))
return AM_VAL_TYPE_F64;
else if (!strcmp(suffix, "Int"))
return AM_VAL_TYPE_INT;
else if (!strcmp(suffix, "Null"))
return AM_VAL_TYPE_NULL;
else if (!strcmp(suffix, "Str"))
return AM_VAL_TYPE_STR;
else if (!strcmp(suffix, "Timestamp"))
return AM_VAL_TYPE_TIMESTAMP;
else if (!strcmp(suffix, "Uint"))
return AM_VAL_TYPE_UINT;
else
return AM_VAL_TYPE_DEFAULT;
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 if (!strcmp(obj_type_label, "Void")) return AM_OBJ_TYPE_VOID;
else return 0;
}

View file

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

View file

@ -1,6 +1,6 @@
#include <setjmp.h>
#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <stdint.h>
/* third-party */
@ -8,14 +8,8 @@
extern int run_actor_id_tests(void);
extern int run_byte_span_tests(void);
extern int run_doc_tests(void);
extern int run_enum_string_tests(void);
extern int run_item_tests(void);
extern int run_list_tests(void);
extern int run_map_tests(void);
@ -23,6 +17,11 @@ extern int run_map_tests(void);
extern int run_ported_wasm_suite(void);
int main(void) {
return (run_actor_id_tests() + run_byte_span_tests() + run_doc_tests() + run_enum_string_tests() +
run_item_tests() + run_list_tests() + run_map_tests() + run_ported_wasm_suite());
return (
run_actor_id_tests() +
run_doc_tests() +
run_list_tests() +
run_map_tests() +
run_ported_wasm_suite()
);
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
#include <setjmp.h>
#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <stdint.h>
/* third-party */
@ -11,5 +11,8 @@ extern int run_ported_wasm_basic_tests(void);
extern int run_ported_wasm_sync_tests(void);
int run_ported_wasm_suite(void) {
return (run_ported_wasm_basic_tests() + run_ported_wasm_sync_tests());
return (
run_ported_wasm_basic_tests() +
run_ported_wasm_sync_tests()
);
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,31 @@
#include <setjmp.h>
#include <stdarg.h>
#include <stddef.h>
/* third-party */
#include <cmocka.h>
/* local */
#include "cmocka_utils.h"
#include "stack_utils.h"
void cmocka_cb(AMresultStack** stack, uint8_t discriminant) {
assert_non_null(stack);
assert_non_null(*stack);
assert_non_null((*stack)->result);
if (AMresultStatus((*stack)->result) != AM_STATUS_OK) {
fail_msg_view("%s", AMerrorMessage((*stack)->result));
}
assert_int_equal(AMresultValue((*stack)->result).tag, discriminant);
}
int setup_stack(void** state) {
*state = NULL;
return 0;
}
int teardown_stack(void** state) {
AMresultStack* stack = *state;
AMfreeStack(&stack);
return 0;
}

View file

@ -0,0 +1,38 @@
#ifndef STACK_UTILS_H
#define STACK_UTILS_H
#include <stdint.h>
/* local */
#include <automerge-c/automerge.h>
/**
* \brief Reports an error through a cmocka assertion.
*
* \param[in,out] stack A pointer to a pointer to an `AMresultStack` struct.
* \param[in] discriminant An `AMvalueVariant` enum tag.
* \pre \p stack` != NULL`.
*/
void cmocka_cb(AMresultStack** stack, uint8_t discriminant);
/**
* \brief Allocates a result stack for storing the results allocated during one
* or more test cases.
*
* \param[in,out] state A pointer to a pointer to an `AMresultStack` struct.
* \pre \p state` != NULL`.
* \warning The `AMresultStack` struct returned through \p state must be
* deallocated with `teardown_stack()` in order to prevent memory leaks.
*/
int setup_stack(void** state);
/**
* \brief Deallocates a result stack after deallocating any results that were
* stored in it by one or more test cases.
*
* \param[in] state A pointer to a pointer to an `AMresultStack` struct.
* \pre \p state` != NULL`.
*/
int teardown_stack(void** state);
#endif /* STACK_UTILS_H */

View file

@ -1,5 +1,5 @@
#include <stdint.h>
#include <stdio.h>
#include <stdint.h>
/* local */
#include "str_utils.h"

View file

@ -1,17 +1,14 @@
#ifndef TESTS_STR_UTILS_H
#define TESTS_STR_UTILS_H
#ifndef STR_UTILS_H
#define STR_UTILS_H
/**
* \brief Converts a hexadecimal string into an array of bytes.
* \brief Converts a hexadecimal string into a sequence of bytes.
*
* \param[in] hex_str A hexadecimal string.
* \param[in] src A pointer to an array of bytes.
* \param[in] count The count of bytes to copy into the array pointed to by
* \p src.
* \pre \p src `!= NULL`
* \pre `sizeof(`\p src `) > 0`
* \pre \p count `<= sizeof(`\p src `)`
* \param[in] hex_str A string.
* \param[in] src A pointer to a contiguous sequence of bytes.
* \param[in] count The number of bytes to copy to \p src.
* \pre \p count `<=` length of \p src.
*/
void hex_to_bytes(char const* hex_str, uint8_t* src, size_t const count);
#endif /* TESTS_STR_UTILS_H */
#endif /* STR_UTILS_H */

857
rust/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

@ -57,6 +57,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

@ -8,7 +8,7 @@
"description": "wasm-bindgen bindings to the automerge rust implementation",
"homepage": "https://github.com/automerge/automerge-rs/tree/main/automerge-wasm",
"repository": "github:automerge/automerge-rs",
"version": "0.1.25",
"version": "0.1.23",
"license": "MIT",
"files": [
"README.md",

View file

@ -41,7 +41,6 @@ use wasm_bindgen::JsCast;
mod interop;
mod observer;
mod sequence_tree;
mod sync;
mod value;

View file

@ -6,12 +6,10 @@ use crate::{
interop::{self, alloc, js_set},
TextRepresentation,
};
use automerge::{ObjId, OpObserver, Prop, ReadDoc, ScalarValue, Value};
use automerge::{ObjId, OpObserver, Prop, ReadDoc, ScalarValue, SequenceTree, Value};
use js_sys::{Array, Object};
use wasm_bindgen::prelude::*;
use crate::sequence_tree::SequenceTree;
#[derive(Debug, Clone, Default)]
pub(crate) struct Observer {
enabled: bool,

View file

@ -1447,7 +1447,7 @@ describe('Automerge', () => {
sync(n1, n2, s1, s2)
// Having n3's last change concurrent to the last sync heads forces us into the slower code path
const change3 = n3.getLastLocalChange()
const change3 = n2.getLastLocalChange()
if (change3 === null) throw new RangeError("no local change")
n2.applyChanges([change3])
n1.put("_root", "n1", "final"); n1.commit("", 0)

View file

@ -47,7 +47,6 @@ criterion = "0.4.0"
test-log = { version = "0.2.10", features=["trace"], default-features = false}
tracing-subscriber = {version = "0.3.9", features = ["fmt", "env-filter"] }
automerge-test = { path = "../automerge-test" }
prettytable = "0.10.0"
[[bench]]
name = "range"

View file

@ -159,7 +159,7 @@ impl<Obs: Observation> AutoCommitWithObs<Obs> {
///
/// This is a cheap operation, it just changes the way indexes are calculated
pub fn with_encoding(mut self, encoding: TextEncoding) -> Self {
self.doc = self.doc.with_encoding(encoding);
self.doc.text_encoding = encoding;
self
}

View file

@ -4,7 +4,8 @@ use std::fmt::Debug;
use std::num::NonZeroU64;
use std::ops::RangeBounds;
use crate::change_graph::ChangeGraph;
use crate::clock::ClockData;
use crate::clocks::Clocks;
use crate::columnar::Key as EncodedKey;
use crate::exid::ExId;
use crate::keys::Keys;
@ -25,8 +26,6 @@ use crate::{
};
use serde::Serialize;
mod current_state;
#[cfg(test)]
mod tests;
@ -36,15 +35,6 @@ pub(crate) enum Actor {
Cached(usize),
}
/// What to do when loading a document partially succeeds
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OnPartialLoad {
/// Ignore the error and return the loaded changes
Ignore,
/// Fail the entire load
Error,
}
/// An automerge document which does not manage transactions for you.
///
/// ## Creating, loading, merging and forking documents
@ -81,26 +71,26 @@ pub enum OnPartialLoad {
#[derive(Debug, Clone)]
pub struct Automerge {
/// The list of unapplied changes that are not causally ready.
queue: Vec<Change>,
pub(crate) queue: Vec<Change>,
/// The history of changes that form this document, topologically sorted too.
history: Vec<Change>,
pub(crate) history: Vec<Change>,
/// Mapping from change hash to index into the history list.
history_index: HashMap<ChangeHash, usize>,
/// Graph of changes
change_graph: ChangeGraph,
pub(crate) history_index: HashMap<ChangeHash, usize>,
/// Mapping from change hash to vector clock at this state.
pub(crate) clocks: HashMap<ChangeHash, Clock>,
/// Mapping from actor index to list of seqs seen for them.
states: HashMap<usize, Vec<usize>>,
pub(crate) states: HashMap<usize, Vec<usize>>,
/// Current dependencies of this document (heads hashes).
deps: HashSet<ChangeHash>,
pub(crate) deps: HashSet<ChangeHash>,
/// Heads at the last save.
saved: Vec<ChangeHash>,
pub(crate) saved: Vec<ChangeHash>,
/// The set of operations that form this document.
ops: OpSet,
pub(crate) ops: OpSet,
/// The current actor.
actor: Actor,
pub(crate) actor: Actor,
/// The maximum operation counter this document has seen.
max_op: u64,
text_encoding: TextEncoding,
pub(crate) max_op: u64,
pub(crate) text_encoding: TextEncoding,
}
impl Automerge {
@ -110,7 +100,7 @@ impl Automerge {
queue: vec![],
history: vec![],
history_index: HashMap::new(),
change_graph: ChangeGraph::new(),
clocks: HashMap::new(),
states: HashMap::new(),
ops: Default::default(),
deps: Default::default(),
@ -121,50 +111,6 @@ impl Automerge {
}
}
pub(crate) fn ops_mut(&mut self) -> &mut OpSet {
&mut self.ops
}
pub(crate) fn ops(&self) -> &OpSet {
&self.ops
}
/// Whether this document has any operations
pub fn is_empty(&self) -> bool {
self.history.is_empty() && self.queue.is_empty()
}
pub(crate) fn actor_id(&self) -> ActorId {
match &self.actor {
Actor::Unused(id) => id.clone(),
Actor::Cached(idx) => self.ops.m.actors[*idx].clone(),
}
}
/// Remove the current actor from the opset if it has no ops
///
/// If the current actor ID has no ops in the opset then remove it from the cache of actor IDs.
/// This us used when rolling back a transaction. If the rolled back ops are the only ops for
/// the current actor then we want to remove that actor from the opset so it doesn't end up in
/// any saved version of the document.
///
/// # Panics
///
/// If the last actor in the OpSet is not the actor ID of this document
pub(crate) fn rollback_last_actor(&mut self) {
if let Actor::Cached(actor_idx) = self.actor {
if self.states.get(&actor_idx).is_none() && self.ops.m.actors.len() > 0 {
assert!(self.ops.m.actors.len() == actor_idx + 1);
let actor = self.ops.m.actors.remove_last();
self.actor = Actor::Unused(actor);
}
}
}
pub(crate) fn text_encoding(&self) -> TextEncoding {
self.text_encoding
}
/// Change the text encoding of this view of the document
///
/// This is a cheap operation, it just changes the way indexes are calculated
@ -430,26 +376,20 @@ impl Automerge {
/// Load a document.
pub fn load(data: &[u8]) -> Result<Self, AutomergeError> {
Self::load_with::<()>(data, OnPartialLoad::Error, VerificationMode::Check, None)
Self::load_with::<()>(data, VerificationMode::Check, None)
}
/// Load a document without verifying the head hashes
///
/// This is useful for debugging as it allows you to examine a corrupted document.
pub fn load_unverified_heads(data: &[u8]) -> Result<Self, AutomergeError> {
Self::load_with::<()>(
data,
OnPartialLoad::Error,
VerificationMode::DontCheck,
None,
)
Self::load_with::<()>(data, VerificationMode::DontCheck, None)
}
/// Load a document with an observer
#[tracing::instrument(skip(data, observer), err)]
pub fn load_with<Obs: OpObserver>(
data: &[u8],
on_error: OnPartialLoad,
mode: VerificationMode,
mut observer: Option<&mut Obs>,
) -> Result<Self, AutomergeError> {
@ -464,7 +404,6 @@ impl Automerge {
return Err(load::Error::BadChecksum.into());
}
let mut change: Option<Change> = None;
let mut am = match first_chunk {
storage::Chunk::Document(d) => {
tracing::trace!("first chunk is document chunk, inflating");
@ -473,18 +412,23 @@ impl Automerge {
result: op_set,
changes,
heads,
} = storage::load::reconstruct_document(&d, mode, OpSet::builder())
.map_err(|e| load::Error::InflateDocument(Box::new(e)))?;
} = match &mut observer {
Some(o) => {
storage::load::reconstruct_document(&d, mode, OpSet::observed_builder(*o))
}
None => storage::load::reconstruct_document(&d, mode, OpSet::builder()),
}
.map_err(|e| load::Error::InflateDocument(Box::new(e)))?;
let mut hashes_by_index = HashMap::new();
let mut actor_to_history: HashMap<usize, Vec<usize>> = HashMap::new();
let mut change_graph = ChangeGraph::new();
let mut clocks = Clocks::new();
for (index, change) in changes.iter().enumerate() {
// SAFETY: This should be fine because we just constructed an opset containing
// all the changes
let actor_index = op_set.m.actors.lookup(change.actor_id()).unwrap();
actor_to_history.entry(actor_index).or_default().push(index);
hashes_by_index.insert(index, change.hash());
change_graph.add_change(change, actor_index)?;
clocks.add_change(change, actor_index)?;
}
let history_index = hashes_by_index.into_iter().map(|(k, v)| (v, k)).collect();
Self {
@ -492,7 +436,7 @@ impl Automerge {
history: changes,
history_index,
states: actor_to_history,
change_graph,
clocks: clocks.into(),
ops: op_set,
deps: heads.into_iter().collect(),
saved: Default::default(),
@ -502,41 +446,33 @@ impl Automerge {
}
}
storage::Chunk::Change(stored_change) => {
tracing::trace!("first chunk is change chunk");
change = Some(
Change::new_from_unverified(stored_change.into_owned(), None)
.map_err(|e| load::Error::InvalidChangeColumns(Box::new(e)))?,
);
Self::new()
tracing::trace!("first chunk is change chunk, applying");
let change = Change::new_from_unverified(stored_change.into_owned(), None)
.map_err(|e| load::Error::InvalidChangeColumns(Box::new(e)))?;
let mut am = Self::new();
am.apply_change(change, &mut observer);
am
}
storage::Chunk::CompressedChange(stored_change, compressed) => {
tracing::trace!("first chunk is compressed change");
change = Some(
Change::new_from_unverified(
stored_change.into_owned(),
Some(compressed.into_owned()),
)
.map_err(|e| load::Error::InvalidChangeColumns(Box::new(e)))?,
);
Self::new()
tracing::trace!("first chunk is compressed change, decompressing and applying");
let change = Change::new_from_unverified(
stored_change.into_owned(),
Some(compressed.into_owned()),
)
.map_err(|e| load::Error::InvalidChangeColumns(Box::new(e)))?;
let mut am = Self::new();
am.apply_change(change, &mut observer);
am
}
};
tracing::trace!("loading change chunks");
tracing::trace!("first chunk loaded, loading remaining chunks");
match load::load_changes(remaining.reset()) {
load::LoadedChanges::Complete(c) => {
am.apply_changes(change.into_iter().chain(c))?;
if !am.queue.is_empty() {
return Err(AutomergeError::MissingDeps);
for change in c {
am.apply_change(change, &mut observer);
}
}
load::LoadedChanges::Partial { error, .. } => {
if on_error == OnPartialLoad::Error {
return Err(error.into());
}
}
}
if let Some(observer) = &mut observer {
current_state::observe_current_state(&am, *observer);
load::LoadedChanges::Partial { error, .. } => return Err(error.into()),
}
Ok(am)
}
@ -558,18 +494,6 @@ impl Automerge {
data: &[u8],
op_observer: Option<&mut Obs>,
) -> Result<usize, AutomergeError> {
if self.is_empty() {
let mut doc =
Self::load_with::<()>(data, OnPartialLoad::Ignore, VerificationMode::Check, None)?;
doc = doc
.with_encoding(self.text_encoding)
.with_actor(self.actor_id());
if let Some(obs) = op_observer {
current_state::observe_current_state(&doc, obs);
}
*self = doc;
return Ok(self.ops.len());
}
let changes = match load::load_changes(storage::parse::Input::new(data)) {
load::LoadedChanges::Complete(c) => c,
load::LoadedChanges::Partial { error, loaded, .. } => {
@ -610,11 +534,6 @@ impl Automerge {
changes: I,
mut op_observer: Option<&mut Obs>,
) -> Result<(), AutomergeError> {
// Record this so we can avoid observing each individual change and instead just observe
// the final state after all the changes have been applied. We can only do this for an
// empty document right now, once we have logic to produce the diffs between arbitrary
// states of the OpSet we can make this cleaner.
let empty_at_start = self.is_empty();
for c in changes {
if !self.history_index.contains_key(&c.hash()) {
if self.duplicate_seq(&c) {
@ -624,11 +543,7 @@ impl Automerge {
));
}
if self.is_causally_ready(&c) {
if empty_at_start {
self.apply_change::<()>(c, &mut None);
} else {
self.apply_change(c, &mut op_observer);
}
self.apply_change(c, &mut op_observer);
} else {
self.queue.push(c);
}
@ -636,16 +551,7 @@ impl Automerge {
}
while let Some(c) = self.pop_next_causally_ready_change() {
if !self.history_index.contains_key(&c.hash()) {
if empty_at_start {
self.apply_change::<()>(c, &mut None);
} else {
self.apply_change(c, &mut op_observer);
}
}
}
if empty_at_start {
if let Some(observer) = &mut op_observer {
current_state::observe_current_state(self, *observer);
self.apply_change(c, &mut op_observer);
}
}
Ok(())
@ -723,7 +629,7 @@ impl Automerge {
obj,
Op {
id,
action: OpType::from_action_and_value(c.action, c.val),
action: OpType::from_index_and_value(c.action, c.val).unwrap(),
key,
succ: Default::default(),
pred,
@ -766,7 +672,7 @@ impl Automerge {
let c = self.history.iter();
let bytes = crate::storage::save::save_document(
c,
self.ops.iter().map(|(objid, _, op)| (objid, op)),
self.ops.iter(),
&self.ops.m.actors,
&self.ops.m.props,
&heads,
@ -782,7 +688,7 @@ impl Automerge {
let c = self.history.iter();
let bytes = crate::storage::save::save_document(
c,
self.ops.iter().map(|(objid, _, op)| (objid, op)),
self.ops.iter(),
&self.ops.m.actors,
&self.ops.m.props,
&heads,
@ -825,8 +731,16 @@ impl Automerge {
.filter(|hash| self.history_index.contains_key(hash))
.copied()
.collect::<Vec<_>>();
let heads_clock = self.clock_at(&heads)?;
self.change_graph.remove_ancestors(changes, &heads);
// keep the hashes that are concurrent or after the heads
changes.retain(|hash| {
self.clocks
.get(hash)
.unwrap()
.partial_cmp(&heads_clock)
.map_or(true, |o| o == Ordering::Greater)
});
Ok(())
}
@ -834,7 +748,7 @@ impl Automerge {
/// Get the changes since `have_deps` in this document using a clock internally.
fn get_changes_clock(&self, have_deps: &[ChangeHash]) -> Result<Vec<&Change>, AutomergeError> {
// get the clock for the given deps
let clock = self.clock_at(have_deps);
let clock = self.clock_at(have_deps)?;
// get the documents current clock
@ -868,8 +782,26 @@ impl Automerge {
.find(|c| c.actor_id() == self.get_actor());
}
fn clock_at(&self, heads: &[ChangeHash]) -> Clock {
self.change_graph.clock_for_heads(heads)
fn clock_at(&self, heads: &[ChangeHash]) -> Result<Clock, AutomergeError> {
if let Some(first_hash) = heads.first() {
let mut clock = self
.clocks
.get(first_hash)
.ok_or(AutomergeError::MissingHash(*first_hash))?
.clone();
for hash in &heads[1..] {
let c = self
.clocks
.get(hash)
.ok_or(AutomergeError::MissingHash(*hash))?;
clock.merge(c);
}
Ok(clock)
} else {
Ok(Clock::new())
}
}
fn get_hash(&self, actor: usize, seq: u64) -> Result<ChangeHash, AutomergeError> {
@ -895,10 +827,24 @@ impl Automerge {
.push(history_index);
self.history_index.insert(change.hash(), history_index);
self.change_graph
.add_change(&change, actor_index)
.expect("Change's deps should already be in the document");
let mut clock = Clock::new();
for hash in change.deps() {
let c = self
.clocks
.get(hash)
.expect("Change's deps should already be in the document");
clock.merge(c);
}
clock.include(
actor_index,
ClockData {
max_op: change.max_op(),
seq: change.seq(),
},
);
self.clocks.insert(change.hash(), clock);
self.history_index.insert(change.hash(), history_index);
self.history.push(change);
history_index
@ -955,7 +901,7 @@ impl Automerge {
"pred",
"succ"
);
for (obj, _, op) in self.ops.iter() {
for (obj, op) in self.ops.iter() {
let id = self.to_string(op.id);
let obj = self.to_string(obj);
let key = match op.key {
@ -1158,8 +1104,9 @@ impl ReadDoc for Automerge {
fn keys_at<O: AsRef<ExId>>(&self, obj: O, heads: &[ChangeHash]) -> KeysAt<'_, '_> {
if let Ok((obj, _)) = self.exid_to_obj(obj.as_ref()) {
let clock = self.clock_at(heads);
return KeysAt::new(self, self.ops.keys_at(obj, clock));
if let Ok(clock) = self.clock_at(heads) {
return KeysAt::new(self, self.ops.keys_at(obj, clock));
}
}
KeysAt::new(self, None)
}
@ -1183,9 +1130,10 @@ impl ReadDoc for Automerge {
heads: &[ChangeHash],
) -> MapRangeAt<'_, R> {
if let Ok((obj, _)) = self.exid_to_obj(obj.as_ref()) {
let clock = self.clock_at(heads);
let iter_range = self.ops.map_range_at(obj, range, clock);
return MapRangeAt::new(self, iter_range);
if let Ok(clock) = self.clock_at(heads) {
let iter_range = self.ops.map_range_at(obj, range, clock);
return MapRangeAt::new(self, iter_range);
}
}
MapRangeAt::new(self, None)
}
@ -1209,9 +1157,10 @@ impl ReadDoc for Automerge {
heads: &[ChangeHash],
) -> ListRangeAt<'_, R> {
if let Ok((obj, _)) = self.exid_to_obj(obj.as_ref()) {
let clock = self.clock_at(heads);
let iter_range = self.ops.list_range_at(obj, range, clock);
return ListRangeAt::new(self, iter_range);
if let Ok(clock) = self.clock_at(heads) {
let iter_range = self.ops.list_range_at(obj, range, clock);
return ListRangeAt::new(self, iter_range);
}
}
ListRangeAt::new(self, None)
}
@ -1230,20 +1179,20 @@ impl ReadDoc for Automerge {
fn values_at<O: AsRef<ExId>>(&self, obj: O, heads: &[ChangeHash]) -> Values<'_> {
if let Ok((obj, obj_type)) = self.exid_to_obj(obj.as_ref()) {
let clock = self.clock_at(heads);
match obj_type {
ObjType::Map | ObjType::Table => {
let iter_range = self.ops.map_range_at(obj, .., clock);
Values::new(self, iter_range)
}
ObjType::List | ObjType::Text => {
let iter_range = self.ops.list_range_at(obj, .., clock);
Values::new(self, iter_range)
}
if let Ok(clock) = self.clock_at(heads) {
return match obj_type {
ObjType::Map | ObjType::Table => {
let iter_range = self.ops.map_range_at(obj, .., clock);
Values::new(self, iter_range)
}
ObjType::List | ObjType::Text => {
let iter_range = self.ops.list_range_at(obj, .., clock);
Values::new(self, iter_range)
}
};
}
} else {
Values::empty(self)
}
Values::empty(self)
}
fn length<O: AsRef<ExId>>(&self, obj: O) -> usize {
@ -1261,18 +1210,18 @@ impl ReadDoc for Automerge {
fn length_at<O: AsRef<ExId>>(&self, obj: O, heads: &[ChangeHash]) -> usize {
if let Ok((inner_obj, obj_type)) = self.exid_to_obj(obj.as_ref()) {
let clock = self.clock_at(heads);
if obj_type == ObjType::Map || obj_type == ObjType::Table {
self.keys_at(obj, heads).count()
} else {
let encoding = ListEncoding::new(obj_type, self.text_encoding);
self.ops
.search(&inner_obj, query::LenAt::new(clock, encoding))
.len
if let Ok(clock) = self.clock_at(heads) {
return if obj_type == ObjType::Map || obj_type == ObjType::Table {
self.keys_at(obj, heads).count()
} else {
let encoding = ListEncoding::new(obj_type, self.text_encoding);
self.ops
.search(&inner_obj, query::LenAt::new(clock, encoding))
.len
};
}
} else {
0
}
0
}
fn object_type<O: AsRef<ExId>>(&self, obj: O) -> Result<ObjType, AutomergeError> {
@ -1296,7 +1245,7 @@ impl ReadDoc for Automerge {
heads: &[ChangeHash],
) -> Result<String, AutomergeError> {
let obj = self.exid_to_obj(obj.as_ref())?.0;
let clock = self.clock_at(heads);
let clock = self.clock_at(heads)?;
let query = self.ops.search(&obj, query::ListValsAt::new(clock));
let mut buffer = String::new();
for q in &query.ops {
@ -1371,7 +1320,7 @@ impl ReadDoc for Automerge {
) -> Result<Vec<(Value<'_>, ExId)>, AutomergeError> {
let prop = prop.into();
let obj = self.exid_to_obj(obj.as_ref())?.0;
let clock = self.clock_at(heads);
let clock = self.clock_at(heads)?;
let result = match prop {
Prop::Map(p) => {
let prop = self.ops.m.props.lookup(&p);

View file

@ -1,915 +0,0 @@
use std::{borrow::Cow, collections::HashSet, iter::Peekable};
use itertools::Itertools;
use crate::{
types::{ElemId, Key, ListEncoding, ObjId, Op, OpId},
ObjType, OpObserver, OpType, ScalarValue, Value,
};
/// Traverse the "current" state of the document, notifying `observer`
///
/// The "current" state of the document is the set of visible operations. This function will
/// traverse that set of operations and call the corresponding methods on the `observer` as it
/// encounters values. The `observer` methods will be called in the order in which they appear in
/// the document. That is to say that the observer will be notified of parent objects before the
/// objects they contain and elements of a sequence will be notified in the order they occur.
///
/// Due to only notifying of visible operations the observer will only be called with `put`,
/// `insert`, and `splice`, operations.
pub(super) fn observe_current_state<O: OpObserver>(doc: &crate::Automerge, observer: &mut O) {
// The OpSet already exposes operations in the order they appear in the document.
// `OpSet::iter_objs` iterates over the objects in causal order, this means that parent objects
// will always appear before their children. Furthermore, the operations within each object are
// ordered by key (which means by their position in a sequence for sequences).
//
// Effectively then we iterate over each object, then we group the operations in the object by
// key and for each key find the visible operations for that key. Then we notify the observer
// for each of those visible operations.
let mut visible_objs = HashSet::new();
visible_objs.insert(ObjId::root());
for (obj, typ, ops) in doc.ops().iter_objs() {
if !visible_objs.contains(obj) {
continue;
}
let ops_by_key = ops.group_by(|o| o.key);
let actions = ops_by_key
.into_iter()
.flat_map(|(key, key_ops)| key_actions(key, key_ops));
if typ == ObjType::Text && !observer.text_as_seq() {
track_new_objs_and_notify(
&mut visible_objs,
doc,
obj,
typ,
observer,
text_actions(actions),
)
} else if typ == ObjType::List {
track_new_objs_and_notify(
&mut visible_objs,
doc,
obj,
typ,
observer,
list_actions(actions),
)
} else {
track_new_objs_and_notify(&mut visible_objs, doc, obj, typ, observer, actions)
}
}
}
fn track_new_objs_and_notify<N: Action, I: Iterator<Item = N>, O: OpObserver>(
visible_objs: &mut HashSet<ObjId>,
doc: &crate::Automerge,
obj: &ObjId,
typ: ObjType,
observer: &mut O,
actions: I,
) {
let exid = doc.id_to_exid(obj.0);
for action in actions {
if let Some(obj) = action.made_object() {
visible_objs.insert(obj);
}
action.notify_observer(doc, &exid, obj, typ, observer);
}
}
trait Action {
/// Notify an observer of whatever this action does
fn notify_observer<O: OpObserver>(
self,
doc: &crate::Automerge,
exid: &crate::ObjId,
obj: &ObjId,
typ: ObjType,
observer: &mut O,
);
/// If this action created an object, return the ID of that object
fn made_object(&self) -> Option<ObjId>;
}
fn key_actions<'a, I: Iterator<Item = &'a Op>>(
key: Key,
key_ops: I,
) -> impl Iterator<Item = SimpleAction<'a>> {
#[derive(Clone)]
enum CurrentOp<'a> {
Put {
value: Value<'a>,
id: OpId,
conflicted: bool,
},
Insert(Value<'a>, OpId),
}
let current_ops = key_ops
.filter(|o| o.visible())
.filter_map(|o| match o.action {
OpType::Make(obj_type) => {
let value = Value::Object(obj_type);
if o.insert {
Some(CurrentOp::Insert(value, o.id))
} else {
Some(CurrentOp::Put {
value,
id: o.id,
conflicted: false,
})
}
}
OpType::Put(ref value) => {
let value = Value::Scalar(Cow::Borrowed(value));
if o.insert {
Some(CurrentOp::Insert(value, o.id))
} else {
Some(CurrentOp::Put {
value,
id: o.id,
conflicted: false,
})
}
}
_ => None,
});
current_ops
.coalesce(|previous, current| match (previous, current) {
(CurrentOp::Put { .. }, CurrentOp::Put { value, id, .. }) => Ok(CurrentOp::Put {
value,
id,
conflicted: true,
}),
(previous, current) => Err((previous, current)),
})
.map(move |op| match op {
CurrentOp::Put {
value,
id,
conflicted,
} => SimpleAction::Put {
prop: key,
tagged_value: (value, id),
conflict: conflicted,
},
CurrentOp::Insert(val, id) => SimpleAction::Insert {
elem_id: ElemId(id),
tagged_value: (val, id),
},
})
}
/// Either a "put" or "insert" action. i.e. not splicing for text values
enum SimpleAction<'a> {
Put {
prop: Key,
tagged_value: (Value<'a>, OpId),
conflict: bool,
},
Insert {
elem_id: ElemId,
tagged_value: (Value<'a>, OpId),
},
}
impl<'a> Action for SimpleAction<'a> {
fn notify_observer<O: OpObserver>(
self,
doc: &crate::Automerge,
exid: &crate::ObjId,
obj: &ObjId,
typ: ObjType,
observer: &mut O,
) {
let encoding = match typ {
ObjType::Text => ListEncoding::Text(doc.text_encoding()),
_ => ListEncoding::List,
};
match self {
Self::Put {
prop,
tagged_value,
conflict,
} => {
let tagged_value = (tagged_value.0, doc.id_to_exid(tagged_value.1));
let prop = doc.ops().export_key(*obj, prop, encoding).unwrap();
observer.put(doc, exid.clone(), prop, tagged_value, conflict);
}
Self::Insert {
elem_id,
tagged_value: (value, opid),
} => {
let index = doc
.ops()
.search(obj, crate::query::ElemIdPos::new(elem_id, encoding))
.index()
.unwrap();
let tagged_value = (value, doc.id_to_exid(opid));
observer.insert(doc, doc.id_to_exid(obj.0), index, tagged_value);
}
}
}
fn made_object(&self) -> Option<ObjId> {
match self {
Self::Put {
tagged_value: (Value::Object(_), id),
..
} => Some((*id).into()),
Self::Insert {
tagged_value: (Value::Object(_), id),
..
} => Some((*id).into()),
_ => None,
}
}
}
/// An `Action` which splices for text values
enum TextAction<'a> {
Action(SimpleAction<'a>),
Splice { start: ElemId, chars: String },
}
impl<'a> Action for TextAction<'a> {
fn notify_observer<O: OpObserver>(
self,
doc: &crate::Automerge,
exid: &crate::ObjId,
obj: &ObjId,
typ: ObjType,
observer: &mut O,
) {
match self {
Self::Action(action) => action.notify_observer(doc, exid, obj, typ, observer),
Self::Splice { start, chars } => {
let index = doc
.ops()
.search(
obj,
crate::query::ElemIdPos::new(
start,
ListEncoding::Text(doc.text_encoding()),
),
)
.index()
.unwrap();
observer.splice_text(doc, doc.id_to_exid(obj.0), index, chars.as_str());
}
}
}
fn made_object(&self) -> Option<ObjId> {
match self {
Self::Action(action) => action.made_object(),
_ => None,
}
}
}
fn list_actions<'a, I: Iterator<Item = SimpleAction<'a>>>(
actions: I,
) -> impl Iterator<Item = SimpleAction<'a>> {
actions.map(|a| match a {
SimpleAction::Put {
prop: Key::Seq(elem_id),
tagged_value,
..
} => SimpleAction::Insert {
elem_id,
tagged_value,
},
a => a,
})
}
/// Condense consecutive `SimpleAction::Insert` actions into one `TextAction::Splice`
fn text_actions<'a, I>(actions: I) -> impl Iterator<Item = TextAction<'a>>
where
I: Iterator<Item = SimpleAction<'a>>,
{
TextActions {
ops: actions.peekable(),
}
}
struct TextActions<'a, I: Iterator<Item = SimpleAction<'a>>> {
ops: Peekable<I>,
}
impl<'a, I: Iterator<Item = SimpleAction<'a>>> Iterator for TextActions<'a, I> {
type Item = TextAction<'a>;
fn next(&mut self) -> Option<Self::Item> {
if let Some(SimpleAction::Insert { .. }) = self.ops.peek() {
let (start, value) = match self.ops.next() {
Some(SimpleAction::Insert {
tagged_value: (value, opid),
..
}) => (opid, value),
_ => unreachable!(),
};
let mut chars = match value {
Value::Scalar(Cow::Borrowed(ScalarValue::Str(s))) => s.to_string(),
_ => "\u{fffc}".to_string(),
};
while let Some(SimpleAction::Insert { .. }) = self.ops.peek() {
if let Some(SimpleAction::Insert {
tagged_value: (value, _),
..
}) = self.ops.next()
{
match value {
Value::Scalar(Cow::Borrowed(ScalarValue::Str(s))) => chars.push_str(s),
_ => chars.push('\u{fffc}'),
}
}
}
Some(TextAction::Splice {
start: ElemId(start),
chars,
})
} else {
self.ops.next().map(TextAction::Action)
}
}
}
#[cfg(test)]
mod tests {
use std::{borrow::Cow, fs};
use crate::{transaction::Transactable, Automerge, ObjType, OpObserver, Prop, ReadDoc, Value};
// Observer ops often carry a "tagged value", which is a value and the OpID of the op which
// created that value. For a lot of values (i.e. any scalar value) we don't care about the
// opid. This type implements `PartialEq` for the `Untagged` variant by ignoring the tag, which
// allows us to express tests which don't care about the tag.
#[derive(Clone, Debug)]
enum ObservedValue {
Tagged(crate::Value<'static>, crate::ObjId),
Untagged(crate::Value<'static>),
}
impl<'a> From<(Value<'a>, crate::ObjId)> for ObservedValue {
fn from(value: (Value<'a>, crate::ObjId)) -> Self {
Self::Tagged(value.0.into_owned(), value.1)
}
}
impl PartialEq<ObservedValue> for ObservedValue {
fn eq(&self, other: &ObservedValue) -> bool {
match (self, other) {
(Self::Tagged(v1, o1), Self::Tagged(v2, o2)) => equal_vals(v1, v2) && o1 == o2,
(Self::Untagged(v1), Self::Untagged(v2)) => equal_vals(v1, v2),
(Self::Tagged(v1, _), Self::Untagged(v2)) => equal_vals(v1, v2),
(Self::Untagged(v1), Self::Tagged(v2, _)) => equal_vals(v1, v2),
}
}
}
/// Consider counters equal if they have the same current value
fn equal_vals(v1: &Value<'_>, v2: &Value<'_>) -> bool {
match (v1, v2) {
(Value::Scalar(v1), Value::Scalar(v2)) => match (v1.as_ref(), v2.as_ref()) {
(crate::ScalarValue::Counter(c1), crate::ScalarValue::Counter(c2)) => {
c1.current == c2.current
}
_ => v1 == v2,
},
_ => v1 == v2,
}
}
#[derive(Debug, Clone, PartialEq)]
enum ObserverCall {
Put {
obj: crate::ObjId,
prop: Prop,
value: ObservedValue,
conflict: bool,
},
Insert {
obj: crate::ObjId,
index: usize,
value: ObservedValue,
},
SpliceText {
obj: crate::ObjId,
index: usize,
chars: String,
},
}
// A Vec<ObserverCall> is pretty hard to look at in a test failure. This wrapper prints the
// calls out in a nice table so it's easier to see what's different
#[derive(Clone, PartialEq)]
struct Calls(Vec<ObserverCall>);
impl std::fmt::Debug for Calls {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut table = prettytable::Table::new();
table.set_format(*prettytable::format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
table.set_titles(prettytable::row![
"Op", "Object", "Property", "Value", "Conflict"
]);
for call in &self.0 {
match call {
ObserverCall::Put {
obj,
prop,
value,
conflict,
} => {
table.add_row(prettytable::row![
"Put",
format!("{}", obj),
prop,
match value {
ObservedValue::Tagged(v, o) => format!("{} ({})", v, o),
ObservedValue::Untagged(v) => format!("{}", v),
},
conflict
]);
}
ObserverCall::Insert { obj, index, value } => {
table.add_row(prettytable::row![
"Insert",
format!("{}", obj),
index,
match value {
ObservedValue::Tagged(v, o) => format!("{} ({})", v, o),
ObservedValue::Untagged(v) => format!("{}", v),
},
""
]);
}
ObserverCall::SpliceText { obj, index, chars } => {
table.add_row(prettytable::row![
"SpliceText",
format!("{}", obj),
index,
chars,
""
]);
}
}
}
let mut out = Vec::new();
table.print(&mut out).unwrap();
write!(f, "\n{}\n", String::from_utf8(out).unwrap())
}
}
struct ObserverStub {
ops: Vec<ObserverCall>,
text_as_seq: bool,
}
impl ObserverStub {
fn new() -> Self {
Self {
ops: Vec::new(),
text_as_seq: true,
}
}
fn new_text_v2() -> Self {
Self {
ops: Vec::new(),
text_as_seq: false,
}
}
}
impl OpObserver for ObserverStub {
fn insert<R: ReadDoc>(
&mut self,
_doc: &R,
objid: crate::ObjId,
index: usize,
tagged_value: (crate::Value<'_>, crate::ObjId),
) {
self.ops.push(ObserverCall::Insert {
obj: objid,
index,
value: tagged_value.into(),
});
}
fn splice_text<R: ReadDoc>(
&mut self,
_doc: &R,
objid: crate::ObjId,
index: usize,
value: &str,
) {
self.ops.push(ObserverCall::SpliceText {
obj: objid,
index,
chars: value.to_string(),
});
}
fn put<R: ReadDoc>(
&mut self,
_doc: &R,
objid: crate::ObjId,
prop: crate::Prop,
tagged_value: (crate::Value<'_>, crate::ObjId),
conflict: bool,
) {
self.ops.push(ObserverCall::Put {
obj: objid,
prop,
value: tagged_value.into(),
conflict,
});
}
fn expose<R: ReadDoc>(
&mut self,
_doc: &R,
_objid: crate::ObjId,
_prop: crate::Prop,
_tagged_value: (crate::Value<'_>, crate::ObjId),
_conflict: bool,
) {
panic!("expose not expected");
}
fn increment<R: ReadDoc>(
&mut self,
_doc: &R,
_objid: crate::ObjId,
_prop: crate::Prop,
_tagged_value: (i64, crate::ObjId),
) {
panic!("increment not expected");
}
fn delete_map<R: ReadDoc>(&mut self, _doc: &R, _objid: crate::ObjId, _key: &str) {
panic!("delete not expected");
}
fn delete_seq<R: ReadDoc>(
&mut self,
_doc: &R,
_objid: crate::ObjId,
_index: usize,
_num: usize,
) {
panic!("delete not expected");
}
fn text_as_seq(&self) -> bool {
self.text_as_seq
}
}
#[test]
fn basic_test() {
let mut doc = crate::AutoCommit::new();
doc.put(crate::ROOT, "key", "value").unwrap();
let map = doc.put_object(crate::ROOT, "map", ObjType::Map).unwrap();
doc.put(&map, "nested_key", "value").unwrap();
let list = doc.put_object(crate::ROOT, "list", ObjType::List).unwrap();
doc.insert(&list, 0, "value").unwrap();
let text = doc.put_object(crate::ROOT, "text", ObjType::Text).unwrap();
doc.insert(&text, 0, "a").unwrap();
let mut obs = ObserverStub::new();
super::observe_current_state(doc.document(), &mut obs);
assert_eq!(
Calls(obs.ops),
Calls(vec![
ObserverCall::Put {
obj: crate::ROOT,
prop: "key".into(),
value: ObservedValue::Untagged("value".into()),
conflict: false,
},
ObserverCall::Put {
obj: crate::ROOT,
prop: "list".into(),
value: ObservedValue::Tagged(Value::Object(ObjType::List), list.clone()),
conflict: false,
},
ObserverCall::Put {
obj: crate::ROOT,
prop: "map".into(),
value: ObservedValue::Tagged(Value::Object(ObjType::Map), map.clone()),
conflict: false,
},
ObserverCall::Put {
obj: crate::ROOT,
prop: "text".into(),
value: ObservedValue::Tagged(Value::Object(ObjType::Text), text.clone()),
conflict: false,
},
ObserverCall::Put {
obj: map.clone(),
prop: "nested_key".into(),
value: ObservedValue::Untagged("value".into()),
conflict: false,
},
ObserverCall::Insert {
obj: list,
index: 0,
value: ObservedValue::Untagged("value".into()),
},
ObserverCall::Insert {
obj: text,
index: 0,
value: ObservedValue::Untagged("a".into()),
},
])
);
}
#[test]
fn test_deleted_ops_omitted() {
let mut doc = crate::AutoCommit::new();
doc.put(crate::ROOT, "key", "value").unwrap();
doc.delete(crate::ROOT, "key").unwrap();
let map = doc.put_object(crate::ROOT, "map", ObjType::Map).unwrap();
doc.put(&map, "nested_key", "value").unwrap();
doc.delete(&map, "nested_key").unwrap();
let list = doc.put_object(crate::ROOT, "list", ObjType::List).unwrap();
doc.insert(&list, 0, "value").unwrap();
doc.delete(&list, 0).unwrap();
let text = doc.put_object(crate::ROOT, "text", ObjType::Text).unwrap();
doc.insert(&text, 0, "a").unwrap();
doc.delete(&text, 0).unwrap();
doc.put_object(crate::ROOT, "deleted_map", ObjType::Map)
.unwrap();
doc.delete(crate::ROOT, "deleted_map").unwrap();
doc.put_object(crate::ROOT, "deleted_list", ObjType::List)
.unwrap();
doc.delete(crate::ROOT, "deleted_list").unwrap();
doc.put_object(crate::ROOT, "deleted_text", ObjType::Text)
.unwrap();
doc.delete(crate::ROOT, "deleted_text").unwrap();
let mut obs = ObserverStub::new();
super::observe_current_state(doc.document(), &mut obs);
assert_eq!(
Calls(obs.ops),
Calls(vec![
ObserverCall::Put {
obj: crate::ROOT,
prop: "list".into(),
value: ObservedValue::Tagged(Value::Object(ObjType::List), list.clone()),
conflict: false,
},
ObserverCall::Put {
obj: crate::ROOT,
prop: "map".into(),
value: ObservedValue::Tagged(Value::Object(ObjType::Map), map.clone()),
conflict: false,
},
ObserverCall::Put {
obj: crate::ROOT,
prop: "text".into(),
value: ObservedValue::Tagged(Value::Object(ObjType::Text), text.clone()),
conflict: false,
},
])
);
}
#[test]
fn test_text_spliced() {
let mut doc = crate::AutoCommit::new();
let text = doc.put_object(crate::ROOT, "text", ObjType::Text).unwrap();
doc.insert(&text, 0, "a").unwrap();
doc.splice_text(&text, 1, 0, "bcdef").unwrap();
doc.splice_text(&text, 2, 2, "g").unwrap();
let mut obs = ObserverStub::new_text_v2();
super::observe_current_state(doc.document(), &mut obs);
assert_eq!(
Calls(obs.ops),
Calls(vec![
ObserverCall::Put {
obj: crate::ROOT,
prop: "text".into(),
value: ObservedValue::Tagged(Value::Object(ObjType::Text), text.clone()),
conflict: false,
},
ObserverCall::SpliceText {
obj: text,
index: 0,
chars: "abgef".to_string()
}
])
);
}
#[test]
fn test_counters() {
let actor1 = crate::ActorId::from("aa".as_bytes());
let actor2 = crate::ActorId::from("bb".as_bytes());
let mut doc = crate::AutoCommit::new().with_actor(actor2);
let mut doc2 = doc.fork().with_actor(actor1);
doc2.put(crate::ROOT, "key", "someval").unwrap();
doc.put(crate::ROOT, "key", crate::ScalarValue::Counter(1.into()))
.unwrap();
doc.increment(crate::ROOT, "key", 2).unwrap();
doc.increment(crate::ROOT, "key", 3).unwrap();
doc.merge(&mut doc2).unwrap();
let mut obs = ObserverStub::new_text_v2();
super::observe_current_state(doc.document(), &mut obs);
assert_eq!(
Calls(obs.ops),
Calls(vec![ObserverCall::Put {
obj: crate::ROOT,
prop: "key".into(),
value: ObservedValue::Untagged(Value::Scalar(Cow::Owned(
crate::ScalarValue::Counter(6.into())
))),
conflict: true,
},])
);
}
#[test]
fn test_multiple_list_insertions() {
let mut doc = crate::AutoCommit::new();
let list = doc.put_object(crate::ROOT, "list", ObjType::List).unwrap();
doc.insert(&list, 0, 1).unwrap();
doc.insert(&list, 1, 2).unwrap();
let mut obs = ObserverStub::new_text_v2();
super::observe_current_state(doc.document(), &mut obs);
assert_eq!(
Calls(obs.ops),
Calls(vec![
ObserverCall::Put {
obj: crate::ROOT,
prop: "list".into(),
value: ObservedValue::Tagged(Value::Object(ObjType::List), list.clone()),
conflict: false,
},
ObserverCall::Insert {
obj: list.clone(),
index: 0,
value: ObservedValue::Untagged(1.into()),
},
ObserverCall::Insert {
obj: list,
index: 1,
value: ObservedValue::Untagged(2.into()),
},
])
);
}
#[test]
fn test_concurrent_insertions_at_same_index() {
let mut doc = crate::AutoCommit::new().with_actor(crate::ActorId::from("aa".as_bytes()));
let list = doc.put_object(crate::ROOT, "list", ObjType::List).unwrap();
let mut doc2 = doc.fork().with_actor(crate::ActorId::from("bb".as_bytes()));
doc.insert(&list, 0, 1).unwrap();
doc2.insert(&list, 0, 2).unwrap();
doc.merge(&mut doc2).unwrap();
let mut obs = ObserverStub::new_text_v2();
super::observe_current_state(doc.document(), &mut obs);
assert_eq!(
Calls(obs.ops),
Calls(vec![
ObserverCall::Put {
obj: crate::ROOT,
prop: "list".into(),
value: ObservedValue::Tagged(Value::Object(ObjType::List), list.clone()),
conflict: false,
},
ObserverCall::Insert {
obj: list.clone(),
index: 0,
value: ObservedValue::Untagged(2.into()),
},
ObserverCall::Insert {
obj: list,
index: 1,
value: ObservedValue::Untagged(1.into()),
},
])
);
}
#[test]
fn test_insert_objects() {
let mut doc = crate::AutoCommit::new().with_actor(crate::ActorId::from("aa".as_bytes()));
let list = doc.put_object(crate::ROOT, "list", ObjType::List).unwrap();
let map = doc.insert_object(&list, 0, ObjType::Map).unwrap();
doc.put(&map, "key", "value").unwrap();
let mut obs = ObserverStub::new_text_v2();
super::observe_current_state(doc.document(), &mut obs);
assert_eq!(
Calls(obs.ops),
Calls(vec![
ObserverCall::Put {
obj: crate::ROOT,
prop: "list".into(),
value: ObservedValue::Tagged(Value::Object(ObjType::List), list.clone()),
conflict: false,
},
ObserverCall::Insert {
obj: list.clone(),
index: 0,
value: ObservedValue::Tagged(Value::Object(ObjType::Map), map.clone()),
},
ObserverCall::Put {
obj: map,
prop: "key".into(),
value: ObservedValue::Untagged("value".into()),
conflict: false
},
])
);
}
#[test]
fn test_insert_and_update() {
let mut doc = crate::AutoCommit::new();
let list = doc.put_object(crate::ROOT, "list", ObjType::List).unwrap();
doc.insert(&list, 0, "one").unwrap();
doc.insert(&list, 1, "two").unwrap();
doc.put(&list, 0, "three").unwrap();
doc.put(&list, 1, "four").unwrap();
let mut obs = ObserverStub::new_text_v2();
super::observe_current_state(doc.document(), &mut obs);
assert_eq!(
Calls(obs.ops),
Calls(vec![
ObserverCall::Put {
obj: crate::ROOT,
prop: "list".into(),
value: ObservedValue::Tagged(Value::Object(ObjType::List), list.clone()),
conflict: false,
},
ObserverCall::Insert {
obj: list.clone(),
index: 0,
value: ObservedValue::Untagged("three".into()),
},
ObserverCall::Insert {
obj: list.clone(),
index: 1,
value: ObservedValue::Untagged("four".into()),
},
])
);
}
#[test]
fn test_load_changes() {
fn fixture(name: &str) -> Vec<u8> {
fs::read("./tests/fixtures/".to_owned() + name).unwrap()
}
let mut obs = ObserverStub::new();
let _doc = Automerge::load_with(
&fixture("counter_value_is_ok.automerge"),
crate::OnPartialLoad::Error,
crate::storage::VerificationMode::Check,
Some(&mut obs),
);
assert_eq!(
Calls(obs.ops),
Calls(vec![ObserverCall::Put {
obj: crate::ROOT,
prop: "a".into(),
value: ObservedValue::Untagged(crate::ScalarValue::Counter(2000.into()).into()),
conflict: false,
},])
);
}
}

View file

@ -1507,11 +1507,6 @@ fn observe_counter_change_application() {
let changes = doc.get_changes(&[]).unwrap().into_iter().cloned();
let mut new_doc = AutoCommit::new().with_observer(VecOpObserver::default());
// make a new change to the doc to stop the empty doc logic from skipping the intermediate
// patches. The is probably not really necessary, we could update this test to just test that
// the correct final state is emitted. For now though, we leave it as is.
new_doc.put(ROOT, "foo", "bar").unwrap();
new_doc.observer().take_patches();
new_doc.apply_changes(changes).unwrap();
assert_eq!(
new_doc.observer().take_patches(),

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