1use std::collections::{BTreeMap, HashSet};
2
3use serde_json::Value;
4
5use super::error::ParseError;
6use super::model::{
7 ApiSpec, ContentSchema, DispatchConfig, Message, MiddlewareConfig, Operation, Parameter,
8 RequestBody, ResponseContent, SpecFormat,
9};
10
11fn resolve_ref<'a>(root: &'a Value, ref_path: &str) -> Option<&'a Value> {
15 if !ref_path.starts_with("#/") {
16 return None;
17 }
18 let mut current = root;
19 for segment in ref_path[2..].split('/') {
20 let unescaped = segment.replace("~1", "/").replace("~0", "~");
21 current = current.get(&unescaped)?;
22 }
23 Some(current)
24}
25
26fn resolve_schema_refs(
31 value: &Value,
32 root: &Value,
33 visited: &mut HashSet<String>,
34) -> Result<Value, ParseError> {
35 match value {
36 Value::Object(obj) => {
37 if let Some(ref_str) = obj.get("$ref").and_then(|v| v.as_str()) {
38 if !visited.insert(ref_str.to_string()) {
39 return Err(ParseError::SchemaError(format!(
40 "circular $ref detected: {}",
41 ref_str
42 )));
43 }
44 let target = resolve_ref(root, ref_str)
45 .ok_or_else(|| ParseError::UnresolvedRef(ref_str.to_string()))?;
46 let resolved = resolve_schema_refs(target, root, visited)?;
47 visited.remove(ref_str);
48 Ok(resolved)
49 } else {
50 let mut new_obj = serde_json::Map::with_capacity(obj.len());
51 for (key, val) in obj {
52 new_obj.insert(key.clone(), resolve_schema_refs(val, root, visited)?);
53 }
54 Ok(Value::Object(new_obj))
55 }
56 }
57 Value::Array(arr) => {
58 let items: Result<Vec<_>, _> = arr
59 .iter()
60 .map(|v| resolve_schema_refs(v, root, visited))
61 .collect();
62 Ok(Value::Array(items?))
63 }
64 other => Ok(other.clone()),
65 }
66}
67
68const HTTP_METHODS: &[&str] = &[
71 "get", "post", "put", "delete", "patch", "head", "options", "trace", "query",
72];
73
74pub fn parse_spec(input: &str) -> Result<ApiSpec, ParseError> {
76 let root: Value =
78 serde_yaml::from_str(input).map_err(|e| ParseError::ParseError(e.to_string()))?;
79
80 let root_obj = root
81 .as_object()
82 .ok_or_else(|| ParseError::ParseError("spec root must be an object".into()))?;
83
84 let (format, version) = detect_format(root_obj)?;
86
87 let info = root_obj
89 .get("info")
90 .and_then(|v| v.as_object())
91 .ok_or_else(|| ParseError::SchemaError("missing 'info' object".into()))?;
92
93 let title = info
94 .get("title")
95 .and_then(|v| v.as_str())
96 .ok_or_else(|| ParseError::SchemaError("missing 'info.title'".into()))?
97 .to_string();
98
99 let api_version = info
100 .get("version")
101 .and_then(|v| v.as_str())
102 .unwrap_or("0.0.0")
103 .to_string();
104
105 let extensions = extract_extensions(root_obj);
107
108 let global_middlewares = extract_middlewares(root_obj);
110
111 let operations = match format {
113 SpecFormat::OpenApi => parse_openapi_paths(root_obj, &root)?,
114 SpecFormat::AsyncApi => parse_asyncapi_channels(root_obj, &root)?,
115 };
116
117 Ok(ApiSpec {
118 filename: None,
119 format,
120 version,
121 title,
122 api_version,
123 operations,
124 global_middlewares,
125 extensions,
126 })
127}
128
129pub fn parse_spec_file(path: &std::path::Path) -> Result<ApiSpec, ParseError> {
131 let content = std::fs::read_to_string(path)?;
132 let mut spec = parse_spec(&content)?;
133 spec.filename = path
134 .file_name()
135 .and_then(|s| s.to_str())
136 .map(|s| s.to_string());
137 Ok(spec)
138}
139
140fn detect_format(
142 root: &serde_json::Map<String, Value>,
143) -> Result<(SpecFormat, String), ParseError> {
144 if let Some(version) = root.get("openapi").and_then(|v| v.as_str()) {
145 if !version.starts_with("3.") {
146 return Err(ParseError::SchemaError(format!(
147 "unsupported OpenAPI version: {} (only 3.x supported)",
148 version
149 )));
150 }
151 Ok((SpecFormat::OpenApi, version.to_string()))
152 } else if let Some(version) = root.get("asyncapi").and_then(|v| v.as_str()) {
153 if !version.starts_with("3.") {
154 return Err(ParseError::SchemaError(format!(
155 "unsupported AsyncAPI version: {} (only 3.x supported)",
156 version
157 )));
158 }
159 Ok((SpecFormat::AsyncApi, version.to_string()))
160 } else {
161 Err(ParseError::UnknownFormat)
162 }
163}
164
165fn extract_extensions(obj: &serde_json::Map<String, Value>) -> BTreeMap<String, Value> {
167 obj.iter()
168 .filter(|(k, _)| k.starts_with("x-barbacane-"))
169 .map(|(k, v)| (k.clone(), v.clone()))
170 .collect()
171}
172
173fn extract_middlewares(obj: &serde_json::Map<String, Value>) -> Vec<MiddlewareConfig> {
175 obj.get("x-barbacane-middlewares")
176 .and_then(|v| v.as_array())
177 .map(|arr| {
178 arr.iter()
179 .filter_map(|item| serde_json::from_value(item.clone()).ok())
180 .collect()
181 })
182 .unwrap_or_default()
183}
184
185fn extract_dispatch(obj: &serde_json::Map<String, Value>) -> Option<DispatchConfig> {
187 obj.get("x-barbacane-dispatch")
188 .and_then(|v| serde_json::from_value(v.clone()).ok())
189}
190
191fn parse_openapi_paths(
193 root: &serde_json::Map<String, Value>,
194 spec_root: &Value,
195) -> Result<Vec<Operation>, ParseError> {
196 let mut operations = Vec::new();
197
198 let paths = match root.get("paths").and_then(|v| v.as_object()) {
199 Some(p) => p,
200 None => return Ok(operations), };
202
203 for (path, path_item) in paths {
204 let path_obj = path_item.as_object().ok_or_else(|| {
205 ParseError::SchemaError(format!("path item for '{}' must be an object", path))
206 })?;
207
208 let path_params = parse_parameters(path_obj, spec_root)?;
210
211 for method in HTTP_METHODS {
212 if let Some(op_value) = path_obj.get(*method) {
213 let op_obj = op_value.as_object().ok_or_else(|| {
214 ParseError::SchemaError(format!(
215 "operation {} {} must be an object",
216 method.to_uppercase(),
217 path
218 ))
219 })?;
220
221 let mut params = path_params.clone();
223 params.extend(parse_parameters(op_obj, spec_root)?);
224
225 let operation_id = op_obj
226 .get("operationId")
227 .and_then(|v| v.as_str())
228 .map(|s| s.to_string());
229
230 let summary = op_obj
231 .get("summary")
232 .and_then(|v| v.as_str())
233 .map(|s| s.to_string());
234
235 let description = op_obj
236 .get("description")
237 .and_then(|v| v.as_str())
238 .map(|s| s.to_string());
239
240 let request_body = parse_request_body(op_obj, spec_root)?;
241 let responses = parse_responses(op_obj, spec_root)?;
242
243 let dispatch = extract_dispatch(op_obj);
244
245 let middlewares = if op_obj.contains_key("x-barbacane-middlewares") {
246 Some(extract_middlewares(op_obj))
247 } else {
248 None
249 };
250
251 let deprecated = op_obj
253 .get("deprecated")
254 .and_then(|v| v.as_bool())
255 .unwrap_or(false);
256
257 let sunset = op_obj
259 .get("x-sunset")
260 .and_then(|v| v.as_str())
261 .map(|s| s.to_string());
262
263 let extensions = extract_extensions(op_obj);
264
265 operations.push(Operation {
266 path: path.clone(),
267 method: method.to_uppercase(),
268 operation_id,
269 summary,
270 description,
271 parameters: params,
272 request_body,
273 dispatch,
274 middlewares,
275 deprecated,
276 sunset,
277 extensions,
278 messages: Vec::new(), bindings: BTreeMap::new(), responses,
281 });
282 }
283 }
284
285 if let Some(additional) = path_obj
287 .get("additionalOperations")
288 .and_then(|v| v.as_object())
289 {
290 for (method_name, op_value) in additional {
291 let op_obj = op_value.as_object().ok_or_else(|| {
292 ParseError::SchemaError(format!(
293 "additionalOperations.{} on {} must be an object",
294 method_name, path
295 ))
296 })?;
297
298 let mut params = path_params.clone();
299 params.extend(parse_parameters(op_obj, spec_root)?);
300
301 let operation_id = op_obj
302 .get("operationId")
303 .and_then(|v| v.as_str())
304 .map(|s| s.to_string());
305
306 let summary = op_obj
307 .get("summary")
308 .and_then(|v| v.as_str())
309 .map(|s| s.to_string());
310
311 let description = op_obj
312 .get("description")
313 .and_then(|v| v.as_str())
314 .map(|s| s.to_string());
315
316 let request_body = parse_request_body(op_obj, spec_root)?;
317 let responses = parse_responses(op_obj, spec_root)?;
318 let dispatch = extract_dispatch(op_obj);
319
320 let middlewares = if op_obj.contains_key("x-barbacane-middlewares") {
321 Some(extract_middlewares(op_obj))
322 } else {
323 None
324 };
325
326 let deprecated = op_obj
327 .get("deprecated")
328 .and_then(|v| v.as_bool())
329 .unwrap_or(false);
330
331 let sunset = op_obj
332 .get("x-sunset")
333 .and_then(|v| v.as_str())
334 .map(|s| s.to_string());
335
336 let extensions = extract_extensions(op_obj);
337
338 operations.push(Operation {
339 path: path.clone(),
340 method: method_name.to_uppercase(),
341 operation_id,
342 summary,
343 description,
344 parameters: params,
345 request_body,
346 dispatch,
347 middlewares,
348 deprecated,
349 sunset,
350 extensions,
351 messages: Vec::new(),
352 bindings: BTreeMap::new(),
353 responses,
354 });
355 }
356 }
357 }
358
359 Ok(operations)
360}
361
362fn parse_parameters(
367 obj: &serde_json::Map<String, Value>,
368 spec_root: &Value,
369) -> Result<Vec<Parameter>, ParseError> {
370 let Some(arr) = obj.get("parameters").and_then(|v| v.as_array()) else {
371 return Ok(Vec::new());
372 };
373
374 let mut params = Vec::with_capacity(arr.len());
375 for item in arr {
376 let Some(param_obj) = item.as_object() else {
377 continue;
378 };
379 let Some(location) = param_obj.get("in").and_then(|v| v.as_str()) else {
380 continue;
381 };
382 let location = location.to_string();
383
384 let raw_schema = if location == "querystring" {
386 extract_content_schema(param_obj)
387 } else {
388 param_obj.get("schema").cloned()
389 };
390
391 let schema = raw_schema
392 .map(|s| resolve_schema_refs(&s, spec_root, &mut HashSet::new()))
393 .transpose()?;
394
395 let Some(name) = param_obj.get("name").and_then(|v| v.as_str()) else {
396 continue;
397 };
398 params.push(Parameter {
399 name: name.to_string(),
400 location,
401 required: param_obj
402 .get("required")
403 .and_then(|v| v.as_bool())
404 .unwrap_or(false),
405 schema,
406 });
407 }
408 Ok(params)
409}
410
411fn extract_content_schema(param_obj: &serde_json::Map<String, Value>) -> Option<Value> {
416 let content = param_obj.get("content")?.as_object()?;
417 let (_media_type, media_obj) = content.iter().next()?;
419 media_obj.as_object()?.get("schema").cloned()
420}
421
422fn parse_request_body(
424 obj: &serde_json::Map<String, Value>,
425 spec_root: &Value,
426) -> Result<Option<RequestBody>, ParseError> {
427 let Some(body) = obj.get("requestBody").and_then(|v| v.as_object()) else {
428 return Ok(None);
429 };
430
431 let required = body
432 .get("required")
433 .and_then(|v| v.as_bool())
434 .unwrap_or(false);
435
436 let Some(content_obj) = body.get("content").and_then(|v| v.as_object()) else {
437 return Ok(None);
438 };
439
440 let mut content = BTreeMap::new();
441 for (media_type, media_obj) in content_obj {
442 let raw_schema = media_obj.as_object().and_then(|o| o.get("schema").cloned());
443 let schema = raw_schema
444 .map(|s| resolve_schema_refs(&s, spec_root, &mut HashSet::new()))
445 .transpose()?;
446 content.insert(media_type.clone(), ContentSchema { schema });
447 }
448
449 Ok(Some(RequestBody { required, content }))
450}
451
452fn parse_responses(
454 obj: &serde_json::Map<String, Value>,
455 spec_root: &Value,
456) -> Result<BTreeMap<String, ResponseContent>, ParseError> {
457 let Some(responses) = obj.get("responses").and_then(|v| v.as_object()) else {
458 return Ok(BTreeMap::new());
459 };
460
461 let mut result = BTreeMap::new();
462 for (status_code, resp_value) in responses {
463 let resolved = resolve_schema_refs(resp_value, spec_root, &mut HashSet::new())?;
465 let Some(resp_obj) = resolved.as_object() else {
466 continue;
467 };
468
469 let Some(content_obj) = resp_obj.get("content").and_then(|v| v.as_object()) else {
470 continue;
471 };
472
473 let mut content = BTreeMap::new();
474 for (media_type, media_obj) in content_obj {
475 let raw_schema = media_obj.as_object().and_then(|o| o.get("schema").cloned());
476 let schema = raw_schema
477 .map(|s| resolve_schema_refs(&s, spec_root, &mut HashSet::new()))
478 .transpose()?;
479 content.insert(media_type.clone(), ContentSchema { schema });
480 }
481
482 if !content.is_empty() {
483 result.insert(status_code.clone(), ResponseContent { content });
484 }
485 }
486 Ok(result)
487}
488
489fn parse_asyncapi_channels(
495 root: &serde_json::Map<String, Value>,
496 spec_root: &Value,
497) -> Result<Vec<Operation>, ParseError> {
498 let mut operations = Vec::new();
499
500 let channels = root.get("channels").and_then(|v| v.as_object());
502 let ops = root.get("operations").and_then(|v| v.as_object());
503
504 let ops = match ops {
506 Some(o) => o,
507 None => return Ok(operations),
508 };
509
510 let channel_lookup = build_channel_lookup(channels, spec_root)?;
512
513 for (op_id, op_value) in ops {
514 let op_obj = op_value.as_object().ok_or_else(|| {
515 ParseError::SchemaError(format!("operation '{}' must be an object", op_id))
516 })?;
517
518 let action = op_obj
520 .get("action")
521 .and_then(|v| v.as_str())
522 .ok_or_else(|| {
523 ParseError::SchemaError(format!("operation '{}' missing 'action' field", op_id))
524 })?;
525
526 let method = match action {
528 "send" => "SEND",
529 "receive" => "RECEIVE",
530 other => {
531 return Err(ParseError::SchemaError(format!(
532 "operation '{}' has invalid action '{}' (must be 'send' or 'receive')",
533 op_id, other
534 )))
535 }
536 }
537 .to_string();
538
539 let (address, channel_messages, channel_params, channel_bindings) =
541 resolve_channel_ref(op_obj, &channel_lookup, spec_root)?;
542
543 let messages = parse_operation_messages(op_obj, &channel_messages, spec_root)?;
545
546 let request_body = if method == "SEND" && !messages.is_empty() {
548 messages.first().and_then(|msg| {
549 msg.payload.as_ref().map(|schema| {
550 let content_type = msg
551 .content_type
552 .clone()
553 .unwrap_or_else(|| "application/json".to_string());
554 let mut content = BTreeMap::new();
555 content.insert(
556 content_type,
557 ContentSchema {
558 schema: Some(schema.clone()),
559 },
560 );
561 RequestBody {
562 required: true,
563 content,
564 }
565 })
566 })
567 } else {
568 None
569 };
570
571 let mut bindings = channel_bindings;
573 if let Some(op_bindings) = op_obj.get("bindings").and_then(|v| v.as_object()) {
574 for (protocol, config) in op_bindings {
575 bindings.insert(protocol.clone(), config.clone());
576 }
577 }
578
579 let dispatch = extract_dispatch(op_obj);
581
582 let middlewares = if op_obj.contains_key("x-barbacane-middlewares") {
584 Some(extract_middlewares(op_obj))
585 } else {
586 None
587 };
588
589 let deprecated = op_obj
591 .get("deprecated")
592 .and_then(|v| v.as_bool())
593 .unwrap_or(false);
594
595 let sunset = op_obj
596 .get("x-sunset")
597 .and_then(|v| v.as_str())
598 .map(|s| s.to_string());
599
600 let extensions = extract_extensions(op_obj);
601
602 operations.push(Operation {
603 path: address,
604 method,
605 operation_id: Some(op_id.clone()),
606 summary: None,
607 description: None,
608 parameters: channel_params,
609 request_body,
610 dispatch,
611 middlewares,
612 deprecated,
613 sunset,
614 extensions,
615 messages,
616 bindings,
617 responses: BTreeMap::new(),
618 });
619 }
620
621 Ok(operations)
622}
623
624type ChannelInfo = (
626 String,
627 Vec<Message>,
628 Vec<Parameter>,
629 BTreeMap<String, Value>,
630);
631
632fn build_channel_lookup(
634 channels: Option<&serde_json::Map<String, Value>>,
635 spec_root: &Value,
636) -> Result<BTreeMap<String, ChannelInfo>, ParseError> {
637 let mut lookup = BTreeMap::new();
638
639 let channels = match channels {
640 Some(c) => c,
641 None => return Ok(lookup),
642 };
643
644 for (name, channel_value) in channels {
645 let channel_obj = match channel_value.as_object() {
646 Some(o) => o,
647 None => continue,
648 };
649
650 let address = channel_obj
652 .get("address")
653 .and_then(|v| v.as_str())
654 .map(|s| s.to_string())
655 .unwrap_or_else(|| name.clone());
656
657 let messages = parse_channel_messages(channel_obj, spec_root)?;
659
660 let parameters = parse_channel_parameters(channel_obj, spec_root)?;
662
663 let bindings = channel_obj
665 .get("bindings")
666 .and_then(|v| v.as_object())
667 .map(|b| {
668 b.iter()
669 .map(|(k, v)| (k.clone(), v.clone()))
670 .collect::<BTreeMap<_, _>>()
671 })
672 .unwrap_or_default();
673
674 lookup.insert(name.clone(), (address, messages, parameters, bindings));
675 }
676
677 Ok(lookup)
678}
679
680fn parse_channel_messages(
682 channel: &serde_json::Map<String, Value>,
683 spec_root: &Value,
684) -> Result<Vec<Message>, ParseError> {
685 let messages_obj = match channel.get("messages").and_then(|v| v.as_object()) {
686 Some(m) => m,
687 None => return Ok(Vec::new()),
688 };
689
690 let mut messages = Vec::with_capacity(messages_obj.len());
691 for (name, msg_value) in messages_obj {
692 let Some(msg_obj) = msg_value.as_object() else {
693 continue;
694 };
695
696 let payload = msg_obj
697 .get("payload")
698 .map(|p| resolve_schema_refs(p, spec_root, &mut HashSet::new()))
699 .transpose()?;
700
701 let content_type = msg_obj
702 .get("contentType")
703 .and_then(|v| v.as_str())
704 .map(|s| s.to_string());
705
706 let bindings = msg_obj
707 .get("bindings")
708 .and_then(|v| v.as_object())
709 .map(|b| {
710 b.iter()
711 .map(|(k, v)| (k.clone(), v.clone()))
712 .collect::<BTreeMap<_, _>>()
713 })
714 .unwrap_or_default();
715
716 messages.push(Message {
717 name: name.clone(),
718 payload,
719 content_type,
720 bindings,
721 });
722 }
723 Ok(messages)
724}
725
726fn parse_channel_parameters(
728 channel: &serde_json::Map<String, Value>,
729 spec_root: &Value,
730) -> Result<Vec<Parameter>, ParseError> {
731 let params = match channel.get("parameters").and_then(|v| v.as_object()) {
732 Some(p) => p,
733 None => return Ok(Vec::new()),
734 };
735
736 let mut result = Vec::with_capacity(params.len());
737 for (name, param_value) in params {
738 let raw_schema = param_value
739 .as_object()
740 .and_then(|o| o.get("schema").cloned());
741 let schema = raw_schema
742 .map(|s| resolve_schema_refs(&s, spec_root, &mut HashSet::new()))
743 .transpose()?;
744
745 result.push(Parameter {
747 name: name.clone(),
748 location: "path".to_string(),
749 required: true,
750 schema,
751 });
752 }
753 Ok(result)
754}
755
756fn resolve_channel_ref(
758 op: &serde_json::Map<String, Value>,
759 lookup: &BTreeMap<String, ChannelInfo>,
760 spec_root: &Value,
761) -> Result<ChannelInfo, ParseError> {
762 let channel = op
763 .get("channel")
764 .ok_or_else(|| ParseError::SchemaError("operation missing 'channel' field".into()))?;
765
766 if let Some(channel_obj) = channel.as_object() {
768 if let Some(ref_str) = channel_obj.get("$ref").and_then(|v| v.as_str()) {
769 let channel_name = ref_str.strip_prefix("#/channels/").ok_or_else(|| {
771 ParseError::SchemaError(format!(
772 "invalid channel $ref '{}' (expected #/channels/...)",
773 ref_str
774 ))
775 })?;
776
777 lookup.get(channel_name).cloned().ok_or_else(|| {
778 ParseError::SchemaError(format!(
779 "channel '{}' referenced but not defined",
780 channel_name
781 ))
782 })
783 } else {
784 let address = channel_obj
786 .get("address")
787 .and_then(|v| v.as_str())
788 .map(|s| s.to_string())
789 .unwrap_or_default();
790
791 let messages = parse_channel_messages(channel_obj, spec_root)?;
792 let parameters = parse_channel_parameters(channel_obj, spec_root)?;
793 let bindings = channel_obj
794 .get("bindings")
795 .and_then(|v| v.as_object())
796 .map(|b| {
797 b.iter()
798 .map(|(k, v)| (k.clone(), v.clone()))
799 .collect::<BTreeMap<_, _>>()
800 })
801 .unwrap_or_default();
802
803 Ok((address, messages, parameters, bindings))
804 }
805 } else {
806 Err(ParseError::SchemaError(
807 "operation 'channel' must be an object (either $ref or inline)".into(),
808 ))
809 }
810}
811
812fn parse_operation_messages(
814 op: &serde_json::Map<String, Value>,
815 channel_messages: &[Message],
816 spec_root: &Value,
817) -> Result<Vec<Message>, ParseError> {
818 let Some(msgs) = op.get("messages").and_then(|v| v.as_array()) else {
820 return Ok(channel_messages.to_vec());
822 };
823
824 let mut result = Vec::with_capacity(msgs.len());
825 for msg in msgs {
826 let Some(obj) = msg.as_object() else {
827 continue;
828 };
829
830 if let Some(ref_str) = obj.get("$ref").and_then(|v| v.as_str()) {
831 let parts: Vec<&str> = ref_str.split('/').collect();
834 if parts.len() >= 5 && parts[3] == "messages" {
835 let msg_name = parts[4];
836 if let Some(m) = channel_messages.iter().find(|m| m.name == msg_name) {
837 result.push(m.clone());
838 }
839 }
840 continue;
841 }
842
843 let name = obj
845 .get("name")
846 .and_then(|v| v.as_str())
847 .unwrap_or("default")
848 .to_string();
849 let payload = obj
850 .get("payload")
851 .map(|p| resolve_schema_refs(p, spec_root, &mut HashSet::new()))
852 .transpose()?;
853 let content_type = obj
854 .get("contentType")
855 .and_then(|v| v.as_str())
856 .map(|s| s.to_string());
857 let bindings = obj
858 .get("bindings")
859 .and_then(|v| v.as_object())
860 .map(|b| {
861 b.iter()
862 .map(|(k, v)| (k.clone(), v.clone()))
863 .collect::<BTreeMap<_, _>>()
864 })
865 .unwrap_or_default();
866
867 result.push(Message {
868 name,
869 payload,
870 content_type,
871 bindings,
872 });
873 }
874 Ok(result)
875}
876
877#[cfg(test)]
878mod tests {
879 use super::*;
880
881 #[test]
882 fn parse_minimal_openapi() {
883 let yaml = r#"
884openapi: "3.1.0"
885info:
886 title: Test API
887 version: "1.0.0"
888paths:
889 /health:
890 get:
891 operationId: getHealth
892 x-barbacane-dispatch:
893 name: mock
894 config:
895 status: 200
896"#;
897 let spec = parse_spec(yaml).unwrap();
898 assert_eq!(spec.format, SpecFormat::OpenApi);
899 assert_eq!(spec.version, "3.1.0");
900 assert_eq!(spec.title, "Test API");
901 assert_eq!(spec.operations.len(), 1);
902
903 let op = &spec.operations[0];
904 assert_eq!(op.path, "/health");
905 assert_eq!(op.method, "GET");
906 assert_eq!(op.operation_id, Some("getHealth".to_string()));
907
908 let dispatch = op.dispatch.as_ref().unwrap();
909 assert_eq!(dispatch.name, "mock");
910 }
911
912 #[test]
913 fn parse_path_with_parameters() {
914 let yaml = r#"
915openapi: "3.1.0"
916info:
917 title: Test API
918 version: "1.0.0"
919paths:
920 /users/{id}:
921 get:
922 operationId: getUser
923 parameters:
924 - name: id
925 in: path
926 required: true
927 schema:
928 type: integer
929 x-barbacane-dispatch:
930 name: mock
931 config:
932 status: 200
933"#;
934 let spec = parse_spec(yaml).unwrap();
935 let op = &spec.operations[0];
936 assert_eq!(op.parameters.len(), 1);
937
938 let param = &op.parameters[0];
939 assert_eq!(param.name, "id");
940 assert_eq!(param.location, "path");
941 assert!(param.required);
942 }
943
944 #[test]
945 fn parse_global_middlewares() {
946 let yaml = r#"
947openapi: "3.1.0"
948info:
949 title: Test API
950 version: "1.0.0"
951x-barbacane-middlewares:
952 - name: rate-limit
953 config:
954 quota: 100
955 window: 60
956paths:
957 /health:
958 get:
959 x-barbacane-dispatch:
960 name: mock
961"#;
962 let spec = parse_spec(yaml).unwrap();
963 assert_eq!(spec.global_middlewares.len(), 1);
964 assert_eq!(spec.global_middlewares[0].name, "rate-limit");
965 }
966
967 #[test]
968 fn parse_operation_middlewares_override() {
969 let yaml = r#"
970openapi: "3.1.0"
971info:
972 title: Test API
973 version: "1.0.0"
974x-barbacane-middlewares:
975 - name: global-auth
976paths:
977 /public:
978 get:
979 x-barbacane-middlewares: []
980 x-barbacane-dispatch:
981 name: mock
982"#;
983 let spec = parse_spec(yaml).unwrap();
984 let op = &spec.operations[0];
985 assert!(op.middlewares.is_some());
987 assert_eq!(op.middlewares.as_ref().unwrap().len(), 0);
988 }
989
990 #[test]
991 fn reject_openapi_2() {
992 let yaml = r#"
993swagger: "2.0"
994info:
995 title: Old API
996 version: "1.0.0"
997paths: {}
998"#;
999 let result = parse_spec(yaml);
1000 assert!(matches!(result, Err(ParseError::UnknownFormat)));
1001 }
1002
1003 #[test]
1004 fn parse_multiple_methods() {
1005 let yaml = r#"
1006openapi: "3.1.0"
1007info:
1008 title: Test API
1009 version: "1.0.0"
1010paths:
1011 /users:
1012 get:
1013 x-barbacane-dispatch:
1014 name: mock
1015 post:
1016 x-barbacane-dispatch:
1017 name: mock
1018"#;
1019 let spec = parse_spec(yaml).unwrap();
1020 assert_eq!(spec.operations.len(), 2);
1021
1022 let methods: Vec<&str> = spec
1023 .operations
1024 .iter()
1025 .map(|op| op.method.as_str())
1026 .collect();
1027 assert!(methods.contains(&"GET"));
1028 assert!(methods.contains(&"POST"));
1029 }
1030
1031 #[test]
1032 fn extract_barbacane_extensions() {
1033 let yaml = r#"
1034openapi: "3.1.0"
1035info:
1036 title: Test API
1037 version: "1.0.0"
1038x-barbacane-middlewares:
1039 - name: rate-limit
1040 config:
1041 requests_per_second: 100
1042paths:
1043 /health:
1044 get:
1045 x-barbacane-dispatch:
1046 name: mock
1047 x-barbacane-middlewares:
1048 - name: cache
1049 config:
1050 ttl: 60
1051"#;
1052 let spec = parse_spec(yaml).unwrap();
1053 assert!(spec.extensions.contains_key("x-barbacane-middlewares"));
1054
1055 let op = &spec.operations[0];
1056 assert!(op.extensions.contains_key("x-barbacane-middlewares"));
1057 }
1058
1059 #[test]
1060 fn parse_request_body() {
1061 let yaml = r#"
1062openapi: "3.1.0"
1063info:
1064 title: Test API
1065 version: "1.0.0"
1066paths:
1067 /users:
1068 post:
1069 operationId: createUser
1070 requestBody:
1071 required: true
1072 content:
1073 application/json:
1074 schema:
1075 type: object
1076 required:
1077 - name
1078 properties:
1079 name:
1080 type: string
1081 email:
1082 type: string
1083 format: email
1084 x-barbacane-dispatch:
1085 name: mock
1086"#;
1087 let spec = parse_spec(yaml).unwrap();
1088 let op = &spec.operations[0];
1089
1090 let body = op.request_body.as_ref().expect("should have request body");
1091 assert!(body.required);
1092 assert!(body.content.contains_key("application/json"));
1093
1094 let json_content = &body.content["application/json"];
1095 let schema = json_content.schema.as_ref().expect("should have schema");
1096 assert_eq!(schema.get("type").and_then(|v| v.as_str()), Some("object"));
1097 }
1098
1099 #[test]
1100 fn parse_deprecated_operation() {
1101 let yaml = r#"
1102openapi: "3.1.0"
1103info:
1104 title: Test API
1105 version: "1.0.0"
1106paths:
1107 /old-endpoint:
1108 get:
1109 deprecated: true
1110 x-sunset: "Sat, 31 Dec 2025 23:59:59 GMT"
1111 x-barbacane-dispatch:
1112 name: mock
1113 /new-endpoint:
1114 get:
1115 x-barbacane-dispatch:
1116 name: mock
1117"#;
1118 let spec = parse_spec(yaml).unwrap();
1119 assert_eq!(spec.operations.len(), 2);
1120
1121 let old_op = spec
1123 .operations
1124 .iter()
1125 .find(|op| op.path == "/old-endpoint")
1126 .unwrap();
1127 assert!(old_op.deprecated);
1128 assert_eq!(
1129 old_op.sunset,
1130 Some("Sat, 31 Dec 2025 23:59:59 GMT".to_string())
1131 );
1132
1133 let new_op = spec
1135 .operations
1136 .iter()
1137 .find(|op| op.path == "/new-endpoint")
1138 .unwrap();
1139 assert!(!new_op.deprecated);
1140 assert!(new_op.sunset.is_none());
1141 }
1142
1143 #[test]
1146 fn parse_minimal_asyncapi() {
1147 let yaml = r#"
1148asyncapi: "3.0.0"
1149info:
1150 title: User Events API
1151 version: "1.0.0"
1152channels:
1153 userSignedUp:
1154 address: user/signedup
1155 messages:
1156 UserSignedUpMessage:
1157 payload:
1158 type: object
1159 properties:
1160 userId:
1161 type: string
1162operations:
1163 processUserSignup:
1164 action: receive
1165 channel:
1166 $ref: '#/channels/userSignedUp'
1167 x-barbacane-dispatch:
1168 name: kafka
1169 config:
1170 topic: user-events
1171"#;
1172 let spec = parse_spec(yaml).unwrap();
1173 assert_eq!(spec.format, SpecFormat::AsyncApi);
1174 assert_eq!(spec.version, "3.0.0");
1175 assert_eq!(spec.title, "User Events API");
1176 assert_eq!(spec.operations.len(), 1);
1177
1178 let op = &spec.operations[0];
1179 assert_eq!(op.path, "user/signedup");
1180 assert_eq!(op.method, "RECEIVE");
1181 assert_eq!(op.operation_id, Some("processUserSignup".to_string()));
1182
1183 let dispatch = op.dispatch.as_ref().unwrap();
1185 assert_eq!(dispatch.name, "kafka");
1186
1187 assert_eq!(op.messages.len(), 1);
1189 assert_eq!(op.messages[0].name, "UserSignedUpMessage");
1190 assert!(op.messages[0].payload.is_some());
1191 }
1192
1193 #[test]
1194 fn parse_asyncapi_send_operation() {
1195 let yaml = r#"
1196asyncapi: "3.0.0"
1197info:
1198 title: Notification Service
1199 version: "1.0.0"
1200channels:
1201 notifications:
1202 address: notifications/{userId}
1203 parameters:
1204 userId:
1205 schema:
1206 type: string
1207 messages:
1208 NotificationMessage:
1209 contentType: application/json
1210 payload:
1211 type: object
1212 required:
1213 - title
1214 - body
1215 properties:
1216 title:
1217 type: string
1218 body:
1219 type: string
1220operations:
1221 sendNotification:
1222 action: send
1223 channel:
1224 $ref: '#/channels/notifications'
1225 x-barbacane-dispatch:
1226 name: nats
1227 config:
1228 subject: notifications
1229"#;
1230 let spec = parse_spec(yaml).unwrap();
1231 let op = &spec.operations[0];
1232
1233 assert_eq!(op.method, "SEND");
1234 assert_eq!(op.path, "notifications/{userId}");
1235 assert_eq!(op.operation_id, Some("sendNotification".to_string()));
1236
1237 assert_eq!(op.parameters.len(), 1);
1239 assert_eq!(op.parameters[0].name, "userId");
1240 assert_eq!(op.parameters[0].location, "path");
1241 assert!(op.parameters[0].required);
1242
1243 assert!(op.request_body.is_some());
1245 let body = op.request_body.as_ref().unwrap();
1246 assert!(body.required);
1247 assert!(body.content.contains_key("application/json"));
1248
1249 assert_eq!(op.messages.len(), 1);
1251 assert_eq!(
1252 op.messages[0].content_type,
1253 Some("application/json".to_string())
1254 );
1255 }
1256
1257 #[test]
1258 fn parse_asyncapi_with_bindings() {
1259 let yaml = r#"
1260asyncapi: "3.0.0"
1261info:
1262 title: Order Events
1263 version: "1.0.0"
1264channels:
1265 orderCreated:
1266 address: orders.created
1267 bindings:
1268 kafka:
1269 topic: order-events
1270 partitions: 10
1271 replicas: 3
1272 messages:
1273 OrderCreatedMessage:
1274 bindings:
1275 kafka:
1276 key:
1277 type: string
1278 payload:
1279 type: object
1280operations:
1281 handleOrderCreated:
1282 action: receive
1283 channel:
1284 $ref: '#/channels/orderCreated'
1285 bindings:
1286 kafka:
1287 groupId: order-processor
1288 x-barbacane-dispatch:
1289 name: kafka
1290"#;
1291 let spec = parse_spec(yaml).unwrap();
1292 let op = &spec.operations[0];
1293
1294 assert!(op.bindings.contains_key("kafka"));
1296 let kafka_binding = op.bindings.get("kafka").unwrap();
1297 assert!(kafka_binding.get("groupId").is_some());
1299
1300 assert!(op.messages[0].bindings.contains_key("kafka"));
1302 }
1303
1304 #[test]
1305 fn parse_asyncapi_inline_channel() {
1306 let yaml = r#"
1307asyncapi: "3.0.0"
1308info:
1309 title: Inline Channel Test
1310 version: "1.0.0"
1311operations:
1312 inlineOp:
1313 action: receive
1314 channel:
1315 address: inline/topic
1316 messages:
1317 InlineMessage:
1318 payload:
1319 type: string
1320 x-barbacane-dispatch:
1321 name: mock
1322"#;
1323 let spec = parse_spec(yaml).unwrap();
1324 let op = &spec.operations[0];
1325
1326 assert_eq!(op.path, "inline/topic");
1327 assert_eq!(op.messages.len(), 1);
1328 assert_eq!(op.messages[0].name, "InlineMessage");
1329 }
1330
1331 #[test]
1332 fn parse_asyncapi_multiple_operations() {
1333 let yaml = r#"
1334asyncapi: "3.0.0"
1335info:
1336 title: Multi-Op API
1337 version: "1.0.0"
1338channels:
1339 events:
1340 address: events
1341 messages:
1342 Event:
1343 payload:
1344 type: object
1345operations:
1346 publishEvent:
1347 action: send
1348 channel:
1349 $ref: '#/channels/events'
1350 x-barbacane-dispatch:
1351 name: kafka
1352 consumeEvent:
1353 action: receive
1354 channel:
1355 $ref: '#/channels/events'
1356 x-barbacane-dispatch:
1357 name: kafka
1358"#;
1359 let spec = parse_spec(yaml).unwrap();
1360 assert_eq!(spec.operations.len(), 2);
1361
1362 let send_op = spec
1363 .operations
1364 .iter()
1365 .find(|op| op.method == "SEND")
1366 .unwrap();
1367 let recv_op = spec
1368 .operations
1369 .iter()
1370 .find(|op| op.method == "RECEIVE")
1371 .unwrap();
1372
1373 assert_eq!(send_op.operation_id, Some("publishEvent".to_string()));
1374 assert_eq!(recv_op.operation_id, Some("consumeEvent".to_string()));
1375 }
1376
1377 #[test]
1378 fn parse_asyncapi_global_middlewares() {
1379 let yaml = r#"
1380asyncapi: "3.0.0"
1381info:
1382 title: Middleware Test
1383 version: "1.0.0"
1384x-barbacane-middlewares:
1385 - name: auth
1386 config:
1387 type: jwt
1388channels:
1389 events:
1390 address: events
1391 messages:
1392 Event:
1393 payload:
1394 type: object
1395operations:
1396 handleEvent:
1397 action: receive
1398 channel:
1399 $ref: '#/channels/events'
1400 x-barbacane-dispatch:
1401 name: kafka
1402"#;
1403 let spec = parse_spec(yaml).unwrap();
1404 assert_eq!(spec.global_middlewares.len(), 1);
1405 assert_eq!(spec.global_middlewares[0].name, "auth");
1406 }
1407
1408 #[test]
1409 fn parse_asyncapi_3_1() {
1410 let yaml = r#"
1411asyncapi: "3.1.0"
1412info:
1413 title: User Events API
1414 version: "1.0.0"
1415channels:
1416 userSignedUp:
1417 address: user/signedup
1418 messages:
1419 UserSignedUpMessage:
1420 payload:
1421 type: object
1422 properties:
1423 userId:
1424 type: string
1425operations:
1426 processUserSignup:
1427 action: send
1428 channel:
1429 $ref: '#/channels/userSignedUp'
1430 x-barbacane-dispatch:
1431 name: kafka
1432 config:
1433 topic: user-events
1434"#;
1435 let spec = parse_spec(yaml).unwrap();
1436 assert_eq!(spec.format, SpecFormat::AsyncApi);
1437 assert_eq!(spec.version, "3.1.0");
1438 assert_eq!(spec.operations.len(), 1);
1439 }
1440
1441 #[test]
1442 fn reject_asyncapi_2() {
1443 let yaml = r#"
1444asyncapi: "2.6.0"
1445info:
1446 title: Old AsyncAPI
1447 version: "1.0.0"
1448channels: {}
1449"#;
1450 let result = parse_spec(yaml);
1451 assert!(matches!(result, Err(ParseError::SchemaError(_))));
1452 }
1453
1454 #[test]
1457 fn parse_query_method() {
1458 let yaml = r#"
1459openapi: "3.2.0"
1460info:
1461 title: Query Method API
1462 version: "1.0.0"
1463paths:
1464 /search:
1465 query:
1466 operationId: searchItems
1467 requestBody:
1468 required: true
1469 content:
1470 application/json:
1471 schema:
1472 type: object
1473 properties:
1474 filter:
1475 type: string
1476 x-barbacane-dispatch:
1477 name: mock
1478 config:
1479 status: 200
1480"#;
1481 let spec = parse_spec(yaml).unwrap();
1482 assert_eq!(spec.version, "3.2.0");
1483 assert_eq!(spec.operations.len(), 1);
1484
1485 let op = &spec.operations[0];
1486 assert_eq!(op.path, "/search");
1487 assert_eq!(op.method, "QUERY");
1488 assert_eq!(op.operation_id, Some("searchItems".to_string()));
1489 assert!(op.request_body.is_some());
1490 }
1491
1492 #[test]
1493 fn parse_additional_operations() {
1494 let yaml = r#"
1495openapi: "3.2.0"
1496info:
1497 title: Custom Methods API
1498 version: "1.0.0"
1499paths:
1500 /cache/{key}:
1501 get:
1502 operationId: getCache
1503 x-barbacane-dispatch:
1504 name: mock
1505 additionalOperations:
1506 purge:
1507 operationId: purgeCache
1508 parameters:
1509 - name: key
1510 in: path
1511 required: true
1512 schema:
1513 type: string
1514 x-barbacane-dispatch:
1515 name: mock
1516 config:
1517 status: 204
1518"#;
1519 let spec = parse_spec(yaml).unwrap();
1520 assert_eq!(spec.operations.len(), 2);
1521
1522 let get_op = spec
1523 .operations
1524 .iter()
1525 .find(|op| op.method == "GET")
1526 .unwrap();
1527 assert_eq!(get_op.operation_id, Some("getCache".to_string()));
1528
1529 let purge_op = spec
1530 .operations
1531 .iter()
1532 .find(|op| op.method == "PURGE")
1533 .unwrap();
1534 assert_eq!(purge_op.operation_id, Some("purgeCache".to_string()));
1535 assert_eq!(purge_op.parameters.len(), 1);
1536 assert_eq!(purge_op.parameters[0].name, "key");
1537 }
1538
1539 #[test]
1540 fn parse_additional_operations_inherits_path_params() {
1541 let yaml = r#"
1542openapi: "3.2.0"
1543info:
1544 title: Path Params Inheritance
1545 version: "1.0.0"
1546paths:
1547 /items/{id}:
1548 parameters:
1549 - name: id
1550 in: path
1551 required: true
1552 schema:
1553 type: string
1554 additionalOperations:
1555 link:
1556 operationId: linkItem
1557 x-barbacane-dispatch:
1558 name: mock
1559"#;
1560 let spec = parse_spec(yaml).unwrap();
1561 assert_eq!(spec.operations.len(), 1);
1562
1563 let op = &spec.operations[0];
1564 assert_eq!(op.method, "LINK");
1565 assert_eq!(op.parameters.len(), 1);
1567 assert_eq!(op.parameters[0].name, "id");
1568 }
1569
1570 #[test]
1571 fn parse_querystring_parameter() {
1572 let yaml = r#"
1573openapi: "3.2.0"
1574info:
1575 title: Querystring API
1576 version: "1.0.0"
1577paths:
1578 /search:
1579 get:
1580 operationId: search
1581 parameters:
1582 - name: q
1583 in: querystring
1584 required: true
1585 content:
1586 application/x-www-form-urlencoded:
1587 schema:
1588 type: string
1589 minLength: 1
1590 x-barbacane-dispatch:
1591 name: mock
1592"#;
1593 let spec = parse_spec(yaml).unwrap();
1594 let op = &spec.operations[0];
1595 assert_eq!(op.parameters.len(), 1);
1596
1597 let param = &op.parameters[0];
1598 assert_eq!(param.name, "q");
1599 assert_eq!(param.location, "querystring");
1600 assert!(param.required);
1601 assert!(param.schema.is_some());
1603 assert_eq!(
1604 param.schema.as_ref().unwrap().get("type").unwrap(),
1605 "string"
1606 );
1607 }
1608
1609 #[test]
1612 fn resolve_ref_in_parameter_schema() {
1613 let yaml = r##"
1614openapi: "3.1.0"
1615info:
1616 title: Test API
1617 version: "1.0.0"
1618components:
1619 schemas:
1620 UserId:
1621 type: integer
1622 format: int64
1623paths:
1624 /users/{id}:
1625 get:
1626 parameters:
1627 - name: id
1628 in: path
1629 required: true
1630 schema:
1631 $ref: "#/components/schemas/UserId"
1632 x-barbacane-dispatch:
1633 name: mock
1634"##;
1635 let spec = parse_spec(yaml).unwrap();
1636 let param = &spec.operations[0].parameters[0];
1637 let schema = param.schema.as_ref().unwrap();
1638 assert!(schema.get("$ref").is_none());
1640 assert_eq!(schema.get("type").unwrap(), "integer");
1641 assert_eq!(schema.get("format").unwrap(), "int64");
1642 }
1643
1644 #[test]
1645 fn resolve_ref_in_request_body() {
1646 let yaml = r##"
1647openapi: "3.1.0"
1648info:
1649 title: Test API
1650 version: "1.0.0"
1651components:
1652 schemas:
1653 CreateUser:
1654 type: object
1655 required: [name]
1656 properties:
1657 name:
1658 type: string
1659paths:
1660 /users:
1661 post:
1662 requestBody:
1663 required: true
1664 content:
1665 application/json:
1666 schema:
1667 $ref: "#/components/schemas/CreateUser"
1668 x-barbacane-dispatch:
1669 name: mock
1670"##;
1671 let spec = parse_spec(yaml).unwrap();
1672 let body = spec.operations[0].request_body.as_ref().unwrap();
1673 let schema = body.content["application/json"].schema.as_ref().unwrap();
1674 assert!(schema.get("$ref").is_none());
1675 assert_eq!(schema.get("type").unwrap(), "object");
1676 assert!(schema.get("properties").is_some());
1677 }
1678
1679 #[test]
1680 fn resolve_nested_ref() {
1681 let yaml = r##"
1682openapi: "3.1.0"
1683info:
1684 title: Test API
1685 version: "1.0.0"
1686components:
1687 schemas:
1688 Address:
1689 type: object
1690 properties:
1691 street:
1692 type: string
1693 User:
1694 type: object
1695 properties:
1696 address:
1697 $ref: "#/components/schemas/Address"
1698paths:
1699 /users:
1700 post:
1701 requestBody:
1702 required: true
1703 content:
1704 application/json:
1705 schema:
1706 $ref: "#/components/schemas/User"
1707 x-barbacane-dispatch:
1708 name: mock
1709"##;
1710 let spec = parse_spec(yaml).unwrap();
1711 let body = spec.operations[0].request_body.as_ref().unwrap();
1712 let schema = body.content["application/json"].schema.as_ref().unwrap();
1713 assert!(schema.get("$ref").is_none());
1714 let address_schema = schema.get("properties").unwrap().get("address").unwrap();
1716 assert!(address_schema.get("$ref").is_none());
1717 assert_eq!(address_schema.get("type").unwrap(), "object");
1718 }
1719
1720 #[test]
1721 fn unresolved_ref_returns_error() {
1722 let yaml = r##"
1723openapi: "3.1.0"
1724info:
1725 title: Test API
1726 version: "1.0.0"
1727paths:
1728 /users:
1729 get:
1730 parameters:
1731 - name: id
1732 in: query
1733 schema:
1734 $ref: "#/components/schemas/DoesNotExist"
1735 x-barbacane-dispatch:
1736 name: mock
1737"##;
1738 let err = parse_spec(yaml).unwrap_err();
1739 assert!(
1740 matches!(err, ParseError::UnresolvedRef(ref s) if s.contains("DoesNotExist")),
1741 "expected UnresolvedRef, got: {:?}",
1742 err
1743 );
1744 }
1745
1746 #[test]
1747 fn circular_ref_returns_error() {
1748 let yaml = r##"
1749openapi: "3.1.0"
1750info:
1751 title: Test API
1752 version: "1.0.0"
1753components:
1754 schemas:
1755 Node:
1756 type: object
1757 properties:
1758 child:
1759 $ref: "#/components/schemas/Node"
1760paths:
1761 /nodes:
1762 get:
1763 parameters:
1764 - name: root
1765 in: query
1766 schema:
1767 $ref: "#/components/schemas/Node"
1768 x-barbacane-dispatch:
1769 name: mock
1770"##;
1771 let err = parse_spec(yaml).unwrap_err();
1772 assert!(
1773 matches!(err, ParseError::SchemaError(ref s) if s.contains("circular")),
1774 "expected SchemaError with 'circular', got: {:?}",
1775 err
1776 );
1777 }
1778
1779 #[test]
1780 fn asyncapi_message_payload_ref() {
1781 let yaml = r##"
1782asyncapi: "3.0.0"
1783info:
1784 title: Test API
1785 version: "1.0.0"
1786components:
1787 schemas:
1788 UserEvent:
1789 type: object
1790 properties:
1791 userId:
1792 type: string
1793channels:
1794 userSignedUp:
1795 address: user/signedup
1796 messages:
1797 userSignedUp:
1798 payload:
1799 $ref: "#/components/schemas/UserEvent"
1800operations:
1801 onUserSignedUp:
1802 action: receive
1803 channel:
1804 $ref: "#/channels/userSignedUp"
1805"##;
1806 let spec = parse_spec(yaml).unwrap();
1807 let op = &spec.operations[0];
1808 let msg = &op.messages[0];
1809 let payload = msg.payload.as_ref().unwrap();
1810 assert!(payload.get("$ref").is_none());
1811 assert_eq!(payload.get("type").unwrap(), "object");
1812 }
1813
1814 #[test]
1815 fn parse_summary_and_description() {
1816 let yaml = r##"
1817openapi: "3.1.0"
1818info:
1819 title: Test
1820 version: "1.0.0"
1821paths:
1822 /orders:
1823 post:
1824 operationId: createOrder
1825 summary: Create a new order
1826 description: Creates an order with items and shipping address
1827 x-barbacane-dispatch:
1828 name: mock
1829 config:
1830 status: 200
1831"##;
1832 let spec = parse_spec(yaml).expect("should parse");
1833 let op = &spec.operations[0];
1834 assert_eq!(op.summary.as_deref(), Some("Create a new order"));
1835 assert_eq!(
1836 op.description.as_deref(),
1837 Some("Creates an order with items and shipping address")
1838 );
1839 }
1840
1841 #[test]
1842 fn parse_summary_and_description_absent() {
1843 let yaml = r##"
1844openapi: "3.1.0"
1845info:
1846 title: Test
1847 version: "1.0.0"
1848paths:
1849 /health:
1850 get:
1851 x-barbacane-dispatch:
1852 name: mock
1853 config:
1854 status: 200
1855"##;
1856 let spec = parse_spec(yaml).expect("should parse");
1857 let op = &spec.operations[0];
1858 assert!(op.summary.is_none());
1859 assert!(op.description.is_none());
1860 }
1861
1862 #[test]
1863 fn parse_responses_with_schema() {
1864 let yaml = r##"
1865openapi: "3.1.0"
1866info:
1867 title: Test
1868 version: "1.0.0"
1869paths:
1870 /orders:
1871 post:
1872 operationId: createOrder
1873 x-barbacane-dispatch:
1874 name: mock
1875 config:
1876 status: 200
1877 responses:
1878 "200":
1879 content:
1880 application/json:
1881 schema:
1882 type: object
1883 properties:
1884 order_id:
1885 type: string
1886 "404":
1887 content:
1888 application/json:
1889 schema:
1890 type: object
1891 properties:
1892 error:
1893 type: string
1894"##;
1895 let spec = parse_spec(yaml).expect("should parse");
1896 let op = &spec.operations[0];
1897 assert_eq!(op.responses.len(), 2);
1898 assert!(op.responses.contains_key("200"));
1899 assert!(op.responses.contains_key("404"));
1900 let resp_200 = &op.responses["200"];
1901 let schema = resp_200.content["application/json"]
1902 .schema
1903 .as_ref()
1904 .expect("schema");
1905 assert!(schema["properties"]["order_id"].is_object());
1906 }
1907
1908 #[test]
1909 fn parse_responses_with_ref() {
1910 let yaml = r##"
1911openapi: "3.1.0"
1912info:
1913 title: Test
1914 version: "1.0.0"
1915components:
1916 schemas:
1917 Order:
1918 type: object
1919 properties:
1920 id:
1921 type: string
1922paths:
1923 /orders:
1924 post:
1925 operationId: createOrder
1926 x-barbacane-dispatch:
1927 name: mock
1928 config:
1929 status: 200
1930 responses:
1931 "200":
1932 content:
1933 application/json:
1934 schema:
1935 $ref: '#/components/schemas/Order'
1936"##;
1937 let spec = parse_spec(yaml).expect("should parse");
1938 let op = &spec.operations[0];
1939 let schema = op.responses["200"].content["application/json"]
1940 .schema
1941 .as_ref()
1942 .expect("schema");
1943 assert!(schema.get("$ref").is_none());
1945 assert!(schema["properties"]["id"].is_object());
1946 }
1947
1948 #[test]
1949 fn parse_responses_empty_when_no_content() {
1950 let yaml = r##"
1951openapi: "3.1.0"
1952info:
1953 title: Test
1954 version: "1.0.0"
1955paths:
1956 /health:
1957 get:
1958 x-barbacane-dispatch:
1959 name: mock
1960 config:
1961 status: 204
1962 responses:
1963 "204":
1964 description: No content
1965"##;
1966 let spec = parse_spec(yaml).expect("should parse");
1967 let op = &spec.operations[0];
1968 assert!(op.responses.is_empty());
1969 }
1970}