From 68254945d0d8e24ee81f1707beec0d95c24b5e5e Mon Sep 17 00:00:00 2001 From: creativcoder Date: Thu, 8 Oct 2020 21:42:19 +0530 Subject: [PATCH 1/5] Initial implementation --- .github/FUNDING.yml | 2 + .github/ISSUE_TEMPLATE/1-problem.md | 5 + .github/ISSUE_TEMPLATE/2-suggestion.md | 5 + .github/ISSUE_TEMPLATE/3-documentation.md | 5 + .github/workflows/ci.yml | 52 ++ .gitignore | 11 + .vscode/launch.json | 16 + CHANGELOG.md | 16 + CODE_OF_CONDUCT.md | 75 +++ CONTRIBUTING.md | 26 + Cargo.toml | 73 +++ LICENSE-APACHE | 201 ++++++ LICENSE-MIT | 23 + README.md | 425 +++++++++++++ assets/avrow_logo.png | Bin 0 -> 409371 bytes avrow-cli/Cargo.toml | 18 + avrow-cli/README.md | 31 + avrow-cli/src/main.rs | 43 ++ avrow-cli/src/subcommand.rs | 157 +++++ avrow-cli/src/utils.rs | 11 + benches/complex.rs | 150 +++++ benches/primitives.rs | 149 +++++ benches/schema.rs | 61 ++ benches/write.rs | 1 + examples/canonical.rs | 24 + examples/from_json_to_struct.rs | 72 +++ examples/hello_world.rs | 41 ++ examples/recursive_record.rs | 56 ++ examples/writer_builder.rs | 23 + rustfmt.toml | 2 + src/codec.rs | 273 +++++++++ src/config.rs | 15 + src/error.rs | 184 ++++++ src/lib.rs | 81 +++ src/reader.rs | 707 +++++++++++++++++++++ src/schema/canonical.rs | 259 ++++++++ src/schema/common.rs | 360 +++++++++++ src/schema/mod.rs | 258 ++++++++ src/schema/parser.rs | 494 +++++++++++++++ src/schema/tests.rs | 437 +++++++++++++ src/serde_avro/de.rs | 170 ++++++ src/serde_avro/de_impl.rs | 193 ++++++ src/serde_avro/mod.rs | 8 + src/serde_avro/ser.rs | 261 ++++++++ src/serde_avro/ser_impl.rs | 195 ++++++ src/util.rs | 34 ++ src/value.rs | 710 ++++++++++++++++++++++ src/writer.rs | 318 ++++++++++ tests/common.rs | 90 +++ tests/read_write.rs | 414 +++++++++++++ tests/schema_resolution.rs | 315 ++++++++++ 51 files changed, 7550 insertions(+) create mode 100644 .github/FUNDING.yml create mode 100644 .github/ISSUE_TEMPLATE/1-problem.md create mode 100644 .github/ISSUE_TEMPLATE/2-suggestion.md create mode 100644 .github/ISSUE_TEMPLATE/3-documentation.md create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 .vscode/launch.json create mode 100644 CHANGELOG.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 Cargo.toml create mode 100644 LICENSE-APACHE create mode 100644 LICENSE-MIT create mode 100644 README.md create mode 100644 assets/avrow_logo.png create mode 100644 avrow-cli/Cargo.toml create mode 100644 avrow-cli/README.md create mode 100644 avrow-cli/src/main.rs create mode 100644 avrow-cli/src/subcommand.rs create mode 100644 avrow-cli/src/utils.rs create mode 100644 benches/complex.rs create mode 100644 benches/primitives.rs create mode 100644 benches/schema.rs create mode 100644 benches/write.rs create mode 100644 examples/canonical.rs create mode 100644 examples/from_json_to_struct.rs create mode 100644 examples/hello_world.rs create mode 100644 examples/recursive_record.rs create mode 100644 examples/writer_builder.rs create mode 100644 rustfmt.toml create mode 100644 src/codec.rs create mode 100644 src/config.rs create mode 100644 src/error.rs create mode 100644 src/lib.rs create mode 100644 src/reader.rs create mode 100644 src/schema/canonical.rs create mode 100644 src/schema/common.rs create mode 100644 src/schema/mod.rs create mode 100644 src/schema/parser.rs create mode 100644 src/schema/tests.rs create mode 100644 src/serde_avro/de.rs create mode 100644 src/serde_avro/de_impl.rs create mode 100644 src/serde_avro/mod.rs create mode 100644 src/serde_avro/ser.rs create mode 100644 src/serde_avro/ser_impl.rs create mode 100644 src/util.rs create mode 100644 src/value.rs create mode 100644 src/writer.rs create mode 100644 tests/common.rs create mode 100644 tests/read_write.rs create mode 100644 tests/schema_resolution.rs diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..186b30a --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +liberapay: creativcoder +custom: ["https://www.buymeacoffee.com/creativcoder"] diff --git a/.github/ISSUE_TEMPLATE/1-problem.md b/.github/ISSUE_TEMPLATE/1-problem.md new file mode 100644 index 0000000..3abc470 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/1-problem.md @@ -0,0 +1,5 @@ +--- +name: Problem +about: Something does not seem right + +--- diff --git a/.github/ISSUE_TEMPLATE/2-suggestion.md b/.github/ISSUE_TEMPLATE/2-suggestion.md new file mode 100644 index 0000000..6444981 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/2-suggestion.md @@ -0,0 +1,5 @@ +--- +name: Suggestion +about: Share how Avrow could support your use case better + +--- diff --git a/.github/ISSUE_TEMPLATE/3-documentation.md b/.github/ISSUE_TEMPLATE/3-documentation.md new file mode 100644 index 0000000..37de1c2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/3-documentation.md @@ -0,0 +1,5 @@ +--- +name: Documentation +about: Certainly there is room for improvement + +--- diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..a0e6c42 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,52 @@ +on: [push, pull_request] + +jobs: + linux: + name: Test Suite (linux) + runs-on: ubuntu-latest + strategy: + matrix: + rust: + - stable + - nightly + - 1.37.0 + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ matrix.rust }} + - run: cargo test --release --all-features + + windows: + name: Test suite (windows) + runs-on: windows-latest + steps: + - uses: actions/checkout@v2 + - run: cargo test --all-features + + lints: + name: Lints + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v2 + + - name: Install stable toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + components: rustfmt, clippy + + - name: Run cargo fmt + uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check + + - name: Run cargo clippy + uses: actions-rs/cargo@v1 + with: + command: clippy + args: -- -D warnings \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4f865be --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ + +/target +**/*.rs.bk +Cargo.lock +NOTES.md +*.avro +*.jar +experiments/ +TODO.md +# .vscode +avrow-cli/target \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..224af64 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Debug", + "program": "${workspaceFolder}/", + "args": [], + "cwd": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a4424f4 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,16 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +# [Unreleased] + + +# [0.1.0] - 2020-10-08 + +## Added + +Initial implementation of +- avrow +- avrow-cli (av) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..4ff57d6 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,75 @@ + +## Code of Conduct + +### Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +nationality, personal appearance, race, religion, or sexual identity and +orientation. + +### Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or +advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +### Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +### Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +### Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at [INSERT EMAIL ADDRESS]. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +### Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..87bc366 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,26 @@ + +# Contributing + +When contributing to this repository, please first discuss the change you wish to make via issue, +email, or any other method with the owners of this repository before making a change. + +Please note we have a [code of conduct](./CODE_OF_CONDUCT.md), please follow it in all your interactions with the project. + +## Pull Request Process + +Following is a cursory guideline on how to make the process of making changes more efficient for the contributer and the maintainer. + +1. File an issue for the change you want to make. This way we can track the why of the change. + Get consensus from community for the change. +2. Clone the project and perform a fresh build. Create a branch with the naming "feature/issue-number. +3. Ensure that the PR only changes the parts of code which implements/solves the issue. This includes running + the linter (cargo fmt) and removing any extra spaces and any formatting that accidentally were made by + the code editor in use. +4. If your PR has changes that should also reflect in README.md, please update that as well. +5. Document non obvious changes and the `why` of your changes if it's unclear. +6. If you are adding a public API, add the documentation as well. +7. Increase the version numbers in Cargo.toml files and the README.md to the new version that this + Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/). +8. Update the CHANGELOG.md to reflect the change if applicable. + +More details: https://github.community/t/best-practices-for-pull-requests/10195 \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..1738267 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,73 @@ +[package] +name = "avrow" +version = "0.1.0" +authors = ["creativcoder "] +edition = "2018" +repository = "https://github.com/creativcoder/avrow" +license = "MIT OR Apache-2.0" +description = "Avrow is a fast, type safe serde based data serialization library" +homepage = "avrow.github.io" +documentation = "https://docs.rs/avrow" +readme = "README.md" +keywords = ["avro", "avrow", "rust-avro", "serde-avro","encoding", "kafka", "spark"] +categories = ["encoding", "compression", "command-line-utilities"] + +publish = false + +[dependencies] +serde = {version= "1", features=["derive"] } +serde_derive = "1" +serde_json = { version="1", features=["preserve_order"] } +rand = "0.4.2" +byteorder = "1" +integer-encoding = "2" +snap = { version = "0.2", optional = true } +flate2 = { version = "1", features = ["zlib"], default-features = false, optional = true } +crc = "1" +thiserror = "1.0" +indexmap = {version = "1", features = ["serde-1"]} +once_cell = "1.4.1" +zstdd = { version = "0.5.3", optional = true, package="zstd" } +bzip2 = { version = "0.4.1", optional = true } +xz2 = { version = "0.1", optional = true } +shatwo = { version = "0.9.1", optional = true, package="sha2" } +mdfive = { version = "0.7.0", optional = true, package="md5" } + +[dev-dependencies] +criterion = "0.2" +pretty_env_logger = "0.4" +fstrings = "0.2" +env_logger = "0.4" +anyhow = "1.0.32" + +[[bench]] +name = "primitives" +harness = false + +[[bench]] +name = "complex" +harness = false + +[[bench]] +name = "schema" +harness = false + +[features] +# compression codecs +snappy = ["snap"] +deflate = ["flate2"] +zstd = ["zstdd"] +bzip = ["bzip2"] +xz = ["xz2"] +# fingerprint codecs +sha2 = ["shatwo"] +md5 = ["mdfive"] + +codec = ["snappy", "deflate", "zstd", "bzip2", "xz"] +fingerprint = ["sha2", "md5"] +all = ["codec", "fingerprint"] + +[profile.release] +opt-level = 'z' +lto = true +codegen-units = 1 diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..16fe87b --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed 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. diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..31aa793 --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..97b15f5 --- /dev/null +++ b/README.md @@ -0,0 +1,425 @@ +
+ avrow + +[![github actions](https://github.com/creativcoder/avrow/workflows/Rust/badge.svg)](https://github.com/creativcoder/avrow/actions) +[![crates](https://img.shields.io/crates/v/avrow.svg)](https://crates.io/crates/io-uring) +[![docs.rs](https://docs.rs/avrow/badge.svg)](https://docs.rs/avrow/) +[![license](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/creativcoder/avrow/blob/master/LICENSE-MIT) +[![license](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/creativcoder/avrow/blob/master/LICENSE-APACHE) +[![Contributor Covenant](https://img.shields.io/badge/contributor%20covenant-v1.4%20adopted-ff69b4.svg)](CODE_OF_CONDUCT.md) + +
+
+ + +### Avrow is a pure Rust implementation of the [Avro specification](https://avro.apache.org/docs/current/spec.html) with [Serde](https://github.com/serde-rs/serde) support. + + +
+
+ +
+ +### Table of Contents +- [Overview](#overview) +- [Features](#features) +- [Getting started](#getting-started) +- [Examples](#examples) + - [Writing avro data](#writing-avro-data) + - [Reading avro data](#reading-avro-data) + - [Writer builder](#writer-customization) +- [Supported Codecs](#supported-codecs) +- [Using the avrow-cli tool](#using-avrow-cli-tool) +- [Benchmarks](#benchmarks) +- [Todo](#todo) +- [Changelog](#changelog) +- [Contributions](#contributions) +- [Support](#support) +- [MSRV](#msrv) +- [License](#license) + +## Overview + +Avrow is a pure Rust implementation of the [Avro specification](https://avro.apache.org/docs/current/spec.html): a row based data serialization system. The Avro data serialization format finds its use quite a lot in big data streaming systems such as [Kafka](https://kafka.apache.org/) and [Spark](https://spark.apache.org/). +Within avro's context, an avro encoded file or byte stream is called a "data file". +To write data in avro encoded format, one needs a schema which is provided in json format. Here's an example of an avro schema represented in json: + +```json +{ + "type": "record", + "name": "LongList", + "aliases": ["LinkedLongs"], + "fields" : [ + {"name": "value", "type": "long"}, + {"name": "next", "type": ["null", "LongList"]} + ] +} +``` +The above schema is of type record with fields and represents a linked list of 64-bit integers. In most implementations, this schema is then fed to a `Writer` instance along with a buffer to write encoded data to. One can then call one +of the `write` methods on the writer to write data. One distinguishing aspect of avro is that the schema for the encoded data is written on the header of the data file. This means that for reading data you don't need to provide a schema to a `Reader` instance. The spec also allows providing a reader schema to filter data when reading. + +The Avro specification provides two kinds of encoding: +* Binary encoding - Efficent and takes less space on disk. +* JSON encoding - When you want a readable version of avro encoded data. Also used for debugging purposes. + +This crate implements only the binary encoding as that's the format practically used for performance and storage reasons. + +## Features. + +* Full support for recursive self-referential schemas with Serde serialization/deserialization. +* All compressions codecs (`deflate`, `bzip2`, `snappy`, `xz`, `zstd`) supported as per spec. +* Simple and intuitive API - As the underlying structures in use are `Read` and `Write` types, avrow tries to mimic the same APIs as Rust's standard library APIs for minimal learning overhead. Writing avro values is simply calling `write` or `serialize` (with serde) and reading avro values is simply using iterators. +* Less bloat / Lightweight - Compile times in Rust are costly. Avrow tries to use minimal third-party crates. Compression codec and schema fingerprinting support are feature gated by default. To use them, compile with respective feature flags (e.g. `--features zstd`). +* Schema evolution - One can configure the avrow `Reader` with a reader schema and only read data relevant to their use case. +* Schema's in avrow supports querying their canonical form and have fingerprinting (`rabin64`, `sha256`, `md5`) support. + +**Note**: This is not a complete spec implemention and remaining features being implemented are listed under [Todo](#todo) section. + +## Getting started: + +Add avrow as a dependency to `Cargo.toml`: + +```toml +[dependencies] +avrow = "0.1" +``` + +## Examples: + +### Writing avro data + +```rust + +use anyhow::Error; +use avrow::{Schema, Writer}; +use std::str::FromStr; + +fn main() -> Result<(), Error> { + // Create schema from json + let schema = Schema::from_str(r##"{"type":"string"}"##)?; + // or from a path + let schema2 = Schema::from_path("./string_schema.avsc")?; + // Create an output stream + let stream = Vec::new(); + // Create a writer + let writer = Writer::new(&schema, stream.as_slice())?; + // Write your data! + let res = writer.write("Hey")?; + // or using serialize method for serde derived types. + let res = writer.serialize("there!")?; + + Ok(()) +} + +``` +For simple and native Rust types, avrow provides a `From` impl for Avro value types. For compound or user defined types (structs, enums), one can use the `serialize` method which relies on serde. Alternatively, one can construct `avrow::Value` instances which is a more verbose way to write avro values and should be a last resort. + +### Reading avro data + +```rust +fn main() -> Result<(), Error> { + let schema = Schema::from_str(r##""null""##); + let data = vec![ + 79, 98, 106, 1, 4, 22, 97, 118, 114, 111, 46, 115, 99, 104, 101, + 109, 97, 32, 123, 34, 116, 121, 112, 101, 34, 58, 34, 98, 121, 116, + 101, 115, 34, 125, 20, 97, 118, 114, 111, 46, 99, 111, 100, 101, + 99, 14, 100, 101, 102, 108, 97, 116, 101, 0, 145, 85, 112, 15, 87, + 201, 208, 26, 183, 148, 48, 236, 212, 250, 38, 208, 2, 18, 227, 97, + 96, 100, 98, 102, 97, 5, 0, 145, 85, 112, 15, 87, 201, 208, 26, + 183, 148, 48, 236, 212, 250, 38, 208, + ]; + // Create a Reader + let reader = Reader::with_schema(v.as_slice(), schema)?; + for i in reader { + dbg!(&i); + } + + Ok(()) +} + +``` + +A more involved self-referential recursive schema example: + +```rust +use anyhow::Error; +use avrow::{from_value, Codec, Reader, Schema, Writer}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +struct LongList { + value: i64, + next: Option>, +} + +fn main() -> Result<(), Error> { + let schema = r##" + { + "type": "record", + "name": "LongList", + "aliases": ["LinkedLongs"], + "fields" : [ + {"name": "value", "type": "long"}, + {"name": "next", "type": ["null", "LongList"]} + ] + } + "##; + + let schema = Schema::from_str(schema)?; + let mut writer = Writer::with_codec(&schema, vec![], Codec::Null)?; + + let value = LongList { + value: 1i64, + next: Some(Box::new(LongList { + value: 2i64, + next: Some(Box::new(LongList { + value: 3i64, + next: Some(Box::new(LongList { + value: 4i64, + next: Some(Box::new(LongList { + value: 5i64, + next: None, + })), + })), + })), + })), + }; + + writer.serialize(value)?; + + // Calling into_inner performs flush internally. Alternatively, one can call flush explicitly. + let buf = writer.into_inner()?; + + // read + let reader = Reader::with_schema(buf.as_slice(), schema)?; + for i in reader { + let a: LongList = from_value(&i)?; + dbg!(a); + } + + Ok(()) +} + +``` + +An example of writing a json object with a confirming schema. The json object maps to an `avrow::Record` type. + +```rust +use anyhow::Error; +use avrow::{from_value, Reader, Record, Schema, Writer}; +use serde::{Deserialize, Serialize}; +use std::str::FromStr; + +#[derive(Debug, Serialize, Deserialize)] +struct Mentees { + id: i32, + username: String, +} + +#[derive(Debug, Serialize, Deserialize)] +struct RustMentors { + name: String, + github_handle: String, + active: bool, + mentees: Mentees, +} + +fn main() -> Result<(), Error> { + let schema = Schema::from_str( + r##" + { + "name": "rust_mentors", + "type": "record", + "fields": [ + { + "name": "name", + "type": "string" + }, + { + "name": "github_handle", + "type": "string" + }, + { + "name": "active", + "type": "boolean" + }, + { + "name":"mentees", + "type": { + "name":"mentees", + "type": "record", + "fields": [ + {"name":"id", "type": "int"}, + {"name":"username", "type": "string"} + ] + } + } + ] + } +"##, + )?; + + let json_data = serde_json::from_str( + r##" + { "name": "bob", + "github_handle":"ghbob", + "active": true, + "mentees":{"id":1, "username":"alice"} }"##, + )?; + let rec = Record::from_json(json_data, &schema)?; + let mut writer = crate::Writer::new(&schema, vec![])?; + writer.write(rec)?; + + let avro_data = writer.into_inner()?; + let reader = crate::Reader::from(avro_data.as_slice())?; + for value in reader { + let mentors: RustMentors = from_value(&value)?; + dbg!(mentors); + } + Ok(()) +} + +``` + +### Writer customization + +If you want to have more control over the parameters of `Writer`, consider using `WriterBuilder` as shown below: + +```rust + +use anyhow::Error; +use avrow::{Codec, Reader, Schema, WriterBuilder}; + +fn main() -> Result<(), Error> { + let schema = Schema::from_str(r##""null""##)?; + let v = vec![]; + let mut writer = WriterBuilder::new() + .set_codec(Codec::Null) + .set_schema(&schema) + .set_datafile(v) + // set any custom metadata in the header + .set_metadata("hello", "world") + // set after how many bytes, the writer should flush + .set_flush_interval(128_000) + .build() + .unwrap(); + writer.serialize(())?; + let v = writer.into_inner()?; + + let reader = Reader::with_schema(v.as_slice(), schema)?; + for i in reader { + dbg!(i?); + } + + Ok(()) +} +``` + +Refer to [examples](./examples) for more code examples. + +## Supported Codecs + +In order to facilitate efficient encoding, avro spec also defines compression codecs to use when serializing data. + +Avrow supports all compression codecs as per spec: + +- Null - The default is no codec. +- [Deflate](https://en.wikipedia.org/wiki/DEFLATE) +- [Snappy](https://github.com/google/snappy) +- [Zstd](https://facebook.github.io/zstd/) +- [Bzip2](https://www.sourceware.org/bzip2/) +- [Xz](https://linux.die.net/man/1/xz) + +These are feature-gated behind their respective flags. Check `Cargo.toml` `features` section for more details. + +## Using avrow-cli tool: + +Quite often you will need a quick way to examine avro file for debugging purposes. +For that, this repository also comes with the [`avrow-cli`](./avrow-cli) tool (av) +by which one can examine avro datafiles from the command line. + +See [avrow-cli](avrow-cli/) repository for more details. + +Installing avrow-cli: + +``` +cd avrow-cli +cargo install avrow-cli +``` + +Using avrow-cli (binary name is `av`): + +```bash +av read -d data.avro +``` + +The `read` subcommand will print all rows in `data.avro` to standard out in debug format. + +### Rust native types to Avro value mapping (via Serde) + +Primitives +--- + +| Rust native types (primitive types) | Avro (`Value`) | +| ----------------------------------- | -------------- | +| `(), Option::None` | `null` | +| `bool` | `boolean` | +| `i8, u8, i16, u16, i32, u32` | `int` | +| `i64, u64` | `long` | +| `f32` | `float` | +| `f64` | `double` | +| `&[u8], Vec` | `bytes` | +| `&str, String` | `string` | +--- +Complex + +| Rust native types (complex types) | Avro | +| ---------------------------------------------------- | -------- | +| `struct Foo {..}` | `record` | +| `enum Foo {A,B}` (variants cannot have data in them) | `enum` | +| `Vec where T: Into` | `array` | +| `HashMap where T: Into` | `map` | +| `T where T: Into` | `union` | +| `Vec` : Length equal to size defined in schema | `fixed` | + +
+ +## Todo + +* [Logical types](https://avro.apache.org/docs/current/spec.html#Logical+Types) support. +* Sorted reads. +* Single object encoding. +* Schema Registry as a trait - would allow avrow to read from and write to remote schema registries. +* AsyncRead + AsyncWrite Reader and Writers. +* Avro protocol message and RPC support. +* Benchmarks and optimizations. + +## Changelog + +Please see the [CHANGELOG](CHANGELOG.md) for a release history. + +## Contributions + +All kinds of contributions are welcome. + +Head over to [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines. + +## Support + +Buy Me A Coffee + +[![ko-fi](https://www.ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/P5P71YZ0L) + +## MSRV + +Avrow works on stable Rust, starting 1.37+. +It does not use any nightly features. + +## License + +Dual licensed under either of Apache License, Version +2.0 or MIT license at your option. + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in this crate by you, as defined in the Apache-2.0 license, shall +be dual licensed as above, without any additional terms or conditions. diff --git a/assets/avrow_logo.png b/assets/avrow_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..d8c48d48d54cec6d609fa27b2e09629d6378b556 GIT binary patch literal 409371 zcmeFZXIK;M)&`n{00E>}u}~sidJ&W!8(lz<-ld6D=`AD(h*$uF6zK|xD7{KYcmab5 z80keoK?uFKkmO9<`+MK-+Z)gSb1t|p$YhwAXP#%Rd#!twnJ8UtRYv-=^dJz3QC;o2 z9tZ>jHenzb4e(K#`%N790DI}FUIi6)vEhL);r7Pr4mWRt1b-h!4W&rQ`xsN;lkyV|{DS}fq{;;U`)FWaCggvPhJiAv{@JFS z2Jdcc13u_H)J(iUpc6MKzu*@g&%{6=2(!~|V{hY|T5`7TuA(+}?sx1({armMXMq&_ z<$z6Bdv6=KzpIOzmz=*M*Y6|bfNjcdF)sM;L%f|8xr}e>!mqh|+QX$qFN%tDDbd5> za0O302RXg#D*rwm_@>Bp*W24ePE5?t&rj4(O4QxcQA|QsR#r@0QcO}(1UN#(E5Oa$ z#$UwEi~C;}`Jd}txA(I3bn@_aa(9DMu4{A0-N##zi;FVQ-#`DlPj4rOe@1fi`uDVe z35rqvBPJm#F824efm0PId*zJv?7iGwd??pfl8};A_|En)NNl!MUfG4Z5dC$-hF%L*`opfC<%?H zdANip!&hj+T&ZnNgoCv+9BV3vX8au#q#T#Mwx-wln1eFPx-EkWx|3ZR0>=I7od-mp zfxr+bj0KKBA8s5j!l5P{qTmn(hbTBi!66C`QE-TYLlhjM;1C6eC^$sH|7TDj?i$+H z*B9?FoMB7&m)M#%sP~nTdu>Xge@~(*pMgz&Uj8ome?|U-$>@*hg57{izY_vU{Gr!~ zD3rauy-#{{OT)2$FyP@0;RCUyrT88FdtZ`C=ry1H?gge1iGL*%tmh!}^Hj$nZ?u=F z{uyH75jsc7FMKTWpQ|OMHW^2fUd<60i*~-PDpz}1YyQ4z>wak4T-au<%sbZpUfQ__ zn_3}!f2SzKGay+HP)gh1Esy_w3&wh(CECPmeH+elo2puJ+p5Pn=qOo@rZw2pKE`?G zz`E!i*TqarWnLY=U1$qfzw>Rbl3jA zzuzL!GO*5y_D@^Z=dHT`?{l@Rks_~|&a|w)_*-a)8-GjtaN{49`v+f#S-gKxe7HV` z=Hk#w9HQV51&1g&M8P2n4pDH3fqTmn(hbTBi!66C`QE-TYLlhjM;1C6eC^$sH zAqoyraEO9K6da=95Cw-QI7GoA3Jy_lh=M~D9HQV51&1g&M8P2n4pH#`XB32H9XFP( zsS02Pdfy$atbKec#_xZTX!*IfT-UcBf?RuF=VYJLIB;;Qt+fjEL#2tY-=FP;wR5T# zZQk!?-Pg^Sr{c(6O$CMCIA*MTx>BwEmobdR_-g2@i6CPp;*Oo)+Na3?)WC9`HqG5( z)0Jc83i1E*E`W-`TIK3X)%?3RJ>daxS!39*Rcn2gXdZ=s+UEb4EUbg5Wlk@+D=(K| z?_;?!$K#G;BKfnPG_P$~D=)t-+Wxp7Y_;}&YM^DEc9W=bl^bDg1JwcNA+S;a|EqA%uaYzw96Ggjkwm_8J9iKvB%>tktl z=6N3U7H!pd6|w0O{{VjwaLjl{!#L@g3K|qzeM%f`U8s|h#$P-Nu{9!8O^n3VT@En8 zg_2K&uy44Vo{l!<2bEXEI?KHsj{;?>gV`4v<(-aR7KFNYub+BwiBIH+yv+pqjyV0< z1K)7)1}<*C(f6KE+u=;RlY!Z2E|QAOkOyZ5+{P*W_-c)!C5`Zps#Dz@+3`0fgv%ez z#79`C!B$$Q=CR+ik$)Ld!yc%hn}X?|q9(u0936CqKohjBPwy;ib)WJ@y{O}=<{FAC zKh4Iu(jAj>FDnX+WYZZReNV2v+SD**hBd-y!)EQ!K{ug~fXf_UTtew!N2ti`e=TQ{ zsj)fO8`vTBq80aX53}|UZBly{oH5xxR}o@+!zsa{)iS#=*{qKht#1qp+=aqAoE?o*~rt|qdWbKgHV5MD$(o4G)QJ_Ca(&Gfg z7TGv6waCg}q&3XEpOh_BuU+fYI#_su@N>#4&Ka{sHBVrqtiwJm2_&vO(~*A6I56}T zXhFF95)bOR_eA4%;Ba&(Ik+>q|4#9H$i4tRzvoogCEiBKqwqfnM&`Wu2Eo^UfSyX37}{^v44ab^tPSwaiGaQ^KDl>u6OvR}2LA+KkL{6&Fnx zDJ2CfBOB^1&=i$?Nm0o+Ix%V7h)l?}^Mg60@i=d6?kRNHU|l$;gbte!bOW^<1F??+Sq*|7cv zt<1`}zw^{`mAs#gY+&VgHow`E0aMVF9GW%##$yA}pyVd; z9dg^kKqTgygDY94M45TYeMzk$HTd$bn`bupY#!>gEx|Oh z3@1lm%`lpA`f;{#0nUw|m>Ly#@9x=*Fn!LD0eLiaQh*_q+Z@dT*9|@KFpao7pzf%f{!&;kNPyPM%F}BNKc$J? z#F9=#Azis#hg%8PeQ_!W4tO-wX^YcgqC_|Ra0|Q&RCte zT0H@6J887jiM1i+gI+=tDtj_XC1bLuG00P`OI=jmGEPAXuK{Z3dontlNEn-{#}3?t z7*FXh4#GNP0KBYw6)s!|jX0rpf6(OB3@<9NPDxh8yPVhbg{0BXFCbr z13Xnh9$}kTPCtYV?pC=hnj}94s3)H$lFkUbMelpXtm0`TG(hf|jHl4_FFy&!Dvl z9-4(*g9S(O#e*J@anQBKAs@7Csjy0?rCbZS?`e;V0>!8$RBRf82$&cia_VS;(pc2! zob(6|P)v{O5JS*qE>V;gx`_v>$Fo&CQ#aKe*Y#9E;;ZlCoTrJ7I!Q3#s^?8l{nASg zjYD%h=&ywKCnOiM1MO2NwF>cpnc`ostV+W6gD_jRme0K*O+)JLn>C$_No@N+F-APf z+J(^r7ICHBTa*d7m;$TqzNQ~C=R?$D;4vL|WxH$%`al4|ujGDm`uTWXM)qK%i(ph-q8C^mdpn9b*=ZtjqJ&BhAUG za|Vs)*ARIgR{dIS%BJ6T(L7*a;A#&c*D~B_D{JrItFgT5MV`vaP8m6shM9b*)H=~! zlYKXJtEB|7Ia9*X5a6-Hrb%io(B--qBRZC(d_<#{+_jB9Y#GMh>XaZf(k1Qm*N5cl9-|q}_qn!X&pzI9k=S4|{bNt0k%hqZ@mc&(Z-XcRdam(UG@EmeA=_rysY963Mw( zhU1!K_oa;M4EV1i$2l@)GBg`rj-L9Kp>t>J?vw6|RW`792k;#0bZS27o>Wq^W#-0U zHWC9Em&a3Q)D!nF?Qn*-lt=;$ol|j3CKOcT@@ABwW-Bj|_uM}ZHDX* zPBw#QVv5v7)r5(h;aPWyT}x9@a=vHs#Q4TF-J(|0F2MOcHPJ3Q2ab=0;@Ss=2s-Lr z>gg#PcM4f~a{1fh9(X_tSerIq7b>fVBn8VgxKXpffEC9x>@=r0l<2^Vdm$I@na-vr zdq=%J^~8cpLtZY_)OYPVF!%K+tbw{aU5`n2mE*A{VnD;ROlnow$jjb^Ns;9jjQ^MX zgpzHS6`K|Xy1d7lugb#dc+Tr0z|X@F(6En>tRFm+XtQo%acMf;rnIFXHIw>MK7C$( zh9f9iktt@if>uk?srZUDi_8^17y~mO!{@74=|W?}k70EC4JSgZ-a9)l@+=l#EwWrD zD~vB$)(83+)%OJzylW)AdjNvML10+8-p-QuO#=IN{6mfOs0{|(D~xKX?IQPt(q)2& z*b4`BG0ha7BM=reLY$FVM?-*s<;Noz? z2ED9KwGCNmTdV64&*&^z!~Qi80t8&-|7=Pk;Cst*$uD}zj+}+LJDl7BHDoCsTF4-6 zl+RNHGjjhlz6{5;nqE|D%Qp}$G5w7N-Q)&i0TrJ^)G>l#Z=YIL-Rq-hL zEcgkm0()YXyTt|0BsWgn7Nb-2V30bRspx`1{=;xw_?7><1Ihvb_jLoVE1Z!Q{3z`O zo6asOb82%=3=<7eC| zItfX8jovZW-7WSm=Os>UeqcK-u47+(d*n;&J(gM1)4x~xzZU?mI}OzvFQ9d1#Hgrm zjR#`CUgPPeL7z-S({?y)S4d*0r{3%GecMeM8QTU#x5*A z&dMciKk~`H2xZF~QI&Mp`C_jq`X%Ai`>WL|Cp%ti3B}7Wd7BhW7q@e8^2+ar{-5># z_v|T?daRv!qT#Z|w-8gpvF&)Kgs!-SmQCm24-a&?${P3mv2{Q7Ks!d7(3HofOquMwJ>Ql0H+aT=Z`R^6<0E6~C-!ZSy1q3T}aHI-ac zEh?N4Q<%Vk=H9>Z92MT|Tdj)I@WSi3nna7}m(l=uME< zIT#`nV4xt~YY{gM&+GC*3dK1@klJ_V$dXswf63gp$kW_Rr;>c5=C00QP$E2x0o8C& zBzNGX?OvdRL+^Kx&uhHS-B&h{Q(cpMy>Q83Cq`M5qSkog6TMzk*PSC78jjkS$+WKOoeu~~j8)LgXW4q1MYd~2?drsd0gE(dDTCuN8Q^dul7jP2<@xVt^wID>bMANVXtE{MN*%r6oI zT>}CQv_d>zKq!Km?k(oW@^PK>K2cNnY&msf8Q2FGwm!mrrwo4am7#~Bx zfn_^z-EYf-clUn0AgBKrFMAMJL)Ot09uS0`kzh_ObMOBw;eQGwM!jMw5r+7!GH-1l zP*AVOWSkG|#1ph2e_^rz*$X;ERO-~Mo^TNKj;w5HdE!QR?+w#zN%K5fo474mz>)7$=$$-e0$MotSLwB;(6a^&s&?hroJ(<)c&%q}yykR>T96t5%<6XQBUIeQN z$?x&iln_j5{qPX?;7ydDD7(i0CH5MazIpi9 z!#mO%-u16_+Q|3DL$+FYn9GAS zyFXiP)}63_*e`eop?4k>%5@A5%6ueVeFAzf3USt^CUI>+1N-rqKG|rQYGV9B2o8_Q zrKz>ri<>&bZumCe=v}kPi;CRQ9`%eg-ABu>EqlgO`_M5w=kXFy}o;~5oc(@N*YYlis`bs~>_j6DA@&;Oq| z(dB})ZlJLIGhH^_{rG6odE8~JJ}D-i?F4!{4B=@1Qvk1kXN)JjyQ8GKi?}Wd2Zi>t z08){DahVpvau)$F@CfYKiP5yYl>U{>2%G*9f#PJlF{;0&$BarCO)`GHW|g%lBbf0T z{U^=54xNfD8}AD%S_s?xM5f{)*eUY|Sf4^FJc#A?3J3~_l*;DM!Qy28B?>;s{^fEy zR{m=s$OW(;G$uhio-N?syFqzIhMWUDXE)mJBa|PrUkl6(1Pm(WY_$w(<3*BYqm+;t|PAO#Nb-@PTD^$ zKM$mJ<$eyyh+wWLl}o&zzD2%{O(UynZ1vgf@=aXvbJU2L5AZkd?&}YBOdJK)u$2!1 zLX<>#D#E5R(2bXvFm+7NgbDoXD-C!-(`S#519&Wl%xRO$cm-1{ zw$(oTsLt?ybx}82AV3yogh<5iz7@UZ z^{a=SL_bl&w@nv?CSj2D?KQiT06hifcognQ2kT`47b#C2ok(X&&wF$4?F2*VoXVpC zcCVi;QddUB>ILgXWMLE63{9-a#i!#>A`^;+YF zk3JVZ{U?rV%4N-&jC2Ydr*c~`D~tKkAVU~k2x_4ff=RnDkyeC3xwEo12zN$hm>oOB zmA8pi#VqQ+67=2+#PF+m{J1jrX5p2Gugd7%3)Y$BWSYW8mV7PeJkAEkjk}0PI#s8?l6wHU4mnt=Z%rV18GTfab(I8byc*sM zm#4=`7lyEfY_$fRLW`-PFbV1u`Gi^k9>hA)-~Tv-MUC!q$BI9mCu;fG3HlTVx{cmj zmyFrO*MgiLPpLc$1NA!-j)sE%WT$$MgqHstXRqE$t6oPX*>NV*nzQrlUZx36fDQ7K z)$ve8%@}NBO?}RBSzwBYCg|*^pR$yBUu()DDN^@+KbA!{yO!_UeZTloR(!kqHQsKT zmDr1BraT3DyguiyfVe^r2F_GE08{r26J;lq(^K!~CuqM;xSu@Yk_g>@y1kB}31Z82?dFeu8bcpPC z)&S)>$U*J$#O@g?%D-{^>^(y^rkEqmdR_pkVCR!y!Y`fY(Y;=q3RL-~txx0Nx@CYm zy&ZbZ|C8mebm)@Yw|RyDS3kk*g0{9Ou8Hx}sxm==@1EnX<4v3jHyQ-rao(#<>v$*d zhmot#!y6NcZ#j&oUmDPAX61a#Vc_Xb#&3(m$|KMj7nJ1_)r!>q%~8srvz^YT7$}w% zP^fpRcL%bnRMmqWVUT_Y2q*ryS#~_`Gj5sJNH0-ch^LVi1{J41I3mh4rOPJW<{!_h zP_!o?@R#vYxK%wAc6rkABsBjEbipx=-$I~1O1a|8{tgIw?;%JE)T?zX?iQ^43Bm~3 zF;CAJ_x434!+B1P40au#$|CP{@63(xDFg`kqC5PS<`^K^KS=jBXK4BQxYF(Jw{t_c zx8p`vQH6G>(?p2}XrD*u3^Br-qU|!9Q`x^P@l`ZaJ)hcTz>y~*Ol^GZf7m$X$Jnq4 zK)|Yb{py-IGdb-!RoQ;LVsypNJ{jC3Je zN`xQtfF1BWXtcb8;*DB)2a}X{pd+=s+&%h6Tjd6p{H`dc#}|zPtxR(to-6~yDDl#N z%NDMC2IAD-=)Tl7V2QKFlJsN#+WJI(j&U?-h3hgwLbeuKwoBjrwv05uOz)z@SSeiQ z`KDjXS4?hEGv%QBGN5kA#9JpLYm`v^1rdNr$>4W;AKHVD!Tyj=5s(di5RGDf;YmyW zjssys^1C&eKr3o4GOe?+gh@dP)-GI>mrMF?Uo5g6U)JFFZA_Mf!U4i%#$fq z>BEhU7jMhm*n4_WWz7ZTO=5r;8vY(1l~G=u@Au0AnC&mRp6bM1F0N3Px3Ey8w~;Dy z8@=0&V;^riC!lk0d9gG}V9wo+ev>ZnC05~F(^;n{Qy{eKz1RR#bj%~qlYcTHV-#oT z_seYS){yeB$sBh;FZmUY8ly@iRCIrY>(W6&WENYh#85uG$C?xYDzyMOCU~v=wGO!M z1xVrLrx=C3Vh`8Krj865Qd54axI zf-mk@DR;1wzSofdD$}b0{M1FmwYOw$9?o^y(@?=1O$|YW6;*Y2&8_FZ)$SM&=&t}& z3-XL^C+Oqzu=E=GFhmaBPQXU*JK%0ubzd+9)*z3w@?fe z4;0torc-~Kf0fwe^_&+HUFS|q%VM4<9jhCHEQ5cW{z@>pO+dDDX?HDvu1VG;( zC{b}7VtCb^Q_p6mkv916Hvxo z3i3mKjUI)V@J-Mc4IDWud=wNrjd{0NUVZ*o-T)wkDjkio>>NxYb@8r=FQUm%dKU7v zNaYWK{t0hVE$XijK4!TRh3Q6lJ!VYTbEWf)v9qd2p~7*{1waMIm58!9-`~+x6EkZ! zCB8+YbC1Yr&5cb6h{0n;Accc_CO*?!3QpPHjT<%P#-8nsRA9D>#T+m|J`YBr?mQIN zg<@ulVkdvE`8hs9%(l%I)1cL~6pX*3_W*65jNr`aQX)EL7rfNI5CvH34i)sLiqcoV zR9HUAgqE+(Yb+XLj}}S0a!5D0j#OU%J^)+m$&Aeg+_RGnjT!cYjiAS0`1UhSiZZ$}hdu>@UN|@P z6-jt37*g01s3GL{rav2a*=dCR=Y)vq&xw(J?~NjFJUJC{Qphlz@tjQf(pYUIa%+fx zWA-ce5T~5?-P0HE>dt=4)tSZRFo41FL3j2?eG}_MM-q9I8<{nV)IaA)W3r_4H{U_! z>9@EAn-8XGk5MHv!dbN1Ml#u?K8XFuy{$ooe`Z{hcC&+ z-gw-~_n_s7XXh^QDVY-P)w^>RAS(#Vji}j?D)-ak)`-)WJ*NDsZ{q}&GKWF{jR-7&>-bD-T)UYh^ul?dkg=$4O~Ze+m9@pWsQ#2g zSL#deefQz=3p>b^EDgoE@wxs2GrekswNKnFU|Kd7CWVzIk4}kpo|88-X0|QpHkAOs z5E8;(vPhFMa4&_2cq>rVlrO*p#QG?aX7KgHao<03e_1|?S9PCaB46%n%q4INu)~ph za%N|_0t|1n1wPOf)!*e|Cv3F<@mHB8inuB6*p<4{CBGc*^z99ASo!M=nJ$`%jT_1Z zRA46AsaTJNlJ~91A!Gen2}#&el$v9Ttn{U%qJ8OBYz=Geb-K<2 zND(yVxIq-bqtvUdAWbJV{Pr97=H5?6%P*r6>398#8CIeP-*J}`R>T^O8#(FFrPFpQq>5B3S z+oN7KOQY2v8-{o_6Takxm2Lw!s*R7`@^Dt9JLcp+#s)%@A6s!N^ zOxo;{FKKa!wwB8GI$m6uWNqBJk#x~X@6k6+SyT!4c>HzMr*g2Scr8?0Jl2@bXIe5@ z+16`XDjB$idYZmgRx7r`y0s2F8IX}yN$GJ9RkA5w2j`w+f)m8Hvuz%Agj%bEJ|3(j z(#O?^slL6fN68#R<7R6UxnbGeni;Xdou47r_B<0^$e6uG!cuKZINfm!o;ViDiOkp5 zSlDo12zv%3<7zka^FtQ91Nnf$i0uTrXmUQ_Wq(<>l6)|6-ikRlJ@DSznZ`uhly`L& zTwnA_esaZLsWznmD=pUo*j>WV3KI}1p#}8|Ph{}PYV^!oP2)zNlV`JH9xfx7gKYLT z@anBSXGHR?1C<+mdCQ7l2iFvm@p$jn(5IfQ>I) z3EU$GP;#6%OyRBeV74O@Ly*cmwmde5gZc2gXXt(3rR5bEn;)q*tnvL6o)oFxuAaU& z9+nxL+HmxWK(6cP2MJ(>g(myW+!=M0iQbY7Xn6tyvEX}5(NCE-5_>ibR_(SC8S8}A zGNx0P_g7k4sv3!lD?{BOGRs#$pI`WNFA-8dEF{T&6sH&qKDy$SC%zkG-|4km6popad8%~GDjQM)0V?c${hMsF>MR28aAVCKWj&1l*ZFYh5n}Btr|m+rO|zl zPYN|pCt7iI$<61BTQ{Ni;9+AH8QV{;HZjUw+E>xEO4v$FPjJxW)NtDX!uhn0Z&EbH ze?ms)S=grx+wqv`qs02`!CiN&nbUYJys(oyHUysk6uO{n(GVQqKbM!=T)+K2hG>a?sscK zK~wo2SSIMO23nx1^4QN{*iG()v+aArROj?a;xsaQx#~G~qj$klbGvYly zl8uUu8UCCOuxA7##Xmk|$jHA2Kc;&lAa;urqaEG8R*obrR<&Ge-2Ec%gv;jpMyfh- zVfgvJJgmtx7gc0g{~!!hM-yKFI2wwzoqYGcke)vy*mJF+yR}23$>uK~9KBQNM)*eM z6fgJ=2u%g4eCLwPEZd)QaJmsO4a-g=r%w;ElO!lr#h4de@oky}H47d#55O0eb^DCf z_)IpE<{O(W{KigN0#?4*w_wcmEV4gOAGf=_%?_XN(wxrPB>t#SRGVKY>t<3W40Shu zdLDA!(}|p8Zo!lB3upc-S_`mn1$<30mhW%f=Yp=OBTm8rx+hqD-23i#&uV~dtkf?f z#rF4Sl$p=T_@lR(ymEu`_gigSzwdDr_5c;nkDzF~irvjeN4c9RR!y{SrCHo%P~3q^ zKxV(+symD5sBt`JASVJaY|Kv*Y5y!@Ls&LRi*QkKx-nOA`z<*H$AUkNhu~STSHQ+E z=|1SAK0ctHoQ&jTI?ksBTc7FHk=su@67$zKfrGdV)}LPKKJ`6IHHKsI-1v^l&Q?_; zJ79|R9VG3m$AW5}gjqR=7v~ArRntKwH32);d5RIppasCPPdXUUFC{A6G<56KSFe7( zTi}iqsw0yKCqE|w+!!<=pl@_pWO;jjKrA(IE^qR&P$7&z&HGpx`1YG{h30Whi2oV1 zqu!T3--_~EYxy81nm!5qUo5uwn0u`%OEjd5l9@G!QUc^ z)va9pFq0nC{NjRs^E-q0m@_7eoD~a2&$39L;|tri&nWnNd{}Ke7Z+->_4QY$LM@&( zhaU#|WP-)8!^)q7l+muxHayFxb6|r`a~qNwo*$Ft;AQ*dln8yIg|ErZw+zy4<-LaL z&gwNh{k;ocwpUz03Cs4fKPt?s+$J-_nCKI&LIQT0SsPernAnn1^*hQv79Hqzz#frg zZ#*5I+9|JGWlalIsNnwUhcNwt%KH`s-u?FDEsFWh+9wqABnM9#PXPMcB!&tyyUh~r znXI}lW>zVzurm=h36NOEGKVkpDKf4a!KGVIOqFtvWs%N#73*vr&)B*C0U%b*B*OCe z&f-1P;*b{Ea*tVJT!P)TV&>5XP$^pEimhFF<9lo38l{B=un9?*pwT?UFYY17-)N`R zeC|Tc)b^&4S2;=^oO6V%%x}&QL;`d%f|EF_YSWb}JDKcUNEgWwD4+>>K@Rn9>G{?S z7?jD&8i5*tW}enh2Obl2yEW1(J89ss1E(rLOUWK9X%QUnv^Nr8a_5v~>t`c*2_CaR z1mc-YES7TUgjJ1)1SMU*f9Du%;yI~uaCdpYlB2>rWYA)I9ADf$*znUmWIZKdqh$}( zLKcI97Ur%@07alw5EvvYW?f1fYRKYY-xU87#}w**U~SKYiDd-?nm0&*{weXKp`Mw!-k7`e^8&fx749gG8tAG@q-yHWYHHACc1u5KB{H?2w`ggw8@8S;VB zO3Yx`Z4sdS^saJN?J^|+d8;HPdlU-#aA$Yat2^SQlw-eYyxeeadzrPwa9_N2?&l&4 z3Cl8voBqFEIyOI`W@t8QSc~_-@+O8Ox5qB!2|J!2lTM)V+X@=K+)Xnv4N6b={$ez9 zA7NTB0|fYX?>AqE|4yd=XXX^3ms3GeREc|n%0ZeOL{yUZ8=#J_h;AtZs1%KBGr zvzdxnfhA<~y%n!iire=(S@j5Haml*!-j&a(b*J1}k$-7tA=|gkTlw@E1e^`Gr~QB= zc7}ibq2F|liYc%b1Flk)ybPsMxMH?{z|qj?hsV!IX6Fm-pWJ?8?3 zq2<}OD?VX1W%Usmnf(!$hXXj5n_X`s z{C4v~W2zD|aA#T!b%rBop`M ze+!fjGz+_-#1P6x38QHv)K0;Bu0RTp_*<-x-ljhVgiV$~ixqo$Id}eYvY{>t>wLVn z+Pras2JC+`KSwk1ArP`Ecc#f-qC_c^fan>CgzX~cQvw>04{FYI-UCsz>TVu^gz$@I zp6Dh|K$I@7H`_y;yS{E7m@|wOf)s2AVAq69kXHg`5RS<%yH2PqOnXb*iOW{I%XV8QXMI^*G#(PZutQSLR+$8 zl#+#QOpS~N?63v|IkPoCho~pWHtk&GCs_tiag6fn%{>&ovVB6j91aq?MhX7v>Z;EH z`T08#4n^`(?eaq)uORb5zh5X_J3}bF(iprcVU#`7ox!=(Kv)uY?$5WAL9hO7k5oeL z^=6F2v+97O<|cQJLlrwI-49 zLL40b4L6Bf&QaEI%NBjn|L#aV529tadqc(co2;C)(^Sps^AvJR+wPR~0UeSRS27Di zn9|4+&+jWou-b59=HiA!RL+VHjhB|S8;Swc)q|fr;@-&4;Xe_&G7-kiCk6V@GTOda zRen2UGsJk=DD32$Z-g!0k=#`7_t6hN9rY-M04+oS+QDi# z)e%!h^m~f;rLO5QryDD_xrV7qac_{NgY0w|H5}8H!)NyZJc;J{`-lwNMmwlELXk&D z*{@(KqB%YGJLZ?V(Cq}E0gA9BxAl`Cr+i^!$28-2m&AWoB@hNIci&raD~_iDap*Fh zq^eGYLN zt%W&cB~09PxO`#=)E3QtuEuwBVFO^oJE(TX{;Zr=I6J(0UgAb{x#{#T!|T9hF5!X>Whz55%{f;S7NeZSXXDvLT+bY zAdM?^(=O1|%tWa?00nCZ-^AI}T%n>suN)B_XPJ|RYJIWq#5iZZ#;;KlR&>Ytc)orZ zuncm&3}mvdjW-}upiI4}*fhPP9+MwdtfeU#PvnhQ zTm(vBJ_j}?>Ka16`c0-(w)s6zA6L%}+IZ}Ei|ss6uepH<11ZdnF9Y^Jvy|?=QIWO- zTm53?Nb@=Oy64`H-2ro=t`Y3^y)&erZQ41PO4gnFMhK7>lQ{~A>*j~cXCT!)6Q(Ib zG}U?aCk+RCLF)7cE5;QGuh&`De=6)$2>qZWPQ@B_?ihdYu-TcnBA=z?cEkfWW_zm3 z2w5eP$33p~Irp-22hhs{N$%8*gee4CTChq)u-!uCN87`)E_)#G{arQr50HIg0uzK% zlV=+TEjqj#RCz~k^{Jjc&n}$C7cfPJW0!4D!YA^U>pN%1{KnZi0%u?S)i`TgqF+}c zRtixU-V?D+-!KO{&PQI4zm;SiP5mx&se#mCJypo^HiY8NP-ieyu<{hN5MIUtZglcN z+t<9p-sv2!nPB6HlLj*b@oT&5Gm&O))x4dlvQl)A%Riq=T?PDqOS_WFxp!k^o<(xz zt7UErxGc-Ww%#SYqSMQc9w|4MO>$c^><8lk1XFM71zmq zr-bqfi--4z;|+8MHYq7XAY~I_WEsF~9XyT15OyH1`NWGgs4XLP@Y* zpk7EMwm)x_4cg0qr*4r2%V@B!`!yd^WCNc|S(P$; zZ6L90|743lZUVGC538U#hbmWpxWK03(eiLVS>AvOJ9Ea^o&vKfyYS%r>^9emsaGO+ z@9(KI%?dTgmh)buy+#E6p>!fP7P91$T}R(3N8cq>Da7 z@^23TcG0!=o&rJ91PCVV6mHc$IH<2r*A$Yc+^iIp?dNYqV#wuD9A_tUJo^!PG<^y6 z;iBaloQ^Zg>MBczt6+-V3o<2yb*&mZMcT+!<}LaO^KW!_UT_xnlhZ^tCUqwb%xa2m zB0qa02|IqWgKxF)6EXnLiJ)7#W;O|#*kbtv0vpj++QHdxs6WW9T^_D*zEx--!g?XU zyp0IV`k}#l!wB^_zKDSJUlBruAGpkeS6gici*3$3-YV$R(+Uxw)nrxA%Hih~V_bQ2 z(P%icbln9cLi6J<8eh}4XqZQsi|S`B)`PjS1l_0YiD+{6dZV-wEN%G;!;;tR`62TC zq>ER9?)F}?CCAbCATWWRRoIw?Pb-N9;1B6`E3vdkb3)$GT)4Mn9CqAus~HI3%&zt| zkWL|=$_go!QUN(s1dw|;)#Py+BCTTmel6vRM(A1YSB~B3o2@hS?_=UyejrQr)P3d5 zpQGjrs+wa^`#;5zdmA3a&4EbUXxTR*3E`d|lN^k&%X>svUeQKsugw$&QL+hyia z!ue_m^Q33XKQH>9Eus|H#z|XMHd_lrlYj*cyi8aqYe9pl)=rHLUnZCYFSZqDO|+{m z?Vv2yF8aH{$cg1@!*4xDUeCO3{YnfGLpIcXn^Oz6ICo&Th{!QD;*t9ozqlpBwYrz{ z8GI8JMkcY8Cxpnr!#W*gf5pW+(_9nUB%t8OpGIojX%)kKymIc6TW5H;q~xQ9+l|^t zi`4>*TNb(BjDT6x?}{2~AAhYQ!5_eL4$xX5N`E=MS}G(o^|(hw(>F+fvw=^U_LV)9w$G&YMls`nIAhPf7Wjo`?UTH6^O0gME%LO*lcEjo#)*0;$zZ})+a4R zKyXZn3OrgEFNIU$0^sSazV2jQl%#wP-$c(3OH6Ldh)(ha#^;vL`P4Nx_wxsQoi!Pk z{1uR0m?4n2Fe~|U$E5@MUJx&VB4;D<0yI+!48`6NqL>ap0#SPUiD?{bNr`Y(TNw&*H(WJ+NU`6ZY6?Ck}@h}XDzXj zO|*UkC+$Pz|WP^EImw(MInQjvsg?l(1T_;hL5XB#(ZV zRI`9428y@;T^IvW*+3n$d-NgT5zn=a@z8=c;vQ;5CpD@j3l^e0Gx$h&3 zWf%g+l%p6|rly}+ET8DWetb20u}|_s*$Re$>Z5}YW?Hbw*v7R`(yZ`e`IeF!EZ@9Z zQY%UPJROY(OVjR5hSmLt1vgP$U0(CKWm7{S1%vM%cw@W+UcxB~`088DgV@{WW%OU1 zmv9OA1+rj1NTf7$pByK;_U{(p_x-FMs1lcjkft@3YY3W{9rxj^_hH6(x;tQK9x1v1d#>D-nbi0;OK zAP1S6;xKeD4K_3Mh3XGPvDhnKCAslXNP0z&)F}=DVi@oMkS&Ld>v)(9qidvd)}(~x z$;2k0SD>XXU>$=eE!QqL6#K9KCfNFw00 zbbmr;hw@fw(F@lQP;&{7+Y@K8@nOxgR`vNxUabW{S;yw5%79;SKZ{4}$g_tL46F}R z+-fcW^>%J^pebOoS6_!wF=)%>3-9Gzc4((!2j_OfIkvp=p_ayP^*pNm@~0K zv|8;E2S=)&8B9V|QKBjCbWHotE593>|0Da-%LLw~yEB9OAwC$N0av`V$GdeZCq|QO zA$L`ShGl43SHMEY9sfmTWD1UYtYJ;u+q9w7gMX#CGXc( zBSP(NbR+<62&+<5i05d}GqBrI6qr_TsL4Ygt2V`lRp^y*KYrquvSyCGl~NzH&BPh9 zV-w&u*j4*MVoF6Xp%Zv6h>{UWzIZ{{xEO8@q#Y-Lya*!|&5=*vZX26iwH(*=eSIfu zs9S}*ntG9DYdeQiBQA#*7rCdE*5$f|GHT!S#eEpaljmAPc8VR8mAMy*nByG$dacN1 zxkEYAOA+x&>UiMGt2(=6I($aXM?J|;h?!o3MBVtHH?b|!-vSD>;)z%_DD3YIG%^A8 zbfY9$FNe49-+xYx`D(H$)iYXl3wq>6Z8-81V5RJEsdl!h;&Oyw+Ir_%|HZ{RJuV)>24Sy-QC^YImVvvIKOkA z|2y9(a_^4obJZdhKWKgIkC{4PRJ|qx1n0}iPmvybmUOXtgHS6teB#n|XDW*Flvi+Q z%)n;KOG$TLbaSu!&I2<1YZC>{`^mqLj`#(^@KHnVQkC$t67Y4C{T{n*tgywAcu%#W zF8vH8er|tx^sq~y(kS1=v(GOc|0_y&VYMW) z=kUe7BHPu1Vc#aacJk4{oKK7#KFaeP-s!MQ!cvIcY^7CC9=wYZER2cTy&D79VTL9U ziA9GG1f=aN0Pv#5o>Gh(w4Np2gN7>j2DsTGhSw_ayYT=}ESy@)f~jd=T!};W9F{y_ zX{r4TvMHu^m#Hl2^lWH#>Cb_-bA2OS%V@SjJM3zSauLvVKM(LueLWi#;sBV1U^M-? zpVilx6%iqfH&vv=fkS^HvrD{KUN1xB4Bd{Tt%@skb#t9`^`Eq)N9OqOZ|d0`cb!eY z`Cm5ShY0d5xf>${+(%x!Cto7786I<~z$-h46h(eTQMYxrUwXtbqb2W(Z5KC>3tX;_ zfMzs3DzZ2^Ic6pBzGX{bco<=t``LD#nzkISs zMr+p7U(3jU!E#YPR~mUfC@u!EF!58ydTPAE?8= z=j#mw)kP0$DqWDDEfj6^2&OWItCGMdZH-1u;5lA=Ah;O+&iqu1AuhU;DDtLcbGuW4`<`7`q^lbFGt9!m4AI(J#$Q#drtBW}(jZ_i{-jO}~h;B6M zkasYcR+PR2N}MKY)!w-)?;Wb9RCpVyoY{q1lQ|U&>N)AbPuIg`Al=7o@X{j- zB6J5^j0nkc4f!~7Ln#E3-kx2f6i7oK9;Uv}7n>``pV!Si-DuSTJ>3s&{Y0VG&%%@t-tM|wci-UhIqkQ{)=l=uS? z)P?u;N-)LKch3MJQilomd$m#aNyYQ@zBbF*u7_1c-vnzxFAXd$(fr7szSQv(mqDa7 z)xLOBAZsd%KO>*;5_NPoPC={eb;_a`9!!-47?|im7wwSDj>m?8(WH2vSCnXprK`FW zS$C(hGRElaBnniZRrDBvoe=1?oUSF1*zkFymR7%{Li(2b8EXH41XY^VuQFa=22#l7 zjaA)#_7GU60z{T^u<#h8lzS0!w0Ch|9EX}`|8q~6K9CB;{VxX`OP8@^F0_2mC?qYY z0k{)G%!9qTE{dx+_;Pzfxbagme9*~6IYjb!hK4U)dM>`_#J*_yFC?KVifp$=yjYZd z>Fb#WLl?j26d$e(xd}6 zRz8*G1IRorgw7G0@frtNfN^Afkv& zXW5v%3XOedzqaR?n_1=IEZP1k3AQn)uI~!D~}HwVA#P?MoRO6h?cn ztn5QuVWN@&qW>|q{-^k|;7C~V{vVhay#Z5rT)nIgH#*IEv?xm|?Lh2d+;&CDUp>2zv_|KKqORD%S?l-<<*V;!VA<)|tH^Q^#Vk5Bf z{MDo=ZXpa+7;9^PrXpJBHKot_NU3>Gl=+9jH}?k+Pb<+K$C{3!E0&*&afe51$|`F- z9v{@9Kd{8MDz(dicc~0j{24KgT(Ecmkz>E2wzjD6h&YSJ!rX z_}4G{rRHCpAgax52aBODEEdm!CQ32A{-Ps7mW=Qt%1Gif-;VmYv4hTT#25)&!=z+^ zcYt`$9Y!f8dcL#PYW3a=qt<^P6#9ICJRMIAOD%9JeCSV29O?FKTJauP7VL=Wc}wQ$ z{71JAaP;D*ayq|~+)sN&rKQ&k_gl_g_6m~O)>Z`#Yc1T*ytQwW z&P8?zHmt&TpTDMfgz`T`9}_BWGuCD6bamk+XlEqB-$M&W@9uJn@~wenQu)EghWL&p zry12g3g$}4@tT~j_pDfoL3P{#x`q&v)0>TSbZOMYlO8rCa}OAs)?HvcNG7}H<*G-yg+nthcFReTk3pOHjGdW&v<++}H%p)Sk7ekJUpC0I5Sf|jr z)w!kT*%bWyB6yoyY>2?6dOfRWlv(HGjs5TRli`kf zp1jvBS5o7ZwT|m-e-Kt8U14wX_1tMkaK|VHVl`{00c#ukVfyc@7xPVg&DGlmxCGeK zGR^J>Tw(3B@ZkJ2xc&KJ{Knc&(bRX2Hs5dCp~F?)wK^4)G`cI6tFNky7>(nOAaGxV zHzul;?`(4b%f-dzJ4AJ3>=sLNTTzTr%S0FJsxP+ix{=TAXftBQ&4$uZxv&f@#yCTR zplG}>uh9|g6qmSo?2oul_CqMDv}~*`7@x2rsowNkw$z( z&Qj29C_FjxDSq`#Ie7!^doDu*!h1uHRNQnq`xy|Qg6tOZC%^`eb&DW5qyr}&tlmUP z4R3^pD$w1vZp+^B3bg)=+4mJ2`qktQ|&+@jb!f;%PhNQQ(J52f16v1Zn+wI z$R3}z)+&05elsr_)cW5E8asfYyeAl z))!>&1c@R{@wa@?q=tSzBD%(XE_2uDeA4i zkAczU0)R08-$tI!hMb>4#~WMAX0}UK-k^4?diqo%0=3wB4f5p@2%=U&mu0RIv;Kp=*33s>L2c_zp$N;(+KX4gdNlN#9N-8fD(Hl7519eznZ z7o0*^p|ZqnKhve+Bg7ae&qp;(0EjcrUn$Z9#eZVtaaC4)DEf3;RVde`|I==2)Pip( zJT{62tJy7pXU9b|k;Ne`WRW)0Y|GOGO`kd}R@HvZcST@hmSey6LWCtT?s9X+if;>% zT;N}Hr}e&SSVCB?)gNAS}2<^MKchAYN)O?`o*kp3;{j>71Dlu*x^O|d;T(76*4 z+=7y|zw<6hGdd&n^ zC$G+&bgu<-T8F#_<6c#%z)^Zc@5_^}A9FW%l)u^ffdBCklBc;Y(sy+#Rc7RS2iA$I zId_98Rs*^B)V5Y@mfRloEN;PMjayF7Ks)^(=)MPnfN+QMUda?-#wvjgYqx$S|akS*w9;@;im{wVj10^ks!sV{^KpR~ZKC<1HP z?kTrnh29(!T1ZiP529i>ITEaT_T+F74rfcHyNKWaQNp`NM}#XYtUz)^@1s{@xl=_R z^WSAj4q(@bX{Vzi%axOTNs)<5%Dknyvkk1;w_5<1uzR=4{c_%ul7$Tk+AD0l37CDA zY7s_yfOaK(nnK{im)+#?y~OaE^+s#(6TRY)d%&EaftJ%w8Di;DgrS4(4BpUt_9$M~ zZ)n+VSb%9<9|&Wy}k~eHA4Hzlz&U`~=DAkHxZ>#ujBy?7{onEdhFs@hCH)6Yl!T%8?DR&@+sxfDf2T zvgSx+1MF6HgC{!&U*nZE2E#TFNn}1AUF+B_XuxL}f54v3id~-eaWGa`h+d4Vn{4#P zN^Fdo$29hGK@+VQ0)5j~xh*Jj&wgGsx}$-h5l-fpJ99Ngx#fA=0qaLu^10M98}eFI z!*;-qt0X0-{3z# zx6%HesrGZR&&4k+BrBT~Jd*Q+h8%6DrLA!KQK7bM_8 zL2JI*XA|!PQ|zE&2SNZH7k@MYPs9XuV``Tp0ZlzWlI?5r*&jW@Ln2V^$XqX2Mg|Cj58E>pj5s4vF>l5~J3A9=JU2ZH^l>Mv<8v}ee+3O4jC%Vp zOXF~pimQJmL$sl(YNDQt94WUjko#LPB)OCNG#B=q=(sfUgE1NPjoSt4O?=R{SF^(Y-H>afrJ^VS&vF1BN z*op(VH~tH-zZ64)bKc6lLJq=+3$f`g3NM@O#U?MVj=0lk4<`$O$W8BOTKNmd zGk~v-3#$dtrJis0ClVs298TB$X~2CvSMpu}qgL_V%qvdWRJe$y7A)&kQrw{(+fb>- z&Nm5=iEaY#Yg!|*mq3G@08VNz9It9ORz(? zha}xtjuI9y)+m0sIrriNCJlixtz9q8Pigxs)kCjkw6@3WwMk1dSV(-sAGut!4KK~MFhkBlByCuudL~>EW1c&y;J63x$+%E`=H*=`m zEcJ+T7B-@;bIZlmG_FMmwITEwA~xNa{x`czI(PEH>BMbqZ1I zR3KjCc0^o)t!DnoA~2O6hueG8rhIP0?0JH#BkNX_)rvzEtH5Yw6sv&ObXWmoW7L3L zS%W$@kwUT1{vStY&K5qf+sDuc>xnoT=61uZSfxY$`>DOgBQ>-8!)lZnb*bAb89k^7 z&`8oe^`o-!NbuK?O7qd$Q7r_H1x6Y5+Nn&{y1pOGoT!Q4RGOJ-XMK@OLdmeebP->A zNx&%+X->wrz~m$EDQT@bbj(c;O~|UrUmCsJ&%-A+@9M%btS2WNVw(3{g4a0kc zfTG=W%_As$nq1m$>&6>$>4m#KpR;FY@6AE}&*)D@i#mD)22Tmq4livj@IAJ?~DN9==dxhF07cyMUNorTal2M({FX2H`<%bP%p!u z0Uv~U@!U+jeR?zczQ($L!MKl;MuhnzWGP1RfsAe0Ix5rSfV*bjD@fk8)k@MkYdL%?*ZW!<= z+;*xD;+yjcvE%K~@*46hU{wjJXJXx}TksS(Z4LItelB78U^`J3!o$vm75Pvc1UTe; z>>G-5y@_7w`9l$XrnjasT$jNMeg%AR+IpK78lyO^aVwe$y zhq3MtstOQ}q4Dqoh?8yfIo#iw^ePaK@9Wai6Uj31UIw|mpZ3q%%B5RyX#2?FeEdIv zgzC+5Ye4%WVtsrBqi@ax#M+(bh*^mJr{7P&hqWFdv1$sijo+IwCwDzXU5=oRi0H<*hlsbIyh1)K)Yg(@_6-Gh7uY(*Xcli1| zinB!F@O5P-Q(D0S+9s4gfjXnLLBQkZWGcrySG(BxnUH6ZegW^AnMJa0gVQ_ab#LKG zcSAhUJ<~f!EjDmGO4?LffghF`>mwOy3+)HB?sj)ZlfOR1|DMiYyQfTJUSJWSFM6_d zZIBWF&%mn@C4M`%?3o-;>zQ{3rmX4_V>9Z>y+{+fN4Em2K;N+zdZY57NInEd0C5vR z!9{1YYVyf#fmyMj)SAlQd*itd$90hb2O=h*C*@yBR$n@ihzhsT%pmIV@aspYf|D?kJ3fTmuE}#aExnZeNq}j1isr7XjpwE_$L?|HB_LlH3y47vc zKl^(1z5BzNOHy607TkgXmocLPvNzzDz!UM>TrDdMp>fm6a`Z7NeNh&)q!&Ji;(SM8 zOYC|uJE6R#$;^$8bS?DI+X?c2{ABp53{nvpeXk>^?ufop%<37wHmb7Ni7N#jr2Q@u z+F}wFfVmju%LX&lr4kTt@kc%poL#6h_cK}_`8$(Zga9IWDNi9k2F7i2LodQ!KyDJfyBPv2=lo-52Ha~=5#X_J z8!Llgtht`IL(Vy$r)6y5W)c0O-q6zpc9xs=+|%(j!&A`^vS)YzCfUHK<$8GMzM<;V zI)Q*tR4f`~P%6~)?)qc}sKd~3EjQCC0BI*FhW&_;04YaC78x|~3B%6ERsewo3t<3k zr;pcv0;D_NFV*Hd*MC~Mx9iTqMT->O&Uf7T4Z;zkATtrxEYG|_5gCi%8iajQ*&uj0 z%5uoJ6t1~{WR&2U>m#ilmo{~4C$c-LJ+w<;TBV;v@)TtUgOXE}Y)3+zRBj(~_=q<|}f#+ThydH|X63P6FzoW6E5M^m~c4C^0f;|}B zDJO=%rPnc&+5L|-s4Ug*m&oXN-qu;QR*(D>}W4qbig4tY?4I04L z58N(Th?0s`y%gS0S^#tOiL-5K>n%j9ZZ!4+njl9sAi7GHNIe&9Z*-5wlsp{E377(b z5yKj5!*Y}UO5gyguc+}_I9~6gq&Upv&=M&v@WGQ=)$~2R+N1^iVlp1^xO&!*; z=J-c>(ZK0IMMO@-1u(aT>L7<#N2A19qVEKDKfCe`#4yN%V~Au6=+R^97k8^V6ht3O zT`?tpuc-4pKj$7zC|{_pw&&5C5tHmS!1qgI)OII-zqXa%Fs}WA!RH;&67trO(dW+q zyu^^>Kw<~H3!nUN4GACC5E;&}pUDJAw;qRYRl%8j81drJtmNLR+}N|I=huc~=CQT` zlHgwMoe@wSpgJ=S&G(KFy5G6u|8A26w>%e%qZ@=G`e9yQPMcLEPFLslk|Z<2D6OI@ z-+Rpe)MDQDyGmSTX&uI zS*iqx9Ls$zK8>Xi$A%igKfJpI5_^HhwIpszg4CmZ^}lv=0xHOCuh&uQ0d3K}?Cnh5OK zA1=F5wU7b5$^ttgOlW}{3>Z`v2S4Gi#pDbo8??*f(JFFvn8?FtPle#ogA^OKdCp1B zznnAH;TGxY>`p>s@IG9-I%VBA2Fgz#wk18ok4X-FHz%rTs;UYZS#7H&(YjV^%OiRt z+1zc$kyk)5i7@Sk(~tDi3{-3wS+t#B#RgZ||gb0uUzY;!HU|SitPHw7|r#h@9+F zRuv^AUs#yd}A^d_sf_a+Aw};N?D>I`*S*`Ct zM1F-r|3f&~`<9~em#>A+jo^N0zMYcmZ>1S@fD&ahCB5SBs zB^;x!S;ep3lgEpU-#}zisLSrr_G4oFdg)mW8(+_Z;V?v#&qHg#2jpWVP4){>=111c z+N@S`&UdTq9PySs&>$aHnN7hmpXXWaopJGXwNmwJR?TvA6BE~v0_O+mMqZf;W~XvEHha{2&V4_4VgE%4E|b3oe?l&1 zFg2}xaNz%r7%FjIzwWV&CWJY?!~@`kV@mi5<;r))Gz?1bV+q){Lww-I$a-Awr!DlE zK34xW$8!7EmLHay5UZ{2(&94M8@Mk{MTnXwVgCEMEGnw5ObZUGj@+mC`ry zcsZXe{b?Y)8_l~uk(X~T{!9zm>1R*Mhc+Bmx7UExO3$RuC>QC+sZ&Poc&j zAEFAXT*wDj4LRv$M@4!D1`8OrG7V=&bF}ns_ea?gaOF$?Z7=QMQbz&=v6TSC?XP!K zz%PJk-U~jv^W7jJ*0@0QKhx;H9mDL)1~~oS+p%-NRXjdS-@^+JBw=Pyy>r5GX8?PrXUW}`KH2#M84vBGL|MP~COc-3MUVc`=`q<~slnyue zh$GII5oQP!I4fkc6~UB->n!-oT*p^TMj`T+3TbiVeYi+d{0wq%=B7WEOck_g=d5); z8)_E;bdlHau=R{6SQb#v&Z2X%*%^f*d=%Bp;v=4_h`6jXHZdz}nv(7rUOz7^aSodu zPS+s4HSd8C1WW`S7V}j$Tkpj$7I;ijs~feedvYY5 zP-7PZ^i*)9cx)!F=q=r9y#~r20;JQYKtFBFM=Xr3X#67SYGOn*_dR3#XmAYOwdR7I zZJH}3tfy>1w<)BHJUrM43e!5mui4B%=InDUIEpN5Mkb%(>|=~uHZ(go+bT+kh`Wka zyw{_2^a5wc)T;8UMSP1sz3mskwgF?EARM){QpxV~7o?c*$T^p?s$Kt?K4DH_@L{Wb zN?k9JesBWyXC>eUolpTThLR!KPY}GZp__)KzNWArlxs9q$tqK>;HE8$i`)E##@sB0 z(HkSYT5O@DKx)NgKDK%HweS@LSqzL6@^^~t!9XKM1X#gBGw7*9sOgi>0-cUR0KjN3 zC~4O1@3UOd#ftexDH|yt8nYB7l66nPvT;!XisligvIhu}ZoThOlo-xG{J{M#m@+i! z^svx%HRigT;)mv=T+s_A3(NwbV5+6SBn1`8eMtfoch%ssM-(A?yZw+?K{Q{TAI$L! z7!KSNA7am0u-p9-)kRc(xo_M(U+vhXXpcY0UhY%27JBf`{BONmy*0t(jZw`} zwJ0I`vVFqoe7bw7dWCxJ{B6&Ror-nkv_+t_SLmK&>|`XrU1N`Xqkc+ohhYcW8yt`J z@c4~C^*@I;i4GOn#shEP6)@1nMowLV^K8ApR0c0PMZ(jpxxxLSxNfh_HmIpCO)hpP zOh&WhR&tW<9@{sbNKaQ8LMQMFH-^uS&nE6U zF895V>HHOC-}se2lF({@p~FKJ8nSJM8G-@UpuoVhAeJJKI`;o&gC|Aw%v|lQxWzM! za(jOpbChyIWYqb4h`mz!U}}zKj#rN5=HlFzw`lI(A~1z%!Sd^OQ`D1ZPw`?RwL7Ll z6ERvYgjPcszcJY~{28vnR(qRH2sUgCOepe5Ee~#i%AY!!?vCTEK1yZce!BYvMflFB z>)X{6_1w*0V1|+uNqqS zb6eNpKMkyDZ%XJRLZ6iywFqo8a;X!S;vj1N^qmHHAN;URoBiJ}>3Umwk-g9Y;9s)~ zQ_90Z)$LbHU@`k@iE1cV)w}vyZDJ|bF_jESriI`o%?}Tp$^=5lXH+u(Bx*mJ-`$-d zLr*BFHbwb)O#gm+xxK|8>5@k9!GgBCq)N+-m-i7)M-kxGJCXGRNJ(~^ngv&fQ<>EE z39A_y3277dMAQ~Lkb;IG@k{E$JAKwmIZ&z=^F1~2qq=F3tyQM9>Lc;Ndi~-qY0j?; z|0tp+P&aGdHMH>xb$m7lK^0xUb!gUdbq^Vf&D~MHTomu8d!M^c>v!T5ksx^S>BK2t zMiDnp;19(K_^ZyzP#GENcQN`LWrqPczdc-MFiAx64%`+CFNvmI#(0ffCzCO}SmSaG z&HxE2R)?tbBnXQ;6j5Kc!rAkAv5{2(B2V9S#=@ZwUbE=Qf_t27``z3@xp5}0{$=9* z%nRCfsDmeQZaVPMgMD7v{wX{5GFaLohgn7J7g`komtu<*Y3iymEQf zS+1`Hdgw%IzG^y>LZZy>W17R@BqiOzb~~t=im>6XQd>v#i6F9??sP3cV-e-;)zK06 zp3txEYpQV}2nhNYNIOyQ++!X_M>oP_bzW*C9BSoETPs zqvtz+MZPIcIXvTH86a`iYV5GTmI4HYLm--igOLB`PvJlh7&fK%@axF%NAxl1)pU0@ z)RJA|>w+SL6(ZBv=y^iFc|??6^soMLA%FB}km}+~?d0cv??ixT9XMi~kCr(@m_C?w5DL?(txpbqv$CXMuB1REz!IV_HpI<^S9F*FJQITX%sx z!;dZ9j;OP=60e-J;^~yUsU7z7)p$hHIh|*lOS^xQapY=`KMvA=-sk zXd+MB+o+$bk-Ik$ApxJ#)Rr)k-H^w&{>pq$x5H^~3|P5w0(RaVwdzNxS!cf0+T$Z? zqhZUiv1K@aHu@0@+-~(bEZ*feM41)PKf!KyBL=>|`nB2xZ} z&qWut>xAj=;~0>Iiwiq6)9URj!!&+qcWA~~{AT&lp1<1M2at!I z;xl9gBV&}BK9?qqem}OuO4K2?>oVjZx95HqRIaqI0$_xHlS31Sd+ksPFWvHuQ1AZ~ zs%vv5h;sOf_hp0Z%9{X6mwF0)+Q{VhMkzb!DQFFpL-kiTbKsoNzI@+&yvPU z5{4?DD5~(i9PuzFf&Y4CSeqC}=m~^*fsc%s|wDBUWe|PJQ zVwiX$=c?{NU0|lt7rE!t;TqB>HtuNfx?;EA5nsY>csc@kbvm*E^1~!EobNGEKlTyP zt8n_hk~{WKbO%_Yt|_?-+cOQg$uoQ~YELqKu5acd#M1~$HMtaGde9t-!*qd$CK>nh z5t;V3K&;G+bWM{ue_J#yHW5b>M_vg!;|$pb42~XQ-GJ=AF z{BT|ezx`!Wk={iH5EY@GVGJT6+_}q80 z6}TF*V=;F&X_k{(2K`w2>5yvEx?^J97Cjg_*N8tHCoWJVkR2m@*8l;;Z-?Xx zG$elo0?It1C5&P9G$nkPez)DIIK7ww-*)#gZhVZU^tz88A1re!?LdQzSwx?RJG^vX zqXQK;(17oi9-bv@f4^BStrD<=cV)tNka9Q%RcMlMuW_ML*tRC9d)Bv&t^_M`!2vai zN?ha@MXSwNzqN0P5b`hi-{HTDbn~kBbbE~Ve=k4$qT-+IzSsWS1;9Y?F;rB?Js8vU zQ_m!)?!oe_Q|OmkBbw%q$z`^1ZcOFQCj-m8m(A(0eM^K}I5ZSoDxr!d2gT>3*(YQ! z2-_%y^*q9=<$)WUxF^irm$>+1Pp5lc>^`%FO#3;>YUtWrP9N^|Z6~+4owEuclLz{# zfVN+nix|pKK{2RgOBZ&@UTqbYe_`h50V+{B?dEH1VZ_{DfK-x~5Pc$*oqX0)B~sOJ z_N)Gnn+YJawmn-R?qi9aOdGXx{5ydc#vLKgPHgylVeybV3t{vKS|ry6m#JDaQ0+!) zOHRS*_a~h-Or*VmmEVxaQF=x^`Lw1iL(`u@I3QX>o0_$cOMeU*u}EZUjo#|fJ4 z6>{LP_5n+Y+ao0Zny_bj6|8VFGwR^>bFS%_b+?(1!0;fG!Ucy(#NY$Uc0XCPij6pc z-oW#x>8WCMU6garU8yk|@B`q}bi3hs!PA}7Y-g14Yni{TyTLP`YSEKfkC6RD#%&+9>whnMc{9S352|=@%8IVJa^=C+ELvL+@d*t;8d-^9(L4&?aHWdSH z$1Cj*h2AxHZ(0Uus52@iL==4yE!wpMByECiK!kYC&|y%gNeMYaxYeI+Az=a=7l^X5 zFncM60iqi^vJ5hr$NJ9|r*x?Pd*H9F?ccFZ2Fj1k#|EK~c3&O52K5HR)7~!@n7o`( zx;Wo@d}%PT09|K~T@u^Sa;EcZBY+s5$bpPk5F2nt!p~LVPb!tDQe$z#g!wquK9uW20qdHe^z3V@YA5~`;^Ek)wGj#Mf%v^CSh>zB^+d~wp69w-GomUflF13=}R za#Hu|8>UlawapK-9hV9>TR%!C%qom_9JO&3rYhDk#xaoinI|x0BRdJYnUMP#Qm8-h znG|D~d|vPV`IF?S^z;DpPh!$}(Sypdwm-RPFAxSJ^vkw$4!O=46Z>7~(8y@(&{rJEhEJT6T!fKftA?4@UO$SOg8Jyyb`8Gvl&)lqP z;rhFgpQwWOXjeF1kATVdQRIo{ow5{x7uCZi6~y(Y4!M1Ea%-BNx8U&Fs~<63c%go* z_}K8V9c@a~GjinN3Y(=hzNM)*%Pu2!ez(7~okyBk(R=jfl^|=q&A8Zz+gRx-WYpV2 zgp4l*0SjiGOAn0KS|DP5LJ$=lUh3cWECj{gKF#oQ`SQwSJ*f>I4eNx5>G|sLPNt&^|O0K zMr5DJ(FEbIysR zFo#gsD7Yb0nJ}|pGw zA3T*_r~JlG2&FqvmRb_q1okZN8^7vMzA*n%4*5&R0OxSOi;sx+`4ZTiv`-BG_*v~u zQ#&c_$}lS96Qz{faGOKn)RIAOlR2uFQl38=ov;MNBsD2>Lr84MS+}g7_T@ z%<^cgqNW3$wYa+w!fcVMhMs&(iYIE~jVkU^JfHV^?iqE6$2t`F%8ZM`O~?r~)PmDE29 zS{p0<_jVjm9sNLW{yu)C_jtojni}~iPc3sbPBjH*2Q~XglAPaQ)Yn+w? z9b-WV1c1ou1F>ty$Jbw_mSw0m@3>J-KIicxqccbWXqr@wz?{Z!09(c+1&BHTtL=-Q zAb#?fP~~m^E>7m)Acs7Y%PSp9te`lTtc-}Miz`h&#=QITa%@^G8DtF?R&mR%9>^8~ z!H5T9U~ATnb%=D#h(zbL^?Pz6mY4Tod6%AyRSP^^s6TX2hh;_tVv*d*ozR*-05WC&Z%tU^B&&tYv10-U-G|32|GZA-$Vjv7uTU)FL-+~CitBtC@KX@jv;t!|Xe4eV$3UKR z%Tf13-zecB#yBr>U^SKqVQPi81iHwhRE;6&<#d}R#Ync?u}R(^ddjyNIOM--{92E_3FKn|il@@bi6nHh= z_X>n~{ZdH!6}3M!efW;wE62^9Y6P4dhBtz5z><=Zh%sb!M%yyo$F-kP&-WGS3)4@m zZ9lJAA73A^v6Siojf)!C=y()lK&5_Dp17kZS%NU|A($Y(BLe7Rz)pwXZM6eXW6!be zt(Qabx=#%6?yCzzX0>tcXt3DCL_kO8#WnIt`)c+o_@&LbvQ>@q{~n8c0@dU|2@-?- zf>o8L#T&2x*{B|K)wBlC;wOMwj76)(CYIidmARP^3$Oo>G)*w~d1Pc0GTm)_SRs9fPIA}P z8Cpb~Y_E(-DpGw5{)4@M7B}|h(nbKeN*t6K{SQCIM%^MiZ*#J@dhu6@m)yfV5#M#n z2n;LjVMs=NSBcaLK0VUPYlW}IxZG!TKwtbT7(@5kZ|qm@A^`T?=BkMIPNMNM^~-29 z?Fd<2{uFn*v{?Bia3ghQ_$8L&V?Pf*pla!OjS~J^F;$Qk;pICaD`TY~88J)|JoN$k zY=vSpG3KeE9IH>HFUWB0NC(y-;@lG@8rKcZk*n=tao}fd7JGPwUxk207GI$D6TWab z;qNGpo-jD^3L^Kf7x$|U=R4GSb(+*?iQI~K6h}k-oaNTJ6TT}h{LRb;9~QI@bL`Fp z)f={-Yw2SUccz)v7m9=;-Q_Z1l={E`MxFFQ`UEPdS!S(=VLDBNu1B0$uK}Cm)0(?2 z9g!y&KQxc9z0HoykU^-*q&B4zDZ;HSwMDo5VJ%pCG<;GMXNtJ@XAs(@X|b{s)4hQm z@Xy}xa0)%2%$5GfqyHdvdHj!+Y38 z@c%W^Tv(D?GPXct;nA9M5+lmc9TsT>{&UVRkSo=0r(eF(gKycnWhH7lrkB+ngoMX%drn^BQWjkp1b%>@H-n`GDWTNTKi-SQPT+v zE6Udu#Po_yF34l&A6S~AvCSNo(-%OU(T*VeDjwsZ6p-50nqXG|+;Nc0qz>a|>_OA? zY1&r*ZPAt-=-qKUBHs;Qz2B>UiL7kWrBjX3E@&f#pD2sL&L8S_fp^^E-K*Wx-dRQD zD7a8Es$cTCN@IP}u9p}I4OEX2YzRTYF#b( zx>E$CLl`X~2m|R*B$RHDM!LJZ1Q`t@lrHJm^O^VW`}_sUc6QD?uIqJK^mc#Kn*>_3 zv*JYPWz}ggv4|`f0>!FEJ8Pp3rn2m!o_HE>w1<3wyu>52N`1T-o_lFsC4f-zlG-jJKYfwR_^q|)EoCfyZVBZ>%)=X_R ztOEvf0g2g%?tR_WwUlLW}fnMFk4O2kX|2rzV>1 zgkE_5v?5YNV883?UiN>VC#btp99a?^^<{x99VcB8@)%<`X?sFP<@y;O2i>U#j;hbU ze?8|VYMSef$%k<|;LDY3r>76b!b*$ZPKBYtOjTzwM>hxK;-XQX=U2oU1qv!)9$+>h z7Ol$-Wrd-P*Z9EY{}1&U$@m$wH~dMApsfy3iq5C5$uZ+})Ty-f<;C-FGYP-!b#FJv zc171d2(|%8ZFkQ4?hp*6ZS3*|+C#)T4Uv+Q!LE_wr!OX%cb{;Jc?22_IZ1Bdq z;BR(ii+^a(A^jV3Y9ExTb6+kw&*Rq4f7euIFv$69hiB}ypjG96&vr36s>8X%-Dmc? zu!{uO$s)wbCP1oJI3SDA$jIEBqMWAjkxPD2^r8`5Qt{mUq8q~S3yEnt{~>Jd5A=0a zvxR>*oj<7vWkd7eJ{{)YCD*)K??iQ^dmd@mv`$w;DkwVtJ{oh8n2*9l(4A0bViuo!?@EUGXVG#D?1&(rT!QF1e5%9)r z+9>gU<)@CAL&~KocBQb`c^#HAx3mnSpjA~(NvTXg@jxYW6ov7XG^+MpZ;>1kwI(+w8RrA&dedza3k7(lYW!%^Z7Y&C7VSdhGnXM zAAtF-(|Uma_!76^UXl4@k0NEz2>YuY`{=NhL2`jdg*RPZH)q>~D0U?I zzLm{8Y1M%_V2x}UX;Zguw0~|tV6%k-j9A}ik$%?X*4ZPmwE(s*V3T6=vXTk!`SbQZ z#=jF@ss|+e*fY;g@6MD|ZsTC%*@beHhz~n>_8Ffdd#mojl6g@>q?9G%LdL?JLZ2bi zBUq}L(0-)>u?E}JZu_vSYS=dBVE&P0LT6v0uP#!56z0>KB^hz z!2x!YCe&L-sV`fxLl(>b4Dj^I)gQhdK9|yAz7)dgU6)09gzi=?E$m$(mJS&JBiu6G zxgqMJp6EjtA=R^nzH$u^2>;AQ5@hkSzaibahcCwY(b>J@5hTtdG?~UY^%ax;?|&8O z)HCETK*}Mo`j+1{9nd?*+FSECjUG&wk3W6+_Bm)L_J?Dnk2V1!=d23IjZwlS z$kRckSXXQg!op~LLH51gjk`a-!G0aokWthCAn=;Tsh1}B34{E4JneF8rB{F`A|eD= zpMu5tX2{X6T$3Lw2+ri9cA>W29S^cDYHzK&8>HZmM-h(^Jdv7_E<>g8 z2ONpbS7C3xcT%c= zwAXc%uSS87sNy_)h|hHnQ;2iMNu zabWT7@-V?Q!^KX{CJ#17;C@nELf2KXEVIo(9XI2y1I|aTKv4$5V|zV@tT-G5R&)~& z8@*TO-&nmLs}PcYi(|iwVZMht@=XKT!UtrR8#0H8@IW})K8x?9xc$kEP-I4x%2Dvn9&-UvzdZ}Se^3S?{hL|RL0#o=BWxnBBw-KLc>0PQvYic-(ey<+G`K!Jew!OCyXO=WimUJ(*Qv=mMaJpSGTdG z>k^{=Y_EB*?GO+g1v82Ps*C}*eo`ZZNKnzA*teD*%^#=96O|DK4$bq$n2?B9!X$41 z3>u<15ufmvs9#x7?Jm^XFJC?)f_lvFVdVMTKVE~YDl8QGe$6dkm3^ue>tB&hs zyYnpJ+mX8hzpxEphaLC3l~MVb)H4G6FoMMevQN(UOEnjy_RHBo&tVB5Zt&;Le9U}r zwv%Iim*swt7s1LQNi3K_b_pHS0=B1V%(i(#uFEEm6!*Vlj>_OLB>Pt=-d zIe9$B8w!5ENKt~g_{XMv--MKX!^78qvY=s8vX(`Q^cEo#$)+AEO&7mR{_WFT@Kq)R0*(_@7N68=tLiwsl@R;UhCZyC)A z;JL6yBBZ4SMOLdbHN-fI7`g7r2!7dPwMq3hob7aA22Hlo{!Tghn6+`v+I{>o_z_ET z4m}uw+~a9!{yU@A8-g!GL9QFbdr5M@0p6H^VW+5opb7eK>Ti6>2dum>lUu^l$4t)N z3v^yv-|@9RHCYv0Z=W+X3A2uq76D>FX`LH{PXoXa-gVE(d-N`fN_f|i5mir&`RAzo z_t_-Y>64Fi&j!)9jXhCkLgjccPG(T6iuwtMaFB__m?4k$g=f9S3Zwli{mrvMB2bGE zU)-*rTCy}4zJ&CY*NS`-$hmAe+WawT^7a!bPB_)uiwKwv!c}COY(_>Kc)cOU{!!kQ zgusqg5Hm|(U0eBpHu?9u;wwGFliv>oOW*!_T}T#Wx8;5j_!Bq!0z|_988v4jr)m{I z^PRGxkDx)qxRGAmUlM zxMV+y@piQ_L8K+2q2q?PU>`_ROyIGR7Je@m1Xp696FHe=%I}8p!!$1LP+$0oxyaEEXhi$JGA}6ivm{ocOyy zxAn;$cbtR(oH@Sm_PfP4Hr2$JMPMS^i-=3W@E~lkB#Gl9crN)I*fz-0?M!4+0m>0G zeZEo+S;3gpm;OKlrNq1mWNBdod2?~KAsTP1RY2dlf=K7D6}&Zk^&D{3{EHUf6*5ch zvhohFxv3)~@^|}&?8-f3dwhUQNM9z4DE9v9rSFwRtnMQ}_S zMe~axMmGqBPF*a>d7IQx5?_~2pmRItt8D#c`0?38L7W08{U6cFsrwLS%4AT z14lO|krYLNxA*n-m*20c+}^Sl^5P{gmC~5Pn;m2=PqLkWyD%sq5(&=vqfG#z((LDk zcT-Sdhct1Oc7^v?MHfhyh_L@jd!;WLLnlJ@G!Xj5tpcd4VT%V&ZGF;@m$Vd5fDyp~ zV7=b}`m4w#xSrOMY5`I@LxTc1a=5NRcw)j2-_+s^p9Shxy}grCtPF)!57)Z&|F?OG zCUEfQ)zSG&3*_6NdW3@**UOV^2R44e3>($;QpO0MUm}L>-mig7waDKV{Ix>jlA?#H z&ngh&#$`=sK$?6=6iQeux&*H6GQLF2I-7RCr}vDd3g%ncVMnpEe;+!*Q^kJ#4t3M; zy?4*$Vy6ID0^7m&?siXaY_Cw01l?W%#E^1H95OCUBQr096cOjweF>CH;zrtOA>})d zv#eE>HbfvgBgzW)%^pk*x0{cvG~7R0`*@@6qmrZ62q8H&&Q~s=XvV~lyphgC#wjl3 z+cgqyV^b69-)OCx;crFqcmk&g-|9c|OPDjQDrTsOm&C-3%N_$K^+qA6AwlH)S$(cj zv9q0kU26&B;<-Ut7^hJFTW2c=(RS0q=&qQI`| zxQtIy`D#BMlk@njpzeAd^4?)_J0>%fhMBi{E1~H^HsqcouDJJVZ;rFl!X8axj0sfB zP`{tg?w9I^rZC)Q98lX0FBp%v-1M54ND>=!KOMt72pUA3(hk$=VJw~}V zIegH~2x}5UZa+|MBguxIZ3!kxWhu1TXONnjs1|KMVa5*oW(_J91zX#rJgB4pi%nEv z%_KqiIIA3{^SqNa)ZUgB+0QcXQKjhpe$9w0_u6<$i&)g#2)fyyF*n}FVi;{91UtZG zQ59%eu+z}pjYcUvj8b?D+?=|fKo`qa!c|~e(VhVy+uRI5Xn}a+ z3^AwdX1p~y>I&VhzI_6cW6!nEngdc_D zMesyuM!5J$wa`jr2WSP36GU*(HuV^IH3VKg7TC$FlT}H zPKKHB@i)qL^4A*INWl7BLdjs`+Htdk$TW2q;ksh`SbnCcttl8U6Y3+0zMg8I6}5Dg z_;^V$g7JHv&SQuH0et;u5YYRs9%bLO0JhX~aHGWhojx|Sy;#KUwPzIU0(e4GgZzq`So%<^<^J% zmI*4h0C7W*2&Lgc08;2de<@_~^D68~|7Ksdc1mVJ%oXX6T&MS6S+m$_bjX$Bz{fd` zH$P^JcqMKC8B*_@kvUD{y}iymAqC}HNYhD=T_O$MMTSApHlQiCYimrh-1A?Qn zH?_CQW(As7A%Oq+u!nz;8b~-zA#g84!-J|-kuF}s!y(+Tqam>_jO8)Km~pZt?knKw z;|HHWVX_4168&i-;&_sxLdhMoeA9em#TqeEYgd3tmMa}C zz>gA3aS>E^t}=PA#mqpUP(aKa!`(il1b=+U7nojskzWjGTHojp-YiT8k~O;dpO1@f ziaYlKraPXH70lmbM~WrE8o7e=Go!6LgIKxzQ`b$16gJLfhNd=Ucn~ zO(jtWs;KV{nWdNOm|l8@1#_~(J)c`-cECh(O-Iw2DP+&^6R!$B#o8OP8Y_orcmNI3lYfp| zktMKRt-Q-jUnv9}2E|puu-UDz`#vq)%H;yAF7qphGdwhf?4aw_{i0-s6M_c|2r1?w z1EA}1j3^ug#3(q(n6#sP5b|T5i9;DRfYA`svqj^xOb8ILUhCshw!O5_&cv3&7b+ku{hI?LU4 z#FzcEv9_3%-Rqm`4;y*v@#cK;3x(9s%g@7vO~9lpo6#H5DTtkPp=`Q5Szn)(LGAkD zhkay7XA&JRplj?kFKJ!yPRsC>H%gPSM2V7n zh7R#04Bn~~nGmm33hTa(UW)WQoDD?P*R? z@^W8M{G@WVE%sDi^sDL^%c3q#eTnPI91+VdM_?q63&=ao5h3@;l8R@5D>$u+(2BFq z&SCqDx>H4m!KiN+*+Ca(V{R_PB5?KYWW0*~_=MKv!~Q3eiSCns7q1sWx3PC}Hia07 zMS|?{rMVTaVsW7bE*YSD zcq;}F)_T#Yzy$x&UIFd7SmIJg98S7bVg{-O#Tm6Khw9sbhM79!4mOO+64|X(8j_Tf zv2tGu@EaiR$_*&A`~3zEjsVNTJ#PkT1v zn||%*rp!7f=_o9=GI?WQdRmzDz;tfJ^ET|hk|%#BO@gSX==N8Xz>7@gBTe6S^CMw| z0Quik3m56{Xl?&N&%3y5n)=pqUnloxa!$^TtCOhAclt!jSszGYd3plkL0DuWI^$cQ zs*2ZSQ<9pW<%m&L_gxYrTHZRDY4)kD*p5QTa{m&?5gqe97W4=yukA-o6gj=GSI4Ed znheTx3;{tqfrR*Avu6k~(f|!UJ5H6I=!=w>1h`^L&#o6h;hJi|myR2AFA1?Wuz-0MX? ziVJG|Br6bm2{tIL7aTuxeLy42w-Hx_>!ccLI`=jYX#)PkwaVI$i^j4YYtt zn_QMCN0Ai7Ol3R0XQAd5V?V2=qtz~Gc;Rk#%F@!Qai8QHoHFNd9JTO--tbjeNofz! z>Z*Yh@R@56u+6e_rHcHSq#ptCFa4~Q-lKOnKb5eGEq0Ab{?l%>J;AZuDAtw7r!llw z(_9d|&l2qMPOnQ0R(8nZ|2Ax5V$X?Jm#kZ)LAa?0H2A-l*%@={`%FWi#yvTI4Y?Bk zLoMDd#Vvtt{q18NpkC>`A`^{F9gC*!l1_xLl6Nw{VY2(!?{P|W+UiG+lrjv0j|dKo zWb2XBPkoaxD4zWG+J=|qba$L_yrx6Am{%EyJSeQ7_QOT1g=1t$B-OKF9Qtpnr1S& zYrx)I&+Qk{Y-S?(;u89Eq*^I(X{E%l$LqPcsw`pz8d$B9gMVsDawC)(Iwf+|?6}_t zw9@i=yBgV7t|`Cm5ZS+plrj$B-yX4R7i?|S%9hcQS0vn`ZbL-Xx=S`7h9=Y>PP&o$ zS+soL88m-n@cMiUO>-Z5l=PU_r{Yrv?tyZ?oAzs$Q+0lnq6Vs{4NY@~}3dnI&huMpU6`Irv30s_ta{5)b-DeRRG;W z*DxVlOwgWk^~AR)+9&08w8;bUk=e)4QV2^;EXgtVV`vY!pk+yc`Vgxn+2_2Vj>PEU z-#gRwVLI=ndp~?T*__q~)Mba>0sT!mx3#+6I%M+efu2IF6bCS7b3g*LRa3}fkBDCS^^2hHA4>fZ(K$AFIe z^aB^F4XV1`N!nRnTTuc5Oob5{(D9sG-vZO$C9(BoLM(LhhRV*p(y|_7`%F@xGHvjE zOfT$B!lpVAWya)E!?587D;3mo!nnrqx2>@GT9I-n^85JsL1C0(2Y?tq23x3=^Ew_> z&n~Rj6zACayeBtZ$?=ddTR*J(uj_VQKR?=e0(SiA&Jc z+vgu;{7u08LmVuxk(S)GUN{bUng$RxR;zpCo%ZtwFirfb!A z?gKfgk!Y@dB1UoxYt=#qVg7eTFMjUw^Sda55%)t#@00>^}QR zvV&v>H=MNJ&ZRSw$#9Ge+Ec92WUKGI<2_RkHZNR@$@ffu1--c#u{A36zc@A_SI`%5S6hbV^Kk*QcAt zTAx|n5sWc;psb8D=$U1SVU;_9ghBmpMKuKwXz=Ffs>G!MrR=n#1MvLlNqphT#(r#F zN2@LCbE%oODjOF+jgZGsB3F9+&x4WUi24CpZtlws4p6HE5z6%eXz)JIK4mi%Tdl?$ z29jhTf<-2lR=TCBlxE{6dd<%aJ=FIk+=ky|AFnt6rE0eOT|__kR4SliMrXKJlD*ZD zirc?ig9n<9PvnSA z8N`AkR!@iI_Pufuk^;xPDzv#EG%BC}MN!P>&khb*Zte4lHYd(c;wPlf$}O$Ju5pyp zf2<(dxX}z&#x?z0h0*6cKNr}$>M1IV>EL06Ur(8BxFYqs`Du|vw?$CeFJYs^Y!@S% zGToPzlx!AO<{{T=Xm9ARj9{ISMbXU=bFf#IJnEhkv9&AhUh$*nx@GKKilDtLvy2ux zo3}yaA;ux54cuDLBQ2Lh<}cm9odk8TBca@p{Nru)^}Ew!z&{p0yW;`YutouAQF^M~ zjeIOu>iD!9QngaOPivALM|#X7Q|`uFB$0PaRKa6x>3pH*w4WYO5O=RMW;3tj*HDXo zc$w^9X!B_e5ai7Ipn$3+zCUjVV+Fq|A)(*nExc$cBtJBUQT4sdv|p!N@D9CBgawd> zM^#FXWzl!$9rSx8knk)@7a!aCD9c>!H!ybP$e28udkiyg0QtkO1Yq;=6!GX}7?xChM>e;rN`bXx z?je-E3lPdBE|J&ZdU4iAA?-zWu5n;K$h64v(9 zhew$bJ>Q8WWQ2hZ08lT1JwM}z9$fnGsoFK#pHttDm!1KV7M@sYaRHZwwfJ4d({uoa zKq2a4`p;vGQSIE{$}aQ0E`)J}Fo_hcpaQBx`~Ae-Io3nn?76X>NGdpH{i?5L@ z1S(OAMf{;MpBL?@yd99kQH)&bFCX||FvgU!Wdm9f$;1s(`lfud4Yx&sn_631t#6mX ztou)|88@a2@Dm}`&c9RRFFd`ED}h-rVohX#IW=jYVEv4QfY|#b^*)rc?C3;{!o+g!RG*-Z+S+F; z`iZSqKj~L}gs0BhdTjk_UZ%^JZj$48)#{?AsiRi>W$t0_Pa^++;HRat~~Ufs%_bj zRnzo`LHb_aNqq+`>j%ak(Dw-q=zGj#>%u4tvJbmjn-(I0G#W`*2$uo$ca^J6uG9ep zSMh+SdX%U21(h?+{SSUaOV7c}4#$@UN=77cYlsOk!bvn_;O=Kf9jvJY3W4{+6#AN1 z{iA{xfXv`?oGp2^4%#((3@^efm%&213X z>WG5@^h8eK7B7to@cWCZ{0eW6Nv!|Is52nq7mw1I-nESc)Qkkw5p|M15PGFH)G-!o z(``atz=;FwE-L;yUsK(#{`6R2xu!h@pZZ%ps;h@~fATW4l+pLO;QCtd4yAw}u7G|= z86<8pMz2mxqxzon(z=e3FQt3fJIt->f196lf*fv{H&Ut}8h{B=VM;jZ2D@G>5?lK( zhV|FkAxma$xxRTl)LcGJ%`1J?TDd_~h@%UY(IgkxwjnCT|Y^#%xh3_^57 z-KU88vdMm(@9@P_oxZFOXT!yH)zQ=&&G}f%DPXtFPom)ngv7tJOh83Lr;Du<+eOr~ zO*``OIT7*V1l=gBY6=aKg%YxxBGu!M&PP8fb5{gH+sXXyb%yDN5^^2!bTal|@*p%jS@)1@8-9;u$^q-Of;*nICppc5qC|E?%X8S+3=cZ(-la!s#mk64 zuNm3}!VG=OP-OOXzM~?2^~%@&nUb|7EHmu?Q)N-;I{O+Aj>K}!cO7RIWdJuPaOFMX zZT8f-a|>p8=~lQDlmm78(IGEQVF+jNNYIU0c&PUOm%;tlJmH8;dR=HV2FTEi7;SDeU!s zqgUW3B02<0CRc6|qO<#h>H{5NOfA;!d@kmVLQ(FqtY9fh1h4F3b2>op;$Zhi(f%T$ zg{(~ypMWlU?gTrdkt%&KOjN#S`HJb#IxZtnmC0Z|v%#_UNU{7Ugv&CXfv@UBL%sfua)I`R7#Wvrv2T=a?^coj0XG`$ z)Bc{L=dSCb5L?OBqWbuS_0Eq->)Wja40Neodv5}|96tY&Yzk%bTqouU^y{1t8=p#_=W-<^u}8O!%V*&pWmF}-Bc+p+G1*tmtKq2m?W*~8$Cs9 zb$s*Yf}7={Y9DR%eMF3`!8&>&YvrxzjmnkZtb=4EIkT*xy`-KDkIHi%SK(dOWk5KA z7!7l{(BS`q`c@ff^Bg2Zwdd89gZf6elh@hwERd+;&8^QdI9XK70YBD1UmFIsn_QyL z!OG`%)tv~Dc4YhT3^abN{N?f`WWbhzSi~4y>m>)*r>(BH_B1cl|0O>l9fLSlCa45; z|IG)04~WQ)A12u4m7P+taNEG17uy!vHOvpf&8l5VjisNa|H!aLl38bmwvp96@-l`j z41j)f9RODqKV3n4E#@}HOZ!9(5iA4>9KlNm+{O(Z5$Z?`)VnZR*lK0I(nKJa>+lEN z>;rWXNwv7!!C1e0@y5eg!3j*>&U+68ib@JzE;uS+|5AGl&$cUvg9o1M0e9l$^^fAa zF8Sd1r+vI{e{X>dw%=E-+AB8V0-|Fx)n6w0ZpD}y#{D7rs*0y;SWNX;CGkzgy=3$x zdp~Be)O=02*Pj>#WN`LxkogB~ zmY)T_ZqtvIN1^=aN4;OuLxSNL8Rsv(?*=ouRaS2`ZM8#N{04K4l4YrM{Fr}6vqZSf z(7^a<0etWqn>Zr+EClBLzXL~wjGL9snZ@?_`P!IYfII|V_xrCPd~fWL>89d2JkN&2 zR+NMn?Do)a5`(Xsq5@;slZ<%?JKBYRq1^lP_x+B^!_C z$j(y%(bc5;Klntf19^gg76xgt5CE2Nn+9CWZQSnvzz4#wVh%}O)rLtAhVlJzjUi{t zwxMMH;gp#7w|@r8B>FwZiH~z!R19#&(wqWW9QeRC?eK3mWJXwFP{j&3Zw|N0w*<~< z&-T*z#Xd5KEJ)mBGfrAKL6p2kSwTV-PXiG;bpv`}`%nrR4ME7PjZw94vk5wR6x}%u zh_=7d!9G-~B(j9DX8AtjkYI^@rd^b&{o<9%BOpBwXV?24ipVwx5?s9;YjmPhKyLdL zjp42zHmUv@$ZPv4XGL&TSivJnW}7>yT-jUGXVF%6OSQQ+dTZ+4S&}|3`t(H_>13J` zy9LF#xg_F1!$+dJN0<144Tm$%W~?wU$skvnqS)t37penWi(DWKx}5e#i3};k)-woW zX&Z`liLSh8_$Fj{NAt{El^mI4uznWr;75(r8!u0gqVL{oqc1aEaQV+X`WRpFh=`ta zpVegYz-&PEyXxp&a)K|qKmR(1>q39`K}govvsm6R*ad~a!!jnA0}}sQvo~OY$_J6JUiZ?4 z17YK$ZGaR`=W`*AMKCgtH&*SYw&Jjd>Kl8is2x46_K_ILR5+G_Cn0p9 zj}NKvxtqQ{2sj9eT+M4+_@Qfx;-ISEDzO43Z@M@9Zz%K*D#7M9na4O4kJx*Belfr4 zjka@;&8*Z%^R?4C@YRZ9C!$f1)t4luGUPd=QIXmijOy6Ft@SA)Bb3=Q>^X#ASrR4km49`^P_hB8|DuAjE23=#>xe(O=X}%WB{_!$XOWO z@kN#HS*!ZH-d;n5k|)Y4m*JNI<&P~D`tI^q$Ek*6k*?ya!Hp_Y%@v~j>@PYi1#eeK?E*&sOBnS^MFL#+e`Z&+>@A=YiX)9G_`GZ>deuMN|emU|KPvM8qsahqB;@5GgN8uS|#R zO$ilmSC=zmBh#Nn42i|~OZvo3T65CGTt_;upH6!H44Zk)A8riNY80FsLWLf{w% z5NLP|l)G@Z6S$`lgy6op!|&R<@71eoqLa{KxhoF665s!Rs)+Uc#?InthIUtXsQnn9 z?4O(v0H%~d5a(+l9IEF`(8V@46A!v1w+`F%K9>NTA$x%rEsq-*4?Ua zXC-Ymg^wDKrd_j3YaCsC*25-XOS0$EbDnInQ&8v?o69}df72T6|NS8{PIYOg2cAm} z^V}W=ArsrCkaR&pNyFTUf=qE4aMzjkd3{sIf}KonA?sP_gm$CG=(j#($Ly=}QrgQK zHUrMfOu<-(Xv@el!$$XuKxc>$p$MRoaYFN9cYXLV=H!f!>Bg>KMN?XCPkt&>?F+my z5j(;frDq9A{VVyGTODkj4*%4L3)UpTCx=8jPMHU?O;n!9VGy?G-%D|2Zb5)RpNk&5 zbmsw7oMMd{=gqRmrFcw|t?@0^;>p6Kd)Yqw1g!*k@XU$rG0xhv%KE+`^Lr&I&7lK- z+2i~sJV?{2hVaVGGc*G=ABRy?94M_ug7VQI2u`nwOEdG)l(KkGJ-3V;dBaPr!tGhz z)H#;0-6%Bm1e^DRbayzu5A%=*BdIeF00Cmgv2@<4yF^VM<92Un*Z{{+OMmzAFdU=W^7l~wn*DMM=UtbW^fAeK6-AZC6%b!nFlp1nS#l>XB1{D+)~xiCW@H?oWTq`J z8eH{;J*0#5GKit#tX^3s1@gPjtd#D7X+*v54cGzX&I8)XNhQPHuEGj~RGyVP%w^}F zTJ8p%kydU>&V48n`Y5#)Voz>mZQGcJ`=`DB_2w(f@%49Fdv6w1n(9TPaFeLncOV|w zkFqfro&H8gtsazJ#wKH3$iygLB(Hz8F}|U1_?mei2c8;uR0|pe!72C$@#%})q}7*? zUT}Vnj`Bv$uyCUDHNL29N{HtB|8px1f}96P*k_OBZ*F?Shl9do6TiQmq zG&SY+SvO20=rQ@;M7Df%BA@5mwghj!^EDBao{cx6v_Y`15XReGrWH(j1d|lG$v7U{FIS0``nBUTp+p&|)69h4@y4YoD<- zpt=nP+DPyTfD>MGHrhbB%#)p3xsNUS<%EC#gzTHFVPnvJaI={6Yxa>KMj3G)Nn)Qq z0wh+L%UwxuzigR+yhDQkb;E?U)pvQ5RwhNV&bRhXu49h~lg}(OvJ6rVPt1zoA?7R@ z5|({}KfHIO?XFKLa=z@4e1@DVdP?T*fEc(WD}C`KM+kb@v4hx=cGbMBqm}7Ymw?qD zN^Owr?Q|S^=v&l-%4ewej}Lc)hx;lxf|H1dXtDE*x~JdGD&)M-WVWPJT{&euXep-V2yO%O&CmMX^Tgsp_)ddP2o;v*7xC;a7azAn7 zvDD=GXmc{boMA_y37xNuYNlb~B0W)j%csxAgl$|lS4!6?zWH!G43rw3L*@)pm`1*w z8JmQ_%W0RDmM8a2AE)EQnc!5>`15e)%Bdu^-=U}0sVNc@FF@$#6tR|5JKX$}@d_!J zK~5WdVn3LVSdTc5JalO|FB^v+HN5CA$G^$-bpJOI1kf(>LFiwj>`sotD0XH4nEX-u zBl~JbQ_S%4Cj^QOxy8RFQtbb-z-21CpT8wAwO|%&LjHm-EH>}4EKZ;;&Ss@(`wgdE zlFF(XqZ5bO5J;05Q=1lz*Hi6zpGoh;aOrPnSNq^fB#eg zYmXx2pWkN8dhONK`x-iC?rNXr?dgakd&MHO@|!(Z<&wz4ia`eNd7~CeBNtYhe~l5S zQaydzQ=pRA185mHyEvbhvMY{2O0mNFy^+=_LMm6$y1J~a`m_9xD68866%8PYS|<2P zimuHt^W>7iMfWs1OZ-7)C!OncM@2_2MYn~O$+`6TnBYcv?F2R;;l@O0eE|zDC!WMM z$#>0BGPqY2I{X{S+mYdsiILJ1p2Px{C{a>##tx&{yogiE{N2urH|2kBHT5`SXulq5|e+*1{<{k_AJ5At}eVJ}#70ViysWg_4E5?3gtYlDe9pu!;w|&am=V_<9lC26)w^2iN{5H&GzSF=wA|6uRO^Z?P;wM*eF6o1061qDyxvKFp^F ze1@m`8}7Q7@5Wj%krJjqjiJ!bdn9I@|pl!L@)ed4c_l`-$m7m>}zcll+Ic9YoxL!$%av1 zmsV?ssyrNz=Bdn8^JLgRSpRIX4q>m*7PdcKgN*mXmPG22P{EI8p&;Q4{G*jtpSord zA)x~F`JEXvMu^=@OTEE{RR`1xoj%%6htqAH`}G>|*SP_nBy<*3Xzb%L4kYCqy&ECFFBRWo>=Wg~r}<(^0Ls5#q^W^C6Wm2}7BjJHt;P5hO4Q z6tjxdLNG__i&8anQ1b#gn8^RF@z+(O%t+jJIcXu)9V&nN1`e z+51A%Kr%@W!gY)gl2jpBM8HIbv|59M&$zpl44QCkvwFA+^xu;hsqvw|5%DRVfVr{< zp`Zt-(6S_xf6HF_?MdV9(Y{L0EfAJsW98&D{fb|f91#T&Pqo?_Q#|eXGxE02GThv! z()Pb8?^^yaSbRgF7G|~-&^(W2q;tRhX5wS^MnpR7@4%ey;w=QH(B0>P^+ErellEZ7 zSqx|!hf$JV;nn-H%7KQ}MckwRHMG>hLeI4SjD9h`Hr$fIy&FE#jkas0IW7TWOib6fB&19OnJ3Z7Nx?-jZ{p+4bsG z2e~uw<;}c6Fdr=UM`3%Azj-nNw=Ybb08~zU9nyVuRiU*WJSV|hOv7v9C*oV$eVJ43 zqvd)lW&2sqk>E=edBw>pR^uxsu#u2SY?pxQ819Lw-qq2{W~{#5FByRGVBQeDJUZkdN2{(W$Nzr{!E4=BXvGakO3s6q{i_G$dT3Vj0cWc0^nwBeQksi+} zidCO54-3@1V&~-KHC-jO=7%bN_;zi0`owBccOuQ!4sWTG-PhK>WJZh7C-?x_$Dq(y zSiY<0G}!dQ(|AaW;nVs=^JPY&gpQQrW5=zz+$@T*q$M$C1&t9+q;ptv9cgI&$wkno z-~SkNfF1b(bBcF5;;lBhv0W^Mv0d6V_vzhVYi#SY;x>uw*S8%r$!a%^xFagkL4zXL zuC~_1QKM~(v!i^8yzW*;b|;-hz6z(m`wmd6E1S|a?1YR0ZA-MZ@-jlkHc{ZVmX5Wo z*E|61>weFu6PvnT3{b%qw-PbH%}>6^s$MK?VMOI;4cQ#obd;o{VBp0L8X{JBmNDE7 z$}0 zImVM%E#q(75*6)yWv|ym5cXR9{U4oye-?;Wq^0XF7o8$18WuS|06J7Wgb}83*+b12 z(BZ?nL`s-|T;~k5F{iNd?XMrG@UAJ-pG|tACu#2>*iC>(pF9-ENfbr|L%WYk?3L-= z5rXXRjzhehLakhE!#Hgl(_s|3S9s*E?2oC}?)(FK4Ide2Anf3mBcEZqQv+3glS5CL zVO36^+_<}r<@#p~S+I~KbEcmfx`IjGvETQLxoJ6kv`#^D7|floaL+tOB$1Ou6?u#6 z0@i73e!mfYM*X-69Dd-=`a*;_IP)Xd>xD;(zpYG5(zhY-D3S~kj?eX-Xzaj zCHNqT`y|`-m{T{?ck;N5H=KUqG%*(r_)tOO2#}-&{C0kp_QCRIq!fF&73mN?)fwgK z_{f>PT~reqZ={V_Xu%g?Gt*gS7_N$Tcj0lb-NyR$%FfVV!~Jucllibyz?#`T<8hLy zAjfdOn-svflT2exG}duxto$jDEP9q?_*NF$Bqt z$-SwiLclaaku1Gn@S7rdoG8Q@X&rA^bRqs5D}2O28*5*#!FB%Z&2zc{+drgz%_tv>3@1Wz$l+?d}iFg zT~|1AroPJ9-SOD2QT?^f)M5xga%e469jja1KEDPKNLqk~r*EoZUzU8-fU_FjLD_3^ zi)WW*oB1MZOLBACKSf_SA2THD+VSr%>tDF*YX*~8znr7?az}1?2Hlf>ElZK9Jf<2t z3szraur%S|`mM441r_4^(GAAuj!%96Eb{NTq=LM+_-^(aR6%0}?=s)1C@rEmw`(nI z=ywc8X2!R?fQR>!^_Fdf4eDXf^AM6kCsR7z7PWt$_Mgm%he^IK5~r-aGUm2kk@R|M zGuK+Ob-2#iE6slF9Zg)~i~b*Guhs1WyT#_c!meLA`GNMv&anm-@aNqeOf@0*Q~+v> zRsk2dQHFgZ62y0o2sT43vBtt+@cBNN_ce&WqeIxe7Osf9e}Bj%JPXYtJ)vN2_|`yY zRuhpOa-5cius`))wl#dQ5t7JT-9B%%3Sa&@7UCSRaQ*aOM*F#m{}YD+JR*NU7{`Dw zx(v*?Zg?D8Zw*8z^T-fC*g0fH9mqT9oELGwA=sTac04@+xGh!4z&Av-|3RtP%t6sX zZ?bQ!N{F5YN9p7dbH2H-Dp=4j==_m<`*~tk#!Ge%k&JP=UzVcsg}hweNJ%rGLTwN9 z$U_LzjV$Prqk+p=8f)EAT`UKr`-&X`g2!L+P(_{?*5VGZmz;3KA4;QV@wnweNIH_9SzQrJWR#Dg5SjWn4+Dz8>^)UHr zSv^iLCMYKfqzFP#|m93C$ruvOZId%4znry!-HHQbb+ z>Gst=;x=r$sH~a}V19ikPY_(GY(5LR)!;loTSh8F0ZV3Gfocz+M!?E*ZdS|3vAn3a z(f!>d1(t(=rE?-^uINXd;#_vQEok1yoP1`xdbYfH!|%gBlf2BdRD*jbL1d#RH!cMYA=g1l!hM= zeG$oJU-jV#`AjQPsF**ShD2`i(`y#}=Wj+&{W{UViiQsT0sa$D88|k)IC|*(=m?8{{Kc!BOSii#rQDPQQ|`UsHMY5g z#%UGmC1aa@-M~%0ohPx1{4JWq|XM> zH%hk$oSJT`XWoWItRq34o)3^;qyPI+QLT+Hrvc@@VywP1_{kLY9z(^6F;u_w}Zyo_J9DCo{cx<5x)y7ZP zxYMb}Es=@oS)J9{dC6Ggd~r9U&KHH4^hg8s@CNt-a1f%(!+QYT$$&F#a~K%84?n6w zrU#|S#}7&W48&Hve)unWw4n(_-Ov0-B^cN z!Ugnujbz7T+N%aUBsSQOO2jtsKd@hr+{Xx^H;~4!c$RN6X3idcYB>J0h=a?fk{#mp z7guzFfyQ$}U<2g-22ucn^XwFl*^=2zQcn!e2|zi=+c`V7N-qX2nVoiY#o-d=J!+q` z?R?MkO+*uaP@>%vWTzXl_3N*gU+i&T*Srv38)aF&K(H;05Q;?b;#f!^A1U-pm9U*o zGxo9es!OZ&l!#x=ehRn>-5^osa_WZUxP7u52+x6~1RdJA@AAZJnT%#OHw5l*xr<)< z?G+dU6R{)b*lsx`NjdApBjYZxbI7AU;L&V z{HN5jOy*crB8v%EFI?tvoMx?`R+7@@BE; z3N|Bo^9U@1M63!9h>;G^YVP4+zWjNm>IJApeN*TsK12Q(%Ez{&hZq<(VM9D1f()w0 zKpr0KUdej@NpsF2=*&zC`RFkN9O5%K^Y2C zTTR}CSp{)>QX=*VkzvT;@XBYD&jBFPq%7rJFgFs(t~)!#cvB(>D{&dw^GHt$HD~_{ zD9UB=U_wxhHT=It!p74WaKjCk3w$x-klfF$i}ra@fZ8Fmbk)p&UqajiO77|G4v>nN zqc%SOfu`~#UfAT}^D)x#f?iYIPr^^tJ=RiRqmSQYD-6`U-gjM`eWUNYos4*k5m2rw zB7Xd1XsCQM^X8I{W4t^PeJk!{ZLs{kvHMm@S=WeA6VC#;5~vL=7I`6}>u-MsvS+#5 zCkpw6W!d23CUryJi>$m?_vSeVNneG8!Ccf%+I^H>pI27zOo#T7!glrlf~K-pD`Q>!ti{7ona&ax9}T`O+n z-I8@pz4;A989BsD2p!W~=Rpa$x>g;$iqw#lXM)ao{e?Xri_@i{(=?gx%a^uC(gZj9 z8peO%Uo304I9>1v3x%cpME`6KabE8`g(sqPi>@meRo*Iu%0`t1248irMfHJmHmoj~ z^7|eI1RcfM5?!xKioiSOYTqU`S6qnf3Y>=t#7JOgLn?bwtrZ34gN@=Z2`v6s7H{%6 zG#Hwza)e7eQ*UisP!vbq!7v@A`;8zrscZBo>$3@O?JX+xwb(p5tZ9RvNbLE&-{0O% ztIdxGMY61mUq||kAMFb^X*K<^N4bmmKv1mZVAx0P+9Pm$CfsXO4f^DCwes*S@jft-V8tNdG|@mJwiOd1`{KX*KwL9P+(HyXDnYT=sC`Ov!%KB~4m;RX z`4cT&qHwqDTedc{_XUIVBZG^hriCfPqwSb8s|&cm=3F&4ShRenK{im;sBgS2z12JX zlOE*f`S)W;FM*|cNV01eV$_R}Pu!gXRoR!vf!h*=c?0zG2kJ8^{E>DTkX2nXg>D$b{u7ilbchl%v6%vS2is?O@hl=qc^;N_rn z@g4CbuzU5UaX)Q_{)f{`^2QQt^>0|mBH<*aH_tYsK19?|O;RZr&b`zN$vdyH+`6`H z6P8(Vl!pvVB5KH`Sb4|F+2NOG_f##E^yG5f_PZg+zi3EeK*WyfmyIa$7qQWLnY_B( zk&jhjHE=7_Pp-%!z$+Km&(0@zyy|)LkatlKhW~o|Rt$WqX zGpeiAm1q5%#9{RlDzSz(n-)1;BtI;3T0zRPp|ohsIPm$~tB<<%qs zSHzlM*%t$A8KLy9_l4w~iyiB68sRWx_5&q>9*b65q&Fvirvn&6;5VnB1F<+C6Y!hD zB-#l_&|!5w97X=+?{QqbDY^R-R#@1z3Tb-J%lQz?_x-nM`6C!-)a_EHJ&$GUdVb_% z%&5tHvAdAV;A<@Ruwak;gl;M!>gl|viHWM$k*9|`mvHOSL-g^w9KnmcU7)0awo)S!r77^amVKfJttk&fZ zL^|bQLY)1+X1lL{5$#7r3@tyszTQenL4jDla-T(cO`EIg);bUw&K{ZL4i{Z+|Lo9HuyA(Fu;K+!AvnYt&C+Edx-^o8b9m_-&T178t z?ki73bHCo=)KKqeH#!)&I*UQfa)cr!G(+RiK)L86{bux!o&?Lv6!>_<`#e$0SrG_oC8I}3 z4U}lcXKu;AL>L;EE;E{Wk%21$-ptqOpbrg|+v^cRIt4Xqk_W^NWq3b!|6>2`{>4Xw z7rSRo+ur*0Q!gzW1=vp5u0jpL4EK5r?re@Wvqp~}Ct2fW8#SI1kfL!{p^{)DKO!sDDVn;1al&sVKY1=( zxgGE1drnDoe+!iLyaJ=!=AW)@xga(H#D|L?HV(}aGCcFn3Pd1fUj?wl6K7^Ox52`0 z#}w$Ct0YVx;JGB^?Ppr$lWi;2&#NSuPYGVpX|S9mYfU~1b?DfIengS~#&|)#ONd0D zxWwb#Lsdi`tnVvjl{v1FF^jPx836=19%RQNBJn`oArHli_jd40zX5!^}*%Kx{oxlt0r z{o5YCLdc;K+8fc7$ovqK-=JgismAD-W(%4(=2N0U7Yd>Ydw(kzT20B>s8tmmh zmgWHQrRzaL%Kl`a3e;f_1y>{c`TlengF5%`6^{J#LtVy-$BhdmYN$ zzU(m`h0XhsK^n}c(6+>H-b-7Ou#WrGklUWpkC8I*wbjnv&sybpnsDNoen9xEBQ&?sUakg*QX~vCfUY{Ip~2hk@SJ7O6!-#}Sxts<8n_ zyaAIK?2xT5wa7qt@7%KSJ=rY7A+vPv&9@YN#?TT{${s#(Z2gOLVQH2du5%&BC$38~ zFTG=WQJQ;cmdoII)4F`OMYBxuH5c*&-nRcaH zSxcwPPC)T?fhzV11e~Zp3F-_^#{4?g5Ho5Q=G2|0blLK9xdSdJ!yN+(t108_?kb{x z3F{`QQ)-!(%TIH*uQr`{;`U_6`f~)W&Wbp2_X%S#PZ)YiPsKQ;ZbN=N+cwElG9Ie< z;dZQNit2WbFd}{goI;Uvxso+t;_8-7Rze<3D{5LE_B<}(E|Jbkm{ZFztfliT7>Pk( zbgvPPr}nkrJH2{oP>+0jS1u~$>_f2QY*$B4bo=!d^ROZlrTs^K9Sr_79-aC|^*O!i z#U3V4b2N+k!RwkZ6la>EX!#TRQE~7}r2a*|JQP6>+kE^%z~b4Qq4pU)vFy{FxN3X# zwtU-MaQ{+X0-^-J%aX9j0#htGm$lkNUpeuY5t4YIl@odoR3IDiL`{I){(%@0FE~j- zF+R$S#So9-yfb*gt?LfEF7k-(4}}X*$Wst^HWI{R$HB%6#(eDd%neUNdOnw=pwyqW<>l zclV!V*dpFBqpP;RKj7^tI3*l(g8C5t6zD(4fw?AB^)>$H&zG7bL*mr(zPtmYgj!RY z$$SZ2!|Q*fhqU9|5pWgly`b|qKNv4c?>GF)_B+kTH`DxM53w^Gh9dtgh`(72@>vn} z0BTfi8A%t4RCa(>!$lR9f{EqI{q`%-n7~uj<8-cA(DYQQg@ve7PrV%C^oG5U2ln)=Pfj7T?!6d<k{?Gu!OaYIrwNptIjvNJ&v#K>8Vd36eRtzq!<(=3z}BIc-O_+ z4>(q3#E|=A(3^9Bt?{gykWs6?SSP?%@MuCZ*`vEJBDKh07+<_(c8{?%+XkL+!^5 z#jF0#IVy%_lUv|PVqzU#rrcwc_C97=Br~Nxl8^o_)vu$)Q4QD29Y~Z#bbR5Vf zs_Y-amy=#+cy;k1{p9)+Vt+-CO5ypQ&sftV@$g^aeTwb}U0ebJGwks9>yO~DJh><* zo+8WB7@Y;nZEb^x5W|msrzw$F1om_A=Vv{a7yG069m<(p&XU-Bnr*6~P2?E;AQ$0r z0b~(9)+zB*t#!;zQ5Jjzl?}t>=@OS>H@lPS0n>H=I+82_yhVQ-Q8R;hrJ|^c3bs$# z!}q%NK%<HW$V?3BI3 z@iu5QB}=4>xdB|plo-S*V8UIu&aRn@hbFbm;r_FNx73Qf3F?o@F>=*c__HeFXM2r1yg7P=(Pl)RJ`ldx@g{ zjqP)$TfBg`^is(X@3bz=%QSr$>mQDboVa(f8_6#rrck)bHOGd2RBk9NnLo-gTHbch zz@sFyGO){UO(glA?+R3tVUuTA`GWlYATnD-x&IdX)|qXPNAJ5o z8{Ysb#vh-8b3+|!qo_r0T%kG^*A+oUtsrSj?S~50&3~I7kc>zmzx(*|F|YRl1M5>7rbS1p~K0Q508IfSu-81TEpqds`oj2>iAJ&iaYYIdj&5=bui@p2tG*hfy z!gKh2ZBj-)JHMDIM}kIUnl;ysNisByGVhvupsX`|m*d}&o&G`NRCy~#Dts5}1p&&6 zX4?KydT#{ujUjaWOngHoDqwy$YcvEK3;oVS28cj50Hy0YYFQ%7iqWg5sE+0z-Aa!6 z+5xI13c&iL=CFvd{vl$gL6kWe@2B(U<~lJM5m}g6Q0J%2;@eU2|FF=_P^c%3ccxyU z{K5+*#c1d`dnyk8u88mku)`0IaW&LF^?AYXq$m6BN!VkQc8`shJNA1t-w0-{Q5mWF z9}TKPZ2~_TUz2W3MaH4anV^= z_*Oc7HGW_o6xE;zhzlh?T_UoM0#5?g#`6d}@K`NYxIWbx#N^4%X`}XXUbJ>3a(0>= z81qG~kQ7dKRx}#&9F9bB?X+$v#6(1@lSoV*C-<30`9%Njy3t9D_c$O@r8gh< z(?T`}R{Gvqp(>3`RL+{e$Q2wtZ$AAoc!#}Lim&jOLFsl<>V#(ZTON7u{h9iOf{Kxt z-Lu9gEG2;cWk5n4ewl6hdV2JQ!eC{tHYx@#)q6rD-mZP76)D^X|12Ei6Y)2JJDp^k_-H{=3iSm)G+e?VGC1Npo3Yw-r*$`@%)-)=P4>$F4KV*?-)tC=B zc5ElSr7F!P;-YT3cUV(`ZTvdz0ygv`Pl$qzatOR4i?+T@pl@a+GefdjXs5OdZgu=t zzltH1qtEK|AE|}SuA9TKREt@V^%^*Su=oE|b&EWt3&6Btxa$^Tvo@=M_q*5YR%%8a zSsuQS`-y=xY{2#t^KRdO5Qd%K(S=E86R6ZDC>o2f1;zMMDet2lXH zwzXH7IeiF;$p_U?B=eUgJsYwcv{9a0$8j6?2(UlGnFNePU04Uuehxo1g9UCE4l;@& zdH@n{1ozs9j@L_3-lq<2ONf~lFOU~ql)}KK8Y?=riYb@u%ZJ;ailOB{W+zY1Q*F zTd-mEyAS@sT6-j|o3oe?Ck6f64zV~=@%z%)YC*P>w_nW#h@~YJC>-9B(s&BNyivJB z+PA!8#^1f;`~OlILwB zyqXeO)AOaa%92Y%`i`$*$Zs6hRNz^4T}A$&wL-u!gP#EdIj!`qt`6z+ zgKSolS7gUU0^*OR=6G~4L)=?e!(&F(X1C~M{9n#~&CrsS5Ec>POvap=B+5|j{z8ZG zYhg-6d{jxoDxl@e1Q>VcMB|f$9nPz_$xW{n2ACG3vRlIc9s-)PDmWXOUkn!Z84(a5 z)fzjcH{A928WCE4%IB6=_BGtt6*u=tw^LX!w@DHi1dp()WCnaGlrg2@X9cz;1+BRX z%Hmy!p<&lxjbXzJv>|zual9yh(DPsdaWyvBIpT>eyG5ut7HeoTjCNBh9Ku_7+n9g1 z->S(p-u6pqLMZYDv=?5XDv%Yq4^vX%u76j=*Eb6)tzzEiyD`2ck7eE&B1HEY8(re- z%e6_kPeyV}J^e7~gC)nS@kFu|iT`Fw{ddtK69i4)0e6S?tct5PT(vFo@EK~TW@Z{_ zOrWS$PSe3TvPh*`jj%8%@4T7kGZ4zc?zLu2LvkcrSOn_Ok3gaBZT&Y@5pP=U#W19D ziLu{SZWxZR5re>snh4r6rbM%JmeS)?wi){%N>gM)+}Oaz=7bSK%{E1dG4>w8wi}H% zNX_c2K2$cAZ~cqdP2E!89TXz6D(c#Pj(vWwdZc|W_9ZTKcFyj7pQQqoG4y7p8Vl5S zRggGxdN}bWedkI(#OPv8(bvJs;DPzXr?jupy@>h2M@fw1eD7z&xR9o|N~nkL$)~G9 zKxF)|1rj3iM(k%uj1xRsMnjJGLd2(Rz27qXhG9o0)w!8D;h_ZR={euo zO6MH$%MhQ}g!1Bu^sO)|=KfGp+WuGfw|ae=jW=&CTjT}Z`%ZTU4`tqBH#hzRu#2kt zrN=q~QM|u1#-H1N&}&-C(^sw4kOC?MY^$CbvrrDY+H^?(KvDpXl0nwDW<+2`H}~j0 zRW#v3CO5FGrJZm@V3a_szph8PzrzdzBnnkUa=|bGuYGM1`Lc6~IeMgn7Q(&Z%&a+! znyEAD*H=#1a1Gz$Q`*4>>=54jHvX|{2_&~l{skz$3DjPZgFU;b|{^((}*DBQ14Phh6-5N8;9}9W;XLfS!!27>$F87>bYcbeL@v zEdSA?MXtTKQ6?vbB$kYrnyGJYhX}`f(S#)4F{>ueo+G$+1I$Ru(;trW+sAC-3r#lO zaxXpcn%oPRm3~#M8c98XGvE7HJ;Th|&K~l{V~>;54j8#!GJ_3q+_9~ppf7vrczV`h3R-B@YdQ6pQeQS_9J1SGEwJ)t)$gbt0US@e6s)w<`_0A zogeEN9^P=fWSHb@^!O-j@@QaDlbR@!vnYUoUt4OWNVFkGL~<-c!Ii* z&9Kyau~sWNbSmw>`FZfg<~1^yiVv!8Z`}SjdSIST>G~vj-x85Fz1i zAjc_EOZG4%wJY-1{l!*7|qZ%k)q8&WlQPrqC9$ zW^%y=kT_9!Mq1=BLyr+)%e#$KdN|#nsI#a6{Lc8^a|e7z3*t}A?G^mMbQTYBaEUzC z&t}UR<@yD)j2I$|S3fubJOSaXHI53 zVKEkO-trvSUMWjx`v*k@r5kb{*`bJp3CBw4Y!-XNYf?Nk&3e@2Szu zkhq>a4N)f9;^peW#c0_4KNdjFwn*5EF@cN7(vjZkzPpu^*_Y2^1;@~sPPAvn)rc2E z!Uii*vuIw;aDCpj?^F$hr< z9Bvl;2idXUX1+mIOs$q0w&i3hDrOho7M@g5+jZ{hp(C%ec7(Cwky{!L>jozvh$s8_ zmbb_Tcf^|cjsNoV(mM54wy7RuH2VGm`ffC0Gm1kmYeu{4`?J~um4BN^ABo$&%2Jv& z5(fO(OOJX2c3fXC6VKv~6$xj&Ig@`eLhQ$!qM5}EsSq!l8SYOPRsGfSX5od@j`7m@ z_1~?zCRBD}kiVgcYI5*LOsPl#-Q+2(WAsowBP-rDLLd8uWi+v}9-C^tl7Xtd>KoN) z)oj&@`B8XPk3}s$1HQNj8PXCmVet2)>9pyr>0CyymtKxzQBJ!?Q$lX5m}VL&NlW&P zi|cN&xEQ9IKiPoN+$vxQJ%%T_hrLtQNFq0hxHAfkc|pD@%0os~X-Ju1_)^BA(NXl# zE)+8L0c1QIWBA);+H)spY8vOKyIf-?JU5G?}+s(!LN1kEw&Z6Lu za`qA%_^Jx&grj*{b?= zZMF@)#3oH>#pD4~vN*C^!&q9Px5sg-EjY-T`{P@wPeFF$rhNYrY3?DlnKmG z&$Gw~Ouy~h^aDc=52BE#FV$N)m&jZa+g0*fE<0GbZwl_*dp`oVi&p@CA(#?vDulYn z_;Y?GWyQr2eVxEv#*ZpQOg4bMNKGmID1*6-xklCQ3Wya$W_Euk{KoE=`+&k9Hzra! z%p`A2K#N~Mu|h#hX`b6G@1qh^vHZQd?(n6UAN#4m9%&;)HP&h2RpX*%cayT{t&Ivy z8@~}ePDPdXy)3!4wG+K@(u#gocc0PYd4wGz>jFyo+g%MapOVOKnc3EIxg$PTny_T1 zbp;<)!t*^^Fs&fiP%ZbSpCH3fYkd5Q{qs4`;0gAgO3ULLwm_JHsZ04&Yz3LZ9*)4s z0QF{tWaM~=mcQ|{a}X6CiJ<#&ZPT}VD2$Fu8Gn*z zw9}G2z(^*Ubv%&r{RC(lRD+iYGag^>we@D!Ja;yyH)+N3U?#5W7`_eh&eu+D@(tX` zke`G3Eg}1YgSC=_g#G!D9waD*9g&e0DuZ-Q1~UN{vIb7`ZDQ8a`Seb*-88IFHiDpi zO7^bySY%R(wI;b>Nfm-K00*7?G^R(pl{{JNC|p?TdFo{qF~n~OGqfjtFI%7@DAWVS zZF*d=nMffbt*K-eF06_N5WM9#b-02+}nDx$RjDDHz_#D z+{qfr&M&wdd`eP^`{djAq#ynBd~xi{ck<=~BXTCDB!3tGudZV1AF8@+`xE<<```Cx zvM#SgH0(EDCPjUIy) zY6WbcMmjRF+qImF7wA2dC=43CZT`WYoc^7ID@XBp8@s55KYP8463=iNLK_y%kXkq0 z#F%=cN)t9lV9D@rp!V5>Hmnl5g?rlAE*M!gXla#3Ru1>D5i?8(jPSHe3Hw@(-Q&&4WYZM5apTm>ks zU$&AeSB^?>CEQ|e(e$s@y*yctU_8rv-s{#Z#jEymB669B|Dn6)ic;XM(xjZ0sl$bw zMqZ_C^YcoxbjjKv$&)785Wq;2G-zJR>0PZBMz;K_bZG9gqMvi`Fq5xlQ7;tip=A=+ znt^eqP&kq&ft2b{UnvoC!Arh`=K}4_5Jk&w6%e{M3@=c&E=@8G*pmS)aWg3Y&nL9U zkJc(PJWXw-&=|#=n1~WBZCz|%+q&Ai*}B_$*m`D6-;F%f89a=;qDic62fvkjT>dwb znY@b+@vwtFWxhQaS$jD4(PvlmX_qZp%eU=oC6mIwnCIW1BA!^Vr{HP=+3TYwD{Oth zg{o~zEh^^hI)b9cHU{ZNZl|}!&v2CTK6sCK(h|(h%L`pie%)K9af5FqxBjuRy8Ts8 zmaij}kNw{$TryPKM)+H3JNC|&52hkG_1Wq2QEBK$4AuCoVrUZjNvjbnX@EP60K? za|0DMZa#I>vSdq$%{c~hPoF!oy!#&2#XU%0!K4p74Xb}D1uR8vv`X&IdM5v1v_tvL z8~JD9rZf<}u(y9GC>M2GJ5IX>uP~>GU`7;NsELo$jnQtsR(A}zkjhXwdBdkdLOZ~x zOpaXpz4zaw)LdMOXoAMzE1^j)*20pt^&7w~<1zz$*rh3YU?(d9`njbJz1zv!ReXb@ zz%w<(E}S%E_%T?c7Y-VkJfmd6(#79ekQ)Mg+Ofc8L6st}p67r}{UNa5F_`yXNcjU3 z%iu1gU!qxby2I!q<(sXm)gk*moOUU&bt+c#I->ikBm6mzQfDC8WgRRaEE@ZC$B7r2 z_!$f0um1Mc-R=cf+N0L4dZ3b1EI0BH9Khf9-PuF7LsVrMgBAQVGVjUlP+1cn{vhCjZt#aI zr(W&{V~Nt@6L28B68?19JU$54Qf8;i>?xVViRl@w&G9uj{YX10b2#e?GLL@?p`D-$5jUTPF zZ{wI=qLd$d<1Jw5duOeIm11Sa#cWJ6(FOKR)|ICw31kTdaN-qPL8@yfQ&Pxt3=A%G zl(D2o2+4zK8{V|-tK`PIdsQ-c?`EWyL@Z;P>);y@dZn! z_bT^~(fL=fKIKHw?|Cl$a;)NFy*vOn@A8@!aNhr3f{3c*bDsWqO+6EYc8A$6 z!o=ZTXe*J{98cs;6BOEuIXH@z$KF1*8BjK5cbGo`PvIAR?Yy$kQxH#JIYB&@e5RWz z2cIW=LWG=PpPQcfNGk!@pdKV8~>aLf8ii->R_m_|A z;)a=qBxZw4jOWz3>#p$1n^*xm@Lu8|q`u+@Rb^h!nLp%|e&-*c{gl0X|C8w`vrhu4 zu|t4@w`D@`)oF2IBNKAj2>8k=3;ho`MM7scyJ5*=gvGGI*|nE4!^O~!R;SHNToPG> zYg90;71*}U5k6W=%!=e~b2@9uV1UqxukoV&1m`LZl>dbs81_q2I@t(kL`*kz&v^Z< zy&yin(RfWv3XPGVK|Ykq>J{U<$_N>jV(T-G`u56qER1x`&1Adx>3Bq7hS)f}AKQr~ zgv@f&duy$02@X_NbE1mBkXl~meIW7aF7o-_&geFb6w$6F0kR?8K~K%>O-RksoQkcG zra)lU&pdWN1_xL6MABgrxVBdpZ!z=kQ^ro&=Es9H1sM9@%XaZd@^coYbV=Ts%sW+7`fLUS!}*gfvYcz{wQ6m1f! zJ^39FIZyDyK>UXa5($-bpOwKL14S7s)2Slcdfw*Hbp3B!%xgveCw%dkbVu7^r}YR2 zjQDn{qG0C%gtel&nlnH6_P2k}?mH(hY*Z53SdZrK_Ujp~f(bQC+FH)xm}se4p|d`T z%!h>c00>kSb2rLfM1xrWUmT}!)%n5dFT`d&4|w6aZ-#l2k(5v@r630Mgm2D0Hr__{ z5mhj9pFz??Ojfs^Hdsz8)cQL`r9G^xsumMMC-TJj5G${{ZIfi3p7C4U(CcrR1~tOJXs#r=E2`6zJl-`i%x3@#jNQ z$L!&5CbL6t_9N0o3a zDrbXz(+S^?rG^%9_mrPDw>sQnQOl*yr>#g(A)YMb>CIkIObner$A7u&V5-9-ZY_W` zwk;PVu+{f0b2xM2bI{L&*R$CQOu73OE^Tvz?q6xigU`$8?F9V^{9<1I8c7&=LV(A# zu=MiJeYSA3R|fA4`LnWTFq=QZ-45rl_dDg$ze|c?yooN<)Ex~la)PI6voYKH2hTAt zoRa0y3EVJ3aY#xX7-gWb@+>y__#R(a(b*J~kSO?iB2F1LCaBT1UlYVKvQ6orZ&a;M zbgq|Oxq(KB+ybU+aC^Sj4@V2(Z5$?5u+`x zy4L4xf3&FL`Ih$FUSBLE;?KHvCYSyEImiXIV3dBG6MPTH6dG{BDtL2x&o{LoawFDp zPM@s!dR+E0QRu~wd4%5Nhr(U-D^Ame?tQVG@3!S)Yj&h&tND=r@K5ss+pB;BOSMbu zhiSHOi6w*B zpNcs;J4N$t(ht-Gb;ORX5xl?ED9Rq5(@mK=A#M}{+4jE_w6#)8GM=y*ri#wcu?90Q zp^4H%^VlyV@(IvHjVXS$H^5xWzEi>@O$_rzk1xg%ieq=wWu)RQid$-$ux7$jqetTEUe$3KYjwo zbu_5bl#_b0*~Tj$iY}Sy?hnhBS{$@+JtrQ$5qe$L^Ssw`6AM%W;)ihcP^x#4M|Xw2GSLj=1G3sg0(=4y--iZB64=|dvnS$8AHzgbSc^iXTc)morJ^Q?-z7&Bcm7?~7J(|8sPT$M zk-~YtzM;7Y`9)C=@gySUZT>z&MkVCZ-3~`M@l{e#`kJbFnQW>L6H;i3GO{?Ji0Ku^ zC(Ec!s;kbZz}A4D`oAWBgDbr!+x`1;e1O|J5g<5|c$ilgipVPxxj{?(D z7Z;}3cb&o`o?VjDH%$kt=Aipmomy`6F4NPtgr&auHf?n58IOr{9z;{S0DB3v1slM1 zGY6vyfZXzC6fIyni}+}aYHT}H_pPIt((X|upuJh~p63U{W!#&oNRRB;rrba(MtW<- zE$H8X%%ffwS~WRGZsi5hBcDk}YzcP&{n>hw_I~(MmeW37Kn=*Gt1nM_(#Po}x{-8s z2Y6yX$4UlBLR)tlKAo4wz6PX|uJAvgc$pu8WR_T2+fAh1ONlVcgOODvLEQSFQs@!( zS6QE0iiaJ(5K5qOFODYq$|H-)L7yW@b5zDLM7Brv=#AV4NU34z$#OHa8qE7%qKuoj zmf4Q9*C|kZ*fq$-fS45J*Y4|D_->#*@E>&&e)f%>&YN4+@NGWGbuwl<1AbTRyQyut z444LV+xu*7jM}->C=?fu$r8=puZp@A%$Z!m+o!9fP4g=8i}{MRKxn14Ji|OO)6A+n zsy5A?zigw2 z$}!xxpUOoEsY3@jgmebzl&f+y+;%&j_mQvB&%?7dlR0>6UrPdYrzvNbnr2Ab$C7n0 z^Nnhml;q0yno;wA{gm5oypvPLcp^mPqmF9-@a1Eb->fYBTrB+wqhWEx?C6(CupdP* zz0=5sT~nCGSHi~L7_h`*LEg(`CWi7PLsoXwrLZixrIm8B)IUEAt*j;Hdn_FiFii2T zF8*~=sb=lV-ZP`_6EgW6H$|4FZ*+sWuwVWve;B-AT74$xIze{|q`hycJk#(GZjStS zs`IbX`7S^HH;N8_tAAX3YJXOw7GAsCP2+q>#}C2gN22Dp7NnWz=dGbg<2#TgrfNl7 zifqGAzL*d@5v}^Z#*Mq65Lijc(i3Fk6BQ#Sx``@Sx8X0`8?Sp#K+dz z)@6yWxkF-0#m=*Ho|6sLs4l5Kelq?z9xtIjQZwOoLh`uGe$U16o=tf1hpBg(j-o`= z*aP*|)oLz}77WDqOn%1RQ>Xbj+mB%INP1iKv8U$KwFC1BC3zXzM3&!U<)az>&-Q|LNi5F zMm`xcj2?>#9xZ1_DU5+FgoSZ|C_){p^5YG)0NG#K*s|Gxf>Ybg4A4m*1BMRK(Snhq z3`0@vsx{Lc@bvv4J<@O!s<65jlYSKBN_K*Q&3^;he5?#6CFJ7z<^$LAEQ2w#mH%Q! z;P?MTq+9FmUG)mqrCR;i@N-j3!KZ2j%#!~sGpDSHpSl#DaiFN(>2?}o@x1!6p06#v zU@L{$z!R7WN)x)D_7Z}@WG%~EB+SVBvH<*yEw<`wLm_0JDB0kiB-^h3mKQCEEC%u; z!&>8Ysq8awjV>HWV6tf3gF~HMQ~IwGZWMm9G5cAHS?IS!0ZA^U-qOTxLR)wc4rvRh z&Cqc6gWU6}@pf0JfuyM3Sz_SOe?Tm7S&kHPnJ*e&QPTqd)+$twvwxlQCJi)`e3%ZZ zMA;KcnB^GC7>^7El&r#kC~RYw?}ATcB5H$L*&Fn<_~aIW?ufs%>sK@Y4;KJrq2+ z2lq5wM54sR2>N!)Xb2l}&OJix;mumC1?w;$@5f<>@LQ*&PW0p945Y8M$Ztn<(amr7 zSy8y$MOIa46}y6?lqwP8VfmVjN$t+i*zk-dD+m|G*2dP>*3NeK(+-U8r@;<9Jm00O z>`zKD5no9{X~yPAY;t`4Q@IqxXULlXKEq$7$=&322!!}Y=OJ9jm+GD)DV!IJA>21YyE}~>}{<%=!WMwm6?|ad={tmaqQ1E0M zh}TCIfmR^hkgbpjTyM~g5VT0+!L3L+vU;P?bO$0W=sc$87VxQY^TVpW#%OS z|Ab{}={%a$J?{`MD!X^>+{ymXk4GuUQrkF)CApYlDCQjR+Bv5Cu8BkJXcmpc;!?#H z88U{{lXl~D2+PcF;ZH;-)B|3}kVctzd4TYM-PN>aL!?vRphq@_bbT0t5K zNoj^|X%GSFkPsvW=}rk1P>}A77m=ZU>P+g9LhUbzCJOwjdDkMZ!0 z{2+7I`z;sKMTeg0;byBG2vk<;-Q^)tnp8S;XpAj%EEIHq%lB%AjizFLn6{@g{BHei z=@c{=PZI(jKFfX|{B+9?-n-<)<$q_qw%-*S-Ye^1s=dAuMF9jzkrr!P5@u-^2Qn zp1b_2w7qD?{Oi?dVE?}?D@=&f?0jY23*^sh5BcHZWr_#}8YgnuK1%Y(%fu5Bbl23v zQ%=qc2nva~u^3(z0wvfu)ZTrNSFX0oJsx`?c{$kz)w6Ep#qmG%7mt78kn!Mx8C%W1 zq?0NWT{>%9dK#0hn!{hgF;RIubTGaCd^*3z<|~3B>b!0|E>I+8DQXpD0$T&V5<_cj z9|2iD+o4p~>#fpA5V;~wOpJE+y`J#k(>|r?V@Sm~_k$4^)vWWRMqE-m+i7#SWcb2H z>a0btyMt9_;&QzTENB=^w=onF`TEqGHOQS5p}w#30?!K;m| zx+F>E;%BSJ6Wl`(v{CSv%=aV>MBlNHD>0$aqXjb;rgaOCdcCiLl_9?zhHtc>^t+{N z$Tj+uBHij5REPk@;2RUpm)FD&mVU49*&XfIszieZ+6OuYx(46_-v|2Muir~e@pOjo z;xecyufIz9a;A(R#z8&J6)h=zUj%-F0&fb71&$KHOE`2H&NF4!ZG>m&OXR}!jkAx2`;(_e5BlaM8cdp=f13YXXKdZp7jehi zUH0ptjO*l3*W}o`*#Dlu?Fa~J+5aMR@MH_*Rv*Z|ia%rmN-2DTu~entmvnIu`YX;@ z_04~F-Hny@e>$RpynY*W{kQ!%y+gh?3{B&!2 zc9^R72P2B>!_pG2*KxW>H(eoVj$xW>1D_!zO1LvV1LIkOtY#pr6R8D-vgUd|H+o(5 z1$(dT^2L3SH9=I9^%%Q+rs{+HjISM))FNp!r=-AtQ+j9$-%V4tSmqeQ86Uda+Wh(W z=n;Xt2syBmVH8|d%`(YQpz{sUPgzUS2$6txB+zI|Trx*!Yl*9FO?(4OpBb z!?9Hg0@w}{Dczlq5rAEF?)EJ-6kUxkmZ*ZOyhfSEOAnbi%=y>%wBvDJ%6^uh{e$f@ zkP-Gg^ETmW)r$IJVg(mAB;Y$3xt6*+{@gk_*g!=|zhK0%og!s-hDFm@W2bsN5N4$F zBsJH+l(A|W+UCAMl*SDYjPxCwe!iBN%GQ4=mw*56dlVr@LqW<^2GQI*aG~KKLAai? z(hnh9Lu3-taYgYdPrdSPmuBB;+ox<%=ZKfyGueg zs0bq;kIs;DYvhd-SsusjM?PXjwRZ;ls(1_%#kjK2{#*QJ#h=zce+M{A(p`9wNS#wj z0=g5a;BjAWlV2cRxHl3EM)tx}N2VKWDmp9j(eMS~c1G-G zw67n0-Qn?qbk)Xp_p%3~S6b9}jF1FuxAH~&P7B9>BxT|Ab~P<5gYch7is}~n6Q@@X z9$B;8-{4rIgJg^C_~5ETX6mXz{Ko#rgjViJcw-VLBZu1+HR;Y^M2o*N*wMLb=#qk1 z3wMjv54oikG0r#B6z6Dd&biR{z|Rb!ArED?mYff*+p9Ah^{2YSy(-|*EiGSKV^&q5 zlcBV_eh^EL{87lyL?8}wOk1!0DTkp%vV520>bY1`3Hl(X08c%S0B=370AKy#&_JIs z3zJ~>EmtS_jjy~d?V}HlkFJiM7W>NjmnbYS;Y|#9KZjauQLh7kqYLDv7kw8KRHOoBT+)90r9AeJ7LzOT-p!@;Jv=Pl^k_ zgX=cPwv!%1$)!4Od@+O+!!Rr!-{yrd`q=T93FjGU&$9?DN$4uF&J({PAjevuwwH3uw+*yc2?*5ferh0%;S=}Jg<@ETqt^C zcw)4G?3B03V0AM4z49l;CdEF*ImLg9=f9rqTU0Buqz930o%Yr@+;0ZGlnuToxFe<^ zevuNG%#W2z^MWKdbpY}+<)fQ4X>E+uGs(VBA$Mr)ta3j>aK8Skv# z^A>RtKcri~0#J81dUP||D0d*k`r@TXZi@EPb6VqT20b>vm61*=7W05uPG5bMvREiS z_DLuPjwcSJ2^~~JW@Tzl7%~BjLVu%K^T_8wFr^s<@pgzW_ixo8gP(>+((CNbtJ3Qi zY3olhacX#h^HISneRg64A>YFHo@IP+Z<+0ux3eT|bCIU!=^mK)w=}Ub+_VV(Fod#U4Mk3S7HBQ%penA|%Vkub5@ReW}qA2tt zbZ*}Xt$H6Bs((ZS2HNBN>!%TJOREb~PiEO!j1XIcZz`|u?!I+phML-)zL`NaY=!#_c#mxMB%IUxTG$Z0u+)}(K~ z=)O^Z=?wt(o_mY>XWDCA`|BcLp;p!do8|k9D~tzystn|WLZ^~T*94LbT4yw-eIO!x z#!fa@mC4vQO?SDK?4P-l<-9~WF$E*Xc^iDbtWMv$vn;(Gt(;U0AoYR!Mu}cm(siNr z9NBcxKSZB!P<${Gol_r1O-R|AubSV9CC$|S=@38MA6jZIuS9NloL#)8o4{hEcE9*| zpSvR;F)jD;3}{yTFVAX%Xn+;s(@lK+TiPGPgF)aVaOAGxhi61@cLdx-fI7$|uD7yF zfE99*DEwaT!%&l6-fK5J+%7aU+74Szj!HiwU*KYByUvd z#t{zLnr@7id#x?bOW)t01}LD-%lX)@;)NfO1nOQMz!9wKB;+dQg%9TH;oDE7lUq7r z#%eeh>SjX{vRR;lQUvJZx!NyH*ZJO=dPOz#0qWNRZbOL5fZ3l#*MN z*g{-UhO%O>9KxVUd# zx1Jgn&bGsoYjw;4U3shbWuqRd<0WfvNjONkVPt(uaUlFQd87VORr--Ut=a*(${!>c zmR=I+fcG03(%pvIpoF{vW-s=iHqr!2U5sJotgAEQ`wZm81ci^YLW6@x&Y0OB+`buy zl0H-O>)Tg@CpFfK93g4S6D4Y7zc3$i9m}Nh`qE1HxFUP;*)BdSIC`!EpW;HQJ*@tX znM3uX8H!ru_8vP4;`*tJy-N zv6B;e3W(oI!OtSp6)?C4e7x@MM$LKm`dMx@1}$f$>qd;v#^u6u!|;jv-b@;|@F`7* zWSTiC2PR~15+|qTDgBkbAMH*^1C#IZIaMbIF0Yrs-5^6iz@TlTsGnNV zi`LT>gwx4+8^cF?#x`G0_Q9xR#D+IaCa%lup&KvE6>p(|8r%wLvmUz@KgxAW4Q{&9 zV&zSaO*nEZUoL^sFX|Es!ApX1S2?T?b(wy$Hs~bEOGNY<>!uvzG~bl&`Hcj(P+6Rq zPy)eP?q$SlB!TX~EJ5IwrO^bkoLUf&O^W-I<}PqCPs`*fqhzWT)fQalYdPN-gxU?= zM`;{q%^ z=P!D2cdEyEvjMJj-J0WKC0{8_86c{4`#00LTrJ*w>2Br}wv> zNumzO4Ja(9i(lfGzaOM2VANJKNs`tuZuZC_?_h$-qY>Y$-yYDbr@pttrxFV(I(9j2 zzxv^WVBwaGQ$YK5161(XLAQVDdO);KaXjSomnh302c>}^78k47bNS`0w?$)JE8o|o zGx^OuY;eD4A>qc;1Cb!zaUf3Wjn{j(Gk`PFHzgD1ZOcpdvknMN6I?fWL+jJG-}@=8 zZ~!KBs})|S+Q|uv`zjxrXStpm9lVKbsLvF^BxF0brt=qgHb@zQEv>OniYnIg>&B=0 z;RlV>I-hlaMKX{J9*rv0Vhz|qni0$i&dI;5fz?|%d{3B8eFC42X!lp(oU#4Z$2|bH zsC-D6o`KH%g-5lU$uI8792Cjm1&uWl&A1Be^ZKk15(g5NZOn@-Ip=lr4D^zfk~KQJ z&?wa)SE~}~2uy=F1fP>;DUjLRz#^+Dy7e)|)56qsM3A{eV^UexAR%@tyb#E6e#3XL zmYHvB58qjlI5lC&3>ql5t}h6&joN&`j;d=*<2Sko>X%nBkO$j<2CYl8Tbav0$C9PO z`p|6hK1AvsR2~x@e}GjEf2X?bjZ>dBOa8+9d+;pboNSEa17lOT=6$w@d2-#T++rCW z`P3*@FoGQ?2Z4;~+FVnozOtFx1cpIfiuA#tY;s0!JGN$Fa^>rxE0!&Vi{Q+Hf&+SN zNW}}Fwi9Oz<_c2}b2^oVM`x^mg3|Q zmcQ1~9s5l_29{2?knncA_MiTac$=|&q<3d{dTPzPok(LY3SgoM*wF4sSIxQe6M@yX15iHT-plyYv?zM^5yGGPyEu5en>4 zpgDPhKW;DE5)3OWqugO!(wF7o%Uz}!HEF9c;2z_PUv25ZejVkO#2{+bwS(ElYBOpT>C^KRY-Y}>6unYIS*K=8giLR=l((zSwqgE zF=*#oYJYu9Vzb|2O7!I@EYl!1wU1~bmAmXGjnkl966)gG-dxYej&dtmwCB8pEpPuf zk$b>0-cEq5Gxec*Tq{u%?DdUO5w77~y|^eTGBjsRsfORR!ua}`Nabq#-Y=r>x$9zr zrF(9(rNKVu%TD|fLbPoRGP8od8K7u*AvvVO!R=DVT0e%>H7}Fd4ob`i)C_rn*N(X) zJ>;^#EahMUr>>|gCW}KYw1*)m^Ol*uU`(#x`kfFGi3O`6e+)26v|~Wv$){X|a(me? z{Sc`Q=wVIVPP(XUpI&?$eGNnT5_$L#{u+xI%QEM12hKHqWntA9;xg2LOQian2vW?y zYGp{K*tO0@fW&cBh@ud*+Z5J2wLo6A69uwXKpv&Xnzs2-h4gqiM1mfNftZ1c zfr;5YLVYO1Qz39Q@GOR{2`4_#n z+Uk1vbfNCWP3hXufSJkQ?{)xgDqi3juZEvt=^oaXRM90rtpxMM0R(i9Iv%!|>C?xdbD|PwCN*DG^_>WLL!bg0XTxKoWcp{a*TR>$pLq8mY zexBmZ!Y44u79CdiYFMNG@y>FSlI!mumsGKiCF~&l~unde{E=G`VR-p*A%3z z-l=5cYJY}pmToQccS(cA!75qjr6Rf1J0B!RWao{M-7x~ln`ia^?c%{aFo;p5_R~4a z`^W`X1ZHTkXnnBzAe;T zn_ztk7P(%DP(5I2oh^F-s-ya$Oee)&FH`=JOJT>U$GNyqSAx-I2!mAxSQQ~z&;2;fE9BU|BZm`v{!Qz!{)+Q(RXW18SD4Es>1F}9l2?CTbj9)XA;m}fiYXC>_f zD($I}HSWSoZ7!tZ+fn6R_HJE!Z|<015Z$^T$PajEpC8a9E{h}&)(W5~Qss0--gL!^ zr}Q0HKZ&;tE*gA{r_6$HyqCbA9!*`n3}9_B>+GdDL;z3Z|Ds%b$Mf<=JOZ+pQ_nT| zyN=q4>!+5F+G<-^DU>+$;I);+F@k-Z#Z3*ew)Yzz5aywg%I$mvFeWqlv&bePn&_I} z)Jm~aDe4SB$-)2#ZOdi}NT6y6QJ^TD0D$MPys3j?V#s_A1}?z-Zb(ktMWsoVWVfx* zNLOId?--V53kyefu<_w%15_z*)$c7p&QXZ?P#A5+SVh|8c2Zw^XG*pa~v;06u8uo0_gU$mBhD=_Mn$hDJ#>Q{V79Q-ps|4Jy)M z=pif*KD-w#y02YVN&PI>5+r6=M0Z&KL@3$I#tBh_xUO;CD#>_7LU!#r6r&n+sx%be z6eD39;8*WXp!Ev8ZD{A1)Vo`ME!VuOiK}*ifBPW_4Qt8UD8FB7(aIm6_Z?);DC3U( z+Uw%@cxZemQJ^})LE0`~$lw0Sf$FB9T|%F5RXCTAzV7_2M2fbGgfZxn5!+8a*Q@>Z^SzR()3Ij?ieXe>~ijQ!{G>n>0HV@L;MXLQ(d56R*Qzt8`nL(JK79XWUWEU_N2 zJ8~7B!h^sTAd-Ly4OOY(osaaaza>KK(cG`oF5|vV9X?QfUysMI7 znAp-R@A40ESI5J31`rJ6_qur%V=oza%a~P7_!xPa+B^6jteFuLGk~D<>ceyo%zmeqe78oxJY40mJthjt=2tj!#{00Hma!lq7hfhUtnjg z7g1^ZIq8e5w(T(jpa`61T@-I>O#&N|4vMObpBCd6^2#bLIuyrY|DejsdmMn{#D)NT zW%mTrqedH(zgi7R|ufpgCGGDZL zt&IQKmuh(>3CkmDj-3%#U3*dYC9s0$J z-`rKw*~XewdH&c?L#m7k7CnDh>-F4FK!Myz&^BdEd%?#5NovJICnYvpY8gk4S3%Q_ zgPs_s^O=*8P)6}ykAV^sf?gfkY^%uoY-N<~c_pf>Xy1w*wtk5!23?rb@5rvPI*bN* z-PGGYFX=5MEM-;JHA#jgtgSqN{yS7ok^=uhvsl60C`iSWlFByrdRz+nvqv$X*iGEh zOrLkC-N_R6%g92NsaagnEqhTbM7syi|1o(U0ytGoN))LhBm32WzxZZ9{Z9Wbg48V0 z!jO=b3%p{HCYKswf$X=P$3LalvWzZL(}WVSGWD&OpuA1b)vMPdHk&hz?tmhQ#KHgc zq!Hl6p37+O_uXn*D7V=$i*o8=i457$ttyRXRnPd>2gcp+gL`CZRvCt;?ceeV5C8b3 zJDyj>pgAK-qKjNw!`Cv>?>F~q2AK69ayE&jt}Jl$c5WVSqXl3oWa)UrE}rhz%8xQC z90o4_03MDChGj?VIbLJ`3Kt%#6YoWeD@HBR7fJSv&lz6-OX_;B>c9PHiIMLgy|g>` zVyF9?pQM^AF%3M{bk_7^<-m?>KgXiDI2365BPlvD?w;!wiyT|CZtR09r3$eC3o)Vz zV|L&Dq2N*HJTB{21eBh2pkCt+X4!{8*@3AJi~hWYY>ffpPZg)F5ki3w6sgT>|} z*n&{qvenK<9@{WBvt69|k98WvH=Q3)?YmUIzr;|DL~cdbAV}_%EQ3EOO~^-j5OXZU z##Nez^^AL*(X6hqijskoov-m3CcJjfF#frI^b}TlsM>14?f8khjXO>F<0=KI%u3r5 zHf)xZM)f}*jtNHJnw%KD`KnXa40sU};7tiVLY2qB7Ok+4Mh|{DrqHa2S-`WFTPDwz zKf}aATP|%0^{zZrFs76A^F*SmD0!irc_M!mDYv7(cvVcF2dORce<#8!NI0B>o@ff10cLn7Fge$KQW<0Ow5ld5n^ z>#_L>I6E^|)l65`$8@Z1plnduNz=N2i$2;XMdgKBpjCidXd;R-@dHL7sC044EY$dg zbGs;4M7a6vhVrV8e~!^=7t~``(;6b&W;M${Qcmoy$ynT?#NSU-E``+oSw3xK(@cL= z{l$=O_idk4(J~+DpOz-1>=fIyr(!({wDMuF(|A{>C5XyZBB@ic0yF(9U5iy&cpH!# zwO6`w)*m+sPj4$DSP7v8WLkL=Vk-&>9&~&7@bYMV1cAE_#b{F zS29;tDsov9sqMKUx)}jVDePFGW7Mp(jg^qx)d;FL+S!jb%+v(4?)5%P>P<=ccll5k{gI|Ebnzo|!lNZ}bmcFYdM8eg@dE$XnJFU2yo#<@qsSWw4V{BDnQ?i| z*zU{IfSSuy&ngT;3rPY$Qb~*hNbZaDV=qVyBKhM3)I=Pc7|hypgp^GaPp*P zvOYq~(8C*eP=@`z*1k)bqEd%yKW<9XUibp1aC6wvA-D;qtJF7(hjMRSirc@?PBdkG z^gZe&^9pCF5CqTMQZ4XfRxm+U(**5YIgEq4QfA95V?^3_jgCW7%@tUpn*5Ik!gi4@ z=ie11Sufsv)&`%oPBn#PYx_UiMaOX`C4qHd-!)pYECmr3Byt{@V5_(F=LWFJ&#`uf zAL}%}(4i|v^yl1t2*{II@KMW`Tdx1@x3kWO4Y9ojD*c`B2xLSz(rl7j*&K~%l6lbJ zzKwb@udoQtq^G*gF)j^Xf0$F_w?mRueA8_U`80zo7B$^YZkX=YGx$>|xQ06T1k5{+4lY4SEOvxy{DBIAyG2l&0 zsAzoae!9B4q5w?+2SsG1xYzH+;4eXaI4rl(n*t`JB$f?-OemuT!W`+S)fSStme>D{ z@7urYnais4gOktdWP8f@x4I9H9w3XKUB-!LnWJxnCGC-Q5k0y%Ku))*2@(c za%65IgL*mEFpVnxP$brZ_gcUgpSQ|W`lAhN7--TDj95J856J8xy4s(8?M?CvxsD3# zl8XV-6@Xx5rfK8Z-R-(^2^Ar=YL|0D^pd zm7{_UPR@%KqC4vutl;|Z#>s9kcdD9s;vcb~{67m|U}4=R*zXAtW*vg%DL-f?p6@7> ze>EIyoATBY%0Fd42Su0JcKf;1UrV2ZhpXzJr|EqbGP z#KAh=p1_}Yz|TVT?SUlnIEBH4G%tx)gP@;KoqKGatO=bbZn3IAW|zBjVH-I<>|GP) zbQ${1o&}xgiy}~19$n+|Z}5m4eC>p#K>FQxM($qslR@?1h|b^Vir@O&;s=A`VJQD< zV+Fe|IxZyRK19O4jR>F*4bbTrmTAq<^(i~)&3|r5S1KBbQR8LrtE8<2EF+W zYHI1(#AY#zG=0v?*Y~tLob1yMh`D11+@^0~Z8$Zow;vACz**w6$a7Q>g7mSqs9vh9 znweg(vo4Ym&o(}}X0O%<+pvg>mz6dNcCgMcXFGR{-v9^NIBE?UBO*lkhgAG1$rRLB z5GffCwXKIs@PbhS>udH4WNt=f+y2*AKq|Z_{zs;tV;PK}dDQczez78!P&pKTY++{p z3}MsFfjCeN#H(9!MJI|`fbBZcR}g`;A+J|3OQ!TLI~uMCznEuqzp{ggG__NCUPo9-IXMwz=VqNT5B63}nofD^RLkkY4c*Wb)@jhX6NLeD z=;Jaz3HEpya{`$a!en{<&;(!8qL$J;bhO+VyXP4m1L$_~vqa;JT}#h={mGVVPhKt4 z^n0)wR8Fycgo0L89T8Yc`G+1q%ZOL zkQwzl`GrXNjith7kSjkc1hR6bnH1G*LgXvEi zIRBAnC?WpTc3hq>>^=vdWu=zSI=7JyK7R1RAfv8kM+u&Q18iyXc~{Tkzx*bJEPSB& zbbyo3qwcU^M%Ksgl9 zyL^ZG`rW5e8vXeu;%S~57}BN1I!+yH@0+P=M(MYn=T3Zl|4p@HXd`Ohs= zqg53PL8tjz`1S;lH~nnX{%Xxyd-t1&-tl?KNv2Hr zsTeE{mPG6J%$C{a6L0v!!tx0+3iOrUfQ|O>8OM&}cT9{}XGoDACkk~qhFt64 z-1DGQQ%5n@JGKKYiK8$MV(FWOaX97c%ED)y*h;i%&yQjDQa5uK2g}k(d8xQ>T<9xK zYA#C1v(M~rzkIfEEPq-uh4p}33>fc2pr3M$j0RdgHX_t`S`~Vt>E8Zy+jjl)VDY(1 zU(o2HlnPVz(`3WF?d?Myuqi*1hR~pU)KGb-`?T><>ln&Cn7%1P3v;Pld*wNqfC(# zLi!){$@eK9zu-8~p+0txUk&PmQEWrHKL2Krj|c_uyMJ;jn|+n+8?z+UmD3iYT; z)k_s)3DM}!!wzP-I_aetga`J7T2 zrPPLzU{9nfFk_RL{3#!5!<5t0Rnu@*oKDu;5(&ekI^+F*g0AnSR)-OGq#6%_Z}?zE18*VNW`mNE7xCM3%|uSAJa3e zcE>;ZBg{Wh=BdvINAnX)a<)37_D?*`%uPA?w_w8 zMgE=Q`%Ju@GT-!gfJ?3|5Gpsd!tiUr4TqF#!htT8mlKg8bYzi(UxCy6V=t&f7jMDG z!P@%C!N7{(fV%ofKF3dsOO{oUaDpRs0k^~S7X{S+QJAg@A%Ov&677Bie& z|MnRV>VT}MCo*31t1S*feqMiKgH4MCG0Gs-D(2`kRH@Y?44EdQP`XuW@y=P(6yN$- z3slnf&c{uRwdL{=_zz1!W?#x;L2Vt$1=*u-qW`k19$HeZ<#=}Q17W(6ul31&VHLVxZh!E41vPaoPa5T)-c8>nVpukw|voj;cq+6%l|+?ARY zUOaq18RoMS^XDqr`{RK#I?CnlA@C4Z551$ZC+VtzbZ4NK5XMFZul9gZ z7A^2(D!tfycJ=qm*a7*s?tYTH#vkD(X^~c8$u6smS$q)I927twqj{}+YP;o@|f4R>7$oLpgk@e8q761R6iS@ zkh0S@C|GHJG8lw{QuLxOG?hs`OHcmXtWl!157GN$(O-Hm_dJdXk|G~=)PB%$14n}V zn&w~P{CbY@*DX03%3Nt=yus;`T7rdyy|*Vy{2Jv%LKZk< zTrus-CjGj?Z7r+b&K;`U$*%ip^Rv<#vkem?+5|ocRhOzgwWG$%7X@(>KPEPib05k8 z&giX1A%@@f{nx(p27lFun+mPI1Yv1dgX?B(R61 z?MWrogn{yxoUP~hV(Cm7R;-qP=t@Tpumq3BNwt|-@7J$iu2XQo4t)EJ6!kxv3-CL8 zz9w5zLZb>6YoE?H&0kJx`cP^1R^rLb>mIDi36E}USj$-Y+j;$Xkik(q^^9|NRCx9% zX9j_137?Prc?dYvksv=*-&t$EpkBMQGNGcBT0Ixum&K0>ISuN|j8WZ=>oQ-g);=1( zMFD39naT_FJRImFE^onoq(CpToJN5Of!6>IUkyEVv0kROIcKBuRQSbW+jM1nK_X#& z+TFd;-Pmu>FgK-cnT1-3Iqc5H_WsnjET4~B?DVM^;&t|&rs%@y>9)o@7P7oxU%=Gd*orSPqsYXAO=E-+*>L zzRd*I9J;{<1h;Z}mEbV;(tHm(;%%5Afw|gcDd^vc4JrE}ZCaWq8mE$toY#A=Zm5SQ zhQ`LK%=R4E$TB)#B+&Xkuq$%--R>)%huowZMicLQf)v%~${f@vyKR#jelY#{_1jSy4K6_{ZuvKP2YsZAcy|_ z>}8B#Zm**%9C6COqJWNZop*nCv!h^2G-c;lFtRzGG&@hZ4o=CB&4)oCSbP!h%w=3T zVNy!kgiMro-(}C_^olw61lO7&Q$X0{o7t3y*cHtJmSq07qw`1RnwR3MS#Ua>Anr?J zHimXA=O-)z6vu}6t5xA|34d$1sJ46@O>t*aoIK_|YqN;OM_I)ED|WqyWNyC3A0iCE z`-b-va?L&R%{k5OomkYtFgQKY9(;10;a$#PF==o|%F0^=S!<=yb}5q{NN}R_0M~Na zDAp43k7@PPiZt$Zi-g-?y5$0MtH0`hgZ5|85hYI!&IMnO+t|5>t1j`p_*#~zKBvLvLKiU2i!QU^y)(y6{qC^45a2w_+H7Ds?OemU>AlFkL4vF5bpHmsoO4$ckDP>6F(TJaoWqGO0T zFYXV;UA_m?e)Rpgmw)x;S4m@x6 zz>#ctdF%1nK`;&86?kGTh?+Jd+0T^?Rk(S-=_~AEnr&umF|@SlvFQ)OTN5TK$l88> z*J^ytGP#hQrNSf1R{H{FQaaP9=HoBr6UtHJ#-O;l>`k!?G6Y?K@{!n<9E-h^4Qy?p zsl(#d>ES&YTgMSys4Cgu4lUdi%Ulcm8#=pKyJezCK#WNWbbo(_7=u@yF3V=B2IWVY(lc6w%6o+f}r2%a?^!v9zMzG0bVWb<}8>ZB60#T=>aGhJMr~+ zb4Z~*GNcGDIjZ&VfcLEL+MaqKiZQ$jEBn7=(tPHTKJK^H-yxgZ)0ahZSFYsyC8X%j zLnYVp%Ph*01&jZJ>17;UjQV^3D|XPCg4g0!Y~hBXDJFqa+8x^Fgqb=#>aIKpy!Btv zwc~AjcaDbfI_6ohZ{S_@DoQ6917vBZU%Bi<@=t%aX&-}Gt?84g`sbnC#F?U~f-H^P z>Wt5fOO&R3JPej=OtT@xwe{R6UAXpGxSG4~hhIHs) zyiVS-Njy$?r`|s0{vNW5=6CWTaflwSw&gq}c@V|K9s$eL=dc z%RvbHMy_bQ+OswZN>!n?Gw&tm(s;=0%DRYnUN_46Oum=AuX9M$4d@e2_5TN|nxRCw z*6fptl8Wqzl-dMz zZXFY#^u$xU9~NXu5L6Q9j+arRSn+Mn$vI%MKKT87ACdeQfrV&3B%49Ia%Nv$$C!3$ zmT0&fer8s__M0f-rsM&mOxyji#eES2V(Y!bUjky-rsIH;j#$G7;f1bUp;O%{O}!1} z15DN(x^e_pjwsM-c&;OmON4*`dE(511RWPk*O%`K$E>X3bEz0&*#eQ=`DdYd7rF-{ z5zlK5zMq;m$d@irJ!Uz)Z2)x$0UatW3cb|s9kPtora^_8X4QK#MKyd))L-%;lZTF- z(fjkt`7)6N`>hpT=CV14*>(a$y58jF{Y7U@;SSeN@~aU}t47K{vTp zk&`%0_Sj1;fpuD~j~w{!TMTh;A#0f@dlpXA4F_ZyD2OG=bJNogl7`z3&iUT)NvVwu z^{I(d3h3lQ4%+j8gWlsl%uNTt1|JpL z&$c$CM0`h6Ay5j$lkw2XOZ%soHcxVdvIwffoM0Yjws){!NKTY)Sj1(%& z>$$;lqI(yWt)tPsZ8juohbQ5Qby9g};|A#Kwy}3XN|kYLjibm6=JfEypu}3x*2~gm zJ0em^x}3^{wV>+*>v%Dif}TH6f;}9yPVObcEqP0)$D%p4akx#)PU>cAPjxyzbxu_e zyp|mJJ$zB?{PORJ<*)iGlpU19PUMgbSeocT_v#fgYk^$)oXN<9DJQWb;%rM7_S$Ia zqPKE$#l5g4{Z6$}7Q$?Z3ts0w{*6<7>sDDxq`lMFd?rOAc>c8$MEL4YcJX0-S|b!t zU-FY0{xNt1&L0YrbX(=Trrd^N?pd zsX=hHcjdtd!7u`-QyhhYHG~(wH>0bVGGyfcc5$sZWA^4k9KHLkN_7piK8>BL?^FX~ zSHFI|DRNLc__DD_e#d>SF1B$RXUAuIumco|oa&i;3{H)cAcN$+b~(9*afsbA=f=0$ z-||5kr-UpWn?aW>S34@S6&h-^d4lgw5`R>#O%7K|F+1MjUQS(1U`~?J1e{LdCHY=T z5gRJh`2qc?m;OH>DH0lNe^127IiAt@NWJ5J^lOS_JLp@pOX8Vaqg`< z-#eOjE70mH9Il!!8=>U^36Q*?FS!F&Se#D&VQC`26Y-sU>=hVWDIZ^ zZRl;LkDnK=>$gp;RYa&A8`1H`;G+IBMq*7b2|-hNIozeY0-ZreAzX8EXWR*eUfl%2 zvxZn1z8ji2mPAklWzNg++5dr)Y<$rBo@pM+C@*D{7S}ACvXJfEl%+XWw3{hFq^ce{dVHLGukhx{F(^^w3AilHBfArBs9AAT-%&xFbhsfVq~+`$i>m%PX|D~Z74 zj_DcRrOKL1f+TIdpxO1RRu3i)G;O4=;!&t!mbj{8#UIIRtkYNCV}_a)&r|;1lhnP< zmb5-tb@&l;fE;=%FG~A%k*lz2!`_o?Z|Ne_%xgjX?GLYIz2o5GNuYi=F}l;bRy9>V zG30sZ`*nOBx1-Ct`|8;pHT$w2Ram9=<325%KS)YlnvU7yF8hHkc8{X&ape4r$Q`sT-*Mb8)KahgLVOwCzdx78i%ev*qyE@T61EfrS)5hI3vVp@95*E+ z`&=)|KV4tgYzHNa@3k|^bVYM5)!?5}F{miYOmkOq+)4?*i_|&Wd1ikfStwGQ#d0L` z&9Y!i3+Vg@wRwnK>6<0_MI6UN(7tGYV(E_^5=tS2Wcs|Y3V(#%_r;I1zMZZ(C4BsE z`|o8gj{Xk_wQGKodT}U5M!R#%YDBm`s$EOvRiy5z+&RJ&2)KH{8j}D?&FTZhz?&Dv zr!VQd%24Hv%ZR^lP;RI5TiNkfX0l4o>l_#{yaTq!?faH6T{GZyeu+hBULkSgPpS;D zj?lG*PP~D-n2ljm9@^TH^|1?TAH92`x971J5c_tD_s{A3B-idxjITuLQ4lA*S|B$I zLuz&LK7Xm^e0KsQ^V9d?Qjr%av_0kDJa1gm7%XiK6U6n5pWW_*jM5R%pV6gB9b7cW z$u!oi<^;5CrDUpXFMfHwgl}+!vp(>B7^F=TvS{U17T$xrnPvwoG9m5Tn&UnAc^ zGg377tzebC7Zt(cUlNR@a~E-a;A)J)?|Q-zAf3{yjVUKU{a<%szPIhed9w@eX-cKV zaE<$Et@n?VU9F#-uXMlf!D7uJ`0(MwsmV_1$ratDc1BckSBp2G8CGy(?bd5|mJx3n zG}85r`)x#DoM>!QH)+JqCDkpK5JCjfOAQv1+&znxITdc{l@AF|!8{nUlLF{tYLiLARL z9#O?$5cFajLF$U5_(d+>o-A#2?ex^<1$tlXE}Z1`TaWE#<>vZ!TAbu9_;!{$2~EWO zo~Ah}`j@SSh7IgbT|pD1Z}w9Z3W*5sRE=<+yg)>(7ByPvj!rcU+aTJ!qxe7}Fahin{%<2>QmWb@gO8^0>}b z3e`&5dA_MePy6jm$@cWNKOO?txe_w^~n`oY9@T(#>S=v3)nV8B9g{E)uTqI{sn_nlFr8Mk^b{;c7 zI&R1*K`jPk%j&fl6)LwpJ!P%Z!8W|3lEP^f=LxwLW1VsWZD3+f8UwWVfo*Ui%F_<$ z&_jt@Q+HrDz9)tN+kYzHgw09>t>EV8?so<2FCe9p(pW1w9%DyOjdg;Vi3Kd}3i+CJ z1L!SYv9e9E%?+gex$<85n;^YUrjHD0r2Px@@+>c@U08Eq2J#Se2Qu}|ve8ukaROeg zY`81Qxujo*v0j>%nE3W!e!CI6Om?I)DQY{20Sl}TTykzc!$C1}>OIOhoG!B#Z~~5< zzz{;)81JJghmP}~a_C)pW3G>g_NYGRc7-ge-MFXF4b2LNvB%PGWs4feOlwhkou0v# zz+QYm^EBvmQu%H_$ouv+3WaUOtSdSDZQbzuza7$)OK=OJ zi({a$ST4s%Re7Gx8r~=>3fhmIT610+QZdGn&Uj+;oE?^TC|le2eG+A{KyrKD4zZWE z>@_d{UQ*m1Y#wmj@=xLv{>?Y0bYDwz#KSRb-^@h%O1j-TDZ28q$jkfe2K z^{2*YP>?(PIhWPsNj`xb&H3Yfg;=Ssx+YneW$;XWZXi#`QxEXSnnCTyq*~8E%AbuG zCUiGSoZo~00*Q9}{`d9IN9DiL`3FZ(LP@lv>hNh7qFaxxTBUx~XV9!;AB4f{%RnK> zpkoVi>uij~jpdJ@f`5t5$;1dzr1LnD$z}zbU)_$kGW+PT<=`_A(_Io*kM@2fnzc-O z^KOG7b|TJ5dSBZY-2WFq$n8GMz?^3T0FbVyEIr1o9f|MWI-d<95|I&R_ZC$RbcI>n8Z*F=wZP6OOIp$C9v+<)KWyjvxLG!UP$Q`rW&6MKDRL~1x!Wec zSqGzLuOmHmWPOI-7yal^x7JurI-Qk}4nF|Wdu0+d78VSVACYIxt1|e+%Q1{;a7X4& zc)mt=M0Ko{A^o$Y`j*&NHJ^6P(uNGVfx!eD@@WvNNrBufM@wNMVMB|PYK$!bl%;KEy&Dn{t zRc+&alVZ+$9Av6hFT@iC-#68sor)`KOM-m7_=!aGLn<#MaWyxiA9eC^V6b#PnCBw?V486I=ifv zeJ{@=?bhEMKQ`&f^*g!0Ty_U$>WgLF4}l2M`(Rz6m+g&5JO7ZJMgQsO6kNS@<(}jP zZvxJfYPc8L!-~*@VOp3GC9;e(#B&>kK!U*uyo)y>lB+%B(ZmWLZ3|rvrc%daIRU8K zkxS&)_w2+AC|Fv+%es(zMtbq8*m*BY_r}m1tF6Wz;1XO*+QC1+QZETLOux}xKAy%f z_R`Uy89VJv@j0UsRI$lKb(zci6Q#X@3i0En`!+&Tye2)|P=Dt6c+YD^88$NlpCCHi%O?LD}3vrxH=bUq^0T zyuP3&tWo|Hxl9%Rmrt_`ndzV?QPruuqBdf=`Ip8FnTy0>EhGBepBBXh#E7dj0`|z9i<9B73IX6}z zrZ=xKarp`>WT6xM!T&hUaShhecDz8APjzRZ@*}oMr?Ydg zC`YlgIUqbh#q4tcYHI}{(r=^e*ak+=6kEDqw89#Gg|>`2#w2hVXA%oneR7k?(%75p zG5^EfY`6KcJ<6#w|NXB%z~;4>#HMvuS^`{wTN3wYTJT+>Y;Iy`J(bbLdMJ+Lx`lz`8~W$~Rkq@6FermKuu^;~jRH73Ee(4CEg39^(S1 zW{RGkB$|F+LPszK&TAQPFqCCU{xW6^ZFj^5;E6kM+J1@K1^-d?ueiokrxs=`EH5=IxSPi2A1&`-KPmyYN zfT8Yo>9S*+rI5R61=&CNxoBhBXpeAsKGJochZfPbeCjS(t^FArG>-s?zp~VgN zW-8Zd4Lk6>E(iHDaG3QTrRXf-cMZw<2WsW!H@-5^$eLHl@>|pqJ7A(I^;QT`*ifUn zihBn#5^H;t9|96z(Ey>$a`ogTEIj}ezg|lOEAYiIG1)#jV+>K5fLRIIoOf6aqEZcEPYq_EOBGceC>a~XN-`t4)EB$zw2dVr86IurAA(F^(*;twb zTj{wk!KUGI^W2I@lpeMNCpkTA8!eNqF}I&a#DY`^TMb?r(b-O*R&tn)fMp_~1skq1 z5l$d7JwEPT2Ia{nC85Wy_XhSLLj;u^O5wCsPw*=rA4qkeINc+gdz}`|v1zU14e1V# zs$_d{&*@BUv13Du&HTA%f1E%B$JH1oNN?x1D=SWcnTJ};D!CI(G5G(rYppZ@fbwEL zsqwyD$MuJ5)*w39pukW8POPsb7$gL_pTq*=YYJ4>yD`Vimw@85{nZ^1Z8!q8sx#ot z8nTUlzvQ~|H81&kkAKA1KK50%0j~k`fn&)y=_=f;*I!(R-m^f%^iOmWxkRCL2)?2* zk_1)ZPmLlvd()QJ>9wPuWN9m6GoC84nX*Q*5o@?NdiD;^D}$znxkn~r$faPyUA$En z{8W~zKbb@?XhRykrNO?e=xCa*sH60Ug5Nso>qm06Q<@c&H2N1cY$19Cp3id>83IQ6 zCn9Kmm0k-!4<5EQv~;G;o>$n-dLc?uQBP*Nf!i2xtq+?7wm^I zHY^Y;2K$mKV8+XW`XtIOkQhwNws4?p%0{)%!xaL! zBTnPDWjsTxT+b`$4iT^{fF}5YywO>7%*HCfQSy zRRsIt^G}Yp`kYp;EVpnYXQ(_&^P^`Mi%8L5A(mZ6D+hf0Oubo8R(L}abqN7C9bOv? zxdG|#kT#NJx%F}}WKJ9kA-gOf061Mh4AQn#pU`*YlzI8M`@(FCePXgb(JRI~t<=s) zO;Fu@T4GT*J@zSQmY^f(*eAJwC#?=LBP=*l=?!s>OIFrm&VglcCHe@QRL5QDmG`EE zs}QDh<@KWd#Dqk$m!g_a>+*q9>OCF!S|Z-&4n)(9vtjV=D1Pi)l0qT3;n#f!@cjFe zu1&O~6A#?U=R0bjQK9O7ScyYFG-=oq1+0>XA5*%SdRagxi(b>XCzfPsqT2|M6&f)F`c9TBb%30OLMh= z^0zNS?JUwu`}6pg?kI7t67DVl$7unKz^EgP+)MamsKZ6x1*&P*n~)iqx~3X8te=~fhpL?RN`dsSPyrpx5?wC=s_zKTHfc*zj@N$ z{oQQv#_5;vI?j(_=FF?bS@s|4ZIM@;atViEUwvTxftU@<1~G|;DAI-lf}GMqqUsq2 zdeE3C@KpOuebRvA{Pw54-?}vQ;@2aSMuuwHIy!v%fT&YdETcKK;8{}&qp-_$Ul;u& zjASErP8mE~3ew3^7c8aCx7=0nAEBETC+7_6-)Q#6n9^Bbua($h;@(ck&nf;P<|-&n zu21UumbgaI|IW2kJuRg6x8)MFJe{{l`ua^FOB+u|k1%YNI(^1GX{UZ1f9MGkbJ;Z& zMs7kY=qy>>|5Mk#6tvI(p@1t7T|p9>(CEgFoz;bHB-$5&Ib+-SXCT|_Uzr_nPq;>M zHR_6fPiCteqAFDv1L1{-c>Y&GKg>+96?@BKS&lp;b7eg@6w*DCOKT=zjLviPaiaU; z8+vTZarRZM?nj5V8KypkXB)>NN%|6U*1y0>f)m&%Wdf#vN_oNexJY1~95^AY3z(4U zJx~CC2s9t##Z)@w*0=+h2?LPA9qq{hilGgpCZbh^3JW2*)LlZ6dv>TXnr_)`a(-c?k{08M6CMI-Wdm6`{YF ziw;ShP!?cT3END?MK-b=#2#aV8mZ%Z40n)fkX=wr zp<~`WUW>YLeztp#{;Q|D6Hg~EgRk;~3L^^>3p4+2_+QWG`}c1JRibC&H9mbPhjYQr zeciuu3=66;XZwqMdc(aroT_AC-?pLF##)htH%41M$M@ZeWZE<+M@M}riVHk+R$gQ` z}bdl!9s5{_7hrg2j z+CT5X&^CaVjdUyfO-?Z^d`h`FB9YFZe#gJehm*S#dJyQF6w^eR%NJ>fS802k@T-$5+cc9swPeD>1N?b%!x0^5tU!^R>UWFxQ=A2nSAA&r zGJOU&kK*W&Eyc*ehBbl!l(>SHumr((fkt3b^PGpm!x8J{DwXIvG(4Jp9p_mmG4dc= zD!M_r(9pV~Zc}xhQm1pt=s3O}zC`YzJ79HJ68bPo2Cu9oR1=#}v#Cpv`013m#V~T$ zRSZgQ55F+PIDpaUycMs9kfsq9Q1aN|jZj6G$H`ONwb+ z^551IHYT`DOn!Rtue*9`O7R_rEA?QJp^uo6COrPBCrr8aE^h2tJe}wuC6+FZ33-kd zkWHIa3~gVRN&Vm_PvNVA^Og+m^H!zJ2z$;@YdK*c z`9e+M33)_3gPfNEo?vdh1<%19YRpD3ZzbO!-C(15K8N2>0+qRm8Y7ecec<3=1`yB} zVjJ+hQDwVGAVf`0(a)vxx-dqgJ#HCcIBg`q6iEY2i6oeHfwR}@61aP2`{J4JEna>T zd|AylMH>%GM9xz`c{SO`!zR5+z-+{P9e4_WY4I(PNLMe}ILd6up~E!T;=5O8-^OT*QS|#DA>Z6f$6GV`xyX`qQ@WDNU<{Vp;CIHR^~!DV8n7ttN}AjAlMb zf6Dzdt2a5TY7ydLwGd@w39hD&2;(><7|#q4DXF60Gt7;?=lOxovIj;i8?iaB$+*sA z>Ui0QVqVOV4T()=BS9E?Xx77*RI!u-7Md_zLW2^);Jzc$6-mLJ`KWqSlNnvaM!$x* zG(>2n4usQk5}8T3KS3e6jdgFjb=Bz|(z-Tpub_2_F@OYD=g2v(G1 z%ShnV^3-UAsuU{MnDv~48NpSFt076!&liMqoXDkxqhWW(KsZ{jZF;Kx&N(_Q*MOPu zY;e}BFA1mMF0lH+Uz{|OKrq1(pDrdFYke@*9dJ9Ly?JgPXLlhOzr?H@uGGBccIY=<8L8bZ6&7LFLI~M=)@bTZN$!%O=h7w zS20719dAo|1Z+6zcqW$_Q&+`T8TGHmT$hJJ!@2mQbmozRZ6tcpqPga?=uE%_WE;|T z=j`=2!kl3SoU|{<11))nyY|Y5I;pSWT=XQM8#s#+kmj?{Kc601qODu8LN;qcXeNqk z4Cjm1q(c~@`MU9X`_E^MkCJ6NOw&SZ(}-w1hr?QSfV9;t^~KaMwPKiyaO~<>! z96j?Td0gbbhtfDa(PmxZL}>x(ybFMeSvhRprV(;KG~@?`Giy;X-!S@?RZNZFov%_n z`C1vQ@H%j0c35>_Qjmrja3*wP7=T}WtiF!otITk~S#E193(f2L^cdM4+?&O0ah&leu;A#mVmisw*&1A7oma>Y#wNru>)=pmwnZhL1FxBzn*G}-0KxiJt+MJnGVmb!dsAPk94(b?FZuExh=srvp1(aZQS1V`uq{(Qu=PDH*@ zce`B%Qk#c2ST-+e3xB6b+mY=0sH5_i z;~9w3OMbdw-DUjgxKcPI8D@7uW3f`alK2#!&l3sjg=K6OPoI(rmZ)a`6aM8F&XnQ+ ztS_m)-t9!{Ol!RAHsAY6>_UjO9YDTiODkw=S_Iy!6=Jz0Ide3T)fXIkY_C346-pU) zVU2$2HS0sP(=39c;RwV!i3r)u@%{jb3fF$0>$h+M#pvTcc$XT*`Ln|NPCDPDkw}>8 zStXlZli&ra+x1h|z9A}gqx0*tkM|(5Cn*UEFlV(*kGBamZ5!oWve?bDY^Ff|$?($K zWEQ*{%STmBYgm^+L2;v&T$VlVHtqLC%jcAdlpnn&flFpx_o2@D21Es(zIeCX;oCY% zL>RKsB((&}tNGo*Uu-R}YJvWBs@W~ttWC0S;AnW(Bd{z-_u=;yk!{gB1>$M{-dGiKNH3r5{chqziHL!aOybeQW9j*zkPLg@%frRL)#e}6qcwHyi0jZ6 z^-Sq3AQ|c}9SUAuPL?qJbb0`AWD5}GHW4t$UL`_+DT1^!K~GV2zOBLeVdyoe)?VB# z%?0gU8@lo&Zvx8xMF|)xxy`39*ISZm4^b%!=qmil1?9EgU~Ip07XrtomHX(kL61@R z!Lp5fsInKk}7P$gL^ZV@ksN!ypws_H)PY<`c5^&Q%E&gJY5HcF_+F)CW+1? z;*_FsIL^OS5JizL&aL$&&8AYan&JHVTMuRD+HX|GVAh`g45<|XEfzvf8}gm48T~`~ zD#-1sf=K2f2Z9FJKRs0+X|H<6S-Yv?k@0p>1IxxC$mf7%;=x9xb1bEogxTytZqkxl z5H5PFv_7}ewXC3nm)vk@0uyeMahaH9fn%LG#DnFcDejCP+GvPD@J$? z;|Cy%nz6Z?x}u*-F5TZB`Cr!3iC85+X=N?GzDHMuaU!eV0%Y0}kCc z<}M(KjpHE-y1Dp8iSNim?_wF> z5(UW|b~+|`85slm%IPmh`Nv##oi@Vu75yfZltVcRD+JY8E1Di+;a7M+c1to_5X0@$ z``E>9!vhQx;}w2Sn-^VR2}&^@|LIb&n_lWsA=W!)(~C|;HTncEAOJ7=D6*){Sgy?7 zuVtjSCbD4U1D75e1Rdz%`iY7hIs0>0DnVh05v1!!9T;@hJ5RV|WJLo{)w?r-&1b(8 zJqlq(YWel3GJ5hj*8i?o9Nc{$7otP2K)3KFiGj^`*}A=^?JgVODL!BRY(~%p2ExTG z5$6`SXnDQYDG{_hR&ROdOmwA(OYk5P??MNl|EnGH-PHh+mUFt8`$euwUay3U51RozIgyV;jYewi-u@ z{p1god@w$#eEe2UrXscNeU58(P*58*B`Or|nypPX+0`H-P0NE(vj>U=jNK5iD2SO8 zarq~mSQa1;m!QvPuECa{m?$BXW90<9x-n5yT56cgkO)@0SD94m!d|FO^w<_V%vi^k9Dow zaXltx&T<`QD)bJ)8G2Fs0 z`8V90SR5tn!L-G|^=#Mv*mU``-zb|WBFRYdS!}r0WLh~j(!SZ4B)koAfoOY({8@q# zcBSq;xVx1B@5k+(6lB+8h*c1yjT!h! zc+QU#!s<;*1i6i(g;fd7f53rba?NeY^P~OVcZs|XirxnS$F67w$FU;+y zX*t%1j#Q=o{Pq830YG^30KNVLULw@$Z4qbwL|@UI<{Ng4?sa+LiS#B44Xo&^Km@56 zXr>$!2k}EWV_ItBzbgT`ahVO~M-XePig5sJp?OKL;YUljn_lux4Vq4AlrH0T)4D)D+?Njo8=V-Obc%G^( zx(ttGwajipmX@=M`Q)*RpZ>wm+T0=%ZOUR!q3%n&BUnG$MoM<}rW=E)FoG(~=_{S4 zXZMVdjJ=G%F0XX1DNf{nVHl}h^PIyfHVmjNQ2h$F)=0~-;MKW>(r!r`NM$pHudIwma`D}bqqudCWv+QM7z%UA)^Q<{>Fj5cG3^^dDtQP@WIUo)ngB2Ke zd;0-}5;?RB8RdaITR z0C}xIH(OWzDkhfg@{Q!oVTuaJp$sx zSrowm0=LG@z%ss?AqHRMgBZV8rAPskm%2&(AVp6I%oLZPV@Xs38xgnFY`54#toN)M zCu7kT6S>|h>J?1k$*Uab)8@fcr;K_H{Ra+l$9g)cHP`Al5(oG!S(yE1B8KyUxn7n; zo#$eSAN2pxx@2qO(Tycng@ie1e!Ws0`{2mK<6_>;VG+>T=An~DjH{6Hi05qY-{q>z z5i*pHrqpj4&UPX#(Z=BMuH$uWXu<2ko>0E;VF+yLWe%vd<#z`&=I#2!VC>T)SEDt@ zp<-s*^uCms{99ZSgPpFx0_UJ2ft#II zW|ZI)Mnh$#T4^_jU=J5>u|)WgX3jkI85a;#EImg0*{1eg?|mz0O`%!e|Nn5{EX|?z z)HWrlMTl5+M#=9A&uZpVUNsUyQFZIZOz5AqN|RBYGQ z(nz84a)pHi&r%$obOAH*KZKAix9S)|9@H%TQnr#2lR-`8m;9m9rW$2_`#79c+|gq~ zM(lDJz4YQOd#BjoE1@c*-D*;Ue`J8_GKnyq?48aKYywCoK2O!@=TyrPLKW~xu{PDlBp2tx97d(H?QcTo@FdlzYHc@g#kPbPpJOi`#(pHv_5Z%0fRG-Z_Opam zps~OE1;>-P%Ck;*mgH?IJ&5;Lonyx~jISHlNO@g8nkT)FTG$~Ep+;!uo+*^Wv%bZB z^OYG62YDBs1~`K0iudl1Wa{q-+DVJ1rc|x`J)ON`p{kGC=QzI`k<4ij%;juzgrk0# z2B0q>sp!>*6zbkZJnY=}BCnZx?&P97cu`5BC> zaa>oM2o+aCT^RIlKOWlydSP?01o*N<3qUZDXNc2GddKxK87OGQ9mTK$dc^;;#_xy#e6tcul zR@*fN611=1DjEE{R&*>-v+|G|Sa%tdlBQKa8OL1m+@^L1j@uSs0$F-m925OY)r@w7 zpqa(D^#Xg1Eiz7U^tGg}0vuV|O~D>0BK=^w_>|$zHw5DwaKvTNmRANey=!m8Gi{*c ze6QS`m{4<|FWU)vhd5QR_iIGVymP9_NYXUSeu5n>A{>lQtRLDbbP)24faf_!yqnJE zSub&Un+|`Mr|$i!E0rw3W6BB?gfb~g(L}k7(}m?jEKET9;TaZBV)Hm>D!P}95!-Y`pPf(AssBc}l$r7D0{DUx8#`JFHN5>{hA&@@3zntxS`0WcDW-%h1C(}}A z*7$6pzWX}s+ewr%bjp4RpP1r^_gEa%@*RR`zq8Va8bS+%f^fMI=dWW!?S2QHIvz5g z<%u%I5d>CbABRJz%^v36mTE~^Pb}sLTVROsv6M4R)V{=wTzeyQFsij=nM$Q73zN;= zftq1x)A<>0qjt89Pbjxz{-r;rr&>{XcQ1Ik67NS@dKggaVs_W5rJU zD!IN|8b`bt*huCorJo&P^FV8V;QB`$?fDj6jAQbEgw@2GU>~biPKy4ZR5$CXoIksr zV-mMbpo}KW?&tH_yZQHX^vAiVctdLdq+Lt98@%|8E9;agb8VFxh%-Ee)(}aPNx|1I zPGw4SMM6EtXtCX^9saX+<>;BsueqGaLJWG{EgU52JTnAlPp$QP;47UvCrUy0M**~l zSvdk7V_8!?+?dGz;?w0wX{!x`B|gK=XgN0Ianxw#JD!GVO=D3^TIEzBnt#RUs5Zbu zeF%irr_eN2WRsqGLtl)%{n~&vkOCXBx7^uD`TSE-ZjtQjVBBZEVS0$v1B2NX5E2-E z#-sBH;?nY>nyqUcVq=ZvK|EIj-LcJE1Q0Ee-Z%2oq*y|x3SXOU zoNA|m_4i422d7_W_Cq9Th^cXC+lp%}KKiobk}Ho-Xnh}tswY8ieGtu3bQ=TPU>02* z8nQAYjE&drZW(ZKb{G49{5~67Bu|klGl!_};xCNfk~4&w8In9YE^B3o5$>UOn=^gr z6Ft``cCMXs!Wb8#Z&djI5c;K~xqkIN z6H&Fh&;g{0{ywfqh%jcr?a3IDIhIL8NH=BqxIK{Cjj=>RzH%6#s7vm!#tz+psElpI z90B0F?dFs9!CN(B+ROcS9|>MwDFmor{NwrFq!thSwu%{=Fd;VayQ)5b#n$NFuoe3CW`{SJ<)rk6AQ!Bw z)Ct$0Nq?#SUGh!h2c4VacZIVX{i4r&B2=Ba*~4(el)T6IYLek} z2R*pxHyM|L2#<_T>tG^;xto}I*^ z3z^FK032=+WNrSlMFLamKYdztl#aTsS6~&84_o%Cl$qgWy_}o5Fo)osZB9rUz|(%X zW7zLf-{h9!SL9`@k(iYCNobi2ezHewW?<|9cbM@Mj5*|h#tK1(d2srW&>qLKX6cy= zkX;qNcB&v*J{&1bL=JcoFx!Wb_%I$TMa!f&5XN~GKtg4jfko|E$A14y^ythd#|g@2 z2WqLG_jh(H4tz!SL%5EN07AWhpxJ>F%c!$UD)E68Iz$3bUlG)yKpGaOqZ!r3c=lc=1oHcIx^2_k zYChsp!EXOWt=l}6aY3SLbs1Lseh!`x#o5QEqLT}cn<%0~U*-^4Zo8><0=eYCdSC2~ zc|Soco@Gq&qDrq?UG&ma|wziJa`lle+_K)4pVx8-OXBz?kC~?^jIiY0LC;=VGT2 zQ3BWsaEH71DfJdwM@T*OBTpKWq`jxN&{RjKL&+zDkD4nipV&48n}iI?hF5By&A@Cj zOS-qB4*vWviGvseEJr@^7yJ*30*~t1jsN&|>+a7Is!#;cu@RT{jB56a1|_y(%~xgU zN=YVJQFg3+QGJ+{V1|$J<*gx zzo>yz%ntPN!F*%qU1_eKQOsi_F$;QK?C-iank*70bSF-dAZ#LVIBTGm@HR>`=IAa+eKJ&Gd6^H=T`Xv zLD4-3-*Im8kJQF-h+W{!GdMBHi59m{R-JC;>BU&vB+ap7AJ#lKa~`;``CswMYrrAb zTQrbQ{0~hb+kf;rhiMLO|H^RnGr*dfK)J;P2ePJcv4DBm*J)%kOBqMgNpw{5 zUh47|N4+U73xJysXC5`?*A#St49PM(kl+DpE5Fm{trx&1=!&ZaKt>DFBeW*Thm+Vt za8rS?n9>?3-!WKIiK8zKrOXfZQ0*&X$X{TdkdYqqiCNM?(7u59@|pJQt<1xCIT}Yq zH2fmqO>J{g@#9Elq9IPQ1}e~rTtz>t(}V-3>pj0YymIQ}D415(WTN|3)Fk^feL%sb zxCx|sMFV?jC5X_i$Jwvq9&wzqq5QMuDbkVrdg)%~6Nz8yjzOpD8*yXPzuS2jM9}d@ zY%KWl%6K@wG2X96GiPCak+tJnal>usODZJuOw=7j^=*E9=+0eUlE4Rzm}^1iA@L;-2X@W1JICm!qKjD6|ZFH{6Am}ps?*7 zu_{cq*G0zmA%%uw2Ty1w+C5|U#0#&q$F<9pCn~-2?YqHuRlD-o;IuVn7rc3>QK1<6hu^e#mjZW^b3!Yu${Z3Pnk zkt5YkfLbqr_iLNjg8AW@xoO&~Mi+{g8s9BbcpA{9$RW*Wq75pG>va&Sc42_M{{W-v zRUy}`zBN~8Ua{Vb{p(+^XbrQ1Y?yY<<786GMUtsI@V;n?(OtS2UlNfHI0IY1KD0{K zAm=F3{-g|Epm((^X)31TF;)e3jXXZ&Pm;gqQzk=f>!$gWm8^4HkI7-2?=YqDQc7Zi}QVUq<^d_RTGH>&eo*ayUp^aMUuWzT za~Lg6DmNpQu9Nw-bW~MQDf8o_N9*D>Dfgc}RLgEbH|HKO6YI|YuwSlvX3DafNK`#U z*O&-264OQw-KdCL9xPLCGDWQ&y-#j_vy3Wah(KV-t^4^AnG5B7`F!o&62)HW4fJb<<%PKjSLJ(x+EZs>EXx=Alt08uBcm_-#p#-foTuWoG1r}d zmc*vxzix2v+;TVd0V+hhSn-SFLGMqKU(@eIPxKr>s!FctdKCWxLeV4}L$%xK7~V~U zO77VsUkB>Lyk+!71zr%8e-bMfkBhMuyPEj$Ow56+@R4w6E~+qy)m;Qr$sw*W^T_YG z##`C|9EF@Kp7iw=@=~8T1L)WO6Py_LfF*RO+*sJmt!FbIc z`ioj$U9i@7iK}ZCoLlVk!6O)z3ut`OSbE$-BwK{{SLe<%2lHq)!MMx`hF>b7EfxWc z`#VIj!F{J$lm9vEAzl4o>hiZy5k=dYB1{bvDi9(`ta)P8v(n)|zSS|_3y z4ap%JBuD=zDwH8L+5isE{o9^;3xSHd`jLoP0gEX?%-zfG0BiGdAVqp8l3v1eY8W3p zZ79;p`5BxPRI04n-6cDG`yG6%{DL2y|6AjFE(A$qTmB0JG1ptJkVi|x&mDtNI@B}> zOam5$^o?RP1A1c3RBn0FKLfgtIQ{;E82TZCTxaa0P-%C+I@l(rKdIut&2+e~i2gf{pGHl6BMP^XU^~U-9?mUEC|Gn|&T-T7J-bADsiT)`#)|?azO5nmV%pZN8 z>wI>Q+x1f0+OOc%5uG^83(RIxZifl+;@`InG}X7B82R6|NlQXXo3Ph=>Q7t%6UK{D zD?@9OnM4IQR`bT48aN$JDW5EUr6r&G!14H|U;56s&?CgF(M6%yj2w^YAWynJBDQFx z<}M-#!q+1k;J7}}{NMa200$a@Hk@uI*w5Eg>mHMQvI#zp%jP&l?4rlg=HZj_It(T07f|(#9eK%$V4*LcZveD&qOT8g z)+f;caTLz-v>i*O(n*!)Xpo#KXSlH0L$QdHIH9Fg>Xu`oM`Ff;a%KGC)RS~KoE5l8 z%vWJJYNRaDq4`~=AQ@sw zH{D=KOtLD@O8?I4ENZP6{`>1kAEFcuE?=%o8IEfhLWh_n9q?Uv@E*R&G))MJvN#-_zXKqip&X=K1{ni0nLoNm7kSMtd~3FPu<5>kiRhd6Yv_8+~#q#(Xb?Xf~~|E z*DO*tQ>$dT4%T{ol$Y1aSx!#^uGH%G3$~&s|JZl4AOUW*q1yM!$_P*AnQRtQif#`N za+f@_OzYP)fnJ?C?yhinFd0+N7&1P33W(v{yHRCe;oEOJ{dJ)i@PR$AYmA=%-(`F` zBlY$0Y=%7>#ZF}JgRbY3fA7j}8&c9gw8T}(LfJy(($wm%PkkErdSF1TzVil!!q$4# zd2>Evt+&2mjY^`>=aaACu1gXR#cNVJM~c?3qsJ1Ct96Q6)^nqE$x!@7FF9>R>&q8} zU<;jsgAHB+NMvR~YPt8X{B>GU;WMj&f0OO5MNNl^%xznp2=TtCY8;3^ZN%sZZaN5khikZ&Y2 zud^pyn{XNsFmFinH+$((f-m`ax5^niBe#!E@X5kRPx#UY=(prU{So?3O{=$$FI_tI z=RaIc)+H6!66zaSbSE%qrpNVh&LoEsjnu>-;wngs5{J$F2xw4tWnykqvuH>3 zir7Y4En34tRz0k$_US9t)6Cpb_^G$DdhW^D?T`FA7ekYy-P zw?Lt#3y5fUN60}4zw-tl(^-5a#==R1hRjANy$Bu@ao%d0edqo2JAZcJ67LIh4Y$d* z4K_)lF<_kfgDCZSxAkE(ZIz`oUUVKO$B3E}jW)*1GNf%2v+h&9JaaeZ=y9K^O_A@q zODw7a8CMR-n6>ZH1#uP&0696B(&iXEBz=nHRwWL`9lsOONQVR)@BA#m2u9B@%u){= z#vxEq<>N32Y1)@WrJy&aBNgxV$)Y`TQH6(R!^Z56bHj$M z6)f;&p=-r9MSQvN_D0~o(&5NvWi5tM14}=ffAOq;$mW`>B+X2wgip@L&E6|cz>i5y z(D@Om$!9$XNg0TZ$-*#l2`Z!1(oyE^S`Y3N6iq2w50z2Ivq2}4tNR~|)Mm-MRXn@9 zs}ny*XJL|$`QrDOErQsG&#!s ziCX=99tPpN25E7(W4b$Y{u_d}r5y&F^#!(;`%084>xpq7ZLkequ?kw9eKNze_la+U z^C!2MroFNKYFbT}%>eRc#K>d(+Q*pu=W$jse(r|2=1>FBLF16SO1WK{49*d$qy)qx&Y#??-&90*za{; z{`s)|B9xD$M18C;MA74vh=@O|#^^IC3_HeSk>PRHpNW$mY@Q}eLgp*C*psLbZoT^Q z_XWl{RZISG-%dz?U>T~Z=elw&ryf(%Sj3E`1xJ+z#XsLtK?52WcD$Z(GxgG3<|6pG zxADCqgBZI%tVam&?(MqIHB-c=-eCm8m_#&md03!IwQ8d3S+HsU%}S4Zdqx|^SlJu` z+qGA8CqFkzeXKFxmO!;KnHg2#*25g-0%rz0O2EJE2#REa41l7%j(r|FOo!Ztrzdco z+IiQnb_YMO_jiuU7f^_3<0XieD@m`B9g}T5<=plR%J3swq@+f@OS>Z-s2_Bl(HqPL zSk@t!qb_XgCQf9QuI!k^hgcHfh+qqNFakSl90CyRA!)3DCno&LPpHS=d_(dJ31lOmJqqnV z7Mnt;W4%fp`pFC9nNnZcx@CZ%5^8Zsb+)#V=JF2wkp*)nuuKeeQjY zcgA-D4HlA(0$QD!YL^f6mLOijXmEenGVpzT!~2rj5OWd%6Z3JQ>F#adZ*gt+I){$R zv%iZZ(RAlTHW@_2=0NymFNYXnT@Er{eIFPvR71`)43dO>suDo{6dRmB=n)Ew9fV-U zENnMvN?>=fks_|}LN+!nyCp_w(UZlxHt_^`zQZ+Gxe6EG6UIKAL~i%`?+~>Yh~!lL zae5mH3y16YaG5@-f1V+iWp~e#M2a^QB(^!c=f{-~do#pum}uWFHl5i)a>J?efNxo! z6VDNK>6OEK*5Cx?A7VLD+ARBcJ@y&0S?=-RtQG@5Zz;;b-2EDcCSDr%kutwbGkt@4 z%lKOqBUG6_HZC^q!&3ZESo1HL07n7QdqAaXc3Q$fUFtsyQ96#OAI~Rb_0PyRn3G11 zTb$!L$3%QZgbUCow+qnUS3`sjpRtW7P_g8A6iC8yr$k1d$3m{&|7Zc%qBfsS;%xJD z8Gq7??+z0Y7f84s&;jS4^ojOB>A%D*D>@(>S{dcNGnfCSmRd>(Jww znc1)E)rJ*flmEjVFd@UHRw4)x0-)ebTj{5gk7}IW+?B)|SD>1kYcfV4MeHH|u{*|B z^|jM2Ud6nXy#2gB-s={SqTW*3I1fdr#o7UiatOduqu1op*LKO+&v3xEjc$0gL`hhj z^aE&EZ_P03SVg&VU{#=wf&{)t$5A=QZ-q+HP3;2P$WtRpc?xin*cqPQRvKulbS@S$I}WpRw8SB zTLLsza2AV&rcdt5cy_$M;RZG2jM`90suc!Q;MZy$KoVPo|FXnkb?%L0bP8R%oc8*` zOHV@WmYH^0E6c5BM(E>f>rj%M=xrvSWc$0$wNv~Ew*mDV6v#T0b9NZy1z+w2kf7Bq zfH~pLoc8_^-{+<3v9$bNUejor!iU1s@o%|i4zJZ-9hoIO9sF#;^-k8l=ztpZXB^5& z-o#5Hb-AS6IC7t(_#Okb+u~atqfOZ;vQ@z0WGfGRn)ck$Kj98Q8^BA7-j=KtG#|ph z1SQZjxIpa*<~XB6fSR4dfheVxMey0rK!H@^0b?`*2J4ZjtJGkfCGSpP^_4 zhteC|Sk(;?B!}bPj4(gS3$#E>BnXLvK+8}1Wqw#>%uV5!0%7gSSOA{3gY{|FdMp)H zY-u`|(2XqP4a{}Nt#x`hyq+*~|9aD}WFc6xX~rb8da1a}oX9J6k6FDCW?(L4`L6{U zc?dTh!Vm*jdCUB?ArsB>HHm_>w-h-Gw>{KNO1&r{dY&_FwHx1zJPk`uDu#PA@hO>5 z8D1L+c9B$?MTRI!JtV-)wqc6h{q+C_`#Co&F{yV&N$1b4LU(JG5T-Z`FPXZkk^55N z!w`W@UtBb&(s4Px(a=wgD4pyfU6bFUU$V~YLjR<#9sUWElDj1eUvZ! zcKOnlVE-rhP}lhe9zbwcdMn7cx!=D2GSZ{*!>Q7e=ihQZQqFa(VP)sT&7n@`P4Zu$ zClse4HFAlcO;iZVs=duk{0!TY63cf69#uEMqdH8GUYZ%_kX-LE!1e2QIVo5FA4_K$ zS5?|}@k1XPq+38i`XES`q%?|3HwZ{~cS$QqH%N(ubhnfsAQA!s(%ndN-h0pUejDe* z{AOmKz3=N@*INHIqPIpP4n1B<=<%9-{z=J;EmNm@<%~js=)(Vmj2-%_`S?+i5dC zK?t;io6tODTJu10VW;=Tj>8skCFvX9i&~0*chvfQDA7hlle>naW@^sFJFGIu^<9jbnqi?QJ1{^ zH6f!N^m1khi>toVnI>YGpPcn|C5&Y3G z_$4jOEmQ8d;z>gP&3nl>ExCQf=dW<%{rM&Xl4;8M)Y^4^7^30Nle1~2$9VRh2y}JF z{uvqCXuVW;Ho_#hKj)j|fY6zVv!Ud|-YI%Zz)yYH`=XnV-&^#3xz1i}>&&W`(YX$$ zUZeGY@yCeM6oEM5f_iN#IJ@0o}47?d7mo?J%+xyzjkjU>SSIk7IsMluE*Zdo|KV;OGWe zo7^C*EJdYPfiRfl1NdnX3{n|Dwt!h-^gYBXykMDhcKz!d-iz)%P+tynlBC#}r{= zSUKg+<&PLF(h})3K6dSflBgXPqrh8Un>_#M%v%i#|AzL#TlX(K8-x&>fhWL&Y8pyk zfDu>@M!7iiP+DArXPq77Ii6=Q_Kp?Fkqz)rnhx}y>?y>FSTMp`>_I?Au!9-3C!)!e zN$QrF!g}u><3VlaF~Oojlugm_gX5B7-_7FT_wh=jT1phf;&&Y{+TBNi!n7@ut9$)I z?-k`by@VbRcN5~SO}zgoKaSZjiY$7&#$Jga{PxPHe*D^cZM5Y>t6l4FtC8A%mzbb5 zQaXTfJ_nn-bO`#{!czDFP0Y;d)Z!;I#qnO3#ZD=@j2gr0K!7VQqA!Hw76aE_;E$^& z6vJqA061*_AgP)7+U$^jMX=1@V}S+{fA(x&TgxpbzYZ|4*$Z9s*qIu^rx6&QYpwDd zZ~q!*LNXC5NonE%&nc_>!Z|VCUiH*8dN*3adhUSHP8MtFCi4;?-S4&1HP=Tuc+Y?8VkAwbCngvf@*6Xlo__|M+&oM8D9T z?nQ~^@)j9G(cbCK171d+zH?5oJ>UzT13uk75*P+4Vj)E(cmaIG{9l~LiS9c(w4Eh0 z5azio)n3zzqi2PFtzM|X!G){lbBtmXKBRWY`m2a&&Bi3ASFft5g0t0WHauUaXh@b1n zliAV#Mi)K9I`Y-KG!l!%oSlYTWAZe1zv{Xa&T+_1wftF+8)W_8hW7Xt$akgkyF(gb z4P>~($qLhB3bwYUWfBO{JZ^NtdHj*v&E|B3Lr@lvg~DOU^* z*GK$L+#etLh9M-ML-Dk~}2&s$Z~}rc8ih?(y@J z;{I;Us5@{L757VWT;ea9fk&jEb}&@sYo3s}FT|ar30Fi@19DH4*&`%Ig*7?$@MxnjxmIlrKqfR@`KtqoPB|n^yEWUM85C=W+`qApxFtM*MF750k{l(j|^7Z&w zvlkjlMCZMFWf#w(XM-m9o{kB$in{+7CJ#xKdXj>N`s-mhQ`}dx4;W=fi$DgbOl2+* z36D?Oe{@*OVg7h@kAuoQVFaV~(D<{z(%w5Hr8FYE9o%8=9?8aSnRns3F^9-hlj_*( zFn6ND$jio4N)0SjWr)8Q(1o>=#{c!JW3B5z_%->L>-%2V5?8DU1(6wQhcQk!8(~j; zal-b3SMC%t`wFyH2-U1_Sw5FE;z4+b>oN^w2clsA_v%>HP zGL-|;seh?i)0d%_AVHjcmh&EiCDYvtz~!#g3Ae?SJD}EUZyNerl+m9?p&3TqzPf(p zN8~d`t<>EOm4Lmy(*{hwH2PB#|Ed`~a z=yalT7ntUYRv$2ME&-l#UjpR!d3<)~yGnwdM`EzwXz7zbet$oMYeWfC6|s`;59@*3 zWeX7WaiXSz3D^e=Qhy-d=5-i!rw!=Qlut~4Y!W6I6BwiWS>Y@Xhvr9P2m^YHbDYoG zKJJMpk_>6YeZbTkF|(OY6dyBGmJ@7gPKm}9=lS#4$jO9ZgXGoaUoQk}Z`kyaLQ=S= z0mn<=t5=)8QePOuA`C6073-lexu6|gcKTa)3_tj`a%#kA-P77Mb)uxn{mUgk@f<6A zW3uCrf!^_PQBS3q^{Z^@pW)LtJi%oVtR`hdcW$Z@YsXBgO+Ip+2vz#fO6gTe}E4F+XGw>8!fZF`WQkgu>H3){iEth{%M-%)wsXXqYHXSa<}Q^iCEXf zRrwsuD8%=>1E{f=SxAtye?47H{+6Tb#bhwdlX*ks&SjRu+xhk5FA}68HSBzG*kDo` z58QQ3LwKxRuX1=;%gW>%s3|#%d zv)MBfbWe6b4gNaPfyOT+zThI4E^I4f@uuj%1^9p3Cv_m6>5J?o)u?C5zTp?rHst&o zm{I(TqkUCfOyEA!@V;r4s57t@z$Ap!h^hFk-nT%g+1()FaC|%^k=HL;lr`obW(+=m zF`xO%uv3NPaeCEw0HV+cB2eE%!bJCK6BjKIo`=RAA;pcGq#1nM^y=c@EhZyAw=#u! zk8*?xR`ej3we~9XOI+m$Wb|FjiQ0Q?%t^m&7H=ZgLUZm&K$dwe4?(Z){b%9{vdpW{ z;-;Wh|2s+l6EoIt?a8vTfbYzbdrBn7w@@m05DCQP zd8a77*VVND@}Og#)#|;LWSp((ba!$X22}D_2dFFbDWWRgc4tc zO?}KWbsYPLv}^|}<^0FH-gHq3{)5i)88~VCIk)>6l?V8;5Xl5mg*PxG->hCM_&?t< zA$|Tp&~Zts0TAS`2OqN$;f&?Zra=2XuADInX z`(=KTP!zGHsF$`z;!e24PRqt$39_sTho_z^G}!;S*=K80ro48{(HJF8FvPStHkS@@ z71}?^@lUil%loK6%_8p_ZyCuroiXV^;+%6c zkYm$Y^+1w~`#yfQ^FZ|bgN0)P3n#jR*KO)`OM%Cyv|gVVmc;LJ8kN*WrCFy#*Ypd0 zhKlEF_9%I>LzTry=|h`YR4?>JkF!|QWjNikSg&G|x8Y-2d1VVgsk&=f07KM~z%8Yl zQoyrC6_G2&Glx}MH$%Eb1O@=$3ACwq#e2M45S-um(+pI=>w}MkRF6N|bm^tW8wke3 z4xHU@zythk4?>=If3lOrcKpyObdq#%csDyI>YZxOAS)Y$f?7xp`!d9vZ5kw6&r`KR zWu;XDm>7RmFMdXtxb*&;LE$#NHgb@^>7YAmc0BXOLfCUc1MpX3?Tgp!0y%wIK-zYN zp^i(%qRbT&mys}c*sX4ivMW%?xzc(?Bmk4txp&BCPdu#MsP8hAld35Rp(vq>chK|- zAqEVJVN$G7;)@hLLbs4mSNUqIR6Y zq#eX*!0f8R<9vjgz|mUf1;If|)QW#9p>;R1nWZ;`FIxgfJ)lq`deFU(vsF}xEgZuX zXa=re#oc%k@8gc}Rw!BQ4mn{(>lA{eB*|Pl^VERxTN|?8mRT^E@>(FWw<3jG7t3mpNi4+)90)RaPtinXd7|k+Xc( zP(^|a9iS76P#X<*atHKRW9pQ-+3ebvT-U~TrJ0M~~j(_h=^mve1xd`~2^TZ0Yf6s)LhaZ9;oEb|^+Kqzx_ZNin z1gpBgL-XAo>0sVWH4SYT z#Y4)6OcKVUG#z>QA;))MZTVkXGRyaP(6E%E?GbeZt@AsrkKDj>;()%cL(0WwU}EUa z9gXOvYOTo)d8|>TBYfAgc>9AU0Ze8aa)|H2=W35a-0J`g@p9!_drQK!7cK}yTuJMo zJnS_(3`!~s;y`yErMv_5n1rS0d-92bOBISw_KDRQjzHsA3lO{J+JJq`9*CDm{a(kV zn0P_!$FZ=pfmON#*6oBlQ69AerL(^Y{jFmZ#b_dOeCNO9;VZTM$o`YJYXL`jM7q5x zo3ANTC_s3SnEoXJ<87?_cRu3LP}Exyk`qe%CWU9T*^0h<3kTqyd4RkmV0TKNC%mpY z)NTEIBX8xfN_C|8EP#C%Zc!Vqrq4F3^o@;c1tDDVWW2EPBH&vJ&a2AMF)Fp)9o=a3 zO=ThBKx8Y8YZNRROZ$C zpmKTHtNu73ljSyo@QCT^)X17Nr$M7OS}m-+DAE004AXQgN1k$OeYyZghZyQ9vu2UX zQU9&WC&5IY-mNUT4khtpBfnI0HIvKvJvCYs@Z*YE_|_6uFWtf2+SFe>4>=7RpgHQi zuC72RYVZc@954(sQqYRK#Df%ndl>mNBfixY4c``Xp2fA?oGfCe8GGp z#GU+ShSK*hZhTPEhTb>UweZ@gb$*$l-cEkt{Oo|k%>DtY$mJ6`K2>UwhYs7xky&Vj zVq***(5Bi;cyh=uM*Y#zr>9%f(l9)~@`OS6FBwD}Rp2a~Pqa+H9F--=!Ds~6s#|-K z_N^w+Hho1`pKfcda2Kt{=hpAx_?xWQKv(BHBMCuZ!TRx=(rE3nAuXR(>;w&R5l3Xj zBHYylf5B6=wdP%f|3{BB7RBy!?>4bgG3g`)BWdB?AC`g=@+$5dF0sXg{d{R10Ni?I zaH;a|B$FKFo&AO8LNkF3@JolHxdw63v)Wo0EXUWDJ2Dngrs#wcdvG=rjhK|!92E@) ztVYVA43OW(lj9)9huF|Wtd~Fp@MOAmg34%1T(*5k&jkEl;uk6Uy6JsCu#%^D(yT3Q zk;r+ZDtMMZ)+G*i?ZicrQKXRlalpu``-ts#{5eNATUiv}lL>3!(Sgz|bG0dV4*b4o zrbh6>Q0M|6WZo<@ZO1l$Dz^!aX_vjlo-f&3O|0>KA(v4oj>sF57ITy*EI^Vj%;1b+ z{Re0VONM0MfYrp7e+4+A!yp0k7g9(%5ZOn19Qqt{_Z};XMqr8|Kd7_Z#S5=fMu>&S z@z2<8@=->tZrzgnr0NLg5x%O0t~NQ2xs9k1&y#Lu%Etn$CCbK~S%uY8td#*sfr2pL{S^z1 z%b{PhCr}Wp3&8H`&qTDQh-p&+{i1?hz}U!Xu5hJenY9*s-l zEO*VPp_bJuZG8QcGMQ@@E<@PfT&zfZrBU%AQTKWnp?-xWc3aD`lQwKwohso2C^zD`LFZ#QY&a0qWj>_XeAZJkdUX81q8Z46O&}t# z*u9$hvK*%vV5>jV{GjEC-|L#ti19~EmT*wKoDkj89o11hzL)#z-UX7N#L;Y=X!8N| zWBN2JV2B??Aa$R*R?ciHU?f)CkNfyqwtWm2rkw<#jane&83Q2$!OUD8olDDk{knpt^DOa_a8Sx$47UxPqi`r{yEm{~|2 ziL=T;SH&??6w{dYfcoh(?IxO4{dV7w14aGL|*#6`4)L)7RH`}%)_(; z#C4}?j>I1x0^|JT^Nu*XX(rU4eB?y+c`+pY!4zU@XL-kDS_IdK%EPobl``mseV|@N=)x8s^K>33U$|yV?&(JoRkPNB6h} z5Ya~37u~Cp(Mw(mZ`kIxQ))Q*3?s(COF(^iV@cc9Dj-WgE04M7_i9v8lYBJALza?)`tVVP54e^qr&oMB;gI9-ed)4Xr`xQ% zpTbVqtTg_bT{630ll3(1G>fx7AOC!7qTh(5UzV%o;`eWeM~KWhJ;fC(^Lp}B)ZjCxX;&>tONFa!vw&uIB5hexw*W)L?*63EprY4srisNu}-i1KTyTwNKk3sNqKo$_Cz$$ z8hl{@MGy<}mkPVtWm2PNg~F2SfHok_A7~&-Jxr!6YMyk{N8_*?-)JFS{fAZwvn3zw zBY4ljmg!pY^~amBJWr_7V&78oI!&-h0>}dX7$SY-KbN0ZfY*~&j1PP0voDKmJm6JH zKEJ0WD~g@S?CL5@#m!LS1d0~h54>k=C-_3g51?L>l_D?IbWX>Q9zVbeZH_Dc z@Ym3F{UP2K>E0N*Vak=d{skv;&9i=9ys(rM;ONr{{a6pI_ckq`k|kb(Tjcq{Kj7n< za|-2&E~$MV)&6m#`M><@GcW}&qP zTuOB!wyhPN3GJ_L+0Fd6rHV_zh9^*9#u&}xry^?;6zAoke6XM|0>3}XX$$4c0WL#U zPtBAyD8@Xo)#^Z~59UixltEfw`oiqt!q!Y}m7U4T+v&TkQA-mub1_5cLxQB36r>r@ zMjz9`{Fbj(aRiOY^x*el91Jb-!ayfdtDIS5fFCjMzn89U9;Do28#RveKXTv@o%xpg zp*vIEiaB!>F=jHT>6-yI2VH+tSRse&Bkf0w9d~Dyx9Gbdr7Ko>82uNv7}pvhCg6-e z`i0xlO(Jmw>Q7o^B<`>L(7Kma$qcJd(bxm7>$~ej;3AHcT!?poYf?~_YvV~#yH}WQ zS|+3mt(E6(y;?&s6E0;GJU*sxLzKIJJjN=6wU?}M zOv?0vTS6r<#pZ11NW*usJNl=q;l&Fsf%jLl6Lefti3iDKMt& z+||O!u@T|Mw8N>4UlNHvytU&|(c4P*9hxi~g6|lT&^A*LawJV|`{Y0Q6w-*0XeF4i za8tcp+ZN-X{zAJ!u?AMr9RB@CXiS|eVmYuPRJq|5Ry7z;#-22)*A1VoyX#Eih4tU^ zy)jl%H+nR`<&zpZ51jdbpjf*$=QHtCWkMA5H2jCqGV8oG=lz+Q%Ro_I;c^a=Q}z-< zm)pJY77U*W%(2xC-J248XXv?t{=hcI__-^^(YrlQ{nZWm={Hqu-eEKW;Zj&kz_}_? z^)uE&CRc`Ni4T^@EXhng;2w|Z97f-Cd^-o)kA}bUWgE!hEtu$pT(nLLyXK#QwbAW< z5ElJmq6ekUa-0z9ha6B#zTa`T(=iV!Onf8g3@>R@*9S8-)-bGlFo+|US{W?hP zeAfM=_C=I+fyInVG*ci-lqL*;ipV>IIL_ah6URAwrLuLhQ!7T ziAFMxlqgbD_ z6fw?nc~Kve4iQfe#9fVOEQpYl_cdY4kmp!76uCiJHh4y2uiK+-_FTXJK`?T#ePP=Es1s^LONf}qOL3eq?^vjON_Aw_v@-%fLhJsxO3@`&@)+3BM>OUr-VMK#adDuP1uMSD0O4*bL z)m0;1lHs&R%Z&8ltJ{W@DXULh$M#)gBysb!Q^DeP#|zD`(y+}pK%s>{#R0uUnF}@W zg}FjD^0aQA=gH-FKbJG+-1;x9Izm0cFtIpnd3XHB**)s%{DYt%Tho5IH_{-7aLFJ= zUX<`86ew-)F}_{WjK|+`#4exEt|z@VNweIIlBcQeEH!|l6)lieH12wulEg2;gYK5W ziQ4S(LhstE?0fdTEb7_^{KtQm%4z2Z}_3H#(!?f6ng|vRCxEECxdC~Qn3@9CWW?z}= zzd5&tThHB!EN8t^phd>-dH!vvn$6-ZfWmEfK@iE6D9|JY^3n;ROGFEKvmg7&wVys~ zyU)pIbwIOMyJ98gPB`dE%rLx>X7KR==?FO8CK-g!%ZzS^4jysJ@s%Zb0ME?m2DEMM zaEaRZOH+Uvr#PkNvJ^qQ2F8!ms11-V-G+eaWWUvVn3Vl_RqViG^Ui0AHthoPfgj{b z(Wf2uTMaP5B(MW_O&!fmq(rGOt9xr+=yc=hXypXuVd|G+RV9B&uJ0tkYwvkSyaE)z z3d!Kitbz5P3Q~TL^R~HeUqtIk3*&wN#u^sdM_gOE(B^af=LQHa71!&69a{qPjxzm6 zck^c5M%Ib!bH|DdDBCI|H>YLWdMkUct$!Am)F|iDjTasMLfP8|T=v6au{K~EyK~I| zu#@rlv&&tI#5awfu7f=N2NcCBYHZl4_Pa=V+PKp6xl-Q35tsp>C~f|~9;4gQRNkI= zx>8*8Ecbz4id)A1aZO2+MQyHt79(nuDhs?H=z!l;5&^;=t|p_N*@>YuN2AMcB?_f8 zn_P*d+Q*ZANLqN*s7^)L0;C z;~ExS_S-fx#cf8iTsDT&jKj5P=c$fBgMY^LAx5Bz}(_BpEJ>F!on9B>Qr?5YV(^XCzUCTDKsJamdKVHTa z5nlnuz`jXrw&y&jPdT-4oa*}2ME6!_E;c?g2E%M;jlJHq_dMVgu*#f$ecJKBO7ayW z%>6lS8b=!6$b)DYrAx=9fybYlX$$dow!4HUwt)igC5@@S9!Z z?>AjVlBnZjDpqnb_jSKUR6v*M73vm9#;SFHQo=U&1N6}d|Gr@mwAbHOtLVodvE!e= zDIQ&kkiw}I7h|2Z;*}Al7j^$~SS9^8$n>w?V2<7Nf&c+&4IF+8C8<0n9 zqbWG!v@fg3plgM{R3|#@%R?wDnoRuO1}0XpM%IzKvy7X*#T%c916(F#(qSz!-+4E> zze&XA>R-q|Aw5RXGzLIECcpV|y#@h!9e%3FGZC*sXaplHgoI1pCW4pbP3a zZdlK9j`O_s~)IKCJ=_d{;XD=73f9>i3e+#GJ4s!AUz;A@@-2?&ga9|po zIRJPv$mP+_KK2VxYqjKp_N(L(!Yheky;O>rNVstzk5JE&H;{2;iJ(w%q9`qujOzqP zg_w?*imOG#Siqsv}`; z@#9At{jYAUTw)3+?KqD6{i(wPhxgvMJ z>@`!tK69+YysH+#Isv~Z+tJPN34Xdl5YH>{SyI;O6n5y zeQ@eQdvH|v^NQD+rjToUUECiSxf4YHhLVI%_ck}C=FxS4nW?)aIAT(11!E1a+SE!+ zpNC}?n0eZRJWK85xa(Xo&p?`(eYJ#4Dx|1I>TH^m)?rYSDp3%yPadBK+y>k091#2y zn4tJsZEP7rfg0K({hjL&CBi{>66|MKnY_e|I|CEY;j9Hh9|npTScQ!bOP+T>tWVBwC+VeuV;hO|Gm^=l|n|ZxKI`0zlu-|f`pmK(wCJmfERHGu-I%C+uOcyxtcX>GQqr0k>*w@O=dUzRes+`hX=g^{_ zDo)*gIUq`wYr)@<2*ppken}ZeyGfa;Z_l~@mT2rQhHJx8^*57bWl>h!~LFCgM8_)klZa|s< zEzhOYc?Jd65VrG!Q^Cs_`Hq5|QnuF})R+{sWBk37Uc$mc$8|jClk^3O3;zAD2;Tss z0CZLr5=CqhFgeWc${hxjOd-1gc+il&g0Dv zu5IfN#SaYtrQIAYwmy;t6CLH{NLjW@L}U2OlVeMN(!yRaDgUs) zIQ*Ocd3u|J;Nt`tmK*)7DV`ZuQXR2eeiQdu=WN_g4H3+^t?Me~%Bd%aneRg#HbozzNjDjmMyNCEgkAY;$l5ss_rE3f*!D}$ zz1bV-ljL3@BKP%*c%28rTd${RKn-V`K7Q^k)7$`mgKMo_C}K<_qFyf4%nIpccXArQ z$zM>fif%L>c-Aj@_k7l(l-kT*ZiUHLfsB)#to_5mrh3w8DFQFJ(`s;qNM>_!z*i8# zbOJ4 zumno2bAG)toU_h+m9HJlo9z7#M{h|IY^~41r|$dhU`z-g8suXS^u8oUcr$<&v+-e6 z&9+|`beaiiaV&~A+JcTeJm>omf~A@<$r@4aXhEFYR>~#7YMt@2!S)mReJAwPer-Nx z7y@w5bYuel{kDvR!?fF`4YN!ZrC~7DH~L!|N+Yg&rE5FNQ{(WSMVC?W{Mj2LfVp%on+M8p7>Hgc4p zix&;4QH)YFIS>#7V!wdm_9a7rUXoL5qqyddzKiJ^3qO39i`9iGDbP7C4;t|z-Z|Qq zR!?N7*^K80R`EI2M&)r1Ei&iKNQXPVg?9k1S2MumNuvi(O7?J*2`tQIv>Ggh3E_l% zG=Y7wf=qx6=qj-Ll5TbnpP)NOO5KNbZXp>L-ds_`*wZHP<}pOPj#uk7Q!Xg@nh!0< z<0J3Eui9tM6|ZGVoGGY^&5Wr(K1R5}u@X1HfOZ{81hcStxcud%&a%J4?++Kyu!)Wq z3ft{`Z%V9b_#cn&ECMPvk- zI%tnKo@3c6xoqd0MW$U4X=HY#*9p3HU(tj}b%HwiwO>fEkY&@tV?#2w(`u`P5#?qT z5U&6Rj5`!Jf`3~ocJaz{sNSWpzuTb<6!;QCAo5J0GY{=M)#e3|tY0C@Wl!ETUA*;U z$kGH$)i5rtj=v43Ixmq0A42>y(N1D{R>&44_Vxl)ZvzP4&PQ3nS1E(^W447Wj5wMZ z-}|7G8=?ievVPo{THs#Wi_fpPQ=h_%_ES4?I!0N?#6>zf8GUI@3T69emvCnr=lnKT zJx9WsL9fZTd*&&40nwykzGXC@|FZ2oIGO+K6oO_KTt?Q3!3Yy5ioHzmjUBfawy1*k zLimF78mUKCTY)BEcJb|GS`98-Ty|iQK=0I2zt5}Ixv7!q6|vn1rpMn90RORnC%pRZ zx#(SHCsY~x2mGf_P9=YjU92lk{=*Oqxc3f0IQ=A%ab+HtH&o*kPH_zCK{2UdsW*8>H)T zdj42vcY2S?8GNih&rahUdES9Es9?w+si_a?3%ZlpO!E(VBRu7Eb{etmk{ZvGYox6g zBAkJ=eJ}m}Fh_Lqe8!AW@TZL>Ey6f3bfWz|p=-L9(p4AmU+i#q+&Dukje%0Po8z~X zSd=7IIU)pzfBO&-1nNu#WjXpfR+JriRO6p=BRGcqf91kNj1p-te>D|p9EYxnbNhX~ z`wjSgQJxWD%gP@Zifyb@#oh@Z^@H}E`A%=BOPsFkf)Tq+X; zRdFRKHTwVDsPbb;A&G9HM%Pvbn-Yx*bQxS2YHsuI=c;1V$rmrBp5MCKM%5GhIqc`d ztt7j&a-5(pqz1r(i3Z4pj;eq{BI7@pv4Dn7*xYfu=kVWr^6!TTp+0fz8iDt^3c8de zfI#PAyUnwcHMEt{qI^rQd(1Mg{jCzTX506^Ben;%*KUw9i=C}SVE2b{3m4w6?>a6Q zw}TkNNz|H6a39j{yh;(k5uogYhiXC`$YcPwtWM?gN9;RZ3`o+~h%F zx-jk4+wM74^5(QYG)y{`b_IGb*$0OlT%5hqjGFHnACYpiNu~_&;mkNP-n*3whlE#v)uI>t>Mo<)5cFY1VkCnNk}+rtfaAaW(xx zIM;5Lz1E<+@srwx5kB!^>b*~*WQXP|OQH8D`3w>E+l_&e)YL3$nUC1{gz>w#h_-uG zEZ~k=sKdOo0-h-}H5}1TB=b5scF%7t_a{j5YZrC#SJX$d#n)CPZ9bX01e71mdHj|M z{8tR2#58k2^SjpFjJhUqF1Nsfr&)iUjIF=(lW2SIUkSVdYNR=b%n)EOQB_X_P7(}$Uy zW@2M}e1nlb_ExMuicRv_C$$f$!@<3mpDJR;>dAxa0;)}Q-0m^pB4vgrnnVK12?Q2HZy1d${sgBIsEWrx?Ep@h8 z$qT}^U>M8>?=FKa4ras0k87~`jzXpW&?FG@HKHTGIaC2nPFYlY7TBSs_Pew7s&6k(NOW?Lpi^;LBk_ zz)tkQ#t@m)zwTzVAMM*E@h^DnFmeQZfx*aR4UJ3_lQv-x>PW5=7j}4I)G?+v*|OuE z;l)lGoT+*MDlW8SVO1CXOXN(Hhtr6ARb7&+*lu^Ayt@c-@BZr0TEsbGQfTWP5sY=g zMD0-`JjVD9%QwoOH-A6dk=mxaH=W)OHTmSEOTh?JW9a%+dV1}AG<^?yfIqu zm7>#pS^&6>^k9>il1Wg+cP%_ZT`IJ@_hcb#-3niCDD3y|IvXuP;GOpX2@{-?_LrVM zmT+0(vGy|i3klnlRvuCoJcrR(nLszFF6a*!qPTbPLf@qxe17ta*X7}nNgBh*%ecUQ zkyQbQ!_BBDfq*fp)4_(yy2CHP3r*o^9U5);xA}JOpHS^scaOJ^yY|(jT@V|Gd=QAX z$Qll3*Pci&4FO$ifQbtT@o{9>d;K{*H+=(?s(y}1zX1$o9;8klq_1~hHc^U*NRat3 z6nDf0r580j73DecYUL4!3iAM6$&U#RN!j&w={tg9`UrE|CP2fKy?9m!C&kIAHR5e0 zomSe`JAvn*aJtW0vu;gn=a`&}#!qmy`<{DUmzN?(9aZ(lAzwQ_3JJ_Aq?;c6_5JT~ z&Q?s=S4;P%hC}xw&Xg2zbr>Do2LVo8l21R6!_npPHpVJ{3a#K^{q1A9gGy&aTsQB> zggamU6)!G(ZvC$di=h@9r4zACm*I!tN-(}h9Katij&w57e3Y#;5gfQy%bh%-Fz=;y z4Ia3(b%v1}JWvo1UV#ofCzQO=5Kv=WGN~Asur4Fox_Bisqm`2A8-ZyfAV|s3DN4&= zOg#c5U2%ZNbWTWc!Z<4m<*j*yTC1f-53TF$PU7QOx4EyvZaKnWG&Ga~{BQTJOQhuk z?R)SAUB5&UVIs^xu~pzd6Sp2ASO4X#*~2z*K|&OPJ#Dk9xHs8me6Ix6%SOm{oTiw2 z2V1R+K+)G3_EaF$yP%I>SS-j9&C8HC*4Z1*!rt&793CvJ>&m&TBH#l-G=EcB%T-w> zxUD*Di>i;}%YtiSu9xEPUmE)CtwH55Ap6S^Ts^qjYswW0JCkBa1+isY4M|W_aBQ@( zCcRj~;HKOtPx%u^^)%q3V!?8jZO3T+i$nXR{Llartm?b6K<7+FB1Q9O^I;5{KC(W> zP)wpJhh(-;3je(FPd}a@$Etx#yy0+~y-T`7`K##k#;v|XvDdIIO5$=3)QqN&o=+ObrV398Jn^Fx@& z2PWgLzBf${iNih%%SBw(rNc=uI#GAhax$xmTzjjWS}n>f_RQz_PC0^{J6t^4eDZ<3 zx3J-(9_Rd9aMJ<@;CUt@aSYRV8kI9jXom685g2!(bhYUsY{hg)i)vJ+ z7`x{$Bz|3kHehqFP?J76nBWCT!>w|-PYEPvKZk$yH^M9IyXGUuJ4=Tx0LDRc7%Y`g zr$M9R%k$xn3io$OOgfA+835^b2l#+(za+=AF)&)0!3_<8NyIt*67!4)cTpc5}t(`9sukJq&vxp=Mf=y9CKqJwmK(u*a>@XTHdg~`#T`7Z} z>XN4pbvBX?&5FCVzy>t0{RL27Vkb4KE3>n>q6?0|IZ0tsi))pwbensG3fn*ROYV7Dq7Z)gaXZP= zaZ`;@Rzi31`R4JB9JyU-Kc|52zhus)3ls4wwflp|m4a~`vjmeRU}rXY)b%L1saNz} z?}Jbnj!BpiBhuNTyzq~bnil^tN6njOs@~?3gavvmG>8#KipC!?Xg^+_BLL4@mUJPTR6*u zZ7ve_s#g#i|xo>L=l57M@jzbmm zs()6@Y%07X%BzE>r09}sOMDg4qCeFVeRTrb@WHEFtKzYNFD}F{pLUW2$!@=^Zv{r* z<}8l|9W^<^vu-&nvn3jEP6yUkPMgKhi|bYd{F>M=bj~~bRnjiwo&KiO_uCQR5>h@D zeTAYsic+^q+!qe?)vr6VZg0DLCTIUs*(V%UPDp5&%es>QJFNp9_0sHJ$QV4Z_!me*kXyq+v?erU~-J<&}5GFcJx(Hn(?jO&Z$K5epvI_4oS?K+*>M zD{l&r)@Ze&*wwmgFG2UE?&Vw$HJ;Q^2j5pxdGu_% zvf@f`)*>;zWt(x?*!*=mL-*IG(`8ybzpFGEVO4+W>Zvn7b2StZ`xpD?(X>~tr8VXk zy*J{=Lz=KS9)qD9ZoXf%(Y~n^xhCzDZgd9MFj`X6vp_ol;Tuvrn0G>uHxnM-bCra> z`m1@%1Kh*DU_L*T?|d*Bu` zCU$i~{zs)QS0tYWxUk;9H8~n-RA$u`tES5-}vw2 zIO)iykYlEdkiGXVTe8V2drPwSkzI*!WF}jLjI8YJy+^XijBMw3f43sKTp?{ zt9;JqzTfZHe6-M5K#x<}L9?m(htGw#+AZXX@NrbT^^rbdO7}%oyzRQJnJf$@TO#qh zcRHzvy|n4dr?%-Lk~Q7+@7llzA$)pAg{7}qo%plvPP)88)k27y9 zjYbOWICCK8i5t5wJYs-Y$2D;Rw}Eg}8R3iX<2tozT4qE0tZ`y@;*5gZ^5DEY@xY zX>qu_U?!AEW@K1q9?Y}+LY~Gt!ucWV&`Z>l(Iibg$MRosI9_Kr17a%DS{999rR+bN zfX&~XOGdEwbJF!JHM}1o2Oig8_aE}UH@oc~Wsze0$CD0OcDewP*hJrU{tPVj;I%^D zjfGjI8=ZMGJ4G&q?*6}NDH2PdkPvo&shW~4K*vNbiR%PT?NhSAU3AVT~`io{rhUv`i}=lE%TqrBVo6!SGsSm;=;=T z$=g!@tKauYF-Isf;fFSl~fY^(gfH}2XfeWkM@Y5hVXc)kg_vBEAsIc<< zoMKcrcG4>T=NSL}7oShGUfxRY5+i<;_{9VjBD+xQVFR_LitdDL14+kY!}3=|!5NG) z=TD5CAKafv3M8-fz8eDjOQCNi2@;q1!0D`IpZ3kCsDb9D_^Lv>CzotY%-rSKQ9|7> z(Jk~>=H4^#gnMoYI$CDq$m~9_ku0KSB9y^!3V(h^SMjOm`h2DOG=qUW700SAD4Kir z8tN^Z0fx>MAHJ4UI|+DkiJ9-V;Ly(DK;YF#U%I5RP+nDO)XM9#F{xYVAKb(`ob*Tf zAN-Pf9Jo%Iu6T$}%up`%<>DYRO#$;%@Pgn#`P@cL?$^ELtg(01K21aJWSXx{*Uj$7 z+KOE9FI;SB26jh}-+t9Z;_Dk-{r579$13T)J`D{X=s zgmm$B5#IYpwbJ!ulbK>qWQBG{W}r7SLC ziWD+HElHbKD6bA%u1c*bWJiUe+#D`8?P-}T@&b-(ER1M0nWX)x>Xui)rS}r7c%?kl zTI$_%hu-EIXpzbme$S3bT8)KFqM&~o-xVLq-_9BRz?WpqG+;P@`-Uw%-0Hq3dBJXhMr;`=kgX3H0*ZcxiBq&rxT zuSZsXvn&B&FV%O<)tYJna30>6|0!OpDcRMelLU3vu5GOzU2|wSGW7D6vX}*JYTVhz zPb?s(t2X-rf5RwV*_eER7!8L?b&s5tk(m6Z-$hs2`mF`h(}DlyeO;=-WNjVLXlSrH z(BPX;O;LZ@KZ@m6Xr~1)Y!?xn1@;D0_$>rDa)a)a?a{zqGF-)1hc=k@&%9E|@K-3Y zWt?XU10@vfqN2s#Vbrh55oa2vbCvk5;R7_KDLczQ6IPzUS?Q-3OIQNm#}he?-t%xd z0&0fIZ+40)g^@)@gs;VE0%sLo8K7;A!iTw^;2pGmrpNS6K2I`K()gek?U_~6_r(Wc zTY)TPtkwL!vfK%Ja;L+5^SMimXYWowIjZfyqUs*K5pHGP?G~{O8M^^dbJsJNv=53# zoq~1UxevA!4`BBzK}y>iKO@fOLaVKCCQ1I}3pQhKtqCd&M8N)wH2=whd4I!smoQj? z47tWgeq)Wvf1D@My6w&v>R{?fF`*(t%gi)@(}lv9O5FP8T=2mg+WV%+fb&gG`r180rID8!QV99i3r02jw;TdRk*xh?Lww}bXG5&27DbC-s99ui zEsy@$J`?R%yl#wO7?PUv#h-)y&D~_9q5TMtV+APNM8^1_D!2burA>+3L}Ki z(dIolSiUy_{^s4DhqI45z8q!S<)hA&FAq)EU8gDW-`r&U(mJl>rbN9Zr2tP&yPr|jVf6FV?|Um zIOP8Jj*+a?1NYst;z6O;T4Byt5!?J2t}Gxv5sjg!Z$%S%fM{^tC4LKb8aNr^#g4Y{ zpYyX`aes!>*()I&905W%NlLgw@#&=NaGkx}OTLeHx6{kTKM7}!iM)+`W0OSa^t!06 z1C(AuX>z5tt29tqXQ!R@eDE~~mT~+_aT5oxpVcs@&>E)sO4YDU;Jl=Q`tfvkkjk!O zY96GS&Xa$d{`QN{0mV57YQ%Pppfk==G{>3O;INOQlJ`5Z0tLP-9@pdMK#MWcC@x(pZm3v- za)aU}cJ1?(^mRdJMJUN5gAvS9(!p&K5Qs&BL`=Y3&jR06OZWKHik39Ak*jt!^pd_! zq)CR65|dIg;gCt`)9Alj6nP$fV+7s1*0 z&62;5nBlMyKoCtPqn93ZbT9IgFoNW-WXuM4Zem`MRZnIk z2ODox@d{l(2aVp4N7xJ0N%c(2T{FsYo5U`YWlCoXE)^D)x&y&wxi7G1DY`11YZlPj zUZVXA-KpLm1+d(Tg-7BdFTzG5#``;@#2TfGqQ z9Xa{o>@(SJ_R3xLBP-$`PloC5W@2S2=0L^p9V3_kz5fU8V^y=#t@@Aa|1rHv4pUK3 z)#g1i9-N?edy}fr0ev6-Ll9P+D!eabtju-e2`zyi0tVcj>c>DGQ%PpILU~0>X>f10 zJDmH?3;Fsks^d`*<2LPXkAtUF<(->`#Cfqh-@rbBBLHV>N9bof(#Hb6`9NMgsB5`Y zwXDnwOR;+Q-99#eAtbd{s|YQVvtZc3?GAqVq3(2 zzh-u%)pPB2x{a{SqxQT_GP+*TsH4AtnW05!VI)9%F&X+f*N;a|{QGxEzxn$s{mQl( zNOuM*U5aUr`I{&d>bWf25tvimbCly>l1S_YSy{!#2E+`1SnO!}Jvf=R$|2Zc&@M&m z;%zNi4m6=XtG~d;{=hGkN3HRVeSlq7b*;}X5RiN*s z^qRD|UJa;Fg1IUC=c;VdBmVUte8fkr{w-~!hmlv<{+Bmss|aniw;b9JpyCT6S@SH) zAnaM2yBiTKGn+3>{WEk`mOnCx)c&3QuB?wxUW#d0B!ze>hT<)c$ljNZJtBJ&i{|nl zM1Sw!tt^WDl3FR~IBXZTPvt{=DWI8E)Fz?82ft@R5HxM@Bq<~#Wt9H7^?$ZM1G2oGl)*AeZtJ+1r1K)xb(sH}0Z?EttBXMy|p9;@8~m~{Xk zQFp2)(j8s2qG{zl^4{jGYPf@Gb&$mN1<2}dodwV%*Yuu)awKiQRU6NuFSR@)Ds5Lw?7KangVo-F7?$&jeNlBqj-cp-)-W~ z!`{v?>eC8v6iL!E%AKFugpnqoimd=g)4)L9@{i>0Z5Ud0@Vd z#qe68F#>k)kWQ{q;rn@%Tr9C1+Kca#jRE9CE-F~a#Fm%xiY$}JjbRrYYWG>wA)eHZ zd;ha#`EB@5u)ws3S;?RD`Vu9m5p#~KkY*A?cCgwax)MPoafCyp%I`9s6*A`#NKnZ-E+sJ%ms!;EwOZrR*AEnB zd8SNZQQEWnYc$o6y$1?`+y(mHXi{2r#MpitXxSp-(Sgc@8L__!=!$Y~3BxB2Ue%+|$=7-1kyuI49_147 z7qif{fdw3)Nkhd>zzV^u#Oj3l%lT2PO+#IYAGECyg6GPHjyZ+sfK@VNDUM8~mxl{~ zAQ3FF`!fzw zel5FFPsO zvk?3vu$k6Ev2jkD*aTUgg>t^qmP2EZ&J{N04GOpGXn79~OiypKNp1^QTD)!Xe%Kne zyIs4FEK8!6cvWVi-%1Jw-dkSd8H2Sz-V-l=b+V-BYhYBR@u@bTrQX4;4w`tT zY%;h)ek3|8M0l|5uVam6LpaK=~7LcxDe#z+VP@aG`vrfzG%-36SW zo_BZ-t^;dwWpAG#jPryE2BvFKQP0AdAeGE8Lm2OqKH`v`120 zYMw}~JSdwP>f5!omWkcX%Lu6J!j~%XEy~+0D`6G5ROH&%y8cU(!O|~9I^~cy`{y9hA=#Q zm5#1x<;y$R|7HQ8yz_+Sa@7KNrfx3VGnKxS-Acg6xoe!~_^#r16+l|M1i62H)-(Hq zglS@g`b>d_&>2MhXaCEng0UuV^b8%T7md6*${I2<;}4Llw(=$}R~Dhiu@Jf<5%~q1 z0GeV&(42Pvk(9f9phaIGI48vs;$-<2^(D_j_1C)BFCNWVjY{`DeB3cub{ap8g?yKu zPFJBs)6BJT+W^FUBX$C?#N4K^jjCb(rTdj&EDkV@%;E}~0L_Bz69~Nnb-b3$tH9!F z?7=?=@ym>0nyNbx)Yk_+$iBg;2=F$(xrU&$$1!)SNv<}2bskuuRqFm+W{v?d01>cReXXQdY}l`pFhc+5*`en+|MPlI`b^?qFi8zNKfPP_7! z!IsHobZuA!^7-F)=YP#fK_x1Xc%@KrIHX<_Nbe*mCtT~5qVBQ94XPy@=RMLi{__6q z8x}$%(J!Molz$3vH>1UZyyIm0fUi)2I_OLe1xs=*v7SWK0Hx(DB!$~g8@7M`P(I*= zl-&307%p350P=kz!|4#K3O``z*9_wyF+mLOw2jFjKbNz&d%}#A`}z$3J-$8xG9ja2 z2LSsU`RXAxJuXrZ)BOjKMRRU8-%7bW)#VNUj0P^vuvxLR>=siM;EpHOj1ok^Ue0;y z*IxGm4n|P=XA%g0BUq6V| zoKsnHuogKe6iOwbR^*A{87&|8$XyBj5oQVySx2Kg+6w^3w|bx$?A#K#nIu#X+;Wlj z-qclT0@|xMo^&NxmS_B4kL;yeb9c?(+eI*-xMGBD9ijU(%zLKM-}m-}QFyR?BcRW2 zTeJ<8LY&7@EDs8E1?7E#MvzA=OwnVcwv6!hab^q3`}h>->a}O2LC!MCr0THZ-4_+Y zDWeB;94IXlN%UYln6+n>Tm(?Jg#MCg)BpkW%KHQGHRPm$*^(4rTQ4fUXK1|HA$?VB z(|Z^6LsXy@9Ig-4b+A6a$g~0{xvduv^L7bZj3%b6-jTtcoGiu9iLRwBfv{*5@6Wrx z%=*`7^DD!~rBTZ#WSW!50us(fF{`?!-5pf|j#tc#xdJ*|D>$gKCLnu&47#kzMdRT{ zza2(}&9gk8K1v-V=>^UaqMnhBK$eeQ^^yxS@t0ZLn6UeHoRl39Xcrw+a>8d^rN;v{ ziu{{6+%TCz?5pqSY6st$GpYw?YU2(fw+O9*#4&IAzO;oYJiE7uxrLW)m_h@39}-8- zc?Q`k$lr{u68(UfSnj}LJM`k zVmMV;nGy{5=U=_4GS2sF5+~XVv$x{3@Q!=3Et9^l7I19Rs8T0$jNZbI46ltm4V=a{ zr@4jitDX}C*b2yc;G(wkujR(OH{jZvvYTpBktB#)j5TeO6wcE%{Ji92l! zQHlij{0yXimFCB1g?x4mr-P@eeU;WfBatt!igxyP|7@rXjS(!9A!^LfsAuy2>1sEN z&n-F+IZS6$fF7EW-j!fYH|q@;UQaq>N$rz!`{UFLIqc9YS3yCw3`gHI%y@mP5Ed0ou}PeLt{KcfY!-H=*MCJvAo7spe<-e^YpD>X~F` zTG$>{i=Cb6^=hAcJe_x3d+~L{;TatnZlWw&`7vn^dTJmeMC804?xyeS2~+ zq$V2$=iMj9bgBo1N%d&&JPAiJCPb#lcsGw0-YSH{9@<9`)7{uG{@Y}#{DQD_ z^=Us&-f*oofs=t_YMC>qp3SLJ;kV(De%@jn(rm>^%S2s0vPE?vUzEi|%=dE96u;Az z58r`y#KcxflD%k)Y?B^Us!Q`$T6O9AbZo5l_~s~;r@UmDvrTLhD4(nYlT;1h#L72} z?iM!&$OHwn(wqUE@q59!ll`R`gpmD@*PeHbevl$RUmb}N{~kVlJZJRVWmQ;=_;DlX zf;JGi$8)T;>O&}!<-OQOq4}Oe?KQ`J0d38Tl0&XM?D33?G=!Cpdz#8|Bo>6!H&?zI zn<1$GXp@KKkm(J9CYV*Se3#W;2?;$J2%8}Q95qaXU;bTTHU6hZqA97xfb8U94F0cdEb8=A+_+LNC7i#4u!)uc=%Y|7`YN07mQr}tboYtz6Ec?f(X7~SFs)oL;I4GS~7>M%xSK^H|qfRbh-mn}n!Y&W*U{~KippzZG$^~KBi2T4?} zS`2;c^ri41Sa)i0nI6mSyyr||B8s@O1qf@d=Pdo;o2Q|n*-9->%IMUVlfrfg%LGeC^C*>#N?%6! zembcjiY|khOZUf>aCXm6UQ~9Bwtq%a|7rak=j2pD}=1 zhBRcFr0+}A(z}xqr?wHPTdHTM`x&C!0`{Y9Q+tuD*P_9g=7gvu`DIBRnys`@fbw_2 zCMXA0$z!iBS5#6@|E~4mxn5+MyU+tnzwH9Nke2fyF%P zviLO54K7Gx8h@OR>=Oz{qerkYxFF@iBV7ZlllZs(5>vf<_U+lb zNJdL&m~oGndGN$!tucuR22C?a9L<|NEZ>ePBZp!e`r4`SdBYH5o8fut7`-<`o8=Fy zg5NA3;i18*9;wyoc-PGR=RNl`0_$5x>ZmL( zTNE#zYkkcGIp&`+wI$Eb(Z|At?@~B410X%snf{K`_=x5GUTsd(-!ibU1QjIJEC9lG z8qaqq1lvqRN#op>aVgGjMNeF)GG}_KNXv`F`(^+uB+Uen#Sb$~8_P3>BG-MwMA_;Xg!G5>59d&!{@R z(`N16?%j8!mB+f>p!^{lCUTNqg>Wm2({gZVJlpy@PyM2J#7(nPar4OpAy`ft#KG}z z3c%y=mqo(r=Th{Dd=7+TjmA=%M?aam!%3e$@(hQ@G`WcZ%tP@a1xRuAMUI#x3z;1NeU>nMsxg;|X#8#**++E*cmJi=*@c>qPfwZbI^yZ)7G` zNd0)HDzhLD+bG_G%J>$ZQ1368<54oarj-w7^CbXuZ_(NsFTogt@5GD{A+lANr&EMZ z{xZj%I*UaE^|hdNBir^YwJBeEKLyBrHp$d#zk7i>(!(ah{~L<$*~25UDJry#5~;bP zg~nqPW%N}x#zKrT93VMvNC_a|aW701U!vkPVDwOI*lO!oX{^#2{zmuFW?%WJ`bKoQ zA)1nP)o0af9JhFmw~oa;AmXWovoZ&bha~2L`t02evR|D(LAz07IjgEt|D=a53Yuz< zm|^1vn-4n`+EG3?EZ*P}i!@(?p7w+@8%Kc=8e|#qT2MPsnX}oiC8ie&dX1JG@RLrD zOBFgPn4WPtDa<@7*$`Y3KOq7=_aE~wBc4X)NQRC<`7|05`wF$=3a7X2+Vb9sWyR7$ z5h=iY!&j<*psv)?Tt>aRt=PzY(Olp4@JBKAe@x_PXmhqnyN~5pm;Z3{E*Lx=8DzP$ zvHfxWWxveZ`}g5(10b?aPYb_szKS7#-QdIRs0}& zvah4OTda^h8vz;@IR{j$)Owvb>Y9Ha-)Ep1pY0}t>2F5VSy8OL*U@l;2tSVaW;q)8 zZN;E+wA(``1GO_k2Yk8@Enfd-4G@6q(9Mj^iKffHR0} zrj6fYK5#Ur$JU?@plNd7B0 z*LjR`G20gz^)kA*8e{n4zsD*-5sb}Ci8P-6?^7iWoVkI<%cXJu+lOigZwvvM5Axa* zMGtbsm@TQIp@0ga^+fIl?NN7rrVXxIx-|H=YG5i`!Qe*LkU7s6NB`90?o^PBBhaB zb=!prFY@MqO*xr!G87LGazokS$@y$09FNZ@NxP76vH3;ImU88cqmOmB*?kJ5K%^r{ z3dbN#D~pGoU8J?%jcxI?iBnC6X1=BhcIy6Jzz2Ld^L)y550plTTBkhfk!(9{g;sO}mBEl-57@^|1PaPElu?MqzDhj!A*(AMJnA9YrZNU(dulJU1G4>=0gNaH14m@bhrQV9kTH_~F~2iQ+Q` za5cT^hUpo<9Jayvm`$b-UXRhvP=A;}ggtLq5u3nX3}7!bDZZLNf`lPVgQ*a7=P(|< z(hbhx)G!!;acsW<9zvs1ZPNNbtxQY5kl{fxktZI;j531SwSrE6*b_&&|82{z^sul3-Xb z%V>`~_D-*oBvWZ#cY$=ww`-op%*Ru`*ZfIVzwj**B);lPtwjX263s4>`H zs&OR$Mb4ncUvkXzz_*kLr=5m{4sMrr23ZYM$6cJ*r}WV>K4i#FRu!^1*~N-Dvkfl0 zI*<8k7qcmIi`Enet0(3>bGh~(XGg}zz7h|5OFAA8HOlSCOPiAHNxCudbfuOKb~FX_ z2&d97Xnm=%ekUulMWMfCyagvBN&6=a@bTxN@t(MzR-Zjd&09~QyWK*g5_g=R*xopk zlfq=^@hl$cySv}1((x+`zV9LF* zKj4qC6L^msS~Orp{G3za^2opH!9LTQeE?WRtGH9c(!ydnuI&F>EmA24`D9Ave|Xy9 z^Rr_aBr4uzy~r1LA~{PO!`vZjBKT&Id^IU|5>aqKuk^urc1t|=IvxrxJj$KdX@19Q zU*DS9ZT;BLI+a~0_Qj86pJ#sqLC!WQ5g*`og{%XZIerYQ5W&Tq!FyH9>NoCMz;1x% z>rRHcIJ1}ytUPY$Z7IzW-m11!#8F+f62i6H1sC3xwT6wxqS)H97aN!41RFuY3Tz=trz4m zpM_HJG83bYN>tp7FEvfi>}!bi1Ah;{7%jPbrH3ys3VmDs5Bm*yg+3;qgB$h*=Rat!$@31@j=&Y<|Nfm3P<0OnQOT)1G&53hD-U&#soTa@F>9tN}c>y%$bp8XIT;Sn;?UMK|8)* zDFa>d;2yz81blP?7$<)}XgskL9yVi-q?9RXH5wkNYj0QT9NhC4^sh^*zk5ow^6N(P zb^2E_wt-yw4R-eat{BN9Sr*HYg*K*^(aH z0GPQRYtr4NI~^^RMk^_OsyiId{{kLP?`=Yv+X@j=5D3Nyz!` zRg1p9&V+DC@@+zQ*vmiA<)aY}qe<|Q(i!?@=BV-ITB0q(upn+Jh%|T|_xx8hgIox* zKwkC#2H3q5#J#dMx{h-;TWnaN@Y6gW|p=1(4ESQyJ_1%ld+m!Qk7Xb|W^`v!ekK9Mv? z_RJh#T0FpH@;#~5y9wc$AUsyIBQ&So61)<(?Q7Gzqa^q)|{60Gnaa{MzHb z4>ZUsipzsY3fh=wlKxNrcWax-k6C+A6sb(Y$_O4yLbh2u|c3~KM$Sx#S<kn-gJF)Gl+ZmLX+2?gkZ!BPGTXPK?BdISa9X>1!{zhoZY_rZCTfvIagSBCW6%?2mZbwa&9xVh$iW&kDa#Sbb$`*5T1;*90yD(g*;5xSWM5?m--$7p*e+q(2Po8lJ_}lL5_NuhJKbxN) zi)*HFC}zT7K~HXBF;NRgsD1ocV+;yso4JPC#B`}*y!q<`VG0I#lqj##ghz3S@Qfor z#KzP>w!#w7o=uG7UjnvH3*NC%*wPdT2KWuQO=(e)A6TE5Z}^^((QE5841rGk5}g(_ zQ{Ns&7k!}zWu;_yqOk^&(X3fLBCZIU_nl<&)#oFEe(L(22Wg#DEwE7OnQ2o*2+}q0S z*Yt0PMUe#rhs1*4_R#jwRzP5E)5QvR zH1cu#+oX02b)f~-qG&rl~F==pL6VUl0eDvUpU{(yRp|{oW9{mLNn_T#E8vw(z}nWl9tk42Zt&mY4sOQUFHKiQ z*_!-M+xKvk1uGN!KZ<_`dE8*UFt^@y(k!X|KDv?|Wk%LfMp2o-NZ*Qr{QEs0nifheMO9L$24z`GP=t!v^(GaiI)iYE?z+bt1t|ATUu*mT~p7ZhoC4owF z)4Y{&Zi(p~?ZXTSH2jY=N1PEY?<4vGHLh~pcr|b}AT1C|*f|Xauu2p42s7p$DjRQ= z_T25Tm#<}AUyooRRiMO&GojGk#-rzTvooC%53qgar^OHd`}Vhm1eLH@1rObUQs0X2 z{KCK9<}mT^pS_E(HwNQIksaBGUAX#J{gu5xFi}olZglCbJq3BVgpShd5#U|yG6ixV z;;~#LPMedK(}4BAnA`l*VI03Tm}9s(S)a#@kO~Vud%`BGVr0Lio*>|Jyt56+{0FtZ z>4&|Qpae`fn85C(>9D(1byv&S*2V0C7 z_4(|ro}ZItaWo&8RezFpvA|Ns8wp48<%t|Etau03jK{teR_6SB;BC7B2fUYiw+Y*5 z(gzLfRb$Uf_V+)2BPsD%WlMiVfclo=GmPIfR=GFwkGe7bJmts*d>3ay(iCIJZ@13) zm$$=sL0>b^kU75ddtOfC6I!pc*r<5E4H~rQd#vpuH=8#`?2KH092jb0QP zYhL9n*k8G|p0uv53s+rWfm9%@c@DsZw9_}=!%FWxIA^(y$oJ@Y4Mki4Ly}II&|opI zO@CTQA$3)3K1QvSQ%^qA8)-X(K7F)c0Km36yS$I9P#-a>HC3N0$z6#ov#B8Wb)6LZ z6O(OB_aK0<4C4lOcLtHDstk?0*Kj9LR&#VQ7zHe6*W+WWor9z-lYvPcN^qh_~iOVmJ|5B;*T9Uy^5ARb~(=JG^ra)XG zQ0J%nxib=rX=vz~aRC~{#C1bgh*`P?f9dx?f6OTyA}{R8_7LRnKntSy_{`LNhp*qX z@?`fa#1}uT@%nchWx<$Umd`^R*{SzHzPA3>9i(WA094BYJR#yc{MB;&cJyY;iFNyHsLn52m7pV@t#A8IcV= zD_NKHrcRAtD>0rKOdH%D%pA-fwV6eK3m&VzA?!@?t>Y zJAgM&B$1YHry!bR$FiSM7kXXUyHag$9VGl;zdrCF0&kvEVwzh*Xy2=g7H+R71b(4^ z?wF_L{TVMw>;?C+rJVV4*IQ1urhhNhU3_0*P>bwHqDokAy5x*{xu3f!R?bE} z_NPJ!IYg!877LX}7lh;WOJn^uC@c(op@04>vcao7=d4M9H-)z(gKu})?MmQ{==Oxo zW|eF`H5Kl_q_$fl1+L?CymvJj`lo(wW|x~d3xskTAkK(|_2*2LgAEGXSMWLNaqdp0 zVa;%i&p8M$yMI>tPG(xkRo5VvSKDq)<##?y%xC>R;r13ia;ohH<#>9i39v<#ol?K# z>Q|-t54iTJG_bMMrphioCLGhnR@VLMvkMH1I5d)6zfn3C{GmaSpm2c2>p~AK24%YJlWV$rSJ_yzpZe`9YMVvW9X|e?eweeMg#DmW zo#Zuil7n@K)M;8oFdQQT_S>o4b#cR7()Jz1R>wh8g4}{3*oaL5OGdZJK=umQJg2|M zv48sHneTjAU`&XX0j3Tjs}#*-ZJv1gM&<9CSqke_37ae=LkR2oCCJ+C=9PuhGs^-GIbhuSj7wVfMa9n=Ai$*?tr_bi8_8W&kB$ zYS~_B(YzLvf$mbk$gRamU%i4r^BjE|V@bhWrAxOj7dY2xe}U|Iv=G;8K}E{bhij>2 zbL*>tcWpTX1siz2x?Z(5X(jX2qnS(&XKZVLEPDE5z}bc~NF#0$FSxs7OPhO@Acgq_ zcH(rX{B#bWxt!QUuh#071B3fldcM~KXzMLF7k}YC4@J)hVlP3t>#_J^XO&SpML&t( z)c*X_m2KKUgPu_=D`AZyB!t~i2FTyAS%e-Ox~E}~UH(aptDhS*uJXHhSTW{nRma^B z*4UQ&Wcfxx=`3|*(vVLj|FJVtO^6+Y*!fO;_j0kAZfgjZ_i3wz!X!c5hA~K^nRtwL z5y)t7u}kJLITXDx<4hO1XnoPL=Cg!G27u~-SpXPs2~9ntsP5(KK_(q_I4rP6z%bd! zTUmyoBLonB#t?W>ekk}jSHq6kKA|VF-4O}&cTOL8>6mH(I-|XF3SIb)jw*Q0K!nE@ z8B{7dN+gWeVuSHeX`$XyEmOKB<{*SH6CI--S1gV+e1?_@L9W-3#c@ki;K{hA6$Sq1 z<^#<0_eA}tQ=^xx22^f}L}p9CpymZG@e*I)$g!fsRDQJmZZWHa(!XkbB|xVx?AKY* z7=NaW-8x?BunMgf&+0j8Yf- z>kKfMJwH%9rdF|nvye69@C7+__2W~5#_1 zX5i8AAZ{@I=c{a(N;F-VA)a4VA%NwLPQ6cuQgBAgnfmeRKfOJYa*2QRIfnd+iDwWc z?DZGmnWH)WvhlsXE(=Dfrq~8%5#>xV@)PC?x$gzaIUyPRq_dzcbW&yX*d~(K0*NT$f5#+y;ctL zpQ}d`xY&Z8!&?C;eVxZr9q+)KGs|SqYmmU-YKNt2Xy}?TZ0x&oz0YpLXyEoAb(l|y zvlOQvog6LCTG>N^CvX1+U)FvnmJy*x;>w1d~z!oA`X9rXjB^hJm3AWvp^* z=cV+{q1L@X@M5TlEAplLS}$oZI};wzGi=kz-r2#mW`@BHjf zM}D{T7+8egQh1o|Ak5AYK^G^!BLY@TQOjOd{jX2Aw&mvotn#o{3gg*Og0LDC>X1_U zlmp8BZ;Nb}10wx8?EDN7WF^jc>@m+@gr|!p?R?lFAI5lZpaX4>(`8x9#KQE=)k5PA zqFEQl^NP{co2r^gV^pbCr1OB}?CBg2@r4|APn3{q&EuV2nVSR{ zafLyCuZ0|EJDx9AErO8sr*AicS3CQ)V}_Ns+bvCH;H$hGJ+yt`@B(>fL0M8O~) zz@JZpU;k@?XZtPvu7;04b8UGcbzT*6P&0?6&duCI{bttkRnGjY^}(!cix`IvgHGTH zHzLEKqk2I&}5VT#KpR12u7e2aT#r6`JoC^a{ zPi@jW^}vI7x_!Lwi7YI;J7sSY9=3yQF-~~LBYdM#V}1O90gGbchK9izFG;DXx=;bx zU!3;;9W-jN=4r^|3j&ZB!MC22=I2Oa{r1*27Hn>WVR#DjKqNRb`dc=JDW%uil-vl$ z7+~~+E%BBw$={rJkc~_dCAy}JkAdm<{6*Fw4 zLCoJgF$wPSIY6ix@^j0tMO%8H&mx>y@p}b6TDtvB{}x$fF-Fv+-Ne&$vd=}CP8uNt z8Jb&+_d*UaJn1#*i@Z#<0Y9!$)qwj%Jq+g|cdwa=oi0lA@rsfNed7#3$it2iJ19`a z2L;|M@MKQ=Suv*WI3e7X8Xg5BQ0GPj;lXw= z(;)Js-YJ&OKI5%Yvf1UPzTBZCB=u01MBGM|&MNCg)}0w0AT1Jay6LJ1bm7K?OjX$Q zT&|=YnpdzE)RS?y9YNt=$Fm*-wNO-USqFph>-BJvHd|`0475I0Zo%H*QtBe3V-4uQ z2ohjp2Ox2~dr8u}ph#$%l}1tQ<)3xn;Qa+bvv$W&*|%o0dzdbTR8dAPT80^T$6AWF z2D3u{>tl2YJfCS>MJpxs4D%trM#21r#JtLc9v7{|p|t}&0PpUd5sL*beD%uM=SNR$ z>hld9AAw6EtZs2Kh#`an8OGq=#pHev2x`5)ZgsvZev`bk`Uxlz7Is0!64lhRSkW`I zvi?t#TtE1@LjDJJ*k?k@R6{jRSM*lZU7Pd1_jm8dBXx1t{vVJI4Wlc3e6J(%t|wr1R-n^WXH}W1~^19t2XZY{;3V6sUQq>NQY-|`q!cSXl<-ymxU`B}06e3chkzT%!FV|r5 z4Fu;zcuQ)w1o9na!Nt}?*SVk3m{C2{PU%PddfIGL?VQF0H6+NtheD`2lI^MqxMGcA}l@UXKc}iSUo-IBWv8giDvBYqi6yUmR0RNLpk4q=n|Yz=34Jg$D>m6g}!+ z4-ne)e3^elp2hTA6Y83ofi%r}kM*R+^W1cvOtD3VY9#55G7ER>t4n#3qkQ17iHnz| zP($O&(;tZPp}Nv5UU%$%ORyJigAI~uHm>8j?XNM!lRCZC+3y&J%q(=ue_RVmmX{>Ke zR>yrCW6k=|8+td`dO6tlh5|mXdz@lUtjL2)fNd*wPyR09k#Cn+8TUx|p`k*vDBp%@ zJ6C&tuYrxSH>V&MFQz#KVN6zh5pzcV;xhJ@)dQ)h*W}7FWo*S*A5w6Ycpm+<>;OnP`L$fYrMkQ_IvYkP&NKw^QL=zNlwtX4JVFi?z2zac5 z^dAy~bnR9__nF|#K0a3dJ*l7_DC2;uj?fU}Wl8niQ+gF6ma-b*;LkFCqnY zJ?={37!8A=bz^`(_SIRe$>yc+V%3jzFg#uUB7~ugNO&J#L$N3Xcn=0z6U=p?5`eKs zNbCY?%y|N&Q={SV)M#U0!cVDSTsRBID2A%O^+&x41!|pEs@h6W1YrnB5vn2izigm!F7XTO=q;hyFvX)?zI*kBNz; zwISTvBdx=fNd(-%%V*(ZePt`q8w}K12RP+)>2f2Z9vikI;9!F3z1e#OJfp7r6lb!V zEVJ=ELUJb0;IHB|#^VO6P?Bd7Z)UfqYN*}!fQu$>xOTu&h>0iMlpCG%@MUlE0-$zC zj{!bLz&O)=8pY;>>bIsJMW03gBk4zeEjQ=g*&Xi-n3Q@`4jN|jplu%L4@(+i*kalj zrIBa=EjvR6gx`SYZ|X!TrPFM#eXGKg-037u^I;3!v=$)=dhp)AFsTcfwT0TAq&Fw? z-ySY*xzGOEb8iU{xPCIwh5mk8Ri5%=W!CjS0P^mGlr6?d^TQo_uX94DfMvS;GU*4) z)Lj-lL}J^2UfjaF71B3$TUqhdxWQZ>@hoUC&fH#Y z4|v_Fe-R$qgWyr&oFw2bW!cmTj?}_-)!;XKw~C2zCUA(C)?IaWiM~nM7 z#BroKzu5#gtlU1?OE0sxgb%0=8XJF1y==UUNxwS2oBW?%;3;&QKW*WGXd)U+7BXuL zX(t0G5G0z)4TOm|XkLLfi<@ugTItMG@PFpw2@XZkT@ai;Zfn2H6lDFS-UHdVy7O;y z$i#ss;3h043nL4&x6WZA`TJes_Rmwb6SLEIW6x;r`9{;%Sam!Wm-lG}{)W6Jk?+!} z58_DR)=7tank?m@H?@^amJP=b`LEO=#b3J7R{RC}Rs4|aM;y4dK3Dt~aO!q-NYm{* zctJrSi+BSSqqc=>7ug5l^>n|cJ;|lj<4zeJ^bLfm%ECM~swVcsk>%70RO;U0SA7i4 zz+zZGcd;tGog1g>s&D^UezA4dt1Rqx4{@U}fH9!ziPpx$A*FYQ+Q}BQT<^rw;#yx` zzPNNA9lG)geP|WAqI)S4^2;;^VwMDr!2~eTpf>cLw8I}I2v`~g63O>QIe%b4EBAT#APr367sV zpACkxz;8_AVP-W6K9SS?4POx?2xjHT7G=f$N4DG0xu5B+E-QIEt?dt@qVES^@8j5h z6Mj&5)`?H%ES>!3t3A6UUQ4{?Us_NcP&>lm;mw5=5j&8W@F6xMc7?4#Od-#!A=|;_ zs0TpmkU`qPT=8K_c>+H!fH86%vu|yRyAuNGz!|wobO5l*O#=_iH2a!%KxtG=;OkFc z_loqxLjW_F+^MT3%fZFTj+mejg5N52ix8H*BqK#f$`^T^ zqe5=TWS8{26lgv1RV>a)3;?2QDppu~R;-g9zML-9T=eb1xWOCvm&&4l{#aI)(SO$_AMo>Oi z9vf5y=Ke>lNN4tqv1~rFS1XzpdVh!=I5x(@BK-`xG3WbJ)P>RSIBqiK^|@?hYSci& zghjbdFDzg+ZCo$fcc9mU&IV!_D=kQ!(mswP|nOWKokjamn19cV|Jg z{S5bzH7znhK#DD`imqB6bvE^Ok*Yd{9JS#C9Y5YtqzcW3SrTW)r3J*l7=0ZM>j0kX z^{+Fpry2-otA0!Up6Y4zn;UOCcBv7L=!fRnzy>HG$g9YPy8xV5x8ESCVJ4>GFllk8 zbDz||gHD;!CA4o7{%uWtgVOhbIS@r|d&2SLc3{vAi`u8CVq*bk*{6(eK9-P+q^G?y zddrOOm)lmCN~+L#ae5%{%CN}C1y@(`Z>?<4NDXvJvl+4yvD`h|tcYNdhN#9zPCBz9 z!&SAhGC=eEum8#SUq*zuts;lm$%-SNqve7%WN40**dJ5bal(gYn}13vi;iS5`JXI& zx>2j?wUrJ&axMi1W68+q^IfSlxf5pM9MirsW(;ehRj0K`K=F%Jb9k0Tjm%MPLSoYdxka!!}Nh{915}>e`F+ zkI{QPn6uM;Vw`#UuFY*=`BMnt|LRzF!f{qw#lEOq=&lo5!YHX8kP-&>BXm ziGC1V%LPqZnkT$2&SZZLUt~n&KGm z%$roRcD@!6CA@V{$0Y=dqqpS?U~)Qkm2~rlgz5@nX=X+PRq~I{6DrQHmThD|?_l=w+UCI4Ql5@0Y$&w-MxT*OOH(+g zK+JWi+flH^C7(W#mo&1eYn9HrM%;O$1& zf<%hhbRb@G1X+DWO=7t;`;+(FNp7WqOf-eIkhy(g6gegWPc!Fe5j4@c2H_CfCcl>0 zKLA3Vh?b6IKJ(lFDsH9Nt8h}A;QlB1Pp0Q%2J48w3wL~iRN?p)O2~@~=bsytPNJ7m zGgCW_Fr)_rwq2*8K}wvFHz|G)bs5?s3)gw;%?hI`Tx&A^HRTTW56eaS!z$L+b%wXX zGtNQ3-@Lo=aglbI^oK4RKFalX{@~N3(jvEBA%$QS247y>+c<pQ zyn@XKG#d6#{hPh_RMImB=*wj%XlQ}vWV%e%i#5gM)9ojMoaIlmD9`wlXBbHz}Ld;b3gwwXN&vV8j9GInqBDhZ30wbcy|E&G5sBsq1c~)U+Lh5;{Y@4 zy_Rx;X1Z;F3f}x~JfmF3UjBlF{uj5{c8Gu8J`-`l+yOC4{oCuU-21@La?cSYg57wC z=~wV9bd#`1YGg((Ot|Nf%c7J{h-Z7c@DR){Pvu>v-R*vnB_eW*m{rd0Lw&)AB-$zm z|As=g1Au5bVl3XW2YjXHLEv@C%`fK30dLPhF{E0S{?~Mn*oSVK6^gdqe^>n`P+G>2 zmctFiOPvaFS_!Ejo=~XelHZB_gvKAf+qRwy@S(o4rn8MN&qnUbrtnui8O>9oerDX| z4$BO%Wy>s;w1+W3I5a_gB$%QjpS}n-|2cCjGv$$Ry*=s>G!*=7ZnYP zS2R8*wl22sRj>rjoWs?dB#{kh#)r1F)(1MK@599Zd1OYp*gS z+&mX}C!~q-tbeg3?Sym$o=jyQKd>-;<}HmprJ;WWE-!B~XG^(pko3(ajW*Ya^Gb(% z4n~INW$s6FPXf~mgQoosNCTHaG6o2gg}^S+0C{%THuB2*cCr`&d7i&j+_Eo=jW75* z;*sxL`q7uXT}#KWWJFD84&3MCTY%+r1iS<3fx({RO%AoB>fb;Bw*fK~MzR9$_cwIs zrT=(`ZY?%w@Sfw_exC$?L8fDt9>}7AYe#z?)_G**w6ph*A_lBJ6*bwqzTm1_^MulGjlej7&EP^T+CX(K!kV>U$R=n2S$?ldDk~-Zh zP{=AX^80h8RoW z7v*FGE8@n7pVR_CJ~+7kYDkDS>BS+JA;|j5K|JIWV!bKH$c87K$-+=Th~E?NTeY*l zTPiNFTLE)Yx|W(UyICP>B@_xTxk-xA=jHsMAeI{h_&M(j)cC_&Ze}+@y=5xtVh`{z zb{U1u@sO}Nt5p|BZ5C7#6)=hCfV-s*b-F+$G(qxxqHa=~C1s=jG)|&NG2JJG_i947$N$HVw|R4-_mWDi;g!j)4RMXL+=iYA!7I$<_q2p2aj&fg4*)(a#hA85XknmCdo(q`&7W0 zJGK9R4Vwt~2GoqZ0qg&C?aX0;Tj9f6K|RQDoyz9@>AMvx%^fY^VF zqhCJ0^PA6SC4c_CstdaHFJ8Tz7UhpY(ATrW_oR{cwh!0>R8eg3tW6kdK-rA-Y?*!T zKx)e{#aM6p#Xc6>Wbn~x&*xdf8` zZI0i!XJG9O>)<^%`V;cnE>|hl#3=gxJv-(rfq=&_Seu70sG)TMdICGLMMzE7=ABTN z{c%hdsev{ag<>;78fQ`+aPs1mu}nU4C>x3|NPL}k(KN5!y{r1dbn!&uAmera0}nh6 z$mkh;)k0TfF!}U8H?hwBSGZub* zHiu<;O}+$rLt&cUmOMOK0s&XR41>%%k9#$J{#;~tkARv(Me zCde>+akC(;#Pg}}>z;PI4)H+(Rf9jUHp~Soq3F6Xj1IF%EdgtkM2Q?yOkUC+*s9ys zsXMsNu(cnyy!}4(vPe6*JNfx#Zhac>D{fY4ann+TiI?EsCOFQqm9Wlpr6~iS|2-*Y zBrY9=^QmgCANQj3srt~zBL0e%@Dkwf6EMJL!D#yRDCgj*O0!u;2OVIpX;9*WB%Wb& zApDO`=<@f)oJE{Us{+SPFNz9tWF44kr-8|H<(m2RK3r=tzPsPi+;KU9eWW*KtS6CI z_UiP$a_ro#+0R|QDKDWSArOL3%z>`sNAPY*=;Qg~38B@*)^i759`u$Jpv;NzQ-72d zoK!bx8y1!;1_8CZ;H*gyh_PX=j%O_pDYUO^o0@949MBiP+>rr&L=fRkh4*T?LzAjS zm+t!c+$MjEg0aAnsEv-|9-PoJ)a*+ ze}lZE9VA;U&jOAxOZMh>0-E>oIgKA{t7T^qts<6zT&6s>!M~MHK$fgr^LrN9!Wv<^ zh-0;^DHlb(j?jtxKD3SWX>5Fw?E`yYRgT7r^b?f?`fk{Twk;t=~`3@frf>%UV;=Ec%7(aXshoUg9dT`lzA zeR(%TdXjV>rtTaBjJsxc#GUTLRus$H=KlaP{^34P-1ksjF8m9n0>NP)uFtu`V&J=j~y3T z`PA^J*3b(ofahktLk`2HGk=D1a7@MF^y*}dvB9P3KCAR#NwwZ(-d_&*pd8C&4X*+Y zfq^JS@iCP{ZLG>CwE%3y$+zJ5{m^ZO@}pR~+HZQKJ$*b7j?{ws zPdqEugK057-jZ%!IPdL{>UU{Q0V7x|rCnOqB?x-@3kh**S}+M;9oH%2fBxYLe5mqr zjJ8)`HJE0Bwa}GlHop-6nNUbTF+Ks(PHoDhb$+{cajS$3qD36*`U1)&<^o4 z?gqc_|L^&g2>GHDt@ZS-k1tR7`}rfB#tl3SoJFyOLg^#Z39W2r^SVaB2>6h3hoc*>e6N0V1 z^vNdaoN|}19x+CgR9d{59RAUF0KXV;2rTQB5kwiuWv%1g{)2nw_!iNa#2w(aDF@XA zZv&P;HC4jPl}yCyp&iY5n9T<>jikMNF7)T#--$YA?^p_tt1>Tpw}7{qB+6ijL_L?E zhF6lUW!-0hL`$XL5Z_nLbeYZrzE@TT;hE&bzx;`_cugu;V9t>EK zH!zJ;4nyE|wAhJ2iND}59FGsejc2Aa1}r-us0->qD=NGqTCiAZloFQ@HHKs!{tm)n zj=j%cI)SAWR~Qb;kO2|`RZ=X6mF3xkH-a&|g@+dwpV7th24;e|r%XgEs?zJ^L#8UWvsN2OetaYLknhH#1b&YhrEtE0 zKfCXK0Dsg;K+zuyro7e%Q^uCq)I5_gP+ZMa4hKwtPz(-Hu*uD@zXkQL5?H1ESbqlY z-YB+MX9ivf8=B_Amwb*RFMPySG<3_Uia+8N#(c3ss#7iP0~sWF$4{#vOD{-$w8@&a zmwR)?`+kk4$Uy}IM#nZZR{=zB#1EwG%2>yomYN!fhRPE?xwK&_tB+^ixEh=YetK}N zg@eC=Wwug&mQc_T!|W}2Mv4ZP*0*cs2Jpc|xVVell7-r(V}|RT4?49`T_FU6MGxev z<3GHxKQ#7wUq1G?O9HP=EVQK0e?Km>77F;{t(||PvQ9tn58Npsnx_^O^8(T5c&y_%R>s3C_l?hoTY$Zdv zoFiG#=J|#@%tbOU-ErXD)t#<+N%S;R(2QN-!f2hkPfIk5zUMN2WD{(|a;Yy5Jb^$Y zBd1iJJ7_R28s<(IM4Qk51_s;#KiQh7kjcRsne(EKZR?z=P9H#13}S@!s814H!QQqe zKPlg-=0J?T9w9~)uZ`wr`ZNl$s?CVm`*Lu0AIO+%K_ohVsqhICeaGm6PVj(ljHOmU zEglw2LnPe+^Tj-_jJwdrRcdveF1%DP9Y^T1Zon+CfSbmhfE|$(J-C>CRU#y<8zIyg zDx^CqPZ!mNSn0lzl;j*x)um|WD^!U#79qL9O2R3n!CqCHr#X>+}KL*n&OT4Y2(kw;f$-wE?@N7e3P z##0)m5ng59jif&FbTeC=vc#8Lc~i>0;z~>y2g&qq;%Qkr`H=i zR+4yyq)T6lDwkuGLgSv#fm|{|$$ujnRuOjK7j()-7Q>t8$`;w0Kwy3~%3Aq}+FJho z5x9==^&L40W?ZJ%|si#S#dbRt(@T>N<@mGW@{0qL@)-?PfKCg_*)^+9egUON?d z{{6uso(GKwFB84K0XOG_dU<^YS0fb zU@`wwSjP{cn*2B;u~4h591Lz{h}B{x#Z@~0kawLIbPc|qUI`XIPKD}&qR%fZP+6mB zRku6v%2opIS|D*2ogN=2A~K=l0Zi5cN`M2mb~q#MmtHBmO>6Ur)k`(^;^W#cE@xd5 zlo^OUH->_b{hyxPJ9HDns@?-DUw#!miV&B1*BJA}Z(giH%1KM(f45l<< zsN>Uy<2$y&LD^OVONxmHjV3uPo0fsZhHcAZ9=$` zvCJmqzYa14sgd8XZHD%{regMv0sik%kp5b4AUeszUtTryOrvoxGK(&y0~8<5h)#Na z=y^}!z!1N9h)$nv+SzKBeAphlzxN2pIU%1LBiY(SRN?DF3GN{94R6w8RA9p}KmND{ zN6xfNVK;6m^1^sLMJ!1Kpc92K)ru*?#B;n0r|86GsRKKdN*2bmm}**xt4iWb&1kDB zDvuqc;6cQ0W5%bHhB{L^h*;{}QOY9^`%||@|A&j>-h|0-h75koaC!kNWswKbWUx0N z^R|_v#|9k{0dwzo;!@r_0hT3(H{wu2N~N{GdZPv=KIY9D!E}|6XUqk-w8tiCSAYX` z1zUDHab&_QII8$TkdVTXN*ZF0P`T*XMaQR{0b_(yAc+Z#w_y2@i1f18W7Nzj4(Y|h z*a)t>F=v`F%CVcnG?+2k9DuIei?L!_H3au{%%~+(!XQ$x$at)Qf?e5J+rTB{8z_Nk zJJY7CW?|hfuq7pCDO=>pQrmj*A@)@xXd}xI=D7LrXPhUe!n2-j4#2e)xPZ;xhgc?bD+7WnnbB21QtVW&sHzuTJ#ri3 z)glSuK6=>KoG48b=g6;oLUiP?_ei56u|i(z2rW{~5$x7V->{|A8+Pp>RC?e&kXp+M zYRz8-SKCcw{k`;Y%OoW?=WD}4Yg~Zv|-_9YQKj@drTtbm- zwQSVAPFqUe(En`wpg%mO$DLz@mV=0Vaw*#MKUtMUl(fY5?P1kl*|9rY0Y`YqV_@fJ zMBUHUOFN}*^`aS-KPvha`ON7;@4z9+bG~yLpPT0+^XI+(^$FnI>j5kD9AUu~sCqfy zjam%`hKonYJz@+7p%9Qpu;JV{1Tr#$T6j|1X{xa@miFGGCb4al2i|V79N* z^f%C*rFcx!zTm~q(C`M>yWp&}BxEh-&-A)>qd;1}LF?@)%=L<>ELN7VKKFVFpk>R~h!{J(Fu90JY6A*FfH{TYgU3~h-*=YyEA zKQ^pR<9lDtMQM3F6AfIuoNtAdz)ZSLQR)N%Vkco5i#-48sLOxb-ZQOr`6F78R+I_8 zo;boDl^j*_#QzBnzUh9UejcZAex73g0QIoq%rDSN4atDEfqRHBwbL5QWQmVN8`=q0 z$g^hGxIxawqqQ2q@cIvs>;jbv&QaTlS8t2bCff=y`^jd_KD^QS_~$zck;12!eNO%q zO-kLJkVOyy|DqthL|VS8+$G^z;N0c$uUQP9tj(9?4|)Vz^@5HIe4#p&GuIpHQQ*ym=hGtuTEJ|nXufm^QK2mO77n1Y`^)80dU z8e9N^YvMOdeOcLxUd2Z86nabkhdIo3;1}9BtcAwH?`_rx5v2OtZ%=AEdsNXrEdQ!Mca}}05FOyR8Om+xP8m@7OCpgwQ8;`V&ora1VBieMyJ>q!fB=ol~m)+e`YCL460e7Y?T+sMQwB=rHqR`7iF}4UYGU)^fJ0evE zHv-y{2ahGmikLQ^kQ%zhUAS#>G>?86mx*~Gv&Mbsz5g}Xr)-dr(@KTgRCMpmU3lAEtn zn}9RJ^r{}{(?Xj@HH^HP|9FQ1EKd;6TT}0tutlEs-p2n0QUirVpr`-O_1Wf32D% z1%@(`qcr|5`N|9NzLjt|jX4OA$G0s`)%D+aWGng`A=fyEvKfBL|4etcn5h~s^y;K; zY(Fe=2SjuR8nccDT^Y9r6@z)~z~uL#tv`PO=(^zsEO8qAm@jP7@3U~sQB zDFb2_8Q2O7E3Cwc_4#BOHCsEbTa#4U)tq%sRI6bcyQc=7hYz%7{l3+rbGzgeCb}Cr zC?2Kzec0bt0qKNjO-xfw06in)K@}T?R-eRJZqe*DMhT-&$8O#2I>T|F__p;m6{=#I z26xoL#81?z)21-d8P&lIqymF-OlH7v>j<>O**op#27^j;$1a16!)dG3%{%M5OKSX? z&oQ3{=j>!z{!KQIcEM$EmL@YB75h|hvG3czir!sl3-b1{(d`mwzEx1>t#v*h0D$fp zP@S~?!r1SqzDZpEo(Cn$Z>#O<`_yAlO}Eu7p&-B^brtiUu#YIv7m|YeI#+968@ZCP z0(E6!%8qq^NvgF=e7=>|FqVMzkz5-Cx}3K`Rt9N7mL$`6>%CmGZmn1DM{L8qArp)! zXE^;f56$DEpXA(DRzg(6G# zL}*Lm1NaRe;Klsr!;fy9#~Ktpd)lS2IZM>!kLb$T$#-aar(+E@@yRP1PpTWN`hAM7V-l8E9=|1l=qlr_d%&c>J4gT>Cx>cMPY zU%n3@Cwf&m*nbgn8YOh;QoRS7DJrsX&I@y*U7=X}C8^XDc}_WUXM0P!9eDPwmWcGWFC{RBKB~k=u_SVue1ja>|U!{YD#s z+sky=6KLbU#yyiSOe3<+;(f~20-#NXk-0Cd#8|}aHuiXgOAUB9dLx}FGHPXhBGEts zL`5W`x`af7Xbp9m$kf1+6&vtzL2~@(C+1p{A|)QKscJ5p&yhmBLU-#r2r$N$S1ovH z#&2-g!BbVRP6dqL316Mg@;A$*Qck{xaveOt41iO;f+xs!^S6C{Re6}I3aLyAu!8)Y z0d&d2)iH>KW3hhMCgq^aeM=Q3|2x@AvZQPypv+QT%*7~Im&>pl0goFZI;+bD*Jf(TZDQD_db zdC6$82wJ;L6M2Uxa>&9+B@fj>2-K%$wkkZZUD}I&K$5{C8-Pfm?iMR-)ZwpHR1MX_ zQ{hwNEBRNZ3@ynDA*+BQ!K5JWz@Se;X!fBVVxk_yF$%S}_YHtg8S{X2f%L3CvP0T{ zFaEIngQMDILBa9SKryO>w|Q}a|Ai^Jka@3c;VRJ5ljy-VvilaV6$S6HG3hFv=Cz+^ zw8n?qT~(3Ki?^g5RRmnx)rR6?z*hD7zV03TOWar(KmimOnhM2WIgpVEEE|y(lHVM)=`|pEVHqZG$`o;>2nej_)PuRS&8V zHkaHWIx@zfTlrz${w;=7v~{mEEUToX1hLryp>Fn9%;2$`ZW$L7^_7k*tg0BcA3dEN zVI03@57s<(dx^L|UGLCJjq-T&6fsFfSI5%-Bh`Nfa)RrgDe6DGe%S&0n@9$ugQ*9# zueA9!@^(B^Q&SWmQWjNxTbcEU0!CCfahqCe@0+SuXxAHd80wC*H%_R274cDPhhRbR z03IZ3{T3s2ReM_i3rFV5y*P9gmqwc7MVi)`S3z6XN8rHw5ydR^a6Y`-wH#aeB!{vm zPS~g{`QZ|ivTLw6Fxrv9G1u4-YIpFY4G(y<+N16jOr}hW1YF$3ay05~)*GfWbj>?s zmZ^f^M8{NzukRwv`maAa$foKLv*#QhK7YQ~8=wlWB|tvF!A$hk{;0o1;4`h)^fFxP zrQ`35VGkbO2h|Z02Wmaq{>wcbQmI!uV{ij~A?Nv)-7-mNuk4VTGwL?Ymn6%IVL_ci z)nWqu62mjzn`P0PJ;@bgLjfrv;b&@T0ZfOTmm2*mt4D{FV`m_Y@yirY&9nsRMZj>uu*BSN#gKX_?CsN7v*XiirSy$nO((pW4k!Vj9n6Z~}uIc!}YkVgqx2ubu*TRVC`i58H#rU3%B=$ABA-#Qm@ z$XG&;=8vxJnjC=|S%JeaNF~xi=Lb(HF4K%wSQ@iE{XQZ`hJ4rv0#xcLU+A!o-0#&A z;vkERm6oQm)R0oFx3THm;vrGfS?efPD`>CijJYUQ(H9ICs@e(hc^5h% z9+fB}nWvPZ<5#uT)BV`7PVlDGdSZmembkS(2(|(}96&b7WAFWWFXYUwsqs_e7^11&LDw_w=5OFq_pEr#p4H^hkDI#=qR>Orm}>3+&^y z5M(e7`W#xPAE@RSfT7u3#T$@8wV4;o95VBnV)wNZ8`e2x&x+|6kkU6V$by9jFiqI4 zB^dv>h)hl4Guq zR=hCjC!-vm`BL#46fPpIW+UugWA7>~eA|`IC<-_xL(XN*6~1cgTl_lh6nEiT67&GMbivJc-+ zC|KlfXzfY}ki3UF0#c_mywznX^C#rxTqOm{RKkoHLP#)l{s|>zX3}g408rVTDGP6c zzGJOSb>{b})RxO(-|tkop7I$(_RO}Tfbz2E(+$=o?1p3P$8!MzwL|6`BSpO0JEQT{ zv6aR222GjS*{&<#~c9=N(mTK>T~ z!s>c-C}C@XPo(zyiEbZ#ZLY!fo*G=@@2vp}gb`v!u@S%u*^uNjgtqfX6uU;Ol2!Hv z+?DE;qB)!9?kqA@oqEeR@pn<a%;lL)^Qby;aIsGT)**U4;%0ma@m`WwoR1P6z81J>T|V+! zT6~PQ3X=3PjA$iSu;GrfT`w1s@H!wS_d_hJU-l>e(D8aN^}#w7UCin>MT>Dpl6nc4 zNPYFb-o{>thdYUW`%J|+n>i=t8(=uEQ-2}BSCBgL%wF7FbL0!YPv7QkxO6%{Y0Iwa zLbzV^r{VS*q!T7b^6wD~H^QWy6rtZC9(k78(Zyv+A`KZze&?@_lGY;DZ#fou25+wW zrMk^3b_JbX>HB zhNn|8gpw#taP3QRfiR@7Q#@%7W-vl}b%L)b60*=|9OPHvzpfP>sMgh_b@> zH@rHJnpe*A`)Y~7eRqLxJFi2w^~VB%Uc78vH6#oI>vp!jdcqY;!yV6OgdL#NQDpk0 z?xwZx@JUlUij&DDKHA`vQ!+uN{KC#XTmCfXGJ+2~+oFR+U@HiA`Nr<&fW)=15TAlQ zc+=vG?c*rfrw$1hbn@T!d6JmzRzU)5=XVOgQ>PQ;?K79sIL&%45qoSD71VFj{iMIFhIs3YwWLA zUl;~^tN-_~S;UCB-^NzQ?CXo-1^v0RM14S%&VZEC8zXMEE_v8VorO!p$qm3-x*&#} zZS?tetB4>s12j07XSt@H_nH!X$v%Zd!}sIkC?_K8wK8hUOtRv%Ur?B{eV;yDxdOE?E@jE{@iOmG9UW4Kr`#VE{`R}sQ&LFqsE#J=lygqtOyF8>MP(}GQJ-s{GyK7Q z9m(A2eMG=jdBj*Qq(W%S0Jk;|05vQ@#b~!*q8x<*kE@6>%m78@|kG9i4D2to>(5kZ?Phe z{e%d;r#h#cR+23j;;dXkxSmk8UV8(8*{j}W+amu)t-8Mf)r!vU0?8bBF|)Q)JOG3o z$by^dRIh&mU~W=({DZrFWCeqV+){NmuPEEqPTqcm=g{$zUE}1(kz_EB^fpcY;d;CV zAPd_DEH+UquUxdqB3?}wd+JC^L%RT+;2p99$3SrF0u)G*YPV%E7G^wDFeo=(JcFH) z(chempHDy2XO*!L__X->=3lJ}*QH~J7l)ygxWs(DA3GvxDVpqhOUUw{iT9WEm296_ zt=AREMo_Ks*5GqbK2dY=e)?_f*u@AEf=nP3mVVue;CXHrOH~2G8^9&wUmZaMzs7b) z4D-&wzO+yGHx*0Wi&{TfkDSPT-UN;VYZRkn^W{GoYbTI*D`Ty-M<@TX&NnOF{oMF! zpV|YY>B9qMSLp|IZj1FKux@&AiI;CAz9bQMc3*)u*)dk#KT~hk{Z?(Rf@+$N|7pYw zrVTCf>aei=zGgjMJZ=&BQDzjR04d8+*w>q$k?jd{6GE}4d|_*U_LoG*8nf77%T4ox zWb838TvJeGv<7;lTbrD@oV{3i$@TDCk>5RCRx{-$~sIH0}`MsMn6`0QNq$w^F zU#7dL_vZi3_VIuEmD=pb)-6?yE7zqF%ILkY<3r=gj0?&{k1sbKEd=Q4v=@zQ`MZ9P zP+?R>11HjWy6=A1jJZ{#7ZCYp;o(&dDi~YDg+`qjgQ!^Kb6Fs2I%@`6IbE>2#-Y-c^1i_ggaCy z53pf`b&hN*(WR_|#+D*FRmDy3$|coNFnwirfl=FHKx*P*&Z+rw8Z2haO`pS)USwFE z@R39_hz>FO?tb3};|1P<=b6}Cz$)haq_ur&V9>s1YHQTEVCjQeR~x`SlC*Xb%zeK{ zl)=&1*fRBwsp3_r8ZMTF{C{{iEwt1NsMknbpUH|cCKHvWZ}Z(KTasq~9aH(WGxiO} z1xpm9naXZ1i$vU~%1*bsE4(pl%2at0Z%Hu^!pVs|aPI-qKBNst0mEaIja4n(wUwZZ z#NI64j7j~4JzLa%+|Ve4oK?Ue5Ffw#RPF*W7Lo#?YJ7c_GpetmAcjBf zix`9~Ry=%Wn6$yMa`(O!cL88Q-1fx4bncUptZXM#a3oq%3oI3d_um71(3t8A}KystI(DWSl~TeD__ z)$&E%g3Vt*Q*DE(htzJ{`@kmXWS4*L=%0ZT^YOn-Yq9@TFb()Z4*yeklbhEtCmC6? zJ0+}su)w=#6@3P-Vm|KVJ}}RjnLJ{_%L+-NGS>_CDiv#0HyYV*MVL^i(qrFS6L+0q$ln-FGq;PPxp`O3% z0JFdCW=rv%&>*Bl(2sE36K?Xw5JZhG{Gg1~Av#iM9OM5*u8qm^qkzjbnu4_)6-5?G^YZ;yrkdju!#gXQ)2LVKSQkLEgu6huYH=vqv@l@79spmge^N4j z;&(NIGA1!_4>Z|;*iC0HH`_D1+WdudY|LVTp+#LZ_o>x}zN$_;Dd}L$P{iC)L9O@w zX7BDIlp+?uj?V30eUY)spTJXpiAN&P!G`Q!59siQM=brz`^Q>io%e~jW2^GRA?Mt4 zFMeTpr8!`|r4qVoK1knz^M<>!ZFXi)>cot^? zYf!7bK~03%ptyJ$DSml4jFnkjAoN~13i}DOL;kpD#2LC@f<5Y+M-B^&hW7eqz6phX z-?iNE$&;*&?32fhH%Q&d@=G(5{ zs0tSf%)T;}HCv{>I$>VPLqAD6{_%g9I?J#syR~Zr(v5^P2ugQ1A_9UmNF&|d-6gHk z-JQ}6l2Xzk-2zH?u5T{(-tY1K)Whd7*1G3?&2f!!j)JVqcUU(JcF}VANh6#oQp}JX z#)hbHt*wmE=JBzsf z`E25>rJVxlF9a6?ngg*GF0mygkP{jTzU#%$EQsKVvhvy*MHec06m1U+cEo`tn*)@n zvgkA>ybMJ9O#&_R96SBA7d$ihA2I+QmfiYsD(u2%Zb$S0)&OTgO3R~cuEa#-o6 zdz*>1;Kj7cGLSr42By^LRpgy(=1JENSlfNZ79n5<_&;YBDs(c#%cqJ$e_GKz`Kif8 zuUuOipS9_ZFaA!~g%nMpM=?wf7)i#Glga-OR>*d zsSllOYT147TAmF~Oh+jfK8-6%-bu1Io+xJA45x>|J5i&TMrc6Sl=_?Y^`mcM%*%0e zc-FxcYw0kEXRLBI!HcJtGV-la)uH(ucp2tb0xG-N$~- zHN|``u9bobygAUee#RIJlcrNvUK4Q#Djtc0qc18{nbym|>TQrK4l;GBNO&tM`S9tn z>HF)bN8Cg+pQ4+p>)^71-c6jepZB1hZB!1sid@kt;zLI5VGBsk^>~M~KLlZkAg>aP zo~dS8Qft=7F-7LP%B4mZRu#2*jID=&pv5W%xe#A*gcM)G1>sSIX;*C7*7&Q>*f5(E z&Ky-cTFeX2Y!DyC^S;|6s>Nq;7_mE~OxQdA`##VDATX@RWV}Hgf!m-MeR1V+3DodB zBn@DR{>LOi2!{YwBTgz*yrHBJA@h6T%YTWcQfzU<@q zU^#ojsgn(Kh$|1%$k(ng10poowXyKqKbc;?p3DZlJ~%`e<0C+jM^2ybt44{CE0Y=; zFL$KVYnHH7&qaArCil`jpSi1e9S`Y0E@F_D^myyXu7VCvI1J?;*dOf!y^ip6F3TR0 zg-?#!hv1w4u>hWz&XD8j$ud1gmK3phO0`_S#_+9sv8(Di$abqR&tb z#K^6WjRw>B5uIOZ^zTXTP>qjh{P5zuq52Za?()j7`9^udw`3}le|@OLJXq`cQ-9Nr z(neFIdw=5JDTizK18Tp5fZ;~=%P`*rB)`mrr0y9N$%35v_s|tEa3U`Wvli3ZM5bZD zm7{4$=>g>1`j+SzfclzTwWRV$v??vC-~FVy`lqaitWxZx8Ks0x|-OjU$_<` z*ZA$K22Bg0?SjDVYh*a{iP3I6Nq&fdhI2$tIg^Y*Tcjp+C!K6d!jXn(@dI*Z_&Um)8J zwO}9xh!T!n<4Q85l1Nnb!`za|I^2>GqeAaXZ-&9&iGh2_v@(@n&uP=cN5r*js6nhBEzJ5%@fwmByy^IvJ0 zgj{Tv8pZu2F4us+ofgo{2{tQ~lwWz7>+=;de(0~|zf01St!)Z*mDm4a{hqV;tDIL> zkzVa7vpS7=_s?z3D4_*-cf%h}P+(i%3q5o#*zvAvpzXS$p<$278sV(Lvyxz>_ii|2 zgnE{{k$z}Ec%aqYeyQ%m@{1QUO!NSW8gfi~BF+~rw%z?YcjjLGMobi4y%L{)FF$Sx zl;J2jr4meFo72D{TTUeI*(nHL#2am{fwTtNCch8bGfpj92#Fly1Mxz#(>SD>kPCOo zeKUIfmtEy$fEo`vU1W+kHuhzHCaK+pXl0&vvH6&4ib9^;4tW2&7S|ND8wFc^Dp1SV zIMw}2-^u4qEZ0VhQIH_3aPpDokx^dD6YT9V3c}U$fI3EtY$qO;vP0?bN+9p5=ebvK z2y`hF^`Mts1>e)7JVuzgm=aD$e3muV0j$O0-^dQs#C#j8VLU=I0t#eZ8Q- zbi@k?a_8qA<8-R~u%vE_OOQ=%zC-2ce}kzuZZ?5AL7xVhMsks8{ytJ)&7s5G|B@^? z)mjLxxVqT8Z;`W&40wNk3_zh7&>i^+)MBzR6RrfU&x5YaTTo$OVO;fi&Mz0&O^_hf zYk|_RMu%XNxwn!Wk%Av>ipte0Kqr`pAb73ldjs9?-5-IxQ%=fBx}~vr>YaMC6?PB} zOmdaQ1reuVYJgwG&7U9e5mU005?(OcQOTax966^>|K`8vjAoxU6uT+&`~hZE z3i)g07w4KYCR%A#@s)fS^+-CC>Mf|$hy)OiqN||~M^h{GQA>PGzJqmmENCx02eF2m z!YGHKvk)}tXEbC2TU{`r2*1l1HO98|yOBuWJ3W=T?=d4^A*!wH&>q&9fXTaz~({XAPm%{%CM9B=aU>FeCdhDm^i|(P< z9yj2a4*gq*J@f^e=T6GD|Bg8>6yv?4c7#2Vc~+JHk2aY=K}^TLt2&~ zf{nhv4z`O|*~{|t{RWZeva3RW(w(~Tjgem%n&ToLDbd_kalOL}KWwv@xGs34)ERen z9v4Fj>7nzO8mHRx`?t5X1rc8HJ#er}FHI+a{rM9x&ww~i2fe{7S$6L}iL8_eP(#sY za&XG?+s&OfY{n|u3xHFR!mYSDOp_%~f*-9>o|Db0nrTV1F4CiCBcaukf3%mLlxH}? zGCM8qxeiKo@AD7vh*TGM;J*qxeYIEEVN@u4>azl|8f~3*FBr-| zVS~(jlXIFz1rdEV#VD6gNgNu!uNC+2@Bq8FU^!0t;nK%xH>*+U3kC1n_(zp1!iOYA z(e{D9=e*<}-fle?&6m*V`>6FgaPC0vQ{6`6` zXddb&S(fP1dnjG3rNqi*B>x~8L%Ij-3@g<*H9x5$gB=|_zu0|A`G*7Jr8$GtY>qzL z+7we$l>4B(MH0kAw7|LGQO8eUlHa4*L(5HZ}R=wCK=7IAo~+Y%1g^TeBlwZ z0wNu*P@;tfrwZ*f=U=gYlM0l&-z7Yit|~Si z+ud=j4GVQMEM?K^F^Mv(7xBD&mEcw(W`qU&v%ptXovBPw%#|e>wrLCjd51mdi0W#! zWWx6uzkAh2)uy24Z!WNT#KTvJ%tKX|?J>zMHOPA50&(+H0W z^(5gnIYx2)ccPykDTC=ajXm;c{oWYqvFIVV)G^M13?l>@u?#kq_w6q#<8%qi*rfD# zt2MTXA|b^}d4Z_EKs363Jj#7{2k(_-Fb=#wju@HBAHy!1zl1bsC#eQM)f*=7C9LXC zzU>X1Y;vKzm=l&2W}jSvHl6-dZa!IzWVB z#HdiEo`?|x$d2rk+&pVVY)z;|-qSpI>X3_$_%f0A*KHVe6EWZGG!NB;K}zD83d>O& zvyV6>BY4)W$9H3X1EFK-2`a(HSSFO3fZ1(QoEj?K0Wkid9Lj39<^5Tv6SFZlffi9^ z{RYjR6k!%pq%|y4Z_$|Xf?qfDNbgS$iZ_{NWKF6F4_0t>8oNKd7(Ma3S>LE~+ro$Z z@nM$S!%6BGGu;Fbv;D4V*$!mIFmuILFz7A;_X@@AaCy##2c;1^q=aNv?PRC3SVbE! zdTerMuN~TC?gDnJ=3XB;I%%&^`$r5xpw9E+dyxOhN&}h2mOGVpt}Xnm(Fdq)pA2li zoU9+IN8Y5&uni#fmF?Oqd$tL|PMjhrs|cVO>z5=ukzhK*Im2Gi}i2-9@P zvq*O)#5O*YpEN{~YN~hy54N9npVlw~5=)sUqo zBG@YMcCZ?3O>`fMJKfi0@ijaTCtoq>8=3Ue35J38V%QfTE>MA)`%UA5Wm(AR$4QPj zVYqXZL(suj$6hS*s#wV?ru2xfhRNle&3rJg^mV+d6sHUh$Mq=6Gq)JdlW{F7C?Y>Ic#8l&mnG7rFnAb zN9T~A>rPzh8H^kd!FDi%u;4iR0|Cv}^bm(syOsy-+fe!i0T_;*E zWp7m3ll}+r&<;MOO{azRL1^Ht|3T@#hk(BB(?w#y#N$*lHP3-G<6eGSD6CIJ%Vz23 zXw-?F-A%DEXSm^>`#sUwGF*Ix!A)yLKe$k!vSA)X-hyB(%gmL6D7rj}GhxQ!V>?jC z{5dwIvg8=um^B(aNp~2JQ`w>*x0wx|&HMQfGzyB$S8aoScdjZBsXX~WCTOY?`fm1s z%5Xv=m$Og0`&-Jc-USlpCE zeQ&dp%DjJ)ltBtkr|y!~g#YT6G+i^6FU`lA?Gr~Tc{=vB?U11}vGsRTdV@{bGPs-` zK`$cmHTJ{_awP1}!i{K1;Gwbpaja%u?N9+_;lV1kq=uZ!9Onx?3x=xsZA}O6Uku&~ zm8X03I>JXe(U(7y$HGbbLkvEBh)H(3$hUF82#}=g(g|Tt`kpKY0WGg>6+}bv4uh|g zoB^H-nf5ikAYn50o6Cym6C7`L;@E8l-u|(z#y3YJ4>WMz+SmQnB}4SCw-vJ_`e{i@ z3kkFM>o=y1-lTeCjz+25Qf zZ1Me{SJy!BCaqiY27>ZoVRPpFS@hrD)@)!r(PKb=W1EFV$oY@p=7?J? z_Y$mnxCj1;pOlVajmNJb>+C~E;>jwoOqx$8w*U^1=5~9v8abf;Y)0xX+X;k~%ZrBC z##%RkkCtaQw6CF@8boBC6+{Sf+%^svf+O$g#%IidNw$aX_j@d84tWz2{T=tX&p<-(G{u!-_vJ~n&ga9)#Mmw@iXg@ zdWX^`_z$330V-@3UbQe%sgbi0=-u zN7ZJKrT3exr1dRvp1QC1=xYK$*DZ#O2wXG~F)%*CBERlK*w+AB2J`BOAmfl}cT*dB zld!+N%8g_}UB$hh&Z_;{GnovMckE52?G`$7em-bh#I_)5%^PYribUa!eOtM}Vw)-H zQeDE_2GOYMj-mRARk(p$mSy(8_Q1x`9gz6)sjy$Pzz zVSL!f8}_XJCa2q7TCrYgL$`R{em}WnEYywN_&l#EA++k%sAcyTQIFw%2af?sQLQ4V z_3|p?$a~;B4fCylmWe-BSAx+o%o>8Ck!JY zt=0V=eLFfL3$HOBxtE8~K~R(a{5K!~krjP8Gu1c!NWrF|rsE(1P2e;`d9=_jb5P`z zOz<6NN-`??S~o9qkVM78OZ7Ur!&v8W_&G}qHKG+MtuIAUnl}yU!8E`jBhklj?-t zAk+}KB06oodZ%PNu!*;8qA~^%1^la8>yq!&=mA4G`jvq>7z*I^k6a5)Fp|x<5;4-9 z0U?x(ZM;!?UNxpLuv10 z9Y3pR^Nx57Hjjm88aKZBlCt_9(jPe1KwBwbNGRs={0OVzR6MKLZSp8?d-lFTm(%9K_&OFJO z?y<*|>k^LxMye^s3PIs@|DJqCZ15f7UFkY!b{)JFPfs;^0NhJ9tK5fr+C5i0ii=A> zw0EC+-iuw-!oz5MF_WAu{lHEe8tJOn{sss112sW3_hPQ zy_JW;_f3jwL$ber^T9?Ercq5SVAh%c3PU?2+=m%SIdf579L^QFnLQp|U$N^D(`QJN zxGk70ZpcKY7?=Rr!~}{*2QT^;l|^n_(q27ZXvRi-lArz7si8d|>O|@UCdSZaekbRh z{gK~4Quu}DIg89}t6EsIHbxpty&f(8QFd5s5d-C+qw3WW-M(Vt?L;%swaE|UYm_U$`&{Feur*TX#VV=MDPqkl z7RaRiUt8uj(C1_Y;PiXC$$JbbsCQUtCHOFo!O<5g=y@Rm&v1-2c?AeDiQ1qzQdeC) z3EG>m49Z6)eXu$-CT%3|is%l#Zxvyw}9AkDxWa1%W^#+Y4P)Z!9iGUOKG-Jgw+1U7V|4 zC?O0{&D$Ww8O>O13N;N%j>MW*d0cHbt*XYU6Wi?TowRN_-FFI633q<`SI~3>kI{p~T4|V-p6D`%w^4zEAohY;2&Yys`w5jR3d~6-4M;-rh0z|L7{yj+Mf;hgW zb7QTe!*oY{l}?}ZW0GI34mhn7mO2;8)LEgC<@yng8Kxzs)t#4Je5KzXfiUO%aCe0j zq5EZ~Cv-zhZ%WjBuqy z7ZK}@J{;!Yn;q7bIVbJp4$*aqO;XfS*NgGzb0v?o%Z{&1Gz$qy7RbQ63eBP5EmBq? zGm_taD^!|41XHD(-*peg67T{X6v}51&$^x)tbeX1S~na~!TtLq?z+w{#a&iSa6?y1 z#KKob%C^R5sFRLm+oaYHN_(_~+=#G4qV`x&D++eAt9*c`)F66HD+@BN;FaS|+{sW< zrR-7H>bEre1APTos2Gzg>xQGp$@@oRPD+uC05&{h&C~o{5!E#^qtBSsW&R&Cgg6c~ zt+>?63ksdIxnP|~l79t8sr*eZ9NmZy8mOd>BB(LkI;I%*@z0Ztb_A0)ifq1`f&Cj= zG>1czf6J|I`m>=H_|vho~jbAxxvsQ#NdZ`4}(>c_0Cw z;st;ppjBk^Eq|h>X-c9c=Y_tqg3dm?O9gll`zrR8c;;o8D(U6Q?R}%+vE@X?ZFN`) zj)n}Ze;bo&oID=9p1C(`qY_=9@*(3Mf#U1Ar8P9fvkj0x55{=SYMOg$WdV5 zNJW`_a{j2TFeo7R7`wNXF{H?%Lt3<8Rw11Hp++fMA8+-%?X)bhBRBMqcHlk$6?6)Z zyo8)4bq2eTJeCJEWd`yK&XrZ)=*3FUB0#v2{?hB#XJvNN$IA3v#ebST;$I&Pg#fZQ zQEK@6ytjBZO#(BfY`e_uQWNvdw`q{)%M zGOwra-lg+yc^9~&HfiKi@sih&$eb1V24`}Ls2Bpz!5z&R=t5R}{KyT4!E@RQ7AbSz z3KxvDJPYnE2a8m-gl{-MO&|g@o^B&J!mjX^R!HD5q~1IPboBO9&P_udk;|)E$W&3? z-059#Cdi1lwqgVicL2lb1guo`o2Ag}VIxVAR(~NJem!=+e@+L#z|03SHnT4|nCMnH zN-NtW!Lv*}I=@E}Ln}_d@B5Ycs>sPqThtrL*RM0?NLjqaI>2N`9SH}V1;mCFXZYl> zpc$7r-9LxmOwIn4aVhCH10cjsDM@8^MMbJ9c1!&m4&oQIyeV4r5I!BD4%O!(F^iwg zc8%iPz(J2m0wJ+$a~KaMm|8{_*dGmg>>GptRe`qr;HIi2f7!p?=21P2V^yJgGonq+ z3Tr0eoFvCUx$Tg-tzW_C_b?}O2hOt4ek)6iP8dM%(f%(L+2MStFt1|5 zZ|vXQb7)DKT8wJMvH%(P=BdOR5=oGO>iPN8H%0E z)2*r^HCR0ek@ln)C#4zY_{wr1YZKdy?L@UB>5wU%DsUm1oShNf%g8j5$2BQ$KBB*D3DDXXmCp zjM-k%koPbfyna7gjv;LDygGRXkzW?}A6S(T$umRE)XXId85}u3^g%tKGeC%}$Bcmd zfM|Th=NV)pu?E`P-r&zYuZb)c?3I1`4gPu*#yZ?m5bL|g#ojlCfwP|77FZb8i99n03%CpOJMgd%ez zaC(mG$EI@KwrBb0JjA6Y$}-N=PJ!ZAQk>5ul`(F#At1-nmCxCxx2W(u%rT>0WBLB`Qj7aA6 zv-p<4kp1aNHOGMehw49DO#9zKDib^T@L6_dSyp4@C0-NRiqk@xgkYo=ftl9sj&6hG zPJ)?c7n&qYq?Uc}=ZQ0rLgs(poCPj1wg6XW+Z@7zUj8~sXATz6;i% z))kD-mdy=u_-Zm?0%96_6H*ihA`c38`L;D&mf|v;Hxv*q(Gb>LwDzbYw&j*KwlVN4 zmPdP;sM;k}%wE1(Rm(D@j%+Re9dBTrA6sic&E0x(y`)K(z*5nn?w!}A>dOS35$;Hx5O@POs|!@{7MX z)6*g=V~yAVHlw-y$(`i9KwP6&PSiW^kzw;ERIEfQ2(hfO^Pyu;&qvd>w~KI$vz8hQ zgd`9hj4v+Qd8<~sqm7KVWA$73b7ssZXO-f+n~Q*w-r5A*dQ3}4E2VQv z9TC}8U0mAR{W)u}!HE10CQ6}n0xQJ4ny+YMLX0?}VodNbwMdZ=(BF60MM%VBt?>aB z)}JJb^wkAm@LMzvF_Z;QahX5M_LkrT2C7|5`M$MGtb0A~!TdiZ zF-ACVDQ_rAkAZDGG%gL05Otc$+Ck8T%%>(&4mMbUZxq#)?Q1bs<{4YTWM5}uLjqFt zO#Wc_n#2p;=>tGFsvJgx1%b&3e;A)+hgjc`)aB9ke$rbWH;&D*{zM*gnTtWH_&j7T z|JZ(7ww_~~^}D`7OL%|DD8h9pfw!N{Ko5RFI{~o+p-w&Oc#iXWU;8C5D9N^??BWf# z!#$G;_N%VYpI*)8_KmS%-KGdD*-mku3DdM=<28(_15di!!p;Ke@k@<6D7p^X1_4H2 zpBy>%u^q2&ji2e6CtvK)t^K%uL1uR1NO@}t>$N;5yH!5H_krx}c(s}) zXe)*5w=jr2l=9L)%eoM-a*JM%I?*^D^JJcvmHl?WXs7iv_Oja(enlnkY^>G$^(18E zPVWzMTc07joRHZ62-~GnU6nA!=&6zQbD(=*F((W&~>PP(sEq>z=# zf5ueSKpV9^`;Em17Oe+64e2bJYtO9?YXKt)=U$(f3c0Jj5_(zPqV}H%xS~N6+7*XR zyyi~R^atO``z^J+-diz#&)OHg{P3aZz3;e8>)U72&=&JmzT`Di^Z--pH-5Pkoc-kP zYXOG%jYlyFf(?cME~YjwpPW82_Z+>w$B-Y6!gx8t#*5RLuy$>EGx}~B`fUb=w?b%I72;IOg z8$q)9*1Cjiagc($f_K?jrOV&ZVv}K5;PXku)VAh97@KEIPp?DdC3^dkODm|}ZlVB< zX6w!7T*Sf|A^x{`HgO$ti^eL5K@>-4nPGS(kR6Fg`|0G@*3ja!P_O7BkUZUawI;6- zve}ukk2CHT&i~BaaMJ_lXf$5Z{|IlDPQHed+@1`XkYKs^sMs%jCD}D(qP1LSvTw4% z+zNSzL;bDMG#nb!mybLl3Sk&N@FqC+Zx2M@zFP23>Y_dfZ}wWEtz{r|8CxP7*6GOf z{={&TR_K}d<<#K zYj5vcWU*WP7S|mZHEOQfX5|pNSd5S};DTA`M1A)9NGOUBL4cd3pA@|m%m7F`w zE0ZJqJjMKSBjL_qeR(@7}jvio%tuJIWk}Dbw(h5*o+495;63Ox$afV&q8E)oQ9WmXQB>ulrN}$A2Zh$R4(d=&d z#kA|Rueo?#y(`;v&VnZO;tP*JMi!wWHDP2W36eJ#qC)T7!;kS6^I&fR|C`C5!ZDZA zmCE>JrRMwegeqenm;Re)4;BonJ1knM+_hn~Gdp^9D|%q4@voaL73^H-o<}Z8vO3pO z+H4tejos#v>c4NshR1e-FCT~@Ba#1B1^5E%b?-Bsw^DebaCorkq=1U(1+=d#jyZL<@OEMI*{$Y-zrA#3Om*$?bkXena9>_}IXB4G{E-K- zdFWNSF$9|<_2u~Z04WhO4B-OSc7ezl_9aW0)5;Oui_puNpFD_#brh$1AZOTv4`V)K zrP5;x(Q9v?)lRn3K{(c{)aU#!=XLxR>aF=Zmy-acb)g6N!{DA9Jc11vcez=URccfnH%-H`A62x|;-o%g@xNB5h$i9v1x|A_j5NE^!t6@`b^my@J)1d|&TgS+ zUAfL-W=xIyLzHKs=9ks4bP`A`KU>iwFt*%Q-$0-QhEB-CQBDS%)U>G(2}B<>lOPpz zC!Rdlw)>Q?Zb$49kE^qn1X6~Pf`A%A24{qvRC@2R_4_lI1-0MpRMrwJ=$ji4?fTS) zJWHfm#Z9bKsO+JrK26ON*`URc#pQP@c_8|i_t*pz%macAwf>V_u{{YZB7d1vVIB|I zi*-4E(*7UFgb#ThAEvJaEwA_KQ*FV~&ppJDlEp!y*kq2o&ShPWI(`o5+ECvP(sTjg*pQHGkoLwWWUSEnG(BTiGe~1Q zA@RcHDLyme&}mP-sF-)0C;y#|>y&5fStwhHxs*;j%o(EJz5bUVPOaCEN05ikbz@AI z2^K^eo~vdVL6iqtg;18J{!oELy?fFy`hCs=c>FZB1#B2#i{g3)+wr$fVg%Uth~$0x zRuQ7}&N(mYO~-%Jd!QQG#}07Gk8xnNfuz$iK*P51qNuuM1S$}0!>g|oG+NyTq94s% zZkrui!(W@`Ao%ll0WbDEhm-Wa;Gy~nXTj9n@7JR{;m#=}7BRi);muRPrHHo!`+IBh zUN!lO>WvTda%~l^*r}W)?*3}b!|-`XQ!MRjeUIP5G@zp3%HM;`$BJ$JjGC3jflDdw z!XQY)8X#H?9~?1vcU23uXi?I$0MabOpd9E|MN1RPr@h%jUfFE(&6{R9%*7CE?W zR}cQojY6P`T$Lo=9fwQ!i0&Uz*R>M7$Xr2DFh0{4L*9 zTo!TIc(*t`SgdTpc`ki{qjRrs2+fLtrp}kJgpAOY=dc|M8{&kh;&3>FeuAJ?A?co) zAp?fGw3bI$fLgFbOG`^&sS8&$cmohe!>kH-fX1yjd4a))T`AGjO|LhC+rsPT(bini z8R-=B1EB&?98%E2KTzCAHS+P;s^B{-ivnlX580;Iiw$gY8>_OOk%_!#(!oSofWSlv zKfZhMUf8{O2dfe--o$kC=ONaTvd4m+j%HxzmRu41H> z!EgEC{cdqII3N<#1gj*R7t}C?$Irmkxi$Ng;zxCYnpO!jkL^TmDE1&La1|fMLG}gf z^2+CkF6JbC=oORVV;=);>egZ4t)8FuOT@veOTSSeA+E%H$Y_l9xJw!6XeZRN(J)YZ z_4JM_mB?o9{4^7&zy6dYG#7_YZleBiSJ&uw){xa`W1``o^EO@LH+08LU~ElUBbH&T zv!iRzZmYk$%YP(9@>PVdTeZt1Pfz(eUEq4A$`_x9FIMo759+Vx4oqJ<&DKfBTlWkC z={CV)d=Y3Oztz%Nd#K`zoGjrE%ER*=yeNJfN_z%WW*A_ott1ilbml9W0uWeA9k|UR ze2fuYRVQVdg&6tBt(h24nZ}|kVS{qdV&06*({s6mbwg5U(&=pJqIay?|YpCwuir3!pSy=gqxt_llOEe_+B%zKqf%?l~iQulb$lh-C=seHQlUif^8CS^)w{TV6vnnUgE zXnzG}D?REKCB^&eslY#MM=Pk^ZQ+Fw6%oZPo@|L;pJwEEJ9Jv$9a?^f`mlypg_5a~ zX8ZVzvF)vU=rN`hfb4`SI*Tc%5>_sJK}XH8c4x<9SR1mHOZadp2H(2E+J5bCy8w=1h0)u1dg&^vQo{ zMqPtq#DF60;?5)K##3MF+AB?q^6c~Xp71L0)uv5vKyc&o;tE<{IAcU)PVDd?3H;3-m014Kj zEP5ALjd2gzU)bf#rA^_lV!?eyCyuf*XVT#tR6VvGqP$1gN)66E-Y7ewRj&0U zaslmC$?XIakvhhm_m);dPSJ2qC2H1YBf2R)9Edq$q0P)rIRMFIDUbmduDZRdb#Uv4#hAA6XF>t=u$bDjH# zlT4bMah#Eg%6brH6?W!2p#LN}1Hh;9Q@VW37>#Cg1{t`xgu% z$RiaPJoA*rA~qMN;-UxU2_MxQ02D47?2oVB64AG&SYwYPzKx=nodU2YK67|~k0I$} zaB8&O!Ta#=|4BmyMew^_O@%^iv2@hv2Tako8c}+QdyLH6c@U2lHqK^5Ll9AP1Tvx5 zVoaWgp}ECe42(_2D52`#OrV69px}hsQP3Q9B+2-DAi@_T*$^a_BJnI=|3W9`DsKIS z-T?sHX4aF)XUa!$>w+huPA}>{w*i?XCayJl{-=CdnNal@HN`^wEWt>|IA+LikgV!y zp^A0f`buLp31Id^y3P|4Vk9UF==0eHkMu7RkI*Yq{$Xbyc>fVxb$}MgEdsl+wpOG@ z6zW5u%LMTDlUFvUfPREzulLw`-P6Qzn@tsuBYaaMjxU9}3q^%q-yT6rgl1`Uh~`6g zIQ0sL+k7TAGY;9s zIY2#?l-OPEhtc{QJT2*?Lkc$1mC3>&*FFW^5$f%bYuvg_DCmy!kb7)>wZBCUB|Gna zlPeVeomTvYagR44c#pnHW`!|J#=PoPyQ9HRE2xhcit#K;S6sL3$2NZjXZ_P&>I^Dq z-&$*}c;hxr26fo3266=x{~5S0mHSf19>H#Me|=(rzN3|r>nBParsKjNkk^4QnCz*( z@5A9u`+yV&5FBA~|DfH|$+Wz=_oZ<{XUaaq%t`9p=i{+B9W#>rfw*N=9iG_z6|brs zowpt^Q*o}}EJL7|;9!_+3cn8gh4Vjt3+@TLJ_qR{tHxZ+=??uwl>miiqY9Q+*l!>= zV}6H+ZD;ZWJEAt>9>|3mypYK8PehSB0JCX79(>aS<*Hx`{==?Jri1e#nmR63I8wGN za&o8JtYhZ?uO<>mc?=;4#-#4~&!&MA7;UGvM165?Via!Kp*?aHmA(df$lPgcfxpdr zBG!#%6aSImC3pXXGm_`+`Rt$57fOqcI<1olcy9qHD$b$0uXhzg`v|m09?pwIT>$FJ zWfegDpW)I}*x>Mtc7F2~;6`^37h~ur@JX4{vtH@CCl+)q7X&=Ar|dpFY(&Hin6-9+ zfw?j}I@s}t5YTr&k%V|Oa4$GgFLI*m-2MjnTC!l<2(1B0buX$ndY&M`wgiya&brF-zvHOHCtQ+FBI z0LkH)mqlmuR8TV}KD~J^sE+Fu)@Um7Oc+_?c)n2l6_Qo4d4 z9VJ;CV2?^jKL&eU0Szi?}-HTR#H@T}9g4tlp58 zeEAaX{drkXl*PKr=`$A}(XrUjO#iN9?^I6QJ>E{I#PxPmqyCO-BaKWY$s{E^gFkM8 z`0UM&>PSVh5&q~KTqot7!V#sEGWjf#cCW|MW~OywT~>*nQgYA9>bz+Cb~GbFE6x*` z{lOwS2YOa549yQteJ`!7`ZY#{>qKOF3g}GCkpu`A*XRw*^^jq@oGitk*bqvJ-Ve3p25<2(|I9 zuP+kW^v4mqGD?dcA^+M@&dDU|A=-b1s~$tx#^AeIU%qawyUxnAJ6bbTK zPAy}5hfeZYx>hqKA~W3oZVyZM7g`G`9>+m-xXmH386DDvFwpAjfP4QVvW9!l9-$?* zg?qv6H?^E39-Y*-O@pDzPT4gKVy>SVfbrqW!73m7zzZJ%HZ;@!e%CI82k}sWwB%wm z7b1mX9mg->NbR$5tJZ}p7u9Rj^FjV=K;v-}f!*fGKS(L^vF`Z1!2PEmNcMfqr>%7i z63gp7SJ8GPt9+Tm91?y5Q>R+%s%FFqdXY3l;s$(bS>N%E%H`SP!`bPKl=}0d9y@P9&Nr?D@3$rg7YBDJ$7zgb0neleLs?dB zhs$m}8`Ij`IaRUc=K^BRLA%IhdDY>lM*n@@^gqH|hZ=vB9f6i2q6aWg`Qv%08#B7N zGRp*Z?EVssS?JBSXMbH4mU~QLFNqU?;6{I~7&w|Gx(Zb`A9uSMPJ%cbx3-`WOg9km zx%Mn_M6-_%QuZ7gTRKNZ7Tw1idJ$jJL|S&+*!eKZsMBneK*pi`D_=nv@@0JRdle~n zv?HbjS?fDF21&Z`SGQ1a=N6=f{{V^Rw?FA%2hH{ z@-oL;V?TcDfMIZ2!XQPe<+k!G!9^wOB=^UXj*2ei49b=t+l+E*?y3J(mK5dStzqAA z-&htrDfW~xFj8K^6u*Zo-r$TtQ>9fJ;uii?h3K!fN!LEzVa-1OqBI%Mil;|+JpbJZ z;C-M4?ahvn9Le^CpnWb&mGAzz$7;3_l>tqrWJmc?sk2yH3%={?FA&f92J|`KKfgro ztgoO{XDHy-rb16g1%|AN(Dg=IGYNrdD7`-)BZ;QMHO%0OMW!CCL)w*Fn--$WwzDu{ z7oJDB#w71#+T#6_4)sk(DLE!d!uxeUBo4DOG1iUiuJTtKO<|o`D0J@QzUZ*{T~B&f zO1NhKd^f4=joPp;;X>vcn6~TwlZwqWb^N#n<^l{_9rM~cn~Y>^3L|=*6c(4X?=(ij z{h3i>M3Edc1z3}ne%(qdIAQSGyQ$8f$L1x_+Q{(b(di)##z9x?zQ!vVI~8`l9n8eB20ZTSBD~d*TrX*j@4Okc6Nq7d=Mm)h5q+DK&p!Ju3vxrj3tG@RsSyg~%)6xKxgs5s zKc$+TGBQ?io!32=+|G`%IF0^O@FD3|*I?;rK)`4K;~oRMLXmQ%{n7{EeRfTN|L-Ie^rJ z`Mbn+UU)|-f9W*nbgb`v*@4TUGsDFh24ft(d&$5S)1mYQd#N#^7Ll+m> za7z_JS)?O9;;%5ui{?4CNp&DFV(Qm|HsKhhe@qM@*m^;e3AN#i9&TyDMN4tBe`u22 zW{BKrJC*J!6FQ@+c!j=K1r+ADeoP{#zBA}PKfqf4q5HWpiLu#M$w0gA|5yN|_YNjn z6-N7UacpYjJ1kN#<vwa#+f9LA zV(rN8p`W)y=?p&lQ%~=ZHaT2fGqgAy+I;S7#^-}3i8lXMX5NM_#JjCRt)dEJc-dmi zp?p1rMtVc=ZAx}%5~2I>p`Sa(*rw3GI!9Ll;bT}5L-f%#8Nmtn>ZxHR8lP#24i~03 z>x*+%G*)SOqa2BbvLWkuF#TFl%|jefuZdJR8jPa`HR9OK_z**WG0eIxxT$4w_q@2+ z+rl4}ZEzxTjfu(AMX88V9}Pwz1XnWb>7(kS?z-F>8ttOW{vL$4 z?GMizY{K~)`%56Db~%>#2c-Z;1VkBh*Rkrnby3Kf=rUN}|9yerO;|>(@DCd-YRHy= z1_Z60oNK{7uGmONWOpC8p}}A&)utQG4=(U^7&wEXoUhuCe8wJ*AdgkrZSLQzf+P%K zFu=kjt8vZTTB`V9%3#LG8n+bn+o5qL6- z?(JP}MMzxgtpY^eoqoM+njo^fao2erR?P^*9sX!qkAr2}ulsxL^G`ia?iPjI@Lfbv z-@DWC`fznPL82#P+xzVL@}mWZbQ|X1A49Ll9%EfcH6qyYjTjfo`?V!lU(wk0fBFAD z4hh&O;T*BmAjyJZ+$2el?Tb%Bqm1qO&im`#g2wd5491MNSEWSzS5UCzRb1A6@9@uQ z)_o7}@`?Y)(^*GFxxH_HNa>L7lI~ED8bUxqLb^d(8YQK>ySqV=2I=krBm|`!3CWR; znfDpb_xGMZUF)3VT6>FGs@Ci-jUXnUTSknqc(24Tu7nPF>>v z=5#ngyBJzfW$TIO2mw|vfgT!Clj~P#^$IcwRVe=5I*`zGN79|T7HXny>mai$W0IM~ zf-Dq`Lu|zblCrx3Vg}ay4wOxyVlPDQb4yK57`sAq&%=yoQ-e29m6xCDQrrJ0V^ zZu=8rJKwQR@N+reP=_Dv?tRTvudSOCG&+gTdv2fp z34oY)7H1lJ+Shh@0CP(9Ialf_BKGUI8b$3e! z?65#Z~$+*!nvDuqXE`@(UrdLhB`;bvf1~H za2xy1`Z6P1WR@J2%wGV>&^*llsd(7!{5ED5v|A&yKk`p+=q-%DNWyVRD1amEE*tOJ zb3sKrue@fS>C*<3j3^B*qKa6$Z;RGc;FlR$qm+bt4rc)J@kS1USVMM-w^@!o1)r-G z1mh-H)+S9Tt)*)F%_#Bu+^FTk`=12d*|Qi1oQ2S}0#b+NG;xnS=-XPv+VFZK1pn@C zC|HP_i;&h^i_n}~I6>$W1MXR}l?DT=1~ow1^FLDWgYw2ggkCl{%U>06`3n4BDQAp& zgc^7V{i*aHW|$i3yF~TcOCOD*%-8Y^gZx2n?}o9R;r2{{vq+qBK`TrAvpo#PsWwM$ zW}diH0MWm4{ZW1B-YZLJ3E^-DV5`3odDYjS)%+lwzMnHTIMDE?ZuI?(+juYw()?23 zgz1Vei5uCbD=qX$k?P1JSD#aF-T^@{W0P5eBbhdme4LTP>hFb^`Q{Cx9dDV?~Png*Gu#{E`(Rr62MX`Q^ywGqB&w6iD`i zT5!p)C6}k@R=4y0l=*#3UT9N%cQ5ukV(2=<_s&dwN7hyBF)EMuJLKQ@gB{eWci-kM z0L+ITI69OYhrdb{(>~*sqljZ0#edZ`bq&<)Xo#E(f1nuo4s|#(mV`o3z{3*f^Z7GB zY>SDn?)p}fA>TPl8N3xbpU~x>+i2i4JQW~nAQDKugK_}iBXtu?6Au^@ie^hv)Mp#< zJ$=(qex8HhSsvOqGnLeH${&^X)2I2h=L(6M#F985M*o{AxS~ovy{?1*uj5UV3zY0h z#_l1Q1+Yh)Ju+hAYuf$JRxh(H`qT*mGx$<+bV=E!)pwG3qVo_5=z0Ek2M7rHHH0?w z_zO8i#H<(e&y80k8zJgJ45ZyGcHJFTJ$6L0acQtQ;%>Gxn)K*4&+Qw`(FIjN{4*8FO+1IfJcK7GLp42g?p3I`Id;b}z z&S6-cm!i>i1&ocAnK-WjzaZL?CFf$Pg$l{qw~T1ysnaoV-6n9g0{Fu=S$v7P7R;S8 zYwx8o5sd^=Zi5{JtcUmh?MUWB#c@^qu3XQZL0i9A?L}2?JYPwy_=k4{?PVN4$4Go% zoxl4l&DqQMU%UE+3GifT0i^a@eshwV1_l;$x8p#3Bo4hes{-++rmDUmQl!ZFvdLx;k5ny3t$7_-{r)ji77& z7TlNB9>tmCd@}YqNp~K%_3emDU-td;y{UrJv`&==7dX$0PC-#zmeP@T!T$nifpPD( zMyAUwVZPlg0jyMeoK2fx2Nbr6~*BVtlLfse216TrTF zc!kfrl~Vx=hO1Ej0N05nbPuZ7&?n9bhx>DlzH^hakXkdJUw?nZZ#hcPBZ4oPV@HFu z`874P1s0>=w!2=%BbK{pCn>x%7Ju}Xgb4&1cgs=9C(gGC5H^`Ah157vfyf~`%o%KtEplYrm_#z^YYc!o*;Dh~{TIy4Xx`IYUz z8gZT=G8Fubl7;~O$Xt4cw={!MnUmn zWD-98Xu~J5qP=cb)=14e{2q(s&()SApbLV+@t@GT=)FeBcl%U+5-qlTNhDl2zyT7qH!=-)3=34PW z2S2e&n%;Nuju$YQ0EcG}421W*%l!0j4S>uIfL(ooa{v!Gg(=n+fZzPMhn29ug5g1f*P* z$~37sNK~)9wI*&yzz)H)8o@5}`gHz#O z+!;M`T)z;sz4y5U@D_F50}1mRo`FLNfyBSFVBB!>ja^-YWwLb%zb&Vi-Pu`RU zF~3Ru!?vaOF*t_NX|q2p{tptK=sx{OUgz9^PlvRkeP1xzNxGrax!9GuLqDg-X_7T% ziQ8|D?xx$cG+hawUONB{aH0DSpv|8_7V!C|lO|$w|4!2$EWAMB~ zAc<8r$@ssKpbi0FR13)k*M9+iH1SeR*<^31(qaBFqbD8x)w$?+WdtK`V4zPm`TWC4 zPC4|$FG_RW(s$wF>Yepf*)&j<(18v>dw9PMEgL*FARxj}_#B9wk{6Lm#LN<{n3`CG z#&cz}cHo`LtWye^>ENEv?pQY2aaK3QxZvYUi*Ga@3kuI;0c5u&rdJ%{Yv>POx2%YX*kTZq5spKD26cruuZM6+U zA#?!``_8N?nH${z0QM%V--bslA*jj-BSC0 zfmEONtV9xG73;DQ^rZcKd*uGk&~nXD<&$*kbyt!fa|4WAObn?oZl&()KPd(qIg%kr zov$~&^Iy+#{K{+y8T8CS)pwDjfeS=Vz6hD*VQghELzs(Us!pK}irUzoflN(a_ePtkE68*9 z!XF$op8vX9CtyQET3TJ%?>?q-YVG4C z_5}nRzTgr}jAn}^Tz6876nI%(`Dc~nJKRB-Wj+o9*Aaf?aFS`A? z$C2K8c-j;H*a|@4egfQ2;9FNJ&)N^Cq7@i?=Tlp6hIBi{jUXtFzOuj}|B_K7^xlx39K^z*DnQZG93P_xaEBx{LzGSZLUGP!E?k!S!2FvnY~uz$(yCLahJv8#C8+1t%YN976ZQZynX=%fa+_4lC`6M+Pxuh!2a1YkU$_!uzjpnDxnHrb zo>xKxU(E*Y!0$^gISO}tD?Tg>Rb(KkE8N13V0Q{w&-&&)!|AwKc$_+)xnRF|KB#Oz zd!}wK1FLug#>vnXHP49OLf@Nb6U0T)Io9vTCVmoEkdL0IeAh&YWS?%LeJ z@xPE&5t^qIYg&^0z@Et50P)m#q@C^^Nv!Ssu|o{&v;k^TocPk;d07txAm?+YHIoa~ z)4$`*Vwy{Utat_RgKv)n#&Rw>4i@MfydGE=GM5q6${=TYn&IDGTRRm;&fni+$8xP- z>J1p4ZlVJ2d{upldu-mNp9bEcm64RMLj-}%w#$TsUraZXd46^2dXy`>pdb70FfRWt z*kT{}ZB6C@EWi`M?tg{dab8gKW!m8( zHJh488~FnO*E`W=qiAD7d%IV=AxU{O`X>#Iz@cLN?@&3A$6G|Snm_9B{I}CaA~0*9 zJN`YwBxW`QAN`zf{!7Hun+d0cJehu^G3yAUa6-C>epj{92!ljoQPbo(x&livTafPA zc@O-)=aD~}nqo{SRG-Lp ziLo4suVmWv3%kH`wI+k?V$hB#&byuMpv~eQ@LQg@?q+=}v%;{HHanOtJNaCZ4~;=_ zUy8odY9?7iT^EJ@VBI`4PVeM$_(VWZyFt@Q!aJm35QHuFnFx_Y*yB+tp`|OV zTc<;Xy-YOa6S4d!oF8p{^yP=&H)!Fp+|fd2HQ$m`6P6TbJFFiLcgd!zO!S~E;x~LR zcHT1#TgFbB0D%kZiH(O zJTS)g>5KQO`EgBIH!unzAO1rW2qC+DT)jBbd%IfQUL4_8ETrwd$zOq_ zzOKHt_1jS^z;!IAon?r7!~ptf*7joH<3J?V;88^rH2(dqMx$C$dmKmNJ zXLn-!crSTryFuga()8l;4p4Tz|L02%V~{eo(Pd-xbzv1c);)f90Wejb=QNSD7EL9y49 zglLjXjYi4IW1|I1aZM?AlUP(FDAUxs_K%gn zqS9eqX>pU^{R|ixS9R$ zepDh6M(iKnevZE1*P?#Kng8!$T@4Q82JMroQsn(BeSs|`%BRU}oxew2BnySU=KWDL zlIymg^cl(c zn0R~0h^GFlOcn5)fOOjvAbs;nEGx4VMC3C3{w<3CXf4tHR0ukIooz>I+|^gnk$|o# zCf+jXb^-B{rm~)1xv253&XuWg%NI>`DXm$sXA=syW0O^q6^apjm$rm&s@snyEUv*L z-NghifNd)_5-9<2X?}oN;;%MVdk|c)UsRJ{Rv#Dtf;!?N-3fbPfa|yFA71C#93oeA zsS9V;y<`2BEp#U3XV&FA8~T~d z^z^Wd$$E)NY{ts|OvxiAt~N)29_b_vq>E z%na}#g4Sd=LMlfWAe5<3mmOR33fSa9BO4Li@0k57kl7ht@ZJ#se<3quTIrXAg3h}r z-bE9#S>eH9s}kZLv?Mv9?h3vpyMIbQW{S`Nc+727)I)&^q&gy+u;Lr~U3?MkH@79w zI*h=*y6#=$TG{Rh_e|fuoc0Q1OYsj9QX0_`wwQJpcH?(Oym>yS4Zk%x#eLuXBJ-qb zbO9(O)2gTogb+sB`0X+Yi^`Iw4-~NR+SnI>rkWca$qOT3NcWr=JRI^+!K2PvNi|0Z z)PFd<8$o<~6i3?+eLcT$*CD{L_a|?lm3j1`gGTF|a)GD!*|XY@TjbO66fv$oPg2~Q zOjR9c2gPn7!`r`a>0;}WqsUo@$>?^nOvQc_=O$P5y*M(>V%yCO@OY#H6lwnag6384 z*~EOy@!TAjOBGWd4%|QNeZQnkk79^(Sbk}H@o$xv7%26uHEc=fnZ}V=(&HzYGl&;MlYlE^ zHRSK!)HO0!G(~8uOMklnT8#z)>UAfY*X*mAzrM#DWklt{JmsH@g$!`8bg9`uMI z1{FD^v65Kl#W{ZE7j>s$bSb8I%r;i*j>R3OLlDS%c%K@7Q|cTs9B{K;aW;nra6dvs zWeUnpV`l|~=}zN0Fei!DMO-RNZ;r#&(8HNNck4YioaIgT*gB3)~jKx7CrNCb@`k z#HLA}M~TYa-g+QpmnT3f&_#@g2$U`l#Vz>Ixa%&z**}pKJ>xDM8+S|Y=fz>zRLY6Z z?Z;WMd8_U>sdTqg#!sjIy9JfhUO!%j#Y4J{$ZJGEI^zFo6(3BX=a^Pr`PKmN(4?m5 z3xhh)p&ET{QumLeJnN*U=Fb4cDz~u10^c=Y4)I2!K#CuG7ixw1n}o+dV1|v;YN})6 zt-mcHh=K#Z?eUe+EohZ|R<@|W2Ft?~0ao{qz-L8rxn}K9C0HTp9y=0!K^Z*>xKl=O zDFg;H9s#G#l>%wW4Js9vl>!5*X`Qw}s)AKoavF5AK-XBgpHsz9C{%1bd8_G=?Dyh( zgX38I0-#tu$pX>{O7cqe*1`h|$g_cZ_Eflyj8eAXb9b_h{*qa>hu*m9!EI~^#_Zm~-$LK}bthsuE??oE0LnMKogn{es)AlO3i5cd)T>@Xwmo^_7rPI=q#MSx{@ z;9V#;T)(bi(K@HCBwiE(G^4I2s+c1Oa(@etF4)D~WADDXY|8Zfg#+;Eb0RnwIsyFe z$-rwhQeSTt-peBIEvcm~*ZczyH-l6Pzi`NRLD6R)6(!83F^j2QDeS)ql|84a6cGIN zpHQ+;3!zN;>|$vB?^A3-L#jTKyYG6|@Bmk@S|Ux39#7q-2U6q6t}}WbhE6l(GjYQ@ zw%kr8Qkmy|OVj8j(?&lbPT7dYOiQiiz)yX05bePd^zsXt72P$L+uWWF_+G!(Wme)m zw7OV&QxM>tdjhh~JF(~gGI{_R!60vBVtoGXMVkbNdBzc6}pA=|GivG1HjN^^WS{4D13{y1T2x<{XS5XB49!oQG*CO@ObI(D(aXj8i>h{ z!EaAWb`ApW$g9nr09x0#IViPOy5E7RtP8j?-Id?>~N$S6Y) zG+GFODQeeb0B09agk0bu+b)pr^ib&MXB1S>NN7#kUcT@g64Z#5il7T_B#zVO{7?;j z{W37HYR|y@#~mllo4bFsR~Dh7gvU-T#gR( zHIeX}%;ND(g}Wwc4|oYV0E$+jRWzUsm->M*T?1qsz7)3q$rX`oa;#GGw4Aq(_oVi^ z`N6x^|F!ufHy+YCJNpx_)Isd?=P=bx*U!L1b_fB+LHU`gZ}Wd=&>-FPdJ&d5i;9~~ z^ouPXvFoy;ud8+!9dY~)#d^cz@d0yw?J$kuO{T$llwmpmQgZ#a(g5XUqJu<=2XQ)h zmmfO;=}a(=Ze0kF_0}45bBmyPca5z=AB1%J^+CdWDI+Ce>6Ezcs zjREXM9~w+fM4lQsGRZ6Yr-!8^<}Oq8@a9qLj=}bE7wODxo?m?-Pd)`vY*K?$iJw+3 zYd_oXO?On6C)V&FCe(w3g5s1Vy?4+iiwzI@l!Sq22CB`O6a}IsW8` z*)oSU%drGe@%M9V*CBKdy$AvzZ1kN6d!_2G@v383)O?VdGd9UonW@}VXZY;^Q~Kaq-SW&%Qn5HCxB}Cl3s2J@(_suN2JtoYE0&*?Xej)%*Mg$i_vomJ(VMKxCw`H z@8)2c91FeY^q!ev%RBSCSy9F5@9aZ~IVw4~z3I*==o53u*^=-dr8G)`0t4Uaar#ll zE`i%APb7V!ETTmsil+UsfODCK=AGltKV-L&Q{ENEyj^wq^VqsOVh~lDaMC3K_zX>v zY=7+*x-q?I9}Eqoaa$(%)obSeN<0?iG}EP@IS$Ql|J4?CPmyu<>*0|{Rh%nuqPDc> zY~K+KP+=fVKJA1+q18U3kP12ODVj7<*@wlZ9esaW0EhQfywQMG4h?Q)z+JG#6#19y z9!Pq;5hHhQ-9!&iIRs>O-YvlTD^gqv3h4d(MVQ*4+NggL(%{@Mw1FcspYXK_<3F72lqKg)n6eT-R(AdzJ^>j)~Uasv>PX6ErrR zJ#iaaRwwm&-#o<^#}|9Eu9R=S?-?flv`y2IYXdjOUx9jRHi#hm)}0xI!>yyG>DOvxR$|6nceR!$ z4Iv!>Lhoj~aSGUWuB+;nPs1pXcNu%HfpLL}3jo|LUZ1ai5genPGqiip2)F*MbvHTO z;x&eW@yq&NYbau5%X`%R;b!V4s9h${9Gb=-nzaSQT@HWP3NwnMd48@llVPKf&3G~Vxh&log zI#)s)NPI1V>AH}9edRxx`i@vw_M1vnNMiZUMcCoYa7+!Ml*NtfvD9A3-o2*MwC-S3 zt4TD=kod)(n&qkIAu#~%zC5E0`HrzlC|Vj#CCRdnJOh*!4(lV#kd^;K$GpT?Kn`?s z`DXMV5=a;s=OoF6R3RM+Zk_b#;=y;P$sD5I0&EO##xzJl^iuXiFOh9KLRFzwLt-85 zR{(5aBtH1|?_P#H{;ejAkxxn*={uWKpD9+1I5gO4)*=WAi~TsfH{t#h(1;NBkOz)A zvkbM3kom`?bIc#tai!J*bG}o^rcfY|mbgHtITl}kgGa)biu;E7)4={TC@6th{vY}w624ze0gZ=(>jzM)yY^qBf- z|L1b4V#&0NkA1X>o}G~3`^H+N&DI<70|QuSNqpUXf#`sYjH)2NCKvKqh%;;Fe2eB( zX9{iC`P0NLfL!pF=x4tRmML?Ff27Yarr+pqfD)5<511!+$&Pve zEBxbSzUbH>zd)^JwT>&nx7K8Kq1*NMOXoL`DjllWh^cL`H)GC+cXaFecZ~vn6EdVl zG=I&vG!)XV65i;p&=(eCzhaGas{7VU*(LOUau-}w4!C<2dd^qke+A-BFvw=5n_}@c z=}oQT81o+}(ITMFT?N)`2WT%tee#giC7C(*BBiE`8v*`ZV!>Cd{&p3nAVaJ`ai~uA z+x*vc=}(ywI!95C>NfEL9yBKEKAi$ZVip3#{~(BsqJAi=nTEU2aDNBHk-GqYh;UMF zcP8(a&^=b_=6^XllX-#f&$i0Z{$Au)EgdM+Z`yQ@$H zJH~m6CKBWrEVc@`KokRfdDlS?mh*58&)tZec_AWJdsGbl;4;W8LxFZH=qA%9Td5;Irp5wb^_yG=;h`QaIX2zXt%XR4GJRA4 zDtIKs0qHq1E8_G0i2iSnZh$D?4NAFk|C>oAfGAH|n8a8csBg9H1_+%HTm5xRRcU=t z?}=+@rw3JhO#x8r7y@+K6+~|NX>N+T3A*FchrbgkLiHH4Y&@^UoWCj-G;J~~k*lKi zZcLrP+dl01giQiEi~P#U#L<&N)!@25LEIm8ldo9??X`~=L-gkr{81u zUn~r3eK#t7qLxQ7u7%9)%hD7mn`gft_5h~0+p#D>rFI(?{q>x?lOa(UneAMI?=NoT zsetAu+-3E*hAL3(1BVMq9Q&zT#%YZE((9Wsu*s9cc+WYKLmc-AQOj~K)D1KpB2x&V zzb&$g?~e`>x2%V~We75;yZQLVR@mK?_5N6L_#u)Lr}qn48}02t3$`bbbU;b^$RzeU zUaDN`TV=bn{41TEL8)$}(L@y(ljfnpb3UoxsA87tcdX_aWRhnfSg4gia`fA6Lhd*C`|q0ynO)ywN#` z27}WLG3~BhP$+uq9wHNB0py5Y>0JZg%_t23EP{c_F$0W<_1G@k-5Eeu>fWM36?2}K zyk82uXGwVN9|H2dtrTOrQ=>3XE_UMe4htw`3s*7fLY(g|4&pyGZg))B7Uca5Xrih$ z88-zcO`+kyD^+x&9x;&`0(#h3?(=%~5Z+sG)?tA6JTDE~>kRl%##_>zomew($e+P_ zv?-s*zojXp{TQtQpJBf!EkkKf8HU|3F*yJPg%e4dks*<}|0?rEffIIw!{`>T zr5p=9i6)65iFx2WWQXlR3;dU@>X1n9FSROcFR6Q$-VvuM!)lah63=(yAZmh0RO(Vm^eerSyR8M@6Bs41Fr`H81Qq~@7j%^FAQ~C7FFQ(2=QM*I72DFb zKk*tEAg=(b_Em&XMmOKor4J1T^C9N7nvs{B4xJQKn@#S=D?`_T|DAZ^uT;dGpNtQ= z|EDHG1eqfMlzZ3VR8ISnu=51ZL1cpYh+-p;XxD`EjlPi8=_1)xg#H;Yi=)-jm zR+e7TpprAC?N@$3HX??ZW3^@cJ&-4@W_nxFoVkcn$J4q0ZZ8{Lw!O}l=|1}khX}$< zVTv_N#$6)j6a#yu7Z7O3nv+lyH_Ho(Ym3ATTfVs2 zO!#sl;y5nQk^2Jkr6rfyOcIWcMt^gT$zytCZ;A{6pp8XNK>`87E5eJqW1(p%;L>aZ zcICeiR+>j-)?ncOW;~}x8O-+WHPfcpsqbqtw3Jj84rR_;0^R7RWhiRR1RyY!r2CiA zhq!x$xi#Se%_iP6mJZYS*|%dADl0myI88N)c|U}A`IDR&6|kw603YD1sSgK|DG}tM zPpkSg{^o|+#QmDwWN$odg%n*?!L8cncs-sq)cr0^QGNfZz(A*syWqQgV4l^{hINI$ z*GK0C)w%d&rKjP_KF$>0-Scjth%*z>Viqgoi}TKpP@81qXTyrMBRW+lwD+9SBWo zKr*$DdcTp^W|)bHoRrlbl2ej=bP(6Adb&J$GLI6j3;uv`v~Jbc!4OZgQNaU?zNaw- zkU%ij1F%?|;%osR)rX$mT*mElob-iNhv?$!A);5x{shghDE*;bpZc4vBOm0^CKeA10;ulLT==Nrtxkq z;^WYA($ZV<;ryy7P$y~6fDb`>y6C{;;2gtYa+VumAaAU!P&|{T7rH5VV^qNI+j-c^ z%`mij-cxw6Lgi5CHv!#Td_P?OKqFg!JB`M~id$^m`frHIm~v|qgC(Qakl)_i=PAFF z4<;|9Nlk=eFwgfDGDGD_E%(3;4A}IEBw_fQwiX)Q<-mSAr- zi+JNSz(rSCEUJR$qYCX<`7`E)}jfA@>)t$_Mm!FF6Tb@mCc1w}UyHfeqf75=H3SF3xCk z21mQUf#vcalXytcK;~tMw#2oBxY2aw3495gH0hp49-AWTjZ>1#7L)v6j0n#FQSKEd zBjxV=@#$yqKZ)5@OV4rld7Sf29v)kA-)ji>4P#ll*WB1{c#FUn>^sw@s=G91x*77> zJm&6x4#|T9{Zoj`SVo@ZO2$~%AVS_zt6;U`2w*8L2@2tRUs8Z-)v3(b+FWd`isokt z)3Ml{{!GdXRpE$|uX$&V8h#k{8FV};vWO!dCSHyGBs_zsRu{WE(2i`nAy>RyOv)`=>~O z8{d^pi(o^qzD?dPkm$S0NmM}l#1d>NWhuSmY$QpMe+C9W4@I_rHGD^ zMN2dGV?Msxk8a|lXEzCZP!}mec|{hfI{oJflXyC3AtU~OIacJz^RIeIL0T!bIvoFA z)no#*aY4b-g|MVNvPFEGnPRZEP>xgk=Vd@^)!D0XO8_l1N_7ElG}$1{rWHUmjfjyn zvJyB2WcnVOvOA48s-?vWNw97bwM>0v!_P+ zcJ#H~({Rl72-u+XA}|TtYX$1xNjkaF6MSOnxd;>kfn%z6n5NOm;F- z_%?CMI)ZeqGKX#PvA(JuK~43aze;~BU& z4ZJRMNZ=kNYE^E8TFDv<-m)OLNfA2X%xJ&b9d1e21l?S{N3s#C)DY{N2WI6j?F-7i z>>$NWjf09QPT72roVw#tFn8R0vFwRbutn@tG2!q->|+vAB3dFQG0&U3Rcds7{4D&% zgzfz#w3igPiv+$hvfvQY+^L5>V|pKIDU6dgGG!as?fZ^UhtHPiX3fu$;wyGc_WIf#bPvFHg-zaZjkpM64V zsOSUI;`}dwbTznFp*>8!*Vrh-xlfCD^ zj2hv3X2c8_fv!bB0&EJaH&Z0z1CLSb^do@SyzJo9F?4KBV?jksqY9S2CWMqpy)P9H#KWAk3-vCe879Pwt7D}O7awXd5ZT~eN|p33xycf7EnYu%1#Vm) zD{>e$eB?Y#Ti8rmA3-y0L6~K_ZGR0^x2zpnPvswzSeQwZ&#S>K>^E1OI%Q~M(i`k> z|CHHAcG&d~ZifP>ckRXgJw79fDQX(!g6M{$K_WFSCgt$~0$ZTPw3sQDgPnNXdEZsH zYKSHGETzCQc4Z=1KoCfyCRAf_`O-LOZ#q!JFY*_PZ3*6+0?E~1?A=+>KiEZd^6=(~ zyh_@OZPJWWjPBO4dDd4D8t5*dokv7=o;!&_a0BL?=3ZQYu_Xo>{4-77`tBxO)Vv1o za<*g%kSdyHz7SIK7zoo@!kfA<|CcWw0DoNlH_DGCpCJ-RIFoiN@8UB2Ho<$+z7@^6 zLI*pGNQ-}X0yZ!AauxQiZ7=jXuRDi-kv)@j-B4qa)!Q;lwdoSU%3(S7IS}%BVmvyj z0s}<5qklL6v+}lfcN#$WUGIuVF@ADUb(FkKwVQJNX{-f8w@M(&xRXuGrzP?+;J=pW z!9~vR;{WGtxD!@hl&ZNeS~LG!X3$F@&xBUQU-l0%D!zLn=V4^Y*+s6>&v$)~uvnOY zhVaL~M+zakhiWi{cM{HOs2l+RnyQt)$ij)kkNdgcfNt5u-|_v$XrHjCIA|zIOpx7~ zxR`HJXm9?2otTKi8R&MITTCAKcorrz`3HWXd2rR`V98Qw#rxQ4Z_@1UR1O0a!=Jxi zvZsJ=HbP*=yzhlmg$HXjA-vfxR^#Pwgi6PB&E=|KM7sXXvl@Db?fLd$+_R>a;U$vz zFLwymZAh{T${aCDPh7lE9ck^z7I^`d zqj+yRe>kQq?s%b1n__2#$@FZVOaMUc-H9dnp;g}VS>d` zx>i68Pmfm$i*p#sn>*gM%@;}v=87d6db>ZqaEe~8+CJ|p-jc1j(>5BmGYhq<7qjp? z5e9FD5lG(6?HG!9xVv*9rr1zzOy z%-y^YWhZewc+?YcREAJF<~AFb|K%*sb`x}B2mC7uk;z$K=nqIUtGGCV;sJ>5BJlI{ z_TLQauDZ#Pw&?WT7>z>Pnam`d2*;;8LD;N>f%q@4GY*)|EIfQvHHJ$Us7=i+MQ{XChwPcH!Nsr*9>LzKs7l1{qRw!kQVzb#8 z8$=*UD9#KLte={RwHS^)5B49$VZkD((?G7n0EN*-W2Z6US*ZV$C)!s-?!l+7%H_pbz~?`o&(0J>r|y3thb6s)jMae1EqOdf z7RUI7vnIE}OdAcpqpK~k`oSqWo2uUwA+$vXuWcgEhx_0Sa2QeWu%>FXD4zTR z;S2jmMjnE&=9Byw8sL#!xJ7VO zkRgbm$owC^A_$8qQMJm{9Q4)pU-_s-6ZCrjmh}Zk1y_}yK3XSR!>Rl?DEjU(pp*Jd z!iX{RRFbe}*{TpFh?op*2tfexyc#@Uz!4LP2Wa*^At9LdsLrUmYb18Pn0LLOHB8v( z2)_PME#!3EO8Vx|1(m`sC*(dCij2VhE5+Z}6>56F^y{H3EEliu*;8yTE%4TX);U|U?;$GnmOFhZFRaGaXTT@x`Kg!JOaCR4<-5ehnXUhDLRS(0^^9iDEVzi*~%UiZ3Tgvo|xUgUr<5Uke?)}y%TBd?jM(F?0h9{0?2 z=39T4FKYzhvFUt_G@VYm;KBAYH?V2}@5WJ$=6TxK7X*b&lF-CWTf&5ZHlI;* z9CTO3q!ARp)4R&o^o?_v-o z4vHGDMNSNrhfjssd{Xcz8HIp-zUE8s?+^zfOsm|$CL7xyV;&>qEqYVQ+|x|LSBqnf zfDZfhHHBc$5Vv{dhW_DGdB_q;r6S825+YJK zh=347_ZB|I$IBih3;E~q+IJo?d$>6Yym7D$YR=)gJ^{2AhDYR6> zF5JoFAO$bom25k+6T)jiHAB(;tI?>8u>A|?Q8CT)_o)?PzRpv-^q>UGF%4gh6Cd4# z3@k&EjQVUb0@O_+)oN zx60HzuXe~;p7)%E@gmX$CR}+tSS0_H8K!xTULdfG;H!Z1t)UZOeB}Z^r-N_8V-ou7 zvYZKA`IV|@ogfOXqqQ1F{r$Y84)hUED}LIAVHwKPF!Sz9>+Bd%v%nL?p|5$#Dt#G9 zr~LrYT^qvN#5=e_vpKs#TAnf@csr}3r;cPiNBFP&KnzSiw*@jJbc(UzqE5X`>f&jr zl}}FyHwBrA!I_@@H+r2$LF}Js%^FB%{#Bv`di5YFv${0vxY78y(E6ezmsCe|ozEYm zl+Ot&3%s_XU>$;lFam4Bugs|##<>I@sb2OqZRPiatdg4|@5P)jV%%(i^ux7zcMw20 z;Ce?Nz*oiG`Io23%z)FO)8PTX{ zBFv-x!^0EtY%va|8&`@CH13LH;QeG|qKVocpWP#jO^A(^u>XTsfD-Dt&k%i8-7@v~ z>R(_Z`x&vWnh$vBtV2opD!}0yW<|LcW7-;;Q9Xj)KkM`{o7OdPsLr*dF4V4^>-^&I zk(JlBI3Abk#RMd59J>Ybs!i${QxLh2NKIl1rG6R@oDa(% z;|q1y2qRJ+D(I076#ZhS78An9`%pwj=_Ow$&z~mGF71_drtg2b@^y2dDKaL^38%?z zRQOBw6e6$eqXW2ENDYWb3d-PKG@3#KQ!xWlX*&zqD zwx)K=jAcxnOD?o!%-s8CTX>>3*4x%a>`{0#Y{Kj+RsKqbPK^hIraIzdpN`5d>}SZ+ zrKj9dyn^T+p$N_GmQ+}ETK6qk1ZV*p{>&uoG^V6j@NN0mT;wwV4K?p-pxL2@M8L6y zbevQF4Bj}(a!9t}{4^m=ZKeiN)k8&tk)~G~N>hlj&D977QzAn**z4JvVXz4XAMM0?R@>V0&=_&=f8z1f*Hb z2i%?warP(p)^8~3e>A|V$VPy_oGS7>pfI_eRYf~_a=uM4bAzW`dKCbA8Hk4^NC5<9 zV4)*bORs*5Szuybn)a`~4HCk5g$+vl`K+D+)0tpVP5!C`QKg#6FCt=;JA~KF&ObCE zzZ-h>JUYY!qkNivGu5UB$*eeTn<|T~(u!bsv23#bQWO&uWSD~0ytTWD-?c$1yuqqW zo~qVrmr4|xf?2SuTgn+m;4KBdev|}a$!OOBVaTtHAZR#N3**5-XwxQ=m;^##;^|*v zR_-*bI;SJEQkW%TbzY+(dHjA{v6HCUEv0|sNq&>}rRIC(=KhRbd+O$FvIuCah^==@~o*&Dnz$sY?Os(jt1wMD@UCuI_G|CIrFM^CwVwAccc6mEO z@I*()BQ9s^s7P-tX@L=`Lv) zNMR+C5nfL>%b3LMguFnCR@u(lNvgLX zGaEXJ4(&Fxs5N99XxDcB_$^+>GwOH;P{mv@zZ!3_-M&lsG!9v7wv<6^GD0Bd-+IW>2+YzFrb>eAxJCZU87)Tiqm$|_cWMl7wlm2t6kKM!cS z3pv0OjH;VF+@kX^#eHUy+hf`D+>?v=SXEnB=#tnZp@NfqpFR57EF4zZ9Mz_q`?YDE z)ie02dooxR0Rz5)1K>JpV~ zB$n8#3(G6iHoV?gWL*4zpN%*a3CSyXOh@&f5Grfh^9Yi; z-M2w-GnT+WJ2U7&V@l=@#3OrDCZd|@{x>Q-r;KU4G~5u2WDokUA|MIbo%5GD2B(VC zUlfrLLBeihrAZOIGblr>`xJK^%zWcpH28$0$_+x1RAby}HJ#RosfT!BD7&Ye-!@w_ z#apkieE%d!9*hCKfzw(u+ET@2=Y82T&l$8~TL}HFI)mKfq7X^C0seJun}D%{Z<7UWS-}H)fSY77fwQ8rOij<)O5iGH(kT z-H&R|R0{rP3g_@=&x$N%HP5;$bL>#sb~e8n;K(IHew5O7o@~d31#lJh9ODZv~NQCV3pC=V`@w$4-r#rkB(BXRw{pC4vu|N zn;a6{$r&#uQu$rvuo4^ripYk2&dN;A$Tu$@JskBr>Y@6MQkc_1;Dr^{LXoho4=GUt zeuF(Pxu`8O3*w|__rWmKax7)NF%*<=`^<7Fnx_xhm1d3|Yia&}L{tkD7lEM*kM)1` z1kNPs7b{#4<@#wg*6(jE^$9d>S*?BnAnBny*x9ha%^w~8CW4SWw<6igFkNsNk-B{k zIj-1X`QL4Fqk(Ib1-A7Fld`e4R=ZfkH4!d5XZ-)!JSSZ9s0LK&GA(D{at($xxIEi z1hlQ0&@;F=TC(fLW0+NAJZ~}3VllEsv_&6Lu($Sg_VFmSa)fYET_+)7PhF~ zD{ih^1?Ya{A+e%QvtdZY_wEiMJgPX|!?r6N8)lFM?DUb%g zBLVL0YmRw$n$BFqg}zQ>GSq(l6Ug04BDhV)S2G;2QmrkC%QsQrom*Qr`pXfgz3kD# zLqkR(S2@~W9PBPLJ0qWc+rYuF0PGiwZIvd|E1eZQXBtl5%%4i~D884s=T%)xLh{(e z6XZ2KO{=`v@%bwB?Mhqy6oZGyEI%9h>k;A*FcW-_VcNJ8s)(d>FyhUF#QHMTJzV#d z#FRRnt|Y!I23O|g-c^hrsOA0u2W(G7vnEq1tsBXd@ABO8FmCTU;Vfv>MmwGt65js* z4Irp5Nyvnb`oAv;P|0{9%Ur+Sd||v@lBm98ju>0orE#13_BNh%mqh*E4GZm&A#$sfXSf}atR89oQP#^DkZkEUFN}6WV>&8 znd!#k_j&4;P-w$)jCY^Q?1d9!;b)HZAiG$}fr}7KLZ?eTs{~ab3>T}Ob;dsss;rc! zV}{EOqcUSaZED>_iDS@WV{xxA#*h%|BzCpkrFYOiMYAEto@cWWl)9Sg!g`hdSbHDZ z`3I0*mPnGBE!44dgRt#GDslc-NEC+pq)2t!ahLvcg)#9vUE)Hv(l4Z)6F3K?>W!HV{y&HqgC{k-SIL3Mz%Dr0qjd)I!=jdM zit$9~VuvpjDQG=u;eoyw`=V|aYf8DzX_YxTpIO9JIARkPdyJB)c*2=XW<1ipkSV(9 z&>ep;&oAz{FXcHZ*rJywm!%P0IB#@=u7j3Dn}Ni8xBH4EHf&~Os~H@|Pj&5cZA(<5 zyt(+ISlij(=4g8#v)GqVQ@rNP*2i!JCC8UuOI$TH3_w}^$y-0aYTta+i_ms>@*;8e zWx?N<^3W%lKS#+@@9i|=yh=L8y*>V+ZM$Mlcf?-E`qL%d_bdE}Kfg%F!-oE$Ww;Dk zM(d*LHEH;X>Y4I`rPoJ|x4$Um&^Fd2eyVWx6RO6#b#g&EjqHDqzcuU;l_>iW|ER*l z!^S@Kz~fa6BJ7gWTKt_F+$%wSBbzi}L z01Rxr>ZU~H%j!9De53v0e)`9OZJ9SEU*8~E!=K|Fnj4`C7dZ$_2FNk5&H`%#w?sg*v6pP*?Rmsp;LIyZjIv=>8}auPA6xGE=pj3mQmR(w<3Fa ztW%6D=NQfXn3KwPeZb{g6l6RZb}EpZvZXWh`7~K%@TFU(jR;Xv;d;>xMfvW721m(t zu+`hRy1Ht9LR3VLbuT6Pbuf9TlZZz2>SgyI(_GJRMs?;llf z%bfCGj+8UGp;c)s-e^<15C*uhz-Ql9T;{ya-jr*jcM!QM?z4>!!(S?kox&#%)j0Wr zU_KN!#`tPGl*Dd0%iqbN@gt?4e;moCdwKGJ047ho^yo_7ZLwNz93YrTj`<@TR<4Kh zME&lkGx-9$^g_Nu)J;R$kscW(hy{r%tiVDTSNU!GEZL5zb*&HAv|X^! zpPATGdbGp5!njI@nfX|-Sq0<6*oz0Wh01t>dng1hH4tx1qCfUWAwg}TJIeY1V;XsQ zN$h9ea;9_MCxaMsDEGG?4A5%4fsy$IKWc((2QkJXx1I{lYQ<5Sju(vb?yp6Tb26sFUkY=E ziM}@n$xLJ0URy|K_7zVvO%Z47Z?C0&L~Fp?VZC`0#YbEJtOR zII~D$XYd|_)Y=;=;SzCzShp*vb3gL|I6U2fclIDHC-diby0s2>OEbzn&~CbbD!GU) z^ZT^+u#;FiLf82&dklB0U@_+t>3cqwAs6d@%4LI3=-Op8#FevLZFN=u2JzlvzK_j0 zqK_E?D>~vg%<{DJICuPfkCR}hCKch(%Mh!s3Z8;$mXdKlrYx=E5${dG#izttlv3{o zIRcR|2YEe1#toz;m@=0+5CuPFK@3+-waV^)l|EFcRIX9KLF>QeOATp&4H~5JSx__y zd`wxEn!GU!EMKfMS@&27)H5FTK+|YaX7GD(N^!dPDfCER35>P}?}GH!!Cje5c{m^4 z4M{4)W1ob?OrZwu1WX7{2M&@Gs(F9iNXLfV!H!;TFa&ye%t*`cd0%9;GTtOR)+7DG zZ!CgtmY?3X+TRjbqGKo6T>V(^Fb97>3q8*FU=%P9u5dxMYb=eSyOml<4sGj~-<&vu zCUffzpKn6((5LLP#02tm8HR{#D@c( z7izu+Z2~FMxphTuDXEOLeKji+!P=Qlua0g0Je441@C+RgG#SU^ZCKBc-q5)^y!_E+9R1J^naSm#u6K(zg8AK9?#cveFA0(k)xJ5Am0zeT`rVg*2FpC~(B|OW zLb2ZK!Bxi#$#QZSUxQoBc<=o}I8z?-Y)aR{K6D-wLPcFt@Hs@RWZTdxNhdnX0U zccHez2H4f(atu0BI=RkC<9Iu~PiE4St!ZVhFx?Q}aF(-{Zi;18*wiHPEpU~j;U>30^ZXqYMMQZ`wgwcf4g%%mH8@uF3s#DLFdamVL^ z{;;;%9<(u}>LpERGfi96?@FQs8N^TOlxIuDqc3y&Nlv+V_%r#<0=9P$xX&r3Mo*_2w7slYW zP4zly_1+~jygDm5LZ97oh9-D44jnz{p}(Y%Q%16)jtuV2ddx=KO)@1?YZcBIg2S#s zo2b% zyfK^YcNiDG3!Ym!2n!12Od?H&!PjEw^0Gc^rzEJeCM^A(!aqzk(?4d}J8M%FDZwfUc{fu_ znfXA{+t2xrFMI$BS#ws0THrTBjQ4vNM{6J&`bcg3alf^-aOWjh z^fseEA^*mS$?vJ&{;cYw%CzXuQF-cPHUd0;L|=;ik%vAq36P#>)8J-?yI49_U6UUu z2vB6yED85=-RjDq%&Q(-2p+h{FHnaaS9b*ukh(o|3dGze@VkiV!J(Rebtb=pQvjEC zCNV+Y)j@WNu4XKztRz~0^?pg&SlgNM*g75iETw5Eg~;S~S9bvnzjs^ftmX3daU4f@ z$k9~^*wpMl!$LJ{E4<{XW;EjKdMVDXg1k;Az0!*9YFF!4OO)GEVhN2xMX3HUfJGE!+n2YCu(iL6rDF}vXBDo;6E_c)3 zHYI;sC2Z^d<|$rR3$1&`*J=`nQF!TZ14D>R&N_q->jdnj98^5hz%Y7tLPU8#?we#esf8lhYFV<)u4VX4jwl)g$-j`~NO!Sbs;d{@<-8 z2!}_8sstO~UP1VOvn?E_5|3I1r^8gs5*EI<-Cs@$ti>4Zs72?YPA)KuQIU~SWZexs zLFay9AI}~MaZ5)b@W6~$@j2yBas(!1{;<#T8WpyPdoE8S>&uDV9u)D4zh4dweWCE) zMp|-w;n#<&O1Yd$H6#HG7@k}*$r|69wY&{&&{-?jWfUJERwwZJnwQfH&C?#6bn~&} z72do}jkS-$X5`FjXfj8sE=e33f4{pmhMI$$uW?5hS)`+BxK#;Od9q2TFptDalIr$| zOS=7YaTVM34JBXw;~!+ge6&g9Xo6R0$tio1b3pG)3e12pj|yXKTYxeM{kG{{AhGQq zO3260wj#=S-Ja5s8+kVFbAsApmr2#|qQR1@LU-rSA9yYb9ysf|fA5Qaxu(Fl5#Ubi zE>KKzC{#z`aME8kF}^(aQzTov?V|W?_L9B$O2Dj+)GHhgUl6tKmWVk~uvT_aPFn1< zjhA_Fi~c^|dO*f&1cRp}gc(XeFFNW2CQeuI@lO8}Wr?+TmXF4|9@IGYWEZ%62aJFT z|5i65APkNzr+>2!4Yj3gt2Q9FI*dKhf`&#b-<^apk-=8dogOCzfqh ztYi0ag=7mmZGQ}FLQzbHY<~K7Z5xcLb3n>=+Z#~`62l7Kg~Unq>>zxs3{=xPdD>|1l;9-bU$nIU9YmRoQk&FC}P7fbEB=y zB3YphxVu79Pado*fX$rBDs?{lmai?VjRq}Mbe=fUoWu$vicwP;9=$w#6J(p|*o3vx|8c1Jl-sMXUi*TgiPp+6Ec*^URRN(P= zY(RysMzAtZJ?yF0E)# zqPbzAggX1dWG>%)rbV}32XdKf03j<$HA$p^Uw-vbv8LPhPc3p)L1KuvH6m1Wq+4V0 zTY)36RP49)Dm(qyC%||!$#En!{AW0?>HU3bInaOeJQi16-RyaWfrb=}LOg~s<-RdT zr+A^et)WL=gtrH2ui3*??V0r8jo~v;2Hnfxd$Hon?9U#kpi$9~=!60zR-Bt{$t-

0XRz82%CD91jO7B79%x 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 TGbW&nLd;HBA20mL{kQ)E+QG}n literal 0 HcmV?d00001 diff --git a/avrow-cli/Cargo.toml b/avrow-cli/Cargo.toml new file mode 100644 index 0000000..cee4397 --- /dev/null +++ b/avrow-cli/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "avrow-cli" +version = "0.1.0" +authors = ["creativcoder "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +clap = {version="2.33.1", features=["yaml"] } +avrow = { path = "../../ravro", features=["all"] } +argh = "0.1.3" +anyhow = "1.0.32" +colored = "2.0.0" + +[[bin]] +name = "av" +path="src/main.rs" diff --git a/avrow-cli/README.md b/avrow-cli/README.md new file mode 100644 index 0000000..0b5f44e --- /dev/null +++ b/avrow-cli/README.md @@ -0,0 +1,31 @@ + +## Avrow-cli - command line tool to examine avro files [WIP] + +Inspired from avro-tools.jar + +### Following subcommands are the supported as of now. + +``` +Usage: target/debug/av [] + +av: command line tool for examining avro datafiles. + +Options: + --help display usage information + +Commands: + getmeta Get metadata information of the avro datafile. + getschema Prints the writer's schema encoded in the provided datafile. + read Prints data from datafile as human readable value + tobytes Dumps the avro datafile as bytes for debugging purposes + fingerprint Prints fingerprint of the canonical form of writer's schema. + canonical Prints the canonical form of writer's schema encoded in the + provided datafile. +canonical +``` + +Usage: + +```bash +av read -d ./data.avro +``` diff --git a/avrow-cli/src/main.rs b/avrow-cli/src/main.rs new file mode 100644 index 0000000..01824b0 --- /dev/null +++ b/avrow-cli/src/main.rs @@ -0,0 +1,43 @@ +//! avrow-cli is a command line tool to examine and analyze avro data files. +//! +//! Usage: avrow-cli -i tojson // This prints the data contained in the in a readable format. + +mod subcommand; +mod utils; + +use argh::FromArgs; +use utils::read_datafile; + +use subcommand::{Canonical, Fingerprint, GetMeta, GetSchema, ToBytes, ReadData}; + +#[derive(Debug, FromArgs)] +/// av: command line tool for examining avro datafiles. +struct AvrowCli { + #[argh(subcommand)] + subcommand: SubCommand, +} + +#[derive(FromArgs, PartialEq, Debug)] +#[argh(subcommand)] +enum SubCommand { + GetMeta(GetMeta), + GetSchema(GetSchema), + Read(ReadData), + ToBytes(ToBytes), + Fingerprint(Fingerprint), + Canonical(Canonical), +} + +fn main() -> anyhow::Result<()> { + let flags: AvrowCli = argh::from_env(); + match flags.subcommand { + SubCommand::GetMeta(cmd) => cmd.getmeta()?, + SubCommand::Read(cmd) => cmd.read_data()?, + SubCommand::ToBytes(cmd) => cmd.tobytes()?, + SubCommand::GetSchema(cmd) => cmd.getschema()?, + SubCommand::Fingerprint(cmd) => cmd.fingerprint()?, + SubCommand::Canonical(cmd) => cmd.canonical()? + } + + Ok(()) +} diff --git a/avrow-cli/src/subcommand.rs b/avrow-cli/src/subcommand.rs new file mode 100644 index 0000000..8b78bd2 --- /dev/null +++ b/avrow-cli/src/subcommand.rs @@ -0,0 +1,157 @@ +use crate::read_datafile; +use anyhow::{anyhow, Context}; +use argh::FromArgs; +use avrow::{Header, Reader}; +use std::io::Read; +use std::path::PathBuf; +use std::str; + +#[derive(FromArgs, PartialEq, Debug)] +/// Get metadata information of the avro datafile. +#[argh(subcommand, name = "getmeta")] +pub struct GetMeta { + /// datafile as input + #[argh(option, short = 'd')] + datafile: PathBuf, +} + +impl GetMeta { + pub fn getmeta(&self) -> Result<(), anyhow::Error> { + let mut avro_datafile = read_datafile(&self.datafile)?; + let header = Header::from_reader(&mut avro_datafile)?; + for (k, v) in header.metadata() { + print!("{}\t", k); + println!( + "{}", + str::from_utf8(v).expect("Invalid UTF-8 in avro header") + ); + } + Ok(()) + } +} + +#[derive(FromArgs, PartialEq, Debug)] +/// Prints data from datafile in debug format. +#[argh(subcommand, name = "read")] +pub struct ReadData { + /// datafile as input + #[argh(option, short = 'd')] + datafile: PathBuf, +} +impl ReadData { + pub fn read_data(&self) -> Result<(), anyhow::Error> { + let mut avro_datafile = read_datafile(&self.datafile)?; + let reader = Reader::new(&mut avro_datafile)?; + // TODO: remove irrelevant fields + for i in reader { + println!("{:#?}", i?); + } + + Ok(()) + } +} + +#[derive(FromArgs, PartialEq, Debug)] +/// Dumps the avro datafile as bytes for debugging purposes +#[argh(subcommand, name = "tobytes")] +pub struct ToBytes { + /// datafile as input + #[argh(option, short = 'd')] + datafile: PathBuf, +} + +impl ToBytes { + pub fn tobytes(&self) -> Result<(), anyhow::Error> { + let mut avro_datafile = read_datafile(&self.datafile)?; + let mut v = vec![]; + + avro_datafile + .read_to_end(&mut v) + .with_context(|| "Failed to read data file in memory")?; + + println!("{:?}", v); + Ok(()) + } +} + +#[derive(FromArgs, PartialEq, Debug)] +/// Prints the writer's schema encoded in the provided datafile. +#[argh(subcommand, name = "getschema")] +pub struct GetSchema { + /// datafile as input + #[argh(option, short = 'd')] + datafile: PathBuf, +} + +impl GetSchema{ + pub fn getschema(&self) -> Result<(), anyhow::Error> { + let mut avro_datafile = read_datafile(&self.datafile)?; + let header = Header::from_reader(&mut avro_datafile)?; + // TODO print human readable schema + dbg!(header.schema()); + Ok(()) + } +} + +#[derive(FromArgs, PartialEq, Debug)] +/// Prints fingerprint of the canonical form of writer's schema. +#[argh(subcommand, name = "fingerprint")] +pub struct Fingerprint { + /// datafile as input + #[argh(option, short = 'd')] + datafile: String, + /// the fingerprinting algorithm (rabin64 (default), sha256, md5) + #[argh(option, short = 'f')] + fingerprint: String, +} +impl Fingerprint { + pub fn fingerprint(&self) -> Result<(), anyhow::Error> { + let mut avro_datafile = read_datafile(&self.datafile)?; + let header = Header::from_reader(&mut avro_datafile)?; + match self.fingerprint.as_ref() { + "rabin64" => { + println!("0x{:x}", header.schema().canonical_form().rabin64()); + }, + "sha256" => { + let mut fingerprint_str = String::new(); + let sha256 = header.schema().canonical_form().sha256(); + for i in sha256 { + let a = format!("{:x}", i); + fingerprint_str.push_str(&a); + } + + println!("{}", fingerprint_str); + } + "md5" => { + let mut fingerprint_str = String::new(); + let md5 = header.schema().canonical_form().md5(); + for i in md5 { + let a = format!("{:x}", i); + fingerprint_str.push_str(&a); + } + + println!("{}", fingerprint_str); + } + other => return Err(anyhow!("invalid or unsupported fingerprint: {}", other)) + } + Ok(()) + } +} + +#[derive(FromArgs, PartialEq, Debug)] +/// Prints the canonical form of writer's schema encoded in the provided datafile. +#[argh(subcommand, name = "canonical")] +pub struct Canonical { + /// datafile as input + #[argh(option, short = 'd')] + datafile: String, +} + +impl Canonical { + pub fn canonical(&self) -> Result<(), anyhow::Error> { + let mut avro_datafile = read_datafile(&self.datafile)?; + let header = Header::from_reader(&mut avro_datafile)?; + println!("{}", header.schema().canonical_form()); + Ok(()) + } +} diff --git a/avrow-cli/src/utils.rs b/avrow-cli/src/utils.rs new file mode 100644 index 0000000..2b63045 --- /dev/null +++ b/avrow-cli/src/utils.rs @@ -0,0 +1,11 @@ +use anyhow::Context; +use anyhow::Result; +use std::path::Path; + +// Open an avro datafile for reading avro data +pub(crate) fn read_datafile>(path: P) -> Result { + std::fs::OpenOptions::new() + .read(true) + .open(path) + .with_context(|| "Could not read datafile") +} diff --git a/benches/complex.rs b/benches/complex.rs new file mode 100644 index 0000000..3f8794a --- /dev/null +++ b/benches/complex.rs @@ -0,0 +1,150 @@ +extern crate avrow; +extern crate serde; +#[macro_use] +extern crate serde_derive; + +#[macro_use] +extern crate criterion; + +use avrow::Codec; +use avrow::Schema; +use avrow::Writer; +use criterion::Criterion; +use std::str::FromStr; + +#[derive(Debug, Serialize, Deserialize)] +struct LongList { + value: i64, + next: Option>, +} + +fn simple_record(c: &mut Criterion) { + c.bench_function("simple_record", |b| { + let schema = Schema::from_str( + r##"{ + "namespace": "atherenergy.vcu_cloud_connect", + "type": "record", + "name": "can_raw", + "fields" : [ + {"name": "one", "type": "int"}, + {"name": "two", "type": "long"}, + {"name": "three", "type": "long"}, + {"name": "four", "type": "int"}, + {"name": "five", "type": "long"} + ] + }"##, + ) + .unwrap(); + let v = vec![]; + let mut writer = Writer::with_codec(&schema, v, Codec::Null).unwrap(); + b.iter(|| { + for _ in 0..1000 { + let data = Data { + one: 34, + two: 334, + three: 45765, + four: 45643, + five: 834, + }; + + writer.serialize(data).unwrap(); + } + + // batch and write data + writer.flush().unwrap(); + }); + }); +} + +#[derive(Serialize, Deserialize)] +struct Data { + one: u32, + two: u64, + three: u64, + four: u32, + five: u64, +} + +fn array_record(c: &mut Criterion) { + c.bench_function("Array of records", |b| { + let schema = Schema::from_str( + r##"{"type": "array", "items": { + "namespace": "atherenergy.vcu_cloud_connect", + "type": "record", + "name": "can_raw", + "fields" : [ + {"name": "one", "type": "int"}, + {"name": "two", "type": "long"}, + {"name": "three", "type": "long"}, + {"name": "four", "type": "int"}, + {"name": "five", "type": "long"} + ] + }}"##, + ) + .unwrap(); + let mut v = vec![]; + let mut writer = Writer::with_codec(&schema, &mut v, Codec::Null).unwrap(); + b.iter(|| { + let mut can_array = vec![]; + for _ in 0..1000 { + let data = Data { + one: 34, + two: 334, + three: 45765, + four: 45643, + five: 834, + }; + + can_array.push(data); + } + + // batch and write data + writer.serialize(can_array).unwrap(); + writer.flush().unwrap(); + }); + }); +} + +fn nested_recursive_record(c: &mut Criterion) { + c.bench_function("recursive_nested_record", |b| { + let schema = r##" + { + "type": "record", + "name": "LongList", + "aliases": ["LinkedLongs"], + "fields" : [ + {"name": "value", "type": "long"}, + {"name": "next", "type": ["null", "LongList"]} + ] + } + "##; + + let schema = Schema::from_str(schema).unwrap(); + let mut writer = Writer::with_codec(&schema, vec![], Codec::Null).unwrap(); + + b.iter(|| { + for _ in 0..1000 { + let value = LongList { + value: 1i64, + next: Some(Box::new(LongList { + value: 2, + next: Some(Box::new(LongList { + value: 3, + next: None, + })), + })), + }; + writer.serialize(value).unwrap(); + } + }); + writer.flush().unwrap(); + }); +} + +criterion_group!( + benches, + nested_recursive_record, + array_record, + simple_record +); +criterion_main!(benches); diff --git a/benches/primitives.rs b/benches/primitives.rs new file mode 100644 index 0000000..9651535 --- /dev/null +++ b/benches/primitives.rs @@ -0,0 +1,149 @@ +extern crate avrow; + +#[macro_use] +extern crate criterion; + +use criterion::Criterion; + +use avrow::from_value; +use avrow::Reader; +use avrow::Schema; +use avrow::Writer; +use std::str::FromStr; + +fn criterion_benchmark(c: &mut Criterion) { + // Write benchmarks + c.bench_function("write_null", |b| { + let schema = Schema::from_str(r##"{"type": "null" }"##).unwrap(); + let mut out = vec![]; + let mut writer = Writer::new(&schema, &mut out).unwrap(); + + b.iter(|| { + for _ in 0..100_000 { + writer.write(()).unwrap(); + } + }); + + writer.flush().unwrap(); + }); + + c.bench_function("write_boolean", |b| { + let schema = Schema::from_str(r##"{"type": "boolean" }"##).unwrap(); + let mut out = vec![]; + let mut writer = Writer::new(&schema, &mut out).unwrap(); + + b.iter(|| { + for i in 0..100_000 { + writer.write(i % 2 == 0).unwrap(); + } + }); + + writer.flush().unwrap(); + }); + + c.bench_function("write_int", |b| { + let schema = Schema::from_str(r##"{"type": "int" }"##).unwrap(); + let mut out = vec![]; + let mut writer = Writer::new(&schema, &mut out).unwrap(); + + b.iter(|| { + for _ in 0..100_000 { + writer.write(45).unwrap(); + } + }); + + writer.flush().unwrap(); + }); + + c.bench_function("write_long", |b| { + let schema = Schema::from_str(r##"{"type": "long" }"##).unwrap(); + let mut out = vec![]; + let mut writer = Writer::new(&schema, &mut out).unwrap(); + + b.iter(|| { + for _ in 0..100_000 { + writer.write(45i64).unwrap(); + } + }); + + writer.flush().unwrap(); + }); + + c.bench_function("write_float", |b| { + let schema = Schema::from_str(r##"{"type": "float" }"##).unwrap(); + let mut out = vec![]; + let mut writer = Writer::new(&schema, &mut out).unwrap(); + + b.iter(|| { + for _ in 0..100_000 { + writer.write(45.0f32).unwrap(); + } + }); + + writer.flush().unwrap(); + }); + + c.bench_function("write_double", |b| { + let schema = Schema::from_str(r##"{"type": "double" }"##).unwrap(); + let mut out = vec![]; + let mut writer = Writer::new(&schema, &mut out).unwrap(); + + b.iter(|| { + for _ in 0..100_000 { + writer.write(45.0f64).unwrap(); + } + }); + + writer.flush().unwrap(); + }); + + c.bench_function("write_bytes", |b| { + let schema = Schema::from_str(r##"{"type": "bytes" }"##).unwrap(); + let mut out = vec![]; + let mut writer = Writer::new(&schema, &mut out).unwrap(); + + b.iter(|| { + for _ in 0..100_000 { + let v = vec![0u8, 1, 2, 3]; + writer.write(v).unwrap(); + } + }); + + writer.flush().unwrap(); + }); + + c.bench_function("write_string", |b| { + let schema = Schema::from_str(r##"{"type": "string" }"##).unwrap(); + let mut out = vec![]; + let mut writer = Writer::new(&schema, &mut out).unwrap(); + + b.iter(|| { + for _ in 0..100_000 { + writer.write("hello").unwrap(); + } + }); + + writer.flush().unwrap(); + }); + + // Read benchmarks + c.bench_function("avro_read_bytes_from_vec", |b| { + let avro_data = vec![ + 79, 98, 106, 1, 4, 22, 97, 118, 114, 111, 46, 115, 99, 104, 101, 109, 97, 32, 123, 34, + 116, 121, 112, 101, 34, 58, 34, 98, 121, 116, 101, 115, 34, 125, 20, 97, 118, 114, 111, + 46, 99, 111, 100, 101, 99, 8, 110, 117, 108, 108, 0, 149, 158, 112, 231, 150, 73, 245, + 11, 130, 6, 13, 141, 239, 19, 146, 71, 2, 14, 12, 0, 1, 2, 3, 4, 5, 149, 158, 112, 231, + 150, 73, 245, 11, 130, 6, 13, 141, 239, 19, 146, 71, + ]; + + b.iter(|| { + let reader = Reader::new(avro_data.as_slice()).unwrap(); + for i in reader { + let _: Vec = from_value(&i).unwrap(); + } + }); + }); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); diff --git a/benches/schema.rs b/benches/schema.rs new file mode 100644 index 0000000..61b3355 --- /dev/null +++ b/benches/schema.rs @@ -0,0 +1,61 @@ +#[macro_use] +extern crate criterion; +extern crate avrow; + +use criterion::criterion_group; +use criterion::Criterion; +use std::str::FromStr; + +use avrow::Schema; + +fn parse_enum_schema() { + let _ = Schema::from_str( + r##"{ "type": "enum", + "name": "Suit", + "symbols" : ["SPADES", "HEARTS", "DIAMONDS", "CLUBS"] + }"##, + ) + .unwrap(); +} + +fn parse_string_schema() { + let _ = Schema::from_str(r##""string""##).unwrap(); +} + +fn parse_record_schema(c: &mut Criterion) { + c.bench_function("parse_record_schema", |b| { + b.iter(|| { + let _ = Schema::from_str( + r##"{ + "namespace": "sensor_data", + "type": "record", + "name": "can", + "fields" : [ + {"name": "can_id", "type": "int"}, + {"name": "data", "type": "long"}, + {"name": "timestamp", "type": "double"}, + {"name": "seq_num", "type": "int"}, + {"name": "global_seq", "type": "long"} + ] + }"##, + ) + .unwrap(); + }); + }); +} + +fn bench_string_schema(c: &mut Criterion) { + c.bench_function("parse string schema", |b| b.iter(parse_string_schema)); +} + +fn bench_enum_schema(c: &mut Criterion) { + c.bench_function("parse enum schema", |b| b.iter(parse_enum_schema)); +} + +criterion_group!( + benches, + bench_string_schema, + bench_enum_schema, + parse_record_schema +); +criterion_main!(benches); diff --git a/benches/write.rs b/benches/write.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/benches/write.rs @@ -0,0 +1 @@ + diff --git a/examples/canonical.rs b/examples/canonical.rs new file mode 100644 index 0000000..9b7293c --- /dev/null +++ b/examples/canonical.rs @@ -0,0 +1,24 @@ +use anyhow::Error; +use avrow::Schema; +use std::str::FromStr; + +fn main() -> Result<(), Error> { + let schema = Schema::from_str( + r##" + { + "type": "record", + "name": "LongList", + "aliases": ["LinkedLongs"], + "fields" : [ + {"name": "value", "type": "long"}, + {"name": "next", "type": ["null", "LongList"] + }] + } + "##, + ) + .unwrap(); + println!("{}", schema.canonical_form()); + // get the rabin fingerprint of the canonical form. + dbg!(schema.canonical_form().rabin64()); + Ok(()) +} diff --git a/examples/from_json_to_struct.rs b/examples/from_json_to_struct.rs new file mode 100644 index 0000000..0938a6d --- /dev/null +++ b/examples/from_json_to_struct.rs @@ -0,0 +1,72 @@ +use anyhow::Error; +use avrow::{from_value, Reader, Record, Schema, Writer}; +use serde::{Deserialize, Serialize}; +use std::str::FromStr; +#[derive(Debug, Serialize, Deserialize)] +struct Mentees { + id: i32, + username: String, +} + +#[derive(Debug, Serialize, Deserialize)] +struct RustMentors { + name: String, + github_handle: String, + active: bool, + mentees: Mentees, +} + +fn main() -> Result<(), Error> { + let schema = Schema::from_str( + r##" + { + "name": "rust_mentors", + "type": "record", + "fields": [ + { + "name": "name", + "type": "string" + }, + { + "name": "github_handle", + "type": "string" + }, + { + "name": "active", + "type": "boolean" + }, + { + "name":"mentees", + "type": { + "name":"mentees", + "type": "record", + "fields": [ + {"name":"id", "type": "int"}, + {"name":"username", "type": "string"} + ] + } + } + ] + } +"##, + )?; + + let json_data = serde_json::from_str( + r##" + { "name": "bob", + "github_handle":"ghbob", + "active": true, + "mentees":{"id":1, "username":"alice"} }"##, + )?; + let rec = Record::from_json(json_data, &schema)?; + let mut writer = crate::Writer::new(&schema, vec![])?; + writer.write(rec)?; + + let avro_data = writer.into_inner()?; + let reader = Reader::new(avro_data.as_slice())?; + for value in reader { + let mentors: RustMentors = from_value(&value)?; + dbg!(mentors); + } + Ok(()) +} diff --git a/examples/hello_world.rs b/examples/hello_world.rs new file mode 100644 index 0000000..4b2f5fd --- /dev/null +++ b/examples/hello_world.rs @@ -0,0 +1,41 @@ +// A hello world example of reading and writing avro data files + +use anyhow::Error; +use avrow::from_value; +use avrow::Reader; +use avrow::Schema; +use avrow::Writer; +use std::str::FromStr; + +use std::io::Cursor; + +fn main() -> Result<(), Error> { + // Writing data + + // Create a schema + let schema = Schema::from_str(r##""null""##)?; + // Create writer using schema and provide a buffer (implements Read) to write to + let mut writer = Writer::new(&schema, vec![])?; + // Write the data using write and creating a Value manually. + writer.write(())?; + // or the more convenient and intuitive serialize method that takes native Rust types. + writer.serialize(())?; + // retrieve the underlying buffer using the buffer method. + // TODO buffer is not intuive when using a file. into_inner is much better here. + let buf = writer.into_inner()?; + + // Reading data + + // Create Reader by providing a Read wrapped version of `buf` + let reader = Reader::new(Cursor::new(buf))?; + // Use iterator for reading data in an idiomatic manner. + for i in reader { + // reading values can fail due to decoding errors, so the return value of iterator is a Option> + // it allows one to examine the failure reason on the underlying avro reader. + dbg!(&i); + // This value can be converted to a native Rust type using `from_value` method that uses serde underneath. + let _val: () = from_value(&i)?; + } + + Ok(()) +} diff --git a/examples/recursive_record.rs b/examples/recursive_record.rs new file mode 100644 index 0000000..c97aa6d --- /dev/null +++ b/examples/recursive_record.rs @@ -0,0 +1,56 @@ +use anyhow::Error; +use avrow::{from_value, Codec, Reader, Schema, Writer}; +use serde::{Deserialize, Serialize}; +use std::str::FromStr; + +#[derive(Debug, Serialize, Deserialize)] +struct LongList { + value: i64, + next: Option>, +} + +fn main() -> Result<(), Error> { + let schema = r##" + { + "type": "record", + "name": "LongList", + "aliases": ["LinkedLongs"], + "fields" : [ + {"name": "value", "type": "long"}, + {"name": "next", "type": ["null", "LongList"]} + ] + } + "##; + + let schema = Schema::from_str(schema)?; + let mut writer = Writer::with_codec(&schema, vec![], Codec::Null)?; + + let value = LongList { + value: 1i64, + next: Some(Box::new(LongList { + value: 2i64, + next: Some(Box::new(LongList { + value: 3i64, + next: Some(Box::new(LongList { + value: 4i64, + next: Some(Box::new(LongList { + value: 5i64, + next: None, + })), + })), + })), + })), + }; + writer.serialize(value)?; + + let buf = writer.into_inner()?; + + // read + let reader = Reader::with_schema(buf.as_slice(), schema)?; + for i in reader { + let a: LongList = from_value(&i)?; + dbg!(a); + } + + Ok(()) +} diff --git a/examples/writer_builder.rs b/examples/writer_builder.rs new file mode 100644 index 0000000..6b555bc --- /dev/null +++ b/examples/writer_builder.rs @@ -0,0 +1,23 @@ +use anyhow::Error; +use avrow::{Codec, Reader, Schema, WriterBuilder}; +use std::str::FromStr; + +fn main() -> Result<(), Error> { + let schema = Schema::from_str(r##""null""##)?; + let v = vec![]; + let mut writer = WriterBuilder::new() + .set_codec(Codec::Null) + .set_schema(&schema) + .set_datafile(v) + .set_flush_interval(128_000) + .build()?; + writer.serialize(())?; + let v = writer.into_inner()?; + + let reader = Reader::with_schema(v.as_slice(), schema)?; + for i in reader { + dbg!(i?); + } + + Ok(()) +} diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..27eb93b --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,2 @@ +edition = "2018" +reorder_imports = true \ No newline at end of file diff --git a/src/codec.rs b/src/codec.rs new file mode 100644 index 0000000..93ccfd1 --- /dev/null +++ b/src/codec.rs @@ -0,0 +1,273 @@ +use crate::error::AvrowErr; +use crate::util::{encode_long, encode_raw_bytes}; + +use std::io::Write; + +// Given a slice of bytes, generates a CRC for it +#[cfg(feature = "snappy")] +pub fn get_crc_uncompressed(pre_comp_buf: &[u8]) -> Result, AvrowErr> { + use byteorder::{BigEndian, WriteBytesExt}; + use crc::crc32; + + let crc_checksum = crc32::checksum_ieee(pre_comp_buf); + let mut checksum_bytes = Vec::with_capacity(1); + let _ = checksum_bytes + .write_u32::(crc_checksum) + .map_err(|_| { + let err: AvrowErr = AvrowErr::CRCGenFailed; + err + })?; + Ok(checksum_bytes) +} + +/// Given a uncompressed slice of bytes, returns a compresed Vector of bytes using the snappy codec +#[cfg(feature = "snappy")] +pub(crate) fn compress_snappy(uncompressed_buffer: &[u8]) -> Result, AvrowErr> { + let mut encoder = snap::Encoder::new(); + encoder + .compress_vec(uncompressed_buffer) + .map_err(|e| AvrowErr::DecodeFailed(e.into())) +} + +#[cfg(feature = "deflate")] +pub fn compress_deflate(uncompressed_buffer: &[u8]) -> Result, AvrowErr> { + use flate2::{write::DeflateEncoder, Compression}; + + let mut encoder = DeflateEncoder::new(Vec::new(), Compression::default()); + encoder + .write(uncompressed_buffer) + .map_err(AvrowErr::EncodeFailed)?; + encoder.finish().map_err(AvrowErr::EncodeFailed) +} + +#[cfg(feature = "zstd")] +pub(crate) fn zstd_compress(level: i32, uncompressed_buffer: &[u8]) -> Result, AvrowErr> { + let comp = zstdd::encode_all(std::io::Cursor::new(uncompressed_buffer), level) + .map_err(AvrowErr::EncodeFailed)?; + Ok(comp) +} + +#[cfg(feature = "deflate")] +pub fn decompress_deflate( + compressed_buffer: &[u8], + uncompressed: &mut Vec, +) -> Result<(), AvrowErr> { + use flate2::bufread::DeflateDecoder; + use std::io::Read; + + let mut decoder = DeflateDecoder::new(compressed_buffer); + uncompressed.clear(); + decoder + .read_to_end(uncompressed) + .map_err(AvrowErr::DecodeFailed)?; + Ok(()) +} + +#[cfg(feature = "snappy")] +pub(crate) fn decompress_snappy( + compressed_buffer: &[u8], + uncompressed: &mut Vec, +) -> Result<(), AvrowErr> { + use byteorder::ByteOrder; + + let data_minus_cksum = &compressed_buffer[..compressed_buffer.len() - 4]; + let decompressed_size = + snap::decompress_len(data_minus_cksum).map_err(|e| AvrowErr::DecodeFailed(e.into()))?; + uncompressed.resize(decompressed_size, 0); + snap::Decoder::new() + .decompress(data_minus_cksum, &mut uncompressed[..]) + .map_err(|e| AvrowErr::DecodeFailed(e.into()))?; + + let expected = + byteorder::BigEndian::read_u32(&compressed_buffer[compressed_buffer.len() - 4..]); + let found = crc::crc32::checksum_ieee(&uncompressed); + if expected != found { + return Err(AvrowErr::CRCMismatch { found, expected }); + } + Ok(()) +} + +#[cfg(feature = "zstd")] +pub(crate) fn decompress_zstd( + compressed_buffer: &[u8], + uncompressed: &mut Vec, +) -> Result<(), AvrowErr> { + let mut decoder = zstdd::Decoder::new(compressed_buffer).map_err(AvrowErr::DecodeFailed)?; + std::io::copy(&mut decoder, uncompressed).map_err(AvrowErr::DecodeFailed)?; + Ok(()) +} + +#[cfg(feature = "bzip2")] +pub(crate) fn decompress_bzip2( + compressed_buffer: &[u8], + uncompressed: &mut Vec, +) -> Result<(), AvrowErr> { + use bzip2::read::BzDecoder; + let decompressor = BzDecoder::new(compressed_buffer); + let mut buf = decompressor.into_inner(); + std::io::copy(&mut buf, uncompressed).map_err(AvrowErr::DecodeFailed)?; + Ok(()) +} + +#[cfg(feature = "xz")] +pub(crate) fn decompress_xz( + compressed_buffer: &[u8], + uncompressed: &mut Vec, +) -> Result<(), AvrowErr> { + use xz2::read::XzDecoder; + let decompressor = XzDecoder::new(compressed_buffer); + let mut buf = decompressor.into_inner(); + std::io::copy(&mut buf, uncompressed).map_err(AvrowErr::DecodeFailed)?; + Ok(()) +} +/// Defines codecs one can use when writing avro data. +#[derive(Debug, PartialEq, Clone, Copy)] +pub enum Codec { + /// The Null codec. When no codec is specified at the time of Writer creation, null is the default. + Null, + #[cfg(feature = "deflate")] + /// The Deflate codec.
Uses https://docs.rs/flate2 as the underlying implementation. + Deflate, + #[cfg(feature = "snappy")] + /// The Snappy codec.
Uses https://docs.rs/snap as the underlying implementation. + Snappy, + #[cfg(feature = "zstd")] + /// The Zstd codec.
Uses https://docs.rs/zstd as the underlying implementation. + Zstd, + #[cfg(feature = "bzip2")] + /// The Bzip2 codec.
Uses https://docs.rs/bzip2 as the underlying implementation. + Bzip2, + #[cfg(feature = "xz")] + /// The Xz codec.
Uses https://docs.rs/crate/xz2 as the underlying implementation. + Xz, +} + +impl AsRef for Codec { + fn as_ref(&self) -> &str { + match self { + Codec::Null => "null", + #[cfg(feature = "deflate")] + Codec::Deflate => "deflate", + #[cfg(feature = "snappy")] + Codec::Snappy => "snappy", + #[cfg(feature = "zstd")] + Codec::Zstd => "zstd", + #[cfg(feature = "bzip2")] + Codec::Bzip2 => "bzip2", + #[cfg(feature = "xz")] + Codec::Xz => "xz", + } + } +} + +// TODO allow all of these to be configurable for setting compression ratio/level +impl Codec { + pub(crate) fn encode( + &self, + block_stream: &mut [u8], + out_stream: &mut W, + ) -> Result<(), AvrowErr> { + match self { + Codec::Null => { + // encode size of data in block + encode_long(block_stream.len() as i64, out_stream)?; + // encode actual data bytes + encode_raw_bytes(&block_stream, out_stream)?; + } + #[cfg(feature = "snappy")] + Codec::Snappy => { + let checksum_bytes = get_crc_uncompressed(&block_stream)?; + let compressed_data = compress_snappy(&block_stream)?; + encode_long( + compressed_data.len() as i64 + crate::config::CRC_CHECKSUM_LEN as i64, + out_stream, + )?; + + out_stream + .write(&*compressed_data) + .map_err(AvrowErr::EncodeFailed)?; + out_stream + .write(&*checksum_bytes) + .map_err(AvrowErr::EncodeFailed)?; + } + #[cfg(feature = "deflate")] + Codec::Deflate => { + let compressed_data = compress_deflate(block_stream)?; + encode_long(compressed_data.len() as i64, out_stream)?; + encode_raw_bytes(&*compressed_data, out_stream)?; + } + #[cfg(feature = "zstd")] + Codec::Zstd => { + let compressed_data = zstd_compress(0, block_stream)?; + encode_long(compressed_data.len() as i64, out_stream)?; + encode_raw_bytes(&*compressed_data, out_stream)?; + } + #[cfg(feature = "bzip2")] + Codec::Bzip2 => { + use bzip2::read::BzEncoder; + use bzip2::Compression; + use std::io::Cursor; + let compressor = BzEncoder::new(Cursor::new(block_stream), Compression::new(5)); + let vec = compressor.into_inner().into_inner(); + + encode_long(vec.len() as i64, out_stream)?; + encode_raw_bytes(&*vec, out_stream)?; + } + #[cfg(feature = "xz")] + Codec::Xz => { + use std::io::Cursor; + use xz2::read::XzEncoder; + let compressor = XzEncoder::new(Cursor::new(block_stream), 6); + let vec = compressor.into_inner().into_inner(); + + encode_long(vec.len() as i64, out_stream)?; + encode_raw_bytes(&*vec, out_stream)?; + } + } + Ok(()) + } + + pub(crate) fn decode( + &self, + compressed: Vec, + uncompressed: &mut std::io::Cursor>, + ) -> Result<(), AvrowErr> { + match self { + Codec::Null => { + *uncompressed = std::io::Cursor::new(compressed); + Ok(()) + } + #[cfg(feature = "snappy")] + Codec::Snappy => decompress_snappy(&compressed, uncompressed.get_mut()), + #[cfg(feature = "deflate")] + Codec::Deflate => decompress_deflate(&compressed, uncompressed.get_mut()), + #[cfg(feature = "zstd")] + Codec::Zstd => decompress_zstd(&compressed, uncompressed.get_mut()), + #[cfg(feature = "bzip2")] + Codec::Bzip2 => decompress_bzip2(&compressed, uncompressed.get_mut()), + #[cfg(feature = "xz")] + Codec::Xz => decompress_xz(&compressed, uncompressed.get_mut()), + } + } +} + +impl std::convert::TryFrom<&str> for Codec { + type Error = AvrowErr; + + fn try_from(value: &str) -> Result { + match value { + "null" => Ok(Codec::Null), + #[cfg(feature = "snappy")] + "snappy" => Ok(Codec::Snappy), + #[cfg(feature = "deflate")] + "deflate" => Ok(Codec::Deflate), + #[cfg(feature = "zstd")] + "zstd" => Ok(Codec::Zstd), + #[cfg(feature = "bzip2")] + "bzip2" => Ok(Codec::Bzip2), + #[cfg(feature = "bzip2")] + "xz" => Ok(Codec::Xz), + o => Err(AvrowErr::UnsupportedCodec(o.to_string())), + } + } +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..b60a74c --- /dev/null +++ b/src/config.rs @@ -0,0 +1,15 @@ +//! This module contains constants and configuration parameters for configuring avro writers and readers. + +/// Synchronization marker bytes length, defaults to 16 bytes. +pub const SYNC_MARKER_SIZE: usize = 16; +/// The magic header for recognizing a file as an avro data file. +pub const MAGIC_BYTES: &[u8] = b"Obj\x01"; +/// Checksum length for snappy compressed data. +#[cfg(feature = "snappy")] +pub const CRC_CHECKSUM_LEN: usize = 4; +/// Minimum flush interval that a block can have. +pub const BLOCK_SIZE: usize = 4096; +/// This value defines the threshold post which the scratch buffer is +/// is flushed/synced to the main buffer. Suggested values are between 2K (bytes) and 2M +// TODO make this configurable +pub const DEFAULT_FLUSH_INTERVAL: usize = 16 * BLOCK_SIZE; diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..ce76d61 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,184 @@ +#![allow(missing_docs)] + +use serde::{de, ser}; +use std::fmt::Debug; +use std::fmt::Display; +use std::io::{Error, ErrorKind}; + +#[inline(always)] +pub(crate) fn io_err(msg: &str) -> Error { + Error::new(ErrorKind::Other, msg) +} + +// Required impls for Serde +impl ser::Error for AvrowErr { + fn custom(msg: T) -> Self { + Self::Message(msg.to_string()) + } +} + +impl de::Error for AvrowErr { + fn custom(msg: T) -> Self { + Self::Message(msg.to_string()) + } +} + +pub type AvrowResult = Result; + +/// Errors returned from avrow +#[derive(thiserror::Error, Debug)] +pub enum AvrowErr { + // Encode errors + #[error("Write failed")] + EncodeFailed(#[source] std::io::Error), + #[error("Encoding failed. Value does not match schema")] + SchemaDataMismatch, + #[error("Expected magic header: `Obj\n`")] + InvalidDataFile, + #[error("Sync marker does not match as expected")] + SyncMarkerMismatch, + #[error("Named schema not found in union")] + SchemaNotFoundInUnion, + #[error("Invalid field value: {0}")] + InvalidFieldValue(String), + #[error("Writer seek failed, not a valid avro data file")] + WriterSeekFailed, + #[error("Unions must not contain immediate union values")] + NoImmediateUnion, + #[error("Failed building the Writer")] + WriterBuildFailed, + #[error("Json must be an object for record")] + ExpectedJsonObject, + + // Decode errors + #[error("Read failed")] + DecodeFailed(#[source] std::io::Error), + #[error("failed reading `avro.schema` metadata from header")] + HeaderDecodeFailed, + #[error("Unsupported codec {0}, did you enable the feature?")] + UnsupportedCodec(String), + #[error("Named schema was not found in schema registry")] + NamedSchemaNotFound, + #[error("Schema resolution failed. reader's schema {0} != writer's schema {1}")] + SchemaResolutionFailed(String, String), + #[error("Index read for enum is out of range as per schema. got: {0} symbols: {1}")] + InvalidEnumSymbolIdx(usize, String), + #[error("Field not found in record")] + FieldNotFound, + #[error("Writer schema not found in reader's schema")] + WriterNotInReader, + #[error("Reader's union schema does not match with writer's union schema")] + UnionSchemaMismatch, + #[error("Map's value schema do not match")] + MapSchemaMismatch, + #[error("Fixed schema names do not match")] + FixedSchemaNameMismatch, + #[error("Could not find symbol at index {idx} in reader schema")] + EnumSymbolNotFound { idx: usize }, + #[error("Reader's enum name does not match writer's enum name")] + EnumNameMismatch, + #[error("Readers' record name does not match writer's record name")] + RecordNameMismatch, + #[error("Array items schema does not match")] + ArrayItemsMismatch, + #[error("Snappy decoder failed to get length of decompressed buffer")] + SnappyDecompressLenFailed, + #[error("End of file reached")] + Eof, + + // Schema parse errors + #[error("Failed to parse avro schema")] + SchemaParseErr(#[source] std::io::Error), + #[error("Unknown schema, expecting a required `type` field in schema")] + SchemaParseFailed, + #[error("Expecting fields key as a json array, found: {0}")] + SchemaFieldParseErr(String), + #[error("Expected: {0}, found: {1}")] + SchemaDataValidationFailed(String, String), + #[error("Schema has a field not found in the value")] + RecordFieldMissing, + #[error("Record schema does not a have a required field named `name`")] + RecordNameNotFound, + #[error("Record schema does not a have a required field named `type`")] + RecordTypeNotFound, + #[error("Expected record field to be a json array")] + ExpectedFieldsJsonArray, + #[error("Record's field json schema must be an object")] + InvalidRecordFieldType, + #[error("{0}")] + ParseFieldOrderErr(String), + #[error("Could not parse name from json value")] + NameParseFailed, + #[error("Parsing canonical form failed")] + ParsingCanonicalForm, + #[error("Duplicate definition of named schema")] + DuplicateSchema, + #[error("Invalid default value for union. Must be the first entry from union definition")] + FailedDefaultUnion, + #[error("Invalid default value for given schema")] + DefaultValueParse, + #[error("Unknown field ordering value.")] + UnknownFieldOrdering, + #[error("Field ordering value must be a string")] + InvalidFieldOrdering, + #[error("Failed to parse symbol from enum's symbols field")] + EnumSymbolParseErr, + #[error("Enum schema must contain required `symbols` field")] + EnumSymbolsMissing, + #[error("Enum value symbol not present in enum schema `symbols` field")] + EnumSymbolNotPresent, + #[error("Fixed schema `size` field must be a number")] + FixedSizeNotNumber, + #[error("Fixed schema `size` field missing")] + FixedSizeNotFound, + #[error("Unions cannot have multiple schemas of same type or immediate unions")] + DuplicateSchemaInUnion, + #[error("Expected the avro schema to be as one of json string, object or an array")] + UnknownSchema, + #[error("Expected record field to be a json object, found {0}")] + InvalidSchema(String), + #[error("{0}")] + InvalidDefaultValue(String), + #[error("Invalid type for {0}")] + InvalidType(String), + #[error("Enum schema parsing failed, found: {0}")] + EnumParseErr(String), + #[error("Primitve schema must be a string")] + InvalidPrimitiveSchema, + + // Validation errors + #[error("Mismatch in fixed bytes length: {found}, {expected}")] + FixedValueLenMismatch { found: usize, expected: usize }, + #[error("namespaces must either be empty or follow the grammer [()*")] + InvalidNamespace, + #[error("Field name must be [A-Za-z_] and subsequently contain only [A-Za-z0-9_]")] + InvalidName, + #[error("Array value is empty")] + EmptyArray, + #[error("Map value is empty")] + EmptyMap, + #[error("Crc generation failed")] + CRCGenFailed, + #[error("Snappy Crc mismatch")] + CRCMismatch { found: u32, expected: u32 }, + #[error("Named schema was not found for given value")] + NamedSchemaNotFoundForValue, + #[error("Value schema not found in union")] + NotFoundInUnion, + + // Serde specific errors + #[error("Serde error: {0}")] + Message(String), + #[error("Syntax error occured")] + Syntax, + #[error("Expected a string value")] + ExpectedString, + #[error("Unsupported type")] + Unsupported, + #[error("Unexpected avro value: {value}")] + UnexpectedAvroValue { value: String }, + + // Value errors + #[error("Expected value not found in variant instance")] + ExpectedVariantNotFound, +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..9503a39 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,81 @@ +//! Avrow is a pure Rust implementation of the [Apache Avro specification](https://avro.apache.org/docs/current/spec.html). +//! +//! For more details on the spec, head over to [FAQ](https://cwiki.apache.org/confluence/display/AVRO/FAQ) +//! +//! ## Using the library +//! +//! Add to your `Cargo.toml`: +//!```toml +//! [dependencies] +//! avrow = "0.1" +//!``` +//! ### A hello world example of reading and writing avro data files + +//!```rust +//!use avrow::{Reader, Schema, Writer, from_value}; +//!use std::str::FromStr; +//!use std::error::Error; +//! +//!use std::io::Cursor; +//! +//!fn main() -> Result<(), Box> { +//! // Writing data +//! +//! // Create a schema +//! let schema = Schema::from_str(r##""null""##)?; +//! // Create writer using schema and provide a buffer to write to +//! let mut writer = Writer::new(&schema, vec![])?; +//! // Write the data using append +//! writer.serialize(())?; +//! // or serialize +//! writer.serialize(())?; +//! // retrieve the underlying buffer using the into_inner method. +//! let buf = writer.into_inner()?; +//! +//! // Reading data +//! +//! // Create Reader by providing a Read wrapped version of `buf` +//! let reader = Reader::new(buf.as_slice())?; +//! // Use iterator for reading data in an idiomatic manner. +//! for i in reader { +//! // reading values can fail due to decoding errors, so the return value of iterator is a Option> +//! // it allows one to examine the failure reason on the underlying avro reader. +//! dbg!(&i); +//! // This value can be converted to a native Rust type using from_value method from the serde impl. +//! let _: () = from_value(&i)?; +//! } +//! +//! Ok(()) +//!} +//! +//!``` + +// TODO update logo +#![doc(html_favicon_url = "")] +#![doc(html_logo_url = "assets/avrow_logo.png")] +#![deny(missing_docs)] +#![recursion_limit = "1024"] +#![deny(unused_must_use)] +// #![deny(warnings)] + +mod codec; +pub mod config; +mod error; +mod reader; +mod schema; +mod serde_avro; +mod util; +mod value; +mod writer; + +pub use codec::Codec; +pub use error::AvrowErr; +pub use reader::from_value; +pub use reader::Header; +pub use reader::Reader; +pub use schema::Schema; +pub use serde_avro::to_value; +pub use value::Record; +pub use value::Value; +pub use writer::Writer; +pub use writer::WriterBuilder; diff --git a/src/reader.rs b/src/reader.rs new file mode 100644 index 0000000..8052d36 --- /dev/null +++ b/src/reader.rs @@ -0,0 +1,707 @@ +use crate::codec::Codec; +use crate::config::DEFAULT_FLUSH_INTERVAL; +use crate::error; +use crate::schema; +use crate::serde_avro; +use crate::util::{decode_bytes, decode_string}; +use crate::value; +use byteorder::LittleEndian; +use byteorder::ReadBytesExt; +use error::AvrowErr; +use indexmap::IndexMap; +use integer_encoding::VarIntReader; +use schema::Registry; +use schema::Schema; +use schema::Variant; +use serde::Deserialize; +use serde_avro::SerdeReader; +use std::collections::HashMap; +use std::convert::TryFrom; +use std::io::Cursor; +use std::io::Read; +use std::io::{Error, ErrorKind}; +use std::str; +use std::str::FromStr; +use value::{FieldValue, Record, Value}; + +/// Reader is the primary interface for reading data from an avro datafile. +pub struct Reader { + source: R, + header: Header, + // TODO when reading data call resolve schema https://avro.apache.org/docs/1.8.2/spec.html#Schema+Resolution + // This is the schema after it has been resolved using both reader and writer schema + // NOTE: This is a partially resolved schema + // schema: Option, + // TODO this is for experimental purposes, ideally we can just use references + reader_schema: Option, + block_buffer: Cursor>, + entries_in_block: u64, +} + +impl Reader +where + R: Read, +{ + /// Creates a Reader from an avro encoded readable buffer. + pub fn new(mut avro_source: R) -> Result { + let header = Header::from_reader(&mut avro_source)?; + Ok(Reader { + source: avro_source, + header, + reader_schema: None, + block_buffer: Cursor::new(vec![0u8; DEFAULT_FLUSH_INTERVAL]), + entries_in_block: 0, + }) + } + + /// Create a Reader with the given reader schema and a readable buffer. + pub fn with_schema(mut source: R, reader_schema: Schema) -> Result { + let header = Header::from_reader(&mut source)?; + + Ok(Reader { + source, + header, + reader_schema: Some(reader_schema), + block_buffer: Cursor::new(vec![0u8; DEFAULT_FLUSH_INTERVAL]), + entries_in_block: 0, + }) + } + + // TODO optimize based on benchmarks + fn next_block(&mut self) -> Result<(), std::io::Error> { + // if no more bytes to read, read_varint below returns an EOF + let entries_in_block: i64 = self.source.read_varint()?; + self.entries_in_block = entries_in_block as u64; + + let block_stream_len: i64 = self.source.read_varint()?; + + let mut compressed_block = vec![0u8; block_stream_len as usize]; + self.source.read_exact(&mut compressed_block)?; + + self.header + .codec + .decode(compressed_block, &mut self.block_buffer) + .map_err(|e| { + Error::new( + ErrorKind::Other, + format!("Failed decoding block data with codec, {:?}", e), + ) + })?; + + // Ready for reading from block + self.block_buffer.set_position(0); + + let mut sync_marker_buf = [0u8; 16]; + let _ = self.source.read_exact(&mut sync_marker_buf); + + if sync_marker_buf != self.header.sync_marker { + let err = Error::new( + ErrorKind::Other, + "Sync marker does not match as expected while reading", + ); + return Err(err); + } + + Ok(()) + } + + /// Retrieves a reference to the header metadata map. + pub fn meta(&self) -> &HashMap> { + self.header.metadata() + } +} + +/// `from_value` is the serde API for deserialization of avro encoded data to native Rust types. +pub fn from_value<'de, D: Deserialize<'de>>( + value: &'de Result, +) -> Result { + match value { + Ok(v) => { + let mut serde_reader = SerdeReader::new(v); + D::deserialize(&mut serde_reader) + } + Err(e) => Err(AvrowErr::UnexpectedAvroValue { + value: e.to_string(), + }), + } +} + +impl<'a, 's, R: Read> Iterator for Reader { + type Item = Result; + + fn next(&mut self) -> Option { + // invariant: True on start and end of an avro datafile + if self.entries_in_block == 0 { + if let Err(e) = self.next_block() { + // marks the end of the avro datafile + if let std::io::ErrorKind::UnexpectedEof = e.kind() { + return None; + } else { + return Some(Err(AvrowErr::DecodeFailed(e))); + } + } + } + + let writer_schema = &self.header.schema; + let w_cxt = &writer_schema.cxt; + let reader_schema = &self.reader_schema; + let value = if let Some(r_schema) = reader_schema { + let r_cxt = &r_schema.cxt; + decode_with_resolution( + &r_schema.variant, + &writer_schema.variant, + &r_cxt, + &w_cxt, + &mut self.block_buffer, + ) + } else { + // decode without the reader schema + decode(&writer_schema.variant, &mut self.block_buffer, &w_cxt) + }; + + self.entries_in_block -= 1; + + if let Err(e) = value { + return Some(Err(e)); + } + + Some(value) + } +} + +// Reads places priority on reader's schema when passing any schema context if a reader schema is provided. +pub(crate) fn decode_with_resolution( + r_schema: &Variant, + w_schema: &Variant, + r_cxt: &Registry, + w_cxt: &Registry, + reader: &mut R, +) -> Result { + // LHS: Writer schema, RHS: Reader schema + let value = match (w_schema, r_schema) { + (Variant::Null, Variant::Null) => Value::Null, + (Variant::Boolean, Variant::Boolean) => { + let mut buf = [0u8; 1]; + reader + .read_exact(&mut buf) + .map_err(AvrowErr::DecodeFailed)?; + match buf { + [0x00] => Value::Boolean(false), + [0x01] => Value::Boolean(true), + _o => { + return Err(AvrowErr::DecodeFailed(Error::new( + ErrorKind::InvalidData, + "expecting a 0x00 or 0x01 as a byte for boolean value", + ))) + } + } + } + (Variant::Int, Variant::Int) => { + Value::Int(reader.read_varint().map_err(AvrowErr::DecodeFailed)?) + } + // int is promotable to long, float, or double (we read as int and cast to promotable.) + (Variant::Int, Variant::Long) => Value::Long( + reader + .read_varint::() + .map_err(AvrowErr::DecodeFailed)? as i64, + ), + (Variant::Int, Variant::Float) => Value::Float( + reader + .read_varint::() + .map_err(AvrowErr::DecodeFailed)? as f32, + ), + (Variant::Int, Variant::Double) => Value::Double( + reader + .read_varint::() + .map_err(AvrowErr::DecodeFailed)? as f64, + ), + (Variant::Long, Variant::Long) => { + Value::Long(reader.read_varint().map_err(AvrowErr::DecodeFailed)?) + } + // long is promotable to float or double + (Variant::Long, Variant::Float) => Value::Float( + reader + .read_varint::() + .map_err(AvrowErr::DecodeFailed)? as f32, + ), + (Variant::Long, Variant::Double) => Value::Double( + reader + .read_varint::() + .map_err(AvrowErr::DecodeFailed)? as f64, + ), + (Variant::Float, Variant::Float) => Value::Float( + reader + .read_f32::() + .map_err(AvrowErr::DecodeFailed)?, + ), + (Variant::Double, Variant::Double) => Value::Double( + reader + .read_f64::() + .map_err(AvrowErr::DecodeFailed)?, + ), + // float is promotable to double + (Variant::Float, Variant::Double) => Value::Double( + reader + .read_f32::() + .map_err(AvrowErr::DecodeFailed)? as f64, + ), + (Variant::Bytes, Variant::Bytes) => Value::Bytes(decode_bytes(reader)?), + // bytes is promotable to string + (Variant::Bytes, Variant::Str) => { + let bytes = decode_bytes(reader)?; + let s = str::from_utf8(&bytes).map_err(|_e| { + let err = Error::new(ErrorKind::InvalidData, "failed converting bytes to string"); + AvrowErr::DecodeFailed(err) + })?; + + Value::Str(s.to_string()) + } + (Variant::Str, Variant::Str) => { + let buf = decode_bytes(reader)?; + let s = str::from_utf8(&buf).map_err(|_e| { + let err = Error::new(ErrorKind::InvalidData, "failed converting bytes to string"); + AvrowErr::DecodeFailed(err) + })?; + Value::Str(s.to_string()) + } + // string is promotable to bytes + (Variant::Str, Variant::Bytes) => { + let buf = decode_bytes(reader)?; + Value::Bytes(buf) + } + (Variant::Array { items: w_items }, Variant::Array { items: r_items }) => { + if w_items == r_items { + let block_count: i64 = reader.read_varint().map_err(AvrowErr::DecodeFailed)?; + let mut v = Vec::with_capacity(block_count as usize); + + for _ in 0..block_count { + let decoded = + decode_with_resolution(&*r_items, &*w_items, r_cxt, w_cxt, reader)?; + v.push(decoded); + } + + Value::Array(v) + } else { + return Err(AvrowErr::ArrayItemsMismatch); + } + } + // Resolution rules + // if both are records: + // * The ordering of fields may be different: fields are matched by name. [1] + // * Schemas for fields with the same name in both records are resolved recursively. [2] + // * If the writer's record contains a field with a name not present in the reader's record, + // the writer's value for that field is ignored. [3] + // * If the reader's record schema has a field that contains a default value, + // and writer's schema does not have a field with the same name, + // then the reader should use the default value from its field. [4] + // * If the reader's record schema has a field with no default value, + // and writer's schema does not have a field with the same name, an error is signalled. [5] + ( + Variant::Record { + name: writer_name, + fields: writer_fields, + .. + }, + Variant::Record { + name: reader_name, + fields: reader_fields, + .. + }, + ) => { + // [1] + let reader_name = reader_name.fullname(); + let writer_name = writer_name.fullname(); + if writer_name != reader_name { + return Err(AvrowErr::RecordNameMismatch); + } + + let mut rec = Record::new(&reader_name); + for f in reader_fields { + let reader_fieldname = f.0.as_str(); + let reader_field = f.1; + // [3] + if let Some(wf) = writer_fields.get(reader_fieldname) { + // [2] + let f_decoded = + decode_with_resolution(&reader_field.ty, &wf.ty, r_cxt, w_cxt, reader)?; + rec.insert(&reader_fieldname, f_decoded)?; + } else { + // [4] + let default_field = f.1; + if let Some(a) = &default_field.default { + rec.insert(&reader_fieldname, a.clone())?; + } else { + // [5] + return Err(AvrowErr::FieldNotFound); + } + } + } + + return Ok(Value::Record(rec)); + } + ( + Variant::Enum { + name: w_name, + symbols: w_symbols, + .. + }, + Variant::Enum { + name: r_name, + symbols: r_symbols, + .. + }, + ) => { + if w_name.fullname() != r_name.fullname() { + return Err(AvrowErr::EnumNameMismatch); + } + + let idx: i32 = reader.read_varint().map_err(AvrowErr::DecodeFailed)?; + let idx = idx as usize; + if idx >= w_symbols.len() { + return Err(AvrowErr::InvalidEnumSymbolIdx( + idx, + format!("{:?}", w_symbols), + )); + } + + let symbol = r_symbols.get(idx as usize); + if let Some(s) = symbol { + return Ok(Value::Enum(s.to_string())); + } else { + return Err(AvrowErr::EnumSymbolNotFound { idx }); + } + } + ( + Variant::Fixed { + name: w_name, + size: w_size, + }, + Variant::Fixed { + name: r_name, + size: r_size, + }, + ) => { + if w_name.fullname() != r_name.fullname() && w_size != r_size { + return Err(AvrowErr::FixedSchemaNameMismatch); + } else { + let mut fixed = vec![0u8; *r_size]; + reader + .read_exact(&mut fixed) + .map_err(AvrowErr::DecodeFailed)?; + Value::Fixed(fixed) + } + } + ( + Variant::Map { + values: writer_values, + }, + Variant::Map { + values: reader_values, + }, + ) => { + // here equality will be based + if writer_values == reader_values { + let block_count: i32 = reader.read_varint().map_err(AvrowErr::DecodeFailed)?; + let mut hm = HashMap::new(); + for _ in 0..block_count { + let key = decode_string(reader)?; + let value = decode(reader_values, reader, r_cxt)?; + hm.insert(key, value); + } + Value::Map(hm) + } else { + return Err(AvrowErr::MapSchemaMismatch); + } + } + ( + Variant::Union { + variants: writer_variants, + }, + Variant::Union { + variants: reader_variants, + }, + ) => { + let union_idx: i32 = reader.read_varint().map_err(AvrowErr::DecodeFailed)?; + if let Some(writer_schema) = writer_variants.get(union_idx as usize) { + for i in reader_variants { + if i == writer_schema { + return decode(i, reader, r_cxt); + } + } + } + + return Err(AvrowErr::UnionSchemaMismatch); + } + /* + if reader's is a union but writer's is not. The first schema in the reader's union that matches + the writer's schema is recursively resolved against it. If none match, an error is signalled. + */ + ( + writer_schema, + Variant::Union { + variants: reader_variants, + }, + ) => { + for i in reader_variants { + if i == writer_schema { + return decode(i, reader, r_cxt); + } + } + + return Err(AvrowErr::WriterNotInReader); + } + /* + if writer's schema is a union, but reader's is not. + If the reader's schema matches the selected writer's schema, + it is recursively resolved against it. If they do not match, an error is signalled. + */ + ( + Variant::Union { + variants: writer_variants, + }, + reader_schema, + ) => { + // Read the index value in the schema + let union_idx: i32 = reader.read_varint().map_err(AvrowErr::DecodeFailed)?; + let schema = writer_variants.get(union_idx as usize); + if let Some(s) = schema { + if s == reader_schema { + return decode(reader_schema, reader, r_cxt); + } + } + let writer_schema = format!("writer schema: {:?}", writer_variants); + let reader_schema = format!("reader schema: {:?}", reader_schema); + return Err(AvrowErr::SchemaResolutionFailed( + reader_schema, + writer_schema, + )); + } + other => { + return Err(AvrowErr::SchemaResolutionFailed( + format!("{:?}", other.0), + format!("{:?}", other.1), + )) + } + }; + + Ok(value) +} + +pub(crate) fn decode( + schema: &Variant, + reader: &mut R, + r_cxt: &Registry, +) -> Result { + let value = match schema { + Variant::Null => Value::Null, + Variant::Boolean => { + let mut buf = [0u8; 1]; + reader + .read_exact(&mut buf) + .map_err(AvrowErr::DecodeFailed)?; + match buf { + [0x00] => Value::Boolean(false), + [0x01] => Value::Boolean(true), + _ => { + return Err(AvrowErr::DecodeFailed(Error::new( + ErrorKind::InvalidData, + "Invalid boolean value, expected a 0x00 or a 0x01", + ))) + } + } + } + Variant::Int => Value::Int(reader.read_varint().map_err(AvrowErr::DecodeFailed)?), + Variant::Double => Value::Double( + reader + .read_f64::() + .map_err(AvrowErr::DecodeFailed)?, + ), + Variant::Long => Value::Long(reader.read_varint().map_err(AvrowErr::DecodeFailed)?), + Variant::Float => Value::Float( + reader + .read_f32::() + .map_err(AvrowErr::DecodeFailed)?, + ), + Variant::Str => { + let buf = decode_bytes(reader)?; + let s = str::from_utf8(&buf).map_err(|_e| { + let err = Error::new( + ErrorKind::InvalidData, + "failed converting from bytes to string", + ); + AvrowErr::DecodeFailed(err) + })?; + Value::Str(s.to_string()) + } + Variant::Array { items } => { + let block_count: i64 = reader.read_varint().map_err(AvrowErr::DecodeFailed)?; + + if block_count == 0 { + // FIXME do we send an empty array? + return Ok(Value::Array(Vec::new())); + } + + let mut it = Vec::with_capacity(block_count as usize); + for _ in 0..block_count { + let decoded = decode(&**items, reader, r_cxt)?; + it.push(decoded); + } + + Value::Array(it) + } + Variant::Bytes => Value::Bytes(decode_bytes(reader)?), + Variant::Map { values } => { + let block_count: usize = reader.read_varint().map_err(AvrowErr::DecodeFailed)?; + let mut hm = HashMap::new(); + for _ in 0..block_count { + let key = decode_string(reader)?; + let value = decode(values, reader, r_cxt)?; + hm.insert(key, value); + } + + Value::Map(hm) + } + Variant::Record { name, fields, .. } => { + let mut v = IndexMap::with_capacity(fields.len()); + for (field_name, field) in fields { + let field_name = field_name.to_string(); + let field_value = decode(&field.ty, reader, r_cxt)?; + let field_value = FieldValue::new(field_value); + v.insert(field_name, field_value); + } + + let rec = Record { + name: name.fullname(), + fields: v, + }; + Value::Record(rec) + } + Variant::Union { variants } => { + let variant_idx: i64 = reader.read_varint().map_err(AvrowErr::DecodeFailed)?; + decode(&variants[variant_idx as usize], reader, r_cxt)? + } + Variant::Named(schema_name) => { + let schema_variant = r_cxt + .get(schema_name) + .ok_or(AvrowErr::NamedSchemaNotFound)?; + decode(schema_variant, reader, r_cxt)? + } + a => { + return Err(AvrowErr::DecodeFailed(Error::new( + ErrorKind::InvalidData, + format!("Read failed for schema {:?}", a), + ))) + } + }; + + Ok(value) +} + +/// Header represents the avro datafile header. +#[derive(Debug)] +pub struct Header { + /// Writer's schema + pub(crate) schema: Schema, + /// A Map which stores avro metadata, like `avro.codec` and `avro.schema`. + /// Additional key values can be added through the + /// [WriterBuilder](struct.WriterBuilder.html)'s `set_metadata` method. + pub(crate) metadata: HashMap>, + /// A unique 16 byte sequence for file integrity when writing avro data to file. + pub(crate) sync_marker: [u8; 16], + /// codec parsed from the datafile + pub(crate) codec: Codec, +} + +fn decode_header_map(reader: &mut R) -> Result>, AvrowErr> +where + R: Read, +{ + let count: i64 = reader.read_varint().map_err(AvrowErr::DecodeFailed)?; + let count = count as usize; + let mut map = HashMap::with_capacity(count); + + for _ in 0..count { + let key = decode_string(reader)?; + let val = decode_bytes(reader)?; + map.insert(key, val); + } + + let _map_end: i64 = reader.read_varint().map_err(AvrowErr::DecodeFailed)?; + + Ok(map) +} + +impl Header { + /// Reads the header from an avro datafile + pub fn from_reader(reader: &mut R) -> Result { + let mut magic_buf = [0u8; 4]; + reader + .read_exact(&mut magic_buf[..]) + .map_err(|_| AvrowErr::HeaderDecodeFailed)?; + + if &magic_buf != b"Obj\x01" { + return Err(AvrowErr::InvalidDataFile); + } + + let map = decode_header_map(reader)?; + + let mut sync_marker = [0u8; 16]; + let _ = reader + .read_exact(&mut sync_marker) + .map_err(|_| AvrowErr::HeaderDecodeFailed)?; + + let schema_bytes = map.get("avro.schema").ok_or(AvrowErr::HeaderDecodeFailed)?; + + let schema = str::from_utf8(schema_bytes) + .map(Schema::from_str) + .map_err(|_| AvrowErr::HeaderDecodeFailed)??; + + let codec = if let Some(c) = map.get("avro.codec") { + match std::str::from_utf8(c) { + Ok(s) => Codec::try_from(s)?, + Err(s) => return Err(AvrowErr::UnsupportedCodec(s.to_string())), + } + } else { + Codec::Null + }; + + let header = Header { + schema, + metadata: map, + sync_marker, + codec, + }; + + Ok(header) + } + + /// Returns a reference to metadata from avro datafile header + pub fn metadata(&self) -> &HashMap> { + &self.metadata + } + + /// Returns a reference to the writer's schema in this header + pub fn schema(&self) -> &Schema { + &self.schema + } +} + +#[cfg(test)] +mod tests { + use crate::Reader; + #[test] + fn has_required_headers() { + let data = vec![ + 79, 98, 106, 1, 4, 22, 97, 118, 114, 111, 46, 115, 99, 104, 101, 109, 97, 32, 123, 34, + 116, 121, 112, 101, 34, 58, 34, 98, 121, 116, 101, 115, 34, 125, 20, 97, 118, 114, 111, + 46, 99, 111, 100, 101, 99, 14, 100, 101, 102, 108, 97, 116, 101, 0, 145, 85, 112, 15, + 87, 201, 208, 26, 183, 148, 48, 236, 212, 250, 38, 208, 2, 18, 227, 97, 96, 100, 98, + 102, 97, 5, 0, 145, 85, 112, 15, 87, 201, 208, 26, 183, 148, 48, 236, 212, 250, 38, + 208, + ]; + + let reader = Reader::new(data.as_slice()).unwrap(); + assert!(reader.meta().contains_key("avro.codec")); + assert!(reader.meta().contains_key("avro.schema")); + } +} diff --git a/src/schema/canonical.rs b/src/schema/canonical.rs new file mode 100644 index 0000000..bfc5fcd --- /dev/null +++ b/src/schema/canonical.rs @@ -0,0 +1,259 @@ +use crate::schema::Name; +use crate::serde_avro::AvrowErr; +use serde_json::json; +use serde_json::Value as JsonValue; +use std::cmp::PartialEq; + +// wrap overflow of 0xc15d213aa4d7a795 +const EMPTY: i64 = -4513414715797952619; + +static FP_TABLE: once_cell::sync::Lazy<[i64; 256]> = { + use once_cell::sync::Lazy; + Lazy::new(|| { + let mut fp_table: [i64; 256] = [0; 256]; + for i in 0..256 { + let mut fp = i; + for _ in 0..8 { + fp = (fp as u64 >> 1) as i64 ^ (EMPTY & -(fp & 1)); + } + fp_table[i as usize] = fp; + } + fp_table + }) +}; + +// relevant fields and in order fields according to spec +const RELEVANT_FIELDS: [&str; 7] = [ + "name", "type", "fields", "symbols", "items", "values", "size", +]; +/// Represents canonical form of an avro schema. This representation removes irrelevant fields +/// such as docs and aliases in the schema. +/// Fingerprinting methods are available on this instance. +#[derive(Debug, PartialEq)] +pub struct CanonicalSchema(pub(crate) JsonValue); + +impl std::fmt::Display for CanonicalSchema { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let c = serde_json::to_string_pretty(&self.0); + write!(f, "{}", c.map_err(|_| std::fmt::Error)?) + } +} + +impl CanonicalSchema { + #[cfg(feature = "sha2")] + pub fn sha256(&self) -> Vec { + use shatwo::{Digest, Sha256}; + let mut hasher = Sha256::new(); + hasher.update(self.0.to_string()); + let result = hasher.finalize(); + result.to_vec() + } + + #[cfg(feature = "md5")] + pub fn md5(&self) -> Vec { + let v = mdfive::compute(self.0.to_string().as_bytes()); + v.to_vec() + } + + pub fn rabin64(&self) -> i64 { + let buf = self.0.to_string(); + let buf = buf.as_bytes(); + let mut fp: i64 = EMPTY; + + buf.iter().for_each(|b| { + let idx = ((fp ^ *b as i64) & 0xff) as usize; + fp = (fp as u64 >> 8) as i64 ^ FP_TABLE[idx]; + }); + + fp + } +} + +// TODO unescape \uXXXX +// pub fn normalize_unescape(s: &str) -> &str { +// s +// } + +// [FULLNAMES] - traverse the `type` field and replace names with fullnames +pub fn normalize_name( + json_map: &mut serde_json::map::Map, + enclosing_namespace: Option<&str>, +) -> Result<(), AvrowErr> { + let name = Name::from_json_mut(json_map, enclosing_namespace)?; + + json_map["name"] = json!(name.fullname()); + + if let Some(JsonValue::Array(fields)) = json_map.get_mut("fields") { + for f in fields.iter_mut() { + if let JsonValue::Object(ref mut o) = f { + if let Some(JsonValue::Object(ref mut o)) = o.get_mut("type") { + if o.contains_key("name") { + normalize_name(o, name.namespace())?; + } + } + } + } + } + + Ok(()) +} + +// [STRIP] +pub fn normalize_strip( + schema: &mut serde_json::map::Map, +) -> Result<(), AvrowErr> { + if schema.contains_key("doc") { + schema.remove("doc").ok_or(AvrowErr::ParsingCanonicalForm)?; + } + if schema.contains_key("aliases") { + schema + .remove("aliases") + .ok_or(AvrowErr::ParsingCanonicalForm)?; + } + + Ok(()) +} + +type JsonMap = serde_json::map::Map; + +pub fn order_fields(json: &JsonMap) -> Result { + let mut ordered = JsonMap::new(); + + for field in RELEVANT_FIELDS.iter() { + if let Some(value) = json.get(*field) { + match value { + JsonValue::Object(m) => { + ordered.insert(field.to_string(), json!(order_fields(m)?)); + } + JsonValue::Array(a) => { + let mut obj_arr = vec![]; + for field in a { + match field { + JsonValue::Object(m) => { + obj_arr.push(json!(order_fields(m)?)); + } + _ => { + obj_arr.push(field.clone()); + } + } + } + + ordered.insert(field.to_string(), json!(obj_arr)); + } + _ => { + ordered.insert(field.to_string(), value.clone()); + } + } + } + } + + Ok(ordered) +} + +// The following steps in parsing canonical form are handled by serde so we rely on that. +// [INTEGERS] - serde will not parse a string with a zero prefixed integer. +// [WHITESPACE] - serde also eliminates whitespace. +// [STRINGS] - TODO in `normalize_unescape` +// For rest of the steps, we implement them as below +pub(crate) fn normalize_schema(json_schema: &JsonValue) -> Result { + match json_schema { + // Normalize a complex schema + JsonValue::Object(ref scm) => { + // [PRIMITIVES] + if let Some(JsonValue::String(s)) = scm.get("type") { + match s.as_ref() { + "record" | "enum" | "array" | "maps" | "union" | "fixed" => {} + _ => { + return Ok(json!(s)); + } + } + } + + let mut schema = scm.clone(); + // [FULLNAMES] + if schema.contains_key("name") { + normalize_name(&mut schema, None)?; + } + // [ORDER] + let mut schema = order_fields(&schema)?; + // [STRIP] + normalize_strip(&mut schema)?; + Ok(json!(schema)) + } + // [PRIMITIVES] + // Normalize a primitive schema + a @ JsonValue::String(_) => Ok(json!(a)), + // Normalize a union schema + JsonValue::Array(v) => { + let mut variants = Vec::with_capacity(v.len()); + for i in v { + let normalized = normalize_schema(i)?; + variants.push(normalized); + } + Ok(json!(v)) + } + _other => Err(AvrowErr::UnknownSchema), + } +} + +#[cfg(test)] +mod tests { + use crate::Schema; + use std::str::FromStr; + #[test] + fn canonical_primitives() { + let schema_str = r##"{"type": "null"}"##; + let _ = Schema::from_str(schema_str).unwrap(); + } + + #[test] + #[cfg(feature = "fingerprint")] + fn canonical_schema_sha256_fingerprint() { + let header_schema = r##"{"type": "record", "name": "org.apache.avro.file.Header", + "fields" : [ + {"name": "magic", "type": {"type": "fixed", "name": "Magic", "size": 4}}, + {"name": "meta", "type": {"type": "map", "values": "bytes"}}, + {"name": "sync", "type": {"type": "fixed", "name": "Sync", "size": 16}} + ] + }"##; + let schema = Schema::from_str(header_schema).unwrap(); + let canonical = schema.canonical_form(); + + let expected = "809bed56cf47c84e221ad8b13e28a66ed9cd6b1498a43bad9aa0c868205e"; + let found = canonical.sha256(); + let mut fingerprint_str = String::new(); + for i in found { + let a = format!("{:x}", i); + fingerprint_str.push_str(&a); + } + + assert_eq!(expected, fingerprint_str); + } + + #[test] + #[cfg(feature = "fingerprint")] + fn schema_rabin_fingerprint() { + let schema = r##""null""##; + let expected = "0x63dd24e7cc258f8a"; + let schema = Schema::from_str(schema).unwrap(); + let canonical = schema.canonical_form(); + let actual = format!("0x{:x}", canonical.rabin64()); + assert_eq!(expected, actual); + } + + #[test] + #[cfg(feature = "fingerprint")] + fn schema_md5_fingerprint() { + let schema = r##""null""##; + let expected = "9b41ef67651c18488a8b8bb67c75699"; + let schema = Schema::from_str(schema).unwrap(); + let canonical = schema.canonical_form(); + let actual = canonical.md5(); + let mut fingerprint_str = String::new(); + for i in actual { + let a = format!("{:x}", i); + fingerprint_str.push_str(&a); + } + assert_eq!(expected, fingerprint_str); + } +} diff --git a/src/schema/common.rs b/src/schema/common.rs new file mode 100644 index 0000000..cf1a893 --- /dev/null +++ b/src/schema/common.rs @@ -0,0 +1,360 @@ +// This module contains definition of types that are common across a subset of +// avro schemas. + +use crate::error::AvrowErr; +use crate::schema::Variant; +use crate::value::Value; +use serde_json::Value as JsonValue; +use std::fmt::{self, Display}; +use std::str::FromStr; + +/////////////////////////////////////////////////////////////////////////////// +/// Name implementation for named types: record, fixed, enum +/////////////////////////////////////////////////////////////////////////////// + +pub(crate) fn validate_name(idx: usize, name: &str) -> Result<(), AvrowErr> { + if name.contains('.') + || (name.starts_with(|a: char| a.is_ascii_digit()) && idx == 0) + || name.is_empty() + || !name.chars().any(|a| a.is_ascii_alphanumeric() || a == '_') + { + Err(AvrowErr::InvalidName) + } else { + Ok(()) + } +} + +// Follows the grammer: | [()*] +pub(crate) fn validate_namespace(s: &str) -> Result<(), AvrowErr> { + let split = s.split('.'); + for (i, n) in split.enumerate() { + let _ = validate_name(i, n).map_err(|_| AvrowErr::InvalidNamespace)?; + } + Ok(()) +} + +/// Represents `fullname` attribute and its constituents +/// of a named avro type i.e, Record, Fixed and Enum +#[derive(Debug, Clone, Eq, PartialOrd, Ord)] +pub struct Name { + pub(crate) name: String, + pub(crate) namespace: Option, +} + +impl Name { + // Creates an validates the name. This will also extract the namespace if a dot is present in `name` + // Any further calls to set_namespace, will be a noop if the name already contains a dot. + pub(crate) fn new(name: &str) -> Result { + let mut namespace = None; + let name = if name.contains('.') { + // should not have multiple dots and dots in end or start + let _ = validate_namespace(name)?; + // strip namespace + let idx = name.rfind('.').unwrap(); // we check for ., so it's okay + namespace = Some(name[..idx].to_string()); + let name = &name[idx + 1..]; + validate_name(0, name)?; + name + } else { + // TODO perform namespace lookups from enclosing schema if any + // This will require us to pass context to this method. + // Update: this is now handled by from_json method as that's called from places + // where we have context on most tightly enclosing schema. + validate_name(0, name)?; + name + }; + + Ok(Self { + name: name.to_string(), + namespace, + }) + } + + // TODO also parse namespace from json value + pub(crate) fn from_json( + json: &serde_json::map::Map, + enclosing_namespace: Option<&str>, + ) -> Result { + let mut name = if let Some(JsonValue::String(ref s)) = json.get("name") { + Name::new(s) + } else { + return Err(AvrowErr::NameParseFailed); + }?; + + // As per spec, If the name field has a dot, that is a fullname. any namespace provided is ignored. + // If no namespace was extracted from the name itself (i.e., name did not contain a dot) + // we then see if we have the namespace field on the json itself + // otherwise we use the enclosing namespace if that is a Some(namespace) + if name.namespace.is_none() { + if let Some(namespace) = json.get("namespace") { + if let JsonValue::String(s) = namespace { + validate_namespace(s)?; + name.set_namespace(s)?; + } + } else if let Some(a) = enclosing_namespace { + validate_namespace(a)?; + name.set_namespace(a)?; + } + } + + Ok(name) + } + + pub(crate) fn namespace(&self) -> Option<&str> { + self.namespace.as_deref() + } + + // receives a mutable json and parses a Name and removes namespace. Used for canonicalization. + // TODO change as above from_json method, should take enclosing namespace. + pub(crate) fn from_json_mut( + json: &mut serde_json::map::Map, + enclosing_namespace: Option<&str>, + ) -> Result { + let mut name = if let Some(JsonValue::String(ref s)) = json.get("name") { + Name::new(s) + } else { + return Err(AvrowErr::NameParseFailed); + }?; + + if name.namespace.is_none() { + if let Some(namespace) = json.get("namespace") { + if let JsonValue::String(s) = namespace { + validate_namespace(s)?; + name.set_namespace(s)?; + json.remove("namespace"); + } + } else if let Some(a) = enclosing_namespace { + validate_namespace(a)?; + name.set_namespace(a)?; + } + } + + // if let Some(namespace) = json.get("namespace") { + // if let JsonValue::String(s) = namespace { + // name.set_namespace(s)?; + // json.remove("namespace"); + // } + // } + + Ok(name) + } + + pub(crate) fn set_namespace(&mut self, namespace: &str) -> Result<(), AvrowErr> { + // empty string is a null namespace + if namespace.is_empty() { + return Ok(()); + } + + validate_namespace(namespace)?; + // If a namespace was already extracted when constructing name (name had a dot) + // then this is a noop + if self.namespace.is_none() { + let _ = validate_namespace(namespace)?; + self.namespace = Some(namespace.to_string()); + } + Ok(()) + } + + // TODO according to Rust convention, item path separators are :: instead of . + // TODO should we add a configurable separator. + // TODO should do namespace lookup from enclosing name schema if applicable. (pass enclosing schema as a context) + pub(crate) fn fullname(&self) -> String { + // if self.name.contains(".") { + // self.name.to_string() + // } else if let Some(n) = &self.namespace { + // if n.is_empty() { + // // According to spec, it's fine to put "" as a namespace, which becomes a null namespace + // format!("{}", self.name) + // } else { + // format!("{}.{}", n, self.name) + // } + // } else { + // // The case when only name exists. + // // TODO As of now we just return without any enclosing namespace. + // // TODO pass the most tightly enclosing namespace here when only name is provided. + // self.name.to_string() + // } + if let Some(n) = &self.namespace { + if n.is_empty() { + // According to spec, it's fine to put "" as a namespace, which becomes a null namespace + self.name.to_string() + } else { + format!("{}.{}", n, self.name) + } + } else { + self.name.to_string() + } + } +} + +impl Display for Name { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if let Some(ref namespace) = self.namespace { + write!(f, "{}.{}", namespace, self.name) + } else { + write!(f, "{}", self.name) + } + } +} + +impl FromStr for Name { + type Err = AvrowErr; + + fn from_str(s: &str) -> Result { + Name::new(s) + } +} + +impl std::convert::TryFrom<&str> for Name { + type Error = AvrowErr; + + fn try_from(value: &str) -> Result { + Name::new(value) + } +} + +impl PartialEq for Name { + fn eq(&self, other: &Self) -> bool { + self.fullname() == other.fullname() + } +} + +/////////////////////////////////////////////////////////////////////////////// +/// Ordering for record fields +/////////////////////////////////////////////////////////////////////////////// + +#[derive(Debug, PartialEq, Clone)] +pub enum Order { + Ascending, + Descending, + Ignore, +} + +impl FromStr for Order { + type Err = AvrowErr; + fn from_str(s: &str) -> Result { + match s { + "ascending" => Ok(Order::Ascending), + "descending" => Ok(Order::Descending), + "ignore" => Ok(Order::Ignore), + _ => Err(AvrowErr::UnknownFieldOrdering), + } + } +} + +/////////////////////////////////////////////////////////////////////////////// +/// Record field definition. +/////////////////////////////////////////////////////////////////////////////// + +#[derive(Debug, Clone)] +pub struct Field { + pub(crate) name: String, + pub(crate) ty: Variant, + pub(crate) default: Option, + pub(crate) order: Order, + pub(crate) aliases: Option>, +} + +// TODO do we also use order for equality? +impl std::cmp::PartialEq for Field { + fn eq(&self, other: &Self) -> bool { + self.name == other.name && self.ty == other.ty + } +} + +impl Field { + pub(crate) fn new( + name: &str, + ty: Variant, + default: Option, + order: Order, + aliases: Option>, + ) -> Result { + validate_name(0, name)?; + Ok(Field { + name: name.to_string(), + ty, + default, + order, + aliases, + }) + } +} + +#[cfg(test)] +mod tests { + use super::validate_namespace; + use super::Name; + + #[test] + #[should_panic(expected = "InvalidName")] + fn name_starts_with_number() { + Name::new("2org.apache.avro").unwrap(); + } + + #[test] + #[should_panic(expected = "InvalidNamespace")] + fn invalid_namespace() { + let mut name = Name::new("org.apache.avro").unwrap(); + name.set_namespace("23").unwrap(); + } + + #[test] + fn name_with_seperate_namespace() { + let mut name = Name::new("hello").unwrap(); + let _ = name.set_namespace("org.foo"); + assert_eq!("org.foo.hello", name.fullname().to_string()); + } + + #[test] + fn name_contains_dots() { + let name = Name::new("org.apache.avro").unwrap(); + assert_eq!("avro", name.name.to_string()); + assert_eq!("org.apache.avro", name.fullname().to_string()); + } + + #[test] + fn fullname_with_empty_namespace() { + let mut name = Name::new("org.apache.avro").unwrap(); + name.set_namespace("").unwrap(); + assert_eq!("org.apache.avro", name.fullname()); + } + + #[test] + fn multiple_dots_invalid() { + let a = "some.namespace..foo"; + assert!(validate_namespace(a).is_err()); + } + + #[test] + fn name_has_dot_and_namespace_present() { + let json_str = r##" + { + "name":"my.longlist", + "namespace":"com.some", + "type":"record" + } + "##; + let json: serde_json::Value = serde_json::from_str(json_str).unwrap(); + let name = Name::from_json(json.as_object().unwrap(), None).unwrap(); + assert_eq!(name.name, "longlist"); + assert_eq!(name.namespace, Some("my".to_string())); + assert_eq!(name.fullname(), "my.longlist"); + } + + #[test] + fn name_no_dot_and_namespace_present() { + let json_str = r##" + { + "name":"longlist", + "namespace":"com.some", + "type":"record" + } + "##; + let json: serde_json::Value = serde_json::from_str(json_str).unwrap(); + let name = Name::from_json(json.as_object().unwrap(), None).unwrap(); + assert_eq!(name.name, "longlist"); + assert_eq!(name.namespace, Some("com.some".to_string())); + assert_eq!(name.fullname(), "com.some.longlist"); + } +} diff --git a/src/schema/mod.rs b/src/schema/mod.rs new file mode 100644 index 0000000..224a2ac --- /dev/null +++ b/src/schema/mod.rs @@ -0,0 +1,258 @@ +//! Contains routines for parsing and validating an Avro schema. +//! Schemas in avro are written as JSON and can be provided as .avsc files +//! to a Writer or a Reader. + +pub mod common; +#[cfg(test)] +mod tests; +use crate::error::AvrowErr; +pub use common::Order; +mod canonical; +pub mod parser; +pub(crate) use parser::Registry; + +use crate::error::AvrowResult; +use crate::value::Value; +use canonical::normalize_schema; +use canonical::CanonicalSchema; +use common::{Field, Name}; +use indexmap::IndexMap; +use serde_json::{self, Value as JsonValue}; +use std::fmt::Debug; +use std::fs::OpenOptions; +use std::path::Path; + +/// A schema parsed from json value +#[derive(Debug, Clone, PartialEq)] +pub(crate) enum Variant { + Null, + Boolean, + Int, + Long, + Float, + Double, + Bytes, + Str, + Record { + name: Name, + aliases: Option>, + fields: IndexMap, + }, + Fixed { + name: Name, + size: usize, + }, + Enum { + name: Name, + aliases: Option>, + symbols: Vec, + }, + Map { + values: Box, + }, + Array { + items: Box, + }, + Union { + variants: Vec, + }, + Named(String), +} + +/// Represents the avro schema used to write encoded avro data +#[derive(Debug)] +pub struct Schema { + // TODO can remove this if not needed + inner: JsonValue, + // Schema context that has a lookup table to resolve named schema references + pub(crate) cxt: Registry, + // typed and stripped version of schema used internally. + pub(crate) variant: Variant, + // canonical form of schema. This is used for equality. + pub(crate) canonical: CanonicalSchema, +} + +impl PartialEq for Schema { + fn eq(&self, other: &Self) -> bool { + self.canonical == other.canonical + } +} + +impl std::str::FromStr for Schema { + type Err = AvrowErr; + /// Parse an avro schema from a json string + /// One can use Rust's raw string syntax (r##""##) to pass schema. + fn from_str(schema: &str) -> Result { + let schema_json = + serde_json::from_str(schema).map_err(|e| AvrowErr::SchemaParseErr(e.into()))?; + Schema::parse_imp(schema_json) + } +} + +impl Schema { + /// Parses an avro schema from a json description of schema in a file. + /// Alternatively, one can use the `FromStr` impl to create a `Schema` from a JSON string: + /// ``` + /// use std::str::FromStr; + /// use avrow::Schema; + /// + /// let schema = Schema::from_str(r##""null""##).unwrap(); + /// ``` + pub fn from_path + Debug>(path: P) -> AvrowResult { + let schema_file = OpenOptions::new() + .read(true) + .open(&path) + .map_err(AvrowErr::SchemaParseErr)?; + let value = + serde_json::from_reader(schema_file).map_err(|e| AvrowErr::SchemaParseErr(e.into()))?; + Schema::parse_imp(value) + } + + fn parse_imp(schema_json: JsonValue) -> AvrowResult { + let mut parser = Registry::new(); + let pcf = CanonicalSchema(normalize_schema(&schema_json)?); + // TODO see if we can use canonical form to parse variant + let variant = parser.parse_schema(&schema_json, None)?; + Ok(Schema { + inner: schema_json, + cxt: parser, + variant, + canonical: pcf, + }) + } + + pub(crate) fn as_bytes(&self) -> Vec { + format!("{}", self.inner).into_bytes() + } + + pub(crate) fn variant(&self) -> &Variant { + &self.variant + } + + #[inline(always)] + pub(crate) fn validate(&self, value: &Value) -> AvrowResult<()> { + self.variant.validate(value, &self.cxt) + } + + /// Returns the canonical form of an Avro schema + /// ```rust + /// use avrow::Schema; + /// use std::str::FromStr; + /// + /// let schema = Schema::from_str(r##" + /// { + /// "type": "record", + /// "name": "LongList", + /// "aliases": ["LinkedLongs"], + /// "fields" : [ + /// {"name": "value", "type": "long"}, + /// {"name": "next", "type": ["null", "LongList"] + /// }] + /// } + /// "##).unwrap(); + /// let canonical = schema.canonical_form(); + /// ``` + pub fn canonical_form(&self) -> &CanonicalSchema { + &self.canonical + } +} + +impl Variant { + pub fn validate(&self, value: &Value, cxt: &Registry) -> AvrowResult<()> { + let variant = self; + match (value, variant) { + (Value::Null, Variant::Null) + | (Value::Boolean(_), Variant::Boolean) + | (Value::Int(_), Variant::Int) + // long is promotable to float or double + | (Value::Long(_), Variant::Long) + | (Value::Long(_), Variant::Float) + | (Value::Long(_), Variant::Double) + // int is promotable to long, float or double + | (Value::Int(_), Variant::Long) + | (Value::Int(_), Variant::Float) + | (Value::Int(_), Variant::Double) + | (Value::Float(_), Variant::Float) + // float is promotable to double + | (Value::Float(_), Variant::Double) + | (Value::Double(_), Variant::Double) + | (Value::Str(_), Variant::Str) + // string is promotable to bytes + | (Value::Str(_), Variant::Bytes) + // bytes is promotable to string + | (Value::Bytes(_), Variant::Str) + | (Value::Bytes(_), Variant::Bytes) => {}, + (Value::Fixed(v), Variant::Fixed { size, .. }) + | (Value::Bytes(v), Variant::Fixed { size, .. }) => { + if v.len() != *size { + return Err(AvrowErr::FixedValueLenMismatch { + found: v.len(), + expected: *size, + }); + } + } + (Value::Record(rec), Variant::Record { ref fields, .. }) => { + for (fname, fvalue) in &rec.fields { + if let Some(ftype) = fields.get(fname) { + ftype.ty.validate(&fvalue.value, cxt)?; + } else { + return Err(AvrowErr::RecordFieldMissing); + } + } + } + (Value::Map(hmap), Variant::Map { values }) => { + return if let Some(v) = hmap.values().next() { + values.validate(v, cxt) + } else { + Err(AvrowErr::EmptyMap) + } + } + (Value::Enum(sym), Variant::Enum { symbols, .. }) if symbols.contains(sym) => { + return Ok(()) + } + (Value::Array(item), Variant::Array { items }) => { + return if let Some(v) = item.first() { + items.validate(v, cxt) + } else { + Err(AvrowErr::EmptyArray) + } + } + (v, Variant::Named(name)) => { + if let Some(schema) = cxt.get(&name) { + if schema.validate(v, cxt).is_ok() { + return Ok(()); + } + } + return Err(AvrowErr::NamedSchemaNotFoundForValue) + } + // Value `a` can be any of the above schemas + any named schema in the schema registry + (a, Variant::Union { variants }) => { + for s in variants.iter() { + if s.validate(a, cxt).is_ok() { + return Ok(()); + } + } + + return Err(AvrowErr::NotFoundInUnion) + } + + (v, s) => { + return Err(AvrowErr::SchemaDataValidationFailed( + format!("{:?}", v), + format!("{:?}", s), + )) + } + } + + Ok(()) + } + + fn get_named_mut(&mut self) -> Option<&mut Name> { + match self { + Variant::Record { name, .. } + | Variant::Fixed { name, .. } + | Variant::Enum { name, .. } => Some(name), + _ => None, + } + } +} diff --git a/src/schema/parser.rs b/src/schema/parser.rs new file mode 100644 index 0000000..adb3c38 --- /dev/null +++ b/src/schema/parser.rs @@ -0,0 +1,494 @@ +use super::common::{Field, Name, Order}; +use super::Variant; +use crate::error::io_err; +use crate::error::AvrowErr; +use crate::error::AvrowResult; +use crate::schema::common::validate_name; +use crate::value::FieldValue; +use crate::value::Value; +use indexmap::IndexMap; +use serde_json::{Map, Value as JsonValue}; +use std::borrow::ToOwned; +use std::collections::HashMap; + +// Wraps a { name -> schema } lookup table to aid parsing named references in complex schemas +// During parsing, the value for each key may get updated as a schema discovers +// more information about the schema during parsing. +#[derive(Debug, Clone)] +pub(crate) struct Registry { + // TODO: use a reference to Variant? + cxt: HashMap, +} + +impl Registry { + pub(crate) fn new() -> Self { + Self { + cxt: HashMap::new(), + } + } + + pub(crate) fn get<'a>(&'a self, name: &str) -> Option<&'a Variant> { + self.cxt.get(name) + } + + pub(crate) fn parse_schema( + &mut self, + value: &JsonValue, + enclosing_namespace: Option<&str>, + ) -> Result { + match value { + // Parse a complex schema + JsonValue::Object(ref schema) => self.parse_object(schema, enclosing_namespace), + // Parse a primitive schema, could also be a named schema reference + JsonValue::String(ref schema) => self.parse_primitive(&schema, enclosing_namespace), + // Parse a union schema + JsonValue::Array(ref schema) => self.parse_union(schema, enclosing_namespace), + _ => Err(AvrowErr::UnknownSchema), + } + } + + fn parse_union( + &mut self, + schema: &[JsonValue], + enclosing_namespace: Option<&str>, + ) -> Result { + let mut union_schema = vec![]; + for s in schema { + let parsed_schema = self.parse_schema(s, enclosing_namespace)?; + match parsed_schema { + Variant::Union { .. } => { + return Err(AvrowErr::DuplicateSchemaInUnion); + } + _ => { + if union_schema.contains(&parsed_schema) { + return Err(AvrowErr::DuplicateSchemaInUnion); + } else { + union_schema.push(parsed_schema); + } + } + } + } + Ok(Variant::Union { + variants: union_schema, + }) + } + + fn get_fullname(&self, name: &str, enclosing_namespace: Option<&str>) -> String { + if let Some(namespace) = enclosing_namespace { + format!("{}.{}", namespace, name) + } else { + name.to_string() + } + } + + /// Parse a `serde_json::Value` representing a primitive Avro type into a `Schema`. + fn parse_primitive( + &mut self, + schema: &str, + enclosing_namespace: Option<&str>, + ) -> Result { + match schema { + "null" => Ok(Variant::Null), + "boolean" => Ok(Variant::Boolean), + "int" => Ok(Variant::Int), + "long" => Ok(Variant::Long), + "double" => Ok(Variant::Double), + "float" => Ok(Variant::Float), + "bytes" => Ok(Variant::Bytes), + "string" => Ok(Variant::Str), + other if !other.is_empty() => { + let name = self.get_fullname(other, enclosing_namespace); + if self.cxt.contains_key(&name) { + Ok(Variant::Named(name)) + } else { + Err(AvrowErr::SchemaParseErr(io_err(&format!( + "named schema `{}` must be defined before use", + other + )))) + } + } + _ => Err(AvrowErr::InvalidPrimitiveSchema), + } + } + + fn parse_record_fields( + &mut self, + fields: &[serde_json::Value], + enclosing_namespace: Option<&str>, + ) -> Result, AvrowErr> { + let mut fields_parsed = IndexMap::with_capacity(fields.len()); + for field_obj in fields { + match field_obj { + JsonValue::Object(o) => { + let name = o + .get("name") + .and_then(|a| a.as_str()) + .ok_or(AvrowErr::RecordNameNotFound)?; + + let ty: &JsonValue = o.get("type").ok_or(AvrowErr::RecordTypeNotFound)?; + let mut ty = self.parse_schema(ty, enclosing_namespace)?; + + // if ty is named use enclosing namespace to construct the fullname + if let Some(name) = ty.get_named_mut() { + // if parsed type has its own namespace + if name.namespace().is_none() { + if let Some(namespace) = enclosing_namespace { + name.set_namespace(namespace)?; + } + } + } + + let default = if let Some(v) = o.get("default") { + Some(parse_default(v, &ty)?) + } else { + None + }; + + let order = if let Some(order) = o.get("order") { + parse_field_order(order)? + } else { + Order::Ascending + }; + + let aliases = parse_aliases(o.get("aliases")); + + fields_parsed.insert( + name.to_string(), + Field::new(name, ty, default, order, aliases)?, + ); + } + _ => return Err(AvrowErr::InvalidRecordFieldType), + } + } + + Ok(fields_parsed) + } + + fn parse_object( + &mut self, + value: &Map, + enclosing_namespace: Option<&str>, + ) -> Result { + match value.get("type") { + Some(&JsonValue::String(ref s)) if s == "record" => { + let rec_name = Name::from_json(value, enclosing_namespace)?; + + // Insert a named reference to support recursive schema definitions. + self.cxt + .insert(rec_name.to_string(), Variant::Named(rec_name.to_string())); + + let fields = if let Some(JsonValue::Array(ref fields_vec)) = value.get("fields") { + fields_vec + } else { + return Err(AvrowErr::ExpectedFieldsJsonArray); + }; + + let fields = self.parse_record_fields(fields, { + if rec_name.namespace().is_some() { + // Most tightly enclosing namespace, which is this namespace + rec_name.namespace() + } else { + enclosing_namespace + } + })?; + + let aliases = parse_aliases(value.get("aliases")); + + let rec = Variant::Record { + name: rec_name.clone(), + aliases, + fields, + }; + + let rec_for_registry = rec.clone(); + let rec_name = rec_name.to_string(); + + // if a record schema is being redefined throw an error. + if let Some(Variant::Named(_)) = self.cxt.get(&rec_name) { + self.cxt.insert(rec_name, rec_for_registry); + } else { + return Err(AvrowErr::DuplicateSchema); + } + + Ok(rec) + } + Some(&JsonValue::String(ref s)) if s == "enum" => { + let name = Name::from_json(value, enclosing_namespace)?; + let aliases = parse_aliases(value.get("aliases")); + let mut symbols = vec![]; + + if let Some(v) = value.get("symbols") { + match v { + JsonValue::Array(sym) => { + // let mut symbols = Vec::with_capacity(sym.len()); + for v in sym { + let symbol = v.as_str().ok_or(AvrowErr::EnumSymbolParseErr)?; + validate_name(0, symbol)?; + symbols.push(symbol.to_string()); + } + } + other => { + return Err(AvrowErr::EnumParseErr(format!("{:?}", other))); + } + } + } else { + return Err(AvrowErr::EnumSymbolsMissing); + } + + let name_str = name.fullname(); + + let enum_schema = Variant::Enum { + name, + aliases, + symbols, + }; + + self.cxt.insert(name_str, enum_schema.clone()); + + Ok(enum_schema) + } + Some(&JsonValue::String(ref s)) if s == "array" => { + let item_missing_err = AvrowErr::SchemaParseErr(io_err( + "Array schema must have `items` field defined", + )); + let items_schema = value.get("items").ok_or(item_missing_err)?; + let parsed_items = self.parse_schema(items_schema, enclosing_namespace)?; + Ok(Variant::Array { + items: Box::new(parsed_items), + }) + } + Some(&JsonValue::String(ref s)) if s == "map" => { + let item_missing_err = + AvrowErr::SchemaParseErr(io_err("Map schema must have `values` field defined")); + let items_schema = value.get("values").ok_or(item_missing_err)?; + let parsed_items = self.parse_schema(items_schema, enclosing_namespace)?; + Ok(Variant::Map { + values: Box::new(parsed_items), + }) + } + Some(&JsonValue::String(ref s)) if s == "fixed" => { + let name = Name::from_json(value, enclosing_namespace)?; + let size = value.get("size").ok_or(AvrowErr::FixedSizeNotFound)?; + let name_str = name.fullname(); + + let fixed_schema = Variant::Fixed { + name, + size: size.as_u64().ok_or(AvrowErr::FixedSizeNotNumber)? as usize, // clamp to usize + }; + + self.cxt.insert(name_str, fixed_schema.clone()); + + Ok(fixed_schema) + } + Some(JsonValue::String(ref s)) if s == "null" => Ok(Variant::Null), + Some(JsonValue::String(ref s)) if s == "boolean" => Ok(Variant::Boolean), + Some(JsonValue::String(ref s)) if s == "int" => Ok(Variant::Int), + Some(JsonValue::String(ref s)) if s == "long" => Ok(Variant::Long), + Some(JsonValue::String(ref s)) if s == "float" => Ok(Variant::Float), + Some(JsonValue::String(ref s)) if s == "double" => Ok(Variant::Double), + Some(JsonValue::String(ref s)) if s == "bytes" => Ok(Variant::Bytes), + Some(JsonValue::String(ref s)) if s == "string" => Ok(Variant::Str), + _other => Err(AvrowErr::SchemaParseFailed), + } + } +} + +// TODO add support if needed +// fn parse_doc(value: Option<&JsonValue>) -> Option { +// if let Some(JsonValue::String(s)) = value { +// Some(s.to_string()) +// } else { +// None +// } +// } + +// Parses the `order` of a field, defaults to `ascending` order +pub(crate) fn parse_field_order(order: &JsonValue) -> AvrowResult { + match *order { + JsonValue::String(ref s) => match &**s { + "ascending" => Ok(Order::Ascending), + "descending" => Ok(Order::Descending), + "ignore" => Ok(Order::Ignore), + _ => Err(AvrowErr::UnknownFieldOrdering), + }, + _ => Err(AvrowErr::InvalidFieldOrdering), + } +} + +// Parses aliases of a field +fn parse_aliases(aliases: Option<&JsonValue>) -> Option> { + match aliases { + Some(JsonValue::Array(ref aliases)) => { + let mut alias_parsed = Vec::with_capacity(aliases.len()); + for a in aliases { + let a = a.as_str().map(ToOwned::to_owned)?; + alias_parsed.push(a); + } + Some(alias_parsed) + } + _ => None, + } +} + +pub(crate) fn parse_default( + default_value: &JsonValue, + schema_variant: &Variant, +) -> Result { + match (default_value, schema_variant) { + (d, Variant::Union { variants }) => { + let first_variant = variants.first().ok_or(AvrowErr::FailedDefaultUnion)?; + parse_default(d, first_variant) + } + (JsonValue::Null, Variant::Null) => Ok(Value::Null), + (JsonValue::Bool(v), Variant::Boolean) => Ok(Value::Boolean(*v)), + (JsonValue::Number(n), Variant::Int) => Ok(Value::Int(n.as_i64().unwrap() as i32)), + (JsonValue::Number(n), Variant::Long) => Ok(Value::Long(n.as_i64().unwrap())), + (JsonValue::Number(n), Variant::Float) => Ok(Value::Float(n.as_f64().unwrap() as f32)), + (JsonValue::Number(n), Variant::Double) => Ok(Value::Double(n.as_f64().unwrap() as f64)), + (JsonValue::String(n), Variant::Bytes) => Ok(Value::Bytes(n.as_bytes().to_vec())), + (JsonValue::String(n), Variant::Str) => Ok(Value::Str(n.clone())), + (JsonValue::Object(v), Variant::Record { name, fields, .. }) => { + let mut values = IndexMap::with_capacity(v.len()); + + for (k, v) in v { + let parsed_value = + parse_default(v, &fields.get(k).ok_or(AvrowErr::DefaultValueParse)?.ty)?; + values.insert(k.to_string(), FieldValue::new(parsed_value)); + } + + Ok(Value::Record(crate::value::Record { + fields: values, + name: name.to_string(), + })) + } + (JsonValue::String(n), Variant::Enum { symbols, .. }) => { + if symbols.contains(n) { + Ok(Value::Str(n.clone())) + } else { + Err(AvrowErr::EnumSymbolNotPresent) + } + } + (JsonValue::Array(arr), Variant::Array { items }) => { + let mut default_arr_items: Vec = Vec::with_capacity(arr.len()); + for v in arr { + let parsed_default = parse_default(v, items); + default_arr_items.push(parsed_default?); + } + + Ok(Value::Array(default_arr_items)) + } + ( + JsonValue::Object(map), + Variant::Map { + values: values_schema, + }, + ) => { + let mut values = std::collections::HashMap::with_capacity(map.len()); + for (k, v) in map { + let parsed_value = parse_default(v, values_schema)?; + values.insert(k.to_string(), parsed_value); + } + + Ok(Value::Map(values)) + } + + (JsonValue::String(n), Variant::Fixed { .. }) => Ok(Value::Fixed(n.as_bytes().to_vec())), + (_d, _s) => Err(AvrowErr::DefaultValueParse), + } +} + +#[cfg(test)] +mod tests { + use crate::schema::common::Order; + use crate::schema::Field; + use crate::schema::Name; + use crate::schema::Variant; + use crate::Schema; + use crate::Value; + use indexmap::IndexMap; + use std::str::FromStr; + #[test] + fn schema_parse_default_values() { + let schema = Schema::from_str( + r##"{ + "type": "record", + "name": "Can", + "doc":"Represents a can data", + "namespace": "com.avrow", + "aliases": ["my_linked_list"], + "fields" : [ + { + "name": "next", + "type": ["null", "Can"] + }, + { + "name": "value", + "type": "long", + "default": 1, + "aliases": ["data"], + "order": "descending", + "doc": "This field holds the value of the linked list" + } + ] + }"##, + ) + .unwrap(); + + let mut fields = IndexMap::new(); + let f1 = Field::new( + "value", + Variant::Long, + Some(Value::Long(1)), + Order::Ascending, + None, + ) + .unwrap(); + let f2 = Field::new( + "next", + Variant::Union { + variants: vec![Variant::Null, Variant::Named("com.avrow.Can".to_string())], + }, + None, + Order::Ascending, + None, + ) + .unwrap(); + fields.insert("value".to_string(), f1); + fields.insert("next".to_string(), f2); + + let mut name = Name::new("Can").unwrap(); + name.set_namespace("com.avrow").unwrap(); + + let s = Variant::Record { + name, + aliases: Some(vec!["my_linked_list".to_string()]), + fields, + }; + + assert_eq!(&s, schema.variant()); + } + + #[test] + fn nested_record_fields_parses_properly_with_fullnames() { + let schema = Schema::from_str(r##"{ + "name": "longlist", + "namespace": "com.some", + "type":"record", + "fields": [ + {"name": "magic", "type": {"type": "fixed", "name": "magic", "size": 4, "namespace": "com.bar"} + }, + {"name": "inner_rec", "type": {"type": "record", "name": "inner_rec", "fields": [ + { + "name": "test", + "type": {"type": "fixed", "name":"hello", "size":5} + } + ]}} + ] + }"##).unwrap(); + + assert!(schema.cxt.cxt.contains_key("com.bar.magic")); + assert!(schema.cxt.cxt.contains_key("com.some.hello")); + assert!(schema.cxt.cxt.contains_key("com.some.longlist")); + assert!(schema.cxt.cxt.contains_key("com.some.inner_rec")); + } +} diff --git a/src/schema/tests.rs b/src/schema/tests.rs new file mode 100644 index 0000000..a75484e --- /dev/null +++ b/src/schema/tests.rs @@ -0,0 +1,437 @@ +use super::common::{Field, Name, Order}; +use super::{Schema, Variant}; +use indexmap::IndexMap; +use std::collections::HashMap; +use std::str::FromStr; + +fn primitive_schema_objects() -> HashMap<&'static str, Variant> { + let mut s = HashMap::new(); + s.insert(r##"{ "type": "null" }"##, Variant::Null); + s.insert(r##"{ "type": "boolean" }"##, Variant::Boolean); + s.insert(r##"{ "type": "int" }"##, Variant::Int); + s.insert(r##"{ "type": "long" }"##, Variant::Long); + s.insert(r##"{ "type": "float" }"##, Variant::Float); + s.insert(r##"{ "type": "double" }"##, Variant::Double); + s.insert(r##"{ "type": "bytes" }"##, Variant::Bytes); + s.insert(r##"{ "type": "string" }"##, Variant::Str); + s +} + +fn primitive_schema_canonical() -> HashMap<&'static str, Variant> { + let mut s = HashMap::new(); + s.insert(r##""null""##, Variant::Null); + s.insert(r##""boolean""##, Variant::Boolean); + s.insert(r##""int""##, Variant::Int); + s.insert(r##""long""##, Variant::Long); + s.insert(r##""float""##, Variant::Float); + s.insert(r##""double""##, Variant::Double); + s.insert(r##""bytes""##, Variant::Bytes); + s.insert(r##""string""##, Variant::Str); + s +} + +#[test] +fn parse_primitives_as_json_objects() { + for (s, v) in primitive_schema_objects() { + let schema = Schema::from_str(s).unwrap(); + assert_eq!(schema.variant, v); + } +} + +#[test] +fn parse_primitives_as_defined_types() { + for (s, v) in primitive_schema_canonical() { + let schema = Schema::from_str(s).unwrap(); + assert_eq!(schema.variant, v); + } +} + +#[test] +fn parse_record() { + let record_schema = Schema::from_str( + r##"{ + "type": "record", + "name": "LongOrNull", + "namespace":"com.test", + "aliases": ["MaybeLong"], + "fields" : [ + {"name": "value", "type": "long"}, + {"name": "other", "type": ["null", "LongOrNull"]} + ] + }"##, + ) + .unwrap(); + + let union_variants = vec![ + Variant::Null, + Variant::Named("com.test.LongOrNull".to_string()), + ]; + + let mut fields_map = IndexMap::new(); + fields_map.insert( + "value".to_string(), + Field::new("value", Variant::Long, None, Order::Ascending, None).unwrap(), + ); + fields_map.insert( + "other".to_string(), + Field::new( + "other", + Variant::Union { + variants: union_variants, + }, + None, + Order::Ascending, + None, + ) + .unwrap(), + ); + + let mut name = Name::new("LongOrNull").unwrap(); + name.set_namespace("com.test").unwrap(); + + assert_eq!( + record_schema.variant, + Variant::Record { + name, + aliases: Some(vec!["MaybeLong".to_string()]), + fields: fields_map, + } + ); +} + +#[test] +fn parse_fixed() { + let fixed_schema = + Schema::from_str(r##"{"type": "fixed", "size": 16, "name": "md5"}"##).unwrap(); + assert_eq!( + fixed_schema.variant, + Variant::Fixed { + name: Name::new("md5").unwrap(), + size: 16 + } + ); +} + +#[test] +fn parse_enum() { + let json = r##"{ + "type": "enum", + "name": "Suit", + "symbols" : ["SPADES", "HEARTS", "DIAMONDS", "CLUBS"] + }"##; + let enum_schema = Schema::from_str(json).unwrap(); + let name = Name::new("Suit").unwrap(); + let mut symbols = vec![]; + symbols.push("SPADES".to_owned()); + symbols.push("HEARTS".to_owned()); + symbols.push("DIAMONDS".to_owned()); + symbols.push("CLUBS".to_owned()); + + assert_eq!( + enum_schema.variant, + Variant::Enum { + name, + aliases: None, + symbols + } + ); +} + +#[test] +fn parse_array() { + let json = r##"{"type": "array", "items": "string"}"##; + let array_schema = Schema::from_str(json).unwrap(); + assert_eq!( + array_schema.variant, + Variant::Array { + items: Box::new(Variant::Str) + } + ); +} + +#[test] +fn parse_map() { + let map_schema = Schema::from_str(r##"{"type": "map", "values": "long"}"##).unwrap(); + assert_eq!( + map_schema.variant, + Variant::Map { + values: Box::new(Variant::Long) + } + ); +} + +/////////////////////////////////////////////////////////////////////////////// +/// Union +/////////////////////////////////////////////////////////////////////////////// + +#[test] +fn parse_simple_union() { + let union_schema = Schema::from_str(r##"["null", "string"]"##).unwrap(); + assert_eq!( + union_schema.variant, + Variant::Union { + variants: vec![Variant::Null, Variant::Str] + } + ); +} + +#[test] +#[should_panic] +fn parse_union_duplicate_primitive_fails() { + let mut results = vec![]; + for i in primitive_schema_canonical() { + let json = &format!("[{}, {}]", i.0, i.0); + results.push(Schema::from_str(json).is_err()); + } + + assert!(results.iter().any(|a| !(*a))); +} + +#[test] +fn parse_union_with_different_named_type_but_same_schema_succeeds() { + let union_schema = Schema::from_str( + r##"[ + { + "type":"record", + "name": "record_one", + "fields" : [ + {"name": "value", "type": "long"} + ] + }, + { + "type":"record", + "name": "record_two", + "fields" : [ + {"name": "value", "type": "long"} + ] + }]"##, + ); + + assert!(union_schema.is_ok()); +} + +#[test] +fn parse_union_with_same_named_type_fails() { + let union_schema = Schema::from_str( + r##"[ + { + "type":"record", + "name": "record_one", + "fields" : [ + {"name": "value", "type": "long"} + ] + }, + { + "type":"record", + "name": "record_one", + "fields" : [ + {"name": "value", "type": "long"} + ] + }]"##, + ); + + assert!(union_schema.is_err()); +} + +#[test] +fn parse_union_field_invalid_default_values() { + let default_valued_schema = Schema::from_str( + r##" + { + "name": "Company", + "type": "record", + "fields": [ + { + "name": "emp_name", + "type": "string", + "doc": "employee name" + }, + { + "name": "bonus", + "type": ["null", "long"], + "default": null, + "doc": "bonus received on a yearly basis" + }, + { + "name": "subordinates", + "type": ["null", {"type": "map", "values": "string"}], + "default": {"foo":"bar"}, + "doc": "map of subordinates Name and Designation" + }, + { + "name": "departments", + "type":["null", {"type":"array", "items":"string" }], + "default": ["Sam", "Bob"], + "doc": "Departments under the employee" + } + ] + } + "##, + ); + + assert!(default_valued_schema.is_err()); +} + +#[test] +fn parse_default_values_record() { + let default_valued_schema = Schema::from_str( + r##" + { + "name": "Company", + "type": "record", + "namespace": "com.test.avrow", + "fields": [ + { + "name": "bonus", + "type": ["null", "long"], + "default": null, + "doc": "bonus received on a yearly basis" + } + ] + } + "##, + ); + + assert!(default_valued_schema.is_ok()); +} + +#[test] +#[should_panic(expected = "DuplicateSchema")] +fn fails_on_duplicate_schema() { + let schema = r##"{ + "type": "record", + "namespace": "test.avro.training", + "name": "SomeMessage", + "fields": [{ + "name": "is_error", + "type": "boolean", + "default": false + }, { + "name": "outcome", + "type": [{ + "type": "record", + "name": "SomeMessage", + "fields": [] + }, { + "type": "record", + "name": "ErrorRecord", + "fields": [{ + "name": "errors", + "type": { + "type": "map", + "values": "string" + }, + "doc": "doc" + }] + }] + }] + }"##; + + Schema::from_str(schema).unwrap(); +} + +#[test] +#[should_panic] +fn parse_immediate_unions_fails() { + let default_valued_schema = Schema::from_str( + r##" + ["null", "string", ["null", "int"]]"##, + ); + + assert!(default_valued_schema.is_ok()); +} + +#[test] +fn parse_simple_default_values_record() { + let _default_valued_schema = Schema::from_str( + r##" + { + "name": "com.school.Student", + "type": "record", + "fields": [ + { + "name": "departments", + "type":[{"type":"array", "items":"string" }, "null"], + "default": ["Computer Science", "Finearts"], + "doc": "Departments of a student" + } + ] + } + "##, + ) + .unwrap(); +} + +#[test] +fn parse_default_record_value_in_union() { + let schema = Schema::from_str( + r##" + { + "name": "com.big.data.avro.schema.Employee", + "type": "record", + "fields": [ + { + "name": "departments", + "type":[ + {"type":"record", + "name": "dept_name", + "fields":[{"name":"id","type": "string"}, {"name":"foo", "type": "null"}] }], + "default": {"id": "foo", "foo": null} + } + ] + } + "##, + ) + .unwrap(); + + if let Variant::Record { fields, .. } = schema.variant { + match &fields["departments"].default { + Some(crate::Value::Record(r)) => { + assert!(r.fields.contains_key("id")); + assert_eq!( + r.fields["id"], + crate::value::FieldValue::new(crate::Value::Str("foo".to_string())) + ); + } + _ => panic!("should be a record"), + } + } +} + +#[test] +#[should_panic(expected = "must be defined before use")] +fn named_schema_must_be_defined_before_being_used() { + let _schema = Schema::from_str( + r##"{ + "type": "record", + "name": "LongList", + "aliases": ["LinkedLongs"], + "fields" : [ + {"name": "value", "type": "long"}, + {"name": "next", "type": ["null", "OtherList"]} + ] + }"##, + ) + .unwrap(); +} + +#[test] +fn test_two_instance_schema_equality() { + let raw_schema = r#" + { + "type": "record", + "name": "User", + "doc": "Hi there.", + "fields": [ + {"name": "likes_pizza", "type": "boolean", "default": false}, + {"name": "aa-i32", + "type": {"type": "array", "items": {"type": "array", "items": "int"}}, + "default": [[0], [12, -1]]} + ] + } + "#; + + let schema = Schema::from_str(raw_schema).unwrap(); + let schema2 = Schema::from_str(raw_schema).unwrap(); + assert_eq!(schema, schema2); +} diff --git a/src/serde_avro/de.rs b/src/serde_avro/de.rs new file mode 100644 index 0000000..fec2a41 --- /dev/null +++ b/src/serde_avro/de.rs @@ -0,0 +1,170 @@ +use super::de_impl::{ArrayDeserializer, ByteSeqDeserializer, MapDeserializer, StructReader}; +use crate::error::AvrowErr; + +use crate::value::Value; + +use serde::de::IntoDeserializer; +use serde::de::{self, Visitor}; +use serde::forward_to_deserialize_any; + +pub(crate) struct SerdeReader<'de> { + pub(crate) inner: &'de Value, +} + +impl<'de> SerdeReader<'de> { + pub(crate) fn new(inner: &'de Value) -> Self { + SerdeReader { inner } + } +} + +impl<'de, 'a> de::Deserializer<'de> for &'a mut SerdeReader<'de> { + type Error = AvrowErr; + + fn deserialize_any(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + match self.inner { + Value::Null => visitor.visit_unit(), + Value::Boolean(v) => visitor.visit_bool(*v), + Value::Int(v) => visitor.visit_i32(*v), + Value::Long(v) => visitor.visit_i64(*v), + Value::Float(v) => visitor.visit_f32(*v), + Value::Double(v) => visitor.visit_f64(*v), + Value::Str(ref v) => visitor.visit_borrowed_str(v), + Value::Bytes(ref bytes) => visitor.visit_borrowed_bytes(&bytes), + Value::Array(items) => visitor.visit_seq(ArrayDeserializer::new(&items)), + Value::Enum(s) => visitor.visit_enum(s.as_str().into_deserializer()), + _ => Err(AvrowErr::Unsupported), + } + } + + forward_to_deserialize_any! { + unit bool u8 i8 i16 i32 i64 u16 u32 u64 f32 f64 str bytes byte_buf string ignored_any enum + } + + fn deserialize_option(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + visitor.visit_some(self) + } + + fn deserialize_unit_struct( + self, + _name: &'static str, + visitor: V, + ) -> Result + where + V: Visitor<'de>, + { + visitor.visit_unit() + } + + fn deserialize_seq(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + match self.inner { + Value::Array(ref items) => visitor.visit_seq(ArrayDeserializer::new(items)), + // TODO figure out the correct byte stram to use + Value::Bytes(buf) | Value::Fixed(buf) => { + let byte_seq_deser = ByteSeqDeserializer { input: buf.iter() }; + visitor.visit_seq(byte_seq_deser) + } + Value::Union(v) => match v.as_ref() { + Value::Array(ref items) => visitor.visit_seq(ArrayDeserializer::new(items)), + _ => Err(AvrowErr::Unsupported), + }, + _ => Err(AvrowErr::Unsupported), + } + } + + // avro bytes + fn deserialize_tuple(self, _len: usize, visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + self.deserialize_seq(visitor) + } + + // for struct field + fn deserialize_identifier(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + self.deserialize_str(visitor) + } + + fn deserialize_map(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + match self.inner { + Value::Map(m) => { + let map_de = MapDeserializer { + keys: m.keys(), + values: m.values(), + }; + visitor.visit_map(map_de) + } + v => Err(AvrowErr::UnexpectedAvroValue { + value: format!("{:?}", v), + }), + } + } + + fn deserialize_struct( + self, + _a: &'static str, + _b: &'static [&'static str], + visitor: V, + ) -> Result + where + V: Visitor<'de>, + { + match self.inner { + Value::Record(ref r) => visitor.visit_map(StructReader::new(r.fields.iter())), + Value::Union(ref inner) => match **inner { + Value::Record(ref rec) => visitor.visit_map(StructReader::new(rec.fields.iter())), + _ => Err(de::Error::custom("Union variant not a record/struct")), + }, + _ => Err(de::Error::custom("Must be a record/struct")), + } + } + + /////////////////////////////////////////////////////////////////////////// + /// Not yet supported types + /////////////////////////////////////////////////////////////////////////// + + fn deserialize_tuple_struct( + self, + _name: &'static str, + _len: usize, + _visitor: V, + ) -> Result + where + V: Visitor<'de>, + { + // TODO it is not clear to what avro schema can a tuple map to + Err(AvrowErr::Unsupported) + } + + fn deserialize_newtype_struct( + self, + _name: &'static str, + _visitor: V, + ) -> Result + where + V: Visitor<'de>, + { + Err(AvrowErr::Unsupported) + } + + fn deserialize_char(self, _visitor: V) -> Result + where + V: Visitor<'de>, + { + Err(AvrowErr::Unsupported) + } +} diff --git a/src/serde_avro/de_impl.rs b/src/serde_avro/de_impl.rs new file mode 100644 index 0000000..eb47bba --- /dev/null +++ b/src/serde_avro/de_impl.rs @@ -0,0 +1,193 @@ +use super::de::SerdeReader; +use crate::error::AvrowErr; +use crate::value::FieldValue; +use crate::Value; +use indexmap::map::Iter as MapIter; +use serde::de; +use serde::de::DeserializeSeed; +use serde::de::Visitor; +use serde::forward_to_deserialize_any; +use std::collections::hash_map::Keys; +use std::collections::hash_map::Values; +use std::slice::Iter; + +pub(crate) struct StructReader<'de> { + input: MapIter<'de, String, FieldValue>, + value: Option<&'de FieldValue>, +} + +impl<'de> StructReader<'de> { + pub fn new(input: MapIter<'de, String, FieldValue>) -> Self { + StructReader { input, value: None } + } +} + +impl<'de> de::MapAccess<'de> for StructReader<'de> { + type Error = AvrowErr; + + fn next_key_seed(&mut self, seed: K) -> Result, Self::Error> + where + K: DeserializeSeed<'de>, + { + match self.input.next() { + Some(item) => { + let (ref field, ref value) = item; + self.value = Some(value); + seed.deserialize(StrDeserializer { input: &field }) + .map(Some) + } + None => Ok(None), + } + } + + fn next_value_seed(&mut self, seed: V) -> Result + where + V: DeserializeSeed<'de>, + { + let a = self.value.take(); + if let Some(a) = a { + match &a.value { + Value::Null => seed.deserialize(NullDeserializer), + value => seed.deserialize(&mut SerdeReader { inner: &value }), + } + } else { + Err(de::Error::custom("Unexpected call to next_value_seed.")) + } + } +} + +pub(crate) struct ArrayDeserializer<'de> { + input: Iter<'de, Value>, +} + +impl<'de> ArrayDeserializer<'de> { + pub fn new(input: &'de [Value]) -> Self { + Self { + input: input.iter(), + } + } +} + +impl<'de> de::SeqAccess<'de> for ArrayDeserializer<'de> { + type Error = AvrowErr; + + fn next_element_seed(&mut self, seed: T) -> Result, Self::Error> + where + T: DeserializeSeed<'de>, + { + match self.input.next() { + Some(item) => seed.deserialize(&mut SerdeReader::new(item)).map(Some), + None => Ok(None), + } + } +} + +pub(crate) struct ByteSeqDeserializer<'de> { + pub(crate) input: Iter<'de, u8>, +} + +impl<'de> de::SeqAccess<'de> for ByteSeqDeserializer<'de> { + type Error = AvrowErr; + + fn next_element_seed(&mut self, seed: T) -> Result, Self::Error> + where + T: DeserializeSeed<'de>, + { + match self.input.next() { + Some(item) => seed.deserialize(ByteDeserializer { byte: item }).map(Some), + None => Ok(None), + } + } +} + +pub(crate) struct ByteDeserializer<'de> { + pub(crate) byte: &'de u8, +} + +impl<'de> de::Deserializer<'de> for ByteDeserializer<'de> { + type Error = AvrowErr; + + fn deserialize_any(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + visitor.visit_u8(*self.byte) + } + + forward_to_deserialize_any! { + bool u8 u16 u32 u64 i8 i16 i32 i64 f32 f64 char str string unit option + seq bytes byte_buf map unit_struct newtype_struct + tuple_struct struct tuple enum identifier ignored_any + } +} + +pub(crate) struct MapDeserializer<'de> { + pub(crate) keys: Keys<'de, String, Value>, + pub(crate) values: Values<'de, String, Value>, +} + +impl<'de> de::MapAccess<'de> for MapDeserializer<'de> { + type Error = AvrowErr; + + fn next_key_seed(&mut self, seed: K) -> Result, Self::Error> + where + K: DeserializeSeed<'de>, + { + match self.keys.next() { + Some(key) => seed.deserialize(StrDeserializer { input: key }).map(Some), + None => Ok(None), + } + } + + fn next_value_seed(&mut self, seed: V) -> Result + where + V: DeserializeSeed<'de>, + { + match self.values.next() { + Some(value) => seed.deserialize(&mut SerdeReader::new(value)), + None => Err(Self::Error::Message( + "Unexpected call to next_value_seed".to_string(), + )), + } + } +} + +pub(crate) struct StrDeserializer<'de> { + input: &'de str, +} + +impl<'de> de::Deserializer<'de> for StrDeserializer<'de> { + type Error = AvrowErr; + + fn deserialize_any(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + visitor.visit_borrowed_str(&self.input) + } + + forward_to_deserialize_any! { + bool u8 u16 u32 u64 i8 i16 i32 i64 f32 f64 char str string unit option + seq bytes byte_buf map unit_struct newtype_struct + tuple_struct struct tuple enum identifier ignored_any + } +} + +pub(crate) struct NullDeserializer; + +impl<'de> de::Deserializer<'de> for NullDeserializer { + type Error = AvrowErr; + + fn deserialize_any(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + visitor.visit_none() + } + + forward_to_deserialize_any! { + bool u8 u16 u32 u64 i8 i16 i32 i64 f32 f64 char str string unit option + seq bytes byte_buf map unit_struct newtype_struct + tuple_struct struct tuple enum identifier ignored_any + } +} diff --git a/src/serde_avro/mod.rs b/src/serde_avro/mod.rs new file mode 100644 index 0000000..af2f22b --- /dev/null +++ b/src/serde_avro/mod.rs @@ -0,0 +1,8 @@ +mod de; +mod de_impl; +mod ser; +mod ser_impl; + +pub(crate) use self::de::SerdeReader; +pub use self::ser::{to_value, SerdeWriter}; +pub use crate::error::AvrowErr; diff --git a/src/serde_avro/ser.rs b/src/serde_avro/ser.rs new file mode 100644 index 0000000..359dc9e --- /dev/null +++ b/src/serde_avro/ser.rs @@ -0,0 +1,261 @@ +use super::ser_impl::{MapSerializer, SeqSerializer, StructSerializer}; +use crate::error::AvrowErr; +use crate::value::Value; +use serde::ser::{self, Serialize}; + +pub struct SerdeWriter; + +/// `to_value` is the serde API for serialization of Rust types to an [avrow::Value](enum.Value.html) +pub fn to_value(value: &T) -> Result +where + T: Serialize, +{ + let mut serializer = SerdeWriter; + value.serialize(&mut serializer) +} + +impl<'b> ser::Serializer for &'b mut SerdeWriter { + type Ok = Value; + type Error = AvrowErr; + type SerializeSeq = SeqSerializer; + type SerializeMap = MapSerializer; + type SerializeStruct = StructSerializer; + type SerializeTuple = SeqSerializer; + type SerializeTupleStruct = Unsupported; + type SerializeTupleVariant = Unsupported; + type SerializeStructVariant = Unsupported; + + fn serialize_bool(self, v: bool) -> Result { + Ok(Value::Boolean(v)) + } + + fn serialize_i8(self, v: i8) -> Result { + Ok(Value::Byte(v as u8)) + } + + fn serialize_i16(self, v: i16) -> Result { + Ok(Value::Int(v as i32)) + } + + fn serialize_i32(self, v: i32) -> Result { + Ok(Value::Int(v as i32)) + } + + fn serialize_i64(self, v: i64) -> Result { + Ok(Value::Long(v)) + } + + fn serialize_u8(self, v: u8) -> Result { + // using the auxiliary avro value + Ok(Value::Byte(v)) + } + + fn serialize_u16(self, v: u16) -> Result { + Ok(Value::Int(v as i32)) + } + + fn serialize_u32(self, v: u32) -> Result { + Ok(Value::Int(v as i32)) + } + + fn serialize_u64(self, v: u64) -> Result { + Ok(Value::Long(v as i64)) + } + + fn serialize_f32(self, v: f32) -> Result { + Ok(Value::Float(v)) + } + + fn serialize_f64(self, v: f64) -> Result { + Ok(Value::Double(v)) + } + + fn serialize_char(self, v: char) -> Result { + Ok(Value::Str(v.to_string())) + } + + fn serialize_str(self, v: &str) -> Result { + Ok(Value::Str(v.to_owned())) + } + + fn serialize_bytes(self, v: &[u8]) -> Result { + // todo: identify call path to this + Ok(Value::Bytes(v.to_owned())) + } + + fn serialize_none(self) -> Result { + Ok(Value::Null) + } + + fn serialize_some(self, value: &T) -> Result + where + T: Serialize, + { + Ok(value.serialize(&mut SerdeWriter)?) + } + + fn serialize_unit(self) -> Result { + Ok(Value::Null) + } + + fn serialize_unit_struct(self, _: &'static str) -> Result { + self.serialize_unit() + } + + fn serialize_unit_variant( + self, + _name: &'static str, + _index: u32, + variant: &'static str, + ) -> Result { + Ok(Value::Enum(variant.to_string())) + } + + fn serialize_newtype_struct( + self, + _: &'static str, + value: &T, + ) -> Result + where + T: Serialize, + { + value.serialize(self) + } + + fn serialize_seq(self, len: Option) -> Result { + Ok(SeqSerializer::new(len)) + } + + fn serialize_map(self, len: Option) -> Result { + Ok(MapSerializer::new(len)) + } + + fn serialize_struct( + self, + name: &'static str, + len: usize, + ) -> Result { + Ok(StructSerializer::new(name, len)) + } + + fn serialize_tuple(self, _len: usize) -> Result { + self.serialize_seq(Some(_len)) + } + + fn serialize_tuple_struct( + self, + _: &'static str, + _len: usize, + ) -> Result { + unimplemented!("Avro does not support Rust tuple structs"); + } + + fn serialize_tuple_variant( + self, + _: &'static str, + _: u32, + _: &'static str, + _: usize, + ) -> Result { + // TODO Is there a way we can map union type to some valid avro type + Err(AvrowErr::Message( + "Tuple type is not currently supported as per avro spec".to_string(), + )) + } + + fn serialize_struct_variant( + self, + _: &'static str, + _: u32, + _: &'static str, + _: usize, + ) -> Result { + unimplemented!("Avro enums does not support struct variants in enum") + } + + fn serialize_newtype_variant( + self, + _: &'static str, + _: u32, + _: &'static str, + _value: &T, + ) -> Result + where + T: Serialize, + { + unimplemented!("Avro does not support newtype struct variants in enums"); + } +} + +/////////////////////////////////////////////////////////////////////////////// +/// Unsupported types in avro +/////////////////////////////////////////////////////////////////////////////// + +pub struct Unsupported; + +// struct enum variant +impl ser::SerializeStructVariant for Unsupported { + type Ok = Value; + type Error = AvrowErr; + + fn serialize_field(&mut self, _: &'static str, _: &T) -> Result<(), Self::Error> + where + T: Serialize, + { + unimplemented!("Avro enums does not support data in its variant") + } + + fn end(self) -> Result { + unimplemented!("Avro enums does not support data in its variant") + } +} + +// tuple enum variant +impl ser::SerializeTupleVariant for Unsupported { + type Ok = Value; + type Error = AvrowErr; + + fn serialize_field(&mut self, _: &T) -> Result<(), Self::Error> + where + T: Serialize, + { + unimplemented!("Avro enums does not support Rust tuple variants in enums") + } + + fn end(self) -> Result { + unimplemented!("Avro enums does not support Rust tuple variant in enums") + } +} + +// TODO maybe we can map it by looking at the schema +impl ser::SerializeTupleStruct for Unsupported { + type Ok = Value; + type Error = AvrowErr; + + fn serialize_field(&mut self, _value: &T) -> Result<(), Self::Error> + where + T: Serialize, + { + unimplemented!("Avro enums does not support Rust tuple struct") + } + + fn end(self) -> Result { + unimplemented!("Avro enums does not support Rust tuple struct") + } +} + +impl<'a> ser::SerializeTuple for Unsupported { + type Ok = Value; + type Error = AvrowErr; + + fn serialize_element(&mut self, _value: &T) -> Result<(), Self::Error> + where + T: Serialize, + { + unimplemented!("Avro enums does not support Rust tuples") + } + + fn end(self) -> Result { + unimplemented!("Avro enums does not support Rust tuples") + } +} diff --git a/src/serde_avro/ser_impl.rs b/src/serde_avro/ser_impl.rs new file mode 100644 index 0000000..c8e9c78 --- /dev/null +++ b/src/serde_avro/ser_impl.rs @@ -0,0 +1,195 @@ +use super::SerdeWriter; +use crate::error::AvrowErr; +use crate::value::FieldValue; +use crate::value::Record; +use crate::Value; +use serde::Serialize; +use std::collections::HashMap; + +pub struct MapSerializer { + map: HashMap, +} + +impl MapSerializer { + pub fn new(len: Option) -> Self { + let map = match len { + Some(len) => HashMap::with_capacity(len), + None => HashMap::new(), + }; + + MapSerializer { map } + } +} + +impl serde::ser::SerializeMap for MapSerializer { + type Ok = Value; + type Error = AvrowErr; + + fn serialize_entry( + &mut self, + key: &K, + value: &V, + ) -> Result<(), Self::Error> + where + K: Serialize, + V: Serialize, + { + let key = key.serialize(&mut SerdeWriter)?; + if let Value::Str(s) = key { + let value = value.serialize(&mut SerdeWriter)?; + self.map.insert(s, value); + Ok(()) + } else { + Err(AvrowErr::ExpectedString) + } + } + + fn serialize_key(&mut self, _key: &T) -> Result<(), Self::Error> + where + T: Serialize, + { + Ok(()) + } + + fn serialize_value(&mut self, _value: &T) -> Result<(), Self::Error> + where + T: Serialize, + { + Ok(()) + } + + fn end(self) -> Result { + Ok(Value::Map(self.map)) + } +} + +////////////////////////////////////////////////////////////////////////////// +/// Rust structs to avro record +////////////////////////////////////////////////////////////////////////////// +pub struct StructSerializer { + name: String, + fields: indexmap::IndexMap, +} + +impl StructSerializer { + pub fn new(name: &str, len: usize) -> StructSerializer { + StructSerializer { + name: name.to_string(), + fields: indexmap::IndexMap::with_capacity(len), + } + } +} + +impl serde::ser::SerializeStruct for StructSerializer { + type Ok = Value; + type Error = AvrowErr; + + fn serialize_field( + &mut self, + name: &'static str, + value: &T, + ) -> Result<(), Self::Error> + where + T: Serialize, + { + self.fields.insert( + name.to_owned(), + FieldValue::new(value.serialize(&mut SerdeWriter)?), + ); + Ok(()) + } + + fn end(self) -> Result { + let record = Record { + name: self.name, + fields: self.fields, + }; + Ok(Value::Record(record)) + } +} + +////////////////////////////////////////////////////////////////////////////// +/// Sequences +////////////////////////////////////////////////////////////////////////////// + +pub struct SeqSerializer { + items: Vec, +} + +impl SeqSerializer { + pub fn new(len: Option) -> SeqSerializer { + let items = match len { + Some(len) => Vec::with_capacity(len), + None => Vec::new(), + }; + + SeqSerializer { items } + } +} + +// Helper function to extract a Vec from a Vec +// This should only be called by the caller who knows that the items +// in the Vec a Value::Byte(u8). +// NOTE: Does collect on an into_iter() allocate a new vec? +fn as_byte_vec(a: Vec) -> Vec { + a.into_iter() + .map(|v| { + if let Value::Byte(b) = v { + b + } else { + unreachable!("Expecting a byte value in the Vec") + } + }) + .collect() +} + +impl<'a> serde::ser::SerializeSeq for SeqSerializer { + type Ok = Value; + type Error = AvrowErr; + + fn serialize_element(&mut self, value: &T) -> Result<(), Self::Error> + where + T: Serialize, + { + let v = value.serialize(&mut SerdeWriter)?; + self.items.push(v); + Ok(()) + } + + // If the items in vec are of Value::Byte(u8) then return a byte array. + // FIXME: maybe implement Serialize directly for Vec to avoid this way. + fn end(self) -> Result { + match self.items.first() { + Some(Value::Byte(_)) => Ok(Value::Bytes(as_byte_vec(self.items))), + _ => Ok(Value::Array(self.items)), + } + } +} + +////////////////////////////////////////////////////////////////////////////// +/// Tuples: avro bytes, fixed +////////////////////////////////////////////////////////////////////////////// + +impl<'a> serde::ser::SerializeTuple for SeqSerializer { + type Ok = Value; + type Error = AvrowErr; + + fn serialize_element(&mut self, value: &T) -> Result<(), Self::Error> + where + T: Serialize, + { + let v = value.serialize(&mut SerdeWriter)?; + self.items.push(v); + Ok(()) + } + + // If the items in vec are of Value::Byte(u8) then return a byte array. + // FIXME: maybe implement Serialize directly for Vec to avoid this way. + fn end(self) -> Result { + match self.items.first() { + Some(Value::Byte(_)) => Ok(Value::Bytes(as_byte_vec(self.items))), + Some(Value::Fixed(_)) => Ok(Value::Fixed(as_byte_vec(self.items))), + _ => Ok(Value::Array(self.items)), + } + } +} diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..4306105 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,34 @@ +use crate::error::AvrowErr; +use integer_encoding::VarIntReader; +use integer_encoding::VarIntWriter; +use std::io::{Error, ErrorKind, Read, Write}; +use std::str; + +pub(crate) fn decode_string(reader: &mut R) -> Result { + let buf = decode_bytes(reader)?; + let s = str::from_utf8(&buf).map_err(|_e| { + let err = Error::new(ErrorKind::InvalidData, "Failed decoding string from bytes"); + AvrowErr::DecodeFailed(err) + })?; + Ok(s.to_string()) +} + +pub(crate) fn decode_bytes(reader: &mut R) -> Result, AvrowErr> { + let len: i64 = reader.read_varint().map_err(AvrowErr::DecodeFailed)?; + let mut byte_buf = vec![0u8; len as usize]; + reader + .read_exact(&mut byte_buf) + .map_err(AvrowErr::DecodeFailed)?; + Ok(byte_buf) +} + +pub fn encode_long(value: i64, writer: &mut W) -> Result { + writer.write_varint(value).map_err(AvrowErr::EncodeFailed) +} + +pub fn encode_raw_bytes(value: &[u8], writer: &mut W) -> Result<(), AvrowErr> { + writer + .write(value) + .map_err(AvrowErr::EncodeFailed) + .map(|_| ()) +} diff --git a/src/value.rs b/src/value.rs new file mode 100644 index 0000000..90b9d79 --- /dev/null +++ b/src/value.rs @@ -0,0 +1,710 @@ +//! Represents the types that + +use crate::error::AvrowErr; +use crate::schema; +use crate::schema::common::validate_name; +use crate::schema::Registry; +use crate::util::{encode_long, encode_raw_bytes}; +use crate::Schema; +use byteorder::LittleEndian; +use byteorder::WriteBytesExt; +use indexmap::IndexMap; +use integer_encoding::VarIntWriter; +use schema::Order; +use schema::Variant; +use serde::Serialize; +use std::collections::{BTreeMap, HashMap}; +use std::fmt::Display; +use std::io::Write; + +// Convenient type alias for map initialzation. +pub type Map = HashMap; + +#[derive(Debug, Clone, PartialEq, Serialize)] +pub(crate) struct FieldValue { + pub(crate) value: Value, + #[serde(skip_serializing)] + order: schema::Order, +} + +impl FieldValue { + pub(crate) fn new(value: Value) -> Self { + FieldValue { + value, + order: Order::Ascending, + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize)] +/// The [record](https://avro.apache.org/docs/current/spec.html#schema_record) avro type +pub struct Record { + pub(crate) name: String, + pub(crate) fields: IndexMap, +} + +impl Record { + /// Creates a new avro record type with the given name. + pub fn new(name: &str) -> Self { + Record { + fields: IndexMap::new(), + name: name.to_string(), + } + } + + /// Adds a field to the record. + pub fn insert>(&mut self, field_name: &str, ty: T) -> Result<(), AvrowErr> { + validate_name(0, field_name)?; + self.fields + .insert(field_name.to_string(), FieldValue::new(ty.into())); + Ok(()) + } + + /// Sets the ordering of the field. + pub fn set_field_order(&mut self, field_name: &str, order: Order) -> Result<(), AvrowErr> { + let a = self + .fields + .get_mut(field_name) + .ok_or(AvrowErr::FieldNotFound)?; + a.order = order; + Ok(()) + } + + /// Creates a record from a [BTreeMap](https://doc.rust-lang.org/std/collections/struct.BTreeMap.html) by consuming it. + /// The values in btree must implement Into. The name provided must match with the name in the record + /// schema being provided to the writer. + pub fn from_btree + Ord + Display, V: Into>( + name: &str, + btree: BTreeMap, + ) -> Result { + let mut record = Record::new(name); + for (k, v) in btree { + let field_value = FieldValue { + value: v.into(), + order: Order::Ascending, + }; + record.fields.insert(k.to_string(), field_value); + } + + Ok(record) + } + + /// Creates a record from a json object. A confirming record schema must be provided. + pub fn from_json( + json: serde_json::Map, + schema: &Schema, + ) -> Result { + // let variant = schema.variant; + if let Variant::Record { name, fields, .. } = &schema.variant { + let mut values = IndexMap::new(); + for (k, v) in json { + let parsed_value = crate::schema::parser::parse_default( + &v, + &fields.get(&k).ok_or(AvrowErr::DefaultValueParse)?.ty, + )?; + values.insert(k.to_string(), FieldValue::new(parsed_value)); + } + + Ok(Value::Record(crate::value::Record { + fields: values, + name: name.fullname(), + })) + } else { + Err(AvrowErr::ExpectedJsonObject) + } + } +} + +// TODO: Avro sort order +// impl PartialOrd for Value { +// fn partial_cmp(&self, other: &Self) -> Option { +// match (self, other) { +// (Value::Null, Value::Null) => Some(Ordering::Equal), +// (Value::Boolean(self_v), Value::Boolean(other_v)) => { +// if self_v == other_v { +// return Some(Ordering::Equal); +// } +// if *self_v == false && *other_v { +// Some(Ordering::Less) +// } else { +// Some(Ordering::Greater) +// } +// } +// (Value::Int(self_v), Value::Int(other_v)) => Some(self_v.cmp(other_v)), +// (Value::Long(self_v), Value::Long(other_v)) => Some(self_v.cmp(other_v)), +// (Value::Float(self_v), Value::Float(other_v)) => self_v.partial_cmp(other_v), +// (Value::Double(self_v), Value::Double(other_v)) => self_v.partial_cmp(other_v), +// (Value::Bytes(self_v), Value::Bytes(other_v)) => self_v.partial_cmp(other_v), +// (Value::Byte(self_v), Value::Byte(other_v)) => self_v.partial_cmp(other_v), +// (Value::Fixed(self_v), Value::Fixed(other_v)) => self_v.partial_cmp(other_v), +// (Value::Str(self_v), Value::Str(other_v)) => self_v.partial_cmp(other_v), +// (Value::Array(self_v), Value::Array(other_v)) => self_v.partial_cmp(other_v), +// (Value::Enum(self_v), Value::Enum(other_v)) => self_v.partial_cmp(other_v), +// (Value::Record(_self_v), Value::Record(_other_v)) => todo!(), +// _ => todo!(), +// } +// } +// } + +/// Represents an Avro value +#[derive(Debug, Clone, PartialEq, Serialize)] +pub enum Value { + /// A null value. + Null, + /// An i32 integer value. + Int(i32), + /// An i64 long value. + Long(i64), + /// A boolean value. + Boolean(bool), + /// A f32 float value. + Float(f32), + /// A f64 float value. + Double(f64), + /// A Record value (BTreeMap). + Record(Record), + /// A Fixed value. + Fixed(Vec), + /// A Map value. + Map(Map), + /// A sequence of u8 bytes. + Bytes(Vec), + /// Rust strings map directly to avro strings + Str(String), + /// A union is a sequence of unique `Value`s + Union(Box), + /// An enumeration. Unlike Rust enums, enums in avro don't support data within their variants. + Enum(String), + /// An array of `Value`s + Array(Vec), + /// auxiliary u8 helper for serde. Not an avro value. + Byte(u8), +} + +impl Value { + pub(crate) fn encode( + &self, + writer: &mut W, + schema: &Variant, + cxt: &Registry, + ) -> Result<(), AvrowErr> { + match (self, schema) { + (Value::Null, Variant::Null) => {} + (Value::Boolean(b), Variant::Boolean) => writer + .write_all(&[*b as u8]) + .map_err(AvrowErr::EncodeFailed)?, + (Value::Int(i), Variant::Int) => { + writer.write_varint(*i).map_err(AvrowErr::EncodeFailed)?; + } + // int is promotable to long, float or double --- + (Value::Int(i), Variant::Long) => { + writer + .write_varint(*i as i64) + .map_err(AvrowErr::EncodeFailed)?; + } + (Value::Int(i), Variant::Float) => { + writer + .write_f32::(*i as f32) + .map_err(AvrowErr::EncodeFailed)?; + } + (Value::Int(i), Variant::Double) => { + writer + .write_f64::(*i as f64) + .map_err(AvrowErr::EncodeFailed)?; + } + // --- + (Value::Long(l), Variant::Long) => { + writer.write_varint(*l).map_err(AvrowErr::EncodeFailed)?; + } + (Value::Long(l), Variant::Float) => { + writer + .write_f32::(*l as f32) + .map_err(AvrowErr::EncodeFailed)?; + } + (Value::Long(l), Variant::Double) => { + writer + .write_f64::(*l as f64) + .map_err(AvrowErr::EncodeFailed)?; + } + (Value::Float(f), Variant::Float) => { + writer + .write_f32::(*f) + .map_err(AvrowErr::EncodeFailed)?; + } + // float is promotable to double --- + (Value::Float(f), Variant::Double) => { + writer + .write_f64::(*f as f64) + .map_err(AvrowErr::EncodeFailed)?; + } // --- + (Value::Double(d), Variant::Double) => { + writer + .write_f64::(*d) + .map_err(AvrowErr::EncodeFailed)?; + } + // Match with union happens first than more specific match arms + (ref value, Variant::Union { variants, .. }) => { + // the get index function returns the index if the value's schema is in the variants of the union + let (union_idx, schema) = resolve_union(&value, &variants, cxt)?; + let union_idx = union_idx as i32; + writer + .write_varint(union_idx) + .map_err(AvrowErr::EncodeFailed)?; + value.encode(writer, &schema, cxt)? + } + (Value::Record(ref record), Variant::Record { fields, .. }) => { + for (f_name, f_value) in &record.fields { + let field_type = fields.get(f_name); + if let Some(field_ty) = field_type { + f_value.value.encode(writer, &field_ty.ty, cxt)?; + } + } + } + (Value::Map(hmap), Variant::Map { values }) => { + // number of keys/value (start of a block) + encode_long(hmap.keys().len() as i64, writer)?; + for (k, v) in hmap.iter() { + encode_long(k.len() as i64, writer)?; + encode_raw_bytes(&*k.as_bytes(), writer)?; + v.encode(writer, values, cxt)?; + } + // marks end of block + encode_long(0, writer)?; + } + (Value::Fixed(ref v), Variant::Fixed { .. }) => { + writer.write_all(&*v).map_err(AvrowErr::EncodeFailed)?; + } + (Value::Str(s), Variant::Str) => { + encode_long(s.len() as i64, writer)?; + encode_raw_bytes(&*s.as_bytes(), writer)?; + } + // string is promotable to bytes --- + (Value::Str(s), Variant::Bytes) => { + encode_long(s.len() as i64, writer)?; + encode_raw_bytes(&*s.as_bytes(), writer)?; + } // -- + (Value::Bytes(b), Variant::Bytes) => { + encode_long(b.len() as i64, writer)?; + encode_raw_bytes(&*b, writer)?; + } + // bytes is promotable to string --- + (Value::Bytes(b), Variant::Str) => { + encode_long(b.len() as i64, writer)?; + encode_raw_bytes(&*b, writer)?; + } // --- + (Value::Bytes(b), Variant::Fixed { size: _size, .. }) => { + encode_raw_bytes(&*b, writer)?; + } + (Value::Enum(ref sym), Variant::Enum { symbols, .. }) => { + if let Some(idx) = symbols.iter().position(|r| r == sym) { + writer + .write_varint(idx as i32) + .map_err(AvrowErr::EncodeFailed)?; + } else { + // perf issues on creating error objects? + return Err(AvrowErr::SchemaDataMismatch); + } + } + ( + Value::Array(ref values), + Variant::Array { + items: items_schema, + }, + ) => { + let array_items_count = Value::from(values.len() as i64); + array_items_count.encode(writer, &Variant::Long, cxt)?; + + for i in values { + i.encode(writer, items_schema, cxt)?; + } + Value::from(0i64).encode(writer, &Variant::Long, cxt)?; + } + // case where serde serializes a Vec to a Array of Byte + // FIXME:figure out a better way for this? + (Value::Array(ref values), Variant::Bytes) => { + let mut v = Vec::with_capacity(values.len()); + for i in values { + if let Value::Byte(b) = i { + v.push(*b); + } + } + encode_long(values.len() as i64, writer)?; + encode_raw_bytes(&*v, writer)?; + } + _ => return Err(AvrowErr::SchemaDataMismatch), + }; + Ok(()) + } +} + +// Given a value, returns the index and the variant of the union +fn resolve_union<'a>( + value: &Value, + union_variants: &'a [Variant], + cxt: &'a Registry, +) -> Result<(usize, &'a Variant), AvrowErr> { + for (idx, variant) in union_variants.iter().enumerate() { + match (value, variant) { + (Value::Null, Variant::Null) + | (Value::Boolean(_), Variant::Boolean) + | (Value::Int(_), Variant::Int) + | (Value::Long(_), Variant::Long) + | (Value::Float(_), Variant::Float) + | (Value::Double(_), Variant::Double) + | (Value::Bytes(_), Variant::Bytes) + | (Value::Str(_), Variant::Str) + | (Value::Map(_), Variant::Map { .. }) + | (Value::Array(_), Variant::Array { .. }) => return Ok((idx, variant)), + (Value::Fixed(_), Variant::Fixed { .. }) => return Ok((idx, variant)), + (Value::Array(v), Variant::Fixed { size, .. }) => { + if v.len() == *size { + return Ok((idx, variant)); + } + return Err(AvrowErr::FixedValueLenMismatch { + found: v.len(), + expected: *size, + }); + } + (Value::Union(_), _) => return Err(AvrowErr::NoImmediateUnion), + (Value::Record(_), Variant::Named(name)) => { + if let Some(schema) = cxt.get(&name) { + return Ok((idx, schema)); + } else { + return Err(AvrowErr::SchemaNotFoundInUnion); + } + } + (Value::Enum(_), Variant::Named(name)) => { + if let Some(schema) = cxt.get(&name) { + return Ok((idx, schema)); + } else { + return Err(AvrowErr::SchemaNotFoundInUnion); + } + } + (Value::Fixed(_), Variant::Named(name)) => { + if let Some(schema) = cxt.get(&name) { + return Ok((idx, schema)); + } else { + return Err(AvrowErr::SchemaNotFoundInUnion); + } + } + _a => {} + } + } + + Err(AvrowErr::SchemaNotFoundInUnion) +} + +/////////////////////////////////////////////////////////////////////////////// +/// From impls for Value +/////////////////////////////////////////////////////////////////////////////// + +impl From<()> for Value { + fn from(_v: ()) -> Value { + Value::Null + } +} + +impl From for Value { + fn from(v: String) -> Value { + Value::Str(v) + } +} + +impl> From> for Value { + fn from(v: HashMap) -> Value { + let mut map = HashMap::with_capacity(v.len()); + for (k, v) in v.into_iter() { + map.insert(k, v.into()); + } + Value::Map(map) + } +} + +impl From for Value { + fn from(value: bool) -> Value { + Value::Boolean(value) + } +} + +impl From> for Value { + fn from(value: Vec) -> Value { + Value::Bytes(value) + } +} + +impl<'a> From<&'a [u8]> for Value { + fn from(value: &'a [u8]) -> Value { + Value::Bytes(value.to_vec()) + } +} + +impl From for Value { + fn from(value: i32) -> Value { + Value::Int(value) + } +} + +impl From for Value { + fn from(value: isize) -> Value { + Value::Int(value as i32) + } +} + +impl From for Value { + fn from(value: usize) -> Value { + Value::Int(value as i32) + } +} + +impl> From> for Value { + fn from(values: Vec) -> Value { + let mut new_vec = vec![]; + for i in values { + new_vec.push(i.into()); + } + Value::Array(new_vec) + } +} + +impl From for Value { + fn from(value: i64) -> Value { + Value::Long(value) + } +} + +impl From for Value { + fn from(value: u64) -> Value { + Value::Long(value as i64) + } +} + +impl From for Value { + fn from(value: f32) -> Value { + Value::Float(value) + } +} + +impl From for Value { + fn from(value: f64) -> Value { + Value::Double(value) + } +} + +impl<'a> From<&'a str> for Value { + fn from(value: &'a str) -> Value { + Value::Str(value.to_string()) + } +} + +#[macro_export] +/// Convenient macro to create a avro fixed value +macro_rules! fixed { + ($vec:tt) => { + avrow::Value::Fixed($vec) + }; +} + +/////////////////////////////////////////////////////////////////////////////// +/// Value -> Rust value +/////////////////////////////////////////////////////////////////////////////// + +impl Value { + /// Try to retrieve an avro null + pub fn as_null(&self) -> Result<(), AvrowErr> { + if let Value::Null = self { + Ok(()) + } else { + Err(AvrowErr::ExpectedVariantNotFound) + } + } + /// Try to retrieve an avro boolean + pub fn as_boolean(&self) -> Result<&bool, AvrowErr> { + if let Value::Boolean(b) = self { + Ok(b) + } else { + Err(AvrowErr::ExpectedVariantNotFound) + } + } + /// Try to retrieve an avro int + pub fn as_int(&self) -> Result<&i32, AvrowErr> { + if let Value::Int(v) = self { + Ok(v) + } else { + Err(AvrowErr::ExpectedVariantNotFound) + } + } + /// Try to retrieve an avro long + pub fn as_long(&self) -> Result<&i64, AvrowErr> { + if let Value::Long(v) = self { + Ok(v) + } else { + Err(AvrowErr::ExpectedVariantNotFound) + } + } + /// Try to retrieve an avro float + pub fn as_float(&self) -> Result<&f32, AvrowErr> { + if let Value::Float(v) = self { + Ok(v) + } else { + Err(AvrowErr::ExpectedVariantNotFound) + } + } + /// Try to retrieve an avro double + pub fn as_double(&self) -> Result<&f64, AvrowErr> { + if let Value::Double(v) = self { + Ok(v) + } else { + Err(AvrowErr::ExpectedVariantNotFound) + } + } + /// Try to retrieve an avro bytes + pub fn as_bytes(&self) -> Result<&[u8], AvrowErr> { + if let Value::Bytes(v) = self { + Ok(v) + } else { + Err(AvrowErr::ExpectedVariantNotFound) + } + } + /// Try to retrieve an avro string + pub fn as_string(&self) -> Result<&str, AvrowErr> { + if let Value::Str(v) = self { + Ok(v) + } else { + Err(AvrowErr::ExpectedVariantNotFound) + } + } + /// Try to retrieve an avro record + pub fn as_record(&self) -> Result<&Record, AvrowErr> { + if let Value::Record(v) = self { + Ok(v) + } else { + Err(AvrowErr::ExpectedVariantNotFound) + } + } + /// Try to retrieve the variant of the enum as a string + pub fn as_enum(&self) -> Result<&str, AvrowErr> { + if let Value::Enum(v) = self { + Ok(v) + } else { + Err(AvrowErr::ExpectedVariantNotFound) + } + } + /// Try to retrieve an avro array + pub fn as_array(&self) -> Result<&[Value], AvrowErr> { + if let Value::Array(v) = self { + Ok(v) + } else { + Err(AvrowErr::ExpectedVariantNotFound) + } + } + /// Try to retrieve an avro map + pub fn as_map(&self) -> Result<&HashMap, AvrowErr> { + if let Value::Map(v) = self { + Ok(v) + } else { + Err(AvrowErr::ExpectedVariantNotFound) + } + } + /// Try to retrieve an avro union + pub fn as_union(&self) -> Result<&Value, AvrowErr> { + if let Value::Union(v) = self { + Ok(v) + } else { + Err(AvrowErr::ExpectedVariantNotFound) + } + } + /// Try to retrieve an avro fixed + pub fn as_fixed(&self) -> Result<&[u8], AvrowErr> { + if let Value::Fixed(v) = self { + Ok(v) + } else { + Err(AvrowErr::ExpectedVariantNotFound) + } + } +} + +#[cfg(test)] +mod tests { + use super::Record; + use crate::from_value; + use crate::Schema; + use serde::{Deserialize, Serialize}; + use std::collections::BTreeMap; + use std::str::FromStr; + + #[test] + fn record_from_btree() { + let mut rec = BTreeMap::new(); + rec.insert("foo", "bar"); + let _r = Record::from_btree("test", rec).unwrap(); + } + + #[derive(Debug, Serialize, Deserialize)] + struct Mentees { + id: i32, + username: String, + } + + #[derive(Debug, Serialize, Deserialize)] + struct RustMentors { + name: String, + github_handle: String, + active: bool, + mentees: Mentees, + } + #[test] + fn record_from_json() { + let schema = Schema::from_str( + r##" + { + "name": "rust_mentors", + "type": "record", + "fields": [ + { + "name": "name", + "type": "string" + }, + { + "name": "github_handle", + "type": "string" + }, + { + "name": "active", + "type": "boolean" + }, + { + "name":"mentees", + "type": { + "name":"mentees", + "type": "record", + "fields": [ + {"name":"id", "type": "int"}, + {"name":"username", "type": "string"} + ] + } + } + ] + } +"##, + ) + .unwrap(); + + let json = serde_json::from_str( + r##" + { "name": "bob", + "github_handle":"ghbob", + "active": true, + "mentees":{"id":1, "username":"alice"} }"##, + ) + .unwrap(); + let rec = super::Record::from_json(json, &schema).unwrap(); + let mut writer = crate::Writer::new(&schema, vec![]).unwrap(); + writer.write(rec).unwrap(); + // writer.flush().unwrap(); + let avro_data = writer.into_inner().unwrap(); + let reader = crate::Reader::new(avro_data.as_slice()).unwrap(); + for value in reader { + let _mentors: RustMentors = from_value(&value).unwrap(); + } + } +} diff --git a/src/writer.rs b/src/writer.rs new file mode 100644 index 0000000..cc12471 --- /dev/null +++ b/src/writer.rs @@ -0,0 +1,318 @@ +//! The Writer is the primary interface for writing values in avro encoded format. + +use rand::{thread_rng, Rng}; + +use crate::codec::Codec; +use crate::schema::Schema; +use crate::value::Value; +use serde::Serialize; + +use crate::config::{DEFAULT_FLUSH_INTERVAL, MAGIC_BYTES, SYNC_MARKER_SIZE}; +use crate::error::{AvrowErr, AvrowResult}; +use crate::schema::Registry; +use crate::schema::Variant; +use crate::serde_avro; +use crate::util::{encode_long, encode_raw_bytes}; +use crate::value::Map; +use std::collections::HashMap; +use std::default::Default; +use std::io::Write; + +fn sync_marker() -> [u8; SYNC_MARKER_SIZE] { + let mut vec = [0u8; SYNC_MARKER_SIZE]; + thread_rng().fill_bytes(&mut vec[..]); + vec +} + +/// Convenient builder struct for configuring and instantiating a Writer. +pub struct WriterBuilder<'a, W> { + metadata: HashMap, + codec: Codec, + schema: Option<&'a Schema>, + datafile: Option, + flush_interval: usize, +} + +impl<'a, W: Write> WriterBuilder<'a, W> { + /// Creates a builder instance to construct a Writer + pub fn new() -> Self { + WriterBuilder { + metadata: Default::default(), + codec: Codec::Null, + schema: None, + datafile: None, + flush_interval: DEFAULT_FLUSH_INTERVAL, + } + } + + /// Set any custom metadata for the datafile. + pub fn set_metadata(mut self, k: &str, v: &str) -> Self { + self.metadata + .insert(k.to_string(), Value::Bytes(v.as_bytes().to_vec())); + self + } + + /// Set one of the available codec. This requires the respective code feature flags to be enabled. + pub fn set_codec(mut self, codec: Codec) -> Self { + self.codec = codec; + self + } + + /// Provide the writer with a reference to the schema file + pub fn set_schema(mut self, schema: &'a Schema) -> Self { + self.schema = Some(schema); + self + } + + /// Set the underlying output stream. This can be anything which implements the Write trait. + pub fn set_datafile(mut self, w: W) -> Self { + self.datafile = Some(w); + self + } + + /// Set the flush interval (bytes) for the internal block buffer. It's the amount of bytes post which + /// the internal buffer is written to the underlying datafile. Defaults to [DEFAULT_FLUSH_INTERVAL]. + pub fn set_flush_interval(mut self, interval: usize) -> Self { + self.flush_interval = interval; + self + } + + /// Builds the Writer instance consuming this builder. + pub fn build(self) -> AvrowResult> { + // write the metadata + // Writer::with_codec(&self.schema, self.datafile, self.codec) + let mut writer = Writer { + out_stream: self.datafile.ok_or(AvrowErr::WriterBuildFailed)?, + schema: self.schema.ok_or(AvrowErr::WriterBuildFailed)?, + block_stream: Vec::with_capacity(self.flush_interval), + block_count: 0, + codec: self.codec, + sync_marker: sync_marker(), + flush_interval: self.flush_interval, + }; + writer.encode_custom_header(self.metadata)?; + Ok(writer) + } +} + +impl<'a, W: Write> Default for WriterBuilder<'a, W> { + fn default() -> Self { + Self::new() + } +} + +/// The Writer is the primary interface for writing values to an avro datafile or a byte container (say a `Vec`). +/// It takes a reference to the schema for validating the values being written +/// and an output stream W which can be any type +/// implementing the [Write](https://doc.rust-lang.org/std/io/trait.Write.html) trait. +pub struct Writer<'a, W> { + out_stream: W, + schema: &'a Schema, + block_stream: Vec, + block_count: usize, + codec: Codec, + sync_marker: [u8; 16], + flush_interval: usize, +} + +impl<'a, W: Write> Writer<'a, W> { + /// Creates a new avro Writer instance taking a reference to a `Schema` and and a `Write`. + pub fn new(schema: &'a Schema, out_stream: W) -> AvrowResult { + let mut writer = Writer { + out_stream, + schema, + block_stream: Vec::with_capacity(DEFAULT_FLUSH_INTERVAL), + block_count: 0, + codec: Codec::Null, + sync_marker: sync_marker(), + flush_interval: DEFAULT_FLUSH_INTERVAL, + }; + writer.encode_header()?; + Ok(writer) + } + + /// Same as the new method, but additionally takes a `Codec` as parameter. + /// Codecs can be used to compress the encoded data being written in avro format. + /// Supported codecs as per spec are: + /// * null (default): No compression is applied. + /// * [snappy](https://en.wikipedia.org/wiki/Snappy_(compression)) (`--features snappy`) + /// * [deflate](https://en.wikipedia.org/wiki/DEFLATE) (`--features deflate`) + /// * [zstd](https://facebook.github.io/zstd/) compression (`--feature zstd`) + /// * [bzip](http://www.bzip.org/) compression (`--feature bzip`) + /// * [xz](https://tukaani.org/xz/) compression (`--features xz`) + pub fn with_codec(schema: &'a Schema, out_stream: W, codec: Codec) -> AvrowResult { + let mut writer = Writer { + out_stream, + schema, + block_stream: Vec::with_capacity(DEFAULT_FLUSH_INTERVAL), + block_count: 0, + codec, + sync_marker: sync_marker(), + flush_interval: DEFAULT_FLUSH_INTERVAL, + }; + writer.encode_header()?; + Ok(writer) + } + + /// Appends a value to the buffer. + /// Before a value gets written, it gets validated with the schema referenced + /// by this writer. + /// **Note**: writes are buffered internally as per the flush interval and the underlying + /// buffer may not reflect values immediately. + /// Call [`flush`](struct.Writer.html#method.flush) to explicitly write all buffered data. + /// Alternatively calling [`into_inner`](struct.Writer.html#method.into_inner) on the writer + /// guarantees that flush will happen and will hand over + /// the underlying buffer with all data written. + pub fn write>(&mut self, value: T) -> AvrowResult<()> { + let val: Value = value.into(); + self.schema.validate(&val)?; + + val.encode( + &mut self.block_stream, + &self.schema.variant(), + &self.schema.cxt, + )?; + self.block_count += 1; + + if self.block_stream.len() >= self.flush_interval { + self.flush()?; + } + + Ok(()) + } + + /// Appends a native Rust value to the buffer. The value must implement Serde's `Serialize` trait. + pub fn serialize(&mut self, value: T) -> AvrowResult<()> { + let value = serde_avro::to_value(&value)?; + self.write(value)?; + Ok(()) + } + + fn reset_block_buffer(&mut self) { + self.block_count = 0; + self.block_stream.clear(); + } + + /// Sync/flush any buffered data to the underlying buffer. + /// Note: This method is called to ensure that all + pub fn flush(&mut self) -> AvrowResult<()> { + // bail if no data is written or it has already been flushed before + if self.block_count == 0 { + return Ok(()); + } + // encode datum count + encode_long(self.block_count as i64, &mut self.out_stream)?; + // encode with codec + self.codec + .encode(&mut self.block_stream, &mut self.out_stream)?; + // Write sync marker + encode_raw_bytes(&self.sync_marker, &mut self.out_stream)?; + // Reset block buffer + self.out_stream.flush().map_err(AvrowErr::EncodeFailed)?; + self.reset_block_buffer(); + Ok(()) + } + + // Used via WriterBuilder + fn encode_custom_header(&mut self, mut map: HashMap) -> AvrowResult<()> { + self.out_stream + .write(MAGIC_BYTES) + .map_err(AvrowErr::EncodeFailed)?; + map.insert("avro.schema".to_string(), self.schema.as_bytes().into()); + let codec_str = self.codec.as_ref().as_bytes(); + map.insert("avro.codec".to_string(), codec_str.into()); + let meta_schema = &Variant::Map { + values: Box::new(Variant::Bytes), + }; + + Value::Map(map).encode(&mut self.out_stream, meta_schema, &Registry::new())?; + encode_raw_bytes(&self.sync_marker, &mut self.out_stream)?; + Ok(()) + } + + fn encode_header(&mut self) -> AvrowResult<()> { + self.out_stream + .write(MAGIC_BYTES) + .map_err(AvrowErr::EncodeFailed)?; + // encode metadata + let mut metamap = Map::with_capacity(2); + metamap.insert("avro.schema".to_string(), self.schema.as_bytes().into()); + let codec_str = self.codec.as_ref().as_bytes(); + metamap.insert("avro.codec".to_string(), codec_str.into()); + let meta_schema = &Variant::Map { + values: Box::new(Variant::Bytes), + }; + + Value::Map(metamap).encode(&mut self.out_stream, meta_schema, &Registry::new())?; + encode_raw_bytes(&self.sync_marker, &mut self.out_stream)?; + Ok(()) + } + + /// Consumes self and yields the inner Write instance. + /// Additionally calls flush if no flush has happened before this call. + pub fn into_inner(mut self) -> AvrowResult { + self.flush()?; + Ok(self.out_stream) + } +} + +#[cfg(test)] +mod tests { + use crate::{from_value, Codec, Reader, Schema, Writer, WriterBuilder}; + use std::io::Cursor; + use std::str::FromStr; + + #[test] + fn header_written_on_writer_creation() { + let schema = Schema::from_str(r##""null""##).unwrap(); + let v = Cursor::new(vec![]); + let writer = Writer::new(&schema, v).unwrap(); + let buf = writer.into_inner().unwrap().into_inner(); + // writer. + let slice = &buf[0..4]; + + assert_eq!(slice[0], b'O'); + assert_eq!(slice[1], b'b'); + assert_eq!(slice[2], b'j'); + assert_eq!(slice[3], 1); + } + + #[test] + fn writer_with_builder() { + let schema = Schema::from_str(r##""null""##).unwrap(); + let v = vec![]; + let mut writer = WriterBuilder::new() + .set_codec(Codec::Null) + .set_schema(&schema) + .set_datafile(v) + .set_flush_interval(128_000) + .build() + .unwrap(); + writer.serialize(()).unwrap(); + let _v = writer.into_inner().unwrap(); + + let reader = Reader::with_schema(_v.as_slice(), schema).unwrap(); + for i in reader { + let _: () = from_value(&i).unwrap(); + } + } + + #[test] + fn custom_metadata_header() { + let schema = Schema::from_str(r##""null""##).unwrap(); + let v = vec![]; + let mut writer = WriterBuilder::new() + .set_codec(Codec::Null) + .set_schema(&schema) + .set_datafile(v) + .set_flush_interval(128_000) + .set_metadata("hello", "world") + .build() + .unwrap(); + writer.serialize(()).unwrap(); + let _v = writer.into_inner().unwrap(); + + let reader = Reader::with_schema(_v.as_slice(), schema).unwrap(); + assert!(reader.meta().contains_key("hello")); + } +} diff --git a/tests/common.rs b/tests/common.rs new file mode 100644 index 0000000..3e71d5e --- /dev/null +++ b/tests/common.rs @@ -0,0 +1,90 @@ +#![allow(dead_code)] + +use avrow::Codec; +use avrow::Schema; +use avrow::{Reader, Writer}; +use std::io::Cursor; +use std::str::FromStr; + +#[derive(Debug)] +pub(crate) enum Primitive { + Null, + Boolean, + Int, + Long, + Float, + Double, + Bytes, + String, +} + +impl std::fmt::Display for Primitive { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use Primitive::*; + let str_repr = match self { + Null => "null", + Boolean => "boolean", + Int => "int", + Long => "long", + Float => "float", + Double => "double", + Bytes => "bytes", + String => "string", + }; + write!(f, "{}", str_repr) + } +} + +pub(crate) fn writer_from_schema<'a>(schema: &'a Schema, codec: Codec) -> Writer<'a, Vec> { + let writer = Writer::with_codec(&schema, vec![], codec).unwrap(); + writer +} + +pub(crate) fn reader_with_schema<'a>(schema: Schema, buffer: Vec) -> Reader>> { + let reader = Reader::with_schema(Cursor::new(buffer), schema).unwrap(); + reader +} + +pub(crate) struct MockSchema; +impl MockSchema { + // creates a primitive schema + pub fn prim(self, ty: &str) -> Schema { + let schema_str = format!("{{\"type\": \"{}\"}}", ty); + Schema::from_str(&schema_str).unwrap() + } + + pub fn record(self) -> Schema { + Schema::from_str( + r#" + { + "type": "record", + "name": "LongList", + "aliases": ["LinkedLongs"], + "fields" : [ + {"name": "value", "type": "long"}, + {"name": "next", "type": ["null", "LongList"]} + ] + } + "#, + ) + .unwrap() + } + + pub fn record_default(self) -> Schema { + Schema::from_str( + r#" + { + "type": "record", + "name": "LongList", + "aliases": ["LinkedLongs"], + "fields" : [ + {"name": "value", "type": "long"}, + {"name": "next", "type": ["null", "LongList"]}, + {"name": "other", "type":"long", "default": 1} + ] + } + "#, + ) + .unwrap() + } +} diff --git a/tests/read_write.rs b/tests/read_write.rs new file mode 100644 index 0000000..28530d6 --- /dev/null +++ b/tests/read_write.rs @@ -0,0 +1,414 @@ +extern crate pretty_env_logger; +extern crate serde_derive; + +mod common; + +use avrow::{from_value, Reader, Schema, Codec, Value}; +use std::str::FromStr; +use crate::common::{MockSchema, writer_from_schema}; +use std::collections::HashMap; + + +use common::{Primitive}; +use serde_derive::{Deserialize, Serialize}; + +const DATUM_COUNT: usize = 10000; + +/////////////////////////////////////////////////////////////////////////////// +/// Primitive schema tests +/////////////////////////////////////////////////////////////////////////////// + +// #[cfg(feature = "codec")] +static PRIMITIVES: [Primitive; 8] = [ + Primitive::Null, + Primitive::Boolean, + Primitive::Int, + Primitive::Long, + Primitive::Float, + Primitive::Double, + Primitive::Bytes, + Primitive::String, +]; + +// static PRIMITIVES: [Primitive; 1] = [Primitive::Int]; + +#[cfg(feature = "codec")] +const CODECS: [Codec; 6] = [ + Codec::Null, + Codec::Deflate, + Codec::Snappy, + Codec::Zstd, + Codec::Bzip2, + Codec::Xz, +]; + +// #[cfg(feature = "bzip2")] +// const CODECS: [Codec; 1] = [Codec::Bzip2]; + +#[test] +#[cfg(feature = "codec")] +fn read_write_primitive() { + for codec in CODECS.iter() { + for primitive in PRIMITIVES.iter() { + // write + let name = &format!("{}", primitive); + let schema = MockSchema.prim(name); + let mut writer = writer_from_schema(&schema, *codec); + (0..DATUM_COUNT).for_each(|i| match primitive { + Primitive::Null => { + writer.write(()).unwrap(); + } + Primitive::Boolean => { + writer.write(i % 2 == 0).unwrap(); + } + Primitive::Int => { + writer.write(std::i32::MAX).unwrap(); + } + Primitive::Long => { + writer.write(std::i64::MAX).unwrap(); + } + Primitive::Float => { + writer.write(std::f32::MAX).unwrap(); + } + Primitive::Double => { + writer.write(std::f64::MAX).unwrap(); + } + Primitive::Bytes => { + writer.write(vec![b'a', b'v', b'r', b'o', b'w']).unwrap(); + } + Primitive::String => { + writer.write("avrow").unwrap(); + } + }); + + let buf = writer.into_inner().unwrap(); + + // read + let reader = Reader::with_schema(buf.as_slice(), MockSchema.prim(name)).unwrap(); + for i in reader { + match primitive { + Primitive::Null => { + let _: () = from_value(&i).unwrap(); + } + Primitive::Boolean => { + let _: bool = from_value(&i).unwrap(); + } + Primitive::Int => { + let _: i32 = from_value(&i).unwrap(); + } + Primitive::Long => { + let _: i64 = from_value(&i).unwrap(); + } + Primitive::Float => { + let _: f32 = from_value(&i).unwrap(); + } + Primitive::Double => { + let _: f64 = from_value(&i).unwrap(); + } + Primitive::Bytes => { + let _: &[u8] = from_value(&i).unwrap(); + } + Primitive::String => { + let _: &str = from_value(&i).unwrap(); + } + } + } + } + } +} + +/////////////////////////////////////////////////////////////////////////////// +/// Complex schema tests +/////////////////////////////////////////////////////////////////////////////// + +#[derive(Debug, Serialize, Deserialize)] +struct LongList { + value: i64, + next: Option>, +} + +#[test] +#[cfg(feature = "codec")] +fn io_read_write_self_referential_record() { + // write + for codec in CODECS.iter() { + let schema = r##" + { + "type": "record", + "name": "LongList", + "aliases": ["LinkedLongs"], + "fields" : [ + {"name": "value", "type": "long"}, + {"name": "next", "type": ["null", "LongList"]} + ] + } + "##; + + let schema = Schema::from_str(schema).unwrap(); + let mut writer = writer_from_schema(&schema, *codec); + for _ in 0..1 { + let value = LongList { + value: 1i64, + next: Some(Box::new(LongList { + value: 2, + next: Some(Box::new(LongList { + value: 3, + next: None, + })), + })), + }; + // let value = LongList { + // value: 1i64, + // next: None, + // }; + writer.serialize(value).unwrap(); + } + + let buf = writer.into_inner().unwrap(); + + // read + let reader = Reader::with_schema(buf.as_slice(), schema).unwrap(); + for i in reader { + let _: LongList = from_value(&i).unwrap(); + } + } +} + +#[derive(Serialize, Deserialize)] +enum Suit { + SPADES, + HEARTS, + DIAMONDS, + CLUBS, +} + +#[test] +#[cfg(feature = "codec")] +fn enum_read_write() { + // write + for codec in CODECS.iter() { + let schema = r##" + { + "type": "enum", + "name": "Suit", + "symbols" : ["SPADES", "HEARTS", "DIAMONDS", "CLUBS"] + } + "##; + + let schema = Schema::from_str(schema).unwrap(); + let mut writer = writer_from_schema(&schema, *codec); + for _ in 0..1 { + let value = Suit::SPADES; + writer.serialize(value).unwrap(); + } + + let buf = writer.into_inner().unwrap(); + + // read + let reader = Reader::with_schema(buf.as_slice(), schema).unwrap(); + for i in reader { + let _: Suit = from_value(&i).unwrap(); + } + } +} + +#[test] +#[cfg(feature = "codec")] +fn array_read_write() { + // write + for codec in CODECS.iter() { + let schema = r##" + {"type": "array", "items": "string"} + "##; + + let schema = Schema::from_str(schema).unwrap(); + let mut writer = writer_from_schema(&schema, *codec); + for _ in 0..DATUM_COUNT { + let value = vec!["a", "v", "r", "o", "w"]; + writer.serialize(value).unwrap(); + } + + let buf = writer.into_inner().unwrap(); + + // read + let reader = Reader::with_schema(buf.as_slice(), schema).unwrap(); + for i in reader { + let _: Vec<&str> = from_value(&i).unwrap(); + } + } +} + +#[test] +#[cfg(feature = "codec")] +fn map_read_write() { + // write + for codec in CODECS.iter() { + let schema = r##" + {"type": "map", "values": "long"} + "##; + + let schema = Schema::from_str(schema).unwrap(); + let mut writer = writer_from_schema(&schema, *codec); + for _ in 0..DATUM_COUNT { + let mut value = HashMap::new(); + value.insert("foo", 1i64); + value.insert("bar", 2); + writer.serialize(value).unwrap(); + } + + let buf = writer.into_inner().unwrap(); + + // read + let reader = Reader::with_schema(buf.as_slice(), schema).unwrap(); + for i in reader { + let _: HashMap = from_value(&i).unwrap(); + } + } +} + +#[test] +#[cfg(feature = "codec")] +fn union_read_write() { + // write + for codec in CODECS.iter() { + let schema = r##" + ["null", "string"] + "##; + + let schema = Schema::from_str(schema).unwrap(); + let mut writer = writer_from_schema(&schema, *codec); + for _ in 0..1 { + writer.serialize(()).unwrap(); + writer.serialize("hello".to_string()).unwrap(); + } + + let buf = writer.into_inner().unwrap(); + + // read + let reader = Reader::with_schema(buf.as_slice(), schema).unwrap(); + for i in reader { + let val = i.as_ref().unwrap(); + match val { + Value::Null => { + let _a: () = from_value(&i).unwrap(); + } + Value::Str(_) => { + let _a: &str = from_value(&i).unwrap(); + } + _ => unreachable!("should not happen"), + } + } + } +} + +#[test] +#[cfg(feature = "codec")] +fn fixed_read_write() { + // write + for codec in CODECS.iter() { + let schema = r##" + {"type": "fixed", "size": 16, "name": "md5"} + "##; + + let schema = Schema::from_str(schema).unwrap(); + let mut writer = writer_from_schema(&schema, *codec); + for _ in 0..1 { + let value = vec![ + b'1', b'2', b'3', b'4', b'5', b'6', b'7', b'8', b'9', b'a', b'b', b'c', b'd', b'e', + b'f', b'g', + ]; + writer.serialize(value.as_slice()).unwrap(); + } + + let buf = writer.into_inner().unwrap(); + + // read + let reader = Reader::with_schema(buf.as_slice(), schema).unwrap(); + for i in reader { + let a: [u8; 16] = from_value(&i).unwrap(); + assert_eq!(a.len(), 16); + } + } +} + +#[test] +#[cfg(feature = "codec")] +fn bytes_read_write() { + let schema = Schema::from_str(r##"{"type": "bytes"}"##).unwrap(); + let mut writer = writer_from_schema(&schema, avrow::Codec::Deflate); + let data = vec![0u8, 1u8, 2u8, 3u8, 4u8, 5u8]; + writer.serialize(&data).unwrap(); + + let buf = writer.into_inner().unwrap(); + // let mut v: Vec = vec![]; + + let reader = Reader::with_schema(buf.as_slice(), schema).unwrap(); + for i in reader { + // dbg!(i); + let b: &[u8] = from_value(&i).unwrap(); + dbg!(b); + } + + // assert_eq!(v, data); +} + +#[test] +#[should_panic] +#[cfg(feature = "codec")] +fn write_invalid_union_data_fails() { + let schema = Schema::from_str(r##"["int", "float"]"##).unwrap(); + let mut writer = writer_from_schema(&schema, avrow::Codec::Null); + writer.serialize("string").unwrap(); +} + +// #[derive(Debug, serde::Serialize, serde::Deserialize)] +// struct LongList { +// value: i64, +// next: Option>, +// } + +#[test] +#[cfg(feature = "snappy")] +fn read_deflate_reuse() { + let schema = Schema::from_str( + r##" + { + "type": "record", + "name": "LongList", + "aliases": ["LinkedLongs"], + "fields" : [ + {"name": "value", "type": "long"}, + {"name": "next", "type": ["null", "LongList"]} + ] + } + "##, + ) + .unwrap(); + let vec = vec![]; + let mut writer = avrow::Writer::with_codec(&schema, vec, Codec::Snappy).unwrap(); + for _ in 0..100000 { + let value = LongList { + value: 1i64, + next: Some(Box::new(LongList { + value: 2i64, + next: Some(Box::new(LongList { + value: 3i64, + next: Some(Box::new(LongList { + value: 4i64, + next: Some(Box::new(LongList { + value: 5i64, + next: None, + })), + })), + })), + })), + }; + writer.serialize(value).unwrap(); + } + let vec = writer.into_inner().unwrap(); + + let reader = Reader::new(&*vec).unwrap(); + for i in reader { + let _v: LongList = from_value(&i).unwrap(); + } +} diff --git a/tests/schema_resolution.rs b/tests/schema_resolution.rs new file mode 100644 index 0000000..7747e86 --- /dev/null +++ b/tests/schema_resolution.rs @@ -0,0 +1,315 @@ +/// Tests for schema resolution +mod common; + +use serde::{Deserialize, Serialize}; + +use avrow::{from_value, Codec, Reader, Schema, Value}; +use std::collections::HashMap; +use std::str::FromStr; + +use common::{reader_with_schema, writer_from_schema, MockSchema}; + +#[test] +#[should_panic] +fn null_fails_with_other_primitive_schema() { + let name = "null"; + let schema = MockSchema.prim(name); + let mut writer = writer_from_schema(&schema, Codec::Null); + writer.serialize(()).unwrap(); + writer.flush().unwrap(); + + let buf = writer.into_inner().unwrap(); + + let reader_schema = MockSchema.prim("boolean"); + let reader = Reader::with_schema(buf.as_slice(), reader_schema).unwrap(); + + for i in reader { + let _ = i.unwrap(); + } +} + +#[test] +fn writer_to_reader_promotion_primitives() { + // int -> long, float, double + for reader_schema in &["long", "float", "double"] { + let name = "int"; + let schema = MockSchema.prim(name); + let mut writer = writer_from_schema(&schema, Codec::Null); + writer.serialize(1024).unwrap(); + writer.flush().unwrap(); + + let buf = writer.into_inner().unwrap(); + + let reader_schema = MockSchema.prim(reader_schema); + let reader = Reader::with_schema(buf.as_slice(), reader_schema).unwrap(); + for i in reader { + assert!(i.is_ok()); + let _a = i.unwrap(); + } + } + + // long -> float, double + for reader_schema in &["float", "double"] { + let name = "long"; + let schema = MockSchema.prim(name); + let mut writer = writer_from_schema(&schema, Codec::Null); + writer.serialize(1024i64).unwrap(); + writer.flush().unwrap(); + + let buf = writer.into_inner().unwrap(); + + let reader_schema = MockSchema.prim(reader_schema); + let reader = Reader::with_schema(buf.as_slice(), reader_schema).unwrap(); + for i in reader { + assert!(i.is_ok()); + } + } + + // float -> double + for reader_schema in &["double"] { + let name = "float"; + let schema = MockSchema.prim(name); + let mut writer = writer_from_schema(&schema, Codec::Null); + writer.serialize(1026f32).unwrap(); + writer.flush().unwrap(); + + let buf = writer.into_inner().unwrap(); + + let reader_schema = MockSchema.prim(reader_schema); + let reader = Reader::with_schema(buf.as_slice(), reader_schema).unwrap(); + for i in reader { + assert!(i.is_ok()); + } + } + + // string -> bytes + for reader_schema in &["bytes"] { + let name = "string"; + let schema = MockSchema.prim(name); + let mut writer = writer_from_schema(&schema, Codec::Null); + writer.serialize("hello").unwrap(); + writer.flush().unwrap(); + + let buf = writer.into_inner().unwrap(); + + let reader_schema = MockSchema.prim(reader_schema); + let reader = Reader::with_schema(buf.as_slice(), reader_schema).unwrap(); + for i in reader { + assert!(i.is_ok()); + let a = i.unwrap(); + assert_eq!(Value::Bytes(vec![104, 101, 108, 108, 111]), a); + } + } + + // bytes -> string + for reader_schema in &["string"] { + let name = "bytes"; + let schema = MockSchema.prim(name); + let mut writer = writer_from_schema(&schema, Codec::Null); + writer.serialize([104u8, 101, 108, 108, 111]).unwrap(); + writer.flush().unwrap(); + + let buf = writer.into_inner().unwrap(); + + let reader_schema = MockSchema.prim(reader_schema); + let reader = Reader::with_schema(buf.as_slice(), reader_schema).unwrap(); + for i in reader { + assert!(i.is_ok()); + let a = i.unwrap(); + assert_eq!(Value::Str("hello".to_string()), a); + } + } +} + +#[derive(Serialize, Deserialize)] +enum Foo { + A, + B, + C, + E, +} + +#[test] +#[should_panic] +fn enum_fails_schema_resolution() { + let schema = + Schema::from_str(r##"{"type": "enum", "name": "Foo", "symbols": ["A", "B", "C", "D"] }"##) + .unwrap(); + let mut writer = writer_from_schema(&schema, Codec::Null); + writer.serialize(Foo::B).unwrap(); + writer.flush().unwrap(); + + let buf = writer.into_inner().unwrap(); + + // Reading a symbol which does not exist in writer's schema should fail + let reader_schema = + Schema::from_str(r##"{"type": "enum", "name": "Foo", "symbols": ["F"] }"##).unwrap(); + let reader = Reader::with_schema(buf.as_slice(), reader_schema).unwrap(); + + // let reader = reader_with_schema(reader_schema, name); + for i in reader { + i.unwrap(); + } +} + +#[test] +#[should_panic] +fn schema_resolution_map() { + let schema = Schema::from_str(r##"{"type": "map", "values": "string"}"##).unwrap(); + let mut writer = writer_from_schema(&schema, Codec::Null); + let mut m = HashMap::new(); + m.insert("1", "b"); + writer.serialize(m).unwrap(); + writer.flush().unwrap(); + + let buf = writer.into_inner().unwrap(); + + // // Reading a symbol which does not exist in writer's schema should fail + let reader_schema = Schema::from_str(r##"{"type": "map", "values": "int"}"##).unwrap(); + + let reader = reader_with_schema(reader_schema, buf); + for i in reader { + let _ = i.unwrap(); + } +} + +#[derive(Serialize, Deserialize)] +struct LongList { + value: i64, + next: Option>, +} + +#[derive(Serialize, Deserialize, Debug)] +struct LongListDefault { + value: i64, + next: Option>, + other: i64, +} + +#[test] +fn record_schema_resolution_with_default_value() { + let schema = MockSchema.record(); + let mut writer = writer_from_schema(&schema, Codec::Null); + let list = LongList { + value: 1, + next: None, + }; + + writer.serialize(list).unwrap(); + + let buf = writer.into_inner().unwrap(); + + let schema = MockSchema.record_default(); + let reader = reader_with_schema(schema, buf); + for i in reader { + let rec: Result = from_value(&i); + assert!(rec.is_ok()); + } +} + +#[test] +#[cfg(feature = "codec")] +fn writer_is_a_union_but_reader_is_not() { + let writer_schema = Schema::from_str(r##"["null", "int"]"##).unwrap(); + let mut writer = writer_from_schema(&writer_schema, Codec::Deflate); + writer.serialize(()).unwrap(); + writer.serialize(3).unwrap(); + + let buf = writer.into_inner().unwrap(); + + let schema_str = r##""int""##; + let reader_schema = Schema::from_str(schema_str).unwrap(); + let mut reader = reader_with_schema(reader_schema, buf); + assert!(reader.next().unwrap().is_err()); + assert!(reader.next().unwrap().is_ok()); +} + +#[test] +fn reader_is_a_union_but_writer_is_not() { + let writer_schema = Schema::from_str(r##""int""##).unwrap(); + let mut writer = writer_from_schema(&writer_schema, Codec::Null); + writer.serialize(3).unwrap(); + + let buf = writer.into_inner().unwrap(); + + // err + let reader_schema = Schema::from_str(r##"["null", "string"]"##).unwrap(); + let mut reader = reader_with_schema(reader_schema, buf.clone()); + assert!(reader.next().unwrap().is_err()); + + // ok + let reader_schema = Schema::from_str(r##"["null", "int"]"##).unwrap(); + let mut reader = reader_with_schema(reader_schema, buf); + assert!(reader.next().unwrap().is_ok()); +} + +#[test] +fn both_are_unions_but_different() { + let writer_schema = Schema::from_str(r##"["null", "int"]"##).unwrap(); + let mut writer = writer_from_schema(&writer_schema, Codec::Null); + writer.serialize(3).unwrap(); + + let buf = writer.into_inner().unwrap(); + + let reader_schema = Schema::from_str(r##"["boolean", "string"]"##).unwrap(); + let mut reader = reader_with_schema(reader_schema, buf); + + // err + assert!(reader.next().unwrap().is_err()); +} + +#[test] +fn both_are_map() { + let writer_schema = Schema::from_str(r##"{"type": "map", "values": "string"}"##).unwrap(); + let mut writer = writer_from_schema(&writer_schema, Codec::Null); + let mut map = HashMap::new(); + map.insert("hello", "world"); + writer.serialize(map).unwrap(); + + let buf = writer.into_inner().unwrap(); + + // let reader_schema = + // Schema::from_str(r##"["boolean", {"type":"map", "values": "string"}]"##).unwrap(); + let reader_schema = Schema::from_str(r##"{"type": "map", "values": "string"}"##).unwrap(); + let mut reader = reader_with_schema(reader_schema, buf); + assert!(reader.next().unwrap().is_ok()); +} + +#[test] +fn both_are_arrays() { + let writer_schema = Schema::from_str(r##"{"type": "array", "items": "int"}"##).unwrap(); + let mut writer = writer_from_schema(&writer_schema, Codec::Null); + writer.serialize(vec![1, 2, 3]).unwrap(); + + let buf = writer.into_inner().unwrap(); + + let reader_schema = Schema::from_str(r##"{"type": "array", "items": "int"}"##).unwrap(); + let mut reader = reader_with_schema(reader_schema, buf); + assert!(reader.next().unwrap().is_ok()); +} + +#[test] +fn both_are_enums() { + let writer_schema = Schema::from_str(r##"{"type": "array", "items": "int"}"##).unwrap(); + let mut writer = writer_from_schema(&writer_schema, Codec::Null); + writer.serialize(vec![1, 2, 3]).unwrap(); + + let buf = writer.into_inner().unwrap(); + + let reader_schema = Schema::from_str(r##"{"type": "array", "items": "int"}"##).unwrap(); + let mut reader = reader_with_schema(reader_schema, buf); + assert!(reader.next().unwrap().is_ok()); +} + +#[test] +fn null() { + let writer_schema = Schema::from_str(r##"{"type": "null"}"##).unwrap(); + let mut writer = writer_from_schema(&writer_schema, Codec::Null); + writer.serialize(()).unwrap(); + + let buf = writer.into_inner().unwrap(); + + let reader_schema = Schema::from_str(r##"{"type": "null"}"##).unwrap(); + let mut reader = reader_with_schema(reader_schema, buf); + assert!(reader.next().unwrap().is_ok()); +} From b9c52471874cfc8826143440056f9d08bf95e818 Mon Sep 17 00:00:00 2001 From: creativcoder Date: Thu, 8 Oct 2020 23:35:35 +0530 Subject: [PATCH 2/5] Add github pages site --- .github/workflows/ci.yml | 52 -- .vscode/launch.json | 16 - CHANGELOG.md | 16 - CODE_OF_CONDUCT.md | 75 --- CONTRIBUTING.md | 26 - Cargo.toml | 73 --- LICENSE-APACHE | 201 ------- LICENSE-MIT | 23 - README.md | 425 -------------- avrow-cli/Cargo.toml | 18 - avrow-cli/README.md | 31 -- avrow-cli/src/main.rs | 43 -- avrow-cli/src/subcommand.rs | 157 ------ avrow-cli/src/utils.rs | 11 - assets/avrow_logo.png => avrow_logo.png | Bin benches/complex.rs | 150 ----- benches/primitives.rs | 149 ----- benches/schema.rs | 61 -- benches/write.rs | 1 - examples/canonical.rs | 24 - examples/from_json_to_struct.rs | 72 --- examples/hello_world.rs | 41 -- examples/recursive_record.rs | 56 -- examples/writer_builder.rs | 23 - index.html | 79 +++ rustfmt.toml | 2 - src/codec.rs | 273 --------- src/config.rs | 15 - src/error.rs | 184 ------ src/lib.rs | 81 --- src/reader.rs | 707 ----------------------- src/schema/canonical.rs | 259 --------- src/schema/common.rs | 360 ------------ src/schema/mod.rs | 258 --------- src/schema/parser.rs | 494 ----------------- src/schema/tests.rs | 437 --------------- src/serde_avro/de.rs | 170 ------ src/serde_avro/de_impl.rs | 193 ------- src/serde_avro/mod.rs | 8 - src/serde_avro/ser.rs | 261 --------- src/serde_avro/ser_impl.rs | 195 ------- src/util.rs | 34 -- src/value.rs | 710 ------------------------ src/writer.rs | 318 ----------- tests/common.rs | 90 --- tests/read_write.rs | 414 -------------- tests/schema_resolution.rs | 315 ----------- 47 files changed, 79 insertions(+), 7522 deletions(-) delete mode 100644 .github/workflows/ci.yml delete mode 100644 .vscode/launch.json delete mode 100644 CHANGELOG.md delete mode 100644 CODE_OF_CONDUCT.md delete mode 100644 CONTRIBUTING.md delete mode 100644 Cargo.toml delete mode 100644 LICENSE-APACHE delete mode 100644 LICENSE-MIT delete mode 100644 README.md delete mode 100644 avrow-cli/Cargo.toml delete mode 100644 avrow-cli/README.md delete mode 100644 avrow-cli/src/main.rs delete mode 100644 avrow-cli/src/subcommand.rs delete mode 100644 avrow-cli/src/utils.rs rename assets/avrow_logo.png => avrow_logo.png (100%) delete mode 100644 benches/complex.rs delete mode 100644 benches/primitives.rs delete mode 100644 benches/schema.rs delete mode 100644 benches/write.rs delete mode 100644 examples/canonical.rs delete mode 100644 examples/from_json_to_struct.rs delete mode 100644 examples/hello_world.rs delete mode 100644 examples/recursive_record.rs delete mode 100644 examples/writer_builder.rs create mode 100644 index.html delete mode 100644 rustfmt.toml delete mode 100644 src/codec.rs delete mode 100644 src/config.rs delete mode 100644 src/error.rs delete mode 100644 src/lib.rs delete mode 100644 src/reader.rs delete mode 100644 src/schema/canonical.rs delete mode 100644 src/schema/common.rs delete mode 100644 src/schema/mod.rs delete mode 100644 src/schema/parser.rs delete mode 100644 src/schema/tests.rs delete mode 100644 src/serde_avro/de.rs delete mode 100644 src/serde_avro/de_impl.rs delete mode 100644 src/serde_avro/mod.rs delete mode 100644 src/serde_avro/ser.rs delete mode 100644 src/serde_avro/ser_impl.rs delete mode 100644 src/util.rs delete mode 100644 src/value.rs delete mode 100644 src/writer.rs delete mode 100644 tests/common.rs delete mode 100644 tests/read_write.rs delete mode 100644 tests/schema_resolution.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index a0e6c42..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,52 +0,0 @@ -on: [push, pull_request] - -jobs: - linux: - name: Test Suite (linux) - runs-on: ubuntu-latest - strategy: - matrix: - rust: - - stable - - nightly - - 1.37.0 - steps: - - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 - with: - toolchain: ${{ matrix.rust }} - - run: cargo test --release --all-features - - windows: - name: Test suite (windows) - runs-on: windows-latest - steps: - - uses: actions/checkout@v2 - - run: cargo test --all-features - - lints: - name: Lints - runs-on: ubuntu-latest - steps: - - name: Checkout sources - uses: actions/checkout@v2 - - - name: Install stable toolchain - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true - components: rustfmt, clippy - - - name: Run cargo fmt - uses: actions-rs/cargo@v1 - with: - command: fmt - args: --all -- --check - - - name: Run cargo clippy - uses: actions-rs/cargo@v1 - with: - command: clippy - args: -- -D warnings \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 224af64..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "type": "lldb", - "request": "launch", - "name": "Debug", - "program": "${workspaceFolder}/", - "args": [], - "cwd": "${workspaceFolder}" - } - ] -} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index a4424f4..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,16 +0,0 @@ -# Changelog -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -# [Unreleased] - - -# [0.1.0] - 2020-10-08 - -## Added - -Initial implementation of -- avrow -- avrow-cli (av) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md deleted file mode 100644 index 4ff57d6..0000000 --- a/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,75 +0,0 @@ - -## Code of Conduct - -### Our Pledge - -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to making participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, gender identity and expression, level of experience, -nationality, personal appearance, race, religion, or sexual identity and -orientation. - -### Our Standards - -Examples of behavior that contributes to creating a positive environment -include: - -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members - -Examples of unacceptable behavior by participants include: - -* The use of sexualized language or imagery and unwelcome sexual attention or -advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic - address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting - -### Our Responsibilities - -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. - -Project maintainers have the right and responsibility to remove, edit, or -reject comments, commits, code, wiki edits, issues, and other contributions -that are not aligned to this Code of Conduct, or to ban temporarily or -permanently any contributor for other behaviors that they deem inappropriate, -threatening, offensive, or harmful. - -### Scope - -This Code of Conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. Examples of -representing a project or community include using an official project e-mail -address, posting via an official social media account, or acting as an appointed -representative at an online or offline event. Representation of a project may be -further defined and clarified by project maintainers. - -### Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at [INSERT EMAIL ADDRESS]. All -complaints will be reviewed and investigated and will result in a response that -is deemed necessary and appropriate to the circumstances. The project team is -obligated to maintain confidentiality with regard to the reporter of an incident. -Further details of specific enforcement policies may be posted separately. - -Project maintainers who do not follow or enforce the Code of Conduct in good -faith may face temporary or permanent repercussions as determined by other -members of the project's leadership. - -### Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, -available at [http://contributor-covenant.org/version/1/4][version] - -[homepage]: http://contributor-covenant.org -[version]: http://contributor-covenant.org/version/1/4/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 87bc366..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,26 +0,0 @@ - -# Contributing - -When contributing to this repository, please first discuss the change you wish to make via issue, -email, or any other method with the owners of this repository before making a change. - -Please note we have a [code of conduct](./CODE_OF_CONDUCT.md), please follow it in all your interactions with the project. - -## Pull Request Process - -Following is a cursory guideline on how to make the process of making changes more efficient for the contributer and the maintainer. - -1. File an issue for the change you want to make. This way we can track the why of the change. - Get consensus from community for the change. -2. Clone the project and perform a fresh build. Create a branch with the naming "feature/issue-number. -3. Ensure that the PR only changes the parts of code which implements/solves the issue. This includes running - the linter (cargo fmt) and removing any extra spaces and any formatting that accidentally were made by - the code editor in use. -4. If your PR has changes that should also reflect in README.md, please update that as well. -5. Document non obvious changes and the `why` of your changes if it's unclear. -6. If you are adding a public API, add the documentation as well. -7. Increase the version numbers in Cargo.toml files and the README.md to the new version that this - Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/). -8. Update the CHANGELOG.md to reflect the change if applicable. - -More details: https://github.community/t/best-practices-for-pull-requests/10195 \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml deleted file mode 100644 index 1738267..0000000 --- a/Cargo.toml +++ /dev/null @@ -1,73 +0,0 @@ -[package] -name = "avrow" -version = "0.1.0" -authors = ["creativcoder "] -edition = "2018" -repository = "https://github.com/creativcoder/avrow" -license = "MIT OR Apache-2.0" -description = "Avrow is a fast, type safe serde based data serialization library" -homepage = "avrow.github.io" -documentation = "https://docs.rs/avrow" -readme = "README.md" -keywords = ["avro", "avrow", "rust-avro", "serde-avro","encoding", "kafka", "spark"] -categories = ["encoding", "compression", "command-line-utilities"] - -publish = false - -[dependencies] -serde = {version= "1", features=["derive"] } -serde_derive = "1" -serde_json = { version="1", features=["preserve_order"] } -rand = "0.4.2" -byteorder = "1" -integer-encoding = "2" -snap = { version = "0.2", optional = true } -flate2 = { version = "1", features = ["zlib"], default-features = false, optional = true } -crc = "1" -thiserror = "1.0" -indexmap = {version = "1", features = ["serde-1"]} -once_cell = "1.4.1" -zstdd = { version = "0.5.3", optional = true, package="zstd" } -bzip2 = { version = "0.4.1", optional = true } -xz2 = { version = "0.1", optional = true } -shatwo = { version = "0.9.1", optional = true, package="sha2" } -mdfive = { version = "0.7.0", optional = true, package="md5" } - -[dev-dependencies] -criterion = "0.2" -pretty_env_logger = "0.4" -fstrings = "0.2" -env_logger = "0.4" -anyhow = "1.0.32" - -[[bench]] -name = "primitives" -harness = false - -[[bench]] -name = "complex" -harness = false - -[[bench]] -name = "schema" -harness = false - -[features] -# compression codecs -snappy = ["snap"] -deflate = ["flate2"] -zstd = ["zstdd"] -bzip = ["bzip2"] -xz = ["xz2"] -# fingerprint codecs -sha2 = ["shatwo"] -md5 = ["mdfive"] - -codec = ["snappy", "deflate", "zstd", "bzip2", "xz"] -fingerprint = ["sha2", "md5"] -all = ["codec", "fingerprint"] - -[profile.release] -opt-level = 'z' -lto = true -codegen-units = 1 diff --git a/LICENSE-APACHE b/LICENSE-APACHE deleted file mode 100644 index 16fe87b..0000000 --- a/LICENSE-APACHE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed 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. diff --git a/LICENSE-MIT b/LICENSE-MIT deleted file mode 100644 index 31aa793..0000000 --- a/LICENSE-MIT +++ /dev/null @@ -1,23 +0,0 @@ -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md deleted file mode 100644 index 97b15f5..0000000 --- a/README.md +++ /dev/null @@ -1,425 +0,0 @@ -

- avrow - -[![github actions](https://github.com/creativcoder/avrow/workflows/Rust/badge.svg)](https://github.com/creativcoder/avrow/actions) -[![crates](https://img.shields.io/crates/v/avrow.svg)](https://crates.io/crates/io-uring) -[![docs.rs](https://docs.rs/avrow/badge.svg)](https://docs.rs/avrow/) -[![license](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/creativcoder/avrow/blob/master/LICENSE-MIT) -[![license](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/creativcoder/avrow/blob/master/LICENSE-APACHE) -[![Contributor Covenant](https://img.shields.io/badge/contributor%20covenant-v1.4%20adopted-ff69b4.svg)](CODE_OF_CONDUCT.md) - -
-
- - -### Avrow is a pure Rust implementation of the [Avro specification](https://avro.apache.org/docs/current/spec.html) with [Serde](https://github.com/serde-rs/serde) support. - - -
-
- -
- -### Table of Contents -- [Overview](#overview) -- [Features](#features) -- [Getting started](#getting-started) -- [Examples](#examples) - - [Writing avro data](#writing-avro-data) - - [Reading avro data](#reading-avro-data) - - [Writer builder](#writer-customization) -- [Supported Codecs](#supported-codecs) -- [Using the avrow-cli tool](#using-avrow-cli-tool) -- [Benchmarks](#benchmarks) -- [Todo](#todo) -- [Changelog](#changelog) -- [Contributions](#contributions) -- [Support](#support) -- [MSRV](#msrv) -- [License](#license) - -## Overview - -Avrow is a pure Rust implementation of the [Avro specification](https://avro.apache.org/docs/current/spec.html): a row based data serialization system. The Avro data serialization format finds its use quite a lot in big data streaming systems such as [Kafka](https://kafka.apache.org/) and [Spark](https://spark.apache.org/). -Within avro's context, an avro encoded file or byte stream is called a "data file". -To write data in avro encoded format, one needs a schema which is provided in json format. Here's an example of an avro schema represented in json: - -```json -{ - "type": "record", - "name": "LongList", - "aliases": ["LinkedLongs"], - "fields" : [ - {"name": "value", "type": "long"}, - {"name": "next", "type": ["null", "LongList"]} - ] -} -``` -The above schema is of type record with fields and represents a linked list of 64-bit integers. In most implementations, this schema is then fed to a `Writer` instance along with a buffer to write encoded data to. One can then call one -of the `write` methods on the writer to write data. One distinguishing aspect of avro is that the schema for the encoded data is written on the header of the data file. This means that for reading data you don't need to provide a schema to a `Reader` instance. The spec also allows providing a reader schema to filter data when reading. - -The Avro specification provides two kinds of encoding: -* Binary encoding - Efficent and takes less space on disk. -* JSON encoding - When you want a readable version of avro encoded data. Also used for debugging purposes. - -This crate implements only the binary encoding as that's the format practically used for performance and storage reasons. - -## Features. - -* Full support for recursive self-referential schemas with Serde serialization/deserialization. -* All compressions codecs (`deflate`, `bzip2`, `snappy`, `xz`, `zstd`) supported as per spec. -* Simple and intuitive API - As the underlying structures in use are `Read` and `Write` types, avrow tries to mimic the same APIs as Rust's standard library APIs for minimal learning overhead. Writing avro values is simply calling `write` or `serialize` (with serde) and reading avro values is simply using iterators. -* Less bloat / Lightweight - Compile times in Rust are costly. Avrow tries to use minimal third-party crates. Compression codec and schema fingerprinting support are feature gated by default. To use them, compile with respective feature flags (e.g. `--features zstd`). -* Schema evolution - One can configure the avrow `Reader` with a reader schema and only read data relevant to their use case. -* Schema's in avrow supports querying their canonical form and have fingerprinting (`rabin64`, `sha256`, `md5`) support. - -**Note**: This is not a complete spec implemention and remaining features being implemented are listed under [Todo](#todo) section. - -## Getting started: - -Add avrow as a dependency to `Cargo.toml`: - -```toml -[dependencies] -avrow = "0.1" -``` - -## Examples: - -### Writing avro data - -```rust - -use anyhow::Error; -use avrow::{Schema, Writer}; -use std::str::FromStr; - -fn main() -> Result<(), Error> { - // Create schema from json - let schema = Schema::from_str(r##"{"type":"string"}"##)?; - // or from a path - let schema2 = Schema::from_path("./string_schema.avsc")?; - // Create an output stream - let stream = Vec::new(); - // Create a writer - let writer = Writer::new(&schema, stream.as_slice())?; - // Write your data! - let res = writer.write("Hey")?; - // or using serialize method for serde derived types. - let res = writer.serialize("there!")?; - - Ok(()) -} - -``` -For simple and native Rust types, avrow provides a `From` impl for Avro value types. For compound or user defined types (structs, enums), one can use the `serialize` method which relies on serde. Alternatively, one can construct `avrow::Value` instances which is a more verbose way to write avro values and should be a last resort. - -### Reading avro data - -```rust -fn main() -> Result<(), Error> { - let schema = Schema::from_str(r##""null""##); - let data = vec![ - 79, 98, 106, 1, 4, 22, 97, 118, 114, 111, 46, 115, 99, 104, 101, - 109, 97, 32, 123, 34, 116, 121, 112, 101, 34, 58, 34, 98, 121, 116, - 101, 115, 34, 125, 20, 97, 118, 114, 111, 46, 99, 111, 100, 101, - 99, 14, 100, 101, 102, 108, 97, 116, 101, 0, 145, 85, 112, 15, 87, - 201, 208, 26, 183, 148, 48, 236, 212, 250, 38, 208, 2, 18, 227, 97, - 96, 100, 98, 102, 97, 5, 0, 145, 85, 112, 15, 87, 201, 208, 26, - 183, 148, 48, 236, 212, 250, 38, 208, - ]; - // Create a Reader - let reader = Reader::with_schema(v.as_slice(), schema)?; - for i in reader { - dbg!(&i); - } - - Ok(()) -} - -``` - -A more involved self-referential recursive schema example: - -```rust -use anyhow::Error; -use avrow::{from_value, Codec, Reader, Schema, Writer}; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Serialize, Deserialize)] -struct LongList { - value: i64, - next: Option>, -} - -fn main() -> Result<(), Error> { - let schema = r##" - { - "type": "record", - "name": "LongList", - "aliases": ["LinkedLongs"], - "fields" : [ - {"name": "value", "type": "long"}, - {"name": "next", "type": ["null", "LongList"]} - ] - } - "##; - - let schema = Schema::from_str(schema)?; - let mut writer = Writer::with_codec(&schema, vec![], Codec::Null)?; - - let value = LongList { - value: 1i64, - next: Some(Box::new(LongList { - value: 2i64, - next: Some(Box::new(LongList { - value: 3i64, - next: Some(Box::new(LongList { - value: 4i64, - next: Some(Box::new(LongList { - value: 5i64, - next: None, - })), - })), - })), - })), - }; - - writer.serialize(value)?; - - // Calling into_inner performs flush internally. Alternatively, one can call flush explicitly. - let buf = writer.into_inner()?; - - // read - let reader = Reader::with_schema(buf.as_slice(), schema)?; - for i in reader { - let a: LongList = from_value(&i)?; - dbg!(a); - } - - Ok(()) -} - -``` - -An example of writing a json object with a confirming schema. The json object maps to an `avrow::Record` type. - -```rust -use anyhow::Error; -use avrow::{from_value, Reader, Record, Schema, Writer}; -use serde::{Deserialize, Serialize}; -use std::str::FromStr; - -#[derive(Debug, Serialize, Deserialize)] -struct Mentees { - id: i32, - username: String, -} - -#[derive(Debug, Serialize, Deserialize)] -struct RustMentors { - name: String, - github_handle: String, - active: bool, - mentees: Mentees, -} - -fn main() -> Result<(), Error> { - let schema = Schema::from_str( - r##" - { - "name": "rust_mentors", - "type": "record", - "fields": [ - { - "name": "name", - "type": "string" - }, - { - "name": "github_handle", - "type": "string" - }, - { - "name": "active", - "type": "boolean" - }, - { - "name":"mentees", - "type": { - "name":"mentees", - "type": "record", - "fields": [ - {"name":"id", "type": "int"}, - {"name":"username", "type": "string"} - ] - } - } - ] - } -"##, - )?; - - let json_data = serde_json::from_str( - r##" - { "name": "bob", - "github_handle":"ghbob", - "active": true, - "mentees":{"id":1, "username":"alice"} }"##, - )?; - let rec = Record::from_json(json_data, &schema)?; - let mut writer = crate::Writer::new(&schema, vec![])?; - writer.write(rec)?; - - let avro_data = writer.into_inner()?; - let reader = crate::Reader::from(avro_data.as_slice())?; - for value in reader { - let mentors: RustMentors = from_value(&value)?; - dbg!(mentors); - } - Ok(()) -} - -``` - -### Writer customization - -If you want to have more control over the parameters of `Writer`, consider using `WriterBuilder` as shown below: - -```rust - -use anyhow::Error; -use avrow::{Codec, Reader, Schema, WriterBuilder}; - -fn main() -> Result<(), Error> { - let schema = Schema::from_str(r##""null""##)?; - let v = vec![]; - let mut writer = WriterBuilder::new() - .set_codec(Codec::Null) - .set_schema(&schema) - .set_datafile(v) - // set any custom metadata in the header - .set_metadata("hello", "world") - // set after how many bytes, the writer should flush - .set_flush_interval(128_000) - .build() - .unwrap(); - writer.serialize(())?; - let v = writer.into_inner()?; - - let reader = Reader::with_schema(v.as_slice(), schema)?; - for i in reader { - dbg!(i?); - } - - Ok(()) -} -``` - -Refer to [examples](./examples) for more code examples. - -## Supported Codecs - -In order to facilitate efficient encoding, avro spec also defines compression codecs to use when serializing data. - -Avrow supports all compression codecs as per spec: - -- Null - The default is no codec. -- [Deflate](https://en.wikipedia.org/wiki/DEFLATE) -- [Snappy](https://github.com/google/snappy) -- [Zstd](https://facebook.github.io/zstd/) -- [Bzip2](https://www.sourceware.org/bzip2/) -- [Xz](https://linux.die.net/man/1/xz) - -These are feature-gated behind their respective flags. Check `Cargo.toml` `features` section for more details. - -## Using avrow-cli tool: - -Quite often you will need a quick way to examine avro file for debugging purposes. -For that, this repository also comes with the [`avrow-cli`](./avrow-cli) tool (av) -by which one can examine avro datafiles from the command line. - -See [avrow-cli](avrow-cli/) repository for more details. - -Installing avrow-cli: - -``` -cd avrow-cli -cargo install avrow-cli -``` - -Using avrow-cli (binary name is `av`): - -```bash -av read -d data.avro -``` - -The `read` subcommand will print all rows in `data.avro` to standard out in debug format. - -### Rust native types to Avro value mapping (via Serde) - -Primitives ---- - -| Rust native types (primitive types) | Avro (`Value`) | -| ----------------------------------- | -------------- | -| `(), Option::None` | `null` | -| `bool` | `boolean` | -| `i8, u8, i16, u16, i32, u32` | `int` | -| `i64, u64` | `long` | -| `f32` | `float` | -| `f64` | `double` | -| `&[u8], Vec` | `bytes` | -| `&str, String` | `string` | ---- -Complex - -| Rust native types (complex types) | Avro | -| ---------------------------------------------------- | -------- | -| `struct Foo {..}` | `record` | -| `enum Foo {A,B}` (variants cannot have data in them) | `enum` | -| `Vec where T: Into` | `array` | -| `HashMap where T: Into` | `map` | -| `T where T: Into` | `union` | -| `Vec` : Length equal to size defined in schema | `fixed` | - -
- -## Todo - -* [Logical types](https://avro.apache.org/docs/current/spec.html#Logical+Types) support. -* Sorted reads. -* Single object encoding. -* Schema Registry as a trait - would allow avrow to read from and write to remote schema registries. -* AsyncRead + AsyncWrite Reader and Writers. -* Avro protocol message and RPC support. -* Benchmarks and optimizations. - -## Changelog - -Please see the [CHANGELOG](CHANGELOG.md) for a release history. - -## Contributions - -All kinds of contributions are welcome. - -Head over to [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines. - -## Support - -
Buy Me A Coffee - -[![ko-fi](https://www.ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/P5P71YZ0L) - -## MSRV - -Avrow works on stable Rust, starting 1.37+. -It does not use any nightly features. - -## License - -Dual licensed under either of Apache License, Version -2.0 or MIT license at your option. - -Unless you explicitly state otherwise, any contribution intentionally submitted -for inclusion in this crate by you, as defined in the Apache-2.0 license, shall -be dual licensed as above, without any additional terms or conditions. diff --git a/avrow-cli/Cargo.toml b/avrow-cli/Cargo.toml deleted file mode 100644 index cee4397..0000000 --- a/avrow-cli/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[package] -name = "avrow-cli" -version = "0.1.0" -authors = ["creativcoder "] -edition = "2018" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -clap = {version="2.33.1", features=["yaml"] } -avrow = { path = "../../ravro", features=["all"] } -argh = "0.1.3" -anyhow = "1.0.32" -colored = "2.0.0" - -[[bin]] -name = "av" -path="src/main.rs" diff --git a/avrow-cli/README.md b/avrow-cli/README.md deleted file mode 100644 index 0b5f44e..0000000 --- a/avrow-cli/README.md +++ /dev/null @@ -1,31 +0,0 @@ - -## Avrow-cli - command line tool to examine avro files [WIP] - -Inspired from avro-tools.jar - -### Following subcommands are the supported as of now. - -``` -Usage: target/debug/av [] - -av: command line tool for examining avro datafiles. - -Options: - --help display usage information - -Commands: - getmeta Get metadata information of the avro datafile. - getschema Prints the writer's schema encoded in the provided datafile. - read Prints data from datafile as human readable value - tobytes Dumps the avro datafile as bytes for debugging purposes - fingerprint Prints fingerprint of the canonical form of writer's schema. - canonical Prints the canonical form of writer's schema encoded in the - provided datafile. -canonical -``` - -Usage: - -```bash -av read -d ./data.avro -``` diff --git a/avrow-cli/src/main.rs b/avrow-cli/src/main.rs deleted file mode 100644 index 01824b0..0000000 --- a/avrow-cli/src/main.rs +++ /dev/null @@ -1,43 +0,0 @@ -//! avrow-cli is a command line tool to examine and analyze avro data files. -//! -//! Usage: avrow-cli -i tojson // This prints the data contained in the in a readable format. - -mod subcommand; -mod utils; - -use argh::FromArgs; -use utils::read_datafile; - -use subcommand::{Canonical, Fingerprint, GetMeta, GetSchema, ToBytes, ReadData}; - -#[derive(Debug, FromArgs)] -/// av: command line tool for examining avro datafiles. -struct AvrowCli { - #[argh(subcommand)] - subcommand: SubCommand, -} - -#[derive(FromArgs, PartialEq, Debug)] -#[argh(subcommand)] -enum SubCommand { - GetMeta(GetMeta), - GetSchema(GetSchema), - Read(ReadData), - ToBytes(ToBytes), - Fingerprint(Fingerprint), - Canonical(Canonical), -} - -fn main() -> anyhow::Result<()> { - let flags: AvrowCli = argh::from_env(); - match flags.subcommand { - SubCommand::GetMeta(cmd) => cmd.getmeta()?, - SubCommand::Read(cmd) => cmd.read_data()?, - SubCommand::ToBytes(cmd) => cmd.tobytes()?, - SubCommand::GetSchema(cmd) => cmd.getschema()?, - SubCommand::Fingerprint(cmd) => cmd.fingerprint()?, - SubCommand::Canonical(cmd) => cmd.canonical()? - } - - Ok(()) -} diff --git a/avrow-cli/src/subcommand.rs b/avrow-cli/src/subcommand.rs deleted file mode 100644 index 8b78bd2..0000000 --- a/avrow-cli/src/subcommand.rs +++ /dev/null @@ -1,157 +0,0 @@ -use crate::read_datafile; -use anyhow::{anyhow, Context}; -use argh::FromArgs; -use avrow::{Header, Reader}; -use std::io::Read; -use std::path::PathBuf; -use std::str; - -#[derive(FromArgs, PartialEq, Debug)] -/// Get metadata information of the avro datafile. -#[argh(subcommand, name = "getmeta")] -pub struct GetMeta { - /// datafile as input - #[argh(option, short = 'd')] - datafile: PathBuf, -} - -impl GetMeta { - pub fn getmeta(&self) -> Result<(), anyhow::Error> { - let mut avro_datafile = read_datafile(&self.datafile)?; - let header = Header::from_reader(&mut avro_datafile)?; - for (k, v) in header.metadata() { - print!("{}\t", k); - println!( - "{}", - str::from_utf8(v).expect("Invalid UTF-8 in avro header") - ); - } - Ok(()) - } -} - -#[derive(FromArgs, PartialEq, Debug)] -/// Prints data from datafile in debug format. -#[argh(subcommand, name = "read")] -pub struct ReadData { - /// datafile as input - #[argh(option, short = 'd')] - datafile: PathBuf, -} -impl ReadData { - pub fn read_data(&self) -> Result<(), anyhow::Error> { - let mut avro_datafile = read_datafile(&self.datafile)?; - let reader = Reader::new(&mut avro_datafile)?; - // TODO: remove irrelevant fields - for i in reader { - println!("{:#?}", i?); - } - - Ok(()) - } -} - -#[derive(FromArgs, PartialEq, Debug)] -/// Dumps the avro datafile as bytes for debugging purposes -#[argh(subcommand, name = "tobytes")] -pub struct ToBytes { - /// datafile as input - #[argh(option, short = 'd')] - datafile: PathBuf, -} - -impl ToBytes { - pub fn tobytes(&self) -> Result<(), anyhow::Error> { - let mut avro_datafile = read_datafile(&self.datafile)?; - let mut v = vec![]; - - avro_datafile - .read_to_end(&mut v) - .with_context(|| "Failed to read data file in memory")?; - - println!("{:?}", v); - Ok(()) - } -} - -#[derive(FromArgs, PartialEq, Debug)] -/// Prints the writer's schema encoded in the provided datafile. -#[argh(subcommand, name = "getschema")] -pub struct GetSchema { - /// datafile as input - #[argh(option, short = 'd')] - datafile: PathBuf, -} - -impl GetSchema{ - pub fn getschema(&self) -> Result<(), anyhow::Error> { - let mut avro_datafile = read_datafile(&self.datafile)?; - let header = Header::from_reader(&mut avro_datafile)?; - // TODO print human readable schema - dbg!(header.schema()); - Ok(()) - } -} - -#[derive(FromArgs, PartialEq, Debug)] -/// Prints fingerprint of the canonical form of writer's schema. -#[argh(subcommand, name = "fingerprint")] -pub struct Fingerprint { - /// datafile as input - #[argh(option, short = 'd')] - datafile: String, - /// the fingerprinting algorithm (rabin64 (default), sha256, md5) - #[argh(option, short = 'f')] - fingerprint: String, -} -impl Fingerprint { - pub fn fingerprint(&self) -> Result<(), anyhow::Error> { - let mut avro_datafile = read_datafile(&self.datafile)?; - let header = Header::from_reader(&mut avro_datafile)?; - match self.fingerprint.as_ref() { - "rabin64" => { - println!("0x{:x}", header.schema().canonical_form().rabin64()); - }, - "sha256" => { - let mut fingerprint_str = String::new(); - let sha256 = header.schema().canonical_form().sha256(); - for i in sha256 { - let a = format!("{:x}", i); - fingerprint_str.push_str(&a); - } - - println!("{}", fingerprint_str); - } - "md5" => { - let mut fingerprint_str = String::new(); - let md5 = header.schema().canonical_form().md5(); - for i in md5 { - let a = format!("{:x}", i); - fingerprint_str.push_str(&a); - } - - println!("{}", fingerprint_str); - } - other => return Err(anyhow!("invalid or unsupported fingerprint: {}", other)) - } - Ok(()) - } -} - -#[derive(FromArgs, PartialEq, Debug)] -/// Prints the canonical form of writer's schema encoded in the provided datafile. -#[argh(subcommand, name = "canonical")] -pub struct Canonical { - /// datafile as input - #[argh(option, short = 'd')] - datafile: String, -} - -impl Canonical { - pub fn canonical(&self) -> Result<(), anyhow::Error> { - let mut avro_datafile = read_datafile(&self.datafile)?; - let header = Header::from_reader(&mut avro_datafile)?; - println!("{}", header.schema().canonical_form()); - Ok(()) - } -} diff --git a/avrow-cli/src/utils.rs b/avrow-cli/src/utils.rs deleted file mode 100644 index 2b63045..0000000 --- a/avrow-cli/src/utils.rs +++ /dev/null @@ -1,11 +0,0 @@ -use anyhow::Context; -use anyhow::Result; -use std::path::Path; - -// Open an avro datafile for reading avro data -pub(crate) fn read_datafile>(path: P) -> Result { - std::fs::OpenOptions::new() - .read(true) - .open(path) - .with_context(|| "Could not read datafile") -} diff --git a/assets/avrow_logo.png b/avrow_logo.png similarity index 100% rename from assets/avrow_logo.png rename to avrow_logo.png diff --git a/benches/complex.rs b/benches/complex.rs deleted file mode 100644 index 3f8794a..0000000 --- a/benches/complex.rs +++ /dev/null @@ -1,150 +0,0 @@ -extern crate avrow; -extern crate serde; -#[macro_use] -extern crate serde_derive; - -#[macro_use] -extern crate criterion; - -use avrow::Codec; -use avrow::Schema; -use avrow::Writer; -use criterion::Criterion; -use std::str::FromStr; - -#[derive(Debug, Serialize, Deserialize)] -struct LongList { - value: i64, - next: Option>, -} - -fn simple_record(c: &mut Criterion) { - c.bench_function("simple_record", |b| { - let schema = Schema::from_str( - r##"{ - "namespace": "atherenergy.vcu_cloud_connect", - "type": "record", - "name": "can_raw", - "fields" : [ - {"name": "one", "type": "int"}, - {"name": "two", "type": "long"}, - {"name": "three", "type": "long"}, - {"name": "four", "type": "int"}, - {"name": "five", "type": "long"} - ] - }"##, - ) - .unwrap(); - let v = vec![]; - let mut writer = Writer::with_codec(&schema, v, Codec::Null).unwrap(); - b.iter(|| { - for _ in 0..1000 { - let data = Data { - one: 34, - two: 334, - three: 45765, - four: 45643, - five: 834, - }; - - writer.serialize(data).unwrap(); - } - - // batch and write data - writer.flush().unwrap(); - }); - }); -} - -#[derive(Serialize, Deserialize)] -struct Data { - one: u32, - two: u64, - three: u64, - four: u32, - five: u64, -} - -fn array_record(c: &mut Criterion) { - c.bench_function("Array of records", |b| { - let schema = Schema::from_str( - r##"{"type": "array", "items": { - "namespace": "atherenergy.vcu_cloud_connect", - "type": "record", - "name": "can_raw", - "fields" : [ - {"name": "one", "type": "int"}, - {"name": "two", "type": "long"}, - {"name": "three", "type": "long"}, - {"name": "four", "type": "int"}, - {"name": "five", "type": "long"} - ] - }}"##, - ) - .unwrap(); - let mut v = vec![]; - let mut writer = Writer::with_codec(&schema, &mut v, Codec::Null).unwrap(); - b.iter(|| { - let mut can_array = vec![]; - for _ in 0..1000 { - let data = Data { - one: 34, - two: 334, - three: 45765, - four: 45643, - five: 834, - }; - - can_array.push(data); - } - - // batch and write data - writer.serialize(can_array).unwrap(); - writer.flush().unwrap(); - }); - }); -} - -fn nested_recursive_record(c: &mut Criterion) { - c.bench_function("recursive_nested_record", |b| { - let schema = r##" - { - "type": "record", - "name": "LongList", - "aliases": ["LinkedLongs"], - "fields" : [ - {"name": "value", "type": "long"}, - {"name": "next", "type": ["null", "LongList"]} - ] - } - "##; - - let schema = Schema::from_str(schema).unwrap(); - let mut writer = Writer::with_codec(&schema, vec![], Codec::Null).unwrap(); - - b.iter(|| { - for _ in 0..1000 { - let value = LongList { - value: 1i64, - next: Some(Box::new(LongList { - value: 2, - next: Some(Box::new(LongList { - value: 3, - next: None, - })), - })), - }; - writer.serialize(value).unwrap(); - } - }); - writer.flush().unwrap(); - }); -} - -criterion_group!( - benches, - nested_recursive_record, - array_record, - simple_record -); -criterion_main!(benches); diff --git a/benches/primitives.rs b/benches/primitives.rs deleted file mode 100644 index 9651535..0000000 --- a/benches/primitives.rs +++ /dev/null @@ -1,149 +0,0 @@ -extern crate avrow; - -#[macro_use] -extern crate criterion; - -use criterion::Criterion; - -use avrow::from_value; -use avrow::Reader; -use avrow::Schema; -use avrow::Writer; -use std::str::FromStr; - -fn criterion_benchmark(c: &mut Criterion) { - // Write benchmarks - c.bench_function("write_null", |b| { - let schema = Schema::from_str(r##"{"type": "null" }"##).unwrap(); - let mut out = vec![]; - let mut writer = Writer::new(&schema, &mut out).unwrap(); - - b.iter(|| { - for _ in 0..100_000 { - writer.write(()).unwrap(); - } - }); - - writer.flush().unwrap(); - }); - - c.bench_function("write_boolean", |b| { - let schema = Schema::from_str(r##"{"type": "boolean" }"##).unwrap(); - let mut out = vec![]; - let mut writer = Writer::new(&schema, &mut out).unwrap(); - - b.iter(|| { - for i in 0..100_000 { - writer.write(i % 2 == 0).unwrap(); - } - }); - - writer.flush().unwrap(); - }); - - c.bench_function("write_int", |b| { - let schema = Schema::from_str(r##"{"type": "int" }"##).unwrap(); - let mut out = vec![]; - let mut writer = Writer::new(&schema, &mut out).unwrap(); - - b.iter(|| { - for _ in 0..100_000 { - writer.write(45).unwrap(); - } - }); - - writer.flush().unwrap(); - }); - - c.bench_function("write_long", |b| { - let schema = Schema::from_str(r##"{"type": "long" }"##).unwrap(); - let mut out = vec![]; - let mut writer = Writer::new(&schema, &mut out).unwrap(); - - b.iter(|| { - for _ in 0..100_000 { - writer.write(45i64).unwrap(); - } - }); - - writer.flush().unwrap(); - }); - - c.bench_function("write_float", |b| { - let schema = Schema::from_str(r##"{"type": "float" }"##).unwrap(); - let mut out = vec![]; - let mut writer = Writer::new(&schema, &mut out).unwrap(); - - b.iter(|| { - for _ in 0..100_000 { - writer.write(45.0f32).unwrap(); - } - }); - - writer.flush().unwrap(); - }); - - c.bench_function("write_double", |b| { - let schema = Schema::from_str(r##"{"type": "double" }"##).unwrap(); - let mut out = vec![]; - let mut writer = Writer::new(&schema, &mut out).unwrap(); - - b.iter(|| { - for _ in 0..100_000 { - writer.write(45.0f64).unwrap(); - } - }); - - writer.flush().unwrap(); - }); - - c.bench_function("write_bytes", |b| { - let schema = Schema::from_str(r##"{"type": "bytes" }"##).unwrap(); - let mut out = vec![]; - let mut writer = Writer::new(&schema, &mut out).unwrap(); - - b.iter(|| { - for _ in 0..100_000 { - let v = vec![0u8, 1, 2, 3]; - writer.write(v).unwrap(); - } - }); - - writer.flush().unwrap(); - }); - - c.bench_function("write_string", |b| { - let schema = Schema::from_str(r##"{"type": "string" }"##).unwrap(); - let mut out = vec![]; - let mut writer = Writer::new(&schema, &mut out).unwrap(); - - b.iter(|| { - for _ in 0..100_000 { - writer.write("hello").unwrap(); - } - }); - - writer.flush().unwrap(); - }); - - // Read benchmarks - c.bench_function("avro_read_bytes_from_vec", |b| { - let avro_data = vec![ - 79, 98, 106, 1, 4, 22, 97, 118, 114, 111, 46, 115, 99, 104, 101, 109, 97, 32, 123, 34, - 116, 121, 112, 101, 34, 58, 34, 98, 121, 116, 101, 115, 34, 125, 20, 97, 118, 114, 111, - 46, 99, 111, 100, 101, 99, 8, 110, 117, 108, 108, 0, 149, 158, 112, 231, 150, 73, 245, - 11, 130, 6, 13, 141, 239, 19, 146, 71, 2, 14, 12, 0, 1, 2, 3, 4, 5, 149, 158, 112, 231, - 150, 73, 245, 11, 130, 6, 13, 141, 239, 19, 146, 71, - ]; - - b.iter(|| { - let reader = Reader::new(avro_data.as_slice()).unwrap(); - for i in reader { - let _: Vec = from_value(&i).unwrap(); - } - }); - }); -} - -criterion_group!(benches, criterion_benchmark); -criterion_main!(benches); diff --git a/benches/schema.rs b/benches/schema.rs deleted file mode 100644 index 61b3355..0000000 --- a/benches/schema.rs +++ /dev/null @@ -1,61 +0,0 @@ -#[macro_use] -extern crate criterion; -extern crate avrow; - -use criterion::criterion_group; -use criterion::Criterion; -use std::str::FromStr; - -use avrow::Schema; - -fn parse_enum_schema() { - let _ = Schema::from_str( - r##"{ "type": "enum", - "name": "Suit", - "symbols" : ["SPADES", "HEARTS", "DIAMONDS", "CLUBS"] - }"##, - ) - .unwrap(); -} - -fn parse_string_schema() { - let _ = Schema::from_str(r##""string""##).unwrap(); -} - -fn parse_record_schema(c: &mut Criterion) { - c.bench_function("parse_record_schema", |b| { - b.iter(|| { - let _ = Schema::from_str( - r##"{ - "namespace": "sensor_data", - "type": "record", - "name": "can", - "fields" : [ - {"name": "can_id", "type": "int"}, - {"name": "data", "type": "long"}, - {"name": "timestamp", "type": "double"}, - {"name": "seq_num", "type": "int"}, - {"name": "global_seq", "type": "long"} - ] - }"##, - ) - .unwrap(); - }); - }); -} - -fn bench_string_schema(c: &mut Criterion) { - c.bench_function("parse string schema", |b| b.iter(parse_string_schema)); -} - -fn bench_enum_schema(c: &mut Criterion) { - c.bench_function("parse enum schema", |b| b.iter(parse_enum_schema)); -} - -criterion_group!( - benches, - bench_string_schema, - bench_enum_schema, - parse_record_schema -); -criterion_main!(benches); diff --git a/benches/write.rs b/benches/write.rs deleted file mode 100644 index 8b13789..0000000 --- a/benches/write.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/examples/canonical.rs b/examples/canonical.rs deleted file mode 100644 index 9b7293c..0000000 --- a/examples/canonical.rs +++ /dev/null @@ -1,24 +0,0 @@ -use anyhow::Error; -use avrow::Schema; -use std::str::FromStr; - -fn main() -> Result<(), Error> { - let schema = Schema::from_str( - r##" - { - "type": "record", - "name": "LongList", - "aliases": ["LinkedLongs"], - "fields" : [ - {"name": "value", "type": "long"}, - {"name": "next", "type": ["null", "LongList"] - }] - } - "##, - ) - .unwrap(); - println!("{}", schema.canonical_form()); - // get the rabin fingerprint of the canonical form. - dbg!(schema.canonical_form().rabin64()); - Ok(()) -} diff --git a/examples/from_json_to_struct.rs b/examples/from_json_to_struct.rs deleted file mode 100644 index 0938a6d..0000000 --- a/examples/from_json_to_struct.rs +++ /dev/null @@ -1,72 +0,0 @@ -use anyhow::Error; -use avrow::{from_value, Reader, Record, Schema, Writer}; -use serde::{Deserialize, Serialize}; -use std::str::FromStr; -#[derive(Debug, Serialize, Deserialize)] -struct Mentees { - id: i32, - username: String, -} - -#[derive(Debug, Serialize, Deserialize)] -struct RustMentors { - name: String, - github_handle: String, - active: bool, - mentees: Mentees, -} - -fn main() -> Result<(), Error> { - let schema = Schema::from_str( - r##" - { - "name": "rust_mentors", - "type": "record", - "fields": [ - { - "name": "name", - "type": "string" - }, - { - "name": "github_handle", - "type": "string" - }, - { - "name": "active", - "type": "boolean" - }, - { - "name":"mentees", - "type": { - "name":"mentees", - "type": "record", - "fields": [ - {"name":"id", "type": "int"}, - {"name":"username", "type": "string"} - ] - } - } - ] - } -"##, - )?; - - let json_data = serde_json::from_str( - r##" - { "name": "bob", - "github_handle":"ghbob", - "active": true, - "mentees":{"id":1, "username":"alice"} }"##, - )?; - let rec = Record::from_json(json_data, &schema)?; - let mut writer = crate::Writer::new(&schema, vec![])?; - writer.write(rec)?; - - let avro_data = writer.into_inner()?; - let reader = Reader::new(avro_data.as_slice())?; - for value in reader { - let mentors: RustMentors = from_value(&value)?; - dbg!(mentors); - } - Ok(()) -} diff --git a/examples/hello_world.rs b/examples/hello_world.rs deleted file mode 100644 index 4b2f5fd..0000000 --- a/examples/hello_world.rs +++ /dev/null @@ -1,41 +0,0 @@ -// A hello world example of reading and writing avro data files - -use anyhow::Error; -use avrow::from_value; -use avrow::Reader; -use avrow::Schema; -use avrow::Writer; -use std::str::FromStr; - -use std::io::Cursor; - -fn main() -> Result<(), Error> { - // Writing data - - // Create a schema - let schema = Schema::from_str(r##""null""##)?; - // Create writer using schema and provide a buffer (implements Read) to write to - let mut writer = Writer::new(&schema, vec![])?; - // Write the data using write and creating a Value manually. - writer.write(())?; - // or the more convenient and intuitive serialize method that takes native Rust types. - writer.serialize(())?; - // retrieve the underlying buffer using the buffer method. - // TODO buffer is not intuive when using a file. into_inner is much better here. - let buf = writer.into_inner()?; - - // Reading data - - // Create Reader by providing a Read wrapped version of `buf` - let reader = Reader::new(Cursor::new(buf))?; - // Use iterator for reading data in an idiomatic manner. - for i in reader { - // reading values can fail due to decoding errors, so the return value of iterator is a Option> - // it allows one to examine the failure reason on the underlying avro reader. - dbg!(&i); - // This value can be converted to a native Rust type using `from_value` method that uses serde underneath. - let _val: () = from_value(&i)?; - } - - Ok(()) -} diff --git a/examples/recursive_record.rs b/examples/recursive_record.rs deleted file mode 100644 index c97aa6d..0000000 --- a/examples/recursive_record.rs +++ /dev/null @@ -1,56 +0,0 @@ -use anyhow::Error; -use avrow::{from_value, Codec, Reader, Schema, Writer}; -use serde::{Deserialize, Serialize}; -use std::str::FromStr; - -#[derive(Debug, Serialize, Deserialize)] -struct LongList { - value: i64, - next: Option>, -} - -fn main() -> Result<(), Error> { - let schema = r##" - { - "type": "record", - "name": "LongList", - "aliases": ["LinkedLongs"], - "fields" : [ - {"name": "value", "type": "long"}, - {"name": "next", "type": ["null", "LongList"]} - ] - } - "##; - - let schema = Schema::from_str(schema)?; - let mut writer = Writer::with_codec(&schema, vec![], Codec::Null)?; - - let value = LongList { - value: 1i64, - next: Some(Box::new(LongList { - value: 2i64, - next: Some(Box::new(LongList { - value: 3i64, - next: Some(Box::new(LongList { - value: 4i64, - next: Some(Box::new(LongList { - value: 5i64, - next: None, - })), - })), - })), - })), - }; - writer.serialize(value)?; - - let buf = writer.into_inner()?; - - // read - let reader = Reader::with_schema(buf.as_slice(), schema)?; - for i in reader { - let a: LongList = from_value(&i)?; - dbg!(a); - } - - Ok(()) -} diff --git a/examples/writer_builder.rs b/examples/writer_builder.rs deleted file mode 100644 index 6b555bc..0000000 --- a/examples/writer_builder.rs +++ /dev/null @@ -1,23 +0,0 @@ -use anyhow::Error; -use avrow::{Codec, Reader, Schema, WriterBuilder}; -use std::str::FromStr; - -fn main() -> Result<(), Error> { - let schema = Schema::from_str(r##""null""##)?; - let v = vec![]; - let mut writer = WriterBuilder::new() - .set_codec(Codec::Null) - .set_schema(&schema) - .set_datafile(v) - .set_flush_interval(128_000) - .build()?; - writer.serialize(())?; - let v = writer.into_inner()?; - - let reader = Reader::with_schema(v.as_slice(), schema)?; - for i in reader { - dbg!(i?); - } - - Ok(()) -} diff --git a/index.html b/index.html new file mode 100644 index 0000000..7bf83b9 --- /dev/null +++ b/index.html @@ -0,0 +1,79 @@ + + + + + + + Document + + + + + + +
+
+
+ hero +
+
+

Avrow - a simple, type safe + implementation + of the avro + specification + in Rust. +

+ + + + +
+ +
+
+ + + diff --git a/rustfmt.toml b/rustfmt.toml deleted file mode 100644 index 27eb93b..0000000 --- a/rustfmt.toml +++ /dev/null @@ -1,2 +0,0 @@ -edition = "2018" -reorder_imports = true \ No newline at end of file diff --git a/src/codec.rs b/src/codec.rs deleted file mode 100644 index 93ccfd1..0000000 --- a/src/codec.rs +++ /dev/null @@ -1,273 +0,0 @@ -use crate::error::AvrowErr; -use crate::util::{encode_long, encode_raw_bytes}; - -use std::io::Write; - -// Given a slice of bytes, generates a CRC for it -#[cfg(feature = "snappy")] -pub fn get_crc_uncompressed(pre_comp_buf: &[u8]) -> Result, AvrowErr> { - use byteorder::{BigEndian, WriteBytesExt}; - use crc::crc32; - - let crc_checksum = crc32::checksum_ieee(pre_comp_buf); - let mut checksum_bytes = Vec::with_capacity(1); - let _ = checksum_bytes - .write_u32::(crc_checksum) - .map_err(|_| { - let err: AvrowErr = AvrowErr::CRCGenFailed; - err - })?; - Ok(checksum_bytes) -} - -/// Given a uncompressed slice of bytes, returns a compresed Vector of bytes using the snappy codec -#[cfg(feature = "snappy")] -pub(crate) fn compress_snappy(uncompressed_buffer: &[u8]) -> Result, AvrowErr> { - let mut encoder = snap::Encoder::new(); - encoder - .compress_vec(uncompressed_buffer) - .map_err(|e| AvrowErr::DecodeFailed(e.into())) -} - -#[cfg(feature = "deflate")] -pub fn compress_deflate(uncompressed_buffer: &[u8]) -> Result, AvrowErr> { - use flate2::{write::DeflateEncoder, Compression}; - - let mut encoder = DeflateEncoder::new(Vec::new(), Compression::default()); - encoder - .write(uncompressed_buffer) - .map_err(AvrowErr::EncodeFailed)?; - encoder.finish().map_err(AvrowErr::EncodeFailed) -} - -#[cfg(feature = "zstd")] -pub(crate) fn zstd_compress(level: i32, uncompressed_buffer: &[u8]) -> Result, AvrowErr> { - let comp = zstdd::encode_all(std::io::Cursor::new(uncompressed_buffer), level) - .map_err(AvrowErr::EncodeFailed)?; - Ok(comp) -} - -#[cfg(feature = "deflate")] -pub fn decompress_deflate( - compressed_buffer: &[u8], - uncompressed: &mut Vec, -) -> Result<(), AvrowErr> { - use flate2::bufread::DeflateDecoder; - use std::io::Read; - - let mut decoder = DeflateDecoder::new(compressed_buffer); - uncompressed.clear(); - decoder - .read_to_end(uncompressed) - .map_err(AvrowErr::DecodeFailed)?; - Ok(()) -} - -#[cfg(feature = "snappy")] -pub(crate) fn decompress_snappy( - compressed_buffer: &[u8], - uncompressed: &mut Vec, -) -> Result<(), AvrowErr> { - use byteorder::ByteOrder; - - let data_minus_cksum = &compressed_buffer[..compressed_buffer.len() - 4]; - let decompressed_size = - snap::decompress_len(data_minus_cksum).map_err(|e| AvrowErr::DecodeFailed(e.into()))?; - uncompressed.resize(decompressed_size, 0); - snap::Decoder::new() - .decompress(data_minus_cksum, &mut uncompressed[..]) - .map_err(|e| AvrowErr::DecodeFailed(e.into()))?; - - let expected = - byteorder::BigEndian::read_u32(&compressed_buffer[compressed_buffer.len() - 4..]); - let found = crc::crc32::checksum_ieee(&uncompressed); - if expected != found { - return Err(AvrowErr::CRCMismatch { found, expected }); - } - Ok(()) -} - -#[cfg(feature = "zstd")] -pub(crate) fn decompress_zstd( - compressed_buffer: &[u8], - uncompressed: &mut Vec, -) -> Result<(), AvrowErr> { - let mut decoder = zstdd::Decoder::new(compressed_buffer).map_err(AvrowErr::DecodeFailed)?; - std::io::copy(&mut decoder, uncompressed).map_err(AvrowErr::DecodeFailed)?; - Ok(()) -} - -#[cfg(feature = "bzip2")] -pub(crate) fn decompress_bzip2( - compressed_buffer: &[u8], - uncompressed: &mut Vec, -) -> Result<(), AvrowErr> { - use bzip2::read::BzDecoder; - let decompressor = BzDecoder::new(compressed_buffer); - let mut buf = decompressor.into_inner(); - std::io::copy(&mut buf, uncompressed).map_err(AvrowErr::DecodeFailed)?; - Ok(()) -} - -#[cfg(feature = "xz")] -pub(crate) fn decompress_xz( - compressed_buffer: &[u8], - uncompressed: &mut Vec, -) -> Result<(), AvrowErr> { - use xz2::read::XzDecoder; - let decompressor = XzDecoder::new(compressed_buffer); - let mut buf = decompressor.into_inner(); - std::io::copy(&mut buf, uncompressed).map_err(AvrowErr::DecodeFailed)?; - Ok(()) -} -/// Defines codecs one can use when writing avro data. -#[derive(Debug, PartialEq, Clone, Copy)] -pub enum Codec { - /// The Null codec. When no codec is specified at the time of Writer creation, null is the default. - Null, - #[cfg(feature = "deflate")] - /// The Deflate codec.
Uses https://docs.rs/flate2 as the underlying implementation. - Deflate, - #[cfg(feature = "snappy")] - /// The Snappy codec.
Uses https://docs.rs/snap as the underlying implementation. - Snappy, - #[cfg(feature = "zstd")] - /// The Zstd codec.
Uses https://docs.rs/zstd as the underlying implementation. - Zstd, - #[cfg(feature = "bzip2")] - /// The Bzip2 codec.
Uses https://docs.rs/bzip2 as the underlying implementation. - Bzip2, - #[cfg(feature = "xz")] - /// The Xz codec.
Uses https://docs.rs/crate/xz2 as the underlying implementation. - Xz, -} - -impl AsRef for Codec { - fn as_ref(&self) -> &str { - match self { - Codec::Null => "null", - #[cfg(feature = "deflate")] - Codec::Deflate => "deflate", - #[cfg(feature = "snappy")] - Codec::Snappy => "snappy", - #[cfg(feature = "zstd")] - Codec::Zstd => "zstd", - #[cfg(feature = "bzip2")] - Codec::Bzip2 => "bzip2", - #[cfg(feature = "xz")] - Codec::Xz => "xz", - } - } -} - -// TODO allow all of these to be configurable for setting compression ratio/level -impl Codec { - pub(crate) fn encode( - &self, - block_stream: &mut [u8], - out_stream: &mut W, - ) -> Result<(), AvrowErr> { - match self { - Codec::Null => { - // encode size of data in block - encode_long(block_stream.len() as i64, out_stream)?; - // encode actual data bytes - encode_raw_bytes(&block_stream, out_stream)?; - } - #[cfg(feature = "snappy")] - Codec::Snappy => { - let checksum_bytes = get_crc_uncompressed(&block_stream)?; - let compressed_data = compress_snappy(&block_stream)?; - encode_long( - compressed_data.len() as i64 + crate::config::CRC_CHECKSUM_LEN as i64, - out_stream, - )?; - - out_stream - .write(&*compressed_data) - .map_err(AvrowErr::EncodeFailed)?; - out_stream - .write(&*checksum_bytes) - .map_err(AvrowErr::EncodeFailed)?; - } - #[cfg(feature = "deflate")] - Codec::Deflate => { - let compressed_data = compress_deflate(block_stream)?; - encode_long(compressed_data.len() as i64, out_stream)?; - encode_raw_bytes(&*compressed_data, out_stream)?; - } - #[cfg(feature = "zstd")] - Codec::Zstd => { - let compressed_data = zstd_compress(0, block_stream)?; - encode_long(compressed_data.len() as i64, out_stream)?; - encode_raw_bytes(&*compressed_data, out_stream)?; - } - #[cfg(feature = "bzip2")] - Codec::Bzip2 => { - use bzip2::read::BzEncoder; - use bzip2::Compression; - use std::io::Cursor; - let compressor = BzEncoder::new(Cursor::new(block_stream), Compression::new(5)); - let vec = compressor.into_inner().into_inner(); - - encode_long(vec.len() as i64, out_stream)?; - encode_raw_bytes(&*vec, out_stream)?; - } - #[cfg(feature = "xz")] - Codec::Xz => { - use std::io::Cursor; - use xz2::read::XzEncoder; - let compressor = XzEncoder::new(Cursor::new(block_stream), 6); - let vec = compressor.into_inner().into_inner(); - - encode_long(vec.len() as i64, out_stream)?; - encode_raw_bytes(&*vec, out_stream)?; - } - } - Ok(()) - } - - pub(crate) fn decode( - &self, - compressed: Vec, - uncompressed: &mut std::io::Cursor>, - ) -> Result<(), AvrowErr> { - match self { - Codec::Null => { - *uncompressed = std::io::Cursor::new(compressed); - Ok(()) - } - #[cfg(feature = "snappy")] - Codec::Snappy => decompress_snappy(&compressed, uncompressed.get_mut()), - #[cfg(feature = "deflate")] - Codec::Deflate => decompress_deflate(&compressed, uncompressed.get_mut()), - #[cfg(feature = "zstd")] - Codec::Zstd => decompress_zstd(&compressed, uncompressed.get_mut()), - #[cfg(feature = "bzip2")] - Codec::Bzip2 => decompress_bzip2(&compressed, uncompressed.get_mut()), - #[cfg(feature = "xz")] - Codec::Xz => decompress_xz(&compressed, uncompressed.get_mut()), - } - } -} - -impl std::convert::TryFrom<&str> for Codec { - type Error = AvrowErr; - - fn try_from(value: &str) -> Result { - match value { - "null" => Ok(Codec::Null), - #[cfg(feature = "snappy")] - "snappy" => Ok(Codec::Snappy), - #[cfg(feature = "deflate")] - "deflate" => Ok(Codec::Deflate), - #[cfg(feature = "zstd")] - "zstd" => Ok(Codec::Zstd), - #[cfg(feature = "bzip2")] - "bzip2" => Ok(Codec::Bzip2), - #[cfg(feature = "bzip2")] - "xz" => Ok(Codec::Xz), - o => Err(AvrowErr::UnsupportedCodec(o.to_string())), - } - } -} diff --git a/src/config.rs b/src/config.rs deleted file mode 100644 index b60a74c..0000000 --- a/src/config.rs +++ /dev/null @@ -1,15 +0,0 @@ -//! This module contains constants and configuration parameters for configuring avro writers and readers. - -/// Synchronization marker bytes length, defaults to 16 bytes. -pub const SYNC_MARKER_SIZE: usize = 16; -/// The magic header for recognizing a file as an avro data file. -pub const MAGIC_BYTES: &[u8] = b"Obj\x01"; -/// Checksum length for snappy compressed data. -#[cfg(feature = "snappy")] -pub const CRC_CHECKSUM_LEN: usize = 4; -/// Minimum flush interval that a block can have. -pub const BLOCK_SIZE: usize = 4096; -/// This value defines the threshold post which the scratch buffer is -/// is flushed/synced to the main buffer. Suggested values are between 2K (bytes) and 2M -// TODO make this configurable -pub const DEFAULT_FLUSH_INTERVAL: usize = 16 * BLOCK_SIZE; diff --git a/src/error.rs b/src/error.rs deleted file mode 100644 index ce76d61..0000000 --- a/src/error.rs +++ /dev/null @@ -1,184 +0,0 @@ -#![allow(missing_docs)] - -use serde::{de, ser}; -use std::fmt::Debug; -use std::fmt::Display; -use std::io::{Error, ErrorKind}; - -#[inline(always)] -pub(crate) fn io_err(msg: &str) -> Error { - Error::new(ErrorKind::Other, msg) -} - -// Required impls for Serde -impl ser::Error for AvrowErr { - fn custom(msg: T) -> Self { - Self::Message(msg.to_string()) - } -} - -impl de::Error for AvrowErr { - fn custom(msg: T) -> Self { - Self::Message(msg.to_string()) - } -} - -pub type AvrowResult = Result; - -/// Errors returned from avrow -#[derive(thiserror::Error, Debug)] -pub enum AvrowErr { - // Encode errors - #[error("Write failed")] - EncodeFailed(#[source] std::io::Error), - #[error("Encoding failed. Value does not match schema")] - SchemaDataMismatch, - #[error("Expected magic header: `Obj\n`")] - InvalidDataFile, - #[error("Sync marker does not match as expected")] - SyncMarkerMismatch, - #[error("Named schema not found in union")] - SchemaNotFoundInUnion, - #[error("Invalid field value: {0}")] - InvalidFieldValue(String), - #[error("Writer seek failed, not a valid avro data file")] - WriterSeekFailed, - #[error("Unions must not contain immediate union values")] - NoImmediateUnion, - #[error("Failed building the Writer")] - WriterBuildFailed, - #[error("Json must be an object for record")] - ExpectedJsonObject, - - // Decode errors - #[error("Read failed")] - DecodeFailed(#[source] std::io::Error), - #[error("failed reading `avro.schema` metadata from header")] - HeaderDecodeFailed, - #[error("Unsupported codec {0}, did you enable the feature?")] - UnsupportedCodec(String), - #[error("Named schema was not found in schema registry")] - NamedSchemaNotFound, - #[error("Schema resolution failed. reader's schema {0} != writer's schema {1}")] - SchemaResolutionFailed(String, String), - #[error("Index read for enum is out of range as per schema. got: {0} symbols: {1}")] - InvalidEnumSymbolIdx(usize, String), - #[error("Field not found in record")] - FieldNotFound, - #[error("Writer schema not found in reader's schema")] - WriterNotInReader, - #[error("Reader's union schema does not match with writer's union schema")] - UnionSchemaMismatch, - #[error("Map's value schema do not match")] - MapSchemaMismatch, - #[error("Fixed schema names do not match")] - FixedSchemaNameMismatch, - #[error("Could not find symbol at index {idx} in reader schema")] - EnumSymbolNotFound { idx: usize }, - #[error("Reader's enum name does not match writer's enum name")] - EnumNameMismatch, - #[error("Readers' record name does not match writer's record name")] - RecordNameMismatch, - #[error("Array items schema does not match")] - ArrayItemsMismatch, - #[error("Snappy decoder failed to get length of decompressed buffer")] - SnappyDecompressLenFailed, - #[error("End of file reached")] - Eof, - - // Schema parse errors - #[error("Failed to parse avro schema")] - SchemaParseErr(#[source] std::io::Error), - #[error("Unknown schema, expecting a required `type` field in schema")] - SchemaParseFailed, - #[error("Expecting fields key as a json array, found: {0}")] - SchemaFieldParseErr(String), - #[error("Expected: {0}, found: {1}")] - SchemaDataValidationFailed(String, String), - #[error("Schema has a field not found in the value")] - RecordFieldMissing, - #[error("Record schema does not a have a required field named `name`")] - RecordNameNotFound, - #[error("Record schema does not a have a required field named `type`")] - RecordTypeNotFound, - #[error("Expected record field to be a json array")] - ExpectedFieldsJsonArray, - #[error("Record's field json schema must be an object")] - InvalidRecordFieldType, - #[error("{0}")] - ParseFieldOrderErr(String), - #[error("Could not parse name from json value")] - NameParseFailed, - #[error("Parsing canonical form failed")] - ParsingCanonicalForm, - #[error("Duplicate definition of named schema")] - DuplicateSchema, - #[error("Invalid default value for union. Must be the first entry from union definition")] - FailedDefaultUnion, - #[error("Invalid default value for given schema")] - DefaultValueParse, - #[error("Unknown field ordering value.")] - UnknownFieldOrdering, - #[error("Field ordering value must be a string")] - InvalidFieldOrdering, - #[error("Failed to parse symbol from enum's symbols field")] - EnumSymbolParseErr, - #[error("Enum schema must contain required `symbols` field")] - EnumSymbolsMissing, - #[error("Enum value symbol not present in enum schema `symbols` field")] - EnumSymbolNotPresent, - #[error("Fixed schema `size` field must be a number")] - FixedSizeNotNumber, - #[error("Fixed schema `size` field missing")] - FixedSizeNotFound, - #[error("Unions cannot have multiple schemas of same type or immediate unions")] - DuplicateSchemaInUnion, - #[error("Expected the avro schema to be as one of json string, object or an array")] - UnknownSchema, - #[error("Expected record field to be a json object, found {0}")] - InvalidSchema(String), - #[error("{0}")] - InvalidDefaultValue(String), - #[error("Invalid type for {0}")] - InvalidType(String), - #[error("Enum schema parsing failed, found: {0}")] - EnumParseErr(String), - #[error("Primitve schema must be a string")] - InvalidPrimitiveSchema, - - // Validation errors - #[error("Mismatch in fixed bytes length: {found}, {expected}")] - FixedValueLenMismatch { found: usize, expected: usize }, - #[error("namespaces must either be empty or follow the grammer [()*")] - InvalidNamespace, - #[error("Field name must be [A-Za-z_] and subsequently contain only [A-Za-z0-9_]")] - InvalidName, - #[error("Array value is empty")] - EmptyArray, - #[error("Map value is empty")] - EmptyMap, - #[error("Crc generation failed")] - CRCGenFailed, - #[error("Snappy Crc mismatch")] - CRCMismatch { found: u32, expected: u32 }, - #[error("Named schema was not found for given value")] - NamedSchemaNotFoundForValue, - #[error("Value schema not found in union")] - NotFoundInUnion, - - // Serde specific errors - #[error("Serde error: {0}")] - Message(String), - #[error("Syntax error occured")] - Syntax, - #[error("Expected a string value")] - ExpectedString, - #[error("Unsupported type")] - Unsupported, - #[error("Unexpected avro value: {value}")] - UnexpectedAvroValue { value: String }, - - // Value errors - #[error("Expected value not found in variant instance")] - ExpectedVariantNotFound, -} diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index 9503a39..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,81 +0,0 @@ -//! Avrow is a pure Rust implementation of the [Apache Avro specification](https://avro.apache.org/docs/current/spec.html). -//! -//! For more details on the spec, head over to [FAQ](https://cwiki.apache.org/confluence/display/AVRO/FAQ) -//! -//! ## Using the library -//! -//! Add to your `Cargo.toml`: -//!```toml -//! [dependencies] -//! avrow = "0.1" -//!``` -//! ### A hello world example of reading and writing avro data files - -//!```rust -//!use avrow::{Reader, Schema, Writer, from_value}; -//!use std::str::FromStr; -//!use std::error::Error; -//! -//!use std::io::Cursor; -//! -//!fn main() -> Result<(), Box> { -//! // Writing data -//! -//! // Create a schema -//! let schema = Schema::from_str(r##""null""##)?; -//! // Create writer using schema and provide a buffer to write to -//! let mut writer = Writer::new(&schema, vec![])?; -//! // Write the data using append -//! writer.serialize(())?; -//! // or serialize -//! writer.serialize(())?; -//! // retrieve the underlying buffer using the into_inner method. -//! let buf = writer.into_inner()?; -//! -//! // Reading data -//! -//! // Create Reader by providing a Read wrapped version of `buf` -//! let reader = Reader::new(buf.as_slice())?; -//! // Use iterator for reading data in an idiomatic manner. -//! for i in reader { -//! // reading values can fail due to decoding errors, so the return value of iterator is a Option> -//! // it allows one to examine the failure reason on the underlying avro reader. -//! dbg!(&i); -//! // This value can be converted to a native Rust type using from_value method from the serde impl. -//! let _: () = from_value(&i)?; -//! } -//! -//! Ok(()) -//!} -//! -//!``` - -// TODO update logo -#![doc(html_favicon_url = "")] -#![doc(html_logo_url = "assets/avrow_logo.png")] -#![deny(missing_docs)] -#![recursion_limit = "1024"] -#![deny(unused_must_use)] -// #![deny(warnings)] - -mod codec; -pub mod config; -mod error; -mod reader; -mod schema; -mod serde_avro; -mod util; -mod value; -mod writer; - -pub use codec::Codec; -pub use error::AvrowErr; -pub use reader::from_value; -pub use reader::Header; -pub use reader::Reader; -pub use schema::Schema; -pub use serde_avro::to_value; -pub use value::Record; -pub use value::Value; -pub use writer::Writer; -pub use writer::WriterBuilder; diff --git a/src/reader.rs b/src/reader.rs deleted file mode 100644 index 8052d36..0000000 --- a/src/reader.rs +++ /dev/null @@ -1,707 +0,0 @@ -use crate::codec::Codec; -use crate::config::DEFAULT_FLUSH_INTERVAL; -use crate::error; -use crate::schema; -use crate::serde_avro; -use crate::util::{decode_bytes, decode_string}; -use crate::value; -use byteorder::LittleEndian; -use byteorder::ReadBytesExt; -use error::AvrowErr; -use indexmap::IndexMap; -use integer_encoding::VarIntReader; -use schema::Registry; -use schema::Schema; -use schema::Variant; -use serde::Deserialize; -use serde_avro::SerdeReader; -use std::collections::HashMap; -use std::convert::TryFrom; -use std::io::Cursor; -use std::io::Read; -use std::io::{Error, ErrorKind}; -use std::str; -use std::str::FromStr; -use value::{FieldValue, Record, Value}; - -/// Reader is the primary interface for reading data from an avro datafile. -pub struct Reader { - source: R, - header: Header, - // TODO when reading data call resolve schema https://avro.apache.org/docs/1.8.2/spec.html#Schema+Resolution - // This is the schema after it has been resolved using both reader and writer schema - // NOTE: This is a partially resolved schema - // schema: Option, - // TODO this is for experimental purposes, ideally we can just use references - reader_schema: Option, - block_buffer: Cursor>, - entries_in_block: u64, -} - -impl Reader -where - R: Read, -{ - /// Creates a Reader from an avro encoded readable buffer. - pub fn new(mut avro_source: R) -> Result { - let header = Header::from_reader(&mut avro_source)?; - Ok(Reader { - source: avro_source, - header, - reader_schema: None, - block_buffer: Cursor::new(vec![0u8; DEFAULT_FLUSH_INTERVAL]), - entries_in_block: 0, - }) - } - - /// Create a Reader with the given reader schema and a readable buffer. - pub fn with_schema(mut source: R, reader_schema: Schema) -> Result { - let header = Header::from_reader(&mut source)?; - - Ok(Reader { - source, - header, - reader_schema: Some(reader_schema), - block_buffer: Cursor::new(vec![0u8; DEFAULT_FLUSH_INTERVAL]), - entries_in_block: 0, - }) - } - - // TODO optimize based on benchmarks - fn next_block(&mut self) -> Result<(), std::io::Error> { - // if no more bytes to read, read_varint below returns an EOF - let entries_in_block: i64 = self.source.read_varint()?; - self.entries_in_block = entries_in_block as u64; - - let block_stream_len: i64 = self.source.read_varint()?; - - let mut compressed_block = vec![0u8; block_stream_len as usize]; - self.source.read_exact(&mut compressed_block)?; - - self.header - .codec - .decode(compressed_block, &mut self.block_buffer) - .map_err(|e| { - Error::new( - ErrorKind::Other, - format!("Failed decoding block data with codec, {:?}", e), - ) - })?; - - // Ready for reading from block - self.block_buffer.set_position(0); - - let mut sync_marker_buf = [0u8; 16]; - let _ = self.source.read_exact(&mut sync_marker_buf); - - if sync_marker_buf != self.header.sync_marker { - let err = Error::new( - ErrorKind::Other, - "Sync marker does not match as expected while reading", - ); - return Err(err); - } - - Ok(()) - } - - /// Retrieves a reference to the header metadata map. - pub fn meta(&self) -> &HashMap> { - self.header.metadata() - } -} - -/// `from_value` is the serde API for deserialization of avro encoded data to native Rust types. -pub fn from_value<'de, D: Deserialize<'de>>( - value: &'de Result, -) -> Result { - match value { - Ok(v) => { - let mut serde_reader = SerdeReader::new(v); - D::deserialize(&mut serde_reader) - } - Err(e) => Err(AvrowErr::UnexpectedAvroValue { - value: e.to_string(), - }), - } -} - -impl<'a, 's, R: Read> Iterator for Reader { - type Item = Result; - - fn next(&mut self) -> Option { - // invariant: True on start and end of an avro datafile - if self.entries_in_block == 0 { - if let Err(e) = self.next_block() { - // marks the end of the avro datafile - if let std::io::ErrorKind::UnexpectedEof = e.kind() { - return None; - } else { - return Some(Err(AvrowErr::DecodeFailed(e))); - } - } - } - - let writer_schema = &self.header.schema; - let w_cxt = &writer_schema.cxt; - let reader_schema = &self.reader_schema; - let value = if let Some(r_schema) = reader_schema { - let r_cxt = &r_schema.cxt; - decode_with_resolution( - &r_schema.variant, - &writer_schema.variant, - &r_cxt, - &w_cxt, - &mut self.block_buffer, - ) - } else { - // decode without the reader schema - decode(&writer_schema.variant, &mut self.block_buffer, &w_cxt) - }; - - self.entries_in_block -= 1; - - if let Err(e) = value { - return Some(Err(e)); - } - - Some(value) - } -} - -// Reads places priority on reader's schema when passing any schema context if a reader schema is provided. -pub(crate) fn decode_with_resolution( - r_schema: &Variant, - w_schema: &Variant, - r_cxt: &Registry, - w_cxt: &Registry, - reader: &mut R, -) -> Result { - // LHS: Writer schema, RHS: Reader schema - let value = match (w_schema, r_schema) { - (Variant::Null, Variant::Null) => Value::Null, - (Variant::Boolean, Variant::Boolean) => { - let mut buf = [0u8; 1]; - reader - .read_exact(&mut buf) - .map_err(AvrowErr::DecodeFailed)?; - match buf { - [0x00] => Value::Boolean(false), - [0x01] => Value::Boolean(true), - _o => { - return Err(AvrowErr::DecodeFailed(Error::new( - ErrorKind::InvalidData, - "expecting a 0x00 or 0x01 as a byte for boolean value", - ))) - } - } - } - (Variant::Int, Variant::Int) => { - Value::Int(reader.read_varint().map_err(AvrowErr::DecodeFailed)?) - } - // int is promotable to long, float, or double (we read as int and cast to promotable.) - (Variant::Int, Variant::Long) => Value::Long( - reader - .read_varint::() - .map_err(AvrowErr::DecodeFailed)? as i64, - ), - (Variant::Int, Variant::Float) => Value::Float( - reader - .read_varint::() - .map_err(AvrowErr::DecodeFailed)? as f32, - ), - (Variant::Int, Variant::Double) => Value::Double( - reader - .read_varint::() - .map_err(AvrowErr::DecodeFailed)? as f64, - ), - (Variant::Long, Variant::Long) => { - Value::Long(reader.read_varint().map_err(AvrowErr::DecodeFailed)?) - } - // long is promotable to float or double - (Variant::Long, Variant::Float) => Value::Float( - reader - .read_varint::() - .map_err(AvrowErr::DecodeFailed)? as f32, - ), - (Variant::Long, Variant::Double) => Value::Double( - reader - .read_varint::() - .map_err(AvrowErr::DecodeFailed)? as f64, - ), - (Variant::Float, Variant::Float) => Value::Float( - reader - .read_f32::() - .map_err(AvrowErr::DecodeFailed)?, - ), - (Variant::Double, Variant::Double) => Value::Double( - reader - .read_f64::() - .map_err(AvrowErr::DecodeFailed)?, - ), - // float is promotable to double - (Variant::Float, Variant::Double) => Value::Double( - reader - .read_f32::() - .map_err(AvrowErr::DecodeFailed)? as f64, - ), - (Variant::Bytes, Variant::Bytes) => Value::Bytes(decode_bytes(reader)?), - // bytes is promotable to string - (Variant::Bytes, Variant::Str) => { - let bytes = decode_bytes(reader)?; - let s = str::from_utf8(&bytes).map_err(|_e| { - let err = Error::new(ErrorKind::InvalidData, "failed converting bytes to string"); - AvrowErr::DecodeFailed(err) - })?; - - Value::Str(s.to_string()) - } - (Variant::Str, Variant::Str) => { - let buf = decode_bytes(reader)?; - let s = str::from_utf8(&buf).map_err(|_e| { - let err = Error::new(ErrorKind::InvalidData, "failed converting bytes to string"); - AvrowErr::DecodeFailed(err) - })?; - Value::Str(s.to_string()) - } - // string is promotable to bytes - (Variant::Str, Variant::Bytes) => { - let buf = decode_bytes(reader)?; - Value::Bytes(buf) - } - (Variant::Array { items: w_items }, Variant::Array { items: r_items }) => { - if w_items == r_items { - let block_count: i64 = reader.read_varint().map_err(AvrowErr::DecodeFailed)?; - let mut v = Vec::with_capacity(block_count as usize); - - for _ in 0..block_count { - let decoded = - decode_with_resolution(&*r_items, &*w_items, r_cxt, w_cxt, reader)?; - v.push(decoded); - } - - Value::Array(v) - } else { - return Err(AvrowErr::ArrayItemsMismatch); - } - } - // Resolution rules - // if both are records: - // * The ordering of fields may be different: fields are matched by name. [1] - // * Schemas for fields with the same name in both records are resolved recursively. [2] - // * If the writer's record contains a field with a name not present in the reader's record, - // the writer's value for that field is ignored. [3] - // * If the reader's record schema has a field that contains a default value, - // and writer's schema does not have a field with the same name, - // then the reader should use the default value from its field. [4] - // * If the reader's record schema has a field with no default value, - // and writer's schema does not have a field with the same name, an error is signalled. [5] - ( - Variant::Record { - name: writer_name, - fields: writer_fields, - .. - }, - Variant::Record { - name: reader_name, - fields: reader_fields, - .. - }, - ) => { - // [1] - let reader_name = reader_name.fullname(); - let writer_name = writer_name.fullname(); - if writer_name != reader_name { - return Err(AvrowErr::RecordNameMismatch); - } - - let mut rec = Record::new(&reader_name); - for f in reader_fields { - let reader_fieldname = f.0.as_str(); - let reader_field = f.1; - // [3] - if let Some(wf) = writer_fields.get(reader_fieldname) { - // [2] - let f_decoded = - decode_with_resolution(&reader_field.ty, &wf.ty, r_cxt, w_cxt, reader)?; - rec.insert(&reader_fieldname, f_decoded)?; - } else { - // [4] - let default_field = f.1; - if let Some(a) = &default_field.default { - rec.insert(&reader_fieldname, a.clone())?; - } else { - // [5] - return Err(AvrowErr::FieldNotFound); - } - } - } - - return Ok(Value::Record(rec)); - } - ( - Variant::Enum { - name: w_name, - symbols: w_symbols, - .. - }, - Variant::Enum { - name: r_name, - symbols: r_symbols, - .. - }, - ) => { - if w_name.fullname() != r_name.fullname() { - return Err(AvrowErr::EnumNameMismatch); - } - - let idx: i32 = reader.read_varint().map_err(AvrowErr::DecodeFailed)?; - let idx = idx as usize; - if idx >= w_symbols.len() { - return Err(AvrowErr::InvalidEnumSymbolIdx( - idx, - format!("{:?}", w_symbols), - )); - } - - let symbol = r_symbols.get(idx as usize); - if let Some(s) = symbol { - return Ok(Value::Enum(s.to_string())); - } else { - return Err(AvrowErr::EnumSymbolNotFound { idx }); - } - } - ( - Variant::Fixed { - name: w_name, - size: w_size, - }, - Variant::Fixed { - name: r_name, - size: r_size, - }, - ) => { - if w_name.fullname() != r_name.fullname() && w_size != r_size { - return Err(AvrowErr::FixedSchemaNameMismatch); - } else { - let mut fixed = vec![0u8; *r_size]; - reader - .read_exact(&mut fixed) - .map_err(AvrowErr::DecodeFailed)?; - Value::Fixed(fixed) - } - } - ( - Variant::Map { - values: writer_values, - }, - Variant::Map { - values: reader_values, - }, - ) => { - // here equality will be based - if writer_values == reader_values { - let block_count: i32 = reader.read_varint().map_err(AvrowErr::DecodeFailed)?; - let mut hm = HashMap::new(); - for _ in 0..block_count { - let key = decode_string(reader)?; - let value = decode(reader_values, reader, r_cxt)?; - hm.insert(key, value); - } - Value::Map(hm) - } else { - return Err(AvrowErr::MapSchemaMismatch); - } - } - ( - Variant::Union { - variants: writer_variants, - }, - Variant::Union { - variants: reader_variants, - }, - ) => { - let union_idx: i32 = reader.read_varint().map_err(AvrowErr::DecodeFailed)?; - if let Some(writer_schema) = writer_variants.get(union_idx as usize) { - for i in reader_variants { - if i == writer_schema { - return decode(i, reader, r_cxt); - } - } - } - - return Err(AvrowErr::UnionSchemaMismatch); - } - /* - if reader's is a union but writer's is not. The first schema in the reader's union that matches - the writer's schema is recursively resolved against it. If none match, an error is signalled. - */ - ( - writer_schema, - Variant::Union { - variants: reader_variants, - }, - ) => { - for i in reader_variants { - if i == writer_schema { - return decode(i, reader, r_cxt); - } - } - - return Err(AvrowErr::WriterNotInReader); - } - /* - if writer's schema is a union, but reader's is not. - If the reader's schema matches the selected writer's schema, - it is recursively resolved against it. If they do not match, an error is signalled. - */ - ( - Variant::Union { - variants: writer_variants, - }, - reader_schema, - ) => { - // Read the index value in the schema - let union_idx: i32 = reader.read_varint().map_err(AvrowErr::DecodeFailed)?; - let schema = writer_variants.get(union_idx as usize); - if let Some(s) = schema { - if s == reader_schema { - return decode(reader_schema, reader, r_cxt); - } - } - let writer_schema = format!("writer schema: {:?}", writer_variants); - let reader_schema = format!("reader schema: {:?}", reader_schema); - return Err(AvrowErr::SchemaResolutionFailed( - reader_schema, - writer_schema, - )); - } - other => { - return Err(AvrowErr::SchemaResolutionFailed( - format!("{:?}", other.0), - format!("{:?}", other.1), - )) - } - }; - - Ok(value) -} - -pub(crate) fn decode( - schema: &Variant, - reader: &mut R, - r_cxt: &Registry, -) -> Result { - let value = match schema { - Variant::Null => Value::Null, - Variant::Boolean => { - let mut buf = [0u8; 1]; - reader - .read_exact(&mut buf) - .map_err(AvrowErr::DecodeFailed)?; - match buf { - [0x00] => Value::Boolean(false), - [0x01] => Value::Boolean(true), - _ => { - return Err(AvrowErr::DecodeFailed(Error::new( - ErrorKind::InvalidData, - "Invalid boolean value, expected a 0x00 or a 0x01", - ))) - } - } - } - Variant::Int => Value::Int(reader.read_varint().map_err(AvrowErr::DecodeFailed)?), - Variant::Double => Value::Double( - reader - .read_f64::() - .map_err(AvrowErr::DecodeFailed)?, - ), - Variant::Long => Value::Long(reader.read_varint().map_err(AvrowErr::DecodeFailed)?), - Variant::Float => Value::Float( - reader - .read_f32::() - .map_err(AvrowErr::DecodeFailed)?, - ), - Variant::Str => { - let buf = decode_bytes(reader)?; - let s = str::from_utf8(&buf).map_err(|_e| { - let err = Error::new( - ErrorKind::InvalidData, - "failed converting from bytes to string", - ); - AvrowErr::DecodeFailed(err) - })?; - Value::Str(s.to_string()) - } - Variant::Array { items } => { - let block_count: i64 = reader.read_varint().map_err(AvrowErr::DecodeFailed)?; - - if block_count == 0 { - // FIXME do we send an empty array? - return Ok(Value::Array(Vec::new())); - } - - let mut it = Vec::with_capacity(block_count as usize); - for _ in 0..block_count { - let decoded = decode(&**items, reader, r_cxt)?; - it.push(decoded); - } - - Value::Array(it) - } - Variant::Bytes => Value::Bytes(decode_bytes(reader)?), - Variant::Map { values } => { - let block_count: usize = reader.read_varint().map_err(AvrowErr::DecodeFailed)?; - let mut hm = HashMap::new(); - for _ in 0..block_count { - let key = decode_string(reader)?; - let value = decode(values, reader, r_cxt)?; - hm.insert(key, value); - } - - Value::Map(hm) - } - Variant::Record { name, fields, .. } => { - let mut v = IndexMap::with_capacity(fields.len()); - for (field_name, field) in fields { - let field_name = field_name.to_string(); - let field_value = decode(&field.ty, reader, r_cxt)?; - let field_value = FieldValue::new(field_value); - v.insert(field_name, field_value); - } - - let rec = Record { - name: name.fullname(), - fields: v, - }; - Value::Record(rec) - } - Variant::Union { variants } => { - let variant_idx: i64 = reader.read_varint().map_err(AvrowErr::DecodeFailed)?; - decode(&variants[variant_idx as usize], reader, r_cxt)? - } - Variant::Named(schema_name) => { - let schema_variant = r_cxt - .get(schema_name) - .ok_or(AvrowErr::NamedSchemaNotFound)?; - decode(schema_variant, reader, r_cxt)? - } - a => { - return Err(AvrowErr::DecodeFailed(Error::new( - ErrorKind::InvalidData, - format!("Read failed for schema {:?}", a), - ))) - } - }; - - Ok(value) -} - -/// Header represents the avro datafile header. -#[derive(Debug)] -pub struct Header { - /// Writer's schema - pub(crate) schema: Schema, - /// A Map which stores avro metadata, like `avro.codec` and `avro.schema`. - /// Additional key values can be added through the - /// [WriterBuilder](struct.WriterBuilder.html)'s `set_metadata` method. - pub(crate) metadata: HashMap>, - /// A unique 16 byte sequence for file integrity when writing avro data to file. - pub(crate) sync_marker: [u8; 16], - /// codec parsed from the datafile - pub(crate) codec: Codec, -} - -fn decode_header_map(reader: &mut R) -> Result>, AvrowErr> -where - R: Read, -{ - let count: i64 = reader.read_varint().map_err(AvrowErr::DecodeFailed)?; - let count = count as usize; - let mut map = HashMap::with_capacity(count); - - for _ in 0..count { - let key = decode_string(reader)?; - let val = decode_bytes(reader)?; - map.insert(key, val); - } - - let _map_end: i64 = reader.read_varint().map_err(AvrowErr::DecodeFailed)?; - - Ok(map) -} - -impl Header { - /// Reads the header from an avro datafile - pub fn from_reader(reader: &mut R) -> Result { - let mut magic_buf = [0u8; 4]; - reader - .read_exact(&mut magic_buf[..]) - .map_err(|_| AvrowErr::HeaderDecodeFailed)?; - - if &magic_buf != b"Obj\x01" { - return Err(AvrowErr::InvalidDataFile); - } - - let map = decode_header_map(reader)?; - - let mut sync_marker = [0u8; 16]; - let _ = reader - .read_exact(&mut sync_marker) - .map_err(|_| AvrowErr::HeaderDecodeFailed)?; - - let schema_bytes = map.get("avro.schema").ok_or(AvrowErr::HeaderDecodeFailed)?; - - let schema = str::from_utf8(schema_bytes) - .map(Schema::from_str) - .map_err(|_| AvrowErr::HeaderDecodeFailed)??; - - let codec = if let Some(c) = map.get("avro.codec") { - match std::str::from_utf8(c) { - Ok(s) => Codec::try_from(s)?, - Err(s) => return Err(AvrowErr::UnsupportedCodec(s.to_string())), - } - } else { - Codec::Null - }; - - let header = Header { - schema, - metadata: map, - sync_marker, - codec, - }; - - Ok(header) - } - - /// Returns a reference to metadata from avro datafile header - pub fn metadata(&self) -> &HashMap> { - &self.metadata - } - - /// Returns a reference to the writer's schema in this header - pub fn schema(&self) -> &Schema { - &self.schema - } -} - -#[cfg(test)] -mod tests { - use crate::Reader; - #[test] - fn has_required_headers() { - let data = vec![ - 79, 98, 106, 1, 4, 22, 97, 118, 114, 111, 46, 115, 99, 104, 101, 109, 97, 32, 123, 34, - 116, 121, 112, 101, 34, 58, 34, 98, 121, 116, 101, 115, 34, 125, 20, 97, 118, 114, 111, - 46, 99, 111, 100, 101, 99, 14, 100, 101, 102, 108, 97, 116, 101, 0, 145, 85, 112, 15, - 87, 201, 208, 26, 183, 148, 48, 236, 212, 250, 38, 208, 2, 18, 227, 97, 96, 100, 98, - 102, 97, 5, 0, 145, 85, 112, 15, 87, 201, 208, 26, 183, 148, 48, 236, 212, 250, 38, - 208, - ]; - - let reader = Reader::new(data.as_slice()).unwrap(); - assert!(reader.meta().contains_key("avro.codec")); - assert!(reader.meta().contains_key("avro.schema")); - } -} diff --git a/src/schema/canonical.rs b/src/schema/canonical.rs deleted file mode 100644 index bfc5fcd..0000000 --- a/src/schema/canonical.rs +++ /dev/null @@ -1,259 +0,0 @@ -use crate::schema::Name; -use crate::serde_avro::AvrowErr; -use serde_json::json; -use serde_json::Value as JsonValue; -use std::cmp::PartialEq; - -// wrap overflow of 0xc15d213aa4d7a795 -const EMPTY: i64 = -4513414715797952619; - -static FP_TABLE: once_cell::sync::Lazy<[i64; 256]> = { - use once_cell::sync::Lazy; - Lazy::new(|| { - let mut fp_table: [i64; 256] = [0; 256]; - for i in 0..256 { - let mut fp = i; - for _ in 0..8 { - fp = (fp as u64 >> 1) as i64 ^ (EMPTY & -(fp & 1)); - } - fp_table[i as usize] = fp; - } - fp_table - }) -}; - -// relevant fields and in order fields according to spec -const RELEVANT_FIELDS: [&str; 7] = [ - "name", "type", "fields", "symbols", "items", "values", "size", -]; -/// Represents canonical form of an avro schema. This representation removes irrelevant fields -/// such as docs and aliases in the schema. -/// Fingerprinting methods are available on this instance. -#[derive(Debug, PartialEq)] -pub struct CanonicalSchema(pub(crate) JsonValue); - -impl std::fmt::Display for CanonicalSchema { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let c = serde_json::to_string_pretty(&self.0); - write!(f, "{}", c.map_err(|_| std::fmt::Error)?) - } -} - -impl CanonicalSchema { - #[cfg(feature = "sha2")] - pub fn sha256(&self) -> Vec { - use shatwo::{Digest, Sha256}; - let mut hasher = Sha256::new(); - hasher.update(self.0.to_string()); - let result = hasher.finalize(); - result.to_vec() - } - - #[cfg(feature = "md5")] - pub fn md5(&self) -> Vec { - let v = mdfive::compute(self.0.to_string().as_bytes()); - v.to_vec() - } - - pub fn rabin64(&self) -> i64 { - let buf = self.0.to_string(); - let buf = buf.as_bytes(); - let mut fp: i64 = EMPTY; - - buf.iter().for_each(|b| { - let idx = ((fp ^ *b as i64) & 0xff) as usize; - fp = (fp as u64 >> 8) as i64 ^ FP_TABLE[idx]; - }); - - fp - } -} - -// TODO unescape \uXXXX -// pub fn normalize_unescape(s: &str) -> &str { -// s -// } - -// [FULLNAMES] - traverse the `type` field and replace names with fullnames -pub fn normalize_name( - json_map: &mut serde_json::map::Map, - enclosing_namespace: Option<&str>, -) -> Result<(), AvrowErr> { - let name = Name::from_json_mut(json_map, enclosing_namespace)?; - - json_map["name"] = json!(name.fullname()); - - if let Some(JsonValue::Array(fields)) = json_map.get_mut("fields") { - for f in fields.iter_mut() { - if let JsonValue::Object(ref mut o) = f { - if let Some(JsonValue::Object(ref mut o)) = o.get_mut("type") { - if o.contains_key("name") { - normalize_name(o, name.namespace())?; - } - } - } - } - } - - Ok(()) -} - -// [STRIP] -pub fn normalize_strip( - schema: &mut serde_json::map::Map, -) -> Result<(), AvrowErr> { - if schema.contains_key("doc") { - schema.remove("doc").ok_or(AvrowErr::ParsingCanonicalForm)?; - } - if schema.contains_key("aliases") { - schema - .remove("aliases") - .ok_or(AvrowErr::ParsingCanonicalForm)?; - } - - Ok(()) -} - -type JsonMap = serde_json::map::Map; - -pub fn order_fields(json: &JsonMap) -> Result { - let mut ordered = JsonMap::new(); - - for field in RELEVANT_FIELDS.iter() { - if let Some(value) = json.get(*field) { - match value { - JsonValue::Object(m) => { - ordered.insert(field.to_string(), json!(order_fields(m)?)); - } - JsonValue::Array(a) => { - let mut obj_arr = vec![]; - for field in a { - match field { - JsonValue::Object(m) => { - obj_arr.push(json!(order_fields(m)?)); - } - _ => { - obj_arr.push(field.clone()); - } - } - } - - ordered.insert(field.to_string(), json!(obj_arr)); - } - _ => { - ordered.insert(field.to_string(), value.clone()); - } - } - } - } - - Ok(ordered) -} - -// The following steps in parsing canonical form are handled by serde so we rely on that. -// [INTEGERS] - serde will not parse a string with a zero prefixed integer. -// [WHITESPACE] - serde also eliminates whitespace. -// [STRINGS] - TODO in `normalize_unescape` -// For rest of the steps, we implement them as below -pub(crate) fn normalize_schema(json_schema: &JsonValue) -> Result { - match json_schema { - // Normalize a complex schema - JsonValue::Object(ref scm) => { - // [PRIMITIVES] - if let Some(JsonValue::String(s)) = scm.get("type") { - match s.as_ref() { - "record" | "enum" | "array" | "maps" | "union" | "fixed" => {} - _ => { - return Ok(json!(s)); - } - } - } - - let mut schema = scm.clone(); - // [FULLNAMES] - if schema.contains_key("name") { - normalize_name(&mut schema, None)?; - } - // [ORDER] - let mut schema = order_fields(&schema)?; - // [STRIP] - normalize_strip(&mut schema)?; - Ok(json!(schema)) - } - // [PRIMITIVES] - // Normalize a primitive schema - a @ JsonValue::String(_) => Ok(json!(a)), - // Normalize a union schema - JsonValue::Array(v) => { - let mut variants = Vec::with_capacity(v.len()); - for i in v { - let normalized = normalize_schema(i)?; - variants.push(normalized); - } - Ok(json!(v)) - } - _other => Err(AvrowErr::UnknownSchema), - } -} - -#[cfg(test)] -mod tests { - use crate::Schema; - use std::str::FromStr; - #[test] - fn canonical_primitives() { - let schema_str = r##"{"type": "null"}"##; - let _ = Schema::from_str(schema_str).unwrap(); - } - - #[test] - #[cfg(feature = "fingerprint")] - fn canonical_schema_sha256_fingerprint() { - let header_schema = r##"{"type": "record", "name": "org.apache.avro.file.Header", - "fields" : [ - {"name": "magic", "type": {"type": "fixed", "name": "Magic", "size": 4}}, - {"name": "meta", "type": {"type": "map", "values": "bytes"}}, - {"name": "sync", "type": {"type": "fixed", "name": "Sync", "size": 16}} - ] - }"##; - let schema = Schema::from_str(header_schema).unwrap(); - let canonical = schema.canonical_form(); - - let expected = "809bed56cf47c84e221ad8b13e28a66ed9cd6b1498a43bad9aa0c868205e"; - let found = canonical.sha256(); - let mut fingerprint_str = String::new(); - for i in found { - let a = format!("{:x}", i); - fingerprint_str.push_str(&a); - } - - assert_eq!(expected, fingerprint_str); - } - - #[test] - #[cfg(feature = "fingerprint")] - fn schema_rabin_fingerprint() { - let schema = r##""null""##; - let expected = "0x63dd24e7cc258f8a"; - let schema = Schema::from_str(schema).unwrap(); - let canonical = schema.canonical_form(); - let actual = format!("0x{:x}", canonical.rabin64()); - assert_eq!(expected, actual); - } - - #[test] - #[cfg(feature = "fingerprint")] - fn schema_md5_fingerprint() { - let schema = r##""null""##; - let expected = "9b41ef67651c18488a8b8bb67c75699"; - let schema = Schema::from_str(schema).unwrap(); - let canonical = schema.canonical_form(); - let actual = canonical.md5(); - let mut fingerprint_str = String::new(); - for i in actual { - let a = format!("{:x}", i); - fingerprint_str.push_str(&a); - } - assert_eq!(expected, fingerprint_str); - } -} diff --git a/src/schema/common.rs b/src/schema/common.rs deleted file mode 100644 index cf1a893..0000000 --- a/src/schema/common.rs +++ /dev/null @@ -1,360 +0,0 @@ -// This module contains definition of types that are common across a subset of -// avro schemas. - -use crate::error::AvrowErr; -use crate::schema::Variant; -use crate::value::Value; -use serde_json::Value as JsonValue; -use std::fmt::{self, Display}; -use std::str::FromStr; - -/////////////////////////////////////////////////////////////////////////////// -/// Name implementation for named types: record, fixed, enum -/////////////////////////////////////////////////////////////////////////////// - -pub(crate) fn validate_name(idx: usize, name: &str) -> Result<(), AvrowErr> { - if name.contains('.') - || (name.starts_with(|a: char| a.is_ascii_digit()) && idx == 0) - || name.is_empty() - || !name.chars().any(|a| a.is_ascii_alphanumeric() || a == '_') - { - Err(AvrowErr::InvalidName) - } else { - Ok(()) - } -} - -// Follows the grammer: | [()*] -pub(crate) fn validate_namespace(s: &str) -> Result<(), AvrowErr> { - let split = s.split('.'); - for (i, n) in split.enumerate() { - let _ = validate_name(i, n).map_err(|_| AvrowErr::InvalidNamespace)?; - } - Ok(()) -} - -/// Represents `fullname` attribute and its constituents -/// of a named avro type i.e, Record, Fixed and Enum -#[derive(Debug, Clone, Eq, PartialOrd, Ord)] -pub struct Name { - pub(crate) name: String, - pub(crate) namespace: Option, -} - -impl Name { - // Creates an validates the name. This will also extract the namespace if a dot is present in `name` - // Any further calls to set_namespace, will be a noop if the name already contains a dot. - pub(crate) fn new(name: &str) -> Result { - let mut namespace = None; - let name = if name.contains('.') { - // should not have multiple dots and dots in end or start - let _ = validate_namespace(name)?; - // strip namespace - let idx = name.rfind('.').unwrap(); // we check for ., so it's okay - namespace = Some(name[..idx].to_string()); - let name = &name[idx + 1..]; - validate_name(0, name)?; - name - } else { - // TODO perform namespace lookups from enclosing schema if any - // This will require us to pass context to this method. - // Update: this is now handled by from_json method as that's called from places - // where we have context on most tightly enclosing schema. - validate_name(0, name)?; - name - }; - - Ok(Self { - name: name.to_string(), - namespace, - }) - } - - // TODO also parse namespace from json value - pub(crate) fn from_json( - json: &serde_json::map::Map, - enclosing_namespace: Option<&str>, - ) -> Result { - let mut name = if let Some(JsonValue::String(ref s)) = json.get("name") { - Name::new(s) - } else { - return Err(AvrowErr::NameParseFailed); - }?; - - // As per spec, If the name field has a dot, that is a fullname. any namespace provided is ignored. - // If no namespace was extracted from the name itself (i.e., name did not contain a dot) - // we then see if we have the namespace field on the json itself - // otherwise we use the enclosing namespace if that is a Some(namespace) - if name.namespace.is_none() { - if let Some(namespace) = json.get("namespace") { - if let JsonValue::String(s) = namespace { - validate_namespace(s)?; - name.set_namespace(s)?; - } - } else if let Some(a) = enclosing_namespace { - validate_namespace(a)?; - name.set_namespace(a)?; - } - } - - Ok(name) - } - - pub(crate) fn namespace(&self) -> Option<&str> { - self.namespace.as_deref() - } - - // receives a mutable json and parses a Name and removes namespace. Used for canonicalization. - // TODO change as above from_json method, should take enclosing namespace. - pub(crate) fn from_json_mut( - json: &mut serde_json::map::Map, - enclosing_namespace: Option<&str>, - ) -> Result { - let mut name = if let Some(JsonValue::String(ref s)) = json.get("name") { - Name::new(s) - } else { - return Err(AvrowErr::NameParseFailed); - }?; - - if name.namespace.is_none() { - if let Some(namespace) = json.get("namespace") { - if let JsonValue::String(s) = namespace { - validate_namespace(s)?; - name.set_namespace(s)?; - json.remove("namespace"); - } - } else if let Some(a) = enclosing_namespace { - validate_namespace(a)?; - name.set_namespace(a)?; - } - } - - // if let Some(namespace) = json.get("namespace") { - // if let JsonValue::String(s) = namespace { - // name.set_namespace(s)?; - // json.remove("namespace"); - // } - // } - - Ok(name) - } - - pub(crate) fn set_namespace(&mut self, namespace: &str) -> Result<(), AvrowErr> { - // empty string is a null namespace - if namespace.is_empty() { - return Ok(()); - } - - validate_namespace(namespace)?; - // If a namespace was already extracted when constructing name (name had a dot) - // then this is a noop - if self.namespace.is_none() { - let _ = validate_namespace(namespace)?; - self.namespace = Some(namespace.to_string()); - } - Ok(()) - } - - // TODO according to Rust convention, item path separators are :: instead of . - // TODO should we add a configurable separator. - // TODO should do namespace lookup from enclosing name schema if applicable. (pass enclosing schema as a context) - pub(crate) fn fullname(&self) -> String { - // if self.name.contains(".") { - // self.name.to_string() - // } else if let Some(n) = &self.namespace { - // if n.is_empty() { - // // According to spec, it's fine to put "" as a namespace, which becomes a null namespace - // format!("{}", self.name) - // } else { - // format!("{}.{}", n, self.name) - // } - // } else { - // // The case when only name exists. - // // TODO As of now we just return without any enclosing namespace. - // // TODO pass the most tightly enclosing namespace here when only name is provided. - // self.name.to_string() - // } - if let Some(n) = &self.namespace { - if n.is_empty() { - // According to spec, it's fine to put "" as a namespace, which becomes a null namespace - self.name.to_string() - } else { - format!("{}.{}", n, self.name) - } - } else { - self.name.to_string() - } - } -} - -impl Display for Name { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - if let Some(ref namespace) = self.namespace { - write!(f, "{}.{}", namespace, self.name) - } else { - write!(f, "{}", self.name) - } - } -} - -impl FromStr for Name { - type Err = AvrowErr; - - fn from_str(s: &str) -> Result { - Name::new(s) - } -} - -impl std::convert::TryFrom<&str> for Name { - type Error = AvrowErr; - - fn try_from(value: &str) -> Result { - Name::new(value) - } -} - -impl PartialEq for Name { - fn eq(&self, other: &Self) -> bool { - self.fullname() == other.fullname() - } -} - -/////////////////////////////////////////////////////////////////////////////// -/// Ordering for record fields -/////////////////////////////////////////////////////////////////////////////// - -#[derive(Debug, PartialEq, Clone)] -pub enum Order { - Ascending, - Descending, - Ignore, -} - -impl FromStr for Order { - type Err = AvrowErr; - fn from_str(s: &str) -> Result { - match s { - "ascending" => Ok(Order::Ascending), - "descending" => Ok(Order::Descending), - "ignore" => Ok(Order::Ignore), - _ => Err(AvrowErr::UnknownFieldOrdering), - } - } -} - -/////////////////////////////////////////////////////////////////////////////// -/// Record field definition. -/////////////////////////////////////////////////////////////////////////////// - -#[derive(Debug, Clone)] -pub struct Field { - pub(crate) name: String, - pub(crate) ty: Variant, - pub(crate) default: Option, - pub(crate) order: Order, - pub(crate) aliases: Option>, -} - -// TODO do we also use order for equality? -impl std::cmp::PartialEq for Field { - fn eq(&self, other: &Self) -> bool { - self.name == other.name && self.ty == other.ty - } -} - -impl Field { - pub(crate) fn new( - name: &str, - ty: Variant, - default: Option, - order: Order, - aliases: Option>, - ) -> Result { - validate_name(0, name)?; - Ok(Field { - name: name.to_string(), - ty, - default, - order, - aliases, - }) - } -} - -#[cfg(test)] -mod tests { - use super::validate_namespace; - use super::Name; - - #[test] - #[should_panic(expected = "InvalidName")] - fn name_starts_with_number() { - Name::new("2org.apache.avro").unwrap(); - } - - #[test] - #[should_panic(expected = "InvalidNamespace")] - fn invalid_namespace() { - let mut name = Name::new("org.apache.avro").unwrap(); - name.set_namespace("23").unwrap(); - } - - #[test] - fn name_with_seperate_namespace() { - let mut name = Name::new("hello").unwrap(); - let _ = name.set_namespace("org.foo"); - assert_eq!("org.foo.hello", name.fullname().to_string()); - } - - #[test] - fn name_contains_dots() { - let name = Name::new("org.apache.avro").unwrap(); - assert_eq!("avro", name.name.to_string()); - assert_eq!("org.apache.avro", name.fullname().to_string()); - } - - #[test] - fn fullname_with_empty_namespace() { - let mut name = Name::new("org.apache.avro").unwrap(); - name.set_namespace("").unwrap(); - assert_eq!("org.apache.avro", name.fullname()); - } - - #[test] - fn multiple_dots_invalid() { - let a = "some.namespace..foo"; - assert!(validate_namespace(a).is_err()); - } - - #[test] - fn name_has_dot_and_namespace_present() { - let json_str = r##" - { - "name":"my.longlist", - "namespace":"com.some", - "type":"record" - } - "##; - let json: serde_json::Value = serde_json::from_str(json_str).unwrap(); - let name = Name::from_json(json.as_object().unwrap(), None).unwrap(); - assert_eq!(name.name, "longlist"); - assert_eq!(name.namespace, Some("my".to_string())); - assert_eq!(name.fullname(), "my.longlist"); - } - - #[test] - fn name_no_dot_and_namespace_present() { - let json_str = r##" - { - "name":"longlist", - "namespace":"com.some", - "type":"record" - } - "##; - let json: serde_json::Value = serde_json::from_str(json_str).unwrap(); - let name = Name::from_json(json.as_object().unwrap(), None).unwrap(); - assert_eq!(name.name, "longlist"); - assert_eq!(name.namespace, Some("com.some".to_string())); - assert_eq!(name.fullname(), "com.some.longlist"); - } -} diff --git a/src/schema/mod.rs b/src/schema/mod.rs deleted file mode 100644 index 224a2ac..0000000 --- a/src/schema/mod.rs +++ /dev/null @@ -1,258 +0,0 @@ -//! Contains routines for parsing and validating an Avro schema. -//! Schemas in avro are written as JSON and can be provided as .avsc files -//! to a Writer or a Reader. - -pub mod common; -#[cfg(test)] -mod tests; -use crate::error::AvrowErr; -pub use common::Order; -mod canonical; -pub mod parser; -pub(crate) use parser::Registry; - -use crate::error::AvrowResult; -use crate::value::Value; -use canonical::normalize_schema; -use canonical::CanonicalSchema; -use common::{Field, Name}; -use indexmap::IndexMap; -use serde_json::{self, Value as JsonValue}; -use std::fmt::Debug; -use std::fs::OpenOptions; -use std::path::Path; - -/// A schema parsed from json value -#[derive(Debug, Clone, PartialEq)] -pub(crate) enum Variant { - Null, - Boolean, - Int, - Long, - Float, - Double, - Bytes, - Str, - Record { - name: Name, - aliases: Option>, - fields: IndexMap, - }, - Fixed { - name: Name, - size: usize, - }, - Enum { - name: Name, - aliases: Option>, - symbols: Vec, - }, - Map { - values: Box, - }, - Array { - items: Box, - }, - Union { - variants: Vec, - }, - Named(String), -} - -/// Represents the avro schema used to write encoded avro data -#[derive(Debug)] -pub struct Schema { - // TODO can remove this if not needed - inner: JsonValue, - // Schema context that has a lookup table to resolve named schema references - pub(crate) cxt: Registry, - // typed and stripped version of schema used internally. - pub(crate) variant: Variant, - // canonical form of schema. This is used for equality. - pub(crate) canonical: CanonicalSchema, -} - -impl PartialEq for Schema { - fn eq(&self, other: &Self) -> bool { - self.canonical == other.canonical - } -} - -impl std::str::FromStr for Schema { - type Err = AvrowErr; - /// Parse an avro schema from a json string - /// One can use Rust's raw string syntax (r##""##) to pass schema. - fn from_str(schema: &str) -> Result { - let schema_json = - serde_json::from_str(schema).map_err(|e| AvrowErr::SchemaParseErr(e.into()))?; - Schema::parse_imp(schema_json) - } -} - -impl Schema { - /// Parses an avro schema from a json description of schema in a file. - /// Alternatively, one can use the `FromStr` impl to create a `Schema` from a JSON string: - /// ``` - /// use std::str::FromStr; - /// use avrow::Schema; - /// - /// let schema = Schema::from_str(r##""null""##).unwrap(); - /// ``` - pub fn from_path + Debug>(path: P) -> AvrowResult { - let schema_file = OpenOptions::new() - .read(true) - .open(&path) - .map_err(AvrowErr::SchemaParseErr)?; - let value = - serde_json::from_reader(schema_file).map_err(|e| AvrowErr::SchemaParseErr(e.into()))?; - Schema::parse_imp(value) - } - - fn parse_imp(schema_json: JsonValue) -> AvrowResult { - let mut parser = Registry::new(); - let pcf = CanonicalSchema(normalize_schema(&schema_json)?); - // TODO see if we can use canonical form to parse variant - let variant = parser.parse_schema(&schema_json, None)?; - Ok(Schema { - inner: schema_json, - cxt: parser, - variant, - canonical: pcf, - }) - } - - pub(crate) fn as_bytes(&self) -> Vec { - format!("{}", self.inner).into_bytes() - } - - pub(crate) fn variant(&self) -> &Variant { - &self.variant - } - - #[inline(always)] - pub(crate) fn validate(&self, value: &Value) -> AvrowResult<()> { - self.variant.validate(value, &self.cxt) - } - - /// Returns the canonical form of an Avro schema - /// ```rust - /// use avrow::Schema; - /// use std::str::FromStr; - /// - /// let schema = Schema::from_str(r##" - /// { - /// "type": "record", - /// "name": "LongList", - /// "aliases": ["LinkedLongs"], - /// "fields" : [ - /// {"name": "value", "type": "long"}, - /// {"name": "next", "type": ["null", "LongList"] - /// }] - /// } - /// "##).unwrap(); - /// let canonical = schema.canonical_form(); - /// ``` - pub fn canonical_form(&self) -> &CanonicalSchema { - &self.canonical - } -} - -impl Variant { - pub fn validate(&self, value: &Value, cxt: &Registry) -> AvrowResult<()> { - let variant = self; - match (value, variant) { - (Value::Null, Variant::Null) - | (Value::Boolean(_), Variant::Boolean) - | (Value::Int(_), Variant::Int) - // long is promotable to float or double - | (Value::Long(_), Variant::Long) - | (Value::Long(_), Variant::Float) - | (Value::Long(_), Variant::Double) - // int is promotable to long, float or double - | (Value::Int(_), Variant::Long) - | (Value::Int(_), Variant::Float) - | (Value::Int(_), Variant::Double) - | (Value::Float(_), Variant::Float) - // float is promotable to double - | (Value::Float(_), Variant::Double) - | (Value::Double(_), Variant::Double) - | (Value::Str(_), Variant::Str) - // string is promotable to bytes - | (Value::Str(_), Variant::Bytes) - // bytes is promotable to string - | (Value::Bytes(_), Variant::Str) - | (Value::Bytes(_), Variant::Bytes) => {}, - (Value::Fixed(v), Variant::Fixed { size, .. }) - | (Value::Bytes(v), Variant::Fixed { size, .. }) => { - if v.len() != *size { - return Err(AvrowErr::FixedValueLenMismatch { - found: v.len(), - expected: *size, - }); - } - } - (Value::Record(rec), Variant::Record { ref fields, .. }) => { - for (fname, fvalue) in &rec.fields { - if let Some(ftype) = fields.get(fname) { - ftype.ty.validate(&fvalue.value, cxt)?; - } else { - return Err(AvrowErr::RecordFieldMissing); - } - } - } - (Value::Map(hmap), Variant::Map { values }) => { - return if let Some(v) = hmap.values().next() { - values.validate(v, cxt) - } else { - Err(AvrowErr::EmptyMap) - } - } - (Value::Enum(sym), Variant::Enum { symbols, .. }) if symbols.contains(sym) => { - return Ok(()) - } - (Value::Array(item), Variant::Array { items }) => { - return if let Some(v) = item.first() { - items.validate(v, cxt) - } else { - Err(AvrowErr::EmptyArray) - } - } - (v, Variant::Named(name)) => { - if let Some(schema) = cxt.get(&name) { - if schema.validate(v, cxt).is_ok() { - return Ok(()); - } - } - return Err(AvrowErr::NamedSchemaNotFoundForValue) - } - // Value `a` can be any of the above schemas + any named schema in the schema registry - (a, Variant::Union { variants }) => { - for s in variants.iter() { - if s.validate(a, cxt).is_ok() { - return Ok(()); - } - } - - return Err(AvrowErr::NotFoundInUnion) - } - - (v, s) => { - return Err(AvrowErr::SchemaDataValidationFailed( - format!("{:?}", v), - format!("{:?}", s), - )) - } - } - - Ok(()) - } - - fn get_named_mut(&mut self) -> Option<&mut Name> { - match self { - Variant::Record { name, .. } - | Variant::Fixed { name, .. } - | Variant::Enum { name, .. } => Some(name), - _ => None, - } - } -} diff --git a/src/schema/parser.rs b/src/schema/parser.rs deleted file mode 100644 index adb3c38..0000000 --- a/src/schema/parser.rs +++ /dev/null @@ -1,494 +0,0 @@ -use super::common::{Field, Name, Order}; -use super::Variant; -use crate::error::io_err; -use crate::error::AvrowErr; -use crate::error::AvrowResult; -use crate::schema::common::validate_name; -use crate::value::FieldValue; -use crate::value::Value; -use indexmap::IndexMap; -use serde_json::{Map, Value as JsonValue}; -use std::borrow::ToOwned; -use std::collections::HashMap; - -// Wraps a { name -> schema } lookup table to aid parsing named references in complex schemas -// During parsing, the value for each key may get updated as a schema discovers -// more information about the schema during parsing. -#[derive(Debug, Clone)] -pub(crate) struct Registry { - // TODO: use a reference to Variant? - cxt: HashMap, -} - -impl Registry { - pub(crate) fn new() -> Self { - Self { - cxt: HashMap::new(), - } - } - - pub(crate) fn get<'a>(&'a self, name: &str) -> Option<&'a Variant> { - self.cxt.get(name) - } - - pub(crate) fn parse_schema( - &mut self, - value: &JsonValue, - enclosing_namespace: Option<&str>, - ) -> Result { - match value { - // Parse a complex schema - JsonValue::Object(ref schema) => self.parse_object(schema, enclosing_namespace), - // Parse a primitive schema, could also be a named schema reference - JsonValue::String(ref schema) => self.parse_primitive(&schema, enclosing_namespace), - // Parse a union schema - JsonValue::Array(ref schema) => self.parse_union(schema, enclosing_namespace), - _ => Err(AvrowErr::UnknownSchema), - } - } - - fn parse_union( - &mut self, - schema: &[JsonValue], - enclosing_namespace: Option<&str>, - ) -> Result { - let mut union_schema = vec![]; - for s in schema { - let parsed_schema = self.parse_schema(s, enclosing_namespace)?; - match parsed_schema { - Variant::Union { .. } => { - return Err(AvrowErr::DuplicateSchemaInUnion); - } - _ => { - if union_schema.contains(&parsed_schema) { - return Err(AvrowErr::DuplicateSchemaInUnion); - } else { - union_schema.push(parsed_schema); - } - } - } - } - Ok(Variant::Union { - variants: union_schema, - }) - } - - fn get_fullname(&self, name: &str, enclosing_namespace: Option<&str>) -> String { - if let Some(namespace) = enclosing_namespace { - format!("{}.{}", namespace, name) - } else { - name.to_string() - } - } - - /// Parse a `serde_json::Value` representing a primitive Avro type into a `Schema`. - fn parse_primitive( - &mut self, - schema: &str, - enclosing_namespace: Option<&str>, - ) -> Result { - match schema { - "null" => Ok(Variant::Null), - "boolean" => Ok(Variant::Boolean), - "int" => Ok(Variant::Int), - "long" => Ok(Variant::Long), - "double" => Ok(Variant::Double), - "float" => Ok(Variant::Float), - "bytes" => Ok(Variant::Bytes), - "string" => Ok(Variant::Str), - other if !other.is_empty() => { - let name = self.get_fullname(other, enclosing_namespace); - if self.cxt.contains_key(&name) { - Ok(Variant::Named(name)) - } else { - Err(AvrowErr::SchemaParseErr(io_err(&format!( - "named schema `{}` must be defined before use", - other - )))) - } - } - _ => Err(AvrowErr::InvalidPrimitiveSchema), - } - } - - fn parse_record_fields( - &mut self, - fields: &[serde_json::Value], - enclosing_namespace: Option<&str>, - ) -> Result, AvrowErr> { - let mut fields_parsed = IndexMap::with_capacity(fields.len()); - for field_obj in fields { - match field_obj { - JsonValue::Object(o) => { - let name = o - .get("name") - .and_then(|a| a.as_str()) - .ok_or(AvrowErr::RecordNameNotFound)?; - - let ty: &JsonValue = o.get("type").ok_or(AvrowErr::RecordTypeNotFound)?; - let mut ty = self.parse_schema(ty, enclosing_namespace)?; - - // if ty is named use enclosing namespace to construct the fullname - if let Some(name) = ty.get_named_mut() { - // if parsed type has its own namespace - if name.namespace().is_none() { - if let Some(namespace) = enclosing_namespace { - name.set_namespace(namespace)?; - } - } - } - - let default = if let Some(v) = o.get("default") { - Some(parse_default(v, &ty)?) - } else { - None - }; - - let order = if let Some(order) = o.get("order") { - parse_field_order(order)? - } else { - Order::Ascending - }; - - let aliases = parse_aliases(o.get("aliases")); - - fields_parsed.insert( - name.to_string(), - Field::new(name, ty, default, order, aliases)?, - ); - } - _ => return Err(AvrowErr::InvalidRecordFieldType), - } - } - - Ok(fields_parsed) - } - - fn parse_object( - &mut self, - value: &Map, - enclosing_namespace: Option<&str>, - ) -> Result { - match value.get("type") { - Some(&JsonValue::String(ref s)) if s == "record" => { - let rec_name = Name::from_json(value, enclosing_namespace)?; - - // Insert a named reference to support recursive schema definitions. - self.cxt - .insert(rec_name.to_string(), Variant::Named(rec_name.to_string())); - - let fields = if let Some(JsonValue::Array(ref fields_vec)) = value.get("fields") { - fields_vec - } else { - return Err(AvrowErr::ExpectedFieldsJsonArray); - }; - - let fields = self.parse_record_fields(fields, { - if rec_name.namespace().is_some() { - // Most tightly enclosing namespace, which is this namespace - rec_name.namespace() - } else { - enclosing_namespace - } - })?; - - let aliases = parse_aliases(value.get("aliases")); - - let rec = Variant::Record { - name: rec_name.clone(), - aliases, - fields, - }; - - let rec_for_registry = rec.clone(); - let rec_name = rec_name.to_string(); - - // if a record schema is being redefined throw an error. - if let Some(Variant::Named(_)) = self.cxt.get(&rec_name) { - self.cxt.insert(rec_name, rec_for_registry); - } else { - return Err(AvrowErr::DuplicateSchema); - } - - Ok(rec) - } - Some(&JsonValue::String(ref s)) if s == "enum" => { - let name = Name::from_json(value, enclosing_namespace)?; - let aliases = parse_aliases(value.get("aliases")); - let mut symbols = vec![]; - - if let Some(v) = value.get("symbols") { - match v { - JsonValue::Array(sym) => { - // let mut symbols = Vec::with_capacity(sym.len()); - for v in sym { - let symbol = v.as_str().ok_or(AvrowErr::EnumSymbolParseErr)?; - validate_name(0, symbol)?; - symbols.push(symbol.to_string()); - } - } - other => { - return Err(AvrowErr::EnumParseErr(format!("{:?}", other))); - } - } - } else { - return Err(AvrowErr::EnumSymbolsMissing); - } - - let name_str = name.fullname(); - - let enum_schema = Variant::Enum { - name, - aliases, - symbols, - }; - - self.cxt.insert(name_str, enum_schema.clone()); - - Ok(enum_schema) - } - Some(&JsonValue::String(ref s)) if s == "array" => { - let item_missing_err = AvrowErr::SchemaParseErr(io_err( - "Array schema must have `items` field defined", - )); - let items_schema = value.get("items").ok_or(item_missing_err)?; - let parsed_items = self.parse_schema(items_schema, enclosing_namespace)?; - Ok(Variant::Array { - items: Box::new(parsed_items), - }) - } - Some(&JsonValue::String(ref s)) if s == "map" => { - let item_missing_err = - AvrowErr::SchemaParseErr(io_err("Map schema must have `values` field defined")); - let items_schema = value.get("values").ok_or(item_missing_err)?; - let parsed_items = self.parse_schema(items_schema, enclosing_namespace)?; - Ok(Variant::Map { - values: Box::new(parsed_items), - }) - } - Some(&JsonValue::String(ref s)) if s == "fixed" => { - let name = Name::from_json(value, enclosing_namespace)?; - let size = value.get("size").ok_or(AvrowErr::FixedSizeNotFound)?; - let name_str = name.fullname(); - - let fixed_schema = Variant::Fixed { - name, - size: size.as_u64().ok_or(AvrowErr::FixedSizeNotNumber)? as usize, // clamp to usize - }; - - self.cxt.insert(name_str, fixed_schema.clone()); - - Ok(fixed_schema) - } - Some(JsonValue::String(ref s)) if s == "null" => Ok(Variant::Null), - Some(JsonValue::String(ref s)) if s == "boolean" => Ok(Variant::Boolean), - Some(JsonValue::String(ref s)) if s == "int" => Ok(Variant::Int), - Some(JsonValue::String(ref s)) if s == "long" => Ok(Variant::Long), - Some(JsonValue::String(ref s)) if s == "float" => Ok(Variant::Float), - Some(JsonValue::String(ref s)) if s == "double" => Ok(Variant::Double), - Some(JsonValue::String(ref s)) if s == "bytes" => Ok(Variant::Bytes), - Some(JsonValue::String(ref s)) if s == "string" => Ok(Variant::Str), - _other => Err(AvrowErr::SchemaParseFailed), - } - } -} - -// TODO add support if needed -// fn parse_doc(value: Option<&JsonValue>) -> Option { -// if let Some(JsonValue::String(s)) = value { -// Some(s.to_string()) -// } else { -// None -// } -// } - -// Parses the `order` of a field, defaults to `ascending` order -pub(crate) fn parse_field_order(order: &JsonValue) -> AvrowResult { - match *order { - JsonValue::String(ref s) => match &**s { - "ascending" => Ok(Order::Ascending), - "descending" => Ok(Order::Descending), - "ignore" => Ok(Order::Ignore), - _ => Err(AvrowErr::UnknownFieldOrdering), - }, - _ => Err(AvrowErr::InvalidFieldOrdering), - } -} - -// Parses aliases of a field -fn parse_aliases(aliases: Option<&JsonValue>) -> Option> { - match aliases { - Some(JsonValue::Array(ref aliases)) => { - let mut alias_parsed = Vec::with_capacity(aliases.len()); - for a in aliases { - let a = a.as_str().map(ToOwned::to_owned)?; - alias_parsed.push(a); - } - Some(alias_parsed) - } - _ => None, - } -} - -pub(crate) fn parse_default( - default_value: &JsonValue, - schema_variant: &Variant, -) -> Result { - match (default_value, schema_variant) { - (d, Variant::Union { variants }) => { - let first_variant = variants.first().ok_or(AvrowErr::FailedDefaultUnion)?; - parse_default(d, first_variant) - } - (JsonValue::Null, Variant::Null) => Ok(Value::Null), - (JsonValue::Bool(v), Variant::Boolean) => Ok(Value::Boolean(*v)), - (JsonValue::Number(n), Variant::Int) => Ok(Value::Int(n.as_i64().unwrap() as i32)), - (JsonValue::Number(n), Variant::Long) => Ok(Value::Long(n.as_i64().unwrap())), - (JsonValue::Number(n), Variant::Float) => Ok(Value::Float(n.as_f64().unwrap() as f32)), - (JsonValue::Number(n), Variant::Double) => Ok(Value::Double(n.as_f64().unwrap() as f64)), - (JsonValue::String(n), Variant::Bytes) => Ok(Value::Bytes(n.as_bytes().to_vec())), - (JsonValue::String(n), Variant::Str) => Ok(Value::Str(n.clone())), - (JsonValue::Object(v), Variant::Record { name, fields, .. }) => { - let mut values = IndexMap::with_capacity(v.len()); - - for (k, v) in v { - let parsed_value = - parse_default(v, &fields.get(k).ok_or(AvrowErr::DefaultValueParse)?.ty)?; - values.insert(k.to_string(), FieldValue::new(parsed_value)); - } - - Ok(Value::Record(crate::value::Record { - fields: values, - name: name.to_string(), - })) - } - (JsonValue::String(n), Variant::Enum { symbols, .. }) => { - if symbols.contains(n) { - Ok(Value::Str(n.clone())) - } else { - Err(AvrowErr::EnumSymbolNotPresent) - } - } - (JsonValue::Array(arr), Variant::Array { items }) => { - let mut default_arr_items: Vec = Vec::with_capacity(arr.len()); - for v in arr { - let parsed_default = parse_default(v, items); - default_arr_items.push(parsed_default?); - } - - Ok(Value::Array(default_arr_items)) - } - ( - JsonValue::Object(map), - Variant::Map { - values: values_schema, - }, - ) => { - let mut values = std::collections::HashMap::with_capacity(map.len()); - for (k, v) in map { - let parsed_value = parse_default(v, values_schema)?; - values.insert(k.to_string(), parsed_value); - } - - Ok(Value::Map(values)) - } - - (JsonValue::String(n), Variant::Fixed { .. }) => Ok(Value::Fixed(n.as_bytes().to_vec())), - (_d, _s) => Err(AvrowErr::DefaultValueParse), - } -} - -#[cfg(test)] -mod tests { - use crate::schema::common::Order; - use crate::schema::Field; - use crate::schema::Name; - use crate::schema::Variant; - use crate::Schema; - use crate::Value; - use indexmap::IndexMap; - use std::str::FromStr; - #[test] - fn schema_parse_default_values() { - let schema = Schema::from_str( - r##"{ - "type": "record", - "name": "Can", - "doc":"Represents a can data", - "namespace": "com.avrow", - "aliases": ["my_linked_list"], - "fields" : [ - { - "name": "next", - "type": ["null", "Can"] - }, - { - "name": "value", - "type": "long", - "default": 1, - "aliases": ["data"], - "order": "descending", - "doc": "This field holds the value of the linked list" - } - ] - }"##, - ) - .unwrap(); - - let mut fields = IndexMap::new(); - let f1 = Field::new( - "value", - Variant::Long, - Some(Value::Long(1)), - Order::Ascending, - None, - ) - .unwrap(); - let f2 = Field::new( - "next", - Variant::Union { - variants: vec![Variant::Null, Variant::Named("com.avrow.Can".to_string())], - }, - None, - Order::Ascending, - None, - ) - .unwrap(); - fields.insert("value".to_string(), f1); - fields.insert("next".to_string(), f2); - - let mut name = Name::new("Can").unwrap(); - name.set_namespace("com.avrow").unwrap(); - - let s = Variant::Record { - name, - aliases: Some(vec!["my_linked_list".to_string()]), - fields, - }; - - assert_eq!(&s, schema.variant()); - } - - #[test] - fn nested_record_fields_parses_properly_with_fullnames() { - let schema = Schema::from_str(r##"{ - "name": "longlist", - "namespace": "com.some", - "type":"record", - "fields": [ - {"name": "magic", "type": {"type": "fixed", "name": "magic", "size": 4, "namespace": "com.bar"} - }, - {"name": "inner_rec", "type": {"type": "record", "name": "inner_rec", "fields": [ - { - "name": "test", - "type": {"type": "fixed", "name":"hello", "size":5} - } - ]}} - ] - }"##).unwrap(); - - assert!(schema.cxt.cxt.contains_key("com.bar.magic")); - assert!(schema.cxt.cxt.contains_key("com.some.hello")); - assert!(schema.cxt.cxt.contains_key("com.some.longlist")); - assert!(schema.cxt.cxt.contains_key("com.some.inner_rec")); - } -} diff --git a/src/schema/tests.rs b/src/schema/tests.rs deleted file mode 100644 index a75484e..0000000 --- a/src/schema/tests.rs +++ /dev/null @@ -1,437 +0,0 @@ -use super::common::{Field, Name, Order}; -use super::{Schema, Variant}; -use indexmap::IndexMap; -use std::collections::HashMap; -use std::str::FromStr; - -fn primitive_schema_objects() -> HashMap<&'static str, Variant> { - let mut s = HashMap::new(); - s.insert(r##"{ "type": "null" }"##, Variant::Null); - s.insert(r##"{ "type": "boolean" }"##, Variant::Boolean); - s.insert(r##"{ "type": "int" }"##, Variant::Int); - s.insert(r##"{ "type": "long" }"##, Variant::Long); - s.insert(r##"{ "type": "float" }"##, Variant::Float); - s.insert(r##"{ "type": "double" }"##, Variant::Double); - s.insert(r##"{ "type": "bytes" }"##, Variant::Bytes); - s.insert(r##"{ "type": "string" }"##, Variant::Str); - s -} - -fn primitive_schema_canonical() -> HashMap<&'static str, Variant> { - let mut s = HashMap::new(); - s.insert(r##""null""##, Variant::Null); - s.insert(r##""boolean""##, Variant::Boolean); - s.insert(r##""int""##, Variant::Int); - s.insert(r##""long""##, Variant::Long); - s.insert(r##""float""##, Variant::Float); - s.insert(r##""double""##, Variant::Double); - s.insert(r##""bytes""##, Variant::Bytes); - s.insert(r##""string""##, Variant::Str); - s -} - -#[test] -fn parse_primitives_as_json_objects() { - for (s, v) in primitive_schema_objects() { - let schema = Schema::from_str(s).unwrap(); - assert_eq!(schema.variant, v); - } -} - -#[test] -fn parse_primitives_as_defined_types() { - for (s, v) in primitive_schema_canonical() { - let schema = Schema::from_str(s).unwrap(); - assert_eq!(schema.variant, v); - } -} - -#[test] -fn parse_record() { - let record_schema = Schema::from_str( - r##"{ - "type": "record", - "name": "LongOrNull", - "namespace":"com.test", - "aliases": ["MaybeLong"], - "fields" : [ - {"name": "value", "type": "long"}, - {"name": "other", "type": ["null", "LongOrNull"]} - ] - }"##, - ) - .unwrap(); - - let union_variants = vec![ - Variant::Null, - Variant::Named("com.test.LongOrNull".to_string()), - ]; - - let mut fields_map = IndexMap::new(); - fields_map.insert( - "value".to_string(), - Field::new("value", Variant::Long, None, Order::Ascending, None).unwrap(), - ); - fields_map.insert( - "other".to_string(), - Field::new( - "other", - Variant::Union { - variants: union_variants, - }, - None, - Order::Ascending, - None, - ) - .unwrap(), - ); - - let mut name = Name::new("LongOrNull").unwrap(); - name.set_namespace("com.test").unwrap(); - - assert_eq!( - record_schema.variant, - Variant::Record { - name, - aliases: Some(vec!["MaybeLong".to_string()]), - fields: fields_map, - } - ); -} - -#[test] -fn parse_fixed() { - let fixed_schema = - Schema::from_str(r##"{"type": "fixed", "size": 16, "name": "md5"}"##).unwrap(); - assert_eq!( - fixed_schema.variant, - Variant::Fixed { - name: Name::new("md5").unwrap(), - size: 16 - } - ); -} - -#[test] -fn parse_enum() { - let json = r##"{ - "type": "enum", - "name": "Suit", - "symbols" : ["SPADES", "HEARTS", "DIAMONDS", "CLUBS"] - }"##; - let enum_schema = Schema::from_str(json).unwrap(); - let name = Name::new("Suit").unwrap(); - let mut symbols = vec![]; - symbols.push("SPADES".to_owned()); - symbols.push("HEARTS".to_owned()); - symbols.push("DIAMONDS".to_owned()); - symbols.push("CLUBS".to_owned()); - - assert_eq!( - enum_schema.variant, - Variant::Enum { - name, - aliases: None, - symbols - } - ); -} - -#[test] -fn parse_array() { - let json = r##"{"type": "array", "items": "string"}"##; - let array_schema = Schema::from_str(json).unwrap(); - assert_eq!( - array_schema.variant, - Variant::Array { - items: Box::new(Variant::Str) - } - ); -} - -#[test] -fn parse_map() { - let map_schema = Schema::from_str(r##"{"type": "map", "values": "long"}"##).unwrap(); - assert_eq!( - map_schema.variant, - Variant::Map { - values: Box::new(Variant::Long) - } - ); -} - -/////////////////////////////////////////////////////////////////////////////// -/// Union -/////////////////////////////////////////////////////////////////////////////// - -#[test] -fn parse_simple_union() { - let union_schema = Schema::from_str(r##"["null", "string"]"##).unwrap(); - assert_eq!( - union_schema.variant, - Variant::Union { - variants: vec![Variant::Null, Variant::Str] - } - ); -} - -#[test] -#[should_panic] -fn parse_union_duplicate_primitive_fails() { - let mut results = vec![]; - for i in primitive_schema_canonical() { - let json = &format!("[{}, {}]", i.0, i.0); - results.push(Schema::from_str(json).is_err()); - } - - assert!(results.iter().any(|a| !(*a))); -} - -#[test] -fn parse_union_with_different_named_type_but_same_schema_succeeds() { - let union_schema = Schema::from_str( - r##"[ - { - "type":"record", - "name": "record_one", - "fields" : [ - {"name": "value", "type": "long"} - ] - }, - { - "type":"record", - "name": "record_two", - "fields" : [ - {"name": "value", "type": "long"} - ] - }]"##, - ); - - assert!(union_schema.is_ok()); -} - -#[test] -fn parse_union_with_same_named_type_fails() { - let union_schema = Schema::from_str( - r##"[ - { - "type":"record", - "name": "record_one", - "fields" : [ - {"name": "value", "type": "long"} - ] - }, - { - "type":"record", - "name": "record_one", - "fields" : [ - {"name": "value", "type": "long"} - ] - }]"##, - ); - - assert!(union_schema.is_err()); -} - -#[test] -fn parse_union_field_invalid_default_values() { - let default_valued_schema = Schema::from_str( - r##" - { - "name": "Company", - "type": "record", - "fields": [ - { - "name": "emp_name", - "type": "string", - "doc": "employee name" - }, - { - "name": "bonus", - "type": ["null", "long"], - "default": null, - "doc": "bonus received on a yearly basis" - }, - { - "name": "subordinates", - "type": ["null", {"type": "map", "values": "string"}], - "default": {"foo":"bar"}, - "doc": "map of subordinates Name and Designation" - }, - { - "name": "departments", - "type":["null", {"type":"array", "items":"string" }], - "default": ["Sam", "Bob"], - "doc": "Departments under the employee" - } - ] - } - "##, - ); - - assert!(default_valued_schema.is_err()); -} - -#[test] -fn parse_default_values_record() { - let default_valued_schema = Schema::from_str( - r##" - { - "name": "Company", - "type": "record", - "namespace": "com.test.avrow", - "fields": [ - { - "name": "bonus", - "type": ["null", "long"], - "default": null, - "doc": "bonus received on a yearly basis" - } - ] - } - "##, - ); - - assert!(default_valued_schema.is_ok()); -} - -#[test] -#[should_panic(expected = "DuplicateSchema")] -fn fails_on_duplicate_schema() { - let schema = r##"{ - "type": "record", - "namespace": "test.avro.training", - "name": "SomeMessage", - "fields": [{ - "name": "is_error", - "type": "boolean", - "default": false - }, { - "name": "outcome", - "type": [{ - "type": "record", - "name": "SomeMessage", - "fields": [] - }, { - "type": "record", - "name": "ErrorRecord", - "fields": [{ - "name": "errors", - "type": { - "type": "map", - "values": "string" - }, - "doc": "doc" - }] - }] - }] - }"##; - - Schema::from_str(schema).unwrap(); -} - -#[test] -#[should_panic] -fn parse_immediate_unions_fails() { - let default_valued_schema = Schema::from_str( - r##" - ["null", "string", ["null", "int"]]"##, - ); - - assert!(default_valued_schema.is_ok()); -} - -#[test] -fn parse_simple_default_values_record() { - let _default_valued_schema = Schema::from_str( - r##" - { - "name": "com.school.Student", - "type": "record", - "fields": [ - { - "name": "departments", - "type":[{"type":"array", "items":"string" }, "null"], - "default": ["Computer Science", "Finearts"], - "doc": "Departments of a student" - } - ] - } - "##, - ) - .unwrap(); -} - -#[test] -fn parse_default_record_value_in_union() { - let schema = Schema::from_str( - r##" - { - "name": "com.big.data.avro.schema.Employee", - "type": "record", - "fields": [ - { - "name": "departments", - "type":[ - {"type":"record", - "name": "dept_name", - "fields":[{"name":"id","type": "string"}, {"name":"foo", "type": "null"}] }], - "default": {"id": "foo", "foo": null} - } - ] - } - "##, - ) - .unwrap(); - - if let Variant::Record { fields, .. } = schema.variant { - match &fields["departments"].default { - Some(crate::Value::Record(r)) => { - assert!(r.fields.contains_key("id")); - assert_eq!( - r.fields["id"], - crate::value::FieldValue::new(crate::Value::Str("foo".to_string())) - ); - } - _ => panic!("should be a record"), - } - } -} - -#[test] -#[should_panic(expected = "must be defined before use")] -fn named_schema_must_be_defined_before_being_used() { - let _schema = Schema::from_str( - r##"{ - "type": "record", - "name": "LongList", - "aliases": ["LinkedLongs"], - "fields" : [ - {"name": "value", "type": "long"}, - {"name": "next", "type": ["null", "OtherList"]} - ] - }"##, - ) - .unwrap(); -} - -#[test] -fn test_two_instance_schema_equality() { - let raw_schema = r#" - { - "type": "record", - "name": "User", - "doc": "Hi there.", - "fields": [ - {"name": "likes_pizza", "type": "boolean", "default": false}, - {"name": "aa-i32", - "type": {"type": "array", "items": {"type": "array", "items": "int"}}, - "default": [[0], [12, -1]]} - ] - } - "#; - - let schema = Schema::from_str(raw_schema).unwrap(); - let schema2 = Schema::from_str(raw_schema).unwrap(); - assert_eq!(schema, schema2); -} diff --git a/src/serde_avro/de.rs b/src/serde_avro/de.rs deleted file mode 100644 index fec2a41..0000000 --- a/src/serde_avro/de.rs +++ /dev/null @@ -1,170 +0,0 @@ -use super::de_impl::{ArrayDeserializer, ByteSeqDeserializer, MapDeserializer, StructReader}; -use crate::error::AvrowErr; - -use crate::value::Value; - -use serde::de::IntoDeserializer; -use serde::de::{self, Visitor}; -use serde::forward_to_deserialize_any; - -pub(crate) struct SerdeReader<'de> { - pub(crate) inner: &'de Value, -} - -impl<'de> SerdeReader<'de> { - pub(crate) fn new(inner: &'de Value) -> Self { - SerdeReader { inner } - } -} - -impl<'de, 'a> de::Deserializer<'de> for &'a mut SerdeReader<'de> { - type Error = AvrowErr; - - fn deserialize_any(self, visitor: V) -> Result - where - V: Visitor<'de>, - { - match self.inner { - Value::Null => visitor.visit_unit(), - Value::Boolean(v) => visitor.visit_bool(*v), - Value::Int(v) => visitor.visit_i32(*v), - Value::Long(v) => visitor.visit_i64(*v), - Value::Float(v) => visitor.visit_f32(*v), - Value::Double(v) => visitor.visit_f64(*v), - Value::Str(ref v) => visitor.visit_borrowed_str(v), - Value::Bytes(ref bytes) => visitor.visit_borrowed_bytes(&bytes), - Value::Array(items) => visitor.visit_seq(ArrayDeserializer::new(&items)), - Value::Enum(s) => visitor.visit_enum(s.as_str().into_deserializer()), - _ => Err(AvrowErr::Unsupported), - } - } - - forward_to_deserialize_any! { - unit bool u8 i8 i16 i32 i64 u16 u32 u64 f32 f64 str bytes byte_buf string ignored_any enum - } - - fn deserialize_option(self, visitor: V) -> Result - where - V: Visitor<'de>, - { - visitor.visit_some(self) - } - - fn deserialize_unit_struct( - self, - _name: &'static str, - visitor: V, - ) -> Result - where - V: Visitor<'de>, - { - visitor.visit_unit() - } - - fn deserialize_seq(self, visitor: V) -> Result - where - V: Visitor<'de>, - { - match self.inner { - Value::Array(ref items) => visitor.visit_seq(ArrayDeserializer::new(items)), - // TODO figure out the correct byte stram to use - Value::Bytes(buf) | Value::Fixed(buf) => { - let byte_seq_deser = ByteSeqDeserializer { input: buf.iter() }; - visitor.visit_seq(byte_seq_deser) - } - Value::Union(v) => match v.as_ref() { - Value::Array(ref items) => visitor.visit_seq(ArrayDeserializer::new(items)), - _ => Err(AvrowErr::Unsupported), - }, - _ => Err(AvrowErr::Unsupported), - } - } - - // avro bytes - fn deserialize_tuple(self, _len: usize, visitor: V) -> Result - where - V: serde::de::Visitor<'de>, - { - self.deserialize_seq(visitor) - } - - // for struct field - fn deserialize_identifier(self, visitor: V) -> Result - where - V: Visitor<'de>, - { - self.deserialize_str(visitor) - } - - fn deserialize_map(self, visitor: V) -> Result - where - V: Visitor<'de>, - { - match self.inner { - Value::Map(m) => { - let map_de = MapDeserializer { - keys: m.keys(), - values: m.values(), - }; - visitor.visit_map(map_de) - } - v => Err(AvrowErr::UnexpectedAvroValue { - value: format!("{:?}", v), - }), - } - } - - fn deserialize_struct( - self, - _a: &'static str, - _b: &'static [&'static str], - visitor: V, - ) -> Result - where - V: Visitor<'de>, - { - match self.inner { - Value::Record(ref r) => visitor.visit_map(StructReader::new(r.fields.iter())), - Value::Union(ref inner) => match **inner { - Value::Record(ref rec) => visitor.visit_map(StructReader::new(rec.fields.iter())), - _ => Err(de::Error::custom("Union variant not a record/struct")), - }, - _ => Err(de::Error::custom("Must be a record/struct")), - } - } - - /////////////////////////////////////////////////////////////////////////// - /// Not yet supported types - /////////////////////////////////////////////////////////////////////////// - - fn deserialize_tuple_struct( - self, - _name: &'static str, - _len: usize, - _visitor: V, - ) -> Result - where - V: Visitor<'de>, - { - // TODO it is not clear to what avro schema can a tuple map to - Err(AvrowErr::Unsupported) - } - - fn deserialize_newtype_struct( - self, - _name: &'static str, - _visitor: V, - ) -> Result - where - V: Visitor<'de>, - { - Err(AvrowErr::Unsupported) - } - - fn deserialize_char(self, _visitor: V) -> Result - where - V: Visitor<'de>, - { - Err(AvrowErr::Unsupported) - } -} diff --git a/src/serde_avro/de_impl.rs b/src/serde_avro/de_impl.rs deleted file mode 100644 index eb47bba..0000000 --- a/src/serde_avro/de_impl.rs +++ /dev/null @@ -1,193 +0,0 @@ -use super::de::SerdeReader; -use crate::error::AvrowErr; -use crate::value::FieldValue; -use crate::Value; -use indexmap::map::Iter as MapIter; -use serde::de; -use serde::de::DeserializeSeed; -use serde::de::Visitor; -use serde::forward_to_deserialize_any; -use std::collections::hash_map::Keys; -use std::collections::hash_map::Values; -use std::slice::Iter; - -pub(crate) struct StructReader<'de> { - input: MapIter<'de, String, FieldValue>, - value: Option<&'de FieldValue>, -} - -impl<'de> StructReader<'de> { - pub fn new(input: MapIter<'de, String, FieldValue>) -> Self { - StructReader { input, value: None } - } -} - -impl<'de> de::MapAccess<'de> for StructReader<'de> { - type Error = AvrowErr; - - fn next_key_seed(&mut self, seed: K) -> Result, Self::Error> - where - K: DeserializeSeed<'de>, - { - match self.input.next() { - Some(item) => { - let (ref field, ref value) = item; - self.value = Some(value); - seed.deserialize(StrDeserializer { input: &field }) - .map(Some) - } - None => Ok(None), - } - } - - fn next_value_seed(&mut self, seed: V) -> Result - where - V: DeserializeSeed<'de>, - { - let a = self.value.take(); - if let Some(a) = a { - match &a.value { - Value::Null => seed.deserialize(NullDeserializer), - value => seed.deserialize(&mut SerdeReader { inner: &value }), - } - } else { - Err(de::Error::custom("Unexpected call to next_value_seed.")) - } - } -} - -pub(crate) struct ArrayDeserializer<'de> { - input: Iter<'de, Value>, -} - -impl<'de> ArrayDeserializer<'de> { - pub fn new(input: &'de [Value]) -> Self { - Self { - input: input.iter(), - } - } -} - -impl<'de> de::SeqAccess<'de> for ArrayDeserializer<'de> { - type Error = AvrowErr; - - fn next_element_seed(&mut self, seed: T) -> Result, Self::Error> - where - T: DeserializeSeed<'de>, - { - match self.input.next() { - Some(item) => seed.deserialize(&mut SerdeReader::new(item)).map(Some), - None => Ok(None), - } - } -} - -pub(crate) struct ByteSeqDeserializer<'de> { - pub(crate) input: Iter<'de, u8>, -} - -impl<'de> de::SeqAccess<'de> for ByteSeqDeserializer<'de> { - type Error = AvrowErr; - - fn next_element_seed(&mut self, seed: T) -> Result, Self::Error> - where - T: DeserializeSeed<'de>, - { - match self.input.next() { - Some(item) => seed.deserialize(ByteDeserializer { byte: item }).map(Some), - None => Ok(None), - } - } -} - -pub(crate) struct ByteDeserializer<'de> { - pub(crate) byte: &'de u8, -} - -impl<'de> de::Deserializer<'de> for ByteDeserializer<'de> { - type Error = AvrowErr; - - fn deserialize_any(self, visitor: V) -> Result - where - V: Visitor<'de>, - { - visitor.visit_u8(*self.byte) - } - - forward_to_deserialize_any! { - bool u8 u16 u32 u64 i8 i16 i32 i64 f32 f64 char str string unit option - seq bytes byte_buf map unit_struct newtype_struct - tuple_struct struct tuple enum identifier ignored_any - } -} - -pub(crate) struct MapDeserializer<'de> { - pub(crate) keys: Keys<'de, String, Value>, - pub(crate) values: Values<'de, String, Value>, -} - -impl<'de> de::MapAccess<'de> for MapDeserializer<'de> { - type Error = AvrowErr; - - fn next_key_seed(&mut self, seed: K) -> Result, Self::Error> - where - K: DeserializeSeed<'de>, - { - match self.keys.next() { - Some(key) => seed.deserialize(StrDeserializer { input: key }).map(Some), - None => Ok(None), - } - } - - fn next_value_seed(&mut self, seed: V) -> Result - where - V: DeserializeSeed<'de>, - { - match self.values.next() { - Some(value) => seed.deserialize(&mut SerdeReader::new(value)), - None => Err(Self::Error::Message( - "Unexpected call to next_value_seed".to_string(), - )), - } - } -} - -pub(crate) struct StrDeserializer<'de> { - input: &'de str, -} - -impl<'de> de::Deserializer<'de> for StrDeserializer<'de> { - type Error = AvrowErr; - - fn deserialize_any(self, visitor: V) -> Result - where - V: Visitor<'de>, - { - visitor.visit_borrowed_str(&self.input) - } - - forward_to_deserialize_any! { - bool u8 u16 u32 u64 i8 i16 i32 i64 f32 f64 char str string unit option - seq bytes byte_buf map unit_struct newtype_struct - tuple_struct struct tuple enum identifier ignored_any - } -} - -pub(crate) struct NullDeserializer; - -impl<'de> de::Deserializer<'de> for NullDeserializer { - type Error = AvrowErr; - - fn deserialize_any(self, visitor: V) -> Result - where - V: Visitor<'de>, - { - visitor.visit_none() - } - - forward_to_deserialize_any! { - bool u8 u16 u32 u64 i8 i16 i32 i64 f32 f64 char str string unit option - seq bytes byte_buf map unit_struct newtype_struct - tuple_struct struct tuple enum identifier ignored_any - } -} diff --git a/src/serde_avro/mod.rs b/src/serde_avro/mod.rs deleted file mode 100644 index af2f22b..0000000 --- a/src/serde_avro/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -mod de; -mod de_impl; -mod ser; -mod ser_impl; - -pub(crate) use self::de::SerdeReader; -pub use self::ser::{to_value, SerdeWriter}; -pub use crate::error::AvrowErr; diff --git a/src/serde_avro/ser.rs b/src/serde_avro/ser.rs deleted file mode 100644 index 359dc9e..0000000 --- a/src/serde_avro/ser.rs +++ /dev/null @@ -1,261 +0,0 @@ -use super::ser_impl::{MapSerializer, SeqSerializer, StructSerializer}; -use crate::error::AvrowErr; -use crate::value::Value; -use serde::ser::{self, Serialize}; - -pub struct SerdeWriter; - -/// `to_value` is the serde API for serialization of Rust types to an [avrow::Value](enum.Value.html) -pub fn to_value(value: &T) -> Result -where - T: Serialize, -{ - let mut serializer = SerdeWriter; - value.serialize(&mut serializer) -} - -impl<'b> ser::Serializer for &'b mut SerdeWriter { - type Ok = Value; - type Error = AvrowErr; - type SerializeSeq = SeqSerializer; - type SerializeMap = MapSerializer; - type SerializeStruct = StructSerializer; - type SerializeTuple = SeqSerializer; - type SerializeTupleStruct = Unsupported; - type SerializeTupleVariant = Unsupported; - type SerializeStructVariant = Unsupported; - - fn serialize_bool(self, v: bool) -> Result { - Ok(Value::Boolean(v)) - } - - fn serialize_i8(self, v: i8) -> Result { - Ok(Value::Byte(v as u8)) - } - - fn serialize_i16(self, v: i16) -> Result { - Ok(Value::Int(v as i32)) - } - - fn serialize_i32(self, v: i32) -> Result { - Ok(Value::Int(v as i32)) - } - - fn serialize_i64(self, v: i64) -> Result { - Ok(Value::Long(v)) - } - - fn serialize_u8(self, v: u8) -> Result { - // using the auxiliary avro value - Ok(Value::Byte(v)) - } - - fn serialize_u16(self, v: u16) -> Result { - Ok(Value::Int(v as i32)) - } - - fn serialize_u32(self, v: u32) -> Result { - Ok(Value::Int(v as i32)) - } - - fn serialize_u64(self, v: u64) -> Result { - Ok(Value::Long(v as i64)) - } - - fn serialize_f32(self, v: f32) -> Result { - Ok(Value::Float(v)) - } - - fn serialize_f64(self, v: f64) -> Result { - Ok(Value::Double(v)) - } - - fn serialize_char(self, v: char) -> Result { - Ok(Value::Str(v.to_string())) - } - - fn serialize_str(self, v: &str) -> Result { - Ok(Value::Str(v.to_owned())) - } - - fn serialize_bytes(self, v: &[u8]) -> Result { - // todo: identify call path to this - Ok(Value::Bytes(v.to_owned())) - } - - fn serialize_none(self) -> Result { - Ok(Value::Null) - } - - fn serialize_some(self, value: &T) -> Result - where - T: Serialize, - { - Ok(value.serialize(&mut SerdeWriter)?) - } - - fn serialize_unit(self) -> Result { - Ok(Value::Null) - } - - fn serialize_unit_struct(self, _: &'static str) -> Result { - self.serialize_unit() - } - - fn serialize_unit_variant( - self, - _name: &'static str, - _index: u32, - variant: &'static str, - ) -> Result { - Ok(Value::Enum(variant.to_string())) - } - - fn serialize_newtype_struct( - self, - _: &'static str, - value: &T, - ) -> Result - where - T: Serialize, - { - value.serialize(self) - } - - fn serialize_seq(self, len: Option) -> Result { - Ok(SeqSerializer::new(len)) - } - - fn serialize_map(self, len: Option) -> Result { - Ok(MapSerializer::new(len)) - } - - fn serialize_struct( - self, - name: &'static str, - len: usize, - ) -> Result { - Ok(StructSerializer::new(name, len)) - } - - fn serialize_tuple(self, _len: usize) -> Result { - self.serialize_seq(Some(_len)) - } - - fn serialize_tuple_struct( - self, - _: &'static str, - _len: usize, - ) -> Result { - unimplemented!("Avro does not support Rust tuple structs"); - } - - fn serialize_tuple_variant( - self, - _: &'static str, - _: u32, - _: &'static str, - _: usize, - ) -> Result { - // TODO Is there a way we can map union type to some valid avro type - Err(AvrowErr::Message( - "Tuple type is not currently supported as per avro spec".to_string(), - )) - } - - fn serialize_struct_variant( - self, - _: &'static str, - _: u32, - _: &'static str, - _: usize, - ) -> Result { - unimplemented!("Avro enums does not support struct variants in enum") - } - - fn serialize_newtype_variant( - self, - _: &'static str, - _: u32, - _: &'static str, - _value: &T, - ) -> Result - where - T: Serialize, - { - unimplemented!("Avro does not support newtype struct variants in enums"); - } -} - -/////////////////////////////////////////////////////////////////////////////// -/// Unsupported types in avro -/////////////////////////////////////////////////////////////////////////////// - -pub struct Unsupported; - -// struct enum variant -impl ser::SerializeStructVariant for Unsupported { - type Ok = Value; - type Error = AvrowErr; - - fn serialize_field(&mut self, _: &'static str, _: &T) -> Result<(), Self::Error> - where - T: Serialize, - { - unimplemented!("Avro enums does not support data in its variant") - } - - fn end(self) -> Result { - unimplemented!("Avro enums does not support data in its variant") - } -} - -// tuple enum variant -impl ser::SerializeTupleVariant for Unsupported { - type Ok = Value; - type Error = AvrowErr; - - fn serialize_field(&mut self, _: &T) -> Result<(), Self::Error> - where - T: Serialize, - { - unimplemented!("Avro enums does not support Rust tuple variants in enums") - } - - fn end(self) -> Result { - unimplemented!("Avro enums does not support Rust tuple variant in enums") - } -} - -// TODO maybe we can map it by looking at the schema -impl ser::SerializeTupleStruct for Unsupported { - type Ok = Value; - type Error = AvrowErr; - - fn serialize_field(&mut self, _value: &T) -> Result<(), Self::Error> - where - T: Serialize, - { - unimplemented!("Avro enums does not support Rust tuple struct") - } - - fn end(self) -> Result { - unimplemented!("Avro enums does not support Rust tuple struct") - } -} - -impl<'a> ser::SerializeTuple for Unsupported { - type Ok = Value; - type Error = AvrowErr; - - fn serialize_element(&mut self, _value: &T) -> Result<(), Self::Error> - where - T: Serialize, - { - unimplemented!("Avro enums does not support Rust tuples") - } - - fn end(self) -> Result { - unimplemented!("Avro enums does not support Rust tuples") - } -} diff --git a/src/serde_avro/ser_impl.rs b/src/serde_avro/ser_impl.rs deleted file mode 100644 index c8e9c78..0000000 --- a/src/serde_avro/ser_impl.rs +++ /dev/null @@ -1,195 +0,0 @@ -use super::SerdeWriter; -use crate::error::AvrowErr; -use crate::value::FieldValue; -use crate::value::Record; -use crate::Value; -use serde::Serialize; -use std::collections::HashMap; - -pub struct MapSerializer { - map: HashMap, -} - -impl MapSerializer { - pub fn new(len: Option) -> Self { - let map = match len { - Some(len) => HashMap::with_capacity(len), - None => HashMap::new(), - }; - - MapSerializer { map } - } -} - -impl serde::ser::SerializeMap for MapSerializer { - type Ok = Value; - type Error = AvrowErr; - - fn serialize_entry( - &mut self, - key: &K, - value: &V, - ) -> Result<(), Self::Error> - where - K: Serialize, - V: Serialize, - { - let key = key.serialize(&mut SerdeWriter)?; - if let Value::Str(s) = key { - let value = value.serialize(&mut SerdeWriter)?; - self.map.insert(s, value); - Ok(()) - } else { - Err(AvrowErr::ExpectedString) - } - } - - fn serialize_key(&mut self, _key: &T) -> Result<(), Self::Error> - where - T: Serialize, - { - Ok(()) - } - - fn serialize_value(&mut self, _value: &T) -> Result<(), Self::Error> - where - T: Serialize, - { - Ok(()) - } - - fn end(self) -> Result { - Ok(Value::Map(self.map)) - } -} - -////////////////////////////////////////////////////////////////////////////// -/// Rust structs to avro record -////////////////////////////////////////////////////////////////////////////// -pub struct StructSerializer { - name: String, - fields: indexmap::IndexMap, -} - -impl StructSerializer { - pub fn new(name: &str, len: usize) -> StructSerializer { - StructSerializer { - name: name.to_string(), - fields: indexmap::IndexMap::with_capacity(len), - } - } -} - -impl serde::ser::SerializeStruct for StructSerializer { - type Ok = Value; - type Error = AvrowErr; - - fn serialize_field( - &mut self, - name: &'static str, - value: &T, - ) -> Result<(), Self::Error> - where - T: Serialize, - { - self.fields.insert( - name.to_owned(), - FieldValue::new(value.serialize(&mut SerdeWriter)?), - ); - Ok(()) - } - - fn end(self) -> Result { - let record = Record { - name: self.name, - fields: self.fields, - }; - Ok(Value::Record(record)) - } -} - -////////////////////////////////////////////////////////////////////////////// -/// Sequences -////////////////////////////////////////////////////////////////////////////// - -pub struct SeqSerializer { - items: Vec, -} - -impl SeqSerializer { - pub fn new(len: Option) -> SeqSerializer { - let items = match len { - Some(len) => Vec::with_capacity(len), - None => Vec::new(), - }; - - SeqSerializer { items } - } -} - -// Helper function to extract a Vec from a Vec -// This should only be called by the caller who knows that the items -// in the Vec a Value::Byte(u8). -// NOTE: Does collect on an into_iter() allocate a new vec? -fn as_byte_vec(a: Vec) -> Vec { - a.into_iter() - .map(|v| { - if let Value::Byte(b) = v { - b - } else { - unreachable!("Expecting a byte value in the Vec") - } - }) - .collect() -} - -impl<'a> serde::ser::SerializeSeq for SeqSerializer { - type Ok = Value; - type Error = AvrowErr; - - fn serialize_element(&mut self, value: &T) -> Result<(), Self::Error> - where - T: Serialize, - { - let v = value.serialize(&mut SerdeWriter)?; - self.items.push(v); - Ok(()) - } - - // If the items in vec are of Value::Byte(u8) then return a byte array. - // FIXME: maybe implement Serialize directly for Vec to avoid this way. - fn end(self) -> Result { - match self.items.first() { - Some(Value::Byte(_)) => Ok(Value::Bytes(as_byte_vec(self.items))), - _ => Ok(Value::Array(self.items)), - } - } -} - -////////////////////////////////////////////////////////////////////////////// -/// Tuples: avro bytes, fixed -////////////////////////////////////////////////////////////////////////////// - -impl<'a> serde::ser::SerializeTuple for SeqSerializer { - type Ok = Value; - type Error = AvrowErr; - - fn serialize_element(&mut self, value: &T) -> Result<(), Self::Error> - where - T: Serialize, - { - let v = value.serialize(&mut SerdeWriter)?; - self.items.push(v); - Ok(()) - } - - // If the items in vec are of Value::Byte(u8) then return a byte array. - // FIXME: maybe implement Serialize directly for Vec to avoid this way. - fn end(self) -> Result { - match self.items.first() { - Some(Value::Byte(_)) => Ok(Value::Bytes(as_byte_vec(self.items))), - Some(Value::Fixed(_)) => Ok(Value::Fixed(as_byte_vec(self.items))), - _ => Ok(Value::Array(self.items)), - } - } -} diff --git a/src/util.rs b/src/util.rs deleted file mode 100644 index 4306105..0000000 --- a/src/util.rs +++ /dev/null @@ -1,34 +0,0 @@ -use crate::error::AvrowErr; -use integer_encoding::VarIntReader; -use integer_encoding::VarIntWriter; -use std::io::{Error, ErrorKind, Read, Write}; -use std::str; - -pub(crate) fn decode_string(reader: &mut R) -> Result { - let buf = decode_bytes(reader)?; - let s = str::from_utf8(&buf).map_err(|_e| { - let err = Error::new(ErrorKind::InvalidData, "Failed decoding string from bytes"); - AvrowErr::DecodeFailed(err) - })?; - Ok(s.to_string()) -} - -pub(crate) fn decode_bytes(reader: &mut R) -> Result, AvrowErr> { - let len: i64 = reader.read_varint().map_err(AvrowErr::DecodeFailed)?; - let mut byte_buf = vec![0u8; len as usize]; - reader - .read_exact(&mut byte_buf) - .map_err(AvrowErr::DecodeFailed)?; - Ok(byte_buf) -} - -pub fn encode_long(value: i64, writer: &mut W) -> Result { - writer.write_varint(value).map_err(AvrowErr::EncodeFailed) -} - -pub fn encode_raw_bytes(value: &[u8], writer: &mut W) -> Result<(), AvrowErr> { - writer - .write(value) - .map_err(AvrowErr::EncodeFailed) - .map(|_| ()) -} diff --git a/src/value.rs b/src/value.rs deleted file mode 100644 index 90b9d79..0000000 --- a/src/value.rs +++ /dev/null @@ -1,710 +0,0 @@ -//! Represents the types that - -use crate::error::AvrowErr; -use crate::schema; -use crate::schema::common::validate_name; -use crate::schema::Registry; -use crate::util::{encode_long, encode_raw_bytes}; -use crate::Schema; -use byteorder::LittleEndian; -use byteorder::WriteBytesExt; -use indexmap::IndexMap; -use integer_encoding::VarIntWriter; -use schema::Order; -use schema::Variant; -use serde::Serialize; -use std::collections::{BTreeMap, HashMap}; -use std::fmt::Display; -use std::io::Write; - -// Convenient type alias for map initialzation. -pub type Map = HashMap; - -#[derive(Debug, Clone, PartialEq, Serialize)] -pub(crate) struct FieldValue { - pub(crate) value: Value, - #[serde(skip_serializing)] - order: schema::Order, -} - -impl FieldValue { - pub(crate) fn new(value: Value) -> Self { - FieldValue { - value, - order: Order::Ascending, - } - } -} - -#[derive(Debug, Clone, PartialEq, Serialize)] -/// The [record](https://avro.apache.org/docs/current/spec.html#schema_record) avro type -pub struct Record { - pub(crate) name: String, - pub(crate) fields: IndexMap, -} - -impl Record { - /// Creates a new avro record type with the given name. - pub fn new(name: &str) -> Self { - Record { - fields: IndexMap::new(), - name: name.to_string(), - } - } - - /// Adds a field to the record. - pub fn insert>(&mut self, field_name: &str, ty: T) -> Result<(), AvrowErr> { - validate_name(0, field_name)?; - self.fields - .insert(field_name.to_string(), FieldValue::new(ty.into())); - Ok(()) - } - - /// Sets the ordering of the field. - pub fn set_field_order(&mut self, field_name: &str, order: Order) -> Result<(), AvrowErr> { - let a = self - .fields - .get_mut(field_name) - .ok_or(AvrowErr::FieldNotFound)?; - a.order = order; - Ok(()) - } - - /// Creates a record from a [BTreeMap](https://doc.rust-lang.org/std/collections/struct.BTreeMap.html) by consuming it. - /// The values in btree must implement Into. The name provided must match with the name in the record - /// schema being provided to the writer. - pub fn from_btree + Ord + Display, V: Into>( - name: &str, - btree: BTreeMap, - ) -> Result { - let mut record = Record::new(name); - for (k, v) in btree { - let field_value = FieldValue { - value: v.into(), - order: Order::Ascending, - }; - record.fields.insert(k.to_string(), field_value); - } - - Ok(record) - } - - /// Creates a record from a json object. A confirming record schema must be provided. - pub fn from_json( - json: serde_json::Map, - schema: &Schema, - ) -> Result { - // let variant = schema.variant; - if let Variant::Record { name, fields, .. } = &schema.variant { - let mut values = IndexMap::new(); - for (k, v) in json { - let parsed_value = crate::schema::parser::parse_default( - &v, - &fields.get(&k).ok_or(AvrowErr::DefaultValueParse)?.ty, - )?; - values.insert(k.to_string(), FieldValue::new(parsed_value)); - } - - Ok(Value::Record(crate::value::Record { - fields: values, - name: name.fullname(), - })) - } else { - Err(AvrowErr::ExpectedJsonObject) - } - } -} - -// TODO: Avro sort order -// impl PartialOrd for Value { -// fn partial_cmp(&self, other: &Self) -> Option { -// match (self, other) { -// (Value::Null, Value::Null) => Some(Ordering::Equal), -// (Value::Boolean(self_v), Value::Boolean(other_v)) => { -// if self_v == other_v { -// return Some(Ordering::Equal); -// } -// if *self_v == false && *other_v { -// Some(Ordering::Less) -// } else { -// Some(Ordering::Greater) -// } -// } -// (Value::Int(self_v), Value::Int(other_v)) => Some(self_v.cmp(other_v)), -// (Value::Long(self_v), Value::Long(other_v)) => Some(self_v.cmp(other_v)), -// (Value::Float(self_v), Value::Float(other_v)) => self_v.partial_cmp(other_v), -// (Value::Double(self_v), Value::Double(other_v)) => self_v.partial_cmp(other_v), -// (Value::Bytes(self_v), Value::Bytes(other_v)) => self_v.partial_cmp(other_v), -// (Value::Byte(self_v), Value::Byte(other_v)) => self_v.partial_cmp(other_v), -// (Value::Fixed(self_v), Value::Fixed(other_v)) => self_v.partial_cmp(other_v), -// (Value::Str(self_v), Value::Str(other_v)) => self_v.partial_cmp(other_v), -// (Value::Array(self_v), Value::Array(other_v)) => self_v.partial_cmp(other_v), -// (Value::Enum(self_v), Value::Enum(other_v)) => self_v.partial_cmp(other_v), -// (Value::Record(_self_v), Value::Record(_other_v)) => todo!(), -// _ => todo!(), -// } -// } -// } - -/// Represents an Avro value -#[derive(Debug, Clone, PartialEq, Serialize)] -pub enum Value { - /// A null value. - Null, - /// An i32 integer value. - Int(i32), - /// An i64 long value. - Long(i64), - /// A boolean value. - Boolean(bool), - /// A f32 float value. - Float(f32), - /// A f64 float value. - Double(f64), - /// A Record value (BTreeMap). - Record(Record), - /// A Fixed value. - Fixed(Vec), - /// A Map value. - Map(Map), - /// A sequence of u8 bytes. - Bytes(Vec), - /// Rust strings map directly to avro strings - Str(String), - /// A union is a sequence of unique `Value`s - Union(Box), - /// An enumeration. Unlike Rust enums, enums in avro don't support data within their variants. - Enum(String), - /// An array of `Value`s - Array(Vec), - /// auxiliary u8 helper for serde. Not an avro value. - Byte(u8), -} - -impl Value { - pub(crate) fn encode( - &self, - writer: &mut W, - schema: &Variant, - cxt: &Registry, - ) -> Result<(), AvrowErr> { - match (self, schema) { - (Value::Null, Variant::Null) => {} - (Value::Boolean(b), Variant::Boolean) => writer - .write_all(&[*b as u8]) - .map_err(AvrowErr::EncodeFailed)?, - (Value::Int(i), Variant::Int) => { - writer.write_varint(*i).map_err(AvrowErr::EncodeFailed)?; - } - // int is promotable to long, float or double --- - (Value::Int(i), Variant::Long) => { - writer - .write_varint(*i as i64) - .map_err(AvrowErr::EncodeFailed)?; - } - (Value::Int(i), Variant::Float) => { - writer - .write_f32::(*i as f32) - .map_err(AvrowErr::EncodeFailed)?; - } - (Value::Int(i), Variant::Double) => { - writer - .write_f64::(*i as f64) - .map_err(AvrowErr::EncodeFailed)?; - } - // --- - (Value::Long(l), Variant::Long) => { - writer.write_varint(*l).map_err(AvrowErr::EncodeFailed)?; - } - (Value::Long(l), Variant::Float) => { - writer - .write_f32::(*l as f32) - .map_err(AvrowErr::EncodeFailed)?; - } - (Value::Long(l), Variant::Double) => { - writer - .write_f64::(*l as f64) - .map_err(AvrowErr::EncodeFailed)?; - } - (Value::Float(f), Variant::Float) => { - writer - .write_f32::(*f) - .map_err(AvrowErr::EncodeFailed)?; - } - // float is promotable to double --- - (Value::Float(f), Variant::Double) => { - writer - .write_f64::(*f as f64) - .map_err(AvrowErr::EncodeFailed)?; - } // --- - (Value::Double(d), Variant::Double) => { - writer - .write_f64::(*d) - .map_err(AvrowErr::EncodeFailed)?; - } - // Match with union happens first than more specific match arms - (ref value, Variant::Union { variants, .. }) => { - // the get index function returns the index if the value's schema is in the variants of the union - let (union_idx, schema) = resolve_union(&value, &variants, cxt)?; - let union_idx = union_idx as i32; - writer - .write_varint(union_idx) - .map_err(AvrowErr::EncodeFailed)?; - value.encode(writer, &schema, cxt)? - } - (Value::Record(ref record), Variant::Record { fields, .. }) => { - for (f_name, f_value) in &record.fields { - let field_type = fields.get(f_name); - if let Some(field_ty) = field_type { - f_value.value.encode(writer, &field_ty.ty, cxt)?; - } - } - } - (Value::Map(hmap), Variant::Map { values }) => { - // number of keys/value (start of a block) - encode_long(hmap.keys().len() as i64, writer)?; - for (k, v) in hmap.iter() { - encode_long(k.len() as i64, writer)?; - encode_raw_bytes(&*k.as_bytes(), writer)?; - v.encode(writer, values, cxt)?; - } - // marks end of block - encode_long(0, writer)?; - } - (Value::Fixed(ref v), Variant::Fixed { .. }) => { - writer.write_all(&*v).map_err(AvrowErr::EncodeFailed)?; - } - (Value::Str(s), Variant::Str) => { - encode_long(s.len() as i64, writer)?; - encode_raw_bytes(&*s.as_bytes(), writer)?; - } - // string is promotable to bytes --- - (Value::Str(s), Variant::Bytes) => { - encode_long(s.len() as i64, writer)?; - encode_raw_bytes(&*s.as_bytes(), writer)?; - } // -- - (Value::Bytes(b), Variant::Bytes) => { - encode_long(b.len() as i64, writer)?; - encode_raw_bytes(&*b, writer)?; - } - // bytes is promotable to string --- - (Value::Bytes(b), Variant::Str) => { - encode_long(b.len() as i64, writer)?; - encode_raw_bytes(&*b, writer)?; - } // --- - (Value::Bytes(b), Variant::Fixed { size: _size, .. }) => { - encode_raw_bytes(&*b, writer)?; - } - (Value::Enum(ref sym), Variant::Enum { symbols, .. }) => { - if let Some(idx) = symbols.iter().position(|r| r == sym) { - writer - .write_varint(idx as i32) - .map_err(AvrowErr::EncodeFailed)?; - } else { - // perf issues on creating error objects? - return Err(AvrowErr::SchemaDataMismatch); - } - } - ( - Value::Array(ref values), - Variant::Array { - items: items_schema, - }, - ) => { - let array_items_count = Value::from(values.len() as i64); - array_items_count.encode(writer, &Variant::Long, cxt)?; - - for i in values { - i.encode(writer, items_schema, cxt)?; - } - Value::from(0i64).encode(writer, &Variant::Long, cxt)?; - } - // case where serde serializes a Vec to a Array of Byte - // FIXME:figure out a better way for this? - (Value::Array(ref values), Variant::Bytes) => { - let mut v = Vec::with_capacity(values.len()); - for i in values { - if let Value::Byte(b) = i { - v.push(*b); - } - } - encode_long(values.len() as i64, writer)?; - encode_raw_bytes(&*v, writer)?; - } - _ => return Err(AvrowErr::SchemaDataMismatch), - }; - Ok(()) - } -} - -// Given a value, returns the index and the variant of the union -fn resolve_union<'a>( - value: &Value, - union_variants: &'a [Variant], - cxt: &'a Registry, -) -> Result<(usize, &'a Variant), AvrowErr> { - for (idx, variant) in union_variants.iter().enumerate() { - match (value, variant) { - (Value::Null, Variant::Null) - | (Value::Boolean(_), Variant::Boolean) - | (Value::Int(_), Variant::Int) - | (Value::Long(_), Variant::Long) - | (Value::Float(_), Variant::Float) - | (Value::Double(_), Variant::Double) - | (Value::Bytes(_), Variant::Bytes) - | (Value::Str(_), Variant::Str) - | (Value::Map(_), Variant::Map { .. }) - | (Value::Array(_), Variant::Array { .. }) => return Ok((idx, variant)), - (Value::Fixed(_), Variant::Fixed { .. }) => return Ok((idx, variant)), - (Value::Array(v), Variant::Fixed { size, .. }) => { - if v.len() == *size { - return Ok((idx, variant)); - } - return Err(AvrowErr::FixedValueLenMismatch { - found: v.len(), - expected: *size, - }); - } - (Value::Union(_), _) => return Err(AvrowErr::NoImmediateUnion), - (Value::Record(_), Variant::Named(name)) => { - if let Some(schema) = cxt.get(&name) { - return Ok((idx, schema)); - } else { - return Err(AvrowErr::SchemaNotFoundInUnion); - } - } - (Value::Enum(_), Variant::Named(name)) => { - if let Some(schema) = cxt.get(&name) { - return Ok((idx, schema)); - } else { - return Err(AvrowErr::SchemaNotFoundInUnion); - } - } - (Value::Fixed(_), Variant::Named(name)) => { - if let Some(schema) = cxt.get(&name) { - return Ok((idx, schema)); - } else { - return Err(AvrowErr::SchemaNotFoundInUnion); - } - } - _a => {} - } - } - - Err(AvrowErr::SchemaNotFoundInUnion) -} - -/////////////////////////////////////////////////////////////////////////////// -/// From impls for Value -/////////////////////////////////////////////////////////////////////////////// - -impl From<()> for Value { - fn from(_v: ()) -> Value { - Value::Null - } -} - -impl From for Value { - fn from(v: String) -> Value { - Value::Str(v) - } -} - -impl> From> for Value { - fn from(v: HashMap) -> Value { - let mut map = HashMap::with_capacity(v.len()); - for (k, v) in v.into_iter() { - map.insert(k, v.into()); - } - Value::Map(map) - } -} - -impl From for Value { - fn from(value: bool) -> Value { - Value::Boolean(value) - } -} - -impl From> for Value { - fn from(value: Vec) -> Value { - Value::Bytes(value) - } -} - -impl<'a> From<&'a [u8]> for Value { - fn from(value: &'a [u8]) -> Value { - Value::Bytes(value.to_vec()) - } -} - -impl From for Value { - fn from(value: i32) -> Value { - Value::Int(value) - } -} - -impl From for Value { - fn from(value: isize) -> Value { - Value::Int(value as i32) - } -} - -impl From for Value { - fn from(value: usize) -> Value { - Value::Int(value as i32) - } -} - -impl> From> for Value { - fn from(values: Vec) -> Value { - let mut new_vec = vec![]; - for i in values { - new_vec.push(i.into()); - } - Value::Array(new_vec) - } -} - -impl From for Value { - fn from(value: i64) -> Value { - Value::Long(value) - } -} - -impl From for Value { - fn from(value: u64) -> Value { - Value::Long(value as i64) - } -} - -impl From for Value { - fn from(value: f32) -> Value { - Value::Float(value) - } -} - -impl From for Value { - fn from(value: f64) -> Value { - Value::Double(value) - } -} - -impl<'a> From<&'a str> for Value { - fn from(value: &'a str) -> Value { - Value::Str(value.to_string()) - } -} - -#[macro_export] -/// Convenient macro to create a avro fixed value -macro_rules! fixed { - ($vec:tt) => { - avrow::Value::Fixed($vec) - }; -} - -/////////////////////////////////////////////////////////////////////////////// -/// Value -> Rust value -/////////////////////////////////////////////////////////////////////////////// - -impl Value { - /// Try to retrieve an avro null - pub fn as_null(&self) -> Result<(), AvrowErr> { - if let Value::Null = self { - Ok(()) - } else { - Err(AvrowErr::ExpectedVariantNotFound) - } - } - /// Try to retrieve an avro boolean - pub fn as_boolean(&self) -> Result<&bool, AvrowErr> { - if let Value::Boolean(b) = self { - Ok(b) - } else { - Err(AvrowErr::ExpectedVariantNotFound) - } - } - /// Try to retrieve an avro int - pub fn as_int(&self) -> Result<&i32, AvrowErr> { - if let Value::Int(v) = self { - Ok(v) - } else { - Err(AvrowErr::ExpectedVariantNotFound) - } - } - /// Try to retrieve an avro long - pub fn as_long(&self) -> Result<&i64, AvrowErr> { - if let Value::Long(v) = self { - Ok(v) - } else { - Err(AvrowErr::ExpectedVariantNotFound) - } - } - /// Try to retrieve an avro float - pub fn as_float(&self) -> Result<&f32, AvrowErr> { - if let Value::Float(v) = self { - Ok(v) - } else { - Err(AvrowErr::ExpectedVariantNotFound) - } - } - /// Try to retrieve an avro double - pub fn as_double(&self) -> Result<&f64, AvrowErr> { - if let Value::Double(v) = self { - Ok(v) - } else { - Err(AvrowErr::ExpectedVariantNotFound) - } - } - /// Try to retrieve an avro bytes - pub fn as_bytes(&self) -> Result<&[u8], AvrowErr> { - if let Value::Bytes(v) = self { - Ok(v) - } else { - Err(AvrowErr::ExpectedVariantNotFound) - } - } - /// Try to retrieve an avro string - pub fn as_string(&self) -> Result<&str, AvrowErr> { - if let Value::Str(v) = self { - Ok(v) - } else { - Err(AvrowErr::ExpectedVariantNotFound) - } - } - /// Try to retrieve an avro record - pub fn as_record(&self) -> Result<&Record, AvrowErr> { - if let Value::Record(v) = self { - Ok(v) - } else { - Err(AvrowErr::ExpectedVariantNotFound) - } - } - /// Try to retrieve the variant of the enum as a string - pub fn as_enum(&self) -> Result<&str, AvrowErr> { - if let Value::Enum(v) = self { - Ok(v) - } else { - Err(AvrowErr::ExpectedVariantNotFound) - } - } - /// Try to retrieve an avro array - pub fn as_array(&self) -> Result<&[Value], AvrowErr> { - if let Value::Array(v) = self { - Ok(v) - } else { - Err(AvrowErr::ExpectedVariantNotFound) - } - } - /// Try to retrieve an avro map - pub fn as_map(&self) -> Result<&HashMap, AvrowErr> { - if let Value::Map(v) = self { - Ok(v) - } else { - Err(AvrowErr::ExpectedVariantNotFound) - } - } - /// Try to retrieve an avro union - pub fn as_union(&self) -> Result<&Value, AvrowErr> { - if let Value::Union(v) = self { - Ok(v) - } else { - Err(AvrowErr::ExpectedVariantNotFound) - } - } - /// Try to retrieve an avro fixed - pub fn as_fixed(&self) -> Result<&[u8], AvrowErr> { - if let Value::Fixed(v) = self { - Ok(v) - } else { - Err(AvrowErr::ExpectedVariantNotFound) - } - } -} - -#[cfg(test)] -mod tests { - use super::Record; - use crate::from_value; - use crate::Schema; - use serde::{Deserialize, Serialize}; - use std::collections::BTreeMap; - use std::str::FromStr; - - #[test] - fn record_from_btree() { - let mut rec = BTreeMap::new(); - rec.insert("foo", "bar"); - let _r = Record::from_btree("test", rec).unwrap(); - } - - #[derive(Debug, Serialize, Deserialize)] - struct Mentees { - id: i32, - username: String, - } - - #[derive(Debug, Serialize, Deserialize)] - struct RustMentors { - name: String, - github_handle: String, - active: bool, - mentees: Mentees, - } - #[test] - fn record_from_json() { - let schema = Schema::from_str( - r##" - { - "name": "rust_mentors", - "type": "record", - "fields": [ - { - "name": "name", - "type": "string" - }, - { - "name": "github_handle", - "type": "string" - }, - { - "name": "active", - "type": "boolean" - }, - { - "name":"mentees", - "type": { - "name":"mentees", - "type": "record", - "fields": [ - {"name":"id", "type": "int"}, - {"name":"username", "type": "string"} - ] - } - } - ] - } -"##, - ) - .unwrap(); - - let json = serde_json::from_str( - r##" - { "name": "bob", - "github_handle":"ghbob", - "active": true, - "mentees":{"id":1, "username":"alice"} }"##, - ) - .unwrap(); - let rec = super::Record::from_json(json, &schema).unwrap(); - let mut writer = crate::Writer::new(&schema, vec![]).unwrap(); - writer.write(rec).unwrap(); - // writer.flush().unwrap(); - let avro_data = writer.into_inner().unwrap(); - let reader = crate::Reader::new(avro_data.as_slice()).unwrap(); - for value in reader { - let _mentors: RustMentors = from_value(&value).unwrap(); - } - } -} diff --git a/src/writer.rs b/src/writer.rs deleted file mode 100644 index cc12471..0000000 --- a/src/writer.rs +++ /dev/null @@ -1,318 +0,0 @@ -//! The Writer is the primary interface for writing values in avro encoded format. - -use rand::{thread_rng, Rng}; - -use crate::codec::Codec; -use crate::schema::Schema; -use crate::value::Value; -use serde::Serialize; - -use crate::config::{DEFAULT_FLUSH_INTERVAL, MAGIC_BYTES, SYNC_MARKER_SIZE}; -use crate::error::{AvrowErr, AvrowResult}; -use crate::schema::Registry; -use crate::schema::Variant; -use crate::serde_avro; -use crate::util::{encode_long, encode_raw_bytes}; -use crate::value::Map; -use std::collections::HashMap; -use std::default::Default; -use std::io::Write; - -fn sync_marker() -> [u8; SYNC_MARKER_SIZE] { - let mut vec = [0u8; SYNC_MARKER_SIZE]; - thread_rng().fill_bytes(&mut vec[..]); - vec -} - -/// Convenient builder struct for configuring and instantiating a Writer. -pub struct WriterBuilder<'a, W> { - metadata: HashMap, - codec: Codec, - schema: Option<&'a Schema>, - datafile: Option, - flush_interval: usize, -} - -impl<'a, W: Write> WriterBuilder<'a, W> { - /// Creates a builder instance to construct a Writer - pub fn new() -> Self { - WriterBuilder { - metadata: Default::default(), - codec: Codec::Null, - schema: None, - datafile: None, - flush_interval: DEFAULT_FLUSH_INTERVAL, - } - } - - /// Set any custom metadata for the datafile. - pub fn set_metadata(mut self, k: &str, v: &str) -> Self { - self.metadata - .insert(k.to_string(), Value::Bytes(v.as_bytes().to_vec())); - self - } - - /// Set one of the available codec. This requires the respective code feature flags to be enabled. - pub fn set_codec(mut self, codec: Codec) -> Self { - self.codec = codec; - self - } - - /// Provide the writer with a reference to the schema file - pub fn set_schema(mut self, schema: &'a Schema) -> Self { - self.schema = Some(schema); - self - } - - /// Set the underlying output stream. This can be anything which implements the Write trait. - pub fn set_datafile(mut self, w: W) -> Self { - self.datafile = Some(w); - self - } - - /// Set the flush interval (bytes) for the internal block buffer. It's the amount of bytes post which - /// the internal buffer is written to the underlying datafile. Defaults to [DEFAULT_FLUSH_INTERVAL]. - pub fn set_flush_interval(mut self, interval: usize) -> Self { - self.flush_interval = interval; - self - } - - /// Builds the Writer instance consuming this builder. - pub fn build(self) -> AvrowResult> { - // write the metadata - // Writer::with_codec(&self.schema, self.datafile, self.codec) - let mut writer = Writer { - out_stream: self.datafile.ok_or(AvrowErr::WriterBuildFailed)?, - schema: self.schema.ok_or(AvrowErr::WriterBuildFailed)?, - block_stream: Vec::with_capacity(self.flush_interval), - block_count: 0, - codec: self.codec, - sync_marker: sync_marker(), - flush_interval: self.flush_interval, - }; - writer.encode_custom_header(self.metadata)?; - Ok(writer) - } -} - -impl<'a, W: Write> Default for WriterBuilder<'a, W> { - fn default() -> Self { - Self::new() - } -} - -/// The Writer is the primary interface for writing values to an avro datafile or a byte container (say a `Vec`). -/// It takes a reference to the schema for validating the values being written -/// and an output stream W which can be any type -/// implementing the [Write](https://doc.rust-lang.org/std/io/trait.Write.html) trait. -pub struct Writer<'a, W> { - out_stream: W, - schema: &'a Schema, - block_stream: Vec, - block_count: usize, - codec: Codec, - sync_marker: [u8; 16], - flush_interval: usize, -} - -impl<'a, W: Write> Writer<'a, W> { - /// Creates a new avro Writer instance taking a reference to a `Schema` and and a `Write`. - pub fn new(schema: &'a Schema, out_stream: W) -> AvrowResult { - let mut writer = Writer { - out_stream, - schema, - block_stream: Vec::with_capacity(DEFAULT_FLUSH_INTERVAL), - block_count: 0, - codec: Codec::Null, - sync_marker: sync_marker(), - flush_interval: DEFAULT_FLUSH_INTERVAL, - }; - writer.encode_header()?; - Ok(writer) - } - - /// Same as the new method, but additionally takes a `Codec` as parameter. - /// Codecs can be used to compress the encoded data being written in avro format. - /// Supported codecs as per spec are: - /// * null (default): No compression is applied. - /// * [snappy](https://en.wikipedia.org/wiki/Snappy_(compression)) (`--features snappy`) - /// * [deflate](https://en.wikipedia.org/wiki/DEFLATE) (`--features deflate`) - /// * [zstd](https://facebook.github.io/zstd/) compression (`--feature zstd`) - /// * [bzip](http://www.bzip.org/) compression (`--feature bzip`) - /// * [xz](https://tukaani.org/xz/) compression (`--features xz`) - pub fn with_codec(schema: &'a Schema, out_stream: W, codec: Codec) -> AvrowResult { - let mut writer = Writer { - out_stream, - schema, - block_stream: Vec::with_capacity(DEFAULT_FLUSH_INTERVAL), - block_count: 0, - codec, - sync_marker: sync_marker(), - flush_interval: DEFAULT_FLUSH_INTERVAL, - }; - writer.encode_header()?; - Ok(writer) - } - - /// Appends a value to the buffer. - /// Before a value gets written, it gets validated with the schema referenced - /// by this writer. - /// **Note**: writes are buffered internally as per the flush interval and the underlying - /// buffer may not reflect values immediately. - /// Call [`flush`](struct.Writer.html#method.flush) to explicitly write all buffered data. - /// Alternatively calling [`into_inner`](struct.Writer.html#method.into_inner) on the writer - /// guarantees that flush will happen and will hand over - /// the underlying buffer with all data written. - pub fn write>(&mut self, value: T) -> AvrowResult<()> { - let val: Value = value.into(); - self.schema.validate(&val)?; - - val.encode( - &mut self.block_stream, - &self.schema.variant(), - &self.schema.cxt, - )?; - self.block_count += 1; - - if self.block_stream.len() >= self.flush_interval { - self.flush()?; - } - - Ok(()) - } - - /// Appends a native Rust value to the buffer. The value must implement Serde's `Serialize` trait. - pub fn serialize(&mut self, value: T) -> AvrowResult<()> { - let value = serde_avro::to_value(&value)?; - self.write(value)?; - Ok(()) - } - - fn reset_block_buffer(&mut self) { - self.block_count = 0; - self.block_stream.clear(); - } - - /// Sync/flush any buffered data to the underlying buffer. - /// Note: This method is called to ensure that all - pub fn flush(&mut self) -> AvrowResult<()> { - // bail if no data is written or it has already been flushed before - if self.block_count == 0 { - return Ok(()); - } - // encode datum count - encode_long(self.block_count as i64, &mut self.out_stream)?; - // encode with codec - self.codec - .encode(&mut self.block_stream, &mut self.out_stream)?; - // Write sync marker - encode_raw_bytes(&self.sync_marker, &mut self.out_stream)?; - // Reset block buffer - self.out_stream.flush().map_err(AvrowErr::EncodeFailed)?; - self.reset_block_buffer(); - Ok(()) - } - - // Used via WriterBuilder - fn encode_custom_header(&mut self, mut map: HashMap) -> AvrowResult<()> { - self.out_stream - .write(MAGIC_BYTES) - .map_err(AvrowErr::EncodeFailed)?; - map.insert("avro.schema".to_string(), self.schema.as_bytes().into()); - let codec_str = self.codec.as_ref().as_bytes(); - map.insert("avro.codec".to_string(), codec_str.into()); - let meta_schema = &Variant::Map { - values: Box::new(Variant::Bytes), - }; - - Value::Map(map).encode(&mut self.out_stream, meta_schema, &Registry::new())?; - encode_raw_bytes(&self.sync_marker, &mut self.out_stream)?; - Ok(()) - } - - fn encode_header(&mut self) -> AvrowResult<()> { - self.out_stream - .write(MAGIC_BYTES) - .map_err(AvrowErr::EncodeFailed)?; - // encode metadata - let mut metamap = Map::with_capacity(2); - metamap.insert("avro.schema".to_string(), self.schema.as_bytes().into()); - let codec_str = self.codec.as_ref().as_bytes(); - metamap.insert("avro.codec".to_string(), codec_str.into()); - let meta_schema = &Variant::Map { - values: Box::new(Variant::Bytes), - }; - - Value::Map(metamap).encode(&mut self.out_stream, meta_schema, &Registry::new())?; - encode_raw_bytes(&self.sync_marker, &mut self.out_stream)?; - Ok(()) - } - - /// Consumes self and yields the inner Write instance. - /// Additionally calls flush if no flush has happened before this call. - pub fn into_inner(mut self) -> AvrowResult { - self.flush()?; - Ok(self.out_stream) - } -} - -#[cfg(test)] -mod tests { - use crate::{from_value, Codec, Reader, Schema, Writer, WriterBuilder}; - use std::io::Cursor; - use std::str::FromStr; - - #[test] - fn header_written_on_writer_creation() { - let schema = Schema::from_str(r##""null""##).unwrap(); - let v = Cursor::new(vec![]); - let writer = Writer::new(&schema, v).unwrap(); - let buf = writer.into_inner().unwrap().into_inner(); - // writer. - let slice = &buf[0..4]; - - assert_eq!(slice[0], b'O'); - assert_eq!(slice[1], b'b'); - assert_eq!(slice[2], b'j'); - assert_eq!(slice[3], 1); - } - - #[test] - fn writer_with_builder() { - let schema = Schema::from_str(r##""null""##).unwrap(); - let v = vec![]; - let mut writer = WriterBuilder::new() - .set_codec(Codec::Null) - .set_schema(&schema) - .set_datafile(v) - .set_flush_interval(128_000) - .build() - .unwrap(); - writer.serialize(()).unwrap(); - let _v = writer.into_inner().unwrap(); - - let reader = Reader::with_schema(_v.as_slice(), schema).unwrap(); - for i in reader { - let _: () = from_value(&i).unwrap(); - } - } - - #[test] - fn custom_metadata_header() { - let schema = Schema::from_str(r##""null""##).unwrap(); - let v = vec![]; - let mut writer = WriterBuilder::new() - .set_codec(Codec::Null) - .set_schema(&schema) - .set_datafile(v) - .set_flush_interval(128_000) - .set_metadata("hello", "world") - .build() - .unwrap(); - writer.serialize(()).unwrap(); - let _v = writer.into_inner().unwrap(); - - let reader = Reader::with_schema(_v.as_slice(), schema).unwrap(); - assert!(reader.meta().contains_key("hello")); - } -} diff --git a/tests/common.rs b/tests/common.rs deleted file mode 100644 index 3e71d5e..0000000 --- a/tests/common.rs +++ /dev/null @@ -1,90 +0,0 @@ -#![allow(dead_code)] - -use avrow::Codec; -use avrow::Schema; -use avrow::{Reader, Writer}; -use std::io::Cursor; -use std::str::FromStr; - -#[derive(Debug)] -pub(crate) enum Primitive { - Null, - Boolean, - Int, - Long, - Float, - Double, - Bytes, - String, -} - -impl std::fmt::Display for Primitive { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - use Primitive::*; - let str_repr = match self { - Null => "null", - Boolean => "boolean", - Int => "int", - Long => "long", - Float => "float", - Double => "double", - Bytes => "bytes", - String => "string", - }; - write!(f, "{}", str_repr) - } -} - -pub(crate) fn writer_from_schema<'a>(schema: &'a Schema, codec: Codec) -> Writer<'a, Vec> { - let writer = Writer::with_codec(&schema, vec![], codec).unwrap(); - writer -} - -pub(crate) fn reader_with_schema<'a>(schema: Schema, buffer: Vec) -> Reader>> { - let reader = Reader::with_schema(Cursor::new(buffer), schema).unwrap(); - reader -} - -pub(crate) struct MockSchema; -impl MockSchema { - // creates a primitive schema - pub fn prim(self, ty: &str) -> Schema { - let schema_str = format!("{{\"type\": \"{}\"}}", ty); - Schema::from_str(&schema_str).unwrap() - } - - pub fn record(self) -> Schema { - Schema::from_str( - r#" - { - "type": "record", - "name": "LongList", - "aliases": ["LinkedLongs"], - "fields" : [ - {"name": "value", "type": "long"}, - {"name": "next", "type": ["null", "LongList"]} - ] - } - "#, - ) - .unwrap() - } - - pub fn record_default(self) -> Schema { - Schema::from_str( - r#" - { - "type": "record", - "name": "LongList", - "aliases": ["LinkedLongs"], - "fields" : [ - {"name": "value", "type": "long"}, - {"name": "next", "type": ["null", "LongList"]}, - {"name": "other", "type":"long", "default": 1} - ] - } - "#, - ) - .unwrap() - } -} diff --git a/tests/read_write.rs b/tests/read_write.rs deleted file mode 100644 index 28530d6..0000000 --- a/tests/read_write.rs +++ /dev/null @@ -1,414 +0,0 @@ -extern crate pretty_env_logger; -extern crate serde_derive; - -mod common; - -use avrow::{from_value, Reader, Schema, Codec, Value}; -use std::str::FromStr; -use crate::common::{MockSchema, writer_from_schema}; -use std::collections::HashMap; - - -use common::{Primitive}; -use serde_derive::{Deserialize, Serialize}; - -const DATUM_COUNT: usize = 10000; - -/////////////////////////////////////////////////////////////////////////////// -/// Primitive schema tests -/////////////////////////////////////////////////////////////////////////////// - -// #[cfg(feature = "codec")] -static PRIMITIVES: [Primitive; 8] = [ - Primitive::Null, - Primitive::Boolean, - Primitive::Int, - Primitive::Long, - Primitive::Float, - Primitive::Double, - Primitive::Bytes, - Primitive::String, -]; - -// static PRIMITIVES: [Primitive; 1] = [Primitive::Int]; - -#[cfg(feature = "codec")] -const CODECS: [Codec; 6] = [ - Codec::Null, - Codec::Deflate, - Codec::Snappy, - Codec::Zstd, - Codec::Bzip2, - Codec::Xz, -]; - -// #[cfg(feature = "bzip2")] -// const CODECS: [Codec; 1] = [Codec::Bzip2]; - -#[test] -#[cfg(feature = "codec")] -fn read_write_primitive() { - for codec in CODECS.iter() { - for primitive in PRIMITIVES.iter() { - // write - let name = &format!("{}", primitive); - let schema = MockSchema.prim(name); - let mut writer = writer_from_schema(&schema, *codec); - (0..DATUM_COUNT).for_each(|i| match primitive { - Primitive::Null => { - writer.write(()).unwrap(); - } - Primitive::Boolean => { - writer.write(i % 2 == 0).unwrap(); - } - Primitive::Int => { - writer.write(std::i32::MAX).unwrap(); - } - Primitive::Long => { - writer.write(std::i64::MAX).unwrap(); - } - Primitive::Float => { - writer.write(std::f32::MAX).unwrap(); - } - Primitive::Double => { - writer.write(std::f64::MAX).unwrap(); - } - Primitive::Bytes => { - writer.write(vec![b'a', b'v', b'r', b'o', b'w']).unwrap(); - } - Primitive::String => { - writer.write("avrow").unwrap(); - } - }); - - let buf = writer.into_inner().unwrap(); - - // read - let reader = Reader::with_schema(buf.as_slice(), MockSchema.prim(name)).unwrap(); - for i in reader { - match primitive { - Primitive::Null => { - let _: () = from_value(&i).unwrap(); - } - Primitive::Boolean => { - let _: bool = from_value(&i).unwrap(); - } - Primitive::Int => { - let _: i32 = from_value(&i).unwrap(); - } - Primitive::Long => { - let _: i64 = from_value(&i).unwrap(); - } - Primitive::Float => { - let _: f32 = from_value(&i).unwrap(); - } - Primitive::Double => { - let _: f64 = from_value(&i).unwrap(); - } - Primitive::Bytes => { - let _: &[u8] = from_value(&i).unwrap(); - } - Primitive::String => { - let _: &str = from_value(&i).unwrap(); - } - } - } - } - } -} - -/////////////////////////////////////////////////////////////////////////////// -/// Complex schema tests -/////////////////////////////////////////////////////////////////////////////// - -#[derive(Debug, Serialize, Deserialize)] -struct LongList { - value: i64, - next: Option>, -} - -#[test] -#[cfg(feature = "codec")] -fn io_read_write_self_referential_record() { - // write - for codec in CODECS.iter() { - let schema = r##" - { - "type": "record", - "name": "LongList", - "aliases": ["LinkedLongs"], - "fields" : [ - {"name": "value", "type": "long"}, - {"name": "next", "type": ["null", "LongList"]} - ] - } - "##; - - let schema = Schema::from_str(schema).unwrap(); - let mut writer = writer_from_schema(&schema, *codec); - for _ in 0..1 { - let value = LongList { - value: 1i64, - next: Some(Box::new(LongList { - value: 2, - next: Some(Box::new(LongList { - value: 3, - next: None, - })), - })), - }; - // let value = LongList { - // value: 1i64, - // next: None, - // }; - writer.serialize(value).unwrap(); - } - - let buf = writer.into_inner().unwrap(); - - // read - let reader = Reader::with_schema(buf.as_slice(), schema).unwrap(); - for i in reader { - let _: LongList = from_value(&i).unwrap(); - } - } -} - -#[derive(Serialize, Deserialize)] -enum Suit { - SPADES, - HEARTS, - DIAMONDS, - CLUBS, -} - -#[test] -#[cfg(feature = "codec")] -fn enum_read_write() { - // write - for codec in CODECS.iter() { - let schema = r##" - { - "type": "enum", - "name": "Suit", - "symbols" : ["SPADES", "HEARTS", "DIAMONDS", "CLUBS"] - } - "##; - - let schema = Schema::from_str(schema).unwrap(); - let mut writer = writer_from_schema(&schema, *codec); - for _ in 0..1 { - let value = Suit::SPADES; - writer.serialize(value).unwrap(); - } - - let buf = writer.into_inner().unwrap(); - - // read - let reader = Reader::with_schema(buf.as_slice(), schema).unwrap(); - for i in reader { - let _: Suit = from_value(&i).unwrap(); - } - } -} - -#[test] -#[cfg(feature = "codec")] -fn array_read_write() { - // write - for codec in CODECS.iter() { - let schema = r##" - {"type": "array", "items": "string"} - "##; - - let schema = Schema::from_str(schema).unwrap(); - let mut writer = writer_from_schema(&schema, *codec); - for _ in 0..DATUM_COUNT { - let value = vec!["a", "v", "r", "o", "w"]; - writer.serialize(value).unwrap(); - } - - let buf = writer.into_inner().unwrap(); - - // read - let reader = Reader::with_schema(buf.as_slice(), schema).unwrap(); - for i in reader { - let _: Vec<&str> = from_value(&i).unwrap(); - } - } -} - -#[test] -#[cfg(feature = "codec")] -fn map_read_write() { - // write - for codec in CODECS.iter() { - let schema = r##" - {"type": "map", "values": "long"} - "##; - - let schema = Schema::from_str(schema).unwrap(); - let mut writer = writer_from_schema(&schema, *codec); - for _ in 0..DATUM_COUNT { - let mut value = HashMap::new(); - value.insert("foo", 1i64); - value.insert("bar", 2); - writer.serialize(value).unwrap(); - } - - let buf = writer.into_inner().unwrap(); - - // read - let reader = Reader::with_schema(buf.as_slice(), schema).unwrap(); - for i in reader { - let _: HashMap = from_value(&i).unwrap(); - } - } -} - -#[test] -#[cfg(feature = "codec")] -fn union_read_write() { - // write - for codec in CODECS.iter() { - let schema = r##" - ["null", "string"] - "##; - - let schema = Schema::from_str(schema).unwrap(); - let mut writer = writer_from_schema(&schema, *codec); - for _ in 0..1 { - writer.serialize(()).unwrap(); - writer.serialize("hello".to_string()).unwrap(); - } - - let buf = writer.into_inner().unwrap(); - - // read - let reader = Reader::with_schema(buf.as_slice(), schema).unwrap(); - for i in reader { - let val = i.as_ref().unwrap(); - match val { - Value::Null => { - let _a: () = from_value(&i).unwrap(); - } - Value::Str(_) => { - let _a: &str = from_value(&i).unwrap(); - } - _ => unreachable!("should not happen"), - } - } - } -} - -#[test] -#[cfg(feature = "codec")] -fn fixed_read_write() { - // write - for codec in CODECS.iter() { - let schema = r##" - {"type": "fixed", "size": 16, "name": "md5"} - "##; - - let schema = Schema::from_str(schema).unwrap(); - let mut writer = writer_from_schema(&schema, *codec); - for _ in 0..1 { - let value = vec![ - b'1', b'2', b'3', b'4', b'5', b'6', b'7', b'8', b'9', b'a', b'b', b'c', b'd', b'e', - b'f', b'g', - ]; - writer.serialize(value.as_slice()).unwrap(); - } - - let buf = writer.into_inner().unwrap(); - - // read - let reader = Reader::with_schema(buf.as_slice(), schema).unwrap(); - for i in reader { - let a: [u8; 16] = from_value(&i).unwrap(); - assert_eq!(a.len(), 16); - } - } -} - -#[test] -#[cfg(feature = "codec")] -fn bytes_read_write() { - let schema = Schema::from_str(r##"{"type": "bytes"}"##).unwrap(); - let mut writer = writer_from_schema(&schema, avrow::Codec::Deflate); - let data = vec![0u8, 1u8, 2u8, 3u8, 4u8, 5u8]; - writer.serialize(&data).unwrap(); - - let buf = writer.into_inner().unwrap(); - // let mut v: Vec = vec![]; - - let reader = Reader::with_schema(buf.as_slice(), schema).unwrap(); - for i in reader { - // dbg!(i); - let b: &[u8] = from_value(&i).unwrap(); - dbg!(b); - } - - // assert_eq!(v, data); -} - -#[test] -#[should_panic] -#[cfg(feature = "codec")] -fn write_invalid_union_data_fails() { - let schema = Schema::from_str(r##"["int", "float"]"##).unwrap(); - let mut writer = writer_from_schema(&schema, avrow::Codec::Null); - writer.serialize("string").unwrap(); -} - -// #[derive(Debug, serde::Serialize, serde::Deserialize)] -// struct LongList { -// value: i64, -// next: Option>, -// } - -#[test] -#[cfg(feature = "snappy")] -fn read_deflate_reuse() { - let schema = Schema::from_str( - r##" - { - "type": "record", - "name": "LongList", - "aliases": ["LinkedLongs"], - "fields" : [ - {"name": "value", "type": "long"}, - {"name": "next", "type": ["null", "LongList"]} - ] - } - "##, - ) - .unwrap(); - let vec = vec![]; - let mut writer = avrow::Writer::with_codec(&schema, vec, Codec::Snappy).unwrap(); - for _ in 0..100000 { - let value = LongList { - value: 1i64, - next: Some(Box::new(LongList { - value: 2i64, - next: Some(Box::new(LongList { - value: 3i64, - next: Some(Box::new(LongList { - value: 4i64, - next: Some(Box::new(LongList { - value: 5i64, - next: None, - })), - })), - })), - })), - }; - writer.serialize(value).unwrap(); - } - let vec = writer.into_inner().unwrap(); - - let reader = Reader::new(&*vec).unwrap(); - for i in reader { - let _v: LongList = from_value(&i).unwrap(); - } -} diff --git a/tests/schema_resolution.rs b/tests/schema_resolution.rs deleted file mode 100644 index 7747e86..0000000 --- a/tests/schema_resolution.rs +++ /dev/null @@ -1,315 +0,0 @@ -/// Tests for schema resolution -mod common; - -use serde::{Deserialize, Serialize}; - -use avrow::{from_value, Codec, Reader, Schema, Value}; -use std::collections::HashMap; -use std::str::FromStr; - -use common::{reader_with_schema, writer_from_schema, MockSchema}; - -#[test] -#[should_panic] -fn null_fails_with_other_primitive_schema() { - let name = "null"; - let schema = MockSchema.prim(name); - let mut writer = writer_from_schema(&schema, Codec::Null); - writer.serialize(()).unwrap(); - writer.flush().unwrap(); - - let buf = writer.into_inner().unwrap(); - - let reader_schema = MockSchema.prim("boolean"); - let reader = Reader::with_schema(buf.as_slice(), reader_schema).unwrap(); - - for i in reader { - let _ = i.unwrap(); - } -} - -#[test] -fn writer_to_reader_promotion_primitives() { - // int -> long, float, double - for reader_schema in &["long", "float", "double"] { - let name = "int"; - let schema = MockSchema.prim(name); - let mut writer = writer_from_schema(&schema, Codec::Null); - writer.serialize(1024).unwrap(); - writer.flush().unwrap(); - - let buf = writer.into_inner().unwrap(); - - let reader_schema = MockSchema.prim(reader_schema); - let reader = Reader::with_schema(buf.as_slice(), reader_schema).unwrap(); - for i in reader { - assert!(i.is_ok()); - let _a = i.unwrap(); - } - } - - // long -> float, double - for reader_schema in &["float", "double"] { - let name = "long"; - let schema = MockSchema.prim(name); - let mut writer = writer_from_schema(&schema, Codec::Null); - writer.serialize(1024i64).unwrap(); - writer.flush().unwrap(); - - let buf = writer.into_inner().unwrap(); - - let reader_schema = MockSchema.prim(reader_schema); - let reader = Reader::with_schema(buf.as_slice(), reader_schema).unwrap(); - for i in reader { - assert!(i.is_ok()); - } - } - - // float -> double - for reader_schema in &["double"] { - let name = "float"; - let schema = MockSchema.prim(name); - let mut writer = writer_from_schema(&schema, Codec::Null); - writer.serialize(1026f32).unwrap(); - writer.flush().unwrap(); - - let buf = writer.into_inner().unwrap(); - - let reader_schema = MockSchema.prim(reader_schema); - let reader = Reader::with_schema(buf.as_slice(), reader_schema).unwrap(); - for i in reader { - assert!(i.is_ok()); - } - } - - // string -> bytes - for reader_schema in &["bytes"] { - let name = "string"; - let schema = MockSchema.prim(name); - let mut writer = writer_from_schema(&schema, Codec::Null); - writer.serialize("hello").unwrap(); - writer.flush().unwrap(); - - let buf = writer.into_inner().unwrap(); - - let reader_schema = MockSchema.prim(reader_schema); - let reader = Reader::with_schema(buf.as_slice(), reader_schema).unwrap(); - for i in reader { - assert!(i.is_ok()); - let a = i.unwrap(); - assert_eq!(Value::Bytes(vec![104, 101, 108, 108, 111]), a); - } - } - - // bytes -> string - for reader_schema in &["string"] { - let name = "bytes"; - let schema = MockSchema.prim(name); - let mut writer = writer_from_schema(&schema, Codec::Null); - writer.serialize([104u8, 101, 108, 108, 111]).unwrap(); - writer.flush().unwrap(); - - let buf = writer.into_inner().unwrap(); - - let reader_schema = MockSchema.prim(reader_schema); - let reader = Reader::with_schema(buf.as_slice(), reader_schema).unwrap(); - for i in reader { - assert!(i.is_ok()); - let a = i.unwrap(); - assert_eq!(Value::Str("hello".to_string()), a); - } - } -} - -#[derive(Serialize, Deserialize)] -enum Foo { - A, - B, - C, - E, -} - -#[test] -#[should_panic] -fn enum_fails_schema_resolution() { - let schema = - Schema::from_str(r##"{"type": "enum", "name": "Foo", "symbols": ["A", "B", "C", "D"] }"##) - .unwrap(); - let mut writer = writer_from_schema(&schema, Codec::Null); - writer.serialize(Foo::B).unwrap(); - writer.flush().unwrap(); - - let buf = writer.into_inner().unwrap(); - - // Reading a symbol which does not exist in writer's schema should fail - let reader_schema = - Schema::from_str(r##"{"type": "enum", "name": "Foo", "symbols": ["F"] }"##).unwrap(); - let reader = Reader::with_schema(buf.as_slice(), reader_schema).unwrap(); - - // let reader = reader_with_schema(reader_schema, name); - for i in reader { - i.unwrap(); - } -} - -#[test] -#[should_panic] -fn schema_resolution_map() { - let schema = Schema::from_str(r##"{"type": "map", "values": "string"}"##).unwrap(); - let mut writer = writer_from_schema(&schema, Codec::Null); - let mut m = HashMap::new(); - m.insert("1", "b"); - writer.serialize(m).unwrap(); - writer.flush().unwrap(); - - let buf = writer.into_inner().unwrap(); - - // // Reading a symbol which does not exist in writer's schema should fail - let reader_schema = Schema::from_str(r##"{"type": "map", "values": "int"}"##).unwrap(); - - let reader = reader_with_schema(reader_schema, buf); - for i in reader { - let _ = i.unwrap(); - } -} - -#[derive(Serialize, Deserialize)] -struct LongList { - value: i64, - next: Option>, -} - -#[derive(Serialize, Deserialize, Debug)] -struct LongListDefault { - value: i64, - next: Option>, - other: i64, -} - -#[test] -fn record_schema_resolution_with_default_value() { - let schema = MockSchema.record(); - let mut writer = writer_from_schema(&schema, Codec::Null); - let list = LongList { - value: 1, - next: None, - }; - - writer.serialize(list).unwrap(); - - let buf = writer.into_inner().unwrap(); - - let schema = MockSchema.record_default(); - let reader = reader_with_schema(schema, buf); - for i in reader { - let rec: Result = from_value(&i); - assert!(rec.is_ok()); - } -} - -#[test] -#[cfg(feature = "codec")] -fn writer_is_a_union_but_reader_is_not() { - let writer_schema = Schema::from_str(r##"["null", "int"]"##).unwrap(); - let mut writer = writer_from_schema(&writer_schema, Codec::Deflate); - writer.serialize(()).unwrap(); - writer.serialize(3).unwrap(); - - let buf = writer.into_inner().unwrap(); - - let schema_str = r##""int""##; - let reader_schema = Schema::from_str(schema_str).unwrap(); - let mut reader = reader_with_schema(reader_schema, buf); - assert!(reader.next().unwrap().is_err()); - assert!(reader.next().unwrap().is_ok()); -} - -#[test] -fn reader_is_a_union_but_writer_is_not() { - let writer_schema = Schema::from_str(r##""int""##).unwrap(); - let mut writer = writer_from_schema(&writer_schema, Codec::Null); - writer.serialize(3).unwrap(); - - let buf = writer.into_inner().unwrap(); - - // err - let reader_schema = Schema::from_str(r##"["null", "string"]"##).unwrap(); - let mut reader = reader_with_schema(reader_schema, buf.clone()); - assert!(reader.next().unwrap().is_err()); - - // ok - let reader_schema = Schema::from_str(r##"["null", "int"]"##).unwrap(); - let mut reader = reader_with_schema(reader_schema, buf); - assert!(reader.next().unwrap().is_ok()); -} - -#[test] -fn both_are_unions_but_different() { - let writer_schema = Schema::from_str(r##"["null", "int"]"##).unwrap(); - let mut writer = writer_from_schema(&writer_schema, Codec::Null); - writer.serialize(3).unwrap(); - - let buf = writer.into_inner().unwrap(); - - let reader_schema = Schema::from_str(r##"["boolean", "string"]"##).unwrap(); - let mut reader = reader_with_schema(reader_schema, buf); - - // err - assert!(reader.next().unwrap().is_err()); -} - -#[test] -fn both_are_map() { - let writer_schema = Schema::from_str(r##"{"type": "map", "values": "string"}"##).unwrap(); - let mut writer = writer_from_schema(&writer_schema, Codec::Null); - let mut map = HashMap::new(); - map.insert("hello", "world"); - writer.serialize(map).unwrap(); - - let buf = writer.into_inner().unwrap(); - - // let reader_schema = - // Schema::from_str(r##"["boolean", {"type":"map", "values": "string"}]"##).unwrap(); - let reader_schema = Schema::from_str(r##"{"type": "map", "values": "string"}"##).unwrap(); - let mut reader = reader_with_schema(reader_schema, buf); - assert!(reader.next().unwrap().is_ok()); -} - -#[test] -fn both_are_arrays() { - let writer_schema = Schema::from_str(r##"{"type": "array", "items": "int"}"##).unwrap(); - let mut writer = writer_from_schema(&writer_schema, Codec::Null); - writer.serialize(vec![1, 2, 3]).unwrap(); - - let buf = writer.into_inner().unwrap(); - - let reader_schema = Schema::from_str(r##"{"type": "array", "items": "int"}"##).unwrap(); - let mut reader = reader_with_schema(reader_schema, buf); - assert!(reader.next().unwrap().is_ok()); -} - -#[test] -fn both_are_enums() { - let writer_schema = Schema::from_str(r##"{"type": "array", "items": "int"}"##).unwrap(); - let mut writer = writer_from_schema(&writer_schema, Codec::Null); - writer.serialize(vec![1, 2, 3]).unwrap(); - - let buf = writer.into_inner().unwrap(); - - let reader_schema = Schema::from_str(r##"{"type": "array", "items": "int"}"##).unwrap(); - let mut reader = reader_with_schema(reader_schema, buf); - assert!(reader.next().unwrap().is_ok()); -} - -#[test] -fn null() { - let writer_schema = Schema::from_str(r##"{"type": "null"}"##).unwrap(); - let mut writer = writer_from_schema(&writer_schema, Codec::Null); - writer.serialize(()).unwrap(); - - let buf = writer.into_inner().unwrap(); - - let reader_schema = Schema::from_str(r##"{"type": "null"}"##).unwrap(); - let mut reader = reader_with_schema(reader_schema, buf); - assert!(reader.next().unwrap().is_ok()); -} From 8db6a00660327d8d0efc3f4cd80e908af378f963 Mon Sep 17 00:00:00 2001 From: creativcoder <5155745+creativcoder@users.noreply.github.com> Date: Fri, 9 Oct 2020 00:05:42 +0530 Subject: [PATCH 3/5] Create CNAME --- CNAME | 1 + 1 file changed, 1 insertion(+) create mode 100644 CNAME diff --git a/CNAME b/CNAME new file mode 100644 index 0000000..0133fc9 --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +creativcoder.dev From a2fb9cf2e7b6ceab2e6b2865416de1c3548db85b Mon Sep 17 00:00:00 2001 From: creativcoder <5155745+creativcoder@users.noreply.github.com> Date: Fri, 9 Oct 2020 00:10:06 +0530 Subject: [PATCH 4/5] Update index.html --- index.html | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/index.html b/index.html index 7bf83b9..0d4e57f 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ - Document + Avrow @@ -45,18 +45,17 @@ -
+
hero
-

Avrow - a simple, type safe - implementation +

Avrow is a pure + Rust implementation of the avro - specification - in Rust. + specification with Serde support.

From e75194d168b76360bb088593edc74be85374b2aa Mon Sep 17 00:00:00 2001 From: creativcoder <5155745+creativcoder@users.noreply.github.com> Date: Sat, 5 Nov 2022 12:21:33 +0530 Subject: [PATCH 5/5] Delete CNAME --- CNAME | 1 - 1 file changed, 1 deletion(-) delete mode 100644 CNAME diff --git a/CNAME b/CNAME deleted file mode 100644 index 0133fc9..0000000 --- a/CNAME +++ /dev/null @@ -1 +0,0 @@ -creativcoder.dev