///| Typed Cloudflare binding contract helpers.
///|
/// Cloudflare binding category used for dev/test validation and docs.
pub(all) enum BindingKind {
EnvVar
Secret
KV
R2
D1
DurableObject
Service
AI
Queue
}
///|
pub fn BindingKind::label(self : BindingKind) -> String {
match self {
EnvVar => "var"
Secret => "secret"
KV => "kv"
R2 => "r2"
D1 => "d1"
DurableObject => "durable_object"
Service => "service"
AI => "ai"
Queue => "queue"
}
}
///|
/// One expected Cloudflare binding.
pub(all) struct BindingSpec {
name : String
kind : BindingKind
optional : Bool
}
///|
/// Phantom-typed binding key for generated env contracts.
pub(all) struct BindingKey[T] {
name : String
kind : BindingKind
optional : Bool
_type : T?
}
///|
pub fn[T] BindingKey::name(self : BindingKey[T]) -> String {
self.name
}
///|
pub fn[T] BindingKey::kind(self : BindingKey[T]) -> BindingKind {
self.kind
}
///|
pub fn[T] BindingKey::optional(self : BindingKey[T]) -> Bool {
self.optional
}
///|
/// A required binding that is missing from the Worker env.
pub(all) struct MissingBinding {
name : String
kind : BindingKind
}
///|
pub fn binding(
name : String,
kind : BindingKind,
optional? : Bool = false,
) -> BindingSpec {
{ name, kind, optional }
}
///|
pub fn[T] binding_key(
name : String,
kind : BindingKind,
optional? : Bool = false,
) -> BindingKey[T] {
{ name, kind, optional, _type: None }
}
///|
/// Required plain env var.
pub fn env_var(name : String, optional? : Bool = false) -> BindingSpec {
binding(name, EnvVar, optional~)
}
///|
pub fn env_var_key(
name : String,
optional? : Bool = false,
) -> BindingKey[String] {
binding_key(name, EnvVar, optional~)
}
///|
/// Required secret env var.
pub fn secret(name : String, optional? : Bool = false) -> BindingSpec {
binding(name, Secret, optional~)
}
///|
pub fn secret_key(
name : String,
optional? : Bool = false,
) -> BindingKey[String] {
binding_key(name, Secret, optional~)
}
///|
/// Required KV namespace binding.
pub fn kv_namespace(name : String, optional? : Bool = false) -> BindingSpec {
binding(name, KV, optional~)
}
///|
/// Required R2 bucket binding.
pub fn r2_bucket(name : String, optional? : Bool = false) -> BindingSpec {
binding(name, R2, optional~)
}
///|
/// Required D1 database binding.
pub fn d1_database(name : String, optional? : Bool = false) -> BindingSpec {
binding(name, D1, optional~)
}
///|
/// Required Durable Object namespace binding.
pub fn durable_object(name : String, optional? : Bool = false) -> BindingSpec {
binding(name, DurableObject, optional~)
}
///|
/// Required Service Binding.
pub fn service_binding(name : String, optional? : Bool = false) -> BindingSpec {
binding(name, Service, optional~)
}
///|
/// Required Workers AI binding.
pub fn ai_binding(name : String, optional? : Bool = false) -> BindingSpec {
binding(name, AI, optional~)
}
///|
/// Required Queue binding.
pub fn queue_binding(name : String, optional? : Bool = false) -> BindingSpec {
binding(name, Queue, optional~)
}
///|
extern "js" fn ffi_env_has_binding(env : @mars.JsEnv, name : String) -> Bool =
#| (env, name) => env != null && env[name] !== undefined && env[name] !== null
///|
/// Return all missing required bindings for a Cloudflare Worker env.
pub fn missing_required_bindings(
env : @mars.JsEnv,
specs : Array[BindingSpec],
) -> Array[MissingBinding] {
let missing : Array[MissingBinding] = []
for spec in specs {
if !spec.optional && !ffi_env_has_binding(env, spec.name) {
missing.push({ name: spec.name, kind: spec.kind })
}
}
missing
}
///|
/// True when every required binding exists.
pub fn validate_bindings(env : @mars.JsEnv, specs : Array[BindingSpec]) -> Bool {
missing_required_bindings(env, specs).is_empty()
}
///|
/// Human-readable validation error for missing bindings.
pub fn describe_missing_bindings(missing : Array[MissingBinding]) -> String {
if missing.is_empty() {
return "all required Cloudflare bindings are present"
}
let buf = StringBuilder::new()
buf.write_string("missing Cloudflare bindings: ")
for i, item in missing {
if i > 0 {
buf.write_string(", ")
}
buf.write_string(item.name)
buf.write_string(" (")
buf.write_string(item.kind.label())
buf.write_string(")")
}
buf.to_string()
}
///|
pub fn bindings_typescript_decl(
specs : Array[BindingSpec],
interface_name? : String = "CloudflareEnv",
) -> String {
let buf = StringBuilder::new()
buf.write_string(
"// Auto-generated Cloudflare binding contract. Do not edit.\n\n",
)
buf.write_string("export interface \{interface_name} {\n")
for spec in specs {
let marker = if spec.optional { "?" } else { "" }
buf.write_string(
" \{binding_typescript_property_key(spec.name)}\{marker}: \{binding_kind_typescript_type(spec.kind)};\n",
)
}
buf.write_string("}\n")
buf.to_string()
}
///|
fn binding_kind_typescript_type(kind : BindingKind) -> String {
match kind {
EnvVar | Secret => "string"
KV => "KVNamespace"
R2 => "R2Bucket"
D1 => "D1Database"
DurableObject => "DurableObjectNamespace"
Service => "Fetcher"
AI => "Ai"
Queue => "Queue"
}
}
///|
fn binding_typescript_property_key(value : String) -> String {
if binding_is_typescript_identifier(value) {
value
} else {
binding_typescript_string_literal(value)
}
}
///|
fn binding_is_typescript_identifier(value : String) -> Bool {
if value.is_empty() {
return false
}
let chars = value.to_array()
let first = chars[0]
let first_ok = (first >= 'a' && first <= 'z') ||
(first >= 'A' && first <= 'Z') ||
first == '_' ||
first == '$'
if !first_ok {
return false
}
for c in chars {
let ok = (c >= 'a' && c <= 'z') ||
(c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') ||
c == '_' ||
c == '$'
if !ok {
return false
}
}
true
}
///|
fn binding_typescript_string_literal(value : String) -> String {
let buf = StringBuilder::new()
buf.write_char('"')
for c in value {
match c {
'"' => buf.write_string("\\\"")
'\\' => buf.write_string("\\\\")
'\n' => buf.write_string("\\n")
'\r' => buf.write_string("\\r")
'\t' => buf.write_string("\\t")
_ => buf.write_char(c)
}
}
buf.write_char('"')
buf.to_string()
}