Skip to main content
Glama
prop.rs33.9 kB
use std::collections::{ HashMap, HashSet, }; use derive_builder::UninitializedFieldError; use indexmap::IndexMap; use serde::{ Deserialize, Serialize, }; use strum::{ AsRefStr, Display, EnumIter, EnumString, }; use url::Url; use super::{ AttrFuncInputSpec, HasUniqueId, MapKeyFuncSpec, SpecError, }; #[remain::sorted] #[derive( Debug, Serialize, Deserialize, Clone, PartialEq, Eq, AsRefStr, Display, EnumIter, EnumString, Copy, Default, )] pub enum PropSpecWidgetKind { Array, Checkbox, CodeEditor, Color, ComboBox, Header, Map, Password, Secret, Select, #[default] Text, TextArea, } impl From<&PropSpec> for PropSpecWidgetKind { fn from(node: &PropSpec) -> Self { match node { PropSpec::Array { .. } => Self::Array, PropSpec::Boolean { .. } => Self::Checkbox, PropSpec::String { .. } | PropSpec::Number { .. } | PropSpec::Float { .. } | PropSpec::Json { .. } => Self::Text, PropSpec::Object { .. } => Self::Header, PropSpec::Map { .. } => Self::Map, } } } #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct PropSpecData { pub name: String, pub validation_format: Option<String>, pub default_value: Option<serde_json::Value>, pub func_unique_id: Option<String>, pub inputs: Option<Vec<AttrFuncInputSpec>>, pub widget_kind: Option<PropSpecWidgetKind>, pub widget_options: Option<serde_json::Value>, pub hidden: Option<bool>, pub doc_link: Option<Url>, pub documentation: Option<String>, pub ui_optionals: Option<HashMap<String, serde_json::Value>>, } #[remain::sorted] #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(tag = "kind", rename_all = "camelCase")] pub enum PropSpec { #[serde(rename_all = "camelCase")] Array { name: String, data: Option<PropSpecData>, unique_id: Option<String>, type_prop: Box<PropSpec>, }, #[serde(rename_all = "camelCase")] Boolean { name: String, data: Option<PropSpecData>, unique_id: Option<String>, }, #[serde(rename_all = "camelCase")] Float { name: String, data: Option<PropSpecData>, unique_id: Option<String>, }, #[serde(rename_all = "camelCase")] Json { name: String, data: Option<PropSpecData>, unique_id: Option<String>, }, #[serde(rename_all = "camelCase")] Map { name: String, data: Option<PropSpecData>, unique_id: Option<String>, type_prop: Box<PropSpec>, map_key_funcs: Option<Vec<MapKeyFuncSpec>>, }, #[serde(rename_all = "camelCase")] Number { name: String, data: Option<PropSpecData>, unique_id: Option<String>, }, #[serde(rename_all = "camelCase")] Object { name: String, data: Option<PropSpecData>, unique_id: Option<String>, entries: Vec<PropSpec>, }, #[serde(rename_all = "camelCase")] String { name: String, data: Option<PropSpecData>, unique_id: Option<String>, }, } #[remain::sorted] #[derive(Clone, Debug, PartialEq, Eq)] pub enum MergeSkip { FuncInputInputSocketMissing { prop_path: String, missing_socket_name: String, input_name: String, func_unique_id: String, }, FuncInputOutputSocketMissing { prop_path: String, missing_socket_name: String, input_name: String, func_unique_id: String, }, FuncInputPropMissing { prop_path: String, input_name: String, missing_prop_path: String, func_unique_id: String, }, InputSocketMissing { socket_name: String, }, OutputSocketMissing { socket_name: String, }, PropKindMismatch { path: String, other_kind: PropSpecKind, self_kind: PropSpecKind, }, PropMissing(String), } pub const PROP_PATH_SEPARATOR: &str = "\x0B"; const SI_PATH: &str = "root\x0Bsi"; pub(crate) enum InputMismatchTruth<'a, 'b> { PropSpecMap(&'a IndexMap<String, (&'b PropSpec, Option<String>)>), MissingPropSet(&'a HashSet<String>), } impl PropSpec { pub fn builder() -> PropSpecBuilder { PropSpecBuilder::default() } pub fn anonymize(&mut self) { let (unique_id, data) = match self { PropSpec::Array { type_prop, unique_id, data, .. } | PropSpec::Map { type_prop, unique_id, data, .. } => { type_prop.anonymize(); (unique_id, data) } PropSpec::Object { entries, unique_id, data, .. } => { entries.iter_mut().for_each(|f| f.anonymize()); (unique_id, data) } PropSpec::Boolean { unique_id, data, .. } | PropSpec::Float { unique_id, data, .. } | PropSpec::Json { unique_id, data, .. } | PropSpec::Number { unique_id, data, .. } | PropSpec::String { unique_id, data, .. } => (unique_id, data), }; *unique_id = None; if let Some(PropSpecData { inputs: Some(inputs), .. }) = data { inputs.iter_mut().for_each(AttrFuncInputSpec::anonymize) } } pub(crate) fn to_builder_without_children(&self) -> PropSpecBuilder { let mut builder = PropSpec::builder(); builder.name(self.name()).kind(self.kind()); if let Some(PropSpecData { name: _, // handled at the PropSpec level validation_format, default_value, func_unique_id, inputs, widget_kind, widget_options, hidden, doc_link, documentation, ui_optionals, }) = self.data() { if let Some(validation_format) = validation_format { builder.validation_format(validation_format.as_str()); } if let Some(default_value) = default_value { builder.default_value(default_value.to_owned()); } if let Some(func_unique_id) = func_unique_id { builder.func_unique_id(func_unique_id.as_str()); } if let Some(inputs) = inputs { builder.inputs(inputs.to_owned()); } if let Some(widget_kind) = widget_kind { builder.widget_kind(widget_kind.to_owned()); } if let Some(widget_options) = widget_options { builder.widget_options(widget_options.to_owned()); } if let Some(doc_link) = doc_link { builder.doc_link(doc_link.to_owned()); } if let Some(docs) = documentation { builder.documentation(docs.as_str()); } if let &Some(hidden) = hidden { builder.hidden(hidden); } if let Some(ui_optionals) = ui_optionals { builder.ui_optionals(ui_optionals.to_owned()); } } if let PropSpec::Map { map_key_funcs: Some(map_key_funcs), .. } = self { builder.map_key_funcs(map_key_funcs.to_owned()); } builder } pub fn name(&self) -> &str { match self { Self::Array { name, .. } | Self::Boolean { name, .. } | Self::Map { name, .. } | Self::Json { name, .. } | Self::Number { name, .. } | Self::Float { name, .. } | Self::Object { name, .. } | Self::String { name, .. } => name.as_str(), } } pub fn kind(&self) -> PropSpecKind { match self { Self::Array { .. } => PropSpecKind::Array, Self::Boolean { .. } => PropSpecKind::Boolean, Self::Json { .. } => PropSpecKind::Json, Self::Map { .. } => PropSpecKind::Map, Self::Number { .. } => PropSpecKind::Number, Self::Float { .. } => PropSpecKind::Float, Self::Object { .. } => PropSpecKind::Object, Self::String { .. } => PropSpecKind::String, } } pub fn data(&self) -> Option<&PropSpecData> { match self { Self::Array { data, .. } | Self::Boolean { data, .. } | Self::Map { data, .. } | Self::Number { data, .. } | Self::Float { data, .. } | Self::Object { data, .. } | Self::Json { data, .. } | Self::String { data, .. } => data.as_ref(), } } pub fn inputs(&self) -> Option<&Vec<AttrFuncInputSpec>> { self.data().and_then(|data| data.inputs.as_ref()) } pub fn func_unique_id(&self) -> Option<&str> { self.data().and_then(|data| data.func_unique_id.as_deref()) } pub fn direct_children(&self) -> Vec<&PropSpec> { // would be better to just produce an iterator here match self { Self::Json { .. } | Self::Boolean { .. } | Self::Number { .. } | Self::Float { .. } | Self::String { .. } => vec![], Self::Object { entries, .. } => entries.iter().collect(), Self::Map { type_prop, .. } | Self::Array { type_prop, .. } => vec![type_prop.as_ref()], } } pub(crate) fn make_path(parts: &[impl Into<String> + Clone], with_sep: Option<&str>) -> String { parts .iter() .map(|part| part.clone().into()) .collect::<Vec<String>>() .join(with_sep.unwrap_or(PROP_PATH_SEPARATOR)) } /// Process a PropSpec tree into a mapping between the path of the prop and the PropSpec and a list of its direct child paths pub(crate) fn build_prop_spec_index_map( &self, ) -> IndexMap<String, (&PropSpec, Option<String>)> { let mut prop_map = IndexMap::new(); let mut prop_queue = Vec::from([(self, self.name().to_owned(), None)]); while let Some((current_prop, current_path, maybe_parent)) = prop_queue.pop() { let children = current_prop.direct_children(); let children_as_paths: Vec<String> = children .iter() .map(|&prop_spec| Self::make_path(&[current_path.as_str(), prop_spec.name()], None)) .collect(); prop_queue.extend(children.into_iter().enumerate().map(|(idx, spec)| { ( spec, children_as_paths .get(idx) .expect("this index will exist") .to_owned(), Some(current_path.to_owned()), ) })); prop_map.insert(current_path, (current_prop, maybe_parent)); } prop_map } pub(crate) fn get_input_mismatches( current_path: &str, prop_truth: InputMismatchTruth, other_inputs: &[AttrFuncInputSpec], other_func_unique_id: &str, input_sockets: &[String], output_sockets: &[String], ) -> Vec<MergeSkip> { let mut merge_skips = vec![]; for other_input in other_inputs { match other_input { AttrFuncInputSpec::Prop { prop_path, name, .. } => { // If we take a prop from the SI tree as an input we won't // be present in the prop map but will exist when the schema // variant is created if prop_path.starts_with(SI_PATH) { continue; } let prop_is_missing = match prop_truth { InputMismatchTruth::MissingPropSet(missing_prop_set) => { missing_prop_set.contains(prop_path) } InputMismatchTruth::PropSpecMap(prop_spec_map) => { !prop_spec_map.contains_key(prop_path) } }; if prop_is_missing { merge_skips.push(MergeSkip::FuncInputPropMissing { prop_path: current_path.to_string(), input_name: name.to_owned(), missing_prop_path: prop_path.to_owned(), func_unique_id: other_func_unique_id.to_string(), }) } } AttrFuncInputSpec::InputSocket { name, socket_name, .. } => { if !input_sockets.contains(socket_name) { merge_skips.push(MergeSkip::FuncInputInputSocketMissing { prop_path: current_path.to_string(), input_name: name.to_owned(), missing_socket_name: socket_name.to_owned(), func_unique_id: other_func_unique_id.to_string(), }) } } AttrFuncInputSpec::OutputSocket { name, socket_name, .. } => { if !output_sockets.contains(socket_name) { merge_skips.push(MergeSkip::FuncInputOutputSocketMissing { prop_path: current_path.to_string(), input_name: name.to_owned(), missing_socket_name: socket_name.to_owned(), func_unique_id: other_func_unique_id.to_string(), }) } } } } merge_skips } pub fn merge_with( &self, other: &PropSpec, input_sockets: &[String], output_sockets: &[String], ) -> (PropSpec, Vec<MergeSkip>) { let other_map = other.build_prop_spec_index_map(); let mut self_map = self.build_prop_spec_index_map(); self_map.reverse(); // reversing this means we walk it leaves to parents let mut merge_skips: Vec<MergeSkip> = other_map .keys() .filter_map(|path| { if self_map.contains_key(path) { None } else { Some(MergeSkip::PropMissing(path.to_owned())) } }) .collect(); let mut child_map: HashMap<String, Vec<PropSpec>> = HashMap::new(); for (current_path, (current_prop_spec, maybe_parent_path)) in &self_map { let mut current_prop_spec_builder = current_prop_spec.to_builder_without_children(); // Merge in the inputs from the matching prop in other, if it exists if let Some(&(other_prop_spec, _)) = other_map.get(current_path) { let other_kind = other_prop_spec.kind(); let self_kind = current_prop_spec.kind(); if other_kind != self_kind { merge_skips.push(MergeSkip::PropKindMismatch { path: current_path.to_owned(), other_kind, self_kind, }); } else if let (Some(other_func_unique_id), Some(other_inputs)) = (other_prop_spec.func_unique_id(), other_prop_spec.inputs()) { let mismatches = Self::get_input_mismatches( current_path, InputMismatchTruth::PropSpecMap(&self_map), other_inputs.as_slice(), other_func_unique_id, input_sockets, output_sockets, ); if mismatches.is_empty() { current_prop_spec_builder.func_unique_id(other_func_unique_id); current_prop_spec_builder.inputs(other_inputs.to_owned()); } else { merge_skips.extend(mismatches); } } } match current_prop_spec.kind() { PropSpecKind::Map | PropSpecKind::Array => { if let Some(children) = child_map.get(current_path) { if let Some(type_child) = children.first() { current_prop_spec_builder.type_prop(type_child.to_owned()); } } } PropSpecKind::Object => { if let Some(children) = child_map.get(current_path) { for entry in children { current_prop_spec_builder.entry(entry.to_owned()); } } } _ => {} } let current_prop = current_prop_spec_builder .build() .expect("failure here is a programming error"); match maybe_parent_path { Some(parent_path) => { child_map .entry(parent_path.to_owned()) .and_modify(|children| children.push(current_prop.clone())) .or_insert_with(|| vec![current_prop]); } None => return (current_prop, merge_skips), } } // unreachable, but the compiler doesn't know this (self.to_owned(), vec![]) } } #[remain::sorted] #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum PropSpecKind { Array, Boolean, Float, Json, Map, Number, Object, String, } #[derive(Clone, Debug)] pub struct PropSpecBuilder { default_value: Option<serde_json::Value>, doc_link: Option<Url>, documentation: Option<String>, entries: Vec<PropSpec>, func_unique_id: Option<String>, hidden: bool, inputs: Vec<AttrFuncInputSpec>, kind: Option<PropSpecKind>, map_key_funcs: Vec<MapKeyFuncSpec>, pub name: Option<String>, type_prop: Option<PropSpec>, validation_format: Option<String>, ui_optionals: Option<HashMap<String, serde_json::Value>>, widget_kind: Option<PropSpecWidgetKind>, widget_options: Option<serde_json::Value>, unique_id: Option<String>, has_data: bool, } impl Default for PropSpecBuilder { fn default() -> Self { Self { default_value: None, doc_link: None, documentation: None, entries: vec![], func_unique_id: None, hidden: false, inputs: vec![], kind: None, map_key_funcs: vec![], name: None, type_prop: None, ui_optionals: None, validation_format: None, widget_kind: None, widget_options: None, unique_id: None, has_data: true, } } } impl PropSpecBuilder { #[allow(unused_mut)] pub fn default_value(&mut self, value: serde_json::Value) -> &mut Self { self.default_value = Some(value); self.has_data = true; self } #[allow(unused_mut)] pub fn kind(&mut self, value: impl Into<PropSpecKind>) -> &mut Self { self.kind = Some(value.into()); self } pub fn get_kind(&self) -> Option<PropSpecKind> { self.kind } #[allow(unused_mut)] pub fn name(&mut self, value: impl Into<String>) -> &mut Self { self.name = Some(value.into()); self } #[allow(unused_mut)] pub fn type_prop(&mut self, value: impl Into<PropSpec>) -> &mut Self { self.type_prop = Some(value.into()); self } #[allow(unused_mut)] pub fn entry(&mut self, value: impl Into<PropSpec>) -> &mut Self { self.entries.push(value.into()); self } #[allow(unused_mut)] pub fn validation_format(&mut self, value: impl Into<String>) -> &mut Self { self.has_data = true; self.validation_format = Some(value.into()); self } #[allow(unused_mut)] pub fn ui_optionals( &mut self, value: impl Into<HashMap<String, serde_json::Value>>, ) -> &mut Self { self.has_data = true; self.ui_optionals = Some(value.into()); self } #[allow(unused_mut)] pub fn entries(&mut self, value: Vec<impl Into<PropSpec>>) -> &mut Self { self.entries = value.into_iter().map(Into::into).collect(); self } #[allow(unused_mut)] pub fn func_unique_id(&mut self, value: impl Into<String>) -> &mut Self { self.has_data = true; self.func_unique_id = Some(value.into()); self } pub fn input(&mut self, value: impl Into<AttrFuncInputSpec>) -> &mut Self { self.has_data = true; self.inputs.push(value.into()); self } pub fn inputs(&mut self, inputs: Vec<AttrFuncInputSpec>) -> &mut Self { self.has_data = true; self.inputs = inputs; self } pub fn widget_kind(&mut self, value: impl Into<PropSpecWidgetKind>) -> &mut Self { self.widget_kind = Some(value.into()); self } pub fn widget_options(&mut self, value: impl Into<serde_json::Value>) -> &mut Self { self.widget_options = Some(value.into()); self } pub fn hidden(&mut self, value: impl Into<bool>) -> &mut Self { self.hidden = value.into(); self } pub fn doc_link(&mut self, value: impl Into<Url>) -> &mut Self { self.doc_link = Some(value.into()); self } pub fn documentation(&mut self, value: impl Into<String>) -> &mut Self { self.documentation = Some(value.into()); self } pub fn map_key_func(&mut self, value: impl Into<MapKeyFuncSpec>) -> &mut Self { self.has_data = true; self.map_key_funcs.push(value.into()); self } pub fn map_key_funcs(&mut self, value: Vec<MapKeyFuncSpec>) -> &mut Self { self.has_data = true; self.map_key_funcs = value; self } pub fn has_data(&mut self, value: impl Into<bool>) -> &mut Self { self.has_data = value.into(); self } pub fn unique_id(&mut self, value: impl Into<String>) -> &mut Self { self.unique_id = Some(value.into()); self } #[allow(unused_mut)] pub fn try_doc_link<V>(&mut self, value: V) -> Result<&mut Self, V::Error> where V: TryInto<Url>, { let converted: Url = value.try_into()?; Ok(self.doc_link(converted)) } /// Builds a new `Prop`. /// /// # Errors /// /// If a required field has not been initialized. pub fn build(&self) -> Result<PropSpec, SpecError> { let name = match self.name { Some(ref name) => name.clone(), None => { return Err(UninitializedFieldError::from("name").into()); } }; let unique_id = self.unique_id.clone(); let data = if self.has_data { Some(PropSpecData { name: name.clone(), validation_format: self.validation_format.clone(), default_value: self.default_value.clone(), func_unique_id: self.func_unique_id.clone(), inputs: Some(self.inputs.clone()), widget_kind: self.widget_kind, widget_options: self.widget_options.clone(), hidden: Some(self.hidden), doc_link: self.doc_link.clone(), documentation: self.documentation.clone(), ui_optionals: Some(self.ui_optionals.clone().unwrap_or_default()), }) } else { None }; Ok(match self.kind { Some(kind) => match kind { PropSpecKind::String => PropSpec::String { name, unique_id, data, }, PropSpecKind::Json => PropSpec::Json { name, unique_id, data, }, PropSpecKind::Number => PropSpec::Number { name, unique_id, data, }, PropSpecKind::Float => PropSpec::Float { name, unique_id, data, }, PropSpecKind::Boolean => PropSpec::Boolean { name, unique_id, data, }, PropSpecKind::Map => PropSpec::Map { name, unique_id, data, type_prop: match self.type_prop { Some(ref value) => Box::new(value.clone()), None => { return Err(UninitializedFieldError::from("type_prop").into()); } }, map_key_funcs: Some(self.map_key_funcs.to_owned()), }, PropSpecKind::Array => PropSpec::Array { name, unique_id, data, type_prop: match self.type_prop { Some(ref value) => Box::new(value.clone()), None => { return Err(UninitializedFieldError::from("type_prop").into()); } }, }, PropSpecKind::Object => PropSpec::Object { name, unique_id, data, entries: self.entries.clone(), }, }, None => { return Err(UninitializedFieldError::from("kind").into()); } }) } } impl TryFrom<PropSpecBuilder> for PropSpec { type Error = SpecError; fn try_from(value: PropSpecBuilder) -> Result<Self, Self::Error> { value.build() } } impl HasUniqueId for PropSpec { fn unique_id(&self) -> Option<&str> { match self { Self::Array { unique_id, .. } | Self::Boolean { unique_id, .. } | Self::Float { unique_id, .. } | Self::Json { unique_id, .. } | Self::Map { unique_id, .. } | Self::Number { unique_id, .. } | Self::Object { unique_id, .. } | Self::String { unique_id, .. } => unique_id.as_deref(), } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_prop_merge() { let prop_a_path = PropSpec::make_path(&["root", "a"], None); let prop_b_path = PropSpec::make_path(&["root", "b"], None); let prop_c_path = PropSpec::make_path(&["root", "c"], None); let pride_path = PropSpec::make_path(&["root", "objét d'art", "l'orgueil"], None); let prop_b_input_spec = AttrFuncInputSpec::Prop { name: "arg_1".into(), prop_path: prop_a_path.to_owned(), unique_id: None, deleted: false, }; let prop_tree_a = PropSpec::builder() .name("root") .kind(PropSpecKind::Object) .entry( PropSpec::builder() .name("a") .kind(PropSpecKind::String) .func_unique_id("function_2") .input(AttrFuncInputSpec::Prop { name: "arg_1".into(), prop_path: prop_c_path.to_owned(), unique_id: None, deleted: false, }) .build() .expect("able to build prop a"), ) .entry( PropSpec::builder() .name("b") .kind(PropSpecKind::Number) .func_unique_id("function_1") .input(prop_b_input_spec.to_owned()) .build() .expect("able to build prop b"), ) .entry( PropSpec::builder() .name("c") .kind(PropSpecKind::String) .build() .expect("able to build prop a"), ) .entry( PropSpec::builder() .name("objét d'art") .kind(PropSpecKind::Object) .entry( PropSpec::builder() .name("l'orgueil") .kind(PropSpecKind::Number) .build() .expect("before the fall"), ) .entry( PropSpec::builder() .name("un morceau") .kind(PropSpecKind::Object) .entry( PropSpec::builder() .name("le couleur? bleu") .kind(PropSpecKind::Number) .build() .expect("should build"), ) .build() .expect("morceau?"), ) .build() .expect("objét?"), ) .build() .expect("able to build"); let prop_tree_b = PropSpec::builder() .name("root") .kind(PropSpecKind::Object) .entry( PropSpec::builder() .name("a") .kind(PropSpecKind::String) .build() .expect("able to build prop a"), ) .entry( PropSpec::builder() .name("b") .kind(PropSpecKind::Number) .build() .expect("able to build prop b"), ) .entry( PropSpec::builder() .name("objét d'art") .kind(PropSpecKind::Object) .entry( PropSpec::builder() .name("un morceau") .kind(PropSpecKind::Object) .entry( PropSpec::builder() .name("le couleur? bleu") .kind(PropSpecKind::Number) .build() .expect("should build"), ) .build() .expect("morceau?"), ) .build() .expect("objét?"), ) .entry( PropSpec::builder() .name("objét de fart") .kind(PropSpecKind::Object) .entry( PropSpec::builder() .name("un morceau de pet") .kind(PropSpecKind::Object) .entry( PropSpec::builder() .name("un chien") .kind(PropSpecKind::String) .build() .expect("a dog?"), ) .build() .expect("morceau?"), ) .build() .expect("objét?"), ) .build() .expect("able to build"); let (merged_prop_root, merge_skips) = prop_tree_b.merge_with(&prop_tree_a, &[], &[]); // Confirm merge skips are correct assert_eq!( &[ MergeSkip::PropMissing(pride_path), MergeSkip::PropMissing(prop_c_path.to_owned()), MergeSkip::FuncInputPropMissing { prop_path: prop_a_path, input_name: "arg_1".into(), missing_prop_path: prop_c_path, func_unique_id: "function_2".into(), } ], merge_skips.as_slice(), "correct merge skips reported" ); let prop_b_after_merge = merged_prop_root .build_prop_spec_index_map() .get(&prop_b_path) .expect("prop b present in new prop spec") .to_owned() .0; // Confirm attribute function copied over assert_eq!( Some(&vec![prop_b_input_spec]), prop_b_after_merge.inputs(), "attribute function for prop b copied over in merge" ); } }

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