use std::sync::Arc;
use jsonschema::error::ValidationErrorKind;
use jsonschema::paths::LazyLocation;
use jsonschema::paths::Location;
use miette::Diagnostic;
use miette::SourceSpan;
use saphyr::LoadableYamlNode as _;
use saphyr::MarkedYamlOwned;
use schemars::JsonSchema;
use schemars::SchemaGenerator;
use serde::Deserialize;
mod errors;
pub mod expansion;
mod overridable;
#[doc(hidden)]
pub mod private;
pub mod types;
mod validate;
pub use apollo_configuration_macros::configuration;
pub use errors::*;
pub use overridable::Overridable;
pub use validate::ErrorCollector;
pub use validate::Validate;
pub use validate::validate;
pub trait Configuration: JsonSchema + for<'de> Deserialize<'de> + Validate {}
impl<T> Configuration for Box<T> where T: Configuration {}
pub fn export_json_schema<T: Configuration>() -> serde_json::Value {
let schema = SchemaGenerator::default().root_schema_for::<T>().to_value();
jsonschema::meta::validate(&schema).expect("generated schema is invalid");
schema
}
fn to_miette_span(yaml_span: saphyr_parser::Span) -> SourceSpan {
let start = yaml_span.start.index();
let end = yaml_span.end.index();
SourceSpan::new(start.into(), end - start)
}
fn empty_span() -> SourceSpan {
SourceSpan::new(0.into(), 0)
}
fn yaml_to_json<'a, 'b>(
location_data: &YamlLocationData,
location: LazyLocation<'a, 'b>,
yaml: serde_yaml::Value,
) -> Result<serde_json::Value, YamlToJsonError> {
let source_code = location_data.source_code.clone();
let make_span = || {
location_data
.resolve_instance_span(&(&location).into())
.unwrap_or_else(empty_span)
};
Ok(match yaml {
serde_yaml::Value::Null => serde_json::Value::Null,
serde_yaml::Value::Bool(value) => serde_json::Value::Bool(value),
serde_yaml::Value::Number(value) => {
serde_json::Value::Number(if let Some(inner) = value.as_i64() {
inner.into()
} else if let Some(inner) = value.as_f64() {
serde_json::Number::from_f64(inner).ok_or_else(|| {
YamlToJsonError::InvalidNumberError {
source_code: source_code.clone(),
label: make_span(),
}
})?
} else {
return Err(YamlToJsonError::InvalidNumberError {
source_code: source_code.clone(),
label: make_span(),
});
})
}
serde_yaml::Value::String(value) => serde_json::Value::String(value),
serde_yaml::Value::Tagged(tagged) => yaml_to_json(location_data, location, tagged.value)?,
serde_yaml::Value::Mapping(value) => serde_json::Value::Object(
value
.into_iter()
.map(|(key, value)| {
let key = key
.as_str()
.ok_or_else(|| YamlToJsonError::InvalidMappingError {
source_code: source_code.clone(),
label: make_span(),
})?;
Ok::<_, YamlToJsonError>((
key.to_string(),
yaml_to_json(location_data, location.push(key), value)?,
))
})
.collect::<Result<_, _>>()?,
),
serde_yaml::Value::Sequence(value) => serde_json::Value::Array(
value
.into_iter()
.enumerate()
.map(|(index, value)| yaml_to_json(location_data, location.push(index), value))
.collect::<Result<_, _>>()?,
),
})
}
#[derive(Debug, PartialEq)]
struct YamlLocationData {
source_code: Arc<str>,
marked_yaml: MarkedYamlOwned,
}
impl YamlLocationData {
fn parse(text: &str) -> Result<Self, errors::ConfigError> {
let source_code = Arc::from(text);
let mut loaded = MarkedYamlOwned::load_from_str(&source_code)?;
match loaded.len() {
0 => Ok(Self::empty()),
_ => Ok(Self {
source_code,
marked_yaml: loaded.remove(0),
}),
}
}
fn empty() -> Self {
Self {
source_code: Arc::from(""),
marked_yaml: MarkedYamlOwned::from_bare_yaml(saphyr::Yaml::BadValue),
}
}
fn resolve_instance_span(&self, location: &Location) -> Option<miette::SourceSpan> {
Some(to_miette_span(self.resolve_element(location)?.span))
}
fn resolve_element(&self, location: &Location) -> Option<&MarkedYamlOwned> {
let mut element = &self.marked_yaml;
for segment in location.iter() {
match segment {
jsonschema::paths::LocationSegment::Property(name) => {
element = element.data.as_mapping_get(&name)?;
}
jsonschema::paths::LocationSegment::Index(index) => {
element = element.data.as_sequence_get(index)?;
}
}
}
Some(element)
}
fn resolve_key_span(&self, location: &Location, key: &str) -> Option<miette::SourceSpan> {
let element = self.resolve_element(location)?;
let mapping = element.data.as_mapping()?;
for (k, _v) in mapping {
if k.data.as_str() == Some(key) {
return Some(to_miette_span(k.span));
}
}
None
}
fn resolve_error_path(&self, path: &serde_path_to_error::Path) -> Option<miette::SourceSpan> {
let mut element = &self.marked_yaml;
for segment in path.iter() {
match segment {
serde_path_to_error::Segment::Unknown => return None,
serde_path_to_error::Segment::Seq { index } => {
element = element.data.as_sequence_get(*index)?;
}
serde_path_to_error::Segment::Map { key } => {
element = element.data.as_mapping_get(key)?;
}
serde_path_to_error::Segment::Enum { variant } => {
element = element.data.as_mapping_get(variant)?;
}
}
}
Some(to_miette_span(element.span))
}
}
#[derive(Default)]
pub struct ParseYamlOptions {
variables: Option<Box<dyn expansion::VariableProvider>>,
}
impl ParseYamlOptions {
pub fn variables(mut self, variables: impl expansion::VariableProvider + 'static) -> Self {
self.variables = Some(Box::new(variables));
self
}
pub fn parse<T: Configuration>(&self, text: &str) -> Result<T, errors::ConfigError> {
parse_yaml(text, self)
}
}
fn resolve_schema_error_span(
location_data: &YamlLocationData,
err: &jsonschema::ValidationError<'_>,
) -> SourceSpan {
let fallback = || SourceSpan::new(0.into(), 0);
if let ValidationErrorKind::AdditionalProperties { unexpected } = err.kind()
&& let Some(first_unexpected) = unexpected.first()
&& let Some(span) = location_data.resolve_key_span(err.instance_path(), first_unexpected)
{
return span;
}
location_data
.resolve_instance_span(err.instance_path())
.unwrap_or_else(fallback)
}
pub fn parse_yaml<T: Configuration>(
text: &str,
options: &ParseYamlOptions,
) -> Result<T, errors::ConfigError> {
let location_data = YamlLocationData::parse(text)?;
let instance = serde_yaml::from_str(text)?;
let mut instance: serde_json::Value = if matches!(instance, serde_yaml::Value::Null) {
serde_json::Value::Object(Default::default())
} else {
yaml_to_json(&location_data, LazyLocation::new(), instance)?
};
if let Some(variables) = options.variables.as_deref() {
expansion::expand(&mut instance, variables, Some(&location_data))?;
}
let schema = export_json_schema::<T>();
let errors = jsonschema::validator_for(&schema)
.expect("schema is known to be valid")
.iter_errors(&instance)
.map(|err| {
Box::new(errors::ValidationError {
label: resolve_schema_error_span(&location_data, &err),
message: err.to_string(),
}) as Box<dyn Diagnostic + Send + Sync + 'static>
})
.collect::<Vec<_>>();
if !errors.is_empty() {
return Err(ConfigError::ValidationError(ValidationErrors {
source_code: location_data.source_code.clone(),
errors,
}));
}
let instance: T = match serde_path_to_error::deserialize(&instance) {
Ok(instance) => instance,
Err(err) => {
return Err(ConfigError::InvalidValue {
source_code: location_data.source_code.clone(),
label: location_data.resolve_error_path(err.path()),
error: err.into_inner(),
});
}
};
let mut errors = vec![];
instance.validate(ErrorCollector::new(&location_data, &mut errors));
if !errors.is_empty() {
return Err(ConfigError::ValidationError(ValidationErrors {
source_code: location_data.source_code.clone(),
errors,
}));
}
Ok(instance)
}
#[cfg(test)]
mod tests {
use crate::YamlLocationData;
use jsonschema::paths::Location;
#[test]
fn json_schema_instance_path() {
let yaml = YamlLocationData::parse(
r#"
core:
- a: { c: "/core/0/a/c" }
- b: { c: 2 }
"#,
)
.expect("valid YAML");
assert_eq!(
yaml.resolve_instance_span(&Location::new().join("core").join(1)),
Some(miette::SourceSpan::new(71.into(), 20)),
);
assert_eq!(
yaml.resolve_instance_span(&Location::new().join("core").join(1).join("b")),
Some(miette::SourceSpan::new(74.into(), 7)),
);
assert_eq!(
yaml.resolve_instance_span(&Location::new().join("core").join(0).join("a").join("c")),
Some(miette::SourceSpan::new(41.into(), 14)),
);
assert_eq!(
yaml.resolve_instance_span(&Location::new().join("core").join(2)),
None,
);
}
#[test]
fn empty_yaml_string_empty_yaml_location_data() {
assert_eq!(
YamlLocationData::parse("").unwrap(),
YamlLocationData::empty()
);
}
}