use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::LitStr;
use super::emit_ctx::CrudEmitCtx;
use super::emit_update::model_update_with_before_inherent_tokens;
pub(super) fn ownership_resolution_tokens(cx: &CrudEmitCtx<'_>) -> TokenStream {
let system_lit = syn::LitBool::new(cx.ownership_system_owned, proc_macro2::Span::call_site());
match &cx.ownership_resolver {
None => quote! {
if let Some(o) = valence.owner_override() {
o.clone()
} else if #system_lit {
valence::OwnerRef::system()
} else {
valence::OwnerRef::from_actor(valence.actor())
}
},
Some(path) => quote! {
if let Some(o) = valence.owner_override() {
o.clone()
} else if #system_lit {
valence::OwnerRef::system()
} else {
#path::default()
.resolve_owner(&__payload, valence.actor(), valence)
.await?
}
},
}
}
pub(super) fn ownership_after_row_persisted(cx: &CrudEmitCtx<'_>, row_ident: &str) -> TokenStream {
if cx.ownership_skip {
return quote! {};
}
let row = format_ident!("{}", row_ident);
quote! {
#row.__ensure_ownership_after_write(valence).await?;
}
}
pub(super) fn emit_ownership_support_tokens(cx: &CrudEmitCtx<'_>) -> TokenStream {
if cx.ownership_skip {
return quote! {};
}
let struct_name = &cx.struct_name;
let table_name_lit = cx.table_name_lit;
let resolve = ownership_resolution_tokens(cx);
quote! {
#[allow(dead_code)]
impl #struct_name {
pub(crate) async fn __ensure_ownership_after_write(
&self,
valence: &valence::Valence,
) -> valence::Result<()> {
let __rec_id = self.id().ok_or_else(|| {
valence::Error::Validation(format!(
"{}: record has no id after write",
#table_name_lit,
))
})?;
let __key = __rec_id.id().to_string();
let __bare = valence::ownership::normalize_record_id_for_ownership(&__key);
let __payload = serde_json::to_value(self)
.map_err(|e| valence::Error::Serialization(e.to_string()))?;
let __owner: valence::OwnerRef = #resolve;
valence::ownership::OwnershipService::ensure_active_ownership(
#table_name_lit,
&__bare,
__owner,
valence,
)
.await
}
}
}
}
pub(super) fn emit_batch_creatable_tokens(cx: &CrudEmitCtx<'_>) -> TokenStream {
let struct_name = &cx.struct_name;
let table_name_lit = cx.table_name_lit;
let ownership_hook = if cx.ownership_skip {
quote! {}
} else {
quote! {
async fn ensure_ownership_after_batch_create(
created_row: serde_json::Value,
valence: &valence::Valence,
) -> valence::Result<()> {
let row: Self = serde_json::from_value(created_row)
.map_err(|e| valence::Error::Serialization(e.to_string()))?;
row.__ensure_ownership_after_write(valence).await
}
}
};
quote! {
#[async_trait::async_trait]
impl valence::BatchCreatable for #struct_name {
fn table_name() -> &'static str {
#table_name_lit
}
fn set_id(&mut self, id: valence::RecordId) {
self.id = Some(id);
}
#ownership_hook
}
}
}
fn schema_metadata_fn_tokens(table_name_lit: &str) -> TokenStream {
quote! {
fn __schema_metadata() -> &'static valence::SchemaMetadataStruct {
valence::SchemaRegistry::global()
.get_schema(#table_name_lit)
.expect(concat!("SchemaRegistry missing entry for ", #table_name_lit))
}
}
}
fn unique_constraint_helpers_tokens(unique_field_names: &[LitStr]) -> TokenStream {
quote! {
async fn __assert_unique_constraints_for_record(
record: &serde_json::Value,
excluding_id: Option<&str>,
valence: &valence::Valence,
) -> valence::Result<()> {
for field_name in Self::__unique_field_names() {
let Some(value) = record.get(field_name) else {
continue;
};
if value.is_null() {
continue;
}
Self::__assert_unique_field_value(field_name, value, excluding_id, valence)
.await?;
}
Ok(())
}
fn __unique_field_names() -> &'static [&'static str] {
&[#(#unique_field_names),*]
}
async fn __assert_unique_field_value(
field_name: &str,
field_value: &serde_json::Value,
excluding_id: Option<&str>,
valence: &valence::Valence,
) -> valence::Result<()> {
valence
.ensure_unique_field_index(<Self as valence::Model>::table_name(), field_name)
.await?;
let compiled = valence::__internal::CompiledQuery {
query_string: format!(
"SELECT VALUE id FROM {} WHERE {} = $value LIMIT 2",
<Self as valence::Model>::table_name(),
field_name,
),
params: vec![("value".to_string(), field_value.clone())],
};
let matches: Vec<serde_json::Value> = valence::retry_on_database_tx_conflict(
"Model::__assert_unique_field_value",
|| {
let compiled = compiled.clone();
async move {
let backend = valence
.backend_for_table(<Self as valence::Model>::table_name())?;
backend.execute_compiled_query(&compiled).await
}
},
)
.await?;
for row in matches {
let matched_id = valence::extract_id_from_select_value(&row).map_err(|e| {
valence::Error::Internal(format!("Failed to parse unique check ID: {}", e))
})?;
if excluding_id.is_some_and(|id| id == matched_id) {
continue;
}
return Err(valence::Error::Validation(format!(
"Unique constraint violation on {}.{}",
<Self as valence::Model>::table_name(),
field_name
)));
}
Ok(())
}
}
}
fn entity_privacy_check_tokens() -> TokenStream {
quote! {
async fn check_read_privacy(&self, valence: &valence::Valence) -> valence::Result<()> {
let record = serde_json::json!(self);
valence::PrivacyEvaluator::check_entity_access(
Self::__schema_metadata(), valence::PrivacyOperation::Read, &record, valence,
)
.await
}
async fn check_create_privacy(&self, valence: &valence::Valence) -> valence::Result<()> {
let record = serde_json::json!(self);
valence::PrivacyEvaluator::check_entity_access(
Self::__schema_metadata(), valence::PrivacyOperation::Create, &record, valence,
)
.await
}
async fn check_update_privacy(&self, valence: &valence::Valence) -> valence::Result<()> {
let record = serde_json::json!(self);
valence::PrivacyEvaluator::check_entity_access(
Self::__schema_metadata(), valence::PrivacyOperation::Update, &record, valence,
)
.await
}
async fn check_delete_privacy(&self, valence: &valence::Valence) -> valence::Result<()> {
let record = serde_json::json!(self);
valence::PrivacyEvaluator::check_entity_access(
Self::__schema_metadata(), valence::PrivacyOperation::Delete, &record, valence,
)
.await
}
}
}
pub(super) fn emit_model_privacy_and_unique_impl(cx: &CrudEmitCtx<'_>) -> TokenStream {
let struct_name = &cx.struct_name;
let field_changes_name = &cx.field_changes_name;
let table_name_lit = cx.table_name_lit;
let unique_field_names = &cx.unique_field_names;
let meta = schema_metadata_fn_tokens(table_name_lit);
let unique = unique_constraint_helpers_tokens(unique_field_names);
let privacy = entity_privacy_check_tokens();
let update_with_before = model_update_with_before_inherent_tokens(field_changes_name);
quote! {
impl #struct_name {
#meta
#unique
#privacy
#update_with_before
}
}
}