Skip to main content
Glama
attributes.rs25.9 kB
use dal::{ AttributePrototype, AttributeValue, Component, DalContext, Func, action::{ Action, prototype::ActionPrototype, }, attribute::{ attributes::{ self, AttributeSources, }, value::AttributeValueError, }, }; use dal_test::{ Result, helpers::{ attribute::value::{ self, }, change_set, component::{ self, }, create_component_for_default_schema_name_in_default_view, schema::variant, }, prelude::ChangeSetTestHelpers, test, }; use pretty_assertions_sorted::assert_eq; use serde_json::json; use si_events::{ ActionKind, ActionState, }; // Test that updating attributes sets them (and their parents) correctly, but leaves default // values and other values alone. #[test] async fn update_attributes(ctx: &DalContext) -> Result<()> { variant::create( ctx, "test", r#" function main() { return { props: [ { name: "Parent", kind: "object", children: [ { name: "Updated", kind: "string" }, { name: "New", kind: "string" }, { name: "Unchanged", kind: "string" }, { name: "Missing", kind: "string" }, { name: "Default", kind: "string", defaultValue: "default" }, ]}, { name: "Missing", kind: "string" }, { name: "Default", kind: "string", defaultValue: "default" }, ] }; } "#, ) .await?; // Set some initial values and make sure they are set correctly without messing with other // values or defaults. let component_id = component::create(ctx, "test", "test").await?; attributes::update_attributes( ctx, component_id, serde_json::from_value(json!({ "/domain/Parent/Updated": "old", "/domain/Parent/Unchanged": "old", }))?, ) .await?; assert_eq!( json!({ "Parent": { "Updated": "old", "Unchanged": "old", "Default": "default", }, "Default": "default", }), component::domain(ctx, "test").await? ); assert!(value::is_set(ctx, ("test", "/domain/Parent/Updated")).await?); assert!(!value::is_set(ctx, ("test", "/domain/Parent/New")).await?); assert!(value::is_set(ctx, ("test", "/domain/Parent/Unchanged")).await?); assert!(!value::is_set(ctx, ("test", "/domain/Parent/Missing")).await?); assert!(!value::is_set(ctx, ("test", "/domain/Parent/Default")).await?); assert!(!value::is_set(ctx, ("test", "/domain/Default")).await?); // Update values and make sure they are updated correctly without messing with other values // or defaults. attributes::update_attributes( ctx, component_id, serde_json::from_value(json!({ "/domain/Parent/Updated": "new", "/domain/Parent/New": "new", }))?, ) .await?; assert_eq!( json!({ "Parent": { "Updated": "new", "New": "new", "Unchanged": "old", "Default": "default", }, "Default": "default", }), component::domain(ctx, "test").await? ); assert!(value::is_set(ctx, ("test", "/domain/Parent/Updated")).await?); assert!(value::is_set(ctx, ("test", "/domain/Parent/New")).await?); assert!(value::is_set(ctx, ("test", "/domain/Parent/Unchanged")).await?); assert!(!value::is_set(ctx, ("test", "/domain/Parent/Missing")).await?); assert!(!value::is_set(ctx, ("test", "/domain/Parent/Default")).await?); assert!(!value::is_set(ctx, ("test", "/domain/Default")).await?); Ok(()) } // Test that updating an attribute value via the new subscription interface correctly enqueues // update actions #[test] async fn update_attributes_enqueues_update_fn(ctx: &mut DalContext) -> Result<()> { // ====================================================== // Creating a component should enqueue a create action // ====================================================== let component_jack = create_component_for_default_schema_name_in_default_view(ctx, "swifty", "jack antonoff") .await?; let component_swift = create_component_for_default_schema_name_in_default_view(ctx, "swifty", "taylor swift") .await?; ChangeSetTestHelpers::commit_and_update_snapshot_to_visibility(ctx).await?; // Apply changeset so it runs the creation action ChangeSetTestHelpers::apply_change_set_to_base(ctx).await?; // wait for actions to run ChangeSetTestHelpers::wait_for_actions_to_run(ctx).await?; ChangeSetTestHelpers::fork_from_head_change_set(ctx).await?; Action::remove_all_for_component_id(ctx, component_jack.id()).await?; // ====================================================== // Updating values in a component that has a resource should enqueue an update action // ====================================================== attributes::update_attributes( ctx, component_jack.id(), serde_json::from_value(json!({ "/domain/name": "whomever", }))?, ) .await?; ChangeSetTestHelpers::commit_and_update_snapshot_to_visibility(ctx).await?; let action_ids = Action::list_topologically(ctx).await?; let mut update_action_count = 0; for action_id in action_ids { let action = Action::get_by_id(ctx, action_id).await?; if action.state() == ActionState::Queued { let prototype_id = Action::prototype_id(ctx, action_id).await?; let prototype = ActionPrototype::get_by_id(ctx, prototype_id).await?; let component_id = Action::component_id(ctx, action_id) .await? .expect("is some"); if prototype.kind == ActionKind::Update.into() && component_id == component_jack.id() { update_action_count += 1; }; } } assert_eq!(1, update_action_count); // ====================================================== // Updating values in a component that has a resource should not enqueue an update // action if the value didn't change // ====================================================== attributes::update_attributes( ctx, component_swift.id(), serde_json::from_value(json!({ "/domain/name": "taylor swift", }))?, ) .await?; change_set::commit(ctx).await?; Action::remove_all_for_component_id(ctx, component_swift.id()).await?; let action_ids = Action::list_topologically(ctx).await?; let mut update_action_count = 0; for action_id in &action_ids { let action_id = *action_id; let action = Action::get_by_id(ctx, action_id).await?; if action.state() == ActionState::Queued { let prototype_id = Action::prototype_id(ctx, action_id).await?; let prototype = ActionPrototype::get_by_id(ctx, prototype_id).await?; let component_id = Action::component_id(ctx, action_id) .await? .expect("is some"); if prototype.kind == ActionKind::Update.into() && component_id == component_swift.id() { update_action_count += 1; }; } } // didn't actually change the value, so there should not be an update function for swifty! assert_eq!(0, update_action_count); Ok(()) } // Test that attribute updates are processed in the order they are specified sets them (and their parents) correctly, but leaves default // values and other values alone. #[test] async fn update_attributes_runs_in_order_and_allows_duplicates(ctx: &mut DalContext) -> Result<()> { variant::create( ctx, "test", r#" function main() { return { props: [ { name: "Arr", kind: "array", entry: { name: "ArrayItem", kind: "string" }, }, ] }; } "#, ) .await?; // Subscribe source.Obj -> dest.Obj, source.Arr -> dest.Arr let test = component::create(ctx, "test", "test").await?; attributes::update_attributes( ctx, test, serde_json::from_str( r#"{ "/domain/Arr/-": "0", "/domain/Arr/-": "1", "/domain/Arr/2": "oops", "/domain/Arr/3": "3", "/domain/Arr/4": "4", "/domain/Arr/-": "5", "/domain/Arr/-": "6", "/domain/Arr/-": "7", "/domain/Arr/-": "8", "/domain/Arr/-": "9", "/domain/Arr/10": "oops", "/domain/Arr/10": "10", "/domain/Arr/11": "11", "/domain/Arr/2": "2" }"#, )?, ) .await?; assert_eq!( json!({ "Arr": ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11"] }), component::domain(ctx, "test").await?, ); Ok(()) } #[test] async fn update_attributes_serializes_json(ctx: &mut DalContext) -> Result<()> { variant::create( ctx, "test", r#" function main() { return { props: [ { name: "Json", kind: "json" }, ] }; } "#, ) .await?; let test = component::create(ctx, "test", "test").await?; // Set Json = string attributes::update_attributes( ctx, test, serde_json::from_str( r#"{ "/domain/Json": "{ \"foo\": \"bar\" }" }"#, )?, ) .await?; assert_eq!( r#"{ "foo": "bar" }"#, value::get(ctx, ("test", "/domain/Json")).await? ); // Set Json = object attributes::update_attributes( ctx, test, serde_json::from_str( r#"{ "/domain/Json": { "foo": "bar" } }"#, )?, ) .await?; assert_eq!( r#"{ "foo": "bar" }"#, value::get(ctx, ("test", "/domain/Json")).await? ); // Set Json = array attributes::update_attributes( ctx, test, serde_json::from_str( r#"{ "/domain/Json": [ 1, 2 ] }"#, )?, ) .await?; assert_eq!( r#"[ 1, 2 ]"#, value::get(ctx, ("test", "/domain/Json")).await? ); Ok(()) } // Test that attribute updates are processed in the order they are specified sets them (and their parents) correctly, but leaves default // values and other values alone. #[test] async fn component_sources_in_order(ctx: &mut DalContext) -> Result<()> { variant::create( ctx, "test", r#" function main() { return { props: [ { name: "Foo", kind: "string" }, { name: "Bar", kind: "string" }, { name: "Arr", kind: "array", entry: { name: "ArrItem", kind: "string" }, }, ] }; } "#, ) .await?; // Subscribe source.Obj -> dest.Obj, source.Arr -> dest.Arr let test = component::create(ctx, "test", "test").await?; // If none of the values are set, sources should be empty assert_eq!( json!({ "/si/name": "test", "/si/type": "component", }), serde_json::to_value(AttributeSources::from(Component::sources(ctx, test).await?))? ); // If some of the values are set, sources should contain them value::set(ctx, ("test", "/domain/Foo"), "foo").await?; assert_eq!( json!({ "/si/name": "test", "/si/type": "component", "/domain/Foo": "foo" }), serde_json::to_value(AttributeSources::from(Component::sources(ctx, test).await?))? ); // If all of the values are set, sources should contain them value::set(ctx, ("test", "/domain/Bar"), "bar").await?; value::set(ctx, ("test", "/domain/Arr"), ["a", "b"]).await?; assert_eq!( json!({ "/si/name": "test", "/si/type": "component", "/domain/Foo": "foo", "/domain/Bar": "bar", "/domain/Arr/0": "a", "/domain/Arr/1": "b", }), serde_json::to_value(AttributeSources::from(Component::sources(ctx, test).await?))? ); // If some of the array items are subscriptions, sources should show that let subscriber = component::create(ctx, "test", "subscriber").await?; value::subscribe(ctx, ("subscriber", "/domain/Foo"), ("test", "/domain/Foo")).await?; value::set(ctx, ("subscriber", "/domain/Bar"), "bar2").await?; value::subscribe( ctx, ("subscriber", "/domain/Arr/-"), ("test", "/domain/Arr/0"), ) .await?; value::set(ctx, ("subscriber", "/domain/Arr/-"), "a2").await?; value::subscribe( ctx, ("subscriber", "/domain/Arr/-"), ("test", "/domain/Arr/1"), ) .await?; value::set(ctx, ("subscriber", "/domain/Arr/-"), "b2").await?; change_set::commit(ctx).await?; assert_eq!( json!({ "Foo": "foo", "Bar": "bar2", "Arr": [ "a", "a2", "b", "b2" ] }), component::domain(ctx, "subscriber").await? ); assert_eq!( json!({ "/si/name": "subscriber", "/si/type": "component", "/domain/Foo": { "$source": { "component": test.to_string(), "path": "/domain/Foo" } }, "/domain/Bar": "bar2", "/domain/Arr/0": { "$source": { "component": test.to_string(), "path": "/domain/Arr/0" } }, "/domain/Arr/1": "a2", "/domain/Arr/2": { "$source": { "component": test.to_string(), "path": "/domain/Arr/1" } }, "/domain/Arr/3": "b2" }), serde_json::to_value(AttributeSources::from( Component::sources(ctx, subscriber).await? ))? ); // If the entire array is a subscription, child values should not be included even if DVU has run let subscriber2 = component::create(ctx, "test", "subscriber2").await?; value::subscribe(ctx, ("subscriber2", "/domain/Arr"), ("test", "/domain/Arr")).await?; change_set::commit(ctx).await?; assert_eq!( json!({ "Arr": [ "a", "b" ] }), component::domain(ctx, "subscriber2").await? ); assert_eq!( json!({ "/si/name": "subscriber2", "/si/type": "component", "/domain/Arr": { "$source": { "component": test.to_string(), "path": "/domain/Arr", } }, }), serde_json::to_value(AttributeSources::from( Component::sources(ctx, subscriber2).await? ))? ); Ok(()) } // Test that updating attributes sets them (and their parents) correctly, but leaves default // values and other values alone. #[test] async fn update_attribute_child_of_subscription(ctx: &mut DalContext) -> Result<()> { variant::create( ctx, "test", r#" function main() { return { props: [ { name: "Obj", kind: "object", children: [ { name: "Field", kind: "string" }, ]}, { name: "Map", kind: "map", entry: { name: "MapItem", kind: "string" }, }, { name: "Arr", kind: "array", entry: { name: "ArrayItem", kind: "string" }, }, ] }; } "#, ) .await?; // Subscribe source.Obj -> dest.Obj, source.Arr -> dest.Arr component::create(ctx, "test", "source").await?; value::set(ctx, ("source", "/domain/Obj/Field"), "value").await?; value::set(ctx, ("source", "/domain/Map/a"), "valueA").await?; value::set(ctx, ("source", "/domain/Map/b"), "valueB").await?; value::set(ctx, ("source", "/domain/Arr"), ["a", "b"]).await?; let dest = component::create(ctx, "test", "dest").await?; value::subscribe(ctx, ("dest", "/domain/Obj"), ("source", "/domain/Obj")).await?; value::subscribe(ctx, ("dest", "/domain/Map"), ("source", "/domain/Map")).await?; value::subscribe(ctx, ("dest", "/domain/Arr"), ("source", "/domain/Arr")).await?; change_set::commit(ctx).await?; assert_eq!( json!({ "Obj": { "Field": "value", }, "Map": { "a": "valueA", "b": "valueB", }, "Arr": ["a", "b"], }), component::domain(ctx, "dest").await? ); // Check that updating a child value of an object/map/array yields an error assert!(matches!( attributes::update_attributes( ctx, dest, serde_json::from_value(json!({ "/domain/Obj/Field": "new", }))?, ) .await, Err(attributes::AttributesError::AttributeValue( AttributeValueError::CannotSetChildOfDynamicValue(..) )), )); assert!(matches!( attributes::update_attributes( ctx, dest, serde_json::from_value(json!({ "/domain/Map/a": "updated", }))?, ) .await, Err(attributes::AttributesError::AttributeValue( AttributeValueError::CannotSetChildOfDynamicValue(..) )), )); assert!(matches!( attributes::update_attributes( ctx, dest, serde_json::from_value(json!({ "/domain/Arr/0": "new", }))?, ) .await, Err(attributes::AttributesError::AttributeValue( AttributeValueError::CannotSetChildOfDynamicValue(..) )), )); assert!(matches!( attributes::update_attributes( ctx, dest, serde_json::from_value(json!({ "/domain/Arr/-": "new", }))?, ) .await, Err(attributes::AttributesError::AttributeValue( AttributeValueError::CannotSetChildOfDynamicValue(..) )), )); // Check that removing a child value of an object/map/array yields an error assert!(matches!( attributes::update_attributes( ctx, dest, serde_json::from_value(json!({ "/domain/Obj/Field": { "$source": null }, }))?, ) .await, Err(attributes::AttributesError::AttributeValue( AttributeValueError::CannotSetChildOfDynamicValue(..) )), )); assert!(matches!( attributes::update_attributes( ctx, dest, serde_json::from_value(json!({ "/domain/Map/a": { "$source": null }, }))?, ) .await, Err(attributes::AttributesError::AttributeValue( AttributeValueError::CannotSetChildOfDynamicValue(..) )), )); assert!(matches!( attributes::update_attributes( ctx, dest, serde_json::from_value(json!({ "/domain/Arr/0": { "$source": null }, }))?, ) .await, Err(attributes::AttributesError::AttributeValue( AttributeValueError::CannotSetChildOfDynamicValue(..) )), )); Ok(()) } /// Test that when a user manually overrides a value that has a default, /// then deletes that override with { $source: null }, the value reverts to its default. /// /// This tests the fix for a bug where use_default_prototype was calling update(None) /// which created a si:Unset component prototype BEFORE removing the manual override, /// causing the component prototype to remain instead of reverting to the schema variant prototype. #[test] async fn unset_manual_override_reverts_to_default(ctx: &mut DalContext) -> Result<()> { // Create a schema with a default value variant::create( ctx, "test", r#" function main() { return { props: [ { name: "Value", kind: "string", defaultValue: "default-value" }, ] }; } "#, ) .await?; // Create a component - it should have the default value let component_id = component::create(ctx, "test", "test").await?; change_set::commit(ctx).await?; // Verify the default value is present assert_eq!( json!({ "Value": "default-value", }), component::domain(ctx, "test").await? ); // Manually override the value value::set(ctx, ("test", "/domain/Value"), "manual-override").await?; change_set::commit(ctx).await?; // Verify the manual override took effect assert_eq!( json!({ "Value": "manual-override", }), component::domain(ctx, "test").await? ); // Verify the value is marked as overridden (has a component prototype) let av_id = value::id(ctx, ("test", "/domain/Value")).await?; let component_prototype = AttributeValue::component_prototype_id(ctx, av_id).await?; assert!( component_prototype.is_some(), "Value should have a component prototype after manual override" ); // Delete the manual override with { $source: null } attributes::update_attributes( ctx, component_id, serde_json::from_value(json!({ "/domain/Value": { "$source": null }, }))?, ) .await?; change_set::commit(ctx).await?; // Verify the value reverted to the default assert_eq!( json!({ "Value": "default-value", }), component::domain(ctx, "test").await?, "After unsetting manual override, value should revert to default" ); // Verify the component prototype is gone (reverted to schema variant prototype) let component_prototype_after = AttributeValue::component_prototype_id(ctx, av_id).await?; assert!( component_prototype_after.is_none(), "Value should no longer have a component prototype after unsetting" ); Ok(()) } /// Test that unsetting a value that was never manually overridden creates a si:Unset component prototype. /// This tests the else branch of use_default_prototype where no component prototype exists. #[test] async fn unset_value_without_existing_override_creates_unset_prototype( ctx: &mut DalContext, ) -> Result<()> { // Create a schema with a default value variant::create( ctx, "test", r#" function main() { return { props: [ { name: "WithDefault", kind: "string", defaultValue: "default-value" }, ] }; } "#, ) .await?; // Create a component - it should have the default value let component_id = component::create(ctx, "test", "test").await?; change_set::commit(ctx).await?; // Verify the default value is present assert_eq!( json!({ "WithDefault": "default-value", }), component::domain(ctx, "test").await? ); // Verify there's NO component prototype (using schema variant prototype) let av_id = value::id(ctx, ("test", "/domain/WithDefault")).await?; let component_prototype_before = AttributeValue::component_prototype_id(ctx, av_id).await?; assert!( component_prototype_before.is_none(), "Value should not have a component prototype initially (using default)" ); // Send { $source: null } even though there's no override to remove // This should trigger the else branch and create a si:Unset component prototype attributes::update_attributes( ctx, component_id, serde_json::from_value(json!({ "/domain/WithDefault": { "$source": null }, }))?, ) .await?; change_set::commit(ctx).await?; // Verify a component prototype was created (si:Unset) let component_prototype_after = AttributeValue::component_prototype_id(ctx, av_id).await?; assert!( component_prototype_after.is_some(), "Value should now have a component prototype (si:Unset) after explicit unsetting" ); // Verify it's actually a si:Unset intrinsic function let prototype_id = component_prototype_after.expect("component prototype exists"); let func_id = AttributePrototype::func_id(ctx, prototype_id).await?; let func = Func::get_by_id(ctx, func_id).await?; assert!( func.is_intrinsic(), "Component prototype should use an intrinsic function (si:Unset)" ); assert_eq!( "si:unset", func.name.as_str(), "Component prototype should specifically use si:unset" ); Ok(()) }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/systeminit/si'

If you have feedback or need assistance with the MCP directory API, please join our Discord server