jimmy 0.5.5 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. checksums.yaml +5 -5
  2. data/.circleci/config.yml +35 -0
  3. data/.gitignore +4 -3
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +61 -0
  6. data/.ruby-version +1 -1
  7. data/.travis.yml +6 -0
  8. data/CODE_OF_CONDUCT.md +74 -0
  9. data/Gemfile +13 -0
  10. data/LICENSE +1 -1
  11. data/README.md +22 -134
  12. data/Rakefile +91 -1
  13. data/bin/console +15 -0
  14. data/bin/setup +8 -0
  15. data/jimmy.gemspec +25 -21
  16. data/lib/jimmy.rb +23 -15
  17. data/lib/jimmy/declaration.rb +150 -0
  18. data/lib/jimmy/declaration/assertion.rb +81 -0
  19. data/lib/jimmy/declaration/casting.rb +34 -0
  20. data/lib/jimmy/declaration/composites.rb +41 -0
  21. data/lib/jimmy/declaration/number.rb +27 -0
  22. data/lib/jimmy/declaration/object.rb +11 -0
  23. data/lib/jimmy/declaration/string.rb +100 -0
  24. data/lib/jimmy/declaration/types.rb +57 -0
  25. data/lib/jimmy/error.rb +9 -0
  26. data/lib/jimmy/file_map.rb +166 -0
  27. data/lib/jimmy/index.rb +78 -0
  28. data/lib/jimmy/json/array.rb +93 -0
  29. data/lib/jimmy/json/collection.rb +90 -0
  30. data/lib/jimmy/json/hash.rb +118 -0
  31. data/lib/jimmy/json/pointer.rb +119 -0
  32. data/lib/jimmy/json/uri.rb +144 -0
  33. data/lib/jimmy/loaders/base.rb +30 -0
  34. data/lib/jimmy/loaders/json.rb +15 -0
  35. data/lib/jimmy/loaders/ruby.rb +21 -0
  36. data/lib/jimmy/macros.rb +37 -0
  37. data/lib/jimmy/schema.rb +106 -87
  38. data/lib/jimmy/schema/array.rb +95 -0
  39. data/lib/jimmy/schema/casting.rb +17 -0
  40. data/lib/jimmy/schema/json.rb +40 -0
  41. data/lib/jimmy/schema/number.rb +47 -0
  42. data/lib/jimmy/schema/object.rb +108 -0
  43. data/lib/jimmy/schema/operators.rb +96 -0
  44. data/lib/jimmy/schema/string.rb +44 -0
  45. data/lib/jimmy/schema_with_uri.rb +53 -0
  46. data/lib/jimmy/schemer_factory.rb +65 -0
  47. data/lib/jimmy/version.rb +3 -1
  48. data/schema07.json +172 -0
  49. metadata +50 -101
  50. data/circle.yml +0 -11
  51. data/lib/jimmy/combination.rb +0 -34
  52. data/lib/jimmy/definitions.rb +0 -38
  53. data/lib/jimmy/domain.rb +0 -111
  54. data/lib/jimmy/link.rb +0 -93
  55. data/lib/jimmy/reference.rb +0 -39
  56. data/lib/jimmy/schema_creation.rb +0 -121
  57. data/lib/jimmy/schema_type.rb +0 -100
  58. data/lib/jimmy/schema_types.rb +0 -42
  59. data/lib/jimmy/schema_types/array.rb +0 -30
  60. data/lib/jimmy/schema_types/boolean.rb +0 -6
  61. data/lib/jimmy/schema_types/integer.rb +0 -8
  62. data/lib/jimmy/schema_types/null.rb +0 -6
  63. data/lib/jimmy/schema_types/number.rb +0 -34
  64. data/lib/jimmy/schema_types/object.rb +0 -45
  65. data/lib/jimmy/schema_types/string.rb +0 -40
  66. data/lib/jimmy/symbol_array.rb +0 -17
  67. data/lib/jimmy/transform_keys.rb +0 -39
  68. data/lib/jimmy/type_reference.rb +0 -14
  69. data/lib/jimmy/validation_error.rb +0 -20
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jimmy
4
+ module Loaders
5
+ # Base class for all file loaders
6
+ # @abstract
7
+ class Base
8
+ # Load the given file. Intended to be used by a {Jimmy::FileMap}.
9
+ # @api private
10
+ # @param [Pathname, String] file Path of the file to load
11
+ def self.call(file)
12
+ new(file).load
13
+ end
14
+
15
+ # The source file to be loaded.
16
+ # @return Pathname
17
+ attr_reader :source
18
+
19
+ # @param [Pathname] source The source file to load.
20
+ def initialize(source)
21
+ @source = Pathname(source)
22
+ end
23
+
24
+ # @return [Jimmy::Schema]
25
+ def load
26
+ raise NotImplementedError, "Please implement #load on #{self.class}"
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jimmy/loaders/base'
4
+
5
+ module Jimmy
6
+ module Loaders
7
+ # Loads a plain .json file
8
+ class JSON < Base
9
+ # @return [Jimmy::Schema]
10
+ def load
11
+ Schema.new ::JSON.parse(source.read)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jimmy/macros'
4
+ require 'jimmy/loaders/base'
5
+
6
+ module Jimmy
7
+ module Loaders
8
+ # Loads .rb files
9
+ class Ruby < Base
10
+ include Macros
11
+
12
+ # @param [Pathname, string] file
13
+ # @return [Jimmy::Schema]
14
+ def load(file = source)
15
+ file = Pathname(file)
16
+ file = source.parent + file if file.relative?
17
+ Jimmy::Schema(instance_eval file.read, file.to_s)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jimmy/declaration'
4
+
5
+ module Jimmy
6
+ # The +Macros+ module includes methods that can be called directly on the
7
+ # +Jimmy+ module for quickly making common types of schemas.
8
+ module Macros
9
+ include Declaration
10
+
11
+ # Make a new schema. Shortcut for +Schema.new+.
12
+ # @yieldparam schema [Schema] The new schema
13
+ # @return [Schema] The new schema.
14
+ def schema(&block)
15
+ Schema.new &block
16
+ end
17
+
18
+ # Make a schema that never validates.
19
+ # @return [Schema] The new schema.
20
+ def nothing
21
+ schema.nothing
22
+ end
23
+
24
+ # Make a schema that references another schema by URI.
25
+ # @param [String, URI, Json::URI] uri
26
+ # @return [Schema] The new schema.
27
+ def ref(uri)
28
+ schema.ref uri
29
+ end
30
+
31
+ private
32
+
33
+ def get(*args, &block)
34
+ {}.fetch(*args, &block)
35
+ end
36
+ end
37
+ end
@@ -1,114 +1,133 @@
1
- module Jimmy
2
- class Schema
3
- JSON_SCHEMA_URI = 'http://json-schema.org/draft-04/schema#'
4
- JSON_HYPER_SCHEMA_URI = 'http://json-schema.org/draft-04/hyper-schema#'
5
-
6
- attr_reader :dsl, :attrs, :domain, :type, :parent
7
- attr_writer :name
8
- attr_accessor :nullable
9
-
10
- @argument_handlers = Hash.new { |hash, key| hash[key] = {} }
11
-
12
- def self.set_argument_handler(schema_class, arg_class, handler)
13
- @argument_handlers[schema_class][arg_class] = handler
14
- end
1
+ # frozen_string_literal: true
15
2
 
