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