json_schemer 0.2.16 → 0.2.25

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0cc2fef0eeaaab0e1664a9fa5aa39c08ef2e1fbd8fb68d202bf1b50b7c0840ee
4
- data.tar.gz: 438c507a7d8ffa674d34d22c647af0852eb031e02c491d274cc5d4dec5c185dd
3
+ metadata.gz: cd29a78d2cbd45dc0409c551d0e3c11e39a801204bab75e22ded249c4ef8edf8
4
+ data.tar.gz: 9d0556d1f4723ae50b9a038595f44ffdc9c0dcc8d05d9b9771f58248fc88a764
5
5
  SHA512:
6
- metadata.gz: 84ba1ea8a43459ff6c5c835fd5f0aedaf00f8077ecd3ec0aefe88322f09ef868712702122790a56f30c33bb1ab3f2585bd7e9a57c94eb07f553d845322095ae8
7
- data.tar.gz: 6709666d3f2cf98ba1b2f06712e90f564d33d6e861b15d9ff2cb3c109d9b3ca0816b456f22b837a18062824429c97b8d7ebeb9fbaaa75b67bb203dec8f56b310
6
+ metadata.gz: d3bb75eb37c9ad70776fbf8d7fc5c1d7cc4a79bf7c68e2430dbf30f256fb0a3e1cddf718fa1799486814058d98455ef47b62baa514b1b49beafe0c7fc6a489dc
7
+ data.tar.gz: eea979521c2d65afef0325f94513ecf64bf604c562af41afae2ec258a4449b70a28aee927be925681ceecc434ba716271d7bbc6a713f8413b0f22710b7ed7c9c
@@ -1,16 +1,31 @@
1
1
  name: ci
2
2
  on: [push, pull_request]
3
3
  jobs:
4
- ruby:
4
+ test:
5
5
  strategy:
6
+ fail-fast: false
6
7
  matrix:
7
- ruby: [2.4, 2.5, 2.6, 2.7, truffleruby-head]
8
- runs-on: ubuntu-latest
8
+ os: [ubuntu-latest, windows-latest, macos-latest]
9
+ ruby: [2.4, 2.5, 2.6, 2.7, 3.0, 3.1, 3.2, head, jruby, jruby-head, truffleruby, truffleruby-head]
10
+ exclude:
11
+ - os: windows-latest
12
+ ruby: jruby
13
+ - os: windows-latest
14
+ ruby: jruby-head
15
+ - os: windows-latest
16
+ ruby: truffleruby
17
+ - os: windows-latest
18
+ ruby: truffleruby-head
19
+ runs-on: ${{ matrix.os }}
9
20
  steps:
10
21
  - uses: actions/checkout@v2
11
22
  - uses: ruby/setup-ruby@v1
12
23
  with:
13
24
  ruby-version: ${{ matrix.ruby }}
25
+ bundler-cache: true
14
26
  - run: |
15
- bundle install
16
- bundle exec rake test
27
+ mkdir -p tmp/gems
28
+ gem build json_schemer.gemspec
29
+ gem install --local --ignore-dependencies --no-document --install-dir tmp/gems json_schemer-*.gem
30
+ rm json_schemer-*.gem
31
+ bin/rake test
data/Gemfile.lock CHANGED
@@ -1,24 +1,39 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- json_schemer (0.2.16)
5
- ecma-re-validator (~> 0.2)
4
+ json_schemer (0.2.25)
5
+ ecma-re-validator (~> 0.3)
6
6
  hana (~> 1.3)
7
- regexp_parser (~> 1.5)
7
+ regexp_parser (~> 2.0)
8
+ simpleidn (~> 0.2)
8
9
  uri_template (~> 0.7)
9
10
 
10
11
  GEM
11
12
  remote: https://rubygems.org/
12
13
  specs:
13
- ecma-re-validator (0.2.1)
14
- regexp_parser (~> 1.2)
15
- hana (1.3.6)
16
- minitest (5.11.3)
17
- rake (13.0.1)
18
- regexp_parser (1.8.1)
14
+ docile (1.4.0)
15
+ ecma-re-validator (0.3.0)
16
+ regexp_parser (~> 2.0)
17
+ hana (1.3.7)
18
+ minitest (5.15.0)
19
+ rake (13.0.6)
20
+ regexp_parser (2.6.1)
21
+ simplecov (0.22.0)
22
+ docile (~> 1.1)
23
+ simplecov-html (~> 0.11)
24
+ simplecov_json_formatter (~> 0.1)
25
+ simplecov-html (0.12.3)
26
+ simplecov_json_formatter (0.1.4)
27
+ simpleidn (0.2.1)
28
+ unf (~> 0.1.4)
29
+ unf (0.1.4)
30
+ unf_ext
31
+ unf (0.1.4-java)
32
+ unf_ext (0.0.8.2)
19
33
  uri_template (0.7.0)
20
34
 
21
35
  PLATFORMS
36
+ java
22
37
  ruby
23
38
 
24
39
  DEPENDENCIES
@@ -26,6 +41,7 @@ DEPENDENCIES
26
41
  json_schemer!
27
42
  minitest (~> 5.0)
28
43
  rake (~> 13.0)
44
+ simplecov (~> 0.22)
29
45
 
30
46
  BUNDLED WITH
31
- 2.1.4
47
+ 2.3.25
data/README.md CHANGED
@@ -110,10 +110,41 @@ JSONSchemer.schema(
110
110
  # 'net/http'/proc/lambda/respond_to?(:call)
111
111
  # 'net/http': proc { |uri| JSON.parse(Net::HTTP.get(uri)) }
112
112
  # default: proc { |uri| raise UnknownRef, uri.to_s }
113
- ref_resolver: 'net/http'
113
+ ref_resolver: 'net/http',
114
+
115
+ # use different method to match regexes
116
+ # 'ecma'/'ruby'/proc/lambda/respond_to?(:call)
117
+ # default: 'ecma'
118
+ regexp_resolver: proc do |pattern|
119
+ RE2::Regexp.new(pattern)
120
+ end
114
121
  )