16
- def self.argument_hander(schema_class, argument)
17
- handlers = {}
18
- until schema_class == SchemaType do
19
- handlers = (@argument_handlers[schema_class] || {}).merge(handlers)
20
- schema_class = schema_class.superclass
21
- end
22
- result = handlers.find { |k, _| argument.is_a? k }
23
- result && result.last
24
- end
3
+ require 'json'
4
+
5
+ require 'jimmy/json/hash'
6
+ require 'jimmy/declaration'
25
7
 
26
- def compile
27
- compiler = nil
28
- schema_class = SchemaTypes[type]
29
- until schema_class == SchemaType do
30
- compiler ||= SchemaTypes.compilers[schema_class]
31
- schema_class = schema_class.superclass
8
+ module Jimmy
9
+ # Represents a schema as defined by http://json-schema.org/draft-07/schema
10
+ class Schema < Json::Hash
11
+ include Declaration
12
+
13
+ PROPERTIES = %w[
14
+ title description default readOnly writeOnly examples
15
+ multipleOf maximum exclusiveMaximum minimum exclusiveMinimum
16
+ maxLength minLength pattern
17
+ additionalItems items maxItems minItems uniqueItems contains
18
+ maxProperties minProperties required additionalProperties
19
+ definitions properties patternProperties dependencies propertyNames
20
+ const enum type format
21
+ contentMediaType contentEncoding
22
+ if then else
23
+ allOf anyOf oneOf
24
+ not
25
+ ].freeze
26
+
27
+ # @yieldparam schema [self] The new schema
28
+ def initialize(schema = {})
29
+ @nothing = false
30
+ case schema
31
+ when *CASTABLE_CLASSES
32
+ super({})
33
+ apply_cast self, schema
34
+ when Hash then super
35
+ else raise Error::WrongType, "Unexpected #{schema.class}"
32
36
  end
