diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 4eaa9ac..d2efd8e 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -94,7 +94,7 @@ jobs: uses: actions/checkout@v4 - name: Typos - uses: crate-ci/typos@v1.18.2 + uses: crate-ci/typos@v1.20.4 msrv: runs-on: ubuntu-latest diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..3e8c5ab --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,23 @@ +name: Release + +permissions: + contents: write + +on: + push: + tags: + - 'v[0-9]+.[0-9]+.[0-9]+*' + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Create Release + uses: taiki-e/create-gh-release-action@v1 + with: + changelog: CHANGELOG.md + branch: main + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..aa446d6 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,18 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.1.0] - 2024-04-05 + +The initial release of `compose_spec`! + +### Features + +- (De)serialize from/to the structure of the Compose specification. +- Values are fully validated and parsed. +- Completely documented. +- Conversion between short and long syntax forms of values. +- Conversion between `std::time::Duration` and the duration string format from the compose-spec. diff --git a/Cargo.lock b/Cargo.lock index 850697a..26f1b4b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "autocfg" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" [[package]] name = "bit-set" @@ -25,15 +25,9 @@ checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" [[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.2" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" [[package]] name = "cfg-if" @@ -52,7 +46,6 @@ dependencies = [ "pomsky-macro", "proptest", "serde", - "serde-untagged", "serde_yaml", "thiserror", "url", @@ -75,15 +68,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" -[[package]] -name = "erased-serde" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55d05712b2d8d88102bc9868020c9e5c7a1f5527c452b9b97450a1d006140ba7" -dependencies = [ - "serde", -] - [[package]] name = "errno" version = "0.3.8" @@ -96,9 +80,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.0.1" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" [[package]] name = "fnv" @@ -144,9 +128,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.3" +version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233cf39063f058ea2caae4091bf4a3ef70a653afbc026f5c4a4135d114e3c177" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", "hashbrown", @@ -164,9 +148,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "lazy_static" @@ -176,9 +160,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.152" +version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "libm" @@ -194,9 +178,9 @@ checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "num-traits" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" dependencies = [ "autocfg", "libm", @@ -240,9 +224,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.78" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" dependencies = [ "unicode-ident", ] @@ -255,7 +239,7 @@ checksum = "31b476131c3c86cb68032fdc5cb6d5a1045e3e42d96b69fa599fd77701e1f5bf" dependencies = [ "bit-set", "bit-vec", - "bitflags 2.4.2", + "bitflags", "lazy_static", "num-traits", "rand", @@ -321,28 +305,19 @@ dependencies = [ "rand_core", ] -[[package]] -name = "redox_syscall" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" -dependencies = [ - "bitflags 1.3.2", -] - [[package]] name = "regex-syntax" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" [[package]] name = "rustix" -version = "0.38.30" +version = "0.38.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca" +checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" dependencies = [ - "bitflags 2.4.2", + "bitflags", "errno", "libc", "linux-raw-sys", @@ -363,34 +338,24 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" +checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" [[package]] name = "serde" -version = "1.0.196" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" dependencies = [ "serde_derive", ] -[[package]] -name = "serde-untagged" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a160535368dfc353348e7eaa299156bd508c60c45a9249725f5f6d370d82a66" -dependencies = [ - "erased-serde", - "serde", -] - [[package]] name = "serde_derive" -version = "1.0.196" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", @@ -399,9 +364,9 @@ dependencies = [ [[package]] name = "serde_yaml" -version = "0.9.31" +version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adf8a49373e98a4c5f0ceb5d05aa7c648d75f63774981ed95b7c7443bbd50c6e" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ "indexmap", "itoa", @@ -412,9 +377,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.48" +version = "2.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687" dependencies = [ "proc-macro2", "quote", @@ -423,31 +388,30 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.9.0" +version = "3.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", "fastrand", - "redox_syscall", "rustix", "windows-sys", ] [[package]] name = "thiserror" -version = "1.0.56" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" +checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.56" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" +checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", @@ -477,9 +441,9 @@ checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" [[package]] name = "unicode-bidi" -version = "0.3.14" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f2528f27a9eb2b21e69c95319b30bd0efd85d09c379741b0f78ea1d86be2416" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" [[package]] name = "unicode-ident" @@ -489,18 +453,18 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" dependencies = [ "tinyvec", ] [[package]] name = "unsafe-libyaml" -version = "0.2.10" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab4c90930b95a82d00dc9e9ac071b4991924390d46cbd0dfe566148667605e4b" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" [[package]] name = "url" @@ -540,9 +504,9 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", @@ -555,42 +519,42 @@ dependencies = [ [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" +checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" [[package]] name = "windows_aarch64_msvc" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" +checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" [[package]] name = "windows_i686_gnu" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" +checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" [[package]] name = "windows_i686_msvc" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" +checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" [[package]] name = "windows_x86_64_gnu" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" +checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" +checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" [[package]] name = "windows_x86_64_msvc" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" +checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" diff --git a/Cargo.toml b/Cargo.toml index 8b87ed6..b0b8d4b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,12 +10,88 @@ readme = "README.md" repository = "https://github.com/k9withabone/compose_spec_rs" rust-version = "1.70" +[workspace.lints.rust] +missing_copy_implementations = "warn" +missing_debug_implementations = "warn" +missing_docs = "warn" +unreachable_pub = "warn" +unstable_features = "deny" +unused_crate_dependencies = "warn" +unused_import_braces = "warn" +unused_lifetimes = "warn" +unused_macro_rules = "warn" +unused_qualifications = "warn" +variant_size_differences = "warn" + [workspace.lints.clippy] pedantic = "warn" +cargo = "warn" + +nursery = { level = "warn", priority = -1 } +# conflicts with `unreachable_pub` +redundant_pub_crate = "allow" + +# restriction lint group +absolute_paths = "warn" +as_conversions = "warn" +assertions_on_result_states = "warn" +clone_on_ref_ptr = "warn" +dbg_macro = "warn" +decimal_literal_representation = "warn" +default_numeric_fallback = "warn" +deref_by_slicing = "warn" +empty_drop = "warn" +empty_enum_variants_with_brackets = "warn" +empty_structs_with_brackets = "warn" +error_impl_error = "warn" +exit = "deny" +format_push_string = "warn" +get_unwrap = "warn" +if_then_some_else_none = "warn" +indexing_slicing = "warn" +infinite_loop = "warn" +integer_division = "warn" +large_include_file = "warn" +map_err_ignore = "warn" +mem_forget = "warn" +min_ident_chars = "warn" +missing_docs_in_private_items = "warn" +mixed_read_write_in_expression = "warn" +mod_module_files = "warn" +multiple_inherent_impl = "warn" +needless_raw_strings = "warn" +panic = "warn" +partial_pub_fields = "warn" +print_stderr = "warn" +print_stdout = "warn" +pub_without_shorthand = "warn" +rc_buffer = "warn" +rc_mutex = "warn" +redundant_type_annotations = "warn" +rest_pat_in_fully_bound_structs = "warn" +same_name_method = "warn" +semicolon_outside_block = "warn" +str_to_string = "warn" +string_add = "warn" +string_lit_chars_any = "warn" +string_slice = "warn" +string_to_string = "warn" +suspicious_xor_used_as_pow = "warn" +tests_outside_test_module = "warn" +todo = "warn" +try_err = "warn" +undocumented_unsafe_blocks = "warn" +unimplemented = "warn" +unnecessary_safety_comment = "warn" +unnecessary_safety_doc = "warn" +unnecessary_self_imports = "warn" +unreachable = "warn" +unwrap_used = "warn" +use_debug = "warn" [workspace.dependencies] compose_spec_macros = { version = "=0.1.0", path = "compose_spec_macros" } -serde = "1" +serde = "1.0.147" serde_yaml = "0.9" [package] @@ -36,11 +112,10 @@ workspace = true [dependencies] compose_spec_macros.workspace = true -indexmap = { version = "2", features = ["serde"] } +indexmap = { version = "2.2.3", features = ["serde"] } ipnet = { version = "2", features = ["serde"] } itoa = "1" serde = { workspace = true, features = ["derive"] } -serde-untagged = "0.1" serde_yaml.workspace = true thiserror = "1.0.28" url = { version = "2.3", features = ["serde"] } diff --git a/README.md b/README.md index f2abf70..16d51b7 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,100 @@ # compose_spec -`compose_spec` is a [Rust] library crate which provides types for (de)serializing from/to the [Compose specification]. +[![Crates.io Version](https://img.shields.io/crates/v/compose_spec?style=flat-square&logo=rust)](https://crates.io/crates/compose_spec) +[![Crates.io MSRV](https://img.shields.io/crates/msrv/compose_spec?style=flat-square&logo=rust)](#minimum-supported-rust-version-msrv) +[![docs.rs](https://img.shields.io/docsrs/compose_spec?style=flat-square&logo=rust)](https://docs.rs/compose_spec) +[![License](https://img.shields.io/crates/l/compose_spec?style=flat-square)](./LICENSE) +[![GitHub Actions CI Workflow Status](https://img.shields.io/github/actions/workflow/status/k9withabone/compose_spec_rs/ci.yaml?branch=main&style=flat-square&logo=github&label=ci)](https://github.com/k9withabone/compose_spec_rs/actions/workflows/ci.yaml?query=branch%3Amain) -## Status +`compose_spec` is a [Rust] library crate for (de)serializing from/to the [Compose specification]. -This library is currently a work-in-progress. +`compose_spec` strives for: + +- Idiomatic Rust 🦀 + - Uses semantically appropriate types from the standard library like `PathBuf` and `Duration`. +- Correctness + - Values are fully validated and parsed. + - Enums are used for fields which conflict with each other. For example, in `services`, `network_mode` and `networks` are combined into `network_config`. +- Ease of use + - Fully documented, though the [documentation] could be fleshed out more with examples and explanations, help in this regard would be appreciated! + - Helpful functions such as conversion between short and long syntax forms of values with multiple representations (e.g. `build` and `ports`). + +See the [documentation] for more details. + +## Examples + +```rust +use compose_spec::{Compose, Service, service::Image}; + +let yaml = "\ +services: + caddy: + image: docker.io/library/caddy:latest + ports: + - 8000:80 + - 8443:443 + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile + - caddy-data:/data +volumes: + caddy-data: +"; + +// Deserialize `Compose` +let compose: Compose = serde_yaml::from_str(yaml)?; + +// Serialize `Compose` +let value = serde_yaml::to_value(&compose)?; + +// Get the `Image` of the "caddy" service +let caddy: Option<&Service> = compose.services.get("caddy"); +let image: &Option = &caddy.unwrap().image; +let image: &Image = image.as_ref().unwrap(); + +assert_eq!(image, "docker.io/library/caddy:latest"); +assert_eq!(image.name(), "docker.io/library/caddy"); +assert_eq!(image.tag(), Some("latest")); +``` + +## Minimum Supported Rust Version (MSRV) + +The minimum version of the Rust compiler `compose_spec` can currently compile with is 1.70, which is tested in CI. +Increasing the MSRV is **not** considered to be a breaking change. + +## Contribution + +Contributions, suggestions, and/or comments are appreciated! Feel free to create an [issue](https://github.com/k9withabone/compose_spec_rs/issues), [discussion](https://github.com/k9withabone/compose_spec_rs/discussions), or [pull request](https://github.com/k9withabone/compose_spec_rs/pulls). +Generally, it is preferable to start a discussion for a feature request or open an issue for reporting a bug before submitting changes with a pull request. + +### Project Layout + +`compose_spec` is composed of two packages set up in a Cargo workspace. The root package, `compose_spec`, is the main library. +The other package, `compose_spec_macros`, located in a directory of the same name, is a procedural macro library used in `compose_spec`. `compose_spec_macros` is not designed to be used outside the `compose_spec` library. + +### Local CI + +If you are submitting code changes in a pull request and would like to run the CI jobs locally, use the following commands: + +- format: `cargo fmt --check --all` +- clippy: `cargo clippy --workspace --tests` +- test: `cargo test --workspace -- --include-ignored` +- doc: `cargo doc --workspace --document-private-items` +- docs-rs: + - Install the nightly Rust toolchain, `rustup toolchain install nightly`. + - Install [cargo-docs-rs](https://github.com/dtolnay/cargo-docs-rs). + - `cargo docs-rs` +- spellcheck: + - Install [typos](https://github.com/crate-ci/typos). + - `typos` +- msrv: + - Install [cargo-msrv](https://github.com/foresterre/cargo-msrv). + - `cargo msrv verify` +- minimal-versions: + - Install the nightly Rust toolchain, `rustup toolchain install nightly`. + - Install [cargo-hack](https://github.com/taiki-e/cargo-hack). + - Install [cargo-minimal-versions](https://github.com/taiki-e/cargo-minimal-versions). + - `cargo minimal-versions check --workspace` + - `cargo minimal-versions test --workspace` ## License @@ -15,4 +105,5 @@ The [Compose specification] itself is licensed under the [Apache License v2.0](h See that project's [LICENSE](https://github.com/compose-spec/compose-spec/blob/master/LICENSE) file for more information. [Compose specification]: https://github.com/compose-spec/compose-spec +[documentation]: https://docs.rs/compose_spec [Rust]: https://www.rust-lang.org/ diff --git a/clippy.toml b/clippy.toml index 54924d9..e22745f 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1,3 +1,5 @@ # Clippy configuration doc-valid-idents = ["SELinux", ".."] +absolute-paths-max-segments = 3 +allowed-idents-below-min-chars = ["s", "f", "v", ".."] diff --git a/compose_spec_macros/Cargo.toml b/compose_spec_macros/Cargo.toml index bc9e69d..2f209ca 100644 --- a/compose_spec_macros/Cargo.toml +++ b/compose_spec_macros/Cargo.toml @@ -8,6 +8,8 @@ readme.workspace = true repository.workspace = true rust-version.workspace = true description = "Procedural macros for use in the compose_spec crate" +keywords = ["compose", "containers", "proc_macro"] +categories = ["development-tools", "development-tools::procedural-macro-helpers"] [lib] proc-macro = true @@ -16,9 +18,9 @@ proc-macro = true workspace = true [dependencies] -proc-macro2 = "1" +proc-macro2 = "1.0.60" quote = "1" -syn = "2" +syn = "2.0.1" [dev-dependencies] serde.workspace = true diff --git a/compose_spec_macros/src/as_short.rs b/compose_spec_macros/src/as_short.rs index deb5771..0318de3 100644 --- a/compose_spec_macros/src/as_short.rs +++ b/compose_spec_macros/src/as_short.rs @@ -12,7 +12,7 @@ use syn::{ /// [`AsShort`](super::as_short()) and [`FromShort`](super::from_short()) derive macro input. /// /// Created with [`Input::from_syn()`] -pub struct Input<'a> { +pub(super) struct Input<'a> { /// Name of the input struct. ident: &'a Ident, @@ -33,7 +33,7 @@ impl<'a> Input<'a> { /// /// Returns an error if the input is not a struct with named fields, a `as_short` attribute has /// an incorrect format or duplicates, or the `#[as_short(short)]` attribute is missing. - pub fn from_syn( + pub(super) fn from_syn( DeriveInput { ident, generics, @@ -89,7 +89,7 @@ impl<'a> Input<'a> { } /// Generate a `AsShort` implementation for the input type. - pub fn impl_as_short(&self) -> TokenStream { + pub(super) fn impl_as_short(&self) -> TokenStream { let Self { ident, generics, @@ -144,7 +144,7 @@ impl<'a> Input<'a> { } /// Generate a [`From`] implementation for the input type. - pub fn impl_from_short(&self) -> TokenStream { + pub(super) fn impl_from_short(&self) -> TokenStream { let Self { ident, generics, @@ -198,6 +198,8 @@ fn option_type(ty: &Type) -> Option<&Type> { if path.segments.len() != 1 { return None; } + // path.segments.len() == 1 + #[allow(clippy::indexing_slicing)] let segment = &path.segments[0]; let PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }) = @@ -209,8 +211,11 @@ fn option_type(ty: &Type) -> Option<&Type> { if args.len() != 1 || segment.ident != "Option" { return None; } + // args.len() == 1 + #[allow(clippy::indexing_slicing)] + let arg = &args[0]; - if let GenericArgument::Type(ty) = &args[0] { + if let GenericArgument::Type(ty) = arg { Some(ty) } else { None @@ -241,19 +246,18 @@ struct ShortField<'a> { impl<'a> ShortField<'a> { /// Create a [`ShortField`]. fn new(ident: &'a Ident, ty: &'a Type) -> Self { - if let Some(ty) = option_type(ty) { + option_type(ty).map_or( Self { ident, ty, - optional: true, - } - } else { - Self { + optional: false, + }, + |ty| Self { ident, ty, - optional: false, - } - } + optional: true, + }, + ) } } diff --git a/compose_spec_macros/src/default.rs b/compose_spec_macros/src/default.rs index 3ddb5cd..805e7f6 100644 --- a/compose_spec_macros/src/default.rs +++ b/compose_spec_macros/src/default.rs @@ -10,7 +10,7 @@ use syn::{ /// [`Default`](super::default()) derive macro input. /// /// Created with [`Input::from_syn()`]. -pub struct Input<'a> { +pub(super) struct Input<'a> { /// Name of the input struct. ident: &'a Ident, @@ -28,7 +28,7 @@ impl<'a> Input<'a> { /// /// Returns an error if the input is not a struct with named fields or a `default` attribute has /// an incorrect format or duplicates. - pub fn from_syn( + pub(super) fn from_syn( DeriveInput { ident, generics, @@ -57,7 +57,7 @@ impl<'a> Input<'a> { } /// Expand the input into a [`Default`] implementation. - pub fn expand(self) -> TokenStream { + pub(super) fn expand(self) -> TokenStream { let Self { ident, generics, @@ -121,10 +121,9 @@ impl<'a> Field<'a> { fn expand(self) -> TokenStream { let Self { default, ident } = self; - if let Some(default) = default { - quote!(#ident: #default) - } else { - quote!(#ident: ::std::default::Default::default()) - } + default.map_or_else( + || quote!(#ident: ::std::default::Default::default()), + |default| quote!(#ident: #default), + ) } } diff --git a/compose_spec_macros/src/lib.rs b/compose_spec_macros/src/lib.rs index 3ad5bcd..3f79cfb 100644 --- a/compose_spec_macros/src/lib.rs +++ b/compose_spec_macros/src/lib.rs @@ -18,6 +18,10 @@ //! [`FromStr`]: std::str::FromStr //! [`Serialize`]: https://docs.rs/serde/latest/serde/trait.Serialize.html +// Undocumented items are the struct fields which are fairly self-explanatory and are not documented +// in `syn` either. +#![allow(clippy::missing_docs_in_private_items)] + use proc_macro::TokenStream; use syn::{parse_macro_input, Error}; @@ -700,3 +704,11 @@ pub fn platforms(input: TokenStream) -> TokenStream { .unwrap_or_else(Error::into_compile_error) .into() } + +#[cfg(test)] +mod tests { + // This is to satisfy the `unused_crate_dependencies` lint. `serde` and `serde_yaml` are used in + // the examples for the proc macros above but nowhere else. + use serde as _; + use serde_yaml as _; +} diff --git a/compose_spec_macros/src/platforms.rs b/compose_spec_macros/src/platforms.rs index 4a75b4b..9c6563c 100644 --- a/compose_spec_macros/src/platforms.rs +++ b/compose_spec_macros/src/platforms.rs @@ -33,7 +33,7 @@ use self::{ /// Input for the [`platforms!`](super::platforms()) macro. /// /// `#![apply_to_all(#(#apply_to_all),*)] #platform #os #arch #parse_error_type #try_from_arch_error_type` -pub struct Input { +pub(super) struct Input { apply_to_all: Vec, platform: Platform, os: Os, @@ -107,7 +107,7 @@ impl Parse for Input { impl Input { /// Expand into `Platform`, `Os`, `Arch`, and `{Os}Arch` enums. - pub fn expand(&self) -> Result { + pub(super) fn expand(&self) -> Result { let Self { apply_to_all, platform, @@ -176,6 +176,7 @@ struct Platform { _semicolon: Token![;], } +#[allow(clippy::multiple_inherent_impl)] impl Prefix { /// Continue parsing the `input` into a [`Platform`]. fn parse_platform(self, input: ParseStream) -> Result { diff --git a/compose_spec_macros/src/platforms/arch.rs b/compose_spec_macros/src/platforms/arch.rs index a8a65aa..cc9db59 100644 --- a/compose_spec_macros/src/platforms/arch.rs +++ b/compose_spec_macros/src/platforms/arch.rs @@ -17,7 +17,7 @@ use super::{concat, impl_traits, kw, prefix::Prefix}; /// Definition of platform architectures. /// /// `pub enum Arch { #(#items),* }` -pub struct Arch { +pub(super) struct Arch { attributes: Vec, visibility: Visibility, _enum: Token![enum], @@ -26,9 +26,10 @@ pub struct Arch { items: Punctuated, } +#[allow(clippy::multiple_inherent_impl)] impl Prefix { /// Continue parsing the `input` into an [`Arch`]. - pub fn parse_arch(self, input: ParseStream) -> Result { + pub(super) fn parse_arch(self, input: ParseStream) -> Result { let Self { attributes, visibility, @@ -54,7 +55,10 @@ impl Arch { /// # Errors /// /// Checks if each OS arch in `os_arch_names` is defined and that no arch is unused. - pub fn to_map<'a>(&self, os_arch_names: impl IntoIterator) -> Result { + pub(super) fn to_map<'a>( + &self, + os_arch_names: impl IntoIterator, + ) -> Result { let map = Map { inner: self .items @@ -104,7 +108,7 @@ impl Arch { /// - [`Display`](std::fmt::Display) /// - [`FromStr`](std::str::FromStr) /// - [`TryFrom<&str>`] - pub fn expand(&self, apply_to_all: &[Attribute], from_str_error: &Type) -> TokenStream { + pub(super) fn expand(&self, apply_to_all: &[Attribute], from_str_error: &Type) -> TokenStream { let Self { attributes, visibility, @@ -151,7 +155,7 @@ impl Arch { /// Map of architecture names to [`Arch`] values. /// /// Created with [`Arch::to_map()`]. -pub struct Map<'a> { +pub(super) struct Map<'a> { inner: HashMap, } @@ -166,7 +170,7 @@ impl<'a> Map<'a> { } /// Match arm(s) for use in `Platform::as_str()`. - pub fn platform_as_str_arms( + pub(super) fn platform_as_str_arms( &self, arch: &str, os: &Ident, @@ -177,7 +181,7 @@ impl<'a> Map<'a> { } /// Match arm(s) for use in [`FromStr`](std::str::FromStr) for `Platform`. - pub fn platform_from_str_arms( + pub(super) fn platform_from_str_arms( &self, arch: &str, os: &Ident, @@ -188,27 +192,27 @@ impl<'a> Map<'a> { } /// Variant for use in defining `Arch` and `{Os}Arch` enums. - pub fn arch_enum_variant(&self, arch: &str) -> TokenStream { + pub(super) fn arch_enum_variant(&self, arch: &str) -> TokenStream { self.get(arch).arch_enum_variant() } /// Match arm(s) for use in `Arch::as_str()` or `{Os}Arch::as_str()`. - pub fn arch_as_str_arms(&self, arch: &str) -> TokenStream { + pub(super) fn arch_as_str_arms(&self, arch: &str) -> TokenStream { self.get(arch).arch_as_str_arms() } /// Match arm(s) for use in [`FromStr`](std::str::FromStr). - pub fn arch_from_str_arms(&self, arch: &str) -> TokenStream { + pub(super) fn arch_from_str_arms(&self, arch: &str) -> TokenStream { self.get(arch).arch_from_str_arms() } /// Match arm for use in [`TryFrom`] for `{Os}Arch`. - pub fn os_arch_try_from_arch_arm(&self, arch: &str) -> TokenStream { + pub(super) fn os_arch_try_from_arch_arm(&self, arch: &str) -> TokenStream { self.get(arch).os_arch_try_from_arch_arm() } /// Match arm for use in [`From<{Os}Arch>`] for `Arch`. - pub fn arch_from_os_arch_arm(&self, arch: &str, os_arch: &Ident) -> TokenStream { + pub(super) fn arch_from_os_arch_arm(&self, arch: &str, os_arch: &Ident) -> TokenStream { self.get(arch).arch_from_os_arch_arm(os_arch) } } @@ -406,9 +410,12 @@ struct Fields { impl Parse for Fields { fn parse(input: ParseStream) -> Result { let content; + let brace = braced!(content in input); + let variants = content.parse()?; + Ok(Self { - _brace: braced!(content in input), - variants: content.parse()?, + _brace: brace, + variants, }) } } diff --git a/compose_spec_macros/src/platforms/os.rs b/compose_spec_macros/src/platforms/os.rs index df5f425..1e64651 100644 --- a/compose_spec_macros/src/platforms/os.rs +++ b/compose_spec_macros/src/platforms/os.rs @@ -17,7 +17,7 @@ use super::{impl_traits, kw, prefix::Prefix, ArchMap, Platform}; /// Definition of platform operating systems and their architectures. /// /// `pub enum Os { #(#items),* }` -pub struct Os { +pub(super) struct Os { attributes: Vec, visibility: Visibility, _enum: Token![enum], @@ -26,9 +26,10 @@ pub struct Os { items: Punctuated, } +#[allow(clippy::multiple_inherent_impl)] impl Prefix { /// Continue parsing the `input` into an [`Os`]. - pub fn parse_os(self, input: ParseStream) -> Result { + pub(super) fn parse_os(self, input: ParseStream) -> Result { let Self { attributes, visibility, @@ -50,7 +51,7 @@ impl Prefix { impl Os { /// Iterator of string literals in all OS arch lists. - pub fn arch_list(&self) -> impl Iterator { + pub(super) fn arch_list(&self) -> impl Iterator { self.items.iter().flat_map(Item::arch_list) } @@ -69,7 +70,7 @@ impl Os { /// - [`From`] /// /// Additionally, `{Os}Arch` enums are generated. - pub fn expand_platform( + pub(super) fn expand_platform( &self, platform: &Platform, apply_to_all: &[Attribute], @@ -160,7 +161,7 @@ impl Os { /// - [`Display`](std::fmt::Display) /// - [`FromStr`](std::str::FromStr) /// - [`TryFrom<&str>`] - pub fn expand(&self, apply_to_all: &[Attribute], from_str_error: &Type) -> TokenStream { + pub(super) fn expand(&self, apply_to_all: &[Attribute], from_str_error: &Type) -> TokenStream { let Self { attributes, visibility, @@ -395,9 +396,12 @@ struct Fields { impl Parse for Fields { fn parse(input: ParseStream) -> Result { let content; + let brace = braced!(content in input); + let arch = content.parse()?; + Ok(Self { - _brace: braced!(content in input), - arch: content.parse()?, + _brace: brace, + arch, }) } } diff --git a/compose_spec_macros/src/platforms/prefix.rs b/compose_spec_macros/src/platforms/prefix.rs index dcc625d..d092a1b 100644 --- a/compose_spec_macros/src/platforms/prefix.rs +++ b/compose_spec_macros/src/platforms/prefix.rs @@ -6,11 +6,11 @@ use syn::{ }; /// Enum definition prefix, e.g. `#[doc = "Platform docs"] pub enum Platform`. -pub struct Prefix { - pub attributes: Vec, - pub visibility: Visibility, - pub enum_token: Token![enum], - pub ident: Ident, +pub(super) struct Prefix { + pub(super) attributes: Vec, + pub(super) visibility: Visibility, + pub(super) enum_token: Token![enum], + pub(super) ident: Ident, } impl Parse for Prefix { @@ -26,7 +26,7 @@ impl Parse for Prefix { impl Prefix { /// Construct a helper for checking that the ident is an expected value. - pub fn peek_ident(&self) -> PeekIdent { + pub(super) fn peek_ident(&self) -> PeekIdent { PeekIdent { ident: &self.ident, comparisons: Vec::with_capacity(3), @@ -37,28 +37,28 @@ impl Prefix { /// Support for checking if the [`Prefix`] ident is an expected value. /// /// Created with [`Prefix::peek_ident()`]. -pub struct PeekIdent<'a> { +pub(super) struct PeekIdent<'a> { ident: &'a Ident, comparisons: Vec<&'static str>, } impl<'a> PeekIdent<'a> { /// Whether the [`Prefix`] ident matches an expected value. - pub fn is(&mut self, ident: &'static str) -> bool { + pub(super) fn is(&mut self, ident: &'static str) -> bool { self.comparisons.push(ident); self.ident == ident } /// Trigger an error at the [`Prefix`] ident location, with a message that the last checked /// ident was a duplicate. - pub fn duplicate(&self) -> Error { + pub(super) fn duplicate(&self) -> Error { let duplicate = self.comparisons.last().copied().unwrap_or_default(); Error::new(self.ident.span(), format_args!("duplicate `{duplicate}`")) } /// Trigger an error at the [`Prefix`] ident location, with a message containing a list of /// checked idents. - pub fn error(self) -> Error { + pub(super) fn error(self) -> Error { let mut message = String::from("expected"); for comparison in self.comparisons { let prefix = if message.is_empty() { diff --git a/compose_spec_macros/src/serde.rs b/compose_spec_macros/src/serde.rs index 2b07f04..9be5f25 100644 --- a/compose_spec_macros/src/serde.rs +++ b/compose_spec_macros/src/serde.rs @@ -7,7 +7,7 @@ use quote::{quote, ToTokens}; use syn::{Attribute, DeriveInput, Generics, Ident, Lifetime, LifetimeParam, LitStr, Result}; /// [`SerializeDisplay`](super::serialize_display()) derive macro input. -pub struct Input { +pub(super) struct Input { expecting: Option, ident: Ident, generics: Generics, @@ -15,7 +15,7 @@ pub struct Input { impl Input { /// Create an [`Input`] from [`DeriveInput`]. - pub fn from_syn( + pub(super) fn from_syn( DeriveInput { attrs, ident, @@ -34,7 +34,7 @@ impl Input { /// [`Display`](std::fmt::Display) implementation. /// /// [`Serialize`]: https://docs.rs/serde/latest/serde/trait.Serialize.html - pub fn impl_serialize_display(self) -> TokenStream { + pub(super) fn impl_serialize_display(self) -> TokenStream { let (impl_generics, type_generics, where_clause) = self.generics.split_for_impl(); let ident = &self.ident; @@ -56,7 +56,7 @@ impl Input { /// [`FromStr`](std::str::FromStr) implementation. /// /// [`Deserialize`]: https://docs.rs/serde/latest/serde/trait.Deserialize.html - pub fn impl_deserialize_from_str(self) -> TokenStream { + pub(super) fn impl_deserialize_from_str(self) -> TokenStream { self.impl_deserialize(quote!(crate::serde::FromStrVisitor)) } @@ -64,7 +64,7 @@ impl Input { /// implementation. /// /// [`Deserialize`]: https://docs.rs/serde/latest/serde/trait.Deserialize.html - pub fn impl_deserialize_try_from_string(self) -> TokenStream { + pub(super) fn impl_deserialize_try_from_string(self) -> TokenStream { self.impl_deserialize(quote!(crate::serde::TryFromStringVisitor)) } diff --git a/src/common.rs b/src/common.rs index 753fdaa..4694bb9 100644 --- a/src/common.rs +++ b/src/common.rs @@ -14,11 +14,14 @@ use std::{ use indexmap::{indexset, IndexMap, IndexSet}; use serde::{ - de::{self, IntoDeserializer}, + de::{ + self, + value::{MapAccessDeserializer, SeqAccessDeserializer}, + IntoDeserializer, MapAccess, SeqAccess, Visitor, + }, ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer, }; -use serde_untagged::UntaggedEnumVisitor; pub use serde_yaml::Value as YamlValue; use thiserror::Error; @@ -80,7 +83,7 @@ impl ItemOrList { /// Returns [`Some`] if a list. #[must_use] - pub fn as_list(&self) -> Option<&IndexSet> { + pub const fn as_list(&self) -> Option<&IndexSet> { if let Self::List(v) = self { Some(v) } else { @@ -161,7 +164,7 @@ impl ListOrMap { /// Return [`Some`] if a list. #[must_use] - pub fn as_list(&self) -> Option<&IndexSet> { + pub const fn as_list(&self) -> Option<&IndexSet> { if let Self::List(v) = self { Some(v) } else { @@ -171,7 +174,7 @@ impl ListOrMap { /// Return [`Some`] if a map. #[must_use] - pub fn as_map(&self) -> Option<&Map> { + pub const fn as_map(&self) -> Option<&Map> { if let Self::Map(v) = self { Some(v) } else { @@ -229,7 +232,7 @@ impl ListOrMap { /// Returns an error if a key is not a valid [`MapKey`]. pub fn into_map_split_on(self, delimiters: &[char]) -> Result { match self { - ListOrMap::List(list) => list + Self::List(list) => list .into_iter() .map(|item| { let (key, value) = item @@ -241,17 +244,33 @@ impl ListOrMap { )) }) .collect(), - ListOrMap::Map(map) => Ok(map), + Self::Map(map) => Ok(map), } } } impl<'de> Deserialize<'de> for ListOrMap { fn deserialize>(deserializer: D) -> Result { - UntaggedEnumVisitor::new() - .seq(|seq| seq.deserialize().map(Self::List)) - .map(|map| map.deserialize().map(Self::Map)) - .deserialize(deserializer) + deserializer.deserialize_any(ListOrMapVisitor) + } +} + +/// [`Visitor`] for deserializing [`ListOrMap`]. +struct ListOrMapVisitor; + +impl<'de> Visitor<'de> for ListOrMapVisitor { + type Value = ListOrMap; + + fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { + formatter.write_str("a sequence of strings or map of strings to optional values") + } + + fn visit_seq>(self, seq: A) -> Result { + IndexSet::deserialize(SeqAccessDeserializer::new(seq)).map(ListOrMap::List) + } + + fn visit_map>(self, map: A) -> Result { + Map::deserialize(MapAccessDeserializer::new(map)).map(ListOrMap::Map) } } @@ -296,13 +315,13 @@ impl Value { /// Returns `true` if the value is a [`String`]. #[must_use] - pub fn is_string(&self) -> bool { + pub const fn is_string(&self) -> bool { matches!(self, Self::String(..)) } /// Returns [`Some`] if the value is a [`String`]. #[must_use] - pub fn as_string(&self) -> Option<&String> { + pub const fn as_string(&self) -> Option<&String> { if let Self::String(v) = self { Some(v) } else { @@ -312,13 +331,13 @@ impl Value { /// Returns `true` if the value is a [`Number`]. #[must_use] - pub fn is_number(&self) -> bool { + pub const fn is_number(&self) -> bool { matches!(self, Self::Number(..)) } /// Returns [`Some`] if the value is a [`Number`]. #[must_use] - pub fn as_number(&self) -> Option<&Number> { + pub const fn as_number(&self) -> Option<&Number> { if let Self::Number(v) = self { Some(v) } else { @@ -328,13 +347,13 @@ impl Value { /// Returns `true` if the value is a [`bool`]. #[must_use] - pub fn is_bool(&self) -> bool { + pub const fn is_bool(&self) -> bool { matches!(self, Self::Bool(..)) } /// Returns [`Some`] if the value is a [`bool`]. #[must_use] - pub fn as_bool(&self) -> Option { + pub const fn as_bool(&self) -> Option { if let Self::Bool(v) = self { Some(*v) } else { @@ -519,7 +538,7 @@ impl Number { /// /// [`UnsignedInt`]: Number::UnsignedInt #[must_use] - pub fn is_unsigned_int(&self) -> bool { + pub const fn is_unsigned_int(&self) -> bool { matches!(self, Self::UnsignedInt(..)) } @@ -527,7 +546,7 @@ impl Number { /// /// [`UnsignedInt`]: Number::UnsignedInt #[must_use] - pub fn as_unsigned_int(&self) -> Option { + pub const fn as_unsigned_int(&self) -> Option { if let Self::UnsignedInt(v) = *self { Some(v) } else { @@ -539,7 +558,7 @@ impl Number { /// /// [`SignedInt`]: Number::SignedInt #[must_use] - pub fn is_signed_int(&self) -> bool { + pub const fn is_signed_int(&self) -> bool { matches!(self, Self::SignedInt(..)) } @@ -547,7 +566,7 @@ impl Number { /// /// [`SignedInt`]: Number::SignedInt #[must_use] - pub fn as_signed_int(&self) -> Option { + pub const fn as_signed_int(&self) -> Option { if let Self::SignedInt(v) = *self { Some(v) } else { @@ -559,7 +578,7 @@ impl Number { /// /// [`Float`]: Number::Float #[must_use] - pub fn is_float(&self) -> bool { + pub const fn is_float(&self) -> bool { matches!(self, Self::Float(..)) } @@ -567,7 +586,7 @@ impl Number { /// /// [`Float`]: Number::Float #[must_use] - pub fn as_float(&self) -> Option { + pub const fn as_float(&self) -> Option { if let Self::Float(v) = *self { Some(v) } else { @@ -709,7 +728,7 @@ impl TryFrom for f64 { } /// Error returned when failing to convert a [`Number`] into another type. -#[derive(Error, Debug, Clone, PartialEq, Eq)] +#[derive(Error, Debug, Clone, Copy, PartialEq, Eq)] pub enum TryFromNumberError { /// Cannot convert from a [`Float`](Number::Float) to an integer. #[error("cannot convert a float to an integer")] @@ -750,13 +769,13 @@ impl StringOrNumber { /// Returns `true` if the value is a [`String`]. #[must_use] - pub fn is_string(&self) -> bool { + pub const fn is_string(&self) -> bool { matches!(self, Self::String(..)) } /// Returns [`Some`] if the value is a [`String`]. #[must_use] - pub fn as_string(&self) -> Option<&String> { + pub const fn as_string(&self) -> Option<&String> { if let Self::String(v) = self { Some(v) } else { @@ -766,13 +785,13 @@ impl StringOrNumber { /// Returns `true` if the value is a [`Number`]. #[must_use] - pub fn is_number(&self) -> bool { + pub const fn is_number(&self) -> bool { matches!(self, Self::Number(..)) } /// Returns [`Some`] if the value is a [`Number`]. #[must_use] - pub fn as_number(&self) -> Option { + pub const fn as_number(&self) -> Option { if let Self::Number(v) = *self { Some(v) } else { @@ -895,7 +914,7 @@ impl Resource { /// Create a [`Resource::External`] with an optional `name`. #[must_use] - pub fn external(name: Option) -> Self { + pub const fn external(name: Option) -> Self { Self::External { name } } @@ -903,7 +922,7 @@ impl Resource { /// /// [`External`]: Resource::External #[must_use] - pub fn is_external(&self) -> bool { + pub const fn is_external(&self) -> bool { matches!(self, Self::External { .. }) } @@ -911,14 +930,14 @@ impl Resource { /// /// [`Compose`]: Resource::Compose #[must_use] - pub fn is_compose(&self) -> bool { + pub const fn is_compose(&self) -> bool { matches!(self, Self::Compose(..)) } /// Returns [`Some`] if the resource is managed by the [`Compose`] implementation. /// /// [`Compose`]: Resource::Compose - pub fn as_compose(&self) -> Option<&T> { + pub const fn as_compose(&self) -> Option<&T> { if let Self::Compose(v) = self { Some(v) } else { @@ -985,7 +1004,7 @@ mod tests { assert_eq!(Value::parse("true"), Value::Bool(true)); assert_eq!(Value::parse("1"), Value::Number(1_u64.into())); assert_eq!(Value::parse("-1"), Value::Number((-1_i64).into())); - assert_eq!(Value::parse("1.23"), Value::Number(1.23.into())); + assert_eq!(Value::parse("1.23"), Value::Number(1.23_f64.into())); assert_eq!( Value::parse("string"), Value::String(String::from("string")), diff --git a/src/common/keys.rs b/src/common/keys.rs index bea56b5..e4fd12f 100644 --- a/src/common/keys.rs +++ b/src/common/keys.rs @@ -45,7 +45,7 @@ impl Identifier { } /// Error returned when attempting to create a [`Identifier`]. -#[derive(Error, Debug, Clone, PartialEq, Eq)] +#[derive(Error, Debug, Clone, Copy, PartialEq, Eq)] pub enum InvalidIdentifierError { /// Empty identifier #[error("identifier cannot be empty")] @@ -101,19 +101,15 @@ impl MapKey { Ok(Self(key.into())) } } - - /// Create a new [`MapKey`] without checking for its constraints. - pub(crate) fn new_unchecked(key: impl Into>) -> Self { - Self(key.into()) - } } /// Error returned when attempting to create a [`MapKey`]. -#[derive(Error, Debug, Clone, PartialEq, Eq)] +#[derive(Error, Debug, Clone, Copy, PartialEq, Eq)] pub enum InvalidMapKeyError { /// Empty map key #[error("map key cannot be empty")] Empty, + /// Map key has multiple lines #[error("map key cannot have multiple lines (newline character `\\n` found)")] MultipleLines, @@ -197,10 +193,10 @@ macro_rules! key_impls { String, Box, &str, - std::borrow::Cow<'_, str>, + ::std::borrow::Cow<'_, str>, } - impl std::str::FromStr for $Ty { + impl ::std::str::FromStr for $Ty { type Err = $Error; fn from_str(s: &str) -> Result { @@ -214,14 +210,14 @@ macro_rules! key_impls { } } - impl std::borrow::Borrow for $Ty { + impl ::std::borrow::Borrow for $Ty { fn borrow(&self) -> &str { self.as_str() } } - impl std::fmt::Display for $Ty { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + impl ::std::fmt::Display for $Ty { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { f.write_str(self.as_str()) } } diff --git a/src/common/short_or_long.rs b/src/common/short_or_long.rs index 41ecdf0..88dfc8f 100644 --- a/src/common/short_or_long.rs +++ b/src/common/short_or_long.rs @@ -1,3 +1,5 @@ +//! The [`ShortOrLong`] type, along with associated implementations and traits. + use std::{ ffi::{OsStr, OsString}, fmt::{self, Formatter}, @@ -43,10 +45,10 @@ use crate::{ #[derive(Serialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] #[serde(untagged)] pub enum ShortOrLong { - /// Short syntax, a single value. + /// Short syntax, a single value or sequence. Short(S), - /// Long syntax, a sequence or map. + /// Long syntax, a map. Long(L), } @@ -64,7 +66,7 @@ impl ShortOrLong { /// /// [`Short`]: Self::Short #[must_use] - pub fn is_short(&self) -> bool { + pub const fn is_short(&self) -> bool { matches!(self, Self::Short(..)) } @@ -72,7 +74,7 @@ impl ShortOrLong { /// /// [`Long`]: Self::Long #[must_use] - pub fn is_long(&self) -> bool { + pub const fn is_long(&self) -> bool { matches!(self, Self::Long(..)) } @@ -80,7 +82,7 @@ impl ShortOrLong { /// /// [`Long`]: Self::Long #[must_use] - pub fn as_long(&self) -> Option<&L> { + pub const fn as_long(&self) -> Option<&L> { if let Self::Long(v) = self { Some(v) } else { @@ -102,6 +104,7 @@ where /// Trait for types that represent a long syntax which could also be represented in a short syntax. pub trait AsShort { + /// The short syntax type, returned from [`as_short()`](AsShort::as_short()). type Short: ?Sized; /// Returns [`Some`] if the long syntax can be represented as the short syntax. @@ -245,11 +248,11 @@ macro_rules! impl_long_conversion { } } - impl From> for $t + impl From> for $t where - S: Into<$t>, + S: Into, { - fn from(value: ShortOrLong) -> Self { + fn from(value: ShortOrLong) -> Self { match value { ShortOrLong::Short(short) => short.into(), ShortOrLong::Long(long) => long, @@ -276,13 +279,13 @@ impl From> for ShortOrLong> { } } -impl From>> for IndexMap +impl From> for IndexMap where S: IntoIterator, K: Hash + Eq, V: Default, { - fn from(value: ShortOrLong>) -> Self { + fn from(value: ShortOrLong) -> Self { match value { ShortOrLong::Short(short) => short .into_iter() @@ -311,13 +314,15 @@ where /// [`de::Visitor`] for deserializing [`ShortOrLong`]. struct Visitor { + /// The type of the [`Short`](ShortOrLong::Short) to deserialize. short: PhantomData, + /// The type of the [`Long`](ShortOrLong::Long) to deserialize. long: PhantomData, } impl Visitor { /// Create a new [`Visitor`]. - fn new() -> Self { + const fn new() -> Self { Self { short: PhantomData, long: PhantomData, diff --git a/src/config.rs b/src/config.rs index 8ac00fc..652c0e9 100644 --- a/src/config.rs +++ b/src/config.rs @@ -95,6 +95,7 @@ enum SourceField { } impl SourceField { + /// [`Source`] field name as a static string slice. const fn as_str(self) -> &'static str { match self { Self::File => "file", diff --git a/src/duration.rs b/src/duration.rs index 11b03a4..72c2a8c 100644 --- a/src/duration.rs +++ b/src/duration.rs @@ -49,10 +49,14 @@ pub fn to_string(duration: Duration) -> String { let mut seconds = duration.as_secs(); + // remainder captured in `seconds` + #[allow(clippy::integer_division)] let hours = seconds / SECONDS_PER_HOUR; push_value(&mut string, hours, "h"); seconds %= SECONDS_PER_HOUR; + // remainder captured in `seconds` + #[allow(clippy::integer_division)] let minutes = seconds / SECONDS_PER_MINUTE; push_value(&mut string, minutes, "m"); seconds %= SECONDS_PER_MINUTE; @@ -61,6 +65,8 @@ pub fn to_string(duration: Duration) -> String { let mut microseconds = duration.subsec_micros(); + // remainder captured in `microseconds` + #[allow(clippy::integer_division)] let milliseconds = microseconds / MICROSECONDS_PER_MILLISECOND; push_value(&mut string, milliseconds.into(), "ms"); microseconds %= MICROSECONDS_PER_MILLISECOND; @@ -177,6 +183,7 @@ pub enum ParseDurationError { } #[cfg(test)] +#[allow(clippy::unwrap_used)] pub(crate) mod tests { use proptest::{prop_assert_eq, prop_compose, proptest}; @@ -300,6 +307,8 @@ pub(crate) mod tests { prop_compose! { /// [`Duration`]s truncated to whole microseconds. + // Discarding the remainder is the desired behavior. + #[allow(clippy::integer_division)] pub(crate) fn duration_truncated()(secs: u64, micros in ..=(u32::MAX / 1000)) -> Duration { Duration::new(secs, micros * 1000) } diff --git a/src/lib.rs b/src/lib.rs index aa67d9a..5d5c37c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,13 +1,69 @@ -//! Types for (de)serializing from/to the -//! [compose-spec](https://github.com/compose-spec/compose-spec). The types are validated while they -//! are deserialized when possible. +//! `compose_spec` is a library for (de)serializing from/to the [Compose specification]. +//! +//! This library attempts to make interacting with and creating Compose files as idiomatic and +//! correct as possible. +//! +//! - [`PathBuf`]s are used for fields which denote a path. +//! - Enums are used for fields which conflict with each other. +//! - Values are fully parsed and validated when they have a defined format. +//! - Lists that must contain unique values use [`IndexSet`](indexmap::IndexSet), otherwise they are +//! [`Vec`]s. +//! - Strings which represent a span of time are converted to/from +//! [`Duration`](std::time::Duration)s, see the [`duration`] module. //! //! Note that the [`Deserialize`] implementations of many types make use of //! [`Deserializer::deserialize_any()`](::serde::de::Deserializer::deserialize_any). This means that //! you should only attempt to deserialize them from self-describing formats like YAML or JSON. //! -//! Lists that must contain unique values use [`IndexSet`](indexmap::IndexSet) otherwise they are -//! [`Vec`]s. +//! # Examples +//! +//! ``` +//! use compose_spec::{Compose, Service, service::Image}; +//! +//! let yaml = "\ +//! services: +//! caddy: +//! image: docker.io/library/caddy:latest +//! ports: +//! - 8000:80 +//! - 8443:443 +//! volumes: +//! - ./Caddyfile:/etc/caddy/Caddyfile +//! - caddy-data:/data +//! volumes: +//! caddy-data: +//! "; +//! +//! // Deserialize `Compose` +//! let compose: Compose = serde_yaml::from_str(yaml)?; +//! +//! // Serialize `Compose` +//! let value = serde_yaml::to_value(&compose)?; +//! # let yaml: serde_yaml::Value = serde_yaml::from_str(yaml)?; +//! # assert_eq!(value, yaml); +//! +//! // Get the `Image` of the "caddy" service +//! let caddy: Option<&Service> = compose.services.get("caddy"); +//! let image: &Option = &caddy.unwrap().image; +//! let image: &Image = image.as_ref().unwrap(); +//! +//! assert_eq!(image, "docker.io/library/caddy:latest"); +//! assert_eq!(image.name(), "docker.io/library/caddy"); +//! assert_eq!(image.tag(), Some("latest")); +//! # Ok::<(), serde_yaml::Error>(()) +//! ``` +//! +//! # Short or Long Syntax Values +//! +//! Many values within the [Compose specification] can be represented in either a short or long +//! syntax. The enum [`ShortOrLong`] is used to for these values. Conversion from the [`Short`] +//! syntax to the [`Long`] syntax is always possible. The [`AsShort`] trait is used for [`Long`] +//! syntax types which may be represented directly as the [`Short`] syntax type if additional +//! options are not set. +//! +//! [Compose specification]: https://github.com/compose-spec/compose-spec +//! [`Short`]: ShortOrLong::Short +//! [`Long`]: ShortOrLong::Long mod common; pub mod config; @@ -184,7 +240,7 @@ use impl_try_from; macro_rules! impl_from_str { ($($Ty:ident => $Error:ty),* $(,)?) => { $( - impl std::str::FromStr for $Ty { + impl ::std::str::FromStr for $Ty { type Err = $Error; fn from_str(s: &str) -> Result { @@ -197,13 +253,13 @@ macro_rules! impl_from_str { &str, String, Box, - std::borrow::Cow<'_, str>, + ::std::borrow::Cow<'_, str>, } )* }; ($($Ty:ident),* $(,)?) => { $( - impl std::str::FromStr for $Ty { + impl ::std::str::FromStr for $Ty { type Err = std::convert::Infallible; fn from_str(s: &str) -> Result { @@ -211,9 +267,28 @@ macro_rules! impl_from_str { } } - crate::impl_from!($Ty::parse, &str, String, Box, std::borrow::Cow<'_, str>); + crate::impl_from!($Ty::parse, &str, String, Box, ::std::borrow::Cow<'_, str>); )* }; } use impl_from_str; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn full_round_trip() -> serde_yaml::Result<()> { + let yaml = include_str!("test-full.yaml"); + + let compose: Compose = serde_yaml::from_str(yaml)?; + + assert_eq!( + serde_yaml::from_str::(yaml)?, + serde_yaml::to_value(compose)?, + ); + + Ok(()) + } +} diff --git a/src/name.rs b/src/name.rs index 7ceaed6..34ece95 100644 --- a/src/name.rs +++ b/src/name.rs @@ -10,6 +10,9 @@ use thiserror::Error; /// Validated [`Compose`](super::Compose) project name. /// +/// Names cannot be empty, they must start with a lowercase ASCII letter (a-z) or digit (0-9), and +/// must only contain lowercase ASCII letters (a-z), digits (0-9), underscores (_), or dashes (-). +/// /// [compose-spec](https://github.com/compose-spec/compose-spec/blob/master/04-version-and-name.md#name-top-level-element) #[derive( SerializeDisplay, DeserializeTryFromString, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, @@ -21,10 +24,9 @@ impl Name { /// /// # Errors /// - /// Returns an error if the given string is not a valid [`Name`]. - /// Names cannot be empty, the first character must be a lowercase ASCII letter (a-z) - /// or a digit (0-9), and all other characters must be a lowercase ASCII letter (a-z), - /// a digit (0-9), an underscore (_), or a dash (-). + /// Returns an error if the given string is not a valid [`Name`]. Names cannot be empty, they + /// must start with a lowercase ASCII letter (a-z) or digit (0-9), and must only contain + /// lowercase ASCII letters (a-z), digits (0-9), underscores (_), or dashes (-). pub fn new(name: T) -> Result where T: AsRef + Into>, @@ -35,11 +37,11 @@ impl Name { // pattern from schema: "^[a-z0-9][a-z0-9_-]*$" if !matches!(first, 'a'..='z' | '0'..='9') { - return Err(InvalidNameError::InvalidFirstChar(first)); + return Err(InvalidNameError::Start(first)); } for char in chars { if !matches!(char, 'a'..='z' | '0'..='9' | '_' | '-') { - return Err(InvalidNameError::InvalidChar(char)); + return Err(InvalidNameError::Character(char)); } } @@ -56,23 +58,30 @@ impl Name { } /// Error returned when attempting to create a [`Name`]. -#[derive(Error, Debug, Clone, PartialEq, Eq)] +#[derive(Error, Debug, Clone, Copy, PartialEq, Eq)] pub enum InvalidNameError { - /// Empty name + /// Empty name. #[error("name cannot be empty")] Empty, - /// First character is invalid + + /// Invalid start character. + /// + /// Names must start with a lowercase ASCII letter (a-z) or digit (0-9). #[error( - "invalid character `{0}`, first character in name must be \ - a lowercase ASCII letter (a-z) or a digit (0-9)" + "invalid character `{0}`, names must start with \ + a lowercase ASCII letter (a-z) or digit (0-9)" )] - InvalidFirstChar(char), - /// Invalid character + Start(char), + + /// Invalid character. + /// + /// Names must contain only lowercase ASCII letters (a-z), digits (0-9), underscores (_), or + /// dashes (-). #[error( - "invalid character `{0}`, characters in name must be \ - a lowercase ASCII letter (a-z), a digit (0-9), an underscore (_), or a dash (-)" + "invalid character `{0}`, names must contain only \ + lowercase ASCII letters (a-z), digits (0-9), underscores (_), or dashes (-)" )] - InvalidChar(char), + Character(char), } impl TryFrom for Name { diff --git a/src/network.rs b/src/network.rs index 9d8f8a5..3df2563 100644 --- a/src/network.rs +++ b/src/network.rs @@ -21,7 +21,7 @@ impl Resource { /// /// [compose-spec](https://github.com/compose-spec/compose-spec/blob/master/06-networks.md#name) #[must_use] - pub fn name(&self) -> Option<&String> { + pub const fn name(&self) -> Option<&String> { match self { Self::External { name } => name.as_ref(), Self::Compose(network) => network.name.as_ref(), @@ -181,8 +181,8 @@ pub struct Ipam { pub driver: Option, /// IPAM configuration. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub config: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub config: Vec, /// Driver-specific options. #[serde(default, skip_serializing_if = "IndexMap::is_empty")] diff --git a/src/secret.rs b/src/secret.rs index cff4089..fd04c6d 100644 --- a/src/secret.rs +++ b/src/secret.rs @@ -98,6 +98,7 @@ enum SourceField { } impl SourceField { + /// [`Source`] field name as a static string slice. const fn as_str(self) -> &'static str { match self { Self::File => "file", diff --git a/src/serde.rs b/src/serde.rs index c018966..715052a 100644 --- a/src/serde.rs +++ b/src/serde.rs @@ -1,3 +1,5 @@ +//! Utilities for (de)serializing with [`serde`]. + pub(crate) mod display_from_str_option; pub(crate) mod duration_option; pub(crate) mod duration_us_option; @@ -14,10 +16,12 @@ use serde::{ Deserialize, Deserializer, }; +/// Return `true`, for use in `#[serde(default = "default_true")]`. pub(crate) const fn default_true() -> bool { true } +/// Return `true` if `bool` is `true`, for use in `#[serde(skip_serializing_if = "skip_true")]`. #[allow(clippy::trivially_copy_pass_by_ref)] pub(crate) const fn skip_true(bool: &bool) -> bool { *bool @@ -36,18 +40,28 @@ macro_rules! forward_visitor { pub(crate) use forward_visitor; +/// A [`Visitor`] for deserializing enums composed of several basic types. #[derive(Debug)] pub(crate) struct ValueEnumVisitor { + /// Expected value, used in [`Visitor::expecting()`]. expecting: &'static str, + /// Closure to use when deserializing from a [`u64`]. visit_u64: U, + /// Closure to use when deserializing from a [`i64`]. visit_i64: I, + /// Closure to use when deserializing from a [`f64`]. visit_f64: F, + /// Closure to use when deserializing from a [`bool`]. visit_bool: B, + /// Closure to use when deserializing from a [`String`]. visit_string: S, } impl ValueEnumVisitor { - pub fn new(expecting: &'static str) -> Self { + /// Create a new [`ValueEnumVisitor`]. + /// + /// `expecting` should complete the sentence "This Visitor expects to receive ...". + pub(crate) const fn new(expecting: &'static str) -> Self { Self { expecting, visit_u64: (), @@ -60,7 +74,11 @@ impl ValueEnumVisitor { } impl ValueEnumVisitor<(), I, F, B, S> { - pub fn u64 V, V>(self, visit_u64: U) -> ValueEnumVisitor { + /// Set the closure to use when deserializing from a [`u64`]. + pub(crate) fn u64(self, visit_u64: U) -> ValueEnumVisitor + where + U: FnOnce(u64) -> V, + { let Self { expecting, visit_u64: (), @@ -82,7 +100,8 @@ impl ValueEnumVisitor<(), I, F, B, S> { } impl ValueEnumVisitor { - pub fn i64(self, visit_i64: I) -> ValueEnumVisitor + /// Set the closure to use when deserializing from a [`i64`]. + pub(crate) fn i64(self, visit_i64: I) -> ValueEnumVisitor where I: FnOnce(i64) -> V, { @@ -107,7 +126,8 @@ impl ValueEnumVisitor { } impl ValueEnumVisitor { - pub fn f64(self, visit_f64: F) -> ValueEnumVisitor + /// Set the closure to use when deserializing from a [`f64`]. + pub(crate) fn f64(self, visit_f64: F) -> ValueEnumVisitor where F: FnOnce(f64) -> V, { @@ -132,7 +152,8 @@ impl ValueEnumVisitor { } impl ValueEnumVisitor { - pub fn bool(self, visit_bool: B) -> ValueEnumVisitor + /// Set the closure to use when deserializing from a [`bool`]. + pub(crate) fn bool(self, visit_bool: B) -> ValueEnumVisitor where B: FnOnce(bool) -> V, { @@ -157,7 +178,8 @@ impl ValueEnumVisitor { } impl ValueEnumVisitor { - pub fn string(self, visit_string: S) -> ValueEnumVisitor + /// Set the closure to use when deserializing from a [`String`]. + pub(crate) fn string(self, visit_string: S) -> ValueEnumVisitor where S: FnOnce(String) -> V, { @@ -182,7 +204,8 @@ impl ValueEnumVisitor { } impl ValueEnumVisitor { - pub fn deserialize<'de, V, D>(self, deserializer: D) -> Result + /// Alias for `deserializer.deserialize_any(visitor)`. + pub(crate) fn deserialize<'de, V, D>(self, deserializer: D) -> Result where D: Deserializer<'de>, Self: Visitor<'de, Value = V>, @@ -304,9 +327,13 @@ where /// A [`Visitor`] for deserializing a single item or a list. #[derive(Debug)] pub(crate) struct ItemOrListVisitor> { + /// Expected value, used in [`Visitor::expecting()`]. expecting: &'static str, + /// The type of the value to deserialize. value: PhantomData, + /// The type of the item to deserialize. item: PhantomData, + /// The type of the list to deserialize. list: PhantomData, } @@ -315,7 +342,7 @@ impl ItemOrListVisitor { /// /// `expecting` should complete the sentence "This Visitor expects to receive ...", /// the [`Default`] implementation uses "a single value or sequence". - pub fn new(expecting: &'static str) -> Self { + pub(crate) const fn new(expecting: &'static str) -> Self { Self { expecting, value: PhantomData, @@ -337,7 +364,7 @@ where L: Into + Deserialize<'de>, { /// Alias for `deserializer.deserialize_any(visitor)`. - pub fn deserialize>(self, deserializer: D) -> Result { + pub(crate) fn deserialize>(self, deserializer: D) -> Result { deserializer.deserialize_any(self) } } @@ -395,7 +422,9 @@ where /// A [`Visitor`] which deserializes a type using its [`FromStr`] implementation. #[derive(Debug)] pub(crate) struct FromStrVisitor { + /// Expected value, used in [`Visitor::expecting()`]. expecting: &'static str, + /// The type of the value to deserialize. value: PhantomData, } @@ -404,7 +433,7 @@ impl FromStrVisitor { /// /// `expecting` should complete the sentence "This Visitor expects to receive ...", /// the [`Default`] implementation uses "a string". - pub fn new(expecting: &'static str) -> Self { + pub(crate) const fn new(expecting: &'static str) -> Self { Self { expecting, value: PhantomData, @@ -418,7 +447,10 @@ where V::Err: Error, { /// Alias for `deserializer.deserialize_str(visitor)`. - pub fn deserialize<'de, D: Deserializer<'de>>(self, deserializer: D) -> Result { + pub(crate) fn deserialize<'de, D: Deserializer<'de>>( + self, + deserializer: D, + ) -> Result { deserializer.deserialize_str(self) } } @@ -449,7 +481,9 @@ where /// implementations. #[derive(Debug)] pub(crate) struct TryFromStringVisitor { + /// Expected value, used in [`Visitor::expecting()`]. expecting: &'static str, + /// The type of the value to deserialize. value: PhantomData, } @@ -458,7 +492,7 @@ impl TryFromStringVisitor { /// /// `expecting` should complete the sentence "This Visitor expects to receive ...", /// the [`Default`] implementation uses "a string". - pub fn new(expecting: &'static str) -> Self { + pub(crate) const fn new(expecting: &'static str) -> Self { Self { expecting, value: PhantomData, @@ -474,7 +508,10 @@ where for<'a> <&'a str as TryInto>::Error: Error, { /// Alias for `deserializer.deserialize_string(visitor)`. - pub fn deserialize<'de, D: Deserializer<'de>>(self, deserializer: D) -> Result { + pub(crate) fn deserialize<'de, D: Deserializer<'de>>( + self, + deserializer: D, + ) -> Result { deserializer.deserialize_string(self) } } @@ -509,7 +546,9 @@ where /// A [`Visitor`] for deserializing via [`FromStr`] or from a [`u16`]. pub(crate) struct FromStrOrU16Visitor { + /// Expected value, used in [`Visitor::expecting()`]. expecting: &'static str, + /// The type of the value to deserialize. value: PhantomData, } @@ -518,7 +557,7 @@ impl FromStrOrU16Visitor { /// /// `expecting` should complete the sentence "This Visitor expects to receive ...", /// the [`Default`] implementation uses "a string or integer". - pub fn new(expecting: &'static str) -> Self { + pub(crate) const fn new(expecting: &'static str) -> Self { Self { expecting, value: PhantomData, @@ -533,7 +572,10 @@ where V::Err: Error, { /// Alias for `deserializer.deserialize_any(visitor)`. - pub fn deserialize<'de, D: Deserializer<'de>>(self, deserializer: D) -> Result { + pub(crate) fn deserialize<'de, D: Deserializer<'de>>( + self, + deserializer: D, + ) -> Result { deserializer.deserialize_any(self) } } @@ -601,12 +643,17 @@ where de::Error::custom(output) } +/// An [`Iterator`] of [`Error`] sources. +/// +/// Modeled after the unstable `std::error::Source`. #[derive(Debug, Clone)] struct ErrorSources<'a> { + /// The next [`Error`] to return from [`Iterator::next()`]. current: Option<&'a (dyn Error + 'static)>, } impl<'a> ErrorSources<'a> { + /// Create a new [`ErrorSources`] [`Iterator`]. fn new(error: &'a (dyn Error + 'static)) -> Self { Self { current: Some(error), diff --git a/src/serde/display_from_str_option.rs b/src/serde/display_from_str_option.rs index 0ce4e81..0a54377 100644 --- a/src/serde/display_from_str_option.rs +++ b/src/serde/display_from_str_option.rs @@ -9,7 +9,7 @@ use std::{ use serde::{de, Deserialize, Deserializer, Serializer}; -use super::FromStrVisitor; +use super::{error_chain, FromStrVisitor}; /// Serialize an [`Option`]al value using its [`Display`] implementation. pub(crate) fn serialize(value: &Option, serializer: S) -> Result @@ -34,12 +34,15 @@ where deserializer.deserialize_option(Visitor::new()) } +/// [`de::Visitor`] for deserializing [`Option`] using `T`'s [`FromStr`] implementation. struct Visitor { + /// The optional value type to deserialize. value: PhantomData, } impl Visitor { - fn new() -> Self { + /// Create a new [`Visitor`]. + const fn new() -> Self { Self { value: PhantomData } } } @@ -55,26 +58,17 @@ where formatter.write_str("a string or none") } - fn visit_str(self, v: &str) -> Result - where - E: de::Error, - { - v.parse().map(Some).map_err(de::Error::custom) + fn visit_str(self, v: &str) -> Result { + v.parse().map(Some).map_err(error_chain) } - fn visit_some(self, deserializer: D) -> Result - where - D: Deserializer<'de>, - { + fn visit_some>(self, deserializer: D) -> Result { FromStrVisitor::default() .deserialize(deserializer) .map(Some) } - fn visit_none(self) -> Result - where - E: de::Error, - { + fn visit_none(self) -> Result { Ok(None) } } diff --git a/src/service.rs b/src/service.rs index ab81520..216f438 100644 --- a/src/service.rs +++ b/src/service.rs @@ -3,7 +3,6 @@ pub mod blkio_config; pub mod build; mod byte_value; -mod cgroup; mod config_or_secret; mod cpuset; mod credential_spec; @@ -24,6 +23,7 @@ pub mod user_or_group; pub mod volumes; use std::{ + borrow::Cow, fmt::{self, Display, Formatter}, net::IpAddr, ops::Not, @@ -51,7 +51,6 @@ pub use self::{ blkio_config::BlkioConfig, build::Build, byte_value::{ByteValue, ParseByteValueError}, - cgroup::{Cgroup, ParseCgroupError}, config_or_secret::ConfigOrSecret, cpuset::{CpuSet, ParseCpuSetError}, credential_spec::{CredentialSpec, Kind as CredentialSpecKind}, @@ -378,6 +377,12 @@ pub struct Service { #[serde(default, skip_serializing_if = "Not::not")] pub init: bool, + /// IPC isolation mode for the service container. + /// + /// [compose-spec](https://github.com/compose-spec/compose-spec/blob/master/05-services.md#ipc) + #[serde(default, skip_serializing_if = "Option::is_none")] + pub ipc: Option, + /// UTS namespace mode for the service container. /// /// The default is the decision of the container runtime, if supported. @@ -707,13 +712,13 @@ impl Percent { /// Return the inner value. #[must_use] - pub fn into_inner(self) -> u8 { + pub const fn into_inner(self) -> u8 { self.0 } } /// Error returned when trying to convert an integer into a type with a limited range. -#[derive(Error, Debug, Clone, PartialEq, Eq)] +#[derive(Error, Debug, Clone, Copy, PartialEq, Eq)] #[error("value `{value}` is not between {start} and {end}")] pub struct RangeError { /// Value attempted to convert from. @@ -744,6 +749,43 @@ impl PartialEq for Percent { } } +/// [Cgroup](https://man7.org/linux/man-pages/man7/cgroups.7.html) namespace for a [`Service`]'s +/// container to join. +/// +/// [compose-spec](https://github.com/compose-spec/compose-spec/blob/master/05-services.md#cgroup) +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum Cgroup { + /// Run the container in the Container runtime cgroup namespace. + Host, + + /// Run the container in its own private cgroup namespace. + Private, +} + +impl Cgroup { + /// [`Cgroup`] option as a static string slice. + #[must_use] + pub const fn as_str(self) -> &'static str { + match self { + Self::Host => "host", + Self::Private => "private", + } + } +} + +impl AsRef for Cgroup { + fn as_ref(&self) -> &str { + self.as_str() + } +} + +impl Display for Cgroup { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str(self.as_str()) + } +} + /// Override the default command or entrypoint declared by the container image. /// /// [command compose-spec](https://github.com/compose-spec/compose-spec/blob/master/05-services.md#command) @@ -786,7 +828,9 @@ pub type DependsOn = ShortOrLong, IndexMap(ipc: T) -> Result + where + T: AsRef + Into, + { + if ipc.as_ref() == Self::SHAREABLE { + Ok(Self::Shareable) + } else if let Some(service) = ipc.as_ref().strip_prefix(Self::SERVICE_PREFIX) { + service.parse().map(Self::Service).map_err(Into::into) + } else { + Ok(Self::Other(ipc.into())) + } + } + + /// Returns `true` if the IPC isolation mode is [`Shareable`]. + /// + /// [`Shareable`]: Ipc::Shareable + #[must_use] + pub const fn is_shareable(&self) -> bool { + matches!(self, Self::Shareable) + } + + /// Returns `true` if the IPC isolation mode is [`Service`]. + /// + /// [`Service`]: Ipc::Service + #[must_use] + pub const fn is_service(&self) -> bool { + matches!(self, Self::Service(..)) + } + + /// Returns [`Some`] if the IPC isolation mode is [`Service`]. + /// + /// [`Service`]: Ipc::Service + #[must_use] + pub const fn as_service(&self) -> Option<&Identifier> { + if let Self::Service(v) = self { + Some(v) + } else { + None + } + } + + /// Returns `true` if the IPC isolation mode is [`Other`]. + /// + /// [`Other`]: Ipc::Other + #[must_use] + pub const fn is_other(&self) -> bool { + matches!(self, Self::Other(..)) + } + + /// Returns [`Some`] if the IPC isolation mode is [`Other`]. + /// + /// [`Other`]: Ipc::Other + #[must_use] + pub const fn as_other(&self) -> Option<&String> { + if let Self::Other(v) = self { + Some(v) + } else { + None + } + } +} + +impl_from_str!(Ipc => ParseIpcError); + +/// Error returned when [parsing](Ipc::parse()) [`Ipc`] from a string. +#[derive(Error, Debug, Clone, Copy, PartialEq, Eq)] +#[error("error parsing service IPC isolation mode")] +pub struct ParseIpcError(#[from] InvalidIdentifierError); + +impl Display for Ipc { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::Shareable => f.write_str(Self::SHAREABLE), + Self::Service(service) => write!(f, "{}{service}", Self::SERVICE_PREFIX), + Self::Other(other) => f.write_str(other), + } + } +} + +impl From for String { + fn from(value: Ipc) -> Self { + if let Ipc::Other(other) = value { + other + } else { + value.to_string() + } + } +} + +impl From for Cow<'static, str> { + fn from(value: Ipc) -> Self { + if value.is_shareable() { + Ipc::SHAREABLE.into() + } else { + value.to_string().into() + } + } +} + +/// UTS namespace mode for a [`Service`] container. /// /// [compose-spec](https://github.com/compose-spec/compose-spec/blob/master/05-services.md#uts) #[derive(Serialize, Deserialize, Debug, Default, Clone, Copy, PartialEq, Eq)] @@ -1209,16 +1381,24 @@ impl VolumesFrom { T: AsRef + TryInto, T::Error: Into, { - if let Some(volumes_from) = volumes_from.as_ref().strip_suffix(Self::READ_ONLY_SUFFIX) { - volumes_from.parse().map(|source| Self { - source, - read_only: true, + #[allow(clippy::map_unwrap_or)] + volumes_from + .as_ref() + .strip_suffix(Self::READ_ONLY_SUFFIX) + .map(|volumes_from| { + volumes_from.parse().map(|source| Self { + source, + read_only: true, + }) + }) + .unwrap_or_else(|| { + volumes_from + .as_ref() + .strip_suffix(":rw") + .map(str::parse) + .unwrap_or_else(|| VolumesFromSource::parse(volumes_from)) + .map(Into::into) }) - } else if let Some(volumes_from) = volumes_from.as_ref().strip_suffix(":rw") { - volumes_from.parse().map(VolumesFromSource::into) - } else { - VolumesFromSource::parse(volumes_from).map(Into::into) - } } } @@ -1284,11 +1464,12 @@ impl VolumesFromSource { T: AsRef + TryInto, T::Error: Into, { - if let Some(container) = source.as_ref().strip_prefix(Self::CONTAINER_PREFIX) { - container.parse().map(Self::Container) - } else { - source.try_into().map(Self::Service).map_err(Into::into) - } + #[allow(clippy::map_unwrap_or)] + source + .as_ref() + .strip_prefix(Self::CONTAINER_PREFIX) + .map(|container| container.parse().map(Self::Container)) + .unwrap_or_else(|| source.try_into().map(Self::Service).map_err(Into::into)) } } diff --git a/src/service/blkio_config.rs b/src/service/blkio_config.rs index 6ac5646..fa6c0fc 100644 --- a/src/service/blkio_config.rs +++ b/src/service/blkio_config.rs @@ -99,11 +99,14 @@ pub struct Weight(NonZeroU16); impl Weight { /// The default value, 500. - // TODO: Remove unsafe once `Option::expect()` in const is - // [stable](https://github.com/rust-lang/rust/issues/67441). Then, replace it with - // `NonZeroU16::new(500).expect("500 is not zero")` and remove clippy allow above. - // SAFETY: 500 is not zero. - pub const DEFAULT: Self = Self(unsafe { NonZeroU16::new_unchecked(500) }); + // TODO: + // Remove unsafe once `Option::expect()` in const is + // [stable](https://github.com/rust-lang/rust/issues/67441). Then, replace it with + // `NonZeroU16::new(500).expect("500 is not zero")` and remove clippy allow above. + pub const DEFAULT: Self = Self( + // SAFETY: 500 is not zero. + unsafe { NonZeroU16::new_unchecked(500) }, + ); /// Create a new [`Weight`]. /// @@ -124,14 +127,14 @@ impl Weight { /// Return the inner value. #[must_use] - pub fn into_inner(self) -> NonZeroU16 { + pub const fn into_inner(self) -> NonZeroU16 { self.0 } } /// Error returned when attempting to create a [`Weight`] and the number is not between 10 and 1000, /// inclusive. -#[derive(Error, Debug, Clone, PartialEq, Eq)] +#[derive(Error, Debug, Clone, Copy, PartialEq, Eq)] #[error("weights must be between 10 and 1000")] pub struct WeightRangeError { /// Source of the error when converting into a [`NonZeroU16`] fails. diff --git a/src/service/build.rs b/src/service/build.rs index 5c27edf..001987a 100644 --- a/src/service/build.rs +++ b/src/service/build.rs @@ -15,7 +15,7 @@ use serde::{de, Deserialize, Deserializer, Serialize}; use crate::{Extensions, Identifier, ListOrMap, MapKey, ShortOrLong}; pub use self::{ - cache::{Cache, Error as InvalidCacheError, Kind as CacheType, ParseCacheError}, + cache::{Cache, CacheOption, CacheType, InvalidCacheOptionError, ParseCacheError}, context::Context, dockerfile::Dockerfile, network::Network, diff --git a/src/service/build/cache.rs b/src/service/build/cache.rs index 79f151b..b42afb9 100644 --- a/src/service/build/cache.rs +++ b/src/service/build/cache.rs @@ -1,64 +1,89 @@ //! Provides [`Cache`] for the `cache_from` and `cache_to` fields of the long //! [`Build`](super::Build) syntax. -use std::{ - borrow::Cow, - fmt::{self, Display, Formatter}, - str::FromStr, -}; +use std::fmt::{self, Display, Formatter}; -use compose_spec_macros::{DeserializeFromStr, SerializeDisplay}; -use indexmap::{indexmap, IndexMap}; +use compose_spec_macros::{DeserializeTryFromString, SerializeDisplay}; +use indexmap::IndexMap; use thiserror::Error; -use crate::{impl_from_str, InvalidMapKeyError, MapKey}; +use crate::{ + common::key_impls, + impl_from_str, + service::{image::InvalidImageError, Image}, +}; /// Cache options for the `cache_from` and `cache_to` fields of the long [`Build`](super::Build) /// syntax. /// -/// (De)serializes from/to "type=TYPE[,KEY=VALUE[,...]]", or deserializes/parses from an image name, -/// see [`Cache::from_image()`]. +/// (De)serializes from/to an [`Image`] name or "type=TYPE[,KEY=VALUE...]". /// /// [compose-spec](https://github.com/compose-spec/compose-spec/blob/master/build.md#cache_from) -#[derive(SerializeDisplay, DeserializeFromStr, Default, Clone, Debug, PartialEq, Eq)] -#[serde(expecting = "an image name or a string of the format \"type=TYPE[,KEY=VALUE,...]\"")] +#[derive(SerializeDisplay, DeserializeTryFromString, Clone, Debug, PartialEq, Eq)] +#[serde(expecting = "an image name or string in the format \"type=TYPE[,KEY=VALUE,...]\"")] pub struct Cache { - kind: Kind, - options: IndexMap>, + /// The type of the cache. + pub cache_type: CacheType, + /// Cache options. + pub options: IndexMap, } impl Cache { - /// Create a new [`Cache`]. + /// Parse [`Cache`] from a string. + /// + /// The format is `{image}|type={cache_type}[,{key}={value}...]` where `image` is shorthand for + /// `type=registry,ref={image}`. /// /// # Errors /// - /// Returns an error if a key in options fails to convert into a [`MapKey`], - /// or if `cache_type` is [`Registry`](Kind::Registry) and `options` is missing a "ref" option. - pub fn new(cache_type: Kind, options: O) -> Result + /// Returns an error if the string is just an image name and it is not a valid [`Image`], + /// otherwise the string must start with `type=`, its options must be valid [`CacheOption`]s, + /// and if its [`CacheType::Registry`], it must contain a `ref` option with value being a valid + /// [`Image`]. + pub fn parse(cache: T) -> Result where - O: IntoIterator, - K: TryInto, - Error: From, - V: Into>, + T: AsRef + Into, { - let options: IndexMap<_, _> = options - .into_iter() - // .map(|(key, value)| match key.try_into() { - // Ok(key) => Ok((key, value.into())), - // Err(error) => Err(error.into()), - // }) - .map(|(key, value)| key.try_into().map(|key| (key, value.into()))) - .collect::>()?; - - if cache_type.is_registry() { - let ref_option = options.get("ref"); - if ref_option.is_none() || ref_option.is_some_and(|option| option.is_empty()) { - return Err(Error::RegistryMissingRef); - } + if cache.as_ref().contains(',') { + Self::parse_str(cache.as_ref()) + } else { + Image::parse(cache) + .map(Self::from_image) + .map_err(Into::into) } + } + + /// Concrete implementation for [`Cache::parse()`] for string slices. + fn parse_str(cache: &str) -> Result { + // Format is "type=TYPE[,KEY=VALUE[,...]]" + + let mut options = cache.split(','); + + let cache_type = options + .next() + .expect("split has at least one element") + .strip_prefix("type=") + .ok_or(ParseCacheError::TypeFirst)?; + + let mut options: IndexMap = options + .map(|option| { + let (key, value) = option.split_once('=').unwrap_or((option, "")); + Ok((key.parse()?, value.parse()?)) + }) + .collect::>()?; + + let cache_type = match cache_type { + "registry" => options + .shift_remove("ref") + .ok_or(ParseCacheError::MissingRef)? + .0 + .try_into() + .map(CacheType::Registry)?, + other => other.parse().map(CacheType::Other)?, + }; Ok(Self { - kind: cache_type, + cache_type, options, }) } @@ -70,86 +95,111 @@ impl Cache { /// # Examples /// /// ``` - /// use compose_spec::service::build::{Cache, CacheType}; + /// # fn main() -> Result<(), compose_spec::service::build::ParseCacheError> { + /// use indexmap::IndexMap; + /// use compose_spec::service::{build::{Cache, CacheType}, Image}; /// - /// assert_eq!( - /// Cache::from_image("image"), - /// "type=registry,ref=image".parse().unwrap(), - /// ); + /// let image = Image::parse("image")?; + /// let cache = Cache::from_image(image.clone()); + /// + /// assert_eq!(cache,"type=registry,ref=image".parse()?); /// /// assert_eq!( - /// Cache::from_image("image"), - /// Cache::new(CacheType::Registry, [("ref", "image")]).unwrap(), + /// cache, + /// Cache { + /// cache_type: CacheType::Registry(image), + /// options: IndexMap::default(), + /// }, /// ); + /// # Ok(()) } /// ``` #[must_use] - pub fn from_image(image: &str) -> Self { - let key = MapKey::new_unchecked("ref"); - Self { - kind: Kind::Registry, - options: indexmap! { - key => image.into(), - }, - } + pub fn from_image(image: Image) -> Self { + CacheType::from(image).into() } - /// The value of the cache "type" field. - #[doc(alias = "kind", alias = "type")] - #[must_use] - pub fn cache_type(&self) -> &Kind { - &self.kind - } - - /// Cache options. + /// Convert cache into a map of options. + /// + /// Inserts the [`CacheType`] into the options at the beginning. #[must_use] - pub fn options(&self) -> &IndexMap> { - &self.options + pub fn into_options(self) -> IndexMap { + let Self { + cache_type, + mut options, + } = self; + + match cache_type { + CacheType::Registry(image) => { + let (key, value) = CacheOption::pair_from_image(image); + options.shift_insert(0, key, value); + + options.shift_insert( + 0, + CacheOption("type".into()), + CacheOption("registry".into()), + ); + + options + } + CacheType::Other(cache_type) => { + options.shift_insert(0, CacheOption("type".into()), cache_type); + options + } + } } } -/// Error returned when creating a [`Cache`]. -#[derive(Error, Debug, Clone, PartialEq, Eq)] -pub enum Error { - /// [`Registry`](Kind::Registry) cache type given without a corresponding "ref" option. - #[error("caches with type \"registry\" must have a \"ref\" option")] - RegistryMissingRef, - - /// Option keys must be valid [`MapKey`]s. - #[error("invalid option key")] - OptionKey(#[from] InvalidMapKeyError), -} +impl_from_str!(Cache => ParseCacheError); -impl FromStr for Cache { - type Err = ParseCacheError; +/// Error returned when parsing a [`Cache`] from a string. +#[derive(Error, Debug, Clone, PartialEq, Eq)] +pub enum ParseCacheError { + /// Error parsing [`Image`] for [`CacheType::Registry`]. + #[error("error parsing cache image ref")] + Image(#[from] InvalidImageError), - fn from_str(s: &str) -> Result { - // Format is "NAME | type=TYPE[,KEY=VALUE[,...]]", where NAME is an image name. + /// Cache did not start with `type=`. + #[error("cache options must start with `type=` if not an image")] + TypeFirst, - let mut options = s.split(','); - let kind = options.next().expect("Split has at least one element"); + /// Error parsing [`CacheOption`]. + #[error("error parsing cache option")] + CacheOption(#[from] InvalidCacheOptionError), - if let Some(kind) = kind.strip_prefix("type=") { - let options: Vec<_> = options - .map(|option| { - option - .split_once('=') - .filter(|(_, value)| !value.is_empty()) - .ok_or(ParseCacheError::OptionValueMissing) - }) - .collect::>()?; + /// [`CacheType::Registry`] requires a `ref` option. + #[error("cache type `registry` missing required `ref` option")] + MissingRef, +} - Self::new(kind.into(), options).map_err(Into::into) - } else { - Ok(Self::from_image(s)) +impl From for Cache { + fn from(cache_type: CacheType) -> Self { + Self { + cache_type, + options: IndexMap::default(), } } } +impl From for Cache { + fn from(image: Image) -> Self { + Self::from_image(image) + } +} + impl Display for Cache { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - let Self { kind, options } = self; + let Self { + cache_type, + options, + } = self; - write!(f, "type={kind}")?; + if options.is_empty() { + if let CacheType::Registry(image) = cache_type { + return Display::fmt(image, f); + } + } + + Display::fmt(cache_type, f)?; for (key, value) in options { write!(f, ",{key}={value}")?; @@ -159,106 +209,143 @@ impl Display for Cache { } } -/// Error returned when parsing a [`Cache`] from a string. -#[derive(Error, Debug, Clone, PartialEq, Eq)] -pub enum ParseCacheError { - /// Error while creating [`Cache`]. - #[error(transparent)] - Cache(#[from] Error), - - /// An option was missing a value. - #[error("cache options must have a value")] - OptionValueMissing, -} - -/// Cache type, all compose implementations must support the [`Registry`](Kind::Registry) type. +/// [`Cache`] type. +/// +/// The [`Display`] format is `type=registry,ref={image}` or `type={other}`. /// /// [compose-spec](https://github.com/compose-spec/compose-spec/blob/master/build.md#cache_from) -#[doc(alias = "CacheKind")] -#[derive(Default, Clone, Debug, PartialEq, Eq, Hash)] -pub enum Kind { +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[allow(clippy::module_name_repetitions)] +pub enum CacheType { /// Retrieve build cache from an OCI image set by option "ref". - #[default] - Registry, + Registry(Image), /// Some other cache type. - Other(String), + Other(CacheOption), } -impl Kind { - /// [`Self::Registry`] string value. - const REGISTRY: &'static str = "registry"; +impl CacheType { + /// Returns `true` if the cache type is [`Registry`]. + /// + /// [`Registry`]: CacheType::Registry + #[must_use] + pub const fn is_registry(&self) -> bool { + matches!(self, Self::Registry(_)) + } - /// Parse [`CacheType`](Self) from a string. - pub fn parse(cache_kind: T) -> Self - where - T: AsRef + Into, - { - match cache_kind.as_ref() { - Self::REGISTRY => Kind::Registry, - _ => Kind::Other(cache_kind.into()), + /// Returns [`Some`] if the cache type is [`Registry`]. + /// + /// [`Registry`]: CacheType::Registry + #[must_use] + pub const fn as_registry(&self) -> Option<&Image> { + if let Self::Registry(v) = self { + Some(v) + } else { + None } } - /// Returns `true` if the cache type is [`Registry`]. + /// Returns `true` if the cache type is [`Other`]. /// - /// [`Registry`]: Kind::Registry + /// [`Other`]: CacheType::Other #[must_use] - pub fn is_registry(&self) -> bool { - matches!(self, Self::Registry) + pub const fn is_other(&self) -> bool { + matches!(self, Self::Other(..)) } - /// Cache type as a string slice. + /// Returns [`Some`] if the cache type is [`Other`]. + /// + /// [`Other`]: CacheType::Other #[must_use] - pub fn as_str(&self) -> &str { - match self { - Self::Registry => Self::REGISTRY, - Self::Other(kind) => kind, + pub const fn as_other(&self) -> Option<&CacheOption> { + if let Self::Other(v) = self { + Some(v) + } else { + None } } } -impl_from_str!(Kind); - -impl AsRef for Kind { - fn as_ref(&self) -> &str { - self.as_str() +impl From for CacheType { + fn from(image: Image) -> Self { + Self::Registry(image) } } -impl From for String { - fn from(value: Kind) -> Self { - match value { - Kind::Registry => value.as_str().to_owned(), - Kind::Other(value) => value, +impl Display for CacheType { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::Registry(image) => write!(f, "type=registry,ref={image}"), + Self::Other(other) => write!(f, "type={other}"), } } } -impl From for Cow<'static, str> { - fn from(value: Kind) -> Self { - match value { - Kind::Registry => Self::Borrowed(Kind::REGISTRY), - Kind::Other(other) => Self::Owned(other), +/// An option for a [`Cache`]. +/// +/// Cache options cannot be empty or contain whitespace. +#[derive( + SerializeDisplay, DeserializeTryFromString, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, +)] +#[allow(clippy::module_name_repetitions)] +pub struct CacheOption(Box); + +impl CacheOption { + /// Create a new [`CacheOption`]. + /// + /// # Errors + /// + /// Returns an error if the `option` is empty or contains whitespace. + pub fn new(option: T) -> Result + where + T: AsRef + Into>, + { + if option.as_ref().is_empty() { + Err(InvalidCacheOptionError::Empty) + } else if option.as_ref().contains(char::is_whitespace) { + Err(InvalidCacheOptionError::Whitespace) + } else { + Ok(Self(option.into())) } } + + /// Return a pair of cache options appropriate for inserting into a [`Cache`]'s `options` map. + fn pair_from_image(image: Image) -> (Self, Self) { + (Self("ref".into()), image.into()) + } } -impl Display for Kind { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.write_str(self.as_str()) +/// Error returned when creating a new [`CacheOption`]. +#[derive(Error, Debug, Clone, Copy, PartialEq, Eq)] +pub enum InvalidCacheOptionError { + /// Cache options cannot be empty. + #[error("cache options cannot be empty")] + Empty, + + /// Cache options cannot container whitespace. + #[error("cache options cannot contain whitespace")] + Whitespace, +} + +key_impls!(CacheOption => InvalidCacheOptionError); + +impl From for CacheOption { + fn from(value: Image) -> Self { + // Images are never empty or contain whitespace. + Self(value.into_inner().into_boxed_str()) } } #[cfg(test)] +#[allow(clippy::unwrap_used)] mod tests { use super::*; #[test] fn round_trip() { - let cache = Cache::from_image("test"); + let cache = Cache::from_image("image".parse().unwrap()); let string = cache.to_string(); - assert_eq!(string, "type=registry,ref=test"); + assert_eq!(string, "image"); assert_eq!(cache, string.parse().unwrap()); } } diff --git a/src/service/build/context.rs b/src/service/build/context.rs index 971cb6b..0b8586c 100644 --- a/src/service/build/context.rs +++ b/src/service/build/context.rs @@ -58,7 +58,7 @@ impl Context { /// Returns [`Some`] if a path. #[must_use] - pub fn as_path(&self) -> Option<&PathBuf> { + pub const fn as_path(&self) -> Option<&PathBuf> { if let Self::Path(v) = self { Some(v) } else { @@ -68,7 +68,7 @@ impl Context { /// Returns [`Some`] if a URL. #[must_use] - pub fn as_url(&self) -> Option<&Url> { + pub const fn as_url(&self) -> Option<&Url> { if let Self::Url(v) = self { Some(v) } else { @@ -215,6 +215,7 @@ impl Display for Context { } #[cfg(test)] +#[allow(clippy::unwrap_used)] mod tests { use super::*; diff --git a/src/service/build/dockerfile.rs b/src/service/build/dockerfile.rs index dff092c..99b7668 100644 --- a/src/service/build/dockerfile.rs +++ b/src/service/build/dockerfile.rs @@ -45,7 +45,9 @@ impl Dockerfile { #[derive(Deserialize, Debug, Clone, Copy)] #[serde(field_identifier, rename_all = "snake_case")] enum Field { + /// [`Dockerfile::File`] / `dockerfile` Dockerfile, + /// [`Dockerfile::Inline`] / `dockerfile_inline` DockerfileInline, } @@ -123,10 +125,10 @@ pub(super) mod option { /// # Errors /// /// Returns an error if the `serializer` does while serializing. - pub fn serialize(value: &Option, serializer: S) -> Result - where - S: Serializer, - { + pub(in super::super) fn serialize( + value: &Option, + serializer: S, + ) -> Result { value.serialize(serializer) } @@ -136,10 +138,9 @@ pub(super) mod option { /// /// Returns an error if the `deserializer` does, there is an error deserializing either /// [`Dockerfile`] variant, or both fields are present. - pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> - where - D: Deserializer<'de>, - { + pub(in super::super) fn deserialize<'de, D: Deserializer<'de>>( + deserializer: D, + ) -> Result, D::Error> { let DockerfileFlat { dockerfile, dockerfile_inline, @@ -162,14 +163,18 @@ pub(super) mod option { expecting = "a struct with either a `dockerfile` or `dockerfile_inline` field" )] struct DockerfileFlat { + /// [`Dockerfile::File`] #[serde(default)] dockerfile: Option, + + /// [`Dockerfile::Inline`] #[serde(default)] dockerfile_inline: Option, } } #[cfg(test)] +#[allow(clippy::unwrap_used)] mod tests { use super::*; diff --git a/src/service/build/network.rs b/src/service/build/network.rs index 4225569..9d2d6e7 100644 --- a/src/service/build/network.rs +++ b/src/service/build/network.rs @@ -58,7 +58,7 @@ impl Network { /// /// [`None`]: Network::None #[must_use] - pub fn is_none(&self) -> bool { + pub const fn is_none(&self) -> bool { matches!(self, Self::None) } diff --git a/src/service/build/ssh_auth.rs b/src/service/build/ssh_auth.rs index 410ce5d..f053a5d 100644 --- a/src/service/build/ssh_auth.rs +++ b/src/service/build/ssh_auth.rs @@ -28,7 +28,7 @@ pub enum SshAuth { impl SshAuth { /// Returns [`Some`] if [`SshAuth::Id`]. #[must_use] - pub fn as_id(&self) -> Option<&Id> { + pub const fn as_id(&self) -> Option<&Id> { if let Self::Id(v) = self { Some(v) } else { @@ -39,15 +39,25 @@ impl SshAuth { /// The ID of the SSH authentication. /// /// Returns [`Some`] if [`SshAuth::Id`]. - pub fn id(&self) -> Option<&str> { - self.as_id().map(Id::id) + #[must_use] + pub const fn id(&self) -> Option<&str> { + if let Self::Id(v) = self { + Some(v.id()) + } else { + None + } } /// The path of the PEM file or ssh-agent socket. /// /// Returns [`Some`] if [`SshAuth::Id`]. - pub fn path(&self) -> Option<&Path> { - self.as_id().map(Id::path) + #[must_use] + pub const fn path(&self) -> Option<&Path> { + if let Self::Id(v) = self { + Some(v.path()) + } else { + None + } } } @@ -114,13 +124,13 @@ impl Id { /// The ID of the SSH authentication. #[must_use] - pub fn id(&self) -> &str { + pub const fn id(&self) -> &str { &self.id } /// The path of the PEM file or ssh-agent socket. #[must_use] - pub fn path(&self) -> &Path { + pub const fn path(&self) -> &Path { &self.path } } @@ -143,7 +153,7 @@ impl Display for Id { } /// Error returned when creating an [`Id`]. -#[derive(Error, Debug, Clone, PartialEq, Eq)] +#[derive(Error, Debug, Clone, Copy, PartialEq, Eq)] pub enum IdError { /// Given `id` was empty #[error("ssh auth ID cannot be empty")] diff --git a/src/service/byte_value.rs b/src/service/byte_value.rs index 2c90bb1..04f84c6 100644 --- a/src/service/byte_value.rs +++ b/src/service/byte_value.rs @@ -148,6 +148,7 @@ impl<'de> Deserialize<'de> for ByteValue { } } +/// [`de::Visitor`] for deserializing [`ByteValue`] from a [`u64`] or a string. struct Visitor; impl<'de> de::Visitor<'de> for Visitor { @@ -168,6 +169,7 @@ impl<'de> de::Visitor<'de> for Visitor { } #[cfg(test)] +#[allow(clippy::unwrap_used)] mod tests { use serde::de::value::U64Deserializer; diff --git a/src/service/cgroup.rs b/src/service/cgroup.rs deleted file mode 100644 index 733b47d..0000000 --- a/src/service/cgroup.rs +++ /dev/null @@ -1,73 +0,0 @@ -use std::{ - fmt::{self, Display, Formatter}, - str::FromStr, -}; - -use compose_spec_macros::{DeserializeFromStr, SerializeDisplay}; -use thiserror::Error; - -/// [Cgroup](https://man7.org/linux/man-pages/man7/cgroups.7.html) namespace to join. -/// -/// [compose-spec](https://github.com/compose-spec/compose-spec/blob/master/05-services.md#cgroup) -#[derive(SerializeDisplay, DeserializeFromStr, Debug, Clone, Copy, PartialEq, Eq)] -pub enum Cgroup { - /// Run the container in the Container runtime cgroup namespace. - Host, - - /// Run the container in its own private cgroup namespace. - Private, -} - -impl Cgroup { - /// [`Cgroup`] option as a static string slice. - #[must_use] - pub const fn as_str(self) -> &'static str { - match self { - Self::Host => "host", - Self::Private => "private", - } - } -} - -impl AsRef for Cgroup { - fn as_ref(&self) -> &str { - self.as_str() - } -} - -impl Display for Cgroup { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.write_str(self.as_str()) - } -} - -impl From for &'static str { - fn from(value: Cgroup) -> Self { - value.as_str() - } -} - -impl FromStr for Cgroup { - type Err = ParseCgroupError; - - fn from_str(s: &str) -> Result { - match s { - "host" => Ok(Self::Host), - "private" => Ok(Self::Private), - s => Err(ParseCgroupError(s.to_owned())), - } - } -} - -impl TryFrom<&str> for Cgroup { - type Error = ParseCgroupError; - - fn try_from(value: &str) -> Result { - value.parse() - } -} - -/// Error returned when parsing a [`Cgroup`] from a string. -#[derive(Error, Debug, Clone, PartialEq, Eq)] -#[error("invalid cgroup option `{0}`, cgroup must be `host` or `private`")] -pub struct ParseCgroupError(String); diff --git a/src/service/config_or_secret.rs b/src/service/config_or_secret.rs index 0f75bd3..59dd57b 100644 --- a/src/service/config_or_secret.rs +++ b/src/service/config_or_secret.rs @@ -1,3 +1,6 @@ +//! Provides [`ConfigOrSecret`] for the `configs` and `secrets` fields of +//! [`Service`](super::Service) and the `secrets` field of the long [`Build`](super::Build) syntax. + use std::path::PathBuf; use compose_spec_macros::{AsShort, FromShort}; diff --git a/src/service/cpuset.rs b/src/service/cpuset.rs index 7d8baa4..e391d5f 100644 --- a/src/service/cpuset.rs +++ b/src/service/cpuset.rs @@ -128,6 +128,7 @@ impl From for BTreeSet { } #[cfg(test)] +#[allow(clippy::unwrap_used)] mod tests { use proptest::{prop_assert_eq, proptest}; diff --git a/src/service/credential_spec.rs b/src/service/credential_spec.rs index 3a1103b..c48f200 100644 --- a/src/service/credential_spec.rs +++ b/src/service/credential_spec.rs @@ -116,6 +116,7 @@ enum Field { } #[cfg(test)] +#[allow(clippy::unwrap_used)] mod tests { use super::*; diff --git a/src/service/deploy/endpoint_mode.rs b/src/service/deploy/endpoint_mode.rs index 3b57c39..dac2d9c 100644 --- a/src/service/deploy/endpoint_mode.rs +++ b/src/service/deploy/endpoint_mode.rs @@ -55,7 +55,7 @@ impl EndpointMode { /// /// [`VIp`]: EndpointMode::VIp #[must_use] - pub fn is_vip(&self) -> bool { + pub const fn is_vip(&self) -> bool { matches!(self, Self::VIp) } @@ -63,7 +63,7 @@ impl EndpointMode { /// /// [`DnsRR`]: EndpointMode::DnsRR #[must_use] - pub fn is_dnsrr(&self) -> bool { + pub const fn is_dnsrr(&self) -> bool { matches!(self, Self::DnsRR) } diff --git a/src/service/deploy/resources.rs b/src/service/deploy/resources.rs index e74a43c..0d7c2d9 100644 --- a/src/service/deploy/resources.rs +++ b/src/service/deploy/resources.rs @@ -132,7 +132,7 @@ impl Cpus { /// Return the inner value. #[must_use] - pub fn into_inner(self) -> f64 { + pub const fn into_inner(self) -> f64 { self.0 } } @@ -332,7 +332,7 @@ impl Capability { /// /// [`Gpu`]: Capability::Gpu #[must_use] - pub fn is_gpu(&self) -> bool { + pub const fn is_gpu(&self) -> bool { matches!(self, Self::Gpu) } @@ -340,7 +340,7 @@ impl Capability { /// /// [`Tpu`]: Capability::Tpu #[must_use] - pub fn is_tpu(&self) -> bool { + pub const fn is_tpu(&self) -> bool { matches!(self, Self::Tpu) } @@ -465,6 +465,7 @@ impl<'de> Visitor<'de> for CountVisitor { } fn visit_str(self, v: &str) -> Result { + #[allow(clippy::map_err_ignore)] v.parse() .map_err(|_| E::invalid_value(Unexpected::Str(v), &self)) } diff --git a/src/service/device.rs b/src/service/device.rs index a4d17f6..54c3e5d 100644 --- a/src/service/device.rs +++ b/src/service/device.rs @@ -60,7 +60,7 @@ impl TryFrom<&str> for Device { } /// Error returned when parsing a [`Device`] from a string. -#[derive(Error, Debug, Clone, PartialEq, Eq)] +#[derive(Error, Debug, Clone, Copy, PartialEq, Eq)] pub enum ParseDeviceError { /// Given device was an empty string. #[error("device cannot be an empty string")] @@ -109,7 +109,7 @@ pub struct Permissions { impl Permissions { /// Create [`Permissions`] where all fields are `true`. #[must_use] - pub fn all() -> Self { + pub const fn all() -> Self { Self { read: true, write: true, @@ -119,7 +119,7 @@ impl Permissions { /// Returns `true` if any of the permissions are `true`. #[must_use] - pub fn any(self) -> bool { + pub const fn any(self) -> bool { let Self { read, write, mknod } = self; read || write || mknod } @@ -155,7 +155,7 @@ impl TryFrom<&str> for Permissions { } /// Error returned when parsing [`Permissions`] from a string. -#[derive(Error, Debug, Clone, PartialEq, Eq)] +#[derive(Error, Debug, Clone, Copy, PartialEq, Eq)] #[error("invalid device permission `{0}`, must be `r` (read), `w` (write), or `m` (mknod)")] pub struct ParsePermissionsError(char); @@ -296,7 +296,7 @@ pub enum Kind { impl Kind { /// The character the device type corresponds to. #[must_use] - pub fn as_char(self) -> char { + pub const fn as_char(self) -> char { match self { Self::All => 'a', Self::Char => 'c', @@ -411,6 +411,7 @@ impl Display for MajorMinorNumber { } #[cfg(test)] +#[allow(clippy::unwrap_used)] mod tests { use proptest::{ arbitrary::any, diff --git a/src/service/healthcheck.rs b/src/service/healthcheck.rs index 3f15060..9bfd754 100644 --- a/src/service/healthcheck.rs +++ b/src/service/healthcheck.rs @@ -378,6 +378,7 @@ impl Serialize for Test { } #[cfg(test)] +#[allow(clippy::unwrap_used)] mod tests { use proptest::{ arbitrary::any, diff --git a/src/service/hostname.rs b/src/service/hostname.rs index eafc962..b54dea8 100644 --- a/src/service/hostname.rs +++ b/src/service/hostname.rs @@ -48,7 +48,7 @@ impl Hostname { } /// Error returned when creating a [`Hostname`]. -#[derive(Error, Debug, Clone, PartialEq, Eq)] +#[derive(Error, Debug, Clone, Copy, PartialEq, Eq)] pub enum InvalidHostnameError { /// One of the hostname's labels was empty. #[error("hostname label was empty")] diff --git a/src/service/image.rs b/src/service/image.rs index b613605..8621004 100644 --- a/src/service/image.rs +++ b/src/service/image.rs @@ -176,7 +176,14 @@ impl Image { /// ``` #[must_use] pub fn registry(&self) -> Option<&str> { - self.registry_end.map(|end| &self.inner[..end]) + self.registry_end.map(|end| { + // PANIC_SAFETY: + // `registry_end` is always within `inner`. + // `inner` only contains ASCII. + // Checked with `registry()` test. + #[allow(clippy::indexing_slicing, clippy::string_slice)] + &self.inner[..end] + }) } /// Set the registry portion of the image name, use [`None`] to remove it. @@ -242,6 +249,11 @@ impl Image { /// ``` #[must_use] pub fn name(&self) -> &str { + // PANIC_SAFETY: + // `name_end()` is always within `inner`. + // `inner` only contains ASCII. + // Checked with `name()` test. + #[allow(clippy::indexing_slicing, clippy::string_slice)] &self.inner[..self.name_end()] } @@ -293,6 +305,11 @@ impl Image { #[must_use] pub fn tag(&self) -> Option<&str> { if let Some(TagOrDigestStart::Tag(start)) = self.tag_or_digest_start { + // PANIC_SAFETY: + // `start` is always within `inner`. + // `inner` only contains ASCII. + // Checked with `tag_and_digest()` test. + #[allow(clippy::indexing_slicing, clippy::string_slice)] Some(&self.inner[start..]) } else { None @@ -339,6 +356,11 @@ impl Image { #[must_use] pub fn digest(&self) -> Option<&str> { if let Some(TagOrDigestStart::Digest(start)) = self.tag_or_digest_start { + // PANIC_SAFETY: + // `start` is always within `inner`. + // `inner` only contains ASCII. + // Checked with `tag_and_digest()` test. + #[allow(clippy::indexing_slicing, clippy::string_slice)] Some(&self.inner[start..]) } else { None @@ -375,10 +397,20 @@ impl Image { pub fn as_tag_or_digest(&self) -> Option { match self.tag_or_digest_start { Some(TagOrDigestStart::Tag(start)) => { + // PANIC_SAFETY: + // `start` is always within `inner`. + // `inner` only contains ASCII. + // Checked with `tag_and_digest()` test. + #[allow(clippy::indexing_slicing, clippy::string_slice)] let tag = Tag::new_unchecked(&self.inner[start..]); Some(TagOrDigest::Tag(tag)) } Some(TagOrDigestStart::Digest(start)) => { + // PANIC_SAFETY: + // `start` is always within `inner`. + // `inner` only contains ASCII. + // Checked with `tag_and_digest()` test. + #[allow(clippy::indexing_slicing, clippy::string_slice)] let digest = Digest::new_unchecked(&self.inner[start..]); Some(TagOrDigest::Digest(digest)) } @@ -676,11 +708,12 @@ impl<'a> AsRef for TagOrDigest<'a> { } /// Returns `true` if `char` is a lowercase ASCII alphanumeric character. -fn char_is_alnum(char: char) -> bool { +const fn char_is_alnum(char: char) -> bool { matches!(char, 'a'..='z' | '0'..='9') } #[cfg(test)] +#[allow(clippy::unwrap_used)] mod tests { use super::*; diff --git a/src/service/image/digest.rs b/src/service/image/digest.rs index 935efa3..7417c98 100644 --- a/src/service/image/digest.rs +++ b/src/service/image/digest.rs @@ -106,7 +106,7 @@ impl<'a> Digest<'a> { /// Return the inner string slice. #[must_use] - pub fn into_inner(self) -> &'a str { + pub const fn into_inner(self) -> &'a str { self.0 } } diff --git a/src/service/image/name.rs b/src/service/image/name.rs index b9d2e8d..554bb1f 100644 --- a/src/service/image/name.rs +++ b/src/service/image/name.rs @@ -102,7 +102,8 @@ impl<'a> Name<'a> { } } - pub(super) fn registry_end(&self) -> Option { + /// Byte position of `inner` where the registry ends, if the image name has a registry part. + pub(super) const fn registry_end(&self) -> Option { self.registry_end } @@ -118,12 +119,19 @@ impl<'a> Name<'a> { /// ``` #[must_use] pub fn registry(&self) -> Option<&str> { - self.registry_end.map(|end| &self.inner[..end]) + self.registry_end.map(|end| { + // PANIC_SAFETY: + // `registry_end` is always within `inner`. + // `inner` only contains ASCII. + // Checked with `registry()` test. + #[allow(clippy::indexing_slicing, clippy::string_slice)] + &self.inner[..end] + }) } /// Return the inner string slice. #[must_use] - pub fn into_inner(self) -> &'a str { + pub const fn into_inner(self) -> &'a str { self.inner } } diff --git a/src/service/image/tag.rs b/src/service/image/tag.rs index d16b317..cd99294 100644 --- a/src/service/image/tag.rs +++ b/src/service/image/tag.rs @@ -67,7 +67,7 @@ impl<'a> Tag<'a> { /// Return the inner string slice. #[must_use] - pub fn into_inner(self) -> &'a str { + pub const fn into_inner(self) -> &'a str { self.0 } } diff --git a/src/service/network_config.rs b/src/service/network_config.rs index 25c3046..395a220 100644 --- a/src/service/network_config.rs +++ b/src/service/network_config.rs @@ -73,7 +73,7 @@ enum Field { impl Field { /// Field identifier as a static string slice. - pub const fn as_str(self) -> &'static str { + const fn as_str(self) -> &'static str { match self { Self::NetworkMode => "network_mode", Self::Networks => "networks", @@ -137,10 +137,10 @@ pub(super) mod option { /// # Errors /// /// Returns an error if the `serializer` does while serializing. - pub fn serialize(value: &Option, serializer: S) -> Result - where - S: Serializer, - { + pub(in super::super) fn serialize( + value: &Option, + serializer: S, + ) -> Result { value.serialize(serializer) } @@ -150,10 +150,9 @@ pub(super) mod option { /// /// Returns an error if the `deserializer` does, there is an error deserializing either /// [`NetworkConfig`] variant, or both fields are present. - pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> - where - D: Deserializer<'de>, - { + pub(in super::super) fn deserialize<'de, D: Deserializer<'de>>( + deserializer: D, + ) -> Result, D::Error> { let NetworkConfigFlat { network_mode, networks, @@ -176,8 +175,11 @@ pub(super) mod option { expecting = "a struct with either a `network_mode` or `networks` field" )] struct NetworkConfigFlat { + /// [`NetworkConfig::NetworkMode`] #[serde(default)] network_mode: Option, + + /// [`NetworkConfig::Networks`] #[serde(default)] networks: Option, } @@ -240,7 +242,7 @@ impl NetworkMode { /// /// [`None`]: NetworkMode::None #[must_use] - pub fn is_none(&self) -> bool { + pub const fn is_none(&self) -> bool { matches!(self, Self::None) } @@ -248,7 +250,7 @@ impl NetworkMode { /// /// [`Host`]: NetworkMode::Host #[must_use] - pub fn is_host(&self) -> bool { + pub const fn is_host(&self) -> bool { matches!(self, Self::Host) } @@ -256,7 +258,7 @@ impl NetworkMode { /// /// [`Service`]: NetworkMode::Service #[must_use] - pub fn is_service(&self) -> bool { + pub const fn is_service(&self) -> bool { matches!(self, Self::Service(..)) } @@ -264,7 +266,7 @@ impl NetworkMode { /// /// [`Service`]: NetworkMode::Service #[must_use] - pub fn as_service(&self) -> Option<&Identifier> { + pub const fn as_service(&self) -> Option<&Identifier> { if let Self::Service(v) = self { Some(v) } else { @@ -276,7 +278,7 @@ impl NetworkMode { /// /// [`Other`]: NetworkMode::Other #[must_use] - pub fn is_other(&self) -> bool { + pub const fn is_other(&self) -> bool { matches!(self, Self::Other(..)) } @@ -284,7 +286,7 @@ impl NetworkMode { /// /// [`Other`]: NetworkMode::Other #[must_use] - pub fn as_other(&self) -> Option<&String> { + pub const fn as_other(&self) -> Option<&String> { if let Self::Other(v) = self { Some(v) } else { @@ -294,7 +296,7 @@ impl NetworkMode { } /// Error returned when [parsing](NetworkMode::parse()) a [`NetworkMode`] from a string. -#[derive(Error, Debug, Clone, PartialEq, Eq)] +#[derive(Error, Debug, Clone, Copy, PartialEq, Eq)] #[error("error parsing service network mode")] pub struct ParseNetworkModeError(#[from] InvalidIdentifierError); @@ -531,6 +533,7 @@ impl Display for MacAddress { } #[cfg(test)] +#[allow(clippy::unwrap_used)] mod tests { use indexmap::indexset; diff --git a/src/service/ports.rs b/src/service/ports.rs index f417798..632cfbe 100644 --- a/src/service/ports.rs +++ b/src/service/ports.rs @@ -238,7 +238,7 @@ pub struct ShortPort { impl ShortPort { /// Create a new [`ShortPort`]. #[must_use] - pub fn new(ranges: ShortRanges) -> Self { + pub const fn new(ranges: ShortRanges) -> Self { Self { host_ip: None, ranges, @@ -426,7 +426,7 @@ impl ShortRanges { /// Host port range. #[must_use] - pub fn host(&self) -> Option { + pub const fn host(&self) -> Option { self.host } @@ -448,7 +448,7 @@ impl ShortRanges { /// Container port range. #[must_use] - pub fn container(&self) -> Range { + pub const fn container(&self) -> Range { self.container } @@ -493,7 +493,9 @@ fn range_size_eq(host: Option, container: Range) -> Result<(), ShortRange container port range size `{container_size}`" )] pub struct ShortRangesError { + /// Size of the host port [`Range`]. host_size: u16, + /// Size of the container port [`Range`]. container_size: u16, } @@ -590,6 +592,7 @@ impl IntoIterator for ShortRanges { } /// An [`Iterator`] which yields host-container port pairs from [`ShortRanges`]. +#[derive(Debug, Clone, PartialEq, Eq)] pub struct ShortRangesIter { /// Host port iterator. host: Option>, @@ -629,23 +632,19 @@ impl Range { /// /// Returns an error if `start` is greater than `end`. pub fn new(start: u16, end: Option) -> Result { - if let Some(end) = end { - match start.cmp(&end) { - Ordering::Less => Ok(Self { - start, - end: Some(end), - }), - Ordering::Equal => Ok(Self { start, end: None }), - Ordering::Greater => Err(RangeError { start, end }), - } - } else { - Ok(Self { start, end: None }) - } + end.map_or(Ok(Self { start, end: None }), |end| match start.cmp(&end) { + Ordering::Less => Ok(Self { + start, + end: Some(end), + }), + Ordering::Equal => Ok(Self { start, end: None }), + Ordering::Greater => Err(RangeError { start, end }), + }) } /// Start of the port range. #[must_use] - pub fn start(&self) -> u16 { + pub const fn start(&self) -> u16 { self.start } @@ -653,7 +652,7 @@ impl Range { /// /// Returns [`None`] if [`start()`](Self::start()) is the only port in the range. #[must_use] - pub fn end(&self) -> Option { + pub const fn end(&self) -> Option { self.end } @@ -681,10 +680,12 @@ impl Range { } /// Error returned when creating a [`Range`]. -#[derive(Error, Debug, Clone, PartialEq, Eq)] +#[derive(Error, Debug, Clone, Copy, PartialEq, Eq)] #[error("the start `{start}` of the port range must be less than or equal to the end `{end}`")] pub struct RangeError { + /// Given start of the port range. start: u16, + /// Given end of the port range. end: u16, } @@ -809,7 +810,9 @@ pub enum ParseRangeError { /// Error parsing an integer. #[error("error parsing `{value}` as an integer")] Int { + /// Source of the error. source: ParseIntError, + /// Value attempted to parse. value: String, }, } @@ -1004,11 +1007,10 @@ pub(super) mod tests { proptest! { #[test] fn short_ranges_iter(ranges in short_ranges()) { - let iter: Vec<_> = if let Some(host) = ranges.host { - host.into_iter().map(Some).zip(ranges.container).collect() - } else { - std::iter::repeat(None).zip(ranges.container).collect() - }; + let iter: Vec<_> = ranges.host.map_or_else( + || std::iter::repeat(None).zip(ranges.container).collect(), + |host| host.into_iter().map(Some).zip(ranges.container).collect(), + ); let ranges: Vec<_> = ranges.into_iter().collect(); prop_assert_eq!(ranges, iter); @@ -1041,7 +1043,7 @@ pub(super) mod tests { } } - pub fn range() -> impl Strategy { + pub(in super::super) fn range() -> impl Strategy { any::() .prop_flat_map(|start| (Just(start), option::of(start..))) .prop_map(|(start, end)| Range { @@ -1050,7 +1052,7 @@ pub(super) mod tests { }) } - pub fn protocol() -> impl Strategy { + pub(in super::super) fn protocol() -> impl Strategy { prop_oneof![ Just(Protocol::Tcp), Just(Protocol::Udp), diff --git a/src/service/ulimit.rs b/src/service/ulimit.rs index cf927b3..921488d 100644 --- a/src/service/ulimit.rs +++ b/src/service/ulimit.rs @@ -1,3 +1,6 @@ +//! Provides [`Ulimits`] for the `ulimits` field of [`Service`](super::Service) and the long +//! [`Build`](super::Build) syntax. + use compose_spec_macros::{DeserializeTryFromString, SerializeDisplay}; use indexmap::IndexMap; use serde::{Deserialize, Serialize}; @@ -5,7 +8,7 @@ use thiserror::Error; use crate::{common::key_impls, AsShort, Extensions, ShortOrLong}; -/// Override the default ulimits for a container. +/// Override the default ulimits for a [`Service`](super::Service) container. /// /// Ulimits are defined as map from a [`Resource`] to either a singe limit ([`u64`]) or a mapping /// of a soft and hard limit ([`Ulimit`]). @@ -51,7 +54,7 @@ impl Resource { } /// Error returned when creating a [`Resource`]. -#[derive(Error, Debug, Clone, PartialEq, Eq)] +#[derive(Error, Debug, Clone, Copy, PartialEq, Eq)] pub enum InvalidResourceError { /// Resource was empty. #[error("ulimit resources cannot be empty")] @@ -96,11 +99,7 @@ impl AsShort for Ulimit { extensions, } = self; - if *soft == *hard && extensions.is_empty() { - Some(soft) - } else { - None - } + (*soft == *hard && extensions.is_empty()).then_some(soft) } } diff --git a/src/service/user_or_group.rs b/src/service/user_or_group.rs index a936c84..96f0172 100644 --- a/src/service/user_or_group.rs +++ b/src/service/user_or_group.rs @@ -34,22 +34,21 @@ impl UserOrGroup { where T: AsRef + TryInto, { - if let Ok(id) = user_or_group.as_ref().parse() { - Ok(Self::Id(id)) - } else { - user_or_group.try_into().map(Self::Name) - } + user_or_group.as_ref().parse().map_or_else( + |_| user_or_group.try_into().map(Self::Name), + |id| Ok(Self::Id(id)), + ) } /// Returns `true` if the user or group is an [`Id`](Self::Id). #[must_use] - pub fn is_id(&self) -> bool { + pub const fn is_id(&self) -> bool { matches!(self, Self::Id(..)) } /// Returns [`Some`] if [`Id`](Self::Id). #[must_use] - pub fn as_id(&self) -> Option { + pub const fn as_id(&self) -> Option { if let Self::Id(v) = self { Some(*v) } else { @@ -59,13 +58,13 @@ impl UserOrGroup { /// Returns `true` if the user or group is a [`Name`](Self::Name). #[must_use] - pub fn is_name(&self) -> bool { + pub const fn is_name(&self) -> bool { matches!(self, Self::Name(..)) } /// Returns [`Some`] if [`Name`](Self::Name). #[must_use] - pub fn as_name(&self) -> Option<&Name> { + pub const fn as_name(&self) -> Option<&Name> { if let Self::Name(v) = self { Some(v) } else { @@ -207,7 +206,7 @@ impl Name { } /// Error returned when parsing a [`Name`] from a string. -#[derive(Error, Debug, Clone, PartialEq, Eq)] +#[derive(Error, Debug, Clone, Copy, PartialEq, Eq)] pub enum InvalidNameError { /// User/group name was empty. #[error("user and group names cannot be empty")] @@ -241,6 +240,7 @@ pub enum InvalidNameError { key_impls!(Name => InvalidNameError); #[cfg(test)] +#[allow(clippy::unwrap_used)] mod tests { use super::*; diff --git a/src/service/volumes.rs b/src/service/volumes.rs index fbca655..7dfc5fe 100644 --- a/src/service/volumes.rs +++ b/src/service/volumes.rs @@ -73,7 +73,7 @@ pub struct ShortVolume { impl ShortVolume { /// Create a new [`ShortVolume`]. #[must_use] - pub fn new(container_path: AbsolutePath) -> Self { + pub const fn new(container_path: AbsolutePath) -> Self { Self { container_path, options: None, @@ -192,7 +192,13 @@ impl FromStr for ShortVolume { } } +/// Parse `container_path` into an [`AbsolutePath`]. +/// +/// # Errors +/// +/// Returns an error if the container path is not an absolute path. fn parse_container_path(container_path: &str) -> Result { + #[allow(clippy::map_err_ignore)] container_path .parse() .map_err(|_| ParseShortVolumeError::AbsoluteContainerPath(container_path.to_owned())) @@ -305,6 +311,9 @@ impl AbsolutePath { #[error("path is not absolute")] pub struct AbsolutePathError; +/// Implement methods and traits for a [`PathBuf`] newtype. +/// +/// The type must have a `new()` function which returns a [`Result`]. macro_rules! path_impls { ($Ty:ident => $Error:ty) => { impl $Ty { @@ -323,7 +332,7 @@ macro_rules! path_impls { /// Return a reference to the inner value. #[must_use] - pub fn as_inner(&self) -> &PathBuf { + pub const fn as_inner(&self) -> &PathBuf { &self.0 } @@ -405,7 +414,7 @@ pub struct ShortOptions { impl ShortOptions { /// Create a new [`ShortOptions`]. #[must_use] - pub fn new(source: Source) -> Self { + pub const fn new(source: Source) -> Self { Self { source, read_only: false, @@ -457,7 +466,7 @@ impl Source { } /// Error returned when [parsing](Source::parse()) a [`Source`] from a string. -#[derive(Error, Debug, Clone, PartialEq, Eq)] +#[derive(Error, Debug, Clone, Copy, PartialEq, Eq)] pub enum ParseSourceError { /// Error parsing [`HostPath`]. #[error("error parsing host path")] diff --git a/src/service/volumes/mount.rs b/src/service/volumes/mount.rs index cced3eb..2a359ca 100644 --- a/src/service/volumes/mount.rs +++ b/src/service/volumes/mount.rs @@ -61,7 +61,7 @@ impl Mount { /// /// [`Volume`]: Mount::Volume #[must_use] - pub fn is_volume(&self) -> bool { + pub const fn is_volume(&self) -> bool { matches!(self, Self::Volume(..)) } @@ -69,7 +69,7 @@ impl Mount { /// /// [`Volume`]: Mount::Volume #[must_use] - pub fn as_volume(&self) -> Option<&Volume> { + pub const fn as_volume(&self) -> Option<&Volume> { if let Self::Volume(v) = self { Some(v) } else { @@ -81,7 +81,7 @@ impl Mount { /// /// [`Bind`]: Mount::Bind #[must_use] - pub fn is_bind(&self) -> bool { + pub const fn is_bind(&self) -> bool { matches!(self, Self::Bind(..)) } @@ -89,7 +89,7 @@ impl Mount { /// /// [`Bind`]: Mount::Bind #[must_use] - pub fn as_bind(&self) -> Option<&Bind> { + pub const fn as_bind(&self) -> Option<&Bind> { if let Self::Bind(v) = self { Some(v) } else { @@ -101,7 +101,7 @@ impl Mount { /// /// [`Tmpfs`]: Mount::Tmpfs #[must_use] - pub fn is_tmpfs(&self) -> bool { + pub const fn is_tmpfs(&self) -> bool { matches!(self, Self::Tmpfs(..)) } @@ -109,7 +109,7 @@ impl Mount { /// /// [`Tmpfs`]: Mount::Tmpfs #[must_use] - pub fn as_tmpfs(&self) -> Option<&Tmpfs> { + pub const fn as_tmpfs(&self) -> Option<&Tmpfs> { if let Self::Tmpfs(v) = self { Some(v) } else { @@ -121,7 +121,7 @@ impl Mount { /// /// [`NamedPipe`]: Mount::NamedPipe #[must_use] - pub fn is_named_pipe(&self) -> bool { + pub const fn is_named_pipe(&self) -> bool { matches!(self, Self::NamedPipe(..)) } @@ -129,7 +129,7 @@ impl Mount { /// /// [`NamedPipe`]: Mount::NamedPipe #[must_use] - pub fn as_named_pipe(&self) -> Option<&NamedPipe> { + pub const fn as_named_pipe(&self) -> Option<&NamedPipe> { if let Self::NamedPipe(v) = self { Some(v) } else { @@ -141,7 +141,7 @@ impl Mount { /// /// [`Cluster`]: Mount::Cluster #[must_use] - pub fn is_cluster(&self) -> bool { + pub const fn is_cluster(&self) -> bool { matches!(self, Self::Cluster(..)) } @@ -149,7 +149,7 @@ impl Mount { /// /// [`Cluster`]: Mount::Cluster #[must_use] - pub fn as_cluster(&self) -> Option<&Cluster> { + pub const fn as_cluster(&self) -> Option<&Cluster> { if let Self::Cluster(v) = self { Some(v) } else { @@ -159,7 +159,7 @@ impl Mount { /// [`Common`] mount options. #[must_use] - pub fn common(&self) -> &Common { + pub const fn common(&self) -> &Common { match self { Self::Volume(mount) => &mount.common, Self::Bind(mount) => &mount.common, @@ -282,7 +282,7 @@ pub struct Volume { impl Volume { /// Create a [`Volume`] [`Mount`] from [`Common`] mount options. #[must_use] - pub fn new(common: Common) -> Self { + pub const fn new(common: Common) -> Self { Self { source: None, volume: None, @@ -367,7 +367,7 @@ pub struct Bind { impl Bind { /// Create a [`Bind`] [`Mount`] from a `source` and [`Common`] mount options. #[must_use] - pub fn new(source: HostPath, common: Common) -> Self { + pub const fn new(source: HostPath, common: Common) -> Self { Self { source, bind: None, @@ -561,7 +561,7 @@ pub struct Tmpfs { impl Tmpfs { /// Create a [`Tmpfs`] [`Mount`] from [`Common`] mount options. #[must_use] - pub fn new(common: Common) -> Self { + pub const fn new(common: Common) -> Self { Self { tmpfs: None, common, diff --git a/src/test-full.yaml b/src/test-full.yaml new file mode 100644 index 0000000..cfc00b1 --- /dev/null +++ b/src/test-full.yaml @@ -0,0 +1,725 @@ +# A compose file for testing the structural correctness of `compose_spec::Compose`. +# +# yaml-language-server: $schema=https://raw.githubusercontent.com/compose-spec/compose-spec/master/schema/compose-spec.json + +version: version + +name: name + +include: + - include + - path: path + project_directory: project_directory + env_file: env_file + +services: + build-short: + build: context + + build-long-dockerfile: + build: + dockerfile: dockerfile + + build-long-dockerfile_inline: + build: + dockerfile_inline: dockerfile_inline + + build-long-args-list: + build: + args: + - arg=value + + build-long-args-map: + build: + args: + arg: value + + build-long-network-none: + build: + network: none + + build-long-other: + build: + context: context + ssh: + - default + - id=key + cache_from: + - image:latest + cache_to: + - type=registry,ref=image,key=value + - type=local,src=path + additional_contexts: + additional_context: https://example.com/ + additional_context2: /path + extra_hosts: + host4: 127.0.0.1 + host6: ::1 + isolation: isolation + privileged: true + labels: + - label=value + no_cache: true + pull: true + network: network + shm_size: 1b + target: target + secrets: + - secret + - source: secret + target: target + uid: "1000" + gid: "1000" + mode: 365 + x-test: test + tags: + - image:tag + - registry/username/repo:tag + ulimits: + ulimit: 500 + softhard: + soft: 100 + hard: 200 + x-test: test + platforms: + - linux/amd64 + - linux/arm + - linux/arm/v6 + - linux/arm/v7 + - linux/arm/v8 + - linux/arm64 + - linux/arm64/v8 + + blkio_config: + blkio_config: + device_read_bps: + - path: path + rate: 1kb + device_read_iops: + - path: path + rate: 100 + device_write_bps: + - path: path + rate: 1mb + device_write_iops: + - path: path + rate: 100 + weight: 10 + weight_device: + - path: path + weight: 1000 + + cgroup-host: + cgroup: host + + cgroup-private: + cgroup: private + + command-string: + command: string + + command-list: + command: + - one + - two + + credential_spec-config: + credential_spec: + config: config + x-test: test + + credential_spec-file: + credential_spec: + file: file + x-test: test + + credential_spec-registry: + credential_spec: + registry: registry + x-test: test + + depends_on-short: + depends_on: + - service + + depends_on-long-condition-started: + depends_on: + service: + condition: service_started + restart: true + required: false + + depends_on-long-condition-healthy: + depends_on: + service: + condition: service_healthy + + depends_on-long-condition-completed_successfully: + depends_on: + service: + condition: service_completed_successfully + + deploy-endpoint_mode-vip: + deploy: + endpoint_mode: vip + + deploy-endpoint_mode-dnsrr: + deploy: + endpoint_mode: dnsrr + + deploy-mode-global: + deploy: + mode: global + + deploy-mode-replicated: + deploy: + mode: replicated + + deploy-restart_policy-condition-none: + deploy: + restart_policy: + condition: none + + deploy-restart_policy-condition-on-failure: + deploy: + restart_policy: + condition: on-failure + + deploy-restart_policy-condition-any: + deploy: + restart_policy: + condition: any + + deploy-other: + deploy: + endpoint_mode: other + labels: + label: value + placement: + constraints: + - constraint + preferences: + - spread: spread + x-test: test + max_replicas_per_node: 2 + x-test: test + replicas: 2 + resources: + limits: + cpus: 1.5 + memory: 1gb + pids: 10 + x-test: test + reservations: + cpus: 1.5 + memory: 1gb + devices: + - capabilities: + - gpu + - tpu + driver: driver + count: all + options: + - opt=value + - capabilities: + - other + count: 1 + device_ids: + - id + options: + opt: value + x-test: test + generic_resources: + - discrete_resource_spec: + kind: kind + value: 1 + x-test: test + x-test: test + x-test: test + x-test: test + restart_policy: + delay: 1s + max_attempts: 1 + window: 1m + x-test: test + rollback_config: + parallelism: 0 + delay: 1h + failure_action: continue + monitor: 1s + max_failure_ratio: 0 + order: stop-first + x-test: test + update_config: + parallelism: 0 + delay: 1h + failure_action: continue + monitor: 1s + max_failure_ratio: 0 + order: stop-first + x-test: test + x-test: test + + develop: + develop: + watch: + - action: rebuild + path: path + - action: sync + path: path + - action: sync+restart + ignore: + - ignore + path: path + target: target + x-test: test + + dns-string: + dns: 1.1.1.1 + + dns-list: + dns: + - 1.1.1.1 + - ::1 + + dns_search-string: + dns_search: search + + dns_search-list: + dns_search: + - search + + entrypoint-string: + entrypoint: entrypoint + + entrypoint-list: + entrypoint: + - one + - two + + env_file-string: + env_file: env_file + + env_file-list: + env_file: + - env_file + - path: path + required: false + + environment-list: + environment: + - var=value + + environment-map: + environment: + var: value + + annotations-list: + annotations: + - annotation=value + + annotations-map: + annotations: + annotation: value + + healthcheck-disable: + healthcheck: + disable: true + + healthcheck-test-string: + healthcheck: + test: test + + healthcheck-test-list: + healthcheck: + test: + - CMD + - test + + healthcheck-other: + healthcheck: + interval: 1s + timeout: 1s + retries: 1 + start_period: 1s + start_interval: 1s + x-test: test + + ipc-shareable: + ipc: shareable + + ipc-service: + ipc: service:service + + ipc-other: + ipc: other + + labels-list: + labels: + - label=value + + labels-map: + labels: + label: value + + network_mode-none: + network_mode: none + + network_mode-host: + network_mode: host + + network_mode-service: + network_mode: service:service + + network_mode-other: + network_mode: other + + networks-short: + networks: + - network + + networks-long: + networks: + network: + aliases: + - alias + ipv4_address: 127.0.0.1 + ipv6_address: ::1 + link_local_ips: + - 127.0.0.1 + - ::1 + mac_address: 92:d0:c6:0a:29:33 + priority: 100 + x-test: test + + pull_policy-always: + pull_policy: always + + pull_policy-never: + pull_policy: never + + pull_policy-missing: + pull_policy: missing + + pull_policy-build: + pull_policy: build + + restart-no: + restart: no + + restart-always: + restart: always + + restart-on-failure: + restart: on-failure + + restart-unless-stopped: + restart: unless-stopped + + sysctls-list: + sysctls: + - one + - two + + sysctls-map: + sysctls: + key: value + + tmpfs-string: + tmpfs: tmpfs + + tmpfs-list: + tmpfs: + - tmpfs + + other: + attach: false + cpu_count: 2 + cpu_percent: 100 + cpu_shares: 1 + cpu_period: 1 + cpu_quota: 1 + cpu_rt_runtime: 1us + cpu_rt_period: 1ms + cpus: 1.5 + cpuset: 0-2,4,6-8 + cap_add: + - ALL + cap_drop: + - ALL + cgroup_parent: parent + configs: + - config + - source: config + target: target + uid: "1000" + gid: "1000" + mode: 365 + x-test: test + container_name: name + device_cgroup_rules: + - a *:* r + - c 1:1 w + - b 2:2 m + - a 3:3 rwm + devices: + - host:container + - host:container:rwm + dns_opt: + - opt + domainname: domainname + expose: + - 100 + - 100-200 + - 100/tcp + - 100-200/udp + - 100-200/other + extends: + service: service + file: file + external_links: + - service + - service:alias + extra_hosts: + host4: 127.0.0.1 + host6: ::1 + group_add: + - group + - 1000 + hostname: hostname + image: image + init: true + uts: host + isolation: isolation + links: + - service + - service:alias + logging: + driver: driver + options: + string: string + number: 1.5 + "null": null + x-test: test + mac_address: 92:d0:c6:0a:29:33 + mem_limit: 1gb + mem_reservation: 1gb + mem_swappiness: 0 + memswap_limit: -1 + oom_kill_disable: true + oom_score_adj: -1000 + pid: pid + pids_limit: 10 + platform: darwin/arm64 + ports: + - 100 + - 100:100 + - 100-200 + - 100-200:100-200 + - 100/tcp + - 100/udp + - 100/other + - target: 100 + published: 100-200 + host_ip: ::1 + protocol: tcp + mode: host + - name: name + target: 100 + published: 100 + host_ip: 127.0.0.1 + protocol: udp + app_protocol: http + mode: ingress + x-test: test + privileged: true + profiles: + - profile + read_only: true + runtime: crun + scale: 1 + secrets: + - secret + - source: secret + target: target + uid: "1000" + gid: "1000" + mode: 365 + x-test: test + security_opt: + - security_opt + shm_size: 1gb + stdin_open: true + stop_grace_period: 1s + stop_signal: signal + storage_opt: + key: value + tty: true + ulimits: + ulimit: 500 + softhard: + soft: 100 + hard: 200 + x-test: test + user: user + userns_mode: userns_mode + volumes: + - /container + - host:/container + - ./host:/container + - ./host:/container:ro,z + - ./host:/container:Z + - type: volume + source: source + target: /target + read_only: true + volume: + nocopy: true + subpath: subpath + x-test: test + consistency: consistency + x-test: test + - type: bind + source: ./source + target: /target + read_only: true + bind: + propagation: private + create_host_path: true + selinux: z + consistency: consistency + x-test: test + - type: tmpfs + target: /target + read_only: true + tmpfs: + size: 1gb + mode: 365 + consistency: consistency + x-test: test + - type: npipe + source: ./source + target: /target + read_only: true + consistency: consistency + x-test: test + - type: cluster + source: source + target: /target + read_only: true + consistency: consistency + x-test: test + volumes_from: + - service + - container:container + - container:container:ro + working_dir: working_dir + x-test: test + +networks: + "null": null + + empty: {} + + external: + external: true + name: name + + driver-host: + driver: host + + driver-none: + driver: none + + driver-other: + driver: other + + labels-list: + labels: + - label=value + + labels-map: + labels: + label: value + + other: + driver_opts: + string: string + number: 1.5 + attachable: true + enable_ipv6: true + ipam: + driver: driver + config: + - subnet: 10.0.0.0/24 + ip_range: 10.0.0.0/32 + gateway: 10.0.0.1 + aux_addresses: + host: 10.0.0.5 + x-test: test + options: + option: value + x-test: test + name: name + x-test: test + +volumes: + "null": null + + empty: {} + + external: + external: true + name: name + + labels-list: + labels: + - label=value + + labels-map: + labels: + label: value + + other: + driver: driver + driver_opts: + string: string + number: 1.5 + name: name + x-test: test + +configs: + external: + external: true + name: name + + file: + file: file + x-test: test + + environment: + environment: environment + x-test: test + + content: + content: content + x-test: test + +secrets: + external: + external: true + name: name + + file: + file: file + x-test: test + + environment: + environment: environment + labels: + label: value + driver: driver + driver_opts: + string: string + number: 1.5 + x-test: test diff --git a/src/volume.rs b/src/volume.rs index 720965a..708416b 100644 --- a/src/volume.rs +++ b/src/volume.rs @@ -10,7 +10,7 @@ impl Resource { /// /// [compose-spec](https://github.com/compose-spec/compose-spec/blob/master/07-volumes.md#name) #[must_use] - pub fn name(&self) -> Option<&String> { + pub const fn name(&self) -> Option<&String> { match self { Self::External { name } => name.as_ref(), Self::Compose(volume) => volume.name.as_ref(),