This is an automated email from the ASF dual-hosted git repository.

Kriskras99 pushed a commit to branch feat/schema_parser_v2
in repository https://gitbox.apache.org/repos/asf/avro-rs.git

commit c10d80c4b86a94af5b07e1cd0c503e23ec1d3e02
Author: Kriskras99 <[email protected]>
AuthorDate: Mon Sep 7 20:42:13 2026 +0200

    wip
---
 Cargo.lock                     | 196 +++++++++++
 avro/Cargo.toml                |   5 +
 avro/benches/large_schema.avsc |   1 +
 avro/benches/schema_parsing.rs |  46 +++
 avro/src/error.rs              |  12 +
 avro/src/schema/mod.rs         |   6 +
 avro/src/schema/parser2.rs     | 745 +++++++++++++++++++++++++++++++++++++++++
 7 files changed, 1011 insertions(+)

diff --git a/Cargo.lock b/Cargo.lock
index 9f9d897..d593edc 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -8,6 +8,19 @@ version = "2.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
 
+[[package]]
+name = "ahash"
+version = "0.8.12"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
+dependencies = [
+ "cfg-if",
+ "getrandom 0.3.4",
+ "once_cell",
+ "version_check",
+ "zerocopy",
+]
+
 [[package]]
 name = "aho-corasick"
 version = "1.1.4"
@@ -74,6 +87,7 @@ dependencies = [
  "serde_json",
  "sha2",
  "snap",
+ "sonic-rs",
  "strum",
  "thiserror",
  "uuid",
@@ -183,6 +197,12 @@ version = "3.20.3"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
 
+[[package]]
+name = "bytes"
+version = "1.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
+
 [[package]]
 name = "bzip2"
 version = "0.6.1"
@@ -461,6 +481,18 @@ version = "2.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
 
+[[package]]
+name = "faststr"
+version = "0.2.34"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "1ca7d44d22004409a61c393afb3369c8f7bb74abcae49fe249ee01dcc3002113"
+dependencies = [
+ "bytes",
+ "rkyv",
+ "serde",
+ "simdutf8",
+]
+
 [[package]]
 name = "find-msvc-tools"
 version = "0.1.9"
@@ -750,6 +782,26 @@ dependencies = [
  "adler2",
 ]
 
+[[package]]
+name = "munge"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "5e17401f259eba956ca16491461b6e8f72913a0a114e39736ce404410f915a0c"
+dependencies = [
+ "munge_macro",
+]
+
+[[package]]
+name = "munge_macro"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "4568f25ccbd45ab5d5603dc34318c1ec56b117531781260002151b8530a9f931"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
 [[package]]
 name = "nu-ansi-term"
 version = "0.50.3"
@@ -929,6 +981,26 @@ dependencies = [
  "unarray",
 ]
 
+[[package]]
+name = "ptr_meta"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "743da816b98c921cdbe8628ef7381b76f25ecf4da599fc80aca90eae7ef70cc0"
+dependencies = [
+ "ptr_meta_derive",
+]
+
+[[package]]
+name = "ptr_meta_derive"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "1c8d9ca532f185d5d4db7a7c9d51420b452168ea1c2b913953281bd6fe1fcbd0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.4",
+]
+
 [[package]]
 name = "quad-rand"
 version = "0.2.3"
@@ -956,6 +1028,15 @@ version = "6.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
 
+[[package]]
+name = "rancor"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "9b534442d0fcdb55d66f373d9cac6d33b6293a2335bc2136dbd06ce0e87d2572"
+dependencies = [
+ "ptr_meta",
+]
+
 [[package]]
 name = "rand"
 version = "0.9.4"
@@ -1011,6 +1092,26 @@ dependencies = [
  "rand_core 0.9.5",
 ]
 
+[[package]]
+name = "ref-cast"
+version = "1.0.27"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3"
+dependencies = [
+ "ref-cast-impl",
+]
+
+[[package]]
+name = "ref-cast-impl"
+version = "1.0.27"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.4",
+]
+
 [[package]]
 name = "regex"
 version = "1.12.4"
@@ -1052,6 +1153,41 @@ version = "1.9.3"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2"
 
+[[package]]
+name = "rend"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "663ba70707f96e871406fe10d68128412e619b06d1d47cb91c3a4c6501176240"
+
+[[package]]
+name = "rkyv"
+version = "0.8.18"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "d9776093b7ca170454ab1406954f7b7d97a57c51dc6c0642957fb2ef25c2d399"
+dependencies = [
+ "bytes",
+ "hashbrown",
+ "indexmap",
+ "munge",
+ "ptr_meta",
+ "rancor",
+ "rend",
+ "rkyv_derive",
+ "tinyvec",
+ "uuid",
+]
+
+[[package]]
+name = "rkyv_derive"
+version = "0.8.18"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "1c25ef604ac7dd839d44d64648952ea23c97866f124ff671b0ed2cf3ad9bb06e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.4",
+]
+
 [[package]]
 name = "rstest"
 version = "0.26.1"
@@ -1187,6 +1323,12 @@ version = "2.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
 
+[[package]]
+name = "simdutf8"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
+
 [[package]]
 name = "slab"
 version = "0.4.12"
@@ -1199,6 +1341,45 @@ version = "1.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "199905e6153d6405f9728fe44daace35f8f837bbf830bb6e85fbd5828709a886"
 
