Skip to main content

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}