jimmy 0.5.1 → 2.0.0

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 +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 -86
  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 -10
  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,113 +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
37
+ yield self if block_given?
40
38
  end
41
39
 
42
- def name
43
- @name || (parent && parent.name)
40
+ # Returns true when the schema will never validate against anything.
41
+ # @return [true, false]
42
+ def nothing?
43
+ @nothing
44
44
  end
45
45
 
46
- def uri
47
- domain.uri_for name
46
+ # Returns true when the schema will validate against anything.
47
+ # @return [true, false]
48
+ def anything?
49
+ !@nothing && empty?
48
50
  end
49
51
 
50
- def definitions
51
- @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
52
67
  end
53
68
 
54
- def links
55
- @links ||= []
69
+ # @see ::Object#inspect
70
+ def inspect
71
+ "#<#{self.class} #{super}>"
56
72
  end
57
73
 
58
- def data
59
- @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
60
86
  end
61
87
 
62
- def hyper?
63
- 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
64
94
  end
65
95
 
66
- def schema_uri
67
- 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']
68
101
  end
69
102
 
70
- def to_h
71
- {'$schema' => schema_uri}.tap do |h|
72
- h['id'] = uri.to_s if name
73
- h.merge! compile
74
- end
103
+ # Returns true if the schema refers to another schema.
104
+ # @return [true, false]
105
+ def ref?
106
+ key? '$ref'
75
107
  end
76
108
 
77
- def validate(data)
78
- errors = JSON::Validator.fully_validate(JSON::Validator.schema_for_uri(uri).schema, data, errors_as_objects: true)
79
- raise ValidationError.new(self, data, errors) unless errors.empty?
80
- end
109
+ alias get fetch
81
110
 
82
- def initialize(type, parent)
83
- @attrs = {}
84
- @type = type
85
- @domain = parent.domain
86
- @dsl = SchemaTypes.dsls[type].new(self)
87
- @parent = parent if parent.is_a? self.class
88
- end
111
+ PROPERTY_SEQUENCE = PROPERTIES.each.with_index.to_h.freeze
89
112
 
90
- def setup(*args, **locals, &block)
91
- args.each do |arg|
92
- case arg
93
- when Symbol
94
- dsl.__send__ arg
95
- when Hash
96
- arg.each { |k, v| dsl.__send__ k, v }
97
- else
98
- handler = Schema.argument_hander(SchemaTypes[type], arg)
99
- raise "`#{type}` cannot handle arguments of type #{arg.class.name}" unless handler
100
- dsl.evaluate handler, arg
101
- end
102
- end
103
- if block
104
- if dsl.respond_to? :with_locals
105
- dsl.with_locals(locals) { dsl.evaluate block }
106
- else
107
- dsl.evaluate block
108
- end
109
- end
113
+ # @api private
114
+ def sort_keys_by(key, _value) # :nodoc:
115
+ PROPERTY_SEQUENCE.fetch(key) { raise KeyError, 'Not a valid schema key' }
110
116
  end
111
117
 
118
+ protected
119
+
120
+ def schema
121
+ self
122
+ end
112
123
  end
113
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