Skip to main content

panproto_protocols/api/
jsonapi.rs

1//! JSON:API protocol definition.
2//!
3//! JSON:API uses a constrained multigraph schema theory
4//! (`colimit(ThGraph, ThConstraint, ThMulti)`) and a W-type
5//! instance theory (`ThWType`).
6//!
7//! Vertex kinds: resource-type, attribute, relationship,
8//! string, integer, number, boolean, array, object.
9//!
10//! Edge kinds: prop, ref.
11
12use std::collections::HashMap;
13
14use panproto_gat::Theory;
15use panproto_schema::{EdgeRule, Protocol, Schema, SchemaBuilder};
16
17use crate::emit::{children_by_edge, constraint_value, find_roots};
18use crate::error::ProtocolError;
19use crate::theories;
20
21/// Returns the JSON:API protocol definition.
22#[must_use]
23pub fn protocol() -> Protocol {
24    Protocol {
25        name: "jsonapi".into(),
26        schema_theory: "ThJsonAPISchema".into(),
27        instance_theory: "ThJsonAPIInstance".into(),
28        edge_rules: edge_rules(),
29        obj_kinds: vec![
30            "resource-type".into(),
31            "attribute".into(),
32            "relationship".into(),
33            "string".into(),
34            "integer".into(),
35            "number".into(),
36            "boolean".into(),
37            "array".into(),
38            "object".into(),
39        ],
40        constraint_sorts: vec!["required".into()],
41        has_order: true,
42        has_recursion: true,
43        nominal_identity: true,
44        ..Protocol::default()
45    }
46}
47
48/// Register the component GATs for JSON:API with a theory registry.
49pub fn register_theories<S: ::std::hash::BuildHasher>(registry: &mut HashMap<String, Theory, S>) {
50    theories::register_constrained_multigraph_wtype(
51        registry,
52        "ThJsonAPISchema",
53        "ThJsonAPIInstance",
54    );
55}
56
57/// Parse a JSON:API schema document into a [`Schema`].
58///
59/// Expects a JSON object describing resource types with their
60/// attributes and relationships.
61///
62/// # Errors
63///
64/// Returns [`ProtocolError`] if parsing or schema construction fails.
65pub fn parse_jsonapi(json: &serde_json::Value) -> Result<Schema, ProtocolError> {
66    let proto = protocol();
67    let mut builder = SchemaBuilder::new(&proto);
68
69    let resources = json
70        .get("resources")
71        .and_then(serde_json::Value::as_object)
72        .ok_or_else(|| ProtocolError::MissingField("resources".into()))?;
73
74    for (res_name, res_def) in resources {
75        let resource_id = format!("resource:{res_name}");
76        builder = builder.vertex(&resource_id, "resource-type", None)?;
77
78        // Attributes.
79        if let Some(attrs) = res_def
80            .get("attributes")
81            .and_then(serde_json::Value::as_object)
82        {
83            let required_fields: Vec<&str> = res_def
84                .get("required")
85                .and_then(serde_json::Value::as_array)
86                .map(|arr| arr.iter().filter_map(serde_json::Value::as_str).collect())
87                .unwrap_or_default();
88
89            for (attr_name, attr_def) in attrs {
90                let attr_id = format!("{resource_id}.{attr_name}");
91                let attr_type = attr_def
92                    .get("type")
93                    .and_then(serde_json::Value::as_str)
94                    .unwrap_or("string");
95
96                let kind = match attr_type {
97                    "integer" => "integer",
98                    "number" => "number",
99                    "boolean" => "boolean",
100                    "array" => "array",
101                    "object" => "object",
102                    _ => "string",
103                };
104
105                builder = builder.vertex(&attr_id, kind, None)?;
106                builder = builder.edge(&resource_id, &attr_id, "prop", Some(attr_name))?;
107
108                if required_fields.contains(&attr_name.as_str()) {
109                    builder = builder.constraint(&attr_id, "required", "true");
110                }
111            }
112        }
113
114        // Relationships.
115        if let Some(relationships) = res_def
116            .get("relationships")
117            .and_then(serde_json::Value::as_object)
118        {
119            for (relationship_name, relationship_def) in relationships {
120                let relationship_id = format!("{resource_id}:rel:{relationship_name}");
121                builder = builder.vertex(&relationship_id, "relationship", None)?;
122                builder = builder.edge(
123                    &resource_id,
124                    &relationship_id,
125                    "prop",
126                    Some(relationship_name),
127                )?;
128
129                // Target resource type.
130                if let Some(target) = relationship_def
131                    .get("target")
132                    .and_then(serde_json::Value::as_str)
133                {
134                    let target_id = format!("resource:{target}");
135                    // Store target as a constraint since the target vertex may not exist yet.
136                    builder = builder.constraint(&relationship_id, "required", &target_id);
137                }
138            }
139        }
140    }
141
142    let schema = builder.build()?;
143    Ok(schema)
144}
145
146/// Emit a [`Schema`] as a JSON:API schema document.
147///
148/// # Errors
149///
150/// Returns [`ProtocolError`] if emission fails.
151pub fn emit_jsonapi(schema: &Schema) -> Result<serde_json::Value, ProtocolError> {
152    let mut resources = serde_json::Map::new();
153
154    let roots = find_roots(schema, &["prop", "ref"]);
155
156    for root in &roots {
157        if root.kind != "resource-type" {
158            continue;
159        }
160        let resource_name = root.id.strip_prefix("resource:").unwrap_or(&root.id);
161        let mut resource_obj = serde_json::Map::new();
162        let mut attrs = serde_json::Map::new();
163        let mut relationships = serde_json::Map::new();
164        let mut required_list = Vec::new();
165
166        for (edge, child) in children_by_edge(schema, &root.id, "prop") {
167            let name = edge.name.as_deref().unwrap_or("");
168            if child.kind == "relationship" {
169                let mut relationship_obj = serde_json::Map::new();
170                if let Some(target_val) = constraint_value(schema, &child.id, "required") {
171                    let target_name = target_val.strip_prefix("resource:").unwrap_or(target_val);
172                    relationship_obj.insert(
173                        "target".into(),
174                        serde_json::Value::String(target_name.to_string()),
175                    );
176                }
177                relationships.insert(
178                    name.to_string(),
179                    serde_json::Value::Object(relationship_obj),
180                );
181            } else {
182                let type_name = match child.kind.as_str() {
183                    "integer" | "number" | "boolean" | "array" | "object" => child.kind.as_str(),
184                    _ => "string",
185                };
186                attrs.insert(name.to_string(), serde_json::json!({"type": type_name}));
187                if constraint_value(schema, &child.id, "required") == Some("true") {
188                    required_list.push(serde_json::Value::String(name.to_string()));
189                }
190            }
191        }
192
193        if !attrs.is_empty() {
194            resource_obj.insert("attributes".into(), serde_json::Value::Object(attrs));
195        }
196        if !relationships.is_empty() {
197            resource_obj.insert(
198                "relationships".into(),
199                serde_json::Value::Object(relationships),
200            );
201        }
202        if !required_list.is_empty() {
203            resource_obj.insert("required".into(), serde_json::Value::Array(required_list));
204        }
205
206        resources.insert(
207            resource_name.to_string(),
208            serde_json::Value::Object(resource_obj),
209        );
210    }
211
212    let mut result = serde_json::Map::new();
213    result.insert("resources".into(), serde_json::Value::Object(resources));
214
215    Ok(serde_json::Value::Object(result))
216}
217
218/// Well-formedness rules for JSON:API edges.
219fn edge_rules() -> Vec<EdgeRule> {
220    vec![
221        EdgeRule {
222            edge_kind: "prop".into(),
223            src_kinds: vec!["resource-type".into()],
224            tgt_kinds: vec![],
225        },
226        EdgeRule {
227            edge_kind: "ref".into(),
228            src_kinds: vec!["relationship".into()],
229            tgt_kinds: vec!["resource-type".into()],
230        },
231    ]
232}
233
234#[cfg(test)]
235#[allow(clippy::expect_used, clippy::unwrap_used)]
236mod tests {
237    use super::*;
238
239    #[test]
240    fn protocol_def() {
241        let p = protocol();
242        assert_eq!(p.name, "jsonapi");
243        assert_eq!(p.schema_theory, "ThJsonAPISchema");
244        assert_eq!(p.instance_theory, "ThJsonAPIInstance");
245    }
246
247    #[test]
248    fn register_theories_works() {
249        let mut registry = HashMap::new();
250        register_theories(&mut registry);
251        assert!(registry.contains_key("ThJsonAPISchema"));
252        assert!(registry.contains_key("ThJsonAPIInstance"));
253    }
254
255    #[test]
256    fn parse_minimal() {
257        let doc = serde_json::json!({
258            "resources": {
259                "articles": {
260                    "attributes": {
261                        "title": {"type": "string"},
262                        "body": {"type": "string"}
263                    },
264                    "relationships": {
265                        "author": {"target": "people"}
266                    },
267                    "required": ["title"]
268                },
269                "people": {
270                    "attributes": {
271                        "name": {"type": "string"}
272                    }
273                }
274            }
275        });
276        let schema = parse_jsonapi(&doc).expect("should parse");
277        assert!(schema.has_vertex("resource:articles"));
278        assert!(schema.has_vertex("resource:people"));
279        assert!(schema.has_vertex("resource:articles.title"));
280    }
281
282    #[test]
283    fn emit_minimal() {
284        let doc = serde_json::json!({
285            "resources": {
286                "posts": {
287                    "attributes": {
288                        "title": {"type": "string"}
289                    }
290                }
291            }
292        });
293        let schema = parse_jsonapi(&doc).expect("should parse");
294        let emitted = emit_jsonapi(&schema).expect("should emit");
295        assert!(emitted.get("resources").is_some());
296    }
297
298    #[test]
299    fn roundtrip() {
300        let doc = serde_json::json!({
301            "resources": {
302                "users": {
303                    "attributes": {
304                        "name": {"type": "string"},
305                        "age": {"type": "integer"}
306                    }
307                }
308            }
309        });
310        let schema = parse_jsonapi(&doc).expect("parse");
311        let emitted = emit_jsonapi(&schema).expect("emit");
312        let schema2 = parse_jsonapi(&emitted).expect("re-parse");
313        assert_eq!(schema.vertices.len(), schema2.vertices.len());
314    }
315}