Skip to content

Commit 6a96599

Browse files
authored
enhance(worker): Support wrangler.jsonc (#425)
* enhance(worker): Support wrangler.jsonc * pass regression tests (toml / by samples/test.sh) * add `samples/worker-bindings-jsonc` & pass test ( compiles / by samples/test.sh ) * fix test
1 parent 73a4669 commit 6a96599

13 files changed

Lines changed: 396 additions & 171 deletions

File tree

ohkami_macros/Cargo.toml

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,16 @@ license = { workspace = true }
1919
features = ["worker", "openapi"]
2020

2121
[dependencies]
22-
proc-macro2 = "1.0"
23-
quote = "1.0"
24-
syn = { version = "2.0", features = ["full"] }
25-
toml = { optional = true, version = "0.8", features = ["parse"], default-features = false }
26-
serde_json = { optional = true, workspace = true }
22+
proc-macro2 = "1.0"
23+
quote = "1.0"
24+
syn = { version = "2.0", features = ["full"] }
25+
toml = { optional = true, version = "0.8", features = ["parse"], default-features = false }
26+
jsonc-parser = { optional = true, version = "0.26", features = ["serde"] }
27+
serde = { optional = true, workspace = true }
28+
serde_json = { optional = true, workspace = true }
2729

2830
[features]
29-
worker = ["dep:toml", "dep:serde_json"]
31+
worker = ["dep:toml", "dep:jsonc-parser", "dep:serde", "dep:serde_json"]
3032
openapi = []
3133

3234
##### DEBUG #####

ohkami_macros/src/util.rs

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -69,15 +69,15 @@ pub(crate) fn extract_doc_comment(attrs: &[syn::Attribute]) -> Option<String> {
6969
/// So, if found the file by simple reading of `file_path`, this returns the file,
7070
/// but if not found, this assumes a Cargo workspace and search all `workspace.members`
7171
/// to find one having file at `file_path`.
72-
pub(crate) fn find_a_file_in_maybe_workspace(file_path: impl AsRef<Path>) -> Result<File, io::Error> {
72+
pub(crate) fn find_file_at_package_or_workspace_root(file_path: impl AsRef<Path>) -> Result<Option<File>, io::Error> {
7373
let file_path: &Path = file_path.as_ref();
7474

7575
match File::open(file_path) {
7676
Ok(file) => {
77-
Ok(file)
77+
Ok(Some(file))
7878
}
7979
Err(e) if matches!(e.kind(), ErrorKind::NotFound) => {
80-
find_a_file_in_workspace(file_path)
80+
find_file_at_workspace_root(file_path)
8181
}
8282
Err(e) => {
8383
Err(e)
@@ -103,7 +103,7 @@ fn unescaped_doc_attr(raw_doc: String) -> String {
103103
}
104104

105105
#[cfg(feature="worker")]
106-
fn find_a_file_in_workspace(file_path: impl AsRef<Path>) -> Result<File, io::Error> {
106+
fn find_file_at_workspace_root(file_path: impl AsRef<Path>) -> Result<Option<File>, io::Error> {
107107
let file_path: &Path = file_path.as_ref();
108108

109109
let cargo_toml: toml::Value = {use std::io::Read;
@@ -113,16 +113,18 @@ fn find_a_file_in_workspace(file_path: impl AsRef<Path>) -> Result<File, io::Err
113113
toml::from_str(&buf).expect("Invalid Cargo.toml")
114114
};
115115

116-
let workspace_members = cargo_toml
117-
.as_table()
118-
.and_then(|c| c.get("workspace"))
119-
.and_then(|w| w.as_table())
120-
.and_then(|w| w.get("members"))
121-
.and_then(|m| m.as_array())
122-
.ok_or_else(|| io::Error::new(ErrorKind::InvalidInput, "\
123-
assumed as Cargo workspace, but `workspace.members` \
124-
array is not found in Cargo.toml at project root. \
125-
"))?;
116+
fn get_workspace_members(cargo_toml: &toml::Value) -> Option<&toml::value::Array> {
117+
cargo_toml
118+
.as_table()?
119+
.get("workspace")?
120+
.as_table()?
121+
.get("members")?
122+
.as_array()
123+
}
124+
125+
let Some(workspace_members) = get_workspace_members(&cargo_toml) else {
126+
return Ok(None)
127+
};
126128

127129
let mut matching_files = Vec::with_capacity(1);
128130
for member in workspace_members {
@@ -133,12 +135,10 @@ fn find_a_file_in_workspace(file_path: impl AsRef<Path>) -> Result<File, io::Err
133135

134136
match (matching_files.pop(), matching_files.is_empty()) {
135137
(Some(file), true) => {
136-
Ok(file)
138+
Ok(Some(file))
137139
}
138140
(None, _) => {
139-
Err(io::Error::new(ErrorKind::NotFound, format!(
140-
"No workspace member having `{}` found.", file_path.display()
141-
)))
141+
Ok(None)
142142
}
143143
(Some(_), false) => {
144144
Err(io::Error::new(ErrorKind::Other, format!(

ohkami_macros/src/worker.rs

Lines changed: 6 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#![cfg(feature="worker")]
22

3+
mod wrangler;
34
mod meta;
45
mod durable;
56
mod binding;
@@ -112,40 +113,13 @@ pub fn worker(args: TokenStream, ohkami_fn: TokenStream) -> Result<TokenStream,
112113
pub fn bindings(env_name: TokenStream, bindings_struct: TokenStream) -> Result<TokenStream, syn::Error> {
113114
use self::binding::Binding;
114115

115-
fn callsite(msg: impl std::fmt::Display) -> Error {
116-
Error::new(Span::call_site(), msg)
117-
}
118-
fn invalid_wrangler_toml() -> Error {
119-
Error::new(Span::call_site(), "Invalid wrangler.toml")
120-
}
121-
122-
//////////////////////////////////////////////////////////////////////////////////////////////////////
123-
124-
let wrangler_toml: toml::Value = {use std::io::Read;
125-
let mut file = util::find_a_file_in_maybe_workspace("wrangler.toml")
126-
.map_err(|e| callsite(e.to_string()))?;
127-
let mut buf = String::new();
128-
file.read_to_string(&mut buf)
129-
.map_err(|_| callsite("wrangler.toml found but it's not readable"))?;
130-
toml::from_str(&buf)
131-
.map_err(|_| callsite("Failed to read wrangler.toml"))?
116+
let bindings: Vec<(Ident, Binding)> = {
117+
let env_name: Option<Ident> = (!env_name.is_empty())
118+
.then(|| syn::parse2(env_name))
119+
.transpose()?;
120+
Binding::collect_from_env(env_name)?
132121
};
133122

134-
let env: &toml::Table = {
135-
let top_level = wrangler_toml.as_table().ok_or_else(invalid_wrangler_toml)?;
136-
let env_name: Option<Ident> = (!env_name.is_empty()).then(|| syn::parse2(env_name)).transpose()?;
137-
match env_name {
138-
None => top_level,
139-
Some(env) => top_level.get("env")
140-
.and_then(|e| e.as_table())
141-
.and_then(|t| t.get(&env.to_string()))
142-
.and_then(|e| e.as_table())
143-
.ok_or_else(|| callsite(format!("env `{env}` is not found in wrangler.toml")))?
144-
}
145-
};
146-
147-
let bindings: Vec<(Ident, Binding)> = Binding::collect_from_env(&env)?;
148-
149123
let bindings_struct: ItemStruct = syn::parse2(bindings_struct)?; {
150124
if !bindings_struct.generics.params.is_empty() {
151125
return Err(Error::new(
Lines changed: 100 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use proc_macro2::{TokenStream, Span};
2-
use quote::quote;
32
use syn::{Ident, LitStr};
3+
use quote::quote;
44

55
pub enum Binding {
66
Variable(String),
@@ -58,100 +58,120 @@ impl Binding {
5858
Self::DurableObject => from_env(quote! { durable_object(#name_str) }),
5959
}
6060
}
61+
}
6162

62-
pub fn collect_from_env(env: &toml::Table) -> Result<Vec<(Ident, Self)>, syn::Error> {
63-
fn invalid_wrangler_toml() -> syn::Error {
64-
syn::Error::new(
65-
Span::call_site(),
66-
"Invalid wrangler.toml: a binding doesn't have `binding = \"...\"`, or some unexpected structure"
67-
)
68-
}
69-
70-
fn invalid_name(name: &str) -> syn::Error {
71-
syn::Error::new(
72-
Span::call_site(),
73-
format!("Can't bind binding `{name}` into Rust struct field")
74-
)
75-
}
76-
77-
///////////////////////////////////////////////////////////////////////////////////////////
63+
#[derive(serde::Deserialize, Default)]
64+
struct EnvBindingCollection {
65+
vars: Option<std::collections::BTreeMap<String, String>>,
66+
ai: Option<BindingDeclare>,
67+
d1_databases: Option<Vec<BindingDeclare>>,
68+
kv_namespaces: Option<Vec<BindingDeclare>>,
69+
r2_buckets: Option<Vec<BindingDeclare>>,
70+
services: Option<Vec<BindingDeclare>>,
71+
queues: Option<QueueProducers>,
72+
durable_objects: Option<BindingsArray>,
73+
// #[serde(flatten)]
74+
// root: BindingCollection,
75+
#[serde(default)]
76+
env: std::collections::BTreeMap<String, EnvBindingCollection>,
77+
}
7878

79-
fn get_field_as_ident(t: &toml::Table, field: &str) -> Result<Ident, syn::Error> {
80-
t.get(field)
81-
.and_then(|b| b.as_str())
82-
.ok_or_else(invalid_wrangler_toml)
83-
.and_then(|name| syn::parse_str::<Ident>(name)
84-
.map_err(|_| invalid_name(name))
85-
)
86-
}
79+
// #[derive(serde::Deserialize, Default)]
80+
// struct BindingCollection {
81+
// vars: Option<std::collections::BTreeMap<String, String>>,
82+
// ai: Option<BindingDeclare>,
83+
// d1_databases: Option<Vec<BindingDeclare>>,
84+
// kv_namespaces: Option<Vec<BindingDeclare>>,
85+
// r2_buckets: Option<Vec<BindingDeclare>>,
86+
// services: Option<Vec<BindingDeclare>>,
87+
// queues: Option<QueueProducers>,
88+
// durable_objects: Option<BindingsArray>,
89+
// }
90+
91+
#[derive(serde::Deserialize)]
92+
struct BindingDeclare {
93+
binding: String,
94+
}
8795

88-
fn binding_of(t: &toml::Table) -> Result<Ident, syn::Error> {
89-
get_field_as_ident(t, "binding")
90-
}
91-
fn name_of(t: &toml::Table) -> Result<Ident, syn::Error> {
92-
get_field_as_ident(t, "name")
93-
}
96+
#[derive(serde::Deserialize)]
97+
struct QueueProducers {
98+
producers: Vec<BindingDeclare>,
99+
}
94100

95-
fn table_array(a: &toml::value::Array) -> Result<impl IntoIterator<Item = &toml::Table>, syn::Error> {
96-
a.iter()
97-
.map(|v| v.as_table().ok_or_else(invalid_wrangler_toml))
98-
.collect::<Result<Vec<_>, _>>()
99-
}
101+
#[derive(serde::Deserialize)]
102+
struct BindingsArray {
103+
bindings: Vec<BindingName>,
104+
}
100105

101-
///////////////////////////////////////////////////////////////////////////////////////////
106+
#[derive(serde::Deserialize)]
107+
struct BindingName {
108+
name: String,
109+
}
102110

103-
let mut bindings = Vec::new();
111+
impl Binding {
112+
pub fn collect_from_env(env_name: Option<Ident>) -> Result<Vec<(Ident, Self)>, syn::Error> {
113+
let mut config = super::wrangler::parse_wrangler::<EnvBindingCollection>()
114+
.map_err(|e| syn::Error::new(Span::call_site(), e))?;
115+
let config = match env_name.as_ref() {
116+
None => config,
117+
Some(name) => {
118+
let config = config.env.get_mut(&name.to_string())
119+
.ok_or_else(|| syn::Error::new(name.span(), format!("env `{name}` is not found in wrangler config")))?;
120+
std::mem::take(config)
121+
}
122+
};
104123

105-
if let Some(toml::Value::Table(vars)) = env.get("vars") {
106-
for (name, value) in vars {
107-
let name = syn::parse_str(name).map_err(|_| invalid_name(name))?;
108-
let value = value.as_str()
109-
.ok_or_else(|| syn::Error::new(
110-
Span::call_site(),
111-
"`#[bindings]` doesn't support JSON values in `vars` binding"
112-
))?
113-
.to_owned();
114-
bindings.push((name, Self::Variable(value)))
124+
let mut collection = Vec::new();
125+
{
126+
if let Some(vars) = config.vars {
127+
for (name, value) in vars {
128+
collection.push((name, Self::Variable(value)));
129+
}
115130
}
116-
}
117-
if let Some(toml::Value::Table(ai)) = env.get("ai") {
118-
bindings.push((binding_of(ai)?, Self::AI))
119-
}
120-
if let Some(toml::Value::Array(d1_databases)) = env.get("d1_databases") {
121-
for d1 in table_array(d1_databases)? {
122-
bindings.push((binding_of(d1)?, Self::D1))
131+
if let Some(BindingDeclare { binding }) = config.ai {
132+
collection.push((binding, Self::AI));
123133
}
124-
}
125-
if let Some(toml::Value::Array(kv_namespaces)) = env.get("kv_namespaces") {
126-
for kv in table_array(kv_namespaces)? {
127-
bindings.push((binding_of(kv)?, Self::KV))
134+
if let Some(d1_databases) = config.d1_databases {
135+
for BindingDeclare { binding } in d1_databases {
136+
collection.push((binding, Self::D1));
137+
}
128138
}
129-
}
130-
if let Some(toml::Value::Array(r2_buckets)) = env.get("r2_buckets") {
131-
for r2 in table_array(r2_buckets)? {
132-
bindings.push((binding_of(r2)?, Self::R2))
139+
if let Some(kv_namespaces) = config.kv_namespaces {
140+
for BindingDeclare { binding } in kv_namespaces {
141+
collection.push((binding, Self::KV));
142+
}
133143
}
134-
}
135-
if let Some(toml::Value::Array(services)) = env.get("services") {
136-
for service in table_array(services)? {
137-
bindings.push((binding_of(service)?, Self::Service))
144+
if let Some(r2_buckets) = config.r2_buckets {
145+
for BindingDeclare { binding } in r2_buckets {
146+
collection.push((binding, Self::R2));
147+
}
138148
}
139-
}
140-
if let Some(toml::Value::Table(queues)) = env.get("queues") {
141-
if let Some(toml::Value::Array(producers)) = queues.get("producers") {
142-
for producer in table_array(producers)? {
143-
bindings.push((binding_of(producer)?, Self::Queue))
149+
if let Some(services) = config.services {
150+
for BindingDeclare { binding } in services {
151+
collection.push((binding, Self::Service));
144152
}
145153
}
146-
}
147-
if let Some(toml::Value::Table(durable_objects)) = env.get("durable_objects") {
148-
if let Some(toml::Value::Array(durable_object_bindings)) = durable_objects.get("bindings") {
149-
for durable_object in table_array(durable_object_bindings)? {
150-
bindings.push((name_of(durable_object)?, Self::DurableObject))
154+
if let Some(QueueProducers { producers }) = config.queues {
155+
for BindingDeclare { binding } in producers {
156+
collection.push((binding, Self::Queue));
151157
}
152158
}
153-
}
154-
155-
Ok(bindings)
159+
if let Some(BindingsArray { bindings }) = config.durable_objects {
160+
for BindingName { name } in bindings {
161+
collection.push((name, Self::DurableObject));
162+
}
163+
}
164+
}
165+
166+
collection
167+
.into_iter()
168+
.map(|(name, binding)| {
169+
let name = syn::parse_str(&name).map_err(|_| syn::Error::new(
170+
Span::call_site(),
171+
format!("can't handle binding name `{name}` as a Rust identifier")
172+
))?;
173+
Ok((name, binding))
174+
})
175+
.collect()
156176
}
157177
}

0 commit comments

Comments
 (0)