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..dbc5314 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -3,16 +3,13 @@ "dockerComposeFile": "./docker-compose.yml", "service": "app", "workspaceFolder": "/cypher-dto", + "remoteUser": "developer", "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/Cargo.lock b/Cargo.lock index 8ad82c1..c56e318 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" @@ -186,11 +210,11 @@ dependencies = [ "chrono", "cypher-dto-macros", "lenient_semver", - "neo4j_testcontainers", "neo4rs", "pretty_env_logger", "serde", "testcontainers", + "testcontainers-modules", "thiserror", "tokio", "uuid", @@ -203,7 +227,7 @@ 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..2f2f321 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -5,8 +5,9 @@ version = "0.2.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" +chrono = { version = "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" } +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..db194bd 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, 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..af5b999 --- /dev/null +++ b/lib/include/create_query.rs @@ -0,0 +1,18 @@ +{ + #[derive(Node)] + struct Person { + name: String, + } + + // Build a query string: + let query = format!("CREATE (n:{})", Person::as_query_obj()); + assert_eq!(query, "CREATE (n:Person { name: $name })"); + + // Use it in a [neo4rs::Query]: + let mut query = neo4rs::Query::new(query); + let person = Person::new("Alice"); + person.add_values_to_params(query); + + // A shorter way to do the same thing: + let query = person.create(); +} 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..ed1c462 --- /dev/null +++ b/lib/include/read_query.rs @@ -0,0 +1,19 @@ +{ + #[derive(Node)] + struct Person { + id: String, + name: String, + } + + // Query string: + let query = format!("MATCH (n:{}) RETURN n", PersonId::as_query_obj()); + assert_eq!(query, "MATCH (n:Person { id: $id }) RETURN n"); + + // Using [neo4rs::Query] manually: + let mut query = neo4rs::Query::new(query); + let id = PersonId::new("1234"); + id.add_values_to_params(query); + + // A shorter way to do the same thing: + let query = id.read(); +} 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..9a95ac5 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()`), @@ -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..0273245 100644 --- a/macros/Cargo.toml +++ b/macros/Cargo.toml @@ -5,7 +5,7 @@ version = "0.2.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/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..ff92161 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -1,22 +1,21 @@ 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. #[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. #[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() } @@ -28,22 +27,6 @@ pub fn derive_cypher_relation(input: TokenStream) -> TokenStream { /// 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