use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use syn::Type;
fn convert_ref_to_wrapper(type_str: &str) -> String {
Regex::new(r"Ref<(\w+)>")
.unwrap()
.replace_all(type_str, |caps: ®ex::Captures| format!("<{} as ::ankurah::model::Model>::RefWrapper", &caps[1]))
.to_string()
}
fn has_option_ref(type_str: &str) -> bool { Regex::new(r"Option<\s*Ref<\w+>").unwrap().is_match(type_str) }
fn has_bare_ref(type_str: &str) -> bool { type_str.contains("Ref<") && !has_option_ref(type_str) }
fn extract_ref_model_name(type_str: &str) -> Option<String> {
Regex::new(r"Ref<(\w+)>").unwrap().captures(type_str).map(|caps| caps[1].to_string())
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BackendConfig {
pub backend_name: String,
pub namespace: String,
pub provided_wrapper_types: Vec<String>,
#[serde(default)]
pub uniffi_provided_wrapper_types: Option<Vec<String>>,
pub substitutions: HashMap<String, HashMap<String, String>>,
pub values: Vec<ValueConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValueConfig {
pub type_pattern: String,
pub fully_qualified_type: String,
pub accepts: String,
pub generic_params: Vec<String>,
pub materialized_pattern: String,
pub methods: Vec<Method>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Method {
pub name: String,
pub args: Vec<(String, String)>, pub return_type: String,
#[serde(default = "default_true")]
pub wasm: bool,
#[serde(default = "default_true")]
pub uniffi: bool,
}
fn default_true() -> bool { true }
pub struct ActiveTypeDesc {
pub(crate) backend_config: BackendConfig,
pub(crate) value_config: ValueConfig, pub(crate) concrete_types: HashMap<String, String>, }
impl ActiveTypeDesc {
pub fn new(backend_config: BackendConfig, value_config: ValueConfig, concrete_types: HashMap<String, String>) -> Self {
Self { backend_config, value_config, concrete_types }
}
pub fn wrapper_type_name(&self) -> String {
let mut pattern = self.value_config.materialized_pattern.clone();
for param in &self.value_config.generic_params {
if let Some(concrete_type) = self.concrete_types.get(param) {
let placeholder = format!("{{{}}}", param);
let sanitized_type: String = concrete_type.chars().filter(|c| c.is_alphanumeric()).collect();
pattern = pattern.replace(&placeholder, &sanitized_type);
}
}
pattern
}
pub fn wrapper_type_name_for_model(&self, model_name: &str) -> String { format!("{}_{}", model_name, self.wrapper_type_name()) }
pub fn wrapper_type_path(&self, context: &str) -> String {
let wrapper_name = self.wrapper_type_name();
match context {
"local" => {
format!("{}::{}", self.backend_config.namespace, wrapper_name)
}
"external" => {
wrapper_name
}
_ => wrapper_name,
}
}
pub fn is_provided_type(&self) -> bool {
for concrete_type in self.concrete_types.values() {
if self.backend_config.provided_wrapper_types.contains(concrete_type) {
return true;
}
}
false
}
#[allow(unused)]
pub fn get_wrapper_type_path(&self) -> String {
if self.is_provided_type() {
self.wrapper_type_path("local")
} else {
self.wrapper_type_path("external")
}
}
#[allow(unused)]
pub fn wrapper_type(&self) -> syn::Result<syn::Type> {
let wrapper_name = self.wrapper_type_name();
syn::parse_str(&wrapper_name)
}
#[allow(unused)]
pub fn rust_type_name(&self) -> String {
let rust_type = self.rust_type().unwrap();
quote!(#rust_type).to_string()
}
pub fn rust_type(&self) -> syn::Result<syn::Type> { self.rust_type_with_context("external") }
pub fn rust_type_with_context(&self, context: &str) -> syn::Result<syn::Type> {
let mut type_str = self.value_config.fully_qualified_type.clone();
if let Some(context_substitutions) = self.backend_config.substitutions.get(context) {
for (token, replacement) in context_substitutions {
let placeholder = format!("{{{}}}", token);
type_str = type_str.replace(&placeholder, replacement);
}
}
for (param, concrete_type) in &self.concrete_types {
let placeholder = format!("{{{}}}", param);
type_str = type_str.replace(&placeholder, concrete_type);
}
syn::parse_str(&type_str)
}
fn wasm_methods(&self, context: &str) -> (Vec<TokenStream>, Option<String>) {
let value_config = &self.backend_config.values[0];
let substitute_fn = |backend: &ActiveTypeDesc, pattern: &str| backend.do_substitutions(pattern, context);
let mut ts_set_override: Option<String> = None;
let methods = value_config
.methods
.iter()
.filter(|method| method.wasm)
.map(|method| {
let method_name = format_ident!("{}", method.name);
let ref_model_for_set: Option<String> = if method.name == "set" {
method.args.iter().find_map(|(_, arg_type)| {
let substituted = substitute_fn(self, arg_type);
if has_bare_ref(&substituted) {
extract_ref_model_name(&substituted)
} else {
None
}
})
} else {
None
};
if let Some(ref model_name) = ref_model_for_set {
let model_ident = format_ident!("{}", model_name);
ts_set_override = Some(format!("{}Ref | {}View | string", model_name, model_name));
return quote! {
#[wasm_bindgen(skip_typescript)]
pub fn set(&self, value: JsValue) -> Result<(), JsValue> {
let id: ::ankurah::proto::EntityId = if let Some(s) = value.as_string() {
::ankurah::proto::EntityId::from_base64(&s)
.map_err(|e| JsValue::from_str(&format!("Invalid EntityId string: {}", e)))?
} else if let Ok(id) = value.clone().try_into() {
id
} else {
let id_value = ::ankurah::derive_deps::js_sys::Reflect::get(&value, &JsValue::from_str("id"))
.map_err(|_| JsValue::from_str("Expected string, EntityId, or object with 'id' property"))?;
id_value.try_into()
.map_err(|_| JsValue::from_str("'id' property is not an EntityId"))?
};
let r: ::ankurah::property::Ref<#model_ident> = ::ankurah::property::Ref::new(id);
self.0.set(&r).map_err(|e| JsValue::from(e.to_string()))
}
};
}
let args: Vec<TokenStream> = method
.args
.iter()
.map(|(arg_name, arg_type)| {
let arg_name = format_ident!("{}", arg_name);
let substituted = substitute_fn(self, arg_type);
let arg_type_str = convert_ref_to_wrapper(&substituted);
let arg_type: Type = syn::parse_str(&arg_type_str).expect("Failed to parse arg type");
quote! { #arg_name: #arg_type }
})
.collect();
let original_return_type_str = substitute_fn(self, &method.return_type);
let return_has_ref = original_return_type_str.contains("Ref<");
let return_has_option_ref = has_option_ref(&original_return_type_str);
let return_type_str = convert_ref_to_wrapper(&original_return_type_str);
let arg_names: Vec<syn::Ident> = method.args.iter().map(|(name, _)| format_ident!("{}", name)).collect();
let converted_args: Vec<TokenStream> = method
.args
.iter()
.zip(arg_names.iter())
.map(|((_, arg_type), arg_name)| {
let substituted = substitute_fn(self, arg_type);
let is_ref_arg = has_bare_ref(&substituted);
let is_option_ref_arg = has_option_ref(&substituted);
if arg_type == "{T}" && method.name == "set" {
if is_option_ref_arg {
quote! { &#arg_name.map(Into::into) }
} else if is_ref_arg {
quote! { &(#arg_name.into()) }
} else {
quote! { &#arg_name }
}
} else if is_option_ref_arg {
quote! { #arg_name.map(Into::into) }
} else if is_ref_arg {
quote! { #arg_name.into() }
} else {
quote! { #arg_name }
}
})
.collect();
let (wasm_return_type_str, method_call) = if return_type_str.starts_with("Result<") {
let inner = return_type_str.strip_prefix("Result<").unwrap().strip_suffix(">").unwrap();
let ok_type = inner.split(", ").next().unwrap();
let call = if return_has_option_ref {
quote! { self.0.#method_name(#(#converted_args),*).map(|opt| opt.map(Into::into)).map_err(|e| ::wasm_bindgen::JsValue::from(e.to_string())) }
} else if return_has_ref {
quote! { self.0.#method_name(#(#converted_args),*).map(Into::into).map_err(|e| ::wasm_bindgen::JsValue::from(e.to_string())) }
} else {
quote! { self.0.#method_name(#(#converted_args),*).map_err(|e| ::wasm_bindgen::JsValue::from(e.to_string())) }
};
(format!("Result<{}, ::wasm_bindgen::JsValue>", ok_type), call)
} else if return_has_option_ref {
(return_type_str.clone(), quote! { self.0.#method_name(#(#converted_args),*).map(Into::into) })
} else if return_has_ref {
(return_type_str.clone(), quote! { self.0.#method_name(#(#converted_args),*).into() })
} else {
(return_type_str.clone(), quote! { self.0.#method_name(#(#converted_args),*) })
};
let wasm_return_type: Type = syn::parse_str(&wasm_return_type_str).expect("Failed to parse return type");
quote! {
pub fn #method_name(&self, #(#args),*) -> #wasm_return_type {
#method_call
}
}
})
.collect();
(methods, ts_set_override)
}
#[cfg(feature = "wasm")]
pub fn wasm_wrapper(&self, context: &str, model_scope: Option<&str>) -> TokenStream {
let wrapper_name_str = match model_scope {
Some(model) => self.wrapper_type_name_for_model(model),
None => self.wrapper_type_name(),
};
let wrapper_name = format_ident!("{}", wrapper_name_str);
let original_type: Type = self.rust_type_with_context(context).expect("Failed to parse original type");
let (wasm_methods, ts_set_override) = self.wasm_methods(context);
let ts_custom_section = if let Some(ref union_type) = ts_set_override {
let ts_decl = format!("interface {} {{ set(value: {}): void; }}", wrapper_name_str, union_type);
quote! {
#[wasm_bindgen(typescript_custom_section)]
const _: &'static str = #ts_decl;
}
} else {
quote! {}
};
quote! {
#[wasm_bindgen]
pub struct #wrapper_name(
#[wasm_bindgen(skip)]
pub #original_type
);
#[wasm_bindgen]
impl #wrapper_name {
#(#wasm_methods)*
}
#ts_custom_section
}
}
#[cfg(all(feature = "uniffi", not(feature = "wasm")))]
pub fn uniffi_wrapper(&self, context: &str, model_scope: Option<&str>) -> TokenStream {
let wrapper_name_str = match model_scope {
Some(model) => self.wrapper_type_name_for_model(model),
None => self.wrapper_type_name(),
};
let wrapper_name = format_ident!("{}", wrapper_name_str);
let original_type: Type = self.rust_type_with_context(context).expect("Failed to parse original type");
let uniffi_methods = self.uniffi_methods(context);
quote! {
#[derive(::uniffi::Object)]
pub struct #wrapper_name(pub #original_type);
#[::uniffi::export]
impl #wrapper_name {
#(#uniffi_methods)*
}
}
}
#[cfg(not(feature = "wasm"))]
pub fn wasm_wrapper(&self, _context: &str, _model_scope: Option<&str>) -> TokenStream {
quote! {}
}
#[cfg(any(not(feature = "uniffi"), feature = "wasm"))]
pub fn uniffi_wrapper(&self, _context: &str, _model_scope: Option<&str>) -> TokenStream {
quote! {}
}
fn uniffi_methods(&self, context: &str) -> Vec<TokenStream> {
let substitute_fn = |backend: &ActiveTypeDesc, pattern: &str| backend.do_substitutions(pattern, context);
self.value_config.methods.iter().filter(|method| method.uniffi).map(|method| {
let method_name = format_ident!("{}", method.name);
if method.name == "set" {
let substituted_arg = method.args.iter().find_map(|(_, arg_type)| {
let substituted = substitute_fn(self, arg_type);
if substituted.contains("Ref<") { Some(substituted) } else { None }
});
if let Some(ref substituted) = substituted_arg {
if let Some(model_name) = extract_ref_model_name(substituted) {
let model_ident = format_ident!("{}", model_name);
if has_option_ref(substituted) {
return quote! {
pub fn set(&self, value: Option<String>) -> Result<(), ::ankurah::property::PropertyError> {
let r: Option<::ankurah::property::Ref<#model_ident>> = match value {
Some(ref s) => {
let id = ::ankurah::proto::EntityId::from_base64(s)
.map_err(|e| ::ankurah::property::PropertyError::InvalidValue {
value: s.clone(),
ty: format!("EntityId ({})", e)
})?;
Some(::ankurah::property::Ref::from(id))
},
None => None,
};
self.0.set(&r)
}
};
} else {
return quote! {
pub fn set(&self, value: String) -> Result<(), ::ankurah::property::PropertyError> {
let id = ::ankurah::proto::EntityId::from_base64(&value)
.map_err(|e| ::ankurah::property::PropertyError::InvalidValue {
value: value.clone(),
ty: format!("EntityId ({})", e)
})?;
let r: ::ankurah::property::Ref<#model_ident> = ::ankurah::property::Ref::from(id);
self.0.set(&r)
}
};
}
}
}
}
let args: Vec<TokenStream> = method.args.iter().map(|(arg_name, arg_type)| {
let arg_name = format_ident!("{}", arg_name);
let substituted = substitute_fn(self, arg_type);
let arg_type_str = if substituted == "&str" { "String".to_string() } else { substituted };
let arg_type: Type = syn::parse_str(&arg_type_str).expect("Failed to parse arg type");
quote! { #arg_name: #arg_type }
}).collect();
let return_type_str = substitute_fn(self, &method.return_type);
let return_has_option_ref = has_option_ref(&return_type_str);
let return_has_bare_ref = has_bare_ref(&return_type_str);
let arg_names: Vec<syn::Ident> = method.args.iter().map(|(name, _)| format_ident!("{}", name)).collect();
let converted_args: Vec<TokenStream> = method.args.iter().zip(arg_names.iter()).map(|((_, arg_type), arg_name)| {
let substituted = substitute_fn(self, arg_type);
if substituted == "&str" || (arg_type == "{T}" && method.name == "set") { quote! { &#arg_name } } else { quote! { #arg_name } }
}).collect();
if return_type_str.starts_with("Result<") {
let inner = return_type_str.strip_prefix("Result<").unwrap().strip_suffix(">").unwrap();
let ok_type_str = inner.split(", ").next().unwrap();
let err_type_str = inner.split(", ").nth(1).unwrap_or("::ankurah::property::PropertyError");
let err_type: Type = syn::parse_str(err_type_str).expect("Failed to parse error type");
if return_has_option_ref {
quote! { pub fn #method_name(&self, #(#args),*) -> Result<Option<String>, #err_type> { self.0.#method_name(#(#converted_args),*).map(|opt| opt.map(|r| r.id().to_base64())) } }
} else if return_has_bare_ref {
quote! { pub fn #method_name(&self, #(#args),*) -> Result<String, #err_type> { self.0.#method_name(#(#converted_args),*).map(|r| r.id().to_base64()) } }
} else {
let ok_type: Type = syn::parse_str(ok_type_str).expect("Failed to parse ok type");
quote! { pub fn #method_name(&self, #(#args),*) -> Result<#ok_type, #err_type> { self.0.#method_name(#(#converted_args),*) } }
}
} else if return_has_option_ref {
quote! { pub fn #method_name(&self, #(#args),*) -> Option<String> { self.0.#method_name(#(#converted_args),*).and_then(|r| Some(r.id().to_base64())) } }
} else if return_has_bare_ref {
quote! { pub fn #method_name(&self, #(#args),*) -> Option<String> { self.0.#method_name(#(#converted_args),*).map(|r| r.id().to_base64()) } }
} else {
let return_type: Type = syn::parse_str(&return_type_str).expect("Failed to parse return type");
quote! { pub fn #method_name(&self, #(#args),*) -> #return_type { self.0.#method_name(#(#converted_args),*) } }
}
}).collect()
}
fn substitute_generics(&self, pattern: &str) -> String {
let mut result = pattern.to_string();
for (param, concrete_type) in &self.concrete_types {
let placeholder = format!("{{{}}}", param);
result = result.replace(&placeholder, concrete_type);
}
result
}
fn do_substitutions(&self, pattern: &str, context: &str) -> String {
let mut result = self.substitute_generics(pattern);
if let Some(context_substitutions) = self.backend_config.substitutions.get(context) {
for (token, replacement) in context_substitutions {
let placeholder = format!("{{{}}}", token);
result = result.replace(&placeholder, replacement);
}
}
result
}
}
#[cfg(test)]
mod tests {
use maplit::hashmap;
use super::*;
#[test]
fn test_materialized_name_simple() {
let backend_config = BackendConfig {
backend_name: "LWW".to_string(),
namespace: "test".to_string(),
provided_wrapper_types: vec![],
uniffi_provided_wrapper_types: None,
substitutions: HashMap::new(),
values: vec![],
};
let value_config = ValueConfig {
type_pattern: "LWW(?:<(.+)>)?".to_string(),
fully_qualified_type: "LWW<{T}>".to_string(),
accepts: ".*".to_string(),
generic_params: vec!["T".to_string()],
materialized_pattern: "LWW{T}".to_string(),
methods: vec![],
};
let desc = ActiveTypeDesc::new(backend_config.clone(), value_config.clone(), hashmap! { "T".into() => "String".into() });
assert_eq!(desc.wrapper_type_name(), "LWWString");
let desc =
ActiveTypeDesc::new(backend_config.clone(), value_config.clone(), hashmap! { "T".into() => "HashMap<String, i32>".into() });
assert_eq!(desc.wrapper_type_name(), "LWWHashMapStringi32");
}
}