use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NodeSpecJson {
pub id: String,
pub icon: String,
pub name: String,
pub color: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub editor: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub spec: Option<String>,
pub inputs: usize,
pub outputs: usize,
#[serde(rename = "inFilters", skip_serializing_if = "Option::is_none")]
pub in_filters: Option<String>,
pub properties: Vec<PropertyJson>,
#[serde(rename = "customPorts", skip_serializing_if = "Option::is_none")]
pub custom_ports: Option<Vec<Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool: Option<Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PropertyJson {
pub schema: SchemaJson,
#[serde(rename = "formData")]
pub form_data: HashMap<String, Value>,
#[serde(rename = "uiSchema")]
pub ui_schema: HashMap<String, Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SchemaJson {
#[serde(rename = "type")]
pub schema_type: String,
pub title: String,
pub properties: HashMap<String, SchemaPropertyJson>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SchemaPropertyJson {
#[serde(rename = "type")]
pub property_type: String,
pub title: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub subtitle: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub category: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub properties: Option<HashMap<String, Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub items: Option<HashMap<String, Value>>,
#[serde(rename = "csScope", skip_serializing_if = "Option::is_none")]
pub cs_scope: Option<bool>,
#[serde(rename = "jsScope", skip_serializing_if = "Option::is_none")]
pub js_scope: Option<bool>,
#[serde(rename = "customScope", skip_serializing_if = "Option::is_none")]
pub custom_scope: Option<bool>,
#[serde(rename = "messageScope", skip_serializing_if = "Option::is_none")]
pub message_scope: Option<bool>,
#[serde(rename = "messageOnly", skip_serializing_if = "Option::is_none")]
pub message_only: Option<bool>,
#[serde(rename = "aiScope", skip_serializing_if = "Option::is_none")]
pub ai_scope: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub multiple: Option<bool>,
#[serde(rename = "variableType", skip_serializing_if = "Option::is_none")]
pub variable_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<String>,
#[serde(rename = "enum", skip_serializing_if = "Option::is_none")]
pub enum_values: Option<Vec<Value>>,
#[serde(rename = "enumNames", skip_serializing_if = "Option::is_none")]
pub enum_names: Option<Vec<String>>,
}
pub trait NodeSpec {
fn node_id() -> &'static str;
fn node_name() -> &'static str;
fn node_icon() -> &'static str;
fn node_color() -> &'static str;
fn inputs() -> usize {
1
}
fn outputs() -> usize {
1
}
fn tool_spec() -> Option<super::ToolSpec> {
None
}
fn properties() -> Vec<PropertySpec>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PropertyType {
Input,
Output,
Option,
}
#[derive(Debug, Clone)]
pub struct PropertySpec {
pub field_name: String,
pub title: String,
pub var_type: String,
pub scope: String,
pub name: String,
pub default_value: String,
pub property_type: PropertyType,
pub is_variable: bool,
pub description: Option<String>,
pub message_scope: bool,
pub js_scope: bool,
pub custom_scope: bool,
pub ai_scope: bool,
}
pub fn generate_spec_file(plugin_name: &str, version: &str) -> String {
use crate::runtime::registered_node_specs;
let nodes: Vec<NodeSpecJson> = registered_node_specs()
.into_iter()
.map(|spec| spec_to_json(spec))
.collect();
let data = serde_json::json!({
"name": plugin_name,
"version": version,
"nodes": nodes,
});
serde_json::to_string_pretty(&data).unwrap_or_else(|_| "{}".to_string())
}
fn spec_to_json(spec: RegisteredNodeSpec) -> NodeSpecJson {
let mut in_property = PropertyJson {
schema: SchemaJson {
schema_type: "object".to_string(),
title: "Input".to_string(),
properties: HashMap::new(),
},
form_data: HashMap::new(),
ui_schema: HashMap::new(),
};
in_property.ui_schema.insert("ui:order".to_string(), Value::Array(vec![]));
let mut out_property = PropertyJson {
schema: SchemaJson {
schema_type: "object".to_string(),
title: "Output".to_string(),
properties: HashMap::new(),
},
form_data: HashMap::new(),
ui_schema: HashMap::new(),
};
out_property.ui_schema.insert("ui:order".to_string(), Value::Array(vec![]));
let mut opt_property = PropertyJson {
schema: SchemaJson {
schema_type: "object".to_string(),
title: "Options".to_string(),
properties: HashMap::new(),
},
form_data: HashMap::new(),
ui_schema: HashMap::new(),
};
opt_property.ui_schema.insert("ui:order".to_string(), Value::Array(vec![]));
for prop in spec.properties {
let schema_prop = SchemaPropertyJson {
property_type: if prop.is_variable { "object".to_string() } else { prop.var_type.to_lowercase() },
title: prop.title.clone(),
description: prop.description.clone(),
subtitle: None,
category: None,
properties: if prop.is_variable {
Some(HashMap::from([
("scope".to_string(), serde_json::json!({"type": "string"})),
("name".to_string(), serde_json::json!({"type": "string"})),
]))
} else {
None
},
items: None,
cs_scope: None,
js_scope: if prop.js_scope { Some(true) } else { None },
custom_scope: if prop.custom_scope { Some(true) } else { None },
message_scope: if prop.message_scope { Some(true) } else { None },
message_only: None,
ai_scope: if prop.ai_scope { Some(true) } else { None },
multiple: None,
variable_type: if prop.is_variable { Some(capitalize_first(&prop.var_type)) } else { None },
format: None,
enum_values: None,
enum_names: None,
};
let form_value = if prop.is_variable {
serde_json::json!({
"scope": prop.scope,
"name": prop.name,
})
} else {
serde_json::json!(prop.default_value)
};
let ui_value = if prop.is_variable {
serde_json::json!({"ui:field": "variable"})
} else {
Value::Null
};
match prop.property_type {
PropertyType::Input => {
in_property.schema.properties.insert(prop.field_name.clone(), schema_prop);
in_property.form_data.insert(prop.field_name.clone(), form_value);
if !ui_value.is_null() {
in_property.ui_schema.insert(prop.field_name.clone(), ui_value);
}
if let Some(Value::Array(ref mut arr)) = in_property.ui_schema.get_mut("ui:order") {
arr.push(Value::String(prop.field_name));
}
}
PropertyType::Output => {
out_property.schema.properties.insert(prop.field_name.clone(), schema_prop);
out_property.form_data.insert(prop.field_name.clone(), form_value);
if !ui_value.is_null() {
out_property.ui_schema.insert(prop.field_name.clone(), ui_value);
}
if let Some(Value::Array(ref mut arr)) = out_property.ui_schema.get_mut("ui:order") {
arr.push(Value::String(prop.field_name));
}
}
PropertyType::Option => {
opt_property.schema.properties.insert(prop.field_name.clone(), schema_prop);
opt_property.form_data.insert(prop.field_name.clone(), form_value);
if !ui_value.is_null() {
opt_property.ui_schema.insert(prop.field_name.clone(), ui_value);
}
if let Some(Value::Array(ref mut arr)) = opt_property.ui_schema.get_mut("ui:order") {
arr.push(Value::String(prop.field_name));
}
}
}
}
let mut properties = vec![];
if !in_property.schema.properties.is_empty() {
properties.push(in_property);
}
if !out_property.schema.properties.is_empty() {
properties.push(out_property);
}
if !opt_property.schema.properties.is_empty() {
properties.push(opt_property);
}
let tool = spec.tool.map(|t| serde_json::json!({
"name": t.name,
"description": t.description,
}));
NodeSpecJson {
id: spec.id,
icon: spec.icon,
name: spec.name,
color: spec.color,
editor: None,
spec: None,
inputs: spec.inputs,
outputs: spec.outputs,
in_filters: None,
properties,
custom_ports: None,
tool,
}
}
fn capitalize_first(s: &str) -> String {
let mut c = s.chars();
match c.next() {
None => String::new(),
Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
}
}
pub struct RegisteredNodeSpec {
pub id: String,
pub name: String,
pub icon: String,
pub color: String,
pub inputs: usize,
pub outputs: usize,
pub tool: Option<super::ToolSpec>,
pub properties: Vec<PropertySpec>,
}