Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 36 additions & 29 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions internal/mithril-build-script/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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 }
Expand All @@ -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"
6 changes: 4 additions & 2 deletions internal/mithril-build-script/src/open_api.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -29,7 +31,7 @@ pub fn list_all_open_api_spec_files(paths: &[&Path]) -> Vec<PathBuf> {

fn read_version_from_open_api_spec_file<P: AsRef<Path>>(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()
}

Expand Down
4 changes: 2 additions & 2 deletions internal/tests/mithril-api-spec/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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]
Expand Down
7 changes: 3 additions & 4 deletions internal/tests/mithril-api-spec/src/apispec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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"),
Expand Down
1 change: 1 addition & 0 deletions internal/tests/mithril-api-spec/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;

Expand Down
24 changes: 24 additions & 0 deletions internal/tests/mithril-api-spec/src/spec_parser/mod.rs
Original file line number Diff line number Diff line change
@@ -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<serde_json::Value, String> {
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])
}
}
}
148 changes: 148 additions & 0 deletions internal/tests/mithril-api-spec/src/spec_parser/yaml.rs
Original file line number Diff line number Diff line change
@@ -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<Value, String> {
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");
}
}