Skip to main content

openapi_to_rust/server/
mod.rs

1//! Server codegen — opt-in Axum scaffolding for user-selected operations.
2//!
3//! Phase 1 (this file): read-only operation index for the
4//! `openapi-to-rust server list` command. Later phases build on
5//! [`OperationIndex`] for selector resolution, codegen, and TOML edits.
6//!
7//! See `docs/planning/server-codegen.md` for the full design.
8
9use crate::analysis::{OperationInfo, SchemaAnalysis};
10
11pub mod codegen;
12pub mod edit;
13pub mod list;
14pub mod selector;
15
16pub use selector::{Resolution, Selector, SelectorParseError, SelectorResolveError, resolve};
17
18/// Parse and resolve raw operation selectors with enough context for config
19/// errors. Shared by client and server generation.
20pub fn resolve_operation_selectors(
21    selectors: &[String],
22    analysis: &SchemaAnalysis,
23) -> Result<Resolution, OperationSelectionError> {
24    let parsed = selectors
25        .iter()
26        .map(|selector| {
27            Selector::parse(selector).map_err(|source| OperationSelectionError::Parse {
28                selector: selector.clone(),
29                source,
30            })
31        })
32        .collect::<Result<Vec<_>, _>>()?;
33    let index = OperationIndex::from_analysis(analysis);
34    resolve(&parsed, &index).map_err(OperationSelectionError::Resolve)
35}
36
37/// Resolve a field whose contract is specifically one operation ID.
38///
39/// Unlike [`resolve_operation_selectors`], this does not interpret
40/// `METHOD /path` or `tag:<name>` syntax. It still uses analyzer alias metadata
41/// so renamed and ambiguous source IDs receive the same actionable errors.
42pub fn resolve_operation_id(
43    operation_id: &str,
44    analysis: &SchemaAnalysis,
45) -> Result<Resolution, OperationSelectionError> {
46    let index = OperationIndex::from_analysis(analysis);
47    resolve(&[Selector::OperationId(operation_id.to_string())], &index)
48        .map_err(OperationSelectionError::Resolve)
49}
50
51#[derive(Debug, thiserror::Error)]
52pub enum OperationSelectionError {
53    #[error("invalid operation selector `{selector}`: {source}")]
54    Parse {
55        selector: String,
56        #[source]
57        source: SelectorParseError,
58    },
59    #[error("operation selector did not resolve: {0}")]
60    Resolve(#[source] SelectorResolveError),
61}
62
63/// Read-only snapshot of every operation in a spec, in the order the
64/// analyzer surfaced them. Built once per command invocation.
65#[derive(Debug, Clone)]
66pub struct OperationIndex {
67    operations: Vec<OperationSummary>,
68    operation_id_aliases: std::collections::BTreeMap<String, Vec<String>>,
69}
70
71/// Display-friendly subset of [`OperationInfo`] used by `server list`
72/// and (later) by selector resolution and `server add`/`remove`.
73#[derive(Debug, Clone, serde::Serialize)]
74pub struct OperationSummary {
75    pub operation_id: String,
76    pub method: String,
77    pub path: String,
78    pub tags: Vec<String>,
79    /// True when any response declares `text/event-stream`. Surfaces
80    /// in the listing so users can spot SSE endpoints at a glance.
81    pub supports_streaming: bool,
82}
83
84impl OperationIndex {
85    /// Build an index from an already-completed [`SchemaAnalysis`].
86    pub fn from_analysis(analysis: &SchemaAnalysis) -> Self {
87        let operations = analysis
88            .operations
89            .values()
90            .map(OperationSummary::from)
91            .collect();
92        Self {
93            operations,
94            operation_id_aliases: analysis.operation_id_aliases.clone(),
95        }
96    }
97
98    /// Build directly from pre-built summaries. Used by tests and by
99    /// callers that have already projected `OperationInfo` to the
100    /// display shape.
101    pub fn from_summaries(operations: Vec<OperationSummary>) -> Self {
102        Self {
103            operations,
104            operation_id_aliases: Default::default(),
105        }
106    }
107
108    pub fn operations(&self) -> &[OperationSummary] {
109        &self.operations
110    }
111
112    pub(crate) fn operation_id_aliases(&self, raw_id: &str) -> Option<&[String]> {
113        self.operation_id_aliases.get(raw_id).map(Vec::as_slice)
114    }
115
116    #[cfg(test)]
117    pub(crate) fn with_aliases(
118        mut self,
119        aliases: std::collections::BTreeMap<String, Vec<String>>,
120    ) -> Self {
121        self.operation_id_aliases = aliases;
122        self
123    }
124
125    /// Count of distinct tags across all operations. Untagged ops are
126    /// counted under a synthetic `<untagged>` bucket only for display;
127    /// they do not appear in this count.
128    pub fn tag_count(&self) -> usize {
129        let mut tags: std::collections::BTreeSet<&str> = std::collections::BTreeSet::new();
130        for op in &self.operations {
131            for t in &op.tags {
132                tags.insert(t.as_str());
133            }
134        }
135        tags.len()
136    }
137}
138
139impl From<&OperationInfo> for OperationSummary {
140    fn from(op: &OperationInfo) -> Self {
141        Self {
142            operation_id: op.operation_id.clone(),
143            method: op.method.clone(),
144            path: op.path.clone(),
145            tags: op.tags.clone(),
146            supports_streaming: op.supports_streaming,
147        }
148    }
149}