docs(core): Document ploidy-core; export views, AnyView.

This commit is contained in:
Lina Butler
2026-03-14 18:56:24 -07:00
parent 811276ce5d
commit b67ba463bb
23 changed files with 547 additions and 52 deletions
+2
View File
@@ -0,0 +1,2 @@
[build]
rustdocflags = ["-Dwarnings"]
+11 -2
View File
@@ -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<T>` only to break recursive types; `Vec`/`HashMap` provide their own indirection.
- `.collect_vec()` (from `itertools`) instead of `.collect::<Vec<_>>()` 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
+26
View File
@@ -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<str>`
//! formats the token stream with [prettyplease] and writes it
//! to the path given by `T`.
//! - **`cargo_toml`**: `(&str, Manifest<T>)` 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};
+3 -3
View File
@@ -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<usize>,
@@ -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,
+24 -2
View File
@@ -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::*,
};
+21 -6
View File
@@ -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<SpecOperation<'a>>,
/// 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<Self, IrError> {
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>),
}
+3
View File
@@ -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,
+2 -2
View File
@@ -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;
+2 -2
View File
@@ -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};
+7
View File
@@ -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;
+34 -1
View File
@@ -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>,
+19
View File
@@ -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
+28
View File
@@ -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, _)
+7
View File
@@ -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};
+61 -33
View File
@@ -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<Self>
@@ -175,34 +201,12 @@ where
}
}
pub trait ViewNode<'a> {
pub(crate) trait ViewNode<'a> {
fn cooked(&self) -> &'a CookedGraph<'a>;
fn index(&self) -> NodeIndex<usize>;
}
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>(X);
@@ -244,7 +244,7 @@ impl<X> ViewExtensions<X> {
fn new_mut(view: &mut X) -> &mut Self;
}
impl<'a, X: Extendable<'a>> ViewExtensions<X> {
impl<'a, X: internal::Extendable<'a>> ViewExtensions<X> {
/// 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;
}
}
+65
View File
@@ -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<ParameterStyle> {
self.info.style
+7
View File
@@ -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};
+8
View File
@@ -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, .. }, ..)
+51
View File
@@ -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<T>`] 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
+41
View File
@@ -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
+32
View File
@@ -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<SomeUntaggedVariant>`]. 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>`]: 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>,
}
+88
View File
@@ -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<dyn std::error::Error>> {
//! # 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;
+5 -1
View File
@@ -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<Vec<PathSegment<'a>>, BadPath> {
let stateful = Input {
input,
@@ -31,6 +33,7 @@ pub fn parse<'a>(arena: &'a Arena, input: &'a str) -> Result<Vec<PathSegment<'a>
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 {