1use serde_json::{json, Value};
10
11use crate::ast::{BasicEventType, EtlDocument, Node};
12use crate::spanned::{
13 parse_document_with_spans, ElementKind, IndexedElement, PathPart, PathKey, Span, SpanIndex,
14 SpanKey,
15};
16
17const SYMBOL_NAMESPACE: u32 = 3;
19const SYMBOL_OBJECT: u32 = 19;
20const SYMBOL_FIELD: u32 = 8;
21const SYMBOL_EVENT: u32 = 24;
22const SYMBOL_METHOD: u32 = 6;
23
24const COMPLETION_KEYWORD: u32 = 14;
25const COMPLETION_FIELD: u32 = 5;
26const COMPLETION_REFERENCE: u32 = 18;
27
28fn char_to_byte(content: &str, offset: u32) -> usize {
31 content
32 .char_indices()
33 .nth(offset as usize)
34 .map(|(i, _)| i)
35 .unwrap_or(content.len())
36}
37
38fn range(span: &Span) -> Value {
39 json!({
40 "start": { "line": span.line, "character": span.column },
41 "end": { "line": span.end_line, "character": span.end_column }
42 })
43}
44
45fn location(el: &IndexedElement) -> Value {
46 let span = el.key_span.unwrap_or(el.span);
47 json!({ "range": range(&span) })
48}
49
50pub fn document_symbols(content: &str) -> Result<Value, String> {
55 let (doc, index) = parse_document_with_spans(content)?;
56 let mut symbols: Vec<Value> = Vec::new();
57
58 if let Some(section_el) = index.resolve(&SpanKey::Section("event_trees")) {
59 let mut children = Vec::new();
60 for (tree_name, tree) in &doc.event_trees {
61 let mut tree_children = Vec::new();
62 if let Some(ie) = index.resolve(&SpanKey::InitiatingEvent {
63 tree: tree_name.clone(),
64 field: "id",
65 }) {
66 tree_children.push(symbol("initiatingEvent", None, SYMBOL_EVENT, ie, vec![]));
67 }
68 let mut node_children = Vec::new();
69 for (node_id, node) in &tree.nodes {
70 let Some(el) = index.resolve(&SpanKey::Node {
71 tree: tree_name.clone(),
72 id: node_id.clone(),
73 }) else {
74 continue;
75 };
76 let detail = match node {
77 Node::Barrier(b) => {
78 Some(format!("barrier · {} branch(es)", b.branches.len()))
79 }
80 Node::Operation(op) => Some(format!("operation · handler {}", op.handler)),
81 Node::Consequence(c) => {
82 let op = match c.consequence_operation {
83 crate::ast::ConsequenceOperation::Send => "send",
84 crate::ast::ConsequenceOperation::Terminate => "terminate",
85 };
86 Some(format!("consequence · {}", op))
87 }
88 };
89 node_children.push(symbol(node_id, detail, SYMBOL_OBJECT, el, vec![]));
90 }
91 let nodes_el = index
92 .resolve(&SpanKey::NodeField {
93 tree: tree_name.clone(),
94 id: String::new(),
95 field: "branches",
96 })
97 .or_else(|| index.resolve(&SpanKey::Tree {
98 tree: tree_name.clone(),
99 }));
100 if !node_children.is_empty() {
101 let group_el = nodes_el.unwrap_or(section_el);
102 tree_children.push(symbol("nodes", None, SYMBOL_FIELD, group_el, node_children));
103 }
104 let tree_el = index
105 .resolve(&SpanKey::Tree {
106 tree: tree_name.clone(),
107 })
108 .unwrap_or(section_el);
109 children.push(symbol(tree_name, None, SYMBOL_NAMESPACE, tree_el, tree_children));
110 }
111 symbols.push(symbol("eventTrees", None, SYMBOL_NAMESPACE, section_el, children));
112 }
113
114 if let Some(section_el) = index.resolve(&SpanKey::Section("fault_trees")) {
115 let mut children = Vec::new();
116 if let Some(ftrees) = &doc.fault_trees {
117 for (ft_name, ft) in ftrees {
118 let mut ft_children = Vec::new();
119 if let Some(te) = index.resolve(&SpanKey::TopEvent {
120 tree: ft_name.clone(),
121 field: "id",
122 }) {
123 ft_children.push(symbol("topEvent", None, SYMBOL_EVENT, te, vec![]));
124 }
125 let mut gate_children = Vec::new();
126 if let Some(gates) = &ft.gates {
127 for (gate_id, gate) in gates {
128 let Some(el) = index.resolve(&SpanKey::Gate {
129 tree: ft_name.clone(),
130 id: gate_id.clone(),
131 }) else {
132 continue;
133 };
134 let detail = format!(
135 "{:?} gate · {} input(s)",
136 gate.gate_type,
137 gate.inputs.len()
138 );
139 gate_children.push(symbol(gate_id, Some(detail), SYMBOL_METHOD, el, vec![]));
140 }
141 }
142 if !gate_children.is_empty() {
143 let group_el = index
144 .resolve(&SpanKey::Gate {
145 tree: ft_name.clone(),
146 id: String::new(),
147 })
148 .or_else(|| index.resolve(&SpanKey::FaultTree {
149 tree: ft_name.clone(),
150 }));
151 let group_el = group_el.unwrap_or(section_el);
152 ft_children.push(symbol("gates", None, SYMBOL_FIELD, group_el, gate_children));
153 }
154 let mut be_children = Vec::new();
155 for (be_id, be) in &ft.basic_events {
156 let Some(el) = index.resolve(&SpanKey::BasicEvent {
157 tree: ft_name.clone(),
158 id: be_id.clone(),
159 }) else {
160 continue;
161 };
162 let detail = match be.event_type {
163 Some(BasicEventType::House) => "house event".to_string(),
164 Some(BasicEventType::Undeveloped) => "undeveloped event".to_string(),
165 Some(BasicEventType::Conditional) => "conditional event".to_string(),
166 _ => "basic event".to_string(),
167 };
168 be_children.push(symbol(be_id, Some(detail), SYMBOL_EVENT, el, vec![]));
169 }
170 if !be_children.is_empty() {
171 let group_el = index
172 .resolve(&SpanKey::BasicEvent {
173 tree: ft_name.clone(),
174 id: String::new(),
175 })
176 .or_else(|| index.resolve(&SpanKey::FaultTree {
177 tree: ft_name.clone(),
178 }));
179 let group_el = group_el.unwrap_or(section_el);
180 ft_children.push(symbol(
181 "basicEvents",
182 None,
183 SYMBOL_FIELD,
184 group_el,
185 be_children,
186 ));
187 }
188 let ft_el = index
189 .resolve(&SpanKey::FaultTree {
190 tree: ft_name.clone(),
191 })
192 .unwrap_or(section_el);
193 children.push(symbol(ft_name, None, SYMBOL_NAMESPACE, ft_el, ft_children));
194 }
195 }
196 symbols.push(symbol("faultTrees", None, SYMBOL_NAMESPACE, section_el, children));
197 }
198
199 Ok(json!({
200 "symbols": symbols
201 }))
202}
203
204fn symbol(
205 name: &str,
206 detail: Option<String>,
207 kind: u32,
208 el: &IndexedElement,
209 children: Vec<Value>,
210) -> Value {
211 let mut obj = serde_json::Map::new();
212 obj.insert("name".to_string(), Value::String(name.to_string()));
213 if let Some(d) = detail {
214 obj.insert("detail".to_string(), Value::String(d));
215 }
216 obj.insert("kind".to_string(), Value::from(kind));
217 obj.insert("range".to_string(), range(&el.span));
218 let selection = el.key_span.unwrap_or(el.span);
219 obj.insert("selectionRange".to_string(), range(&selection));
220 if !children.is_empty() {
221 obj.insert("children".to_string(), Value::Array(children));
222 }
223 Value::Object(obj)
224}
225
226pub fn hover(content: &str, offset: u32) -> Result<Value, String> {
231 let (doc, index) = parse_document_with_spans(content)?;
232 let Some(el) = index.find_deepest(offset) else {
233 return Ok(json!(null));
234 };
235 let text = hover_text(&doc, &index, el);
236 let span = el.key_span.unwrap_or(el.span);
237 Ok(json!({
238 "contents": { "kind": "markdown", "value": text },
239 "range": range(&span)
240 }))
241}
242
243fn hover_text(doc: &EtlDocument, index: &SpanIndex, el: &IndexedElement) -> String {
244 let tree = el.tree.as_deref().unwrap_or("");
245 match el.kind {
246 ElementKind::Reference => {
247 let field = el.field.as_deref().unwrap_or("");
248 if matches!(field, "message" | "emits" | "channel") {
249 format!(
250 "**AsyncAPI reference** `{}`\n\nField: `{}`",
251 el.name, field
252 )
253 } else {
254 let mut text = format!("**References** `{}`", el.name);
255 if let Some(def) = index.definition(tree, &el.name) {
256 let span = def.key_span.unwrap_or(def.span);
257 text.push_str(&format!(
258 "\n\nDefined at line {}",
259 span.line + 1
260 ));
261 }
262 text
263 }
264 }
265 ElementKind::Definition => {
266 if let Some((detail, description)) = definition_detail(doc, el) {
267 let mut text = format!("**{}**", el.name);
268 if !detail.is_empty() {
269 text.push_str(&format!(" — {}", detail));
270 }
271 if let Some(d) = description {
272 if !d.is_empty() {
273 text.push_str(&format!("\n\n{}", d));
274 }
275 }
276 text
277 } else {
278 format!("**{}**", el.name)
279 }
280 }
281 ElementKind::Field => {
282 let field = el.field.as_deref().unwrap_or("");
283 if !el.name.is_empty() {
284 format!("`{}` = `{}`", field, el.name)
285 } else {
286 format!("`{}`", field)
287 }
288 }
289 ElementKind::Section => format!("**Section** `{}`", el.name),
290 }
291}
292
293fn definition_detail(doc: &EtlDocument, el: &IndexedElement) -> Option<(String, Option<String>)> {
295 let tree = el.tree.as_deref().unwrap_or("");
296 if let Some(node) = doc.event_trees.get(tree).and_then(|t| t.nodes.get(&el.name)) {
297 match node {
298 Node::Barrier(b) => Some((
299 format!("barrier · {} branch(es)", b.branches.len()),
300 b.description.clone(),
301 )),
302 Node::Operation(op) => Some((
303 format!("operation · handler `{}`", op.handler),
304 op.description.clone(),
305 )),
306 Node::Consequence(c) => {
307 let op = match c.consequence_operation {
308 crate::ast::ConsequenceOperation::Send => "send",
309 crate::ast::ConsequenceOperation::Terminate => "terminate",
310 };
311 Some((format!("consequence · `{}`", op), c.description.clone()))
312 }
313 }
314 } else if doc.event_trees.contains_key(tree) && el.field.is_none() {
315 Some((
316 "event tree".to_string(),
317 doc.event_trees[tree].description.clone(),
318 ))
319 } else if let Some(ft) = doc.fault_trees.as_ref().and_then(|f| f.get(tree)) {
320 if let Some(gate) = ft.gates.as_ref().and_then(|g| g.get(&el.name)) {
321 Some((
322 format!("{:?} gate", gate.gate_type),
323 gate.description.clone(),
324 ))
325 } else if let Some(be) = ft.basic_events.get(&el.name) {
326 let detail = if be.failure_rate.is_some() {
327 "basic event (failure rate model)".to_string()
328 } else {
329 "basic event".to_string()
330 };
331 Some((detail, Some(be.description.clone())))
332 } else if el.name == tree {
333 Some(("fault tree".to_string(), ft.description.clone()))
334 } else {
335 None
336 }
337 } else if el.name == tree {
338 Some((
339 "event tree".to_string(),
340 doc.event_trees
341 .get(tree)
342 .and_then(|t| t.description.clone()),
343 ))
344 } else {
345 None
346 }
347}
348
349pub fn goto_definition(content: &str, offset: u32) -> Result<Value, String> {
354 let (_doc, index) = parse_document_with_spans(content)?;
355 let Some(el) = index.find_deepest(offset) else {
356 return Ok(json!(null));
357 };
358 match el.kind {
359 ElementKind::Reference => {
360 let field = el.field.as_deref().unwrap_or("");
361 let tree = el.tree.as_deref().unwrap_or("");
362 if matches!(field, "message" | "emits" | "channel") {
363 return Ok(json!(null)); }
365 if matches!(
367 field,
368 "on_failure_probability_source" | "probability_source"
369 ) {
370 let ft_id = el
371 .name
372 .trim_start_matches("#/faultTrees/")
373 .split('/')
374 .next()
375 .unwrap_or_default();
376 if let Some(def) = index.definition(ft_id, ft_id) {
377 return Ok(location(def));
378 }
379 return Ok(json!(null));
380 }
381 if let Some(def) = index.definition(tree, &el.name) {
382 return Ok(location(def));
383 }
384 Ok(json!(null))
385 }
386 ElementKind::Definition => Ok(location(el)),
387 _ => Ok(json!(null)),
388 }
389}
390
391pub fn find_references(content: &str, offset: u32) -> Result<Value, String> {
392 let (_doc, index) = parse_document_with_spans(content)?;
393 let Some(el) = index.find_deepest(offset) else {
394 return Ok(json!([]));
395 };
396 let (tree, id) = match el.kind {
397 ElementKind::Reference | ElementKind::Definition => {
398 (el.tree.as_deref().unwrap_or("").to_string(), el.name.clone())
399 }
400 _ => return Ok(json!([])),
401 };
402 let locations: Vec<Value> = index
403 .by_identity(&tree, &id)
404 .iter()
405 .map(|r| location(r))
406 .collect();
407 Ok(Value::Array(locations))
408}
409
410pub fn complete(content: &str, offset: u32) -> Result<Value, String> {
415 let (doc, index) = parse_document_with_spans(content)?;
416 let items = completion_items(&doc, &index, content, offset);
417 Ok(json!({ "isIncomplete": false, "items": items }))
418}
419
420fn completion_items(
421 doc: &EtlDocument,
422 index: &SpanIndex,
423 content: &str,
424 offset: u32,
425) -> Vec<Value> {
426 let byte = char_to_byte(content, offset);
427 let line_start = content[..byte].rfind('\n').map(|i| i + 1).unwrap_or(0);
428 let line = &content[line_start..byte];
429
430 let is_value_position = line.contains(':');
431 let field = if is_value_position {
432 line.split(':').next().unwrap_or("").trim().to_string()
433 } else {
434 String::new()
435 };
436 let prefix = if is_value_position {
437 line.split(':').nth(1).unwrap_or("").trim()
438 } else {
439 line.trim()
440 };
441
442 let map_path = enclosing_map_path(index, offset);
443 let mut items: Vec<Value> = Vec::new();
444
445 if is_value_position {
446 for (label, kind, detail) in value_completions(doc, &map_path, &field) {
447 if label.starts_with(prefix) {
448 items.push(completion_item(&label, kind, detail));
449 }
450 }
451 } else if let Some(path) = map_path {
452 for (label, kind) in key_completions(doc, &path) {
453 if label.starts_with(prefix) {
454 items.push(completion_item(&label, kind, None));
455 }
456 }
457 }
458
459 items
460}
461
462fn enclosing_map_path(index: &SpanIndex, offset: u32) -> Option<PathKey> {
463 let el = index.find_deepest(offset)?;
464 match el.kind {
465 ElementKind::Definition => Some(el.path.clone()),
466 _ => {
467 let mut p = el.path.clone();
468 p.pop();
469 Some(p)
470 }
471 }
472}
473
474fn completion_item(label: &str, kind: u32, detail: Option<String>) -> Value {
475 json!({ "label": label, "kind": kind, "detail": detail })
476}
477
478fn key_completions(doc: &EtlDocument, path: &PathKey) -> Vec<(String, u32)> {
480 let last = path.last().map(|p| match p {
481 PathPart::Key(k) => k.as_str(),
482 PathPart::Index(_) => "",
483 });
484
485 let field = |s: &'static str| (s.to_string(), COMPLETION_FIELD);
486 let node = |s: &'static str| (s.to_string(), COMPLETION_KEYWORD);
487
488 match last {
489 None => vec![
490 node("etdl"),
491 node("info"),
492 node("asyncapi_imports"),
493 node("components"),
494 node("eventTrees"),
495 node("faultTrees"),
496 ],
497 Some("info") => vec![
498 field("title"),
499 field("version"),
500 field("domain"),
501 field("description"),
502 ],
503 Some("asyncapi_imports") => vec![],
504 Some("event_trees") => vec![],
505 Some("fault_trees") => vec![],
506 Some("initiating_event") => vec![field("id"), field("message"), field("next")],
507 Some("top_event") => vec![
508 field("id"),
509 field("description"),
510 field("message"),
511 field("rootCause"),
512 ],
513 Some("nodes") => vec![],
514 Some("gates") => vec![],
515 Some("basic_events") => vec![],
516 Some("transfers") => vec![],
517 Some("branches") => vec![
518 field("outcome"),
519 field("condition"),
520 field("probability"),
521 field("probabilityOfSuccess"),
522 field("probabilityOfFailure"),
523 field("probabilitySource"),
524 field("next"),
525 field("description"),
526 ],
527 Some("components") => vec![
528 node("barriers"),
529 node("operations"),
530 node("gates"),
531 node("basicEvents"),
532 ],
533 _ => {
534 if let Some((section, tree, sub)) = tree_sub_context(path) {
536 match sub {
537 TreeSub::Node(id) => node_field_keys(doc, section, &tree, &id),
538 TreeSub::Gate => vec![
539 node("type"),
540 field("inputs"),
541 field("k"),
542 field("inhibitCondition"),
543 field("description"),
544 ],
545 TreeSub::BasicEvent => vec![
546 field("description"),
547 field("probability"),
548 field("failureRate"),
549 field("missionTime"),
550 field("undeveloped"),
551 field("eventType"),
552 field("message"),
553 ],
554 TreeSub::None => {
555 if section == "event_trees" {
556 vec![
557 node("initiatingEvent"),
558 node("nodes"),
559 field("description"),
560 ]
561 } else {
562 vec![
563 node("topEvent"),
564 node("gates"),
565 node("basicEvents"),
566 node("transfers"),
567 field("description"),
568 ]
569 }
570 }
571 }
572 } else {
573 vec![]
574 }
575 }
576 }
577 .into_iter()
578 .collect()
579}
580
581enum TreeSub {
582 None,
583 Node(String),
584 Gate,
585 BasicEvent,
586}
587
588fn tree_sub_context(path: &PathKey) -> Option<(&str, String, TreeSub)> {
590 let section_idx = path
591 .iter()
592 .position(|p| matches!(p, PathPart::Key(k) if k == "event_trees" || k == "fault_trees"))?;
593 let PathPart::Key(section) = &path[section_idx] else { return None };
594 let PathPart::Key(tree) = &path[section_idx + 1] else { return None };
595
596 let after = &path[section_idx + 2..];
597 match after {
598 [] => Some((section, tree.clone(), TreeSub::None)),
599 [PathPart::Key(k)] => match k.as_str() {
600 "nodes" => Some((section, tree.clone(), TreeSub::Node(String::new()))),
601 "gates" => Some((section, tree.clone(), TreeSub::Gate)),
602 "basic_events" => Some((section, tree.clone(), TreeSub::BasicEvent)),
603 _ => Some((section, tree.clone(), TreeSub::None)),
604 },
605 [PathPart::Key(k), PathPart::Key(id)] => match k.as_str() {
606 "nodes" => Some((section, tree.clone(), TreeSub::Node(id.clone()))),
607 "gates" => Some((section, tree.clone(), TreeSub::Gate)),
608 "basic_events" => Some((section, tree.clone(), TreeSub::BasicEvent)),
609 _ => Some((section, tree.clone(), TreeSub::None)),
610 },
611 _ => Some((section, tree.clone(), TreeSub::None)),
612 }
613}
614
615fn node_field_keys(doc: &EtlDocument, section: &str, tree: &str, id: &str) -> Vec<(String, u32)> {
616 let field = |s: &'static str| (s.to_string(), COMPLETION_FIELD);
617 let kw = |s: &'static str| (s.to_string(), COMPLETION_KEYWORD);
618 let tree_map = if section == "event_trees" {
619 doc.event_trees.get(tree)
620 } else {
621 None
622 };
623 if let Some(node) = tree_map.and_then(|t| t.nodes.get(id)) {
624 match node {
625 Node::Barrier(_) => vec![kw("type"), kw("branches"), field("description")],
626 Node::Operation(_) => vec![
627 kw("type"),
628 kw("action"),
629 field("handler"),
630 field("emits"),
631 field("next"),
632 field("onFailure"),
633 field("onFailureProbabilitySource"),
634 kw("retryPolicy"),
635 field("timeoutMs"),
636 field("description"),
637 ],
638 Node::Consequence(_) => vec![
639 kw("type"),
640 kw("operation"),
641 field("channel"),
642 field("message"),
643 field("description"),
644 ],
645 }
646 } else if id.is_empty() {
647 vec![kw("type")]
649 } else {
650 vec![]
651 }
652}
653
654fn value_completions(
656 doc: &EtlDocument,
657 map_path: &Option<PathKey>,
658 field: &str,
659) -> Vec<(String, u32, Option<String>)> {
660 let mut out = Vec::new();
661 let Some(path) = map_path else { return out };
662
663 let tree = tree_from_path(path);
664 let section = tree.as_ref().map(|(s, _)| *s).unwrap_or("");
665
666 let mut push = |label: String, detail: Option<String>| {
667 out.push((label, COMPLETION_REFERENCE, detail));
668 };
669
670 match field {
671 "type" => {
672 if section == "fault_trees" {
673 for t in ["AND", "OR", "NOT", "XOR", "VOTING", "INHIBIT", "PRIORITY_AND"] {
674 push(t.to_string(), Some("gate type".to_string()));
675 }
676 } else {
677 for t in ["barrier", "operation", "consequence"] {
678 push(t.to_string(), Some("node type".to_string()));
679 }
680 }
681 }
682 "operation" => {
683 for t in ["send", "terminate"] {
684 push(t.to_string(), Some("consequence operation".to_string()));
685 }
686 }
687 "action" => push("execute".to_string(), Some("operation action".to_string())),
688 "eventType" => {
689 for t in ["basic", "house", "undeveloped", "conditional"] {
690 push(t.to_string(), Some("basic event type".to_string()));
691 }
692 }
693 "backoffStrategy" => {
694 for t in ["fixed", "exponential"] {
695 push(t.to_string(), Some("backoff strategy".to_string()));
696 }
697 }
698 "condition" => push("default".to_string(), Some("default branch condition".to_string())),
699 "next" | "onFailure" => {
700 if let Some((_, tree_name)) = tree {
701 if let Some(t) = doc.event_trees.get(&tree_name) {
702 for nid in t.nodes.keys() {
703 push(nid.clone(), Some("node".to_string()));
704 }
705 }
706 }
707 }
708 "inputs" | "rootCause" => {
709 if let Some((_, tree_name)) = tree {
710 if let Some(ft) = doc.fault_trees.as_ref().and_then(|f| f.get(&tree_name)) {
711 if let Some(gates) = &ft.gates {
712 for gid in gates.keys() {
713 push(gid.clone(), Some("gate".to_string()));
714 }
715 }
716 for be_id in ft.basic_events.keys() {
717 push(be_id.clone(), Some("basic event".to_string()));
718 }
719 }
720 }
721 }
722 "message" | "emits" | "channel" => {
723 for alias in doc.asyncapi_imports.keys() {
724 push(
725 format!("{}#/", alias),
726 Some(format!("import alias `{}`", alias)),
727 );
728 }
729 }
730 "onFailureProbabilitySource" | "probabilitySource" => {
731 if let Some(ftrees) = &doc.fault_trees {
732 for ft_id in ftrees.keys() {
733 push(
734 format!("#/faultTrees/{}/topEvent", ft_id),
735 Some("fault tree top event".to_string()),
736 );
737 }
738 }
739 }
740 "target" => {
741 if let Some(ftrees) = &doc.fault_trees {
742 for ft_id in ftrees.keys() {
743 push(
744 format!("#/faultTrees/{}/topEvent", ft_id),
745 Some("fault tree top event".to_string()),
746 );
747 }
748 }
749 }
750 _ => {}
751 }
752 out
753}
754
755fn tree_from_path(path: &PathKey) -> Option<(&str, String)> {
756 for (i, p) in path.iter().enumerate() {
757 if let PathPart::Key(k) = p {
758 if k == "event_trees" || k == "fault_trees" {
759 if let Some(PathPart::Key(t)) = path.get(i + 1) {
760 return Some((k.as_str(), t.clone()));
761 }
762 }
763 }
764 }
765 None
766}
767
768pub fn format(content: &str) -> Result<Value, String> {
773 use saphyr::{LoadableYamlNode, Yaml, YamlEmitter};
774
775 let docs = Yaml::load_from_str(content).map_err(|e| e.to_string())?;
776 let doc = docs.first().ok_or("empty ETDL document")?;
777 let mut out = String::new();
778 let mut emitter = YamlEmitter::new(&mut out);
779 emitter.dump(doc).map_err(|e| e.to_string())?;
780 Ok(json!({ "text": out }))
781}
782
783#[cfg(test)]
784mod tests {
785 use super::*;
786
787 const FIXTURE: &str = include_str!("../tests/fixtures/order-fulfillment.etdl");
788
789 fn char_offset(content: &str, line: usize, col: usize) -> u32 {
790 let line_start = content
791 .lines()
792 .take(line)
793 .map(|l| l.len() + 1)
794 .sum::<usize>();
795 (line_start + col) as u32
796 }
797
798 #[test]
799 fn goto_definition_follows_references() {
800 let offset = char_offset(FIXTURE, 39, 15);
802 let loc = goto_definition(FIXTURE, offset).unwrap();
803 assert!(!loc.is_null(), "expected a definition location");
804 let range = &loc["range"];
805 assert_eq!(range["start"]["line"], 48, "target is the node definition line");
806 assert_eq!(range["start"]["character"], 6);
807 }
808
809 #[test]
810 fn goto_definition_null_for_external_ref() {
811 let offset = char_offset(FIXTURE, 16, 12);
813 let loc = goto_definition(FIXTURE, offset).unwrap();
814 assert!(loc.is_null(), "asyncapi refs have no local definition");
815 }
816
817 #[test]
818 fn find_references_returns_all() {
819 let offset = char_offset(FIXTURE, 49, 7);
821 let refs = find_references(FIXTURE, offset).unwrap();
822 let array = refs.as_array().expect("array of locations");
823 assert!(array.len() >= 2, "definition plus at least one reference: {:?}", array);
824 }
825
826 #[test]
827 fn hover_renders_markdown() {
828 let offset = char_offset(FIXTURE, 39, 15);
829 let hover = hover(FIXTURE, offset).unwrap();
830 let value = hover["contents"]["value"].as_str().unwrap();
831 assert!(value.contains("FulfillmentConsequence"));
832 assert!(hover.get("range").is_some());
833 }
834
835 #[test]
836 fn document_symbols_has_trees_and_nodes() {
837 let symbols = document_symbols(FIXTURE).unwrap();
838 let event = &symbols["symbols"][0];
839 assert_eq!(event["name"], "eventTrees");
840 let tree = &event["children"][0];
841 assert_eq!(tree["name"], "OrderFulfillment");
842 let names: Vec<&str> = tree["children"][1]["children"]
843 .as_array()
844 .unwrap()
845 .iter()
846 .map(|s| s["name"].as_str().unwrap())
847 .collect();
848 assert!(names.contains(&"InventoryCheckBarrier"));
849 }
850
851 #[test]
852 fn complete_suggests_node_fields() {
853 let offset = char_offset(FIXTURE, 21, 30);
855 let result = complete(FIXTURE, offset).unwrap();
856 let items = result["items"].as_array().unwrap();
857 let labels: Vec<&str> = items.iter().map(|i| i["label"].as_str().unwrap()).collect();
858 assert!(labels.contains(&"branches"));
859 assert!(labels.contains(&"type"));
860 }
861
862 #[test]
863 fn complete_suggests_next_values() {
864 let line = " next: ";
866 let byte_start = FIXTURE
867 .lines()
868 .take(39)
869 .map(|l| l.len() + 1)
870 .sum::<usize>();
871 let offset = (byte_start + line.find(':').unwrap() + 1) as u32;
872 let result = complete(FIXTURE, offset).unwrap();
873 let items = result["items"].as_array().unwrap();
874 let labels: Vec<&str> = items.iter().map(|i| i["label"].as_str().unwrap()).collect();
875 assert!(labels.contains(&"FulfillmentConsequence"));
876 assert!(labels.contains(&"PaymentFailedConsequence"));
877 }
878
879 #[test]
880 fn format_round_trips() {
881 let result = format(FIXTURE).unwrap();
882 let text = result["text"].as_str().unwrap();
883 assert!(text.contains("eventTrees:"));
884 assert!(text.contains("OrderFulfillment:"));
885 }
886}