From 454acf90676f7a34ec5b16d40e282b7c831c4d21 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Thu, 30 Jan 2025 16:22:55 +0000 Subject: [PATCH] chore: remove I/O functions to account for YAML optionality --- crates/oas3/CHANGELOG.md | 10 +++ crates/oas3/Cargo.toml | 10 ++- crates/oas3/README.md | 4 +- crates/oas3/examples/printer.rs | 13 ++-- crates/oas3/src/error.rs | 28 -------- crates/oas3/src/lib.rs | 80 +++++++++------------ crates/oas3/src/spec/discriminator.rs | 2 +- crates/oas3/src/spec/media_type_examples.rs | 7 +- crates/oas3/src/spec/mod.rs | 43 ----------- crates/oas3/src/spec/parameter.rs | 2 +- crates/oas3/src/spec/schema.rs | 2 +- crates/oas3/src/spec/spec_extensions.rs | 73 ++++++++++++++++--- crates/roast/Cargo.toml | 1 + crates/roast/examples/complex-validation.rs | 13 ++-- crates/roast/examples/conformance.rs | 5 +- crates/roast/src/lib.rs | 9 --- crates/roast/src/validation/error.rs | 6 +- crates/roast/src/validation/validator.rs | 8 +-- 18 files changed, 145 insertions(+), 171 deletions(-) delete mode 100644 crates/oas3/src/error.rs diff --git a/crates/oas3/CHANGELOG.md b/crates/oas3/CHANGELOG.md index 3938343..5a7e0a6 100644 --- a/crates/oas3/CHANGELOG.md +++ b/crates/oas3/CHANGELOG.md @@ -2,6 +2,16 @@ ## Unreleased +- Add new crate feature `yaml-spec` (on-by-default). +- Add top-level `from_json()` function. +- Add top-level `from_yaml()` function, guarded by the `yaml-spec` crate feature. +- The top-level `to_json()` function now returns `serde_json` errors directly. +- The top-level `to_yaml()` function now returns `serde_yaml` errors directly. +- The top-level `to_yaml()` function is now guarded by the `yaml-spec` crate feature. +- Remove top-level `from_reader()` function. +- Remove top-level `from_path()` function. +- Remove top-level `Error` type. + ## 0.14.0 - Implement `Default` for `spec::{Components, Contact, Example, Flows}`. diff --git a/crates/oas3/Cargo.toml b/crates/oas3/Cargo.toml index ffe9ec2..58798bc 100644 --- a/crates/oas3/Cargo.toml +++ b/crates/oas3/Cargo.toml @@ -11,9 +11,8 @@ edition = { workspace = true } rust-version = { workspace = true } [features] -default = ["validation", "yaml_spec"] -validation = [] -yaml_spec = ["dep:serde_yaml"] +default = [] +yaml-spec = ["dep:serde_yaml"] [dependencies] derive_more = { workspace = true, features = ["display", "error", "from"] } @@ -33,9 +32,8 @@ indoc = { workspace = true } pretty_assertions = { workspace = true } [[example]] -name = "printers" -path = "examples/printer.rs" -required-features = ["yaml_spec"] +name = "printer" +required-features = ["yaml-spec"] [lints] workspace = true diff --git a/crates/oas3/README.md b/crates/oas3/README.md index 2914baa..9241cd5 100644 --- a/crates/oas3/README.md +++ b/crates/oas3/README.md @@ -24,7 +24,9 @@ specs in the older format. ## Example ```rust -match oas3::from_path("path/to/openapi.yml") { +let yaml = std::fs::read_to_string("path/to/openapi.yml").unwrap(); + +match oas3::from_yaml(yaml) { Ok(spec) => println!("spec: {:?}", spec), Err(err) => println!("error: {}", err) } diff --git a/crates/oas3/examples/printer.rs b/crates/oas3/examples/printer.rs index 6e3a661..bf7c8ea 100644 --- a/crates/oas3/examples/printer.rs +++ b/crates/oas3/examples/printer.rs @@ -1,12 +1,15 @@ //! Demonstrates reading an OpenAPI spec file and printing back to stdout. -use std::env; +use std::{env, fs}; fn main() -> eyre::Result<()> { - if let Some(path) = env::args().nth(1) { - let spec = oas3::from_path(path)?; - println!("{}", oas3::to_yaml(&spec).unwrap()); - } + let Some(path) = env::args().nth(1) else { + return Ok(()); + }; + + let yaml = fs::read_to_string(path)?; + let spec = oas3::from_yaml(yaml)?; + println!("{}", oas3::to_yaml(&spec).unwrap()); Ok(()) } diff --git a/crates/oas3/src/error.rs b/crates/oas3/src/error.rs deleted file mode 100644 index 5b6f3c8..0000000 --- a/crates/oas3/src/error.rs +++ /dev/null @@ -1,28 +0,0 @@ -//! Error types - -use std::io; - -use derive_more::derive::{Display, Error, From}; - -use crate::spec::Error as SpecError; - -/// Top-level errors. -#[derive(Debug, Display, Error, From)] -pub enum Error { - /// I/O error. - #[display("I/O error")] - Io(io::Error), - - /// YAML error. - #[display("YAML error")] - #[cfg(feature = "yaml_spec")] - Yaml(serde_yaml::Error), - - /// JSON error. - #[display("JSON error")] - Serialize(serde_json::Error), - - /// Spec error. - #[display("Spec error")] - Spec(SpecError), -} diff --git a/crates/oas3/src/lib.rs b/crates/oas3/src/lib.rs index 22c57bd..717eb88 100644 --- a/crates/oas3/src/lib.rs +++ b/crates/oas3/src/lib.rs @@ -6,7 +6,9 @@ //! # Example //! //! ```no_run -//! match oas3::from_path("path/to/openapi.yml") { +//! let yaml = std::fs::read_to_string("path/to/openapi.yml").unwrap(); +//! +//! match oas3::from_yaml(yaml) { //! Ok(spec) => println!("spec: {:?}", spec), //! Err(err) => println!("error: {}", err) //! } @@ -17,12 +19,9 @@ #![warn(missing_docs)] #![cfg_attr(docsrs, feature(doc_auto_cfg))] -use std::{fs::File, io::Read, path::Path}; - -mod error; pub mod spec; -pub use self::{error::Error, spec::Spec}; +pub use self::spec::Spec; /// Version 3.1.0 of the OpenAPI specification. /// @@ -32,63 +31,48 @@ pub use self::{error::Error, spec::Spec}; pub type OpenApiV3Spec = spec::Spec; /// Try deserializing an OpenAPI spec (YAML or JSON) from a file, giving the path. -/// -/// If the `yaml` feature flag is disabled only `JSON` specs are supported -pub fn from_path

