Skip to content

Commit

Permalink
Merge pull request #106 from davishmcclurg/cli
Browse files Browse the repository at this point in the history
Add CLI
  • Loading branch information
davishmcclurg committed May 5, 2022
2 parents 9300715 + 2391daa commit 8ea8ab1
Show file tree
Hide file tree
Showing 4 changed files with 291 additions and 1 deletion.
7 changes: 6 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,9 @@ jobs:
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true
- run: bundle exec rake test
- run: |
mkdir -p tmp/gems
gem build json_schemer.gemspec
gem install --local --ignore-dependencies --no-document --install-dir tmp/gems json_schemer-*.gem
rm json_schemer-*.gem
bin/rake test
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,30 @@ JSONSchemer.schema(
)
```

## CLI

The `json_schemer` executable takes a JSON schema file as the first argument followed by one or more JSON data files to validate. If there are any validation errors, it outputs them and returns an error code.

Validation errors are output as single-line JSON objects. The `--errors` option can be used to limit the number of errors returned or prevent output entirely (and fail fast).

The schema or data can also be read from stdin using `-`.

```
% json_schemer --help
Usage:
json_schemer [options] <schema> <data>...
json_schemer [options] <schema> -
json_schemer [options] - <data>...
json_schemer -h | --help
json_schemer --version
Options:
-e, --errors MAX Maximum number of errors to output
Use "0" to validate with no output
-h, --help Show help
-v, --version Show version
```

## Development

After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
Expand Down
62 changes: 62 additions & 0 deletions exe/json_schemer
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#!/usr/bin/env ruby

require 'json'
require 'optparse'
require 'pathname'
require 'json_schemer'

parser = OptionParser.new('Usage:', 32, ' ')
parser.separator(" #{parser.program_name} [options] <schema> <data>...")
parser.separator(" #{parser.program_name} [options] <schema> -")
parser.separator(" #{parser.program_name} [options] - <data>...")
parser.separator(" #{parser.program_name} -h | --help")
parser.separator(" #{parser.program_name} --version")
parser.separator('')
parser.separator('Options:')
parser.on('-e', '--errors MAX', Integer, 'Maximum number of errors to output', 'Use "0" to validate with no output')
parser.on_tail('-h', '--help', 'Show help')
parser.on_tail('-v', '--version', 'Show version')

options = {}
parser.parse!(:into => options)

if options[:help]
$stdout.puts(parser)
exit
end

if options[:version]
$stdout.puts("#{parser.program_name} #{JSONSchemer::VERSION}")
exit
end

if ARGV.size == 0
$stderr.puts("#{parser.program_name}: no schema or data")
exit(false)
end

if ARGV.size == 1
$stderr.puts("#{parser.program_name}: no data")
exit(false)
end

if ARGV.count('-') > 1
$stderr.puts("#{parser.program_name}: multiple stdin")
exit(false)
end

errors = 0
schema = ARGF.file.is_a?(File) ? Pathname.new(ARGF.file.path) : ARGF.file.read
schemer = JSONSchemer.schema(schema)

while ARGV.any?
data = JSON.parse(ARGF.skip.file.read)
schemer.validate(data).each do |error|
exit(false) if options[:errors] == 0
errors += 1
$stdout.puts(JSON.generate(error))
exit(false) if options[:errors] == errors
end
end

exit(errors.zero?)
199 changes: 199 additions & 0 deletions test/exe_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
require 'test_helper'
require 'open3'

class ExeTest < Minitest::Test
GEM_PATH = File.join(__dir__, '..', 'tmp', 'gems')
CMD = File.join(GEM_PATH, 'bin', 'json_schemer')
SCHEMA1 = File.join(__dir__, 'schemas', 'schema1.json')
VALID = { 'id' => 1, 'a' => 'valid' }
INVALID1 = { 'a' => 'invalid' }
INVALID2 = { 'id' => 1 }
INVALID3 = { 'id' => 1, 'a' => -1 }
INVALID4 = { 'id' => 'invalid', 'a' => 'valid' }
INVALID5 = { 'x' => 'invalid' }

def test_help
stdout, stderr, status = exe('-h')
assert_predicate(status, :success?)
assert_empty(stderr)
assert_includes(stdout, 'json_schemer [options]')
assert_includes(stdout, '-e, --errors MAX')
assert_includes(stdout, '-h, --help')
assert_includes(stdout, '-v, --version')
end

def test_version
stdout, stderr, status = exe('--version')
assert_predicate(status, :success?)
assert_empty(stderr)
assert_includes(stdout, JSONSchemer::VERSION)
end

def test_errors
stdout, stderr, status = exe
refute_predicate(status, :success?)
assert_includes(stderr, 'json_schemer: no schema or data')
assert_empty(stdout)

stdout, stderr, status = exe(SCHEMA1)
refute_predicate(status, :success?)
assert_includes(stderr, 'json_schemer: no data')
assert_empty(stdout)

stdout, stderr, status = exe('-', SCHEMA1, '-')
refute_predicate(status, :success?)
assert_includes(stderr, 'json_schemer: multiple stdin')
assert_empty(stdout)
end

def test_success
tmp_json(VALID) do |path|
stdout, stderr, status = exe(SCHEMA1, path)
assert_predicate(status, :success?)
assert_empty(stderr)
assert_empty(stdout)

stdout, stderr, status = exe('--errors', '0', SCHEMA1, path)
assert_predicate(status, :success?)
assert_empty(stderr)
assert_empty(stdout)

stdout, stderr, status = exe('--errors', '1', SCHEMA1, path)
assert_predicate(status, :success?)
assert_empty(stderr)
assert_empty(stdout)
end
end

def test_error_output
stdout, stderr, status = tmp_json(VALID, INVALID1, INVALID2, INVALID3, INVALID4, INVALID5) do |*paths|
exe(SCHEMA1, *paths)
end
refute_predicate(status, :success?)
assert_empty(stderr)
errors = stdout.each_line("\n", :chomp => true).map do |line|
JSON.parse(line).select { |key, _| ['data', 'type', 'details'].include?(key) }
end
assert_equal(6, errors.size)
assert_includes(errors, 'data' => INVALID1, 'type' => 'required', 'details' => { 'missing_keys' => ['id'] })
assert_includes(errors, 'data' => INVALID2, 'type' => 'required', 'details' => { 'missing_keys' => ['a'] })
assert_includes(errors, 'data' => INVALID3['a'], 'type' => 'string')
assert_includes(errors, 'data' => INVALID4['id'], 'type' => 'integer')
assert_includes(errors, 'data' => INVALID5, 'type' => 'required', 'details' => { 'missing_keys' => ['id'] })
assert_includes(errors, 'data' => INVALID5, 'type' => 'required', 'details' => { 'missing_keys' => ['a'] })
end

def test_max_errors
tmp_json(INVALID1, INVALID2, INVALID3, INVALID4, INVALID5) do |*paths|
stdout, stderr, status = exe('-e0', SCHEMA1, *paths)
refute_predicate(status, :success?)
assert_empty(stderr)
assert_empty(stdout)

stdout, stderr, status = exe('--errors', '0', SCHEMA1, *paths)
refute_predicate(status, :success?)
assert_empty(stderr)
assert_empty(stdout)

stdout, stderr, status = exe('--errors', '1', SCHEMA1, *paths)
refute_predicate(status, :success?)
assert_empty(stderr)
assert_equal(1, stdout.split("\n").size)

stdout, stderr, status = exe('--errors', '2', SCHEMA1, *paths)
refute_predicate(status, :success?)
assert_empty(stderr)
assert_equal(2, stdout.split("\n").size)

stdout, stderr, status = exe('-e2', SCHEMA1, *paths)
refute_predicate(status, :success?)
assert_empty(stderr)
assert_equal(2, stdout.split("\n").size)

stdout, stderr, status = exe('--errors', '10', SCHEMA1, *paths)
refute_predicate(status, :success?)
assert_empty(stderr)
assert_equal(6, stdout.split("\n").size)
end
end

def test_stdin
schema = {
'type' => 'object',
'properties' => {
'id' => {
'type' => 'integer'
}
}
}
valid_data = { 'id' => 1 }
invalid_data = { 'id' => 'invalid' }

tmp_json(schema, valid_data, invalid_data) do |schema_path, valid_path, invalid_path|
stdout, stderr, status = exe('-', valid_path, :stdin_data => JSON.generate(schema))
assert_predicate(status, :success?)
assert_empty(stderr)
assert_empty(stdout)

stdout, stderr, status = exe('-', valid_path, invalid_path, :stdin_data => JSON.generate(schema))
refute_predicate(status, :success?)
assert_empty(stderr)
refute_empty(stdout)

stdout, stderr, status = exe(schema_path, valid_path, '-', :stdin_data => JSON.generate(valid_data))
assert_predicate(status, :success?)
assert_empty(stderr)
assert_empty(stdout)

stdout, stderr, status = exe(schema_path, valid_path, '-', :stdin_data => JSON.generate(invalid_data))
refute_predicate(status, :success?)
assert_empty(stderr)
refute_empty(stdout)

stdout, stderr, status = exe('-e0', schema_path, invalid_path, '-', :stdin_data => JSON.generate(valid_data))
refute_predicate(status, :success?)
assert_empty(stderr)
assert_empty(stdout)

stdout, stderr, status = exe(schema_path, '-', valid_path, :stdin_data => JSON.generate(valid_data))
assert_predicate(status, :success?)
assert_empty(stderr)
assert_empty(stdout)

stdout, stderr, status = exe(schema_path, '-', valid_path, :stdin_data => JSON.generate(invalid_data))
refute_predicate(status, :success?)
assert_empty(stderr)
refute_empty(stdout)

stdout, stderr, status = exe('-e0', schema_path, '-', invalid_path, :stdin_data => JSON.generate(valid_data))
refute_predicate(status, :success?)
assert_empty(stderr)
assert_empty(stdout)
end
end

private

def exe(*args, **kwargs)
env = {
'GEM_HOME' => Gem.dir,
'GEM_PATH' => [GEM_PATH, *Gem.path].uniq.join(File::PATH_SEPARATOR),
'GEM_SPEC_CACHE' => Gem.spec_cache_dir,
'RUBYOPT' => nil # prevent bundler/setup
}
Open3.capture3(env, CMD, *args, **kwargs)
end

def tmp_json(*json)
files = json.map do |data, index|
file = Tempfile.new(['data', '.json'])
file.sync = true
file.write(JSON.generate(data))
file
end
yield(*files.map(&:path))
ensure
files.each(&:close)
files.each(&:unlink)
end
end

0 comments on commit 8ea8ab1

Please sign in to comment.