json_model_rb 0.1.14 → 0.1.16

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 55a34992353fa6800b488fba56a9516d78c0b1c256c987ea7f22cb61782477dc
4
- data.tar.gz: c3d0b4289612c86e3e7ba805b26209534099503947df12885591decb2942a647
3
+ metadata.gz: f5c3f4ee507240e543a97f2ef35f1ec1bc998530547c4f841164c86baaf06c88
4
+ data.tar.gz: 456f3afae37e3a2f5d040f20dabc7c86a3ae534c60ca40fefae2d6b24155e200
5
5
  SHA512:
6
- metadata.gz: 128cceae1c36a9b9f130013417d2e0131a8b3b673b6cedde600297bb1d1d327fa7cfb91ebecf974af72f05e6efe96849ee2477f3e734b2b5e90ee6b0f2655c09
7
- data.tar.gz: bb21f9d7ff5ccedcd7448f148d69ce78502b7f7200d0e6cb90edf5b54d77afae0284a3729c9d4dcba9fbac82253b3a95d957bd1aef56bc8a641453f0971a5283
6
+ metadata.gz: 6404570b5ae3113f6f067da85b595fe066f866f226a87b381909593da31046f427ed59ccb08f7174986106b58dc47487bca9eeee1b7b4eb1312bb7f6102b6712
7
+ data.tar.gz: 65a200948801a4238e3780ab24953b7e36a1404c1a4012a21af6e117b2cf24b2609aeb87eaaa36f8fa7d0d8917c9a97e9843dcb3e5976689c99152bc7e855ef1
data/README.md CHANGED
@@ -99,7 +99,7 @@ class Product
99
99
  # Properties
100
100
  property :id, type: String
101
101
  property :name, type: String
102
- property :price, type: Float, minimum: 0
102
+ property :price, type: T::Float[minimum: 0]
103
103
  property :available, type: T::Boolean, default: true, optional: true
104
104
  end
