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 response_stream: false,
242 query_params: Vec::new(),
243 headers: Vec::new(),
244 vars: Vec::new(),
245 body: None,
246 exec: ExecSpec {
247 raw: String::new(),
248 span: 0..0,
249 statements: Vec::new(),
250 },
251 span: header_span,
252 };
253 let _ = path_span;
254
255 loop {
256 self.skip_blank_and_comments();
257 let Some((line_text, line_off)) = self.peek() else {
258 break;
259 };
260 let t = line_text.trim_start();
261 let first_word = t.split_whitespace().next().unwrap_or("");
262 if matches!(first_word, "GET" | "POST" | "PUT" | "DELETE" | "PATCH") {
263 break;
264 }
265 self.cursor += 1;
266 let is_exec = self.parse_endpoint_directive(&mut endpoint, line_text, line_off);
267 endpoint.span.end = line_off + line_text.len();
268 if is_exec {
270 break;
271 }
272 }
273 if endpoint.exec.statements.is_empty() {
274 self.err(
275 "endpoint missing Exec directive",
276 endpoint.span.clone(),
277 "every endpoint requires an `Exec:` line",
278 );
279 }
280 Some(endpoint)
281 }
282
283 fn parse_endpoint_directive(&mut self, ep: &mut Endpoint, text: &str, offset: usize) -> bool {
284 let leading = text.len() - text.trim_start().len();
285 let body = text.trim_start();
286
287 if let Some(rest) = body.strip_prefix("Response-Type") {
290 let rest = rest.trim_start_matches([':', ' ', '\t']);
291 let trimmed = rest.trim();
292 let (stream, ty) = if let Some(after) = trimmed.strip_prefix("stream") {
293 if after.is_empty() || after.starts_with(|c: char| c.is_whitespace()) {
294 (true, after.trim().to_string())
295 } else {
296 (false, trimmed.to_string())
297 }
298 } else {
299 (false, trimmed.to_string())
300 };
301 ep.response_stream = stream;
302 ep.response_type = Some(ty);
303 return false;
304 }
305 if let Some(rest) = body.strip_prefix("Exec:") {
306 let exec_off = offset + leading + "Exec:".len();
307 let trim_off = rest.len() - rest.trim_start().len();
308 let value_after_colon = rest.trim_start();
309 if let Some(after_open) = value_after_colon.strip_prefix("<<<") {
311 let trailing = after_open.trim();
312 let opener_span_start = exec_off + trim_off;
313 if !trailing.is_empty() {
314 self.err(
315 "unexpected text after `<<<` on Exec line",
316 opener_span_start..(opener_span_start + value_after_colon.len()),
317 "the multi-line Exec opener must be alone on its line",
318 );
319 }
320 let mut lines: Vec<(String, Span)> = Vec::new();
321 let mut closed = false;
322 let mut end_off = offset + text.len();
323 while let Some((line_text, line_off)) = self.peek() {
324 self.cursor += 1;
325 end_off = line_off + line_text.len();
326 let trimmed_line = line_text.trim();
327 if trimmed_line == ">>>" {
328 closed = true;
329 break;
330 }
331 if trimmed_line.is_empty() || trimmed_line.starts_with('#') {
332 continue;
333 }
334 let lead = line_text.len() - line_text.trim_start().len();
335 let span_start = line_off + lead;
336 let span = span_start..(span_start + trimmed_line.len());
337 lines.push((trimmed_line.to_string(), span));
338 }
339 if !closed {
340 self.err(
341 "unterminated multi-line Exec block",
342 opener_span_start..(opener_span_start + 3),
343 "missing closing `>>>` on its own line",
344 );
345 }
346 let mut statements = Vec::new();
347 let mut raw_parts = Vec::new();
348 for (line, span) in lines {
349 raw_parts.push(line.clone());
350 match crate::parse::exec::parse_exec(&line, span.start) {
351 Ok(stages) => statements.push(stages),
352 Err(d) => self.diags.push(d),
353 }
354 }
355 ep.exec = ExecSpec {
356 raw: raw_parts.join("\n"),
357 span: opener_span_start..end_off,
358 statements,
359 };
360 ep.span.end = end_off;
361 return true;
362 }
363 let raw = rest.trim().to_string();
364 let span = (exec_off + trim_off)..(exec_off + trim_off + raw.len());
365 let statements = match crate::parse::exec::parse_exec(&raw, span.start) {
366 Ok(p) => vec![p],
367 Err(d) => {
368 self.diags.push(d);
369 Vec::new()
370 }
371 };
372 ep.exec = ExecSpec {
373 raw,
374 span,
375 statements,
376 };
377 ep.span.end = offset + text.len();
378 return true;
379 }
380
381 let (key, rest) = split_first_word(body);
382 let key_off = offset + leading;
383 let rest_off = key_off + key.len();
384 let rest_trim_off = rest.len() - rest.trim_start().len();
385 let value = rest.trim();
386 let val_off = rest_off + rest_trim_off;
387
388 match key {
389 "QUERY" => match self.parse_named_field(value, val_off) {
390 Ok(f) => ep.query_params.push(f),
391 Err(d) => self.diags.push(d),
392 },
393 "HEADER" => match self.parse_named_field(value, val_off) {
394 Ok(f) => ep.headers.push(f),
395 Err(d) => self.diags.push(d),
396 },
397 "VAR" => match self.parse_var_def(value, val_off) {
398 Ok(v) => ep.vars.push(v),
399 Err(d) => self.diags.push(d),
400 },
401 "BODY" => self.parse_body(ep, value, val_off),
402 other => self.err(
403 format!("unknown directive `{}`", other),
404 key_off..key_off + key.len(),
405 "expected QUERY, HEADER, VAR, BODY, Response-Type or Exec",
406 ),
407 }
408 false
409 }
410
411 fn parse_path(&mut self, path: &str, offset: usize) -> Vec<PathSegment> {
412 let mut segs = Vec::new();
413 if !path.starts_with('/') {
414 self.err(
415 "path must start with `/`",
416 offset..(offset + path.len()),
417 "add a leading slash",
418 );
419 }
420 for (idx, raw) in path.split('/').enumerate() {
421 if idx == 0 {
422 continue;
423 }
424 let local_off = offset
427 + path
428 .match_indices('/')
429 .nth(idx - 1)
430 .map(|(i, _)| i + 1)
431 .unwrap_or(0);
432 let seg_span = local_off..(local_off + raw.len());
433 if raw.is_empty() {
434 continue;
435 }
436 if let Some(rest) = raw.strip_prefix(':') {
437 let mut parts = rest.splitn(2, ':');
438 let name = parts.next().unwrap_or("").to_string();
439 let ty_str = parts.next().unwrap_or("string");
440 if name.is_empty() {
441 self.err(
442 "empty path parameter name",
443 seg_span.clone(),
444 "use `:name:type`",
445 );
446 continue;
447 }
448 let ty = match parse_type_expr(ty_str, seg_span.end - ty_str.len()) {
449 Ok(t) => t,
450 Err(d) => {
451 self.diags.push(d);
452 TypeExpr::String
453 }
454 };
455 segs.push(PathSegment::Param {
456 name,
457 ty,
458 span: seg_span,
459 });
460 } else {
461 segs.push(PathSegment::Literal(raw.to_string()));
462 }
463 }
464 segs
465 }
466
467 fn parse_named_field(&mut self, value: &str, offset: usize) -> Result<NamedField, Diag> {
468 let head = split_field_head(value, offset)?;
470 let ty = parse_type_expr(head.tail, head.tail_off)?;
471 Ok(NamedField {
472 name: head.name,
473 optional: head.optional,
474 ty,
475 span: offset..(offset + value.len()),
476 })
477 }
478
479 fn parse_var_def(&mut self, value: &str, offset: usize) -> Result<VarDef, Diag> {
480 let (name, rest) = split_first_word(value);
482 if name.is_empty() {
483 return Err(Diag::error(
484 "missing var name",
485 offset..offset + value.len(),
486 "expected `VAR name <source>`",
487 ));
488 }
489 let rest_trim_off = rest.len() - rest.trim_start().len();
490 let src_str = rest.trim();
491 let src_off = offset + name.len() + rest_trim_off;
492 let source = parse_value_source(src_str, src_off)?;
493 Ok(VarDef {
494 name: name.to_string(),
495 source,
496 span: offset..(offset + value.len()),
497 })
498 }
499
500 fn parse_body(&mut self, ep: &mut Endpoint, value: &str, offset: usize) {
501 let (kind, rest) = split_first_word(value);
508 let kind_span = offset..(offset + kind.len());
509 let rest_trim = rest.trim();
510 let opens_block = rest_trim.starts_with('{');
511 match kind {
512 "string" => {
513 if opens_block {
514 self.err("BODY string takes no schema", kind_span.clone(), "");
515 }
516 ep.body = Some(BodySpec::String { span: kind_span });
517 }
518 "binary" => {
519 if opens_block {
520 self.err("BODY binary takes no schema", kind_span.clone(), "");
521 }
522 ep.body = Some(BodySpec::Binary { span: kind_span });
523 }
524 "json" => {
525 if !opens_block {
526 ep.body = Some(BodySpec::Json {
527 schema: None,
528 span: kind_span,
529 });
530 } else {
531 let fields = self.parse_json_block();
532 ep.body = Some(BodySpec::Json {
533 schema: Some(JsonSchema { fields }),
534 span: kind_span,
535 });
536 }
537 }
538 "form" => {
539 if !opens_block {
540 self.err(
541 "BODY form requires `{ ... }` schema",
542 kind_span.clone(),
543 "add a `{` block listing form fields",
544 );
545 ep.body = Some(BodySpec::Form {
546 fields: Vec::new(),
547 span: kind_span,
548 });
549 } else {
550 let fields = self.parse_form_block();
551 ep.body = Some(BodySpec::Form {
552 fields,
553 span: kind_span,
554 });
555 }
556 }
557 other => self.err(
558 format!("unknown body kind `{}`", other),
559 kind_span,
560 "expected one of: string, json, form, binary",
561 ),
562 }
563 }
564
565 fn parse_form_block(&mut self) -> Vec<NamedField> {
566 self.parse_brace_block("BODY form", |this, val, off| {
567 this.parse_named_field(val, off)
568 })
569 }
570
571 fn parse_json_block(&mut self) -> Vec<JsonField> {
572 self.parse_brace_block("BODY json", |this, val, off| {
573 this.parse_json_field(val, off)
574 })
575 }
576
577 fn parse_brace_block<T, F>(&mut self, label: &str, mut line_parser: F) -> Vec<T>
582 where
583 F: FnMut(&mut Self, &str, usize) -> Result<T, Diag>,
584 {
585 let mut out = Vec::new();
586 loop {
587 self.skip_blank_and_comments();
588 let Some((text, off)) = self.peek() else {
589 self.err(format!("unterminated {} block", label), 0..0, "missing `}`");
590 break;
591 };
592 let t = text.trim();
593 if t == "}" {
594 self.cursor += 1;
595 break;
596 }
597 self.cursor += 1;
598 let leading = text.len() - text.trim_start().len();
599 let val = t.trim_end_matches(',').trim();
600 match line_parser(self, val, off + leading) {
601 Ok(f) => out.push(f),
602 Err(d) => self.diags.push(d),
603 }
604 }
605 out
606 }
607
608 fn parse_json_field(&mut self, value: &str, offset: usize) -> Result<JsonField, Diag> {
609 let head = split_field_head(value, offset)?;
610 let ty = if let Some(inner) = head
611 .tail
612 .strip_prefix('[')
613 .and_then(|s| s.strip_suffix(']'))
614 {
615 JsonFieldType::Array(parse_type_expr(inner.trim(), head.tail_off + 1)?)
616 } else {
617 JsonFieldType::Scalar(parse_type_expr(head.tail, head.tail_off)?)
618 };
619 Ok(JsonField {
620 name: head.name,
621 optional: head.optional,
622 ty,
623 span: offset..(offset + value.len()),
624 })
625 }
626}
627
628struct FieldHead<'a> {
632 name: String,
633 optional: bool,
634 tail: &'a str,
635 tail_off: usize,
636}
637
638fn split_field_head(value: &str, offset: usize) -> Result<FieldHead<'_>, Diag> {
639 let colon_pos = value.find(':').ok_or_else(|| {
640 Diag::error(
641 "missing `:` in field declaration",
642 offset..offset + value.len(),
643 "expected `name: <type>`",
644 )
645 })?;
646 let head = &value[..colon_pos];
647 let after = &value[colon_pos + 1..];
648 let tail = after.trim_start();
649 let tail_off = offset + colon_pos + 1 + (after.len() - tail.len());
650 let (name, optional) = if let Some(stripped) = head.strip_suffix('?') {
651 (stripped.trim().to_string(), true)
652 } else {
653 (head.trim().to_string(), false)
654 };
655 if name.is_empty() {
656 return Err(Diag::error(
657 "empty field name",
658 offset..offset + value.len(),
659 "expected a name before `:`",
660 ));
661 }
662 Ok(FieldHead {
663 name,
664 optional,
665 tail,
666 tail_off,
667 })
668}
669
670fn split_first_word(s: &str) -> (&str, &str) {
671 let s = s.trim_start();
672 let end = s.find(|c: char| c.is_whitespace()).unwrap_or(s.len());
673 (&s[..end], &s[end..])
674}
675
676fn parse_value_source(s: &str, offset: usize) -> Result<ValueSource, Diag> {
677 let s = s.trim();
678 if let Some(inner) = s.strip_prefix('[').and_then(|x| x.strip_suffix(']')) {
679 let inner = inner.trim();
680 let (kind, rest) = split_first_word(inner);
681 let rest = rest.trim();
682 match kind {
683 "ENV" => Ok(ValueSource::Env {
684 name: rest.to_string(),
685 span: offset..offset + s.len(),
686 }),
687 "HEADER" => Ok(ValueSource::Header {
688 name: rest.to_string(),
689 span: offset..offset + s.len(),
690 }),
691 other => Err(Diag::error(
692 format!("unknown value source `{}`", other),
693 offset..offset + s.len(),
694 "expected [ENV NAME] or [HEADER NAME]",
695 )),
696 }
697 } else if !s.is_empty() {
698 Ok(ValueSource::Literal {
699 value: s.to_string(),
700 span: offset..offset + s.len(),
701 })
702 } else {
703 Err(Diag::error(
704 "missing value source",
705 offset..offset,
706 "expected [ENV NAME], [HEADER NAME] or a literal",
707 ))
708 }
709}
710
711fn parse_auth(value: &str, offset: usize) -> Result<AuthSpec, Diag> {
712 let value = value.trim();
713 let (scheme, rest) = split_first_word(value);
714 let rest = rest.trim();
715 if !scheme.eq_ignore_ascii_case("Bearer") {
716 return Err(Diag::error(
717 format!("unsupported auth scheme `{}`", scheme),
718 offset..offset + scheme.len(),
719 "only Bearer is supported",
720 ));
721 }
722 let inner = rest
723 .strip_prefix('[')
724 .and_then(|s| s.strip_suffix(']'))
725 .ok_or_else(|| {
726 Diag::error(
727 "missing `[HEADER name]` after Bearer",
728 offset..offset + value.len(),
729 "expected `AUTH Bearer [HEADER NAME]`",
730 )
731 })?;
732 let (kind, name) = split_first_word(inner.trim());
733 let name = name.trim();
734 if !kind.eq_ignore_ascii_case("HEADER") {
735 return Err(Diag::error(
736 format!("unsupported auth source `{}`", kind),
737 offset..offset + value.len(),
738 "only [HEADER NAME] is supported",
739 ));
740 }
741 if name.is_empty() {
742 return Err(Diag::error(
743 "missing header name",
744 offset..offset + value.len(),
745 "expected `[HEADER NAME]`",
746 ));
747 }
748 Ok(AuthSpec::BearerHeader {
749 header: name.to_string(),
750 span: offset..offset + value.len(),
751 })
752}
753
754fn parse_size(s: &str) -> Option<u64> {
755 parse_suffixed(
756 s,
757 &[
758 ("kb", 1024),
759 ("mb", 1024 * 1024),
760 ("gb", 1024 * 1024 * 1024),
761 ("b", 1),
762 ],
763 1,
764 )
765}
766
767fn parse_duration_ms(s: &str) -> Option<u64> {
768 parse_suffixed(s, &[("ms", 1), ("s", 1000), ("m", 60_000)], 1000)
769}
770
771fn parse_suffixed(s: &str, suffixes: &[(&str, u64)], default_mult: u64) -> Option<u64> {
775 let s = s.trim().to_ascii_lowercase();
776 let (num, mult) = suffixes
777 .iter()
778 .find_map(|(suf, m)| s.strip_suffix(suf).map(|rest| (rest.trim(), *m)))
779 .unwrap_or((s.as_str(), default_mult));
780 num.trim().parse::<u64>().ok()?.checked_mul(mult)
781}
782
783pub fn parse_type_expr(s: &str, offset: usize) -> Result<TypeExpr, Diag> {
784 let s = s.trim();
785 if s.is_empty() {
786 return Err(Diag::error(
787 "missing type",
788 offset..offset,
789 "expected a type expression",
790 ));
791 }
792 if let Some(stripped) = s.strip_prefix('/') {
794 if let Some(pat) = stripped.strip_suffix('/') {
795 return Ok(TypeExpr::Regex {
796 pattern: pat.to_string(),
797 span: offset..offset + s.len(),
798 });
799 } else {
800 return Err(Diag::error(
801 "unterminated regex",
802 offset..offset + s.len(),
803 "regex must be enclosed in `/.../`",
804 ));
805 }
806 }
807 if let Some(rest) = s.strip_prefix("int(")
809 && let Some(inner) = rest.strip_suffix(')')
810 {
811 let parts: Vec<&str> = inner.splitn(2, "..").collect();
812 if parts.len() == 2
813 && let (Ok(a), Ok(b)) = (
814 parts[0].trim().parse::<i64>(),
815 parts[1].trim().parse::<i64>(),
816 )
817 {
818 return Ok(TypeExpr::IntRange {
819 min: a,
820 max: b,
821 span: offset..offset + s.len(),
822 });
823 }
824 return Err(Diag::error(
825 "invalid int range",
826 offset..offset + s.len(),
827 "expected `int(a..b)`",
828 ));
829 }
830 if let Some(rest) = s.strip_prefix("float(")
831 && let Some(inner) = rest.strip_suffix(')')
832 {
833 let parts: Vec<&str> = inner.splitn(2, "..").collect();
834 if parts.len() == 2
835 && let (Ok(a), Ok(b)) = (
836 parts[0].trim().parse::<f64>(),
837 parts[1].trim().parse::<f64>(),
838 )
839 {
840 return Ok(TypeExpr::FloatRange {
841 min: a,
842 max: b,
843 span: offset..offset + s.len(),
844 });
845 }
846 return Err(Diag::error(
847 "invalid float range",
848 offset..offset + s.len(),
849 "expected `float(a..b)`",
850 ));
851 }
852 match s {
853 "int" => Ok(TypeExpr::Int),
854 "float" => Ok(TypeExpr::Float),
855 "boolean" | "bool" => Ok(TypeExpr::Boolean),
856 "uuid" => Ok(TypeExpr::Uuid),
857 "string" => Ok(TypeExpr::String),
858 "json" => Ok(TypeExpr::Json),
859 "binary" => Ok(TypeExpr::Binary),
860 _ if s.contains('|') => {
861 let variants: Vec<String> = s
862 .split('|')
863 .map(|v| v.trim().to_string())
864 .filter(|v| !v.is_empty())
865 .collect();
866 if variants.is_empty() {
867 Err(Diag::error(
868 "empty union",
869 offset..offset + s.len(),
870 "expected at least one variant",
871 ))
872 } else {
873 Ok(TypeExpr::Union {
874 variants,
875 span: offset..offset + s.len(),
876 })
877 }
878 }
879 other => Err(Diag::error(
880 format!("unknown type `{}`", other),
881 offset..offset + s.len(),
882 "expected int, float, boolean, uuid, string, json, binary, a range, union or regex",
883 )),
884 }
885}