jddf 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7cc8ef91dfd4d35ae749703e5d5b18c15c0645e37bb6c459a63b154582815a5c
4
- data.tar.gz: 19793c4423eb19de83ee3fc46100bdcb7d2eb86a492995e498df9b274362f262
3
+ metadata.gz: b488c9c91ad6f8eed43e3295b1a7bfbce20985d3aee9217eb2c9defc5f737e8c
4
+ data.tar.gz: 42676f592a08589204ccbb03aa68118714c60c10b0415a0a194cee9b65a7e228
5
5
  SHA512:
6
- metadata.gz: dd744ebb3fdd5276e880026858bf34db51ce8a6b7a2c344f1b8d4d725724a38bed741dd16a96df5eb3afc496a0a7958382b365107ed22e7d3ce8dbc1b1b00817
7
- data.tar.gz: 6fab08eedbc0091ed390ad1102afda5cd00ddc553f5d7fd2d8a5e31eb9776c75554f66c4ac5853e588fc30c00a23b5c853befb0c88c96ca68dcd1889823f013a
6
+ metadata.gz: c9a69812ceae8889294f9b6adc0848d14b7f66511145156af42a34fe7e9f1de5aa678b684372249d8cef6a643b48f4b7cc3c9b8a341f3d960bb449593785cf4c
7
+ data.tar.gz: 0e04a88ceac3add797277390fcffed2256229abef37f68cb0515291f85096ff9475a27c6372ec9c267c90976aabef96768cc550afa81e945ebffb8e5f3cc843b
@@ -0,0 +1,3 @@
1
+ [submodule "jddf-spec"]
2
+ path = jddf-spec
3
+ url = git@github.com:jddf/spec.git
@@ -0,0 +1,29 @@
1
+ Metrics/AbcSize:
2
+ Enabled: false
3
+
4
+ Metrics/MethodLength:
5
+ Enabled: false
6
+
7
+ Metrics/CyclomaticComplexity:
8
+ Enabled: false
9
+
10
+ Metrics/BlockLength:
11
+ Enabled: false
12
+
13
+ Metrics/ModuleLength:
14
+ Enabled: false
15
+
16
+ Metrics/PerceivedComplexity:
17
+ Enabled: false
18
+
19
+ Lint/ShadowingOuterLocalVariable:
20
+ Enabled: false
21
+
22
+ Metrics/ClassLength:
23
+ Enabled: false
24
+
25
+ Metrics/BlockNesting:
26
+ Enabled: false
27
+
28
+ Style/Next:
29
+ Enabled: false
data/Gemfile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source "https://rubygems.org"
2
4
 
3
5
  git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
