From 5dd8d1f5cd9fcc8f9fa44ef8f8b812e09f85c274 Mon Sep 17 00:00:00 2001 From: Patrick Cleavelin Date: Tue, 4 Nov 2025 19:56:47 -0600 Subject: [PATCH] write the dumbest macro I could think of --- service/Cargo.lock | 142 ++++++++---- service/Cargo.toml | 1 + service/src/lib/outbound/db_custom/mod.rs | 161 +++----------- .../src/lib/outbound/db_custom/write_set.rs | 209 ++++++++++++++++++ 4 files changed, 345 insertions(+), 168 deletions(-) diff --git a/service/Cargo.lock b/service/Cargo.lock index 1516f64..9b85a18 100644 --- a/service/Cargo.lock +++ b/service/Cargo.lock @@ -10,6 +10,7 @@ dependencies = [ "axum", "bincode", "dotenv", + "field_types", "reqwest", "serde", "serde_json", @@ -30,9 +31,9 @@ version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.103", + "quote 1.0.41", + "syn 2.0.108", ] [[package]] @@ -211,9 +212,9 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.103", + "quote 1.0.41", + "syn 2.0.108", ] [[package]] @@ -253,6 +254,17 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "field_types" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "152ff4498a8c01b91737676420a1cd84e92724cb610320fdebee6838e4651c9a" +dependencies = [ + "heck", + "quote 0.6.13", + "syn 0.15.44", +] + [[package]] name = "find-msvc-tools" version = "0.1.4" @@ -399,6 +411,15 @@ dependencies = [ "http", ] +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "http" version = "0.2.12" @@ -707,9 +728,9 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.103", + "quote 1.0.41", + "syn 2.0.108", ] [[package]] @@ -751,9 +772,9 @@ version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.103", + "quote 1.0.41", + "syn 2.0.108", ] [[package]] @@ -783,6 +804,15 @@ dependencies = [ "zerovec", ] +[[package]] +name = "proc-macro2" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" +dependencies = [ + "unicode-xid", +] + [[package]] name = "proc-macro2" version = "1.0.103" @@ -792,13 +822,22 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quote" +version = "0.6.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" +dependencies = [ + "proc-macro2 0.4.30", +] + [[package]] name = "quote" version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" dependencies = [ - "proc-macro2", + "proc-macro2 1.0.103", ] [[package]] @@ -938,9 +977,9 @@ version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.103", + "quote 1.0.41", + "syn 2.0.108", ] [[package]] @@ -1043,14 +1082,25 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "syn" +version = "0.15.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5" +dependencies = [ + "proc-macro2 0.4.30", + "quote 0.6.13", + "unicode-xid", +] + [[package]] name = "syn" version = "2.0.108" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.103", + "quote 1.0.41", "unicode-ident", ] @@ -1066,9 +1116,9 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.103", + "quote 1.0.41", + "syn 2.0.108", ] [[package]] @@ -1120,9 +1170,9 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.103", + "quote 1.0.41", + "syn 2.0.108", ] [[package]] @@ -1157,9 +1207,9 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.103", + "quote 1.0.41", + "syn 2.0.108", ] [[package]] @@ -1269,6 +1319,18 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-xid" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" + [[package]] name = "unty" version = "0.0.4" @@ -1367,7 +1429,7 @@ version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" dependencies = [ - "quote", + "quote 1.0.41", "wasm-bindgen-macro-support", ] @@ -1378,9 +1440,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" dependencies = [ "bumpalo", - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.103", + "quote 1.0.41", + "syn 2.0.108", "wasm-bindgen-shared", ] @@ -1670,9 +1732,9 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.103", + "quote 1.0.41", + "syn 2.0.108", "synstructure", ] @@ -1691,9 +1753,9 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.103", + "quote 1.0.41", + "syn 2.0.108", "synstructure", ] @@ -1725,7 +1787,7 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.103", + "quote 1.0.41", + "syn 2.0.108", ] diff --git a/service/Cargo.toml b/service/Cargo.toml index 1c68dfc..d8de35c 100644 --- a/service/Cargo.toml +++ b/service/Cargo.toml @@ -15,6 +15,7 @@ anyhow = "1.0.100" axum = { version = "0.6.9", features = ["headers"] } bincode = "2.0.1" dotenv = "0.15.0" +field_types = "1.1.0" reqwest = "0.11.14" serde = "1.0.152" serde_json = "1.0.93" diff --git a/service/src/lib/outbound/db_custom/mod.rs b/service/src/lib/outbound/db_custom/mod.rs index 29951cf..e417b12 100644 --- a/service/src/lib/outbound/db_custom/mod.rs +++ b/service/src/lib/outbound/db_custom/mod.rs @@ -9,7 +9,9 @@ use std::{ use crate::outbound::db_custom::write_set::{Diffable, Storable, Write, WriteOperation, WriteSet}; -#[derive(Default, Copy, Clone, PartialOrd, PartialEq, Eq, Hash, Debug)] +#[derive( + Default, Copy, Clone, PartialOrd, PartialEq, Eq, Hash, Debug, bincode::Encode, bincode::Decode, +)] struct TransactionId(u64); impl From for TransactionId { @@ -179,6 +181,9 @@ impl Database { self.log.push(Transaction::commit(timestamp, candidate)); self.build_snapshot(timestamp); + + let b = bincode::encode_to_vec(&self.log, bincode::config::standard()) + .expect("encode MUST succeed"); } } @@ -262,7 +267,7 @@ impl DatabaseAccessor { } } -#[derive(Debug)] +#[derive(Debug, bincode::Encode, bincode::Decode)] struct Transaction { id: TransactionId, write_set: WriteSet, @@ -496,21 +501,26 @@ impl Value { mod tests { use std::collections::HashSet; - use crate::outbound::db_custom::write_set::FieldDiff; + use crate::{db_type, outbound::db_custom::write_set::FieldDiff}; use super::*; - #[derive(Debug, Clone, bincode::Encode, bincode::Decode)] - struct MyTestData { - name: Name, - age: i64, - contacts: HashSet>, - } + db_type! { + struct MyTestData { + name: Name, + age: i64, + contacts: (HashSet>), + } - #[derive(Debug, Clone, bincode::Encode, bincode::Decode)] - struct Name { - first: String, - last: String, + struct Name { + first: String, + last: String, + nest: SuperNested, + } + + struct SuperNested { + i_am_a_really_nested_field: i64, + } } #[derive(bincode::Encode)] @@ -528,120 +538,6 @@ mod tests { } } - impl Storable for MyTestData { - fn as_full_write_op(&self) -> WriteOperation - where - Self: Sized, - { - let data = - bincode::encode_to_vec(A::with("MyTestData", self), bincode::config::standard()) - .expect("encode MUST succeed"); - - WriteOperation::Full(data) - } - - fn as_partial_write_op(&self, other: &Self) -> WriteOperation - where - Self: Sized, - { - WriteOperation::Partial(self.diff(other).into_iter().map(Into::into).collect()) - } - - fn apply_partial_write(&mut self, op: &write_set::PartialWrite) { - let value = Value::from_bytes(&op.data); - self.set_field(&op.field_ident, value); - } - - fn set_field(&mut self, field_ident: &str, value: Value) - where - Self: Sized, - { - match field_ident { - "name.first" => { - let Value::String(name) = value else { - panic!("expected 'name.first' to be a 'String'"); - }; - - self.name.first = name; - } - "name.last" => { - let Value::String(name) = value else { - panic!("expected 'name.last' to be a 'String'"); - }; - - self.name.last = name; - } - "age" => { - let Value::Int(age) = value else { - panic!("expected 'age' to be a 'Int'"); - }; - - self.age = age; - } - "contacts" => { - let Value::Array(contacts) = value else { - panic!("expected 'contacts' to be a 'Vec'"); - }; - - self.contacts = contacts.into_iter().map(|id| Id::<_>::new(id.0)).collect(); - } - _ => panic!("invalid field '{field_ident}'"), - }; - } - - fn field(&self, field_ident: &str) -> Option - where - Self: Sized, - { - match field_ident { - "name.first" => Some(Value::String(self.name.first.clone())), - "name.last" => Some(Value::String(self.name.last.clone())), - "age" => Some(Value::Int(self.age)), - "contacts" => Some(Value::Array( - self.contacts.iter().map(|id| id.into()).collect(), - )), - _ => None, - } - } - } - - impl Diffable for MyTestData { - fn diff(&self, other: &Self) -> Vec - where - Self: Sized, - { - let mut modifications = vec![]; - - if self.name.first != other.name.first { - modifications.push(FieldDiff::of( - "name.first", - Value::String(other.name.first.clone()), - )); - } - - if self.name.last != other.name.last { - modifications.push(FieldDiff::of( - "name.last", - Value::String(other.name.last.clone()), - )); - } - - if self.age != other.age { - modifications.push(FieldDiff::of("age", Value::Int(other.age))); - } - - let contacts_diff = self.contacts.difference(&other.contacts); - if contacts_diff.count() > 0 { - modifications.push(FieldDiff::of( - "contacts", - Value::Array(other.contacts.iter().map(|id| id.into()).collect()), - )); - } - - modifications - } - } - #[test] fn test() { let mut db = Database::default(); @@ -659,9 +555,12 @@ mod tests { name: Name { first: "John".to_string(), last: "Doe".to_string(), + nest: SuperNested { + i_am_a_really_nested_field: 67, + }, }, age: 42, - contacts: HashSet::new(), + contacts: vec![Id::new(1)].into_iter().collect(), }; println!("initial state: {:#?}", accessor.db.lock().unwrap()); @@ -674,6 +573,9 @@ mod tests { name: Name { first: "Oldy".to_string(), last: "McOlderton".to_string(), + nest: SuperNested { + i_am_a_really_nested_field: 32, + }, }, age: 69, contacts: vec![Id::new(1)].into_iter().collect(), @@ -691,6 +593,8 @@ mod tests { }; t.modify(Id::::new(1), |mut data| { + data.age = 9; + data.name.nest.i_am_a_really_nested_field = 9; data.name.last = format!("Gearbox {}", last_name_of_oldy); data }); @@ -700,6 +604,7 @@ mod tests { let id = Id::::new(1); accessor.transact(|t| { t.modify(id.clone(), |mut data| { + data.age = 9; data.name.last = "Not McOlderton Gearbox".to_string(); data diff --git a/service/src/lib/outbound/db_custom/write_set.rs b/service/src/lib/outbound/db_custom/write_set.rs index cdd3741..2b5d3fb 100644 --- a/service/src/lib/outbound/db_custom/write_set.rs +++ b/service/src/lib/outbound/db_custom/write_set.rs @@ -75,3 +75,212 @@ pub struct PartialWrite { pub(super) field_ident: String, pub(super) data: Vec, } + +#[macro_export] +macro_rules! primitive_assign { + ($self:ident, $field_name:ident, $value:ident, $ty:ty, $variant:ident) => {{ + let Value::$variant(value) = $value else { + panic!( + "expected '{}' to be a '{}'", + stringify!($field_name), + stringify!($ty) + ); + }; + + $self.$field_name = value; + }}; +} + +#[macro_export] +macro_rules! primitive_return { + ($self:ident, $field_name:ident, $variant:ident) => { + Some(Value::$variant($self.$field_name.clone())) + }; +} + +#[macro_export] +macro_rules! db_type { + ($self:ident, $field_name:ident, i64, $value:ident, $field_ident:ident) => { + $crate::primitive_assign!($self, $field_name, $value, i64, Int) + }; + ($self:ident, $field_name:ident, String, $value:ident, $field_ident:ident) => { + $crate::primitive_assign!($self, $field_name, $value, String, String) + }; + + ($self:ident, $field_name:ident, (HashSet>), $value:ident, $field_ident:ident) => { + { + let Value::Array(value) = $value else { + panic!( + "expected '{}' to be a 'Vec>'", + stringify!($field_name), + stringify!($ty) + ); + }; + + $self.$field_name = value.into_iter().map(|id| Id::<_>::new(id.0)).collect(); + } + }; + + ($self:ident, $field_name:ident, $field_type:ty, $value:ident, $field_ident:ident) => { + { + let field_name_len = stringify!($field_name).len(); + if field_name_len == $field_ident.len() { + panic!("attempt to assign non-primitive"); + } + + $self.$field_name.set_field(&$field_ident[(field_name_len+1)..], $value); + } + }; + + ($self:ident, $field_name:ident, i64, $field_ident:ident) => {{ + Some(Value::Int($self.$field_name.clone())) + }}; + ($self:ident, $field_name:ident, String, $field_ident:ident) => {{ + Some(Value::String($self.$field_name.clone())) + }}; + ($self:ident, $field_name:ident, (HashSet>), $field_ident:ident) => { + Some(Value::Array($self.$field_name.iter().map(|id| id.into()).collect())) + }; + ($self:ident, $field_name:ident, $field_type:ty, $field_ident:ident) => {{ + let field_name_len = stringify!($field_name).len(); + if field_name_len == $field_ident.len() { + panic!("attempt to assign non-primitive"); + } + + $self.$field_name.field(&$field_ident[(field_name_len+1)..]) + }}; + + (@diff $self:ident, $other:ident, $modifications:ident, $field_name:ident, i64) => { + if $self.$field_name != $other.$field_name { + $modifications.push(FieldDiff::of( + stringify!($field_name), + Value::Int($other.$field_name.clone()), + )); + } + }; + (@diff $self:ident, $other:ident, $modifications:ident, $field_name:ident, String) => { + if $self.$field_name != $other.$field_name { + $modifications.push(FieldDiff::of( + stringify!($field_name), + Value::String($other.$field_name.clone()), + )); + } + }; + (@diff $self:ident, $other:ident, $modifications:ident, $field_name:ident, (HashSet>)) => { + let diff = $self.$field_name.difference(&$other.$field_name); + if diff.count() > 0 { + $modifications.push(FieldDiff::of( + stringify!($field_name), + Value::Array($other.$field_name.iter().map(|id| id.into()).collect()) + )); + } + }; + (@diff $self:ident, $other:ident, $modifications:ident, $field_name:ident, $field_type:ty) => { + $modifications.extend( + $self.$field_name + .diff(&$other.$field_name) + .into_iter() + .map(|diff| { + // NOTE(pcleavelin): we have to recreate the `field_ident` path via recursive + // string building lol + FieldDiff::of( + format!("{}.{}", stringify!($field_name), &diff.field_ident), + diff.value + ) + }) + ); + }; + + { + $( + struct $db_type_name:ident { + $( + $field_name:ident: $field_type:tt, + )* + } + )* + } => { + $( + #[derive(Debug, Clone, ::bincode::Encode, ::bincode::Decode)] + pub struct $db_type_name { + $( + $field_name: $field_type, + )* + } + + impl Storable for $db_type_name { + fn as_full_write_op(&self) -> WriteOperation + where + Self: Sized, + { + let data = + bincode::encode_to_vec(A::with(stringify!($db_type_name), self), bincode::config::standard()) + .expect("encode MUST succeed"); + + WriteOperation::Full(data) + } + fn as_partial_write_op(&self, other: &Self) -> WriteOperation + where + Self: Sized, + { + WriteOperation::Partial(self.diff(other).into_iter().map(Into::into).collect()) + } + + fn apply_partial_write(&mut self, op: &write_set::PartialWrite) { + let value = Value::from_bytes(&op.data); + self.set_field(&op.field_ident, value); + } + + fn set_field(&mut self, field_ident: &str, value: Value) + where Self: Sized, + { + let mut idents = field_ident.split("."); + let next = idents.next().unwrap(); + + match next { + $( + stringify!($field_name) => { + db_type!(self, $field_name, $field_type, value, field_ident) + } + ),* + + _ => panic!("set_field: invalid field '{next}'"), + } + } + + fn field(&self, field_ident: &str) -> Option + where + Self: Sized, + { + let mut idents = field_ident.split("."); + let next = idents.next().unwrap(); + + match next { + $( + stringify!($field_name) => { + db_type!(self, $field_name, $field_type, field_ident) + } + ),* + + _ => panic!("field: invalid field '{next}'"), + } + } + } + + impl Diffable for $db_type_name { + fn diff(&self, other: &Self) -> Vec + where + Self: Sized, + { + let mut modifications = vec![]; + + $( + db_type!(@diff self, other, modifications, $field_name, $field_type); + )* + + modifications + } + } + )* + }; +}