mirror of
https://github.com/linabutler/ploidy
synced 2026-07-14 18:45:32 +00:00
docs(core): Document ploidy-core; export views, AnyView.
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
[build]
|
||||
rustdocflags = ["-Dwarnings"]
|
||||
@@ -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
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,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>),
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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, _)
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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, .. }, ..)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user