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,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jimmy
4
+ class Schema
5
+ # Set the minimum value.
6
+ # @param [Numeric] number The minimum numeric value.
7
+ # @param [true, false] exclusive Whether the value is included in the
8
+ # minimum.
9
+ # @return [self] self, for chaining
10
+ def minimum(number, exclusive: false)
11
+ set_numeric_boundary 'minimum', number, exclusive
12
+ end
13
+
14
+ # Set the exclusive minimum value.
15
+ # @param [Numeric] number The exclusive minimum numeric value.
16
+ # @return [self] self, for chaining
17
+ def exclusive_minimum(number)
18
+ minimum number, exclusive: true
19
+ end
20
+
21
+ # Set the maximum value.
22
+ # @param [Numeric] number The maximum numeric value.
23
+ # @param [true, false] exclusive Whether the value is included in the
24
+ # maximum.
25
+ # @return [self] self, for chaining
26
+ def maximum(number, exclusive: false)
27
+ set_numeric_boundary 'maximum', number, exclusive
28
+ end
29
+
30
+ # Set the exclusive maximum value.
31
+ # @param [Numeric] number The exclusive maximum numeric value.
32
+ # @return [self] self, for chaining
33
+ def exclusive_maximum(number)
34
+ maximum number, exclusive: true
35
+ end
36
+
37
+ private
38
+
39
+ def set_numeric_boundary(name, number, exclusive)
40
+ valid_for 'number', 'integer'
41
+ assert_numeric number
42
+ assert_boolean exclusive
43
+ name = 'exclusive' + name[0].upcase + name[1..] if exclusive
44
+ set name => number
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jimmy
4
+ class Schema
5
+ # Define a property for an object value.
6
+ # @param [String, Symbol] name The name of the property.
7
+ # @param [Jimmy::Schema] schema The schema for the property. If
8
+ # omitted, a new Schema will be created, and will be yielded if a block
9
+ # is given.
10
+ # @param [true, false] required If true, +name+ will be added to the
11
+ # +required+ property.
12
+ # @yieldparam schema [Jimmy::Schema] The schema being assigned.
13
+ # @return [self] self, for chaining
14
+ def property(name, schema = Schema.new, required: false, &block)
15
+ return properties(name, required: required, &block) if name.is_a? Hash
16
+
17
+ valid_for 'object'
18
+ collection = collection_for_property_key(name)
19
+ assign_to_schema_hash collection, name, schema, &block
20
+ require name if required
21
+ self
22
+ end
23
+
24
+ # Define properties for an object value.
25
+ # @param [Hash{String, Symbol => Jimmy::Schema, nil}] properties
26
+ # @param [true, false] required If true, literal (non-pattern) properties
27
+ # will be added to the +required+ property.
28
+ # @yieldparam name [String] The name of a property that was given with a nil
29
+ # schema.
30
+ # @yieldparam schema [Jimmy::Schema] A new schema created for a property
31
+ # that was given without one.
32
+ # @return [self] self, for chaining
33
+ def properties(properties, required: false, &block)
34
+ valid_for 'object'
35
+ assert_hash properties
36
+ groups = properties.group_by { |k, _| collection_for_property_key k }
37
+ groups.each do |collection, pairs|
38
+ batch_assign_to_schema_hash collection, pairs.to_h, &block
39
+ end
40
+ require *properties.keys if required
41
+ self
42
+ end
43
+
44
+ alias allow properties
45
+
46
+ # Designate the given properties as required for object values.
47
+ # @param [Array<String, Symbol, Hash{String, Symbol => Jimmy::Schema, nil}>]
48
+ # properties Names of properties that are required, or hashes that can be
49
+ # passed to +#properties+, and whose keys will also be added to the
50
+ # +required+ property.
51
+ # @yieldparam name [String] The name of a property that was given with a nil
52
+ # schema.
53
+ # @yieldparam schema [Jimmy::Schema] A new schema created for a property
54
+ # that was given without one.
55
+ # @return [self] self, for chaining
56
+ def require(*properties, &block)
57
+ properties.each do |name|
58
+ if name.is_a? Hash
59
+ self.properties name, required: true, &block
60
+ else
61
+ getset('required') { Set.new } << validate_property_name(name)
62
+ end
63
+ end
64
+ self
65
+ end
66
+
67
+ alias requires require
68
+
69
+ # Require all properties that have been explicitly defined for object
70
+ # values.
71
+ # @return [self] self, for chaining
72
+ def require_all
73
+ require *get('properties') { {} }.keys
74
+ end
75
+
76
+ # Set the schema for additional properties not matching those given to
77
+ # +#require+, +#property+, and +#properties+. Pass +false+ to disallow
78
+ # additional properties.
79
+ # @param [Jimmy::Schema, true, false] schema
80
+ # @return [self] self, for chaining
81
+ def additional_properties(schema)
82
+ set additionalProperties: cast_schema(schema)
83
+ end
84
+
85
+ private
86
+
87
+ def collection_for_property_key(key)
88
+ if key.is_a? Regexp
89
+ 'patternProperties'
90
+ else
91
+ 'properties'
92
+ end
93
+ end
94
+
95
+ def validate_property_name(name)
96
+ name = cast_key(name)
97
+ assert_string name
98
+ return name unless get('additionalProperties', nil) == Schema.new.nothing
99
+
100
+ names = get('properties') { {} }.keys
101
+ patterns = get('patternProperties') { {} }.keys
102
+ assert names.include?(name) || patterns.any? { |p| p.match? name } do
103
+ "Expected '#{name}' to be an existing property"
104
+ end
105
+ name
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jimmy
4
+ class Schema
5
+ # Compare the schema to another schema.
6
+ # @param [Schema] other
7
+ # @return [true, false]
8
+ def ==(other)
9
+ return false unless other.is_a? Schema
10
+
11
+ other.as_json == as_json
12
+ end
13
+
14
+ # Get the opposite of this schema, by wrapping it in a new schema's +not+
15
+ # property.
16
+ #
17
+ # If this schema's only property is +not+, its value will instead
18
+ # be returned. Therefore:
19
+ #
20
+ # schema.negated.negated == schema
21
+ #
22
+ # Since +#!+ is an alias for +#negated+, this also works:
23
+ #
24
+ # !!schema == schema
25
+ #
26
+ # Schemas matching absolutes +ANYTHING+ or +NOTHING+ will return the
27
+ # opposite absolute.
28
+ # @return [Jimmy::Schema]
29
+ def negated
30
+ return Schema.new if nothing?
31
+ return Schema.new.nothing if anything?
32
+ return get('not') if keys == ['not']
33
+
34
+ Schema.new.not self
35
+ end
36
+
37
+ # Combine this schema with another schema using an +allOf+ schema. If this
38
+ # schema's only property is +allOf+, its items will be flattened into the
39
+ # new schema.
40
+ #
41
+ # Since +#&+ is an alias of +#and+, the following two statements are
42
+ # equivalent:
43
+ #
44
+ # schema.and(other)
45
+ # schema & other
46
+ # @param [Jimmy::Schema] other The other schema.
47
+ # @return [Jimmy::Schema] The new schema.
48
+ def and(other)
49
+ make_new_composite 'allOf', other
50
+ end
51
+
52
+ # Combine this schema with another schema using an +anyOf+ schema. If this
53
+ # schema's only property is +anyOf+, its items will be flattened into the
54
+ # new schema.
55
+ #
56
+ # Since +#|+ is an alias of +#or+, the following two statements are
57
+ # equivalent:
58
+ #
59
+ # schema.or(other)
60
+ # schema | other
61
+ # @param [Jimmy::Schema] other The other schema.
62
+ # @return [Jimmy::Schema] The new schema.
63
+ def or(other)
64
+ make_new_composite 'anyOf', other
65
+ end
66
+
67
+ # Combine this schema with another schema using a +oneOf+ schema. If this
68
+ # schema's only property is +oneOf+, its items will be flattened into the
69
+ # new schema.
70
+ #
71
+ # Since +#^+ is an alias of +#xor+, the following two statements are
72
+ # equivalent:
73
+ #
74
+ # schema.xor(other)
75
+ # schema ^ other
76
+ # @param [Jimmy::Schema] other The other schema.
77
+ # @return [Jimmy::Schema] The new schema.
78
+ def xor(other)
79
+ make_new_composite 'oneOf', other
80
+ end
81
+
82
+ alias & and
83
+ alias | or
84
+ alias ^ xor
85
+ alias ! negated
86
+
87
+ private
88
+
89
+ def make_new_composite(name, other)
90
+ return self if other == self
91
+
92
+ this = keys == [name] ? get(name) : [self]
93
+ Schema.new.instance_exec { set_composite name, [*this, other] }
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jimmy
4
+ class Schema
5
+ # Set the maximum length for a string value.
6
+ # @param [Numeric] length The maximum length for a string value.
7
+ # @return [self] self, for chaining
8
+ def max_length(length)
9
+ valid_for 'string'
10
+ assert_numeric length, minimum: 0
11
+ set maxLength: length
12
+ end
13
+
14
+ # Set the minimum length for a string value.
15
+ # @param [Numeric] length The minimum length for a string value.
16
+ # @return [self] self, for chaining
17
+ def min_length(length)
18
+ valid_for 'string'
19
+ assert_numeric length, minimum: 0
20
+ set minLength: length
21
+ end
22
+
23
+ # Set the minimum and maximum length for a string value, using a range.
24
+ # @param [Range, Integer] range The minimum and maximum length for a string
25
+ # value. If an integer is given, it is taken to be both.
26
+ # @return [self] self, for chaining
27
+ def length(range)
28
+ range = range..range if range.is_a?(Integer)
29
+ assert_range range
30
+ min_length range.min
31
+ max_length range.max unless range.end.nil?
32
+ self
33
+ end
34
+
35
+ # Set the format for a string value.
36
+ # @param [String] format_name The named format for a string value.
37
+ # @return [self] self, for chaining
38
+ def format(format_name)
39
+ valid_for 'string'
40
+ assert_string format_name
41
+ set format: format_name
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jimmy
4
+ # Represents a schema with a URI.
5
+ class SchemaWithURI
6
+ # @return [Json::URI]
7
+ attr_reader :uri
8
+ # @return [Schema]
9
+ attr_reader :schema
10
+
11
+ # @param [URI, String, Json::URI] uri
12
+ # @param [Schema] schema
13
+ def initialize(uri, schema)
14
+ @uri = Json::URI.new(uri)
15
+ @schema = schema
16
+ freeze
17
+ end
18
+
19
+ # @return [Hash{String => Object}]
20
+ def as_json(*)
21
+ @schema.as_json id: @uri
22
+ end
23
+
24
+ # @return [String]
25
+ def to_json(**opts)
26
+ ::JSON.generate as_json, **opts
27
+ end
28
+
29
+ # Returns true if +other+ has a matching URI and Schema.
30
+ # @param [SchemaWithURI] other
31
+ # @return [true, false]
32
+ def ==(other)
33
+ other.is_a?(self.class) && uri == other.uri && schema == other.schema
34
+ end
35
+
36
+ # Attempt to resolve URI using {#schema}. This will only succeed if +uri+
37
+ # represents a fragment of {#schema}.
38
+ # @raise [Error::BadArgument] Raised if the URI is outside {#uri}.
39
+ # @param [String, URI, Json::URI] uri
40
+ # @return [SchemaWithURI, nil]
41
+ def resolve(uri)
42
+ uri = Json::URI.new(uri)
43
+ raise Error::BadArgument, 'Cannot resolve relative URIs' if uri.relative?
44
+ raise Error::BadArgument, 'Wrong URI base' unless uri + '#' == @uri + '#'
45
+
46
+ pointer = uri.pointer.remove_prefix(@uri.pointer) or return
47
+
48
+ return unless (fragment = @schema.get_fragment(pointer))
49
+
50
+ self.class.new uri, fragment
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jimmy
4
+ # Factory class for making +JSONSchemer::Schema::Base+ instances
5
+ class SchemerFactory
6
+ # Returns true if the +json_schemer+ gem is loaded.
7
+ # @return [true, false]
8
+ def self.available?
9
+ defined? ::JSONSchemer
10
+ end
11
+
12
+ # @param [Schema, #as_json] schema
13
+ # @param [Array<#resolve, 'net/http'>] resolvers
14
+ # @param [true, false] cache_resolvers
15
+ # @param [Hash] opts Options to be passed to +JSONSchemer+
16
+ def initialize(schema, *resolvers, cache_resolvers: true, **opts)
17
+ unless self.class.available?
18
+ raise LoadError, 'Please add the json_schemer gem to your Gemfile'
19
+ end
20
+
21
+ @schema = schema
22
+ @resolvers = resolvers.map(&method(:cast_resolver))
23
+ @options = opts.dup
24
+
25
+ return if @resolvers.empty?
26
+
27
+ res = method(:resolve)
28
+ res = JSONSchemer::CachedRefResolver.new(&res) if cache_resolvers
29
+ @options[:ref_resolver] = res
30
+ end
31
+
32
+ # Get an instance of {JSONSchemer::Schema::Base} that can be used to
33
+ # validate JSON documents against the given {Schema}.
34
+ # @return [JSONSchemer::Schema::Base]
35
+ def schemer
36
+ @schemer ||= JSONSchemer.schema(@schema.as_json, **@options)
37
+ end
38
+
39
+ # @param [String, URI, Json::URI] uri
40
+ # @return [Hash{String => Object}, nil]
41
+ def resolve(uri)
42
+ @resolvers.each do |resolver|
43
+ return resolver.call(uri) unless resolver.respond_to? :resolve
44
+
45
+ schema = resolver.resolve(uri)
46
+ return schema.as_json if schema
47
+ end
48
+ nil
49
+ end
50
+
51
+ private
52
+
53
+ def cast_resolver(resolver)
54
+ if resolver == 'net/http'
55
+ return JSONSchemer::Schema::Base::NET_HTTP_REF_RESOLVER
56
+ end
57
+
58
+ unless resolver.respond_to? :resolve
59
+ raise Error::BadArgument, 'Expected an object responding to :resolve'
60
+ end
61
+
62
+ resolver
63
+ end
64
+ end
65
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Jimmy
2
- VERSION = '0.5.1'
4
+ VERSION = '2.0.0'
3
5
  end
