require 'uri' require 'open-uri' require 'pathname' require 'bigdecimal' require 'digest/sha1' require 'date' module JSON class Schema class ValidationError < Exception attr_reader :fragments, :schema def initialize(message, fragments, schema) @fragments = fragments @schema = schema message = "#{message} in schema #{schema.uri}" super(message) end end class SchemaError < Exception end class JsonParseError < Exception end class Attribute def self.validate(current_schema, data, fragments, validator, options = {}) end def self.build_fragment(fragments) "#/#{fragments.join('/')}" end def self.validation_error(message, fragments, current_schema, record_errors) error = ValidationError.new(message, fragments, current_schema) if record_errors ::JSON::Validator.validation_error(error.message) else raise error end end end class Validator attr_accessor :attributes, :uri def initialize() @attributes = {} @uri = nil end def extend_schema_definition(schema_uri) u = URI.parse(schema_uri) validator = JSON::Validator.validators["#{u.scheme}://#{u.host}#{u.path}"] if validator.nil? raise SchemaError.new("Schema not found: #{u.scheme}://#{u.host}#{u.path}") end @attributes.merge!(validator.attributes) end def to_s "#{@uri.scheme}://#{uri.host}#{uri.path}" end def validate(current_schema, data, fragments, options = {}) current_schema.schema.each do |attr_name,attribute| if @attributes.has_key?(attr_name.to_s) @attributes[attr_name.to_s].validate(current_schema, data, fragments, self, options) end end data end end end class Validator @@schemas = {} @@cache_schemas = false @@default_opts = { :list => false, :version => nil, :validate_schema => false, :record_errors => false } @@validators = {} @@default_validator = nil @@available_json_backends = [] @@json_backend = nil @@errors = [] def initialize(schema_data, data, opts={}) @options = @@default_opts.clone.merge(opts) # I'm not a fan of this, but it's quick and dirty to get it working for now version_string = "draft-03" if @options[:version] @options[:version] = case @options[:version].to_s when "draft3" "draft-03" when "draft2" "draft-02" when "draft1" "draft-01" else raise JSON::Schema::SchemaError.new("The requested JSON schema version is not supported") end version_string = @options[:version] u = URI.parse("http://json-schema.org/#{@options[:version]}/schema#") validator = JSON::Validator.validators["#{u.scheme}://#{u.host}#{u.path}"] @options[:version] = validator end @validation_options = @options[:record_errors] ? {:record_errors => true} : {} # validate the schema, if requested if @options[:validate_schema] begin metaschema_file = File.join(Pathname.new(File.dirname(__FILE__)).parent.parent, "resources", "#{version_string}.json").to_s meta_validator = JSON::Validator.new(metaschema_file, schema_data) meta_validator.validate rescue JSON::Schema::ValidationError, JSON::Schema::SchemaError raise $! end end @base_schema = initialize_schema(schema_data) @data = initialize_data(data) build_schemas(@base_schema) end # Run a simple true/false validation of data against a schema def validate() begin Validator.clear_errors @base_schema.validate(@data,[],@validation_options) Validator.clear_cache @@errors rescue JSON::Schema::ValidationError Validator.clear_cache raise $! end end def load_ref_schema(parent_schema,ref) uri = URI.parse(ref) if uri.relative? uri = parent_schema.uri.clone # Check for absolute path path = ref.split("#")[0] # This is a self reference and thus the schema does not need to be re-loaded if path.nil? || path == '' return end if path && path[0,1] == '/' uri.path = Pathname.new(path).cleanpath.to_s else uri = parent_schema.uri.merge(path) end uri.fragment = '' end if Validator.schemas[uri.to_s].nil? begin schema = JSON::Schema.new(JSON::Validator.parse(open(uri.to_s).read), uri, @options[:version]) Validator.add_schema(schema) build_schemas(schema) rescue JSON::ParserError # Don't rescue this error, we want JSON formatting issues to bubble up raise $! rescue Exception # Failures will occur when this URI cannot be referenced yet. Don't worry about it, # the proper error will fall out if the ref isn't ever defined end end end # Build all schemas with IDs, mapping out the namespace def build_schemas(parent_schema) # Build ref schemas if they exist if parent_schema.schema["$ref"] load_ref_schema(parent_schema, parent_schema.schema["$ref"]) end # Check for schemas in union types ["type", "disallow"].each do |key| if parent_schema.schema[key] && parent_schema.schema[key].is_a?(Array) parent_schema.schema[key].each_with_index do |type,i| if type.is_a?(Hash) handle_schema(parent_schema, type) end end end end # All properties are schemas if parent_schema.schema["properties"] parent_schema.schema["properties"].each do |k,v| handle_schema(parent_schema, v) end end # Items are always schemas if parent_schema.schema["items"] items = parent_schema.schema["items"].clone single = false if !items.is_a?(Array) items = [items] single = true end items.each_with_index do |item,i| handle_schema(parent_schema, item) end end # Each of these might be schemas ["additionalProperties", "additionalItems", "dependencies", "extends"].each do |key| if parent_schema.schema[key].is_a?(Hash) handle_schema(parent_schema, parent_schema.schema[key]) end end end # Either load a reference schema or create a new schema def handle_schema(parent_schema, obj) schema_uri = parent_schema.uri.clone schema = JSON::Schema.new(obj,schema_uri,@options[:version]) if obj['id'] Validator.add_schema(schema) end build_schemas(schema) end class << self def validate(schema, data,opts={}) begin validator = JSON::Validator.new(schema, data, opts) validator.validate return true rescue JSON::Schema::ValidationError, JSON::Schema::SchemaError return false end end def validate!(schema, data,opts={}) validator = JSON::Validator.new(schema, data, opts) validator.validate return true end alias_method 'validate2', 'validate!' def fully_validate(schema, data, opts={}) opts[:record_errors] = true validator = JSON::Validator.new(schema, data, opts) validator.validate end def clear_cache @@schemas = {} if @@cache_schemas == false end def clear_errors @@errors = [] end def validation_error(error) @@errors.push(error) end def schemas @@schemas end def add_schema(schema) @@schemas[schema.uri.to_s] = schema if @@schemas[schema.uri.to_s].nil? end def cache_schemas=(val) @@cache_schemas = val == true ? true : false end def validators @@validators end def default_validator @@default_validator end def register_validator(v) @@validators[v.to_s] = v end def register_default_validator(v) @@default_validator = v end def json_backend @@json_backend end def json_backend=(backend) backend = backend.to_s if @@available_json_backends.include?(backend) @@json_backend = backend else raise JSON::Schema::JsonParseError.new("The JSON backend '#{backend}' could not be found.") end end def parse(s) case @@json_backend.to_s when 'json' JSON.parse(s) when 'yajl' json = StringIO.new(s) parser = Yajl::Parser.new parser.parse(json) else raise JSON::Schema::JsonParseError.new("No supported JSON parsers found. The following parsers are suported:\n * yajl-ruby\n * json") end end end if begin Gem::Specification::find_by_name('json') rescue Gem::LoadError false rescue Gem.available?('json') end require 'json' @@available_json_backends << 'json' @@json_backend = 'json' end if begin Gem::Specification::find_by_name('yajl-ruby') rescue Gem::LoadError false rescue Gem.available?('yajl-ruby') end require 'yajl' @@available_json_backends << 'yajl' @@json_backend = 'yajl' end private if begin Gem::Specification::find_by_name('uuidtools') rescue Gem::LoadError false rescue Gem.available?('uuidtools') end require 'uuidtools' @@fake_uri_generator = lambda{|s| UUIDTools::UUID.sha1_create(UUIDTools::UUID_URL_NAMESPACE, s).to_s } else require 'uri/uuid' @@fake_uri_generator = lambda{|s| JSON::Util::UUID.create_v5(s,JSON::Util::UUID::Nil).to_s } end if @@json_backend == 'yajl' @@serializer = lambda{|o| Yajl::Encoder.encode(o) } else @@serializer = lambda{|o| Marshal.dump(o) } end def serialize schema @@serializer.call(schema) end def fake_uri schema @@fake_uri_generator.call(schema) end def initialize_schema(schema) if schema.is_a?(String) begin # Build a fake URI for this schema_uri = URI.parse(fake_uri(schema)) schema = JSON::Validator.parse(schema) if @options[:list] schema = {"type" => "array", "items" => schema} end schema = JSON::Schema.new(schema,schema_uri,@options[:version]) Validator.add_schema(schema) rescue # Build a uri for it schema_uri = URI.parse(schema) if schema_uri.relative? # Check for absolute path if schema[0,1] == '/' schema_uri = URI.parse("file://#{schema}") else schema_uri = URI.parse("file://#{Dir.pwd}/#{schema}") end end if Validator.schemas[schema_uri.to_s].nil? schema = JSON::Validator.parse(open(schema_uri.to_s).read) if @options[:list] schema = {"type" => "array", "items" => schema} end schema = JSON::Schema.new(schema,schema_uri,@options[:version]) Validator.add_schema(schema) else schema = Validator.schemas[schema_uri.to_s] end end elsif schema.is_a?(Hash) if @options[:list] schema = {"type" => "array", "items" => schema} end schema_uri = URI.parse(fake_uri(serialize(schema))) schema = JSON::Schema.new(schema,schema_uri,@options[:version]) Validator.add_schema(schema) else raise "Invalid schema - must be either a string or a hash" end schema end def initialize_data(data) # Parse the data, if any if data.is_a?(String) begin data = JSON::Validator.parse(data) rescue json_uri = URI.parse(data) if json_uri.relative? if data[0,1] == '/' schema_uri = URI.parse("file://#{data}") else schema_uri = URI.parse("file://#{Dir.pwd}/#{data}") end end data = JSON::Validator.parse(open(json_uri.to_s).read) end end data end end end