├── 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 | [](https://crystal-lang.org/)
4 | [](https://github.com/blacksmoke16/oq/actions?query=workflow%3ACI)
5 | [](https://github.com/blacksmoke16/oq/releases)
6 | [](https://snapcraft.io/oq)
7 | [](https://aur.archlinux.org/packages/oq/)
8 | [](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(%(JaneDoe15061123 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 |
--------------------------------------------------------------------------------