diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..172c6a8 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +rustdocflags = ["-Dwarnings"] diff --git a/AGENTS.md b/AGENTS.md index fdf75ed..b14b7f9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,11 +11,14 @@ After making changes, **always** run in order: ```bash cargo check --workspace cargo test --workspace --no-fail-fast --all-features +cargo doc --workspace --no-deps cargo clippy --workspace --all-targets --fix --allow-dirty --allow-staged --no-deps # Auto-fixes lint suggestions cargo +nightly fmt --all ``` -**Task is not complete until all commands pass.** If any fails: fix, re-run from step 1, repeat. +Proofread new documentation and comments for tone and style. + +**Task is not complete until all commands pass and proofreader approves.** If any fails: fix, re-run from step 1, repeat. **If failing 3+ times:** Stop, re-read errors carefully, check if failure is in your code or pre-existing, ask for guidance if stuck. @@ -94,10 +97,16 @@ struct MyView { - `Box` only to break recursive types; `Vec`/`HashMap` provide their own indirection. - `.collect_vec()` (from `itertools`) instead of `.collect::>()` or `let v: Vec<_> = … .collect()`. -### Documentation (`///`) +### Documentation (`///` and `//!`) +**Tone:** +- Match Rust stdlib's tone: precise, terse, declarative. No filler, no hedging, no marketing. +- Proofread with Strunk & White's *Elements of Style*: omit needless words, use active voice, prefer positive statements. + +**Style rules:** - Complete sentences, indicative mood ("Returns", not "Return"), backticks for code items. - Describe args/returns in prose, never separate sections. +- Document interface (what/why), not implementation details (how). - Wrap at 80 chars. ```rust diff --git a/ploidy-core/src/codegen/mod.rs b/ploidy-core/src/codegen/mod.rs index ed5186e..9a20da1 100644 --- a/ploidy-core/src/codegen/mod.rs +++ b/ploidy-core/src/codegen/mod.rs @@ -1,3 +1,29 @@ +//! Code generation output and file writing. +//! +//! This module defines the [`Code`] trait, which represents a +//! single generated output file with a relative path and a +//! content string. +//! +//! [`IntoCode`] converts codegen types into [`Code`]. Any type +//! that implements [`Code`] automatically implements +//! [`IntoCode`], so codegen types can implement either trait. +//! +//! [`write_to_disk`] takes an output directory and any [`IntoCode`] +//! value, creates intermediate directories as needed, and writes the file. +//! +//! # Feature-gated blanket implementations +//! +//! Two blanket implementations of [`Code`] are available behind +//! feature flags: +//! +//! - **`proc-macro2`**: `(T, TokenStream)` where `T: AsRef` +//! formats the token stream with [prettyplease] and writes it +//! to the path given by `T`. +//! - **`cargo_toml`**: `(&str, Manifest)` serializes a Cargo +//! manifest to TOML and writes it to the given path. +//! +//! [prettyplease]: https://docs.rs/prettyplease/latest/prettyplease/ + use std::path::Path; use miette::{Context, IntoDiagnostic}; diff --git a/ploidy-core/src/ir/graph.rs b/ploidy-core/src/ir/graph.rs index 9ff26d0..a7af73a 100644 --- a/ploidy-core/src/ir/graph.rs +++ b/ploidy-core/src/ir/graph.rs @@ -27,7 +27,7 @@ use super::{ GraphInlineType, GraphOperation, GraphSchemaType, GraphStruct, GraphTagged, GraphTaggedVariant, GraphType, InlineTypePath, InlineTypePathRoot, InlineTypePathSegment, SchemaTypeInfo, SpecInlineType, SpecSchemaType, SpecType, SpecUntaggedVariant, - StructFieldName, TypeMapper, + StructFieldName, mapper::TypeMapper, }, views::{operation::OperationView, primitive::PrimitiveView, schema::SchemaTypeView}, }; @@ -571,7 +571,7 @@ pub enum EdgeKind { /// Precomputed metadata for schema types and operations in the graph. #[derive(Debug, Default)] -pub struct CookedGraphMetadata<'a> { +pub(super) struct CookedGraphMetadata<'a> { /// Maps each node index to its strongly connected component index. /// Nodes in the same SCC form a cycle. pub scc_indices: Vec, @@ -582,7 +582,7 @@ pub struct CookedGraphMetadata<'a> { /// Precomputed metadata for an operation that references /// types in the graph. #[derive(Debug, Default)] -pub struct GraphOperationMeta { +pub(super) struct GraphOperationMeta { /// Indices of all the types that this operation directly depends on: /// parameters, request body, and response body. pub types: FixedBitSet, diff --git a/ploidy-core/src/ir/mod.rs b/ploidy-core/src/ir/mod.rs index 19cc53f..7b586ac 100644 --- a/ploidy-core/src/ir/mod.rs +++ b/ploidy-core/src/ir/mod.rs @@ -1,9 +1,31 @@ +//! Intermediate representation for OpenAPI code generation. +//! +//! The IR has three layers: +//! +//! * **Types** define the data types that all other layers share. +//! Each type shape is parameterized over _how it references other types_: +//! spec types hold unresolved JSON Pointer references, while graph types +//! hold resolved node indices. +//! +//! * A [`Spec`] is a tree of those data types, lowered from +//! a parsed [`Document`], with references still unresolved. +//! +//! * The **graph** resolves those references into a dependency graph. +//! [`RawGraph`] is the mutable form used for in-place transformations; +//! [`CookedGraph`] is the frozen form used for traversal and codegen. +//! +//! The [`views`] module wraps cooked graph nodes in read-only view types +//! that expose traversal and metadata. See that module and the +//! [crate root](crate) for usage. +//! +//! [`Document`]: crate::parse::Document + mod error; mod graph; mod spec; mod transform; mod types; -mod views; +pub mod views; #[cfg(test)] mod tests; @@ -13,6 +35,6 @@ pub use spec::Spec; pub use types::*; pub use views::{ - ExtendableView, Reach, View, container::*, enum_::*, inline::*, ir::*, operation::*, + ExtendableView, Reach, View, any::*, container::*, enum_::*, inline::*, ir::*, operation::*, primitive::*, schema::*, struct_::*, tagged::*, untagged::*, }; diff --git a/ploidy-core/src/ir/spec.rs b/ploidy-core/src/ir/spec.rs index a345148..8730694 100644 --- a/ploidy-core/src/ir/spec.rs +++ b/ploidy-core/src/ir/spec.rs @@ -21,14 +21,29 @@ use super::{ }, }; +/// The intermediate representation of an OpenAPI document. +/// +/// A [`Spec`] is a type tree lowered from a parsed document, with references +/// still unresolved. Construct one with [`Spec::from_doc()`], then pass it to +/// [`RawGraph::new()`] to build the type graph. +/// +/// [`RawGraph::new()`]: crate::ir::RawGraph::new #[derive(Debug)] pub struct Spec<'a> { + /// The document's `info` section: title, OpenAPI version, etc. pub info: &'a Info, + /// All operations extracted from the document's `paths` section. pub operations: Vec>, + /// Named schemas from `components/schemas`, keyed by name. pub schemas: IndexMap<&'a str, SpecType<'a>>, } impl<'a> Spec<'a> { + /// Builds a [`Spec`] from a parsed OpenAPI [`Document`]. + /// + /// Lowers each schema and operation to IR types, allocating all + /// long-lived data in the `arena`. Returns an error if the document is + /// malformed. pub fn from_doc(arena: &'a Arena, doc: &'a Document) -> Result { let schemas = match &doc.components { Some(components) => components @@ -279,9 +294,9 @@ impl<'a> Spec<'a> { }) } - /// Dereferences a [`SpecType`], following type references through the spec. + /// Resolves a [`SpecType`], following type references through the spec. #[inline] - pub fn resolve(&'a self, mut ty: &'a SpecType<'a>) -> ResolvedSpecType<'a> { + pub(super) fn resolve(&'a self, mut ty: &'a SpecType<'a>) -> ResolvedSpecType<'a> { loop { match ty { SpecType::Schema(ty) => return ResolvedSpecType::Schema(ty), @@ -295,11 +310,11 @@ impl<'a> Spec<'a> { /// A dereferenced type in the spec. /// /// The derived [`Eq`] and [`Hash`][std::hash::Hash] implementations -/// work on the underlying values, so structurally identical types -/// compare and hash equal. This is important: all types in a [`Spec`] -/// are distinct in memory, but can refer to the same logical type. +/// use structural equality, not pointer identity. Multiple [`SpecType`]s +/// in a [`Spec`] may resolve to the same logical type, so value-based +/// comparison is necessary. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -pub enum ResolvedSpecType<'a> { +pub(super) enum ResolvedSpecType<'a> { Schema(&'a SpecSchemaType<'a>), Inline(&'a SpecInlineType<'a>), } diff --git a/ploidy-core/src/ir/types/mapper.rs b/ploidy-core/src/ir/types/mapper.rs index 9d99c41..3d23927 100644 --- a/ploidy-core/src/ir/types/mapper.rs +++ b/ploidy-core/src/ir/types/mapper.rs @@ -11,6 +11,9 @@ use crate::ir::types::shape::{ /// Translates shape types parameterized by `T` references into /// shape types parameterized by `U` references. +/// +/// Mapping isn't recursive: the mapper translates references within +/// each type, but doesn't follow them to map the types they point to. pub struct TypeMapper<'a, F, T, U> { arena: &'a Arena, map: F, diff --git a/ploidy-core/src/ir/types/mod.rs b/ploidy-core/src/ir/types/mod.rs index 86a2199..9cb5c04 100644 --- a/ploidy-core/src/ir/types/mod.rs +++ b/ploidy-core/src/ir/types/mod.rs @@ -7,10 +7,10 @@ use std::{ use crate::arena::Arena; -pub use self::{graph::*, mapper::*, spec::*}; +pub use self::{graph::*, spec::*}; mod graph; -mod mapper; +pub(crate) mod mapper; pub mod shape; mod spec; diff --git a/ploidy-core/src/ir/types/shape.rs b/ploidy-core/src/ir/types/shape.rs index 65b7718..7266c92 100644 --- a/ploidy-core/src/ir/types/shape.rs +++ b/ploidy-core/src/ir/types/shape.rs @@ -1,8 +1,8 @@ //! Generic structural shapes for IR types, parameterized over //! the type reference representation. //! -//! Prefer the [spec and graph type aliases][super], unless -//! you're writing generic code that abstracts over type references. +//! Prefer the spec and graph type aliases, unless you're +//! writing generic code that abstracts over type references. use crate::parse::{Method, path::PathSegment}; diff --git a/ploidy-core/src/ir/views/any.rs b/ploidy-core/src/ir/views/any.rs index 873073d..d4432f9 100644 --- a/ploidy-core/src/ir/views/any.rs +++ b/ploidy-core/src/ir/views/any.rs @@ -1,3 +1,10 @@ +//! Any type: a schema with no type constraints. +//! +//! An [`AnyView`] represents an arbitrary JSON value: a schema without +//! `type`, `properties`, or composition keywords. Codegen maps this to a +//! dynamic type in the target language, like [`serde_json::Value`] in Rust, +//! `Any` in Python, or `any` in TypeScript. + use petgraph::graph::NodeIndex; use crate::ir::CookedGraph; diff --git a/ploidy-core/src/ir/views/container.rs b/ploidy-core/src/ir/views/container.rs index 7659e88..8cc9172 100644 --- a/ploidy-core/src/ir/views/container.rs +++ b/ploidy-core/src/ir/views/container.rs @@ -1,3 +1,36 @@ +//! Container types: arrays, maps, and optionals. +//! +//! In OpenAPI, `type: array` with `items` defines a list, +//! and `type: object` without `properties` and with +//! `additionalProperties` defines a map. Schemas with +//! `nullable: true` (OpenAPI 3.0), `type: [T, "null"]` +//! (OpenAPI 3.1), or `oneOf` with a `null` branch all +//! become optionals: +//! +//! ```yaml +//! components: +//! schemas: +//! Tags: +//! type: array +//! items: +//! type: string +//! Metadata: +//! type: object +//! additionalProperties: +//! type: string +//! NullableName: +//! type: [string, null] +//! ``` +//! +//! Ploidy represents all three as [`ContainerView`] variants— +//! [`Array`][array], [`Map`][map], and [`Optional`][opt]— +//! each wrapping an [`InnerView`] that provides access to +//! the contained type. +//! +//! [array]: ContainerView::Array +//! [map]: ContainerView::Map +//! [opt]: ContainerView::Optional + use petgraph::graph::NodeIndex; use crate::ir::{ @@ -37,7 +70,7 @@ impl<'a> ViewNode<'a> for ContainerView<'a> { } } -/// A graph-aware view of the inner type of a [`Container`]. +/// A graph-aware view of the inner type of a [container][ContainerView]. #[derive(Debug)] pub struct InnerView<'a> { cooked: &'a CookedGraph<'a>, diff --git a/ploidy-core/src/ir/views/enum_.rs b/ploidy-core/src/ir/views/enum_.rs index 170a24c..9d711a7 100644 --- a/ploidy-core/src/ir/views/enum_.rs +++ b/ploidy-core/src/ir/views/enum_.rs @@ -1,3 +1,20 @@ +//! Enum types: schemas with a fixed set of literal values. +//! +//! In OpenAPI, a schema with `enum` restricts a value to +//! one of a fixed set of literals: +//! +//! ```yaml +//! components: +//! schemas: +//! Status: +//! type: string +//! enum: [active, paused, canceled] +//! ``` +//! +//! Ploidy represents this as an [`EnumView`]. Each variant carries a +//! literal value: string, number, or boolean. See [`EnumVariant`] +//! for the full set. + use petgraph::graph::NodeIndex; use crate::ir::{ @@ -25,11 +42,13 @@ impl<'a> EnumView<'a> { Self { cooked, index, ty } } + /// Returns the description, if present in the schema. #[inline] pub fn description(&self) -> Option<&'a str> { self.ty.description } + /// Returns the enum's variants. #[inline] pub fn variants(&self) -> &'a [EnumVariant<'a>] { self.ty.variants diff --git a/ploidy-core/src/ir/views/inline.rs b/ploidy-core/src/ir/views/inline.rs index 2221c81..2a234d0 100644 --- a/ploidy-core/src/ir/views/inline.rs +++ b/ploidy-core/src/ir/views/inline.rs @@ -1,3 +1,29 @@ +//! Inline types. +//! +//! [`InlineTypeView`] mirrors [`SchemaTypeView`][schema] for anonymous schemas +//! that are nested inside other schemas or operations: +//! +//! ```yaml +//! components: +//! schemas: +//! Pet: +//! type: object +//! properties: +//! address: +//! type: object +//! properties: +//! street: +//! type: string +//! ``` +//! +//! Here, `address` isn't a named schema in `components/schemas`, so Ploidy +//! assigns it the inline path `Type("Pet") / Field("address")`. Each +//! [`InlineTypeView`] variant pairs an OpenAPI type view with an +//! [`InlineTypePath`] like this one, which codegen uses to derive a +//! stable generated name. +//! +//! [schema]: super::schema::SchemaTypeView + use petgraph::graph::NodeIndex; use crate::ir::{InlineTypePath, graph::CookedGraph, types::GraphInlineType}; @@ -47,6 +73,8 @@ impl<'a> InlineTypeView<'a> { } } + /// Returns the path describing where this inline type was found + /// in the spec. #[inline] pub fn path(&self) -> InlineTypePath<'a> { let (Self::Enum(path, _) diff --git a/ploidy-core/src/ir/views/ir.rs b/ploidy-core/src/ir/views/ir.rs index 65b6ece..b1fdbd1 100644 --- a/ploidy-core/src/ir/views/ir.rs +++ b/ploidy-core/src/ir/views/ir.rs @@ -1,3 +1,10 @@ +//! An OpenAPI type. +//! +//! A [`TypeView`] represents an arbitrary type in the graph. Each type is +//! either a named top-level [`Schema`][SchemaTypeView] definition from +//! `components/schemas`, or an anonymous [`Inline`][InlineTypeView] schema +//! nested inside another type or operation. + use petgraph::graph::NodeIndex; use crate::ir::{graph::CookedGraph, types::GraphType}; diff --git a/ploidy-core/src/ir/views/mod.rs b/ploidy-core/src/ir/views/mod.rs index c9f9d92..a957675 100644 --- a/ploidy-core/src/ir/views/mod.rs +++ b/ploidy-core/src/ir/views/mod.rs @@ -1,8 +1,29 @@ //! Graph-aware views of IR types. //! -//! These views provide a representation of schema and inline types -//! for code generation. Each view decorates an IR type with -//! additional information from the type graph. +//! Views are cheap, read-only types that pair a node with its cooked graph, +//! so they can answer questions about an IR type and its relationships. +//! The submodules document the OpenAPI concept that each view represents. +//! +//! # The `View` trait +//! +//! All view types implement [`View`], which provides graph traversal methods: +//! +//! * [`View::inlines()`] iterates over inline types nested within this type. +//! Use this to emit inline type definitions alongside their parent. +//! * [`View::used_by()`] iterates over operations that reference this type. +//! Useful for generating per-operation imports or feature gates. +//! * [`View::dependencies()`] iterates over all types that this type +//! transitively depends on. Use this for import lists, feature gates, +//! and topological ordering. +//! * [`View::dependents()`] iterates over all types that transitively depend on +//! this type. Useful for impact analysis or invalidation. +//! * [`View::traverse()`] traverses the graph breadth-first with a filter that +//! controls which nodes to yield and explore. +//! +//! # Extensions +//! +//! [`ExtendableView`] attaches a type-erased extension map to each view node. +//! Codegen backends use this to store and retrieve arbitrary metadata. use std::{any::TypeId, fmt::Debug}; @@ -75,6 +96,11 @@ pub trait View<'a> { F: Fn(EdgeKind, &TypeView<'a>) -> Traversal; } +/// A view of a graph type with extended data. +/// +/// Codegen backends use extended data to decorate types with extra information. +/// For example, Rust codegen stores a unique identifier on each schema type, +/// so that names never collide after case conversion. pub trait ExtendableView<'a>: View<'a> { /// Returns a reference to this type's extended data. fn extensions(&self) -> &ViewExtensions @@ -175,34 +201,12 @@ where } } -pub trait ViewNode<'a> { +pub(crate) trait ViewNode<'a> { fn cooked(&self) -> &'a CookedGraph<'a>; fn index(&self) -> NodeIndex; } -pub trait Extendable<'graph> { - // These lifetime requirements might look redundant, but they're not: - // we're shortening the lifetime of the `AtomicRef` from `'graph` to `'view`, - // to prevent overlapping mutable borrows of the underlying `AtomicRefCell` - // at compile time. - // - // (`AtomicRefCell` panics on these illegal borrows at runtime, which is - // always memory-safe; we just want some extra type safety). - // - // This approach handles the obvious case of overlapping borrows from - // `ext()` and `ext_mut()`, and the `AtomicRefCell` avoids plumbing - // mutable references to the graph through every IR layer. - - fn ext<'view>(&'view self) -> AtomicRef<'view, ExtensionMap> - where - 'graph: 'view; - - fn ext_mut<'view>(&'view mut self) -> AtomicRefMut<'view, ExtensionMap> - where - 'graph: 'view; -} - -impl<'graph, T> Extendable<'graph> for T +impl<'graph, T> internal::Extendable<'graph> for T where T: ViewNode<'graph>, { @@ -227,11 +231,7 @@ where } } -/// Extended data attached to a type in the graph. -/// -/// Generators can use extended data to decorate types with extra information, -/// like name mappings. For example, the Rust generator stores a normalized, -/// deduplicated identifier name on every named schema type. +/// Extended data attached to a graph type. #[derive(RefCastCustom)] #[repr(transparent)] pub struct ViewExtensions(X); @@ -244,7 +244,7 @@ impl ViewExtensions { fn new_mut(view: &mut X) -> &mut Self; } -impl<'a, X: Extendable<'a>> ViewExtensions { +impl<'a, X: internal::Extendable<'a>> ViewExtensions { /// Returns a reference to a value of an arbitrary type that was /// previously inserted into this extended data. #[inline] @@ -287,3 +287,31 @@ pub enum Reach { /// Traverse in toward types that depend on this node. Dependents, } + +mod internal { + use atomic_refcell::{AtomicRef, AtomicRefMut}; + + use super::ExtensionMap; + + pub trait Extendable<'graph> { + // These lifetime requirements might look redundant, but they're not: + // we're shortening the lifetime of the `AtomicRef` from `'graph` to `'view`, + // to prevent overlapping mutable borrows of the underlying `AtomicRefCell` + // at compile time. + // + // (`AtomicRefCell` panics on these illegal borrows at runtime, which is + // always memory-safe; we just want some extra type safety). + // + // This approach handles the obvious case of overlapping borrows from + // `ext()` and `ext_mut()`, and the `AtomicRefCell` avoids plumbing + // mutable references to the graph through every IR layer. + + fn ext<'view>(&'view self) -> AtomicRef<'view, ExtensionMap> + where + 'graph: 'view; + + fn ext_mut<'view>(&'view mut self) -> AtomicRefMut<'view, ExtensionMap> + where + 'graph: 'view; + } +} diff --git a/ploidy-core/src/ir/views/operation.rs b/ploidy-core/src/ir/views/operation.rs index 44da15b..708e678 100644 --- a/ploidy-core/src/ir/views/operation.rs +++ b/ploidy-core/src/ir/views/operation.rs @@ -1,3 +1,59 @@ +//! Operations: per-path methods with parameter, request, and response schemas. +//! +//! In OpenAPI, each path item defines operations for HTTP methods like +//! `GET` and `POST`. An operation has path and query parameters, an +//! optional request body, and an optional response body: +//! +//! ```yaml +//! paths: +//! /pets/{pet_id}: +//! post: +//! operationId: updatePet +//! parameters: +//! - name: pet_id +//! in: path +//! required: true +//! schema: +//! type: string +//! - name: expand +//! in: query +//! schema: +//! type: boolean +//! requestBody: +//! content: +//! application/json: +//! schema: +//! $ref: '#/components/schemas/PetUpdate' +//! responses: +//! '200': +//! content: +//! application/json: +//! schema: +//! $ref: '#/components/schemas/Pet' +//! ``` +//! +//! Ploidy represents this as an [`OperationView`] with: +//! +//! * An [ID], an [HTTP method], and a [path template] with +//! segments and path parameters. +//! * [Query parameters], each with a name, type, and +//! optional serialization style. +//! * An optional [request] and [response] body, each wrapping +//! a [`TypeView`] of the body schema. +//! * An optional [resource name] from the `x-resource-name` extension, +//! used to group operations by resource. +//! +//! Unlike types, operations are not nodes in Ploidy's dependency graph, +//! but they implement [`View`] for traversal. +//! +//! [ID]: OperationView::id +//! [HTTP method]: OperationView::method +//! [path template]: OperationView::path +//! [Query parameters]: OperationView::query +//! [request]: OperationView::request +//! [response]: OperationView::response +//! [resource name]: OperationView::resource + use std::marker::PhantomData; use enum_map::enum_map; @@ -34,21 +90,25 @@ impl<'a> OperationView<'a> { Self { cooked, op } } + /// Returns the `operationId`. #[inline] pub fn id(&self) -> &'a str { self.op.id } + /// Returns the HTTP method. #[inline] pub fn method(&self) -> Method { self.op.method } + /// Returns a view of this operation's path template. #[inline] pub fn path(&self) -> OperationViewPath<'_, 'a> { OperationViewPath(self) } + /// Returns the description, if present in the spec. #[inline] pub fn description(&self) -> Option<&'a str> { self.op.description @@ -192,6 +252,7 @@ impl<'a> View<'a> for OperationView<'a> { pub struct OperationViewPath<'view, 'a>(&'view OperationView<'a>); impl<'view, 'a> OperationViewPath<'view, 'a> { + /// Returns an iterator over this path's segments. #[inline] pub fn segments(self) -> std::slice::Iter<'view, PathSegment<'a>> { self.0.op.path.iter() @@ -232,21 +293,25 @@ impl<'a, T> ParameterView<'a, T> { } } + /// Returns the parameter name. #[inline] pub fn name(&self) -> &'a str { self.info.name } + /// Returns a view of the parameter's type. #[inline] pub fn ty(&self) -> TypeView<'a> { TypeView::new(self.cooked, self.info.ty) } + /// Returns `true` if this parameter is required. #[inline] pub fn required(&self) -> bool { self.info.required } + /// Returns the serialization style, if specified. #[inline] pub fn style(&self) -> Option { self.info.style diff --git a/ploidy-core/src/ir/views/primitive.rs b/ploidy-core/src/ir/views/primitive.rs index 9a77a7a..d070c63 100644 --- a/ploidy-core/src/ir/views/primitive.rs +++ b/ploidy-core/src/ir/views/primitive.rs @@ -1,3 +1,10 @@ +//! Primitives: scalar types. +//! +//! Primitives are leaf nodes that don't reference other types in the graph. +//! Codegen maps each [`PrimitiveType`] variant to a language-specific type: +//! for example, [`PrimitiveType::String`] becomes a [`String`] in Rust. +//! See [`PrimitiveType`] for the full list of variants. + use petgraph::graph::NodeIndex; use crate::ir::{CookedGraph, PrimitiveType}; diff --git a/ploidy-core/src/ir/views/schema.rs b/ploidy-core/src/ir/views/schema.rs index 37e7121..1c373ea 100644 --- a/ploidy-core/src/ir/views/schema.rs +++ b/ploidy-core/src/ir/views/schema.rs @@ -1,3 +1,10 @@ +//! Named schema types. +//! +//! A [`SchemaTypeView`] pairs each OpenAPI type view with a [`SchemaTypeInfo`], +//! which carries the schema's name and optional `x-resourceId` for grouping. +//! Ploidy extracts named schema types from the `components/schemas` section +//! of the source [`Spec`][crate::ir::Spec]. + use petgraph::graph::NodeIndex; use crate::ir::{SchemaTypeInfo, graph::CookedGraph, types::GraphSchemaType}; @@ -47,6 +54,7 @@ impl<'a> SchemaTypeView<'a> { } } + /// Returns the schema name from `components/schemas`. #[inline] pub fn name(&self) -> &'a str { let (Self::Enum(SchemaTypeInfo { name, .. }, ..) diff --git a/ploidy-core/src/ir/views/struct_.rs b/ploidy-core/src/ir/views/struct_.rs index fa95e6b..bc935af 100644 --- a/ploidy-core/src/ir/views/struct_.rs +++ b/ploidy-core/src/ir/views/struct_.rs @@ -1,3 +1,48 @@ +//! Struct types: object schemas and `allOf` composition. +//! +//! In OpenAPI, a `type: object` schema with `properties` describes a record +//! with named fields. A schema can also inherit fields from other schemas via +//! `allOf`, which is how OpenAPI models composition and inheritance: +//! +//! ```yaml +//! components: +//! schemas: +//! Address: +//! type: object +//! required: [city] +//! properties: +//! city: +//! type: string +//! zip: +//! type: string +//! Office: +//! allOf: +//! - $ref: '#/components/schemas/Address' +//! - type: object +//! required: [floor] +//! properties: +//! floor: +//! type: integer +//! ``` +//! +//! Ploidy represents both cases as a [`StructView`]. A struct has +//! its own fields plus fields inherited from its `allOf` parents. +//! Each field carries properties that guide codegen: +//! +//! * **Required vs. optional.** A field listed in `required` is +//! non-optional; others are wrapped in [`ContainerView::Optional`]. +//! * **Flattened.** Fields originating from `anyOf` parents are +//! flattened into the struct as optional fields. +//! * **Tag.** A field is a tag if its name matches the discriminator of a +//! [tagged union] that references this struct as a variant. +//! * **Indirection.** A field needs indirection (e.g., [`Box`] in Rust) +//! when it and any of its parent structs form a cycle in the type graph. +//! * **Inherited.** A field that comes from an `allOf` parent rather than +//! this struct's own `properties`. +//! +//! [`ContainerView::Optional`]: super::container::ContainerView::Optional +//! [tagged union]: super::tagged::TaggedView + use petgraph::{ Direction, graph::NodeIndex, @@ -31,6 +76,7 @@ impl<'a> StructView<'a> { Self { cooked, index, ty } } + /// Returns the description, if present in the schema. #[inline] pub fn description(&self) -> Option<&'a str> { self.ty.description @@ -118,6 +164,7 @@ pub struct StructFieldView<'view, 'a> { } impl<'view, 'a> StructFieldView<'view, 'a> { + /// Returns the field name. #[inline] pub fn name(&self) -> StructFieldName<'a> { self.field.name @@ -129,11 +176,13 @@ impl<'view, 'a> StructFieldView<'view, 'a> { TypeView::new(self.parent.cooked, self.field.ty) } + /// Returns `true` if this field is listed in `required`. #[inline] pub fn required(&self) -> bool { self.field.required } + /// Returns the description, if present in the schema. #[inline] pub fn description(&self) -> Option<&'a str> { self.field.description @@ -160,6 +209,8 @@ impl<'view, 'a> StructFieldView<'view, 'a> { .any(|neighbor| neighbor.tag == name) } + /// Returns `true` if this field is flattened from an + /// `anyOf` parent. #[inline] pub fn flattened(&self) -> bool { self.field.flattened diff --git a/ploidy-core/src/ir/views/tagged.rs b/ploidy-core/src/ir/views/tagged.rs index 42607e7..46b7c17 100644 --- a/ploidy-core/src/ir/views/tagged.rs +++ b/ploidy-core/src/ir/views/tagged.rs @@ -1,3 +1,38 @@ +//! Tagged unions: `oneOf` with a discriminator. +//! +//! In OpenAPI, a `oneOf` schema with a `discriminator` defines +//! a tagged union, where the discriminator property is a tag +//! that selects the concrete type: +//! +//! ```yaml +//! components: +//! schemas: +//! Shape: +//! oneOf: +//! - $ref: '#/components/schemas/Circle' +//! - $ref: '#/components/schemas/Square' +//! discriminator: +//! propertyName: kind +//! ``` +//! +//! The `kind` field in the JSON payload determines the variant. +//! Ploidy represents this as a [`TaggedView`] with a [tag] and [variants]. +//! Each [`TaggedVariantView`] carries: +//! +//! * A [name]: the discriminator value that selects this variant +//! (e.g., `"Circle"`). +//! * Optional [aliases]: additional discriminator values that +//! also select this variant, defined via the discriminator's +//! `mapping` in the spec. +//! * A [type]: the schema that the variant deserializes into, +//! accessed as a [`TypeView`]. +//! +//! [tag]: TaggedView::tag +//! [variants]: TaggedView::variants +//! [name]: TaggedVariantView::name +//! [aliases]: TaggedVariantView::aliases +//! [type]: TaggedVariantView::ty + use petgraph::graph::NodeIndex; use crate::ir::{ @@ -25,11 +60,13 @@ impl<'a> TaggedView<'a> { Self { cooked, index, ty } } + /// Returns the description, if present in the schema. #[inline] pub fn description(&self) -> Option<&'a str> { self.ty.description } + /// Returns the discriminator property name. #[inline] pub fn tag(&self) -> &'a str { self.ty.tag @@ -79,11 +116,15 @@ impl<'a> TaggedVariantView<'a> { } } + /// Returns the discriminator value that selects this + /// variant. #[inline] pub fn name(&self) -> &'a str { self.variant.name } + /// Returns additional discriminator values that also + /// select this variant. #[inline] pub fn aliases(&self) -> &'a [&'a str] { self.variant.aliases diff --git a/ploidy-core/src/ir/views/untagged.rs b/ploidy-core/src/ir/views/untagged.rs index 33e0d48..bf50e22 100644 --- a/ploidy-core/src/ir/views/untagged.rs +++ b/ploidy-core/src/ir/views/untagged.rs @@ -1,3 +1,30 @@ +//! Untagged unions: `type` arrays and `oneOf` without a discriminator. +//! +//! In OpenAPI, a `oneOf` schema without a `discriminator` defines +//! an untagged union: there's no explicit tag, so deserialization tries +//! each variant in order until one matches. In OpenAPI 3.1+, an array of +//! `type`s also expresses an untagged union: +//! +//! ```yaml +//! components: +//! schemas: +//! StringOrInt: +//! oneOf: +//! - type: string +//! - type: integer +//! DateOrUnix: +//! type: [string, integer] +//! format: date-time +//! ``` +//! +//! Ploidy represents this as an [`UntaggedView`] with a list of variants. +//! Each variant is either a typed value or `null`, modeled as +//! [`Option`]. The typed case pairs an +//! [`UntaggedVariantNameHint`] with a [`TypeView`]; the hint helps codegen +//! produce readable variant names when the schema has no explicit name. +//! +//! [`Option`]: SomeUntaggedVariant + use petgraph::graph::NodeIndex; use crate::ir::{ @@ -26,6 +53,7 @@ impl<'a> UntaggedView<'a> { Self { cooked, index, ty } } + /// Returns the description, if present in the schema. #[inline] pub fn description(&self) -> Option<&'a str> { self.ty.description @@ -74,8 +102,12 @@ impl<'view, 'a> UntaggedVariantView<'view, 'a> { } } +/// A non-`null` variant of an untagged union, pairing a name hint +/// with the variant's type. #[derive(Debug)] pub struct SomeUntaggedVariant<'a> { + /// A hint for generating a readable variant name. pub hint: UntaggedVariantNameHint, + /// A view of this variant's type. pub view: TypeView<'a>, } diff --git a/ploidy-core/src/lib.rs b/ploidy-core/src/lib.rs index 8d88ada..f2358af 100644 --- a/ploidy-core/src/lib.rs +++ b/ploidy-core/src/lib.rs @@ -1,3 +1,91 @@ +//! Language-agnostic intermediate representation and type graph +//! for OpenAPI code generation. +//! +//! **ploidy-core** transforms a parsed OpenAPI document into a +//! typed dependency graph that **ploidy-codegen-\*** backends +//! traverse to emit code. +//! +//! # Pipeline +//! +//! ```rust +//! # fn main() -> Result<(), Box> { +//! # let source = indoc::indoc! {" +//! # openapi: 3.0.0 +//! # info: +//! # title: Test API +//! # version: 1.0 +//! # "}; +//! use ploidy_core::{arena::Arena, ir::{RawGraph, Spec}, parse::Document}; +//! +//! let doc = Document::from_yaml(&source)?; +//! +//! let arena = Arena::new(); +//! let spec = Spec::from_doc(&arena, &doc)?; +//! let mut raw = RawGraph::new(&arena, &spec); +//! raw.inline_tagged_variants(); +//! let graph = raw.cook(); +//! +//! for view in graph.schemas() { /* ... */ } +//! for view in graph.operations() { /* ... */ } +//! # Ok(()) +//! # } +//! ``` +//! +//! # Arena +//! +//! An [`Arena`] is a bump allocator that owns all long-lived data. +//! Types throughout the pipeline hold borrowed references to other +//! arena-allocated types, making them cheaply copyable. Callers +//! create an arena at the start, and pass it to each constructor. +//! +//! # Named vs. inline types +//! +//! The IR distinguishes two kinds of types: +//! +//! - **Named schema types** originate from `components/schemas` +//! in the OpenAPI document. Each carries a [`SchemaTypeInfo`] with +//! the schema name and additional metadata. +//! - **Inline types** are anonymous schemas nested inside other types. +//! Each carries an [`InlineTypePath`](ir::InlineTypePath) that +//! encodes the type's position in the document. +//! +//! The two kinds carry different metadata, but share the same structural +//! shapes: [any], [containers], [enums], [primitives], [structs], +//! [tagged unions], and [untagged unions]. +//! +//! # Using the graph +//! +//! A [`RawGraph`] represents types and references as they exist in the +//! OpenAPI document. Transformations on this graph rewrite it in-place. +//! The transformed graph is then "cooked" into a [`CookedGraph`] that's +//! ready for codegen. +//! +//! [`CookedGraph::schemas()`] yields [`SchemaTypeView`]s. Match on the variant +//! to get the specific shape (e.g., `SchemaTypeView::Struct`) for generating +//! type models. +//! +//! [`CookedGraph::operations()`] yields [`OperationView`]s. Use these to +//! access paths, methods, query parameters, and request and response types +//! for generating client endpoints. +//! +//! See the [`ir::views`] module for all view types and traversal methods. +//! +//! [`Arena`]: arena::Arena +//! [`SchemaTypeInfo`]: ir::SchemaTypeInfo +//! [any]: ir::views::any +//! [containers]: ir::views::container +//! [enums]: ir::views::enum_ +//! [primitives]: ir::views::primitive +//! [structs]: ir::views::struct_ +//! [tagged unions]: ir::views::tagged +//! [untagged unions]: ir::views::untagged +//! [`RawGraph`]: ir::RawGraph +//! [`CookedGraph`]: ir::CookedGraph +//! [`CookedGraph::schemas()`]: ir::CookedGraph::schemas +//! [`SchemaTypeView`]: ir::SchemaTypeView +//! [`CookedGraph::operations()`]: ir::CookedGraph::operations +//! [`OperationView`]: ir::OperationView + #[macro_use] mod macros; diff --git a/ploidy-core/src/parse/path.rs b/ploidy-core/src/parse/path.rs index 3f440d9..5e15dae 100644 --- a/ploidy-core/src/parse/path.rs +++ b/ploidy-core/src/parse/path.rs @@ -13,7 +13,9 @@ type Input<'a> = Stateful<&'a str, &'a Arena>; /// Parses a path template, like `/v1/pets/{petId}/toy`. /// /// The grammar for path templating is adapted directly from -/// https://spec.openapis.org/oas/v3.2.0.html#x4-8-2-path-templating. +/// [the OpenAPI spec][spec]. +/// +/// [spec]: https://spec.openapis.org/oas/v3.2.0.html#x4-8-2-path-templating pub fn parse<'a>(arena: &'a Arena, input: &'a str) -> Result>, BadPath> { let stateful = Input { input, @@ -31,6 +33,7 @@ pub fn parse<'a>(arena: &'a Arena, input: &'a str) -> Result pub struct PathSegment<'input>(&'input [PathFragment<'input>]); impl<'input> PathSegment<'input> { + /// Returns the template fragments within this segment. pub fn fragments(&self) -> &'input [PathFragment<'input>] { self.0 } @@ -104,6 +107,7 @@ mod parser { } } +/// An error returned when a path template can't be parsed. #[derive(Debug, miette::Diagnostic, thiserror::Error)] #[error("invalid URL path template")] pub struct BadPath {