Expand description
Semantic model of Rust types for code generation.
The crate is organized around the code generation lifecycle: consumers
create a TypespaceBuilder from settings::Settings, assemble types
from the build module’s vocabulary, insert them with
TypespaceBuilder::insert, and call TypespaceBuilder::finalize to
produce a Typespace. A finalized typespace renders code via
Typespace::to_codespace and answers queries through the view
module’s types.
§Using Typespace
Here’s a small example, constructing a single named type:
use typespace::build::{Struct, StructProperty, StructPropertyState, Type};
use typespace::settings::Settings;
use typespace::{no_cycles, TypespaceBuilder, TypespaceTrait};
let settings = Settings::minimal()
.with_required_trait(TypespaceTrait::Serialize)
.with_required_trait(TypespaceTrait::Deserialize);
let mut builder = TypespaceBuilder::<String>::new(settings);
// Ids are the consumer's to choose. These two types are unnamed, so
// they render inline wherever they are referenced rather.
builder.insert("str".to_string(), Type::String)?;
builder.insert("u32".to_string(), Type::Integer("u32".to_string()))?;
let pet = Struct::new()
.name("Pet")
.properties([
StructProperty::new("name", "str".to_string()),
StructProperty::new("age", "u32".to_string())
.with_state(StructPropertyState::Default),
StructProperty::new("breed", "str".to_string())
.with_state(StructPropertyState::Optional),
])
.build()?;
builder.insert("Pet".to_string(), pet)?;
// finalize resolves traits and breaks containment cycles. This graph
// has no cycles, so `no_cycles` asserts none are found.
let code: codespace::Codespace =
builder.finalize(no_cycles)?.to_codespace();
let rust = code.into_stream();
The produces Rust code like this:
#[derive(::serde::Deserialize, ::serde::Serialize)]
pub struct Pet {
pub name: ::std::string::String,
#[serde(default)]
pub age: u32,
pub breed: Option<::std::string::String>,
}§Details
§Named and unnamed types
Typespace allows the construction of named and unnamed types. Named types
are custom type definitions that result in a generated type definition such
as a struct Foo { .. } or enum Bar { .. }; unnamed types include
anonymous tuples with a collection of types or a parameterized Vec. Named
types get their own generated type block with associated impl blocks;
unnamed types are rendered inline.
§Struct fields: optionality and defaults
A struct field (or a field of a struct-style enum variant) has several associated states.
- Required: the field must always be present; this is modeled as a bare
type with no special
serdeattributes. - Optional: the field may be absent; the specific
serdeattributes may depend onsettings::Settings. - Default: the field if absent takes its value from the
Defaultimpl for the field’s type (#[serde(default)]). - DefaultValue: the field if absent takes its value from the specific, specified value (that is produced by a generated function).
In addition, settings::Settings provides for special handling
of fields that are both Optional and represented by the Rust Option type.
Such a field may be absent, null, or another value. See
settings::OptionalNullable.
§Type defaults
In addition to a field having a default value, any generated struct or
enum type (a named type, as above) may have an explicit default value.
This causes an implementation of the Default trait to be generated for
the type (if Default is one of the output traits).
§Trait resolution
Consumers may specify relevant traits for the Rust code output. Required
traits are generated for each type. If a type is unable to satisfy that
requirement, construction of the Typespace fails during
TypespaceBuilder::finalize. Desired traits are generated if
possible–if a type is unable to implement a particular trait, that’s
ignored during finalize.
Trait resolution occurs in two main passes. A forward pass propagates required traits to all types and, in the case of failure, produces a list of all unsatisfiable conditions along with their reasons (for debugging). A reverse pass (i.e. from types that don’t implement a given trait to the types that refer to it) “poisons” desired traits so that they are absent from types whose transitive references wouldn’t support them.
§Breaking containment cycles
Also during finalization, containment cycles in the type graph are broken
by inserting Box types. No attempt is made to optimize exactly how cycles
are broken (e.g. to minimize the number of inserted Boxes), but in
practice the generated code has not suffered.
§Generation
Code generation has some nuances, enumerated here:
§Never vs. Absent
A Type::Never represents a type that can never be instantiated. It is
typically rendered as ::json_serde::Never (an enum with no variants).
However, if a Never type appears as an
Optional field in a struct (or
struct-like enum variant), it is rendered as ::json_serde::Absent to
ensure proper handling by serde and schemars.
§Special enums
A tagged enum composed only of unit variants is treated like a value;
the generated type implements each of the following traits (if they’re in
the specified trait set): Eq, PartialEq, Ord, PartialOrd, Hash,
Clone, Copy, Display, and FromStr.
An untagged enum composed exclusively of
Item variants may implement Display and
FromStr if the type for each item also does so.
§Generated modules
Some default values require the generation of a function to produce those
values. Those functions live in a generated defaults module.
With Settings::with_struct_builder enabled, the generated builder
machinery lives in a builder module.
If there are types that include implementations of fallible conversions, the
error mod is generated to contain the error.
§When Default requires serde_json
Types or fields may have associated default values. The value is
constructed by generated code explicitly (i.e. without Deserialize). The
exception is Native types. Since the construction of a native type
is–necessarily–beyond the knowledge of typespace, the generated code
constructs it with a call to serde_json::from_str.
§Dependencies of generated code
Rendered code can reference crates that typespace itself does not depend on. Crates containing generating code must declare them as dependencies. Which crates are needed depends on the constructs in the output:
-
serde, with the
derivefeature: required by generated structs, enums, newtype structs, unit structs, and tuple structs whose trait set holdsTypespaceTrait::SerializeorTypespaceTrait::Deserialize; each is emitted with serde derives or hand-writtenSerialize/Deserializeimpls. Settings that require neither trait produce no derive, no impl, and no#[serde(..)]attribute (and so no dependency). -
serde_json: required when the output contains a
build::Type::JsonValue(rendered as::serde_json::Value), in several situations that involve serializing or deserializing a type from a JSON value such as default value handling and deserializing various types (see above), or when aschemars::JsonSchemaimplementation requires it. -
regress: required if the output contains a
build::NewtypeStructwhosebuild::NewtypeConstraints::Stringcarries a pattern. Each pattern renders a::regress::Regexin aLazyLock, checked on conversion. -
jsonschema: required if the output contains a
build::NewtypeStructwhosebuild::NewtypeConstraints::JsonSchemacarries a schema. The conversion validates the value against that schema. -
json-serde: required if the output contains any of:
- a property with
build::StructPropertyState::Optionalwhose type is not anOption(deserialized with::json_serde::deserialize_some, which distinguishes an absent field from a present one and rejectsnull); - a property with
build::StructPropertyState::Optionalwhose type is anOption, whensettings::OptionalNullable::DoubleOptionis selected (also::json_serde::deserialize_some); - a
build::TupleStructwith arestfield (its serde impls use::json_serde::FlattenedSequenceSerializerand::json_serde::FlattenedSequenceDeserializer); - the
build::Type::Never(rendered as ::json_serde::Never or–if optional–::json_serde::Absent); - a custom type to represent a field whose value may be absent,
null, or a type value, specified with
settings::OptionalNullable::CustomType.
Absentis emitted (as needed) independent of what the trait set holds. The rest are only used forserde::Deserializeandserde::Serialize. - a property with
Generated code also reproduces, verbatim, every type path the consumer
supplies: the name of a build::Native. For example, a converter might
inject uuid::Uuid or chrono types. The crates behind those paths are
dependencies chosen by the consumer that builds the typespace, not by
typespace, and the consumer should document them.
Modules§
- build
- Construction-side vocabulary for assembling a typespace.
- error
- The error type, and the detail types its messages are built from.
- settings
- Settings that govern how types are processed and rendered.
- view
- Query-side views of a finalized
Typespace.
Structs§
- Typespace
- A finalized, validated collection of types.
- Typespace
Builder - Accumulator for the type graph prior to finalization.
- Typespace
Trait Iter - An iterator over the variants of TypespaceTrait
- Typespace
Trait Set - An unordered collection of
TypespaceTraitvalues.
Enums§
- Trait
Provision - What a
build::Nativeor a configured container (settings::ContainerType) declares about one trait. - Typespace
Trait - A trait that typespace tracks for generated and native types.
Functions§
- no_
cycles - A
make_box_idargument forTypespaceBuilder::finalizethat asserts the type graph contains no containment cycles: it panics if finalization ever needs to insert aBox.