115
122
  ```
116
123
 
124
+ ## CLI
125
+
126
+ 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.
127
+
128
+ 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).
129
+
130
+ The schema or data can also be read from stdin using `-`.
131
+
132
+ ```
133
+ % json_schemer --help
134
+ Usage:
135
+ json_schemer [options] <schema> <data>...
136
+ json_schemer [options] <schema> -
137
+ json_schemer [options] - <data>...
138
+ json_schemer -h | --help
139
+ json_schemer --version
140
+
141
+ Options:
142
+ -e, --errors MAX Maximum number of errors to output
143
+ Use "0" to validate with no output
144
+ -h, --help Show help
145
+ -v, --version Show version
146
+ ```
147
+
117
148
  ## Development
118
149
 
119
150
  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.
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'open-uri'
4
+ require 'csv'
5
+
6
+ # https://datatracker.ietf.org/doc/html/rfc5892#appendix-A.1
7
+ # https://datatracker.ietf.org/doc/html/rfc5892#appendix-A.2
8
+
9
+ csv_options = { :col_sep => ';', :skip_blanks => true, :skip_lines => /\A#/ }
10
+
11
+ unicode_data = URI('https://www.unicode.org/Public/UCD/latest/ucd/UnicodeData.txt')
12
+ derived_joining_type = URI('https://www.unicode.org/Public/UCD/latest/ucd/extracted/DerivedJoiningType.txt')
13
+
14
+ # https://www.unicode.org/reports/tr44/#Canonical_Combining_Class_Values
15
+ virama_canonical_combining_class = '9'
16
+
17
+ virama_codes = CSV.new(unicode_data.read, **csv_options).select do |code, _name, _category, canonical_combining_class|
18
+ canonical_combining_class == virama_canonical_combining_class
19
+ end.map(&:first)
20
+
21
+ # https://www.unicode.org/reports/tr44/#Default_Values
22
+ # https://www.unicode.org/reports/tr44/#Derived_Extracted
23
+ codes_by_joining_type = CSV.new(derived_joining_type.read, **csv_options).group_by do |_code, joining_type|
24
+ joining_type.gsub(/#.+/, '').strip
25
+ end.transform_values do |rows|
26
+ rows.map do |code, _joining_type|
27
+ code.strip
28
+ end
29
+ end
30
+
31
+ def codes_to_character_class(codes)
32
+ characters = codes.map do |code|
33
+ code.gsub(/(\h+)/, '\u{\1}').gsub('..', '-')
34
+ end
35
+ "[#{characters.join}]"
36
+ end
37
+
38
+ puts "VIRAMA_CHARACTER_CLASS = '#{codes_to_character_class(virama_codes)}'"
39
+
40
+ codes_by_joining_type.slice('L', 'D', 'T', 'R').each do |joining_type, codes|
41
+ puts "JOINING_TYPE_#{joining_type}_CHARACTER_CLASS = '#{codes_to_character_class(codes)}'"
42
+ end
data/bin/rake ADDED
@@ -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")
data/exe/json_schemer ADDED
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'json'
4
+ require 'optparse'
5
+ require 'pathname'
6
+ require 'json_schemer'
7
+
8
+ parser = OptionParser.new('Usage:', 32, ' ')
9
+ parser.separator(" #{parser.program_name} [options] <schema> <data>...")
10
+ parser.separator(" #{parser.program_name} [options] <schema> -")
11
+ parser.separator(" #{parser.program_name} [options] - <data>...")
12
+ parser.separator(" #{parser.program_name} -h | --help")
13
+ parser.separator(" #{parser.program_name} --version")
14
+ parser.separator('')
15
+ parser.separator('Options:')
16
+ parser.on('-e', '--errors MAX', Integer, 'Maximum number of errors to output', 'Use "0" to validate with no output')
17
+ parser.on_tail('-h', '--help', 'Show help')
18
+ parser.on_tail('-v', '--version', 'Show version')
19
+
20
+ options = {}
21
+ parser.parse!(:into => options)
22
+
23
+ if options[:help]
24
+ $stdout.puts(parser)
25
+ exit
26
+ end
27
+
28
+ if options[:version]
29
+ $stdout.puts("#{parser.program_name} #{JSONSchemer::VERSION}")
30
+ exit
31
+ end
32
+
33
+ if ARGV.size == 0
34
+ $stderr.puts("#{parser.program_name}: no schema or data")
35
+ exit(false)
36
+ end
37
+
38
+ if ARGV.size == 1
39
+ $stderr.puts("#{parser.program_name}: no data")
40
+ exit(false)
41
+ end
42
+
43
+ if ARGV.count('-') > 1
44
+ $stderr.puts("#{parser.program_name}: multiple stdin")
45
+ exit(false)
46
+ end
47
+
48
+ errors = 0
49
+ schema = ARGF.file.is_a?(File) ? Pathname.new(ARGF.file.path) : ARGF.file.read
50
+ schemer = JSONSchemer.schema(schema)
51
+
52
+ while ARGV.any?
53
+ data = JSON.parse(ARGF.skip.file.read)
54
+ schemer.validate(data).each do |error|
55
+ exit(false) if options[:errors] == 0
56
+ errors += 1
57
+ $stdout.puts(JSON.generate(error))
58
+ exit(false) if options[:errors] == errors
59
+ end
60
+ end
61
+
62
+ exit(errors.zero?)
data/json_schemer.gemspec CHANGED
@@ -20,22 +20,16 @@ Gem::Specification.new do |spec|
20
20
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
21
  spec.require_paths = ["lib"]
22
22
 
23
- spec.required_ruby_version = '~> 2.4'
23
+ spec.required_ruby_version = '>= 2.4'
24
24
 
25
25
  spec.add_development_dependency "bundler", "~> 2.0"
26
26
  spec.add_development_dependency "rake", "~> 13.0"
27
27
  spec.add_development_dependency "minitest", "~> 5.0"
28
+ spec.add_development_dependency "simplecov", "~> 0.22"
28
29
 
29
- # spec.add_development_dependency "benchmark-ips", "~> 2.7.2"
30
- # spec.add_development_dependency "jschema", "~> 0.2.1"
31
- # spec.add_development_dependency "json-schema", "~> 2.8.0"
32
- # spec.add_development_dependency "json_schema", "~> 0.17.0"
33
- # spec.add_development_dependency "json_validation", "~> 0.1.0"
34
- # spec.add_development_dependency "jsonschema", "~> 2.0.2"
35
- # spec.add_development_dependency "rj_schema", "~> 0.2.0"
36
-
37
- spec.add_runtime_dependency "ecma-re-validator", "~> 0.2"
30
+ spec.add_runtime_dependency "ecma-re-validator", "~> 0.3"
38
31
  spec.add_runtime_dependency "hana", "~> 1.3"
32
+ spec.add_runtime_dependency "regexp_parser", "~> 2.0"
33
+ spec.add_runtime_dependency "simpleidn", "~> 0.2"
39
34
  spec.add_runtime_dependency "uri_template", "~> 0.7"
40
- spec.add_runtime_dependency "regexp_parser", "~> 1.5"
41
35
  end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+ module JSONSchemer
3
+ class CachedResolver
4
+ def initialize(&resolver)
5
+ @resolver = resolver
6
+ @cache = {}
7
+ end
8
+
9
+ def call(*args)
10
+ @cache[args] = @resolver.call(*args) unless @cache.key?(args)
11
+ @cache[args]
12
+ end
13
+ end
14
+
15
+ class CachedRefResolver < CachedResolver; end
16
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+ module JSONSchemer
3
+ module Format
4
+ module Hostname
5
+ # https://datatracker.ietf.org/doc/html/rfc5892#section-2.1
6
+ MARKS = '\p{Mn}\p{Mc}'
7
+ LETTER_DIGITS = "\\p{Ll}\\p{Lu}\\p{Lo}\\p{Nd}\\p{Lm}#{MARKS}"
8
+ # https://datatracker.ietf.org/doc/html/rfc5892#section-2.6
9
+ EXCEPTIONS_PVALID = '\u{06FD}\u{06FE}\u{0F0B}\u{3007}' # \u{00DF}\u{03C2} covered by \p{Ll}
10
+ EXCEPTIONS_DISALLOWED = '\u{0640}\u{07FA}\u{302E}\u{302F}\u{3031}\u{3032}\u{3033}\u{3034}\u{3035}\u{303B}'
11
+ LABEL_CHARACTER_CLASS = "[#{LETTER_DIGITS}#{EXCEPTIONS_PVALID}&&[^#{EXCEPTIONS_DISALLOWED}]]"
12
+ # https://datatracker.ietf.org/doc/html/rfc5891#section-4.2.3.2
13
+ LEADING_CHARACTER_CLASS = "[#{LABEL_CHARACTER_CLASS}&&[^#{MARKS}]]"
14
+ LABEL_REGEX_STRING = "#{LEADING_CHARACTER_CLASS}([#{LABEL_CHARACTER_CLASS}\-]*#{LABEL_CHARACTER_CLASS})?"
15
+ HOSTNAME_REGEX = /\A(#{LABEL_REGEX_STRING}\.)*#{LABEL_REGEX_STRING}\z/i.freeze
16
+ # bin/hostname_character_classes
17
+ VIRAMA_CHARACTER_CLASS = '[\u{094D}\u{09CD}\u{0A4D}\u{0ACD}\u{0B4D}\u{0BCD}\u{0C4D}\u{0CCD}\u{0D3B}\u{0D3C}\u{0D4D}\u{0DCA}\u{0E3A}\u{0EBA}\u{0F84}\u{1039}\u{103A}\u{1714}\u{1715}\u{1734}\u{17D2}\u{1A60}\u{1B44}\u{1BAA}\u{1BAB}\u{1BF2}\u{1BF3}\u{2D7F}\u{A806}\u{A82C}\u{A8C4}\u{A953}\u{A9C0}\u{AAF6}\u{ABED}\u{10A3F}\u{11046}\u{11070}\u{1107F}\u{110B9}\u{11133}\u{11134}\u{111C0}\u{11235}\u{112EA}\u{1134D}\u{11442}\u{114C2}\u{115BF}\u{1163F}\u{116B6}\u{1172B}\u{11839}\u{1193D}\u{1193E}\u{119E0}\u{11A34}\u{11A47}\u{11A99}\u{11C3F}\u{11D44}\u{11D45}\u{11D97}\u{11F41}\u{11F42}]'
18
+ JOINING_TYPE_L_CHARACTER_CLASS = '[\u{A872}\u{10ACD}\u{10AD7}\u{10D00}\u{10FCB}]'
19
+ JOINING_TYPE_D_CHARACTER_CLASS = '[\u{0620}\u{0626}\u{0628}\u{062A}-\u{062E}\u{0633}-\u{063F}\u{0641}-\u{0647}\u{0649}-\u{064A}\u{066E}-\u{066F}\u{0678}-\u{0687}\u{069A}-\u{06BF}\u{06C1}-\u{06C2}\u{06CC}\u{06CE}\u{06D0}-\u{06D1}\u{06FA}-\u{06FC}\u{06FF}\u{0712}-\u{0714}\u{071A}-\u{071D}\u{071F}-\u{0727}\u{0729}\u{072B}\u{072D}-\u{072E}\u{074E}-\u{0758}\u{075C}-\u{076A}\u{076D}-\u{0770}\u{0772}\u{0775}-\u{0777}\u{077A}-\u{077F}\u{07CA}-\u{07EA}\u{0841}-\u{0845}\u{0848}\u{084A}-\u{0853}\u{0855}\u{0860}\u{0862}-\u{0865}\u{0868}\u{0886}\u{0889}-\u{088D}\u{08A0}-\u{08A9}\u{08AF}-\u{08B0}\u{08B3}-\u{08B8}\u{08BA}-\u{08C8}\u{1807}\u{1820}-\u{1842}\u{1843}\u{1844}-\u{1878}\u{1887}-\u{18A8}\u{18AA}\u{A840}-\u{A871}\u{10AC0}-\u{10AC4}\u{10AD3}-\u{10AD6}\u{10AD8}-\u{10ADC}\u{10ADE}-\u{10AE0}\u{10AEB}-\u{10AEE}\u{10B80}\u{10B82}\u{10B86}-\u{10B88}\u{10B8A}-\u{10B8B}\u{10B8D}\u{10B90}\u{10BAD}-\u{10BAE}\u{10D01}-\u{10D21}\u{10D23}\u{10F30}-\u{10F32}\u{10F34}-\u{10F44}\u{10F51}-\u{10F53}\u{10F70}-\u{10F73}\u{10F76}-\u{10F81}\u{10FB0}\u{10FB2}-\u{10FB3}\u{10FB8}\u{10FBB}-\u{10FBC}\u{10FBE}-\u{10FBF}\u{10FC1}\u{10FC4}\u{10FCA}\u{1E900}-\u{1E943}]'
20
+ JOINING_TYPE_T_CHARACTER_CLASS = '[\u{00AD}\u{0300}-\u{036F}\u{0483}-\u{0487}\u{0488}-\u{0489}\u{0591}-\u{05BD}\u{05BF}\u{05C1}-\u{05C2}\u{05C4}-\u{05C5}\u{05C7}\u{0610}-\u{061A}\u{061C}\u{064B}-\u{065F}\u{0670}\u{06D6}-\u{06DC}\u{06DF}-\u{06E4}\u{06E7}-\u{06E8}\u{06EA}-\u{06ED}\u{070F}\u{0711}\u{0730}-\u{074A}\u{07A6}-\u{07B0}\u{07EB}-\u{07F3}\u{07FD}\u{0816}-\u{0819}\u{081B}-\u{0823}\u{0825}-\u{0827}\u{0829}-\u{082D}\u{0859}-\u{085B}\u{0898}-\u{089F}\u{08CA}-\u{08E1}\u{08E3}-\u{0902}\u{093A}\u{093C}\u{0941}-\u{0948}\u{094D}\u{0951}-\u{0957}\u{0962}-\u{0963}\u{0981}\u{09BC}\u{09C1}-\u{09C4}\u{09CD}\u{09E2}-\u{09E3}\u{09FE}\u{0A01}-\u{0A02}\u{0A3C}\u{0A41}-\u{0A42}\u{0A47}-\u{0A48}\u{0A4B}-\u{0A4D}\u{0A51}\u{0A70}-\u{0A71}\u{0A75}\u{0A81}-\u{0A82}\u{0ABC}\u{0AC1}-\u{0AC5}\u{0AC7}-\u{0AC8}\u{0ACD}\u{0AE2}-\u{0AE3}\u{0AFA}-\u{0AFF}\u{0B01}\u{0B3C}\u{0B3F}\u{0B41}-\u{0B44}\u{0B4D}\u{0B55}-\u{0B56}\u{0B62}-\u{0B63}\u{0B82}\u{0BC0}\u{0BCD}\u{0C00}\u{0C04}\u{0C3C}\u{0C3E}-\u{0C40}\u{0C46}-\u{0C48}\u{0C4A}-\u{0C4D}\u{0C55}-\u{0C56}\u{0C62}-\u{0C63}\u{0C81}\u{0CBC}\u{0CBF}\u{0CC6}\u{0CCC}-\u{0CCD}\u{0CE2}-\u{0CE3}\u{0D00}-\u{0D01}\u{0D3B}-\u{0D3C}\u{0D41}-\u{0D44}\u{0D4D}\u{0D62}-\u{0D63}\u{0D81}\u{0DCA}\u{0DD2}-\u{0DD4}\u{0DD6}\u{0E31}\u{0E34}-\u{0E3A}\u{0E47}-\u{0E4E}\u{0EB1}\u{0EB4}-\u{0EBC}\u{0EC8}-\u{0ECE}\u{0F18}-\u{0F19}\u{0F35}\u{0F37}\u{0F39}\u{0F71}-\u{0F7E}\u{0F80}-\u{0F84}\u{0F86}-\u{0F87}\u{0F8D}-\u{0F97}\u{0F99}-\u{0FBC}\u{0FC6}\u{102D}-\u{1030}\u{1032}-\u{1037}\u{1039}-\u{103A}\u{103D}-\u{103E}\u{1058}-\u{1059}\u{105E}-\u{1060}\u{1071}-\u{1074}\u{1082}\u{1085}-\u{1086}\u{108D}\u{109D}\u{135D}-\u{135F}\u{1712}-\u{1714}\u{1732}-\u{1733}\u{1752}-\u{1753}\u{1772}-\u{1773}\u{17B4}-\u{17B5}\u{17B7}-\u{17BD}\u{17C6}\u{17C9}-\u{17D3}\u{17DD}\u{180B}-\u{180D}\u{180F}\u{1885}-\u{1886}\u{18A9}\u{1920}-\u{1922}\u{1927}-\u{1928}\u{1932}\u{1939}-\u{193B}\u{1A17}-\u{1A18}\u{1A1B}\u{1A56}\u{1A58}-\u{1A5E}\u{1A60}\u{1A62}\u{1A65}-\u{1A6C}\u{1A73}-\u{1A7C}\u{1A7F}\u{1AB0}-\u{1ABD}\u{1ABE}\u{1ABF}-\u{1ACE}\u{1B00}-\u{1B03}\u{1B34}\u{1B36}-\u{1B3A}\u{1B3C}\u{1B42}\u{1B6B}-\u{1B73}\u{1B80}-\u{1B81}\u{1BA2}-\u{1BA5}\u{1BA8}-\u{1BA9}\u{1BAB}-\u{1BAD}\u{1BE6}\u{1BE8}-\u{1BE9}\u{1BED}\u{1BEF}-\u{1BF1}\u{1C2C}-\u{1C33}\u{1C36}-\u{1C37}\u{1CD0}-\u{1CD2}\u{1CD4}-\u{1CE0}\u{1CE2}-\u{1CE8}\u{1CED}\u{1CF4}\u{1CF8}-\u{1CF9}\u{1DC0}-\u{1DFF}\u{200B}\u{200E}-\u{200F}\u{202A}-\u{202E}\u{2060}-\u{2064}\u{206A}-\u{206F}\u{20D0}-\u{20DC}\u{20DD}-\u{20E0}\u{20E1}\u{20E2}-\u{20E4}\u{20E5}-\u{20F0}\u{2CEF}-\u{2CF1}\u{2D7F}\u{2DE0}-\u{2DFF}\u{302A}-\u{302D}\u{3099}-\u{309A}\u{A66F}\u{A670}-\u{A672}\u{A674}-\u{A67D}\u{A69E}-\u{A69F}\u{A6F0}-\u{A6F1}\u{A802}\u{A806}\u{A80B}\u{A825}-\u{A826}\u{A82C}\u{A8C4}-\u{A8C5}\u{A8E0}-\u{A8F1}\u{A8FF}\u{A926}-\u{A92D}\u{A947}-\u{A951}\u{A980}-\u{A982}\u{A9B3}\u{A9B6}-\u{A9B9}\u{A9BC}-\u{A9BD}\u{A9E5}\u{AA29}-\u{AA2E}\u{AA31}-\u{AA32}\u{AA35}-\u{AA36}\u{AA43}\u{AA4C}\u{AA7C}\u{AAB0}\u{AAB2}-\u{AAB4}\u{AAB7}-\u{AAB8}\u{AABE}-\u{AABF}\u{AAC1}\u{AAEC}-\u{AAED}\u{AAF6}\u{ABE5}\u{ABE8}\u{ABED}\u{FB1E}\u{FE00}-\u{FE0F}\u{FE20}-\u{FE2F}\u{FEFF}\u{FFF9}-\u{FFFB}\u{101FD}\u{102E0}\u{10376}-\u{1037A}\u{10A01}-\u{10A03}\u{10A05}-\u{10A06}\u{10A0C}-\u{10A0F}\u{10A38}-\u{10A3A}\u{10A3F}\u{10AE5}-\u{10AE6}\u{10D24}-\u{10D27}\u{10EAB}-\u{10EAC}\u{10EFD}-\u{10EFF}\u{10F46}-\u{10F50}\u{10F82}-\u{10F85}\u{11001}\u{11038}-\u{11046}\u{11070}\u{11073}-\u{11074}\u{1107F}-\u{11081}\u{110B3}-\u{110B6}\u{110B9}-\u{110BA}\u{110C2}\u{11100}-\u{11102}\u{11127}-\u{1112B}\u{1112D}-\u{11134}\u{11173}\u{11180}-\u{11181}\u{111B6}-\u{111BE}\u{111C9}-\u{111CC}\u{111CF}\u{1122F}-\u{11231}\u{11234}\u{11236}-\u{11237}\u{1123E}\u{11241}\u{112DF}\u{112E3}-\u{112EA}\u{11300}-\u{11301}\u{1133B}-\u{1133C}\u{11340}\u{11366}-\u{1136C}\u{11370}-\u{11374}\u{11438}-\u{1143F}\u{11442}-\u{11444}\u{11446}\u{1145E}\u{114B3}-\u{114B8}\u{114BA}\u{114BF}-\u{114C0}\u{114C2}-\u{114C3}\u{115B2}-\u{115B5}\u{115BC}-\u{115BD}\u{115BF}-\u{115C0}\u{115DC}-\u{115DD}\u{11633}-\u{1163A}\u{1163D}\u{1163F}-\u{11640}\u{116AB}\u{116AD}\u{116B0}-\u{116B5}\u{116B7}\u{1171D}-\u{1171F}\u{11722}-\u{11725}\u{11727}-\u{1172B}\u{1182F}-\u{11837}\u{11839}-\u{1183A}\u{1193B}-\u{1193C}\u{1193E}\u{11943}\u{119D4}-\u{119D7}\u{119DA}-\u{119DB}\u{119E0}\u{11A01}-\u{11A0A}\u{11A33}-\u{11A38}\u{11A3B}-\u{11A3E}\u{11A47}\u{11A51}-\u{11A56}\u{11A59}-\u{11A5B}\u{11A8A}-\u{11A96}\u{11A98}-\u{11A99}\u{11C30}-\u{11C36}\u{11C38}-\u{11C3D}\u{11C3F}\u{11C92}-\u{11CA7}\u{11CAA}-\u{11CB0}\u{11CB2}-\u{11CB3}\u{11CB5}-\u{11CB6}\u{11D31}-\u{11D36}\u{11D3A}\u{11D3C}-\u{11D3D}\u{11D3F}-\u{11D45}\u{11D47}\u{11D90}-\u{11D91}\u{11D95}\u{11D97}\u{11EF3}-\u{11EF4}\u{11F00}-\u{11F01}\u{11F36}-\u{11F3A}\u{11F40}\u{11F42}\u{13430}-\u{1343F}\u{13440}\u{13447}-\u{13455}\u{16AF0}-\u{16AF4}\u{16B30}-\u{16B36}\u{16F4F}\u{16F8F}-\u{16F92}\u{16FE4}\u{1BC9D}-\u{1BC9E}\u{1BCA0}-\u{1BCA3}\u{1CF00}-\u{1CF2D}\u{1CF30}-\u{1CF46}\u{1D167}-\u{1D169}\u{1D173}-\u{1D17A}\u{1D17B}-\u{1D182}\u{1D185}-\u{1D18B}\u{1D1AA}-\u{1D1AD}\u{1D242}-\u{1D244}\u{1DA00}-\u{1DA36}\u{1DA3B}-\u{1DA6C}\u{1DA75}\u{1DA84}\u{1DA9B}-\u{1DA9F}\u{1DAA1}-\u{1DAAF}\u{1E000}-\u{1E006}\u{1E008}-\u{1E018}\u{1E01B}-\u{1E021}\u{1E023}-\u{1E024}\u{1E026}-\u{1E02A}\u{1E08F}\u{1E130}-\u{1E136}\u{1E2AE}\u{1E2EC}-\u{1E2EF}\u{1E4EC}-\u{1E4EF}\u{1E8D0}-\u{1E8D6}\u{1E944}-\u{1E94A}\u{1E94B}\u{E0001}\u{E0020}-\u{E007F}\u{E0100}-\u{E01EF}]'
21
+ JOINING_TYPE_R_CHARACTER_CLASS = '[\u{0622}-\u{0625}\u{0627}\u{0629}\u{062F}-\u{0632}\u{0648}\u{0671}-\u{0673}\u{0675}-\u{0677}\u{0688}-\u{0699}\u{06C0}\u{06C3}-\u{06CB}\u{06CD}\u{06CF}\u{06D2}-\u{06D3}\u{06D5}\u{06EE}-\u{06EF}\u{0710}\u{0715}-\u{0719}\u{071E}\u{0728}\u{072A}\u{072C}\u{072F}\u{074D}\u{0759}-\u{075B}\u{076B}-\u{076C}\u{0771}\u{0773}-\u{0774}\u{0778}-\u{0779}\u{0840}\u{0846}-\u{0847}\u{0849}\u{0854}\u{0856}-\u{0858}\u{0867}\u{0869}-\u{086A}\u{0870}-\u{0882}\u{088E}\u{08AA}-\u{08AC}\u{08AE}\u{08B1}-\u{08B2}\u{08B9}\u{10AC5}\u{10AC7}\u{10AC9}-\u{10ACA}\u{10ACE}-\u{10AD2}\u{10ADD}\u{10AE1}\u{10AE4}\u{10AEF}\u{10B81}\u{10B83}-\u{10B85}\u{10B89}\u{10B8C}\u{10B8E}-\u{10B8F}\u{10B91}\u{10BA9}-\u{10BAC}\u{10D22}\u{10F33}\u{10F54}\u{10F74}-\u{10F75}\u{10FB4}-\u{10FB6}\u{10FB9}-\u{10FBA}\u{10FBD}\u{10FC2}-\u{10FC3}\u{10FC9}]'
22
+ # https://datatracker.ietf.org/doc/html/rfc5892#appendix-A.1
23
+ # https://datatracker.ietf.org/doc/html/rfc5892#appendix-A.2
24
+ ZERO_WIDTH_VIRAMA = "#{VIRAMA_CHARACTER_CLASS}[\\u{200C}\\u{200D}]"
25
+ ZERO_WIDTH_NON_JOINER_JOINING_TYPE = "[#{JOINING_TYPE_L_CHARACTER_CLASS}#{JOINING_TYPE_D_CHARACTER_CLASS}]#{JOINING_TYPE_T_CHARACTER_CLASS}*\\u{200C}#{JOINING_TYPE_T_CHARACTER_CLASS}*[#{JOINING_TYPE_R_CHARACTER_CLASS}#{JOINING_TYPE_D_CHARACTER_CLASS}]"
26
+ # https://datatracker.ietf.org/doc/html/rfc5892#appendix-A.3
27
+ MIDDLE_DOT = '\u{006C}\u{00B7}\u{006C}'
28
+ # https://datatracker.ietf.org/doc/html/rfc5892#appendix-A.4
29
+ GREEK_LOWER_NUMERAL_SIGN = '\u{0375}\p{Greek}'
30
+ # https://datatracker.ietf.org/doc/html/rfc5892#appendix-A.5
31
+ # https://datatracker.ietf.org/doc/html/rfc5892#appendix-A.6
32
+ HEBREW_PUNCTUATION = '\p{Hebrew}[\u{05F3}\u{05F4}]'
33
+ CONTEXT_REGEX = /(#{ZERO_WIDTH_VIRAMA}|#{ZERO_WIDTH_NON_JOINER_JOINING_TYPE}|#{MIDDLE_DOT}|#{GREEK_LOWER_NUMERAL_SIGN}|#{HEBREW_PUNCTUATION})/.freeze
34
+ # https://datatracker.ietf.org/doc/html/rfc5892#appendix-A.7
35
+ KATAKANA_MIDDLE_DOT_REGEX = /\u{30FB}/.freeze
36
+ KATAKANA_MIDDLE_DOT_CONTEXT_REGEX = /[\p{Hiragana}\p{Katakana}\p{Han}]/.freeze
37
+ # https://datatracker.ietf.org/doc/html/rfc5892#appendix-A.8
38
+ # https://datatracker.ietf.org/doc/html/rfc5892#appendix-A.9
39
+ ARABIC_INDIC_DIGITS_REGEX = /[\u{0660}-\u{0669}]/.freeze
40
+ ARABIC_EXTENDED_DIGITS_REGEX = /[\u{06F0}-\u{06F9}]/.freeze
41
+
42
+ def valid_hostname?(data)
43
+ data.split('.').map do |a_label|
44
+ return false if a_label.size > 63
45
+ u_label = SimpleIDN.to_unicode(a_label)
46
+ # https://datatracker.ietf.org/doc/html/rfc5891#section-4.2.3.1
47
+ return false if u_label.slice(2, 2) == '--'
48
+ return false if ARABIC_INDIC_DIGITS_REGEX.match?(u_label) && ARABIC_EXTENDED_DIGITS_REGEX.match?(u_label)
49
+ u_label.gsub!(CONTEXT_REGEX, 'ok')
50
+ u_label.gsub!(KATAKANA_MIDDLE_DOT_REGEX, 'ok') if KATAKANA_MIDDLE_DOT_CONTEXT_REGEX.match?(u_label)
51
+ u_label
52
+ end.join('.').match?(HOSTNAME_REGEX)
53
+ rescue SimpleIDN::ConversionError
54
+ false
55
+ end
56
+ end
57
+ end
58
+ end
@@ -1,15 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
  module JSONSchemer
3
3
  module Format
4
+ include Hostname
5
+
4
6
  # this is no good
5
7
  EMAIL_REGEX = /\A[^@\s]+@([\p{L}\d-]+\.)+[\p{L}\d\-]{2,}\z/i.freeze
6
- LABEL_REGEX_STRING = '[\p{L}\p{N}]([\p{L}\p{N}\-]*[\p{L}\p{N}])?'
7
- HOSTNAME_REGEX = /\A(#{LABEL_REGEX_STRING}\.)*#{LABEL_REGEX_STRING}\z/i.freeze
8
8
  JSON_POINTER_REGEX_STRING = '(\/([^~\/]|~[01])*)*'
9
9
  JSON_POINTER_REGEX = /\A#{JSON_POINTER_REGEX_STRING}\z/.freeze
10
10
  RELATIVE_JSON_POINTER_REGEX = /\A(0|[1-9]\d*)(#|#{JSON_POINTER_REGEX_STRING})?\z/.freeze
11
11
  DATE_TIME_OFFSET_REGEX = /(Z|[\+\-]([01][0-9]|2[0-3]):[0-5][0-9])\z/i.freeze
12
- INVALID_QUERY_REGEX = /[[:space:]]/.freeze
12
+ HOUR_24_REGEX = /T24/.freeze
13
+ LEAP_SECOND_REGEX = /T\d{2}:\d{2}:6/.freeze
14
+ IP_REGEX = /\A[\h:.]+\z/.freeze
15
+ INVALID_QUERY_REGEX = /\s/.freeze
13
16
 
14
17
  def valid_spec_format?(data, format)
15
18
  case format
@@ -28,9 +31,9 @@ module JSONSchemer
28
31
  when 'idn-hostname'
29
32
  valid_hostname?(data)
30
33
  when 'ipv4'
31
- valid_ip?(data, :v4)
34
+ valid_ip?(data, Socket::AF_INET)
32
35
  when 'ipv6'
33
- valid_ip?(data, :v6)
36
+ valid_ip?(data, Socket::AF_INET6)
34
37
  when 'uri'
35
38
  valid_uri?(data)
36
39
  when 'uri-reference'
@@ -47,6 +50,8 @@ module JSONSchemer
47
50
  valid_relative_json_pointer?(data)
48
51
  when 'regex'
49
52
  EcmaReValidator.valid?(data)
53
+ else
54
+ raise UnknownFormat, format
50
55
  end
51
56
  end
52
57
 
@@ -58,25 +63,24 @@ module JSONSchemer
58
63
  end
59
64
 
60
65
  def valid_date_time?(data)
61
- DateTime.rfc3339(data)
66
+ return false if HOUR_24_REGEX.match?(data)
67
+ datetime = DateTime.rfc3339(data)
68
+ return false if LEAP_SECOND_REGEX.match?(data) && datetime.to_time.utc.strftime('%H:%M') != '23:59'
62
69
  DATE_TIME_OFFSET_REGEX.match?(data)
63
- rescue ArgumentError => e
64
- raise e unless e.message == 'invalid date'
70
+ rescue ArgumentError
65
71
  false
66
72
  end
67
73
 
68
74
  def valid_email?(data)
69
- EMAIL_REGEX.match?(data)
70
- end
71
-
72
- def valid_hostname?(data)
73
- HOSTNAME_REGEX.match?(data) && data.split('.').all? { |label| label.size <= 63 }
75
+ return false unless EMAIL_REGEX.match?(data)
76
+ local, _domain = data.partition('@')
77
+ !local.start_with?('.') && !local.end_with?('.') && !local.include?('..')
74
78
  end
75
79
 
76
- def valid_ip?(data, type)
77
- ip_address = IPAddr.new(data)
78
- type == :v4 ? ip_address.ipv4? : ip_address.ipv6?
79
- rescue IPAddr::InvalidAddressError
80
+ def valid_ip?(data, family)
81
+ IPAddr.new(data, family)
82
+ IP_REGEX.match?(data)
83
+ rescue IPAddr::Error
80
84
  false
81
85
  end
82
86
 
@@ -101,7 +105,14 @@ module JSONSchemer
101
105
  end
102
106
 
103
107
  def iri_escape(data)
104
- URI.escape(data, /[^[[:ascii:]]]/)
108
+ data.gsub(/[^[:ascii:]]/) do |match|
109
+ us = match
110
+ tmp = +''
111
+ us.each_byte do |uc|
112
+ tmp << sprintf('%%%02X', uc)
113
+ end
114
+ tmp
115
+ end.force_encoding(Encoding::US_ASCII)
105
116
  end
106
117
 
107
118
  def valid_uri_template?(data)
@@ -30,12 +30,23 @@ module JSONSchemer
30
30
  :eol => '\z'
31
31
  }.freeze
32
32
 
33
+ ECMA_262_REGEXP_RESOLVER = proc do |pattern|
34
+ Regexp.new(
35
+ Regexp::Scanner.scan(pattern).map do |type, token, text|
36
+ type == :anchor ? RUBY_REGEX_ANCHORS_TO_ECMA_262.fetch(token, text) : text
37
+ end.join
38
+ )
39
+ end
40
+
33
41
  INSERT_DEFAULT_PROPERTY = proc do |data, property, property_schema, _parent|
34
42
  if !data.key?(property) && property_schema.is_a?(Hash) && property_schema.key?('default')
35
43
  data[property] = property_schema.fetch('default').clone
36
44
  end
37
45
  end
38
46
 
47
+ JSON_POINTER_TOKEN_ESCAPE_CHARS = { '~' => '~0', '/' => '~1' }
48
+ JSON_POINTER_TOKEN_ESCAPE_REGEXP = Regexp.union(JSON_POINTER_TOKEN_ESCAPE_CHARS.keys)
49
+
39
50
  def initialize(
40
51
  schema,
41
52
  format: true,
@@ -44,7 +55,8 @@ module JSONSchemer
44
55
  after_property_validation: nil,
45
56
  formats: nil,
46
57
  keywords: nil,
47
- ref_resolver: DEFAULT_REF_RESOLVER
58
+ ref_resolver: DEFAULT_REF_RESOLVER,
59
+ regexp_resolver: 'ecma'
48
60
  )
49
61
  raise InvalidSymbolKey, 'schemas must use string keys' if schema.is_a?(Hash) && !schema.empty? && !schema.first.first.is_a?(String)
50
62
  @root = schema
@@ -54,7 +66,15 @@ module JSONSchemer
54
66
  @after_property_validation = [*after_property_validation]
55
67
  @formats = formats
56
68
  @keywords = keywords
57
- @ref_resolver = ref_resolver == 'net/http' ? CachedRefResolver.new(&NET_HTTP_REF_RESOLVER) : ref_resolver
69
+ @ref_resolver = ref_resolver == 'net/http' ? CachedResolver.new(&NET_HTTP_REF_RESOLVER) : ref_resolver
70
+ @regexp_resolver = case regexp_resolver
71
+ when 'ecma'
72
+ CachedResolver.new(&ECMA_262_REGEXP_RESOLVER)
73
+ when 'ruby'
74
+ CachedResolver.new(&Regexp.method(:new))
75
+ else
76
+ regexp_resolver
77
+ end
58
78
  end
59
79
 
60
80
  def valid?(data)
@@ -180,7 +200,7 @@ module JSONSchemer
180
200
 
181
201
  if if_schema && valid_instance?(instance.merge(schema: if_schema, before_property_validation: false, after_property_validation: false))
182
202
  validate_instance(instance.merge(schema: then_schema, schema_pointer: "#{instance.schema_pointer}/then"), &block) unless then_schema.nil?
183
- elsif if_schema
203
+ elsif schema.key?('if')
184
204
  validate_instance(instance.merge(schema: else_schema, schema_pointer: "#{instance.schema_pointer}/else"), &block) unless else_schema.nil?
185
205
  end
186
206
 
@@ -204,7 +224,7 @@ module JSONSchemer
204
224
 
205
225
  private
206
226
 
207
- attr_reader :root, :formats, :keywords, :ref_resolver
227
+ attr_reader :root, :formats, :keywords, :ref_resolver, :regexp_resolver
208
228
 
209
229
  def id_keyword
210
230
  ID_KEYWORD
@@ -225,10 +245,12 @@ module JSONSchemer
225
245
  def child(schema)
226
246
  JSONSchemer.schema(
227
247
  schema,
248
+ default_schema_class: self.class,
228
249
  format: format?,
229
250
  formats: formats,
230
251
  keywords: keywords,
231
- ref_resolver: ref_resolver
252
+ ref_resolver: ref_resolver,
253
+ regexp_resolver: regexp_resolver
232
254
  )
233
255
  end
234
256
 
@@ -355,8 +377,7 @@ module JSONSchemer
355
377
  validate_exclusive_minimum(instance, exclusive_minimum, minimum, &block) if exclusive_minimum
356
378
 
357
379
  if multiple_of
358
- quotient = data / multiple_of.to_f
359
- yield error(instance, 'multipleOf') unless quotient.floor == quotient
380
+ yield error(instance, 'multipleOf') unless BigDecimal(data.to_s).modulo(multiple_of).zero?
360
381
  end
361
382
  end
362
383
 
@@ -399,7 +420,7 @@ module JSONSchemer
399
420
 
400
421
  yield error(instance, 'maxLength') if max_length && data.size > max_length
401
422
  yield error(instance, 'minLength') if min_length && data.size < min_length
402
- yield error(instance, 'pattern') if pattern && ecma_262_regex(pattern) !~ data
423
+ yield error(instance, 'pattern') if pattern && !resolve_regexp(pattern).match?(data)
403
424
  yield error(instance, 'format') if format? && spec_format?(format) && !valid_spec_format?(data, format)
404
425
 
405
426
  if content_encoding || content_media_type
@@ -514,7 +535,8 @@ module JSONSchemer
514
535
  dependencies.each do |key, value|
515
536
  next unless data.key?(key)
516
537
  subschema = value.is_a?(Array) ? { 'required' => value } : value
517
- subinstance = instance.merge(schema: subschema, schema_pointer: "#{instance.schema_pointer}/dependencies/#{key}")
538
+ escaped_key = escape_json_pointer_token(key)
539
+ subinstance = instance.merge(schema: subschema, schema_pointer: "#{instance.schema_pointer}/dependencies/#{escaped_key}")
518
540
  validate_instance(subinstance, &block)
519
541
  end
520
542
  end
@@ -528,6 +550,8 @@ module JSONSchemer
528
550
 
529
551
  regex_pattern_properties = nil
530
552
  data.each do |key, value|
553
+ escaped_key = escape_json_pointer_token(key)
554
+
531
555
  unless property_names.nil?
532
556
  subinstance = instance.merge(
533
557
  data: key,
@@ -542,9 +566,9 @@ module JSONSchemer
542
566
  if properties && properties.key?(key)
543
567
  subinstance = instance.merge(
544
568
  data: value,
545
- data_pointer: "#{instance.data_pointer}/#{key}",
569
+ data_pointer: "#{instance.data_pointer}/#{escaped_key}",
546
570
  schema: properties[key],
547
- schema_pointer: "#{instance.schema_pointer}/properties/#{key}"
571
+ schema_pointer: "#{instance.schema_pointer}/properties/#{escaped_key}"
548
572
  )
549
573
  validate_instance(subinstance, &block)
550
574
  matched_key = true
@@ -552,15 +576,16 @@ module JSONSchemer
552
576
 
553
577
  if pattern_properties
554
578
  regex_pattern_properties ||= pattern_properties.map do |pattern, property_schema|
555
- [pattern, ecma_262_regex(pattern), property_schema]
579
+ [pattern, resolve_regexp(pattern), property_schema]
556
580
  end
557
581
  regex_pattern_properties.each do |pattern, regex, property_schema|
582
+ escaped_pattern = escape_json_pointer_token(pattern)
558
583
  if regex.match?(key)
559
584
  subinstance = instance.merge(
560
585
  data: value,
561
- data_pointer: "#{instance.data_pointer}/#{key}",
586
+ data_pointer: "#{instance.data_pointer}/#{escaped_key}",
562
587
  schema: property_schema,
563
- schema_pointer: "#{instance.schema_pointer}/patternProperties/#{pattern}"
588
+ schema_pointer: "#{instance.schema_pointer}/patternProperties/#{escaped_pattern}"
564
589
  )
565
590
  validate_instance(subinstance, &block)
566
591
  matched_key = true
@@ -573,7 +598,7 @@ module JSONSchemer
573
598
  unless additional_properties.nil?
574
599
  subinstance = instance.merge(
575
600
  data: value,
576
- data_pointer: "#{instance.data_pointer}/#{key}",
601
+ data_pointer: "#{instance.data_pointer}/#{escaped_key}",
577
602
  schema: additional_properties,
578
603
  schema_pointer: "#{instance.schema_pointer}/additionalProperties"
579
604
  )
@@ -592,18 +617,12 @@ module JSONSchemer
592
617
 
593
618
  def safe_strict_decode64(data)
594
619
  Base64.strict_decode64(data)
595
- rescue ArgumentError => e
596
- raise e unless e.message == 'invalid base64'
620
+ rescue ArgumentError
597
621
  nil
598
622
  end
599
623
 
600
- def ecma_262_regex(pattern)
601
- @ecma_262_regex ||= {}
602
- @ecma_262_regex[pattern] ||= Regexp.new(
603
- Regexp::Scanner.scan(pattern).map do |type, token, text|
604
- type == :anchor ? RUBY_REGEX_ANCHORS_TO_ECMA_262.fetch(token, text) : text
605
- end.join
606
- )
624
+ def escape_json_pointer_token(token)
625
+ token.gsub(JSON_POINTER_TOKEN_ESCAPE_REGEXP, JSON_POINTER_TOKEN_ESCAPE_CHARS)
607
626
  end
608
627
 
609
628
  def join_uri(a, b)
@@ -653,6 +672,10 @@ module JSONSchemer
653
672
  def resolve_ref(uri)
654
673
  ref_resolver.call(uri) || raise(InvalidRefResolution, uri.to_s)
655
674
  end
675
+
676
+ def resolve_regexp(pattern)
677
+ regexp_resolver.call(pattern) || raise(InvalidRegexpResolution, pattern)
678
+ end
656
679
  end
657
680
  end
658
681
  end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module JSONSchemer
3
- VERSION = '0.2.16'
3
+ VERSION = '0.2.25'
4
4
  end
data/lib/json_schemer.rb CHANGED
@@ -1,21 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
  require 'base64'
3
+ require 'bigdecimal'
3
4
  require 'ipaddr'
4
5
  require 'json'
5
6
  require 'net/http'
6
7
  require 'pathname'
8
+ require 'set'
7
9
  require 'time'
8
10
  require 'uri'
9
11
 
10
12
  require 'ecma-re-validator'
11
13
  require 'hana'
12
14
  require 'regexp_parser'
15
+ require 'simpleidn'
13
16
  require 'uri_template'
14
17
 
15
18
  require 'json_schemer/version'
19
+ require 'json_schemer/format/hostname'
16
20
  require 'json_schemer/format'
17
21
  require 'json_schemer/errors'
18
- require 'json_schemer/cached_ref_resolver'
22
+ require 'json_schemer/cached_resolver'
19
23
  require 'json_schemer/schema/base'
20
24
  require 'json_schemer/schema/draft4'
21
25
  require 'json_schemer/schema/draft6'
@@ -24,49 +28,57 @@ require 'json_schemer/schema/draft7'
24
28
  module JSONSchemer
25
29
  class UnsupportedMetaSchema < StandardError; end
26
30
  class UnknownRef < StandardError; end
31
+ class UnknownFormat < StandardError; end
27
32
  class InvalidRefResolution < StandardError; end
33
+ class InvalidRegexpResolution < StandardError; end
28
34
  class InvalidFileURI < StandardError; end
29
35
  class InvalidSymbolKey < StandardError; end
30
36
 
31
- DRAFT_CLASS_BY_META_SCHEMA = {
37
+ SCHEMA_CLASS_BY_META_SCHEMA = {
32
38
  'http://json-schema.org/schema#' => Schema::Draft4, # Version-less $schema deprecated after Draft 4
33
39
  'http://json-schema.org/draft-04/schema#' => Schema::Draft4,
34
40
  'http://json-schema.org/draft-06/schema#' => Schema::Draft6,
35
41
  'http://json-schema.org/draft-07/schema#' => Schema::Draft7
36
42
  }.freeze
37
43
 
38
- DEFAULT_META_SCHEMA = 'http://json-schema.org/draft-07/schema#'
44
+ WINDOWS_URI_PATH_REGEX = /\A\/[a-z]:/i
39
45
 
40
46
  FILE_URI_REF_RESOLVER = proc do |uri|
41
47
  raise InvalidFileURI, 'must use `file` scheme' unless uri.scheme == 'file'
42
48
  raise InvalidFileURI, 'cannot have a host (use `file:///`)' if uri.host && !uri.host.empty?
