Skip to content

Commit 05fa824

Browse files
authored
Merge pull request #25 from koxudaxi/add-yaml-template-validation-api
Add YAML template validation API for plain scalar interpolations
2 parents 6750012 + 94ff462 commit 05fa824

8 files changed

Lines changed: 338 additions & 65 deletions

File tree

rust/json-tstring-rs/src/lib.rs

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -600,11 +600,36 @@ pub fn parse_template(template: &TemplateInput) -> BackendResult<JsonDocumentNod
600600
parse_template_with_profile(template, JsonProfile::default())
601601
}
602602

603+
pub fn parse_validated_template_with_profile(
604+
template: &TemplateInput,
605+
profile: JsonProfile,
606+
) -> BackendResult<JsonDocumentNode> {
607+
// JSON does not add format-specific post-parse validation yet. Keep the
608+
// validated entry point aligned with the other backends so callers can rely
609+
// on one API shape as backend-specific validation rules are introduced.
610+
parse_template_with_profile(template, profile)
611+
}
612+
613+
pub fn parse_validated_template(template: &TemplateInput) -> BackendResult<JsonDocumentNode> {
614+
parse_validated_template_with_profile(template, JsonProfile::default())
615+
}
616+
617+
pub fn validate_template_with_profile(
618+
template: &TemplateInput,
619+
profile: JsonProfile,
620+
) -> BackendResult<()> {
621+
parse_validated_template_with_profile(template, profile).map(|_| ())
622+
}
623+
624+
pub fn validate_template(template: &TemplateInput) -> BackendResult<()> {
625+
validate_template_with_profile(template, JsonProfile::default())
626+
}
627+
603628
pub fn check_template_with_profile(
604629
template: &TemplateInput,
605630
profile: JsonProfile,
606631
) -> BackendResult<()> {
607-
parse_template_with_profile(template, profile).map(|_| ())
632+
validate_template_with_profile(template, profile)
608633
}
609634

