Skip to main content

polydat_grammar/comprehension/spec/
text.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! [`parse_text`] — entry point for text-block consumers.
5//!
6//! Accepts a single string holding either YAML or JSON, picks
7//! the right parser, deserializes into a
8//! [`ComprehensionSpec`], and routes through
9//! [`ComprehensionSpec::into_algebra`].
10//!
11//! Detection rule: a leading `{` (after trimming whitespace)
12//! is JSON; anything else is YAML.  YAML is a strict superset
13//! of JSON for the shapes we accept, so YAML can read JSON
14//! input fine — but choosing the matching parser surfaces
15//! sharper error messages for JSON authors.
16
17use super::serde_form::{ComprehensionSpec, SpecConvertError};
18use crate::comprehension::ast::Comprehension as AlgebraAst;
19
20/// Parse a YAML or JSON text block describing a comprehension
21/// into the algebra-layer AST.
22///
23/// Detection is leading-character based: `{...}` ⇒ JSON,
24/// otherwise YAML.
25pub fn parse_text(text: &str) -> Result<AlgebraAst, TextParseError> {
26    let spec = deserialize_spec(text)?;
27    let algebra = spec.into_algebra().map_err(TextParseError::Convert)?;
28    Ok(algebra)
29}
30
31fn deserialize_spec(text: &str) -> Result<ComprehensionSpec, TextParseError> {
32    let trimmed = text.trim_start();
33    if trimmed.starts_with('{') {
34        serde_json::from_str(text).map_err(|e| TextParseError::Json(e.to_string()))
35    } else {
36        serde_yaml::from_str(text).map_err(|e| TextParseError::Yaml(e.to_string()))
37    }
38}
39
40/// Errors produced by [`parse_text`].
41#[derive(Debug, Clone)]
42pub enum TextParseError {
43    /// YAML deserialization failed.
44    Yaml(String),
45    /// JSON deserialization failed.
46    Json(String),
47    /// The deserialized [`ComprehensionSpec`] failed to
48    /// convert into the algebra AST.
49    Convert(SpecConvertError),
50}
51
52impl std::fmt::Display for TextParseError {
53    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54        match self {
55            TextParseError::Yaml(msg) => write!(f, "YAML parse error: {msg}"),
56            TextParseError::Json(msg) => write!(f, "JSON parse error: {msg}"),
57            TextParseError::Convert(e) => write!(f, "spec conversion error: {e}"),
58        }
59    }
60}
61
62impl std::error::Error for TextParseError {}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67    use crate::comprehension::strategy::StrategyName;
68
69    #[test]
70    fn yaml_text_block() {
71        let text = r#"
72            for: "k in 1..10, limit in [10, 100]"
73            where: "{k} > 0"
74            order: "halton/20"
75        "#;
76        let algebra = parse_text(text).unwrap();
77        match algebra {
78            AlgebraAst::Order {
79                strategy: StrategyName::Halton,
80                truncation: Some(20),
81                ..
82            } => {}
83            other => panic!("expected Order(Halton, Some(20)), got {other:?}"),
84        }
85    }
86
87    #[test]
88    fn json_text_block() {
89        let text = r#"
90            {
91                "for": "k in 1..10",
92                "order": "lex/5"
93            }
94        "#;
95        let algebra = parse_text(text).unwrap();
96        match algebra {
97            AlgebraAst::Order {
98                strategy: StrategyName::Lex,
99                truncation: Some(5),
100                ..
101            } => {}
102            other => panic!("expected Order(Lex, Some(5)), got {other:?}"),
103        }
104    }
105
106    #[test]
107    fn json_union_form() {
108        let text = r#"{
109            "for": [
110                ["k in 10",  "limit in [1, 2, 3]"],
111                ["k in 100", "limit in [10, 20, 30]"]
112            ]
113        }"#;
114        let algebra = parse_text(text).unwrap();
115        match algebra {
116            AlgebraAst::Union { children } => assert_eq!(children.len(), 2),
117            other => panic!("expected Union, got {other:?}"),
118        }
119    }
120
121    #[test]
122    fn malformed_yaml_surfaces_error() {
123        let text = r#"
124            for: [unterminated
125        "#;
126        let err = parse_text(text).unwrap_err();
127        assert!(matches!(err, TextParseError::Yaml(_)));
128    }
129
130    #[test]
131    fn malformed_json_surfaces_error() {
132        let text = r#"{ "for": "k in 1..10", oops }"#;
133        let err = parse_text(text).unwrap_err();
134        assert!(matches!(err, TextParseError::Json(_)));
135    }
136
137    #[test]
138    fn convert_error_surfaces() {
139        let text = r#"
140            for: "k in 1..10"
141            order: "garbage(((not valid"
142        "#;
143        let err = parse_text(text).unwrap_err();
144        assert!(matches!(err, TextParseError::Convert(_)));
145    }
146}