269 lines
7.0 KiB
Rust
269 lines
7.0 KiB
Rust
use std::collections::BTreeMap;
|
|
|
|
use decodal::{Constraint, Data, DecodalDecode, DecodalSchema, EmptyLoader, Engine, Value};
|
|
use decodal_derive::Decodal;
|
|
|
|
#[derive(Debug, PartialEq, Decodal)]
|
|
struct Service {
|
|
name: String,
|
|
#[decodal(gt = 443, default = 8443)]
|
|
port: i64,
|
|
#[decodal(default = true, rename = "feature.enable")]
|
|
feature_enabled: bool,
|
|
#[decodal(default)]
|
|
tags: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Decodal)]
|
|
struct Worker {
|
|
name: String,
|
|
#[decodal(default = true)]
|
|
enabled: bool,
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Decodal)]
|
|
struct Fleet {
|
|
workers: Vec<Worker>,
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Decodal)]
|
|
struct OpenConfig {
|
|
name: String,
|
|
#[decodal(default = true, rename = "feature.enabled")]
|
|
enabled: bool,
|
|
#[decodal(rest)]
|
|
extra: BTreeMap<String, Data>,
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Decodal)]
|
|
struct StringLabels {
|
|
name: String,
|
|
#[decodal(rest)]
|
|
labels: BTreeMap<String, String>,
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Decodal)]
|
|
struct OpenBag {
|
|
#[decodal(rest)]
|
|
values: BTreeMap<String, Data>,
|
|
}
|
|
|
|
#[test]
|
|
fn derives_schema_and_decode() {
|
|
let mut engine = Engine::new(EmptyLoader);
|
|
engine
|
|
.bind_global("Service", Service::decodal_schema())
|
|
.unwrap();
|
|
let module = engine
|
|
.add_root_source(
|
|
"test",
|
|
"test",
|
|
r#"
|
|
Service & {
|
|
name = "api";
|
|
port = 9443;
|
|
feature.enable = false;
|
|
tags = ["web", "prod"];
|
|
}
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
let value = engine.eval_module(module).unwrap();
|
|
let data = engine.materialize(&value).unwrap();
|
|
let service = Service::decodal_decode(&data).unwrap();
|
|
assert_eq!(
|
|
service,
|
|
Service {
|
|
name: "api".into(),
|
|
port: 9443,
|
|
feature_enabled: false,
|
|
tags: vec!["web".into(), "prod".into()],
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn defaults_are_available_in_schema() {
|
|
let mut engine = Engine::new(EmptyLoader);
|
|
engine
|
|
.bind_global("Service", Service::decodal_schema())
|
|
.unwrap();
|
|
let module = engine
|
|
.add_root_source(
|
|
"test",
|
|
"test",
|
|
r#"
|
|
Service & {
|
|
name = "api";
|
|
}
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
let value = engine.eval_module(module).unwrap();
|
|
let data = engine.materialize(&value).unwrap();
|
|
let service = Service::decodal_decode(&data).unwrap();
|
|
assert_eq!(service.port, 8443);
|
|
assert!(service.feature_enabled);
|
|
assert!(service.tags.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn decode_reports_field_path() {
|
|
let data = Data::Object(vec![]);
|
|
let error = Service::decodal_decode(&data).unwrap_err();
|
|
assert_eq!(error.path, "name");
|
|
}
|
|
|
|
#[test]
|
|
fn vec_schema_rejects_invalid_element_before_decode() {
|
|
let mut engine = Engine::new(EmptyLoader);
|
|
engine
|
|
.bind_global("Service", Service::decodal_schema())
|
|
.unwrap();
|
|
let module = engine
|
|
.add_root_source(
|
|
"test",
|
|
"test",
|
|
r#"
|
|
Service & {
|
|
name = "api";
|
|
port = 9443;
|
|
tags = ["web", 1];
|
|
}
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
let error = engine.eval_module(module).unwrap_err();
|
|
assert!(
|
|
error
|
|
.labels
|
|
.iter()
|
|
.any(|label| label.message.contains("array element [1]"))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn vec_schema_applies_nested_struct_defaults() {
|
|
let mut engine = Engine::new(EmptyLoader);
|
|
engine
|
|
.bind_global("Fleet", Fleet::decodal_schema())
|
|
.unwrap();
|
|
let module = engine
|
|
.add_root_source(
|
|
"test",
|
|
"test",
|
|
r#"
|
|
Fleet & {
|
|
workers = [{ name = "api"; }];
|
|
}
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
let value = engine.eval_module(module).unwrap();
|
|
let data = engine.materialize(&value).unwrap();
|
|
let fleet = Fleet::decodal_decode(&data).unwrap();
|
|
assert_eq!(
|
|
fleet,
|
|
Fleet {
|
|
workers: vec![Worker {
|
|
name: "api".into(),
|
|
enabled: true,
|
|
}],
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn rest_map_opens_the_derived_object_and_collects_additional_fields() {
|
|
let schema = OpenConfig::decodal_schema();
|
|
let Value::Object {
|
|
fields,
|
|
rest: Some(rest),
|
|
} = schema
|
|
else {
|
|
panic!("derived schema should have an object rest range")
|
|
};
|
|
assert_eq!(fields.len(), 2);
|
|
assert!(matches!(
|
|
*rest,
|
|
Value::Range { ref constraints, .. }
|
|
if constraints == &[Constraint::Unknown]
|
|
));
|
|
|
|
let mut engine = Engine::new(EmptyLoader);
|
|
engine
|
|
.bind_global("OpenConfig", OpenConfig::decodal_schema())
|
|
.unwrap();
|
|
let module = engine
|
|
.add_root_source(
|
|
"test",
|
|
"test",
|
|
r#"
|
|
{
|
|
name = "api";
|
|
plugin = { name = "cache"; };
|
|
retries = 3;
|
|
} as OpenConfig
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
let value = engine.eval_module(module).unwrap();
|
|
let data = engine.materialize(&value).unwrap();
|
|
let config = OpenConfig::decodal_decode(&data).unwrap();
|
|
|
|
assert_eq!(config.name, "api");
|
|
assert!(config.enabled);
|
|
assert_eq!(config.extra.get("retries"), Some(&Data::Int(3)));
|
|
assert!(matches!(config.extra.get("plugin"), Some(Data::Object(_))));
|
|
assert!(!config.extra.contains_key("name"));
|
|
assert!(!config.extra.contains_key("feature"));
|
|
}
|
|
|
|
#[test]
|
|
fn typed_rest_map_constrains_each_additional_field() {
|
|
let mut engine = Engine::new(EmptyLoader);
|
|
engine
|
|
.bind_global("StringLabels", StringLabels::decodal_schema())
|
|
.unwrap();
|
|
let module = engine
|
|
.add_root_source(
|
|
"test",
|
|
"test",
|
|
r#"{ name = "service"; region = "ap-northeast-1"; tier = "edge"; } as StringLabels"#,
|
|
)
|
|
.unwrap();
|
|
let value = engine.eval_module(module).unwrap();
|
|
let data = engine.materialize(&value).unwrap();
|
|
let labels = StringLabels::decodal_decode(&data).unwrap();
|
|
assert_eq!(labels.labels.get("region"), Some(&"ap-northeast-1".into()));
|
|
assert_eq!(labels.labels.get("tier"), Some(&"edge".into()));
|
|
|
|
let module = engine
|
|
.add_root_source(
|
|
"invalid",
|
|
"invalid",
|
|
r#"{ name = "service"; priority = 1; } as StringLabels"#,
|
|
)
|
|
.unwrap();
|
|
assert!(engine.eval_module(module).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn rest_only_struct_collects_every_field() {
|
|
let schema = OpenBag::decodal_schema();
|
|
assert!(matches!(
|
|
schema,
|
|
Value::Object {
|
|
ref fields,
|
|
rest: Some(_),
|
|
} if fields.is_empty()
|
|
));
|
|
|
|
let data = Data::Object(vec![decodal::runtime::DataField {
|
|
name: "answer".into(),
|
|
value: Data::Int(42),
|
|
}]);
|
|
let bag = OpenBag::decodal_decode(&data).unwrap();
|
|
assert_eq!(bag.values.get("answer"), Some(&Data::Int(42)));
|
|
}
|