oxide_gen/ir.rs
1//! # Intermediate Representation
2//!
3//! Parsers convert their respective spec formats into a common [`ApiSpec`].
4//! Emitters then consume the IR to produce Rust code, CLI definitions,
5//! `SKILL.md`, and MCP server configurations.
6//!
7//! The IR is intentionally simple — just enough to drive a basic generator.
8//! Features like polymorphism (`oneOf`/`anyOf`), recursive `$ref`s, and rich
9//! validation rules are deliberately out of scope.
10
11use serde::{Deserialize, Serialize};
12
13/// The full description of an API.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct ApiSpec {
16 /// Crate name (snake_case) for the generated output.
17 pub name: String,
18 /// Human-readable display name.
19 pub display_name: String,
20 /// Semantic version.
21 pub version: String,
22 /// Optional crate-level description.
23 pub description: Option<String>,
24 /// Source API kind.
25 pub kind: ApiKind,
26 /// Default base URL, if known.
27 pub base_url: Option<String>,
28 /// User-defined types (structs, enums, aliases).
29 pub types: Vec<TypeDef>,
30 /// Callable operations.
31 pub operations: Vec<Operation>,
32 /// The raw spec contents (useful for build.rs or schema files).
33 #[serde(default)]
34 pub raw_spec: Option<String>,
35}
36
37/// The format of the original API specification.
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
39#[serde(rename_all = "snake_case")]
40pub enum ApiKind {
41 /// OpenAPI 3.x.
42 OpenApi,
43 /// GraphQL schema definition language.
44 GraphQl,
45 /// Protocol Buffers / gRPC.
46 Grpc,
47}
48
49impl ApiKind {
50 /// Short stable identifier (used in MCP/SKILL.md generation).
51 pub fn slug(self) -> &'static str {
52 match self {
53 ApiKind::OpenApi => "openapi",
54 ApiKind::GraphQl => "graphql",
55 ApiKind::Grpc => "grpc",
56 }
57 }
58}
59
60/// A user-defined type that becomes a Rust struct/enum/alias.
61#[derive(Debug, Clone, Serialize, Deserialize)]
62#[serde(tag = "kind", rename_all = "snake_case")]
63pub enum TypeDef {
64 /// Plain object → Rust struct.
65 Struct {
66 /// PascalCase Rust name.
67 name: String,
68 /// Doc comment.
69 description: Option<String>,
70 /// Field list.
71 fields: Vec<Field>,
72 },
73 /// Closed string enum → Rust enum.
74 Enum {
75 /// PascalCase Rust name.
76 name: String,
77 /// Doc comment.
78 description: Option<String>,
79 /// Variants — each one tracks its Rust name plus an optional original
80 /// spec name for serde renaming.
81 variants: Vec<EnumVariant>,
82 },
83 /// Simple alias → `pub type Alias = Target;`.
84 Alias {
85 /// PascalCase Rust name.
86 name: String,
87 /// Target Rust type expression.
88 target: String,
89 },
90}
91
92impl TypeDef {
93 /// Return the Rust identifier of the type.
94 pub fn name(&self) -> &str {
95 match self {
96 TypeDef::Struct { name, .. }
97 | TypeDef::Enum { name, .. }
98 | TypeDef::Alias { name, .. } => name,
99 }
100 }
101}
102
103/// A single enum variant.
104#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct EnumVariant {
106 /// PascalCase Rust identifier.
107 pub name: String,
108 /// Original spec value, used for serde rename if it differs from `name`.
109 pub serde_rename: Option<String>,
110}
111
112/// A single struct field.
113#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct Field {
115 /// snake_case Rust identifier.
116 pub name: String,
117 /// Original spec name (used for serde rename when it differs).
118 pub serde_rename: Option<String>,
119 /// Pre-rendered Rust type (e.g. `Vec<String>`, `Option<i64>`).
120 pub rust_type: String,
121 /// Whether the field is optional (already accounted for in `rust_type`).
122 pub optional: bool,
123 /// Doc comment.
124 pub description: Option<String>,
125}
126
127/// An invocable API operation.
128#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct Operation {
130 /// snake_case Rust method name.
131 pub id: String,
132 /// Original operation id from the spec (used for CLI subcommand name).
133 pub original_id: String,
134 /// Doc comment / summary.
135 pub description: Option<String>,
136 /// Protocol used by this operation.
137 pub protocol: Protocol,
138 /// For HTTP-based operations: METHOD + path. For gRPC: full method
139 /// identifier (`service/method`).
140 pub endpoint: String,
141 /// HTTP method (only meaningful for OpenAPI/GraphQL).
142 pub http_method: HttpMethod,
143 /// Inputs.
144 pub params: Vec<Param>,
145 /// Pre-rendered Rust return type, e.g. `Pet` or `Vec<Pet>` or `()`.
146 pub return_type: String,
147 /// Streaming direction. `Unary` for plain request/response operations,
148 /// `ServerStream` for GraphQL subscriptions and gRPC `stream` responses,
149 /// `ClientStream` / `BidiStream` for gRPC stream-in / stream-both
150 /// signatures.
151 #[serde(default)]
152 pub streaming: StreamingMode,
153}
154
155/// Streaming direction tag attached to an [`Operation`].
156#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
157#[serde(rename_all = "snake_case")]
158pub enum StreamingMode {
159 /// Plain request/response.
160 #[default]
161 Unary,
162 /// Server pushes multiple messages (GraphQL `Subscription`, gRPC
163 /// `stream` response).
164 ServerStream,
165 /// Client pushes multiple messages (gRPC `stream` request).
166 ClientStream,
167 /// Both sides stream (gRPC `stream` request + `stream` response).
168 BidiStream,
169}
170
171impl StreamingMode {
172 /// `true` for any non-unary mode.
173 pub fn is_streaming(self) -> bool {
174 !matches!(self, StreamingMode::Unary)
175 }
176
177 /// Short human-readable label used in SKILL.md / docs.
178 pub fn label(self) -> &'static str {
179 match self {
180 StreamingMode::Unary => "unary",
181 StreamingMode::ServerStream => "server-stream",
182 StreamingMode::ClientStream => "client-stream",
183 StreamingMode::BidiStream => "bidi-stream",
184 }
185 }
186}
187
188/// The wire protocol used by an operation.
189#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
190#[serde(rename_all = "snake_case")]
191pub enum Protocol {
192 /// REST / HTTP+JSON.
193 Rest,
194 /// GraphQL over HTTP POST.
195 GraphQl,
196 /// gRPC.
197 Grpc,
198}
199
200/// HTTP method.
201#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
202#[serde(rename_all = "UPPERCASE")]
203pub enum HttpMethod {
204 /// `GET`.
205 Get,
206 /// `POST`.
207 Post,
208 /// `PUT`.
209 Put,
210 /// `PATCH`.
211 Patch,
212 /// `DELETE`.
213 Delete,
214 /// `OPTIONS`.
215 Options,
216 /// `HEAD`.
217 Head,
218 /// Not applicable (gRPC, etc.).
219 None,
220}
221
222impl HttpMethod {
223 /// Reqwest method-builder name (`get`, `post`, …). Returns `None` for
224 /// [`HttpMethod::None`].
225 pub fn reqwest_fn(self) -> Option<&'static str> {
226 Some(match self {
227 HttpMethod::Get => "get",
228 HttpMethod::Post => "post",
229 HttpMethod::Put => "put",
230 HttpMethod::Patch => "patch",
231 HttpMethod::Delete => "delete",
232 HttpMethod::Options => return None,
233 HttpMethod::Head => "head",
234 HttpMethod::None => return None,
235 })
236 }
237
238 /// Uppercase HTTP verb.
239 pub fn as_str(self) -> &'static str {
240 match self {
241 HttpMethod::Get => "GET",
242 HttpMethod::Post => "POST",
243 HttpMethod::Put => "PUT",
244 HttpMethod::Patch => "PATCH",
245 HttpMethod::Delete => "DELETE",
246 HttpMethod::Options => "OPTIONS",
247 HttpMethod::Head => "HEAD",
248 HttpMethod::None => "NONE",
249 }
250 }
251}
252
253/// A single operation parameter.
254#[derive(Debug, Clone, Serialize, Deserialize)]
255pub struct Param {
256 /// snake_case Rust identifier.
257 pub name: String,
258 /// Original spec name (used for serde / query string keys).
259 pub original_name: String,
260 /// Pre-rendered Rust type.
261 pub rust_type: String,
262 /// Where the parameter lives in the request.
263 pub location: ParamLocation,
264 /// Whether the parameter is mandatory.
265 pub required: bool,
266 /// Doc comment.
267 pub description: Option<String>,
268}
269
270/// Where a parameter appears in the request.
271#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
272#[serde(rename_all = "snake_case")]
273pub enum ParamLocation {
274 /// URL path segment, e.g. `/pets/{petId}`.
275 Path,
276 /// Query string parameter.
277 Query,
278 /// Request body (typically JSON).
279 Body,
280 /// HTTP header.
281 Header,
282 /// gRPC field on the request message.
283 GrpcField,
284 /// GraphQL operation variable.
285 GraphQlVariable,
286}