From 3111e8a41db19c2cf81007bb8d1f57279584638b Mon Sep 17 00:00:00 2001 From: Thomas B <9094255+Ten0@users.noreply.github.com> Date: Sun, 10 Mar 2024 13:57:43 +0100 Subject: [PATCH] Add derive for Schema (#13) Fixes #3 Introduces two new crates: - `serde_avro_derive` the user-facing crate that contains the necessary traits/structs and reexports the macro - `serde_avro_derive_macros` the proc-macro crate that contains the actual derive (not user-facing) The new traits and construction structs are not in the main crate for several reasons: - `serde_avro_fast` can be built before `serde_avro_derive`, which improves compilation time - The API of `serde_avro_derive` is not deemed stable and/or private enough for the 1.0 release of `serde_avro_fast` --- .gitignore | 2 +- Cargo.toml | 58 +-- serde_avro_derive/Cargo.toml | 19 + serde_avro_derive/src/lib.rs | 257 ++++++++++++ serde_avro_derive/tests/derive_schema.rs | 263 +++++++++++++ serde_avro_derive_macros/Cargo.toml | 22 ++ serde_avro_derive_macros/src/lib.rs | 69 ++++ serde_avro_derive_macros/src/schema.rs | 369 ++++++++++++++++++ serde_avro_fast/Cargo.toml | 52 +++ .../object_container_file_encoding.rs | 0 .../benches}/single.rs | 0 .../src}/de/deserializer/allowed_depth.rs | 0 .../src}/de/deserializer/mod.rs | 0 .../src}/de/deserializer/types/blocks.rs | 0 .../src}/de/deserializer/types/boolean.rs | 0 .../src}/de/deserializer/types/decimal.rs | 0 .../de/deserializer/types/discriminant.rs | 0 .../src}/de/deserializer/types/duration.rs | 0 .../src}/de/deserializer/types/enums.rs | 0 .../de/deserializer/types/length_delimited.rs | 0 .../src}/de/deserializer/types/mod.rs | 0 .../src}/de/deserializer/types/record.rs | 0 .../src}/de/deserializer/types/union.rs | 0 .../deserializer/unit_variant_enum_access.rs | 0 {src => serde_avro_fast/src}/de/error.rs | 0 {src => serde_avro_fast/src}/de/mod.rs | 0 {src => serde_avro_fast/src}/de/read/mod.rs | 0 {src => serde_avro_fast/src}/de/read/take.rs | 0 {src => serde_avro_fast/src}/lib.rs | 0 .../object_container_file_encoding/mod.rs | 0 .../reader/decompression.rs | 0 .../reader/mod.rs | 0 .../writer/compression.rs | 0 .../writer/mod.rs | 0 .../writer/vectored_write_polyfill.rs | 0 {src => serde_avro_fast/src}/schema/error.rs | 0 {src => serde_avro_fast/src}/schema/mod.rs | 0 .../src}/schema/safe/canonical_form.rs | 0 .../src}/schema/safe/check_for_cycles.rs | 0 .../src}/schema/safe/mod.rs | 0 .../src}/schema/safe/parsing/mod.rs | 0 .../src}/schema/safe/parsing/raw.rs | 0 .../src}/schema/safe/rabin.rs | 0 .../src}/schema/safe/serialize.rs | 0 .../src}/schema/self_referential.rs | 0 .../schema/union_variants_per_type_lookup.rs | 0 {src => serde_avro_fast/src}/ser/error.rs | 0 {src => serde_avro_fast/src}/ser/mod.rs | 0 .../src}/ser/serializer/blocks.rs | 0 .../src}/ser/serializer/decimal.rs | 0 .../ser/serializer/extract_for_duration.rs | 0 .../src}/ser/serializer/mod.rs | 0 .../src}/ser/serializer/seq_or_tuple.rs | 0 .../src}/ser/serializer/struct_or_map.rs | 0 .../src}/single_object_encoding.rs | 0 {tests => serde_avro_fast/tests}/duration.rs | 0 .../tests}/from_benches.rs | 0 .../max_depth_prevents_stack_overflow.rs | 0 .../tests}/no_cyclic_debug_on_schema.rs | 0 .../tests}/object_container_file_encoding.rs | 0 .../tests}/round_trips.rs | 0 {tests => serde_avro_fast/tests}/schema.rs | 0 .../tests}/schema_construction.rs | 0 .../tests}/single_object_encoding.rs | 0 {tests => serde_avro_fast/tests}/unions.rs | 0 65 files changed, 1059 insertions(+), 52 deletions(-) create mode 100644 serde_avro_derive/Cargo.toml create mode 100644 serde_avro_derive/src/lib.rs create mode 100644 serde_avro_derive/tests/derive_schema.rs create mode 100644 serde_avro_derive_macros/Cargo.toml create mode 100644 serde_avro_derive_macros/src/lib.rs create mode 100644 serde_avro_derive_macros/src/schema.rs create mode 100644 serde_avro_fast/Cargo.toml rename {benches => serde_avro_fast/benches}/object_container_file_encoding.rs (100%) rename {benches => serde_avro_fast/benches}/single.rs (100%) rename {src => serde_avro_fast/src}/de/deserializer/allowed_depth.rs (100%) rename {src => serde_avro_fast/src}/de/deserializer/mod.rs (100%) rename {src => serde_avro_fast/src}/de/deserializer/types/blocks.rs (100%) rename {src => serde_avro_fast/src}/de/deserializer/types/boolean.rs (100%) rename {src => serde_avro_fast/src}/de/deserializer/types/decimal.rs (100%) rename {src => serde_avro_fast/src}/de/deserializer/types/discriminant.rs (100%) rename {src => serde_avro_fast/src}/de/deserializer/types/duration.rs (100%) rename {src => serde_avro_fast/src}/de/deserializer/types/enums.rs (100%) rename {src => serde_avro_fast/src}/de/deserializer/types/length_delimited.rs (100%) rename {src => serde_avro_fast/src}/de/deserializer/types/mod.rs (100%) rename {src => serde_avro_fast/src}/de/deserializer/types/record.rs (100%) rename {src => serde_avro_fast/src}/de/deserializer/types/union.rs (100%) rename {src => serde_avro_fast/src}/de/deserializer/unit_variant_enum_access.rs (100%) rename {src => serde_avro_fast/src}/de/error.rs (100%) rename {src => serde_avro_fast/src}/de/mod.rs (100%) rename {src => serde_avro_fast/src}/de/read/mod.rs (100%) rename {src => serde_avro_fast/src}/de/read/take.rs (100%) rename {src => serde_avro_fast/src}/lib.rs (100%) rename {src => serde_avro_fast/src}/object_container_file_encoding/mod.rs (100%) rename {src => serde_avro_fast/src}/object_container_file_encoding/reader/decompression.rs (100%) rename {src => serde_avro_fast/src}/object_container_file_encoding/reader/mod.rs (100%) rename {src => serde_avro_fast/src}/object_container_file_encoding/writer/compression.rs (100%) rename {src => serde_avro_fast/src}/object_container_file_encoding/writer/mod.rs (100%) rename {src => serde_avro_fast/src}/object_container_file_encoding/writer/vectored_write_polyfill.rs (100%) rename {src => serde_avro_fast/src}/schema/error.rs (100%) rename {src => serde_avro_fast/src}/schema/mod.rs (100%) rename {src => serde_avro_fast/src}/schema/safe/canonical_form.rs (100%) rename {src => serde_avro_fast/src}/schema/safe/check_for_cycles.rs (100%) rename {src => serde_avro_fast/src}/schema/safe/mod.rs (100%) rename {src => serde_avro_fast/src}/schema/safe/parsing/mod.rs (100%) rename {src => serde_avro_fast/src}/schema/safe/parsing/raw.rs (100%) rename {src => serde_avro_fast/src}/schema/safe/rabin.rs (100%) rename {src => serde_avro_fast/src}/schema/safe/serialize.rs (100%) rename {src => serde_avro_fast/src}/schema/self_referential.rs (100%) rename {src => serde_avro_fast/src}/schema/union_variants_per_type_lookup.rs (100%) rename {src => serde_avro_fast/src}/ser/error.rs (100%) rename {src => serde_avro_fast/src}/ser/mod.rs (100%) rename {src => serde_avro_fast/src}/ser/serializer/blocks.rs (100%) rename {src => serde_avro_fast/src}/ser/serializer/decimal.rs (100%) rename {src => serde_avro_fast/src}/ser/serializer/extract_for_duration.rs (100%) rename {src => serde_avro_fast/src}/ser/serializer/mod.rs (100%) rename {src => serde_avro_fast/src}/ser/serializer/seq_or_tuple.rs (100%) rename {src => serde_avro_fast/src}/ser/serializer/struct_or_map.rs (100%) rename {src => serde_avro_fast/src}/single_object_encoding.rs (100%) rename {tests => serde_avro_fast/tests}/duration.rs (100%) rename {tests => serde_avro_fast/tests}/from_benches.rs (100%) rename {tests => serde_avro_fast/tests}/max_depth_prevents_stack_overflow.rs (100%) rename {tests => serde_avro_fast/tests}/no_cyclic_debug_on_schema.rs (100%) rename {tests => serde_avro_fast/tests}/object_container_file_encoding.rs (100%) rename {tests => serde_avro_fast/tests}/round_trips.rs (100%) rename {tests => serde_avro_fast/tests}/schema.rs (100%) rename {tests => serde_avro_fast/tests}/schema_construction.rs (100%) rename {tests => serde_avro_fast/tests}/single_object_encoding.rs (100%) rename {tests => serde_avro_fast/tests}/unions.rs (100%) diff --git a/.gitignore b/.gitignore index 4fffb2f..96ef6c0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ /target -/Cargo.lock +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml index 77b8cc4..d2ce5e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,51 +1,7 @@ -[package] - authors = ["Thomas BESSOU "] - description = "An idiomatic implementation of serde/avro (de)serialization" - edition = "2021" - license = "LGPL-3.0-only" - name = "serde_avro_fast" - repository = "https://github.com/Ten0/serde_avro_fast" - version = "1.0.0-rc.4" - -[features] - default = ["deflate"] - deflate = ["flate2"] - snappy = ["snap", "crc32fast"] - xz = ["xz2"] - zstandard = ["zstd"] - -[dependencies] - bzip2 = { version = "0.4", optional = true } - crc32fast = { version = "1", optional = true } - flate2 = { version = "1", optional = true } - integer-encoding = { default-features = false, version = "4" } - num-traits = "0.2" - rand = "0.8" - rust_decimal = { version = "1", default-features = false, features = ["std", "serde-with-str"] } - serde = "1" - serde-transcode = "1" - serde_derive = "1" - serde_json = "1" - serde_serializer_quick_unsupported = "0.1" - snap = { version = "1", optional = true } - thiserror = "1" - xz2 = { version = "0.1", optional = true } - zstd = { version = "0.13", optional = true } - -[dev-dependencies] - anyhow = "1" - apache-avro = { version = "0.14", features = ["bzip", "snappy", "xz", "zstandard"] } - criterion = "0.5" - lazy_static = "1" - paste = "1" - pretty_assertions = "1" - serde-tuple-vec-map = "1" - serde_bytes = "0.11" - -[[bench]] - harness = false - name = "single" - -[[bench]] - harness = false - name = "object_container_file_encoding" +[workspace] + members = [ + "serde_avro_derive", + "serde_avro_derive_macros", + "serde_avro_fast", + ] + resolver = "2" diff --git a/serde_avro_derive/Cargo.toml b/serde_avro_derive/Cargo.toml new file mode 100644 index 0000000..50ed568 --- /dev/null +++ b/serde_avro_derive/Cargo.toml @@ -0,0 +1,19 @@ +[package] + authors = ["Thomas BESSOU "] + description = "Derive avro schema for Rust structs for serde_avro_fast" + edition = "2021" + license = "LGPL-3.0-only" + name = "serde_avro_derive" + repository = "https://github.com/Ten0/serde_avro_fast" + version = "0.1.0" + workspace = ".." + +[dependencies] + serde_avro_derive_macros = { path = "../serde_avro_derive_macros", version = "0.1" } + serde_avro_fast = { path = "../serde_avro_fast", version = "1.0.0-rc.4" } + +[dev-dependencies] + lazy_static = "1" + pretty_assertions = "1" + regex = "1" + serde_json = "1" diff --git a/serde_avro_derive/src/lib.rs b/serde_avro_derive/src/lib.rs new file mode 100644 index 0000000..963e7da --- /dev/null +++ b/serde_avro_derive/src/lib.rs @@ -0,0 +1,257 @@ +//! Bring automatic Avro Schema generation to [`serde_avro_fast`] +//! +//! See the [`#[derive(Schema)]`](derive@Schema) documentation for more +//! information + +pub use serde_avro_fast; + +pub use serde_avro_derive_macros::*; + +use std::{any::TypeId, collections::HashMap}; + +use serde_avro_fast::schema::*; + +/// We can automatically build a schema for this type (can be `derive`d) +/// +/// This trait can be derived using [`#[derive(Schema)]`](derive@Schema) +pub trait BuildSchema { + /// Build a [`struct@Schema`] for this type + fn schema() -> Result { + Self::schema_mut().try_into() + } + /// Build a [`SchemaMut`] for this type + fn schema_mut() -> SchemaMut { + let mut builder = SchemaBuilder::default(); + Self::append_schema(&mut builder); + SchemaMut::from_nodes(builder.nodes) + } + + /// Largely internal method to build the schema. Registers the schema within + /// the builder. + /// + /// This does not check if this type already exists in the builder, so it + /// should never be called directly (instead, use + /// [`SchemaBuilder::find_or_build`]) + /// + /// The [`SchemaNode`] for this type should be put at the current end of the + /// `nodes` array, and its non-already-built dependencies should be put + /// after in the array. + fn append_schema(builder: &mut SchemaBuilder); + + /// Largely internal type used by [`#[derive(Schema)]`](derive@Schema) + /// + /// The TypeId of this type will be used to lookup whether the + /// [`SchemaNode`] for this type has already been built in the + /// [`SchemaBuilder`]. + /// + /// This indirection is required to allow non-static types to implement + /// [`BuildSchema`], and also enables using the same node for types that we + /// know map to the same schema. + type TypeLookup: std::any::Any; +} + +/// Largely internal type used by [`#[derive(Schema)]`](derive@Schema) +/// +/// You should typically not use this directly +#[derive(Default)] +pub struct SchemaBuilder { + pub nodes: Vec, + pub already_built_types: HashMap, + _private: (), +} + +impl SchemaBuilder { + /// Reserve a slot in the `nodes` array + /// + /// After building the `SchemaNode`, it should be put at the corresponding + /// position in `nodes`. + pub fn reserve(&mut self) -> usize { + let idx = self.nodes.len(); + self.nodes.push(SchemaNode::RegularType(RegularType::Null)); + idx + } + + pub fn find_or_build(&mut self) -> SchemaKey { + match self + .already_built_types + .entry(TypeId::of::()) + { + std::collections::hash_map::Entry::Occupied(entry) => *entry.get(), + std::collections::hash_map::Entry::Vacant(entry) => { + let idx = SchemaKey::from_idx(self.nodes.len()); + entry.insert(idx); + T::append_schema(self); + assert!( + self.nodes.len() > idx.idx(), + "append_schema should always insert at least a node \ + (and its dependencies below itself)" + ); + idx + } + } + } + + pub fn build_logical_type( + &mut self, + logical_type: LogicalType, + ) -> SchemaKey { + let reserved_schema_key = self.reserve(); + let new_node = SchemaNode::LogicalType { + logical_type, + inner: self.find_or_build::(), + }; + self.nodes[reserved_schema_key] = new_node; + SchemaKey::from_idx(reserved_schema_key) + } +} + +macro_rules! impl_primitive { + ($($ty:ty, $variant:ident;)+) => { + $( + impl BuildSchema for $ty { + fn append_schema(builder: &mut SchemaBuilder) { + builder.nodes.push(SchemaNode::RegularType(RegularType::$variant)); + } + type TypeLookup = Self; + } + )* + }; +} +impl_primitive!( + (), Null; + bool, Boolean; + i32, Int; + i64, Long; + f32, Float; + f64, Double; + String, String; + Vec, Bytes; +); + +macro_rules! impl_forward { + ($($ty:ty, $to:ty;)+) => { + $( + impl BuildSchema for $ty { + fn append_schema(builder: &mut SchemaBuilder) { + <$to as BuildSchema>::append_schema(builder) + } + type TypeLookup = <$to as BuildSchema>::TypeLookup; + } + )* + }; +} +impl_forward! { + str, String; + [u8], Vec; + u16, i32; + u32, i64; + u64, i64; + i8, i32; + i16, i32; + usize, i64; +} + +macro_rules! impl_ptr { + ($($($ty_path:ident)::+,)+) => { + $( + impl BuildSchema for $($ty_path)::+ { + fn append_schema(builder: &mut SchemaBuilder) { + ::append_schema(builder) + } + type TypeLookup = T::TypeLookup; + } + )* + }; +} +impl_ptr! { + Box, + std::sync::Arc, + std::rc::Rc, + std::cell::RefCell, + std::cell::Cell, +} +impl BuildSchema for &'_ T { + fn append_schema(builder: &mut SchemaBuilder) { + ::append_schema(builder) + } + type TypeLookup = T::TypeLookup; +} +impl BuildSchema for &'_ mut T { + fn append_schema(builder: &mut SchemaBuilder) { + ::append_schema(builder) + } + type TypeLookup = T::TypeLookup; +} + +impl BuildSchema for Vec { + fn append_schema(builder: &mut SchemaBuilder) { + let reserved_schema_key = builder.reserve(); + let new_node = + SchemaNode::RegularType(RegularType::Array(Array::new(builder.find_or_build::()))); + builder.nodes[reserved_schema_key] = new_node; + } + + type TypeLookup = Vec; +} + +impl BuildSchema for [T] { + fn append_schema(builder: &mut SchemaBuilder) { + as BuildSchema>::append_schema(builder) + } + type TypeLookup = as BuildSchema>::TypeLookup; +} + +impl BuildSchema for Option { + fn append_schema(builder: &mut SchemaBuilder) { + let reserved_schema_key = builder.reserve(); + let new_node = SchemaNode::RegularType(RegularType::Union(Union::new(vec![ + builder.find_or_build::<()>(), + builder.find_or_build::(), + ]))); + builder.nodes[reserved_schema_key] = new_node; + } + + type TypeLookup = Option; +} + +impl BuildSchema for [u8; N] { + fn append_schema(builder: &mut SchemaBuilder) { + builder + .nodes + .push(SchemaNode::RegularType(RegularType::Fixed(Fixed::new( + Name::from_fully_qualified_name(format!("u8_array_{}", N)), + N, + )))); + } + type TypeLookup = Self; +} + +impl, V: BuildSchema> BuildSchema for HashMap { + fn append_schema(builder: &mut SchemaBuilder) { + let reserved_schema_key = builder.reserve(); + let new_node = + SchemaNode::RegularType(RegularType::Map(Map::new(builder.find_or_build::()))); + builder.nodes[reserved_schema_key] = new_node; + } + type TypeLookup = HashMap; +} +impl, V: BuildSchema> BuildSchema + for std::collections::BTreeMap +{ + fn append_schema(builder: &mut SchemaBuilder) { + as BuildSchema>::append_schema(builder) + } + type TypeLookup = as BuildSchema>::TypeLookup; +} + +#[doc(hidden)] +pub fn hash_type_id(struct_name: &mut String, type_id: TypeId) { + use std::{ + fmt::Write, + hash::{Hash as _, Hasher as _}, + }; + #[allow(deprecated)] // I actually want to not change hasher + let mut hasher = std::hash::SipHasher::new(); + type_id.hash(&mut hasher); + write!(struct_name, "_{:016x?}", hasher.finish()).unwrap(); +} diff --git a/serde_avro_derive/tests/derive_schema.rs b/serde_avro_derive/tests/derive_schema.rs new file mode 100644 index 0000000..50300dd --- /dev/null +++ b/serde_avro_derive/tests/derive_schema.rs @@ -0,0 +1,263 @@ +use serde_avro_derive::BuildSchema; + +use pretty_assertions::assert_eq; + +fn test(expected: &str) { + let schema_raw = serde_json::to_string_pretty(&T::schema_mut()).unwrap(); + let schema = clean_schema(&schema_raw); + println!("{schema}"); + assert_eq!(schema, expected); + + // Round trip + let schema_mut: serde_avro_fast::schema::SchemaMut = schema_raw.parse().unwrap(); + dbg!(&schema_mut); + let schema2 = clean_schema(&serde_json::to_string_pretty(&schema_mut).unwrap()); + assert_eq!(schema2, expected); + let _schema: serde_avro_fast::Schema = schema_mut.try_into().unwrap(); +} + +fn clean_schema(schema: &str) -> String { + lazy_static::lazy_static! { + static ref REGEX: regex::Regex = regex::Regex::new(r#""(derive_schema\.[^_]+_)\w{16}""#).unwrap(); + } + REGEX + .replace_all(schema, r#""${1}TYPEIDHASH""#) + .into_owned() +} + +#[derive(serde_avro_derive::Schema)] +#[allow(unused)] +struct Bar { + a: i32, + b: String, +} + +#[test] +fn primitives() { + test::( + r#"{ + "type": "record", + "name": "derive_schema.Bar", + "fields": [ + { + "name": "a", + "type": "int" + }, + { + "name": "b", + "type": "string" + } + ] +}"#, + ); +} + +#[derive(serde_avro_derive::Schema)] +struct Foo { + #[allow(unused)] + primitives: Bar, +} + +#[test] +fn substruct() { + test::( + r#"{ + "type": "record", + "name": "derive_schema.Foo", + "fields": [ + { + "name": "primitives", + "type": { + "type": "record", + "name": "Bar", + "fields": [ + { + "name": "a", + "type": "int" + }, + { + "name": "b", + "type": "string" + } + ] + } + } + ] +}"#, + ); +} + +#[derive(serde_avro_derive::Schema)] +#[allow(unused)] +struct Complex { + s1: Foo, + s2: Foo, + vec: Vec, +} + +#[test] +fn complex() { + test::( + r#"{ + "type": "record", + "name": "derive_schema.Complex", + "fields": [ + { + "name": "s1", + "type": { + "type": "record", + "name": "Foo", + "fields": [ + { + "name": "primitives", + "type": { + "type": "record", + "name": "Bar", + "fields": [ + { + "name": "a", + "type": "int" + }, + { + "name": "b", + "type": "string" + } + ] + } + } + ] + } + }, + { + "name": "s2", + "type": "Foo" + }, + { + "name": "vec", + "type": { + "type": "array", + "items": "string" + } + } + ] +}"#, + ); +} + +#[derive(serde_avro_derive::Schema)] +#[allow(unused)] +struct Generics<'a, F> { + s1: F, + s2: &'a F, + s: &'a str, +} + +#[test] +fn generics() { + test::>( + r#"{ + "type": "record", + "name": "derive_schema.Generics_TYPEIDHASH", + "fields": [ + { + "name": "s1", + "type": { + "type": "record", + "name": "Bar", + "fields": [ + { + "name": "a", + "type": "int" + }, + { + "name": "b", + "type": "string" + } + ] + } + }, + { + "name": "s2", + "type": "Bar" + }, + { + "name": "s", + "type": "string" + } + ] +}"#, + ); +} + +#[derive(serde_avro_derive::Schema)] +#[allow(unused)] +struct Lifetimes<'a, 'b> { + s: &'a [&'b str], + #[avro_schema(skip)] + z: String, +} + +#[test] +fn lifetimes() { + test::>( + r#"{ + "type": "record", + "name": "derive_schema.Lifetimes", + "fields": [ + { + "name": "s", + "type": { + "type": "array", + "items": "string" + } + } + ] +}"#, + ); +} + +#[derive(serde_avro_derive::Schema)] +#[allow(unused)] +struct LogicalTypes<'a> { + #[avro_schema(logical_type = Uuid)] + uuid: &'a str, + #[avro_schema(logical_type = Decimal, scale = 1, precision = 4)] + decimal: f64, + #[avro_schema(logical_type = CustomLogicalType)] + custom: &'a str, +} + +#[test] +fn logical_types() { + test::>( + r#"{ + "type": "record", + "name": "derive_schema.LogicalTypes", + "fields": [ + { + "name": "uuid", + "type": { + "logicalType": "uuid", + "type": "string" + } + }, + { + "name": "decimal", + "type": { + "logicalType": "decimal", + "type": "double", + "scale": 1, + "precision": 4 + } + }, + { + "name": "custom", + "type": { + "logicalType": "CustomLogicalType", + "type": "string" + } + } + ] +}"#, + ); +} diff --git a/serde_avro_derive_macros/Cargo.toml b/serde_avro_derive_macros/Cargo.toml new file mode 100644 index 0000000..719e129 --- /dev/null +++ b/serde_avro_derive_macros/Cargo.toml @@ -0,0 +1,22 @@ +[package] + authors = ["Thomas BESSOU "] + description = "Derive avro schema for Rust structs for serde_avro_fast" + edition = "2021" + license = "LGPL-3.0-only" + name = "serde_avro_derive_macros" + repository = "https://github.com/Ten0/serde_avro_fast" + version = "0.1.0" + workspace = ".." + +[lib] + proc-macro = true + +[dependencies] + darling = "0.20" + proc-macro2 = "1" + quote = "1" + syn = { version = "2", features = ["visit", "visit-mut", "extra-traits"] } + +[dev-dependencies] + serde_avro_derive = { path = "../serde_avro_derive" } + serde_json = "1" diff --git a/serde_avro_derive_macros/src/lib.rs b/serde_avro_derive_macros/src/lib.rs new file mode 100644 index 0000000..668a4b7 --- /dev/null +++ b/serde_avro_derive_macros/src/lib.rs @@ -0,0 +1,69 @@ +//! Internal macros crate for the `serde_avro_derive` crate +//! +//! Use [`serde_avro_derive`](https://docs.rs/serde_avro_derive/) instead of using this crate directly + +mod schema; + +use darling::FromDeriveInput; + +#[proc_macro_derive(Schema, attributes(avro_schema))] +/// Derive the ability to build an Avro schema for a type +/// (implements `BuildSchema`) +/// +/// # Example +/// ``` +/// #[derive(serde_avro_derive::Schema)] +/// struct Foo { +/// primitives: Bar, +/// } +/// +/// #[derive(serde_avro_derive::Schema)] +/// #[allow(unused)] +/// struct Bar { +/// a: i32, +/// b: String, +/// } +/// +/// let schema = Foo::schema(); +/// +/// // The [`serde_avro_fast::schema::BuildSchema`] implementation will +/// // generate the following schema: +/// let schema_str = r#"{ +/// "type": "record", +/// "name": "rust_out.Foo", +/// "fields": [ +/// { +/// "name": "primitives", +/// "type": { +/// "type": "record", +/// "name": "Bar", +/// "fields": [ +/// { +/// "name": "a", +/// "type": "int" +/// }, +/// { +/// "name": "b", +/// "type": "string" +/// } +/// ] +/// } +/// } +/// ] +/// }"#; +/// // Note that the `rust_out` namespace here is only due to the fact this is a doctest: +/// // the name will always be crate_name.path.to.module.Foo +/// // (but for doctests the crate is called rust_out and the struct is at top level) +/// # use serde_avro_derive::BuildSchema; +/// # let actual_schema = serde_json::to_string_pretty(&Foo::schema_mut()).unwrap(); +/// # assert_eq!(actual_schema, schema_str); +/// ``` +pub fn schema_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let derive_input = syn::parse_macro_input!(input as syn::DeriveInput); + + match FromDeriveInput::from_derive_input(&derive_input).map(schema::schema_impl) { + Err(e) => e.write_errors().into(), + Ok(Ok(tokens)) => tokens.into(), + Ok(Err(e)) => e.into_compile_error().into(), + } +} diff --git a/serde_avro_derive_macros/src/schema.rs b/serde_avro_derive_macros/src/schema.rs new file mode 100644 index 0000000..f280b8c --- /dev/null +++ b/serde_avro_derive_macros/src/schema.rs @@ -0,0 +1,369 @@ +use { + proc_macro2::{Span, TokenStream}, + quote::{format_ident, quote}, + syn::{ + parse_quote, + visit::{self, Visit}, + visit_mut::{self, VisitMut}, + Error, + }, +}; + +#[derive(darling::FromDeriveInput)] +#[darling(attributes(avro_schema), supports(struct_named))] +pub(crate) struct SchemaDeriveInput { + ident: proc_macro2::Ident, + data: darling::ast::Data<(), SchemaDeriveField>, + generics: syn::Generics, +} + +#[derive(darling::FromField)] +#[darling(attributes(avro_schema))] +pub(crate) struct SchemaDeriveField { + ident: Option, + ty: syn::Type, + + skip: darling::util::Flag, + logical_type: Option, + scale: Option>, + precision: Option>, +} + +pub(crate) fn schema_impl(input: SchemaDeriveInput) -> Result { + let mut errors = TokenStream::default(); + + let mut fields = input + .data + .take_struct() + .expect("Supports directive should prevent enums"); + fields.fields.retain(|f| !f.skip.is_present()); + + let struct_ident = &input.ident; + let struct_name = struct_ident.to_string(); + let mut generics = input.generics; + + let mut added_where_clause_predicate_for_types: std::collections::HashSet<_> = + Default::default(); + let field_types = fields + .iter() + .map(|f| { + let mut ty = &f.ty; + while let syn::Type::Reference(r) = ty { + // This allows not requiring the user to specify that T: 'a + // as an explicit where predicate, and simplifies the calls + ty = &r.elem; + } + if !generics.params.is_empty() { + let mut is_relevant_generic = IsRelevantGeneric { + generics: &generics, + result: false, + }; + is_relevant_generic.visit_type(ty); + if is_relevant_generic.result { + if added_where_clause_predicate_for_types.insert(ty) { + generics + .make_where_clause() + .predicates + .push(parse_quote!(#ty: serde_avro_derive::BuildSchema)); + } + } + } + ty + }) + .collect::>(); + + let field_instantiations = fields.iter().zip(&field_types).map(|(field, ty)| { + let mut logical_type_ident = field.logical_type.as_ref(); + if logical_type_ident.is_none() { + if let syn::Type::Path(path) = &field.ty { + if let Some(last_type_ident) = path.path.segments.last().map(|s| &s.ident) { + let last_type_str = last_type_ident.to_string(); + match last_type_str.as_str() { + "Uuid" => logical_type_ident = Some(last_type_ident), + _ => {} + } + } + } + } + match logical_type_ident { + None => quote! { builder.find_or_build::<#ty>() }, + Some(logical_type_ident) => { + let logical_type_str = logical_type_ident.to_string(); + let mut logical_type = if [ + "Decimal", + "Uuid", + "Date", + "TimeMillis", + "TimeMicros", + "TimestampMillis", + "TimestampMicros", + "Duration", + ] + .contains(&logical_type_str.as_str()) + { + // This is a known logical type + quote! { schema::LogicalType::#logical_type_ident } + } else { + quote! { schema::LogicalType::Unknown( + #logical_type_str.to_owned() + ) } + }; + if logical_type_str == "Decimal" { + let zero = parse_quote!(0); + let mut error = |missing_field: &str| { + errors.extend( + Error::new_spanned( + logical_type_ident, + format_args!( + "`Decimal` logical type requires \ + `{missing_field}` attribute to be set" + ), + ) + .to_compile_error(), + ); + &zero + }; + let scale = field + .scale + .as_ref() + .map_or_else(|| error("scale"), |w| &w.value); + let precision = field + .precision + .as_ref() + .map_or_else(|| error("precision"), |w| &w.value); + logical_type.extend(quote! { + (schema::Decimal::new(#scale, #precision)) + }); + } else { + let mut error = |field_that_should_not_be_here: &WithMetaPath| { + errors.extend( + Error::new_spanned( + &field_that_should_not_be_here.path, + format_args!( + "`{}` attribute is not relevant for `{}` logical type", + darling::util::path_to_string( + &field_that_should_not_be_here.path + ), + logical_type_str + ), + ) + .to_compile_error(), + ); + }; + if let Some(f) = &field.scale { + error(&f); + } + if let Some(f) = &field.precision { + error(&f); + } + } + quote! { builder.build_logical_type::<#ty>(#logical_type) } + } + } + }); + + let field_names = fields + .iter() + .map(|f| f.ident.as_ref().map(|i| i.to_string())) + .collect::>>() + .ok_or_else(|| Error::new(Span::call_site(), "Unnamed fields are not supported"))?; + + let has_non_lifetime_generics = generics + .params + .iter() + .any(|gp| !matches!(gp, syn::GenericParam::Lifetime(_))); + let (type_lookup, type_lookup_decl): (syn::Type, _) = match has_non_lifetime_generics { + false => { + let type_lookup = if generics.params.is_empty() { + parse_quote!(Self) + } else { + let mut generics_static = generics.clone(); + TurnLifetimesToStatic.visit_generics_mut(&mut generics_static); + let (_, ty_generics, _) = generics_static.split_for_impl(); + parse_quote!(#struct_ident #ty_generics) + }; + (type_lookup, None) + } + true => { + // The struct we are deriving on is generic, but we need the TypeLookup to be + // 'static otherwise it won't implement `Any`, so we need to generate a + // dedicated struct for it. + + // E.g., for a struct + // struct Foo { + // f1: Bar, + // f2: Baz; + // } + // We'll generate + // struct FooTypeLookup { + // f1: T0, + // f1: T1, + // } + // and then use type TypeLookup = + // TypeLookup< + // ::TypeLookup, + // ::TypeLookup, + // >; + let type_lookup_ident = format_ident!("{struct_ident}TypeLookup"); + let type_params: Vec = + (0..fields.len()).map(|i| format_ident!("T{}", i)).collect(); + let struct_decl = syn::ItemStruct { + attrs: Default::default(), + vis: syn::Visibility::Inherited, + struct_token: syn::token::Struct::default(), + ident: type_lookup_ident.clone(), + generics: syn::Generics { + lt_token: Some(Default::default()), + params: type_params + .iter() + .map(|ident| -> syn::GenericParam { parse_quote!(#ident) }) + .collect(), + gt_token: Some(Default::default()), + where_clause: None, + }, + fields: syn::Fields::Named(syn::FieldsNamed { + named: fields + .iter() + .zip(&type_params) + .map(|(field, ident)| syn::Field { + attrs: Default::default(), + vis: syn::Visibility::Inherited, + ident: field.ident.clone(), + colon_token: Some(Default::default()), + ty: { parse_quote!(#ident) }, + mutability: syn::FieldMutability::None, + }) + .collect(), + brace_token: Default::default(), + }), + semi_token: None, + }; + let type_lookup = syn::PathSegment { + ident: type_lookup_ident, + arguments: syn::PathArguments::AngleBracketed( + syn::AngleBracketedGenericArguments { + args: field_types + .iter() + .map(|ty| -> syn::GenericArgument { + parse_quote!(<#ty as serde_avro_derive::BuildSchema>::TypeLookup) + }) + .collect(), + colon2_token: Default::default(), + lt_token: Default::default(), + gt_token: Default::default(), + }, + ), + }; + (parse_quote!(#type_lookup), Some(struct_decl)) + } + }; + + let add_type_id_to_fqn = if has_non_lifetime_generics { + quote! { + serde_avro_derive::hash_type_id( + &mut struct_name, + std::any::TypeId::of::<::TypeLookup>(), + ); + } + } else { + quote! {} + }; + + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + + Ok(quote! { + const _: () = { + use serde_avro_derive::serde_avro_fast::schema; + + impl #impl_generics serde_avro_derive::BuildSchema for #struct_ident #ty_generics #where_clause { + fn append_schema(builder: &mut serde_avro_derive::SchemaBuilder) { + let reserved_schema_key = builder.reserve(); + let mut struct_name = module_path!().replace("::", "."); + struct_name.push('.'); + struct_name.push_str(#struct_name); + #add_type_id_to_fqn + let new_node = schema::SchemaNode::RegularType(schema::RegularType::Record( + schema::Record::new( + schema::Name::from_fully_qualified_name(struct_name), + vec![#( + schema::RecordField::new( + #field_names, + #field_instantiations, + ), + )*], + ), + )); + builder.nodes[reserved_schema_key] = new_node; + } + + type TypeLookup = #type_lookup; + } + + #type_lookup_decl + + #errors + }; + }) +} + +struct IsRelevantGeneric<'a> { + generics: &'a syn::Generics, + result: bool, +} +impl Visit<'_> for IsRelevantGeneric<'_> { + fn visit_type(&mut self, v: &syn::Type) { + match v { + syn::Type::Path(v) => { + if let Some(v) = v.path.get_ident() { + if self.generics.params.iter().any(|p| match p { + syn::GenericParam::Type(t) => t.ident == *v, + _ => false, + }) { + self.result = true; + } + } + } + _ => {} + } + visit::visit_type(self, v); + } + fn visit_lifetime(&mut self, v: &syn::Lifetime) { + if self.generics.params.iter().any(|p| match p { + syn::GenericParam::Lifetime(l) => l.lifetime == *v, + _ => false, + }) { + self.result = true; + } + visit::visit_lifetime(self, v) + } + fn visit_const_param(&mut self, v: &syn::ConstParam) { + if self.generics.params.iter().any(|p| match p { + syn::GenericParam::Const(c) => c == v, + _ => false, + }) { + self.result = true; + } + visit::visit_const_param(self, v) + } +} + +struct TurnLifetimesToStatic; +impl VisitMut for TurnLifetimesToStatic { + fn visit_lifetime_mut(&mut self, i: &mut syn::Lifetime) { + i.ident = format_ident!("static"); + visit_mut::visit_lifetime_mut(self, i) + } +} + +struct WithMetaPath { + path: syn::Path, + value: T, +} +impl darling::FromMeta for WithMetaPath { + fn from_meta(meta: &syn::Meta) -> Result { + Ok(Self { + value: ::from_meta(meta)?, + path: meta.path().clone(), + }) + } +} diff --git a/serde_avro_fast/Cargo.toml b/serde_avro_fast/Cargo.toml new file mode 100644 index 0000000..a555dc7 --- /dev/null +++ b/serde_avro_fast/Cargo.toml @@ -0,0 +1,52 @@ +[package] + authors = ["Thomas BESSOU "] + description = "An idiomatic implementation of serde/avro (de)serialization" + edition = "2021" + license = "LGPL-3.0-only" + name = "serde_avro_fast" + repository = "https://github.com/Ten0/serde_avro_fast" + version = "1.0.0-rc.4" + workspace = ".." + +[features] + default = ["deflate"] + deflate = ["flate2"] + snappy = ["snap", "crc32fast"] + xz = ["xz2"] + zstandard = ["zstd"] + +[dependencies] + bzip2 = { version = "0.4", optional = true } + crc32fast = { version = "1", optional = true } + flate2 = { version = "1", optional = true } + integer-encoding = { default-features = false, version = "4" } + num-traits = "0.2" + rand = "0.8" + rust_decimal = { version = "1", default-features = false, features = ["std", "serde-with-str"] } + serde = "1" + serde-transcode = "1" + serde_derive = "1" + serde_json = "1" + serde_serializer_quick_unsupported = "0.1" + snap = { version = "1", optional = true } + thiserror = "1" + xz2 = { version = "0.1", optional = true } + zstd = { version = "0.13", optional = true } + +[dev-dependencies] + anyhow = "1" + apache-avro = { version = "0.14", features = ["bzip", "snappy", "xz", "zstandard"] } + criterion = "0.5" + lazy_static = "1" + paste = "1" + pretty_assertions = "1" + serde-tuple-vec-map = "1" + serde_bytes = "0.11" + +[[bench]] + harness = false + name = "single" + +[[bench]] + harness = false + name = "object_container_file_encoding" diff --git a/benches/object_container_file_encoding.rs b/serde_avro_fast/benches/object_container_file_encoding.rs similarity index 100% rename from benches/object_container_file_encoding.rs rename to serde_avro_fast/benches/object_container_file_encoding.rs diff --git a/benches/single.rs b/serde_avro_fast/benches/single.rs similarity index 100% rename from benches/single.rs rename to serde_avro_fast/benches/single.rs diff --git a/src/de/deserializer/allowed_depth.rs b/serde_avro_fast/src/de/deserializer/allowed_depth.rs similarity index 100% rename from src/de/deserializer/allowed_depth.rs rename to serde_avro_fast/src/de/deserializer/allowed_depth.rs diff --git a/src/de/deserializer/mod.rs b/serde_avro_fast/src/de/deserializer/mod.rs similarity index 100% rename from src/de/deserializer/mod.rs rename to serde_avro_fast/src/de/deserializer/mod.rs diff --git a/src/de/deserializer/types/blocks.rs b/serde_avro_fast/src/de/deserializer/types/blocks.rs similarity index 100% rename from src/de/deserializer/types/blocks.rs rename to serde_avro_fast/src/de/deserializer/types/blocks.rs diff --git a/src/de/deserializer/types/boolean.rs b/serde_avro_fast/src/de/deserializer/types/boolean.rs similarity index 100% rename from src/de/deserializer/types/boolean.rs rename to serde_avro_fast/src/de/deserializer/types/boolean.rs diff --git a/src/de/deserializer/types/decimal.rs b/serde_avro_fast/src/de/deserializer/types/decimal.rs similarity index 100% rename from src/de/deserializer/types/decimal.rs rename to serde_avro_fast/src/de/deserializer/types/decimal.rs diff --git a/src/de/deserializer/types/discriminant.rs b/serde_avro_fast/src/de/deserializer/types/discriminant.rs similarity index 100% rename from src/de/deserializer/types/discriminant.rs rename to serde_avro_fast/src/de/deserializer/types/discriminant.rs diff --git a/src/de/deserializer/types/duration.rs b/serde_avro_fast/src/de/deserializer/types/duration.rs similarity index 100% rename from src/de/deserializer/types/duration.rs rename to serde_avro_fast/src/de/deserializer/types/duration.rs diff --git a/src/de/deserializer/types/enums.rs b/serde_avro_fast/src/de/deserializer/types/enums.rs similarity index 100% rename from src/de/deserializer/types/enums.rs rename to serde_avro_fast/src/de/deserializer/types/enums.rs diff --git a/src/de/deserializer/types/length_delimited.rs b/serde_avro_fast/src/de/deserializer/types/length_delimited.rs similarity index 100% rename from src/de/deserializer/types/length_delimited.rs rename to serde_avro_fast/src/de/deserializer/types/length_delimited.rs diff --git a/src/de/deserializer/types/mod.rs b/serde_avro_fast/src/de/deserializer/types/mod.rs similarity index 100% rename from src/de/deserializer/types/mod.rs rename to serde_avro_fast/src/de/deserializer/types/mod.rs diff --git a/src/de/deserializer/types/record.rs b/serde_avro_fast/src/de/deserializer/types/record.rs similarity index 100% rename from src/de/deserializer/types/record.rs rename to serde_avro_fast/src/de/deserializer/types/record.rs diff --git a/src/de/deserializer/types/union.rs b/serde_avro_fast/src/de/deserializer/types/union.rs similarity index 100% rename from src/de/deserializer/types/union.rs rename to serde_avro_fast/src/de/deserializer/types/union.rs diff --git a/src/de/deserializer/unit_variant_enum_access.rs b/serde_avro_fast/src/de/deserializer/unit_variant_enum_access.rs similarity index 100% rename from src/de/deserializer/unit_variant_enum_access.rs rename to serde_avro_fast/src/de/deserializer/unit_variant_enum_access.rs diff --git a/src/de/error.rs b/serde_avro_fast/src/de/error.rs similarity index 100% rename from src/de/error.rs rename to serde_avro_fast/src/de/error.rs diff --git a/src/de/mod.rs b/serde_avro_fast/src/de/mod.rs similarity index 100% rename from src/de/mod.rs rename to serde_avro_fast/src/de/mod.rs diff --git a/src/de/read/mod.rs b/serde_avro_fast/src/de/read/mod.rs similarity index 100% rename from src/de/read/mod.rs rename to serde_avro_fast/src/de/read/mod.rs diff --git a/src/de/read/take.rs b/serde_avro_fast/src/de/read/take.rs similarity index 100% rename from src/de/read/take.rs rename to serde_avro_fast/src/de/read/take.rs diff --git a/src/lib.rs b/serde_avro_fast/src/lib.rs similarity index 100% rename from src/lib.rs rename to serde_avro_fast/src/lib.rs diff --git a/src/object_container_file_encoding/mod.rs b/serde_avro_fast/src/object_container_file_encoding/mod.rs similarity index 100% rename from src/object_container_file_encoding/mod.rs rename to serde_avro_fast/src/object_container_file_encoding/mod.rs diff --git a/src/object_container_file_encoding/reader/decompression.rs b/serde_avro_fast/src/object_container_file_encoding/reader/decompression.rs similarity index 100% rename from src/object_container_file_encoding/reader/decompression.rs rename to serde_avro_fast/src/object_container_file_encoding/reader/decompression.rs diff --git a/src/object_container_file_encoding/reader/mod.rs b/serde_avro_fast/src/object_container_file_encoding/reader/mod.rs similarity index 100% rename from src/object_container_file_encoding/reader/mod.rs rename to serde_avro_fast/src/object_container_file_encoding/reader/mod.rs diff --git a/src/object_container_file_encoding/writer/compression.rs b/serde_avro_fast/src/object_container_file_encoding/writer/compression.rs similarity index 100% rename from src/object_container_file_encoding/writer/compression.rs rename to serde_avro_fast/src/object_container_file_encoding/writer/compression.rs diff --git a/src/object_container_file_encoding/writer/mod.rs b/serde_avro_fast/src/object_container_file_encoding/writer/mod.rs similarity index 100% rename from src/object_container_file_encoding/writer/mod.rs rename to serde_avro_fast/src/object_container_file_encoding/writer/mod.rs diff --git a/src/object_container_file_encoding/writer/vectored_write_polyfill.rs b/serde_avro_fast/src/object_container_file_encoding/writer/vectored_write_polyfill.rs similarity index 100% rename from src/object_container_file_encoding/writer/vectored_write_polyfill.rs rename to serde_avro_fast/src/object_container_file_encoding/writer/vectored_write_polyfill.rs diff --git a/src/schema/error.rs b/serde_avro_fast/src/schema/error.rs similarity index 100% rename from src/schema/error.rs rename to serde_avro_fast/src/schema/error.rs diff --git a/src/schema/mod.rs b/serde_avro_fast/src/schema/mod.rs similarity index 100% rename from src/schema/mod.rs rename to serde_avro_fast/src/schema/mod.rs diff --git a/src/schema/safe/canonical_form.rs b/serde_avro_fast/src/schema/safe/canonical_form.rs similarity index 100% rename from src/schema/safe/canonical_form.rs rename to serde_avro_fast/src/schema/safe/canonical_form.rs diff --git a/src/schema/safe/check_for_cycles.rs b/serde_avro_fast/src/schema/safe/check_for_cycles.rs similarity index 100% rename from src/schema/safe/check_for_cycles.rs rename to serde_avro_fast/src/schema/safe/check_for_cycles.rs diff --git a/src/schema/safe/mod.rs b/serde_avro_fast/src/schema/safe/mod.rs similarity index 100% rename from src/schema/safe/mod.rs rename to serde_avro_fast/src/schema/safe/mod.rs diff --git a/src/schema/safe/parsing/mod.rs b/serde_avro_fast/src/schema/safe/parsing/mod.rs similarity index 100% rename from src/schema/safe/parsing/mod.rs rename to serde_avro_fast/src/schema/safe/parsing/mod.rs diff --git a/src/schema/safe/parsing/raw.rs b/serde_avro_fast/src/schema/safe/parsing/raw.rs similarity index 100% rename from src/schema/safe/parsing/raw.rs rename to serde_avro_fast/src/schema/safe/parsing/raw.rs diff --git a/src/schema/safe/rabin.rs b/serde_avro_fast/src/schema/safe/rabin.rs similarity index 100% rename from src/schema/safe/rabin.rs rename to serde_avro_fast/src/schema/safe/rabin.rs diff --git a/src/schema/safe/serialize.rs b/serde_avro_fast/src/schema/safe/serialize.rs similarity index 100% rename from src/schema/safe/serialize.rs rename to serde_avro_fast/src/schema/safe/serialize.rs diff --git a/src/schema/self_referential.rs b/serde_avro_fast/src/schema/self_referential.rs similarity index 100% rename from src/schema/self_referential.rs rename to serde_avro_fast/src/schema/self_referential.rs diff --git a/src/schema/union_variants_per_type_lookup.rs b/serde_avro_fast/src/schema/union_variants_per_type_lookup.rs similarity index 100% rename from src/schema/union_variants_per_type_lookup.rs rename to serde_avro_fast/src/schema/union_variants_per_type_lookup.rs diff --git a/src/ser/error.rs b/serde_avro_fast/src/ser/error.rs similarity index 100% rename from src/ser/error.rs rename to serde_avro_fast/src/ser/error.rs diff --git a/src/ser/mod.rs b/serde_avro_fast/src/ser/mod.rs similarity index 100% rename from src/ser/mod.rs rename to serde_avro_fast/src/ser/mod.rs diff --git a/src/ser/serializer/blocks.rs b/serde_avro_fast/src/ser/serializer/blocks.rs similarity index 100% rename from src/ser/serializer/blocks.rs rename to serde_avro_fast/src/ser/serializer/blocks.rs diff --git a/src/ser/serializer/decimal.rs b/serde_avro_fast/src/ser/serializer/decimal.rs similarity index 100% rename from src/ser/serializer/decimal.rs rename to serde_avro_fast/src/ser/serializer/decimal.rs diff --git a/src/ser/serializer/extract_for_duration.rs b/serde_avro_fast/src/ser/serializer/extract_for_duration.rs similarity index 100% rename from src/ser/serializer/extract_for_duration.rs rename to serde_avro_fast/src/ser/serializer/extract_for_duration.rs diff --git a/src/ser/serializer/mod.rs b/serde_avro_fast/src/ser/serializer/mod.rs similarity index 100% rename from src/ser/serializer/mod.rs rename to serde_avro_fast/src/ser/serializer/mod.rs diff --git a/src/ser/serializer/seq_or_tuple.rs b/serde_avro_fast/src/ser/serializer/seq_or_tuple.rs similarity index 100% rename from src/ser/serializer/seq_or_tuple.rs rename to serde_avro_fast/src/ser/serializer/seq_or_tuple.rs diff --git a/src/ser/serializer/struct_or_map.rs b/serde_avro_fast/src/ser/serializer/struct_or_map.rs similarity index 100% rename from src/ser/serializer/struct_or_map.rs rename to serde_avro_fast/src/ser/serializer/struct_or_map.rs diff --git a/src/single_object_encoding.rs b/serde_avro_fast/src/single_object_encoding.rs similarity index 100% rename from src/single_object_encoding.rs rename to serde_avro_fast/src/single_object_encoding.rs diff --git a/tests/duration.rs b/serde_avro_fast/tests/duration.rs similarity index 100% rename from tests/duration.rs rename to serde_avro_fast/tests/duration.rs diff --git a/tests/from_benches.rs b/serde_avro_fast/tests/from_benches.rs similarity index 100% rename from tests/from_benches.rs rename to serde_avro_fast/tests/from_benches.rs diff --git a/tests/max_depth_prevents_stack_overflow.rs b/serde_avro_fast/tests/max_depth_prevents_stack_overflow.rs similarity index 100% rename from tests/max_depth_prevents_stack_overflow.rs rename to serde_avro_fast/tests/max_depth_prevents_stack_overflow.rs diff --git a/tests/no_cyclic_debug_on_schema.rs b/serde_avro_fast/tests/no_cyclic_debug_on_schema.rs similarity index 100% rename from tests/no_cyclic_debug_on_schema.rs rename to serde_avro_fast/tests/no_cyclic_debug_on_schema.rs diff --git a/tests/object_container_file_encoding.rs b/serde_avro_fast/tests/object_container_file_encoding.rs similarity index 100% rename from tests/object_container_file_encoding.rs rename to serde_avro_fast/tests/object_container_file_encoding.rs diff --git a/tests/round_trips.rs b/serde_avro_fast/tests/round_trips.rs similarity index 100% rename from tests/round_trips.rs rename to serde_avro_fast/tests/round_trips.rs diff --git a/tests/schema.rs b/serde_avro_fast/tests/schema.rs similarity index 100% rename from tests/schema.rs rename to serde_avro_fast/tests/schema.rs diff --git a/tests/schema_construction.rs b/serde_avro_fast/tests/schema_construction.rs similarity index 100% rename from tests/schema_construction.rs rename to serde_avro_fast/tests/schema_construction.rs diff --git a/tests/single_object_encoding.rs b/serde_avro_fast/tests/single_object_encoding.rs similarity index 100% rename from tests/single_object_encoding.rs rename to serde_avro_fast/tests/single_object_encoding.rs diff --git a/tests/unions.rs b/serde_avro_fast/tests/unions.rs similarity index 100% rename from tests/unions.rs rename to serde_avro_fast/tests/unions.rs