├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── README.md ├── autosurgeon-derive ├── Cargo.toml ├── src │ ├── attrs.rs │ ├── hydrate.rs │ ├── hydrate │ │ ├── named_field.rs │ │ ├── newtype_field.rs │ │ ├── unnamed_field.rs │ │ └── variant_fields.rs │ ├── lib.rs │ ├── reconcile.rs │ └── reconcile │ │ ├── enum_impl.rs │ │ ├── field_wrapper.rs │ │ └── struct_impl.rs └── tests │ ├── hydrate.rs │ ├── hydrate_with.rs │ ├── reconcile.rs │ ├── reconcile_with.rs │ ├── reconcile_with_hydrate_with_key.rs │ └── with.rs ├── autosurgeon ├── Cargo.toml └── src │ ├── bytes.rs │ ├── counter.rs │ ├── doc.rs │ ├── hydrate.rs │ ├── hydrate │ ├── impls.rs │ └── map.rs │ ├── lib.rs │ ├── path.rs │ ├── prop.rs │ ├── reconcile.rs │ ├── reconcile │ ├── impls.rs │ ├── map.rs │ └── seq.rs │ ├── text.rs │ └── uuid.rs ├── deny.toml ├── rustfmt.toml └── scripts └── ci ├── build-test ├── clippy ├── fmt └── run /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | 10 | jobs: 11 | fmt: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions-rs/toolchain@v1 16 | with: 17 | profile: minimal 18 | toolchain: 1.65.0 19 | default: true 20 | components: rustfmt 21 | - uses: Swatinem/rust-cache@v2 22 | - run: ./scripts/ci/fmt 23 | shell: bash 24 | 25 | clippy: 26 | runs-on: ubuntu-latest 27 | steps: 28 | - uses: actions/checkout@v3 29 | - uses: actions-rs/toolchain@v1 30 | with: 31 | profile: minimal 32 | toolchain: 1.65.0 33 | default: true 34 | components: clippy 35 | - uses: Swatinem/rust-cache@v2 36 | - run: ./scripts/ci/clippy 37 | shell: bash 38 | 39 | cargo-deny: 40 | runs-on: ubuntu-latest 41 | strategy: 42 | matrix: 43 | checks: 44 | - advisories 45 | - bans licenses sources 46 | continue-on-error: ${{ matrix.checks == 'advisories' }} 47 | steps: 48 | - uses: actions/checkout@v3 49 | - uses: EmbarkStudios/cargo-deny-action@v1 50 | with: 51 | arguments: '--manifest-path ./Cargo.toml' 52 | command: check ${{ matrix.checks }} 53 | 54 | linux: 55 | runs-on: ubuntu-latest 56 | strategy: 57 | matrix: 58 | toolchain: 59 | - 1.65.0 60 | - nightly 61 | continue-on-error: ${{ matrix.toolchain == 'nightly' }} 62 | steps: 63 | - uses: actions/checkout@v3 64 | - uses: actions-rs/toolchain@v1 65 | with: 66 | profile: minimal 67 | toolchain: ${{ matrix.toolchain }} 68 | default: true 69 | - uses: Swatinem/rust-cache@v2 70 | - run: ./scripts/ci/build-test 71 | shell: bash 72 | 73 | macos: 74 | runs-on: macos-latest 75 | steps: 76 | - uses: actions/checkout@v3 77 | - uses: actions-rs/toolchain@v1 78 | with: 79 | profile: minimal 80 | toolchain: 1.65.0 81 | default: true 82 | - uses: Swatinem/rust-cache@v2 83 | - run: ./scripts/ci/build-test 84 | shell: bash 85 | 86 | windows: 87 | runs-on: windows-latest 88 | steps: 89 | - uses: actions/checkout@v3 90 | - uses: actions-rs/toolchain@v1 91 | with: 92 | profile: minimal 93 | toolchain: 1.65.0 94 | default: true 95 | - uses: Swatinem/rust-cache@v2 96 | - run: ./scripts/ci/build-test 97 | shell: bash 98 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## `0.2.1` 2 | 3 | * Add `Hydrate` for HashMap and BTreeMap 4 | * Fix hydrate_path failing to hydrate some items correctly 5 | * Add implementations of Reconcile and Hydrate for Uuid behind the `uuid` feature flag 6 | 7 | ## `0.2.0` 8 | 9 | * **BREAKING** Remove implementation of `Hydrate` for `Vec` 10 | * Add `ByteArray` and `ByteVec` wrappers for `[u8; N]` and `Vec 11 | * Add an implementation of `Hdyrate` for `u8` 12 | * Accept any `Doc` in `reconcile_prop` 13 | 14 | ## `0.1.1` 15 | 16 | * Fix visibility of key types for derived `Reconcile` on enum types not 17 | matching the visibility of the target enum type 18 | 19 | ## `0.1.0` 20 | 21 | Initial release 22 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | members = [ 4 | "autosurgeon", 5 | "autosurgeon-derive", 6 | ] 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 Alex Good 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `autosurgeon` 2 | 3 | [![Build](https://github.com/alexjg/autosurgeon/actions/workflows/ci.yaml/badge.svg)](https://github.com/alexjg/autosurgeon/actions/workflows/ci.yaml) 4 | [![crates](https://img.shields.io/crates/v/autosurgeon)](https://crates.io/crates/autosurgeon) 5 | [![docs](https://img.shields.io/docsrs/autosurgeon?color=blue)](https://docs.rs/autosurgeon/latest/autosurgeon/) 6 | 7 | Autosurgeon is a Rust library for working with data in 8 | [automerge](https://automerge.org/) documents. See the [documentation](https://docs.rs/autosurgeon/latest/autosurgeon/) for a detailed guide. 9 | 10 | 11 | ## Quickstart 12 | 13 | `autosurgeon` requires rust `1.65` or newer. 14 | 15 | Add `autosurgeon` to your dependencies with `cargo add` 16 | 17 | ```shell 18 | cargo add autosurgeon 19 | ``` 20 | 21 | Then we can define a data model which derives `Reconcile` and `Hydrate` and 22 | start reading and writing from automerge documents 23 | 24 | ```rust 25 | use autosurgeon::{Reconcile, Hydrate, hydrate, reconcile}; 26 | 27 | // A simple contact document 28 | 29 | #[derive(Debug, Clone, Reconcile, Hydrate, PartialEq)] 30 | struct Contact { 31 | name: String, 32 | address: Address, 33 | } 34 | 35 | #[derive(Debug, Clone, Reconcile, Hydrate, PartialEq)] 36 | struct Address { 37 | line_one: String, 38 | line_two: Option, 39 | city: String, 40 | postcode: String, 41 | } 42 | 43 | let mut contact = Contact { 44 | name: "Sherlock Holmes".to_string(), 45 | address: Address{ 46 | line_one: "221B Baker St".to_string(), 47 | line_two: None, 48 | city: "London".to_string(), 49 | postcode: "NW1 6XE".to_string(), 50 | }, 51 | }; 52 | 53 | // Put data into a document 54 | let mut doc = automerge::AutoCommit::new(); 55 | reconcile(&mut doc, &contact).unwrap(); 56 | 57 | // Get data out of a document 58 | let contact2: Contact = hydrate(&doc).unwrap(); 59 | assert_eq!(contact, contact2); 60 | 61 | // Fork and make changes 62 | let mut doc2 = doc.fork().with_actor(automerge::ActorId::random()); 63 | let mut contact2: Contact = hydrate(&doc2).unwrap(); 64 | contact2.name = "Dangermouse".to_string(); 65 | reconcile(&mut doc2, &contact2).unwrap(); 66 | 67 | // Concurrently on doc1 68 | contact.address.line_one = "221C Baker St".to_string(); 69 | reconcile(&mut doc, &contact).unwrap(); 70 | 71 | // Now merge the documents 72 | // Reconciled changes will merge in somewhat sensible ways 73 | doc.merge(&mut doc2).unwrap(); 74 | 75 | let merged: Contact = hydrate(&doc).unwrap(); 76 | assert_eq!(merged, Contact { 77 | name: "Dangermouse".to_string(), // This was updated in the first doc 78 | address: Address { 79 | line_one: "221C Baker St".to_string(), // This was concurrently updated in doc2 80 | line_two: None, 81 | city: "London".to_string(), 82 | postcode: "NW1 6XE".to_string(), 83 | } 84 | }) 85 | ``` 86 | -------------------------------------------------------------------------------- /autosurgeon-derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "autosurgeon-derive" 3 | description = "Procedural macros for implementing autosurgeon" 4 | version = "0.4.0" 5 | edition = "2021" 6 | authors = ["Alex Good "] 7 | license = "MIT" 8 | rust-version = "1.65.0" 9 | repository = "https://github.com/alexjg/autosurgeon" 10 | 11 | [lib] 12 | proc_macro = true 13 | 14 | [dependencies] 15 | quote = "1.0.21" 16 | syn = "1.0.103" 17 | proc-macro2 = "1.0" 18 | smol_str = { version = "^0.1.21" } 19 | thiserror = "1.0.37" 20 | 21 | [dev-dependencies] 22 | autosurgeon = { path = "../autosurgeon" } 23 | automerge = { version = "^0.3" } 24 | automerge-test = "^0.2" 25 | -------------------------------------------------------------------------------- /autosurgeon-derive/src/hydrate.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Span, TokenStream}; 2 | use quote::{quote, quote_spanned}; 3 | use syn::{ 4 | parse_macro_input, parse_quote, spanned::Spanned, DeriveInput, Fields, GenericParam, Generics, 5 | }; 6 | 7 | use crate::attrs; 8 | mod named_field; 9 | mod newtype_field; 10 | mod unnamed_field; 11 | mod variant_fields; 12 | 13 | pub fn derive_hydrate(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 14 | let input = parse_macro_input!(input as DeriveInput); 15 | 16 | let container_attrs = match attrs::Container::from_attrs(input.attrs.iter()) { 17 | Ok(a) => a.unwrap_or_default(), 18 | Err(e) => { 19 | return proc_macro::TokenStream::from( 20 | syn::Error::new(input.span(), e.to_string()).into_compile_error(), 21 | ); 22 | } 23 | }; 24 | 25 | if let Some(hydrate_with) = container_attrs.hydrate_with() { 26 | return proc_macro::TokenStream::from(on_hydrate_with(&input, &hydrate_with)); 27 | } 28 | 29 | let result = match &input.data { 30 | syn::Data::Struct(datastruct) => on_struct(&input, datastruct), 31 | syn::Data::Enum(dataenum) => on_enum(&input, dataenum), 32 | _ => todo!(), 33 | }; 34 | let tokens = match result { 35 | Ok(t) => t, 36 | Err(e) => syn::Error::new(e.span().unwrap_or_else(|| input.span()), e.to_string()) 37 | .to_compile_error(), 38 | }; 39 | 40 | proc_macro::TokenStream::from(tokens) 41 | } 42 | 43 | fn add_trait_bounds(mut generics: Generics) -> Generics { 44 | for param in &mut generics.params { 45 | if let GenericParam::Type(ref mut type_param) = *param { 46 | type_param.bounds.push(parse_quote!(autosurgeon::Hydrate)); 47 | } 48 | } 49 | generics 50 | } 51 | 52 | fn on_hydrate_with(input: &DeriveInput, hydrate_with: &TokenStream) -> TokenStream { 53 | let generics = add_trait_bounds(input.generics.clone()); 54 | let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); 55 | let name = &input.ident; 56 | 57 | quote! { 58 | impl #impl_generics autosurgeon::Hydrate for #name #ty_generics #where_clause { 59 | fn hydrate<'a, D: autosurgeon::ReadDoc>( 60 | doc: &D, 61 | obj: &automerge::ObjId, 62 | prop: autosurgeon::Prop<'a>, 63 | ) -> Result { 64 | #hydrate_with(doc, obj, prop) 65 | } 66 | } 67 | } 68 | } 69 | 70 | fn on_struct( 71 | input: &DeriveInput, 72 | datastruct: &syn::DataStruct, 73 | ) -> Result { 74 | let name = &input.ident; 75 | 76 | let generics = add_trait_bounds(input.generics.clone()); 77 | 78 | let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); 79 | 80 | match datastruct.fields { 81 | Fields::Named(ref fields) => { 82 | let fields = fields 83 | .named 84 | .iter() 85 | .map(|field| { 86 | named_field::NamedField::new(field, field.ident.as_ref().unwrap()) 87 | .map_err(|e| error::DeriveError::InvalidFieldAttrs(e, field.clone())) 88 | }) 89 | .collect::, _>>()?; 90 | let the_impl = gen_named_struct_impl(name, &fields); 91 | 92 | Ok(quote! { 93 | impl #impl_generics autosurgeon::Hydrate for #name #ty_generics #where_clause { 94 | #the_impl 95 | } 96 | }) 97 | } 98 | Fields::Unnamed(ref fields) => { 99 | if fields.unnamed.len() == 1 { 100 | Ok(gen_newtype_struct_wrapper(input, fields, &generics)?) 101 | } else { 102 | gen_tuple_struct_wrapper(input, fields, &generics) 103 | } 104 | } 105 | Fields::Unit => Err(error::DeriveError::HydrateForUnit), 106 | } 107 | } 108 | 109 | fn on_enum( 110 | input: &DeriveInput, 111 | enumstruct: &syn::DataEnum, 112 | ) -> Result { 113 | let name = &input.ident; 114 | 115 | let generics = add_trait_bounds(input.generics.clone()); 116 | 117 | let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); 118 | 119 | let unit_fields = EnumUnitFields::new(name, enumstruct); 120 | let named_fields = EnumAsMapFields::new(name, enumstruct)?; 121 | 122 | let hydrate_string = unit_fields.hydrate_string(); 123 | let hydrate_map = named_fields.hydrate_map(); 124 | 125 | Ok(quote! { 126 | impl #impl_generics autosurgeon::Hydrate for #name #ty_generics 127 | #where_clause 128 | { 129 | #hydrate_string 130 | 131 | #hydrate_map 132 | } 133 | }) 134 | } 135 | 136 | struct EnumUnitFields<'a> { 137 | ty: &'a syn::Ident, 138 | fields: Vec<&'a syn::Ident>, 139 | } 140 | 141 | impl<'a> EnumUnitFields<'a> { 142 | fn new(ty: &'a syn::Ident, data: &'a syn::DataEnum) -> Self { 143 | Self { 144 | ty, 145 | fields: data 146 | .variants 147 | .iter() 148 | .filter_map(|f| match f.fields { 149 | Fields::Unit => Some(&f.ident), 150 | _ => None, 151 | }) 152 | .collect(), 153 | } 154 | } 155 | 156 | fn branches(&self) -> TokenStream { 157 | let ty = self.ty; 158 | let branches = self.fields.iter().map(|i| { 159 | let branch_name = i.to_string(); 160 | quote!(#branch_name => Ok(#ty::#i)) 161 | }); 162 | quote!(#(#branches),*) 163 | } 164 | 165 | fn expected(&self) -> TokenStream { 166 | let names = self.fields.iter().map(|f| format!("{}", f)); 167 | let expected = quote!(One of (#(#names),*)).to_string(); 168 | quote!(#expected) 169 | } 170 | 171 | fn hydrate_string(&self) -> TokenStream { 172 | if self.fields.is_empty() { 173 | quote!() 174 | } else { 175 | let unit_branches = self.branches(); 176 | let unit_error = self.expected(); 177 | 178 | quote! { 179 | fn hydrate_string( 180 | val: &'_ str 181 | ) -> Result { 182 | match val { 183 | #unit_branches, 184 | other => Err(autosurgeon::HydrateError::unexpected( 185 | #unit_error, 186 | other.to_string(), 187 | )), 188 | } 189 | } 190 | } 191 | } 192 | } 193 | } 194 | 195 | struct EnumAsMapFields<'a> { 196 | ty: &'a syn::Ident, 197 | variants: Vec>, 198 | } 199 | 200 | impl<'a> EnumAsMapFields<'a> { 201 | fn new(ty: &'a syn::Ident, data: &'a syn::DataEnum) -> Result { 202 | let variants = data 203 | .variants 204 | .iter() 205 | .filter_map(|v| variant_fields::Variant::from_variant(v).transpose()) 206 | .collect::, _>>()?; 207 | Ok(Self { ty, variants }) 208 | } 209 | 210 | fn hydrate_map(&self) -> TokenStream { 211 | if self.variants.is_empty() { 212 | quote!() 213 | } else { 214 | let stanzas = self.variants.iter().map(|v| v.visitor_def(self.ty)); 215 | quote! { 216 | fn hydrate_map( 217 | doc: &D, 218 | obj: &automerge::ObjId, 219 | ) -> Result { 220 | #(#stanzas)* 221 | Err(autosurgeon::HydrateError::unexpected( 222 | "A map with one key", 223 | "something else".to_string(), 224 | )) 225 | } 226 | } 227 | } 228 | } 229 | } 230 | 231 | fn gen_named_struct_impl(name: &syn::Ident, fields: &[named_field::NamedField]) -> TokenStream { 232 | let obj_ident = syn::Ident::new("obj", Span::mixed_site()); 233 | let field_hydrators = fields.iter().map(|f| f.hydrator(&obj_ident)); 234 | 235 | let field_initializers = fields.iter().map(|f| f.initializer()); 236 | 237 | quote! { 238 | fn hydrate_map(doc: &D, #obj_ident: &automerge::ObjId) -> Result { 239 | #(#field_hydrators)* 240 | Ok(#name { 241 | #(#field_initializers),* 242 | }) 243 | } 244 | } 245 | } 246 | 247 | fn gen_newtype_struct_wrapper( 248 | input: &DeriveInput, 249 | fields: &syn::FieldsUnnamed, 250 | generics: &syn::Generics, 251 | ) -> Result { 252 | let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); 253 | 254 | let field = fields.unnamed.first().unwrap(); 255 | let attrs = attrs::Field::from_field(field) 256 | .map_err(|e| error::DeriveError::InvalidFieldAttrs(e, field.clone()))? 257 | .unwrap_or_default(); 258 | let ty = &input.ident; 259 | 260 | let inner_ty = &field.ty; 261 | 262 | let inner_ty = quote_spanned!(field.span() => #inner_ty); 263 | 264 | if let Some(hydrate_with) = attrs.hydrate_with().map(|h| h.hydrate_with()) { 265 | Ok(quote! { 266 | impl #impl_generics autosurgeon::hydrate::Hydrate for #ty #ty_generics #where_clause { 267 | fn hydrate<'a, D: autosurgeon::ReadDoc>( 268 | doc: &D, 269 | obj: &automerge::ObjId, 270 | prop: autosurgeon::Prop<'a>, 271 | ) -> Result { 272 | let inner = #hydrate_with(doc, obj, prop)?; 273 | Ok(#ty(inner)) 274 | } 275 | } 276 | }) 277 | } else { 278 | Ok(quote! { 279 | impl #impl_generics autosurgeon::hydrate::Hydrate for #ty #ty_generics #where_clause { 280 | fn hydrate<'a, D: autosurgeon::ReadDoc>( 281 | doc: &D, 282 | obj: &automerge::ObjId, 283 | prop: autosurgeon::Prop<'a>, 284 | ) -> Result { 285 | let inner = #inner_ty::hydrate(doc, obj, prop)?; 286 | Ok(#ty(inner)) 287 | } 288 | } 289 | }) 290 | } 291 | } 292 | 293 | fn gen_tuple_struct_wrapper( 294 | input: &DeriveInput, 295 | fields: &syn::FieldsUnnamed, 296 | generics: &syn::Generics, 297 | ) -> Result { 298 | let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); 299 | let name = &input.ident; 300 | 301 | let fields = fields 302 | .unnamed 303 | .iter() 304 | .enumerate() 305 | .map(|(i, f)| { 306 | unnamed_field::UnnamedField::new(f, i) 307 | .map_err(|e| error::DeriveError::InvalidFieldAttrs(e, f.clone())) 308 | }) 309 | .collect::, _>>()?; 310 | 311 | let obj_ident = syn::Ident::new("obj", Span::mixed_site()); 312 | let field_hydrators = fields.iter().map(|f| f.hydrator(&obj_ident)); 313 | let field_initializers = fields.iter().map(|f| f.initializer()); 314 | 315 | Ok(quote! { 316 | impl #impl_generics autosurgeon::Hydrate for #name #ty_generics #where_clause { 317 | fn hydrate_seq(doc: &D, #obj_ident: &automerge::ObjId) -> Result { 318 | #(#field_hydrators)* 319 | Ok(#name ( 320 | #(#field_initializers),* 321 | )) 322 | } 323 | } 324 | }) 325 | } 326 | 327 | mod error { 328 | use proc_macro2::Span; 329 | use syn::spanned::Spanned; 330 | 331 | use crate::attrs; 332 | 333 | #[derive(Debug, thiserror::Error)] 334 | pub(crate) enum DeriveError { 335 | #[error("{0}")] 336 | InvalidFieldAttrs(attrs::error::InvalidFieldAttrs, syn::Field), 337 | #[error("cannot derive hydrate for unit struct")] 338 | HydrateForUnit, 339 | } 340 | 341 | impl DeriveError { 342 | pub(crate) fn span(&self) -> Option { 343 | match self { 344 | Self::InvalidFieldAttrs(e, f) => Some(e.span().unwrap_or_else(|| f.span())), 345 | Self::HydrateForUnit => None, 346 | } 347 | } 348 | } 349 | } 350 | -------------------------------------------------------------------------------- /autosurgeon-derive/src/hydrate/named_field.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::{format_ident, quote, quote_spanned}; 3 | use syn::spanned::Spanned; 4 | 5 | use crate::attrs; 6 | 7 | pub(crate) struct NamedField<'a> { 8 | field: syn::Field, 9 | name: &'a syn::Ident, 10 | attrs: attrs::Field, 11 | } 12 | 13 | impl<'a> NamedField<'a> { 14 | pub(crate) fn new( 15 | syn_field: &'a syn::Field, 16 | name: &'a syn::Ident, 17 | ) -> Result { 18 | let attrs = attrs::Field::from_field(syn_field)?.unwrap_or_default(); 19 | Ok(Self { 20 | field: syn_field.clone(), 21 | attrs, 22 | name, 23 | }) 24 | } 25 | 26 | pub(crate) fn hydrator(&self, obj_ident: &syn::Ident) -> TokenStream { 27 | let name = &self.name; 28 | let string_name = format_ident!("{}", name).to_string(); 29 | if let Some(hydrate_with) = self.attrs.hydrate_with() { 30 | let function_name = hydrate_with.hydrate_with(); 31 | quote! { 32 | let #name = #function_name(doc, &#obj_ident, #string_name.into())?; 33 | } 34 | } else { 35 | quote_spanned!(self.field.span() => let #name = autosurgeon::hydrate_prop(doc, &#obj_ident, #string_name)?;) 36 | } 37 | } 38 | 39 | pub(crate) fn initializer(&self) -> TokenStream { 40 | let name = &self.name; 41 | quote! {#name} 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /autosurgeon-derive/src/hydrate/newtype_field.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::{quote, quote_spanned, ToTokens}; 3 | use syn::spanned::Spanned; 4 | 5 | use crate::attrs; 6 | 7 | pub(crate) struct NewtypeField<'a> { 8 | field: &'a syn::Field, 9 | attrs: attrs::Field, 10 | } 11 | 12 | impl<'a> NewtypeField<'a> { 13 | pub(crate) fn from_field( 14 | field: &'a syn::Field, 15 | ) -> Result { 16 | let attrs = attrs::Field::from_field(field)?.unwrap_or_default(); 17 | Ok(Self { field, attrs }) 18 | } 19 | 20 | /// Generate a stream like `let #target = ` 21 | pub(crate) fn hydrate_into( 22 | &self, 23 | target: &syn::Ident, 24 | prop_ident: T, 25 | ) -> TokenStream { 26 | if let Some(hydrate_with) = self.attrs.hydrate_with() { 27 | let hydrate_func = hydrate_with.hydrate_with(); 28 | quote! { 29 | let #target = #hydrate_func(doc, obj, #prop_ident.into())?; 30 | } 31 | } else { 32 | quote_spanned! {self.field.span() => 33 | let #target = autosurgeon::hydrate_prop(doc, obj, #prop_ident)?; 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /autosurgeon-derive/src/hydrate/unnamed_field.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::{format_ident, quote, quote_spanned}; 3 | use syn::spanned::Spanned; 4 | 5 | use crate::attrs; 6 | 7 | pub(crate) struct UnnamedField { 8 | field: syn::Field, 9 | attrs: attrs::Field, 10 | index: usize, 11 | } 12 | 13 | impl UnnamedField { 14 | pub(crate) fn new( 15 | field: &syn::Field, 16 | index: usize, 17 | ) -> Result { 18 | let attrs = attrs::Field::from_field(field)?.unwrap_or_default(); 19 | Ok(Self { 20 | field: field.clone(), 21 | attrs, 22 | index, 23 | }) 24 | } 25 | 26 | pub(crate) fn hydrator(&self, obj_ident: &syn::Ident) -> TokenStream { 27 | let name = self.name(); 28 | let idx = self.index; 29 | if let Some(hydrate_with) = self.attrs.hydrate_with() { 30 | let function_name = hydrate_with.hydrate_with(); 31 | quote! { 32 | let #name = #function_name(doc, &#obj_ident, #idx.into())?; 33 | } 34 | } else { 35 | quote_spanned!(self.field.span() => let #name = autosurgeon::hydrate_prop(doc, &#obj_ident, #idx)?;) 36 | } 37 | } 38 | 39 | pub(crate) fn initializer(&self) -> TokenStream { 40 | let name = self.name(); 41 | quote! { #name } 42 | } 43 | 44 | fn name(&self) -> syn::Ident { 45 | format_ident!("field_{}", self.index) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /autosurgeon-derive/src/hydrate/variant_fields.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Span, TokenStream}; 2 | use quote::{format_ident, quote}; 3 | 4 | use super::{ 5 | error::DeriveError, named_field::NamedField, newtype_field::NewtypeField, 6 | unnamed_field::UnnamedField, 7 | }; 8 | 9 | pub(crate) struct Variant<'a> { 10 | ident: &'a syn::Ident, 11 | fields: VariantFields<'a>, 12 | } 13 | 14 | impl<'a> Variant<'a> { 15 | pub(crate) fn visitor_def(&self, outer_ty: &syn::Ident) -> TokenStream { 16 | self.fields.visitor_def(outer_ty, self.ident) 17 | } 18 | 19 | pub(crate) fn from_variant(variant: &'a syn::Variant) -> Result, DeriveError> { 20 | let fields = match &variant.fields { 21 | syn::Fields::Named(nf) => VariantFields::Named( 22 | nf.named 23 | .iter() 24 | .map(|f| { 25 | NamedField::new(f, f.ident.as_ref().unwrap()) 26 | .map_err(|e| DeriveError::InvalidFieldAttrs(e, f.clone())) 27 | }) 28 | .collect::, _>>()?, 29 | ), 30 | syn::Fields::Unnamed(uf) => { 31 | if uf.unnamed.len() == 1 { 32 | let f = uf.unnamed.first().unwrap(); 33 | let field = NewtypeField::from_field(f) 34 | .map_err(|e| DeriveError::InvalidFieldAttrs(e, f.clone()))?; 35 | VariantFields::NewType(field) 36 | } else { 37 | VariantFields::Unnamed( 38 | uf.unnamed 39 | .iter() 40 | .enumerate() 41 | .map(|(i, f)| { 42 | UnnamedField::new(f, i) 43 | .map_err(|e| DeriveError::InvalidFieldAttrs(e, f.clone())) 44 | }) 45 | .collect::, _>>()?, 46 | ) 47 | } 48 | } 49 | syn::Fields::Unit => return Ok(None), 50 | }; 51 | Ok(Some(Self { 52 | ident: &variant.ident, 53 | fields, 54 | })) 55 | } 56 | } 57 | 58 | enum VariantFields<'a> { 59 | Named(Vec>), 60 | Unnamed(Vec), 61 | NewType(NewtypeField<'a>), 62 | } 63 | 64 | impl<'a> VariantFields<'a> { 65 | fn visitor_def(&self, outer_ty: &syn::Ident, variant_name: &'a syn::Ident) -> TokenStream { 66 | match self { 67 | Self::Named(fields) => named_field_variant_stanza(outer_ty, variant_name, fields), 68 | Self::Unnamed(fields) => unnamed_field_variant_stanza(outer_ty, variant_name, fields), 69 | Self::NewType(field) => newtype_field_variant_stanza(outer_ty, variant_name, field), 70 | } 71 | } 72 | } 73 | 74 | fn newtype_field_variant_stanza( 75 | outer_ty: &syn::Ident, 76 | variant_name: &syn::Ident, 77 | field: &NewtypeField, 78 | ) -> TokenStream { 79 | let ty = outer_ty; 80 | 81 | let name = syn::Ident::new("field_0", proc_macro2::Span::mixed_site()); 82 | let variant_name_str = format_ident!("{}", variant_name).to_string(); 83 | 84 | let hydrator = field.hydrate_into(&name, &variant_name_str); 85 | quote! { 86 | if doc.get(obj, #variant_name_str)?.is_some() { 87 | #hydrator 88 | //let #name = autosurgeon::hydrate_prop(doc, obj, #variant_name_str)?; 89 | return Ok(#ty::#variant_name(#name)) 90 | } 91 | } 92 | } 93 | 94 | fn named_field_variant_stanza( 95 | outer_ty: &syn::Ident, 96 | variant_name: &syn::Ident, 97 | fields: &[NamedField<'_>], 98 | ) -> TokenStream { 99 | let ty = outer_ty; 100 | 101 | let variant_name_str = variant_name.to_string(); 102 | let obj_ident = syn::Ident::new("id", Span::mixed_site()); 103 | let field_hydrators = fields.iter().map(|f| f.hydrator(&obj_ident)); 104 | let field_initializers = fields.iter().map(|f| f.initializer()); 105 | 106 | quote! { 107 | if let Some((val, #obj_ident)) = doc.get(obj, #variant_name_str)? { 108 | if matches!(val, automerge::Value::Object(automerge::ObjType::Map)) { 109 | #(#field_hydrators)* 110 | return Ok(#ty::#variant_name { 111 | #(#field_initializers),* 112 | }) 113 | } 114 | } 115 | } 116 | } 117 | 118 | fn unnamed_field_variant_stanza( 119 | outer_ty: &syn::Ident, 120 | variant_name: &syn::Ident, 121 | fields: &[UnnamedField], 122 | ) -> TokenStream { 123 | let ty = outer_ty; 124 | 125 | let obj_ident = syn::Ident::new("id", Span::mixed_site()); 126 | let hydrators = fields.iter().map(|f| f.hydrator(&obj_ident)); 127 | let initializers = fields.iter().map(|f| f.initializer()); 128 | 129 | let variant_name_str = variant_name.to_string(); 130 | quote! { 131 | if let Some((val, #obj_ident)) = doc.get(obj, #variant_name_str)? { 132 | if matches!(val, automerge::Value::Object(automerge::ObjType::List)) { 133 | #(#hydrators)* 134 | return Ok(#ty::#variant_name(#(#initializers),*)) 135 | } 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /autosurgeon-derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod attrs; 2 | mod hydrate; 3 | mod reconcile; 4 | 5 | #[proc_macro_derive(Hydrate, attributes(autosurgeon))] 6 | pub fn derive_hydrate(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 7 | hydrate::derive_hydrate(input) 8 | } 9 | 10 | #[proc_macro_derive(Reconcile, attributes(key, autosurgeon))] 11 | pub fn derive_reconcile(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 12 | reconcile::derive_reconcile(input) 13 | } 14 | -------------------------------------------------------------------------------- /autosurgeon-derive/src/reconcile.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Span, TokenStream}; 2 | use quote::{format_ident, quote, quote_spanned}; 3 | use syn::{ 4 | parse_macro_input, parse_quote, spanned::Spanned, Data, DeriveInput, Fields, GenericParam, 5 | Generics, 6 | }; 7 | 8 | use crate::attrs; 9 | mod enum_impl; 10 | pub(crate) mod field_wrapper; 11 | mod struct_impl; 12 | 13 | struct ReconcileImpl { 14 | key_type_def: Option, 15 | key_type: Option, 16 | reconcile: TokenStream, 17 | hydrate_key: Option, 18 | get_key: Option, 19 | } 20 | 21 | pub fn derive_reconcile(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 22 | let input = parse_macro_input!(input as DeriveInput); 23 | 24 | let span = input.span(); 25 | 26 | let name = &input.ident; 27 | 28 | let generics = add_trait_bounds(input.generics.clone()); 29 | 30 | let container_attrs = match attrs::Container::from_attrs(input.attrs.iter()) { 31 | Ok(c) => c.unwrap_or_default(), 32 | Err(e) => { 33 | let span = e.span().unwrap_or(span); 34 | return proc_macro::TokenStream::from( 35 | syn::Error::new(span, e.to_string()).to_compile_error(), 36 | ); 37 | } 38 | }; 39 | 40 | let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); 41 | let reconciler_ident = syn::Ident::new("reconciler", Span::call_site()); 42 | 43 | match reconcile_impl( 44 | container_attrs, 45 | span, 46 | &reconciler_ident, 47 | &generics, 48 | name, 49 | &input.data, 50 | &input.vis, 51 | ) { 52 | Ok(ReconcileImpl { 53 | reconcile: the_impl, 54 | key_type_def, 55 | key_type, 56 | hydrate_key, 57 | get_key, 58 | }) => { 59 | let key_lifetime = syn::Lifetime::new("'k", Span::mixed_site()); 60 | let key_type_def = key_type_def.unwrap_or_else(|| quote! {}); 61 | let key_type = key_type.unwrap_or(quote! { 62 | type Key<#key_lifetime> = autosurgeon::reconcile::NoKey; 63 | }); 64 | let expanded = quote! { 65 | impl #impl_generics autosurgeon::Reconcile for #name #ty_generics #where_clause { 66 | #key_type 67 | fn reconcile<__R123: autosurgeon::Reconciler>(&self, mut #reconciler_ident: __R123) -> Result<(), __R123::Error> { 68 | #the_impl 69 | } 70 | #hydrate_key 71 | #get_key 72 | } 73 | #key_type_def 74 | }; 75 | 76 | proc_macro::TokenStream::from(expanded) 77 | } 78 | Err(e) => proc_macro::TokenStream::from( 79 | syn::Error::new(e.span().unwrap_or_else(|| input.span()), e.to_string()) 80 | .to_compile_error(), 81 | ), 82 | } 83 | } 84 | 85 | fn add_trait_bounds(mut generics: Generics) -> Generics { 86 | for param in &mut generics.params { 87 | if let GenericParam::Type(ref mut type_param) = *param { 88 | type_param.bounds.push(parse_quote!(autosurgeon::Reconcile)); 89 | } 90 | } 91 | generics 92 | } 93 | 94 | fn reconcile_impl( 95 | container_attrs: attrs::Container, 96 | _span: proc_macro2::Span, 97 | reconciler_ident: &syn::Ident, 98 | generics: &syn::Generics, 99 | name: &syn::Ident, 100 | data: &Data, 101 | vis: &syn::Visibility, 102 | ) -> Result { 103 | if let Some(reconcile) = container_attrs.reconcile_with() { 104 | return Ok(reconcile_with_impl(reconcile, reconciler_ident)); 105 | } 106 | match *data { 107 | Data::Struct(ref data) => match data.fields { 108 | Fields::Named(ref fields) => struct_impl::named_field_impl(reconciler_ident, fields), 109 | Fields::Unnamed(ref fields) => { 110 | if fields.unnamed.len() == 1 { 111 | let field = fields.unnamed.first().unwrap(); 112 | newtype_struct_impl(field) 113 | } else { 114 | struct_impl::tuple_struct_impl(reconciler_ident, fields) 115 | } 116 | } 117 | Fields::Unit => Err(error::DeriveError::Unit), 118 | }, 119 | Data::Enum(ref data) => enum_impl::enum_impl(vis, name, generics, reconciler_ident, data), 120 | Data::Union(_) => Err(error::DeriveError::Union), 121 | } 122 | } 123 | 124 | fn reconcile_with_impl( 125 | reconcile_with: &attrs::ReconcileWith, 126 | reconciler_ident: &syn::Ident, 127 | ) -> ReconcileImpl { 128 | let key_lifetime = syn::Lifetime::new("'k", Span::mixed_site()); 129 | let key_type = match reconcile_with { 130 | attrs::ReconcileWith::Function { .. } => quote! { 131 | type Key<#key_lifetime> = std::borrow::Cow<#key_lifetime, Self>; 132 | }, 133 | attrs::ReconcileWith::Module { module_name, .. } 134 | | attrs::ReconcileWith::With { module_name, .. } => { 135 | let key_ident = syn::Ident::new("Key", Span::mixed_site()); 136 | quote! { 137 | type Key<#key_lifetime> = #module_name::#key_ident<#key_lifetime>; 138 | } 139 | } 140 | }; 141 | let hydrate_key = match reconcile_with { 142 | attrs::ReconcileWith::Function { .. } => quote! { 143 | fn hydrate_key<#key_lifetime, D: autosurgeon::ReadDoc>( 144 | doc: &D, 145 | obj: &automerge::ObjId, 146 | prop: autosurgeon::Prop<'_>, 147 | ) -> Result>, autosurgeon::ReconcileError> { 148 | use autosurgeon::{reconcile::LoadKey, hydrate::HydrateResultExt}; 149 | let key = autosurgeon::hydrate::hydrate_path(doc, obj, std::iter::once(prop)).strip_unexpected()?; 150 | Ok(key.map(|k| LoadKey::Found(std::borrow::Cow::Owned(k))).unwrap_or(LoadKey::KeyNotFound)) 151 | } 152 | }, 153 | attrs::ReconcileWith::Module { module_name, .. } 154 | | attrs::ReconcileWith::With { module_name, .. } => { 155 | let hydrate_key_ident = syn::Ident::new("hydrate_key", Span::mixed_site()); 156 | quote! { 157 | fn hydrate_key<#key_lifetime, D: autosurgeon::ReadDoc>( 158 | doc: &D, 159 | obj: &automerge::ObjId, 160 | prop: autosurgeon::Prop<'_>, 161 | ) -> Result>, autosurgeon::ReconcileError> { 162 | #module_name::#hydrate_key_ident(doc, obj, prop) 163 | } 164 | } 165 | } 166 | }; 167 | let get_key = match reconcile_with { 168 | attrs::ReconcileWith::Function { .. } => quote! { 169 | fn key<#key_lifetime>(&#key_lifetime self) -> autosurgeon::reconcile::LoadKey> { 170 | autosurgeon::reconcile::LoadKey::Found(std::borrow::Cow::Borrowed(self)) 171 | } 172 | }, 173 | attrs::ReconcileWith::Module { module_name, .. } 174 | | attrs::ReconcileWith::With { module_name, .. } => { 175 | let get_ident = syn::Ident::new("key", Span::mixed_site()); 176 | quote! { 177 | fn key<#key_lifetime>(&#key_lifetime self) -> autosurgeon::reconcile::LoadKey> { 178 | #module_name::#get_ident(self) 179 | } 180 | } 181 | } 182 | }; 183 | let reconcile = match reconcile_with { 184 | attrs::ReconcileWith::Function { function_name, .. } => quote! { 185 | #function_name(self, #reconciler_ident) 186 | }, 187 | attrs::ReconcileWith::Module { module_name, .. } 188 | | attrs::ReconcileWith::With { module_name, .. } => quote! { 189 | #module_name::reconcile(self, #reconciler_ident) 190 | }, 191 | }; 192 | ReconcileImpl { 193 | key_type_def: None, 194 | key_type: Some(key_type), 195 | reconcile, 196 | hydrate_key: Some(hydrate_key), 197 | get_key: Some(get_key), 198 | } 199 | } 200 | 201 | fn newtype_struct_impl(field: &syn::Field) -> Result { 202 | let field_ty = &field.ty; 203 | let fieldattrs = attrs::Field::from_field(field) 204 | .map_err(|e| error::DeriveError::InvalidFieldAttrs(e, field.clone()))?; 205 | let key_lifetime = syn::Lifetime::new("'k", Span::mixed_site()); 206 | if let Some(reconcile_with) = fieldattrs.as_ref().and_then(|f| f.reconcile_with()) { 207 | let name = syn::Ident::new("inner", Span::mixed_site()); 208 | let wrapper_tyname = format_ident!("___{}Wrapper", name, span = Span::mixed_site()); 209 | let wrapper = reconcile_with.wrapper(field_ty, &wrapper_tyname, true); 210 | Ok(ReconcileImpl { 211 | reconcile: quote! { 212 | #wrapper 213 | #wrapper_tyname(&self.0).reconcile(reconciler) 214 | }, 215 | key_type: reconcile_with.key_type(), 216 | key_type_def: None, 217 | hydrate_key: reconcile_with.hydrate_key(), 218 | get_key: reconcile_with.get_key(quote! {&self.0}), 219 | }) 220 | } else { 221 | Ok(ReconcileImpl { 222 | reconcile: quote_spanned! {field.span() => self.0.reconcile(reconciler)}, 223 | key_type: Some( 224 | quote! { type Key<#key_lifetime> = <#field_ty as Reconcile>::Key<#key_lifetime>; }, 225 | ), 226 | key_type_def: None, 227 | hydrate_key: Some(quote! { 228 | fn hydrate_key<#key_lifetime, D: autosurgeon::ReadDoc>( 229 | doc: &D, 230 | obj: &automerge::ObjId, 231 | prop: autosurgeon::Prop<'_>, 232 | ) -> Result>, autosurgeon::ReconcileError> { 233 | <#field_ty as autosurgeon::Reconcile>::hydrate_key(doc, obj, prop) 234 | } 235 | }), 236 | get_key: Some(quote! { 237 | fn key<#key_lifetime>(&#key_lifetime self) -> autosurgeon::reconcile::LoadKey> { 238 | <#field_ty as autosurgeon::Reconcile>::key(&self.0) 239 | } 240 | }), 241 | }) 242 | } 243 | } 244 | 245 | mod error { 246 | use proc_macro2::Span; 247 | use syn::spanned::Spanned; 248 | 249 | use crate::attrs; 250 | 251 | #[derive(Debug, thiserror::Error)] 252 | pub(crate) enum DeriveError { 253 | #[error(transparent)] 254 | InvalidKeyAttr(#[from] InvalidKeyAttr), 255 | #[error("{0}")] 256 | InvalidFieldAttrs(attrs::error::InvalidFieldAttrs, syn::Field), 257 | #[error("{0}")] 258 | InvalidEnumNewtypeFieldAttrs(attrs::error::InvalidEnumNewtypeFieldAttrs, syn::Field), 259 | #[error("cannot derive Reconcile for a unit struct")] 260 | Unit, 261 | #[error("cannot derive Reconcile for a Union")] 262 | Union, 263 | } 264 | 265 | impl DeriveError { 266 | pub(super) fn span(&self) -> Option { 267 | match self { 268 | Self::InvalidKeyAttr(e) => e.span(), 269 | Self::InvalidFieldAttrs(_, f) => Some(f.span()), 270 | Self::InvalidEnumNewtypeFieldAttrs(e, f) => e.span().or_else(|| Some(f.span())), 271 | Self::Unit => None, 272 | Self::Union => None, 273 | } 274 | } 275 | } 276 | 277 | #[derive(Debug, thiserror::Error)] 278 | pub(crate) enum InvalidKeyAttr { 279 | #[error(transparent)] 280 | Parse(#[from] syn::Error), 281 | #[error("multiple key attributes specified")] 282 | MultipleKey, 283 | } 284 | 285 | impl InvalidKeyAttr { 286 | fn span(&self) -> Option { 287 | match self { 288 | Self::Parse(p) => Some(p.span()), 289 | Self::MultipleKey => None, 290 | } 291 | } 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /autosurgeon-derive/src/reconcile/field_wrapper.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::{quote, ToTokens}; 3 | 4 | pub(crate) fn nokey_wrapper( 5 | ty: &syn::Type, 6 | wrapper_tyname: &syn::Ident, 7 | func: T, 8 | ) -> TokenStream { 9 | quote! { 10 | struct #wrapper_tyname<'a>(&'a #ty); 11 | impl<'a> autosurgeon::Reconcile for #wrapper_tyname<'a> { 12 | type Key<'k> = autosurgeon::reconcile::NoKey; 13 | 14 | fn reconcile(&self, reconciler: R) -> Result<(), R::Error> { 15 | #func(self.0, reconciler) 16 | } 17 | 18 | fn hydrate_key<'b, D: autosurgeon::ReadDoc>( 19 | _doc: &D, 20 | _obj: &automerge::ObjId, 21 | _prop: autosurgeon::Prop<'_>, 22 | ) -> Result>, autosurgeon::reconcile::ReconcileError> { 23 | Ok(autosurgeon::reconcile::LoadKey::NoKey) 24 | } 25 | fn key<'b>(&'b self) -> autosurgeon::reconcile::LoadKey> { 26 | autosurgeon::reconcile::LoadKey::NoKey 27 | } 28 | } 29 | } 30 | } 31 | 32 | pub(crate) fn with_key_wrapper( 33 | ty: &syn::Type, 34 | wrapper_tyname: &syn::Ident, 35 | module_name: &syn::Ident, 36 | ) -> TokenStream { 37 | quote! { 38 | struct #wrapper_tyname<'a>(&'a #ty); 39 | impl<'a> autosurgeon::Reconcile for #wrapper_tyname<'a> { 40 | type Key<'k> = #module_name::Key<'k>; 41 | 42 | fn reconcile(&self, reconciler: R) -> Result<(), R::Error> { 43 | #module_name::reconcile(self.0, reconciler) 44 | } 45 | 46 | fn hydrate_key<'b, D: autosurgeon::ReadDoc>( 47 | doc: &D, 48 | obj: &automerge::ObjId, 49 | prop: autosurgeon::Prop<'_>, 50 | ) -> Result>, autosurgeon::reconcile::ReconcileError> { 51 | #module_name::hydrate_key(doc, obj, prop) 52 | } 53 | fn key<'b>(&'b self) -> autosurgeon::reconcile::LoadKey> { 54 | #module_name::key(self.0) 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /autosurgeon-derive/src/reconcile/struct_impl.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, convert::TryFrom}; 2 | 3 | use proc_macro2::{Span, TokenStream}; 4 | use quote::{format_ident, quote, quote_spanned}; 5 | use syn::spanned::Spanned; 6 | 7 | use crate::attrs; 8 | 9 | use super::{ 10 | error::{DeriveError, InvalidKeyAttr}, 11 | ReconcileImpl, 12 | }; 13 | 14 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 15 | pub enum ReconcilerType { 16 | Seq, 17 | Map, 18 | } 19 | 20 | pub(super) trait Field { 21 | /// The original span of this field 22 | fn span(&self) -> Span; 23 | 24 | /// The attributes of this field 25 | fn attrs(&self) -> &[syn::Attribute]; 26 | 27 | /// The type of this field 28 | fn ty(&self) -> &syn::Type; 29 | 30 | /// A `TokenStream` which renders as the [`Prop`] which gets this value from a reconciler 31 | fn as_prop(&self) -> TokenStream; 32 | 33 | /// A token stream which accesses this field on the struct or tuple at runtime (e.g. 34 | /// self.myfeild) 35 | fn accessor(&self) -> TokenStream; 36 | 37 | fn name(&self) -> syn::Ident; 38 | 39 | fn reconcile_with(&self) -> Option<&attrs::ReconcileWith>; 40 | 41 | fn hydrate_with(&self) -> Option<&attrs::HydrateWith>; 42 | 43 | fn upsert(&self, reconciler_ident: &syn::Ident, reconciler_ty: ReconcilerType) -> TokenStream { 44 | let prop = self.as_prop(); 45 | let accessor = self.accessor(); 46 | let ty = self.ty(); 47 | let (reconcile_wrapper, value) = match self.reconcile_with() { 48 | Some(r) => { 49 | let wrapper_tyname = 50 | format_ident!("___{}Wrapper", self.name(), span = Span::call_site()); 51 | let wrapper = r.wrapper(ty, &wrapper_tyname, false); 52 | let value = quote!(#wrapper_tyname(&#accessor)); 53 | (wrapper, value) 54 | } 55 | None => (quote! {}, quote!(&#accessor)), 56 | }; 57 | let get = match reconciler_ty { 58 | ReconcilerType::Map => quote_spanned!(self.span() => #reconciler_ident.entry(#prop)), 59 | ReconcilerType::Seq => quote_spanned!(self.span() => #reconciler_ident.get(#prop)?), 60 | }; 61 | let insert = match reconciler_ty { 62 | ReconcilerType::Seq => { 63 | quote_spanned!(self.span() => #reconciler_ident.insert(#prop, #value)?;) 64 | } 65 | ReconcilerType::Map => { 66 | quote_spanned!(self.span() => #reconciler_ident.put(#prop, #value)?;) 67 | } 68 | }; 69 | let update = match reconciler_ty { 70 | ReconcilerType::Seq => { 71 | quote_spanned!(self.span() => #reconciler_ident.set(#prop, #value)?;) 72 | } 73 | ReconcilerType::Map => { 74 | quote_spanned!(self.span() => #reconciler_ident.put(#prop, #value)?;) 75 | } 76 | }; 77 | quote! { 78 | 79 | #reconcile_wrapper 80 | if #get.is_some() { 81 | #update 82 | } else { 83 | #insert 84 | } 85 | } 86 | } 87 | } 88 | 89 | #[derive(Clone, Eq, PartialEq)] 90 | pub(super) struct NamedField<'a> { 91 | name: Cow<'a, syn::Ident>, 92 | field: Cow<'a, syn::Field>, 93 | attrs: attrs::Field, 94 | } 95 | 96 | impl<'a> NamedField<'a> { 97 | pub(super) fn new( 98 | name: Cow<'a, syn::Ident>, 99 | field: &'a syn::Field, 100 | ) -> Result { 101 | let attrs = attrs::Field::from_field(field)?.unwrap_or_default(); 102 | Ok(Self { 103 | name, 104 | field: Cow::Borrowed(field), 105 | attrs, 106 | }) 107 | } 108 | 109 | pub(super) fn name(&self) -> &syn::Ident { 110 | &self.name 111 | } 112 | 113 | fn to_owned(&self) -> NamedField<'static> { 114 | NamedField { 115 | name: Cow::Owned(self.name.as_ref().clone()), 116 | field: Cow::Owned(self.field.as_ref().clone()), 117 | attrs: self.attrs.clone(), 118 | } 119 | } 120 | } 121 | 122 | impl<'a> Field for NamedField<'a> { 123 | fn attrs(&self) -> &[syn::Attribute] { 124 | &self.field.attrs 125 | } 126 | 127 | fn ty(&self) -> &syn::Type { 128 | &self.field.ty 129 | } 130 | 131 | fn span(&self) -> Span { 132 | self.field.span() 133 | } 134 | 135 | fn as_prop(&self) -> TokenStream { 136 | let propname = &self.name.to_string(); 137 | quote! {#propname} 138 | } 139 | 140 | fn accessor(&self) -> TokenStream { 141 | let propname = &self.name; 142 | quote! {self.#propname} 143 | } 144 | 145 | fn name(&self) -> syn::Ident { 146 | self.field.ident.clone().unwrap() 147 | } 148 | 149 | fn reconcile_with(&self) -> Option<&attrs::ReconcileWith> { 150 | self.attrs.reconcile_with() 151 | } 152 | 153 | fn hydrate_with(&self) -> Option<&attrs::HydrateWith> { 154 | self.attrs.hydrate_with() 155 | } 156 | } 157 | 158 | #[derive(Clone, Eq, PartialEq)] 159 | pub(super) struct TupleField<'a> { 160 | index: usize, 161 | field: Cow<'a, syn::Field>, 162 | attrs: attrs::Field, 163 | } 164 | 165 | impl<'a> TupleField<'a> { 166 | fn new( 167 | index: usize, 168 | field: Cow<'a, syn::Field>, 169 | ) -> Result, attrs::error::InvalidFieldAttrs> { 170 | let attrs = attrs::Field::from_field(&field)?.unwrap_or_default(); 171 | Ok(Self { 172 | index, 173 | field, 174 | attrs, 175 | }) 176 | } 177 | 178 | fn to_owned(&self) -> TupleField<'static> { 179 | let field: syn::Field = self.field.as_ref().clone(); 180 | TupleField { 181 | index: self.index, 182 | field: Cow::Owned(field), 183 | attrs: self.attrs.clone(), 184 | } 185 | } 186 | } 187 | 188 | impl<'a> Field for TupleField<'a> { 189 | fn attrs(&self) -> &[syn::Attribute] { 190 | &self.field.attrs 191 | } 192 | 193 | fn span(&self) -> Span { 194 | self.field.span() 195 | } 196 | 197 | fn ty(&self) -> &syn::Type { 198 | &self.field.ty 199 | } 200 | 201 | fn as_prop(&self) -> TokenStream { 202 | let idx = self.index; 203 | quote! {#idx} 204 | } 205 | 206 | fn accessor(&self) -> TokenStream { 207 | let idx = syn::Index::from(self.index); 208 | quote! { 209 | self.#idx 210 | } 211 | } 212 | 213 | fn name(&self) -> syn::Ident { 214 | format_ident!("field_{}", self.index) 215 | } 216 | 217 | fn reconcile_with(&self) -> Option<&attrs::ReconcileWith> { 218 | self.attrs.reconcile_with() 219 | } 220 | 221 | fn hydrate_with(&self) -> Option<&attrs::HydrateWith> { 222 | self.attrs.hydrate_with() 223 | } 224 | } 225 | 226 | #[derive(Clone, Eq, PartialEq)] 227 | pub(super) struct KeyField<'a, F: Clone> { 228 | ty: Cow<'a, syn::Type>, 229 | field: Cow<'a, F>, 230 | } 231 | 232 | impl<'a> KeyField<'a, NamedField<'a>> { 233 | pub(super) fn into_owned(self) -> KeyField<'static, NamedField<'static>> { 234 | KeyField { 235 | ty: Cow::Owned(self.ty.into_owned()), 236 | field: Cow::Owned(self.field.as_ref().to_owned()), 237 | } 238 | } 239 | 240 | pub(super) fn name(&self) -> &syn::Ident { 241 | self.field.name() 242 | } 243 | } 244 | 245 | impl<'a> KeyField<'a, TupleField<'a>> { 246 | pub(super) fn into_owned(self) -> KeyField<'static, TupleField<'static>> { 247 | KeyField { 248 | ty: Cow::Owned(self.ty.into_owned()), 249 | field: Cow::Owned(self.field.as_ref().to_owned()), 250 | } 251 | } 252 | 253 | pub(super) fn index(&self) -> usize { 254 | self.field.index 255 | } 256 | } 257 | 258 | impl<'a, F: Field + Clone> KeyField<'a, F> { 259 | pub(super) fn from_fields>( 260 | fields: I, 261 | ) -> Result>, InvalidKeyAttr> { 262 | let mut key_field = None; 263 | for field in fields { 264 | for attr in field.attrs() { 265 | let meta = attr.parse_meta()?; 266 | match meta { 267 | syn::Meta::Path(p) if p.is_ident("key") => { 268 | if key_field.is_some() { 269 | return Err(InvalidKeyAttr::MultipleKey); 270 | } else { 271 | key_field = Some(KeyField { 272 | ty: Cow::Borrowed(field.ty()), 273 | field: Cow::Borrowed(field), 274 | }); 275 | } 276 | } 277 | _ => {} 278 | } 279 | } 280 | } 281 | Ok(key_field) 282 | } 283 | 284 | fn key_type_def(&self) -> proc_macro2::TokenStream { 285 | let ty = &self.ty; 286 | let lifetime = syn::Lifetime::new("'k", Span::mixed_site()); 287 | quote! {type Key<#lifetime> = std::borrow::Cow<#lifetime, #ty>;} 288 | } 289 | 290 | pub(super) fn key_type(&self) -> &syn::Type { 291 | self.ty.as_ref() 292 | } 293 | 294 | fn hydrate_impl(&self) -> proc_macro2::TokenStream { 295 | let key_prop = self.field.as_prop(); 296 | let key_lifetime = syn::Lifetime::new("'k", Span::mixed_site()); 297 | if let Some(hydrate_with) = self.field.hydrate_with() { 298 | let hydrate_func = hydrate_with.hydrate_with(); 299 | quote! { 300 | fn hydrate_key<#key_lifetime, D: autosurgeon::ReadDoc>( 301 | doc: &D, 302 | obj: &automerge::ObjId, 303 | prop: autosurgeon::Prop<'_>, 304 | ) -> Result>, autosurgeon::ReconcileError> { 305 | use automerge::{ObjType, transaction::Transactable}; 306 | use autosurgeon::{Prop, reconcile::LoadKey, hydrate::HydrateResultExt}; 307 | let Some(outer_type) = doc.object_type(&obj) else { 308 | return Ok(LoadKey::KeyNotFound) 309 | }; 310 | let maybe_inner = match (outer_type, prop) { 311 | (ObjType::Map | ObjType::Table, Prop::Key(k)) => { 312 | doc.get(&obj, k.as_ref())? 313 | }, 314 | (ObjType::List | ObjType::Text, Prop::Index(i)) => { 315 | doc.get(&obj, i as usize)? 316 | }, 317 | _ => return Ok(LoadKey::KeyNotFound), 318 | }; 319 | let Some((_, inner_obj)) = maybe_inner else { 320 | return Ok(LoadKey::KeyNotFound) 321 | }; 322 | let Some(inner_type) = doc.object_type(&inner_obj) else { 323 | return Ok(LoadKey::KeyNotFound) 324 | }; 325 | let inner_val = match (inner_type, Prop::from(#key_prop)) { 326 | (ObjType::Map | ObjType::Table, Prop::Key(k)) => { 327 | doc.get(&inner_obj, k.as_ref())? 328 | }, 329 | (ObjType::List | ObjType::Text, Prop::Index(i)) => { 330 | doc.get(&inner_obj, i as usize)? 331 | }, 332 | _ => return Ok(LoadKey::KeyNotFound), 333 | }; 334 | if inner_val.is_none() { 335 | return Ok(LoadKey::KeyNotFound) 336 | } else { 337 | match #hydrate_func(doc, &inner_obj, #key_prop.into()).map(Some).strip_unexpected()? { 338 | Some(k) => Ok(LoadKey::Found(std::borrow::Cow::Owned(k))), 339 | None => Ok(LoadKey::KeyNotFound), 340 | } 341 | } 342 | 343 | } 344 | } 345 | } else { 346 | quote! { 347 | fn hydrate_key<#key_lifetime, D: autosurgeon::ReadDoc>( 348 | doc: &D, 349 | obj: &automerge::ObjId, 350 | prop: autosurgeon::Prop<'_>, 351 | ) -> Result>, autosurgeon::ReconcileError> { 352 | autosurgeon::reconcile::hydrate_key::<_, std::borrow::Cow<'_, _>>(doc, obj, prop.into(), #key_prop.into()) 353 | } 354 | } 355 | } 356 | } 357 | 358 | pub(super) fn prop(&self) -> TokenStream { 359 | let key_prop = self.field.as_prop(); 360 | quote! {#key_prop} 361 | } 362 | 363 | fn get_key(&self) -> proc_macro2::TokenStream { 364 | let get_key = self.field.accessor(); 365 | let key_lifetime = syn::Lifetime::new("'k", Span::mixed_site()); 366 | quote! { 367 | fn key<#key_lifetime>(&#key_lifetime self) -> autosurgeon::reconcile::LoadKey> { 368 | autosurgeon::reconcile::LoadKey::Found(std::borrow::Cow::Borrowed(&#get_key)) 369 | } 370 | } 371 | } 372 | } 373 | 374 | pub(super) struct NamedFields<'a>(Vec>); 375 | 376 | impl<'a> NamedFields<'a> { 377 | pub(super) fn key( 378 | &'a self, 379 | ) -> Result>>, InvalidKeyAttr> { 380 | Ok(KeyField::from_fields(self.0.iter())?.map(|k| k.into_owned())) 381 | } 382 | } 383 | 384 | impl<'a> TryFrom<&'a syn::FieldsNamed> for NamedFields<'a> { 385 | type Error = DeriveError; 386 | 387 | fn try_from(fields: &'a syn::FieldsNamed) -> Result { 388 | Ok(Self( 389 | fields 390 | .named 391 | .iter() 392 | .map(|f| { 393 | NamedField::new(Cow::Borrowed(f.ident.as_ref().unwrap()), f) 394 | .map_err(|e| DeriveError::InvalidFieldAttrs(e, f.clone())) 395 | }) 396 | .collect::, _>>()?, 397 | )) 398 | } 399 | } 400 | 401 | impl<'a> From>> for NamedFields<'a> { 402 | fn from(f: Vec>) -> Self { 403 | Self(f) 404 | } 405 | } 406 | 407 | impl<'a, E> TryFrom, E>>> for NamedFields<'a> { 408 | type Error = E; 409 | fn try_from(f: Vec, E>>) -> Result { 410 | Ok(Self(f.into_iter().collect::, _>>()?)) 411 | } 412 | } 413 | 414 | pub(super) fn named_field_impl<'a, F: TryInto, Error = DeriveError>>( 415 | reconciler_ident: &syn::Ident, 416 | fields: F, 417 | ) -> Result { 418 | let fields = fields.try_into()?.0; 419 | 420 | let inner_reconciler_ident = syn::Ident::new("m", Span::mixed_site()); 421 | 422 | let StructImpl { 423 | field_impls, 424 | key_type, 425 | get_key, 426 | hydrate_key, 427 | } = struct_impl(fields, &inner_reconciler_ident, ReconcilerType::Map)?; 428 | 429 | let the_impl = quote! { 430 | use autosurgeon::reconcile::MapReconciler; 431 | let mut #inner_reconciler_ident = #reconciler_ident.map()?; 432 | #( #field_impls)* 433 | Ok(()) 434 | }; 435 | 436 | Ok(ReconcileImpl { 437 | key_type, 438 | reconcile: the_impl, 439 | hydrate_key, 440 | get_key, 441 | key_type_def: None, 442 | }) 443 | } 444 | 445 | pub(super) struct UnnamedFields(Vec); 446 | 447 | impl UnnamedFields { 448 | pub(super) fn key(&self) -> Result>, InvalidKeyAttr> { 449 | KeyField::from_fields(self.0.iter()) 450 | } 451 | } 452 | 453 | impl<'a> TryFrom<&'a syn::FieldsUnnamed> for UnnamedFields> { 454 | type Error = DeriveError; 455 | 456 | fn try_from(f: &'a syn::FieldsUnnamed) -> Result { 457 | Ok(UnnamedFields( 458 | f.unnamed 459 | .iter() 460 | .enumerate() 461 | .map(|(index, f)| { 462 | TupleField::new(index, Cow::Borrowed(f)) 463 | .map_err(|e| DeriveError::InvalidFieldAttrs(e, f.clone())) 464 | }) 465 | .collect::, _>>()?, 466 | )) 467 | } 468 | } 469 | 470 | impl TryFrom<&[F]> for UnnamedFields { 471 | type Error = DeriveError; 472 | 473 | fn try_from(f: &[F]) -> Result { 474 | Ok(UnnamedFields(f.to_vec())) 475 | } 476 | } 477 | 478 | pub(super) fn tuple_struct_impl< 479 | F: Field + Clone, 480 | I: TryInto, Error = DeriveError>, 481 | >( 482 | reconciler_ident: &syn::Ident, 483 | fields: I, 484 | ) -> Result { 485 | let fields = fields.try_into()?.0; 486 | 487 | let seq_reconciler_ident = syn::Ident::new("s", Span::mixed_site()); 488 | 489 | let StructImpl { 490 | field_impls, 491 | key_type, 492 | get_key, 493 | hydrate_key, 494 | } = struct_impl(fields, &seq_reconciler_ident, ReconcilerType::Seq)?; 495 | 496 | let the_impl = quote! { 497 | use autosurgeon::reconcile::SeqReconciler; 498 | let mut #seq_reconciler_ident = #reconciler_ident.seq()?; 499 | #( #field_impls)* 500 | Ok(()) 501 | }; 502 | 503 | Ok(ReconcileImpl { 504 | key_type, 505 | reconcile: the_impl, 506 | hydrate_key, 507 | get_key, 508 | key_type_def: None, 509 | }) 510 | } 511 | 512 | struct StructImpl { 513 | key_type: Option, 514 | get_key: Option, 515 | hydrate_key: Option, 516 | field_impls: Vec, 517 | } 518 | 519 | fn struct_impl( 520 | fields: Vec, 521 | reconciler_ident: &syn::Ident, 522 | reconciler_type: ReconcilerType, 523 | ) -> Result { 524 | let key_field = KeyField::from_fields(fields.iter())?; 525 | let field_impls = fields 526 | .iter() 527 | .map(|f| f.upsert(reconciler_ident, reconciler_type)) 528 | .collect(); 529 | let key_type = key_field.as_ref().map(|k| k.key_type_def()); 530 | 531 | let hydrate_key = key_field.as_ref().map(|k| k.hydrate_impl()); 532 | 533 | let get_key = key_field.as_ref().map(|k| k.get_key()); 534 | 535 | Ok(StructImpl { 536 | key_type, 537 | field_impls, 538 | hydrate_key, 539 | get_key, 540 | }) 541 | } 542 | -------------------------------------------------------------------------------- /autosurgeon-derive/tests/hydrate.rs: -------------------------------------------------------------------------------- 1 | use automerge::{transaction::Transactable, ObjType}; 2 | use autosurgeon::{hydrate, hydrate_prop, Hydrate}; 3 | 4 | #[derive(Debug, Hydrate, PartialEq)] 5 | struct Company { 6 | employees: Vec, 7 | name: String, 8 | } 9 | 10 | #[derive(Debug, Hydrate, PartialEq)] 11 | struct Employee { 12 | name: String, 13 | number: u64, 14 | } 15 | 16 | #[test] 17 | fn hydrate_struct() { 18 | let mut doc = automerge::AutoCommit::new(); 19 | let microsoft = doc 20 | .put_object(automerge::ROOT, "microsoft", ObjType::Map) 21 | .unwrap(); 22 | doc.put(µsoft, "name", "Microsoft").unwrap(); 23 | let emps = doc 24 | .put_object(µsoft, "employees", ObjType::List) 25 | .unwrap(); 26 | let satya = doc.insert_object(&emps, 0, ObjType::Map).unwrap(); 27 | doc.put(&satya, "name", "Satya Nadella").unwrap(); 28 | doc.put(&satya, "number", 1_u64).unwrap(); 29 | 30 | let result: Company = hydrate_prop(&doc, &automerge::ROOT, "microsoft").unwrap(); 31 | assert_eq!( 32 | result, 33 | Company { 34 | name: "Microsoft".to_string(), 35 | employees: vec![Employee { 36 | name: "Satya Nadella".to_string(), 37 | number: 1_u64, 38 | }] 39 | } 40 | ); 41 | } 42 | 43 | #[derive(Debug, Hydrate, PartialEq)] 44 | struct SpecialString(String); 45 | 46 | #[test] 47 | fn hydrate_newtype_struct() { 48 | let mut doc = automerge::AutoCommit::new(); 49 | doc.put(&automerge::ROOT, "key", "value").unwrap(); 50 | let result: SpecialString = hydrate_prop(&doc, &automerge::ROOT, "key").unwrap(); 51 | assert_eq!(result, SpecialString("value".to_string())); 52 | } 53 | 54 | // Just here to check that generics are propagated correctly 55 | #[derive(Hydrate)] 56 | struct Wrapped(T); 57 | 58 | #[derive(Debug, Hydrate, PartialEq)] 59 | struct LatLng(f64, f64); 60 | 61 | #[test] 62 | fn hydrate_tuple_struct() { 63 | let mut doc = automerge::AutoCommit::new(); 64 | let coord = doc 65 | .put_object(&automerge::ROOT, "coord", ObjType::List) 66 | .unwrap(); 67 | doc.insert(&coord, 0, 1.2).unwrap(); 68 | doc.insert(&coord, 1, 2.3).unwrap(); 69 | let coordinate: LatLng = hydrate_prop(&doc, &automerge::ROOT, "coord").unwrap(); 70 | assert_eq!(coordinate, LatLng(1.2, 2.3)); 71 | } 72 | 73 | #[derive(Debug, Hydrate, PartialEq)] 74 | enum Color { 75 | Red, 76 | Green, 77 | } 78 | 79 | #[test] 80 | fn hydrate_unit_enum() { 81 | let mut doc = automerge::AutoCommit::new(); 82 | doc.put(&automerge::ROOT, "color", "Red").unwrap(); 83 | let red: Color = hydrate_prop(&doc, &automerge::ROOT, "color").unwrap(); 84 | assert_eq!(red, Color::Red); 85 | } 86 | 87 | #[derive(Debug, Hydrate, PartialEq)] 88 | enum Coordinate { 89 | LatLng { lat: f64, lng: f64 }, 90 | } 91 | 92 | #[test] 93 | fn hydrate_named_field_enum() { 94 | let mut doc = automerge::AutoCommit::new(); 95 | let latlng = doc 96 | .put_object(&automerge::ROOT, "LatLng", ObjType::Map) 97 | .unwrap(); 98 | doc.put(&latlng, "lat", 1.2).unwrap(); 99 | doc.put(&latlng, "lng", 2.3).unwrap(); 100 | let coordinate: Coordinate = hydrate(&doc).unwrap(); 101 | assert_eq!(coordinate, Coordinate::LatLng { lat: 1.2, lng: 2.3 }); 102 | } 103 | 104 | #[derive(Debug, Hydrate, PartialEq)] 105 | enum ValueHolder { 106 | Int(u32), 107 | } 108 | 109 | #[test] 110 | fn hydrate_single_value_tuple_enum_variant() { 111 | let mut doc = automerge::AutoCommit::new(); 112 | doc.put(&automerge::ROOT, "Int", 234_u64).unwrap(); 113 | let holder: ValueHolder = hydrate(&doc).unwrap(); 114 | assert_eq!(holder, ValueHolder::Int(234)); 115 | } 116 | 117 | #[derive(Debug, Hydrate, PartialEq)] 118 | enum Vector { 119 | ThreeD(f64, f64, f64), 120 | } 121 | 122 | #[test] 123 | fn hydrate_multi_value_tuple_enum_variant() { 124 | let mut doc = automerge::AutoCommit::new(); 125 | let three = doc 126 | .put_object(&automerge::ROOT, "ThreeD", ObjType::List) 127 | .unwrap(); 128 | doc.insert(&three, 0, 1.2).unwrap(); 129 | doc.insert(&three, 1, 3.4).unwrap(); 130 | doc.insert(&three, 2, 5.6).unwrap(); 131 | let vec: Vector = hydrate(&doc).unwrap(); 132 | assert_eq!(vec, Vector::ThreeD(1.2, 3.4, 5.6)); 133 | } 134 | -------------------------------------------------------------------------------- /autosurgeon-derive/tests/hydrate_with.rs: -------------------------------------------------------------------------------- 1 | use automerge::{transaction::Transactable, ObjType}; 2 | use autosurgeon::{hydrate, hydrate_prop, Hydrate, HydrateError, Prop, ReadDoc}; 3 | 4 | #[derive(Clone, Debug, PartialEq, Eq)] 5 | struct Inner(u64); 6 | 7 | #[derive(Clone, Debug, PartialEq, Eq, Hydrate)] 8 | #[autosurgeon(hydrate = "hydrate_outer")] 9 | struct Outer(Inner); 10 | 11 | fn hydrate_outer( 12 | doc: &D, 13 | obj: &automerge::ObjId, 14 | prop: Prop<'_>, 15 | ) -> Result { 16 | Ok(Outer(Inner(u64::hydrate(doc, obj, prop)?))) 17 | } 18 | 19 | #[test] 20 | fn hydrate_with() { 21 | let mut doc = automerge::AutoCommit::new(); 22 | doc.put(automerge::ROOT, "key", 5_u64).unwrap(); 23 | let result: Outer = hydrate_prop(&doc, &automerge::ROOT, "key").unwrap(); 24 | assert_eq!(result, Outer(Inner(5))); 25 | } 26 | 27 | #[derive(Debug, PartialEq)] 28 | struct UserId(String); 29 | 30 | #[derive(Debug, PartialEq, Hydrate)] 31 | struct User { 32 | name: String, 33 | #[autosurgeon(hydrate = "hydrate_userid")] 34 | id: UserId, 35 | } 36 | 37 | fn hydrate_userid( 38 | doc: &D, 39 | obj: &automerge::ObjId, 40 | prop: Prop<'_>, 41 | ) -> Result { 42 | Ok(UserId(String::hydrate(doc, obj, prop)?)) 43 | } 44 | 45 | #[test] 46 | fn hydrate_on_named_field() { 47 | let mut doc = automerge::AutoCommit::new(); 48 | doc.put(&automerge::ROOT, "id", "someid").unwrap(); 49 | doc.put(&automerge::ROOT, "name", "somename").unwrap(); 50 | let user: User = hydrate(&doc).unwrap(); 51 | assert_eq!( 52 | user, 53 | User { 54 | id: UserId("someid".to_string()), 55 | name: "somename".to_string(), 56 | } 57 | ); 58 | } 59 | 60 | #[derive(Debug, PartialEq, Hydrate)] 61 | struct UserAndName(#[autosurgeon(hydrate = "hydrate_userid")] UserId, String); 62 | 63 | #[test] 64 | fn hydrate_on_tuple_field() { 65 | let mut doc = automerge::AutoCommit::new(); 66 | let user = doc 67 | .put_object(automerge::ROOT, "user", ObjType::List) 68 | .unwrap(); 69 | doc.insert(&user, 0, "someid").unwrap(); 70 | doc.insert(&user, 1, "somename").unwrap(); 71 | let user: UserAndName = hydrate_prop(&doc, &automerge::ROOT, "user").unwrap(); 72 | assert_eq!( 73 | user, 74 | UserAndName(UserId("someid".to_string()), "somename".to_string()) 75 | ) 76 | } 77 | 78 | #[derive(Debug, PartialEq)] 79 | struct SpecialFloat(f64); 80 | 81 | #[derive(Debug, PartialEq, Hydrate)] 82 | enum Temperature { 83 | Celsius(#[autosurgeon(hydrate = "hydrate_specialfloat")] SpecialFloat), 84 | } 85 | 86 | fn hydrate_specialfloat( 87 | doc: &D, 88 | obj: &automerge::ObjId, 89 | prop: Prop<'_>, 90 | ) -> Result { 91 | Ok(SpecialFloat(f64::hydrate(doc, obj, prop)?)) 92 | } 93 | 94 | #[test] 95 | fn hydrate_on_enum_newtype_field() { 96 | let mut doc = automerge::AutoCommit::new(); 97 | let temp = doc 98 | .put_object(&automerge::ROOT, "temp", ObjType::Map) 99 | .unwrap(); 100 | doc.put(&temp, "Celsius", 1.23).unwrap(); 101 | let temp: Temperature = hydrate_prop(&doc, &automerge::ROOT, "temp").unwrap(); 102 | assert_eq!(temp, Temperature::Celsius(SpecialFloat(1.23))); 103 | } 104 | 105 | #[derive(Debug, PartialEq, Hydrate)] 106 | enum UserType { 107 | Admin { 108 | #[autosurgeon(hydrate = "hydrate_userid")] 109 | id: UserId, 110 | name: String, 111 | }, 112 | } 113 | 114 | #[test] 115 | fn hydrate_on_enum_named_field() { 116 | let mut doc = automerge::AutoCommit::new(); 117 | let user = doc 118 | .put_object(automerge::ROOT, "user", ObjType::Map) 119 | .unwrap(); 120 | let admin = doc.put_object(&user, "Admin", ObjType::Map).unwrap(); 121 | doc.put(&admin, "id", "someid").unwrap(); 122 | doc.put(&admin, "name", "somename").unwrap(); 123 | let user: UserType = hydrate_prop(&doc, &automerge::ROOT, "user").unwrap(); 124 | assert_eq!( 125 | user, 126 | UserType::Admin { 127 | id: UserId("someid".to_string()), 128 | name: "somename".to_string() 129 | } 130 | ); 131 | } 132 | 133 | #[derive(Debug, PartialEq, Hydrate)] 134 | enum UserWithProp { 135 | Name(#[autosurgeon(hydrate = "hydrate_userid")] UserId, String), 136 | } 137 | 138 | #[test] 139 | fn hydrate_on_enum_tuple_field() { 140 | let mut doc = automerge::AutoCommit::new(); 141 | let user = doc 142 | .put_object(automerge::ROOT, "user", ObjType::Map) 143 | .unwrap(); 144 | let namevariant = doc.put_object(&user, "Name", ObjType::List).unwrap(); 145 | doc.insert(&namevariant, 0, "someid").unwrap(); 146 | doc.insert(&namevariant, 1, "somename").unwrap(); 147 | let user: UserWithProp = hydrate_prop(&doc, &automerge::ROOT, "user").unwrap(); 148 | assert_eq!( 149 | user, 150 | UserWithProp::Name(UserId("someid".to_string()), "somename".to_string()) 151 | ); 152 | } 153 | -------------------------------------------------------------------------------- /autosurgeon-derive/tests/reconcile.rs: -------------------------------------------------------------------------------- 1 | use automerge::transaction::Transactable; 2 | use automerge_test::{assert_doc, list, map}; 3 | use autosurgeon::{reconcile, reconcile::reconcile_insert, reconcile::reconcile_prop, Reconcile}; 4 | 5 | #[derive(Reconcile)] 6 | struct Company { 7 | employees: Vec, 8 | name: String, 9 | } 10 | 11 | #[derive(Reconcile)] 12 | struct Employee { 13 | name: String, 14 | number: u64, 15 | } 16 | 17 | #[test] 18 | fn basic_struct_reconcile() { 19 | let mut doc = automerge::AutoCommit::new(); 20 | let facebook = Company { 21 | name: "Meta".to_string(), 22 | employees: vec![Employee { 23 | name: "Yann LeCun".to_string(), 24 | number: 8, 25 | }], 26 | }; 27 | reconcile(&mut doc, facebook).unwrap(); 28 | assert_doc!( 29 | doc.document(), 30 | map! { 31 | "name" => { "Meta" }, 32 | "employees" => { list! { 33 | { map! { 34 | "name" => { "Yann LeCun" }, 35 | "number" => { 8_u64 }, 36 | }} 37 | }} 38 | } 39 | ) 40 | } 41 | 42 | #[derive(Reconcile)] 43 | struct SpecialString(String); 44 | 45 | #[test] 46 | fn test_newtype_struct_reconcile() { 47 | let mut doc = automerge::AutoCommit::new(); 48 | reconcile_prop( 49 | &mut doc, 50 | automerge::ROOT, 51 | "special", 52 | SpecialString("special".to_string()), 53 | ) 54 | .unwrap(); 55 | assert_doc!( 56 | doc.document(), 57 | map! { 58 | "special" => { "special" } 59 | } 60 | ); 61 | } 62 | 63 | #[derive(Reconcile)] 64 | struct CartesianCoordinate(f64, f64); 65 | 66 | #[test] 67 | fn test_unnamed_struct_variant_reconcile() { 68 | let mut doc = automerge::AutoCommit::new(); 69 | reconcile_prop( 70 | &mut doc, 71 | automerge::ROOT, 72 | "coordinate", 73 | CartesianCoordinate(5.4, 3.2), 74 | ) 75 | .unwrap(); 76 | assert_doc!( 77 | doc.document(), 78 | map! { 79 | "coordinate" => { list! { { 5.4 }, { 3.2 }}} 80 | } 81 | ); 82 | } 83 | 84 | #[derive(Reconcile)] 85 | enum Color { 86 | Red, 87 | } 88 | 89 | #[test] 90 | fn enum_no_variant_reconcile() { 91 | let mut doc = automerge::AutoCommit::new(); 92 | let colors = doc 93 | .put_object(automerge::ROOT, "colors", automerge::ObjType::List) 94 | .unwrap(); 95 | reconcile_insert(&mut doc, colors, 0, Color::Red).unwrap(); 96 | assert_doc!( 97 | doc.document(), 98 | map! { 99 | "colors" => { list! { 100 | { "Red" }, 101 | }} 102 | } 103 | ); 104 | } 105 | 106 | #[derive(Reconcile)] 107 | enum TvCommand { 108 | VolumeUp { amount: f64 }, 109 | } 110 | 111 | #[test] 112 | fn enum_named_field_variant_reconcile() { 113 | let mut doc = automerge::AutoCommit::new(); 114 | reconcile(&mut doc, TvCommand::VolumeUp { amount: 10.0 }).unwrap(); 115 | assert_doc!( 116 | doc.document(), 117 | map! { 118 | "VolumeUp" => { map! { 119 | "amount" => { 10.0 } 120 | }} 121 | } 122 | ); 123 | } 124 | 125 | #[derive(Reconcile)] 126 | enum RefString<'a> { 127 | Ref { theref: &'a str }, 128 | } 129 | 130 | #[test] 131 | fn enum_namedfield_with_refs() { 132 | let mut doc = automerge::AutoCommit::new(); 133 | reconcile(&mut doc, RefString::Ref { theref: "somestr" }).unwrap(); 134 | assert_doc!( 135 | doc.document(), 136 | map! { 137 | "Ref" => { map! { 138 | "theref" => {{ "somestr"}} 139 | }} 140 | } 141 | ); 142 | } 143 | 144 | #[derive(Reconcile)] 145 | enum Measurement { 146 | Amount(f64), 147 | } 148 | 149 | #[test] 150 | fn enum_tuple_variant_single_field_reconcile() { 151 | let mut doc = automerge::AutoCommit::new(); 152 | reconcile(&mut doc, Measurement::Amount(1.2)).unwrap(); 153 | assert_doc!( 154 | doc.document(), 155 | map! { 156 | "Amount" => { 1.2 } 157 | } 158 | ); 159 | } 160 | 161 | #[derive(Reconcile)] 162 | enum Coordinate { 163 | LatLng(f64, f64), 164 | } 165 | 166 | #[test] 167 | fn enum_tuple_variant_multi_field_reconcile() { 168 | let mut doc = automerge::AutoCommit::new(); 169 | reconcile(&mut doc, Coordinate::LatLng(1.2, 3.4)).unwrap(); 170 | assert_doc!( 171 | doc.document(), 172 | map! { 173 | "LatLng" => { list! { { 1.2 }, { 3.4 }}} 174 | } 175 | ); 176 | } 177 | 178 | #[derive(Reconcile)] 179 | enum CoordinateRef<'b> { 180 | LatLng(&'b f64, &'b f64), 181 | } 182 | 183 | #[test] 184 | fn enum_tuple_variant_with_refs() { 185 | let mut doc = automerge::AutoCommit::new(); 186 | reconcile(&mut doc, CoordinateRef::LatLng(&1.2, &3.4)).unwrap(); 187 | assert_doc!( 188 | doc.document(), 189 | map! { 190 | "LatLng" => { list! { { 1.2 }, { 3.4 }}} 191 | } 192 | ); 193 | } 194 | 195 | #[derive(Clone, Reconcile)] 196 | struct Cereal { 197 | name: String, 198 | #[key] 199 | id: u64, 200 | } 201 | 202 | #[test] 203 | fn reconcile_with_key() { 204 | let mut doc = automerge::AutoCommit::new(); 205 | let mut cereals = vec![ 206 | Cereal { 207 | name: "Weetabix".to_string(), 208 | id: 1, 209 | }, 210 | Cereal { 211 | name: "Quavars".to_string(), 212 | id: 2, 213 | }, 214 | ]; 215 | reconcile_prop(&mut doc, automerge::ROOT, "cereals", &cereals).unwrap(); 216 | 217 | let mut doc2 = doc.fork().with_actor(automerge::ActorId::random()); 218 | let mut cereals2 = cereals.clone(); 219 | cereals2.insert( 220 | 0, 221 | Cereal { 222 | name: "Oats".to_string(), 223 | id: 3, 224 | }, 225 | ); 226 | reconcile_prop(&mut doc2, automerge::ROOT, "cereals", cereals2).unwrap(); 227 | 228 | cereals.remove(0); 229 | reconcile_prop(&mut doc, automerge::ROOT, "cereals", &cereals).unwrap(); 230 | 231 | doc.merge(&mut doc2).unwrap(); 232 | 233 | assert_doc!( 234 | doc.document(), 235 | map! { 236 | "cereals" => { list! { 237 | { map! { 238 | "name" => { "Oats" }, 239 | "id" => { 3_u64 }, 240 | }}, 241 | { map! { 242 | "name" => { "Quavars" }, 243 | "id" => { 2_u64 }, 244 | }}, 245 | }} 246 | } 247 | ); 248 | } 249 | 250 | #[derive(Clone, Reconcile)] 251 | struct SpecialCereal(Cereal); 252 | 253 | #[test] 254 | fn reconcile_with_key_newtype_struct() { 255 | let mut doc = automerge::AutoCommit::new(); 256 | let mut cereals = vec![ 257 | SpecialCereal(Cereal { 258 | name: "Weetabix".to_string(), 259 | id: 1, 260 | }), 261 | SpecialCereal(Cereal { 262 | name: "Quavars".to_string(), 263 | id: 2, 264 | }), 265 | ]; 266 | reconcile_prop(&mut doc, automerge::ROOT, "cereals", &cereals).unwrap(); 267 | 268 | let mut doc2 = doc.fork().with_actor(automerge::ActorId::random()); 269 | let mut cereals2 = cereals.clone(); 270 | cereals2.insert( 271 | 0, 272 | SpecialCereal(Cereal { 273 | name: "Oats".to_string(), 274 | id: 3, 275 | }), 276 | ); 277 | reconcile_prop(&mut doc2, automerge::ROOT, "cereals", cereals2).unwrap(); 278 | 279 | cereals.remove(0); 280 | reconcile_prop(&mut doc, automerge::ROOT, "cereals", &cereals).unwrap(); 281 | 282 | doc.merge(&mut doc2).unwrap(); 283 | 284 | assert_doc!( 285 | doc.document(), 286 | map! { 287 | "cereals" => { list! { 288 | { map! { 289 | "name" => { "Oats" }, 290 | "id" => { 3_u64 }, 291 | }}, 292 | { map! { 293 | "name" => { "Quavars" }, 294 | "id" => { 2_u64 }, 295 | }}, 296 | }} 297 | } 298 | ); 299 | } 300 | 301 | #[derive(Clone, Reconcile)] 302 | struct NameWithIndex(String, #[key] u64); 303 | 304 | #[test] 305 | fn reconcile_tuple_struct_with_key() { 306 | let mut doc = automerge::AutoCommit::new(); 307 | let mut names = vec![ 308 | NameWithIndex("one".to_string(), 1), 309 | NameWithIndex("two".to_string(), 2), 310 | ]; 311 | reconcile_prop(&mut doc, automerge::ROOT, "names", &names).unwrap(); 312 | 313 | let mut doc2 = doc.fork().with_actor(automerge::ActorId::random()); 314 | let mut names2 = names.clone(); 315 | names2.insert(0, NameWithIndex("three".to_string(), 3)); 316 | reconcile_prop(&mut doc2, automerge::ROOT, "names", names2).unwrap(); 317 | 318 | names.remove(0); 319 | reconcile_prop(&mut doc, automerge::ROOT, "names", &names).unwrap(); 320 | 321 | doc.merge(&mut doc2).unwrap(); 322 | 323 | assert_doc!( 324 | doc.document(), 325 | map! { 326 | "names" => { list! { 327 | { list! { { "three" }, { 3_u64 } } }, 328 | { list! { { "two" }, { 2_u64 } } }, 329 | }} 330 | } 331 | ); 332 | } 333 | 334 | #[derive(Clone, Reconcile)] 335 | enum Fruit { 336 | Orange, 337 | Banana, 338 | Kiwi, 339 | } 340 | 341 | #[test] 342 | fn reconcile_unit_enum_key() { 343 | let mut doc = automerge::AutoCommit::new(); 344 | let mut fruits = vec![Fruit::Orange, Fruit::Banana]; 345 | reconcile_prop(&mut doc, automerge::ROOT, "fruits", &fruits).unwrap(); 346 | 347 | let mut doc2 = doc.fork().with_actor(automerge::ActorId::random()); 348 | let mut fruits2 = fruits.clone(); 349 | fruits2.remove(0); 350 | reconcile_prop(&mut doc2, automerge::ROOT, "fruits", &fruits2).unwrap(); 351 | 352 | fruits.insert(0, Fruit::Kiwi); 353 | reconcile_prop(&mut doc, automerge::ROOT, "fruits", &fruits).unwrap(); 354 | 355 | doc.merge(&mut doc2).unwrap(); 356 | 357 | assert_doc!( 358 | doc.document(), 359 | map! { 360 | "fruits" => { list! { 361 | { "Kiwi" }, 362 | { "Banana" }, 363 | } } 364 | } 365 | ); 366 | } 367 | 368 | #[derive(Clone, Reconcile)] 369 | enum Vehicle { 370 | Car { 371 | #[key] 372 | id: String, 373 | manufacturer: String, 374 | }, 375 | Truck { 376 | #[key] 377 | id: String, 378 | num_wheels: u64, 379 | }, 380 | } 381 | 382 | #[test] 383 | fn reconcile_struct_enum_key() { 384 | let mut doc = automerge::AutoCommit::new(); 385 | let mut vehicles = vec![ 386 | Vehicle::Car { 387 | id: "one".to_string(), 388 | manufacturer: "ford".to_string(), 389 | }, 390 | Vehicle::Truck { 391 | id: "two".to_string(), 392 | num_wheels: 18, 393 | }, 394 | ]; 395 | reconcile_prop(&mut doc, automerge::ROOT, "vehicles", &vehicles).unwrap(); 396 | 397 | let mut doc2 = doc.fork().with_actor(automerge::ActorId::random()); 398 | let mut vehicles2 = vehicles.clone(); 399 | vehicles2.remove(0); 400 | reconcile_prop(&mut doc2, automerge::ROOT, "vehicles", &vehicles2).unwrap(); 401 | 402 | vehicles.insert( 403 | 0, 404 | Vehicle::Car { 405 | id: "three".to_string(), 406 | manufacturer: "Audi".to_string(), 407 | }, 408 | ); 409 | let Vehicle::Truck{num_wheels, ..} = &mut vehicles[2] else { 410 | panic!("should be a truck"); 411 | }; 412 | *num_wheels = 20; 413 | reconcile_prop(&mut doc, automerge::ROOT, "vehicles", &vehicles).unwrap(); 414 | 415 | doc.merge(&mut doc2).unwrap(); 416 | 417 | assert_doc!( 418 | doc.document(), 419 | map! { 420 | "vehicles" => { list! { 421 | { map! { 422 | "Car" => { map! { 423 | "id" => { "three" }, 424 | "manufacturer" => { "Audi" }, 425 | } } 426 | } } , 427 | { map! { 428 | "Truck" => { map!{ 429 | "id" => { "two" }, 430 | "num_wheels" => { 20_u64 }, 431 | } } 432 | } 433 | } } 434 | } 435 | } 436 | ); 437 | } 438 | 439 | #[derive(Clone, Reconcile)] 440 | enum TempReading { 441 | Celsius(#[key] String, f64), 442 | Fahrenheit(#[key] String, f64), 443 | } 444 | 445 | #[test] 446 | fn reconcile_tuple_enum_key() { 447 | let mut doc = automerge::AutoCommit::new(); 448 | let mut temps = vec![ 449 | TempReading::Celsius("one".to_string(), 1.2), 450 | TempReading::Fahrenheit("two".to_string(), 3.4), 451 | ]; 452 | reconcile_prop(&mut doc, automerge::ROOT, "temps", &temps).unwrap(); 453 | 454 | let mut doc2 = doc.fork().with_actor(automerge::ActorId::random()); 455 | let mut temps2 = temps.clone(); 456 | temps2.remove(0); 457 | reconcile_prop(&mut doc2, automerge::ROOT, "temps", &temps2).unwrap(); 458 | 459 | temps.insert(0, TempReading::Celsius("three".to_string(), 5.6)); 460 | let TempReading::Fahrenheit(_, temp) = &mut temps[2] else { 461 | panic!("should be a fahrenheit"); 462 | }; 463 | *temp = 7.8; 464 | reconcile_prop(&mut doc, automerge::ROOT, "temps", &temps).unwrap(); 465 | 466 | doc.merge(&mut doc2).unwrap(); 467 | 468 | assert_doc!( 469 | doc.document(), 470 | map! { 471 | "temps" => { list! { 472 | { map! { 473 | "Celsius" => { list! { 474 | { "three" }, 475 | { 5.6_f64 }, 476 | } } 477 | } } , 478 | { map! { 479 | "Fahrenheit" => { list!{ 480 | { "two" }, 481 | { 7.8_f64 }, 482 | } } 483 | } 484 | }} 485 | } 486 | } 487 | ); 488 | } 489 | 490 | #[test] 491 | fn reconcile_between_enum_variants() { 492 | let mut doc = automerge::AutoCommit::new(); 493 | let temp = TempReading::Celsius("one".to_string(), 1.2); 494 | reconcile_prop(&mut doc, automerge::ROOT, "temp", &temp).unwrap(); 495 | 496 | let temp = TempReading::Fahrenheit("three".to_string(), 6.7); 497 | reconcile_prop(&mut doc, automerge::ROOT, "temp", &temp).unwrap(); 498 | 499 | assert_doc!( 500 | doc.document(), 501 | map! { 502 | "temp" => { map! { 503 | "Fahrenheit" => { list! { 504 | { "three" }, 505 | { 6.7_f64 }, 506 | } } 507 | } 508 | } 509 | } 510 | ); 511 | } 512 | 513 | mod enumkeyvisibility { 514 | use autosurgeon::Reconcile; 515 | 516 | // Check that the key type derived by `Reconcile` has the correct visibility 517 | #[derive(Reconcile)] 518 | #[allow(dead_code)] 519 | pub enum Thing { 520 | ThatThing, 521 | TheOtherThing, 522 | } 523 | } 524 | 525 | // Reproduce https://github.com/alexjg/autosurgeon/issues/9 526 | #[derive(Reconcile)] 527 | pub enum Ports { 528 | Range(u16, u16), 529 | Collection(Vec), 530 | } 531 | -------------------------------------------------------------------------------- /autosurgeon-derive/tests/reconcile_with.rs: -------------------------------------------------------------------------------- 1 | use automerge::ActorId; 2 | use automerge_test::{assert_doc, list, map}; 3 | use autosurgeon::{reconcile_prop, Hydrate, Reconcile, Reconciler}; 4 | 5 | #[derive(Debug, Clone, PartialEq, Eq)] 6 | struct Inner(u64); 7 | 8 | #[derive(Debug, Clone, PartialEq, Eq, Reconcile)] 9 | #[autosurgeon(reconcile = "reconcile_outer")] 10 | struct Outer(Inner); 11 | 12 | impl Hydrate for Outer { 13 | fn hydrate_uint(u: u64) -> Result { 14 | Ok(Outer(Inner(u))) 15 | } 16 | } 17 | 18 | fn reconcile_outer(outer: &Outer, reconciler: R) -> Result<(), R::Error> { 19 | outer.0 .0.reconcile(reconciler)?; 20 | Ok(()) 21 | } 22 | 23 | #[test] 24 | fn reconcile_on_newtype() { 25 | let mut doc = automerge::AutoCommit::new(); 26 | let val = Outer(Inner(2)); 27 | reconcile_prop(&mut doc, automerge::ROOT, "value", &val).unwrap(); 28 | assert_doc!( 29 | doc.document(), 30 | map! { 31 | "value" => { 2_u64 } 32 | } 33 | ); 34 | } 35 | 36 | #[test] 37 | fn reconcile_with_uses_identity_key() { 38 | let mut doc = automerge::AutoCommit::new(); 39 | let mut vals = vec![Outer(Inner(1)), Outer(Inner(2))]; 40 | reconcile_prop(&mut doc, automerge::ROOT, "values", &vals).unwrap(); 41 | 42 | let mut doc2 = doc.fork().with_actor(automerge::ActorId::random()); 43 | let mut vals2 = vals.clone(); 44 | vals2.remove(0); 45 | reconcile_prop(&mut doc2, automerge::ROOT, "values", &vals2).unwrap(); 46 | 47 | vals.insert(0, Outer(Inner(3))); 48 | reconcile_prop(&mut doc, automerge::ROOT, "values", &vals).unwrap(); 49 | 50 | doc.merge(&mut doc2).unwrap(); 51 | 52 | assert_doc!( 53 | doc.document(), 54 | map! { 55 | "values" => {list!{ 56 | { 3_u64 }, 57 | { 2_u64 }, 58 | }} 59 | } 60 | ); 61 | } 62 | 63 | #[derive(Debug, Clone, PartialEq)] 64 | struct InnerString(String); 65 | 66 | #[derive(Debug, Clone, PartialEq, Reconcile)] 67 | #[autosurgeon(reconcile_with = "autosurgeon_customerstring")] 68 | struct CustomerString(InnerString); 69 | 70 | impl CustomerString { 71 | fn id(&self) -> u64 { 72 | self.0 .0.split('_').nth(1).unwrap().parse().unwrap() 73 | } 74 | } 75 | 76 | mod autosurgeon_customerstring { 77 | use autosurgeon::{ 78 | hydrate::{hydrate_path, HydrateResultExt}, 79 | reconcile::LoadKey, 80 | ReadDoc, Reconcile, 81 | }; 82 | 83 | use super::CustomerString; 84 | 85 | pub type Key<'a> = u64; 86 | 87 | pub fn hydrate_key<'k, D: ReadDoc>( 88 | doc: &D, 89 | obj: &automerge::ObjId, 90 | prop: autosurgeon::Prop<'_>, 91 | ) -> Result>, autosurgeon::ReconcileError> { 92 | let val: Option = 93 | hydrate_path(doc, obj, std::iter::once(prop)).strip_unexpected()?; 94 | Ok(val 95 | .and_then(|v| v.split('_').nth(1).map(|s| s.to_string())) 96 | .and_then(|id| id.parse().ok()) 97 | .map(LoadKey::Found) 98 | .unwrap_or(LoadKey::KeyNotFound)) 99 | } 100 | 101 | pub(crate) fn key(s: &CustomerString) -> LoadKey { 102 | LoadKey::Found(s.id()) 103 | } 104 | 105 | pub(crate) fn reconcile( 106 | c: &CustomerString, 107 | reconciler: R, 108 | ) -> Result<(), R::Error> { 109 | c.0 .0.reconcile(reconciler)?; 110 | Ok(()) 111 | } 112 | } 113 | 114 | #[test] 115 | fn reconcile_key_with_module() { 116 | let mut doc = automerge::AutoCommit::new(); 117 | let mut vals = vec![ 118 | CustomerString(InnerString("albert_1".to_string())), 119 | CustomerString(InnerString("emma_2".to_string())), 120 | ]; 121 | reconcile_prop(&mut doc, automerge::ROOT, "values", &vals).unwrap(); 122 | 123 | let mut doc2 = doc.fork().with_actor(automerge::ActorId::random()); 124 | let mut vals2 = vals.clone(); 125 | 126 | vals2.insert(0, CustomerString(InnerString("clive_3".to_string()))); 127 | reconcile_prop(&mut doc2, automerge::ROOT, "values", &vals2).unwrap(); 128 | 129 | vals.remove(0); 130 | reconcile_prop(&mut doc, automerge::ROOT, "values", &vals).unwrap(); 131 | 132 | doc.merge(&mut doc2).unwrap(); 133 | 134 | assert_doc!( 135 | doc.document(), 136 | map! { 137 | "values" => {list!{ 138 | { "clive_3" }, 139 | { "emma_2" }, 140 | }} 141 | } 142 | ); 143 | } 144 | 145 | #[derive(Clone, Debug, PartialEq, Eq, Hydrate)] 146 | struct InnerId(u64); 147 | #[derive(Clone, Debug, PartialEq, Eq, Hydrate)] 148 | struct ProductId(InnerId); 149 | 150 | #[derive(Clone, Debug, PartialEq, Reconcile)] 151 | struct Product { 152 | #[key] 153 | #[autosurgeon(reconcile = "reconcile_productid")] 154 | id: ProductId, 155 | name: String, 156 | } 157 | 158 | fn reconcile_productid(id: &ProductId, mut reconciler: R) -> Result<(), R::Error> { 159 | reconciler.u64(id.0 .0) 160 | } 161 | 162 | #[test] 163 | fn reconcile_with_struct_field() { 164 | let mut vals = vec![ 165 | Product { 166 | id: ProductId(InnerId(1)), 167 | name: "Christmas Tree".to_string(), 168 | }, 169 | Product { 170 | id: ProductId(InnerId(2)), 171 | name: "Crackers".to_string(), 172 | }, 173 | ]; 174 | let mut doc = automerge::AutoCommit::new(); 175 | reconcile_prop(&mut doc, automerge::ROOT, "products", &vals).unwrap(); 176 | 177 | let mut doc2 = doc.fork().with_actor(automerge::ActorId::random()); 178 | let mut vals2 = vals.clone(); 179 | vals2.insert( 180 | 0, 181 | Product { 182 | id: ProductId(InnerId(3)), 183 | name: "Cake".to_string(), 184 | }, 185 | ); 186 | reconcile_prop(&mut doc2, automerge::ROOT, "products", &vals2).unwrap(); 187 | 188 | vals.remove(0); 189 | reconcile_prop(&mut doc, automerge::ROOT, "products", &vals).unwrap(); 190 | 191 | doc.merge(&mut doc2).unwrap(); 192 | 193 | assert_doc!( 194 | doc.document(), 195 | map! { 196 | "products" => { list!{ 197 | { map! { 198 | "id" => { 3_u64 }, 199 | "name" => { "Cake" }, 200 | }}, 201 | { map! { 202 | "id" => { 2_u64 }, 203 | "name" => { "Crackers" }, 204 | }}, 205 | }} 206 | } 207 | ) 208 | } 209 | 210 | #[derive(Debug, Clone)] 211 | struct SpecialFloat(f64); 212 | #[derive(Debug, Clone, Reconcile)] 213 | struct TwoVector( 214 | #[autosurgeon(reconcile = "reconcile_specialfloat")] SpecialFloat, 215 | #[autosurgeon(reconcile = "reconcile_specialfloat")] SpecialFloat, 216 | ); 217 | 218 | fn reconcile_specialfloat( 219 | f: &SpecialFloat, 220 | mut reconciler: R, 221 | ) -> Result<(), R::Error> { 222 | reconciler.f64(f.0) 223 | } 224 | 225 | #[test] 226 | fn test_reconcile_tuple_field() { 227 | let mut doc = automerge::AutoCommit::new(); 228 | reconcile_prop( 229 | &mut doc, 230 | automerge::ROOT, 231 | "value", 232 | TwoVector(SpecialFloat(1.0), SpecialFloat(2.0)), 233 | ) 234 | .unwrap(); 235 | assert_doc!( 236 | doc.document(), 237 | map! { 238 | "value" => {list! { 239 | { 1.0 }, 240 | { 2.0 }, 241 | }} 242 | } 243 | ); 244 | } 245 | 246 | #[derive(Clone, Debug, PartialEq, Reconcile)] 247 | enum KnownProduct { 248 | TennisRacket { 249 | #[key] 250 | #[autosurgeon(reconcile = "reconcile_productid")] 251 | id: ProductId, 252 | brand: String, 253 | }, 254 | } 255 | 256 | #[test] 257 | fn test_reconcile_with_enum_variants() { 258 | let mut vals = vec![ 259 | KnownProduct::TennisRacket { 260 | id: ProductId(InnerId(1)), 261 | brand: "slazenger".to_string(), 262 | }, 263 | KnownProduct::TennisRacket { 264 | id: ProductId(InnerId(2)), 265 | brand: "Nike".to_string(), 266 | }, 267 | ]; 268 | let mut doc = automerge::AutoCommit::new(); 269 | reconcile_prop(&mut doc, automerge::ROOT, "products", &vals).unwrap(); 270 | 271 | let mut doc2 = doc.fork().with_actor(automerge::ActorId::random()); 272 | let mut vals2 = vals.clone(); 273 | vals2.insert( 274 | 0, 275 | KnownProduct::TennisRacket { 276 | id: ProductId(InnerId(3)), 277 | brand: "Adidas".to_string(), 278 | }, 279 | ); 280 | reconcile_prop(&mut doc2, automerge::ROOT, "products", &vals2).unwrap(); 281 | 282 | vals.remove(0); 283 | reconcile_prop(&mut doc, automerge::ROOT, "products", &vals).unwrap(); 284 | 285 | doc.merge(&mut doc2).unwrap(); 286 | 287 | assert_doc!( 288 | doc.document(), 289 | map! { 290 | "products" => { list!{ 291 | { map! { 292 | "TennisRacket" => { map! { 293 | "id" => { 3_u64 }, 294 | "brand" => { "Adidas" }, 295 | }} 296 | }}, 297 | { map! { 298 | "TennisRacket" => { map! { 299 | "id" => { 2_u64 }, 300 | "brand" => { "Nike" }, 301 | }} 302 | }}, 303 | }} 304 | } 305 | ) 306 | } 307 | 308 | #[derive(Clone, PartialEq, Eq, Debug, Hydrate)] 309 | struct InnerInt(u64); 310 | #[derive(Clone, PartialEq, Eq, Debug, Hydrate)] 311 | struct ColorInt(InnerInt); 312 | 313 | #[derive(Clone, Debug, Reconcile)] 314 | enum Color { 315 | Rgb( 316 | #[key] 317 | #[autosurgeon(reconcile = "reconcile_color")] 318 | ColorInt, 319 | u64, 320 | u64, 321 | ), 322 | Cmyk( 323 | #[key] 324 | #[autosurgeon(reconcile = "reconcile_color")] 325 | ColorInt, 326 | u64, 327 | u64, 328 | u64, 329 | ), 330 | } 331 | 332 | fn reconcile_color(color: &ColorInt, mut reconciler: R) -> Result<(), R::Error> { 333 | reconciler.u64(color.0 .0) 334 | } 335 | 336 | #[test] 337 | fn test_reconcile_with_tuple_variants() { 338 | let mut doc = automerge::AutoCommit::new(); 339 | let vals = vec![ 340 | Color::Rgb(ColorInt(InnerInt(0)), 0, 0), 341 | Color::Cmyk(ColorInt(InnerInt(256)), 256, 256, 256), 342 | ]; 343 | reconcile_prop(&mut doc, automerge::ROOT, "colors", &vals).unwrap(); 344 | 345 | let mut vals2 = vals.clone(); 346 | vals2.insert(1, Color::Rgb(ColorInt(InnerInt(5)), 5, 5)); 347 | let mut doc2 = doc.fork().with_actor(ActorId::random()); 348 | reconcile_prop(&mut doc2, automerge::ROOT, "colors", &vals2).unwrap(); 349 | 350 | doc.merge(&mut doc2).unwrap(); 351 | 352 | assert_doc!( 353 | doc.document(), 354 | map! { 355 | "colors" => { list! { 356 | { map! { 357 | "Rgb" => { list!{ {0_u64}, {0_u64}, {0_u64} }}, 358 | }}, 359 | { map! { 360 | "Rgb" => { list!{ {5_u64}, {5_u64}, {5_u64} }}, 361 | }}, 362 | { map! { 363 | "Cmyk" => { list!{ {256_u64}, {256_u64}, {256_u64}, {256_u64} }}, 364 | }}, 365 | }} 366 | } 367 | ); 368 | } 369 | 370 | struct CustomerId(&'static str); 371 | 372 | #[derive(Reconcile)] 373 | struct Customer { 374 | #[autosurgeon(with = "autosurgeon_customerid")] 375 | id: CustomerId, 376 | } 377 | 378 | mod autosurgeon_customerid { 379 | use autosurgeon::Reconcile; 380 | 381 | pub(super) fn reconcile( 382 | c: &super::CustomerId, 383 | reconciler: R, 384 | ) -> Result<(), R::Error> { 385 | c.0.reconcile(reconciler) 386 | } 387 | } 388 | 389 | #[test] 390 | fn reconcile_with_without_key() { 391 | let mut doc = automerge::AutoCommit::new(); 392 | let customer = Customer { 393 | id: CustomerId("customer"), 394 | }; 395 | autosurgeon::reconcile(&mut doc, &customer).unwrap(); 396 | 397 | assert_doc!( 398 | doc.document(), 399 | map! { 400 | "id" => { "customer" } 401 | } 402 | ); 403 | } 404 | -------------------------------------------------------------------------------- /autosurgeon-derive/tests/reconcile_with_hydrate_with_key.rs: -------------------------------------------------------------------------------- 1 | use automerge::ActorId; 2 | use automerge_test::{assert_doc, list, map}; 3 | use autosurgeon::{reconcile_prop, Hydrate, HydrateError, Prop, ReadDoc, Reconcile, Reconciler}; 4 | 5 | #[derive(Debug, Clone, Eq, PartialEq)] 6 | struct UserId(String); 7 | 8 | #[derive(Clone, Reconcile)] 9 | struct User { 10 | #[key] 11 | #[autosurgeon(reconcile = "reconcile_userid", hydrate = "hydrate_userid")] 12 | id: UserId, 13 | name: String, 14 | } 15 | 16 | fn reconcile_userid(id: &UserId, reconciler: R) -> Result<(), R::Error> { 17 | id.0.reconcile(reconciler) 18 | } 19 | 20 | fn hydrate_userid( 21 | doc: &D, 22 | obj: &automerge::ObjId, 23 | prop: Prop<'_>, 24 | ) -> Result { 25 | Ok(UserId(String::hydrate(doc, obj, prop)?)) 26 | } 27 | 28 | #[test] 29 | fn on_struct_namedfield() { 30 | let mut users = vec![ 31 | User { 32 | id: UserId("one".to_string()), 33 | name: "one".to_string(), 34 | }, 35 | User { 36 | id: UserId("two".to_string()), 37 | name: "two".to_string(), 38 | }, 39 | ]; 40 | let mut doc = automerge::AutoCommit::new(); 41 | reconcile_prop(&mut doc, automerge::ROOT, "users", &users).unwrap(); 42 | 43 | let mut users2 = users.clone(); 44 | users2.insert( 45 | 0, 46 | User { 47 | id: UserId("three".to_string()), 48 | name: "three".to_string(), 49 | }, 50 | ); 51 | let mut doc2 = doc.fork().with_actor(ActorId::random()); 52 | reconcile_prop(&mut doc2, automerge::ROOT, "users", &users2).unwrap(); 53 | 54 | users.remove(0); 55 | reconcile_prop(&mut doc, automerge::ROOT, "users", &users).unwrap(); 56 | 57 | doc.merge(&mut doc2).unwrap(); 58 | 59 | assert_doc!( 60 | doc.document(), 61 | map! { 62 | "users" => { list! { 63 | { map! { 64 | "id" => { "three" }, 65 | "name" => { "three" }, 66 | }}, 67 | { map! { 68 | "id" => { "two" }, 69 | "name" => { "two" }, 70 | }} 71 | }} 72 | } 73 | ); 74 | } 75 | 76 | #[derive(Debug, PartialEq, Clone, Reconcile)] 77 | enum Ids { 78 | User(#[autosurgeon(reconcile_with = "reconcile_userid_mod")] UserId), 79 | } 80 | 81 | mod reconcile_userid_mod { 82 | use super::UserId; 83 | use autosurgeon::{ 84 | hydrate::{hydrate_path, HydrateResultExt}, 85 | reconcile::LoadKey, 86 | ReadDoc, Reconcile, Reconciler, 87 | }; 88 | pub type Key<'a> = std::borrow::Cow<'a, String>; 89 | 90 | pub(super) fn reconcile(id: &UserId, reconciler: R) -> Result<(), R::Error> { 91 | id.0.reconcile(reconciler) 92 | } 93 | 94 | pub(super) fn hydrate_key<'k, D: ReadDoc>( 95 | doc: &D, 96 | obj: &automerge::ObjId, 97 | prop: autosurgeon::Prop<'_>, 98 | ) -> Result>, autosurgeon::ReconcileError> { 99 | let val: Option> = 100 | hydrate_path(doc, obj, std::iter::once(prop)).strip_unexpected()?; 101 | Ok(val.map(LoadKey::Found).unwrap_or(LoadKey::KeyNotFound)) 102 | } 103 | 104 | pub(super) fn key(u: &UserId) -> LoadKey> { 105 | LoadKey::Found(std::borrow::Cow::Borrowed(&u.0)) 106 | } 107 | } 108 | 109 | #[test] 110 | fn reconcile_and_hydrate_on_newtype_field() { 111 | let mut ids = vec![ 112 | Ids::User(UserId("one".to_string())), 113 | Ids::User(UserId("two".to_string())), 114 | ]; 115 | let mut doc = automerge::AutoCommit::new(); 116 | reconcile_prop(&mut doc, automerge::ROOT, "ids", &ids).unwrap(); 117 | 118 | let mut ids2 = ids.clone(); 119 | let mut doc2 = doc.fork().with_actor(ActorId::random()); 120 | ids2.insert(0, Ids::User(UserId("three".to_string()))); 121 | reconcile_prop(&mut doc2, automerge::ROOT, "ids", &ids2).unwrap(); 122 | 123 | ids.remove(0); 124 | reconcile_prop(&mut doc, automerge::ROOT, "ids", &ids).unwrap(); 125 | 126 | doc.merge(&mut doc2).unwrap(); 127 | 128 | assert_doc!( 129 | doc.document(), 130 | map! { 131 | "ids" => { list! { 132 | { map! {"User" => { "three" } }}, 133 | { map! {"User" => { "two" } }}, 134 | }} 135 | } 136 | ); 137 | } 138 | -------------------------------------------------------------------------------- /autosurgeon-derive/tests/with.rs: -------------------------------------------------------------------------------- 1 | use automerge::{transaction::Transactable, ObjType}; 2 | use automerge_test::{assert_doc, list, map}; 3 | use autosurgeon::{hydrate_prop, reconcile_prop, Hydrate, Reconcile}; 4 | 5 | struct UserId(String); 6 | 7 | #[derive(Hydrate, Reconcile)] 8 | struct User { 9 | #[autosurgeon(with = "autosurgeon_userid")] 10 | id: UserId, 11 | name: String, 12 | } 13 | 14 | mod autosurgeon_userid { 15 | use super::UserId; 16 | use autosurgeon::{ 17 | hydrate::{hydrate_path, Hydrate, HydrateResultExt}, 18 | reconcile::LoadKey, 19 | ReadDoc, Reconcile, Reconciler, 20 | }; 21 | pub type Key<'a> = std::borrow::Cow<'a, String>; 22 | 23 | pub(super) fn reconcile(id: &UserId, reconciler: R) -> Result<(), R::Error> { 24 | id.0.reconcile(reconciler) 25 | } 26 | 27 | pub(super) fn hydrate_key<'k, D: ReadDoc>( 28 | doc: &D, 29 | obj: &automerge::ObjId, 30 | prop: autosurgeon::Prop<'_>, 31 | ) -> Result>, autosurgeon::ReconcileError> { 32 | let val = 33 | hydrate_path::<_, std::borrow::Cow<'_, String>, _>(doc, obj, std::iter::once(prop)) 34 | .strip_unexpected()?; 35 | Ok(val.map(LoadKey::Found).unwrap_or(LoadKey::KeyNotFound)) 36 | } 37 | 38 | pub(super) fn key(u: &UserId) -> LoadKey> { 39 | LoadKey::Found(std::borrow::Cow::Borrowed(&u.0)) 40 | } 41 | 42 | pub(super) fn hydrate( 43 | doc: &D, 44 | obj: &automerge::ObjId, 45 | prop: autosurgeon::Prop<'_>, 46 | ) -> Result { 47 | Ok(UserId(String::hydrate(doc, obj, prop)?)) 48 | } 49 | } 50 | 51 | #[test] 52 | fn test_with() { 53 | let mut doc = automerge::AutoCommit::new(); 54 | let users = doc 55 | .put_object(&automerge::ROOT, "users", ObjType::List) 56 | .unwrap(); 57 | let user1 = doc.insert_object(&users, 0, ObjType::Map).unwrap(); 58 | doc.put(&user1, "id", "one".to_string()).unwrap(); 59 | doc.put(&user1, "name", "nameone".to_string()).unwrap(); 60 | 61 | let mut users: Vec = hydrate_prop(&doc, &automerge::ROOT, "users").unwrap(); 62 | 63 | users.insert( 64 | 0, 65 | User { 66 | id: UserId("two".to_string()), 67 | name: "nametwo".to_string(), 68 | }, 69 | ); 70 | 71 | reconcile_prop(&mut doc, automerge::ROOT, "users", &users).unwrap(); 72 | 73 | assert_doc!( 74 | doc.document(), 75 | map! { 76 | "users" => { list! { 77 | { map! { 78 | "id" => { "two" }, 79 | "name" => { "nametwo" }, 80 | }}, 81 | { map! { 82 | "id" => { "one" }, 83 | "name" => { "nameone" }, 84 | }} 85 | }} 86 | } 87 | ); 88 | } 89 | 90 | #[derive(Reconcile, Hydrate)] 91 | struct SpecialUserId(#[autosurgeon(with = "autosurgeon_userid")] UserId); 92 | 93 | #[test] 94 | fn with_on_tuplestruct() { 95 | let mut doc = automerge::AutoCommit::new(); 96 | doc.put(automerge::ROOT, "userid", "one".to_string()) 97 | .unwrap(); 98 | let mut uid: SpecialUserId = hydrate_prop(&doc, &automerge::ROOT, "userid").unwrap(); 99 | 100 | uid.0 = UserId("two".to_string()); 101 | reconcile_prop(&mut doc, automerge::ROOT, "userid", &uid).unwrap(); 102 | assert_doc!(doc.document(), map! {"userid" => { "two" }}); 103 | } 104 | -------------------------------------------------------------------------------- /autosurgeon/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "autosurgeon" 3 | description = "A library for working with data in automerge documents" 4 | version = "0.4.0" 5 | edition = "2021" 6 | authors = ["Alex Good "] 7 | license = "MIT" 8 | rust-version = "1.65.0" 9 | repository = "https://github.com/alexjg/autosurgeon" 10 | readme = "../README.md" 11 | 12 | [dependencies] 13 | automerge = { version = "^0.3" } 14 | thiserror = "1.0.37" 15 | smol_str = { version = "^0.1.21" } 16 | autosurgeon-derive = { path = "../autosurgeon-derive", version = "0.4.0" } 17 | similar = "2.2.1" 18 | uuid = { version = "1.2.2", optional = true } 19 | 20 | [dev-dependencies] 21 | automerge-test = "^0.2" 22 | 23 | [features] 24 | uuid = ["dep:uuid"] 25 | -------------------------------------------------------------------------------- /autosurgeon/src/bytes.rs: -------------------------------------------------------------------------------- 1 | //! Newtypes for `[u8;N]` and `Vec` which encode to [`automerge::ScalarValue::Bytes`] 2 | //! 3 | //! This is necessary because otherwise we get conflicting implementations of `Reconcile` and 4 | //! `Hydrate` when we implement these traits for `u8` and `Vec`. 5 | 6 | use std::ops::Deref; 7 | 8 | use crate::{Hydrate, HydrateError, Reconcile}; 9 | 10 | #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)] 11 | pub struct ByteArray([u8; N]); 12 | 13 | impl From<[u8; N]> for ByteArray { 14 | fn from(b: [u8; N]) -> Self { 15 | Self(b) 16 | } 17 | } 18 | 19 | impl From> for [u8; N] { 20 | fn from(b: ByteArray) -> Self { 21 | b.0 22 | } 23 | } 24 | 25 | impl ByteArray { 26 | fn expected() -> String { 27 | format!("a byte array of length {}", N) 28 | } 29 | } 30 | 31 | impl Reconcile for ByteArray { 32 | type Key<'a> = crate::reconcile::NoKey; 33 | 34 | fn reconcile(&self, mut reconciler: R) -> Result<(), R::Error> { 35 | reconciler.bytes(self.0) 36 | } 37 | } 38 | 39 | impl Hydrate for ByteArray { 40 | fn hydrate_bytes(bytes: &[u8]) -> Result { 41 | let raw = bytes.to_vec(); 42 | let inner = raw.try_into().map_err(|e: Vec| { 43 | HydrateError::unexpected(Self::expected(), format!("an array of length {}", e.len())) 44 | })?; 45 | Ok(Self(inner)) 46 | } 47 | } 48 | 49 | impl Deref for ByteArray { 50 | type Target = [u8; N]; 51 | 52 | fn deref(&self) -> &Self::Target { 53 | &self.0 54 | } 55 | } 56 | 57 | impl AsMut<[u8; N]> for ByteArray { 58 | fn as_mut(&mut self) -> &mut [u8; N] { 59 | &mut self.0 60 | } 61 | } 62 | 63 | impl AsRef<[u8; N]> for ByteArray { 64 | fn as_ref(&self) -> &[u8; N] { 65 | &self.0 66 | } 67 | } 68 | 69 | #[derive(Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)] 70 | pub struct ByteVec(Vec); 71 | 72 | impl From> for ByteVec { 73 | fn from(b: Vec) -> Self { 74 | Self(b) 75 | } 76 | } 77 | 78 | impl From for Vec { 79 | fn from(s: ByteVec) -> Self { 80 | s.0 81 | } 82 | } 83 | 84 | impl Reconcile for ByteVec { 85 | type Key<'a> = crate::reconcile::NoKey; 86 | 87 | fn reconcile(&self, mut reconciler: R) -> Result<(), R::Error> { 88 | reconciler.bytes(&self.0) 89 | } 90 | } 91 | 92 | impl Hydrate for ByteVec { 93 | fn hydrate_bytes(bytes: &[u8]) -> Result { 94 | Ok(Self(bytes.to_vec())) 95 | } 96 | } 97 | 98 | impl Deref for ByteVec { 99 | type Target = Vec; 100 | 101 | fn deref(&self) -> &Self::Target { 102 | &self.0 103 | } 104 | } 105 | 106 | impl AsMut> for ByteVec { 107 | fn as_mut(&mut self) -> &mut Vec { 108 | &mut self.0 109 | } 110 | } 111 | 112 | impl AsRef> for ByteVec { 113 | fn as_ref(&self) -> &Vec { 114 | &self.0 115 | } 116 | } 117 | 118 | #[cfg(test)] 119 | mod tests { 120 | use super::{ByteArray, ByteVec}; 121 | use automerge as am; 122 | 123 | use crate::{hydrate_prop, reconcile_prop}; 124 | 125 | #[test] 126 | fn round_trip_array() { 127 | let mut doc = am::AutoCommit::new(); 128 | let value: ByteArray<4> = [1_u8, 2, 3, 4].into(); 129 | reconcile_prop(&mut doc, am::ROOT, "values", value).unwrap(); 130 | 131 | let result: ByteArray<4> = hydrate_prop(&doc, am::ROOT, "values").unwrap(); 132 | assert_eq!(result, value); 133 | } 134 | 135 | #[test] 136 | fn round_trip_vec() { 137 | let mut doc = am::AutoCommit::new(); 138 | let value: ByteVec = vec![1_u8, 2, 3, 4].into(); 139 | reconcile_prop(&mut doc, am::ROOT, "values", &value).unwrap(); 140 | 141 | let result: ByteVec = hydrate_prop(&doc, am::ROOT, "values").unwrap(); 142 | assert_eq!(result, value); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /autosurgeon/src/counter.rs: -------------------------------------------------------------------------------- 1 | use crate::{reconcile::CounterReconciler, Hydrate, Reconcile}; 2 | 3 | /// A type which reconciles to an [`automerge::ScalarValue::Counter`] 4 | /// 5 | /// # Example 6 | /// 7 | /// ```rust 8 | /// # use autosurgeon::{Counter, reconcile, hydrate, Reconcile, Hydrate}; 9 | /// # use automerge::ActorId; 10 | /// #[derive(Debug, Reconcile, Hydrate)] 11 | /// struct Stats { 12 | /// num_clicks: Counter, 13 | /// } 14 | /// let mut doc = automerge::AutoCommit::new(); 15 | /// let mut stats = Stats {num_clicks: Counter::default() }; 16 | /// reconcile(&mut doc, &stats).unwrap(); 17 | /// 18 | /// // Fork the doc and increment the counter 19 | /// let mut doc2 = doc.fork().with_actor(ActorId::random()); 20 | /// let mut stats2: Stats = hydrate(&doc).unwrap(); 21 | /// stats2.num_clicks.increment(5); 22 | /// reconcile(&mut doc2, &stats2).unwrap(); 23 | /// 24 | /// // Concurrently increment in the original doc 25 | /// let mut stats: Stats = hydrate(&doc).unwrap(); 26 | /// stats.num_clicks.increment(3); 27 | /// reconcile(&mut doc, &stats).unwrap(); 28 | /// 29 | /// // Merge the two docs 30 | /// doc.merge(&mut doc2).unwrap(); 31 | /// 32 | /// // Observe that `num_clicks` is the sum of the concurrent increments 33 | /// let stats: Stats = hydrate(&doc).unwrap(); 34 | /// assert_eq!(stats.num_clicks.value(), 8); 35 | /// ``` 36 | pub struct Counter(State); 37 | 38 | impl std::fmt::Debug for Counter { 39 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 40 | f.debug_struct("Counter") 41 | .field("value", &self.value()) 42 | .finish() 43 | } 44 | } 45 | 46 | impl std::default::Default for Counter { 47 | fn default() -> Self { 48 | Self::with_value(0) 49 | } 50 | } 51 | 52 | enum State { 53 | Fresh(i64), 54 | Rehydrated { original: i64, increment: i64 }, 55 | } 56 | 57 | impl Counter { 58 | pub fn with_value(value: i64) -> Self { 59 | Self(State::Fresh(value)) 60 | } 61 | 62 | pub fn increment(&mut self, by: i64) { 63 | match &mut self.0 { 64 | State::Fresh(v) => *v += by, 65 | State::Rehydrated { increment, .. } => *increment += by, 66 | } 67 | } 68 | 69 | pub fn value(&self) -> i64 { 70 | match self.0 { 71 | State::Fresh(v) => v, 72 | State::Rehydrated { 73 | original, 74 | increment, 75 | } => original + increment, 76 | } 77 | } 78 | } 79 | 80 | impl Reconcile for Counter { 81 | type Key<'a> = crate::reconcile::NoKey; 82 | 83 | fn reconcile(&self, mut reconciler: R) -> Result<(), R::Error> { 84 | let mut c = reconciler.counter()?; 85 | match self.0 { 86 | State::Fresh(v) => c.set(v)?, 87 | State::Rehydrated { increment, .. } => c.increment(increment)?, 88 | }; 89 | Ok(()) 90 | } 91 | } 92 | 93 | impl Hydrate for Counter { 94 | fn hydrate_counter(c: i64) -> Result { 95 | Ok(Counter(State::Rehydrated { 96 | original: c, 97 | increment: 0, 98 | })) 99 | } 100 | } 101 | 102 | #[cfg(test)] 103 | mod tests { 104 | use automerge::ActorId; 105 | 106 | use super::Counter; 107 | use crate::{hydrate_prop, reconcile_prop}; 108 | 109 | #[test] 110 | fn simple_increment() { 111 | let mut doc = automerge::AutoCommit::new(); 112 | let counter = Counter::default(); 113 | reconcile_prop(&mut doc, automerge::ROOT, "counter", &counter).unwrap(); 114 | 115 | let mut counter2: Counter = hydrate_prop(&doc, &automerge::ROOT, "counter").unwrap(); 116 | let mut doc2 = doc.fork().with_actor(ActorId::random()); 117 | counter2.increment(5); 118 | reconcile_prop(&mut doc2, automerge::ROOT, "counter", &counter2).unwrap(); 119 | 120 | let mut counter3: Counter = hydrate_prop(&doc, &automerge::ROOT, "counter").unwrap(); 121 | counter3.increment(3); 122 | reconcile_prop(&mut doc, automerge::ROOT, "counter", &counter3).unwrap(); 123 | 124 | doc.merge(&mut doc2).unwrap(); 125 | 126 | let counter: Counter = hydrate_prop(&doc, &automerge::ROOT, "counter").unwrap(); 127 | assert_eq!(counter.value(), 8); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /autosurgeon/src/doc.rs: -------------------------------------------------------------------------------- 1 | use std::ops::RangeBounds; 2 | 3 | use automerge::{self as am, AutomergeError, ObjId, Value}; 4 | 5 | /// An abstraction over the different ways of reading an automerge document 6 | pub trait ReadDoc { 7 | type Parents<'a>: Iterator 8 | where 9 | Self: 'a; 10 | fn get_heads(&self) -> Vec; 11 | 12 | fn get>( 13 | &self, 14 | obj: &ObjId, 15 | prop: P, 16 | ) -> Result, ObjId)>, AutomergeError>; 17 | 18 | fn object_type>(&self, obj: O) -> Option; 19 | fn map_range, R: RangeBounds>( 20 | &self, 21 | obj: O, 22 | range: R, 23 | ) -> am::MapRange<'_, R>; 24 | 25 | fn list_range, R: RangeBounds>( 26 | &self, 27 | obj: O, 28 | range: R, 29 | ) -> am::ListRange<'_, R>; 30 | 31 | fn length>(&self, obj: O) -> usize; 32 | 33 | fn text>(&self, obj: O) -> Result; 34 | fn parents>(&self, obj: O) -> Result, AutomergeError>; 35 | } 36 | 37 | /// An abstraction over the read + write operations we need from an automerge document 38 | pub trait Doc: ReadDoc { 39 | fn put, P: Into, V: Into>( 40 | &mut self, 41 | obj: O, 42 | prop: P, 43 | value: V, 44 | ) -> Result<(), AutomergeError>; 45 | 46 | fn put_object, P: Into>( 47 | &mut self, 48 | obj: O, 49 | prop: P, 50 | value: am::ObjType, 51 | ) -> Result; 52 | 53 | fn insert, V: Into>( 54 | &mut self, 55 | obj: O, 56 | index: usize, 57 | value: V, 58 | ) -> Result<(), AutomergeError>; 59 | 60 | fn insert_object>( 61 | &mut self, 62 | obj: O, 63 | index: usize, 64 | value: am::ObjType, 65 | ) -> Result; 66 | 67 | fn increment, P: Into>( 68 | &mut self, 69 | obj: O, 70 | prop: P, 71 | value: i64, 72 | ) -> Result<(), AutomergeError>; 73 | 74 | fn delete, P: Into>( 75 | &mut self, 76 | obj: O, 77 | prop: P, 78 | ) -> Result<(), AutomergeError>; 79 | fn splice_text>( 80 | &mut self, 81 | obj: O, 82 | pos: usize, 83 | del: usize, 84 | text: &str, 85 | ) -> Result<(), AutomergeError>; 86 | } 87 | 88 | impl ReadDoc for am::AutoCommit { 89 | type Parents<'a> = am::Parents<'a>; 90 | fn get_heads(&self) -> Vec { 91 | am::transaction::Transactable::base_heads(self) 92 | } 93 | 94 | fn get>( 95 | &self, 96 | obj: &ObjId, 97 | prop: P, 98 | ) -> Result, ObjId)>, AutomergeError> { 99 | am::ReadDoc::get(self, obj, prop) 100 | } 101 | 102 | fn object_type>(&self, obj: O) -> Option { 103 | am::ReadDoc::object_type(self, obj) 104 | .map(Some) 105 | .unwrap_or(None) 106 | } 107 | 108 | fn map_range, R: RangeBounds>( 109 | &self, 110 | obj: O, 111 | range: R, 112 | ) -> am::MapRange<'_, R> { 113 | am::ReadDoc::map_range(self, obj, range) 114 | } 115 | 116 | fn list_range, R: RangeBounds>( 117 | &self, 118 | obj: O, 119 | range: R, 120 | ) -> am::ListRange<'_, R> { 121 | am::ReadDoc::list_range(self, obj, range) 122 | } 123 | 124 | fn length>(&self, obj: O) -> usize { 125 | am::ReadDoc::length(self, obj) 126 | } 127 | 128 | fn text>(&self, obj: O) -> Result { 129 | am::ReadDoc::text(self, obj) 130 | } 131 | 132 | fn parents>(&self, obj: O) -> Result, AutomergeError> { 133 | am::ReadDoc::parents(self, obj) 134 | } 135 | } 136 | 137 | impl<'a, Obs: am::transaction::Observation> ReadDoc for am::transaction::Transaction<'a, Obs> { 138 | type Parents<'b> = am::Parents<'b> where Self: 'b; 139 | fn get_heads(&self) -> Vec { 140 | am::transaction::Transactable::base_heads(self) 141 | } 142 | 143 | fn get>( 144 | &self, 145 | obj: &ObjId, 146 | prop: P, 147 | ) -> Result, ObjId)>, AutomergeError> { 148 | am::ReadDoc::get(self, obj, prop) 149 | } 150 | 151 | fn object_type>(&self, obj: O) -> Option { 152 | am::ReadDoc::object_type(self, obj) 153 | .map(Some) 154 | .unwrap_or(None) 155 | } 156 | 157 | fn map_range, R: RangeBounds>( 158 | &self, 159 | obj: O, 160 | range: R, 161 | ) -> am::MapRange<'_, R> { 162 | am::ReadDoc::map_range(self, obj, range) 163 | } 164 | 165 | fn list_range, R: RangeBounds>( 166 | &self, 167 | obj: O, 168 | range: R, 169 | ) -> am::ListRange<'_, R> { 170 | am::ReadDoc::list_range(self, obj, range) 171 | } 172 | 173 | fn length>(&self, obj: O) -> usize { 174 | am::ReadDoc::length(self, obj) 175 | } 176 | 177 | fn text>(&self, obj: O) -> Result { 178 | am::ReadDoc::text(self, obj) 179 | } 180 | 181 | fn parents>(&self, obj: O) -> Result, AutomergeError> { 182 | am::ReadDoc::parents(self, obj) 183 | } 184 | } 185 | 186 | impl ReadDoc for am::Automerge { 187 | type Parents<'a> = am::Parents<'a>; 188 | fn get_heads(&self) -> Vec { 189 | am::Automerge::get_heads(self) 190 | } 191 | 192 | fn get>( 193 | &self, 194 | obj: &ObjId, 195 | prop: P, 196 | ) -> Result, ObjId)>, AutomergeError> { 197 | am::ReadDoc::get(self, obj, prop) 198 | } 199 | 200 | fn object_type>(&self, obj: O) -> Option { 201 | am::ReadDoc::object_type(self, obj) 202 | .map(Some) 203 | .unwrap_or(None) 204 | } 205 | 206 | fn map_range, R: RangeBounds>( 207 | &self, 208 | obj: O, 209 | range: R, 210 | ) -> am::MapRange<'_, R> { 211 | am::ReadDoc::map_range(self, obj, range) 212 | } 213 | 214 | fn list_range, R: RangeBounds>( 215 | &self, 216 | obj: O, 217 | range: R, 218 | ) -> am::ListRange<'_, R> { 219 | am::ReadDoc::list_range(self, obj, range) 220 | } 221 | 222 | fn length>(&self, obj: O) -> usize { 223 | am::ReadDoc::length(self, obj) 224 | } 225 | 226 | fn text>(&self, obj: O) -> Result { 227 | am::ReadDoc::text(self, obj) 228 | } 229 | 230 | fn parents>(&self, obj: O) -> Result, AutomergeError> { 231 | am::ReadDoc::parents(self, obj) 232 | } 233 | } 234 | 235 | impl Doc for T { 236 | fn put, P: Into, V: Into>( 237 | &mut self, 238 | obj: O, 239 | prop: P, 240 | value: V, 241 | ) -> Result<(), AutomergeError> { 242 | am::transaction::Transactable::put(self, obj, prop, value) 243 | } 244 | 245 | fn put_object, P: Into>( 246 | &mut self, 247 | obj: O, 248 | prop: P, 249 | value: am::ObjType, 250 | ) -> Result { 251 | am::transaction::Transactable::put_object(self, obj, prop, value) 252 | } 253 | 254 | fn insert, V: Into>( 255 | &mut self, 256 | obj: O, 257 | index: usize, 258 | value: V, 259 | ) -> Result<(), AutomergeError> { 260 | am::transaction::Transactable::insert(self, obj, index, value) 261 | } 262 | 263 | fn insert_object>( 264 | &mut self, 265 | obj: O, 266 | index: usize, 267 | value: am::ObjType, 268 | ) -> Result { 269 | am::transaction::Transactable::insert_object(self, obj, index, value) 270 | } 271 | 272 | fn increment, P: Into>( 273 | &mut self, 274 | obj: O, 275 | prop: P, 276 | value: i64, 277 | ) -> Result<(), AutomergeError> { 278 | am::transaction::Transactable::increment(self, obj, prop, value) 279 | } 280 | 281 | fn delete, P: Into>( 282 | &mut self, 283 | obj: O, 284 | prop: P, 285 | ) -> Result<(), AutomergeError> { 286 | am::transaction::Transactable::delete(self, obj, prop) 287 | } 288 | 289 | fn splice_text>( 290 | &mut self, 291 | obj: O, 292 | pos: usize, 293 | del: usize, 294 | text: &str, 295 | ) -> Result<(), AutomergeError> { 296 | am::transaction::Transactable::splice_text(self, obj, pos, del, text) 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /autosurgeon/src/hydrate.rs: -------------------------------------------------------------------------------- 1 | use automerge::{ObjType, Parent, ScalarValue, Value}; 2 | use std::borrow::Cow; 3 | 4 | use crate::{Prop, ReadDoc}; 5 | 6 | mod impls; 7 | mod map; 8 | 9 | /// A type which can be hydrated from an automerge document 10 | /// 11 | /// There are no required methods on this trait. Instead implementors should choose the `hydrate_*` 12 | /// method which matches the automerge types they wish to hydrate. 13 | /// 14 | /// ```rust 15 | /// # use autosurgeon::{Hydrate, HydrateError}; 16 | /// struct UserId(String); 17 | /// 18 | /// impl Hydrate for UserId { 19 | /// fn hydrate_string(s: &'_ str) -> Result { 20 | /// Ok(UserId(s.to_string())) 21 | /// } 22 | /// } 23 | /// ``` 24 | pub trait Hydrate: Sized { 25 | fn hydrate( 26 | doc: &D, 27 | obj: &automerge::ObjId, 28 | prop: Prop<'_>, 29 | ) -> Result { 30 | match doc.get(obj, &prop)? { 31 | None => Self::hydrate_none(), 32 | Some((Value::Object(ObjType::Map), id)) => Self::hydrate_map(doc, &id), 33 | Some((Value::Object(ObjType::Table), id)) => Self::hydrate_map(doc, &id), 34 | Some((Value::Object(ObjType::List), id)) => Self::hydrate_seq(doc, &id), 35 | Some((Value::Object(ObjType::Text), id)) => Self::hydrate_text(doc, &id), 36 | Some((Value::Scalar(v), _)) => Self::hydrate_scalar(v), 37 | } 38 | } 39 | 40 | fn hydrate_scalar(s: Cow<'_, automerge::ScalarValue>) -> Result { 41 | match s.as_ref() { 42 | ScalarValue::Null => Self::hydrate_none(), 43 | ScalarValue::Boolean(b) => Self::hydrate_bool(*b), 44 | ScalarValue::Bytes(b) => Self::hydrate_bytes(b), 45 | ScalarValue::Counter(c) => Self::hydrate_counter(c.into()), 46 | ScalarValue::F64(f) => Self::hydrate_f64(*f), 47 | ScalarValue::Int(i) => Self::hydrate_int(*i), 48 | ScalarValue::Uint(u) => Self::hydrate_uint(*u), 49 | ScalarValue::Str(s) => Self::hydrate_string(s), 50 | ScalarValue::Timestamp(t) => Self::hydrate_timestamp(*t), 51 | ScalarValue::Unknown { type_code, bytes } => Self::hydrate_unknown(*type_code, bytes), 52 | } 53 | } 54 | 55 | fn hydrate_bool(_b: bool) -> Result { 56 | Err(HydrateError::Unexpected(Unexpected::Boolean)) 57 | } 58 | 59 | fn hydrate_bytes(_bytes: &[u8]) -> Result { 60 | Err(HydrateError::Unexpected(Unexpected::Bytes)) 61 | } 62 | 63 | fn hydrate_f64(_f: f64) -> Result { 64 | Err(HydrateError::Unexpected(Unexpected::F64)) 65 | } 66 | 67 | fn hydrate_counter(_c: i64) -> Result { 68 | Err(HydrateError::Unexpected(Unexpected::Counter)) 69 | } 70 | 71 | fn hydrate_int(_i: i64) -> Result { 72 | Err(HydrateError::Unexpected(Unexpected::Int)) 73 | } 74 | 75 | fn hydrate_uint(_u: u64) -> Result { 76 | Err(HydrateError::Unexpected(Unexpected::Uint)) 77 | } 78 | 79 | fn hydrate_string(_string: &'_ str) -> Result { 80 | Err(HydrateError::Unexpected(Unexpected::String)) 81 | } 82 | 83 | fn hydrate_timestamp(_t: i64) -> Result { 84 | Err(HydrateError::Unexpected(Unexpected::Timestamp)) 85 | } 86 | 87 | fn hydrate_unknown(_type_code: u8, _bytes: &[u8]) -> Result { 88 | Err(HydrateError::Unexpected(Unexpected::Unknown)) 89 | } 90 | 91 | fn hydrate_map(_doc: &D, _obj: &automerge::ObjId) -> Result { 92 | Err(HydrateError::Unexpected(Unexpected::Map)) 93 | } 94 | 95 | fn hydrate_seq(_doc: &D, _obj: &automerge::ObjId) -> Result { 96 | Err(HydrateError::Unexpected(Unexpected::Seq)) 97 | } 98 | 99 | fn hydrate_text(_doc: &D, _obj: &automerge::ObjId) -> Result { 100 | Err(HydrateError::Unexpected(Unexpected::Text)) 101 | } 102 | 103 | fn hydrate_none() -> Result { 104 | Err(HydrateError::Unexpected(Unexpected::None)) 105 | } 106 | } 107 | 108 | /// Hydrate an instance of `H` from `doc` 109 | pub fn hydrate(doc: &D) -> Result { 110 | H::hydrate_map(doc, &automerge::ROOT) 111 | } 112 | 113 | /// Hydrate an instance of `H` located at property `prop` of object `obj` 114 | pub fn hydrate_prop<'a, D: ReadDoc, H: Hydrate, P: Into>, O: AsRef>( 115 | doc: &D, 116 | obj: O, 117 | prop: P, 118 | ) -> Result { 119 | H::hydrate(doc, obj.as_ref(), prop.into()) 120 | } 121 | 122 | /// Hydrate an instance of `H` located at a path in the document 123 | /// 124 | /// The path must be an iterator of properties which start at `obj`. If any of the properties does 125 | /// not exist this will return `Ok(None)` 126 | pub fn hydrate_path<'a, D: ReadDoc, H: Hydrate, P: IntoIterator>>( 127 | doc: &D, 128 | obj: &automerge::ObjId, 129 | path: P, 130 | ) -> Result, HydrateError> { 131 | let mut path = path.into_iter().peekable(); 132 | let (mut obj, mut prop): (automerge::ObjId, Prop<'_>) = match path.next() { 133 | Some(p) => (obj.clone(), p.clone()), 134 | None => { 135 | if obj == &automerge::ROOT { 136 | return Ok(Some(hydrate(doc)?)); 137 | } else { 138 | let Some(Parent{obj: parent_obj, prop: parent_prop, ..}) = doc.parents(obj)?.next() else { 139 | return Ok(None) 140 | }; 141 | return hydrate_prop(doc, parent_obj, parent_prop); 142 | } 143 | } 144 | }; 145 | let Some(mut obj_type) = doc.object_type(&obj) else { 146 | return Ok(None) 147 | }; 148 | while let Some(path_elem) = path.next() { 149 | match (&prop, obj_type) { 150 | (Prop::Key(key), ObjType::Map | ObjType::Table) => { 151 | match doc.get(&obj, key.as_ref())? { 152 | Some((Value::Object(objtype), id)) => { 153 | obj = id; 154 | obj_type = objtype; 155 | } 156 | Some((Value::Scalar(_), _)) => { 157 | if path.peek().is_some() { 158 | return Ok(None); 159 | } 160 | } 161 | None => return Ok(None), 162 | } 163 | } 164 | (Prop::Index(idx), ObjType::List | ObjType::Text) => { 165 | match doc.get(&obj, (*idx) as usize)? { 166 | Some((Value::Object(objtype), id)) => { 167 | obj = id; 168 | obj_type = objtype; 169 | } 170 | Some((Value::Scalar(_), _)) => { 171 | if path.peek().is_some() { 172 | return Ok(None); 173 | } 174 | } 175 | None => return Ok(None), 176 | } 177 | } 178 | _ => return Ok(None), 179 | } 180 | prop = path_elem; 181 | } 182 | Ok(Some(hydrate_prop::<_, H, _, _>(doc, obj, prop)?)) 183 | } 184 | 185 | #[derive(Debug, thiserror::Error)] 186 | pub enum HydrateError { 187 | #[error(transparent)] 188 | Automerge(#[from] automerge::AutomergeError), 189 | #[error("unexpected {0}")] 190 | Unexpected(Unexpected), 191 | } 192 | 193 | impl HydrateError { 194 | /// Create a hydrate error for an unexpected value 195 | /// 196 | /// This is typically used when some data in the document couldn't be parsed into the target 197 | /// data type: 198 | /// 199 | /// ```rust 200 | /// # use autosurgeon::{HydrateError}; 201 | /// fn hydrate_path(s: &str) -> Result { 202 | /// s.parse().map_err(|e| HydrateError::unexpected( 203 | /// "a valid path", 204 | /// "a string which was not a path".to_string() 205 | /// )) 206 | /// } 207 | /// ``` 208 | pub fn unexpected>(expected: S, found: String) -> Self { 209 | HydrateError::Unexpected(Unexpected::Other { 210 | expected: expected.as_ref().to_string(), 211 | found, 212 | }) 213 | } 214 | } 215 | 216 | #[derive(Debug)] 217 | pub enum Unexpected { 218 | Map, 219 | Seq, 220 | Text, 221 | Boolean, 222 | Bytes, 223 | Counter, 224 | F64, 225 | Int, 226 | Uint, 227 | String, 228 | Timestamp, 229 | Unknown, 230 | None, 231 | Other { expected: String, found: String }, 232 | } 233 | 234 | impl std::fmt::Display for Unexpected { 235 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 236 | match self { 237 | Self::Map => write!(f, "map"), 238 | Self::Seq => write!(f, "sequence"), 239 | Self::Text => write!(f, "text"), 240 | Self::Boolean => write!(f, "boolean"), 241 | Self::Bytes => write!(f, "bytes"), 242 | Self::Counter => write!(f, "counter"), 243 | Self::F64 => write!(f, "f64"), 244 | Self::Int => write!(f, "int"), 245 | Self::Uint => write!(f, "uint"), 246 | Self::String => write!(f, "string"), 247 | Self::Timestamp => write!(f, "timestamp"), 248 | Self::Unknown => write!(f, "unknown"), 249 | Self::None => write!(f, "None"), 250 | Self::Other { expected, found } => write!(f, "{}, expected {}", found, expected), 251 | } 252 | } 253 | } 254 | 255 | pub trait HydrateResultExt { 256 | fn strip_unexpected(self) -> Result; 257 | } 258 | 259 | impl HydrateResultExt> for Result, HydrateError> { 260 | fn strip_unexpected(self) -> Result, automerge::AutomergeError> { 261 | match self { 262 | Ok(v) => Ok(v), 263 | Err(HydrateError::Unexpected(_)) => Ok(None), 264 | Err(HydrateError::Automerge(e)) => Err(e), 265 | } 266 | } 267 | } 268 | 269 | #[cfg(test)] 270 | mod tests { 271 | use super::*; 272 | use automerge::transaction::Transactable; 273 | use std::collections::HashMap; 274 | 275 | #[derive(Clone, Debug, PartialEq)] 276 | struct Company { 277 | name: String, 278 | employees: Vec, 279 | } 280 | 281 | #[derive(Clone, Debug, PartialEq)] 282 | struct Employee { 283 | name: String, 284 | number: u64, 285 | } 286 | 287 | impl Hydrate for Company { 288 | fn hydrate_map(doc: &D, obj: &automerge::ObjId) -> Result { 289 | let name = hydrate_prop(doc, obj, "name")?; 290 | let employees = hydrate_prop(doc, obj, "employees")?; 291 | Ok(Company { name, employees }) 292 | } 293 | } 294 | 295 | impl Hydrate for Employee { 296 | fn hydrate_map(doc: &D, obj: &automerge::ObjId) -> Result { 297 | let name = hydrate_prop(doc, obj, "name")?; 298 | let number = hydrate_prop(doc, obj, "number")?; 299 | Ok(Employee { name, number }) 300 | } 301 | } 302 | 303 | #[test] 304 | fn basic_hydrate() { 305 | let mut doc = automerge::AutoCommit::new(); 306 | doc.put(automerge::ROOT, "name", "Microsoft").unwrap(); 307 | let emps = doc 308 | .put_object(automerge::ROOT, "employees", automerge::ObjType::List) 309 | .unwrap(); 310 | let emp = doc 311 | .insert_object(&emps, 0, automerge::ObjType::Map) 312 | .unwrap(); 313 | doc.put(&emp, "name", "Satya Nadella").unwrap(); 314 | doc.put(&emp, "number", 1_u64).unwrap(); 315 | 316 | let microsoft = hydrate::<_, Company>(&doc).unwrap(); 317 | assert_eq!( 318 | microsoft, 319 | Company { 320 | name: "Microsoft".to_string(), 321 | employees: vec![Employee { 322 | name: "Satya Nadella".to_string(), 323 | number: 1, 324 | }], 325 | } 326 | ); 327 | } 328 | 329 | #[test] 330 | fn hydrate_from_doc() { 331 | let mut doc = automerge::AutoCommit::new(); 332 | doc.put(automerge::ROOT, "name", "Microsoft").unwrap(); 333 | let emps = doc 334 | .put_object(automerge::ROOT, "employees", automerge::ObjType::List) 335 | .unwrap(); 336 | let emp = doc 337 | .insert_object(&emps, 0, automerge::ObjType::Map) 338 | .unwrap(); 339 | doc.put(&emp, "name", "Satya Nadella").unwrap(); 340 | doc.put(&emp, "number", 1_u64).unwrap(); 341 | 342 | let microsoft = hydrate::<_, Company>(doc.document()).unwrap(); 343 | assert_eq!( 344 | microsoft, 345 | Company { 346 | name: "Microsoft".to_string(), 347 | employees: vec![Employee { 348 | name: "Satya Nadella".to_string(), 349 | number: 1, 350 | }], 351 | } 352 | ); 353 | } 354 | 355 | #[test] 356 | fn basic_hydrate_path() { 357 | let mut doc = automerge::AutoCommit::new(); 358 | let companies = doc 359 | .put_object(automerge::ROOT, "companies", ObjType::Map) 360 | .unwrap(); 361 | let ms = doc 362 | .put_object(&companies, "Microsoft", ObjType::Map) 363 | .unwrap(); 364 | doc.put(&ms, "name", "Microsoft").unwrap(); 365 | let employees = doc.put_object(&ms, "employees", ObjType::List).unwrap(); 366 | let emp = doc.insert_object(&employees, 0, ObjType::Map).unwrap(); 367 | doc.put(&emp, "name", "Satya Nadella").unwrap(); 368 | doc.put(&emp, "number", 1_u64).unwrap(); 369 | 370 | let expected_ms = Company { 371 | name: "Microsoft".to_string(), 372 | employees: vec![Employee { 373 | name: "Satya Nadella".to_string(), 374 | number: 1, 375 | }], 376 | }; 377 | let result: HashMap = 378 | hydrate_path(&doc, &automerge::ROOT, vec!["companies".into()].into_iter()) 379 | .unwrap() 380 | .unwrap(); 381 | let mut expected = HashMap::new(); 382 | expected.insert("Microsoft".to_string(), expected_ms.clone()); 383 | assert_eq!(expected, result); 384 | 385 | let result: Company = hydrate_path(&doc, &companies, vec!["Microsoft".into()].into_iter()) 386 | .unwrap() 387 | .unwrap(); 388 | assert_eq!(result, expected_ms); 389 | 390 | let satya: Employee = hydrate_path( 391 | &doc, 392 | &companies, 393 | vec!["Microsoft".into(), "employees".into(), 0_usize.into()].into_iter(), 394 | ) 395 | .unwrap() 396 | .unwrap(); 397 | assert_eq!(satya, expected_ms.employees[0]); 398 | 399 | let name_from_comp: String = hydrate_path( 400 | &doc, 401 | &companies, 402 | vec![ 403 | "Microsoft".into(), 404 | "employees".into(), 405 | 0_usize.into(), 406 | "name".into(), 407 | ] 408 | .into_iter(), 409 | ) 410 | .unwrap() 411 | .unwrap(); 412 | assert_eq!(name_from_comp, "Satya Nadella"); 413 | } 414 | 415 | #[test] 416 | fn hydrate_path_root() { 417 | let mut doc = automerge::AutoCommit::new(); 418 | doc.put(&automerge::ROOT, "name", "Moist von Lipwig") 419 | .unwrap(); 420 | doc.put(&automerge::ROOT, "number", 1_u64).unwrap(); 421 | let moist = hydrate_path::<_, Employee, _>(&doc, &automerge::ROOT, vec![].into_iter()) 422 | .unwrap() 423 | .unwrap(); 424 | assert_eq!( 425 | moist, 426 | Employee { 427 | name: "Moist von Lipwig".to_string(), 428 | number: 1, 429 | } 430 | ); 431 | } 432 | 433 | #[test] 434 | fn hydrate_empty_path() { 435 | let mut doc = automerge::AutoCommit::new(); 436 | let moist = doc 437 | .put_object(automerge::ROOT, "moist", ObjType::Map) 438 | .unwrap(); 439 | doc.put(&moist, "name", "Moist von Lipwig").unwrap(); 440 | doc.put(&moist, "number", 1_u64).unwrap(); 441 | let moist = hydrate_path::<_, Employee, _>(&doc, &moist, vec![].into_iter()) 442 | .unwrap() 443 | .unwrap(); 444 | assert_eq!( 445 | moist, 446 | Employee { 447 | name: "Moist von Lipwig".to_string(), 448 | number: 1, 449 | } 450 | ); 451 | } 452 | } 453 | -------------------------------------------------------------------------------- /autosurgeon/src/hydrate/impls.rs: -------------------------------------------------------------------------------- 1 | use super::{hydrate_prop, Hydrate, HydrateError}; 2 | use crate::ReadDoc; 3 | use std::borrow::Cow; 4 | 5 | impl Hydrate for String { 6 | fn hydrate_string(s: &'_ str) -> Result { 7 | Ok(s.to_string()) 8 | } 9 | } 10 | 11 | impl Hydrate for Vec 12 | where 13 | T: Hydrate, 14 | { 15 | fn hydrate_seq(doc: &D, obj: &automerge::ObjId) -> Result { 16 | let mut result = Vec::with_capacity(doc.length(obj)); 17 | for idx in 0..doc.length(obj) { 18 | let elem = hydrate_prop(doc, obj, idx)?; 19 | result.push(elem); 20 | } 21 | Ok(result) 22 | } 23 | } 24 | 25 | macro_rules! int_impl { 26 | ($ty:ident, $hydrator: ident, $from_ty:ident) => { 27 | impl Hydrate for $ty { 28 | fn $hydrator(u: $from_ty) -> Result { 29 | u.try_into().map_err(|_| { 30 | HydrateError::unexpected( 31 | stringify!("a ", $ty), 32 | "an integer which is too large".to_string(), 33 | ) 34 | }) 35 | } 36 | } 37 | }; 38 | } 39 | 40 | int_impl!(u8, hydrate_uint, u64); 41 | int_impl!(u16, hydrate_uint, u64); 42 | int_impl!(u32, hydrate_uint, u64); 43 | int_impl!(u64, hydrate_uint, u64); 44 | int_impl!(i8, hydrate_int, i64); 45 | int_impl!(i16, hydrate_int, i64); 46 | int_impl!(i32, hydrate_int, i64); 47 | int_impl!(i64, hydrate_int, i64); 48 | 49 | impl Hydrate for bool { 50 | fn hydrate_bool(b: bool) -> Result { 51 | Ok(b) 52 | } 53 | } 54 | 55 | impl Hydrate for f64 { 56 | fn hydrate_f64(f: f64) -> Result { 57 | Ok(f) 58 | } 59 | } 60 | 61 | impl Hydrate for f32 { 62 | fn hydrate_f64(f: f64) -> Result { 63 | Ok(f as f32) 64 | } 65 | } 66 | 67 | impl Hydrate for Option { 68 | fn hydrate( 69 | doc: &D, 70 | obj: &automerge::ObjId, 71 | prop: crate::Prop<'_>, 72 | ) -> Result { 73 | use automerge::{ObjType, ScalarValue, Value}; 74 | Ok(match doc.get(obj, &prop)? { 75 | None => { 76 | return Err(HydrateError::unexpected( 77 | "a ScalarValue::Null", 78 | "nothing at all".to_string(), 79 | )) 80 | } 81 | Some((Value::Object(ObjType::Map), id)) => Some(T::hydrate_map(doc, &id)?), 82 | Some((Value::Object(ObjType::Table), id)) => Some(T::hydrate_map(doc, &id)?), 83 | Some((Value::Object(ObjType::List), id)) => Some(T::hydrate_seq(doc, &id)?), 84 | Some((Value::Object(ObjType::Text), id)) => Some(T::hydrate_text(doc, &id)?), 85 | Some((Value::Scalar(v), _)) => match v.as_ref() { 86 | ScalarValue::Null => None, 87 | ScalarValue::Boolean(b) => Some(T::hydrate_bool(*b)?), 88 | ScalarValue::Bytes(b) => Some(T::hydrate_bytes(b)?), 89 | ScalarValue::Counter(c) => Some(T::hydrate_counter(c.into())?), 90 | ScalarValue::F64(f) => Some(T::hydrate_f64(*f)?), 91 | ScalarValue::Int(i) => Some(T::hydrate_int(*i)?), 92 | ScalarValue::Uint(u) => Some(T::hydrate_uint(*u)?), 93 | ScalarValue::Str(s) => Some(T::hydrate_string(s)?), 94 | ScalarValue::Timestamp(t) => Some(T::hydrate_timestamp(*t)?), 95 | ScalarValue::Unknown { type_code, bytes } => { 96 | Some(T::hydrate_unknown(*type_code, bytes)?) 97 | } 98 | }, 99 | }) 100 | } 101 | } 102 | 103 | impl<'a, T: Hydrate + Clone> Hydrate for Cow<'a, T> { 104 | fn hydrate( 105 | doc: &D, 106 | obj: &automerge::ObjId, 107 | prop: crate::Prop<'_>, 108 | ) -> Result { 109 | Ok(Cow::Owned(T::hydrate(doc, obj, prop)?)) 110 | } 111 | } 112 | 113 | impl Hydrate for Box { 114 | fn hydrate( 115 | doc: &D, 116 | obj: &automerge::ObjId, 117 | prop: crate::Prop<'_>, 118 | ) -> Result { 119 | Ok(Box::new(T::hydrate(doc, obj, prop)?)) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /autosurgeon/src/hydrate/map.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::{BTreeMap, HashMap}, 3 | hash::Hash, 4 | ops::RangeFull, 5 | }; 6 | 7 | use automerge::ObjType; 8 | 9 | use crate::{Hydrate, HydrateError}; 10 | 11 | impl Hydrate for HashMap 12 | where 13 | K: From + Hash + Eq, 14 | V: Hydrate, 15 | { 16 | fn hydrate_map( 17 | doc: &D, 18 | obj: &automerge::ObjId, 19 | ) -> Result { 20 | map_impl(doc, obj, |range| { 21 | let mut result = HashMap::new(); 22 | for (key, _, _) in range { 23 | let val = V::hydrate(doc, obj, key.into())?; 24 | result.insert(K::from(key.to_string()), val); 25 | } 26 | Ok(result) 27 | }) 28 | } 29 | } 30 | 31 | impl Hydrate for BTreeMap 32 | where 33 | K: From + Ord, 34 | V: Hydrate, 35 | { 36 | fn hydrate_map( 37 | doc: &D, 38 | obj: &automerge::ObjId, 39 | ) -> Result { 40 | map_impl(doc, obj, |range| { 41 | let mut result = BTreeMap::new(); 42 | for (key, _, _) in range { 43 | let val = V::hydrate(doc, obj, key.into())?; 44 | result.insert(K::from(key.to_string()), val); 45 | } 46 | Ok(result) 47 | }) 48 | } 49 | } 50 | 51 | fn map_impl<'a, D, F, O>(doc: &'a D, obj: &automerge::ObjId, f: F) -> Result 52 | where 53 | D: crate::ReadDoc, 54 | F: Fn(automerge::MapRange<'a, RangeFull>) -> Result, 55 | { 56 | let Some(obj_type) = doc.object_type(obj) else { 57 | return Err(HydrateError::unexpected("a map", "a scalar value".to_string())) 58 | }; 59 | match obj_type { 60 | ObjType::Map | ObjType::Table => f(doc.map_range(obj, ..)), 61 | ObjType::Text => Err(HydrateError::unexpected( 62 | "a map", 63 | "a text object".to_string(), 64 | )), 65 | ObjType::List => Err(HydrateError::unexpected( 66 | "a map", 67 | "a list object".to_string(), 68 | )), 69 | } 70 | } 71 | 72 | #[cfg(test)] 73 | mod tests { 74 | use am::transaction::Transactable; 75 | use automerge as am; 76 | use std::collections::HashMap; 77 | 78 | use crate::{hydrate, Hydrate}; 79 | 80 | #[derive(Debug, PartialEq)] 81 | struct User { 82 | id: u64, 83 | name: String, 84 | } 85 | impl Hydrate for User { 86 | fn hydrate_map( 87 | doc: &D, 88 | obj: &am::ObjId, 89 | ) -> Result { 90 | let id = u64::hydrate(doc, obj, "id".into())?; 91 | let name = String::hydrate(doc, obj, "name".into())?; 92 | Ok(User { id, name }) 93 | } 94 | } 95 | 96 | #[derive(Debug, PartialEq, Eq, Hash)] 97 | struct UserName(String); 98 | impl From for UserName { 99 | fn from(s: String) -> Self { 100 | UserName(s) 101 | } 102 | } 103 | impl<'a> From<&'a str> for UserName { 104 | fn from(s: &'a str) -> Self { 105 | UserName(s.to_string()) 106 | } 107 | } 108 | 109 | #[test] 110 | fn basic_hydrate_map() { 111 | let mut doc = am::AutoCommit::new(); 112 | let u1 = doc.put_object(am::ROOT, "user1", am::ObjType::Map).unwrap(); 113 | doc.put(&u1, "name", "One").unwrap(); 114 | doc.put(&u1, "id", 1_u64).unwrap(); 115 | 116 | let u2 = doc.put_object(am::ROOT, "user2", am::ObjType::Map).unwrap(); 117 | doc.put(&u2, "name", "Two").unwrap(); 118 | doc.put(&u2, "id", 2_u64).unwrap(); 119 | 120 | let mut expected: HashMap = HashMap::new(); 121 | expected.insert( 122 | "user1".into(), 123 | User { 124 | id: 1, 125 | name: "One".to_string(), 126 | }, 127 | ); 128 | expected.insert( 129 | "user2".into(), 130 | User { 131 | id: 2, 132 | name: "Two".to_string(), 133 | }, 134 | ); 135 | 136 | let result: HashMap = hydrate(&doc).unwrap(); 137 | assert_eq!(result, expected); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /autosurgeon/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # AutoSurgeon 2 | //! 3 | //! `autosurgeon` is a library for interaction with [`automerge`] documents in Rust with an API 4 | //! inspired by `serde`. The core of the library are two traits: [`Reconcile`], which describes how 5 | //! to take a rust value and update an automerge document to match the value; and [`Hydrate`], 6 | //! which describes how to create a rust value given an automerge document. 7 | //! 8 | //! Whilst you can implement [`Reconcile`] and [`Hydrate`] manually, `autosurgeon` provides derive 9 | //! macros to do this work mechanically. 10 | //! 11 | //! Additionally `autosurgeon` provides the [`Counter`] and [`Text`] data types which implement 12 | //! [`Reconcile`] and [`Hydrate`] for counters and text respectively. 13 | //! 14 | //! Currently this library does not handle incremental updates, that means that every time you 15 | //! receive concurrent changes from other documents you will need to re-`hydrate` your data 16 | //! structures from your document. This will be addressed in future versions. 17 | //! 18 | //! ## Feature Flags 19 | //! 20 | //! * `uuid` - Includes implementations of `Reconcile` and `Hydrate` for the [`Uuid`](https://docs.rs/uuid/latest/uuid/) crate which will 21 | //! reconcile to a [`automerge::ScalarValue::Bytes`] 22 | //! 23 | //! ## Example 24 | //! 25 | //! Imagine we are writing a program to interact with a document containing some contact details. 26 | //! We start by writing some data types to represent the contact and deriving the [`Reconcile`] and 27 | //! [`Hydrate`] traits. 28 | //! 29 | //! ```rust 30 | //! # use autosurgeon::{Reconcile, Hydrate}; 31 | //! #[derive(Debug, Clone, Reconcile, Hydrate, PartialEq)] 32 | //! struct Contact { 33 | //! name: String, 34 | //! address: Address, 35 | //! } 36 | //! #[derive(Debug, Clone, Reconcile, Hydrate, PartialEq)] 37 | //! struct Address { 38 | //! line_one: String, 39 | //! line_two: Option, 40 | //! city: String, 41 | //! postcode: String, 42 | //! } 43 | //! ``` 44 | //! 45 | //! First we create a contact and put it into a document 46 | //! 47 | //! ```rust 48 | //! # use autosurgeon::{Reconcile, Hydrate, reconcile}; 49 | //! # #[derive(Debug, Clone, Reconcile, Hydrate, PartialEq)] 50 | //! # struct Contact { 51 | //! # name: String, 52 | //! # address: Address, 53 | //! # } 54 | //! 55 | //! # #[derive(Debug, Clone, Reconcile, Hydrate, PartialEq)] 56 | //! # struct Address { 57 | //! # line_one: String, 58 | //! # line_two: Option, 59 | //! # city: String, 60 | //! # } 61 | //! let contact = Contact { 62 | //! name: "Sherlock Holmes".to_string(), 63 | //! address: Address{ 64 | //! line_one: "221B Baker St".to_string(), 65 | //! line_two: None, 66 | //! city: "London".to_string(), 67 | //! }, 68 | //! }; 69 | //! 70 | //! let mut doc = automerge::AutoCommit::new(); 71 | //! reconcile(&mut doc, &contact).unwrap(); 72 | //! ``` 73 | //! 74 | //! Now we can reconstruct the contact from the document 75 | //! 76 | //! ```rust 77 | //! # use autosurgeon::{Reconcile, Hydrate, reconcile, hydrate}; 78 | //! # #[derive(Debug, Clone, Reconcile, Hydrate, PartialEq)] 79 | //! # struct Contact { 80 | //! # name: String, 81 | //! # address: Address, 82 | //! # } 83 | //! 84 | //! # #[derive(Debug, Clone, Reconcile, Hydrate, PartialEq)] 85 | //! # struct Address { 86 | //! # line_one: String, 87 | //! # line_two: Option, 88 | //! # city: String, 89 | //! # } 90 | //! # 91 | //! # let mut contact = Contact { 92 | //! # name: "Sherlock Holmes".to_string(), 93 | //! # address: Address{ 94 | //! # line_one: "221B Baker St".to_string(), 95 | //! # line_two: None, 96 | //! # city: "London".to_string(), 97 | //! # }, 98 | //! # }; 99 | //! # 100 | //! # let mut doc = automerge::AutoCommit::new(); 101 | //! # reconcile(&mut doc, &contact).unwrap(); 102 | //! let contact2: Contact = hydrate(&doc).unwrap(); 103 | //! assert_eq!(contact, contact2); 104 | //! ``` 105 | //! 106 | //! `reconcile` is smart though, it doesn't just update everything in the document, it figures out 107 | //! what's changed, which means merging modified documents works as you would imagine. Let's fork 108 | //! our document and make concurrent changes to it, then merge it and see how it looks. 109 | //! 110 | //! ```rust 111 | //! # use autosurgeon::{Reconcile, Hydrate, reconcile, hydrate}; 112 | //! # #[derive(Debug, Clone, Reconcile, Hydrate, PartialEq)] 113 | //! # struct Contact { 114 | //! # name: String, 115 | //! # address: Address, 116 | //! # } 117 | //! # #[derive(Debug, Clone, Reconcile, Hydrate, PartialEq)] 118 | //! # struct Address { 119 | //! # line_one: String, 120 | //! # line_two: Option, 121 | //! # city: String, 122 | //! # } 123 | //! # 124 | //! # let mut contact = Contact { 125 | //! # name: "Sherlock Holmes".to_string(), 126 | //! # address: Address{ 127 | //! # line_one: "221B Baker St".to_string(), 128 | //! # line_two: None, 129 | //! # city: "London".to_string(), 130 | //! # }, 131 | //! # }; 132 | //! # 133 | //! # let mut doc = automerge::AutoCommit::new(); 134 | //! # reconcile(&mut doc, &contact).unwrap(); 135 | //! // Fork and make changes 136 | //! let mut doc2 = doc.fork().with_actor(automerge::ActorId::random()); 137 | //! let mut contact2: Contact = hydrate(&doc2).unwrap(); 138 | //! contact2.name = "Dangermouse".to_string(); 139 | //! reconcile(&mut doc2, &contact2).unwrap(); 140 | //! 141 | //! // Concurrently on doc1 142 | //! contact.address.line_one = "221C Baker St".to_string(); 143 | //! reconcile(&mut doc, &contact).unwrap(); 144 | //! 145 | //! // Now merge the documents 146 | //! doc.merge(&mut doc2).unwrap(); 147 | //! 148 | //! let merged: Contact = hydrate(&doc).unwrap(); 149 | //! assert_eq!(merged, Contact { 150 | //! name: "Dangermouse".to_string(), // This was updated in the first doc 151 | //! address: Address { 152 | //! line_one: "221C Baker St".to_string(), // This was concurrently updated in doc2 153 | //! line_two: None, 154 | //! city: "London".to_string(), 155 | //! } 156 | //! }) 157 | //! ``` 158 | //! 159 | //! ## Derive Macro 160 | //! 161 | //! ### Automerge Representation 162 | //! 163 | //! The derive macros map rust structs to the automerge structures in a similar manner to `serde` 164 | //! 165 | //! ```rust,no_run 166 | //! struct W { 167 | //! a: i32, 168 | //! b: i32, 169 | //! } 170 | //! let w = W { a: 0, b: 0 }; // Represented as `{"a":0,"b":0}` 171 | //! 172 | //! struct X(i32, i32); 173 | //! let x = X(0, 0); // Represented as `[0,0]` 174 | //! 175 | //! struct Y(i32); 176 | //! let y = Y(0); // Represented as just the inner value `0` 177 | //! 178 | //! enum E { 179 | //! W { a: i32, b: i32 }, 180 | //! X(i32, i32), 181 | //! Y(i32), 182 | //! Z, 183 | //! } 184 | //! let w = E::W { a: 0, b: 0 }; // Represented as `{"W":{"a":0,"b":0}}` 185 | //! let x = E::X(0, 0); // Represented as `{"X":[0,0]}` 186 | //! let y = E::Y(0); // Represented as `{"Y":0}` 187 | //! let z = E::Z; // Represented as `"Z"` 188 | //! ``` 189 | //! 190 | //! ### The `key` attribute 191 | //! 192 | //! `autosurgeon` will generally do its best to generate smart diffs. But sometimes you know 193 | //! additional information about your data which can make merges smarter. Consider the following 194 | //! scenario where we create a product catalog and then make concurrent changes to it. 195 | //! 196 | //! ```rust 197 | //! # use automerge_test::{assert_doc, map, list}; 198 | //! # use autosurgeon::{reconcile, Reconcile, Hydrate}; 199 | //! #[derive(Reconcile, Hydrate, Clone, Debug, Eq, PartialEq)] 200 | //! struct Product { 201 | //! id: u64, 202 | //! name: String, 203 | //! } 204 | //! 205 | //! #[derive(Reconcile, Hydrate, Clone, Debug, Eq, PartialEq)] 206 | //! struct Catalog { 207 | //! products: Vec, 208 | //! } 209 | //! 210 | //! let mut catalog = Catalog { 211 | //! products: vec![ 212 | //! Product { 213 | //! id: 1, 214 | //! name: "Lawnmower".to_string(), 215 | //! }, 216 | //! Product { 217 | //! id: 2, 218 | //! name: "Strimmer".to_string(), 219 | //! } 220 | //! ] 221 | //! }; 222 | //! 223 | //! // Put the catalog into the document 224 | //! let mut doc = automerge::AutoCommit::new(); 225 | //! reconcile(&mut doc, &catalog).unwrap(); 226 | //! 227 | //! // Fork the document and insert a new product at the start of the catalog 228 | //! let mut doc2 = doc.fork().with_actor(automerge::ActorId::random()); 229 | //! let mut catalog2 = catalog.clone(); 230 | //! catalog2.products.insert(0, Product { 231 | //! id: 3, 232 | //! name: "Leafblower".to_string(), 233 | //! }); 234 | //! reconcile(&mut doc2, &catalog2).unwrap(); 235 | //! 236 | //! // Concurrenctly remove a product from the catalog in the original doc 237 | //! catalog.products.remove(0); 238 | //! reconcile(&mut doc, &catalog).unwrap(); 239 | //! 240 | //! // Merge the two changes 241 | //! doc.merge(&mut doc2).unwrap(); 242 | //! assert_doc!( 243 | //! doc.document(), 244 | //! map! { 245 | //! "products" => { list! { 246 | //! // This first item is conflicted, we expected it to be the leafblower 247 | //! { map! { 248 | //! "id" => { 2_u64, 3_u64 }, // Conflict on the ID 249 | //! "name" => { "Strimmer", "Leafblower" }, // Conflict on the name 250 | //! }}, 251 | //! { map! { 252 | //! "id" => { 2_u64 }, 253 | //! "name" => { "Strimmer" }, 254 | //! }} 255 | //! }} 256 | //! } 257 | //! ); 258 | //! ``` 259 | //! 260 | //! This is surprising, we have a bunch of merge conflicts on the fields of the first product in 261 | //! the list (as signified by the multiple values in the `{..}` on the inner values of the `map!`) 262 | //! and the second product in the list is also a strimmer. This is because `autosurgeon` has no way 263 | //! of knowing the difference between "I inserted an item at the front of the products list" and "I 264 | //! updated the first item in the products list". 265 | //! 266 | //! But we have an `id` field on the product, we can make autosurgeon aware of this. 267 | //! 268 | //! ```rust 269 | //! # use automerge_test::{assert_doc, map, list}; 270 | //! # use autosurgeon::{reconcile, Reconcile, Hydrate}; 271 | //! #[derive(Reconcile, Hydrate, Clone, Debug, Eq, PartialEq)] 272 | //! struct Product { 273 | //! #[key] // This is the important bit 274 | //! id: u64, 275 | //! name: String, 276 | //! } 277 | //! ``` 278 | //! 279 | //! And with this our concurrent changes look like the following: 280 | //! 281 | //! ```rust 282 | //! # use automerge_test::{assert_doc, map, list}; 283 | //! # use autosurgeon::{reconcile, Reconcile, Hydrate}; 284 | //! # #[derive(Reconcile, Hydrate, Clone, Debug, Eq, PartialEq)] 285 | //! # struct Product { 286 | //! # #[key] 287 | //! # id: u64, 288 | //! # name: String, 289 | //! # } 290 | //! # #[derive(Reconcile, Hydrate, Clone, Debug, Eq, PartialEq)] 291 | //! # struct Catalog { 292 | //! # products: Vec, 293 | //! # } 294 | //! # let mut catalog = Catalog { 295 | //! # products: vec![ 296 | //! # Product { 297 | //! # id: 1, 298 | //! # name: "Lawnmower".to_string(), 299 | //! # }, 300 | //! # Product { 301 | //! # id: 2, 302 | //! # name: "Strimmer".to_string(), 303 | //! # } 304 | //! # ] 305 | //! # }; 306 | //! # let mut doc = automerge::AutoCommit::new(); 307 | //! # reconcile(&mut doc, &catalog).unwrap(); 308 | //! # let mut doc2 = doc.fork().with_actor(automerge::ActorId::random()); 309 | //! # let mut catalog2 = catalog.clone(); 310 | //! # catalog2.products.insert(0, Product { 311 | //! # id: 3, 312 | //! # name: "Leafblower".to_string(), 313 | //! # }); 314 | //! # reconcile(&mut doc2, &catalog2).unwrap(); 315 | //! # catalog.products.remove(0); 316 | //! # reconcile(&mut doc, &catalog).unwrap(); 317 | //! doc.merge(&mut doc2).unwrap(); 318 | //! assert_doc!( 319 | //! doc.document(), 320 | //! map! { 321 | //! "products" => { list! { 322 | //! { map! { 323 | //! "id" => { 3_u64 }, 324 | //! "name" => { "Leafblower" }, 325 | //! }}, 326 | //! { map! { 327 | //! "id" => { 2_u64 }, 328 | //! "name" => { "Strimmer" }, 329 | //! }} 330 | //! }} 331 | //! } 332 | //! ); 333 | //! ``` 334 | //! 335 | //! ### Providing Implementations for foreign types 336 | //! 337 | //! Deriving `Hydrate` and `Reconcile` is fine for your own types, but sometimes you are using a 338 | //! type which you did not write. For these situations there are a few attributes you can use. 339 | //! 340 | //! #### `reconcile=` 341 | //! 342 | //! The value of this attribute must be the name of a function with the same signature as 343 | //! [`Reconcile::reconcile`] 344 | //! 345 | //! ```rust 346 | //! # use autosurgeon::{Reconcile, Reconciler}; 347 | //! #[derive(Reconcile)] 348 | //! struct File { 349 | //! #[autosurgeon(reconcile="reconcile_path")] 350 | //! path: std::path::PathBuf, 351 | //! } 352 | //! 353 | //! fn reconcile_path( 354 | //! path: &std::path::PathBuf, mut reconciler: R 355 | //! ) -> Result<(), R::Error> { 356 | //! reconciler.str(path.display().to_string()) 357 | //! } 358 | //! ``` 359 | //! 360 | //! #### `hydrate=` 361 | //! 362 | //! The value of this attribute must be the name of a function with the same signature as 363 | //! [`Hydrate::hydrate`] 364 | //! 365 | //! ```rust 366 | //! # use autosurgeon::{Hydrate, ReadDoc, Prop, HydrateError}; 367 | //! #[derive(Hydrate)] 368 | //! struct File { 369 | //! #[autosurgeon(hydrate="hydrate_path")] 370 | //! path: std::path::PathBuf, 371 | //! } 372 | //! 373 | //! fn hydrate_path<'a, D: ReadDoc>( 374 | //! doc: &D, 375 | //! obj: &automerge::ObjId, 376 | //! prop: Prop<'a>, 377 | //! ) -> Result { 378 | //! let inner = String::hydrate(doc, obj, prop)?; 379 | //! inner.parse().map_err(|e| HydrateError::unexpected( 380 | //! "a valid path", format!("a path which failed to parse due to {}", e) 381 | //! )) 382 | //! } 383 | //! ``` 384 | //! 385 | //! #### `with=` 386 | //! 387 | //! The value of this attribute must be the name of a module wich has both a `reconcile` function 388 | //! and a `hydrate` function, with the same signatures as [`Reconcile::reconcile`] and 389 | //! [`Hydrate::hydrate`] respectively. 390 | //! 391 | //! ```rust 392 | //! # use autosurgeon::{Reconcile, Hydrate, ReadDoc, Prop, HydrateError}; 393 | //! #[derive(Hydrate)] 394 | //! struct File { 395 | //! #[autosurgeon(with="autosurgeon_path")] 396 | //! path: std::path::PathBuf, 397 | //! } 398 | //! 399 | //! mod autosurgeon_path { 400 | //! use autosurgeon::{Reconcile, Reconciler, Hydrate, ReadDoc, Prop, HydrateError}; 401 | //! pub(super) fn hydrate<'a, D: ReadDoc>( 402 | //! doc: &D, 403 | //! obj: &automerge::ObjId, 404 | //! prop: Prop<'a>, 405 | //! ) -> Result { 406 | //! let inner = String::hydrate(doc, obj, prop)?; 407 | //! inner.parse().map_err(|e| HydrateError::unexpected( 408 | //! "a valid path", format!("a path which failed to parse due to {}", e) 409 | //! )) 410 | //! } 411 | //! 412 | //! pub(super) fn reconcile( 413 | //! path: &std::path::PathBuf, mut reconciler: R 414 | //! ) -> Result<(), R::Error> { 415 | //! reconciler.str(path.display().to_string()) 416 | //! } 417 | //! } 418 | //! ``` 419 | 420 | #[doc = include_str!("../../README.md")] 421 | #[cfg(doctest)] 422 | pub struct ReadmeDoctests; 423 | 424 | mod counter; 425 | pub use counter::Counter; 426 | pub mod bytes; 427 | mod doc; 428 | pub use doc::{Doc, ReadDoc}; 429 | pub mod hydrate; 430 | #[doc(inline)] 431 | pub use hydrate::{hydrate, hydrate_path, hydrate_prop, Hydrate, HydrateError}; 432 | pub mod reconcile; 433 | #[doc(inline)] 434 | pub use reconcile::{ 435 | hydrate_key, reconcile, reconcile_insert, reconcile_prop, Reconcile, ReconcileError, Reconciler, 436 | }; 437 | mod text; 438 | pub use text::Text; 439 | 440 | mod prop; 441 | pub use prop::Prop; 442 | 443 | pub use autosurgeon_derive::{Hydrate, Reconcile}; 444 | 445 | #[cfg(feature = "uuid")] 446 | mod uuid; 447 | -------------------------------------------------------------------------------- /autosurgeon/src/path.rs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexjg/autosurgeon/7d96e57f2be08b50e953d3d5812a718e2af35239/autosurgeon/src/path.rs -------------------------------------------------------------------------------- /autosurgeon/src/prop.rs: -------------------------------------------------------------------------------- 1 | use automerge as am; 2 | use std::borrow::Cow; 3 | 4 | #[derive(Clone, Debug)] 5 | pub enum Prop<'a> { 6 | Key(Cow<'a, str>), 7 | Index(u32), 8 | } 9 | 10 | impl<'a> std::fmt::Display for Prop<'a> { 11 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 12 | match self { 13 | Self::Key(s) => write!(f, "{}", s), 14 | Self::Index(i) => write!(f, "{}", i), 15 | } 16 | } 17 | } 18 | 19 | impl<'a> From<&Prop<'a>> for automerge::Prop { 20 | fn from(p: &Prop) -> Self { 21 | match p { 22 | Prop::Key(k) => automerge::Prop::Map(k.to_string()), 23 | Prop::Index(i) => automerge::Prop::Seq(*i as usize), 24 | } 25 | } 26 | } 27 | 28 | impl From for Prop<'static> { 29 | fn from(v: u32) -> Self { 30 | Prop::Index(v) 31 | } 32 | } 33 | 34 | impl From for Prop<'static> { 35 | fn from(v: usize) -> Self { 36 | Prop::Index(v as u32) 37 | } 38 | } 39 | 40 | impl<'a> From> for Prop<'a> { 41 | fn from(v: Cow<'a, str>) -> Self { 42 | Self::Key(v) 43 | } 44 | } 45 | 46 | impl<'a> From<&'a str> for Prop<'a> { 47 | fn from(s: &'a str) -> Self { 48 | Self::Key(s.into()) 49 | } 50 | } 51 | 52 | impl<'a> From for Prop<'a> { 53 | fn from(p: am::Prop) -> Self { 54 | match p { 55 | am::Prop::Map(k) => Prop::Key(Cow::Owned(k)), 56 | am::Prop::Seq(idx) => Prop::Index(idx as u32), 57 | } 58 | } 59 | } 60 | 61 | impl<'a> From<&'a am::Prop> for Prop<'a> { 62 | fn from(p: &'a am::Prop) -> Self { 63 | match p { 64 | am::Prop::Map(k) => Prop::Key(Cow::Borrowed(k)), 65 | am::Prop::Seq(idx) => Prop::Index(*idx as u32), 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /autosurgeon/src/reconcile/impls.rs: -------------------------------------------------------------------------------- 1 | use automerge::{ScalarValue, Value}; 2 | use std::borrow::Cow; 3 | 4 | use super::{LoadKey, Reconcile, Reconciler}; 5 | use crate::ReadDoc; 6 | 7 | impl Reconcile for String { 8 | type Key<'a> = Cow<'a, str>; 9 | fn reconcile(&self, mut reconciler: R) -> Result<(), R::Error> { 10 | reconciler.str(self) 11 | } 12 | fn key(&self) -> LoadKey> { 13 | LoadKey::Found(Cow::Borrowed(self)) 14 | } 15 | fn hydrate_key<'a, D: ReadDoc>( 16 | doc: &D, 17 | obj: &automerge::ObjId, 18 | prop: crate::Prop<'_>, 19 | ) -> Result>, crate::ReconcileError> { 20 | Ok(match doc.get(obj, &prop)? { 21 | Some((Value::Scalar(s), _)) => { 22 | if let ScalarValue::Str(s) = s.as_ref() { 23 | LoadKey::Found(Cow::Owned(s.to_string())) 24 | } else { 25 | LoadKey::KeyNotFound 26 | } 27 | } 28 | _ => LoadKey::KeyNotFound, 29 | }) 30 | } 31 | } 32 | 33 | impl Reconcile for str { 34 | type Key<'a> = Cow<'a, str>; 35 | fn reconcile(&self, mut reconciler: R) -> Result<(), R::Error> { 36 | reconciler.str(self) 37 | } 38 | fn key(&self) -> LoadKey> { 39 | LoadKey::Found(Cow::Borrowed(self)) 40 | } 41 | fn hydrate_key<'a, D: ReadDoc>( 42 | doc: &D, 43 | obj: &automerge::ObjId, 44 | prop: crate::Prop<'_>, 45 | ) -> Result>, crate::ReconcileError> { 46 | Ok(match doc.get(obj, &prop)? { 47 | Some((Value::Scalar(s), _)) => { 48 | if let ScalarValue::Str(s) = s.as_ref() { 49 | LoadKey::Found(Cow::Owned(s.to_string())) 50 | } else { 51 | LoadKey::KeyNotFound 52 | } 53 | } 54 | _ => LoadKey::KeyNotFound, 55 | }) 56 | } 57 | } 58 | 59 | impl<'a, T: Reconcile + ?Sized> Reconcile for &'a T { 60 | type Key<'b> = T::Key<'b>; 61 | fn reconcile(&self, reconciler: R) -> Result<(), R::Error> { 62 | (*self).reconcile(reconciler) 63 | } 64 | 65 | fn hydrate_key<'b, D: ReadDoc>( 66 | doc: &D, 67 | obj: &automerge::ObjId, 68 | prop: crate::prop::Prop<'_>, 69 | ) -> Result>, crate::ReconcileError> { 70 | T::hydrate_key(doc, obj, prop) 71 | } 72 | 73 | fn key(&self) -> LoadKey> { 74 | T::key(self) 75 | } 76 | } 77 | 78 | impl Reconcile for f64 { 79 | type Key<'a> = f64; 80 | fn reconcile(&self, mut reconciler: R) -> Result<(), R::Error> { 81 | reconciler.f64(*self) 82 | } 83 | fn key(&self) -> LoadKey> { 84 | LoadKey::Found(*self) 85 | } 86 | fn hydrate_key<'a, D: ReadDoc>( 87 | doc: &D, 88 | obj: &automerge::ObjId, 89 | prop: crate::Prop<'_>, 90 | ) -> Result>, crate::ReconcileError> { 91 | Ok(match doc.get(obj, &prop)? { 92 | Some((Value::Scalar(s), _)) => { 93 | if let ScalarValue::F64(f) = s.as_ref() { 94 | LoadKey::Found(*f) 95 | } else { 96 | LoadKey::KeyNotFound 97 | } 98 | } 99 | _ => LoadKey::KeyNotFound, 100 | }) 101 | } 102 | } 103 | 104 | impl Reconcile for f32 { 105 | type Key<'a> = f32; 106 | fn reconcile(&self, mut reconciler: R) -> Result<(), R::Error> { 107 | reconciler.f64(*self as f64) 108 | } 109 | fn key(&self) -> LoadKey> { 110 | LoadKey::Found(*self) 111 | } 112 | fn hydrate_key<'a, D: ReadDoc>( 113 | doc: &D, 114 | obj: &automerge::ObjId, 115 | prop: crate::Prop<'_>, 116 | ) -> Result>, crate::ReconcileError> { 117 | Ok(match doc.get(obj, &prop)? { 118 | Some((Value::Scalar(s), _)) => { 119 | if let ScalarValue::F64(f) = s.as_ref() { 120 | LoadKey::Found(*f as f32) 121 | } else { 122 | LoadKey::KeyNotFound 123 | } 124 | } 125 | _ => LoadKey::KeyNotFound, 126 | }) 127 | } 128 | } 129 | 130 | impl Reconcile for bool { 131 | type Key<'a> = bool; 132 | fn reconcile(&self, mut reconciler: R) -> Result<(), R::Error> { 133 | reconciler.boolean(*self) 134 | } 135 | fn key(&self) -> LoadKey> { 136 | LoadKey::Found(*self) 137 | } 138 | fn hydrate_key<'a, D: ReadDoc>( 139 | doc: &D, 140 | obj: &automerge::ObjId, 141 | prop: crate::Prop<'_>, 142 | ) -> Result>, crate::ReconcileError> { 143 | Ok(match doc.get(obj, &prop)? { 144 | Some((Value::Scalar(s), _)) => { 145 | if let ScalarValue::Boolean(f) = s.as_ref() { 146 | LoadKey::Found(*f) 147 | } else { 148 | LoadKey::KeyNotFound 149 | } 150 | } 151 | _ => LoadKey::KeyNotFound, 152 | }) 153 | } 154 | } 155 | 156 | macro_rules! int_impl { 157 | ($ty:ident, $from:ident, $to: ident) => { 158 | impl Reconcile for $ty { 159 | type Key<'a> = $ty; 160 | fn reconcile(&self, mut reconciler: R) -> Result<(), R::Error> { 161 | reconciler.$to(*self as $to) 162 | } 163 | fn key(&self) -> LoadKey> { 164 | LoadKey::Found(*self) 165 | } 166 | fn hydrate_key<'a, D: ReadDoc>( 167 | doc: &D, 168 | obj: &automerge::ObjId, 169 | prop: crate::Prop<'_>, 170 | ) -> Result>, crate::ReconcileError> { 171 | Ok(match doc.get(obj, &prop)? { 172 | Some((Value::Scalar(s), _)) => { 173 | if let ScalarValue::$from(i) = s.as_ref() { 174 | if let Ok(v) = $ty::try_from(*i) { 175 | LoadKey::Found(v) 176 | } else { 177 | LoadKey::KeyNotFound 178 | } 179 | } else { 180 | LoadKey::KeyNotFound 181 | } 182 | } 183 | _ => LoadKey::KeyNotFound, 184 | }) 185 | } 186 | } 187 | }; 188 | } 189 | 190 | int_impl!(u8, Uint, u64); 191 | int_impl!(u16, Uint, u64); 192 | int_impl!(u32, Uint, u64); 193 | int_impl!(u64, Uint, u64); 194 | int_impl!(i8, Int, i64); 195 | int_impl!(i16, Int, i64); 196 | int_impl!(i32, Int, i64); 197 | int_impl!(i64, Int, i64); 198 | 199 | impl Reconcile for Box { 200 | type Key<'a> = T::Key<'a>; 201 | fn reconcile(&self, reconciler: R) -> Result<(), R::Error> { 202 | T::reconcile(self, reconciler) 203 | } 204 | fn hydrate_key<'a, D: ReadDoc>( 205 | doc: &D, 206 | obj: &automerge::ObjId, 207 | prop: crate::Prop<'_>, 208 | ) -> Result>, crate::ReconcileError> { 209 | T::hydrate_key(doc, obj, prop) 210 | } 211 | fn key(&self) -> LoadKey> { 212 | T::key(self) 213 | } 214 | } 215 | 216 | impl Reconcile for Option { 217 | type Key<'a> = T::Key<'a>; 218 | fn key(&self) -> LoadKey> { 219 | self.as_ref() 220 | .map(|s| T::key(s)) 221 | .unwrap_or(LoadKey::KeyNotFound) 222 | } 223 | fn reconcile(&self, mut reconciler: R) -> Result<(), R::Error> { 224 | match self { 225 | Some(s) => s.reconcile(reconciler), 226 | None => reconciler.none(), 227 | } 228 | } 229 | fn hydrate_key<'a, D: ReadDoc>( 230 | doc: &D, 231 | obj: &automerge::ObjId, 232 | prop: crate::Prop<'_>, 233 | ) -> Result>, crate::ReconcileError> { 234 | match doc.get(obj, &prop)? { 235 | Some((Value::Scalar(s), _)) => match s.as_ref() { 236 | ScalarValue::Null => Ok(LoadKey::KeyNotFound), 237 | _ => T::hydrate_key(doc, obj, prop), 238 | }, 239 | _ => Ok(LoadKey::KeyNotFound), 240 | } 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /autosurgeon/src/reconcile/map.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{BTreeMap, HashMap}; 2 | 3 | use crate::Reconcile; 4 | 5 | use super::{LoadKey, MapReconciler}; 6 | 7 | impl Reconcile for HashMap 8 | where 9 | K: AsRef, 10 | V: Reconcile, 11 | { 12 | type Key<'a> = super::NoKey; 13 | 14 | fn reconcile(&self, reconciler: R) -> Result<(), R::Error> { 15 | reconcile_map_impl(self.iter(), reconciler) 16 | } 17 | } 18 | 19 | impl Reconcile for BTreeMap 20 | where 21 | K: AsRef, 22 | V: Reconcile, 23 | { 24 | type Key<'a> = super::NoKey; 25 | 26 | fn reconcile(&self, reconciler: R) -> Result<(), R::Error> { 27 | reconcile_map_impl(self.iter(), reconciler) 28 | } 29 | } 30 | 31 | fn reconcile_map_impl< 32 | 'a, 33 | K: AsRef + 'a, 34 | V: Reconcile + 'a, 35 | I: Iterator, 36 | R: crate::Reconciler, 37 | >( 38 | items: I, 39 | mut reconciler: R, 40 | ) -> Result<(), R::Error> { 41 | let mut m = reconciler.map()?; 42 | for (k, val) in items { 43 | if let LoadKey::Found(new_key) = val.key() { 44 | if let LoadKey::Found(existing_key) = m.hydrate_entry_key::(k)? { 45 | if existing_key != new_key { 46 | m.replace(k, val)?; 47 | continue; 48 | } 49 | } 50 | } 51 | m.put(k.as_ref(), val)?; 52 | } 53 | Ok(()) 54 | } 55 | 56 | #[cfg(test)] 57 | mod tests { 58 | use std::collections::HashMap; 59 | 60 | use automerge::ActorId; 61 | use automerge_test::{assert_doc, list, map}; 62 | 63 | use crate::{ 64 | reconcile, 65 | reconcile::{hydrate_key, LoadKey, MapReconciler}, 66 | ReadDoc, Reconcile, 67 | }; 68 | 69 | #[test] 70 | fn reconcile_map() { 71 | let mut map = HashMap::new(); 72 | map.insert("key1", vec!["one", "two"]); 73 | map.insert("key2", vec!["three"]); 74 | let mut doc = automerge::AutoCommit::new(); 75 | reconcile(&mut doc, &map).unwrap(); 76 | assert_doc!( 77 | doc.document(), 78 | map! { 79 | "key1" => { list! { {"one"}, {"two"} }}, 80 | "key2" => { list! { {"three"} }}, 81 | } 82 | ); 83 | } 84 | 85 | #[derive(Clone)] 86 | struct User { 87 | id: u64, 88 | name: &'static str, 89 | } 90 | 91 | impl Reconcile for User { 92 | type Key<'a> = u64; 93 | 94 | fn reconcile(&self, mut reconciler: R) -> Result<(), R::Error> { 95 | let mut m = reconciler.map()?; 96 | m.put("id", self.id)?; 97 | m.put("name", self.name)?; 98 | Ok(()) 99 | } 100 | 101 | fn hydrate_key<'a, D: ReadDoc>( 102 | doc: &D, 103 | obj: &automerge::ObjId, 104 | prop: crate::Prop<'_>, 105 | ) -> Result>, crate::ReconcileError> { 106 | hydrate_key::<_, u64>(doc, obj, prop, "id".into()) 107 | } 108 | 109 | fn key(&self) -> LoadKey> { 110 | LoadKey::Found(self.id) 111 | } 112 | } 113 | 114 | #[test] 115 | fn reconcile_map_with_key() { 116 | let mut map = HashMap::new(); 117 | map.insert("user", User { id: 1, name: "one" }); 118 | let mut doc = automerge::AutoCommit::new(); 119 | reconcile(&mut doc, &map).unwrap(); 120 | 121 | let mut doc2 = doc.fork().with_actor(ActorId::random()); 122 | let mut map2 = map.clone(); 123 | map2.insert("user", User { id: 2, name: "two" }); 124 | reconcile(&mut doc2, &map2).unwrap(); 125 | 126 | map.insert( 127 | "user", 128 | User { 129 | id: 3, 130 | name: "three", 131 | }, 132 | ); 133 | reconcile(&mut doc, &map).unwrap(); 134 | 135 | doc.merge(&mut doc2).unwrap(); 136 | 137 | assert_doc!( 138 | doc.document(), 139 | map! { 140 | "user" => { 141 | map! { 142 | "id" => { 2_u64 }, 143 | "name" => { "two" }, 144 | }, 145 | map! { 146 | "id" => { 3_u64 }, 147 | "name" => { "three" }, 148 | } 149 | } 150 | } 151 | ); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /autosurgeon/src/reconcile/seq.rs: -------------------------------------------------------------------------------- 1 | use super::{LoadKey, NoKey, Reconcile, Reconciler, SeqReconciler}; 2 | 3 | // This module implements an LCS diff for sequences when reconciling. Currently the implementation 4 | // of the Hunt-Szymanski diff is from the `similar` crate. Consequenctly most of the types in this 5 | // module are adapters to express the types from `reconcile` in a way which `similar` can work 6 | // with. 7 | 8 | impl Reconcile for [T] { 9 | type Key<'a> = NoKey; 10 | fn reconcile(&self, reconciler: R) -> Result<(), R::Error> { 11 | reconcile_seq(self, reconciler) 12 | } 13 | } 14 | 15 | impl Reconcile for Vec { 16 | type Key<'a> = NoKey; 17 | fn reconcile(&self, reconciler: R) -> Result<(), R::Error> { 18 | reconcile_seq(self, reconciler) 19 | } 20 | } 21 | 22 | // Represents a key of an element in the document, we don't represent the actual element here 23 | // because we don't want to hydrate the entire element from the document, just the key 24 | struct OldElem { 25 | key: K, 26 | index: usize, 27 | } 28 | 29 | // An element in the new sequence we are reconciling from 30 | struct NewElem<'a, T> { 31 | elem: &'a T, 32 | index: usize, 33 | } 34 | 35 | // `similar::algorithms::lcs` requires that the new sequence elements implement `PartialEqual` with 36 | // the old elements. By implementing this in terms of the key on the old and new elements we can 37 | // get `similar` to do what we want 38 | impl<'a: 'b, 'b, T: Reconcile> PartialEq>>> for NewElem<'a, T> { 39 | fn eq(&self, other: &OldElem>>) -> bool { 40 | match (self.elem.key(), &other.key) { 41 | // Both elements had a key, just compare the keys 42 | (LoadKey::Found(k1), LoadKey::Found(k2)) => &k1 == k2, 43 | 44 | // One of the elements had a key, but the other didn't, they are not eqeual 45 | (LoadKey::Found(_), _) => false, 46 | (_, LoadKey::Found(_)) => false, 47 | 48 | // Neither element had a key, in this case we want to set both of them and diff 49 | // structurally 50 | (_, _) => self.index == other.index, 51 | } 52 | } 53 | } 54 | 55 | struct Hook<'a, T, S> { 56 | idx: usize, 57 | seq: &'a mut S, 58 | items: &'a [T], 59 | } 60 | 61 | impl<'a, T, S> similar::algorithms::DiffHook for Hook<'a, T, S> 62 | where 63 | T: Reconcile, 64 | S: SeqReconciler, 65 | { 66 | type Error = S::Error; 67 | fn equal( 68 | &mut self, 69 | _old_index: usize, 70 | new_index: usize, 71 | len: usize, 72 | ) -> Result<(), Self::Error> { 73 | for elem in &self.items[new_index..(new_index + len)] { 74 | self.seq.set(self.idx, elem)?; 75 | self.idx += 1; 76 | } 77 | Ok(()) 78 | } 79 | 80 | fn delete( 81 | &mut self, 82 | _old_index: usize, 83 | old_len: usize, 84 | _new_index: usize, 85 | ) -> Result<(), Self::Error> { 86 | for _ in 0..old_len { 87 | self.seq.delete(self.idx)?; 88 | } 89 | Ok(()) 90 | } 91 | 92 | fn insert( 93 | &mut self, 94 | _old_index: usize, 95 | new_index: usize, 96 | new_len: usize, 97 | ) -> Result<(), Self::Error> { 98 | for elem in &self.items[new_index..(new_index + new_len)] { 99 | self.seq.insert(self.idx, elem)?; 100 | self.idx += 1; 101 | } 102 | Ok(()) 103 | } 104 | } 105 | 106 | fn reconcile_seq(items: &[T], mut reconciler: R) -> Result<(), R::Error> 107 | where 108 | T: Reconcile, 109 | R: Reconciler, 110 | R::Error: std::fmt::Debug, 111 | { 112 | let mut seq = reconciler.seq()?; 113 | 114 | let old_len = seq.len()?; 115 | let old_keys = (0..old_len).try_fold::<_, _, Result<_, R::Error>>( 116 | Vec::with_capacity(old_len), 117 | |mut items, i| { 118 | items.push(OldElem { 119 | key: seq.hydrate_item_key::(i)?, 120 | index: i, 121 | }); 122 | Ok(items) 123 | }, 124 | )?; 125 | 126 | let new = items 127 | .iter() 128 | .enumerate() 129 | .map(|(i, e)| NewElem { elem: e, index: i }) 130 | .collect::>(); 131 | 132 | let mut hook = Hook { 133 | idx: 0, 134 | items, 135 | seq: &mut seq, 136 | }; 137 | 138 | similar::algorithms::lcs::diff(&mut hook, &old_keys, 0..old_len, &new, 0..items.len())?; 139 | Ok(()) 140 | } 141 | 142 | #[cfg(test)] 143 | mod tests { 144 | use crate::{ 145 | reconcile::{LoadKey, MapReconciler}, 146 | reconcile_prop, ReadDoc, 147 | }; 148 | use automerge_test::{assert_doc, list, map}; 149 | use std::borrow::Cow; 150 | 151 | #[test] 152 | fn test_reconcile_slice_deletes_extra_elems() { 153 | let mut vals = vec![1, 2, 3]; 154 | let mut doc = automerge::AutoCommit::new(); 155 | reconcile_prop(&mut doc, automerge::ROOT, "vals", &vals).unwrap(); 156 | vals.remove(1); 157 | reconcile_prop(&mut doc, automerge::ROOT, "vals", &vals).unwrap(); 158 | assert_doc!( 159 | doc.document(), 160 | map! { 161 | "vals" => { list! { 162 | { 1 }, 163 | { 3 } 164 | }} 165 | } 166 | ) 167 | } 168 | 169 | #[test] 170 | fn test_reconcile_vec_deletes_extra_elems() { 171 | let mut vals = vec![1, 2, 3]; 172 | let mut doc = automerge::AutoCommit::new(); 173 | reconcile_prop(&mut doc, automerge::ROOT, "vals", vals.clone()).unwrap(); 174 | vals.remove(1); 175 | reconcile_prop(&mut doc, automerge::ROOT, "vals", vals).unwrap(); 176 | assert_doc!( 177 | doc.document(), 178 | map! { 179 | "vals" => { list! { 180 | { 1 }, 181 | { 3 } 182 | }} 183 | } 184 | ) 185 | } 186 | 187 | #[derive(Clone, Debug, PartialEq)] 188 | struct Person { 189 | id: String, 190 | name: String, 191 | } 192 | 193 | impl crate::Reconcile for Person { 194 | type Key<'a> = Cow<'a, String>; 195 | 196 | fn reconcile(&self, mut reconciler: R) -> Result<(), R::Error> { 197 | let mut m = reconciler.map()?; 198 | m.put("name", &self.name)?; 199 | m.put("id", &self.id)?; 200 | Ok(()) 201 | } 202 | 203 | fn hydrate_key<'a, D: ReadDoc>( 204 | doc: &D, 205 | obj: &automerge::ObjId, 206 | prop: crate::prop::Prop<'_>, 207 | ) -> Result>, crate::ReconcileError> { 208 | let result = crate::reconcile::hydrate_key(doc, obj, prop, "id".into())?; 209 | Ok(result) 210 | } 211 | 212 | fn key(&self) -> LoadKey> { 213 | LoadKey::Found(Cow::Borrowed(&self.id)) 214 | } 215 | } 216 | 217 | #[test] 218 | fn test_reconcile_vec_with_key() { 219 | let mut vals = vec![ 220 | Person { 221 | id: "one".to_string(), 222 | name: "Burt".to_string(), 223 | }, 224 | Person { 225 | id: "two".to_string(), 226 | name: "Winston".to_string(), 227 | }, 228 | ]; 229 | let mut doc = automerge::AutoCommit::new(); 230 | reconcile_prop(&mut doc, automerge::ROOT, "people", &vals).unwrap(); 231 | 232 | let mut doc2 = doc.fork().with_actor("actor2".as_bytes().into()); 233 | let mut vals2 = vals.clone(); 234 | vals2.insert( 235 | 0, 236 | Person { 237 | id: "three".to_string(), 238 | name: "Charlotte".to_string(), 239 | }, 240 | ); 241 | reconcile_prop(&mut doc2, automerge::ROOT, "people", &vals2).unwrap(); 242 | 243 | vals.remove(1); 244 | reconcile_prop(&mut doc, automerge::ROOT, "people", &vals).unwrap(); 245 | 246 | doc.merge(&mut doc2).unwrap(); 247 | 248 | assert_doc!( 249 | doc.document(), 250 | map! { 251 | "people" => { list! { 252 | { map! { 253 | "id" => { "three" }, 254 | "name" => { "Charlotte" }, 255 | }}, 256 | { map! { 257 | "id" => { "one" }, 258 | "name" => { "Burt" }, 259 | }} 260 | }} 261 | } 262 | ) 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /autosurgeon/src/text.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | reconcile::{NoKey, TextReconciler}, 3 | Hydrate, ReadDoc, Reconcile, 4 | }; 5 | 6 | /// A type which reconciles to an [`automerge::ObjType::Text`] 7 | /// 8 | /// The intended way to use this, as with [`crate::Counter`], is as a field of a struct which implements 9 | /// [`Reconcile`]. Each time you wish to make a change to the text object you hydrate the struct, 10 | /// make mutating calls to [`Text::splice`], and then once you're done [`crate::reconcile()`] the struct 11 | /// with the document. 12 | /// 13 | /// **important** Attempting to reconcile this struct with a document whose heads have changed 14 | /// since the struct was rehydrated will throw a [`crate::reconcile::ReconcileError::StaleHeads`] error. 15 | /// 16 | /// # Example 17 | /// 18 | /// ```rust 19 | /// # use automerge::ActorId; 20 | /// # use autosurgeon::{reconcile, hydrate, Text, Reconcile, Hydrate}; 21 | /// #[derive(Debug, Reconcile, Hydrate)] 22 | /// struct Quote { 23 | /// text: Text, 24 | /// } 25 | /// let mut doc = automerge::AutoCommit::new(); 26 | /// let quote = Quote { 27 | /// text: "glimmers".into() 28 | /// }; 29 | /// reconcile(&mut doc, "e).unwrap(); 30 | /// 31 | /// // Fork and make changes to the text 32 | /// let mut doc2 = doc.fork().with_actor(ActorId::random()); 33 | /// let mut quote2: Quote = hydrate(&doc2).unwrap(); 34 | /// quote2.text.splice(0, 0, "All that "); 35 | /// let end_index = quote2.text.as_str().char_indices().last().unwrap().0; 36 | /// quote2.text.splice(end_index + 1, 0, " is not gold"); 37 | /// reconcile(&mut doc2, "e2).unwrap(); 38 | /// 39 | /// // Concurrently modify the text in the original doc 40 | /// let mut quote: Quote = hydrate(&doc).unwrap(); 41 | /// let m_index = quote.text.as_str().char_indices().nth(3).unwrap().0; 42 | /// quote.text.splice(m_index, 2, "tt"); 43 | /// reconcile(&mut doc, quote).unwrap(); 44 | /// 45 | /// // Merge the changes 46 | /// doc.merge(&mut doc2).unwrap(); 47 | /// 48 | /// let quote: Quote = hydrate(&doc).unwrap(); 49 | /// assert_eq!(quote.text.as_str(), "All that glitters is not gold"); 50 | /// ``` 51 | pub struct Text(State); 52 | 53 | impl std::default::Default for Text { 54 | fn default() -> Self { 55 | Text::with_value("") 56 | } 57 | } 58 | 59 | impl std::fmt::Debug for Text { 60 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 61 | f.debug_struct("Text") 62 | .field("value", &self.as_str()) 63 | .finish() 64 | } 65 | } 66 | 67 | impl Text { 68 | pub fn with_value>(value: S) -> Text { 69 | Self(State::Fresh(value.as_ref().to_string())) 70 | } 71 | 72 | /// Update the value of the `Text` 73 | /// 74 | /// # Arguments 75 | /// 76 | /// * pos - The index to start the splice at 77 | /// * del - The number of characters to delete 78 | /// * insert - The characters to insert 79 | /// 80 | /// The `pos` index uses the same logic as [`String::replace_range`]. This means 81 | /// that the same caveats apply with regards to the indices you can use. To find the correct 82 | /// index to start a splice at you use logic such as `String::char_indices`. 83 | /// 84 | /// # Panics 85 | /// 86 | /// Panics if the starting point or end point do not lie on a char boundary, or if they’re out 87 | /// of bounds. 88 | /// 89 | /// # Example 90 | /// 91 | /// ```rust 92 | /// # use autosurgeon::Text; 93 | /// let mut value = Text::with_value("some value"); 94 | /// // Get char index of the "v" 95 | /// let i = value.as_str().char_indices().nth(5).unwrap().0; 96 | /// value.splice(i, 0, "amazing "); 97 | /// assert_eq!(value.as_str(), "some amazing value"); 98 | /// ``` 99 | pub fn splice>(&mut self, pos: usize, del: usize, insert: S) { 100 | match &mut self.0 { 101 | State::Fresh(v) => v.replace_range(pos..(pos + del), insert.as_ref()), 102 | State::Rehydrated { value, edits, .. } => { 103 | value.replace_range(pos..(pos + del), insert.as_ref()); 104 | edits.push(Splice { 105 | pos, 106 | delete: del, 107 | insert: insert.as_ref().to_string(), 108 | }); 109 | } 110 | } 111 | } 112 | 113 | pub fn as_str(&self) -> &str { 114 | match &self.0 { 115 | State::Fresh(v) => v, 116 | State::Rehydrated { value, .. } => value, 117 | } 118 | } 119 | } 120 | 121 | impl> From for Text { 122 | fn from(s: S) -> Self { 123 | Text::with_value(s) 124 | } 125 | } 126 | 127 | enum State { 128 | Fresh(String), 129 | Rehydrated { 130 | value: String, 131 | edits: Vec, 132 | from_heads: Vec, 133 | }, 134 | } 135 | 136 | struct Splice { 137 | pos: usize, 138 | delete: usize, 139 | insert: String, 140 | } 141 | 142 | impl Reconcile for Text { 143 | type Key<'a> = NoKey; 144 | 145 | fn reconcile(&self, mut reconciler: R) -> Result<(), R::Error> { 146 | let mut t = reconciler.text()?; 147 | match &self.0 { 148 | State::Fresh(v) => { 149 | t.splice(0, 0, v)?; 150 | } 151 | State::Rehydrated { 152 | edits, from_heads, .. 153 | } => { 154 | let to_heads = t.heads(); 155 | if to_heads != from_heads { 156 | return Err(crate::reconcile::StaleHeads { 157 | expected: from_heads.to_vec(), 158 | found: to_heads.to_vec(), 159 | } 160 | .into()); 161 | } else { 162 | for edit in edits { 163 | t.splice(edit.pos, edit.delete, &edit.insert)?; 164 | } 165 | } 166 | } 167 | } 168 | Ok(()) 169 | } 170 | } 171 | 172 | impl Hydrate for Text { 173 | fn hydrate_text( 174 | doc: &D, 175 | obj: &automerge::ObjId, 176 | ) -> Result { 177 | let value = doc.text(obj)?; 178 | Ok(Text(State::Rehydrated { 179 | value, 180 | edits: Vec::new(), 181 | from_heads: doc.get_heads(), 182 | })) 183 | } 184 | } 185 | 186 | #[cfg(test)] 187 | mod tests { 188 | use automerge::ActorId; 189 | 190 | use crate::{hydrate_prop, reconcile_prop}; 191 | 192 | use super::Text; 193 | 194 | #[test] 195 | fn merge_text() { 196 | let mut doc1 = automerge::AutoCommit::new(); 197 | let text = Text::with_value("glitters"); 198 | reconcile_prop(&mut doc1, automerge::ROOT, "text", &text).unwrap(); 199 | let mut doc2 = doc1.fork().with_actor(ActorId::random()); 200 | 201 | let mut text1: Text = hydrate_prop(&doc1, &automerge::ROOT, "text").unwrap(); 202 | let mut text2: Text = hydrate_prop(&doc1, &automerge::ROOT, "text").unwrap(); 203 | 204 | text1.splice(0, 0, "all that "); 205 | reconcile_prop(&mut doc1, automerge::ROOT, "text", &text1).unwrap(); 206 | 207 | let offset = text2.as_str().char_indices().last().unwrap().0; 208 | text2.splice(offset + 1, 0, " is not gold"); 209 | reconcile_prop(&mut doc2, automerge::ROOT, "text", &text2).unwrap(); 210 | 211 | doc1.merge(&mut doc2).unwrap(); 212 | 213 | let result: Text = hydrate_prop(&doc1, &automerge::ROOT, "text").unwrap(); 214 | assert_eq!(result.as_str(), "all that glitters is not gold"); 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /autosurgeon/src/uuid.rs: -------------------------------------------------------------------------------- 1 | use std::mem; 2 | 3 | use uuid::Uuid; 4 | 5 | use crate::{bytes::ByteArray, Hydrate, HydrateError, Reconcile}; 6 | 7 | impl Reconcile for Uuid { 8 | type Key<'a> = () }> as Reconcile>::Key<'a>; 9 | 10 | fn reconcile(&self, reconciler: R) -> Result<(), R::Error> { 11 | ByteArray::from(*self.as_bytes()).reconcile(reconciler) 12 | } 13 | } 14 | 15 | impl Hydrate for Uuid { 16 | fn hydrate_bytes(bytes: &[u8]) -> Result { 17 | let array = ByteArray::<{ mem::size_of::() }>::hydrate_bytes(bytes)?; 18 | Ok(Uuid::from_bytes(*array)) 19 | } 20 | } 21 | 22 | #[cfg(test)] 23 | mod tests { 24 | use automerge::ObjId; 25 | use uuid::Uuid; 26 | 27 | use crate::{hydrate_prop, reconcile_prop}; 28 | 29 | #[test] 30 | fn round_trip_uuids() { 31 | let mut doc = automerge::AutoCommit::new(); 32 | 33 | let uuid = Uuid::new_v4(); 34 | reconcile_prop(&mut doc, ObjId::Root, "secret", uuid).unwrap(); 35 | 36 | let hydrated_uuid = hydrate_prop(&doc, ObjId::Root, "secret").unwrap(); 37 | 38 | assert_eq!(uuid, hydrated_uuid); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | # This template contains all of the possible sections and their default values 2 | 3 | # Note that all fields that take a lint level have these possible values: 4 | # * deny - An error will be produced and the check will fail 5 | # * warn - A warning will be produced, but the check will not fail 6 | # * allow - No warning or error will be produced, though in some cases a note 7 | # will be 8 | 9 | # The values provided in this template are the default values that will be used 10 | # when any section or field is not specified in your own configuration 11 | 12 | # Root options 13 | 14 | # If 1 or more target triples (and optionally, target_features) are specified, 15 | # only the specified targets will be checked when running `cargo deny check`. 16 | # This means, if a particular package is only ever used as a target specific 17 | # dependency, such as, for example, the `nix` crate only being used via the 18 | # `target_family = "unix"` configuration, that only having windows targets in 19 | # this list would mean the nix crate, as well as any of its exclusive 20 | # dependencies not shared by any other crates, would be ignored, as the target 21 | # list here is effectively saying which targets you are building for. 22 | targets = [ 23 | # The triple can be any string, but only the target triples built in to 24 | # rustc (as of 1.40) can be checked against actual config expressions 25 | #{ triple = "x86_64-unknown-linux-musl" }, 26 | # You can also specify which target_features you promise are enabled for a 27 | # particular target. target_features are currently not validated against 28 | # the actual valid features supported by the target architecture. 29 | #{ triple = "wasm32-unknown-unknown", features = ["atomics"] }, 30 | ] 31 | # When creating the dependency graph used as the source of truth when checks are 32 | # executed, this field can be used to prune crates from the graph, removing them 33 | # from the view of cargo-deny. This is an extremely heavy hammer, as if a crate 34 | # is pruned from the graph, all of its dependencies will also be pruned unless 35 | # they are connected to another crate in the graph that hasn't been pruned, 36 | # so it should be used with care. The identifiers are [Package ID Specifications] 37 | # (https://doc.rust-lang.org/cargo/reference/pkgid-spec.html) 38 | #exclude = [] 39 | # If true, metadata will be collected with `--all-features`. Note that this can't 40 | # be toggled off if true, if you want to conditionally enable `--all-features` it 41 | # is recommended to pass `--all-features` on the cmd line instead 42 | all-features = false 43 | # If true, metadata will be collected with `--no-default-features`. The same 44 | # caveat with `all-features` applies 45 | no-default-features = false 46 | # If set, these feature will be enabled when collecting metadata. If `--features` 47 | # is specified on the cmd line they will take precedence over this option. 48 | #features = [] 49 | # When outputting inclusion graphs in diagnostics that include features, this 50 | # option can be used to specify the depth at which feature edges will be added. 51 | # This option is included since the graphs can be quite large and the addition 52 | # of features from the crate(s) to all of the graph roots can be far too verbose. 53 | # This option can be overridden via `--feature-depth` on the cmd line 54 | feature-depth = 1 55 | 56 | # This section is considered when running `cargo deny check advisories` 57 | # More documentation for the advisories section can be found here: 58 | # https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html 59 | [advisories] 60 | # The path where the advisory database is cloned/fetched into 61 | db-path = "~/.cargo/advisory-db" 62 | # The url(s) of the advisory databases to use 63 | db-urls = ["https://github.com/rustsec/advisory-db"] 64 | # The lint level for security vulnerabilities 65 | vulnerability = "deny" 66 | # The lint level for unmaintained crates 67 | unmaintained = "warn" 68 | # The lint level for crates that have been yanked from their source registry 69 | yanked = "warn" 70 | # The lint level for crates with security notices. Note that as of 71 | # 2019-12-17 there are no security notice advisories in 72 | # https://github.com/rustsec/advisory-db 73 | notice = "warn" 74 | # A list of advisory IDs to ignore. Note that ignored advisories will still 75 | # output a note when they are encountered. 76 | ignore = [ 77 | #"RUSTSEC-0000-0000", 78 | ] 79 | # Threshold for security vulnerabilities, any vulnerability with a CVSS score 80 | # lower than the range specified will be ignored. Note that ignored advisories 81 | # will still output a note when they are encountered. 82 | # * None - CVSS Score 0.0 83 | # * Low - CVSS Score 0.1 - 3.9 84 | # * Medium - CVSS Score 4.0 - 6.9 85 | # * High - CVSS Score 7.0 - 8.9 86 | # * Critical - CVSS Score 9.0 - 10.0 87 | #severity-threshold = 88 | 89 | # If this is true, then cargo deny will use the git executable to fetch advisory database. 90 | # If this is false, then it uses a built-in git library. 91 | # Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support. 92 | # See Git Authentication for more information about setting up git authentication. 93 | #git-fetch-with-cli = true 94 | 95 | # This section is considered when running `cargo deny check licenses` 96 | # More documentation for the licenses section can be found here: 97 | # https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html 98 | [licenses] 99 | # The lint level for crates which do not have a detectable license 100 | unlicensed = "deny" 101 | # List of explicitly allowed licenses 102 | # See https://spdx.org/licenses/ for list of possible licenses 103 | # [possible values: any SPDX 3.11 short identifier (+ optional exception)]. 104 | allow = [ 105 | "MIT", 106 | "Apache-2.0", 107 | "Apache-2.0 WITH LLVM-exception", 108 | ] 109 | # List of explicitly disallowed licenses 110 | # See https://spdx.org/licenses/ for list of possible licenses 111 | # [possible values: any SPDX 3.11 short identifier (+ optional exception)]. 112 | deny = [ 113 | #"Nokia", 114 | ] 115 | # Lint level for licenses considered copyleft 116 | copyleft = "warn" 117 | # Blanket approval or denial for OSI-approved or FSF Free/Libre licenses 118 | # * both - The license will be approved if it is both OSI-approved *AND* FSF 119 | # * either - The license will be approved if it is either OSI-approved *OR* FSF 120 | # * osi-only - The license will be approved if is OSI-approved *AND NOT* FSF 121 | # * fsf-only - The license will be approved if is FSF *AND NOT* OSI-approved 122 | # * neither - This predicate is ignored and the default lint level is used 123 | allow-osi-fsf-free = "neither" 124 | # Lint level used when no other predicates are matched 125 | # 1. License isn't in the allow or deny lists 126 | # 2. License isn't copyleft 127 | # 3. License isn't OSI/FSF, or allow-osi-fsf-free = "neither" 128 | default = "deny" 129 | # The confidence threshold for detecting a license from license text. 130 | # The higher the value, the more closely the license text must be to the 131 | # canonical license text of a valid SPDX license file. 132 | # [possible values: any between 0.0 and 1.0]. 133 | confidence-threshold = 0.8 134 | # Allow 1 or more licenses on a per-crate basis, so that particular licenses 135 | # aren't accepted for every possible crate as with the normal allow list 136 | exceptions = [ 137 | # Each entry is the crate and version constraint, and its specific allow 138 | # list 139 | #{ allow = ["Zlib"], name = "adler32", version = "*" }, 140 | 141 | # The Unicode-DFS--2016 license is necessary for unicode-ident because they 142 | # use data from the unicode tables to generate the tables which are 143 | # included in the application. We do not distribute those data files so 144 | # this is not a problem for us. See https://github.com/dtolnay/unicode-ident/pull/9/files 145 | # for more details. 146 | { allow = ["MIT", "Apache-2.0", "Unicode-DFS-2016"], name = "unicode-ident" }, 147 | ] 148 | 149 | # Some crates don't have (easily) machine readable licensing information, 150 | # adding a clarification entry for it allows you to manually specify the 151 | # licensing information 152 | #[[licenses.clarify]] 153 | # The name of the crate the clarification applies to 154 | #name = "ring" 155 | # The optional version constraint for the crate 156 | #version = "*" 157 | # The SPDX expression for the license requirements of the crate 158 | #expression = "MIT AND ISC AND OpenSSL" 159 | # One or more files in the crate's source used as the "source of truth" for 160 | # the license expression. If the contents match, the clarification will be used 161 | # when running the license check, otherwise the clarification will be ignored 162 | # and the crate will be checked normally, which may produce warnings or errors 163 | # depending on the rest of your configuration 164 | #license-files = [ 165 | # Each entry is a crate relative path, and the (opaque) hash of its contents 166 | #{ path = "LICENSE", hash = 0xbd0eed23 } 167 | #] 168 | 169 | [licenses.private] 170 | # If true, ignores workspace crates that aren't published, or are only 171 | # published to private registries. 172 | # To see how to mark a crate as unpublished (to the official registry), 173 | # visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field. 174 | ignore = false 175 | # One or more private registries that you might publish crates to, if a crate 176 | # is only published to private registries, and ignore is true, the crate will 177 | # not have its license(s) checked 178 | registries = [ 179 | #"https://sekretz.com/registry 180 | ] 181 | 182 | # This section is considered when running `cargo deny check bans`. 183 | # More documentation about the 'bans' section can be found here: 184 | # https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html 185 | [bans] 186 | # Lint level for when multiple versions of the same crate are detected 187 | multiple-versions = "warn" 188 | # Lint level for when a crate version requirement is `*` 189 | wildcards = "allow" 190 | # The graph highlighting used when creating dotgraphs for crates 191 | # with multiple versions 192 | # * lowest-version - The path to the lowest versioned duplicate is highlighted 193 | # * simplest-path - The path to the version with the fewest edges is highlighted 194 | # * all - Both lowest-version and simplest-path are used 195 | highlight = "all" 196 | # The default lint level for `default` features for crates that are members of 197 | # the workspace that is being checked. This can be overriden by allowing/denying 198 | # `default` on a crate-by-crate basis if desired. 199 | workspace-default-features = "allow" 200 | # The default lint level for `default` features for external crates that are not 201 | # members of the workspace. This can be overriden by allowing/denying `default` 202 | # on a crate-by-crate basis if desired. 203 | external-default-features = "allow" 204 | # List of crates that are allowed. Use with care! 205 | allow = [ 206 | #{ name = "ansi_term", version = "=0.11.0" }, 207 | ] 208 | # List of crates to deny 209 | deny = [ 210 | # Each entry the name of a crate and a version range. If version is 211 | # not specified, all versions will be matched. 212 | #{ name = "ansi_term", version = "=0.11.0" }, 213 | # 214 | # Wrapper crates can optionally be specified to allow the crate when it 215 | # is a direct dependency of the otherwise banned crate 216 | #{ name = "ansi_term", version = "=0.11.0", wrappers = [] }, 217 | ] 218 | 219 | # List of features to allow/deny 220 | # Each entry the name of a crate and a version range. If version is 221 | # not specified, all versions will be matched. 222 | #[[bans.features]] 223 | #name = "reqwest" 224 | # Features to not allow 225 | #deny = ["json"] 226 | # Features to allow 227 | #allow = [ 228 | # "rustls", 229 | # "__rustls", 230 | # "__tls", 231 | # "hyper-rustls", 232 | # "rustls", 233 | # "rustls-pemfile", 234 | # "rustls-tls-webpki-roots", 235 | # "tokio-rustls", 236 | # "webpki-roots", 237 | #] 238 | # If true, the allowed features must exactly match the enabled feature set. If 239 | # this is set there is no point setting `deny` 240 | #exact = true 241 | 242 | # Certain crates/versions that will be skipped when doing duplicate detection. 243 | skip = [ 244 | #{ name = "ansi_term", version = "=0.11.0" }, 245 | ] 246 | # Similarly to `skip` allows you to skip certain crates during duplicate 247 | # detection. Unlike skip, it also includes the entire tree of transitive 248 | # dependencies starting at the specified crate, up to a certain depth, which is 249 | # by default infinite. 250 | skip-tree = [ 251 | #{ name = "ansi_term", version = "=0.11.0", depth = 20 }, 252 | ] 253 | 254 | # This section is considered when running `cargo deny check sources`. 255 | # More documentation about the 'sources' section can be found here: 256 | # https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html 257 | [sources] 258 | # Lint level for what to happen when a crate from a crate registry that is not 259 | # in the allow list is encountered 260 | unknown-registry = "warn" 261 | # Lint level for what to happen when a crate from a git repository that is not 262 | # in the allow list is encountered 263 | unknown-git = "warn" 264 | # List of URLs for allowed crate registries. Defaults to the crates.io index 265 | # if not specified. If it is specified but empty, no registries are allowed. 266 | allow-registry = ["https://github.com/rust-lang/crates.io-index"] 267 | # List of URLs for allowed Git repositories 268 | allow-git = [] 269 | 270 | [sources.allow-org] 271 | # 1 or more github.com organizations to allow git sources for 272 | # github = [""] 273 | # 1 or more gitlab.com organizations to allow git sources for 274 | # gitlab = [""] 275 | # 1 or more bitbucket.org organizations to allow git sources for 276 | # bitbucket = [""] 277 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 100 2 | -------------------------------------------------------------------------------- /scripts/ci/build-test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eoux pipefail 3 | 4 | cargo build --workspace --all-features 5 | 6 | RUST_LOG=error cargo test --workspace --all-features 7 | 8 | -------------------------------------------------------------------------------- /scripts/ci/clippy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eoux pipefail 3 | 4 | cargo clippy --all-targets --all-features -- -D warnings 5 | 6 | -------------------------------------------------------------------------------- /scripts/ci/fmt: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eoux pipefail 3 | 4 | cargo fmt -- --check 5 | -------------------------------------------------------------------------------- /scripts/ci/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eou pipefail 3 | 4 | ./scripts/ci/fmt 5 | ./scripts/ci/clippy 6 | ./scripts/ci/build-test 7 | --------------------------------------------------------------------------------