diff --git a/README.md b/README.md index 9b3b2cc..55e9369 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Built with Crystal](https://img.shields.io/badge/built%20with-crystal-000000.svg?style=flat-square)](https://crystal-lang.org/) [![Build Status](https://travis-ci.org/Blacksmoke16/oq.svg?branch=master)](https://travis-ci.org/Blacksmoke16/oq) [![Latest release](https://img.shields.io/github/release/Blacksmoke16/oq.svg?style=flat-square)](https://github.com/Blacksmoke16/oq/releases) -[![Snap Status](https://build.snapcraft.io/badge/Blacksmoke16/oq.svg)](https://build.snapcraft.io/user/Blacksmoke16/oq) +[![oq](https://snapcraft.io/omni-q/badge.svg)](https://snapcraft.io/omni-q) A performant, portable `jq` wrapper thats facilitates the consumption and output of formats other than JSON; using `jq` filters to transform the data. @@ -33,7 +33,7 @@ Usage: oq [--help] [oq-arguments] [jq-arguments] jq_filter [file [files...]] --help Show this help message. -i FORMAT, --input FORMAT Format of the input data. Supported formats: json, yaml, xml. -o FORMAT, --output FORMAT Format of the output data. Supported formats: json, yaml, xml. - --xml-root=ROOT Name of the root XML element if converting to XML. + --xml-root ROOT Name of the root XML element if converting to XML. ``` ## Roadmap diff --git a/shard.yml b/shard.yml index 9bf37b6..87e8cbc 100644 --- a/shard.yml +++ b/shard.yml @@ -1,4 +1,4 @@ -name: omni-q +name: oq description: | A performant, and portable jq wrapper thats facilitates the consumption and output of formats other than JSON; using jq filters to transform the data. diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index cc4393c..6226ec7 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -4,7 +4,7 @@ summary: A performant, and portable jq wrapper to support formats other than JSO description: | A performant, and portable jq wrapper thats facilitates the consumption and output of formats other than JSON; using jq filters to transform the data. -grade: devel +grade: stable confinement: strict base: core18 diff --git a/spec/oq_spec.cr b/spec/oq_spec.cr index 73f4386..4154a88 100644 --- a/spec/oq_spec.cr +++ b/spec/oq_spec.cr @@ -12,23 +12,23 @@ JSON describe Oq do describe "when given a filter file" do - it "returns the correct output" do + it "should return the correct output" do run_binary(input: SIMPLE_JSON_OBJECT, args: ["-f", "spec/assets/test_filter"]) do |output| - output.should eq "\"Jim\"\n" + output.should eq %("Jim"\n) end end end describe "with a simple filter" do - it "returns the correct output" do + it "should return the correct output" do run_binary(input: SIMPLE_JSON_OBJECT, args: [".name"]) do |output| - output.should eq "\"Jim\"\n" + output.should eq %("Jim"\n) end end end describe "with a filter to get nested values" do - it "returns the correct output" do + it "should return the correct output" do run_binary(input: NESTED_JSON_OBJECT, args: [".foo.bar.baz"]) do |output| output.should eq "5\n" end @@ -36,8 +36,8 @@ describe Oq do end describe "with a filter to get nested values and YAML input" do - it "returns the correct output" do - run_binary(input: "---\nfoo:\n bar:\n baz: 5", args: [".foo.bar.baz", "-i", "yaml"]) do |output| + it "should return the correct output" do + run_binary(input: "---\nfoo:\n bar:\n baz: 5", args: ["-i", "yaml", ".foo.bar.baz"]) do |output| output.should eq "5\n" end end @@ -45,7 +45,7 @@ describe Oq do describe "with YAML output" do it "should return the correct output" do - run_binary(input: NESTED_JSON_OBJECT, args: [".", "-o", "yaml"]) do |output| + run_binary(input: NESTED_JSON_OBJECT, args: ["-o", "yaml", "."]) do |output| output.should eq "---\nfoo:\n bar:\n baz: 5\n" end end @@ -53,7 +53,7 @@ describe Oq do describe "with XML output" do it "should return the correct output" do - run_binary(input: NESTED_JSON_OBJECT, args: [".", "-o", "xml"]) do |output| + run_binary(input: NESTED_JSON_OBJECT, args: ["-o", "xml", "."]) do |output| output.should eq "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n <foo>\n <bar>\n <baz>5</baz>\n </bar>\n </foo>\n</root>\n" end end @@ -61,7 +61,7 @@ describe Oq do describe "with YAML input" do it "should return the correct output" do - run_binary(input: "---\nfoo:\n bar:\n baz: 5\n", args: [".", "-c", "-i", "yaml"]) do |output| + run_binary(input: "---\nfoo:\n bar:\n baz: 5\n", args: ["-c", "-i", "yaml", "."]) do |output| output.should eq "#{NESTED_JSON_OBJECT}\n" end end @@ -77,7 +77,7 @@ describe Oq do describe "with multiple JSON file input" do it "should return the correct output" do - run_binary(input: "", args: [".", "-c", "spec/assets/data1.json", "spec/assets/data2.json"]) do |output| + run_binary(input: "", args: ["-c", ".", "spec/assets/data1.json", "spec/assets/data2.json"]) do |output| output.should eq %({"name":"Jim"}\n{"name":"Bob"}\n) end end @@ -85,7 +85,7 @@ describe Oq do describe "with multiple JSON file input and slurp" do it "should return the correct output" do - run_binary(input: "", args: [".", "-c", "--slurp", "spec/assets/data1.json", "spec/assets/data2.json"]) do |output| + run_binary(input: "", args: ["-c", "--slurp", ".", "spec/assets/data1.json", "spec/assets/data2.json"]) do |output| output.should eq %([{"name":"Jim"},{"name":"Bob"}]\n) end end @@ -93,7 +93,7 @@ describe Oq do describe "with multiple YAML file input" do it "should return the correct output" do - run_binary(input: "", args: [".", "-c", "-i", "yaml", "spec/assets/data1.yml", "spec/assets/data2.yml"]) do |output| + run_binary(input: "", args: ["-c", "-i", "yaml", ".", "spec/assets/data1.yml", "spec/assets/data2.yml"]) do |output| output.should eq %({"name":"Jim"}\n{"age":17}\n) end end @@ -101,7 +101,7 @@ describe Oq do describe "with multiple YAML file input and slurp" do it "should return the correct output" do - run_binary(input: "", args: [".", "-c", "-i", "yaml", "-s", "spec/assets/data1.yml", "spec/assets/data2.yml"]) do |output| + run_binary(input: "", args: ["-c", "-i", "yaml", "-s", ".", "spec/assets/data1.yml", "spec/assets/data2.yml"]) do |output| output.should eq %([{"name":"Jim"},{"age":17}]\n) end end @@ -109,7 +109,7 @@ describe Oq do describe "with STDIN YAML input and slurp" do it "should return the correct output" do - run_binary(input: "---\nname: Jim", args: [".", "-c", "-i", "yaml", "-s"]) do |output| + run_binary(input: "---\nname: Jim", args: ["-c", "-i", "yaml", "-s", "."]) do |output| output.should eq %([{"name":"Jim"}]\n) end end @@ -117,7 +117,7 @@ describe Oq do describe "with YAML input and XML output" do it "should convert between formats" do - run_binary(input: "---\nfoo:\n bar:\n baz: 5\n", args: [".", "-i", "yaml", "-o", "xml"]) do |output| + run_binary(input: "---\nfoo:\n bar:\n baz: 5\n", args: ["-i", "yaml", "-o", "xml", "."]) do |output| output.should eq "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n <foo>\n <bar>\n <baz>5</baz>\n </bar>\n </foo>\n</root>\n" end end @@ -125,15 +125,15 @@ describe Oq do describe "with raw output" do it "should return the correct output" do - run_binary(input: "", args: [".", "-R", "-o", "yaml", "spec/assets/data1.json"]) do |output| + run_binary(input: "", args: ["-R", "-o", "yaml", ".", "spec/assets/data1.json"]) do |output| output.should eq %(--- '{"name": "Jim"}'\n) end end end describe "with the -c options" do - it "should compact the output" do - run_binary(input: NESTED_JSON_OBJECT, args: [".", "-c"]) do |output| + it "should return the correct output" do + run_binary(input: NESTED_JSON_OBJECT, args: ["-c", "."]) do |output| output.should eq %({"foo":{"bar":{"baz":5}}}\n) end end @@ -147,10 +147,83 @@ describe Oq do end end + describe "with null input option" do + describe "with a scalar value" do + it "should return the correct output" do + run_binary(input: nil, args: ["-n", "0"]) do |output| + output.should eq "0\n" + end + end + + it "should return the correct output" do + run_binary(input: nil, args: ["--null-input", "0"]) do |output| + output.should eq "0\n" + end + end + end + + describe "with a JSON object string" do + it "should return the correct output" do + run_binary(input: nil, args: ["-cn", %([{"foo":"bar"},{"foo":"baz"}])]) do |output| + output.should eq %([{"foo":"bar"},{"foo":"baz"}]\n) + end + end + end + + describe "with input from STDIN" do + it "should return the correct output" do + run_binary(input: "foo", args: ["-n"]) do |output| + output.should eq "null\n" + end + end + end + end + + describe "with a custom indent value with JSON" do + it "should return the correct output" do + run_binary(input: SIMPLE_JSON_OBJECT, args: ["--indent", "1", "."]) do |output| + output.should eq %({\n "name": "Jim"\n}\n) + end + end + end + + describe "with the --tab option" do + it "should return the correct output" do + run_binary(input: SIMPLE_JSON_OBJECT, args: ["--tab", "."]) do |output| + output.should eq %({\n\t"name": "Jim"\n}\n) + end + end + end + + describe "with a custom indent value with XML" do + it "should return the correct output" do + run_binary(input: SIMPLE_JSON_OBJECT, args: ["--indent", "3", "-o", "xml", "."]) do |output| + output.should eq "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n <name>Jim</name>\n</root>\n" + end + end + end + + describe "with a custom XML root" do + it "should return the correct output" do + run_binary(input: SIMPLE_JSON_OBJECT, args: ["--xml-root", "friends", "-o", "xml", "."]) do |output| + output.should eq "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<friends>\n <name>Jim</name>\n</friends>\n" + end + end + end + + describe "when streaming input" do + it "should return the correct output" do + 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| + output.should eq %(1\n2.2\ntrue\n"abc"\nnull\n) + end + end + end + describe "with XML input" do it "should return not implemented" do - run_binary(input: "", args: [".", "-i", "xml"]) do |output| + run_binary(input: "", args: ["-i", "xml", "."]) do |output, status| output.should eq "Not Implemented\n" + status.exit_code.should eq 1 end end end diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr index 36f18e8..da270cc 100644 --- a/spec/spec_helper.cr +++ b/spec/spec_helper.cr @@ -11,11 +11,10 @@ def assert_builder_output(expected : String, &block : XML::Builder -> Nil) : Nil end # Runs the the binary with the given *name* and *args*. -def run_binary(input : String, name : String = "bin/oq", args : Array(String) = [] of String, &block : String -> Nil) +def run_binary(input : String?, name : String = "bin/oq", args : Array(String) = [] of String, &block : String, Process::Status -> Nil) buffer = IO::Memory.new - input = IO::Memory.new input - Process.run(name, args, error: buffer, output: buffer, input: input) - yield buffer.to_s - buffer.close - input.close + in = IO::Memory.new + in << input if input + status = Process.run(name, args, error: buffer, output: buffer, input: in.rewind) + yield buffer.to_s, status end diff --git a/spec/to_xml_spec.cr b/spec/to_xml_spec.cr index f29ceb8..14b2fa1 100644 --- a/spec/to_xml_spec.cr +++ b/spec/to_xml_spec.cr @@ -122,6 +122,14 @@ describe "#to_xml" do end end + describe "with multiple attribute objects" do + it "should convert correctly" do + assert_builder_output("<name>Jim</name><age unit=\"years\" some_key=\"-1\">12</age>") do |b| + {"name" => "Jim", "age" => {"@unit" => "years", "@some_key" => -1, "#text" => 12}}.to_xml b + end + end + end + describe "with a nested attribute object" do it "should convert correctly" do assert_builder_output("<name>Jim</name><age><unit>years</unit>12</age>") do |b| @@ -179,5 +187,13 @@ describe "#to_xml" do end end end + + describe "with multiple attribute objects" do + it "should convert correctly" do + assert_builder_output("<name>Jim</name><age unit=\"years\" some_key=\"-1\">12</age>") do |b| + {"name": "Jim", "age": {"@unit": "years", "@some_key": -1, "#text": 12}}.to_xml b + end + end + end end end diff --git a/src/oq.cr b/src/oq.cr index cfbd75b..c5a83a8 100644 --- a/src/oq.cr +++ b/src/oq.cr @@ -3,7 +3,7 @@ require "yaml" require "./to_xml" -# A performant, and portable jq wrapper to support formats other than JSON. +# A performant and portable jq wrapper to support formats other than JSON. module Oq # The support formats that can be converted to/from. enum Format @@ -27,31 +27,52 @@ module Oq # The root of the XML document when transcoding to XML. property xml_root : String = "root" + # The number of spaces to use for indentation. + property indent : Int32 = 2 + + # :nodoc: + property tab : Bool = false + # :nodoc: - property slurp : Bool = false + property null_input : Bool = false # Consume the input, convert the input to JSON if needed, pass the input/args to `jq`, then convert the output if needed. def process input = IO::Memory.new output = IO::Memory.new - err = IO::Memory.new + error = IO::Memory.new + + ARGV.replace ARGV - @args # Shift off the filter from ARGV @args << ARGV.shift unless ARGV.empty? - case @input_format - when .json? then input << ARGF.gets_to_end - when .yaml? - ARGV.empty? ? (input << YAML.parse(STDIN).to_json) : (ARGV.join('\n', input) { |f, io| io << YAML.parse(File.open(f)).to_json }) + if !@null_input + case @input_format + when .json? then IO.copy(ARGF, input) + when .yaml? + ARGV.empty? ? (input << YAML.parse(STDIN).to_json) : (ARGV.join('\n', input) { |f, io| io << YAML.parse(File.open(f)).to_json }) + else + STDERR.puts "Not Implemented" + exit(1) + end + + input.rewind else - puts "Not Implemented" - exit(1) + @args = @args | ARGV end - run = Process.run("jq", args, input: input.rewind, output: output, error: err) + run = parallel( + Process.run("jq", args, input: input, output: output, error: error) + ) + + unless run[0].success? + if output.empty? && @null_input + puts "null" + exit + end - unless run.success? - puts err.to_s + STDERR.puts error.to_s exit(1) end @@ -66,7 +87,7 @@ module Oq case @output_format when .json? then print io when .yaml? then print JSON.parse(io).to_yaml - when .xml? then print JSON.parse(io).to_xml root: @xml_root + when .xml? then print JSON.parse(io).to_xml root: @xml_root, indent: (@tab ? "\t" : " ")*@indent end end end diff --git a/src/oq_cli.cr b/src/oq_cli.cr index 8cafbd4..7a9d7f5 100644 --- a/src/oq_cli.cr +++ b/src/oq_cli.cr @@ -8,13 +8,17 @@ module Oq OptionParser.parse! do |parser| parser.banner = "Usage: oq [--help] [oq-arguments] [jq-arguments] jq_filter [file [files...]]" parser.on("--help", "Show this help message.") { puts parser; exit } - parser.on("-i FORMAT", "--input FORMAT", "Format of the input data. Supported formats: #{Format.names.map(&.downcase).join(", ")}.") { |format| (f = Format.parse?(format)) ? processor.input_format = f : (puts "Invalid input format: '#{format}'"; exit(1)) } - parser.on("-o FORMAT", "--output FORMAT", "Format of the output data. Supported formats: #{Format.names.map(&.downcase).join(", ")}.") { |format| (f = Format.parse?(format)) ? processor.output_format = f : (puts "Invalid output format: '#{format}'"; exit(1)) } - parser.on("--xml-root=ROOT", "Name of the root XML element if converting to XML.") { |r| processor.xml_root = r } - parser.on("-s", "--slurp", "Read (slurp) all inputs into an array then apply filter to it.") { processor.slurp = true; processor.args << "-s" } + parser.on("-i FORMAT", "--input FORMAT", "Format of the input data. Supported formats: json, yaml.") { |format| (f = Format.parse?(format)) ? processor.input_format = f : (puts "Invalid input format: '#{format}'"; exit(1)) } + parser.on("-o FORMAT", "--output FORMAT", "Format of the output data. Supported formats: json, yaml, xml.") { |format| (f = Format.parse?(format)) ? processor.output_format = f : (puts "Invalid output format: '#{format}'"; exit(1)) } + parser.on("--xml-root ROOT", "Name of the root XML element if converting to XML.") { |r| processor.xml_root = r } + parser.on("--indent NUMBER", "Use the given number of spaces for indentation (JSON/XML only).") { |n| processor.indent = n.to_i; processor.args << "--indent"; processor.args << n } parser.invalid_option do |flag| + case flag + when "-n", "--null-input" then processor.null_input = true + when "--tab" then processor.tab = true + end + processor.args << flag - ARGV.delete flag end end diff --git a/src/to_xml.cr b/src/to_xml.cr index fe63a43..5e7d87a 100644 --- a/src/to_xml.cr +++ b/src/to_xml.cr @@ -3,14 +3,14 @@ require "yaml" require "xml" class Object - def to_xml(*, root : String = "root") : String + def to_xml(*, root : String = "root", indent : String = " ") : String String.build do |str| - to_xml str, root: root + to_xml str, root: root, indent: indent end end - def to_xml(io : IO, *, root : String?) : Nil - XML.build(io, indent: " ", encoding: "utf-8") do |builder| + def to_xml(io : IO, *, root : String?, indent : String) : Nil + XML.build(io, indent: indent, encoding: "utf-8") do |builder| builder.element root do to_xml builder end