├── spec ├── assets │ ├── test_filter │ ├── raw.json │ ├── data1.json │ ├── data1.yml │ ├── data2.json │ ├── data2.yml │ ├── test.jq │ ├── stream-filter │ └── stream-data.json ├── format_spec.cr ├── spec_helper.cr ├── processor_spec.cr ├── oq_spec.cr └── converters │ ├── simple_yaml_spec.cr │ ├── yaml_spec.cr │ └── xml_spec.cr ├── .editorconfig ├── .gitignore ├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ └── deployment.yml ├── src ├── converters │ ├── json.cr │ ├── processor_aware.cr │ ├── yaml.cr │ ├── simple_yaml.cr │ └── xml.cr ├── oq_cli.cr └── oq.cr ├── shard.yml ├── snap └── snapcraft.yaml ├── LICENSE └── README.md /spec/assets/test_filter: -------------------------------------------------------------------------------- 1 | .name -------------------------------------------------------------------------------- /spec/assets/raw.json: -------------------------------------------------------------------------------- 1 | 1 2 | 2 3 | 3 4 | -------------------------------------------------------------------------------- /spec/assets/data1.json: -------------------------------------------------------------------------------- 1 | {"name": "Jim"} 2 | -------------------------------------------------------------------------------- /spec/assets/data1.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Jim 3 | -------------------------------------------------------------------------------- /spec/assets/data2.json: -------------------------------------------------------------------------------- 1 | {"name": "Bob"} 2 | -------------------------------------------------------------------------------- /spec/assets/data2.yml: -------------------------------------------------------------------------------- 1 | age: 17 2 | name: Fred 3 | -------------------------------------------------------------------------------- /spec/assets/test.jq: -------------------------------------------------------------------------------- 1 | def increment: . + 1; 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.cr] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 2 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.dwarf 2 | *.snap 3 | /.shards/ 4 | /bin/ 5 | /docs/ 6 | /lib/ 7 | 8 | # Libraries don't need dependency lock 9 | # Dependencies will be locked in application that uses them 10 | /shard.lock 11 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "monthly" 8 | labels: 9 | - "kind:infrastructure" 10 | - "kind:enhancement" 11 | -------------------------------------------------------------------------------- /spec/format_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | describe OQ::Format do 4 | describe ".to_s" do 5 | it "returns a comma separated list of the formats" do 6 | OQ::Format.to_s.should eq "json, simpleyaml, xml, yaml" 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /src/converters/json.cr: -------------------------------------------------------------------------------- 1 | # Converter for the `OQ::Format::JSON` format. 2 | module OQ::Converters::JSON 3 | def self.deserialize(input : IO, output : IO) : Nil 4 | IO.copy input, output 5 | end 6 | 7 | def self.serialize(input : IO, output : IO) : Nil 8 | IO.copy input, output 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /src/converters/processor_aware.cr: -------------------------------------------------------------------------------- 1 | # :nodoc: 2 | # 3 | # Denotes a converter exposes the related `OQ::Processor` 4 | # instance in order to read configuration options off of it. 5 | module OQ::Converters::ProcessorAware 6 | macro extended 7 | class_property! processor : OQ::Processor 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/assets/stream-filter: -------------------------------------------------------------------------------- 1 | reduce inputs as $line 2 | ({}; 3 | $line.machine as $machine 4 | | $line.domain as $domain 5 | | .[$machine].total as $total 6 | | .[$machine].evildoers as $evildoers 7 | | . + { ($machine): {"total": (1 + $total), 8 | "evildoers": ($evildoers | (.[$domain] += 1)) }} ) 9 | -------------------------------------------------------------------------------- /spec/assets/stream-data.json: -------------------------------------------------------------------------------- 1 | {"machine": "possible_victim01", "domain": "evil.com", "timestamp":1435071870} 2 | {"machine": "possible_victim01", "domain": "evil.com", "timestamp":1435071875} 3 | {"machine": "possible_victim01", "domain": "soevil.com", "timestamp":1435071877} 4 | {"machine": "possible_victim02", "domain": "bad.com", "timestamp":1435071877} 5 | {"machine": "possible_victim03", "domain": "soevil.com", "timestamp":1435071879} 6 | -------------------------------------------------------------------------------- /shard.yml: -------------------------------------------------------------------------------- 1 | name: oq 2 | 3 | description: | 4 | A performant, and portable jq wrapper that facilitates the consumption and output of formats other than JSON; using jq filters to transform the data. 5 | 6 | version: 1.3.5 7 | 8 | authors: 9 | - George Dietrich 10 | 11 | crystal: ~> 1.4 12 | 13 | license: MIT 14 | 15 | targets: 16 | oq: 17 | main: src/oq_cli.cr 18 | 19 | development_dependencies: 20 | ameba: 21 | github: crystal-ameba/ameba 22 | version: ~> 1.5.0 23 | -------------------------------------------------------------------------------- /spec/spec_helper.cr: -------------------------------------------------------------------------------- 1 | require "spec" 2 | require "../src/oq" 3 | 4 | # Runs the binary with the given *name* and *args*. 5 | def run_binary(input : String | Process::Redirect | Nil = nil, name : String = "bin/oq", args : Array(String) = [] of String, *, success : Bool = true, file = __FILE__, line = __LINE__, & : String, Process::Status, String -> Nil) 6 | buffer_io = IO::Memory.new 7 | error_io = IO::Memory.new 8 | input_io = IO::Memory.new 9 | 10 | if input.is_a? Process::Redirect 11 | input_io = input 12 | else 13 | input_io << input if input 14 | input_io = input_io.rewind 15 | end 16 | 17 | status = Process.run(name, args, output: buffer_io, input: input_io, error: error_io) 18 | 19 | if success 20 | status.success?.should be_true, file: file, line: line, failure_message: error_io.to_s 21 | else 22 | status.success?.should_not be_true, file: file, line: line, failure_message: error_io.to_s 23 | end 24 | 25 | yield buffer_io.to_s, status, error_io.to_s 26 | end 27 | -------------------------------------------------------------------------------- /snap/snapcraft.yaml: -------------------------------------------------------------------------------- 1 | name: oq 2 | version: '1.3.5' 3 | summary: A performant, and portable jq wrapper to support formats other than JSON 4 | description: | 5 | A performant, and portable jq wrapper that facilitates the consumption and output of formats other than JSON; using jq filters to transform the data. 6 | 7 | contact: george@dietrich.app 8 | issues: https://github.com/Blacksmoke16/oq/issues 9 | website: https://github.com/Blacksmoke16/oq 10 | source-code: https://github.com/Blacksmoke16/oq.git 11 | license: MIT 12 | 13 | grade: stable 14 | confinement: strict 15 | base: core20 16 | type: app 17 | 18 | apps: 19 | oq: 20 | command: bin/oq 21 | plugs: 22 | - home 23 | - removable-media 24 | 25 | parts: 26 | oq: 27 | plugin: crystal 28 | crystal-build-options: 29 | - --release 30 | - --no-debug 31 | - '--link-flags=-s -Wl,-z,relro,-z,now' 32 | source: ./ 33 | stage-packages: 34 | - jq 35 | override-pull: | 36 | snapcraftctl pull 37 | rm -rf $SNAPCRAFT_PART_SRC/lib $SNAPCRAFT_PART_SRC/bin 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 George Dietrich 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - 'master' 7 | schedule: 8 | - cron: '0 21 * * *' 9 | 10 | jobs: 11 | check_format: 12 | runs-on: ubuntu-latest 13 | container: 14 | image: crystallang/crystal:latest-alpine 15 | steps: 16 | - uses: actions/checkout@v6 17 | - name: Format 18 | run: crystal tool format --check 19 | coding_standards: 20 | runs-on: ubuntu-latest 21 | container: 22 | image: crystallang/crystal:latest 23 | steps: 24 | - uses: actions/checkout@v6 25 | - name: Install Dependencies 26 | run: shards install 27 | - name: Ameba 28 | run: ./bin/ameba 29 | test: 30 | strategy: 31 | fail-fast: false 32 | matrix: 33 | os: 34 | - ubuntu-latest 35 | - macos-latest 36 | crystal: 37 | - latest 38 | - nightly 39 | runs-on: ${{ matrix.os }} 40 | steps: 41 | - uses: actions/checkout@v6 42 | - name: Install Crystal 43 | uses: crystal-lang/install-crystal@v1 44 | with: 45 | crystal: ${{ matrix.crystal }} 46 | - name: Build 47 | run: shards build --production 48 | - name: Specs 49 | run: crystal spec --order=random --error-on-warnings 50 | -------------------------------------------------------------------------------- /src/converters/yaml.cr: -------------------------------------------------------------------------------- 1 | # Converter for the `OQ::Format::YAML` format. 2 | module OQ::Converters::YAML 3 | extend self 4 | 5 | def deserialize(input : IO, output : IO) : Nil 6 | ::YAML.parse(input).to_json output 7 | end 8 | 9 | # ameba:disable Metrics/CyclomaticComplexity 10 | def serialize(input : IO, output : IO) : Nil 11 | json = ::JSON::PullParser.new input 12 | yaml = ::YAML::Builder.new output 13 | 14 | # Return early is there is no JSON to be read. 15 | return if json.kind.eof? 16 | 17 | yaml.stream do 18 | yaml.document do 19 | loop do 20 | case json.kind 21 | when .null? 22 | yaml.scalar nil 23 | when .bool? 24 | yaml.scalar json.bool_value 25 | when .int? 26 | yaml.scalar json.int_value 27 | when .float? 28 | yaml.scalar json.float_value 29 | when .string? 30 | if ::YAML::Schema::Core.reserved_string? json.string_value 31 | yaml.scalar json.string_value, style: :double_quoted 32 | else 33 | yaml.scalar json.string_value 34 | end 35 | when .begin_array? 36 | yaml.start_sequence 37 | when .end_array? 38 | yaml.end_sequence 39 | when .begin_object? 40 | yaml.start_mapping 41 | when .end_object? 42 | yaml.end_mapping 43 | when .eof? 44 | break 45 | end 46 | json.read_next 47 | end 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /src/converters/simple_yaml.cr: -------------------------------------------------------------------------------- 1 | require "./yaml" 2 | 3 | # Converter for the `OQ::Format::SimpleYAML` format. 4 | module OQ::Converters::SimpleYAML 5 | extend OQ::Converters::YAML 6 | extend self 7 | 8 | # ameba:disable Metrics/CyclomaticComplexity 9 | def deserialize(input : IO, output : IO) : Nil 10 | yaml = ::YAML::PullParser.new(input) 11 | json = ::JSON::Builder.new(output) 12 | 13 | yaml.read_stream do 14 | loop do 15 | case yaml.kind 16 | when .document_start? 17 | json.start_document 18 | when .document_end? 19 | json.end_document 20 | yaml.read_next 21 | break 22 | when .scalar? 23 | string = yaml.value 24 | 25 | if json.next_is_object_key? 26 | json.scalar(string) 27 | else 28 | scalar = ::YAML::Schema::Core.parse_scalar(yaml) 29 | case scalar 30 | when Nil 31 | json.scalar(scalar) 32 | when Bool 33 | json.scalar(scalar) 34 | when Int64 35 | json.scalar(scalar) 36 | when Float64 37 | json.scalar(scalar) 38 | else 39 | json.scalar(string) 40 | end 41 | end 42 | when .sequence_start? 43 | json.start_array 44 | when .sequence_end? 45 | json.end_array 46 | when .mapping_start? 47 | json.start_object 48 | when .mapping_end? 49 | json.end_object 50 | end 51 | yaml.read_next 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/processor_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | describe OQ::Processor do 4 | describe "custom IOs" do 5 | it "works with \"STDIN\" input" do 6 | input_io = IO::Memory.new %({"name":"Jim"}) 7 | output_io = IO::Memory.new 8 | 9 | OQ::Processor.new.process input_args: [".name"], input: input_io, output: output_io 10 | 11 | output_io.to_s.should eq %("Jim"\n) 12 | end 13 | 14 | it "works with custom error output" do 15 | input_io = IO::Memory.new %({"name:"Jim"}) 16 | output_io = IO::Memory.new 17 | error_io = IO::Memory.new 18 | 19 | expect_raises RuntimeError do 20 | OQ::Processor.new.process input_args: [".name"], input: input_io, output: output_io, error: error_io 21 | end 22 | 23 | output_io.to_s.should be_empty 24 | error_io.to_s.should contain "parse error: Invalid numeric literal at line 1, column 12\n" 25 | end 26 | 27 | describe "file input" do 28 | it "single file" do 29 | output_io = IO::Memory.new 30 | 31 | OQ::Processor.new.process input_args: [".", "-c", "spec/assets/data1.json"], output: output_io 32 | 33 | output_io.to_s.should eq %({"name":"Jim"}\n) 34 | end 35 | 36 | it "single file, standard input IO" do 37 | input_io = IO::Memory.new 38 | output_io = IO::Memory.new 39 | 40 | OQ::Processor.new.process input_args: [".", "-c", "spec/assets/data1.json"], input: input_io, output: output_io 41 | 42 | output_io.to_s.should eq %({"name":"Jim"}\n) 43 | end 44 | 45 | it "multiple file" do 46 | output_io = IO::Memory.new 47 | 48 | OQ::Processor.new.process input_args: [".", "-c", "spec/assets/data1.json", "spec/assets/data2.json"], output: output_io 49 | 50 | output_io.to_s.should eq %({"name":"Jim"}\n{"name":"Bob"}\n) 51 | end 52 | 53 | it "multiple files and --slurp" do 54 | output_io = IO::Memory.new 55 | 56 | OQ::Processor.new.process input_args: [".", "-c", "-s", "spec/assets/data1.json", "spec/assets/data2.json"], output: output_io 57 | 58 | output_io.to_s.should eq %([{"name":"Jim"},{"name":"Bob"}]\n) 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /.github/workflows/deployment.yml: -------------------------------------------------------------------------------- 1 | name: Deployment 2 | 3 | on: 4 | release: 5 | types: 6 | - created 7 | 8 | jobs: 9 | dist_linux: 10 | runs-on: ubuntu-latest 11 | container: 12 | image: crystallang/crystal:latest-alpine 13 | steps: 14 | - uses: actions/checkout@v6 15 | - name: Update Libs 16 | run: apk add --update --upgrade --no-cache --force-overwrite libxml2-dev yaml-dev yaml-static 17 | - name: Build 18 | run: shards build --production --release --static --no-debug --link-flags="-s -Wl,-z,relro,-z,now" 19 | - name: Upload 20 | uses: actions/upload-release-asset@v1.0.2 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | with: 24 | upload_url: ${{ github.event.release.upload_url }} 25 | asset_path: ./bin/oq 26 | asset_name: oq-${{ github.event.release.tag_name }}-linux-x86_64 27 | asset_content_type: binary/octet-stream 28 | dist_snap: 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@v6 32 | - name: Build Snap 33 | uses: snapcore/action-build@v1 34 | id: build 35 | - name: Publish Snap 36 | uses: snapcore/action-publish@v1 37 | env: 38 | SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.STORE_LOGIN }} 39 | with: 40 | snap: ${{ steps.build.outputs.snap }} 41 | release: stable 42 | dist_homebrew: 43 | runs-on: macos-latest 44 | steps: 45 | - run: git config --global user.email "george@dietrich.app" 46 | - run: git config --global user.name "George Dietrich" 47 | - name: Bump Formula 48 | uses: Homebrew/actions/bump-formulae@b5d9170bc1edf1103e40226592b5842b783dd1e0 49 | with: 50 | token: ${{ secrets.HOMEBREW_GITHUB_API_TOKEN }} 51 | formulae: oq 52 | deploy_docs: 53 | environment: 54 | name: github-pages 55 | url: ${{ steps.deployment.outputs.page_url }} 56 | permissions: 57 | contents: read 58 | pages: write 59 | id-token: write 60 | runs-on: ubuntu-latest 61 | steps: 62 | - uses: actions/checkout@v6 63 | - name: Install Crystal 64 | uses: crystal-lang/install-crystal@v1 65 | - name: Build 66 | run: crystal docs 67 | - name: Setup Pages 68 | uses: actions/configure-pages@v5 69 | - name: Upload artifact 70 | uses: actions/upload-pages-artifact@v4 71 | with: 72 | path: 'docs/' 73 | - name: Deploy to GitHub Pages 74 | id: deployment 75 | uses: actions/deploy-pages@v4 76 | -------------------------------------------------------------------------------- /src/oq_cli.cr: -------------------------------------------------------------------------------- 1 | require "option_parser" 2 | 3 | require "./oq" 4 | 5 | processor = OQ::Processor.new 6 | 7 | OptionParser.parse do |parser| 8 | parser.banner = "Usage: oq [--help] [oq-arguments] [jq-arguments] jq_filter [file [files...]]" 9 | parser.on("-h", "--help", "Show this help message.") do 10 | output = IO::Memory.new 11 | version = IO::Memory.new 12 | 13 | Process.run("jq", ["-h"], output: output) 14 | Process.run("jq", ["--version"], output: version) 15 | 16 | puts "oq version: #{OQ::VERSION}, jq version: #{version}", parser, output.to_s.lines.map(&.gsub('\t', " ")).tap(&.delete_at(0..1)).join('\n') 17 | exit 18 | end 19 | parser.on("-V", "--version", "Returns the current versions of oq and jq.") do 20 | output = IO::Memory.new 21 | 22 | Process.run("jq", ["--version"], output: output) 23 | 24 | puts "jq: #{output}", "oq: #{OQ::VERSION}" 25 | exit 26 | end 27 | parser.on("-i FORMAT", "--input FORMAT", "Format of the input data. Supported formats: #{OQ::Format}") { |format| (f = OQ::Format.parse?(format)) ? processor.input_format = f : abort "Invalid input format: '#{format}'" } 28 | parser.on("-o FORMAT", "--output FORMAT", "Format of the output data. Supported formats: #{OQ::Format}") { |format| (f = OQ::Format.parse?(format)) ? processor.output_format = f : abort "Invalid output format: '#{format}'" } 29 | parser.on("--indent NUMBER", "Use the given number of spaces for indentation (JSON/XML only).") { |n| processor.indent = n.to_i; processor.add_arg "--indent"; processor.add_arg n } 30 | parser.on("--tab", "Use a tab for each indentation level instead of two spaces.") { processor.tab = true; processor.add_arg "--tab" } 31 | parser.on("-n", "--null-input", "Don't read any input at all, running the filter once using `null` as the input.") { processor.null = true; processor.add_arg "--null-input" } 32 | parser.on("--no-prolog", "Whether the XML prolog should be emitted if converting to XML.") { processor.xml_prolog = false } 33 | parser.on("--xml-item NAME", "The name for XML array elements without keys.") { |i| processor.xml_item = i } 34 | parser.on("--xmlns", "If XML namespaces should be parsed. NOTE: This will become the default in oq 2.x.") { processor.xmlns = true } 35 | parser.on("--xml-force-array NAME", "Forces an element with the provided name to be parsed as an array even if it only contains one item.") { |n| processor.add_forced_array n } 36 | parser.on("--xml-namespace-alias ALIAS", "Value should be in the form of: `key=namespace`. Elements within the provided namespace are normalized to the provided key. NOTE: Requires the `--xmlns` option to be passed as well.") { |a| k, v = a.split('=', 2); processor.add_xml_namespace k, v } 37 | parser.on("--xml-root ROOT", "Name of the root XML element if converting to XML.") { |r| processor.xml_root = r } 38 | parser.invalid_option { } 39 | end 40 | 41 | begin 42 | processor.process 43 | rescue ex : RuntimeError 44 | # ignore jq errors as it writes directly to error output. 45 | exit 1 46 | rescue ex 47 | abort "oq error: #{ex.message}" 48 | end 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # oq 2 | 3 | [![Built with Crystal](https://img.shields.io/badge/built%20with-crystal-000000.svg?logo=crystal)](https://crystal-lang.org/) 4 | [![CI](https://github.com/blacksmoke16/oq/workflows/CI/badge.svg)](https://github.com/blacksmoke16/oq/actions?query=workflow%3ACI) 5 | [![Latest release](https://img.shields.io/github/release/blacksmoke16/oq.svg?color=teal&logo=github)](https://github.com/blacksmoke16/oq/releases) 6 | [![oq](https://snapcraft.io/oq/badge.svg)](https://snapcraft.io/oq) 7 | [![oq](https://img.shields.io/aur/version/oq?label=oq&logo=arch-linux)](https://aur.archlinux.org/packages/oq/) 8 | [![oq-bin](https://img.shields.io/aur/version/oq-bin?label=oq-bin&logo=arch-linux)](https://aur.archlinux.org/packages/oq-bin/) 9 | 10 | A performant, portable [jq](https://github.com/stedolan/jq/) wrapper that facilitates the consumption and output of formats other than JSON; using `jq` filters to transform the data. 11 | 12 | * Compiles to a single binary for easy portability. 13 | * Performant, similar performance with JSON data compared to `jq`. Slightly longer execution time when going to/from a non-JSON format. 14 | * Supports various other input/output [formats](https://blacksmoke16.github.io/oq/OQ/Format.html), such as `XML` and `YAML`. 15 | * Can be used as a dependency within other Crystal projects. 16 | 17 | ## Installation 18 | 19 | ### Linux 20 | 21 | A statically linked binary for Linux `x86_64` as available on the [Releases](https://github.com/Blacksmoke16/oq/releases) tab. Additionally it can also be installed via various package managers. 22 | 23 | #### Snapcraft 24 | 25 | For more on installing & using `snap` with your Linux distribution, see the [official documentation](https://docs.snapcraft.io/installing-snapd). 26 | 27 | ```sh 28 | sudo snap install oq 29 | ``` 30 | 31 | #### Arch Linux 32 | 33 | Using [yay](https://github.com/Jguer/yay): 34 | 35 | ```sh 36 | yay -S oq 37 | ``` 38 | 39 | A pre-compiled version is also available: 40 | 41 | ```sh 42 | yay -S oq-bin 43 | ``` 44 | 45 | ### macOS 46 | 47 | ```sh 48 | brew install oq 49 | ``` 50 | 51 | ### From Source 52 | 53 | If building from source, `jq` will need to be installed separately. Installation instructions can be found in the [official documentation](https://stedolan.github.io/jq/). 54 | 55 | Requires Crystal to be installed, see the [installation documentation](https://crystal-lang.org/install). 56 | 57 | ```sh 58 | git clone https://github.com/Blacksmoke16/oq.git 59 | cd oq/ 60 | shards build --production --release 61 | ``` 62 | 63 | The built binary will be available as `./bin/oq`. This can be relocated elsewhere on your machine; be sure it is in your `PATH` to access it as `oq`. 64 | 65 | ### Docker 66 | 67 | `oq` can easily be included into a Docker image by fetching the static binary from Github for the version of `oq` that you want. 68 | 69 | ```dockerfile 70 | # Set an arg to store the oq version that should be installed. 71 | ARG OQ_VERSION=1.3.5 72 | 73 | # Grab the binary from the latest Github release and make it executable; placing it within /usr/local/bin. Can also put it elsewhere if you so desire. 74 | RUN wget https://github.com/Blacksmoke16/oq/releases/download/v${OQ_VERSION}/oq-v${OQ_VERSION}-linux-x86_64 -O /usr/local/bin/oq && chmod +x /usr/local/bin/oq 75 | 76 | # Or using curl (needs to follow Github's redirect): 77 | RUN curl -L -o /usr/local/bin/oq https://github.com/Blacksmoke16/oq/releases/download/v${OQ_VERSION}/oq-v${OQ_VERSION}-linux-x86_64 && chmod +x /usr/local/bin/oq 78 | 79 | # Also be sure to install jq if it is not already! 80 | ``` 81 | 82 | ### Existing Crystal Project 83 | 84 | Add the following to your `shard.yml` and run `shards install`. 85 | 86 | ```yaml 87 | dependencies: 88 | oq: 89 | github: blacksmoke16/oq 90 | version: ~> 1.3.0 91 | ``` 92 | 93 | ## Usage 94 | 95 | ### CLI 96 | 97 | Use the `oq` binary, with a few optional custom arguments, see `oq --help`. All other arguments get passed to `jq`. See [jq manual](https://stedolan.github.io/jq/manual/) for details. 98 | 99 | ### Library 100 | 101 | Checkout the [API Documentation](https://blacksmoke16.github.io/oq/OQ/Processor.html) for using `oq` within an existing Crystal project. 102 | 103 | ### Examples 104 | 105 | Consume JSON and output XML 106 | 107 | ```sh 108 | $ echo '{"name": "Jim"}' | oq -o xml . 109 | 110 | 111 | Jim 112 | 113 | ``` 114 | 115 | Consume YAML from a file and output XML 116 | 117 | data.yaml 118 | 119 | ```yaml 120 | --- 121 | name: Jim 122 | numbers: 123 | - 1 124 | - 2 125 | - 3 126 | ``` 127 | 128 | ```sh 129 | $ oq -i yaml -o xml . data.yaml 130 | 131 | 132 | Jim 133 | 1 134 | 2 135 | 3 136 | 137 | ``` 138 | 139 | Use `oq` as a library, consuming some raw `JSON` input, convert it to `YAML`, and write the transformed data to a file. 140 | 141 | ```crystal 142 | require "oq" 143 | 144 | # This could be any `IO`, e.g. an `HTTP` request body, etc. 145 | input_io = IO::Memory.new %({"name":"Jim"}) 146 | 147 | # Create a processor, specifying that we want the output format to be `YAML`. 148 | processor = OQ::Processor.new output_format: :yaml 149 | 150 | File.open("./out.yml", "w") do |file| 151 | # Process the data using our custom input and output IOs. 152 | # The first argument represents the input arguments; 153 | # i.e. the filter and/or any other arguments that should be passed to `jq`. 154 | processor.process ["."], input: input_io, output: file 155 | end 156 | ``` 157 | 158 | ## Contributing 159 | 160 | 1. Fork it () 161 | 2. Create your feature branch (`git checkout -b my-new-feature`) 162 | 3. Commit your changes (`git commit -am 'Add some feature'`) 163 | 4. Push to the branch (`git push origin my-new-feature`) 164 | 5. Create a new Pull Request 165 | 166 | ## Contributors 167 | 168 | - [George Dietrich](https://github.com/Blacksmoke16) - creator, maintainer 169 | - [Michael Springer](https://github.com/sprngr) - contributor 170 | -------------------------------------------------------------------------------- /src/converters/xml.cr: -------------------------------------------------------------------------------- 1 | # Converter for the `OQ::Format::XML` format. 2 | module OQ::Converters::XML 3 | extend OQ::Converters::ProcessorAware 4 | 5 | def self.deserialize(input : IO, output : IO) : Nil 6 | builder = ::JSON::Builder.new output 7 | xml = ::XML::Reader.new input 8 | 9 | # Set reader to first element 10 | xml.read 11 | 12 | # Raise an error if the document is invalid and could not be read 13 | raise ::XML::Error.new LibXML.xmlGetLastError if xml.node_type.none? 14 | 15 | builder.document do 16 | builder.object do 17 | # Skip non element nodes, i.e. the prolog or DOCTYPE, etc. 18 | until xml.node_type.element? 19 | xml.read 20 | end 21 | 22 | process_element_node xml.expand, builder 23 | end 24 | end 25 | end 26 | 27 | private def self.process_element_node(node : ::XML::Node, builder : ::JSON::Builder) : Nil 28 | # If the node doesn't have nested elements nor attributes nor a namespace (with --xmlns); just emit a scalar value 29 | if self.scalar_node? node 30 | return builder.field self.normalize_node_name(node), get_node_value node 31 | end 32 | 33 | # Otherwise process the node as a key/value pair 34 | builder.field self.normalize_node_name node do 35 | builder.object do 36 | process_children node, builder 37 | end 38 | end 39 | end 40 | 41 | private def self.process_array_node(name : String, children : Array(::XML::Node), builder : ::JSON::Builder) : Nil 42 | builder.field name do 43 | builder.array do 44 | children.each do |node| 45 | # If the node doesn't have nested elements nor attributes nor a namespace (with --xmlns); just emit a scalar value 46 | if self.scalar_node? node 47 | builder.scalar get_node_value node 48 | else 49 | # Otherwise process the node within an object 50 | builder.object do 51 | process_children node, builder 52 | end 53 | end 54 | end 55 | end 56 | end 57 | end 58 | 59 | private def self.process_children(node : ::XML::Node, builder : ::JSON::Builder) : Nil 60 | # Process node attributes 61 | node.attributes.each do |attr| 62 | builder.field "@#{attr.name}", attr.content 63 | end 64 | 65 | # Include attributes for namespaces defined on this node 66 | # TODO: Make this the default behavior in oq 2.x 67 | if self.processor.xmlns? 68 | node.namespace_definitions.each do |ns| 69 | builder.field "@#{self.normalize_namespace_prefix ns}", ns.href 70 | end 71 | end 72 | 73 | # Determine how to process a node's children 74 | node.children.group_by(&->normalize_node_name(::XML::Node)).each do |name, children| 75 | # Skip non significant whitespace; Skip mixed character input 76 | if children.first.text? && has_nested_elements?(node) 77 | # Only emit text content if there is only one child 78 | if children.size == 1 79 | builder.field "#text", children.first.content 80 | end 81 | 82 | next 83 | end 84 | 85 | # Array 86 | if children.size > 1 || self.processor.xml_forced_arrays.includes? name 87 | process_array_node name, children, builder 88 | else 89 | if children.first.text? 90 | # node content in attribute object 91 | builder.field "#text", children.first.content 92 | else 93 | # Element 94 | process_element_node children.first, builder 95 | end 96 | end 97 | end 98 | end 99 | 100 | private def self.has_nested_elements?(node : ::XML::Node) : Bool 101 | node.children.any? { |child| !child.text? && !child.cdata? } 102 | end 103 | 104 | # TODO: Make checking for namespaces the default behavior in oq 2.x 105 | private def self.scalar_node?(node : ::XML::Node) : Bool 106 | !self.has_nested_elements?(node) && node.attributes.empty? && ((self.processor.xmlns? && node.namespace_definitions.empty?) || !self.processor.xmlns?) 107 | end 108 | 109 | private def self.get_node_value(node : ::XML::Node) : String? 110 | node.children.empty? ? nil : node.children.first.content 111 | end 112 | 113 | private def self.normalize_node_name(node : ::XML::Node) : String 114 | return node.name unless namespace = node.namespace 115 | (prefix = (self.processor.xml_namespaces[namespace.href]? || namespace.prefix).presence) ? "#{prefix}:#{node.name}" : node.name 116 | end 117 | 118 | private def self.normalize_namespace_prefix(namespace : ::XML::Namespace) : String 119 | (prefix = (self.processor.xml_namespaces[namespace.href]? || namespace.prefix).presence) ? "xmlns:#{prefix}" : "xmlns" 120 | end 121 | 122 | def self.serialize(input : IO, output : IO) : Nil 123 | json = ::JSON::PullParser.new input 124 | builder = ::XML::Builder.new output 125 | 126 | builder.indent = ((self.processor.tab? ? "\t" : " ")*self.processor.indent) 127 | 128 | builder.start_document "1.0", "UTF-8" if self.processor.xml_prolog? 129 | 130 | if root = self.processor.xml_root.presence 131 | builder.start_element root 132 | end 133 | 134 | loop do 135 | emit builder, json 136 | break if json.kind.eof? 137 | end 138 | 139 | if self.processor.xml_root.presence 140 | builder.end_element 141 | end 142 | 143 | builder.end_document if self.processor.xml_prolog? 144 | builder.flush unless self.processor.xml_prolog? 145 | end 146 | 147 | private def self.emit(builder : ::XML::Builder, json : ::JSON::PullParser, key : String? = nil, array_key : String? = nil) : Nil 148 | case json.kind 149 | when .null? then json.read_null 150 | when .string?, .int?, .float?, .bool? then builder.text get_value json 151 | when .begin_object? then handle_object builder, json, key, array_key 152 | when .begin_array? then handle_array builder, json, key, array_key 153 | else 154 | nil 155 | end 156 | end 157 | 158 | private def self.handle_object(builder : ::XML::Builder, json : ::JSON::PullParser, key : String? = nil, array_key : String? = nil) : Nil 159 | json.read_object do |k| 160 | if k.starts_with?('@') 161 | builder.attribute k.lchop('@'), get_value json 162 | elsif k.starts_with?('!') 163 | builder.element k.lchop('!') do 164 | builder.cdata get_value json 165 | end 166 | elsif json.kind.begin_array? || k == "#text" 167 | emit builder, json, k, k 168 | else 169 | builder.element k do 170 | emit builder, json, k 171 | end 172 | end 173 | end 174 | end 175 | 176 | private def self.handle_array(builder : ::XML::Builder, json : ::JSON::PullParser, key : String? = nil, array_key : String? = nil) : Nil 177 | json.read_begin_array 178 | array_key = array_key || self.processor.xml_item 179 | 180 | if json.kind.end_array? 181 | # If the array is empty don't emit anything 182 | else 183 | until json.kind.end_array? 184 | builder.element array_key do 185 | emit builder, json, key 186 | end 187 | end 188 | end 189 | 190 | json.read_end_array 191 | end 192 | 193 | private def self.get_value(json : ::JSON::PullParser) : String 194 | case json.kind 195 | when .string? then json.read_string 196 | when .int? then json.read_int 197 | when .float? then json.read_float 198 | when .bool? then json.read_bool 199 | when .null? then json.read_null 200 | else 201 | "" 202 | end.to_s 203 | end 204 | end 205 | -------------------------------------------------------------------------------- /src/oq.cr: -------------------------------------------------------------------------------- 1 | require "json" 2 | require "xml" 3 | require "yaml" 4 | 5 | require "./converters/*" 6 | 7 | # A performant, and portable jq wrapper that facilitates the consumption and output of formats other than JSON; using jq filters to transform the data. 8 | module OQ 9 | VERSION = "1.3.5" 10 | 11 | # The support formats that can be converted to/from. 12 | enum Format 13 | # The [JSON](https://www.json.org/) format. 14 | JSON 15 | 16 | # Same as `YAML`, but does not support [anchors or aliases](https://yaml.org/spec/1.2/spec.html#id2765878); 17 | # thus allowing for the input conversion to be streamed, reducing the memory usage for large inputs. 18 | SimpleYAML 19 | 20 | # The [XML](https://en.wikipedia.org/wiki/XML) format. 21 | # 22 | # NOTE: Conversion to and from `JSON` uses [this](https://www.xml.com/pub/a/2006/05/31/converting-between-xml-and-json.html) spec. 23 | XML 24 | 25 | # The [YAML](https://yaml.org/) format. 26 | YAML 27 | 28 | # Returns the list of supported formats. 29 | def self.to_s(io : IO) : Nil 30 | self.names.join(io, ", ") { |str, join_io| str.downcase join_io } 31 | end 32 | 33 | # Maps a given format to its converter. 34 | def converter(processor : OQ::Processor) 35 | case self 36 | in .json? then OQ::Converters::JSON 37 | in .simple_yaml? then OQ::Converters::SimpleYAML 38 | in .xml? then OQ::Converters::XML 39 | in .yaml? then OQ::Converters::YAML 40 | end.tap { |converter| converter.processor = processor if converter.is_a? OQ::Converters::ProcessorAware } 41 | end 42 | end 43 | 44 | # Handles the logic of converting the input format (if needed), 45 | # processing it via [jq](https://stedolan.github.io/jq/), 46 | # and converting the output format (if needed). 47 | # 48 | # ``` 49 | # require "oq" 50 | # 51 | # # This could be any `IO`, e.g. an `HTTP` request body, etc. 52 | # input_io = IO::Memory.new %({"name":"Jim"}) 53 | # 54 | # # Create a processor, specifying that we want the output format to be `YAML`. 55 | # processor = OQ::Processor.new output_format: :yaml 56 | # 57 | # File.open("./out.yml", "w") do |file| 58 | # # Process the data using our custom input and output IOs. 59 | # # The first argument represents the input arguments; 60 | # # i.e. the filter and/or any other arguments that should be passed to `jq`. 61 | # processor.process ["."], input: input_io, output: file 62 | # end 63 | # ``` 64 | class Processor 65 | # The format that the input data is in. 66 | property input_format : Format 67 | 68 | # The format that the output should be transcoded into. 69 | property output_format : Format 70 | 71 | # The root of the XML document when transcoding to XML. 72 | property xml_root : String 73 | 74 | # If the XML prolog should be emitted. 75 | property? xml_prolog : Bool 76 | 77 | # The name for XML array elements without keys. 78 | property xml_item : String 79 | 80 | # The number of spaces to use for indentation. 81 | property indent : Int32 82 | 83 | # If a tab for each indentation level instead of spaces. 84 | property? tab : Bool 85 | 86 | # Do not read any input, using `null` as the singular input value. 87 | property? null : Bool 88 | 89 | # If XML namespaces should be parsed as well. 90 | # TODO: Remove this in oq 2.0 as it'll becomethe default. 91 | property? xmlns : Bool 92 | 93 | # Mapping to namespace aliases to their related namespace. 94 | protected getter xml_namespaces = Hash(String, String).new 95 | 96 | # Set of elements who should be force expanded to an array. 97 | protected getter xml_forced_arrays = Set(String).new 98 | 99 | # The args that'll be passed to `jq`. 100 | @args : Array(String) = [] of String 101 | 102 | # Keep a reference to the created temp files in order to delete them later. 103 | @tmp_files = Set(File).new 104 | 105 | def initialize( 106 | @input_format : Format = Format::JSON, 107 | @output_format : Format = Format::JSON, 108 | @xml_root : String = "root", 109 | @xml_prolog : Bool = true, 110 | @xml_item : String = "item", 111 | @indent : Int32 = 2, 112 | @tab : Bool = false, 113 | @null : Bool = false, 114 | @xmlns : Bool = false, 115 | ) 116 | end 117 | 118 | @[Deprecated("Use `Processor#tab?` instead.")] 119 | def tab : Bool 120 | self.tab? 121 | end 122 | 123 | @[Deprecated("Use `Processor#xml_prolog?` instead.")] 124 | def xml_prolog : Bool 125 | self.xml_prolog? 126 | end 127 | 128 | # Adds the provided *value* to the internal args array. 129 | def add_arg(value : String) : Nil 130 | @args << value 131 | end 132 | 133 | def add_xml_namespace(prefix : String, href : String) : Nil 134 | @xml_namespaces[href] = prefix 135 | end 136 | 137 | def add_forced_array(name : String) : Nil 138 | xml_forced_arrays << name 139 | end 140 | 141 | # Consumes `#input_format` data from the provided *input* `IO`, along with any *input_args*. 142 | # The data is then converted to `JSON`, passed to `jq`, and then converted to `#output_format` while being written to the *output* `IO`. 143 | # Any errors are written to the *error* `IO`. 144 | def process(input_args : Array(String) = ARGV, input : IO = ARGF, output : IO = STDOUT, error : IO = STDERR) : Nil 145 | # Register an at_exit handler to cleanup temp files. 146 | at_exit { @tmp_files.each &.delete } 147 | 148 | # Parse out --rawfile, --argfile, --slurpfile,-f/--from-file, and -L before processing additional args 149 | # since these options use a file that should not be used as input. 150 | self.consume_file_args input_args, "--rawfile", "--argfile", "--slurpfile" 151 | self.consume_file_args input_args, "-f", "--from-file", "-L", count: 1 152 | 153 | # Also parse out --arg, and --argjson as they may include identifiers that also exist as a directory/file 154 | # which would result in incorrect arg extraction. 155 | self.consume_file_args input_args, "--arg", "--argjson" 156 | 157 | # Extract `jq` arguments from `ARGV`. 158 | self.extract_args input_args, output 159 | 160 | # The --xml-namespace-alias option must be used with the --xmlns option. 161 | # TODO: Remove this in oq 2.x 162 | raise ArgumentError.new "The `--xml-namespace-alias` option must be used with the `--xmlns` option." if !@xmlns && !@xml_namespaces.empty? 163 | 164 | # Replace the *input* with a fake `ARGF` `IO` to handle both file and `IO` inputs in case `ARGV` is not being used for the input arguments. 165 | # 166 | # If using `null` input, set the input to an empty memory `IO` to essentially consume nothing. 167 | input = @null ? IO::Memory.new : IO::ARGF.new input_args, input 168 | 169 | input_read, input_write = IO.pipe 170 | output_read, output_write = IO.pipe 171 | 172 | channel = Channel(Bool | Exception).new 173 | 174 | # If the input format is not JSON and there is more than 1 file in ARGV, 175 | # convert each file to JSON from the `#input_format` and save it to a temp file. 176 | # Then replace ARGV with the temp files. 177 | if !@input_format.json? && input_args.size > 1 178 | input_args.replace(input_args.map do |file_name| 179 | File.tempfile ".#{File.basename file_name}" do |tmp_file| 180 | File.open file_name do |file| 181 | @input_format.converter(self).deserialize file, tmp_file 182 | end 183 | end 184 | .tap { |tf| @tmp_files << tf } 185 | .path 186 | end) 187 | 188 | # Conversion has already been completed by this point, so reset input format back to JSON. 189 | @input_format = :json 190 | end 191 | 192 | spawn do 193 | @input_format.converter(self).deserialize input, input_write 194 | input_write.close 195 | channel.send true 196 | rescue ex 197 | input_write.close 198 | channel.send ex 199 | end 200 | 201 | spawn do 202 | output_write.close 203 | @output_format.converter(self).serialize output_read, output 204 | channel.send true 205 | rescue ex 206 | channel.send ex 207 | end 208 | 209 | run = Process.run( 210 | "jq", 211 | @args, 212 | input: input_read, 213 | output: output_write, 214 | error: error 215 | ) 216 | 217 | unless run.success? 218 | # Raise this to represent a jq error. 219 | # jq writes its errors directly to the *error* IO so no need to include a message. 220 | raise RuntimeError.new 221 | end 222 | 223 | 2.times do 224 | case v = channel.receive 225 | when Exception then raise v 226 | end 227 | end 228 | end 229 | 230 | # Parses the *input_args*, extracting `jq` arguments while leaving files 231 | private def extract_args(input_args : Array(String), output : IO) : Nil 232 | # Add color option if *output* is a tty 233 | # and the output format is JSON 234 | # (Since it will go straight to *output* and not converted) 235 | input_args.unshift "-C" if output.tty? && @output_format.json? && !input_args.includes? "-C" 236 | 237 | # If the -C option was explicitly included 238 | # and the output format is not JSON; 239 | # remove it from *input_args* to prevent 240 | # conversion errors 241 | input_args.delete("-C") if !@output_format.json? 242 | 243 | # If there are any files within the *input_args*, ignore "." as it's both a valid file and filter 244 | idx = if first_file_idx = input_args.index { |a| a != "." && File.exists? a } 245 | # extract everything else 246 | first_file_idx - 1 247 | else 248 | # otherwise just take it all 249 | -1 250 | end 251 | 252 | @args.concat input_args.delete_at 0..idx 253 | end 254 | 255 | # Extracts *arg_name* from the provided *input_args* if it exists; 256 | # concatenating the result to the internal arg array. 257 | private def consume_file_arg(input_args : Array(String), arg_name : String, count : Int32 = 2) : Nil 258 | input_args.index(arg_name).try { |idx| @args.concat input_args.delete_at idx..(idx + count) } 259 | end 260 | 261 | private def consume_file_args(input_args : Array(String), *arg_names : String, count : Int32 = 2) : Nil 262 | arg_names.each { |name| consume_file_arg input_args, name, count } 263 | end 264 | end 265 | end 266 | -------------------------------------------------------------------------------- /spec/oq_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | private SIMPLE_JSON_OBJECT = <<-JSON 4 | { 5 | "name": "Jim" 6 | } 7 | JSON 8 | 9 | private NESTED_JSON_OBJECT = <<-JSON 10 | {"foo":{"bar":{"baz":5}}} 11 | JSON 12 | 13 | private ARRAY_JSON_OBJECT = <<-JSON 14 | {"names":[1,2,3]} 15 | JSON 16 | 17 | describe OQ do 18 | describe "when given a filter file" do 19 | it "should return the correct output" do 20 | run_binary(input: SIMPLE_JSON_OBJECT, args: ["-f", "spec/assets/test_filter"]) do |output| 21 | output.should eq %("Jim"\n) 22 | end 23 | end 24 | end 25 | 26 | it "with a simple filter" do 27 | run_binary(input: SIMPLE_JSON_OBJECT, args: [".name"]) do |output| 28 | output.should eq %("Jim"\n) 29 | end 30 | end 31 | 32 | it "with a filter to get nested values" do 33 | run_binary(input: NESTED_JSON_OBJECT, args: [".foo.bar.baz"]) do |output| 34 | output.should eq "5\n" 35 | end 36 | end 37 | 38 | it "should colorize the output with the -C option" do 39 | run_binary(input: SIMPLE_JSON_OBJECT, args: ["-c", "-C", "."]) do |output| 40 | output.should start_with "\e" 41 | output.should contain %("name") 42 | output.should contain %("Jim") 43 | output.should end_with "\e[0m\n" 44 | end 45 | end 46 | 47 | describe "with a non-JSON output format" do 48 | it "should convert the JSON to that format" do 49 | run_binary(input: SIMPLE_JSON_OBJECT, args: ["-o", "yaml", "."]) do |output| 50 | output.should eq "---\nname: Jim\n" 51 | end 52 | end 53 | 54 | describe "with the -C option" do 55 | it "should remove the -C option" do 56 | run_binary(input: SIMPLE_JSON_OBJECT, args: ["-o", "yaml", "-C", "."]) do |output| 57 | output.should eq "---\nname: Jim\n" 58 | end 59 | end 60 | end 61 | end 62 | 63 | describe "files" do 64 | describe "with a file input" do 65 | it "should return the correct output" do 66 | run_binary(args: [".", "spec/assets/data1.json"]) do |output| 67 | output.should eq "#{SIMPLE_JSON_OBJECT}\n" 68 | end 69 | end 70 | end 71 | 72 | describe "with multiple JSON file input" do 73 | it "raw data" do 74 | run_binary(args: ["-c", ".", "spec/assets/data1.json", "spec/assets/data2.json"]) do |output| 75 | output.should eq %({"name":"Jim"}\n{"name":"Bob"}\n) 76 | end 77 | end 78 | 79 | it "--slurp" do 80 | run_binary(args: ["-c", "--slurp", ".", "spec/assets/data1.json", "spec/assets/data2.json"]) do |output| 81 | output.should eq %([{"name":"Jim"},{"name":"Bob"}]\n) 82 | end 83 | end 84 | end 85 | 86 | describe "with multiple non JSON file input" do 87 | it "raw data" do 88 | run_binary(args: ["-i", "yaml", "-c", ".", "spec/assets/data1.yml", "spec/assets/data2.yml"]) do |output| 89 | output.should eq %({"name":"Jim"}\n{"age":17,"name":"Fred"}\n) 90 | end 91 | end 92 | 93 | it "--slurp" do 94 | run_binary(args: ["-i", "yaml", "-c", "--slurp", ".", "spec/assets/data1.yml", "spec/assets/data2.yml"]) do |output| 95 | output.should eq %([{"name":"Jim"},{"age":17,"name":"Fred"}]\n) 96 | end 97 | end 98 | end 99 | 100 | it "with multiple --arg" do 101 | run_binary(args: ["-c", "-r", "--arg", "chart", "stolon", "--arg", "version", "1.5.10", "$version", "spec/assets/data1.json"]) do |output| 102 | output.should eq %(1.5.10\n) 103 | end 104 | end 105 | end 106 | 107 | it "should minify the output with the -c option" do 108 | run_binary(input: NESTED_JSON_OBJECT, args: ["-c", "."]) do |output| 109 | output.should eq %({"foo":{"bar":{"baz":5}}}\n) 110 | end 111 | end 112 | 113 | it "should format the output without the -c option" do 114 | run_binary(input: NESTED_JSON_OBJECT, args: ["."]) do |output| 115 | output.should eq(<<-JSON 116 | { 117 | "foo": { 118 | "bar": { 119 | "baz": 5 120 | } 121 | } 122 | }\n 123 | JSON 124 | ) 125 | end 126 | end 127 | 128 | describe "with null input option" do 129 | describe "with a scalar value" do 130 | it "should return the correct output" do 131 | run_binary(args: ["-n", "0"]) do |output| 132 | output.should eq "0\n" 133 | end 134 | end 135 | 136 | it "should return the correct output" do 137 | run_binary(args: ["--null-input", "0"]) do |output| 138 | output.should eq "0\n" 139 | end 140 | end 141 | end 142 | 143 | describe "with a JSON object string" do 144 | it "should return the correct output" do 145 | run_binary(args: ["-cn", %([{"foo":"bar"},{"foo":"baz"}])]) do |output| 146 | output.should eq %([{"foo":"bar"},{"foo":"baz"}]\n) 147 | end 148 | end 149 | end 150 | 151 | describe "with input from STDIN" do 152 | it "should return the correct output" do 153 | run_binary(input: "foo", args: ["-n", "."]) do |output| 154 | output.should eq "null\n" 155 | end 156 | end 157 | end 158 | 159 | it "should not block waiting for input" do 160 | run_binary(input: Process::Redirect::Inherit, args: ["-n", "."]) do |output| 161 | output.should eq "null\n" 162 | end 163 | end 164 | end 165 | 166 | describe "with a custom indent value with JSON" do 167 | it "should return the correct output" do 168 | run_binary(input: SIMPLE_JSON_OBJECT, args: ["--indent", "1", "."]) do |output| 169 | output.should eq %({\n "name": "Jim"\n}\n) 170 | end 171 | end 172 | end 173 | 174 | describe "when streaming input" do 175 | it "should return the correct output" do 176 | run_binary(input: %({"a": [1, 2.2, true, "abc", null]}), args: ["-nc", "--stream", "fromstream( 1|truncate_stream(inputs) | select(length>1) | .[0] |= .[1:] )"]) do |output| 177 | output.should eq %(1\n2.2\ntrue\n"abc"\nnull\n) 178 | end 179 | end 180 | end 181 | 182 | describe "when using 'input'" do 183 | it "should return the correct output" do 184 | run_binary(args: ["-cn", "-f", "spec/assets/stream-filter", "spec/assets/stream-data.json"]) do |output| 185 | output.should eq %({"possible_victim01":{"total":3,"evildoers":{"evil.com":2,"soevil.com":1}},"possible_victim02":{"total":1,"evildoers":{"bad.com":1}},"possible_victim03":{"total":1,"evildoers":{"soevil.com":1}}}\n) 186 | end 187 | end 188 | end 189 | 190 | describe "with the -L option" do 191 | it "should be passed correctly without a space" do 192 | run_binary(args: ["-n", "-L#{__DIR__}/assets", %(import "test" as test; 9 | test::increment)]) do |output| 193 | output.should eq %(10\n) 194 | end 195 | end 196 | 197 | it "should be passed correctly with a space" do 198 | run_binary(args: ["-n", "-L", "#{__DIR__}/assets", %(import "test" as test; 9 | test::increment)]) do |output| 199 | output.should eq %(10\n) 200 | end 201 | end 202 | end 203 | 204 | describe "--arg" do 205 | it "single arg" do 206 | run_binary(args: ["-cn", "--arg", "foo", "bar", %({"name":$foo})]) do |output| 207 | output.should eq %({"name":"bar"}\n) 208 | end 209 | end 210 | 211 | it "multiple arg" do 212 | run_binary(args: ["-rcn", "-r", "--arg", "chart", "stolon", "--arg", "version", "1.5.10", "$version"]) do |output| 213 | output.should eq %(1.5.10\n) 214 | end 215 | end 216 | 217 | it "different option in between args" do 218 | run_binary(args: ["-rcn", "--arg", "chart", "stolon", "--arg", "version", "1.5.10", "$version"]) do |output| 219 | output.should eq %(1.5.10\n) 220 | end 221 | end 222 | 223 | it "when the arg name matches a directory name" do 224 | run_binary(args: ["-rcn", "--arg", "spec", "dir", "$spec"]) do |output| 225 | output.should eq %(dir\n) 226 | end 227 | end 228 | end 229 | 230 | describe "with the --argjson option" do 231 | it "should be passed correctly" do 232 | run_binary(args: ["-rcn", "--argjson", "foo", "123", %({"id":$foo})]) do |output| 233 | output.should eq %({"id":123}\n) 234 | end 235 | end 236 | 237 | it "when the arg name matches a directory name" do 238 | run_binary(args: ["-rcn", "--argjson", "spec", "123", "$spec"]) do |output| 239 | output.should eq %(123\n) 240 | end 241 | end 242 | end 243 | 244 | describe "with the --slurpfile option" do 245 | it "should be passed correctly" do 246 | run_binary(args: ["-rcn", "--slurpfile", "ids", "spec/assets/raw.json", %({"ids":$ids})]) do |output| 247 | output.should eq %({"ids":[1,2,3]}\n) 248 | end 249 | end 250 | end 251 | 252 | describe "with the --rawfile option" do 253 | it "should be passed correctly" do 254 | run_binary(args: ["-rcn", "--rawfile", "ids", "spec/assets/raw.json", %({"ids":$ids})]) do |output| 255 | output.should eq %({"ids":"1\\n2\\n3\\n"}\n) 256 | end 257 | end 258 | end 259 | 260 | describe "with the --args option" do 261 | it "should be passed correctly" do 262 | run_binary(args: ["-rcn", %({"ids":$ARGS.positional}), "--args", "1", "2", "3"]) do |output| 263 | output.should eq %({"ids":["1","2","3"]}\n) 264 | end 265 | end 266 | end 267 | 268 | describe "with the --jsonargs option" do 269 | it "should be passed correctly" do 270 | run_binary(args: ["-rcn", %({"ids":$ARGS.positional}), "--jsonargs", "1", "2", "3"]) do |output| 271 | output.should eq %({"ids":[1,2,3]}\n) 272 | end 273 | end 274 | end 275 | 276 | describe "when there is a jq error" do 277 | it "should return the error and correct exit code" do 278 | run_binary(input: ARRAY_JSON_OBJECT, args: [".names | .[] | .name"], success: false) do |_, _, error| 279 | error.should eq %(jq: error (at :0): Cannot index number with string "name"\n) 280 | end 281 | end 282 | end 283 | 284 | describe "with an invalid input format" do 285 | it "should return the error and correct exit code" do 286 | run_binary(input: SIMPLE_JSON_OBJECT, args: ["-i", "foo"], success: false) do |_, _, error| 287 | error.should eq %(Invalid input format: 'foo'\n) 288 | end 289 | end 290 | end 291 | 292 | describe "with an invalid output format" do 293 | it "should return the error and correct exit code" do 294 | run_binary(input: SIMPLE_JSON_OBJECT, args: ["-o", "foo"], success: false) do |_, _, error| 295 | error.should eq %(Invalid output format: 'foo'\n) 296 | end 297 | end 298 | end 299 | end 300 | -------------------------------------------------------------------------------- /spec/converters/simple_yaml_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | # Essentially copied from the `YAML` spec, minus the `with anchors` test. 4 | # 5 | # TODO: Allow the test code to be more easily shared. 6 | describe OQ::Converters::SimpleYAML do 7 | describe ".deserialize" do 8 | describe String do 9 | describe "not blank" do 10 | it "should output correctly" do 11 | run_binary(%(--- Jim), args: ["-i", "simpleyaml", "."]) do |output| 12 | output.should eq %("Jim"\n) 13 | end 14 | end 15 | end 16 | 17 | describe "blank" do 18 | it "should output correctly" do 19 | run_binary(%(--- ), args: ["-i", "simpleyaml", "."]) do |output| 20 | output.should eq "null\n" 21 | end 22 | end 23 | end 24 | 25 | describe "with a tag" do 26 | it "should output correctly" do 27 | run_binary(%(--- !!str 0.5), args: ["-i", "simpleyaml", "."]) do |output| 28 | output.should eq %("0.5"\n) 29 | end 30 | end 31 | end 32 | 33 | describe "that is single quoted" do 34 | it "should output correctly" do 35 | run_binary(%(---\nhowever: 'foobar'), args: ["-i", "simpleyaml", "-c", "."]) do |output| 36 | output.should eq %({"however":"foobar"}\n) 37 | end 38 | end 39 | end 40 | 41 | describe "that is double quoted" do 42 | it "should output correctly" do 43 | run_binary(%(---\nhowever: "foobar"), args: ["-i", "simpleyaml", "-c", "."]) do |output| 44 | output.should eq %({"however":"foobar"}\n) 45 | end 46 | end 47 | end 48 | 49 | describe "literal block" do 50 | it "should output correctly" do 51 | run_binary(LITERAL_BLOCK, args: ["-i", "simpleyaml", "-c", "."]) do |output| 52 | output.should eq %({"literal_block":"This entire block of text will be the value of the 'literal_block' key,\\nwith line breaks being preserved.\\n\\nThe literal continues until de-dented, and the leading indentation is\\nstripped.\\n\\n Any lines that are 'more-indented' keep the rest of their indentation -\\n these lines will be indented by 4 spaces."}\n) 53 | end 54 | end 55 | end 56 | 57 | describe "folded block" do 58 | it "should output correctly" do 59 | run_binary(FOLDED_BLOCK, args: ["-i", "simpleyaml", "-c", "."]) do |output| 60 | output.should eq %({"folded_style":"This entire block of text will be the value of 'folded_style', but this time, all newlines will be replaced with a single space.\\nBlank lines, like above, are converted to a newline character.\\n\\n 'More-indented' lines keep their newlines, too -\\n this text will appear over two lines."}\n) 61 | end 62 | end 63 | end 64 | end 65 | 66 | describe Bool do 67 | it "should output correctly" do 68 | run_binary(%(--- true), args: ["-i", "simpleyaml", "."]) do |output| 69 | output.should eq "true\n" 70 | end 71 | end 72 | end 73 | 74 | describe Float do 75 | it "should output correctly" do 76 | run_binary(%(--- 10.50), args: ["-i", "simpleyaml", "."]) do |output| 77 | output.should eq "10.5\n" 78 | end 79 | end 80 | end 81 | 82 | describe Nil do 83 | it "should output correctly" do 84 | run_binary(%(--- ), args: ["-i", "simpleyaml", "."]) do |output| 85 | output.should eq "null\n" 86 | end 87 | end 88 | end 89 | 90 | describe Object do 91 | describe "a simple object" do 92 | it "should output correctly" do 93 | run_binary(%(---\nname: Jim), args: ["-i", "simpleyaml", "-c", "."]) do |output| 94 | output.should eq %({"name":"Jim"}\n) 95 | end 96 | end 97 | end 98 | 99 | describe "with spaces in the key" do 100 | it "should output correctly" do 101 | run_binary(%(---\nkey with spaces: value), args: ["-i", "simpleyaml", "-c", "."]) do |output| 102 | output.should eq %({"key with spaces":"value"}\n) 103 | end 104 | end 105 | end 106 | 107 | describe "with a quoted key key" do 108 | it "should output correctly" do 109 | run_binary(%(---\n'Keys can be quoted too.': value), args: ["-i", "simpleyaml", "-c", "."]) do |output| 110 | output.should eq %({"Keys can be quoted too.":"value"}\n) 111 | end 112 | end 113 | end 114 | 115 | describe "with nested object" do 116 | it "should output correctly" do 117 | run_binary(NESTED_OBJECT, args: ["-i", "simpleyaml", "-c", "."]) do |output| 118 | output.should eq %({"a_nested_map":{"key":"value","another_key":"Another Value","another_nested_map":{"hello":"hello"}}}\n) 119 | end 120 | end 121 | end 122 | 123 | describe "with a non string key" do 124 | it "should output correctly" do 125 | run_binary(%(---\n0.25: a float key), args: ["-i", "simpleyaml", "-c", "."]) do |output| 126 | output.should eq %({"0.25":"a float key"}\n) 127 | end 128 | end 129 | end 130 | 131 | describe "with JSON syntax" do 132 | describe "with quotes" do 133 | it "should output correctly" do 134 | run_binary(%(---\njson_seq: {"key": "value"}), args: ["-i", "simpleyaml", "-c", "."]) do |output| 135 | output.should eq %({"json_seq":{"key":"value"}}\n) 136 | end 137 | end 138 | end 139 | 140 | describe "without quotes" do 141 | it "should output correctly" do 142 | run_binary(%(---\njson_seq: {key: value}), args: ["-i", "simpleyaml", "-c", "."]) do |output| 143 | output.should eq %({"json_seq":{"key":"value"}}\n) 144 | end 145 | end 146 | end 147 | end 148 | 149 | describe "with a complex mapping key" do 150 | it "should output correctly" do 151 | run_binary(COMPLEX_MAPPING_KEY, args: ["-i", "simpleyaml", "-c", "."]) do |output| 152 | output.should eq %({"This is a key\\nthat has multiple lines\\n":"and this is its value"}\n) 153 | end 154 | end 155 | end 156 | 157 | describe "with set notation" do 158 | it "should output correctly" do 159 | run_binary(%(---\nset:\n ? item1\n ? item2), args: ["-i", "simpleyaml", "-c", "."]) do |output| 160 | output.should eq %({"set":{"item1":null,"item2":null}}\n) 161 | end 162 | end 163 | end 164 | 165 | pending "with a complex sequence key" do 166 | it "should output correctly" do 167 | run_binary(COMPLEX_SEQUENCE_KEY, args: ["-i", "simpleyaml", "-c", "."]) do |output| 168 | output.should eq %({"["Manchester United", "Real Madrid"]":["2001-01-01T00:00:00Z","2002-02-02T00:00:00Z"]}\n) 169 | end 170 | end 171 | end 172 | end 173 | 174 | describe Array do 175 | describe "with mixed/nested array values" do 176 | it "should output correctly" do 177 | run_binary(NESTED_ARRAY, args: ["-i", "simpleyaml", "-c", "."]) do |output| 178 | output.should eq %({"a_sequence":["Item 1","Item 2",0.5,"Item 4",{"key":"value","another_key":"another_value"},["This is a sequence","inside another sequence"],[["Nested sequence indicators","can be collapsed"]]]}\n) 179 | end 180 | end 181 | end 182 | 183 | describe "with JSON syntax" do 184 | describe "with quotes" do 185 | it "should output correctly" do 186 | run_binary(%(---\njson_seq: [3, 2, 1, "takeoff"]), args: ["-i", "simpleyaml", "-c", "."]) do |output| 187 | output.should eq %({"json_seq":[3,2,1,"takeoff"]}\n) 188 | end 189 | end 190 | end 191 | 192 | describe "without quotes" do 193 | it "should output correctly" do 194 | run_binary(%(---\njson_seq: [3, 2, 1, takeoff]), args: ["-i", "simpleyaml", "-c", "."]) do |output| 195 | output.should eq %({"json_seq":[3,2,1,"takeoff"]}\n) 196 | end 197 | end 198 | end 199 | end 200 | end 201 | end 202 | 203 | describe ".serialize" do 204 | describe String do 205 | describe "not blank" do 206 | it "should output correctly" do 207 | run_binary(%("Jim"), args: ["-o", "simpleyaml", "."]) do |output| 208 | output.should start_with <<-YAML 209 | --- Jim 210 | YAML 211 | end 212 | end 213 | end 214 | 215 | describe "blank" do 216 | it "should output correctly" do 217 | run_binary(%(""), args: ["-o", "simpleyaml", "."]) do |output| 218 | output.should eq(<<-YAML 219 | --- ""\n 220 | YAML 221 | ) 222 | end 223 | end 224 | end 225 | end 226 | 227 | describe Bool do 228 | it "should output correctly" do 229 | run_binary(%(true), args: ["-o", "simpleyaml", "."]) do |output| 230 | output.should start_with <<-YAML 231 | --- true 232 | YAML 233 | end 234 | end 235 | end 236 | 237 | describe Float do 238 | it "should output correctly" do 239 | run_binary(%("1.5"), args: ["-o", "simpleyaml", "."]) do |output| 240 | output.should eq(<<-YAML 241 | --- "1.5"\n 242 | YAML 243 | ) 244 | end 245 | end 246 | end 247 | 248 | describe Nil do 249 | it "should output correctly" do 250 | run_binary("null", args: ["-o", "simpleyaml", "."]) do |output| 251 | output.should start_with "---" 252 | end 253 | end 254 | end 255 | 256 | describe Array do 257 | describe "empty array on root" do 258 | it "should emit a self closing root tag" do 259 | run_binary("[]", args: ["-o", "simpleyaml", "."]) do |output| 260 | output.should eq(<<-YAML 261 | --- []\n 262 | YAML 263 | ) 264 | end 265 | end 266 | end 267 | 268 | describe "array with values on root" do 269 | it "should emit item tags for non empty values" do 270 | run_binary(%(["x",{}]), args: ["-o", "simpleyaml", "."]) do |output| 271 | output.should eq(<<-YAML 272 | --- 273 | - x 274 | - {}\n 275 | YAML 276 | ) 277 | end 278 | end 279 | end 280 | 281 | describe "object with empty array/values" do 282 | it "should emit self closing tags for each" do 283 | run_binary(%({"a":[],"b":{},"c":null}), args: ["-o", "simpleyaml", "."]) do |output| 284 | output.should start_with <<-YAML 285 | --- 286 | a: [] 287 | b: {} 288 | c: 289 | YAML 290 | end 291 | end 292 | end 293 | 294 | describe "2D array object value" do 295 | it "should emit key name tag then self closing item tag" do 296 | run_binary(%({"a":[[]]}), args: ["-o", "simpleyaml", "."]) do |output| 297 | output.should eq(<<-YAML 298 | --- 299 | a: 300 | - []\n 301 | YAML 302 | ) 303 | end 304 | end 305 | end 306 | 307 | describe "object value mixed/nested array values" do 308 | it "should emit correctly" do 309 | run_binary(%({"x":[1,[2,[3]]]}), args: ["-o", "simpleyaml", "."]) do |output| 310 | output.should eq(<<-YAML 311 | --- 312 | x: 313 | - 1 314 | - - 2 315 | - - 3\n 316 | YAML 317 | ) 318 | end 319 | end 320 | end 321 | 322 | describe "object value array primitive values" do 323 | it "should emit correctly" do 324 | run_binary(%({"x":[1,2,3]}), args: ["-o", "simpleyaml", "."]) do |output| 325 | output.should eq(<<-YAML 326 | --- 327 | x: 328 | - 1 329 | - 2 330 | - 3\n 331 | YAML 332 | ) 333 | end 334 | end 335 | end 336 | 337 | describe "when the jq filter doesn't return data" do 338 | it "should return an empty string" do 339 | run_binary(%([{"name":"foo"}]), args: ["-i", "simpleyaml", "-o", "simpleyaml", %<.[] | select(.name != "foo")>]) do |output| 340 | output.should be_empty 341 | end 342 | end 343 | end 344 | end 345 | 346 | describe Object do 347 | describe "simple key/value" do 348 | it "should output correctly" do 349 | run_binary(%({"name":"Jim"}), args: ["-o", "simpleyaml", "."]) do |output| 350 | output.should eq(<<-YAML 351 | --- 352 | name: Jim\n 353 | YAML 354 | ) 355 | end 356 | end 357 | end 358 | 359 | describe "nested object" do 360 | it "should output correctly" do 361 | run_binary(%({"name":"Jim", "city": {"street":"forbs"}}), args: ["-o", "simpleyaml", "."]) do |output| 362 | output.should eq(<<-YAML 363 | --- 364 | name: Jim 365 | city: 366 | street: forbs\n 367 | YAML 368 | ) 369 | end 370 | end 371 | end 372 | end 373 | end 374 | end 375 | -------------------------------------------------------------------------------- /spec/converters/yaml_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | LITERAL_BLOCK = <<-YAML 4 | --- 5 | literal_block: | 6 | This entire block of text will be the value of the 'literal_block' key, 7 | with line breaks being preserved. 8 | 9 | The literal continues until de-dented, and the leading indentation is 10 | stripped. 11 | 12 | Any lines that are 'more-indented' keep the rest of their indentation - 13 | these lines will be indented by 4 spaces. 14 | YAML 15 | 16 | FOLDED_BLOCK = <<-YAML 17 | folded_style: > 18 | This entire block of text will be the value of 'folded_style', but this 19 | time, all newlines will be replaced with a single space. 20 | 21 | Blank lines, like above, are converted to a newline character. 22 | 23 | 'More-indented' lines keep their newlines, too - 24 | this text will appear over two lines. 25 | YAML 26 | 27 | NESTED_OBJECT = <<-YAML 28 | a_nested_map: 29 | key: value 30 | another_key: Another Value 31 | another_nested_map: 32 | hello: hello 33 | YAML 34 | 35 | COMPLEX_MAPPING_KEY = <<-YAML 36 | ? | 37 | This is a key 38 | that has multiple lines 39 | : and this is its value 40 | YAML 41 | 42 | COMPLEX_SEQUENCE_KEY = <<-YAML 43 | ? - Manchester United 44 | - Real Madrid 45 | : [2001-01-01, 2002-02-02] 46 | YAML 47 | 48 | NESTED_ARRAY = <<-YAML 49 | a_sequence: 50 | - Item 1 51 | - Item 2 52 | - 0.5 # sequences can contain disparate types. 53 | - Item 4 54 | - key: value 55 | another_key: another_value 56 | - 57 | - This is a sequence 58 | - inside another sequence 59 | - - - Nested sequence indicators 60 | - can be collapsed 61 | YAML 62 | 63 | ANCHORS = <<-YAML 64 | base: &base 65 | name: Everyone has same name 66 | foo: &foo 67 | <<: *base 68 | age: 10 69 | bar: &bar 70 | <<: *base 71 | age: 20 72 | YAML 73 | 74 | describe OQ::Converters::YAML do 75 | describe ".deserialize" do 76 | describe String do 77 | describe "not blank" do 78 | it "should output correctly" do 79 | run_binary(%(--- Jim), args: ["-i", "yaml", "."]) do |output| 80 | output.should eq %("Jim"\n) 81 | end 82 | end 83 | end 84 | 85 | describe "blank" do 86 | it "should output correctly" do 87 | run_binary(%(--- ), args: ["-i", "yaml", "."]) do |output| 88 | output.should eq "null\n" 89 | end 90 | end 91 | end 92 | 93 | describe "with a tag" do 94 | it "should output correctly" do 95 | run_binary(%(--- !!str 0.5), args: ["-i", "yaml", "."]) do |output| 96 | output.should eq %("0.5"\n) 97 | end 98 | end 99 | end 100 | 101 | describe "that is single quoted" do 102 | it "should output correctly" do 103 | run_binary(%(---\nhowever: 'foobar'), args: ["-i", "yaml", "-c", "."]) do |output| 104 | output.should eq %({"however":"foobar"}\n) 105 | end 106 | end 107 | end 108 | 109 | describe "that is double quoted" do 110 | it "should output correctly" do 111 | run_binary(%(---\nhowever: "foobar"), args: ["-i", "yaml", "-c", "."]) do |output| 112 | output.should eq %({"however":"foobar"}\n) 113 | end 114 | end 115 | end 116 | 117 | describe "literal block" do 118 | it "should output correctly" do 119 | run_binary(LITERAL_BLOCK, args: ["-i", "yaml", "-c", "."]) do |output| 120 | output.should eq %({"literal_block":"This entire block of text will be the value of the 'literal_block' key,\\nwith line breaks being preserved.\\n\\nThe literal continues until de-dented, and the leading indentation is\\nstripped.\\n\\n Any lines that are 'more-indented' keep the rest of their indentation -\\n these lines will be indented by 4 spaces."}\n) 121 | end 122 | end 123 | end 124 | 125 | describe "folded block" do 126 | it "should output correctly" do 127 | run_binary(FOLDED_BLOCK, args: ["-i", "yaml", "-c", "."]) do |output| 128 | output.should eq %({"folded_style":"This entire block of text will be the value of 'folded_style', but this time, all newlines will be replaced with a single space.\\nBlank lines, like above, are converted to a newline character.\\n\\n 'More-indented' lines keep their newlines, too -\\n this text will appear over two lines."}\n) 129 | end 130 | end 131 | end 132 | end 133 | 134 | describe Bool do 135 | it "should output correctly" do 136 | run_binary(%(--- true), args: ["-i", "yaml", "."]) do |output| 137 | output.should eq "true\n" 138 | end 139 | end 140 | end 141 | 142 | describe Float do 143 | it "should output correctly" do 144 | run_binary(%(--- 10.50), args: ["-i", "yaml", "."]) do |output| 145 | output.should eq "10.5\n" 146 | end 147 | end 148 | end 149 | 150 | describe Nil do 151 | it "should output correctly" do 152 | run_binary(%(--- ), args: ["-i", "yaml", "."]) do |output| 153 | output.should eq "null\n" 154 | end 155 | end 156 | end 157 | 158 | describe Object do 159 | describe "a simple object" do 160 | it "should output correctly" do 161 | run_binary(%(---\nname: Jim), args: ["-i", "yaml", "-c", "."]) do |output| 162 | output.should eq %({"name":"Jim"}\n) 163 | end 164 | end 165 | end 166 | 167 | describe "with spaces in the key" do 168 | it "should output correctly" do 169 | run_binary(%(---\nkey with spaces: value), args: ["-i", "yaml", "-c", "."]) do |output| 170 | output.should eq %({"key with spaces":"value"}\n) 171 | end 172 | end 173 | end 174 | 175 | describe "with a quoted key key" do 176 | it "should output correctly" do 177 | run_binary(%(---\n'Keys can be quoted too.': value), args: ["-i", "yaml", "-c", "."]) do |output| 178 | output.should eq %({"Keys can be quoted too.":"value"}\n) 179 | end 180 | end 181 | end 182 | 183 | describe "with nested object" do 184 | it "should output correctly" do 185 | run_binary(NESTED_OBJECT, args: ["-i", "yaml", "-c", "."]) do |output| 186 | output.should eq %({"a_nested_map":{"key":"value","another_key":"Another Value","another_nested_map":{"hello":"hello"}}}\n) 187 | end 188 | end 189 | end 190 | 191 | describe "with a non string key" do 192 | it "should output correctly" do 193 | run_binary(%(---\n0.25: a float key), args: ["-i", "yaml", "-c", "."]) do |output| 194 | output.should eq %({"0.25":"a float key"}\n) 195 | end 196 | end 197 | end 198 | 199 | describe "with JSON syntax" do 200 | describe "with quotes" do 201 | it "should output correctly" do 202 | run_binary(%(---\njson_seq: {"key": "value"}), args: ["-i", "yaml", "-c", "."]) do |output| 203 | output.should eq %({"json_seq":{"key":"value"}}\n) 204 | end 205 | end 206 | end 207 | 208 | describe "without quotes" do 209 | it "should output correctly" do 210 | run_binary(%(---\njson_seq: {key: value}), args: ["-i", "yaml", "-c", "."]) do |output| 211 | output.should eq %({"json_seq":{"key":"value"}}\n) 212 | end 213 | end 214 | end 215 | end 216 | 217 | describe "with a complex mapping key" do 218 | it "should output correctly" do 219 | run_binary(COMPLEX_MAPPING_KEY, args: ["-i", "yaml", "-c", "."]) do |output| 220 | output.should eq %({"This is a key\\nthat has multiple lines\\n":"and this is its value"}\n) 221 | end 222 | end 223 | end 224 | 225 | describe "with set notation" do 226 | it "should output correctly" do 227 | run_binary(%(---\nset:\n ? item1\n ? item2), args: ["-i", "yaml", "-c", "."]) do |output| 228 | output.should eq %({"set":{"item1":null,"item2":null}}\n) 229 | end 230 | end 231 | end 232 | 233 | pending "with a complex sequence key" do 234 | it "should output correctly" do 235 | run_binary(COMPLEX_SEQUENCE_KEY, args: ["-i", "yaml", "-c", "."]) do |output| 236 | output.should eq %({"["Manchester United", "Real Madrid"]":["2001-01-01T00:00:00Z","2002-02-02T00:00:00Z"]}\n) 237 | end 238 | end 239 | end 240 | 241 | describe "with anchors" do 242 | it "should output correctly" do 243 | run_binary(ANCHORS, args: ["-i", "yaml", "-c", "."]) do |output| 244 | output.should eq %({"base":{"name":"Everyone has same name"},"foo":{"name":"Everyone has same name","age":10},"bar":{"name":"Everyone has same name","age":20}}\n) 245 | end 246 | end 247 | end 248 | end 249 | 250 | describe Array do 251 | describe "with mixed/nested array values" do 252 | it "should output correctly" do 253 | run_binary(NESTED_ARRAY, args: ["-i", "yaml", "-c", "."]) do |output| 254 | output.should eq %({"a_sequence":["Item 1","Item 2",0.5,"Item 4",{"key":"value","another_key":"another_value"},["This is a sequence","inside another sequence"],[["Nested sequence indicators","can be collapsed"]]]}\n) 255 | end 256 | end 257 | end 258 | 259 | describe "with JSON syntax" do 260 | describe "with quotes" do 261 | it "should output correctly" do 262 | run_binary(%(---\njson_seq: [3, 2, 1, "takeoff"]), args: ["-i", "yaml", "-c", "."]) do |output| 263 | output.should eq %({"json_seq":[3,2,1,"takeoff"]}\n) 264 | end 265 | end 266 | end 267 | 268 | describe "without quotes" do 269 | it "should output correctly" do 270 | run_binary(%(---\njson_seq: [3, 2, 1, takeoff]), args: ["-i", "yaml", "-c", "."]) do |output| 271 | output.should eq %({"json_seq":[3,2,1,"takeoff"]}\n) 272 | end 273 | end 274 | end 275 | end 276 | end 277 | end 278 | 279 | describe ".serialize" do 280 | describe String do 281 | describe "not blank" do 282 | it "should output correctly" do 283 | run_binary(%("Jim"), args: ["-o", "yaml", "."]) do |output| 284 | output.should start_with <<-YAML 285 | --- Jim 286 | YAML 287 | end 288 | end 289 | end 290 | 291 | describe "blank" do 292 | it "should output correctly" do 293 | run_binary(%(""), args: ["-o", "yaml", "."]) do |output| 294 | output.should eq(<<-YAML 295 | --- ""\n 296 | YAML 297 | ) 298 | end 299 | end 300 | end 301 | end 302 | 303 | describe Bool do 304 | it "should output correctly" do 305 | run_binary(%(true), args: ["-o", "yaml", "."]) do |output| 306 | output.should start_with <<-YAML 307 | --- true 308 | YAML 309 | end 310 | end 311 | end 312 | 313 | describe Float do 314 | it "should output correctly" do 315 | run_binary(%("1.5"), args: ["-o", "yaml", "."]) do |output| 316 | output.should eq(<<-YAML 317 | --- "1.5"\n 318 | YAML 319 | ) 320 | end 321 | end 322 | end 323 | 324 | describe Nil do 325 | it "should output correctly" do 326 | run_binary("null", args: ["-o", "yaml", "."]) do |output| 327 | output.should start_with "---" 328 | end 329 | end 330 | end 331 | 332 | describe Array do 333 | describe "empty array on root" do 334 | it "should emit a self closing root tag" do 335 | run_binary("[]", args: ["-o", "yaml", "."]) do |output| 336 | output.should eq(<<-YAML 337 | --- []\n 338 | YAML 339 | ) 340 | end 341 | end 342 | end 343 | 344 | describe "array with values on root" do 345 | it "should emit item tags for non empty values" do 346 | run_binary(%(["x",{}]), args: ["-o", "yaml", "."]) do |output| 347 | output.should eq(<<-YAML 348 | --- 349 | - x 350 | - {}\n 351 | YAML 352 | ) 353 | end 354 | end 355 | end 356 | 357 | describe "object with empty array/values" do 358 | it "should emit self closing tags for each" do 359 | run_binary(%({"a":[],"b":{},"c":null}), args: ["-o", "yaml", "."]) do |output| 360 | output.should start_with <<-YAML 361 | --- 362 | a: [] 363 | b: {} 364 | c: 365 | YAML 366 | end 367 | end 368 | end 369 | 370 | describe "2D array object value" do 371 | it "should emit key name tag then self closing item tag" do 372 | run_binary(%({"a":[[]]}), args: ["-o", "yaml", "."]) do |output| 373 | output.should eq(<<-YAML 374 | --- 375 | a: 376 | - []\n 377 | YAML 378 | ) 379 | end 380 | end 381 | end 382 | 383 | describe "object value mixed/nested array values" do 384 | it "should emit correctly" do 385 | run_binary(%({"x":[1,[2,[3]]]}), args: ["-o", "yaml", "."]) do |output| 386 | output.should eq(<<-YAML 387 | --- 388 | x: 389 | - 1 390 | - - 2 391 | - - 3\n 392 | YAML 393 | ) 394 | end 395 | end 396 | end 397 | 398 | describe "object value array primitive values" do 399 | it "should emit correctly" do 400 | run_binary(%({"x":[1,2,3]}), args: ["-o", "yaml", "."]) do |output| 401 | output.should eq(<<-YAML 402 | --- 403 | x: 404 | - 1 405 | - 2 406 | - 3\n 407 | YAML 408 | ) 409 | end 410 | end 411 | end 412 | 413 | describe "when the jq filter doesn't return data" do 414 | it "should return an empty string" do 415 | run_binary(%([{"name":"foo"}]), args: ["-i", "yaml", "-o", "yaml", %<.[] | select(.name != "foo")>]) do |output| 416 | output.should be_empty 417 | end 418 | end 419 | end 420 | end 421 | 422 | describe Object do 423 | describe "simple key/value" do 424 | it "should output correctly" do 425 | run_binary(%({"name":"Jim"}), args: ["-o", "yaml", "."]) do |output| 426 | output.should eq(<<-YAML 427 | --- 428 | name: Jim\n 429 | YAML 430 | ) 431 | end 432 | end 433 | end 434 | 435 | describe "nested object" do 436 | it "should output correctly" do 437 | run_binary(%({"name":"Jim", "city": {"street":"forbs"}}), args: ["-o", "yaml", "."]) do |output| 438 | output.should eq(<<-YAML 439 | --- 440 | name: Jim 441 | city: 442 | street: forbs\n 443 | YAML 444 | ) 445 | end 446 | end 447 | end 448 | end 449 | end 450 | end 451 | -------------------------------------------------------------------------------- /spec/converters/xml_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | WITH_WHITESPACE = <<-XML 4 | 5 | 0 6 | 0 7 | 0 8 | 0 9 | -1 10 | 0 11 | 12 | XML 13 | 14 | XML_SCALAR_ARRAY = <<-XML 15 | 16 | 17 | 1 18 | 2 19 | 3 20 | 21 | XML 22 | 23 | XML_SCALAR_ARRAY_WITH_ATTRIBUTE = <<-XML 24 | 25 | 26 | 1 27 | 2 28 | 3 29 | 30 | XML 31 | 32 | XML_CDATA = <<-XML 33 | Some Description]]> 34 | XML 35 | 36 | XML_OBJECT_ARRAY = <<-XML 37 | 38 | 39 | 40 | 0 41 | 0 42 | 0 43 | 0 44 | -1 45 | 0 46 | 47 | 48 | 0 49 | 1 50 | 0 51 | 0 52 | -1 53 | 0 54 | 55 | 56 | XML 57 | 58 | XML_NESTED_OBJECT_ARRAY = <<-XML 59 | 60 | 61 | 62 | 63 | 64 | cubsfantony 65 | 848 66 | 67 | Visa/MasterCard, Money Order/Cashiers Checks, Personal Checks, See item description for payment methods accepted 68 | 69 | 70 | 71 | ct-inc 72 | 403 73 | 74 | Visa/MasterCard, Discover, Money Order/Cashiers Checks, Personal Checks, See item description for payment methods accepted 75 | 76 | 77 | XML 78 | 79 | XML_INLINE_ARRAY = <<-XML 80 |
81 | E. F. Codd 82 | Robert S. Arnold 83 | Jean-Marc Cadiou 84 | Chin-Liang Chang 85 | Nick Roussopoulos 86 | RENDEZVOUS Version 1: An Experimental English Language Query Formulation System for Casual Users of Relational Data Bases. 87 | IBM Research Report 88 | RJ2144 89 | January 90 | 1978 91 | db/labs/ibm/RJ2144.html 92 | ibmTR/rj2144.pdf 93 |
94 | XML 95 | 96 | XML_INLINE_ARRAY_WITHIN_ARRAY = <<-XML 97 | 98 |
99 | 1997 100 | db/labs/dec/SRC1997-018.html 101 | http://www.mcjones.org/System_R/SQL_Reunion_95/ 102 |
103 |
104 | db/labs/gte/TR-0263-08-94-165.html 105 | 1994 106 |
107 |
108 | XML 109 | 110 | XML_DOCTYPE = <<-XML 111 | 112 | 113 | 114 | 115 | Kurt P. Brown 116 | PRPL: A Database Workload Specification Language, v1.3. 117 | 1992 118 | Univ. of Wisconsin-Madison 119 | 120 | 121 | XML 122 | 123 | XML_ATTRIBUTE_IN_ARRAY = <<-XML 124 | 125 | 126 | 80000 127 | full-time 128 | 129 | 130 | full-time 131 | 132 | 133 | XML 134 | 135 | XML_ATTRIBUTE_IN_ARRAY_ROOT_ELEMENT = <<-XML 136 | 137 | 138 | 139 | 140 | Kurt P. Brown 141 | 142 | 143 | Tolga Yurek 144 | 145 | 146 | XML 147 | 148 | XML_ALL_EMPTY = <<-XML 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | XML 157 | 158 | XML_NAMESPACE_ARRAY = <<-XML 159 | 160 | 161 | 1 162 | 2 163 | 3 164 | 165 | XML 166 | 167 | XML_NAMESPACE_ARRAY_SCALAR_VALUE_PREFIX = <<-XML 168 | 169 | 170 | 1 171 | 2 172 | 3 173 | 174 | XML 175 | 176 | XML_NAMESPACE_PREFIXES = <<-XML 177 | 178 | 179 | foo 180 | bar 181 | 182 | XML 183 | 184 | XML_NESTED_NAMESPACES = <<-XML 185 | 186 | 187 | herp 188 | 189 | 190 | 191 | 192 | 193 | 194 | XML 195 | 196 | describe OQ::Converters::XML do 197 | describe ".deserialize" do 198 | # See https://www.xml.com/pub/a/2006/05/31/converting-between-xml-and-json.html 199 | describe "conventions" do 200 | describe "an empty element" do 201 | it "self closing" do 202 | run_binary("", args: ["-i", "xml", "-c", "."]) do |output| 203 | output.should eq %({"e":null}\n) 204 | end 205 | end 206 | 207 | it "non self closing" do 208 | run_binary("", args: ["-i", "xml", "-c", "."]) do |output| 209 | output.should eq %({"e":null}\n) 210 | end 211 | end 212 | end 213 | 214 | it "an element with pure text content" do 215 | run_binary("text", args: ["-i", "xml", "-c", "."]) do |output| 216 | output.should eq %({"e":"text"}\n) 217 | end 218 | end 219 | 220 | it "an empty element with attributes" do 221 | run_binary(%(), args: ["-i", "xml", "-c", "."]) do |output| 222 | output.should eq %({"e":{"@name":"value"}}\n) 223 | end 224 | end 225 | 226 | it "an element with pure text content and attributes" do 227 | run_binary(%(text), args: ["-i", "xml", "-c", "."]) do |output| 228 | output.should eq %({"e":{"@name":"value","#text":"text"}}\n) 229 | end 230 | end 231 | 232 | it "an element containing elements with different names" do 233 | run_binary(%( text text ), args: ["-i", "xml", "-c", "."]) do |output| 234 | output.should eq %({"e":{"a":"text","b":"text"}}\n) 235 | end 236 | end 237 | 238 | it "an element containing elements with identical names" do 239 | run_binary(%( text text ), args: ["-i", "xml", "-c", "."]) do |output| 240 | output.should eq %({"e":{"a":["text","text"]}}\n) 241 | end 242 | end 243 | 244 | it "an element containing elements and contiguous text" do 245 | run_binary(%(texttext), args: ["-i", "xml", "-c", "."]) do |output| 246 | output.should eq %({"e":{"#text":"text","a":"text"}}\n) 247 | end 248 | end 249 | end 250 | 251 | describe "should raise if invalid" do 252 | it "should output correctly" do 253 | run_binary(%(Fred), args: ["-i", "xml", "-c", "."]) do |output| 262 | output.should eq %({"person":"Fred"}\n) 263 | end 264 | end 265 | 266 | describe "that has only empty children elements" do 267 | it "should output an object with null values" do 268 | run_binary(XML_ALL_EMPTY, args: ["-i", "xml", "-c", "."]) do |output| 269 | output.should eq %({"root":{"one":" ","two":"\\n ","three":null,"four":null}}\n) 270 | end 271 | end 272 | end 273 | 274 | it "with whitespace" do 275 | run_binary(WITH_WHITESPACE, args: ["-i", "xml", "-c", "."]) do |output| 276 | output.should eq %({"item":{"flagID":"0","itemID":"0","locationID":"0","ownerID":"0","quantity":"-1","typeID":"0"}}\n) 277 | end 278 | end 279 | 280 | it "with the prolog" do 281 | run_binary(%(0), args: ["-i", "xml", "-c", "."]) do |output| 282 | output.should eq %({"item":{"typeID":"0"}}\n) 283 | end 284 | end 285 | 286 | it "a simple object" do 287 | run_binary(%(JaneDoe), args: ["-i", "xml", "-c", "."]) do |output| 288 | output.should eq %({"person":{"firstname":"Jane","lastname":"Doe"}}\n) 289 | end 290 | end 291 | 292 | it "attributes" do 293 | run_binary(%(JaneDoe), args: ["-i", "xml", "-c", "."]) do |output| 294 | output.should eq %({"person":{"@id":"1","@foo":"bar","firstname":"Jane","lastname":"Doe"}}\n) 295 | end 296 | end 297 | 298 | it "nested objects" do 299 | run_binary(%(JaneDoe15061
123 Foo Street
), args: ["-i", "xml", "-c", "."]) do |output| 300 | output.should eq %({"person":{"firstname":"Jane","lastname":"Doe","location":{"zip":"15061","address":"123 Foo Street"}}}\n) 301 | end 302 | end 303 | 304 | it "complex object" do 305 | run_binary(%(24), args: ["-i", "xml", "-c", "."]) do |output| 306 | output.should eq %({"root":{"x":{"@a":"1","a":"2"},"y":{"@b":"3","#text":"4"}}}\n) 307 | end 308 | end 309 | 310 | it "with mixed content" do 311 | run_binary(%(xz), args: ["-i", "xml", "-c", ".root"]) do |output| 312 | output.should eq %({"#text":"x","y":"z"}\n) 313 | end 314 | end 315 | 316 | it "with an inline array" do 317 | run_binary(XML_INLINE_ARRAY, args: ["-i", "xml", "-c", "."]) do |output| 318 | output.should eq %({"article":{"@key":"tr/ibm/RJ2144","author":["E. F. Codd","Robert S. Arnold","Jean-Marc Cadiou","Chin-Liang Chang","Nick Roussopoulos"],"title":"RENDEZVOUS Version 1: An Experimental English Language Query Formulation System for Casual Users of Relational Data Bases.","journal":"IBM Research Report","volume":"RJ2144","month":"January","year":"1978","ee":"db/labs/ibm/RJ2144.html","cdrom":"ibmTR/rj2144.pdf"}}\n) 319 | end 320 | end 321 | 322 | it "with a doctype" do 323 | run_binary(XML_DOCTYPE, args: ["-i", "xml", "-c", "."]) do |output| 324 | output.should eq %({"dblp":{"mastersthesis":{"@key":"ms/Brown92","author":"Kurt P. Brown","title":"PRPL: A Database Workload Specification Language, v1.3.","year":"1992","school":"Univ. of Wisconsin-Madison"}}}\n) 325 | end 326 | end 327 | 328 | it "with CDATA" do 329 | run_binary(XML_CDATA, args: ["-i", "xml", "-c", "."]) do |output| 330 | output.should eq %({"desc":"Some Description"}\n) 331 | end 332 | end 333 | 334 | it "with a prefixed key" do 335 | run_binary(%(bar), args: ["-i", "xml", "-c", "."]) do |output| 336 | output.should eq %({"a:foo":"bar"}\n) 337 | end 338 | end 339 | 340 | describe "with namespaces" do 341 | describe "without --xmlns" do 342 | it "retains prefixes but strips namespace declarations of a prefixed namespace" do 343 | run_binary(%(bar), args: ["-i", "xml", "-c", "."]) do |output| 344 | output.should eq %({"a:foo":"bar"}\n) 345 | end 346 | end 347 | 348 | it "does not add pefix if none was already present but strips namespace declarations" do 349 | run_binary(%(bar), args: ["-i", "xml", "-c", "."]) do |output| 350 | output.should eq %({"foo":"bar"}\n) 351 | end 352 | end 353 | 354 | it "adds namespace attribute properties only to declaring element and handles differentiating prefixed elements" do 355 | run_binary(XML_NESTED_NAMESPACES, args: ["-i", "xml", "-c", "."]) do |output| 356 | output.should eq %({"root":{"a:foo":"herp","foo":{"bar":{"baz":null}}}}\n) 357 | end 358 | end 359 | 360 | it "retains prefixes of scalar value elements" do 361 | run_binary(XML_NAMESPACE_PREFIXES, args: ["-i", "xml", "-c", "."]) do |output| 362 | output.should eq %({"root":{"foo":"foo","a:bar":"bar"}}\n) 363 | end 364 | end 365 | 366 | describe "with --xml-namespace-alias" do 367 | it "should error" do 368 | run_binary(%(bar), args: ["-i", "xml", "-c", "--xml-namespace-alias", "aa=https://a-namespace", "."], success: false) do |_, _, error| 369 | error.should start_with "oq error:" 370 | end 371 | end 372 | end 373 | end 374 | 375 | describe "with --xmlns" do 376 | it "creates a namespace attribute property" do 377 | run_binary(%(bar), args: ["-i", "xml", "-c", "--xmlns", "."]) do |output| 378 | output.should eq %({"a:foo":{"@xmlns:a":"http://www.w3.org/1999/xhtml","#text":"bar"}}\n) 379 | end 380 | end 381 | 382 | it "does not add pefix if none was already present and creates multiple namespace attribute properties" do 383 | run_binary(%(bar), args: ["-i", "xml", "-c", "--xmlns", "."]) do |output| 384 | output.should eq %({"foo":{"@xmlns":"urn:oasis:names:tc:SAML:2.0:metadata","@xmlns:a":"http://www.w3.org/1999/xhtml","#text":"bar"}}\n) 385 | end 386 | end 387 | 388 | it "treats prefixed & unprefixed elements as unique elements" do 389 | run_binary(XML_NESTED_NAMESPACES, args: ["-i", "xml", "-c", "--xmlns", "."]) do |output| 390 | output.should eq %({"root":{"@xmlns:a":"https://a","@xmlns":"https://b","a:foo":"herp","foo":{"bar":{"@xmlns":"https://c","baz":{"@xmlns":"https://d"}}}}}\n) 391 | end 392 | end 393 | 394 | it "retains prefixes of scalar value elements and adds a namespace attribute property" do 395 | run_binary(XML_NAMESPACE_PREFIXES, args: ["-i", "xml", "-c", "--xmlns", "."]) do |output| 396 | output.should eq %({"root":{"@xmlns:a":"https://a","foo":"foo","a:bar":"bar"}}\n) 397 | end 398 | end 399 | 400 | describe "with --xml-namespace-alias" do 401 | it "normalizes the provided namespace" do 402 | run_binary(%(bar), args: ["-i", "xml", "-c", "--xmlns", "--xml-namespace-alias", "aa=https://a-namespace", "."]) do |output| 403 | output.should eq %({"aa:foo":{"@xmlns:aa":"https://a-namespace","#text":"bar"}}\n) 404 | end 405 | end 406 | 407 | it "normalizes the default namespace" do 408 | run_binary(%(bar), args: ["-i", "xml", "-c", "--xmlns", "--xml-namespace-alias", "aa=https://a-namespace", "."]) do |output| 409 | output.should eq %({"aa:foo":{"@xmlns:aa":"https://a-namespace","#text":"bar"}}\n) 410 | end 411 | end 412 | 413 | it "normalizes multiple namespaces" do 414 | run_binary(XML_NESTED_NAMESPACES, args: ["-i", "xml", "-c", "--xmlns", "--xml-namespace-alias", "=https://a", "--xml-namespace-alias", "bb=https://b", "."]) do |output| 415 | output.should eq %({"bb:root":{"@xmlns":"https://a","@xmlns:bb":"https://b","foo":"herp","bb:foo":{"bar":{"@xmlns":"https://c","baz":{"@xmlns":"https://d"}}}}}\n) 416 | end 417 | end 418 | end 419 | end 420 | end 421 | end 422 | 423 | describe Array do 424 | it "of scalar values" do 425 | run_binary(XML_SCALAR_ARRAY, args: ["-i", "xml", "-c", "."]) do |output| 426 | output.should eq %({"items":{"number":["1","2","3"]}}\n) 427 | end 428 | end 429 | 430 | it "of scalar values with attribute" do 431 | run_binary(XML_SCALAR_ARRAY_WITH_ATTRIBUTE, args: ["-i", "xml", "-c", "."]) do |output| 432 | output.should eq %({"items":{"number":["1","2",{"@foo":"bar","#text":"3"}]}}\n) 433 | end 434 | end 435 | 436 | describe "of objects" do 437 | describe "with no nested objects" do 438 | it "should output correctly" do 439 | run_binary(XML_OBJECT_ARRAY, args: ["-i", "xml", "-c", "."]) do |output| 440 | output.should eq %({"items":{"item":[{"flagID":"0","itemID":"0","locationID":"0","ownerID":"0","quantity":"-1","typeID":"0"},{"flagID":"0","itemID":"1","locationID":"0","ownerID":"0","quantity":"-1","typeID":"0"}]}}\n) 441 | end 442 | end 443 | end 444 | 445 | describe "with an inline array" do 446 | it "should output correctly" do 447 | run_binary(XML_INLINE_ARRAY_WITHIN_ARRAY, args: ["-i", "xml", "-c", "."]) do |output| 448 | output.should eq %({"articles":{"article":[{"@key":"tr/dec/SRC1997-018","year":"1997","ee":["db/labs/dec/SRC1997-018.html","http://www.mcjones.org/System_R/SQL_Reunion_95/"]},{"@key":"tr/gte/TR-0263-08-94-165","ee":"db/labs/gte/TR-0263-08-94-165.html","year":"1994"}]}}\n) 449 | end 450 | end 451 | end 452 | 453 | describe "with nested objects" do 454 | it "should output correctly" do 455 | run_binary(XML_NESTED_OBJECT_ARRAY, args: ["-i", "xml", "-c", ".root.listing"]) do |output| 456 | output.should eq %([{"seller_info":{"seller_name":" cubsfantony","seller_rating":" 848"},"payment_types":"Visa/MasterCard, Money Order/Cashiers Checks, Personal Checks, See item description for payment methods accepted"},{"seller_info":{"seller_name":" ct-inc","seller_rating":" 403"},"payment_types":"Visa/MasterCard, Discover, Money Order/Cashiers Checks, Personal Checks, See item description for payment methods accepted"}]\n) 457 | end 458 | end 459 | end 460 | end 461 | 462 | describe "with object that has an attribute" do 463 | it "should output correctly" do 464 | run_binary(XML_ATTRIBUTE_IN_ARRAY, args: ["-i", "xml", "-c", "."]) do |output| 465 | output.should eq %({"jobs":{"ad":[{"salary":{"@currency":"CAD","#text":"80000"},"working_hours":"full-time"},{"working_hours":"full-time"}]}}\n) 466 | end 467 | end 468 | end 469 | 470 | describe "where array object element has an attribute" do 471 | it "should output correctly" do 472 | run_binary(XML_ATTRIBUTE_IN_ARRAY_ROOT_ELEMENT, args: ["-i", "xml", "-c", "."]) do |output| 473 | output.should eq %({"dblp":{"mastersthesis":[{"@key":"ms/Brown92","author":"Kurt P. Brown"},{"@key":"ms/Yurek97","author":"Tolga Yurek"}]}}\n) 474 | end 475 | end 476 | end 477 | 478 | describe "with namespaces" do 479 | describe "without --xmlns" do 480 | it "treats prefixed & unprefixed elements as unique elements" do 481 | run_binary(XML_NAMESPACE_ARRAY, args: ["-i", "xml", "-c", "."]) do |output| 482 | output.should eq %({"items":{"n:number":["1","2"],"number":"3"}}\n) 483 | end 484 | end 485 | 486 | it "ignores the namespace declaration" do 487 | run_binary(XML_NAMESPACE_ARRAY_SCALAR_VALUE_PREFIX, args: ["-i", "xml", "-c", "."]) do |output| 488 | output.should eq %({"items":{"n:number":["1","2","3"]}}\n) 489 | end 490 | end 491 | end 492 | 493 | describe "with --xmlns" do 494 | it "treats prefixed & unprefixed elements as unique elements, adding namespace attribute property as needed" do 495 | run_binary(XML_NAMESPACE_ARRAY, args: ["-i", "xml", "-c", "--xmlns", "."]) do |output| 496 | output.should eq %({"items":{"@xmlns:n":"http://n","n:number":["1","2"],"number":{"@xmlns":"http://default","#text":"3"}}}\n) 497 | end 498 | end 499 | 500 | it "expands the scalar value to include a namespace attribute property" do 501 | run_binary(XML_NAMESPACE_ARRAY_SCALAR_VALUE_PREFIX, args: ["-i", "xml", "-c", "--xmlns", "."]) do |output| 502 | output.should eq %({"items":{"@xmlns:n":"http://n","n:number":["1","2",{"@xmlns":"http://default","#text":"3"}]}}\n) 503 | end 504 | end 505 | 506 | describe "with --xml-namespace-alias" do 507 | it do 508 | run_binary(XML_NAMESPACE_ARRAY, args: ["-i", "xml", "-c", "--xmlns", "--xml-namespace-alias", "num=http://n", "."]) do |output| 509 | output.should eq %({"items":{"@xmlns:num":"http://n","num:number":["1","2"],"number":{"@xmlns":"http://default","#text":"3"}}}\n) 510 | end 511 | end 512 | 513 | it do 514 | run_binary(XML_NAMESPACE_ARRAY, args: ["-i", "xml", "-c", "--xmlns", "--xml-namespace-alias", "=http://n", "--xml-namespace-alias", "d=http://default", "."]) do |output| 515 | output.should eq %({"items":{"@xmlns":"http://n","number":["1","2"],"d:number":{"@xmlns:d":"http://default","#text":"3"}}}\n) 516 | end 517 | end 518 | end 519 | end 520 | end 521 | 522 | describe "with a single element" do 523 | it "without --xml-force-array" do 524 | run_binary(%(), args: ["-i", "xml", "-c", "."]) do |output| 525 | output.should eq %({"foo":{"item":null}}\n) 526 | end 527 | end 528 | 529 | describe "with --xml-force-array" do 530 | it "force parses it as an array" do 531 | run_binary(%(), args: ["-i", "xml", "--xml-force-array", "item", "-c", "."]) do |output| 532 | output.should eq %({"foo":{"item":[null]}}\n) 533 | end 534 | end 535 | 536 | it "with an attribute" do 537 | run_binary(%(), args: ["-i", "xml", "--xml-force-array", "item", "-c", "."]) do |output| 538 | output.should eq %({"foo":{"item":[{"@id":"1"}]}}\n) 539 | end 540 | end 541 | 542 | it "with a namespace" do 543 | run_binary(%(), args: ["-i", "xml", "--xmlns", "--xml-force-array", "item", "-c", "."]) do |output| 544 | output.should eq %({"foo":{"item":[{"@xmlns":"https://ns"}]}}\n) 545 | end 546 | end 547 | 548 | it "with an aliased namespace" do 549 | run_binary(%(), args: ["-i", "xml", "--xmlns", "--xml-force-array", "item:item", "--xml-namespace-alias", "item=https://ns", "-c", "."]) do |output| 550 | output.should eq %({"foo":{"item:item":[{"@xmlns:item":"https://ns"}]}}\n) 551 | end 552 | end 553 | end 554 | end 555 | end 556 | end 557 | 558 | describe ".serialize" do 559 | it "allows not emitting the xml prolog" do 560 | run_binary("1", args: ["-o", "xml", "--no-prolog", "."]) do |output| 561 | output.should eq(<<-XML 562 | 1\n 563 | XML 564 | ) 565 | end 566 | end 567 | 568 | describe "allows setting the root element" do 569 | describe "to another string" do 570 | it "should use the provided name" do 571 | run_binary("1", args: ["-o", "xml", "--xml-root", "foo", "."]) do |output| 572 | output.should eq(<<-XML 573 | 574 | 1\n 575 | XML 576 | ) 577 | end 578 | end 579 | end 580 | 581 | describe "to an empty string" do 582 | it "should not be emitted" do 583 | run_binary("1", args: ["-o", "xml", "--xml-root", "", "."]) do |output| 584 | output.should eq(<<-XML 585 | 586 | 1 587 | XML 588 | ) 589 | end 590 | end 591 | end 592 | end 593 | 594 | describe "it allows changing the array item name" do 595 | describe "with a single nesting level" do 596 | it "should emit item tags for non empty values" do 597 | run_binary(%(["x",{}]), args: ["-o", "xml", "--xml-item", "foo", "."]) do |output| 598 | output.should eq(<<-XML 599 | 600 | 601 | x 602 | 603 | \n 604 | XML 605 | ) 606 | end 607 | end 608 | end 609 | 610 | describe "with a larger nesting level" do 611 | it "should emit item tags for non empty values" do 612 | run_binary(%({"a":[[]]}), args: ["-o", "xml", "--xml-item", "foo", "."]) do |output| 613 | output.should eq(<<-XML 614 | 615 | 616 | 617 | \n 618 | XML 619 | ) 620 | end 621 | end 622 | end 623 | end 624 | 625 | describe "it allows changing the indent" do 626 | describe "more spaces" do 627 | it "should emit the extra spaces" do 628 | run_binary(%({"name": "Jim", "age": 12}), args: ["-o", "xml", "--indent", "4", "."]) do |output| 629 | output.should eq(<<-XML 630 | 631 | 632 | Jim 633 | 12 634 | \n 635 | XML 636 | ) 637 | end 638 | end 639 | end 640 | 641 | describe "to tabs" do 642 | it "should emit the indent as tabs" do 643 | run_binary(%({"name": "Jim", "age": 12}), args: ["-o", "xml", "--indent", "3", "--tab", "."]) do |output| 644 | output.should eq(<<-XML 645 | 646 | 647 | \t\t\tJim 648 | \t\t\t12 649 | \n 650 | XML 651 | ) 652 | end 653 | end 654 | end 655 | end 656 | 657 | describe String do 658 | describe "not blank" do 659 | it "should output correctly" do 660 | run_binary(%("Jim"), args: ["-o", "xml", "."]) do |output| 661 | output.should eq(<<-XML 662 | 663 | Jim\n 664 | XML 665 | ) 666 | end 667 | end 668 | end 669 | 670 | describe "blank" do 671 | it "should output correctly" do 672 | run_binary(%(""), args: ["-o", "xml", "."]) do |output| 673 | output.should eq(<<-XML 674 | 675 | \n 676 | XML 677 | ) 678 | end 679 | end 680 | end 681 | 682 | describe "with HTML content" do 683 | it "should escape the HTMl content" do 684 | run_binary(%({"x":"

Hello World!

"}), args: ["-o", "xml", "."]) do |output| 685 | output.should eq(<<-XML 686 | 687 | 688 | <p>Hello World!</p> 689 | \n 690 | XML 691 | ) 692 | end 693 | end 694 | 695 | it "should be wrapped in CDATA if the json key starts with '!'" do 696 | run_binary(%({"!x":"

Hello World!

"}), args: ["-o", "xml", "."]) do |output| 697 | output.should eq(<<-XML 698 | 699 | 700 | Hello World!

]]>
701 |
\n 702 | XML 703 | ) 704 | end 705 | end 706 | 707 | it "should produce an empty CDATA if the json key starts with '!' and the value is null" do 708 | run_binary(%({"!x":null}), args: ["-o", "xml", "."]) do |output| 709 | output.should eq(<<-XML 710 | 711 | 712 | 713 | \n 714 | XML 715 | ) 716 | end 717 | end 718 | end 719 | end 720 | 721 | describe Bool do 722 | it "should output correctly" do 723 | run_binary(%(true), args: ["-o", "xml", "."]) do |output| 724 | output.should eq(<<-XML 725 | 726 | true\n 727 | XML 728 | ) 729 | end 730 | end 731 | end 732 | 733 | describe Float do 734 | it "should output correctly" do 735 | run_binary(%("1.5"), args: ["-o", "xml", "."]) do |output| 736 | output.should eq(<<-XML 737 | 738 | 1.5\n 739 | XML 740 | ) 741 | end 742 | end 743 | end 744 | 745 | describe Nil do 746 | it "should output correctly" do 747 | run_binary("null", args: ["-o", "xml", "."]) do |output| 748 | output.should eq(<<-XML 749 | 750 | \n 751 | XML 752 | ) 753 | end 754 | end 755 | end 756 | 757 | describe Array do 758 | describe "empty array on root" do 759 | it "should emit a self closing root tag" do 760 | run_binary("[]", args: ["-o", "xml", "."]) do |output| 761 | output.should eq(<<-XML 762 | 763 | \n 764 | XML 765 | ) 766 | end 767 | end 768 | end 769 | 770 | describe "array with values on root" do 771 | it "should emit item tags for non empty values" do 772 | run_binary(%(["x",{}]), args: ["-o", "xml", "."]) do |output| 773 | output.should eq(<<-XML 774 | 775 | 776 | x 777 | 778 | \n 779 | XML 780 | ) 781 | end 782 | end 783 | end 784 | 785 | describe "object with empty array/values" do 786 | it "should emit self closing tags for each" do 787 | run_binary(%({"a":[],"b":{},"c":null}), args: ["-o", "xml", "."]) do |output| 788 | output.should eq(<<-XML 789 | 790 | 791 | 792 | 793 | \n 794 | XML 795 | ) 796 | end 797 | end 798 | end 799 | 800 | describe "2D array object value" do 801 | it "should emit key name tag then self closing item tag" do 802 | run_binary(%({"a":[[]]}), args: ["-o", "xml", "."]) do |output| 803 | output.should eq(<<-XML 804 | 805 | 806 |
807 |
\n 808 | XML 809 | ) 810 | end 811 | end 812 | end 813 | 814 | it "object value mixed/nested array values" do 815 | run_binary(%({"x":[1,[2,[3]]]}), args: ["-o", "xml", "."]) do |output| 816 | output.should eq(<<-XML 817 | 818 | 819 | 1 820 | 821 | 2 822 | 823 | 3 824 | 825 | 826 | \n 827 | XML 828 | ) 829 | end 830 | end 831 | 832 | it "object value array primitive values" do 833 | run_binary(%({"x":[1,2,3]}), args: ["-o", "xml", "."]) do |output| 834 | output.should eq(<<-XML 835 | 836 | 837 | 1 838 | 2 839 | 3 840 | \n 841 | XML 842 | ) 843 | end 844 | end 845 | end 846 | 847 | describe Object do 848 | it "simple key/value" do 849 | run_binary(%({"name":"Jim"}), args: ["-o", "xml", "."]) do |output| 850 | output.should eq(<<-XML 851 | 852 | 853 | Jim 854 | \n 855 | XML 856 | ) 857 | end 858 | end 859 | 860 | it "nested object" do 861 | run_binary(%({"name":"Jim", "city": {"street":"forbs"}}), args: ["-o", "xml", "."]) do |output| 862 | output.should eq(<<-XML 863 | 864 | 865 | Jim 866 | 867 | forbs 868 | 869 | \n 870 | XML 871 | ) 872 | end 873 | end 874 | 875 | it "with an attribute" do 876 | run_binary(%({"name":"Jim", "city": {"@street":"forbs"}}), args: ["-o", "xml", "."]) do |output| 877 | output.should eq(<<-XML 878 | 879 | 880 | Jim 881 | 882 | \n 883 | XML 884 | ) 885 | end 886 | end 887 | 888 | it "with an attribute and #text" do 889 | run_binary(%({"name":"Jim", "city": {"@street":"forbs", "#text": "Atlantic"}}), args: ["-o", "xml", "."]) do |output| 890 | output.should eq(<<-XML 891 | 892 | 893 | Jim 894 | Atlantic 895 | \n 896 | XML 897 | ) 898 | end 899 | end 900 | 901 | it "with attributes" do 902 | run_binary(%({"name":"Jim", "city": {"@street":"forbs", "@post": 123}}), args: ["-o", "xml", "."]) do |output| 903 | output.should eq(<<-XML 904 | 905 | 906 | Jim 907 | 908 | \n 909 | XML 910 | ) 911 | end 912 | end 913 | 914 | it "with attributes and #text" do 915 | run_binary(%({"name":"Jim", "city": {"@street":"forbs", "@post": 123, "#text": "Atlantic"}}), args: ["-o", "xml", "."]) do |output| 916 | output.should eq(<<-XML 917 | 918 | 919 | Jim 920 | Atlantic 921 | \n 922 | XML 923 | ) 924 | end 925 | end 926 | 927 | it "with a prefixed key" do 928 | run_binary(%({"foo:name":"Jim"}), args: ["-o", "xml", "."]) do |output| 929 | output.should eq(<<-XML 930 | 931 | 932 | Jim 933 | \n 934 | XML 935 | ) 936 | end 937 | end 938 | end 939 | end 940 | end 941 | --------------------------------------------------------------------------------