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