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: "openapi".into(),
26 schema_theory: "ThOpenAPISchema".into(),
27 instance_theory: "ThOpenAPIInstance".into(),
28 edge_rules: edge_rules(),
29 obj_kinds: vec![
30 "path".into(),
31 "operation".into(),
32 "parameter".into(),
33 "request-body".into(),
34 "response".into(),
35 "schema-object".into(),
36 "header".into(),
37 "string".into(),
38 "integer".into(),
39 "number".into(),
40 "boolean".into(),
41 "array".into(),
42 "object".into(),
43 ],
44 constraint_sorts: vec![
45 "required".into(),
46 "format".into(),
47 "enum".into(),
48 "default".into(),
49 "minimum".into(),
50 "maximum".into(),
51 "pattern".into(),
52 "minLength".into(),
53 "maxLength".into(),
54 "minItems".into(),
55 "maxItems".into(),
56 "deprecated".into(),
57 ],
58 has_order: true,
59 has_coproducts: true,
60 has_recursion: true,
61 nominal_identity: true,
62 ..Protocol::default()
63 }
64}
65
66pub fn register_theories<S: ::std::hash::BuildHasher>(registry: &mut HashMap<String, Theory, S>) {
68 theories::register_constrained_multigraph_wtype(
69 registry,
70 "ThOpenAPISchema",
71 "ThOpenAPIInstance",
72 );
73}
74
75pub fn parse_openapi(json: &serde_json::Value) -> Result<Schema, ProtocolError> {
84 let proto = protocol();
85 let mut builder = SchemaBuilder::new(&proto);
86 let mut counter: usize = 0;
87
88 let mut defs_map: HashMap<String, String> = HashMap::new();
90 if let Some(schemas) = json
91 .pointer("/components/schemas")
92 .and_then(serde_json::Value::as_object)
93 {
94 for (name, schema_val) in schemas {
95 let schema_id = format!("components/schemas/{name}");
96 builder = walk_schema(builder, schema_val, &schema_id, &mut counter)?;
97 let ref_path = format!("#/components/schemas/{name}");
98 defs_map.insert(ref_path, schema_id);
99 }
100 }
101
102 if let Some(paths) = json.get("paths").and_then(serde_json::Value::as_object) {
105 for (path_str, path_item) in paths {
106 let path_id = format!("path:{path_str}");
107 builder = builder.vertex(&path_id, "path", None)?;
108 builder = builder.entry(&path_id);
109 builder = parse_path_item(builder, path_item, &path_id, &mut counter, &defs_map)?;
110 }
111 }
112
113 let schema = builder.build()?;
114 Ok(schema)
115}
116
117fn parse_path_item(
119 mut builder: SchemaBuilder,
120 path_item: &serde_json::Value,
121 path_id: &str,
122 counter: &mut usize,
123 defs_map: &HashMap<String, String>,
124) -> Result<SchemaBuilder, ProtocolError> {
125 for method in &[
126 "get", "post", "put", "delete", "patch", "options", "head", "trace",
127 ] {
128 if let Some(op) = path_item.get(*method) {
129 let op_id = format!("{path_id}:{method}");
130 builder = builder.vertex(&op_id, "operation", None)?;
131 builder = builder.edge(path_id, &op_id, "prop", Some(method))?;
132
133 if op.get("deprecated").and_then(serde_json::Value::as_bool) == Some(true) {
134 builder = builder.constraint(&op_id, "deprecated", "true");
135 }
136
137 builder = parse_operation(builder, op, &op_id, counter, defs_map)?;
138 }
139 }
140 Ok(builder)
141}
142
143fn parse_operation(
145 mut builder: SchemaBuilder,
146 op: &serde_json::Value,
147 op_id: &str,
148 counter: &mut usize,
149 defs_map: &HashMap<String, String>,
150) -> Result<SchemaBuilder, ProtocolError> {
151 if let Some(params) = op.get("parameters").and_then(serde_json::Value::as_array) {
153 for (i, param) in params.iter().enumerate() {
154 let param_name = param
155 .get("name")
156 .and_then(serde_json::Value::as_str)
157 .unwrap_or("unknown");
158 let param_id = format!("{op_id}:param{i}");
159 builder = builder.vertex(¶m_id, "parameter", None)?;
160 builder = builder.edge(op_id, ¶m_id, "prop", Some(param_name))?;
161
162 if param.get("required").and_then(serde_json::Value::as_bool) == Some(true) {
163 builder = builder.constraint(¶m_id, "required", "true");
164 }
165
166 if let Some(schema_val) = param.get("schema") {
167 let s_id = format!("{param_id}:schema");
168 builder = walk_schema_or_ref(builder, schema_val, &s_id, counter, defs_map)?;
169 builder = builder.edge(¶m_id, &s_id, "prop", Some("schema"))?;
170 }
171 }
172 }
173
174 if let Some(req_body) = op.get("requestBody") {
176 let rb_id = format!("{op_id}:requestBody");
177 builder = builder.vertex(&rb_id, "request-body", None)?;
178 builder = builder.edge(op_id, &rb_id, "prop", Some("requestBody"))?;
179
180 if let Some(content) = req_body
181 .get("content")
182 .and_then(serde_json::Value::as_object)
183 {
184 for (media_type, media_obj) in content {
185 if let Some(schema_val) = media_obj.get("schema") {
186 let s_id = format!("{rb_id}:{media_type}");
187 builder = walk_schema_or_ref(builder, schema_val, &s_id, counter, defs_map)?;
188 builder = builder.edge(&rb_id, &s_id, "prop", Some(media_type))?;
189 }
190 }
191 }
192 }
193
194 if let Some(responses) = op.get("responses").and_then(serde_json::Value::as_object) {
196 for (status, resp) in responses {
197 let resp_id = format!("{op_id}:resp{status}");
198 builder = builder.vertex(&resp_id, "response", None)?;
199 builder = builder.edge(op_id, &resp_id, "prop", Some(status))?;
200
201 if let Some(content) = resp.get("content").and_then(serde_json::Value::as_object) {
202 for (media_type, media_obj) in content {
203 if let Some(schema_val) = media_obj.get("schema") {
204 let s_id = format!("{resp_id}:{media_type}");
205 builder =
206 walk_schema_or_ref(builder, schema_val, &s_id, counter, defs_map)?;
207 builder = builder.edge(&resp_id, &s_id, "prop", Some(media_type))?;
208 }
209 }
210 }
211
212 if let Some(headers) = resp.get("headers").and_then(serde_json::Value::as_object) {
213 for (hdr_name, _hdr_obj) in headers {
214 let hdr_id = format!("{resp_id}:hdr:{hdr_name}");
215 builder = builder.vertex(&hdr_id, "header", None)?;
216 builder = builder.edge(&resp_id, &hdr_id, "prop", Some(hdr_name))?;
217 }
218 }
219 }
220 }
221
222 Ok(builder)
223}
224
225fn walk_schema_or_ref(
227 builder: SchemaBuilder,
228 schema: &serde_json::Value,
229 current_id: &str,
230 counter: &mut usize,
231 defs_map: &HashMap<String, String>,
232) -> Result<SchemaBuilder, ProtocolError> {
233 if let Some(ref_str) = schema.get("$ref").and_then(serde_json::Value::as_str) {
234 let mut b = builder.vertex(current_id, "schema-object", None)?;
235 if let Some(def_id) = defs_map.get(ref_str) {
236 b = b.edge(current_id, def_id, "ref", Some(ref_str))?;
237 }
238 Ok(b)
239 } else {
240 walk_schema(builder, schema, current_id, counter)
241 }
242}
243
244fn walk_schema(
246 mut builder: SchemaBuilder,
247 schema: &serde_json::Value,
248 current_id: &str,
249 counter: &mut usize,
250) -> Result<SchemaBuilder, ProtocolError> {
251 let type_str = schema
252 .get("type")
253 .and_then(serde_json::Value::as_str)
254 .unwrap_or("object");
255
256 let kind = match type_str {
257 "string" => "string",
258 "integer" => "integer",
259 "number" => "number",
260 "boolean" => "boolean",
261 "array" => "array",
262 _ => "object",
263 };
264
265 builder = builder.vertex(current_id, kind, None)?;
266
267 for field in &[
269 "format",
270 "minimum",
271 "maximum",
272 "pattern",
273 "minLength",
274 "maxLength",
275 "minItems",
276 "maxItems",
277 ] {
278 if let Some(val) = schema.get(field) {
279 let val_str = match val {
280 serde_json::Value::String(s) => s.clone(),
281 serde_json::Value::Number(n) => n.to_string(),
282 _ => val.to_string(),
283 };
284 builder = builder.constraint(current_id, field, &val_str);
285 }
286 }
287
288 if let Some(enum_val) = schema.get("enum").and_then(serde_json::Value::as_array) {
289 let vals: Vec<String> = enum_val
290 .iter()
291 .map(|v| v.as_str().map_or_else(|| v.to_string(), String::from))
292 .collect();
293 builder = builder.constraint(current_id, "enum", &vals.join(","));
294 }
295
296 if let Some(default_val) = schema.get("default") {
297 let val_str = match default_val {
298 serde_json::Value::String(s) => s.clone(),
299 _ => default_val.to_string(),
300 };
301 builder = builder.constraint(current_id, "default", &val_str);
302 }
303
304 if let Some(properties) = schema
306 .get("properties")
307 .and_then(serde_json::Value::as_object)
308 {
309 let required_fields: Vec<&str> = schema
310 .get("required")
311 .and_then(serde_json::Value::as_array)
312 .map(|arr| arr.iter().filter_map(serde_json::Value::as_str).collect())
313 .unwrap_or_default();
314
315 for (prop_name, prop_schema) in properties {
316 let prop_id = format!("{current_id}.{prop_name}");
317 builder = walk_schema(builder, prop_schema, &prop_id, counter)?;
318 builder = builder.edge(current_id, &prop_id, "prop", Some(prop_name))?;
319 if required_fields.contains(&prop_name.as_str()) {
320 builder = builder.constraint(&prop_id, "required", "true");
321 }
322 }
323 }
324
325 if let Some(items) = schema.get("items") {
327 let items_id = format!("{current_id}:items");
328 builder = walk_schema(builder, items, &items_id, counter)?;
329 builder = builder.edge(current_id, &items_id, "items", None)?;
330 }
331
332 for combiner in &["oneOf", "anyOf", "allOf"] {
334 if let Some(arr) = schema.get(*combiner).and_then(serde_json::Value::as_array) {
335 for (i, sub_schema) in arr.iter().enumerate() {
336 *counter += 1;
337 let sub_id = format!("{current_id}:{combiner}{i}_{counter}");
338 builder = walk_schema(builder, sub_schema, &sub_id, counter)?;
339 builder = builder.edge(current_id, &sub_id, "variant", Some(combiner))?;
340 }
341 }
342 }
343
344 Ok(builder)
345}
346
347pub fn emit_openapi(schema: &Schema) -> Result<serde_json::Value, ProtocolError> {
353 let mut paths = serde_json::Map::new();
354 let mut component_schemas = serde_json::Map::new();
355
356 let roots = find_roots(schema, &["prop", "items", "variant", "ref"]);
357
358 for root in &roots {
359 if root.kind == "path" {
360 let path_name = root.id.strip_prefix("path:").unwrap_or(&root.id);
361 let mut path_obj = serde_json::Map::new();
362
363 for (edge, op_vertex) in children_by_edge(schema, &root.id, "prop") {
364 if op_vertex.kind == "operation" {
365 let method = edge.name.as_deref().unwrap_or("get");
366 let op_obj = emit_operation(schema, &op_vertex.id);
367 path_obj.insert(method.to_string(), op_obj);
368 }
369 }
370
371 paths.insert(path_name.to_string(), serde_json::Value::Object(path_obj));
372 } else {
373 let schema_obj = emit_schema_value(schema, &root.id);
374 let name = root
375 .id
376 .strip_prefix("components/schemas/")
377 .unwrap_or(&root.id);
378 component_schemas.insert(name.to_string(), schema_obj);
379 }
380 }
381
382 let mut result = serde_json::Map::new();
383 result.insert("openapi".into(), serde_json::Value::String("3.0.0".into()));
384 result.insert(
385 "info".into(),
386 serde_json::json!({"title": "Generated", "version": "1.0.0"}),
387 );
388 result.insert("paths".into(), serde_json::Value::Object(paths));
389
390 if !component_schemas.is_empty() {
391 let mut components = serde_json::Map::new();
392 components.insert(
393 "schemas".into(),
394 serde_json::Value::Object(component_schemas),
395 );
396 result.insert("components".into(), serde_json::Value::Object(components));
397 }
398
399 Ok(serde_json::Value::Object(result))
400}
401
402fn emit_operation(schema: &Schema, op_id: &str) -> serde_json::Value {
404 let mut obj = serde_json::Map::new();
405
406 if constraint_value(schema, op_id, "deprecated") == Some("true") {
407 obj.insert("deprecated".into(), serde_json::Value::Bool(true));
408 }
409
410 let children = children_by_edge(schema, op_id, "prop");
411
412 let params: Vec<serde_json::Value> = children
414 .iter()
415 .filter(|(_, v)| v.kind == "parameter")
416 .map(|(edge, v)| {
417 let mut p = serde_json::Map::new();
418 p.insert(
419 "name".into(),
420 serde_json::Value::String(edge.name.as_deref().unwrap_or("unknown").to_string()),
421 );
422 p.insert("in".into(), serde_json::Value::String("query".into()));
423 if constraint_value(schema, &v.id, "required") == Some("true") {
424 p.insert("required".into(), serde_json::Value::Bool(true));
425 }
426 serde_json::Value::Object(p)
427 })
428 .collect();
429 if !params.is_empty() {
430 obj.insert("parameters".into(), serde_json::Value::Array(params));
431 }
432
433 let responses: Vec<_> = children
435 .iter()
436 .filter(|(_, v)| v.kind == "response")
437 .collect();
438 if !responses.is_empty() {
439 let mut resp_obj = serde_json::Map::new();
440 for (edge, _v) in &responses {
441 let status = edge.name.as_deref().unwrap_or("200");
442 let mut r = serde_json::Map::new();
443 r.insert(
444 "description".into(),
445 serde_json::Value::String(String::new()),
446 );
447 resp_obj.insert(status.to_string(), serde_json::Value::Object(r));
448 }
449 obj.insert("responses".into(), serde_json::Value::Object(resp_obj));
450 }
451
452 serde_json::Value::Object(obj)
453}
454
455fn emit_schema_value(schema: &Schema, vertex_id: &str) -> serde_json::Value {
457 let Some(vertex) = schema.vertices.get(vertex_id) else {
458 return serde_json::Value::Object(serde_json::Map::new());
459 };
460
461 let mut obj = serde_json::Map::new();
462
463 let type_str = match vertex.kind.as_str() {
464 "string" => Some("string"),
465 "integer" => Some("integer"),
466 "number" => Some("number"),
467 "boolean" => Some("boolean"),
468 "array" => Some("array"),
469 "object" | "schema-object" => Some("object"),
470 _ => None,
471 };
472
473 if let Some(t) = type_str {
474 obj.insert("type".into(), serde_json::Value::String(t.into()));
475 }
476
477 for field in &[
478 "format",
479 "minimum",
480 "maximum",
481 "pattern",
482 "minLength",
483 "maxLength",
484 "minItems",
485 "maxItems",
486 ] {
487 if let Some(val) = constraint_value(schema, vertex_id, field) {
488 if let Ok(n) = val.parse::<f64>() {
489 obj.insert((*field).into(), serde_json::json!(n));
490 } else {
491 obj.insert((*field).into(), serde_json::Value::String(val.to_string()));
492 }
493 }
494 }
495
496 let props = children_by_edge(schema, vertex_id, "prop");
498 if !props.is_empty() {
499 let mut properties = serde_json::Map::new();
500 let mut required_list = Vec::new();
501 for (edge, _child) in &props {
502 let name = edge.name.as_deref().unwrap_or("");
503 let child_schema = emit_schema_value(schema, &edge.tgt);
504 properties.insert(name.to_string(), child_schema);
505 if constraint_value(schema, &edge.tgt, "required") == Some("true") {
506 required_list.push(serde_json::Value::String(name.to_string()));
507 }
508 }
509 obj.insert("properties".into(), serde_json::Value::Object(properties));
510 if !required_list.is_empty() {
511 obj.insert("required".into(), serde_json::Value::Array(required_list));
512 }
513 }
514
515 let items = children_by_edge(schema, vertex_id, "items");
517 if let Some((edge, _)) = items.first() {
518 let items_schema = emit_schema_value(schema, &edge.tgt);
519 obj.insert("items".into(), items_schema);
520 }
521
522 serde_json::Value::Object(obj)
523}
524
525fn edge_rules() -> Vec<EdgeRule> {
527 vec![
528 EdgeRule {
529 edge_kind: "prop".into(),
530 src_kinds: vec![
531 "path".into(),
532 "operation".into(),
533 "parameter".into(),
534 "request-body".into(),
535 "response".into(),
536 "object".into(),
537 "schema-object".into(),
538 ],
539 tgt_kinds: vec![],
540 },
541 EdgeRule {
542 edge_kind: "items".into(),
543 src_kinds: vec!["array".into()],
544 tgt_kinds: vec![],
545 },
546 EdgeRule {
547 edge_kind: "variant".into(),
548 src_kinds: vec![],
549 tgt_kinds: vec![],
550 },
551 EdgeRule {
552 edge_kind: "ref".into(),
553 src_kinds: vec![],
554 tgt_kinds: vec![],
555 },
556 ]
557}
558
559#[cfg(test)]
560#[allow(clippy::expect_used, clippy::unwrap_used)]
561mod tests {
562 use super::*;
563
564 #[test]
565 fn protocol_def() {
566 let p = protocol();
567 assert_eq!(p.name, "openapi");
568 assert_eq!(p.schema_theory, "ThOpenAPISchema");
569 assert_eq!(p.instance_theory, "ThOpenAPIInstance");
570 }
571
572 #[test]
573 fn register_theories_works() {
574 let mut registry = HashMap::new();
575 register_theories(&mut registry);
576 assert!(registry.contains_key("ThOpenAPISchema"));
577 assert!(registry.contains_key("ThOpenAPIInstance"));
578 }
579
580 #[test]
581 fn parse_minimal() {
582 let doc = serde_json::json!({
583 "openapi": "3.0.0",
584 "info": {"title": "Test", "version": "1.0.0"},
585 "paths": {
586 "/users": {
587 "get": {
588 "parameters": [
589 {"name": "limit", "in": "query", "schema": {"type": "integer"}}
590 ],
591 "responses": {
592 "200": {
593 "description": "OK",
594 "content": {
595 "application/json": {
596 "schema": {
597 "type": "array",
598 "items": {"type": "string"}
599 }
600 }
601 }
602 }
603 }
604 }
605 }
606 }
607 });
608 let schema = parse_openapi(&doc).expect("should parse");
609 assert!(schema.has_vertex("path:/users"));
610 assert!(schema.has_vertex("path:/users:get"));
611 }
612
613 #[test]
614 fn emit_minimal() {
615 let doc = serde_json::json!({
616 "openapi": "3.0.0",
617 "info": {"title": "Test", "version": "1.0.0"},
618 "paths": {
619 "/pets": {
620 "get": {
621 "responses": {
622 "200": {"description": "OK"}
623 }
624 }
625 }
626 }
627 });
628 let schema = parse_openapi(&doc).expect("should parse");
629 let emitted = emit_openapi(&schema).expect("should emit");
630 assert!(emitted.get("paths").is_some());
631 }
632
633 #[test]
634 fn roundtrip() {
635 let doc = serde_json::json!({
636 "openapi": "3.0.0",
637 "info": {"title": "Test", "version": "1.0.0"},
638 "paths": {
639 "/items": {
640 "get": {
641 "responses": {
642 "200": {"description": "OK"}
643 }
644 }
645 }
646 }
647 });
648 let schema = parse_openapi(&doc).expect("parse");
649 let emitted = emit_openapi(&schema).expect("emit");
650 let schema2 = parse_openapi(&emitted).expect("re-parse");
651 assert_eq!(schema.vertices.len(), schema2.vertices.len());
652 }
653}