1use crate::tools::{ToolContext, ToolRegistry};
7use anyhow::{bail, Result};
8use serde::{Deserialize, Serialize};
9use std::collections::HashSet;
10use std::sync::Arc;
11
12pub const PROGRAM_TRACE_SCHEMA: &str = "a3s.program_trace.v1";
13
14#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
15pub struct Program {
16 pub name: String,
17 pub description: String,
18 pub steps: Vec<ProgramStep>,
19}
20
21impl Program {
22 pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
23 Self {
24 name: name.into(),
25 description: description.into(),
26 steps: Vec::new(),
27 }
28 }
29
30 pub fn with_step(mut self, step: ProgramStep) -> Self {
31 self.steps.push(step);
32 self
33 }
34}
35
36#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
37pub struct ProgramTemplate {
38 pub name: String,
39 pub description: String,
40 pub parameters: Vec<ProgramParameter>,
41 pub steps: Vec<ProgramStepTemplate>,
42}
43
44impl ProgramTemplate {
45 pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
46 Self {
47 name: name.into(),
48 description: description.into(),
49 parameters: Vec::new(),
50 steps: Vec::new(),
51 }
52 }
53
54 pub fn with_parameter(mut self, parameter: ProgramParameter) -> Self {
55 self.parameters.push(parameter);
56 self
57 }
58
59 pub fn with_step(mut self, step: ProgramStepTemplate) -> Self {
60 self.steps.push(step);
61 self
62 }
63
64 pub fn validate(&self) -> ProgramTemplateValidation {
65 ProgramTemplateValidation::validate(self)
66 }
67
68 pub fn ensure_valid(&self) -> Result<()> {
69 let validation = self.validate();
70 if validation.is_valid() {
71 Ok(())
72 } else {
73 bail!("{}", validation.summary());
74 }
75 }
76
77 pub fn instantiate(&self, inputs: &serde_json::Value) -> Result<Program> {
78 self.ensure_valid()?;
79 let input_object = inputs.as_object();
80 let mut bindings = serde_json::Map::new();
81
82 for parameter in &self.parameters {
83 let value = input_object.and_then(|object| object.get(¶meter.name));
84 match (value, ¶meter.default, parameter.required) {
85 (Some(value), _, _) => {
86 bindings.insert(parameter.name.clone(), value.clone());
87 }
88 (None, Some(default), _) => {
89 bindings.insert(parameter.name.clone(), default.clone());
90 }
91 (None, None, true) => {
92 bail!("Missing required program parameter: {}", parameter.name);
93 }
94 (None, None, false) => {}
95 }
96 }
97
98 let bindings = serde_json::Value::Object(bindings);
99 let mut program = Program::new(self.name.clone(), self.description.clone());
100 for step in &self.steps {
101 program = program.with_step(ProgramStep {
102 tool_name: step.tool_name.clone(),
103 args: render_template_value(&step.args, &bindings),
104 label: step.label.clone(),
105 });
106 }
107 Ok(program)
108 }
109}
110
111#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
112pub struct ProgramTemplateValidation {
113 pub template_name: String,
114 pub issues: Vec<ProgramTemplateIssue>,
115}
116
117impl ProgramTemplateValidation {
118 pub fn validate(template: &ProgramTemplate) -> Self {
119 let mut issues = Vec::new();
120 validate_program_template(template, &mut issues);
121 Self {
122 template_name: template.name.clone(),
123 issues,
124 }
125 }
126
127 pub fn is_valid(&self) -> bool {
128 self.issues.is_empty()
129 }
130
131 pub fn summary(&self) -> String {
132 if self.is_valid() {
133 return format!("Program template '{}' is valid", self.template_name);
134 }
135
136 let issues = self
137 .issues
138 .iter()
139 .map(|issue| format!("{}: {}", issue.path, issue.message))
140 .collect::<Vec<_>>()
141 .join("; ");
142 format!(
143 "Program template '{}' is invalid: {}",
144 self.template_name, issues
145 )
146 }
147}
148
149#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
150pub struct ProgramTemplateIssue {
151 pub code: String,
152 pub path: String,
153 pub message: String,
154}
155
156impl ProgramTemplateIssue {
157 fn new(code: impl Into<String>, path: impl Into<String>, message: impl Into<String>) -> Self {
158 Self {
159 code: code.into(),
160 path: path.into(),
161 message: message.into(),
162 }
163 }
164}
165
166fn validate_program_template(template: &ProgramTemplate, issues: &mut Vec<ProgramTemplateIssue>) {
167 if template.name.trim().is_empty() {
168 issues.push(ProgramTemplateIssue::new(
169 "empty_name",
170 "name",
171 "program template name is required",
172 ));
173 } else if !is_program_identifier(&template.name) {
174 issues.push(ProgramTemplateIssue::new(
175 "invalid_name",
176 "name",
177 "program template name must contain only ASCII letters, numbers, '_' or '-'",
178 ));
179 }
180
181 if template.description.trim().is_empty() {
182 issues.push(ProgramTemplateIssue::new(
183 "empty_description",
184 "description",
185 "program template description is required",
186 ));
187 }
188
189 let mut parameter_names = HashSet::new();
190 for (index, parameter) in template.parameters.iter().enumerate() {
191 let path = format!("parameters[{index}].name");
192 if parameter.name.trim().is_empty() {
193 issues.push(ProgramTemplateIssue::new(
194 "empty_parameter_name",
195 path,
196 "program parameter name is required",
197 ));
198 } else if !is_program_identifier(¶meter.name) {
199 issues.push(ProgramTemplateIssue::new(
200 "invalid_parameter_name",
201 path,
202 "program parameter name must contain only ASCII letters, numbers, '_' or '-'",
203 ));
204 } else if !parameter_names.insert(parameter.name.clone()) {
205 issues.push(ProgramTemplateIssue::new(
206 "duplicate_parameter",
207 path,
208 format!("duplicate program parameter '{}'", parameter.name),
209 ));
210 }
211
212 if parameter.required && parameter.default.is_some() {
213 issues.push(ProgramTemplateIssue::new(
214 "required_parameter_with_default",
215 format!("parameters[{index}].default"),
216 "required program parameters must not define defaults",
217 ));
218 }
219 }
220
221 if template.steps.is_empty() {
222 issues.push(ProgramTemplateIssue::new(
223 "empty_steps",
224 "steps",
225 "program template must contain at least one step",
226 ));
227 }
228
229 let mut labels = HashSet::new();
230 for (index, step) in template.steps.iter().enumerate() {
231 if step.tool_name.trim().is_empty() {
232 issues.push(ProgramTemplateIssue::new(
233 "empty_tool_name",
234 format!("steps[{index}].tool_name"),
235 "program step tool_name is required",
236 ));
237 }
238
239 if let Some(label) = &step.label {
240 if label.trim().is_empty() {
241 issues.push(ProgramTemplateIssue::new(
242 "empty_step_label",
243 format!("steps[{index}].label"),
244 "program step label must not be empty",
245 ));
246 } else if !labels.insert(label.clone()) {
247 issues.push(ProgramTemplateIssue::new(
248 "duplicate_step_label",
249 format!("steps[{index}].label"),
250 format!("duplicate program step label '{label}'"),
251 ));
252 }
253 }
254
255 validate_template_value(
256 &step.args,
257 &format!("steps[{index}].args"),
258 ¶meter_names,
259 issues,
260 );
261 }
262}
263
264fn validate_template_value(
265 value: &serde_json::Value,
266 path: &str,
267 parameter_names: &HashSet<String>,
268 issues: &mut Vec<ProgramTemplateIssue>,
269) {
270 match value {
271 serde_json::Value::String(text) => {
272 for placeholder in template_placeholders(text) {
273 match placeholder {
274 Ok(name) if !is_program_identifier(&name) => {
275 issues.push(ProgramTemplateIssue::new(
276 "invalid_placeholder",
277 path,
278 format!("invalid placeholder '{{{{{name}}}}}'"),
279 ));
280 }
281 Ok(name) if !parameter_names.contains(&name) => {
282 issues.push(ProgramTemplateIssue::new(
283 "unknown_placeholder",
284 path,
285 format!("unknown program parameter placeholder '{{{{{name}}}}}'"),
286 ));
287 }
288 Ok(_) => {}
289 Err(message) => {
290 issues.push(ProgramTemplateIssue::new(
291 "malformed_placeholder",
292 path,
293 message,
294 ));
295 }
296 }
297 }
298 }
299 serde_json::Value::Array(items) => {
300 for (index, item) in items.iter().enumerate() {
301 validate_template_value(item, &format!("{path}[{index}]"), parameter_names, issues);
302 }
303 }
304 serde_json::Value::Object(object) => {
305 for (key, value) in object {
306 validate_template_value(value, &format!("{path}.{key}"), parameter_names, issues);
307 }
308 }
309 _ => {}
310 }
311}
312
313fn is_program_identifier(value: &str) -> bool {
314 !value.is_empty()
315 && value
316 .chars()
317 .all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '-')
318}
319
320fn template_placeholders(text: &str) -> Vec<std::result::Result<String, String>> {
321 let mut placeholders = Vec::new();
322 let mut rest = text;
323
324 while let Some(start) = rest.find("{{") {
325 let after_start = &rest[start + 2..];
326 let Some(end) = after_start.find("}}") else {
327 placeholders.push(Err(
328 "malformed placeholder: missing closing '}}'".to_string()
329 ));
330 return placeholders;
331 };
332 let name = after_start[..end].trim();
333 if name.is_empty() {
334 placeholders.push(Err("malformed placeholder: empty name".to_string()));
335 } else {
336 placeholders.push(Ok(name.to_string()));
337 }
338 rest = &after_start[end + 2..];
339 }
340
341 if rest.contains("}}") {
342 placeholders.push(Err(
343 "malformed placeholder: missing opening '{{'".to_string()
344 ));
345 }
346
347 placeholders
348}
349
350#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
351pub struct ProgramParameter {
352 pub name: String,
353 pub description: String,
354 pub required: bool,
355 #[serde(default, skip_serializing_if = "Option::is_none")]
356 pub default: Option<serde_json::Value>,
357}
358
359impl ProgramParameter {
360 pub fn required(name: impl Into<String>, description: impl Into<String>) -> Self {
361 Self {
362 name: name.into(),
363 description: description.into(),
364 required: true,
365 default: None,
366 }
367 }
368
369 pub fn optional(
370 name: impl Into<String>,
371 description: impl Into<String>,
372 default: serde_json::Value,
373 ) -> Self {
374 Self {
375 name: name.into(),
376 description: description.into(),
377 required: false,
378 default: Some(default),
379 }
380 }
381}
382
383#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
384pub struct ProgramStep {
385 pub tool_name: String,
386 #[serde(default)]
387 pub args: serde_json::Value,
388 #[serde(default, skip_serializing_if = "Option::is_none")]
389 pub label: Option<String>,
390}
391
392impl ProgramStep {
393 pub fn new(tool_name: impl Into<String>, args: serde_json::Value) -> Self {
394 Self {
395 tool_name: tool_name.into(),
396 args,
397 label: None,
398 }
399 }
400
401 pub fn with_label(mut self, label: impl Into<String>) -> Self {
402 self.label = Some(label.into());
403 self
404 }
405}
406
407#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
408pub struct ProgramStepTemplate {
409 pub tool_name: String,
410 #[serde(default)]
411 pub args: serde_json::Value,
412 #[serde(default, skip_serializing_if = "Option::is_none")]
413 pub label: Option<String>,
414}
415
416impl ProgramStepTemplate {
417 pub fn new(tool_name: impl Into<String>, args: serde_json::Value) -> Self {
418 Self {
419 tool_name: tool_name.into(),
420 args,
421 label: None,
422 }
423 }
424
425 pub fn with_label(mut self, label: impl Into<String>) -> Self {
426 self.label = Some(label.into());
427 self
428 }
429}
430
431#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
432pub struct ProgramResult {
433 pub program_name: String,
434 pub success: bool,
435 pub summary: String,
436 pub steps: Vec<ProgramStepResult>,
437}
438
439#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
440pub struct ProgramStepResult {
441 pub tool_name: String,
442 #[serde(default, skip_serializing_if = "Option::is_none")]
443 pub label: Option<String>,
444 pub success: bool,
445 pub output: String,
446 #[serde(default, skip_serializing_if = "Option::is_none")]
447 pub metadata: Option<serde_json::Value>,
448}
449
450#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
451pub struct ProgramTrace {
452 pub schema: String,
453 #[serde(rename = "type")]
454 pub trace_type: String,
455 pub program_name: String,
456 pub success: bool,
457 pub summary: String,
458 pub step_count: usize,
459 pub failed_steps: usize,
460 pub steps: Vec<ProgramTraceStep>,
461}
462
463impl ProgramTrace {
464 pub fn from_result(result: &ProgramResult, steps: Vec<ProgramTraceStep>) -> Self {
465 Self {
466 schema: PROGRAM_TRACE_SCHEMA.to_string(),
467 trace_type: "program_execution".to_string(),
468 program_name: result.program_name.clone(),
469 success: result.success,
470 summary: result.summary.clone(),
471 step_count: steps.len(),
472 failed_steps: steps.iter().filter(|step| !step.success).count(),
473 steps,
474 }
475 }
476
477 pub fn to_value(&self) -> serde_json::Value {
478 serde_json::to_value(self).unwrap_or_else(|_| {
479 serde_json::json!({
480 "schema": PROGRAM_TRACE_SCHEMA,
481 "type": "program_execution",
482 "program_name": self.program_name,
483 "success": self.success,
484 "summary": self.summary,
485 "step_count": self.step_count,
486 "failed_steps": self.failed_steps,
487 "steps": [],
488 })
489 })
490 }
491}
492
493#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
494pub struct ProgramTraceStep {
495 pub index: usize,
496 pub label: String,
497 pub tool_name: String,
498 pub success: bool,
499 pub output_bytes: usize,
500 pub compacted: bool,
501 #[serde(default)]
502 pub artifact: Option<ProgramTraceArtifact>,
503 #[serde(default)]
504 pub metadata: Option<serde_json::Value>,
505}
506
507impl ProgramTraceStep {
508 pub fn from_result(
509 index: usize,
510 step: &ProgramStepResult,
511 compacted: bool,
512 artifact: Option<ProgramTraceArtifact>,
513 ) -> Self {
514 Self {
515 index,
516 label: step.label.clone().unwrap_or_else(|| step.tool_name.clone()),
517 tool_name: step.tool_name.clone(),
518 success: step.success,
519 output_bytes: step.output.len(),
520 compacted,
521 artifact,
522 metadata: step.metadata.clone(),
523 }
524 }
525}
526
527#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
528pub struct ProgramTraceArtifact {
529 pub artifact_id: String,
530 pub artifact_uri: String,
531 pub original_bytes: usize,
532 pub shown_bytes: usize,
533}
534
535#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
536pub struct ProgramVerificationHint {
537 pub kind: String,
538 pub message: String,
539 #[serde(default)]
540 pub required: bool,
541 #[serde(default, skip_serializing_if = "Vec::is_empty")]
542 pub suggested_tools: Vec<String>,
543 #[serde(default, skip_serializing_if = "Vec::is_empty")]
544 pub evidence_uris: Vec<String>,
545}
546
547impl ProgramVerificationHint {
548 pub fn new(kind: impl Into<String>, message: impl Into<String>) -> Self {
549 Self {
550 kind: kind.into(),
551 message: message.into(),
552 required: false,
553 suggested_tools: Vec::new(),
554 evidence_uris: Vec::new(),
555 }
556 }
557
558 pub fn required(mut self) -> Self {
559 self.required = true;
560 self
561 }
562
563 pub fn with_suggested_tools(
564 mut self,
565 tools: impl IntoIterator<Item = impl Into<String>>,
566 ) -> Self {
567 self.suggested_tools = tools.into_iter().map(Into::into).collect();
568 self
569 }
570
571 pub fn with_evidence_uris(mut self, uris: impl IntoIterator<Item = impl Into<String>>) -> Self {
572 self.evidence_uris = uris.into_iter().map(Into::into).collect();
573 self
574 }
575
576 pub fn to_values(hints: &[Self]) -> Vec<serde_json::Value> {
577 hints
578 .iter()
579 .map(|hint| serde_json::to_value(hint).unwrap_or_else(|_| serde_json::json!({})))
580 .collect()
581 }
582}
583
584pub fn program_verification_hints(
585 result: &ProgramResult,
586 trace: Option<&ProgramTrace>,
587) -> Vec<ProgramVerificationHint> {
588 let mut hints = match result.program_name.as_str() {
589 "program_code_search" => vec![ProgramVerificationHint::new(
590 "inspect_matches",
591 "Review matched files before editing or drawing conclusions.",
592 )
593 .required()
594 .with_suggested_tools(["read", "grep"])],
595 "program_repo_map" => vec![ProgramVerificationHint::new(
596 "inspect_project_files",
597 "Use detected project files to choose build, test, and lint commands.",
598 )
599 .required()
600 .with_suggested_tools(["read", "glob"])],
601 _ => Vec::new(),
602 };
603
604 if !result.success {
605 let failed_steps = result
606 .steps
607 .iter()
608 .filter(|step| !step.success)
609 .map(|step| step.label.as_deref().unwrap_or(&step.tool_name))
610 .collect::<Vec<_>>();
611 let message = if failed_steps.is_empty() {
612 "Investigate the failed program execution before relying on its result.".to_string()
613 } else {
614 format!(
615 "Investigate failed program step(s): {}.",
616 failed_steps.join(", ")
617 )
618 };
619 hints.push(
620 ProgramVerificationHint::new("investigate_failed_steps", message)
621 .required()
622 .with_suggested_tools(["read", "grep"]),
623 );
624 }
625
626 if let Some(trace) = trace {
627 let evidence_uris = trace
628 .steps
629 .iter()
630 .filter_map(|step| step.artifact.as_ref())
631 .map(|artifact| artifact.artifact_uri.clone())
632 .collect::<Vec<_>>();
633
634 if !evidence_uris.is_empty() {
635 hints.push(
636 ProgramVerificationHint::new(
637 "inspect_artifacts",
638 "Inspect compacted program artifacts before treating summarized output as complete evidence.",
639 )
640 .required()
641 .with_evidence_uris(evidence_uris),
642 );
643 }
644 }
645
646 hints
647}
648
649pub struct ProgramExecutor {
662 registry: Arc<ToolRegistry>,
663 context: ToolContext,
664}
665
666#[derive(Debug, Clone, Default)]
667pub struct ProgramCatalog {
668 templates: Vec<ProgramTemplate>,
669}
670
671impl ProgramCatalog {
672 pub fn new() -> Self {
673 Self::default()
674 }
675
676 pub fn with_builtin_programs() -> Self {
677 let mut catalog = Self::new();
678 for template in builtin_program_templates() {
679 catalog.register(template);
680 }
681 catalog
682 }
683
684 pub fn register(&mut self, template: ProgramTemplate) {
685 self.insert(template);
686 }
687
688 pub fn try_register(&mut self, template: ProgramTemplate) -> Result<()> {
689 template.ensure_valid()?;
690 self.insert(template);
691 Ok(())
692 }
693
694 fn insert(&mut self, template: ProgramTemplate) {
695 if let Some(existing) = self
696 .templates
697 .iter_mut()
698 .find(|existing| existing.name == template.name)
699 {
700 *existing = template;
701 } else {
702 self.templates.push(template);
703 }
704 }
705
706 pub fn get(&self, name: &str) -> Option<&ProgramTemplate> {
707 self.templates.iter().find(|template| template.name == name)
708 }
709
710 pub fn list(&self) -> &[ProgramTemplate] {
711 &self.templates
712 }
713
714 pub fn instantiate(&self, name: &str, inputs: &serde_json::Value) -> Result<Program> {
715 let template = self
716 .get(name)
717 .ok_or_else(|| anyhow::anyhow!("Unknown program: {}", name))?;
718 template.instantiate(inputs)
719 }
720}
721
722impl ProgramExecutor {
723 pub fn new(registry: Arc<ToolRegistry>, context: ToolContext) -> Self {
724 Self { registry, context }
725 }
726
727 pub async fn execute(&self, program: &Program) -> Result<ProgramResult> {
728 let mut steps = Vec::with_capacity(program.steps.len());
729 let mut success = true;
730
731 for step in &program.steps {
732 let result = self
733 .registry
734 .execute_with_context(&step.tool_name, &step.args, &self.context)
735 .await?;
736
737 let step_success = result.exit_code == 0;
738 success &= step_success;
739 steps.push(ProgramStepResult {
740 tool_name: step.tool_name.clone(),
741 label: step.label.clone(),
742 success: step_success,
743 output: result.output,
744 metadata: result.metadata,
745 });
746
747 if !step_success {
748 break;
749 }
750 }
751
752 Ok(ProgramResult {
753 program_name: program.name.clone(),
754 success,
755 summary: summarize_program_result(program, success, steps.len()),
756 steps,
757 })
758 }
759}
760
761pub fn builtin_program_templates() -> Vec<ProgramTemplate> {
762 vec![program_code_search(), program_repo_map()]
763}
764
765pub fn program_code_search() -> ProgramTemplate {
766 ProgramTemplate::new(
767 "program_code_search",
768 "Search code with a bounded grep pass and return file/line matches.",
769 )
770 .with_parameter(ProgramParameter::required(
771 "query",
772 "Regex or literal pattern to search for.",
773 ))
774 .with_parameter(ProgramParameter::optional(
775 "path",
776 "Workspace-relative path to search.",
777 serde_json::json!("."),
778 ))
779 .with_parameter(ProgramParameter::optional(
780 "glob",
781 "Optional file glob filter.",
782 serde_json::json!("*"),
783 ))
784 .with_step(
785 ProgramStepTemplate::new(
786 "grep",
787 serde_json::json!({
788 "pattern": "{{query}}",
789 "path": "{{path}}",
790 "glob": "{{glob}}",
791 "context": 2
792 }),
793 )
794 .with_label("search_code"),
795 )
796}
797
798pub fn program_repo_map() -> ProgramTemplate {
799 let mut template = ProgramTemplate::new(
800 "program_repo_map",
801 "Map the repository shape with root listing and key project files.",
802 )
803 .with_parameter(ProgramParameter::optional(
804 "path",
805 "Workspace-relative path to map.",
806 serde_json::json!("."),
807 ))
808 .with_step(
809 ProgramStepTemplate::new("ls", serde_json::json!({ "path": "{{path}}" }))
810 .with_label("list_root"),
811 );
812
813 for pattern in [
814 "Cargo.toml",
815 "package.json",
816 "pyproject.toml",
817 "go.mod",
818 "README.md",
819 "AGENTS.md",
820 ] {
821 template = template.with_step(
822 ProgramStepTemplate::new(
823 "glob",
824 serde_json::json!({
825 "path": "{{path}}",
826 "pattern": pattern
827 }),
828 )
829 .with_label(format!("find_{pattern}")),
830 );
831 }
832
833 template
834}
835
836fn summarize_program_result(program: &Program, success: bool, completed_steps: usize) -> String {
837 let status = if success { "completed" } else { "stopped" };
838 format!(
839 "Program '{}' {} after {}/{} steps.",
840 program.name,
841 status,
842 completed_steps,
843 program.steps.len()
844 )
845}
846
847fn render_template_value(
848 value: &serde_json::Value,
849 bindings: &serde_json::Value,
850) -> serde_json::Value {
851 match value {
852 serde_json::Value::String(text) => render_template_string(text, bindings),
853 serde_json::Value::Array(items) => serde_json::Value::Array(
854 items
855 .iter()
856 .map(|item| render_template_value(item, bindings))
857 .collect(),
858 ),
859 serde_json::Value::Object(object) => serde_json::Value::Object(
860 object
861 .iter()
862 .map(|(key, value)| (key.clone(), render_template_value(value, bindings)))
863 .collect(),
864 ),
865 value => value.clone(),
866 }
867}
868
869fn render_template_string(text: &str, bindings: &serde_json::Value) -> serde_json::Value {
870 if let Some(name) = exact_placeholder_name(text) {
871 return bindings.get(name).cloned().unwrap_or_default();
872 }
873
874 let mut rendered = text.to_string();
875 if let Some(object) = bindings.as_object() {
876 for (key, value) in object {
877 let replacement = value
878 .as_str()
879 .map(ToString::to_string)
880 .unwrap_or_else(|| value.to_string());
881 rendered = rendered.replace(&format!("{{{{{key}}}}}"), &replacement);
882 }
883 }
884 serde_json::Value::String(rendered)
885}
886
887fn exact_placeholder_name(text: &str) -> Option<&str> {
888 text.strip_prefix("{{")?.strip_suffix("}}")
889}
890
891#[cfg(test)]
892#[path = "program/tests.rs"]
893mod tests;