Skip to main content

mii_http/
spec.rs

1//! AST for the .http specs file.
2
3use std::collections::BTreeMap;
4use std::ops::Range;
5
6pub type Span = Range<usize>;
7
8#[derive(Debug, Clone)]
9pub struct Spec {
10    pub setup: Setup,
11    pub endpoints: Vec<Endpoint>,
12}
13
14#[derive(Debug, Clone, Default)]
15pub struct Setup {
16    pub version: Option<u32>,
17    pub base: Option<String>,
18    pub auth: Option<AuthSpec>,
19    pub jwt_verifier: Option<ValueSource>,
20    pub token_secret: Option<ValueSource>,
21    pub max_body_size: Option<u64>,
22    pub max_query_param_size: Option<u64>,
23    pub max_header_size: Option<u64>,
24    pub timeout_ms: Option<u64>,
25    /// span of the setup region (for diagnostics)
26    pub span: Span,
27}
28
29#[derive(Debug, Clone)]
30pub enum AuthSpec {
31    /// Bearer token expected in given header name.
32    BearerHeader { header: String, span: Span },
33}
34
35#[derive(Debug, Clone)]
36pub enum ValueSource {
37    Env { name: String, span: Span },
38    Header { name: String, span: Span },
39    Literal { value: String, span: Span },
40}
41
42impl ValueSource {
43    pub fn span(&self) -> Span {
44        match self {
45            ValueSource::Env { span, .. }
46            | ValueSource::Header { span, .. }
47            | ValueSource::Literal { span, .. } => span.clone(),
48        }
49    }
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
53pub enum Method {
54    Get,
55    Post,
56    Put,
57    Delete,
58    Patch,
59}
60
61impl Method {
62    pub fn as_str(self) -> &'static str {
63        match self {
64            Method::Get => "GET",
65            Method::Post => "POST",
66            Method::Put => "PUT",
67            Method::Delete => "DELETE",
68            Method::Patch => "PATCH",
69        }
70    }
71}
72
73#[derive(Debug, Clone)]
74pub struct Endpoint {
75    pub method: Method,
76    pub path: String,
77    /// Parsed segments of the path: literal or `:name` typed param.
78    pub path_segments: Vec<PathSegment>,
79    pub response_type: Option<String>,
80    /// `Response-Type stream <mime>` produces an HTTP chunked-transfer
81    /// response that streams the command's stdout to the client as it is
82    /// produced rather than buffering the full output.
83    pub response_stream: bool,
84    pub query_params: Vec<NamedField>,
85    pub headers: Vec<NamedField>,
86    pub vars: Vec<VarDef>,
87    pub body: Option<BodySpec>,
88    pub exec: ExecSpec,
89    pub span: Span,
90}
91
92#[derive(Debug, Clone)]
93pub enum PathSegment {
94    Literal(String),
95    Param {
96        name: String,
97        ty: TypeExpr,
98        span: Span,
99    },
100}
101
102#[derive(Debug, Clone)]
103pub struct NamedField {
104    pub name: String,
105    pub optional: bool,
106    pub ty: TypeExpr,
107    pub span: Span,
108}
109
110#[derive(Debug, Clone)]
111pub struct VarDef {
112    pub name: String,
113    pub source: ValueSource,
114    pub span: Span,
115}
116
117#[derive(Debug, Clone)]
118pub enum BodySpec {
119    /// Raw textual body, no schema (`BODY json` unschematized, `BODY string`).
120    Json {
121        schema: Option<JsonSchema>,
122        span: Span,
123    },
124    Form {
125        fields: Vec<NamedField>,
126        span: Span,
127    },
128    String {
129        span: Span,
130    },
131    Binary {
132        span: Span,
133    },
134}
135
136#[derive(Debug, Clone)]
137pub struct JsonSchema {
138    pub fields: Vec<JsonField>,
139}
140
141#[derive(Debug, Clone)]
142pub struct JsonField {
143    pub name: String,
144    pub optional: bool,
145    pub ty: JsonFieldType,
146    pub span: Span,
147}
148
149#[derive(Debug, Clone)]
150pub enum JsonFieldType {
151    Scalar(TypeExpr),
152    Array(TypeExpr),
153}
154
155#[derive(Debug, Clone)]
156pub enum TypeExpr {
157    Int,
158    Float,
159    Boolean,
160    Uuid,
161    String,
162    Json,
163    Binary,
164    IntRange { min: i64, max: i64, span: Span },
165    FloatRange { min: f64, max: f64, span: Span },
166    Union { variants: Vec<String>, span: Span },
167    Regex { pattern: String, span: Span },
168}
169
170impl TypeExpr {
171    pub fn name(&self) -> &'static str {
172        match self {
173            TypeExpr::Int => "int",
174            TypeExpr::Float => "float",
175            TypeExpr::Boolean => "boolean",
176            TypeExpr::Uuid => "uuid",
177            TypeExpr::String => "string",
178            TypeExpr::Json => "json",
179            TypeExpr::Binary => "binary",
180            TypeExpr::IntRange { .. } => "int(range)",
181            TypeExpr::FloatRange { .. } => "float(range)",
182            TypeExpr::Union { .. } => "union",
183            TypeExpr::Regex { .. } => "regex",
184        }
185    }
186}
187
188#[derive(Debug, Clone)]
189pub struct ExecSpec {
190    pub raw: String,
191    pub span: Span,
192    /// One or more pipeline statements. Single-line `Exec:` produces a
193    /// single statement; the multi-line `Exec: <<< ... >>>` form produces
194    /// one statement per non-blank line. Statements run sequentially in the
195    /// same shell invocation, sharing stdout to the client.
196    pub statements: Vec<Vec<ExecStage>>,
197}
198
199impl ExecSpec {
200    pub fn all_stages(&self) -> impl Iterator<Item = &ExecStage> {
201        self.statements.iter().flatten()
202    }
203}
204
205/// A pipeline stage: either a value source piped to next, or a command.
206#[derive(Debug, Clone)]
207pub enum ExecStage {
208    /// A bare value reference (e.g. `$`, `$.path`, `%name`) used as stdin into next stage.
209    Source {
210        reference: ValueRef,
211        span: Span,
212    },
213    Command {
214        tokens: Vec<ExecToken>,
215        span: Span,
216    },
217}
218
219#[derive(Debug, Clone)]
220pub enum ExecToken {
221    /// A token built from text + quoted-string `{...}` interpolations. Always emitted.
222    Text {
223        parts: Vec<TextPart>,
224        force_quote: bool,
225        span: Span,
226    },
227    /// A `[...]` shell-piece group; if any interpolation is missing, omit the whole group.
228    Group { pieces: Vec<GroupPiece>, span: Span },
229}
230
231#[derive(Debug, Clone)]
232pub enum TextPart {
233    Literal(String),
234    Interp(ValueRef),
235}
236
237#[derive(Debug, Clone)]
238pub struct GroupPiece {
239    pub parts: Vec<TextPart>,
240    pub force_quote: bool,
241}
242
243#[derive(Debug, Clone)]
244pub enum ValueRef {
245    Query(String),
246    Path(String),
247    Header(String),
248    Var(String),
249    /// Whole body or a JSON path into the body. Empty path = whole body.
250    Body {
251        path: Vec<String>,
252    },
253}
254
255impl ValueRef {
256    pub fn describe(&self) -> String {
257        match self {
258            ValueRef::Query(n) => format!("query param `{}`", n),
259            ValueRef::Path(n) => format!("path param `{}`", n),
260            ValueRef::Header(n) => format!("header `{}`", n),
261            ValueRef::Var(n) => format!("var `{}`", n),
262            ValueRef::Body { path } if path.is_empty() => "body".to_string(),
263            ValueRef::Body { path } => format!("body field `{}`", path.join(".")),
264        }
265    }
266}
267
268/// Helper map type used by validators.
269pub type FieldMap = BTreeMap<String, NamedField>;