json_schemer-0.2.18/0000755000004100000410000000000014030134656014313 5ustar www-datawww-datajson_schemer-0.2.18/Gemfile.lock0000644000004100000410000000076714030134656016547 0ustar www-datawww-dataPATH remote: . specs: json_schemer (0.2.18) ecma-re-validator (~> 0.3) hana (~> 1.3) regexp_parser (~> 2.0) uri_template (~> 0.7) GEM remote: https://rubygems.org/ specs: ecma-re-validator (0.3.0) regexp_parser (~> 2.0) hana (1.3.7) minitest (5.14.3) rake (13.0.1) regexp_parser (2.1.1) uri_template (0.7.0) PLATFORMS ruby DEPENDENCIES bundler (~> 2.0) json_schemer! minitest (~> 5.0) rake (~> 13.0) BUNDLED WITH 2.2.11 json_schemer-0.2.18/json_schemer.gemspec0000644000004100000410000000316214030134656020341 0ustar www-datawww-data lib = File.expand_path("../lib", __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require "json_schemer/version" Gem::Specification.new do |spec| spec.name = "json_schemer" spec.version = JSONSchemer::VERSION spec.authors = ["David Harsha"] spec.email = ["davishmcclurg@gmail.com"] spec.summary = "JSON Schema validator. Supports drafts 4, 6, and 7." spec.homepage = "https://github.com/davishmcclurg/json_schemer" spec.license = "MIT" spec.files = `git ls-files -z`.split("\x0").reject do |f| f.match(%r{^(test|spec|features|JSON-Schema-Test-Suite)/}) end spec.bindir = "exe" spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ["lib"] spec.required_ruby_version = '>= 2.4' spec.add_development_dependency "bundler", "~> 2.0" spec.add_development_dependency "rake", "~> 13.0" spec.add_development_dependency "minitest", "~> 5.0" # spec.add_development_dependency "benchmark-ips", "~> 2.7.2" # spec.add_development_dependency "jschema", "~> 0.2.1" # spec.add_development_dependency "json-schema", "~> 2.8.0" # spec.add_development_dependency "json_schema", "~> 0.17.0" # spec.add_development_dependency "json_validation", "~> 0.1.0" # spec.add_development_dependency "jsonschema", "~> 2.0.2" # spec.add_development_dependency "rj_schema", "~> 0.2.0" spec.add_runtime_dependency "ecma-re-validator", "~> 0.3" spec.add_runtime_dependency "hana", "~> 1.3" spec.add_runtime_dependency "uri_template", "~> 0.7" spec.add_runtime_dependency "regexp_parser", "~> 2.0" end json_schemer-0.2.18/README.md0000644000004100000410000000554714030134656015605 0ustar www-datawww-data# JSONSchemer JSON Schema validator. Supports drafts 4, 6, and 7. ## Installation Add this line to your application's Gemfile: ```ruby gem 'json_schemer' ``` And then execute: $ bundle Or install it yourself as: $ gem install json_schemer ## Usage ```ruby require 'json_schemer' schema = { 'type' => 'object', 'properties' => { 'abc' => { 'type' => 'integer', 'minimum' => 11 } } } schemer = JSONSchemer.schema(schema) # true/false validation schemer.valid?({ 'abc' => 11 }) # => true schemer.valid?({ 'abc' => 10 }) # => false # error validation (`validate` returns an enumerator) schemer.validate({ 'abc' => 10 }).to_a # => [{"data"=>10, "schema"=>{"type"=>"integer", "minimum"=>11}, "pointer"=>"#/abc", "type"=>"minimum"}] # default property values data = {} JSONSchemer.schema( { 'properties' => { 'foo' => { 'default' => 'bar' } } }, insert_property_defaults: true ).valid?(data) data # => {"foo"=>"bar"} # schema files require 'pathname' schema = Pathname.new('/path/to/schema.json') schemer = JSONSchemer.schema(schema) # schema json string schema = '{ "type": "integer" }' schemer = JSONSchemer.schema(schema) ``` ## Options ```ruby JSONSchemer.schema( schema, # validate `format` (https://tools.ietf.org/html/draft-handrews-json-schema-validation-00#section-7) # true/false # default: true format: true, # insert default property values during validation # true/false # default: false insert_property_defaults: true, # modify properties during validation. You can pass one Proc or a list of Procs to modify data. # Proc/[Proc] # default: nil before_property_validation: proc do |data, property, property_schema, _parent| data[property] ||= 42 end, # modify properties after validation. You can pass one Proc or a list of Procs to modify data. # Proc/[Proc] # default: nil after_property_validation: proc do |data, property, property_schema, _parent| data[property] = Date.iso8601(data[property]) if property_schema.is_a?(Hash) && property_schema['format'] == 'date' end, # resolve external references # 'net/http'/proc/lambda/respond_to?(:call) # 'net/http': proc { |uri| JSON.parse(Net::HTTP.get(uri)) } # default: proc { |uri| raise UnknownRef, uri.to_s } ref_resolver: 'net/http' ) ``` ## 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. ## Build Status ![Build Status](https://github.com/davishmcclurg/json_schemer/workflows/ci/badge.svg) ## Contributing Bug reports and pull requests are welcome on GitHub at https://github.com/davishmcclurg/json_schemer. ## License The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). json_schemer-0.2.18/bin/0000755000004100000410000000000014030134656015063 5ustar www-datawww-datajson_schemer-0.2.18/bin/console0000755000004100000410000000053314030134656016454 0ustar www-datawww-data#!/usr/bin/env ruby require "bundler/setup" require "json_schemer" # You can add fixtures and/or initialization code here to make experimenting # with your gem easier. You can also use a different console, if you like. # (If you use this, don't forget to add pry to your Gemfile!) # require "pry" # Pry.start require "irb" IRB.start(__FILE__) json_schemer-0.2.18/bin/setup0000755000004100000410000000020314030134656016144 0ustar www-datawww-data#!/usr/bin/env bash set -euo pipefail IFS=$'\n\t' set -vx bundle install # Do any other automated setup that you need to do here json_schemer-0.2.18/.gitignore0000644000004100000410000000013014030134656016275 0ustar www-datawww-data/.bundle/ /.yardoc /_yardoc/ /coverage/ /doc/ /pkg/ /spec/reports/ /tmp/ /.ruby-version json_schemer-0.2.18/Rakefile0000644000004100000410000000030614030134656015757 0ustar www-datawww-datarequire "bundler/gem_tasks" require "rake/testtask" Rake::TestTask.new(:test) do |t| t.libs << "test" t.libs << "lib" t.test_files = FileList["test/**/*_test.rb"] end task :default => :test json_schemer-0.2.18/lib/0000755000004100000410000000000014030134656015061 5ustar www-datawww-datajson_schemer-0.2.18/lib/json_schemer.rb0000644000004100000410000000456514030134656020077 0ustar www-datawww-data# frozen_string_literal: true require 'base64' require 'ipaddr' require 'json' require 'net/http' require 'pathname' require 'time' require 'uri' require 'ecma-re-validator' require 'hana' require 'regexp_parser' require 'uri_template' require 'json_schemer/version' require 'json_schemer/format' require 'json_schemer/errors' require 'json_schemer/cached_ref_resolver' require 'json_schemer/schema/base' require 'json_schemer/schema/draft4' require 'json_schemer/schema/draft6' require 'json_schemer/schema/draft7' module JSONSchemer class UnsupportedMetaSchema < StandardError; end class UnknownRef < StandardError; end class InvalidRefResolution < StandardError; end class InvalidFileURI < StandardError; end class InvalidSymbolKey < StandardError; end DRAFT_CLASS_BY_META_SCHEMA = { 'http://json-schema.org/schema#' => Schema::Draft4, # Version-less $schema deprecated after Draft 4 'http://json-schema.org/draft-04/schema#' => Schema::Draft4, 'http://json-schema.org/draft-06/schema#' => Schema::Draft6, 'http://json-schema.org/draft-07/schema#' => Schema::Draft7 }.freeze DEFAULT_META_SCHEMA = 'http://json-schema.org/draft-07/schema#' WINDOWS_URI_PATH_REGEX = /\A\/[a-z]:/i FILE_URI_REF_RESOLVER = proc do |uri| raise InvalidFileURI, 'must use `file` scheme' unless uri.scheme == 'file' raise InvalidFileURI, 'cannot have a host (use `file:///`)' if uri.host && !uri.host.empty? path = uri.path path = path[1..-1] if path.match?(WINDOWS_URI_PATH_REGEX) JSON.parse(File.read(path)) end class << self def schema(schema, **options) case schema when String schema = JSON.parse(schema) when Pathname uri = URI.parse(File.join('file:', schema.realpath)) if options.key?(:ref_resolver) schema = FILE_URI_REF_RESOLVER.call(uri) else ref_resolver = CachedRefResolver.new(&FILE_URI_REF_RESOLVER) schema = ref_resolver.call(uri) options[:ref_resolver] = ref_resolver end schema[draft_class(schema)::ID_KEYWORD] ||= uri.to_s end draft_class(schema).new(schema, **options) end private def draft_class(schema) meta_schema = schema.is_a?(Hash) && schema.key?('$schema') ? schema['$schema'] : DEFAULT_META_SCHEMA DRAFT_CLASS_BY_META_SCHEMA[meta_schema] || raise(UnsupportedMetaSchema, meta_schema) end end end json_schemer-0.2.18/lib/json_schemer/0000755000004100000410000000000014030134656017540 5ustar www-datawww-datajson_schemer-0.2.18/lib/json_schemer/version.rb0000644000004100000410000000011214030134656021544 0ustar www-datawww-data# frozen_string_literal: true module JSONSchemer VERSION = '0.2.18' end json_schemer-0.2.18/lib/json_schemer/schema/0000755000004100000410000000000014030134656021000 5ustar www-datawww-datajson_schemer-0.2.18/lib/json_schemer/schema/base.rb0000644000004100000410000005364014030134656022247 0ustar www-datawww-data# frozen_string_literal: true module JSONSchemer module Schema class Base include Format Instance = Struct.new(:data, :data_pointer, :schema, :schema_pointer, :parent_uri, :before_property_validation, :after_property_validation) do def merge( data: self.data, data_pointer: self.data_pointer, schema: self.schema, schema_pointer: self.schema_pointer, parent_uri: self.parent_uri, before_property_validation: self.before_property_validation, after_property_validation: self.after_property_validation ) self.class.new(data, data_pointer, schema, schema_pointer, parent_uri, before_property_validation, after_property_validation) end end ID_KEYWORD = '$id' DEFAULT_REF_RESOLVER = proc { |uri| raise UnknownRef, uri.to_s } NET_HTTP_REF_RESOLVER = proc { |uri| JSON.parse(Net::HTTP.get(uri)) } BOOLEANS = Set[true, false].freeze RUBY_REGEX_ANCHORS_TO_ECMA_262 = { :bos => 'A', :eos => 'z', :bol => '\A', :eol => '\z' }.freeze INSERT_DEFAULT_PROPERTY = proc do |data, property, property_schema, _parent| if !data.key?(property) && property_schema.is_a?(Hash) && property_schema.key?('default') data[property] = property_schema.fetch('default').clone end end def initialize( schema, format: true, insert_property_defaults: false, before_property_validation: nil, after_property_validation: nil, formats: nil, keywords: nil, ref_resolver: DEFAULT_REF_RESOLVER ) raise InvalidSymbolKey, 'schemas must use string keys' if schema.is_a?(Hash) && !schema.empty? && !schema.first.first.is_a?(String) @root = schema @format = format @before_property_validation = [*before_property_validation] @before_property_validation.unshift(INSERT_DEFAULT_PROPERTY) if insert_property_defaults @after_property_validation = [*after_property_validation] @formats = formats @keywords = keywords @ref_resolver = ref_resolver == 'net/http' ? CachedRefResolver.new(&NET_HTTP_REF_RESOLVER) : ref_resolver end def valid?(data) valid_instance?(Instance.new(data, '', root, '', nil, @before_property_validation, @after_property_validation)) end def validate(data) validate_instance(Instance.new(data, '', root, '', nil, @before_property_validation, @after_property_validation)) end protected def valid_instance?(instance) validate_instance(instance).none? end def validate_instance(instance, &block) return enum_for(:validate_instance, instance) unless block_given? schema = instance.schema if schema == false yield error(instance, 'schema') return end return if schema == true || schema.empty? type = schema['type'] enum = schema['enum'] all_of = schema['allOf'] any_of = schema['anyOf'] one_of = schema['oneOf'] not_schema = schema['not'] if_schema = schema['if'] then_schema = schema['then'] else_schema = schema['else'] format = schema['format'] ref = schema['$ref'] id = schema[id_keyword] instance.parent_uri = join_uri(instance.parent_uri, id) if ref validate_ref(instance, ref, &block) return end if format? && custom_format?(format) validate_custom_format(instance, formats.fetch(format), &block) end data = instance.data if keywords keywords.each do |keyword, callable| if schema.key?(keyword) result = callable.call(data, schema, instance.data_pointer) if result.is_a?(Array) result.each(&block) elsif !result yield error(instance, keyword) end end end end yield error(instance, 'enum') if enum && !enum.include?(data) yield error(instance, 'const') if schema.key?('const') && schema['const'] != data if all_of all_of.each_with_index do |subschema, index| subinstance = instance.merge( schema: subschema, schema_pointer: "#{instance.schema_pointer}/allOf/#{index}", before_property_validation: false, after_property_validation: false ) validate_instance(subinstance, &block) end end if any_of subschemas = any_of.lazy.with_index.map do |subschema, index| subinstance = instance.merge( schema: subschema, schema_pointer: "#{instance.schema_pointer}/anyOf/#{index}", before_property_validation: false, after_property_validation: false ) validate_instance(subinstance) end subschemas.each { |subschema| subschema.each(&block) } unless subschemas.any?(&:none?) end if one_of subschemas = one_of.map.with_index do |subschema, index| subinstance = instance.merge( schema: subschema, schema_pointer: "#{instance.schema_pointer}/oneOf/#{index}", before_property_validation: false, after_property_validation: false ) validate_instance(subinstance) end valid_subschema_count = subschemas.count(&:none?) if valid_subschema_count > 1 yield error(instance, 'oneOf') elsif valid_subschema_count == 0 subschemas.each { |subschema| subschema.each(&block) } end end unless not_schema.nil? subinstance = instance.merge( schema: not_schema, schema_pointer: "#{instance.schema_pointer}/not", before_property_validation: false, after_property_validation: false ) yield error(subinstance, 'not') if valid_instance?(subinstance) end if if_schema && valid_instance?(instance.merge(schema: if_schema, before_property_validation: false, after_property_validation: false)) validate_instance(instance.merge(schema: then_schema, schema_pointer: "#{instance.schema_pointer}/then"), &block) unless then_schema.nil? elsif if_schema validate_instance(instance.merge(schema: else_schema, schema_pointer: "#{instance.schema_pointer}/else"), &block) unless else_schema.nil? end case type when nil validate_class(instance, &block) when String validate_type(instance, type, &block) when Array if valid_type = type.find { |subtype| valid_instance?(instance.merge(schema: { 'type' => subtype })) } validate_type(instance, valid_type, &block) else yield error(instance, 'type') end end end def ids @ids ||= resolve_ids(root) end private attr_reader :root, :formats, :keywords, :ref_resolver def id_keyword ID_KEYWORD end def format? !!@format end def custom_format?(format) !!(formats && formats.key?(format)) end def spec_format?(format) !custom_format?(format) && supported_format?(format) end def child(schema) JSONSchemer.schema( schema, format: format?, formats: formats, keywords: keywords, ref_resolver: ref_resolver ) end def error(instance, type, details = nil) error = { 'data' => instance.data, 'data_pointer' => instance.data_pointer, 'schema' => instance.schema, 'schema_pointer' => instance.schema_pointer, 'root_schema' => root, 'type' => type, } error['details'] = details if details error end def validate_class(instance, &block) case instance.data when Integer validate_integer(instance, &block) when Numeric validate_number(instance, &block) when String validate_string(instance, &block) when Array validate_array(instance, &block) when Hash validate_object(instance, &block) end end def validate_type(instance, type, &block) case type when 'null' yield error(instance, 'null') unless instance.data.nil? when 'boolean' yield error(instance, 'boolean') unless BOOLEANS.include?(instance.data) when 'number' validate_number(instance, &block) when 'integer' validate_integer(instance, &block) when 'string' validate_string(instance, &block) when 'array' validate_array(instance, &block) when 'object' validate_object(instance, &block) end end def validate_ref(instance, ref, &block) if ref.start_with?('#') schema_pointer = ref.slice(1..-1) if valid_json_pointer?(schema_pointer) ref_pointer = Hana::Pointer.new(URI.decode_www_form_component(schema_pointer)) subinstance = instance.merge( schema: ref_pointer.eval(root), schema_pointer: schema_pointer, parent_uri: (pointer_uri(root, ref_pointer) || instance.parent_uri) ) validate_instance(subinstance, &block) return end end ref_uri = join_uri(instance.parent_uri, ref) if valid_json_pointer?(ref_uri.fragment) ref_pointer = Hana::Pointer.new(URI.decode_www_form_component(ref_uri.fragment)) ref_root = resolve_ref(ref_uri) ref_object = child(ref_root) subinstance = instance.merge( schema: ref_pointer.eval(ref_root), schema_pointer: ref_uri.fragment, parent_uri: (pointer_uri(ref_root, ref_pointer) || ref_uri) ) ref_object.validate_instance(subinstance, &block) elsif id = ids[ref_uri.to_s] subinstance = instance.merge( schema: id.fetch(:schema), schema_pointer: id.fetch(:pointer), parent_uri: ref_uri ) validate_instance(subinstance, &block) else ref_root = resolve_ref(ref_uri) ref_object = child(ref_root) id = ref_object.ids[ref_uri.to_s] || { schema: ref_root, pointer: '' } subinstance = instance.merge( schema: id.fetch(:schema), schema_pointer: id.fetch(:pointer), parent_uri: ref_uri ) ref_object.validate_instance(subinstance, &block) end end def validate_custom_format(instance, custom_format) yield error(instance, 'format') if custom_format != false && !custom_format.call(instance.data, instance.schema) end def validate_exclusive_maximum(instance, exclusive_maximum, maximum) yield error(instance, 'exclusiveMaximum') if instance.data >= exclusive_maximum end def validate_exclusive_minimum(instance, exclusive_minimum, minimum) yield error(instance, 'exclusiveMinimum') if instance.data <= exclusive_minimum end def validate_numeric(instance, &block) schema = instance.schema data = instance.data multiple_of = schema['multipleOf'] maximum = schema['maximum'] exclusive_maximum = schema['exclusiveMaximum'] minimum = schema['minimum'] exclusive_minimum = schema['exclusiveMinimum'] yield error(instance, 'maximum') if maximum && data > maximum yield error(instance, 'minimum') if minimum && data < minimum validate_exclusive_maximum(instance, exclusive_maximum, maximum, &block) if exclusive_maximum validate_exclusive_minimum(instance, exclusive_minimum, minimum, &block) if exclusive_minimum if multiple_of quotient = data / multiple_of.to_f yield error(instance, 'multipleOf') unless quotient.floor == quotient end end def validate_number(instance, &block) unless instance.data.is_a?(Numeric) yield error(instance, 'number') return end validate_numeric(instance, &block) end def validate_integer(instance, &block) data = instance.data if !data.is_a?(Numeric) || (!data.is_a?(Integer) && data.floor != data) yield error(instance, 'integer') return end validate_numeric(instance, &block) end def validate_string(instance, &block) data = instance.data unless data.is_a?(String) yield error(instance, 'string') return end schema = instance.schema max_length = schema['maxLength'] min_length = schema['minLength'] pattern = schema['pattern'] format = schema['format'] content_encoding = schema['contentEncoding'] content_media_type = schema['contentMediaType'] yield error(instance, 'maxLength') if max_length && data.size > max_length yield error(instance, 'minLength') if min_length && data.size < min_length yield error(instance, 'pattern') if pattern && ecma_262_regex(pattern) !~ data yield error(instance, 'format') if format? && spec_format?(format) && !valid_spec_format?(data, format) if content_encoding || content_media_type decoded_data = data if content_encoding decoded_data = case content_encoding.downcase when 'base64' safe_strict_decode64(data) else # '7bit', '8bit', 'binary', 'quoted-printable' raise NotImplementedError end yield error(instance, 'contentEncoding') unless decoded_data end if content_media_type && decoded_data case content_media_type.downcase when 'application/json' yield error(instance, 'contentMediaType') unless valid_json?(decoded_data) else raise NotImplementedError end end end end def validate_array(instance, &block) data = instance.data unless data.is_a?(Array) yield error(instance, 'array') return end schema = instance.schema items = schema['items'] additional_items = schema['additionalItems'] max_items = schema['maxItems'] min_items = schema['minItems'] unique_items = schema['uniqueItems'] contains = schema['contains'] yield error(instance, 'maxItems') if max_items && data.size > max_items yield error(instance, 'minItems') if min_items && data.size < min_items yield error(instance, 'uniqueItems') if unique_items && data.size != data.uniq.size yield error(instance, 'contains') if !contains.nil? && data.all? { |item| !valid_instance?(instance.merge(data: item, schema: contains)) } if items.is_a?(Array) data.each_with_index do |item, index| if index < items.size subinstance = instance.merge( data: item, data_pointer: "#{instance.data_pointer}/#{index}", schema: items[index], schema_pointer: "#{instance.schema_pointer}/items/#{index}" ) validate_instance(subinstance, &block) elsif !additional_items.nil? subinstance = instance.merge( data: item, data_pointer: "#{instance.data_pointer}/#{index}", schema: additional_items, schema_pointer: "#{instance.schema_pointer}/additionalItems" ) validate_instance(subinstance, &block) else break end end elsif !items.nil? data.each_with_index do |item, index| subinstance = instance.merge( data: item, data_pointer: "#{instance.data_pointer}/#{index}", schema: items, schema_pointer: "#{instance.schema_pointer}/items" ) validate_instance(subinstance, &block) end end end def validate_object(instance, &block) data = instance.data unless data.is_a?(Hash) yield error(instance, 'object') return end schema = instance.schema max_properties = schema['maxProperties'] min_properties = schema['minProperties'] required = schema['required'] properties = schema['properties'] pattern_properties = schema['patternProperties'] additional_properties = schema['additionalProperties'] dependencies = schema['dependencies'] property_names = schema['propertyNames'] if instance.before_property_validation && properties properties.each do |property, property_schema| instance.before_property_validation.each do |hook| hook.call(data, property, property_schema, schema) end end end if dependencies dependencies.each do |key, value| next unless data.key?(key) subschema = value.is_a?(Array) ? { 'required' => value } : value subinstance = instance.merge(schema: subschema, schema_pointer: "#{instance.schema_pointer}/dependencies/#{key}") validate_instance(subinstance, &block) end end yield error(instance, 'maxProperties') if max_properties && data.size > max_properties yield error(instance, 'minProperties') if min_properties && data.size < min_properties if required missing_keys = required - data.keys yield error(instance, 'required', 'missing_keys' => missing_keys) if missing_keys.any? end regex_pattern_properties = nil data.each do |key, value| unless property_names.nil? subinstance = instance.merge( data: key, schema: property_names, schema_pointer: "#{instance.schema_pointer}/propertyNames" ) validate_instance(subinstance, &block) end matched_key = false if properties && properties.key?(key) subinstance = instance.merge( data: value, data_pointer: "#{instance.data_pointer}/#{key}", schema: properties[key], schema_pointer: "#{instance.schema_pointer}/properties/#{key}" ) validate_instance(subinstance, &block) matched_key = true end if pattern_properties regex_pattern_properties ||= pattern_properties.map do |pattern, property_schema| [pattern, ecma_262_regex(pattern), property_schema] end regex_pattern_properties.each do |pattern, regex, property_schema| if regex.match?(key) subinstance = instance.merge( data: value, data_pointer: "#{instance.data_pointer}/#{key}", schema: property_schema, schema_pointer: "#{instance.schema_pointer}/patternProperties/#{pattern}" ) validate_instance(subinstance, &block) matched_key = true end end end next if matched_key unless additional_properties.nil? subinstance = instance.merge( data: value, data_pointer: "#{instance.data_pointer}/#{key}", schema: additional_properties, schema_pointer: "#{instance.schema_pointer}/additionalProperties" ) validate_instance(subinstance, &block) end end if instance.after_property_validation && properties properties.each do |property, property_schema| instance.after_property_validation.each do |hook| hook.call(data, property, property_schema, schema) end end end end def safe_strict_decode64(data) Base64.strict_decode64(data) rescue ArgumentError => e raise e unless e.message == 'invalid base64' nil end def ecma_262_regex(pattern) @ecma_262_regex ||= {} @ecma_262_regex[pattern] ||= Regexp.new( Regexp::Scanner.scan(pattern).map do |type, token, text| type == :anchor ? RUBY_REGEX_ANCHORS_TO_ECMA_262.fetch(token, text) : text end.join ) end def join_uri(a, b) b = URI.parse(b) if b if a && b && a.relative? && b.relative? b elsif a && b URI.join(a, b) elsif b b else a end end def pointer_uri(schema, pointer) uri_parts = nil pointer.reduce(schema) do |obj, token| next obj.fetch(token.to_i) if obj.is_a?(Array) if obj_id = obj[id_keyword] uri_parts ||= [] uri_parts << obj_id end obj.fetch(token) end uri_parts ? URI.join(*uri_parts) : nil end def resolve_ids(schema, ids = {}, parent_uri = nil, pointer = '') if schema.is_a?(Array) schema.each_with_index { |subschema, index| resolve_ids(subschema, ids, parent_uri, "#{pointer}/#{index}") } elsif schema.is_a?(Hash) uri = join_uri(parent_uri, schema[id_keyword]) schema.each do |key, value| if key == id_keyword && uri != parent_uri ids[uri.to_s] = { schema: schema, pointer: pointer } end resolve_ids(value, ids, uri, "#{pointer}/#{key}") end end ids end def resolve_ref(uri) ref_resolver.call(uri) || raise(InvalidRefResolution, uri.to_s) end end end end json_schemer-0.2.18/lib/json_schemer/schema/draft6.rb0000644000004100000410000000070214030134656022512 0ustar www-datawww-data# frozen_string_literal: true module JSONSchemer module Schema class Draft6 < Base SUPPORTED_FORMATS = Set[ 'date-time', 'email', 'hostname', 'ipv4', 'ipv6', 'uri', 'uri-reference', 'uri-template', 'json-pointer', 'regex' ].freeze private def supported_format?(format) SUPPORTED_FORMATS.include?(format) end end end end json_schemer-0.2.18/lib/json_schemer/schema/draft4.rb0000644000004100000410000000201314030134656022505 0ustar www-datawww-data# frozen_string_literal: true module JSONSchemer module Schema class Draft4 < Base ID_KEYWORD = 'id' SUPPORTED_FORMATS = Set[ 'date-time', 'email', 'hostname', 'ipv4', 'ipv6', 'uri', 'regex' ].freeze private def id_keyword ID_KEYWORD end def supported_format?(format) SUPPORTED_FORMATS.include?(format) end def validate_exclusive_maximum(instance, exclusive_maximum, maximum) yield error(instance, 'exclusiveMaximum') if exclusive_maximum && instance.data >= maximum end def validate_exclusive_minimum(instance, exclusive_minimum, minimum) yield error(instance, 'exclusiveMinimum') if exclusive_minimum && instance.data <= minimum end def validate_integer(instance, &block) if !instance.data.is_a?(Integer) yield error(instance, 'integer') return end validate_numeric(instance, &block) end end end end json_schemer-0.2.18/lib/json_schemer/schema/draft7.rb0000644000004100000410000000113014030134656022507 0ustar www-datawww-data# frozen_string_literal: true module JSONSchemer module Schema class Draft7 < Base SUPPORTED_FORMATS = Set[ 'date-time', 'date', 'time', 'email', 'idn-email', 'hostname', 'idn-hostname', 'ipv4', 'ipv6', 'uri', 'uri-reference', 'iri', 'iri-reference', 'uri-template', 'json-pointer', 'relative-json-pointer', 'regex' ].freeze private def supported_format?(format) SUPPORTED_FORMATS.include?(format) end end end end json_schemer-0.2.18/lib/json_schemer/errors.rb0000644000004100000410000000214614030134656021404 0ustar www-datawww-data# Based on code from @robacarp found in issue 48: # https://github.com/davishmcclurg/json_schemer/issues/48 # module JSONSchemer module Errors class << self def pretty(error) data_pointer, type, schema = error.values_at('data_pointer', 'type', 'schema') location = data_pointer.empty? ? 'root' : "property '#{data_pointer}'" case type when 'required' keys = error.fetch('details').fetch('missing_keys').join(', ') "#{location} is missing required keys: #{keys}" when 'null', 'string', 'boolean', 'integer', 'number', 'array', 'object' "#{location} is not of type: #{type}" when 'pattern' "#{location} does not match pattern: #{schema.fetch('pattern')}" when 'format' "#{location} does not match format: #{schema.fetch('format')}" when 'const' "#{location} is not: #{schema.fetch('const').inspect}" when 'enum' "#{location} is not one of: #{schema.fetch('enum')}" else "#{location} is invalid: error_type=#{type}" end end end end end json_schemer-0.2.18/lib/json_schemer/format.rb0000644000004100000410000000674714030134656021373 0ustar www-datawww-data# frozen_string_literal: true module JSONSchemer module Format # this is no good EMAIL_REGEX = /\A[^@\s]+@([\p{L}\d-]+\.)+[\p{L}\d\-]{2,}\z/i.freeze LABEL_REGEX_STRING = '[\p{L}\p{N}]([\p{L}\p{N}\-]*[\p{L}\p{N}])?' HOSTNAME_REGEX = /\A(#{LABEL_REGEX_STRING}\.)*#{LABEL_REGEX_STRING}\z/i.freeze JSON_POINTER_REGEX_STRING = '(\/([^~\/]|~[01])*)*' JSON_POINTER_REGEX = /\A#{JSON_POINTER_REGEX_STRING}\z/.freeze RELATIVE_JSON_POINTER_REGEX = /\A(0|[1-9]\d*)(#|#{JSON_POINTER_REGEX_STRING})?\z/.freeze DATE_TIME_OFFSET_REGEX = /(Z|[\+\-]([01][0-9]|2[0-3]):[0-5][0-9])\z/i.freeze INVALID_QUERY_REGEX = /[[:space:]]/.freeze def valid_spec_format?(data, format) case format when 'date-time' valid_date_time?(data) when 'date' valid_date_time?("#{data}T04:05:06.123456789+07:00") when 'time' valid_date_time?("2001-02-03T#{data}") when 'email' data.ascii_only? && valid_email?(data) when 'idn-email' valid_email?(data) when 'hostname' data.ascii_only? && valid_hostname?(data) when 'idn-hostname' valid_hostname?(data) when 'ipv4' valid_ip?(data, :v4) when 'ipv6' valid_ip?(data, :v6) when 'uri' valid_uri?(data) when 'uri-reference' valid_uri_reference?(data) when 'iri' valid_uri?(iri_escape(data)) when 'iri-reference' valid_uri_reference?(iri_escape(data)) when 'uri-template' valid_uri_template?(data) when 'json-pointer' valid_json_pointer?(data) when 'relative-json-pointer' valid_relative_json_pointer?(data) when 'regex' EcmaReValidator.valid?(data) end end def valid_json?(data) JSON.parse(data) true rescue JSON::ParserError false end def valid_date_time?(data) DateTime.rfc3339(data) DATE_TIME_OFFSET_REGEX.match?(data) rescue ArgumentError => e false end def valid_email?(data) EMAIL_REGEX.match?(data) end def valid_hostname?(data) HOSTNAME_REGEX.match?(data) && data.split('.').all? { |label| label.size <= 63 } end def valid_ip?(data, type) ip_address = IPAddr.new(data) type == :v4 ? ip_address.ipv4? : ip_address.ipv6? rescue IPAddr::InvalidAddressError false end def parse_uri_scheme(data) scheme, _userinfo, _host, _port, _registry, _path, opaque, query, _fragment = URI::RFC3986_PARSER.split(data) # URI::RFC3986_PARSER.parse allows spaces in these and I don't think it should raise URI::InvalidURIError if INVALID_QUERY_REGEX.match?(query) || INVALID_QUERY_REGEX.match?(opaque) scheme end def valid_uri?(data) !!parse_uri_scheme(data) rescue URI::InvalidURIError false end def valid_uri_reference?(data) parse_uri_scheme(data) true rescue URI::InvalidURIError false end def iri_escape(data) data.gsub(/[^[:ascii:]]/) do |match| us = match tmp = +'' us.each_byte do |uc| tmp << sprintf('%%%02X', uc) end tmp end.force_encoding(Encoding::US_ASCII) end def valid_uri_template?(data) URITemplate.new(data) true rescue URITemplate::Invalid false end def valid_json_pointer?(data) JSON_POINTER_REGEX.match?(data) end def valid_relative_json_pointer?(data) RELATIVE_JSON_POINTER_REGEX.match?(data) end end end json_schemer-0.2.18/lib/json_schemer/cached_ref_resolver.rb0000644000004100000410000000044514030134656024054 0ustar www-datawww-data# frozen_string_literal: true module JSONSchemer class CachedRefResolver def initialize(&ref_resolver) @ref_resolver = ref_resolver @cache = {} end def call(uri) @cache[uri] = @ref_resolver.call(uri) unless @cache.key?(uri) @cache[uri] end end end json_schemer-0.2.18/Gemfile0000644000004100000410000000024714030134656015611 0ustar www-datawww-datasource "https://rubygems.org" git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } # Specify your gem's dependencies in json_schemer.gemspec gemspec json_schemer-0.2.18/.github/0000755000004100000410000000000014030134656015653 5ustar www-datawww-datajson_schemer-0.2.18/.github/workflows/0000755000004100000410000000000014030134656017710 5ustar www-datawww-datajson_schemer-0.2.18/.github/workflows/ci.yml0000644000004100000410000000133214030134656021025 0ustar www-datawww-dataname: ci on: [push, pull_request] jobs: test: strategy: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] ruby: [2.4, 2.5, 2.6, 2.7, 3.0, head, jruby, jruby-head, truffleruby, truffleruby-head] exclude: - os: windows-latest ruby: jruby - os: windows-latest ruby: jruby-head - os: windows-latest ruby: truffleruby - os: windows-latest ruby: truffleruby-head runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v2 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} bundler-cache: true - run: bundle exec rake test json_schemer-0.2.18/LICENSE.txt0000644000004100000410000000206714030134656016143 0ustar www-datawww-dataThe MIT License (MIT) Copyright (c) 2018 David Harsha Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.