43
- JSON.parse(File.read(uri.path))
49
+ path = uri.path
50
+ path = path[1..-1] if path.match?(WINDOWS_URI_PATH_REGEX)
51
+ JSON.parse(File.read(URI::DEFAULT_PARSER.unescape(path)))
44
52
  end
45
53
 
46
54
  class << self
47
- def schema(schema, **options)
55
+ def schema(schema, default_schema_class: Schema::Draft7, **options)
48
56
  case schema
49
57
  when String
50
58
  schema = JSON.parse(schema)
51
59
  when Pathname
52
- uri = URI.parse("file://#{schema.realpath}")
60
+ uri = URI.parse(File.join('file:', URI::DEFAULT_PARSER.escape(schema.realpath.to_s)))
53
61
  if options.key?(:ref_resolver)
54
62
  schema = FILE_URI_REF_RESOLVER.call(uri)
55
63
  else
56
- ref_resolver = CachedRefResolver.new(&FILE_URI_REF_RESOLVER)
64
+ ref_resolver = CachedResolver.new(&FILE_URI_REF_RESOLVER)
57
65
  schema = ref_resolver.call(uri)
58
66
  options[:ref_resolver] = ref_resolver
59
67
  end
60
- schema[draft_class(schema)::ID_KEYWORD] ||= uri.to_s
68
+ schema[draft_class(schema, default_schema_class)::ID_KEYWORD] ||= uri.to_s
61
69
  end
