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