├── .github └── workflows │ └── checks.yml ├── .gitignore ├── .travis.yml ├── Dockerfile ├── LICENSE ├── Makefile ├── NOTICE ├── README.md ├── bin └── php ├── composer.json ├── examples └── write_read.php ├── lib ├── avro.php └── avro │ ├── data_file.php │ ├── datum.php │ ├── debug.php │ ├── gmp.php │ ├── io.php │ ├── protocol.php │ ├── schema.php │ └── util.php ├── phpunit.xml.dist └── test ├── DataFileTest.php ├── DatumIOTest.php ├── FileIOTest.php ├── FloatIntEncodingTest.php ├── IODatumReaderTest.php ├── LongEncodingTest.php ├── NameTest.php ├── ProtocolFileTest.php ├── SchemaTest.php ├── StringIOTest.php ├── data ├── user.avsc ├── users.avro ├── users.json ├── users_deflate.avro └── users_snappy.avro └── test_helper.php /.github/workflows/checks.yml: -------------------------------------------------------------------------------- 1 | name: avro-php 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - master 7 | jobs: 8 | build-source: 9 | runs-on: ubuntu-22.04 10 | steps: 11 | - 12 | name: Checkout 13 | uses: actions/checkout@v4 14 | - 15 | name: Install phars 16 | run: | 17 | make install-phars 18 | - 19 | name: Upload source directory 20 | uses: actions/upload-artifact@v4 21 | with: 22 | name: source 23 | include-hidden-files: true 24 | path: . 25 | php-xdebug-docker: 26 | needs: 27 | - build-source 28 | strategy: 29 | matrix: 30 | php: 31 | - 32 | version: '8.1' 33 | xdebug: '3.4.0' 34 | - 35 | version: '8.2' 36 | xdebug: '3.4.0' 37 | - 38 | version: '8.3' 39 | xdebug: '3.4.0' 40 | - 41 | version: '8.4' 42 | xdebug: '3.4.0' 43 | runs-on: ubuntu-22.04 44 | steps: 45 | - 46 | name: Download sources 47 | uses: actions/download-artifact@v4 48 | with: 49 | name: source 50 | - 51 | name: Set up Docker Buildx 52 | uses: docker/setup-buildx-action@v3 53 | - 54 | name: Build 55 | uses: docker/build-push-action@v6 56 | with: 57 | context: . 58 | file: ./Dockerfile 59 | load: true 60 | tags: avro-php:${{ matrix.php.version }} 61 | build-args: | 62 | PHP_VERSION=${{ matrix.php.version }} 63 | XDEBUG_VERSION=${{ matrix.php.xdebug }} 64 | - 65 | name: Inspect docker image 66 | run: | 67 | docker image inspect avro-php:${{ matrix.php.version }} 68 | - 69 | name: Save docker image 70 | run: | 71 | docker save avro-php:${{ matrix.php.version }} -o php-avro-serde-${{ matrix.php.version }}.tgz 72 | - 73 | name: Upload docker image 74 | uses: actions/upload-artifact@v4 75 | with: 76 | name: php-avro-serde-${{ matrix.php.version }} 77 | path: php-avro-serde-${{ matrix.php.version }}.tgz 78 | ci-checks: 79 | runs-on: ubuntu-22.04 80 | needs: 81 | - php-xdebug-docker 82 | strategy: 83 | matrix: 84 | php: 85 | - 86 | version: '8.1' 87 | composer: --prefer-stable 88 | - 89 | version: '8.2' 90 | composer: --prefer-stable 91 | - 92 | version: '8.3' 93 | composer: --prefer-stable 94 | - 95 | version: '8.4' 96 | composer: --prefer-stable 97 | steps: 98 | - 99 | name: Download sources 100 | uses: actions/download-artifact@v4 101 | with: 102 | name: source 103 | - 104 | name: Set up Docker Buildx 105 | uses: docker/setup-buildx-action@v3 106 | - 107 | name: Download docker image 108 | uses: actions/download-artifact@v4 109 | with: 110 | name: php-avro-serde-${{ matrix.php.version }} 111 | - 112 | name: Load docker image 113 | run: | 114 | docker load -i php-avro-serde-${{ matrix.php.version }}.tgz 115 | - 116 | name: Install vendors 117 | run: | 118 | docker run -i --rm --net=host --sig-proxy=true --pid=host \ 119 | -v "${GITHUB_WORKSPACE}":"${GITHUB_WORKSPACE}" -w "${GITHUB_WORKSPACE}" avro-php:${{ matrix.php.version }} \ 120 | composer update --no-interaction --no-scripts --no-ansi ${{ matrix.php.composer }} 121 | - 122 | name: Run PHPUnit 123 | run: | 124 | mkdir -p build/tmp build/share/test/schemas build/build/interop/data 125 | chmod -R a+w build 126 | docker run -i --rm --net=host --sig-proxy=true --pid=host \ 127 | -v "${GITHUB_WORKSPACE}":"${GITHUB_WORKSPACE}" -w "${GITHUB_WORKSPACE}" avro-php:${{ matrix.php.version }} \ 128 | vendor/bin/phpunit --exclude-group integration 129 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | vendor/ 3 | composer.lock 4 | bin/composer.phar 5 | !bin/php 6 | !build/.gitkeep 7 | .phpunit.result.cache 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | sudo: required 3 | language: php 4 | 5 | php: 6 | - '7.1' 7 | - '7.2' 8 | - '7.3' 9 | - '7.4' 10 | 11 | before_script: 12 | - cd /tmp 13 | - git clone --recursive --depth=1 https://github.com/kjdev/php-ext-snappy.git 14 | - cd php-ext-snappy 15 | - export CXXFLAGS=-std=c++11 16 | - phpize 17 | - ./configure 18 | - make 19 | - make install 20 | - echo "extension = snappy.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini 21 | - cd $TRAVIS_BUILD_DIR 22 | - composer self-update 23 | - composer update 24 | 25 | script: 26 | - COMPOSER=composer PHP=php make phpunit 27 | 28 | after_script: 29 | - make clean 30 | 31 | branches: 32 | only: 33 | - master 34 | - '/^ft-.*/' 35 | - '/^\d+\.\d+\.\d+$/' 36 | 37 | cache: 38 | directories: 39 | - $HOME/.composer/cache/files 40 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG PHP_VERSION=8.1 2 | 3 | FROM php:${PHP_VERSION}-cli-alpine 4 | 5 | ARG XDEBUG_VERSION=3.4.0 6 | 7 | COPY --from=composer /usr/bin/composer /usr/bin/composer 8 | RUN composer --version 9 | 10 | RUN apk add --no-cache --virtual .build-deps $PHPIZE_DEPS \ 11 | && apk add --no-cache --virtual .runtime-deps git libzip-dev gmp-dev \ 12 | && docker-php-source extract \ 13 | && docker-php-ext-configure zip \ 14 | && docker-php-ext-install zip gmp \ 15 | && apk add --update linux-headers \ 16 | && mkdir -p /usr/src/php/ext/xdebug \ 17 | && curl -fsSL https://github.com/xdebug/xdebug/archive/$XDEBUG_VERSION.tar.gz | tar xvz -C /usr/src/php/ext/xdebug --strip 1 \ 18 | && docker-php-ext-install xdebug \ 19 | && docker-php-ext-enable xdebug \ 20 | && git clone --recursive --depth=1 https://github.com/kjdev/php-ext-snappy.git \ 21 | && cd php-ext-snappy \ 22 | && phpize \ 23 | && ./configure \ 24 | && make \ 25 | && make install \ 26 | && docker-php-ext-enable snappy \ 27 | && docker-php-source delete \ 28 | && apk del .build-deps 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | 204 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # no builtin rules and variables 2 | MAKEFLAGS =+ -rR --warn-undefined-variables 3 | 4 | .PHONY: composer-install composer-update examples docker run 5 | 6 | COMPOSER ?= bin/composer.phar 7 | COMPOSER_VERSION ?= 2.8.4 8 | PHP ?= bin/php 9 | PHP_VERSION ?= 8.3 10 | XDEBUG_VERSION ?= 3.4.0 11 | 12 | export 13 | 14 | docker: 15 | docker build \ 16 | --build-arg PHP_VERSION=$(PHP_VERSION) \ 17 | --build-arg XDEBUG_VERSION=$(XDEBUG_VERSION) \ 18 | -t avro-php:$(PHP_VERSION) \ 19 | -f Dockerfile \ 20 | . 21 | 22 | composer-install: 23 | PHP_VERSION=$(PHP_VERSION) $(PHP) $(COMPOSER) install --no-interaction --no-progress --no-suggest --no-scripts 24 | 25 | composer-update: 26 | PHP_VERSION=$(PHP_VERSION) $(PHP) $(COMPOSER) update --no-interaction --no-progress --no-suggest --no-scripts 27 | 28 | phpunit: 29 | @mkdir -p build/tmp build/share/test/schemas build/build/interop/data 30 | @chmod -R a+w build 31 | PHP_VERSION=$(PHP_VERSION) $(PHP) vendor/bin/phpunit 32 | 33 | coverage: 34 | @mkdir -p build/tmp build/share/test/schemas build/build/interop/data 35 | @chmod -R a+w build 36 | PHP_VERSION=$(PHP_VERSION) $(PHP) -d xdebug.mode=coverage vendor/bin/phpunit --coverage-text 37 | 38 | run: 39 | PHP_VERSION=$(PHP_VERSION) $(PHP) $(ARGS) 40 | 41 | examples: 42 | PHP_VERSION=$(PHP_VERSION) $(PHP) examples/* 43 | 44 | install-phars: 45 | curl https://getcomposer.org/download/$(COMPOSER_VERSION)/composer.phar -o bin/composer.phar -LR -z bin/composer.phar 46 | chmod a+x bin/composer.phar 47 | 48 | install: install-phars docker composer-install 49 | 50 | clean: 51 | rm -r build/* 52 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Apache Avro 2 | Copyright 2010-2015 The Apache Software Foundation 3 | 4 | This product includes software developed at 5 | The Apache Software Foundation (http://www.apache.org/). 6 | 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | What the Avro PHP library is 2 | ============================ 3 | 4 | A library for using [Avro](http://avro.apache.org/) with PHP. 5 | 6 | Requirements 7 | ============ 8 | * PHP >= 8.1 9 | * On 32-bit platforms, the [GMP PHP extension](http://php.net/gmp) 10 | * For testing, [PHPUnit](http://www.phpunit.de/) 11 | * For Deflate codec, [Zlib](https://www.php.net/zlib) 12 | * For Snappy codec, [Snappy](http://google.github.io/snappy/) 13 | 14 | Both GMP and PHPUnit are often available via package management 15 | systems as `php5-gmp` and `phpunit`, respectively. 16 | 17 | Getting started 18 | =============== 19 | 20 | Untar the avro-php distribution, untar it, and put it in your include path: 21 | 22 | tar xjf avro-php.tar.bz2 # avro-php.tar.bz2 is likely avro-php-1.4.0.tar.bz2 23 | cp avro-php /path/to/where/you/want/it 24 | 25 | Require the avro.php file in your source, and you should be good to go: 26 | 27 | /dev/null 2>&1 || { echo "docker is required to run this binary. Aborting." >&2; exit 1; } 6 | 7 | USER=${USER:-$( id -un )} 8 | GROUP=${GROUP:-$( id -gn )} 9 | COMPOSER_HOME=${COMPOSER_HOME:-${HOME}/.composer} 10 | PHP_VERSION=${PHP_VERSION:-8.1} 11 | DOCKER_OPTS=${DOCKER_OPTS:-'-it'} 12 | 13 | exec docker run ${DOCKER_OPTS} --rm \ 14 | -u $( id -u ${USER} ):$( id -g ${USER} ) \ 15 | -v "${PWD}":"${PWD}" \ 16 | -v "${COMPOSER_HOME}":/tmp/composer \ 17 | -w ${PWD} \ 18 | -e PHP_IDE_CONFIG="serverName=avro-php" \ 19 | -e COMPOSER_HOME="/tmp/composer" \ 20 | --net=host --sig-proxy=true --pid=host \ 21 | --entrypoint="php" \ 22 | avro-php:${PHP_VERSION} "${@}" 23 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flix-tech/avro-php", 3 | "description": "Avro schema encoder/decoder. Fork of rg/avro-php", 4 | "license": "Apache-2.0", 5 | "require": { 6 | "php": "^8.1" 7 | }, 8 | "require-dev": { 9 | "phpunit/phpunit": "^10.5" 10 | }, 11 | "suggest": { 12 | "ext-gmp": "Large integer support for 32-bit platforms.", 13 | "ext-zlib": "Zlib support for Deflate codec.", 14 | "ext-snappy": "Snappy support for Snappy codec (mainly Google)." 15 | }, 16 | "autoload": { 17 | "classmap": ["lib/"] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/write_read.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 1392, 'member_name' => 'Jose'); 33 | $maria = array('member_id' => 1642, 'member_name' => 'Maria'); 34 | $data = array($jose, $maria); 35 | 36 | $file_name = 'data.avr'; 37 | // Open $file_name for writing, using the given writer's schema 38 | $data_writer = AvroDataIO::open_file($file_name, 'w', $writers_schema_json); 39 | 40 | // Write each datum to the file 41 | foreach ($data as $datum) 42 | $data_writer->append($datum); 43 | // Tidy up 44 | $data_writer->close(); 45 | 46 | // Open $file_name (by default for reading) using the writer's schema 47 | // included in the file 48 | $data_reader = AvroDataIO::open_file($file_name); 49 | echo "from file:\n"; 50 | // Read each datum 51 | foreach ($data_reader->data() as $datum) 52 | echo var_export($datum, true) . "\n"; 53 | $data_reader->close(); 54 | 55 | // Create a data string 56 | // Create a string io object. 57 | $io = new AvroStringIO(); 58 | // Create a datum writer object 59 | $writers_schema = AvroSchema::parse($writers_schema_json); 60 | $writer = new AvroIODatumWriter($writers_schema); 61 | $data_writer = new AvroDataIOWriter($io, $writer, $writers_schema); 62 | foreach ($data as $datum) 63 | $data_writer->append($datum); 64 | $data_writer->close(); 65 | 66 | $binary_string = $io->string(); 67 | 68 | // Load the string data string 69 | $read_io = new AvroStringIO($binary_string); 70 | $data_reader = new AvroDataIOReader($read_io, new AvroIODatumReader()); 71 | echo "from binary string:\n"; 72 | foreach ($data_reader->data() as $datum) 73 | echo var_export($datum, true) . "\n"; 74 | 75 | /** Output 76 | from file: 77 | array ( 78 | 'member_id' => 1392, 79 | 'member_name' => 'Jose', 80 | ) 81 | array ( 82 | 'member_id' => 1642, 83 | 'member_name' => 'Maria', 84 | ) 85 | from binary string: 86 | array ( 87 | 'member_id' => 1392, 88 | 'member_name' => 'Jose', 89 | ) 90 | array ( 91 | 'member_id' => 1642, 92 | 'member_name' => 'Maria', 93 | ) 94 | */ 95 | -------------------------------------------------------------------------------- /lib/avro.php: -------------------------------------------------------------------------------- 1 | io = $io; 250 | $this->decoder = new AvroIOBinaryDecoder($this->io); 251 | $this->datum_reader = $datum_reader; 252 | $this->read_header(); 253 | 254 | $codec = AvroUtil::array_value($this->metadata, 255 | AvroDataIO::METADATA_CODEC_ATTR); 256 | if ($codec && !AvroDataIO::is_valid_codec($codec)) 257 | throw new AvroDataIOException(sprintf('Uknown codec: %s', $codec)); 258 | $this->codec = $codec; 259 | 260 | $this->block_count = 0; 261 | // FIXME: Seems unsanitary to set writers_schema here. 262 | // Can't constructor take it as an argument? 263 | $this->datum_reader->set_writers_schema( 264 | AvroSchema::parse($this->metadata[AvroDataIO::METADATA_SCHEMA_ATTR])); 265 | } 266 | 267 | /** 268 | * Reads header of object container 269 | * @throws AvroDataIOException if the file is not an Avro data file. 270 | */ 271 | private function read_header() 272 | { 273 | $this->seek(0, AvroIO::SEEK_SET); 274 | 275 | $magic = $this->read(AvroDataIO::magic_size()); 276 | 277 | if (strlen($magic) < AvroDataIO::magic_size()) 278 | throw new AvroDataIOException( 279 | 'Not an Avro data file: shorter than the Avro magic block'); 280 | 281 | if (AvroDataIO::magic() != $magic) 282 | throw new AvroDataIOException( 283 | sprintf('Not an Avro data file: %s does not match %s', 284 | $magic, AvroDataIO::magic())); 285 | 286 | $this->metadata = $this->datum_reader->read_data(AvroDataIO::metadata_schema(), 287 | AvroDataIO::metadata_schema(), 288 | $this->decoder); 289 | $this->sync_marker = $this->read(AvroDataIO::SYNC_SIZE); 290 | } 291 | 292 | /** 293 | * @return array of data from object container. 294 | * @throws AvroDataIOException 295 | * @throws AvroIOException 296 | */ 297 | public function data() 298 | { 299 | $data = array(); 300 | while (true) 301 | { 302 | if (0 == $this->block_count) 303 | { 304 | if ($this->is_eof()) 305 | break; 306 | 307 | if ($this->skip_sync()) 308 | if ($this->is_eof()) 309 | break; 310 | 311 | $decoder = $this->apply_codec($this->decoder, $this->codec); 312 | } 313 | $data[] = $this->datum_reader->read($decoder); 314 | $this->block_count -= 1; 315 | } 316 | return $data; 317 | } 318 | 319 | /** 320 | * @throws AvroDataIOException 321 | * @throws AvroIOException 322 | */ 323 | public function data_iterator() 324 | { 325 | while (true) 326 | { 327 | if (0 == $this->block_count) 328 | { 329 | if ($this->is_eof()) 330 | break; 331 | 332 | if ($this->skip_sync()) 333 | if ($this->is_eof()) 334 | break; 335 | 336 | $decoder = $this->apply_codec($this->decoder, $this->codec); 337 | } 338 | yield $this->datum_reader->read($decoder); 339 | $this->block_count -= 1; 340 | } 341 | } 342 | 343 | /** 344 | * @param AvroIOBinaryDecoder $decoder 345 | * @param string $codec 346 | * @return AvroIOBinaryDecoder 347 | * @throws AvroDataIOException 348 | * @throws AvroIOException 349 | */ 350 | protected function apply_codec($decoder, $codec) 351 | { 352 | $length = $this->read_block_header(); 353 | if ($codec == AvroDataIO::DEFLATE_CODEC) { 354 | if (!function_exists('gzinflate')) { 355 | throw new AvroDataIOException('"gzinflate" function not available, "zlib" extension required.'); 356 | } 357 | $compressed = $decoder->read($length); 358 | $datum = gzinflate($compressed); 359 | $decoder = new AvroIOBinaryDecoder(new AvroStringIO($datum)); 360 | } elseif ($codec == AvroDataIO::SNAPPY_CODEC) { 361 | if (!function_exists('snappy_uncompress')) { 362 | throw new AvroDataIOException('"snappy_uncompress" function not available, "snappy" extension required.'); 363 | } 364 | $compressed = $decoder->read($length-4); 365 | $datum = snappy_uncompress($compressed); 366 | $crc32 = unpack('N', $decoder->read(4)); 367 | if ($crc32[1] != crc32($datum)) { 368 | throw new AvroDataIOException('Invalid CRC32 checksum.'); 369 | } 370 | $decoder = new AvroIOBinaryDecoder(new AvroStringIO($datum)); 371 | } 372 | return $decoder; 373 | } 374 | 375 | /** 376 | * Closes this writer (and its AvroIO object.) 377 | * @uses AvroIO::close() 378 | */ 379 | public function close() { return $this->io->close(); } 380 | 381 | /** 382 | * @uses AvroIO::seek() 383 | * @param $offset 384 | * @param $whence 385 | * @return bool 386 | * @throws AvroNotImplementedException 387 | */ 388 | private function seek($offset, $whence) 389 | { 390 | return $this->io->seek($offset, $whence); 391 | } 392 | 393 | /** 394 | * @uses AvroIO::read() 395 | * @param $len 396 | * @return string 397 | * @throws AvroNotImplementedException 398 | */ 399 | private function read($len) { return $this->io->read($len); } 400 | 401 | /** 402 | * @uses AvroIO::is_eof() 403 | */ 404 | private function is_eof() { return $this->io->is_eof(); } 405 | 406 | /** 407 | * @return bool 408 | */ 409 | private function skip_sync() 410 | { 411 | $proposed_sync_marker = $this->read(AvroDataIO::SYNC_SIZE); 412 | if ($proposed_sync_marker != $this->sync_marker) 413 | { 414 | $this->seek(-AvroDataIO::SYNC_SIZE, AvroIO::SEEK_CUR); 415 | return false; 416 | } 417 | return true; 418 | } 419 | 420 | /** 421 | * Reads the block header (which includes the count of items in the block 422 | * and the length in bytes of the block) 423 | * @return int length in bytes of the block. 424 | */ 425 | private function read_block_header() 426 | { 427 | $this->block_count = $this->decoder->read_long(); 428 | return $this->decoder->read_long(); 429 | } 430 | 431 | } 432 | 433 | /** 434 | * Writes Avro data to an AvroIO source using an AvroSchema 435 | * @package Avro 436 | */ 437 | class AvroDataIOWriter 438 | { 439 | /** 440 | * @return string a new, unique sync marker. 441 | */ 442 | private static function generate_sync_marker() 443 | { 444 | // From http://php.net/manual/en/function.mt-rand.php comments 445 | return pack('S8', 446 | mt_rand(0, 0xffff), mt_rand(0, 0xffff), 447 | mt_rand(0, 0xffff), 448 | mt_rand(0, 0xffff) | 0x4000, 449 | mt_rand(0, 0xffff) | 0x8000, 450 | mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)); 451 | } 452 | 453 | /** 454 | * @var AvroIO object container where data is written 455 | */ 456 | private $io; 457 | 458 | /** 459 | * @var AvroIOBinaryEncoder encoder for object container 460 | */ 461 | private $encoder; 462 | 463 | /** 464 | * @var AvroDatumWriter 465 | */ 466 | private $datum_writer; 467 | 468 | /** 469 | * @var AvroStringIO buffer for writing 470 | */ 471 | private $buffer; 472 | 473 | /** 474 | * @var AvroIOBinaryEncoder encoder for buffer 475 | */ 476 | private $buffer_encoder; // AvroIOBinaryEncoder 477 | 478 | /** 479 | * @var int count of items written to block 480 | */ 481 | private $block_count; 482 | 483 | /** 484 | * @var array map of object container metadata 485 | */ 486 | private $metadata; 487 | 488 | /** 489 | * @var string compression codec 490 | */ 491 | private $codec; 492 | 493 | private $sync_marker; 494 | 495 | /** 496 | * @param AvroIO $io 497 | * @param AvroIODatumWriter $datum_writer 498 | * @param AvroSchema $writers_schema 499 | * @param string $codec 500 | * @throws AvroDataIOException 501 | */ 502 | public function __construct($io, $datum_writer, $writers_schema=null, $codec=AvroDataIO::NULL_CODEC) 503 | { 504 | if (!($io instanceof AvroIO)) 505 | throw new AvroDataIOException('io must be instance of AvroIO'); 506 | 507 | $this->io = $io; 508 | $this->encoder = new AvroIOBinaryEncoder($this->io); 509 | $this->datum_writer = $datum_writer; 510 | $this->buffer = new AvroStringIO(); 511 | $this->buffer_encoder = new AvroIOBinaryEncoder($this->buffer); 512 | $this->block_count = 0; 513 | $this->metadata = array(); 514 | 515 | if ($writers_schema) 516 | { 517 | if (!AvroDataIO::is_valid_codec($codec)) 518 | throw new AvroDataIOException( 519 | sprintf('codec %s is not supported', $codec)); 520 | 521 | $this->sync_marker = self::generate_sync_marker(); 522 | $this->metadata[AvroDataIO::METADATA_CODEC_ATTR] = $this->codec = $codec; 523 | $this->metadata[AvroDataIO::METADATA_SCHEMA_ATTR] = strval($writers_schema); 524 | $this->write_header(); 525 | } 526 | else 527 | { 528 | $dfr = new AvroDataIOReader($this->io, new AvroIODatumReader()); 529 | $this->sync_marker = $dfr->sync_marker; 530 | $this->metadata[AvroDataIO::METADATA_CODEC_ATTR] = $this->codec 531 | = $dfr->metadata[AvroDataIO::METADATA_CODEC_ATTR]; 532 | 533 | $schema_from_file = $dfr->metadata[AvroDataIO::METADATA_SCHEMA_ATTR]; 534 | $this->metadata[AvroDataIO::METADATA_SCHEMA_ATTR] = $schema_from_file; 535 | $this->datum_writer->writers_schema = AvroSchema::parse($schema_from_file); 536 | $this->seek(0, SEEK_END); 537 | } 538 | } 539 | 540 | /** 541 | * @param mixed $datum 542 | */ 543 | public function append($datum) 544 | { 545 | $this->datum_writer->write($datum, $this->buffer_encoder); 546 | $this->block_count++; 547 | 548 | if ($this->buffer->length() >= AvroDataIO::SYNC_INTERVAL) 549 | $this->write_block(); 550 | } 551 | 552 | /** 553 | * Flushes buffer to AvroIO object container and closes it. 554 | * @return mixed value of $io->close() 555 | * @see AvroIO::close() 556 | */ 557 | public function close() 558 | { 559 | $this->flush(); 560 | return $this->io->close(); 561 | } 562 | 563 | /** 564 | * Flushes biffer to AvroIO object container. 565 | * @return mixed value of $io->flush() 566 | * @see AvroIO::flush() 567 | */ 568 | private function flush() 569 | { 570 | $this->write_block(); 571 | return $this->io->flush(); 572 | } 573 | 574 | /** 575 | * Writes a block of data to the AvroIO object container. 576 | */ 577 | private function write_block() 578 | { 579 | if ($this->block_count > 0) 580 | { 581 | $this->encoder->write_long($this->block_count); 582 | $to_write = strval($this->buffer); 583 | 584 | if ($this->codec == AvroDataIO::DEFLATE_CODEC) { 585 | if (!function_exists('gzinflate')) { 586 | throw new AvroDataIOException('"gzinflate" function not available, "zlib" extension required.'); 587 | } 588 | $to_write = gzdeflate($to_write); 589 | } elseif ($this->codec == AvroDataIO::SNAPPY_CODEC) { 590 | if (!function_exists('snappy_compress')) { 591 | throw new AvroDataIOException('"snappy_compress" function not available, "snappy" extension required.'); 592 | } 593 | $crc32 = pack('N', crc32($to_write)); 594 | $to_write = snappy_compress($to_write) . $crc32; 595 | } 596 | 597 | $this->encoder->write_long(strlen($to_write)); 598 | $this->write($to_write); 599 | $this->write($this->sync_marker); 600 | $this->buffer->truncate(); 601 | $this->block_count = 0; 602 | } 603 | } 604 | 605 | /** 606 | * Writes the header of the AvroIO object container 607 | */ 608 | private function write_header() 609 | { 610 | $this->write(AvroDataIO::magic()); 611 | $this->datum_writer->write_data(AvroDataIO::metadata_schema(), 612 | $this->metadata, $this->encoder); 613 | $this->write($this->sync_marker); 614 | } 615 | 616 | /** 617 | * @param string $bytes 618 | * @uses AvroIO::write() 619 | * @return int 620 | */ 621 | private function write($bytes) { return $this->io->write($bytes); } 622 | 623 | /** 624 | * @param int $offset 625 | * @param int $whence 626 | * @uses AvroIO::seek() 627 | * @return bool 628 | */ 629 | private function seek($offset, $whence) 630 | { 631 | return $this->io->seek($offset, $whence); 632 | } 633 | } 634 | -------------------------------------------------------------------------------- /lib/avro/debug.php: -------------------------------------------------------------------------------- 1 | = $debug_level); 53 | } 54 | 55 | /** 56 | * @param string $format format string for the given arguments. Passed as is 57 | * to vprintf. 58 | * @param array $args array of arguments to pass to vsprinf. 59 | * @param int $debug_level debug level at which to print this statement 60 | * @return boolean true 61 | */ 62 | static function debug($format, $args, $debug_level=self::DEBUG1) 63 | { 64 | if (self::is_debug($debug_level)) 65 | vprintf($format . "\n", $args); 66 | return true; 67 | } 68 | 69 | /** 70 | * @param string $str 71 | * @return string[] array of hex representation of each byte of $str 72 | */ 73 | static function hex_array($str) { return self::bytes_array($str); } 74 | 75 | /** 76 | * @param string $str 77 | * @param string $joiner string used to join 78 | * @return string hex-represented bytes of each byte of $str 79 | joined by $joiner 80 | */ 81 | static function hex_string($str, $joiner=' ') 82 | { 83 | return join($joiner, self::hex_array($str)); 84 | } 85 | 86 | /** 87 | * @param string $str 88 | * @param string $format format to represent bytes 89 | * @return string[] array of each byte of $str formatted using $format 90 | */ 91 | static function bytes_array($str, $format='x%02x') 92 | { 93 | $x = array(); 94 | foreach (str_split($str) as $b) 95 | $x []= sprintf($format, ord($b)); 96 | return $x; 97 | } 98 | 99 | /** 100 | * @param string $str 101 | * @return string[] array of bytes of $str represented in decimal format ('%3d') 102 | */ 103 | static function dec_array($str) { return self::bytes_array($str, '%3d'); } 104 | 105 | /** 106 | * @param string $str 107 | * @param string $joiner string to join bytes of $str 108 | * @return string of bytes of $str represented in decimal format 109 | * @uses dec_array() 110 | */ 111 | static function dec_string($str, $joiner = ' ') 112 | { 113 | return join($joiner, self::dec_array($str)); 114 | } 115 | 116 | /** 117 | * @param string $str 118 | * @param string $format one of 'ctrl', 'hex', or 'dec' for control, 119 | * hexadecimal, or decimal format for bytes. 120 | * - ctrl: ASCII control characters represented as text. 121 | * For example, the null byte is represented as 'NUL'. 122 | * Visible ASCII characters represent themselves, and 123 | * others are represented as a decimal ('%03d') 124 | * - hex: bytes represented in hexadecimal ('%02X') 125 | * - dec: bytes represented in decimal ('%03d') 126 | * @return string[] array of bytes represented in the given format. 127 | * @throws AvroException 128 | */ 129 | static function ascii_array($str, $format='ctrl') 130 | { 131 | if (!in_array($format, array('ctrl', 'hex', 'dec'))) 132 | throw new AvroException('Unrecognized format specifier'); 133 | 134 | $ctrl_chars = array('NUL', 'SOH', 'STX', 'ETX', 'EOT', 'ENQ', 'ACK', 'BEL', 135 | 'BS', 'HT', 'LF', 'VT', 'FF', 'CR', 'SO', 'SI', 136 | 'DLE', 'DC1', 'DC2', 'DC3', 'DC4', 'NAK', 'SYN', 'ETB', 137 | 'CAN', 'EM', 'SUB', 'ESC', 'FS', 'GS', 'RS', 'US'); 138 | $x = array(); 139 | foreach (str_split($str) as $b) 140 | { 141 | $db = ord($b); 142 | if ($db < 32) 143 | { 144 | switch ($format) 145 | { 146 | case 'ctrl': 147 | $x []= str_pad($ctrl_chars[$db], 3, ' ', STR_PAD_LEFT); 148 | break; 149 | case 'hex': 150 | $x []= sprintf("x%02X", $db); 151 | break; 152 | case 'dec': 153 | $x []= str_pad($db, 3, '0', STR_PAD_LEFT); 154 | break; 155 | } 156 | } 157 | else if ($db < 127) 158 | $x []= " $b"; 159 | else if ($db == 127) 160 | { 161 | switch ($format) 162 | { 163 | case 'ctrl': 164 | $x []= 'DEL'; 165 | break; 166 | case 'hex': 167 | $x []= sprintf("x%02X", $db); 168 | break; 169 | case 'dec': 170 | $x []= str_pad($db, 3, '0', STR_PAD_LEFT); 171 | break; 172 | } 173 | } 174 | else 175 | if ('hex' == $format) 176 | $x []= sprintf("x%02X", $db); 177 | else 178 | $x []= str_pad($db, 3, '0', STR_PAD_LEFT); 179 | } 180 | return $x; 181 | } 182 | 183 | /** 184 | * @param string $str 185 | * @param string $format one of 'ctrl', 'hex', or 'dec'. 186 | * See {@link self::ascii_array()} for more description 187 | * @param string $joiner 188 | * @return string of bytes joined by $joiner 189 | * @uses ascii_array() 190 | */ 191 | static function ascii_string($str, $format='ctrl', $joiner = ' ') 192 | { 193 | return join($joiner, self::ascii_array($str, $format)); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /lib/avro/gmp.php: -------------------------------------------------------------------------------- 1 | gmp_sign($g)) 146 | $g = self::gmp_twos_complement($g); 147 | 148 | $m = gmp_mul($g, gmp_pow(self::gmp_2(), $shift)); 149 | $m = gmp_and($m, self::gmp_0xfs()); 150 | if (gmp_testbit($m, 63)) 151 | $m = gmp_neg(gmp_add(gmp_and(gmp_com($m), self::gmp_0xfs()), 152 | self::gmp_1())); 153 | return $m; 154 | } 155 | 156 | /** 157 | * Arithmetic right shift 158 | * @param resource|int|string $g 159 | * @param int $shift number of bits to shift right 160 | * @return resource $g shifted right $shift bits 161 | */ 162 | static function shift_right($g, $shift) 163 | { 164 | if (0 == $shift) 165 | return $g; 166 | 167 | if (0 <= gmp_sign($g)) 168 | $m = gmp_div($g, gmp_pow(self::gmp_2(), $shift)); 169 | else // negative 170 | { 171 | $g = gmp_and($g, self::gmp_0xfs()); 172 | $m = gmp_div($g, gmp_pow(self::gmp_2(), $shift)); 173 | $m = gmp_and($m, self::gmp_0xfs()); 174 | for ($i = 63; $i >= (63 - $shift); $i--) 175 | gmp_setbit($m, $i); 176 | 177 | $m = gmp_neg(gmp_add(gmp_and(gmp_com($m), self::gmp_0xfs()), 178 | self::gmp_1())); 179 | } 180 | 181 | return $m; 182 | } 183 | 184 | /** 185 | * @param int|string $n integer (or string representation of integer) to encode 186 | * @return string $bytes of the long $n encoded per the Avro spec 187 | */ 188 | static function encode_long($n) 189 | { 190 | $g = gmp_init($n); 191 | $g = gmp_xor(self::shift_left($g, 1), 192 | self::shift_right($g, 63)); 193 | $bytes = ''; 194 | while (0 != gmp_cmp(self::gmp_0(), gmp_and($g, self::gmp_n0x7f()))) 195 | { 196 | $bytes .= chr(gmp_intval(gmp_and($g, self::gmp_0x7f())) | 0x80); 197 | $g = self::shift_right($g, 7); 198 | } 199 | $bytes .= chr(gmp_intval($g)); 200 | return $bytes; 201 | } 202 | 203 | /** 204 | * @param int[] $bytes array of ascii codes of bytes to decode 205 | * @return string represenation of decoded long. 206 | */ 207 | static function decode_long_from_array($bytes) 208 | { 209 | $b = array_shift($bytes); 210 | $g = gmp_init($b & 0x7f); 211 | $shift = 7; 212 | while (0 != ($b & 0x80)) 213 | { 214 | $b = array_shift($bytes); 215 | $g = gmp_or($g, self::shift_left(($b & 0x7f), $shift)); 216 | $shift += 7; 217 | } 218 | $val = gmp_xor(self::shift_right($g, 1), gmp_neg(gmp_and($g, 1))); 219 | return gmp_strval($val); 220 | } 221 | 222 | } 223 | -------------------------------------------------------------------------------- /lib/avro/io.php: -------------------------------------------------------------------------------- 1 | not like eof in C or feof in PHP: 126 | * it returns TRUE if the *next* read would be end of file, 127 | * rather than if the *most recent* read read end of file. 128 | * @return bool true if at the end of file, and false otherwise 129 | * @throws AvroNotImplementedException 130 | */ 131 | public function is_eof() 132 | { 133 | throw new AvroNotImplementedException('Not implemented'); 134 | } 135 | 136 | /** 137 | * Closes this AvroIO instance. 138 | */ 139 | public function close() 140 | { 141 | throw new AvroNotImplementedException('Not implemented'); 142 | } 143 | 144 | } 145 | 146 | /** 147 | * AvroIO wrapper for string access 148 | * @package Avro 149 | */ 150 | class AvroStringIO extends AvroIO 151 | { 152 | /** 153 | * @var string 154 | */ 155 | private $string_buffer; 156 | /** 157 | * @var int current position in string 158 | */ 159 | private $current_index; 160 | /** 161 | * @var boolean whether or not the string is closed. 162 | */ 163 | private $is_closed; 164 | 165 | /** 166 | * @param string $str initial value of AvroStringIO buffer. Regardless 167 | * of the initial value, the pointer is set to the 168 | * beginning of the buffer. 169 | * @throws AvroIOException if a non-string value is passed as $str 170 | */ 171 | public function __construct($str = '') 172 | { 173 | $this->is_closed = false; 174 | $this->string_buffer = ''; 175 | $this->current_index = 0; 176 | 177 | if (is_string($str)) 178 | $this->string_buffer .= $str; 179 | else 180 | throw new AvroIOException( 181 | sprintf('constructor argument must be a string: %s', gettype($str))); 182 | } 183 | 184 | /** 185 | * Append bytes to this buffer. 186 | * (Nothing more is needed to support Avro.) 187 | * @param string $arg bytes to write 188 | * @return int count of bytes written. 189 | * @throws AvroIOException if $args is not a string value. 190 | */ 191 | public function write($arg) 192 | { 193 | $this->check_closed(); 194 | if (is_string($arg)) 195 | return $this->append_str($arg); 196 | throw new AvroIOException( 197 | sprintf('write argument must be a string: (%s) %s', 198 | gettype($arg), var_export($arg, true))); 199 | } 200 | 201 | /** 202 | * @param int $len 203 | * @return string bytes read from buffer 204 | * @throws AvroIOException 205 | * @todo test for fencepost errors wrt updating current_index 206 | */ 207 | public function read($len) 208 | { 209 | $this->check_closed(); 210 | $read=''; 211 | for($i=$this->current_index; $i<($this->current_index+$len); $i++) { 212 | if (!isset($this->string_buffer[$i])) continue; 213 | $read .= $this->string_buffer[$i]; 214 | } 215 | if (strlen($read) < $len) 216 | $this->current_index = $this->length(); 217 | else 218 | $this->current_index += $len; 219 | return $read; 220 | } 221 | 222 | /** 223 | * @param int $offset 224 | * @param int $whence 225 | * @return bool true if successful 226 | * @throws AvroIOException if the seek failed. 227 | */ 228 | public function seek($offset, $whence=self::SEEK_SET) 229 | { 230 | if (!is_int($offset)) 231 | throw new AvroIOException('Seek offset must be an integer.'); 232 | // Prevent seeking before BOF 233 | switch ($whence) 234 | { 235 | case self::SEEK_SET: 236 | if (0 > $offset) 237 | throw new AvroIOException('Cannot seek before beginning of file.'); 238 | $this->current_index = $offset; 239 | break; 240 | case self::SEEK_CUR: 241 | if (0 > $this->current_index + $whence) 242 | throw new AvroIOException('Cannot seek before beginning of file.'); 243 | $this->current_index += $offset; 244 | break; 245 | case self::SEEK_END: 246 | if (0 > $this->length() + $offset) 247 | throw new AvroIOException('Cannot seek before beginning of file.'); 248 | $this->current_index = $this->length() + $offset; 249 | break; 250 | default: 251 | throw new AvroIOException(sprintf('Invalid seek whence %d', $whence)); 252 | } 253 | 254 | return true; 255 | } 256 | 257 | /** 258 | * @return int 259 | * @see AvroIO::tell() 260 | */ 261 | public function tell() { return $this->current_index; } 262 | 263 | /** 264 | * @return boolean 265 | * @see AvroIO::is_eof() 266 | */ 267 | public function is_eof() 268 | { 269 | return ($this->current_index >= $this->length()); 270 | } 271 | 272 | /** 273 | * No-op provided for compatibility with AvroIO interface. 274 | * @return boolean true 275 | */ 276 | public function flush() { return true; } 277 | 278 | /** 279 | * Marks this buffer as closed. 280 | * @return boolean true 281 | */ 282 | public function close() 283 | { 284 | $this->check_closed(); 285 | $this->is_closed = true; 286 | return true; 287 | } 288 | 289 | /** 290 | * @throws AvroIOException if the buffer is closed. 291 | */ 292 | private function check_closed() 293 | { 294 | if ($this->is_closed()) 295 | throw new AvroIOException('Buffer is closed'); 296 | } 297 | 298 | /** 299 | * Appends bytes to this buffer. 300 | * @param string $str 301 | * @return integer count of bytes written. 302 | */ 303 | private function append_str($str) 304 | { 305 | $this->check_closed(); 306 | $this->string_buffer .= $str; 307 | $len = strlen($str); 308 | $this->current_index += $len; 309 | return $len; 310 | } 311 | 312 | /** 313 | * Truncates the truncate buffer to 0 bytes and returns the pointer 314 | * to the beginning of the buffer. 315 | * @return boolean true 316 | */ 317 | public function truncate() 318 | { 319 | $this->check_closed(); 320 | $this->string_buffer = ''; 321 | $this->current_index = 0; 322 | return true; 323 | } 324 | 325 | /** 326 | * @return int count of bytes in the buffer 327 | * @internal Could probably memoize length for performance, but 328 | * no need do this yet. 329 | */ 330 | public function length() { return strlen($this->string_buffer); } 331 | 332 | /** 333 | * @return string 334 | */ 335 | public function __toString() { return $this->string_buffer; } 336 | 337 | 338 | /** 339 | * @return string 340 | * @uses self::__toString() 341 | */ 342 | public function string() { return $this->__toString(); } 343 | 344 | /** 345 | * @return boolean true if this buffer is closed and false 346 | * otherwise. 347 | */ 348 | public function is_closed() { return $this->is_closed; } 349 | } 350 | 351 | /** 352 | * AvroIO wrapper for PHP file access functions 353 | * @package Avro 354 | */ 355 | class AvroFile extends AvroIO 356 | { 357 | /** 358 | * @var string fopen read mode value. Used internally. 359 | */ 360 | const FOPEN_READ_MODE = 'rb'; 361 | 362 | /** 363 | * @var string fopen write mode value. Used internally. 364 | */ 365 | const FOPEN_WRITE_MODE = 'wb'; 366 | 367 | /** 368 | * @var string 369 | */ 370 | private $file_path; 371 | 372 | /** 373 | * @var resource file handle for AvroFile instance 374 | */ 375 | private $file_handle; 376 | 377 | /** 378 | * AvroFile constructor. 379 | * @param $file_path 380 | * @param string $mode 381 | * @throws AvroIOException 382 | */ 383 | public function __construct($file_path, $mode = self::READ_MODE) 384 | { 385 | /** 386 | * XXX: should we check for file existence (in case of reading) 387 | * or anything else about the provided file_path argument? 388 | */ 389 | $this->file_path = $file_path; 390 | switch ($mode) 391 | { 392 | case self::WRITE_MODE: 393 | $this->file_handle = fopen($this->file_path, self::FOPEN_WRITE_MODE); 394 | if (false == $this->file_handle) 395 | throw new AvroIOException('Could not open file for writing'); 396 | break; 397 | case self::READ_MODE: 398 | $this->file_handle = fopen($this->file_path, self::FOPEN_READ_MODE); 399 | if (false == $this->file_handle) 400 | throw new AvroIOException('Could not open file for reading'); 401 | break; 402 | default: 403 | throw new AvroIOException( 404 | sprintf("Only modes '%s' and '%s' allowed. You provided '%s'.", 405 | self::READ_MODE, self::WRITE_MODE, $mode)); 406 | } 407 | } 408 | 409 | /** 410 | * @param string $str 411 | * @return int count of bytes written 412 | * @throws AvroIOException if write failed. 413 | */ 414 | public function write($str) 415 | { 416 | $len = fwrite($this->file_handle, $str); 417 | if (false === $len) 418 | throw new AvroIOException(sprintf('Could not write to file')); 419 | return $len; 420 | } 421 | 422 | /** 423 | * @param int $len count of bytes to read. 424 | * @return string bytes read 425 | * @throws AvroIOException if length value is negative or if the read failed 426 | */ 427 | public function read($len) 428 | { 429 | if (0 > $len) 430 | throw new AvroIOException( 431 | sprintf("Invalid length value passed to read: %d", $len)); 432 | 433 | if (0 == $len) 434 | return ''; 435 | 436 | $bytes = fread($this->file_handle, $len); 437 | if (false === $bytes) 438 | throw new AvroIOException('Could not read from file'); 439 | return $bytes; 440 | } 441 | 442 | /** 443 | * @return int current position within the file 444 | * @throws AvroIOException 445 | */ 446 | public function tell() 447 | { 448 | $position = ftell($this->file_handle); 449 | if (false === $position) 450 | throw new AvroIOException('Could not execute tell on reader'); 451 | return $position; 452 | } 453 | 454 | /** 455 | * @param int $offset 456 | * @param int $whence 457 | * @return boolean true upon success 458 | * @throws AvroIOException if seek failed. 459 | * @see AvroIO::seek() 460 | */ 461 | public function seek($offset, $whence = SEEK_SET) 462 | { 463 | $res = fseek($this->file_handle, $offset, $whence); 464 | // Note: does not catch seeking beyond end of file 465 | if (-1 === $res) 466 | throw new AvroIOException( 467 | sprintf("Could not execute seek (offset = %d, whence = %d)", 468 | $offset, $whence)); 469 | return true; 470 | } 471 | 472 | /** 473 | * Closes the file. 474 | * @return boolean true if successful. 475 | * @throws AvroIOException if there was an error closing the file. 476 | */ 477 | public function close() 478 | { 479 | $res = fclose($this->file_handle); 480 | if (false === $res) 481 | throw new AvroIOException('Error closing file.'); 482 | return $res; 483 | } 484 | 485 | /** 486 | * @return boolean true if the pointer is at the end of the file, 487 | * and false otherwise. 488 | * @see AvroIO::is_eof() as behavior differs from feof() 489 | */ 490 | public function is_eof() 491 | { 492 | $this->read(1); 493 | if (feof($this->file_handle)) 494 | return true; 495 | $this->seek(-1, self::SEEK_CUR); 496 | return false; 497 | } 498 | 499 | /** 500 | * @return boolean true if the flush was successful. 501 | * @throws AvroIOException if there was an error flushing the file. 502 | */ 503 | public function flush() 504 | { 505 | $res = fflush($this->file_handle); 506 | if (false === $res) 507 | throw new AvroIOException('Could not flush file.'); 508 | return true; 509 | } 510 | 511 | } 512 | -------------------------------------------------------------------------------- /lib/avro/protocol.php: -------------------------------------------------------------------------------- 1 | real_parse(json_decode($json, true)); 48 | return $protocol; 49 | } 50 | 51 | /** 52 | * @param $avro 53 | * @throws AvroSchemaParseException 54 | */ 55 | function real_parse($avro) { 56 | $this->protocol = $avro["protocol"]; 57 | $this->namespace = $avro["namespace"]; 58 | $this->schemata = new AvroNamedSchemata(); 59 | $this->name = $avro["protocol"]; 60 | 61 | if (!is_null($avro["types"])) { 62 | $types = AvroSchema::real_parse($avro["types"], $this->namespace, $this->schemata); 63 | } 64 | 65 | if (!is_null($avro["messages"])) { 66 | foreach ($avro["messages"] as $messageName => $messageAvro) { 67 | $message = new AvroProtocolMessage($messageName, $messageAvro, $this); 68 | $this->messages[$messageName] = $message; 69 | } 70 | } 71 | } 72 | } 73 | 74 | /** 75 | * Class AvroProtocolMessage 76 | */ 77 | class AvroProtocolMessage 78 | { 79 | /** 80 | * @var AvroRecordSchema $request 81 | */ 82 | 83 | public $request; 84 | 85 | public $response; 86 | 87 | private $name; 88 | 89 | /** 90 | * AvroProtocolMessage constructor. 91 | * @param $name 92 | * @param $avro 93 | * @param $protocol 94 | */ 95 | public function __construct($name, $avro, $protocol) 96 | { 97 | $this->name = $name; 98 | $this->request = new AvroRecordSchema(new AvroName($name, null, $protocol->namespace), null, $avro['request'], $protocol->schemata, AvroSchema::REQUEST_SCHEMA); 99 | 100 | if (array_key_exists('response', $avro)) { 101 | $this->response = $protocol->schemata->schema_by_name(new AvroName($avro['response'], $protocol->namespace, $protocol->namespace)); 102 | if ($this->response == null) 103 | $this->response = new AvroPrimitiveSchema($avro['response']); 104 | } 105 | } 106 | } 107 | 108 | /** 109 | * Class AvroProtocolParseException 110 | */ 111 | class AvroProtocolParseException extends AvroException {}; 112 | -------------------------------------------------------------------------------- /lib/avro/util.php: -------------------------------------------------------------------------------- 1 | $v) 47 | { 48 | if ($i !== $k) 49 | return false; 50 | $i++; 51 | } 52 | return true; 53 | } 54 | return false; 55 | } 56 | 57 | /** 58 | * @param array $ary 59 | * @param string $key 60 | * @return mixed the value of $ary[$key] if it is set, 61 | * and null otherwise. 62 | */ 63 | static function array_value($ary, $key) 64 | { 65 | return isset($ary[$key]) ? $ary[$key] : null; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | lib 11 | 12 | 13 | 14 | 15 | test 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /test/DataFileTest.php: -------------------------------------------------------------------------------- 1 | data_files)) 42 | $this->data_files = array(); 43 | $data_file = "$data_file.".self::current_timestamp(); 44 | $full = join(DIRECTORY_SEPARATOR, array(TEST_TEMP_DIR, $data_file)); 45 | $this->data_files []= $full; 46 | return $full; 47 | } 48 | 49 | /** 50 | * @param $data_file 51 | */ 52 | protected static function remove_data_file($data_file) 53 | { 54 | if (file_exists($data_file)) 55 | unlink($data_file); 56 | } 57 | 58 | protected function remove_data_files() 59 | { 60 | if (self::REMOVE_DATA_FILES && $this->data_files) 61 | foreach ($this->data_files as $data_file) 62 | $this->remove_data_file($data_file); 63 | } 64 | 65 | protected function setUp(): void 66 | { 67 | if (!file_exists(TEST_TEMP_DIR)) 68 | mkdir(TEST_TEMP_DIR); 69 | $this->remove_data_files(); 70 | } 71 | 72 | protected function tearDown(): void 73 | { 74 | $this->remove_data_files(); 75 | } 76 | 77 | public function provider() 78 | { 79 | $values = []; 80 | foreach (AvroDataIO::valid_codecs() as $codec) { 81 | $values[] = [$codec]; 82 | } 83 | return $values; 84 | } 85 | 86 | /** 87 | * @dataProvider provider 88 | */ 89 | public function test_write_read_nothing_round_trip($codec) 90 | { 91 | $data_file = $this->add_data_file(sprintf('data-wr-nothing-null-%s.avr', $codec)); 92 | $writers_schema = '"null"'; 93 | $dw = AvroDataIO::open_file($data_file, 'w', $writers_schema, $codec); 94 | $dw->close(); 95 | 96 | $dr = AvroDataIO::open_file($data_file); 97 | $data = $dr->data(); 98 | $dr->close(); 99 | $this->assertEmpty($data); 100 | } 101 | 102 | /** 103 | * @dataProvider provider 104 | */ 105 | public function test_write_read_null_round_trip($codec) 106 | { 107 | $data_file = $this->add_data_file(sprintf('data-wr-null-%s.avr', $codec)); 108 | $writers_schema = '"null"'; 109 | $data = null; 110 | $dw = AvroDataIO::open_file($data_file, 'w', $writers_schema, $codec); 111 | $dw->append($data); 112 | $dw->close(); 113 | 114 | $dr = AvroDataIO::open_file($data_file); 115 | $read_data = $dr->data(); 116 | $read_data = reset($read_data); 117 | $dr->close(); 118 | $this->assertEquals($data, $read_data); 119 | } 120 | 121 | /** 122 | * @dataProvider provider 123 | */ 124 | public function test_write_read_string_round_trip($codec) 125 | { 126 | $data_file = $this->add_data_file(sprintf('data-wr-str-%s.avr', $codec)); 127 | $writers_schema = '"string"'; 128 | $data = 'foo'; 129 | $dw = AvroDataIO::open_file($data_file, 'w', $writers_schema, $codec); 130 | $dw->append($data); 131 | $dw->close(); 132 | 133 | $dr = AvroDataIO::open_file($data_file); 134 | $read_data = $dr->data(); 135 | $read_data = reset($read_data); 136 | $dr->close(); 137 | $this->assertEquals($data, $read_data); 138 | } 139 | 140 | /** 141 | * @dataProvider provider 142 | */ 143 | public function test_write_read_round_trip($codec) 144 | { 145 | $data_file = $this->add_data_file(sprintf('data-wr-int-%s.avr', $codec)); 146 | $writers_schema = '"int"'; 147 | $data = 1; 148 | 149 | $dw = AvroDataIO::open_file($data_file, 'w', $writers_schema, $codec); 150 | $dw->append(1); 151 | $dw->close(); 152 | 153 | $dr = AvroDataIO::open_file($data_file); 154 | $read_data = $dr->data(); 155 | $read_data = reset($read_data); 156 | $dr->close(); 157 | $this->assertEquals($data, $read_data); 158 | } 159 | 160 | /** 161 | * @dataProvider provider 162 | */ 163 | public function test_write_read_true_round_trip($codec) 164 | { 165 | $data_file = $this->add_data_file(sprintf('data-wr-true-%s.avr', $codec)); 166 | $writers_schema = '"boolean"'; 167 | $datum = true; 168 | $dw = AvroDataIO::open_file($data_file, 'w', $writers_schema, $codec); 169 | $dw->append($datum); 170 | $dw->close(); 171 | 172 | $dr = AvroDataIO::open_file($data_file); 173 | $read_data = $dr->data(); 174 | $read_data = reset($read_data); 175 | $dr->close(); 176 | $this->assertEquals($datum, $read_data); 177 | } 178 | 179 | /** 180 | * @dataProvider provider 181 | */ 182 | public function test_write_read_false_round_trip($codec) 183 | { 184 | $data_file = $this->add_data_file(sprintf('data-wr-false-%s.avr', $codec)); 185 | $writers_schema = '"boolean"'; 186 | $datum = false; 187 | $dw = AvroDataIO::open_file($data_file, 'w', $writers_schema, $codec); 188 | $dw->append($datum); 189 | $dw->close(); 190 | 191 | $dr = AvroDataIO::open_file($data_file); 192 | $read_data = $dr->data(); 193 | $read_data = reset($read_data); 194 | $dr->close(); 195 | $this->assertEquals($datum, $read_data); 196 | } 197 | 198 | /** 199 | * @dataProvider provider 200 | */ 201 | public function test_write_read_int_array_round_trip($codec) 202 | { 203 | $data_file = $this->add_data_file(sprintf('data-wr-int-ary-%s.avr', $codec)); 204 | $writers_schema = '"int"'; 205 | $data = array(10, 20, 30, 40, 50, 60, 70, 567, 89012345); 206 | $dw = AvroDataIO::open_file($data_file, 'w', $writers_schema, $codec); 207 | foreach ($data as $datum) 208 | $dw->append($datum); 209 | $dw->close(); 210 | 211 | $dr = AvroDataIO::open_file($data_file); 212 | $read_data = $dr->data(); 213 | $dr->close(); 214 | $this->assertEquals($data, $read_data, 215 | sprintf("in: %s\nout: %s", 216 | json_encode($data), json_encode($read_data))); 217 | } 218 | 219 | /** 220 | * @dataProvider provider 221 | */ 222 | public function test_differing_schemas_with_primitives($codec) 223 | { 224 | $data_file = $this->add_data_file(sprintf('data-prim-%s.avr', $codec)); 225 | 226 | $writer_schema = << 'john', 'age' => 25, 'verified' => true), 236 | array('username' => 'ryan', 'age' => 23, 'verified' => false), 237 | array('username' => 'bill', 'age' => 35)); 238 | $dw = AvroDataIO::open_file($data_file, 'w', $writer_schema, $codec); 239 | foreach ($data as $datum) 240 | { 241 | $dw->append($datum); 242 | } 243 | $dw->close(); 244 | $reader_schema = <<data() as $index => $record) 253 | { 254 | $this->assertEquals($data[$index]['username'], $record['username']); 255 | } 256 | } 257 | 258 | /** 259 | * @dataProvider provider 260 | */ 261 | public function test_differing_schemas_with_complex_objects($codec) 262 | { 263 | $data_file = $this->add_data_file(sprintf('data-complex-%s.avr', $codec)); 264 | 265 | $writers_schema = << "john", 286 | "something_fixed" => "foo", 287 | "something_enum" => "hello", 288 | "something_array" => array(1,2,3), 289 | "something_map" => array("a" => 1, "b" => 2), 290 | "something_record" => array("inner" => 2), 291 | "something_error" => array("code" => 403)), 292 | array("username" => "ryan", 293 | "something_fixed" => "bar", 294 | "something_enum" => "goodbye", 295 | "something_array" => array(1,2,3), 296 | "something_map" => array("a" => 2, "b" => 6), 297 | "something_record" => array("inner" => 1), 298 | "something_error" => array("code" => 401))); 299 | $dw = AvroDataIO::open_file($data_file, 'w', $writers_schema, $codec); 300 | foreach ($data as $datum) 301 | $dw->append($datum); 302 | $dw->close(); 303 | 304 | foreach (array('fixed', 'enum', 'record', 'error', 305 | 'array', 'map', 'union') as $s) 306 | { 307 | $readers_schema = json_decode($writers_schema, true); 308 | $dr = AvroDataIO::open_file($data_file, 'r', json_encode($readers_schema)); 309 | foreach ($dr->data() as $idx => $obj) 310 | { 311 | foreach ($readers_schema['fields'] as $field) 312 | { 313 | $field_name = $field['name']; 314 | $this->assertEquals($data[$idx][$field_name], $obj[$field_name]); 315 | } 316 | } 317 | $dr->close(); 318 | 319 | } 320 | 321 | } 322 | 323 | } 324 | -------------------------------------------------------------------------------- /test/DatumIOTest.php: -------------------------------------------------------------------------------- 1 | write($datum, $encoder); 41 | $output = strval($written); 42 | $this->assertEquals($binary, $output, 43 | sprintf("expected: %s\n actual: %s", 44 | AvroDebug::ascii_string($binary, 'hex'), 45 | AvroDebug::ascii_string($output, 'hex'))); 46 | 47 | $read = new AvroStringIO($binary); 48 | $decoder = new AvroIOBinaryDecoder($read); 49 | $reader = new AvroIODatumReader($schema); 50 | $read_datum = $reader->read($decoder); 51 | $this->assertEquals($datum, $read_datum); 52 | } 53 | 54 | /** 55 | * @dataProvider zigzag_unsigned_right_shift_provider 56 | */ 57 | function test_zigzag_unsigned_right_shift(int $expected, int $n, int $x) { 58 | $this->assertEquals($expected, Zigzag::unsigned_right_shift($n, $x)); 59 | } 60 | 61 | public static function zigzag_unsigned_right_shift_provider(): array { 62 | return [ 63 | [4611686018427387902, -8, 2], 64 | [2, 8, 2], 65 | [144115188075855871, -2, 7], 66 | [1125899906842623, 144115188075855871, 7], 67 | [8796093022207, 1125899906842623, 7], 68 | [68719476735, 8796093022207, 7], 69 | [536870911, 68719476735, 7], 70 | [4194303, 536870911, 7], 71 | [32767, 4194303, 7], 72 | [255, 32767, 7], 73 | [1, 255, 7], 74 | [144115188059078656, -2147483648, 7], 75 | [1125899906711552, 144115188059078656, 7], 76 | [8796093021184, 1125899906711552, 7], 77 | [68719476728, 8796093021184, 7], 78 | [536870911, 68719476728, 7], 79 | [4194303, 536870911, 7], 80 | [32767, 4194303, 7], 81 | [255, 32767, 7], 82 | ]; 83 | } 84 | 85 | /** 86 | * @return array 87 | */ 88 | function data_provider() 89 | { 90 | return array(array('"null"', null, ''), 91 | 92 | array('"boolean"', true, "\001"), 93 | array('"boolean"', false, "\000"), 94 | 95 | array('"int"', (int) -2147483648, "\xFF\xFF\xFF\xFF\x0F"), 96 | array('"int"', -1, "\001"), 97 | array('"int"', 0, "\000"), 98 | array('"int"', 1, "\002"), 99 | array('"int"', 2147483647, "\xFE\xFF\xFF\xFF\x0F"), 100 | 101 | array('"long"', (int) -9223372036854775808, "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x01"), 102 | array('"long"', -(1<<62), "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x7F"), 103 | array('"long"', -4294967295, "\xFD\xFF\xFF\xFF\x1F"), 104 | array('"long"', -10, "\x13"), 105 | array('"long"', -3, "\005"), 106 | array('"long"', -2, "\003"), 107 | array('"long"', -1, "\001"), 108 | array('"long"', 0, "\000"), 109 | array('"long"', 1, "\002"), 110 | array('"long"', 2, "\004"), 111 | array('"long"', 3, "\006"), 112 | array('"long"', 10, "\x14"), 113 | array('"long"', 4294967295, "\xFE\xFF\xFF\xFF\x1F"), 114 | array('"long"', 1<<62, "\x80\x80\x80\x80\x80\x80\x80\x80\x80\x01"), 115 | array('"long"', 9223372036854775807, "\xFE\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x01"), 116 | 117 | array('"float"', (float) -10.0, "\000\000 \301"), 118 | array('"float"', (float) -1.0, "\000\000\200\277"), 119 | array('"float"', (float) 0.0, "\000\000\000\000"), 120 | array('"float"', (float) 2.0, "\000\000\000@"), 121 | array('"float"', (float) 9.0, "\000\000\020A"), 122 | 123 | array('"double"', (double) -10.0, "\000\000\000\000\000\000$\300"), 124 | array('"double"', (double) -1.0, "\000\000\000\000\000\000\360\277"), 125 | array('"double"', (double) 0.0, "\000\000\000\000\000\000\000\000"), 126 | array('"double"', (double) 2.0, "\000\000\000\000\000\000\000@"), 127 | array('"double"', (double) 9.0, "\000\000\000\000\000\000\"@"), 128 | 129 | array('"string"', 'foo', "\x06foo"), 130 | array('"bytes"', "\x01\x02\x03", "\x06\x01\x02\x03"), 131 | 132 | array('{"type":"array","items":"int"}', 133 | array(1,2,3), 134 | "\x06\x02\x04\x06\x00"), 135 | array('{"type":"map","values":"int"}', 136 | array('foo' => 1, 'bar' => 2, 'baz' => 3), 137 | "\x06\x06foo\x02\x06bar\x04\x06baz\x06\x00"), 138 | array('["null", "int"]', 1, "\x02\x02"), 139 | array('{"name":"fix","type":"fixed","size":3}', 140 | "\xAA\xBB\xCC", "\xAA\xBB\xCC"), 141 | array('{"name":"enm","type":"enum","symbols":["A","B","C"]}', 142 | 'B', "\x02"), 143 | array('{"name":"rec","type":"record","fields":[{"name":"a","type":"int"},{"name":"b","type":"boolean"}]}', 144 | array('a' => 1, 'b' => false), 145 | "\x02\x00"), 146 | array('{"type":"bytes","logicalType": "decimal","precision": 4,"scale": 1}', 147 | '1', 148 | "\x02\x0a"), 149 | array('{"type":"bytes","logicalType": "decimal","precision": 4,"scale": 1}', 150 | '-0.1', 151 | "\x02\xff"), 152 | array('{"type":"bytes","logicalType": "decimal","precision": 4,"scale": 1}', 153 | -0.1, 154 | "\x02\xff"), 155 | array('{"type":"bytes","logicalType": "decimal","precision": 4,"scale": 1}', 156 | 3.1, 157 | "\x02\x1f"), 158 | array('{"type":"bytes","logicalType": "decimal","precision": 4,"scale": 2}', 159 | 2.55, 160 | "\x04\x00\xff"), 161 | array('{"type":"bytes","logicalType": "decimal","precision": 4,"scale": 0}', 162 | -256, 163 | "\x04\xff\x00"), 164 | array('{"type":"bytes","logicalType": "decimal","precision": 4,"scale": 3}', 165 | 0.127, 166 | "\x02\x7f"), 167 | array('{"type":"bytes","logicalType": "decimal","precision": 19,"scale": 0}', 168 | PHP_INT_MAX, 169 | "\x10\x7f\xff\xff\xff\xff\xff\xff\xff"), 170 | array('{"type":"bytes","logicalType": "decimal","precision": 19,"scale": 0}', 171 | PHP_INT_MIN, 172 | "\x10\x80\x00\x00\x00\x00\x00\x00\x00") 173 | ); 174 | } 175 | 176 | /** 177 | * @return array 178 | */ 179 | function default_provider() 180 | { 181 | return array(array('"null"', 'null', null), 182 | array('"boolean"', 'true', true), 183 | array('"int"', '1', 1), 184 | array('"long"', '2000', 2000), 185 | array('"float"', '1.1', (float) 1.1), 186 | array('"double"', '200.2', (double) 200.2), 187 | array('"string"', '"quux"', 'quux'), 188 | array('"bytes"', '"\u00FF"', "\xC3\xBF"), 189 | array('{"type":"array","items":"int"}', 190 | '[5,4,3,2]', array(5,4,3,2)), 191 | array('{"type":"map","values":"int"}', 192 | '{"a":9}', array('a' => 9)), 193 | array('["int","string"]', '8', 8), 194 | array('{"name":"x","type":"enum","symbols":["A","V"]}', 195 | '"A"', 'A'), 196 | array('{"name":"x","type":"fixed","size":4}', '"\u00ff"', "\xC3\xBF"), 197 | array('{"name":"x","type":"record","fields":[{"name":"label","type":"int"}]}', 198 | '{"label":7}', array('label' => 7))); 199 | } 200 | 201 | /** 202 | * @dataProvider default_provider 203 | * @param $field_schema_json 204 | * @param $default_json 205 | * @param $default_value 206 | */ 207 | function test_field_default_value($field_schema_json, 208 | $default_json, $default_value) 209 | { 210 | $writers_schema_json = '{"name":"foo","type":"record","fields":[]}'; 211 | $writers_schema = AvroSchema::parse($writers_schema_json); 212 | 213 | $readers_schema_json = sprintf( 214 | '{"name":"foo","type":"record","fields":[{"name":"f","type":%s,"default":%s}]}', 215 | $field_schema_json, $default_json); 216 | $readers_schema = AvroSchema::parse($readers_schema_json); 217 | 218 | $reader = new AvroIODatumReader($writers_schema, $readers_schema); 219 | $record = $reader->read(new AvroIOBinaryDecoder(new AvroStringIO())); 220 | if (array_key_exists('f', $record)) 221 | $this->assertEquals($default_value, $record['f']); 222 | else 223 | $this->assertTrue(false, sprintf('expected field record[f]: %s', 224 | print_r($record, true))) ; 225 | } 226 | 227 | } 228 | -------------------------------------------------------------------------------- /test/FileIOTest.php: -------------------------------------------------------------------------------- 1 | getTmpFile(); 30 | if (file_exists($file)) 31 | unlink($file); 32 | } 33 | 34 | private function getFileName() 35 | { 36 | return __DIR__ . '/data/users.avro'; 37 | } 38 | 39 | private function getTmpFile() 40 | { 41 | return __DIR__ . '/tmp/users.avro'; 42 | } 43 | 44 | private function read() 45 | { 46 | $fileName = $this->getFileName(); 47 | $reader = AvroDataIO::open_file($fileName); 48 | return $reader->data(); 49 | } 50 | 51 | private function readIterator() 52 | { 53 | $fileName = $this->getFileName(); 54 | $reader = AvroDataIO::open_file($fileName); 55 | $data = []; 56 | foreach ($reader->data_iterator() as $row) { 57 | $data[] = $row; 58 | } 59 | return $data; 60 | } 61 | 62 | public function testReading() 63 | { 64 | $expected = [ 65 | [ 66 | 'name' => 'Alyssa', 67 | 'favorite_color' => null, 68 | 'favorite_numbers' => [3, 9, 15, 20], 69 | ], 70 | [ 71 | 'name' => 'Ben', 72 | 'favorite_color' => 'red', 73 | 'favorite_numbers' => [], 74 | ] 75 | ]; 76 | 77 | // Classic loading. 78 | $this->assertEquals($expected, $this->read()); 79 | 80 | // Iterator loading. 81 | $this->assertEquals($expected, $this->readIterator()); 82 | } 83 | 84 | /** 85 | * Doesn't work because due to Avro format peculiarities mean that no two 86 | * encodings of the same data will be binary equal. 87 | */ 88 | public function disabled_testRoundTrip() 89 | { 90 | $inFile = $this->getFileName(); 91 | $outFile = $this->getTmpFile(); 92 | $schemaFile = __DIR__ . '/data/user.avsc'; 93 | $data = $this->read(); 94 | $schema = file_get_contents($schemaFile); 95 | $writer = AvroDataIO::open_file($outFile, 'w', $schema); 96 | foreach ($data as $record) 97 | { 98 | $writer->append($record); 99 | } 100 | $writer->close(); 101 | 102 | $oldData = file_get_contents($inFile); 103 | $newData = file_get_contents($outFile); 104 | if ($oldData !== $newData) 105 | { 106 | $diff = shell_exec("bash -c \"diff -y -W 150 <(xxd '$inFile') <(xxd '$outFile')\""); 107 | $this->fail("Round trip failed, files not equal:\n$diff"); 108 | } 109 | $this->assertTrue(true, 'Dummy assert to prevent this test from being marked as risky'); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /test/FloatIntEncodingTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(is_float(self::$FLOAT_NAN), 'float NaN is a float'); 68 | $this->assertTrue(is_nan(self::$FLOAT_NAN), 'float NaN is NaN'); 69 | $this->assertFalse(is_infinite(self::$FLOAT_NAN), 'float NaN is not infinite'); 70 | 71 | $this->assertTrue(is_float(self::$FLOAT_POS_INF), 'float pos infinity is a float'); 72 | $this->assertTrue(is_infinite(self::$FLOAT_POS_INF), 'float pos infinity is infinite'); 73 | $this->assertTrue(0 < self::$FLOAT_POS_INF, 'float pos infinity is greater than 0'); 74 | $this->assertFalse(is_nan(self::$FLOAT_POS_INF), 'float pos infinity is not NaN'); 75 | 76 | $this->assertTrue(is_float(self::$FLOAT_NEG_INF), 'float neg infinity is a float'); 77 | $this->assertTrue(is_infinite(self::$FLOAT_NEG_INF), 'float neg infinity is infinite'); 78 | $this->assertTrue(0 > self::$FLOAT_NEG_INF, 'float neg infinity is less than 0'); 79 | $this->assertFalse(is_nan(self::$FLOAT_NEG_INF), 'float neg infinity is not NaN'); 80 | 81 | $this->assertTrue(is_double(self::$DOUBLE_NAN), 'double NaN is a double'); 82 | $this->assertTrue(is_nan(self::$DOUBLE_NAN), 'double NaN is NaN'); 83 | $this->assertFalse(is_infinite(self::$DOUBLE_NAN), 'double NaN is not infinite'); 84 | 85 | $this->assertTrue(is_double(self::$DOUBLE_POS_INF), 'double pos infinity is a double'); 86 | $this->assertTrue(is_infinite(self::$DOUBLE_POS_INF), 'double pos infinity is infinite'); 87 | $this->assertTrue(0 < self::$DOUBLE_POS_INF, 'double pos infinity is greater than 0'); 88 | $this->assertFalse(is_nan(self::$DOUBLE_POS_INF), 'double pos infinity is not NaN'); 89 | 90 | $this->assertTrue(is_double(self::$DOUBLE_NEG_INF), 'double neg infinity is a double'); 91 | $this->assertTrue(is_infinite(self::$DOUBLE_NEG_INF), 'double neg infinity is infinite'); 92 | $this->assertTrue(0 > self::$DOUBLE_NEG_INF, 'double neg infinity is less than 0'); 93 | $this->assertFalse(is_nan(self::$DOUBLE_NEG_INF), 'double neg infinity is not NaN'); 94 | 95 | } 96 | 97 | /** 98 | * @return array 99 | */ 100 | function special_vals_provider() 101 | { 102 | self::make_special_vals(); 103 | return array(array(self::DOUBLE_TYPE, self::$DOUBLE_POS_INF, self::$LONG_BITS_POS_INF), 104 | array(self::DOUBLE_TYPE, self::$DOUBLE_NEG_INF, self::$LONG_BITS_NEG_INF), 105 | array(self::FLOAT_TYPE, self::$FLOAT_POS_INF, self::$INT_BITS_POS_INF), 106 | array(self::FLOAT_TYPE, self::$FLOAT_NEG_INF, self::$INT_BITS_NEG_INF)); 107 | } 108 | 109 | /** 110 | * @dataProvider special_vals_provider 111 | * @param $type 112 | * @param $val 113 | * @param $bits 114 | */ 115 | function test_encoding_special_values($type, $val, $bits) 116 | { 117 | $this->assert_encode_values($type, $val, $bits); 118 | } 119 | 120 | /** 121 | * @return array 122 | */ 123 | function nan_vals_provider() 124 | { 125 | self::make_special_vals(); 126 | return array(array(self::DOUBLE_TYPE, self::$DOUBLE_NAN, self::$LONG_BITS_NAN), 127 | array(self::FLOAT_TYPE, self::$FLOAT_NAN, self::$INT_BITS_NAN)); 128 | } 129 | 130 | /** 131 | * @dataProvider nan_vals_provider 132 | * @param $type 133 | * @param $val 134 | * @param $bits 135 | */ 136 | function test_encoding_nan_values($type, $val, $bits) 137 | { 138 | $this->assert_encode_nan_values($type, $val, $bits); 139 | } 140 | 141 | /** 142 | * @return array 143 | */ 144 | function normal_vals_provider() 145 | { 146 | return array( 147 | array(self::DOUBLE_TYPE, (double) -10, "\000\000\000\000\000\000$\300", '000000000000420c'), 148 | array(self::DOUBLE_TYPE, (double) -9, "\000\000\000\000\000\000\"\300", '000000000000220c'), 149 | array(self::DOUBLE_TYPE, (double) -8, "\000\000\000\000\000\000 \300", '000000000000020c'), 150 | array(self::DOUBLE_TYPE, (double) -7, "\000\000\000\000\000\000\034\300", '000000000000c10c'), 151 | array(self::DOUBLE_TYPE, (double) -6, "\000\000\000\000\000\000\030\300", '000000000000810c'), 152 | array(self::DOUBLE_TYPE, (double) -5, "\000\000\000\000\000\000\024\300", '000000000000410c'), 153 | array(self::DOUBLE_TYPE, (double) -4, "\000\000\000\000\000\000\020\300", '000000000000010c'), 154 | /**/ array(self::DOUBLE_TYPE, (double) -3, "\000\000\000\000\000\000\010\300", '000000000000800c'), 155 | array(self::DOUBLE_TYPE, (double) -2, "\000\000\000\000\000\000\000\300", '000000000000000c'), 156 | array(self::DOUBLE_TYPE, (double) -1, "\000\000\000\000\000\000\360\277", '0000000000000ffb'), 157 | array(self::DOUBLE_TYPE, (double) 0, "\000\000\000\000\000\000\000\000", '0000000000000000'), 158 | array(self::DOUBLE_TYPE, (double) 1, "\000\000\000\000\000\000\360?", '0000000000000ff3'), 159 | array(self::DOUBLE_TYPE, (double) 2, "\000\000\000\000\000\000\000@", '0000000000000004'), 160 | /**/ array(self::DOUBLE_TYPE, (double) 3, "\000\000\000\000\000\000\010@", '0000000000008004'), 161 | array(self::DOUBLE_TYPE, (double) 4, "\000\000\000\000\000\000\020@", '0000000000000104'), 162 | array(self::DOUBLE_TYPE, (double) 5, "\000\000\000\000\000\000\024@", '0000000000004104'), 163 | array(self::DOUBLE_TYPE, (double) 6, "\000\000\000\000\000\000\030@", '0000000000008104'), 164 | array(self::DOUBLE_TYPE, (double) 7, "\000\000\000\000\000\000\034@", '000000000000c104'), 165 | array(self::DOUBLE_TYPE, (double) 8, "\000\000\000\000\000\000 @", '0000000000000204'), 166 | array(self::DOUBLE_TYPE, (double) 9, "\000\000\000\000\000\000\"@", '0000000000002204'), 167 | array(self::DOUBLE_TYPE, (double) 10, "\000\000\000\000\000\000$@", '0000000000004204'), 168 | /**/ array(self::DOUBLE_TYPE, (double) -1234.2132, "\007\316\031Q\332H\223\300", '70ec9115ad84390c'), 169 | array(self::DOUBLE_TYPE, (double) -2.11e+25, "\311\260\276J\031t1\305", '9c0beba49147135c'), 170 | 171 | array(self::FLOAT_TYPE, (float) -10, "\000\000 \301", '0000021c'), 172 | array(self::FLOAT_TYPE, (float) -9, "\000\000\020\301", '0000011c'), 173 | array(self::FLOAT_TYPE, (float) -8, "\000\000\000\301", '0000001c'), 174 | array(self::FLOAT_TYPE, (float) -7, "\000\000\340\300", '00000e0c'), 175 | array(self::FLOAT_TYPE, (float) -6, "\000\000\300\300", '00000c0c'), 176 | array(self::FLOAT_TYPE, (float) -5, "\000\000\240\300", '00000a0c'), 177 | array(self::FLOAT_TYPE, (float) -4, "\000\000\200\300", '0000080c'), 178 | array(self::FLOAT_TYPE, (float) -3, "\000\000@\300", '0000040c'), 179 | array(self::FLOAT_TYPE, (float) -2, "\000\000\000\300", '0000000c'), 180 | array(self::FLOAT_TYPE, (float) -1, "\000\000\200\277", '000008fb'), 181 | array(self::FLOAT_TYPE, (float) 0, "\000\000\000\000", '00000000'), 182 | array(self::FLOAT_TYPE, (float) 1, "\000\000\200?", '000008f3'), 183 | array(self::FLOAT_TYPE, (float) 2, "\000\000\000@", '00000004'), 184 | array(self::FLOAT_TYPE, (float) 3, "\000\000@@", '00000404'), 185 | array(self::FLOAT_TYPE, (float) 4, "\000\000\200@", '00000804'), 186 | array(self::FLOAT_TYPE, (float) 5, "\000\000\240@", '00000a04'), 187 | array(self::FLOAT_TYPE, (float) 6, "\000\000\300@", '00000c04'), 188 | array(self::FLOAT_TYPE, (float) 7, "\000\000\340@", '00000e04'), 189 | array(self::FLOAT_TYPE, (float) 8, "\000\000\000A", '00000014'), 190 | array(self::FLOAT_TYPE, (float) 9, "\000\000\020A", '00000114'), 191 | array(self::FLOAT_TYPE, (float) 10, "\000\000 A", '00000214'), 192 | array(self::FLOAT_TYPE, (float) -1234.5, "\000P\232\304", '0005a94c'), 193 | array(self::FLOAT_TYPE, (float) -211300000.0, "\352\202I\315", 'ae2894dc'), 194 | ); 195 | } 196 | 197 | /** 198 | * @return array 199 | */ 200 | function float_vals_provider() 201 | { 202 | $ary = array(); 203 | 204 | foreach ($this->normal_vals_provider() as $values) 205 | if (self::FLOAT_TYPE == $values[0]) 206 | $ary []= array($values[0], $values[1], $values[2]); 207 | 208 | return $ary; 209 | } 210 | 211 | /** 212 | * @return array 213 | */ 214 | function double_vals_provider() 215 | { 216 | $ary = array(); 217 | 218 | foreach ($this->normal_vals_provider() as $values) 219 | if (self::DOUBLE_TYPE == $values[0]) 220 | $ary []= array($values[0], $values[1], $values[2]); 221 | 222 | return $ary; 223 | } 224 | 225 | 226 | /** 227 | * @dataProvider float_vals_provider 228 | * @param $type 229 | * @param $val 230 | * @param $bits 231 | */ 232 | function test_encoding_float_values($type, $val, $bits) 233 | { 234 | $this->assert_encode_values($type, $val, $bits); 235 | } 236 | 237 | /** 238 | * @dataProvider double_vals_provider 239 | * @param $type 240 | * @param $val 241 | * @param $bits 242 | */ 243 | function test_encoding_double_values($type, $val, $bits) 244 | { 245 | $this->assert_encode_values($type, $val, $bits); 246 | } 247 | 248 | /** 249 | * @param $type 250 | * @param $val 251 | * @param $bits 252 | */ 253 | function assert_encode_values($type, $val, $bits) 254 | { 255 | if (self::FLOAT_TYPE == $type) 256 | { 257 | $decoder = array('AvroIOBinaryDecoder', 'int_bits_to_float'); 258 | $encoder = array('AvroIOBinaryEncoder', 'float_to_int_bits'); 259 | } 260 | else 261 | { 262 | $decoder = array('AvroIOBinaryDecoder', 'long_bits_to_double'); 263 | $encoder = array('AvroIOBinaryEncoder', 'double_to_long_bits'); 264 | } 265 | 266 | $decoded_bits_val = call_user_func($decoder, $bits); 267 | $this->assertEquals($val, $decoded_bits_val, 268 | sprintf("%s\n expected: '%f'\n given: '%f'", 269 | 'DECODED BITS', $val, $decoded_bits_val)); 270 | 271 | $encoded_val_bits = call_user_func($encoder, $val); 272 | $this->assertEquals($bits, $encoded_val_bits, 273 | sprintf("%s\n expected: '%s'\n given: '%s'", 274 | 'ENCODED VAL', 275 | AvroDebug::hex_string($bits), 276 | AvroDebug::hex_string($encoded_val_bits))); 277 | 278 | $round_trip_value = call_user_func($decoder, $encoded_val_bits); 279 | $this->assertEquals($val, $round_trip_value, 280 | sprintf("%s\n expected: '%f'\n given: '%f'", 281 | 'ROUND TRIP BITS', $val, $round_trip_value)); 282 | } 283 | 284 | /** 285 | * @param $type 286 | * @param $val 287 | * @param $bits 288 | */ 289 | function assert_encode_nan_values($type, $val, $bits) 290 | { 291 | if (self::FLOAT_TYPE == $type) 292 | { 293 | $decoder = array('AvroIOBinaryDecoder', 'int_bits_to_float'); 294 | $encoder = array('AvroIOBinaryEncoder', 'float_to_int_bits'); 295 | } 296 | else 297 | { 298 | $decoder = array('AvroIOBinaryDecoder', 'long_bits_to_double'); 299 | $encoder = array('AvroIOBinaryEncoder', 'double_to_long_bits'); 300 | } 301 | 302 | $decoded_bits_val = call_user_func($decoder, $bits); 303 | $this->assertTrue(is_nan($decoded_bits_val), 304 | sprintf("%s\n expected: '%f'\n given: '%f'", 305 | 'DECODED BITS', $val, $decoded_bits_val)); 306 | 307 | $encoded_val_bits = call_user_func($encoder, $val); 308 | $this->assertEquals($bits, $encoded_val_bits, 309 | sprintf("%s\n expected: '%s'\n given: '%s'", 310 | 'ENCODED VAL', 311 | AvroDebug::hex_string($bits), 312 | AvroDebug::hex_string($encoded_val_bits))); 313 | 314 | $round_trip_value = call_user_func($decoder, $encoded_val_bits); 315 | $this->assertTrue(is_nan($round_trip_value), 316 | sprintf("%s\n expected: '%f'\n given: '%f'", 317 | 'ROUND TRIP BITS', $val, $round_trip_value)); 318 | } 319 | 320 | } 321 | -------------------------------------------------------------------------------- /test/IODatumReaderTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(AvroIODatumReader::schemas_match( 36 | AvroSchema::parse($writers_schema), 37 | AvroSchema::parse($readers_schema))); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /test/LongEncodingTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('Requires 64-bit platform'); 40 | } 41 | 42 | function skip_if_no_gmp() 43 | { 44 | if (!extension_loaded('gmp')) 45 | $this->markTestSkipped('Requires GMP PHP Extension.'); 46 | } 47 | 48 | /** 49 | * @param $expected 50 | * @param $actual 51 | * @param $shift_type 52 | * @param $expected_binary 53 | * @param $actual_binary 54 | */ 55 | function assert_bit_shift($expected, $actual, $shift_type, 56 | $expected_binary, $actual_binary) 57 | { 58 | $this->assertEquals( 59 | $expected, $actual, 60 | sprintf("%s\nexpected: %d\n actual: %d\nexpected b: %s\n actual b: %s", 61 | $shift_type, $expected, $actual, 62 | $expected_binary, $actual_binary)); 63 | } 64 | 65 | /** 66 | * @dataProvider bit_shift_provider 67 | * @param $val 68 | * @param $shift 69 | * @param $expected_lval 70 | * @param $expected_rval 71 | * @param $lbin 72 | * @param $rbin 73 | */ 74 | function test_bit_shift($val, $shift, $expected_lval, $expected_rval, $lbin, $rbin) 75 | { 76 | 77 | $this->skip_64_bit_test_on_32_bit(); 78 | 79 | $lval = (int) ((int) $val << $shift); 80 | $this->assert_bit_shift($expected_lval, strval($lval), 81 | 'lshift', $lbin, decbin($lval)); 82 | $rval = ((int) $val >> $shift); 83 | $this->assert_bit_shift($expected_rval, strval($rval), 84 | 'rshift', $rbin, decbin($rval)); 85 | } 86 | 87 | /** 88 | * @dataProvider bit_shift_provider 89 | * @param $val 90 | * @param $shift 91 | * @param $expected_lval 92 | * @param $expected_rval 93 | * @param $lbin 94 | * @param $rbin 95 | */ 96 | function test_left_shift_gmp($val, $shift, 97 | $expected_lval, $expected_rval, 98 | $lbin, $rbin) 99 | { 100 | $this->skip_if_no_gmp(); 101 | $lval = gmp_strval(AvroGMP::shift_left($val, $shift)); 102 | $this->assert_bit_shift($expected_lval, $lval, 'gmp left shift', 103 | $lbin, decbin((int) $lval)); 104 | } 105 | 106 | /** 107 | * @dataProvider bit_shift_provider 108 | * @param $val 109 | * @param $shift 110 | * @param $expected_lval 111 | * @param $expected_rval 112 | * @param $lbin 113 | * @param $rbin 114 | */ 115 | function test_right_shift_gmp($val, $shift, $expected_lval, $expected_rval, 116 | $lbin, $rbin) 117 | { 118 | $this->skip_if_no_gmp(); 119 | $rval = gmp_strval(AvroGMP::shift_right($val, $shift)); 120 | $this->assert_bit_shift($expected_rval, $rval, 'gmp right shift', 121 | $rbin, decbin((int) $rval)); 122 | } 123 | 124 | /** 125 | * @dataProvider long_provider 126 | * @param $val 127 | * @param $expected_bytes 128 | */ 129 | function test_encode_long($val, $expected_bytes) 130 | { 131 | $this->skip_64_bit_test_on_32_bit(); 132 | $bytes = AvroIOBinaryEncoder::encode_long($val); 133 | $this->assertEquals($expected_bytes, $bytes); 134 | } 135 | 136 | /** 137 | * @dataProvider long_provider 138 | * @param $val 139 | * @param $expected_bytes 140 | */ 141 | function test_gmp_encode_long($val, $expected_bytes) 142 | { 143 | $this->skip_if_no_gmp(); 144 | $bytes = AvroGMP::encode_long($val); 145 | $this->assertEquals($expected_bytes, $bytes); 146 | } 147 | 148 | /** 149 | * @dataProvider long_provider 150 | * @param $expected_val 151 | * @param $bytes 152 | */ 153 | function test_decode_long_from_array($expected_val, $bytes) 154 | { 155 | $this->skip_64_bit_test_on_32_bit(); 156 | $ary = array_map('ord', str_split($bytes)); 157 | $val = AvroIOBinaryDecoder::decode_long_from_array($ary); 158 | $this->assertEquals($expected_val, $val); 159 | } 160 | 161 | /** 162 | * @dataProvider long_provider 163 | * @param $expected_val 164 | * @param $bytes 165 | */ 166 | function test_gmp_decode_long_from_array($expected_val, $bytes) 167 | { 168 | $this->skip_if_no_gmp(); 169 | $ary = array_map('ord', str_split($bytes)); 170 | $val = AvroGMP::decode_long_from_array($ary); 171 | $this->assertEquals($expected_val, $val); 172 | } 173 | 174 | /** 175 | * @return array 176 | */ 177 | function long_provider() 178 | { 179 | return array(array('0', "\x0"), 180 | array('1', "\x2"), 181 | array('7', "\xe"), 182 | array('10000', "\xa0\x9c\x1"), 183 | array('2147483647', "\xfe\xff\xff\xff\xf"), 184 | array('98765432109', "\xda\x94\x87\xee\xdf\x5"), 185 | array('-1', "\x1"), 186 | array('-7', "\xd"), 187 | array('-10000', "\x9f\x9c\x1"), 188 | array('-2147483648', "\xff\xff\xff\xff\xf"), 189 | array('-98765432109', "\xd9\x94\x87\xee\xdf\x5") 190 | ); 191 | 192 | } 193 | 194 | /** 195 | * @return array 196 | */ 197 | function bit_shift_provider() 198 | { 199 | // val shift lval rval 200 | return array( 201 | array('0', 0, '0', '0', 202 | '0', 203 | '0'), 204 | array('0', 1, '0', '0', 205 | '0', 206 | '0'), 207 | array('0', 7, '0', '0', 208 | '0', 209 | '0'), 210 | array('0', 63, '0', '0', 211 | '0', 212 | '0'), 213 | array('1', 0, '1', '1', 214 | '1', 215 | '1'), 216 | array('1', 1, '2', '0', 217 | '10', 218 | '0'), 219 | array('1', 7, '128', '0', 220 | '10000000', 221 | '0'), 222 | array('1', 63, '-9223372036854775808', '0', 223 | '1000000000000000000000000000000000000000000000000000000000000000', 224 | '0'), 225 | array('100', 0, '100', '100', 226 | '1100100', 227 | '1100100'), 228 | array('100', 1, '200', '50', 229 | '11001000', 230 | '110010'), 231 | array('100', 7, '12800', '0', 232 | '11001000000000', 233 | '0'), 234 | array('100', 63, '0', '0', 235 | '0', 236 | '0'), 237 | array('1000000', 0, '1000000', '1000000', 238 | '11110100001001000000', 239 | '11110100001001000000'), 240 | array('1000000', 1, '2000000', '500000', 241 | '111101000010010000000', 242 | '1111010000100100000'), 243 | array('1000000', 7, '128000000', '7812', 244 | '111101000010010000000000000', 245 | '1111010000100'), 246 | array('1000000', 63, '0', '0', 247 | '0', 248 | '0'), 249 | array('2147483647', 0, '2147483647', '2147483647', 250 | '1111111111111111111111111111111', 251 | '1111111111111111111111111111111'), 252 | array('2147483647', 1, '4294967294', '1073741823', 253 | '11111111111111111111111111111110', 254 | '111111111111111111111111111111'), 255 | array('2147483647', 7, '274877906816', '16777215', 256 | '11111111111111111111111111111110000000', 257 | '111111111111111111111111'), 258 | array('2147483647', 63, '-9223372036854775808', '0', 259 | '1000000000000000000000000000000000000000000000000000000000000000', 260 | '0'), 261 | array('10000000000', 0, '10000000000', '10000000000', 262 | '1001010100000010111110010000000000', 263 | '1001010100000010111110010000000000'), 264 | array('10000000000', 1, '20000000000', '5000000000', 265 | '10010101000000101111100100000000000', 266 | '100101010000001011111001000000000'), 267 | array('10000000000', 7, '1280000000000', '78125000', 268 | '10010101000000101111100100000000000000000', 269 | '100101010000001011111001000'), 270 | array('10000000000', 63, '0', '0', 271 | '0', 272 | '0'), 273 | array('9223372036854775807', 0, '9223372036854775807', '9223372036854775807', 274 | '111111111111111111111111111111111111111111111111111111111111111', 275 | '111111111111111111111111111111111111111111111111111111111111111'), 276 | array('9223372036854775807', 1, '-2', '4611686018427387903', 277 | '1111111111111111111111111111111111111111111111111111111111111110', 278 | '11111111111111111111111111111111111111111111111111111111111111'), 279 | array('9223372036854775807', 7, '-128', '72057594037927935', 280 | '1111111111111111111111111111111111111111111111111111111110000000', 281 | '11111111111111111111111111111111111111111111111111111111'), 282 | array('9223372036854775807', 63, '-9223372036854775808', '0', 283 | '1000000000000000000000000000000000000000000000000000000000000000', 284 | '0'), 285 | array('-1', 0, '-1', '-1', 286 | '1111111111111111111111111111111111111111111111111111111111111111', 287 | '1111111111111111111111111111111111111111111111111111111111111111'), 288 | array('-1', 1, '-2', '-1', 289 | '1111111111111111111111111111111111111111111111111111111111111110', 290 | '1111111111111111111111111111111111111111111111111111111111111111'), 291 | array('-1', 7, '-128', '-1', 292 | '1111111111111111111111111111111111111111111111111111111110000000', 293 | '1111111111111111111111111111111111111111111111111111111111111111'), 294 | array('-1', 63, '-9223372036854775808', '-1', 295 | '1000000000000000000000000000000000000000000000000000000000000000', 296 | '1111111111111111111111111111111111111111111111111111111111111111'), 297 | array('-100', 0, '-100', '-100', 298 | '1111111111111111111111111111111111111111111111111111111110011100', 299 | '1111111111111111111111111111111111111111111111111111111110011100'), 300 | array('-100', 1, '-200', '-50', 301 | '1111111111111111111111111111111111111111111111111111111100111000', 302 | '1111111111111111111111111111111111111111111111111111111111001110'), 303 | array('-100', 7, '-12800', '-1', 304 | '1111111111111111111111111111111111111111111111111100111000000000', 305 | '1111111111111111111111111111111111111111111111111111111111111111'), 306 | array('-100', 63, '0', '-1', 307 | '0', 308 | '1111111111111111111111111111111111111111111111111111111111111111'), 309 | array('-1000000', 0, '-1000000', '-1000000', 310 | '1111111111111111111111111111111111111111111100001011110111000000', 311 | '1111111111111111111111111111111111111111111100001011110111000000'), 312 | array('-1000000', 1, '-2000000', '-500000', 313 | '1111111111111111111111111111111111111111111000010111101110000000', 314 | '1111111111111111111111111111111111111111111110000101111011100000'), 315 | array('-1000000', 7, '-128000000', '-7813', 316 | '1111111111111111111111111111111111111000010111101110000000000000', 317 | '1111111111111111111111111111111111111111111111111110000101111011'), 318 | array('-1000000', 63, '0', '-1', 319 | '0', 320 | '1111111111111111111111111111111111111111111111111111111111111111'), 321 | array('-2147483648', 0, '-2147483648', '-2147483648', 322 | '1111111111111111111111111111111110000000000000000000000000000000', 323 | '1111111111111111111111111111111110000000000000000000000000000000'), 324 | array('-2147483648', 1, '-4294967296', '-1073741824', 325 | '1111111111111111111111111111111100000000000000000000000000000000', 326 | '1111111111111111111111111111111111000000000000000000000000000000'), 327 | array('-2147483648', 7, '-274877906944', '-16777216', 328 | '1111111111111111111111111100000000000000000000000000000000000000', 329 | '1111111111111111111111111111111111111111000000000000000000000000'), 330 | array('-2147483648', 63, '0', '-1', 331 | '0', 332 | '1111111111111111111111111111111111111111111111111111111111111111'), 333 | array('-10000000000', 0, '-10000000000', '-10000000000', 334 | '1111111111111111111111111111110110101011111101000001110000000000', 335 | '1111111111111111111111111111110110101011111101000001110000000000'), 336 | array('-10000000000', 1, '-20000000000', '-5000000000', 337 | '1111111111111111111111111111101101010111111010000011100000000000', 338 | '1111111111111111111111111111111011010101111110100000111000000000'), 339 | array('-10000000000', 7, '-1280000000000', '-78125000', 340 | '1111111111111111111111101101010111111010000011100000000000000000', 341 | '1111111111111111111111111111111111111011010101111110100000111000'), 342 | array('-10000000000', 63, '0', '-1', 343 | '0', 344 | '1111111111111111111111111111111111111111111111111111111111111111'), 345 | array('-9223372036854775808', 0, '-9223372036854775808', '-9223372036854775808', 346 | '1000000000000000000000000000000000000000000000000000000000000000', 347 | '1000000000000000000000000000000000000000000000000000000000000000'), 348 | array('-9223372036854775808', 1, '0', '-4611686018427387904', 349 | '0', 350 | '1100000000000000000000000000000000000000000000000000000000000000'), 351 | array('-9223372036854775808', 7, '0', '-72057594037927936', 352 | '0', 353 | '1111111100000000000000000000000000000000000000000000000000000000'), 354 | array('-9223372036854775808', 63, '0', '-1', 355 | '0', 356 | '1111111111111111111111111111111111111111111111111111111111111111'), 357 | ); 358 | } 359 | 360 | } 361 | -------------------------------------------------------------------------------- /test/NameTest.php: -------------------------------------------------------------------------------- 1 | name = $name; 45 | $this->namespace = $namespace; 46 | $this->default_namespace = $default_namespace; 47 | $this->is_valid = $is_valid; 48 | $this->expected_fullname = $expected_fullname; 49 | } 50 | 51 | /** 52 | * @return mixed 53 | */ 54 | function __toString() 55 | { 56 | return var_export($this, true); 57 | } 58 | } 59 | 60 | /** 61 | * Class NameTest 62 | */ 63 | class NameTest extends \PHPUnit\Framework\TestCase 64 | { 65 | 66 | /** 67 | * @return array 68 | */ 69 | function fullname_provider() 70 | { 71 | $examples = array(new NameExample('foo', null, null, true, 'foo'), 72 | new NameExample('foo', 'bar', null, true, 'bar.foo'), 73 | new NameExample('bar.foo', 'baz', null, true, 'bar.foo'), 74 | new NameExample('_bar.foo', 'baz', null, true, '_bar.foo'), 75 | new NameExample('bar._foo', 'baz', null, true, 'bar._foo'), 76 | new NameExample('3bar.foo', 'baz', null, false), 77 | new NameExample('bar.3foo', 'baz', null, false), 78 | new NameExample('b4r.foo', 'baz', null, true, 'b4r.foo'), 79 | new NameExample('bar.f0o', 'baz', null, true, 'bar.f0o'), 80 | new NameExample(' .foo', 'baz', null, false), 81 | new NameExample('bar. foo', 'baz', null, false), 82 | new NameExample('bar. ', 'baz', null, false) 83 | ); 84 | $exes = array(); 85 | foreach ($examples as $ex) 86 | $exes []= array($ex); 87 | return $exes; 88 | } 89 | 90 | /** 91 | * @dataProvider fullname_provider 92 | * @param $ex 93 | */ 94 | function test_fullname($ex) 95 | { 96 | try 97 | { 98 | $name = new AvroName($ex->name, $ex->namespace, $ex->default_namespace); 99 | $this->assertTrue($ex->is_valid); 100 | $this->assertEquals($ex->expected_fullname, $name->fullname()); 101 | } 102 | catch (AvroSchemaParseException $e) 103 | { 104 | $this->assertFalse($ex->is_valid, sprintf("%s:\n%s", 105 | $ex, 106 | $e->getMessage())); 107 | } 108 | } 109 | 110 | /** 111 | * @return array 112 | */ 113 | function name_provider() 114 | { 115 | return array(array('a', true), 116 | array('_', true), 117 | array('1a', false), 118 | array('', false), 119 | array(null, false), 120 | array(' ', false), 121 | array('Cons', true)); 122 | } 123 | 124 | /** 125 | * @dataProvider name_provider 126 | * @param $name 127 | * @param $is_well_formed 128 | */ 129 | function test_name($name, $is_well_formed) 130 | { 131 | $this->assertEquals(AvroName::is_well_formed_name($name), $is_well_formed, $name ?? "null"); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /test/ProtocolFileTest.php: -------------------------------------------------------------------------------- 1 | prot_parseable); 33 | for ($i=0; $i<$cnt; $i++) { 34 | try { 35 | AvroProtocol::parse($this->prot_data[$i]); 36 | } catch (AvroSchemaParseException $x) { 37 | // exception ok if we expected this protocol spec to be unparseable 38 | $this->assertEquals(false, $this->prot_parseable[$i]); 39 | } 40 | } 41 | } 42 | 43 | // test data 44 | private $prot_parseable=array(true, true, true, true, true, true, false, true, true); 45 | private $prot_data = array( 46 | <<<'DATUM' 47 | { 48 | "namespace": "com.acme", 49 | "protocol": "HelloWorld", 50 | 51 | "types": [ 52 | {"name": "Greeting", "type": "record", "fields": [ 53 | {"name": "message", "type": "string"}]}, 54 | {"name": "Curse", "type": "error", "fields": [ 55 | {"name": "message", "type": "string"}]} 56 | ], 57 | 58 | "messages": { 59 | "hello": { 60 | "request": [{"name": "greeting", "type": "Greeting" }], 61 | "response": "Greeting", 62 | "errors": ["Curse"] 63 | } 64 | } 65 | } 66 | DATUM 67 | , 68 | <<<'DATUM' 69 | {"namespace": "org.apache.avro.test", 70 | "protocol": "Simple", 71 | 72 | "types": [ 73 | {"name": "Kind", "type": "enum", "symbols": ["FOO","BAR","BAZ"]}, 74 | 75 | {"name": "MD5", "type": "fixed", "size": 16}, 76 | 77 | {"name": "TestRecord", "type": "record", 78 | "fields": [ 79 | {"name": "name", "type": "string", "order": "ignore"}, 80 | {"name": "kind", "type": "Kind", "order": "descending"}, 81 | {"name": "hash", "type": "MD5"} 82 | ] 83 | }, 84 | 85 | {"name": "TestError", "type": "error", "fields": [ 86 | {"name": "message", "type": "string"} 87 | ] 88 | } 89 | 90 | ], 91 | 92 | "messages": { 93 | 94 | "hello": { 95 | "request": [{"name": "greeting", "type": "string"}], 96 | "response": "string" 97 | }, 98 | 99 | "echo": { 100 | "request": [{"name": "record", "type": "TestRecord"}], 101 | "response": "TestRecord" 102 | }, 103 | 104 | "add": { 105 | "request": [{"name": "arg1", "type": "int"}, {"name": "arg2", "type": "int"}], 106 | "response": "int" 107 | }, 108 | 109 | "echoBytes": { 110 | "request": [{"name": "data", "type": "bytes"}], 111 | "response": "bytes" 112 | }, 113 | 114 | "error": { 115 | "request": [], 116 | "response": "null", 117 | "errors": ["TestError"] 118 | } 119 | } 120 | 121 | } 122 | DATUM 123 | , 124 | <<<'DATUM' 125 | {"namespace": "org.apache.avro.test.namespace", 126 | "protocol": "TestNamespace", 127 | 128 | "types": [ 129 | {"name": "org.apache.avro.test.util.MD5", "type": "fixed", "size": 16}, 130 | {"name": "TestRecord", "type": "record", 131 | "fields": [ {"name": "hash", "type": "org.apache.avro.test.util.MD5"} ] 132 | }, 133 | {"name": "TestError", "namespace": "org.apache.avro.test.errors", 134 | "type": "error", "fields": [ {"name": "message", "type": "string"} ] 135 | } 136 | ], 137 | 138 | "messages": { 139 | "echo": { 140 | "request": [{"name": "record", "type": "TestRecord"}], 141 | "response": "TestRecord" 142 | }, 143 | 144 | "error": { 145 | "request": [], 146 | "response": "null", 147 | "errors": ["org.apache.avro.test.errors.TestError"] 148 | } 149 | 150 | } 151 | 152 | } 153 | DATUM 154 | , 155 | <<<'DATUM' 156 | {"namespace": "org.apache.avro.test.namespace", 157 | "protocol": "TestImplicitNamespace", 158 | 159 | "types": [ 160 | {"name": "org.apache.avro.test.util.MD5", "type": "fixed", "size": 16}, 161 | {"name": "ReferencedRecord", "type": "record", 162 | "fields": [ {"name": "foo", "type": "string"} ] }, 163 | {"name": "TestRecord", "type": "record", 164 | "fields": [ {"name": "hash", "type": "org.apache.avro.test.util.MD5"}, 165 | {"name": "unqalified", "type": "ReferencedRecord"} ] 166 | }, 167 | {"name": "TestError", 168 | "type": "error", "fields": [ {"name": "message", "type": "string"} ] 169 | } 170 | ], 171 | 172 | "messages": { 173 | "echo": { 174 | "request": [{"name": "qualified", 175 | "type": "org.apache.avro.test.namespace.TestRecord"}], 176 | "response": "TestRecord" 177 | }, 178 | 179 | "error": { 180 | "request": [], 181 | "response": "null", 182 | "errors": ["org.apache.avro.test.namespace.TestError"] 183 | } 184 | 185 | } 186 | 187 | } 188 | DATUM 189 | , 190 | <<<'DATUM' 191 | {"namespace": "org.apache.avro.test.namespace", 192 | "protocol": "TestNamespaceTwo", 193 | 194 | "types": [ 195 | {"name": "org.apache.avro.test.util.MD5", "type": "fixed", "size": 16}, 196 | {"name": "ReferencedRecord", "type": "record", 197 | "namespace": "org.apache.avro.other.namespace", 198 | "fields": [ {"name": "foo", "type": "string"} ] }, 199 | {"name": "TestRecord", "type": "record", 200 | "fields": [ {"name": "hash", "type": "org.apache.avro.test.util.MD5"}, 201 | {"name": "qualified", 202 | "type": "org.apache.avro.other.namespace.ReferencedRecord"} 203 | ] 204 | }, 205 | {"name": "TestError", 206 | "type": "error", "fields": [ {"name": "message", "type": "string"} ] 207 | } 208 | ], 209 | 210 | "messages": { 211 | "echo": { 212 | "request": [{"name": "qualified", 213 | "type": "org.apache.avro.test.namespace.TestRecord"}], 214 | "response": "TestRecord" 215 | }, 216 | 217 | "error": { 218 | "request": [], 219 | "response": "null", 220 | "errors": ["org.apache.avro.test.namespace.TestError"] 221 | } 222 | 223 | } 224 | 225 | } 226 | DATUM 227 | , 228 | <<<'DATUM' 229 | {"namespace": "org.apache.avro.test.namespace", 230 | "protocol": "TestValidRepeatedName", 231 | 232 | "types": [ 233 | {"name": "org.apache.avro.test.util.MD5", "type": "fixed", "size": 16}, 234 | {"name": "ReferencedRecord", "type": "record", 235 | "namespace": "org.apache.avro.other.namespace", 236 | "fields": [ {"name": "foo", "type": "string"} ] }, 237 | {"name": "ReferencedRecord", "type": "record", 238 | "fields": [ {"name": "bar", "type": "double"} ] }, 239 | {"name": "TestError", 240 | "type": "error", "fields": [ {"name": "message", "type": "string"} ] 241 | } 242 | ], 243 | 244 | "messages": { 245 | "echo": { 246 | "request": [{"name": "qualified", 247 | "type": "ReferencedRecord"}], 248 | "response": "org.apache.avro.other.namespace.ReferencedRecord" 249 | }, 250 | 251 | "error": { 252 | "request": [], 253 | "response": "null", 254 | "errors": ["org.apache.avro.test.namespace.TestError"] 255 | } 256 | 257 | } 258 | 259 | } 260 | DATUM 261 | , 262 | <<<'DATUM' 263 | {"namespace": "org.apache.avro.test.namespace", 264 | "protocol": "TestInvalidRepeatedName", 265 | 266 | "types": [ 267 | {"name": "org.apache.avro.test.util.MD5", "type": "fixed", "size": 16}, 268 | {"name": "ReferencedRecord", "type": "record", 269 | "fields": [ {"name": "foo", "type": "string"} ] }, 270 | {"name": "ReferencedRecord", "type": "record", 271 | "fields": [ {"name": "bar", "type": "double"} ] }, 272 | {"name": "TestError", 273 | "type": "error", "fields": [ {"name": "message", "type": "string"} ] 274 | } 275 | ], 276 | 277 | "messages": { 278 | "echo": { 279 | "request": [{"name": "qualified", 280 | "type": "ReferencedRecord"}], 281 | "response": "org.apache.avro.other.namespace.ReferencedRecord" 282 | }, 283 | 284 | "error": { 285 | "request": [], 286 | "response": "null", 287 | "errors": ["org.apache.avro.test.namespace.TestError"] 288 | } 289 | 290 | } 291 | 292 | } 293 | DATUM 294 | , 295 | <<<'DATUM' 296 | {"namespace": "org.apache.avro.test", 297 | "protocol": "BulkData", 298 | 299 | "types": [], 300 | 301 | "messages": { 302 | 303 | "read": { 304 | "request": [], 305 | "response": "bytes" 306 | }, 307 | 308 | "write": { 309 | "request": [ {"name": "data", "type": "bytes"} ], 310 | "response": "null" 311 | } 312 | 313 | } 314 | 315 | } 316 | DATUM 317 | , 318 | <<<'DATUM' 319 | { 320 | "protocol" : "API", 321 | "namespace" : "xyz.api", 322 | "types" : [ { 323 | "type" : "enum", 324 | "name" : "Symbology", 325 | "namespace" : "xyz.api.product", 326 | "symbols" : [ "OPRA", "CUSIP", "ISIN", "SEDOL" ] 327 | }, { 328 | "type" : "record", 329 | "name" : "Symbol", 330 | "namespace" : "xyz.api.product", 331 | "fields" : [ { 332 | "name" : "symbology", 333 | "type" : "xyz.api.product.Symbology" 334 | }, { 335 | "name" : "symbol", 336 | "type" : "string" 337 | } ] 338 | }, { 339 | "type" : "record", 340 | "name" : "MultiSymbol", 341 | "namespace" : "xyz.api.product", 342 | "fields" : [ { 343 | "name" : "symbols", 344 | "type" : { 345 | "type" : "map", 346 | "values" : "xyz.api.product.Symbol" 347 | } 348 | } ] 349 | } ], 350 | "messages" : { 351 | } 352 | } 353 | DATUM 354 | ); 355 | } 356 | -------------------------------------------------------------------------------- /test/SchemaTest.php: -------------------------------------------------------------------------------- 1 | schema_string = $schema_string; 45 | $this->is_valid = $is_valid; 46 | $this->name = $name ? $name : $schema_string; 47 | $this->normalized_schema_string = $normalized_schema_string 48 | ? $normalized_schema_string : json_encode(json_decode($schema_string, true)); 49 | $this->comment = $comment; 50 | } 51 | } 52 | 53 | /** 54 | * Class SchemaTest 55 | */ 56 | class SchemaTest extends \PHPUnit\Framework\TestCase 57 | { 58 | static $examples = array(); 59 | static $valid_examples = array(); 60 | 61 | /** 62 | * @return array 63 | */ 64 | protected static function make_primitive_examples() 65 | { 66 | $examples = array(); 67 | foreach (array('null', 'boolean', 68 | 'int', 'long', 69 | 'float', 'double', 70 | 'bytes', 'string') 71 | as $type) 72 | { 73 | $examples []= new SchemaExample(sprintf('"%s"', $type), true, sprintf('{"type":"%s"}', $type)); 74 | $examples []= new SchemaExample(sprintf('{"type": "%s"}', $type), true, sprintf('{"type":"%s"}', $type)); 75 | } 76 | return $examples; 77 | } 78 | 79 | protected static function make_examples() 80 | { 81 | $primitive_examples = array_merge(array(new SchemaExample('"True"', false), 82 | new SchemaExample('{"no_type": "test"}', false), 83 | new SchemaExample('{"type": "panther"}', false)), 84 | self::make_primitive_examples()); 85 | 86 | $array_examples = array( 87 | new SchemaExample('{"type": "array", "items": "long"}', true, '{"type":"array","items":{"type":"long"}}'), 88 | new SchemaExample(' 89 | {"type": "array", 90 | "items": {"type": "enum", "name": "Test", "symbols": ["A", "B"]}} 91 | ', true, '{"type":"array","items":{"type":"enum","name":"Test","symbols":["A","B"]}}')); 92 | 93 | $map_examples = array( 94 | new SchemaExample('{"type": "map", "values": "long"}', true, '{"type":"map","values":{"type":"long"}}'), 95 | new SchemaExample(' 96 | {"type": "map", 97 | "values": {"type": "enum", "name": "Test", "symbols": ["A", "B"]}} 98 | ', true, )); 99 | 100 | $union_examples = array( 101 | new SchemaExample('["string", "null", "long"]', true, '[{"type":"string"},{"type":"null"},{"type":"long"}]'), 102 | new SchemaExample('["null", "null"]', false), 103 | new SchemaExample('["long", "long"]', false), 104 | new SchemaExample(' 105 | [{"type": "array", "items": "long"} 106 | {"type": "array", "items": "string"}] 107 | ', false), 108 | new SchemaExample('["long", 109 | {"type": "long"}, 110 | "int"]', false), 111 | new SchemaExample('["long", 112 | {"type": "array", "items": "long"}, 113 | {"type": "map", "values": "long"}, 114 | "int"]', true, '[{"type":"long"},{"type":"array","items":{"type":"long"}},{"type":"map","values":{"type":"long"}},{"type":"int"}]'), 115 | new SchemaExample('["long", 116 | ["string", "null"], 117 | "int"]', false), 118 | new SchemaExample('["long", 119 | ["string", "null"], 120 | "int"]', false), 121 | new SchemaExample('["null", "boolean", "int", "long", "float", "double", 122 | "string", "bytes", 123 | {"type": "array", "items":"int"}, 124 | {"type": "map", "values":"int"}, 125 | {"name": "bar", "type":"record", 126 | "fields":[{"name":"label", "type":"string"}]}, 127 | {"name": "foo", "type":"fixed", 128 | "size":16}, 129 | {"name": "baz", "type":"enum", "symbols":["A", "B", "C"]} 130 | ]', true, '[{"type":"null"},{"type":"boolean"},{"type":"int"},{"type":"long"},{"type":"float"},{"type":"double"},{"type":"string"},{"type":"bytes"},{"type":"array","items":{"type":"int"}},{"type":"map","values":{"type":"int"}},{"type":"record","name":"bar","fields":[{"name":"label","type":{"type":"string"}}]},{"type":"fixed","name":"foo","size":16},{"type":"enum","name":"baz","symbols":["A","B","C"]}]'), 131 | new SchemaExample(' 132 | [{"name":"subtract", "namespace":"com.example", 133 | "type":"record", 134 | "fields":[{"name":"minuend", "type":"int"}, 135 | {"name":"subtrahend", "type":"int"}]}, 136 | {"name": "divide", "namespace":"com.example", 137 | "type":"record", 138 | "fields":[{"name":"quotient", "type":"int"}, 139 | {"name":"dividend", "type":"int"}]}, 140 | {"type": "array", "items": "string"}] 141 | ', true, '[{"type":"record","name":"subtract","namespace":"com.example","fields":[{"name":"minuend","type":{"type":"int"}},{"name":"subtrahend","type":{"type":"int"}}]},{"type":"record","name":"divide","namespace":"com.example","fields":[{"name":"quotient","type":{"type":"int"}},{"name":"dividend","type":{"type":"int"}}]},{"type":"array","items":{"type":"string"}}]'), 142 | ); 143 | 144 | $fixed_examples = array( 145 | new SchemaExample('{"type": "fixed", "name": "Test", "size": 1}', true), 146 | new SchemaExample(' 147 | {"type": "fixed", 148 | "name": "MyFixed", 149 | "namespace": "org.apache.hadoop.avro", 150 | "size": 1} 151 | ', true), 152 | new SchemaExample(' 153 | {"type": "fixed", 154 | "name": "Missing size"} 155 | ', false), 156 | new SchemaExample(' 157 | {"type": "fixed", 158 | "size": 314} 159 | ', false), 160 | new SchemaExample('{"type":"fixed","name":"ex","doc":"this should be ignored","size": 314}', 161 | true, 162 | '{"type":"fixed","name":"ex","size":314}'), 163 | new SchemaExample('{"name": "bar", 164 | "namespace": "com.example", 165 | "type": "fixed", 166 | "size": 32 }', true, 167 | '{"type":"fixed","name":"bar","namespace":"com.example","size":32}'), 168 | new SchemaExample('{"name": "com.example.bar", 169 | "type": "fixed", 170 | "size": 32 }', true, 171 | '{"type":"fixed","name":"bar","namespace":"com.example","size":32}')); 172 | 173 | $fixed_examples []= new SchemaExample( 174 | '{"type":"fixed","name":"_x.bar","size":4}', true, 175 | '{"type":"fixed","name":"bar","namespace":"_x","size":4}'); 176 | $fixed_examples []= new SchemaExample( 177 | '{"type":"fixed","name":"baz._x","size":4}', true, 178 | '{"type":"fixed","name":"_x","namespace":"baz","size":4}'); 179 | $fixed_examples []= new SchemaExample( 180 | '{"type":"fixed","name":"baz.3x","size":4}', false); 181 | 182 | $enum_examples = array( 183 | new SchemaExample('{"type": "enum", "name": "Test", "symbols": ["A", "B"]}', true), 184 | new SchemaExample(' 185 | {"type": "enum", 186 | "name": "Status", 187 | "symbols": "Normal Caution Critical"} 188 | ', false), 189 | new SchemaExample(' 190 | {"type": "enum", 191 | "name": [ 0, 1, 1, 2, 3, 5, 8 ], 192 | "symbols": ["Golden", "Mean"]} 193 | ', false), 194 | new SchemaExample(' 195 | {"type": "enum", 196 | "symbols" : ["I", "will", "fail", "no", "name"]} 197 | ', false), 198 | new SchemaExample(' 199 | {"type": "enum", 200 | "name": "Test" 201 | "symbols" : ["AA", "AA"]} 202 | ', false), 203 | new SchemaExample('{"type":"enum","name":"Test","symbols":["AA", 16]}', 204 | false), 205 | new SchemaExample(' 206 | {"type": "enum", 207 | "name": "blood_types", 208 | "doc": "AB is freaky.", 209 | "symbols" : ["A", "AB", "B", "O"]} 210 | ', true), 211 | new SchemaExample(' 212 | {"type": "enum", 213 | "name": "blood-types", 214 | "doc": 16, 215 | "symbols" : ["A", "AB", "B", "O"]} 216 | ', false) 217 | ); 218 | 219 | 220 | $record_examples = array(); 221 | $record_examples []= new SchemaExample(' 222 | {"type": "record", 223 | "name": "Test", 224 | "fields": [{"name": "f", 225 | "type": "long"}]} 226 | ', true, '{"type":"record","name":"Test","fields":[{"name":"f","type":{"type":"long"}}]}'); 227 | $record_examples []= new SchemaExample(' 228 | {"type": "error", 229 | "name": "Test", 230 | "fields": [{"name": "f", 231 | "type": "long"}]} 232 | ', true, '{"type":"error","name":"Test","fields":[{"name":"f","type":{"type":"long"}}]}'); 233 | $record_examples []= new SchemaExample(' 234 | {"type": "record", 235 | "name": "Node", 236 | "fields": [{"name": "label", "type": "string"}, 237 | {"name": "children", 238 | "type": {"type": "array", "items": "Node"}}]} 239 | ', true, '{"type":"record","name":"Node","fields":[{"name":"label","type":{"type":"string"}},{"name":"children","type":{"type":"array","items":"Node"}}]}'); 240 | $record_examples []= new SchemaExample(' 241 | {"type": "record", 242 | "name": "ListLink", 243 | "fields": [{"name": "car", "type": "int"}, 244 | {"name": "cdr", "type": "ListLink"}]} 245 | ', true, '{"type":"record","name":"ListLink","fields":[{"name":"car","type":{"type":"int"}},{"name":"cdr","type":"ListLink"}]}'); 246 | $record_examples []= new SchemaExample(' 247 | {"type": "record", 248 | "name": "Lisp", 249 | "fields": [{"name": "value", 250 | "type": ["null", "string"]}]} 251 | ', true, '{"type":"record","name":"Lisp","fields":[{"name":"value","type":[{"type":"null"},{"type":"string"}]}]}'); 252 | $record_examples []= new SchemaExample(' 253 | {"type": "record", 254 | "name": "Lisp", 255 | "fields": [{"name": "value", 256 | "type": ["null", "string", 257 | {"type": "record", 258 | "name": "Cons", 259 | "fields": [{"name": "car", "type": "string"}, 260 | {"name": "cdr", "type": "string"}]}]}]} 261 | ', true, '{"type":"record","name":"Lisp","fields":[{"name":"value","type":[{"type":"null"},{"type":"string"},{"type":"record","name":"Cons","fields":[{"name":"car","type":{"type":"string"}},{"name":"cdr","type":{"type":"string"}}]}]}]}'); 262 | $record_examples []= new SchemaExample(' 263 | {"type": "record", 264 | "name": "Lisp", 265 | "fields": [{"name": "value", 266 | "type": ["null", "string", 267 | {"type": "record", 268 | "name": "Cons", 269 | "fields": [{"name": "car", "type": "Lisp"}, 270 | {"name": "cdr", "type": "Lisp"}]}]}]} 271 | ', true, '{"type":"record","name":"Lisp","fields":[{"name":"value","type":[{"type":"null"},{"type":"string"},{"type":"record","name":"Cons","fields":[{"name":"car","type":"Lisp"},{"name":"cdr","type":"Lisp"}]}]}]}'); 272 | $record_examples []= new SchemaExample(' 273 | {"type": "record", 274 | "name": "HandshakeRequest", 275 | "namespace": "org.apache.avro.ipc", 276 | "fields": [{"name": "clientHash", 277 | "type": {"type": "fixed", "name": "MD5", "size": 16}}, 278 | {"name": "meta", 279 | "type": ["null", {"type": "map", "values": "bytes"}]}]} 280 | ', true, '{"type":"record","name":"HandshakeRequest","namespace":"org.apache.avro.ipc","fields":[{"name":"clientHash","type":{"type":"fixed","name":"MD5","size":16}},{"name":"meta","type":[{"type":"null"},{"type":"map","values":{"type":"bytes"}}]}]}'); 281 | $record_examples []= new SchemaExample(' 282 | {"type": "record", 283 | "name": "HandshakeRequest", 284 | "namespace": "org.apache.avro.ipc", 285 | "fields": [{"name": "clientHash", 286 | "type": {"type": "fixed", "name": "MD5", "size": 16}}, 287 | {"name": "clientProtocol", "type": ["null", "string"]}, 288 | {"name": "serverHash", "type": "MD5"}, 289 | {"name": "meta", 290 | "type": ["null", {"type": "map", "values": "bytes"}]}]} 291 | ', true, '{"type":"record","name":"HandshakeRequest","namespace":"org.apache.avro.ipc","fields":[{"name":"clientHash","type":{"type":"fixed","name":"MD5","size":16}},{"name":"clientProtocol","type":[{"type":"null"},{"type":"string"}]},{"name":"serverHash","type":"MD5"},{"name":"meta","type":[{"type":"null"},{"type":"map","values":{"type":"bytes"}}]}]}'); 292 | $record_examples []= new SchemaExample(' 293 | {"type": "record", 294 | "name": "HandshakeResponse", 295 | "namespace": "org.apache.avro.ipc", 296 | "fields": [{"name": "match", 297 | "type": {"type": "enum", 298 | "name": "HandshakeMatch", 299 | "symbols": ["BOTH", "CLIENT", "NONE"]}}, 300 | {"name": "serverProtocol", "type": ["null", "string"]}, 301 | {"name": "serverHash", 302 | "type": ["null", 303 | {"name": "MD5", "size": 16, "type": "fixed"}]}, 304 | {"name": "meta", 305 | "type": ["null", {"type": "map", "values": "bytes"}]}]} 306 | ', true, 307 | '{"type":"record","name":"HandshakeResponse","namespace":"org.apache.avro.ipc","fields":[{"name":"match","type":{"type":"enum","name":"HandshakeMatch","symbols":["BOTH","CLIENT","NONE"]}},{"name":"serverProtocol","type":[{"type":"null"},{"type":"string"}]},{"name":"serverHash","type":[{"type":"null"},{"type":"fixed","name":"MD5","size":16}]},{"name":"meta","type":[{"type":"null"},{"type":"map","values":{"type":"bytes"}}]}]}' 308 | ); 309 | $record_examples []= new SchemaExample('{"type": "record", 310 | "namespace": "org.apache.avro", 311 | "name": "Interop", 312 | "fields": [{"type": {"fields": [{"type": {"items": "org.apache.avro.Node", 313 | "type": "array"}, 314 | "name": "children"}], 315 | "type": "record", 316 | "name": "Node"}, 317 | "name": "recordField"}]} 318 | ', true, '{"type":"record","name":"Interop","namespace":"org.apache.avro","fields":[{"name":"recordField","type":{"type":"record","name":"Node","fields":[{"name":"children","type":{"type":"array","items":"Node"}}]}}]}'); 319 | $record_examples [] = new SchemaExample('{"type": "record", 320 | "namespace": "org.apache.avro", 321 | "name": "Interop", 322 | "fields": [{"type": {"symbols": ["A", "B", "C"], "type": "enum", "name": "Kind"}, 323 | "name": "enumField"}, 324 | {"type": {"fields": [{"type": "string", "name": "label"}, 325 | {"type": {"items": "org.apache.avro.Node", "type": "array"}, 326 | "name": "children"}], 327 | "type": "record", 328 | "name": "Node"}, 329 | "name": "recordField"}]}', true, '{"type":"record","name":"Interop","namespace":"org.apache.avro","fields":[{"name":"enumField","type":{"type":"enum","name":"Kind","symbols":["A","B","C"]}},{"name":"recordField","type":{"type":"record","name":"Node","fields":[{"name":"label","type":{"type":"string"}},{"name":"children","type":{"type":"array","items":"Node"}}]}}]}'); 330 | 331 | $record_examples []= new SchemaExample(' 332 | {"type": "record", 333 | "name": "Interop", 334 | "namespace": "org.apache.avro", 335 | "fields": [{"name": "intField", "type": "int"}, 336 | {"name": "longField", "type": "long"}, 337 | {"name": "stringField", "type": "string"}, 338 | {"name": "boolField", "type": "boolean"}, 339 | {"name": "floatField", "type": "float"}, 340 | {"name": "doubleField", "type": "double"}, 341 | {"name": "bytesField", "type": "bytes"}, 342 | {"name": "nullField", "type": "null"}, 343 | {"name": "arrayField", 344 | "type": {"type": "array", "items": "double"}}, 345 | {"name": "mapField", 346 | "type": {"type": "map", 347 | "values": {"name": "Foo", 348 | "type": "record", 349 | "fields": [{"name": "label", 350 | "type": "string"}]}}}, 351 | {"name": "unionField", 352 | "type": ["boolean", 353 | "double", 354 | {"type": "array", "items": "bytes"}]}, 355 | {"name": "enumField", 356 | "type": {"type": "enum", 357 | "name": "Kind", 358 | "symbols": ["A", "B", "C"]}}, 359 | {"name": "fixedField", 360 | "type": {"type": "fixed", "name": "MD5", "size": 16}}, 361 | {"name": "recordField", 362 | "type": {"type": "record", 363 | "name": "Node", 364 | "fields": [{"name": "label", "type": "string"}, 365 | {"name": "children", 366 | "type": {"type": "array", 367 | "items": "Node"}}]}}]} 368 | ', true, 369 | '{"type":"record","name":"Interop","namespace":"org.apache.avro","fields":[{"name":"intField","type":{"type":"int"}},{"name":"longField","type":{"type":"long"}},{"name":"stringField","type":{"type":"string"}},{"name":"boolField","type":{"type":"boolean"}},{"name":"floatField","type":{"type":"float"}},{"name":"doubleField","type":{"type":"double"}},{"name":"bytesField","type":{"type":"bytes"}},{"name":"nullField","type":{"type":"null"}},{"name":"arrayField","type":{"type":"array","items":{"type":"double"}}},{"name":"mapField","type":{"type":"map","values":{"type":"record","name":"Foo","fields":[{"name":"label","type":{"type":"string"}}]}}},{"name":"unionField","type":[{"type":"boolean"},{"type":"double"},{"type":"array","items":{"type":"bytes"}}]},{"name":"enumField","type":{"type":"enum","name":"Kind","symbols":["A","B","C"]}},{"name":"fixedField","type":{"type":"fixed","name":"MD5","size":16}},{"name":"recordField","type":{"type":"record","name":"Node","fields":[{"name":"label","type":{"type":"string"}},{"name":"children","type":{"type":"array","items":"Node"}}]}}]}'); 370 | $record_examples []= new SchemaExample('{"type": "record", "namespace": "org.apache.avro", "name": "Interop", "fields": [{"type": "int", "name": "intField"}, {"type": "long", "name": "longField"}, {"type": "string", "name": "stringField"}, {"type": "boolean", "name": "boolField"}, {"type": "float", "name": "floatField"}, {"type": "double", "name": "doubleField"}, {"type": "bytes", "name": "bytesField"}, {"type": "null", "name": "nullField"}, {"type": {"items": "double", "type": "array"}, "name": "arrayField"}, {"type": {"type": "map", "values": {"fields": [{"type": "string", "name": "label"}], "type": "record", "name": "Foo"}}, "name": "mapField"}, {"type": ["boolean", "double", {"items": "bytes", "type": "array"}], "name": "unionField"}, {"type": {"symbols": ["A", "B", "C"], "type": "enum", "name": "Kind"}, "name": "enumField"}, {"type": {"type": "fixed", "name": "MD5", "size": 16}, "name": "fixedField"}, {"type": {"fields": [{"type": "string", "name": "label"}, {"type": {"items": "org.apache.avro.Node", "type": "array"}, "name": "children"}], "type": "record", "name": "Node"}, "name": "recordField"}]} 371 | ', true, '{"type":"record","name":"Interop","namespace":"org.apache.avro","fields":[{"name":"intField","type":{"type":"int"}},{"name":"longField","type":{"type":"long"}},{"name":"stringField","type":{"type":"string"}},{"name":"boolField","type":{"type":"boolean"}},{"name":"floatField","type":{"type":"float"}},{"name":"doubleField","type":{"type":"double"}},{"name":"bytesField","type":{"type":"bytes"}},{"name":"nullField","type":{"type":"null"}},{"name":"arrayField","type":{"type":"array","items":{"type":"double"}}},{"name":"mapField","type":{"type":"map","values":{"type":"record","name":"Foo","fields":[{"name":"label","type":{"type":"string"}}]}}},{"name":"unionField","type":[{"type":"boolean"},{"type":"double"},{"type":"array","items":{"type":"bytes"}}]},{"name":"enumField","type":{"type":"enum","name":"Kind","symbols":["A","B","C"]}},{"name":"fixedField","type":{"type":"fixed","name":"MD5","size":16}},{"name":"recordField","type":{"type":"record","name":"Node","fields":[{"name":"label","type":{"type":"string"}},{"name":"children","type":{"type":"array","items":"Node"}}]}}]}'); 372 | $record_examples []= new SchemaExample(' 373 | {"type": "record", 374 | "name": "ipAddr", 375 | "fields": [{"name": "addr", 376 | "type": [{"name": "IPv6", "type": "fixed", "size": 16}, 377 | {"name": "IPv4", "type": "fixed", "size": 4}]}]} 378 | ', true, 379 | '{"type":"record","name":"ipAddr","fields":[{"name":"addr","type":[{"type":"fixed","name":"IPv6","size":16},{"type":"fixed","name":"IPv4","size":4}]}]}'); 380 | $record_examples []= new SchemaExample(' 381 | {"type": "record", 382 | "name": "Address", 383 | "fields": [{"type": "string"}, 384 | {"type": "string", "name": "City"}]} 385 | ', false); 386 | $record_examples []= new SchemaExample(' 387 | {"type": "record", 388 | "name": "Event", 389 | "fields": [{"name": "Sponsor"}, 390 | {"name": "City", "type": "string"}]} 391 | ', false); 392 | $record_examples []= new SchemaExample(' 393 | {"type": "record", 394 | "fields": "His vision, from the constantly passing bars," 395 | "name", "Rainer"} 396 | ', false); 397 | $record_examples []= new SchemaExample(' 398 | {"name": ["Tom", "Jerry"], 399 | "type": "record", 400 | "fields": [{"name": "name", "type": "string"}]} 401 | ', false); 402 | $record_examples []= new SchemaExample(' 403 | {"type":"record","name":"foo","doc":"doc string", 404 | "fields":[{"name":"bar", "type":"int", "order":"ascending", "default":1}]} 405 | ', 406 | true, 407 | '{"type":"record","name":"foo","doc":"doc string","fields":[{"name":"bar","type":{"type":"int"},"default":1,"order":"ascending"}]}'); 408 | $record_examples []= new SchemaExample(' 409 | {"type":"record", "name":"foo", "doc":"doc string", 410 | "fields":[{"name":"bar", "type":"int", "order":"bad"}]} 411 | ', false); 412 | // `"default":null` should not be lost in `to_avro`. 413 | $record_examples []= new SchemaExample( 414 | '{"type":"record","name":"foo","fields":[{"name":"bar","type":["null","string"],"default":null}]}', 415 | true, 416 | '{"type":"record","name":"foo","fields":[{"name":"bar","type":[{"type":"null"},{"type":"string"}],"default":null}]}'); 417 | // Don't lose the "doc" attributes of record fields. 418 | $record_examples []= new SchemaExample( 419 | '{"type":"record","name":"foo","fields":[{"name":"bar","type":["null","string"],"doc":"Bar name."}]}', 420 | true, 421 | '{"type":"record","name":"foo","fields":[{"name":"bar","type":[{"type":"null"},{"type":"string"}],"doc":"Bar name."}]}'); 422 | 423 | $primitive_examples []= new SchemaExample( 424 | '{ "type": "bytes", "logicalType": "decimal", "precision": 4, "scale": 2 }', 425 | true 426 | ); 427 | $fixed_examples []= new SchemaExample( 428 | '{ "type": "fixed", "size": 32, "name": "hash", "logicalType": "md5" }', 429 | true, 430 | '{"type":"fixed","name":"hash","logicalType":"md5","size":32}' 431 | ); 432 | $enum_examples []= new SchemaExample( 433 | '{"type": "enum", "logicalType": "foo", "name": "foo", "symbols": ["FOO", "BAR"], "foo": "bar"}', 434 | true, 435 | '{"type":"enum","name":"foo","logicalType":"foo","foo":"bar","symbols":["FOO","BAR"]}' 436 | ); 437 | $array_examples []= new SchemaExample( 438 | '{"type": "array", "logicalType": "foo", "items": "string", "foo": "bar"}', 439 | true, 440 | '{"type":"array","items":{"type":"string"},"logicalType":"foo","foo":"bar"}' 441 | ); 442 | $map_examples []= new SchemaExample( 443 | '{"type": "map", "logicalType": "foo", "values": "long", "foo": "bar"}', 444 | true, 445 | '{"type":"map","values":{"type":"long"},"logicalType":"foo","foo":"bar"}' 446 | ); 447 | $record_examples []= new SchemaExample( 448 | '{ "type": "record", "name": "foo", "logicalType": "bar", "fields": [], "foo": "bar" }', 449 | true, 450 | '{"type":"record","name":"foo","logicalType":"bar","foo":"bar","fields":[]}' 451 | ); 452 | 453 | self::$examples = array_merge($primitive_examples, 454 | $fixed_examples, 455 | $enum_examples, 456 | $array_examples, 457 | $map_examples, 458 | $union_examples, 459 | $record_examples); 460 | self::$valid_examples = array(); 461 | foreach (self::$examples as $example) 462 | { 463 | if ($example->is_valid) 464 | self::$valid_examples []= $example; 465 | } 466 | } 467 | 468 | function test_json_decode() 469 | { 470 | $this->assertEquals(json_decode('null', true), null); 471 | $this->assertEquals(json_decode('32', true), 32); 472 | $this->assertEquals(json_decode('"32"', true), '32'); 473 | $this->assertEquals((array) json_decode('{"foo": 27}'), array("foo" => 27)); 474 | $this->assertTrue(is_array(json_decode('{"foo": 27}', true))); 475 | $this->assertEquals(json_decode('{"foo": 27}', true), array("foo" => 27)); 476 | $this->assertEquals(json_decode('["bar", "baz", "blurfl"]', true), 477 | array("bar", "baz", "blurfl")); 478 | $this->assertFalse(is_array(json_decode('null', true))); 479 | $this->assertEquals(json_decode('{"type": "null"}', true), array("type" => 'null')); 480 | $this->assertEquals(json_decode('"boolean"'), 'boolean'); 481 | } 482 | 483 | function parse_bad_json_provider() 484 | { 485 | return array( 486 | // Valid 487 | array('{"type": "array", "items": "long"}', null), 488 | // Trailing comma 489 | array('{"type": "array", "items": "long", }', "JSON decode error 4: Syntax error"), 490 | // Wrong quotes 491 | array("{'type': 'array', 'items': 'long'}", "JSON decode error 4: Syntax error"), 492 | // Binary data 493 | array("\x11\x07", "JSON decode error 3: Control character error, possibly incorrectly encoded"), 494 | ); 495 | } 496 | 497 | /** 498 | * @dataProvider parse_bad_json_provider 499 | */ 500 | function test_parse_bad_json($json, $failure) 501 | { 502 | if (defined('HHVM_VERSION')) 503 | { 504 | // Under HHVM, json_decode is not as strict and feature complete as standard PHP. 505 | $this->markTestSkipped(); 506 | } 507 | try 508 | { 509 | $schema = AvroSchema::parse($json); 510 | $this->assertEquals($failure, null); 511 | } 512 | catch (AvroSchemaParseException $e) 513 | { 514 | $this->assertEquals($failure, $e->getMessage()); 515 | } 516 | } 517 | 518 | /** 519 | * @return array 520 | */ 521 | function schema_examples_provider() 522 | { 523 | self::make_examples(); 524 | $ary = array(); 525 | foreach (self::$examples as $example) 526 | $ary []= array($example); 527 | return $ary; 528 | } 529 | 530 | /** 531 | * @dataProvider schema_examples_provider 532 | * @param $example 533 | */ 534 | function test_parse($example) 535 | { 536 | $schema_string = $example->schema_string; 537 | try 538 | { 539 | $normalized_schema_string = $example->normalized_schema_string; 540 | $schema = AvroSchema::parse($schema_string); 541 | $this->assertTrue($example->is_valid, 542 | sprintf("schema_string: %s\n", 543 | $schema_string)); 544 | // strval() roughly does to_avro() + json_encode() 545 | $this->assertEquals($normalized_schema_string, strval($schema)); 546 | } 547 | catch (AvroSchemaParseException $e) 548 | { 549 | $this->assertFalse($example->is_valid, 550 | sprintf("schema_string: %s\n%s", 551 | $schema_string, 552 | $e->getMessage())); 553 | } 554 | } 555 | 556 | function test_record_doc() 557 | { 558 | $json = '{"type": "record", "name": "foo", "doc": "Foo doc.", 559 | "fields": [{"name": "bar", "type": "int", "doc": "Bar doc."}]}'; 560 | $schema = AvroSchema::parse($json); 561 | $this->assertEquals($schema->doc(), "Foo doc."); 562 | $fields = $schema->fields(); 563 | $this->assertCount(1, $fields); 564 | $bar = $fields[0]; 565 | $this->assertEquals($bar->doc(), "Bar doc."); 566 | } 567 | 568 | function test_enum_doc() 569 | { 570 | $json = '{"type": "enum", "name": "blood_types", "doc": "AB is freaky.", "symbols": ["A", "AB", "B", "O"]}'; 571 | $schema = AvroSchema::parse($json); 572 | $this->assertEquals($schema->doc(), "AB is freaky."); 573 | } 574 | 575 | function test_enum_default() 576 | { 577 | $json = '{"type": "enum", "name": "blood_types", "symbols": ["A", "AB", "B", "O"]}'; 578 | $schema = AvroSchema::parse($json); 579 | 580 | $this->assertEquals(null, $schema->default_value()); 581 | $this->assertEquals(false, $schema->has_default_value()); 582 | 583 | 584 | $json = '{"type": "enum", "name": "blood_types", "default": "AB", "symbols": ["A", "AB", "B", "O"]}'; 585 | $schema = AvroSchema::parse($json); 586 | 587 | $this->assertEquals([ 588 | 'type' => 'enum', 589 | 'name' => 'blood_types', 590 | 'default' => 'AB', 591 | 'symbols' => ["A", "AB", "B", "O"], 592 | ], $schema->to_avro()); 593 | 594 | $this->assertEquals('AB', $schema->default_value()); 595 | $this->assertEquals(true, $schema->has_default_value()); 596 | } 597 | 598 | function test_logical_type() 599 | { 600 | $json = '{ "type": "bytes", "logicalType": "decimal", "precision": 4, "scale": 2 }'; 601 | $schema = AvroSchema::parse($json); 602 | $this->assertEquals($schema->logical_type(), "decimal"); 603 | $this->assertEquals($schema->extra_attributes(), ["precision" => 4, "scale" => 2]); 604 | } 605 | } 606 | -------------------------------------------------------------------------------- /test/StringIOTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(0, $strio->tell()); 32 | $str = 'foo'; 33 | $strlen = strlen($str); 34 | $this->assertEquals($strlen, $strio->write($str)); 35 | $this->assertEquals($strlen, $strio->tell()); 36 | } 37 | 38 | public function test_seek() 39 | { 40 | $this->markTestIncomplete('This test has not been implemented yet.'); 41 | } 42 | 43 | public function test_tell() 44 | { 45 | $this->markTestIncomplete('This test has not been implemented yet.'); 46 | } 47 | 48 | public function test_read() 49 | { 50 | $this->markTestIncomplete('This test has not been implemented yet.'); 51 | } 52 | 53 | public function test_string_rep() 54 | { 55 | $writers_schema_json = '"null"'; 56 | $writers_schema = AvroSchema::parse($writers_schema_json); 57 | $datum_writer = new AvroIODatumWriter($writers_schema); 58 | $strio = new AvroStringIO(); 59 | $this->assertEquals('', $strio->string()); 60 | $dw = new AvroDataIOWriter($strio, $datum_writer, $writers_schema_json); 61 | $dw->close(); 62 | 63 | $this->assertEquals(57, strlen($strio->string()), 64 | AvroDebug::ascii_string($strio->string())); 65 | 66 | $read_strio = new AvroStringIO($strio->string()); 67 | 68 | $datum_reader = new AvroIODatumReader(); 69 | $dr = new AvroDataIOReader($read_strio, $datum_reader); 70 | $read_data = $dr->data(); 71 | $datum_count = count($read_data); 72 | $this->assertEquals(0, $datum_count); 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /test/data/user.avsc: -------------------------------------------------------------------------------- 1 | {"type": "record", "namespace": "example.avro", "name": "User", "fields": [{"type": "string", "name": "name"}, {"type": ["string", "null"], "name": "favorite_color"}, {"type": {"items": "int", "type": "array"}, "name": "favorite_numbers"}]} 2 | -------------------------------------------------------------------------------- /test/data/users.avro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flix-tech/avro-php/1d537bfe4ecab405917d15eee8f2020743fc8596/test/data/users.avro -------------------------------------------------------------------------------- /test/data/users.json: -------------------------------------------------------------------------------- 1 | {"name":"Alyssa","favorite_color":null,"favorite_numbers":[3,9,15,20]} 2 | -------------------------------------------------------------------------------- /test/data/users_deflate.avro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flix-tech/avro-php/1d537bfe4ecab405917d15eee8f2020743fc8596/test/data/users_deflate.avro -------------------------------------------------------------------------------- /test/data/users_snappy.avro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flix-tech/avro-php/1d537bfe4ecab405917d15eee8f2020743fc8596/test/data/users_snappy.avro -------------------------------------------------------------------------------- /test/test_helper.php: -------------------------------------------------------------------------------- 1 |