@@ -1,12 +1,18 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- jddf (0.1.0)
4
+ jddf (0.2.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
8
8
  specs:
9
+ ast (2.4.0)
9
10
  diff-lcs (1.3)
11
+ jaro_winkler (1.5.3)
12
+ parallel (1.18.0)
13
+ parser (2.6.5.0)
14
+ ast (~> 2.4.0)
15
+ rainbow (3.0.0)
10
16
  rake (10.5.0)
11
17
  rspec (3.8.0)
12
18
  rspec-core (~> 3.8.0)
@@ -21,6 +27,15 @@ GEM
21
27
  diff-lcs (>= 1.2.0, < 2.0)
22
28
  rspec-support (~> 3.8.0)
23
29
  rspec-support (3.8.3)
30
+ rubocop (0.75.0)
31
+ jaro_winkler (~> 1.5.1)
32
+ parallel (~> 1.10)
33
+ parser (>= 2.6)
34
+ rainbow (>= 2.2.2, < 4.0)
35
+ ruby-progressbar (~> 1.7)
36
+ unicode-display_width (>= 1.4.0, < 1.7)
37
+ ruby-progressbar (1.10.1)
38
+ unicode-display_width (1.6.0)
24
39
 
25
40
  PLATFORMS
26
41
  ruby
@@ -30,6 +45,7 @@ DEPENDENCIES
30
45
  jddf!
31
46
  rake (~> 10.0)
32
47
  rspec (~> 3.0)
48
+ rubocop (~> 0.75.0)
33
49
 
34
50
  BUNDLED WITH
35
51
  1.17.2
data/README.md CHANGED
@@ -1,39 +1,106 @@
1
- # Jddf
1
+ # jddf-ruby [![Gem Version][badge]][rubygems]
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/jddf`. To experiment with that code, run `bin/console` for an interactive prompt.
3
+ > Documentation on rubydoc.info: https://www.rubydoc.info/github/jddf/jddf-ruby
4
4
 
5
- TODO: Delete this and the text above, and describe your gem
5
+ This gem is a Ruby implementation of [JSON Data Definition Format][jddf], a
6
+ schema language for JSON. You can use this gem to:
6
7
 
7
- ## Installation
8
+ 1. Validate input data against a schema,
9
+ 2. Get a list of validation errors from that input data, or
10
+ 3. Build your own tooling on top of JSON Data Definition Format
8
11
 
9
- Add this line to your application's Gemfile:
12
+ [jddf]: https://jddf.io
13
+ [badge]: https://badge.fury.io/rb/jddf.svg
14
+ [rubygems]: https://rubygems.org/gems/jddf
10
15
 
11
- ```ruby
12
- gem 'jddf'
13
- ```
16
+ ## Installing
14
17
 
15
- And then execute:
18
+ You can install this gem by running:
16
19
 
17
- $ bundle
20
+ ```bash
21
+ gem install jddf
22
+ ```
18
23
 
19
- Or install it yourself as:
24
+ Or if you're using Bundler:
20
25
 
21
- $ gem install jddf
26
+ ```ruby
27
+ gem 'jddf'
28
+ ```
22
29
 
23
30
  ## Usage
24
31
 
25
- TODO: Write usage instructions here
32
+ The two most important classes offered by the `JDDF` module are:
26
33
 
27
- ## Development
34
+ * `Schema`, which represents a JDDF schema,
35
+ * `Validator`, which can validate a `Schema` against any parsed JSON data, and
36
+ * `ValidationError`, which represents a single validation problem with the
37
+ input. `Validator#validate` returns an array of these.
28
38
 
29
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
39
+ Here's a working example:
30
40
 
31
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
32
-
33
- ## Contributing
34
-
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/jddf.
36
-
37
- ## License
38
-
39
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
41
+ ```ruby
42
+ require 'jddf'
43
+
44
+ # In this example, we're passing in a Hash directly into Schema#from_json, but
45
+ # this type of Hash is exactly what JSON#parse returns.
46
+ schema = JDDF::Schema.from_json({
47
+ 'properties' => {
48
+ 'name' => { 'type' => 'string' },
49
+ 'age' => { 'type' => 'uint32' },
50
+ 'phones' => {
51
+ 'elements' => { 'type' => 'string' }
52
+ }
53
+ }
54
+ })
55
+
56
+ # Like before, in order to keep things simple we're construct raw Ruby values
57
+ # here. But you can also get this sort of data by parsing JSON using the
58
+ # standard library's JSON#parse.
59
+ #
60
+ # This input data is perfect. It satisfies all the schema requirements.
61
+ input_ok = {
62
+ 'name' => 'John Doe',
63
+ 'age' => 43,
64
+ 'phones' => [
65
+ '+44 1234567',
66
+ '+44 2345678'
67
+ ]
68
+ }
69
+
70
+ # This input data has problems. "name" is missing, "age" has the wrong type,
71
+ # and "phones[1]" has the wrong type.
72
+ input_bad = {
73
+ 'age' => '43',
74
+ 'phones' => [
75
+ '+44 1234567',
76
+ 442345678
77
+ ]
78
+ }
79
+
80
+ # Validator can validate schemas against inputs. Validator#validate returns an
81
+ # array of ValidationError.
82
+ #
83
+ # These ValidationError instances are portable -- every implementation of JDDF,
84
+ # across every language, returns the same errors.
85
+ validator = JDDF::Validator.new
86
+ result_ok = validator.validate(schema, input_ok)
87
+ result_bad = validator.validate(schema, input_bad)
88
+
89
+ p result_ok.size # 0
90
+ p result_bad.size # 3
91
+
92
+ # This error indicates that "name" is missing.
93
+ #
94
+ # #<struct JDDF::ValidationError instance_path=[], schema_path=["properties", "name"]
95
+ p result_bad[0]
96
+
97
+ # This error indicates that "age" has the wrong type.
98
+ #
99
+ # #<struct JDDF::ValidationError instance_path=["age"], schema_path=["properties", "age", "type"]>
100
+ p result_bad[1]
101
+
102
+ # This error indicates that "phones[1]" has the wrong type.
103
+ #
104
+ # #<struct JDDF::ValidationError instance_path=["phones", "1"], schema_path=["properties", "phones", "elements", "type"]>
105
+ p result_bad[2]
106
+ ```
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'bundle' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "rubygems"
12
+
13
+ m = Module.new do
14
+ module_function
15
+
16
+ def invoked_as_script?
17
+ File.expand_path($0) == File.expand_path(__FILE__)
18
+ end
19
+
20
+ def env_var_version
21
+ ENV["BUNDLER_VERSION"]
22
+ end
23
+
24
+ def cli_arg_version
25
+ return unless invoked_as_script? # don't want to hijack other binstubs
26
+ return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update`
27
+ bundler_version = nil
28
+ update_index = nil
29
+ ARGV.each_with_index do |a, i|
30
+ if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN
31
+ bundler_version = a
32
+ end
33
+ next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/
34
+ bundler_version = $1 || ">= 0.a"
35
+ update_index = i
36
+ end
37
+ bundler_version
38
+ end
39
+
40
+ def gemfile
41
+ gemfile = ENV["BUNDLE_GEMFILE"]
42
+ return gemfile if gemfile && !gemfile.empty?
43
+
44
+ File.expand_path("../../Gemfile", __FILE__)
45
+ end
46
+
47
+ def lockfile
48
+ lockfile =
49
+ case File.basename(gemfile)
50
+ when "gems.rb" then gemfile.sub(/\.rb$/, gemfile)
51
+ else "#{gemfile}.lock"
52
+ end
53
+ File.expand_path(lockfile)
54
+ end
55
+
56
+ def lockfile_version
57
+ return unless File.file?(lockfile)
58
+ lockfile_contents = File.read(lockfile)
59
+ return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/
60
+ Regexp.last_match(1)
61
+ end
62
+
63
+ def bundler_version
64
+ @bundler_version ||= begin
65
+ env_var_version || cli_arg_version ||
66
+ lockfile_version || "#{Gem::Requirement.default}.a"
67
+ end
68
+ end
69
+
70
+ def load_bundler!
71
+ ENV["BUNDLE_GEMFILE"] ||= gemfile
72
+
73
+ # must dup string for RG < 1.8 compatibility
74
+ activate_bundler(bundler_version.dup)
75
+ end
76
+
77
+ def activate_bundler(bundler_version)
78
+ if Gem::Version.correct?(bundler_version) && Gem::Version.new(bundler_version).release < Gem::Version.new("2.0")
79
+ bundler_version = "< 2"
80
+ end
81
+ gem_error = activation_error_handling do
82
+ gem "bundler", bundler_version
83
+ end
84
+ return if gem_error.nil?
85
+ require_error = activation_error_handling do
86
+ require "bundler/version"
87
+ end
88
+ return if require_error.nil? && Gem::Requirement.new(bundler_version).satisfied_by?(Gem::Version.new(Bundler::VERSION))
89
+ warn "Activating bundler (#{bundler_version}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_version}'`"
90
+ exit 42
91
+ end
92
+
93
+ def activation_error_handling
94
+ yield
95
+ nil
96
+ rescue StandardError, LoadError => e
97
+ e
98
+ end
99
+ end
100
+
101
+ m.load_bundler!
102
+
103
+ if m.invoked_as_script?
104
+ load Gem.bin_path("bundler", "bundle")
105
+ end
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'htmldiff' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("diff-lcs", "htmldiff")
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'ldiff' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("diff-lcs", "ldiff")
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rake' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("rake", "rake")
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rspec' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("rspec-core", "rspec")
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rubocop' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("rubocop", "rubocop")
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'ruby-parse' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("parser", "ruby-parse")
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'ruby-rewrite' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("parser", "ruby-rewrite")
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
 
2
3
  lib = File.expand_path("../lib", __FILE__)
3
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
@@ -5,7 +6,7 @@ require "jddf/version"
5
6
 
6
7
  Gem::Specification.new do |spec|
7
8
  spec.name = "jddf"
8
- spec.version = Jddf::VERSION
9
+ spec.version = JDDF::VERSION
9
10
  spec.authors = ["Ulysse Carion"]
10
11
  spec.email = ["ulysse@segment.com"]
11
12
 
@@ -25,4 +26,5 @@ Gem::Specification.new do |spec|
25
26
  spec.add_development_dependency "bundler", "~> 1.17"
26
27
  spec.add_development_dependency "rake", "~> 10.0"
27
28
  spec.add_development_dependency "rspec", "~> 3.0"
29
+ spec.add_development_dependency "rubocop", "~> 0.75.0"
28
30
  end
@@ -1,6 +1,9 @@
1
- require "jddf/version"
1
+ # frozen_string_literal: true
2
2
 
3
- module Jddf
4
- class Error < StandardError; end
5
- # Your code goes here...
3
+ require 'jddf/schema'
4
+ require 'jddf/validator'
5
+ require 'jddf/version'
6
+
7
+ # JDDF asdf
8
+ module JDDF
6
9
  end
@@ -0,0 +1,235 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JDDF
4
+ SCHEMA_KEYWORDS = %i[
5
+ definitions
6
+ ref
7
+ type
8
+ enum
9
+ elements
10
+ properties
11
+ optional_properties
12
+ additional_properties
13
+ values
14
+ discriminator
15
+ ].freeze
16
+
17
+ DISCRIMINATOR_KEYWORDS = %i[
18
+ tag
19
+ mapping
20
+ ].freeze
21
+
22
+ TYPES = %i[
23
+ boolean
24
+ int8
25
+ uint8
26
+ int16
27
+ uint16
28
+ int32
29
+ uint32
30
+ float32
31
+ float64
32
+ string
33
+ timestamp
34
+ ].freeze
35
+
36
+ Schema = Struct.new(*SCHEMA_KEYWORDS) do
37
+ def self.from_json(hash)
38
+ raise TypeError.new, 'hash must be a Hash' unless hash.is_a?(Hash)
39
+
40
+ schema = Schema.new
41
+
42
+ if hash.include?('definitions')
43
+ unless hash['definitions'].is_a?(Hash)
44
+ raise TypeError, 'definitions not Hash'
45
+ end
46
+
47
+ schema.definitions = hash['definitions'].map do |key, schema|
48
+ [key, from_json(schema)]
49
+ end.to_h
50
+ end
51
+
52
+ if hash.include?('ref')
53
+ raise TypeError, 'ref not String' unless hash['ref'].is_a?(String)
54
+
55
+ schema.ref = hash['ref']
56
+ end
57
+
58
+ if hash.include?('type')
59
+ raise TypeError, 'type not String' unless hash['type'].is_a?(String)
60
+
61
+ unless TYPES.map(&:to_s).include?(hash['type'])
62
+ raise TypeError, "type not in #{TYPES}"
63
+ end
64
+
65
+ schema.type = hash['type'].to_sym
66
+ end
67
+
68
+ if hash.include?('enum')
69
+ raise TypeError, 'enum not Array' unless hash['enum'].is_a?(Array)
70
+ raise ArgumentError, 'enum is empty array' if hash['enum'].empty?
71
+
72
+ hash['enum'].each do |value|
73
+ raise TypeError, 'enum element not String' unless value.is_a?(String)
74
+ end
75
+
76
+ schema.enum = hash['enum'].to_set
77
+
78
+ if schema.enum.size != hash['enum'].size
79
+ raise ArgumentError, 'enum contains duplicates'
80
+ end
81
+ end
82
+
83
+ if hash.include?('elements')
84
+ raise TypeError, 'elements not Hash' unless hash['elements'].is_a?(Hash)
85
+
86
+ schema.elements = from_json(hash['elements'])
87
+ end
88
+
89
+ if hash.include?('properties')
90
+ unless hash['properties'].is_a?(Hash)
91
+ raise TypeError, 'properties not Hash'
92
+ end
93
+
94
+ schema.properties = hash['properties'].map do |key, schema|
95
+ [key, from_json(schema)]
96
+ end.to_h
97
+ end
98
+
99
+ if hash.include?('optionalProperties')
100
+ unless hash['optionalProperties'].is_a?(Hash)
101
+ raise TypeError, 'optionalProperties not Hash'
102
+ end
103
+
104
+ optional_properties = hash['optionalProperties'].map do |key, schema|
105
+ [key, from_json(schema)]
106
+ end.to_h
107
+
108
+ schema.optional_properties = optional_properties
109
+ end
110
+
111
+ if hash.include?('additionalProperties')
112
+ unless [true, false].include?(hash['additionalProperties'])
113
+ raise TypeError, 'additionalProperties not boolean'
114
+ end
115
+
116
+ schema.additional_properties = hash['additionalProperties']
117
+ end
118
+
119
+ if hash.include?('values')
120
+ raise TypeError, 'values not Hash' unless hash['values'].is_a?(Hash)
121
+
122
+ schema.values = from_json(hash['values'])
123
+ end
124
+
125
+ if hash.include?('discriminator')
126
+ unless hash['discriminator'].is_a?(Hash)
127
+ raise TypeError, 'discriminator not Hash'
128
+ end
129
+
130
+ schema.discriminator = Discriminator.from_json(hash['discriminator'])
131
+ end
132
+
133
+ schema
134
+ end
135
+
136
+ def form
137
+ return :ref if ref
138
+ return :type if type
139
+ return :enum if enum
140
+ return :elements if elements
141
+ return :properties if properties || optional_properties
142
+ return :values if values
143
+ return :discriminator if discriminator
144
+
145
+ :empty
146
+ end
147
+
148
+ def verify(root = self)
149
+ empty = true
150
+
151
+ if ref
152
+ empty = false
153
+
154
+ unless root.definitions&.keys&.include?(ref)
155
+ raise ArgumentError, 'reference to non-existent definition'
156
+ end
157
+ end
158
+
159
+ if type
160
+ raise ArgumentError, 'invalid form' unless empty
161
+
162
+ empty = false
163
+ end
164
+
165
+ if enum
166
+ raise ArgumentError, 'invalid form' unless empty
167
+
168
+ empty = false
169
+ end
170
+
171
+ if elements
172
+ raise ArgumentError, 'invalid form' unless empty
173
+
174
+ empty = false
175
+
176
+ elements.verify(root)
177
+ end
178
+
179
+ if properties || optional_properties
180
+ raise ArgumentError, 'invalid form' unless empty
181
+
182
+ empty = false
183
+
184
+ properties&.values&.each { |schema| schema.verify(root) }
185
+ optional_properties&.values&.each { |schema| schema.verify(root) }
186
+ end
187
+
188
+ if values
189
+ raise ArgumentError, 'invalid form' unless empty
190
+
191
+ empty = false
192
+
193
+ values.verify(root)
194
+ end
195
+
196
+ if properties && optional_properties
197
+ unless (properties.keys & optional_properties.keys).empty?
198
+ raise ArgumentError, 'properties and optional_properties share key'
199
+ end
200
+ end
201
+
202
+ if discriminator
203
+ raise ArgumentError, 'invalid form' unless empty
204
+
205
+ discriminator.mapping.values.each do |schema|
206
+ schema.verify(root)
207
+
208
+ unless schema.form == :properties
209
+ raise ArgumentError, 'mapping value not of properties form'
210
+ end
211
+
212
+ if schema&.properties&.include?(discriminator.tag) ||
213
+ schema&.optional_properties&.include?(discriminator.tag)
214
+ raise ArgumentError, 'tag appears in mapping properties'
215
+ end
216
+ end
217
+ end
218
+
219
+ self
220
+ end
221
+ end
222
+
223
+ Discriminator = Struct.new(*DISCRIMINATOR_KEYWORDS) do
224
+ def self.from_json(hash)
225
+ raise TypeError, 'tag not String' unless hash['tag'].is_a?(String)
226
+ raise TypeError, 'mapping not Hash' unless hash['mapping'].is_a?(Hash)
227
+
228
+ mapping = hash['mapping'].map do |key, schema|
229
+ [key, Schema.from_json(schema)]
230
+ end.to_h
231
+
232
+ Discriminator.new(hash['tag'], mapping)
233
+ end
234
+ end
235
+ end
@@ -0,0 +1,272 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
4
+
5
+ module JDDF
6
+ # ValidationError
7
+ ValidationError = Struct.new(:instance_path, :schema_path)
8
+
9
+ # MaxDepthExceededError
10
+ class MaxDepthExceededError < StandardError
11
+ def initialize(msg = 'max depth exceeded while validating')
12
+ super
13
+ end
14
+ end
15
+
16
+ # Validator
17
+ class Validator
18
+ # MaxErrorsError
19
+ class MaxErrorsError < StandardError
20
+ end
21
+
22
+ private_constant :MaxErrorsError
23
+
24
+ # VM
25
+ class VM
26
+ attr_accessor :max_depth
27
+ attr_accessor :max_errors
28
+ attr_accessor :root_schema
29
+ attr_accessor :instance_tokens
30
+ attr_accessor :schema_tokens
31
+ attr_accessor :errors
32
+
33
+ def validate(schema, instance, parent_tag = nil)
34
+ case schema.form
35
+ when :ref
36
+ raise MaxDepthExceededError if schema_tokens.size == max_depth
37
+
38
+ schema_tokens << ['definitions', schema.ref]
39
+ validate(root_schema.definitions[schema.ref], instance)
40
+ schema_tokens.pop
41
+ when :type
42
+ push_schema_token('type')
43
+
44
+ case schema.type
45
+ when :boolean
46
+ push_error if instance != true && instance != false
47
+ when :float32, :float64
48
+ push_error unless instance.is_a?(Numeric)
49
+ when :int8
50
+ validate_int(instance, -128, 127)
51
+ when :uint8
52
+ validate_int(instance, 0, 255)
53
+ when :int16
54
+ validate_int(instance, -32_768, 32_767)
55
+ when :uint16
56
+ validate_int(instance, 0, 65_535)
57
+ when :int32
58
+ validate_int(instance, -2_147_483_648, 2_147_483_647)
59
+ when :uint32
60
+ validate_int(instance, 0, 4_294_967_295)
61
+ when :string
62
+ push_error unless instance.is_a?(String)
63
+ when :timestamp
64
+ begin
65
+ DateTime.rfc3339(instance)
66
+ rescue TypeError, ArgumentError
67
+ push_error
68
+ end
69
+ end
70
+
71
+ pop_schema_token
72
+ when :enum
73
+ push_schema_token('enum')
74
+ push_error unless schema.enum.include?(instance)
75
+ pop_schema_token
76
+ when :elements
77
+ push_schema_token('elements')
78
+
79
+ if instance.is_a?(Array)
80
+ instance.each_with_index do |sub_instance, index|
81
+ push_instance_token(index.to_s)
82
+ validate(schema.elements, sub_instance)
83
+ pop_instance_token
84
+ end
85
+ else
86
+ push_error
87
+ end
88
+
89
+ pop_schema_token
90
+ when :properties
91
+ if instance.is_a?(Hash)
92
+ if schema.properties
93
+ push_schema_token('properties')
94
+
95
+ schema.properties.each do |key, sub_schema|
96
+ push_schema_token(key)
97
+
98
+ if instance.include?(key)
99
+ push_instance_token(key)
100
+ validate(sub_schema, instance[key])
101
+ pop_instance_token
102
+ else
103
+ push_error
104
+ end
105
+
106
+ pop_schema_token
107
+ end
108
+
109
+ pop_schema_token
110
+ end
111
+
112
+ if schema.optional_properties
113
+ push_schema_token('optionalProperties')
114
+
115
+ schema.optional_properties.each do |key, sub_schema|
116
+ push_schema_token(key)
117
+
118
+ if instance.include?(key)
119
+ push_instance_token(key)
120
+ validate(sub_schema, instance[key])
121
+ pop_instance_token
122
+ end
123
+
124
+ pop_schema_token
125
+ end
126
+
127
+ pop_schema_token
128
+ end
129
+
130
+ unless schema.additional_properties
131
+ instance.keys.each do |key|
132
+ in_properties =
133
+ schema.properties&.include?(key)
134
+ in_optional_properties =
135
+ schema.optional_properties&.include?(key)
136
+ is_parent_tag = parent_tag == key
137
+
138
+ unless in_properties || in_optional_properties || is_parent_tag
139
+ push_instance_token(key)
140
+ push_error
141
+ pop_instance_token
142
+ end
143
+ end
144
+ end
145
+ else
146
+ if schema.properties.nil?
147
+ push_schema_token('optionalProperties')
148
+ else
149
+ push_schema_token('properties')
150
+ end
151
+
152
+ push_error
153
+ pop_schema_token
154
+ end
155
+ when :values
156
+ push_schema_token('values')
157
+
158
+ if instance.is_a?(Hash)
159
+ instance.each do |key, value|
160
+ push_instance_token(key)
161
+ validate(schema.values, value)
162
+ pop_instance_token
163
+ end
164
+ else
165
+ push_error
166
+ end
167
+
168
+ pop_schema_token
169
+ when :discriminator
170
+ push_schema_token('discriminator')
171
+
172
+ if instance.is_a?(Hash)
173
+ if instance.include?(schema.discriminator.tag)
174
+ tag_value = instance[schema.discriminator.tag]
175
+
176
+ if tag_value.is_a?(String)
177
+ push_schema_token('mapping')
178
+
179
+ if schema.discriminator.mapping.include?(tag_value)
180
+ sub_schema = schema.discriminator.mapping[tag_value]
181
+
182
+ push_schema_token(tag_value)
183
+ validate(sub_schema, instance, schema.discriminator.tag)
184
+ pop_schema_token
185
+ else
186
+ push_instance_token(schema.discriminator.tag)
187
+ push_error
188
+ pop_instance_token
189
+ end
190
+
191
+ pop_schema_token
192
+ else
193
+ push_instance_token(schema.discriminator.tag)
194
+ push_schema_token('tag')
195
+ push_error
196
+ pop_schema_token
197
+ pop_instance_token
198
+ end
199
+ else
200
+ push_schema_token('tag')
201
+ push_error
202
+ pop_schema_token
203
+ end
204
+ else
205
+ push_error
206
+ end
207
+
208
+ pop_schema_token
209
+ end
210
+ end
211
+
212
+ def validate_int(instance, min, max)
213
+ if instance.is_a?(Numeric)
214
+ if instance.modulo(1).nonzero? || instance < min || instance > max
215
+ push_error
216
+ end
217
+ else
218
+ push_error
219
+ end
220
+ end
221
+
222
+ def push_instance_token(token)
223
+ instance_tokens << token
224
+ end
225
+
226
+ def pop_instance_token
227
+ instance_tokens.pop
228
+ end
229
+
230
+ def push_schema_token(token)
231
+ schema_tokens.last << token
232
+ end
233
+
234
+ def pop_schema_token
235
+ schema_tokens.last.pop
236
+ end
237
+
238
+ def push_error
239
+ error = ValidationError.new
240
+ error.instance_path = instance_tokens.clone
241
+ error.schema_path = schema_tokens.last.clone
242
+
243
+ errors << error
244
+
245
+ raise MaxErrorsError if errors.size == max_errors
246
+ end
247
+ end
248
+
249
+ private_constant :VM
250
+
251
+ attr_accessor :max_depth
252
+ attr_accessor :max_errors
253
+
254
+ def validate(schema, instance)
255
+ vm = VM.new
256
+ vm.max_depth = max_depth
257
+ vm.max_errors = max_errors
258
+ vm.root_schema = schema
259
+ vm.instance_tokens = []
260
+ vm.schema_tokens = [[]]
261
+ vm.errors = []
262
+
263
+ begin
264
+ vm.validate(schema, instance)
265
+ rescue MaxErrorsError # rubocop:disable Lint/HandleExceptions
266
+ # There is nothing to do here. MaxErrorsError is just a circuit-breaker.
267
+ end
268
+
269
+ vm.errors
270
+ end
271
+ end
272
+ end
@@ -1,3 +1,5 @@
1
- module Jddf
2
- VERSION = "0.1.0"
1
+ # frozen_string_literal: true
2
+
3
+ module JDDF
4
+ VERSION = '0.2.0'
3
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jddf
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ulysse Carion
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-10-07 00:00:00.000000000 Z
11
+ date: 2019-10-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rubocop
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 0.75.0
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 0.75.0
55
69
  description:
56
70
  email:
57
71
  - ulysse@segment.com
@@ -60,17 +74,28 @@ extensions: []
60
74
  extra_rdoc_files: []
61
75
  files:
62
76
  - ".gitignore"
77
+ - ".gitmodules"
63
78
  - ".rspec"
64
- - ".travis.yml"
79
+ - ".rubocop.yml"
65
80
  - Gemfile
66
81
  - Gemfile.lock
67
82
  - LICENSE.txt
68
83
  - README.md
69
84
  - Rakefile
85
+ - bin/bundle
70
86
  - bin/console
87
+ - bin/htmldiff
88
+ - bin/ldiff
89
+ - bin/rake
90
+ - bin/rspec
91
+ - bin/rubocop
92
+ - bin/ruby-parse
93
+ - bin/ruby-rewrite
71
94
  - bin/setup
72
95
  - jddf.gemspec
73
96
  - lib/jddf.rb
97
+ - lib/jddf/schema.rb
98
+ - lib/jddf/validator.rb
74
99
  - lib/jddf/version.rb
75
100
  homepage: https://github.com/jddf/jddf-ruby
76
101
  licenses:
@@ -1,7 +0,0 @@
1
- ---
2
- sudo: false
3
- language: ruby
4
- cache: bundler
5
- rvm:
6
- - 2.6.5
7
- before_install: gem install bundler -v 1.17.2