Skip to content

Commit 2645e05

Browse files
feat(server): server list CLI (P1)
First slice of opt-in Axum server codegen. Adds a read-only discovery command: openapi-to-rust server list --spec <path> [--tag <name>] [--method <verb>] [--grep <pat>] [--json] Aligned-table output by default, JSON for scripting. [SSE] marker on operations whose responses declare text/event-stream. Surfaces the canonical test cases for later phases: - OpenAI createResponse (POST /responses, [SSE]) - Anthropic messages_post (POST /v1/messages) Mechanics: - OperationInfo gains a tags: Vec<String> field, populated from the spec's operation.tags. Empty when untagged. - New module src/server/ with OperationIndex + list filter/render. - Tests construct OperationSummary directly; 9 unit tests cover filter composition, JSON output, SSE marker, empty-result UX. - Existing OperationInfo struct-literal sites in tests updated. - Snapshot tests refreshed to include the new tags field. Closes openapi-generator-f8q; followups: openapi-generator-in6 (Anthropic spec lacks text/event-stream content type on messages_post — needs schema-extension overlay before SSE works on that endpoint). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 268febe commit 2645e05

13 files changed

Lines changed: 506 additions & 5 deletions

.beads/issues.jsonl

Lines changed: 4 additions & 3 deletions
Large diffs are not rendered by default.

docs/planning/server-codegen.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,13 @@ Reuses the reachability pass that the client generator already has.
189189
3. `server add` / `server remove` — TOML edits.
190190
4. Trait + response-enum emitter.
191191
5. Router factory + extractors + reachability pruning.
192-
6. Snapshot tests against `specs/openai.yaml`, canonical case
193-
`createChatCompletion`.
192+
6. Snapshot tests against the two canonical specs:
193+
- OpenAI Responses API — `createResponse` in `specs/openai.yaml`
194+
(POST `/v1/responses`, SSE via `stream: true`)
195+
- Anthropic Messages API — `messages_post` in `specs/anthropic.yaml`
196+
(POST `/v1/messages`, SSE via `stream: true`)
197+
198+
Both must work end-to-end. They drive everything: streaming, complex
199+
request unions, header params (`anthropic-version`), 4XX response groups.
194200

195201
Each phase ships standalone and is reviewable independently.

src/analysis.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,10 @@ pub struct OperationInfo {
311311
pub supports_streaming: bool,
312312
/// Stream parameter name if applicable
313313
pub stream_parameter: Option<String>,
314+
/// Tags declared on the operation. Empty when the spec sets none.
315+
/// Used by the server codegen selector grammar (e.g. `tag:Chat`)
316+
/// and by `openapi-to-rust server list` for grouping.
317+
pub tags: Vec<String>,
314318
}
315319

316320
/// Content type and schema for a request body
@@ -4232,6 +4236,7 @@ impl SchemaAnalyzer {
42324236
parameters: Vec::new(),
42334237
supports_streaming: false, // Will be determined by StreamingConfig, not spec
42344238
stream_parameter: None, // Will be determined by StreamingConfig, not spec
4239+
tags: operation.tags.clone().unwrap_or_default(),
42354240
};
42364241

42374242
// Extract request body schema with content-type awareness

src/bin/openapi-to-rust.rs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
use clap::{Parser, Subcommand};
22
use openapi_to_rust::cli::{json_from_str_lossy, yaml_to_json_value};
3+
use openapi_to_rust::server::{
4+
OperationIndex,
5+
list::{ListFilter, ListOutput, render as render_list},
6+
};
37
use openapi_to_rust::{CodeGenerator, ConfigFile, SchemaAnalyzer};
48
use std::path::PathBuf;
59

@@ -31,6 +35,37 @@ enum Commands {
3135
#[arg(short, long, default_value = "openapi-to-rust.toml")]
3236
config: PathBuf,
3337
},
38+
/// Server codegen commands (opt-in Axum scaffolding).
39+
Server {
40+
#[command(subcommand)]
41+
action: ServerCommands,
42+
},
43+
}
44+
45+
#[derive(Subcommand)]
46+
enum ServerCommands {
47+
/// List every operation in a spec. Read-only.
48+
List {
49+
/// Path to the OpenAPI spec (.yaml/.yml/.json). If omitted,
50+
/// the spec_path from openapi-to-rust.toml is used.
51+
#[arg(long)]
52+
spec: Option<PathBuf>,
53+
/// Path to TOML config to read spec_path from when --spec is absent.
54+
#[arg(long, default_value = "openapi-to-rust.toml")]
55+
config: PathBuf,
56+
/// Substring match against tag names (case insensitive).
57+
#[arg(long)]
58+
tag: Option<String>,
59+
/// Exact HTTP method filter (GET, POST, ...; case insensitive).
60+
#[arg(long)]
61+
method: Option<String>,
62+
/// Substring match against operationId and path.
63+
#[arg(long)]
64+
grep: Option<String>,
65+
/// Emit JSON instead of an aligned table.
66+
#[arg(long)]
67+
json: bool,
68+
},
3469
}
3570

3671
#[tokio::main]
@@ -190,5 +225,62 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
190225

191226
Ok(())
192227
}
228+
Commands::Server { action } => match action {
229+
ServerCommands::List {
230+
spec,
231+
config,
232+
tag,
233+
method,
234+
grep,
235+
json,
236+
} => run_server_list(spec, config, tag, method, grep, json),
237+
},
193238
}
194239
}
240+
241+
fn run_server_list(
242+
spec: Option<PathBuf>,
243+
config: PathBuf,
244+
tag: Option<String>,
245+
method: Option<String>,
246+
grep: Option<String>,
247+
json: bool,
248+
) -> Result<(), Box<dyn std::error::Error>> {
249+
let spec_path = match spec {
250+
Some(p) => p,
251+
None => {
252+
let cf = ConfigFile::load(&config).map_err(|e| {
253+
format!(
254+
"no --spec provided and failed to load {}: {}",
255+
config.display(),
256+
e
257+
)
258+
})?;
259+
cf.into_generator_config().spec_path
260+
}
261+
};
262+
263+
let spec_content = std::fs::read_to_string(&spec_path)?;
264+
let spec_value: serde_json::Value = if spec_path.extension()
265+
== Some(std::ffi::OsStr::new("yaml"))
266+
|| spec_path.extension() == Some(std::ffi::OsStr::new("yml"))
267+
{
268+
yaml_to_json_value(&spec_content)?
269+
} else {
270+
json_from_str_lossy(&spec_content)?
271+
};
272+
273+
let mut analyzer = SchemaAnalyzer::new(spec_value)?;
274+
let analysis = analyzer.analyze()?;
275+
let index = OperationIndex::from_analysis(&analysis);
276+
277+
let filter = ListFilter { tag, method, grep };
278+
let output = if json {
279+
ListOutput::Json
280+
} else {
281+
ListOutput::Table
282+
};
283+
let (body, _count) = render_list(&index, &filter, output);
284+
print!("{body}");
285+
Ok(())
286+
}

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ pub mod http_error;
1010
pub mod openapi;
1111
pub mod patterns;
1212
pub mod registry_generator;
13+
pub mod server;
1314
pub mod streaming;
1415
pub mod type_mapping;
1516

0 commit comments

Comments
 (0)