Skip to main content
Glama
diff.rs17.4 kB
use std::{ collections::{ HashMap, HashSet, }, fmt, fmt::{ Display, Formatter, }, }; use json_patch::{ Patch, PatchOperation, jsonptr::{ Assign, Pointer, }, }; use serde_json::Value; enum PatchTarget { Socket(String), SocketContents(String), Prop(String), PropContents((String, String)), Func(String), FuncBinding(String), Variant, VariantContent(String), Schema, SchemaContent(String), } impl Display for PatchTarget { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { PatchTarget::Socket(name) => { let split_name = name.split("-").collect::<Vec<_>>(); let (Some(kind), Some(name)) = (split_name.first(), split_name.get(1)) else { return Err(fmt::Error); }; f.write_str(format!("{kind} socket {name}").as_str()) } PatchTarget::SocketContents(name) => { let split_name = name.split("-").collect::<Vec<_>>(); let (Some(kind), Some(name)) = (split_name.first(), split_name.get(1)) else { return Err(fmt::Error); }; f.write_str(format!("contents of {kind} socket {name}").as_str()) } PatchTarget::Prop(name) => f.write_str(format!("prop {name}").as_str()), PatchTarget::PropContents((name, target)) => { f.write_str(format!("contents of prop {name} at {target}").as_str()) } PatchTarget::Func(name) => f.write_str(format!("function {name}").as_str()), PatchTarget::FuncBinding(kind) => f.write_str(format!("{kind} binding").as_str()), PatchTarget::Variant => f.write_str("variant"), PatchTarget::VariantContent(path) => { f.write_str(format!("variant content at {path}").as_str()) } PatchTarget::Schema => f.write_str("schema"), PatchTarget::SchemaContent(path) => { f.write_str(format!("schema content at {path}").as_str()) } }?; Ok(()) } } pub fn patch_list_to_changelog(patch: Patch) -> Vec<String> { let mut logs = vec![]; for operation in patch.iter() { let Some(target) = determine_patch_target(operation) else { continue; }; match operation { PatchOperation::Add(op) => { // Skip null values in diff output if op.value.is_null() { continue; } logs.push(format!( "#### Added {target}:\n```\n{}\n```\n", serde_json::to_string_pretty(&op.value).expect("unable to parse json") )); } PatchOperation::Remove(_) => logs.push(format!("Removed {target}")), PatchOperation::Replace(op) => { // Skip null values in diff output if op.value.is_null() { continue; } logs.push(format!( "#### Replaced value within {target}:\n```\n{}\n```\n", serde_json::to_string_pretty(&op.value).expect("unable to parse json") )); } change @ (PatchOperation::Move(_) | PatchOperation::Copy(_) | PatchOperation::Test(_)) => println!("Unhandled Operation: \n{change}"), } } logs.sort(); logs } pub struct ModificationSets { pub added: HashSet<String>, pub modified: HashSet<String>, pub removed: HashSet<String>, category: &'static str, } impl ModificationSets { fn new(category: &'static str) -> ModificationSets { ModificationSets { added: Default::default(), modified: Default::default(), removed: Default::default(), category, } } pub fn is_empty(&self) -> bool { self.added.len() == 0 && self.removed.len() == 0 && self.modified.len() == 0 } pub fn into_text_summary(self) -> Option<String> { if self.is_empty() { return None; } let mut message = self.category.to_string(); if !self.added.is_empty() { message = format!("{message} ➕{}", self.added.len()); } if !self.removed.is_empty() { message = format!("{message} ➖{}", self.removed.len()); } if !self.modified.is_empty() { message = format!("{message} 🔀{}", self.modified.len()); } Some(message) } } pub fn patch_list_to_summary( asset_name: impl AsRef<str>, patch: Patch, ) -> (Option<String>, ModificationSets) { let mut sockets_set = ModificationSets::new("sockets"); let mut props_set = ModificationSets::new("props"); let mut asset_contents_set = ModificationSets::new("other schema contents"); let funcs_set = ModificationSets::new("funcs"); for operation in patch.iter() { let Some(target) = determine_patch_target(operation) else { continue; }; match operation { PatchOperation::Add(_) => match target { PatchTarget::Socket(name) => { sockets_set.added.insert(name); } PatchTarget::SocketContents(name) => { sockets_set.modified.insert(name); } PatchTarget::Prop(name) => { props_set.added.insert(name); } PatchTarget::PropContents((name, _)) => { props_set.modified.insert(name); } PatchTarget::Func(_) => {} PatchTarget::FuncBinding(func_kind) => { asset_contents_set.added.insert(func_kind); } PatchTarget::Variant => {} PatchTarget::VariantContent(path) => { asset_contents_set.added.insert(path); } PatchTarget::Schema => {} PatchTarget::SchemaContent(path) => { asset_contents_set.added.insert(path); } }, PatchOperation::Remove(_) => match target { PatchTarget::Socket(name) => { sockets_set.removed.insert(name); } PatchTarget::SocketContents(name) => { sockets_set.modified.insert(name); } PatchTarget::Prop(name) => { props_set.removed.insert(name); } PatchTarget::PropContents((name, _)) => { props_set.modified.insert(name); } PatchTarget::Func(_) => {} PatchTarget::FuncBinding(func_kind) => { asset_contents_set.removed.insert(func_kind); } PatchTarget::Variant => {} PatchTarget::VariantContent(path) => { asset_contents_set.removed.insert(path); } PatchTarget::Schema => {} PatchTarget::SchemaContent(path) => { asset_contents_set.removed.insert(path); } }, PatchOperation::Replace(_) => match target { PatchTarget::Socket(name) => { sockets_set.modified.insert(name); } PatchTarget::SocketContents(name) => { sockets_set.modified.insert(name); } PatchTarget::Prop(name) => { props_set.modified.insert(name); } PatchTarget::PropContents((name, _)) => { props_set.modified.insert(name); } PatchTarget::Func(_) => {} PatchTarget::FuncBinding(func_kind) => { asset_contents_set.modified.insert(func_kind); } PatchTarget::Variant => {} PatchTarget::VariantContent(path) => { asset_contents_set.modified.insert(path); } PatchTarget::Schema => {} PatchTarget::SchemaContent(path) => { asset_contents_set.modified.insert(path); } }, PatchOperation::Move(_) | PatchOperation::Copy(_) | PatchOperation::Test(_) => {} } } let prop_summary = props_set.into_text_summary(); let sockets_summary = sockets_set.into_text_summary(); let variant_contents_summary = asset_contents_set.into_text_summary(); let summaries = vec![prop_summary, sockets_summary, variant_contents_summary] .into_iter() .flatten() .collect::<Vec<_>>(); if summaries.is_empty() { return (None, funcs_set); } let summaries_text = summaries.join(", "); let message = format!("[{}]: {summaries_text}", asset_name.as_ref()); (Some(message), funcs_set) } fn determine_patch_target(operation: &PatchOperation) -> Option<PatchTarget> { let path = operation.path(); let target = if path.starts_with(Pointer::from_static("/funcs")) { let Some(name) = path.get(1) else { // println!("Change directly to funcs container"); return None; }; PatchTarget::Func(name.to_string()) } else if path.starts_with(Pointer::from_static("/schemas/0/variants/0/sockets")) { let Some(name) = path.get(5) else { // println!("Change directly to sockets container"); return None; }; // We're in a field deeper than the socket itself if path.get(6).is_some() { PatchTarget::SocketContents(name.to_string()) } else { PatchTarget::Socket(name.to_string()) } } else if path.starts_with(Pointer::from_static("/schemas/0/variants/0")) { if let Some(prop_root) = path.get(4) { if ["domain", "secrets", "resourceValue"].contains(&prop_root.to_string().as_str()) { let mut prop_name_tokens = vec![prop_root.to_string()]; let mut found_prop_root = false; let mut last_one_was_entries = false; let mut prop_contents = vec![]; for token in path.tokens().map(|t| t.to_string()) { if !found_prop_root { if token == prop_root.to_string() { found_prop_root = true } } else if !last_one_was_entries { if token == "entries" && prop_contents.is_empty() { last_one_was_entries = true; } else { prop_contents.push(token); } } else { last_one_was_entries = false; prop_name_tokens.push(token); } } let prop_name = format!("/root/{}", prop_name_tokens.join("/")); if !prop_contents.is_empty() { PatchTarget::PropContents((prop_name, prop_contents.join("/"))) } else { PatchTarget::Prop(prop_name) } } else if [ "actionFuncs", "authFuncs", "leafFunctions", "siPropFuncs", "managementFuncs", "rootPropFuncs", ] .contains(&prop_root.to_string().as_str()) { PatchTarget::FuncBinding(prop_root.to_string()) } else { PatchTarget::VariantContent(path.to_string()) } } else { PatchTarget::Variant } } else if path.starts_with(Pointer::from_static("/schemas/0")) { if let Some(prop_root) = path.get(2) { PatchTarget::SchemaContent(prop_root.to_string()) } else { PatchTarget::Schema } } else { dbg!("Unhandled patch operation", operation); return None; }; Some(target) } pub fn rewrite_spec_for_diff(spec: Value) -> Value { let mut spec = spec.clone(); let module_name = spec .pointer("/name") .expect("could not get module name") .as_str() .expect("could not get module name as str") .to_owned(); let variant = spec .pointer_mut("/schemas/0/variants/0") .expect("Could not find variant"); // Make sockets a record let mut new_sockets = HashMap::new(); { let sockets = variant.get_mut("sockets").expect("could not get sockets"); for socket in sockets.as_array().expect("could not get sockets as array") { let name = socket .get("name") .expect("could not get name") .as_str() .expect("kind should be a string"); let kind = socket .pointer("/data/kind") .expect("Could not find kind") .as_str() .expect("kind should be a string"); let key = format!("{kind}-{name}"); new_sockets.insert(key, socket.clone()); } } variant .assign( Pointer::from_static("/sockets"), serde_json::to_value(new_sockets).expect("sockets could not parse as json"), ) .expect("could not assign value"); // Rewrite the props { fn rewrite_prop(prop: &Value) -> (String, Value) { let name = prop .get("name") .expect("could not get name") .as_str() .expect("kind should be a string"); let kind = prop .get("kind") .expect("Could not find kind") .as_str() .expect("kind should be a string"); let mut prop = prop.clone(); if !["array", "map", "object"].contains(&kind) { return (name.to_string(), prop); }; if kind != "object" { let (_, type_prop) = rewrite_prop(prop.get("typeProp").expect("could not get typeProp")); prop.assign( Pointer::from_static("/typeProp"), serde_json::to_value(type_prop).expect("couldn't make new entries into json"), ) .expect("could not assign value"); return (name.to_string(), prop); } let mut new_entries = HashMap::new(); for entry in prop .get("entries") .unwrap_or_else(|| panic!("couldn't get entries for prop: {name}")) .as_array() .unwrap_or_else(|| panic!("entries field of {name} is not an array")) { let (entry_name, entry_prop) = rewrite_prop(entry); new_entries.insert(entry_name, entry_prop); } prop.assign( Pointer::from_static("/entries"), serde_json::to_value(new_entries).expect("couldn't make new entries into json"), ) .expect("could not assign value"); (name.to_string(), prop) } let (_, rewritten_domain) = rewrite_prop(variant.get("domain").expect("could not get domain")); let (_, rewritten_secrets) = rewrite_prop(variant.get("secrets").expect("could not get secrets")); let (_, rewritten_resource_value) = rewrite_prop( variant .get("resourceValue") .expect("could not get resourceValue"), ); variant .assign(Pointer::from_static("/domain"), rewritten_domain) .expect("could not assign value"); variant .assign(Pointer::from_static("/secrets"), rewritten_secrets) .expect("could not assign value"); variant .assign( Pointer::from_static("/resourceValue"), rewritten_resource_value, ) .expect("could not assign value"); } // Rewrite Funcs { let mut new_funcs = HashMap::new(); let funcs = spec .get("funcs") .expect("couldn't get funcs") .as_array() .expect("funcs field is not an array"); for func in funcs { let func_name = func .get("name") .expect("could not get name") .as_str() .expect("kind should be a string"); // Ignore asset func if func_name == module_name { continue; } new_funcs.insert(func_name, func.clone()); } spec.assign( Pointer::from_static("/funcs"), serde_json::to_value(new_funcs).expect("could not parse func to value"), ) .expect("could not assign value"); } spec }

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