33
- hash = {}
34
- hash['type'] = nullable ? ['null', type.to_s] : type.to_s
35
- hash['definitions'] = definitions.compile unless definitions.empty?
36
- hash['links'] = links.map &:compile unless links.empty?
37
- hash.merge! data
38
- dsl.evaluate compiler, hash if compiler
39
- hash['enum'] |= [nil] if nullable && hash.key?('enum')
40
- hash
37
+ yield self if block_given?
41
38
  end
42
39
 
43
- def name
44
- @name || (parent && parent.name)
40
+ # Returns true when the schema will never validate against anything.
41
+ # @return [true, false]
42
+ def nothing?
43
+ @nothing
45
44
  end
46
45
 
47
- def uri
48
- domain.uri_for name
46
+ # Returns true when the schema will validate against anything.
47
+ # @return [true, false]
48
+ def anything?
49
+ !@nothing && empty?
49
50
  end
50
51
 
51
- def definitions
52
- @definitions ||= Definitions.new(self)
52
+ # Set a property of the schema.
53
+ # @param [String, Symbol] key Symbols are converted to camel-case strings.
54
+ # @param [Object] value
55
+ def []=(key, value)
56
+ @nothing = false
57
+
58
+ case key
59
+ when '$id' then @id = value # TODO: something, with this
60
+ when '$ref' then ref value
61
+ when '$schema'
62
+ URI(value) == URI(SCHEMA) or
63
+ raise Error::BadArgument, 'Unsupported JSON schema draft'
64
+ when '$comment' then @comment = value # TODO: something, with this
65
+ else super
66
+ end
53
67
  end
54
68
 
55
- def links
56
- @links ||= []
69
+ # @see ::Object#inspect
70
+ def inspect
71
+ "#<#{self.class} #{super}>"
57
72
  end
58
73
 
59
- def data
60
- @data ||= {}
74
+ # Turns the schema into a reference to another schema. Freezes the schema
75
+ # so that no further changes can be made.
76
+ # @param [Json::URI, URI, String] uri The URI of the JSON schema to
77
+ # reference.
78
+ # @return [self]
79
+ def ref(uri)
80
+ assert empty? do
81
+ 'Reference schemas cannot have other properties: ' +
82
+ keys.join(', ')
83
+ end
84
+ @members['$ref'] = Json::URI.new(uri)
85
+ freeze
61
86
  end
62
87
 
