Complete local-first content migration slice

This commit is contained in:
2026-08-05 16:24:04 -05:00
parent 993abb96df
commit f850bae77c
60 changed files with 2755 additions and 2729 deletions
+57 -9
View File
@@ -1,8 +1,9 @@
//! Proc macro for `#[derive(SettingsValue)]`.
//!
//! Generates `SettingsValue` implementations:
//! - **Enums**: variant names are converted to snake_case. Data-carrying
//! variants recursively call `to_file_value` on their inner data.
//! - **Enums**: variant names are converted to snake_case, Serde aliases are
//! accepted while reading, and data-carrying variants recursively call
//! `to_file_value` on their inner data.
//! - **Structs**: each field is serialized/deserialized by recursively calling
//! the trait methods. Field names use the Rust identifier (already snake_case)
//! unless overridden by `#[serde(rename = "...")]`.
@@ -22,6 +23,8 @@ use syn::{Data, DeriveInput, Fields, Lit, Meta, MetaNameValue, parse_macro_input
/// Unit variants are serialized as snake_case JSON strings. Data-carrying
/// variants (tuple or struct) are serialized as a single-key JSON object
/// `{ "snake_case_variant": <recursive value> }`.
/// `#[serde(alias = "...")]` names are accepted during deserialization and
/// normalized to the same snake_case file format.
///
/// # Structs
///
@@ -123,12 +126,16 @@ pub fn derive_settings_value(input: TokenStream) -> TokenStream {
let variant_ident = &variant.ident;
let cfg_attrs = get_cfg_attrs(&variant.attrs);
let file_name = file_variant_name(variant_ident, &variant.attrs, container_rename_all.as_deref());
let file_aliases = get_serde_aliases(&variant.attrs)
.into_iter()
.map(|alias| alias.to_case(Case::Snake))
.collect::<Vec<_>>();
match &variant.fields {
Fields::Unit => {
quote! {
#(#cfg_attrs)*
serde_json::Value::String(s) if s == #file_name => {
serde_json::Value::String(s) if s == #file_name #(|| s == #file_aliases)* => {
Some(#name::#variant_ident)
}
}
@@ -137,8 +144,12 @@ pub fn derive_settings_value(input: TokenStream) -> TokenStream {
let ty = &fields.unnamed.first().unwrap().ty;
quote! {
#(#cfg_attrs)*
serde_json::Value::Object(obj) if obj.contains_key(#file_name) => {
let inner_val = obj.get(#file_name)?;
serde_json::Value::Object(obj)
if obj.contains_key(#file_name) #(|| obj.contains_key(#file_aliases))* =>
{
let inner_val = [#file_name, #(#file_aliases),*]
.iter()
.find_map(|key| obj.get(*key))?;
let inner = <#ty as ::settings_value::SettingsValue>::from_file_value(inner_val)?;
Some(#name::#variant_ident(inner))
}
@@ -153,8 +164,13 @@ pub fn derive_settings_value(input: TokenStream) -> TokenStream {
.collect();
quote! {
#(#cfg_attrs)*
serde_json::Value::Object(obj) if obj.contains_key(#file_name) => {
let arr = obj.get(#file_name)?.as_array()?;
serde_json::Value::Object(obj)
if obj.contains_key(#file_name) #(|| obj.contains_key(#file_aliases))* =>
{
let arr = [#file_name, #(#file_aliases),*]
.iter()
.find_map(|key| obj.get(*key))?
.as_array()?;
Some(#name::#variant_ident(
#(
<#field_types as ::settings_value::SettingsValue>::from_file_value(arr.get(#field_indices)?)?
@@ -178,8 +194,13 @@ pub fn derive_settings_value(input: TokenStream) -> TokenStream {
.collect();
quote! {
#(#cfg_attrs)*
serde_json::Value::Object(obj) if obj.contains_key(#file_name) => {
let inner_obj = obj.get(#file_name)?.as_object()?;
serde_json::Value::Object(obj)
if obj.contains_key(#file_name) #(|| obj.contains_key(#file_aliases))* =>
{
let inner_obj = [#file_name, #(#file_aliases),*]
.iter()
.find_map(|key| obj.get(*key))?
.as_object()?;
Some(#name::#variant_ident {
#(
#field_idents: <#field_types as ::settings_value::SettingsValue>::from_file_value(inner_obj.get(#field_keys)?)?,
@@ -426,6 +447,33 @@ fn get_serde_rename(attrs: &[syn::Attribute]) -> Option<String> {
None
}
/// Reads every `#[serde(alias = "...")]` from field/variant attributes.
fn get_serde_aliases(attrs: &[syn::Attribute]) -> Vec<String> {
let mut aliases = Vec::new();
for attr in attrs {
if !attr.path().is_ident("serde") {
continue;
}
if let Ok(nested) = attr
.parse_args_with(syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated)
{
for meta in &nested {
if let Meta::NameValue(MetaNameValue {
path,
value: syn::Expr::Lit(expr_lit),
..
}) = meta
&& path.is_ident("alias")
&& let Lit::Str(s) = &expr_lit.lit
{
aliases.push(s.value());
}
}
}
}
aliases
}
/// Reads `#[serde(rename_all = "...")]` from container attributes.
fn get_serde_rename_all(attrs: &[syn::Attribute]) -> Option<String> {
for attr in attrs {