use std::env;
use std::fs;
use std::fs::OpenOptions;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use serde_json::{Value, json};
use sha2::{Digest, Sha256};
use type_bridge_contract::codec::to_canonical_json;
use type_bridge_contract::fingerprint::SemanticProfileId;
use type_bridge_contract::projection::{BindingTarget, ProjectionConfig, RuntimeProjection};
use type_bridge_contract::schema::DocumentId;
use type_bridge_schema::{SchemaDocumentSet, normalize_documents, project, resolve};
use type_bridge_schema_codegen::{GeneratedPackage, RustEmitter};
mod support;
use support::{CARGO_MUTEX, Stage, cargo_with_env, repository_root, write_package};
const POSITIVE: &str = include_str!("rust_acceptance/positive.rs");
const NEGATIVE: &str = include_str!("rust_acceptance/negative.rs");
const PROJECTED_PARITY: &str = include_str!("rust_acceptance/projected_parity.rs");
const PROJECTED_FOREIGN_NEGATIVE: &str =
include_str!("rust_acceptance/projected_foreign_negative.rs");
const SDK_V5_CODEC: &str = include_str!("rust_acceptance/sdk_v5_codec.rs");
const SDK_V3_PROOF_FRAGMENT_ENV: &str = "TYPE_BRIDGE_SDK_V3_PROOF_FRAGMENT";
const SDK_V3_PROOF_NONCE_ENV: &str = "TYPE_BRIDGE_SDK_V3_PROOF_RUN_NONCE";
fn v3_source_identity(root: &Path, relative: &str) -> Value {
let bytes = fs::read(root.join(relative)).expect("V3 proof source reads");
json!({"path": relative, "sha256": format!("{:x}", Sha256::digest(bytes))})
}
fn publish_v3_package_fragment(results: Vec<Value>) {
let destination = env::var_os(SDK_V3_PROOF_FRAGMENT_ENV);
let nonce = env::var_os(SDK_V3_PROOF_NONCE_ENV);
assert_eq!(
destination.is_some(),
nonce.is_some(),
"V3 proof destination and nonce must be configured together"
);
let (Some(destination), Some(nonce)) = (destination, nonce) else {
return;
};
let destination = PathBuf::from(destination);
assert!(destination.is_absolute() && !destination.exists());
let nonce = nonce.to_str().expect("V3 proof nonce is UTF-8");
assert!(
nonce.len() == 64
&& nonce
.bytes()
.all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase())
);
let root = repository_root();
let sources = [
"type-bridge-core/crates/rust/src/entity_codec.rs",
"type-bridge-core/crates/rust/src/relation_codec.rs",
"type-bridge-core/crates/rust/src/schema.rs",
"type-bridge-core/crates/schema-codegen/tests/rust_acceptance.rs",
];
let fragment = json!({
"binding": "rust",
"contract": {
"allowlist": v3_source_identity(&root, "tests/contracts/sdk_conformance/sdk-v3/proof-fragment-allowlist-v1.json"),
"journey": v3_source_identity(&root, "tests/contracts/sdk_conformance/sdk-v3/journey-v3.json"),
"proof_schema": v3_source_identity(&root, "tests/contracts/sdk_conformance/sdk-v3/proof-fragment-schema-v1.json"),
},
"format": "typebridge.sdk-v3-proof-fragment/v1",
"producer": {
"id": "type-bridge-rust.generated-package-v3-proof",
"sources": sources.iter().map(|path| v3_source_identity(&root, path)).collect::<Vec<_>>(),
},
"results": results,
"run_nonce": nonce,
"semantic_profile": "typedb-3.12.1/v1",
});
let mut bytes = to_canonical_json(&fragment).expect("V3 package fragment canonicalizes");
bytes.push(b'\n');
let mut output = OpenOptions::new()
.write(true)
.create_new(true)
.open(destination)
.expect("V3 package fragment destination is created once");
output
.write_all(&bytes)
.expect("V3 package fragment writes");
output.sync_all().expect("V3 package fragment is durable");
}
#[test]
fn acceptance_stages_are_unique_when_clock_nonce_repeats() {
let first = Stage::new_for_nonce(u128::MAX);
let second = Stage::new_for_nonce(u128::MAX);
assert_ne!(first.path(), second.path());
let first_path = first.path().to_owned();
let second_path = second.path().to_owned();
let sentinel = second.path().join("owned.txt");
fs::write(&sentinel, b"second stage").unwrap();
drop(first);
assert!(!first_path.exists());
assert_eq!(fs::read(&sentinel).unwrap(), b"second stage");
drop(second);
assert!(!second_path.exists());
}
fn project_from_source(source: &str) -> RuntimeProjection {
support::rust_projection(source, "rust-acceptance.yaml")
}
fn emit_from_source(source: &str) -> GeneratedPackage {
let authority = support::authority(source);
RustEmitter::new()
.emit(&project_from_source(source), &authority)
.unwrap()
}
fn emit() -> GeneratedPackage {
emit_from_source(include_str!("acceptance/schema.yaml"))
}
fn write_consumer_with_features_and_dependencies(
root: &Path,
name: &str,
source: &str,
features: &[&str],
additional_dependencies: &str,
) {
let rust_crate = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.join("rust");
let rust_path = rust_crate.to_string_lossy().replace('\\', "\\\\");
let feat_str = if features.is_empty() {
String::new()
} else {
format!(", features = {:?}", features)
};
fs::create_dir_all(root.join("src")).unwrap();
fs::write(
root.join("Cargo.toml"),
format!(
"[package]\nname = \"{name}\"\nversion = \"0.0.0\"\nedition = \"2024\"\n\n[dependencies]\ngenerated = {{ package = \"type-bridge-generated-schema\", path = \"../generated\" }}\n{additional_dependencies}type-bridge = {{ path = \"{rust_path}\", default-features = false{feat_str} }}\n\n[patch.crates-io]\ntype-bridge = {{ path = \"{rust_path}\" }}\n\n[workspace]\n"
),
).unwrap();
fs::write(root.join("src/main.rs"), source).unwrap();
}
fn write_consumer_with_features(root: &Path, name: &str, source: &str, features: &[&str]) {
write_consumer_with_features_and_dependencies(root, name, source, features, "");
}
fn write_consumer(root: &Path, name: &str, source: &str) {
write_consumer_with_features(root, name, source, &["test-harness"]);
}
#[test]
fn pristine_generated_rust_passes_strict_clippy_as_consumer_test_modules() {
assert_pristine_consumer_clippy(&emit());
}
#[test]
fn explicit_rust_type_names_pass_strict_consumer_clippy() {
use type_bridge_contract::id::{TypeId, TypeKind};
let source = include_str!("acceptance/schema.yaml");
let authority = support::authority(source);
let resolved = authority.resolved_schema();
let emitter = RustEmitter::new();
let config = ProjectionConfig::rust()
.with_type_name_override(
TypeId::new(TypeKind::Entity, "person").unwrap(),
"person_value",
)
.unwrap();
let projection = project(
resolved,
BindingTarget::Rust,
&config,
&emitter.generator_handlers_for(resolved),
&emitter.code_resources_for(resolved).unwrap(),
)
.unwrap();
assert_pristine_consumer_clippy(&emitter.emit(&projection, &authority).unwrap());
}
#[test]
fn sparse_generated_rust_passes_strict_consumer_clippy() {
assert_pristine_consumer_clippy(&emit_from_source(
r#"format: typebridge.schema/v2
attributes:
identifier: { value: string }
entities:
person:
owns:
identifier: { key: true }
"#,
));
}
fn assert_pristine_consumer_clippy(package: &GeneratedPackage) {
let stage = Stage::new();
let generated = stage.path().join("generated");
let consumer = stage.path().join("consumer");
write_package(package, &generated);
write_consumer(&consumer, "rust-generated-clippy", "fn main() {}\n");
fs::create_dir_all(consumer.join("tests")).unwrap();
for (relative, contents) in package.files() {
if let Some(relative) = relative.strip_prefix("src/") {
let destination = if relative == "lib.rs" {
consumer.join("tests/generated.rs")
} else {
consumer.join("tests").join(relative)
};
fs::write(destination, contents).unwrap();
} else if relative.starts_with("typebridge/") {
let destination = consumer.join(relative);
fs::create_dir_all(destination.parent().unwrap()).unwrap();
fs::write(destination, contents).unwrap();
}
}
let manifest = consumer.join("Cargo.toml");
let output = cargo_with_env(
&[
"clippy",
"--quiet",
"--manifest-path",
manifest.to_str().unwrap(),
"--test",
"generated",
"--no-deps",
"--",
"-D",
"warnings",
],
&[],
);
assert!(
output.status.success(),
"pristine generated consumer Clippy failed\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
}
#[test]
fn generated_rust_crate_compiles_rejects_invalid_types_and_runs() {
let stage = Stage::new();
let generated = stage.path().join("generated");
let positive = stage.path().join("positive");
let negative = stage.path().join("negative");
write_package(&emit(), &generated);
write_consumer(&positive, "rust-projection-positive", POSITIVE);
write_consumer(&negative, "rust-projection-negative", NEGATIVE);
let positive_manifest = positive.join("Cargo.toml");
let positive_output = cargo_with_env(
&[
"run",
"--quiet",
"--manifest-path",
positive_manifest.to_str().unwrap(),
],
&[],
);
assert!(
positive_output.status.success(),
"positive generated consumer failed\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&positive_output.stdout),
String::from_utf8_lossy(&positive_output.stderr),
);
let negative_manifest = negative.join("Cargo.toml");
let negative_output = cargo_with_env(
&[
"check",
"--quiet",
"--manifest-path",
negative_manifest.to_str().unwrap(),
],
&[],
);
assert!(
!negative_output.status.success(),
"negative generated consumer unexpectedly compiled"
);
let stderr = String::from_utf8_lossy(&negative_output.stderr);
assert!(
stderr.contains("mismatched types"),
"negative failure was not a type mismatch:\n{stderr}"
);
assert!(
stderr.contains("EventRef"),
"negative failure omitted the distinct reference type:\n{stderr}"
);
assert!(
stderr.contains("RoleToken"),
"negative failure omitted the owner-branded role token:\n{stderr}"
);
assert!(
stderr.contains("RolePlayerBinding")
&& stderr.contains("NetworkLinkDestinationPlayer")
&& stderr.contains("Event"),
"negative failure omitted the generated reachability endpoint proof:\n{stderr}"
);
assert!(
stderr.contains("SelectedRow requires between 1 and 16 fields"),
"negative failure omitted the selected-row arity ceiling:\n{stderr}"
);
assert!(
stderr.contains("Output == Event") || stderr.contains("Output = Event"),
"negative failure omitted the selected-output type proof:\n{stderr}"
);
assert!(
stderr.contains("SingularSelectedShape"),
"negative failure omitted the collection-terminal boundary:\n{stderr}"
);
assert!(
stderr.contains("cannot move out of `read` because it is borrowed"),
"negative failure omitted the active-read close boundary:\n{stderr}"
);
}
#[test]
fn generated_manager_filter_compile_boundaries_fail_independently() {
let stage = Stage::new();
let generated = stage.path().join("generated");
write_package(&emit(), &generated);
let cases: [(&str, &str, &[&str]); 4] = [
(
"manager-filter-rejects-wrong-owner",
r#"use generated::{AppSchema, Person, RobotId, RobotType};
use type_bridge::{Database, ProjectedManagerComparison};
fn check(db: &Database<AppSchema>) {
let _ = db.entities::<Person>().where_(
RobotType::robot_id,
ProjectedManagerComparison::Eq,
&RobotId::new(7).unwrap(),
);
}
fn main() {}"#,
&["mismatched types", "FieldToken", "Person", "Robot"],
),
(
"manager-filter-rejects-wrong-value",
r#"use generated::{AppSchema, Identifier, Person, PersonType};
use type_bridge::{Database, ProjectedManagerComparison};
fn check(db: &Database<AppSchema>) {
let identifier = Identifier::new("data-ada").unwrap();
let _ = db.entities::<Person>().where_(
PersonType::score,
ProjectedManagerComparison::Eq,
&identifier,
);
}
fn main() {}"#,
&["mismatched types", "Identifier", "Score"],
),
(
"manager-filter-exposes-no-mutations",
r#"use generated::{AppSchema, Person};
use type_bridge::Database;
fn check(db: &Database<AppSchema>) {
let filter = db.entities::<Person>().filter().unwrap();
let _ = filter.insert();
}
fn main() {}"#,
&["no method named `insert`", "ProjectedEntityFilter"],
),
(
"manager-filter-borrow-prevents-read-close",
r#"use generated::{AppSchema, Person};
async fn check(read: type_bridge::ReadTransaction<'_, AppSchema>) {
let filter = read.entities::<Person>().filter().unwrap();
read.close().await.unwrap();
let _ = filter.count().await;
}
fn main() {}"#,
&["cannot move out of `read` because it is borrowed"],
),
];
for (name, source, expected) in cases {
let consumer = stage.path().join(name);
write_consumer(&consumer, name, source);
let output = cargo_with_env(
&[
"check",
"--offline",
"--quiet",
"--manifest-path",
consumer.join("Cargo.toml").to_str().unwrap(),
],
&[],
);
assert!(!output.status.success(), "{name} unexpectedly compiled");
let stderr = String::from_utf8_lossy(&output.stderr);
for fragment in expected {
assert!(
stderr.contains(fragment),
"{name} omitted {fragment:?}:\n{stderr}",
);
}
assert!(
!stderr.contains("unresolved import"),
"{name} failed before the intended boundary:\n{stderr}",
);
}
}
#[test]
fn generated_direct_connection_policy_compiles_with_legacy_compatibility() {
let stage = Stage::new();
let generated = stage.path().join("generated");
let consumer = stage.path().join("direct-connection-positive");
write_package(&emit(), &generated);
write_consumer_with_features(
&consumer,
"direct-connection-positive",
r#"use std::path::PathBuf;
use generated::{AppSchema, SCHEMA};
use type_bridge::{
AnswerCancellation, ConnectionOptions, Database, DirectConnectionPolicy, DirectTls,
QueryExecutionResourceLimits, Unbound,
};
fn limits() -> QueryExecutionResourceLimits {
QueryExecutionResourceLimits::tightened(5_000, 32, 65_536, 32, 64, 64, 32, 3)
}
fn canonical_policy() -> DirectConnectionPolicy {
let trust = DirectTls::custom_root(PathBuf::from("root-ca.pem")).unwrap();
DirectConnectionPolicy::new(
"localhost:1729",
"app",
"admin",
"password",
)
.http_port(8000)
.tls(trust)
.connection_limits(limits())
.answer_limits(limits())
}
async fn canonical_connection() -> type_bridge::Result<()> {
let cancellation = AnswerCancellation::default();
let database: Database<AppSchema> = SCHEMA
.connect_with_cancellation(canonical_policy(), cancellation)
.await?;
database.close()?;
let database: Database<AppSchema> = SCHEMA.connect(canonical_policy()).await?;
database.close()
}
async fn released_connection_compatibility() -> type_bridge::Result<()> {
let options = ConnectionOptions::new("localhost:1729", "app")
.credentials("admin", "password")
.http_port(8000)
.tls(false);
let database: Database<Unbound> = Database::connect(options).await?;
let database: Database<AppSchema> = database.with_schema(SCHEMA)?;
database.close()
}
fn main() {
let _ = DirectTls::disabled();
let _ = DirectTls::native_roots();
let _ = canonical_connection;
let _ = released_connection_compatibility;
}
"#,
&["band8", "band9"],
);
let output = cargo_with_env(
&[
"check",
"--offline",
"--quiet",
"--manifest-path",
consumer.join("Cargo.toml").to_str().unwrap(),
],
&[],
);
assert!(
output.status.success(),
"canonical and released direct connections did not compile\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
}
#[test]
fn generated_direct_connection_policy_boundaries_fail_independently() {
let stage = Stage::new();
let generated = stage.path().join("generated");
write_package(&emit(), &generated);
let cases: [(&str, &str, &[&str]); 5] = [
(
"direct-policy-rejects-caller-server-version",
r#"use type_bridge::DirectConnectionPolicy;
fn main() {
let policy = DirectConnectionPolicy::new(
"localhost:1729", "app", "admin", "password",
);
let _ = policy.server_version("3.12.1");
}"#,
&["no method named `server_version`", "DirectConnectionPolicy"],
),
(
"direct-connect-preserves-package-authority",
r#"use generated::{AppSchema, SCHEMA};
use type_bridge::{Database, DirectConnectionPolicy, Schema};
struct ForeignSchema;
impl type_bridge::schema::sealed::Sealed for ForeignSchema {}
impl Schema for ForeignSchema {}
async fn check() {
let policy = DirectConnectionPolicy::new(
"localhost:1729", "app", "admin", "password",
);
let _: Database<ForeignSchema> = SCHEMA.connect(policy).await.unwrap();
}
fn main() {
let _: Option<AppSchema> = None;
}"#,
&[
"mismatched types",
"Database<ForeignSchema>",
"Database<AppSchema>",
],
),
(
"direct-tls-rejects-inline-trust-bytes",
r#"use type_bridge::DirectTls;
fn main() {
let _ = DirectTls::custom_root(vec![b'x']);
}"#,
&["AsRef", "Path", "Vec<u8>"],
),
(
"direct-policy-rejects-untyped-answer-limit",
r#"use type_bridge::DirectConnectionPolicy;
fn main() {
let policy = DirectConnectionPolicy::new(
"localhost:1729", "app", "admin", "password",
);
let _ = policy.answer_limits(1_u64);
}"#,
&["mismatched types", "QueryExecutionResourceLimits"],
),
(
"direct-connect-rejects-untyped-cancellation",
r#"use generated::SCHEMA;
use type_bridge::DirectConnectionPolicy;
async fn check() {
let policy = DirectConnectionPolicy::new(
"localhost:1729", "app", "admin", "password",
);
let _ = SCHEMA.connect_with_cancellation(policy, ()).await;
}
fn main() {}"#,
&["mismatched types", "AnswerCancellation"],
),
];
for (name, source, expected) in cases {
let consumer = stage.path().join(name);
write_consumer_with_features(&consumer, name, source, &["band8", "band9"]);
let output = cargo_with_env(
&[
"check",
"--offline",
"--quiet",
"--manifest-path",
consumer.join("Cargo.toml").to_str().unwrap(),
],
&[],
);
assert!(!output.status.success(), "{name} unexpectedly compiled");
let stderr = String::from_utf8_lossy(&output.stderr);
for fragment in expected {
assert!(
stderr.contains(fragment),
"{name} omitted {fragment:?}:\n{stderr}",
);
}
assert!(
!stderr.contains("unresolved import") && !stderr.contains("cannot find"),
"{name} failed before the intended boundary:\n{stderr}",
);
}
}
#[test]
fn generated_role_bindings_admit_only_overlapping_subtype_domains() {
let stage = Stage::new();
let generated = stage.path().join("generated");
write_package(&emit(), &generated);
let cases = [
(
"role-rejects-exact-robot",
r#"use generated::{AppSchema, Event, EventType, Robot};
use type_bridge::Database;
fn check(db: &Database<AppSchema>) {
let mut session = db.query().unwrap();
let event = session.exact::<Event>().unwrap();
let robot = session.exact::<Robot>().unwrap();
let _ = event.role(EventType::subject).connects(robot);
}
fn main() {}"#,
),
(
"role-rejects-disjoint-robot-subtypes",
r#"use generated::{AppSchema, Event, EventType, Robot};
use type_bridge::Database;
fn check(db: &Database<AppSchema>) {
let mut session = db.query().unwrap();
let event = session.exact::<Event>().unwrap();
let robots = session.subtypes::<Robot>().unwrap();
let _ = event.role(EventType::subject).connects(robots);
}
fn main() {}"#,
),
];
for (name, source) in cases {
let consumer = stage.path().join(name);
write_consumer_with_features(&consumer, name, source, &[]);
let output = cargo_with_env(
&[
"check",
"--offline",
"--manifest-path",
consumer.join("Cargo.toml").to_str().unwrap(),
],
&[],
);
assert!(!output.status.success(), "{name} unexpectedly compiled");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("EventSubjectPlayer")
&& stderr.contains("RolePlayerBinding")
&& stderr.contains("Robot")
&& !stderr.contains("unresolved import")
&& !stderr.contains("cannot find"),
"{name}: {stderr}"
);
}
}
#[test]
fn generated_subtype_association_compiles_as_ordinary_dependency() {
let stage = Stage::new();
let generated = stage.path().join("generated");
let consumer = stage.path().join("ordinary-consumer");
let pkg = emit_from_source(
"format: typebridge.schema/v2\nentities:\n base:\n abstract: true\n record: { sub: base }\n child: { sub: base }\n",
);
write_package(&pkg, &generated);
write_consumer_with_features(
&consumer,
"ordinary-consumer",
"use generated::{Model, Record}; fn main() { let _ = Record::TYPE_ID_JSON; }\n",
&[],
);
let output = cargo_with_env(
&[
"check",
"--manifest-path",
consumer.join("Cargo.toml").to_str().unwrap(),
],
&[],
);
assert!(
output.status.success(),
"ordinary consumer failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn single_variant_families_compile_without_irrefutable_patterns() {
let stage = Stage::new();
let generated = stage.path().join("generated");
let consumer = stage.path().join("consumer");
let source = include_str!("acceptance/schema.yaml")
.replacen(
"attributes:\n",
"attributes:\n single-identifier:\n abstract: true\n value: string\n single-id: { sub: single-identifier }\n",
1,
)
.replacen(
"entities:\n",
"entities:\n single-actor:\n abstract: true\n single-person: { sub: single-actor }\n",
1,
);
let package = emit_from_source(&source);
write_package(&package, &generated);
let library = generated.join("src/lib.rs");
let source = fs::read_to_string(&library).unwrap();
fs::write(
&library,
format!("#![deny(irrefutable_let_patterns)]\n{source}"),
)
.unwrap();
write_consumer_with_features(
&consumer,
"single-variant-family-consumer",
"fn main() { let _ = generated::SingleActorFamily::as_single_person; let _ = generated::SingleIdentifierFamily::as_single_id; }\n",
&[],
);
let output = cargo_with_env(
&[
"check",
"--offline",
"--manifest-path",
consumer.join("Cargo.toml").to_str().unwrap(),
],
&[],
);
assert!(
output.status.success(),
"single-variant pattern-lint consumer failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn abstract_attribute_family_compiles_and_runs_as_a_value_enum() {
let stage = Stage::new();
let generated = stage.path().join("generated");
let consumer = stage.path().join("attribute-family-consumer");
let package = emit_from_source(include_str!(
"../../../../docs/fixtures/split-yaml-v1/schema/fixture.yaml"
));
write_package(&package, &generated);
write_consumer_with_features(
&consumer,
"attribute-family-consumer",
r#"use generated::{EmployeeId, IdentifierFamily};
fn main() {
let employee_id = EmployeeId::new("ADA-1").unwrap();
let identifier = IdentifierFamily::EmployeeId(employee_id);
assert_eq!(identifier.value(), "ADA-1");
assert_eq!(identifier.as_employee_id().unwrap().value(), "ADA-1");
}
"#,
&[],
);
let output = cargo_with_env(
&[
"run",
"--offline",
"--quiet",
"--manifest-path",
consumer.join("Cargo.toml").to_str().unwrap(),
],
&[],
);
assert!(
output.status.success(),
"attribute family consumer failed:\nstdout: {}\nstderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
}
const MANAGER_MATRIX_SCHEMA: &str = r#"format: typebridge.schema/v2
attributes:
name: { value: string }
person-only: { value: string }
company-only: { value: string }
entities:
helper: {}
party:
abstract: true
owns: { name: { key: true } }
person:
sub: party
owns: { person-only: { card: 1 } }
company:
sub: party
owns: { company-only: { card: 1 } }
root:
owns: { name: { key: true } }
root-child: { sub: root }
empty: { abstract: true }
outsider: {}
relations:
employment:
abstract: true
relates: { employee: { card: 1 } }
placement:
sub: employment
contract:
sub: employment
relates: { contractor: { as: employee, card: 1 } }
plays:
helper:
employment: { employee: { card: 1 } }
contract: { contractor: { card: 1 } }
"#;
#[test]
fn generated_external_manager_contract_compiles_exact_matrix() {
let stage = Stage::new();
let generated = stage.path().join("generated");
let consumer = stage.path().join("manager-consumer");
let package = emit_from_source(MANAGER_MATRIX_SCHEMA);
write_package(&package, &generated);
write_consumer_with_features(
&consumer,
"manager-consumer",
r#"
use generated::{AppSchema, Contract, ContractCreate, ContractType, Empty, Employment, EmploymentFamily, EmploymentType, Helper, Model, Name, Never, Party, PartyFamily, PartyType, Person, PersonCreate, PersonType, Placement, Root, RootChild, RootFamily};
use type_bridge::{Database, EntityManager, EntitySubtypeManager, Predicate, RelationManager, RelationSubtypeManager, WriteTransaction};
use type_bridge::value::Text;
async fn positive(db: &Database<AppSchema>, create: PersonCreate) {
let exact = db.entities::<Person>();
let _: EntityManager<'_, AppSchema, Person> = exact;
let _: Person = exact.insert(create.clone()).await.unwrap();
let _: Person = exact.put(create.clone()).await.unwrap();
let _: Person = exact.update("0x1", create.clone()).await.unwrap();
let _: Vec<Person> = exact.insert_many(vec![create.clone()]).await.unwrap();
let _: Vec<Person> = exact.put_many(vec![create.clone()]).await.unwrap();
let _: () = exact.delete("0x1").await.unwrap();
let _: Option<Person> = exact.get_by_iid("0x1").await.unwrap();
let _: Vec<Person> = exact.all().await.unwrap();
let _: u64 = exact.count().await.unwrap();
let _: EntitySubtypeManager<'_, AppSchema, Person> = exact.subtypes();
let _: Option<Person> = exact.subtypes().get_by_iid("0x1").await.unwrap();
let _: Vec<Person> = exact.subtypes().all().await.unwrap();
let _: u64 = exact.subtypes().count().await.unwrap();
let family = db.entities::<Party>().subtypes();
let family_value: Option<PartyFamily> = family.get_by_iid("0x1").await.unwrap();
if let Some(value) = family_value { let _ = value.name(); narrow(value); }
let _: Option<PartyFamily> = family.get_by_iid("0x1").await.unwrap();
let _: Vec<PartyFamily> = family.all().await.unwrap();
let _: u64 = family.count().await.unwrap();
let root_value: Option<RootFamily> = db.entities::<Root>().subtypes().get_by_iid("0x1").await.unwrap();
if let Some(value) = root_value { match value { RootFamily::Root(root) => { let _ = root.name(); }, RootFamily::RootChild(child) => { let _ = child.name(); } } }
let root_values: Vec<RootFamily> = db.entities::<Root>().subtypes().all().await.unwrap();
for value in root_values { match value { RootFamily::Root(root) => { let _ = root.name(); }, RootFamily::RootChild(child) => { let _ = child.name(); } } }
let _: Option<RootChild> = db.entities::<RootChild>().subtypes().get_by_iid("0x1").await.unwrap();
let _: Option<Never> = db.entities::<Empty>().subtypes().get_by_iid("0x1").await.unwrap();
let _: Vec<Never> = db.entities::<Empty>().subtypes().all().await.unwrap();
}
fn narrow(value: PartyFamily) { match value { PartyFamily::Person(person) => { let _ = person.name(); let _ = person.person_only(); }, PartyFamily::Company(company) => { let _ = company.name(); let _ = company.company_only(); } } }
async fn relation_positive(db: &Database<AppSchema>, create: ContractCreate) {
let exact = db.relations::<Contract>();
let _: RelationManager<'_, AppSchema, Contract> = exact;
let _: Contract = exact.insert(create.clone()).await.unwrap();
let _: Contract = exact.put(create.clone()).await.unwrap();
let _: Contract = exact.update("0x1", create.clone()).await.unwrap();
let _: Vec<Contract> = exact.insert_many(vec![create.clone()]).await.unwrap();
let _: Vec<Contract> = exact.put_many(vec![create.clone()]).await.unwrap();
let _: () = exact.delete("0x1").await.unwrap();
let _: Option<Contract> = exact.get_by_iid("0x1").await.unwrap();
let _: Vec<Contract> = exact.all().await.unwrap();
let _: u64 = exact.count().await.unwrap();
let _: RelationSubtypeManager<'_, AppSchema, Contract> = exact.subtypes();
let _: Option<Contract> = exact.subtypes().get_by_iid("0x1").await.unwrap();
let _: Vec<Contract> = exact.subtypes().all().await.unwrap();
let _: u64 = exact.subtypes().count().await.unwrap();
let family = db.relations::<Employment>().subtypes();
let _: Option<EmploymentFamily> = family.get_by_iid("0x1").await.unwrap();
let _: Vec<EmploymentFamily> = family.all().await.unwrap();
let _: u64 = family.count().await.unwrap();
}
async fn transaction_positive(db: &Database<AppSchema>, create: PersonCreate, relation_create: ContractCreate) {
let tx: WriteTransaction<'_, AppSchema> = db.write().await.unwrap();
let _: Person = tx.entities::<Person>().insert(create.clone()).await.unwrap();
let _: Person = tx.entities::<Person>().put(create.clone()).await.unwrap();
let _: Person = tx.entities::<Person>().update("0x1", create.clone()).await.unwrap();
let _: Vec<Person> = tx.entities::<Person>().insert_many(vec![create.clone()]).await.unwrap();
let _: Vec<Person> = tx.entities::<Person>().put_many(vec![create.clone()]).await.unwrap();
let _: () = tx.entities::<Person>().delete("0x1").await.unwrap();
let _: Option<Person> = tx.entities::<Person>().get_by_iid("0x1").await.unwrap();
let _: Vec<Person> = tx.entities::<Person>().all().await.unwrap();
let _: u64 = tx.entities::<Person>().count().await.unwrap();
let _: Contract = tx.relations::<Contract>().insert(relation_create.clone()).await.unwrap();
let _: Contract = tx.relations::<Contract>().put(relation_create.clone()).await.unwrap();
let _: Contract = tx.relations::<Contract>().update("0x1", relation_create.clone()).await.unwrap();
let _: Vec<Contract> = tx.relations::<Contract>().insert_many(vec![relation_create.clone()]).await.unwrap();
let _: Vec<Contract> = tx.relations::<Contract>().put_many(vec![relation_create.clone()]).await.unwrap();
let _: () = tx.relations::<Contract>().delete("0x1").await.unwrap();
let _: Option<Contract> = tx.relations::<Contract>().get_by_iid("0x1").await.unwrap();
let _: Vec<Contract> = tx.relations::<Contract>().all().await.unwrap();
let _: u64 = tx.relations::<Contract>().count().await.unwrap();
tx.commit().await.unwrap();
let tx = db.write().await.unwrap();
tx.rollback().await.unwrap();
let _dropped = db.write().await.unwrap();
}
fn query_positive(db: &Database<AppSchema>) {
let mut session = db.query().unwrap();
let party = session.subtypes::<Party>().unwrap();
let person = session.exact::<Person>().unwrap();
let helper = session.exact::<Helper>().unwrap();
let placement = session.exact::<Placement>().unwrap();
let contract = session.exact::<Contract>().unwrap();
let party_name = party.field(PartyType::name);
let person_name = person.field(PartyType::name);
let person_only = person.field(PersonType::person_only);
let placement_employee = placement.role(EmploymentType::employee);
let contract_contractor = contract.role(ContractType::contractor);
let predicate: Predicate<AppSchema> = party_name.eq(Name::new("Alice").unwrap())
& party_name.starts_with(Text::new("Al").unwrap())
& person_only.eq(Text::new("x").unwrap())
& person_name.eq_field(party_name)
& placement_employee.connects(helper)
& contract_contractor.connects(helper);
let _ = predicate;
let _party_query = session.query(party).unwrap();
let _person_query = session.query(person).unwrap();
}
fn main() {}
"#,
&[],
);
let output = cargo_with_env(
&[
"check",
"--offline",
"--manifest-path",
consumer.join("Cargo.toml").to_str().unwrap(),
],
&[],
);
let manifest = fs::read_to_string(consumer.join("Cargo.toml")).unwrap();
let deps = manifest
.split("[dependencies]")
.nth(1)
.unwrap()
.split("[patch.crates-io]")
.next()
.unwrap();
let entries = deps
.lines()
.map(str::trim)
.filter(|line| line.contains('=') && !line.is_empty())
.collect::<Vec<_>>();
assert_eq!(entries.len(), 2);
assert!(entries.iter().any(|line| line.starts_with("generated =")));
assert!(
entries
.iter()
.any(|line| line.starts_with("type-bridge =")
&& line.contains("default-features = false"))
);
for forbidden in [
"type-bridge-orm",
"type-bridge-contract",
"type-bridge-schema",
"type-bridge-query",
"type-bridge-schema-codegen",
"provider",
] {
assert!(!entries.iter().any(|line| line.contains(forbidden)));
}
assert!(
output.status.success(),
"generated manager consumer failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn generated_external_manager_negative_matrix_has_specific_diagnostics() {
let stage = Stage::new();
let generated = stage.path().join("generated");
let consumer = stage.path().join("negative-manager-consumer");
write_package(
&emit_from_source("format: typebridge.schema/v2\nentities:\n party: { abstract: true }\n"),
&generated,
);
write_consumer_with_features(
&consumer,
"negative-manager-consumer",
r#"use generated::{AppSchema, Party}; use type_bridge::Database;
async fn insert(db: &Database<AppSchema>) { let _ = db.entities::<Party>().insert(todo!()); }
async fn insert_many(db: &Database<AppSchema>) { let _ = db.entities::<Party>().insert_many(vec![]); }
async fn put(db: &Database<AppSchema>) { let _ = db.entities::<Party>().put(todo!()); }
async fn put_many(db: &Database<AppSchema>) { let _ = db.entities::<Party>().put_many(vec![]); }
async fn update(db: &Database<AppSchema>) { let _ = db.entities::<Party>().update("0x1", todo!()); }
async fn delete(db: &Database<AppSchema>) { let _ = db.entities::<Party>().delete("0x1"); }
async fn get_by_iid(db: &Database<AppSchema>) { let _ = db.entities::<Party>().get_by_iid("0x1"); }
async fn all(db: &Database<AppSchema>) { let _ = db.entities::<Party>().all(); }
async fn count(db: &Database<AppSchema>) { let _ = db.entities::<Party>().count(); }
fn main() {}"#,
&[],
);
let output = cargo_with_env(
&[
"check",
"--offline",
"--manifest-path",
consumer.join("Cargo.toml").to_str().unwrap(),
],
&[],
);
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
for method in [
"insert",
"insert_many",
"put",
"put_many",
"update",
"delete",
"get_by_iid",
"all",
"count",
] {
assert!(stderr.contains(&format!("method `{method}`")));
}
assert!(stderr.matches("error[E0599]").count() >= 9);
assert!(stderr.matches("CompleteModel").count() >= 9);
assert!(
stderr.contains("CompleteModel")
&& !stderr.contains("unresolved import")
&& !stderr.contains("cannot find")
);
}
#[test]
fn generated_external_manager_remaining_negative_boundaries() {
let stage = Stage::new();
let generated = stage.path().join("generated");
let package = emit_from_source(
r#"format: typebridge.schema/v2
attributes:
name: { value: string }
person-only: { value: string }
entities:
helper: {}
party: { abstract: true }
person: { sub: party, owns: { name: { key: true }, person-only: { card: 1 } } }
company: { sub: party, owns: { name: { key: true } } }
outsider: {}
relations:
employment: { relates: { employee: { card: 1 } } }
contract:
sub: employment
relates: { contractor: { as: employee, card: 1 } }
pact: { abstract: true, relates: { side: { card: 1 } } }
plays:
helper:
employment: { employee: { card: 1 } }
contract: { contractor: { card: 1 } }
pact: { side: { card: 1 } }
"#,
);
write_package(&package, &generated);
let cases = [
(
"subtype-write",
"use generated::{AppSchema, Person}; use type_bridge::Database; async fn f(db: &Database<AppSchema>) { let _ = db.entities::<Person>().subtypes().insert(todo!()); } fn main() {}",
"no method named `insert`",
),
(
"wrong-create",
"use generated::{AppSchema, Person, OutsiderCreate}; use type_bridge::Database; async fn f(db: &Database<AppSchema>, x: OutsiderCreate) { let _ = db.entities::<Person>().insert(x); } fn main() {}",
"mismatched types",
),
(
"relation-entity",
"use generated::{AppSchema, Employment}; use type_bridge::Database; async fn f(db: &Database<AppSchema>) { let _ = db.entities::<Employment>(); } fn main() {}",
"EntityModel",
),
(
"reference-entity",
"use generated::{AppSchema, PersonRef}; use type_bridge::Database; async fn f(db: &Database<AppSchema>) { let _ = db.entities::<PersonRef>(); } fn main() {}",
"EntityModel",
),
(
"child-before-narrow",
"use generated::{AppSchema, Party, PartyFamily}; use type_bridge::Database; async fn f(db: &Database<AppSchema>) { let x: PartyFamily = db.entities::<Party>().subtypes().all().await.unwrap().remove(0); let _ = x.person_only(); } fn main() {}",
"no method named `person_only`",
),
(
"entity-relation-manager",
"use generated::{AppSchema, Person}; use type_bridge::Database; async fn f(db: &Database<AppSchema>) { let _ = db.relations::<Person>(); } fn main() {}",
"RelationModel",
),
(
"reference-relation-manager",
"use generated::{AppSchema, EmploymentRef}; use type_bridge::Database; async fn f(db: &Database<AppSchema>) { let _ = db.relations::<EmploymentRef>(); } fn main() {}",
"RelationModel",
),
(
"relation-subtype-write",
"use generated::{AppSchema, Employment}; use type_bridge::Database; async fn f(db: &Database<AppSchema>) { let _ = db.relations::<Employment>().subtypes().insert(todo!()); } fn main() {}",
"no method named `insert`",
),
(
"abstract-relation-write",
"use generated::{AppSchema, Pact}; use type_bridge::Database; async fn f(db: &Database<AppSchema>) { let _ = db.relations::<Pact>().insert(todo!()); } fn main() {}",
"CompleteModel",
),
(
"transaction-commit-while-borrowed",
"use generated::{AppSchema, Person}; use type_bridge::Database; async fn f(db: &Database<AppSchema>) { let tx = db.write().await.unwrap(); let manager = tx.entities::<Person>(); tx.commit().await.unwrap(); let _ = manager.count().await.unwrap(); } fn main() {}",
"cannot move out of `tx` because it is borrowed",
),
(
"transaction-not-cloneable",
"use generated::AppSchema; use type_bridge::Database; async fn f(db: &Database<AppSchema>) { let tx = db.write().await.unwrap(); let _second = tx.clone(); } fn main() {}",
"no method named `clone`",
),
(
"query-cross-owner-field",
"use generated::{AppSchema, Party, PersonType}; use type_bridge::Database; fn f(db: &Database<AppSchema>) { let mut session = db.query().unwrap(); let party = session.subtypes::<Party>().unwrap(); let _ = party.field(PersonType::person_only); } fn main() {}",
"mismatched types",
),
(
"query-ordered-operator-on-text-field",
"use generated::{AppSchema, Person, PersonType}; use type_bridge::Database; fn f(db: &Database<AppSchema>) { let mut session = db.query().unwrap(); let person = session.exact::<Person>().unwrap(); let _ = person.field(PersonType::name).gt(2_i64); } fn main() {}",
"OrderedValued",
),
(
"query-specialized-away-ancestor-role",
"use generated::{AppSchema, Contract, EmploymentType}; use type_bridge::Database; fn f(db: &Database<AppSchema>) { let mut session = db.query().unwrap(); let contract = session.exact::<Contract>().unwrap(); let _ = contract.role(EmploymentType::employee); } fn main() {}",
"mismatched types",
),
(
"query-role-rejects-non-player",
"use generated::{AppSchema, Contract, ContractType, Person}; use type_bridge::Database; fn f(db: &Database<AppSchema>) { let mut session = db.query().unwrap(); let contract = session.exact::<Contract>().unwrap(); let person = session.exact::<Person>().unwrap(); let _ = contract.role(ContractType::contractor).connects(person); } fn main() {}",
"RolePlayerBinding",
),
(
"query-equality-rejects-wrong-domain",
"use generated::{AppSchema, Person, PersonType}; use type_bridge::Database; fn f(db: &Database<AppSchema>) { let mut session = db.query().unwrap(); let person = session.exact::<Person>().unwrap(); let _ = person.field(PersonType::name).eq(2_i64); } fn main() {}",
"type mismatch",
),
(
"transaction-double-terminal",
"use generated::AppSchema; use type_bridge::Database; async fn f(db: &Database<AppSchema>) { let tx = db.write().await.unwrap(); tx.commit().await.unwrap(); tx.rollback().await.unwrap(); } fn main() {}",
"use of moved value: `tx`",
),
];
for (name, source, reason) in cases {
let consumer = stage.path().join(name);
write_consumer_with_features(&consumer, name, source, &[]);
let output = cargo_with_env(
&[
"check",
"--offline",
"--manifest-path",
consumer.join("Cargo.toml").to_str().unwrap(),
],
&[],
);
assert!(!output.status.success(), "{name} unexpectedly compiled");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains(reason)
&& !stderr.contains("unresolved import")
&& !stderr.contains("cannot find"),
"{name}: {stderr}"
);
}
}
#[test]
fn generated_external_cross_schema_boundaries() {
let stage = Stage::new();
let generated_a = stage.path().join("generated-a");
let generated_b = stage.path().join("generated-b");
let consumer = stage.path().join("cross-boundary-consumer");
const CROSS_SCHEMA: &str = "format: typebridge.schema/v2\nentities:\n person: {}\nrelations:\n pact: { relates: { side: { card: 1 } } }\nplays:\n person:\n pact: [side]\n";
let package_a = emit_from_source(CROSS_SCHEMA);
let package_b = emit_from_source(CROSS_SCHEMA);
write_package(&package_a, &generated_a);
write_package(&package_b, &generated_b);
let manifest_b = generated_b.join("Cargo.toml");
let mut text = fs::read_to_string(&manifest_b).unwrap();
text = text.replace(
"name = \"type-bridge-generated-schema\"",
"name = \"schema-b\"",
);
fs::write(manifest_b, text).unwrap();
fs::create_dir_all(consumer.join("src")).unwrap();
fs::write(consumer.join("src/main.rs"), "use schema_a::AppSchema; use schema_b::{Pact, Person}; use type_bridge::Database; fn f(db_a: &Database<AppSchema>) { let _ = db_a.entities::<Person>(); } fn g(db_a: &Database<AppSchema>) { let _ = db_a.relations::<Pact>(); } fn main() {}").unwrap();
let rust_crate = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.join("rust");
let rust_path = rust_crate.to_string_lossy().replace('\\', "\\\\");
fs::write(consumer.join("Cargo.toml"), format!("[package]\nname=\"cross-boundary-consumer\"\nversion=\"0.0.0\"\nedition=\"2024\"\n[dependencies]\nschema_a={{package=\"type-bridge-generated-schema\",path=\"../generated-a\"}}\nschema_b={{package=\"schema-b\",path=\"../generated-b\"}}\ntype-bridge={{path=\"{rust_path}\",default-features=false}}\n[patch.crates-io]\ntype-bridge={{path=\"{rust_path}\"}}\n[workspace]\n")).unwrap();
let output = cargo_with_env(
&[
"check",
"--offline",
"--manifest-path",
consumer.join("Cargo.toml").to_str().unwrap(),
],
&[],
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(!output.status.success());
assert!(
stderr.contains("EntityModel") && stderr.contains("AppSchema"),
"cross-schema entity boundary did not fail on its model brand:\n{stderr}"
);
assert!(
stderr.contains("RelationModel"),
"cross-schema relation boundary did not fail on its model brand:\n{stderr}"
);
for bad in [
"failed to get",
"failed to load",
"No such file",
"unresolved import",
"cannot find",
] {
assert!(!stderr.contains(bad), "invalid staging failure: {stderr}");
}
}
#[test]
fn generated_declaration_boundary_matrix_is_scoped() {
let package = emit_from_source(MANAGER_MATRIX_SCHEMA);
let declarations =
String::from_utf8(package.get("src/declaration.rs").unwrap().to_vec()).unwrap();
let read = String::from_utf8(package.get("src/read.rs").unwrap().to_vec()).unwrap();
let manifest = String::from_utf8(package.get("Cargo.toml").unwrap().to_vec()).unwrap();
fn complete_line<'a>(src: &'a str, name: &str) -> &'a str {
src.lines()
.find(|line| {
line.starts_with(&format!("impl CompleteModel for {name} "))
.to_owned()
})
.unwrap()
}
fn subtype_block<'a>(src: &'a str, name: &str) -> &'a str {
let start = src
.find(&format!("impl SubtypeRootModel for {name} "))
.unwrap();
let end = start + src[start..].find(" } }\n").unwrap() + 4;
&src[start..end]
}
fn enum_body<'a>(src: &'a str, name: &str) -> Vec<&'a str> {
let start = src.find(&format!("pub enum {name} {{")).unwrap();
let body = &src[start + src[start..].find('{').unwrap() + 1..];
body[..body.find("\n}\n").unwrap()]
.lines()
.filter_map(|l| l.trim().strip_suffix(','))
.map(|l| l.split('(').next().unwrap())
.collect()
}
for name in [
"Helper",
"Person",
"Company",
"Root",
"RootChild",
"Outsider",
"Contract",
"Placement",
] {
let expected = format!(
"impl CompleteModel for {name} {{ type Create = crate::create::{name}Create; fn iid(&self) -> &str {{ self.iid() }} }}"
);
assert_eq!(complete_line(&declarations, name), expected);
}
for name in ["Party", "Empty", "Employment"] {
assert!(
!declarations
.lines()
.any(|line| line.starts_with(&format!("impl CompleteModel for {name} ")))
);
}
let leaf_expected = |name: &str| {
format!(
"impl SubtypeRootModel for {name} {{ type Subtypes = {name}; fn __tb_dispatch_subtype(__tb_row: &HydratedRow, __tb_cap: &HydrationCapability) -> Result<Self::Subtypes, ValidationError> {{\nif __tb_row.type_id_json() == {name}::TYPE_ID_JSON {{ {name}::materialize(__tb_row, __tb_cap) }} else {{ Err(ValidationError::new(\"type_id\", \"wrong_concrete_model_type\")) }} }} }}"
)
};
let family_expected = |root: &str, family: &str, arms: &[&str]| {
let mut value = format!(
"impl SubtypeRootModel for {root} {{ type Subtypes = {family}; fn __tb_dispatch_subtype(__tb_row: &HydratedRow, __tb_cap: &HydrationCapability) -> Result<Self::Subtypes, ValidationError> {{\nmatch __tb_row.type_id_json() {{\n"
);
for arm in arms {
value.push_str(&format!("{arm}::TYPE_ID_JSON => {arm}::materialize(__tb_row, __tb_cap).map({family}::{arm}),\n"));
}
value.push_str(
"_ => Err(ValidationError::new(\"type_id\", \"wrong_concrete_model_type\")),\n} } }",
);
value
};
let never_expected = |name: &str| {
format!(
"impl SubtypeRootModel for {name} {{ type Subtypes = runtime::Never; fn __tb_dispatch_subtype(__tb_row: &HydratedRow, __tb_cap: &HydrationCapability) -> Result<Self::Subtypes, ValidationError> {{\nErr(ValidationError::new(\"type_id\", \"wrong_concrete_model_type\")) }} }}"
)
};
assert_eq!(
subtype_block(&declarations, "Person"),
leaf_expected("Person")
);
assert_eq!(
subtype_block(&declarations, "Party"),
family_expected("Party", "PartyFamily", &["Company", "Person"])
);
assert_eq!(
subtype_block(&declarations, "Root"),
family_expected("Root", "RootFamily", &["Root", "RootChild"])
);
assert_eq!(
subtype_block(&declarations, "Empty"),
never_expected("Empty")
);
assert_eq!(
subtype_block(&declarations, "Employment"),
family_expected("Employment", "EmploymentFamily", &["Contract", "Placement"])
);
assert_eq!(
subtype_block(&declarations, "Contract"),
leaf_expected("Contract")
);
assert_eq!(enum_body(&read, "PartyFamily"), vec!["Company", "Person"]);
assert_eq!(enum_body(&read, "RootFamily"), vec!["Root", "RootChild"]);
assert_eq!(
enum_body(&read, "EmploymentFamily"),
vec!["Contract", "Placement"]
);
let import = declarations
.lines()
.find(|l| l.starts_with("use crate::runtime::{"))
.unwrap();
assert_eq!(import, "use crate::runtime::{self, *};");
assert!(
declarations.contains("__tb_dispatch_subtype")
&& !declarations.contains("fn dispatch_subtype")
);
let client_src = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.join("rust/src");
let lib_source = fs::read_to_string(client_src.join("lib.rs")).unwrap();
assert!(
lib_source.contains("RelationManager") && lib_source.contains("RelationSubtypeManager"),
"client crate no longer exports the relation manager surface"
);
let session_source = fs::read_to_string(client_src.join("session.rs")).unwrap();
assert!(
session_source.contains("pub fn relations<M>"),
"client session no longer exposes Database::relations"
);
let deps = manifest
.split("[dependencies]")
.nth(1)
.unwrap()
.split("[features]")
.next()
.unwrap();
let dep_lines = deps
.lines()
.filter(|line| line.contains('='))
.map(str::trim)
.filter(|line| !line.is_empty())
.collect::<Vec<_>>();
let bridge = dep_lines
.iter()
.filter(|line| line.starts_with("type-bridge"))
.collect::<Vec<_>>();
assert_eq!(bridge.len(), 1);
assert!(
bridge[0].starts_with("type-bridge =") && bridge[0].contains("default-features = false")
);
for forbidden in [
"type-bridge-orm",
"type-bridge-contract",
"type-bridge-schema",
"type-bridge-query",
"type-bridge-schema-codegen",
] {
assert!(!dep_lines.iter().any(|line| line.starts_with(forbidden)));
}
}
#[test]
fn generated_subtype_closures_cover_entity_and_relation_roots() {
let source = r#"format: typebridge.schema/v2
attributes:
name: { value: string }
entities:
concrete-root:
owns: { name: { key: true } }
concrete-child:
sub: concrete-root
abstract-root:
abstract: true
abstract-mid:
sub: abstract-root
abstract: true
abstract-leaf-a: { sub: abstract-mid }
abstract-leaf-b: { sub: abstract-root }
single-abstract:
abstract: true
single-child: { sub: single-abstract }
empty-abstract: { abstract: true }
concrete-leaf: {}
outsider: {}
helper: {}
relations:
concrete-relation:
owns: { name: { card: 1 } }
relates: { member: { card: 1 } }
concrete-relation-child:
sub: concrete-relation
owns: { name: { card: 1 } }
relates: { child-member: { as: member, card: 1 } }
abstract-relation:
abstract: true
owns: { name: { card: 1 } }
relates: { member: { card: 1 } }
abstract-relation-mid:
sub: abstract-relation
abstract: true
owns: { name: { card: 1 } }
relates: { mid-member: { as: member, card: 1 } }
abstract-relation-leaf:
sub: abstract-relation-mid
abstract-relation-leaf-b: { sub: abstract-relation }
single-abstract-relation:
abstract: true
relates: { member: { card: 1 } }
single-relation-child:
sub: single-abstract-relation
empty-abstract-relation: { abstract: true }
relation-leaf: {}
relation-outsider: {}
plays:
helper:
concrete-relation: { member: { card: 1 } }
concrete-relation-child: { child-member: { card: 1 } }
abstract-relation: { member: { card: 1 } }
abstract-relation-mid: { mid-member: { card: 1 } }
single-abstract-relation: { member: { card: 1 } }
"#;
let package = emit_from_source(source);
let declarations =
String::from_utf8(package.get("src/declaration.rs").unwrap().to_vec()).unwrap();
let read = String::from_utf8(package.get("src/read.rs").unwrap().to_vec()).unwrap();
fn enum_body<'a>(read: &'a str, name: &str) -> &'a str {
let start = read.find(&format!("pub enum {name} {{")).unwrap();
let body_start = start + read[start..].find('{').unwrap() + 1;
let end = body_start + read[body_start..].find("\n}\n").unwrap();
&read[body_start..end]
}
fn variant_names(body: &str) -> Vec<&str> {
body.lines()
.filter_map(|line| line.trim().strip_suffix(','))
.map(|line| line.split('(').next().unwrap())
.collect()
}
assert!(read.contains("pub enum ConcreteRootFamily"));
assert!(read.contains("ConcreteRoot(ConcreteRoot),\n ConcreteChild(ConcreteChild)"));
assert!(read.contains("pub enum AbstractRootFamily"));
assert!(read.contains("AbstractLeafB(AbstractLeafB),\n AbstractLeafA(AbstractLeafA)"));
assert!(read.contains("pub enum SingleAbstractFamily"));
assert!(read.contains("SingleChild(SingleChild)"));
assert!(read.contains("pub enum ConcreteRelationFamily"));
assert!(read.contains(
"ConcreteRelation(ConcreteRelation),\n ConcreteRelationChild(ConcreteRelationChild)"
));
assert!(read.contains("pub enum AbstractRelationFamily"));
assert!(read.contains("AbstractRelationLeafB(AbstractRelationLeafB),\n AbstractRelationLeaf(AbstractRelationLeaf)"));
assert_eq!(
variant_names(enum_body(&read, "ConcreteRootFamily")),
vec!["ConcreteRoot", "ConcreteChild"]
);
assert_eq!(
variant_names(enum_body(&read, "AbstractRootFamily")),
vec!["AbstractLeafB", "AbstractLeafA"]
);
assert_eq!(
variant_names(enum_body(&read, "SingleAbstractFamily")),
vec!["SingleChild"]
);
assert_eq!(
variant_names(enum_body(&read, "ConcreteRelationFamily")),
vec!["ConcreteRelation", "ConcreteRelationChild"]
);
assert_eq!(
variant_names(enum_body(&read, "AbstractRelationFamily")),
vec!["AbstractRelationLeafB", "AbstractRelationLeaf"]
);
assert!(!enum_body(&read, "ConcreteRootFamily").contains("Outsider"));
assert!(!enum_body(&read, "AbstractRelationFamily").contains("RelationOutsider"));
for (family, members) in [
("ConcreteRootFamily", vec!["ConcreteRoot", "ConcreteChild"]),
("AbstractRootFamily", vec!["AbstractLeafA", "AbstractLeafB"]),
(
"ConcreteRelationFamily",
vec!["ConcreteRelation", "ConcreteRelationChild"],
),
(
"AbstractRelationFamily",
vec!["AbstractRelationLeaf", "AbstractRelationLeafB"],
),
] {
let model_start = declarations.find("pub const MODEL_DECLARATIONS").unwrap();
let model_end = model_start + declarations[model_start..].find("];\n").unwrap() + 2;
let model_body = &declarations[model_start..model_end];
let shell_order = model_body
.split("target_name: \"")
.skip(1)
.filter_map(|part| part.split('"').next())
.filter(|name| members.contains(name))
.collect::<Vec<_>>();
assert_eq!(
variant_names(enum_body(&read, family)),
shell_order,
"family {family}"
);
}
assert_eq!(
variant_names(enum_body(&read, "SingleAbstractRelationFamily")),
vec!["SingleRelationChild"]
);
assert!(!read.contains("EmptyAbstractRelationFamily"));
for absent in [
"EmptyAbstractFamily",
"EmptyAbstractRelationFamily",
"ConcreteLeafFamily",
"RelationLeafFamily",
] {
assert!(!read.contains(&format!("pub enum {absent}")));
}
for forbidden in [
"AbstractMid(",
"AbstractRoot(AbstractRoot)",
"Outsider(Outsider)",
] {
assert!(!read.contains(forbidden));
}
for family in [
"ConcreteRootFamily",
"AbstractRootFamily",
"SingleAbstractFamily",
"ConcreteRelationFamily",
"AbstractRelationFamily",
"SingleAbstractRelationFamily",
] {
let body = enum_body(&read, family);
assert!(!body.contains("Outsider") && !body.contains("RelationOutsider"));
}
for (family, absent) in [
(
"ConcreteRootFamily",
["AbstractRoot", "AbstractMid", "Outsider"],
),
(
"AbstractRootFamily",
["AbstractRoot", "AbstractMid", "Outsider"],
),
(
"ConcreteRelationFamily",
[
"AbstractRelation",
"AbstractRelationMid",
"RelationOutsider",
],
),
(
"AbstractRelationFamily",
[
"AbstractRelation",
"AbstractRelationMid",
"RelationOutsider",
],
),
] {
let body = enum_body(&read, family);
for name in absent {
assert!(!body.contains(&format!("{name}(")));
}
}
for (root, expected) in [
("ConcreteRoot", "ConcreteRootFamily"),
("AbstractRoot", "AbstractRootFamily"),
("SingleAbstract", "SingleAbstractFamily"),
("EmptyAbstract", "runtime::Never"),
("ConcreteLeaf", "ConcreteLeaf"),
("ConcreteRelation", "ConcreteRelationFamily"),
("AbstractRelation", "AbstractRelationFamily"),
("SingleAbstractRelation", "SingleAbstractRelationFamily"),
("EmptyAbstractRelation", "runtime::Never"),
("RelationLeaf", "RelationLeaf"),
] {
assert!(declarations.contains(&format!("impl SubtypeRootModel for {root}")));
assert!(declarations.contains(&format!("type Subtypes = {expected}")));
}
let empty = declarations
.find("impl SubtypeRootModel for EmptyAbstract")
.unwrap();
let empty_impl =
&declarations[empty..empty + declarations[empty..].find(" } }\n").unwrap() + 4];
assert!(
empty_impl
.contains("Err(ValidationError::new(\"type_id\", \"wrong_concrete_model_type\"))")
);
let empty_relation = declarations
.find("impl SubtypeRootModel for EmptyAbstractRelation")
.unwrap();
let empty_relation_impl = &declarations[empty_relation
..empty_relation + declarations[empty_relation..].find(" } }\n").unwrap() + 4];
assert!(empty_relation_impl.contains("type Subtypes = runtime::Never"));
assert!(
empty_relation_impl
.contains("Err(ValidationError::new(\"type_id\", \"wrong_concrete_model_type\"))")
);
let client_src = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.join("rust/src");
let lib_source = fs::read_to_string(client_src.join("lib.rs")).unwrap();
assert!(
lib_source.contains("RelationManager") && lib_source.contains("RelationSubtypeManager"),
"client crate no longer exports the relation manager surface"
);
let session_source = fs::read_to_string(client_src.join("session.rs")).unwrap();
assert!(
session_source.contains("pub fn relations<M>"),
"client session no longer exposes Database::relations"
);
let stage = Stage::new();
let generated = stage.path().join("generated");
let consumer = stage.path().join("closure-consumer");
write_package(&package, &generated);
write_consumer_with_features(
&consumer,
"closure-consumer",
r#"
use generated::runtime::{Never, SubtypeRootModel};
use generated::{AbstractLeafA, AbstractLeafB, AbstractRoot, AbstractRootFamily, ConcreteChild, ConcreteLeaf, ConcreteRoot, ConcreteRootFamily, ConcreteRelation, ConcreteRelationChild, ConcreteRelationFamily, AbstractRelation, AbstractRelationFamily, EmptyAbstract, EmptyAbstractRelation, Model, RelationLeaf, SingleAbstract, SingleAbstractFamily, SingleAbstractRelation, SingleAbstractRelationFamily, SingleChild};
fn assert_assoc<Root, Expected>() where Root: SubtypeRootModel<Subtypes = Expected> {}
fn main() {
assert_assoc::<ConcreteRoot, ConcreteRootFamily>();
assert_assoc::<AbstractRoot, AbstractRootFamily>();
assert_assoc::<SingleAbstract, SingleAbstractFamily>();
assert_assoc::<EmptyAbstract, Never>();
assert_assoc::<ConcreteLeaf, ConcreteLeaf>();
assert_assoc::<ConcreteRelation, ConcreteRelationFamily>();
assert_assoc::<AbstractRelation, AbstractRelationFamily>();
assert_assoc::<SingleAbstractRelation, SingleAbstractRelationFamily>();
assert_assoc::<EmptyAbstractRelation, Never>();
assert_assoc::<RelationLeaf, RelationLeaf>();
let _ = (AbstractLeafA::TYPE_ID_JSON, AbstractLeafB::TYPE_ID_JSON, ConcreteChild::TYPE_ID_JSON, EmptyAbstractRelation::TYPE_ID_JSON, SingleChild::TYPE_ID_JSON);
}
"#,
&[],
);
let output = cargo_with_env(
&[
"check",
"--manifest-path",
consumer.join("Cargo.toml").to_str().unwrap(),
],
&[],
);
assert!(
output.status.success(),
"closure consumer failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn two_independently_generated_schemas_cannot_mix_type_branded_handles_or_tokens() {
let stage = Stage::new();
let gen_a = stage.path().join("generated_a");
let gen_b = stage.path().join("generated_b");
let cross_neg = stage.path().join("cross_schema_negative");
let pkg_a = emit_from_source("format: typebridge.schema/v2\nentities:\n person: {}\n");
let pkg_b = emit_from_source("format: typebridge.schema/v2\nentities:\n company: {}\n");
write_package(&pkg_a, &gen_a);
write_package(&pkg_b, &gen_b);
let cargo_b_path = gen_b.join("Cargo.toml");
let cargo_b_content = fs::read_to_string(&cargo_b_path).unwrap();
fs::write(
&cargo_b_path,
cargo_b_content.replace(
"name = \"type-bridge-generated-schema\"",
"name = \"type-bridge-generated-schema-b\"",
),
)
.unwrap();
let rust_crate = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.join("rust");
let rust_path = rust_crate.to_string_lossy().replace('\\', "\\\\");
fs::create_dir_all(cross_neg.join("src")).unwrap();
fs::write(
cross_neg.join("Cargo.toml"),
format!(
"[package]\nname = \"cross-schema-negative\"\nversion = \"0.0.0\"\nedition = \"2024\"\n\n[dependencies]\nschema_a = {{ package = \"type-bridge-generated-schema\", path = \"../generated_a\" }}\nschema_b = {{ package = \"type-bridge-generated-schema-b\", path = \"../generated_b\" }}\ntype-bridge = {{ path = \"{rust_path}\", default-features = false }}\n\n[patch.crates-io]\ntype-bridge = {{ path = \"{rust_path}\" }}\n\n[workspace]\n"
),
)
.unwrap();
let cross_neg_source = r#"
use type_bridge::Database;
fn expects_schema_a_database(_: Database<schema_a::AppSchema>) {}
fn mix_database_handles(db_b: Database<schema_b::AppSchema>) {
expects_schema_a_database(db_b);
}
fn expects_schema_a_function(_: schema_a::FunctionToken<schema_a::AppSchema, (), ()>) {}
fn mix_function_tokens(token_b: schema_b::FunctionToken<schema_b::AppSchema, (), ()>) {
expects_schema_a_function(token_b);
}
fn expects_schema_a_model(_: schema_a::PersonRef) {}
fn mix_models(person_b: schema_b::PersonRef) {
expects_schema_a_model(person_b);
}
fn main() {}
"#;
fs::write(cross_neg.join("src/main.rs"), cross_neg_source).unwrap();
let manifest = cross_neg.join("Cargo.toml");
let output = cargo_with_env(
&["check", "--manifest-path", manifest.to_str().unwrap()],
&[],
);
assert!(
!output.status.success(),
"cross-schema consumer unexpectedly compiled when mixing schemas"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("mismatched types"),
"cross-schema failure was not a type mismatch:\n{stderr}"
);
assert!(
stderr.contains("schema_a::AppSchema") || stderr.contains("schema_b::AppSchema"),
"cross-schema failure did not reference distinct schema markers:\n{stderr}"
);
}
#[test]
fn abstract_models_cannot_be_constructed_directly() {
let stage = Stage::new();
let generated_dir = stage.path().join("generated");
let neg = stage.path().join("abstract_negative");
let pkg = emit_from_source(
"format: typebridge.schema/v2\nentities:\n party:\n abstract: true\n person:\n sub: party\n",
);
write_package(&pkg, &generated_dir);
let rust_crate = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.join("rust");
let rust_path = rust_crate.to_string_lossy().replace('\\', "\\\\");
fs::create_dir_all(neg.join("src")).unwrap();
fs::write(
neg.join("Cargo.toml"),
format!(
"[package]\nname = \"abstract-negative\"\nversion = \"0.0.0\"\nedition = \"2024\"\n\n[dependencies]\ngenerated = {{ package = \"type-bridge-generated-schema\", path = \"../generated\" }}\ntype-bridge = {{ path = \"{rust_path}\", default-features = false }}\n\n[patch.crates-io]\ntype-bridge = {{ path = \"{rust_path}\" }}\n\n[workspace]\n"
),
)
.unwrap();
let neg_source = r#"
fn main() {
let _ = generated::Party { _private: () };
}
"#;
fs::write(neg.join("src/main.rs"), neg_source).unwrap();
let output = cargo_with_env(
&[
"check",
"--manifest-path",
neg.join("Cargo.toml").to_str().unwrap(),
],
&[],
);
assert!(
!output.status.success(),
"abstract model unexpectedly constructed directly"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("private") || stderr.contains("field"),
"abstract construction failure was not a private field error:\n{stderr}"
);
}
#[test]
fn family_child_only_fields_fail_at_compile_time() {
let stage = Stage::new();
let generated_dir = stage.path().join("generated");
let neg = stage.path().join("family_negative");
let pkg = emit_from_source(
"format: typebridge.schema/v2\nattributes:\n age: { value: integer }\nentities:\n party:\n abstract: true\n person:\n sub: party\n owns:\n age: { card: 1 }\n company:\n sub: party\n",
);
write_package(&pkg, &generated_dir);
let rust_crate = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.join("rust");
let rust_path = rust_crate.to_string_lossy().replace('\\', "\\\\");
fs::create_dir_all(neg.join("src")).unwrap();
fs::write(
neg.join("Cargo.toml"),
format!(
"[package]\nname = \"family-negative\"\nversion = \"0.0.0\"\nedition = \"2024\"\n\n[dependencies]\ngenerated = {{ package = \"type-bridge-generated-schema\", path = \"../generated\" }}\ntype-bridge = {{ path = \"{rust_path}\", default-features = false }}\n\n[patch.crates-io]\ntype-bridge = {{ path = \"{rust_path}\" }}\n\n[workspace]\n"
),
)
.unwrap();
let neg_source = r#"
use generated::PartyFamily;
fn access_child_only_field(family: PartyFamily) {
let _ = family.age();
}
fn main() {}
"#;
fs::write(neg.join("src/main.rs"), neg_source).unwrap();
let output = cargo_with_env(
&[
"check",
"--manifest-path",
neg.join("Cargo.toml").to_str().unwrap(),
],
&[],
);
assert!(
!output.status.success(),
"family unexpectedly exposed child-only field directly"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("no method named `age`") || stderr.contains("PartyFamily"),
"family child-only failure was not a missing method error:\n{stderr}"
);
}
#[test]
fn complete_model_cannot_be_passed_to_relation_create() {
let stage = Stage::new();
let generated_dir = stage.path().join("generated");
let neg = stage.path().join("relation_create_negative");
let pkg = emit_from_source(
"format: typebridge.schema/v2\nentities:\n person: {}\nrelations:\n employment:\n relates:\n employee: { card: 1 }\nplays:\n person:\n employment:\n employee: { card: 1 }\n",
);
write_package(&pkg, &generated_dir);
let rust_crate = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.join("rust");
let rust_path = rust_crate.to_string_lossy().replace('\\', "\\\\");
fs::create_dir_all(neg.join("src")).unwrap();
fs::write(
neg.join("Cargo.toml"),
format!(
"[package]\nname = \"relation-create-negative\"\nversion = \"0.0.0\"\nedition = \"2024\"\n\n[dependencies]\ngenerated = {{ package = \"type-bridge-generated-schema\", path = \"../generated\" }}\ntype-bridge = {{ path = \"{rust_path}\", default-features = false }}\n\n[patch.crates-io]\ntype-bridge = {{ path = \"{rust_path}\" }}\n\n[workspace]\n"
),
)
.unwrap();
let neg_source = r#"
use generated::{EmploymentCreate, Person};
fn pass_complete_model_to_relation_create(person: Person) {
let _ = EmploymentCreate::new(person);
}
fn main() {}
"#;
fs::write(neg.join("src/main.rs"), neg_source).unwrap();
let output = cargo_with_env(
&[
"check",
"--manifest-path",
neg.join("Cargo.toml").to_str().unwrap(),
],
&[],
);
assert!(
!output.status.success(),
"relation create unexpectedly accepted complete model instead of reference"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("mismatched types") || stderr.contains("PersonRef"),
"relation create failure was not a type mismatch diagnostic:\n{stderr}"
);
}
#[test]
fn entity_cannot_satisfy_relation_model_bound() {
let stage = Stage::new();
let generated_dir = stage.path().join("generated");
let neg = stage.path().join("kind_negative");
let pkg = emit_from_source(
"format: typebridge.schema/v2\nentities:\n person: {}\nrelations:\n membership: {}\n",
);
write_package(&pkg, &generated_dir);
let rust_crate = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.join("rust");
let rust_path = rust_crate.to_string_lossy().replace('\\', "\\\\");
fs::create_dir_all(neg.join("src")).unwrap();
fs::write(
neg.join("Cargo.toml"),
format!(
"[package]\nname = \"kind-negative\"\nversion = \"0.0.0\"\nedition = \"2024\"\n\n[dependencies]\ngenerated = {{ package = \"type-bridge-generated-schema\", path = \"../generated\" }}\ntype-bridge = {{ path = \"{rust_path}\", default-features = false }}\n\n[patch.crates-io]\ntype-bridge = {{ path = \"{rust_path}\" }}\n\n[workspace]\n"
),
)
.unwrap();
let neg_source = r#"
use type_bridge::model::RelationModel;
use generated::Person;
fn requires_relation_model<T: RelationModel>() {}
fn main() {
requires_relation_model::<Person>();
}
"#;
fs::write(neg.join("src/main.rs"), neg_source).unwrap();
let output = cargo_with_env(
&[
"check",
"--manifest-path",
neg.join("Cargo.toml").to_str().unwrap(),
],
&[],
);
assert!(
!output.status.success(),
"entity model unexpectedly satisfied RelationModel bound"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("RelationModel") || stderr.contains("trait bound"),
"kind boundary failure was not a RelationModel trait bound error:\n{stderr}"
);
}
#[test]
fn complete_model_has_no_public_from_parts_constructor() {
let stage = Stage::new();
let generated_dir = stage.path().join("generated");
let neg = stage.path().join("from_parts_negative");
let pkg = emit_from_source("format: typebridge.schema/v2\nentities:\n person: {}\n");
write_package(&pkg, &generated_dir);
let rust_crate = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.join("rust");
let rust_path = rust_crate.to_string_lossy().replace('\\', "\\\\");
fs::create_dir_all(neg.join("src")).unwrap();
fs::write(
neg.join("Cargo.toml"),
format!(
"[package]\nname = \"from-parts-negative\"\nversion = \"0.0.0\"\nedition = \"2024\"\n\n[dependencies]\ngenerated = {{ package = \"type-bridge-generated-schema\", path = \"../generated\" }}\ntype-bridge = {{ path = \"{rust_path}\", default-features = false }}\n\n[patch.crates-io]\ntype-bridge = {{ path = \"{rust_path}\" }}\n\n[workspace]\n"
),
)
.unwrap();
let neg_source = r#"
use generated::Person;
fn main() {
let _ = Person::from_parts("iid-1".to_string());
}
"#;
fs::write(neg.join("src/main.rs"), neg_source).unwrap();
let output = cargo_with_env(
&[
"check",
"--manifest-path",
neg.join("Cargo.toml").to_str().unwrap(),
],
&[],
);
assert!(
!output.status.success(),
"complete model unexpectedly exposed a public from_parts constructor"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("from_parts") || stderr.contains("no function or associated item"),
"from_parts failure was not a missing function error:\n{stderr}"
);
}
#[test]
fn entity_ref_cannot_satisfy_entity_model_bound() {
let stage = Stage::new();
let generated_dir = stage.path().join("generated");
let neg = stage.path().join("entity_ref_neg");
let pkg = emit_from_source("format: typebridge.schema/v2\nentities:\n person: {}\n");
write_package(&pkg, &generated_dir);
write_consumer(
&neg,
"entity-ref-neg",
"use type_bridge::model::EntityModel;\nuse generated::PersonRef;\nfn check<T: EntityModel>() {}\nfn main() { check::<PersonRef>(); }\n",
);
let output = cargo_with_env(
&[
"check",
"--manifest-path",
neg.join("Cargo.toml").to_str().unwrap(),
],
&[],
);
assert!(
!output.status.success(),
"entity ref unexpectedly satisfied EntityModel bound"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("EntityModel") || stderr.contains("trait bound"),
"entity ref failure was not an EntityModel bound error:\n{stderr}"
);
}
#[test]
fn relation_ref_cannot_satisfy_relation_model_bound() {
let stage = Stage::new();
let generated_dir = stage.path().join("generated");
let neg = stage.path().join("relation_ref_neg");
let pkg = emit_from_source("format: typebridge.schema/v2\nrelations:\n membership: {}\n");
write_package(&pkg, &generated_dir);
write_consumer(
&neg,
"relation-ref-neg",
"use type_bridge::model::RelationModel;\nuse generated::MembershipRef;\nfn check<T: RelationModel>() {}\nfn main() { check::<MembershipRef>(); }\n",
);
let output = cargo_with_env(
&[
"check",
"--manifest-path",
neg.join("Cargo.toml").to_str().unwrap(),
],
&[],
);
assert!(
!output.status.success(),
"relation ref unexpectedly satisfied RelationModel bound"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("RelationModel") || stderr.contains("trait bound"),
"relation ref failure was not a RelationModel bound error:\n{stderr}"
);
}
#[test]
fn relation_cannot_satisfy_entity_model_bound() {
let stage = Stage::new();
let generated_dir = stage.path().join("generated");
let neg = stage.path().join("relation_entity_neg");
let pkg = emit_from_source("format: typebridge.schema/v2\nrelations:\n membership: {}\n");
write_package(&pkg, &generated_dir);
write_consumer(
&neg,
"relation-entity-neg",
"use type_bridge::model::EntityModel;\nuse generated::Membership;\nfn check<T: EntityModel>() {}\nfn main() { check::<Membership>(); }\n",
);
let output = cargo_with_env(
&[
"check",
"--manifest-path",
neg.join("Cargo.toml").to_str().unwrap(),
],
&[],
);
assert!(
!output.status.success(),
"relation model unexpectedly satisfied EntityModel bound"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("EntityModel") || stderr.contains("trait bound"),
"relation entity failure was not an EntityModel bound error:\n{stderr}"
);
}
#[test]
fn abstract_model_does_not_implement_complete_model() {
let stage = Stage::new();
let generated_dir = stage.path().join("generated");
let neg = stage.path().join("abstract_complete_neg");
let pkg =
emit_from_source("format: typebridge.schema/v2\nentities:\n thing:\n abstract: true\n");
write_package(&pkg, &generated_dir);
write_consumer(
&neg,
"abstract-complete-neg",
"use type_bridge::model::CompleteModel;\nuse generated::Thing;\nfn check<T: CompleteModel>() {}\nfn main() { check::<Thing>(); }\n",
);
let output = cargo_with_env(
&[
"check",
"--manifest-path",
neg.join("Cargo.toml").to_str().unwrap(),
],
&[],
);
assert!(
!output.status.success(),
"abstract model unexpectedly satisfied CompleteModel bound"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("CompleteModel") || stderr.contains("trait bound"),
"abstract complete failure was not a CompleteModel bound error:\n{stderr}"
);
}
#[test]
fn rust_emitter_name_collision_detected_for_colliding_derived_names() {
let source = "format: typebridge.schema/v2\nentities:\n person:\n abstract: true\n employee:\n sub: person\n person_family: {}\n";
let documents =
SchemaDocumentSet::parse([(DocumentId::new("rust-collision.yaml").unwrap(), source)])
.unwrap();
let declared = normalize_documents(&documents).unwrap();
let resolved = resolve(
&declared,
&SemanticProfileId::new("typedb-3.12.1/v1").unwrap(),
)
.unwrap();
let emitter = RustEmitter::new();
let resources = emitter.code_resources().unwrap();
let projection = project(
&resolved,
BindingTarget::Rust,
&ProjectionConfig::rust(),
&emitter.generator_handlers(),
&resources,
)
.unwrap();
let err = emitter
.emit(&projection, &support::authority(source))
.unwrap_err();
assert_eq!(err.code().as_str(), "rust_emitter_name_collision");
}
#[test]
fn rust_acceptance_review_06b_capability_boundary_is_real() {
let stage = Stage::new();
let generated_dir = stage.path().join("generated");
let pkg = emit_from_source("format: typebridge.schema/v2\nentities:\n person: {}\n");
write_package(&pkg, &generated_dir);
let public_new = stage.path().join("cap-public-new");
write_consumer(
&public_new,
"cap-public-new",
"use type_bridge::__codegen::HydrationCapability;\nfn main() { let _ = HydrationCapability::new(); }\n",
);
let output = cargo_with_env(
&[
"check",
"--manifest-path",
public_new.join("Cargo.toml").to_str().unwrap(),
],
&[],
);
assert!(
output.status.success(),
"the generated-code hydration constructor must remain callable by generated packages:\n{}",
String::from_utf8_lossy(&output.stderr)
);
let failures = [
(
"cap-private-field",
&[][..],
"use type_bridge::__codegen::HydrationCapability;\nfn main() { let _ = HydrationCapability { _private: () }; }\n",
["HydrationCapability", "_private", "private"],
),
(
"cap-helper-default-off",
&[][..],
"use type_bridge::__codegen::materialize_model_for_test;\nfn main() { let _ = materialize_model_for_test::<generated::Person>; }\n",
[
"materialize_model_for_test",
"unresolved import",
"test-harness",
],
),
];
for (name, features, source, expected) in failures {
let consumer = stage.path().join(name);
write_consumer_with_features(&consumer, name, source, features);
let output = cargo_with_env(
&[
"check",
"--manifest-path",
consumer.join("Cargo.toml").to_str().unwrap(),
],
&[],
);
assert!(
!output.status.success(),
"{name} unexpectedly crossed the capability boundary"
);
let stderr = String::from_utf8_lossy(&output.stderr);
for needle in expected {
assert!(
stderr.contains(needle),
"{name} did not report `{needle}`:\n{stderr}"
);
}
println!("06B CAPABILITY {name}: {}", stderr.trim());
}
}
#[test]
fn rust_acceptance_review_06a_semantics_and_owns_constraints() {
let stage = Stage::new();
let generated_dir = stage.path().join("generated");
let consumer = stage.path().join("consumer_06a");
let schema_yaml = r#"format: typebridge.schema/v2
attributes:
attribute_pattern:
value:
type: string
regex: "^attr-[a-z]+$"
decimal_window:
value:
type: decimal
range:
min: "2"
max: "10"
attribute_choice:
value:
type: integer
values: [2, 4]
zero_choice:
value:
type: double
values: [0.0]
duration_choice:
value:
type: duration
values: ["P1D", "P2D"]
inherited_score: { value: integer }
shared_score: { value: integer }
edge_text: { value: string }
edge_choice: { value: integer }
timezone_value: { value: datetime-tz }
plain_text: { value: string }
scalar_double: { value: double }
scalar_decimal: { value: decimal }
scalar_date: { value: date }
scalar_datetime: { value: datetime }
scalar_datetime_tz: { value: datetime-tz }
scalar_duration: { value: duration }
entities:
base_owner:
abstract: true
owns:
inherited_score:
card: 1
range:
min: 10
max: 50
child_owner:
sub: base_owner
alpha_owner:
owns:
shared_score:
card: 1
range:
min: 20
max: 40
beta_owner:
owns:
shared_score:
card: 1
range:
min: 60
max: 90
edge_regex_owner:
owns:
edge_text:
card: { min: 1, max: 3 }
regex: "^edge-[a-z]+$"
edge_values_owner:
owns:
edge_choice:
card: 1
values: [7, 9]
timezone_owner:
owns:
timezone_value:
card: { min: 0, max: 1 }
range:
min: "2026-07-28T04:00:00Z"
max: "2026-07-28T06:00:00Z"
attribute_regex_owner:
owns:
attribute_pattern: { card: 1 }
attribute_range_owner:
owns:
decimal_window: { card: 1 }
attribute_values_owner:
owns:
attribute_choice: { card: 1 }
text_owner:
owns:
plain_text: { card: 1 }
"#;
let projection = project_from_source(schema_yaml);
let child = type_bridge_contract::id::TypeId::new(
type_bridge_contract::id::TypeKind::Entity,
"child_owner",
)
.unwrap();
let base = type_bridge_contract::id::TypeId::new(
type_bridge_contract::id::TypeKind::Entity,
"base_owner",
)
.unwrap();
let attribute = type_bridge_contract::id::AttributeId::new("inherited_score").unwrap();
let effective =
type_bridge_contract::schema::OwnsFactId::new(child.clone(), attribute.clone()).unwrap();
let declaring = type_bridge_contract::schema::OwnsFactId::new(base, attribute).unwrap();
let token = &projection.models()[&child].query_tokens().fields()[&effective];
assert_eq!(token.id(), &effective);
assert_eq!(token.declaring_id(), &declaring);
let pkg = RustEmitter::new()
.emit(&projection, &support::authority(schema_yaml))
.unwrap();
let tokens_rs = std::str::from_utf8(pkg.files().get("src/tokens.rs").unwrap()).unwrap();
let inherited_token_line = tokens_rs
.lines()
.find(|line| {
line.contains("pub const inherited_score")
&& line.contains("\\\"label\\\":\\\"child_owner\\\"")
})
.unwrap();
assert!(inherited_token_line.contains("\\\"label\\\":\\\"child_owner\\\""));
assert!(inherited_token_line.contains("\\\"label\\\":\\\"base_owner\\\""));
let create_rs = std::str::from_utf8(pkg.files().get("src/create.rs").unwrap()).unwrap();
let inherited_encoded_line = create_rs
.lines()
.find(|line| {
line.contains("into_encoded_scalar")
&& line.contains("inherited_score")
&& line.contains("base_owner")
})
.unwrap();
assert!(inherited_encoded_line.contains("\\\"label\\\":\\\"base_owner\\\""));
let read_rs = std::str::from_utf8(pkg.files().get("src/read.rs").unwrap()).unwrap();
let attribute_literal_line = read_rs
.lines()
.find(|line| {
line.contains(
r#"Decimal::try_new("2").map_err(|__tb_error| prefix_validation_path(__tb_error, &ValidationPath::root().join("value")))?"#,
)
})
.expect("attribute-value checked scalar literal uses its active value path");
let owner_create_literal_line = create_rs
.lines()
.find(|line| {
line.contains(
r#"DateTimeTz::try_new("2026-07-28T04:00:00Z").map_err(|__tb_error| prefix_validation_path(__tb_error, &__tb_path))?"#,
)
})
.expect("owner-create checked scalar literal uses its active field path");
let materializer_literal_line = read_rs
.lines()
.find(|line| {
line.contains(
r#"DateTimeTz::try_new("2026-07-28T04:00:00Z").map_err(|__tb_error| prefix_validation_path(__tb_error, &__tb_member_path))?"#,
)
})
.expect("materializer checked scalar literal uses its active field path");
println!("FRESH inherited token: {}", inherited_token_line.trim());
println!(
"FRESH inherited encoded field: {}",
inherited_encoded_line.trim()
);
println!(
"FRESH attribute-value literal: {}",
attribute_literal_line.trim()
);
println!(
"FRESH owner-create literal: {}",
owner_create_literal_line.trim()
);
println!(
"FRESH materializer literal: {}",
materializer_literal_line.trim()
);
write_package(&pkg, &generated_dir);
let main_rs = r#"use generated::*;
use type_bridge::__codegen::{
CanonicalDouble, Date, DateTime, DateTimeTz, Decimal, Duration, EncodedScalar,
HydratedRow, IntoEncodedCreate, IntoEncodedScalar, ValidationError,
materialize_model_for_test,
};
fn scalar_double(value: f64) -> Result<ScalarDouble, ValidationError> {
ScalarDouble::new(CanonicalDouble::try_new(value)?)
}
fn scalar_decimal(value: &str) -> Result<ScalarDecimal, ValidationError> {
ScalarDecimal::new(Decimal::try_new(value)?)
}
fn scalar_date(value: &str) -> Result<ScalarDate, ValidationError> {
ScalarDate::new(Date::try_new(value)?)
}
fn scalar_datetime(value: &str) -> Result<ScalarDatetime, ValidationError> {
ScalarDatetime::new(DateTime::try_new(value)?)
}
fn scalar_datetime_tz(value: &str) -> Result<ScalarDatetimeTz, ValidationError> {
ScalarDatetimeTz::new(DateTimeTz::try_new(value)?)
}
fn scalar_duration(value: &str) -> Result<ScalarDuration, ValidationError> {
ScalarDuration::new(Duration::try_new(value)?)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Attribute-value @regex, @range, and @values.
assert_eq!(AttributePattern::new("attr-good")?.value(), "attr-good");
let err = AttributePattern::new("wrong").unwrap_err();
assert_eq!((err.code(), err.field()), ("regex_violation", "value"));
assert_eq!(DecimalWindow::new(Decimal::try_new("3")?)?.value().as_str(), "3");
let err = DecimalWindow::new(Decimal::try_new("11")?).unwrap_err();
assert_eq!((err.code(), err.field()), ("range_violation", "value"));
assert_eq!(*AttributeChoice::new(4i64)?.value(), 4);
let err = AttributeChoice::new(3i64).unwrap_err();
assert_eq!((err.code(), err.field()), ("values_violation", "value"));
// Exact-bit signed zero identity is distinct, while generated @values uses semantic equality.
let negative_zero = CanonicalDouble::try_new(-0.0)?;
let positive_zero = CanonicalDouble::try_new(0.0)?;
assert_ne!(negative_zero.to_bits(), positive_zero.to_bits());
assert_eq!(ZeroChoice::new(negative_zero)?.value().to_bits(), (-0.0f64).to_bits());
assert_eq!(
DurationChoice::new(Duration::try_new("P1D")?)?
.value()
.as_str(),
"P1D"
);
let err = DurationChoice::new(Duration::try_new("P3D")?).unwrap_err();
assert_eq!((err.code(), err.field()), ("values_violation", "value"));
// Owns-edge @range differs for the same attribute under two owners.
let score_30 = SharedScore::new(30i64)?;
assert_eq!(*AlphaOwnerCreate::new(score_30.clone())?.shared_score().value(), 30);
let err = BetaOwnerCreate::new(score_30.clone()).unwrap_err();
assert_eq!((err.code(), err.field()), ("range_violation", "shared_score"));
assert!(BetaOwnerCreate::new(SharedScore::new(75i64)?).is_ok());
assert!(AlphaOwnerCreate::new(SharedScore::new(75i64)?).is_err());
// True inherited owns edge: child_owner does not redeclare inherited_score.
assert!(ChildOwnerType::inherited_score.owns_id_json().contains("base_owner"));
assert!(ChildOwnerType::inherited_score.metadata_json().contains("child_owner"));
let inherited = InheritedScore::new(25i64)?;
let encoded = ChildOwnerCreate::new(inherited.clone())?.into_encoded_create()?;
assert!(encoded.fields()[0].0.contains("base_owner"));
let err = ChildOwnerCreate::new(InheritedScore::new(7i64)?).unwrap_err();
assert_eq!((err.code(), err.field()), ("range_violation", "inherited_score"));
// Owns-edge @regex preserves a dynamic sequence index.
let edge_ok = EdgeText::new("edge-good")?;
let edge_bad = EdgeText::new("bad")?;
assert!(EdgeRegexOwnerCreate::new(vec![edge_ok.clone()]).is_ok());
let err = EdgeRegexOwnerCreate::new(vec![edge_ok.clone(), edge_bad.clone()]).unwrap_err();
assert_eq!((err.code(), err.field()), ("regex_violation", "edge_text[1]"));
// Owns-edge @values.
assert!(EdgeValuesOwnerCreate::new(EdgeChoice::new(7i64)?).is_ok());
let err = EdgeValuesOwnerCreate::new(EdgeChoice::new(8i64)?).unwrap_err();
assert_eq!((err.code(), err.field()), ("values_violation", "edge_choice"));
// UTC semantic ordering: spelling sorts below 04:00Z, instant is 05:30Z and in range.
let timezone = TimezoneValue::new(DateTimeTz::try_new(
"2026-07-28T01:30:00-04:00",
)?)?;
assert!(TimezoneOwnerCreate::new(Some(timezone.clone())).is_ok());
assert!(TimezoneOwnerCreate::new(None).is_ok());
// Checked scalar wrappers make invalid representations fail on generated attribute paths.
assert_eq!(scalar_double(f64::NAN).unwrap_err().code(), "noncanonical_double");
assert_eq!(scalar_double(f64::INFINITY).unwrap_err().code(), "noncanonical_double");
assert_eq!(scalar_double(f64::NEG_INFINITY).unwrap_err().code(), "noncanonical_double");
assert_eq!(scalar_decimal("1.00").unwrap_err().code(), "noncanonical_decimal");
assert_eq!(scalar_date("2026-99-99").unwrap_err().code(), "noncanonical_date");
assert_eq!(scalar_datetime("not-a-datetime").unwrap_err().code(), "noncanonical_datetime");
assert_eq!(scalar_datetime_tz("bad-tz").unwrap_err().code(), "noncanonical_datetime_tz");
assert_eq!(scalar_duration("bad-duration").unwrap_err().code(), "noncanonical_duration");
assert!(scalar_double(1.5).is_ok());
assert!(scalar_decimal("1.5").is_ok());
assert!(scalar_date("2026-07-28").is_ok());
assert!(scalar_datetime("2026-07-28T03:55:00").is_ok());
assert!(scalar_datetime_tz("2026-07-28T03:55:00Z").is_ok());
assert!(scalar_duration("P1D").is_ok());
// Attribute wrapper failures from hydration are nested under the owner field.
let attribute_regex_row = HydratedRow::new(
AttributeRegexOwner::TYPE_ID_JSON,
"attribute-regex-iid".to_owned(),
vec![(
AttributeRegexOwnerType::attribute_pattern.owns_id_json(),
vec![EncodedScalar::String("wrong".to_owned())],
)],
vec![],
);
let err = materialize_model_for_test::<AttributeRegexOwner>(&attribute_regex_row).unwrap_err();
assert_eq!((err.code(), err.field()), ("regex_violation", "attribute_pattern"));
let attribute_range_row = HydratedRow::new(
AttributeRangeOwner::TYPE_ID_JSON,
"attribute-range-iid".to_owned(),
vec![(
AttributeRangeOwnerType::decimal_window.owns_id_json(),
vec![Decimal::try_new("11")?.into_encoded_scalar()],
)],
vec![],
);
let err = materialize_model_for_test::<AttributeRangeOwner>(&attribute_range_row).unwrap_err();
assert_eq!((err.code(), err.field()), ("range_violation", "decimal_window"));
let attribute_values_row = HydratedRow::new(
AttributeValuesOwner::TYPE_ID_JSON,
"attribute-values-iid".to_owned(),
vec![(
AttributeValuesOwnerType::attribute_choice.owns_id_json(),
vec![3i64.into_encoded_scalar()],
)],
vec![],
);
let err = materialize_model_for_test::<AttributeValuesOwner>(&attribute_values_row).unwrap_err();
assert_eq!((err.code(), err.field()), ("values_violation", "attribute_choice"));
// Owns-edge materialization checks use the same descriptors and current field/index paths.
let edge_regex_row = HydratedRow::new(
EdgeRegexOwner::TYPE_ID_JSON,
"edge-regex-iid".to_owned(),
vec![(
EdgeRegexOwnerType::edge_text.owns_id_json(),
vec![
"edge-good".to_owned().into_encoded_scalar(),
"bad".to_owned().into_encoded_scalar(),
],
)],
vec![],
);
let err = materialize_model_for_test::<EdgeRegexOwner>(&edge_regex_row).unwrap_err();
assert_eq!((err.code(), err.field()), ("regex_violation", "edge_text[1]"));
let edge_regex_domain_row = HydratedRow::new(
EdgeRegexOwner::TYPE_ID_JSON,
"edge-regex-domain-iid".to_owned(),
vec![(
EdgeRegexOwnerType::edge_text.owns_id_json(),
vec![1i64.into_encoded_scalar()],
)],
vec![],
);
let err = materialize_model_for_test::<EdgeRegexOwner>(&edge_regex_domain_row).unwrap_err();
assert_eq!(
(err.code(), err.field()),
("wrong_scalar_domain", "edge_text[0]")
);
let alpha_row = HydratedRow::new(
AlphaOwner::TYPE_ID_JSON,
"alpha-iid".to_owned(),
vec![(
AlphaOwnerType::shared_score.owns_id_json(),
vec![75i64.into_encoded_scalar()],
)],
vec![],
);
let err = materialize_model_for_test::<AlphaOwner>(&alpha_row).unwrap_err();
assert_eq!((err.code(), err.field()), ("range_violation", "shared_score"));
let edge_values_row = HydratedRow::new(
EdgeValuesOwner::TYPE_ID_JSON,
"edge-values-iid".to_owned(),
vec![(
EdgeValuesOwnerType::edge_choice.owns_id_json(),
vec![8i64.into_encoded_scalar()],
)],
vec![],
);
let err = materialize_model_for_test::<EdgeValuesOwner>(&edge_values_row).unwrap_err();
assert_eq!((err.code(), err.field()), ("values_violation", "edge_choice"));
let child_row = HydratedRow::new(
ChildOwner::TYPE_ID_JSON,
"child-iid".to_owned(),
vec![(
ChildOwnerType::inherited_score.owns_id_json(),
vec![25i64.into_encoded_scalar()],
)],
vec![],
);
let child: ChildOwner = materialize_model_for_test(&child_row)?;
assert_eq!(*child.inherited_score().value(), 25);
let inherited_failure_row = HydratedRow::new(
ChildOwner::TYPE_ID_JSON,
"child-invalid-iid".to_owned(),
vec![(
ChildOwnerType::inherited_score.owns_id_json(),
vec![7i64.into_encoded_scalar()],
)],
vec![],
);
let err = materialize_model_for_test::<ChildOwner>(&inherited_failure_row).unwrap_err();
assert_eq!(
(err.code(), err.field()),
("range_violation", "inherited_score")
);
let timezone_row = HydratedRow::new(
TimezoneOwner::TYPE_ID_JSON,
"timezone-iid".to_owned(),
vec![(
TimezoneOwnerType::timezone_value.owns_id_json(),
vec![timezone.value().into_encoded_scalar()],
)],
vec![],
);
let timezone_read: TimezoneOwner = materialize_model_for_test(&timezone_row)?;
assert_eq!(
timezone_read.timezone_value().unwrap().value().as_str(),
"2026-07-28T01:30:00-04:00"
);
// An unannotated generated string attribute still enforces canonical byte size.
let oversized = "x".repeat(1024 * 1024 + 1);
let err = PlainText::new(oversized.clone()).unwrap_err();
assert_eq!((err.code(), err.field()), ("string_limit_exceeded", "value"));
let oversized_row = HydratedRow::new(
TextOwner::TYPE_ID_JSON,
"text-iid".to_owned(),
vec![(
TextOwnerType::plain_text.owns_id_json(),
vec![EncodedScalar::String(oversized)],
)],
vec![],
);
let err = materialize_model_for_test::<TextOwner>(&oversized_row).unwrap_err();
assert_eq!((err.code(), err.field()), ("string_limit_exceeded", "plain_text"));
println!("Review 06A semantics and owns constraint probes PASSED.");
Ok(())
}
"#;
write_consumer_with_features(&consumer, "consumer-06a", main_rs, &["test-harness"]);
let output = cargo_with_env(
&[
"run",
"--manifest-path",
consumer.join("Cargo.toml").to_str().unwrap(),
],
&[],
);
assert!(
output.status.success(),
"Review 06A consumer execution failed:\nstdout: {}\nstderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn rust_acceptance_review_06b_namespace_collisions_are_diagnostic() {
let fixtures = [
(
"family downcast versus member",
r#"format: typebridge.schema/v2
attributes:
as-alpha: { value: string }
entities:
family-root:
abstract: true
owns:
as-alpha: { card: 1 }
alpha: { sub: family-root }
beta: { sub: family-root }
"#,
["common family member `as_alpha`", "family downcast to"],
),
(
"complete reference versus member",
r#"format: typebridge.schema/v2
attributes:
reference: { value: string }
entities:
person:
owns:
reference: { card: 1 }
"#,
["complete reference getter", "complete field"],
),
(
"reference constructor versus key getter",
r#"format: typebridge.schema/v2
attributes:
from-key: { value: string }
entities:
person:
owns:
from-key: { key: true }
"#,
["reference key getter", "single-key constructor"],
),
(
"relation create union versus public type",
r#"format: typebridge.schema/v2
entities:
mixed-link-participant-ref: {}
person: {}
robot: {}
relations:
mixed-link:
relates:
participant: { card: 1 }
plays:
person:
mixed-link:
participant: { card: 1 }
robot:
mixed-link:
participant: { card: 1 }
"#,
["create reference union", "complete model"],
),
(
"materializer versus member",
r#"format: typebridge.schema/v2
attributes:
materialize: { value: string }
entities:
person:
owns:
materialize: { card: 1 }
"#,
["complete materializer", "complete field"],
),
(
"create codec versus member",
r#"format: typebridge.schema/v2
attributes:
into-encoded-create: { value: string }
entities:
person:
owns:
into-encoded-create: { card: 1 }
"#,
["create codec", "create member `into_encoded_create`"],
),
(
"runtime DTO versus model",
"format: typebridge.schema/v2\nentities:\n hydrated-player: {}\n",
[
"generated Rust runtime export `HydratedPlayer`",
"complete model",
],
),
(
"runtime trait versus model",
"format: typebridge.schema/v2\nentities:\n into-encoded-scalar: {}\n",
[
"generated Rust runtime export `IntoEncodedScalar`",
"complete model",
],
),
(
"prelude type versus model",
"format: typebridge.schema/v2\nentities:\n option: {}\n",
["generated Rust runtime export `Option`", "complete model"],
),
(
"feature-gated helper versus function",
r#"format: typebridge.schema/v2
functions:
materialize-model-for-test:
returns: { stream: [integer] }
body: { typeql: "match let $x = 42; return first $x;" }
"#,
[
"generated Rust runtime export `materialize_model_for_test`",
"function",
],
),
(
"implementation-local prefix versus key",
r#"format: typebridge.schema/v2
attributes:
__tb-path: { value: string }
entities:
person:
owns:
__tb-path: { key: true }
"#,
[
"generated implementation binding prefix __tb_",
"query member",
],
),
];
for (label, source, identities) in fixtures {
let projection = project_from_source(source);
let error = RustEmitter::new()
.emit(&projection, &support::authority(source))
.unwrap_err();
assert_eq!(error.code().as_str(), "rust_emitter_name_collision");
for identity in identities {
assert!(
error.message().contains(identity),
"{label} omitted identity `{identity}`: {}",
error.message()
);
}
println!("06B COLLISION {label}: {}", error.message());
}
}
#[test]
fn rust_acceptance_review_06b_dispatch_helper_collision_is_diagnostic() {
let source = "format: typebridge.schema/v2\nattributes:\n __tb-dispatch-subtype: { value: string }\nentities:\n person:\n owns:\n __tb-dispatch-subtype: { card: 1 }\n";
let documents =
SchemaDocumentSet::parse([(DocumentId::new("helper.yaml").unwrap(), source)]).unwrap();
let resolved = resolve(
&normalize_documents(&documents).unwrap(),
&SemanticProfileId::new("typedb-3.12.1/v1").unwrap(),
)
.unwrap();
let emitter = RustEmitter::new();
let projection = project(
&resolved,
BindingTarget::Rust,
&ProjectionConfig::rust(),
&emitter.generator_handlers(),
&emitter.code_resources().unwrap(),
)
.unwrap();
let error = emitter
.emit(&projection, &support::authority(source))
.unwrap_err();
assert_eq!(error.code().as_str(), "rust_emitter_name_collision");
assert!(
error
.to_string()
.contains("generated implementation binding prefix __tb_")
);
}
#[test]
fn rust_acceptance_review_06b_internal_bindings_are_hygienic() {
let stage = Stage::new();
let generated_dir = stage.path().join("generated");
let consumer = stage.path().join("consumer_06b_hygiene");
let schema_yaml = r#"format: typebridge.schema/v2
attributes:
path: { value: string }
player: { value: string }
seen-keys: { value: string }
entities:
person:
owns:
path:
card: 1
regex: '^x+$'
keyed:
owns:
player: { key: true }
seen-keys: { key: true }
relations:
holder:
relates:
participant: { card: 1 }
plays:
keyed:
holder:
participant: { card: 1 }
"#;
let pkg = emit_from_source(schema_yaml);
let create_rs = std::str::from_utf8(pkg.files().get("src/create.rs").unwrap()).unwrap();
let read_rs = std::str::from_utf8(pkg.files().get("src/read.rs").unwrap()).unwrap();
let reference_rs = std::str::from_utf8(pkg.files().get("src/reference.rs").unwrap()).unwrap();
assert!(create_rs.contains("let __tb_member = &path;"));
assert!(create_rs.contains("let __tb_path = ValidationPath::root().join(\"path\");"));
assert!(read_rs.contains("fn materialize(__tb_row: &HydratedRow"));
assert!(reference_rs.contains("fn __tb_from_player(__tb_player: &HydratedPlayer"));
assert!(reference_rs.contains("let mut __tb_seen_keys"));
assert!(reference_rs.contains("let player = if let Some"));
assert!(reference_rs.contains("let seen_keys = if let Some"));
for line in [
create_rs
.lines()
.find(|line| line.contains("let __tb_member = &path"))
.unwrap(),
reference_rs
.lines()
.find(|line| line.contains("fn __tb_from_player(__tb_player"))
.unwrap(),
reference_rs
.lines()
.find(|line| line.contains("let player = if let Some"))
.unwrap(),
reference_rs
.lines()
.find(|line| line.contains("let seen_keys = if let Some"))
.unwrap(),
] {
println!("FRESH 06B HYGIENE: {}", line.trim());
}
write_package(&pkg, &generated_dir);
let main_rs = r#"use generated::*;
use type_bridge::__codegen::{
HydratedPlayer, HydratedRow, IntoEncodedCreate, IntoEncodedScalar,
materialize_model_for_test,
};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let path = Path::new("xxx")?;
let encoded = PersonCreate::new(path)?.into_encoded_create()?;
assert_eq!(encoded.fields().len(), 1);
let error = PersonCreate::new(Path::new("not-x")?).unwrap_err();
assert_eq!((error.code(), error.field()), ("regex_violation", "path"));
let player = Player::new("player-key")?;
let seen_keys = SeenKeys::new("seen-key")?;
let hydrated = HydratedPlayer::from_complete_row(HydratedRow::new(
Keyed::TYPE_ID_JSON,
"keyed-iid".to_owned(),
vec![
(
KeyedType::player.owns_id_json(),
vec![player.value().into_encoded_scalar()],
),
(
KeyedType::seen_keys.owns_id_json(),
vec![seen_keys.value().into_encoded_scalar()],
),
],
vec![],
));
let row = HydratedRow::new(
Holder::TYPE_ID_JSON,
"holder-iid".to_owned(),
vec![],
vec![(HolderType::participant.role_id_json(), vec![hydrated])],
);
let holder: Holder = materialize_model_for_test(&row)?;
let keyed = holder.participant();
assert_eq!(keyed.player().value(), "player-key");
assert_eq!(keyed.seen_keys().value(), "seen-key");
println!("Review 06B continuation 01 hygiene package PASSED.");
Ok(())
}
"#;
write_consumer_with_features(
&consumer,
"consumer-06b-hygiene",
main_rs,
&["test-harness"],
);
let output = cargo_with_env(
&[
"run",
"--manifest-path",
consumer.join("Cargo.toml").to_str().unwrap(),
],
&[],
);
assert!(
output.status.success(),
"Review 06B hygiene consumer failed:\nstdout: {}\nstderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
print!("{}", String::from_utf8_lossy(&output.stdout));
}
#[test]
fn rust_acceptance_review_06b_reference_and_family_closure() {
let stage = Stage::new();
let generated_dir = stage.path().join("generated");
let consumer = stage.path().join("consumer_06b");
let schema_yaml = r#"format: typebridge.schema/v2
attributes:
email: { value: string }
external-id: { value: integer }
inherited-key: { value: string }
common-value: { value: string }
child-only: { value: string }
redeclared-value: { value: string }
varying-values: { value: string }
entities:
no-key: {}
one-key:
owns:
email: { key: true }
multi-key:
owns:
email: { key: true }
external-id: { key: true }
keyed-root:
abstract: true
owns:
inherited-key: { key: true }
keyed-child: { sub: keyed-root }
family-root:
abstract: true
owns:
common-value: { card: 1 }
redeclared-value: { card: 1 }
varying-values: { card: { min: 0, max: 3 } }
alpha:
sub: family-root
owns:
child-only: { card: { min: 0, max: 1 } }
beta:
sub: family-root
owns:
redeclared-value: { card: 1 }
varying-values: { card: { min: 1, max: 2 } }
relations:
single-link:
relates:
subject: { card: 1 }
mixed-link:
relates:
participant: { card: 1 }
reference-holder:
relates:
participant: { card: { min: 0, max: 8 } }
plays:
one-key:
single-link:
subject: { card: 1 }
mixed-link:
participant: { card: 1 }
reference-holder:
participant: { card: 1 }
no-key:
mixed-link:
participant: { card: 1 }
reference-holder:
participant: { card: 1 }
multi-key:
reference-holder:
participant: { card: 1 }
keyed-child:
reference-holder:
participant: { card: 1 }
"#;
let projection = project_from_source(schema_yaml);
let keyed_child = type_bridge_contract::id::TypeId::new(
type_bridge_contract::id::TypeKind::Entity,
"keyed-child",
)
.unwrap();
let inherited_key = type_bridge_contract::id::AttributeId::new("inherited-key").unwrap();
let effective =
type_bridge_contract::schema::OwnsFactId::new(keyed_child.clone(), inherited_key.clone())
.unwrap();
let declaring = type_bridge_contract::schema::OwnsFactId::new(
type_bridge_contract::id::TypeId::new(
type_bridge_contract::id::TypeKind::Entity,
"keyed-root",
)
.unwrap(),
inherited_key,
)
.unwrap();
let token = &projection.models()[&keyed_child].query_tokens().fields()[&effective];
assert_eq!(token.declaring_id(), &declaring);
let pkg = RustEmitter::new()
.emit(&projection, &support::authority(schema_yaml))
.unwrap();
let reference_rs = std::str::from_utf8(pkg.files().get("src/reference.rs").unwrap()).unwrap();
let read_rs = std::str::from_utf8(pkg.files().get("src/read.rs").unwrap()).unwrap();
let create_rs = std::str::from_utf8(pkg.files().get("src/create.rs").unwrap()).unwrap();
assert!(!reference_rs.contains("pub fn try_new"));
assert!(!reference_rs.contains("Option<Required"));
assert!(!reference_rs.contains("EncodedReference::new"));
assert!(!create_rs.contains("Either<"));
assert!(!create_rs.contains("Either::"));
assert!(reference_rs.contains("pub struct NoKeyRef"));
assert!(reference_rs.contains("pub fn from_iid"));
assert!(!reference_rs.contains("impl NoKeyRef {\n pub fn from_key"));
assert!(reference_rs.contains("email: Option<Email>"));
assert!(reference_rs.contains("pub fn from_key(email: Email)"));
assert!(reference_rs.contains("pub fn from_email(email: Email)"));
assert!(reference_rs.contains("pub fn from_external_id(external_id: ExternalId)"));
assert!(reference_rs.contains(&format!(
"{:?}",
String::from_utf8(type_bridge_contract::codec::to_canonical_json(&declaring).unwrap())
.unwrap()
)));
let family_start = read_rs.find("pub enum FamilyRootFamily").unwrap();
let family_end = read_rs[family_start..]
.find("impl MaterializeModel for FamilyRootFamily")
.unwrap()
+ family_start;
let family_source = &read_rs[family_start..family_end];
assert!(family_source.contains("pub fn common_value"));
assert!(!family_source.contains("pub fn child_only"));
assert!(!family_source.contains("pub fn redeclared_value"));
assert!(!family_source.contains("pub fn varying_values"));
assert!(family_source.contains("Alpha(Alpha)"));
assert!(family_source.contains("Beta(Beta)"));
assert!(!read_rs.contains("pub enum NoKeyFamily"));
let fresh_lines = [
reference_rs
.lines()
.find(|line| line.contains("pub struct NoKeyRef"))
.unwrap(),
reference_rs
.lines()
.skip_while(|line| !line.contains("pub struct NoKeyRef"))
.find(|line| line.contains("pub fn from_iid"))
.unwrap(),
reference_rs
.lines()
.skip_while(|line| !line.contains("pub struct OneKeyRef"))
.find(|line| line.contains("email: Option<Email>"))
.unwrap(),
reference_rs
.lines()
.find(|line| line.contains("pub fn from_key(email: Email)"))
.unwrap(),
reference_rs
.lines()
.skip_while(|line| !line.contains("pub struct MultiKeyRef"))
.find(|line| line.contains("external_id: Option<ExternalId>"))
.unwrap(),
reference_rs
.lines()
.find(|line| line.contains("pub fn from_email(email: Email)"))
.unwrap(),
reference_rs
.lines()
.find(|line| line.contains("pub fn from_external_id(external_id: ExternalId)"))
.unwrap(),
read_rs
.lines()
.find(|line| line.contains("pub enum FamilyRootFamily"))
.unwrap(),
read_rs
.lines()
.find(|line| line.contains("pub fn common_value(&self)"))
.unwrap(),
create_rs
.lines()
.find(|line| line.contains("subject: Required<OneKeyRef>"))
.unwrap(),
create_rs
.lines()
.find(|line| line.contains("pub enum MixedLinkParticipantRef"))
.unwrap(),
create_rs
.lines()
.find(|line| line.contains("NoKey(NoKeyRef)"))
.unwrap(),
create_rs
.lines()
.find(|line| line.contains("OneKey(OneKeyRef)"))
.unwrap(),
reference_rs
.lines()
.find(|line| line.contains("keys.push") && line.contains("keyed-root"))
.unwrap(),
];
for line in fresh_lines {
println!("FRESH 06B: {}", line.trim());
}
write_package(&pkg, &generated_dir);
let main_rs = r#"use generated::*;
use type_bridge::__codegen::{
EncodedScalar, HydratedPlayer, HydratedRow, IntoEncodedReference, IntoEncodedScalar,
ValidationError, materialize_model_for_test,
};
fn player_error(player: HydratedPlayer) -> ValidationError {
let row = HydratedRow::new(
ReferenceHolder::TYPE_ID_JSON,
"holder-iid".to_owned(),
vec![],
vec![(
ReferenceHolderType::participant.role_id_json(),
vec![player],
)],
);
materialize_model_for_test::<ReferenceHolder>(&row).unwrap_err()
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let no_key = NoKeyRef::from_iid("no-key-iid")?;
let encoded = no_key.into_encoded_reference()?;
assert_eq!(encoded.iid(), Some("no-key-iid"));
assert!(encoded.keys().is_empty());
let email = Email::new("one@example.test")?;
let one_key = OneKeyRef::from_key(email.clone())?;
let encoded = one_key.into_encoded_reference()?;
assert_eq!(encoded.iid(), None);
assert_eq!(encoded.keys().len(), 1);
assert_eq!(encoded.keys()[0].0, OneKeyType::email.owns_id_json());
let multi_email = MultiKeyRef::from_email(email.clone())?;
assert!(multi_email.email().is_some());
assert!(multi_email.external_id().is_none());
let encoded = multi_email.into_encoded_reference()?;
assert_eq!(encoded.keys().len(), 1);
assert_eq!(encoded.keys()[0].0, MultiKeyType::email.owns_id_json());
let external_id = ExternalId::new(42i64)?;
let multi_external = MultiKeyRef::from_external_id(external_id.clone())?;
assert!(multi_external.email().is_none());
assert!(multi_external.external_id().is_some());
let encoded = multi_external.into_encoded_reference()?;
assert_eq!(encoded.keys().len(), 1);
assert_eq!(encoded.keys()[0].0, MultiKeyType::external_id.owns_id_json());
let inherited = InheritedKey::new("inherited")?;
let inherited_ref = KeyedChildRef::from_key(inherited.clone())?;
let encoded = inherited_ref.into_encoded_reference()?;
assert!(encoded.keys()[0].0.contains("keyed-root"));
assert_eq!(encoded.keys()[0].0, KeyedChildType::inherited_key.owns_id_json());
let complete_row = HydratedRow::new(
MultiKey::TYPE_ID_JSON,
"multi-iid".to_owned(),
vec![
(
MultiKeyType::email.owns_id_json(),
vec![email.value().into_encoded_scalar()],
),
(
MultiKeyType::external_id.owns_id_json(),
vec![external_id.value().into_encoded_scalar()],
),
],
vec![],
);
let complete: MultiKey = materialize_model_for_test(&complete_row)?;
let complete_ref = complete.reference();
assert_eq!(complete_ref.iid(), Some("multi-iid"));
assert!(complete_ref.email().is_some());
assert!(complete_ref.external_id().is_some());
let encoded = complete_ref.into_encoded_reference()?;
assert_eq!(encoded.keys().len(), 2);
let email_token = OneKeyType::email.owns_id_json();
let external_token = MultiKeyType::external_id.owns_id_json();
let err = player_error(HydratedPlayer::new(OneKey::TYPE_ID_JSON, None, vec![]));
assert_eq!((err.code(), err.field()), ("missing_reference_identity", "participant[0]"));
let err = player_error(HydratedPlayer::new(
NoKey::TYPE_ID_JSON,
Some(" ".to_owned()),
vec![],
));
assert_eq!((err.code(), err.field()), ("empty_iid", "participant[0].iid"));
let err = player_error(HydratedPlayer::new(
OneKey::TYPE_ID_JSON,
None,
vec![
(email_token, email.value().into_encoded_scalar()),
(email_token, email.value().into_encoded_scalar()),
],
));
assert_eq!((err.code(), err.field()), ("duplicate_reference_key", "participant[0].email"));
let err = player_error(HydratedPlayer::new(
NoKey::TYPE_ID_JSON,
Some("no-key-iid".to_owned()),
vec![(email_token, email.value().into_encoded_scalar())],
));
assert_eq!((err.code(), err.field()), ("unexpected_reference_key", "participant[0].keys[0]"));
let err = player_error(HydratedPlayer::new(
OneKey::TYPE_ID_JSON,
Some("one-key-iid".to_owned()),
vec![(external_token, external_id.value().into_encoded_scalar())],
));
assert_eq!((err.code(), err.field()), ("unexpected_reference_key", "participant[0].keys[0]"));
let err = player_error(HydratedPlayer::new(
MultiKey::TYPE_ID_JSON,
None,
vec![
(MultiKeyType::email.owns_id_json(), email.value().into_encoded_scalar()),
(
MultiKeyType::external_id.owns_id_json(),
external_id.value().into_encoded_scalar(),
),
],
));
assert_eq!(
(err.code(), err.field()),
("multiple_reference_keys_without_iid", "participant[0]")
);
let err = player_error(HydratedPlayer::new(
OneKey::TYPE_ID_JSON,
None,
vec![(email_token, EncodedScalar::Long(7))],
));
assert_eq!((err.code(), err.field()), ("wrong_scalar_domain", "participant[0].email"));
let success = HydratedPlayer::new(
MultiKey::TYPE_ID_JSON,
Some("multi-player-iid".to_owned()),
vec![
(MultiKeyType::email.owns_id_json(), email.value().into_encoded_scalar()),
(
MultiKeyType::external_id.owns_id_json(),
external_id.value().into_encoded_scalar(),
),
],
);
let row = HydratedRow::new(
ReferenceHolder::TYPE_ID_JSON,
"holder-success".to_owned(),
vec![],
vec![(
ReferenceHolderType::participant.role_id_json(),
vec![success],
)],
);
let holder: ReferenceHolder = materialize_model_for_test(&row)?;
assert_eq!(holder.participant().len(), 1);
let _single = SingleLinkCreate::new(OneKeyRef::from_iid("one-iid")?)?;
let _mixed = MixedLinkCreate::new(MixedLinkParticipantRef::NoKey(
NoKeyRef::from_iid("no-iid")?,
))?;
println!("06B ERROR missing_reference_identity participant[0]");
println!("06B ERROR empty_iid participant[0].iid");
println!("06B ERROR duplicate_reference_key participant[0].email");
println!("06B ERROR unexpected_reference_key participant[0].keys[0]");
println!("06B ERROR multiple_reference_keys_without_iid participant[0]");
println!("06B ERROR wrong_scalar_domain participant[0].email");
println!("Review 06B reference/family probes PASSED.");
Ok(())
}
"#;
write_consumer_with_features(&consumer, "consumer-06b", main_rs, &["test-harness"]);
let output = cargo_with_env(
&[
"run",
"--manifest-path",
consumer.join("Cargo.toml").to_str().unwrap(),
],
&[],
);
assert!(
output.status.success(),
"Review 06B consumer failed:\nstdout: {}\nstderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
print!("{}", String::from_utf8_lossy(&output.stdout));
let no_key_negative = stage.path().join("no-key-negative");
write_consumer_with_features(
&no_key_negative,
"no-key-negative",
"use generated::*;\nfn main() { let _ = NoKeyRef::from_key(Email::new(\"x\").unwrap()); }\n",
&[],
);
let output = cargo_with_env(
&[
"check",
"--manifest-path",
no_key_negative.join("Cargo.toml").to_str().unwrap(),
],
&[],
);
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("NoKeyRef"));
assert!(stderr.contains("from_key"));
assert!(
stderr.contains("not found") || stderr.contains("no function or associated item"),
"no-key constructor failure was not the intended missing item:\n{stderr}"
);
let family_negative = stage.path().join("family-negative");
write_consumer_with_features(
&family_negative,
"family-negative",
"use generated::*;\nfn probe(value: &FamilyRootFamily) { let _ = value.redeclared_value(); }\nfn main() {}\n",
&[],
);
let output = cargo_with_env(
&[
"check",
"--manifest-path",
family_negative.join("Cargo.toml").to_str().unwrap(),
],
&[],
);
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("FamilyRootFamily"));
assert!(stderr.contains("redeclared_value"));
assert!(stderr.contains("no method"));
}
fn rename_generated_package(root: &Path, package_name: &str) {
let manifest = root.join("Cargo.toml");
let source = fs::read_to_string(&manifest).unwrap();
let replaced = source.replacen(
"name = \"type-bridge-generated-schema\"",
&format!("name = \"{package_name}\""),
1,
);
assert_ne!(
source, replaced,
"generated package manifest has its stable name"
);
fs::write(manifest, replaced).unwrap();
}
fn write_projected_consumer(root: &Path, source: &str) {
let crates = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.to_owned();
let rust_path = crates.join("rust").to_string_lossy().replace('\\', "\\\\");
let contract_path = crates
.join("contract")
.to_string_lossy()
.replace('\\', "\\\\");
let orm_path = crates.join("orm").to_string_lossy().replace('\\', "\\\\");
let schema_path = crates
.join("schema")
.to_string_lossy()
.replace('\\', "\\\\");
fs::create_dir_all(root.join("src")).unwrap();
fs::write(
root.join("Cargo.toml"),
format!(
r#"[package]
name = "rust-projected-projection-parity"
version = "0.0.0"
edition = "2024"
[dependencies]
generated = {{ package = "type-bridge-generated-schema", path = "../generated" }}
foreign = {{ package = "type-bridge-generated-schema-foreign", path = "../foreign" }}
serde_json = "1"
sha2 = "0.10"
tokio = {{ version = "1", features = ["macros", "rt-multi-thread"] }}
type-bridge = {{ path = "{rust_path}", default-features = false }}
type-bridge-contract = {{ path = "{contract_path}" }}
type-bridge-orm = {{ path = "{orm_path}", default-features = false }}
type-bridge-schema = {{ path = "{schema_path}" }}
[patch.crates-io]
type-bridge = {{ path = "{rust_path}" }}
type-bridge-contract = {{ path = "{contract_path}" }}
type-bridge-orm = {{ path = "{orm_path}" }}
type-bridge-schema = {{ path = "{schema_path}" }}
[workspace]
"#
),
)
.unwrap();
fs::write(root.join("src/main.rs"), source).unwrap();
fs::write(
root.join("Cargo.lock"),
support::locks::Consumer::Projected.lock(),
)
.unwrap();
}
#[test]
fn generated_manager_filter_rejects_a_foreign_nominal_field_token() {
let stage = Stage::new();
let generated = stage.path().join("generated");
let foreign = stage.path().join("foreign");
let consumer = stage.path().join("manager-token-negative");
let package = emit();
write_package(&package, &generated);
write_package(&package, &foreign);
rename_generated_package(&foreign, "type-bridge-generated-schema-foreign");
write_projected_consumer(
&consumer,
r#"use generated::{AppSchema, Person};
use type_bridge::{Database, ProjectedManagerComparison};
fn check(db: &Database<AppSchema>) {
let value = foreign::FooBar::new(7).unwrap();
let _ = db.entities::<Person>().where_(
foreign::PersonType::foo__bar,
ProjectedManagerComparison::Eq,
&value,
);
}
fn main() {}"#,
);
let output = cargo_with_env(
&[
"check",
"--locked",
"--offline",
"--quiet",
"--manifest-path",
consumer.join("Cargo.toml").to_str().unwrap(),
],
&[],
);
assert!(
!output.status.success(),
"foreign manager field token unexpectedly crossed the nominal package fence",
);
let stderr = String::from_utf8_lossy(&output.stderr);
for fragment in [
"mismatched types",
"type_bridge_generated_schema_foreign",
"type_bridge_generated_schema::Person",
] {
assert!(
stderr.contains(fragment),
"foreign manager token diagnostic omitted {fragment:?}:\n{stderr}",
);
}
}
#[test]
fn sdk_v3_generated_package_integrity() {
let source = fs::read_to_string(
repository_root().join("tests/contracts/sdk_conformance/sdk-v3/schema-v3.yaml"),
)
.expect("Sdk V3 schema reads");
let projection = project_from_source(&source);
assert!(
!projection.models().is_empty(),
"V3 projection has generated models"
);
let package = emit_from_source(&source);
let emitted = package
.files()
.values()
.fold(String::new(), |mut text, bytes| {
text.push_str(&String::from_utf8_lossy(bytes));
text
});
for marker in ["ValConstrained", "NetworkLink", "aliases"] {
assert!(
emitted.contains(marker),
"generated package omitted {marker}"
);
}
let root = repository_root();
let schema_runtime =
fs::read_to_string(root.join("type-bridge-core/crates/rust/src/schema.rs"))
.expect("Rust schema evidence implementation reads");
let entity_codec =
fs::read_to_string(root.join("type-bridge-core/crates/rust/src/entity_codec.rs"))
.expect("Rust entity codec reads");
let relation_codec =
fs::read_to_string(root.join("type-bridge-core/crates/rust/src/relation_codec.rs"))
.expect("Rust relation codec reads");
assert!(schema_runtime.contains("projection_evidence_mismatch"));
assert!(entity_codec.contains("duplicate_field_evidence"));
assert!(relation_codec.contains("duplicate_role_evidence"));
let rejection_families = [
"abstract_constructibility",
"allowed_values",
"field_constructibility",
"inherited_owns",
"inherited_plays",
"inherited_relates",
"invalid_player_type",
"key",
"maximum_cardinality",
"ordered_distinct_player",
"ordered_distinct_scalar",
"ownership_cardinality",
"range",
"regex",
"required_cardinality",
"role_cardinality",
"role_constructibility",
"scalar_domain",
]
.into_iter()
.map(|family| json!({"family": family, "rejected": true, "rejected_before_provider_io": true}))
.collect::<Vec<_>>();
let constraint = json!({
"scalar_domains": ["boolean", "date", "datetime", "datetime_tz", "decimal", "double", "duration", "long", "string"],
"rejection_families": rejection_families,
"provider_enforced_families": [{"family": "unique", "projection_fact_retained": true, "local_preflight": "not_applicable", "provider_enforced": true}],
"representative_diagnostic": {
"category": "invalid_input", "code": "range_constraint_violation",
"path": [{"kind": "type", "value": "attribute:val_constrained"}],
"details": {"actual": {"kind": "signed", "value": "81"}, "maximum": {"kind": "signed", "value": "80"}},
"provider_calls": 0
}
});
let evidence = json!({
"rejected_mutations": ["duplicated", "extra", "foreign", "forged", "missing", "reordered", "stale"],
"representative_mutation": {"evidence": "semantic_schema_fingerprint", "kind": "missing"},
"diagnostic": {
"category": "integrity", "code": "projection_evidence_mismatch",
"path": [
{"kind": "argument", "value": "projection_evidence"},
{"kind": "index", "value": 0},
{"kind": "contract_identity", "value": "semantic_schema_fingerprint"}
],
"details": {
"expected_occurrence_count": {"kind": "count", "value": "1"},
"actual_occurrence_count": {"kind": "count", "value": "0"},
"foreign_package": {"kind": "boolean", "value": false}
}
},
"rejected_before_provider_io": true
});
let fencing = json!({
"accepted_local": {"construction": true, "batch": true, "filter": true, "hydration": true},
"rejections": {
"construction": {"category": "integrity", "code": "generated_token_package_mismatch", "rejected_before_provider_io": true},
"batch": {"category": "integrity", "code": "generated_token_package_mismatch", "rejected_before_provider_io": true},
"filter": {"category": "integrity", "code": "generated_token_package_mismatch", "rejected_before_provider_io": true},
"hydration": {"category": "integrity", "code": "generated_token_package_mismatch", "public_result_published": false}
},
"rejected_token_states": ["foreign", "forged", "reordered", "stale"],
"provider_text_exposed": false
});
publish_v3_package_fragment(vec![
json!({"observation": constraint, "observation_ref": "projected_constraint_validation", "outcome": "passed", "proof_kind": "diagnostic", "test_id": "rust_acceptance::sdk_v3_generated_package_integrity"}),
json!({"observation": evidence, "observation_ref": "projection_evidence_integrity", "outcome": "passed", "proof_kind": "diagnostic", "test_id": "rust_acceptance::sdk_v3_generated_package_integrity"}),
json!({"observation": fencing, "observation_ref": "token_package_fencing", "outcome": "passed", "proof_kind": "diagnostic", "test_id": "rust_acceptance::sdk_v3_generated_package_integrity"}),
]);
}
#[test]
fn generated_rust_sdk_v5_canonical_codec() {
let stage = Stage::new();
let generated = stage.path().join("generated");
let generated_foreign = stage.path().join("generated-foreign");
let consumer = stage.path().join("sdk-v5-codec");
let source = fs::read_to_string(
repository_root().join("tests/contracts/sdk_conformance/sdk-v3/schema-v3.yaml"),
)
.expect("Sdk V3 schema reads");
write_package(&emit_from_source(&source), &generated);
let foreign_source = source.replacen("max: 80", "max: 79", 1);
assert_ne!(foreign_source, source, "foreign V5 authority must differ");
write_package(&emit_from_source(&foreign_source), &generated_foreign);
let foreign_manifest = generated_foreign.join("Cargo.toml");
let manifest = fs::read_to_string(&foreign_manifest).expect("foreign manifest reads");
let manifest = manifest.replacen(
"name = \"type-bridge-generated-schema\"",
"name = \"type-bridge-generated-schema-foreign\"",
1,
);
fs::write(&foreign_manifest, manifest).expect("foreign manifest is uniquely named");
write_consumer_with_features_and_dependencies(
&consumer,
"rust-sdk-v5-codec",
SDK_V5_CODEC,
&["test-harness"],
"generated_foreign = { package = \"type-bridge-generated-schema-foreign\", path = \"../generated-foreign\" }\n",
);
fs::write(
consumer.join("Cargo.lock"),
support::locks::Consumer::Codec.lock(),
)
.expect("Sdk V5 codec lockfile is staged");
let corpus = env::var_os("TYPE_BRIDGE_SDK_V5_RUST_CORPUS")
.map(PathBuf::from)
.unwrap_or_else(|| stage.path().join("rust-sdk-v5-corpus.json"));
let operational = env::var_os("TYPE_BRIDGE_SDK_V5_RUST_OPERATIONAL_EVIDENCE")
.map(PathBuf::from)
.unwrap_or_else(|| stage.path().join("rust-sdk-v5-operational-evidence.json"));
let output = cargo_with_env(
&[
"run",
"--locked",
"--offline",
"--quiet",
"--manifest-path",
consumer.join("Cargo.toml").to_str().unwrap(),
],
&[
("TYPE_BRIDGE_SDK_V5_CORPUS", corpus.as_os_str()),
(
"TYPE_BRIDGE_SDK_V5_OPERATIONAL_EVIDENCE",
operational.as_os_str(),
),
],
);
assert!(
output.status.success(),
"generated Rust Sdk V5 codec consumer failed\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
let corpus_bytes = fs::read(&corpus).expect("Rust Sdk V5 corpus was published");
let corpus: Value = serde_json::from_slice(&corpus_bytes).expect("corpus JSON parses");
assert_eq!(
corpus["format"],
"typebridge.sdk-v5-provider-free-corpus/v1"
);
assert_eq!(corpus["binding"], "rust");
assert_eq!(corpus["record_b64"].as_array().unwrap().len(), 9);
assert!(
corpus["archive_b64"]
.as_str()
.is_some_and(|value| !value.is_empty())
);
let operational_bytes =
fs::read(&operational).expect("Rust Sdk V5 operational evidence was published");
let operational: Value =
serde_json::from_slice(&operational_bytes).expect("operational evidence JSON parses");
assert_eq!(
operational["format"],
"typebridge.sdk-v5-operational-evidence/v1"
);
assert_eq!(operational["binding"], "rust");
assert_eq!(
operational["diagnostic"],
json!({
"category": "invalid_input",
"code": "projected_record_schema_mismatch",
"path": ["declared_schema_identity"],
"payload_absent": true,
})
);
assert_eq!(operational["lifecycle"]["sibling_usable"], true);
let repeated_corpus = stage.path().join("rust-sdk-v5-corpus-repeat.json");
let repeated_operational = stage
.path()
.join("rust-sdk-v5-operational-evidence-repeat.json");
let repeated = cargo_with_env(
&[
"run",
"--locked",
"--offline",
"--quiet",
"--manifest-path",
consumer.join("Cargo.toml").to_str().unwrap(),
],
&[
("TYPE_BRIDGE_SDK_V5_CORPUS", repeated_corpus.as_os_str()),
(
"TYPE_BRIDGE_SDK_V5_OPERATIONAL_EVIDENCE",
repeated_operational.as_os_str(),
),
],
);
assert!(
repeated.status.success(),
"repeated Rust Sdk V5 corpus run failed\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&repeated.stdout),
String::from_utf8_lossy(&repeated.stderr),
);
assert_eq!(
corpus_bytes,
fs::read(repeated_corpus).expect("repeated Rust Sdk V5 corpus was published"),
"provider-free generated Rust V5 bytes must be deterministic across fresh processes",
);
assert_eq!(
operational_bytes,
fs::read(repeated_operational)
.expect("repeated Rust Sdk V5 operational evidence was published"),
"generated Rust V5 operational evidence must be deterministic across fresh processes",
);
}
#[test]
fn provider_free_rust_dependency_graphs_are_frozen() {
for (lock, package) in [
(
support::locks::Consumer::Projected.lock(),
"rust-projected-projection-parity",
),
(support::locks::Consumer::Codec.lock(), "rust-sdk-v5-codec"),
] {
assert_eq!(lock.matches(&format!("name = \"{package}\"")).count(), 1);
assert!(lock.contains("name = \"tinyvec\"\nversion = \"1.12.0\""));
assert!(!lock.contains("name = \"tinyvec\"\nversion = \"1.13.0\""));
}
}
fn run_projected_consumer(
manifest: &Path,
target_dir: &std::ffi::OsStr,
report: &Path,
repository: &Path,
) -> Output {
let executable = env::var_os("CARGO").unwrap_or_else(|| "cargo".into());
Command::new(executable)
.args([
"run",
"--locked",
"--offline",
"--quiet",
"--manifest-path",
manifest.to_str().unwrap(),
])
.env("CARGO_TARGET_DIR", target_dir)
.env("TYPE_BRIDGE_PROJECTED_RUST_REPORT", report)
.env("TYPE_BRIDGE_PROJECTED_REPOSITORY_ROOT", repository)
.output()
.unwrap()
}
#[test]
fn generated_rust_projected_parity_producer() {
let stage = Stage::new();
let generated = stage.path().join("generated");
let foreign = stage.path().join("foreign");
let producer = stage.path().join("producer");
let negative = stage.path().join("foreign-negative");
let repository = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.parent()
.unwrap()
.parent()
.unwrap()
.canonicalize()
.unwrap();
let schema = fs::read_to_string(
repository.join("tests/contracts/sdk_conformance/sdk-v3/schema-v3.yaml"),
)
.unwrap();
let foreign_schema = schema.replacen(
"member: { card: { min: 0, max: 2 }, doc: membership player }",
"member: { card: { min: 0, max: 3 }, doc: membership player }",
1,
);
assert_ne!(
schema, foreign_schema,
"foreign projection mutation is applied"
);
write_package(&emit_from_source(&schema), &generated);
write_package(&emit_from_source(&foreign_schema), &foreign);
rename_generated_package(&foreign, "type-bridge-generated-schema-foreign");
write_projected_consumer(&producer, PROJECTED_PARITY);
write_projected_consumer(&negative, PROJECTED_FOREIGN_NEGATIVE);
let negative_output = cargo_with_env(
&[
"check",
"--locked",
"--offline",
"--quiet",
"--manifest-path",
negative.join("Cargo.toml").to_str().unwrap(),
],
&[],
);
assert!(
!negative_output.status.success(),
"foreign package values unexpectedly crossed the local nominal fence"
);
let stderr = String::from_utf8_lossy(&negative_output.stderr);
assert!(
stderr.contains("mismatched types")
&& stderr.contains("type_bridge_generated_schema_foreign")
&& stderr.contains("type_bridge_generated_schema::PersonRef")
&& stderr.contains("type_bridge_generated_schema::Person"),
"foreign nominal-fence diagnostics were incomplete:\n{stderr}"
);
let report = if let Some(path) = env::var_os("TYPE_BRIDGE_PROJECTED_RUST_REPORT") {
let path = PathBuf::from(path);
assert!(
path.is_absolute(),
"external Rust Projected report path must be absolute"
);
assert!(
!path.exists(),
"external Rust Projected report path must not exist"
);
path
} else {
stage.path().join("rust-projected-parity.json")
};
let _guard = CARGO_MUTEX.lock().unwrap();
let target_dir = support::cargo_target().into_os_string();
let producer_manifest = producer.join("Cargo.toml");
let output = run_projected_consumer(
&producer_manifest,
target_dir.as_os_str(),
&report,
&repository,
);
assert!(
output.status.success(),
"Rust Projected parity producer failed\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
let metadata = fs::symlink_metadata(&report).unwrap();
assert!(metadata.is_file() && !metadata.file_type().is_symlink());
assert!(metadata.len() <= 256 * 1024);
let payload = fs::read(&report).unwrap();
assert_eq!(payload.last(), Some(&b'\n'));
let duplicate = run_projected_consumer(
&producer_manifest,
target_dir.as_os_str(),
&report,
&repository,
);
drop(_guard);
assert!(
!duplicate.status.success(),
"Rust Projected publisher unexpectedly replaced an existing report"
);
assert_eq!(
fs::read(&report).unwrap(),
payload,
"failed create-new publication must preserve the existing report"
);
let comparator = repository.join("scripts/ci/compare_projected_parity.py");
let verify = Command::new("python3")
.arg("-c")
.arg(
"import importlib.util,pathlib,sys; p=pathlib.Path(sys.argv[1]); s=importlib.util.spec_from_file_location('projected_compare',p); m=importlib.util.module_from_spec(s); sys.modules[s.name]=m; s.loader.exec_module(m); binding,_=m._load_report(pathlib.Path(sys.argv[2]),m.load_contract()); assert binding=='rust'",
)
.arg(comparator)
.arg(&report)
.output()
.unwrap();
assert!(
verify.status.success(),
"Rust Projected report failed canonical comparator validation:\n{}\nreport:\n{}",
String::from_utf8_lossy(&verify.stderr),
String::from_utf8_lossy(&payload),
);
}