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

mgrigorov pushed a commit to branch branch-1.11
in repository https://gitbox.apache.org/repos/asf/avro.git


The following commit(s) were added to refs/heads/branch-1.11 by this push:
     new c2cc66a  AVRO-3405: Add API to write/read user metadata in .avro file 
(#1551)
c2cc66a is described below

commit c2cc66aa84476139bd8629dda59e55dfac2f58fc
Author: Martin Grigorov <[email protected]>
AuthorDate: Thu Feb 17 22:37:31 2022 +0200

    AVRO-3405: Add API to write/read user metadata in .avro file (#1551)
    
    * AVRO-3405 Extract duplicate code in a method
    
    Signed-off-by: Martin Tzvetanov Grigorov <[email protected]>
    
    * AVRO-3405 Add API to read/write user metadata in .avro files
    
    Signed-off-by: Martin Tzvetanov Grigorov <[email protected]>
    
    * AVRO-3405 Use 'user_metadata' consistently in the API
    
    Simplify big method by splitting it into several ones.
    
    Signed-off-by: Martin Tzvetanov Grigorov <[email protected]>
    
    * AVRO-3405: Inline few trivial getters
    
    Signed-off-by: Martin Tzvetanov Grigorov <[email protected]>
    
    * AVRO-3405: Store user metadata as String->Bytes tuple
    
    Fixes issues 1) and 2) from feedback at 
https://github.com/apache/avro/pull/1551#pullrequestreview-885412252
    
    Signed-off-by: Martin Tzvetanov Grigorov <[email protected]>
    
    * AVRO-3405: Enable builder API for Writer::user_meta_data
    
    Add a unit test that could be used as an example how to pass the user
    metadata to the builder API.
    Fixes issue 3) from feedback at 
https://github.com/apache/avro/pull/1551#pullrequestreview-885412252
    
    Signed-off-by: Martin Tzvetanov Grigorov <[email protected]>
    
    * AVRO-3405: Test user metadata in Rust SDK interop tests
    
    TODO: add this test to the other SDKs too
    
    Signed-off-by: Martin Tzvetanov Grigorov <[email protected]>
    
    * AVRO-3405: Fix an obsolete warning message
    
    Signed-off-by: Martin Tzvetanov Grigorov <[email protected]>
    
    * AVRO-3405: Fix formatting
    
    Signed-off-by: Martin Tzvetanov Grigorov <[email protected]>
    
    * AVRO-3405: Add read/write interop tests for user metadata to Perl & Java 
SDKs too
    
    Now Rust, Perl & Java interop tests assert the values of 'stringKey' and
    'bytesKey' user metadata.
    
    Signed-off-by: Martin Tzvetanov Grigorov <[email protected]>
    
    * AVRO-3405: Simplify the assertions of user metadata in Perl interop test
    
    Signed-off-by: Martin Tzvetanov Grigorov <[email protected]>
    
    * AVRO-3405: Return an error when trying to add user metadata with a key 
'avro.xyz'
    
    Signed-off-by: Martin Tzvetanov Grigorov <[email protected]>
    (cherry picked from commit f888458721ce2573a6e04113a69a30ce785f8891)
---
 .../main/java/org/apache/avro/util/RandomData.java |   2 +
 .../java/org/apache/avro/DataFileInteropTest.java  |  24 +++-
 lang/perl/share/interop-data-generate              |   6 +
 lang/perl/xt/interop.t                             |  15 ++-
 lang/rust/examples/generate_interop_data.rs        |   9 ++
 lang/rust/examples/test_interop_data.rs            |  24 +++-
 lang/rust/src/error.rs                             |   6 +
 lang/rust/src/reader.rs                            | 129 +++++++++++++++----
 lang/rust/src/writer.rs                            | 141 ++++++++++++++++++---
 9 files changed, 306 insertions(+), 50 deletions(-)

