Skip to main content

panproto_protocols/api/
raml.rs

1//! RAML protocol definition.
2//!
3//! Uses Group A theory: constrained multigraph + W-type.
4
5use std::collections::HashMap;
6use std::hash::BuildHasher;
7
8use panproto_gat::Theory;
9use panproto_schema::{EdgeRule, Protocol, Schema, SchemaBuilder};
10
11use crate::emit::{children_by_edge, find_roots, vertex_constraints};
12use crate::error::ProtocolError;
13use crate::theories;
14
15/// Returns the RAML protocol definition.
16#[must_use]
17pub fn protocol() -> Protocol {
18    Protocol {
19        name: "raml".into(),
20        schema_theory: "ThRamlSchema".into(),
21        instance_theory: "ThRamlInstance".into(),
22        edge_rules: edge_rules(),
23        obj_kinds: vec![
24            "resource".into(),
25            "method".into(),
26            "type".into(),
27            "trait".into(),
28            "string".into(),
29            "integer".into(),
30            "number".into(),
31            "boolean".into(),
32            "array".into(),
33            "object".into(),
34            "date".into(),
35            "file".into(),
36            "nil".into(),
37        ],
38        constraint_sorts: vec![
39            "required".into(),
40            "pattern".into(),
41            "minLength".into(),
42            "maxLength".into(),
43            "minimum".into(),
44            "maximum".into(),
45            "enum".into(),
46            "default".into(),
47        ],
48        has_order: true,
49        has_coproducts: true,
50        has_recursion: true,
51        nominal_identity: true,
52        ..Protocol::default()
53    }
54}
55
56/// Register the component GATs for RAML.
57pub fn register_theories<S: BuildHasher>(registry: &mut HashMap<String, Theory, S>) {
58    theories::register_constrained_multigraph_wtype(registry, "ThRamlSchema", "ThRamlInstance");
59}
60
61/// Parse a JSON-based RAML representation into a [`Schema`].
62///
63/// # Errors
64///
65/// Returns [`ProtocolError`] if parsing fails.
66pub fn parse_raml_schema(json: &serde_json::Value) -> Result<Schema, ProtocolError> {
67    let proto = protocol();
68    let mut builder = SchemaBuilder::new(&proto);
69    let mut counter: usize = 0;
70
71    // Parse types.
72    if let Some(types) = json.get("types").and_then(serde_json::Value::as_object) {
73        for (name, def) in types {
74            builder = walk_raml_type(builder, def, name, &mut counter)?;
75        }
76    }
77
78    // Parse resources.
79    if let Some(resources) = json.get("resources").and_then(serde_json::Value::as_object) {
80        for (path, res_def) in resources {
81            let res_id = format!("resource:{path}");
82            builder = builder.vertex(&res_id, "resource", None)?;
83
84            if let Some(methods) = res_def
85                .get("methods")
86                .and_then(serde_json::Value::as_object)
87            {
88                for (method_name, method_def) in methods {
89                    let method_id = format!("{res_id}:{method_name}");
90                    builder = builder.vertex(&method_id, "method", None)?;
91                    builder = builder.edge(&res_id, &method_id, "prop", Some(method_name))?;
92
93                    if let Some(body) = method_def.get("body") {
94                        counter += 1;
95                        let body_id = format!("{method_id}:body{counter}");
96                        builder = walk_raml_type(builder, body, &body_id, &mut counter)?;
97                        builder = builder.edge(&method_id, &body_id, "prop", Some("body"))?;
98                    }
99                }
100            }
101        }
102    }
103
104    let schema = builder.build()?;
105    Ok(schema)
106}
107
108/// Walk a RAML type definition.
109fn walk_raml_type(
110    mut builder: SchemaBuilder,
111    def: &serde_json::Value,
112    current_id: &str,
113    counter: &mut usize,
114) -> Result<SchemaBuilder, ProtocolError> {
115    let type_str = def
116        .get("type")
117        .and_then(serde_json::Value::as_str)
118        .unwrap_or("object");
119
120    let kind = raml_type_to_kind(type_str);
121    builder = builder.vertex(current_id, kind, None)?;
122
123    // Constraints.
124    for field in &[
125        "pattern",
126        "minLength",
127        "maxLength",
128        "minimum",
129        "maximum",
130        "default",
131    ] {
132        if let Some(val) = def.get(field) {
133            let val_str = match val {
134                serde_json::Value::String(s) => s.clone(),
135                serde_json::Value::Number(n) => n.to_string(),
136                _ => val.to_string(),
137            };
138            builder = builder.constraint(current_id, field, &val_str);
139        }
140    }
141
142    if let Some(enum_val) = def.get("enum").and_then(serde_json::Value::as_array) {
143        let vals: Vec<String> = enum_val
144            .iter()
145            .map(|v| v.as_str().map_or_else(|| v.to_string(), String::from))
146            .collect();
147        builder = builder.constraint(current_id, "enum", &vals.join(","));
148    }
149
150    // Properties.
151    if let Some(properties) = def.get("properties").and_then(serde_json::Value::as_object) {
152        for (prop_name, prop_def) in properties {
153            let prop_id = format!("{current_id}.{prop_name}");
154            builder = walk_raml_type(builder, prop_def, &prop_id, counter)?;
155            builder = builder.edge(current_id, &prop_id, "prop", Some(prop_name))?;
156        }
157    }
158
159    // Items.
160    if let Some(items) = def.get("items") {
161        let items_id = format!("{current_id}:items");
162        builder = walk_raml_type(builder, items, &items_id, counter)?;
163        builder = builder.edge(current_id, &items_id, "items", None)?;
164    }
165
166    Ok(builder)
167}
168
169/// Map RAML type to vertex kind.
170fn raml_type_to_kind(t: &str) -> &'static str {
171    match t {
172        "string" => "string",
173        "integer" => "integer",
174        "number" => "number",
175        "boolean" => "boolean",
176        "array" => "array",
177        "object" => "object",
178        "date-only" | "time-only" | "datetime-only" | "datetime" => "date",
179        "file" => "file",
180        "nil" => "nil",
181        _ => "type",
182    }
183}
184
185/// Emit a [`Schema`] as a JSON RAML representation.
186///
187/// # Errors
188///
189/// Returns [`ProtocolError`] if emission fails.
190pub fn emit_raml_schema(schema: &Schema) -> Result<serde_json::Value, ProtocolError> {
191    let structural = &["prop", "items"];
192    let roots = find_roots(schema, structural);
193
194    let mut types = serde_json::Map::new();
195    let mut resources = serde_json::Map::new();
196
197    for root in &roots {
198        if root.kind.as_str() == "resource" {
199            let path = root.id.strip_prefix("resource:").unwrap_or(&root.id);
200            let methods = children_by_edge(schema, &root.id, "prop");
201            let mut methods_obj = serde_json::Map::new();
202            for (edge, child) in &methods {
203                let name = edge.name.as_deref().unwrap_or(&child.id);
204                methods_obj.insert(name.to_string(), serde_json::json!({}));
205            }
206            resources.insert(
207                path.to_string(),
208                serde_json::json!({"methods": methods_obj}),
209            );
210        } else {
211            let obj = emit_raml_type(schema, &root.id);
212            types.insert(root.id.to_string(), obj);
213        }
214    }
215
216    let mut result = serde_json::Map::new();
217    if !types.is_empty() {
218        result.insert("types".into(), serde_json::Value::Object(types));
219    }
220    if !resources.is_empty() {
221        result.insert("resources".into(), serde_json::Value::Object(resources));
222    }
223
224    Ok(serde_json::Value::Object(result))
225}
226
227/// Emit a single RAML type definition.
228fn emit_raml_type(schema: &Schema, vertex_id: &str) -> serde_json::Value {
229    let vertex = match schema.vertices.get(vertex_id) {
230        Some(v) => v,
231        None => return serde_json::json!({}),
232    };
233
234    let mut obj = serde_json::Map::new();
235    obj.insert("type".into(), serde_json::json!(vertex.kind));
236
237    for c in vertex_constraints(schema, vertex_id) {
238        if let Ok(n) = c.value.parse::<i64>() {
239            obj.insert(c.sort.to_string(), serde_json::json!(n));
240        } else {
241            obj.insert(c.sort.to_string(), serde_json::json!(c.value));
242        }
243    }
244
245    let props = children_by_edge(schema, vertex_id, "prop");
246    if !props.is_empty() {
247        let mut properties = serde_json::Map::new();
248        for (edge, child) in &props {
249            let name = edge.name.as_deref().unwrap_or(&child.id);
250            let val = emit_raml_type(schema, &child.id);
251            properties.insert(name.to_string(), val);
252        }
253        obj.insert("properties".into(), serde_json::Value::Object(properties));
254    }
255
256    serde_json::Value::Object(obj)
257}
258
259fn edge_rules() -> Vec<EdgeRule> {
260    vec![
261        EdgeRule {
262            edge_kind: "prop".into(),
263            src_kinds: vec![
264                "resource".into(),
265                "method".into(),
266                "object".into(),
267                "type".into(),
268            ],
269            tgt_kinds: vec![],
270        },
271        EdgeRule {
272            edge_kind: "items".into(),
273            src_kinds: vec!["array".into()],
274            tgt_kinds: vec![],
275        },
276    ]
277}
278
279#[cfg(test)]
280#[allow(clippy::expect_used, clippy::unwrap_used)]
281mod tests {
282    use super::*;
283
284    #[test]
285    fn protocol_def() {
286        let p = protocol();
287        assert_eq!(p.name, "raml");
288    }
289
290    #[test]
291    fn register_theories_works() {
292        let mut registry = HashMap::new();
293        register_theories(&mut registry);
294        assert!(registry.contains_key("ThRamlSchema"));
295    }
296
297    #[test]
298    fn parse_and_emit() {
299        let json = serde_json::json!({
300            "types": {
301                "User": {
302                    "type": "object",
303                    "properties": {
304                        "name": {"type": "string"},
305                        "age": {"type": "integer"}
306                    }
307                }
308            },
309            "resources": {
310                "/users": {
311                    "methods": {
312                        "get": {}
313                    }
314                }
315            }
316        });
317        let schema = parse_raml_schema(&json).expect("should parse");
318        assert!(schema.has_vertex("User"));
319        assert!(schema.has_vertex("resource:/users"));
320        let emitted = emit_raml_schema(&schema).expect("emit");
321        assert!(emitted.get("types").is_some());
322    }
323
324    #[test]
325    fn roundtrip() {
326        let json = serde_json::json!({
327            "types": {
328                "Item": {
329                    "type": "object",
330                    "properties": {
331                        "id": {"type": "integer"}
332                    }
333                }
334            }
335        });
336        let s1 = parse_raml_schema(&json).expect("parse");
337        let emitted = emit_raml_schema(&s1).expect("emit");
338        let s2 = parse_raml_schema(&emitted).expect("re-parse");
339        assert_eq!(s1.vertex_count(), s2.vertex_count());
340    }
341}