105
105
  ```
@@ -124,30 +124,28 @@ class StringExample
124
124
  property :simple_string, type: String
125
125
 
126
126
  # String with length constraints
127
- property :username, type: String,
128
- min_length: 3,
129
- max_length: 20
127
+ property :username, type: T::String[min_length: 3, max_length: 20]
130
128
 
131
129
  # String with pattern (regex)
132
- property :product_code, type: String, pattern: /\A[A-Z]{3}-\d{4}\z/
130
+ property :product_code, type: T::String[pattern: /\A[A-Z]{3}-\d{4}\z/]
133
131
 
134
132
  # String with format
135
- property :email, type: String, format: :email
136
- property :uri, type: String, format: :uri
137
- property :hostname, type: String, format: :hostname
138
- property :ipv4, type: String, format: :ipv4
139
- property :ipv6, type: String, format: :ipv6
140
- property :uuid, type: String, format: :uuid
141
- property :date, type: String, format: :date
142
- property :time, type: String, format: :time
143
- property :datetime, type: String, format: :date_time
144
- property :duration, type: String, format: :duration
133
+ property :email, type: T::String[format: :email]
134
+ property :uri, type: T::String[format: :uri]
135
+ property :hostname, type: T::String[format: :hostname]
136
+ property :ipv4, type: T::String[format: :ipv4]
137
+ property :ipv6, type: T::String[format: :ipv6]
138
+ property :uuid, type: T::String[format: :uuid]
139
+ property :date, type: T::String[format: :date]
140
+ property :time, type: T::String[format: :time]
141
+ property :datetime, type: T::String[format: :date_time]
142
+ property :duration, type: T::String[format: :duration]
145
143
 
146
144
  # String with enum
147
- property :status, type: String, enum: ["draft", "published", "archived"]
145
+ property :status, T::Enum["draft", "published", "archived"]
148
146
 
149
147
  # String with const
150
- property :api_version, type: String, const: "v1"
148
+ property :api_version, T::Const["v1"]
151
149
 
152
150
  # Optional string
153
151
  property :nickname, type: String, optional: true
@@ -244,19 +242,19 @@ class NumericExample
244
242
  property :count, type: Integer
245
243
 
246
244
  # Integer with range
247
- property :port, type: Integer, minimum: 1024, maximum: 65535
245
+ property :port, type: T::Integer[minimum: 1024, maximum: 65535]
248
246
 
249
247
  # Integer with exclusive bounds
250
- property :positive_int, type: Integer, exclusive_minimum: 0
248
+ property :positive_int, type: T::Integer[exclusive_minimum: 0]
251
249
 
252
250
  # Number (float/double)
253
- property :price, type: Float, minimum: 0
251
+ property :price, type: T::Number[minimum: 0]
254
252
 
255
253
  # Number with multiple_of
256
- property :quantity, type: Integer, multiple_of: 10
254
+ property :quantity, type: T::Integer[multiple_of: 10]
257
255
 
258
256
  # Number with precision
259
- property :temperature, type: Float, minimum: -273.15, maximum: 1000.0
257
+ property :temperature, type: T::Number[minimum: -273.15, maximum: 1000.0]
260
258
 
261
259
  # Optional number
262
260
  property :discount, type: Float, optional: true
@@ -361,7 +359,7 @@ class ArrayExample
361
359
  property :tags, type: T::Array[String]
362
360
 
363
361
  # Array with constraints
364
- property :numbers, type: T::Array[Integer], min_items: 1, max_items: 10, unique_items: true
362
+ property :numbers, type: T::Array[Integer, min_items: 1, max_items: 10, unique_items: true]
365
363
  end
366
364
 
367
365
  # Generate the JSON Schema
@@ -410,15 +408,15 @@ class PersonBase
410
408
  include JsonModel::Schema
411
409
 
412
410
  property :name, type: String
413
- property :age, type: Integer, minimum: 0, optional: true
411
+ property :age, type: T::Integer[minimum: 0], optional: true
414
412
  end
415
413
 
416
414
  class EmployeeDetails
417
415
  include JsonModel::Schema
418
416
 
419
- property :employee_id, type: String, pattern: /\AE-\d{4}\z/
417
+ property :employee_id, type: T::String[pattern: /\AE-\d{4}\z/]
420
418
  property :department, type: String
421
- property :salary, type: Float, minimum: 0, optional: true
419
+ property :salary, type: T::Number[minimum: 0], optional: true
422
420
  end
423
421
 
424
422
  class Employee
@@ -504,7 +502,7 @@ end
504
502
  class PhoneContact
505
503
  include JsonModel::Schema
506
504
 
507
- property :phone, type: String, pattern: /\A\+?[1-9]\\d{1,14}\z/
505
+ property :phone, type: T::String[pattern: /\A\+?[1-9]\\d{1,14}\z/]
508
506
  end
509
507
 
510
508
  class AddressContact
@@ -596,24 +594,24 @@ Use `T::OneOf` when a value must validate against exactly one schema (exclusive
596
594
  class CreditCardPayment
597
595
  include JsonModel::Schema
598
596
 
599
- property :payment_type, type: String, const: "credit_card"
600
- property :card_number, type: String, pattern: /\A\d{16}\z/
601
- property :cvv, type: String, pattern: /\A\d{3,4}\z/
602
- property :expiry, type: String, pattern: /\A\d{2}\/\d{2}\z/
597
+ property :payment_type, T::Const["credit_card"]
598
+ property :card_number, type: T::String[pattern: /\A\d{16}\z/]
599
+ property :cvv, type: T::String[pattern: /\A\d{3,4}\z/]
600
+ property :expiry, type: T::String[pattern: /\A\d{2}\/\d{2}\z/]
603
601
  end
604
602
 
605
603
  class PayPalPayment
606
604
  include JsonModel::Schema
607
605
 
608
- property :payment_type, type: String, const: "paypal"
609
- property :paypal_email, type: String, format: :email
606
+ property :payment_type, T::Const["paypal"]
607
+ property :paypal_email, type: T::String[format: :email]
610
608
  end
611
609
 
612
610
  class BankTransferPayment
613
611
  include JsonModel::Schema
614
612
 
615
- property :payment_type, type: String, const: "bank_transfer"
616
- property :iban, type: String, pattern: "^[A-Z]{2}\\d{2}[A-Z0-9]+$"
613
+ property :payment_type, type: T::Const["bank_transfer"]
614
+ property :iban, type: T::String[pattern: "^[A-Z]{2}\\d{2}[A-Z0-9]+$"]
617
615
  property :swift, type: String, optional: true
618
616
  end
619
617
 
@@ -39,5 +39,7 @@ module JsonModel
39
39
  end
40
40
 
41
41
  option(:validate_after_instantiation, default: true)
42
+
43
+ option(:schema_version)
42
44
  end
43
45
  end
@@ -37,10 +37,9 @@ module JsonModel
37
37
  # @param [Object, Class] type
38
38
  # @param [Hash] options
39
39
  def property(name, type:, **options)
40
- property_options = options.slice(:default, :optional, :ref_mode, :as)
41
- resolved_type = TypeSpec.resolve(type, **options.except(:default, :optional, :ref_mode, :as))
42
- add_property(name, type: resolved_type, **property_options)
43
- descendants.each { |subclass| subclass.add_property(name, type: resolved_type, **property_options) }
40
+ resolved_type = TypeSpec.resolve(type)
41
+ add_property(name, type: resolved_type, **options)
42
+ descendants.each { |subclass| subclass.add_property(name, type: resolved_type, **options) }
44
43
  end
45
44
 
46
45
  protected
@@ -17,12 +17,11 @@ module JsonModel
17
17
  def self.inherited(subclass)
18
18
  super
19
19
  subclass.schema_id(JsonModel.config.schema_id_naming_strategy.call(subclass))
20
- subclass.additional_properties(false)
21
20
  subclass.meta_attributes[:$ref] = schema_id
22
21
  end
23
22
 
24
23
  schema_id(JsonModel.config.schema_id_naming_strategy.call(self))
25
- additional_properties(false)
24
+ schema_version(JsonModel.config.schema_version)
26
25
  end
27
26
 
28
27
  class_methods do
@@ -71,6 +70,16 @@ module JsonModel
71
70
  end
72
71
  end
73
72
 
73
+ # @param [Boolean, nil] value
74
+ # @return [Boolean]
75
+ def unevaluated_properties(value = nil)
76
+ if value.nil?
77
+ meta_attributes[:unevaluatedProperties] || false
78
+ else
79
+ meta_attributes[:unevaluatedProperties] = value
80
+ end
81
+ end
82
+
74
83
  # @param [Symbol, nil] version
75
84
  # @return [Boolean]
76
85
  def schema_version(version = nil)
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JsonModel
4
+ class TypeSpec
5
+ class Castable < TypeSpec
6
+ # @param [String] format
7
+ # @param [Proc] cast_block
8
+ def initialize(format:, &cast_block)
9
+ super()
10
+ @format = format
11
+ @cast_block = cast_block
12
+ end
13
+
14
+ # @param [Hash] _options
15
+ # @return [Hash]
16
+ def as_schema(**_options)
17
+ {
18
+ type: 'string',
19
+ format: @format,
20
+ }
21
+ end
22
+
23
+ # @param [::Object] json
24
+ # @return [::Object, nil]
25
+ def cast(json)
26
+ if json.nil?
27
+ nil
28
+ else
29
+ @cast_block.call(json)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -62,7 +62,7 @@ module JsonModel
62
62
  end
63
63
  },
64
64
  uuid: lambda { |v|
65
- SecureRandom(v.match?(/\A[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\z/i))
65
+ v.match?(/\A[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\z/i)
66
66
  },
67
67
  regex: lambda { |v|
68
68
  begin
@@ -79,7 +79,7 @@ module JsonModel
79
79
  # @param [Integer, nil] min_length
80
80
  # @param [Integer, nil] max_length
81
81
  # @param [Regexp, nil] pattern
82
- # @param [String, nil] format
82
+ # @param [Symbol, nil] format
83
83
  def initialize(min_length: nil, max_length: nil, pattern: nil, format: nil)
84
84
  super(types: [::String], schema_type: 'string')
85
85
 
@@ -115,7 +115,6 @@ module JsonModel
115
115
  end
116
116
  if @format
117
117
  if JSON_SCHEMA_FORMATS.key?(@format)
118
-
119
118
  register_format_validation(klass, name)
120
119
  else
121
120
  raise(ArgumentError, "Invalid format: #{@format}")
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative('type_spec/array')
4
+ require_relative('type_spec/castable')
4
5
  require_relative('type_spec/composition')
5
6
  require_relative('type_spec/const')
6
7
  require_relative('type_spec/enum')
@@ -9,6 +10,20 @@ require_relative('type_spec/primitive')
9
10
 
10
11
  module JsonModel
11
12
  class TypeSpec
13
+ TYPE_MAP = {
14
+ Date => Castable.new(format: 'date') { |v| ::DateTime.iso8601(v) },
15
+ DateTime => Castable.new(format: 'date-time') { |v| ::DateTime.iso8601(v) },
16
+ FalseClass => Primitive::Boolean.new,
17
+ Float => Primitive::Number.new,
18
+ Integer => Primitive::Integer.new,
19
+ NilClass => Primitive::Null.new,
20
+ Regexp => Castable.new(format: 'regex') { |v| Regexp.new(v) },
21
+ String => Primitive::String.new,
22
+ Time => Castable.new(format: 'time') { |v| ::Time.iso8601(v) },
23
+ TrueClass => Primitive::Boolean.new,
24
+ URI => Castable.new(format: 'uri') { |v| URI.parse(v) },
25
+ }.freeze
26
+
12
27
  # @param [Hash] options
13
28
  # @return [Hash]
14
29
  def as_schema(**options)
@@ -32,39 +47,31 @@ module JsonModel
32
47
 
33
48
  class << self
34
49
  # @param [Object, Class] type
35
- # @param [Hash] options
36
50
  # @return [TypeSpec]
37
- def resolve(type, **options)
51
+ def resolve(type)
38
52
  case type
39
53
  when TypeSpec
40
54
  type
41
55
  when Class
42
- resolve_type_from_class(type, **options)
43
- when T::AllOf, T::AnyOf, T::Boolean, T::OneOf, T::Array, T::Enum, T::Const
44
- type.to_type_spec(**options)
56
+ resolve_type_from_class(type)
45
57
  else
46
- raise(ArgumentError, "Unsupported type: #{type}")
58
+ if type.respond_to?(:to_type_spec)
59
+ type.to_type_spec
60
+ else
61
+ raise(ArgumentError, "Unsupported type: #{type}")
62
+ end
47
63
  end
48
64
  end
49
65
 
50
66
  private
51
67
 
52
68
  # @param [Object, Class] type
53
- # @param [Hash] options
54
69
  # @return [TypeSpec]
55
- def resolve_type_from_class(type, **options)
56
- if type == String
57
- Primitive::String.new(**options)
58
- elsif type == Integer
59
- Primitive::Integer.new(**options)
60
- elsif type == Float
61
- Primitive::Number.new(**options)
62
- elsif [TrueClass, FalseClass].include?(type)
63
- Primitive::Boolean.new(**options)
64
- elsif type == NilClass
65
- Primitive::Null.new(**options)
70
+ def resolve_type_from_class(type)
71
+ if TYPE_MAP.key?(type)
72
+ TYPE_MAP[type]
66
73
  elsif type < Schema
67
- TypeSpec::Object.new(type, **options)
74
+ TypeSpec::Object.new(type)
68
75
  else
69
76
  raise(ArgumentError, "Unsupported type: #{type}")
70
77
  end
@@ -3,23 +3,26 @@
3
3
  module T
4
4
  class AllOf
5
5
  # @param [Array<Class>] types
6
- def initialize(*types)
6
+ # @param [Hash] options
7
+ def initialize(*types, **options)
7
8
  @types = types
9
+ @options = options
8
10
  end
9
11
 
10
12
  # @return [JsonModel::TypeSpec::Composition::AllOf]
11
- def to_type_spec(**options)
13
+ def to_type_spec
12
14
  JsonModel::TypeSpec::Composition::AllOf.new(
13
15
  *@types.map { |type| JsonModel::TypeSpec.resolve(type) },
14
- **options,
16
+ **@options,
15
17
  )
16
18
  end
17
19
 
18
20
  class << self
19
21
  # @param [Array] types
22
+ # @param [Hash] options
20
23
  # @return [AllOf]
21
- def [](*types)
22
- AllOf.new(*types)
24
+ def [](*types, **options)
25
+ AllOf.new(*types, **options)
23
26
  end
24
27
  end
25
28
  end
@@ -3,23 +3,26 @@
3
3
  module T
4
4
  class AnyOf
5
5
  # @param [Array<Class>] types
6
- def initialize(*types)
6
+ # @param [Hash] options
7
+ def initialize(*types, **options)
7
8
  @types = types
9
+ @options = options
8
10
  end
9
11
 
10
12
  # @return [JsonModel::TypeSpec::Composition::AnyOf]
11
- def to_type_spec(**options)
13
+ def to_type_spec
12
14
  JsonModel::TypeSpec::Composition::AnyOf.new(
13
15
  *@types.map { |type| JsonModel::TypeSpec.resolve(type) },
14
- **options,
16
+ **@options,
15
17
  )
16
18
  end
17
19
 
18
20
  class << self
19
21
  # @param [Array] types
22
+ # @param [Hash] options
20
23
  # @return [AnyOf]
21
- def [](*types)
22
- AnyOf.new(*types)
24
+ def [](*types, **options)
25
+ AnyOf.new(*types, **options)
23
26
  end
24
27
  end
25
28
  end
@@ -3,23 +3,26 @@
3
3
  module T
4
4
  class Array
5
5
  # @param [Class] type
6
- def initialize(type)
6
+ # @param [Hash] options
7
+ def initialize(type, **options)
7
8
  @type = type
9
+ @options = options
8
10
  end
9
11
 
10
12
  # @return [JsonModel::TypeSpec::Array]
11
- def to_type_spec(**options)
13
+ def to_type_spec
12
14
  JsonModel::TypeSpec::Array.new(
13
15
  JsonModel::TypeSpec.resolve(@type),
14
- **options,
16
+ **@options,
15
17
  )
16
18
  end
17
19
 
18
20
  class << self
19
21
  # @param [Class] type
22
+ # @param [Hash] options
20
23
  # @return [Array]
21
- def [](type)
22
- Array.new(type)
24
+ def [](type, **options)
25
+ Array.new(type, **options)
23
26
  end
24
27
  end
25
28
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module T
4
4
  Boolean = Class.new do
5
- def to_type_spec(**_options) = JsonModel::TypeSpec::Primitive::Boolean.new
5
+ def to_type_spec = JsonModel::TypeSpec::Primitive::Boolean.new
6
6
  end.new
7
7
  end
@@ -8,8 +8,8 @@ module T
8
8
  end
9
9
 
10
10
  # @return [JsonModel::TypeSpec::Const]
11
- def to_type_spec(**options)
12
- JsonModel::TypeSpec::Const.new(*@value, **options)
11
+ def to_type_spec
12
+ JsonModel::TypeSpec::Const.new(*@value)
13
13
  end
14
14
 
15
15
  class << self
@@ -8,8 +8,8 @@ module T
8
8
  end
9
9
 
10
10
  # @return [JsonModel::TypeSpec::Enum]
11
- def to_type_spec(**options)
12
- JsonModel::TypeSpec::Enum.new(*@values, **options)
11
+ def to_type_spec
12
+ JsonModel::TypeSpec::Enum.new(*@values)
13
13
  end
14
14
 
15
15
  class << self
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module T
4
+ class Integer
5
+ # @param [Hash] options
6
+ def initialize(**options)
7
+ @options = options
8
+ end
9
+
10
+ # @return [JsonModel::TypeSpec::Composition::Primitive::Integer]
11
+ def to_type_spec
12
+ JsonModel::TypeSpec::Primitive::Integer.new(**@options)
13
+ end
14
+
15
+ class << self
16
+ # @param [Hash] options
17
+ # @return [T::Integer]
18
+ def [](**options)
19
+ Integer.new(**options)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module T
4
+ Null = Class.new do
5
+ def to_type_spec = JsonModel::TypeSpec::Primitive::Null.new
6
+ end.new
7
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module T
4
+ class Number
5
+ # @param [Hash] options
6
+ def initialize(**options)
7
+ @options = options
8
+ end
9
+
10
+ # @return [JsonModel::TypeSpec::Composition::Primitive::Number]
11
+ def to_type_spec
12
+ JsonModel::TypeSpec::Primitive::Number.new(**@options)
13
+ end
14
+
15
+ class << self
16
+ # @param [Hash] options
17
+ # @return [Number]
18
+ def [](**options)
19
+ Number.new(**options)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -3,23 +3,26 @@
3
3
  module T
4
4
  class OneOf
5
5
  # @param [Array<Class>] types
6
- def initialize(*types)
6
+ # @param [Hash] options
7
+ def initialize(*types, **options)
7
8
  @types = types
9
+ @options = options
8
10
  end
9
11
 
10
12
  # @return [JsonModel::TypeSpec::Composition::OneOf]
11
- def to_type_spec(**options)
13
+ def to_type_spec
12
14
  JsonModel::TypeSpec::Composition::OneOf.new(
13
15
  *@types.map { |type| JsonModel::TypeSpec.resolve(type) },
14
- **options,
16
+ **@options,
15
17
  )
16
18
  end
17
19
 
18
20
  class << self
19
21
  # @param [Array] types
22
+ # @param [Hash] options
20
23
  # @return [OneOf]
21
- def [](*types)
22
- OneOf.new(*types)
24
+ def [](*types, **options)
25
+ OneOf.new(*types, **options)
23
26
  end
24
27
  end
25
28
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module T
4
+ class String
5
+ # @param [Hash] options
6
+ def initialize(**options)
7
+ @options = options
8
+ end
9
+
10
+ # @return [JsonModel::TypeSpec::Composition::Primitive::String]
11
+ def to_type_spec
12
+ JsonModel::TypeSpec::Primitive::String.new(**@options)
13
+ end
14
+
15
+ class << self
16
+ # @param [Hash] options
17
+ # @return [T::String]
18
+ def [](**options)
19
+ String.new(**options)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -2,8 +2,12 @@
2
2
 
3
3
  require_relative('types/all_of')
4
4
  require_relative('types/any_of')
5
- require_relative('types/boolean')
6
- require_relative('types/one_of')
7
5
  require_relative('types/array')
6
+ require_relative('types/boolean')
8
7
  require_relative('types/const')
9
8
  require_relative('types/enum')
9
+ require_relative('types/integer')
10
+ require_relative('types/null')
11
+ require_relative('types/number')
12
+ require_relative('types/one_of')
13
+ require_relative('types/string')
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JsonModel
4
- VERSION = '0.1.14'
4
+ VERSION = '0.1.16'
5
5
  end
@@ -10,7 +10,7 @@ RSpec.describe('File system schema') do
10
10
  include(JsonModel::Schema)
11
11
 
12
12
  property(:type, type: T::Const['disk'])
13
- property(:device, type: String, pattern: %r{\A/dev/[^/]+(/[^/]+)*\z})
13
+ property(:device, type: T::String[pattern: %r{\A/dev/[^/]+(/[^/]+)*\z}])
14
14
  end,
15
15
  )
16
16
 
@@ -22,8 +22,7 @@ RSpec.describe('File system schema') do
22
22
  property(:type, type: T::Enum['diskUUID', 'diskuuid'])
23
23
  property(
24
24
  :label,
25
- type: String,
26
- pattern: /\A[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}\z/,
25
+ type: T::String[pattern: /\A[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}\z/],
27
26
  )
28
27
  end,
29
28
  )
@@ -34,8 +33,8 @@ RSpec.describe('File system schema') do
34
33
  include(JsonModel::Schema)
35
34
 
36
35
  property(:type, type: T::Const['nfs'])
37
- property(:remote_path, type: String, pattern: %r{\A(/[^/]+)+\z}, as: :remotePath)
38
- property(:server, type: String, format: :ipv4)
36
+ property(:remote_path, type: T::String[pattern: %r{\A(/[^/]+)+\z}], as: :remotePath)
37
+ property(:server, type: T::String[format: :ipv4])
39
38
  end,
40
39
  )
41
40
 
@@ -45,7 +44,7 @@ RSpec.describe('File system schema') do
45
44
  include(JsonModel::Schema)
46
45
 
47
46
  property(:type, type: T::Const['tmpfs'])
48
- property(:size_in_mb, type: Integer, minimum: 16, maximum: 512, as: :sizeInMB)
47
+ property(:size_in_mb, type: T::Integer[minimum: 16, maximum: 512], as: :sizeInMB)
49
48
  end,
50
49
  )
51
50
 
@@ -57,12 +56,11 @@ RSpec.describe('File system schema') do
57
56
  description('JSON Schema for an fstab entry')
58
57
  property(
59
58
  :storage,
60
- type: T::OneOf[DiskDevice, DiskUuid, Nfs, Tmpfs],
59
+ type: T::OneOf[DiskDevice, DiskUuid, Nfs, Tmpfs, discriminator: :type],
61
60
  ref_mode: JsonModel::RefMode::LOCAL,
62
- discriminator: :type,
63
61
  )
64
62
  property(:fstype, type: T::Enum['ext3', 'ext4', 'btrfs'], optional: true)
65
- property(:options, type: T::Array[String], min_items: 1, unique_items: true, optional: true)
63
+ property(:options, type: T::Array[String, min_items: 1, unique_items: true], optional: true)
66
64
  property(:readonly, type: T::Boolean, optional: true)
67
65
  end,
68
66
  )
@@ -100,7 +98,6 @@ RSpec.describe('File system schema') do
100
98
  type: 'boolean',
101
99
  },
102
100
  },
103
- additionalProperties: false,
104
101
  '$defs': {
105
102
  DiskDevice: {
106
103
  properties: {
@@ -113,7 +110,6 @@ RSpec.describe('File system schema') do
113
110
  },
114
111
  },
115
112
  required: %i(device type),
116
- additionalProperties: false,
117
113
  type: 'object',
118
114
  },
119
115
  DiskUuid: {
@@ -125,7 +121,6 @@ RSpec.describe('File system schema') do
125
121
  },
126
122
  },
127
123
  required: %i(label type),
128
- additionalProperties: false,
129
124
  type: 'object',
130
125
  },
131
126
  Nfs: {
@@ -141,7 +136,6 @@ RSpec.describe('File system schema') do
141
136
  },
142
137
  },
143
138
  required: %i(remotePath server type),
144
- additionalProperties: false,
145
139
  type: 'object',
146
140
  },
147
141
  Tmpfs: {
@@ -150,7 +144,6 @@ RSpec.describe('File system schema') do
150
144
  sizeInMB: { type: 'integer', minimum: 16, maximum: 512 },
151
145
  },
152
146
  required: %i(sizeInMB type),
153
- additionalProperties: false,
154
147
  type: 'object',
155
148
  },
156
149
  },
@@ -14,7 +14,7 @@ RSpec.describe('User schema') do
14
14
  property(:street, type: String)
15
15
  property(:city, type: String)
16
16
  property(:state, type: String, optional: true)
17
- property(:postal_code, type: String, pattern: /\A\d{5}(-\d{4})?\z/, optional: true)
17
+ property(:postal_code, type: T::String[pattern: /\A\d{5}(-\d{4})?\z/], optional: true)
18
18
  property(:country, type: String, default: 'USA')
19
19
  end
20
20
 
@@ -28,11 +28,12 @@ RSpec.describe('User schema') do
28
28
  end
29
29
 
30
30
  property(:name, type: String)
31
- property(:email, type: String, format: :email)
32
- property(:age, type: Integer, minimum: 0, maximum: 120, optional: true)
31
+ property(:email, type: T::String[format: :email])
32
+ property(:age, type: T::Integer[minimum: 0, maximum: 120], optional: true)
33
33
  property(:active, type: T::Boolean, default: true, optional: true)
34
34
  property(:addresses, type: T::Array[Address], ref_mode: JsonModel::RefMode::LOCAL)
35
35
  property(:tags, type: T::Array[String], optional: true)
36
+ property(:birthday, type: Date, optional: true)
36
37
  end
37
38
 
38
39
  stub_const('User', user_class)
@@ -54,8 +55,8 @@ RSpec.describe('User schema') do
54
55
  items: { '$ref': '#/$defs/Address' },
55
56
  },
56
57
  tags: { type: 'array', items: { type: 'string' } },
58
+ birthday: { type: 'string', format: 'date' },
57
59
  },
58
- additionalProperties: false,
59
60
  required: %i(addresses email name),
60
61
  '$defs': {
61
62
  Address: {
@@ -68,7 +69,6 @@ RSpec.describe('User schema') do
68
69
  street: { type: 'string' },
69
70
  },
70
71
  required: %i(city country street),
71
- additionalProperties: false,
72
72
  },
73
73
  },
74
74
  },
@@ -77,7 +77,12 @@ RSpec.describe('User schema') do
77
77
  end
78
78
 
79
79
  it('can instantiate a model') do
80
- user = User.new(name: 'Foo', email: 'foo@example.com', addresses: [{ street: '123 Main St', city: 'Anytown' }])
80
+ user = User.new(
81
+ name: 'Foo',
82
+ email: 'foo@example.com',
83
+ addresses: [{ street: '123 Main St', city: 'Anytown' }],
84
+ birthday: '2000-01-01',
85
+ )
81
86
 
82
87
  expect(user.name).to(eq('Foo'))
83
88
  expect(user.email).to(eq('foo@example.com'))
@@ -88,5 +93,6 @@ RSpec.describe('User schema') do
88
93
  expect(user.addresses.first.postal_code).to(be_nil)
89
94
  expect(user.addresses.first.state).to(be_nil)
90
95
  expect(user.addresses.first.city).to(eq('Anytown'))
96
+ expect(user.birthday).to(eq(Date.new(2000, 1, 1)))
91
97
  end
92
98
  end
@@ -103,9 +103,9 @@ RSpec.describe(JsonModel::SchemaMeta) do
103
103
  end
104
104
  end
105
105
 
106
- it('is false by default') do
106
+ it('is missing by default') do
107
107
  expect(klass.meta_attributes)
108
- .to(eq({ additionalProperties: false }))
108
+ .to(eq({}))
109
109
  end
110
110
 
111
111
  it('can be changed') do
@@ -115,4 +115,24 @@ RSpec.describe(JsonModel::SchemaMeta) do
115
115
  .to(eq({ additionalProperties: true }))
116
116
  end
117
117
  end
118
+
119
+ describe('.additional_properties') do
120
+ let(:klass) do
121
+ Class.new do
122
+ include(JsonModel::SchemaMeta)
123
+ end
124
+ end
125
+
126
+ it('is missing by default') do
127
+ expect(klass.meta_attributes)
128
+ .to(eq({}))
129
+ end
130
+
131
+ it('can be changed') do
132
+ klass.unevaluated_properties(true)
133
+
134
+ expect(klass.meta_attributes)
135
+ .to(eq({ unevaluatedProperties: true }))
136
+ end
137
+ end
118
138
  end
data/spec/schema_spec.rb CHANGED
@@ -44,7 +44,7 @@ RSpec.describe(JsonModel::Schema) do
44
44
  Class.new do
45
45
  include(JsonModel::Schema)
46
46
 
47
- property(:foo, type: String, min_length: 3)
47
+ property(:foo, type: T::String[min_length: 3])
48
48
  end
49
49
  end
50
50
 
@@ -100,7 +100,7 @@ RSpec.describe(JsonModel::Schema) do
100
100
 
101
101
  it('returns an empty schema') do
102
102
  expect(klass.as_schema)
103
- .to(eq({ type: 'object', additionalProperties: false }))
103
+ .to(eq({ type: 'object' }))
104
104
  end
105
105
 
106
106
  it('includes the schema id') do
@@ -112,7 +112,6 @@ RSpec.describe(JsonModel::Schema) do
112
112
  {
113
113
  '$id': 'https://example.com/schemas/example.json',
114
114
  type: 'object',
115
- additionalProperties: false,
116
115
  },
117
116
  ),
118
117
  )
@@ -145,7 +144,6 @@ RSpec.describe(JsonModel::Schema) do
145
144
  foo: { type: 'string' },
146
145
  },
147
146
  required: %i(bam baz foo),
148
- additionalProperties: false,
149
147
  },
150
148
  ),
151
149
  )
@@ -206,16 +204,13 @@ RSpec.describe(JsonModel::Schema) do
206
204
  type: 'object',
207
205
  properties: { foo: { type: 'string' } },
208
206
  required: %i(foo),
209
- additionalProperties: false,
210
207
  },
211
208
  },
212
- additionalProperties: false,
213
209
  '$defs': {
214
210
  Bar: {
215
211
  type: 'object',
216
212
  properties: { bar: { type: 'string' } },
217
213
  required: %i(bar),
218
- additionalProperties: false,
219
214
  },
220
215
  },
221
216
  required: %i(bam bar foo),
@@ -250,7 +245,6 @@ RSpec.describe(JsonModel::Schema) do
250
245
  '$id': 'https://example.com/schemas/child.json',
251
246
  '$ref': 'https://example.com/schemas/example.json',
252
247
  type: 'object',
253
- additionalProperties: false,
254
248
  properties: { baz: { type: 'string' } },
255
249
  required: %i(baz),
256
250
  },
@@ -265,7 +259,6 @@ RSpec.describe(JsonModel::Schema) do
265
259
  '$ref': 'https://example.com/schemas/example.json',
266
260
  title: 'SecondChild',
267
261
  type: 'object',
268
- additionalProperties: false,
269
262
  properties: { bar: { type: 'string' } },
270
263
  required: %i(bar),
271
264
  },
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require('spec_helper')
4
+
5
+ RSpec.describe(JsonModel::TypeSpec::Castable) do
6
+ describe('#as_schema') do
7
+ it('returns a string schema') do
8
+ expect(described_class.new(format: 'date') { |v| v }.as_schema)
9
+ .to(eq({ type: 'string', format: 'date' }))
10
+ end
11
+ end
12
+
13
+ describe('#cast') do
14
+ it('casts a string') do
15
+ expect(described_class.new(format: 'date') { |v| Date.parse(v) }.cast('2020-01-01'))
16
+ .to(eq(Date.new(2020, 1, 1)))
17
+ end
18
+ end
19
+ end
@@ -27,7 +27,6 @@ RSpec.describe(JsonModel::TypeSpec::Composition) do
27
27
  type: 'object',
28
28
  properties: { foo: { type: 'string' } },
29
29
  required: %i(foo),
30
- additionalProperties: false,
31
30
  },
32
31
  ],
33
32
  },
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: json_model_rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.14
4
+ version: 0.1.16
5
5
  platform: ruby
6
6
  authors:
7
7
  - Paul Gillesberger
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-12-30 00:00:00.000000000 Z
10
+ date: 2026-01-05 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: rake
@@ -117,6 +117,7 @@ files:
117
117
  - lib/json_model/schema_meta.rb
118
118
  - lib/json_model/type_spec.rb
119
119
  - lib/json_model/type_spec/array.rb
120
+ - lib/json_model/type_spec/castable.rb
120
121
  - lib/json_model/type_spec/composition.rb
121
122
  - lib/json_model/type_spec/composition/all_of.rb
122
123
  - lib/json_model/type_spec/composition/any_of.rb
@@ -138,10 +139,13 @@ files:
138
139
  - lib/json_model/types/boolean.rb
139
140
  - lib/json_model/types/const.rb
140
141
  - lib/json_model/types/enum.rb
142
+ - lib/json_model/types/integer.rb
143
+ - lib/json_model/types/null.rb
144
+ - lib/json_model/types/number.rb
141
145
  - lib/json_model/types/one_of.rb
146
+ - lib/json_model/types/string.rb
142
147
  - lib/json_model/version.rb
143
148
  - spec/config_spec.rb
144
- - spec/examples/component_spec.rb
145
149
  - spec/examples/file_system_spec.rb
146
150
  - spec/examples/user_spec.rb
147
151
  - spec/json_model_spec.rb
@@ -151,6 +155,7 @@ files:
151
155
  - spec/schema_spec.rb
152
156
  - spec/spec_helper.rb
153
157
  - spec/type_spec/array_spec.rb
158
+ - spec/type_spec/castable_spec.rb
154
159
  - spec/type_spec/composition/all_of_spec.rb
155
160
  - spec/type_spec/composition/any_of_spec.rb
156
161
  - spec/type_spec/composition/one_of_spec.rb
@@ -1,57 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require('spec_helper')
4
-
5
- RSpec.describe('User schema') do
6
- it('todo') do
7
- stub_const(
8
- 'BaseComponent',
9
- Class.new do
10
- include(JsonModel::Schema)
11
-
12
- property(:type, type: String)
13
- property(:styles, type: T::Array[String], optional: true)
14
- end,
15
- )
16
- stub_const(
17
- 'TextComponent',
18
- Class.new(BaseComponent) do
19
- include(JsonModel::Schema)
20
-
21
- property(:type, type: T::Enum['text'])
22
- property(:value, type: String)
23
- end,
24
- )
25
- stub_const(
26
- 'NumberComponent',
27
- Class.new(BaseComponent) do
28
- include(JsonModel::Schema)
29
-
30
- property(:type, type: T::Const['number'])
31
- property(:value, type: Integer)
32
- end,
33
- )
34
-
35
- all_component_types = ObjectSpace
36
- .each_object(Class)
37
- .select { |klass| klass.ancestors.include?(BaseComponent) && klass != BaseComponent }
38
- stub_const(
39
- 'GridComponent',
40
- Class.new(BaseComponent) do
41
- include(JsonModel::Schema)
42
-
43
- property(:type, type: T::Const['grid'])
44
- property(:components, type: T::Array[T::OneOf[*all_component_types]], optional: true, ref_mode: JsonModel::RefMode::EXTERNAL)
45
- end,
46
- )
47
-
48
-
49
- instance = GridComponent.new(
50
- type: 'grid',
51
- components: [{ type: 'text', value: 'foo' }, { type: 'number', value: 1 }],
52
- )
53
-
54
- expect(instance.components.first).to(be_instance_of(TextComponent))
55
- expect(instance.components.last).to(be_instance_of(NumberComponent))
56
- end
57
- end