@@ -0,0 +1,172 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "http://json-schema.org/draft-07/schema#",
4
+ "title": "Core schema meta-schema",
5
+ "definitions": {
6
+ "schemaArray": {
7
+ "type": "array",
8
+ "minItems": 1,
9
+ "items": { "$ref": "#" }
10
+ },
11
+ "nonNegativeInteger": {
12
+ "type": "integer",
13
+ "minimum": 0
14
+ },
15
+ "nonNegativeIntegerDefault0": {
16
+ "allOf": [
17
+ { "$ref": "#/definitions/nonNegativeInteger" },
18
+ { "default": 0 }
19
+ ]
20
+ },
21
+ "simpleTypes": {
22
+ "enum": [
23
+ "array",
24
+ "boolean",
25
+ "integer",
26
+ "null",
27
+ "number",
28
+ "object",
29
+ "string"
30
+ ]
31
+ },
32
+ "stringArray": {
33
+ "type": "array",
34
+ "items": { "type": "string" },
35
+ "uniqueItems": true,
36
+ "default": []
37
+ }
38
+ },
39
+ "type": ["object", "boolean"],
40
+ "properties": {
41
+ "$id": {
42
+ "type": "string",
43
+ "format": "uri-reference"
44
+ },
45
+ "$schema": {
46
+ "type": "string",
47
+ "format": "uri"
48
+ },
49
+ "$ref": {
50
+ "type": "string",
51
+ "format": "uri-reference"
52
+ },
53
+ "$comment": {
54
+ "type": "string"
55
+ },
56
+ "title": {
57
+ "type": "string"
58
+ },
59
+ "description": {
60
+ "type": "string"
61
+ },
62
+ "default": true,
63
+ "readOnly": {
64
+ "type": "boolean",
65
+ "default": false
66
+ },
67
+ "writeOnly": {
68
+ "type": "boolean",
69
+ "default": false
70
+ },
71
+ "examples": {
72
+ "type": "array",
73
+ "items": true
74
+ },
75
+ "multipleOf": {
76
+ "type": "number",
77
+ "exclusiveMinimum": 0
78
+ },
79
+ "maximum": {
80
+ "type": "number"
81
+ },
82
+ "exclusiveMaximum": {
83
+ "type": "number"
84
+ },
85
+ "minimum": {
86
+ "type": "number"
87
+ },
88
+ "exclusiveMinimum": {
89
+ "type": "number"
90
+ },
91
+ "maxLength": { "$ref": "#/definitions/nonNegativeInteger" },
92
+ "minLength": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
93
+ "pattern": {
94
+ "type": "string",
95
+ "format": "regex"
96
+ },
97
+ "additionalItems": { "$ref": "#" },
98
+ "items": {
99
+ "anyOf": [
100
+ { "$ref": "#" },
101
+ { "$ref": "#/definitions/schemaArray" }
102
+ ],
103
+ "default": true
104
+ },
105
+ "maxItems": { "$ref": "#/definitions/nonNegativeInteger" },
106
+ "minItems": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
107
+ "uniqueItems": {
108
+ "type": "boolean",
109
+ "default": false
110
+ },
111
+ "contains": { "$ref": "#" },
112
+ "maxProperties": { "$ref": "#/definitions/nonNegativeInteger" },
113
+ "minProperties": { "$ref": "#/definitions/nonNegativeIntegerDefault0" },
114
+ "required": { "$ref": "#/definitions/stringArray" },
115
+ "additionalProperties": { "$ref": "#" },
116
+ "definitions": {
117
+ "type": "object",
118
+ "additionalProperties": { "$ref": "#" },
119
+ "default": {}
120
+ },
121
+ "properties": {
122
+ "type": "object",
123
+ "additionalProperties": { "$ref": "#" },
124
+ "default": {}
125
+ },
126
+ "patternProperties": {
127
+ "type": "object",
128
+ "additionalProperties": { "$ref": "#" },
129
+ "propertyNames": { "format": "regex" },
130
+ "default": {}
131
+ },
132
+ "dependencies": {
133
+ "type": "object",
134
+ "additionalProperties": {
135
+ "anyOf": [
136
+ { "$ref": "#" },
137
+ { "$ref": "#/definitions/stringArray" }
138
+ ]
139
+ }
140
+ },
141
+ "propertyNames": { "$ref": "#" },
142
+ "const": true,
143
+ "enum": {
144
+ "type": "array",
145
+ "items": true,
146
+ "minItems": 1,
147
+ "uniqueItems": true
148
+ },
149
+ "type": {
150
+ "anyOf": [
151
+ { "$ref": "#/definitions/simpleTypes" },
152
+ {
153
+ "type": "array",
154
+ "items": { "$ref": "#/definitions/simpleTypes" },
155
+ "minItems": 1,
156
+ "uniqueItems": true
157
+ }
158
+ ]
159
+ },
160
+ "format": { "type": "string" },
161
+ "contentMediaType": { "type": "string" },
162
+ "contentEncoding": { "type": "string" },
163
+ "if": { "$ref": "#" },
164
+ "then": { "$ref": "#" },
165
+ "else": { "$ref": "#" },
166
+ "allOf": { "$ref": "#/definitions/schemaArray" },
167
+ "anyOf": { "$ref": "#/definitions/schemaArray" },
168
+ "oneOf": { "$ref": "#/definitions/schemaArray" },
169
+ "not": { "$ref": "#" }
170
+ },
171
+ "default": true
172
+ }