├── .github ├── CODE_OF_CONDUCT.md └── workflows │ ├── core.yml │ └── stage.yml ├── .gitignore ├── CHANGELOG.org ├── CONTRIBUTING.md ├── CONTRIBUTORS.md ├── LICENSE.md ├── README.md ├── bin └── push-cachix ├── ci └── stack-test ├── emacs └── theta-mode.el ├── flake.lock ├── flake.nix ├── guide ├── example │ ├── ids.theta │ └── music.theta └── index.md ├── nix ├── lib │ ├── theta-python.nix │ └── theta-rust.nix └── overlays │ ├── python.nix │ └── rust.nix ├── python ├── default.nix ├── setup.cfg ├── setup.py └── theta │ ├── __init__.py │ ├── avro.py │ └── container.py ├── rust ├── Cargo.lock ├── Cargo.toml ├── default.nix └── src │ ├── avro.rs │ ├── container.rs │ └── lib.rs ├── test ├── avro │ ├── default.nix │ └── modules │ │ ├── example.theta │ │ └── unicode.theta ├── cli │ ├── default.nix │ └── modules │ │ ├── enum.theta │ │ ├── foo.theta │ │ ├── importing_foo.theta │ │ ├── nested │ │ └── example.theta │ │ └── other.theta ├── cross-language │ ├── README.md │ ├── default.nix │ ├── modules │ │ ├── everything.theta │ │ └── primitives.theta │ ├── python │ │ ├── cat_everything │ │ │ └── cat.py │ │ ├── default.nix │ │ ├── setup.cfg │ │ └── setup.py │ ├── rust │ │ ├── Cargo.lock │ │ ├── Cargo.toml │ │ ├── default.nix │ │ └── src │ │ │ └── main.rs │ ├── test │ │ ├── Main.hs │ │ └── Test │ │ │ ├── Everything.hs │ │ │ └── Primitives.hs │ └── theta-tests.cabal ├── default.nix ├── kotlin │ ├── default.nix │ ├── modules │ │ ├── enum.theta │ │ ├── importing.theta │ │ ├── kotlin_tests.theta │ │ ├── newtype.theta │ │ ├── primitives.theta │ │ ├── shadowing.theta │ │ └── variant.theta │ └── run-kotlin-tests.kt ├── python │ ├── README.md │ ├── avro │ │ ├── container-deflate.avro │ │ └── container.avro │ ├── default.nix │ ├── modules │ │ ├── com │ │ │ └── example │ │ │ │ ├── importing_nested.theta │ │ │ │ └── nested.theta │ │ ├── container.theta │ │ ├── enum.theta │ │ ├── importing.theta │ │ ├── newtype.theta │ │ ├── primitives.theta │ │ ├── python_tests.theta │ │ ├── shadowing.theta │ │ └── variant.theta │ ├── setup.cfg │ ├── setup.py │ └── theta_python_tests │ │ └── test_python.py ├── rust │ ├── Cargo.lock │ ├── Cargo.toml │ ├── README.md │ ├── default.nix │ ├── modules │ │ ├── enums.theta │ │ ├── primitives.theta │ │ ├── rust.theta │ │ └── shadowing.theta │ ├── src │ │ └── lib.rs │ └── theta-rust └── shell.nix └── theta ├── .hlint.yaml ├── apps ├── Apps │ ├── Avro.hs │ ├── Hash.hs │ ├── Kotlin.hs │ ├── List.hs │ ├── Python.hs │ ├── Rust.hs │ └── Subcommand.hs └── Theta.hs ├── bin └── profile-compile-times ├── default.nix ├── hie.yaml ├── src └── Theta │ ├── Error.hs │ ├── Fixed.hs │ ├── Hash.hs │ ├── Import.hs │ ├── LoadPath.hs │ ├── Metadata.hs │ ├── Name.hs │ ├── Parser.hs │ ├── Pretty.hs │ ├── Primitive.hs │ ├── Target │ ├── Avro │ │ ├── Error.hs │ │ ├── Process.hs │ │ ├── Types.hs │ │ └── Values.hs │ ├── Haskell.hs │ ├── Haskell │ │ ├── Conversion.hs │ │ └── HasTheta.hs │ ├── Kotlin.hs │ ├── Kotlin │ │ └── QuasiQuoter.hs │ ├── LanguageQuoter.hs │ ├── Python.hs │ ├── Python │ │ └── QuasiQuoter.hs │ ├── Rust.hs │ └── Rust │ │ └── QuasiQuoter.hs │ ├── Test │ └── Assertions.hs │ ├── Types.hs │ ├── Value.hs │ └── Versions.hs ├── stack-shell.nix ├── stack.yaml ├── test ├── Test.hs ├── Test │ └── Theta │ │ ├── Error.hs │ │ ├── Import.hs │ │ ├── LoadPath.hs │ │ ├── Name.hs │ │ ├── Parser.hs │ │ ├── Target │ │ ├── Avro │ │ │ ├── Process.hs │ │ │ ├── Types.hs │ │ │ └── Values.hs │ │ ├── Haskell.hs │ │ ├── Haskell │ │ │ └── Conversion.hs │ │ ├── Kotlin.hs │ │ ├── Python.hs │ │ ├── Python │ │ │ └── QuasiQuoter.hs │ │ └── Rust.hs │ │ ├── Types.hs │ │ ├── Value.hs │ │ └── Versions.hs └── data │ ├── importing │ ├── direct_a.theta │ ├── direct_b.theta │ ├── importing.theta │ └── indirect_a.theta │ ├── modules │ ├── documentation.theta │ ├── enums.theta │ ├── fixed.theta │ ├── foo.theta │ ├── importing_foo.theta │ ├── logical_dates.theta │ ├── named_types.theta │ ├── nested_newtypes.theta │ ├── newtype.theta │ ├── primitives.theta │ ├── recursive.theta │ ├── rust.theta │ ├── unsupported_avro_version.theta │ └── unsupported_theta_version.theta │ ├── python │ ├── bar_reference.mustache │ ├── empty_record.mustache │ ├── enum.mustache │ ├── foo_reference.mustache │ ├── importing_foo.mustache │ ├── importing_foo_qualified.mustache │ ├── importing_reference.mustache │ ├── newtype.mustache │ ├── one_case.mustache │ ├── one_case_importing.mustache │ ├── one_field.mustache │ ├── two_cases.mustache │ └── two_fields.mustache │ ├── root-1 │ └── a.theta │ ├── root-2 │ └── b.theta │ ├── root-3 │ └── com │ │ ├── example.theta │ │ └── example │ │ ├── nested.theta │ │ ├── nested │ │ └── deeply.theta │ │ └── nested_2.theta │ ├── rust │ ├── enums.rs │ ├── newtype.rs │ ├── recursive.rs │ └── single_file.rs │ └── transitive │ ├── a.theta │ ├── b.theta │ ├── c.theta │ └── d.theta └── theta.cabal /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to make participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at 59 | [opensource@target.com](mailto:opensource@target.com). All 60 | complaints will be reviewed and investigated and will result in a response that 61 | is deemed necessary and appropriate to the circumstances. The project team is 62 | obligated to maintain confidentiality with regard to the reporter of an incident. 63 | Further details of specific enforcement policies may be posted separately. 64 | 65 | Project maintainers who do not follow or enforce the Code of Conduct in good 66 | faith may face temporary or permanent repercussions as determined by other 67 | members of the project's leadership. 68 | 69 | ## Attribution 70 | 71 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 72 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 73 | 74 | [homepage]: https://www.contributor-covenant.org 75 | -------------------------------------------------------------------------------- /.github/workflows/core.yml: -------------------------------------------------------------------------------- 1 | # Base tests we can run on every commit/PR/etc 2 | # 3 | # Thanks to Nix caching, this will only run tests for components that 4 | # have changed since previous successful runs. 5 | name: Core Tests 6 | 7 | on: 8 | push: 9 | branches: 10 | - 'stage' 11 | pull_request: 12 | 13 | jobs: 14 | test: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | 20 | - name: Install Nix 21 | uses: cachix/install-nix-action@v15 22 | 23 | - name: Cachix 24 | uses: cachix/cachix-action@v10 25 | with: 26 | name: theta-idl 27 | authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' 28 | 29 | - name: Build Theta 30 | run: nix build .#theta -L 31 | 32 | - name: Cross-Language Tests 33 | run: nix build .#test -L 34 | -------------------------------------------------------------------------------- /.github/workflows/stage.yml: -------------------------------------------------------------------------------- 1 | # Additional tests run on merge into stage 2 | name: Stage Tests 3 | 4 | on: 5 | push: 6 | branches: 7 | - 'stage' 8 | 9 | jobs: 10 | stack-test: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Install Nix 17 | uses: cachix/install-nix-action@v15 18 | 19 | - name: Cachix 20 | uses: cachix/cachix-action@v10 21 | with: 22 | name: theta-idl 23 | authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' 24 | 25 | - name: Cache Stack Dependencies 26 | uses: actions/cache@v2 27 | with: 28 | path: | 29 | ~/.stack 30 | .stack-work 31 | key: ${{ runner.os }}-stack-work-${{ hashFiles('theta/stack.yaml.lock', 'theta/theta.cabal') }} 32 | 33 | - name: Stack Test 34 | run: ci/stack-test ./theta 35 | 36 | test_ghc_902: 37 | runs-on: ubuntu-latest 38 | 39 | steps: 40 | - uses: actions/checkout@v2 41 | 42 | - name: Install Nix 43 | uses: cachix/install-nix-action@v15 44 | 45 | - name: Cachix 46 | uses: cachix/cachix-action@v10 47 | with: 48 | name: theta-idl 49 | authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' 50 | 51 | - name: Build Theta 52 | run: nix build .#theta_902 -L 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Autogenerated *.avsc schemas 2 | *.avsc 3 | 4 | # Nix 5 | result 6 | result-* 7 | 8 | # Direnv 9 | .direnv 10 | .envrc 11 | 12 | # Emacs 13 | *~ 14 | \#*\# 15 | /.emacs.desktop 16 | /.emacs.desktop.lock 17 | *.elc 18 | auto-save-list 19 | tramp 20 | .\#* 21 | 22 | # Org-mode 23 | .org-id-locations 24 | *_archive 25 | 26 | # flymake-mode 27 | *_flymake.* 28 | 29 | # eshell files 30 | /eshell/history 31 | /eshell/lastdir 32 | 33 | # elpa packages 34 | /elpa/ 35 | 36 | # reftex files 37 | *.rel 38 | 39 | # AUCTeX auto folder 40 | /auto/ 41 | 42 | # cask packages 43 | .cask/ 44 | dist/ 45 | 46 | # Flycheck 47 | flycheck_*.el 48 | 49 | # server auth directory 50 | /server/ 51 | 52 | # projectiles files 53 | .projectile 54 | 55 | # directory configuration 56 | .dir-locals.el 57 | 58 | # Haskell 59 | dist 60 | dist-* 61 | cabal-dev 62 | *.o 63 | *.hi 64 | *.chi 65 | *.chs.h 66 | *.dyn_o 67 | *.dyn_hi 68 | .hpc 69 | .hsenv 70 | .cabal-sandbox/ 71 | cabal.sandbox.config 72 | *.prof 73 | *.aux 74 | *.hp 75 | *.eventlog 76 | .stack-work/ 77 | stack.yaml.lock 78 | cabal.project.local 79 | cabal.project.local~ 80 | .HTF/ 81 | .ghc.environment.* 82 | 83 | # Rust 84 | Cargo.lock 85 | target 86 | 87 | # Python 88 | __pycache__ 89 | *.pyc 90 | .hypothesis 91 | 92 | # Kotlin 93 | .gradle 94 | build 95 | 96 | # Examples 97 | guide/*.py 98 | 99 | # macOS 100 | # General 101 | .DS_Store 102 | .AppleDouble 103 | .LSOverride 104 | 105 | # Icon must end with two \r 106 | Icon 107 | 108 | 109 | # Thumbnails 110 | ._* 111 | 112 | # Files that might appear in the root of a volume 113 | .DocumentRevisions-V100 114 | .fseventsd 115 | .Spotlight-V100 116 | .TemporaryItems 117 | .Trashes 118 | .VolumeIcon.icns 119 | .com.apple.timemachine.donotpresent 120 | 121 | # Directories potentially created on remote AFP share 122 | .AppleDB 123 | .AppleDesktop 124 | Network Trash Folder 125 | Temporary Items 126 | .apdisk 127 | -------------------------------------------------------------------------------- /CHANGELOG.org: -------------------------------------------------------------------------------- 1 | * 1.0.0.0 2 | 3 | Theta 1.0! A fresh start. 4 | 5 | This version has a bunch of breaking changes with the internal version 6 | of Theta—housekeeping and improvements based on experience using Theta 7 | across multiple production-grade projects in Haskell, Python and Rust. 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ### Issues 4 | 5 | Please feel free to submit bug reports, questions and feature requests as issues. 6 | 7 | ### How to Contribute 8 | 9 | 1. Fork Theta and make your changes in a branch. 10 | 2. Open and submit a pull request. 11 | 12 | Take a look at the instructions in the [README] for how to use `nix` and `nix-shell` to work on different components of Theta. 13 | 14 | ### Making Contributions 15 | 16 | Feel free to open a pull request before your contribution is ready—an open PR makes it easier for others to comment on and help with your code. Please mark pull requests that aren't ready to merge as drafts. 17 | 18 | Before a pull request is merged, it will need to meet a few requirements: 19 | 20 | * Haskell code should follow the same style as the rest of the codebase. Please use [stylish-haskell] (provided in the main `nix-shell`) with its default config to format each Haskell file. 21 | * Identifiers exported from any Haskell module should have Haddock comments. Feel free to add documentation comments to unexported definitions too! 22 | * Please include unit tests for any new functionality. 23 | * Tests pass and the Haskell code builds without warnings—you can check this locally with `nix-build test`. 24 | 25 | [stylish-haskell]: https://github.com/jaspervdj/stylish-haskell 26 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | # Contributors 2 | Thank you to [all of our contributors](https://github.com/target/theta-idl/graphs/contributors). For reviewing per-file contributions, run the following commands: 3 | ```sh 4 | git blame 5 | git log -p 6 | ``` 7 | 8 | # Target Team 9 | 10 | Theta was originally built as an internal tool at Target, with contributions from: 11 | 12 | - Tikhon Jelvis 13 | - Greg Hale 14 | - Aron Gruzman 15 | - Badi' Abdul-Wahid 16 | - Moritz Drexl 17 | - Alex Biehl 18 | - Adelbert Chang 19 | - Daniel Winograd-Cort 20 | - Trevor McDonald 21 | - Sooraj Bhat 22 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (C) 2020 Target Brands, Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at 4 | 5 | http://www.apache.org/licenses/LICENSE-2.0 6 | 7 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 8 | -------------------------------------------------------------------------------- /bin/push-cachix: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | read -s -p $'Cachix Token for theta-idl:\n' token 4 | cachix authtoken $token 5 | 6 | # Push the build dependencies of the given target: 7 | function push_dependencies { 8 | nix build --json $1 | jq -r '.[].outputs | to_entries[].value' | cachix push theta-idl 9 | } 10 | 11 | push_dependencies '.#theta' 12 | push_dependencies '.#theta_902' 13 | push_dependencies '.#test' 14 | -------------------------------------------------------------------------------- /ci/stack-test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Run Stack test on the given directory, setting up the Nix 4 | # integration with Nixpkgs controlled by sources.nix 5 | 6 | # Should be run from top-level directory in repo: 7 | # 8 | # ci/stack-test theta 9 | 10 | # Slightly fiddly way to get the path to the version of Nixpkgs pinned 11 | # by the top-level Theta Flake 12 | function get_metadata { 13 | eval echo "$(nix flake metadata $1 --json | jq $2)" 14 | } 15 | nixpkgs_rev=$(get_metadata . .locks.nodes.nixpkgs.locked.rev) 16 | nixpkgs_path=$(get_metadata "github:NixOS/nixpkgs/$nixpkgs_rev" .path) 17 | 18 | cd $1 19 | stack --nix \ 20 | --nix-shell-file stack-shell.nix \ 21 | --nix-path="nixpkgs=$nixpkgs_path" \ 22 | test 23 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "locked": { 5 | "lastModified": 1644229661, 6 | "narHash": "sha256-1YdnJAsNy69bpcjuoKdOYQX0YxZBiCYZo4Twxerqv7k=", 7 | "owner": "numtide", 8 | "repo": "flake-utils", 9 | "rev": "3cecb5b042f7f209c56ffd8371b2711a290ec797", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "numtide", 14 | "repo": "flake-utils", 15 | "type": "github" 16 | } 17 | }, 18 | "naersk-flake": { 19 | "inputs": { 20 | "nixpkgs": "nixpkgs" 21 | }, 22 | "locked": { 23 | "lastModified": 1639947939, 24 | "narHash": "sha256-pGsM8haJadVP80GFq4xhnSpNitYNQpaXk4cnA796Cso=", 25 | "owner": "nix-community", 26 | "repo": "naersk", 27 | "rev": "2fc8ce9d3c025d59fee349c1f80be9785049d653", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "nix-community", 32 | "repo": "naersk", 33 | "type": "github" 34 | } 35 | }, 36 | "nixpkgs": { 37 | "locked": { 38 | "lastModified": 1647887150, 39 | "narHash": "sha256-1TnRvE3qhhafrQnGapaaSVue6nEwRUICkz227TfHHQw=", 40 | "owner": "NixOS", 41 | "repo": "nixpkgs", 42 | "rev": "4c3c80df545ec5cb26b5480979c3e3f93518cbe5", 43 | "type": "github" 44 | }, 45 | "original": { 46 | "id": "nixpkgs", 47 | "type": "indirect" 48 | } 49 | }, 50 | "nixpkgs_2": { 51 | "locked": { 52 | "lastModified": 1648136313, 53 | "narHash": "sha256-y/an2ms+XiGctRz9EuiiRLdaPKanhpc0UApILIp/Ho0=", 54 | "owner": "NixOS", 55 | "repo": "nixpkgs", 56 | "rev": "6ea8d5ee71793e236a19af3b5686a1ccdb0af3da", 57 | "type": "github" 58 | }, 59 | "original": { 60 | "owner": "NixOS", 61 | "repo": "nixpkgs", 62 | "type": "github" 63 | } 64 | }, 65 | "root": { 66 | "inputs": { 67 | "flake-utils": "flake-utils", 68 | "naersk-flake": "naersk-flake", 69 | "nixpkgs": "nixpkgs_2", 70 | "rust-overlay": "rust-overlay" 71 | } 72 | }, 73 | "rust-overlay": { 74 | "locked": { 75 | "lastModified": 1645464064, 76 | "narHash": "sha256-YeN4bpPvHkVOpQzb8APTAfE7/R+MFMwJUMkqmfvytSk=", 77 | "owner": "mozilla", 78 | "repo": "nixpkgs-mozilla", 79 | "rev": "15b7a05f20aab51c4ffbefddb1b448e862dccb7d", 80 | "type": "github" 81 | }, 82 | "original": { 83 | "owner": "mozilla", 84 | "repo": "nixpkgs-mozilla", 85 | "type": "github" 86 | } 87 | } 88 | }, 89 | "root": "root", 90 | "version": 7 91 | } 92 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Theta is a tool for sharing algebraic data types between different languages. You can write a schema once, generate friendly bindings in Haskell, Python and Rust then share data between programs with Avro."; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:NixOS/nixpkgs"; 6 | flake-utils.url = "github:numtide/flake-utils"; 7 | naersk-flake.url = "github:nix-community/naersk"; 8 | rust-overlay.url = "github:mozilla/nixpkgs-mozilla"; 9 | }; 10 | 11 | outputs = { self, nixpkgs, flake-utils, naersk-flake, rust-overlay }: 12 | flake-utils.lib.eachDefaultSystem (system: 13 | let 14 | source-overrides = { 15 | aeson = "2.0.3.0"; 16 | aeson-pretty = "0.8.9"; 17 | attoparsec = "0.14.4"; 18 | avro = "0.6.0.1"; 19 | hashable = "1.4.0.2"; 20 | OneTuple = "0.3.1"; 21 | PyF = "0.10.2.0"; 22 | quickcheck-instances = "0.3.27"; 23 | semialign = "1.2.0.1"; 24 | stache = "2.3.1"; 25 | streamly = "0.8.1.1"; 26 | streamly-bytestring = "0.1.4"; 27 | streamly-process = "0.2.0"; 28 | text-short = "0.1.5"; 29 | time-compat = "1.9.6.1"; 30 | unordered-containers = "0.2.16.0"; 31 | versions = "5.0.2"; 32 | }; 33 | 34 | overrides = new: old: { 35 | foldl = pkgs.haskell.lib.doJailbreak old.foldl; 36 | streamly-process = pkgs.haskell.lib.dontCheck old.streamly-process; 37 | }; 38 | 39 | theta_8107 = import ./theta { 40 | inherit pkgs source-overrides overrides; 41 | compiler-version = "ghc8107"; 42 | }; 43 | theta_902 = import ./theta { 44 | inherit pkgs source-overrides overrides; 45 | compiler-version = "ghc902"; 46 | }; 47 | 48 | # default is 8.10.7 (for now?) 49 | theta = theta_8107; 50 | 51 | rust = import ./rust { inherit pkgs; }; 52 | python = import ./python { inherit pkgs; }; 53 | test = import ./test { inherit pkgs lib source-overrides overrides; }; 54 | 55 | theta-overlay = final: current: { 56 | inherit theta; 57 | theta-rust = rust; 58 | theta-python = python; 59 | }; 60 | 61 | haskell-overlay = final: current: { 62 | all-cabal-hashes = current.fetchurl { 63 | url = "https://github.com/commercialhaskell/all-cabal-hashes/archive/eb3b21c9f04e3a913efd61346bad427d92df3d1b.tar.gz"; 64 | sha256 = "0mm6y1zq1h7j17489fkyb18rfc2z0aglp5ly9f12jzhg5c6z85b7"; 65 | }; 66 | }; 67 | 68 | overlays = [ 69 | theta-overlay 70 | haskell-overlay 71 | rust-overlay.overlay 72 | (import nix/overlays/rust.nix { inherit naersk-flake; }) 73 | (import nix/overlays/python.nix { python-version = "3.8"; }) 74 | ]; 75 | 76 | pkgs = import nixpkgs { inherit system overlays; }; 77 | 78 | lib = { 79 | theta-rust = import nix/lib/theta-rust.nix { inherit pkgs; }; 80 | theta-python = import nix/lib/theta-python.nix { inherit pkgs; }; 81 | }; 82 | in rec { 83 | inherit overlays lib; 84 | 85 | packages = { 86 | inherit theta theta_8107 theta_902 rust python test; 87 | }; 88 | 89 | devShells = { 90 | theta = packages.theta.env; 91 | theta_8107 = packages.theta_8107.env; 92 | theta_902 = packages.theta_902.env; 93 | rust = pkgs.mkShell { 94 | nativeBuildInputs = pkgs.rust-dev-tools; 95 | }; 96 | test = import ./test/shell.nix { inherit pkgs lib; }; 97 | }; 98 | 99 | defaultPackage = packages.theta; 100 | }); 101 | } 102 | -------------------------------------------------------------------------------- /guide/example/ids.theta: -------------------------------------------------------------------------------- 1 | language-version: 1.0.0 2 | avro-version: 1.0.0 3 | --- 4 | 5 | type AlbumId = { 6 | id: String 7 | } -------------------------------------------------------------------------------- /guide/example/music.theta: -------------------------------------------------------------------------------- 1 | language-version: 1.0.0 2 | avro-version: 1.0.0 3 | --- 4 | 5 | import ids 6 | 7 | /** We keep track of people and bands separately because 8 | * people are usually sorted by part of their name (ie 9 | * last name) while bands are always sorted by their 10 | * whole name. 11 | */ 12 | type ArtistName = Band { name : String } 13 | | Person { 14 | sorting_name : String, 15 | name_remainder : String? 16 | } 17 | 18 | type Track = { 19 | title : String, 20 | length : Int, 21 | artists : [ArtistName] 22 | } 23 | 24 | type Album = { 25 | id : ids.AlbumId, 26 | title : String, 27 | published : Date, 28 | artists : [ArtistName], 29 | tracks : [Track] 30 | } 31 | -------------------------------------------------------------------------------- /nix/lib/theta-python.nix: -------------------------------------------------------------------------------- 1 | { pkgs }: 2 | { modules 3 | , theta-paths 4 | , prefix ? null 5 | , ... 6 | } @ attrs: 7 | let 8 | inherit (pkgs.lib) strings; 9 | 10 | to-remove = [ "modules" "theta-paths" "prefix" "buildInputs" ]; 11 | 12 | # Convert a list of directories to a Theta load path, with each 13 | # directory starting in "$src". 14 | THETA_LOAD_PATH = strings.concatStringsSep ":" theta-paths; 15 | 16 | module-flags = strings.concatStringsSep " " (map (name: "-m ${name}") modules); 17 | 18 | prefix-flag = if prefix == null then "" else "--prefix ${prefix}"; 19 | in pkgs.stdenv.mkDerivation (builtins.removeAttrs attrs to-remove // { 20 | buildInputs = (attrs.buildInputs or []) ++ [ pkgs.theta ]; 21 | 22 | installPhase = '' 23 | export THETA_LOAD_PATH="${THETA_LOAD_PATH}" 24 | 25 | # Genrate Python code for the listed Theta modules 26 | mkdir -p $out 27 | theta python ${module-flags} -o $out ${prefix-flag} 28 | ''; 29 | }) 30 | -------------------------------------------------------------------------------- /nix/lib/theta-rust.nix: -------------------------------------------------------------------------------- 1 | { pkgs }: 2 | { modules 3 | , theta-paths 4 | , ... 5 | } @ attrs: 6 | let 7 | inherit (pkgs.lib) strings; 8 | 9 | to-remove = [ "modules" "theta-paths" "buildInputs" ]; 10 | 11 | # Convert a list of directories to a Theta load path, with each 12 | # directory starting in "$src". 13 | THETA_LOAD_PATH = strings.concatStringsSep ":" theta-paths; 14 | 15 | module-flags = strings.concatStringsSep " " (map (name: "-m ${name}") modules); 16 | in pkgs.stdenv.mkDerivation (builtins.removeAttrs attrs to-remove // { 17 | buildInputs = (attrs.buildInputs or []) ++ [ pkgs.theta ]; 18 | 19 | installPhase = '' 20 | export THETA_LOAD_PATH="${THETA_LOAD_PATH}" 21 | 22 | # Generate Rust code for the listed Theta modules 23 | mkdir -p $out/src 24 | theta rust ${module-flags} > $out/src/$name.rs 25 | ''; 26 | }) 27 | -------------------------------------------------------------------------------- /nix/overlays/python.nix: -------------------------------------------------------------------------------- 1 | # Overlay setting up the right version of Python to use. 2 | { python-version }: 3 | pkgs: 4 | _: 5 | let 6 | pythonVersion = v: pkgs.lib.replaceStrings ["."] [""] v; 7 | inherit (pkgs) fetchFromGitHub; 8 | in rec { 9 | pythonPackages = pkgs."python${pythonVersion python-version}".override { 10 | packageOverrides = final: current: { 11 | hypothesis = current.hypothesis.overridePythonAttrs(old: rec { 12 | version = "4.37.0"; 13 | src = fetchFromGitHub { 14 | owner = "HypothesisWorks"; 15 | repo = "hypothesis-python"; 16 | rev = "hypothesis-python-${version}"; 17 | sha256 = "1j9abaapwnvms747rv8izspkgb5qa6jdkl7vp5ij9kfp1cvi7llp"; 18 | }; 19 | doCheck = false; 20 | }); 21 | }; 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /nix/overlays/rust.nix: -------------------------------------------------------------------------------- 1 | # Overlay pinning the version of Rust nightly we use 2 | { naersk-flake }: 3 | final: 4 | current: 5 | let 6 | rust-with-src = final.rust-nightly.rust.override { 7 | extensions = [ "rust-src" ]; 8 | }; 9 | rustPlatform = final.makeRustPlatform { 10 | cargo = final.rust-nightly.cargo; 11 | rustc = rust-with-src; 12 | }; 13 | rust-tools = with rustPlatform.rust; [ rustc cargo ]; 14 | 15 | frameworks = final.darwin.apple_sdk.frameworks; 16 | darwin = if final.stdenv.isDarwin then [ frameworks.Security ] else []; 17 | in rec { 18 | rust-nightly = final.rustChannelOf { 19 | channel = "nightly"; 20 | date = "2020-08-27"; 21 | sha256 = "0d9bna9l8w7sps7hqjq35835p2pp73dvy3y367b0z3wg1ha7gvjj"; 22 | }; 23 | 24 | naersk = naersk-flake.lib.${final.system}.override { 25 | cargo = rust-nightly.rust; 26 | rustc = rust-nightly.rust; 27 | }; 28 | 29 | # Tools for Rust development in Nix shells 30 | rust-dev-tools = rust-tools ++ darwin; 31 | } 32 | -------------------------------------------------------------------------------- /python/default.nix: -------------------------------------------------------------------------------- 1 | { pkgs }: 2 | let 3 | python = pkgs.pythonPackages.pkgs; 4 | in 5 | python.buildPythonPackage { 6 | pname = "theta-python"; 7 | version = "1.0.2"; 8 | src = ./.; 9 | 10 | checkInputs = [ python.hypothesis ]; 11 | 12 | # For development (nix develop) 13 | nativeBuildInputs = with pkgs; [ theta ]; 14 | 15 | meta = { 16 | description = "The library Theta uses for parsing Avro."; 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /python/setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = theta-python 3 | version = 0.0.1 4 | description = Support libraries for using Theta types in Python 5 | 6 | [options] 7 | packages = find: 8 | install_requires = 9 | hypothesis 10 | -------------------------------------------------------------------------------- /python/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup() 4 | -------------------------------------------------------------------------------- /python/theta/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/target/theta-idl/2bda6a8836817f8d782c2165d7f250fe296b97c2/python/theta/__init__.py -------------------------------------------------------------------------------- /rust/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "theta" 3 | version = "0.1.1" 4 | edition = "2018" 5 | 6 | [lib] 7 | 8 | [dependencies] 9 | chrono = "0.4.0" 10 | nom = "5.0.0" 11 | integer-encoding = "1.0.7" 12 | libflate = "0.1.24" 13 | quickcheck = "0.8.5" 14 | rand = "0.6.5" 15 | time = "0.1.42" 16 | uuid = "0.8.2" -------------------------------------------------------------------------------- /rust/default.nix: -------------------------------------------------------------------------------- 1 | { pkgs }: 2 | 3 | pkgs.naersk.buildPackage { 4 | src = builtins.filterSource 5 | (path: type: 6 | type != "directory" || builtins.baseNameOf path != "target") 7 | ./.; 8 | 9 | remapPathPrefix = true; 10 | doCheck = true; 11 | copyLibs = true; 12 | } 13 | -------------------------------------------------------------------------------- /rust/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![feature(test)] 2 | 3 | pub mod avro; 4 | 5 | pub mod container; 6 | -------------------------------------------------------------------------------- /test/avro/default.nix: -------------------------------------------------------------------------------- 1 | # This expression is meant to be built in CI. 2 | # 3 | # Check that Theta's command-line tool can generate Avro schemas 4 | # without any errors. 5 | { pkgs }: 6 | 7 | let 8 | inherit (pkgs) theta; 9 | in 10 | 11 | # Test that we can correctly handle Unicode text in Theta modules. 12 | # 13 | # Depending on Nix settings, it's possible to compile Haskell programs 14 | # with their locale set to ASCII; this test should catch that problem 15 | # for the main theta executable. 16 | pkgs.runCommand "theta-avro-test" { src = ./.; } '' 17 | mkdir -p $out 18 | cd $out 19 | 20 | # avro type 21 | ${theta}/bin/theta --path $src/modules avro type -o unicode.avsc --type unicode.Foo 22 | 23 | # check unicode.avsc exists 24 | if [ ! -e unicode.avsc ]; then echo "unicode.avsc missing"; exit 1; fi 25 | 26 | # avro all 27 | ${theta}/bin/theta --path $src/modules avro all -m unicode -m example 28 | 29 | # Check avro/unicode/Foo.avsc exists 30 | if [ ! -e avro/unicode/Foo.avsc ]; then echo "avro/unicode/Foo.avsc missing"; exit 1; fi 31 | 32 | # Check avro/example/Bar.avsc exists 33 | if [ ! -e avro/unicode/Foo.avsc ]; then echo "avro/unicode/Foo.avsc missing"; exit 1; fi 34 | '' 35 | -------------------------------------------------------------------------------- /test/avro/modules/example.theta: -------------------------------------------------------------------------------- 1 | language-version: 1.0.0 2 | avro-version: 1.0.0 3 | --- 4 | 5 | /// Some documentation. 6 | type Bar = { baz : Date } 7 | -------------------------------------------------------------------------------- /test/avro/modules/unicode.theta: -------------------------------------------------------------------------------- 1 | language-version: 1.0.0 2 | avro-version: 1.0.0 3 | --- 4 | 5 | type Foo = { λ : Int } 6 | -------------------------------------------------------------------------------- /test/cli/default.nix: -------------------------------------------------------------------------------- 1 | # This expression is meant to be built in CI. 2 | # 3 | # Check that various Theta cli subcommands have expected output. 4 | { pkgs }: 5 | let 6 | inherit (pkgs) theta; 7 | 8 | # Note the tab characters in the expected strings! 9 | expected-1 = pkgs.writeTextFile { 10 | name = "theta-hash-expected-1"; 11 | text = '' 12 | foo.Bar acbc45ad628ebccf04ec52d0d275a57c 13 | foo.Baz 261517de3a75f9f11bbdb3b496e08381 14 | ''; 15 | }; 16 | expected-2 = pkgs.writeTextFile { 17 | name = "theta-hash-expected-2"; 18 | text = '' 19 | other.Other a6fd63eefd826a232fb8bfc551200c1e 20 | other.OtherThing 814fc5791e3990d5f55d07e48f560889 21 | foo.Bar acbc45ad628ebccf04ec52d0d275a57c 22 | foo.Baz 261517de3a75f9f11bbdb3b496e08381 23 | ''; 24 | }; 25 | expected-enum = pkgs.writeTextFile { 26 | name = "theta-hash-expected-enum"; 27 | text = '' 28 | enum.Foo c8cf29e0b679d8fd4878ff397292ca56 29 | ''; 30 | }; 31 | expected-list-names = pkgs.writeTextFile { 32 | name = "theta-list-names-expected"; 33 | text = '' 34 | enum 35 | foo 36 | importing_foo 37 | nested.example 38 | other 39 | ''; 40 | }; 41 | expected-list-paths = pkgs.writeTextFile { 42 | name = "theta-list-paths-expected"; 43 | text = '' 44 | modules/enum.theta 45 | modules/foo.theta 46 | modules/importing_foo.theta 47 | modules/nested/example.theta 48 | modules/other.theta 49 | ''; 50 | }; 51 | expected-list-both = pkgs.writeTextFile { 52 | name = "theta-list-both-expected"; 53 | text = '' 54 | enum modules/enum.theta 55 | foo modules/foo.theta 56 | importing_foo modules/importing_foo.theta 57 | nested.example modules/nested/example.theta 58 | other modules/other.theta 59 | ''; 60 | }; 61 | in 62 | pkgs.runCommandWith { 63 | name = "theta-cli-test"; 64 | derivationArgs = {src = ./.;}; 65 | } '' 66 | cd $src 67 | export THETA_LOAD_PATH=modules 68 | 69 | mkdir -p $out 70 | 71 | ${theta}/bin/theta hash --type foo.Bar --type foo.Baz > $out/theta-hash-1 72 | diff ${expected-1} $out/theta-hash-1 73 | 74 | ${theta}/bin/theta hash -m importing_foo -m other > $out/theta-hash-2 75 | diff ${expected-2} $out/theta-hash-2 76 | 77 | ${theta}/bin/theta hash -m enum > $out/theta-hash-enum 78 | diff ${expected-enum} $out/theta-hash-enum 79 | 80 | ${theta}/bin/theta list modules > $out/theta-list-names 81 | diff ${expected-list-names} $out/theta-list-names 82 | 83 | ${theta}/bin/theta list modules --names > $out/theta-list-names 84 | diff ${expected-list-names} $out/theta-list-names 85 | 86 | ${theta}/bin/theta list modules --paths > $out/theta-list-paths 87 | diff ${expected-list-paths} $out/theta-list-paths 88 | 89 | ${theta}/bin/theta list modules --names --paths > $out/theta-list-both 90 | diff ${expected-list-both} $out/theta-list-both 91 | '' 92 | -------------------------------------------------------------------------------- /test/cli/modules/enum.theta: -------------------------------------------------------------------------------- 1 | language-version: 1.1.0 2 | avro-version: 1.0.0 3 | --- 4 | 5 | enum Foo = Bar | Baz 6 | -------------------------------------------------------------------------------- /test/cli/modules/foo.theta: -------------------------------------------------------------------------------- 1 | language-version: 1.0.0 2 | avro-version: 1.0.0 3 | --- 4 | 5 | // This is a minimal module for constructing small test cases. 6 | 7 | type Bar = { 8 | a : Int 9 | } 10 | 11 | type Baz = Long 12 | -------------------------------------------------------------------------------- /test/cli/modules/importing_foo.theta: -------------------------------------------------------------------------------- 1 | language-version: 1.0.0 2 | avro-version: 1.0.0 3 | --- 4 | 5 | // this module imports foo.theta to let use test for issues involving 6 | // imports 7 | 8 | import foo 9 | 10 | -------------------------------------------------------------------------------- /test/cli/modules/nested/example.theta: -------------------------------------------------------------------------------- 1 | language-version: 1.0.0 2 | avro-version: 1.0.0 3 | --- 4 | -------------------------------------------------------------------------------- /test/cli/modules/other.theta: -------------------------------------------------------------------------------- 1 | language-version: 1.0.0 2 | avro-version: 1.0.0 3 | --- 4 | 5 | type Other = String 6 | 7 | type OtherThing = A | B 8 | -------------------------------------------------------------------------------- /test/cross-language/README.md: -------------------------------------------------------------------------------- 1 | # Cross-Language Tests 2 | 3 | This directory sets up cross-language tests, checking that values round-trip through each pair of supported languages: 4 | 5 | * Haskell 6 | * Rust 7 | * Python 8 | 9 | The tests are coordinated through a Haskell project (`theta-tests.cabal`), checking against code generated in Python and Rust. 10 | -------------------------------------------------------------------------------- /test/cross-language/default.nix: -------------------------------------------------------------------------------- 1 | { pkgs 2 | 3 | , lib 4 | 5 | , compiler-version ? "ghc8107" 6 | 7 | , compiler ? pkgs.haskell.packages."${compiler-version}" 8 | 9 | , source-overrides ? {} 10 | 11 | , overrides ? {} 12 | 13 | , extra-build-tools ? [] # extra tools available for nix develop 14 | }: 15 | let 16 | haskell = compiler.extend (_: _: { 17 | inherit (pkgs) theta; 18 | }); 19 | 20 | rust-executable = import ./rust { inherit pkgs lib; }; 21 | 22 | python-executable = import ./python { inherit pkgs lib; }; 23 | 24 | build-tools = 25 | [ haskell.stylish-haskell 26 | haskell.cabal-install 27 | haskell.haskell-language-server 28 | haskell.hlint 29 | pkgs.stack 30 | pkgs.time-ghc-modules 31 | 32 | rust-executable 33 | python-executable 34 | ] ++ extra-build-tools; 35 | 36 | excluded = [ 37 | "dist" 38 | "dist-newstyle" 39 | "stack.yaml" 40 | ".stack-work" 41 | "stack.yaml.lock" 42 | "stack-shell.nix" 43 | ]; 44 | 45 | add-build-tools = p: 46 | pkgs.haskell.lib.addBuildTools p build-tools; 47 | in 48 | haskell.developPackage { 49 | name = "theta-tests"; 50 | root = ./.; 51 | 52 | inherit overrides source-overrides; 53 | 54 | modifier = add-build-tools; 55 | 56 | # explicitly disable "smart" detection of nix-shell status 57 | # 58 | # The default value of returnShellEnv is impure: it checks the 59 | # value of the IN_NIX_SHELL environment variable. 60 | returnShellEnv = false; 61 | } 62 | -------------------------------------------------------------------------------- /test/cross-language/modules/everything.theta: -------------------------------------------------------------------------------- 1 | // This module pulls in every kind of feature Theta has so that we can 2 | // test everything end-to-end. 3 | // 4 | // The module itself has a low language-version and avro-version, but 5 | // it imports modules with newer features. 6 | language-version: 1.1.0 7 | avro-version: 1.0.0 8 | --- 9 | 10 | import primitives 11 | 12 | /// The top-level record we use for cross-language tests. 13 | /// 14 | /// Add new types here for them to be included in the tests! 15 | type Everything = { 16 | primitives : primitives.Primitives, 17 | containers : primitives.Containers, 18 | options : Options, 19 | var : Var, 20 | wrapper : IntWrapper, 21 | list : RecursiveList 22 | } 23 | 24 | /// Testing a relatively simple variant 25 | type Var = A | B { b_field: Int } 26 | 27 | /// Unlike variants, enums compile to Avro enums. Interestingly, 28 | /// thanks to how Avro works, the binary representation of an enum 29 | /// ends up the same as a variant with exactly the same contents, but 30 | /// the generated Avro schema is different. 31 | enum Options = One | Two | Three 32 | 33 | /// A newtype—doesn't affect the Avro schema at all, but might cause 34 | /// issues with code generation. 35 | type IntWrapper = Int 36 | 37 | /// A linked list of ints. Probably not something you'd use in a 38 | /// "real" schema, but recursive types *are* useful and this is a 39 | /// handy way to test them. 40 | /// 41 | /// Note: this probably needs special casing in the random generation 42 | /// code! Without special handling, genTheta tends to produce *really 43 | /// large* inputs for recursive types like this... 44 | type RecursiveList = Cons { x : Int, rest : RecursiveList } 45 | | Nil 46 | -------------------------------------------------------------------------------- /test/cross-language/modules/primitives.theta: -------------------------------------------------------------------------------- 1 | language-version: 1.1.0 2 | avro-version: 1.1.0 3 | --- 4 | 5 | /// A field for each primitive type: 6 | type Primitives = { 7 | bool : Bool, 8 | bytes : Bytes, 9 | int : Int, 10 | long : Long, 11 | float : Float, 12 | double : Double, 13 | string : String, 14 | date : Date, 15 | datetime : Datetime, 16 | uuid : UUID, 17 | time : Time, 18 | local_datetime : LocalDatetime, 19 | fixed_0 : Fixed(0), 20 | fixed_10 : Fixed(10), 21 | fixed_10_again : Fixed(10) 22 | } 23 | 24 | /// Each kind of container: 25 | type Containers = { 26 | array : [Bool], 27 | map : {Int}, 28 | optional : Long?, 29 | nested : {[Date?]} 30 | } 31 | -------------------------------------------------------------------------------- /test/cross-language/python/cat_everything/cat.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from theta.avro import Decoder, Encoder 4 | 5 | from cat_everything.everything import Everything 6 | 7 | def main(): 8 | decoder = Decoder(sys.stdin.buffer) 9 | records = decoder.array(lambda: Everything.decode_avro(decoder)) 10 | 11 | encoder = Encoder(sys.stdout.buffer) 12 | encoder.array(records, lambda everything: everything.encode_avro(encoder)) 13 | sys.stdout.flush() 14 | 15 | -------------------------------------------------------------------------------- /test/cross-language/python/default.nix: -------------------------------------------------------------------------------- 1 | { pkgs, lib }: 2 | let 3 | inherit (pkgs) pythonPackages theta theta-python; 4 | 5 | theta-generated-python = lib.theta-python { 6 | name = "test_modules"; 7 | src = ./.; 8 | theta-paths = [ ../modules ]; 9 | modules = ["everything" "primitives"]; 10 | prefix = "cat_everything"; 11 | }; 12 | in pythonPackages.pkgs.buildPythonPackage { 13 | pname = "cat_everything_python"; 14 | version = "1.0.0"; 15 | src = ./.; 16 | propagatedBuildInputs = [ theta-python pythonPackages.pkgs.hypothesis ]; 17 | nativeBuildInputs = [ theta ]; 18 | preConfigure = '' 19 | cp -r ${theta-generated-python}/* cat_everything 20 | ''; 21 | } 22 | -------------------------------------------------------------------------------- /test/cross-language/python/setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = cat_everything_python 3 | version = 0.0.1 4 | description = Executable for cross-language tests involving Python. 5 | 6 | [options] 7 | packages = cat_everything 8 | install_requires = 9 | theta-python 10 | 11 | [options.entry_points] 12 | console_scripts = 13 | cat_everything_python = cat_everything.cat:main -------------------------------------------------------------------------------- /test/cross-language/python/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup() 4 | -------------------------------------------------------------------------------- /test/cross-language/rust/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cat_everything_rust" 3 | version = "0.1.0" 4 | edition = "2018" 5 | 6 | [dependencies] 7 | chrono = "0.4.0" 8 | nom = "5.0.0" 9 | uuid = "0.8.2" 10 | theta = { path = "./theta-rust" } 11 | -------------------------------------------------------------------------------- /test/cross-language/rust/default.nix: -------------------------------------------------------------------------------- 1 | { pkgs, lib, theta-rust-src ? ../../../rust }: 2 | let 3 | inherit (pkgs) theta; 4 | 5 | theta-rust = builtins.filterSource 6 | (path: type: 7 | type != "directory" || builtins.baseNameOf path != "target") 8 | theta-rust-src; 9 | 10 | rust-modules = lib.theta-rust { 11 | name = "test_modules"; 12 | src = ./.; 13 | theta-paths = [ ../modules ]; 14 | modules = ["everything" "primitives"]; 15 | }; 16 | 17 | generated-rust-src = pkgs.stdenv.mkDerivation { 18 | name = "generated-rust-src"; 19 | src = ./.; 20 | 21 | installPhase = '' 22 | mkdir -p $out/src 23 | 24 | # Theta Rust support library 25 | cp -r ${theta-rust} $out/theta-rust 26 | 27 | # Rust project files 28 | cp $src/Cargo.lock $out 29 | cp $src/Cargo.toml $out 30 | cp $src/src/*.rs $out/src 31 | 32 | # Generate Theta modules 33 | cp ${rust-modules}/src/*.rs $out/src 34 | ''; 35 | }; 36 | in pkgs.naersk.buildPackage { 37 | src = generated-rust-src; 38 | 39 | # Without this, the theta-rust directory created in 40 | # theta-generated-rust (cp -r ${theta-rust} $out/theta-rust) gets 41 | # filtered out by Naersk. 42 | copySources = ["theta-rust"]; 43 | } 44 | -------------------------------------------------------------------------------- /test/cross-language/rust/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::io::{Read, Write}; 3 | 4 | use theta::avro::{FromAvro, ToAvro}; 5 | 6 | mod test_modules; 7 | use test_modules::everything::*; 8 | 9 | fn main() -> io::Result<()> { 10 | let stdin = io::stdin(); 11 | let mut handle = stdin.lock(); 12 | let mut buffer = vec![]; 13 | 14 | handle.read_to_end(&mut buffer)?; 15 | let v = Vec::::from_avro(&buffer); 16 | 17 | match v { 18 | Ok((_, result)) => { 19 | let mut stdout = io::stdout(); 20 | stdout.write_all(&result.to_avro())?; 21 | stdout.flush()?; 22 | } 23 | Err(_) => panic!("Parsing input failed."), 24 | } 25 | Ok(()) 26 | } 27 | -------------------------------------------------------------------------------- /test/cross-language/test/Main.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE LambdaCase #-} 2 | {-# LANGUAGE OverloadedLists #-} 3 | {-# LANGUAGE OverloadedStrings #-} 4 | {-# LANGUAGE TypeApplications #-} 5 | module Main where 6 | 7 | import qualified Data.Text as Text 8 | 9 | import Control.Monad (when) 10 | import Control.Monad.Except (runExceptT) 11 | import Control.Monad.IO.Class (liftIO) 12 | 13 | import Data.Int (Int32) 14 | 15 | import Text.Printf (printf) 16 | 17 | import Test.QuickCheck (Arbitrary (arbitrary), Gen, 18 | forAll, getSize) 19 | import Test.QuickCheck.Monadic (monadicIO) 20 | import Test.Tasty 21 | import Test.Tasty.QuickCheck (testProperty) 22 | 23 | import Test.Everything 24 | import Test.Primitives 25 | 26 | import Theta.Pretty (pretty) 27 | import Theta.Target.Avro.Process (run) 28 | import Theta.Target.Haskell.Conversion (genTheta', toTheta) 29 | import Theta.Target.Haskell.HasTheta (HasTheta (theta)) 30 | import Theta.Test.Assertions (assertDiff, assertFirstDiff) 31 | import Theta.Value (Value, genValue) 32 | 33 | main :: IO () 34 | main = defaultMain tests 35 | 36 | tests :: TestTree 37 | tests = testGroup "Cross-Lanaugage Tests" 38 | [ testProperty "Haskell ⇔ Rust" $ 39 | forAll everything $ \ inputs -> handle $ do 40 | outputs <- run "cat_everything_rust" [] inputs 41 | assertFirstDiff outputs inputs 42 | 43 | , testProperty "Haskell ⇔ Python" $ 44 | forAll everything $ \ inputs -> handle $ do 45 | outputs <- run "cat_everything_python" [] inputs 46 | assertFirstDiff outputs inputs 47 | ] 48 | where handle action = monadicIO $ runExceptT action >>= \case 49 | Left err -> fail $ Text.unpack $ pretty err 50 | Right res -> pure res 51 | 52 | everything = genTheta' @[Everything] [("everything.List", toList <$> arbitrary)] 53 | toList = toTheta . foldr @[] Cons Nil 54 | -------------------------------------------------------------------------------- /test/cross-language/test/Test/Everything.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DeriveAnyClass #-} 2 | {-# LANGUAGE DeriveGeneric #-} 3 | {-# LANGUAGE DerivingStrategies #-} 4 | {-# LANGUAGE DuplicateRecordFields #-} 5 | {-# LANGUAGE OverloadedStrings #-} 6 | {-# LANGUAGE StandaloneDeriving #-} 7 | {-# LANGUAGE TemplateHaskell #-} 8 | {-# LANGUAGE TypeApplications #-} 9 | module Test.Everything where 10 | 11 | import Data.TreeDiff.Class (ToExpr) 12 | 13 | import Theta.Target.Haskell 14 | 15 | import Test.Primitives 16 | 17 | loadModule "modules" "everything" 18 | 19 | deriving anyclass instance ToExpr Var 20 | 21 | deriving anyclass instance ToExpr IntWrapper 22 | 23 | deriving anyclass instance ToExpr Everything 24 | 25 | deriving anyclass instance ToExpr Options 26 | 27 | deriving anyclass instance ToExpr RecursiveList 28 | -------------------------------------------------------------------------------- /test/cross-language/test/Test/Primitives.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DataKinds #-} 2 | {-# LANGUAGE DeriveAnyClass #-} 3 | {-# LANGUAGE DeriveGeneric #-} 4 | {-# LANGUAGE DerivingStrategies #-} 5 | {-# LANGUAGE DuplicateRecordFields #-} 6 | {-# LANGUAGE OverloadedStrings #-} 7 | {-# LANGUAGE StandaloneDeriving #-} 8 | {-# LANGUAGE TemplateHaskell #-} 9 | {-# LANGUAGE TypeApplications #-} 10 | module Test.Primitives where 11 | 12 | import Data.TreeDiff.Class (ToExpr) 13 | 14 | import Theta.Test.Assertions () 15 | 16 | import Theta.Target.Haskell 17 | 18 | loadModule "modules" "primitives" 19 | 20 | deriving anyclass instance ToExpr Primitives 21 | 22 | deriving anyclass instance ToExpr Containers 23 | -------------------------------------------------------------------------------- /test/cross-language/theta-tests.cabal: -------------------------------------------------------------------------------- 1 | cabal-version: 2.2 2 | name: theta-tests 3 | version: 1.0.0.0 4 | synopsis: Cross-language tests for Theta. See README.md for details. 5 | license: Apache-2.0 6 | build-type: Simple 7 | maintainer: Tikhon Jelvis 8 | data-files: 9 | modules/*.theta 10 | 11 | common shared 12 | default-language: Haskell2010 13 | hs-source-dirs: test/ 14 | build-depends: base >=4.13 && <4.15 15 | 16 | , mtl 17 | , text 18 | , tree-diff >=0.2 && <0.3 19 | 20 | , theta ==1.* 21 | , tasty 22 | , tasty-quickcheck 23 | , QuickCheck 24 | 25 | other-modules: Test.Everything 26 | , Test.Primitives 27 | 28 | executable tests 29 | import: shared 30 | main-is: Main.hs 31 | 32 | test-suite theta-tests 33 | import: shared 34 | type: exitcode-stdio-1.0 35 | main-is: Main.hs 36 | -------------------------------------------------------------------------------- /test/default.nix: -------------------------------------------------------------------------------- 1 | { pkgs, lib, source-overrides, overrides }: 2 | pkgs.linkFarmFromDrvs "theta-tests" [ 3 | (import ./avro { inherit pkgs; }) 4 | (import ./cli { inherit pkgs; }) 5 | (import ./kotlin { inherit pkgs; }) 6 | (import ./python { inherit pkgs lib; }) 7 | (import ./rust { inherit pkgs; }) 8 | (import ./cross-language { inherit pkgs lib source-overrides overrides; }) 9 | ] 10 | -------------------------------------------------------------------------------- /test/kotlin/default.nix: -------------------------------------------------------------------------------- 1 | # This expression is meant to be build in CI. It doesn't produce any 2 | # useful output, but if it builds properly, then the Kotlin code 3 | # generation is passing tests in kotlin-tests/tests.ks 4 | # 5 | # If it fails, the Kotlin error message should printed with the call 6 | # to nix build 7 | 8 | { pkgs }: 9 | let 10 | inherit (pkgs) theta; 11 | 12 | theta-generated-kotlin = pkgs.runCommand "theta-generated-kotlin" { 13 | THETA_LOAD_PATH = ./modules; 14 | THETA = "${theta}/bin/theta"; 15 | } '' 16 | mkdir -p $out/theta/com/example 17 | $THETA kotlin --prefix com.example --module kotlin_tests --out $out/theta/com/example 18 | ''; 19 | in 20 | pkgs.stdenv.mkDerivation { 21 | name = "theta-kotlin-tests"; 22 | src = ./.; 23 | nativeBuildInputs = [ pkgs.kotlin theta ]; 24 | installPhase = '' 25 | mkdir -p $out 26 | cp -r ${theta-generated-kotlin}/* $out 27 | 28 | kotlinc $out/theta -d $out/generated-kotlin.jar -cp $out/theta 29 | 30 | kotlinc $src/run-kotlin-tests.kt -include-runtime -d $out/run-kotlin-tests.jar -classpath $out/generated-kotlin.jar 31 | java -jar $out/run-kotlin-tests.jar 32 | ''; 33 | } 34 | -------------------------------------------------------------------------------- /test/kotlin/modules/enum.theta: -------------------------------------------------------------------------------- 1 | language-version: 1.1.0 2 | avro-version: 1.0.0 3 | --- 4 | 5 | enum Foo = Bar | Baz 6 | -------------------------------------------------------------------------------- /test/kotlin/modules/importing.theta: -------------------------------------------------------------------------------- 1 | language-version: 1.0.0 2 | avro-version: 1.0.0 3 | --- 4 | 5 | // A module importing and using a type defined in primitives.theta. 6 | 7 | import primitives 8 | 9 | type Foo = { 10 | bar: [primitives.Primitives] 11 | } 12 | -------------------------------------------------------------------------------- /test/kotlin/modules/kotlin_tests.theta: -------------------------------------------------------------------------------- 1 | language-version: 1.0.0 2 | avro-version: 1.0.0 3 | --- 4 | 5 | /* This module imports every Theta module we use in the Kotlin test 6 | * suite. This lets us generate every single Kotlin class we need with 7 | * a single command: 8 | * 9 | * theta --path modules kotlin -m kotlin_tests --target $out 10 | * 11 | */ 12 | 13 | import enum 14 | 15 | import importing 16 | 17 | import variant 18 | 19 | import newtype 20 | 21 | import shadowing 22 | -------------------------------------------------------------------------------- /test/kotlin/modules/newtype.theta: -------------------------------------------------------------------------------- 1 | language-version: 1.0.0 2 | avro-version: 1.0.0 3 | --- 4 | 5 | type NewtypeRecord = { foo : Newtype } 6 | 7 | type Newtype = Int 8 | 9 | // A newtype that forward-references a type: 10 | type Ref = Referred 11 | 12 | // Same check but with aliases 13 | alias RefAlias = Referred 14 | 15 | type Referred = Int 16 | -------------------------------------------------------------------------------- /test/kotlin/modules/primitives.theta: -------------------------------------------------------------------------------- 1 | language-version: 1.1.0 2 | avro-version: 1.0.0 3 | --- 4 | 5 | // A record that has a field for every primitive type supported in 6 | // Theta. 7 | type Primitives = { 8 | bool : Bool, 9 | bytes : Bytes, 10 | int : Int, 11 | long : Long, 12 | float : Float, 13 | double : Double, 14 | string : String, 15 | date : Date, 16 | datetime : Datetime, 17 | uuid : UUID, // language-version ≥ 1.1.0 18 | time : Time, // language-version ≥ 1.1.0 19 | local_datetime : LocalDatetime // language-version ≥ 1.1.0 20 | } 21 | 22 | // A record with the various kinds of containers Theta supports. 23 | type Containers = { 24 | array : [Bool], 25 | map : {Bool}, 26 | optional : Bool?, 27 | nested : {[Bool?]} 28 | } 29 | -------------------------------------------------------------------------------- /test/kotlin/modules/shadowing.theta: -------------------------------------------------------------------------------- 1 | language-version: 1.0.0 2 | avro-version: 1.0.0 3 | --- 4 | 5 | import primitives 6 | 7 | type Primitives = { 8 | underlying: primitives.Primitives 9 | } 10 | -------------------------------------------------------------------------------- /test/kotlin/modules/variant.theta: -------------------------------------------------------------------------------- 1 | language-version: 1.0.0 2 | avro-version: 1.0.0 3 | --- 4 | 5 | type Either = I { a : Int } 6 | | J { a : Int } 7 | | S { a : String, b : Int } 8 | -------------------------------------------------------------------------------- /test/kotlin/run-kotlin-tests.kt: -------------------------------------------------------------------------------- 1 | package theta 2 | 3 | import com.example.enum.* 4 | import com.example.importing.* 5 | import com.example.newtype.* 6 | import com.example.primitives.* 7 | import com.example.shadowing.* 8 | import com.example.variant.* 9 | 10 | fun main(args: Array) { 11 | println("Generated Kotlin types compiled successfully.") 12 | } 13 | -------------------------------------------------------------------------------- /test/python/README.md: -------------------------------------------------------------------------------- 1 | # Python Tests 2 | 3 | This directory defines a Nix derivation that tests the Python code that Theta generates. 4 | 5 | ## Adding Test Modules 6 | 7 | If you want to add a new module `example` to the Python tests, you need to take three steps: 8 | 9 | 1. Add `example.theta` to `modules`. 10 | 2. Add `import example` to `modules/python_tests.theta`. 11 | 3. Import and test the corresponding Python module in `theta_python_tests/test_python.py`. 12 | 13 | Most of the tests use Hypothesis to randomly generate values of the class and then ensure that they can be encoded to Avro and decoded back to the same value. In the future, Theta itself might generate Hypothesis strategies for its types, but for now you have to do it manually. If `example.theta` had the following record: 14 | 15 | ``` 16 | type Foo = { foo : Int } 17 | ``` 18 | 19 | Here's how you might test it in `test_python.py`: 20 | 21 | ``` 22 | import theta_python_tests.example as example 23 | 24 | example_foo = builds(example.Foo, ints) 25 | 26 | class TestExample(unittest.TestCase): 27 | @given(example_foo) 28 | def test_foo(self, record): 29 | round_trip(record, example.Foo) 30 | ``` 31 | 32 | Of course, feel free to add more specific generators and test cases! 33 | 34 | ## Errors 35 | 36 | If your test doesn't work, double-check the following: 37 | 38 | 1. Did you import your new module from `python_tests.theta`? 39 | 2. Did you import the corresponding `theta_python_tests.*.py` in `test_python.py`? 40 | -------------------------------------------------------------------------------- /test/python/avro/container-deflate.avro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/target/theta-idl/2bda6a8836817f8d782c2165d7f250fe296b97c2/test/python/avro/container-deflate.avro -------------------------------------------------------------------------------- /test/python/avro/container.avro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/target/theta-idl/2bda6a8836817f8d782c2165d7f250fe296b97c2/test/python/avro/container.avro -------------------------------------------------------------------------------- /test/python/default.nix: -------------------------------------------------------------------------------- 1 | # This expression is meant to be built in CI. 2 | # 3 | # It doesn't produce any useful output, but if it builds properly, 4 | # then the python code generation is passing tests in 5 | # python-tests/test.py 6 | # 7 | # If it fails, the python error message should 8 | # printed with the call to nix build 9 | 10 | { pkgs, lib }: 11 | let 12 | inherit (pkgs) pythonPackages theta theta-python; 13 | 14 | theta-generated-python = lib.theta-python { 15 | name = "theta-generated-python"; 16 | src = ./.; 17 | theta-paths = [./modules]; 18 | modules = ["python_tests"]; 19 | prefix = "theta_python_tests"; 20 | }; 21 | in pythonPackages.pkgs.buildPythonPackage { 22 | pname = "theta-python-tests"; 23 | version = "1.0.0"; 24 | src = ./.; 25 | nativeBuildInputs = [ theta ]; 26 | checkInputs = [ theta-python pythonPackages.pkgs.hypothesis ]; 27 | preConfigure = '' 28 | cp -r ${theta-generated-python}/* theta_python_tests 29 | ''; 30 | } 31 | -------------------------------------------------------------------------------- /test/python/modules/com/example/importing_nested.theta: -------------------------------------------------------------------------------- 1 | language-version: 1.0.0 2 | avro-version: 1.0.0 3 | --- 4 | 5 | // This import would break older versions of the Python code generator 6 | // when used with an explicit prefix 7 | // 8 | // See: https://github.com/target/theta-idl/issues/43 9 | import com.example.nested 10 | 11 | type ImportingNested = { 12 | test_field : com.example.nested.TestRecord 13 | } 14 | -------------------------------------------------------------------------------- /test/python/modules/com/example/nested.theta: -------------------------------------------------------------------------------- 1 | language-version: 1.0.0 2 | avro-version: 1.0.0 3 | --- 4 | 5 | /// Just a record so that we can generate an Avro schema for this 6 | /// module 7 | type TestRecord = { 8 | test_field : Int 9 | } 10 | -------------------------------------------------------------------------------- /test/python/modules/container.theta: -------------------------------------------------------------------------------- 1 | language-version: 1.0.0 2 | avro-version: 1.0.0 3 | --- 4 | 5 | // A record that has a field for every primitive type supported in 6 | // Theta. 7 | type ContainerRecord = { 8 | bool : Bool, 9 | bytes : Bytes, 10 | int : Int, 11 | long : Long, 12 | float : Float, 13 | double : Double, 14 | string : String, 15 | date : Date, 16 | datetime : Datetime 17 | } 18 | -------------------------------------------------------------------------------- /test/python/modules/enum.theta: -------------------------------------------------------------------------------- 1 | language-version: 1.1.0 2 | avro-version: 1.0.0 3 | --- 4 | 5 | enum SimpleEnum = SymbolA | SymbolB 6 | 7 | /// A record to wrap SimpleEnum—enums can't be top-level types in an 8 | /// Avro schema by themselves. 9 | type EnumWrapper = { 10 | simple: SimpleEnum 11 | } 12 | -------------------------------------------------------------------------------- /test/python/modules/importing.theta: -------------------------------------------------------------------------------- 1 | language-version: 1.0.0 2 | avro-version: 1.0.0 3 | --- 4 | 5 | // A module importing and using a type defined in primitives.theta. 6 | 7 | import primitives 8 | 9 | type Foo = { 10 | bar: [primitives.Primitives] 11 | } 12 | -------------------------------------------------------------------------------- /test/python/modules/newtype.theta: -------------------------------------------------------------------------------- 1 | language-version: 1.0.0 2 | avro-version: 1.0.0 3 | --- 4 | 5 | type NewtypeRecord = { foo : Newtype } 6 | 7 | type Newtype = Int 8 | 9 | // A newtype that forward-references a type: 10 | type Ref = Referred 11 | 12 | // Same check but with aliases 13 | alias RefAlias = Referred 14 | 15 | type Referred = Int 16 | -------------------------------------------------------------------------------- /test/python/modules/primitives.theta: -------------------------------------------------------------------------------- 1 | language-version: 1.1.0 2 | avro-version: 1.0.0 3 | --- 4 | 5 | // A record that has a field for every primitive type supported in 6 | // Theta. 7 | type Primitives = { 8 | bool : Bool, 9 | bytes : Bytes, 10 | int : Int, 11 | long : Long, 12 | float : Float, 13 | double : Double, 14 | string : String, 15 | date : Date, 16 | datetime : Datetime, 17 | uuid : UUID, // language-version ≥ 1.1.0 18 | time : Time, // language-version ≥ 1.1.0 19 | local_datetime : LocalDatetime, // language-version ≥ 1.1.0 20 | fixed_1 : Fixed(1), // language-version ≥ 1.1.0 21 | fixed_3 : Fixed(3) // language-version ≥ 1.1.0 22 | } 23 | 24 | // A record with the various kinds of containers Theta supports. 25 | type Containers = { 26 | array : [Bool], 27 | map : {Bool}, 28 | optional : Bool?, 29 | nested : {[Bool?]} 30 | } 31 | -------------------------------------------------------------------------------- /test/python/modules/python_tests.theta: -------------------------------------------------------------------------------- 1 | language-version: 1.0.0 2 | avro-version: 1.0.0 3 | --- 4 | 5 | /* This module imports every Theta module we use in the Python test 6 | * suite. This lets us generate every single Python class we need with 7 | * a single command: 8 | * 9 | * theta-python --path ./python-tests/modules --module python_tests --target $out 10 | * 11 | */ 12 | 13 | import container 14 | 15 | import enum 16 | 17 | import importing 18 | 19 | import variant 20 | 21 | import newtype 22 | 23 | import shadowing 24 | 25 | import com.example.importing_nested 26 | -------------------------------------------------------------------------------- /test/python/modules/shadowing.theta: -------------------------------------------------------------------------------- 1 | language-version: 1.0.0 2 | avro-version: 1.0.0 3 | --- 4 | 5 | import primitives 6 | 7 | type Primitives = { 8 | underlying: primitives.Primitives 9 | } 10 | -------------------------------------------------------------------------------- /test/python/modules/variant.theta: -------------------------------------------------------------------------------- 1 | language-version: 1.0.0 2 | avro-version: 1.0.0 3 | --- 4 | 5 | type Either = I { a : Int } 6 | | J { a : Int } 7 | | S { a : String, b : Int } 8 | -------------------------------------------------------------------------------- /test/python/setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = theta-python-tests 3 | version = 0.0.1 4 | description = Tests for Python classes generated by Theta. 5 | 6 | [options] 7 | packages = find: 8 | install_requires = 9 | hypothesis 10 | theta-python -------------------------------------------------------------------------------- /test/python/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup() 4 | -------------------------------------------------------------------------------- /test/rust/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "theta_rust_tests" 3 | version = "0.1.0" 4 | edition = "2018" 5 | 6 | [lib] 7 | 8 | [dependencies] 9 | chrono = "0.4.0" 10 | nom = "5.0.0" 11 | uuid = "0.8.2" 12 | theta = { path = "./theta-rust" } -------------------------------------------------------------------------------- /test/rust/README.md: -------------------------------------------------------------------------------- 1 | # Rust Tests 2 | 3 | This directory uses Nix to test the Rust code that Theta generates. The Nix setup is a bit of a hack; see comments in `default.nix` for details. 4 | 5 | The tests pick up every module in `modules`, generate a corresponding Rust file then compile and run tests for the whole package (ie `lib.rs` + generated modules). 6 | 7 | ## Adding Test Modules 8 | 9 | If you want to add a new module named `foo` to the tests here, you need to do two things: 10 | 11 | 1. Add `foo.theta` to the `modules` directory. 12 | 2. Add `pub mod foo;` to `lib.rs`. 13 | 14 | This will ensure that the module is built and typechecked. 15 | 16 | If you want to do other tests with the types from the new module—like round-tripping encoding/decoding—you will need to add test code for it to the `tests` module in `lib.rs`. 17 | 18 | Note: do not add a module called `lib.theta`! This will overwrite `lib.rs` and break things. 19 | 20 | ## Adding Test Dependencies 21 | 22 | The `Cargo.toml` file *for the tests package* is generated from `default.nix`. If you want access to a new library for testing, you'll have to add it there as well as updating the `Cargo.lock` file in this directory. The whole Nix setup is something of a hack—I can't even guarantee that this will work! 23 | -------------------------------------------------------------------------------- /test/rust/default.nix: -------------------------------------------------------------------------------- 1 | { pkgs 2 | , theta-rust-src ? ../../rust 3 | }: 4 | 5 | let 6 | inherit (pkgs) theta; 7 | 8 | theta-rust = builtins.filterSource 9 | (path: type: 10 | type != "directory" || builtins.baseNameOf path != "target") 11 | theta-rust-src; 12 | 13 | # Set up the *source* for the Rust test code: 14 | # 15 | # 1. Set up Cargo.lock and Cargo.toml (see above) 16 | # 2. Copy over Theta modules used for testing 17 | # 3. Copy over non-generated Rust files (src/*rs) 18 | # 4. Run theta rust for each module in modules 19 | # 20 | # Once these four steps are done, we have a directory that we can 21 | # build and test with Naersk, including the *.rs files *generated by 22 | # Theta*. 23 | # 24 | # This definitely feels like a bit of a hack, but I couldn't figure 25 | # out a better way to deal with building *generated* Rust code in 26 | # Nix, or for depending on a Rust library also packaged with Nix (ie 27 | # theta-rust). 28 | theta-generated-rust = pkgs.stdenv.mkDerivation { 29 | name = "theta-generated-rust"; 30 | src = ./.; 31 | 32 | installPhase = '' 33 | mkdir -p $out 34 | 35 | # Theta's Rust support library 36 | cp -r ${theta-rust} $out/theta-rust 37 | 38 | cp $src/Cargo.toml $out/Cargo.toml 39 | cp $src/Cargo.lock $out 40 | 41 | # Copy over Rust source for tests (src/*.rs from this directory) 42 | mkdir -p $out/src 43 | cp $src/src/*.rs $out/src 44 | 45 | # Copy over Theta modules 46 | mkdir -p $out/modules 47 | cp $src/modules/*.theta $out/modules 48 | export THETA_LOAD_PATH=$out/modules 49 | 50 | # Generate Rust code for each Theta module 51 | for module_name in $out/modules/*.theta 52 | do 53 | module=$(basename $module_name .theta) 54 | echo "$module.theta ⇒ $module.rs" 55 | ${theta}/bin/theta rust -m $module > $out/src/$module.rs 56 | done 57 | ''; 58 | }; 59 | in pkgs.naersk.buildPackage { 60 | src = theta-generated-rust; 61 | 62 | # Without this, the theta-rust directory created in 63 | # theta-generated-rust (cp -r ${theta-rust} $out/theta-rust) gets 64 | # filtered out by Naersk. 65 | copySources = ["theta-rust"]; 66 | 67 | remapPathPrefix = true; 68 | doCheck = true; 69 | } 70 | -------------------------------------------------------------------------------- /test/rust/modules/enums.theta: -------------------------------------------------------------------------------- 1 | language-version: 1.1.0 2 | avro-version: 1.0.0 3 | --- 4 | 5 | enum SimpleEnum = SymbolA | SymbolB 6 | 7 | /// Tricky enum designed to test Rust's name disambiguation logic 8 | enum TrickyEnum = Sym | sym | _Sym 9 | 10 | /// A record with one enum field and one int field. 11 | type EnumRecord = { 12 | enum_field : TrickyEnum, 13 | int_field : Int 14 | } 15 | -------------------------------------------------------------------------------- /test/rust/modules/primitives.theta: -------------------------------------------------------------------------------- 1 | language-version: 1.1.0 2 | avro-version: 1.0.0 3 | --- 4 | 5 | // A record that has a field for every primitive type supported in 6 | // Theta. 7 | type Primitives = { 8 | bool : Bool, 9 | bytes : Bytes, 10 | int : Int, 11 | long : Long, 12 | float : Float, 13 | double : Double, 14 | string : String, 15 | date : Date, 16 | datetime : Datetime, 17 | uuid : UUID, // language-version ≥ 1.1.0 18 | time : Time, // language-version ≥ 1.1.0 19 | local_datetime : LocalDatetime, // language-version ≥ 1.1.0 20 | fixed_1 : Fixed(1), // language-version ≥ 1.1.0 21 | fixed_3 : Fixed(3) // language-version ≥ 1.1.0 22 | } 23 | 24 | // A record with the various kinds of containers Theta supports. 25 | type Containers = { 26 | array : [Bool], 27 | map : {Bool}, 28 | optional : Bool?, 29 | nested : {[Bool?]} 30 | } 31 | -------------------------------------------------------------------------------- /test/rust/modules/rust.theta: -------------------------------------------------------------------------------- 1 | language-version: 1.0.0 2 | avro-version: 1.0.0 3 | --- 4 | 5 | // A module with a few Rust-specific edge-cases 6 | 7 | type Foo = { 8 | input: Int, 9 | nested: [A] 10 | } 11 | 12 | alias A = [Long] 13 | 14 | 15 | // A recursive type, which means the recursive field needs to be boxed 16 | // in Rust: 17 | type Recursive = { recurse: Recursive? } 18 | 19 | // Same thing but with a variant rather than a struct: 20 | type RecursiveVariant = A { recurse: RecursiveVariant? } 21 | | B { recurse: RecursiveVariant? } 22 | -------------------------------------------------------------------------------- /test/rust/modules/shadowing.theta: -------------------------------------------------------------------------------- 1 | language-version: 1.0.0 2 | avro-version: 1.0.0 3 | --- 4 | 5 | import primitives 6 | 7 | type Primitives = { 8 | underlying: primitives.Primitives 9 | } 10 | -------------------------------------------------------------------------------- /test/rust/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![feature(test)] 2 | 3 | pub mod primitives; 4 | 5 | pub mod rust; 6 | 7 | pub mod shadowing; 8 | 9 | pub mod enums; 10 | 11 | #[cfg(test)] 12 | mod tests { 13 | use std::str::FromStr; 14 | 15 | use chrono::{Date, DateTime, NaiveDate, NaiveTime, Utc}; 16 | use uuid::{Uuid}; 17 | 18 | use std::collections::HashMap; 19 | 20 | use theta::avro::{FromAvro, ToAvro, Fixed}; 21 | 22 | use super::enums::*; 23 | use super::rust::*; 24 | use super::primitives::*; 25 | 26 | use super::shadowing; 27 | 28 | // Primitives and Containers from primitives.theta 29 | 30 | #[test] 31 | fn test_round_trip_primitives() { 32 | let example = primitives::Primitives { 33 | bool: true, 34 | bytes: vec![0xC0, 0xFF, 0xEE], 35 | int: 37i32, 36 | long: 42i64, 37 | float: 0.05, 38 | double: 0.12, 39 | string: "blarg".to_string(), 40 | date: Date::from_utc(NaiveDate::from_ymd(20, 12, 23), Utc), 41 | datetime: DateTime::from_utc(NaiveDate::from_ymd(10, 11, 12).and_hms(5, 0, 0), Utc), 42 | uuid: Uuid::from_str("f81d4fae-7dec-11d0-a765-00a0c91e6bf6").unwrap(), 43 | time: NaiveTime::from_hms(12, 23, 10), 44 | local_datetime: NaiveDate::from_ymd(10, 11, 12).and_hms(5, 0, 0), 45 | fixed_1: Fixed(vec![0xC0]), 46 | fixed_3: Fixed(vec![0xC0, 0xFF, 0xEE]), 47 | }; 48 | assert!(check_encoding(example)); 49 | } 50 | 51 | #[test] 52 | fn test_round_trip_containers() { 53 | let mut hashmap = HashMap::new(); 54 | hashmap.insert("key".to_string(), true); 55 | 56 | let mut nested_hashmap = HashMap::new(); 57 | nested_hashmap.insert("key".to_string(), vec![Some(true), None]); 58 | 59 | let example = primitives::Containers { 60 | array: vec![true, false], 61 | map: hashmap, 62 | optional: None, 63 | nested: nested_hashmap, 64 | }; 65 | assert!(check_encoding(example)); 66 | } 67 | 68 | // Foo, Recursive and RecursiveVariant from rust.theta 69 | 70 | #[test] 71 | fn test_round_trip_foo() { 72 | let example = rust::Foo { 73 | input: 10, 74 | nested: vec![vec![1, 2], vec![3, 4i64]], 75 | }; 76 | assert!(check_encoding(example)); 77 | } 78 | 79 | #[test] 80 | fn test_round_trip_recursive() { 81 | let example_inner = rust::Recursive { 82 | recurse: Box::new(None), 83 | }; 84 | let example = rust::Recursive { 85 | recurse: Box::new(Some(example_inner)), 86 | }; 87 | assert!(check_encoding(example)); 88 | } 89 | 90 | #[test] 91 | fn test_round_trip_recursive_variant() { 92 | let example_inner = rust::RecursiveVariant::A { 93 | recurse: Box::new(None), 94 | }; 95 | let example_a = rust::RecursiveVariant::A { 96 | recurse: Box::new(Some(example_inner)), 97 | }; 98 | let example_b = rust::RecursiveVariant::B { 99 | recurse: Box::new(Some(example_a)), 100 | }; 101 | assert!(check_encoding(example_b)); 102 | } 103 | 104 | #[test] 105 | fn test_round_trip_recursive_container() { 106 | let example_inner = rust::Recursive { 107 | recurse: Box::new(None), 108 | }; 109 | let example = rust::Recursive { 110 | recurse: Box::new(Some(example_inner)), 111 | }; 112 | 113 | let mut example_map = HashMap::new(); 114 | example_map.insert("foo".to_string(), example.clone()); 115 | 116 | assert!(check_encoding(vec![example.clone()])); 117 | assert!(check_encoding(example_map)); 118 | assert!(check_encoding(Some(example.clone()))); 119 | } 120 | 121 | #[test] 122 | fn test_round_trip_shadowing() { 123 | let shadowed_record = shadowing::primitives::Primitives { 124 | bool: true, 125 | bytes: vec![0xC0, 0xFF, 0xEE], 126 | int: 37i32, 127 | long: 42i64, 128 | float: 0.05, 129 | double: 0.12, 130 | string: "blarg".to_string(), 131 | date: Date::from_utc(NaiveDate::from_ymd(20, 12, 23), Utc), 132 | datetime: DateTime::from_utc(NaiveDate::from_ymd(10, 11, 12).and_hms(5, 0, 0), Utc), 133 | uuid: Uuid::from_str("f81d4fae-7dec-11d0-a765-00a0c91e6bf6").unwrap(), 134 | time: NaiveTime::from_hms(12, 23, 10), 135 | local_datetime: NaiveDate::from_ymd(10, 11, 12).and_hms(5, 0, 0), 136 | fixed_1: Fixed(vec![0xC0]), 137 | fixed_3: Fixed(vec![0xC0, 0xFF, 0xEE]), 138 | }; 139 | let shadowing_record = shadowing::shadowing::Primitives { 140 | underlying: shadowed_record.clone(), 141 | }; 142 | 143 | assert!(check_encoding(shadowed_record)); 144 | assert!(check_encoding(shadowing_record)); 145 | } 146 | 147 | // Enums 148 | #[test] 149 | fn test_round_trip_simple_enum() { 150 | assert!(check_encoding(enums::SimpleEnum::SymbolA)); 151 | assert!(check_encoding(enums::SimpleEnum::SymbolB)); 152 | } 153 | 154 | #[test] 155 | fn test_round_trip_tricky_enum() { 156 | assert!(check_encoding(enums::TrickyEnum::Sym)); 157 | assert!(check_encoding(enums::TrickyEnum::Sym_)); 158 | assert!(check_encoding(enums::TrickyEnum::Sym__)); 159 | } 160 | 161 | #[test] 162 | fn test_round_trip_enum_record() { 163 | assert!(check_encoding(enums::EnumRecord { 164 | enum_field: enums::TrickyEnum::Sym, 165 | int_field: 1, 166 | })); 167 | } 168 | 169 | fn check_encoding(value: A) -> bool { 170 | match FromAvro::from_avro(value.to_avro().as_slice()) { 171 | Ok(([], result)) => value == result, 172 | Ok((_, _)) => false, // did not consume all input 173 | Err(_) => false, 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /test/rust/theta-rust: -------------------------------------------------------------------------------- 1 | /home/tikhon/Programming/theta-idl/rust/ -------------------------------------------------------------------------------- /test/shell.nix: -------------------------------------------------------------------------------- 1 | { pkgs, lib }: 2 | let 3 | cross-language = import ./cross-language { 4 | inherit pkgs lib; 5 | extra-build-tools = pkgs.rust-dev-tools; 6 | }; 7 | in 8 | cross-language.env 9 | -------------------------------------------------------------------------------- /theta/.hlint.yaml: -------------------------------------------------------------------------------- 1 | # HLint configuration file 2 | # https://github.com/ndmitchell/hlint 3 | ########################## 4 | 5 | # This file contains a template configuration file, which is typically 6 | # placed as .hlint.yaml in the root of your project 7 | 8 | 9 | # Additional command line arguments 10 | - arguments: [-XQuasiQuotes] 11 | 12 | 13 | # - extensions: 14 | # - name: [QuasiQuoted] 15 | 16 | # Control which extensions/flags/modules/functions can be used 17 | # 18 | # - extensions: 19 | # - default: false # all extension are banned by default 20 | # - name: [PatternGuards, ViewPatterns] # only these listed extensions can be used 21 | # - {name: CPP, within: CrossPlatform} # CPP can only be used in a given module 22 | # 23 | # - flags: 24 | # - {name: -w, within: []} # -w is allowed nowhere 25 | # 26 | # - modules: 27 | # - {name: [Data.Set, Data.HashSet], as: Set} # if you import Data.Set qualified, it must be as 'Set' 28 | # - {name: Control.Arrow, within: []} # Certain modules are banned entirely 29 | # 30 | # - functions: 31 | # - {name: unsafePerformIO, within: []} # unsafePerformIO can only appear in no modules 32 | 33 | 34 | # Add custom hints for this project 35 | # 36 | # Will suggest replacing "wibbleMany [myvar]" with "wibbleOne myvar" 37 | # - error: {lhs: "wibbleMany [x]", rhs: wibbleOne x} 38 | 39 | # The hints are named by the string they display in warning messages. 40 | # For example, if you see a warning starting like 41 | # 42 | # Main.hs:116:51: Warning: Redundant == 43 | # 44 | # You can refer to that hint with `{name: Redundant ==}` (see below). 45 | 46 | # Turn on hints that are off by default 47 | # 48 | # Ban "module X(module X) where", to require a real export list 49 | # - warn: {name: Use explicit module export list} 50 | # 51 | # Replace a $ b $ c with a . b $ c 52 | # - group: {name: dollar, enabled: true} 53 | # 54 | # Generalise map to fmap, ++ to <> 55 | # - group: {name: generalise, enabled: true} 56 | 57 | 58 | # Ignore some builtin hints 59 | - ignore: {name: Parse error} 60 | - ignore: {name: Use infix } 61 | - ignore: {name: Use newtype instead of data} 62 | - ignore: {name: Use first} 63 | - ignore: {name: Use uncurry} 64 | # - ignore: {name: Use let} 65 | # - ignore: {name: Use const, within: SpecialModule} # Only within certain modules 66 | 67 | 68 | # Define some custom infix operators 69 | # - fixity: infixr 3 ~^#^~ 70 | 71 | 72 | # To generate a suitable file for HLint do: 73 | # $ hlint --default > .hlint.yaml 74 | -------------------------------------------------------------------------------- /theta/apps/Apps/Avro.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE FlexibleContexts #-} 2 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} 3 | {-# LANGUAGE LambdaCase #-} 4 | {-# LANGUAGE NamedFieldPuns #-} 5 | {-# LANGUAGE OverloadedStrings #-} 6 | {-# LANGUAGE QuasiQuotes #-} 7 | 8 | module Apps.Avro where 9 | 10 | import Control.Monad (forM_) 11 | import Control.Monad.IO.Class (liftIO) 12 | 13 | import qualified Data.Aeson as Aeson 14 | import qualified Data.ByteString.Lazy as LBS 15 | import qualified Data.Text as Text 16 | 17 | import Options.Applicative 18 | 19 | import System.Directory (createDirectoryIfMissing) 20 | import System.FilePath (joinPath, (<.>), ()) 21 | 22 | import qualified Theta.Import as Theta 23 | import Theta.Name (ModuleName, Name (..)) 24 | import qualified Theta.Name as Name 25 | import Theta.Pretty (pr) 26 | import Theta.Target.Avro.Types (toSchema) 27 | import qualified Theta.Types as Theta 28 | 29 | import Apps.Subcommand 30 | 31 | -- * avro 32 | 33 | avroCommand :: Mod CommandFields Subcommand 34 | avroCommand = command "avro" opts 35 | where opts = info 36 | (subcommands <**> helper) 37 | (fullDesc <> progDesc avroDescription) 38 | 39 | subcommands = subparser $ mconcat 40 | [ typeCommand, allCommand ] 41 | 42 | avroDescription :: String 43 | avroDescription = [pr| 44 | Commands for working with Avro schemas generate from Theta types. 45 | |] 46 | 47 | -- * avro type 48 | 49 | typeCommand :: Mod CommandFields Subcommand 50 | typeCommand = command "type" $ runType <$> opts 51 | where opts = info 52 | (typeOpts <**> helper) 53 | (fullDesc <> progDesc "Compile a specific Theta type to an Avro schema.") 54 | 55 | data TypeOptions = TypeOptions 56 | { targetStream :: TargetStream 57 | , toExport :: Name 58 | } 59 | 60 | data TargetStream = TargetFile FilePath | TargetStdout 61 | 62 | typeOpts :: Parser TypeOptions 63 | typeOpts = TypeOptions <$> stream <*> typeName 64 | where stream = parseStream <$> strOption 65 | ( short 'o' 66 | <> long "out" 67 | <> value "-" 68 | <> metavar "FILE" 69 | <> help "File to dump Avro schema to. \ 70 | \If not specified, print to stdout. The filename '-'\ 71 | \ may also be used to write to stdout. Use \ 72 | \ ./- if you want '-' as a filename." 73 | ) 74 | 75 | -- | Interpret the file target '-' filepath as stdout, 76 | -- while passing all other values through as a FilePath 77 | parseStream :: String -> TargetStream 78 | parseStream "-" = TargetStdout 79 | parseStream x = TargetFile x 80 | 81 | runType :: TypeOptions -> Subcommand 82 | runType TypeOptions { targetStream, toExport } loadPath = do 83 | type_ <- Theta.getDefinition loadPath toExport 84 | exported <- toSchema type_ 85 | 86 | let write = case targetStream of 87 | TargetStdout -> LBS.putStr 88 | TargetFile filePath -> LBS.writeFile filePath 89 | liftIO $ write $ Aeson.encode exported 90 | 91 | -- * avro all 92 | 93 | allCommand :: Mod CommandFields Subcommand 94 | allCommand = command "all" $ runAll <$> opts 95 | where opts = info 96 | (allOpts <**> helper) 97 | (fullDesc <> progDesc allDescription) 98 | 99 | allDescription :: String 100 | allDescription = [pr| 101 | Compile every exportable type from the given module and every module 102 | it (transitively) imports to an Avro schema, placing all the schemas 103 | in $TARGET_DIRECTORY/avro. 104 | |] 105 | 106 | data AllOptions = AllOptions 107 | { target :: FilePath 108 | , moduleNames :: [ModuleName] 109 | } 110 | 111 | allOpts :: Parser AllOptions 112 | allOpts = AllOptions <$> targetDirectory "the avro directory" <*> modules 113 | 114 | runAll :: AllOptions -> Subcommand 115 | runAll AllOptions { target, moduleNames } loadPath = do 116 | modules <- traverse (Theta.getModule loadPath) moduleNames 117 | 118 | liftIO $ createDirectoryIfMissing True (target "avro") 119 | 120 | forM_ (Theta.transitiveImports modules) $ \ module_ -> do 121 | forM_ (Theta.types module_) $ \ definition -> 122 | export (Theta.moduleName module_) definition 123 | 124 | where 125 | export moduleName definition = do 126 | schema <- toSchema definition 127 | liftIO $ do 128 | let path = Text.unpack <$> Name.moduleParts moduleName 129 | out = target "avro" joinPath path 130 | createDirectoryIfMissing True out 131 | 132 | let filename = 133 | Text.unpack $ Name.name $ Theta.definitionName definition 134 | LBS.writeFile (out filename <.> "avsc") $ Aeson.encode schema 135 | 136 | -------------------------------------------------------------------------------- /theta/apps/Apps/Hash.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE FlexibleContexts #-} 2 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} 3 | {-# LANGUAGE LambdaCase #-} 4 | {-# LANGUAGE NamedFieldPuns #-} 5 | {-# LANGUAGE OverloadedStrings #-} 6 | {-# LANGUAGE QuasiQuotes #-} 7 | 8 | module Apps.Hash where 9 | 10 | import Control.Monad (forM_, unless) 11 | import Control.Monad.IO.Class (liftIO) 12 | 13 | import Options.Applicative 14 | 15 | import qualified Theta.Import as Theta 16 | import Theta.Name (ModuleName, Name (..)) 17 | import qualified Theta.Name as Name 18 | import Theta.Pretty (pr) 19 | import qualified Theta.Types as Theta 20 | 21 | import Apps.Subcommand 22 | 23 | hashCommand :: Mod CommandFields Subcommand 24 | hashCommand = command "hash" $ runHash <$> opts 25 | where opts = info 26 | (hashOpts <**> helper) 27 | (fullDesc <> progDesc "Calculate hashes for Theta types and modules.") 28 | 29 | data HashOptions = HashOptions 30 | { typeNames :: [Name] 31 | , moduleNames :: [ModuleName] 32 | } 33 | 34 | hashOpts :: Parser HashOptions 35 | hashOpts = HashOptions <$> many typeName 36 | <*> (modules <|> pure []) 37 | 38 | runHash :: HashOptions -> Subcommand 39 | runHash HashOptions { typeNames, moduleNames } loadPath = do 40 | unless (null typeNames) $ hashTypes typeNames loadPath 41 | unless (null moduleNames) $ hashModules moduleNames loadPath 42 | 43 | 44 | hashTypes :: [Name] -> Subcommand 45 | hashTypes types loadPath = forM_ types $ \ typeName -> do 46 | definition <- Theta.getDefinition loadPath typeName 47 | 48 | -- Note the tab character 49 | let hash = Theta.hash . Theta.definitionType 50 | liftIO $ putStrLn [pr|{Name.render typeName} {show $ hash definition}|] 51 | 52 | hashModules :: [ModuleName] -> Subcommand 53 | hashModules moduleNames loadPath = do 54 | modules <- traverse (Theta.getModule loadPath) moduleNames 55 | 56 | forM_ (Theta.transitiveImports modules) $ \ module_ -> 57 | forM_ (Theta.types module_) $ \ Theta.Definition 58 | { Theta.definitionType = type_, Theta.definitionName = name } -> 59 | -- Note the tab character 60 | liftIO $ putStrLn [pr|{Name.render name} {show $ Theta.hash type_}|] 61 | 62 | -------------------------------------------------------------------------------- /theta/apps/Apps/Kotlin.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE LambdaCase #-} 2 | {-# LANGUAGE NamedFieldPuns #-} 3 | {-# LANGUAGE OverloadedStrings #-} 4 | {-# LANGUAGE QuasiQuotes #-} 5 | module Apps.Kotlin where 6 | 7 | import Control.Monad (forM_) 8 | import Control.Monad.Trans (liftIO) 9 | 10 | import qualified Data.List as List 11 | import Data.Maybe (fromMaybe) 12 | import qualified Data.Text as Text 13 | import qualified Data.Text.IO as Text 14 | 15 | import Options.Applicative 16 | 17 | import System.Directory (createDirectoryIfMissing) 18 | import System.FilePath (joinPath, (<.>), ()) 19 | 20 | import qualified Theta.Import as Theta 21 | import Theta.Name (ModuleName) 22 | import qualified Theta.Name as Name 23 | import Theta.Pretty (pr) 24 | import qualified Theta.Types as Theta 25 | 26 | import Theta.Target.Kotlin (Kotlin (..)) 27 | import qualified Theta.Target.Kotlin as Kotlin 28 | 29 | import Apps.Subcommand 30 | 31 | kotlinCommand :: Mod CommandFields Subcommand 32 | kotlinCommand = command "kotlin" $ runKotlin <$> opts 33 | where opts = info 34 | (kotlinOpts <**> helper) 35 | (fullDesc <> progDesc kotlinDescription) 36 | 37 | kotlinDescription :: String 38 | kotlinDescription = [pr| 39 | Compile a Theta module and its transitive imports to Kotlin modules. 40 | |] 41 | 42 | data Opts = Opts 43 | { moduleNames :: [ModuleName] 44 | , prefix :: Maybe [Kotlin] 45 | , target :: FilePath 46 | } 47 | 48 | kotlinOpts :: Parser Opts 49 | kotlinOpts = Opts <$> modules 50 | <*> (fmap parsePrefix <$> importPrefix) 51 | <*> targetDirectory "the theta directory" 52 | where parsePrefix prefix = 53 | case List.find (not . Kotlin.isValidIdentifier) parts of 54 | Nothing -> parts 55 | Just (Kotlin "") -> 56 | error $ "Invalid --prefix specified: ‘" <> prefix <> "’\n" 57 | <> "Did you have an extra ‘.’? Leading, trailing or double \ 58 | \dots are not allowed." 59 | Just invalid -> 60 | let invalidPart = Text.unpack (Kotlin.fromKotlin invalid) in 61 | error $ "Invalid --prefix specified: ‘" <> prefix <> "’\n" 62 | <> "‘" <> invalidPart <> "’ is not a valid identifier." 63 | where parts = Kotlin <$> Text.splitOn "." (Text.pack prefix) 64 | 65 | importPrefix = optional $ 66 | strOption ( long "prefix" 67 | <> metavar "PREFIX" 68 | <> help "An extra prefix to namespace all the Kotlin modules \ 69 | \generated by this command. This should be made up of \ 70 | \valid Kotlin identifiers separated by dots (.)." 71 | ) 72 | 73 | runKotlin :: Opts -> Subcommand 74 | runKotlin Opts { moduleNames, prefix, target } path = do 75 | liftIO $ createDirectoryIfMissing True (target "theta") 76 | 77 | modules <- traverse (Theta.getModule path) moduleNames 78 | 79 | -- create .avsc files for every record and variant 80 | forM_ (Theta.transitiveImports modules) $ \ module_ -> do 81 | liftIO $ generateKotlin target (fromMaybe [] prefix) module_ 82 | 83 | generateKotlin :: FilePath -> [Kotlin] -> Theta.Module -> IO () 84 | generateKotlin target prefix module_@Theta.Module { Theta.moduleName } = do 85 | let Kotlin kotlin = Kotlin.toModule prefix module_ 86 | 87 | createDirectoryIfMissing True target 88 | Text.writeFile (target outPath fileName <.> "kt") kotlin 89 | 90 | where fileName = Text.unpack $ Name.baseName moduleName 91 | outPath = joinPath $ Text.unpack <$> Name.namespace moduleName 92 | -------------------------------------------------------------------------------- /theta/apps/Apps/List.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE MultiWayIf #-} 2 | {-# LANGUAGE NamedFieldPuns #-} 3 | {-# LANGUAGE QuasiQuotes #-} 4 | -- | The @theta list@ subcommand: lists things like module names, 5 | -- types... etc. Handy for automation and querying the environment. 6 | -- 7 | -- Current implementation can: 8 | -- 9 | -- * list all modules in the load path with @theta list modules@ 10 | module Apps.List where 11 | 12 | import Options.Applicative (CommandFields, Mod, Parser, command, 13 | fullDesc, help, helper, info, long, 14 | progDesc, subparser, switch, (<**>)) 15 | 16 | import Control.Monad (forM_) 17 | import Control.Monad.IO.Class (liftIO) 18 | 19 | import Data.Foldable (Foldable (toList)) 20 | import qualified Data.List as List 21 | 22 | import Text.Printf (printf) 23 | 24 | import qualified Theta.LoadPath as LoadPath 25 | import Theta.Pretty (pr, showPretty) 26 | 27 | import Apps.Subcommand (Subcommand) 28 | 29 | -- * list 30 | 31 | listCommand :: Mod CommandFields Subcommand 32 | listCommand = command "list" opts 33 | where opts = info 34 | (subcommands <**> helper) 35 | (fullDesc <> progDesc listDescription) 36 | 37 | subcommands = subparser $ mconcat [ modulesCommand ] 38 | 39 | listDescription :: String 40 | listDescription = [pr| 41 | Commands for listing available Theta objects. 42 | |] 43 | 44 | -- * list modules 45 | 46 | modulesCommand :: Mod CommandFields Subcommand 47 | modulesCommand = command "modules" $ runModules <$> opts 48 | where opts = info (moduleOpts <**> helper) (fullDesc <> progDesc modulesDescription) 49 | 50 | runModules :: ModuleOptions -> Subcommand 51 | runModules options@ModuleOptions { paths, names } loadPath 52 | | not (paths || names) = 53 | runModules (options { names = True }) loadPath 54 | | otherwise = liftIO $ do 55 | modules <- LoadPath.moduleNames loadPath 56 | let sortedModules = List.sortOn (showPretty . fst) $ toList modules 57 | forM_ sortedModules $ \ (moduleName, path) -> if 58 | | paths && names -> printf "%s\t%s\n" (showPretty moduleName) path 59 | | paths -> putStrLn path 60 | | otherwise -> putStrLn (showPretty moduleName) 61 | 62 | modulesDescription :: String 63 | modulesDescription = [pr| 64 | List every Theta module available in the Theta load path: 65 | 66 | • list module names: `theta list modules` or `theta list modules --names` 67 | • list module paths: `theta list modules --paths` 68 | • list both, tab-separated: `theta list modules --names --paths` 69 | |] 70 | 71 | data ModuleOptions = ModuleOptions 72 | { paths :: Bool 73 | , names :: Bool 74 | } 75 | 76 | moduleOpts :: Parser ModuleOptions 77 | moduleOpts = ModuleOptions <$> paths <*> names 78 | where paths = switch ( long "paths" 79 | <> help "List the file path for each module." 80 | ) 81 | names = switch ( long "names" 82 | <> help "List the name of each module." 83 | ) 84 | -------------------------------------------------------------------------------- /theta/apps/Apps/Python.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE LambdaCase #-} 2 | {-# LANGUAGE NamedFieldPuns #-} 3 | {-# LANGUAGE OverloadedStrings #-} 4 | {-# LANGUAGE QuasiQuotes #-} 5 | module Apps.Python where 6 | 7 | import Control.Monad (forM_) 8 | import Control.Monad.Trans (liftIO) 9 | 10 | import qualified Data.List as List 11 | import qualified Data.Text as Text 12 | import qualified Data.Text.IO as Text 13 | 14 | import Options.Applicative 15 | 16 | import System.Directory (createDirectoryIfMissing) 17 | import System.FilePath (joinPath, (<.>), ()) 18 | 19 | import Theta.Import (getModule) 20 | import Theta.Name (ModuleName) 21 | import qualified Theta.Name as Name 22 | import qualified Theta.Types as Theta 23 | 24 | import Theta.Pretty (pr) 25 | import Theta.Target.Python (Python (..)) 26 | import qualified Theta.Target.Python as Python 27 | 28 | import Apps.Subcommand 29 | 30 | pythonCommand :: Mod CommandFields Subcommand 31 | pythonCommand = command "python" $ runPython <$> parser 32 | where parser = info 33 | (opts <**> helper) 34 | (fullDesc <> progDesc pythonDescription) 35 | 36 | pythonDescription :: String 37 | pythonDescription = [pr| 38 | Compile a Theta module and its transitive dependencies to Python modules. 39 | |] 40 | 41 | data Opts = Opts 42 | { moduleNames :: [ModuleName] 43 | , target :: FilePath 44 | , packagePath :: Maybe Python 45 | } 46 | 47 | opts :: Parser Opts 48 | opts = Opts <$> modules 49 | <*> targetDirectory "the theta directory" 50 | <*> importPrefix 51 | where importPrefix = optional $ strOption 52 | ( long "prefix" 53 | <> metavar "PREFIX" 54 | <> help "A prefix to use for imports in the generated Python code." 55 | ) 56 | 57 | runPython :: Opts -> Subcommand 58 | runPython Opts { moduleNames, target, packagePath } path = do 59 | liftIO $ do 60 | createDirectoryIfMissing True target 61 | initPy target 62 | 63 | modules <- traverse (getModule path) moduleNames 64 | 65 | -- create .py files for each module 66 | forM_ (Theta.transitiveImports modules) $ \ module_ -> do 67 | Python python <- Python.toModule module_ packagePath 68 | liftIO $ do 69 | path <- createDirectories $ Theta.moduleName module_ 70 | Text.writeFile path python 71 | 72 | where initPy path = writeFile (path "__init__" <.> "py") "" 73 | 74 | createDirectories moduleName = do 75 | let namespace = Text.unpack <$> Name.namespace moduleName 76 | fullPath = target joinPath namespace 77 | createDirectoryIfMissing True fullPath 78 | 79 | forM_ (List.inits namespace) $ \ parts -> 80 | initPy $ target joinPath parts 81 | 82 | pure $ fullPath Text.unpack (Name.baseName moduleName) <.> "py" 83 | -------------------------------------------------------------------------------- /theta/apps/Apps/Rust.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE LambdaCase #-} 2 | {-# LANGUAGE NamedFieldPuns #-} 3 | {-# LANGUAGE QuasiQuotes #-} 4 | 5 | module Apps.Rust where 6 | 7 | import Control.Monad.Except 8 | 9 | import qualified Data.Text.IO as Text 10 | 11 | 12 | import Options.Applicative 13 | 14 | import qualified Theta.Import as Theta 15 | import qualified Theta.Name as Theta 16 | import qualified Theta.Types as Theta 17 | 18 | import Theta.Pretty (pr) 19 | import Theta.Target.Rust (toFile) 20 | import Theta.Target.Rust.QuasiQuoter (Rust (..)) 21 | 22 | import Apps.Subcommand 23 | 24 | rustCommand :: Mod CommandFields Subcommand 25 | rustCommand = command "rust" $ runRust <$> opts 26 | where opts = info 27 | (rustOpts <**> helper) 28 | (fullDesc <> progDesc rustDescription) 29 | 30 | rustDescription :: String 31 | rustDescription = [pr| 32 | Compile the given Theta modules and their transitive imports to Rust modules. 33 | |] 34 | 35 | 36 | data Opts = Opts { moduleNames :: [Theta.ModuleName] } 37 | 38 | rustOpts :: Parser Opts 39 | rustOpts = Opts <$> modules 40 | 41 | -- | Convert a list of modules to Rust definitions. 42 | -- 43 | -- To get fully valid Rust code, the list of modules needs to be 44 | -- complete and without duplicates: 45 | -- 46 | -- * complete: the list should include every module imported by a 47 | -- module in the list (ie transitive closure) 48 | -- 49 | -- * without duplicates: modules should be included in the list at 50 | -- most once, and the names of both modules and type definitions 51 | -- should be unique inside the entire set 52 | -- 53 | -- If either of these conditions is violated, the generated Rust code 54 | -- is not guaranteed to be compiled—it might refer to Rust definitions 55 | -- that are out of scope, or define conflicting names. 56 | runRust :: Opts -> Subcommand 57 | runRust Opts { moduleNames } path = do 58 | modules <- traverse (Theta.getModule path) moduleNames 59 | let Rust rust = toFile $ Theta.transitiveImports modules 60 | liftIO $ Text.putStrLn rust 61 | -------------------------------------------------------------------------------- /theta/apps/Apps/Subcommand.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE LambdaCase #-} 2 | {-# LANGUAGE QuasiQuotes #-} 3 | {-# LANGUAGE ViewPatterns #-} 4 | module Apps.Subcommand where 5 | 6 | import Control.Monad.Except (ExceptT, runExceptT) 7 | 8 | import Data.Text (Text) 9 | import qualified Data.Text as Text 10 | import qualified Data.Text.IO as Text 11 | 12 | import System.Exit (exitFailure) 13 | import System.IO (stderr) 14 | 15 | import Options.Applicative 16 | 17 | import Theta.Error (Error (..)) 18 | import qualified Theta.Import as Theta 19 | import Theta.Name (ModuleName, Name) 20 | import qualified Theta.Name as Name 21 | import Theta.Pretty (pr) 22 | import qualified Theta.Pretty as Theta 23 | 24 | type Subcommand = Theta.LoadPath -> ExceptT Error IO () 25 | 26 | -- | Run a Theta computation in IO, printing a user-formatted error 27 | -- message to STDERR if it fails. 28 | runTheta :: ExceptT Error IO a -> IO a 29 | runTheta t = runExceptT t >>= \case 30 | Left err -> do 31 | Text.hPutStrLn stderr (Theta.pretty err) 32 | exitFailure 33 | Right a -> pure a 34 | 35 | -- * Flags useful for different subcommands 36 | 37 | -- $ Defining shared flags here reduces code duplication and helps 38 | -- ensure that Theta's command-line interface stays consistent. 39 | 40 | typeName :: Parser Name 41 | typeName = option (eitherReader readName) 42 | ( long "type" 43 | <> short 't' 44 | <> metavar "TYPE" 45 | <> help "A fully-qualified type name. Example: ‘com.example.Foo’." 46 | ) 47 | where readName (Text.pack -> text) = case Name.parse' text of 48 | Left Name.Unqualified -> Left $ Theta.showPretty $ UnqualifiedName text 49 | Left Name.Invalid -> Left $ Theta.showPretty $ InvalidName text 50 | Right name -> Right name 51 | 52 | 53 | targetDirectory :: Text -> Parser FilePath 54 | targetDirectory target = strOption 55 | ( short 'o' 56 | <> long "out" 57 | <> value "." 58 | <> metavar "TARGET_DIRECTORY" 59 | <> help [pr|"Where to create {target} (default ‘.’)."|] 60 | ) 61 | 62 | modules :: Parser [ModuleName] 63 | modules = some $ strOption 64 | ( metavar "MODULE_NAME" 65 | <> long "module" 66 | <> short 'm' 67 | <> help "The name of a Theta module. Flag can be passed multiple times." 68 | ) 69 | -------------------------------------------------------------------------------- /theta/apps/Theta.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE LambdaCase #-} 2 | {-# LANGUAGE NamedFieldPuns #-} 3 | {-# LANGUAGE OverloadedStrings #-} 4 | {-# LANGUAGE QuasiQuotes #-} 5 | module Main where 6 | 7 | import qualified Data.Text.IO as Text 8 | 9 | import GHC.Exts (fromString) 10 | import qualified GHC.IO.Encoding as Encoding 11 | 12 | import Options.Applicative 13 | 14 | import System.Environment (lookupEnv) 15 | import System.Exit (exitFailure) 16 | import System.IO (stderr) 17 | 18 | import qualified Theta.Import as Theta 19 | import Theta.Pretty (pr, pretty) 20 | import Theta.Versions (packageVersion') 21 | 22 | import qualified Apps.Avro as Avro 23 | import qualified Apps.Hash as Hash 24 | import qualified Apps.Kotlin as Kotlin 25 | import qualified Apps.List as List 26 | import qualified Apps.Python as Python 27 | import qualified Apps.Rust as Rust 28 | import Apps.Subcommand (Subcommand, runTheta) 29 | 30 | main :: IO () 31 | main = do 32 | Encoding.setLocaleEncoding Encoding.utf8 33 | 34 | options <- customExecParser (prefs subparserInline) parser 35 | run options 36 | where parser = 37 | info ((version <|> thetaOptions) <**> helper) 38 | (fullDesc <> description <> title) 39 | 40 | -- * Help 41 | 42 | title :: InfoMod a 43 | title = header [pr| 44 | theta ({pretty packageVersion'}) - define interfaces with algebraic data types 45 | |] 46 | 47 | description :: InfoMod a 48 | description = progDesc [pr| 49 | Theta is a language for formally specifying interfaces using 50 | algebraic data types. 51 | 52 | The ‘theta’ command lets you work with Theta schemas and compile 53 | them to different targets like Avro schemas, Python and Rust. 54 | |] 55 | 56 | -- * Subcommands 57 | 58 | subcommands :: Parser Subcommand 59 | subcommands = subparser $ mconcat 60 | [ Avro.avroCommand 61 | , Hash.hashCommand 62 | , Kotlin.kotlinCommand 63 | , List.listCommand 64 | , Python.pythonCommand 65 | , Rust.rustCommand 66 | ] 67 | 68 | -- * Global Options 69 | 70 | data ThetaOptions = 71 | Version 72 | | ThetaOptions 73 | { loadPath :: Maybe Theta.LoadPath 74 | , subcommand :: Subcommand 75 | } 76 | 77 | version :: Parser ThetaOptions 78 | version = flag' Version 79 | ( long "version" 80 | <> help "The version of the Theta package that built this executable." 81 | ) 82 | 83 | thetaOptions :: Parser ThetaOptions 84 | thetaOptions = ThetaOptions <$> (mconcat <$> loadPath) <*> subcommands 85 | where loadPath = many $ Just <$> strOption 86 | ( long "path" 87 | <> short 'p' 88 | <> metavar "THETA_LOAD_PATH" 89 | <> help "The paths that Theta searches for modules, separated by :. \ 90 | \Defaults to THETA_LOAD_PATH environment variable." 91 | ) 92 | 93 | run :: ThetaOptions -> IO () 94 | run Version = putStrLn [pr|Theta {packageVersion'}|] 95 | run ThetaOptions { loadPath, subcommand } = do 96 | path <- maybe lookupPath pure loadPath 97 | runTheta $ subcommand path 98 | where lookupPath = lookupEnv "THETA_LOAD_PATH" >>= \case 99 | Just path -> pure $ fromString path 100 | Nothing -> do 101 | Text.hPutStrLn stderr [pr| 102 | No Theta load path set. You can either: 103 | • specify the load path on the command line with the (--path|-p) argument 104 | • set the THETA_LOAD_PATH environment variable 105 | |] 106 | exitFailure 107 | -------------------------------------------------------------------------------- /theta/bin/profile-compile-times: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | cabal clean 3 | cabal build all \ 4 | --enable-tests \ 5 | --ghc-options='-ddump-to-file -ddump-timings' \ 6 | 2> /dev/null 7 | time-ghc-modules 8 | -------------------------------------------------------------------------------- /theta/default.nix: -------------------------------------------------------------------------------- 1 | { pkgs 2 | 3 | , compiler-version 4 | 5 | , compiler ? pkgs.haskell.packages."${compiler-version}" 6 | 7 | , overrides ? (new: old: {}) 8 | 9 | , source-overrides 10 | 11 | , build-tools ? [ # extra tools available for nix develop 12 | # hack to work around stylish-haskell not building with 13 | # compiler = "ghc902" 14 | pkgs.haskellPackages.stylish-haskell 15 | 16 | compiler.cabal-install 17 | compiler.haskell-language-server 18 | compiler.hlint 19 | 20 | # for ci/stack-test 21 | pkgs.stack 22 | pkgs.jq 23 | 24 | # for bin/profile-compile-times 25 | pkgs.time-ghc-modules 26 | 27 | # not Haskell-specific but eh 28 | pkgs.yaml-language-server 29 | ] 30 | 31 | , werror ? true 32 | 33 | , static-executables-only ? false 34 | }: 35 | 36 | let 37 | lib = pkgs.haskell.lib; 38 | 39 | static-gmp = pkgs.gmp.override { 40 | stdenv = pkgs.makeStaticLibraries pkgs.stdenv; 41 | }; 42 | 43 | static-deps = [ 44 | pkgs.glibc 45 | pkgs.glibc.static 46 | pkgs.zlib.static 47 | static-gmp 48 | (pkgs.libffi.overrideDerivation (old: { 49 | configureFlags = old.configureFlags ++ [ "--enable-static" "--disable-shared" ]; 50 | })) 51 | ]; 52 | 53 | enable-static = p: lib.overrideCabal 54 | (lib.justStaticExecutables p) 55 | ({ configureFlags ? [], extraLibraries ? [], ...}: { 56 | configureFlags = configureFlags ++ [ "-f" "isStatic" ]; 57 | extraLibraries = extraLibraries ++ static-deps; 58 | }); 59 | 60 | enable-werror = p: 61 | if werror 62 | then lib.appendConfigureFlag p "--ghc-option=-Werror" 63 | else p; 64 | 65 | add-build-tools = p: lib.addBuildTools p build-tools; 66 | 67 | excluded = [ 68 | "dist" 69 | "dist-newstyle" 70 | "stack.yaml" 71 | ".stack-work" 72 | "stack.yaml.lock" 73 | "stack-shell.nix" 74 | ]; 75 | in 76 | compiler.developPackage { 77 | name = "theta"; 78 | root = ./.; 79 | 80 | inherit overrides source-overrides; 81 | 82 | # Don't try to build static executables on Darwin systems 83 | modifier = let 84 | base = p: enable-werror (add-build-tools p); 85 | in 86 | if pkgs.stdenv.isDarwin || (!static-executables-only) 87 | then base 88 | else p: enable-static (base p); 89 | 90 | # explicitly disable "smart" detection of nix-shell status 91 | # 92 | # The default value of returnShellEnv is impure: it checks the 93 | # value of the IN_NIX_SHELL environment variable. 94 | returnShellEnv = false; 95 | } 96 | -------------------------------------------------------------------------------- /theta/hie.yaml: -------------------------------------------------------------------------------- 1 | cradle: 2 | cabal: 3 | -------------------------------------------------------------------------------- /theta/src/Theta/Fixed.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE AllowAmbiguousTypes #-} 2 | {-# LANGUAGE DataKinds #-} 3 | {-# LANGUAGE DerivingStrategies #-} 4 | {-# LANGUAGE KindSignatures #-} 5 | {-# LANGUAGE MonadComprehensions #-} 6 | {-# LANGUAGE OverloadedStrings #-} 7 | {-# LANGUAGE ScopedTypeVariables #-} 8 | {-# LANGUAGE TypeApplications #-} 9 | 10 | module Theta.Fixed 11 | ( FixedBytes 12 | , toByteString 13 | 14 | , fixedBytes 15 | , fixedBytes' 16 | , size 17 | , sizeOf 18 | ) 19 | where 20 | 21 | import qualified Data.ByteString.Lazy as LBS 22 | import Data.Data (Proxy (Proxy)) 23 | import Data.Maybe (fromMaybe) 24 | import qualified Data.Text as Text 25 | import Data.TreeDiff (Expr (App), ToExpr (toExpr)) 26 | 27 | import GHC.TypeLits (KnownNat, Nat, natVal) 28 | 29 | import Text.Printf (printf) 30 | 31 | import Theta.Pretty (Pretty (pretty)) 32 | 33 | -- | A bytestring with a size specified at the type level. 34 | -- 35 | -- The @size@ should always be the same as the length of the 36 | -- underlying bytestring, otherwise the behavior of the type is 37 | -- unspecified. 38 | newtype FixedBytes (size :: Nat) = FixedBytes { toByteString :: LBS.ByteString } 39 | deriving stock (Eq) 40 | 41 | instance KnownNat size => Show (FixedBytes size) where 42 | show fixed = printf "FixedBytes @%d %s" (sizeOf fixed) (show $ toByteString fixed) 43 | 44 | instance Pretty (FixedBytes size) where 45 | pretty (FixedBytes bytes) = Text.pack $ show bytes 46 | 47 | instance KnownNat size => ToExpr (FixedBytes size) where 48 | toExpr t = App "FixedBytes" [toExpr $ show t] 49 | 50 | -- | Wrap a bytestring into a 'FixedBytes' with a set size. 51 | -- 52 | -- Returns 'Nothing' if the length of the bytestring does not match 53 | -- the statically expected size. 54 | -- 55 | -- When the @size@ type variable cannot be inferred, you can specify 56 | -- it with type applications: 57 | -- 58 | -- >>> fixedBytes @3 "abc" 59 | -- Just (FixedBytes {toByteString = "abc"}) 60 | -- 61 | -- >>> fixedBytes @4 "abc" 62 | -- Nothing 63 | -- 64 | fixedBytes :: forall size. KnownNat size => LBS.ByteString -> Maybe (FixedBytes size) 65 | fixedBytes bytes = [FixedBytes bytes | LBS.length bytes == size] 66 | where size = fromIntegral $ natVal (Proxy @size) 67 | 68 | -- | Wrap a bytestring into a 'FixedBytes' with a set size. 69 | -- 70 | -- Errors at runtime if the bytestring's length does not match the 71 | -- expected size. 72 | -- 73 | -- When the @size@ type variable cannot be inferred, you can specify 74 | -- it with type applications: 75 | -- 76 | -- >>> fixedBytes' @3 "abc" 77 | -- FixedBytes {toByteString = "abc"} 78 | -- 79 | -- >>> fixedBytes' @4 "abc" 80 | -- fixedBytes expected 4 bytes but got a bytestring of length 3. 81 | -- 82 | fixedBytes' :: forall size. KnownNat size => LBS.ByteString -> FixedBytes size 83 | fixedBytes' bytes = fromMaybe failure $ fixedBytes bytes 84 | where failure = error $ printf 85 | "fixedBytes expected %d bytes but got a bytestring of length %d" expected got 86 | expected = natVal (Proxy @size) 87 | got = LBS.length bytes 88 | 89 | -- | The runtime size that corresponds to a type-level size. 90 | -- 91 | -- The @size@ type variable will always be ambiguous, so this is meant 92 | -- to be used with visible type applications: 93 | -- 94 | -- >>> size @10 95 | -- 10 96 | -- 97 | size :: forall size. KnownNat size => Word 98 | size = fromIntegral $ natVal (Proxy @size) 99 | 100 | -- | The statically specified size of the given 'FixedBytes' value. 101 | -- 102 | -- >>> sizeOf (fixedBytes' @3 "abc") 103 | -- 3 104 | -- 105 | sizeOf :: forall size. KnownNat size => FixedBytes size -> Word 106 | sizeOf _ = size @size 107 | -------------------------------------------------------------------------------- /theta/src/Theta/Hash.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DerivingStrategies #-} 2 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} 3 | {-# LANGUAGE OverloadedStrings #-} 4 | {-# LANGUAGE TemplateHaskellQuotes #-} 5 | -- | Theta types carry a unique, structural hash for quick equality 6 | -- comparisons. 7 | -- 8 | -- Two equivalent types will always have the same hash. A reference to 9 | -- a type will have the same hash as the type itself. 10 | -- 11 | -- The hashing implementation may change in different versions of 12 | -- Theta, so you should not rely on hashes being stable across 13 | -- releases of the @theta@ package. 14 | module Theta.Hash where 15 | 16 | import Data.Binary (Binary, encode) 17 | import qualified Data.Binary as Binary 18 | import qualified Data.ByteString.Lazy as LBS 19 | import Data.Digest.Pure.MD5 (MD5Digest, md5) 20 | import Data.Text (Text) 21 | import qualified Data.Text.Encoding as Text 22 | 23 | import Instances.TH.Lift () 24 | 25 | import Language.Haskell.TH.Syntax (Lift (liftTyped)) 26 | 27 | -- | The output of our hashing function. 28 | -- 29 | -- Currently uses MD5 under the hood, but this might change in the 30 | -- future—don't rely on the hashing implementation directly. 31 | newtype Hash = Hash MD5Digest 32 | deriving newtype (Binary, Show, Eq, Ord) 33 | 34 | instance Semigroup Hash where 35 | Hash a <> Hash b = toHash $ encode a <> encode b 36 | 37 | instance Lift Hash where 38 | liftTyped (Hash hash) = [|| Binary.decode encoded ||] 39 | where encoded = Binary.encode hash 40 | 41 | -- | The hashing function we're using. Designed to be easy to 42 | -- change—just change this function + the type underneath 'Hash'. 43 | toHash :: LBS.ByteString -> Hash 44 | toHash = Hash . md5 45 | 46 | -- | Calculate a hash for the given text. This doesn't do any extra 47 | -- processing, so differences in whitespace/etc will produce different 48 | -- hashes. 49 | hashText :: Text -> Hash 50 | hashText = toHash . LBS.fromStrict . Text.encodeUtf8 51 | 52 | -- | Calculate a canonical hash for a list of hashes. 53 | -- 54 | -- This lets us consistently hash multiple hashable values in a given 55 | -- order. 56 | hashList :: [Hash] -> Hash 57 | hashList = foldr (<>) (hashText "") 58 | -------------------------------------------------------------------------------- /theta/src/Theta/Metadata.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DeriveLift #-} 2 | {-# LANGUAGE DerivingStrategies #-} 3 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} 4 | {-# LANGUAGE TemplateHaskellQuotes #-} 5 | {-# LANGUAGE ViewPatterns #-} 6 | 7 | -- | This module defines types for handling version metadata in Theta 8 | -- schemas. 9 | -- 10 | -- Each Theta schema starts with a section that specifies the version 11 | -- of the language and Avro encoding the schema expects: 12 | -- 13 | -- @ 14 | -- language-version: 1.0.0 15 | -- avro-version: 1.2.0 16 | -- --- 17 | -- @ 18 | -- 19 | -- Theta then guarantees that two identical schemas that specify the 20 | -- same language and encoding version will produce compatible Avro 21 | -- schemas and objects with different versions—or even different 22 | -- /implementations/—of the Theta compiler. 23 | module Theta.Metadata where 24 | 25 | import Data.Text (Text) 26 | import qualified Data.Text as Text 27 | import Data.Versions (SemVer (..), prettySemVer) 28 | import qualified Data.Versions as Version 29 | 30 | import GHC.Exts (IsString (..)) 31 | 32 | import Language.Haskell.TH.Syntax (Lift (liftTyped)) 33 | 34 | import Test.QuickCheck (Arbitrary (arbitrary)) 35 | 36 | import Text.Megaparsec (errorBundlePretty) 37 | 38 | import qualified Theta.Name as Name 39 | import Theta.Pretty (Pretty (..)) 40 | 41 | -- | The data included in a module's metadata section. 42 | data Metadata = Metadata 43 | { languageVersion :: Version 44 | -- ^ The version of the Theta language determines what language 45 | -- features the schema can use and how those features work. 46 | , avroVersion :: Version 47 | -- ^ The Avro version determines how a schema is converted to 48 | -- Avro. The same schema compiled at the same Avro version should 49 | -- always generate compatible Avro data. 50 | , moduleName :: Name.ModuleName 51 | -- ^ The name of the module that this section belongs to. 52 | } 53 | deriving stock (Show, Eq, Lift) 54 | 55 | instance Arbitrary Metadata where 56 | arbitrary = Metadata <$> arbitrary <*> arbitrary <*> arbitrary 57 | 58 | -- | A semantic version that's compliant with the semver spec. 59 | -- 60 | -- This is just a wrapper over 'SemVer' that lets me add typeclass 61 | -- instances. 62 | newtype Version = Version SemVer 63 | deriving newtype (Show, Eq, Ord) 64 | 65 | instance Arbitrary Version where 66 | arbitrary = Version <$> semver 67 | where semver = SemVer <$> arbitrary 68 | <*> arbitrary 69 | <*> arbitrary 70 | <*> pure [] 71 | <*> pure Nothing 72 | 73 | instance Lift Version where 74 | liftTyped (pretty -> v) = [|| either error id (fromText v) ||] 75 | 76 | -- | Render a 'Version' in a compact, human-readable format. 77 | -- 78 | -- @ 79 | -- λ> show ("1.2.1" :: Version) 80 | -- "SemVer {_svMajor = 1, _svMinor = 2, _svPatch = 1, _svPreRel = [], _svMeta = []}" 81 | -- λ> pretty ("1.2.1" :: Version) 82 | -- "1.2.1" 83 | -- @ 84 | instance Pretty Version where 85 | pretty (Version semVer) = prettySemVer semVer 86 | 87 | -- | Turns a literal "1.2.0" into a 'Version'. Errors out if the 88 | -- format is not compliant with semver. 89 | instance IsString Version where 90 | fromString = either error id . fromText . Text.pack 91 | 92 | -- | Parse a 'Version' from a string formatted as semver. 93 | -- 94 | -- Returns a formatted parse error message if the string is not a 95 | -- compliant with semver. 96 | fromText :: Text -> Either String Version 97 | fromText text = case Version.semver text of 98 | Left parseError -> Left $ errorBundlePretty parseError 99 | Right version -> Right $ Version version 100 | -------------------------------------------------------------------------------- /theta/src/Theta/Pretty.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DerivingVia #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | {-# LANGUAGE StandaloneDeriving #-} 4 | -- | This module defines a 'Pretty' class that translates types to 5 | -- user-friendly 'Text' representations. 6 | -- 7 | -- Any user-facing text (like error messages) should use this class to 8 | -- display values in a consistent way. 9 | module Theta.Pretty 10 | ( Pretty(..) 11 | 12 | , ShowPretty(..) 13 | , showPretty 14 | 15 | , prettyList 16 | , indentBy 17 | 18 | , pr 19 | , p 20 | ) 21 | where 22 | 23 | import Data.Text (Text) 24 | import qualified Data.Text as Text 25 | 26 | import Language.Haskell.TH.Quote (QuasiQuoter) 27 | 28 | import qualified Data.Char as Char 29 | import qualified PyF 30 | 31 | class Pretty a where 32 | pretty :: a -> Text 33 | 34 | instance Pretty Text where pretty x = x 35 | 36 | deriving via ShowPretty Word instance Pretty Word 37 | 38 | -- | A quasiquoter for interpolating text, good for writing 'Pretty' 39 | -- instances/etc. 40 | -- 41 | -- At the moment there is no 'Pretty'-specific support, but that is 42 | -- likely to change in the future. 43 | -- 44 | -- Leading whitespace is trimmed from each line of the quoted 45 | -- string. The first line is ignored if it is all whitespace. 46 | -- 47 | -- __Example__ 48 | -- 49 | -- @ 50 | -- let name = "com.example.Name" 51 | -- moduleName = "com.example" 52 | -- in [pr| 53 | -- {pretty name} is not defined in the module {pretty moduleName}. 54 | -- |] 55 | -- @ 56 | -- 57 | -- would produce: 58 | -- 59 | -- @ 60 | -- "com.example.Name is not defined in the module com.example.\n" 61 | -- @ 62 | pr :: QuasiQuoter 63 | pr = PyF.fmtTrim 64 | 65 | -- | Original name for 'pr', but it conflicts with Template Haskell, 66 | -- so use 'pr' instead. 67 | p :: QuasiQuoter 68 | p = pr 69 | {-# DEPRECATED p "Use pr instead" #-} 70 | 71 | -- | The same as 'pretty' but returns a 'String'. 72 | showPretty :: Pretty a => a -> String 73 | showPretty = Text.unpack . pretty 74 | 75 | -- | Render a list of items as a bulleted list, with one item per 76 | -- line. 77 | -- 78 | -- Example: @showPretty ["abc", "def", "ghi"]@ 79 | -- 80 | -- @ 81 | -- • abc 82 | -- • def 83 | -- • ghi 84 | -- @ 85 | prettyList :: Pretty a => [a] -> Text 86 | prettyList = Text.intercalate "\n" . map (("• " <>) . pretty) 87 | 88 | -- | Indent every line in the given string using the specified number 89 | -- of spaces. 90 | indentBy :: Int -> Text -> Text 91 | indentBy level str = case Text.lines str of 92 | [] -> "" 93 | lines -> foldr1 (\ a b -> a <> "\n" <> b) $ indent <$> lines 94 | where indent line 95 | | Text.all Char.isSpace line = line 96 | | otherwise = Text.replicate level " " <> line 97 | 98 | -- | A newtype with: 99 | -- 100 | -- * a Show instance using 'showPretty' on the underlying type 101 | -- 102 | -- * a Pretty instance using 'show' on the underlying type 103 | -- 104 | -- You can use this with @DerivingVia@ to get a 'Show' instance for a 105 | -- type which already has a 'Pretty' instance or vice-versa. 106 | -- 107 | -- @ 108 | -- data MyType = MyType {...} 109 | -- deriving Show via ShowPretty MyType 110 | -- @ 111 | -- 112 | -- @ 113 | -- data MyType = MyType {...} 114 | -- deriving Pretty via ShowPretty MyType 115 | -- @ 116 | -- 117 | -- __Note__: doing this for /both/ 'Pretty' /and/ 'Show' on the same 118 | -- type will lead to an infinite loop. 119 | newtype ShowPretty a = ShowPretty a 120 | 121 | instance Pretty a => Show (ShowPretty a) where 122 | show (ShowPretty x) = showPretty x 123 | 124 | instance Show a => Pretty (ShowPretty a) where 125 | pretty (ShowPretty x) = Text.pack (show x) 126 | -------------------------------------------------------------------------------- /theta/src/Theta/Primitive.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DeriveLift #-} 2 | {-# LANGUAGE DerivingStrategies #-} 3 | {-# LANGUAGE OverloadedStrings #-} 4 | -- | Theta supports a number of "primitive" types that have built-in 5 | -- encodings and behaviors. 6 | -- 7 | -- Based on [Avro's primitive 8 | -- types](https://avro.apache.org/docs/current/spec.html#schema_primitive): 9 | -- 10 | -- * Bool 11 | -- * Bytes 12 | -- * Int 13 | -- * Long 14 | -- * Float 15 | -- * Double 16 | -- * String 17 | -- 18 | -- Based on [Avro's logical 19 | -- types](https://avro.apache.org/docs/current/spec.html#Logical+Types): 20 | -- 21 | -- * Date 22 | -- * Datetime 23 | module Theta.Primitive where 24 | 25 | import Data.Text (Text) 26 | import qualified Data.Text as Text 27 | 28 | import Language.Haskell.TH.Syntax (Lift) 29 | 30 | import Theta.Hash (Hash) 31 | import Theta.Metadata (Version) 32 | import Theta.Name (Name (Name), hashName) 33 | import Theta.Pretty (Pretty, pretty) 34 | 35 | -- | Theta's primitive types. 36 | -- 37 | -- A primitive type has its own encoding and behavior—can be 38 | -- fundamentally different from user-defined types. 39 | data Primitive = Bool 40 | | Bytes 41 | -- ^ A variable-length byte array—can store binary 42 | -- blobs 43 | | Int 44 | -- ^ 32-bit signed integers 45 | | Long 46 | -- ^ 64-bit signed integers 47 | | Float 48 | -- ^ 32-bit floating-point numbers 49 | | Double 50 | -- ^ 64-bit floating-point numbers 51 | | String 52 | -- ^ Unicode-aware strings 53 | | Date 54 | -- ^ An absolute date (eg @2020-01-10@) with no 55 | -- timezone/locale specified. 56 | | Datetime 57 | -- ^ An absolute timestamp in UTC. 58 | | UUID 59 | -- ^ A universally unique identifier (UUID), conforming 60 | -- to [RFC 4122](https://www.ietf.org/rfc/rfc4122.txt) 61 | -- 62 | -- Example: @f81d4fae-7dec-11d0-a765-00a0c91e6bf6@ 63 | | Time 64 | -- ^ The time of day, starting at midnight. 65 | -- 66 | -- Language support for leap seconds is inconsistent, 67 | -- so Theta's Time type explicitly does not support 68 | -- leap seconds. 69 | | LocalDatetime 70 | -- ^ An absolute timestamp in whatever timezone is 71 | -- considered local. (No timezone/locale is specified.) 72 | deriving stock (Eq, Show, Ord, Enum, Bounded, Lift) 73 | 74 | instance Pretty Primitive where pretty = primitiveKeyword 75 | 76 | -- | Returns a canonical name for each primitive type. 77 | -- 78 | -- Primitive types have @theta.primitive@ as their canonical 79 | -- namespace. @Int@ become @theta.primitive.Int@, @String@ becomes 80 | -- @theta.primitive.String@... etc. 81 | -- 82 | -- Currently these names are not defined in Theta itself, but that 83 | -- will probably change in the future. 84 | primitiveName :: Primitive -> Name 85 | primitiveName = Name "theta.primitive" . primitiveKeyword 86 | 87 | -- | The canonical keyword for each primitive type. 88 | -- 89 | -- Currently this is automatically derived from the 'Show' instance 90 | -- for 'Primitive', but this is not guaranteed to hold in the future. 91 | primitiveKeyword :: Primitive -> Text 92 | primitiveKeyword = Text.pack . show 93 | 94 | -- | Return a canonical hash for each primitive type. 95 | hashPrimitive :: Primitive -> Hash 96 | hashPrimitive = hashName . primitiveName 97 | 98 | -- | The earliest @language-version@ that supports the given primitive 99 | -- type. 100 | definedIn :: Primitive -> Version 101 | definedIn Bool = "1.0.0" 102 | definedIn Bytes = "1.0.0" 103 | definedIn Int = "1.0.0" 104 | definedIn Long = "1.0.0" 105 | definedIn Float = "1.0.0" 106 | definedIn Double = "1.0.0" 107 | definedIn String = "1.0.0" 108 | definedIn Date = "1.0.0" 109 | definedIn Datetime = "1.0.0" 110 | definedIn UUID = "1.1.0" 111 | definedIn Time = "1.1.0" 112 | definedIn LocalDatetime = "1.1.0" 113 | 114 | -- | Every single primitive type supported by Theta. 115 | primitives :: [Primitive] 116 | primitives = [minBound..maxBound] 117 | -------------------------------------------------------------------------------- /theta/src/Theta/Target/Avro/Process.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE FlexibleContexts #-} 2 | -- | Functionality for launching and interacting with external 3 | -- processes: streaming Theta values encoded as Avro over STDIN and 4 | -- reading them back over STDOUT. 5 | module Theta.Target.Avro.Process where 6 | 7 | import Control.Monad.Catch (MonadThrow (throwM)) 8 | import Control.Monad.Except (MonadError) 9 | import Control.Monad.IO.Class (MonadIO (liftIO)) 10 | 11 | import qualified Data.ByteString.Lazy as LBS 12 | 13 | import qualified Streamly.Prelude as Streamly 14 | 15 | import qualified Streamly.External.ByteString.Lazy as SBS 16 | import qualified Streamly.System.Process as Streamly 17 | 18 | 19 | import Control.Monad 20 | import Control.Monad.Trans (lift) 21 | import Theta.Error (Error) 22 | import Theta.Target.Haskell.Conversion (FromTheta, ToTheta, 23 | decodeAvro, decodeAvro', 24 | encodeAvro) 25 | 26 | -- | Run an external process with a single input of type @a@, reading 27 | -- a single output of type @b@. 28 | -- 29 | -- If the process terminates with a non-zero error code, raises a 30 | -- 'ProcessFailure' exception. 31 | run :: (ToTheta a, FromTheta b, MonadIO m, MonadError Error m) 32 | => FilePath 33 | -- ^ Path to the executable to run. 34 | -> [String] 35 | -- ^ Arguments for the executable. 36 | -> a 37 | -- ^ Input value. Written to Avro over STDIN with no Avro 38 | -- container. 39 | -> m b 40 | -- ^ Output value. Read as Avro from STDOUT, not expecting an Avro 41 | -- container. 42 | run executable args input = do 43 | let inputStream = SBS.toChunks (encodeAvro input) 44 | out <- liftIO $ SBS.fromChunksIO $ 45 | Streamly.processChunks executable args inputStream 46 | decodeAvro out 47 | 48 | -- | Run an external process, streaming in values of type @a@ and 49 | -- streaming out values of type @b@. 50 | -- 51 | -- If the process terminates with a non-zero error code, raises a 52 | -- 'ProcessFailure' exception. 53 | -- 54 | -- Any decoding errors will result in an exception. 55 | stream :: (ToTheta a, FromTheta b) 56 | => FilePath 57 | -- ^ Path to the executable to run. 58 | -> [String] 59 | -- ^ Arguments for the executable. 60 | -> Streamly.SerialT IO a 61 | -- ^ Stream of inputs that can be encoded to Avro with Theta. 62 | -> Streamly.SerialT IO b 63 | stream executable args inputs = decodeFromChunks $ 64 | Streamly.processChunks executable args encoded 65 | where encoded = SBS.toChunks . encodeAvro =<< inputs 66 | 67 | decodeFromChunks chunks = join $ lift $ 68 | Streamly.unfoldrM decodeChunk <$> SBS.fromChunksIO chunks 69 | 70 | decodeChunk bytes 71 | | LBS.null bytes = pure Nothing 72 | | otherwise = case decodeAvro' bytes of 73 | Left err -> throwM err 74 | Right (result, remainder) -> pure $ Just (result, remainder) 75 | -------------------------------------------------------------------------------- /theta/src/Theta/Target/Haskell/HasTheta.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE AllowAmbiguousTypes #-} 2 | {-# LANGUAGE FlexibleInstances #-} 3 | {-# LANGUAGE ScopedTypeVariables #-} 4 | {-# LANGUAGE TypeApplications #-} 5 | 6 | -- | This module defines a class which maps Haskell types to Theta 7 | -- types. 8 | -- 9 | -- Haskell types generated with Template Haskell use this class to 10 | -- point to the Theta type that created them. 11 | module Theta.Target.Haskell.HasTheta where 12 | 13 | import Data.ByteString.Lazy (ByteString) 14 | import Data.HashMap.Strict (HashMap) 15 | import Data.Int (Int32, Int64) 16 | import Data.Text (Text) 17 | import Data.Time (Day, LocalTime, TimeOfDay, UTCTime) 18 | import Data.UUID (UUID) 19 | 20 | import GHC.TypeLits (KnownNat) 21 | 22 | import Theta.Fixed (FixedBytes) 23 | import qualified Theta.Fixed as Fixed 24 | import qualified Theta.Types as Theta 25 | 26 | -- | A class for Haskell types that correspond to a Theta type. Types 27 | -- generated via Template Haskell will automatically have an instance 28 | -- of this class pointing to the schema that created them. 29 | class HasTheta a where 30 | -- | The Theta type that corresponds to a Haskell type. Because the 31 | -- Haskell type is ambiguous, this method has to be used with a 32 | -- visible type application, with the TypeApplications language extension 33 | -- (e.g. `theta @MyExample`) 34 | theta :: Theta.Type 35 | 36 | -- * Instances for primitive types 37 | 38 | instance HasTheta Bool where 39 | theta = Theta.bool' 40 | 41 | instance HasTheta ByteString where 42 | theta = Theta.bytes' 43 | 44 | instance HasTheta Int32 where 45 | theta = Theta.int' 46 | 47 | instance HasTheta Int64 where 48 | theta = Theta.long' 49 | 50 | instance HasTheta Float where 51 | theta = Theta.float' 52 | 53 | instance HasTheta Double where 54 | theta = Theta.double' 55 | 56 | instance HasTheta Text where 57 | theta = Theta.string' 58 | 59 | instance HasTheta Day where 60 | theta = Theta.date' 61 | 62 | instance HasTheta UTCTime where 63 | theta = Theta.datetime' 64 | 65 | instance HasTheta UUID where 66 | theta = Theta.uuid' 67 | 68 | instance HasTheta TimeOfDay where 69 | theta = Theta.time' 70 | 71 | instance HasTheta LocalTime where 72 | theta = Theta.localDatetime' 73 | 74 | instance KnownNat size => HasTheta (FixedBytes size) where 75 | theta = Theta.fixed' $ Fixed.size @size 76 | 77 | instance HasTheta a => HasTheta [a] where 78 | theta = Theta.array' $ theta @a 79 | 80 | instance HasTheta a => HasTheta (HashMap Text a) where 81 | theta = Theta.map' $ theta @a 82 | 83 | instance HasTheta a => HasTheta (Maybe a) where 84 | theta = Theta.optional' $ theta @a 85 | -------------------------------------------------------------------------------- /theta/src/Theta/Target/Kotlin/QuasiQuoter.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DerivingStrategies #-} 2 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} 3 | {-# LANGUAGE TemplateHaskell #-} 4 | -- | A quasiquoter that generates Kotlin code, with basic templating 5 | -- support. This makes our Kotlin-generating code much easier to read 6 | -- and write. 7 | -- 8 | -- @ 9 | -- -- generate a Kotlin class with the given name and fields: 10 | -- class' :: Kotlin -> Kotlin 11 | -- class' name = [kotlin| 12 | -- class $name() { 13 | -- } 14 | -- |] 15 | -- @ 16 | -- 17 | -- Currently this quasiquoter just does simple string interpolation 18 | -- without checking whether the generated Kotlin code is syntactically 19 | -- valid. Checking syntactic validity at compile time is probably 20 | -- infeasible, but we might add some kind of runtime checking in the 21 | -- future. 22 | -- 23 | -- To make the generated code readable, we have a few rules to handle 24 | -- whitespace: 25 | -- 26 | -- * leading and trailing blank lines are ignored 27 | -- 28 | -- * leading indentation is ignored 29 | -- 30 | -- * interpolating a multi-line snippet will indent each line to the 31 | -- * same level 32 | 33 | module Theta.Target.Kotlin.QuasiQuoter where 34 | 35 | import Data.Text (Text) 36 | 37 | import GHC.Exts (IsString) 38 | 39 | import Language.Haskell.TH.Quote (QuasiQuoter (..)) 40 | 41 | import Theta.Target.LanguageQuoter (Interpolable (..), quoter) 42 | 43 | -- | A valid Kotlin snippet (stored as text). 44 | -- 45 | -- This could either be a standalone Kotlin program (class, function, 46 | -- statement... etc) or a fragment of a program (identifier, 47 | -- expression... etc). 48 | newtype Kotlin = Kotlin { fromKotlin :: Text } 49 | deriving stock (Show, Eq) 50 | deriving newtype (IsString, Semigroup, Monoid) 51 | 52 | instance Interpolable Kotlin where 53 | toText = fromKotlin 54 | fromText = Kotlin 55 | 56 | -- | The 'kotlin' quasiquoter generates 'Kotlin' snippets, allowing 57 | -- you to interpolate other Kotlin snippets from Haskell variables: 58 | -- 59 | -- @ 60 | -- function :: Kotlin 61 | -- function = [kotlin| 62 | -- fun foo($variableName: Int): Int { 63 | -- return $variableName + 1 64 | -- } 65 | -- |] 66 | -- where variableName :: Kotlin 67 | -- variableName = Kotlin "x" 68 | -- @ 69 | -- 70 | -- You can get a literal @$@ by typing two in a row: @$$@. 71 | -- 72 | -- The quasiquoter does straightforward string interpolation with a 73 | -- few rules to make managing indentation easier: 74 | -- 75 | -- * the first and last lines are ignored completely if they are all 76 | -- whitespace (including for applying the rest of these rules) 77 | -- 78 | -- * leading indentation is ignored—you can indent the internals of a 79 | -- @[kotlin| |]@ block as part of your code layout without 80 | -- affecting the generated Kotlin code 81 | -- 82 | -- * interpolating a multi-line snippet in a variable will indent 83 | -- every line to the same level 84 | -- 85 | -- This lets us define a multi-line method, include it in a class body 86 | -- and have all the indentation work out correctly: 87 | -- 88 | -- @ 89 | -- class' :: Kotlin 90 | -- class' = [kotlin| 91 | -- class Fooable() { 92 | -- $method 93 | -- } 94 | -- |] 95 | -- where method = [kotlin| 96 | -- fun foo() { 97 | -- println("foo") 98 | -- println("bar") 99 | -- } 100 | -- |] 101 | -- @ 102 | -- 103 | -- This produces the following Kotlin snippet: 104 | -- 105 | -- @ 106 | -- class Fooable() { 107 | -- def foo() { 108 | -- println("foo") 109 | -- println("bar") 110 | -- } 111 | -- } 112 | -- @ 113 | kotlin :: QuasiQuoter 114 | kotlin = quoter ''Kotlin 115 | -------------------------------------------------------------------------------- /theta/src/Theta/Target/Python/QuasiQuoter.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DerivingStrategies #-} 2 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} 3 | {-# LANGUAGE OverloadedStrings #-} 4 | {-# LANGUAGE TemplateHaskell #-} 5 | {-# LANGUAGE TypeApplications #-} 6 | 7 | -- | A quasiquoter that generates 'Python' values. This makes it easy 8 | -- for us to generate Python code dynamically. 9 | -- 10 | -- @ 11 | -- -- generate a simple Python class with the given name: 12 | -- class' :: Python -> Python 13 | -- class' name = [python| 14 | -- class $name: 15 | -- pass 16 | -- |] 17 | -- @ 18 | -- 19 | -- Right now, this just does string interpolation under the hood with 20 | -- some simple rules to help manage indentation: 21 | -- 22 | -- * leading and trailing blank lines are ignored completely 23 | -- (including for applying the rest of these rules) 24 | -- 25 | -- * leading indentation is ignored—you can indent the internals of a 26 | -- @[python| |]@ block as part of your code layout without 27 | -- affecting the semantics of the generated Python code 28 | -- 29 | -- * interpolating a multi-line snippet in a variable will indent 30 | -- every line to the same level 31 | -- 32 | -- This lets us define a multi-line method, include it in a class body 33 | -- and have all the indentation work out correctly: 34 | -- 35 | -- @ 36 | -- class' :: Python 37 | -- class' = [python| 38 | -- class Fooable: 39 | -- $method 40 | -- |] 41 | -- where method = [python| 42 | -- def foo(): 43 | -- print("foo") 44 | -- print ("bar") 45 | -- |] 46 | -- @ 47 | -- 48 | -- This produces the following Python snippet: 49 | -- 50 | -- @ 51 | -- class Fooable: 52 | -- def foo(): 53 | -- print("foo") 54 | -- print("bar") 55 | -- @ 56 | module Theta.Target.Python.QuasiQuoter 57 | ( Python (..) 58 | , python 59 | ) 60 | where 61 | 62 | import Data.Text (Text) 63 | 64 | import GHC.Exts (IsString) 65 | 66 | import Language.Haskell.TH.Quote (QuasiQuoter (..)) 67 | 68 | import Theta.Target.LanguageQuoter (Interpolable (..), quoter) 69 | 70 | -- | A valid Python snippet (stored as text). 71 | -- 72 | -- This could either be a standalone Python program (class, function, 73 | -- statement... etc) or a fragment of a program (identifier, 74 | -- expression... etc). 75 | newtype Python = Python { fromPython :: Text } 76 | deriving stock (Show, Eq) 77 | deriving newtype (IsString, Semigroup, Monoid) 78 | 79 | instance Interpolable Python where 80 | toText = fromPython 81 | fromText = Python 82 | 83 | -- | The 'python' quasiquoter generates 'Python' snippets, allowing 84 | -- you to interpolate other Python snippets from Haskell variables: 85 | -- 86 | -- @ 87 | -- function :: Python 88 | -- function = [python| 89 | -- def foo($variableName): 90 | -- return $variableName + 1 91 | -- |] 92 | -- where variableName :: Python 93 | -- variableName = Python "x" 94 | -- @ 95 | -- 96 | -- You can get a literal @$@ by typing two in a row: @$$@. 97 | -- 98 | -- The quasiquoter does straightforward string interpolation with a 99 | -- few rules to make managing indentation easier: 100 | -- 101 | -- * the first and last lines are ignored completely if they are all 102 | -- whitespace (including for applying the rest of these rules) 103 | -- 104 | -- * leading indentation is ignored—you can indent the internals of a 105 | -- @[python| |]@ block as part of your code layout without 106 | -- affecting the semantics of the generated Python code 107 | -- 108 | -- * interpolating a multi-line snippet in a variable will indent 109 | -- every line to the same level 110 | -- 111 | -- This lets us define a multi-line method, include it in a class body 112 | -- and have all the indentation work out correctly: 113 | -- 114 | -- @ 115 | -- class' :: Python 116 | -- class' = [python| 117 | -- class Fooable: 118 | -- $method 119 | -- |] 120 | -- where method = [python| 121 | -- def foo(): 122 | -- print("foo") 123 | -- print ("bar") 124 | -- |] 125 | -- @ 126 | -- 127 | -- This produces the following Python snippet: 128 | -- 129 | -- @ 130 | -- class Fooable: 131 | -- def foo(): 132 | -- print("foo") 133 | -- print("bar") 134 | -- @ 135 | python :: QuasiQuoter 136 | python = quoter ''Python 137 | -------------------------------------------------------------------------------- /theta/src/Theta/Target/Rust/QuasiQuoter.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DerivingStrategies #-} 2 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} 3 | {-# LANGUAGE OverloadedStrings #-} 4 | {-# LANGUAGE TemplateHaskell #-} 5 | {-# LANGUAGE TypeApplications #-} 6 | 7 | -- | A quasiquoter that generates 'Rust' values. This makes it easy 8 | -- for us to generate Rust code dynamically. 9 | -- 10 | -- @ 11 | -- -- generate a simple Rust struct with the given name: 12 | -- struct :: Rust -> Rust 13 | -- struct name = [rust| 14 | -- #[derive(Debug)] 15 | -- struct $name { 16 | -- field: i32, 17 | -- } 18 | -- |] 19 | -- @ 20 | -- 21 | -- Right now, this just does string interpolation under the hood with 22 | -- some simple rules to help manage indentation: 23 | -- 24 | -- * leading and trailing blank lines are ignored completely 25 | -- (including for applying the rest of these rules) 26 | -- 27 | -- * leading indentation is ignored—you can indent the internals of a 28 | -- @[rust| |]@ block as part of your code layout without affecting 29 | -- the indentation of the generated Rust code 30 | -- 31 | -- * interpolating a multi-line snippet in a variable will indent 32 | -- every line to the same level 33 | -- 34 | -- This lets us define a multi-line method, include it in an impl 35 | -- statement and have all the indentation work out correctly: 36 | -- 37 | -- @ 38 | -- struct :: Rust -> Rust 39 | -- struct = [rust| 40 | -- #[derive(Debug)] 41 | -- struct Foo { 42 | -- field: i32, 43 | -- } 44 | -- 45 | -- impl Foo { 46 | -- $method 47 | -- } 48 | -- |] 49 | -- where method = [rust| 50 | -- pub fn foo() -> i32 { 51 | -- // ... 52 | -- } 53 | -- |] 54 | -- @ 55 | -- 56 | -- This produces the following Rust snippet: 57 | -- 58 | -- @ 59 | -- #[derive(Debug)] 60 | -- struct Foo { 61 | -- field: i32, 62 | -- } 63 | -- 64 | -- impl Foo { 65 | -- pub fn foo() -> i32 { 66 | -- // ... 67 | -- } 68 | -- } 69 | -- @ 70 | module Theta.Target.Rust.QuasiQuoter 71 | ( Rust (..) 72 | , rust 73 | , normalize 74 | ) 75 | where 76 | 77 | import Data.Text (Text) 78 | import qualified Data.Text as Text 79 | 80 | import GHC.Exts (IsString) 81 | 82 | import Language.Haskell.TH.Quote (QuasiQuoter (..)) 83 | 84 | import Text.Printf (PrintfArg) 85 | 86 | import Theta.Target.LanguageQuoter (Interpolable (..), quoter) 87 | 88 | -- | A valid Rust snippet (stored as text). 89 | -- 90 | -- This could either be standalone code or a fragment. 91 | newtype Rust = Rust { fromRust :: Text } 92 | deriving stock (Show, Eq) 93 | deriving newtype (IsString, Semigroup, Monoid, PrintfArg) 94 | 95 | -- | Heuristics to "normalize" Rust code for comparison. This doesn't 96 | -- use a Rust parser so it makes no guarantees; the goal is to be 97 | -- robust for testing as long as the test code follows roughly the 98 | -- same code formatting as the generation code. 99 | -- 100 | -- Current approach: 101 | -- 102 | -- 1. Trim leading/trailing whitespace on each line 103 | -- 2. Drop resulting empty lines 104 | normalize :: Rust -> Rust 105 | normalize = Rust . Text.unlines . normalizeLines . Text.lines . fromRust 106 | where normalizeLines = filter (not . Text.null) . map Text.strip 107 | 108 | instance Interpolable Rust where 109 | toText = fromRust 110 | fromText = Rust 111 | 112 | -- | The 'rust' quasiquoter generates 'Rust' snippets, allowing you to 113 | -- interpolate other Rust snippets from Haskell variables: 114 | -- 115 | -- @ 116 | -- function :: Rust 117 | -- function = [rust| 118 | -- fn foo($variableName: i32) -> i32 { 119 | -- $variableName + 1 120 | -- } 121 | -- |] 122 | -- where variableName :: Rust 123 | -- variableName = Rust "x" 124 | -- @ 125 | -- 126 | -- You can get a literal @$@ by typing two in a row: @$$@. 127 | -- 128 | -- The quasiquoter does straightforward string interpolation with a 129 | -- few rules to make managing indentation easier: 130 | -- 131 | -- * the first and last lines are ignored completely if they are all 132 | -- whitespace (including for applying the rest of these rules) 133 | -- 134 | -- * leading indentation is ignored—you can indent the internals of a 135 | -- @[rust| |]@ block as part of your code layout without affecting 136 | -- the indentation of the generated Rust code 137 | -- 138 | -- * interpolating a multi-line snippet in a variable will indent 139 | -- every line to the same level 140 | -- 141 | -- This lets us define a multi-line method, include it in an impl 142 | -- statement and have all the indentation work out correctly: 143 | -- 144 | -- @ 145 | -- struct :: Rust -> Rust 146 | -- struct = [rust| 147 | -- #[derive(Debug)] 148 | -- struct Foo { 149 | -- field: i32, 150 | -- } 151 | -- 152 | -- impl Foo { 153 | -- $method 154 | -- } 155 | -- |] 156 | -- where method = [rust| 157 | -- pub fn foo() -> i32 { 158 | -- // ... 159 | -- } 160 | -- |] 161 | -- @ 162 | -- 163 | -- This produces the following Rust snippet: 164 | -- 165 | -- @ 166 | -- #[derive(Debug)] 167 | -- struct Foo { 168 | -- field: i32, 169 | -- } 170 | -- 171 | -- impl Foo { 172 | -- pub fn foo() -> i32 { 173 | -- // ... 174 | -- } 175 | -- } 176 | -- @ 177 | rust :: QuasiQuoter 178 | rust = quoter ''Rust 179 | -------------------------------------------------------------------------------- /theta/src/Theta/Versions.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE NamedFieldPuns #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | 4 | -- | This module specifies which versions of the Theta protocol are 5 | -- supported by this version of the Haskell package (library and 6 | -- executables). 7 | -- 8 | -- Each release of the package can support a range of versions. Theta 9 | -- modules specify the versions needed to work with the module in the 10 | -- module metadata; any module whose versions are not supported by 11 | -- this release will fail with a graceful error message. 12 | module Theta.Versions where 13 | 14 | import Data.Text (Text) 15 | import qualified Data.Text as Text 16 | import qualified Data.Version as Cabal 17 | 18 | import Test.QuickCheck (Arbitrary (arbitrary)) 19 | import qualified Test.QuickCheck as QuickCheck 20 | 21 | import Theta.Metadata (Version) 22 | 23 | -- * Language Versions 24 | 25 | -- | A version range with an /inclusive/ lower bound and an 26 | -- /exclusive/ upper bound. 27 | -- 28 | -- @ 29 | -- Range { lower = "1.0.0", upper = "1.1.0" } 30 | -- @ 31 | -- 32 | -- represents 33 | -- 34 | -- @ 35 | -- version >= "1.0.0" && version < "1.1.0" 36 | -- @ 37 | data Range = Range 38 | { lower :: Version 39 | -- ^ The /inclusive/ lower bound. 40 | , upper :: Version 41 | -- ^ The /exclusive/ upper bound. 42 | , name :: Text 43 | -- ^ A human-readable description of what this version represents 44 | -- (ie "language-version" vs "avro-version"). 45 | } deriving (Show, Eq, Ord) 46 | 47 | instance Arbitrary Range where 48 | arbitrary = Range <$> arbitrary <*> arbitrary <*> name 49 | where name = QuickCheck.elements ["avro-version", "theta-version"] 50 | 51 | -- | Check whether a version is within the specified 'Range'. 52 | inRange :: Range -> Version -> Bool 53 | inRange Range { lower, upper } version = version >= lower && version < upper 54 | 55 | -- | Is the given version of the Theta language supported by this 56 | -- version of the package? 57 | -- 58 | -- Specified as @language-version@ in the header of every Theta 59 | -- module. 60 | theta :: Range 61 | theta = Range { name = "theta-version", lower = "1.0.0", upper = "1.2.0" } 62 | 63 | -- | Is the given version of the Theta Avro encoding supported by this 64 | -- version of the package? 65 | -- 66 | -- Specified as @avro-version@ in the header of every Theta module. 67 | avro :: Range 68 | avro = Range { name = "avro-version", lower = "1.0.0", upper = "1.2.0" } 69 | 70 | -- * Package Version 71 | 72 | -- Having the @theta@ library component depend on @Paths_theta@ caused 73 | -- unnecessary rebuilds with Stack (see GitHub issue[1]). To fix this, 74 | -- we hardcode the packageVersion in this module, then have a unit 75 | -- test that checks that it is up to date with the version in 76 | -- @Paths_theta@. 77 | -- 78 | -- [1]: https://github.com/target/theta-idl/issues/37 79 | 80 | -- | Which version of the Theta package this is. 81 | packageVersion :: Cabal.Version 82 | packageVersion = Cabal.makeVersion [1, 0, 0, 2] 83 | 84 | -- | Which version of the Theta package this is as 'Text'. 85 | packageVersion' :: Text 86 | packageVersion' = Text.pack $ Cabal.showVersion packageVersion 87 | -------------------------------------------------------------------------------- /theta/stack-shell.nix: -------------------------------------------------------------------------------- 1 | { ghc }: 2 | 3 | # On any system with Nix, you can build theta with Stack using: 4 | # 5 | # > stack --nix --nix-shell-file stack-shell.nix build 6 | 7 | let 8 | pkgs = import {}; 9 | in 10 | pkgs.haskell.lib.buildStackProject { 11 | inherit ghc; 12 | name = "theta"; 13 | 14 | buildInputs = [ pkgs.zlib ]; 15 | } 16 | -------------------------------------------------------------------------------- /theta/stack.yaml: -------------------------------------------------------------------------------- 1 | resolver: lts-19.2 2 | extra-deps: 3 | - avro-0.6.1.0 4 | - streamly-process-0.2.0 5 | -------------------------------------------------------------------------------- /theta/test/Test.hs: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | import Test.Tasty 4 | 5 | import qualified Test.Theta.Error as Error 6 | import qualified Test.Theta.Import as Import 7 | import qualified Test.Theta.LoadPath as LoadPath 8 | import qualified Test.Theta.Name as Name 9 | import qualified Test.Theta.Parser as Parser 10 | import qualified Test.Theta.Types as Types 11 | import qualified Test.Theta.Value as Value 12 | import qualified Test.Theta.Versions as Versions 13 | 14 | import qualified Test.Theta.Target.Avro.Process as Avro.Process 15 | import qualified Test.Theta.Target.Avro.Types as Avro.Types 16 | import qualified Test.Theta.Target.Avro.Values as Avro.Values 17 | 18 | import qualified Test.Theta.Target.Haskell as Haskell 19 | import qualified Test.Theta.Target.Haskell.Conversion as Haskell.Conversion 20 | 21 | import qualified Test.Theta.Target.Kotlin as Kotlin 22 | 23 | import qualified Test.Theta.Target.Python as Python 24 | import qualified Test.Theta.Target.Python.QuasiQuoter as Python.QuasiQuoter 25 | 26 | import qualified Test.Theta.Target.Rust as Rust 27 | 28 | tests :: TestTree 29 | tests = testGroup "Theta" 30 | [ Error.tests 31 | , Import.tests 32 | , LoadPath.tests 33 | , Name.tests 34 | , Parser.tests 35 | , Types.tests 36 | , Value.tests 37 | , Versions.tests 38 | 39 | , testGroup "Target" 40 | [ testGroup "Avro" 41 | [ Avro.Process.tests 42 | , Avro.Values.tests 43 | , Avro.Types.tests 44 | ] 45 | 46 | , Haskell.tests 47 | , Haskell.Conversion.tests 48 | 49 | , Kotlin.tests 50 | 51 | , Python.tests 52 | , Python.QuasiQuoter.tests 53 | 54 | , Rust.tests 55 | ] 56 | ] 57 | 58 | main :: IO () 59 | main = defaultMain tests 60 | -------------------------------------------------------------------------------- /theta/test/Test/Theta/Error.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DeriveGeneric #-} 2 | {-# LANGUAGE DuplicateRecordFields #-} 3 | {-# LANGUAGE OverloadedLists #-} 4 | {-# LANGUAGE OverloadedStrings #-} 5 | {-# LANGUAGE QuasiQuotes #-} 6 | {-# LANGUAGE TemplateHaskell #-} 7 | {-# LANGUAGE TypeApplications #-} 8 | module Test.Theta.Error where 9 | 10 | import Control.Monad.Except (runExceptT) 11 | 12 | import qualified Data.Algorithm.Diff as Diff 13 | import qualified Data.Set as Set 14 | 15 | import Test.Tasty 16 | import Test.Tasty.HUnit (testCase, (@?=)) 17 | 18 | import Theta.Test.Assertions ((?=)) 19 | 20 | import Theta.Metadata (Metadata (..)) 21 | import Theta.Pretty (pr) 22 | import Theta.Target.Haskell (loadModule) 23 | import Theta.Types (emptyModule) 24 | 25 | import Theta.Error 26 | import Theta.Import (getModule) 27 | 28 | 29 | loadModule "test/data/modules" "importing_foo" 30 | loadModule "test/data/modules" "recursive" 31 | loadModule "test/data/transitive" "d" 32 | 33 | tests :: TestTree 34 | tests = testGroup "Error" 35 | [ testGroup "prettyModuleError" 36 | [ testCase "suggest primitive type when language-version is too low" $ do 37 | prettyModuleError (exampleModule, UndefinedType "example.UUID") ?= 38 | [pr| 39 | Error in module ‘example’: 40 | The type ‘example.UUID’ is not defined. 41 | 42 | Suggestions: 43 | • UUID (primitive type): requires language-version ≥ 1.1.0 44 | |] 45 | 46 | , testCase "suggest imported types with exact name match" $ do 47 | prettyModuleError (theta'importing_foo, UndefinedType "importing_foo.Bar") ?= 48 | [pr| 49 | Error in module ‘importing_foo’: 50 | The type ‘importing_foo.Bar’ is not defined. 51 | 52 | Suggestions: 53 | • foo.Bar 54 | |] 55 | 56 | , testCase "local suggestion, namespace prefix optional" $ do 57 | prettyModuleError (theta'recursive, UndefinedType "foo.MutualA") ?= 58 | [pr| 59 | Error in module ‘recursive’: 60 | The type ‘foo.MutualA’ is not defined. 61 | 62 | Suggestions: 63 | • MutualA (recursive.MutualA) 64 | |] 65 | 66 | , testCase "note import for suggested type if needed" $ do 67 | prettyModuleError (theta'd, UndefinedType "b.B") ?= 68 | [pr| 69 | Error in module ‘d’: 70 | The type ‘b.B’ is not defined. 71 | 72 | Suggestions: 73 | • D (d.D) 74 | • b.B (import b) 75 | • a.A 76 | • c.C 77 | |] 78 | ] 79 | 80 | , testGroup "suggestions" 81 | [ testGroup "diffParts" 82 | [ testCase "equal" $ 83 | diffParts 2 "foo.bar.baz" "foo.bar.baz" @?= 84 | [Diff.Both ["foo", "bar", "baz"] ["foo", "bar", "baz"]] 85 | 86 | , testCase "within 2" $ 87 | diffParts 2 "foos.bar.baz" "foo.bares.bat" @?= 88 | [Diff.Both ["foos", "bar", "baz"] ["foo", "bares", "bat"]] 89 | 90 | , testCase "removed part" $ 91 | diffParts 2 "foo.bar.baz" "foo.bat" @?= 92 | [Diff.Both ["foo", "bar"] ["foo", "bat"], Diff.First ["baz"]] 93 | 94 | , testCase "added part" $ 95 | diffParts 2 "foo.bar" "foo.bat.baz" @?= 96 | [Diff.Both ["foo", "bar"] ["foo", "bat"], Diff.Second ["baz"]] 97 | 98 | , testCase "different part" $ 99 | diffParts 2 "foo.bar.baz" "foo.bats.bat" @?= 100 | [Diff.Both ["foo"] ["foo"], Diff.First ["bar"], Diff.Second ["bats"], Diff.Both ["baz"] ["bat"]] 101 | ] 102 | 103 | , testGroup "otherModuleSuggestions" 104 | [ testCase "one option" $ 105 | otherModuleSuggestions theta'importing_foo "importing_foo.Bar" @?= ["foo.Bar"] 106 | 107 | , testCase "multiple options" $ do 108 | importingModule <- runExceptT $ getModule "test/data/importing" "importing" 109 | Set.fromList (otherModuleSuggestions (unsafe importingModule) "foo.Importing") @?= 110 | [ "importing.Importing" 111 | , "direct_a.Importing" 112 | , "direct_b.Importing" 113 | , "indirect_a.Importing" 114 | ] 115 | ] 116 | 117 | , testGroup "similarSpellingSuggestions" 118 | [ testCase "added namespace part" $ 119 | similarSpellingSuggestions 4 theta'importing_foo "foo.bar.Bar" @?= ["foo.Bar"] 120 | 121 | , testCase "typo in namespace" $ 122 | similarSpellingSuggestions 4 theta'importing_foo "fobs.Bar" @?= ["foo.Bar"] 123 | 124 | , testCase "typo + added part" $ 125 | similarSpellingSuggestions 4 theta'importing_foo "fobs.bar.Bar" @?= ["foo.Bar"] 126 | 127 | , testCase "namespace too different" $ 128 | similarSpellingSuggestions 4 theta'importing_foo "frobles.Bar" @?= [] 129 | 130 | , testCase "base name too different" $ 131 | similarSpellingSuggestions 4 theta'importing_foo "foo.BargleBarg" @?= [] 132 | 133 | , testCase "base name too different + added namespace part" $ 134 | similarSpellingSuggestions 4 theta'importing_foo "foo.thingy.BargleBarg" @?= [] 135 | ] 136 | ] 137 | ] 138 | where exampleModule = emptyModule "example" (Metadata "1.0.0" "1.0.0" "example") 139 | -------------------------------------------------------------------------------- /theta/test/Test/Theta/LoadPath.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedLists #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | 4 | module Test.Theta.LoadPath where 5 | 6 | import qualified Data.Text.IO as Text 7 | 8 | import System.Directory (canonicalizePath, withCurrentDirectory) 9 | import System.FilePath (equalFilePath, ()) 10 | 11 | import Test.Tasty (TestTree, testGroup) 12 | import Test.Tasty.HUnit (assertBool, testCase, (@?=)) 13 | 14 | import Text.Printf (printf) 15 | 16 | import Theta.LoadPath 17 | 18 | import qualified Paths_theta as Paths 19 | 20 | tests :: TestTree 21 | tests = testGroup "LoadPath" 22 | [ testGroup "findInPath" 23 | [ testCase "relative paths" $ do 24 | dir <- Paths.getDataDir 25 | withCurrentDirectory dir $ do 26 | let loadPath = LoadPath [root1, root2] 27 | 28 | let pathA = dir "test" "data" "root-1" "a.theta" 29 | fileA <- Text.readFile pathA 30 | Just (fileA', pathA') <- findInPath loadPath "a.theta" 31 | assertEqualPath pathA' pathA 32 | fileA' @?= fileA 33 | 34 | let pathB = dir "test" "data" "root-2" "b.theta" 35 | fileB <- Text.readFile pathB 36 | Just (fileB', pathB') <- findInPath loadPath "b.theta" 37 | assertEqualPath pathB' pathB 38 | fileB' @?= fileB 39 | 40 | , testCase "absolute paths" $ do 41 | dir <- Paths.getDataDir 42 | let loadPath = LoadPath [dir root1, dir root2] 43 | 44 | let pathA = dir "test" "data" "root-1" "a.theta" 45 | fileA <- Text.readFile pathA 46 | Just (fileA', pathA') <- findInPath loadPath "a.theta" 47 | assertEqualPath pathA' pathA 48 | fileA' @?= fileA 49 | 50 | let pathB = dir "test" "data" "root-2" "b.theta" 51 | fileB <- Text.readFile pathB 52 | Just (fileB', pathB') <- findInPath loadPath "b.theta" 53 | assertEqualPath pathB' pathB 54 | fileB' @?= fileB 55 | 56 | -- What if we have a .. or . in the middle of a load path? 57 | -- 58 | -- This has caused problems in the past, for uncertain reasons. 59 | , testGroup ".. and ." 60 | [ testCase ".. in start" $ do 61 | dir <- Paths.getDataDir 62 | withCurrentDirectory (dir "test") $ do 63 | let loadPath = LoadPath [".." root1] 64 | 65 | let pathA = dir "test" "data" "root-1" "a.theta" 66 | fileA <- Text.readFile pathA 67 | Just (fileA', pathA') <- findInPath loadPath "a.theta" 68 | assertEqualPath pathA' pathA 69 | fileA' @?= fileA 70 | 71 | , testCase ".. in middle" $ do 72 | dir <- Paths.getDataDir 73 | let loadPath = LoadPath [dir "test" ".." root1] 74 | 75 | let pathA = dir "test" "data" "root-1" "a.theta" 76 | fileA <- Text.readFile pathA 77 | Just (fileA', pathA') <- findInPath loadPath "a.theta" 78 | assertEqualPath pathA' pathA 79 | fileA' @?= fileA 80 | 81 | , testCase ". in start" $ do 82 | dir <- Paths.getDataDir 83 | let loadPath = LoadPath ["." dir root1] 84 | 85 | let pathA = dir "test" "data" "root-1" "a.theta" 86 | fileA <- Text.readFile pathA 87 | Just (fileA', pathA') <- findInPath loadPath "a.theta" 88 | assertEqualPath pathA' pathA 89 | fileA' @?= fileA 90 | 91 | , testCase ". in middle" $ do 92 | dir <- Paths.getDataDir 93 | let loadPath = LoadPath [dir "." root1] 94 | 95 | let pathA = dir "test" "data" "root-1" "a.theta" 96 | fileA <- Text.readFile pathA 97 | Just (fileA', pathA') <- findInPath loadPath "a.theta" 98 | assertEqualPath pathA' pathA 99 | fileA' @?= fileA 100 | ] 101 | 102 | , testCase "missing files" $ do 103 | dir <- Paths.getDataDir 104 | let loadPath = LoadPath [dir root1, dir root2] 105 | 106 | a <- findInPath loadPath ("blarg" "foo.theta") 107 | a @?= Nothing 108 | 109 | b <- findInPath "nope" "a.theta" 110 | b @?= Nothing 111 | 112 | c <- findInPath (LoadPath [dir root1]) "b.theta" 113 | c @?= Nothing 114 | ] 115 | 116 | , testCase "moduleNames" $ do 117 | dir <- Paths.getDataDir 118 | let loadPath = LoadPath [dir root1, dir root2, dir root3] 119 | names <- moduleNames loadPath 120 | names @?= [ ("a", dir root1 "a.theta") 121 | , ("b", dir root2 "b.theta") 122 | , ("com.example", dir root3 "com" "example.theta") 123 | , ("com.example.nested", dir root3 "com" "example" "nested.theta") 124 | , ("com.example.nested_2", dir root3 "com" "example" "nested_2.theta") 125 | , ("com.example.nested.deeply", dir root3 "com" "example" "nested" "deeply.theta") 126 | ] 127 | ] 128 | where root1 = "test" "data" "root-1" 129 | root2 = "test" "data" "root-2" 130 | root3 = "test" "data" "root-3" 131 | 132 | assertEqualPath got expected = do 133 | got' <- canonicalizePath got 134 | expected' <- canonicalizePath expected 135 | assertBool (printf "expected: %s\n but got: %s" expected' got') $ 136 | equalFilePath got' expected' 137 | -------------------------------------------------------------------------------- /theta/test/Test/Theta/Name.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | {-# LANGUAGE ScopedTypeVariables #-} 3 | 4 | module Test.Theta.Name where 5 | 6 | import Data.Tree (Tree (..)) 7 | 8 | import GHC.Exts (fromList, toList) 9 | 10 | import qualified Theta.Name as Name 11 | 12 | import Test.Tasty 13 | import Test.Tasty.HUnit 14 | import Test.Tasty.QuickCheck 15 | 16 | tests :: TestTree 17 | tests = testGroup "Name" 18 | [ test_Name 19 | , test_ModuleName 20 | ] 21 | 22 | test_Name :: TestTree 23 | test_Name = testGroup "Name" 24 | [ testCase "parts" $ do 25 | Name.parts "com.Foo" @?= ["com", "Foo"] 26 | Name.parts "com.example.Foo" @?= ["com", "example", "Foo"] 27 | 28 | , testCase "render" $ do 29 | Name.render "com.Foo" @?= "com.Foo" 30 | Name.render "com.example.Foo" @?= "com.example.Foo" 31 | 32 | , testCase "parse & parse'" $ do 33 | Name.parse' "" @?= Left Name.Invalid 34 | Name.parse "" @?= Nothing 35 | 36 | Name.parse' "Foo" @?= Left Name.Unqualified 37 | Name.parse "Foo" @?= Nothing 38 | 39 | Name.parse' "12" @?= Left Name.Invalid 40 | Name.parse "12" @?= Nothing 41 | 42 | Name.parse' "com.Foo" @?= Right (Name.Name "com" "Foo") 43 | Name.parse "com.Foo" @?= Just (Name.Name "com" "Foo") 44 | 45 | Name.parse' "com.example.Foo" @?= 46 | Right (Name.Name (Name.ModuleName ["com"] "example") "Foo") 47 | Name.parse "com.example.Foo" @?= 48 | Just (Name.Name (Name.ModuleName ["com"] "example") "Foo") 49 | 50 | , testProperty "render ⇔ parse" $ \ name -> 51 | case Name.parse' (Name.render name) of 52 | Left Name.Unqualified -> error "Rendered name had no namespace." 53 | Left Name.Invalid -> error "Rendered name did not parse." 54 | Right parsed -> parsed == name 55 | ] 56 | 57 | test_ModuleName :: TestTree 58 | test_ModuleName = testGroup "ModuleName" 59 | [ testCase "renderModuleName" $ do 60 | let render = Name.renderModuleName . Name.moduleName 61 | render "com.Foo" @?= "com" 62 | render "com.example.Foo" @?= "com.example" 63 | 64 | , testCase "moduleRoot" $ do 65 | Name.moduleRoot "foo" @?= "foo" 66 | Name.moduleRoot "example.foo" @?= "example" 67 | Name.moduleRoot "com.example.foo" @?= "com" 68 | 69 | , testCase "moduleHierarchy" $ do 70 | let got = Name.moduleHierarchy 71 | [ "com.example.foo" 72 | , "com.example.bar" 73 | , "com.example2.foo" 74 | , "com.bar" 75 | , "com.example2.bar" 76 | , "org.example.foo" 77 | ] 78 | expected = 79 | [ Node "com" 80 | [ Node "bar" [] 81 | , Node "example" [ Node "bar" [], Node "foo" [] ] 82 | , Node "example2" [ Node "bar" [], Node "foo" [] ] 83 | ] 84 | , Node "org" 85 | [ Node "example" [ Node "foo" [] ] ] 86 | ] 87 | got @?= expected 88 | 89 | , testProperty "renderModuleName ⇔ parseModuleName" $ \ moduleName -> 90 | Name.parseModuleName (Name.renderModuleName moduleName) == moduleName 91 | 92 | , testProperty "toList ⇔ fromList" $ \ (moduleName :: Name.ModuleName) -> 93 | fromList (toList moduleName) == moduleName 94 | 95 | , testProperty "toList ⇒ moduleRoot" $ \ moduleName -> 96 | head (toList moduleName) == Name.moduleRoot moduleName 97 | ] 98 | -------------------------------------------------------------------------------- /theta/test/Test/Theta/Target/Avro/Process.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DataKinds #-} 2 | {-# LANGUAGE DeriveGeneric #-} 3 | {-# LANGUAGE DuplicateRecordFields #-} 4 | {-# LANGUAGE LambdaCase #-} 5 | {-# LANGUAGE OverloadedStrings #-} 6 | {-# LANGUAGE TemplateHaskell #-} 7 | {-# LANGUAGE TypeApplications #-} 8 | module Test.Theta.Target.Avro.Process where 9 | 10 | 11 | import Control.Monad (forM_) 12 | import Control.Monad.Except (MonadIO (liftIO), runExceptT) 13 | 14 | import qualified Data.Text as Text 15 | 16 | import Test.QuickCheck.Monadic (monadicIO) 17 | import Test.Tasty (TestTree, testGroup) 18 | import Test.Tasty.QuickCheck (forAll, listOf, testProperty) 19 | 20 | import qualified Streamly.Prelude as Streamly 21 | 22 | import Theta.Test.Assertions (assertDiff) 23 | 24 | import Theta.Pretty (Pretty (pretty)) 25 | import Theta.Target.Avro.Process (run, stream) 26 | import Theta.Target.Haskell (loadModule) 27 | import Theta.Target.Haskell.Conversion (genTheta) 28 | 29 | 30 | loadModule "test/data/modules" "primitives" 31 | 32 | tests :: TestTree 33 | tests = testGroup "Process" 34 | [ testGroup "run" 35 | [ testProperty "cat Primitives" $ forAll primitives $ \ input -> handle $ do 36 | output <- run "cat" [] input 37 | assertDiff output input 38 | ] 39 | 40 | , testGroup "stream" 41 | [ testProperty "cat Primitives" $ forAll (listOf primitives) $ \ inputs -> monadicIO $ do 42 | outputs <- liftIO $ Streamly.toList $ stream "cat" [] $ Streamly.fromList inputs 43 | forM_ (outputs `zip` inputs) $ \ (output, input) -> 44 | assertDiff output input 45 | ] 46 | ] 47 | where primitives = genTheta @Primitives 48 | 49 | handle action = monadicIO $ runExceptT action >>= \case 50 | Left err -> fail $ Text.unpack $ pretty err 51 | Right res -> pure res 52 | -------------------------------------------------------------------------------- /theta/test/Test/Theta/Target/Haskell/Conversion.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE FlexibleContexts #-} 2 | {-# LANGUAGE ScopedTypeVariables #-} 3 | {-# LANGUAGE TypeApplications #-} 4 | {-# LANGUAGE ViewPatterns #-} 5 | 6 | -- | Test that the logic that converts directly between Haskell types 7 | -- and Avro matches the logic that converts between Haskell, 8 | -- @Theta.Value@ and Avro. 9 | module Test.Theta.Target.Haskell.Conversion where 10 | 11 | import Control.Monad.State (evalStateT) 12 | 13 | import qualified Data.Avro as Avro 14 | import qualified Data.Binary.Get as Get 15 | import qualified Data.ByteString.Builder as Builder 16 | import qualified Data.ByteString.Lazy as LBS 17 | import Data.HashMap.Strict (HashMap) 18 | import qualified Data.HashMap.Strict as HashMap 19 | import Data.Int (Int32, Int64) 20 | import qualified Data.Set as Set 21 | import Data.Text (Text) 22 | import qualified Data.Text as Text 23 | 24 | import qualified Theta.Metadata as Metadata 25 | import qualified Theta.Pretty as Theta 26 | import qualified Theta.Types as Theta 27 | 28 | import qualified Theta.Target.Avro.Types as Avro.Types 29 | import Theta.Target.Avro.Values (fromDay, fromUTCTime, toDay, 30 | toUTCTime) 31 | 32 | import Theta.Target.Haskell.Conversion 33 | import qualified Theta.Target.Haskell.HasTheta as HasTheta 34 | 35 | import Test.Tasty 36 | import Test.Tasty.QuickCheck 37 | 38 | tests :: TestTree 39 | tests = testGroup "Haskell.Conversion" 40 | [ testGroup "avroEncoding = encodeAvro ∘ toAvro ∘ toTheta" 41 | [ testGroup "primitive types" 42 | [ testProperty "Bool" $ checkAvroEncoding @Bool 43 | , testProperty "Bytes" $ checkAvroEncoding . LBS.pack 44 | , testProperty "Int" $ checkAvroEncoding @Int32 45 | , testProperty "Long" $ checkAvroEncoding @Int64 46 | , testProperty "Float" $ checkAvroEncoding @Float 47 | , testProperty "Double" $ checkAvroEncoding @Double 48 | , testProperty "String" $ checkAvroEncoding . Text.pack 49 | , testProperty "Date" $ checkAvroEncoding . toDay 50 | , testProperty "Datetime" $ checkAvroEncoding . toUTCTime 51 | ] 52 | 53 | , testGroup "containers" 54 | [ testProperty "Array" $ checkAvroEncoding @[Int32] 55 | , testProperty "Map" $ checkAvroEncoding @(HashMap Text Int32) . toMap 56 | , testProperty "Optional" $ checkAvroEncoding @(Maybe Int32) 57 | ] 58 | ] 59 | 60 | , testGroup "avroDecoding = fromTheta ∘ fromAvro ∘ decodeAvro" 61 | [ testGroup "primitive types" 62 | [ testProperty "Bool" $ checkAvroDecoding @Bool 63 | , testProperty "Bytes" $ checkAvroDecoding . LBS.pack 64 | , testProperty "Int" $ checkAvroDecoding @Int32 65 | , testProperty "Long" $ checkAvroDecoding @Int64 66 | , testProperty "Float" $ checkAvroDecoding @Float 67 | , testProperty "Double" $ checkAvroDecoding @Double 68 | , testProperty "String" $ checkAvroDecoding . Text.pack 69 | 70 | , testProperty "Date" $ \ (toDay -> day) -> 71 | let encoded = Builder.toLazyByteString $ avroEncoding (fromDay day) 72 | decoded = Get.runGetOrFail avroDecoding encoded 73 | in case decoded of 74 | Left (_, _, err) -> error err 75 | Right (_, _, res) -> res == day 76 | 77 | , testProperty "Datetime" $ \ (toUTCTime -> datetime) -> 78 | let encoded = Builder.toLazyByteString $ avroEncoding (fromUTCTime datetime) 79 | decoded = Get.runGetOrFail avroDecoding encoded 80 | in case decoded of 81 | Left (_, _, err) -> error err 82 | Right (_, _, res) -> res == datetime 83 | ] 84 | 85 | , testGroup "containers" 86 | [ testProperty "Array" $ checkAvroDecoding @[Int32] 87 | , testProperty "Map" $ checkAvroDecoding @(HashMap Text Int32) . toMap 88 | , testProperty "Optional" $ checkAvroDecoding @(Maybe Int32) 89 | ] 90 | ] 91 | ] 92 | where toMap = HashMap.fromList . map (\ (k, v) -> (Text.pack k, v)) 93 | 94 | -- * Functions for building properties 95 | 96 | checkAvroEncoding :: forall a. (ToTheta a, Avro.HasAvroSchema a, Avro.ToAvro a) => a -> Bool 97 | checkAvroEncoding a = Avro.encodeValue a == Builder.toLazyByteString (avroEncoding a) 98 | 99 | checkAvroDecoding :: forall a. (Eq a, FromTheta a, ToTheta a, Avro.FromAvro a) => a -> Bool 100 | checkAvroDecoding a = decodedAvro == decoded 101 | where encoded = Builder.toLazyByteString (avroEncoding a) 102 | 103 | decoded = case Get.runGetOrFail @a avroDecoding encoded of 104 | Left (_, _, err) -> error err 105 | Right (_, _, res) -> res 106 | 107 | decodedAvro = case Avro.decodeValueWithSchema schema encoded of 108 | Left err -> error err 109 | Right res -> res 110 | schema = case toSchema $ HasTheta.theta @a of 111 | Left err -> error $ Text.unpack $ Theta.pretty err 112 | Right res -> Avro.readSchemaFromSchema res 113 | 114 | toSchema type_ = evalStateT (Avro.Types.typeToAvro avroVersion type_) Set.empty 115 | where avroVersion = Metadata.avroVersion $ Theta.metadata $ Theta.module_ type_ 116 | -------------------------------------------------------------------------------- /theta/test/Test/Theta/Target/Python/QuasiQuoter.hs: -------------------------------------------------------------------------------- 1 | -- NOTE: This file *intentionally* doesn't use OverloadedStrings to 2 | -- ensure the quasiquoter works without the extension enabled (which 3 | -- was a problem with an initial version of the code) 4 | {-# LANGUAGE QuasiQuotes #-} 5 | {-# LANGUAGE TypeApplications #-} 6 | 7 | -- | Tests to check that the Python quasiquoter works as 8 | -- advertised. See the documentation in the 9 | -- 'Theta.Target.Python.QuasiQuoter' module for details. 10 | module Test.Theta.Target.Python.QuasiQuoter where 11 | 12 | import qualified Data.Text as Text 13 | 14 | import Theta.Target.Python.QuasiQuoter 15 | 16 | import Test.Tasty 17 | import Test.Tasty.HUnit 18 | 19 | tests :: TestTree 20 | tests = testGroup "Python.QuasiQuoter" 21 | [ test_literal 22 | , test_indentation 23 | , test_interpolation 24 | ] 25 | 26 | test_literal :: TestTree 27 | test_literal = testCase "literal text" $ do 28 | [python| foo + bar * baz |] @?= Python (Text.pack "foo + bar * baz ") 29 | [python| $$foo + __bar * $$$$$$ |] @?= Python (Text.pack "$foo + __bar * $$$ ") 30 | [python| 31 | foo + bar * baz 32 | |] @?= Python (Text.pack "foo + bar * baz") 33 | 34 | -- note trailing whitespace after ‘baz’: 35 | [python| 36 | foo + bar * baz 37 | |] @?= Python (Text.pack "foo + bar * baz ") 38 | 39 | 40 | test_indentation :: TestTree 41 | test_indentation = testCase "indentation" $ do 42 | let class_ = [python| 43 | class Foo: 44 | def bar(x): 45 | x += 10 46 | print(x) 47 | 48 | def foo(y): 49 | y *= 10 50 | return y 51 | |] 52 | expected = "class Foo:\n\ 53 | \ def bar(x):\n\ 54 | \ x += 10\n\ 55 | \ print(x)\n\ 56 | \\n\ 57 | \ def foo(y):\n\ 58 | \ y *= 10\n\ 59 | \ return y" 60 | class_ @?= Python (Text.pack expected) 61 | 62 | test_interpolation :: TestTree 63 | test_interpolation = testGroup "interpolation" 64 | [ testCase "basic interpolation" $ do 65 | let x = [python|x|] 66 | y = [python|y|] 67 | x_ = [python|x|] 68 | 69 | [python| $x |] @?= Python (Text.pack "x ") 70 | [python| $x_ |] @?= Python (Text.pack "x ") 71 | [python| $x$x |] @?= Python (Text.pack "xx ") 72 | [python| $x$y |] @?= Python (Text.pack "xy ") 73 | [python| $x$$$y |] @?= Python (Text.pack "x$y ") 74 | [python| $x$$a |] @?= Python (Text.pack "x$a ") 75 | 76 | , testCase "indented single-line interpolation" $ do 77 | let body = Python (Text.pack "return x") 78 | function = [python| 79 | def foo(x): 80 | $body 81 | |] 82 | expected = "def foo(x):\n\ 83 | \ return x" 84 | function @?= Python (Text.pack expected) 85 | 86 | , testCase "indented multi-line interpolation" $ do 87 | let arg = Python (Text.pack "x") 88 | method = [python| 89 | def foo($arg): 90 | return $arg + 1 91 | |] 92 | class_ = [python| 93 | class Foo: 94 | $method 95 | |] 96 | expected = "class Foo:\n\ 97 | \ def foo(x):\n\ 98 | \ return x + 1" 99 | class_ @?= Python (Text.pack expected) 100 | 101 | , testCase "indented multi-line interpolation with text following" $ do 102 | let lines = [python| 103 | print('abc '\ 104 | ' def' 105 | |] 106 | context = [python| 107 | def foo(): 108 | $lines) 109 | |] 110 | expected = "def foo():\n\ 111 | \ print('abc '\\\n\ 112 | \ ' def')" 113 | context @?= Python (Text.pack expected) 114 | 115 | , testCase "indented multi-line interpolation with blank lines" $ do 116 | let methods = [python| 117 | def foo(x): 118 | return x 119 | 120 | def bar(x): 121 | return x 122 | |] 123 | class_ = [python| 124 | class Foo: 125 | $methods 126 | |] 127 | expected = "class Foo:\n\ 128 | \ def foo(x):\n\ 129 | \ return x\n\ 130 | \\n\ 131 | \ def bar(x):\n\ 132 | \ return x" 133 | class_ @?= Python (Text.pack expected) 134 | ] 135 | -------------------------------------------------------------------------------- /theta/test/Test/Theta/Value.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DataKinds #-} 2 | {-# LANGUAGE DeriveGeneric #-} 3 | {-# LANGUAGE DuplicateRecordFields #-} 4 | {-# LANGUAGE OverloadedLists #-} 5 | {-# LANGUAGE OverloadedStrings #-} 6 | {-# LANGUAGE ParallelListComp #-} 7 | {-# LANGUAGE TemplateHaskell #-} 8 | {-# LANGUAGE TypeApplications #-} 9 | module Test.Theta.Value where 10 | 11 | import Prelude hiding (map) 12 | 13 | import qualified Data.HashMap.Strict as HashMap 14 | 15 | import Test.Tasty (TestTree, testGroup) 16 | import Test.Tasty.QuickCheck (Gen, getSize, resize, 17 | testProperty) 18 | 19 | import Theta.Target.Haskell (loadModule) 20 | import Theta.Target.Haskell.HasTheta (HasTheta (theta)) 21 | 22 | import Theta.Types (primitiveTypes) 23 | import qualified Theta.Types as Theta 24 | import Theta.Value (BaseValue (Map, Variant), 25 | Value (Value, type_), 26 | checkValue, genValue, genValue') 27 | 28 | loadModule "test/data/modules" "primitives" 29 | loadModule "test/data/modules" "enums" 30 | loadModule "test/data/modules" "recursive" 31 | 32 | tests :: TestTree 33 | tests = testGroup "Value" 34 | [ testProperty "primitive types" $ do 35 | values <- mapM genValue primitiveTypes 36 | pure $ all checkValue values 37 | 38 | , testProperty "Fixed(10)" $ do 39 | value <- genValue (Theta.fixed' 10) 40 | pure $ checkValue value && type_ value == Theta.fixed' 10 41 | 42 | , testProperty "primitives.Primitives" $ do 43 | value <- genValue (theta @Primitives) 44 | pure $ checkValue value && type_ value == theta @Primitives 45 | 46 | , testProperty "primitives.Containers" $ do 47 | value <- genValue (theta @Containers) 48 | pure $ checkValue value && type_ value == theta @Containers 49 | 50 | , testProperty "enums.SimpleEnum" $ do 51 | value <- genValue (theta @SimpleEnum) 52 | pure $ checkValue value && type_ value == theta @SimpleEnum 53 | 54 | , testProperty "enums.TrickyEnum" $ do 55 | value <- genValue (theta @TrickyEnum) 56 | pure $ checkValue value && type_ value == theta @TrickyEnum 57 | 58 | , testProperty "recursive.Recursive" $ do 59 | value <- genRecursive 60 | pure $ checkValue value && type_ value == theta @Recursive 61 | ] 62 | 63 | -- generate recursive.Recursive values with overrides to make sure 64 | -- the generated values don't get too big—a bit hacky, but seems to 65 | -- work well enough 66 | genRecursive :: Gen Value 67 | genRecursive = genValue' [("recursive.Recursive", recursive)] (theta @Recursive) 68 | where recursive = do 69 | size <- getSize 70 | if size <= 10 then base else recurse size 71 | 72 | base = pure $ case_ "recursive.Nil" [] 73 | recurse size = do 74 | contents <- genValue Theta.int' 75 | boxed <- resize size genRecursive 76 | let unboxed = Value (Theta.map' (theta @Recursive)) $ Map HashMap.empty 77 | pure $ case_ "recursive.Recurse" [contents, boxed, unboxed] 78 | 79 | case_ name values = Value (theta @Recursive) $ Variant name values 80 | -------------------------------------------------------------------------------- /theta/test/Test/Theta/Versions.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | 3 | module Test.Theta.Versions where 4 | 5 | import Theta.Versions 6 | 7 | import Test.Tasty (TestTree, testGroup) 8 | import Test.Tasty.HUnit (testCase, (@?=)) 9 | import Test.Tasty.QuickCheck (testProperty) 10 | 11 | import Paths_theta (version) 12 | 13 | tests :: TestTree 14 | tests = testGroup "Versions" 15 | [ test_inRange 16 | , test_packageVersion 17 | ] 18 | 19 | test_inRange :: TestTree 20 | test_inRange = testProperty "inRange" $ \ version range -> 21 | inRange range version == (version >= lower range && -- inclusive inner 22 | version < upper range) -- exclusive outer 23 | 24 | test_packageVersion :: TestTree 25 | test_packageVersion = testCase "packageVersion" $ do 26 | packageVersion @?= Paths_theta.version 27 | -------------------------------------------------------------------------------- /theta/test/data/importing/direct_a.theta: -------------------------------------------------------------------------------- 1 | language-version: 1.0.0 2 | avro-version: 1.0.0 3 | --- 4 | 5 | import indirect_a 6 | 7 | type Importing = indirect_a.Importing 8 | -------------------------------------------------------------------------------- /theta/test/data/importing/direct_b.theta: -------------------------------------------------------------------------------- 1 | language-version: 1.0.0 2 | avro-version: 1.0.0 3 | --- 4 | 5 | type Importing = String 6 | -------------------------------------------------------------------------------- /theta/test/data/importing/importing.theta: -------------------------------------------------------------------------------- 1 | language-version: 1.0.0 2 | avro-version: 1.0.0 3 | --- 4 | 5 | import direct_a 6 | 7 | import direct_b 8 | 9 | /// We define a type with this name in this module *and* every module 10 | /// it imports (directly or transitively) 11 | /// 12 | /// This lets us test code (eg suggestions) that should work for 13 | /// multiple candidates 14 | /// 15 | /// Note that this currently messes up Haskell code generation, so we 16 | /// can't use loadModule for this. 17 | type Importing = Int 18 | -------------------------------------------------------------------------------- /theta/test/data/importing/indirect_a.theta: -------------------------------------------------------------------------------- 1 | language-version: 1.1.0 2 | avro-version: 1.0.0 3 | --- 4 | 5 | type Importing = UUID 6 | -------------------------------------------------------------------------------- /theta/test/data/modules/documentation.theta: -------------------------------------------------------------------------------- 1 | language-version: 1.0.0 2 | avro-version: 1.0.0 3 | --- 4 | 5 | /** User metadata. */ 6 | type User = { 7 | /// An opaque identifier to distinguish users with identical 8 | /// usernames. 9 | user_id : Long, 10 | 11 | /// Base username. Used for display. 12 | /// 13 | /// Only username + user_id have to be unique. 14 | username : String, 15 | 16 | /** The last time the user logged into our system. This date is 17 | * only the *start* of a session—we ignore the session duration. 18 | */ 19 | last_login : Datetime, 20 | 21 | permissions : [Permission] 22 | } 23 | 24 | /// Some kind of opaque resource identifier. 25 | type ResourceId = Long 26 | 27 | /// Security capabilities user accounts can have. 28 | type Permission = 29 | /** Read access to the given resource. 30 | 31 | */ 32 | Read { 33 | /** Any resource may have this permission. 34 | */ 35 | resource : ResourceId 36 | } 37 | | /** Write access to the given resource. 38 | * 39 | * Implies read access as well. 40 | */ 41 | Write { 42 | /// Only writable resources should have this permission. 43 | resource : ResourceId 44 | } 45 | | /// Is the user allowed to log in at all? 46 | Login 47 | 48 | /// Check that we can add documentation to variants with a single 49 | /// case. 50 | type SingleCase = /// This is an edge case in our parser... 51 | OneCase {} 52 | -------------------------------------------------------------------------------- /theta/test/data/modules/enums.theta: -------------------------------------------------------------------------------- 1 | language-version: 1.1.0 2 | avro-version: 1.0.0 3 | --- 4 | 5 | /// A simple documented enum 6 | enum SimpleEnum = Simple 7 | 8 | /// This enum has weird symbol names to stress the disambiguation 9 | /// logic in our Haskell/Rust/etc code generation. 10 | enum TrickyEnum = Sym | sym | _Sym 11 | -------------------------------------------------------------------------------- /theta/test/data/modules/fixed.theta: -------------------------------------------------------------------------------- 1 | language-version: 1.1.0 2 | avro-version: 1.0.0 3 | --- 4 | 5 | /// A record with two fields with the same Fixed(·) type. 6 | /// 7 | /// Multiple occurences of the same Fixed(·) type need special 8 | /// treatment in Avro since fixed types need to have names. 9 | type MultiFixed = { 10 | fixed_a : Fixed(3), // should define a new type named theta.fixed.Fixed_3 11 | fixed_b : Fixed(3), // should be a reference to theta.fixed.Fixed_3 12 | fixed_4: Fixed(4), // should define a new type 13 | nested_fixed : NestedFixed 14 | } 15 | 16 | type NestedFixed = { 17 | // this one should also just be a reference to theta.fixed.Fixed_3 18 | fixed_c : Fixed(3) 19 | } 20 | -------------------------------------------------------------------------------- /theta/test/data/modules/foo.theta: -------------------------------------------------------------------------------- 1 | language-version: 1.0.0 2 | avro-version: 1.0.0 3 | --- 4 | 5 | // This is a minimal module for constructing small test cases. 6 | 7 | type Bar = { 8 | a : Int 9 | } 10 | -------------------------------------------------------------------------------- /theta/test/data/modules/importing_foo.theta: -------------------------------------------------------------------------------- 1 | language-version: 1.0.0 2 | avro-version: 1.0.0 3 | --- 4 | 5 | // this module imports foo.theta to let use test for issues involving 6 | // imports 7 | 8 | import foo 9 | 10 | -------------------------------------------------------------------------------- /theta/test/data/modules/logical_dates.theta: -------------------------------------------------------------------------------- 1 | language-version: 1.0.0 2 | avro-version: 1.1.0 3 | --- 4 | 5 | import primitives 6 | 7 | // We started using Avro's logical "date" and "timestamp-micros" types in 8 | // avro-version 1.2.0 9 | // 10 | // This doesn't change the semantics of the 11 | // encoding—the binary produced is identical—but it changes the generated 12 | // schemas. 13 | type Dates = { 14 | logical_date: Date, 15 | logical_datetime: Datetime, 16 | 17 | // The date and datetime fields in Primtives should *not* use 18 | // logical types since that module has avro-version set to 1.0.0 19 | primitives: primitives.Primitives 20 | } 21 | -------------------------------------------------------------------------------- /theta/test/data/modules/named_types.theta: -------------------------------------------------------------------------------- 1 | language-version: 1.0.0 2 | avro-version: 1.0.0 3 | --- 4 | 5 | // A module containing a record, a variant and a newtype (that uses a 6 | // reference). 7 | 8 | type Record = { foo : Int } 9 | 10 | type Variant = Foo { foo : Int } 11 | | Bar { bar : String } 12 | 13 | type Newtype = Record 14 | 15 | type Newtype2 = Record 16 | 17 | alias Alias1 = Record 18 | -------------------------------------------------------------------------------- /theta/test/data/modules/nested_newtypes.theta: -------------------------------------------------------------------------------- 1 | language-version: 1.0.0 2 | avro-version: 1.0.0 3 | --- 4 | 5 | // A module with nested references, newtypes and aliases. 6 | 7 | type Newtype_0 = Int 8 | 9 | type Newtype_1 = Newtype_0 10 | 11 | alias Alias = Newtype_1 12 | -------------------------------------------------------------------------------- /theta/test/data/modules/newtype.theta: -------------------------------------------------------------------------------- 1 | language-version: 1.0.0 2 | avro-version: 1.0.0 3 | --- 4 | 5 | type NewtypeRecord = { foo : Newtype } 6 | 7 | type Newtype = Int 8 | -------------------------------------------------------------------------------- /theta/test/data/modules/primitives.theta: -------------------------------------------------------------------------------- 1 | language-version: 1.1.0 2 | avro-version: 1.0.0 3 | --- 4 | 5 | // A record that has a field for every primitive type supported in 6 | // Theta. 7 | type Primitives = { 8 | bool : Bool, 9 | bytes : Bytes, 10 | int : Int, 11 | long : Long, 12 | float : Float, 13 | double : Double, 14 | string : String, 15 | date : Date, 16 | datetime : Datetime, 17 | uuid : UUID, 18 | time : Time, 19 | local_datetime : LocalDatetime, 20 | fixed_3 : Fixed(3) 21 | } 22 | 23 | type Containers = { 24 | array : [Bool], 25 | map : {Bool}, 26 | optional : Bool?, 27 | nested : {[Bool?]} 28 | } 29 | -------------------------------------------------------------------------------- /theta/test/data/modules/recursive.theta: -------------------------------------------------------------------------------- 1 | language-version: 1.0.0 2 | avro-version: 1.0.0 3 | --- 4 | 5 | // A module with a recursive type and two mutually recursive types, 6 | // for testing whether targets handle recusrive types correctly. 7 | 8 | type Recursive = Nil {} 9 | | Recurse { contents: Int 10 | , boxed: Recursive 11 | , unboxed: {[Recursive]} 12 | } 13 | 14 | 15 | type MutualA = { mutual: MutualB } 16 | 17 | type MutualB = { mutual: Wrapper } 18 | 19 | type AMutual = MutualA 20 | 21 | alias Wrapper = AMutual? 22 | -------------------------------------------------------------------------------- /theta/test/data/modules/rust.theta: -------------------------------------------------------------------------------- 1 | language-version: 1.1.0 2 | avro-version: 1.0.0 3 | --- 4 | 5 | // A module with a few Rust-specific edge-cases 6 | 7 | type Foo = { 8 | input: Int, 9 | nested: [A] 10 | } 11 | 12 | /// Fixed(n) types require special handling in the Rust codegen 13 | type FixedFields = { 14 | f_1: Fixed(1), 15 | f_10: Fixed(10) 16 | } 17 | 18 | alias A = [Long] 19 | 20 | 21 | // A recursive type, which means the recursive field needs to be boxed 22 | // in Rust: 23 | type RecursiveRecord = { recurse: RecursiveRecord? } 24 | 25 | // Same thing but with a variant rather than a struct: 26 | type RecursiveVariant = A { recurse: RecursiveVariant? } 27 | | B { recurse: RecursiveVariant? } 28 | 29 | // Recursive variant *without* an optional type 30 | type RecursiveList = RCons { x : Int, rest : RecursiveList } 31 | | RNil 32 | 33 | // Including RecursiveList in another type caused an infinite loop at 34 | // one point :/ 35 | type Record = { a : Int, b : RecursiveList } 36 | 37 | type RecursiveNewtype = RecursiveList 38 | -------------------------------------------------------------------------------- /theta/test/data/modules/unsupported_avro_version.theta: -------------------------------------------------------------------------------- 1 | language-version: 1.0.0 2 | avro-version: 999.0.0 3 | --- 4 | 5 | // this invalid definition should not even be parsed 6 | type Blarg = 7 | -------------------------------------------------------------------------------- /theta/test/data/modules/unsupported_theta_version.theta: -------------------------------------------------------------------------------- 1 | avro-version: 1.0.0 2 | language-version: 999.0.0 3 | --- 4 | 5 | // this invalid definition should not even be parsed 6 | type Blarg = 7 | -------------------------------------------------------------------------------- /theta/test/data/python/bar_reference.mustache: -------------------------------------------------------------------------------- 1 | @dataclass 2 | class Bar: 3 | avro_schema: ClassVar[Dict[str, Any]] = json.loads('''{"aliases":[],"doc":"Generated with Theta {{version}}\\nType hash: b65eb4cc094eb0acb7c1c24fd402d36a","fields":[{"aliases":[],"name":"foo","type":{"aliases":[],"fields":[{"aliases":[],"name":"foo","type":"Foo"}],"name":"Foo","type":"record"}}],"name":"test.Bar","type":"record"}''') 4 | 5 | foo: 'Foo' 6 | 7 | def encode_avro(self, encoder: avro.Encoder): 8 | (lambda record: record.encode_avro(encoder))(self.foo) 9 | 10 | def to_avro(self, out): 11 | self.encode_avro(avro.Encoder(out)) 12 | 13 | @staticmethod 14 | def decode_avro(decoder: avro.Decoder): 15 | return Bar(Foo.decode_avro(decoder)) 16 | 17 | @staticmethod 18 | def from_avro(in_): 19 | return Bar.decode_avro(avro.Decoder(in_)) 20 | 21 | @staticmethod 22 | def write_container(objects: List['Bar'], out, 23 | codec: str="deflate", sync_marker: Optional[bytes]=None): 24 | encoder = avro.Encoder(out) 25 | container.encode_container(encoder, objects, codec, sync_marker, Bar) 26 | 27 | @staticmethod 28 | def read_container(in_) -> Iterator['Bar']: 29 | decoder = avro.Decoder(in_) 30 | return container.decode_container(decoder, Bar) 31 | -------------------------------------------------------------------------------- /theta/test/data/python/empty_record.mustache: -------------------------------------------------------------------------------- 1 | @dataclass 2 | class Empty: 3 | avro_schema: ClassVar[Dict[str, Any]] = json.loads('''{"aliases":[],"doc":"Generated with Theta {{version}}\\nType hash: 9195f61d4ba5740a8fb641de7f174566","fields":[],"name":"test.Empty","type":"record"}''') 4 | 5 | 6 | 7 | def encode_avro(self, encoder: avro.Encoder): 8 | pass 9 | 10 | def to_avro(self, out): 11 | self.encode_avro(avro.Encoder(out)) 12 | 13 | @staticmethod 14 | def decode_avro(decoder: avro.Decoder): 15 | return Empty() 16 | 17 | @staticmethod 18 | def from_avro(in_): 19 | return Empty.decode_avro(avro.Decoder(in_)) 20 | 21 | @staticmethod 22 | def write_container(objects: List['Empty'], out, 23 | codec: str="deflate", sync_marker: Optional[bytes]=None): 24 | encoder = avro.Encoder(out) 25 | container.encode_container(encoder, objects, codec, sync_marker, Empty) 26 | 27 | @staticmethod 28 | def read_container(in_) -> Iterator['Empty']: 29 | decoder = avro.Decoder(in_) 30 | return container.decode_container(decoder, Empty) 31 | -------------------------------------------------------------------------------- /theta/test/data/python/enum.mustache: -------------------------------------------------------------------------------- 1 | class Foo(Enum): 2 | Bar = 0 3 | baz = 1 4 | _Baz = 2 5 | 6 | def encode_avro(self, encoder: avro.Encoder): 7 | encoder.integral(self.value) 8 | 9 | def to_avro(self, out): 10 | self.encode_avro(avro.Encoder(out)) 11 | 12 | @staticmethod 13 | def decode_avro(decoder: avro.Decoder): 14 | tag = decoder.integral() 15 | 16 | if tag == 0: 17 | return Foo.Bar 18 | elif tag == 1: 19 | return Foo.baz 20 | elif tag == 2: 21 | return Foo._Baz 22 | else: 23 | raise Exception(f"Invalid tag for enum: {tag}.") 24 | 25 | @staticmethod 26 | def from_avro(in_): 27 | return Foo.decode_avro(avro.Decoder(in_)) 28 | 29 | @staticmethod 30 | def write_container(objects: List['Foo'], out, 31 | codec: str="deflate", sync_marker: Optional[bytes]=None): 32 | encoder = avro.Encoder(out) 33 | container.encode_container(encoder, objects, codec, sync_marker, Foo) 34 | 35 | @staticmethod 36 | def read_container(in_) -> Iterator['Foo']: 37 | decoder = avro.Decoder(in_) 38 | return container.decode_container(decoder, Foo) 39 | -------------------------------------------------------------------------------- /theta/test/data/python/foo_reference.mustache: -------------------------------------------------------------------------------- 1 | @dataclass 2 | class Foo: 3 | avro_schema: ClassVar[Dict[str, Any]] = json.loads('''{"aliases":[],"doc":"Generated with Theta {{version}}\\nType hash: 326792c5b4b7acb338d8e5d7a1415ecf","fields":[{"aliases":[],"name":"foo","type":"Foo"}],"name":"test.Foo","type":"record"}''') 4 | 5 | foo: 'Foo' 6 | 7 | def encode_avro(self, encoder: avro.Encoder): 8 | (lambda record: record.encode_avro(encoder))(self.foo) 9 | 10 | def to_avro(self, out): 11 | self.encode_avro(avro.Encoder(out)) 12 | 13 | @staticmethod 14 | def decode_avro(decoder: avro.Decoder): 15 | return Foo(Foo.decode_avro(decoder)) 16 | 17 | @staticmethod 18 | def from_avro(in_): 19 | return Foo.decode_avro(avro.Decoder(in_)) 20 | 21 | @staticmethod 22 | def write_container(objects: List['Foo'], out, 23 | codec: str="deflate", sync_marker: Optional[bytes]=None): 24 | encoder = avro.Encoder(out) 25 | container.encode_container(encoder, objects, codec, sync_marker, Foo) 26 | 27 | @staticmethod 28 | def read_container(in_) -> Iterator['Foo']: 29 | decoder = avro.Decoder(in_) 30 | return container.decode_container(decoder, Foo) 31 | -------------------------------------------------------------------------------- /theta/test/data/python/importing_foo.mustache: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from dataclasses import dataclass 3 | from datetime import date, datetime, time 4 | from enum import Enum 5 | import json 6 | from typing import Any, ClassVar, Dict, Iterator, List, Mapping, Optional 7 | from uuid import UUID 8 | 9 | from theta import avro, container 10 | 11 | import foo 12 | -------------------------------------------------------------------------------- /theta/test/data/python/importing_foo_qualified.mustache: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from dataclasses import dataclass 3 | from datetime import date, datetime, time 4 | from enum import Enum 5 | import json 6 | from typing import Any, ClassVar, Dict, Iterator, List, Mapping, Optional 7 | from uuid import UUID 8 | 9 | from theta import avro, container 10 | 11 | import theta.foo 12 | -------------------------------------------------------------------------------- /theta/test/data/python/importing_reference.mustache: -------------------------------------------------------------------------------- 1 | @dataclass 2 | class Foo: 3 | avro_schema: ClassVar[Dict[str, Any]] = json.loads('''{"aliases":[],"doc":"Generated with Theta {{version}}\\nType hash: 19c0c8b592ae620fbe187c4507975805","fields":[{"aliases":[],"name":"importing","type":{"aliases":[],"fields":[],"name":"imported.Foo","type":"record"}}],"name":"test.Foo","type":"record"}''') 4 | 5 | importing: 'imported.Foo' 6 | 7 | def encode_avro(self, encoder: avro.Encoder): 8 | (lambda record: record.encode_avro(encoder))(self.importing) 9 | 10 | def to_avro(self, out): 11 | self.encode_avro(avro.Encoder(out)) 12 | 13 | @staticmethod 14 | def decode_avro(decoder: avro.Decoder): 15 | return Foo(imported.Foo.decode_avro(decoder)) 16 | 17 | @staticmethod 18 | def from_avro(in_): 19 | return Foo.decode_avro(avro.Decoder(in_)) 20 | 21 | @staticmethod 22 | def write_container(objects: List['Foo'], out, 23 | codec: str="deflate", sync_marker: Optional[bytes]=None): 24 | encoder = avro.Encoder(out) 25 | container.encode_container(encoder, objects, codec, sync_marker, Foo) 26 | 27 | @staticmethod 28 | def read_container(in_) -> Iterator['Foo']: 29 | decoder = avro.Decoder(in_) 30 | return container.decode_container(decoder, Foo) 31 | -------------------------------------------------------------------------------- /theta/test/data/python/newtype.mustache: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from dataclasses import dataclass 3 | from datetime import date, datetime, time 4 | from enum import Enum 5 | import json 6 | from typing import Any, ClassVar, Dict, Iterator, List, Mapping, Optional 7 | from uuid import UUID 8 | 9 | from theta import avro, container 10 | 11 | 12 | 13 | Newtype = 'int' 14 | 15 | @dataclass 16 | class NewtypeRecord: 17 | avro_schema: ClassVar[Dict[str, Any]] = json.loads('''{"aliases":[],"doc":"Generated with Theta {{version}}\\nType hash: ba7a235e5b8ce0ecfe1458f23efef65f","fields":[{"aliases":[],"name":"foo","type":"int"}],"name":"newtype.NewtypeRecord","type":"record"}''') 18 | 19 | foo: 'Newtype' 20 | 21 | def encode_avro(self, encoder: avro.Encoder): 22 | (encoder.integral)(self.foo) 23 | 24 | def to_avro(self, out): 25 | self.encode_avro(avro.Encoder(out)) 26 | 27 | @staticmethod 28 | def decode_avro(decoder: avro.Decoder): 29 | return NewtypeRecord(decoder.integral()) 30 | 31 | @staticmethod 32 | def from_avro(in_): 33 | return NewtypeRecord.decode_avro(avro.Decoder(in_)) 34 | 35 | @staticmethod 36 | def write_container(objects: List['NewtypeRecord'], out, 37 | codec: str="deflate", sync_marker: Optional[bytes]=None): 38 | encoder = avro.Encoder(out) 39 | container.encode_container(encoder, objects, codec, sync_marker, NewtypeRecord) 40 | 41 | @staticmethod 42 | def read_container(in_) -> Iterator['NewtypeRecord']: 43 | decoder = avro.Decoder(in_) 44 | return container.decode_container(decoder, NewtypeRecord) 45 | -------------------------------------------------------------------------------- /theta/test/data/python/one_case.mustache: -------------------------------------------------------------------------------- 1 | class Variant(ABC): 2 | avro_schema: ClassVar[Dict[str, Any]] = json.loads('''{"aliases":[],"doc":"Generated with Theta {{version}}\\nType hash: c5a4fbbecf4095f13285be06437cbfb8","fields":[{"aliases":[],"name":"constructor","type":[{"aliases":[],"fields":[{"aliases":[],"name":"foo","type":"int"}],"name":"Case","type":"record"}]}],"name":"test.Variant","type":"record"}''') 3 | 4 | @staticmethod 5 | def decode_avro(decoder: avro.Decoder): 6 | tag = decoder.integral() 7 | 8 | if tag == 0: 9 | return Case.decode_avro(decoder) 10 | else: 11 | raise Exception(f"Invalid tag for variant: {tag}.") 12 | 13 | @staticmethod 14 | def from_avro(in_): 15 | return Variant.decode_avro(avro.Decoder(in_)) 16 | 17 | @staticmethod 18 | def write_container(objects: List['Variant'], out, 19 | codec: str="deflate", sync_marker: Optional[bytes]=None): 20 | encoder = avro.Encoder(out) 21 | container.encode_container(encoder, objects, codec, sync_marker, Variant) 22 | 23 | @staticmethod 24 | def read_container(in_) -> Iterator['Variant']: 25 | decoder = avro.Decoder(in_) 26 | return container.decode_container(decoder, Variant) 27 | 28 | @dataclass 29 | class Case(Variant): 30 | foo: 'int' 31 | 32 | @staticmethod 33 | def decode_avro(decoder: avro.Decoder): 34 | return Case(decoder.integral()) 35 | 36 | def encode_avro(self, encoder: avro.Encoder): 37 | # Tag 38 | encoder.integral(0) 39 | 40 | # Record 41 | (encoder.integral)(self.foo) 42 | 43 | def to_avro(self, out): 44 | self.encode_avro(avro.Encoder(out)) 45 | -------------------------------------------------------------------------------- /theta/test/data/python/one_case_importing.mustache: -------------------------------------------------------------------------------- 1 | class Variant(ABC): 2 | avro_schema: ClassVar[Dict[str, Any]] = json.loads('''{"aliases":[],"doc":"Generated with Theta {{version}}\\nType hash: 97da494b4facc6ba36b096de1904ab9e","fields":[{"aliases":[],"name":"constructor","type":[{"aliases":[],"fields":[{"aliases":[],"name":"importing","type":{"aliases":[],"fields":[],"name":"imported.Foo","type":"record"}}],"name":"Case","type":"record"}]}],"name":"test.Variant","type":"record"}''') 3 | 4 | @staticmethod 5 | def decode_avro(decoder: avro.Decoder): 6 | tag = decoder.integral() 7 | 8 | if tag == 0: 9 | return Case.decode_avro(decoder) 10 | else: 11 | raise Exception(f"Invalid tag for variant: {tag}.") 12 | 13 | @staticmethod 14 | def from_avro(in_): 15 | return Variant.decode_avro(avro.Decoder(in_)) 16 | 17 | @staticmethod 18 | def write_container(objects: List['Variant'], out, 19 | codec: str="deflate", sync_marker: Optional[bytes]=None): 20 | encoder = avro.Encoder(out) 21 | container.encode_container(encoder, objects, codec, sync_marker, Variant) 22 | 23 | @staticmethod 24 | def read_container(in_) -> Iterator['Variant']: 25 | decoder = avro.Decoder(in_) 26 | return container.decode_container(decoder, Variant) 27 | 28 | @dataclass 29 | class Case(Variant): 30 | importing: 'imported.Foo' 31 | 32 | @staticmethod 33 | def decode_avro(decoder: avro.Decoder): 34 | return Case(imported.Foo.decode_avro(decoder)) 35 | 36 | def encode_avro(self, encoder: avro.Encoder): 37 | # Tag 38 | encoder.integral(0) 39 | 40 | # Record 41 | (lambda record: record.encode_avro(encoder))(self.importing) 42 | 43 | def to_avro(self, out): 44 | self.encode_avro(avro.Encoder(out)) 45 | -------------------------------------------------------------------------------- /theta/test/data/python/one_field.mustache: -------------------------------------------------------------------------------- 1 | @dataclass 2 | class OneField: 3 | avro_schema: ClassVar[Dict[str, Any]] = json.loads('''{"aliases":[],"doc":"Generated with Theta {{version}}\\nType hash: 3310b8946f20a835ff10561cf0967f96","fields":[{"aliases":[],"name":"foo","type":"int"}],"name":"test.OneField","type":"record"}''') 4 | 5 | foo: 'int' 6 | 7 | def encode_avro(self, encoder: avro.Encoder): 8 | (encoder.integral)(self.foo) 9 | 10 | def to_avro(self, out): 11 | self.encode_avro(avro.Encoder(out)) 12 | 13 | @staticmethod 14 | def decode_avro(decoder: avro.Decoder): 15 | return OneField(decoder.integral()) 16 | 17 | @staticmethod 18 | def from_avro(in_): 19 | return OneField.decode_avro(avro.Decoder(in_)) 20 | 21 | @staticmethod 22 | def write_container(objects: List['OneField'], out, 23 | codec: str="deflate", sync_marker: Optional[bytes]=None): 24 | encoder = avro.Encoder(out) 25 | container.encode_container(encoder, objects, codec, sync_marker, OneField) 26 | 27 | @staticmethod 28 | def read_container(in_) -> Iterator['OneField']: 29 | decoder = avro.Decoder(in_) 30 | return container.decode_container(decoder, OneField) 31 | -------------------------------------------------------------------------------- /theta/test/data/python/two_cases.mustache: -------------------------------------------------------------------------------- 1 | class Variant(ABC): 2 | avro_schema: ClassVar[Dict[str, Any]] = json.loads('''{"aliases":[],"doc":"Generated with Theta {{version}}\\nType hash: af0242d2de13c30251b0eb9b39aa236d","fields":[{"aliases":[],"name":"constructor","type":[{"aliases":[],"fields":[{"aliases":[],"name":"foo","type":"int"}],"name":"One","type":"record"},{"aliases":[],"fields":[{"aliases":[],"name":"foo","type":"int"},{"aliases":[],"name":"bar","type":"string"}],"name":"Two","type":"record"}]}],"name":"test.Variant","type":"record"}''') 3 | 4 | @staticmethod 5 | def decode_avro(decoder: avro.Decoder): 6 | tag = decoder.integral() 7 | 8 | if tag == 0: 9 | return One.decode_avro(decoder) 10 | elif tag == 1: 11 | return Two.decode_avro(decoder) 12 | else: 13 | raise Exception(f"Invalid tag for variant: {tag}.") 14 | 15 | @staticmethod 16 | def from_avro(in_): 17 | return Variant.decode_avro(avro.Decoder(in_)) 18 | 19 | @staticmethod 20 | def write_container(objects: List['Variant'], out, 21 | codec: str="deflate", sync_marker: Optional[bytes]=None): 22 | encoder = avro.Encoder(out) 23 | container.encode_container(encoder, objects, codec, sync_marker, Variant) 24 | 25 | @staticmethod 26 | def read_container(in_) -> Iterator['Variant']: 27 | decoder = avro.Decoder(in_) 28 | return container.decode_container(decoder, Variant) 29 | 30 | @dataclass 31 | class One(Variant): 32 | foo: 'int' 33 | 34 | @staticmethod 35 | def decode_avro(decoder: avro.Decoder): 36 | return One(decoder.integral()) 37 | 38 | def encode_avro(self, encoder: avro.Encoder): 39 | # Tag 40 | encoder.integral(0) 41 | 42 | # Record 43 | (encoder.integral)(self.foo) 44 | 45 | def to_avro(self, out): 46 | self.encode_avro(avro.Encoder(out)) 47 | 48 | @dataclass 49 | class Two(Variant): 50 | foo: 'int' 51 | bar: 'str' 52 | 53 | @staticmethod 54 | def decode_avro(decoder: avro.Decoder): 55 | return Two(decoder.integral(), decoder.string()) 56 | 57 | def encode_avro(self, encoder: avro.Encoder): 58 | # Tag 59 | encoder.integral(1) 60 | 61 | # Record 62 | (encoder.integral)(self.foo) 63 | (encoder.string)(self.bar) 64 | 65 | def to_avro(self, out): 66 | self.encode_avro(avro.Encoder(out)) 67 | -------------------------------------------------------------------------------- /theta/test/data/python/two_fields.mustache: -------------------------------------------------------------------------------- 1 | @dataclass 2 | class TwoFields: 3 | avro_schema: ClassVar[Dict[str, Any]] = json.loads('''{"aliases":[],"doc":"Generated with Theta {{version}}\\nType hash: 0a4212c30172aa12da90e4fb532764b7","fields":[{"aliases":[],"name":"foo","type":"int"},{"aliases":[],"name":"bar","type":"string"}],"name":"test.TwoFields","type":"record"}''') 4 | 5 | foo: 'int' 6 | bar: 'str' 7 | 8 | def encode_avro(self, encoder: avro.Encoder): 9 | (encoder.integral)(self.foo) 10 | (encoder.string)(self.bar) 11 | 12 | def to_avro(self, out): 13 | self.encode_avro(avro.Encoder(out)) 14 | 15 | @staticmethod 16 | def decode_avro(decoder: avro.Decoder): 17 | return TwoFields(decoder.integral(), decoder.string()) 18 | 19 | @staticmethod 20 | def from_avro(in_): 21 | return TwoFields.decode_avro(avro.Decoder(in_)) 22 | 23 | @staticmethod 24 | def write_container(objects: List['TwoFields'], out, 25 | codec: str="deflate", sync_marker: Optional[bytes]=None): 26 | encoder = avro.Encoder(out) 27 | container.encode_container(encoder, objects, codec, sync_marker, TwoFields) 28 | 29 | @staticmethod 30 | def read_container(in_) -> Iterator['TwoFields']: 31 | decoder = avro.Decoder(in_) 32 | return container.decode_container(decoder, TwoFields) 33 | -------------------------------------------------------------------------------- /theta/test/data/root-1/a.theta: -------------------------------------------------------------------------------- 1 | language-version: 1.0.0 2 | avro-version: 1.0.0 3 | --- 4 | 5 | type Foo = Int 6 | -------------------------------------------------------------------------------- /theta/test/data/root-2/b.theta: -------------------------------------------------------------------------------- 1 | language-version: 1.0.0 2 | avro-version: 1.0.0 3 | --- 4 | 5 | import a 6 | 7 | type Bar = a.Foo 8 | -------------------------------------------------------------------------------- /theta/test/data/root-3/com/example.theta: -------------------------------------------------------------------------------- 1 | language-version: 1.0.0 2 | avro-version: 1.0.0 3 | --- 4 | -------------------------------------------------------------------------------- /theta/test/data/root-3/com/example/nested.theta: -------------------------------------------------------------------------------- 1 | language-version: 1.0.0 2 | avro-version: 1.0.0 3 | --- 4 | 5 | import a 6 | 7 | type Nested = a.Foo 8 | -------------------------------------------------------------------------------- /theta/test/data/root-3/com/example/nested/deeply.theta: -------------------------------------------------------------------------------- 1 | language-version: 1.0.0 2 | avro-version: 1.0.0 3 | --- 4 | -------------------------------------------------------------------------------- /theta/test/data/root-3/com/example/nested_2.theta: -------------------------------------------------------------------------------- 1 | language-version: 1.0.0 2 | avro-version: 1.0.0 3 | --- 4 | -------------------------------------------------------------------------------- /theta/test/data/rust/enums.rs: -------------------------------------------------------------------------------- 1 | use chrono::{Date, DateTime, NaiveDateTime, NaiveTime, Utc}; 2 | use std::collections::HashMap; 3 | use theta::avro::{FromAvro, ToAvro, Fixed}; 4 | use nom::{IResult, Err, error::{context, ErrorKind}}; 5 | use uuid::{Uuid}; 6 | 7 | #[derive(Clone, Debug, PartialEq)] 8 | pub enum SimpleEnum { 9 | Simple, 10 | } 11 | 12 | impl ToAvro for SimpleEnum { 13 | fn to_avro_buffer(&self, buffer: &mut Vec) { 14 | match self { 15 | SimpleEnum::Simple => 0i64.to_avro_buffer(buffer), 16 | } 17 | } 18 | } 19 | 20 | impl FromAvro for SimpleEnum { 21 | fn from_avro(input: &[u8]) -> IResult<&[u8], Self> { 22 | context("enums.SimpleEnum", |input| { 23 | let (input, tag) = i64::from_avro(input)?; 24 | match tag { 25 | 0 => Ok((input, SimpleEnum::Simple)), 26 | _ => Err(Err::Error((input, ErrorKind::Tag))), 27 | } 28 | })(input) 29 | } 30 | } 31 | 32 | #[derive(Clone, Debug, PartialEq)] 33 | pub enum TrickyEnum { 34 | Sym, 35 | Sym_, 36 | Sym__, 37 | } 38 | 39 | impl ToAvro for TrickyEnum { 40 | fn to_avro_buffer(&self, buffer: &mut Vec) { 41 | match self { 42 | TrickyEnum::Sym => 0i64.to_avro_buffer(buffer), 43 | TrickyEnum::Sym_ => 1i64.to_avro_buffer(buffer), 44 | TrickyEnum::Sym__ => 2i64.to_avro_buffer(buffer), 45 | } 46 | } 47 | } 48 | 49 | impl FromAvro for TrickyEnum { 50 | fn from_avro(input: &[u8]) -> IResult<&[u8], Self> { 51 | context("enums.TrickyEnum", |input| { 52 | let (input, tag) = i64::from_avro(input)?; 53 | match tag { 54 | 0 => Ok((input, TrickyEnum::Sym)), 55 | 1 => Ok((input, TrickyEnum::Sym_)), 56 | 2 => Ok((input, TrickyEnum::Sym__)), 57 | _ => Err(Err::Error((input, ErrorKind::Tag))), 58 | } 59 | })(input) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /theta/test/data/rust/newtype.rs: -------------------------------------------------------------------------------- 1 | use chrono::{Date, DateTime, NaiveDateTime, NaiveTime, Utc}; 2 | use std::collections::HashMap; 3 | use theta::avro::{FromAvro, ToAvro, Fixed}; 4 | use nom::{IResult, Err, error::{context, ErrorKind}}; 5 | use uuid::{Uuid}; 6 | 7 | #[derive(Copy, Clone, Debug, PartialEq)] 8 | pub struct Newtype(pub i32); 9 | 10 | impl ToAvro for Newtype { 11 | fn to_avro_buffer(&self, buffer: &mut Vec) { 12 | self.0.to_avro_buffer(buffer); 13 | } 14 | } 15 | 16 | impl FromAvro for Newtype { 17 | fn from_avro(input: &[u8]) -> IResult<&[u8], Self> { 18 | context("newtype.Newtype", |input| { 19 | let (input, value) = i32::from_avro(input)?; 20 | Ok((input, Newtype(value))) 21 | })(input) 22 | } 23 | } 24 | 25 | #[derive(Clone, Debug, PartialEq)] 26 | pub struct NewtypeRecord { 27 | pub foo: newtype::Newtype, 28 | } 29 | 30 | impl ToAvro for NewtypeRecord { 31 | fn to_avro_buffer(&self, buffer: &mut Vec) { 32 | self.foo.to_avro_buffer(buffer); 33 | } 34 | } 35 | 36 | impl FromAvro for NewtypeRecord { 37 | fn from_avro(input: &[u8]) -> IResult<&[u8], Self> { 38 | context("newtype.NewtypeRecord", |input| { 39 | let (input, foo) = newtype::Newtype::from_avro(input)?; 40 | Ok((input, NewtypeRecord { foo: foo })) 41 | })(input) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /theta/test/data/rust/recursive.rs: -------------------------------------------------------------------------------- 1 | use chrono::{Date, DateTime, NaiveDateTime, NaiveTime, Utc}; 2 | use std::collections::HashMap; 3 | use theta::avro::{FromAvro, ToAvro, Fixed}; 4 | use nom::{IResult, Err, error::{context, ErrorKind}}; 5 | use uuid::{Uuid}; 6 | 7 | #[derive(Clone, Debug, PartialEq)] 8 | pub struct AMutual(pub recursive::MutualA); 9 | 10 | impl ToAvro for AMutual { 11 | fn to_avro_buffer(&self, buffer: &mut Vec) { 12 | self.0.to_avro_buffer(buffer); 13 | } 14 | } 15 | 16 | impl FromAvro for AMutual { 17 | fn from_avro(input: &[u8]) -> IResult<&[u8], Self> { 18 | context("recursive.AMutual", |input| { 19 | let (input, value) = recursive::MutualA::from_avro(input)?; 20 | Ok((input, AMutual(value))) 21 | })(input) 22 | } 23 | } 24 | 25 | #[derive(Clone, Debug, PartialEq)] 26 | pub struct MutualA { 27 | pub mutual: Box, 28 | } 29 | 30 | impl ToAvro for MutualA { 31 | fn to_avro_buffer(&self, buffer: &mut Vec) { 32 | self.mutual.to_avro_buffer(buffer); 33 | } 34 | } 35 | 36 | impl FromAvro for MutualA { 37 | fn from_avro(input: &[u8]) -> IResult<&[u8], Self> { 38 | context("recursive.MutualA", |input| { 39 | let (input, mutual) = recursive::MutualB::from_avro(input)?; 40 | Ok((input, MutualA { mutual: Box::new(mutual) })) 41 | })(input) 42 | } 43 | } 44 | 45 | #[derive(Clone, Debug, PartialEq)] 46 | pub struct MutualB { 47 | pub mutual: Box, 48 | } 49 | 50 | impl ToAvro for MutualB { 51 | fn to_avro_buffer(&self, buffer: &mut Vec) { 52 | self.mutual.to_avro_buffer(buffer); 53 | } 54 | } 55 | 56 | impl FromAvro for MutualB { 57 | fn from_avro(input: &[u8]) -> IResult<&[u8], Self> { 58 | context("recursive.MutualB", |input| { 59 | let (input, mutual) = recursive::Wrapper::from_avro(input)?; 60 | Ok((input, MutualB { mutual: Box::new(mutual) })) 61 | })(input) 62 | } 63 | } 64 | 65 | #[derive(Clone, Debug, PartialEq)] 66 | pub enum Recursive { 67 | Nil { 68 | }, 69 | Recurse { 70 | contents: i32, 71 | boxed: Box, 72 | unboxed: HashMap>, 73 | }, 74 | } 75 | 76 | impl ToAvro for Recursive { 77 | fn to_avro_buffer(&self, buffer: &mut Vec) { 78 | match self { 79 | Recursive::Nil { } => { 80 | 0i64.to_avro_buffer(buffer); 81 | }, 82 | 83 | Recursive::Recurse { contents, boxed, unboxed } => { 84 | 1i64.to_avro_buffer(buffer); 85 | contents.to_avro_buffer(buffer); 86 | boxed.to_avro_buffer(buffer); 87 | unboxed.to_avro_buffer(buffer); 88 | }, 89 | }; 90 | } 91 | } 92 | 93 | impl FromAvro for Recursive { 94 | fn from_avro(input: &[u8]) -> IResult<&[u8], Self> { 95 | context("recursive.Recursive", |input| { 96 | let (input, tag) = i64::from_avro(input)?; 97 | match tag { 98 | 0 => { 99 | Ok((input, Recursive::Nil { })) 100 | }, 101 | 1 => { 102 | let (input, contents) = i32::from_avro(input)?; 103 | let (input, boxed) = recursive::Recursive::from_avro(input)?; 104 | let (input, unboxed) = HashMap::from_avro(input)?; 105 | Ok((input, Recursive::Recurse { contents: contents, boxed: Box::new(boxed), unboxed: unboxed })) 106 | }, 107 | _ => Err(Err::Error((input, ErrorKind::Tag))), 108 | } 109 | })(input) 110 | } 111 | } 112 | 113 | pub type Wrapper = Option; 114 | -------------------------------------------------------------------------------- /theta/test/data/rust/single_file.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_snake_case)] 2 | #![allow(unused_imports)] 3 | 4 | pub mod newtype { 5 | use super::newtype; 6 | use super::recursive; 7 | 8 | use chrono::{Date, DateTime, NaiveDateTime, NaiveTime, Utc}; 9 | use std::collections::HashMap; 10 | use theta::avro::{FromAvro, ToAvro, Fixed}; 11 | use nom::{IResult, Err, error::{context, ErrorKind}}; 12 | use uuid::{Uuid}; 13 | 14 | #[derive(Copy, Clone, Debug, PartialEq)] 15 | pub struct Newtype(pub i32); 16 | 17 | impl ToAvro for Newtype { 18 | fn to_avro_buffer(&self, buffer: &mut Vec) { 19 | self.0.to_avro_buffer(buffer); 20 | } 21 | } 22 | 23 | impl FromAvro for Newtype { 24 | fn from_avro(input: &[u8]) -> IResult<&[u8], Self> { 25 | context("newtype.Newtype", |input| { 26 | let (input, value) = i32::from_avro(input)?; 27 | Ok((input, Newtype(value))) 28 | })(input) 29 | } 30 | } 31 | 32 | #[derive(Clone, Debug, PartialEq)] 33 | pub struct NewtypeRecord { 34 | pub foo: newtype::Newtype, 35 | } 36 | 37 | impl ToAvro for NewtypeRecord { 38 | fn to_avro_buffer(&self, buffer: &mut Vec) { 39 | self.foo.to_avro_buffer(buffer); 40 | } 41 | } 42 | 43 | impl FromAvro for NewtypeRecord { 44 | fn from_avro(input: &[u8]) -> IResult<&[u8], Self> { 45 | context("newtype.NewtypeRecord", |input| { 46 | let (input, foo) = newtype::Newtype::from_avro(input)?; 47 | Ok((input, NewtypeRecord { foo: foo })) 48 | })(input) 49 | } 50 | } 51 | } 52 | 53 | pub mod recursive { 54 | use super::newtype; 55 | use super::recursive; 56 | 57 | use chrono::{Date, DateTime, NaiveDateTime, NaiveTime, Utc}; 58 | use std::collections::HashMap; 59 | use theta::avro::{FromAvro, ToAvro, Fixed}; 60 | use nom::{IResult, Err, error::{context, ErrorKind}}; 61 | use uuid::{Uuid}; 62 | 63 | #[derive(Clone, Debug, PartialEq)] 64 | pub struct AMutual(pub recursive::MutualA); 65 | 66 | impl ToAvro for AMutual { 67 | fn to_avro_buffer(&self, buffer: &mut Vec) { 68 | self.0.to_avro_buffer(buffer); 69 | } 70 | } 71 | 72 | impl FromAvro for AMutual { 73 | fn from_avro(input: &[u8]) -> IResult<&[u8], Self> { 74 | context("recursive.AMutual", |input| { 75 | let (input, value) = recursive::MutualA::from_avro(input)?; 76 | Ok((input, AMutual(value))) 77 | })(input) 78 | } 79 | } 80 | 81 | #[derive(Clone, Debug, PartialEq)] 82 | pub struct MutualA { 83 | pub mutual: Box, 84 | } 85 | 86 | impl ToAvro for MutualA { 87 | fn to_avro_buffer(&self, buffer: &mut Vec) { 88 | self.mutual.to_avro_buffer(buffer); 89 | } 90 | } 91 | 92 | impl FromAvro for MutualA { 93 | fn from_avro(input: &[u8]) -> IResult<&[u8], Self> { 94 | context("recursive.MutualA", |input| { 95 | let (input, mutual) = recursive::MutualB::from_avro(input)?; 96 | Ok((input, MutualA { mutual: Box::new(mutual) })) 97 | })(input) 98 | } 99 | } 100 | 101 | #[derive(Clone, Debug, PartialEq)] 102 | pub struct MutualB { 103 | pub mutual: Box, 104 | } 105 | 106 | impl ToAvro for MutualB { 107 | fn to_avro_buffer(&self, buffer: &mut Vec) { 108 | self.mutual.to_avro_buffer(buffer); 109 | } 110 | } 111 | 112 | impl FromAvro for MutualB { 113 | fn from_avro(input: &[u8]) -> IResult<&[u8], Self> { 114 | context("recursive.MutualB", |input| { 115 | let (input, mutual) = recursive::Wrapper::from_avro(input)?; 116 | Ok((input, MutualB { mutual: Box::new(mutual) })) 117 | })(input) 118 | } 119 | } 120 | 121 | #[derive(Clone, Debug, PartialEq)] 122 | pub enum Recursive { 123 | Nil { 124 | }, 125 | Recurse { 126 | contents: i32, 127 | boxed: Box, 128 | unboxed: HashMap>, 129 | }, 130 | } 131 | 132 | impl ToAvro for Recursive { 133 | fn to_avro_buffer(&self, buffer: &mut Vec) { 134 | match self { 135 | Recursive::Nil { } => { 136 | 0i64.to_avro_buffer(buffer); 137 | }, 138 | Recursive::Recurse { contents, boxed, unboxed } => { 139 | 1i64.to_avro_buffer(buffer); 140 | contents.to_avro_buffer(buffer); 141 | boxed.to_avro_buffer(buffer); 142 | unboxed.to_avro_buffer(buffer); 143 | }, 144 | }; 145 | } 146 | } 147 | 148 | impl FromAvro for Recursive { 149 | fn from_avro(input: &[u8]) -> IResult<&[u8], Self> { 150 | context("recursive.Recursive", |input| { 151 | let (input, tag) = i64::from_avro(input)?; 152 | match tag { 153 | 0 => { 154 | Ok((input, Recursive::Nil { })) 155 | }, 156 | 1 => { 157 | let (input, contents) = i32::from_avro(input)?; 158 | let (input, boxed) = recursive::Recursive::from_avro(input)?; 159 | let (input, unboxed) = HashMap::from_avro(input)?; 160 | Ok((input, Recursive::Recurse { contents: contents, boxed: Box::new(boxed), unboxed: unboxed })) 161 | }, 162 | _ => Err(Err::Error((input, ErrorKind::Tag))), 163 | } 164 | })(input) 165 | } 166 | } 167 | 168 | pub type Wrapper = Option; 169 | } 170 | -------------------------------------------------------------------------------- /theta/test/data/transitive/a.theta: -------------------------------------------------------------------------------- 1 | language-version: 1.0.0 2 | avro-version: 1.0.0 3 | --- 4 | 5 | type A = String 6 | -------------------------------------------------------------------------------- /theta/test/data/transitive/b.theta: -------------------------------------------------------------------------------- 1 | language-version: 1.0.0 2 | avro-version: 1.0.0 3 | --- 4 | 5 | import a 6 | 7 | type B = { a : a.A } 8 | -------------------------------------------------------------------------------- /theta/test/data/transitive/c.theta: -------------------------------------------------------------------------------- 1 | language-version: 1.0.0 2 | avro-version: 1.0.0 3 | --- 4 | 5 | import a 6 | import b 7 | 8 | type C = { a : a.A, b : b.B } 9 | -------------------------------------------------------------------------------- /theta/test/data/transitive/d.theta: -------------------------------------------------------------------------------- 1 | language-version: 1.0.0 2 | avro-version: 1.0.0 3 | --- 4 | 5 | import a 6 | import c 7 | 8 | type D = { a : a.A, c : c.C } 9 | --------------------------------------------------------------------------------