diff --git a/Cargo.lock b/Cargo.lock index 067b8c6c1f1..b77196d0631 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3788,16 +3788,6 @@ dependencies = [ "redox_syscall", ] -[[package]] -name = "libyml" -version = "0.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3302702afa434ffa30847a83305f0a69d6abd74293b6554c18ec85c7ef30c980" -dependencies = [ - "anyhow", - "version_check", -] - [[package]] name = "libz-rs-sys" version = "0.5.2" @@ -4091,24 +4081,24 @@ dependencies = [ [[package]] name = "mithril-api-spec" -version = "0.1.6" +version = "0.1.7" dependencies = [ "jsonschema", "mithril-common", "reqwest", + "saphyr", "serde", "serde_json", - "serde_yml", "warp", ] [[package]] name = "mithril-build-script" -version = "0.2.26" +version = "0.2.27" dependencies = [ + "saphyr", "semver", "serde_json", - "serde_yml", ] [[package]] @@ -5025,6 +5015,15 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "ordered-float" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2c1f9f56e534ac6a9b8a4600bdf0f530fb393b5f393e7b4d03489c3cf0c3f01" +dependencies = [ + "num-traits", +] + [[package]] name = "ordered-multimap" version = "0.7.3" @@ -6259,6 +6258,29 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "saphyr" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3767dfe8889ebb55a21409df2b6f36e66abfbe1eb92d64ff76ae799d3f91016" +dependencies = [ + "arraydeque", + "encoding_rs", + "hashlink 0.10.0", + "ordered-float", + "saphyr-parser", +] + +[[package]] +name = "saphyr-parser" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb771b59f6b1985d1406325ec28f97cfb14256abcec4fdfb37b36a1766d6af7" +dependencies = [ + "arraydeque", + "hashlink 0.10.0", +] + [[package]] name = "schannel" version = "0.1.27" @@ -6516,21 +6538,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "serde_yml" -version = "0.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59e2dd588bf1597a252c3b920e0143eb99b0f76e4e082f4c92ce34fbc9e71ddd" -dependencies = [ - "indexmap 2.11.0", - "itoa", - "libyml", - "memchr", - "ryu", - "serde", - "version_check", -] - [[package]] name = "sha1" version = "0.10.6" diff --git a/internal/mithril-build-script/Cargo.toml b/internal/mithril-build-script/Cargo.toml index 046eb8a7baf..aa6ca601aaf 100644 --- a/internal/mithril-build-script/Cargo.toml +++ b/internal/mithril-build-script/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mithril-build-script" -version = "0.2.26" +version = "0.2.27" description = "A toolbox for Mithril crates build scripts" authors = { workspace = true } edition = { workspace = true } @@ -10,6 +10,6 @@ repository = { workspace = true } include = ["**/*.rs", "Cargo.toml", "README.md", ".gitignore"] [dependencies] +saphyr = "0.0.6" semver = { workspace = true } serde_json = { workspace = true } -serde_yml = "0.0.12" diff --git a/internal/mithril-build-script/src/open_api.rs b/internal/mithril-build-script/src/open_api.rs index 242e049de16..34a7a9c389c 100644 --- a/internal/mithril-build-script/src/open_api.rs +++ b/internal/mithril-build-script/src/open_api.rs @@ -1,8 +1,10 @@ -use semver::Version; use std::collections::BTreeMap; use std::fs; use std::path::{Path, PathBuf}; +use saphyr::{LoadableYamlNode, Yaml}; +use semver::Version; + type OpenAPIFileName = String; type OpenAPIVersionRaw = String; @@ -29,7 +31,7 @@ pub fn list_all_open_api_spec_files(paths: &[&Path]) -> Vec { fn read_version_from_open_api_spec_file>(spec_file_path: P) -> OpenAPIVersionRaw { let yaml_spec = fs::read_to_string(spec_file_path).unwrap(); - let open_api: serde_yml::Value = serde_yml::from_str(&yaml_spec).unwrap(); + let open_api = &Yaml::load_from_str(&yaml_spec).unwrap()[0]; open_api["info"]["version"].as_str().unwrap().to_owned() } diff --git a/internal/tests/mithril-api-spec/Cargo.toml b/internal/tests/mithril-api-spec/Cargo.toml index 7f287870285..a78b56cfafc 100644 --- a/internal/tests/mithril-api-spec/Cargo.toml +++ b/internal/tests/mithril-api-spec/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mithril-api-spec" -version = "0.1.6" +version = "0.1.7" authors.workspace = true documentation.workspace = true edition.workspace = true @@ -11,9 +11,9 @@ repository.workspace = true [dependencies] jsonschema = { version = "0.33.0" } reqwest = { workspace = true } +saphyr = "0.0.6" serde = { workspace = true } serde_json = { workspace = true } -serde_yml = "0.0.12" warp = { workspace = true } [dev-dependencies] diff --git a/internal/tests/mithril-api-spec/src/apispec.rs b/internal/tests/mithril-api-spec/src/apispec.rs index fabe5ff99b0..168e14d35f4 100644 --- a/internal/tests/mithril-api-spec/src/apispec.rs +++ b/internal/tests/mithril-api-spec/src/apispec.rs @@ -4,11 +4,12 @@ use jsonschema::Validator; use reqwest::Url; use serde::Serialize; use serde_json::{Value, Value::Null, json}; - use warp::http::Response; use warp::http::StatusCode; use warp::hyper::body::Bytes; +use crate::spec_parser::OpenApiSpecParser; + #[cfg(test)] pub(crate) const DEFAULT_SPEC_FILE: &str = "../../../openapi.yaml"; @@ -55,10 +56,8 @@ impl<'a> APISpec<'a> { /// APISpec factory from spec pub fn from_file(path: &str) -> APISpec<'a> { - let yaml_spec = std::fs::read_to_string(path).unwrap(); - let openapi: serde_json::Value = serde_yml::from_str(&yaml_spec).unwrap(); APISpec { - openapi, + openapi: OpenApiSpecParser::parse_yaml(path).unwrap(), path: None, method: None, content_type: Some("application/json"), diff --git a/internal/tests/mithril-api-spec/src/lib.rs b/internal/tests/mithril-api-spec/src/lib.rs index 3ad38179121..6e326ed00cb 100644 --- a/internal/tests/mithril-api-spec/src/lib.rs +++ b/internal/tests/mithril-api-spec/src/lib.rs @@ -2,6 +2,7 @@ //! This crate provides a toolset to verify conformity of http routes against an Open Api specification. mod apispec; +pub(crate) mod spec_parser; pub use apispec::*; diff --git a/internal/tests/mithril-api-spec/src/spec_parser/mod.rs b/internal/tests/mithril-api-spec/src/spec_parser/mod.rs new file mode 100644 index 00000000000..2b5e2b5d744 --- /dev/null +++ b/internal/tests/mithril-api-spec/src/spec_parser/mod.rs @@ -0,0 +1,24 @@ +mod yaml; + +/// Parser for OpenApi Specification +/// +/// Returns the parsed spec as a [serde_json::Value] as this crate uses a jsonschema validator +/// to do the validation. +pub(crate) struct OpenApiSpecParser; + +impl OpenApiSpecParser { + pub(crate) fn parse_yaml(spec_path: &str) -> Result { + use saphyr::LoadableYamlNode; + + let yaml_spec = std::fs::read_to_string(spec_path) + .map_err(|e| format!("Could not read spec file `{spec_path}`: {e}"))?; + let openapi = saphyr::Yaml::load_from_str(&yaml_spec) + .map_err(|e| format!("Could not parse spec file `{spec_path}`: {e}"))?; + + if openapi.is_empty() { + Err("No spec found in file".to_string()) + } else { + yaml::convert_yaml_to_serde_json(&openapi[0]) + } + } +} diff --git a/internal/tests/mithril-api-spec/src/spec_parser/yaml.rs b/internal/tests/mithril-api-spec/src/spec_parser/yaml.rs new file mode 100644 index 00000000000..4ce68ca6258 --- /dev/null +++ b/internal/tests/mithril-api-spec/src/spec_parser/yaml.rs @@ -0,0 +1,148 @@ +use saphyr::{Scalar, Yaml}; +use serde_json::Value; + +/// Converts a [saphyr::Yaml][Yaml] value to a [serde_json::Value][Value]. +/// +/// This is a crude, minimal, implementation aimed only at handling `mithril-api-spec` needs of +/// parsing `openapi.yaml` files. +/// It should not be reused as-is for other cases. +pub(crate) fn convert_yaml_to_serde_json(yaml_value: &Yaml) -> Result { + match yaml_value { + Yaml::Value(scalar) => match scalar { + Scalar::String(s) => Ok(Value::String(s.to_string())), + Scalar::Integer(i) => Ok(Value::from(*i)), + Scalar::FloatingPoint(f) => { + serde_json::Number::from_f64(**f).map(Value::Number).ok_or_else(|| { + format!( + "Could not convert saphyr::Yaml::FloatingPoint {} to JSON number", + f + ) + }) + } + Scalar::Boolean(b) => Ok(Value::Bool(*b)), + Scalar::Null => Ok(Value::Null), + }, + Yaml::Sequence(seq) => { + let mut json_array = Vec::new(); + for item in seq { + json_array.push(convert_yaml_to_serde_json(item)?); + } + Ok(Value::Array(json_array)) + } + Yaml::Mapping(map) => { + let mut json_obj = serde_json::Map::new(); + for (key, value) in map { + let key_str = match key { + Yaml::Value(Scalar::String(s)) => s, + _ => return Err("saphyr::Yaml::Mapping key must be a string".to_string()), + }; + json_obj.insert(key_str.to_string(), convert_yaml_to_serde_json(value)?); + } + Ok(Value::Object(json_obj)) + } + Yaml::Representation(_val, _scalar_style, _tag) => { + Err("saphyr::Yaml::Representation variant is not supported".to_string()) + } + Yaml::Tagged(_, boxed) => convert_yaml_to_serde_json(boxed), + Yaml::Alias(_) => Err("saphyr::Yaml::Alias are not supported".to_string()), + Yaml::BadValue => Err("Bad YAML value".to_string()), + } +} + +#[cfg(test)] +mod tests { + use saphyr::LoadableYamlNode; + + use super::*; + + #[test] + fn test_convert_scalar_string() { + let yaml = Yaml::Value(Scalar::String("test".into())); + let json = convert_yaml_to_serde_json(&yaml).unwrap(); + assert_eq!(json, Value::String("test".to_string())); + } + + #[test] + fn test_convert_scalar_int() { + let yaml = Yaml::Value(Scalar::Integer(42)); + let json = convert_yaml_to_serde_json(&yaml).unwrap(); + assert_eq!(json, Value::Number(42.into())); + } + + #[test] + fn test_convert_scalar_float() { + let yaml = Yaml::load_from_str("2.14").unwrap()[0].to_owned(); + let json = convert_yaml_to_serde_json(&yaml).unwrap(); + assert!(json.is_number()); + assert_eq!(json.as_f64().unwrap(), 2.14); + } + + #[test] + fn test_convert_scalar_bool() { + let yaml = Yaml::Value(Scalar::Boolean(true)); + let json = convert_yaml_to_serde_json(&yaml).unwrap(); + assert_eq!(json, Value::Bool(true)); + } + + #[test] + fn test_convert_scalar_null() { + let yaml = Yaml::Value(Scalar::Null); + let json = convert_yaml_to_serde_json(&yaml).unwrap(); + assert_eq!(json, Value::Null); + } + + #[test] + fn test_convert_sequence() { + let yaml = Yaml::Sequence(vec![ + Yaml::Value(Scalar::Integer(1)), + Yaml::Value(Scalar::Integer(2)), + ]); + let json = convert_yaml_to_serde_json(&yaml).unwrap(); + assert_eq!( + json, + Value::Array(vec![Value::Number(1.into()), Value::Number(2.into())]) + ); + } + + #[test] + fn test_convert_mapping() { + let yaml = Yaml::load_from_str("key: value").unwrap()[0].to_owned(); + let json = convert_yaml_to_serde_json(&yaml).unwrap(); + let mut expected = serde_json::Map::new(); + expected.insert("key".to_string(), Value::String("value".to_string())); + assert_eq!(json, Value::Object(expected)); + } + + #[test] + fn test_invalid_mapping_key() { + let yaml = Yaml::load_from_str("42: value").unwrap()[0].to_owned(); + assert_eq!( + convert_yaml_to_serde_json(&yaml).unwrap_err(), + "saphyr::Yaml::Mapping key must be a string" + ); + } + + #[test] + fn test_representation_is_not_supported() { + let yaml = Yaml::Representation("test".into(), saphyr::ScalarStyle::SingleQuoted, None); + let error = convert_yaml_to_serde_json(&yaml).unwrap_err(); + assert_eq!( + error, + "saphyr::Yaml::Representation variant is not supported" + ); + } + + #[test] + fn test_alias_is_not_supported() { + let yaml = Yaml::Alias(1); + let error = convert_yaml_to_serde_json(&yaml).unwrap_err(); + assert_eq!(error, "saphyr::Yaml::Alias are not supported"); + } + + #[test] + fn test_bad_value_is_not_supported() { + let yaml = Yaml::BadValue; + let error = convert_yaml_to_serde_json(&yaml).unwrap_err(); + assert_eq!(error, "Bad YAML value"); + } +}