610635
pub fn check_template(template: &TemplateInput) -> BackendResult<()> {
@@ -615,7 +640,7 @@ pub fn format_template_with_profile(
615640
template: &TemplateInput,
616641
profile: JsonProfile,
617642
) -> BackendResult<String> {
618-
let document = parse_template_with_profile(template, profile)?;
643+
let document = parse_validated_template_with_profile(template, profile)?;
619644
format_json_value(template, &document.value)
620645
}
621646

@@ -774,9 +799,9 @@ fn normalize_number(number: &serde_json::Number) -> BackendResult<NormalizedValu
774799

775800
#[cfg(test)]
776801
mod tests {
777-
use super::{parse_template, JsonKeyValue, JsonStringPart, JsonValueNode};
802+
use super::{JsonKeyValue, JsonStringPart, JsonValueNode, parse_template};
778803
use pyo3::prelude::*;
779-
use serde_json::{json, Map, Number, Value};
804+
use serde_json::{Map, Number, Value, json};
780805
use tstring_pyo3_bindings::{extract_template, json::render_document};
781806
use tstring_syntax::{BackendError, BackendResult, ErrorKind};
782807

rust/json-tstring-rs/tests/parser.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use tstring_json::{
22
JsonKeyValue, JsonStringPart, JsonValueNode, check_template, format_template, parse_template,
3+
parse_validated_template, validate_template,
34
};
45
use tstring_syntax::{TemplateInput, TemplateInterpolation, TemplateSegment};
56

@@ -68,6 +69,18 @@ fn checks_valid_json_templates() {
6869
check_template(&template).expect("expected check success");
6970
}
7071

72+
#[test]
73+
fn validates_json_templates_with_supported_interpolations() {
74+
let template = TemplateInput::from_segments(vec![
75+
TemplateSegment::StaticText("{\"name\": ".to_owned()),
76+
interpolation(0, "name"),
77+
TemplateSegment::StaticText(", \"active\": true}".to_owned()),
78+
]);
79+
80+
validate_template(&template).expect("expected validate success");
81+
parse_validated_template(&template).expect("expected validated parse success");
82+
}
83+
7184
#[test]
7285
fn formats_json_templates_with_raw_interpolations() {
7386
let template = TemplateInput::from_segments(vec![

rust/python-bindings/src/lib.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ fn parse_json_template(
160160
) -> PyResult<Arc<JsonDocumentNode>> {
161161
json_parse_cache()
162162
.get_or_try_insert_with(&template_cache_key(template, profile.as_str()), || {
163-
tstring_json::parse_template_with_profile(template.input(), profile)
163+
tstring_json::parse_validated_template_with_profile(template.input(), profile)
164164
})
165165
.map_err(backend_error_to_py)
166166
}
@@ -171,7 +171,7 @@ fn parse_toml_template(
171171
) -> PyResult<Arc<TomlDocumentNode>> {
172172
toml_parse_cache()
173173
.get_or_try_insert_with(&template_cache_key(template, profile.as_str()), || {
174-
tstring_toml::parse_template_with_profile(template.input(), profile)
174+
tstring_toml::parse_validated_template_with_profile(template.input(), profile)
175175
})
176176
.map_err(backend_error_to_py)
177177
}
@@ -182,7 +182,7 @@ fn parse_yaml_template(
182182
) -> PyResult<Arc<YamlStreamNode>> {
183183
yaml_parse_cache()
184184
.get_or_try_insert_with(&template_cache_key(template, profile.as_str()), || {
185-
tstring_yaml::parse_template_with_profile(template.input(), profile)
185+
tstring_yaml::parse_validated_template_with_profile(template.input(), profile)
186186
})
187187
.map_err(backend_error_to_py)
188188
}

rust/toml-tstring-rs/src/lib.rs

Lines changed: 104 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1110,11 +1110,36 @@ pub fn parse_template(template: &TemplateInput) -> BackendResult<TomlDocumentNod
11101110
parse_template_with_profile(template, TomlProfile::default())
11111111
}
11121112

1113+
pub fn parse_validated_template_with_profile(
1114+
template: &TemplateInput,
1115+
profile: TomlProfile,
1116+
) -> BackendResult<TomlDocumentNode> {
1117+
// TOML does not add format-specific post-parse validation yet. Keep the
1118+
// validated entry point aligned with the other backends so callers can rely
1119+
// on one API shape as backend-specific validation rules are introduced.
1120+
parse_template_with_profile(template, profile)
1121+
}
1122+
1123+
pub fn parse_validated_template(template: &TemplateInput) -> BackendResult<TomlDocumentNode> {
1124+
parse_validated_template_with_profile(template, TomlProfile::default())
1125+
}
1126+
1127+
pub fn validate_template_with_profile(
1128+
template: &TemplateInput,
1129+
profile: TomlProfile,
1130+
) -> BackendResult<()> {
1131+
parse_validated_template_with_profile(template, profile).map(|_| ())
1132+
}
1133+
1134+
pub fn validate_template(template: &TemplateInput) -> BackendResult<()> {
1135+
validate_template_with_profile(template, TomlProfile::default())
1136+
}
1137+
11131138
pub fn check_template_with_profile(
11141139
template: &TemplateInput,
11151140
profile: TomlProfile,
11161141
) -> BackendResult<()> {
1117-
parse_template_with_profile(template, profile).map(|_| ())
1142+
validate_template_with_profile(template, profile)
11181143
}
11191144

11201145
pub fn check_template(template: &TemplateInput) -> BackendResult<()> {
@@ -1125,7 +1150,7 @@ pub fn format_template_with_profile(
11251150
template: &TemplateInput,
11261151
profile: TomlProfile,
11271152
) -> BackendResult<String> {
1128-
let document = parse_template_with_profile(template, profile)?;
1153+
let document = parse_validated_template_with_profile(template, profile)?;
11291154
format_toml_document(template, &document)
11301155
}
11311156

@@ -1390,7 +1415,7 @@ fn normalize_time(value: toml::value::Time) -> NormalizedTime {
13901415

13911416
#[cfg(test)]
13921417
mod tests {
1393-
use super::{parse_template, TomlKeySegmentValue, TomlStatementNode, TomlValueNode};
1418+
use super::{TomlKeySegmentValue, TomlStatementNode, TomlValueNode, parse_template};
13941419
use pyo3::prelude::*;
13951420
use tstring_pyo3_bindings::{extract_template, toml::render_document};
13961421
use tstring_syntax::{BackendError, BackendResult, ErrorKind};
@@ -1901,10 +1926,12 @@ mod tests {
19011926
);
19021927
let special_floats = table["special_float_array"].as_array().expect("array");
19031928
assert!(special_floats[0].as_float().expect("float").is_infinite());
1904-
assert!(special_floats[1]
1905-
.as_float()
1906-
.expect("float")
1907-
.is_sign_negative());
1929+
assert!(
1930+
special_floats[1]
1931+
.as_float()
1932+
.expect("float")
1933+
.is_sign_negative()
1934+
);
19081935
assert!(special_floats[2].as_float().expect("float").is_nan());
19091936
assert_eq!(
19101937
table["special_float_nested_arrays"]
@@ -1916,33 +1943,43 @@ mod tests {
19161943
let special_float_deeper_arrays = table["special_float_deeper_arrays"]
19171944
.as_array()
19181945
.expect("array");
1919-
assert!(special_float_deeper_arrays[0][0][0]
1920-
.as_float()
1921-
.expect("float")
1922-
.is_infinite());
1923-
assert!(special_float_deeper_arrays[1][0][0]
1924-
.as_float()
1925-
.expect("float")
1926-
.is_sign_negative());
1927-
assert!(special_float_deeper_arrays[2][0][0]
1928-
.as_float()
1929-
.expect("float")
1930-
.is_nan());
1946+
assert!(
1947+
special_float_deeper_arrays[0][0][0]
1948+
.as_float()
1949+
.expect("float")
1950+
.is_infinite()
1951+
);
1952+
assert!(
1953+
special_float_deeper_arrays[1][0][0]
1954+
.as_float()
1955+
.expect("float")
1956+
.is_sign_negative()
1957+
);
1958+
assert!(
1959+
special_float_deeper_arrays[2][0][0]
1960+
.as_float()
1961+
.expect("float")
1962+
.is_nan()
1963+
);
19311964
assert_eq!(
19321965
table["upper_exp_nested_mixed"]
19331966
.as_array()
19341967
.expect("array")
19351968
.len(),
19361969
2
19371970
);
1938-
assert!(table["special_float_inline_table"]["pos"]
1939-
.as_float()
1940-
.expect("float")
1941-
.is_infinite());
1942-
assert!(table["special_float_inline_table"]["nan"]
1943-
.as_float()
1944-
.expect("float")
1945-
.is_nan());
1971+
assert!(
1972+
table["special_float_inline_table"]["pos"]
1973+
.as_float()
1974+
.expect("float")
1975+
.is_infinite()
1976+
);
1977+
assert!(
1978+
table["special_float_inline_table"]["nan"]
1979+
.as_float()
1980+
.expect("float")
1981+
.is_nan()
1982+
);
19461983
assert_eq!(
19471984
table["special_float_mixed_nested"]
19481985
.as_array()
@@ -2026,9 +2063,10 @@ mod tests {
20262063
Err(err) => err,
20272064
};
20282065
assert_eq!(err.kind, ErrorKind::Parse);
2029-
assert!(err
2030-
.message
2031-
.contains("single-line basic strings cannot contain newlines"));
2066+
assert!(
2067+
err.message
2068+
.contains("single-line basic strings cannot contain newlines")
2069+
);
20322070
});
20332071
}
20342072

@@ -2920,30 +2958,42 @@ mod tests {
29202958
rendered.text,
29212959
"special_float_inline_table = { pos = +inf, neg = -inf, nan = nan }\nspecial_float_mixed_nested = [[+inf, -inf], [nan]]"
29222960
);
2923-
assert!(rendered.data["special_float_inline_table"]["pos"]
2924-
.as_float()
2925-
.expect("pos float")
2926-
.is_infinite());
2927-
assert!(rendered.data["special_float_inline_table"]["neg"]
2928-
.as_float()
2929-
.expect("neg float")
2930-
.is_sign_negative());
2931-
assert!(rendered.data["special_float_inline_table"]["nan"]
2932-
.as_float()
2933-
.expect("nan float")
2934-
.is_nan());
2935-
assert!(rendered.data["special_float_mixed_nested"][0][0]
2936-
.as_float()
2937-
.expect("nested pos")
2938-
.is_infinite());
2939-
assert!(rendered.data["special_float_mixed_nested"][0][1]
2940-
.as_float()
2941-
.expect("nested neg")
2942-
.is_sign_negative());
2943-
assert!(rendered.data["special_float_mixed_nested"][1][0]
2944-
.as_float()
2945-
.expect("nested nan")
2946-
.is_nan());
2961+
assert!(
2962+
rendered.data["special_float_inline_table"]["pos"]
2963+
.as_float()
2964+
.expect("pos float")
2965+
.is_infinite()
2966+
);
2967+
assert!(
2968+
rendered.data["special_float_inline_table"]["neg"]
2969+
.as_float()
2970+
.expect("neg float")
2971+
.is_sign_negative()
2972+
);
2973+
assert!(
2974+
rendered.data["special_float_inline_table"]["nan"]
2975+
.as_float()
2976+
.expect("nan float")
2977+
.is_nan()
2978+
);
2979+
assert!(
2980+
rendered.data["special_float_mixed_nested"][0][0]
2981+
.as_float()
2982+
.expect("nested pos")
2983+
.is_infinite()
2984+
);
2985+
assert!(
2986+
rendered.data["special_float_mixed_nested"][0][1]
2987+
.as_float()
2988+
.expect("nested neg")
2989+
.is_sign_negative()
2990+
);
2991+
assert!(
2992+
rendered.data["special_float_mixed_nested"][1][0]
2993+
.as_float()
2994+
.expect("nested nan")
2995+
.is_nan()
2996+
);
29472997
});
29482998
}
29492999

rust/toml-tstring-rs/tests/parser.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use tstring_syntax::{TemplateInput, TemplateInterpolation, TemplateSegment};
22
use tstring_toml::{
3-
check_template, format_template, parse_template, TomlStatementNode, TomlValueNode,
3+
TomlStatementNode, TomlValueNode, check_template, format_template, parse_template,
4+
parse_validated_template, validate_template,
45
};
56

67
fn interpolation(index: usize, expression: &str) -> TemplateSegment {
@@ -64,6 +65,24 @@ fn checks_valid_toml_templates() {
6465
check_template(&template).expect("expected check success");
6566
}
6667

68+
#[test]
69+
fn validates_toml_templates_with_supported_interpolations() {
70+
let template = TemplateInput::from_segments(vec![
71+
TemplateSegment::StaticText("title = ".to_owned()),
72+
TemplateSegment::Interpolation(TemplateInterpolation {
73+
expression: "title".to_owned(),
74+
conversion: None,
75+
format_spec: String::new(),
76+
interpolation_index: 0,
77+
raw_source: Some("{title}".to_owned()),
78+
}),
79+
TemplateSegment::StaticText("\n".to_owned()),
80+
]);
81+
82+
validate_template(&template).expect("expected validate success");
83+
parse_validated_template(&template).expect("expected validated parse success");
84+
}
85+
6786
#[test]
6887
fn formats_toml_templates_with_raw_interpolations() {
6988
let template = TemplateInput::from_segments(vec![

0 commit comments

Comments
 (0)