Skip to main content
Glama
path.rs13.4 kB
use serde::{ Deserialize, Serialize, }; use si_id::PropId; use strum::EnumDiscriminants; use super::value::{ AttributeValueError, AttributeValueResult, }; use crate::{ AttributePrototype, AttributeValue, AttributeValueId, DalContext, Func, Prop, PropKind, prop::PropError, }; /// A path to an attribute value, relative to its root value /// This type is postcard serialized and new enum variants *MUST* be added to the end *ONLY*. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, EnumDiscriminants)] #[strum_discriminants(derive(Hash, Serialize, Deserialize, strum::EnumIter, strum::Display))] pub enum AttributePath { /// A JSON pointer (e.g. `/domain/PolicyDocument/Statements/0/Operation`) JsonPointer(String), } impl std::fmt::Display for AttributePath { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { AttributePath::JsonPointer(path) => write!(f, "{path}"), } } } impl AttributePath { pub fn from_json_pointer(path: impl Into<String>) -> Self { AttributePath::JsonPointer(path.into()) } pub fn as_bytes(&self) -> &[u8] { match self { AttributePath::JsonPointer(path) => path.as_bytes(), } } /// Resolves the attribute value at the JSON pointer, if one exists. /// /// Returns None if it cannot be found. pub async fn resolve( &self, ctx: &DalContext, av_id: AttributeValueId, ) -> AttributeValueResult<Option<AttributeValueId>> { match self { AttributePath::JsonPointer(pointer) => { let pointer = jsonptr::Pointer::parse(pointer) .map_err(|err| AttributeValueError::JsonptrParseError(pointer.clone(), err))?; resolve_json_pointer(ctx, av_id, pointer).await } } } /// Gets the attribute value at the JSON pointer. After this is complete, the AV can be /// set to an explicit value. /// /// Any parents that do not exist will be created. Any parents that are currently set to /// default values will become explicit values. pub async fn vivify( &self, ctx: &DalContext, av_id: AttributeValueId, ) -> AttributeValueResult<AttributeValueId> { match self { AttributePath::JsonPointer(pointer) => { let pointer = jsonptr::Pointer::parse(pointer) .map_err(|err| AttributeValueError::JsonptrParseError(pointer.clone(), err))?; vivify_json_pointer(ctx, av_id, pointer).await } } } /// Validate that the JSON pointer can refer to something real under the given prop. /// /// Returns the PropId of the referenced path. /// /// Errors if the JSON pointer refers to a missing field under an object. pub async fn validate( &self, ctx: &DalContext, prop_id: PropId, ) -> AttributeValueResult<PropId> { match self { AttributePath::JsonPointer(pointer) => { let pointer = jsonptr::Pointer::parse(pointer) .map_err(|err| AttributeValueError::JsonptrParseError(pointer.clone(), err))?; validate_json_pointer(ctx, prop_id, pointer).await } } } /// Get the path of this subscription, relative to the root attribute value. pub async fn from_root( &self, ctx: &DalContext, av_id: AttributeValueId, ) -> AttributeValueResult<(AttributeValueId, Self)> { match self { AttributePath::JsonPointer(relative_path) => { let (root_id, root_path) = AttributeValue::path_from_root(ctx, av_id).await?; Ok(( root_id, Self::JsonPointer(format!("{root_path}{relative_path}")), )) } } } /// Returns true if the `possible_parent_path` is included but doesn't completely match this path pub fn is_under(&self, possible_parent_path: &Self) -> bool { match (self, possible_parent_path) { (Self::JsonPointer(self_path), Self::JsonPointer(parent_path)) => { self_path.starts_with(parent_path) && self_path.as_bytes().get(parent_path.len()) == Some(&b'/') } } } } // Gets the attribute value at the JSON pointer, or None if it cannot be found. // Returns None if the AV points at non-existent props. async fn resolve_json_pointer( ctx: &DalContext, mut parent_id: AttributeValueId, pointer: &jsonptr::Pointer, ) -> AttributeValueResult<Option<AttributeValueId>> { // Go through each segment of the JSON pointer (e.g. /foo/bar/0 = foo, bar, 0) // and look for its child for token in pointer { let kind = AttributeValue::prop_kind(ctx, parent_id).await?; let child_av_id = match kind { // Look up array index in ordering node PropKind::Array => match token.to_index() { Ok(jsonptr::index::Index::Num(index)) => { AttributeValue::get_child_av_ids_in_order(ctx, parent_id) .await? .get(index) .copied() } Ok(jsonptr::index::Index::Next) | Err(_) => None, }, // Look at child Contains edges to find the one with the right name PropKind::Map => { AttributeValue::map_child_opt(ctx, parent_id, token.decoded().as_ref()).await? } // The object's children AVs will already have been vivified. Just grab the right one. PropKind::Object => { AttributeValue::object_child_opt(ctx, parent_id, token.decoded().as_ref()).await? } // These cannot have children PropKind::Boolean | PropKind::Integer | PropKind::Json | PropKind::Float | PropKind::String => { return Err(AttributeValueError::NoChildWithName( parent_id, token.decoded().into_owned(), ))?; } }; let Some(child_av_id) = child_av_id else { return Ok(None); }; parent_id = child_av_id; } Ok(Some(parent_id)) } /// Gets the attribute value at the JSON pointer, or creates it if it doesn't exist. /// Returns an error if an AV cannot be created. async fn vivify_json_pointer( ctx: &DalContext, mut parent_id: AttributeValueId, pointer: &jsonptr::Pointer, ) -> AttributeValueResult<AttributeValueId> { // Go through each segment of the JSON pointer (e.g. /foo/bar/0 = foo, bar, 0) // and look for its child. If it doesn't exist, create it. for token in pointer { // // Ensure the parent is si:setObject/si:setArray/si:setMap, so it can have children. // let kind = AttributeValue::prop_kind(ctx, parent_id).await?; let prototype_id = AttributeValue::prototype_id(ctx, parent_id).await?; let func_id = AttributePrototype::func_id(ctx, prototype_id).await?; let intrinsic = Func::intrinsic_kind(ctx, func_id).await?; // If it's not si:setObject/si:setArray/si:setMap, fix it! if intrinsic != Some(kind.intrinsic_set_func()) { if AttributePrototype::is_dynamic(ctx, prototype_id).await? { // You can't modify children of dynamic values on the component itself--only // if they are defaults from the prototype. i.e. if /domain/Foo is a subscription, // you can't modify /domain/Foo/Bar. If you want to do this, you have to clear // the subscription first. We don't mind, however, if the dynamic value comes // from the default (from the prop); that's just "override the default". if AttributeValue::component_prototype_id(ctx, parent_id) .await? .is_some() { return Err(AttributeValueError::CannotSetChildOfDynamicValue(parent_id)); } // If it's a dynamic default (or socket connections), we have to clear the // existing value. AttributeValue::update(ctx, parent_id, kind.empty_value()).await?; } else { // If it's si:unset (value=None), we set to empty value instead. // NOTE: we use set_value() here because we want to preserve default values! // This matches what AttributeValue::vivify_value_and_parent_values() does. AttributeValue::set_value(ctx, parent_id, kind.empty_value()).await?; } } // // Find or create the child! // parent_id = match kind { PropKind::Array => { // Make sure the user asked to append (can't insert at an index that doesn't exist) let elements = AttributeValue::get_child_av_ids_in_order(ctx, parent_id).await?; match token.to_index().map_err(|err| { AttributeValueError::JsonptrParseIndexError(pointer.to_string(), err) })? { // If it's - or the next index (len+1), we can append! jsonptr::index::Index::Next => { AttributeValue::insert(ctx, parent_id, None, None).await? } jsonptr::index::Index::Num(index) if index == elements.len() => { AttributeValue::insert(ctx, parent_id, None, None).await? } // If it's not the last index + 1, we retrieve the existing one jsonptr::index::Index::Num(index) => elements.get(index).copied().ok_or( AttributeValueError::IndexOutOfRange(index, parent_id, elements.len()), )?, } } // Create empty map entry with given name PropKind::Map => { let name = token.decoded(); match AttributeValue::map_child_opt(ctx, parent_id, name.as_ref()).await? { Some(child_av_id) => child_av_id, None => { AttributeValue::insert(ctx, parent_id, None, Some(name.into_owned())) .await? } } } // Get the matching child AV (all possible child AVs must already exist) PropKind::Object => { let name = token.decoded(); AttributeValue::object_child(ctx, parent_id, name.as_ref()).await? } // These cannot have children PropKind::Boolean | PropKind::Integer | PropKind::Json | PropKind::Float | PropKind::String => { return Err(AttributeValueError::NoChildWithName( parent_id, token.decoded().into_owned(), ))?; } }; } Ok(parent_id) } // Validate that a JSON pointer *could* be used to access a child of the given AV (that it // has valid prop names and indices). async fn validate_json_pointer( ctx: &DalContext, mut parent_id: PropId, pointer: &jsonptr::Pointer, ) -> AttributeValueResult<PropId> { // Go through each segment of the JSON pointer (e.g. /foo/bar/0 = foo, bar, 0) // and validate that the child prop exists or that it is a valid index and not - for token in pointer { let prop = Prop::get_by_id(ctx, parent_id).await?; parent_id = match prop.kind { // Look up array index in ordering node PropKind::Array => match token.to_index().map_err(|err| { AttributeValueError::JsonptrParseIndexError(pointer.to_string(), err) })? { jsonptr::index::Index::Num(_) => Prop::element_prop_id(ctx, parent_id).await?, jsonptr::index::Index::Next => { return Err(PropError::CannotSubscribeToNextElement( parent_id, pointer.to_string(), ))?; } }, // All strings are valid map keys PropKind::Map => Prop::element_prop_id(ctx, parent_id).await?, // The key must be a valid child prop name PropKind::Object => { Prop::child_prop_id(ctx, parent_id.into(), token.decoded().as_ref()).await? } // These cannot have children PropKind::Boolean | PropKind::Integer | PropKind::Json | PropKind::Float | PropKind::String => { return Err(PropError::ChildPropNotFoundByName( parent_id.into(), token.decoded().into_owned(), ))?; } } } Ok(parent_id) }

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