Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions crates/oapi-codegen/src/emit/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ mod operation;
mod package;
mod reqwest;
mod servers;
mod usage;
pub(crate) mod usage;

use std::collections::HashMap;

Expand Down Expand Up @@ -300,13 +300,12 @@ fn render_body(items: &[TokenStream]) -> Result<String> {
}

/// Render a doc attribute, or nothing when there is no documentation.
///
/// This splits the text on its line breaks, so a multi-line `description` prints
/// as a run of `///` lines and not one `/** */` block.
pub(crate) fn doc_attr(doc: &Option<String>) -> TokenStream {
let tokens = match doc {
Some(text) => {
// Leading space matches the `/// text` desugaring rustfmt produces.
let spaced = format!(" {text}");
quote! { #[doc = #spaced] }
}
Some(text) => doc_lines(std::slice::from_ref(text)),
None => quote! {},
};
return tokens;
Expand Down
2 changes: 2 additions & 0 deletions crates/oapi-codegen/src/emit/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,7 @@ fn emit_alias(alias: &Alias) -> Result<TokenStream> {
#[cfg(test)]
mod tests {
use super::*;
use crate::ir::Access;
use crate::naming::Case;
use crate::naming::to_ident;

Expand All @@ -536,6 +537,7 @@ mod tests {
serde_skip: false,
default: Some(DefaultValue::Int(10)),
constraints: None,
access: Access::ReadWrite,
}],
additional_properties: None,
deny_unknown_fields: false,
Expand Down
38 changes: 21 additions & 17 deletions crates/oapi-codegen/src/emit/usage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use std::collections::HashMap;
use crate::emit::Targets;
use crate::emit::models::ModelDerives;
use crate::emit::models::SerdeDerives;
use crate::ir::Direction;
use crate::ir::ForeignDerives;
use crate::ir::Item;
use crate::ir::Module;
Expand All @@ -32,11 +33,11 @@ use crate::naming::to_ident;

/// Whether a model is reachable as a request payload and/or a response payload.
#[derive(Debug, Default, Clone, Copy)]
struct Usage {
pub(crate) struct Usage {
/// Reachable from a request body or request-input struct.
request: bool,
pub(crate) request: bool,
/// Reachable from a response body.
response: bool,
pub(crate) response: bool,
}

/// Compute the derive set for every generated model, keyed by its logical name.
Expand All @@ -46,14 +47,7 @@ struct Usage {
pub(crate) fn model_derives(module: &Module, service: &Service, targets: Targets) -> HashMap<String, ModelDerives> {
let adjacency = adjacency(module);
let foreign = foreign_derives(module, &adjacency);
let mut usage: HashMap<String, Usage> = HashMap::new();

for name in request_seeds(service) {
mark(&adjacency, &name, &mut usage, Direction::Request);
}
for name in response_seeds(service) {
mark(&adjacency, &name, &mut usage, Direction::Response);
}
let usage = direction_usage(module, service);

// Union of both keys. A model can be constrained by a foreign type without
// being reachable from any operation, and the other way round, so taking only
Expand Down Expand Up @@ -264,11 +258,19 @@ fn item_types(item: &Item) -> Vec<RustType> {
return types;
}

/// The direction a seed propagates.
#[derive(Debug, Clone, Copy)]
enum Direction {
Request,
Response,
/// Which direction reaches each model, keyed by its logical name.
///
/// An absent name is reached by no operation, which happens under `skip-prune`.
pub(crate) fn direction_usage(module: &Module, service: &Service) -> HashMap<String, Usage> {
let adjacency = adjacency(module);
let mut usage: HashMap<String, Usage> = HashMap::new();
for name in request_seeds(service) {
mark(&adjacency, &name, &mut usage, Direction::Request);
}
for name in response_seeds(service) {
mark(&adjacency, &name, &mut usage, Direction::Response);
}
return usage;
}

/// Mark `start` and every model reachable from it with `direction`, following
Expand Down Expand Up @@ -301,7 +303,7 @@ fn mark(

/// Build the model-reference graph: each item name mapped to the names of the
/// generated models it references through its fields, variants, or alias target.
fn adjacency(module: &Module) -> HashMap<String, Vec<String>> {
pub(crate) fn adjacency(module: &Module) -> HashMap<String, Vec<String>> {
let mut graph = HashMap::with_capacity(module.items.len());
for item in &module.items {
graph.insert(item.name().to_owned(), item_references(item));
Expand Down Expand Up @@ -427,6 +429,7 @@ fn struct_field_names(strukt: &Struct, out: &mut Vec<String>) {
#[cfg(test)]
mod tests {
use super::*;
use crate::ir::Access;
use crate::ir::Alias;
use crate::ir::Body;
use crate::ir::Field;
Expand Down Expand Up @@ -457,6 +460,7 @@ mod tests {
serde_skip: false,
default: None,
constraints: None,
access: Access::ReadWrite,
};
}

Expand Down
45 changes: 45 additions & 0 deletions crates/oapi-codegen/src/ir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,51 @@ pub struct Field {
/// The generator checks these on the way in, so the check runs only where
/// the code deserializes. A response the server writes is not checked.
pub constraints: Option<Constraints>,
/// Which direction of an exchange carries the property, from `readOnly` and
/// `writeOnly`.
pub access: Access,
}
Comment thread
dotkas marked this conversation as resolved.

/// One direction of an exchange.
///
/// A request travels from the client to the server. A response travels back.
/// The direction does not name a serde trait, because a server deserializes a
/// request and serializes a response, and a client does the opposite.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Direction {
/// The payload an operation reads: parameters and the request body.
Request,
/// The payload an operation writes: the response body and its headers.
Response,
}

/// Which direction of an exchange carries a property.
///
/// `readOnly` gives [`Access::ReadOnly`] and `writeOnly` gives
/// [`Access::WriteOnly`]. [`crate::lower::direction`] then drops a property that
/// the direction of a shape does not carry.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
#[non_exhaustive]
pub enum Access {
/// Both directions carry the property.
#[default]
ReadWrite,
/// A response carries the property, and a request must not.
ReadOnly,
/// A request carries the property, and a response must not.
WriteOnly,
}

impl Access {
/// Whether `direction` carries a property with this access.
pub fn carried_by(self, direction: Direction) -> bool {
return match (self, direction) {
(Access::ReadWrite, _) => true,
(Access::ReadOnly, Direction::Response) | (Access::WriteOnly, Direction::Request) => true,
(Access::ReadOnly, Direction::Request) | (Access::WriteOnly, Direction::Response) => false,
};
}
}

/// A numeric bound, in the form the document writes it.
Expand Down
5 changes: 5 additions & 0 deletions crates/oapi-codegen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ fn lower_spec(spec_path: &Path, config: &Config) -> Result<Lowered> {
.unwrap_or(crate::config::DEFAULT_RESPONSE_SUFFIX);
let mut service = lower::generate_service(&spec, &config.import_mapping, response_type_suffix)?;
lower::rewrite_service(&mut service, names.renames());
// Runs before the prune pass, which drops a shape no operation reaches,
// and before the name checks, which then see the projected names.
lower::split_by_direction(&mut module, Some(&mut service));
if !config.output_options.skip_prune {
lower::prune_unused_models(&mut module, &service);
}
Expand Down Expand Up @@ -128,6 +131,7 @@ fn lower_spec(spec_path: &Path, config: &Config) -> Result<Lowered> {
}
// Models-only generation prunes nothing, so the module holds every schema and
// every collision reports.
lower::split_by_direction(&mut module, None);
names.check_emitted(&module)?;
lower::check_duplicate_models(&module)?;
lower::check_prelude_shadowing(&module, emit::Targets::default())?;
Expand Down Expand Up @@ -216,6 +220,7 @@ pub fn generate_models_string(spec_path: &Path) -> Result<String> {
let spec = Spec::load(spec_path)?;
let names = lower::type_renames(&spec, None)?;
let mut module = lower::generate_models(&spec, &names)?;
lower::split_by_direction(&mut module, None);
// Every schema becomes an item here, so every collision reaches the file.
names.check_emitted(&module)?;
lower::check_duplicate_models(&module)?;
Expand Down
Loading