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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ name: Build

on:
push:
branches: [ main ]
branches: [ main, v0_10 ]
pull_request:
branches: [ main ]
branches: [ main, v0_10 ]

jobs:
check-style:
Expand Down
26 changes: 22 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ typify = { version = "0.10.0-alpha.1", path = "typify" }
typify-impl = { version = "0.10.0-alpha.1", path = "typify-impl" }
typify-macro = { version = "0.10.0-alpha.1", path = "typify-macro" }

typespace = "0.0.1-alpha.2"
typespace = "0.0.1-alpha.3"

assert_cmd = "2.2.2"
chrono = { version = "0.4.45", features = ["serde"] }
Expand Down
10 changes: 3 additions & 7 deletions typify-impl/src/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -799,7 +799,7 @@ impl TypeSpace {
) -> Result<(TypeEntry, &'a Option<Box<Metadata>>)> {
match format.as_ref().map(String::as_str) {
Some("uuid") => {
self.uses_uuid = true;
self.add_dependency(typespace::codespace::Dependency::new("uuid"));
// ::uuid::Uuid Default, but we ignore it because there's no
// useful value. We assume the uuid feature in schemars.
Ok((
Expand Down Expand Up @@ -827,7 +827,7 @@ impl TypeSpace {
}

Some("date") => {
self.uses_chrono = true;
self.add_dependency(typespace::codespace::Dependency::new("chrono"));
// ::chrono::naive::NaiveDate implements Default, but there's
// no useful value. We assume the chrono feaeture in schemars.
Ok((
Expand All @@ -854,7 +854,7 @@ impl TypeSpace {
))
}
Some("date-time") => {
self.uses_chrono = true;
self.add_dependency(typespace::codespace::Dependency::new("chrono"));
// ::chrono::DateTime implements Default, but not usefully.
// We assume the chrono feaeture in schemars.
Ok((
Expand Down Expand Up @@ -976,7 +976,6 @@ impl TypeSpace {
type_name: type_name.clone().into_option(),
reason: format!("invalid pattern '{}' {}", pattern, e),
})?;
self.uses_regress = true;
}

let string = TypeEntry::from(Type::String);
Expand Down Expand Up @@ -1948,7 +1947,6 @@ impl TypeSpace {
unique_items,
contains: None,
} => {
self.uses_serde_json = true;
let type_id = self.assign_type(TypeEntry::from(Type::JsonValue));

// If items are unique, this is a Set; otherwise it's an Array.
Expand All @@ -1969,7 +1967,6 @@ impl TypeSpace {
&mut self,
metadata: &'a Option<Box<Metadata>>,
) -> Result<(TypeEntry, &'a Option<Box<Metadata>>)> {
self.uses_serde_json = true;
let type_id = self.assign_type(TypeEntry::from(Type::JsonValue));
Ok((Type::Vec(type_id).into(), metadata))
}
Expand All @@ -1986,7 +1983,6 @@ impl TypeSpace {
&mut self,
metadata: &'a Option<Box<Metadata>>,
) -> Result<(TypeEntry, &'a Option<Box<Metadata>>)> {
self.uses_serde_json = true;
Ok((Type::JsonValue.into(), metadata))
}

Expand Down
68 changes: 32 additions & 36 deletions typify-impl/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,10 +126,12 @@ pub struct TypeSpace {
name_to_id: BTreeMap<String, TypeId>,
ref_to_id: BTreeMap<RefKey, TypeId>,

uses_chrono: bool,
uses_uuid: bool,
uses_serde_json: bool,
uses_regress: bool,
/// The crates needed for native types: `chrono` and `uuid` for the string
/// formats it maps to them, and each crate an `x-rust-type` extension
/// names, at the version [`TypeSpaceSettings::with_crate`] declared for
/// it. Recorded on the `TypespaceBuilder` so the finalized typespace
/// reports them with the crates of what typespace renders itself.
dependencies: Vec<typespace::codespace::Dependency>,

settings: TypeSpaceSettings,

Expand All @@ -144,10 +146,7 @@ impl Default for TypeSpace {
id_to_entry: Default::default(),
name_to_id: Default::default(),
ref_to_id: Default::default(),
uses_chrono: Default::default(),
uses_uuid: Default::default(),
uses_serde_json: Default::default(),
uses_regress: Default::default(),
dependencies: Default::default(),
settings: Default::default(),
cache: Default::default(),
}
Expand Down Expand Up @@ -745,26 +744,6 @@ impl TypeSpace {
}
}

/// Whether the generated code needs `chrono` crate.
pub fn uses_chrono(&self) -> bool {
self.uses_chrono
}

/// Whether the generated code needs [regress] crate.
pub fn uses_regress(&self) -> bool {
self.uses_regress
}

/// Whether the generated code needs [serde_json] crate.
pub fn uses_serde_json(&self) -> bool {
self.uses_serde_json
}

/// Whether the generated code needs `uuid` crate.
pub fn uses_uuid(&self) -> bool {
self.uses_uuid
}

/// The type inserted under `type_id`, as it was inserted.
///
/// This is the declaration typify handed typespace, available before
Expand All @@ -791,6 +770,10 @@ impl TypeSpace {
pub fn to_typespace(&self) -> Result<typespace::Typespace<TypeId>> {
let mut builder = typespace::TypespaceBuilder::new(self.settings.typespace.clone());

for dependency in &self.dependencies {
builder.add_dependency(dependency.clone());
}

for (type_id, type_entry) in &self.id_to_entry {
match type_entry {
TypeEntry::Type(typ) => {
Expand All @@ -809,17 +792,30 @@ impl TypeSpace {

/// All code for processed types.
///
/// Rendering is delegated to typespace: the stored types are
/// inserted into a `TypespaceBuilder`, finalized, and rendered
/// through codespace. Finalization errors (dangling references,
/// name collisions, unsatisfiable trait requirements) surface as
/// Rendering is delegated to typespace: the stored types are inserted into
/// a `TypespaceBuilder`, finalized, and rendered through codespace. The
/// codespace tracks depenencies. Finalization errors result in
/// [`Error::Typespace`].
pub fn to_stream(&self) -> Result<TokenStream> {
let typespace = self.to_typespace()?;
pub fn to_codespace(&self) -> Result<typespace::codespace::Codespace> {
Ok(self.to_typespace()?.to_codespace())
}

let codespace = typespace.to_codespace();
/// All code for processed types.
///
/// [`TypeSpace::to_codespace`] rendered to a `TokenStream`.
pub fn to_stream(&self) -> Result<TokenStream> {
Ok(self.to_codespace()?.into_stream())
}

Ok(codespace.into_stream())
/// Record a crate behind a native type, once per crate.
fn add_dependency(&mut self, dependency: typespace::codespace::Dependency) {
if !self
.dependencies
.iter()
.any(|known| known.name == dependency.name)
{
self.dependencies.push(dependency);
}
}

/// Allocated the next TypeId.
Expand Down
33 changes: 25 additions & 8 deletions typify-impl/src/rust_extension.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,33 +57,50 @@ impl TypeSpace {
return None;
}

let path = {
let (path, dependency) = {
if let Some(crate_spec) = self.settings.crates.get(crate_name.as_str()) {
// The version must be non-Never and match the requirements
// from the extension.
match &crate_spec.version {
CrateVers::Any => (),
CrateVers::Version(version) if req.matches(version) => (),
let version = match &crate_spec.version {
CrateVers::Any => semver::VersionReq::STAR,
CrateVers::Version(version) if req.matches(version) => {
semver::VersionReq::parse(&version.to_string())
.expect("a version is a valid requirement")
}
_ => return None,
}
};
let dependency = typespace::codespace::Dependency {
version,
..typespace::codespace::Dependency::new(&crate_name)
};

// Replace the initial path component with the new crate name.
if let Some(new_crate) = &crate_spec.rename {
format!("{}{}", new_crate.replace('-', "_"), &path[path_sep..])
let new_crate = new_crate.replace('-', "_");
(
format!("{}{}", new_crate, &path[path_sep..]),
typespace::codespace::Dependency {
rename: Some(new_crate),
..dependency
},
)
} else {
path
(path, dependency)
}
} else {
match self.settings.unknown_crates {
crate::UnknownPolicy::Generate => return None,
crate::UnknownPolicy::Allow => path,
crate::UnknownPolicy::Allow => {
(path, typespace::codespace::Dependency::new(&crate_name))
}

// TODO need to bubble up a coherent compiler error via the
// generated code.
crate::UnknownPolicy::Deny => return None,
}
}
};
self.add_dependency(dependency);

// Convert and collect type parameters.
let param_ids = parameters
Expand Down
75 changes: 75 additions & 0 deletions typify-impl/tests/test_dependencies.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Copyright 2026 Oxide Computer Company

//! The crates a generated type space reports.

use schemars::schema::Schema;
use typify_impl::{CrateVers, TypeSpace, TypeSpaceSettings};

fn names(type_space: &TypeSpace) -> Vec<String> {
type_space
.to_codespace()
.unwrap()
.dependencies()
.map(|dep| dep.name.clone())
.collect()
}

/// A string format mapped to a native type reports that type's crate,
/// alongside what typespace reports for the rendered code.
#[test]
fn formats_report_their_crates() {
let schema: Schema = serde_json::from_value(serde_json::json!({
"type": "object",
"properties": {
"when": { "type": "string", "format": "date-time" },
"id": { "type": "string", "format": "uuid" }
},
"required": ["when", "id"]
}))
.unwrap();
let mut type_space = TypeSpace::new(&TypeSpaceSettings::default());
type_space
.add_type_with_name(&schema, Some("Thing".to_string()))
.unwrap();
assert_eq!(names(&type_space), ["chrono", "serde", "uuid"]);
}

/// A type from an `x-rust-type` extension reports its crate at the
/// version the settings declared for it, renamed if the settings
/// renamed it.
#[test]
fn rust_extension_crates_carry_their_version() {
let schema: Schema = serde_json::from_value(serde_json::json!({
"type": "object",
"properties": {
"value": {
"type": "string",
"x-rust-type": {
"crate": "my-crate",
"version": "1.2.3",
"path": "my_crate::Value"
}
}
},
"required": ["value"]
}))
.unwrap();
let mut settings = TypeSpaceSettings::default();
settings.with_crate(
"my-crate",
CrateVers::Version("1.2.3".parse().unwrap()),
Some(&"their-crate".to_string()),
);
let mut type_space = TypeSpace::new(&settings);
type_space
.add_type_with_name(&schema, Some("Thing".to_string()))
.unwrap();

let codespace = type_space.to_codespace().unwrap();
let my_crate = codespace
.dependencies()
.find(|dep| dep.name == "my-crate")
.unwrap();
assert_eq!(my_crate.version.to_string(), "^1.2.3");
assert_eq!(my_crate.rename.as_deref(), Some("their_crate"));
}
Loading