json_model_rb 0.1.1

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 (59) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +0 -0
  4. data/lib/json_model/config/options.rb +29 -0
  5. data/lib/json_model/config.rb +43 -0
  6. data/lib/json_model/errors/error.rb +7 -0
  7. data/lib/json_model/errors/invalid_ref_mode_error.rb +12 -0
  8. data/lib/json_model/errors/unknown_attribute_error.rb +13 -0
  9. data/lib/json_model/errors.rb +5 -0
  10. data/lib/json_model/properties.rb +66 -0
  11. data/lib/json_model/property.rb +54 -0
  12. data/lib/json_model/ref_mode.rb +9 -0
  13. data/lib/json_model/schema.rb +111 -0
  14. data/lib/json_model/schema_meta.rb +60 -0
  15. data/lib/json_model/type_spec/array.rb +62 -0
  16. data/lib/json_model/type_spec/composition/all_of.rb +15 -0
  17. data/lib/json_model/type_spec/composition/any_of.rb +15 -0
  18. data/lib/json_model/type_spec/composition/one_of.rb +15 -0
  19. data/lib/json_model/type_spec/composition.rb +32 -0
  20. data/lib/json_model/type_spec/enum.rb +33 -0
  21. data/lib/json_model/type_spec/object.rb +24 -0
  22. data/lib/json_model/type_spec/primitive/boolean.rb +13 -0
  23. data/lib/json_model/type_spec/primitive/integer.rb +21 -0
  24. data/lib/json_model/type_spec/primitive/null.rb +13 -0
  25. data/lib/json_model/type_spec/primitive/number.rb +14 -0
  26. data/lib/json_model/type_spec/primitive/numeric.rb +83 -0
  27. data/lib/json_model/type_spec/primitive/string.rb +150 -0
  28. data/lib/json_model/type_spec/primitive.rb +26 -0
  29. data/lib/json_model/type_spec.rb +67 -0
  30. data/lib/json_model/types/all_of.rb +26 -0
  31. data/lib/json_model/types/any_of.rb +26 -0
  32. data/lib/json_model/types/array.rb +26 -0
  33. data/lib/json_model/types/boolean.rb +7 -0
  34. data/lib/json_model/types/enum.rb +23 -0
  35. data/lib/json_model/types/one_of.rb +26 -0
  36. data/lib/json_model/types.rb +8 -0
  37. data/lib/json_model/version.rb +5 -0
  38. data/lib/json_model.rb +31 -0
  39. data/spec/config_spec.rb +106 -0
  40. data/spec/json_model_spec.rb +7 -0
  41. data/spec/properties_spec.rb +76 -0
  42. data/spec/property_spec.rb +86 -0
  43. data/spec/schema_meta_spec.rb +78 -0
  44. data/spec/schema_spec.rb +218 -0
  45. data/spec/spec_helper.rb +13 -0
  46. data/spec/type_spec/array_spec.rb +206 -0
  47. data/spec/type_spec/composition/all_of_spec.rb +20 -0
  48. data/spec/type_spec/composition/any_of_spec.rb +20 -0
  49. data/spec/type_spec/composition/one_of_spec.rb +20 -0
  50. data/spec/type_spec/composition_spec.rb +90 -0
  51. data/spec/type_spec/enum_spec.rb +93 -0
  52. data/spec/type_spec/primitive/boolean_spec.rb +12 -0
  53. data/spec/type_spec/primitive/integer_spec.rb +57 -0
  54. data/spec/type_spec/primitive/null_spec.rb +12 -0
  55. data/spec/type_spec/primitive/number_spec.rb +12 -0
  56. data/spec/type_spec/primitive/numeric_spec.rb +176 -0
  57. data/spec/type_spec/primitive/string_spec.rb +119 -0
  58. data/spec/type_spec_spec.rb +32 -0
  59. metadata +183 -0
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require('spec_helper')
4
+
5
+ RSpec.describe(JsonModel::Properties) do
6
+ describe('.property') do
7
+ let(:klass) do
8
+ Class.new do
9
+ include(JsonModel::Properties)
10
+ end
11
+ end
12
+
13
+ it('has no default properties') do
14
+ expect(klass.properties)
15
+ .to(eq({}))
16
+ end
17
+
18
+ it('adds a property by name') do
19
+ klass.property(:foo, type: String)
20
+
21
+ expect(klass.properties.keys)
22
+ .to(eq(%i(foo)))
23
+ expect(klass.properties[:foo])
24
+ .to(be_a(JsonModel::Property))
25
+ end
26
+ end
27
+
28
+ describe('getter and setter') do
29
+ let(:klass) do
30
+ Class.new do
31
+ include(JsonModel::Properties)
32
+
33
+ property(:foo, type: String)
34
+ end
35
+ end
36
+
37
+ it('responds to the getter') do
38
+ expect(klass.new)
39
+ .to(respond_to(:foo))
40
+ end
41
+
42
+ it('responds to the setter') do
43
+ expect(klass.new)
44
+ .to(respond_to(:foo=))
45
+ end
46
+
47
+ it('allows updating the property') do
48
+ instance = klass.new
49
+
50
+ expect(instance.foo)
51
+ .to(be_nil)
52
+
53
+ instance.foo = 'bar'
54
+
55
+ expect(instance.foo)
56
+ .to(eq('bar'))
57
+ end
58
+
59
+ context('with a default value') do
60
+ before { klass.property(:foo, type: String, default: 'bar') }
61
+
62
+ it('returns the default value') do
63
+ expect(klass.new.foo)
64
+ .to(eq('bar'))
65
+ end
66
+
67
+ it('returns the updated value') do
68
+ instance = klass.new
69
+ instance.foo = 'foo'
70
+
71
+ expect(instance.foo)
72
+ .to(eq('foo'))
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require('spec_helper')
4
+
5
+ RSpec.describe(JsonModel::Property) do
6
+ describe('.register_validations') do
7
+ let(:klass) do
8
+ Class.new do
9
+ include(ActiveModel::Validations)
10
+
11
+ attr_accessor(:foo)
12
+ end
13
+ end
14
+
15
+ context('for an optional property') do
16
+ before do
17
+ described_class
18
+ .new(:foo, type: JsonModel::TypeSpec::Primitive::String.new, optional: true)
19
+ .register_validations(klass)
20
+ end
21
+
22
+ it('does not fail the validation for missing values') do
23
+ instance = klass.new
24
+
25
+ expect(instance.valid?)
26
+ .to(be(true))
27
+ expect(instance.errors)
28
+ .to(be_empty)
29
+ end
30
+
31
+ it('does not fail the validation for present values') do
32
+ instance = klass.new.tap { |obj| obj.foo = 'bar' }
33
+
34
+ expect(instance.valid?)
35
+ .to(be(true))
36
+ expect(instance.errors)
37
+ .to(be_empty)
38
+ end
39
+ end
40
+
41
+ context('for a non-optional property') do
42
+ before do
43
+ described_class
44
+ .new(:foo, type: JsonModel::TypeSpec::Primitive::String.new)
45
+ .register_validations(klass)
46
+ end
47
+
48
+ it('fails the validation for missing values') do
49
+ instance = klass.new
50
+
51
+ expect(instance.valid?)
52
+ .to(be(false))
53
+ expect(instance.errors.map(&:type))
54
+ .to(eq(%i(blank)))
55
+ end
56
+
57
+ it('does not fail the validation for present values') do
58
+ instance = klass.new.tap { |obj| obj.foo = 'bar' }
59
+
60
+ expect(instance.valid?)
61
+ .to(be(true))
62
+ expect(instance.errors)
63
+ .to(be_empty)
64
+ end
65
+ end
66
+ end
67
+
68
+ describe('#as_schema') do
69
+ it('renders the property as a schema') do
70
+ expect(described_class.new(:foo, type: JsonModel::TypeSpec::Primitive::String.new).as_schema)
71
+ .to(eq({ foo: { type: 'string' } }))
72
+ end
73
+
74
+ it('respects the "as" option') do
75
+ expect(described_class.new(:foo, type: JsonModel::TypeSpec::Primitive::String.new, as: :bar).as_schema)
76
+ .to(eq({ bar: { type: 'string' } }))
77
+ end
78
+
79
+ it('falls back to the configured naming strategy') do
80
+ JsonModel.configure { |config| config.property_naming_strategy = :camel_case }
81
+
82
+ expect(described_class.new(:foo_bar, type: JsonModel::TypeSpec::Primitive::String.new).as_schema)
83
+ .to(eq({ fooBar: { type: 'string' } }))
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require('spec_helper')
4
+
5
+ RSpec.describe(JsonModel::SchemaMeta) do
6
+ describe('.schema_id') do
7
+ let(:klass) do
8
+ Class.new do
9
+ include(JsonModel::SchemaMeta)
10
+ end
11
+ end
12
+
13
+ it('is nil by default') do
14
+ expect(klass.schema_id)
15
+ .to(be_nil)
16
+ end
17
+
18
+ it('can be changed') do
19
+ klass.schema_id('foo')
20
+
21
+ expect(klass.schema_id)
22
+ .to(eq('foo'))
23
+ end
24
+
25
+ it('respects the schema_id_base_uri') do
26
+ JsonModel.configure { |config| config.schema_id_base_uri = 'http://example.com/schemas/' }
27
+ klass.schema_id('foo')
28
+
29
+ expect(klass.schema_id)
30
+ .to(eq('http://example.com/schemas/foo'))
31
+ end
32
+
33
+ it('raises an error if the schema id is invalid') do
34
+ expect { klass.schema_id('http://example .com') }
35
+ .to(raise_error(URI::InvalidURIError))
36
+ end
37
+ end
38
+
39
+ describe('.schema_version') do
40
+ let(:klass) do
41
+ Class.new do
42
+ include(JsonModel::SchemaMeta)
43
+ end
44
+ end
45
+
46
+ it('is nil by default') do
47
+ expect(klass.schema_version)
48
+ .to(be_nil)
49
+ end
50
+
51
+ it('can be changed') do
52
+ klass.schema_version(:draft202012)
53
+
54
+ expect(klass.schema_version)
55
+ .to(eq('https://json-schema.org/draft/2020-12/schema'))
56
+ end
57
+ end
58
+
59
+ describe('.additional_properties') do
60
+ let(:klass) do
61
+ Class.new do
62
+ include(JsonModel::SchemaMeta)
63
+ end
64
+ end
65
+
66
+ it('is missing by default') do
67
+ expect(klass.meta_attributes)
68
+ .to(eq({}))
69
+ end
70
+
71
+ it('can be changed') do
72
+ klass.additional_properties(true)
73
+
74
+ expect(klass.meta_attributes)
75
+ .to(eq({ additionalProperties: true }))
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,218 @@
1
+ # frozen_string_literal: true
2
+
3
+ require('spec_helper')
4
+
5
+ RSpec.describe(JsonModel::Schema) do
6
+ describe('.initialize') do
7
+ let(:klass) do
8
+ Class.new do
9
+ include(JsonModel::Schema)
10
+
11
+ property(:foo, type: String, optional: true)
12
+
13
+ def self.name
14
+ 'Foo'
15
+ end
16
+ end
17
+ end
18
+
19
+ it('succeeds without providing attributes') do
20
+ expect { klass.new }.not_to(raise_error)
21
+ end
22
+
23
+ it('sets attribute values') do
24
+ instance = klass.new(foo: 'bar')
25
+
26
+ expect(instance.foo)
27
+ .to(eq('bar'))
28
+ end
29
+
30
+ it('raises an error for unknown attributes when additional properties are not allowed') do
31
+ expect { klass.new(bar: 'baz') }
32
+ .to(raise_error(JsonModel::Errors::UnknownAttributeError))
33
+ end
34
+
35
+ it('does not raise an error for unknown attributes when additional properties are allowed') do
36
+ klass.additional_properties(true)
37
+
38
+ klass.new(bar: 'baz')
39
+ end
40
+ end
41
+
42
+ describe('#valid?') do
43
+ let(:klass) do
44
+ Class.new do
45
+ include(JsonModel::Schema)
46
+
47
+ property(:foo, type: String, min_length: 3)
48
+ end
49
+ end
50
+
51
+ before do
52
+ JsonModel.configure { |config| config.validate_after_instantiation = false }
53
+ end
54
+
55
+ it('returns false for invalid values') do
56
+ expect(klass.new(foo: 'ba').valid?)
57
+ .to(be(false))
58
+ end
59
+
60
+ it('returns true for valid values') do
61
+ expect(klass.new(foo: 'bar').valid?)
62
+ .to(be(true))
63
+ end
64
+ end
65
+
66
+ describe('.as_schema') do
67
+ let(:klass) do
68
+ Class.new do
69
+ include(JsonModel::Schema)
70
+ end
71
+ end
72
+
73
+ it('returns an empty schema') do
74
+ expect(klass.as_schema)
75
+ .to(eq({ type: 'object' }))
76
+ end
77
+
78
+ it('includes the schema id') do
79
+ klass.schema_id('https://example.com/schemas/example.json')
80
+
81
+ expect(klass.as_schema)
82
+ .to(
83
+ eq(
84
+ {
85
+ '$id': 'https://example.com/schemas/example.json',
86
+ type: 'object',
87
+ },
88
+ ),
89
+ )
90
+ end
91
+
92
+ it('returns properties as schema') do
93
+ klass.schema_id('https://example.com/schemas/example.json')
94
+ klass.property(:foo, type: String)
95
+ klass.property(:bar, type: Float, optional: true)
96
+ klass.property(:baz, type: T::Enum[1, 'a', nil])
97
+ klass.property(:bam, type: T::Array[T::AllOf[String, Float]])
98
+ klass.property(:bal, type: klass, ref_mode: JsonModel::RefMode::EXTERNAL, optional: true)
99
+
100
+ expect(klass.as_schema)
101
+ .to(
102
+ eq(
103
+ {
104
+ '$id': 'https://example.com/schemas/example.json',
105
+ type: 'object',
106
+ properties: {
107
+ bal: { '$ref': 'https://example.com/schemas/example.json' },
108
+ bam: {
109
+ type: 'array',
110
+ items: {
111
+ allOf: [{ type: 'string' }, { type: 'number' }],
112
+ },
113
+ },
114
+ bar: { type: 'number' },
115
+ baz: { enum: [1, 'a', nil] },
116
+ foo: { type: 'string' },
117
+ },
118
+ required: %i(bam baz foo),
119
+ },
120
+ ),
121
+ )
122
+ end
123
+
124
+ it('collects local references in $defs') do
125
+ klass.property(
126
+ :foo,
127
+ type: Class.new do
128
+ include(JsonModel::Schema)
129
+
130
+ property(:foo, type: String)
131
+ end,
132
+ )
133
+ klass.property(
134
+ :bam,
135
+ type: Class.new do
136
+ include(JsonModel::Schema)
137
+
138
+ property(:bam, type: String)
139
+ schema_id('https://example.com/schemas/bam.json')
140
+ end,
141
+ ref_mode: JsonModel::RefMode::EXTERNAL,
142
+ )
143
+ klass.property(
144
+ :bar,
145
+ type: T::Array[
146
+ T::Array[
147
+ Class.new do
148
+ include(JsonModel::Schema)
149
+
150
+ property(:bar, type: String)
151
+
152
+ def self.name
153
+ 'Bar'
154
+ end
155
+ end,
156
+ ],
157
+ ],
158
+ ref_mode: JsonModel::RefMode::LOCAL,
159
+ )
160
+
161
+ expect(klass.as_schema)
162
+ .to(
163
+ eq(
164
+ {
165
+ type: 'object',
166
+ properties: {
167
+ bam: { '$ref': 'https://example.com/schemas/bam.json' },
168
+ bar: {
169
+ type: 'array',
170
+ items: {
171
+ type: 'array',
172
+ items: { '$ref': '#/$defs/Bar' },
173
+ },
174
+ },
175
+ foo: {
176
+ type: 'object',
177
+ properties: { foo: { type: 'string' } },
178
+ required: %i(foo),
179
+ },
180
+ },
181
+ '$defs': {
182
+ Bar: {
183
+ type: 'object',
184
+ properties: { bar: { type: 'string' } },
185
+ required: %i(bar),
186
+ },
187
+ },
188
+ required: %i(bam bar foo),
189
+ },
190
+ ),
191
+ )
192
+ end
193
+
194
+ context('inheritance') do
195
+ let(:child) do
196
+ Class.new(klass) do
197
+ schema_id('https://example.com/schemas/child.json')
198
+ end
199
+ end
200
+
201
+ it('uses $ref for inherited schemas if they have a schema id') do
202
+ klass.schema_id('https://example.com/schemas/example.json')
203
+ klass.property(:foo, type: String)
204
+
205
+ expect(child.as_schema)
206
+ .to(
207
+ eq(
208
+ {
209
+ '$id': 'https://example.com/schemas/child.json',
210
+ '$ref': 'https://example.com/schemas/example.json',
211
+ type: 'object',
212
+ },
213
+ ),
214
+ )
215
+ end
216
+ end
217
+ end
218
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require('bundler/setup')
4
+ require('json_model')
5
+
6
+ RSpec.configure do |config|
7
+ config.disable_monkey_patching!
8
+ config.after do
9
+ JsonModel.configure do |json_model_config|
10
+ JsonModel.config.defaults.each { |key, value| json_model_config.send("#{key}=", value) }
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,206 @@
1
+ # frozen_string_literal: true
2
+
3
+ require('spec_helper')
4
+
5
+ RSpec.describe(JsonModel::TypeSpec::Array) do
6
+ describe('#as_schema') do
7
+ it('returns an array schema of primitive types') do
8
+ expect(
9
+ described_class.new(JsonModel::TypeSpec::Primitive::String.new).as_schema,
10
+ )
11
+ .to(
12
+ eq(
13
+ {
14
+ type: 'array',
15
+ items: { type: 'string' },
16
+ },
17
+ ),
18
+ )
19
+ end
20
+
21
+ it('returns an array schema of nested array types') do
22
+ expect(
23
+ described_class.new(described_class.new(JsonModel::TypeSpec::Primitive::String.new)).as_schema,
24
+ )
25
+ .to(
26
+ eq(
27
+ {
28
+ type: 'array',
29
+ items: { type: 'array', items: { type: 'string' } },
30
+ },
31
+ ),
32
+ )
33
+ end
34
+
35
+ it('includes the minItems attribute') do
36
+ expect(
37
+ described_class.new(JsonModel::TypeSpec::Primitive::String.new, min_items: 10).as_schema,
38
+ )
39
+ .to(
40
+ eq(
41
+ {
42
+ type: 'array',
43
+ items: { type: 'string' },
44
+ minItems: 10,
45
+ },
46
+ ),
47
+ )
48
+ end
49
+
50
+ it('includes the maxItems attribute') do
51
+ expect(
52
+ described_class.new(JsonModel::TypeSpec::Primitive::String.new, max_items: 10).as_schema,
53
+ )
54
+ .to(
55
+ eq(
56
+ {
57
+ type: 'array',
58
+ items: { type: 'string' },
59
+ maxItems: 10,
60
+ },
61
+ ),
62
+ )
63
+ end
64
+
65
+ it('uses external references') do
66
+ expect(
67
+ described_class
68
+ .new(
69
+ Class.new do
70
+ include(JsonModel::Schema)
71
+
72
+ property(:foo, type: String)
73
+ schema_id('https://example.com/schemas/foo.json')
74
+ end,
75
+ )
76
+ .as_schema(ref_mode: JsonModel::RefMode::EXTERNAL),
77
+ )
78
+ .to(
79
+ eq(
80
+ {
81
+ type: 'array',
82
+ items: { '$ref': 'https://example.com/schemas/foo.json' },
83
+ },
84
+ ),
85
+ )
86
+ end
87
+
88
+ it('uses local references') do
89
+ expect(
90
+ described_class
91
+ .new(
92
+ Class.new do
93
+ include(JsonModel::Schema)
94
+
95
+ property(:foo, type: String)
96
+
97
+ def self.name
98
+ 'Foo'
99
+ end
100
+ end,
101
+ )
102
+ .as_schema(ref_mode: JsonModel::RefMode::LOCAL),
103
+ )
104
+ .to(
105
+ eq(
106
+ {
107
+ type: 'array',
108
+ items: { '$ref': '#/$defs/Foo' },
109
+ },
110
+ ),
111
+ )
112
+ end
113
+ end
114
+
115
+ describe('.register_validations') do
116
+ let(:klass) do
117
+ Class.new do
118
+ include(ActiveModel::Validations)
119
+
120
+ attr_accessor(:foo)
121
+
122
+ def initialize(foo:)
123
+ @foo = foo
124
+ end
125
+ end
126
+ end
127
+
128
+ context('for min items') do
129
+ before do
130
+ described_class
131
+ .new(JsonModel::TypeSpec::Primitive::String.new, min_items: 5)
132
+ .register_validations(:foo, klass)
133
+ end
134
+
135
+ it('fails the validation for invalid values') do
136
+ instance = klass.new(foo: ['a'] * 4)
137
+
138
+ expect(instance.valid?)
139
+ .to(be(false))
140
+ expect(instance.errors.map(&:type))
141
+ .to(eq(%i(too_short)))
142
+ end
143
+
144
+ it('succeeds the validation for valid values') do
145
+ instance = klass.new(foo: ['a'] * 5)
146
+
147
+ expect(instance.valid?)
148
+ .to(be(true))
149
+ expect(instance.errors)
150
+ .to(be_empty)
151
+ end
152
+ end
153
+
154
+ context('for max items') do
155
+ before do
156
+ described_class
157
+ .new(JsonModel::TypeSpec::Primitive::String.new, max_items: 5)
158
+ .register_validations(:foo, klass)
159
+ end
160
+
161
+ it('fails the validation for invalid values') do
162
+ instance = klass.new(foo: ['a'] * 6)
163
+
164
+ expect(instance.valid?)
165
+ .to(be(false))
166
+ expect(instance.errors.map(&:type))
167
+ .to(eq(%i(too_long)))
168
+ end
169
+
170
+ it('succeeds the validation for valid values') do
171
+ instance = klass.new(foo: ['a'] * 5)
172
+
173
+ expect(instance.valid?)
174
+ .to(be(true))
175
+ expect(instance.errors)
176
+ .to(be_empty)
177
+ end
178
+ end
179
+
180
+ context('for unique items') do
181
+ before do
182
+ described_class
183
+ .new(JsonModel::TypeSpec::Primitive::String.new, unique_items: true)
184
+ .register_validations(:foo, klass)
185
+ end
186
+
187
+ it('fails the validation for invalid values') do
188
+ instance = klass.new(foo: %w(a b a))
189
+
190
+ expect(instance.valid?)
191
+ .to(be(false))
192
+ expect(instance.errors.map(&:type))
193
+ .to(eq(%i(uniqueness)))
194
+ end
195
+
196
+ it('succeeds the validation for valid values') do
197
+ instance = klass.new(foo: %w(a b c))
198
+
199
+ expect(instance.valid?)
200
+ .to(be(true))
201
+ expect(instance.errors)
202
+ .to(be_empty)
203
+ end
204
+ end
205
+ end
206
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require('spec_helper')
4
+
5
+ RSpec.describe(JsonModel::TypeSpec::Composition::AllOf) do
6
+ describe('#as_schema') do
7
+ it('returns an all of schema') do
8
+ expect(
9
+ described_class.new(JsonModel::TypeSpec::Primitive::String.new).as_schema,
10
+ )
11
+ .to(
12
+ eq(
13
+ {
14
+ allOf: [{ type: 'string' }],
15
+ },
16
+ ),
17
+ )
18
+ end
19
+ end
20
+ end