use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use crate::analysis::{GenerationPlan, RequestType};
use crate::google::api::FieldBehavior;
use crate::parsing::CodeGenMetadata;
use crate::parsing::types::BaseType;
use super::{CodeGenConfig, format_tokens};
pub(crate) fn generate_resource_enum(
plan: &GenerationPlan,
metadata: &CodeGenMetadata,
config: &CodeGenConfig,
error_type_path: Option<&str>,
) -> String {
if !config.generate_resource_enum {
return String::new();
}
let package_prefix = infer_package_prefix(
&plan
.services
.iter()
.map(|s| s.package.as_str())
.collect::<Vec<_>>(),
);
let mut resources: Vec<ResourceEntry> = metadata
.messages
.iter()
.filter_map(|(name, info)| {
let rd = info.resource_descriptor.as_ref()?;
if !name.starts_with(&package_prefix) {
return None;
}
let variant_name = match rd.r#type.split('/').next_back() {
Some(v) if !v.is_empty() => v.to_string(),
_ => {
tracing::warn!(
"Skipping resource `{}`: type `{}` has no `/`-separated variant name",
name,
rd.r#type
);
return None;
}
};
let rust_path = message_name_to_rust_path(name, &package_prefix, 1)?;
let id_field = info
.fields
.iter()
.find(|f| f.field_behavior.contains(&FieldBehavior::Identifier));
let (id_field_name, id_is_optional) = match id_field {
Some(f) => (Some(f.name.clone()), f.unified_type.is_optional),
None => (None, false),
};
let message_has_full_name = info.fields.iter().any(|f| f.name == "full_name");
let path_names = derive_path_names(
&rd.singular,
!rd.name_field.is_empty() || message_has_full_name,
plan,
metadata,
);
let known_managed_fields: &[&str] =
&["created_at", "updated_at", "created_by", "updated_by"];
let field_descriptors: Vec<FieldDescriptorEntry> = info
.fields
.iter()
.map(|f| {
let role = if f.field_behavior.contains(&FieldBehavior::Identifier) {
FieldRoleEntry::Identifier
} else if f.is_sensitive {
FieldRoleEntry::Sensitive
} else if f.field_behavior.contains(&FieldBehavior::OutputOnly)
&& known_managed_fields.contains(&f.name.as_str())
{
FieldRoleEntry::Managed
} else {
FieldRoleEntry::Data
};
FieldDescriptorEntry {
name: f.name.clone(),
role,
}
})
.collect();
Some(ResourceEntry {
variant_name,
rust_path,
singular: rd.singular.clone(),
id_field: id_field_name,
id_is_optional,
path_names,
has_full_name: message_has_full_name,
field_descriptors,
})
})
.collect();
resources.sort_by(|a, b| a.singular.cmp(&b.singular));
let resource_variants: Vec<TokenStream> = resources
.iter()
.map(|r| {
let variant = format_ident!("{}", r.variant_name);
let path: syn::Type = syn::parse_str(&r.rust_path)
.unwrap_or_else(|e| panic!("Invalid rust path `{}`: {}", r.rust_path, e));
quote! { #variant(#path) }
})
.collect();
let label_variants: Vec<TokenStream> = resources
.iter()
.map(|r| {
let variant = format_ident!("{}", r.variant_name);
quote! { #variant }
})
.collect();
let extra_impls: TokenStream = if let Some(error_path) = error_type_path {
let error_ty: syn::Type = syn::parse_str(error_path)
.unwrap_or_else(|e| panic!("Invalid error_type_path `{error_path}`: {e}"));
let label_arms: Vec<TokenStream> = resources
.iter()
.map(|r| {
let variant = format_ident!("{}", r.variant_name);
quote! { Resource::#variant(_) => &ObjectLabel::#variant, }
})
.collect();
let from_impls: Vec<TokenStream> = resources
.iter()
.map(|r| {
let variant = format_ident!("{}", r.variant_name);
let path: syn::Type = syn::parse_str(&r.rust_path)
.unwrap_or_else(|e| panic!("Invalid rust path `{}`: {}", r.rust_path, e));
quote! {
impl From<#path> for Resource {
fn from(v: #path) -> Self {
Resource::#variant(v)
}
}
impl TryFrom<Resource> for #path {
type Error = #error_ty;
fn try_from(r: Resource) -> Result<Self, Self::Error> {
match r {
Resource::#variant(v) => Ok(v),
_ => Err(<#error_ty>::generic(concat!(
"Resource is not a ",
stringify!(#variant)
))),
}
}
}
}
})
.collect();
quote! {
impl Resource {
pub fn resource_label(&self) -> &ObjectLabel {
match self {
#(#label_arms)*
}
}
}
#(#from_impls)*
}
} else {
quote! {}
};
let object_conversions_impl: TokenStream = if config.generate_object_conversions {
let mut conversion_impls: Vec<TokenStream> = Vec::new();
let mut qualified_name_impls: Vec<TokenStream> = Vec::new();
for r in &resources {
let Some(ref id_field) = r.id_field else {
continue;
};
let path: syn::Type = syn::parse_str(&r.rust_path)
.unwrap_or_else(|e| panic!("Invalid rust path `{}`: {}", r.rust_path, e));
let label_expr: syn::Expr = syn::parse_str(&format!("ObjectLabel::{}", r.variant_name))
.unwrap_or_else(|e| panic!("Invalid label expr: {e}"));
let id_ident = format_ident!("{}", id_field);
let is_optional = r.id_is_optional;
let path_name_idents: Vec<proc_macro2::Ident> = r
.path_names
.iter()
.map(|n| format_ident!("{}", n))
.collect();
conversion_impls.push(emit_from_object(&path, &id_ident, is_optional));
conversion_impls.push(emit_to_object(&path, &label_expr, &id_ident, is_optional));
conversion_impls.push(emit_resource_impl(
&path,
&label_expr,
&id_ident,
&path_name_idents,
is_optional,
));
let format_expr: TokenStream = build_qualified_name_expr(&r.path_names);
qualified_name_impls.push(quote! {
impl #path {
pub fn qualified_name(&self) -> String {
#format_expr
}
}
});
}
quote! {
use crate::Error;
use crate::models::object::Object;
use crate::models::resources::{ResourceExt, ResourceIdent, ResourceName, ResourceRef};
#(#conversion_impls)*
#(#qualified_name_impls)*
}
} else {
quote! {}
};
let tokens = quote! {
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, Debug, PartialEq)]
pub enum Resource {
#(#resource_variants),*
}
#[derive(
::strum::AsRefStr,
::strum::Display,
::strum::EnumIter,
::strum::EnumString,
::serde::Serialize,
::serde::Deserialize,
Hash,
Clone,
Copy,
Debug,
PartialEq,
Eq,
PartialOrd,
Ord,
)]
#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
#[serde(rename_all = "snake_case")]
#[cfg_attr(feature = "sqlx", derive(::sqlx::Type))]
#[cfg_attr(
feature = "sqlx",
sqlx(type_name = "object_label", rename_all = "snake_case")
)]
pub enum ObjectLabel {
#(#label_variants),*
}
#extra_impls
#object_conversions_impl
};
let registry_impl = if config.generate_store_integration {
generate_resource_registry(&resources, config, plan, metadata)
} else {
quote! {}
};
let all_tokens = quote! {
#tokens
#registry_impl
};
format_tokens(all_tokens)
}
struct ResourceEntry {
variant_name: String,
rust_path: String,
singular: String,
id_field: Option<String>,
id_is_optional: bool,
path_names: Vec<String>,
#[allow(dead_code)]
has_full_name: bool,
field_descriptors: Vec<FieldDescriptorEntry>,
}
struct FieldDescriptorEntry {
name: String,
role: FieldRoleEntry,
}
enum FieldRoleEntry {
Data,
Identifier,
Sensitive,
Managed,
}
fn derive_path_names(
singular: &str,
has_full_name_field: bool,
plan: &GenerationPlan,
metadata: &CodeGenMetadata,
) -> Vec<String> {
let service = plan.services.iter().find(|s| {
s.managed_resources
.iter()
.any(|r| r.descriptor.singular == singular)
});
let Some(service) = service else {
return vec!["name".to_string()];
};
let resource_type = metadata
.resource_from_singular(singular)
.map(|rd| rd.r#type.clone())
.unwrap_or_default();
if !service.hierarchy.is_empty() && !resource_type.is_empty() {
let annotation_parents: Vec<String> = service
.hierarchy
.iter()
.filter(|h| h.child_resource_type == resource_type)
.map(|h| h.parent_field_name.clone())
.collect();
if !annotation_parents.is_empty() {
let mut params = annotation_parents;
params.push("name".to_string());
return params;
}
}
let get_path_param = service
.methods
.iter()
.find(|m| m.request_type == RequestType::Get)
.and_then(|m| m.path_parameters().next().map(|p| p.name.clone()));
let parent_params: Vec<String> = service
.methods
.iter()
.find(|m| m.request_type == RequestType::List)
.map(|m| {
m.parameters
.iter()
.filter(|p| !p.is_path_param() && !p.is_optional())
.filter(|p| matches!(p.field_type().base_type, BaseType::String))
.map(|p| p.name().to_string())
.collect()
})
.unwrap_or_default();
let should_decompose = has_full_name_field
|| (get_path_param.as_deref() == Some("name") && !parent_params.is_empty());
if should_decompose {
let mut params = parent_params;
params.push(format!("{singular}_name"));
let last = params.last_mut().unwrap();
*last = "name".to_string();
params
} else {
vec!["name".to_string()]
}
}
fn build_qualified_name_expr(path_names: &[String]) -> TokenStream {
if path_names.len() == 1 {
let field = format_ident!("{}", &path_names[0]);
return quote! { self.#field.clone() };
}
let format_str = path_names
.iter()
.map(|_| "{}")
.collect::<Vec<_>>()
.join(".");
let field_refs: Vec<TokenStream> = path_names
.iter()
.map(|n| {
let ident = format_ident!("{}", n);
quote! { self.#ident }
})
.collect();
quote! { format!(#format_str, #(#field_refs),*) }
}
fn infer_package_prefix(packages: &[&str]) -> String {
if packages.is_empty() {
return String::new();
}
let first_parts: Vec<&str> = packages[0].split('.').collect();
let _common_len = first_parts
.iter()
.enumerate()
.take_while(|(i, seg)| {
packages
.iter()
.skip(1)
.all(|p| p.split('.').nth(*i) == Some(seg))
})
.count();
format!(".{}.", first_parts[0])
}
fn message_name_to_rust_path(name: &str, prefix: &str, super_levels: u32) -> Option<String> {
let without_prefix = name.strip_prefix(prefix)?;
let parts: Vec<&str> = without_prefix.split('.').collect();
if parts.is_empty() {
return None;
}
let super_prefix = "super::".repeat(super_levels as usize);
Some(format!("{}{}", super_prefix, parts.join("::")))
}
fn generate_resource_registry(
resources: &[ResourceEntry],
config: &CodeGenConfig,
plan: &GenerationPlan,
metadata: &CodeGenMetadata,
) -> TokenStream {
let store_crate = format_ident!("{}", config.resource_store_crate_name);
let label_impl = quote! {
impl ::#store_crate::Label for ObjectLabel {
fn as_str(&self) -> &str {
self.as_ref()
}
}
};
let parent_labels: Vec<Option<String>> = resources
.iter()
.map(|r| {
if r.path_names.len() <= 1 {
return None;
}
let resource_type = metadata
.resource_from_singular(&r.singular)
.map(|rd| rd.r#type.as_str())
.unwrap_or("");
if !resource_type.is_empty() {
for service in &plan.services {
for h in &service.hierarchy {
if h.child_resource_type == resource_type {
if let Some(ref parent_sing) = h.parent_singular {
let found = resources.iter().find_map(|candidate| {
if candidate.singular == *parent_sing {
Some(candidate.variant_name.clone())
} else {
None
}
});
if found.is_some() {
return found;
}
}
}
}
}
}
let parent_path_component = &r.path_names[r.path_names.len() - 2];
let parent_singular = parent_path_component
.strip_suffix("_name")
.unwrap_or(parent_path_component);
resources.iter().find_map(|candidate| {
if candidate.singular == parent_singular {
Some(candidate.variant_name.clone())
} else {
None
}
})
})
.collect();
let descriptor_entries: Vec<TokenStream> = resources
.iter()
.zip(parent_labels.iter())
.map(|(r, parent)| {
let label_variant = format_ident!("{}", r.variant_name);
let field_entries: Vec<TokenStream> = r
.field_descriptors
.iter()
.map(|fd| {
let name = &fd.name;
let role = match fd.role {
FieldRoleEntry::Data => {
quote! { ::#store_crate::FieldRole::Data }
}
FieldRoleEntry::Identifier => {
quote! { ::#store_crate::FieldRole::Identifier }
}
FieldRoleEntry::Sensitive => {
quote! { ::#store_crate::FieldRole::Sensitive }
}
FieldRoleEntry::Managed => {
quote! { ::#store_crate::FieldRole::Managed }
}
};
quote! {
::#store_crate::ResourceFieldDescriptor {
name: #name,
role: #role,
}
}
})
.collect();
let path_name_strs: Vec<&str> = r.path_names.iter().map(|s| s.as_str()).collect();
let parent_expr = match parent {
Some(parent_name) => {
let parent_variant = format_ident!("{}", parent_name);
quote! { Some(ObjectLabel::#parent_variant) }
}
None => quote! { None },
};
quote! {
::#store_crate::ResourceTypeDescriptor {
label: ObjectLabel::#label_variant,
fields: &[#(#field_entries),*],
path_names: &[#(#path_name_strs),*],
parent_label: #parent_expr,
}
}
})
.collect();
let registry = quote! {
pub static RESOURCE_DESCRIPTORS: &[::#store_crate::ResourceTypeDescriptor<ObjectLabel>] = &[
#(#descriptor_entries),*
];
};
quote! {
#label_impl
#registry
}
}
fn emit_from_object(
path: &syn::Type,
id_ident: &proc_macro2::Ident,
is_optional: bool,
) -> TokenStream {
let id_assignment = if is_optional {
quote! { res.#id_ident = Some(object.id.hyphenated().to_string()); }
} else {
quote! { res.#id_ident = object.id.hyphenated().to_string(); }
};
quote! {
impl TryFrom<Object> for #path {
type Error = Error;
fn try_from(object: Object) -> Result<Self, Self::Error> {
let props = object
.properties
.ok_or_else(|| Error::generic("expected properties"))?;
let mut res: #path = ::serde_json::from_value(props)?;
#id_assignment
Ok(res)
}
}
}
}
fn emit_to_object(
path: &syn::Type,
label_expr: &syn::Expr,
id_ident: &proc_macro2::Ident,
is_optional: bool,
) -> TokenStream {
let id_field = if is_optional {
quote! {
let id = obj
.#id_ident
.as_ref()
.map(|id| ::uuid::Uuid::parse_str(id))
.transpose()?
.unwrap_or_else(|| ::uuid::Uuid::nil());
}
} else {
quote! {
let id = ::uuid::Uuid::parse_str(&obj.#id_ident).unwrap_or_else(|_| ::uuid::Uuid::nil());
}
};
quote! {
impl TryFrom<#path> for Object {
type Error = Error;
fn try_from(obj: #path) -> Result<Self, Self::Error> {
#id_field
Ok(Object {
id,
name: obj.resource_name(),
label: #label_expr,
properties: Some(::serde_json::to_value(obj)?),
updated_at: None,
created_at: chrono::Utc::now(),
})
}
}
}
}
fn emit_resource_impl(
path: &syn::Type,
label_expr: &syn::Expr,
id_ident: &proc_macro2::Ident,
path_name_idents: &[proc_macro2::Ident],
is_optional: bool,
) -> TokenStream {
let resource_ref = if is_optional {
quote! {
self
.#id_ident
.as_ref()
.and_then(|id| ::uuid::Uuid::parse_str(id).ok())
.map(ResourceRef::Uuid)
.unwrap_or_else(|| ResourceRef::Name(self.resource_name()))
}
} else {
quote! {
::uuid::Uuid::parse_str(&self.#id_ident)
.ok()
.map(ResourceRef::Uuid)
.unwrap_or_else(|| ResourceRef::Name(self.resource_name()))
}
};
quote! {
impl ResourceExt for #path {
fn resource_name(&self) -> ResourceName {
ResourceName::new([#(&self.#path_name_idents),*])
}
fn resource_ref(&self) -> ResourceRef {
#resource_ref
}
fn resource_ident(&self) -> ResourceIdent {
(#label_expr).to_ident(self.resource_ref())
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_message_name_to_rust_path() {
assert_eq!(
message_name_to_rust_path(".unitycatalog.catalogs.v1.Catalog", ".unitycatalog.", 1),
Some("super::catalogs::v1::Catalog".to_string())
);
assert_eq!(
message_name_to_rust_path(
".unitycatalog.external_locations.v1.ExternalLocation",
".unitycatalog.",
1
),
Some("super::external_locations::v1::ExternalLocation".to_string())
);
assert_eq!(
message_name_to_rust_path(".google.api.Something", ".unitycatalog.", 1),
None
);
}
#[test]
fn test_infer_package_prefix() {
assert_eq!(
infer_package_prefix(&["unitycatalog.catalogs.v1", "unitycatalog.tables.v1"]),
".unitycatalog."
);
assert_eq!(infer_package_prefix(&["example.catalog.v1"]), ".example.");
assert_eq!(
infer_package_prefix(&["example.catalog.v1", "example.items.v1"]),
".example."
);
}
#[test]
fn test_build_qualified_name_expr_flat() {
let expr = build_qualified_name_expr(&["name".to_string()]);
let s = expr.to_string();
assert!(s.contains("self"), "expr: {s}");
assert!(s.contains("name"), "expr: {s}");
assert!(s.contains("clone"), "expr: {s}");
}
#[test]
fn test_build_qualified_name_expr_hierarchical() {
let expr = build_qualified_name_expr(&[
"catalog_name".to_string(),
"schema_name".to_string(),
"name".to_string(),
]);
let s = expr.to_string();
assert!(s.contains("format"), "expr: {s}");
assert!(s.contains("catalog_name"), "expr: {s}");
assert!(s.contains("schema_name"), "expr: {s}");
}
}