Skip to main content

oxide_gen/
lib.rs

1//! # `oxide-gen` — Spec-to-Crate Generator
2//!
3//! `oxide-gen` ingests an API specification (OpenAPI 3.x, GraphQL SDL, or a
4//! `.proto` file) and produces a self-contained Rust crate consisting of:
5//!
6//! * Type-safe Rust structs / enums for the spec's data models.
7//! * A `reqwest`-based asynchronous client (for OpenAPI and GraphQL) or a
8//!   `tonic`-shaped scaffold (for gRPC) with one method per operation.
9//! * A `clap` CLI that exposes every operation as a subcommand.
10//! * A `SKILL.md` file with YAML frontmatter for Claude Code.
11//! * An `mcp.json` file describing the CLI as MCP-callable tools.
12//! * A `module.json` manifest that the [`oxide_k`] kernel can discover and
13//!   register as a module.
14//!
15//! The crate exposes three layers:
16//!
17//! * [`parsers`] — spec format ↔ [`ir::ApiSpec`].
18//! * [`emit`] — [`ir::ApiSpec`] → on-disk artifacts.
19//! * [`generate_from_path`] — convenience entry point that auto-detects the
20//!   spec format from the file extension and runs the full pipeline.
21
22#![deny(rust_2018_idioms)]
23#![warn(missing_docs)]
24
25use std::path::Path;
26
27pub mod emit;
28pub mod error;
29pub mod ir;
30pub mod parsers;
31
32pub use error::{GenError, Result};
33pub use ir::{ApiKind, ApiSpec};
34
35/// Top-level entry point: parse the spec at `spec_path`, optionally
36/// overriding the auto-detected kind, then emit a complete generated crate
37/// into `output_dir`.
38///
39/// `crate_name` overrides the snake_case crate name; if `None`, the parser's
40/// inferred name is kept.
41pub fn generate_from_path(
42    spec_path: &Path,
43    kind: Option<ApiKind>,
44    output_dir: &Path,
45    crate_name: Option<&str>,
46) -> Result<emit::EmitReport> {
47    let kind = kind
48        .or_else(|| ApiKind::infer_from_path(spec_path))
49        .ok_or_else(|| GenError::Parse {
50            kind: "auto-detect",
51            message: format!(
52                "could not infer API kind from {:?}; pass --kind explicitly",
53                spec_path
54            ),
55        })?;
56
57    let raw = std::fs::read_to_string(spec_path).map_err(|source| GenError::ReadSpec {
58        path: spec_path.to_path_buf(),
59        source,
60    })?;
61
62    let mut spec = match kind {
63        ApiKind::OpenApi => parsers::openapi::parse(&raw)?,
64        ApiKind::GraphQl => parsers::graphql::parse(&raw)?,
65        ApiKind::Grpc => parsers::proto::parse(&raw)?,
66    };
67    spec.raw_spec = Some(raw);
68
69    if let Some(name) = crate_name {
70        spec.name = name.to_string();
71    }
72
73    emit::emit_crate(&spec, output_dir)
74}
75
76impl ApiKind {
77    /// Infer the API kind from a spec file's extension.
78    pub fn infer_from_path(path: &Path) -> Option<Self> {
79        let ext = path.extension()?.to_str()?.to_ascii_lowercase();
80        Some(match ext.as_str() {
81            "yaml" | "yml" | "json" => ApiKind::OpenApi,
82            "graphql" | "graphqls" | "gql" => ApiKind::GraphQl,
83            "proto" => ApiKind::Grpc,
84            _ => return None,
85        })
86    }
87}