1use crate::diag::Diag;
15use crate::spec::*;
16
17pub struct ParseResult {
18 pub spec: Option<Spec>,
19 pub diags: Vec<Diag>,
20}
21
22pub fn parse(source: &str) -> ParseResult {
23 tracing::debug!(bytes = source.len(), "parse::parse");
24 let mut p = Parser::new(source);
25 let spec = p.parse_spec();
26 let diag_count = p.diags.len();
27 let endpoint_count = spec.as_ref().map(|s| s.endpoints.len()).unwrap_or(0);
28 tracing::debug!(endpoints = endpoint_count, diags = diag_count, "parse::parse done");
29 ParseResult {
30 spec,
31 diags: p.diags,
32 }
33}
34
35struct Parser<'a> {
36 lines: Vec<(&'a str, usize)>,
38 cursor: usize,
39 diags: Vec<Diag>,
40}
41
42impl<'a> Parser<'a> {
43 fn new(src: &'a str) -> Self {
44 let mut lines = Vec::new();
45 let mut offset = 0usize;
46 for line in src.split_inclusive('\n') {
47 let trimmed = line.strip_suffix('\n').unwrap_or(line);
48 let trimmed = trimmed.strip_suffix('\r').unwrap_or(trimmed);
49 lines.push((trimmed, offset));
50 offset += line.len();
51 }
52 Self {
53 lines,
54 cursor: 0,
55 diags: Vec::new(),
56 }
57 }
58
59 fn err(&mut self, msg: impl Into<String>, span: Span, label: impl Into<String>) {
60 self.diags.push(Diag::error(msg, span, label));
61 }
62
63 fn peek(&self) -> Option<(&'a str, usize)> {
64 self.lines.get(self.cursor).copied()
65 }
66
67 fn advance(&mut self) -> Option<(&'a str, usize)> {
68 let item = self.peek();
69 if item.is_some() {
70 self.cursor += 1;
71 }
72 item
73 }
74
75 fn skip_blank_and_comments(&mut self) {
76 while let Some((text, _)) = self.peek() {
77 let t = text.trim_start();
78 if t.is_empty() || t.starts_with('#') {
79 self.cursor += 1;
80 } else {
81 break;
82 }
83 }
84 }
85
86 fn parse_spec(&mut self) -> Option<Spec> {
87 let setup_start = self.peek().map(|(_, o)| o).unwrap_or(0);
88 let setup = self.parse_setup(setup_start);
89 let mut endpoints = Vec::new();
90 loop {
91 self.skip_blank_and_comments();
92 if self.peek().is_none() {
93 break;
94 }
95 if let Some(ep) = self.parse_endpoint() {
96 endpoints.push(ep);
97 } else {
98 if self.advance().is_none() {
100 break;
101 }
102 }
103 }
104 Some(Spec { setup, endpoints })
105 }
106
107 fn parse_setup(&mut self, start: usize) -> Setup {
108 let mut setup = Setup {
109 span: start..start,
110 ..Setup::default()
111 };
112 loop {
113 self.skip_blank_and_comments();
114 let Some((text, offset)) = self.peek() else {
115 break;
116 };
117 let trimmed = text.trim_start();
118 let upper_first = trimmed
120 .split_whitespace()
121 .next()
122 .unwrap_or("");
123 if matches!(upper_first, "GET" | "POST" | "PUT" | "DELETE" | "PATCH") {
124 break;
125 }
126 self.cursor += 1;
128 self.parse_setup_directive(&mut setup, text, offset);
129 setup.span.end = offset + text.len();
130 }
131 setup
132 }
133
134 fn parse_setup_directive(&mut self, setup: &mut Setup, text: &str, offset: usize) {
135 let leading_ws = text.len() - text.trim_start().len();
136 let body = text.trim_start();
137 let (key, rest) = split_first_word(body);
138 let key_span = (offset + leading_ws)..(offset + leading_ws + key.len());
139 let rest_offset = offset + leading_ws + key.len();
140 let rest_trim_off = rest.len() - rest.trim_start().len();
141 let value = rest.trim();
142 let value_offset = rest_offset + rest_trim_off;
143 let value_span = value_offset..(value_offset + value.len());
144 match key {
145 "VERSION" => match value.parse::<u32>() {
146 Ok(v) => setup.version = Some(v),
147 Err(_) => self.err("invalid VERSION", value_span, "expected positive integer"),
148 },
149 "BASE" => {
150 if value.is_empty() {
151 self.err("missing BASE value", key_span, "expected a path like /api");
152 } else {
153 let mut v = value.to_string();
154 if !v.starts_with('/') {
155 v.insert(0, '/');
156 }
157 setup.base = Some(v.trim_end_matches('/').to_string());
158 }
159 }
160 "AUTH" => match parse_auth(value, value_offset) {
161 Ok(a) => setup.auth = Some(a),
162 Err(d) => self.diags.push(d),
163 },
164 "JWT_VERIFIER" => match parse_value_source(value, value_offset) {
165 Ok(s) => setup.jwt_verifier = Some(s),
166 Err(d) => self.diags.push(d),
167 },
168 "TOKEN_SECRET" => match parse_value_source(value, value_offset) {
169 Ok(s) => setup.token_secret = Some(s),
170 Err(d) => self.diags.push(d),
171 },
172 "MAX_BODY_SIZE" => match parse_size(value) {
173 Some(n) => setup.max_body_size = Some(n),
174 None => self.err("invalid MAX_BODY_SIZE", value_span, "expected e.g. 1mb, 512kb, 1024"),
175 },
176 "MAX_QUERY_PARAM_SIZE" => match value.parse::<u64>() {
177 Ok(n) => setup.max_query_param_size = Some(n),
178 Err(_) => self.err("invalid MAX_QUERY_PARAM_SIZE", value_span, "expected integer"),
179 },
180 "MAX_HEADER_SIZE" => match value.parse::<u64>() {
181 Ok(n) => setup.max_header_size = Some(n),
182 Err(_) => self.err("invalid MAX_HEADER_SIZE", value_span, "expected integer"),
183 },
184 "TIMEOUT" => match parse_duration_ms(value) {
185 Some(n) => setup.timeout_ms = Some(n),
186 None => self.err("invalid TIMEOUT", value_span, "expected e.g. 30s, 500ms, 1m"),
187 },
188 other => {
189 self.err(
190 format!("unknown setup directive `{}`", other),
191 key_span,
192 "expected one of VERSION, BASE, AUTH, JWT_VERIFIER, TOKEN_SECRET, MAX_BODY_SIZE, MAX_QUERY_PARAM_SIZE, MAX_HEADER_SIZE, TIMEOUT",
193 );
194 }
195 }
196 }
197
198 fn parse_endpoint(&mut self) -> Option<Endpoint> {
199 let (text, offset) = self.advance()?;
200 let trimmed = text.trim_start();
201 let leading = text.len() - trimmed.len();
202 let (method_str, rest) = split_first_word(trimmed);
203 let method = match method_str {
204 "GET" => Method::Get,
205 "POST" => Method::Post,
206 "PUT" => Method::Put,
207 "DELETE" => Method::Delete,
208 "PATCH" => Method::Patch,
209 other => {
210 self.err(
211 format!("expected HTTP method, found `{}`", other),
212 (offset + leading)..(offset + leading + method_str.len()),
213 "expected GET/POST/PUT/DELETE/PATCH",
214 );
215 return None;
216 }
217 };
218 let path_off = offset + leading + method_str.len() + (rest.len() - rest.trim_start().len());
219 let path_str = rest.trim().to_string();
220 let path_span = path_off..(path_off + path_str.len());
221 let path_segments = self.parse_path(&path_str, path_off);
222 let header_span = (offset + leading)..(offset + text.len());
223 let mut endpoint = Endpoint {
224 method,
225 path: path_str,
226 path_segments,
227 response_type: None,
228 query_params: Vec::new(),
229 headers: Vec::new(),
230 vars: Vec::new(),
231 body: None,
232 exec: ExecSpec {
233 raw: String::new(),
234 span: 0..0,
235 pipeline: Vec::new(),
236 },
237 span: header_span,
238 };
239 let _ = path_span;
240
241 loop {
242 self.skip_blank_and_comments();
243 let Some((line_text, line_off)) = self.peek() else {
244 break;
245 };
246 let t = line_text.trim_start();
247 let first_word = t.split_whitespace().next().unwrap_or("");
248 if matches!(first_word, "GET" | "POST" | "PUT" | "DELETE" | "PATCH") {
249 break;
250 }
251 self.cursor += 1;
252 let is_exec = self.parse_endpoint_directive(&mut endpoint, line_text, line_off);
253 endpoint.span.end = line_off + line_text.len();
254 if is_exec {
256 break;
257 }
258 }
259 if endpoint.exec.raw.is_empty() {
260 self.err(
261 "endpoint missing Exec directive",
262 endpoint.span.clone(),
263 "every endpoint requires an `Exec:` line",
264 );
265 }
266 Some(endpoint)
267 }
268
269 fn parse_endpoint_directive(&mut self, ep: &mut Endpoint, text: &str, offset: usize) -> bool {
270 let leading = text.len() - text.trim_start().len();
271 let body = text.trim_start();
272
273 if let Some(rest) = body.strip_prefix("Response-Type") {
276 let rest = rest.trim_start_matches([':', ' ', '\t']);
277 ep.response_type = Some(rest.trim().to_string());
278 return false;
279 }
280 if let Some(rest) = body.strip_prefix("Exec:") {
281 let exec_off = offset + leading + "Exec:".len();
282 let trim_off = rest.len() - rest.trim_start().len();
283 let raw = rest.trim().to_string();
284 let span = (exec_off + trim_off)..(exec_off + trim_off + raw.len());
285 let pipeline = match crate::parse::exec::parse_exec(&raw, span.start) {
286 Ok(p) => p,
287 Err(d) => {
288 self.diags.push(d);
289 Vec::new()
290 }
291 };
292 ep.exec = ExecSpec {
293 raw,
294 span,
295 pipeline,
296 };
297 ep.span.end = offset + text.len();
298 return true;
299 }
300
301 let (key, rest) = split_first_word(body);
302 let key_off = offset + leading;
303 let rest_off = key_off + key.len();
304 let rest_trim_off = rest.len() - rest.trim_start().len();
305 let value = rest.trim();
306 let val_off = rest_off + rest_trim_off;
307
308 match key {
309 "QUERY" => match self.parse_named_field(value, val_off) {
310 Ok(f) => ep.query_params.push(f),
311 Err(d) => self.diags.push(d),
312 },
313 "HEADER" => match self.parse_named_field(value, val_off) {
314 Ok(f) => ep.headers.push(f),
315 Err(d) => self.diags.push(d),
316 },
317 "VAR" => match self.parse_var_def(value, val_off) {
318 Ok(v) => ep.vars.push(v),
319 Err(d) => self.diags.push(d),
320 },
321 "BODY" => self.parse_body(ep, value, val_off),
322 other => self.err(
323 format!("unknown directive `{}`", other),
324 key_off..key_off + key.len(),
325 "expected QUERY, HEADER, VAR, BODY, Response-Type or Exec",
326 ),
327 }
328 false
329 }
330
331 fn parse_path(&mut self, path: &str, offset: usize) -> Vec<PathSegment> {
332 let mut segs = Vec::new();
333 if !path.starts_with('/') {
334 self.err(
335 "path must start with `/`",
336 offset..(offset + path.len()),
337 "add a leading slash",
338 );
339 }
340 for (idx, raw) in path.split('/').enumerate() {
341 if idx == 0 {
342 continue;
343 }
344 let local_off = offset
347 + path
348 .match_indices('/')
349 .nth(idx - 1)
350 .map(|(i, _)| i + 1)
351 .unwrap_or(0);
352 let seg_span = local_off..(local_off + raw.len());
353 if raw.is_empty() {
354 continue;
355 }
356 if let Some(rest) = raw.strip_prefix(':') {
357 let mut parts = rest.splitn(2, ':');
358 let name = parts.next().unwrap_or("").to_string();
359 let ty_str = parts.next().unwrap_or("string");
360 if name.is_empty() {
361 self.err(
362 "empty path parameter name",
363 seg_span.clone(),
364 "use `:name:type`",
365 );
366 continue;
367 }
368 let ty = match parse_type_expr(ty_str, seg_span.end - ty_str.len()) {
369 Ok(t) => t,
370 Err(d) => {
371 self.diags.push(d);
372 TypeExpr::String
373 }
374 };
375 segs.push(PathSegment::Param {
376 name,
377 ty,
378 span: seg_span,
379 });
380 } else {
381 segs.push(PathSegment::Literal(raw.to_string()));
382 }
383 }
384 segs
385 }
386
387 fn parse_named_field(&mut self, value: &str, offset: usize) -> Result<NamedField, Diag> {
388 let head = split_field_head(value, offset)?;
390 let ty = parse_type_expr(head.tail, head.tail_off)?;
391 Ok(NamedField {
392 name: head.name,
393 optional: head.optional,
394 ty,
395 span: offset..(offset + value.len()),
396 })
397 }
398
399 fn parse_var_def(&mut self, value: &str, offset: usize) -> Result<VarDef, Diag> {
400 let (name, rest) = split_first_word(value);
402 if name.is_empty() {
403 return Err(Diag::error(
404 "missing var name",
405 offset..offset + value.len(),
406 "expected `VAR name <source>`",
407 ));
408 }
409 let rest_trim_off = rest.len() - rest.trim_start().len();
410 let src_str = rest.trim();
411 let src_off = offset + name.len() + rest_trim_off;
412 let source = parse_value_source(src_str, src_off)?;
413 Ok(VarDef {
414 name: name.to_string(),
415 source,
416 span: offset..(offset + value.len()),
417 })
418 }
419
420 fn parse_body(&mut self, ep: &mut Endpoint, value: &str, offset: usize) {
421 let (kind, rest) = split_first_word(value);
428 let kind_span = offset..(offset + kind.len());
429 let rest_trim = rest.trim();
430 let opens_block = rest_trim.starts_with('{');
431 match kind {
432 "string" => {
433 if opens_block {
434 self.err("BODY string takes no schema", kind_span.clone(), "");
435 }
436 ep.body = Some(BodySpec::String { span: kind_span });
437 }
438 "binary" => {
439 if opens_block {
440 self.err("BODY binary takes no schema", kind_span.clone(), "");
441 }
442 ep.body = Some(BodySpec::Binary { span: kind_span });
443 }
444 "json" => {
445 if !opens_block {
446 ep.body = Some(BodySpec::Json {
447 schema: None,
448 span: kind_span,
449 });
450 } else {
451 let fields = self.parse_json_block();
452 ep.body = Some(BodySpec::Json {
453 schema: Some(JsonSchema { fields }),
454 span: kind_span,
455 });
456 }
457 }
458 "form" => {
459 if !opens_block {
460 self.err(
461 "BODY form requires `{ ... }` schema",
462 kind_span.clone(),
463 "add a `{` block listing form fields",
464 );
465 ep.body = Some(BodySpec::Form {
466 fields: Vec::new(),
467 span: kind_span,
468 });
469 } else {
470 let fields = self.parse_form_block();
471 ep.body = Some(BodySpec::Form {
472 fields,
473 span: kind_span,
474 });
475 }
476 }
477 other => self.err(
478 format!("unknown body kind `{}`", other),
479 kind_span,
480 "expected one of: string, json, form, binary",
481 ),
482 }
483 }
484
485 fn parse_form_block(&mut self) -> Vec<NamedField> {
486 self.parse_brace_block("BODY form", |this, val, off| this.parse_named_field(val, off))
487 }
488
489 fn parse_json_block(&mut self) -> Vec<JsonField> {
490 self.parse_brace_block("BODY json", |this, val, off| this.parse_json_field(val, off))
491 }
492
493 fn parse_brace_block<T, F>(&mut self, label: &str, mut line_parser: F) -> Vec<T>
498 where
499 F: FnMut(&mut Self, &str, usize) -> Result<T, Diag>,
500 {
501 let mut out = Vec::new();
502 loop {
503 self.skip_blank_and_comments();
504 let Some((text, off)) = self.peek() else {
505 self.err(format!("unterminated {} block", label), 0..0, "missing `}`");
506 break;
507 };
508 let t = text.trim();
509 if t == "}" {
510 self.cursor += 1;
511 break;
512 }
513 self.cursor += 1;
514 let leading = text.len() - text.trim_start().len();
515 let val = t.trim_end_matches(',').trim();
516 match line_parser(self, val, off + leading) {
517 Ok(f) => out.push(f),
518 Err(d) => self.diags.push(d),
519 }
520 }
521 out
522 }
523
524 fn parse_json_field(&mut self, value: &str, offset: usize) -> Result<JsonField, Diag> {
525 let head = split_field_head(value, offset)?;
526 let ty = if let Some(inner) = head.tail.strip_prefix('[').and_then(|s| s.strip_suffix(']'))
527 {
528 JsonFieldType::Array(parse_type_expr(inner.trim(), head.tail_off + 1)?)
529 } else {
530 JsonFieldType::Scalar(parse_type_expr(head.tail, head.tail_off)?)
531 };
532 Ok(JsonField {
533 name: head.name,
534 optional: head.optional,
535 ty,
536 span: offset..(offset + value.len()),
537 })
538 }
539}
540
541struct FieldHead<'a> {
545 name: String,
546 optional: bool,
547 tail: &'a str,
548 tail_off: usize,
549}
550
551fn split_field_head(value: &str, offset: usize) -> Result<FieldHead<'_>, Diag> {
552 let colon_pos = value.find(':').ok_or_else(|| {
553 Diag::error(
554 "missing `:` in field declaration",
555 offset..offset + value.len(),
556 "expected `name: <type>`",
557 )
558 })?;
559 let head = &value[..colon_pos];
560 let after = &value[colon_pos + 1..];
561 let tail = after.trim_start();
562 let tail_off = offset + colon_pos + 1 + (after.len() - tail.len());
563 let (name, optional) = if let Some(stripped) = head.strip_suffix('?') {
564 (stripped.trim().to_string(), true)
565 } else {
566 (head.trim().to_string(), false)
567 };
568 if name.is_empty() {
569 return Err(Diag::error(
570 "empty field name",
571 offset..offset + value.len(),
572 "expected a name before `:`",
573 ));
574 }
575 Ok(FieldHead {
576 name,
577 optional,
578 tail,
579 tail_off,
580 })
581}
582
583fn split_first_word(s: &str) -> (&str, &str) {
584 let s = s.trim_start();
585 let end = s
586 .find(|c: char| c.is_whitespace())
587 .unwrap_or(s.len());
588 (&s[..end], &s[end..])
589}
590
591fn parse_value_source(s: &str, offset: usize) -> Result<ValueSource, Diag> {
592 let s = s.trim();
593 if let Some(inner) = s.strip_prefix('[').and_then(|x| x.strip_suffix(']')) {
594 let inner = inner.trim();
595 let (kind, rest) = split_first_word(inner);
596 let rest = rest.trim();
597 match kind {
598 "ENV" => Ok(ValueSource::Env {
599 name: rest.to_string(),
600 span: offset..offset + s.len(),
601 }),
602 "HEADER" => Ok(ValueSource::Header {
603 name: rest.to_string(),
604 span: offset..offset + s.len(),
605 }),
606 other => Err(Diag::error(
607 format!("unknown value source `{}`", other),
608 offset..offset + s.len(),
609 "expected [ENV NAME] or [HEADER NAME]",
610 )),
611 }
612 } else if !s.is_empty() {
613 Ok(ValueSource::Literal {
614 value: s.to_string(),
615 span: offset..offset + s.len(),
616 })
617 } else {
618 Err(Diag::error(
619 "missing value source",
620 offset..offset,
621 "expected [ENV NAME], [HEADER NAME] or a literal",
622 ))
623 }
624}
625
626fn parse_auth(value: &str, offset: usize) -> Result<AuthSpec, Diag> {
627 let value = value.trim();
628 let (scheme, rest) = split_first_word(value);
629 let rest = rest.trim();
630 if !scheme.eq_ignore_ascii_case("Bearer") {
631 return Err(Diag::error(
632 format!("unsupported auth scheme `{}`", scheme),
633 offset..offset + scheme.len(),
634 "only Bearer is supported",
635 ));
636 }
637 let inner = rest
638 .strip_prefix('[')
639 .and_then(|s| s.strip_suffix(']'))
640 .ok_or_else(|| {
641 Diag::error(
642 "missing `[HEADER name]` after Bearer",
643 offset..offset + value.len(),
644 "expected `AUTH Bearer [HEADER NAME]`",
645 )
646 })?;
647 let (kind, name) = split_first_word(inner.trim());
648 let name = name.trim();
649 if !kind.eq_ignore_ascii_case("HEADER") {
650 return Err(Diag::error(
651 format!("unsupported auth source `{}`", kind),
652 offset..offset + value.len(),
653 "only [HEADER NAME] is supported",
654 ));
655 }
656 if name.is_empty() {
657 return Err(Diag::error(
658 "missing header name",
659 offset..offset + value.len(),
660 "expected `[HEADER NAME]`",
661 ));
662 }
663 Ok(AuthSpec::BearerHeader {
664 header: name.to_string(),
665 span: offset..offset + value.len(),
666 })
667}
668
669fn parse_size(s: &str) -> Option<u64> {
670 parse_suffixed(s, &[("kb", 1024), ("mb", 1024 * 1024), ("gb", 1024 * 1024 * 1024), ("b", 1)], 1)
671}
672
673fn parse_duration_ms(s: &str) -> Option<u64> {
674 parse_suffixed(s, &[("ms", 1), ("s", 1000), ("m", 60_000)], 1000)
675}
676
677fn parse_suffixed(s: &str, suffixes: &[(&str, u64)], default_mult: u64) -> Option<u64> {
681 let s = s.trim().to_ascii_lowercase();
682 let (num, mult) = suffixes
683 .iter()
684 .find_map(|(suf, m)| s.strip_suffix(suf).map(|rest| (rest.trim(), *m)))
685 .unwrap_or((s.as_str(), default_mult));
686 num.trim().parse::<u64>().ok()?.checked_mul(mult)
687}
688
689pub fn parse_type_expr(s: &str, offset: usize) -> Result<TypeExpr, Diag> {
690 let s = s.trim();
691 if s.is_empty() {
692 return Err(Diag::error(
693 "missing type",
694 offset..offset,
695 "expected a type expression",
696 ));
697 }
698 if let Some(stripped) = s.strip_prefix('/') {
700 if let Some(pat) = stripped.strip_suffix('/') {
701 return Ok(TypeExpr::Regex {
702 pattern: pat.to_string(),
703 span: offset..offset + s.len(),
704 });
705 } else {
706 return Err(Diag::error(
707 "unterminated regex",
708 offset..offset + s.len(),
709 "regex must be enclosed in `/.../`",
710 ));
711 }
712 }
713 if let Some(rest) = s.strip_prefix("int(") {
715 if let Some(inner) = rest.strip_suffix(')') {
716 let parts: Vec<&str> = inner.splitn(2, "..").collect();
717 if parts.len() == 2 {
718 if let (Ok(a), Ok(b)) = (parts[0].trim().parse::<i64>(), parts[1].trim().parse::<i64>()) {
719 return Ok(TypeExpr::IntRange {
720 min: a,
721 max: b,
722 span: offset..offset + s.len(),
723 });
724 }
725 }
726 return Err(Diag::error(
727 "invalid int range",
728 offset..offset + s.len(),
729 "expected `int(a..b)`",
730 ));
731 }
732 }
733 if let Some(rest) = s.strip_prefix("float(") {
734 if let Some(inner) = rest.strip_suffix(')') {
735 let parts: Vec<&str> = inner.splitn(2, "..").collect();
736 if parts.len() == 2 {
737 if let (Ok(a), Ok(b)) = (parts[0].trim().parse::<f64>(), parts[1].trim().parse::<f64>()) {
738 return Ok(TypeExpr::FloatRange {
739 min: a,
740 max: b,
741 span: offset..offset + s.len(),
742 });
743 }
744 }
745 return Err(Diag::error(
746 "invalid float range",
747 offset..offset + s.len(),
748 "expected `float(a..b)`",
749 ));
750 }
751 }
752 match s {
753 "int" => Ok(TypeExpr::Int),
754 "float" => Ok(TypeExpr::Float),
755 "boolean" | "bool" => Ok(TypeExpr::Boolean),
756 "uuid" => Ok(TypeExpr::Uuid),
757 "string" => Ok(TypeExpr::String),
758 "json" => Ok(TypeExpr::Json),
759 "binary" => Ok(TypeExpr::Binary),
760 _ if s.contains('|') => {
761 let variants: Vec<String> = s
762 .split('|')
763 .map(|v| v.trim().to_string())
764 .filter(|v| !v.is_empty())
765 .collect();
766 if variants.is_empty() {
767 Err(Diag::error(
768 "empty union",
769 offset..offset + s.len(),
770 "expected at least one variant",
771 ))
772 } else {
773 Ok(TypeExpr::Union {
774 variants,
775 span: offset..offset + s.len(),
776 })
777 }
778 }
779 other => Err(Diag::error(
780 format!("unknown type `{}`", other),
781 offset..offset + s.len(),
782 "expected int, float, boolean, uuid, string, json, binary, a range, union or regex",
783 )),
784 }
785}