From aa99a9b3db8c21abdaca319ce24492846b6d7a96 Mon Sep 17 00:00:00 2001 From: Jacob Phillips Date: Fri, 1 Mar 2024 10:12:30 -0500 Subject: [PATCH] Release/0.3.0 (#16) * remove improper categories * make the generated builder infallible, but lose new/default. * remove use/import of traits. * absorb QueryFields into Entity (fewer use statements for users) * remove node/relation shorthand attributes * rename attr: stamps -> timestamps * update examples, test, clippy * build and test the workspace * rename ci action so its more descriptive in the badge * rename Entity to FieldSet, so it's more clear it is more abstract * wip changing readme * add/move scripts * rename as_query* to to_query* because it's not free * add as_query_fields and as_query_obj, static query string that ignores timestamps * rename fn * write a bunch of test fragments as includes * fix var used * upgrade and sort deps * update devcontainer to use my prebuilt rust image * update scripts * fix neo4rs usage (breaking in 0.7), run ./fix.sh * run ./fix.sh * prevent clippy from removing pub use that is actually used in the tests. * need to add the 1.0 tag to dockerfile (no latest) * merge changes from 'breaking 0.7' branch * update readmes, changelog, and bump version * bump macros version --- .devcontainer/Dockerfile | 2 +- .devcontainer/devcontainer.json | 7 +- .devcontainer/docker-compose.yml | 5 + .devcontainer/post-create.sh | 10 +- .github/workflows/ci.yml | 21 ++ .github/workflows/rust.yml | 26 -- CHANGELOG.md | 14 + Cargo.lock | 278 +++++++++++++++---- Cargo.toml | 2 +- coverage.sh | 4 + doc.sh | 4 + example/Cargo.toml | 9 +- example/src/person.rs | 28 +- example/src/worked_at.rs | 4 +- example/tests/basic_crud.rs | 4 +- example/tests/common/container.rs | 140 +++++++--- example/tests/manual_queries.rs | 10 +- fix.sh | 7 + lib/Cargo.toml | 20 +- lib/README.md | 58 ++-- lib/coverage.sh | 5 - lib/include/create_query.rs | 16 ++ lib/include/exec/create_and_read_node.rs | 28 ++ lib/include/exec/create_and_read_relation.rs | 40 +++ lib/include/id.rs | 10 + lib/include/id_inferred.rs | 9 + lib/include/id_multi.rs | 11 + lib/include/read_query.rs | 17 ++ lib/include/relationships.rs | 44 +++ lib/include/rename.rs | 12 + lib/include/static_strings.rs | 10 + lib/src/entity.rs | 233 +++++++++------- lib/src/lib.rs | 13 +- lib/src/node.rs | 30 +- lib/src/relationship.rs | 80 +++--- lib/src/stamps.rs | 10 +- lib/tests/basic_crud.rs | 2 +- lib/tests/common/container.rs | 140 +++++++--- lib/tests/common/fixtures/company.rs | 44 +-- lib/tests/common/fixtures/mod.rs | 1 + lib/tests/common/fixtures/person.rs | 46 +-- lib/tests/common/fixtures/worked_at.rs | 26 +- lib/tests/common/fixtures/works_at.rs | 24 +- lib/tests/manual_queries.rs | 12 +- macros/Cargo.toml | 4 +- macros/README.md | 39 +-- macros/src/derive.rs | 3 - macros/src/derive/entity.rs | 23 +- macros/src/derive/entity/builder.rs | 52 ++-- macros/src/derive/entity/field/field_type.rs | 2 +- macros/src/derive/entity/field/map_helper.rs | 14 +- macros/src/derive/node.rs | 2 - macros/src/derive/relation.rs | 2 - macros/src/lib.rs | 34 +-- macros/src/node_relation.rs | 79 ------ macros/src/{stamps.rs => timestamps.rs} | 24 +- reset-test-database.sh | 4 + test-fast.sh | 5 - test-isolated.sh | 4 + test.sh | 2 + 60 files changed, 1123 insertions(+), 686 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/rust.yml create mode 100755 coverage.sh create mode 100755 doc.sh create mode 100755 fix.sh delete mode 100755 lib/coverage.sh create mode 100644 lib/include/create_query.rs create mode 100644 lib/include/exec/create_and_read_node.rs create mode 100644 lib/include/exec/create_and_read_relation.rs create mode 100644 lib/include/id.rs create mode 100644 lib/include/id_inferred.rs create mode 100644 lib/include/id_multi.rs create mode 100644 lib/include/read_query.rs create mode 100644 lib/include/relationships.rs create mode 100644 lib/include/rename.rs create mode 100644 lib/include/static_strings.rs delete mode 100644 macros/src/node_relation.rs rename macros/src/{stamps.rs => timestamps.rs} (80%) create mode 100755 reset-test-database.sh delete mode 100755 test-fast.sh create mode 100755 test-isolated.sh create mode 100755 test.sh diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 8ec05c1..9485362 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1 +1 @@ -FROM mcr.microsoft.com/devcontainers/rust:bookworm +FROM jifalops/rust:1.0 diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index caf4140..7d067ec 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -2,17 +2,14 @@ "name": "Cypher DTO", "dockerComposeFile": "./docker-compose.yml", "service": "app", + "remoteUser": "developer", "workspaceFolder": "/cypher-dto", "postCreateCommand": ".devcontainer/post-create.sh", - "features": { - "ghcr.io/devcontainers/features/github-cli:1": {}, - "ghcr.io/devcontainers/features/docker-in-docker:2": {} - }, "customizations": { "vscode": { "extensions": [ "rust-lang.rust-analyzer", - "JScearcy.rust-doc-viewer", + "serayuzgur.crates", "tamasfe.even-better-toml" ] } diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 3db83cf..39a5f04 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -11,7 +11,12 @@ services: volumes: - ..:/cypher-dto:cached environment: + PROJECT_ROOT: /cypher-dto GITHUB_TOKEN: ${GITHUB_TOKEN} + TZ: ${TZ} + NEO4J_TEST_URI: "bolt://db:7687" + NEO4J_TEST_USER: "neo4j" + NEO4J_TEST_PASS: "developer" db: image: neo4j:5 diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh index 029d131..0af1ca9 100755 --- a/.devcontainer/post-create.sh +++ b/.devcontainer/post-create.sh @@ -1,14 +1,8 @@ # Ensure devcontainer user owns the project directory -sudo chown -R vscode:vscode /cypher-dto +sudo chown -R developer:developer $PROJECT_ROOT # Remember history on the local machine -ln -s /cypher-dto/.devcontainer/.bash_history ~/.bash_history +ln -s $PROJECT_ROOT/.devcontainer/.bash_history ~/.bash_history # Install dotfiles gh repo clone dotfiles ~/dotfiles && ~/dotfiles/install.sh - -# rustup and cargo bash completion. -sudo apt-get update -qq && sudo apt-get install -y -qq --no-install-recommends bash-completion \ - && mkdir -p ~/.local/share/bash-completion/completions \ - && rustup completions bash > ~/.local/share/bash-completion/completions/rustup \ - && rustup completions bash cargo > ~/.local/share/bash-completion/completions/cargo diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..464d816 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,21 @@ +name: Rust + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Build + run: cargo build --verbose + - name: Run tests + run: cargo test --verbose diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml deleted file mode 100644 index 3fde6d6..0000000 --- a/.github/workflows/rust.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: Rust - -on: - push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] - -env: - CARGO_TERM_COLOR: always - -jobs: - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - name: Build - run: | - cd lib - cargo build --verbose - - name: Run tests - run: | - cd example - cargo test --verbose diff --git a/CHANGELOG.md b/CHANGELOG.md index c9d4b5e..69054a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Change log +## v0.3.0 + +- Upgrade to neo4rs 0.7.1 +- Add a .devcontainer for easy development and contribution. +- Breaking: remove #[node] and #[relation] attributes +- Breaking: renamed the #[stamps] attribute to #[timestamps] +- Breaking: rename derive macros to `Node` and `Relation`. +- Breaking: renamed `as_query_fields()` to `to_query_fields()` and added a parameterless `as_query_fields()` that returns a static string. +- Breaking: renamed `as_query_obj()` to `to_query_obj()` and added a parameterless `as_query_obj()` that returns a static string. +- Breaking: the generated builder struct is now infallible. They no longer have `new()` or `default()` methods. + This is a better fit for the intended use case, modifying an existing entity. +- `Entity` and `QueryFields` have been combined into one struct named `FieldSet`. This is not breaking for macros/derive, but is breaking if you were implementing them manually. +- Share some tests between docs and code. + ## v0.2.0 - Fix handling of Option types in getters and parameters (#2) diff --git a/Cargo.lock b/Cargo.lock index 8ad82c1..7c6495f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -49,7 +49,7 @@ checksum = "a564d521dd56509c4c47480d00b80ee55f7e385ae48db5744c67ad50c92d2ebf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.26", + "syn 2.0.52", ] [[package]] @@ -96,11 +96,10 @@ dependencies = [ [[package]] name = "bollard-stubs" -version = "1.41.0" +version = "1.42.0-rc.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2f2e73fffe9455141e170fb9c1feb0ac521ec7e7dcd47a7cab72a658490fb8" +checksum = "ed59b5c00048f48d7af971b71f800fdf23e858844a6f9e4d32ca72e9399e7864" dependencies = [ - "chrono", "serde", "serde_with", ] @@ -113,9 +112,12 @@ checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" [[package]] name = "bytes" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +dependencies = [ + "serde", +] [[package]] name = "cc" @@ -145,6 +147,28 @@ dependencies = [ "winapi", ] +[[package]] +name = "chrono-tz" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59ae0466b83e838b81a54256c39d5d7c20b9d7daa10510a242d9b75abd5936e" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf", +] + +[[package]] +name = "chrono-tz-build" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "433e39f13c9a060046954e0592a8d0a4bcb1040125cbf91cb8ee58964cfb350f" +dependencies = [ + "parse-zoneinfo", + "phf", + "phf_codegen", +] + [[package]] name = "convert_case" version = "0.6.0" @@ -156,9 +180,9 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "cpufeatures" @@ -181,16 +205,16 @@ dependencies = [ [[package]] name = "cypher-dto" -version = "0.2.0" +version = "0.3.0" dependencies = [ "chrono", "cypher-dto-macros", "lenient_semver", - "neo4j_testcontainers", "neo4rs", "pretty_env_logger", "serde", "testcontainers", + "testcontainers-modules", "thiserror", "tokio", "uuid", @@ -198,12 +222,12 @@ dependencies = [ [[package]] name = "cypher-dto-macros" -version = "0.2.0" +version = "0.3.0" dependencies = [ "convert_case", "quote", "serde", - "syn 2.0.26", + "syn 2.0.52", ] [[package]] @@ -260,6 +284,17 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eaa37046cc0f6c3cc6090fbdbf73ef0b8ef4cfcc37f6befc0020f63e8cf121e1" +[[package]] +name = "delegate" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee5df75c70b95bd3aacc8e2fd098797692fb1d54121019c4de481e42f04c8a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "digest" version = "0.10.7" @@ -312,10 +347,10 @@ dependencies = [ "chrono", "cypher-dto", "lenient_semver", - "neo4j_testcontainers", "neo4rs", "pretty_env_logger", "testcontainers", + "testcontainers-modules", "tokio", "uuid", ] @@ -391,7 +426,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.26", + "syn 2.0.52", ] [[package]] @@ -480,16 +515,16 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "iana-time-zone" -version = "0.1.57" +version = "0.1.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows", + "windows-core", ] [[package]] @@ -637,27 +672,22 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "neo4j_testcontainers" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f502cf0e7658163604bf3ac7975f642d8a6c2c1f4110509493dc1d35dc70b0f7" -dependencies = [ - "testcontainers", -] - [[package]] name = "neo4rs" -version = "0.7.0-alpha.1" -source = "git+https://github.com/neo4j-labs/neo4rs.git?rev=0bd8099017d3ad57a844c9a456e7aaaca5e4721e#0bd8099017d3ad57a844c9a456e7aaaca5e4721e" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1013d61f78c55571b95f0f2bad58dce2f5a346227e21271e8e5641f4d8e5919" dependencies = [ "async-trait", "bytes", "chrono", + "chrono-tz", "deadpool", + "delegate", "futures", "log", "neo4rs-macros", + "paste", "pin-project-lite", "serde", "thiserror", @@ -669,11 +699,12 @@ dependencies = [ [[package]] name = "neo4rs-macros" -version = "0.2.1" -source = "git+https://github.com/neo4j-labs/neo4rs.git?rev=0bd8099017d3ad57a844c9a456e7aaaca5e4721e#0bd8099017d3ad57a844c9a456e7aaaca5e4721e" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a0d57c55d2d1dc62a2b1d16a0a1079eb78d67c36bdf468d582ab4482ec7002" dependencies = [ "quote", - "syn 1.0.109", + "syn 2.0.52", ] [[package]] @@ -730,15 +761,68 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-targets", + "windows-targets 0.48.1", +] + +[[package]] +name = "parse-zoneinfo" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41" +dependencies = [ + "regex", ] +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + [[package]] name = "percent-encoding" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project-lite" version = "0.2.10" @@ -769,18 +853,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.66" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.31" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fe8a65d69dd0808184ebb5f836ab526bb259db23c657efa38711b1072ee47f0" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -955,22 +1039,22 @@ checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" [[package]] name = "serde" -version = "1.0.174" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b88756493a5bd5e5395d53baa70b194b05764ab85b59e43e4b8f4e1192fa9b1" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.174" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e5c3a298c7f978e53536f95a63bdc4c4a64550582f31a0359a9afda6aede62e" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.26", + "syn 2.0.52", ] [[package]] @@ -1026,6 +1110,12 @@ dependencies = [ "libc", ] +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + [[package]] name = "slab" version = "0.4.8" @@ -1082,9 +1172,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.26" +version = "2.0.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45c3457aacde3c65315de5031ec191ce46604304d2446e803d71ade03308d970" +checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" dependencies = [ "proc-macro2", "quote", @@ -1102,9 +1192,9 @@ dependencies = [ [[package]] name = "testcontainers" -version = "0.14.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e2b1567ca8a2b819ea7b28c92be35d9f76fb9edb214321dcc86eb96023d1f87" +checksum = "f83d2931d7f521af5bae989f716c3fa43a6af9af7ec7a5e21b59ae40878cec00" dependencies = [ "bollard-stubs", "futures", @@ -1117,6 +1207,15 @@ dependencies = [ "sha2", ] +[[package]] +name = "testcontainers-modules" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c391cd115649a8a14e5638d0606648d5348b216700a31f402987f57e58693766" +dependencies = [ + "testcontainers", +] + [[package]] name = "thiserror" version = "1.0.43" @@ -1134,7 +1233,7 @@ checksum = "463fe12d7993d3b327787537ce8dd4dfa058de32fc2b195ef3cde03dc4771e8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.26", + "syn 2.0.52", ] [[package]] @@ -1191,7 +1290,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.26", + "syn 2.0.52", ] [[package]] @@ -1302,7 +1401,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.26", + "syn 2.0.52", "wasm-bindgen-shared", ] @@ -1324,7 +1423,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.26", + "syn 2.0.52", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -1386,12 +1485,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "windows" -version = "0.48.0" +name = "windows-core" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets", + "windows-targets 0.52.4", ] [[package]] @@ -1400,7 +1499,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets", + "windows-targets 0.48.1", ] [[package]] @@ -1409,13 +1508,28 @@ version = "0.48.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.48.0", + "windows_aarch64_msvc 0.48.0", + "windows_i686_gnu 0.48.0", + "windows_i686_msvc 0.48.0", + "windows_x86_64_gnu 0.48.0", + "windows_x86_64_gnullvm 0.48.0", + "windows_x86_64_msvc 0.48.0", +] + +[[package]] +name = "windows-targets" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +dependencies = [ + "windows_aarch64_gnullvm 0.52.4", + "windows_aarch64_msvc 0.52.4", + "windows_i686_gnu 0.52.4", + "windows_i686_msvc 0.52.4", + "windows_x86_64_gnu 0.52.4", + "windows_x86_64_gnullvm 0.52.4", + "windows_x86_64_msvc 0.52.4", ] [[package]] @@ -1424,38 +1538,80 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" + [[package]] name = "windows_aarch64_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" + [[package]] name = "windows_i686_gnu" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +[[package]] +name = "windows_i686_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" + [[package]] name = "windows_i686_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +[[package]] +name = "windows_i686_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" + [[package]] name = "windows_x86_64_gnu" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" + [[package]] name = "windows_x86_64_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" diff --git a/Cargo.toml b/Cargo.toml index e88ee23..bf7ce62 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ resolver = "2" members = [ + "example", "lib", "macros", - "example", ] diff --git a/coverage.sh b/coverage.sh new file mode 100755 index 0000000..bf4923b --- /dev/null +++ b/coverage.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -e +cargo llvm-cov test --html +python3 -m http.server --directory target/llvm-cov/html diff --git a/doc.sh b/doc.sh new file mode 100755 index 0000000..e6d3fcc --- /dev/null +++ b/doc.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -e +cargo doc --no-deps +python3 -m http.server --directory target/doc/ diff --git a/example/Cargo.toml b/example/Cargo.toml index 7959d1d..3e48817 100644 --- a/example/Cargo.toml +++ b/example/Cargo.toml @@ -4,15 +4,14 @@ version = "0.1.0" edition = "2021" [dependencies] -chrono = "0.4.26" +chrono = "0.4" cypher-dto = { path = "../lib" } -# 0.7.0-alpha.1 -neo4rs = { git = "https://github.com/neo4j-labs/neo4rs.git", rev = "0bd8099017d3ad57a844c9a456e7aaaca5e4721e" } +neo4rs = "0.7.1" [dev-dependencies] lenient_semver = { version = "0.4.2", features = ["version_lite"] } -neo4j_testcontainers = "0.1.0" pretty_env_logger = "0.5.0" -testcontainers = "0.14.0" +testcontainers = "0.15.0" +testcontainers-modules = { version = "0.3.4", features = ["neo4j"] } tokio = "1.29.1" uuid = { version = "1.4.1", features = ["v4"] } diff --git a/example/src/person.rs b/example/src/person.rs index 20a0536..c65e230 100644 --- a/example/src/person.rs +++ b/example/src/person.rs @@ -1,7 +1,9 @@ -use cypher_dto::{node, relation, stamps}; +use cypher_dto::{timestamps, Node, Relation}; /// Single ID field and optional timestamps. Has example of `new()` and `into_builder()` methods. -#[node(stamps, name = "Person2")] +#[timestamps] +#[derive(Node, Clone)] +#[name = "Person2"] pub struct Person { id: String, #[name = "name2"] @@ -11,12 +13,12 @@ pub struct Person { colors: Vec, } -#[relation] +#[derive(Relation)] struct Knows; #[cfg(test)] mod tests { - use cypher_dto::{NodeEntity, RelationBound}; + use cypher_dto::{FieldSet, NodeEntity, RelationBound, RelationEntity, StampMode}; use super::*; @@ -25,7 +27,19 @@ mod tests { assert_eq!(Person::typename(), "Person2"); assert_eq!( Person::field_names(), - vec!["id", "name2", "age", "colors", "created_at", "updated_at"] + ["id", "name2", "age", "colors", "created_at", "updated_at"] + ); + assert_eq!( + Person::as_query_fields(), + "id: $id, name2: $name2, age: $age, colors: $colors, created_at: $created_at, updated_at: $updated_at" + ); + assert_eq!( + Person::as_query_obj(), + "Person2 { id: $id, name2: $name2, age: $age, colors: $colors, created_at: $created_at, updated_at: $updated_at }" + ); + assert_eq!( + Person::as_query_obj(), + Person::to_query_obj(None, StampMode::Read) ); let p = Person::new( "id", @@ -34,17 +48,17 @@ mod tests { &["red".to_owned(), "blue".to_owned()], ); assert_eq!(p.id(), "id"); - let p = p.into_builder().name("name2").build().unwrap(); + let p = p.into_builder().name("name2").build(); assert_eq!(p.name(), "name2"); assert_eq!(p.colors(), &["red", "blue"]); assert_eq!(p.age(), Some(42)); let now = chrono::Utc::now(); + assert_eq!( p.clone() .into_builder() .created_at(Some(now)) .build() - .unwrap() .created_at(), Some(&now), ); diff --git a/example/src/worked_at.rs b/example/src/worked_at.rs index ad542d2..a8fb8be 100644 --- a/example/src/worked_at.rs +++ b/example/src/worked_at.rs @@ -14,12 +14,12 @@ pub struct WorkedAt { #[cfg(test)] mod tests { use super::*; - use cypher_dto::{QueryFields, StampMode}; + use cypher_dto::{FieldSet, StampMode}; #[test] fn rename() { assert_eq!( - WorkedAt::as_query_fields(None, StampMode::Read), + WorkedAt::to_query_fields(None, StampMode::Read), "foo: $foo" ); } diff --git a/example/tests/basic_crud.rs b/example/tests/basic_crud.rs index 20da4c5..a07b603 100644 --- a/example/tests/basic_crud.rs +++ b/example/tests/basic_crud.rs @@ -46,7 +46,7 @@ async fn basic_crud() { assert_eq!(alice.identifier(), alice_id); // Update Alice's name - let alice = alice.into_builder().name("Allison").build().unwrap(); + let alice = alice.into_builder().name("Allison").build(); graph.run(alice.update()).await.unwrap(); let mut stream = graph.execute(alice_id.read()).await.unwrap(); @@ -59,7 +59,7 @@ async fn basic_crud() { graph.run(acme.identifier().delete()).await.unwrap(); let mut stream = graph - .execute(Query::new(format!("MATCH (n:Company) RETURN n"))) + .execute(Query::new("MATCH (n:Company) RETURN n".to_string())) .await .unwrap(); let row = stream.next().await.unwrap(); diff --git a/example/tests/common/container.rs b/example/tests/common/container.rs index f0114fa..fe01015 100644 --- a/example/tests/common/container.rs +++ b/example/tests/common/container.rs @@ -2,19 +2,52 @@ //! //! It also supports connecting to a real server using environment variables. //! -//! [Original source](https://github.com/neo4j-labs/neo4rs/blob/f1db22cab08c1f911876da43effc61a207828d85/lib/tests/container.rs) +//! [Original source](https://github.com/neo4j-labs/neo4rs/blob/ec0261895f56e476f4f1eb9c6a2151c7b945d454/lib/tests/container.rs) use lenient_semver::Version; -use neo4j_testcontainers::Neo4j; use neo4rs::{ConfigBuilder, Graph}; -use testcontainers::{clients::Cli, Container}; +use testcontainers::{clients::Cli, Container, RunnableImage}; +use testcontainers_modules::neo4j::{Neo4j, Neo4jImage}; -use std::sync::Arc; +use std::{error::Error, io::BufRead as _}; + +#[allow(dead_code)] +#[derive(Default)] +pub struct Neo4jContainerBuilder { + enterprise: bool, + config: ConfigBuilder, +} + +#[allow(dead_code)] +impl Neo4jContainerBuilder { + pub fn new() -> Self { + Self::default() + } + + pub fn with_enterprise_edition(mut self) -> Self { + self.enterprise = true; + self + } + + pub fn with_config(mut self, config: ConfigBuilder) -> Self { + self.config = config; + self + } + + pub fn modify_config(mut self, block: impl FnOnce(ConfigBuilder) -> ConfigBuilder) -> Self { + self.config = block(self.config); + self + } + + pub async fn start(self) -> Result> { + Neo4jContainer::from_config_and_edition(self.config, self.enterprise).await + } +} pub struct Neo4jContainer { - graph: Arc, + graph: Graph, version: String, - _container: Option>, + _container: Option>, } impl Neo4jContainer { @@ -24,31 +57,36 @@ impl Neo4jContainer { } pub async fn from_config(config: ConfigBuilder) -> Self { + Self::from_config_and_edition(config, false).await.unwrap() + } + + pub async fn from_config_and_edition( + config: ConfigBuilder, + enterprise_edition: bool, + ) -> Result> { let _ = pretty_env_logger::try_init(); + let connection = Self::create_test_endpoint(); let server = Self::server_from_env(); - let (connection, _container) = match server { + let (uri, _container) = match server { TestServer::TestContainer => { - let (connection, container) = Self::create_testcontainer(); - (connection, Some(container)) - } - TestServer::External(uri) => { - let connection = Self::create_test_endpoint(uri); - (connection, None) + let (uri, container) = Self::create_testcontainer(&connection, enterprise_edition)?; + (uri, Some(container)) } + TestServer::External(uri) => (uri, None), }; let version = connection.version; - let graph = Self::connect(config, connection.uri, &connection.auth).await; - Self { + let graph = Self::connect(config, uri, &connection.auth).await; + Ok(Self { graph, version, _container, - } + }) } - pub fn graph(&self) -> Arc { + pub fn graph(&self) -> Graph { self.graph.clone() } @@ -70,24 +108,63 @@ impl Neo4jContainer { } } - fn create_testcontainer() -> (TestConnection, Container<'static, Neo4j>) { + fn create_testcontainer( + connection: &TestConnection, + enterprise: bool, + ) -> Result<(String, Container<'static, Neo4jImage>), Box> + { + let image = Neo4j::new() + .with_user(connection.auth.user.to_owned()) + .with_password(connection.auth.pass.to_owned()); + let docker = Cli::default(); let docker = Box::leak(Box::new(docker)); - let container = docker.run(Neo4j::default()); + let container = if enterprise { + const ACCEPTANCE_FILE_NAME: &str = "container-license-acceptance.txt"; + + let version = format!("{}-enterprise", connection.version); + let image_name = format!("neo4j:{}", version); + + let acceptance_file = std::env::current_dir() + .ok() + .map(|o| o.join(ACCEPTANCE_FILE_NAME)); + + let has_license_acceptance = acceptance_file + .as_deref() + .and_then(|o| std::fs::File::open(o).ok()) + .into_iter() + .flat_map(|o| std::io::BufReader::new(o).lines()) + .any(|o| o.map_or(false, |line| line.trim() == image_name)); + + if !has_license_acceptance { + return Err(format!( + concat!( + "You need to accept the Neo4j Enterprise Edition license by ", + "creating the file `{}` with the following content:\n\n\t{}", + ), + acceptance_file.map_or_else( + || ACCEPTANCE_FILE_NAME.to_owned(), + |o| { o.display().to_string() } + ), + image_name + ) + .into()); + } + let image: RunnableImage = image.with_version(version).into(); + let image = image.with_env_var(("NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes")); - let uri = Neo4j::uri_ipv4(&container); - let version = container.image().version().to_owned(); - let user = container.image().user().to_owned(); - let pass = container.image().pass().to_owned(); - let auth = TestAuth { user, pass }; + docker.run(image) + } else { + docker.run(image.with_version(connection.version.to_owned())) + }; - let connection = TestConnection { uri, version, auth }; + let uri = format!("bolt://127.0.0.1:{}", container.image().bolt_port_ipv4()); - (connection, container) + Ok((uri, container)) } - fn create_test_endpoint(uri: String) -> TestConnection { + fn create_test_endpoint() -> TestConnection { const USER_VAR: &str = "NEO4J_TEST_USER"; const PASS_VAR: &str = "NEO4J_TEST_PASS"; const VERSION_VAR: &str = "NEO4J_VERSION_TAG"; @@ -103,10 +180,10 @@ impl Neo4jContainer { let auth = TestAuth { user, pass }; let version = var(VERSION_VAR).unwrap_or_else(|_| DEFAULT_VERSION_TAG.to_owned()); - TestConnection { uri, auth, version } + TestConnection { auth, version } } - async fn connect(config: ConfigBuilder, uri: String, auth: &TestAuth) -> Arc { + async fn connect(config: ConfigBuilder, uri: String, auth: &TestAuth) -> Graph { let config = config .uri(uri) .user(&auth.user) @@ -114,9 +191,7 @@ impl Neo4jContainer { .build() .unwrap(); - let graph = Graph::connect(config).await.unwrap(); - - Arc::new(graph) + Graph::connect(config).await.unwrap() } } @@ -126,7 +201,6 @@ struct TestAuth { } struct TestConnection { - uri: String, version: String, auth: TestAuth, } diff --git a/example/tests/manual_queries.rs b/example/tests/manual_queries.rs index a219e36..eb68682 100644 --- a/example/tests/manual_queries.rs +++ b/example/tests/manual_queries.rs @@ -39,11 +39,11 @@ async fn create_all_at_once() { CREATE (bob)-[w2:{}]->(acme) RETURN alice, bob, acme, w, w2 ", - Person::as_query_obj(Some("alice"), StampMode::Create), - Person::as_query_obj(Some("bob"), StampMode::Create), - Company::as_query_obj(Some("acme"), StampMode::Create), - WorkedAt::as_query_obj(Some("w"), StampMode::Create), - WorkedAt::as_query_obj(Some("w2"), StampMode::Create), + Person::to_query_obj(Some("alice"), StampMode::Create), + Person::to_query_obj(Some("bob"), StampMode::Create), + Company::to_query_obj(Some("acme"), StampMode::Create), + WorkedAt::to_query_obj(Some("w"), StampMode::Create), + WorkedAt::to_query_obj(Some("w2"), StampMode::Create), )); q = alice.add_values_to_params(q, Some("alice"), StampMode::Create); q = bob.add_values_to_params(q, Some("bob"), StampMode::Create); diff --git a/fix.sh b/fix.sh new file mode 100755 index 0000000..ab4e11b --- /dev/null +++ b/fix.sh @@ -0,0 +1,7 @@ +set -e +cargo sort +cargo sort lib +cargo sort macros +cargo fmt +cargo fix --allow-dirty +cargo clippy --fix --allow-dirty diff --git a/lib/Cargo.toml b/lib/Cargo.toml index d250766..66988a7 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -1,12 +1,13 @@ [package] name = "cypher-dto" description = "A collection of traits and macros for working Data Transfer Objects (DTOs) Cypher and Neo4j." -version = "0.2.0" +version = "0.3.0" edition = "2021" license = "MIT" keywords = ["neo4j", "cypher", "dto", "query", "graph"] -categories = ["database", "data-structures", "encoding"] +categories = ["database"] repository = "https://github.com/jifalops/cypher-dto" +metadata = { msrv = "1.60" } [features] default = ["macros"] @@ -14,17 +15,16 @@ macros = ["cypher-dto-macros"] serde = ["macros", "cypher-dto-macros/serde"] [dependencies] -chrono = "0.4" -cypher-dto-macros = { version = "0.2.0", path = "../macros", optional = true } -# neo4rs = "0.6.2" -neo4rs = { version = "0.7.0-alpha.1", git = "https://github.com/neo4j-labs/neo4rs.git", rev = "0bd8099017d3ad57a844c9a456e7aaaca5e4721e" } +chrono = { version = "0.4" } +cypher-dto-macros = { version = "0.3.0", path = "../macros", optional = true } +neo4rs = "0.7.1" thiserror = "1.0" [dev-dependencies] -tokio = "1.29.1" lenient_semver = { version = "0.4.2", features = ["version_lite"] } -neo4j_testcontainers = "0.1.0" pretty_env_logger = "0.5.0" -serde = { version = "1.0.174", features = ["derive"] } -testcontainers = "0.14.0" +serde = { version = "1.0", features = ["derive"] } +testcontainers = "0.15.0" +testcontainers-modules = { version = "0.3.4", features = ["neo4j"] } +tokio = "1.29.1" uuid = { version = "1.4.1", features = ["v4"] } diff --git a/lib/README.md b/lib/README.md index 45ddb65..77d3010 100644 --- a/lib/README.md +++ b/lib/README.md @@ -1,20 +1,27 @@ # cypher-dto [![Crates.io](https://img.shields.io/crates/v/cypher-dto)](https://crates.io/crates/cypher-dto) -[![Github.com](https://github.com/jifalops/cypher-dto/actions/workflows/rust.yml/badge.svg)](https://github.com/jifalops/cypher-dto/actions/workflows/rust.yml) +[![Github.com](https://github.com/jifalops/cypher-dto/actions/workflows/ci.yml/badge.svg)](https://github.com/jifalops/cypher-dto/actions/workflows/ci.yml) [![Docs.rs](https://docs.rs/cypher-dto/badge.svg)](https://docs.rs/cypher-dto) ![License](https://img.shields.io/crates/l/cypher-dto.svg) -A collection of traits and macros for working Data Transfer Objects (DTOs) in Neo4j. +A collection of traits and macros for working with Data Transfer Objects (DTOs) in Neo4j. +```rust +use cypher_dto::Node; +#[derive(Node)] +struct Person { + name: String +} +``` ## Examples ### Basic usage ```rust -#[node] +#[derive(Node)] struct Person { id: String, // Inferred to be the only #[id] field. name: String, @@ -50,7 +57,7 @@ assert_eq!(Knows::typename(), "KNOWS"); ### Multi valued identifiers ```rust -#[node] +#[derive(Node)] struct Company { #[id] name: String, @@ -83,7 +90,7 @@ query = id.add_values_to_params(query); * Doc comments are copied to the getters for the struct, the getter(s) on the `FooId` struct, and the methods on the `FooBuilder` struct. ```rust -#[node] +#[derive(Node)] struct Person { /// This comment is copied to the getter, the Id getter, and the builder method. name: String, @@ -93,28 +100,12 @@ let p = p.into_builder().name("Ferris").build(); assert_eq!(p.name(), "Ferris"); ``` -### Using derive instead of `#[node]` or `#[relation]` - -```rust -#[derive(Relation, Clone, Debug, PartialEq)] -struct Knows; - -// Equivalent to: -#[relation] -struct Knows; - -// Or, if the `serde` feature is enabled: -#[derive(Relation, Clone, Debug, PartialEq, Serialize, Deserialize)] -``` - -For more details about the macro variations, see the [cypher-dto-macros](https://crates.io/crates/cypher-dto-macros) crate. - ### Timestamps There's built-in support for special timestamp fields: `created_at` and `updated_at`, `created` and `updated`, or any single one of those four. ```rust -#[node(stamps)] +#[timestamps] struct Person { name: String, } @@ -122,7 +113,7 @@ struct Person { // created_at: Option>, // updated_at: Option>, -#[node(stamps = "short")] +#[timestamps = "short"] struct Person { name: String, } @@ -130,7 +121,7 @@ struct Person { // created: Option>, // updated: Option>, -#[node(stamps = "updated_at")] +#[timestamps = "updated_at"] struct Person { name: String, } @@ -141,25 +132,18 @@ struct Person { The timestamp fields are treated a little bit differently than other fields: * They are not parameters in the generated `::new()` method. -* They sometimes have hardcoded values in `::as_query_fields()`. - * Calling `as_query_fields()` with `StampMode::Create` will use `datetime()` in the query instead of `$created_at` for example. +* They sometimes have hardcoded values in `::to_query_fields()`. + * Calling `to_query_fields()` with `StampMode::Create` will use `datetime()` in the query instead of `$created_at` for example. `Option>` is used instead of `DateTime` so that the fields can be `None` when creating a new instance, before it exists in the database. -> NOTE: Support for Option params in the `neo4rs` crate is in master, but not yet released. -> -> You are welcome to depend on the master branch of this library to enable full support for timestamps. -> -> ```toml -> [dependencies] -> cypher-dto = { git = "https://github.com/jifalops/cypher-dto.git" } -> ``` +For more details about the macro variations, see the [cypher-dto-macros](https://crates.io/crates/cypher-dto-macros) crate. ### Unitary CRUD operations -This library takes the point of view that non-trivial queries should be managed by hand, but it does provide basic CRUD operations for convenience. Hopefully what you've seen so far shows how it can help create more readable complex queries. +This library takes the point of view that non-trivial queries should be managed by hand, but it does provide basic CRUD operations for convenience. -`#[node]` and `#[relation]` structs get `create()` and `update()` methods, while the corresponding `FooId` structs get `read()` and `delete()` methods, all of which return a `neo4rs::Query`. +`#[derive(Node)]` and `#[derive(Relation)]` structs get `create()` and `update()` methods, while the corresponding `FooId` structs get `read()` and `delete()` methods, all of which return a `neo4rs::Query`. None of those methods even take any arguments, with the exception of creating a relation, which needs to know if the start and end nodes it's between need created or already exist. @@ -174,7 +158,7 @@ struct Knows; let alice = Person::new("Alice"); let bob = Person::new("Bob"); -let knows = Knows; // Relations can have fields and ids too of course. +let knows = Knows; // Relations can have fields and ids too. let query = knows.create(RelationBound::Create(&alice), RelationBound::Create(&bob)); ``` diff --git a/lib/coverage.sh b/lib/coverage.sh deleted file mode 100755 index e276b98..0000000 --- a/lib/coverage.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash -cargo llvm-cov nextest --html -pushd ../target/llvm-cov/html > /dev/null -python3 -m http.server 14532 -popd > /dev/null diff --git a/lib/include/create_query.rs b/lib/include/create_query.rs new file mode 100644 index 0000000..c327388 --- /dev/null +++ b/lib/include/create_query.rs @@ -0,0 +1,16 @@ +{ + #[derive(Node)] + struct Person { + name: String, + } + + let person = Person::new("Alice"); + let query: neor4s::Query = person.create(); + + // Or to build the same query manually: + let query = format!("CREATE (n:{})", Person::as_query_obj()); + assert_eq!(query, "CREATE (n:Person { name: $name })"); + + let mut query = neo4rs::Query::new(query); + person.add_values_to_params(query); +} diff --git a/lib/include/exec/create_and_read_node.rs b/lib/include/exec/create_and_read_node.rs new file mode 100644 index 0000000..0229111 --- /dev/null +++ b/lib/include/exec/create_and_read_node.rs @@ -0,0 +1,28 @@ +{ + #[derive(Node)] + struct Person { + id: String, + name: String, + } + + let person = Person::new("1234", "Alice"); + + // CREATE (n:Person { id: $id, name: $name }) + // + // $id: "1234" + // $name: "Alice" + graph.run(person.create()).await.unwrap(); + + // Find an existing person by id. + let id = PersonId::new("1234"); + + // MATCH (n:Person { id: $id }) RETURN n + // + // $id: "1234" + let mut stream = graph.execute(id.read()).await.unwrap(); + + let row = stream.next().await.unwrap().unwrap(); + let node: neo4rs::Node = row.get("n").unwrap(); + let person = Person::try_from(node).unwrap(); + assert_eq!(person.name(), "Alice"); +} diff --git a/lib/include/exec/create_and_read_relation.rs b/lib/include/exec/create_and_read_relation.rs new file mode 100644 index 0000000..0226581 --- /dev/null +++ b/lib/include/exec/create_and_read_relation.rs @@ -0,0 +1,40 @@ +{ + #[derive(Relation)] + struct Knows { + since: u16, + } + + #[derive(Node)] + struct Person { + name: String, + } + + let alice = Person::new("Alice"); + let knows = Knows::new(2017); + let bob = Person::new("Bob"); + + // CREATE (s:Person { name: $s_name }) + // CREATE (e:Person { name: $e_name }) + // CREATE (s)-[r:KNOWS { since: $since }]->(e) + // + // $s_name: "Alice" + // $e_name: "Bob" + // $since: 2017 + let query = knows.create(RelationBound::Create(&alice), RelationBound::Create(&bob)); + graph.run(query).await.unwrap(); + + // Find the relationship just created. + let id = KnowsId::new(); + + // MATCH (s:Person { name: $s_name })-[r:KNOWS]-(e:Person { name: $e_name }) RETURN r + // + // $s_name: "Alice" + // $e_name: "Bob" + let query = id.read_between(&alice.into(), &bob.into()); + let mut stream = graph.execute(query).await.unwrap(); + + let row = stream.next().await.unwrap().unwrap(); + let relation: neo4rs::UnboundedRelation = row.get("r").unwrap(); + let knows = Knows::try_from(relation).unwrap(); + assert_eq!(knows.since(), 2017); +} diff --git a/lib/include/id.rs b/lib/include/id.rs new file mode 100644 index 0000000..a860166 --- /dev/null +++ b/lib/include/id.rs @@ -0,0 +1,10 @@ +{ + #[derive(Node)] + struct Person { + #[id] + ssn: String, + name: String, + } + assert_eq!(Person::as_query_obj(), "Person { ssn: $ssn, name: $name }"); + assert_eq!(PersonId::as_query_obj(), "Person { ssn: $ssn }"); +} diff --git a/lib/include/id_inferred.rs b/lib/include/id_inferred.rs new file mode 100644 index 0000000..75a1822 --- /dev/null +++ b/lib/include/id_inferred.rs @@ -0,0 +1,9 @@ +{ + #[derive(Node)] + struct Person { + id: String, + name: String, + } + assert_eq!(Person::as_query_obj(), "Person { id: $id, name: $name }"); + assert_eq!(PersonId::as_query_obj(), "Person { id: $id }"); +} diff --git a/lib/include/id_multi.rs b/lib/include/id_multi.rs new file mode 100644 index 0000000..b7c0613 --- /dev/null +++ b/lib/include/id_multi.rs @@ -0,0 +1,11 @@ +{ + #[derive(Node)] + struct Person { + #[id] + name: String, + #[id] + address: String, + } + + assert_eq!(PersonId::as_query_obj(), "Person { name: $name, address: $address }"); +} diff --git a/lib/include/read_query.rs b/lib/include/read_query.rs new file mode 100644 index 0000000..3cf2fa9 --- /dev/null +++ b/lib/include/read_query.rs @@ -0,0 +1,17 @@ +{ + #[derive(Node)] + struct Person { + id: String, + name: String, + } + + let id = PersonId::new("1234"); + let query = id.read(); + + // Or, to build the query manually: + let query = format!("MATCH (n:{}) RETURN n", PersonId::as_query_obj()); + assert_eq!(query, "MATCH (n:Person { id: $id }) RETURN n"); + + let mut query = neo4rs::Query::new(query); + id.add_values_to_params(query); +} diff --git a/lib/include/relationships.rs b/lib/include/relationships.rs new file mode 100644 index 0000000..41bb809 --- /dev/null +++ b/lib/include/relationships.rs @@ -0,0 +1,44 @@ +{ + #[derive(Relation)] + struct Knows { + since: u16, + } + assert_eq!(Knows::typename(), "KNOWS"); + assert_eq!(Knows::field_names(), ["since"]); + assert_eq!(Knows::as_query_fields(), "since: $since"); + assert_eq!(Knows::as_query_obj(), "KNOWS { since: $since }"); + + #[derive(Node)] + struct Person { + name: String, + } + + let alice = Person::new("Alice"); + let knows = Knows::new(2017); + let bob = Person::new("Bob"); + + // Create all three at once: + let query = knows.create(RelationBound::Create(&alice), RelationBound::Create(&bob)); + + // Or, manually build the same query: + let query = format!( + "CREATE (s:{}) \ + CREATE (e:{}) \ + CREATE (s)-[r:{}]->(e)", + Person::to_query_obj(Some("s"), StampMode::Create), + Knows::to_query_obj(None, StampMode::Create), + Person::to_query_obj(Some("e"), StampMode::Create), + ); + assert_eq!( + query, + "CREATE (s:Person { name: $s_name }) \ + CREATE (e:Person { name: $e_name }) \ + CREATE (s)-[:KNOWS { since: $since }]->(e)" + ); + + // Use it in a [neo4rs::Query]: + let mut query = neo4rs::Query::new(query); + alice.add_values_to_params(query, Some("s"), StampMode::Create); // Adds "s_name" to params + knows.add_values_to_params(query, None, StampMode::Create); // Adds "since" to params + bob.add_values_to_params(query, Some("e"), StampMode::Create); // Adds "e_name" to params +} diff --git a/lib/include/rename.rs b/lib/include/rename.rs new file mode 100644 index 0000000..e441fcf --- /dev/null +++ b/lib/include/rename.rs @@ -0,0 +1,12 @@ +{ + #[derive(Node)] + #[name = "Person2"] + struct Person { + #[name = "name2"] + name: String, + } + assert_eq!(Person::typename(), "Person2"); + assert_eq!(Person::field_names(), ["name2"]); + assert_eq!(Person::as_query_fields(), "name2: $name2"); + assert_eq!(Person::as_query_obj(), "Person2 { name2: $name2 }"); +} diff --git a/lib/include/static_strings.rs b/lib/include/static_strings.rs new file mode 100644 index 0000000..86fbc07 --- /dev/null +++ b/lib/include/static_strings.rs @@ -0,0 +1,10 @@ +{ + #[derive(Node)] + struct Person { + name: String, + } + assert_eq!(Person::typename(), "Person"); + assert_eq!(Person::field_names(), ["name"]); + assert_eq!(Person::as_query_fields(), "name: $name"); + assert_eq!(Person::as_query_obj(), "Person { name: $name }"); +} diff --git a/lib/src/entity.rs b/lib/src/entity.rs index b5e913c..c9849e2 100644 --- a/lib/src/entity.rs +++ b/lib/src/entity.rs @@ -1,23 +1,13 @@ use crate::{format_query_fields, Stamps}; use neo4rs::{Query, Row}; -/// A named collection of [QueryFields], such as a node or relationship. -pub trait Entity: QueryFields { +/// The full or partial fields on a node or relationship that may have timestamps. +/// +/// This is the basic unit of query building used by [NodeEntity], [NodeId], [RelationEntity], and [RelationId]. +pub trait FieldSet: TryFrom { /// The primary label for a node, or the type of a relationship. fn typename() -> &'static str; - /// Formatted like `typename() { as_query_fields() }`, or for a fieldless relationship, just `typename()`. - fn as_query_obj(prefix: Option<&str>, mode: StampMode) -> String { - let fields = Self::as_query_fields(prefix, mode); - if fields.is_empty() { - return Self::typename().to_owned(); - } - format!("{} {{ {} }}", Self::typename(), fields) - } -} - -/// A collection of fields to be used in a cypher query. -pub trait QueryFields: TryFrom { /// The fields in this set. fn field_names() -> &'static [&'static str]; @@ -26,12 +16,24 @@ pub trait QueryFields: TryFrom { Stamps::from_fields(Self::field_names()) } + /// Formats the field names as a query string. + /// + /// `struct Foo { bar: u8 }` would be `bar: $bar`. + /// + /// This is a special case of [to_query_fields], where the prefix is `None` and the mode is [StampMode::Read], and is known at compile time. + fn as_query_fields() -> &'static str; + + /// Wraps the fields formatted by [as_query_fields] with [typename] and a pair of curly braces. + /// + /// `Foo { bar: $bar }` + fn as_query_obj() -> &'static str; + /// Formats the field names as a query string. /// /// `struct Foo { bar: u8 }` would be `bar: $bar`. /// /// Prefixes apply to the placeholders only (e.g. bar: $prefix_bar). - fn as_query_fields(prefix: Option<&str>, mode: StampMode) -> String { + fn to_query_fields(prefix: Option<&str>, mode: StampMode) -> String { let (stamps, other_fields) = Self::timestamps(); let stamps = stamps.as_query_fields(prefix, mode); let other_fields = format_query_fields(other_fields, prefix); @@ -44,8 +46,17 @@ pub trait QueryFields: TryFrom { [other_fields, stamps].join(", ") } - /// Adds all field values to the query parameters, matching placeholders in [as_query_fields]. + /// Adds all field values to the query parameters, matching placeholders in [as_query_fields()]. fn add_values_to_params(&self, query: Query, prefix: Option<&str>, mode: StampMode) -> Query; + + /// Formatted like `typename() { as_query_fields() }`, or for a fieldless relationship, just `typename()`. + fn to_query_obj(prefix: Option<&str>, mode: StampMode) -> String { + let fields = Self::to_query_fields(prefix, mode); + if fields.is_empty() { + return Self::typename().to_owned(); + } + format!("{} {{ {} }}", Self::typename(), fields) + } } /// Controls which timestamps are hardcoded in a query (e.g. `datetime()`), @@ -53,7 +64,7 @@ pub trait QueryFields: TryFrom { pub enum StampMode { /// Any timestamp fields ([Stamps]) are treated as normal values. /// - /// Corresponding placeholders are added to the query fields (e.g. $created, $updated). + /// Corresponding placeholders are added to the query fields (e.g. $created_at, $updated_at). Read, /// Stamp fields are added to the query fields with a hardcoded value (e.g. `datetime()`). /// @@ -65,28 +76,6 @@ pub enum StampMode { Update, } -// /// An observable for working with [QueryFields] and [neo4rs::Query]s. -// pub struct QueryBuilder { -// parts: Vec, -// params: HashSet, -// } -// impl QueryBuilder { -// pub fn new() -> Self { -// Self { -// parts: Vec::new(), -// params: HashSet::new(), -// } -// } -// pub fn add(&mut self, s: &str, params: &[&str]) -> &Self { -// self.parts.push(s.to_owned()); -// self.params.extend(params.iter().map(|p| (*p).to_owned())); -// self -// } -// pub fn build(self) -> (String, HashSet) { -// (self.parts.join(" "), self.params) -// } -// } - #[cfg(test)] pub(crate) mod tests { use super::*; @@ -109,16 +98,23 @@ pub(crate) mod tests { // // Foo impl // - impl Entity for Foo { + impl FieldSet for Foo { fn typename() -> &'static str { "Foo" } - } - impl QueryFields for Foo { + fn field_names() -> &'static [&'static str] { &["name", "age"] } + fn as_query_fields() -> &'static str { + "name: $name, age: $age" + } + + fn as_query_obj() -> &'static str { + "Foo { name: $name, age: $age }" + } + fn add_values_to_params(&self, query: Query, prefix: Option<&str>, _: StampMode) -> Query { query .param(&format_param("name", prefix), self.name.clone()) @@ -131,11 +127,11 @@ pub(crate) mod tests { Ok(Self { name: value .get("name") - .ok_or(Error::MissingField("name".to_owned()))?, + .map_err(|_e| Error::MissingField("name".to_owned()))?, age: u8::try_from( value .get::("age") - .ok_or(Error::MissingField("age".to_owned()))?, + .map_err(|_e| Error::MissingField("age".to_owned()))?, ) .map_err(|_| Error::TypeMismatch("age".to_owned()))?, }) @@ -145,16 +141,23 @@ pub(crate) mod tests { // // Bar impl // - impl Entity for Bar { + impl FieldSet for Bar { fn typename() -> &'static str { "Bar" } - } - impl QueryFields for Bar { + fn field_names() -> &'static [&'static str] { &["created", "updated"] } + fn as_query_fields() -> &'static str { + "created: $created, updated: $updated" + } + + fn as_query_obj() -> &'static str { + "Bar { created: $created, updated: $updated }" + } + fn add_values_to_params( &self, query: Query, @@ -193,16 +196,23 @@ pub(crate) mod tests { // // Baz impl // - impl Entity for Baz { + impl FieldSet for Baz { fn typename() -> &'static str { "BAZ" } - } - impl QueryFields for Baz { + fn field_names() -> &'static [&'static str] { &[] } + fn as_query_fields() -> &'static str { + "" + } + + fn as_query_obj() -> &'static str { + "BAZ" + } + fn add_values_to_params(&self, query: Query, _: Option<&str>, _: StampMode) -> Query { query } @@ -218,24 +228,24 @@ pub(crate) mod tests { fn as_obj() { // Foo assert_eq!( - Foo::as_query_obj(None, StampMode::Read), + Foo::to_query_obj(None, StampMode::Read), "Foo { name: $name, age: $age }" ); // Bar assert_eq!( - Bar::as_query_obj(None, StampMode::Read), + Bar::to_query_obj(None, StampMode::Read), "Bar { created: $created, updated: $updated }" ); assert_eq!( - Bar::as_query_obj(None, StampMode::Create), + Bar::to_query_obj(None, StampMode::Create), "Bar { created: datetime(), updated: datetime() }" ); assert_eq!( - Bar::as_query_obj(None, StampMode::Update), + Bar::to_query_obj(None, StampMode::Update), "Bar { created: $created, updated: datetime() }" ); // Baz - assert_eq!(Baz::as_query_obj(None, StampMode::Read), "BAZ"); + assert_eq!(Baz::to_query_obj(None, StampMode::Read), "BAZ"); } #[test] @@ -253,7 +263,7 @@ pub(crate) mod tests { // Foo let mut q = Query::new(format!( "CREATE (n:{})", - Foo::as_query_obj(None, StampMode::Create) + Foo::to_query_obj(None, StampMode::Create) )); q = foo.add_values_to_params(q, None, StampMode::Create); assert!(q.has_param_key("name")); @@ -262,7 +272,7 @@ pub(crate) mod tests { // Bar let mut q = Query::new(format!( "MATCH (n:{})", - Bar::as_query_obj(None, StampMode::Read) + Bar::to_query_obj(None, StampMode::Read) )); q = bar.add_values_to_params(q, None, StampMode::Read); assert!(q.has_param_key("created")); @@ -270,7 +280,7 @@ pub(crate) mod tests { let mut q = Query::new(format!( "CREATE (n:{})", - Bar::as_query_obj(None, StampMode::Create) + Bar::to_query_obj(None, StampMode::Create) )); q = bar.add_values_to_params(q, None, StampMode::Create); assert!(!q.has_param_key("created")); @@ -278,7 +288,7 @@ pub(crate) mod tests { let mut q = Query::new(format!( "MERGE (n:{})", - Bar::as_query_obj(None, StampMode::Update) + Bar::to_query_obj(None, StampMode::Update) )); q = bar.add_values_to_params(q, None, StampMode::Update); assert!(q.has_param_key("created")); @@ -289,9 +299,9 @@ pub(crate) mod tests { "MATCH (s:{}) MATCH (e:{}) CREATE (s)-[r:{}]->(e)", - Foo::as_query_obj(Some("s"), StampMode::Read), - Bar::as_query_obj(Some("e"), StampMode::Read), - Baz::as_query_obj(None, StampMode::Create), + Foo::to_query_obj(Some("s"), StampMode::Read), + Bar::to_query_obj(Some("e"), StampMode::Read), + Baz::to_query_obj(None, StampMode::Create), )); q = foo.add_values_to_params(q, Some("s"), StampMode::Read); q = bar.add_values_to_params(q, Some("e"), StampMode::Read); @@ -336,7 +346,7 @@ pub(crate) mod tests { }; let mut q = Query::new(format!( "CREATE (n:{})", - NumTypes::as_query_obj(None, StampMode::Create) + NumTypes::to_query_obj(None, StampMode::Create) )); q = num_types.add_values_to_params(q, None, StampMode::Create); assert!(q.has_param_key("usize_num")); @@ -422,12 +432,11 @@ pub(crate) mod tests { } } } - impl Entity for NumTypes { + impl FieldSet for NumTypes { fn typename() -> &'static str { "NumTypes" } - } - impl QueryFields for NumTypes { + fn field_names() -> &'static [&'static str] { &[ "usize_num", @@ -461,6 +470,14 @@ pub(crate) mod tests { ] } + fn as_query_fields() -> &'static str { + "usize_num: $usize_num, isize_num: $isize_num, u8_num: $u8_num, u16_num: $u16_num, u32_num: $u32_num, u64_num: $u64_num, u128_num: $u128_num, i8_num: $i8_num, i16_num: $i16_num, i32_num: $i32_num, i64_num: $i64_num, i128_num: $i128_num, f32_num: $f32_num, f64_num: $f64_num, usize_opt: $usize_opt, isize_opt: $isize_opt, u8_opt: $u8_opt, u16_opt: $u16_opt, u32_opt: $u32_opt, u64_opt: $u64_opt, u128_opt: $u128_opt, i8_opt: $i8_opt, i16_opt: $i16_opt, i32_opt: $i32_opt, i64_opt: $i64_opt, i128_opt: $i128_opt, f32_opt: $f32_opt, f64_opt: $f64_opt" + } + // Trusting copilot: ^^ and vv + fn as_query_obj() -> &'static str { + "NumTypes { usize_num: $usize_num, isize_num: $isize_num, u8_num: $u8_num, u16_num: $u16_num, u32_num: $u32_num, u64_num: $u64_num, u128_num: $u128_num, i8_num: $i8_num, i16_num: $i16_num, i32_num: $i32_num, i64_num: $i64_num, i128_num: $i128_num, f32_num: $f32_num, f64_num: $f64_num, usize_opt: $usize_opt, isize_opt: $isize_opt, u8_opt: $u8_opt, u16_opt: $u16_opt, u32_opt: $u32_opt, u64_opt: $u64_opt, u128_opt: $u128_opt, i8_opt: $i8_opt, i16_opt: $i16_opt, i32_opt: $i32_opt, i64_opt: $i64_opt, i128_opt: $i128_opt, f32_opt: $f32_opt, f64_opt: $f64_opt }" + } + fn add_values_to_params(&self, q: Query, prefix: Option<&str>, _: StampMode) -> Query { // These are the minimal conversions needed before neo4rs v0.7.0. q.param(&format_param("usize_num", prefix), self.usize_num as i64) @@ -518,169 +535,169 @@ pub(crate) mod tests { usize_num: usize::try_from( value .get::("usize_num") - .ok_or(Error::MissingField("usize_num".to_owned()))?, + .map_err(|_e| Error::MissingField("usize_num".to_owned()))?, ) .map_err(|_| Error::TypeMismatch("usize_num".to_owned()))?, isize_num: isize::try_from( value .get::("isize_num") - .ok_or(Error::MissingField("isize_num".to_owned()))?, + .map_err(|_e| Error::MissingField("isize_num".to_owned()))?, ) .map_err(|_| Error::TypeMismatch("isize_num".to_owned()))?, u8_num: u8::try_from( value .get::("u8_num") - .ok_or(Error::MissingField("u8_num".to_owned()))?, + .map_err(|_e| Error::MissingField("u8_num".to_owned()))?, ) .map_err(|_| Error::TypeMismatch("u8_num".to_owned()))?, u16_num: u16::try_from( value .get::("u16_num") - .ok_or(Error::MissingField("u16_num".to_owned()))?, + .map_err(|_e| Error::MissingField("u16_num".to_owned()))?, ) .map_err(|_| Error::TypeMismatch("u16_num".to_owned()))?, u32_num: u32::try_from( value .get::("u32_num") - .ok_or(Error::MissingField("u32_num".to_owned()))?, + .map_err(|_e| Error::MissingField("u32_num".to_owned()))?, ) .map_err(|_| Error::TypeMismatch("u32_num".to_owned()))?, u64_num: u64::try_from( value .get::("u64_num") - .ok_or(Error::MissingField("u64_num".to_owned()))?, + .map_err(|_e| Error::MissingField("u64_num".to_owned()))?, ) .map_err(|_| Error::TypeMismatch("u64_num".to_owned()))?, u128_num: u128::try_from( value .get::("u128_num") - .ok_or(Error::MissingField("u128_num".to_owned()))?, + .map_err(|_e| Error::MissingField("u128_num".to_owned()))?, ) .map_err(|_| Error::TypeMismatch("u128_num".to_owned()))?, i8_num: i8::try_from( value .get::("i8_num") - .ok_or(Error::MissingField("i8_num".to_owned()))?, + .map_err(|_e| Error::MissingField("i8_num".to_owned()))?, ) .map_err(|_| Error::TypeMismatch("i8_num".to_owned()))?, i16_num: i16::try_from( value .get::("i16_num") - .ok_or(Error::MissingField("i16_num".to_owned()))?, + .map_err(|_e| Error::MissingField("i16_num".to_owned()))?, ) .map_err(|_| Error::TypeMismatch("i16_num".to_owned()))?, i32_num: i32::try_from( value .get::("i32_num") - .ok_or(Error::MissingField("i32_num".to_owned()))?, + .map_err(|_e| Error::MissingField("i32_num".to_owned()))?, ) .map_err(|_| Error::TypeMismatch("i32_num".to_owned()))?, i64_num: value .get("i64_num") - .ok_or(Error::MissingField("i64_num".to_owned()))?, + .map_err(|_e| Error::MissingField("i64_num".to_owned()))?, i128_num: i128::try_from( value .get::("i128_num") - .ok_or(Error::MissingField("i128_num".to_owned()))?, + .map_err(|_e| Error::MissingField("i128_num".to_owned()))?, ) .map_err(|_| Error::TypeMismatch("i128_num".to_owned()))?, f32_num: value .get::("f32_num") - .ok_or(Error::MissingField("f32_num".to_owned()))? + .map_err(|_e| Error::MissingField("f32_num".to_owned()))? as f32, f64_num: value .get("f64_num") - .ok_or(Error::MissingField("f64_num".to_owned()))?, + .map_err(|_e| Error::MissingField("f64_num".to_owned()))?, usize_opt: match value.get::("usize_opt") { - Some(v) => Some( + Ok(v) => Some( v.try_into() .map_err(|_| Error::TypeMismatch("usize_opt".to_owned()))?, ), - None => None, + Err(_) => None, }, isize_opt: match value.get::("isize_opt") { - Some(v) => Some( + Ok(v) => Some( v.try_into() .map_err(|_| Error::TypeMismatch("isize_opt".to_owned()))?, ), - None => None, + Err(_) => None, }, u8_opt: match value.get::("u8_opt") { - Some(v) => Some( + Ok(v) => Some( v.try_into() .map_err(|_| Error::TypeMismatch("u8_opt".to_owned()))?, ), - None => None, + Err(_) => None, }, u16_opt: match value.get::("u16_opt") { - Some(v) => Some( + Ok(v) => Some( v.try_into() .map_err(|_| Error::TypeMismatch("u16_opt".to_owned()))?, ), - None => None, + Err(_) => None, }, u32_opt: match value.get::("u32_opt") { - Some(v) => Some( + Ok(v) => Some( v.try_into() .map_err(|_| Error::TypeMismatch("u32_opt".to_owned()))?, ), - None => None, + Err(_) => None, }, u64_opt: match value.get::("u64_opt") { - Some(v) => Some( + Ok(v) => Some( v.try_into() .map_err(|_| Error::TypeMismatch("u64_opt".to_owned()))?, ), - None => None, + Err(_) => None, }, u128_opt: match value.get::("u128_opt") { - Some(v) => Some( + Ok(v) => Some( v.try_into() .map_err(|_| Error::TypeMismatch("u128_opt".to_owned()))?, ), - None => None, + Err(_) => None, }, i8_opt: match value.get::("i8_opt") { - Some(v) => Some( + Ok(v) => Some( v.try_into() .map_err(|_| Error::TypeMismatch("i8_opt".to_owned()))?, ), - None => None, + Err(_) => None, }, i16_opt: match value.get::("i16_opt") { - Some(v) => Some( + Ok(v) => Some( v.try_into() .map_err(|_| Error::TypeMismatch("i16_opt".to_owned()))?, ), - None => None, + Err(_) => None, }, i32_opt: match value.get::("i32_opt") { - Some(v) => Some( + Ok(v) => Some( v.try_into() .map_err(|_| Error::TypeMismatch("i32_opt".to_owned()))?, ), - None => None, + Err(_) => None, }, i64_opt: match value.get("i64_opt") { - Some(v) => Some(v), - None => None, + Ok(v) => Some(v), + Err(_) => None, }, i128_opt: match value.get::("i128_opt") { - Some(v) => Some( + Ok(v) => Some( v.try_into() .map_err(|_| Error::TypeMismatch("i128_opt".to_owned()))?, ), - None => None, + Err(_) => None, }, f32_opt: match value.get::("f32_opt") { - Some(v) => Some(v as f32), - None => None, + Ok(v) => Some(v as f32), + Err(_) => None, }, f64_opt: match value.get("f64_opt") { - Some(v) => Some(v), - None => None, + Ok(v) => Some(v), + Err(_) => None, }, }) } diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 0fbc621..73dc638 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -1,5 +1,4 @@ -//! A low-level abstraction for working with serialization, storage, and IPC. -//! [Data Transfer Object](https://en.wikipedia.org/wiki/Data_transfer_object) +//! A collection of traits and macros for working Data Transfer Objects (DTOs) Cypher and Neo4j. //! //! It works with key-value pairs; only structs with named fields are supported. //! @@ -8,6 +7,9 @@ //! an [dto::Identifier]. Identifiers are also DTOs, but they cannot have an //! Identifier themselves. //! +#![doc = include_str!("../include/static_strings.rs")] +#![doc = include_str!("../include/rename.rs")] +//! //! The `derive` macro will implement [dto::WithId] on the attributed struct, //! and create a [dto::Identifier] struct named e.g. `FooId`. The ID struct will use the //! same [typename()] as the attributed struct, and include any fields that are @@ -20,6 +22,9 @@ //! Dynamically added methods: //! 1. `fn into_values(self)` - returns a tuple of all the values in the struct. +// #![warn(missing_docs)] +// #![deny(rustdoc::broken_intra_doc_links)] + mod entity; mod error; mod format; @@ -28,9 +33,9 @@ mod relationship; mod stamps; #[cfg(feature = "macros")] -pub use cypher_dto_macros::*; +pub use cypher_dto_macros::{timestamps, Node, Relation}; -pub use entity::{Entity, QueryFields, StampMode}; +pub use entity::{FieldSet, StampMode}; pub use error::Error; pub use format::{format_param, format_query_fields}; pub use node::{NodeEntity, NodeId}; diff --git a/lib/src/node.rs b/lib/src/node.rs index 5da1966..9e86af6 100644 --- a/lib/src/node.rs +++ b/lib/src/node.rs @@ -1,11 +1,20 @@ -use crate::{Entity, StampMode}; +use crate::{FieldSet, StampMode}; use neo4rs::{Node, Query}; /// A node [Entity]. -pub trait NodeEntity: Entity + TryFrom { +pub trait NodeEntity: FieldSet + TryFrom { type Id: NodeId; + /// Get the [NodeId] for this entity. + /// + /// This is less efficient than using self.into(), but is useful when you + /// don't want to consume the entity. + /// + /// The implementation in derive will clone the individual ID fields as + /// necessary. fn identifier(&self) -> Self::Id; + + /// Convenience method for `self.into()`. fn into_identifier(self) -> Self::Id { self.into() } @@ -13,7 +22,7 @@ pub trait NodeEntity: Entity + TryFrom { fn create(&self) -> Query { let q = Query::new(format!( "CREATE (n:{})", - Self::as_query_obj(None, StampMode::Create), + Self::to_query_obj(None, StampMode::Create), )); self.add_values_to_params(q, None, StampMode::Create) } @@ -24,28 +33,31 @@ pub trait NodeEntity: Entity + TryFrom { fn update(&self) -> Query { let q = Query::new(format!( "MATCH (n:{}) SET n += {{ {} }}", - Self::Id::as_query_obj(None, StampMode::Read), - Self::as_query_fields(None, StampMode::Update), + Self::Id::to_query_obj(None, StampMode::Read), + Self::to_query_fields(None, StampMode::Update), )); self.add_values_to_params(q, None, StampMode::Update) } } /// The identifying fields of a [NodeEntity]. -pub trait NodeId: Entity + From + TryFrom { - type T: NodeEntity; +pub trait NodeId: FieldSet + From + TryFrom { + type T: NodeEntity; + /// Read a [NodeEntity] by its id, using "n" as the variable for the node. fn read(&self) -> Query { let q = Query::new(format!( "MATCH (n:{}) RETURN n", - Self::as_query_obj(None, StampMode::Read) + Self::to_query_obj(None, StampMode::Read) )); self.add_values_to_params(q, None, StampMode::Read) } + + /// Delete a [NodeEntity] by its id, using "n" as the variable for the node. fn delete(&self) -> Query { let q = Query::new(format!( "MATCH (n:{}) DETACH DELETE n", - Self::as_query_obj(None, StampMode::Read) + Self::to_query_obj(None, StampMode::Read) )); self.add_values_to_params(q, None, StampMode::Read) } diff --git a/lib/src/relationship.rs b/lib/src/relationship.rs index 077f461..eb15e3f 100644 --- a/lib/src/relationship.rs +++ b/lib/src/relationship.rs @@ -1,11 +1,20 @@ -use crate::{Entity, NodeEntity, NodeId, QueryFields, StampMode}; +use crate::{FieldSet, NodeEntity, NodeId, StampMode}; use neo4rs::{Query, Relation, UnboundedRelation}; -/// A relationship [Entity]. -pub trait RelationEntity: Entity + TryFrom + TryFrom { +/// A relationship entity. +pub trait RelationEntity: FieldSet + TryFrom + TryFrom { type Id: RelationId; + /// Get the [RelationId] for this entity. + /// + /// This is less efficient than using self.into(), but is useful when you + /// don't want to consume the entity. + /// + /// The implementation in derive will clone the individual ID fields as + /// necessary. fn identifier(&self) -> Self::Id; + + /// Convenience method for `self.into()`. fn into_identifier(self) -> Self::Id { self.into() } @@ -21,9 +30,9 @@ pub trait RelationEntity: Entity + TryFrom + TryFrom(e) "###, - start.to_line("s"), - end.to_line("e"), - Self::as_query_obj(None, StampMode::Create) + start.to_query_clause("s"), + end.to_query_clause("e"), + Self::to_query_obj(None, StampMode::Create) ); // trace!("creating relation: {}", q); let mut q = Query::new(q); @@ -34,7 +43,7 @@ pub trait RelationEntity: Entity + TryFrom + TryFrom + TryFrom + TryFrom + TryFrom + TryFrom + TryFrom + TryFrom + FieldSet + From + TryFrom + TryFrom { - type T: RelationEntity; + type T: RelationEntity; /// Use only for relations that have one or more ID fields, otherwise use the other `read_` methods. /// - /// This will read all relations of the same type if [field_names()] is empty. + /// This will read all relations of the same type if [FieldSet::field_names()] is empty. fn read(&self) -> Query { assert!(!Self::field_names().is_empty()); let q = Query::new(format!( "MATCH [r:{}] RETURN r", - Self::as_query_obj(None, StampMode::Read) + Self::to_query_obj(None, StampMode::Read) )); self.add_values_to_params(q, None, StampMode::Read) } @@ -105,8 +114,8 @@ pub trait RelationId: let mut q = Query::new(format!( "MATCH (n:{})-[r:{}]-() RETURN r", - T::as_query_obj(Some("n"), StampMode::Read), - Self::as_query_obj(None, StampMode::Read) + T::to_query_obj(Some("n"), StampMode::Read), + Self::to_query_obj(None, StampMode::Read) )); q = from.add_values_to_params(q, Some("n"), StampMode::Read); self.add_values_to_params(q, None, StampMode::Read) @@ -116,9 +125,9 @@ pub trait RelationId: let mut q = Query::new(format!( "MATCH (s:{})-[r:{}]-(e:{}) RETURN r", - S::as_query_obj(Some("s"), StampMode::Read), - Self::as_query_obj(None, StampMode::Read), - E::as_query_obj(Some("e"), StampMode::Read), + S::to_query_obj(Some("s"), StampMode::Read), + Self::to_query_obj(None, StampMode::Read), + E::to_query_obj(Some("e"), StampMode::Read), )); q = start.add_values_to_params(q, Some("s"), StampMode::Read); q = end.add_values_to_params(q, Some("e"), StampMode::Read); @@ -126,12 +135,12 @@ pub trait RelationId: } /// Use only for relations that have one or more ID fields, otherwise use the other `delete_` methods. /// - /// This will delete all relations of the same type if [field_names()] is empty. + /// This will delete all relations of the same type if [FieldSet::field_names()] is empty. fn delete(&self) -> Query { assert!(!Self::field_names().is_empty()); let q = Query::new(format!( "MATCH [r:{}] DELETE r", - Self::as_query_obj(None, StampMode::Read) + Self::to_query_obj(None, StampMode::Read) )); self.add_values_to_params(q, None, StampMode::Read) } @@ -140,8 +149,8 @@ pub trait RelationId: let mut q = Query::new(format!( "MATCH (n:{})-[r:{}]-() DELETE r", - T::as_query_obj(Some("n"), StampMode::Read), - Self::as_query_obj(None, StampMode::Read) + T::to_query_obj(Some("n"), StampMode::Read), + Self::to_query_obj(None, StampMode::Read) )); q = from.add_values_to_params(q, Some("n"), StampMode::Read); self.add_values_to_params(q, None, StampMode::Read) @@ -151,9 +160,9 @@ pub trait RelationId: let mut q = Query::new(format!( "MATCH (s:{})-[r:{}]-(e:{}) DELETE r", - S::as_query_obj(Some("s"), StampMode::Read), - Self::as_query_obj(None, StampMode::Read), - E::as_query_obj(Some("e"), StampMode::Read), + S::to_query_obj(Some("s"), StampMode::Read), + Self::to_query_obj(None, StampMode::Read), + E::to_query_obj(Some("e"), StampMode::Read), )); q = start.add_values_to_params(q, Some("s"), StampMode::Read); q = end.add_values_to_params(q, Some("e"), StampMode::Read); @@ -170,17 +179,18 @@ pub enum RelationBound<'a, T: NodeEntity> { // Merge(T), } impl<'a, T: NodeEntity> RelationBound<'a, T> { - pub fn to_line(&self, prefix: &str) -> String { + /// Returns a CREATE (node:...) or MATCH (node:...) clause for this variant. + pub fn to_query_clause(&self, prefix: &str) -> String { match self { RelationBound::Create(_) => format!( "CREATE ({}:{})", prefix, - T::as_query_obj(Some(prefix), StampMode::Create) + T::to_query_obj(Some(prefix), StampMode::Create) ), RelationBound::Match(_) => format!( "MATCH ({}:{})", prefix, - T::Id::as_query_obj(Some(prefix), StampMode::Read) + T::Id::to_query_obj(Some(prefix), StampMode::Read) ), // RelationBound::Merge(_) => { // format!( diff --git a/lib/src/stamps.rs b/lib/src/stamps.rs index 8043490..65fea42 100644 --- a/lib/src/stamps.rs +++ b/lib/src/stamps.rs @@ -60,8 +60,6 @@ impl Stamps { } /// Returns these timestamp fields as a query string, depending on the [StampMode]. - /// - /// Similar to [QueryFields::as_query_fields], but with a Stamps instance. pub fn as_query_fields(&self, prefix: Option<&str>, mode: StampMode) -> String { if self == &Stamps::None { return "".to_owned(); @@ -123,19 +121,19 @@ impl<'a> Neo4jMap<'a> { Neo4jMap::Row(value) => value .get::>(name) .map(|dt| dt.into()) - .ok_or(crate::Error::MissingField(name.to_owned())), + .map_err(|_e| crate::Error::MissingField(name.to_owned())), Neo4jMap::Node(value) => value .get::>(name) .map(|dt| dt.into()) - .ok_or(crate::Error::MissingField(name.to_owned())), + .map_err(|_e| crate::Error::MissingField(name.to_owned())), Neo4jMap::Relation(value) => value .get::>(name) .map(|dt| dt.into()) - .ok_or(crate::Error::MissingField(name.to_owned())), + .map_err(|_e| crate::Error::MissingField(name.to_owned())), Neo4jMap::UnboundedRelation(value) => value .get::>(name) .map(|dt| dt.into()) - .ok_or(crate::Error::MissingField(name.to_owned())), + .map_err(|_e| crate::Error::MissingField(name.to_owned())), } } } diff --git a/lib/tests/basic_crud.rs b/lib/tests/basic_crud.rs index 963b6b7..596d2f0 100644 --- a/lib/tests/basic_crud.rs +++ b/lib/tests/basic_crud.rs @@ -58,7 +58,7 @@ async fn basic_crud() { graph.run(acme.identifier().delete()).await.unwrap(); let mut stream = graph - .execute(Query::new(format!("MATCH (n:Company) RETURN n"))) + .execute(Query::new("MATCH (n:Company) RETURN n".to_string())) .await .unwrap(); let row = stream.next().await.unwrap(); diff --git a/lib/tests/common/container.rs b/lib/tests/common/container.rs index f0114fa..fe01015 100644 --- a/lib/tests/common/container.rs +++ b/lib/tests/common/container.rs @@ -2,19 +2,52 @@ //! //! It also supports connecting to a real server using environment variables. //! -//! [Original source](https://github.com/neo4j-labs/neo4rs/blob/f1db22cab08c1f911876da43effc61a207828d85/lib/tests/container.rs) +//! [Original source](https://github.com/neo4j-labs/neo4rs/blob/ec0261895f56e476f4f1eb9c6a2151c7b945d454/lib/tests/container.rs) use lenient_semver::Version; -use neo4j_testcontainers::Neo4j; use neo4rs::{ConfigBuilder, Graph}; -use testcontainers::{clients::Cli, Container}; +use testcontainers::{clients::Cli, Container, RunnableImage}; +use testcontainers_modules::neo4j::{Neo4j, Neo4jImage}; -use std::sync::Arc; +use std::{error::Error, io::BufRead as _}; + +#[allow(dead_code)] +#[derive(Default)] +pub struct Neo4jContainerBuilder { + enterprise: bool, + config: ConfigBuilder, +} + +#[allow(dead_code)] +impl Neo4jContainerBuilder { + pub fn new() -> Self { + Self::default() + } + + pub fn with_enterprise_edition(mut self) -> Self { + self.enterprise = true; + self + } + + pub fn with_config(mut self, config: ConfigBuilder) -> Self { + self.config = config; + self + } + + pub fn modify_config(mut self, block: impl FnOnce(ConfigBuilder) -> ConfigBuilder) -> Self { + self.config = block(self.config); + self + } + + pub async fn start(self) -> Result> { + Neo4jContainer::from_config_and_edition(self.config, self.enterprise).await + } +} pub struct Neo4jContainer { - graph: Arc, + graph: Graph, version: String, - _container: Option>, + _container: Option>, } impl Neo4jContainer { @@ -24,31 +57,36 @@ impl Neo4jContainer { } pub async fn from_config(config: ConfigBuilder) -> Self { + Self::from_config_and_edition(config, false).await.unwrap() + } + + pub async fn from_config_and_edition( + config: ConfigBuilder, + enterprise_edition: bool, + ) -> Result> { let _ = pretty_env_logger::try_init(); + let connection = Self::create_test_endpoint(); let server = Self::server_from_env(); - let (connection, _container) = match server { + let (uri, _container) = match server { TestServer::TestContainer => { - let (connection, container) = Self::create_testcontainer(); - (connection, Some(container)) - } - TestServer::External(uri) => { - let connection = Self::create_test_endpoint(uri); - (connection, None) + let (uri, container) = Self::create_testcontainer(&connection, enterprise_edition)?; + (uri, Some(container)) } + TestServer::External(uri) => (uri, None), }; let version = connection.version; - let graph = Self::connect(config, connection.uri, &connection.auth).await; - Self { + let graph = Self::connect(config, uri, &connection.auth).await; + Ok(Self { graph, version, _container, - } + }) } - pub fn graph(&self) -> Arc { + pub fn graph(&self) -> Graph { self.graph.clone() } @@ -70,24 +108,63 @@ impl Neo4jContainer { } } - fn create_testcontainer() -> (TestConnection, Container<'static, Neo4j>) { + fn create_testcontainer( + connection: &TestConnection, + enterprise: bool, + ) -> Result<(String, Container<'static, Neo4jImage>), Box> + { + let image = Neo4j::new() + .with_user(connection.auth.user.to_owned()) + .with_password(connection.auth.pass.to_owned()); + let docker = Cli::default(); let docker = Box::leak(Box::new(docker)); - let container = docker.run(Neo4j::default()); + let container = if enterprise { + const ACCEPTANCE_FILE_NAME: &str = "container-license-acceptance.txt"; + + let version = format!("{}-enterprise", connection.version); + let image_name = format!("neo4j:{}", version); + + let acceptance_file = std::env::current_dir() + .ok() + .map(|o| o.join(ACCEPTANCE_FILE_NAME)); + + let has_license_acceptance = acceptance_file + .as_deref() + .and_then(|o| std::fs::File::open(o).ok()) + .into_iter() + .flat_map(|o| std::io::BufReader::new(o).lines()) + .any(|o| o.map_or(false, |line| line.trim() == image_name)); + + if !has_license_acceptance { + return Err(format!( + concat!( + "You need to accept the Neo4j Enterprise Edition license by ", + "creating the file `{}` with the following content:\n\n\t{}", + ), + acceptance_file.map_or_else( + || ACCEPTANCE_FILE_NAME.to_owned(), + |o| { o.display().to_string() } + ), + image_name + ) + .into()); + } + let image: RunnableImage = image.with_version(version).into(); + let image = image.with_env_var(("NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes")); - let uri = Neo4j::uri_ipv4(&container); - let version = container.image().version().to_owned(); - let user = container.image().user().to_owned(); - let pass = container.image().pass().to_owned(); - let auth = TestAuth { user, pass }; + docker.run(image) + } else { + docker.run(image.with_version(connection.version.to_owned())) + }; - let connection = TestConnection { uri, version, auth }; + let uri = format!("bolt://127.0.0.1:{}", container.image().bolt_port_ipv4()); - (connection, container) + Ok((uri, container)) } - fn create_test_endpoint(uri: String) -> TestConnection { + fn create_test_endpoint() -> TestConnection { const USER_VAR: &str = "NEO4J_TEST_USER"; const PASS_VAR: &str = "NEO4J_TEST_PASS"; const VERSION_VAR: &str = "NEO4J_VERSION_TAG"; @@ -103,10 +180,10 @@ impl Neo4jContainer { let auth = TestAuth { user, pass }; let version = var(VERSION_VAR).unwrap_or_else(|_| DEFAULT_VERSION_TAG.to_owned()); - TestConnection { uri, auth, version } + TestConnection { auth, version } } - async fn connect(config: ConfigBuilder, uri: String, auth: &TestAuth) -> Arc { + async fn connect(config: ConfigBuilder, uri: String, auth: &TestAuth) -> Graph { let config = config .uri(uri) .user(&auth.user) @@ -114,9 +191,7 @@ impl Neo4jContainer { .build() .unwrap(); - let graph = Graph::connect(config).await.unwrap(); - - Arc::new(graph) + Graph::connect(config).await.unwrap() } } @@ -126,7 +201,6 @@ struct TestAuth { } struct TestConnection { - uri: String, version: String, auth: TestAuth, } diff --git a/lib/tests/common/fixtures/company.rs b/lib/tests/common/fixtures/company.rs index 81f94b8..29831ec 100644 --- a/lib/tests/common/fixtures/company.rs +++ b/lib/tests/common/fixtures/company.rs @@ -1,7 +1,5 @@ use chrono::{DateTime, Utc}; -use cypher_dto::{ - format_param, Entity, Error, Neo4jMap, NodeEntity, NodeId, QueryFields, StampMode, -}; +use cypher_dto::{format_param, Error, FieldSet, Neo4jMap, NodeEntity, NodeId, StampMode}; use neo4rs::{Node, Query, Row}; /// Has a multi-valued ID and required timestamps. @@ -12,16 +10,23 @@ pub struct Company { pub created: DateTime, pub updated: DateTime, } -impl Entity for Company { +impl FieldSet for Company { fn typename() -> &'static str { "Company" } -} -impl QueryFields for Company { + fn field_names() -> &'static [&'static str] { &["name", "state", "created", "updated"] } + fn as_query_fields() -> &'static str { + "name: $name, state: $state, created: $created, updated: $updated" + } + + fn as_query_obj() -> &'static str { + "Company { name: $name, state: $state, created: $created, updated: $updated }" + } + fn add_values_to_params(&self, mut q: Query, prefix: Option<&str>, mode: StampMode) -> Query { q = q.param(&format_param("name", prefix), self.name.clone()); q = q.param(&format_param("state", prefix), self.state.clone()); @@ -50,10 +55,10 @@ impl TryFrom for Company { Ok(Self { name: value .get("name") - .ok_or(Error::MissingField("name".to_owned()))?, + .map_err(|_e| Error::MissingField("name".to_owned()))?, state: value .get("state") - .ok_or(Error::MissingField("state".to_owned()))?, + .map_err(|_e| Error::MissingField("state".to_owned()))?, created: map.get_timestamp("created")?, updated: map.get_timestamp("updated")?, }) @@ -75,10 +80,10 @@ impl TryFrom for Company { Ok(Self { name: value .get("name") - .ok_or(Error::MissingField("name".to_owned()))?, + .map_err(|_e| Error::MissingField("name".to_owned()))?, state: value .get("state") - .ok_or(Error::MissingField("state".to_owned()))?, + .map_err(|_e| Error::MissingField("state".to_owned()))?, created: map.get_timestamp("created")?, updated: map.get_timestamp("updated")?, }) @@ -110,22 +115,27 @@ impl TryFrom for CompanyId { Ok(Self { name: value .get("name") - .ok_or(Error::MissingField("name".to_owned()))?, + .map_err(|_e| Error::MissingField("name".to_owned()))?, state: value .get("state") - .ok_or(Error::MissingField("state".to_owned()))?, + .map_err(|_e| Error::MissingField("state".to_owned()))?, }) } } -impl Entity for CompanyId { +impl FieldSet for CompanyId { fn typename() -> &'static str { Company::typename() } -} -impl QueryFields for CompanyId { + fn field_names() -> &'static [&'static str] { &["name", "state"] } + fn as_query_fields() -> &'static str { + "name: $name, state: $state" + } + fn as_query_obj() -> &'static str { + "Company { name: $name, state: $state }" + } fn add_values_to_params(&self, query: Query, prefix: Option<&str>, _: StampMode) -> Query { query .param(&format_param("name", prefix), self.name.clone()) @@ -138,10 +148,10 @@ impl TryFrom for CompanyId { Ok(Self { name: value .get("name") - .ok_or(Error::MissingField("name".to_owned()))?, + .map_err(|_e| Error::MissingField("name".to_owned()))?, state: value .get("state") - .ok_or(Error::MissingField("state".to_owned()))?, + .map_err(|_e| Error::MissingField("state".to_owned()))?, }) } } diff --git a/lib/tests/common/fixtures/mod.rs b/lib/tests/common/fixtures/mod.rs index 3a09e2e..3599489 100644 --- a/lib/tests/common/fixtures/mod.rs +++ b/lib/tests/common/fixtures/mod.rs @@ -1,4 +1,5 @@ #![allow(dead_code)] +#![allow(unused_imports)] //! Hand implementations for what the macros would generate. mod company; diff --git a/lib/tests/common/fixtures/person.rs b/lib/tests/common/fixtures/person.rs index 9b84961..1c3d14d 100644 --- a/lib/tests/common/fixtures/person.rs +++ b/lib/tests/common/fixtures/person.rs @@ -1,7 +1,5 @@ use chrono::{DateTime, Utc}; -use cypher_dto::{ - format_param, Entity, Error, Neo4jMap, NodeEntity, NodeId, QueryFields, StampMode, -}; +use cypher_dto::{format_param, Error, FieldSet, Neo4jMap, NodeEntity, NodeId, StampMode}; use neo4rs::{Node, Query, Row}; /// Single ID field and optional timestamps. Has example of `new()` and `into_builder()` methods. @@ -42,15 +40,20 @@ impl Person { self.into() } } -impl Entity for Person { +impl FieldSet for Person { fn typename() -> &'static str { "Person" } -} -impl QueryFields for Person { + fn field_names() -> &'static [&'static str] { &["id", "name", "age", "created_at", "updated_at"] } + fn as_query_fields() -> &'static str { + "id: $id, name: $name, age: $age, created_at: $created_at, updated_at: $updated_at" + } + fn as_query_obj() -> &'static str { + "Person { id: $id, name: $name, age: $age, created_at: $created_at, updated_at: $updated_at }" + } fn add_values_to_params(&self, mut q: Query, prefix: Option<&str>, mode: StampMode) -> Query { q = q.param(&format_param("id", prefix), self.id.clone()); q = q.param(&format_param("name", prefix), self.name.clone()); @@ -81,16 +84,16 @@ impl TryFrom for Person { Ok(Self { id: value .get("id") - .ok_or(Error::MissingField("id".to_owned()))?, + .map_err(|_e| Error::MissingField("id".to_owned()))?, name: value .get("name") - .ok_or(Error::MissingField("name".to_owned()))?, + .map_err(|_e| Error::MissingField("name".to_owned()))?, age: match value.get::("age") { - Some(age) => Some( + Ok(age) => Some( age.try_into() .map_err(|_| Error::TypeMismatch("age".to_owned()))?, ), - None => None, + Err(_) => None, }, created_at: Some(map.get_timestamp("created_at")?), updated_at: Some(map.get_timestamp("updated_at")?), @@ -112,16 +115,16 @@ impl TryFrom for Person { Ok(Self { id: value .get("id") - .ok_or(Error::MissingField("id".to_owned()))?, + .map_err(|_e| Error::MissingField("id".to_owned()))?, name: value .get("name") - .ok_or(Error::MissingField("name".to_owned()))?, + .map_err(|_e| Error::MissingField("name".to_owned()))?, age: match value.get::("age") { - Some(age) => Some( + Ok(age) => Some( age.try_into() .map_err(|_| Error::TypeMismatch("age".to_owned()))?, ), - None => None, + Err(_) => None, }, created_at: Some(map.get_timestamp("created_at")?), updated_at: Some(map.get_timestamp("updated_at")?), @@ -150,19 +153,24 @@ impl TryFrom for PersonId { Ok(Self { id: value .get("id") - .ok_or(Error::MissingField("id".to_owned()))?, + .map_err(|_e| Error::MissingField("id".to_owned()))?, }) } } -impl Entity for PersonId { +impl FieldSet for PersonId { fn typename() -> &'static str { Person::typename() } -} -impl QueryFields for PersonId { + fn field_names() -> &'static [&'static str] { &["id"] } + fn as_query_fields() -> &'static str { + "id: $id" + } + fn as_query_obj() -> &'static str { + "Person { id: $id }" + } fn add_values_to_params(&self, query: Query, prefix: Option<&str>, _: StampMode) -> Query { query.param(&format_param("id", prefix), self.id.clone()) } @@ -173,7 +181,7 @@ impl TryFrom for PersonId { Ok(Self { id: value .get("id") - .ok_or(Error::MissingField("id".to_owned()))?, + .map_err(|_e| Error::MissingField("id".to_owned()))?, }) } } diff --git a/lib/tests/common/fixtures/worked_at.rs b/lib/tests/common/fixtures/worked_at.rs index f845808..1166629 100644 --- a/lib/tests/common/fixtures/worked_at.rs +++ b/lib/tests/common/fixtures/worked_at.rs @@ -1,7 +1,5 @@ use chrono::{DateTime, Utc}; -use cypher_dto::{ - format_param, Entity, Error, Neo4jMap, QueryFields, RelationEntity, RelationId, StampMode, -}; +use cypher_dto::{format_param, Error, FieldSet, Neo4jMap, RelationEntity, RelationId, StampMode}; use neo4rs::{Query, Relation, Row, UnboundedRelation}; /// A relation with an ID field. @@ -11,15 +9,20 @@ use neo4rs::{Query, Relation, Row, UnboundedRelation}; pub struct WorkedAt { pub until: DateTime, } -impl Entity for WorkedAt { +impl FieldSet for WorkedAt { fn typename() -> &'static str { "WORKED_AT" } -} -impl QueryFields for WorkedAt { + fn field_names() -> &'static [&'static str] { &["until"] } + fn as_query_fields() -> &'static str { + "until: $until" + } + fn as_query_obj() -> &'static str { + "WORKED_AT { until: $until }" + } fn add_values_to_params(&self, q: Query, prefix: Option<&str>, _mode: StampMode) -> Query { q.param(&format_param("until", prefix), self.until.fixed_offset()) @@ -88,15 +91,20 @@ impl TryFrom for WorkedAtId { }) } } -impl Entity for WorkedAtId { +impl FieldSet for WorkedAtId { fn typename() -> &'static str { WorkedAt::typename() } -} -impl QueryFields for WorkedAtId { + fn field_names() -> &'static [&'static str] { &["until"] } + fn as_query_fields() -> &'static str { + "until: $until" + } + fn as_query_obj() -> &'static str { + "WORKED_AT { until: $until }" + } fn add_values_to_params(&self, q: Query, prefix: Option<&str>, _mode: StampMode) -> Query { q.param(&format_param("until", prefix), self.until.fixed_offset()) } diff --git a/lib/tests/common/fixtures/works_at.rs b/lib/tests/common/fixtures/works_at.rs index 47fd6fc..b162a84 100644 --- a/lib/tests/common/fixtures/works_at.rs +++ b/lib/tests/common/fixtures/works_at.rs @@ -1,18 +1,23 @@ -use cypher_dto::{Entity, Error, QueryFields, RelationEntity, RelationId, StampMode}; +use cypher_dto::{Error, FieldSet, RelationEntity, RelationId, StampMode}; use neo4rs::{Query, Relation, Row, UnboundedRelation}; /// A fieldless relation. #[derive(Clone, Debug, PartialEq)] pub struct WorksAt {} -impl Entity for WorksAt { +impl FieldSet for WorksAt { fn typename() -> &'static str { "WORKS_AT" } -} -impl QueryFields for WorksAt { + fn field_names() -> &'static [&'static str] { &[] } + fn as_query_fields() -> &'static str { + "" + } + fn as_query_obj() -> &'static str { + "WORKS_AT" + } fn add_values_to_params(&self, query: Query, _: Option<&str>, _: StampMode) -> Query { query @@ -67,15 +72,20 @@ impl TryFrom for WorksAtId { Ok(Self {}) } } -impl Entity for WorksAtId { +impl FieldSet for WorksAtId { fn typename() -> &'static str { WorksAt::typename() } -} -impl QueryFields for WorksAtId { + fn field_names() -> &'static [&'static str] { &[] } + fn as_query_fields() -> &'static str { + "" + } + fn as_query_obj() -> &'static str { + "WORKS_AT" + } fn add_values_to_params(&self, query: Query, _prefix: Option<&str>, _mode: StampMode) -> Query { query } diff --git a/lib/tests/manual_queries.rs b/lib/tests/manual_queries.rs index 24b989d..e4cf29c 100644 --- a/lib/tests/manual_queries.rs +++ b/lib/tests/manual_queries.rs @@ -34,11 +34,11 @@ async fn create_all_at_once() { CREATE (bob)-[w2:{}]->(acme) RETURN alice, bob, acme, w, w2 ", - Person::as_query_obj(Some("alice"), StampMode::Create), - Person::as_query_obj(Some("bob"), StampMode::Create), - Company::as_query_obj(Some("acme"), StampMode::Create), - WorkedAt::as_query_obj(Some("w"), StampMode::Create), - WorkedAt::as_query_obj(Some("w2"), StampMode::Create), + Person::to_query_obj(Some("alice"), StampMode::Create), + Person::to_query_obj(Some("bob"), StampMode::Create), + Company::to_query_obj(Some("acme"), StampMode::Create), + WorkedAt::to_query_obj(Some("w"), StampMode::Create), + WorkedAt::to_query_obj(Some("w2"), StampMode::Create), )); q = alice.add_values_to_params(q, Some("alice"), StampMode::Create); q = bob.add_values_to_params(q, Some("bob"), StampMode::Create); @@ -55,7 +55,7 @@ async fn create_all_at_once() { assert_eq!(alice.name(), alice_db.name()); assert_eq!(alice.age(), alice_db.age()); - let bob_db: Person = row.get::("bob").unwrap().try_into().unwrap(); + let bob_db = Person::try_from(row.get::("bob").unwrap()).unwrap(); assert_eq!(bob.id(), bob_db.id()); assert_eq!(bob.name(), bob_db.name()); assert_eq!(bob.age(), bob_db.age()); diff --git a/macros/Cargo.toml b/macros/Cargo.toml index 4d08df5..537af0d 100644 --- a/macros/Cargo.toml +++ b/macros/Cargo.toml @@ -1,11 +1,11 @@ [package] name = "cypher-dto-macros" description = "The macros for cypher-dto." -version = "0.2.0" +version = "0.3.0" edition = "2021" license = "MIT" keywords = ["neo4j", "cypher", "dto", "query", "graph"] -categories = ["database", "data-structures", "encoding"] +categories = ["database"] repository = "https://github.com/jifalops/cypher-dto" [lib] diff --git a/macros/README.md b/macros/README.md index 47a370b..2ad5035 100644 --- a/macros/README.md +++ b/macros/README.md @@ -19,7 +19,7 @@ There are three types of macros contained: struct Knows; ``` - These will implement `cypher_dto::{Entity, QueryFields, NodeEntity, RelationEntity}` for the structs. + These will implement `cypher_dto::{FieldSet, NodeEntity, RelationEntity}` for the structs. There are two helper attributes that come into scope when using the derive macros, `#[id]`, and `#[name]`. @@ -29,39 +29,6 @@ There are three types of macros contained: A builder is also generated for each struct, e.g. `PersonBuilder`. It can be obtained via `person.into_builder()`. -2. The `#[stamps]` macro, which will add one or two timestamp fields to the struct. By default it adds two fields, `created_at` and `updated_at`, with the type `Option>`. Optional timestamp fields let the `Person::new()` implementation skip having them as arguments, which is how you would create a DTO in application code before it is created in the database. +2. The `#[timestamps]` macro, which will add one or two timestamp fields to the struct. By default it adds two fields, `created_at` and `updated_at`, with the type `Option>`. Optional timestamp fields let the `Person::new()` implementation skip having them as arguments, which is how you would create a DTO in application code before it is created in the database. - You can control which fields it adds by specifying certain values as a string (`#[stamps = "..."]` and `#[stamps("...")]` are supported). The values must be ONE of `full` (the default), `short` (created, updated), `created_at`, `updated_at`, `created`, or `updated`. - -3. The `#[node]` and `#[relation]` macros, which are a shorthand for the derive and stamps macros. The following are equivalent: - - ```rust - #[node] - struct Person {} - - #[derive(Node, Clone, Debug, PartialEq)] - struct Person {} - - // Or, if using the `serde` feature: - #[derive(Node, Clone, Debug, PartialEq, Serialize, Deserialize)] - struct Person {} - ``` - - ```rust - #[relation(stamps = "short")] - struct Knows {} - - #[stamps = "short"] - #[derive(Relation, Clone, Debug, PartialEq)] - struct Knows {} - ``` - - ```rust - #node[stamps, name = "Foo"] - struct Person {} - - #[stamps] - #[derive(Node, Clone, Debug, PartialEq)] - #[name = "Foo"] - struct Person {} - ``` + You can control which fields it adds by specifying certain values as a string (`#[timestamps = "..."]` and `#[timestamps("...")]` are supported). The values must be ONE of `full` (the default), `short` (created, updated), `created_at`, `updated_at`, `created`, or `updated`. diff --git a/macros/src/derive.rs b/macros/src/derive.rs index 578e5aa..7376609 100644 --- a/macros/src/derive.rs +++ b/macros/src/derive.rs @@ -69,8 +69,5 @@ mod tests { assert_eq!(parse_name_meta(&attr.meta), Some("Foo".to_owned())); let attr: Attribute = parse_quote!(#[name = "Foo"]); assert_eq!(parse_name_meta(&attr.meta), Some("Foo".to_owned())); - let args = quote!(#[foo(name = "Foo", stamps = "full")]); - let meta: Meta = syn::parse2(args).unwrap(); - assert_eq!(parse_name_meta(&meta), Some("Foo".to_owned())); } } diff --git a/macros/src/derive/entity.rs b/macros/src/derive/entity.rs index 7e64e9e..dfc15e8 100644 --- a/macros/src/derive/entity.rs +++ b/macros/src/derive/entity.rs @@ -65,20 +65,31 @@ impl Entity { let new_and_getters = new_and_getters::impl_new_and_getters(self); - quote! { - use ::cypher_dto::Entity as _; - use ::cypher_dto::QueryFields as _; + let as_fields = names + .iter() + .map(|n| format!("{n}: ${n}")) + .collect::>() + .join(", "); + let as_obj = format!("{} {{ {} }}", struct_name, as_fields); - impl ::cypher_dto::Entity for #struct_ident { + quote! { + impl ::cypher_dto::FieldSet for #struct_ident { fn typename() -> &'static str { #struct_name } - } - impl ::cypher_dto::QueryFields for #struct_ident { + fn field_names() -> &'static [&'static str] { &[#(#names),*] } + fn as_query_fields() -> &'static str { + #as_fields + } + + fn as_query_obj() -> &'static str { + #as_obj + } + fn add_values_to_params(&self, mut query: ::neo4rs::Query, prefix: Option<&str>, mode: ::cypher_dto::StampMode) -> ::neo4rs::Query { #(query = #into_params;)* query diff --git a/macros/src/derive/entity/builder.rs b/macros/src/derive/entity/builder.rs index a9e7118..97549f7 100644 --- a/macros/src/derive/entity/builder.rs +++ b/macros/src/derive/entity/builder.rs @@ -1,14 +1,13 @@ use super::{ArgHelper, Entity}; use quote::{__private::TokenStream, format_ident, quote}; -use syn::Type; pub fn impl_builder(entity: &Entity) -> TokenStream { let entity_ident = entity.ident(); - let entity_name = entity.name(); + let _entity_name = entity.name(); let ident = format_ident!("{}Builder", entity_ident); let (idents, types, names, comments, _into_params, _from_boltmaps) = entity.fields.to_vectors(); - let mut opt_types = Vec::new(); + let mut all_types = Vec::new(); let ( arg_type, arg_into_field_suffix, @@ -23,29 +22,21 @@ pub fn impl_builder(entity: &Entity) -> TokenStream { for index in 0..idents.len() { let id = idents[index]; - let name = names[index]; + let _name = names[index]; let ty = types[index]; let arg_convert = &arg_into_field_suffix[index]; match ty.is_option() { true => { - opt_types.push(ty.as_type().clone()); - assignments.push(quote!(#id)); + all_types.push(ty.as_type().clone()); + assignments.push(quote!(#id #arg_convert)); from_entity.push(quote!(value.#id)); into_entity.push(quote!(value.#id)); } false => { - let t = ty.as_type(); - let s = format!("Option<{}>", quote!(#t)); - opt_types.push(match syn::parse_str::(&s) { - Ok(ty) => ty, - Err(e) => panic!( - "Failed to wrap type in Option for {}: {}. ({})", - ident, e, s - ), - }); - assignments.push(quote!(Some(#id #arg_convert))); - from_entity.push(quote!(Some(value.#id))); - into_entity.push(quote!(value.#id.ok_or(::cypher_dto::Error::BuilderError(#entity_name.to_owned(), #name.to_owned()))?)); + all_types.push(ty.as_type().clone()); + assignments.push(quote!(#id #arg_convert)); + from_entity.push(quote!(value.#id)); + into_entity.push(quote!(value.#id)); } } } @@ -53,14 +44,9 @@ pub fn impl_builder(entity: &Entity) -> TokenStream { quote! { #vis struct #ident { - #( #idents: #opt_types, )* + #( #idents: #all_types, )* } impl #ident { - pub fn new() -> Self { - Self { - #( #idents: None, )* - } - } #( #( #comments )* pub fn #idents(mut self, #idents: #arg_type) -> Self { @@ -68,13 +54,8 @@ pub fn impl_builder(entity: &Entity) -> TokenStream { self } )* - pub fn build(self) -> ::std::result::Result<#entity_ident, ::cypher_dto::Error> { - self.try_into() - } - } - impl Default for #ident { - fn default() -> Self { - Self::new() + pub fn build(self) -> #entity_ident { + self.into() } } impl From<#entity_ident> for #ident { @@ -84,12 +65,11 @@ pub fn impl_builder(entity: &Entity) -> TokenStream { } } } - impl TryFrom<#ident> for #entity_ident { - type Error = ::cypher_dto::Error; - fn try_from(value: #ident) -> ::std::result::Result { - Ok(Self { + impl From<#ident> for #entity_ident { + fn from(value: #ident) -> Self { + Self { #( #idents: #into_entity, )* - }) + } } } impl #entity_ident { diff --git a/macros/src/derive/entity/field/field_type.rs b/macros/src/derive/entity/field/field_type.rs index 022940d..0691032 100644 --- a/macros/src/derive/entity/field/field_type.rs +++ b/macros/src/derive/entity/field/field_type.rs @@ -119,7 +119,7 @@ impl ArgHelper { field_into_getter_suffix, } } else { - let is_copy = is_copy_type(&ty); + let is_copy = is_copy_type(ty); Self { arg_type: ty.clone(), arg_into_field_suffix: quote!(), diff --git a/macros/src/derive/entity/field/map_helper.rs b/macros/src/derive/entity/field/map_helper.rs index b9d08a7..58da3d9 100644 --- a/macros/src/derive/entity/field/map_helper.rs +++ b/macros/src/derive/entity/field/map_helper.rs @@ -10,27 +10,27 @@ pub fn field_from_boltmap(name: &str, typ: &FieldType) -> TokenStream { quote!( value.get::<::chrono::DateTime<::chrono::FixedOffset>>(#name) .map(|dt| dt.into()) - .ok_or(::cypher_dto::Error::MissingField(#name.to_owned()))? + .map_err(|e| ::cypher_dto::Error::MissingField(#name.to_owned()))? ) } FieldType::OptionDateTimeUtc(_ty) => { quote!( value.get::<::chrono::DateTime<::chrono::FixedOffset>>(#name) - .map(|dt| dt.into()) + .map(|dt| dt.into()).ok() ) } FieldType::Num(_ty, num) => { // Build from the inside out. // Example: cypher-dto/lib/src/entity.rs#L425 let mut tokens = quote!( - value.get(#name).ok_or(::cypher_dto::Error::MissingField(#name.to_owned()))? + value.get(#name).map_err(|e| ::cypher_dto::Error::MissingField(#name.to_owned()))? ); // Handle `value.get::()` if let Some(type_arg) = num.map_getter_type_arg() { let cast_type = type_arg.to_type(); tokens = quote!( - value.get::<#cast_type>(#name).ok_or(::cypher_dto::Error::MissingField(#name.to_owned()))? + value.get::<#cast_type>(#name).map_err(|e| ::cypher_dto::Error::MissingField(#name.to_owned()))? ); } @@ -111,8 +111,8 @@ pub fn field_from_boltmap(name: &str, typ: &FieldType) -> TokenStream { quote!( match #get_call { - Some(v) => Some(#some_inner), - None => None, + Ok(v) => Some(#some_inner), + Err(_) => None, } ) } @@ -123,7 +123,7 @@ pub fn field_from_boltmap(name: &str, typ: &FieldType) -> TokenStream { } FieldType::Other(_ty) => { quote!( - value.get(#name).ok_or(::cypher_dto::Error::MissingField(#name.to_owned()))? + value.get(#name).map_err(|e| ::cypher_dto::Error::MissingField(#name.to_owned()))? ) } } diff --git a/macros/src/derive/node.rs b/macros/src/derive/node.rs index c2cdc89..8e0b3dc 100644 --- a/macros/src/derive/node.rs +++ b/macros/src/derive/node.rs @@ -34,7 +34,6 @@ impl Node { let builder_impl = self.inner.builder_impl(); quote! { #entity_impl - use ::cypher_dto::NodeEntity as _; impl ::cypher_dto::NodeEntity for #main_ident { type Id = #id_ident; fn identifier(&self) -> Self::Id { @@ -72,7 +71,6 @@ impl Node { #( #idents: #types, )* } #entity_impl - use ::cypher_dto::NodeId as _; impl ::cypher_dto::NodeId for #id_ident { type T = #main_ident; } diff --git a/macros/src/derive/relation.rs b/macros/src/derive/relation.rs index 1f8056d..56e6ce8 100644 --- a/macros/src/derive/relation.rs +++ b/macros/src/derive/relation.rs @@ -46,7 +46,6 @@ impl Relation { let builder_impl = self.inner.builder_impl(); quote! { #entity_impl - use ::cypher_dto::RelationEntity as _; impl ::cypher_dto::RelationEntity for #main_ident { type Id = #id_ident; fn identifier(&self) -> Self::Id { @@ -101,7 +100,6 @@ impl Relation { #( #idents: #types, )* } #entity_impl - use ::cypher_dto::RelationId as _; impl ::cypher_dto::RelationId for #id_ident { type T = #main_ident; } diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 55d62b7..b04a10b 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -1,49 +1,33 @@ use proc_macro::TokenStream; mod derive; -mod node_relation; -mod stamps; +mod timestamps; use derive::{Node, Relation}; use syn::{parse_macro_input, DeriveInput}; -/// Derives the [NodeEntity](cypher_dto::NodeEntity) and related traits. +/// Derives the [NodeEntity](::cypher_dto::NodeEntity) and related traits. #[proc_macro_derive(Node, attributes(name, id))] -pub fn derive_cypher_node(input: TokenStream) -> TokenStream { +pub fn derive_node(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); Node::new(input).to_token_stream() } -/// Derives the [RelationEntity](cypher_dto::RelationEntity) and related traits. +/// Derives the [RelationEntity](::cypher_dto::RelationEntity) and related traits. #[proc_macro_derive(Relation, attributes(name, id))] -pub fn derive_cypher_relation(input: TokenStream) -> TokenStream { +pub fn derive_relation(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); Relation::new(input).to_token_stream() } /// Adds created/updated timestamp fields to a struct, using [`Option>`] as the type. /// -/// The default field names are `created_at` and `updated_at`, but can be changed by passing a string to the `#[stamps]` attribute. +/// The default field names are `created_at` and `updated_at`, but can be changed +/// by passing a string to the `#[timestamps]` attribute. /// /// On structs marked with `#[derive(Node)]` or `#[derive(Relation)]`, /// their `::new()` implementation already checks for Optional timestamp fields will set them to `None`. #[proc_macro_attribute] -pub fn stamps(args: TokenStream, input: TokenStream) -> TokenStream { - stamps::stamps_impl(args, input) -} - -/// Shorthand for `#[derive(Node)]`, `#[stamps]`, and other common derive implementations. -/// -/// The default derives are `Clone`, `Debug`, `PartialEq`, and if the `serde` feature is enabled: `Serialize`, and `Deserialize`. -#[proc_macro_attribute] -pub fn node(args: TokenStream, input: TokenStream) -> TokenStream { - node_relation::cypher_entity_impl(args, input, derive::EntityType::Node) -} - -/// Shorthand for `#[derive(Relation)]`, `#[stamps]`, and other common derive implementations. -/// -/// The default derives are `Clone`, `Debug`, `PartialEq`, and if the `serde` feature is enabled: `Serialize`, and `Deserialize`. -#[proc_macro_attribute] -pub fn relation(args: TokenStream, input: TokenStream) -> TokenStream { - node_relation::cypher_entity_impl(args, input, derive::EntityType::Relation) +pub fn timestamps(args: TokenStream, input: TokenStream) -> TokenStream { + timestamps::stamps_impl(args, input) } diff --git a/macros/src/node_relation.rs b/macros/src/node_relation.rs deleted file mode 100644 index 9a241e1..0000000 --- a/macros/src/node_relation.rs +++ /dev/null @@ -1,79 +0,0 @@ -use crate::{ - derive::{derive_serde, EntityType}, - stamps::Stamps, -}; -use proc_macro::TokenStream; -use quote::quote; -use syn::{parse_macro_input, DeriveInput, LitStr}; - -pub fn cypher_entity_impl(attr: TokenStream, input: TokenStream, typ: EntityType) -> TokenStream { - let orig_attr = match typ { - EntityType::Node => "node", - EntityType::Relation => "relation", - }; - let input = parse_macro_input!(input as DeriveInput); - for attr in input.attrs.iter() { - if attr.path().is_ident("stamps") { - panic!("Use {}(stamps = \"...\") instead.", orig_attr); - } else if attr.path().is_ident("name") { - panic!("Use {}(name = \"...\") instead.", orig_attr); - } - } - let args = parse_macro_input!(attr as EntityArgs); - let stamps = args.stamps.map(|s| s.into_attribute()).unwrap_or(quote!()); - let name = args - .name - .map(|name| quote!(#[name = #name])) - .unwrap_or(quote!()); - - let serde = derive_serde(); - let derive = match typ { - EntityType::Node => quote!(::cypher_dto::Node), - EntityType::Relation => quote!(::cypher_dto::Relation), - }; - quote! { - #stamps - #[derive(Clone, Debug, PartialEq, #derive, #serde)] - #name - #input - } - .into() -} -struct EntityArgs { - stamps: Option, - name: Option, -} -impl syn::parse::Parse for EntityArgs { - fn parse(input: syn::parse::ParseStream) -> syn::Result { - let mut stamps: Option = None; - let mut name: Option = None; - - while !input.is_empty() { - let lookahead = input.lookahead1(); - if lookahead.peek(syn::Ident) { - let ident: syn::Ident = input.parse()?; - if ident == "stamps" { - // Check if there's an equals sign after "stamps" - if input.peek(syn::Token![=]) { - let _ = input.parse::()?; - stamps = Some(input.parse()?); - } else { - // If no value is provided, use the default value for "stamps" - stamps = Some(Stamps::Full); - } - } else if ident == "name" { - let _ = input.parse::()?; - name = Some(input.parse::()?.value()); - } else { - return Err(lookahead.error()); - } - - // If there's a comma after this argument, parse it - let _ = input.parse::().ok(); - } else { - return Err(lookahead.error()); - } - } - Ok(EntityArgs { stamps, name }) - } -} diff --git a/macros/src/stamps.rs b/macros/src/timestamps.rs similarity index 80% rename from macros/src/stamps.rs rename to macros/src/timestamps.rs index fe94b2d..ba1a8bc 100644 --- a/macros/src/stamps.rs +++ b/macros/src/timestamps.rs @@ -1,7 +1,7 @@ use std::error::Error; use proc_macro::TokenStream; -use quote::{__private::TokenStream as TokenStream2, format_ident, quote}; +use quote::{format_ident, quote}; use syn::{ parse::{Parse, ParseStream}, parse_macro_input, parse_quote, DeriveInput, Fields, Ident, LitStr, Result as SynResult, Type, @@ -14,7 +14,7 @@ pub fn stamps_impl(args: TokenStream, input: TokenStream) -> TokenStream { let name = &input.ident; let data = match &input.data { syn::Data::Struct(s) => &s.fields, - _ => panic!("#[stamps] can only be used with structs"), + _ => panic!("#[timestamps] can only be used with structs"), }; let input_attrs = input.attrs; let input_vis = input.vis; @@ -30,7 +30,7 @@ pub fn stamps_impl(args: TokenStream, input: TokenStream) -> TokenStream { #vis #name: #ty, } }), - _ => panic!("stamps can only be used on structs with named fields"), + _ => panic!("#[timestamps] can only be used on structs with named fields"), }; let (stamp_idents, stamp_types) = stamps.into_fields(); @@ -74,7 +74,7 @@ impl TryFrom<&str> for Stamps { "updated_at" => Ok(Stamps::UpdatedAt), "full" => Ok(Stamps::Full), "short" => Ok(Stamps::Short), - _ => Err(format!("Invalid `#[stamps]` argument: \"{}\". Allowed: full, short, created, updated, created_at, updated_at.", s).into()), + _ => Err(format!("Invalid `#[timestamps]` argument: \"{}\". Allowed: full, short, created, updated, created_at, updated_at.", s).into()), } } } @@ -112,20 +112,4 @@ impl Stamps { (idents, types) } - - pub fn as_str(&self) -> &'static str { - match self { - Stamps::Created => "created", - Stamps::Updated => "updated", - Stamps::CreatedAt => "created_at", - Stamps::UpdatedAt => "updated_at", - Stamps::Full => "full", - Stamps::Short => "short", - } - } - - pub fn into_attribute(self) -> TokenStream2 { - let s = self.as_str(); - quote!(#[stamps(#s)]) - } } diff --git a/reset-test-database.sh b/reset-test-database.sh new file mode 100755 index 0000000..21896c9 --- /dev/null +++ b/reset-test-database.sh @@ -0,0 +1,4 @@ +curl -H accept:application/json -H content-type:application/json \ + "http://$NEO4J_TEST_USER:$NEO4J_TEST_PASS@db:7474/db/neo4j/tx/commit" \ + -d '{"statements": [{"statement": "MATCH (n) DETACH DELETE n;"}]}' +echo diff --git a/test-fast.sh b/test-fast.sh deleted file mode 100755 index d9f8d41..0000000 --- a/test-fast.sh +++ /dev/null @@ -1,5 +0,0 @@ -# Test on a running Neo4j instance instead of spinning up a container for each test. -export NEO4J_TEST_URI="bolt://db:7687" -export NEO4J_TEST_USER="neo4j" -export NEO4J_TEST_PASS="developer" -cargo test diff --git a/test-isolated.sh b/test-isolated.sh new file mode 100755 index 0000000..9133f10 --- /dev/null +++ b/test-isolated.sh @@ -0,0 +1,4 @@ +unset NEO4J_TEST_URI +unset NEO4J_TEST_USER +unset NEO4J_TEST_PASS +./test.sh diff --git a/test.sh b/test.sh new file mode 100755 index 0000000..51cd30a --- /dev/null +++ b/test.sh @@ -0,0 +1,2 @@ +cargo test --doc --no-fail-fast +cargo nextest run