1use std::collections::HashMap;
36use std::path::{Path, PathBuf};
37use std::sync::Arc;
38
39use bynk_check::index::{ProjectIndex, SymbolKey, SymbolKind};
40use bynk_check::wire::{
41 self, BaseGuard, Expected, JsonKind, Provenance, Revalidation, UncheckedReason, WireBody,
42 WireField, WireInst, WireModel, WireRef, WireScalar, WireSum, WireType,
43};
44use bynk_ide::wire_contract::{
45 BoundaryKind, Envelope, HttpResponse, NoCrossContextReason, ResponseOrigin, WireContractModel,
46};
47use bynk_syntax::ast::{PredKind, TypeDecl};
48use tower_lsp::lsp_types::Range;
49
50pub use crate::architecture_request::WireLoc as WcLoc;
51
52#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
61#[serde(rename_all = "camelCase")]
62pub struct WireContractParams {
63 pub text_document: tower_lsp::lsp_types::TextDocumentIdentifier,
64 pub position: tower_lsp::lsp_types::Position,
65}
66
67#[derive(Debug, Clone, serde::Serialize)]
75pub struct WcModel {
76 pub unit: String,
77 pub service: String,
78 pub kind: WcBoundaryKind,
79 pub range: Range,
83 #[serde(rename = "handlerLine")]
84 pub handler_line: usize,
85 pub envelope: WcEnvelope,
86 pub contract: Option<WcContractForm>,
88 pub types: Vec<WcType>,
89 pub instantiations: Vec<WcInst>,
90 pub recursive: Vec<String>,
91 pub responses: Vec<WcHttpResponse>,
93 #[serde(rename = "noCrossContext")]
94 pub no_cross_context: Option<&'static str>,
95}
96
97#[derive(Debug, Clone, serde::Serialize)]
98#[serde(tag = "kind")]
99pub enum WcBoundaryKind {
100 Http { method: &'static str, path: String },
101 Call,
102 Cron { expr: String },
103 Message,
104 Open,
105 Close,
106 Event,
107 Rejected,
108}
109
110#[derive(Debug, Clone, serde::Serialize)]
111#[serde(tag = "kind")]
112pub enum WcEnvelope {
113 Empty,
114 Bare {
115 param: String,
116 shape: WcRef,
117 },
118 Keyed {
119 params: Vec<WcParam>,
120 },
121 Raw {
122 param: String,
123 #[serde(rename = "contentType")]
124 content_type: String,
125 },
126}
127
128#[derive(Debug, Clone, serde::Serialize)]
129pub struct WcParam {
130 pub name: String,
131 pub shape: WcRef,
132}
133
134#[derive(Debug, Clone, serde::Serialize)]
135pub struct WcContractForm {
136 #[serde(rename = "normalForm")]
137 pub normal_form: String,
138 pub hash: String,
139}
140
141#[derive(Debug, Clone, serde::Serialize)]
142pub struct WcHttpResponse {
143 pub status: u16,
144 pub variant: String,
145 pub origin: WcResponseOrigin,
146}
147
148#[derive(Debug, Clone, serde::Serialize)]
149#[serde(tag = "kind")]
150pub enum WcResponseOrigin {
151 DeclaredSuccess,
152 Constructed { range: Range },
153 BoundaryImplicit { why: &'static str },
154}
155
156#[derive(Debug, Clone, serde::Serialize)]
157pub struct WcType {
158 pub name: String,
159 #[serde(rename = "codecSuffix")]
160 pub codec_suffix: String,
161 pub provenance: WcProvenance,
162 pub body: WcBody,
163 pub loc: Option<WcLoc>,
166}
167
168#[derive(Debug, Clone, serde::Serialize)]
169#[serde(tag = "kind")]
170pub enum WcProvenance {
171 Owned,
172 Consumed {
173 #[serde(rename = "ownerUnit")]
174 owner_unit: String,
175 },
176}
177
178#[derive(Debug, Clone, serde::Serialize)]
179#[serde(tag = "kind")]
180pub enum WcBody {
181 Scalar {
182 base: &'static str,
183 json: &'static str,
184 opaque: bool,
185 predicates: Vec<String>,
188 #[serde(rename = "baseGuards")]
189 base_guards: Vec<&'static str>,
190 revalidation: &'static str,
191 },
192 Record {
193 fields: Vec<WcField>,
194 },
195 Sum {
196 #[serde(rename = "wireDiscriminant")]
197 wire_discriminant: &'static str,
198 #[serde(rename = "memoryDiscriminant")]
199 memory_discriminant: &'static str,
200 variants: Vec<WcVariant>,
201 },
202}
203
204#[derive(Debug, Clone, serde::Serialize)]
205pub struct WcField {
206 pub name: String,
207 pub shape: WcRef,
208 #[serde(rename = "pathSegment")]
209 pub path_segment: String,
210 pub default: Option<String>,
213}
214
215#[derive(Debug, Clone, serde::Serialize)]
216pub struct WcVariant {
217 pub name: String,
218 pub payload: Vec<WcField>,
219}
220
221#[derive(Debug, Clone, serde::Serialize)]
222#[serde(tag = "kind")]
223pub enum WcRef {
224 Base {
225 base: &'static str,
226 json: &'static str,
227 guards: Vec<&'static str>,
228 expected: WcExpected,
229 },
230 Bytes,
231 Named {
232 name: String,
233 },
234 Inst {
235 key: String,
236 },
237 Unit,
238 Unchecked {
239 reason: &'static str,
240 },
241}
242
243#[derive(Debug, Clone, serde::Serialize)]
244#[serde(tag = "kind")]
245pub enum WcExpected {
246 Json { json: &'static str },
247 Integer,
248 FiniteNumber,
249 Base64String,
250 SumVariantKind,
251}
252
253#[derive(Debug, Clone, serde::Serialize)]
254#[serde(tag = "kind")]
255pub enum WcInst {
256 ResultInst {
257 key: String,
258 ok: WcRef,
259 err: WcRef,
260 },
261 OptionInst {
262 key: String,
263 inner: WcRef,
264 },
265 ListInst {
266 key: String,
267 elem: WcRef,
268 },
269 MapInst {
270 key: String,
271 #[serde(rename = "mapKey")]
272 map_key: WcRef,
273 val: WcRef,
274 },
275 RecordInst {
276 key: String,
277 name: String,
278 args: Vec<WcRef>,
279 },
280 SumInst {
281 key: String,
282 name: String,
283 args: Vec<WcRef>,
284 },
285}
286
287fn json_kind_str(j: JsonKind) -> &'static str {
292 match j {
293 JsonKind::Number => "Number",
294 JsonKind::String => "String",
295 JsonKind::Boolean => "Boolean",
296 JsonKind::Object => "Object",
297 JsonKind::Array => "Array",
298 JsonKind::Null => "Null",
299 }
300}
301
302fn base_guard_str(g: BaseGuard) -> &'static str {
303 match g {
304 BaseGuard::Integral => "Integral",
305 BaseGuard::Finite => "Finite",
306 }
307}
308
309fn unchecked_reason_str(r: UncheckedReason) -> &'static str {
310 match r {
311 UncheckedReason::Effect => "Effect",
312 UncheckedReason::ValidationError => "ValidationError",
313 UncheckedReason::JsonError => "JsonError",
314 UncheckedReason::HttpResult => "HttpResult",
315 UncheckedReason::QueueResult => "QueueResult",
316 UncheckedReason::Asset => "Asset",
317 }
318}
319
320fn revalidation_str(r: Revalidation) -> &'static str {
321 match r {
322 Revalidation::ViaConstructor => "ViaConstructor",
323 Revalidation::Inline => "Inline",
324 Revalidation::StructuralOnly => "StructuralOnly",
325 Revalidation::Base64Decode => "Base64Decode",
326 }
327}
328
329fn no_cross_context_str(r: NoCrossContextReason) -> &'static str {
330 match r {
331 NoCrossContextReason::NotACallHandler => "NotACallHandler",
332 NoCrossContextReason::SingleContext => "SingleContext",
333 }
334}
335
336fn pred_kind_str(p: &PredKind) -> String {
345 match p {
346 PredKind::Matches(re) => format!("Matches(\"{}\")", bynk_fmt::escape_string(re)),
347 PredKind::InRange(a, b) => format!("InRange({}, {})", a.value, b.value),
348 PredKind::InRangeF(a, b) => format!("InRange({}, {})", a.lexeme, b.lexeme),
349 PredKind::MinLength(n) => format!("MinLength({n})"),
350 PredKind::MaxLength(n) => format!("MaxLength({n})"),
351 PredKind::Length(n) => format!("Length({n})"),
352 PredKind::NonNegative => "NonNegative".to_string(),
353 PredKind::Positive => "Positive".to_string(),
354 PredKind::NonEmpty => "NonEmpty".to_string(),
355 }
356}
357
358fn provenance_wire(p: &Provenance) -> WcProvenance {
359 match p {
360 Provenance::Owned => WcProvenance::Owned,
361 Provenance::Consumed { owner_unit } => WcProvenance::Consumed {
362 owner_unit: owner_unit.clone(),
363 },
364 }
365}
366
367fn expected_wire(e: &Expected) -> WcExpected {
368 match e {
369 Expected::Json(j) => WcExpected::Json {
370 json: json_kind_str(*j),
371 },
372 Expected::Integer => WcExpected::Integer,
373 Expected::FiniteNumber => WcExpected::FiniteNumber,
374 Expected::Base64String => WcExpected::Base64String,
375 Expected::SumVariantKind => WcExpected::SumVariantKind,
376 }
377}
378
379fn wc_ref(r: &WireRef) -> WcRef {
380 match r {
381 WireRef::Base {
382 base,
383 json,
384 guards,
385 expected,
386 } => WcRef::Base {
387 base: base.name(),
388 json: json_kind_str(*json),
389 guards: guards.iter().map(|g| base_guard_str(*g)).collect(),
390 expected: expected_wire(expected),
391 },
392 WireRef::Bytes => WcRef::Bytes,
393 WireRef::Named { name } => WcRef::Named {
394 name: bynk_check::symbols::display_type_name(name).to_string(),
395 },
396 WireRef::Inst { key } => WcRef::Inst { key: key.clone() },
397 WireRef::Unit => WcRef::Unit,
398 WireRef::Unchecked { reason } => WcRef::Unchecked {
399 reason: unchecked_reason_str(*reason),
400 },
401 }
402}
403
404fn wc_field(f: &WireField) -> WcField {
405 WcField {
406 name: f.name.clone(),
407 shape: wc_ref(&f.shape),
408 path_segment: f.path_segment.clone(),
409 default: f
410 .default
411 .as_ref()
412 .map(|(e, _ty)| bynk_fmt::expr_to_string(e)),
413 }
414}
415
416fn wc_body(b: &WireBody) -> WcBody {
417 match b {
418 WireBody::Scalar(WireScalar {
419 base,
420 json,
421 opaque,
422 predicates,
423 base_guards,
424 revalidation,
425 }) => WcBody::Scalar {
426 base: base.name(),
427 json: json_kind_str(*json),
428 opaque: *opaque,
429 predicates: predicates.iter().map(pred_kind_str).collect(),
430 base_guards: base_guards.iter().map(|g| base_guard_str(*g)).collect(),
431 revalidation: revalidation_str(*revalidation),
432 },
433 WireBody::Record { fields } => WcBody::Record {
434 fields: fields.iter().map(wc_field).collect(),
435 },
436 WireBody::Sum(WireSum {
437 wire_discriminant,
438 memory_discriminant,
439 variants,
440 }) => WcBody::Sum {
441 wire_discriminant,
442 memory_discriminant,
443 variants: variants
444 .iter()
445 .map(|v| WcVariant {
446 name: v.name.clone(),
447 payload: v.payload.iter().map(wc_field).collect(),
448 })
449 .collect(),
450 },
451 }
452}
453
454fn resolve_type_loc(
461 project_root: &Path,
462 index: &ProjectIndex,
463 snapshots: &HashMap<PathBuf, String>,
464 search_order: &[String],
465 name: &str,
466) -> Option<WcLoc> {
467 for unit in search_order {
468 let key = SymbolKey {
469 unit: unit.clone(),
470 kind: SymbolKind::Type,
471 name: name.to_string(),
472 };
473 let Some(entry) = index.symbols.get(&key) else {
474 continue;
475 };
476 let Some(def) = &entry.def else { continue };
477 let Some(text) = snapshots.get(&def.path) else {
478 continue;
479 };
480 let Ok(uri) = tower_lsp::lsp_types::Url::from_file_path(project_root.join(&def.path))
481 else {
482 continue;
483 };
484 return Some(WcLoc {
485 uri,
486 range: crate::position::span_to_range(text, def.span),
487 });
488 }
489 None
490}
491
492fn wc_type(
493 t: &WireType,
494 project_root: &Path,
495 index: &ProjectIndex,
496 snapshots: &HashMap<PathBuf, String>,
497 search_order: &[String],
498) -> WcType {
499 let name = bynk_check::symbols::display_type_name(&t.name);
501 WcType {
502 name: name.to_string(),
503 codec_suffix: t.codec_suffix.clone(),
504 provenance: provenance_wire(&t.provenance),
505 body: wc_body(&t.body),
506 loc: resolve_type_loc(project_root, index, snapshots, search_order, name),
507 }
508}
509
510fn wc_inst(inst: &WireInst, types: &HashMap<String, Arc<TypeDecl>>) -> WcInst {
519 let key = inst.ts_name();
520 match inst {
521 WireInst::ResultInst { ok, err } => WcInst::ResultInst {
522 key,
523 ok: wc_ref(&wire::wire_ref(ok, types)),
524 err: wc_ref(&wire::wire_ref(err, types)),
525 },
526 WireInst::OptionInst { inner } => WcInst::OptionInst {
527 key,
528 inner: wc_ref(&wire::wire_ref(inner, types)),
529 },
530 WireInst::ListInst { elem } => WcInst::ListInst {
531 key,
532 elem: wc_ref(&wire::wire_ref(elem, types)),
533 },
534 WireInst::MapInst { key: k, val } => WcInst::MapInst {
535 key,
536 map_key: wc_ref(&wire::wire_ref(k, types)),
537 val: wc_ref(&wire::wire_ref(val, types)),
538 },
539 WireInst::RecordInst { name, args } => WcInst::RecordInst {
540 key,
541 name: name.clone(),
542 args: args
543 .iter()
544 .map(|a| wc_ref(&wire::wire_ref(a, types)))
545 .collect(),
546 },
547 WireInst::SumInst { name, args } => WcInst::SumInst {
548 key,
549 name: name.clone(),
550 args: args
551 .iter()
552 .map(|a| wc_ref(&wire::wire_ref(a, types)))
553 .collect(),
554 },
555 }
556}
557
558fn boundary_kind_wire(k: &BoundaryKind) -> WcBoundaryKind {
559 match k {
560 BoundaryKind::Http { method, path } => WcBoundaryKind::Http {
561 method: method.as_str(),
562 path: path.clone(),
563 },
564 BoundaryKind::Call => WcBoundaryKind::Call,
565 BoundaryKind::Cron { expr } => WcBoundaryKind::Cron { expr: expr.clone() },
566 BoundaryKind::Message => WcBoundaryKind::Message,
567 BoundaryKind::Open => WcBoundaryKind::Open,
568 BoundaryKind::Close => WcBoundaryKind::Close,
569 BoundaryKind::Event => WcBoundaryKind::Event,
570 BoundaryKind::Rejected => WcBoundaryKind::Rejected,
571 }
572}
573
574fn envelope_wire(e: &Envelope) -> WcEnvelope {
575 match e {
576 Envelope::Empty => WcEnvelope::Empty,
577 Envelope::Bare { param, shape } => WcEnvelope::Bare {
578 param: param.clone(),
579 shape: wc_ref(shape),
580 },
581 Envelope::Raw {
582 param,
583 content_type,
584 } => WcEnvelope::Raw {
585 param: param.clone(),
586 content_type: content_type.clone(),
587 },
588 Envelope::Keyed { params } => WcEnvelope::Keyed {
589 params: params
590 .iter()
591 .map(|(n, s)| WcParam {
592 name: n.clone(),
593 shape: wc_ref(s),
594 })
595 .collect(),
596 },
597 }
598}
599
600fn wc_response(r: &HttpResponse, text: &str) -> WcHttpResponse {
601 WcHttpResponse {
602 status: r.status,
603 variant: r.variant.clone(),
604 origin: match &r.origin {
605 ResponseOrigin::DeclaredSuccess => WcResponseOrigin::DeclaredSuccess,
606 ResponseOrigin::Constructed { span } => WcResponseOrigin::Constructed {
607 range: crate::position::span_to_range(text, *span),
608 },
609 ResponseOrigin::BoundaryImplicit { why } => WcResponseOrigin::BoundaryImplicit { why },
610 },
611 }
612}
613
614pub fn to_wire(
627 model: &WireContractModel,
628 project_root: &Path,
629 text: &str,
630 types: &HashMap<String, Arc<TypeDecl>>,
631 index: &ProjectIndex,
632 snapshots: &HashMap<PathBuf, String>,
633 search_order: &[String],
634) -> WcModel {
635 let WireModel {
636 types: wtypes,
637 instantiations,
638 recursive,
639 } = &model.boundary;
640 WcModel {
641 unit: model.unit.clone(),
642 service: model.service.clone(),
643 kind: boundary_kind_wire(&model.kind),
644 range: crate::position::span_to_range(text, model.handler_span),
645 handler_line: model.handler_line,
646 envelope: envelope_wire(&model.envelope),
647 contract: model.contract.as_ref().map(|c| WcContractForm {
648 normal_form: bynk_check::symbols::display_type_names_in(&c.normal_form),
651 hash: c.hash.clone(),
652 }),
653 types: wtypes
654 .iter()
655 .map(|t| wc_type(t, project_root, index, snapshots, search_order))
656 .collect(),
657 instantiations: instantiations.iter().map(|i| wc_inst(i, types)).collect(),
658 recursive: recursive.iter().cloned().collect(),
659 responses: model
660 .responses
661 .iter()
662 .map(|r| wc_response(r, text))
663 .collect(),
664 no_cross_context: model.no_cross_context.map(no_cross_context_str),
665 }
666}
667
668#[cfg(test)]
669mod tests {
670 use super::*;
671
672 #[test]
673 fn params_deserialize_from_camel_case_wire_json() {
674 let json = serde_json::json!({
675 "textDocument": { "uri": "file:///a/b.bynk" },
676 "position": { "line": 3, "character": 7 },
677 });
678 let params: WireContractParams =
679 serde_json::from_value(json).expect("camelCase textDocument/position must deserialize");
680 assert_eq!(params.text_document.uri.as_str(), "file:///a/b.bynk");
681 assert_eq!(params.position.line, 3);
682 assert_eq!(params.position.character, 7);
683 }
684
685 #[test]
686 fn pred_kind_str_renders_predicate_arguments_not_just_the_bare_name() {
687 assert_eq!(pred_kind_str(&PredKind::NonEmpty), "NonEmpty");
688 assert_eq!(pred_kind_str(&PredKind::MaxLength(20)), "MaxLength(20)");
689 }
690}