use apollo_configuration::ErrorCollector;
use apollo_configuration::Overridable;
use apollo_configuration::Validate;
use apollo_configuration::configuration;
use apollo_configuration::parse_yaml;
use miette::EyreContext as _;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
fn print_miette(diagnostic: &dyn miette::Diagnostic) -> String {
struct F<'a>(&'a dyn miette::Diagnostic);
impl std::fmt::Display for F<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
miette::MietteHandlerOpts::new()
.color(false)
.build()
.debug(self.0, f)
}
}
F(diagnostic).to_string()
}
#[configuration]
struct ApqSubgraphConfig {
#[allow(unused)]
enabled: bool,
#[config(range(min = 0, max = 1024))]
#[allow(unused)]
cache_size: usize,
}
#[test]
fn incorrect_type_raises_error() {
let err = parse_yaml::<ApqSubgraphConfig>("{ enabled: 1, cache_size: 1 }", &Default::default())
.expect_err("should be error");
insta::assert_snapshot!(print_miette(&err), @r#"
apollo::configuration::schema
× schema validation error
╰─▶ schema validation error
Error: apollo::configuration::validation
× 1 is not of type "boolean"
╭────
1 │ { enabled: 1, cache_size: 1 }
· ┬
· ╰── 1 is not of type "boolean"
╰────
"#);
}
#[test]
fn correct_schema_type_but_not_deserializable() {
#[derive(Debug, Clone)]
struct Oops;
impl JsonSchema for Oops {
fn schema_id() -> std::borrow::Cow<'static, str> {
"oops".into()
}
fn schema_name() -> std::borrow::Cow<'static, str> {
"Oops".into()
}
fn inline_schema() -> bool {
true
}
fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::json_schema!({ "type": "number" })
}
}
impl Validate for Oops {}
impl<'de> Deserialize<'de> for Oops {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct Visitor;
impl<'de> serde::de::Visitor<'de> for Visitor {
type Value = Oops;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(formatter, "")
}
fn visit_u64<E>(self, _v: u64) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Err(E::custom("always fails"))
}
}
deserializer.deserialize_u64(Visitor)
}
}
#[configuration]
struct NonZeroConfig {
#[config(default = Oops)]
value: Oops,
}
let err = parse_yaml::<NonZeroConfig>("{ value: 0 }", &Default::default())
.expect_err("should be error");
insta::assert_snapshot!(print_miette(&err), @r"
apollo::configuration::parse
× failed to parse value: always fails
╭────
1 │ { value: 0 }
· ┬
· ╰── this value could not be parsed
╰────
");
}
#[test]
fn nan_is_not_supported() {
let err =
parse_yaml::<ApqSubgraphConfig>("{ enabled: true, cache_size: .nan }", &Default::default())
.expect_err("should be error");
insta::assert_snapshot!(print_miette(&err), @r"
apollo::configuration::invalid_number
× unsupported value
╰─▶ unsupported value
╭────
1 │ { enabled: true, cache_size: .nan }
· ──┬─
· ╰── NaN/Infinity are not suported in configuration
╰────
");
}
#[test]
fn infinity_is_not_supported() {
let err =
parse_yaml::<ApqSubgraphConfig>("{ enabled: true, cache_size: .inf }", &Default::default())
.expect_err("should be error");
insta::assert_snapshot!(print_miette(&err), @r"
apollo::configuration::invalid_number
× unsupported value
╰─▶ unsupported value
╭────
1 │ { enabled: true, cache_size: .inf }
· ──┬─
· ╰── NaN/Infinity are not suported in configuration
╰────
");
}
#[test]
fn unrepresentable_value_raises_validation_error() {
let err =
parse_yaml::<ApqSubgraphConfig>("{ enabled: true, cache_size: -1 }", &Default::default())
.expect_err("should be error");
insta::assert_snapshot!(print_miette(&err), @r"
apollo::configuration::schema
× schema validation error
╰─▶ schema validation error
Error: apollo::configuration::validation
× -1 is less than the minimum of 0
╭────
1 │ { enabled: true, cache_size: -1 }
· ─┬
· ╰── -1 is less than the minimum of 0
╰────
");
}
#[test]
fn out_of_range_value_raises_validation_error() {
let err = parse_yaml::<ApqSubgraphConfig>(
"{ enabled: true, cache_size: 12345 }",
&Default::default(),
)
.expect_err("should be error");
insta::assert_snapshot!(print_miette(&err), @r"
apollo::configuration::schema
× schema validation error
╰─▶ schema validation error
Error: apollo::configuration::validation
× 12345 is greater than the maximum of 1024
╭────
1 │ { enabled: true, cache_size: 12345 }
· ──┬──
· ╰── 12345 is greater than the maximum of 1024
╰────
");
}
#[test]
fn additional_properties_not_allowed() {
let err = parse_yaml::<ApqSubgraphConfig>(
"{ enabled: true, cache_size: 10, unknown: should_error }",
&Default::default(),
)
.expect_err("should be error");
insta::assert_snapshot!(print_miette(&err), @r"
apollo::configuration::schema
× schema validation error
╰─▶ schema validation error
Error: apollo::configuration::validation
× Additional properties are not allowed ('unknown' was unexpected)
╭────
1 │ { enabled: true, cache_size: 10, unknown: should_error }
· ───┬───
· ╰── Additional properties are not allowed ('unknown' was unexpected)
╰────
");
}
#[test]
fn regex_patterns_are_enforced() {
#[configuration]
struct SampleConfig {
#[config(pattern(r"^\d+$"))]
#[allow(unused)]
numeric_string: String,
}
parse_yaml::<SampleConfig>(r#"numeric_string: "20""#, &Default::default())
.expect("should pass pattern");
parse_yaml::<SampleConfig>(r#"numeric_string: "20a""#, &Default::default())
.expect_err("should not pass pattern");
}
#[test]
fn custom_type_validation() {
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
struct AlphanumericString(String);
impl Validate for AlphanumericString {
fn validate(&self, mut errors: ErrorCollector<'_>) {
if !self.0.chars().all(|c| c.is_alphanumeric()) {
errors.report_simple("must be alphanumeric");
}
}
}
#[configuration]
struct Config {
#[config(required)]
alpha: AlphanumericString,
}
parse_yaml::<Config>("alpha: abcdef123", &Default::default()).expect("should be valid");
let err = parse_yaml::<Config>("alpha: abcd!f123", &Default::default())
.expect_err("should report a validation error");
insta::assert_snapshot!(print_miette(&err), @r"
apollo::configuration::schema
× schema validation error
╰─▶ schema validation error
Error: apollo::configuration::validation
× must be alphanumeric
╭────
1 │ alpha: abcd!f123
· ────┬────
· ╰── must be alphanumeric
╰────
");
}
#[test]
fn custom_type_validation_traces_lists() {
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
struct AlphanumericString(String);
impl Validate for AlphanumericString {
fn validate(&self, mut errors: ErrorCollector<'_>) {
if !self.0.chars().all(|c| c.is_alphanumeric()) {
errors.report_simple("must be alphanumeric");
}
}
}
#[configuration]
struct Nested {
alphas: Vec<AlphanumericString>,
}
#[configuration]
struct Config {
nested: Nested,
}
let err = parse_yaml::<Config>(
r#"
nested:
alphas:
- valid
- abcd!f123
"#,
&Default::default(),
)
.expect_err("should report a validation error");
insta::assert_snapshot!(print_miette(&err), @r"
apollo::configuration::schema
× schema validation error
╰─▶ schema validation error
Error: apollo::configuration::validation
× must be alphanumeric
╭─[5:7]
4 │ - valid
5 │ - abcd!f123
· ────┬────
· ╰── must be alphanumeric
6 │
╰────
");
}
#[test]
fn custom_tye_validation_through_overrides() {
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
struct AlphanumericString(String);
impl Validate for AlphanumericString {
fn validate(&self, mut errors: ErrorCollector<'_>) {
if !self.0.chars().all(|c| c.is_alphanumeric()) {
errors.report_simple("must be alphanumeric");
}
}
}
#[configuration]
struct Config {
#[config(required)]
alpha: Overridable<AlphanumericString>,
}
parse_yaml::<Config>(
"
alpha:
global: abcdef123
overrides:
key: abcdef456
",
&Default::default(),
)
.expect("should be valid");
let err = parse_yaml::<Config>(
"
alpha:
global: abcdef!123
overrides:
key: abcdef456
",
&Default::default(),
)
.expect_err("should report a validation error in the global part");
insta::assert_snapshot!(print_miette(&err), @r"
apollo::configuration::schema
× schema validation error
╰─▶ schema validation error
Error: apollo::configuration::validation
× must be alphanumeric
╭─[3:13]
2 │ alpha:
3 │ global: abcdef!123
· ─────┬────
· ╰── must be alphanumeric
4 │ overrides:
╰────
");
let err = parse_yaml::<Config>(
"
alpha:
global: abcdef123
overrides:
key: abcdef!456
",
&Default::default(),
)
.expect_err("should report a validation error in the overrides part");
insta::assert_snapshot!(print_miette(&err), @r"
apollo::configuration::schema
× schema validation error
╰─▶ schema validation error
Error: apollo::configuration::validation
× must be alphanumeric
╭─[5:14]
4 │ overrides:
5 │ key: abcdef!456
· ─────┬────
· ╰── must be alphanumeric
6 │
╰────
");
}
#[test]
fn cross_field_custom_validation() {
#[configuration]
#[derive(PartialEq, Eq)]
struct Nested {
a: usize,
b: usize,
}
#[configuration(validate = validate_everything)]
struct Config {
nested: Nested,
other_nested: Nested,
}
fn validate_everything(config: &Config, mut errors: ErrorCollector<'_>) {
if config.nested == config.other_nested {
let labels = vec![
miette::LabeledSpan::at(errors.nest("nested").span(), "one value specified here"),
miette::LabeledSpan::at(
errors.nest("other_nested").span(),
"other value specified here",
),
];
errors.report(miette::diagnostic!(
code = "test::conflicting_nested",
labels = labels,
"`nested` and `other_nested` must not have the same values",
));
}
}
let err = parse_yaml::<Config>(
r#"
nested: { a: 1, b: 2 }
other_nested: { a: 1, b: 2 }
"#,
&Default::default(),
)
.expect_err("should report a validation error");
insta::assert_snapshot!(print_miette(&err), @r"
apollo::configuration::schema
× schema validation error
╰─▶ schema validation error
Error: test::conflicting_nested
× `nested` and `other_nested` must not have the same values
╭─[2:9]
1 │
2 │ nested: { a: 1, b: 2 }
· ──────┬──────
· ╰── one value specified here
3 │ other_nested: { a: 1, b: 2 }
· ──────┬──────
· ╰── other value specified here
4 │
╰────
");
}
#[test]
fn structure_or_enum_validation_only_runs_with_valid_fields() {
#[configuration(validate = validate_struct)]
struct StructConfig {
#[config(validate = always_errors)]
erroring_field: usize,
}
#[configuration]
struct ValidConfig {
valid_field: usize,
}
#[configuration]
struct InvalidConfig {
#[config(validate = always_errors)]
erroring_field: usize,
}
#[configuration(validate = validate_enum)]
enum EnumConfig {
#[config(default)]
Valid(ValidConfig),
Invalid(InvalidConfig),
}
#[configuration]
struct Config {
s: StructConfig,
e: EnumConfig,
}
fn always_errors(_v: &usize, mut errors: ErrorCollector<'_>) {
errors.report_simple("you're cooked");
}
fn validate_struct(_config: &StructConfig, _errors: ErrorCollector<'_>) {
panic!("struct validator should not be called when field is invalid");
}
fn validate_enum(_config: &EnumConfig, _errors: ErrorCollector<'_>) {
panic!("enum validator should not be called when field is invalid");
}
let err = parse_yaml::<Config>(
r#"
s: { erroring_field: 0 }
e: { invalid: { erroring_field: 0 } }
"#,
&Default::default(),
)
.expect_err("should report a validation error");
insta::assert_snapshot!(print_miette(&err), @r"
apollo::configuration::schema
× schema validation error
╰─▶ schema validation error
Error: apollo::configuration::validation
× you're cooked
╭─[2:22]
1 │
2 │ s: { erroring_field: 0 }
· ┬
· ╰── you're cooked
3 │ e: { invalid: { erroring_field: 0 } }
╰────
Error: apollo::configuration::validation
× you're cooked
╭─[3:33]
2 │ s: { erroring_field: 0 }
3 │ e: { invalid: { erroring_field: 0 } }
· ┬
· ╰── you're cooked
4 │
╰────
");
}
#[test]
fn validate_enum_field() {
#[configuration]
enum EnumConfig {
#[config(default)]
Valid { valid_field: usize },
Invalid {
#[config(validate = always_errors)]
erroring_field: usize,
},
}
#[configuration]
struct Config {
e: EnumConfig,
}
fn always_errors(_v: &usize, mut errors: ErrorCollector<'_>) {
errors.report_simple("you're cooked");
}
parse_yaml::<Config>(
r#"
e: { valid: { valid_field: 0 } }
"#,
&Default::default(),
)
.expect("should parse and validate");
let err = parse_yaml::<Config>(
r#"
e: { invalid: { erroring_field: 0 } }
"#,
&Default::default(),
)
.expect_err("should report a validation error");
insta::assert_snapshot!(print_miette(&err), @r"
apollo::configuration::schema
× schema validation error
╰─▶ schema validation error
Error: apollo::configuration::validation
× you're cooked
╭─[2:33]
1 │
2 │ e: { invalid: { erroring_field: 0 } }
· ┬
· ╰── you're cooked
3 │
╰────
");
}