tstring-bindings 0.2.1__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. {tstring_bindings-0.2.1 → tstring_bindings-0.2.2}/Cargo.lock +8 -8
  2. {tstring_bindings-0.2.1 → tstring_bindings-0.2.2}/Cargo.toml +1 -1
  3. {tstring_bindings-0.2.1 → tstring_bindings-0.2.2}/PKG-INFO +1 -1
  4. {tstring_bindings-0.2.1 → tstring_bindings-0.2.2}/json-tstring-rs/Cargo.toml +1 -1
  5. {tstring_bindings-0.2.1 → tstring_bindings-0.2.2}/json-tstring-rs/src/lib.rs +100 -2
  6. {tstring_bindings-0.2.1 → tstring_bindings-0.2.2}/json-tstring-rs/tests/parser.rs +31 -3
  7. {tstring_bindings-0.2.1 → tstring_bindings-0.2.2}/pyproject.toml +1 -1
  8. {tstring_bindings-0.2.1 → tstring_bindings-0.2.2}/python-bindings/Cargo.toml +5 -5
  9. {tstring_bindings-0.2.1 → tstring_bindings-0.2.2}/python-bindings/src/lib.rs +1 -1
  10. {tstring_bindings-0.2.1 → tstring_bindings-0.2.2}/toml-tstring-rs/Cargo.toml +1 -1
  11. {tstring_bindings-0.2.1 → tstring_bindings-0.2.2}/toml-tstring-rs/src/lib.rs +108 -4
  12. {tstring_bindings-0.2.1 → tstring_bindings-0.2.2}/toml-tstring-rs/tests/parser.rs +40 -3
  13. {tstring_bindings-0.2.1 → tstring_bindings-0.2.2}/tstring-core-rs/src/lib.rs +22 -0
  14. {tstring_bindings-0.2.1 → tstring_bindings-0.2.2}/tstring-pyo3-bindings/Cargo.toml +4 -4
  15. {tstring_bindings-0.2.1 → tstring_bindings-0.2.2}/tstring-pyo3-bindings/benches/render_paths.rs +29 -5
  16. {tstring_bindings-0.2.1 → tstring_bindings-0.2.2}/yaml-tstring-rs/Cargo.toml +1 -1
  17. {tstring_bindings-0.2.1 → tstring_bindings-0.2.2}/yaml-tstring-rs/src/lib.rs +172 -3
  18. {tstring_bindings-0.2.1 → tstring_bindings-0.2.2}/yaml-tstring-rs/tests/parser.rs +62 -2
  19. {tstring_bindings-0.2.1 → tstring_bindings-0.2.2}/README.md +0 -0
  20. {tstring_bindings-0.2.1 → tstring_bindings-0.2.2}/json-tstring-rs/README.md +0 -0
  21. {tstring_bindings-0.2.1 → tstring_bindings-0.2.2}/json-tstring-rs/tests/conformance.rs +0 -0
  22. {tstring_bindings-0.2.1 → tstring_bindings-0.2.2}/python/tstring_bindings/__init__.py +0 -0
  23. {tstring_bindings-0.2.1 → tstring_bindings-0.2.2}/python/tstring_bindings/__init__.pyi +0 -0
  24. {tstring_bindings-0.2.1 → tstring_bindings-0.2.2}/python/tstring_bindings/_profiles.py +0 -0
  25. {tstring_bindings-0.2.1 → tstring_bindings-0.2.2}/python/tstring_bindings/_types.py +0 -0
  26. {tstring_bindings-0.2.1 → tstring_bindings-0.2.2}/python/tstring_bindings/py.typed +0 -0
  27. {tstring_bindings-0.2.1 → tstring_bindings-0.2.2}/python/tstring_bindings/tstring_bindings.pyi +0 -0
  28. {tstring_bindings-0.2.1 → tstring_bindings-0.2.2}/python-bindings/README.md +0 -0
  29. {tstring_bindings-0.2.1 → tstring_bindings-0.2.2}/toml-tstring-rs/README.md +0 -0
  30. {tstring_bindings-0.2.1 → tstring_bindings-0.2.2}/toml-tstring-rs/tests/conformance.rs +0 -0
  31. {tstring_bindings-0.2.1 → tstring_bindings-0.2.2}/tstring-core-rs/Cargo.toml +0 -0
  32. {tstring_bindings-0.2.1 → tstring_bindings-0.2.2}/tstring-core-rs/README.md +0 -0
  33. {tstring_bindings-0.2.1 → tstring_bindings-0.2.2}/tstring-pyo3-bindings/src/json.rs +0 -0
  34. {tstring_bindings-0.2.1 → tstring_bindings-0.2.2}/tstring-pyo3-bindings/src/lib.rs +0 -0
  35. {tstring_bindings-0.2.1 → tstring_bindings-0.2.2}/tstring-pyo3-bindings/src/toml.rs +0 -0
  36. {tstring_bindings-0.2.1 → tstring_bindings-0.2.2}/tstring-pyo3-bindings/src/yaml.rs +0 -0
  37. {tstring_bindings-0.2.1 → tstring_bindings-0.2.2}/yaml-tstring-rs/README.md +0 -0
  38. {tstring_bindings-0.2.1 → tstring_bindings-0.2.2}/yaml-tstring-rs/test-support/renderer_layout.rs +0 -0
  39. {tstring_bindings-0.2.1 → tstring_bindings-0.2.2}/yaml-tstring-rs/tests/conformance.rs +0 -0
  40. {tstring_bindings-0.2.1 → tstring_bindings-0.2.2}/yaml-tstring-rs/tests/normalized.rs +0 -0
@@ -725,7 +725,7 @@ checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607"
725
725
 
726
726
  [[package]]
727
727
  name = "tstring-backend-e2e-tests"
