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