Skip to main content

barbacane_compiler/spec_parser/
parser.rs

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
11/// Resolve a JSON Reference like `#/components/schemas/User` from the spec root.
12///
13/// Only local references (`#/...`) are supported. Returns `None` for external refs.
14fn 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
26/// Recursively resolve all `$ref` pointers in a JSON Schema value.
27///
28/// Inlines the referenced definition in place. `visited` tracks the current resolution
29/// chain to detect circular references.
30fn 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
68/// HTTP methods we recognize in OpenAPI paths.
69/// Includes `query` from OpenAPI 3.2 (RFC 9110 extension).
70const HTTP_METHODS: &[&str] = &[
71    "get", "post", "put", "delete", "patch", "head", "options", "trace", "query",
72];
73
74/// Parse an OpenAPI or AsyncAPI spec from a YAML/JSON string.
75pub fn parse_spec(input: &str) -> Result<ApiSpec, ParseError> {
76    // Parse YAML (also handles JSON since JSON is valid YAML)
77    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    // Detect format
85    let (format, version) = detect_format(root_obj)?;
86
87    // Extract info
88    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    // Extract root-level x-barbacane-* extensions
106    let extensions = extract_extensions(root_obj);
107
108    // Extract global middlewares
109    let global_middlewares = extract_middlewares(root_obj);
110
111    // Parse operations based on format
112    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
129/// Parse a spec from a file path.
130pub 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
140/// Detect whether this is OpenAPI or AsyncAPI and extract the version.
141fn 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
165/// Extract all x-barbacane-* keys from an object.
166fn 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
173/// Extract x-barbacane-middlewares from an object.
174fn 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
185/// Extract x-barbacane-dispatch from an operation object.
186fn 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
191/// Parse OpenAPI 3.x paths into operations.
192fn 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), // No paths is valid (empty API)
201    };
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        // Path-level parameters (inherited by all operations)
209        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                // Merge path-level and operation-level parameters
222                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                // Extract deprecated flag (standard OpenAPI field)
252                let deprecated = op_obj
253                    .get("deprecated")
254                    .and_then(|v| v.as_bool())
255                    .unwrap_or(false);
256
257                // Extract sunset date from x-sunset extension (RFC 8594)
258                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(), // OpenAPI doesn't use AsyncAPI messages
279                    bindings: BTreeMap::new(), // OpenAPI doesn't use protocol bindings
280                    responses,
281                });
282            }
283        }
284
285        // OpenAPI 3.2: parse additionalOperations (custom HTTP methods)
286        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
362/// Parse parameters from a path item or operation object.
363///
364/// OpenAPI 3.2: `in: querystring` parameters use `content` instead of `schema`.
365/// The schema is extracted from `content.<media-type>.schema`.
366fn 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        // OpenAPI 3.2: querystring params use content instead of schema
385        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
411/// Extract schema from a parameter's `content` map (first media type entry).
412///
413/// Used for `in: querystring` parameters where the schema lives under
414/// `content.<media-type>.schema` instead of the top-level `schema` field.
415fn extract_content_schema(param_obj: &serde_json::Map<String, Value>) -> Option<Value> {
416    let content = param_obj.get("content")?.as_object()?;
417    // Use the first (and typically only) media type entry
418    let (_media_type, media_obj) = content.iter().next()?;
419    media_obj.as_object()?.get("schema").cloned()
420}
421
422/// Parse request body from an operation object.
423fn 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
452/// Parse response definitions from an operation object.
453fn 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        // Resolve $ref on the response object itself
464        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
489/// Parse AsyncAPI 3.x channels and operations.
490///
491/// AsyncAPI 3.x structure:
492/// - `channels`: Map of channel names to channel definitions (address, messages)
493/// - `operations`: Map of operation IDs to operation definitions (action, channel ref)
494fn 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    // Parse channels first to build a lookup map
501    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    // If no operations defined, return empty
505    let ops = match ops {
506        Some(o) => o,
507        None => return Ok(operations),
508    };
509
510    // Build channel lookup: channel_name -> (address, messages, parameters, bindings)
511    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        // Extract action (send/receive)
519        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        // Normalize action to uppercase for consistency with HTTP methods
527        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        // Resolve channel reference
540        let (address, channel_messages, channel_params, channel_bindings) =
541            resolve_channel_ref(op_obj, &channel_lookup, spec_root)?;
542
543        // Parse operation-level messages (may override or filter channel messages)
544        let messages = parse_operation_messages(op_obj, &channel_messages, spec_root)?;
545
546        // For SEND operations, create a request body from the first message payload
547        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        // Merge channel and operation-level bindings
572        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        // Extract dispatch config
580        let dispatch = extract_dispatch(op_obj);
581
582        // Extract middlewares
583        let middlewares = if op_obj.contains_key("x-barbacane-middlewares") {
584            Some(extract_middlewares(op_obj))
585        } else {
586            None
587        };
588
589        // Extract deprecated and sunset
590        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
624/// Channel info: (address, messages, parameters, bindings).
625type ChannelInfo = (
626    String,
627    Vec<Message>,
628    Vec<Parameter>,
629    BTreeMap<String, Value>,
630);
631
632/// Build a lookup map of channel names to their definitions.
633fn 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        // Extract address (defaults to channel name if not specified)
651        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        // Parse messages
658        let messages = parse_channel_messages(channel_obj, spec_root)?;
659
660        // Parse parameters
661        let parameters = parse_channel_parameters(channel_obj, spec_root)?;
662
663        // Parse bindings
664        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
680/// Parse messages from a channel definition.
681fn 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
726/// Parse parameters from a channel definition (for templated addresses).
727fn 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        // In AsyncAPI, channel parameters are always required
746        result.push(Parameter {
747            name: name.clone(),
748            location: "path".to_string(),
749            required: true,
750            schema,
751        });
752    }
753    Ok(result)
754}
755
756/// Resolve a channel reference from an operation.
757fn 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    // Channel can be a $ref or inline
767    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            // Parse $ref like "#/channels/userSignedUp"
770            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            // Inline channel definition
785            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
812/// Parse messages from an operation (may reference channel messages via $ref).
813fn parse_operation_messages(
814    op: &serde_json::Map<String, Value>,
815    channel_messages: &[Message],
816    spec_root: &Value,
817) -> Result<Vec<Message>, ParseError> {
818    // If operation has explicit messages array, use those
819    let Some(msgs) = op.get("messages").and_then(|v| v.as_array()) else {
820        // Use all channel messages (already resolved)
821        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            // Reference to channel message
832            // Format: "#/channels/channelName/messages/messageName"
833            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        // Inline message definition
844        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        // Operation has explicit middlewares (empty array = disable all)
986        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        // Check deprecated operation
1122        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        // Check non-deprecated operation
1134        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    // ==================== AsyncAPI 3.x Tests ====================
1144
1145    #[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        // Check dispatch config
1184        let dispatch = op.dispatch.as_ref().unwrap();
1185        assert_eq!(dispatch.name, "kafka");
1186
1187        // Check messages
1188        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        // Check channel parameters
1238        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        // SEND operations should have request_body from message payload
1244        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        // Check messages
1250        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        // Check operation-level bindings (merged from channel and operation)
1295        assert!(op.bindings.contains_key("kafka"));
1296        let kafka_binding = op.bindings.get("kafka").unwrap();
1297        // Operation binding should override channel binding
1298        assert!(kafka_binding.get("groupId").is_some());
1299
1300        // Check message bindings
1301        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    // ==================== OpenAPI 3.2 Tests ====================
1455
1456    #[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        // Path-level parameters should be inherited
1566        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        // Schema should be extracted from content, not top-level schema
1602        assert!(param.schema.is_some());
1603        assert_eq!(
1604            param.schema.as_ref().unwrap().get("type").unwrap(),
1605            "string"
1606        );
1607    }
1608
1609    // ── $ref resolution tests ────────────────────────────────────────────
1610
1611    #[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        // $ref should be inlined — no $ref key, actual schema fields present
1639        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        // Nested $ref inside User.properties.address should also be resolved
1715        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        // $ref should be resolved inline
1944        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}