62
- draft_class(schema).new(schema, **options)
70
+ draft_class(schema, default_schema_class).new(schema, **options)
63
71
  end
64
72
 
65
73
  private
66
74
 
67
- def draft_class(schema)
68
- meta_schema = schema.is_a?(Hash) && schema.key?('$schema') ? schema['$schema'] : DEFAULT_META_SCHEMA
69
- DRAFT_CLASS_BY_META_SCHEMA[meta_schema] || raise(UnsupportedMetaSchema, meta_schema)
75
+ def draft_class(schema, default_schema_class)
76
+ if schema.is_a?(Hash) && schema.key?('$schema')
77
+ meta_schema = schema.fetch('$schema')
78
+ SCHEMA_CLASS_BY_META_SCHEMA[meta_schema] || raise(UnsupportedMetaSchema, meta_schema)
79
+ else
80
+ default_schema_class
81
+ end
70
82
  end
71
83
  end
72
84
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: json_schemer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.16
4
+ version: 0.2.25
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Harsha
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-10-10 00:00:00.000000000 Z
11
+ date: 2023-05-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -52,20 +52,34 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '5.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: simplecov
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.22'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.22'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: ecma-re-validator
57
71
  requirement: !ruby/object:Gem::Requirement
58
72
  requirements:
59
73
  - - "~>"
