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,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.5'
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
+ }