From f00f55b060da4a14b54d272810955bccac018655 Mon Sep 17 00:00:00 2001 From: Clay McLeod Date: Thu, 12 Oct 2023 16:53:16 -0500 Subject: [PATCH] feat: adds `combine` tool to prepare final YAML specification --- .github/workflows/CI-Rust.yml | 40 ++++ bin/combine/Cargo.lock | 297 +++++++++++++++++++++++++++++ bin/combine/Cargo.toml | 10 + bin/combine/src/lib.rs | 7 + bin/combine/src/main.rs | 60 ++++++ bin/combine/src/yaml.rs | 5 + bin/combine/src/yaml/reader.rs | 102 ++++++++++ bin/combine/src/yaml/repository.rs | 236 +++++++++++++++++++++++ 8 files changed, 757 insertions(+) create mode 100644 .github/workflows/CI-Rust.yml create mode 100644 bin/combine/Cargo.lock create mode 100644 bin/combine/Cargo.toml create mode 100644 bin/combine/src/lib.rs create mode 100644 bin/combine/src/main.rs create mode 100644 bin/combine/src/yaml.rs create mode 100644 bin/combine/src/yaml/reader.rs create mode 100644 bin/combine/src/yaml/repository.rs diff --git a/.github/workflows/CI-Rust.yml b/.github/workflows/CI-Rust.yml new file mode 100644 index 0000000..ae83ded --- /dev/null +++ b/.github/workflows/CI-Rust.yml @@ -0,0 +1,40 @@ +name: CI-Rust + +on: [push, pull_request] + +jobs: + format: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + - name: Update Rust + run: rustup update stable && rustup default stable + - name: Install rustfmt + run: rustup component add rustfmt + - run: cargo fmt -- --check + + lint: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + - name: Update Rust + run: rustup update stable && rustup default stable + - name: Install clippy + run: rustup component add clippy + - run: cargo clippy --all-features -- --deny warnings + + test: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + - name: Update Rust + run: rustup update stable && rustup default stable + - run: cargo test --all-features + + docs: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + - name: Update Rust + run: rustup update stable && rustup default stable + - run: cargo doc \ No newline at end of file diff --git a/bin/combine/Cargo.lock b/bin/combine/Cargo.lock new file mode 100644 index 0000000..3212d04 --- /dev/null +++ b/bin/combine/Cargo.lock @@ -0,0 +1,297 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "anstream" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" + +[[package]] +name = "anstyle-parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "clap" +version = "4.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d04704f56c2cde07f43e8e2c154b43f216dc5c92fc98ada720177362f953b956" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e231faeaca65ebd1ea3c737966bf858971cd38c3849107aa3ea7de90a804e45" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "combine" +version = "0.1.0" +dependencies = [ + "clap", + "serde_yaml", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "hashbrown" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "indexmap" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "proc-macro2" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + +[[package]] +name = "serde" +version = "1.0.188" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.188" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_yaml" +version = "0.9.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a49e178e4452f45cb61d0cd8cebc1b0fafd3e41929e996cef79aa3aca91f574" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "2.0.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "239814284fd6f1a4ffe4ca893952cdd93c224b6a1571c9a9eadd670295c0c9e2" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" diff --git a/bin/combine/Cargo.toml b/bin/combine/Cargo.toml new file mode 100644 index 0000000..cca023a --- /dev/null +++ b/bin/combine/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "combine" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +clap = { version = "4.4.6", features = ["derive"] } +serde_yaml = "0.9.25" diff --git a/bin/combine/src/lib.rs b/bin/combine/src/lib.rs new file mode 100644 index 0000000..0c3e788 --- /dev/null +++ b/bin/combine/src/lib.rs @@ -0,0 +1,7 @@ +#![warn(missing_docs)] +#![warn(rust_2018_idioms)] +#![warn(rust_2021_compatibility)] +#![warn(missing_debug_implementations)] +#![deny(rustdoc::broken_intra_doc_links)] + +pub mod yaml; diff --git a/bin/combine/src/main.rs b/bin/combine/src/main.rs new file mode 100644 index 0000000..d4e5518 --- /dev/null +++ b/bin/combine/src/main.rs @@ -0,0 +1,60 @@ +#![warn(rust_2018_idioms)] +#![warn(rust_2021_compatibility)] +#![warn(missing_debug_implementations)] +#![deny(rustdoc::broken_intra_doc_links)] + +use std::path::PathBuf; + +use clap::Parser; + +use combine::yaml::Reader; +use combine::yaml::Repository; + +const ERROR_EXIT_CODE: i32 = 1; + +/// A program to prepare the Childhood Cancer Data Initiative OpenAPI +/// specification from (a) a base specification and (b) an autogenerated +/// specification using TypeScript. +#[derive(Parser, Debug)] +#[command(author, version, about)] +struct Args { + /// A path to the template YAML. + template: PathBuf, + + /// A path to the TypeScript-generated YAML. + generated: PathBuf, +} + +fn inner() -> Result<(), Box> { + let args = Args::parse(); + + let template = Reader::try_from(args.template.as_ref())?; + let generated = Reader::try_from(args.generated.as_ref())?; + + let mut result = Repository::default(); + + result.pull("openapi", &template)?; + result.pull("info", &template)?; + result.pull("externalDocs", &template)?; + result.pull("servers", &template)?; + result.pull("tags", &template)?; + + result.pull("paths", &generated)?; + result.pull("components", &generated)?; + + result.assign_tag_for_path_prefix("/subject", "Subject")?; + + result.write(std::io::stdout().lock())?; + + Ok(()) +} + +pub fn main() { + match inner() { + Ok(_) => {} + Err(err) => { + eprintln!("error: {err}"); + std::process::exit(ERROR_EXIT_CODE); + } + } +} diff --git a/bin/combine/src/yaml.rs b/bin/combine/src/yaml.rs new file mode 100644 index 0000000..144222f --- /dev/null +++ b/bin/combine/src/yaml.rs @@ -0,0 +1,5 @@ +pub mod reader; +pub mod repository; + +pub use reader::Reader; +pub use repository::Repository; diff --git a/bin/combine/src/yaml/reader.rs b/bin/combine/src/yaml/reader.rs new file mode 100644 index 0000000..494ac2b --- /dev/null +++ b/bin/combine/src/yaml/reader.rs @@ -0,0 +1,102 @@ +use std::fs::File; +use std::io; +use std::io::Read; +use std::path::Path; +use std::path::PathBuf; + +use serde_yaml::Value; + +#[derive(Debug)] +pub enum Error { + /// A file does not exist at the path specified by the [`String`]. + FileDoesNotExist(PathBuf), + + /// An expected key was missing in the inner [`Value`]. + MissingKey(String), + + /// An input/output error. + IoError(io::Error), + + /// A `serde_yaml` error. + SerdeYamlError(serde_yaml::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::FileDoesNotExist(path) => write!(f, "file does not exist: {}", path.display()), + Error::MissingKey(key) => write!(f, "missing expected key: {key}"), + Error::IoError(err) => write!(f, "i/o error: {err}"), + Error::SerdeYamlError(err) => write!(f, "serde yaml error: {err}"), + } + } +} + +impl std::error::Error for Error {} + +pub type Result = std::result::Result; + +pub struct Reader { + value: Value, +} + +impl Reader { + /// Attempts to create a new [`Reader`]. + /// + /// # Examples + /// + /// ``` + /// use combine::yaml::Reader; + /// + /// // A valid reader. + /// + /// let data = b"x: 10\ny: 20\n"; + /// let reader = Reader::try_new(&data[..])?; + /// + /// // An invalid reader. + /// + /// let data = b"x: 0\ny\n"; + /// let reader = Reader::try_new(&data[..])?; + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn try_new(reader: R) -> Result { + let value = serde_yaml::from_reader(reader).map_err(Error::SerdeYamlError)?; + Ok(Self { value }) + } + + /// Gets a key throwing an [`Error::MissingKey`] if it is not found. + /// + /// # Examples + /// + /// ``` + /// use combine::yaml::Reader; + /// + /// let data = b"x: 10\ny: 20\n"; + /// let reader = Reader::try_new(&data[..])?; + /// + /// let result = reader.ensure_key("x")?; + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn ensure_key(&self, k: &str) -> Result<&Value> { + self.value + .get(k) + .map(Ok) + .unwrap_or(Err(Error::MissingKey(k.to_owned()))) + } +} + +impl TryFrom<&Path> for Reader { + type Error = Error; + + fn try_from(p: &Path) -> Result { + if !p.exists() { + return Err(Error::FileDoesNotExist(p.to_owned())); + } + + let file = File::open(p).map_err(Error::IoError)?; + + Self::try_new(file) + } +} diff --git a/bin/combine/src/yaml/repository.rs b/bin/combine/src/yaml/repository.rs new file mode 100644 index 0000000..39055f0 --- /dev/null +++ b/bin/combine/src/yaml/repository.rs @@ -0,0 +1,236 @@ +use serde_yaml::Mapping; +use serde_yaml::Value; + +use crate::yaml; + +const PATHS_KEY: &str = "paths"; +const TAGS_KEY: &str = "tags"; + +#[derive(Debug)] +pub enum Error { + /// A reader error. + ReaderError(yaml::reader::Error), + + /// A conflicting key would be inserted into the repository. + KeyAlreadyPresent(String), + + /// A key that was expected was not found in the repository. + MissingExpectedKey(String), + + /// A `serde_yaml` error. + SerdeYamlError(serde_yaml::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::ReaderError(err) => write!(f, "reader error: {err}"), + Error::KeyAlreadyPresent(key) => write!(f, "key already present: {key}"), + Error::MissingExpectedKey(key) => write!(f, "missing expected key: {key}"), + Error::SerdeYamlError(err) => write!(f, "serde yaml error: {err}"), + } + } +} + +impl std::error::Error for Error {} + +type Result = std::result::Result; + +#[derive(Default, Debug)] +pub struct Repository { + inner: Mapping, +} + +impl Repository { + /// Gets the inner [`Mapping`] from the [`Repository`] by reference. + /// + /// # Examples + /// + /// ``` + /// use serde_yaml::Mapping; + /// + /// use combine::yaml::Repository; + /// + /// let mut repository = Repository::default(); + /// assert_eq!(repository.inner(), &Mapping::new()); + /// ``` + pub fn inner(&self) -> &Mapping { + &self.inner + } + + /// Consumes the [`Repository`] to return the inner [`Mapping`]. + /// + /// # Examples + /// + /// ``` + /// use serde_yaml::Mapping; + /// + /// use combine::yaml::Repository; + /// + /// let mut repository = Repository::default(); + /// assert_eq!(repository.into_inner(), Mapping::new()); + /// ``` + pub fn into_inner(self) -> Mapping { + self.inner + } + + /// Inserts a [`Value`] into the mapping at the specified `key`. + /// + /// # Examples + /// + /// ``` + /// use serde_yaml::Value; + /// + /// use combine::yaml::Repository; + /// + /// let mut repository = Repository::default(); + /// repository.insert("hello", Value::String(String::from("world"))); + /// ``` + pub fn insert(&mut self, key: &str, value: Value) -> Result<()> { + if self.inner.contains_key(&key) { + return Err(Error::KeyAlreadyPresent(key.to_owned())); + } + + // We don't care about whether the key is new or if an old value used to + // exist in the map, so we don't do anything with this return value. + self.inner.insert(Value::String(key.to_owned()), value); + + Ok(()) + } + + /// Attempts to retreive the [`Value`] for the given `key`. + /// + /// # Examples + /// + /// ``` + /// use serde_yaml::Value; + /// + /// use combine::yaml::Repository; + /// + /// let mut repository = Repository::default(); + /// repository.insert( + /// Value::String(String::from("hello")), + /// Value::String(String::from("world")), + /// ); + /// + /// assert_eq!( + /// repository.get("hello"), + /// Some(&Value::String(String::from("world"))) + /// ); + /// ``` + pub fn get(&self, key: &str) -> Option<&Value> { + self.inner.get(key) + } + + /// Pulls a particular key from the `reader` into this [`Repository`]. + /// + /// # Examples + /// + /// ``` + /// use serde_yaml::Number; + /// use serde_yaml::Value; + /// + /// use combine::yaml::Reader; + /// use combine::yaml::Repository; + /// + /// let data = b"x: 10\ny: 20\n"; + /// let reader = Reader::try_new(&data[..])?; + /// + /// let mut repository = Repository::default(); + /// repository.pull("x", &reader); + /// + /// assert_eq!( + /// repository.get("x"), + /// Some(&Value::Number(Number::from(10))) + /// ); + /// + /// # Ok::<(), Box>(()) + /// + /// ``` + pub fn pull(&mut self, key: &str, reader: &yaml::Reader) -> Result<()> { + self.insert( + key, + reader.ensure_key(key).map_err(Error::ReaderError)?.clone(), + ) + } + + pub fn assign_tag_for_path_prefix(&mut self, prefix: &str, tag: &str) -> Result<()> { + let paths = self + .inner + .get_mut(PATHS_KEY) + .map(Ok) + .unwrap_or(Err(Error::MissingExpectedKey(PATHS_KEY.to_owned())))?; + + for (path, entry) in paths.as_mapping_mut().unwrap() { + let path = match path { + Value::String(path) => path, + _ => continue, + }; + + if !path.starts_with(prefix) { + continue; + } + + let methods = match entry { + Value::Mapping(mapping) => mapping, + _ => continue, + }; + + for (method, entry) in methods.iter_mut() { + let entry = match entry { + Value::Mapping(entry) => entry, + _ => continue, + }; + + entry.insert( + Value::String(TAGS_KEY.to_owned()), + Value::Sequence(vec![Value::String(tag.to_owned())]), + ); + } + } + + Ok(()) + } + + /// Writes the [`Repository`] to the `writer`. + /// + /// # Examples + /// + /// ``` + /// use serde_yaml::Value; + /// + /// use combine::yaml::Repository; + /// + /// let mut buffer = Vec::new(); + /// let mut repository = Repository::default(); + /// + /// repository.insert("hello", Value::String(String::from("world"))); + /// repository.insert("abcd", Value::String(String::from("efgh"))); + /// repository.write(&mut buffer); + /// + /// let result = std::str::from_utf8(&buffer).unwrap(); + /// assert_eq!(&result, &"hello: world\nabcd: efgh\n"); + /// ``` + pub fn write(&self, writer: W) -> Result<()> { + serde_yaml::to_writer(writer, &self.inner).map_err(Error::SerdeYamlError) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_fails_when_attempting_to_insert_a_key_twice( + ) -> std::result::Result<(), Box> { + let mut repository = Repository::default(); + repository.insert("hello", Value::String(String::from("world")))?; + + let err = repository + .insert("hello", Value::String(String::from("world"))) + .unwrap_err(); + assert_eq!(err.to_string(), "key already present: hello"); + + Ok(()) + } +}