60
74
  - !ruby/object:Gem::Version
61
- version: '0.2'
75
+ version: '0.3'
62
76
  type: :runtime
63
77
  prerelease: false
64
78
  version_requirements: !ruby/object:Gem::Requirement
65
79
  requirements:
66
80
  - - "~>"
67
81
  - !ruby/object:Gem::Version
68
- version: '0.2'
82
+ version: '0.3'
69
83
  - !ruby/object:Gem::Dependency
70
84
  name: hana
71
85
  requirement: !ruby/object:Gem::Requirement
@@ -81,37 +95,52 @@ dependencies:
81
95
  - !ruby/object:Gem::Version
82
96
  version: '1.3'
83
97
  - !ruby/object:Gem::Dependency
84
- name: uri_template
98
+ name: regexp_parser
85
99
  requirement: !ruby/object:Gem::Requirement
86
100
  requirements:
87
101
  - - "~>"
88
102
  - !ruby/object:Gem::Version
89
- version: '0.7'
103
+ version: '2.0'
90
104
  type: :runtime
91
105
  prerelease: false
92
106
  version_requirements: !ruby/object:Gem::Requirement
93
107
  requirements:
94
108
  - - "~>"
95
109
  - !ruby/object:Gem::Version
96
- version: '0.7'
110
+ version: '2.0'
97
111
  - !ruby/object:Gem::Dependency