63
- def hyper?
64
- links.any?
88
+ # Make the schema validate nothing (i.e. everything is invalid).
89
+ # @return [self] self
90
+ def nothing
91
+ clear
92
+ @nothing = true
93
+ self
65
94
  end
66
95
 
67
- def schema_uri
68
- hyper? ? JSON_HYPER_SCHEMA_URI : JSON_SCHEMA_URI
96
+ # Get the URI of the schema to which this schema refers, or nil if the
97
+ # schema is not a reference.
98
+ # @return [Json::URI, nil]
99
+ def target
100
+ self['$ref']
69
101
  end
70
102
 
71
- def to_h
72
- {'$schema' => schema_uri}.tap do |h|
73
- h['id'] = uri.to_s if name
74
- h.merge! compile
75
- end
103
+ # Returns true if the schema refers to another schema.
104
+ # @return [true, false]
105
+ def ref?
106
+ key? '$ref'
76
107
  end
77
108
 
78
- def validate(data)
79
- errors = JSON::Validator.fully_validate(JSON::Validator.schema_for_uri(uri).schema, data, errors_as_objects: true)
80
- raise ValidationError.new(self, data, errors) unless errors.empty?
81
- end
109
+ alias get fetch
82
110
 
83
- def initialize(type, parent)
84
- @attrs = {}
85
- @type = type
86
- @domain = parent.domain
87
- @dsl = SchemaTypes.dsls[type].new(self)
88
- @parent = parent if parent.is_a? self.class
89
- end
111
+ PROPERTY_SEQUENCE = PROPERTIES.each.with_index.to_h.freeze
90
112
 
91
- def setup(*args, **locals, &block)
92
- args.each do |arg|
93
- case arg
94
- when Symbol
95
- dsl.__send__ arg
96
- when Hash
97
- arg.each { |k, v| dsl.__send__ k, v }
98
- else
99
- handler = Schema.argument_hander(SchemaTypes[type], arg)
100
- raise "`#{type}` cannot handle arguments of type #{arg.class.name}" unless handler
101
- dsl.evaluate handler, arg
102
- end
103
- end
104
- if block
105
- if dsl.respond_to? :with_locals
106
- dsl.with_locals(locals) { dsl.evaluate block }
107
- else
108
- dsl.evaluate block
109
- end
110
- end
113
+ # @api private
114
+ def sort_keys_by(key, _value) # :nodoc:
115
+ PROPERTY_SEQUENCE.fetch(key) { raise KeyError, 'Not a valid schema key' }
111
116
  end
112
117
 
118
+ protected
119
+
120
+ def schema
121
+ self
122
+ end
113
123
  end
114
124
  end
