harn_vm/composition/
harn_api.rs1use std::collections::BTreeSet;
2
3use serde_json::Value;
4
5use super::manifest::{BindingManifest, BindingPolicyDisposition};
6
7pub fn composition_harn_api(manifest: &BindingManifest) -> String {
8 let mut out = String::from(
9 "// Harn Code Mode API. Snippets call these bindings; the executor supplies __composition_call.\n\
10 // Use map_bounded(items, { item -> ... }, {concurrency: N}) for settled fan-out.\n",
11 );
12 out.push_str("type JsonValue = unknown\n\n");
13 for binding in &manifest.bindings {
14 if binding.policy.disposition != BindingPolicyDisposition::Allowed {
15 continue;
16 }
17 let args_type_name = unique_type_name(&binding.binding, "Args");
18 let args_type = json_schema_to_harn_type(&binding.input_schema);
19 let result_type = binding
20 .output_schema
21 .as_ref()
22 .map(json_schema_to_harn_type)
23 .unwrap_or_else(|| "JsonValue".to_string());
24 if let Some(server) = binding
25 .metadata
26 .get("_mcp_server")
27 .or_else(|| binding.metadata.get("mcp_server"))
28 .and_then(Value::as_str)
29 {
30 let tool = binding
31 .metadata
32 .get("_mcp_tool_name")
33 .and_then(Value::as_str)
34 .unwrap_or(&binding.name);
35 out.push_str(&format!("// MCP {server}/{tool} -> {}\n", binding.binding));
36 } else if binding.source != "harn" {
37 out.push_str(&format!("// {} -> {}\n", binding.source, binding.binding));
38 }
39 out.push_str(&format!("type {args_type_name} = {args_type}\n"));
40 out.push_str(&format!(
41 "fn {}(args: {args_type_name}) -> {result_type} {{\n return __composition_call({}, args)\n}}\n\n",
42 binding.binding,
43 harn_string_literal(&binding.name),
44 ));
45 }
46 out
47}
48
49fn unique_type_name(binding: &str, suffix: &str) -> String {
50 let mut out = String::new();
51 let mut upper_next = true;
52 for ch in binding.chars() {
53 if ch == '_' || ch == '-' || ch == '.' || ch == '/' {
54 upper_next = true;
55 continue;
56 }
57 if ch.is_ascii_alphanumeric() {
58 if out.is_empty() && ch.is_ascii_digit() {
59 out.push_str("Tool");
60 }
61 if upper_next {
62 out.push(ch.to_ascii_uppercase());
63 upper_next = false;
64 } else {
65 out.push(ch);
66 }
67 }
68 }
69 if out.is_empty() {
70 out.push_str("Tool");
71 }
72 out.push_str(suffix);
73 out
74}
75
76fn json_schema_to_harn_type(schema: &Value) -> String {
77 if let Some(shorthand) = schema.as_str() {
78 return match shorthand {
79 "string" => "string".to_string(),
80 "int" | "integer" => "int".to_string(),
81 "float" | "number" => "float".to_string(),
82 "bool" | "boolean" => "bool".to_string(),
83 "list" | "array" => "list<JsonValue>".to_string(),
84 "dict" | "object" => "dict<string, JsonValue>".to_string(),
85 _ => "JsonValue".to_string(),
86 };
87 }
88 let schema_type = schema.get("type").and_then(Value::as_str);
89 match schema_type {
90 Some("string") => enum_string_literals(schema).unwrap_or_else(|| "string".to_string()),
91 Some("integer") => "int".to_string(),
92 Some("number") => "float".to_string(),
93 Some("boolean") => "bool".to_string(),
94 Some("array") => {
95 let item_type = schema
96 .get("items")
97 .map(json_schema_to_harn_type)
98 .unwrap_or_else(|| "JsonValue".to_string());
99 format!("list<{item_type}>")
100 }
101 Some("object") | None if schema.get("properties").is_some() => object_shape(schema),
102 None if schema.as_object().is_some() => object_shape_from_shorthand(schema),
103 Some("object") => "dict<string, JsonValue>".to_string(),
104 _ => "JsonValue".to_string(),
105 }
106}
107
108fn object_shape(schema: &Value) -> String {
109 let required = schema
110 .get("required")
111 .and_then(Value::as_array)
112 .map(|items| {
113 items
114 .iter()
115 .filter_map(Value::as_str)
116 .collect::<BTreeSet<_>>()
117 })
118 .unwrap_or_default();
119 let Some(properties) = schema.get("properties").and_then(Value::as_object) else {
120 return "dict<string, JsonValue>".to_string();
121 };
122 let mut fields = Vec::new();
123 for (name, value) in properties {
124 if !is_harn_identifier(name) {
125 return "dict<string, JsonValue>".to_string();
126 }
127 let optional = if required.contains(name.as_str()) {
128 ""
129 } else {
130 "?"
131 };
132 fields.push(format!(
133 "{name}{optional}: {}",
134 json_schema_to_harn_type(value)
135 ));
136 }
137 if fields.is_empty() {
138 "dict<string, JsonValue>".to_string()
139 } else {
140 format!("{{{}}}", fields.join(", "))
141 }
142}
143
144fn object_shape_from_shorthand(schema: &Value) -> String {
145 let Some(properties) = schema.as_object() else {
146 return "dict<string, JsonValue>".to_string();
147 };
148 let mut fields = Vec::new();
149 for (name, value) in properties {
150 if !is_harn_identifier(name) {
151 return "dict<string, JsonValue>".to_string();
152 }
153 let optional = if value
154 .get("required")
155 .and_then(Value::as_bool)
156 .unwrap_or(true)
157 {
158 ""
159 } else {
160 "?"
161 };
162 fields.push(format!(
163 "{name}{optional}: {}",
164 json_schema_to_harn_type(value)
165 ));
166 }
167 if fields.is_empty() {
168 "dict<string, JsonValue>".to_string()
169 } else {
170 format!("{{{}}}", fields.join(", "))
171 }
172}
173
174fn enum_string_literals(schema: &Value) -> Option<String> {
175 let variants = schema.get("enum")?.as_array()?;
176 let strings = variants
177 .iter()
178 .map(|value| value.as_str().map(harn_string_literal))
179 .collect::<Option<Vec<_>>>()?;
180 (!strings.is_empty()).then(|| strings.join(" | "))
181}
182
183fn is_harn_identifier(name: &str) -> bool {
184 let mut chars = name.chars();
185 let Some(first) = chars.next() else {
186 return false;
187 };
188 if first != '_' && !first.is_ascii_alphabetic() {
189 return false;
190 }
191 chars.all(|ch| ch == '_' || ch.is_ascii_alphanumeric()) && !HARN_KEYWORDS.contains(&name)
192}
193
194fn harn_string_literal(value: &str) -> String {
195 serde_json::to_string(value).unwrap_or_else(|_| "\"\"".to_string())
196}
197
198const HARN_KEYWORDS: &[&str] = &[
199 "agent",
200 "as",
201 "await",
202 "break",
203 "catch",
204 "continue",
205 "defer",
206 "else",
207 "enum",
208 "false",
209 "fn",
210 "for",
211 "if",
212 "impl",
213 "import",
214 "in",
215 "interface",
216 "let",
217 "match",
218 "nil",
219 "pipeline",
220 "pub",
221 "return",
222 "skill",
223 "spawn",
224 "struct",
225 "throw",
226 "true",
227 "try",
228 "type",
229 "var",
230 "while",
231];