98
- name: regexp_parser
112
+ name: simpleidn
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '0.2'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '0.2'
125
+ - !ruby/object:Gem::Dependency
126
+ name: uri_template
99
127
  requirement: !ruby/object:Gem::Requirement
100
128
  requirements:
101
129
  - - "~>"
102
130
  - !ruby/object:Gem::Version
103
- version: '1.5'
131
+ version: '0.7'
104
132
  type: :runtime
105
133
  prerelease: false
106
134
  version_requirements: !ruby/object:Gem::Requirement
107
135
  requirements:
108
136
  - - "~>"
109
137
  - !ruby/object:Gem::Version
110
- version: '1.5'
111
- description:
138
+ version: '0.7'
139
+ description:
112
140
  email:
113
141
  - davishmcclurg@gmail.com
114
- executables: []
142
+ executables:
143
+ - json_schemer
115
144
  extensions: []
116
145
  extra_rdoc_files: []
117
146
  files:
@@ -123,12 +152,16 @@ files:
123
152
  - README.md
124
153
  - Rakefile
125
154
  - bin/console
155
+ - bin/hostname_character_classes
156
+ - bin/rake
126
157
  - bin/setup
158
+ - exe/json_schemer
127
159
  - json_schemer.gemspec
128
160
  - lib/json_schemer.rb
