From f641a6fd66921a438c49c695e74f84bb0a7654d2 Mon Sep 17 00:00:00 2001 From: Clay McLeod Date: Thu, 12 Oct 2023 16:53:16 -0500 Subject: [PATCH 1/5] feat: adds Rust tools to prepare final YAML specification --- .github/workflows/specification-tool.yml | 52 + packages/.vscode/settings.json | 4 + packages/Cargo.lock | 1832 +++++++++++++++++ packages/Cargo.toml | 28 + packages/ccdi-cde/Cargo.toml | 11 + packages/ccdi-cde/src/lib.rs | 28 + packages/ccdi-cde/src/v1.rs | 9 + packages/ccdi-cde/src/v1/identifier.rs | 186 ++ packages/ccdi-cde/src/v1/race.rs | 131 ++ packages/ccdi-cde/src/v1/sex.rs | 92 + packages/ccdi-cde/src/v2.rs | 5 + packages/ccdi-cde/src/v2/ethnicity.rs | 95 + packages/ccdi-models/Cargo.toml | 15 + packages/ccdi-models/src/count.rs | 5 + packages/ccdi-models/src/count/total.rs | 19 + packages/ccdi-models/src/lib.rs | 16 + packages/ccdi-models/src/metadata.rs | 4 + packages/ccdi-models/src/metadata/field.rs | 119 ++ .../src/metadata/field/description.rs | 77 + .../metadata/field/description/harmonized.rs | 53 + .../field/description/unharmonized.rs | 186 ++ .../ccdi-models/src/metadata/field/owned.rs | 189 ++ .../ccdi-models/src/metadata/field/unowned.rs | 172 ++ packages/ccdi-models/src/metadata/fields.rs | 53 + packages/ccdi-models/src/subject.rs | 220 ++ packages/ccdi-models/src/subject/kind.rs | 37 + packages/ccdi-models/src/subject/metadata.rs | 188 ++ .../src/subject/metadata/builder.rs | 155 ++ packages/ccdi-openapi/Cargo.toml | 11 + packages/ccdi-openapi/src/api.rs | 112 + packages/ccdi-openapi/src/lib.rs | 12 + packages/ccdi-server/Cargo.toml | 16 + packages/ccdi-server/src/lib.rs | 12 + packages/ccdi-server/src/responses.rs | 10 + packages/ccdi-server/src/responses/by.rs | 3 + .../ccdi-server/src/responses/by/count.rs | 51 + packages/ccdi-server/src/responses/error.rs | 35 + .../ccdi-server/src/responses/metadata.rs | 24 + packages/ccdi-server/src/responses/subject.rs | 36 + packages/ccdi-server/src/routes.rs | 4 + packages/ccdi-server/src/routes/metadata.rs | 39 + packages/ccdi-server/src/routes/subject.rs | 206 ++ packages/ccdi-spec/Cargo.lock | 195 ++ packages/ccdi-spec/Cargo.toml | 15 + packages/ccdi-spec/src/main.rs | 148 ++ 45 files changed, 4910 insertions(+) create mode 100644 .github/workflows/specification-tool.yml create mode 100644 packages/.vscode/settings.json create mode 100644 packages/Cargo.lock create mode 100644 packages/Cargo.toml create mode 100644 packages/ccdi-cde/Cargo.toml create mode 100644 packages/ccdi-cde/src/lib.rs create mode 100644 packages/ccdi-cde/src/v1.rs create mode 100644 packages/ccdi-cde/src/v1/identifier.rs create mode 100644 packages/ccdi-cde/src/v1/race.rs create mode 100644 packages/ccdi-cde/src/v1/sex.rs create mode 100644 packages/ccdi-cde/src/v2.rs create mode 100644 packages/ccdi-cde/src/v2/ethnicity.rs create mode 100644 packages/ccdi-models/Cargo.toml create mode 100644 packages/ccdi-models/src/count.rs create mode 100644 packages/ccdi-models/src/count/total.rs create mode 100644 packages/ccdi-models/src/lib.rs create mode 100644 packages/ccdi-models/src/metadata.rs create mode 100644 packages/ccdi-models/src/metadata/field.rs create mode 100644 packages/ccdi-models/src/metadata/field/description.rs create mode 100644 packages/ccdi-models/src/metadata/field/description/harmonized.rs create mode 100644 packages/ccdi-models/src/metadata/field/description/unharmonized.rs create mode 100644 packages/ccdi-models/src/metadata/field/owned.rs create mode 100644 packages/ccdi-models/src/metadata/field/unowned.rs create mode 100644 packages/ccdi-models/src/metadata/fields.rs create mode 100644 packages/ccdi-models/src/subject.rs create mode 100644 packages/ccdi-models/src/subject/kind.rs create mode 100644 packages/ccdi-models/src/subject/metadata.rs create mode 100644 packages/ccdi-models/src/subject/metadata/builder.rs create mode 100644 packages/ccdi-openapi/Cargo.toml create mode 100644 packages/ccdi-openapi/src/api.rs create mode 100644 packages/ccdi-openapi/src/lib.rs create mode 100644 packages/ccdi-server/Cargo.toml create mode 100644 packages/ccdi-server/src/lib.rs create mode 100644 packages/ccdi-server/src/responses.rs create mode 100644 packages/ccdi-server/src/responses/by.rs create mode 100644 packages/ccdi-server/src/responses/by/count.rs create mode 100644 packages/ccdi-server/src/responses/error.rs create mode 100644 packages/ccdi-server/src/responses/metadata.rs create mode 100644 packages/ccdi-server/src/responses/subject.rs create mode 100644 packages/ccdi-server/src/routes.rs create mode 100644 packages/ccdi-server/src/routes/metadata.rs create mode 100644 packages/ccdi-server/src/routes/subject.rs create mode 100644 packages/ccdi-spec/Cargo.lock create mode 100644 packages/ccdi-spec/Cargo.toml create mode 100644 packages/ccdi-spec/src/main.rs diff --git a/.github/workflows/specification-tool.yml b/.github/workflows/specification-tool.yml new file mode 100644 index 0000000..2569177 --- /dev/null +++ b/.github/workflows/specification-tool.yml @@ -0,0 +1,52 @@ +name: Specification Tool + +on: + push: + branches: + - main + paths: + - 'packages/**' + pull_request: + paths: + - 'packages/**' + +defaults: + run: + working-directory: packages/ + +jobs: + format: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + - name: Update Rust + run: rustup update nightly && rustup default nightly + - name: Install rustfmt + run: rustup component add rustfmt + - run: cargo fmt -- --check + + lint: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + - name: Update Rust + run: rustup update nightly && rustup default nightly + - name: Install clippy + run: rustup component add clippy + - run: cargo clippy --all-features -- --deny warnings + + test: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + - name: Update Rust + run: rustup update nightly && rustup default nightly + - run: cargo test --all-features + + docs: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + - name: Update Rust + run: rustup update nightly && rustup default nightly + - run: cargo doc \ No newline at end of file diff --git a/packages/.vscode/settings.json b/packages/.vscode/settings.json new file mode 100644 index 0000000..1570300 --- /dev/null +++ b/packages/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "editor.formatOnSave": true, + "rust-analyzer.check.command": "clippy" +} \ No newline at end of file diff --git a/packages/Cargo.lock b/packages/Cargo.lock new file mode 100644 index 0000000..17cccb0 --- /dev/null +++ b/packages/Cargo.lock @@ -0,0 +1,1832 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "actix-codec" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617a8268e3537fe1d8c9ead925fca49ef6400927ee7bc26750e90ecee14ce4b8" +dependencies = [ + "bitflags 1.3.2", + "bytes", + "futures-core", + "futures-sink", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "actix-http" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92ef85799cba03f76e4f7c10f533e66d87c9a7e7055f3391f09000ad8351bc9" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-utils", + "ahash", + "base64", + "bitflags 2.4.0", + "brotli", + "bytes", + "bytestring", + "derive_more", + "encoding_rs", + "flate2", + "futures-core", + "h2", + "http", + "httparse", + "httpdate", + "itoa", + "language-tags", + "local-channel", + "mime", + "percent-encoding", + "pin-project-lite", + "rand", + "sha1", + "smallvec", + "tokio", + "tokio-util", + "tracing", + "zstd", +] + +[[package]] +name = "actix-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" +dependencies = [ + "quote", + "syn 2.0.38", +] + +[[package]] +name = "actix-router" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66ff4d247d2b160861fa2866457e85706833527840e4133f8f49aa423a38799" +dependencies = [ + "bytestring", + "http", + "regex", + "serde", + "tracing", +] + +[[package]] +name = "actix-rt" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28f32d40287d3f402ae0028a9d54bef51af15c8769492826a69d28f81893151d" +dependencies = [ + "futures-core", + "tokio", +] + +[[package]] +name = "actix-server" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eb13e7eef0423ea6eab0e59f6c72e7cb46d33691ad56a726b3cd07ddec2c2d4" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "futures-util", + "mio", + "socket2", + "tokio", + "tracing", +] + +[[package]] +name = "actix-service" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a" +dependencies = [ + "futures-core", + "paste", + "pin-project-lite", +] + +[[package]] +name = "actix-utils" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" +dependencies = [ + "local-waker", + "pin-project-lite", +] + +[[package]] +name = "actix-web" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4a5b5e29603ca8c94a77c65cf874718ceb60292c5a5c3e5f4ace041af462b9" +dependencies = [ + "actix-codec", + "actix-http", + "actix-macros", + "actix-router", + "actix-rt", + "actix-server", + "actix-service", + "actix-utils", + "actix-web-codegen", + "ahash", + "bytes", + "bytestring", + "cfg-if", + "cookie", + "derive_more", + "encoding_rs", + "futures-core", + "futures-util", + "itoa", + "language-tags", + "log", + "mime", + "once_cell", + "pin-project-lite", + "regex", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "socket2", + "time", + "url", +] + +[[package]] +name = "actix-web-codegen" +version = "4.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb1f50ebbb30eca122b188319a4398b3f7bb4a8cdf50ecfb73bfc6a3c3ce54f5" +dependencies = [ + "actix-router", + "proc-macro2", + "quote", + "syn 2.0.38", +] + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "anstream" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" + +[[package]] +name = "anstyle-parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "brotli" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516074a47ef4bce09577a3b379392300159ce5b1ba2e501ff1c819950066100f" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da74e2b81409b1b743f8f0c62cc6254afefb8b8e50bbfe3735550f7aeefa3448" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" + +[[package]] +name = "bytestring" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "238e4886760d98c4f899360c834fa93e62cf7f721ac3c2da375cbdf4b8679aae" +dependencies = [ + "bytes", +] + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "jobserver", + "libc", +] + +[[package]] +name = "ccdi-cde" +version = "0.1.0" +dependencies = [ + "rand", + "serde", + "serde_json", + "utoipa", +] + +[[package]] +name = "ccdi-models" +version = "0.1.0" +dependencies = [ + "ccdi-cde", + "indexmap 2.0.2", + "macropol", + "rand", + "serde", + "serde_json", + "serde_test", + "utoipa", +] + +[[package]] +name = "ccdi-openapi" +version = "0.1.0" +dependencies = [ + "ccdi-cde", + "ccdi-models", + "ccdi-server", + "utoipa", +] + +[[package]] +name = "ccdi-server" +version = "0.1.0" +dependencies = [ + "actix-web", + "ccdi-cde", + "ccdi-models", + "indexmap 2.0.2", + "mime", + "rand", + "serde", + "serde_json", + "utoipa", +] + +[[package]] +name = "ccdi-spec" +version = "0.1.0" +dependencies = [ + "actix-web", + "ccdi-openapi", + "ccdi-server", + "clap", + "env_logger", + "log", + "utoipa", + "utoipa-swagger-ui", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "4.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d04704f56c2cde07f43e8e2c154b43f216dc5c92fc98ada720177362f953b956" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e231faeaca65ebd1ea3c737966bf858971cd38c3849107aa3ea7de90a804e45" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.38", +] + +[[package]] +name = "clap_lex" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cookie" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "cpufeatures" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" +dependencies = [ + "darling_core", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "deranged" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 1.0.109", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "encoding_rs" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_logger" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "flate2" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-core" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" + +[[package]] +name = "futures-sink" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" + +[[package]] +name = "futures-task" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" + +[[package]] +name = "futures-util" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" + +[[package]] +name = "h2" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap 1.9.3", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" + +[[package]] +name = "http" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" +dependencies = [ + "equivalent", + "hashbrown 0.14.1", + "serde", +] + +[[package]] +name = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi", + "rustix", + "windows-sys", +] + +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "jobserver" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d" +dependencies = [ + "libc", +] + +[[package]] +name = "language-tags" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" + +[[package]] +name = "libc" +version = "0.2.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" + +[[package]] +name = "linux-raw-sys" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f" + +[[package]] +name = "local-channel" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a493488de5f18c8ffcba89eebb8532ffc562dc400490eb65b84893fae0b178" +dependencies = [ + "futures-core", + "futures-sink", + "local-waker", +] + +[[package]] +name = "local-waker" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34f76eb3611940e0e7d53a9aaa4e6a3151f69541a282fd0dad5571420c53ff1" + +[[package]] +name = "lock_api" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "macropol" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a037b9563fd07a0cf51e56bc79151a4c29a9a2f08e7006afcb79cf3e8653709" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "memchr" +version = "2.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys", +] + +[[package]] +name = "object" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.3.5", + "smallvec", + "windows-targets", +] + +[[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 = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom", + "redox_syscall 0.2.16", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d119d7c7ca818f8a53c300863d4f87566aac09943aef5b355bb83969dae75d87" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465c6fc0621e4abc4187a2bda0937bfd4f722c2730b29562e19689ea796c9a4b" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56d84fdd47036b038fc80dd333d10b6aab10d5d31f4a366e20014def75328d33" + +[[package]] +name = "rust-embed" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e7d90385b59f0a6bf3d3b757f3ca4ece2048265d70db20a2016043d4509a40" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3d8c6fd84090ae348e63a84336b112b5c3918b3bf0493a581f7bd8ee623c29" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "shellexpand", + "syn 2.0.38", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "873feff8cb7bf86fdf0a71bb21c95159f4e4a37dd7a4bd1855a940909b583ada" +dependencies = [ + "sha2", + "walkdir", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "745ecfa778e66b2b63c88a61cb36e0eea109e803b0b86bf9879fbc77c70e86ed" +dependencies = [ + "bitflags 2.4.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" + +[[package]] +name = "serde" +version = "1.0.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e422a44e74ad4001bdc8eede9a4570ab52f71190e9c076d14369f38b9200537" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e48d1f918009ce3145511378cf68d613e3b3d9137d67272562080d68a2b32d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.38", +] + +[[package]] +name = "serde_json" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" +dependencies = [ + "indexmap 2.0.2", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_test" +version = "1.0.176" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a2f49ace1498612d14f7e0b8245519584db8299541dfe31a06374a828d620ab" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a49e178e4452f45cb61d0cd8cebc1b0fafd3e41929e996cef79aa3aca91f574" +dependencies = [ + "indexmap 2.0.2", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shellexpand" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ccc8076840c4da029af4f87e4e8daeb0fca6b87bbb02e10cb60b791450e11e4" +dependencies = [ + "dirs", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" + +[[package]] +name = "socket2" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6093bad37da69aab9d123a8091e4be0aa4a03e4d601ec641c327398315f62b64" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.38", +] + +[[package]] +name = "time" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "426f806f4089c493dcac0d24c29c01e2c38baf8e30f1b716ee37e83d200b18fe" +dependencies = [ + "deranged", + "itoa", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" +dependencies = [ + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f38200e3ef7995e5ef13baec2f432a6da0aa9ac495b2c0e8f3b7eec2c92d653" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "windows-sys", +] + +[[package]] +name = "tokio-util" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unsafe-libyaml" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa" + +[[package]] +name = "url" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "utoipa" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b208a50ff438dcdc887ea3f2db59530bd2f4bc3d2c70630e4d7ee7a281a1d1b" +dependencies = [ + "indexmap 2.0.2", + "serde", + "serde_json", + "serde_yaml", + "utoipa-gen", +] + +[[package]] +name = "utoipa-gen" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bd516d8879043e081537690bc96c8f17b5a4602c336aecb8f1de89d9d9c7e72" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.38", +] + +[[package]] +name = "utoipa-swagger-ui" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "154517adf0d0b6e22e8e1f385628f14fcaa3db43531dc74303d3edef89d6dfe5" +dependencies = [ + "actix-web", + "mime_guess", + "regex", + "rust-embed", + "serde", + "serde_json", + "utoipa", + "zip", +] + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "walkdir" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +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", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "byteorder", + "crc32fast", + "crossbeam-utils", + "flate2", +] + +[[package]] +name = "zstd" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a27595e173641171fc74a1232b7b1c7a7cb6e18222c11e9dfb9888fa424c53c" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "6.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee98ffd0b48ee95e6c5168188e44a54550b1564d9d530ee21d5f0eaed1069581" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.9+zstd.1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e16efa8a874a0481a574084d34cc26fdb3b99627480f785888deb6386506656" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/packages/Cargo.toml b/packages/Cargo.toml new file mode 100644 index 0000000..11f286b --- /dev/null +++ b/packages/Cargo.toml @@ -0,0 +1,28 @@ +[workspace] +members = [ + "ccdi-cde", + "ccdi-models", + "ccdi-openapi", + "ccdi-server", + "ccdi-spec", +] +resolver = "2" + +[workspace.package] +license = "MIT OR Apache-2.0" +edition = "2021" + +[workspace.dependencies] +actix-web = "4.4.0" +indexmap = "2.0.2" +mime = "0.3.17" +rand = "0.8.5" +serde = { version = "1.0.189", features = ["serde_derive"] } +serde_json = { version = "1.0.107", features = ["preserve_order"] } +serde_test = "1.0.176" +utoipa = { version = "4.0.0", features = [ + "preserve_order", + "preserve_path_order", + "yaml", +] } +utoipa-swagger-ui = { version = "4.0.0", features = ["actix-web"] } diff --git a/packages/ccdi-cde/Cargo.toml b/packages/ccdi-cde/Cargo.toml new file mode 100644 index 0000000..b77f8c5 --- /dev/null +++ b/packages/ccdi-cde/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "ccdi-cde" +version = "0.1.0" +license.workspace = true +edition.workspace = true + +[dependencies] +rand.workspace = true +serde.workspace = true +serde_json.workspace = true +utoipa.workspace = true diff --git a/packages/ccdi-cde/src/lib.rs b/packages/ccdi-cde/src/lib.rs new file mode 100644 index 0000000..3437d91 --- /dev/null +++ b/packages/ccdi-cde/src/lib.rs @@ -0,0 +1,28 @@ +//! A crate containing information on the common data elements (CDEs) defined +//! within the Cancer Data Standards Registry and Repository (caDSR) that are +//! used in the Childhood Cancer Data Initiative's federated API. + +#![warn(missing_docs)] +#![warn(rust_2018_idioms)] +#![warn(rust_2021_compatibility)] +#![warn(missing_debug_implementations)] +#![deny(rustdoc::broken_intra_doc_links)] + +pub mod v1; +pub mod v2; + +/// A trait to define the harmonization standard used for a common data element. +pub trait Standard { + /// Gets the harmonization standard name for a common data element. + fn standard() -> &'static str; +} + +/// A trait to define the URL where users can learn more about a common data +/// element. +pub trait Url { + /// Gets the URL to learn more about a common data element. + fn url() -> &'static str; +} + +/// A marker trait for common data elements (CDEs). +pub trait CDE: std::fmt::Display + Eq + PartialEq + Standard + Url {} diff --git a/packages/ccdi-cde/src/v1.rs b/packages/ccdi-cde/src/v1.rs new file mode 100644 index 0000000..d7ee22a --- /dev/null +++ b/packages/ccdi-cde/src/v1.rs @@ -0,0 +1,9 @@ +//! Common data elements that have a major version of one. + +mod identifier; +mod race; +mod sex; + +pub use identifier::Identifier; +pub use race::Race; +pub use sex::Sex; diff --git a/packages/ccdi-cde/src/v1/identifier.rs b/packages/ccdi-cde/src/v1/identifier.rs new file mode 100644 index 0000000..252d3df --- /dev/null +++ b/packages/ccdi-cde/src/v1/identifier.rs @@ -0,0 +1,186 @@ +use serde::Deserialize; +use serde::Serialize; +use utoipa::ToSchema; + +use crate::CDE; + +#[derive(Debug)] +pub enum ParseError { + /// An invalid format was encountered. + InvalidFormat(String), +} + +impl std::fmt::Display for ParseError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ParseError::InvalidFormat(value) => write!(f, "invalid format: {value}"), + } + } +} + +impl std::error::Error for ParseError {} + +#[derive(Debug)] +pub enum Error { + /// A parse error. + ParseError(ParseError), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::ParseError(err) => write!(f, "parse error: {err}"), + } + } +} + +impl std::error::Error for Error {} + +/// **caDSR CDE 6380049 v1.00** +/// +/// This metadata element is defined by the caDSR as "A unique subject +/// identifier within a site and a study.". No permissible values are defined +/// for this CDE. +/// +/// Link: +/// +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)] +#[schema(as = cde::v1::Identifier)] +pub struct Identifier +where + Self: CDE, +{ + /// The namespace of the identifier. + #[schema(example = "organization")] + namespace: String, + + /// The name of the identifier. + #[schema(example = "SubjectName001")] + name: String, +} + +impl Identifier { + /// Creates a new [`Identifier`]. + /// + /// # Examples + /// + /// ``` + /// use ccdi_cde as cde; + /// use cde::v1::Identifier; + /// + /// let identifier = Identifier::new("organization", "Name"); + /// assert_eq!(identifier.namespace(), &String::from("organization")); + /// assert_eq!(identifier.name(), &String::from("Name")); + /// ``` + pub fn new>(namespace: S, name: S) -> Self { + Self { + namespace: namespace.into(), + name: name.into(), + } + } + + /// Gets the namespace for the [`Identifier`] by reference. + /// + /// # Examples + /// + /// ``` + /// use ccdi_cde as cde; + /// use cde::v1::Identifier; + /// + /// let identifier = Identifier::parse("organization:Name", ":")?; + /// assert_eq!(identifier.namespace(), &String::from("organization")); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn namespace(&self) -> &String { + &self.namespace + } + + /// Gets the name for the [`Identifier`] by reference. + /// + /// # Examples + /// + /// ``` + /// use ccdi_cde as cde; + /// use cde::v1::Identifier; + /// + /// let identifier = Identifier::parse("organization:Name", ":")?; + /// assert_eq!(identifier.name(), &String::from("Name")); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn name(&self) -> &String { + &self.name + } + + /// Parses an [`Identifier`] from a [`&str`](str) using the provided + /// delimiter. + /// + /// # Examples + /// + /// ``` + /// use ccdi_cde as cde; + /// use cde::v1::Identifier; + /// + /// let identifier = Identifier::parse("organization:Name", ":")?; + /// assert_eq!(identifier.namespace(), &String::from("organization")); + /// assert_eq!(identifier.name(), &String::from("Name")); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn parse(s: &str, separator: &str) -> Result { + let parts = s.split(separator).collect::>(); + + if parts.len() != 2 { + return Err(Error::ParseError(ParseError::InvalidFormat(s.to_owned()))); + } + + let mut parts = parts.iter(); + + // SAFETY: we just checked that two parts must exist. Since we have not + // consumed any items from the iterator, these two unwraps will always + // succeed. + let namespace = parts.next().unwrap().to_string(); + let name = parts.next().unwrap().to_string(); + + Ok(Self { namespace, name }) + } +} + +impl CDE for Identifier {} + +impl crate::Standard for Identifier { + fn standard() -> &'static str { + "caDSR CDE 6380049 v1.00" + } +} + +impl crate::Url for Identifier { + fn url() -> &'static str { + "https://cadsr.cancer.gov/onedata/dmdirect/NIH/NCI/CO/CDEDD?filter=CDEDD.ITEM_ID=6380049%20and%20ver_nr=1" + } +} + +impl std::fmt::Display for Identifier { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{{ namespace: {}, name: {} }}", + self.namespace, self.name + ) + } +} + +#[cfg(test)] +mod tests { + use crate::v1::Identifier; + + #[test] + fn it_displays_correctly() { + let identifier = Identifier::new("organization", "Name"); + assert_eq!( + identifier.to_string(), + "{ namespace: organization, name: Name }" + ); + } +} diff --git a/packages/ccdi-cde/src/v1/race.rs b/packages/ccdi-cde/src/v1/race.rs new file mode 100644 index 0000000..8d4e226 --- /dev/null +++ b/packages/ccdi-cde/src/v1/race.rs @@ -0,0 +1,131 @@ +use rand::distributions::Standard; +use rand::prelude::Distribution; +use serde::Deserialize; +use serde::Serialize; +use utoipa::ToSchema; + +use crate::CDE; + +/// **caDSR CDE 2192199 v1.00** +/// +/// This metadata element is defined by the caDSR as "The text for reporting +/// information about race based on the Office of Management and Budget (OMB) +/// categories.". Upon examination of the large number of projects using the +/// term, it appears to be the preferred term for the general concept of race. +/// +/// Link: +/// +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)] +#[schema(as = cde::v1::Race)] +pub enum Race +where + Self: CDE, +{ + /// Not Allowed To Collect + /// + /// An indicator that specifies that a collection event was not permitted. + #[serde(rename = "Not allowed to collect")] + NotAllowedToCollect, + + /// Native Hawaiian or Other Pacific Islander + /// + /// Denotes a person having origins in any of the original peoples of + /// Hawaii, Guam, Samoa, or other Pacific Islands. The term covers + /// particularly people who identify themselves as part-Hawaiian, Native + /// Hawaiian, Guamanian or Chamorro, Carolinian, Samoan, Chuukese (Trukese), + /// Fijian, Kosraean, Melanesian, Micronesian, Northern Mariana Islander, + /// Palauan, Papua New Guinean, Pohnpeian, Polynesian, Solomon Islander, + /// Tahitian, Tokelauan, Tongan, Yapese, or Pacific Islander, not specified. + #[serde(rename = "Native Hawaiian or other Pacific Islander")] + NativeHawaiianOrOtherPacificIslander, + + /// Not Reported + /// + /// Not provided or available. + #[serde(rename = "Not Reported")] + NotReported, + + /// Unknown + /// + /// Not known, not observed, not recorded, or refused. + #[serde(rename = "Unknown")] + Unknown, + + /// American Indian or Alaska Native + /// + /// A person having origins in any of the original peoples of North and + /// South America (including Central America) and who maintains tribal + /// affiliation or community attachment. (OMB) + #[serde(rename = "American Indian or Alaska Native")] + AmericanIndianOrAlaskaNative, + + /// Asian + /// + /// A person having origins in any of the original peoples of the Far East, + /// Southeast Asia, or the Indian subcontinent, including for example, + /// Cambodia, China, India, Japan, Korea, Malaysia, Pakistan, the Philippine + /// Islands, Thailand, and Vietnam. (OMB) + #[serde(rename = "Asian")] + Asian, + + /// Black or African American + /// + /// A person having origins in any of the Black racial groups of Africa. + /// Terms such as "Haitian" or "Negro" can be used in addition to "Black or + /// African American". (OMB) + #[serde(rename = "Black or African American")] + BlackOrAfricanAmerican, + + /// White + /// + /// Denotes person with European, Middle Eastern, or North African ancestral + /// origin who identifies, or is identified, as White. + #[serde(rename = "White")] + White, +} + +impl CDE for Race {} + +impl crate::Standard for Race { + fn standard() -> &'static str { + "caDSR CDE 2192199 v1.00" + } +} + +impl crate::Url for Race { + fn url() -> &'static str { + "https://cadsr.cancer.gov/onedata/dmdirect/NIH/NCI/CO/CDEDD?filter=CDEDD.ITEM_ID=2192199%20and%20ver_nr=1" + } +} + +impl std::fmt::Display for Race { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Race::NotAllowedToCollect => write!(f, "Not allowed to collect"), + Race::NativeHawaiianOrOtherPacificIslander => { + write!(f, "Native Hawaiian or other Pacific Islander") + } + Race::NotReported => write!(f, "Not Reported"), + Race::Unknown => write!(f, "Unknown"), + Race::AmericanIndianOrAlaskaNative => write!(f, "American Indian or Alaska Native"), + Race::Asian => write!(f, "Asian"), + Race::BlackOrAfricanAmerican => write!(f, "Black or African American"), + Race::White => write!(f, "White"), + } + } +} + +impl Distribution for Standard { + fn sample(&self, rng: &mut R) -> Race { + match rng.gen_range(0..=7) { + 0 => Race::NotAllowedToCollect, + 1 => Race::NativeHawaiianOrOtherPacificIslander, + 2 => Race::NotReported, + 3 => Race::Unknown, + 4 => Race::AmericanIndianOrAlaskaNative, + 5 => Race::Asian, + 6 => Race::BlackOrAfricanAmerican, + _ => Race::White, + } + } +} diff --git a/packages/ccdi-cde/src/v1/sex.rs b/packages/ccdi-cde/src/v1/sex.rs new file mode 100644 index 0000000..c39734c --- /dev/null +++ b/packages/ccdi-cde/src/v1/sex.rs @@ -0,0 +1,92 @@ +use rand::distributions::Standard; +use rand::prelude::Distribution; +use serde::Deserialize; +use serde::Serialize; +use utoipa::ToSchema; + +use crate::CDE; + +/// **caDSR CDE 6343385 v1.00** +/// +/// This metadata element is defined by the caDSR as "Sex of the subject as +/// determined by the investigator." In particular, this field does not dictate +/// the time period: whether it represents sex at birth, sex at sample +/// collection, or any other determined time point. Further, the descriptions +/// for F and M suggest that this term can represent either biological sex, +/// culture gender roles, or both. Thus, this field cannot be assumed to +/// strictly represent biological sex. +/// +/// Link: +/// +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)] +#[schema(as = cde::v1::Sex)] +pub enum Sex +where + Self: CDE, +{ + /// Unknown + /// + /// Not known, not observed, not recorded, or refused. + #[serde(rename = "U")] + Unknown, + + /// Female + /// + /// A person who belongs to the sex that normally produces ova. The term is + /// used to indicate biological sex distinctions, or cultural gender role + /// distinctions, or both. + #[serde(rename = "F")] + Female, + + /// Male + /// + /// A person who belongs to the sex that normally produces sperm. The term + /// is used to indicate biological sex distinctions, cultural gender role + /// distinctions, or both. + #[serde(rename = "M")] + Male, + + /// Intersex + /// + /// A person (one of unisexual specimens) who is born with genitalia and/or + /// secondary sexual characteristics of indeterminate sex, or which combine + /// features of both sexes. + #[serde(rename = "UNDIFFERENTIATED")] + Undifferentiated, +} + +impl CDE for Sex {} + +impl crate::Standard for Sex { + fn standard() -> &'static str { + "caDSR CDE 6343385 v1.00" + } +} + +impl crate::Url for Sex { + fn url() -> &'static str { + "https://cadsr.cancer.gov/onedata/dmdirect/NIH/NCI/CO/CDEDD?filter=CDEDD.ITEM_ID=6343385%20and%20ver_nr=1" + } +} + +impl std::fmt::Display for Sex { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Sex::Unknown => write!(f, "U"), + Sex::Female => write!(f, "F"), + Sex::Male => write!(f, "M"), + Sex::Undifferentiated => write!(f, "UNDIFFERENTIATED"), + } + } +} + +impl Distribution for Standard { + fn sample(&self, rng: &mut R) -> Sex { + match rng.gen_range(0..=3) { + 0 => Sex::Unknown, + 1 => Sex::Female, + 2 => Sex::Male, + _ => Sex::Undifferentiated, + } + } +} diff --git a/packages/ccdi-cde/src/v2.rs b/packages/ccdi-cde/src/v2.rs new file mode 100644 index 0000000..d0a39a3 --- /dev/null +++ b/packages/ccdi-cde/src/v2.rs @@ -0,0 +1,5 @@ +//! Common data elements that have a major version of one. + +mod ethnicity; + +pub use ethnicity::Ethnicity; diff --git a/packages/ccdi-cde/src/v2/ethnicity.rs b/packages/ccdi-cde/src/v2/ethnicity.rs new file mode 100644 index 0000000..c0e25f7 --- /dev/null +++ b/packages/ccdi-cde/src/v2/ethnicity.rs @@ -0,0 +1,95 @@ +use rand::distributions::Standard; +use rand::prelude::Distribution; +use serde::Deserialize; +use serde::Serialize; +use utoipa::ToSchema; + +use crate::CDE; + +/// **caDSR CDE 2192217 v2.00** +/// +/// This metadata element is defined by the caDSR as "The text for reporting +/// information about ethnicity based on the Office of Management and Budget +/// (OMB) categories." Upon examination of the large number of projects using +/// the term, it appears to be the preferred term for the general concept of +/// ethnicity. +/// +/// Link: +/// +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)] +#[schema(as = cde::v2::Ethnicity)] +pub enum Ethnicity +where + Self: CDE, +{ + /// Not Allowed To Collect + /// + /// An indicator that specifies that a collection event was not permitted. + #[serde(rename = "Not allowed to collect")] + NotAllowedToCollect, + + /// Hispanic or Latino + /// + /// A person of Cuban, Mexican, Puerto Rican, South or Central American, or + /// other Spanish culture or origin, regardless of race. The term, "Spanish + /// origin," can be used in addition to "Hispanic or Latino." (OMB) + #[serde(rename = "Hispanic or Latino")] + HispanicOrLatino, + + /// Not Hispanic or Latino + /// + /// A person not of Cuban, Mexican, Puerto Rican, South or Central American, + /// or other Spanish culture or origin, regardless of race. + #[serde(rename = "Not Hispanic or Latino")] + NotHispanicOrLatino, + + /// Unknown + /// + /// Not known, not observed, not recorded, or refused. + #[serde(rename = "Unknown")] + Unknown, + + /// Not Reported + /// + /// Not provided or available. + #[serde(rename = "Not reported")] + NotReported, +} + +impl CDE for Ethnicity {} + +impl crate::Standard for Ethnicity { + fn standard() -> &'static str { + "caDSR CDE 2192217 v2.00" + } +} + +impl crate::Url for Ethnicity { + fn url() -> &'static str { + "https://cadsr.cancer.gov/onedata/dmdirect/NIH/NCI/CO/CDEDD?filter=CDEDD.ITEM_ID=2192217%20and%20ver_nr=2.0" + } +} + +impl std::fmt::Display for Ethnicity { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Ethnicity::NotAllowedToCollect => write!(f, "Not allowed to collect"), + Ethnicity::HispanicOrLatino => write!(f, "Hispanic or Latino"), + Ethnicity::NotHispanicOrLatino => write!(f, "Not Hispanic or Latino"), + Ethnicity::Unknown => write!(f, "Unknown"), + Ethnicity::NotReported => write!(f, "Not reported"), + } + } +} + +impl Distribution for Standard { + fn sample(&self, rng: &mut R) -> Ethnicity { + match rng.gen_range(0..=4) { + 0 => Ethnicity::NotAllowedToCollect, + 1 => Ethnicity::HispanicOrLatino, + 2 => Ethnicity::NotHispanicOrLatino, + 3 => Ethnicity::Unknown, + _ => Ethnicity::NotReported, + } + } +} diff --git a/packages/ccdi-models/Cargo.toml b/packages/ccdi-models/Cargo.toml new file mode 100644 index 0000000..1933b23 --- /dev/null +++ b/packages/ccdi-models/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "ccdi-models" +version = "0.1.0" +license.workspace = true +edition.workspace = true + +[dependencies] +ccdi-cde = { path = "../ccdi-cde", version = "0.1.0" } +indexmap.workspace = true +macropol = "0.1.3" +rand.workspace = true +serde.workspace = true +serde_json.workspace = true +serde_test.workspace = true +utoipa.workspace = true diff --git a/packages/ccdi-models/src/count.rs b/packages/ccdi-models/src/count.rs new file mode 100644 index 0000000..17dfe48 --- /dev/null +++ b/packages/ccdi-models/src/count.rs @@ -0,0 +1,5 @@ +//! Representations of counts. + +mod total; + +pub use total::Total; diff --git a/packages/ccdi-models/src/count/total.rs b/packages/ccdi-models/src/count/total.rs new file mode 100644 index 0000000..b6a77a9 --- /dev/null +++ b/packages/ccdi-models/src/count/total.rs @@ -0,0 +1,19 @@ +//! Counts of totals. + +use serde::Deserialize; +use serde::Serialize; +use utoipa::ToSchema; + +/// Total count of some entity as reported alongside an API call. +#[derive(Debug, Deserialize, Serialize, ToSchema)] +#[schema(as = models::count::Total)] +pub struct Total { + /// The total number of entities returned in the API call. + total: usize, +} + +impl From for Total { + fn from(value: usize) -> Self { + Self { total: value } + } +} diff --git a/packages/ccdi-models/src/lib.rs b/packages/ccdi-models/src/lib.rs new file mode 100644 index 0000000..ac0d01e --- /dev/null +++ b/packages/ccdi-models/src/lib.rs @@ -0,0 +1,16 @@ +//! A crate containing data models for common objects used within the Childhood +//! Cancer Data Initiative federated API. + +#![warn(missing_docs)] +#![warn(rust_2018_idioms)] +#![warn(rust_2021_compatibility)] +#![warn(missing_debug_implementations)] +#![deny(rustdoc::broken_intra_doc_links)] +#![feature(decl_macro)] +#![feature(trivial_bounds)] + +pub mod count; +pub mod metadata; +pub mod subject; + +pub use subject::Subject; diff --git a/packages/ccdi-models/src/metadata.rs b/packages/ccdi-models/src/metadata.rs new file mode 100644 index 0000000..3bdbc22 --- /dev/null +++ b/packages/ccdi-models/src/metadata.rs @@ -0,0 +1,4 @@ +//! Representations of metadata. + +pub mod field; +pub mod fields; diff --git a/packages/ccdi-models/src/metadata/field.rs b/packages/ccdi-models/src/metadata/field.rs new file mode 100644 index 0000000..ad7390c --- /dev/null +++ b/packages/ccdi-models/src/metadata/field.rs @@ -0,0 +1,119 @@ +//! A metadata field. + +use serde::Deserialize; +use serde::Serialize; +use utoipa::ToSchema; + +pub mod description; +pub mod owned; +pub mod unowned; + +pub use description::Description; + +pub use owned::Identifier; +pub use unowned::Ethnicity; +pub use unowned::Race; +pub use unowned::Sex; + +/// A metadata field. +#[derive(Clone, Debug, Deserialize, Eq, Serialize, PartialEq, ToSchema)] +#[serde(untagged)] +pub enum Field { + /// An owned field. + Owned(owned::Field), + + /// An unowned field. + Unowned(unowned::Field), +} + +macro_rules! unowned_field_or_null { + ($name: ident, $as: ty, $inner: ty) => { + /// An unowned metadata field or null. + #[derive(Clone, Debug, Deserialize, Eq, Serialize, PartialEq, ToSchema)] + #[schema(as = $as)] + #[serde(untagged)] + pub enum $name { + /// An unowned value representing the field. + #[schema(value_type = field::$inner)] + Unowned($inner), + + /// A null field. + Null, + } + }; +} + +unowned_field_or_null!(SexOrNull, field::SexOrNull, Sex); +unowned_field_or_null!(EthnicityOrNull, field::EthnicityOrNull, Ethnicity); + +macro_rules! multiple_unowned_fields_or_null { + ($name: ident, $as: ty, $inner: ty) => { + /// Multiple unowned metadata fields or null. + #[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq, ToSchema)] + #[schema(as = $as)] + #[serde(untagged)] + pub enum $name { + /// Multiple unowned values representing the field(s). + #[schema(value_type = Vec)] + MultipleUnowned(Vec<$inner>), + + /// A null field. + Null, + } + }; +} + +multiple_unowned_fields_or_null!(RacesOrNull, field::RacesOrNull, Race); + +#[allow(unused_macros)] +macro_rules! owned_field_or_null { + ($name: ident, $as: ty, $inner: ty) => { + /// An owned metadata field. + #[derive(Clone, Debug, Deserialize, Eq, Serialize, PartialEq, ToSchema)] + #[schema(as = $as)] + #[serde(untagged)] + pub enum $name { + /// An owned value representing the field. + #[schema(value_type = field::$inner)] + Owned($inner), + + /// A null field. + Null, + } + }; +} + +macro_rules! multiple_owned_fields_or_null { + ($name: ident, $as: ty, $inner: ty) => { + /// Multiple owned metadata fields or null. + #[derive(Clone, Debug, Deserialize, Eq, Serialize, PartialEq, ToSchema)] + #[schema(as = $as)] + #[serde(untagged)] + pub enum $name { + /// Multiple owned values representing the field(s). + #[schema(value_type = Vec)] + MultipleOwned(Vec<$inner>), + + /// A null field. + Null, + } + }; +} + +multiple_owned_fields_or_null!(IdentifiersOrNull, field::IdentifiersOrNull, Identifier); + +#[cfg(test)] +mod tests { + use serde_test::assert_tokens; + use serde_test::Token; + + use super::*; + + #[test] + fn it_serializes_null_correctly() { + let field = SexOrNull::Null; + + assert_tokens(&field, &[Token::Unit]); + assert_eq!(serde_json::to_string(&field).unwrap(), "null"); + } +} diff --git a/packages/ccdi-models/src/metadata/field/description.rs b/packages/ccdi-models/src/metadata/field/description.rs new file mode 100644 index 0000000..b3880a0 --- /dev/null +++ b/packages/ccdi-models/src/metadata/field/description.rs @@ -0,0 +1,77 @@ +//! Metadata field descriptions. + +use serde::Deserialize; +use serde::Serialize; +use utoipa::ToSchema; + +mod harmonized; +mod unharmonized; + +pub use harmonized::Harmonized; +pub use unharmonized::Unharmonized; + +/// A description for a metadata field. +#[derive(Debug, Deserialize, Serialize, ToSchema)] +#[serde(untagged)] +#[schema(as = models::metadata::field::Description)] +pub enum Description { + /// A harmonized metadata field description. + #[schema(value_type = models::metadata::field::description::Harmonized)] + Harmonized(Harmonized), + + /// An unharmonized metadata field description. + #[schema(value_type = models::metadata::field::description::Unharmonized)] + Unharmonized(Unharmonized), +} + +/// Traits related to a [`Description`]. +pub mod r#trait { + use ccdi_cde as cde; + + use cde::Standard; + use cde::Url; + use cde::CDE; + + use crate::metadata::field::description::Harmonized; + + /// A trait to get a [`Description`] for a [`CDE`]. + pub trait Description + where + Self: CDE, + { + /// Gets the [`Description`]. + fn description() -> super::Description; + } + + impl Description for cde::v1::Sex { + fn description() -> super::Description { + super::Description::Harmonized(Harmonized::new("sex", Self::standard(), Self::url())) + } + } + + impl Description for cde::v1::Race { + fn description() -> super::Description { + super::Description::Harmonized(Harmonized::new("race", Self::standard(), Self::url())) + } + } + + impl Description for cde::v2::Ethnicity { + fn description() -> super::Description { + super::Description::Harmonized(Harmonized::new( + "ethnicity", + Self::standard(), + Self::url(), + )) + } + } + + impl Description for cde::v1::Identifier { + fn description() -> super::Description { + super::Description::Harmonized(Harmonized::new( + "identifiers", + Self::standard(), + Self::url(), + )) + } + } +} diff --git a/packages/ccdi-models/src/metadata/field/description/harmonized.rs b/packages/ccdi-models/src/metadata/field/description/harmonized.rs new file mode 100644 index 0000000..132a730 --- /dev/null +++ b/packages/ccdi-models/src/metadata/field/description/harmonized.rs @@ -0,0 +1,53 @@ +//! Harmonized metadata field descriptions. + +use serde::Deserialize; +use serde::Serialize; +use utoipa::ToSchema; + +/// A harmonized metadata field description. +#[derive(Debug, Deserialize, Serialize, ToSchema)] +#[schema(as = models::metadata::field::description::Harmonized)] +pub struct Harmonized { + /// Whether or not this field is harmonized across the ecosystem. + /// + /// This will always be set to `true`. + #[schema(default = true)] + harmonized: bool, + + /// A comma (`.`) delimited path to the field's location on the `metadata` + /// objects returned by the various subject endpoints. + path: String, + + /// The proper name of the standard to which this field is harmonized (defined + /// by the documentation for the CCDI metadata fields). + standard: String, + + /// A URL to the CCDI documentation where the definition of this harmonized + /// field resides. + url: String, +} + +impl Harmonized { + /// Creates a new [harmonized metadata field description](Harmonized). + /// + /// # Examples + /// + /// ``` + /// use ccdi_models as models; + /// use models::metadata::field::description::Harmonized; + /// + /// let description = Harmonized::new( + /// "test", + /// "caDSR ------ v1.00", + /// "https://cancer.gov" + /// ); + /// ``` + pub fn new>(path: S, standard: S, url: S) -> Self { + Harmonized { + harmonized: true, + path: path.into(), + standard: standard.into(), + url: url.into(), + } + } +} diff --git a/packages/ccdi-models/src/metadata/field/description/unharmonized.rs b/packages/ccdi-models/src/metadata/field/description/unharmonized.rs new file mode 100644 index 0000000..4244b3b --- /dev/null +++ b/packages/ccdi-models/src/metadata/field/description/unharmonized.rs @@ -0,0 +1,186 @@ +//! Unharmonized metadata field descriptions. + +use serde::Deserialize; +use serde::Serialize; +use utoipa::ToSchema; + +/// An unharmonized metadata field description. +#[derive(Debug, Deserialize, Serialize, ToSchema)] +#[schema(as = models::metadata::field::description::Unharmonized)] +pub struct Unharmonized { + /// Whether or not this field is harmonized across the ecosystem. + /// + /// This will always be set to `false`. + #[schema(default = false)] + harmonized: bool, + + /// A display name for this metadata field as _suggested_ by the server (this + /// is not considered authoritative and can be ignored by the client if it so + /// chooses). This is mainly to avoid naming collisions of common fields across + /// servers. + name: Option, + + /// A plain-text description of what the field represents. + description: Option, + + /// A comma (`.`) delimited path to the field's location on the `metadata` + /// objects returned by the various subject endpoints. + path: String, + + /// If the field is considered harmonized across the federation ecosystem, the + /// name of the standard to which the field is harmonized. + /// + /// If the field is _not_ harmonized across the federation ecosystem, then this + /// should be [`None`]. + standard: Option, + + /// A url that describes more about the metadata field, if available. + url: Option, +} + +impl Unharmonized { + /// Creates a new [unharmonized metadata field description](Unharmonized). + /// + /// # Examples + /// + /// ``` + /// use ccdi_models as models; + /// use models::metadata::field::description::Unharmonized; + /// + /// let field = Unharmonized::new( + /// Some("test".into()), + /// Some("A description.".into()), + /// "test", + /// None, + /// None + /// ); + /// ``` + pub fn new, O: Into>>( + name: O, + description: O, + path: S, + standard: O, + url: O, + ) -> Self { + Unharmonized { + harmonized: false, + name: name.into(), + description: description.into(), + path: path.into(), + standard: standard.into(), + url: url.into(), + } + } + + /// Gets the name of the [`Unharmonized`] by reference. + /// + /// # Examples + /// + /// ``` + /// use ccdi_models as models; + /// use models::metadata::field::description::Unharmonized; + /// + /// let field = Unharmonized::new( + /// Some("test".into()), + /// Some("A description.".into()), + /// "test", + /// None, + /// None + /// ); + /// + /// assert_eq!(field.name(), Some(&String::from("test"))) + /// ``` + pub fn name(&self) -> Option<&String> { + self.name.as_ref() + } + + /// Gets the description of the [`Unharmonized`] by reference. + /// + /// # Examples + /// + /// ``` + /// use ccdi_models as models; + /// use models::metadata::field::description::Unharmonized; + /// + /// let field = Unharmonized::new( + /// Some("test".into()), + /// Some("A description.".into()), + /// "test", + /// None, + /// None + /// ); + /// + /// assert_eq!(field.description(), Some(&String::from("A description."))) + /// ``` + pub fn description(&self) -> Option<&String> { + self.description.as_ref() + } + + /// Gets the path of the [`Unharmonized`] by reference. + /// + /// # Examples + /// + /// ``` + /// use ccdi_models as models; + /// use models::metadata::field::description::Unharmonized; + /// + /// let field = Unharmonized::new( + /// Some("test".into()), + /// Some("A description.".into()), + /// "test", + /// None, + /// None + /// ); + /// + /// assert_eq!(field.path(), &String::from("test")) + /// ``` + pub fn path(&self) -> &String { + &self.path + } + + /// Gets the harmonization standard name of the [`Unharmonized`] by + /// reference. + /// + /// # Examples + /// + /// ``` + /// use ccdi_models as models; + /// use models::metadata::field::description::Unharmonized; + /// + /// let field = Unharmonized::new( + /// Some("test".into()), + /// Some("A description.".into()), + /// "test", + /// Some("US Census Bureau".into()), + /// None + /// ); + /// + /// assert_eq!(field.standard().unwrap(), &String::from("US Census Bureau")) + /// ``` + pub fn standard(&self) -> Option<&String> { + self.standard.as_ref() + } + + /// Gets the URL for which one can learn more about the [`Unharmonized`] by + /// reference. + /// + /// # Examples + /// + /// ``` + /// use ccdi_models as models; + /// use models::metadata::field::description::Unharmonized; + /// + /// let field = Unharmonized::new( + /// Some("test".into()), + /// Some("A description.".into()), + /// "test", + /// None, + /// Some("https://cancer.gov".into()) + /// ); + /// + /// assert_eq!(field.url().unwrap(), &String::from("https://cancer.gov")) + /// ``` + pub fn url(&self) -> Option<&String> { + self.url.as_ref() + } +} diff --git a/packages/ccdi-models/src/metadata/field/owned.rs b/packages/ccdi-models/src/metadata/field/owned.rs new file mode 100644 index 0000000..3689090 --- /dev/null +++ b/packages/ccdi-models/src/metadata/field/owned.rs @@ -0,0 +1,189 @@ +//! Owned metadata fields. + +// This must be present because we are using aliases from utoipa. Utoipa does +// not give us a way to document those generated types, and it is not possible +// to add this statement only for those generated types, so we must allow it in +// the entire file. +#![allow(missing_docs)] + +use rand::distributions::Standard; +use rand::prelude::Distribution; +use serde::Deserialize; +use serde::Serialize; +use serde_json::Value; +use utoipa::ToSchema; + +use ccdi_cde as cde; + +#[macropol::macropol] +macro_rules! owned_field { + ($name: ident, $as: ty, $inner: ty, $value: expr, $import: expr) => { + #[derive(Clone, Debug, Deserialize, Eq, Serialize, PartialEq, ToSchema)] + #[schema(as = $as)] + /// An owned field representing a [`${stringify!($name)}`]. + pub struct $name { + /// The value of the metadata field. + value: $inner, + + /// The ancestors from which this field was derived. + /// + /// Ancestors should be provided as period (`.`) delimited paths + /// from the `metadata` key in the subject response object. + #[serde(skip_serializing_if = "Option::is_none")] + ancestors: Option>, + + /// A free-text comment field. + #[serde(skip_serializing_if = "Option::is_none")] + comment: Option, + + /// Whether or not the field is owned by the source server. + #[serde(skip_serializing_if = "Option::is_none")] + owned: Option, + } + + impl $name { + /// Creates a new [`${stringify!($name)}`]. + /// + /// # Examples + /// + /// ``` + /// use ${stringify!($import)}; + /// use ccdi_models as models; + /// + /// use models::metadata::field::owned::${stringify!($name)}; + /// + /// let field = ${stringify!($name)}::new( + /// ${stringify!($value)}, + /// None, + /// None, + /// Some(true) + /// ); + /// ``` + pub fn new( + value: $inner, + ancestors: Option>, + comment: Option, + owned: Option, + ) -> Self { + Self { + value, + ancestors, + comment, + owned, + } + } + + /// Gets the value from the [`${stringify!($name)}`] by reference. + /// + /// # Examples + /// + /// ``` + /// use ${stringify!($import)}; + /// use ccdi_models as models; + /// + /// use models::metadata::field::owned::${stringify!($name)}; + /// + /// let field = ${stringify!($name)}::new( + /// ${stringify!($value)}, + /// None, + /// None, + /// Some(false) + /// ); + /// + /// assert_eq!(field.value(), &${stringify!($value)}); + /// ``` + pub fn value(&self) -> &$inner { + &self.value + } + + /// Gets the ancestors from the [`${stringify!($name)}`] by reference. + /// + /// # Examples + /// + /// ``` + /// use ${stringify!($import)}; + /// use ccdi_models as models; + /// + /// use models::metadata::field::owned::${stringify!($name)}; + /// + /// let field = ${stringify!($name)}::new( + /// ${stringify!($value)}, + /// Some(vec![String::from("reported_sex")]), + /// None, + /// Some(false) + /// ); + /// + /// assert_eq!(field.ancestors(), Some(&vec![String::from("reported_sex")])); + /// ``` + pub fn ancestors(&self) -> Option<&Vec> { + self.ancestors.as_ref() + } + + /// Gets the comment from the [`${stringify!($name)}`] by reference. + /// + /// # Examples + /// + /// ``` + /// use ${stringify!($import)}; + /// use ccdi_models as models; + /// + /// use models::metadata::field::owned::${stringify!($name)}; + /// + /// let field = ${stringify!($name)}::new( + /// ${stringify!($value)}, + /// None, + /// Some(String::from("Comment.")), + /// Some(false) + /// ); + /// + /// assert_eq!(field.comment(), Some(&String::from("Comment."))); + /// ``` + pub fn comment(&self) -> Option<&String> { + self.comment.as_ref() + } + + /// Gets the ownership from the [`${stringify!($name)}`]. + /// + /// # Examples + /// + /// ``` + /// use ${stringify!($import)}; + /// use ccdi_models as models; + /// + /// use models::metadata::field::owned::${stringify!($name)}; + /// + /// let field = ${stringify!($name)}::new( + /// ${stringify!($value)}, + /// None, + /// None, + /// Some(true) + /// ); + /// + /// assert_eq!(field.owned(), Some(true)); + /// ``` + pub fn owned(&self) -> Option { + self.owned.as_ref().copied() + } + } + + #[allow(trivial_bounds)] + impl Distribution<$name> for Standard + where + Standard: Distribution<$inner>, + { + fn sample(&self, _: &mut R) -> $name { + $name::new(rand::random(), None, None, Some(false)) + } + } + }; +} + +owned_field!(Field, field::Owned, Value, Value::Null, serde_json::Value); + +owned_field!( + Identifier, + field::Identifier, + cde::v1::Identifier, + cde::v1::Identifier::new("organization", "Name"), + ccdi_cde as cde +); diff --git a/packages/ccdi-models/src/metadata/field/unowned.rs b/packages/ccdi-models/src/metadata/field/unowned.rs new file mode 100644 index 0000000..34a9a37 --- /dev/null +++ b/packages/ccdi-models/src/metadata/field/unowned.rs @@ -0,0 +1,172 @@ +//! Unowned metadata fields. + +// This must be present because we are using aliases from utoipa. Utoipa does +// not give us a way to document those generated types, and it is not possible +// to add this statement only for those generated types, so we must allow it in +// the entire file. +#![allow(missing_docs)] + +use rand::distributions::Standard; +use rand::prelude::Distribution; +use serde::Deserialize; +use serde::Serialize; +use serde_json::Value; +use utoipa::ToSchema; + +use ccdi_cde as cde; + +#[macropol::macropol] +macro_rules! unowned_field { + ($name: ident, $as: ty, $inner: ty, $value: expr, $import: expr) => { + #[derive(Clone, Debug, Deserialize, Eq, Serialize, PartialEq, ToSchema)] + #[schema(as = $as)] + /// An unowned field representing a [`${stringify!($name)}`]. + pub struct $name { + /// The value of the metadata field. + value: $inner, + + /// The ancestors from which this field was derived. + /// + /// Ancestors should be provided as period (`.`) delimited paths + /// from the `metadata` key in the subject response object. + #[serde(skip_serializing_if = "Option::is_none")] + ancestors: Option>, + + /// A free-text comment field. + #[serde(skip_serializing_if = "Option::is_none")] + comment: Option, + } + + impl $name { + /// Creates a new [`${stringify!($name)}`]. + /// + /// # Examples + /// + /// ``` + /// use ${stringify!($import)}; + /// use ccdi_models as models; + /// + /// use models::metadata::field::unowned::${stringify!($name)}; + /// + /// let field = ${stringify!($name)}::new( + /// ${stringify!($value)}, + /// None, + /// None + /// ); + /// ``` + pub fn new( + value: $inner, + ancestors: Option>, + comment: Option, + ) -> Self { + Self { + value, + ancestors, + comment, + } + } + + /// Gets the value from the [`${stringify!($name)}`] by reference. + /// + /// # Examples + /// + /// ``` + /// use ${stringify!($import)}; + /// use ccdi_models as models; + /// + /// use models::metadata::field::unowned::${stringify!($name)}; + /// + /// let field = ${stringify!($name)}::new( + /// ${stringify!($value)}, + /// None, + /// None + /// ); + /// + /// assert_eq!(field.value(), &${stringify!($value)}); + /// ``` + pub fn value(&self) -> &$inner { + &self.value + } + + /// Gets the ancestors from the [`${stringify!($name)}`] by reference. + /// + /// # Examples + /// + /// ``` + /// use ${stringify!($import)}; + /// use ccdi_models as models; + /// + /// use models::metadata::field::unowned::${stringify!($name)}; + /// + /// let field = ${stringify!($name)}::new( + /// ${stringify!($value)}, + /// Some(vec![String::from("another_field")]), + /// None + /// ); + /// + /// assert_eq!(field.ancestors(), Some(&vec![String::from("another_field")])); + /// ``` + pub fn ancestors(&self) -> Option<&Vec> { + self.ancestors.as_ref() + } + + /// Gets the comment from the [`${stringify!($name)}`] by reference. + /// + /// # Examples + /// + /// ``` + /// use ${stringify!($import)}; + /// use ccdi_models as models; + /// + /// use models::metadata::field::unowned::${stringify!($name)}; + /// + /// let field = ${stringify!($name)}::new( + /// ${stringify!($value)}, + /// None, + /// Some(String::from("Comment.")) + /// ); + /// + /// assert_eq!(field.comment(), Some(&String::from("Comment."))); + /// ``` + pub fn comment(&self) -> Option<&String> { + self.comment.as_ref() + } + } + + #[allow(trivial_bounds)] + impl Distribution<$name> for Standard + where + Standard: Distribution<$inner>, + { + fn sample(&self, _: &mut R) -> $name { + $name::new(rand::random(), None, None) + } + } + }; +} + +unowned_field!(Field, field::Unowned, Value, Value::Null, serde_json::Value); + +unowned_field!( + Sex, + field::Sex, + cde::v1::Sex, + cde::v1::Sex::Unknown, + ccdi_cde as cde +); + +unowned_field!( + Race, + field::Race, + cde::v1::Race, + cde::v1::Race::Unknown, + ccdi_cde as cde +); + +unowned_field!( + Ethnicity, + field::Ethnicity, + cde::v2::Ethnicity, + cde::v2::Ethnicity::Unknown, + ccdi_cde as cde +); diff --git a/packages/ccdi-models/src/metadata/fields.rs b/packages/ccdi-models/src/metadata/fields.rs new file mode 100644 index 0000000..89a8e26 --- /dev/null +++ b/packages/ccdi-models/src/metadata/fields.rs @@ -0,0 +1,53 @@ +//! Collections of metadata fields. + +use indexmap::IndexMap; + +use crate::metadata::field::Field; + +/// A map of unharmonized metadata fields. +/// +/// # Examples +/// +/// ``` +/// use serde_json::Value; +/// +/// use ccdi_models as models; +/// +/// use models::metadata::field::Field; +/// use models::metadata::field::owned; +/// use models::metadata::field::unowned; +/// use models::metadata::fields::Unharmonized; +/// +/// let mut unharmonized = Unharmonized::default(); +/// +/// unharmonized.insert( +/// String::from("hello"), +/// Field::Unowned( +/// unowned::Field::new( +/// Value::String(String::from("world")), +/// None, +/// None +/// ) +/// ) +/// ); +/// +/// unharmonized.insert( +/// String::from("foo"), +/// Field::Owned( +/// owned::Field::new( +/// Value::String(String::from("bar")), +/// None, +/// None, +/// Some(true) +/// ) +/// ) +/// ); +/// +/// assert_eq!( +/// serde_json::to_string(&unharmonized)?, +/// "{\"hello\":{\"value\":\"world\"},\"foo\":{\"value\":\"bar\",\"owned\":true}}" +/// ); +/// +/// # Ok::<(), Box>(()) +/// ``` +pub type Unharmonized = IndexMap; diff --git a/packages/ccdi-models/src/subject.rs b/packages/ccdi-models/src/subject.rs new file mode 100644 index 0000000..e14441b --- /dev/null +++ b/packages/ccdi-models/src/subject.rs @@ -0,0 +1,220 @@ +//! Representations of subjects. + +use rand::thread_rng; +use rand::Rng; +use serde::Deserialize; +use serde::Serialize; +use utoipa::ToSchema; + +use ccdi_cde as cde; + +use cde::v1::Identifier; + +mod kind; +pub mod metadata; + +pub use kind::Kind; +pub use metadata::Metadata; + +/// A subject. +#[derive(Clone, Debug, Deserialize, Serialize, ToSchema)] +#[schema(as = models::Subject)] +pub struct Subject { + /// The primary identifier used by the site. + /// + /// This identifier should *ALWAYS* be included in the `identifiers` key + /// under `metadata`, should that key exist. + #[schema(value_type = cde::v1::Identifier)] + id: Identifier, + + /// The primary name or identifier for a subject used within the source + /// server. + #[schema(example = "SubjectName001")] + name: String, + + /// The kind of [`Subject`]. + #[schema(value_type = models::subject::Kind)] + kind: Kind, + + /// Metadata associated with this [`Subject`]. + #[serde(skip_serializing_if = "Option::is_none")] + #[schema(value_type = Option)] + metadata: Option, +} + +impl Subject { + /// Creates a new [`Subject`]. + /// + /// # Examples + /// + /// ``` + /// use ccdi_cde as cde; + /// use ccdi_models as models; + /// + /// use cde::v1::Identifier; + /// use models::Subject; + /// use models::subject::Kind; + /// use models::subject::metadata::Builder; + /// + /// let subject = Subject::new( + /// Identifier::parse("organization:Name", ":")?, + /// String::from("Name"), + /// Kind::Participant, + /// Some(Builder::default().build()) + /// ); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn new(id: Identifier, name: String, kind: Kind, metadata: Option) -> Self { + Self { + id, + name, + kind, + metadata, + } + } + + /// Gets the primary identifier for this [`Subject`] by reference. + /// + /// # Examples + /// + /// ``` + /// use ccdi_cde as cde; + /// use ccdi_models as models; + /// + /// use cde::v1::Identifier; + /// use models::Subject; + /// use models::subject::Kind; + /// use models::subject::metadata::Builder; + /// + /// let subject = Subject::new( + /// Identifier::parse("organization:Name", ":")?, + /// String::from("Name"), + /// Kind::Participant, + /// Some(Builder::default().build()) + /// ); + /// + /// assert_eq!( + /// subject.id(), + /// &Identifier::parse("organization:Name", ":").unwrap() + /// ); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn id(&self) -> &Identifier { + &self.id + } + + /// Gets the name for this [`Subject`] by reference. + /// + /// # Examples + /// + /// ``` + /// use ccdi_cde as cde; + /// use ccdi_models as models; + /// + /// use cde::v1::Identifier; + /// use models::Subject; + /// use models::subject::Kind; + /// use models::subject::metadata::Builder; + /// + /// let subject = Subject::new( + /// Identifier::parse("organization:Name", ":")?, + /// String::from("Name"), + /// Kind::Participant, + /// Some(Builder::default().build()) + /// ); + /// + /// assert_eq!(subject.name(), &String::from("Name")); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn name(&self) -> &String { + &self.name + } + + /// Gets the kind for this [`Subject`] by reference. + /// + /// # Examples + /// + /// ``` + /// use ccdi_cde as cde; + /// use ccdi_models as models; + /// + /// use cde::v1::Identifier; + /// use models::Subject; + /// use models::subject::Kind; + /// use models::subject::metadata::Builder; + /// + /// let subject = Subject::new( + /// Identifier::parse("organization:Name", ":")?, + /// String::from("Name"), + /// Kind::Participant, + /// Some(Builder::default().build()) + /// ); + /// + /// assert_eq!(subject.kind(), &Kind::Participant); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn kind(&self) -> &Kind { + &self.kind + } + + /// Gets the metadata for this [`Subject`] by reference. + /// + /// # Examples + /// + /// ``` + /// use ccdi_cde as cde; + /// use ccdi_models as models; + /// + /// use cde::v1::Identifier; + /// use models::Subject; + /// use models::subject::Kind; + /// use models::subject::metadata::Builder; + /// + /// let metadata = Builder::default().build(); + /// + /// let subject = Subject::new( + /// Identifier::parse("organization:Name", ":")?, + /// String::from("Name"), + /// Kind::Participant, + /// Some(metadata.clone()) + /// ); + /// + /// assert_eq!(subject.metadata(), Some(&metadata)); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn metadata(&self) -> Option<&Metadata> { + self.metadata.as_ref() + } + + /// Generates a random [`Subject`] based on a particular [`Identifier`]. + /// + /// # Examples + /// + /// ``` + /// use ccdi_cde as cde; + /// use ccdi_models as models; + /// + /// use models::Subject; + /// + /// let identifier = cde::v1::Identifier::parse("organization:Name", ":").unwrap(); + /// let subject = Subject::random(identifier); + /// ``` + pub fn random(identifier: Identifier) -> Self { + let mut rng = thread_rng(); + + Self { + id: identifier.clone(), + name: identifier.name().clone(), + kind: rand::random(), + metadata: match rng.gen_bool(0.7) { + true => Some(Metadata::random(identifier)), + false => None, + }, + } + } +} diff --git a/packages/ccdi-models/src/subject/kind.rs b/packages/ccdi-models/src/subject/kind.rs new file mode 100644 index 0000000..976a0e7 --- /dev/null +++ b/packages/ccdi-models/src/subject/kind.rs @@ -0,0 +1,37 @@ +use rand::distributions::Standard; +use rand::prelude::Distribution; +use serde::Deserialize; +use serde::Serialize; +use utoipa::ToSchema; + +/// A kind of [`Subject`](super::Subject). +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)] +#[schema(as = models::subject::Kind)] +pub enum Kind { + /// A participant or individual. + #[serde(rename = "Participant")] + Participant, + + /// A xenograft which was originally derived from a patient tumor. + #[serde(rename = "Patient Derived Xenograft")] + PatientDerivedXenograft, + + /// A cell line. + #[serde(rename = "Cell Line")] + CellLine, + + /// An organoid. + #[serde(rename = "Organoid")] + Organoid, +} + +impl Distribution for Standard { + fn sample(&self, rng: &mut R) -> Kind { + match rng.gen_range(0..=3) { + 0 => Kind::Participant, + 1 => Kind::PatientDerivedXenograft, + 2 => Kind::CellLine, + _ => Kind::Organoid, + } + } +} diff --git a/packages/ccdi-models/src/subject/metadata.rs b/packages/ccdi-models/src/subject/metadata.rs new file mode 100644 index 0000000..b1d5413 --- /dev/null +++ b/packages/ccdi-models/src/subject/metadata.rs @@ -0,0 +1,188 @@ +//! Metadata for a [`Subject`](super::Subject). + +use serde::Deserialize; +use serde::Serialize; +use utoipa::ToSchema; + +use crate::metadata::field; +use crate::metadata::field::EthnicityOrNull; +use crate::metadata::field::IdentifiersOrNull; +use crate::metadata::field::RacesOrNull; +use crate::metadata::field::SexOrNull; + +use ccdi_cde as cde; + +pub mod builder; + +pub use builder::Builder; + +/// Metadata associated with a subject. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)] +#[schema(as = models::subject::Metadata)] +pub struct Metadata { + /// The sex of the subject. + #[schema(value_type = field::SexOrNull, nullable = true)] + sex: SexOrNull, + + /// The race(s) of the subject. + #[schema(value_type = field::RacesOrNull, nullable = true)] + race: RacesOrNull, + + /// The ethnicity of the subject. + #[schema(value_type = field::EthnicityOrNull, nullable = true)] + ethnicity: EthnicityOrNull, + + /// The identifiers for the subject. + /// + /// Note that this list of identifiers *must* include the main identifier + /// for the [`Subject`]. + #[schema(value_type = field::IdentifiersOrNull, nullable = true)] + identifiers: IdentifiersOrNull, +} + +impl Metadata { + /// Gets the harmonized sex for the [`Metadata`]. + /// + /// # Examples + /// + /// ``` + /// use ccdi_cde as cde; + /// use ccdi_models as models; + /// + /// use models::metadata::field::SexOrNull; + /// use models::metadata::field::unowned::Sex; + /// use models::subject::metadata::Builder; + /// + /// let metadata = Builder::default() + /// .sex(Sex::new(cde::v1::Sex::Female, None, None)) + /// .build(); + /// + /// assert_eq!( + /// metadata.sex(), + /// &SexOrNull::Unowned( + /// Sex::new(cde::v1::Sex::Female, None, None) + /// ) + /// ); + /// ``` + pub fn sex(&self) -> &SexOrNull { + &self.sex + } + + /// Gets the harmonized race(s) for the [`Metadata`]. + /// + /// # Examples + /// + /// ``` + /// use ccdi_cde as cde; + /// use ccdi_models as models; + /// + /// use models::metadata::field::RacesOrNull; + /// use models::metadata::field::unowned::Race; + /// use models::subject::metadata::Builder; + /// + /// let metadata = Builder::default() + /// .append_race(Race::new(cde::v1::Race::Asian, None, None)) + /// .build(); + /// + /// assert_eq!( + /// metadata.race(), + /// &RacesOrNull::MultipleUnowned( + /// vec![ + /// Race::new(cde::v1::Race::Asian, None, None) + /// ] + /// ) + /// ); + /// ``` + pub fn race(&self) -> &RacesOrNull { + &self.race + } + + /// Gets the harmonized ethnicity for the [`Metadata`]. + /// + /// # Examples + /// + /// ``` + /// use ccdi_cde as cde; + /// use ccdi_models as models; + /// + /// use models::metadata::field::EthnicityOrNull; + /// use models::metadata::field::unowned::Ethnicity; + /// use models::subject::metadata::Builder; + /// + /// let metadata = Builder::default() + /// .ethnicity(Ethnicity::new(cde::v2::Ethnicity::NotHispanicOrLatino, None, None)) + /// .build(); + /// + /// assert_eq!( + /// metadata.ethnicity(), + /// &EthnicityOrNull::Unowned(Ethnicity::new(cde::v2::Ethnicity::NotHispanicOrLatino, None, None)) + /// ); + /// ``` + pub fn ethnicity(&self) -> &EthnicityOrNull { + &self.ethnicity + } + + /// Gets the harmonized identifier(s) for the [`Metadata`]. + /// + /// # Examples + /// + /// ``` + /// use ccdi_cde as cde; + /// use ccdi_models as models; + /// + /// use models::metadata::field::IdentifiersOrNull; + /// use models::metadata::field::owned::Identifier; + /// use models::subject::metadata::Builder; + /// + /// let metadata = Builder::default() + /// .append_identifier( + /// Identifier::new( + /// cde::v1::Identifier::parse("organization:Name", ":").unwrap(), + /// None, None, Some(true) + /// ) + /// ) + /// .build(); + /// + /// assert_eq!( + /// metadata.identifiers(), + /// &IdentifiersOrNull::MultipleOwned( + /// vec![ + /// Identifier::new( + /// cde::v1::Identifier::parse("organization:Name", ":").unwrap(), + /// None, None, Some(true) + /// ) + /// ] + /// ) + /// ); + /// ``` + pub fn identifiers(&self) -> &IdentifiersOrNull { + &self.identifiers + } + + /// Generates a random [`Metadata`] based on a particular [`Identifier`](cde::v1::Identifier). + /// + /// # Examples + /// + /// ``` + /// use ccdi_cde as cde; + /// use ccdi_models as models; + /// + /// use models::subject::Metadata; + /// + /// let identifier = cde::v1::Identifier::parse("organization:Name", ":").unwrap(); + /// let metadata = Metadata::random(identifier); + /// ``` + pub fn random(identifier: cde::v1::Identifier) -> Metadata { + Metadata { + sex: SexOrNull::Unowned(rand::random()), + race: RacesOrNull::MultipleUnowned(vec![rand::random()]), + ethnicity: EthnicityOrNull::Unowned(rand::random()), + identifiers: IdentifiersOrNull::MultipleOwned(vec![field::owned::Identifier::new( + identifier, + None, + None, + Some(true), + )]), + } + } +} diff --git a/packages/ccdi-models/src/subject/metadata/builder.rs b/packages/ccdi-models/src/subject/metadata/builder.rs new file mode 100644 index 0000000..6be5524 --- /dev/null +++ b/packages/ccdi-models/src/subject/metadata/builder.rs @@ -0,0 +1,155 @@ +//! A builder for [`Metadata`]. + +use crate::metadata::field; +use crate::metadata::field::EthnicityOrNull; +use crate::metadata::field::IdentifiersOrNull; +use crate::metadata::field::RacesOrNull; +use crate::metadata::field::SexOrNull; +use crate::subject::Metadata; + +/// A builder for [`Metadata`]. +#[derive(Clone, Debug, Default)] +pub struct Builder { + /// The sex of the subject. + sex: Option, + + /// The race of the subject. + race: Option>, + + /// The ethnicity of the subject. + ethnicity: Option, + + /// The identifiers for the subject. + identifiers: Option>, +} + +impl Builder { + /// Sets the `sex` field of the [`Builder`]. + /// + /// # Examples + /// + /// ``` + /// use ccdi_cde as cde; + /// use ccdi_models as models; + /// + /// use models::metadata::field::unowned::Sex; + /// use models::subject::metadata::Builder; + /// + /// let field = Sex::new(cde::v1::Sex::Unknown, None, None); + /// let builder = Builder::default().sex(field); + /// ``` + pub fn sex(mut self, sex: field::unowned::Sex) -> Self { + self.sex = Some(sex); + self + } + + /// Append a value to the `race` field of the [`Builder`]. + /// + /// # Examples + /// + /// ``` + /// use ccdi_cde as cde; + /// use ccdi_models as models; + /// + /// use models::metadata::field::unowned::Race; + /// use models::subject::metadata::Builder; + /// + /// let field = Race::new(cde::v1::Race::Unknown, None, None); + /// let builder = Builder::default().append_race(field); + /// ``` + pub fn append_race(mut self, race: field::unowned::Race) -> Self { + let mut inner = self.race.unwrap_or_default(); + inner.push(race); + + self.race = Some(inner); + + self + } + + /// Sets the `ethnicity` field of the [`Builder`]. + /// + /// # Examples + /// + /// ``` + /// use ccdi_cde as cde; + /// use ccdi_models as models; + /// + /// use models::metadata::field::unowned::Ethnicity; + /// use models::subject::metadata::Builder; + /// + /// let field = Ethnicity::new(cde::v2::Ethnicity::Unknown, None, None); + /// let builder = Builder::default().ethnicity(field); + /// ``` + pub fn ethnicity(mut self, ethnicity: field::unowned::Ethnicity) -> Self { + self.ethnicity = Some(ethnicity); + self + } + + /// Append a value to the `identifier` field of the [`Builder`]. + /// + /// # Examples + /// + /// ``` + /// use ccdi_cde as cde; + /// use ccdi_models as models; + /// + /// use models::metadata::field::owned::Identifier; + /// use models::subject::metadata::Builder; + /// + /// let field = Identifier::new( + /// cde::v1::Identifier::parse("organization:Name", ":")?, + /// None, + /// None, + /// None + /// ); + /// + /// let builder = Builder::default().append_identifier(field); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn append_identifier(mut self, identifier: field::owned::Identifier) -> Self { + let mut inner = self.identifiers.unwrap_or_default(); + inner.push(identifier); + + self.identifiers = Some(inner); + + self + } + + /// Consumes `self` to build a [`Metadata`]. + /// + /// # Examples + /// + /// ``` + /// use ccdi_models as models; + /// + /// use models::subject::metadata::Builder; + /// + /// let builder = Builder::default(); + /// ``` + pub fn build(self) -> Metadata { + let sex = self.sex.map(SexOrNull::Unowned).unwrap_or(SexOrNull::Null); + + let race = self + .race + .map(RacesOrNull::MultipleUnowned) + .unwrap_or(RacesOrNull::Null); + + let ethnicity = self + .ethnicity + .map(EthnicityOrNull::Unowned) + .unwrap_or(EthnicityOrNull::Null); + + let identifiers = self + .identifiers + .map(IdentifiersOrNull::MultipleOwned) + .unwrap_or(IdentifiersOrNull::Null); + + Metadata { + sex, + race, + ethnicity, + identifiers, + } + } +} diff --git a/packages/ccdi-openapi/Cargo.toml b/packages/ccdi-openapi/Cargo.toml new file mode 100644 index 0000000..976c9b2 --- /dev/null +++ b/packages/ccdi-openapi/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "ccdi-openapi" +version = "0.1.0" +license.workspace = true +edition.workspace = true + +[dependencies] +ccdi-cde = { path = "../ccdi-cde", version = "0.1.0" } +ccdi-models = { path = "../ccdi-models", version = "0.1.0" } +ccdi-server = { path = "../ccdi-server", version = "0.1.0" } +utoipa.workspace = true diff --git a/packages/ccdi-openapi/src/api.rs b/packages/ccdi-openapi/src/api.rs new file mode 100644 index 0000000..c9013f2 --- /dev/null +++ b/packages/ccdi-openapi/src/api.rs @@ -0,0 +1,112 @@ +use models::metadata::field; +use utoipa::Modify; +use utoipa::OpenApi; + +use ccdi_cde as cde; +use ccdi_models as models; +use ccdi_server as server; + +use server::responses; +use utoipa::openapi; + +/// The OpenAPI specification. +#[derive(Debug, OpenApi)] +#[openapi( + info( + title = "CCDI Pediatric Cancer Data Catalog", + description = "The CCDI Pediatric Cancer Data Catalog is an API that supports the querying +of federated pediatric cancer within the broader community. The goal of the +API is to support identification of pediatric cancer samples of interest via +a variety of query parameters.", + contact( + name = "Childhood Cancer Data Initiative support email", + email = "NCIChildhoodCancerDataInitiative@mail.nih.gov", + url = "https://www.cancer.gov/research/areas/childhood/childhood-cancer-data-initiative", + ), + version = "0.1", + ), + external_docs( + description = "Learn more about the Childhood Cancer Data Initiative", + url = "https://www.cancer.gov/research/areas/childhood/childhood-cancer-data-initiative" + ), + servers( + ( + url = "https://ccdi.stjude.cloud/api/v0", + description = "St. Jude Children's Research Hospital CCDI API server" + ), + ( + url = "https://ccdifederation.pedscommons.org/api/v0", + description = "UCSC Treehouse CCDI API server" + ), + ( + url = "https://ccdi.treehouse.gi.ucsc.edu/api/v0", + description = "Pediatric Cancer Data Commons CCDI API server" + ), + ), + tags( + ( + name = "Info", + description = "Information about the API implementation itself." + ), + ( + name = "Subject", + description = "Subjects within the CCDI federated ecosystem." + ) + ), + paths( + // Metadata. + server::routes::metadata::metadata_fields_subject, + + // Subject. + server::routes::subject::index, + server::routes::subject::show, + server::routes::subject::subjects_by_count, + ), + components(schemas( + // Common data elements. + cde::v1::Race, + cde::v1::Sex, + cde::v2::Ethnicity, + cde::v1::Identifier, + + // Fields. + field::Sex, + field::Race, + field::Ethnicity, + field::Identifier, + + // Fields or null. + field::SexOrNull, + field::RacesOrNull, + field::EthnicityOrNull, + field::IdentifiersOrNull, + + // Models. + models::Subject, + models::subject::Kind, + models::subject::Metadata, + + models::metadata::field::Description, + models::metadata::field::description::Harmonized, + models::metadata::field::description::Unharmonized, + + // Counts. + models::count::Total, + + // Responses. + responses::Error, + responses::Subject, + responses::Subjects, + responses::metadata::FieldDescriptions, + )), + modifiers(&RemoveLicense) +)] +pub struct Api; + +pub struct RemoveLicense; + +impl Modify for RemoveLicense { + fn modify(&self, openapi: &mut openapi::OpenApi) { + openapi.info.license = None; + } +} diff --git a/packages/ccdi-openapi/src/lib.rs b/packages/ccdi-openapi/src/lib.rs new file mode 100644 index 0000000..8aa4571 --- /dev/null +++ b/packages/ccdi-openapi/src/lib.rs @@ -0,0 +1,12 @@ +//! A crate for encapsulating the [OpenAPI +//! specification](https://swagger.io/specification/). + +#![warn(missing_docs)] +#![warn(rust_2018_idioms)] +#![warn(rust_2021_compatibility)] +#![warn(missing_debug_implementations)] +#![deny(rustdoc::broken_intra_doc_links)] + +mod api; + +pub use api::Api; diff --git a/packages/ccdi-server/Cargo.toml b/packages/ccdi-server/Cargo.toml new file mode 100644 index 0000000..e40e816 --- /dev/null +++ b/packages/ccdi-server/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "ccdi-server" +version = "0.1.0" +license.workspace = true +edition.workspace = true + +[dependencies] +actix-web.workspace = true +ccdi-cde = { path = "../ccdi-cde", version = "0.1.0" } +ccdi-models = { path = "../ccdi-models", version = "0.1.0" } +indexmap.workspace = true +mime.workspace = true +rand.workspace = true +serde.workspace = true +serde_json.workspace = true +utoipa.workspace = true diff --git a/packages/ccdi-server/src/lib.rs b/packages/ccdi-server/src/lib.rs new file mode 100644 index 0000000..f9ac333 --- /dev/null +++ b/packages/ccdi-server/src/lib.rs @@ -0,0 +1,12 @@ +//! A crate for encapsulating the an example Childhood Cancer Data Initiative +//! federation API server along with the definitions for the OpenAPI +//! specification. + +#![warn(missing_docs)] +#![warn(rust_2018_idioms)] +#![warn(rust_2021_compatibility)] +#![warn(missing_debug_implementations)] +#![deny(rustdoc::broken_intra_doc_links)] + +pub mod responses; +pub mod routes; diff --git a/packages/ccdi-server/src/responses.rs b/packages/ccdi-server/src/responses.rs new file mode 100644 index 0000000..705d4db --- /dev/null +++ b/packages/ccdi-server/src/responses.rs @@ -0,0 +1,10 @@ +//! Responses for the server. + +pub mod by; +mod error; +pub mod metadata; +mod subject; + +pub use error::Error; +pub use subject::Subject; +pub use subject::Subjects; diff --git a/packages/ccdi-server/src/responses/by.rs b/packages/ccdi-server/src/responses/by.rs new file mode 100644 index 0000000..ee34b0c --- /dev/null +++ b/packages/ccdi-server/src/responses/by.rs @@ -0,0 +1,3 @@ +//! Responses related to grouping by fields. + +pub mod count; diff --git a/packages/ccdi-server/src/responses/by/count.rs b/packages/ccdi-server/src/responses/by/count.rs new file mode 100644 index 0000000..00dc083 --- /dev/null +++ b/packages/ccdi-server/src/responses/by/count.rs @@ -0,0 +1,51 @@ +//! Responses for grouping by fields and counting them. + +use indexmap::IndexMap; +use serde::Deserialize; +use serde::Serialize; +use utoipa::ToSchema; + +use ccdi_models as models; + +use models::count::Total; + +/// A response for grouping [`Subject`](models::Subject)s by a metadata field +/// and then summing the counts. +#[derive(Debug, Deserialize, Serialize, ToSchema)] +pub struct Subjects { + #[serde(flatten)] + total: Total, + + values: IndexMap, +} + +impl From> for Subjects { + /// Creates a new [`Subjects`] from an [`IndexMap`]. + /// + /// # Examples + /// + /// ``` + /// use indexmap::IndexMap; + /// + /// use ccdi_models as models; + /// use ccdi_server as server; + /// + /// use models::count::Total; + /// use server::responses::by::count::Subjects; + /// + /// let mut map = IndexMap::::new(); + /// map.insert("U".into(), 18); + /// map.insert("F".into(), 37); + /// map.insert("M".into(), 26); + /// map.insert("UNDIFFERENTIATED".into(), 31); + /// + /// let subjects = Subjects::from(map); + /// ``` + fn from(values: IndexMap) -> Self { + let total = values.values().sum::(); + Self { + total: Total::from(total), + values, + } + } +} diff --git a/packages/ccdi-server/src/responses/error.rs b/packages/ccdi-server/src/responses/error.rs new file mode 100644 index 0000000..b209c95 --- /dev/null +++ b/packages/ccdi-server/src/responses/error.rs @@ -0,0 +1,35 @@ +use serde::Deserialize; +use serde::Serialize; +use utoipa::ToSchema; + +/// A response indicating an error from the API. +#[derive(Debug, Deserialize, Serialize, ToSchema)] +#[schema(as = responses::Error)] +pub struct Error { + #[schema(example = "An error occurred.")] + error: String, +} + +impl Error { + /// Creates a new [`Error`]. + /// + /// # Examples + /// + /// ``` + /// use ccdi_server as server; + /// + /// use server::responses::Error; + /// + /// let error = Error::new("This is an error."); + /// + /// let result = serde_json::to_string(&error)?; + /// assert_eq!(&result, "{\"error\":\"This is an error.\"}"); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn new>(error: S) -> Self { + Self { + error: error.into(), + } + } +} diff --git a/packages/ccdi-server/src/responses/metadata.rs b/packages/ccdi-server/src/responses/metadata.rs new file mode 100644 index 0000000..4d6ced2 --- /dev/null +++ b/packages/ccdi-server/src/responses/metadata.rs @@ -0,0 +1,24 @@ +//! Responses related to metadata fields. + +use serde::Deserialize; +use serde::Serialize; +use utoipa::ToSchema; + +use ccdi_models as models; + +use models::metadata::field::Description; + +/// A response for describing metadata fields for a subject. +#[derive(Debug, Deserialize, Serialize, ToSchema)] +#[schema(as = responses::metadata::FieldDescriptions)] +pub struct FieldDescriptions { + /// Field descriptions. + #[schema(value_type = Vec)] + fields: Vec, +} + +impl From> for FieldDescriptions { + fn from(fields: Vec) -> Self { + Self { fields } + } +} diff --git a/packages/ccdi-server/src/responses/subject.rs b/packages/ccdi-server/src/responses/subject.rs new file mode 100644 index 0000000..71c63fb --- /dev/null +++ b/packages/ccdi-server/src/responses/subject.rs @@ -0,0 +1,36 @@ +use serde::Deserialize; +use serde::Serialize; +use utoipa::ToSchema; + +use ccdi_models as models; + +use models::count::Total; + +/// A response representing a single [`Subject`](models::Subject). +#[derive(Debug, Deserialize, Serialize, ToSchema)] +#[serde(transparent)] +#[schema(as = responses::Subject)] +pub struct Subject(models::Subject); + +/// A response representing multiple subjects known about by the server with a +/// summarized total count. +#[derive(Debug, Deserialize, Serialize, ToSchema)] +#[schema(as = responses::Subjects)] +pub struct Subjects { + /// The total number of subjects in this response. + #[schema(value_type = models::count::Total)] + count: Total, + + /// The subjects, if available. + #[serde(skip_serializing_if = "Option::is_none")] + subjects: Option>, +} + +impl From> for Subjects { + fn from(subjects: Vec) -> Self { + Self { + count: subjects.len().into(), + subjects: Some(subjects), + } + } +} diff --git a/packages/ccdi-server/src/routes.rs b/packages/ccdi-server/src/routes.rs new file mode 100644 index 0000000..b9ced73 --- /dev/null +++ b/packages/ccdi-server/src/routes.rs @@ -0,0 +1,4 @@ +//! Routing. + +pub mod metadata; +pub mod subject; diff --git a/packages/ccdi-server/src/routes/metadata.rs b/packages/ccdi-server/src/routes/metadata.rs new file mode 100644 index 0000000..4af7102 --- /dev/null +++ b/packages/ccdi-server/src/routes/metadata.rs @@ -0,0 +1,39 @@ +//! Routes related to metadata. + +use actix_web::get; +use actix_web::web::ServiceConfig; +use actix_web::HttpResponse; +use actix_web::Responder; + +use ccdi_cde as cde; +use ccdi_models as models; + +use models::metadata::field::description::r#trait::Description as _; + +use crate::responses::metadata::FieldDescriptions; + +/// Configures the [`ServiceConfig`] with the metadata paths. +pub fn configure() -> impl FnOnce(&mut ServiceConfig) { + |config: &mut ServiceConfig| { + config.service(metadata_fields_subject); + } +} + +/// Gets the metadata fields for subjects that are supported by this server. +#[utoipa::path( + get, + path = "/metadata/fields/subject", + tag = "Metadata", + responses( + (status = 200, description = "Successful operation.", body = [responses::Subjects]) + ) +)] +#[get("/metadata/fields/subject")] +pub async fn metadata_fields_subject() -> impl Responder { + HttpResponse::Ok().json(FieldDescriptions::from(vec![ + cde::v1::Sex::description(), + cde::v1::Race::description(), + cde::v2::Ethnicity::description(), + cde::v1::Identifier::description(), + ])) +} diff --git a/packages/ccdi-server/src/routes/subject.rs b/packages/ccdi-server/src/routes/subject.rs new file mode 100644 index 0000000..8f5c0ac --- /dev/null +++ b/packages/ccdi-server/src/routes/subject.rs @@ -0,0 +1,206 @@ +//! Routes related to subjects. + +use std::sync::Mutex; + +use actix_web::get; +use actix_web::web::Data; +use actix_web::web::Path; +use actix_web::web::ServiceConfig; +use actix_web::HttpResponse; +use actix_web::Responder; +use indexmap::IndexMap; +use models::metadata::field::RacesOrNull; +use models::metadata::field::SexOrNull; +use serde_json::Value; + +use ccdi_cde as cde; +use ccdi_models as models; + +use cde::v1::Identifier; +use models::Subject; + +use crate::responses::by; +use crate::responses::Error; +use crate::responses::Subjects; + +/// A store for [`Subject`]s. +#[derive(Debug)] +pub struct Store { + subjects: Mutex>, +} + +impl Store { + /// Creates a new [`Store`] with randomized [`Subject`]s. + /// + /// # Examples + /// + /// ``` + /// use ccdi_server as server; + /// + /// use server::routes::subject::Store; + /// + /// let store = Store::random(100); + /// ``` + pub fn random(count: usize) -> Self { + Self { + subjects: Mutex::new( + (0..count) + .map(|i| { + // SAFETY: this is manually crafted to never fail, so it can be + // unwrapped. + let identifier = Identifier::parse( + format!("organization:Subject{}", i + 1).as_ref(), + ":", + ) + .unwrap(); + + Subject::random(identifier) + }) + .collect::>(), + ), + } + } +} + +/// Configures the [`ServiceConfig`] with the subject paths. +pub fn configure(store: Data) -> impl FnOnce(&mut ServiceConfig) { + |config: &mut ServiceConfig| { + config + .app_data(store) + .service(index) + .service(show) + .service(subjects_by_count); + } +} + +/// Gets the subjects known by this server. +#[utoipa::path( + get, + path = "/subject", + tag = "Subject", + responses( + (status = 200, description = "Successful operation.", body = responses::Subjects) + ) +)] +#[get("/subject")] +pub async fn index(subjects: Data) -> impl Responder { + let subjects = subjects.subjects.lock().unwrap().clone(); + HttpResponse::Ok().json(Subjects::from(subjects)) +} + +/// Gets the subject matching the provided id (if it exists). +#[utoipa::path( + get, + path = "/subject/{namespace}/{name}", + params( + ( + "namespace", + description = "The namespace portion of the subject identifier.", + ), + ( + "name", + description = "The name portion of the subject identifier." + ) + ), + tag = "Subject", + responses( + (status = 200, description = "Successful operation.", body = responses::Subject), + ( + status = 404, + description = "Not found.", + body = responses::Error, + example = json!(Error::new("Subject with namespace 'foo' and name 'bar' not found.")) + ) + ) +)] +#[get("/subject/{namespace}/{name}")] +pub async fn show(path: Path<(String, String)>, subjects: Data) -> impl Responder { + let subjects = subjects.subjects.lock().unwrap(); + let (namespace, name) = path.into_inner(); + + subjects + .iter() + .find(|subject| subject.id().namespace() == &namespace && subject.id().name() == &name) + .map(|subject| HttpResponse::Ok().json(subject)) + .unwrap_or_else(|| { + HttpResponse::NotFound().json(Error::new(format!( + "Subject with namespace '{}' and name '{}' not found.", + namespace, name + ))) + }) +} + +/// Groups the subjects by the specified metadata field and returns counts. +#[utoipa::path( + get, + path = "/subject/by/{field}/count", + params( + ("field", description = "The field to group by and count."), + ), + tag = "Subject", + responses( + (status = 200, description = "Successful operation.", body = responses::Subject), + ( + status = 422, + description = "Unsupported field.", + body = responses::Error, + example = json!(Error::new("Field 'handedness' is unsupported.")) + ), + ) +)] +#[get("/subject/by/{field}/count")] +pub async fn subjects_by_count(path: Path, subjects: Data) -> impl Responder { + let subjects = subjects.subjects.lock().unwrap().clone(); + let field = path.into_inner(); + + let values = subjects + .iter() + .map(|subject| parse_field(&field, subject)) + .collect::>>(); + + let result = match values { + Some(values) => values + .into_iter() + .map(|value| match value { + Value::Null => String::from("null"), + Value::String(s) => s, + _ => todo!(), + }) + .fold(IndexMap::new(), |mut map, value| { + *map.entry(value).or_insert_with(|| 0usize) += 1; + map + }), + None => { + return HttpResponse::UnprocessableEntity() + .json(Error::new(format!("Field '{}' is not supported.", field))) + } + }; + + HttpResponse::Ok().json(by::count::Subjects::from(result)) +} + +fn parse_field(field: &str, subject: &Subject) -> Option { + match field { + "sex" => match subject.metadata() { + Some(metadata) => match metadata.sex() { + SexOrNull::Unowned(unowned) => Some(Value::String(unowned.value().to_string())), + SexOrNull::Null => Some(Value::Null), + }, + None => None, + }, + "race" => match subject.metadata() { + Some(metadata) => match metadata.race() { + RacesOrNull::MultipleUnowned(unowned) => Some(Value::String( + unowned + .iter() + .map(|race| race.value().to_string()) + .collect::>() + .join(" AND "), + )), + RacesOrNull::Null => Some(Value::Null), + }, + None => None, + }, + _ => None, + } +} diff --git a/packages/ccdi-spec/Cargo.lock b/packages/ccdi-spec/Cargo.lock new file mode 100644 index 0000000..aa09996 --- /dev/null +++ b/packages/ccdi-spec/Cargo.lock @@ -0,0 +1,195 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "hashbrown" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12" + +[[package]] +name = "indexmap" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" +dependencies = [ + "equivalent", + "hashbrown", + "serde", +] + +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + +[[package]] +name = "serde" +version = "1.0.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e422a44e74ad4001bdc8eede9a4570ab52f71190e9c076d14369f38b9200537" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e48d1f918009ce3145511378cf68d613e3b3d9137d67272562080d68a2b32d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.38", +] + +[[package]] +name = "serde_json" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a49e178e4452f45cb61d0cd8cebc1b0fafd3e41929e996cef79aa3aca91f574" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "spec" +version = "0.1.0" +dependencies = [ + "utoipa", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa" + +[[package]] +name = "utoipa" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b208a50ff438dcdc887ea3f2db59530bd2f4bc3d2c70630e4d7ee7a281a1d1b" +dependencies = [ + "indexmap", + "serde", + "serde_json", + "serde_yaml", + "utoipa-gen", +] + +[[package]] +name = "utoipa-gen" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bd516d8879043e081537690bc96c8f17b5a4602c336aecb8f1de89d9d9c7e72" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.38", +] + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" diff --git a/packages/ccdi-spec/Cargo.toml b/packages/ccdi-spec/Cargo.toml new file mode 100644 index 0000000..5d24756 --- /dev/null +++ b/packages/ccdi-spec/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "ccdi-spec" +version = "0.1.0" +license.workspace = true +edition.workspace = true + +[dependencies] +actix-web.workspace = true +env_logger = "0.10.0" +ccdi-openapi = { path = "../ccdi-openapi", version = "0.1.0" } +ccdi-server = { path = "../ccdi-server", version = "0.1.0" } +clap = { version = "4.4.6", features = ["derive"] } +log = "0.4.20" +utoipa.workspace = true +utoipa-swagger-ui.workspace = true diff --git a/packages/ccdi-spec/src/main.rs b/packages/ccdi-spec/src/main.rs new file mode 100644 index 0000000..8d4bc41 --- /dev/null +++ b/packages/ccdi-spec/src/main.rs @@ -0,0 +1,148 @@ +use std::fs::File; +use std::io; +use std::net::Ipv4Addr; +use std::path::PathBuf; + +use actix_web::middleware::Logger; +use actix_web::rt; +use actix_web::web::Data; +use actix_web::App; +use actix_web::HttpServer; +use clap::Parser; +use clap::Subcommand; +use log::LevelFilter; +use utoipa::OpenApi; +use utoipa_swagger_ui::SwaggerUi; + +use ccdi_openapi as api; +use ccdi_server as server; + +use api::Api; +use server::routes::metadata; +use server::routes::subject; +use server::routes::subject::Store; + +const ERROR_EXIT_CODE: i32 = 1; + +/// An error related to the main program. +#[derive(Debug)] +pub enum Error { + /// A file already exists at the specified location. + FileExists(PathBuf), + + /// An input/output error. + IoError(io::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::FileExists(path) => write!(f, "file already exists: {}", path.display()), + Error::IoError(err) => write!(f, "i/o error: {err}"), + } + } +} + +impl std::error::Error for Error {} + +#[derive(Debug, Parser)] +pub struct GenerateArgs { + /// A path to write the output to. + #[arg(short = 'o')] + output: Option, + + /// Whether to force the output file to be overwritten (if it exists). + #[arg(short, long)] + force: bool, +} + +#[derive(Debug, Parser)] +pub struct ServeArgs { + /// Number of subjects for the server to generate. + #[arg(short = 'n', default_value_t = 100)] + number_of_subjects: usize, + + /// A path to write the output to. + #[arg(short = 'p', default_value_t = 8000)] + port: u16, +} + +#[derive(Debug, Subcommand)] +pub enum Command { + /// Generate the OpenAPI specification as YAML. + Generate(GenerateArgs), + + /// Runs the test server. + Serve(ServeArgs), +} + +// A program to prepare the Childhood Cancer Data Initiative OpenAPI +/// specification from (a) a base specification and (b) an autogenerated +/// specification using TypeScript. +#[derive(Parser, Debug)] +#[command(author, version, about, long_about)] +struct Args { + /// The subcommand to execute. + #[command(subcommand)] + command: Command, +} + +fn get_output(path: Option, force: bool) -> Result, Error> { + match path { + Some(path) => { + if !force && path.exists() { + return Err(Error::FileExists(path)); + } + + File::create(path) + .map(|file| Box::new(file) as Box) + .map_err(Error::IoError) + } + None => Ok(Box::new(std::io::stdout())), + } +} + +fn inner() -> Result<(), Box> { + let args = Args::parse(); + + env_logger::builder() + .filter_level(LevelFilter::Debug) + .init(); + + match args.command { + Command::Generate(args) => { + let api = Api::openapi(); + let mut writer = get_output(args.output, args.force)?; + write!(writer, "{}", api.to_yaml()?)?; + } + Command::Serve(args) => { + rt::System::new().block_on( + HttpServer::new(move || { + let data = Data::new(Store::random(args.number_of_subjects)); + App::new() + .wrap(Logger::default()) + .configure(subject::configure(data)) + .configure(metadata::configure()) + .service( + SwaggerUi::new("/swagger-ui/{_:.*}") + .url("/api-docs/openapi.json", Api::openapi()), + ) + }) + .bind((Ipv4Addr::UNSPECIFIED, args.port))? + .run(), + )?; + } + } + + Ok(()) +} + +fn main() { + match inner() { + Ok(_) => {} + Err(err) => { + eprintln!("error: {err}"); + std::process::exit(ERROR_EXIT_CODE); + } + } +} From e16e9ce5867b07db6106c985e4551a3d558b797b Mon Sep 17 00:00:00 2001 From: Clay McLeod Date: Sun, 15 Oct 2023 16:29:17 -0500 Subject: [PATCH 2/5] chore: updates the spec to reflect the v0.1 --- swagger.yml | 588 ++++++++++++++++++++++++++++++++++------------------ 1 file changed, 389 insertions(+), 199 deletions(-) diff --git a/swagger.yml b/swagger.yml index a9ce615..1733c63 100644 --- a/swagger.yml +++ b/swagger.yml @@ -1,4 +1,3 @@ ---- openapi: 3.0.3 info: title: CCDI Pediatric Cancer Data Catalog @@ -8,310 +7,501 @@ info: API is to support identification of pediatric cancer samples of interest via a variety of query parameters. contact: + name: Childhood Cancer Data Initiative support email + url: https://www.cancer.gov/research/areas/childhood/childhood-cancer-data-initiative email: NCIChildhoodCancerDataInitiative@mail.nih.gov - version: "0.1" -externalDocs: - description: Learn more about the Childhood Cancer Data Initiative - url: >- - https://www.cancer.gov/research/areas/childhood/childhood-cancer-data-initiative + version: '0.1' servers: - - url: https://ccdi.stjude.cloud/api/v0 - description: St. Jude Children's Research Hospital API server - - url: https://ccdi.treehouse.gi.ucsc.edu/api/v0 - description: Treehouse CCDI API server - - url: https://ccdifederation.pedscommons.org/api/v0 - description: Pediatric Cancer Data Common CCDI API server -tags: - - name: info - description: Information about the API implementation itself. - - name: subject - description: CCDI Metadata Template Subject. +- url: https://ccdi.stjude.cloud/api/v0 + description: St. Jude Children's Research Hospital CCDI API server +- url: https://ccdifederation.pedscommons.org/api/v0 + description: UCSC Treehouse CCDI API server +- url: https://ccdi.treehouse.gi.ucsc.edu/api/v0 + description: Pediatric Cancer Data Commons CCDI API server paths: - /info: + /metadata/fields/subject: get: tags: - - info - summary: Information about the API implementation itself. + - Metadata + summary: Gets the metadata fields for subjects that are supported by this server. + description: Gets the metadata fields for subjects that are supported by this server. + operationId: metadata_fields_subject responses: '200': - description: Successful operation + description: Successful operation. content: application/json: schema: - $ref: '#/components/schemas/Info' - /subject/{id}: + type: array + items: + $ref: '#/components/schemas/responses.Subjects' + /subject: get: tags: - - subject - summary: Find a subject by id. - operationId: retrieveSubject - parameters: - - name: id - in: path - description: Subject to return by Id - required: true - schema: - type: string + - Subject + summary: Gets the subjects known by this server. + description: Gets the subjects known by this server. + operationId: index responses: '200': - description: Successful operation + description: Successful operation. content: application/json: schema: - $ref: '#/components/schemas/Subject' - '400': - description: TBD - '404': - description: TBD - '403': - description: >- - Cohort is too small. - /subject: - # this endpoint name and related attribute and parameters names - TBD + $ref: '#/components/schemas/responses.Subjects' + /subject/{namespace}/{name}: get: tags: - - subject - summary: Find subjects - description: >- - A list of subjecta. - Retrieve all subject or found by attributes. - operationId: searchParticipants + - Subject + summary: Gets the subject matching the provided id (if it exists). + description: Gets the subject matching the provided id (if it exists). + operationId: show parameters: - - name: sex - in: query - description: CDE 6343385 v1.0. - required: false - schema: - type: string - - name: race - in: query - description: CDE 2192199 v1.0. - required: false - schema: - type: string - - name: ethnicity - in: query - description: CDE 2192217 v.2.0. - required: false - schema: - type: string + - name: namespace + in: path + description: The namespace portion of the subject identifier. + required: true + - name: name + in: path + description: The name portion of the subject identifier. + required: true responses: '200': - description: success + description: Successful operation. content: application/json: schema: - $ref: '#/components/schemas/Subjects' - '400': - description: TBD - '403': - description: >- - Cohort is too small. + $ref: '#/components/schemas/responses.Subject' '404': - description: TBD + description: Not found. + content: + application/json: + schema: + $ref: '#/components/schemas/responses.Error' + example: + error: Subject with namespace 'foo' and name 'bar' not found. /subject/by/{field}/count: get: tags: - - subject - summary: A response for grouping by field values and then counting. - operationId: retrieveSubjectsByCount + - Subject + summary: Groups the subjects by the specified metadata field and returns counts. + description: Groups the subjects by the specified metadata field and returns counts. + operationId: subjects_by_count parameters: - - name: field - in: path - description: Field to group by, - required: true - schema: - type: string + - name: field + in: path + description: The field to group by and count. + required: true responses: '200': - description: Successful operation + description: Successful operation. content: application/json: schema: - $ref: '#/components/schemas/SubjectsCounts' - '400': - description: TBD + $ref: '#/components/schemas/responses.Subject' '422': - description: requested field to group by does not exist - /metadata/fields/subject: - get: - tags: - - subject - summary: A listing the metadata fields supported for subjects - operationId: retrieveSubjectsMetadataFields - responses: - '200': - description: Successful operation + description: Unsupported field. content: application/json: schema: - $ref: '#/components/schemas/MetadataFieldsForSubject' - '400': - description: TBD + $ref: '#/components/schemas/responses.Error' + example: + error: Field 'handedness' is unsupported. components: schemas: - Info: + cde.v1.Identifier: type: object + description: |- + **caDSR CDE 6380049 v1.00** + + This metadata element is defined by the caDSR as "A unique subject + identifier within a site and a study.". No permissible values are defined + for this CDE. + + Link: + + required: + - namespace + - name properties: - name: - type: string - example: "Organization A — CCDI Pediatric Cancer Catalog API" - version: + namespace: type: string - example: 1.0.0 - support_email: + description: The namespace of the identifier. + example: organization + name: type: string - example: "support@organization.org" - SubjectKind: + description: The name of the identifier. + example: SubjectName001 + cde.v1.Race: + type: string + description: |- + **caDSR CDE 2192199 v1.00** + + This metadata element is defined by the caDSR as "The text for reporting + information about race based on the Office of Management and Budget (OMB) + categories.". Upon examination of the large number of projects using the + term, it appears to be the preferred term for the general concept of race. + + Link: + + enum: + - Not allowed to collect + - Native Hawaiian or other Pacific Islander + - Not Reported + - Unknown + - American Indian or Alaska Native + - Asian + - Black or African American + - White + cde.v1.Sex: type: string + description: |- + **caDSR CDE 6343385 v1.00** + + This metadata element is defined by the caDSR as "Sex of the subject as + determined by the investigator." In particular, this field does not dictate + the time period: whether it represents sex at birth, sex at sample + collection, or any other determined time point. Further, the descriptions + for F and M suggest that this term can represent either biological sex, + culture gender roles, or both. Thus, this field cannot be assumed to + strictly represent biological sex. + + Link: + enum: - - 'Participant' - - 'Patient Derived Xenograft' - - 'Cell Line' - - 'Organoid' - # CDE ID here - MetadataField: + - U + - F + - M + - UNDIFFERENTIATED + cde.v2.Ethnicity: + type: string + description: |- + **caDSR CDE 2192217 v2.00** + + This metadata element is defined by the caDSR as "The text for reporting + information about ethnicity based on the Office of Management and Budget + (OMB) categories." Upon examination of the large number of projects using + the term, it appears to be the preferred term for the general concept of + ethnicity. + + Link: + + enum: + - Not allowed to collect + - Hispanic or Latino + - Not Hispanic or Latino + - Unknown + - Not reported + field.Ethnicity: type: object + required: + - value properties: value: - type: string + $ref: '#/components/schemas/cde.v2.Ethnicity' ancestors: type: array items: type: string + description: |- + The ancestors from which this field was derived. + + Ancestors should be provided as period (`.`) delimited paths + from the `metadata` key in the subject response object. nullable: true comment: type: string + description: A free-text comment field. nullable: true - required: - - value - OwnedMetadataField: - allOf: # Combines the MetadataField and the inline model - - $ref: '#/components/schemas/MetadataField' - - type: object - properties: - owned: - type: boolean - nullable: true - UnharmonizedMetadataFields: - type: object - additionalProperties: - $ref: '#/components/schemas/MetadataField' - # [key: string]: MetadataField - SubjectMetadata: + field.EthnicityOrNull: + oneOf: + - $ref: '#/components/schemas/field.Ethnicity' + - type: object + default: null + nullable: true + description: An unowned metadata field or null. + field.Identifier: type: object + required: + - value properties: - sex: - $ref: '#/components/schemas/MetadataField' - # CDE 6343385 v1.0 Required. - race: + value: + $ref: '#/components/schemas/cde.v1.Identifier' + ancestors: type: array items: - $ref: '#/components/schemas/MetadataField' - # CDE 2192199 v1.0 - ethnicity: - $ref: '#/components/schemas/MetadataField' - # CDE 2192217 v.2.0. - identifiers: + type: string + description: |- + The ancestors from which this field was derived. + + Ancestors should be provided as period (`.`) delimited paths + from the `metadata` key in the subject response object. + nullable: true + comment: + type: string + description: A free-text comment field. + nullable: true + owned: + type: boolean + description: Whether or not the field is owned by the source server. + nullable: true + field.IdentifiersOrNull: + oneOf: + - type: array + items: + $ref: '#/components/schemas/field.Identifier' + description: Multiple owned values representing the field(s). + - type: object + default: null + nullable: true + description: Multiple owned metadata fields or null. + field.Race: + type: object + required: + - value + properties: + value: + $ref: '#/components/schemas/cde.v1.Race' + ancestors: type: array items: - $ref: '#/components/schemas/OwnedMetadataField' - description: >- - CDE 6380049 v1.00 - unharmonized: - $ref: '#/components/schemas/UnharmonizedMetadataFields' + type: string + description: |- + The ancestors from which this field was derived. + + Ancestors should be provided as period (`.`) delimited paths + from the `metadata` key in the subject response object. + nullable: true + comment: + type: string + description: A free-text comment field. + nullable: true + field.RacesOrNull: + oneOf: + - type: array + items: + $ref: '#/components/schemas/field.Race' + description: Multiple unowned values representing the field(s). + - type: object + default: null + nullable: true + description: Multiple unowned metadata fields or null. + field.Sex: + type: object required: - - sex - - race - - ethnicity - Subject: + - value + properties: + value: + $ref: '#/components/schemas/cde.v1.Sex' + ancestors: + type: array + items: + type: string + description: |- + The ancestors from which this field was derived. + + Ancestors should be provided as period (`.`) delimited paths + from the `metadata` key in the subject response object. + nullable: true + comment: + type: string + description: A free-text comment field. + nullable: true + field.SexOrNull: + oneOf: + - $ref: '#/components/schemas/field.Sex' + - type: object + default: null + nullable: true + description: An unowned metadata field or null. + models.Subject: type: object + description: A subject. + required: + - id + - name + - kind properties: id: - type: integer - description: >- - ID + $ref: '#/components/schemas/cde.v1.Identifier' name: type: string - description: >- - The primary name or identifier for a subject used - within the source server. This identifier should - ALWAYS be included in the 'identifiers' key under - metadata, should that object exist. + description: |- + The primary name or identifier for a subject used within the source + server. + example: SubjectName001 kind: - $ref: '#/components/schemas/SubjectKind' + $ref: '#/components/schemas/models.subject.Kind' metadata: - $ref: '#/components/schemas/SubjectMetadata' - Subjects: - allOf: # Combines the Subject array and count - - type: object - properties: - counts: - type: integer - - type: array - items: - $ref: '#/components/schemas/Subject' - TotalCounts: + allOf: + - $ref: '#/components/schemas/models.subject.Metadata' + nullable: true + models.count.Total: type: object + description: Total count of some entity as reported alongside an API call. + required: + - total properties: total: type: integer - SubjectsByCountCounts: # Dictionary structure - allOf: # Combines the TotalCounts and Count Dictionary - - $ref: '#/components/schemas/TotalCounts' - - type: object - properties: - values: - $ref: '#/components/schemas/SubjectMapCounts' - SubjectMapCounts: # Dictionary key-value counts - type: object - additionalProperties: - type: integer - SubjectsCounts: - type: object - properties: - counts: - $ref: '#/components/schemas/SubjectsByCountCounts' - HarmonizedMetadataFieldDescr: + description: The total number of entities returned in the API call. + minimum: 0 + models.metadata.field.Description: + oneOf: + - $ref: '#/components/schemas/models.metadata.field.description.Harmonized' + - $ref: '#/components/schemas/models.metadata.field.description.Unharmonized' + description: A description for a metadata field. + models.metadata.field.description.Harmonized: type: object + description: A harmonized metadata field description. + required: + - harmonized + - path + - standard + - url properties: harmonized: type: boolean - example: true + description: |- + Whether or not this field is harmonized across the ecosystem. + + This will always be set to `true`. + default: true path: type: string + description: |- + A comma (`.`) delimited path to the field's location on the `metadata` + objects returned by the various subject endpoints. standard: type: string + description: |- + The proper name of the standard to which this field is harmonized (defined + by the documentation for the CCDI metadata fields). url: type: string - UnharmonizedMetadataFieldDescr: + description: |- + A URL to the CCDI documentation where the definition of this harmonized + field resides. + models.metadata.field.description.Unharmonized: type: object + description: An unharmonized metadata field description. + required: + - harmonized + - path properties: harmonized: type: boolean - example: false + description: |- + Whether or not this field is harmonized across the ecosystem. + + This will always be set to `false`. + default: false name: type: string + description: |- + A display name for this metadata field as _suggested_ by the server (this + is not considered authoritative and can be ignored by the client if it so + chooses). This is mainly to avoid naming collisions of common fields across + servers. nullable: true description: type: string + description: A plain-text description of what the field represents. nullable: true path: type: string - nullable: true + description: |- + A comma (`.`) delimited path to the field's location on the `metadata` + objects returned by the various subject endpoints. standard: type: string + description: |- + If the field is considered harmonized across the federation ecosystem, the + name of the standard to which the field is harmonized. + + If the field is _not_ harmonized across the federation ecosystem, then this + should be [`None`]. nullable: true url: type: string + description: A url that describes more about the metadata field, if available. nullable: true - MetadataFieldsForSubject: + models.subject.Kind: + type: string + description: A kind of [`Subject`](super::Subject). + enum: + - Participant + - Patient Derived Xenograft + - Cell Line + - Organoid + models.subject.Metadata: + type: object + description: Metadata associated with a subject. + required: + - sex + - race + - ethnicity + - identifiers + properties: + sex: + allOf: + - $ref: '#/components/schemas/field.SexOrNull' + nullable: true + race: + allOf: + - $ref: '#/components/schemas/field.RacesOrNull' + nullable: true + ethnicity: + allOf: + - $ref: '#/components/schemas/field.EthnicityOrNull' + nullable: true + identifiers: + allOf: + - $ref: '#/components/schemas/field.IdentifiersOrNull' + nullable: true + responses.Error: + type: object + description: A response indicating an error from the API. + required: + - error + properties: + error: + type: string + example: An error occurred. + responses.Subject: + $ref: '#/components/schemas/models.Subject' + responses.Subjects: type: object + description: |- + A response representing multiple subjects known about by the server with a + summarized total count. + required: + - count + properties: + count: + $ref: '#/components/schemas/models.count.Total' + subjects: + type: array + items: + $ref: '#/components/schemas/models.Subject' + description: The subjects, if available. + nullable: true + responses.metadata.FieldDescriptions: + type: object + description: A response for describing metadata fields for a subject. + required: + - fields properties: fields: type: array items: - anyOf: - - $ref: '#/components/schemas/UnharmonizedMetadataFieldDescr' - - $ref: '#/components/schemas/HarmonizedMetadataFieldDescr' -... + $ref: '#/components/schemas/models.metadata.field.Description' + description: Field descriptions. +tags: +- name: Info + description: Information about the API implementation itself. +- name: Subject + description: Subjects within the CCDI federated ecosystem. +externalDocs: + url: https://www.cancer.gov/research/areas/childhood/childhood-cancer-data-initiative + description: Learn more about the Childhood Cancer Data Initiative From 8e49a5750ad22a55ac54fa95f98b10501ff993a6 Mon Sep 17 00:00:00 2001 From: Clay McLeod Date: Sun, 15 Oct 2023 17:10:35 -0500 Subject: [PATCH 3/5] feat: adds specification linting with Spectral --- .github/workflows/specification.yml | 23 +++++++++++++++++++++++ .spectral.yaml | 1 + 2 files changed, 24 insertions(+) create mode 100644 .github/workflows/specification.yml create mode 100644 .spectral.yaml diff --git a/.github/workflows/specification.yml b/.github/workflows/specification.yml new file mode 100644 index 0000000..4ee2d97 --- /dev/null +++ b/.github/workflows/specification.yml @@ -0,0 +1,23 @@ +name: Specification + +on: + push: + branches: + - main + paths: + - 'swagger.yml' + pull_request: + paths: + - 'swagger.yml' + +jobs: + lint: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 'latest' + - name: Install spectral + run: npm install -g @stoplight/spectral-cli + - run: spectral lint swagger.yml \ No newline at end of file diff --git a/.spectral.yaml b/.spectral.yaml new file mode 100644 index 0000000..1d37769 --- /dev/null +++ b/.spectral.yaml @@ -0,0 +1 @@ +extends: ["spectral:oas"] \ No newline at end of file From b619a72b7051aa12cf7ba2b6d8ca922ed04d1682 Mon Sep 17 00:00:00 2001 From: Clay McLeod Date: Sun, 15 Oct 2023 17:19:16 -0500 Subject: [PATCH 4/5] fix: fixes the linting errors from Spectral --- packages/ccdi-openapi/src/api.rs | 9 +- packages/ccdi-server/src/routes/subject.rs | 6 +- swagger.yml | 99 ++-------------------- 3 files changed, 15 insertions(+), 99 deletions(-) diff --git a/packages/ccdi-openapi/src/api.rs b/packages/ccdi-openapi/src/api.rs index c9013f2..ac5e232 100644 --- a/packages/ccdi-openapi/src/api.rs +++ b/packages/ccdi-openapi/src/api.rs @@ -51,6 +51,10 @@ a variety of query parameters.", ( name = "Subject", description = "Subjects within the CCDI federated ecosystem." + ), + ( + name = "Metadata", + description = "List and describe provided metadata fields." ) ), paths( @@ -86,10 +90,6 @@ a variety of query parameters.", models::subject::Kind, models::subject::Metadata, - models::metadata::field::Description, - models::metadata::field::description::Harmonized, - models::metadata::field::description::Unharmonized, - // Counts. models::count::Total, @@ -97,7 +97,6 @@ a variety of query parameters.", responses::Error, responses::Subject, responses::Subjects, - responses::metadata::FieldDescriptions, )), modifiers(&RemoveLicense) )] diff --git a/packages/ccdi-server/src/routes/subject.rs b/packages/ccdi-server/src/routes/subject.rs index 8f5c0ac..f230cb3 100644 --- a/packages/ccdi-server/src/routes/subject.rs +++ b/packages/ccdi-server/src/routes/subject.rs @@ -94,11 +94,11 @@ pub async fn index(subjects: Data) -> impl Responder { path = "/subject/{namespace}/{name}", params( ( - "namespace", + "namespace" = String, description = "The namespace portion of the subject identifier.", ), ( - "name", + "name" = String, description = "The name portion of the subject identifier." ) ), @@ -135,7 +135,7 @@ pub async fn show(path: Path<(String, String)>, subjects: Data) -> impl R get, path = "/subject/by/{field}/count", params( - ("field", description = "The field to group by and count."), + ("field" = String, description = "The field to group by and count."), ), tag = "Subject", responses( diff --git a/swagger.yml b/swagger.yml index 1733c63..5c91565 100644 --- a/swagger.yml +++ b/swagger.yml @@ -61,10 +61,14 @@ paths: in: path description: The namespace portion of the subject identifier. required: true + schema: + type: string - name: name in: path description: The name portion of the subject identifier. required: true + schema: + type: string responses: '200': description: Successful operation. @@ -92,6 +96,8 @@ paths: in: path description: The field to group by and count. required: true + schema: + type: string responses: '200': description: Successful operation. @@ -346,86 +352,6 @@ components: type: integer description: The total number of entities returned in the API call. minimum: 0 - models.metadata.field.Description: - oneOf: - - $ref: '#/components/schemas/models.metadata.field.description.Harmonized' - - $ref: '#/components/schemas/models.metadata.field.description.Unharmonized' - description: A description for a metadata field. - models.metadata.field.description.Harmonized: - type: object - description: A harmonized metadata field description. - required: - - harmonized - - path - - standard - - url - properties: - harmonized: - type: boolean - description: |- - Whether or not this field is harmonized across the ecosystem. - - This will always be set to `true`. - default: true - path: - type: string - description: |- - A comma (`.`) delimited path to the field's location on the `metadata` - objects returned by the various subject endpoints. - standard: - type: string - description: |- - The proper name of the standard to which this field is harmonized (defined - by the documentation for the CCDI metadata fields). - url: - type: string - description: |- - A URL to the CCDI documentation where the definition of this harmonized - field resides. - models.metadata.field.description.Unharmonized: - type: object - description: An unharmonized metadata field description. - required: - - harmonized - - path - properties: - harmonized: - type: boolean - description: |- - Whether or not this field is harmonized across the ecosystem. - - This will always be set to `false`. - default: false - name: - type: string - description: |- - A display name for this metadata field as _suggested_ by the server (this - is not considered authoritative and can be ignored by the client if it so - chooses). This is mainly to avoid naming collisions of common fields across - servers. - nullable: true - description: - type: string - description: A plain-text description of what the field represents. - nullable: true - path: - type: string - description: |- - A comma (`.`) delimited path to the field's location on the `metadata` - objects returned by the various subject endpoints. - standard: - type: string - description: |- - If the field is considered harmonized across the federation ecosystem, the - name of the standard to which the field is harmonized. - - If the field is _not_ harmonized across the federation ecosystem, then this - should be [`None`]. - nullable: true - url: - type: string - description: A url that describes more about the metadata field, if available. - nullable: true models.subject.Kind: type: string description: A kind of [`Subject`](super::Subject). @@ -486,22 +412,13 @@ components: $ref: '#/components/schemas/models.Subject' description: The subjects, if available. nullable: true - responses.metadata.FieldDescriptions: - type: object - description: A response for describing metadata fields for a subject. - required: - - fields - properties: - fields: - type: array - items: - $ref: '#/components/schemas/models.metadata.field.Description' - description: Field descriptions. tags: - name: Info description: Information about the API implementation itself. - name: Subject description: Subjects within the CCDI federated ecosystem. +- name: Metadata + description: List and describe provided metadata fields. externalDocs: url: https://www.cancer.gov/research/areas/childhood/childhood-cancer-data-initiative description: Learn more about the Childhood Cancer Data Initiative From 5ab618bc07856411a244be576d0dd30050483e4f Mon Sep 17 00:00:00 2001 From: Clay McLeod Date: Sun, 15 Oct 2023 17:25:55 -0500 Subject: [PATCH 5/5] chore: updates the README to include information about the Rust packages --- README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3913f67..a81e320 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,10 @@

+## Developing + +The specification is generated using the [Rust] packages contained with `packages` directory. In particular, [`utoipa`] is used to autogenerate the OpenAPI 3.0 specification. An [Actix Web] server is provided that (a) provides the foundation for `utoipa` to generate the API documentation and (b) provides an example server using fake data. Please refer to the [Learn Rust] guide to learn how to develop using Rust. + ## Contributing ### Development Process @@ -27,4 +31,9 @@ - This repository uses the [Conventional Commit](https://www.conventionalcommits.org/en/v1.0.0/) style for commit messages. Please make sure all commits conform to this style. - This repository, as well as the API itself, are versioned using the latest version of [Semantic Versioning](https://semver.org/). -- All changes will either be squashed and merged or rebased off of the `main` branch—no merge commits are allowed in this repository. \ No newline at end of file +- All changes will either be squashed and merged or rebased off of the `main` branch—no merge commits are allowed in this repository. + +[Actix Web]: https://actix.rs/ +[Learn Rust]: https://www.rust-lang.org/learn +[Rust]: https://www.rust-lang.org/ +[`utoipa`]: https://github.com/juhaku/utoipa \ No newline at end of file