1use 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#[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
48pub 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
57pub 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 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 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 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 builder = builder.constraint(&relationship_id, "required", &target_id);
137 }
138 }
139 }
140 }
141
142 let schema = builder.build()?;
143 Ok(schema)
144}
145
146pub 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
218fn 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}