(path: P) -> Result -where - P: AsRef, -{ - from_reader(File::open(path)?) +#[cfg(all(test, feature = "yaml-spec"))] +pub(crate) fn from_path( + path: impl AsRef, +) -> std::io::Result> { + let file = std::fs::File::open(path.as_ref())?; + Ok(from_reader(file)) } /// Try deserializing an OpenAPI spec (YAML or JSON) from a [`Read`] type. -/// -/// If the `yaml` feature flag is disabled only `JSON` specs are supported -pub fn from_reader(read: R) -> Result -where - R: Read, -{ - #[cfg(feature = "yaml_spec")] - { - Ok(serde_yaml::from_reader::(read)?) - } - #[cfg(not(feature = "yaml_spec"))] - { - Ok(serde_json::from_reader::(read)?) - } +#[cfg(all(test, feature = "yaml-spec"))] +pub(crate) fn from_reader(read: impl std::io::Read) -> Result { + serde_yaml::from_reader::<_, OpenApiV3Spec>(read) } -/// Try deserializing an OpenAPI spec (YAML or JSON) from string. -/// -/// If the `yaml` feature flag is disabled only `JSON` specs are supported -pub fn from_str(val: impl AsRef) -> Result { - #[cfg(feature = "yaml_spec")] - { - Ok(serde_yaml::from_str::(val.as_ref())?) - } - #[cfg(not(feature = "yaml_spec"))] - { - Ok(serde_json::from_str::(val.as_ref())?) - } +/// Deserializes an OpenAPI spec (YAML-format) from a string. +#[cfg(feature = "yaml-spec")] +pub fn from_yaml(yaml: impl AsRef) -> Result { + serde_yaml::from_str(yaml.as_ref()) +} + +/// Deserializes an OpenAPI spec (JSON-format) from a string. +pub fn from_json(json: impl AsRef) -> Result { + serde_json::from_str(json.as_ref()) } -/// Try serializing to a YAML string. -#[cfg(feature = "yaml_spec")] -pub fn to_yaml(spec: &OpenApiV3Spec) -> Result { - Ok(serde_yaml::to_string(spec)?) +/// Serializes OpenAPI spec to a YAML string. +#[cfg(feature = "yaml-spec")] +pub fn to_yaml(spec: &OpenApiV3Spec) -> Result { + serde_yaml::to_string(spec) } -/// Try serializing to a JSON string. -pub fn to_json(spec: &OpenApiV3Spec) -> Result { - Ok(serde_json::to_string_pretty(spec)?) +/// Serializes OpenAPI spec to a JSON string. +pub fn to_json(spec: &OpenApiV3Spec) -> Result { + serde_json::to_string_pretty(spec) } -#[cfg(all(test, feature = "yaml_spec"))] +#[cfg(all(test, feature = "yaml-spec"))] mod tests { use std::{ fs::{self, read_to_string, File}, io::Write, - path, + path::{self, Path}, }; use pretty_assertions::assert_eq; @@ -143,7 +127,7 @@ mod tests { // File -> `Spec` -> `serde_json::Value` -> `String` // Parse the input file - let parsed_spec = from_path(input_file).unwrap(); + let parsed_spec = from_path(input_file).unwrap().unwrap(); // Convert to serde_json::Value let parsed_spec_json = serde_json::to_value(parsed_spec).unwrap(); // Convert to a JSON string diff --git a/crates/oas3/src/spec/discriminator.rs b/crates/oas3/src/spec/discriminator.rs index 83f971a..f65ed13 100644 --- a/crates/oas3/src/spec/discriminator.rs +++ b/crates/oas3/src/spec/discriminator.rs @@ -24,7 +24,7 @@ pub struct Discriminator { pub mapping: Option>, } -#[cfg(all(test, feature = "yaml_spec"))] +#[cfg(all(test, feature = "yaml-spec"))] mod tests { use super::*; diff --git a/crates/oas3/src/spec/media_type_examples.rs b/crates/oas3/src/spec/media_type_examples.rs index f675136..ca42e4b 100644 --- a/crates/oas3/src/spec/media_type_examples.rs +++ b/crates/oas3/src/spec/media_type_examples.rs @@ -81,9 +81,8 @@ impl MediaTypeExamples { } } -#[cfg(test)] +#[cfg(all(test, feature = "yaml-spec"))] mod tests { - #[cfg(feature = "yaml_spec")] use serde_json::json; use super::*; @@ -99,7 +98,6 @@ mod tests { } #[test] - #[cfg(feature = "yaml_spec")] fn deserialize() { assert_eq!( serde_yaml::from_str::(indoc::indoc! {" @@ -121,7 +119,6 @@ mod tests { } #[test] - #[cfg(feature = "yaml_spec")] fn serialize() { let mt_examples = MediaTypeExamples::default(); assert_eq!( @@ -143,7 +140,6 @@ mod tests { } #[test] - #[cfg(feature = "yaml_spec")] fn single_example() { let spec = serde_yaml::from_str::(indoc::indoc! {" openapi: 3.1.0 @@ -171,7 +167,6 @@ paths: {} } #[test] - #[cfg(feature = "yaml_spec")] fn resolve_references() { let spec = serde_yaml::from_str::(indoc::indoc! {" openapi: 3.1.0 diff --git a/crates/oas3/src/spec/mod.rs b/crates/oas3/src/spec/mod.rs index 721e31b..e0bfefb 100644 --- a/crates/oas3/src/spec/mod.rs +++ b/crates/oas3/src/spec/mod.rs @@ -227,46 +227,3 @@ impl Spec { self.servers.first() } } - -#[cfg(all(test, feature = "yaml_spec"))] -mod tests { - use pretty_assertions::assert_eq; - - use super::*; - - #[test] - fn spec_extensions_deserialize() { - let spec = indoc::indoc! {" - openapi: '3.1.0' - info: - title: test - version: v1 - components: {} - x-bar: true - qux: true - "}; - - let spec = serde_yaml::from_str::(spec).unwrap(); - assert!(spec.components.is_some()); - assert!(!spec.extensions.contains_key("x-bar")); - assert!(!spec.extensions.contains_key("qux")); - assert_eq!(spec.extensions.get("bar").unwrap(), true); - } - - #[test] - fn spec_extensions_serialize() { - let spec = indoc::indoc! {" - openapi: 3.1.0 - info: - title: test - version: v1 - components: {} - x-bar: true - "}; - - let parsed_spec = serde_yaml::from_str::(spec).unwrap(); - let round_trip_spec = serde_yaml::to_string(&parsed_spec).unwrap(); - - assert_eq!(spec, round_trip_spec); - } -} diff --git a/crates/oas3/src/spec/parameter.rs b/crates/oas3/src/spec/parameter.rs index a04761d..29ce966 100644 --- a/crates/oas3/src/spec/parameter.rs +++ b/crates/oas3/src/spec/parameter.rs @@ -223,7 +223,7 @@ impl FromRef for Parameter { } } -#[cfg(all(test, feature = "yaml_spec"))] +#[cfg(all(test, feature = "yaml-spec"))] mod tests { use indoc::indoc; diff --git a/crates/oas3/src/spec/schema.rs b/crates/oas3/src/spec/schema.rs index 4c66385..58d0f79 100644 --- a/crates/oas3/src/spec/schema.rs +++ b/crates/oas3/src/spec/schema.rs @@ -593,7 +593,7 @@ where T::deserialize(de).map(Some) } -#[cfg(all(test, feature = "yaml_spec"))] +#[cfg(all(test, feature = "yaml-spec"))] mod tests { use super::*; diff --git a/crates/oas3/src/spec/spec_extensions.rs b/crates/oas3/src/spec/spec_extensions.rs index a05e006..84634d4 100644 --- a/crates/oas3/src/spec/spec_extensions.rs +++ b/crates/oas3/src/spec/spec_extensions.rs @@ -5,11 +5,6 @@ use std::{ use serde::{de, Deserializer, Serializer}; -#[cfg(feature = "yaml_spec")] -type KeyType = serde_yaml::Value; -#[cfg(not(feature = "yaml_spec"))] -type KeyType = serde_json::Value; - /// Deserializes fields of a map beginning with `x-`. pub(crate) fn deserialize<'de, D>( deserializer: D, @@ -30,7 +25,7 @@ where where M: de::MapAccess<'de>, { - let mut map = HashMap::::new(); + let mut map = HashMap::::new(); while let Some((key, value)) = access.next_entry()? { map.insert(key, value); @@ -39,9 +34,7 @@ where Ok(map .into_iter() .filter_map(|(key, value)| { - key.as_str()? - .strip_prefix("x-") - .map(|key| (key.to_owned(), value)) + key.strip_prefix("x-").map(|key| (key.to_owned(), value)) }) .collect()) } @@ -64,3 +57,65 @@ where .map(|(key, value)| (format!("x-{key}"), value)), ) } + +#[cfg(all(test, feature = "yaml-spec"))] +mod tests { + use pretty_assertions::assert_eq; + + use crate::Spec; + + #[test] + fn spec_extensions_deserialize() { + let spec = indoc::indoc! {" + openapi: '3.1.0' + info: + title: test + version: v1 + components: {} + x-bar: true + qux: true + "}; + + let spec = serde_yaml::from_str::(spec).unwrap(); + assert!(spec.components.is_some()); + assert!(!spec.extensions.contains_key("x-bar")); + assert!(!spec.extensions.contains_key("qux")); + assert_eq!(spec.extensions.get("bar").unwrap(), true); + } + + #[test] + fn spec_extensions_deserialize_with_numeric_yaml_key_nearby() { + let spec = indoc::indoc! {" + openapi: '3.1.0' + info: + title: test + version: v1 + components: {} + 42: test numeric key doesn't break it + x-bar: true + 44: test numeric key doesn't break it + "}; + + let spec = serde_yaml::from_str::(spec).unwrap(); + assert!(spec.components.is_some()); + assert!(!spec.extensions.contains_key("x-bar")); + assert_eq!(spec.extensions.get("bar").unwrap(), true); + } + + #[test] + fn spec_extensions_serialize() { + let spec = indoc::indoc! {" + openapi: 3.1.0 + info: + title: test + version: v1 + components: {} + x-bar: true + "}; + + let parsed_spec = serde_yaml::from_str::(spec).unwrap(); + let round_trip_spec = serde_yaml::to_string(&parsed_spec).unwrap(); + + assert_eq!(spec, round_trip_spec); + } +} diff --git a/crates/roast/Cargo.toml b/crates/roast/Cargo.toml index 520fbbc..d95083d 100644 --- a/crates/roast/Cargo.toml +++ b/crates/roast/Cargo.toml @@ -29,6 +29,7 @@ color-eyre = { workspace = true } dotenvy = { workspace = true } eyre = { workspace = true } maplit = { workspace = true } +oas3 = { workspace = true, features = ["yaml-spec"] } pretty_env_logger = { workspace = true } tokio = { workspace = true, features = ["full"] } diff --git a/crates/roast/examples/complex-validation.rs b/crates/roast/examples/complex-validation.rs index 0b1eb4a..327bd3d 100644 --- a/crates/roast/examples/complex-validation.rs +++ b/crates/roast/examples/complex-validation.rs @@ -1,9 +1,11 @@ #![allow(dead_code, unused_variables)] +use std::fs; + use http::Method; use serde_json::json; -fn main() { +fn main() -> eyre::Result<()> { let _ = dotenvy::dotenv(); pretty_env_logger::init(); @@ -29,15 +31,16 @@ fn main() { "page": 1 }); - let spec = oas3::from_path("./data/oas-samples/allof.yml").expect("api spec parse error"); + let yaml = fs::read_to_string("./data/oas-samples/pet-store.yml")?; + let spec = oas3::from_yaml(yaml).expect("api spec parse error"); let op = spec.operation(&Method::GET, "/").unwrap(); - let schema = &op.responses(&spec)["200"].content["application/json"] - .schema(&spec) - .unwrap(); + let schema = &op.responses(&spec)["200"].content["application/json"].schema(&spec)?; // let v = ValidatorTree::from_schema(&schema, &spec).unwrap(); // v.validate(valid).unwrap(); // v.validate(invalid1).unwrap(); // v.validate(invalid2).unwrap(); // v.validate(invalid3).unwrap(); + + Ok(()) } diff --git a/crates/roast/examples/conformance.rs b/crates/roast/examples/conformance.rs index 7ec9bc4..78fa011 100644 --- a/crates/roast/examples/conformance.rs +++ b/crates/roast/examples/conformance.rs @@ -1,5 +1,7 @@ #![allow(dead_code, unused_variables)] +use std::fs; + use roast::{ConformanceTestSpec, OperationSpec, RequestSpec, ResponseSpec, TestRunner}; #[tokio::main] @@ -9,7 +11,8 @@ async fn main() -> eyre::Result<()> { dotenvy::dotenv().ok(); pretty_env_logger::init(); - let spec = oas3::from_path("./data/oas-samples/pet-store.yml").expect("api spec parse error"); + let yaml = fs::read_to_string("./data/oas-samples/pet-store.yml")?; + let spec = oas3::from_yaml(yaml).expect("api spec parse error"); let base_url: &str = &spec.primary_server().expect("no primary server").url; let mut runner = TestRunner::new(base_url, spec.clone()); diff --git a/crates/roast/src/lib.rs b/crates/roast/src/lib.rs index 5c22cf4..bed7ce0 100644 --- a/crates/roast/src/lib.rs +++ b/crates/roast/src/lib.rs @@ -12,15 +12,6 @@ pub use self::{conformance::*, validation::*}; /// Top-level errors. #[derive(Debug, Display, Error, From)] pub enum Error { - // #[display("I/O error")] - // Io(io::Error), - - // #[display("YAML error")] - // Yaml(serde_yaml::Error), - - // #[display("JSON error")] - // Serialize(serde_json::Error), - // #[display("Spec error")] Spec(oas3::spec::Error), diff --git a/crates/roast/src/validation/error.rs b/crates/roast/src/validation/error.rs index 20bd5ee..3171529 100644 --- a/crates/roast/src/validation/error.rs +++ b/crates/roast/src/validation/error.rs @@ -2,7 +2,7 @@ use std::fmt; use derive_more::derive::{Display, Error}; use http::{Method, StatusCode}; -use oas3::{spec::SchemaTypeSet, Error as SchemaError}; +use oas3::spec::{Error as SpecError, SchemaTypeSet}; use serde_json::Value as JsonValue; use super::Path; @@ -45,8 +45,8 @@ pub enum Error { // // Wrapped Errors // - #[display("Schema error")] - Schema(SchemaError), + #[display("Spec error")] + Spec(SpecError), // // Leaf Errors diff --git a/crates/roast/src/validation/validator.rs b/crates/roast/src/validation/validator.rs index 444d86a..d84a0c5 100644 --- a/crates/roast/src/validation/validator.rs +++ b/crates/roast/src/validation/validator.rs @@ -400,7 +400,7 @@ components: required: [size] "#; - let spec = oas3::from_reader(spec_str.as_bytes()).unwrap(); + let spec = oas3::from_yaml(spec_str).unwrap(); let schema = get_schema(&spec, "data"); let valtree = ValidationTree::from_schema(&schema, &spec).unwrap(); @@ -438,7 +438,7 @@ components: items: { type: integer } "#; - let spec = oas3::from_reader(spec_str.as_bytes()).unwrap(); + let spec = oas3::from_yaml(spec_str).unwrap(); let schema = get_schema(&spec, "assets"); let valtree = ValidationTree::from_schema(&schema, &spec).unwrap(); @@ -477,7 +477,7 @@ components: required: [size] "#; - let spec = oas3::from_reader(spec_str.as_bytes()).unwrap(); + let spec = oas3::from_yaml(spec_str).unwrap(); let schema = get_schema(&spec, "data"); let valtree = ValidationTree::from_schema(&schema, &spec).unwrap(); @@ -507,7 +507,7 @@ components: anyOf: [{ type: number }, { type: string }] "#; - let spec = oas3::from_reader(spec_str.as_bytes()).unwrap(); + let spec = oas3::from_yaml(spec_str).unwrap(); let schema = get_schema(&spec, "data"); let valtree = ValidationTree::from_schema(&schema, &spec).unwrap();