1use anyhow::{Context, Result};
7use asyncapiv3::spec::{AsyncApiSpec, AsyncApiV3Spec};
8use serde_json::Value;
9use std::collections::{HashMap, HashSet};
10use std::fs;
11use std::path::Path;
12
13#[derive(Debug, Clone)]
15pub struct MessageDefinition {
16 pub schema: Value,
17 pub examples: Vec<Value>,
18}
19
20#[derive(Debug, Clone)]
22pub struct MessageOperationMetadata {
23 pub name: String,
24 pub action: String,
25 pub replies: Vec<String>,
26}
27
28#[derive(Debug, Clone)]
30#[allow(dead_code)]
31pub struct ChannelOperation {
32 pub name: String,
33 pub action: String,
34 pub messages: Vec<String>,
35 pub replies: Vec<String>,
36}
37
38pub fn parse_asyncapi_schema(path: &Path) -> Result<AsyncApiV3Spec> {
42 let content =
43 fs::read_to_string(path).with_context(|| format!("Failed to read AsyncAPI file: {}", path.display()))?;
44
45 let spec: AsyncApiSpec = if path.extension().and_then(|s| s.to_str()) == Some("json") {
46 serde_json::from_str(&content)
47 .with_context(|| format!("Failed to parse AsyncAPI JSON from {}", path.display()))?
48 } else {
49 serde_saphyr::from_str(&content)
50 .with_context(|| format!("Failed to parse AsyncAPI YAML from {}", path.display()))?
51 };
52
53 match spec {
54 AsyncApiSpec::V3_0_0(v3_spec) => Ok(v3_spec),
55 }
56}
57
58pub fn extract_message_schemas(spec: &AsyncApiV3Spec) -> Result<HashMap<String, MessageDefinition>> {
62 use asyncapiv3::spec::common::Either;
63 use asyncapiv3::spec::{channel::Channel, message::Message};
64
65 let mut schemas = HashMap::new();
66 let spec_doc = serde_json::to_value(spec).context("Failed to serialize AsyncAPI spec for $ref resolution")?;
67
68 for (message_name, message_ref_or) in &spec.components.messages {
69 tracing::debug!("Processing message: {}", message_name);
70
71 match message_ref_or {
72 Either::Right(message) => {
73 if let Some(definition) = build_message_definition(message, message_name, &spec_doc)? {
74 schemas.insert(message_name.clone(), definition);
75 }
76 }
77 Either::Left(reference) => {
78 if let Some(message) = resolve_ref_as::<Message>(&spec_doc, &reference.reference) {
79 if let Some(definition) = build_message_definition(&message, message_name, &spec_doc)? {
80 schemas.insert(message_name.clone(), definition);
81 }
82 } else {
83 tracing::debug!(
84 "Skipping unresolved message reference: {} -> {}",
85 message_name,
86 reference.reference
87 );
88 }
89 }
90 }
91 }
92
93 for (channel_name, channel_ref_or) in &spec.channels {
94 tracing::debug!("Processing channel: {}", channel_name);
95
96 match channel_ref_or {
97 Either::Right(channel) => {
98 process_channel_messages(channel_name, channel, &spec_doc, &mut schemas)?;
99 }
100 Either::Left(reference) => {
101 if let Some(channel) = resolve_ref_as::<Channel>(&spec_doc, &reference.reference) {
102 process_channel_messages(channel_name, &channel, &spec_doc, &mut schemas)?;
103 } else {
104 tracing::debug!("Skipping unresolved channel reference: {}", reference.reference);
105 }
106 }
107 }
108 }
109
110 Ok(schemas)
111}
112
113fn process_channel_messages(
114 channel_name: &str,
115 channel: &asyncapiv3::spec::channel::Channel,
116 spec_doc: &Value,
117 schemas: &mut HashMap<String, MessageDefinition>,
118) -> Result<()> {
119 use asyncapiv3::spec::common::Either;
120 use asyncapiv3::spec::message::Message;
121
122 for (msg_name, msg_ref_or) in &channel.messages {
123 let full_name = format!("{}_{}", channel_name.trim_start_matches('/'), msg_name);
124 match msg_ref_or {
125 Either::Right(message) => {
126 if let Some(definition) = build_message_definition(message, &full_name, spec_doc)? {
127 schemas.insert(full_name, definition);
128 }
129 }
130 Either::Left(reference) => {
131 if let Some(message) = resolve_ref_as::<Message>(spec_doc, &reference.reference) {
132 if let Some(definition) = build_message_definition(&message, &full_name, spec_doc)? {
133 schemas.insert(full_name, definition);
134 }
135 } else {
136 tracing::debug!(
137 "Channel {} message {} unresolved reference: {}",
138 channel_name,
139 msg_name,
140 reference.reference
141 );
142 }
143 }
144 }
145 }
146
147 Ok(())
148}
149
150fn build_message_definition(
151 message: &asyncapiv3::spec::message::Message,
152 message_name: &str,
153 spec_doc: &Value,
154) -> Result<Option<MessageDefinition>> {
155 let schema = match extract_schema_from_message(message, message_name, spec_doc)? {
156 Some(schema) => schema,
157 None => return Ok(None),
158 };
159 let schema = resolve_schema_tree(spec_doc, &schema, 32);
160
161 let mut examples: Vec<Value> = Vec::new();
162 for example in &message.examples {
163 if !example.payload.is_empty() {
164 examples.push(sorted_payload_to_value(&example.payload)?);
165 }
166 }
167
168 if examples.is_empty() {
169 examples = generate_example_from_schema(&schema)?;
170 }
171
172 Ok(Some(MessageDefinition { schema, examples }))
173}
174
175fn sorted_payload_to_value(payload: &HashMap<String, Value>) -> Result<Value> {
182 let mut keys: Vec<&String> = payload.keys().collect();
183 keys.sort();
184
185 let mut object = serde_json::Map::with_capacity(payload.len());
186 for key in keys {
187 let value = payload
188 .get(key)
189 .cloned()
190 .context("AsyncAPI message example payload key vanished during sort")?;
191 object.insert(key.clone(), value);
192 }
193
194 Ok(Value::Object(object))
195}
196
197fn extract_schema_from_message(
199 message: &asyncapiv3::spec::message::Message,
200 message_name: &str,
201 spec_doc: &Value,
202) -> Result<Option<Value>> {
203 use asyncapiv3::spec::common::Either;
204
205 let payload = if let Some(payload_ref_or) = &message.payload {
206 payload_ref_or
207 } else {
208 tracing::debug!("Message {} has no payload", message_name);
209 return Ok(None);
210 };
211
212 match payload {
213 Either::Right(schema_or_multiformat) => match schema_or_multiformat {
214 Either::Left(schema) => {
215 let schema_json =
216 serde_json::to_value(schema).context("Failed to serialize schemars::Schema to JSON")?;
217 Ok(Some(schema_json))
218 }
219 Either::Right(multi_format) => Ok(Some(multi_format.schema.clone())),
220 },
221 Either::Left(reference) => {
222 if let Some(resolved) = resolve_ref_value(spec_doc, &reference.reference) {
223 Ok(Some(normalize_schema_ref_value(resolved)))
224 } else {
225 tracing::debug!(
226 "Message {} payload has unresolved reference: {}",
227 message_name,
228 reference.reference
229 );
230 Ok(None)
231 }
232 }
233 }
234}
235
236pub fn generate_example_from_schema(schema: &Value) -> Result<Vec<Value>> {
240 let mut examples = Vec::new();
241
242 if let Some(schema_examples) = schema.get("examples").and_then(|e| e.as_array()) {
243 examples.extend(schema_examples.clone());
244 }
245
246 if examples.is_empty()
247 && schema
248 .get("type")
249 .and_then(|value| value.as_str())
250 .is_some_and(|ty| ty.eq_ignore_ascii_case("array"))
251 {
252 if let Some(items) = schema.get("items") {
253 let generated = generate_example_from_schema(items)?;
254 let template = generated
255 .into_iter()
256 .next()
257 .unwrap_or_else(|| Value::Object(serde_json::Map::new()));
258 let min_items = schema.get("minItems").and_then(serde_json::Value::as_u64).unwrap_or(1);
259 let mut target_len = usize::try_from(min_items).unwrap_or(usize::MAX);
260 if target_len == 0 {
261 target_len = 1;
262 }
263 let capped_len = target_len.min(5);
264 let mut array_values = Vec::new();
265 for _ in 0..capped_len {
266 array_values.push(template.clone());
267 }
268 examples.push(Value::Array(array_values));
269 } else {
270 examples.push(Value::Array(vec![]));
271 }
272 }
273
274 if examples.is_empty()
275 && let Some(obj) = schema.get("properties").and_then(|p| p.as_object())
276 {
277 let mut example = serde_json::Map::new();
278
279 for (prop_name, prop_schema) in obj {
280 let example_value = if let Some(const_val) = prop_schema.get("const") {
281 const_val.clone()
282 } else if let Some(type_str) = prop_schema.get("type").and_then(|t| t.as_str()) {
283 match type_str {
284 "string" => {
285 if let Some(format) = prop_schema.get("format").and_then(|f| f.as_str()) {
286 match format {
287 "date-time" => Value::String("2024-01-15T10:30:00Z".to_string()),
288 "date" => Value::String("2024-01-15".to_string()),
289 "time" => Value::String("10:30:00".to_string()),
290 "email" => Value::String("user@example.com".to_string()),
291 "uri" => Value::String("https://example.com".to_string()),
292 "uuid" => Value::String("550e8400-e29b-41d4-a716-446655440000".to_string()),
293 _ => Value::String(format!("example_{prop_name}")),
294 }
295 } else {
296 Value::String(format!("example_{prop_name}"))
297 }
298 }
299 "number" => Value::Number(
300 serde_json::Number::from_f64(std::f64::consts::PI)
301 .unwrap_or_else(|| serde_json::Number::from(314)),
302 ),
303 "integer" => Value::Number(serde_json::Number::from(42)),
304 "boolean" => Value::Bool(true),
305 _ => Value::Null,
306 }
307 } else {
308 Value::Null
309 };
310
311 example.insert(prop_name.clone(), example_value);
312 }
313
314 examples.push(Value::Object(example));
315 }
316
317 if examples.is_empty() {
318 examples.push(Value::Object(serde_json::Map::new()));
319 }
320
321 Ok(examples)
322}
323
324#[derive(Debug, Clone, Copy, PartialEq, Eq)]
326pub enum Protocol {
327 WebSocket,
328 Sse,
329 Http,
330 Kafka,
331 Mqtt,
332 Amqp,
333 Other,
334}
335
336impl Protocol {
337 #[must_use]
339 pub fn from_protocol_string(protocol: &str) -> Self {
340 match protocol.to_lowercase().as_str() {
341 "ws" | "wss" | "websocket" | "websockets" => Self::WebSocket,
342 "sse" | "server-sent-events" => Self::Sse,
343 "http" | "https" => Self::Http,
344 "kafka" => Self::Kafka,
345 "mqtt" => Self::Mqtt,
346 "amqp" => Self::Amqp,
347 _ => Self::Other,
348 }
349 }
350
351 #[must_use]
352 pub const fn as_str(&self) -> &'static str {
353 match self {
354 Self::WebSocket => "websocket",
355 Self::Sse => "sse",
356 Self::Http => "http",
357 Self::Kafka => "kafka",
358 Self::Mqtt => "mqtt",
359 Self::Amqp => "amqp",
360 Self::Other => "other",
361 }
362 }
363}
364
365impl std::fmt::Display for Protocol {
366 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
367 write!(f, "{}", self.as_str())
368 }
369}
370
371pub fn detect_primary_protocol(spec: &AsyncApiV3Spec) -> Result<Protocol> {
373 use asyncapiv3::spec::common::Either;
374 use asyncapiv3::spec::server::Server;
375
376 let spec_doc =
377 serde_json::to_value(spec).context("Failed to serialize AsyncAPI spec for server $ref resolution")?;
378
379 for server_or_ref in spec.servers.values() {
380 match server_or_ref {
381 Either::Right(server) => {
382 let protocol = Protocol::from_protocol_string(&server.protocol);
383 tracing::debug!("Detected protocol: {:?} from '{}'", protocol, server.protocol);
384 return Ok(protocol);
385 }
386 Either::Left(reference) => {
387 if let Some(server) = resolve_ref_as::<Server>(&spec_doc, &reference.reference) {
388 let protocol = Protocol::from_protocol_string(&server.protocol);
389 tracing::debug!(
390 "Detected protocol: {:?} from referenced '{}'",
391 protocol,
392 server.protocol
393 );
394 return Ok(protocol);
395 }
396 tracing::debug!("Skipping unresolved server reference: {}", reference.reference);
397 }
398 }
399 }
400
401 tracing::warn!("Could not determine protocol from spec, defaulting to WebSocket");
402 Ok(Protocol::WebSocket)
403}
404
405pub fn decode_pointer_segment(segment: &str) -> String {
407 segment.replace("~1", "/").replace("~0", "~")
408}
409
410fn reference_to_pointer(reference: &str) -> Option<String> {
411 let raw = reference.strip_prefix("#/")?;
412 let mut pointer = String::new();
413 for segment in raw.split('/') {
414 pointer.push('/');
415 pointer.push_str(&decode_pointer_segment(segment));
416 }
417 Some(pointer)
418}
419
420fn resolve_ref_value(document: &Value, reference: &str) -> Option<Value> {
421 let mut current = reference.to_string();
422 let mut visited = HashSet::new();
423
424 for _ in 0..32 {
425 if !visited.insert(current.clone()) {
426 return None;
427 }
428
429 let pointer = reference_to_pointer(¤t)?;
430 let value = document.pointer(&pointer)?;
431
432 if let Some(next_ref) = value.get("$ref").and_then(Value::as_str) {
433 current = next_ref.to_string();
434 continue;
435 }
436
437 return Some(value.clone());
438 }
439
440 None
441}
442
443fn resolve_ref_as<T>(document: &Value, reference: &str) -> Option<T>
444where
445 T: serde::de::DeserializeOwned,
446{
447 let value = resolve_ref_value(document, reference)?;
448 serde_json::from_value(value).ok()
449}
450
451fn normalize_schema_ref_value(value: Value) -> Value {
452 if let Some(obj) = value.as_object()
453 && obj.get("schemaFormat").is_some()
454 && let Some(schema) = obj.get("schema")
455 {
456 return schema.clone();
457 }
458 value
459}
460
461fn resolve_schema_tree(document: &Value, schema: &Value, remaining_depth: usize) -> Value {
462 if remaining_depth == 0 {
463 return schema.clone();
464 }
465
466 if let Some(reference) = schema.get("$ref").and_then(Value::as_str)
467 && let Some(resolved) = resolve_ref_value(document, reference)
468 {
469 return resolve_schema_tree(document, &normalize_schema_ref_value(resolved), remaining_depth - 1);
470 }
471
472 match schema {
473 Value::Object(map) => {
474 let mut resolved = serde_json::Map::new();
475 for (key, value) in map {
476 resolved.insert(key.clone(), resolve_schema_tree(document, value, remaining_depth - 1));
477 }
478 Value::Object(resolved)
479 }
480 Value::Array(items) => Value::Array(
481 items
482 .iter()
483 .map(|item| resolve_schema_tree(document, item, remaining_depth - 1))
484 .collect(),
485 ),
486 _ => schema.clone(),
487 }
488}
489
490pub fn resolve_channel_from_ref(reference: &str) -> Option<String> {
492 let raw = reference.strip_prefix("#/channels/")?;
493 let decoded = raw.split('/').map(decode_pointer_segment).collect::<Vec<_>>().join("/");
494 let normalized = decoded.trim_start_matches('/').to_string();
495 Some(format!("/{normalized}"))
496}
497
498pub fn resolve_message_from_ref(reference: &str) -> Option<String> {
500 if let Some(name) = reference.strip_prefix("#/components/messages/") {
501 return Some(name.to_string());
502 }
503
504 if let Some(rest) = reference.strip_prefix("#/channels/") {
505 let mut parts = rest.split('/');
506 let channel = parts.next()?;
507 if parts.next()? != "messages" {
508 return None;
509 }
510 let message = parts.next()?;
511 let channel_name = decode_pointer_segment(channel);
512 let slug = channel_name.trim_start_matches('/').replace('/', "_");
513 return Some(format!("{}_{}", slug, decode_pointer_segment(message)));
514 }
515
516 None
517}
518
519pub const fn operation_action_name(action: &asyncapiv3::spec::operation::OperationAction) -> &'static str {
521 use asyncapiv3::spec::operation::OperationAction;
522 match action {
523 OperationAction::Send => "send",
524 OperationAction::Receive => "receive",
525 }
526}
527
528pub fn collect_message_channels(spec: &AsyncApiV3Spec) -> (HashMap<String, String>, HashMap<String, String>) {
530 use asyncapiv3::spec::common::Either;
531
532 let mut map = HashMap::new();
533 let mut aliases = HashMap::new();
534
535 for (channel_path, channel_ref_or) in &spec.channels {
536 let address = match channel_ref_or {
537 Either::Right(channel) => channel.address.clone().unwrap_or_else(|| channel_path.clone()),
538 Either::Left(_) => continue,
539 };
540 let normalized_address = if address.starts_with('/') {
541 address.clone()
542 } else {
543 format!("/{address}")
544 };
545
546 if let Either::Right(channel) = channel_ref_or {
547 for (message_name, message_ref) in &channel.messages {
548 let slug = channel_path.trim_start_matches('/').replace('/', "_");
549 let inline_key = format!("{slug}_{message_name}");
550 match message_ref {
551 Either::Right(_) => {
552 map.entry(inline_key.clone())
553 .or_insert_with(|| normalized_address.clone());
554 }
555 Either::Left(reference) => {
556 let target =
557 resolve_message_from_ref(&reference.reference).unwrap_or_else(|| message_name.clone());
558 map.entry(target.clone()).or_insert_with(|| normalized_address.clone());
559 aliases.insert(inline_key, target);
560 }
561 }
562 }
563 }
564 }
565
566 (map, aliases)
567}
568
569pub fn collect_message_operations(
571 spec: &AsyncApiV3Spec,
572 aliases: &HashMap<String, String>,
573) -> HashMap<String, Vec<MessageOperationMetadata>> {
574 use asyncapiv3::spec::common::Either;
575
576 let mut map: HashMap<String, Vec<MessageOperationMetadata>> = HashMap::new();
577
578 for (op_name, operation_ref) in &spec.operations {
579 let operation = match operation_ref {
580 Either::Right(op) => op,
581 Either::Left(_) => continue,
582 };
583
584 let replies: Vec<String> = if let Some(Either::Right(reply)) = &operation.reply {
585 reply
586 .messages
587 .iter()
588 .filter_map(|reference| resolve_message_from_ref(&reference.reference))
589 .collect()
590 } else {
591 Vec::new()
592 };
593
594 if let Some(message_refs) = &operation.messages {
595 for reference in message_refs {
596 if let Some(name) = resolve_message_from_ref(&reference.reference) {
597 let resolved_name = aliases.get(&name).cloned().unwrap_or(name.clone());
598 map.entry(resolved_name).or_default().push(MessageOperationMetadata {
599 name: op_name.clone(),
600 action: operation_action_name(&operation.action).to_string(),
601 replies: replies.clone(),
602 });
603 }
604 }
605 }
606 }
607
608 map
609}
610
611pub fn collect_channel_operations(spec: &AsyncApiV3Spec) -> HashMap<String, Vec<ChannelOperation>> {
613 use asyncapiv3::spec::common::Either;
614
615 let mut map: HashMap<String, Vec<ChannelOperation>> = HashMap::new();
616
617 for (op_name, operation_ref) in &spec.operations {
618 let operation = match operation_ref {
619 Either::Right(op) => op,
620 Either::Left(_) => continue,
621 };
622
623 let channel_path = match resolve_channel_from_ref(&operation.channel.reference) {
624 Some(path) => path,
625 None => continue,
626 };
627
628 let messages = operation
629 .messages
630 .as_ref()
631 .map(|refs| {
632 refs.iter()
633 .filter_map(|reference| resolve_message_from_ref(&reference.reference))
634 .collect::<Vec<_>>()
635 })
636 .unwrap_or_default();
637
638 let replies = if let Some(Either::Right(reply)) = &operation.reply {
639 reply
640 .messages
641 .iter()
642 .filter_map(|reference| resolve_message_from_ref(&reference.reference))
643 .collect::<Vec<_>>()
644 } else {
645 Vec::new()
646 };
647
648 map.entry(channel_path.clone()).or_default().push(ChannelOperation {
649 name: op_name.clone(),
650 action: operation_action_name(&operation.action).to_string(),
651 messages,
652 replies,
653 });
654 }
655
656 map
657}
658
659#[cfg(test)]
660mod tests {
661 use super::*;
662
663 #[test]
664 fn test_protocol_detection() {
665 assert_eq!(Protocol::from_protocol_string("ws"), Protocol::WebSocket);
666 assert_eq!(Protocol::from_protocol_string("wss"), Protocol::WebSocket);
667 assert_eq!(Protocol::from_protocol_string("websocket"), Protocol::WebSocket);
668 assert_eq!(Protocol::from_protocol_string("sse"), Protocol::Sse);
669 assert_eq!(Protocol::from_protocol_string("server-sent-events"), Protocol::Sse);
670 assert_eq!(Protocol::from_protocol_string("http"), Protocol::Http);
671 assert_eq!(Protocol::from_protocol_string("https"), Protocol::Http);
672 assert_eq!(Protocol::from_protocol_string("kafka"), Protocol::Kafka);
673 assert_eq!(Protocol::from_protocol_string("unknown"), Protocol::Other);
674 }
675
676 #[test]
677 fn test_decode_pointer_segment() {
678 assert_eq!(decode_pointer_segment("hello~1world"), "hello/world");
679 assert_eq!(decode_pointer_segment("test~0value"), "test~value");
680 }
681
682 #[test]
683 fn test_resolve_message_from_ref_components() {
684 let result = resolve_message_from_ref("#/components/messages/UserMessage");
685 assert_eq!(result, Some("UserMessage".to_string()));
686 }
687
688 #[test]
689 fn test_reference_to_pointer_decodes_json_pointer_segments() {
690 let pointer = reference_to_pointer("#/channels/user~1signedup/messages/user~0created");
691 assert_eq!(
692 pointer,
693 Some("/channels/user/signedup/messages/user~created".to_string())
694 );
695 }
696
697 #[test]
698 fn test_resolve_ref_value_follows_nested_local_refs() {
699 let doc = serde_json::json!({
700 "components": {
701 "schemas": {
702 "A": { "$ref": "#/components/schemas/B" },
703 "B": { "type": "object", "properties": { "id": { "type": "string" } } }
704 }
705 }
706 });
707
708 let resolved = resolve_ref_value(&doc, "#/components/schemas/A").expect("resolved schema");
709 assert_eq!(resolved["type"], "object");
710 assert!(resolved["properties"].get("id").is_some());
711 }
712
713 #[test]
714 fn test_detect_primary_protocol_resolves_server_refs() {
715 let spec_value = serde_json::json!({
716 "asyncapi": "3.0.0",
717 "info": { "title": "Test", "version": "1.0.0" },
718 "servers": {
719 "default": { "$ref": "#/components/servers/wsServer" }
720 },
721 "channels": {},
722 "operations": {},
723 "components": {
724 "servers": {
725 "wsServer": {
726 "host": "example.com",
727 "protocol": "wss"
728 }
729 }
730 }
731 });
732
733 let spec = match serde_json::from_value::<AsyncApiSpec>(spec_value).expect("valid asyncapi spec") {
734 AsyncApiSpec::V3_0_0(v3) => v3,
735 };
736
737 let protocol = detect_primary_protocol(&spec).expect("protocol detection");
738 assert_eq!(protocol, Protocol::WebSocket);
739 }
740}