Skip to main content

bynk_emit/emitter/
wrangler.rs

1//! `wrangler.toml` generation per Worker (v0.8 §4.4 / §4.5).
2//!
3//! Each context becomes a Cloudflare Worker with its own wrangler config.
4//! Service Bindings are declared for every consumed context. Durable
5//! Object bindings + migrations are declared for every agent.
6
7use std::fmt::Write as _;
8
9use crate::project::{UnitTable, worker_dir_name};
10
11/// Compile-time pinned compatibility date. Cloudflare uses this to lock
12/// Workers runtime behaviour. Bump cautiously when changing the runtime
13/// dependencies.
14const COMPATIBILITY_DATE: &str = "2024-11-01";
15
16pub fn emit_wrangler_toml(
17    context: &str,
18    table: &UnitTable,
19    consumes: &[String],
20    // v0.19 (C1): this Worker's closure reaches bynk.cloudflare — declare the
21    // KV namespace binding (the `id` is a deploy-time placeholder).
22    needs_kv: bool,
23) -> String {
24    let name = worker_dir_name(context);
25    let mut out = String::new();
26    let _ = writeln!(out, "# Generated by bynkc — do not edit by hand.");
27    let _ = writeln!(out, "name = \"{name}\"");
28    let _ = writeln!(out, "main = \"index.ts\"");
29    let _ = writeln!(out, "compatibility_date = \"{COMPATIBILITY_DATE}\"");
30    writeln!(out).unwrap();
31
32    let mut sorted_consumes: Vec<&String> = consumes.iter().collect();
33    sorted_consumes.sort();
34    for target in &sorted_consumes {
35        let binding = consumed_binding_name(target);
36        let service = worker_dir_name(target);
37        let _ = writeln!(out, "[[services]]");
38        let _ = writeln!(out, "binding = \"{binding}\"");
39        let _ = writeln!(out, "service = \"{service}\"");
40        writeln!(out).unwrap();
41    }
42
43    if needs_kv {
44        let _ = writeln!(out, "[[kv_namespaces]]");
45        let _ = writeln!(
46            out,
47            "binding = \"{}\"",
48            bynk_check::firstparty::KV_BINDING_NAME
49        );
50        let _ = writeln!(out, "id = \"<KV_NAMESPACE_ID>\" # set at deploy time");
51        writeln!(out).unwrap();
52    }
53
54    // Agents → Durable Object bindings + migrations.
55    let mut agent_names: Vec<&String> = table.agents.keys().collect();
56    agent_names.sort();
57    for agent_name in &agent_names {
58        let binding = agent_binding_name(agent_name);
59        let _ = writeln!(out, "[[durable_objects.bindings]]");
60        let _ = writeln!(out, "name = \"{binding}\"");
61        let _ = writeln!(out, "class_name = \"{agent_name}\"");
62        writeln!(out).unwrap();
63    }
64    if !agent_names.is_empty() {
65        let _ = writeln!(out, "[[migrations]]");
66        let _ = writeln!(out, "tag = \"v1\"");
67        let classes: Vec<String> = agent_names.iter().map(|n| format!("\"{n}\"")).collect();
68        let _ = writeln!(out, "new_classes = [{}]", classes.join(", "));
69        writeln!(out).unwrap();
70    }
71
72    // v0.10a: cron triggers. Cloudflare uses a single `[triggers]` table with a
73    // `crons` array aggregating every `on cron` schedule in the context.
74    let mut crons: Vec<&String> = Vec::new();
75    for service in table.services.values() {
76        for handler in &service.handlers {
77            if let bynk_syntax::ast::HandlerKind::Cron { expr } = &handler.kind {
78                crons.push(expr);
79            }
80        }
81    }
82    crons.sort();
83    crons.dedup();
84    if !crons.is_empty() {
85        let quoted: Vec<String> = crons.iter().map(|e| format!("\"{e}\"")).collect();
86        let _ = writeln!(out, "[triggers]");
87        let _ = writeln!(out, "crons = [{}]", quoted.join(", "));
88        writeln!(out).unwrap();
89    }
90
91    // v0.10b: queue consumers. Each `on queue "name"` becomes a
92    // `[[queues.consumers]]` binding.
93    let mut queues: Vec<&String> = Vec::new();
94    for service in table.services.values() {
95        // v0.44: one queue binding per service, on the `from queue("name")` header.
96        if let bynk_syntax::ast::ServiceProtocol::Queue { name } = &service.protocol {
97            queues.push(name);
98        }
99    }
100    queues.sort();
101    queues.dedup();
102    for name in &queues {
103        let _ = writeln!(out, "[[queues.consumers]]");
104        let _ = writeln!(out, "queue = \"{name}\"");
105        let _ = writeln!(out, "max_batch_size = 10");
106        writeln!(out).unwrap();
107    }
108
109    out
110}
111
112/// Service Binding identifier for a consumed context: uppercase with
113/// underscores. `commerce.payment` → `COMMERCE_PAYMENT`.
114pub fn consumed_binding_name(target: &str) -> String {
115    target.replace('.', "_").to_uppercase()
116}
117
118/// Durable Object binding identifier for an agent class. We use the
119/// class name in screaming snake case so handlers can grab it by a
120/// predictable name (`OrderEntity` → `ORDER_ENTITY`).
121pub fn agent_binding_name(class_name: &str) -> String {
122    let mut out = String::new();
123    for (i, ch) in class_name.chars().enumerate() {
124        if i > 0 && ch.is_uppercase() {
125            out.push('_');
126        }
127        out.push(ch.to_ascii_uppercase());
128    }
129    out
130}