jimmy 0.5.3 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +98 -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 +107 -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,134 @@
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
+ yield self if block_given?
122
+ self
123
+ end
113
124
  end
114
125
  end
126
+
127
+ require 'jimmy/schema/array'
128
+ require 'jimmy/schema/number'
129
+ require 'jimmy/schema/object'
130
+ require 'jimmy/schema/string'
131
+
132
+ require 'jimmy/schema/operators'
133
+ require 'jimmy/schema/json'
134
+ 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