728
- version = "0.2.1"
728
+ version = "0.2.2"
729
729
  dependencies = [
730
730
  "tstring-json",
731
731
  "tstring-syntax",
@@ -735,7 +735,7 @@ dependencies = [
735
735
 
736
736
  [[package]]
737
737
  name = "tstring-bindings"
738
- version = "0.2.1"
738
+ version = "0.2.2"
739
739
  dependencies = [
740
740
  "pyo3",
741
741
  "pythonize",
@@ -751,7 +751,7 @@ dependencies = [
751
751
 
752
752
  [[package]]
753
753
  name = "tstring-json"
754
- version = "0.2.1"
754
+ version = "0.2.2"
755
755
  dependencies = [
756
756
  "serde_json",
757
757
  "toml",
@@ -760,7 +760,7 @@ dependencies = [
760
760
 
761
761
  [[package]]
762
762
  name = "tstring-pyo3-bindings"
763
- version = "0.2.1"
763
+ version = "0.2.2"
764
764
  dependencies = [
765
765
  "criterion",
766
766
  "pyo3",
@@ -776,14 +776,14 @@ dependencies = [
776
776
 
777
777
  [[package]]
778
778
  name = "tstring-syntax"
779
- version = "0.2.1"
779
+ version = "0.2.2"
780
780
  dependencies = [
781
781
  "num-bigint",
782
782
  ]
783
783
 
784
784
  [[package]]
785
785
  name = "tstring-toml"
786
- version = "0.2.1"
786
+ version = "0.2.2"
787
787
  dependencies = [
788
788
  "serde_json",
789
789
  "toml",
@@ -792,7 +792,7 @@ dependencies = [
792
792
 
793
793
  [[package]]
794
794
  name = "tstring-yaml"
795
- version = "0.2.1"
795
+ version = "0.2.2"
796
796
  dependencies = [
797
797
  "saphyr",
798
798
  "saphyr-parser",
@@ -803,7 +803,7 @@ dependencies = [
803
803
 
804
804
  [[package]]
805
805
  name = "tstring-yaml-pyo3-tests"
806
- version = "0.2.1"
806
+ version = "0.2.2"
807
807
  dependencies = [
808
808
  "pyo3",
809
809
  "saphyr",
@@ -9,7 +9,7 @@ homepage = "https://github.com/koxudaxi/tstring-structured-data"
9
9
  license = "MIT"
10
10
  repository = "https://github.com/koxudaxi/tstring-structured-data"
11
11
  rust-version = "1.94.0"
12
- version = "0.2.1"
12
+ version = "0.2.2"
13
13
 
14
14
  [workspace.dependencies]
15
15
  num-bigint = "0.4.6"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tstring-bindings
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -18,7 +18,7 @@ test = false
18
18
 
19
19
  [dependencies]
20
20
  serde_json = { workspace = true }
21
- tstring-syntax = { version = "0.2.1", path = "../tstring-core-rs" }
21
+ tstring-syntax = { version = "0.2.2", path = "../tstring-core-rs" }
22
22
 
23
23
  [dev-dependencies]
24
24
  toml = { workspace = true }
@@ -1,10 +1,15 @@
1
1
  use serde_json::Value;
2
2
  use std::str::FromStr;
3
3
  use tstring_syntax::{
4
- BackendError, BackendResult, NormalizedDocument, NormalizedFloat, NormalizedKey,
5
- NormalizedStream, NormalizedValue, SourcePosition, SourceSpan, StreamItem, TemplateInput,
4
+ BackendError, BackendResult, InterpolationTypeRequirement, NormalizedDocument, NormalizedFloat,
5
+ NormalizedKey, NormalizedStream, NormalizedValue, SourcePosition, SourceSpan, StreamItem,
6
+ TemplateInput,
6
7
  };
7
8
 
9
+ const JSON_VALUE_PYTHON_TYPE: &str =
10
+ "str | int | float | bool | None | dict[str, object] | list[object]";
11
+ const STRING_PYTHON_TYPE: &str = "str";
12
+
8
13
  #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
9
14
  pub enum JsonProfile {
10
15
  Rfc8259,
@@ -636,6 +641,99 @@ pub fn check_template(template: &TemplateInput) -> BackendResult<()> {
636
641
  check_template_with_profile(template, JsonProfile::default())
637
642
  }
638
643
 
644
+ pub fn interpolation_type_requirements_with_profile(
645
+ template: &TemplateInput,
646
+ profile: JsonProfile,
647
+ ) -> BackendResult<Vec<InterpolationTypeRequirement>> {
648
+ let document = parse_validated_template_with_profile(template, profile)?;
649
+ let mut requirements = Vec::new();
650
+ collect_json_value_type_requirements(&document.value, &mut requirements);
651
+ requirements.sort_by_key(|requirement| requirement.interpolation_index);
652
+ Ok(requirements)
653
+ }
654
+
655
+ pub fn interpolation_type_requirements(
656
+ template: &TemplateInput,
657
+ ) -> BackendResult<Vec<InterpolationTypeRequirement>> {
658
+ interpolation_type_requirements_with_profile(template, JsonProfile::default())
659
+ }
660
+
661
+ fn collect_json_value_type_requirements(
662
+ value: &JsonValueNode,
663
+ requirements: &mut Vec<InterpolationTypeRequirement>,
664
+ ) {
665
+ match value {
666
+ JsonValueNode::String(node) => {
667
+ collect_json_string_type_requirements(node, requirements);
668
+ }
669
+ JsonValueNode::Literal(_) => {}
670
+ JsonValueNode::Interpolation(node) => {
671
+ requirements.push(json_type_requirement(node));
672
+ }
673
+ JsonValueNode::Object(node) => {
674
+ for member in &node.members {
675
+ collect_json_key_type_requirements(&member.key, requirements);
676
+ collect_json_value_type_requirements(&member.value, requirements);
677
+ }
678
+ }
679
+ JsonValueNode::Array(node) => {
680
+ for item in &node.items {
681
+ collect_json_value_type_requirements(item, requirements);
682
+ }
683
+ }
684
+ }
685
+ }
686
+
687
+ fn collect_json_key_type_requirements(
688
+ key: &JsonKeyNode,
689
+ requirements: &mut Vec<InterpolationTypeRequirement>,
690
+ ) {
691
+ match &key.value {
692
+ JsonKeyValue::String(node) => {
693
+ collect_json_string_type_requirements(node, requirements);
694
+ }
695
+ JsonKeyValue::Interpolation(node) => {
696
+ requirements.push(json_type_requirement(node));
697
+ }
698
+ }
699
+ }
700
+
701
+ fn collect_json_string_type_requirements(
702
+ string: &JsonStringNode,
703
+ requirements: &mut Vec<InterpolationTypeRequirement>,
704
+ ) {
705
+ for chunk in &string.chunks {
706
+ if let JsonStringPart::Interpolation(node) = chunk {
707
+ requirements.push(json_type_requirement(node));
708
+ }
709
+ }
710
+ }
711
+
712
+ fn json_type_requirement(node: &JsonInterpolationNode) -> InterpolationTypeRequirement {
713
+ match node.role.as_str() {
714
+ "value" => InterpolationTypeRequirement::new(
715
+ node.interpolation_index,
716
+ JSON_VALUE_PYTHON_TYPE,
717
+ "json value",
718
+ ),
719
+ "key" => InterpolationTypeRequirement::new(
720
+ node.interpolation_index,
721
+ STRING_PYTHON_TYPE,
722
+ "json object key",
723
+ ),
724
+ "string_fragment" => InterpolationTypeRequirement::new(
725
+ node.interpolation_index,
726
+ STRING_PYTHON_TYPE,
727
+ "json string fragment",
728
+ ),
729
+ _ => InterpolationTypeRequirement::new(
730
+ node.interpolation_index,
731
+ JSON_VALUE_PYTHON_TYPE,
732
+ "json interpolation",
733
+ ),
734
+ }
735
+ }
736
+
639
737
  pub fn format_template_with_profile(
640
738
  template: &TemplateInput,
641
739
  profile: JsonProfile,
@@ -1,8 +1,10 @@
1
1
  use tstring_json::{
2
- JsonKeyValue, JsonStringPart, JsonValueNode, check_template, format_template, parse_template,
3
- parse_validated_template, validate_template,
2
+ JsonKeyValue, JsonStringPart, JsonValueNode, check_template, format_template,
3
+ interpolation_type_requirements, parse_template, parse_validated_template, validate_template,
4
+ };
5
+ use tstring_syntax::{
6
+ InterpolationTypeRequirement, TemplateInput, TemplateInterpolation, TemplateSegment,
4
7
  };
5
- use tstring_syntax::{TemplateInput, TemplateInterpolation, TemplateSegment};
6
8
 
7
9
  fn interpolation(index: usize, expression: &str) -> TemplateSegment {
8
10
  TemplateSegment::Interpolation(TemplateInterpolation {
@@ -69,6 +71,32 @@ fn checks_valid_json_templates() {
69
71
  check_template(&template).expect("expected check success");
70
72
  }
71
73
 
74
+ #[test]
75
+ fn reports_contextual_interpolation_type_requirements() {
76
+ let template = TemplateInput::from_segments(vec![
77
+ TemplateSegment::StaticText("{".to_owned()),
78
+ interpolation(0, "key"),
79
+ TemplateSegment::StaticText(": ".to_owned()),
80
+ interpolation(1, "value"),
81
+ TemplateSegment::StaticText(", \"label\": \"".to_owned()),
82
+ interpolation(2, "label"),
83
+ TemplateSegment::StaticText("\"}".to_owned()),
84
+ ]);
85
+
86
+ assert_eq!(
87
+ interpolation_type_requirements(&template).expect("expected type requirements"),
88
+ vec![
89
+ InterpolationTypeRequirement::new(0, "str", "json object key"),
90
+ InterpolationTypeRequirement::new(
91
+ 1,
92
+ "str | int | float | bool | None | dict[str, object] | list[object]",
93
+ "json value"
94
+ ),
95
+ InterpolationTypeRequirement::new(2, "str", "json string fragment"),
96
+ ]
97
+ );
98
+ }
99
+
72
100
  #[test]
73
101
  fn validates_json_templates_with_supported_interpolations() {
74
102
  let template = TemplateInput::from_segments(vec![
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tstring-bindings"
3
- version = "0.2.1"
3
+ version = "0.2.2"
4
4
  description = "Native Python bindings for t-string structured data backends"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -24,12 +24,12 @@ pyo3 = { workspace = true, features = ["abi3-py314"] }
24
24
  pythonize = { workspace = true }
25
25
  saphyr = { workspace = true }
26
26
  serde_json = { workspace = true }
27
- tstring-json = { version = "0.2.1", path = "../json-tstring-rs" }
28
- tstring-pyo3-bindings = { version = "0.2.1", path = "../tstring-pyo3-bindings" }
29
- tstring-syntax = { version = "0.2.1", path = "../tstring-core-rs" }
27
+ tstring-json = { version = "0.2.2", path = "../json-tstring-rs" }
28
+ tstring-pyo3-bindings = { version = "0.2.2", path = "../tstring-pyo3-bindings" }
29
+ tstring-syntax = { version = "0.2.2", path = "../tstring-core-rs" }
30
30
  toml = { workspace = true }
31
- tstring-toml = { version = "0.2.1", path = "../toml-tstring-rs" }
32
- tstring-yaml = { version = "0.2.1", path = "../yaml-tstring-rs" }
31
+ tstring-toml = { version = "0.2.2", path = "../toml-tstring-rs" }
32
+ tstring-yaml = { version = "0.2.2", path = "../yaml-tstring-rs" }
33
33
 
34
34
  [dev-dependencies]
35
35
  pyo3 = { workspace = true, features = ["auto-initialize"] }
@@ -627,7 +627,7 @@ fn normalized_offset_to_python(py: Python<'_>, offset_minutes: i16) -> PyResult<
627
627
 
628
628
  #[pymodule]
629
629
  fn tstring_bindings(py: Python<'_>, module: &Bound<'_, PyModule>) -> PyResult<()> {
630
- module.add("__version__", "0.2.1")?;
630
+ module.add("__version__", "0.2.2")?;
631
631
  module.add("__contract_version__", CONTRACT_VERSION)?;
632
632
  module.add("__contract_symbols__", PyTuple::new(py, CONTRACT_SYMBOLS)?)?;
633
633
  module.add("TemplateError", py.get_type::<TemplateError>())?;
@@ -19,4 +19,4 @@ test = false
19
19
  [dependencies]
20
20
  serde_json = { workspace = true }
21
21
  toml = { workspace = true }
22
- tstring-syntax = { version = "0.2.1", path = "../tstring-core-rs" }
22
+ tstring-syntax = { version = "0.2.2", path = "../tstring-core-rs" }
@@ -1,10 +1,13 @@
1
1
  use tstring_syntax::{
2
- BackendError, BackendResult, NormalizedDate, NormalizedDocument, NormalizedEntry,
3
- NormalizedFloat, NormalizedKey, NormalizedLocalDateTime, NormalizedOffsetDateTime,
4
- NormalizedStream, NormalizedTemporal, NormalizedTime, NormalizedValue, SourcePosition,
5
- SourceSpan, StreamItem, TemplateInput,
2
+ BackendError, BackendResult, InterpolationTypeRequirement, NormalizedDate, NormalizedDocument,
3
+ NormalizedEntry, NormalizedFloat, NormalizedKey, NormalizedLocalDateTime,
4
+ NormalizedOffsetDateTime, NormalizedStream, NormalizedTemporal, NormalizedTime,
5
+ NormalizedValue, SourcePosition, SourceSpan, StreamItem, TemplateInput,
6
6
  };
7
7
 
8
+ const TOML_VALUE_PYTHON_TYPE: &str = "str | int | float | bool | datetime.date | datetime.time | datetime.datetime | list[object] | dict[str, object]";
9
+ const STRING_PYTHON_TYPE: &str = "str";
10
+
8
11
  #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
9
12
  pub enum TomlProfile {
10
13
  V1_0,
@@ -1146,6 +1149,107 @@ pub fn check_template(template: &TemplateInput) -> BackendResult<()> {
1146
1149
  check_template_with_profile(template, TomlProfile::default())
1147
1150
  }
1148
1151
 
1152
+ pub fn interpolation_type_requirements_with_profile(
1153
+ template: &TemplateInput,
1154
+ profile: TomlProfile,
1155
+ ) -> BackendResult<Vec<InterpolationTypeRequirement>> {
1156
+ let document = parse_validated_template_with_profile(template, profile)?;
1157
+ let mut requirements = Vec::new();
1158
+ collect_toml_document_type_requirements(&document, &mut requirements);
1159
+ requirements.sort_by_key(|requirement| requirement.interpolation_index);
1160
+ Ok(requirements)
1161
+ }
1162
+
1163
+ pub fn interpolation_type_requirements(
1164
+ template: &TemplateInput,
1165
+ ) -> BackendResult<Vec<InterpolationTypeRequirement>> {
1166
+ interpolation_type_requirements_with_profile(template, TomlProfile::default())
1167
+ }
1168
+
1169
+ fn collect_toml_document_type_requirements(
1170
+ document: &TomlDocumentNode,
1171
+ requirements: &mut Vec<InterpolationTypeRequirement>,
1172
+ ) {
1173
+ for statement in &document.statements {
1174
+ match statement {
1175
+ TomlStatementNode::Assignment(node) => {
1176
+ collect_toml_key_path_type_requirements(&node.key_path, requirements);
1177
+ collect_toml_value_type_requirements(&node.value, requirements);
1178
+ }
1179
+ TomlStatementNode::TableHeader(node) => {
1180
+ collect_toml_key_path_type_requirements(&node.key_path, requirements);
1181
+ }
1182
+ TomlStatementNode::ArrayTableHeader(node) => {
1183
+ collect_toml_key_path_type_requirements(&node.key_path, requirements);
1184
+ }
1185
+ }
1186
+ }
1187
+ }
1188
+
1189
+ fn collect_toml_key_path_type_requirements(
1190
+ key_path: &TomlKeyPathNode,
1191
+ requirements: &mut Vec<InterpolationTypeRequirement>,
1192
+ ) {
1193
+ for segment in &key_path.segments {
1194
+ match &segment.value {
1195
+ TomlKeySegmentValue::Bare(_) => {}
1196
+ TomlKeySegmentValue::String(node) => {
1197
+ collect_toml_string_type_requirements(node, requirements);
1198
+ }
1199
+ TomlKeySegmentValue::Interpolation(node) => {
1200
+ requirements.push(InterpolationTypeRequirement::new(
1201
+ node.interpolation_index,
1202
+ STRING_PYTHON_TYPE,
1203
+ "toml key",
1204
+ ));
1205
+ }
1206
+ }
1207
+ }
1208
+ }
1209
+
1210
+ fn collect_toml_value_type_requirements(
1211
+ value: &TomlValueNode,
1212
+ requirements: &mut Vec<InterpolationTypeRequirement>,
1213
+ ) {
1214
+ match value {
1215
+ TomlValueNode::String(node) => collect_toml_string_type_requirements(node, requirements),
1216
+ TomlValueNode::Literal(_) => {}
1217
+ TomlValueNode::Interpolation(node) => {
1218
+ requirements.push(InterpolationTypeRequirement::new(
1219
+ node.interpolation_index,
1220
+ TOML_VALUE_PYTHON_TYPE,
1221
+ "toml value",
1222
+ ));
1223
+ }
1224
+ TomlValueNode::Array(node) => {
1225
+ for item in &node.items {
1226
+ collect_toml_value_type_requirements(item, requirements);
1227
+ }
1228
+ }
1229
+ TomlValueNode::InlineTable(node) => {
1230
+ for entry in &node.entries {
1231
+ collect_toml_key_path_type_requirements(&entry.key_path, requirements);
1232
+ collect_toml_value_type_requirements(&entry.value, requirements);
1233
+ }
1234
+ }
1235
+ }
1236
+ }
1237
+
1238
+ fn collect_toml_string_type_requirements(
1239
+ string: &TomlStringNode,
1240
+ requirements: &mut Vec<InterpolationTypeRequirement>,
1241
+ ) {
1242
+ for chunk in &string.chunks {
1243
+ if let TomlStringPart::Interpolation(node) = chunk {
1244
+ requirements.push(InterpolationTypeRequirement::new(
1245
+ node.interpolation_index,
1246
+ STRING_PYTHON_TYPE,
1247
+ "toml string fragment",
1248
+ ));
1249
+ }
1250
+ }
1251
+ }
1252
+
1149
1253
  pub fn format_template_with_profile(
1150
1254
  template: &TemplateInput,
1151
1255
  profile: TomlProfile,
@@ -1,7 +1,9 @@
1
- use tstring_syntax::{TemplateInput, TemplateInterpolation, TemplateSegment};
1
+ use tstring_syntax::{
2
+ InterpolationTypeRequirement, TemplateInput, TemplateInterpolation, TemplateSegment,
3
+ };
2
4
  use tstring_toml::{
3
- TomlStatementNode, TomlValueNode, check_template, format_template, parse_template,
4
- parse_validated_template, validate_template,
5
+ TomlStatementNode, TomlValueNode, check_template, format_template,
6
+ interpolation_type_requirements, parse_template, parse_validated_template, validate_template,
5
7
  };
6
8
 
7
9
  fn interpolation(index: usize, expression: &str) -> TemplateSegment {
@@ -83,6 +85,41 @@ fn validates_toml_templates_with_supported_interpolations() {
83
85
  parse_validated_template(&template).expect("expected validated parse success");
84
86
  }
85
87
 
88
+ #[test]
89
+ fn reports_contextual_interpolation_type_requirements() {
90
+ let template = TemplateInput::from_segments(vec![
91
+ interpolation(0, "table"),
92
+ TemplateSegment::StaticText(".title = ".to_owned()),
93
+ interpolation(1, "value"),
94
+ TemplateSegment::StaticText("\nmessage = \"Hello ".to_owned()),
95
+ interpolation(2, "fragment"),
96
+ TemplateSegment::StaticText("\"\nmeta = { ".to_owned()),
97
+ interpolation(3, "inline_key"),
98
+ TemplateSegment::StaticText(" = ".to_owned()),
99
+ interpolation(4, "inline_value"),
100
+ TemplateSegment::StaticText(" }\n".to_owned()),
101
+ ]);
102
+
103
+ assert_eq!(
104
+ interpolation_type_requirements(&template).expect("expected type requirements"),
105
+ vec![
106
+ InterpolationTypeRequirement::new(0, "str", "toml key"),
107
+ InterpolationTypeRequirement::new(
108
+ 1,
109
+ "str | int | float | bool | datetime.date | datetime.time | datetime.datetime | list[object] | dict[str, object]",
110
+ "toml value"
111
+ ),
112
+ InterpolationTypeRequirement::new(2, "str", "toml string fragment"),
113
+ InterpolationTypeRequirement::new(3, "str", "toml key"),
114
+ InterpolationTypeRequirement::new(
115
+ 4,
116
+ "str | int | float | bool | datetime.date | datetime.time | datetime.datetime | list[object] | dict[str, object]",
117
+ "toml value"
118
+ ),
119
+ ]
120
+ );
121
+ }
122
+
86
123
  #[test]
87
124
  fn formats_toml_templates_with_raw_interpolations() {
88
125
  let template = TemplateInput::from_segments(vec![
@@ -176,6 +176,28 @@ impl std::error::Error for BackendError {}
176
176
 
177
177
  pub type BackendResult<T> = Result<T, BackendError>;
178
178
 
179
+ #[derive(Clone, Debug, PartialEq, Eq)]
180
+ pub struct InterpolationTypeRequirement {
181
+ pub interpolation_index: usize,
182
+ pub expected_python_type: String,
183
+ pub expected_description: String,
184
+ }
185
+
186
+ impl InterpolationTypeRequirement {
187
+ #[must_use]
188
+ pub fn new(
189
+ interpolation_index: usize,
190
+ expected_python_type: impl Into<String>,
191
+ expected_description: impl Into<String>,
192
+ ) -> Self {
193
+ Self {
194
+ interpolation_index,
195
+ expected_python_type: expected_python_type.into(),
196
+ expected_description: expected_description.into(),
197
+ }
198
+ }
199
+ }
200
+
179
201
  #[derive(Clone, Debug, PartialEq)]
180
202
  pub struct NormalizedStream {
181
203
  pub documents: Vec<NormalizedDocument>,
@@ -16,10 +16,10 @@ serde_json = { workspace = true }
16
16
  saphyr = { workspace = true }
17
17
  saphyr-parser = { workspace = true }
18
18
  toml = { workspace = true }
19
- tstring-json = { version = "0.2.1", path = "../json-tstring-rs" }
20
- tstring-syntax = { version = "0.2.1", path = "../tstring-core-rs" }
21
- tstring-toml = { version = "0.2.1", path = "../toml-tstring-rs" }
22
- tstring-yaml = { version = "0.2.1", path = "../yaml-tstring-rs" }
19
+ tstring-json = { version = "0.2.2", path = "../json-tstring-rs" }
20
+ tstring-syntax = { version = "0.2.2", path = "../tstring-core-rs" }
21
+ tstring-toml = { version = "0.2.2", path = "../toml-tstring-rs" }
22
+ tstring-yaml = { version = "0.2.2", path = "../yaml-tstring-rs" }
23
23
 
24
24
  [dev-dependencies]
25
25
  criterion = "0.8.2"
@@ -11,7 +11,7 @@ fn benchmark_json_metadata_render(criterion: &mut Criterion) {
11
11
  let module = PyModule::from_code(
12
12
  py,
13
13
  pyo3::ffi::c_str!(
14
- "value=3.14159\nlabel='service'\nitems=[1, 2, 3]\nmeta={'region': 'us-east-1', 'count': 3}\ntemplate=t'{\"format\": {value:.2f}, \"label\": \"{label!s}\", \"items\": {items}, \"meta\": {meta}, \"fragment\": \"pi={value:.2f}\"}'\n"
14
+ "value=3.14159\nlabel='service'\nitems=[1, 2, 3]\nmeta={'region': 'us-east-1', 'count': 3}\ntemplate=t'{{\"format\": {value:.2f}, \"label\": \"{label!s}\", \"items\": {items}, \"meta\": {meta}, \"fragment\": \"pi={value:.2f}\"}}'\n"
15
15
  ),
16
16
  pyo3::ffi::c_str!("bench_json_metadata.py"),
17
17
  pyo3::ffi::c_str!("bench_json_metadata"),
@@ -26,7 +26,13 @@ fn benchmark_json_metadata_render(criterion: &mut Criterion) {
26
26
  criterion.bench_function("json_metadata_render", |bench| {
27
27
  bench.iter(|| {
28
28
  Python::with_gil(|py| {
29
- json_backend::render_document(py, &template, node.as_ref()).unwrap();
29
+ json_backend::render_document(
30
+ py,
31
+ &template,
32
+ tstring_json::JsonProfile::default(),
33
+ node.as_ref(),
34
+ )
35
+ .unwrap();
30
36
  });
31
37
  });
32
38
  });
@@ -52,7 +58,13 @@ fn benchmark_toml_metadata_render(criterion: &mut Criterion) {
52
58
  criterion.bench_function("toml_metadata_render", |bench| {
53
59
  bench.iter(|| {
54
60
  Python::with_gil(|py| {
55
- toml_backend::render_document(py, &template, node.as_ref()).unwrap();
61
+ toml_backend::render_document(
62
+ py,
63
+ &template,
64
+ tstring_toml::TomlProfile::default(),
65
+ node.as_ref(),
66
+ )
67
+ .unwrap();
56
68
  });
57
69
  });
58
70
  });
@@ -78,7 +90,13 @@ fn benchmark_toml_dynamic_header_render(criterion: &mut Criterion) {
78
90
  criterion.bench_function("toml_dynamic_header_render", |bench| {
79
91
  bench.iter(|| {
80
92
  Python::with_gil(|py| {
81
- toml_backend::render_document(py, &template, node.as_ref()).unwrap();
93
+ toml_backend::render_document(
94
+ py,
95
+ &template,
96
+ tstring_toml::TomlProfile::default(),
97
+ node.as_ref(),
98
+ )
99
+ .unwrap();
82
100
  });
83
101
  });
84
102
  });
@@ -104,7 +122,13 @@ fn benchmark_yaml_metadata_render(criterion: &mut Criterion) {
104
122
  criterion.bench_function("yaml_metadata_render", |bench| {
105
123
  bench.iter(|| {
106
124
  Python::with_gil(|py| {
107
- yaml_backend::render_document(py, &template, node.as_ref()).unwrap();
125
+ yaml_backend::render_document(
126
+ py,
127
+ &template,
128
+ tstring_yaml::YamlProfile::default(),
129
+ node.as_ref(),
130
+ )
131
+ .unwrap();
108
132
  });
109
133
  });
110
134
  });
@@ -20,7 +20,7 @@ test = false
20
20
  saphyr = { workspace = true }
21
21
  saphyr-parser = { workspace = true }
22
22
  serde_json = { workspace = true }
23
- tstring-syntax = { version = "0.2.1", path = "../tstring-core-rs" }
23
+ tstring-syntax = { version = "0.2.2", path = "../tstring-core-rs" }
24
24
 
25
25
  [dev-dependencies]
26
26
  toml = { workspace = true }
@@ -3,11 +3,14 @@ use saphyr_parser::{ScalarStyle, Tag};
3
3
  use std::borrow::Cow;
4
4
  use std::str::FromStr;
5
5
  use tstring_syntax::{
6
- BackendError, BackendResult, NormalizedDocument, NormalizedEntry, NormalizedFloat,
7
- NormalizedKey, NormalizedKeyEntry, NormalizedStream, NormalizedValue, SourcePosition,
8
- SourceSpan, StreamItem, TemplateInput,
6
+ BackendError, BackendResult, InterpolationTypeRequirement, NormalizedDocument, NormalizedEntry,
7
+ NormalizedFloat, NormalizedKey, NormalizedKeyEntry, NormalizedStream, NormalizedValue,
8
+ SourcePosition, SourceSpan, StreamItem, TemplateInput,
9
9
  };
10
10
 
11
+ const YAML_VALUE_PYTHON_TYPE: &str = "str | int | float | bool | None | datetime.date | datetime.time | datetime.datetime | list[object] | dict[object, object]";
12
+ const STRING_PYTHON_TYPE: &str = "str";
13
+
11
14
  #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
12
15
  pub enum YamlProfile {
13
16
  V1_2_2,
@@ -2084,6 +2087,172 @@ pub fn check_template(template: &TemplateInput) -> BackendResult<()> {
2084
2087
  check_template_with_profile(template, YamlProfile::default())
2085
2088
  }
2086
2089
 
2090
+ pub fn interpolation_type_requirements_with_profile(
2091
+ template: &TemplateInput,
2092
+ profile: YamlProfile,
2093
+ ) -> BackendResult<Vec<InterpolationTypeRequirement>> {
2094
+ let stream = parse_validated_template_with_profile(template, profile)?;
2095
+ let mut requirements = Vec::new();
2096
+ collect_yaml_stream_type_requirements(&stream, &mut requirements);
2097
+ requirements.sort_by_key(|requirement| requirement.interpolation_index);
2098
+ Ok(requirements)
2099
+ }
2100
+
2101
+ pub fn interpolation_type_requirements(
2102
+ template: &TemplateInput,
2103
+ ) -> BackendResult<Vec<InterpolationTypeRequirement>> {
2104
+ interpolation_type_requirements_with_profile(template, YamlProfile::default())
2105
+ }
2106
+
2107
+ #[derive(Clone, Copy, Debug, Eq, PartialEq)]
2108
+ enum YamlInterpolationContext {
2109
+ Value,
2110
+ MappingKey,
2111
+ }
2112
+
2113
+ fn collect_yaml_stream_type_requirements(
2114
+ stream: &YamlStreamNode,
2115
+ requirements: &mut Vec<InterpolationTypeRequirement>,
2116
+ ) {
2117
+ for document in &stream.documents {
2118
+ collect_yaml_value_type_requirements(
2119
+ &document.value,
2120
+ YamlInterpolationContext::Value,
2121
+ requirements,
2122
+ );
2123
+ }
2124
+ }
2125
+
2126
+ fn collect_yaml_value_type_requirements(
2127
+ value: &YamlValueNode,
2128
+ context: YamlInterpolationContext,
2129
+ requirements: &mut Vec<InterpolationTypeRequirement>,
2130
+ ) {
2131
+ match value {
2132
+ YamlValueNode::Scalar(node) => {
2133
+ collect_yaml_scalar_type_requirements(node, "yaml scalar fragment", requirements);
2134
+ }
2135
+ YamlValueNode::Interpolation(node) => {
2136
+ requirements.push(yaml_value_type_requirement(node, context));
2137
+ }
2138
+ YamlValueNode::Mapping(node) => {
2139
+ for entry in &node.entries {
2140
+ collect_yaml_key_type_requirements(&entry.key, requirements);
2141
+ collect_yaml_value_type_requirements(
2142
+ &entry.value,
2143
+ YamlInterpolationContext::Value,
2144
+ requirements,
2145
+ );
2146
+ }
2147
+ }
2148
+ YamlValueNode::Sequence(node) => {
2149
+ for item in &node.items {
2150
+ collect_yaml_value_type_requirements(item, context, requirements);
2151
+ }
2152
+ }
2153
+ YamlValueNode::Decorated(node) => {
2154
+ if let Some(tag) = &node.tag {
2155
+ collect_yaml_chunk_type_requirements(
2156
+ &tag.chunks,
2157
+ "yaml metadata fragment",
2158
+ requirements,
2159
+ );
2160
+ }
2161
+ if let Some(anchor) = &node.anchor {
2162
+ collect_yaml_chunk_type_requirements(
2163
+ &anchor.chunks,
2164
+ "yaml metadata fragment",
2165
+ requirements,
2166
+ );
2167
+ }
2168
+ collect_yaml_value_type_requirements(&node.value, context, requirements);
2169
+ }
2170
+ }
2171
+ }
2172
+
2173
+ fn collect_yaml_key_type_requirements(
2174
+ key: &YamlKeyNode,
2175
+ requirements: &mut Vec<InterpolationTypeRequirement>,
2176
+ ) {
2177
+ match &key.value {
2178
+ YamlKeyValue::Scalar(node) => {
2179
+ collect_yaml_scalar_type_requirements(node, "yaml scalar fragment", requirements);
2180
+ }
2181
+ YamlKeyValue::Interpolation(node) => {
2182
+ requirements.push(yaml_value_type_requirement(
2183
+ node,
2184
+ YamlInterpolationContext::MappingKey,
2185
+ ));
2186
+ }
2187
+ YamlKeyValue::Complex(node) => {
2188
+ collect_yaml_value_type_requirements(
2189
+ node,
2190
+ YamlInterpolationContext::MappingKey,
2191
+ requirements,
2192
+ );
2193
+ }
2194
+ }
2195
+ }
2196
+
2197
+ fn collect_yaml_scalar_type_requirements(
2198
+ scalar: &YamlScalarNode,
2199
+ description: &'static str,
2200
+ requirements: &mut Vec<InterpolationTypeRequirement>,
2201
+ ) {
2202
+ match scalar {
2203
+ YamlScalarNode::Plain(node) => {
2204
+ collect_yaml_chunk_type_requirements(&node.chunks, description, requirements);
2205
+ }
2206
+ YamlScalarNode::DoubleQuoted(node) => {
2207
+ collect_yaml_chunk_type_requirements(&node.chunks, description, requirements);
2208
+ }
2209
+ YamlScalarNode::SingleQuoted(node) => {
2210
+ collect_yaml_chunk_type_requirements(&node.chunks, description, requirements);
2211
+ }
2212
+ YamlScalarNode::Block(node) => {
2213
+ collect_yaml_chunk_type_requirements(&node.chunks, description, requirements);
2214
+ }
2215
+ YamlScalarNode::Alias(node) => {
2216
+ collect_yaml_chunk_type_requirements(
2217
+ &node.chunks,
2218
+ "yaml metadata fragment",
2219
+ requirements,
2220
+ );
2221
+ }
2222
+ }
2223
+ }
2224
+
2225
+ fn collect_yaml_chunk_type_requirements(
2226
+ chunks: &[YamlChunk],
2227
+ description: &'static str,
2228
+ requirements: &mut Vec<InterpolationTypeRequirement>,
2229
+ ) {
2230
+ for chunk in chunks {
2231
+ if let YamlChunk::Interpolation(node) = chunk {
2232
+ requirements.push(InterpolationTypeRequirement::new(
2233
+ node.interpolation_index,
2234
+ STRING_PYTHON_TYPE,
2235
+ description,
2236
+ ));
2237
+ }
2238
+ }
2239
+ }
2240
+
2241
+ fn yaml_value_type_requirement(
2242
+ node: &YamlInterpolationNode,
2243
+ context: YamlInterpolationContext,
2244
+ ) -> InterpolationTypeRequirement {
2245
+ let description = match context {
2246
+ YamlInterpolationContext::Value => "yaml value",
2247
+ YamlInterpolationContext::MappingKey => "yaml mapping key",
2248
+ };
2249
+ InterpolationTypeRequirement::new(
2250
+ node.interpolation_index,
2251
+ YAML_VALUE_PYTHON_TYPE,
2252
+ description,
2253
+ )
2254
+ }
2255
+
2087
2256
  pub fn format_template_with_profile(
2088
2257
  template: &TemplateInput,
2089
2258
  profile: YamlProfile,
@@ -1,6 +1,9 @@
1
- use tstring_syntax::{TemplateInput, TemplateInterpolation, TemplateSegment};
1
+ use tstring_syntax::{
2
+ InterpolationTypeRequirement, TemplateInput, TemplateInterpolation, TemplateSegment,
3
+ };
2
4
  use tstring_yaml::{
3
- YamlValueNode, check_template, format_template, parse_template, validate_template,
5
+ YamlValueNode, check_template, format_template, interpolation_type_requirements,
6
+ parse_template, validate_template,
4
7
  };
5
8
 
6
9
  fn interpolation(index: usize, expression: &str) -> TemplateSegment {
@@ -129,6 +132,63 @@ fn validates_plain_scalars_with_multiple_interpolations_and_no_whitespace() {
129
132
  validate_template(&template).expect("expected YAML validation success");
130
133
  }
131
134
 
135
+ #[test]
136
+ fn reports_contextual_interpolation_type_requirements() {
137
+ let template = TemplateInput::from_segments(vec![
138
+ interpolation(0, "key"),
139
+ TemplateSegment::StaticText(": ".to_owned()),
140
+ interpolation(1, "value"),
141
+ TemplateSegment::StaticText("\nmessage: \"Hello ".to_owned()),
142
+ interpolation(2, "fragment"),
143
+ TemplateSegment::StaticText("\"\ntagged: !<tag:".to_owned()),
144
+ interpolation(3, "tag"),
145
+ TemplateSegment::StaticText("> ".to_owned()),
146
+ interpolation(4, "tagged_value"),
147
+ TemplateSegment::StaticText("\n".to_owned()),
148
+ ]);
149
+
150
+ assert_eq!(
151
+ interpolation_type_requirements(&template).expect("expected type requirements"),
152
+ vec![
153
+ InterpolationTypeRequirement::new(
154
+ 0,
155
+ "str | int | float | bool | None | datetime.date | datetime.time | datetime.datetime | list[object] | dict[object, object]",
156
+ "yaml mapping key"
157
+ ),
158
+ InterpolationTypeRequirement::new(
159
+ 1,
160
+ "str | int | float | bool | None | datetime.date | datetime.time | datetime.datetime | list[object] | dict[object, object]",
161
+ "yaml value"
162
+ ),
163
+ InterpolationTypeRequirement::new(2, "str", "yaml scalar fragment"),
164
+ InterpolationTypeRequirement::new(3, "str", "yaml metadata fragment"),
165
+ InterpolationTypeRequirement::new(
166
+ 4,
167
+ "str | int | float | bool | None | datetime.date | datetime.time | datetime.datetime | list[object] | dict[object, object]",
168
+ "yaml value"
169
+ ),
170
+ ]
171
+ );
172
+ }
173
+
174
+ #[test]
175
+ fn reports_complex_key_interpolation_requirements_in_key_context() {
176
+ let template = TemplateInput::from_segments(vec![
177
+ TemplateSegment::StaticText("? [".to_owned()),
178
+ interpolation(0, "item"),
179
+ TemplateSegment::StaticText("]\n: ok\n".to_owned()),
180
+ ]);
181
+
182
+ assert_eq!(
183
+ interpolation_type_requirements(&template).expect("expected type requirements"),
184
+ vec![InterpolationTypeRequirement::new(
185
+ 0,
186
+ "str | int | float | bool | None | datetime.date | datetime.time | datetime.datetime | list[object] | dict[object, object]",
187
+ "yaml mapping key"
188
+ )]
189
+ );
190
+ }
191
+
132
192
  #[test]
133
193
  fn formats_yaml_templates_with_raw_interpolations() {
134
194
  let template = TemplateInput::from_segments(vec![