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,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require('spec_helper')
4
+
5
+ RSpec.describe(JsonModel::TypeSpec::Composition::AnyOf) do
6
+ describe('#as_schema') do
7
+ it('returns an any of schema') do
8
+ expect(
9
+ described_class.new(JsonModel::TypeSpec::Primitive::String.new).as_schema,
10
+ )
11
+ .to(
12
+ eq(
13
+ {
14
+ anyOf: [{ type: 'string' }],
15
+ },
16
+ ),
17
+ )
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require('spec_helper')
4
+
5
+ RSpec.describe(JsonModel::TypeSpec::Composition::OneOf) do
6
+ describe('#as_schema') do
7
+ it('returns a one of schema') do
8
+ expect(
9
+ described_class.new(JsonModel::TypeSpec::Primitive::String.new).as_schema,
10
+ )
11
+ .to(
12
+ eq(
13
+ {
14
+ oneOf: [{ type: 'string' }],
15
+ },
16
+ ),
17
+ )
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require('spec_helper')
4
+
5
+ RSpec.describe(JsonModel::TypeSpec::Composition) do
6
+ describe('#as_schema') do
7
+ it('returns schema of complex types') do
8
+ expect(
9
+ described_class
10
+ .new(
11
+ :allOf,
12
+ JsonModel::TypeSpec::Primitive::String.new,
13
+ Class.new do
14
+ include(JsonModel::Schema)
15
+
16
+ property(:foo, type: String)
17
+ end,
18
+ )
19
+ .as_schema,
20
+ )
21
+ .to(
22
+ eq(
23
+ {
24
+ allOf: [
25
+ { type: 'string' },
26
+ {
27
+ type: 'object',
28
+ properties: { foo: { type: 'string' } },
29
+ required: %i(foo),
30
+ },
31
+ ],
32
+ },
33
+ ),
34
+ )
35
+ end
36
+
37
+ it('uses external references') do
38
+ expect(
39
+ described_class
40
+ .new(
41
+ :allOf,
42
+ Class.new do
43
+ include(JsonModel::Schema)
44
+
45
+ property(:foo, type: String)
46
+ schema_id('https://example.com/schemas/foo.json')
47
+ end,
48
+ )
49
+ .as_schema(ref_mode: JsonModel::RefMode::EXTERNAL),
50
+ )
51
+ .to(
52
+ eq(
53
+ {
54
+ allOf: [
55
+ { '$ref': 'https://example.com/schemas/foo.json' },
56
+ ],
57
+ },
58
+ ),
59
+ )
60
+ end
61
+
62
+ it('uses local references') do
63
+ expect(
64
+ described_class
65
+ .new(
66
+ :allOf,
67
+ Class.new do
68
+ include(JsonModel::Schema)
69
+
70
+ property(:foo, type: String)
71
+
72
+ def self.name
73
+ 'Foo'
74
+ end
75
+ end,
76
+ )
77
+ .as_schema(ref_mode: JsonModel::RefMode::LOCAL),
78
+ )
79
+ .to(
80
+ eq(
81
+ {
82
+ allOf: [
83
+ { '$ref': '#/$defs/Foo' },
84
+ ],
85
+ },
86
+ ),
87
+ )
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require('spec_helper')
4
+
5
+ RSpec.describe(JsonModel::TypeSpec::Enum) do
6
+ describe('#as_schema') do
7
+ it('returns an enum schema') do
8
+ expect(described_class.new(1, 'a').as_schema)
9
+ .to(
10
+ eq(
11
+ {
12
+ enum: [1, 'a'],
13
+ },
14
+ ),
15
+ )
16
+ end
17
+ end
18
+
19
+ describe('.register_validations') do
20
+ let(:klass) do
21
+ Class.new do
22
+ include(ActiveModel::Validations)
23
+
24
+ attr_accessor(:foo)
25
+
26
+ def initialize(foo:)
27
+ @foo = foo
28
+ end
29
+ end
30
+ end
31
+
32
+ context('for nil values') do
33
+ it('fails the validation if not allowed') do
34
+ described_class
35
+ .new(1)
36
+ .register_validations(:foo, klass)
37
+ instance = klass.new(foo: nil)
38
+
39
+ expect(instance.valid?)
40
+ .to(be(false))
41
+ expect(instance.errors.map(&:type))
42
+ .to(eq(%i(inclusion)))
43
+ end
44
+
45
+ it('succeeds the validation if allowed') do
46
+ described_class
47
+ .new(nil)
48
+ .register_validations(:foo, klass)
49
+ instance = klass.new(foo: nil)
50
+
51
+ expect(instance.valid?)
52
+ .to(be(true))
53
+ expect(instance.errors)
54
+ .to(be_empty)
55
+ end
56
+ end
57
+
58
+ context('for non-nil values') do
59
+ before do
60
+ described_class
61
+ .new(1, 'a')
62
+ .register_validations(:foo, klass)
63
+ end
64
+
65
+ it('fails the validation if nil') do
66
+ instance = klass.new(foo: nil)
67
+
68
+ expect(instance.valid?)
69
+ .to(be(false))
70
+ expect(instance.errors.map(&:type))
71
+ .to(eq(%i(inclusion)))
72
+ end
73
+
74
+ it('fails the validation if not allowed') do
75
+ instance = klass.new(foo: 0)
76
+
77
+ expect(instance.valid?)
78
+ .to(be(false))
79
+ expect(instance.errors.map(&:type))
80
+ .to(eq(%i(inclusion)))
81
+ end
82
+
83
+ it('succeeds the validation if allowed') do
84
+ instance = klass.new(foo: 'a')
85
+
86
+ expect(instance.valid?)
87
+ .to(be(true))
88
+ expect(instance.errors)
89
+ .to(be_empty)
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require('spec_helper')
4
+
5
+ RSpec.describe(JsonModel::TypeSpec::Primitive::Boolean) do
6
+ describe('#as_schema') do
7
+ it('returns a boolean schema') do
8
+ expect(subject.as_schema)
9
+ .to(eq({ type: 'boolean' }))
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require('spec_helper')
4
+
5
+ RSpec.describe(JsonModel::TypeSpec::Primitive::Integer) do
6
+ describe('#as_schema') do
7
+ it('returns an integer schema') do
8
+ expect(subject.as_schema)
9
+ .to(eq({ type: 'integer' }))
10
+ end
11
+
12
+ it('includes the multipleOf attribute') do
13
+ expect(described_class.new(multiple_of: 5).as_schema)
14
+ .to(eq({ type: 'integer', multipleOf: 5 }))
15
+ end
16
+ end
17
+
18
+ describe('.register_validations') do
19
+ let(:klass) do
20
+ Class.new do
21
+ include(ActiveModel::Validations)
22
+
23
+ attr_accessor(:foo)
24
+
25
+ def initialize(foo:)
26
+ @foo = foo
27
+ end
28
+ end
29
+ end
30
+
31
+ context('for integer-ness') do
32
+ before do
33
+ described_class
34
+ .new
35
+ .register_validations(:foo, klass)
36
+ end
37
+
38
+ it('fails the validation for floats') do
39
+ instance = klass.new(foo: 1.234)
40
+
41
+ expect(instance.valid?)
42
+ .to(be(false))
43
+ expect(instance.errors.map(&:type))
44
+ .to(eq(%i(not_an_integer)))
45
+ end
46
+
47
+ it('succeeds the validation for integers') do
48
+ instance = klass.new(foo: 1234)
49
+
50
+ expect(instance.valid?)
51
+ .to(be(true))
52
+ expect(instance.errors)
53
+ .to(be_empty)
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require('spec_helper')
4
+
5
+ RSpec.describe(JsonModel::TypeSpec::Primitive::Null) do
6
+ describe('#as_schema') do
7
+ it('returns a null schema') do
8
+ expect(subject.as_schema)
9
+ .to(eq({ type: 'null' }))
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require('spec_helper')
4
+
5
+ RSpec.describe(JsonModel::TypeSpec::Primitive::Number) do
6
+ describe('#as_schema') do
7
+ it('returns a number schema') do
8
+ expect(subject.as_schema)
9
+ .to(eq({ type: 'number' }))
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ require('spec_helper')
4
+
5
+ RSpec.describe(JsonModel::TypeSpec::Primitive::Numeric) do
6
+ describe('#as_schema') do
7
+ it('includes the multipleOf attribute') do
8
+ expect(described_class.new('number', multiple_of: 5).as_schema)
9
+ .to(eq({ type: 'number', multipleOf: 5 }))
10
+ end
11
+
12
+ it('includes the minimum attribute') do
13
+ expect(described_class.new('number', minimum: 5).as_schema)
14
+ .to(eq({ type: 'number', minimum: 5 }))
15
+ end
16
+
17
+ it('includes the maximum attribute') do
18
+ expect(described_class.new('number', maximum: 5).as_schema)
19
+ .to(eq({ type: 'number', maximum: 5 }))
20
+ end
21
+
22
+ it('includes the exclusiveMinimum attribute') do
23
+ expect(described_class.new('number', exclusive_minimum: 5).as_schema)
24
+ .to(eq({ type: 'number', exclusiveMinimum: 5 }))
25
+ end
26
+
27
+ it('includes the exclusiveMaximum attribute') do
28
+ expect(described_class.new('number', exclusive_maximum: 5).as_schema)
29
+ .to(eq({ type: 'number', exclusiveMaximum: 5 }))
30
+ end
31
+ end
32
+
33
+ describe('.register_validations') do
34
+ let(:klass) do
35
+ Class.new do
36
+ include(ActiveModel::Validations)
37
+
38
+ attr_accessor(:foo)
39
+
40
+ def initialize(foo:)
41
+ @foo = foo
42
+ end
43
+ end
44
+ end
45
+
46
+ context('for multiple of') do
47
+ before do
48
+ described_class
49
+ .new('number', multiple_of: 5)
50
+ .register_validations(:foo, klass)
51
+ end
52
+
53
+ it('fails the validation for non-multiples') do
54
+ instance = klass.new(foo: 1234)
55
+
56
+ expect(instance.valid?)
57
+ .to(be(false))
58
+ expect(instance.errors.map(&:type))
59
+ .to(eq(%i(multiple_of)))
60
+ end
61
+
62
+ it('succeeds the validation for integers') do
63
+ instance = klass.new(foo: 12_345)
64
+
65
+ expect(instance.valid?)
66
+ .to(be(true))
67
+ expect(instance.errors)
68
+ .to(be_empty)
69
+ end
70
+ end
71
+
72
+ context('for minimum') do
73
+ before do
74
+ described_class
75
+ .new('number', minimum: 5)
76
+ .register_validations(:foo, klass)
77
+ end
78
+
79
+ it('fails the validation for invalid values') do
80
+ instance = klass.new(foo: 4)
81
+
82
+ expect(instance.valid?)
83
+ .to(be(false))
84
+ expect(instance.errors.map(&:type))
85
+ .to(eq(%i(greater_than_or_equal_to)))
86
+ end
87
+
88
+ it('succeeds the validation for valid values') do
89
+ instance = klass.new(foo: 5)
90
+
91
+ expect(instance.valid?)
92
+ .to(be(true))
93
+ expect(instance.errors)
94
+ .to(be_empty)
95
+ end
96
+ end
97
+
98
+ context('for maximum') do
99
+ before do
100
+ described_class
101
+ .new('number', maximum: 5)
102
+ .register_validations(:foo, klass)
103
+ end
104
+
105
+ it('fails the validation for invalid values') do
106
+ instance = klass.new(foo: 6)
107
+
108
+ expect(instance.valid?)
109
+ .to(be(false))
110
+ expect(instance.errors.map(&:type))
111
+ .to(eq(%i(less_than_or_equal_to)))
112
+ end
113
+
114
+ it('succeeds the validation for valid values') do
115
+ instance = klass.new(foo: 5)
116
+
117
+ expect(instance.valid?)
118
+ .to(be(true))
119
+ expect(instance.errors)
120
+ .to(be_empty)
121
+ end
122
+ end
123
+
124
+ context('for exclusive_minimum') do
125
+ before do
126
+ described_class
127
+ .new('number', exclusive_minimum: 5)
128
+ .register_validations(:foo, klass)
129
+ end
130
+
131
+ it('fails the validation for invalid values') do
132
+ instance = klass.new(foo: 5)
133
+
134
+ expect(instance.valid?)
135
+ .to(be(false))
136
+ expect(instance.errors.map(&:type))
137
+ .to(eq(%i(greater_than)))
138
+ end
139
+
140
+ it('succeeds the validation for valid values') do
141
+ instance = klass.new(foo: 6)
142
+
143
+ expect(instance.valid?)
144
+ .to(be(true))
145
+ expect(instance.errors)
146
+ .to(be_empty)
147
+ end
148
+ end
149
+
150
+ context('for exclusive_maximum') do
151
+ before do
152
+ described_class
153
+ .new('number', exclusive_maximum: 5)
154
+ .register_validations(:foo, klass)
155
+ end
156
+
157
+ it('fails the validation for invalid values') do
158
+ instance = klass.new(foo: 5)
159
+
160
+ expect(instance.valid?)
161
+ .to(be(false))
162
+ expect(instance.errors.map(&:type))
163
+ .to(eq(%i(less_than)))
164
+ end
165
+
166
+ it('succeeds the validation for valid values') do
167
+ instance = klass.new(foo: 4)
168
+
169
+ expect(instance.valid?)
170
+ .to(be(true))
171
+ expect(instance.errors)
172
+ .to(be_empty)
173
+ end
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require('spec_helper')
4
+
5
+ RSpec.describe(JsonModel::TypeSpec::Primitive::String) do
6
+ describe('#as_schema') do
7
+ it('returns a string schema') do
8
+ expect(subject.as_schema)
9
+ .to(eq({ type: 'string' }))
10
+ end
11
+
12
+ it('includes the minLength attribute') do
13
+ expect(described_class.new(min_length: 10).as_schema)
14
+ .to(eq({ type: 'string', minLength: 10 }))
15
+ end
16
+
17
+ it('includes the maxLength attribute') do
18
+ expect(described_class.new(max_length: 10).as_schema)
19
+ .to(eq({ type: 'string', maxLength: 10 }))
20
+ end
21
+
22
+ it('includes the pattern attribute') do
23
+ expect(described_class.new(pattern: /\A\w+\z/).as_schema)
24
+ .to(eq({ type: 'string', pattern: '\\A\\w+\\z' }))
25
+ end
26
+ end
27
+
28
+ describe('.register_validations') do
29
+ let(:klass) do
30
+ Class.new do
31
+ include(ActiveModel::Validations)
32
+
33
+ attr_accessor(:foo)
34
+
35
+ def initialize(foo:)
36
+ @foo = foo
37
+ end
38
+ end
39
+ end
40
+
41
+ context('for min length') do
42
+ before do
43
+ described_class
44
+ .new(min_length: 3)
45
+ .register_validations(:foo, klass)
46
+ end
47
+
48
+ it('fails the validation for too short strings') do
49
+ instance = klass.new(foo: 'ba')
50
+
51
+ expect(instance.valid?)
52
+ .to(be(false))
53
+ expect(instance.errors.map(&:type))
54
+ .to(eq(%i(too_short)))
55
+ end
56
+
57
+ it('succeeds the validation for valid strings') do
58
+ instance = klass.new(foo: 'bar')
59
+
60
+ expect(instance.valid?)
61
+ .to(be(true))
62
+ expect(instance.errors)
63
+ .to(be_empty)
64
+ end
65
+ end
66
+
67
+ context('for max length') do
68
+ before do
69
+ described_class
70
+ .new(max_length: 2)
71
+ .register_validations(:foo, klass)
72
+ end
73
+
74
+ it('fails the validation for too short strings') do
75
+ instance = klass.new(foo: 'bar')
76
+
77
+ expect(instance.valid?)
78
+ .to(be(false))
79
+ expect(instance.errors.map(&:type))
80
+ .to(eq(%i(too_long)))
81
+ end
82
+
83
+ it('succeeds the validation for valid strings') do
84
+ instance = klass.new(foo: 'ba')
85
+
86
+ expect(instance.valid?)
87
+ .to(be(true))
88
+ expect(instance.errors)
89
+ .to(be_empty)
90
+ end
91
+ end
92
+
93
+ context('for a pattern') do
94
+ before do
95
+ described_class
96
+ .new(pattern: /\A\w+\z/)
97
+ .register_validations(:foo, klass)
98
+ end
99
+
100
+ it('fails the validation for an invalid value') do
101
+ instance = klass.new(foo: ' bar')
102
+
103
+ expect(instance.valid?)
104
+ .to(be(false))
105
+ expect(instance.errors.map(&:type))
106
+ .to(eq(%i(invalid)))
107
+ end
108
+
109
+ it('succeeds the validation for valid strings') do
110
+ instance = klass.new(foo: 'bar')
111
+
112
+ expect(instance.valid?)
113
+ .to(be(true))
114
+ expect(instance.errors)
115
+ .to(be_empty)
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require('spec_helper')
4
+
5
+ RSpec.describe(JsonModel::TypeSpec) do
6
+ describe('#resolve') do
7
+ shared_examples_for('primitive type') do |type:, expected_type:|
8
+ it("resolves to a primitive type spec for #{type}") do
9
+ expect(JsonModel::TypeSpec.resolve(type))
10
+ .to(be_a(expected_type))
11
+ end
12
+ end
13
+
14
+ shared_examples_for('unsupported type') do |type|
15
+ it("raises an error for #{type}") do
16
+ expect { JsonModel::TypeSpec.resolve(type) }
17
+ .to(raise_error(ArgumentError))
18
+ end
19
+ end
20
+
21
+ it_behaves_like('primitive type', type: String, expected_type: JsonModel::TypeSpec::Primitive::String)
22
+ it_behaves_like('primitive type', type: Integer, expected_type: JsonModel::TypeSpec::Primitive::Integer)
23
+ it_behaves_like('primitive type', type: Float, expected_type: JsonModel::TypeSpec::Primitive::Number)
24
+ it_behaves_like('primitive type', type: TrueClass, expected_type: JsonModel::TypeSpec::Primitive::Boolean)
25
+ it_behaves_like('primitive type', type: FalseClass, expected_type: JsonModel::TypeSpec::Primitive::Boolean)
26
+ it_behaves_like('primitive type', type: JsonModel::TypeSpec::Primitive::String.new, expected_type: JsonModel::TypeSpec::Primitive::String)
27
+
28
+ it_behaves_like('unsupported type', Class.new)
29
+ it_behaves_like('unsupported type', Object.new)
30
+ it_behaves_like('unsupported type', nil)
31
+ end
32
+ end