diff --git a/lang/java/avro/src/main/java/org/apache/avro/util/RandomData.java 
b/lang/java/avro/src/main/java/org/apache/avro/util/RandomData.java
index 8806920..12b8a7b 100644
--- a/lang/java/avro/src/main/java/org/apache/avro/util/RandomData.java
+++ b/lang/java/avro/src/main/java/org/apache/avro/util/RandomData.java
@@ -170,6 +170,8 @@ public class RandomData implements Iterable<Object> {
     Schema sch = new Schema.Parser().parse(new File(args[0]));
     try (DataFileWriter<Object> writer = new DataFileWriter<>(new 
GenericDatumWriter<>())) {
       writer.setCodec(CodecFactory.fromString(args.length >= 4 ? args[3] : 
"null"));
+      writer.setMeta("stringKey", "stringValue");
+      writer.setMeta("bytesKey", 
"bytesValue".getBytes(StandardCharsets.UTF_8));
       writer.create(sch, new File(args[1]));
 
       for (Object datum : new RandomData(sch, Integer.parseInt(args[2]))) {
diff --git 
a/lang/java/ipc/src/test/java/org/apache/avro/DataFileInteropTest.java 
b/lang/java/ipc/src/test/java/org/apache/avro/DataFileInteropTest.java
index 6093015..7828532 100644
--- a/lang/java/ipc/src/test/java/org/apache/avro/DataFileInteropTest.java
+++ b/lang/java/ipc/src/test/java/org/apache/avro/DataFileInteropTest.java
@@ -17,16 +17,20 @@
  */
 package org.apache.avro;
 
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+
 import java.io.File;
 import java.io.IOException;
+import java.nio.charset.StandardCharsets;
 import java.util.Objects;
 
 import org.apache.avro.file.DataFileReader;
-import org.apache.avro.file.FileReader;
 import org.apache.avro.generic.GenericDatumReader;
 import org.apache.avro.io.DatumReader;
 import org.apache.avro.specific.SpecificDatumReader;
-import org.junit.Assert;
 import org.junit.BeforeClass;
 import org.junit.Test;
 
@@ -79,19 +83,27 @@ public class DataFileInteropTest {
   private <T extends Object> void readFiles(DatumReaderProvider<T> provider) 
throws IOException {
     for (File f : Objects.requireNonNull(DATAFILE_DIR.listFiles())) {
       System.out.println("Reading: " + f.getName());
-      try (FileReader<? extends Object> reader = DataFileReader.openReader(f, 
provider.get())) {
+      try (DataFileReader<? extends Object> reader = (DataFileReader<? extends 
Object>) DataFileReader.openReader(f,
+          provider.get())) {
+
+        // Ignore avro.schema & avro.codec. Some SDKs do not support user 
metadata.
+        if (reader.getMetaKeys().size() > 2) {
+          assertEquals("stringValue", reader.getMetaString("stringKey"));
+          assertArrayEquals("bytesValue".getBytes(StandardCharsets.UTF_8), 
reader.getMeta("bytesKey"));
+        }
+
         int i = 0;
         for (Object datum : reader) {
           i++;
-          Assert.assertNotNull(datum);
+          assertNotNull(datum);
         }
-        Assert.assertNotEquals(0, i);
+        assertNotEquals(0, i);
       }
     }
   }
 
   interface DatumReaderProvider<T extends Object> {
-    public DatumReader<T> get();
+    DatumReader<T> get();
   }
 
 }
diff --git a/lang/perl/share/interop-data-generate 
b/lang/perl/share/interop-data-generate
index c659605..5d2cd79 100644
--- a/lang/perl/share/interop-data-generate
+++ b/lang/perl/share/interop-data-generate
@@ -54,6 +54,11 @@ my $datum = {
     },
 };
 
+my $metadata = {
+    stringKey => 'stringValue',
+    bytesKey => 'bytesValue'
+};
+
 while (my ($codec, $enabled) = each(%Avro::DataFile::ValidCodec)) {
     next unless $enabled;
     my $outdir = '../../build/interop/data';
@@ -65,6 +70,7 @@ while (my ($codec, $enabled) = 
each(%Avro::DataFile::ValidCodec)) {
     my $writer = Avro::DataFileWriter->new(
         fh => $fh,
         codec => $codec,
+        metadata => $metadata,
         writer_schema => $writer_schema
     );
     $writer->print($datum);
diff --git a/lang/perl/xt/interop.t b/lang/perl/xt/interop.t
index 9af1648..d50ca6c 100644
--- a/lang/perl/xt/interop.t
+++ b/lang/perl/xt/interop.t
@@ -24,6 +24,11 @@ use IO::File;
 use_ok 'Avro::DataFile';
 use_ok 'Avro::DataFileReader';
 
+my $expected_metadata = {
+    stringKey => 'stringValue',
+    bytesKey => 'bytesValue'
+};
+
 for my $path (glob '../../build/interop/data/*.avro') {
     my $fn = basename($path);
     substr($fn, rindex $fn, '.') = '';
@@ -36,7 +41,15 @@ for my $path (glob '../../build/interop/data/*.avro') {
         }
     }
     my $fh = IO::File->new($path);
-    Avro::DataFileReader->new(fh => $fh);
+    my $reader = Avro::DataFileReader->new(fh => $fh);
+
+    my $metadata = $reader->metadata;
+    if (exists $metadata->{stringKey}) {
+        is($metadata->{stringKey}, $expected_metadata->{stringKey}, "check 
user metadata: stringKey ");
+    }
+    if (exists $metadata->{bytesKey}) {
+        is($metadata->{bytesKey}, join('', $expected_metadata->{bytesKey}), 
"check user metadata: bytesKey ");
+    }
     diag("Succeeded: ${path}");
 }
 
diff --git a/lang/rust/examples/generate_interop_data.rs 
b/lang/rust/examples/generate_interop_data.rs
index 514ee77..3d91140 100644
--- a/lang/rust/examples/generate_interop_data.rs
+++ b/lang/rust/examples/generate_interop_data.rs
@@ -78,6 +78,8 @@ fn main() -> anyhow::Result<()> {
 
     for codec in Codec::iter() {
         let mut writer = Writer::with_codec(&schema, Vec::new(), codec);
+        write_user_metadata(&mut writer)?;
+
         let datum = create_datum(&schema);
         writer.append(datum)?;
         let bytes = writer.into_inner()?;
@@ -97,3 +99,10 @@ fn main() -> anyhow::Result<()> {
 
     Ok(())
 }
+
+fn write_user_metadata(writer: &mut Writer<Vec<u8>>) -> anyhow::Result<()> {
+    writer.add_user_metadata("stringKey".to_string(), "stringValue")?;
+    writer.add_user_metadata("bytesKey".to_string(), b"bytesValue")?;
+
+    Ok(())
+}
diff --git a/lang/rust/examples/test_interop_data.rs 
b/lang/rust/examples/test_interop_data.rs
index e04020e..85d59de 100644
--- a/lang/rust/examples/test_interop_data.rs
+++ b/lang/rust/examples/test_interop_data.rs
@@ -16,9 +16,11 @@
 // under the License.
 
 use apache_avro::Reader;
-use std::ffi::OsStr;
+use std::{collections::HashMap, ffi::OsStr, fs::File};
 
 fn main() -> anyhow::Result<()> {
+    let expected_user_metadata: HashMap<String, Vec<u8>> = 
create_expected_user_metadata();
+
     let data_dir = std::fs::read_dir("../../build/interop/data/")
         .expect("Unable to list the interop data directory");
 
@@ -36,6 +38,9 @@ fn main() -> anyhow::Result<()> {
                 println!("Checking {:?}", &path);
                 let content = std::fs::File::open(&path)?;
                 let reader = Reader::new(&content)?;
+
+                test_user_metadata(&reader, &expected_user_metadata);
+
                 for value in reader {
                     if let Err(e) = value {
                         errors.push(format!(
@@ -57,3 +62,20 @@ fn main() -> anyhow::Result<()> {
         );
     }
 }
+
+fn create_expected_user_metadata() -> HashMap<String, Vec<u8>> {
+    let mut user_metadata: HashMap<String, Vec<u8>> = HashMap::new();
+    user_metadata.insert(
+        "stringKey".to_string(),
+        "stringValue".to_string().into_bytes(),
+    );
+    user_metadata.insert("bytesKey".to_string(), b"bytesValue".to_vec());
+    user_metadata
+}
+
+fn test_user_metadata(reader: &Reader<&File>, expected_user_metadata: 
&HashMap<String, Vec<u8>>) {
+    let user_metadata = reader.user_metadata();
+    if !user_metadata.is_empty() {
+        assert_eq!(user_metadata, expected_user_metadata);
+    }
+}
diff --git a/lang/rust/src/error.rs b/lang/rust/src/error.rs
index d687eea..1ed2f38 100644
--- a/lang/rust/src/error.rs
+++ b/lang/rust/src/error.rs
@@ -376,6 +376,12 @@ pub enum Error {
     /// Error while resolving Schema::Ref
     #[error("Unresolved schema reference: {0}")]
     SchemaResolutionError(String),
+
+    #[error("The file metadata is already flushed.")]
+    FileHeaderAlreadyWritten,
+
+    #[error("Metadata keys starting with 'avro.' are reserved for internal 
usage: {0}.")]
+    InvalidMetadataKey(String),
 }
 
 impl serde::ser::Error for Error {
diff --git a/lang/rust/src/reader.rs b/lang/rust/src/reader.rs
index d46b3bd..e4bf706 100644
--- a/lang/rust/src/reader.rs
+++ b/lang/rust/src/reader.rs
@@ -19,6 +19,7 @@
 use crate::{decode::decode, schema::Schema, types::Value, util, AvroResult, 
Codec, Error};
 use serde_json::from_slice;
 use std::{
+    collections::HashMap,
     io::{ErrorKind, Read},
     str::FromStr,
 };
@@ -35,6 +36,7 @@ struct Block<R> {
     marker: [u8; 16],
     codec: Codec,
     writer_schema: Schema,
+    user_metadata: HashMap<String, Vec<u8>>,
 }
 
 impl<R: Read> Block<R> {
@@ -47,6 +49,7 @@ impl<R: Read> Block<R> {
             buf_idx: 0,
             message_count: 0,
             marker: [0; 16],
+            user_metadata: Default::default(),
         };
 
         block.read_header()?;
@@ -67,32 +70,18 @@ impl<R: Read> Block<R> {
             return Err(Error::HeaderMagic);
         }
 
-        if let Value::Map(meta) = decode(&meta_schema, &mut self.reader)? {
-            // TODO: surface original parse schema errors instead of 
coalescing them here
-            let json = meta
-                .get("avro.schema")
-                .and_then(|bytes| {
-                    if let Value::Bytes(ref bytes) = *bytes {
-                        from_slice(bytes.as_ref()).ok()
-                    } else {
-                        None
-                    }
-                })
-                .ok_or(Error::GetAvroSchemaFromMap)?;
-            self.writer_schema = Schema::parse(&json)?;
-
-            if let Some(codec) = meta
-                .get("avro.codec")
-                .and_then(|codec| {
-                    if let Value::Bytes(ref bytes) = *codec {
-                        std::str::from_utf8(bytes.as_ref()).ok()
-                    } else {
-                        None
-                    }
-                })
-                .and_then(|codec| Codec::from_str(codec).ok())
-            {
-                self.codec = codec;
+        if let Value::Map(metadata) = decode(&meta_schema, &mut self.reader)? {
+            self.read_writer_schema(&metadata)?;
+            self.read_codec(&metadata)?;
+
+            for (key, value) in metadata {
+                if key == "avro.schema" || key == "avro.codec" {
+                    // already processed
+                } else if key.starts_with("avro.") {
+                    warn!("Ignoring unknown metadata key: {}", key);
+                } else {
+                    self.read_user_metadata(key, value)?;
+                }
             }
         } else {
             return Err(Error::GetHeaderMetadata);
@@ -184,6 +173,54 @@ impl<R: Read> Block<R> {
         self.message_count -= 1;
         Ok(Some(item))
     }
+
+    fn read_writer_schema(&mut self, metadata: &HashMap<String, Value>) -> 
AvroResult<()> {
+        let json = metadata
+            .get("avro.schema")
+            .and_then(|bytes| {
+                if let Value::Bytes(ref bytes) = *bytes {
+                    from_slice(bytes.as_ref()).ok()
+                } else {
+                    None
+                }
+            })
+            .ok_or(Error::GetAvroSchemaFromMap)?;
+        self.writer_schema = Schema::parse(&json)?;
+        Ok(())
+    }
+
+    fn read_codec(&mut self, metadata: &HashMap<String, Value>) -> 
AvroResult<()> {
+        if let Some(codec) = metadata
+            .get("avro.codec")
+            .and_then(|codec| {
+                if let Value::Bytes(ref bytes) = *codec {
+                    std::str::from_utf8(bytes.as_ref()).ok()
+                } else {
+                    None
+                }
+            })
+            .and_then(|codec| Codec::from_str(codec).ok())
+        {
+            self.codec = codec;
+        }
+        Ok(())
+    }
+
+    fn read_user_metadata(&mut self, key: String, value: Value) -> 
AvroResult<()> {
+        match value {
+            Value::Bytes(ref vec) => {
+                self.user_metadata.insert(key, vec.clone());
+                Ok(())
+            }
+            wrong => {
+                warn!(
+                    "User metadata values must be Value::Bytes, found {:?}",
+                    wrong
+                );
+                Ok(())
+            }
+        }
+    }
 }
 
 /// Main interface for reading Avro formatted values.
@@ -242,15 +279,23 @@ impl<'a, R: Read> Reader<'a, R> {
     }
 
     /// Get a reference to the writer `Schema`.
+    #[inline]
     pub fn writer_schema(&self) -> &Schema {
         &self.block.writer_schema
     }
 
     /// Get a reference to the optional reader `Schema`.
+    #[inline]
     pub fn reader_schema(&self) -> Option<&Schema> {
         self.reader_schema
     }
 
+    /// Get a reference to the user metadata
+    #[inline]
+    pub fn user_metadata(&self) -> &HashMap<String, Vec<u8>> {
+        &self.block.user_metadata
+    }
+
     #[inline]
     fn read_next(&mut self) -> AvroResult<Option<Value>> {
         let read_schema = if self.should_resolve_schema {
@@ -499,4 +544,36 @@ mod tests {
             assert!(value.is_err());
         }
     }
+
+    #[test]
+    fn test_avro_3405_read_user_metadata_success() {
+        use crate::writer::Writer;
+
+        let schema = Schema::parse_str(SCHEMA).unwrap();
+        let mut writer = Writer::new(&schema, Vec::new());
+
+        let mut user_meta_data: HashMap<String, Vec<u8>> = HashMap::new();
+        user_meta_data.insert(
+            "stringKey".to_string(),
+            "stringValue".to_string().into_bytes(),
+        );
+        user_meta_data.insert("bytesKey".to_string(), b"bytesValue".to_vec());
+        user_meta_data.insert("vecKey".to_string(), vec![1, 2, 3]);
+
+        for (k, v) in user_meta_data.iter() {
+            writer.add_user_metadata(k.to_string(), v).unwrap();
+        }
+
+        let mut record = Record::new(&schema).unwrap();
+        record.put("a", 27i64);
+        record.put("b", "foo");
+
+        writer.append(record.clone()).unwrap();
+        writer.append(record.clone()).unwrap();
+        writer.flush().unwrap();
+        let result = writer.into_inner().unwrap();
+
+        let reader = Reader::new(&result[..]).unwrap();
+        assert_eq!(reader.user_metadata(), &user_meta_data);
+    }
 }
diff --git a/lang/rust/src/writer.rs b/lang/rust/src/writer.rs
index a222a0f..f8a4554 100644
--- a/lang/rust/src/writer.rs
+++ b/lang/rust/src/writer.rs
@@ -49,6 +49,8 @@ pub struct Writer<'a, W> {
     marker: Vec<u8>,
     #[builder(default = false, setter(skip))]
     has_header: bool,
+    #[builder(default)]
+    user_metadata: HashMap<String, Value>,
 }
 
 impl<'a, W: Write> Writer<'a, W> {
@@ -83,14 +85,7 @@ impl<'a, W: Write> Writer<'a, W> {
     /// internal buffering for performance reasons. If you want to be sure the 
value has been
     /// written, then call [`flush`](struct.Writer.html#method.flush).
     pub fn append<T: Into<Value>>(&mut self, value: T) -> AvroResult<usize> {
-        let n = if !self.has_header {
-            let header = self.header()?;
-            let n = self.append_bytes(header.as_ref())?;
-            self.has_header = true;
-            n
-        } else {
-            0
-        };
+        let n = self.maybe_write_header()?;
 
         let avro = value.into();
         write_value_ref(self.schema, &avro, &mut self.buffer)?;
@@ -112,14 +107,7 @@ impl<'a, W: Write> Writer<'a, W> {
     /// internal buffering for performance reasons. If you want to be sure the 
value has been
     /// written, then call [`flush`](struct.Writer.html#method.flush).
     pub fn append_value_ref(&mut self, value: &Value) -> AvroResult<usize> {
-        let n = if !self.has_header {
-            let header = self.header()?;
-            let n = self.append_bytes(header.as_ref())?;
-            self.has_header = true;
-            n
-        } else {
-            0
-        };
+        let n = self.maybe_write_header()?;
 
         write_value_ref(self.schema, value, &mut self.buffer)?;
 
@@ -286,6 +274,21 @@ impl<'a, W: Write> Writer<'a, W> {
         self.writer.write(bytes).map_err(Error::WriteBytes)
     }
 
+    /// Adds custom metadata to the file.
+    /// This method could be used only before adding the first record to the 
writer.
+    pub fn add_user_metadata<T: AsRef<[u8]>>(&mut self, key: String, value: T) 
-> AvroResult<()> {
+        if !self.has_header {
+            if key.starts_with("avro.") {
+                return Err(Error::InvalidMetadataKey(key));
+            }
+            self.user_metadata
+                .insert(key, Value::Bytes(value.as_ref().to_vec()));
+            Ok(())
+        } else {
+            Err(Error::FileHeaderAlreadyWritten)
+        }
+    }
+
     /// Create an Avro header based on schema, codec and sync marker.
     fn header(&self) -> Result<Vec<u8>, Error> {
         let schema_bytes = serde_json::to_string(self.schema)
@@ -296,6 +299,10 @@ impl<'a, W: Write> Writer<'a, W> {
         metadata.insert("avro.schema", Value::Bytes(schema_bytes));
         metadata.insert("avro.codec", self.codec.into());
 
+        for (k, v) in &self.user_metadata {
+            metadata.insert(k.as_str(), v.clone());
+        }
+
         let mut header = Vec::new();
         header.extend_from_slice(AVRO_OBJECT_HEADER);
         encode(
@@ -307,6 +314,17 @@ impl<'a, W: Write> Writer<'a, W> {
 
         Ok(header)
     }
+
+    fn maybe_write_header(&mut self) -> AvroResult<usize> {
+        if !self.has_header {
+            let header = self.header()?;
+            let n = self.append_bytes(header.as_ref())?;
+            self.has_header = true;
+            Ok(n)
+        } else {
+            Ok(0)
+        }
+    }
 }
 
 /// Encode a compatible value (implementing the `ToAvro` trait) into Avro 
format, also performing
@@ -812,4 +830,95 @@ mod tests {
             data.as_slice()
         );
     }
+
+    #[test]
+    fn test_avro_3405_writer_add_metadata_success() {
+        let schema = Schema::parse_str(SCHEMA).unwrap();
+        let mut writer = Writer::new(&schema, Vec::new());
+
+        writer
+            .add_user_metadata("stringKey".to_string(), 
"stringValue".to_string())
+            .unwrap();
+        writer
+            .add_user_metadata("strKey".to_string(), "strValue")
+            .unwrap();
+        writer
+            .add_user_metadata("bytesKey".to_string(), b"bytesValue")
+            .unwrap();
+        writer
+            .add_user_metadata("vecKey".to_string(), vec![1, 2, 3])
+            .unwrap();
+
+        let mut record = Record::new(&schema).unwrap();
+        record.put("a", 27i64);
+        record.put("b", "foo");
+
+        writer.append(record.clone()).unwrap();
+        writer.append(record.clone()).unwrap();
+        writer.flush().unwrap();
+        let result = writer.into_inner().unwrap();
+
+        assert_eq!(result.len(), 260);
+    }
+
+    #[test]
+    fn test_avro_3405_writer_add_metadata_failure() {
+        let schema = Schema::parse_str(SCHEMA).unwrap();
+        let mut writer = Writer::new(&schema, Vec::new());
+
+        let mut record = Record::new(&schema).unwrap();
+        record.put("a", 27i64);
+        record.put("b", "foo");
+        writer.append(record.clone()).unwrap();
+
+        match writer.add_user_metadata("stringKey".to_string(), 
"value2".to_string()) {
+            Err(e @ Error::FileHeaderAlreadyWritten) => {
+                assert_eq!(e.to_string(), "The file metadata is already 
flushed.")
+            }
+            Err(e) => panic!(
+                "Unexpected error occurred while writing user metadata: {:?}",
+                e
+            ),
+            Ok(_) => panic!("Expected an error that metadata cannot be added 
after adding data"),
+        }
+    }
+
+    #[test]
+    fn test_avro_3405_writer_add_metadata_reserved_prefix_failure() {
+        let schema = Schema::parse_str(SCHEMA).unwrap();
+        let mut writer = Writer::new(&schema, Vec::new());
+
+        let key = "avro.stringKey".to_string();
+        match writer.add_user_metadata(key.clone(), "value") {
+            Err(ref e @ Error::InvalidMetadataKey(_)) => {
+                assert_eq!(e.to_string(), format!("Metadata keys starting with 
'avro.' are reserved for internal usage: {}.", key))
+            }
+            Err(e) => panic!(
+                "Unexpected error occurred while writing user metadata with 
reserved prefix ('avro.'): {:?}",
+                e
+            ),
+            Ok(_) => panic!("Expected an error that the metadata key cannot be 
prefixed with 'avro.'"),
+        }
+    }
+
+    #[test]
+    fn test_avro_3405_writer_add_metadata_with_builder_api_success() {
+        let schema = Schema::parse_str(SCHEMA).unwrap();
+
+        let mut user_meta_data: HashMap<String, Value> = HashMap::new();
+        user_meta_data.insert(
+            "stringKey".to_string(),
+            Value::String("stringValue".to_string()),
+        );
+        user_meta_data.insert("bytesKey".to_string(), 
Value::Bytes(b"bytesValue".to_vec()));
+        user_meta_data.insert("vecKey".to_string(), Value::Bytes(vec![1, 2, 
3]));
+
+        let writer: Writer<'_, Vec<u8>> = Writer::builder()
+            .writer(Vec::new())
+            .schema(&schema)
+            .user_metadata(user_meta_data.clone())
+            .build();
+
+        assert_eq!(writer.user_metadata, user_meta_data);
+    }
 }

Reply via email to