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}