use proc_macro2::TokenStream;
use quote::quote;
use syn::{Attribute, Field, ItemStruct, Path};
#[derive(Debug)]
pub struct BelongsToInfo {
pub field_name: String,
pub field_name_json: String,
pub foreign_type: String,
pub is_optional: bool,
pub exclude_from_tree: bool,
}
#[derive(Debug)]
pub struct OwnsManyInfo {
pub field_name: String,
#[allow(dead_code)]
pub field_name_json: String,
pub foreign_type: String,
pub exclude_from_tree: bool,
}
#[derive(Debug)]
pub struct EnsureForFieldInfo {
pub field_name: String,
pub field_name_json: String,
pub foreign_type: String,
pub exclude_from_tree: bool,
}
#[derive(Debug)]
pub struct EnsureForInfo {
pub dependencies: Vec<(String, String, String, bool)>,
}
#[derive(Debug)]
pub struct DefaultValueInfo {
#[allow(dead_code)]
pub field_name: String,
#[allow(dead_code)]
pub field_name_json: String,
pub value_tokens: TokenStream,
}
#[derive(Debug)]
pub struct ClientIdFieldInfo {
pub field_name_json: String,
}
#[derive(Debug)]
pub struct FallbackToIdFieldInfo {
pub field_name_json: String,
}
#[derive(Debug)]
pub struct ServerOwnedFieldInfo {
pub field_name: String,
pub field_name_json: String,
}
#[derive(Debug)]
pub struct SearchableFieldInfo {
pub field_name_json: String,
}
pub fn to_camel_case(s: &str) -> String {
let mut result = String::new();
let mut capitalize_next = false;
for c in s.chars() {
if c == '_' {
capitalize_next = true;
} else if capitalize_next {
result.push(c.to_ascii_uppercase());
capitalize_next = false;
} else {
result.push(c);
}
}
result
}
pub fn is_relationship_attr(attr: &Attribute) -> bool {
let path = attr.path();
path.is_ident("belongs_to")
|| path.is_ident("owns_many")
|| path.is_ident("ensure_for")
|| path.is_ident("default_value")
|| path.is_ident("myko_client_id")
|| path.is_ident("fallback_to_id")
|| path.is_ident("searchable")
|| path.is_ident("exclude_from_tree")
|| path.is_ident("server_owned")
}
fn is_option_type(ty: &syn::Type) -> bool {
if let syn::Type::Path(type_path) = ty
&& let Some(segment) = type_path.path.segments.last()
{
return segment.ident == "Option";
}
false
}
pub fn parse_belongs_to(field: &Field) -> Option<BelongsToInfo> {
let field_name = field.ident.as_ref()?.to_string();
let field_name_json = to_camel_case(&field_name);
let is_optional = is_option_type(&field.ty);
let exclude_from_tree = field
.attrs
.iter()
.any(|a| a.path().is_ident("exclude_from_tree"));
for attr in &field.attrs {
if attr.path().is_ident("belongs_to")
&& let Ok(path) = attr.parse_args::<Path>()
{
let foreign_type = path.segments.last()?.ident.to_string();
return Some(BelongsToInfo {
field_name,
field_name_json,
foreign_type,
is_optional,
exclude_from_tree,
});
}
}
None
}
pub fn parse_owns_many(field: &Field) -> Option<OwnsManyInfo> {
let field_name = field.ident.as_ref()?.to_string();
let field_name_json = to_camel_case(&field_name);
let exclude_from_tree = field
.attrs
.iter()
.any(|a| a.path().is_ident("exclude_from_tree"));
for attr in &field.attrs {
if attr.path().is_ident("owns_many")
&& let Ok(path) = attr.parse_args::<Path>()
{
let foreign_type = path.segments.last()?.ident.to_string();
return Some(OwnsManyInfo {
field_name,
field_name_json,
foreign_type,
exclude_from_tree,
});
}
}
None
}
pub fn parse_ensure_for_field(field: &Field) -> Option<EnsureForFieldInfo> {
let field_name = field.ident.as_ref()?.to_string();
let field_name_json = to_camel_case(&field_name);
let exclude_from_tree = field
.attrs
.iter()
.any(|a| a.path().is_ident("exclude_from_tree"));
for attr in &field.attrs {
if attr.path().is_ident("ensure_for")
&& let Ok(path) = attr.parse_args::<Path>()
{
let foreign_type = path.segments.last()?.ident.to_string();
return Some(EnsureForFieldInfo {
field_name,
field_name_json,
foreign_type,
exclude_from_tree,
});
}
}
None
}
pub fn parse_default_value(field: &Field) -> Option<DefaultValueInfo> {
let field_name = field.ident.as_ref()?.to_string();
let field_name_json = to_camel_case(&field_name);
for attr in &field.attrs {
if attr.path().is_ident("default_value") {
if let Ok(lit) = attr.parse_args::<syn::Lit>() {
let value_tokens = quote! { #lit };
return Some(DefaultValueInfo {
field_name,
field_name_json,
value_tokens,
});
}
if let Ok(expr) = attr.parse_args::<syn::Expr>() {
let value_tokens = quote! { #expr };
return Some(DefaultValueInfo {
field_name,
field_name_json,
value_tokens,
});
}
}
}
None
}
pub fn parse_client_id(field: &Field) -> Option<ClientIdFieldInfo> {
let field_name = field.ident.as_ref()?.to_string();
let field_name_json = to_camel_case(&field_name);
for attr in &field.attrs {
if attr.path().is_ident("myko_client_id") {
return Some(ClientIdFieldInfo { field_name_json });
}
}
None
}
pub fn parse_fallback_to_id(field: &Field) -> Option<FallbackToIdFieldInfo> {
let field_name = field.ident.as_ref()?.to_string();
let field_name_json = to_camel_case(&field_name);
for attr in &field.attrs {
if attr.path().is_ident("fallback_to_id") {
return Some(FallbackToIdFieldInfo { field_name_json });
}
}
None
}
pub fn parse_server_owned(field: &Field) -> Option<ServerOwnedFieldInfo> {
let field_name = field.ident.as_ref()?.to_string();
let field_name_json = to_camel_case(&field_name);
for attr in &field.attrs {
if attr.path().is_ident("server_owned") {
return Some(ServerOwnedFieldInfo {
field_name,
field_name_json,
});
}
}
None
}
pub fn parse_searchable(field: &Field) -> Option<SearchableFieldInfo> {
let field_name = field.ident.as_ref()?.to_string();
let field_name_json = to_camel_case(&field_name);
for attr in &field.attrs {
if attr.path().is_ident("searchable") {
return Some(SearchableFieldInfo { field_name_json });
}
}
None
}
pub fn strip_relationship_attrs(field: &mut Field) {
field.attrs.retain(|attr| !is_relationship_attr(attr));
}
#[derive(Debug, Default)]
pub struct RelationshipInfo {
pub belongs_to: Vec<BelongsToInfo>,
pub owns_many: Vec<OwnsManyInfo>,
pub ensure_for_fields: Vec<EnsureForFieldInfo>,
pub default_values: Vec<DefaultValueInfo>,
pub client_id_field: Option<ClientIdFieldInfo>,
pub fallback_to_id_fields: Vec<FallbackToIdFieldInfo>,
pub searchable_fields: Vec<SearchableFieldInfo>,
pub server_owned_field: Option<ServerOwnedFieldInfo>,
}
impl RelationshipInfo {
pub fn ensure_for(&self) -> Option<EnsureForInfo> {
if self.ensure_for_fields.is_empty() {
None
} else {
Some(EnsureForInfo {
dependencies: self
.ensure_for_fields
.iter()
.map(|ef| {
(
ef.foreign_type.clone(),
ef.field_name.clone(),
ef.field_name_json.clone(),
ef.exclude_from_tree,
)
})
.collect(),
})
}
}
}
pub fn collect_relationships(input: &ItemStruct) -> RelationshipInfo {
let mut info = RelationshipInfo::default();
if let syn::Fields::Named(ref fields) = input.fields {
for field in &fields.named {
if let Some(bt) = parse_belongs_to(field) {
info.belongs_to.push(bt);
}
if let Some(om) = parse_owns_many(field) {
info.owns_many.push(om);
}
if let Some(ef) = parse_ensure_for_field(field) {
info.ensure_for_fields.push(ef);
}
if let Some(dv) = parse_default_value(field) {
info.default_values.push(dv);
}
if let Some(ci) = parse_client_id(field) {
info.client_id_field = Some(ci);
}
if let Some(fi) = parse_fallback_to_id(field) {
info.fallback_to_id_fields.push(fi);
}
if let Some(sf) = parse_searchable(field) {
info.searchable_fields.push(sf);
}
if let Some(so) = parse_server_owned(field) {
info.server_owned_field = Some(so);
}
}
}
info
}
pub fn generate_registrations(local_type: &str, info: &RelationshipInfo) -> TokenStream {
let mut registrations = Vec::new();
let local_type_ident = syn::Ident::new(local_type, proc_macro2::Span::call_site());
let krate = crate::myko_path();
for bt in &info.belongs_to {
let field_ident = syn::Ident::new(&bt.field_name, proc_macro2::Span::call_site());
let foreign_type = &bt.foreign_type;
let extract_fk = if bt.is_optional {
quote! {
|item: &dyn std::any::Any| -> Option<std::sync::Arc<str>> {
item.downcast_ref::<#local_type_ident>()
.and_then(|e| e.#field_ident.as_ref().map(|s| std::sync::Arc::<str>::from(&**s)))
}
}
} else {
quote! {
|item: &dyn std::any::Any| -> Option<std::sync::Arc<str>> {
item.downcast_ref::<#local_type_ident>()
.map(|e| std::sync::Arc::<str>::from(&*e.#field_ident))
}
}
};
let exclude_from_tree = bt.exclude_from_tree;
let fk_field_json = &bt.field_name_json;
registrations.push(quote! {
#krate::submit! {
#krate::relationship::RelationRegistration {
relation: #krate::relationship::Relation::BelongsTo {
local_type: #local_type,
foreign_type: #foreign_type,
fk_field_json: #fk_field_json,
extract_fk: #extract_fk,
exclude_from_tree: #exclude_from_tree,
}
}
}
});
}
for om in &info.owns_many {
let field_ident = syn::Ident::new(&om.field_name, proc_macro2::Span::call_site());
let foreign_type = &om.foreign_type;
let exclude_from_tree = om.exclude_from_tree;
registrations.push(quote! {
#krate::submit! {
#krate::relationship::RelationRegistration {
relation: #krate::relationship::Relation::OwnsMany {
local_type: #local_type,
foreign_type: #foreign_type,
extract_ids: |item: &dyn std::any::Any| -> Option<Vec<std::sync::Arc<str>>> {
item.downcast_ref::<#local_type_ident>()
.map(|e| e.#field_ident.iter().map(|id| std::sync::Arc::<str>::from(&**id)).collect())
},
remove_id: |item: &dyn std::any::Any, id_to_remove: &str| -> Option<std::sync::Arc<dyn #krate::item::AnyItem>> {
item.downcast_ref::<#local_type_ident>().map(|e| {
let mut updated = e.clone();
updated.#field_ident.retain(|id| &**id != id_to_remove);
std::sync::Arc::new(updated) as std::sync::Arc<dyn #krate::item::AnyItem>
})
},
exclude_from_tree: #exclude_from_tree,
}
}
}
});
}
if let Some(ref ef) = info.ensure_for() {
let exclude_from_tree = ef.dependencies.iter().any(|(_, _, _, ex)| *ex);
let deps: Vec<_> = ef
.dependencies
.iter()
.map(|(ft, lk, _lkj, _ex)| {
let field_ident = syn::Ident::new(lk, proc_macro2::Span::call_site());
quote! {
#krate::relationship::EnsureForDependency {
foreign_type: #ft,
extract_fk: |item: &dyn std::any::Any| -> Option<std::sync::Arc<str>> {
item.downcast_ref::<#local_type_ident>()
.map(|e| std::sync::Arc::<str>::from(&*e.#field_ident))
},
}
}
})
.collect();
let fk_field_assignments: Vec<_> = ef
.dependencies
.iter()
.enumerate()
.map(|(i, (_, lk, _lkj, _ex))| {
let field_ident = syn::Ident::new(lk, proc_macro2::Span::call_site());
let idx = syn::Index::from(i);
quote! {
entity.#field_ident = dep_ids[#idx].clone().into();
}
})
.collect();
let default_assignments: Vec<_> = info
.default_values
.iter()
.map(|dv| {
let field_ident = syn::Ident::new(&dv.field_name, proc_macro2::Span::call_site());
let value = &dv.value_tokens;
quote! {
entity.#field_ident = #value.into();
}
})
.collect();
registrations.push(quote! {
#krate::submit! {
#krate::relationship::RelationRegistration {
relation: #krate::relationship::Relation::EnsureFor {
local_type: #local_type,
dependencies: &[#(#deps),*],
exclude_from_tree: #exclude_from_tree,
make_entity: |dep_ids: &[std::sync::Arc<str>]| {
let mut entity = #local_type_ident::default();
entity.id = uuid::Uuid::new_v4().to_string().into();
#(#fk_field_assignments)*
#(#default_assignments)*
std::sync::Arc::new(entity) as std::sync::Arc<dyn #krate::item::AnyItem>
},
}
}
}
});
}
if let Some(ref ci) = info.client_id_field {
let field_name_json = &ci.field_name_json;
registrations.push(quote! {
#krate::submit! {
#krate::relationship::ClientIdRegistration {
entity_type: #local_type,
field_name_json: #field_name_json,
}
}
});
}
for fi in &info.fallback_to_id_fields {
let field_name_json = &fi.field_name_json;
registrations.push(quote! {
#krate::submit! {
#krate::relationship::FallbackToIdRegistration {
entity_type: #local_type,
field_name_json: #field_name_json,
}
}
});
}
if let Some(ref so) = info.server_owned_field {
let field_name_json = &so.field_name_json;
registrations.push(quote! {
#[cfg(not(target_arch = "wasm32"))]
#krate::submit! {
#krate::relationship::ServerOwnedRegistration {
entity_type: #local_type,
field_name_json: #field_name_json,
}
}
});
}
if !info.searchable_fields.is_empty() {
let fields: Vec<_> = info
.searchable_fields
.iter()
.map(|sf| {
let field = &sf.field_name_json;
quote! { #field }
})
.collect();
registrations.push(quote! {
#[cfg(not(target_arch = "wasm32"))]
#krate::submit! {
#krate::search::SearchableRegistration {
entity_type: #local_type,
fields: &[#(#fields),*],
}
}
});
}
quote! {
#(#registrations)*
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_to_camel_case() {
assert_eq!(to_camel_case("scope_id"), "scopeId");
assert_eq!(to_camel_case("node_ids"), "nodeIds");
assert_eq!(to_camel_case("name"), "name");
assert_eq!(to_camel_case("my_long_field_name"), "myLongFieldName");
}
}