Skip to main content
Glama
prop.rs9.74 kB
use async_trait::async_trait; use petgraph::prelude::*; use si_id::{ AttributePrototypeId, PropId, }; use crate::{ DalContext, EdgeWeightKindDiscriminants, PropKind, WorkspaceSnapshot, WorkspaceSnapshotGraphVCurrent, prop::{ PropError, PropResult, }, slow_rt, workspace_snapshot::{ graph::traits::prop::PropExt as _, node_weight::PropNodeWeight, traits::{ attribute_prototype::AttributePrototypeExt as _, attribute_prototype_argument::AttributePrototypeArgumentExt, func::FuncExt as _, }, }, }; #[async_trait] pub trait PropExt { /// The default value for a [`Prop`][crate::prop::Prop], if there is a default value. async fn prop_default_value( &self, ctx: &DalContext, prop_id: PropId, ) -> PropResult<Option<serde_json::Value>>; /// The first [`AttributePrototypeId`] for the given [`PropId`]. It is possible for a /// prop to have multiple prototypes (for example with maps having ones for different /// keys in the map), but it is an error for the prop to not have any prototypes. async fn prop_prototype_id(&self, prop_id: PropId) -> PropResult<AttributePrototypeId>; /// Generate a TypeScript type for a prop tree. async fn ts_type(&self, prop_id: PropId) -> PropResult<String>; /// Build complete prop schema tree. async fn build_prop_schema_tree( &self, ctx: &DalContext, root_prop_id: PropId, ) -> PropResult<si_frontend_mv_types::prop_schema::PropSchemaV1>; } #[async_trait] impl PropExt for WorkspaceSnapshot { async fn prop_default_value( &self, ctx: &DalContext, prop_id: PropId, ) -> PropResult<Option<serde_json::Value>> { let prototype_id = self.prop_prototype_id(prop_id).await?; let func_id = self.attribute_prototype_func_id(prototype_id).await?; if self.func_is_dynamic(func_id).await? { return Ok(None); } match self .attribute_prototype_arguments(prototype_id) .await? .first() { Some(&apa_id) => self .attribute_prototype_argument_static_value(ctx, apa_id) .await .map_err(Into::into), None => Ok(None), } } async fn prop_prototype_id(&self, prop_id: PropId) -> PropResult<AttributePrototypeId> { self.outgoing_targets_for_edge_weight_kind(prop_id, EdgeWeightKindDiscriminants::Prototype) .await? .first() .copied() .map(Into::into) .ok_or_else(|| PropError::MissingPrototypeForProp(prop_id)) } async fn ts_type(&self, prop_id: PropId) -> PropResult<String> { let self_clone = self.clone(); slow_rt::spawn(async move { ts_type(self_clone, prop_id).await })?.await? } async fn build_prop_schema_tree( &self, ctx: &DalContext, root_prop_id: PropId, ) -> PropResult<si_frontend_mv_types::prop_schema::PropSchemaV1> { use std::collections::{ HashMap, VecDeque, }; let tree_data = self .working_copy() .await .build_prop_schema_tree_data(root_prop_id)? .ok_or_else(|| PropError::PropNotFound(root_prop_id))?; let mut sub_schemas = HashMap::new(); let mut forward_queue = VecDeque::from([root_prop_id]); let mut work_stack = Vec::with_capacity(tree_data.props.len()); // Build work stack ensuring children appear after parents (BFS) while let Some(prop_id) = forward_queue.pop_front() { work_stack.push(prop_id); if let Some(child_ids) = tree_data.children.get(&prop_id) { forward_queue.extend(child_ids); } } // PProcess work stack in reverse (post-order) so children are built before parents while let Some(prop_id) = work_stack.pop() { let prop_data = tree_data .props .get(&prop_id) .ok_or_else(|| PropError::PropNotFound(prop_id))?; let prop_content = ctx .layer_db() .cas() .try_read_as::<crate::layer_db_types::PropContent>(&prop_data.content_hash) .await? .ok_or_else(|| PropError::PropNotFound(prop_id))?; let content_v2: crate::layer_db_types::PropContentV2 = prop_content.into(); let validation_format = content_v2.validation_format.clone(); let hidden = Some(content_v2.hidden); let doc_link = content_v2.doc_link.clone(); let description = content_v2.documentation.clone(); let default_value = self.prop_default_value(ctx, prop_id).await?; let children = if let Some(child_ids) = tree_data.children.get(&prop_id) { let mut child_schemas = Vec::with_capacity(child_ids.len()); for &child_id in child_ids { if let Some(child_schema) = sub_schemas.remove(&child_id) { child_schemas.push(child_schema); } else { return Err(PropError::PropNotFound(child_id)); // Child not ready, shouldn't happen with post-order } } if child_schemas.is_empty() { None } else { Some(child_schemas) } } else { None }; let prop_type = prop_data.kind.as_ref(); let schema = si_frontend_mv_types::prop_schema::PropSchemaV1 { prop_id, name: prop_data.name.clone(), prop_type: prop_type.to_string(), description, children, validation_format, default_value, hidden, doc_link, }; sub_schemas.insert(prop_id, schema); } sub_schemas .remove(&root_prop_id) .ok_or_else(|| PropError::PropNotFound(root_prop_id)) } } async fn ts_type(snap: WorkspaceSnapshot, prop_id: PropId) -> PropResult<String> { let graph = snap.working_copy().await; let index = graph.get_node_index_by_id(prop_id)?; let node = graph.get_node_weight(index)?.as_prop_node_weight()?; let mut result = String::new(); append_ts_type(&graph, node, index, &mut result)?; Ok(result) } fn append_ts_type( graph: &WorkspaceSnapshotGraphVCurrent, node: &PropNodeWeight, index: NodeIndex, buf: &mut String, ) -> PropResult<()> { /// Check if the parent of the given node has the specified path. fn parent_has_path( graph: &WorkspaceSnapshotGraphVCurrent, index: NodeIndex, path: &[&str], ) -> PropResult<bool> { // Get the parent let parent_index = graph.get_edge_weight_kind_target_idx_opt( index, Incoming, EdgeWeightKindDiscriminants::Use, )?; // If the path is empty, we match iff there is no parent let Some((&name, parent_path)) = path.split_last() else { return Ok(parent_index.is_none()); }; // If the path is non-empty, but we have a parent, we don't match let Some(parent_index) = parent_index else { return Ok(false); }; let node = graph.get_node_weight(parent_index)?.as_prop_node_weight()?; Ok(name == node.name() && parent_has_path(graph, parent_index, parent_path)?) } // Special cases if node.name() == "status" && parent_has_path(graph, index, &["root", "resource"])? { buf.push_str("'ok' | 'warning' | 'error' | undefined | null"); return Ok(()); } if node.name() == "payload" && parent_has_path(graph, index, &["root", "resource"])? { buf.push_str("any"); return Ok(()); } match node.kind() { PropKind::Array => { append_ts_element_type(graph, index, buf)?; buf.push_str("[]"); } PropKind::Boolean => buf.push_str("boolean"), PropKind::Float | PropKind::Integer => buf.push_str("number"), PropKind::Json => buf.push_str("any"), PropKind::Map => { buf.push_str("Record<string, "); append_ts_element_type(graph, index, buf)?; buf.push('>'); } PropKind::Object => { buf.push_str("{\n"); for child_index in graph.ordered_children_for_node(index)?.unwrap_or(vec![]) { let child_node = graph.get_node_weight(child_index)?.as_prop_node_weight()?; buf.push_str(&serde_json::to_string(child_node.name())?); buf.push_str("?: "); append_ts_type(graph, child_node, child_index, buf)?; buf.push_str(" | null;\n"); } buf.push('}'); } PropKind::String => buf.push_str("string"), }; Ok(()) } /// Generate a TypeScript type for the element type of an array or map. fn append_ts_element_type( graph: &WorkspaceSnapshotGraphVCurrent, parent_index: NodeIndex, buf: &mut String, ) -> PropResult<()> { let element_prop_index = graph.get_edge_weight_kind_target_idx( parent_index, Outgoing, EdgeWeightKindDiscriminants::Use, )?; let element_prop_node = graph .get_node_weight(element_prop_index)? .as_prop_node_weight()?; append_ts_type(graph, element_prop_node, element_prop_index, buf) }

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