125
+
126
+ require 'jimmy/schema/array'
127
+ require 'jimmy/schema/number'
128
+ require 'jimmy/schema/object'
129
+ require 'jimmy/schema/string'
130
+
131
+ require 'jimmy/schema/operators'
132
+ require 'jimmy/schema/json'
133
+ require 'jimmy/schema/casting'
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jimmy
4
+ class Schema
5
+ # Set whether the array value is required to have unique items.
6
+ # @param [true, false] unique Whether the array value should have unique
7
+ # items.
8
+ # @return [self] self, for chaining
9
+ def unique_items(unique = true)
10
+ valid_for 'array'
11
+ assert_boolean unique
12
+ set uniqueItems: unique
13
+ end
14
+
15
+ alias unique unique_items
16
+
17
+ # Set the maximum items for an array value.
18
+ # @param [Numeric] count The maximum items for an array value.
19
+ # @return [self] self, for chaining
20
+ def max_items(count)
21
+ valid_for 'array'
22
+ assert_numeric count, minimum: 0
23
+ set maxItems: count
24
+ end
25
+
26
+ # Set the minimum items for an array value.
27
+ # @param [Numeric] count The minimum items for an array value.
28
+ # @return [self] self, for chaining
29
+ def min_items(count)
30
+ valid_for 'array'
31
+ assert_numeric count, minimum: 0
32
+ set minItems: count
33
+ end
34
+
35
+ # Set the minimum and maximum items for an array value, using a range.
36
+ # @param [Range, Integer] range The minimum and maximum items for an array
37
+ # value. If an integer is given, it is taken to be both.
38
+ # @return [self] self, for chaining
39
+ def count(range)
40
+ range = range..range if range.is_a?(Integer)
41
+ assert_range range
42
+ min_items range.min
43
+ max_items range.max unless range.end.nil?
44
+ self
45
+ end
46
+
47
+ # Set the schema or schemas for validating items in an array value.
48
+ # @param [Jimmy::Schema, Array<Jimmy::Schema>] schema_or_schemas A schema
49
+ # or array of schemas for validating items in an array value. If an
50
+ # array of schemas is given, the first schema will apply to the first
51
+ # item, and so on.
52
+ # @param [Jimmy::Schema, nil] rest_schema The schema to apply to items with
53
+ # indexes greater than the length of the first argument. Only applicable
54
+ # when an array is given for the first argument.
55
+ # @return [self] self, for chaining
56
+ def items(schema_or_schemas, rest_schema = nil)
57
+ if schema_or_schemas.is_a? Array
58
+ item *schema_or_schemas
59
+ set additionalItems: cast_schema(rest_schema) if rest_schema
60
+ else
61
+ match_all_items schema_or_schemas, rest_schema
62
+ end
63
+ self
64
+ end
65
+
66
+ # Add a single-item schema, or several, to the +items+ array. Only valid
67
+ # if a match-all schema has not been set.
68
+ # @param [Array<Jimmy::Schema>] single_item_schemas One or more schemas
69
+ # to add to the existing +items+ array.
70
+ # @return [self] self, for chaining
71
+ def item(*single_item_schemas)
72
+ valid_for 'array'
73
+ assert_array(single_item_schemas, minimum: 1)
74
+ existing = getset('items') { [] }
75
+ assert !existing.is_a?(Schema) do
76
+ 'Cannot add individual item schemas after adding a match-all schema'
77
+ end
78
+ single_item_schemas.each do |schema|
79
+ existing << cast_schema(schema)
80
+ end
81
+ self
82
+ end
83
+
84
+ private
85
+
86
+ def match_all_items(schema, rest_schema)
87
+ valid_for 'array'
88
+ assert(rest_schema.nil?) do
89
+ 'You cannot specify an additional items schema when using a '\
90
+ 'match-all schema'
91
+ end
92
+ set items: cast_schema(schema)
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jimmy
4
+ class Schema
5
+ private
6
+
7
+ def cast_key(key)
8
+ case key
9
+ when Regexp
10
+ assert_regexp key
11
+ super key.source
12
+ else
13
+ super
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jimmy
4
+ class Schema
5
+ # The JSON Schema draft 7 schema URI
6
+ SCHEMA = 'http://json-schema.org/draft-07/schema#'
7
+
8
+ # Get the schema as a plain Hash. Given an +id+, the +$id+ and +$schema+
9
+ # keys will also be set.
10
+ # @param [Json::URI, URI, String] id
11
+ # @return [Hash, true, false]
12
+ def as_json(id: '', index: nil)
13
+ id = Json::URI.new(id)
14
+
15
+ if index.nil? && id.absolute?
16
+ return top_level_json(id) { super index: {}, id: id }
17
+ end
18
+
19
+ return true if anything?
20
+ return false if nothing?
21
+
22
+ super index: index || {}, id: id
23
+ end
24
+
25
+ private
26
+
27
+ def top_level_json(id)
28
+ hash = {
29
+ '$id' => id.to_s,
30
+ '$schema' => SCHEMA
31
+ }
32
+ if nothing?
33
+ hash['not'] = true
34
+ else
35
+ hash.merge! yield
36
+ end
37
+ hash
38
+ end
39
+ end
40
+ end