From 87ba2d66b553626ffc37b55c8d9ce2d78dc2003f Mon Sep 17 00:00:00 2001 From: Darin Kishore Date: Sat, 7 Feb 2026 17:51:47 -0800 Subject: [PATCH] test(bamltype): freeze parity oracles and decouple from bridge --- Cargo.lock | 4 +- crates/baml-bridge/TODO.md | 15 - crates/bamltype-derive/src/lib.rs | 2 +- crates/bamltype/AGENTS.md | 29 + crates/bamltype/Cargo.toml | 4 +- .../bamltype/tests/contract_bridge_oracle.rs | 1202 ----------------- .../tests/contract_bridge_ui_messages.rs | 56 +- .../bamltype/tests/contract_frozen_oracle.rs | 1194 ++++++++++++++++ crates/bamltype/tests/golden_parity.rs | 72 + crates/bamltype/tests/property_parity.rs | 456 +++++++ .../bamltype/tests/render_options_parity.rs | 212 +++ 11 files changed, 2010 insertions(+), 1236 deletions(-) delete mode 100644 crates/baml-bridge/TODO.md create mode 100644 crates/bamltype/AGENTS.md delete mode 100644 crates/bamltype/tests/contract_bridge_oracle.rs create mode 100644 crates/bamltype/tests/contract_frozen_oracle.rs create mode 100644 crates/bamltype/tests/golden_parity.rs create mode 100644 crates/bamltype/tests/property_parity.rs create mode 100644 crates/bamltype/tests/render_options_parity.rs diff --git a/Cargo.lock b/Cargo.lock index 5b4d727b..455a6a69 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -524,15 +524,17 @@ name = "bamltype" version = "0.1.0" dependencies = [ "anyhow", - "baml-bridge", "baml-types", "bamltype-derive", + "expect-test", "facet", "facet-reflect", "indexmap", "internal-baml-jinja", "jsonish", "minijinja", + "proptest", + "serde", "serde_json", "sha2", "thiserror", diff --git a/crates/baml-bridge/TODO.md b/crates/baml-bridge/TODO.md deleted file mode 100644 index e314739a..00000000 --- a/crates/baml-bridge/TODO.md +++ /dev/null @@ -1,15 +0,0 @@ -# TODO - -- [x] Add `baml-bridge-derive` proc-macro crate and wire `#[derive(BamlType)]` -- [x] Implement a registry (dependency collection + recursion detection + stable ordering) -- [x] Generate conversions from `BamlValue` with path-aware errors -- [x] Enforce representability invariants (ints, maps, tuples) in the derive macro -- [x] Doc comment extraction → BAML descriptions (type/field/variant) -- [x] Enum descriptions in output renderer -- [x] Add end-to-end integration tests for render/parse/convert flows -- [x] Add list-of-pairs map key representation - -- [x] Add compile-fail UI tests for unsupported serde patterns and types -- [ ] Re-enable jsonish streaming helpers behind a feature flag -- [ ] Port or trim jsonish tests to run without full compiler stack -- [x] Add round-trip property tests and golden schema snapshots diff --git a/crates/bamltype-derive/src/lib.rs b/crates/bamltype-derive/src/lib.rs index d83e291c..fef235ec 100644 --- a/crates/bamltype-derive/src/lib.rs +++ b/crates/bamltype-derive/src/lib.rs @@ -840,7 +840,7 @@ fn parse_baml_container_meta(attr: &Attribute, out: &mut ContainerCompatAttrs) - for meta in parse_meta_list(attr)? { match meta { Meta::NameValue(meta) if meta.path.is_ident("name") => { - out.rename = Some(parse_string_expr(&meta.value, meta.span())?); + out.rename = Some(parse_string_expr(&meta.value, meta.path.span())?); } Meta::NameValue(meta) if meta.path.is_ident("rename_all") => { out.rename_all = Some(parse_rename_rule(&meta.value, meta.span())?); diff --git a/crates/bamltype/AGENTS.md b/crates/bamltype/AGENTS.md new file mode 100644 index 00000000..f9a680c8 --- /dev/null +++ b/crates/bamltype/AGENTS.md @@ -0,0 +1,29 @@ +# AGENTS (bamltype) + +## Baseline Commands + +1. `cargo test -p bamltype --tests` +2. `cargo test -p dspy-rs --test typed_integration --test test_typed_alias --test test_typed_prompt_format` + +## Test Layers + +1. `integration.rs`: runtime behavior and conversion semantics. +2. `contract_frozen_oracle.rs`: frozen contract fixtures for schema/parse/error behavior. +3. `render_options_parity.rs`: frozen fixtures for render option combinations. +4. `golden_parity.rs`: frozen schema text snapshots for canonical examples. +5. `property_parity.rs`: deterministic property tests plus frozen fixtures for key parse/error outputs. +6. `contract_bridge_ui_messages.rs` + `ui.rs`: compile-fail diagnostics and fixture integrity. + +## When Editing Behavior + +1. Run target tests first: + `cargo test -p bamltype --test contract_frozen_oracle --test render_options_parity --test golden_parity --test property_parity --test contract_bridge_ui_messages --test parity_bridge_api --test ui` +2. If a behavior change is intentional, bless fixtures with `UPDATE_EXPECT=1` for the affected tests only. +3. Rerun all `bamltype` tests without `UPDATE_EXPECT`. +4. Record the behavior change and rationale in the commit message. + +## Snapshot Stability Rules + +1. Keep fixture serialization canonical (sorted map keys, explicit fields, deterministic seed for proptest). +2. Prefer snapshotting semantic payloads (value/checks/flags/explanations/error fields) over raw debug dumps when possible. +3. Avoid snapshotting non-deterministic data (pointer addresses, hash iteration order). diff --git a/crates/bamltype/Cargo.toml b/crates/bamltype/Cargo.toml index 255ff736..aa1c3565 100644 --- a/crates/bamltype/Cargo.toml +++ b/crates/bamltype/Cargo.toml @@ -45,6 +45,8 @@ default = ["derive"] derive = [] [dev-dependencies] -baml-bridge = { path = "../baml-bridge" } trybuild = "1.0.96" serde_json = { version = "1", features = ["float_roundtrip", "preserve_order"] } +expect-test = "1.5.0" +proptest = "1.5.0" +serde = { version = "1", features = ["alloc", "derive", "rc"] } diff --git a/crates/bamltype/tests/contract_bridge_oracle.rs b/crates/bamltype/tests/contract_bridge_oracle.rs deleted file mode 100644 index 2033c869..00000000 --- a/crates/bamltype/tests/contract_bridge_oracle.rs +++ /dev/null @@ -1,1202 +0,0 @@ -use std::collections::{BTreeMap, HashMap}; -use std::rc::Rc; -use std::sync::Arc; - -use baml_bridge as legacy; -use baml_bridge::baml_types::{BamlValue, LiteralValue, StreamingMode, TypeIR}; -use bamltype as facet_runtime; - -#[derive(Debug, Clone, PartialEq, legacy::BamlType)] -#[baml(name = "ContractUser")] -#[baml(internal_name = "contract::User")] -struct BridgeUser { - #[baml(alias = "fullName")] - name: String, - age: i64, -} - -#[derive(Debug, Clone, PartialEq)] -#[bamltype::BamlType] -#[baml(name = "ContractUser")] -#[baml(internal_name = "contract::User")] -struct FacetUser { - #[baml(alias = "fullName")] - name: String, - age: i64, -} - -#[derive(Debug, Clone, PartialEq, legacy::BamlType)] -#[baml(name = "ContractShape")] -#[baml(internal_name = "contract::Shape")] -#[baml(tag = "kind")] -enum BridgeShape { - Circle { radius: f64 }, - Square { side: f64 }, -} - -#[derive(Debug, Clone, PartialEq)] -#[bamltype::BamlType] -#[baml(name = "ContractShape")] -#[baml(internal_name = "contract::Shape")] -#[baml(tag = "kind")] -enum FacetShape { - Circle { radius: f64 }, - Square { side: f64 }, -} - -#[derive(Debug, Clone, PartialEq, legacy::BamlType)] -#[baml(internal_name = "contract::Checked")] -struct BridgeChecked { - #[baml(check(label = "positive", expr = "this > 0"))] - value: i64, -} - -#[derive(Debug, Clone, PartialEq)] -#[bamltype::BamlType] -#[baml(internal_name = "contract::Checked")] -struct FacetChecked { - #[baml(check(label = "positive", expr = "this > 0"))] - value: i64, -} - -#[derive(Debug, Clone, PartialEq, legacy::BamlType)] -#[baml(internal_name = "contract::Asserted")] -struct BridgeAsserted { - #[baml(assert(label = "positive", expr = "this > 0"))] - value: i64, -} - -#[derive(Debug, Clone, PartialEq)] -#[bamltype::BamlType] -#[baml(internal_name = "contract::Asserted")] -struct FacetAsserted { - #[baml(assert(label = "positive", expr = "this > 0"))] - value: i64, -} - -#[derive(Debug, Clone, PartialEq, legacy::BamlType)] -#[baml(internal_name = "contract::MapPairs")] -struct BridgeMapPairs { - #[baml(map_key_repr = "pairs")] - values: HashMap, -} - -#[derive(Debug, Clone, PartialEq)] -#[bamltype::BamlType] -#[baml(internal_name = "contract::MapPairs")] -struct FacetMapPairs { - #[baml(map_key_repr = "pairs")] - values: HashMap, -} - -#[derive(Debug, Clone, PartialEq, legacy::BamlType)] -#[baml(name = "OrderB")] -#[baml(internal_name = "contract::OrderB")] -struct BridgeOrderB { - value: i64, -} - -#[derive(Debug, Clone, PartialEq)] -#[bamltype::BamlType] -#[baml(name = "OrderB")] -#[baml(internal_name = "contract::OrderB")] -struct FacetOrderB { - value: i64, -} - -#[derive(Debug, Clone, PartialEq, legacy::BamlType)] -#[baml(name = "OrderA")] -#[baml(internal_name = "contract::OrderA")] -struct BridgeOrderA { - value: i64, -} - -#[derive(Debug, Clone, PartialEq)] -#[bamltype::BamlType] -#[baml(name = "OrderA")] -#[baml(internal_name = "contract::OrderA")] -struct FacetOrderA { - value: i64, -} - -#[derive(Debug, Clone, PartialEq, legacy::BamlType)] -#[baml(name = "OrderRoot")] -#[baml(internal_name = "contract::OrderRoot")] -struct BridgeOrderRoot { - b: BridgeOrderB, - a: BridgeOrderA, -} - -#[derive(Debug, Clone, PartialEq)] -#[bamltype::BamlType] -#[baml(name = "OrderRoot")] -#[baml(internal_name = "contract::OrderRoot")] -struct FacetOrderRoot { - b: FacetOrderB, - a: FacetOrderA, -} - -#[derive(Debug, Clone, PartialEq, legacy::BamlType)] -#[baml(internal_name = "contract::BigIntString")] -struct BridgeBigIntString { - #[baml(int_repr = "string")] - id: u64, -} - -#[derive(Debug, Clone, PartialEq)] -#[bamltype::BamlType] -#[baml(internal_name = "contract::BigIntString")] -struct FacetBigIntString { - #[baml(int_repr = "string")] - id: u64, -} - -#[derive(Debug, Clone, PartialEq, legacy::BamlType)] -#[baml(internal_name = "contract::Unsigned32")] -struct BridgeUnsigned32 { - value: u32, -} - -#[derive(Debug, Clone, PartialEq)] -#[bamltype::BamlType] -#[baml(internal_name = "contract::Unsigned32")] -struct FacetUnsigned32 { - value: u32, -} - -#[derive(Debug, Clone, PartialEq, legacy::BamlType)] -#[baml(internal_name = "contract::FuzzUser")] -struct BridgeFuzzUser { - name: String, - age: u32, - nickname: Option, -} - -#[derive(Debug, Clone, PartialEq)] -#[bamltype::BamlType] -#[baml(internal_name = "contract::FuzzUser")] -struct FacetFuzzUser { - name: String, - age: u32, - nickname: Option, -} - -#[derive(Debug, Clone, PartialEq, legacy::BamlType)] -#[baml(internal_name = "contract::FuzzExplain")] -struct BridgeFuzzExplain { - values: HashMap, -} - -#[derive(Debug, Clone, PartialEq)] -#[bamltype::BamlType] -#[baml(internal_name = "contract::FuzzExplain")] -struct FacetFuzzExplain { - values: HashMap, -} - -#[derive(Debug, Clone, PartialEq, legacy::BamlType)] -#[baml(internal_name = "contract::ColorAlias")] -enum BridgeColorAlias { - Red, - #[baml(alias = "green")] - Green, -} - -#[derive(Debug, Clone, PartialEq)] -#[bamltype::BamlType] -#[baml(internal_name = "contract::ColorAlias")] -enum FacetColorAlias { - Red, - #[baml(alias = "green")] - Green, -} - -#[derive(Debug, Clone, PartialEq, legacy::BamlType)] -#[baml(name = "ContractDocShape")] -#[baml(internal_name = "contract::DocShape")] -#[baml(tag = "type")] -enum BridgeDocShape { - /// A circle, defined by its radius. - Circle { - /// Radius in meters. - radius: f64, - }, - Rectangle { - width: f64, - height: f64, - }, -} - -#[derive(Debug, Clone, PartialEq)] -#[bamltype::BamlType] -#[baml(name = "ContractDocShape")] -#[baml(internal_name = "contract::DocShape")] -#[baml(tag = "type")] -enum FacetDocShape { - /// A circle, defined by its radius. - Circle { - /// Radius in meters. - radius: f64, - }, - Rectangle { - width: f64, - height: f64, - }, -} - -#[derive(Debug, Clone, PartialEq, legacy::BamlType)] -#[baml(internal_name = "contract::BigIntOption")] -struct BridgeBigIntOption { - #[baml(int_repr = "i64")] - id: Option, -} - -#[derive(Debug, Clone, PartialEq)] -#[bamltype::BamlType] -#[baml(internal_name = "contract::BigIntOption")] -struct FacetBigIntOption { - #[baml(int_repr = "i64")] - id: Option, -} - -#[derive(Debug, Clone, PartialEq, legacy::BamlType)] -#[baml(internal_name = "contract::MapKeys")] -struct BridgeMapKeys { - #[baml(map_key_repr = "string")] - values: HashMap, -} - -#[derive(Debug, Clone, PartialEq)] -#[bamltype::BamlType] -#[baml(internal_name = "contract::MapKeys")] -struct FacetMapKeys { - #[baml(map_key_repr = "string")] - values: HashMap, -} - -#[derive(Debug, Clone, PartialEq, legacy::BamlType)] -#[baml(internal_name = "contract::MapKeysOption")] -struct BridgeMapKeysOption { - #[baml(map_key_repr = "string")] - values: Option>, -} - -#[derive(Debug, Clone, PartialEq)] -#[bamltype::BamlType] -#[baml(internal_name = "contract::MapKeysOption")] -struct FacetMapKeysOption { - #[baml(map_key_repr = "string")] - values: Option>, -} - -#[derive(Debug, Clone, PartialEq, legacy::BamlType)] -#[baml(internal_name = "contract::Node")] -struct BridgeNode { - value: i64, - next: Option>, -} - -#[derive(Debug, Clone, PartialEq)] -#[bamltype::BamlType] -#[baml(internal_name = "contract::Node")] -struct FacetNode { - value: i64, - next: Option>, -} - -#[derive(Debug, Clone, PartialEq, legacy::BamlType)] -#[baml(rename_all = "camelCase")] -#[baml(internal_name = "contract::RenameAllUser")] -struct BridgeRenameAllUser { - full_name: String, -} - -#[derive(Debug, Clone, PartialEq)] -#[bamltype::BamlType] -#[baml(rename_all = "camelCase")] -#[baml(internal_name = "contract::RenameAllUser")] -struct FacetRenameAllUser { - full_name: String, -} - -#[derive(Debug, Clone, PartialEq, legacy::BamlType)] -#[baml(internal_name = "contract::RoundtripStruct")] -struct BridgeRoundtripStruct { - name: String, - count: i32, - tags: Vec, - meta: Option, -} - -#[derive(Debug, Clone, PartialEq)] -#[bamltype::BamlType] -#[baml(internal_name = "contract::RoundtripStruct")] -struct FacetRoundtripStruct { - name: String, - count: i32, - tags: Vec, - meta: Option, -} - -#[derive(Debug, Clone, PartialEq, legacy::BamlType)] -#[baml(internal_name = "contract::RoundtripUnitEnum")] -enum BridgeRoundtripUnitEnum { - Alpha, - #[baml(alias = "beta")] - Beta, -} - -#[derive(Debug, Clone, PartialEq)] -#[bamltype::BamlType] -#[baml(internal_name = "contract::RoundtripUnitEnum")] -enum FacetRoundtripUnitEnum { - Alpha, - #[baml(alias = "beta")] - Beta, -} - -#[derive(Debug, Clone, PartialEq, legacy::BamlType)] -#[baml(internal_name = "contract::RoundtripDataEnum")] -#[baml(tag = "kind")] -enum BridgeRoundtripDataEnum { - Message { body: String, count: i64 }, - Empty, -} - -#[derive(Debug, Clone, PartialEq)] -#[bamltype::BamlType] -#[baml(internal_name = "contract::RoundtripDataEnum")] -#[baml(tag = "kind")] -enum FacetRoundtripDataEnum { - Message { body: String, count: i64 }, - Empty, -} - -#[derive(Debug, Clone, PartialEq, legacy::BamlType)] -#[baml(internal_name = "contract::NestedStruct")] -struct BridgeNestedStruct { - title: String, - items: Option>, - metadata: HashMap>, -} - -#[derive(Debug, Clone, PartialEq)] -#[bamltype::BamlType] -#[baml(internal_name = "contract::NestedStruct")] -struct FacetNestedStruct { - title: String, - items: Option>, - metadata: HashMap>, -} - -#[derive(Debug, Clone, PartialEq, legacy::BamlType)] -#[baml(internal_name = "contract::AsUnionColor")] -#[baml(as_union)] -enum BridgeAsUnionColor { - Red, - #[baml(alias = "green")] - Green, -} - -#[derive(Debug, Clone, PartialEq)] -#[bamltype::BamlType] -#[baml(internal_name = "contract::AsUnionColor")] -#[baml(as_union)] -enum FacetAsUnionColor { - Red, - #[baml(alias = "green")] - Green, -} - -struct LegacyI64ObjectAdapter; - -impl legacy::BamlAdapter for LegacyI64ObjectAdapter { - fn type_ir() -> TypeIR { - TypeIR::class("AdapterI64Wrapper") - } - - fn register(reg: &mut legacy::Registry) { - use legacy::internal_baml_jinja::types::{Class, Name}; - - if !reg.mark_type("AdapterI64Wrapper") { - return; - } - - reg.register_class(Class { - name: Name::new("AdapterI64Wrapper".to_string()), - description: None, - namespace: StreamingMode::NonStreaming, - fields: vec![( - Name::new("value".to_string()), - TypeIR::string(), - None, - false, - )], - constraints: Vec::new(), - streaming_behavior: Default::default(), - }); - } - - fn try_from_baml( - value: BamlValue, - mut path: Vec, - ) -> Result { - let map = match value { - BamlValue::Class(_, fields) | BamlValue::Map(fields) => fields, - other => { - return Err(legacy::BamlConvertError::new( - path, - "object", - format!("{other:?}"), - "expected object adapter payload", - )); - } - }; - - let raw_value = map.get("value").ok_or_else(|| { - legacy::BamlConvertError::new( - path.clone(), - "value", - "", - "missing required field", - ) - })?; - path.push("value".to_string()); - - match raw_value { - BamlValue::String(s) => s.parse::().map_err(|_| { - legacy::BamlConvertError::new(path.clone(), "i64", s.clone(), "failed to parse i64") - }), - BamlValue::Int(i) => Ok(*i), - other => Err(legacy::BamlConvertError::new( - path, - "i64", - format!("{other:?}"), - "expected value field to be string or int", - )), - } - } -} - -struct FacetI64ObjectAdapter; - -impl facet_runtime::BamlAdapter for FacetI64ObjectAdapter { - fn type_ir() -> TypeIR { - TypeIR::class("AdapterI64Wrapper") - } - - fn register(reg: &mut facet_runtime::Registry) { - use facet_runtime::internal_baml_jinja::types::{Class, Name}; - - if !reg.mark_type("AdapterI64Wrapper") { - return; - } - - reg.register_class(Class { - name: Name::new("AdapterI64Wrapper".to_string()), - description: None, - namespace: StreamingMode::NonStreaming, - fields: vec![( - Name::new("value".to_string()), - TypeIR::string(), - None, - false, - )], - constraints: Vec::new(), - streaming_behavior: Default::default(), - }); - } - - fn try_from_baml( - value: BamlValue, - mut path: Vec, - ) -> Result { - let map = match value { - BamlValue::Class(_, fields) | BamlValue::Map(fields) => fields, - other => { - return Err(facet_runtime::BamlConvertError::new( - path, - "object", - format!("{other:?}"), - "expected object adapter payload", - )); - } - }; - - let raw_value = map.get("value").ok_or_else(|| { - facet_runtime::BamlConvertError::new( - path.clone(), - "value", - "", - "missing required field", - ) - })?; - path.push("value".to_string()); - - match raw_value { - BamlValue::String(s) => s.parse::().map_err(|_| { - facet_runtime::BamlConvertError::new( - path.clone(), - "i64", - s.clone(), - "failed to parse i64", - ) - }), - BamlValue::Int(i) => Ok(*i), - other => Err(facet_runtime::BamlConvertError::new( - path, - "i64", - format!("{other:?}"), - "expected value field to be string or int", - )), - } - } -} - -#[derive(Debug, Clone, PartialEq, legacy::BamlType)] -#[baml(internal_name = "contract::WithAdapter")] -struct BridgeWithAdapter { - #[baml(with = "LegacyI64ObjectAdapter")] - id: i64, -} - -#[derive(Debug, Clone, PartialEq)] -#[bamltype::BamlType] -#[baml(internal_name = "contract::WithAdapter")] -struct FacetWithAdapter { - #[baml(with = "FacetI64ObjectAdapter")] - id: i64, -} - -mod legacy_collision_a { - use super::legacy; - - #[derive(Debug, Clone, PartialEq, legacy::BamlType)] - pub struct User { - pub name: String, - } -} - -mod legacy_collision_b { - use super::legacy; - - #[derive(Debug, Clone, PartialEq, legacy::BamlType)] - pub struct User { - pub name: String, - } -} - -mod facet_collision_a { - #[derive(Debug, Clone, PartialEq)] - #[bamltype::BamlType] - pub struct User { - pub name: String, - } -} - -mod facet_collision_b { - #[derive(Debug, Clone, PartialEq)] - #[bamltype::BamlType] - pub struct User { - pub name: String, - } -} - -fn sorted_union_string_literals(type_ir: TypeIR) -> Vec { - let TypeIR::Union(union, _) = type_ir else { - panic!("expected union type IR"); - }; - - let mut literals = union - .iter_skip_null() - .into_iter() - .map(|item| match item { - TypeIR::Literal(LiteralValue::String(value), _) => value.clone(), - other => panic!("expected string literal in union, got {other:?}"), - }) - .collect::>(); - literals.sort(); - literals -} - -#[test] -fn contract_render_schema_default_matches_legacy() { - let old = legacy::render_schema::(legacy::RenderOptions::default()) - .expect("legacy render") - .unwrap_or_default(); - let new = facet_runtime::render_schema::(facet_runtime::RenderOptions::default()) - .expect("facet render") - .unwrap_or_default(); - - assert_eq!(old, new); -} - -#[test] -fn contract_render_schema_hoisted_matches_legacy() { - let opts = legacy::RenderOptions::hoist_classes(legacy::HoistClasses::All); - let old = legacy::render_schema::(opts) - .expect("legacy render") - .unwrap_or_default(); - - let opts = facet_runtime::RenderOptions::hoist_classes(facet_runtime::HoistClasses::All); - let new = facet_runtime::render_schema::(opts) - .expect("facet render") - .unwrap_or_default(); - - assert_eq!(old, new); -} - -#[test] -fn contract_render_schema_ordering_matches_legacy() { - let opts = legacy::RenderOptions::hoist_classes(legacy::HoistClasses::All); - let old = legacy::render_schema::(opts) - .expect("legacy render") - .unwrap_or_default(); - - let opts = facet_runtime::RenderOptions::hoist_classes(facet_runtime::HoistClasses::All); - let new = facet_runtime::render_schema::(opts) - .expect("facet render") - .unwrap_or_default(); - - assert_eq!(old, new); -} - -#[test] -fn contract_parse_envelope_matches_legacy() { - let raw = r#"{ "kind": "Circle", "radius": 2.5 }"#; - let old = legacy::parse_llm_output::(raw, true).expect("legacy parse"); - let new = facet_runtime::parse_llm_output::(raw, true).expect("facet parse"); - - assert_eq!(old.baml_value, new.baml_value); - assert_eq!(format!("{:?}", old.flags), format!("{:?}", new.flags)); - assert_eq!(format!("{:?}", old.checks), format!("{:?}", new.checks)); - assert_eq!( - old.explanations - .iter() - .map(ToString::to_string) - .collect::>(), - new.explanations - .iter() - .map(ToString::to_string) - .collect::>() - ); -} - -#[test] -fn contract_parse_streaming_mode_matches_legacy() { - let raw = r#"{ "name": "Ada", "age": 36 }"#; - let old = legacy::parse_llm_output::(raw, false).expect("legacy parse"); - let new = facet_runtime::parse_llm_output::(raw, false).expect("facet parse"); - - assert_eq!(old.value.name, new.value.name); - assert_eq!(old.value.age, new.value.age); - assert_eq!(old.value.nickname, new.value.nickname); - assert_eq!(old.baml_value, new.baml_value); - assert_eq!(format!("{:?}", old.flags), format!("{:?}", new.flags)); - assert_eq!(format!("{:?}", old.checks), format!("{:?}", new.checks)); - assert_eq!( - old.explanations - .iter() - .map(ToString::to_string) - .collect::>(), - new.explanations - .iter() - .map(ToString::to_string) - .collect::>() - ); -} - -#[test] -fn contract_int_repr_string_matches_legacy() { - let raw = r#"{ "id": "18446744073709551615" }"#; - let old = legacy::parse_llm_output::(raw, true).expect("legacy parse"); - let new = facet_runtime::parse_llm_output::(raw, true).expect("facet parse"); - - assert_eq!(old.value.id, new.value.id); - assert_eq!(old.baml_value, new.baml_value); -} - -#[test] -fn contract_map_key_pairs_parse_and_registration_match_legacy() { - let raw = r#"{ "values": [ { "key": 1, "value": "a" }, { "key": 2, "value": "b" } ] }"#; - let old = legacy::parse_llm_output::(raw, true).expect("legacy parse"); - let new = facet_runtime::parse_llm_output::(raw, true).expect("facet parse"); - assert_eq!(old.baml_value, new.baml_value); - - let old_entry = format!( - "{}::values__Entry", - ::baml_internal_name() - ); - let old_class = ::baml_output_format() - .classes - .get(&(old_entry, StreamingMode::NonStreaming)) - .expect("legacy entry class"); - - let new_entry = format!( - "{}::values__Entry", - ::baml_internal_name() - ); - let new_class = ::baml_output_format() - .classes - .get(&(new_entry, StreamingMode::NonStreaming)) - .expect("facet entry class"); - - assert_eq!( - old_class.name.rendered_name(), - new_class.name.rendered_name() - ); -} - -#[test] -fn contract_check_results_match_legacy() { - let raw = r#"{ "value": 3 }"#; - let old = legacy::parse_llm_output::(raw, true).expect("legacy parse"); - let new = facet_runtime::parse_llm_output::(raw, true).expect("facet parse"); - - assert_eq!(format!("{:?}", old.checks), format!("{:?}", new.checks)); -} - -#[test] -fn contract_assert_error_shape_matches_legacy() { - let raw = r#"{ "value": -1 }"#; - let old = legacy::parse_llm_output::(raw, true).expect_err("legacy err"); - let new = facet_runtime::parse_llm_output::(raw, true).expect_err("facet err"); - - let old_failed = match old { - legacy::BamlParseError::ConstraintAssertsFailed { failed } => failed, - other => panic!("legacy expected assert failure, got {other:?}"), - }; - let new_failed = match new { - facet_runtime::BamlParseError::ConstraintAssertsFailed { failed } => failed, - other => panic!("facet expected assert failure, got {other:?}"), - }; - - assert_eq!(format!("{:?}", old_failed), format!("{:?}", new_failed)); -} - -#[test] -fn contract_markdown_flags_and_explanations_match_legacy() { - let raw = "```json\n{ \"name\": \"Ada\", \"age\": 36 }\n```"; - let old = legacy::parse_llm_output::(raw, true).expect("legacy parse"); - let new = facet_runtime::parse_llm_output::(raw, true).expect("facet parse"); - assert_eq!(format!("{:?}", old.flags), format!("{:?}", new.flags)); - - let raw = r#"{ "values": { "ok": 1, "bad": "oops" } }"#; - let old = legacy::parse_llm_output::(raw, true).expect("legacy parse"); - let new = facet_runtime::parse_llm_output::(raw, true).expect("facet parse"); - assert_eq!( - old.explanations - .iter() - .map(ToString::to_string) - .collect::>(), - new.explanations - .iter() - .map(ToString::to_string) - .collect::>() - ); -} - -#[test] -fn contract_unit_enum_alias_parse_matches_legacy() { - let raw = r#""green""#; - let old = legacy::parse_llm_output::(raw, true).expect("legacy parse"); - let new = facet_runtime::parse_llm_output::(raw, true).expect("facet parse"); - - assert_eq!(old.baml_value, new.baml_value); - assert!(matches!(old.value, BridgeColorAlias::Green)); - assert!(matches!(new.value, FacetColorAlias::Green)); -} - -#[test] -fn contract_as_union_type_ir_and_parse_match_legacy() { - let old_literals = - sorted_union_string_literals(::baml_type_ir()); - let new_literals = sorted_union_string_literals( - ::baml_type_ir(), - ); - assert_eq!(old_literals, new_literals); - - let raw = r#""green""#; - let old = legacy::parse_llm_output::(raw, true).expect("legacy parse"); - let new = facet_runtime::parse_llm_output::(raw, true).expect("facet parse"); - - assert_eq!(old.baml_value, new.baml_value); - assert!(matches!(old.value, BridgeAsUnionColor::Green)); - assert!(matches!(new.value, FacetAsUnionColor::Green)); -} - -#[test] -fn contract_data_enum_docs_render_match_legacy() { - let old = legacy::render_schema::(legacy::RenderOptions::default()) - .expect("legacy render") - .unwrap_or_default(); - let new = - facet_runtime::render_schema::(facet_runtime::RenderOptions::default()) - .expect("facet render") - .unwrap_or_default(); - - assert_eq!(old, new); -} - -#[test] -fn contract_int_repr_option_matches_legacy() { - let raw = r#"{ "id": 42 }"#; - let old = legacy::parse_llm_output::(raw, true).expect("legacy parse"); - let new = facet_runtime::parse_llm_output::(raw, true).expect("facet parse"); - assert_eq!(old.baml_value, new.baml_value); - assert_eq!(old.value.id, new.value.id); - - let raw = r#"{ "id": null }"#; - let old = legacy::parse_llm_output::(raw, true).expect("legacy parse"); - let new = facet_runtime::parse_llm_output::(raw, true).expect("facet parse"); - assert_eq!(old.baml_value, new.baml_value); - assert_eq!(old.value.id, new.value.id); -} - -#[test] -fn contract_map_key_repr_string_and_option_match_legacy() { - let raw = r#"{ "values": { "1": "a", "2": "b" } }"#; - let old = legacy::parse_llm_output::(raw, true).expect("legacy parse"); - let new = facet_runtime::parse_llm_output::(raw, true).expect("facet parse"); - assert_eq!(old.baml_value, new.baml_value); - assert_eq!(old.value.values, new.value.values); - - let raw = r#"{ "values": { "10": "x" } }"#; - let old = legacy::parse_llm_output::(raw, true).expect("legacy parse"); - let new = - facet_runtime::parse_llm_output::(raw, true).expect("facet parse"); - assert_eq!(old.baml_value, new.baml_value); - assert_eq!(old.value.values, new.value.values); -} - -#[test] -fn contract_rename_all_matches_legacy() { - let raw = r#"{ "fullName": "Ada" }"#; - let old = legacy::parse_llm_output::(raw, true).expect("legacy parse"); - let new = - facet_runtime::parse_llm_output::(raw, true).expect("facet parse"); - - assert_eq!(old.baml_value, new.baml_value); - assert_eq!(old.value.full_name, new.value.full_name); -} - -#[test] -fn contract_recursion_class_detection_matches_legacy() { - let old_of = ::baml_output_format(); - let new_of = ::baml_output_format(); - - assert_eq!(old_of.recursive_classes, new_of.recursive_classes); - assert!( - old_of - .recursive_classes - .contains(::baml_internal_name()) - ); - assert!( - new_of - .recursive_classes - .contains(::baml_internal_name()) - ); -} - -#[test] -fn contract_internal_name_collision_behavior_matches_legacy() { - let old_a = ::baml_internal_name(); - let old_b = ::baml_internal_name(); - let new_a = ::baml_internal_name(); - let new_b = ::baml_internal_name(); - - assert_ne!(old_a, old_b); - assert_ne!(new_a, new_b); - - let old_a_class = ::baml_output_format() - .classes - .get(&(old_a.to_string(), StreamingMode::NonStreaming)) - .expect("legacy class a missing"); - let old_b_class = ::baml_output_format() - .classes - .get(&(old_b.to_string(), StreamingMode::NonStreaming)) - .expect("legacy class b missing"); - let new_a_class = ::baml_output_format() - .classes - .get(&(new_a.to_string(), StreamingMode::NonStreaming)) - .expect("facet class a missing"); - let new_b_class = ::baml_output_format() - .classes - .get(&(new_b.to_string(), StreamingMode::NonStreaming)) - .expect("facet class b missing"); - - assert_eq!(old_a_class.name.rendered_name(), "User"); - assert_eq!(old_b_class.name.rendered_name(), "User"); - assert_eq!(new_a_class.name.rendered_name(), "User"); - assert_eq!(new_b_class.name.rendered_name(), "User"); -} - -#[test] -fn contract_with_adapter_schema_and_parse_match_legacy() { - let old_of = ::baml_output_format(); - let new_of = ::baml_output_format(); - - let old_adapter = old_of - .classes - .get(&("AdapterI64Wrapper".to_string(), StreamingMode::NonStreaming)) - .expect("legacy adapter class missing"); - let new_adapter = new_of - .classes - .get(&("AdapterI64Wrapper".to_string(), StreamingMode::NonStreaming)) - .expect("facet adapter class missing"); - assert_eq!(old_adapter.name.real_name(), new_adapter.name.real_name()); - assert_eq!( - old_adapter - .fields - .iter() - .map(|(name, _, _, _)| name.real_name()) - .collect::>(), - new_adapter - .fields - .iter() - .map(|(name, _, _, _)| name.real_name()) - .collect::>() - ); - - let old_owner_name = ::baml_internal_name().to_string(); - let new_owner_name = - ::baml_internal_name().to_string(); - let old_owner = old_of - .classes - .get(&(old_owner_name, StreamingMode::NonStreaming)) - .expect("legacy owner class missing"); - let new_owner = new_of - .classes - .get(&(new_owner_name, StreamingMode::NonStreaming)) - .expect("facet owner class missing"); - let (_, old_field_ir, _, _) = old_owner - .fields - .iter() - .find(|(name, _, _, _)| name.real_name() == "id") - .expect("legacy id field missing"); - let (_, new_field_ir, _, _) = new_owner - .fields - .iter() - .find(|(name, _, _, _)| name.real_name() == "id") - .expect("facet id field missing"); - assert_eq!(format!("{old_field_ir:?}"), format!("{new_field_ir:?}")); - - let raw = r#"{ "id": { "value": "9223372036854775807" } }"#; - let old = legacy::parse_llm_output::(raw, true).expect("legacy parse"); - let new = facet_runtime::parse_llm_output::(raw, true).expect("facet parse"); - assert_eq!(old.value.id, new.value.id); - assert_eq!(old.baml_value, new.baml_value); -} - -fn assert_cross_runtime_roundtrip(old_value: Old, new_value: New) -where - Old: Clone + std::fmt::Debug + PartialEq + legacy::ToBamlValue + legacy::BamlValueConvert, - New: Clone - + std::fmt::Debug - + PartialEq - + facet_runtime::compat::ToBamlValue - + facet_runtime::compat::BamlValueConvert, -{ - let old_baml = legacy::ToBamlValue::to_baml_value(&old_value); - let new_baml = facet_runtime::compat::ToBamlValue::to_baml_value(&new_value); - assert_eq!(old_baml, new_baml); - - let old_back = ::try_from_baml_value(old_baml, Vec::new()) - .expect("legacy roundtrip"); - let new_back = - ::try_from_baml_value(new_baml, Vec::new()) - .expect("facet roundtrip"); - assert_eq!(old_back, old_value); - assert_eq!(new_back, new_value); -} - -fn assert_default_adapter_parity(value: T) -where - T: Clone - + std::fmt::Debug - + PartialEq - + legacy::ToBamlValue - + legacy::BamlValueConvert - + facet_runtime::compat::ToBamlValue - + facet_runtime::compat::BamlValueConvert, -{ - let old_baml = legacy::ToBamlValue::to_baml_value(&value); - let new_baml = facet_runtime::compat::ToBamlValue::to_baml_value(&value); - assert_eq!(old_baml, new_baml); - - let old_back = ::try_from_baml_value(old_baml, Vec::new()) - .expect("legacy convert"); - let new_back = - ::try_from_baml_value(new_baml, Vec::new()) - .expect("facet convert"); - assert_eq!(old_back, new_back); - assert_eq!(old_back, value); -} - -#[test] -fn contract_default_adapter_complex_roundtrips_match_legacy() { - let old_struct = BridgeRoundtripStruct { - name: "example".to_string(), - count: 7, - tags: vec!["tag".to_string()], - meta: Some("meta".to_string()), - }; - let new_struct = FacetRoundtripStruct { - name: "example".to_string(), - count: 7, - tags: vec!["tag".to_string()], - meta: Some("meta".to_string()), - }; - assert_cross_runtime_roundtrip(old_struct, new_struct); - - assert_cross_runtime_roundtrip(BridgeRoundtripUnitEnum::Beta, FacetRoundtripUnitEnum::Beta); - - assert_cross_runtime_roundtrip( - BridgeRoundtripDataEnum::Message { - body: "hello".to_string(), - count: 3, - }, - FacetRoundtripDataEnum::Message { - body: "hello".to_string(), - count: 3, - }, - ); - - let mut old_metadata = HashMap::new(); - old_metadata.insert("alpha".to_string(), Some(1)); - old_metadata.insert("beta".to_string(), None); - - let mut new_metadata = HashMap::new(); - new_metadata.insert("alpha".to_string(), Some(1)); - new_metadata.insert("beta".to_string(), None); - - let old_nested = BridgeNestedStruct { - title: "nested".to_string(), - items: Some(vec![BridgeRoundtripStruct { - name: "child".to_string(), - count: 2, - tags: vec!["x".to_string(), "y".to_string()], - meta: None, - }]), - metadata: old_metadata, - }; - let new_nested = FacetNestedStruct { - title: "nested".to_string(), - items: Some(vec![FacetRoundtripStruct { - name: "child".to_string(), - count: 2, - tags: vec!["x".to_string(), "y".to_string()], - meta: None, - }]), - metadata: new_metadata, - }; - assert_cross_runtime_roundtrip(old_nested, new_nested); -} - -#[test] -fn contract_default_adapter_roundtrips_match_legacy() { - assert_default_adapter_parity("hello".to_string()); - assert_default_adapter_parity(true); - assert_default_adapter_parity(123i32); - assert_default_adapter_parity(-99i64); - assert_default_adapter_parity(3.5f32); - assert_default_adapter_parity(9.75f64); - - assert_default_adapter_parity(Some(42i32)); - assert_default_adapter_parity(None::); - assert_default_adapter_parity(vec!["a".to_string(), "b".to_string()]); - assert_default_adapter_parity(Box::new("boxed".to_string())); - assert_default_adapter_parity(Arc::new("arc".to_string())); - assert_default_adapter_parity(Rc::new("rc".to_string())); - - let mut hm = HashMap::new(); - hm.insert("answer".to_string(), 42i32); - assert_default_adapter_parity(hm); - - let mut bt = BTreeMap::new(); - bt.insert("left".to_string(), 1i64); - bt.insert("right".to_string(), 2i64); - assert_default_adapter_parity(bt); -} - -#[test] -fn contract_default_adapter_error_messages_match_legacy() { - let invalid = BamlValue::Class( - "contract::Unsigned32".to_string(), - [("value".to_string(), BamlValue::Int(-1))] - .into_iter() - .collect(), - ); - - let old_err = ::try_from_baml_value( - invalid.clone(), - Vec::new(), - ) - .expect_err("legacy should fail"); - let new_err = - ::try_from_baml_value( - invalid, - Vec::new(), - ) - .expect_err("facet should fail"); - - assert_eq!(old_err.expected, new_err.expected); - assert_eq!(old_err.path, new_err.path); - assert_eq!(old_err.to_string(), new_err.to_string()); -} - -#[test] -fn contract_direct_integer_string_conversion_matches_legacy() { - let old_err = ::try_from_baml_value( - BamlValue::String("123".into()), - Vec::new(), - ) - .expect_err("legacy should reject string->int direct conversion"); - let new_err = ::try_from_baml_value( - BamlValue::String("123".into()), - Vec::new(), - ) - .expect_err("facet should reject string->int direct conversion"); - - assert_eq!(old_err.expected, new_err.expected); -} - -#[test] -fn contract_direct_map_pairs_conversion_matches_legacy() { - let pair_entry = BamlValue::Map( - [ - ("key".to_string(), BamlValue::String("k".to_string())), - ("value".to_string(), BamlValue::Int(1)), - ] - .into_iter() - .collect(), - ); - let raw = BamlValue::List(vec![pair_entry]); - - let old_err = as legacy::BamlValueConvert>::try_from_baml_value( - raw.clone(), - Vec::new(), - ) - .expect_err("legacy should reject map pair-list direct conversion"); - let new_err = - as facet_runtime::compat::BamlValueConvert>::try_from_baml_value( - raw, - Vec::new(), - ) - .expect_err("facet should reject map pair-list direct conversion"); - - assert_eq!(old_err.expected, new_err.expected); -} - -#[test] -fn contract_schema_fingerprint_matches_legacy() { - let old_of = ::baml_output_format(); - let new_of = ::baml_output_format(); - - let old_fp = legacy::schema_fingerprint(old_of, legacy::RenderOptions::default()) - .expect("legacy fingerprint"); - let new_fp = facet_runtime::schema_fingerprint(new_of, facet_runtime::RenderOptions::default()) - .expect("facet fingerprint"); - - assert_eq!(old_fp, new_fp); -} diff --git a/crates/bamltype/tests/contract_bridge_ui_messages.rs b/crates/bamltype/tests/contract_bridge_ui_messages.rs index b8fb307c..be9e4f5f 100644 --- a/crates/bamltype/tests/contract_bridge_ui_messages.rs +++ b/crates/bamltype/tests/contract_bridge_ui_messages.rs @@ -1,14 +1,23 @@ +use std::collections::BTreeMap; use std::fs; use std::path::Path; +use expect_test::expect; +use sha2::{Digest, Sha256}; + +fn sha256_hex(bytes: &[u8]) -> String { + let mut hasher = Sha256::new(); + hasher.update(bytes); + format!("{:x}", hasher.finalize()) +} + #[test] -fn contract_ui_error_messages_match_legacy() { +fn contract_ui_error_messages_match_frozen_fixtures() { let root = Path::new(env!("CARGO_MANIFEST_DIR")); - let legacy_ui = root.join("../baml-bridge/tests/ui"); let facet_ui = root.join("tests/ui"); - let mut compared = 0usize; - for entry in fs::read_dir(&legacy_ui).expect("read legacy ui dir") { + let mut hashes = BTreeMap::new(); + for entry in fs::read_dir(&facet_ui).expect("read facet ui dir") { let entry = entry.expect("read dir entry"); let path = entry.path(); @@ -19,18 +28,33 @@ fn contract_ui_error_messages_match_legacy() { let file_name = path .file_name() .and_then(|name| name.to_str()) - .expect("stderr file name"); - let facet_path = facet_ui.join(file_name); - assert!( - facet_path.exists(), - "missing facet stderr fixture for {file_name}" - ); - - let legacy = fs::read_to_string(&path).expect("read legacy stderr"); - let facet = fs::read_to_string(&facet_path).expect("read facet stderr"); - assert_eq!(legacy, facet, "stderr mismatch for {file_name}"); - compared += 1; + .expect("stderr file name") + .to_string(); + let content = fs::read(&path).expect("read stderr fixture"); + hashes.insert(file_name, sha256_hex(&content)); } - assert!(compared > 0, "no stderr fixtures compared"); + assert!(!hashes.is_empty(), "no stderr fixtures discovered"); + + let snapshot = serde_json::to_string_pretty(&hashes).expect("serialize fixture digest map"); + expect![[r#" + { + "as_enum_data_enum.stderr": "56fbfa048d2c8ee533da741e1521ef5482a2ac8ccd2aaa8bbbe70426cb735d0f", + "function_type.stderr": "150024c5f2be81e9baa6dae06cb64ae050edd8a0f5f2734c02478e01c559577a", + "large_int_without_repr.stderr": "55b8916098c952008baaa4e96fadc792727eccfc40d1319b1aed837e291a8a71", + "map_key_non_string.stderr": "53fdb34520a4b258b21a543103bb9e58c9327d26e6f9ce8026cbb3522cddbbd0", + "map_key_repr_non_map.stderr": "85ea714df372a6c2746c4eb7398a05d5b1edc31de981746ed2d500759263d72e", + "non_string_literal_attr.stderr": "285e7b3fb2da106342895f016126d973d30470bad99d4edcc4edac7e6845c18a", + "serde_default_path.stderr": "7628e1ccec4d97edc09dab392fbd0a39c7fe2b8e55fca8fe35cfa589ac61561d", + "serde_flatten.stderr": "4ecdf04b1000d85a9b503d0d4a7c1e4567524bfe9c58d6c98e70322bc36bb57a", + "serde_json_value.stderr": "ef346adbb204631fe7e96dd1a71ab2d47f4edd37d76610ce753ad8509fa6a9b2", + "serde_skip_variant.stderr": "d89852ff38b3433ab4140d5790dcbaefd9533f88a8eb7c4300f1efd5d2d3b754", + "serde_untagged.stderr": "dfd185a97bc8b99658e55754dc959c10b84167c6dcc6d1713307be656b1e3bf2", + "trait_object.stderr": "dca3ad8c84eb406ebba578d7d5df48c0fcdc0f1fdd2c0854221814ee78cd19fe", + "tuple_enum_variant.stderr": "8dcc1098be3166c6cdbdbe23470f78230fd81f819d6fc58657474d221f3b8e12", + "tuple_field.stderr": "e0cfa7477dc9ebd083734ba9b4ae6e81da0c7ccc02f609a1a50547aa10adccac", + "tuple_struct.stderr": "c540e375631f795a8f421ddfbad48e75808e8798ba120e4a7adaa742ceac97a4", + "unit_struct.stderr": "82b287a2199e20a5febf675be80f9d175047a9f046c0356a90b28c7367e9d269", + "unsupported_baml_attr.stderr": "6a068b8fe63e74d9d408d85de4345be41bb6517e601024633a115cf0b677d667" + }"#]].assert_eq(&snapshot); } diff --git a/crates/bamltype/tests/contract_frozen_oracle.rs b/crates/bamltype/tests/contract_frozen_oracle.rs new file mode 100644 index 00000000..1c4652fd --- /dev/null +++ b/crates/bamltype/tests/contract_frozen_oracle.rs @@ -0,0 +1,1194 @@ +use std::collections::{BTreeMap, HashMap}; +use std::rc::Rc; +use std::sync::Arc; + +use bamltype as facet_runtime; +use bamltype::baml_types::{BamlValue, LiteralValue, StreamingMode, TypeIR}; +use expect_test::expect; +use serde_json::json; + +#[derive(Debug, Clone, PartialEq)] +#[bamltype::BamlType] +#[baml(name = "ContractUser")] +#[baml(internal_name = "contract::User")] +struct FacetUser { + #[baml(alias = "fullName")] + name: String, + age: i64, +} + +#[derive(Debug, Clone, PartialEq)] +#[bamltype::BamlType] +#[baml(name = "ContractShape")] +#[baml(internal_name = "contract::Shape")] +#[baml(tag = "kind")] +enum FacetShape { + Circle { radius: f64 }, + Square { side: f64 }, +} + +#[derive(Debug, Clone, PartialEq)] +#[bamltype::BamlType] +#[baml(internal_name = "contract::Checked")] +struct FacetChecked { + #[baml(check(label = "positive", expr = "this > 0"))] + value: i64, +} + +#[derive(Debug, Clone, PartialEq)] +#[bamltype::BamlType] +#[baml(internal_name = "contract::Asserted")] +struct FacetAsserted { + #[baml(assert(label = "positive", expr = "this > 0"))] + value: i64, +} + +#[derive(Debug, Clone, PartialEq)] +#[bamltype::BamlType] +#[baml(internal_name = "contract::MapPairs")] +struct FacetMapPairs { + #[baml(map_key_repr = "pairs")] + values: HashMap, +} + +#[derive(Debug, Clone, PartialEq)] +#[bamltype::BamlType] +#[baml(name = "OrderB")] +#[baml(internal_name = "contract::OrderB")] +struct FacetOrderB { + value: i64, +} + +#[derive(Debug, Clone, PartialEq)] +#[bamltype::BamlType] +#[baml(name = "OrderA")] +#[baml(internal_name = "contract::OrderA")] +struct FacetOrderA { + value: i64, +} + +#[derive(Debug, Clone, PartialEq)] +#[bamltype::BamlType] +#[baml(name = "OrderRoot")] +#[baml(internal_name = "contract::OrderRoot")] +struct FacetOrderRoot { + b: FacetOrderB, + a: FacetOrderA, +} + +#[derive(Debug, Clone, PartialEq)] +#[bamltype::BamlType] +#[baml(internal_name = "contract::BigIntString")] +struct FacetBigIntString { + #[baml(int_repr = "string")] + id: u64, +} + +#[derive(Debug, Clone, PartialEq)] +#[bamltype::BamlType] +#[baml(internal_name = "contract::Unsigned32")] +struct FacetUnsigned32 { + value: u32, +} + +#[derive(Debug, Clone, PartialEq)] +#[bamltype::BamlType] +#[baml(internal_name = "contract::FuzzUser")] +struct FacetFuzzUser { + name: String, + age: u32, + nickname: Option, +} + +#[derive(Debug, Clone, PartialEq)] +#[bamltype::BamlType] +#[baml(internal_name = "contract::FuzzExplain")] +struct FacetFuzzExplain { + values: HashMap, +} + +#[derive(Debug, Clone, PartialEq)] +#[bamltype::BamlType] +#[baml(internal_name = "contract::ColorAlias")] +enum FacetColorAlias { + Red, + #[baml(alias = "green")] + Green, +} + +#[derive(Debug, Clone, PartialEq)] +#[bamltype::BamlType] +#[baml(name = "ContractDocShape")] +#[baml(internal_name = "contract::DocShape")] +#[baml(tag = "type")] +enum FacetDocShape { + /// A circle, defined by its radius. + Circle { + /// Radius in meters. + radius: f64, + }, + Rectangle { + width: f64, + height: f64, + }, +} + +#[derive(Debug, Clone, PartialEq)] +#[bamltype::BamlType] +#[baml(internal_name = "contract::BigIntOption")] +struct FacetBigIntOption { + #[baml(int_repr = "i64")] + id: Option, +} + +#[derive(Debug, Clone, PartialEq)] +#[bamltype::BamlType] +#[baml(internal_name = "contract::MapKeys")] +struct FacetMapKeys { + #[baml(map_key_repr = "string")] + values: HashMap, +} + +#[derive(Debug, Clone, PartialEq)] +#[bamltype::BamlType] +#[baml(internal_name = "contract::MapKeysOption")] +struct FacetMapKeysOption { + #[baml(map_key_repr = "string")] + values: Option>, +} + +#[derive(Debug, Clone, PartialEq)] +#[bamltype::BamlType] +#[baml(internal_name = "contract::Node")] +struct FacetNode { + value: i64, + next: Option>, +} + +#[derive(Debug, Clone, PartialEq)] +#[bamltype::BamlType] +#[baml(rename_all = "camelCase")] +#[baml(internal_name = "contract::RenameAllUser")] +struct FacetRenameAllUser { + full_name: String, +} + +#[derive(Debug, Clone, PartialEq)] +#[bamltype::BamlType] +#[baml(internal_name = "contract::RoundtripStruct")] +struct FacetRoundtripStruct { + name: String, + count: i32, + tags: Vec, + meta: Option, +} + +#[derive(Debug, Clone, PartialEq)] +#[bamltype::BamlType] +#[baml(internal_name = "contract::RoundtripUnitEnum")] +enum FacetRoundtripUnitEnum { + Alpha, + #[baml(alias = "beta")] + Beta, +} + +#[derive(Debug, Clone, PartialEq)] +#[bamltype::BamlType] +#[baml(internal_name = "contract::RoundtripDataEnum")] +#[baml(tag = "kind")] +enum FacetRoundtripDataEnum { + Message { body: String, count: i64 }, + Empty, +} + +#[derive(Debug, Clone, PartialEq)] +#[bamltype::BamlType] +#[baml(internal_name = "contract::NestedStruct")] +struct FacetNestedStruct { + title: String, + items: Option>, + metadata: HashMap>, +} + +#[derive(Debug, Clone, PartialEq)] +#[bamltype::BamlType] +#[baml(internal_name = "contract::AsUnionColor")] +#[baml(as_union)] +enum FacetAsUnionColor { + Red, + #[baml(alias = "green")] + Green, +} + +struct FacetI64ObjectAdapter; + +impl facet_runtime::BamlAdapter for FacetI64ObjectAdapter { + fn type_ir() -> TypeIR { + TypeIR::class("AdapterI64Wrapper") + } + + fn register(reg: &mut facet_runtime::Registry) { + use facet_runtime::internal_baml_jinja::types::{Class, Name}; + + if !reg.mark_type("AdapterI64Wrapper") { + return; + } + + reg.register_class(Class { + name: Name::new("AdapterI64Wrapper".to_string()), + description: None, + namespace: StreamingMode::NonStreaming, + fields: vec![( + Name::new("value".to_string()), + TypeIR::string(), + None, + false, + )], + constraints: Vec::new(), + streaming_behavior: Default::default(), + }); + } + + fn try_from_baml( + value: BamlValue, + mut path: Vec, + ) -> Result { + let map = match value { + BamlValue::Class(_, fields) | BamlValue::Map(fields) => fields, + other => { + return Err(facet_runtime::BamlConvertError::new( + path, + "object", + format!("{other:?}"), + "expected object adapter payload", + )); + } + }; + + let raw_value = map.get("value").ok_or_else(|| { + facet_runtime::BamlConvertError::new( + path.clone(), + "value", + "", + "missing required field", + ) + })?; + path.push("value".to_string()); + + match raw_value { + BamlValue::String(s) => s.parse::().map_err(|_| { + facet_runtime::BamlConvertError::new( + path.clone(), + "i64", + s.clone(), + "failed to parse i64", + ) + }), + BamlValue::Int(i) => Ok(*i), + other => Err(facet_runtime::BamlConvertError::new( + path, + "i64", + format!("{other:?}"), + "expected value field to be string or int", + )), + } + } +} + +#[derive(Debug, Clone, PartialEq)] +#[bamltype::BamlType] +#[baml(internal_name = "contract::WithAdapter")] +struct FacetWithAdapter { + #[baml(with = "FacetI64ObjectAdapter")] + id: i64, +} + +mod facet_collision_a { + #[derive(Debug, Clone, PartialEq)] + #[bamltype::BamlType] + pub struct User { + pub name: String, + } +} + +mod facet_collision_b { + #[derive(Debug, Clone, PartialEq)] + #[bamltype::BamlType] + pub struct User { + pub name: String, + } +} + +fn sorted_union_string_literals(type_ir: TypeIR) -> Vec { + let TypeIR::Union(union, _) = type_ir else { + panic!("expected union type IR"); + }; + + let mut literals = union + .iter_skip_null() + .into_iter() + .map(|item| match item { + TypeIR::Literal(LiteralValue::String(value), _) => value.clone(), + other => panic!("expected string literal in union, got {other:?}"), + }) + .collect::>(); + literals.sort(); + literals +} + +fn canonical_baml(value: &BamlValue) -> serde_json::Value { + match value { + BamlValue::String(v) => json!({ "kind": "string", "value": v }), + BamlValue::Int(v) => json!({ "kind": "int", "value": v }), + BamlValue::Float(v) => json!({ "kind": "float", "value": v }), + BamlValue::Bool(v) => json!({ "kind": "bool", "value": v }), + BamlValue::Null => json!({ "kind": "null" }), + BamlValue::Enum(name, variant) => { + json!({ "kind": "enum", "name": name, "variant": variant }) + } + BamlValue::List(items) => { + let canonical_items = items.iter().map(canonical_baml).collect::>(); + json!({ "kind": "list", "value": canonical_items }) + } + BamlValue::Map(map) => { + let mut entries = map + .iter() + .map(|(k, v)| (k.clone(), canonical_baml(v))) + .collect::>(); + entries.sort_by(|(left, _), (right, _)| left.cmp(right)); + + let mut object = serde_json::Map::new(); + for (key, value) in entries { + object.insert(key, value); + } + json!({ "kind": "map", "value": object }) + } + BamlValue::Class(name, fields) => { + let mut entries = fields + .iter() + .map(|(k, v)| (k.clone(), canonical_baml(v))) + .collect::>(); + entries.sort_by(|(left, _), (right, _)| left.cmp(right)); + + let mut object = serde_json::Map::new(); + for (key, value) in entries { + object.insert(key, value); + } + + json!({ "kind": "class", "name": name, "fields": object }) + } + BamlValue::Media(media) => json!({ "kind": "media", "value": format!("{media:?}") }), + } +} + +fn parsed_fixture(parsed: &facet_runtime::Parsed) -> String { + let fixture = json!({ + "baml_value": canonical_baml(&parsed.baml_value), + "flags": format!("{:?}", parsed.flags), + "checks": format!("{:?}", parsed.checks), + "explanations": parsed + .explanations + .iter() + .map(ToString::to_string) + .collect::>(), + }); + + serde_json::to_string_pretty(&fixture).expect("serialize parsed fixture") +} + +#[test] +fn contract_render_schema_default_fixture() { + let rendered = + facet_runtime::render_schema::(facet_runtime::RenderOptions::default()) + .expect("render") + .unwrap_or_default(); + expect![[r#" + Answer in JSON using this schema: + { + fullName: string, + age: int, + }"#]] + .assert_eq(&rendered); +} + +#[test] +fn contract_render_schema_hoisted_fixture() { + let rendered = facet_runtime::render_schema::( + facet_runtime::RenderOptions::hoist_classes(facet_runtime::HoistClasses::All), + ) + .expect("render") + .unwrap_or_default(); + expect![[r#" + ContractShape_Circle { + kind: "Circle", + radius: float, + } + + ContractShape_Square { + kind: "Square", + side: float, + } + + Answer in JSON using any of these schemas: + ContractShape_Circle or ContractShape_Square"#]] + .assert_eq(&rendered); +} + +#[test] +fn contract_render_schema_ordering_fixture() { + let rendered = facet_runtime::render_schema::( + facet_runtime::RenderOptions::hoist_classes(facet_runtime::HoistClasses::All), + ) + .expect("render") + .unwrap_or_default(); + expect![[r#" + OrderA { + value: int, + } + + OrderB { + value: int, + } + + OrderRoot { + b: OrderB, + a: OrderA, + } + + Answer in JSON using this schema: OrderRoot"#]] + .assert_eq(&rendered); +} + +#[test] +fn contract_parse_envelope_fixture() { + let raw = r#"{ "kind": "Circle", "radius": 2.5 }"#; + let parsed = facet_runtime::parse_llm_output::(raw, true).expect("parse"); + expect![[r#" + { + "baml_value": { + "kind": "class", + "name": "contract::Shape__Circle", + "fields": { + "kind": { + "kind": "string", + "value": "Circle" + }, + "radius": { + "kind": "float", + "value": 2.5 + } + } + }, + "flags": "[UnionMatch(0, []), FirstMatch(0, [])]", + "checks": "[]", + "explanations": [] + }"#]] + .assert_eq(&parsed_fixture(&parsed)); +} + +#[test] +fn contract_parse_streaming_mode_fixture() { + let raw = r#"{ "name": "Ada", "age": 36 }"#; + let parsed = facet_runtime::parse_llm_output::(raw, false).expect("parse"); + expect![[r#" + { + "baml_value": { + "kind": "class", + "name": "contract::FuzzUser", + "fields": { + "age": { + "kind": "int", + "value": 36 + }, + "name": { + "kind": "string", + "value": "Ada" + }, + "nickname": { + "kind": "null" + } + } + }, + "flags": "[FirstMatch(0, []), OptionalDefaultFromNoValue, Pending]", + "checks": "[]", + "explanations": [] + }"#]] + .assert_eq(&parsed_fixture(&parsed)); +} + +#[test] +fn contract_int_repr_string_fixture() { + let raw = r#"{ "id": "18446744073709551615" }"#; + let parsed = facet_runtime::parse_llm_output::(raw, true).expect("parse"); + expect![[r#" + { + "baml_value": { + "kind": "class", + "name": "contract::BigIntString", + "fields": { + "id": { + "kind": "string", + "value": "18446744073709551615" + } + } + }, + "flags": "[FirstMatch(0, [])]", + "checks": "[]", + "explanations": [] + }"#]] + .assert_eq(&parsed_fixture(&parsed)); +} + +#[test] +fn contract_map_key_pairs_parse_and_registration_fixture() { + let raw = r#"{ "values": [ { "key": 1, "value": "a" }, { "key": 2, "value": "b" } ] }"#; + let parsed = facet_runtime::parse_llm_output::(raw, true).expect("parse"); + + let entry_name = format!( + "{}::values__Entry", + ::baml_internal_name() + ); + let class = ::baml_output_format() + .classes + .get(&(entry_name, StreamingMode::NonStreaming)) + .expect("entry class"); + + let snapshot = serde_json::to_string_pretty(&json!({ + "parsed": serde_json::from_str::(&parsed_fixture(&parsed)).expect("json parsed fixture"), + "entry_rendered_name": class.name.rendered_name(), + })) + .expect("serialize"); + + expect![[r#" + { + "parsed": { + "baml_value": { + "kind": "class", + "name": "contract::MapPairs", + "fields": { + "values": { + "kind": "list", + "value": [ + { + "kind": "class", + "name": "contract::MapPairs::values__Entry", + "fields": { + "key": { + "kind": "int", + "value": 1 + }, + "value": { + "kind": "string", + "value": "a" + } + } + }, + { + "kind": "class", + "name": "contract::MapPairs::values__Entry", + "fields": { + "key": { + "kind": "int", + "value": 2 + }, + "value": { + "kind": "string", + "value": "b" + } + } + } + ] + } + } + }, + "flags": "[FirstMatch(0, [])]", + "checks": "[]", + "explanations": [] + }, + "entry_rendered_name": "valuesEntry" + }"#]] + .assert_eq(&snapshot); +} + +#[test] +fn contract_check_results_fixture() { + let raw = r#"{ "value": 3 }"#; + let parsed = facet_runtime::parse_llm_output::(raw, true).expect("parse"); + expect![[r#" + { + "baml_value": { + "kind": "class", + "name": "contract::Checked", + "fields": { + "value": { + "kind": "int", + "value": 3 + } + } + }, + "flags": "[FirstMatch(0, []), ConstraintResults([(\"positive\", JinjaExpression(\"this > 0\"), true)])]", + "checks": "[ResponseCheck { name: \"positive\", expression: \"this > 0\", status: \"succeeded\" }]", + "explanations": [] + }"#]].assert_eq(&parsed_fixture(&parsed)); +} + +#[test] +fn contract_assert_error_shape_fixture() { + let raw = r#"{ "value": -1 }"#; + let err = facet_runtime::parse_llm_output::(raw, true).expect_err("err"); + + let facet_runtime::BamlParseError::ConstraintAssertsFailed { failed } = err else { + panic!("expected assert failure error"); + }; + + let snapshot = serde_json::to_string_pretty(&json!({ + "failed": format!("{:?}", failed), + })) + .expect("serialize"); + + expect![[r#" + { + "failed": "[ResponseCheck { name: \"positive\", expression: \"this > 0\", status: \"failed\" }]" + }"#]].assert_eq(&snapshot); +} + +#[test] +fn contract_markdown_flags_and_explanations_fixture() { + let markdown_raw = "```json\n{ \"name\": \"Ada\", \"age\": 36 }\n```"; + let markdown = + facet_runtime::parse_llm_output::(markdown_raw, true).expect("parse"); + + let explain_raw = r#"{ "values": { "ok": 1, "bad": "oops" } }"#; + let explain = + facet_runtime::parse_llm_output::(explain_raw, true).expect("parse"); + + let snapshot = serde_json::to_string_pretty(&json!({ + "markdown": serde_json::from_str::(&parsed_fixture(&markdown)).expect("json markdown fixture"), + "explain": serde_json::from_str::(&parsed_fixture(&explain)).expect("json explain fixture"), + })) + .expect("serialize"); + + expect![[r#" + { + "markdown": { + "baml_value": { + "kind": "class", + "name": "contract::FuzzUser", + "fields": { + "age": { + "kind": "int", + "value": 36 + }, + "name": { + "kind": "string", + "value": "Ada" + }, + "nickname": { + "kind": "null" + } + } + }, + "flags": "[FirstMatch(0, []), ObjectFromMarkdown(0), Incomplete, FirstMatch(0, []), Incomplete, OptionalDefaultFromNoValue, Pending]", + "checks": "[]", + "explanations": [] + }, + "explain": { + "baml_value": { + "kind": "class", + "name": "contract::FuzzExplain", + "fields": { + "values": { + "kind": "map", + "value": { + "ok": { + "kind": "int", + "value": 1 + } + } + } + } + }, + "flags": "[FirstMatch(0, []), ObjectToMap(Object([(\"ok\", Number(Number(1), Complete)), (\"bad\", String(\"oops\", Complete))], Complete)), MapValueParseError(\"bad\", ParsingError { scope: [\"values\", \"bad\"], reason: \"Expected int, got String(\\\"oops\\\", Complete).\", causes: [] })]", + "checks": "[]", + "explanations": [ + ".values: error while parsing map\n - values.bad: Expected int, got String(\"oops\", Complete)." + ] + } + }"#]].assert_eq(&snapshot); +} + +#[test] +fn contract_unit_enum_alias_parse_fixture() { + let raw = r#""green""#; + let parsed = facet_runtime::parse_llm_output::(raw, true).expect("parse"); + expect![[r#" + { + "baml_value": { + "kind": "enum", + "name": "contract::ColorAlias", + "variant": "Green" + }, + "flags": "[SubstringMatch(\"\\\"green\\\"\")]", + "checks": "[]", + "explanations": [] + }"#]] + .assert_eq(&parsed_fixture(&parsed)); +} + +#[test] +fn contract_as_union_type_ir_and_parse_fixture() { + let literals = sorted_union_string_literals( + ::baml_type_ir(), + ); + let raw = r#""green""#; + let parsed = facet_runtime::parse_llm_output::(raw, true).expect("parse"); + + let snapshot = serde_json::to_string_pretty(&json!({ + "literals": literals, + "parsed": serde_json::from_str::(&parsed_fixture(&parsed)).expect("json parsed fixture"), + })) + .expect("serialize"); + + expect![[r#" + { + "literals": [ + "Red", + "green" + ], + "parsed": { + "baml_value": { + "kind": "string", + "value": "green" + }, + "flags": "[UnionMatch(1, []), FirstMatch(0, [])]", + "checks": "[]", + "explanations": [] + } + }"#]] + .assert_eq(&snapshot); +} + +#[test] +fn contract_data_enum_docs_render_fixture() { + let rendered = + facet_runtime::render_schema::(facet_runtime::RenderOptions::default()) + .expect("render") + .unwrap_or_default(); + expect![[r#" + Answer in JSON using any of these schemas: + { + // A circle, defined by its radius. + + type: "Circle", + // Radius in meters. + radius: float, + } or { + type: "Rectangle", + width: float, + height: float, + }"#]] + .assert_eq(&rendered); +} + +#[test] +fn contract_int_repr_option_fixture() { + let parsed_num = facet_runtime::parse_llm_output::(r#"{ "id": 42 }"#, true) + .expect("parse num"); + let parsed_null = + facet_runtime::parse_llm_output::(r#"{ "id": null }"#, true) + .expect("parse null"); + + let snapshot = serde_json::to_string_pretty(&json!({ + "num": serde_json::from_str::(&parsed_fixture(&parsed_num)).expect("json parsed fixture"), + "null": serde_json::from_str::(&parsed_fixture(&parsed_null)).expect("json parsed fixture"), + })) + .expect("serialize"); + + expect![[r#" + { + "num": { + "baml_value": { + "kind": "class", + "name": "contract::BigIntOption", + "fields": { + "id": { + "kind": "int", + "value": 42 + } + } + }, + "flags": "[FirstMatch(0, []), UnionMatch(0, [])]", + "checks": "[]", + "explanations": [] + }, + "null": { + "baml_value": { + "kind": "class", + "name": "contract::BigIntOption", + "fields": { + "id": { + "kind": "null" + } + } + }, + "flags": "[FirstMatch(0, [])]", + "checks": "[]", + "explanations": [] + } + }"#]] + .assert_eq(&snapshot); +} + +#[test] +fn contract_map_key_repr_string_and_option_fixture() { + let parsed_keys = facet_runtime::parse_llm_output::( + r#"{ "values": { "1": "a", "2": "b" } }"#, + true, + ) + .expect("parse keys"); + let parsed_option = facet_runtime::parse_llm_output::( + r#"{ "values": { "10": "x" } }"#, + true, + ) + .expect("parse option"); + + let snapshot = serde_json::to_string_pretty(&json!({ + "keys": serde_json::from_str::(&parsed_fixture(&parsed_keys)).expect("json parsed fixture"), + "option": serde_json::from_str::(&parsed_fixture(&parsed_option)).expect("json parsed fixture"), + })) + .expect("serialize"); + + expect![[r#" + { + "keys": { + "baml_value": { + "kind": "class", + "name": "contract::MapKeys", + "fields": { + "values": { + "kind": "map", + "value": { + "1": { + "kind": "string", + "value": "a" + }, + "2": { + "kind": "string", + "value": "b" + } + } + } + } + }, + "flags": "[FirstMatch(0, []), ObjectToMap(Object([(\"1\", String(\"a\", Complete)), (\"2\", String(\"b\", Complete))], Complete))]", + "checks": "[]", + "explanations": [] + }, + "option": { + "baml_value": { + "kind": "class", + "name": "contract::MapKeysOption", + "fields": { + "values": { + "kind": "map", + "value": { + "10": { + "kind": "string", + "value": "x" + } + } + } + } + }, + "flags": "[FirstMatch(0, []), ObjectToMap(Object([(\"10\", String(\"x\", Complete))], Complete)), UnionMatch(0, [])]", + "checks": "[]", + "explanations": [] + } + }"#]].assert_eq(&snapshot); +} + +#[test] +fn contract_rename_all_fixture() { + let raw = r#"{ "fullName": "Ada" }"#; + let parsed = facet_runtime::parse_llm_output::(raw, true).expect("parse"); + expect![[r#" + { + "baml_value": { + "kind": "class", + "name": "contract::RenameAllUser", + "fields": { + "full_name": { + "kind": "string", + "value": "Ada" + } + } + }, + "flags": "[FirstMatch(0, [])]", + "checks": "[]", + "explanations": [] + }"#]] + .assert_eq(&parsed_fixture(&parsed)); +} + +#[test] +fn contract_recursion_class_detection_fixture() { + let output = ::baml_output_format(); + let mut recursive = output.recursive_classes.iter().cloned().collect::>(); + recursive.sort(); + + let snapshot = serde_json::to_string_pretty(&json!({ + "recursive_classes": recursive, + })) + .expect("serialize"); + + expect![[r#" + { + "recursive_classes": [ + "contract::Node" + ] + }"#]] + .assert_eq(&snapshot); +} + +#[test] +fn contract_internal_name_collision_behavior_fixture() { + let a_name = ::baml_internal_name(); + let b_name = ::baml_internal_name(); + + let a_class = ::baml_output_format() + .classes + .get(&(a_name.to_string(), StreamingMode::NonStreaming)) + .expect("class a"); + let b_class = ::baml_output_format() + .classes + .get(&(b_name.to_string(), StreamingMode::NonStreaming)) + .expect("class b"); + + let snapshot = serde_json::to_string_pretty(&json!({ + "a_name": a_name, + "b_name": b_name, + "a_rendered": a_class.name.rendered_name(), + "b_rendered": b_class.name.rendered_name(), + })) + .expect("serialize"); + + expect![[r#" + { + "a_name": "contract_frozen_oracle::facet_collision_a::User", + "b_name": "contract_frozen_oracle::facet_collision_b::User", + "a_rendered": "User", + "b_rendered": "User" + }"#]] + .assert_eq(&snapshot); +} + +#[test] +fn contract_with_adapter_schema_and_parse_fixture() { + let output = ::baml_output_format(); + let adapter = output + .classes + .get(&("AdapterI64Wrapper".to_string(), StreamingMode::NonStreaming)) + .expect("adapter class"); + let adapter_fields = adapter + .fields + .iter() + .map(|(name, type_ir, _, _)| format!("{}: {type_ir:?}", name.real_name())) + .collect::>(); + + let raw = r#"{ "id": { "value": "9223372036854775807" } }"#; + let parsed = facet_runtime::parse_llm_output::(raw, true).expect("parse"); + + let snapshot = serde_json::to_string_pretty(&json!({ + "adapter_real_name": adapter.name.real_name(), + "adapter_fields": adapter_fields, + "parsed": serde_json::from_str::(&parsed_fixture(&parsed)).expect("json parsed fixture"), + })) + .expect("serialize"); + + expect![[r#" + { + "adapter_real_name": "AdapterI64Wrapper", + "adapter_fields": [ + "value: Primitive(String, TypeMeta { constraints: [], streaming_behavior: StreamingBehavior { needed: false, done: false, state: false } })" + ], + "parsed": { + "baml_value": { + "kind": "class", + "name": "contract::WithAdapter", + "fields": { + "id": { + "kind": "class", + "name": "AdapterI64Wrapper", + "fields": { + "value": { + "kind": "string", + "value": "9223372036854775807" + } + } + } + } + }, + "flags": "[FirstMatch(0, [])]", + "checks": "[]", + "explanations": [] + } + }"#]].assert_eq(&snapshot); +} + +fn assert_default_adapter_roundtrip(value: T) +where + T: Clone + + std::fmt::Debug + + PartialEq + + facet_runtime::compat::ToBamlValue + + facet_runtime::compat::BamlValueConvert, +{ + let baml = facet_runtime::compat::ToBamlValue::to_baml_value(&value); + let back = + ::try_from_baml_value(baml, Vec::new()) + .expect("roundtrip"); + assert_eq!(back, value); +} + +#[test] +fn contract_default_adapter_roundtrips() { + assert_default_adapter_roundtrip("hello".to_string()); + assert_default_adapter_roundtrip(true); + assert_default_adapter_roundtrip(123i32); + assert_default_adapter_roundtrip(-99i64); + assert_default_adapter_roundtrip(3.5f32); + assert_default_adapter_roundtrip(9.75f64); + + assert_default_adapter_roundtrip(Some(42i32)); + assert_default_adapter_roundtrip(None::); + assert_default_adapter_roundtrip(vec!["a".to_string(), "b".to_string()]); + assert_default_adapter_roundtrip(Box::new("boxed".to_string())); + assert_default_adapter_roundtrip(Arc::new("arc".to_string())); + assert_default_adapter_roundtrip(Rc::new("rc".to_string())); + + let mut hm = HashMap::new(); + hm.insert("answer".to_string(), 42i32); + assert_default_adapter_roundtrip(hm); + + let mut bt = BTreeMap::new(); + bt.insert("left".to_string(), 1i64); + bt.insert("right".to_string(), 2i64); + assert_default_adapter_roundtrip(bt); + + let nested = FacetNestedStruct { + title: "nested".to_string(), + items: Some(vec![FacetRoundtripStruct { + name: "child".to_string(), + count: 2, + tags: vec!["x".to_string(), "y".to_string()], + meta: None, + }]), + metadata: HashMap::from([("alpha".to_string(), Some(1)), ("beta".to_string(), None)]), + }; + assert_default_adapter_roundtrip(nested); + assert_default_adapter_roundtrip(FacetRoundtripUnitEnum::Beta); + assert_default_adapter_roundtrip(FacetRoundtripDataEnum::Message { + body: "hello".to_string(), + count: 3, + }); +} + +#[test] +fn contract_default_adapter_error_shape_fixture() { + let invalid = BamlValue::Class( + "contract::Unsigned32".to_string(), + [("value".to_string(), BamlValue::Int(-1))] + .into_iter() + .collect(), + ); + + let err = ::try_from_baml_value( + invalid, + Vec::new(), + ) + .expect_err("should fail"); + + let snapshot = serde_json::to_string_pretty(&json!({ + "expected": err.expected, + "path": err.path, + "display": err.to_string(), + })) + .expect("serialize"); + + expect![[r#" + { + "expected": "u32", + "path": [ + "value" + ], + "display": "integer out of range (expected u32, got -1) at value" + }"#]] + .assert_eq(&snapshot); +} + +#[test] +fn contract_direct_integer_string_conversion_error_fixture() { + let err = ::try_from_baml_value( + BamlValue::String("123".into()), + Vec::new(), + ) + .expect_err("should reject"); + + let snapshot = serde_json::to_string_pretty(&json!({ + "expected": err.expected, + "path": err.path, + "display": err.to_string(), + })) + .expect("serialize"); + + expect![[r#" + { + "expected": "int", + "path": [], + "display": "expected a int (expected int, got String(\"123\")) at " + }"#]] + .assert_eq(&snapshot); +} + +#[test] +fn contract_direct_map_pairs_conversion_error_fixture() { + let pair_entry = BamlValue::Map( + [ + ("key".to_string(), BamlValue::String("k".to_string())), + ("value".to_string(), BamlValue::Int(1)), + ] + .into_iter() + .collect(), + ); + let raw = BamlValue::List(vec![pair_entry]); + + let err = + as facet_runtime::compat::BamlValueConvert>::try_from_baml_value( + raw, + Vec::new(), + ) + .expect_err("should reject"); + + let snapshot = serde_json::to_string_pretty(&json!({ + "expected": err.expected, + "path": err.path, + "display": err.to_string(), + })) + .expect("serialize"); + + expect![[r#" + { + "expected": "map", + "path": [], + "display": "expected a map (expected map, got List([Map({\"key\": String(\"k\"), \"value\": Int(1)})])) at " + }"#]].assert_eq(&snapshot); +} + +#[test] +fn contract_schema_fingerprint_fixture() { + let output = ::baml_output_format(); + let fp = facet_runtime::schema_fingerprint(output, facet_runtime::RenderOptions::default()) + .expect("fingerprint"); + + expect!["bd09ecdbcb1ddf0746150789905fcce576fb67b74499a9d4738ca28018ae859c"].assert_eq(&fp); +} diff --git a/crates/bamltype/tests/golden_parity.rs b/crates/bamltype/tests/golden_parity.rs new file mode 100644 index 00000000..75baa8d9 --- /dev/null +++ b/crates/bamltype/tests/golden_parity.rs @@ -0,0 +1,72 @@ +use bamltype as facet_runtime; +use expect_test::expect; + +/// Golden user docs. +#[derive(Debug, Clone, PartialEq)] +#[bamltype::BamlType] +#[baml(name = "GoldenUser")] +#[baml(internal_name = "contract::golden::User")] +struct GoldenUser { + /// Full name for display. + #[baml(alias = "fullName")] + name: String, + age: i64, +} + +#[derive(Debug, Clone, PartialEq)] +#[bamltype::BamlType] +#[baml(name = "GoldenShape")] +#[baml(internal_name = "contract::golden::Shape")] +#[baml(tag = "kind")] +enum GoldenShape { + /// A circle. + Circle { + radius: f64, + }, + Square { + side: f64, + }, +} + +#[test] +fn golden_schema_snapshot_user_default_fixture() { + let schema = + facet_runtime::render_schema::(facet_runtime::RenderOptions::default()) + .expect("facet render") + .unwrap_or_default(); + + expect![[r#"Answer in JSON using this schema: +{ + // Golden user docs. + + // Full name for display. + fullName: string, + age: int, +}"#]] + .assert_eq(&schema); +} + +#[test] +fn golden_schema_snapshot_shape_hoisted_fixture() { + let schema = facet_runtime::render_schema::( + facet_runtime::RenderOptions::hoist_classes(facet_runtime::HoistClasses::All), + ) + .expect("facet render") + .unwrap_or_default(); + + expect![[r#"GoldenShape_Circle { + // A circle. + + kind: "Circle", + radius: float, +} + +GoldenShape_Square { + kind: "Square", + side: float, +} + +Answer in JSON using any of these schemas: +GoldenShape_Circle or GoldenShape_Square"#]] + .assert_eq(&schema); +} diff --git a/crates/bamltype/tests/property_parity.rs b/crates/bamltype/tests/property_parity.rs new file mode 100644 index 00000000..214fa181 --- /dev/null +++ b/crates/bamltype/tests/property_parity.rs @@ -0,0 +1,456 @@ +use std::collections::HashMap; + +use bamltype as facet_runtime; +use bamltype::baml_types::{BamlValue, StreamingMode, TypeIR}; +use expect_test::expect; +use proptest::collection::{hash_map, vec}; +use proptest::prelude::*; +use proptest::string::string_regex; +use proptest::test_runner::RngSeed; +use serde_json::json; + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +#[bamltype::BamlType] +#[baml(internal_name = "contract::property::User")] +struct RoundTripUser { + name: String, + age: u32, + active: bool, + tags: Vec, + meta: HashMap, + nickname: Option, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +struct RecursiveSeed { + value: i64, + note: Option, + next: Option>, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +#[bamltype::BamlType] +#[baml(internal_name = "contract::property::RecursiveNode")] +struct RecursiveNode { + value: i64, + note: Option, + next: Option>, +} + +struct PropertyI64Adapter; + +impl facet_runtime::BamlAdapter for PropertyI64Adapter { + fn type_ir() -> TypeIR { + TypeIR::class("PropertyAdapterI64Wrapper") + } + + fn register(reg: &mut facet_runtime::Registry) { + use facet_runtime::internal_baml_jinja::types::{Class, Name}; + + if !reg.mark_type("PropertyAdapterI64Wrapper") { + return; + } + + reg.register_class(Class { + name: Name::new("PropertyAdapterI64Wrapper".to_string()), + description: None, + namespace: facet_runtime::baml_types::StreamingMode::NonStreaming, + fields: vec![( + Name::new("value".to_string()), + TypeIR::string(), + None, + false, + )], + constraints: Vec::new(), + streaming_behavior: Default::default(), + }); + } + + fn try_from_baml( + value: BamlValue, + mut path: Vec, + ) -> Result { + let map = match value { + BamlValue::Class(_, fields) | BamlValue::Map(fields) => fields, + other => { + return Err(facet_runtime::BamlConvertError::new( + path, + "object", + format!("{other:?}"), + "expected object adapter payload", + )); + } + }; + + let raw_value = map.get("value").ok_or_else(|| { + facet_runtime::BamlConvertError::new( + path.clone(), + "value", + "", + "missing required field", + ) + })?; + path.push("value".to_string()); + + match raw_value { + BamlValue::String(s) => s.parse::().map_err(|_| { + facet_runtime::BamlConvertError::new( + path.clone(), + "i64", + s.clone(), + "failed to parse i64", + ) + }), + BamlValue::Int(i) => Ok(*i), + other => Err(facet_runtime::BamlConvertError::new( + path, + "i64", + format!("{other:?}"), + "expected value field to be string or int", + )), + } + } +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +#[bamltype::BamlType] +#[baml(internal_name = "contract::property::AdapterHeavy")] +struct AdapterHeavy { + #[baml(with = "PropertyI64Adapter")] + primary: i64, + #[baml(with = "PropertyI64Adapter")] + secondary: i64, + #[baml(with = "PropertyI64Adapter")] + tertiary: i64, +} + +#[derive(Debug, Clone, PartialEq)] +#[bamltype::BamlType] +#[baml(internal_name = "contract::property::FuzzUser")] +struct FuzzUser { + name: String, + age: u32, + nickname: Option, +} + +fn arb_string() -> impl Strategy { + string_regex("[a-zA-Z0-9 _-]{0,12}").expect("valid regex") +} + +fn arb_user_fields() -> impl Strategy< + Value = ( + String, + u32, + bool, + Vec, + HashMap, + Option, + ), +> { + ( + arb_string(), + 0_u32..1000_u32, + any::(), + vec(arb_string(), 0..5), + hash_map(arb_string(), -1000_i64..1000_i64, 0..5), + proptest::option::of(arb_string()), + ) +} + +fn arb_recursive_seed() -> impl Strategy { + let leaf = + (-1000_i64..1000_i64, proptest::option::of(arb_string())).prop_map(|(value, note)| { + RecursiveSeed { + value, + note, + next: None, + } + }); + + leaf.prop_recursive(6, 128, 2, |inner| { + ( + -1000_i64..1000_i64, + proptest::option::of(arb_string()), + proptest::option::of(inner), + ) + .prop_map(|(value, note, next)| RecursiveSeed { + value, + note, + next: next.map(Box::new), + }) + }) +} + +fn seed_to_node(seed: &RecursiveSeed) -> RecursiveNode { + RecursiveNode { + value: seed.value, + note: seed.note.clone(), + next: seed.next.as_ref().map(|next| Box::new(seed_to_node(next))), + } +} + +fn canonical_baml(value: &BamlValue) -> serde_json::Value { + match value { + BamlValue::String(v) => json!({ "kind": "string", "value": v }), + BamlValue::Int(v) => json!({ "kind": "int", "value": v }), + BamlValue::Float(v) => json!({ "kind": "float", "value": v }), + BamlValue::Bool(v) => json!({ "kind": "bool", "value": v }), + BamlValue::Null => json!({ "kind": "null" }), + BamlValue::Enum(name, variant) => { + json!({ "kind": "enum", "name": name, "variant": variant }) + } + BamlValue::List(items) => { + let canonical_items = items.iter().map(canonical_baml).collect::>(); + json!({ "kind": "list", "value": canonical_items }) + } + BamlValue::Map(map) => { + let mut entries = map + .iter() + .map(|(k, v)| (k.clone(), canonical_baml(v))) + .collect::>(); + entries.sort_by(|(left, _), (right, _)| left.cmp(right)); + + let mut object = serde_json::Map::new(); + for (key, value) in entries { + object.insert(key, value); + } + json!({ "kind": "map", "value": object }) + } + BamlValue::Class(name, fields) => { + let mut entries = fields + .iter() + .map(|(k, v)| (k.clone(), canonical_baml(v))) + .collect::>(); + entries.sort_by(|(left, _), (right, _)| left.cmp(right)); + + let mut object = serde_json::Map::new(); + for (key, value) in entries { + object.insert(key, value); + } + + json!({ "kind": "class", "name": name, "fields": object }) + } + BamlValue::Media(media) => json!({ "kind": "media", "value": format!("{media:?}") }), + } +} + +fn parsed_fixture(parsed: &facet_runtime::Parsed) -> String { + let fixture = json!({ + "baml_value": canonical_baml(&parsed.baml_value), + "flags": format!("{:?}", parsed.flags), + "checks": format!("{:?}", parsed.checks), + "explanations": parsed + .explanations + .iter() + .map(ToString::to_string) + .collect::>(), + }); + + serde_json::to_string_pretty(&fixture).expect("serialize parsed fixture") +} + +proptest! { + #![proptest_config(ProptestConfig { + cases: 96, + rng_seed: RngSeed::Fixed(0x5eed_f00d), + .. ProptestConfig::default() + })] + + #[test] + fn property_round_trip_user((name, age, active, tags, meta, nickname) in arb_user_fields()) { + let expected = RoundTripUser { + name, + age, + active, + tags, + meta, + nickname, + }; + + let json = serde_json::to_string(&expected).expect("serialize"); + let parsed = facet_runtime::parse_llm_output::(&json, true).expect("facet parse"); + + prop_assert_eq!(parsed.value, expected); + } + + #[test] + fn property_recursive_parse(seed in arb_recursive_seed()) { + let expected = seed_to_node(&seed); + let json = serde_json::to_string(&seed).expect("serialize"); + let parsed = facet_runtime::parse_llm_output::(&json, true).expect("facet parse"); + + prop_assert_eq!(parsed.value, expected); + } + + #[test] + fn property_adapter_heavy_parse( + primary in -1_000_000_i64..1_000_000_i64, + secondary in -1_000_000_i64..1_000_000_i64, + tertiary in -1_000_000_i64..1_000_000_i64, + primary_as_int in any::(), + secondary_as_int in any::(), + tertiary_as_int in any::(), + ) { + let expected = AdapterHeavy { + primary, + secondary, + tertiary, + }; + + let payload = serde_json::json!({ + "primary": { "value": if primary_as_int { serde_json::json!(primary) } else { serde_json::json!(primary.to_string()) } }, + "secondary": { "value": if secondary_as_int { serde_json::json!(secondary) } else { serde_json::json!(secondary.to_string()) } }, + "tertiary": { "value": if tertiary_as_int { serde_json::json!(tertiary) } else { serde_json::json!(tertiary.to_string()) } }, + }) + .to_string(); + + let parsed = facet_runtime::parse_llm_output::(&payload, true).expect("facet parse"); + + prop_assert_eq!(parsed.value, expected); + } +} + +#[test] +fn trailing_comma_parses_fixture() { + let raw = r#"{ "name": "Ada", "age": 36, }"#; + let parsed = facet_runtime::parse_llm_output::(raw, true).expect("facet parse"); + + assert_eq!(parsed.value.age, 36); + expect![[r#" + { + "baml_value": { + "kind": "class", + "name": "contract::property::FuzzUser", + "fields": { + "age": { + "kind": "int", + "value": 36 + }, + "name": { + "kind": "string", + "value": "Ada" + }, + "nickname": { + "kind": "null" + } + } + }, + "flags": "[ObjectFromFixedJson([]), FirstMatch(0, []), ObjectFromFixedJson([GreppedForJSON]), FirstMatch(0, []), OptionalDefaultFromNoValue, Pending]", + "checks": "[]", + "explanations": [] + }"#]].assert_eq(&parsed_fixture(&parsed)); +} + +#[test] +fn extra_keys_are_ignored_fixture() { + let raw = r#"{ "name": "Ada", "age": 36, "extra": "ignored" }"#; + let parsed = facet_runtime::parse_llm_output::(raw, true).expect("facet parse"); + + assert_eq!(parsed.value.name, "Ada"); + assert_eq!(parsed.value.nickname, None); + expect![[r#" + { + "baml_value": { + "kind": "class", + "name": "contract::property::FuzzUser", + "fields": { + "age": { + "kind": "int", + "value": 36 + }, + "name": { + "kind": "string", + "value": "Ada" + }, + "nickname": { + "kind": "null" + } + } + }, + "flags": "[ExtraKey(\"extra\", String(\"ignored\", Complete)), FirstMatch(0, []), OptionalDefaultFromNoValue, Pending]", + "checks": "[]", + "explanations": [] + }"#]].assert_eq(&parsed_fixture(&parsed)); +} + +#[test] +fn adapter_heavy_schema_registration_fixture() { + let output_format = ::baml_output_format(); + + let adapter = output_format + .classes + .get(&( + "PropertyAdapterI64Wrapper".to_string(), + StreamingMode::NonStreaming, + )) + .expect("adapter class missing"); + let adapter_fields = adapter + .fields + .iter() + .map(|(name, type_ir, _, _)| format!("{}: {type_ir:?}", name.real_name())) + .collect::>(); + + let owner_name = ::baml_internal_name().to_string(); + let owner = output_format + .classes + .get(&(owner_name, StreamingMode::NonStreaming)) + .expect("owner class missing"); + let owner_fields = owner + .fields + .iter() + .map(|(name, type_ir, _, _)| format!("{}: {type_ir:?}", name.real_name())) + .collect::>(); + + let snapshot = serde_json::to_string_pretty(&json!({ + "adapter_name": adapter.name.real_name(), + "adapter_fields": adapter_fields, + "owner_fields": owner_fields, + })) + .expect("serialize adapter schema fixture"); + + expect![[r#" + { + "adapter_name": "PropertyAdapterI64Wrapper", + "adapter_fields": [ + "value: Primitive(String, TypeMeta { constraints: [], streaming_behavior: StreamingBehavior { needed: false, done: false, state: false } })" + ], + "owner_fields": [ + "primary: Class { name: \"PropertyAdapterI64Wrapper\", mode: NonStreaming, dynamic: false, meta: TypeMeta { constraints: [], streaming_behavior: StreamingBehavior { needed: false, done: false, state: false } } }", + "secondary: Class { name: \"PropertyAdapterI64Wrapper\", mode: NonStreaming, dynamic: false, meta: TypeMeta { constraints: [], streaming_behavior: StreamingBehavior { needed: false, done: false, state: false } } }", + "tertiary: Class { name: \"PropertyAdapterI64Wrapper\", mode: NonStreaming, dynamic: false, meta: TypeMeta { constraints: [], streaming_behavior: StreamingBehavior { needed: false, done: false, state: false } } }" + ] + }"#]].assert_eq(&snapshot); +} + +#[test] +fn adapter_heavy_error_shape_fixture() { + let raw = serde_json::json!({ + "primary": { "value": "11" }, + "secondary": { "value": "oops" }, + "tertiary": { "value": "22" } + }) + .to_string(); + + let err = facet_runtime::parse_llm_output::(&raw, true).expect_err("facet err"); + let facet_runtime::BamlParseError::Convert(convert) = err else { + panic!("expected conversion error for adapter parse failure"); + }; + + let snapshot = serde_json::to_string_pretty(&json!({ + "expected": convert.expected, + "path": convert.path, + "display": convert.to_string(), + })) + .expect("serialize adapter error fixture"); + + expect![[r#" + { + "expected": "i64", + "path": [ + "secondary", + "value" + ], + "display": "failed to parse i64 (expected i64, got oops) at secondary.value" + }"#]] + .assert_eq(&snapshot); +} diff --git a/crates/bamltype/tests/render_options_parity.rs b/crates/bamltype/tests/render_options_parity.rs new file mode 100644 index 00000000..e8fd6c28 --- /dev/null +++ b/crates/bamltype/tests/render_options_parity.rs @@ -0,0 +1,212 @@ +use std::collections::HashMap; + +use bamltype as facet_runtime; +use expect_test::expect; + +#[derive(Debug, Clone, PartialEq)] +#[bamltype::BamlType] +#[baml(name = "RenderLeaf")] +#[baml(internal_name = "contract::render::Leaf")] +struct RenderLeaf { + value: String, +} + +#[derive(Debug, Clone, PartialEq)] +#[bamltype::BamlType] +#[baml(name = "RenderChoice")] +#[baml(internal_name = "contract::render::Choice")] +#[baml(tag = "kind")] +enum RenderChoice { + First { leaf: RenderLeaf }, + Second { score: i64 }, +} + +#[derive(Debug, Clone, PartialEq)] +#[bamltype::BamlType] +#[baml(name = "RenderRoot")] +#[baml(internal_name = "contract::render::Root")] +struct RenderRoot { + metadata: HashMap, + choice: RenderChoice, +} + +fn render(options: facet_runtime::RenderOptions) -> String { + facet_runtime::render_schema::(options) + .expect("facet render") + .unwrap_or_default() +} + +#[test] +fn render_options_default_fixture() { + let rendered = render(facet_runtime::RenderOptions::default()); + expect![[r#" + Answer in JSON using this schema: + { + metadata: map, + choice: { + kind: "First", + leaf: { + value: string, + }, + } or { + kind: "Second", + score: int, + }, + }"#]] + .assert_eq(&rendered); +} + +#[test] +fn render_options_map_style_object_literal_fixture() { + let rendered = render( + facet_runtime::RenderOptions::default() + .with_map_style(facet_runtime::MapStyle::ObjectLiteral), + ); + expect![[r#" + Answer in JSON using this schema: + { + metadata: {string: string}, + choice: { + kind: "First", + leaf: { + value: string, + }, + } or { + kind: "Second", + score: int, + }, + }"#]] + .assert_eq(&rendered); +} + +#[test] +fn render_options_quote_class_fields_fixture() { + let rendered = render(facet_runtime::RenderOptions::default().with_quote_class_fields(true)); + expect![[r#" + Answer in JSON using this schema: + { + "metadata": map, + "choice": { + "kind": "First", + "leaf": { + "value": string, + }, + } or { + "kind": "Second", + "score": int, + }, + }"#]] + .assert_eq(&rendered); +} + +#[test] +fn render_options_prefix_fixture() { + let rendered = render( + facet_runtime::RenderOptions::default().with_prefix(Some("contract schema".to_string())), + ); + expect![[r#" + contract schema{ + metadata: map, + choice: { + kind: "First", + leaf: { + value: string, + }, + } or { + kind: "Second", + score: int, + }, + }"#]] + .assert_eq(&rendered); +} + +#[test] +fn render_options_or_splitter_fixture() { + let rendered = render( + facet_runtime::RenderOptions::hoist_classes(facet_runtime::HoistClasses::All) + .with_or_splitter(" || "), + ); + expect![[r#" + RenderChoice_First { + kind: "First", + leaf: RenderLeaf, + } + + RenderChoice_Second { + kind: "Second", + score: int, + } + + RenderLeaf { + value: string, + } + + RenderRoot { + metadata: map, + choice: RenderChoice_First || RenderChoice_Second, + } + + Answer in JSON using this schema: RenderRoot"#]] + .assert_eq(&rendered); +} + +#[test] +fn render_options_hoist_subset_fixture() { + let rendered = render(facet_runtime::RenderOptions::default().with_hoist_classes( + facet_runtime::HoistClasses::Subset(vec!["contract::render::Leaf".to_string()]), + )); + expect![[r#" + RenderLeaf { + value: string, + } + + Answer in JSON using this schema: + { + metadata: map, + choice: { + kind: "First", + leaf: RenderLeaf, + } or { + kind: "Second", + score: int, + }, + }"#]] + .assert_eq(&rendered); +} + +#[test] +fn render_options_new_constructor_fixture() { + let rendered = render(facet_runtime::RenderOptions::new( + Some(Some("contract schema".to_string())), + Some(" || ".to_string()), + Some(Some("* ".to_string())), + Some(true), + Some(facet_runtime::MapStyle::ObjectLiteral), + Some(Some("TYPE ".to_string())), + Some(facet_runtime::HoistClasses::All), + Some(true), + )); + + expect![[r#" + TYPE RenderChoice_First { + "kind": "First", + "leaf": RenderLeaf, + } + + TYPE RenderChoice_Second { + "kind": "Second", + "score": int, + } + + TYPE RenderLeaf { + "value": string, + } + + TYPE RenderRoot { + "metadata": {string: string}, + "choice": RenderChoice_First || RenderChoice_Second, + } + + contract schemaRenderRoot"#]] + .assert_eq(&rendered); +}