+[[package]]
+name = "sonic-number"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "3775c3390edf958191f1ab1e8c5c188907feebd0f3ce1604cb621f72961dbf32"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "sonic-rs"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "d971cc77a245ccf1756dbd1a87c3e7f709c0191464096510d43eec056d0f2c4f"
+dependencies = [
+ "ahash",
+ "bumpalo",
+ "bytes",
+ "cfg-if",
+ "faststr",
+ "itoa",
+ "ref-cast",
+ "serde",
+ "simdutf8",
+ "sonic-number",
+ "sonic-simd",
+ "thiserror",
+ "zmij",
+]
+
+[[package]]
+name = "sonic-simd"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "f99e664ecd2d85a68c87e3c7a3cfe691f647ea9e835de984aba4d54a41f817d4"
+dependencies = [
+ "cfg-if",
+]
+
 [[package]]
 name = "static_assertions"
 version = "1.1.0"
@@ -1299,6 +1480,21 @@ dependencies = [
  "serde_json",
 ]
 
+[[package]]
+name = "tinyvec"
+version = "1.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "4cf0ded5c4e56918d8f8a339e1bb67d038d3bc6d144ac407904015ba2e4cde9b"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
 [[package]]
 name = "toml"
 version = "1.1.2+spec-1.1.0"
diff --git a/avro/Cargo.toml b/avro/Cargo.toml
index 7817d01..be061d5 100644
--- a/avro/Cargo.toml
+++ b/avro/Cargo.toml
@@ -47,6 +47,10 @@ name = "serde"
 harness = false
 name = "single"
 
+[[bench]]
+harness = false
+name = "schema_parsing"
+
 [dependencies]
 apache-avro-derive = { default-features = false, version = "0.23.0", path = 
"../avro_derive", optional = true }
 bigdecimal = { default-features = false, version = "0.4.10", features = 
["std", "serde"] }
@@ -68,6 +72,7 @@ thiserror = { default-features = false, version = "2.0.18" }
 uuid = { workspace = true }
 liblzma = { default-features = false, version = "0.4.6", optional = true }
 zstd = { default-features = false, version = "0.13.3", optional = true }
+sonic-rs = "0.5.8"
 
 
 [target.'cfg(target_arch = "wasm32")'.dependencies]
