Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions ohkami_macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,16 @@ license = { workspace = true }
features = ["worker", "openapi"]

[dependencies]
proc-macro2 = "1.0"
quote = "1.0"
syn = { version = "2.0", features = ["full"] }
toml = { optional = true, version = "0.8", features = ["parse"], default-features = false }
serde_json = { optional = true, workspace = true }
proc-macro2 = "1.0"
quote = "1.0"
syn = { version = "2.0", features = ["full"] }
toml = { optional = true, version = "0.8", features = ["parse"], default-features = false }
jsonc-parser = { optional = true, version = "0.26", features = ["serde"] }
serde = { optional = true, workspace = true }
serde_json = { optional = true, workspace = true }

[features]
worker = ["dep:toml", "dep:serde_json"]
worker = ["dep:toml", "dep:jsonc-parser", "dep:serde", "dep:serde_json"]
openapi = []

##### DEBUG #####
Expand Down
36 changes: 18 additions & 18 deletions ohkami_macros/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,15 @@ pub(crate) fn extract_doc_comment(attrs: &[syn::Attribute]) -> Option<String> {
/// So, if found the file by simple reading of `file_path`, this returns the file,
/// but if not found, this assumes a Cargo workspace and search all `workspace.members`
/// to find one having file at `file_path`.
pub(crate) fn find_a_file_in_maybe_workspace(file_path: impl AsRef<Path>) -> Result<File, io::Error> {
pub(crate) fn find_file_at_package_or_workspace_root(file_path: impl AsRef<Path>) -> Result<Option<File>, io::Error> {
let file_path: &Path = file_path.as_ref();

match File::open(file_path) {
Ok(file) => {
Ok(file)
Ok(Some(file))
}
Err(e) if matches!(e.kind(), ErrorKind::NotFound) => {
find_a_file_in_workspace(file_path)
find_file_at_workspace_root(file_path)
}
Err(e) => {
Err(e)
Expand All @@ -103,7 +103,7 @@ fn unescaped_doc_attr(raw_doc: String) -> String {
}

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

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

let workspace_members = cargo_toml
.as_table()
.and_then(|c| c.get("workspace"))
.and_then(|w| w.as_table())
.and_then(|w| w.get("members"))
.and_then(|m| m.as_array())
.ok_or_else(|| io::Error::new(ErrorKind::InvalidInput, "\
assumed as Cargo workspace, but `workspace.members` \
array is not found in Cargo.toml at project root. \
"))?;
fn get_workspace_members(cargo_toml: &toml::Value) -> Option<&toml::value::Array> {
cargo_toml
.as_table()?
.get("workspace")?
.as_table()?
.get("members")?
.as_array()
}

let Some(workspace_members) = get_workspace_members(&cargo_toml) else {
return Ok(None)
};

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

match (matching_files.pop(), matching_files.is_empty()) {
(Some(file), true) => {
Ok(file)
Ok(Some(file))
}
(None, _) => {
Err(io::Error::new(ErrorKind::NotFound, format!(
"No workspace member having `{}` found.", file_path.display()
)))
Ok(None)
}
(Some(_), false) => {
Err(io::Error::new(ErrorKind::Other, format!(
Expand Down
38 changes: 6 additions & 32 deletions ohkami_macros/src/worker.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#![cfg(feature="worker")]

mod wrangler;
mod meta;
mod durable;
mod binding;
Expand Down Expand Up @@ -112,40 +113,13 @@ pub fn worker(args: TokenStream, ohkami_fn: TokenStream) -> Result<TokenStream,
pub fn bindings(env_name: TokenStream, bindings_struct: TokenStream) -> Result<TokenStream, syn::Error> {
use self::binding::Binding;

fn callsite(msg: impl std::fmt::Display) -> Error {
Error::new(Span::call_site(), msg)
}
fn invalid_wrangler_toml() -> Error {
Error::new(Span::call_site(), "Invalid wrangler.toml")
}

//////////////////////////////////////////////////////////////////////////////////////////////////////

let wrangler_toml: toml::Value = {use std::io::Read;
let mut file = util::find_a_file_in_maybe_workspace("wrangler.toml")
.map_err(|e| callsite(e.to_string()))?;
let mut buf = String::new();
file.read_to_string(&mut buf)
.map_err(|_| callsite("wrangler.toml found but it's not readable"))?;
toml::from_str(&buf)
.map_err(|_| callsite("Failed to read wrangler.toml"))?
let bindings: Vec<(Ident, Binding)> = {
let env_name: Option<Ident> = (!env_name.is_empty())
.then(|| syn::parse2(env_name))
.transpose()?;
Binding::collect_from_env(env_name)?
};

let env: &toml::Table = {
let top_level = wrangler_toml.as_table().ok_or_else(invalid_wrangler_toml)?;
let env_name: Option<Ident> = (!env_name.is_empty()).then(|| syn::parse2(env_name)).transpose()?;
match env_name {
None => top_level,
Some(env) => top_level.get("env")
.and_then(|e| e.as_table())
.and_then(|t| t.get(&env.to_string()))
.and_then(|e| e.as_table())
.ok_or_else(|| callsite(format!("env `{env}` is not found in wrangler.toml")))?
}
};

let bindings: Vec<(Ident, Binding)> = Binding::collect_from_env(&env)?;

let bindings_struct: ItemStruct = syn::parse2(bindings_struct)?; {
if !bindings_struct.generics.params.is_empty() {
return Err(Error::new(
Expand Down
180 changes: 100 additions & 80 deletions ohkami_macros/src/worker/binding.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use proc_macro2::{TokenStream, Span};
use quote::quote;
use syn::{Ident, LitStr};
use quote::quote;

pub enum Binding {
Variable(String),
Expand Down Expand Up @@ -58,100 +58,120 @@ impl Binding {
Self::DurableObject => from_env(quote! { durable_object(#name_str) }),
}
}
}

pub fn collect_from_env(env: &toml::Table) -> Result<Vec<(Ident, Self)>, syn::Error> {
fn invalid_wrangler_toml() -> syn::Error {
syn::Error::new(
Span::call_site(),
"Invalid wrangler.toml: a binding doesn't have `binding = \"...\"`, or some unexpected structure"
)
}

fn invalid_name(name: &str) -> syn::Error {
syn::Error::new(
Span::call_site(),
format!("Can't bind binding `{name}` into Rust struct field")
)
}

///////////////////////////////////////////////////////////////////////////////////////////
#[derive(serde::Deserialize, Default)]
struct EnvBindingCollection {
vars: Option<std::collections::BTreeMap<String, String>>,
ai: Option<BindingDeclare>,
d1_databases: Option<Vec<BindingDeclare>>,
kv_namespaces: Option<Vec<BindingDeclare>>,
r2_buckets: Option<Vec<BindingDeclare>>,
services: Option<Vec<BindingDeclare>>,
queues: Option<QueueProducers>,
durable_objects: Option<BindingsArray>,
// #[serde(flatten)]
// root: BindingCollection,
#[serde(default)]
env: std::collections::BTreeMap<String, EnvBindingCollection>,
}

fn get_field_as_ident(t: &toml::Table, field: &str) -> Result<Ident, syn::Error> {
t.get(field)
.and_then(|b| b.as_str())
.ok_or_else(invalid_wrangler_toml)
.and_then(|name| syn::parse_str::<Ident>(name)
.map_err(|_| invalid_name(name))
)
}
// #[derive(serde::Deserialize, Default)]
// struct BindingCollection {
// vars: Option<std::collections::BTreeMap<String, String>>,
// ai: Option<BindingDeclare>,
// d1_databases: Option<Vec<BindingDeclare>>,
// kv_namespaces: Option<Vec<BindingDeclare>>,
// r2_buckets: Option<Vec<BindingDeclare>>,
// services: Option<Vec<BindingDeclare>>,
// queues: Option<QueueProducers>,
// durable_objects: Option<BindingsArray>,
// }

#[derive(serde::Deserialize)]
struct BindingDeclare {
binding: String,
}

fn binding_of(t: &toml::Table) -> Result<Ident, syn::Error> {
get_field_as_ident(t, "binding")
}
fn name_of(t: &toml::Table) -> Result<Ident, syn::Error> {
get_field_as_ident(t, "name")
}
#[derive(serde::Deserialize)]
struct QueueProducers {
producers: Vec<BindingDeclare>,
}

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

///////////////////////////////////////////////////////////////////////////////////////////
#[derive(serde::Deserialize)]
struct BindingName {
name: String,
}

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

if let Some(toml::Value::Table(vars)) = env.get("vars") {
for (name, value) in vars {
let name = syn::parse_str(name).map_err(|_| invalid_name(name))?;
let value = value.as_str()
.ok_or_else(|| syn::Error::new(
Span::call_site(),
"`#[bindings]` doesn't support JSON values in `vars` binding"
))?
.to_owned();
bindings.push((name, Self::Variable(value)))
let mut collection = Vec::new();
{
if let Some(vars) = config.vars {
for (name, value) in vars {
collection.push((name, Self::Variable(value)));
}
}
}
if let Some(toml::Value::Table(ai)) = env.get("ai") {
bindings.push((binding_of(ai)?, Self::AI))
}
if let Some(toml::Value::Array(d1_databases)) = env.get("d1_databases") {
for d1 in table_array(d1_databases)? {
bindings.push((binding_of(d1)?, Self::D1))
if let Some(BindingDeclare { binding }) = config.ai {
collection.push((binding, Self::AI));
}
}
if let Some(toml::Value::Array(kv_namespaces)) = env.get("kv_namespaces") {
for kv in table_array(kv_namespaces)? {
bindings.push((binding_of(kv)?, Self::KV))
if let Some(d1_databases) = config.d1_databases {
for BindingDeclare { binding } in d1_databases {
collection.push((binding, Self::D1));
}
}
}
if let Some(toml::Value::Array(r2_buckets)) = env.get("r2_buckets") {
for r2 in table_array(r2_buckets)? {
bindings.push((binding_of(r2)?, Self::R2))
if let Some(kv_namespaces) = config.kv_namespaces {
for BindingDeclare { binding } in kv_namespaces {
collection.push((binding, Self::KV));
}
}
}
if let Some(toml::Value::Array(services)) = env.get("services") {
for service in table_array(services)? {
bindings.push((binding_of(service)?, Self::Service))
if let Some(r2_buckets) = config.r2_buckets {
for BindingDeclare { binding } in r2_buckets {
collection.push((binding, Self::R2));
}
}
}
if let Some(toml::Value::Table(queues)) = env.get("queues") {
if let Some(toml::Value::Array(producers)) = queues.get("producers") {
for producer in table_array(producers)? {
bindings.push((binding_of(producer)?, Self::Queue))
if let Some(services) = config.services {
for BindingDeclare { binding } in services {
collection.push((binding, Self::Service));
}
}
}
if let Some(toml::Value::Table(durable_objects)) = env.get("durable_objects") {
if let Some(toml::Value::Array(durable_object_bindings)) = durable_objects.get("bindings") {
for durable_object in table_array(durable_object_bindings)? {
bindings.push((name_of(durable_object)?, Self::DurableObject))
if let Some(QueueProducers { producers }) = config.queues {
for BindingDeclare { binding } in producers {
collection.push((binding, Self::Queue));
}
}
}

Ok(bindings)
if let Some(BindingsArray { bindings }) = config.durable_objects {
for BindingName { name } in bindings {
collection.push((name, Self::DurableObject));
}
}
}

collection
.into_iter()
.map(|(name, binding)| {
let name = syn::parse_str(&name).map_err(|_| syn::Error::new(
Span::call_site(),
format!("can't handle binding name `{name}` as a Rust identifier")
))?;
Ok((name, binding))
})
.collect()
}
}
Loading