use crate::collection::ValueTemplate;
use slumber_template::{Template, TemplateParseError, Value};
use thiserror::Error;
impl ValueTemplate {
pub fn from_raw_json(json: serde_json::Value) -> Self {
match json {
serde_json::Value::Null => Self::Null,
serde_json::Value::Bool(b) => Self::Boolean(b),
serde_json::Value::Number(n) => Self::from_json_number(n),
serde_json::Value::String(s) => Self::String(Template::raw(s)),
serde_json::Value::Array(values) => Self::Array(
values.into_iter().map(Self::from_raw_json).collect(),
),
serde_json::Value::Object(map) => Self::Object(
map.into_iter()
.map(|(key, value)| {
(Template::raw(key), Self::from_raw_json(value))
})
.collect(),
),
}
}
pub fn from_json_number(n: serde_json::Number) -> Self {
if let Some(i) = n.as_i64() {
Self::Integer(i)
} else if let Some(f) = n.as_f64() {
Self::Float(f)
} else {
unreachable!(
"serde_json doesn't support >64-bit numbers with \
arbitrary_precision disabled"
);
}
}
pub fn parse_json(s: &str) -> Result<Self, JsonTemplateError> {
let json: serde_json::Value = serde_json::from_str(s)?;
let mapped = json.try_into()?;
Ok(mapped)
}
}
impl TryFrom<serde_json::Value> for ValueTemplate {
type Error = TemplateParseError;
fn try_from(json: serde_json::Value) -> Result<Self, Self::Error> {
let mapped = match json {
primitive @ (serde_json::Value::Null
| serde_json::Value::Bool(_)
| serde_json::Value::Number(_)) => {
ValueTemplate::from_raw_json(primitive)
}
serde_json::Value::String(s) => s.parse()?,
serde_json::Value::Array(values) => Self::Array(
values
.into_iter()
.map(Self::try_from)
.collect::<Result<Vec<_>, _>>()?,
),
serde_json::Value::Object(map) => Self::Object(
map.into_iter()
.map(|(key, value)| {
let key = key.parse()?;
let value = value.try_into()?;
Ok::<_, TemplateParseError>((key, value))
})
.collect::<Result<_, _>>()?,
),
};
Ok(mapped)
}
}
impl TryFrom<serde_yaml::Value> for ValueTemplate {
type Error = YamlTemplateError;
fn try_from(yaml: serde_yaml::Value) -> Result<Self, Self::Error> {
let mapped = match yaml {
serde_yaml::Value::Null => Self::Null,
serde_yaml::Value::Bool(b) => Self::Boolean(b),
serde_yaml::Value::Number(n) => {
if let Some(i) = n.as_i64() {
Self::Integer(i)
} else if let Some(f) = n.as_f64() {
Self::Float(f)
} else {
unreachable!("serde_yaml doesn't support >64-bit numbers");
}
}
serde_yaml::Value::String(s) => s.parse()?,
serde_yaml::Value::Sequence(values) => Self::Array(
values
.into_iter()
.map(Self::try_from)
.collect::<Result<Vec<_>, _>>()?,
),
serde_yaml::Value::Mapping(map) => Self::Object(
map.into_iter()
.map(|(key, value)| {
let key = key
.as_str()
.ok_or_else(|| {
YamlTemplateError::InvalidKey(key.clone())
})?
.parse()?;
let value = value.try_into()?;
Ok::<_, YamlTemplateError>((key, value))
})
.collect::<Result<_, _>>()?,
),
serde_yaml::Value::Tagged(value) => value.value.try_into()?,
};
Ok(mapped)
}
}
#[derive(Debug, Error)]
pub enum JsonTemplateError {
#[error(transparent)]
JsonParse(#[from] serde_json::Error),
#[error(transparent)]
TemplateParse(#[from] TemplateParseError),
}
#[derive(Debug, Error)]
pub enum YamlTemplateError {
#[error(transparent)]
YamlParse(#[from] serde_yaml::Error),
#[error(transparent)]
TemplateParse(#[from] TemplateParseError),
#[error("Mapping keys must be strings, but received: {0:?}")]
InvalidKey(serde_yaml::Value),
}
pub fn value_to_json(value: Value) -> serde_json::Value {
match value {
Value::Null => serde_json::Value::Null,
Value::Boolean(b) => b.into(),
Value::Integer(i) => i.into(),
Value::Float(f) => f.into(),
Value::String(s) => s.into(),
Value::Array(array) => array.into_iter().map(value_to_json).collect(),
Value::Object(object) => object
.into_iter()
.map(|(key, value)| (key, value_to_json(value)))
.collect(),
Value::Bytes(bytes) => bytes.to_vec().into(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
use serde_json::json;
use serde_yaml::{
Mapping,
value::{Tag, TaggedValue},
};
use slumber_util::assert_result;
#[rstest]
#[case::null(serde_json::Value::Null, ValueTemplate::Null)]
#[case::valid_template(json!("{{valid}}"), "{_{valid}}".into())]
#[case::invalid_template(json!("{{invalid"), "{_{invalid".into())]
fn test_from_raw_json(
#[case] json: serde_json::Value,
#[case] expected: ValueTemplate,
) {
assert_eq!(ValueTemplate::from_raw_json(json), expected);
}
#[rstest]
#[case::int(3.into(), 3.into())]
#[case::int_too_big(
serde_json::Number::from(u64::MAX), (u64::MAX as f64).into()
)]
#[case::float(serde_json::Number::from_f64(42.9).unwrap(), 42.9.into())]
fn test_from_json_num(
#[case] number: serde_json::Number,
#[case] expected: ValueTemplate,
) {
assert_eq!(ValueTemplate::from_json_number(number), expected);
}
#[rstest]
#[case::null("null", Ok(ValueTemplate::Null))]
#[case::object(r#"{"{{w}}": 3}"#, Ok(vec![("{{ w }}", 3)].into()))]
#[case::error_invalid_template_key(
r#"{"{{invalid": 3}"#,
Err("invalid expression")
)]
fn test_parse_json(
#[case] s: &str,
#[case] expected: Result<ValueTemplate, &str>,
) {
assert_result(ValueTemplate::parse_json(s), expected);
}
#[rstest]
#[case::null(json!(null), Ok(ValueTemplate::Null))]
#[case::template_string(json!("{{ w }}"), Ok("{{w}}".into()))]
#[case::template_key(json!({"{{ w }}": 3}), Ok(vec![("{{w}}", 3)].into()))]
#[case::error_invalid_template_key(
json!({"{{ invalid_key": {"name": "{{ username }}"}}),
Err("invalid expression")
)]
#[case::error_invalid_template_value(
json!({"key": "{{ invalid"}), Err("invalid expression")
)]
fn test_from_json(
#[case] json: serde_json::Value,
#[case] expected: Result<ValueTemplate, &str>,
) {
assert_result(ValueTemplate::try_from(json), expected);
}
#[test]
fn test_arbitrary_precision_disabled() {
assert_eq!(
serde_json::Number::from_i128(i128::from(u64::MAX) + 1),
None
);
}
#[rstest]
#[case::null(serde_yaml::Value::Null, Ok(ValueTemplate::Null))]
#[case::int_too_big(u64::MAX.into(), Ok((u64::MAX as f64).into()))]
#[case::float(42.9.into(), Ok(42.9.into()))]
#[case::float_inf(f64::INFINITY.into(), Ok(f64::INFINITY.into()))]
#[case::template_string("{{ w }}".into(), Ok("{{w}}".into()))]
#[case::template_key(
Mapping::from_iter([("{{w}}".into(), 3.into())]).into(),
Ok(vec![("{{w}}", 3)].into()),
)]
#[case::error_invalid_template_key(
Mapping::from_iter([("{{invalid".into(), 3.into())]).into(),
Err("invalid expression"),
)]
#[case::error_invalid_template_value(
Mapping::from_iter([("valid".into(), "{{invalid".into())]).into(),
Err("invalid expression"),
)]
#[case::tagged(
// Tags are thrown out, but the inner value is used
serde_yaml::Value::Tagged(TaggedValue {
tag: Tag::new("test"),
value: "{{w}}".into(),
}.into()),
Ok("{{ w }}".into()),
)]
fn test_from_yaml(
#[case] yaml: serde_yaml::Value,
#[case] expected: Result<ValueTemplate, &str>,
) {
assert_result(ValueTemplate::try_from(yaml), expected);
}
}