129
- - lib/json_schemer/cached_ref_resolver.rb
161
+ - lib/json_schemer/cached_resolver.rb
130
162
  - lib/json_schemer/errors.rb
131
163
  - lib/json_schemer/format.rb
164
+ - lib/json_schemer/format/hostname.rb
132
165
  - lib/json_schemer/schema/base.rb
133
166
  - lib/json_schemer/schema/draft4.rb
134
167
  - lib/json_schemer/schema/draft6.rb
@@ -138,13 +171,13 @@ homepage: https://github.com/davishmcclurg/json_schemer
138
171
  licenses:
139
172
  - MIT
140
173
  metadata: {}
141
- post_install_message:
174
+ post_install_message:
142
175
  rdoc_options: []
143
176
  require_paths:
144
177
  - lib
145
178
  required_ruby_version: !ruby/object:Gem::Requirement
146
179
  requirements:
147
- - - "~>"
180
+ - - ">="
148
181
  - !ruby/object:Gem::Version
149
182
  version: '2.4'
150
183
  required_rubygems_version: !ruby/object:Gem::Requirement
@@ -153,8 +186,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
153
186
  - !ruby/object:Gem::Version
154
187
  version: '0'
155
188
  requirements: []
156
- rubygems_version: 3.1.2
157
- signing_key:
189
+ rubygems_version: 3.4.10
190
+ signing_key:
158
191
  specification_version: 4
159
192
  summary: JSON Schema validator. Supports drafts 4, 6, and 7.
160
193
  test_files: []
@@ -1,14 +0,0 @@
1
- # frozen_string_literal: true
2
- module JSONSchemer
3
- class CachedRefResolver
4
- def initialize(&ref_resolver)
5
- @ref_resolver = ref_resolver
6
- @cache = {}
7
- end
8
-
9
- def call(uri)
10
- @cache[uri] = @ref_resolver.call(uri) unless @cache.key?(uri)
11
- @cache[uri]
12
- end
13
- end
14
- end