diff --git a/avro/benches/large_schema.avsc b/avro/benches/large_schema.avsc
new file mode 100644
index 0000000..c5819b6
--- /dev/null
+++ b/avro/benches/large_schema.avsc
@@ -0,0 +1 @@
+["null",{"type":"record","name":"El","namespace":"foo.e.f.g.h.ac","fields":[{"name":"ref1","type":{"type":"record","name":"J","namespace":"foo.a.b.c.d","fields":[{"name":"attr11","type":"long","declared":"true"},{"name":"attr12","type":"long","declared":"true"}]}},{"name":"attr13","type":["null","string"],"default":null},{"name":"attr1","type":{"type":"long","date":"true"}},{"name":"attr2","type":["null","string"],"default":null},{"name":"attr3","type":["null","string"],"default":null},{
 [...]
\ No newline at end of file
diff --git a/avro/benches/schema_parsing.rs b/avro/benches/schema_parsing.rs
new file mode 100644
index 0000000..c2ac53f
--- /dev/null
+++ b/avro/benches/schema_parsing.rs
@@ -0,0 +1,46 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use apache_avro::schema::Schema;
+use criterion::{Criterion, criterion_group, criterion_main};
+use std::time::Duration;
+
+fn read_large_record() -> String {
+    std::fs::read_to_string("benches/large_schema.avsc").unwrap()
+}
+
+fn bench_large_record(c: &mut Criterion) {
+    let string = read_large_record();
+    c.bench_function("large record", |b| {
+        b.iter(|| Schema::parse_str(&string));
+    });
+}
+fn bench_large_record2(c: &mut Criterion) {
+    let string = read_large_record();
+    c.bench_function("large record", |b| {
+        b.iter(|| Schema::parse_str2(&string));
+    });
+}
+
+criterion_group!(
+    name = benches;
+    config = 
Criterion::default().sample_size(200).measurement_time(Duration::from_secs(10));
+    targets =
+        bench_large_record,
+        bench_large_record2,
+);
+criterion_main!(benches);
diff --git a/avro/src/error.rs b/avro/src/error.rs
index 58f9145..f20eba2 100644
--- a/avro/src/error.rs
+++ b/avro/src/error.rs
@@ -428,9 +428,15 @@ pub enum Details {
     #[error("logicalType must be a string, but is {0:?}")]
     GetLogicalTypeFieldType(serde_json::Value),
 
+    #[error("logicalType must be a string, but is {0:?}")]
+    GetLogicalTypeFieldType2(sonic_rs::JsonType),
+
     #[error("Unknown complex type: {0}")]
     GetComplexType(serde_json::Value),
 
+    #[error("Unknown complex type: {0:?}")]
+    GetComplexType2(sonic_rs::JsonType),
+
     #[error("No `type` in complex type")]
     GetComplexTypeField,
 
@@ -729,6 +735,12 @@ pub enum Details {
         position: usize,
         total_elements: usize,
     },
+
+    #[error("{0}")]
+    Sonic(sonic_rs::Error),
+
+    #[error("{0}")]
+    Custom(String),
 }
 
 #[derive(thiserror::Error, PartialEq)]
diff --git a/avro/src/schema/mod.rs b/avro/src/schema/mod.rs
index b503f12..ca5d471 100644
--- a/avro/src/schema/mod.rs
+++ b/avro/src/schema/mod.rs
@@ -20,10 +20,12 @@
 mod builders;
 mod name;
 mod parser;
+mod parser2;
 mod record;
 mod resolve;
 mod union;
 
+use crate::schema::parser2::Parser2;
 pub(crate) use crate::schema::resolve::{
     ResolvedOwnedSchema, resolve_names, resolve_names_with_schemata,
 };
@@ -530,6 +532,10 @@ impl Schema {
         parser.parse_str(input)
     }
 
+    pub fn parse_str2(input: &str) -> Result<Schema, Error> {
+        Parser2::parse_str(input)
+    }
+
     /// Create an array of `Schema`'s from a list of named JSON Avro schemas 
(Record, Enum, and
     /// Fixed).
     ///
diff --git a/avro/src/schema/parser2.rs b/avro/src/schema/parser2.rs
new file mode 100644
index 0000000..38c6fd8
--- /dev/null
+++ b/avro/src/schema/parser2.rs
@@ -0,0 +1,745 @@
+use crate::error::Details;
+use crate::schema::{
+    Alias, ArraySchema, DecimalSchema, EnumSchema, FixedSchema, 
InnerDecimalSchema, MapSchema,
+    Name, NamespaceRef, RecordField, RecordSchema, UnionSchema, UuidSchema,
+};
+use crate::{AvroResult, Error, Schema};
+use log::warn;
+use sonic_rs::{ArrayJsonIter, JsonType, JsonValueTrait, LazyValue};
+use std::borrow::Cow;
+use std::collections::{BTreeMap, HashSet};
+use std::ops::Deref;
+use std::sync::LazyLock;
+
+static BYTES_UUID_WARNING: LazyLock<()> =
+    LazyLock::new(|| warn!("`bytes` with a logical type of `uuid` is 
deprecated"));
+
+#[derive(Default)]
+pub struct Parser2 {
+    known_names: HashSet<Name>,
+    unknown_references: HashSet<Name>,
+}
+
+impl Parser2 {
+    /// Create a `Schema` from a string representing a JSON Avro schema.
+    pub(super) fn parse_str(input: &str) -> AvroResult<Schema> {
+        let lazy = sonic_rs::get_from_str::<[usize; 0]>(input, 
[]).map_err(Details::Sonic)?;
+        let mut parser = Self::default();
+        let schema = parser.parse(lazy, None)?;
+        if parser.has_unknown_references() {
+            Err(Details::SchemaResolutionError(
+                parser.unknown_references.into_iter().next().unwrap(),
+            )
+            .into())
+        } else {
+            Ok(schema)
+        }
+    }
+
+    fn has_unknown_references(&mut self) -> bool {
+        self.unknown_references
+            .retain(|r| !self.known_names.contains(r));
+        !self.unknown_references.is_empty()
+    }
+
+    fn add_reference(&mut self, name: &Name) {
+        if !self.known_names.contains(name) {
+            self.unknown_references.insert(name.clone());
+        }
+    }
+
+    fn parse(&mut self, value: LazyValue, enclosing_namespace: NamespaceRef) 
-> AvroResult<Schema> {
+        match value.get_type() {
+            JsonType::String => {
+                self.parse_string(value.as_str().expect("unreachable"), 
enclosing_namespace)
+            }
+            JsonType::Object => self.parse_complex(value, enclosing_namespace),
+            JsonType::Array => self.parse_union(
+                value.into_array_iter().expect("unreachable"),
+                enclosing_namespace,
+            ),
+            _ => Err(Details::ParseSchemaFromValidJson.into()),
+        }
+    }
+
+    fn parse_string(
+        &mut self,
+        value: &str,
+        enclosing_namespace: NamespaceRef,
+    ) -> AvroResult<Schema> {
+        match value {
+            "null" => Ok(Schema::Null),
+            "boolean" => Ok(Schema::Boolean),
+            "int" => Ok(Schema::Int),
+            "long" => Ok(Schema::Long),
+            "double" => Ok(Schema::Double),
+            "float" => Ok(Schema::Float),
+            "bytes" => Ok(Schema::Bytes),
+            "string" => Ok(Schema::String),
+            _ => {
+                let name = Name::new_with_enclosing_namespace(value, 
enclosing_namespace)?;
+                self.unknown_references.insert(name.clone());
+                Ok(Schema::Ref { name })
+            }
+        }
+    }
+
+    fn parse_union(
+        &mut self,
+        array: ArrayJsonIter,
+        enclosing_namespace: NamespaceRef,
+    ) -> AvroResult<Schema> {
+        let mut builder = UnionSchema::builder();
+        for value in array {
+            let value = value.map_err(Details::Sonic)?;
+            let schema = self.parse(value, enclosing_namespace)?;
+            builder.variant(schema)?;
+        }
+        Ok(Schema::Union(builder.build()))
+    }
+
+    fn parse_complex(
+        &mut self,
+        value: LazyValue,
+        enclosing_namespace: NamespaceRef,
+    ) -> AvroResult<Schema> {
+        assert!(value.is_object());
+
+        let ty_value = value
+            .pointer(["type"])
+            .ok_or(Details::GetComplexTypeField)?;
+        let ty = ty_value
+            .as_str()
+            .ok_or_else(|| Details::GetComplexType2(ty_value.get_type()))?;
+        let logical_ty_value = value.pointer(["logicalType"]);
+        let logical_ty = if let Some(val) = &logical_ty_value {
+            Some(
+                val.as_str()
+                    .ok_or_else(|| 
Details::GetLogicalTypeFieldType2(val.get_type()))?,
+            )
+        } else {
+            None
+        };
+
+        // This clone is practically a copy
+        let object_iter = value
+            .clone()
+            .into_object_iter()
+            .expect("This is an object")
+            .filter_map(|r| {
+                if let Ok((key, _)) = &r
+                    && key == "type"
+                {
+                    None
+                } else {
+                    Some(r.map_err(|e| Error::new(Details::Sonic(e))))
+                }
+            });
+        match (ty, logical_ty) {
+            ("null", _) => Ok(Schema::Null),
+            ("boolean", _) => Ok(Schema::Boolean),
+            ("int", Some("date")) => Ok(Schema::Date),
+            ("int", Some("time-millis")) => Ok(Schema::TimeMillis),
+            ("int", _) => Ok(Schema::Int),
+            ("long", Some("time-micros")) => Ok(Schema::TimeMicros),
+            ("long", Some("timestamp-millis")) => Ok(Schema::TimestampMillis),
+            ("long", Some("timestamp-micros")) => Ok(Schema::TimestampMicros),
+            ("long", Some("timestamp-nanos")) => Ok(Schema::TimestampNanos),
+            ("long", Some("local-timestamp-millis")) => 
Ok(Schema::LocalTimestampMillis),
+            ("long", Some("local-timestamp-micros")) => 
Ok(Schema::LocalTimestampMicros),
+            ("long", Some("local-timestamp-nanos")) => 
Ok(Schema::LocalTimestampNanos),
+            ("long", _) => Ok(Schema::Long),
+            ("float", _) => Ok(Schema::Float),
+            ("double", _) => Ok(Schema::Double),
+            ("bytes", Some("decimal")) => {
+                self.parse_decimal(object_iter, "bytes", enclosing_namespace)
+            }
+            ("bytes", Some("big-decimal")) => Ok(Schema::BigDecimal),
+            ("bytes", Some("uuid")) => {
+                let _ = BYTES_UUID_WARNING.deref();
+                Ok(Schema::Uuid(UuidSchema::Bytes))
+            }
+            ("bytes", _) => Ok(Schema::Bytes),
+            ("string", Some("uuid")) => Ok(Schema::Uuid(UuidSchema::String)),
+            ("string", _) => Ok(Schema::String),
+            ("record", _) => self.parse_record(object_iter, 
enclosing_namespace),
+            ("enum", _) => self.parse_enum(object_iter, enclosing_namespace),
+            ("array", _) => self.parse_array(object_iter, enclosing_namespace),
+            ("maps", _) => self.parse_map(object_iter, enclosing_namespace),
+            ("fixed", Some("decimal")) => {
+                self.parse_decimal(object_iter, "fixed", enclosing_namespace)
+            }
+            ("fixed", Some("uuid")) => self.parse_uuid_fixed(object_iter, 
enclosing_namespace),
+            ("fixed", Some("duration")) => self.parse_duration(object_iter, 
enclosing_namespace),
+            ("fixed", _) => self.parse_fixed(object_iter, enclosing_namespace),
+            (_, None) => {
+                let name = Name::new_with_enclosing_namespace(ty, 
enclosing_namespace)?;
+                self.add_reference(&name);
+                Ok(Schema::Ref { name })
+            }
+            (_, Some(lt)) => {
+                panic!(
+                    "References cannot be combined with logical types, 
reference: {ty}, logical type: {lt}"
+                );
+            }
+        }
+    }
+
+    fn parse_decimal<'de>(
+        &mut self,
+        object: impl Iterator<Item = AvroResult<(Cow<'de, str>, 
LazyValue<'de>)>>,
+        ty: &str,
+        enclosing_namespace: NamespaceRef,
+    ) -> AvroResult<Schema> {
+        let mut precision = None;
+        let mut scale = None;
+
+        let extracted =
+            object.filter_map(|r| match r.as_ref().map(|(k, v)| (k.as_ref(), 
v)) {
+                Ok(("precision", value)) if value.is_u64() => {
+                    let value = value.as_u64().expect("Is a u64");
+                    match usize::try_from(value) {
+                        Ok(0) => {
+                            Some(Err(Details::Custom("The `precision` for 
decimal cannot be 0".into()).into()))
+                        }
+                        Ok(value) => {
+                            precision = Some(value);
+                            None
+                        }
+                        Err(err) => Some(Err(Details::ConvertU64ToUsize(err, 
value).into()))
+                    }
+                }
+                Ok(("precision", value)) if value.is_i64() => 
Some(Err(Details::Custom("Expected a non-zero positive integer for decimal 
`precision` got a negative number".into()).into())),
+                Ok(("precision", value)) if value.is_f64() => 
Some(Err(Details::Custom("Expected a non-zero positive integer for decimal 
`precision` got a floating point number".into()).into())),
+                Ok(("precision", value)) => 
Some(Err(Details::Custom(format!("Expected a non-zero positive integer for 
decimal `precision` got {:?}", value.get_type())).into())),
+                Ok(("scale", value)) if value.is_u64() => {
+                    let value = value.as_u64().expect("Is a u64");
+                    match usize::try_from(value) {
+                        Ok(value) => {
+                            scale = Some(value);
+                            None
+                        }
+                        Err(err) => Some(Err(Details::ConvertU64ToUsize(err, 
value).into()))
+                    }
+                }
+                Ok(("scale", value)) if value.is_i64() => 
Some(Err(Details::Custom("Expected a positive integer for decimal `scale` got a 
negative number".into()).into())),
+                Ok(("scale", value)) if value.is_f64() => 
Some(Err(Details::Custom("Expected a positive integer for decimal `scale` got a 
floating point number".into()).into())),
+                Ok(("scale", value)) => 
Some(Err(Details::Custom(format!("Expected a positive integer for decimal 
`scale` got {:?}", value.get_type())).into())),
+                _ => Some(r),
+            });
+
+        match ty {
+            "bytes" => {
+                for result in extracted {
+                    result?;
+                }
+                let precision = precision
+                    .ok_or_else(|| Details::Custom("Missing `precision` for 
decimal".into()))?;
+                let scale = scale.unwrap_or(0);
+                if scale > precision {
+                    return Err(Details::DecimalPrecisionLessThanScale { scale, 
precision }.into());
+                }
+
+                Ok(Schema::Decimal(DecimalSchema {
+                    precision,
+                    scale,
+                    inner: InnerDecimalSchema::Bytes,
+                }))
+            }
+            "fixed" => {
+                let fixed = self.parse_fixed_bare(extracted, 
enclosing_namespace)?;
+                let precision = precision
+                    .ok_or_else(|| Details::Custom("Missing `precision` for 
decimal".into()))?;
+                let scale = scale.unwrap_or(0);
+                if scale > precision {
+                    return Err(Details::DecimalPrecisionLessThanScale { scale, 
precision }.into());
+                }
+
+                let max_precision = (2usize.pow((8 * (fixed.size - 1)) as u32) 
- 1).ilog10();
+                if (max_precision as usize) < precision {
+                    return Err(Details::Custom(format!("Maximum precision for 
Fixed({}) is {max_precision} but precision is {precision}", 
fixed.size)).into());
+                }
+
+                Ok(Schema::Decimal(DecimalSchema {
+                    precision,
+                    scale,
+                    inner: InnerDecimalSchema::Fixed(fixed),
+                }))
+            }
+            _ => unreachable!(),
+        }
+    }
+
+    fn parse_record<'de>(
+        &mut self,
+        object: impl Iterator<Item = AvroResult<(Cow<'de, str>, 
LazyValue<'de>)>>,
+        enclosing_namespace: NamespaceRef,
+    ) -> AvroResult<Schema> {
+        let mut name = None;
+        let mut namespace = None;
+        let mut aliases = None;
+        let mut doc = None;
+        let mut fields = None;
+        let mut attributes = BTreeMap::new();
+
+        for result in object {
+            let (key, value) = result?;
+            // TODO: panics to actual errors
+            match (key.as_ref(), value.get_type()) {
+                ("name", _) if name.is_some() => panic!("Duplicate `name` 
field"),
+                ("name", JsonType::String) => {
+                    name = Some(value.as_str().expect("Is a 
string").to_string())
+                }
+                ("name", ty) => panic!("`name` must be a string not a {ty:?}"),
+                ("namespace", _) if namespace.is_some() => panic!("Duplicate 
`namespace` field"),
+                ("namespace", JsonType::String) => {
+                    namespace = Some(value.as_str().expect("Is a 
string").to_string())
+                }
+                ("namespace", ty) => panic!("`namespace` must be a string not 
a {ty:?}"),
+                ("aliases", _) if aliases.is_none() => panic!("Duplicate 
`aliases` field"),
+                ("aliases", JsonType::Array) => {
+                    aliases = Some(
+                        self.parse_aliases(value.into_array_iter().unwrap(), 
enclosing_namespace)?,
+                    )
+                }
+                ("aliases", ty) => panic!("`aliases` must be a string not a 
{ty:?}"),
+                ("doc", _) if doc.is_some() => panic!("Duplicate `doc` field"),
+                ("doc", JsonType::String) => {
+                    doc = Some(value.as_str().expect("Is a 
string").to_string())
+                }
+                ("doc", ty) => panic!("`doc` must be a string not a {ty:?}"),
+                ("fields", _) if fields.is_some() => panic!("Duplicate 
`fields` field"),
+                ("fields", JsonType::Array) => {
+                    fields = Some(self.parse_fields(
+                        value.into_array_iter().expect("Is an array"),
+                        enclosing_namespace,
+                    ))
+                }
+                ("fields", ty) => panic!("`fields` must be an positive integer 
not a {ty:?}"),
+                (_, _) => {
+                    if attributes.contains_key(key.as_ref()) {
+                        panic!("Duplicate custom attribute found for {key}");
+                    }
+                    attributes.insert(key.into_owned(), 
sonic_value_to_serde_value(value));
+                }
+            }
+        }
+
+        let Some(name) = name else {
+            return Err(Details::GetNameField.into());
+        };
+        let Some(fields) = fields else {
+            panic!("`fields` is missing on the struct")
+        };
+        let name =
+            Name::new_with_enclosing_namespace(name, 
namespace.as_deref().or(enclosing_namespace))?;
+
+        Ok(Schema::Record(
+            RecordSchema::builder()
+                .name(name)
+                .aliases(aliases)
+                .doc(doc)
+                .fields(fields?)
+                .attributes(attributes)
+                .build(),
+        ))
+    }
+
+    fn parse_enum<'de>(
+        &mut self,
+        object: impl Iterator<Item = AvroResult<(Cow<'de, str>, 
LazyValue<'de>)>>,
+        enclosing_namespace: NamespaceRef,
+    ) -> AvroResult<Schema> {
+        let mut name = None;
+        let mut namespace = None;
+        let mut aliases = None;
+        let mut doc = None;
+        let mut symbols = None;
+        let mut default = None;
+        let mut attributes = BTreeMap::new();
+
+        for result in object {
+            let (key, value) = result?;
+            // TODO: panics to actual errors
+            match (key.as_ref(), value.get_type()) {
+                ("name", _) if name.is_some() => panic!("Duplicate `name` 
field"),
+                ("name", JsonType::String) => {
+                    name = Some(value.as_str().expect("Is a 
string").to_string())
+                }
+                ("name", ty) => panic!("`name` must be a string not a {ty:?}"),
+                ("namespace", _) if namespace.is_some() => panic!("Duplicate 
`namespace` field"),
+                ("namespace", JsonType::String) => {
+                    namespace = Some(value.as_str().expect("Is a 
string").to_string())
+                }
+                ("namespace", ty) => panic!("`namespace` must be a string not 
a {ty:?}"),
+                ("aliases", _) if aliases.is_none() => panic!("Duplicate 
`aliases` field"),
+                ("aliases", JsonType::Array) => {
+                    aliases = Some(
+                        self.parse_aliases(value.into_array_iter().unwrap(), 
enclosing_namespace)?,
+                    )
+                }
+                ("aliases", ty) => panic!("`aliases` must be a string not a 
{ty:?}"),
+                ("doc", _) if doc.is_some() => panic!("Duplicate `doc` field"),
+                ("doc", JsonType::String) => {
+                    doc = Some(value.as_str().expect("Is a 
string").to_string())
+                }
+                ("doc", ty) => panic!("`doc` must be a string not a {ty:?}"),
+                ("symbols", _) if symbols.is_some() => panic!("Duplicate 
`symbols` field"),
+                ("symbols", JsonType::Array) => {
+                    symbols = Some(
+                        value.into_array_iter().unwrap().map(|r| {
+                            r.map_err(Details::Sonic).and_then(|v| 
v.as_str().map(str::to_string).ok_or_else(|| Details::Custom(format!("`symbols` 
must be an array of string, but got an {:?} in the array", v.get_type()))))
+                        }).collect::<Result<Vec<_>, _>>()?,
+                    )
+                }
+                ("default", _) if default.is_some() => panic!("Duplicate 
`default` field"),
+                ("default", JsonType::String)  => default = 
Some(value.as_str().expect("Is a string").to_string()),
+                ("default", ty) => panic!("`default` must be a string not a 
{ty:?}"),
+                (_, _) => {
+                    if attributes.contains_key(key.as_ref()) {
+                        panic!("Duplicate custom attribute found for {key}");
+                    }
+                    attributes.insert(key.into_owned(), 
sonic_value_to_serde_value(value));
+                }
+            }
+        }
+
+        let Some(name) = name else {
+            return Err(Details::GetNameField.into());
+        };
+        let Some(symbols) = symbols else {
+            panic!("`symbols` is missing on the struct")
+        };
+        let name =
+            Name::new_with_enclosing_namespace(name, 
namespace.as_deref().or(enclosing_namespace))?;
+        if let Some(default) = &default
+            && !symbols.contains(default)
+        {
+            panic!("`default` of {default} does not exist in `symbols`")
+        }
+
+        Ok(Schema::Enum(EnumSchema {
+            name,
+            aliases,
+            doc,
+            symbols,
+            default,
+            attributes,
+        }))
+    }
+
+    fn parse_array<'de>(
+        &mut self,
+        object: impl Iterator<Item = AvroResult<(Cow<'de, str>, 
LazyValue<'de>)>>,
+        enclosing_namespace: NamespaceRef,
+    ) -> AvroResult<Schema> {
+        let mut items = None;
+        let mut attributes = BTreeMap::new();
+
+        for result in object {
+            let (key, value) = result?;
+            match key.as_ref() {
+                "items" if items.is_some() => panic!("Duplicate `items` 
field"),
+                "items" => items = Some(self.parse(value, 
enclosing_namespace)?),
+                _ => {
+                    if attributes.contains_key(key.as_ref()) {
+                        panic!("Duplicate custom attribute found for {key}");
+                    }
+                    attributes.insert(key.into_owned(), 
sonic_value_to_serde_value(value));
+                }
+            }
+        }
+
+        let Some(items) = items else {
+            panic!("Mising `items` for array")
+        };
+
+        Ok(Schema::Array(ArraySchema {
+            items: Box::new(items),
+            attributes,
+        }))
+    }
+
+    fn parse_map<'de>(
+        &mut self,
+        object: impl Iterator<Item = AvroResult<(Cow<'de, str>, 
LazyValue<'de>)>>,
+        enclosing_namespace: NamespaceRef,
+    ) -> AvroResult<Schema> {
+        let mut ty = None;
+        let mut attributes = BTreeMap::new();
+
+        for result in object {
+            let (key, value) = result?;
+            match key.as_ref() {
+                "type" if ty.is_some() => panic!("Duplicate `type` field"),
+                "type" => ty = Some(self.parse(value, enclosing_namespace)?),
+                _ => {
+                    if attributes.contains_key(key.as_ref()) {
+                        panic!("Duplicate custom attribute found for {key}");
+                    }
+                    attributes.insert(key.into_owned(), 
sonic_value_to_serde_value(value));
+                }
+            }
+        }
+
+        let Some(ty) = ty else {
+            panic!("Mising `items` for array")
+        };
+
+        Ok(Schema::Map(MapSchema {
+            types: Box::new(ty),
+            attributes,
+        }))
+    }
+
+    fn parse_uuid_fixed<'de>(
+        &mut self,
+        object: impl Iterator<Item = AvroResult<(Cow<'de, str>, 
LazyValue<'de>)>>,
+        enclosing_namespace: NamespaceRef,
+    ) -> AvroResult<Schema> {
+        let filtered = object.filter(|r| {
+            if let Ok((key, _)) = r
+                && key == "logicalType"
+            {
+                false
+            } else {
+                true
+            }
+        });
+        let fixed = self.parse_fixed_bare(filtered, enclosing_namespace)?;
+        if fixed.size != 16 {
+            // TODO: Wrong error?
+            Err(Details::ConvertFixedToUuid(fixed.size).into())
+        } else {
+            Ok(Schema::Uuid(UuidSchema::Fixed(fixed)))
+        }
+    }
+
+    fn parse_duration<'de>(
+        &mut self,
+        object: impl Iterator<Item = AvroResult<(Cow<'de, str>, 
LazyValue<'de>)>>,
+        enclosing_namespace: NamespaceRef,
+    ) -> AvroResult<Schema> {
+        let filtered = object.filter(|r| {
+            if let Ok((key, _)) = r
+                && key == "logicalType"
+            {
+                false
+            } else {
+                true
+            }
+        });
+        Ok(Schema::Duration(
+            self.parse_fixed_bare(filtered, enclosing_namespace)?,
+        ))
+    }
+
+    fn parse_fixed<'de>(
+        &mut self,
+        object: impl Iterator<Item = AvroResult<(Cow<'de, str>, 
LazyValue<'de>)>>,
+        enclosing_namespace: NamespaceRef,
+    ) -> AvroResult<Schema> {
+        self.parse_fixed_bare(object, enclosing_namespace)
+            .map(|f| Schema::Uuid(UuidSchema::Fixed(f)))
+    }
+
+    fn parse_fixed_bare<'de>(
+        &mut self,
+        object: impl Iterator<Item = AvroResult<(Cow<'de, str>, 
LazyValue<'de>)>>,
+        enclosing_namespace: NamespaceRef,
+    ) -> AvroResult<FixedSchema> {
+        let mut name = None;
+        let mut namespace = None;
+        let mut aliases = None;
+        let mut doc = None;
+        let mut size = None;
+        let mut attributes = BTreeMap::new();
+
+        for result in object {
+            let (key, value) = result?;
+            // TODO: panics to actual errors
+            match (key.as_ref(), value.get_type()) {
+                ("name", _) if name.is_some() => panic!("Duplicate `name` 
field"),
+                ("name", JsonType::String) => {
+                    name = Some(value.as_str().expect("Is a 
string").to_string())
+                }
+                ("name", ty) => panic!("`name` must be a string not a {ty:?}"),
+                ("namespace", _) if namespace.is_some() => panic!("Duplicate 
`namespace` field"),
+                ("namespace", JsonType::String) => {
+                    namespace = Some(value.as_str().expect("Is a 
string").to_string())
+                }
+                ("namespace", ty) => panic!("`namespace` must be a string not 
a {ty:?}"),
+                ("aliases", _) if aliases.is_none() => panic!("Duplicate 
`aliases` field"),
+                ("aliases", JsonType::Array) => {
+                    aliases = Some(
+                        self.parse_aliases(value.into_array_iter().unwrap(), 
enclosing_namespace)?,
+                    )
+                }
+                ("aliases", ty) => panic!("`aliases` must be a string not a 
{ty:?}"),
+                ("doc", _) if doc.is_some() => panic!("Duplicate `doc` field"),
+                ("doc", JsonType::String) => {
+                    doc = Some(value.as_str().expect("Is a 
string").to_string())
+                }
+                ("doc", ty) => panic!("`doc` must be a string not a {ty:?}"),
+                ("size", _) if size.is_some() => panic!("Duplicate `size` 
field"),
+                ("size", _) if value.is_u64() => size = 
Some(value.as_u64().expect("Is a u64")),
+                ("size", _) if value.is_i64() => panic!("`size` must be an 
positive integer"),
+                ("size", _) if value.is_f64() => {
+                    panic!("`size` must be an positive integer not a float")
+                }
+                ("size", ty) => panic!("`size` must be an positive integer not 
a {ty:?}"),
+                (_, _) => {
+                    if attributes.contains_key(key.as_ref()) {
+                        panic!("Duplicate custom attribute found for {key}");
+                    }
+                    attributes.insert(key.into_owned(), 
sonic_value_to_serde_value(value));
+                }
+            }
+        }
+
+        let Some(name) = name else {
+            return Err(Details::GetNameField.into());
+        };
+        let Some(size) = size else {
+            return Err(Details::GetFixedSizeField.into());
+        };
+        let name =
+            Name::new_with_enclosing_namespace(name, 
namespace.as_deref().or(enclosing_namespace))?;
+
+        Ok(FixedSchema {
+            name,
+            aliases: None,
+            doc,
+            size: usize::try_from(size).map_err(|e| 
Details::ConvertU64ToUsize(e, size))?,
+            attributes,
+        })
+    }
+
+    fn parse_aliases(
+        &mut self,
+        array: ArrayJsonIter,
+        enclosing_namespace: NamespaceRef,
+    ) -> AvroResult<Vec<Alias>> {
+        // This function is only entered if `aliases` exist, and the most 
common length of `aliases`
+        // is probably 1.
+        let mut aliases = Vec::with_capacity(1);
+
+        for result in array {
+            let value = result.map_err(Details::Sonic)?;
+            if let Some(str) = value.as_str() {
+                aliases.push(Alias::new_with_enclosing_namespace(
+                    str,
+                    enclosing_namespace,
+                )?);
+            } else {
+                panic!(
+                    "aliases must be an array of strings, found an {:?} in the 
array",
+                    value.get_type()
+                )
+            }
+        }
+
+        aliases.shrink_to_fit();
+        Ok(aliases)
+    }
+
+    fn parse_fields(
+        &mut self,
+        array: ArrayJsonIter,
+        enclosing_namespace: NamespaceRef,
+    ) -> AvroResult<Vec<RecordField>> {
+        let mut fields = Vec::new();
+        for result in array {
+            let item = result.map_err(Details::Sonic)?;
+            let ty = item.get_type();
+            if let Some(object) = item.into_object_iter() {
+                let field = self.parse_field(
+                    object.map(|r| r.map_err(|e| 
Error::new(Details::Sonic(e)))),
+                    enclosing_namespace,
+                )?;
+                fields.push(field);
+            } else {
+                panic!("Expected array of objects for `fields` but found 
{ty:?} in the array")
+            }
+        }
+        Ok(fields)
+    }
+
+    fn parse_field<'de>(
+        &mut self,
+        object: impl Iterator<Item = AvroResult<(Cow<'de, str>, 
LazyValue<'de>)>>,
+        enclosing_namespace: NamespaceRef,
+    ) -> AvroResult<RecordField> {
+        let mut name = None;
+        let mut doc = None;
+        let mut ty = None;
+        let mut order_found = false;
+        let mut aliases = None;
+        let mut default = None;
+        let mut attributes = BTreeMap::new();
+
+        for result in object {
+            let (key, value) = result?;
+            // TODO: panics to actual errors
+            match (key.as_ref(), value.get_type()) {
+                ("name", _) if name.is_some() => panic!("Duplicate `name` 
field"),
+                ("name", JsonType::String) => {
+                    name = Some(value.as_str().expect("Is a 
string").to_string())
+                }
+                ("name", ty) => panic!("`name` must be a string not a {ty:?}"),
+                ("doc", _) if doc.is_some() => panic!("Duplicate `doc` field"),
+                ("doc", JsonType::String) => {
+                    doc = Some(value.as_str().expect("Is a 
string").to_string())
+                }
+                ("doc", ty) => panic!("`doc` must be a string not a {ty:?}"),
+                ("type", _) if ty.is_some() => panic!("Duplicate `type` 
field"),
+                ("type", _) => ty = Some(self.parse(value, 
enclosing_namespace)),
+                ("order", _) if order_found => panic!("Duplicate `order` 
field"),
+                ("order", JsonType::String) => match value.as_str().expect("Is 
a string") {
+                    "ascending" | "descending" | "ignore" => order_found = 
true,
+                    v => panic!("Unexpected value for `order`: {v}"),
+                }
+                ("order", ty) => panic!("`order` must be a string not a 
{ty:?}"),
+                ("aliases", _) if aliases.is_none() => panic!("Duplicate 
`aliases` field"),
+                ("aliases", JsonType::Array) => {
+                    aliases = Some(
+                        value.into_array_iter().unwrap().map(|r| {
+                            r.map_err(Details::Sonic).and_then(|v| 
v.as_str().map(str::to_string).ok_or_else(|| Details::Custom(format!("`aliases` 
must be an array of string, but got an {:?} in the array", v.get_type()))))
+                        }).collect::<Result<Vec<_>, _>>()?,
+                    )
+                }
+                ("aliases", ty) => panic!("`aliases` must be a string not a 
{ty:?}"),
+                ("default", _) if default.is_some() => panic!("Duplicate 
`default` field"),
+                ("default", _) => {
+                    default = 
Some(serde_json::from_str(value.as_raw_str()).expect("This is valid JSON"))
+                }
+                (_, _) => {
+                    if attributes.contains_key(key.as_ref()) {
+                        panic!("Duplicate custom attribute found for {key}");
+                    }
+                    attributes.insert(key.into_owned(), 
sonic_value_to_serde_value(value));
+                }
+            }
+        }
+
+        let Some(name) = name else {
+            panic!("`name` is missing for field")
+        };
+        let Some(ty) = ty else {
+            panic!("`ty` is missing for field")
+        };
+
+        Ok(RecordField::builder()
+            .name(name)
+            .doc(doc)
+            .maybe_aliases(aliases)
+            .maybe_default(default)
+            .schema(ty?)
+            .custom_attributes(attributes)
+            .build())
+    }
+}
+
+fn sonic_value_to_serde_value(value: LazyValue) -> serde_json::Value {
+    serde_json::from_str(value.as_raw_str()).expect("This should parse")
+}

Reply via email to