jsapi 2.0.1 → 2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 515f05ccd955f40e8f3e4b614ea70c084387c92e58cdc10e8333711b2c1cdb07
4
- data.tar.gz: 455af67cf83aaa3cff47c644ea54eb2968962ad389acd2f018b0e83ba3d2469e
3
+ metadata.gz: cf7d85202927308ea2c287dccbd655cfd56f9b55fe8e56205e686ada853bab9f
4
+ data.tar.gz: 13e0d13f69562c7bba7ac629e45ac5b34890c5ea673e4e2e6b63e24a7c32f232
5
5
  SHA512:
6
- metadata.gz: 9d4c6fa41137915c0a2d18d65035e3926a4b273f61e7aa86ff3403ae6d50e7a29c2622afd25c0a6caa40268a2ec6fe1889beae803f684865706fbdd1d37de1a7
7
- data.tar.gz: a94b12a8245173f5bc683401d43eca5296a41171af2f11b9f9916722acc38199d399872af2217c35f307e11ea5eb74acad0f564cf8f95618a03d6dce8b31dadc
6
+ metadata.gz: 5ff67acae2b3be718a8f0fd4dd08b4a9dbde0f6d919a0f0e21b5f096f88703cf06941ad34d77ff63bdcd0dea244c5339f3c25beb729e9a56a52aa83b19f53848
7
+ data.tar.gz: dd9601553a3ec5537c7650778b7ea3e66888058825bd7f734d2f19ea2beaa732c1866e7aecc3fc3307241ec83f00adb5800f8f18a309ea972dd50f22b1510c96
@@ -127,6 +127,12 @@ module Jsapi
127
127
  [true, false].each do |bang|
128
128
  define_method(bang ? :api_operation! : :api_operation) \
129
129
  do |operation_name = nil, omit: nil, status: nil, strong: false, &block|
130
+ raise ArgumentError, Messages.invalid_value(
131
+ name: 'omit',
132
+ value: omit,
133
+ valid_values: %i[empty nil]
134
+ ) if [nil, :empty, :nil].exclude?(omit)
135
+
130
136
  operation = _api_operation(operation_name)
131
137
  response_model = nil
132
138
 
@@ -247,6 +253,12 @@ module Jsapi
247
253
  # - +:empty+ - All of the properties whose value is empty are omitted.
248
254
  # - +:nil+ - All of the properties whose value is +nil+ are omitted.
249
255
  def api_response(result, operation_name = nil, omit: nil, status: nil)
256
+ raise ArgumentError, Messages.invalid_value(
257
+ name: 'omit',
258
+ value: omit,
259
+ valid_values: %i[empty nil]
260
+ ) if [nil, :empty, :nil].exclude?(omit)
261
+
250
262
  status = Status::Code.from(status)
251
263
  operation = _api_operation(operation_name)
252
264
  response_model = _api_response_model(operation, status)
@@ -4,44 +4,6 @@ module Jsapi
4
4
  module Controller
5
5
  # Used to jsonify a response.
6
6
  class Response
7
- class JsonifyError < RuntimeError # :nodoc:
8
- def message
9
- [@path&.delete_prefix('.') || 'response body', super].join(' ')
10
- end
11
-
12
- def prepend(origin)
13
- @path = "#{origin}#{@path}"
14
- self
15
- end
16
- end
17
-
18
- class HashReader # :nodoc:
19
- delegate_missing_to :@hash
20
-
21
- def initialize(hash)
22
- @hash = hash
23
- end
24
-
25
- def [](key)
26
- return unless @hash.key?(key)
27
-
28
- (@read_keys ||= []) << key
29
- @hash[key]
30
- end
31
-
32
- def additional_properties
33
- if @hash.key?('additional_properties')
34
- @hash['additional_properties']
35
- elsif @hash.key?(:additional_properties)
36
- @hash[:additional_properties]
37
- elsif @read_keys
38
- @hash.except(*@read_keys)
39
- else
40
- @hash
41
- end
42
- end
43
- end
44
-
45
7
  # Creates a new instance to jsonify +object+ according to +content_model+.
46
8
  #
47
9
  # The +:omit+ option specifies on which conditions properties are omitted.
@@ -49,29 +11,25 @@ module Jsapi
49
11
  #
50
12
  # - +:empty+ - All of the properties whose value is empty are omitted.
51
13
  # - +:nil+ - All of the properties whose value is +nil+ are omitted.
52
- #
53
- # Raises an +ArgumentError+ when +:omit+ is other than +:empty+, +:nil+ or +nil+.
54
- def initialize(object, content_model, omit: nil, locale: nil)
14
+ def initialize(object, content_model, locale: nil, omit: nil)
55
15
  @object = object
56
16
  @content_model = content_model
57
-
58
- @omittable_check =
59
- case omit
60
- when nil
61
- nil
62
- when :nil
63
- ->(value, schema) { schema.omittable? && value.nil? }
64
- when :empty
65
- ->(value, schema) { schema.omittable? && value.try(:empty?) }
66
- else
67
- raise ArgumentError, Messages.invalid_value(
68
- name: 'omit',
69
- value: omit,
70
- valid_values: %i[empty nil]
71
- )
72
- end
73
-
74
17
  @locale = locale
18
+ @omit = omit
19
+ end
20
+
21
+ # Returns the \JSON representation of the response.
22
+ def as_json(*)
23
+ with_locale do
24
+ @content_model.schema.jsonify(
25
+ @object,
26
+ context: :response,
27
+ omit: @omit
28
+ )
29
+ end
30
+ rescue JsonifyError => e
31
+ e.prepend('response body') if e.path.blank?
32
+ raise e
75
33
  end
76
34
 
77
35
  def inspect # :nodoc:
@@ -80,24 +38,35 @@ module Jsapi
80
38
 
81
39
  # Returns the \JSON representation of the response as a string.
82
40
  def to_json(*)
83
- with_locale { jsonify(@object, @content_model.schema) }.to_json
41
+ as_json.to_json
84
42
  end
85
43
 
86
44
  # Writes the response in \JSON sequence text format to +stream+.
87
45
  def write_json_seq_to(stream)
88
46
  schema = @content_model.schema
47
+ object = @object
48
+ object = schema.default_value(context: :response) if object.nil?
49
+
89
50
  with_locale do
90
51
  items, item_schema =
91
- if schema.array? && @object.respond_to?(:each)
92
- [@object, schema.items]
52
+ if schema.array? && object.respond_to?(:each)
53
+ [object, schema.items]
93
54
  else
94
- [[@object], schema]
55
+ [[object], schema]
95
56
  end
96
57
 
97
- items.each do |item|
58
+ items.each_with_index do |item, index|
98
59
  stream.write("\u001E") # Record separator (see RFC 7464)
99
- stream.write(jsonify(item, item_schema).to_json)
60
+ stream.write(
61
+ item_schema.jsonify(
62
+ item,
63
+ context: :response,
64
+ omit: @omit
65
+ ).to_json
66
+ )
100
67
  stream.write("\n")
68
+ rescue JsonifyError => e
69
+ raise e.prepend("[#{index}]")
101
70
  end
102
71
  end
103
72
  nil
@@ -105,88 +74,6 @@ module Jsapi
105
74
 
106
75
  private
107
76
 
108
- def jsonify(object, schema)
109
- object = schema.default_value(context: :response) if object.nil?
110
-
111
- if object.nil?
112
- raise JsonifyError, "can't be nil" unless schema.nullable?
113
- else
114
- case schema.type
115
- when 'array'
116
- jsonify_array(object, schema)
117
- when 'boolean'
118
- object
119
- when 'integer'
120
- schema.convert(object.to_i)
121
- when 'number'
122
- schema.convert(object.to_f)
123
- when 'object'
124
- jsonify_object(object, schema)
125
- when 'string'
126
- schema.convert(
127
- case schema.format
128
- when 'date'
129
- object.to_date
130
- when 'date-time'
131
- object.to_datetime
132
- when 'duration'
133
- object.iso8601
134
- else
135
- object.to_s
136
- end
137
- )
138
- else
139
- raise JsonifyError, "has an invalid type: #{schema.type.inspect}"
140
- end
141
- end
142
- end
143
-
144
- def jsonify_array(array, schema)
145
- item_schema = schema.items
146
- index = 0
147
-
148
- Array(array).map do |item|
149
- item = jsonify(item, item_schema)
150
- index += 1
151
- item
152
- rescue JsonifyError => e
153
- raise e.prepend("[#{index}]")
154
- end
155
- end
156
-
157
- def jsonify_object(object, schema)
158
- schema = schema.resolve_schema(object, context: :response)
159
- additional_properties = schema.additional_properties
160
- object = HashReader.new(object) if additional_properties && object.is_a?(Hash)
161
-
162
- {}.tap do |properties|
163
- # Add properties
164
- schema.resolve_properties(context: :response).each_value do |property|
165
- property_schema = property.schema
166
- property_value = property.reader.call(object)
167
- property_value = property_schema.default if property_value.nil?
168
- next if @omittable_check&.call(property_value, property_schema)
169
-
170
- properties[property.name] = jsonify(property_value, property_schema)
171
- rescue JsonifyError => e
172
- raise e.prepend(".#{property.name}")
173
- end
174
- # Add additional properties
175
- if additional_properties
176
- additional_properties_schema = additional_properties.schema
177
-
178
- additional_properties.source.call(object)&.each do |key, value|
179
- key = key.to_s
180
- next if properties.key?(key)
181
-
182
- properties[key] = jsonify(value, additional_properties_schema)
183
- rescue JsonifyError => e
184
- raise e.prepend(".#{key}")
185
- end
186
- end
187
- end.presence
188
- end
189
-
190
77
  def with_locale(&block)
191
78
  if @locale
192
79
  I18n.with_locale(@locale, &block)
@@ -89,6 +89,11 @@ module Jsapi
89
89
 
90
90
  private
91
91
 
92
+ def deprecation_warning(message)
93
+ caller_location = caller.find { |line| line.exclude?('lib/jsapi') }
94
+ warn("[DEPRECATION WARNING] #{message}\nDefined at: #{caller_location}")
95
+ end
96
+
92
97
  def define(*args, &block)
93
98
  result = block.call
94
99
  result.freeze_attributes if result.respond_to?(:freeze_attributes)
@@ -81,6 +81,14 @@ module Jsapi
81
81
  if name && (keywords.any? || block)
82
82
 
83
83
  keywords = { ref: name } if name
84
+
85
+ if (%i[type ref schema contents].none? { |key| keywords.key?(key) }) && !block
86
+ deprecation_warning(
87
+ 'Starting with Jsapi 3.0, a `request_body` without :type will no longer ' \
88
+ 'define an :object schema. Please specify `type: :object` explicitly.'
89
+ )
90
+ end
91
+
84
92
  @meta_model.request_body = keywords
85
93
 
86
94
  @meta_model.request_body.tap do |request_body|
@@ -123,6 +131,13 @@ module Jsapi
123
131
  raise Error, "name can't be specified together with keywords or a block"
124
132
  end
125
133
 
134
+ if (%i[type ref schema contents].none? { |key| keywords.key?(key) }) && !block
135
+ deprecation_warning(
136
+ 'Starting with Jsapi 3.0, a `response` without :type will no longer ' \
137
+ 'define an :object schema. Please specify `type: :object` explicitly.'
138
+ )
139
+ end
140
+
126
141
  @meta_model.add_response(status, keywords).tap do |response_model|
127
142
  Response.new(response_model, &block) if block
128
143
  end
@@ -4,9 +4,9 @@ module Jsapi
4
4
  module JSON
5
5
  # Represents a JSON array.
6
6
  class Array < Value
7
- def initialize(array, schema, context: nil)
7
+ def initialize(value, schema, context: nil)
8
8
  super(schema)
9
- @json_values = Array(array).map do |item|
9
+ @json_values = Array(value).map do |item|
10
10
  JSON.wrap(item, schema.items, context: context)
11
11
  end
12
12
  end
@@ -8,7 +8,14 @@ module Jsapi
8
8
 
9
9
  def initialize(value, schema)
10
10
  super(schema)
11
- @value = schema.convert(value.to_i)
11
+ @value = schema.convert(
12
+ begin
13
+ value.to_i
14
+ rescue StandardError => _e
15
+ self.malformed = true
16
+ 0
17
+ end
18
+ )
12
19
  end
13
20
  end
14
21
  end
@@ -8,7 +8,14 @@ module Jsapi
8
8
 
9
9
  def initialize(value, schema)
10
10
  super(schema)
11
- @value = schema.convert(value.to_f)
11
+ @value = schema.convert(
12
+ begin
13
+ value.to_f
14
+ rescue StandardError => _e
15
+ self.malformed = true
16
+ 0.0
17
+ end
18
+ )
12
19
  end
13
20
  end
14
21
  end
@@ -10,7 +10,15 @@ module Jsapi
10
10
 
11
11
  attr_reader :raw_additional_attributes, :raw_attributes
12
12
 
13
- def initialize(hash, schema, context: nil)
13
+ def initialize(value, schema, context: nil)
14
+ hash =
15
+ begin
16
+ value.to_h
17
+ rescue StandardError => _e
18
+ self.malformed = true
19
+ {}
20
+ end
21
+
14
22
  schema = schema.resolve_schema(hash, context: context)
15
23
  properties = schema.resolve_properties(context: context)
16
24
 
@@ -21,23 +21,16 @@ module Jsapi
21
21
  else
22
22
  value.to_s
23
23
  end
24
- rescue StandardError => e
25
- @error = e
24
+ rescue StandardError => _e
25
+ self.malformed = true
26
26
  value
27
27
  end
28
- @value = schema.convert(@value) unless defined? @error
28
+ @value = schema.convert(@value) unless malformed?
29
29
  end
30
30
 
31
31
  def empty? # :nodoc:
32
32
  value.blank?
33
33
  end
34
-
35
- def validate(errors) # :nodoc:
36
- return super unless defined? @error
37
-
38
- errors.add(:base, :invalid)
39
- false
40
- end
41
34
  end
42
35
  end
43
36
  end
@@ -13,7 +13,7 @@ module Jsapi
13
13
  @schema = schema
14
14
  end
15
15
 
16
- # Used by #validate to test whether or not it is empty.
16
+ # Called by #validate to test whether or not it is empty.
17
17
  # Returns false by default.
18
18
  def empty?
19
19
  false
@@ -23,7 +23,7 @@ module Jsapi
23
23
  "#<#{self.class} #{value.inspect}>"
24
24
  end
25
25
 
26
- # Used by #validate to test whether or not it is null.
26
+ # Called by #validate to test whether or not it is null.
27
27
  # Returns false by default.
28
28
  def null?
29
29
  false
@@ -37,6 +37,10 @@ module Jsapi
37
37
  # Validates it against #schema. Returns true if it is valid, false otherwise.
38
38
  # Detected errors are added to +errors+.
39
39
  def validate(errors)
40
+ if malformed?
41
+ errors.add(:base, :invalid)
42
+ return false
43
+ end
40
44
  unless schema.existence.reach?(self)
41
45
  errors.add(:base, :blank)
42
46
  return false
@@ -47,6 +51,14 @@ module Jsapi
47
51
  validation.validate(value, errors)
48
52
  end.all?
49
53
  end
54
+
55
+ protected
56
+
57
+ attr_writer :malformed
58
+
59
+ def malformed?
60
+ @malformed == true
61
+ end
50
62
  end
51
63
  end
52
64
  end
data/lib/jsapi/json.rb CHANGED
@@ -13,23 +13,23 @@ module Jsapi
13
13
  # Provides a DOM for JSON values.
14
14
  module JSON
15
15
  class << self
16
- def wrap(object, schema, context: nil)
17
- object = schema.default_value(context: context) if object.nil?
18
- return Null.new(schema) if object.nil?
16
+ def wrap(value, schema, context: nil)
17
+ value = schema.default_value(context: context) if value.nil?
18
+ return Null.new(schema) if value.nil?
19
19
 
20
20
  case schema.type
21
21
  when 'array'
22
- Array.new(object, schema, context: context)
22
+ Array.new(value, schema, context: context)
23
23
  when 'boolean'
24
- Boolean.new(object, schema)
24
+ Boolean.new(value, schema)
25
25
  when 'integer'
26
- Integer.new(object, schema)
26
+ Integer.new(value, schema)
27
27
  when 'number'
28
- Number.new(object, schema)
28
+ Number.new(value, schema)
29
29
  when 'object'
30
- Object.new(object, schema, context: context)
30
+ Object.new(value, schema, context: context)
31
31
  when 'string'
32
- String.new(object, schema)
32
+ String.new(value, schema)
33
33
  else
34
34
  raise "invalid type: #{schema.type}"
35
35
  end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jsapi
4
+ # Raised when jsonifying an object fails.
5
+ class JsonifyError < RuntimeError
6
+ attr_reader :path # :nodoc:
7
+
8
+ def message
9
+ [@path&.delete_prefix('.'), super].compact.join(' ')
10
+ end
11
+
12
+ def prepend(origin) # :nodoc:
13
+ @path = "#{origin}#{@path}"
14
+ self
15
+ end
16
+ end
17
+ end
@@ -36,10 +36,19 @@ module Jsapi
36
36
  @schema = Schema.new(keywords)
37
37
  end
38
38
 
39
+ # Returns the data value of the first example.
40
+ def example_data_value(definitions, locale: nil)
41
+ examples
42
+ .values.first
43
+ &.resolve(definitions)
44
+ &.data_value(builder: example_builder(definitions, locale))
45
+ end
46
+
39
47
  # Returns a hash representing the \OpenAPI media type object describing
40
48
  # the content. Applies to \OpenAPI 3.0 and higher.
41
- def to_openapi(version, media_type = nil)
49
+ def to_openapi(version, definitions = nil, locale: nil, media_type: nil)
42
50
  version = OpenAPI::Version.from(version)
51
+ example_builder = nil
43
52
 
44
53
  with_openapi_extensions(
45
54
  **if media_type == Media::Type::APPLICATION_JSON_SEQ &&
@@ -50,10 +59,17 @@ module Jsapi
50
59
  end,
51
60
  examples:
52
61
  examples.transform_values do |example|
53
- example.to_openapi(version)
62
+ example_builder ||= example_builder(definitions, locale)
63
+ example.to_openapi(version, builder: example_builder)
54
64
  end.presence
55
65
  )
56
66
  end
67
+
68
+ private
69
+
70
+ def example_builder(definitions, locale)
71
+ Example::Builder.new(schema, definitions, locale: locale) if definitions.present?
72
+ end
57
73
  end
58
74
  end
59
75
  end
@@ -313,6 +313,10 @@ module Jsapi
313
313
  end
314
314
  end
315
315
 
316
+ ##
317
+ # :method: find_example
318
+ # Returns the reusable example with the specified name.
319
+
316
320
  ##
317
321
  # :method: find_parameter
318
322
  # Returns the reusable parameter with the specified name.
@@ -333,7 +337,8 @@ module Jsapi
333
337
  # :method: find_security_scheme
334
338
  # Returns the security scheme with the specified name.
335
339
 
336
- %i[parameters request_bodies responses schemas security_schemes].each do |attribute_name|
340
+ %i[examples parameters request_bodies responses
341
+ schemas security_schemes].each do |attribute_name|
337
342
  define_method(:"find_#{attribute_name.to_s.singularize}") do |name|
338
343
  cached_attributes.dig(attribute_name, name&.to_s)
339
344
  end
@@ -29,7 +29,7 @@ module Jsapi
29
29
 
30
30
  ##
31
31
  # :attr: value
32
- # The sample value.
32
+ # The sample value. Can be a +Proc+.
33
33
  attribute :value
34
34
 
35
35
  def external_value=(value) # :nodoc:
@@ -41,7 +41,7 @@ module Jsapi
41
41
  end
42
42
  end
43
43
 
44
- def serialized_value=(value) # :nodoc:
44
+ def serialized_value=(value) # :nodoc:
45
45
  try_modify_attribute!(:serialized_value) do
46
46
  raise 'external value and serialized value are mutually exclusive' \
47
47
  unless external_value.nil?
@@ -50,18 +50,27 @@ module Jsapi
50
50
  end
51
51
  end
52
52
 
53
+ # Returns the data structure of the sample value.
54
+ def data_value(builder: nil)
55
+ value = self.value
56
+ return value unless value.is_a?(Proc)
57
+
58
+ value.arity.zero? ? value.call : value.call(builder)
59
+ end
60
+
53
61
  # Returns a hash representing the \OpenAPI example object.
54
- def to_openapi(version, *)
62
+ def to_openapi(version, *, builder: nil)
55
63
  version = OpenAPI::Version.from(version)
64
+ data_value = data_value(builder: builder)
56
65
 
57
66
  with_openapi_extensions(
58
67
  summary: summary,
59
68
  description: description,
60
69
  **if version < OpenAPI::V3_2
61
- { value: value }
70
+ { value: data_value }
62
71
  else
63
72
  {
64
- dataValue: value,
73
+ dataValue: data_value,
65
74
  serializedValue: serialized_value
66
75
  }
67
76
  end,
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jsapi
4
+ module Meta
5
+ module Example
6
+ class Builder # :nodoc:
7
+ def initialize(schema, definitions, locale: nil)
8
+ @schema = Schema.wrap(schema, definitions)
9
+ @locale = locale
10
+ end
11
+
12
+ def generate_json(object, omit: nil)
13
+ raise ArgumentError, Messages.invalid_value(
14
+ name: 'omit',
15
+ value: omit,
16
+ valid_values: %i[empty nil]
17
+ ) if [nil, :empty, :nil].exclude?(omit)
18
+
19
+ with_locale { @schema.jsonify(object, omit: omit) }
20
+ end
21
+
22
+ private
23
+
24
+ def with_locale(&block)
25
+ if @locale
26
+ I18n.with_locale(@locale, &block)
27
+ else
28
+ yield
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'example/base'
4
+ require_relative 'example/builder'
4
5
  require_relative 'example/reference'
5
6
 
6
7
  module Jsapi
@@ -104,7 +104,7 @@ module Jsapi
104
104
  with_openapi_extensions(
105
105
  if version == OpenAPI::V2_0
106
106
  media_type, content = contents.first
107
- example = content&.examples&.values&.first
107
+ example = content&.example_data_value(definitions, locale: locale)
108
108
  {
109
109
  description: description,
110
110
  schema: content&.schema&.to_openapi(version),
@@ -114,7 +114,7 @@ module Jsapi
114
114
  end.compact.presence,
115
115
  examples:
116
116
  if media_type.present? && example.present?
117
- { media_type => example.resolve(definitions).value }
117
+ { media_type => example }
118
118
  end
119
119
  }
120
120
  else
@@ -127,7 +127,15 @@ module Jsapi
127
127
  end.presence,
128
128
  content:
129
129
  contents.to_h do |nth_media_type, nth_content|
130
- [nth_media_type, nth_content.to_openapi(version, nth_media_type)]
130
+ [
131
+ nth_media_type,
132
+ nth_content.to_openapi(
133
+ version,
134
+ definitions,
135
+ locale: locale,
136
+ media_type: nth_media_type
137
+ )
138
+ ]
131
139
  end.presence,
132
140
  links:
133
141
  links.transform_values do |link|
@@ -12,6 +12,10 @@ module Jsapi
12
12
  super
13
13
  end
14
14
 
15
+ def contents
16
+ super.transform_values { |content| Content.wrap(content, definitions) }
17
+ end
18
+
15
19
  def media_type_and_content_for(*media_ranges)
16
20
  super&.then do |media_type_and_content|
17
21
  [
@@ -8,6 +8,16 @@ module Jsapi
8
8
  def items
9
9
  @items ||= Schema.wrap(super, definitions)
10
10
  end
11
+
12
+ private
13
+
14
+ def jsonify_value(value, context:, omit:)
15
+ Array(value).each_with_index.map do |item, index|
16
+ items.jsonify(item, context: context, omit: omit)
17
+ rescue JsonifyError => e
18
+ raise e.prepend("[#{index}]")
19
+ end
20
+ end
11
21
  end
12
22
 
13
23
  ##
@@ -3,7 +3,15 @@
3
3
  module Jsapi
4
4
  module Meta
5
5
  module Schema
6
- class Boolean < Base; end
6
+ class Boolean < Base
7
+ class Wrapper < Schema::Wrapper
8
+ private
9
+
10
+ def jsonify_value(value, **)
11
+ value ? true : false
12
+ end
13
+ end
14
+ end
7
15
  end
8
16
  end
9
17
  end
@@ -10,13 +10,13 @@ module Jsapi
10
10
  base.attr_accessor :conversion
11
11
  end
12
12
 
13
- def convert(object)
14
- return object if conversion.nil?
13
+ def convert(value)
14
+ return value if conversion.nil?
15
15
 
16
16
  if conversion.respond_to?(:call)
17
- conversion.call(object)
17
+ conversion.call(value)
18
18
  else
19
- object.send(conversion)
19
+ value.send(conversion)
20
20
  end
21
21
  end
22
22
  end
@@ -3,7 +3,15 @@
3
3
  module Jsapi
4
4
  module Meta
5
5
  module Schema
6
- class Integer < Numeric; end
6
+ class Integer < Numeric
7
+ class Wrapper < Schema::Wrapper
8
+ private
9
+
10
+ def jsonify_value(value, **)
11
+ convert(value.to_i)
12
+ end
13
+ end
14
+ end
7
15
  end
8
16
  end
9
17
  end
@@ -3,7 +3,15 @@
3
3
  module Jsapi
4
4
  module Meta
5
5
  module Schema
6
- class Number < Numeric; end
6
+ class Number < Numeric
7
+ class Wrapper < Schema::Wrapper
8
+ private
9
+
10
+ def jsonify_value(value, **)
11
+ convert(value.to_f)
12
+ end
13
+ end
14
+ end
7
15
  end
8
16
  end
9
17
  end
@@ -5,6 +5,33 @@ module Jsapi
5
5
  module Schema
6
6
  class Object < Base
7
7
  class Wrapper < Schema::Wrapper
8
+ class HashReader # :nodoc:
9
+ delegate_missing_to :@hash
10
+
11
+ def initialize(hash)
12
+ @hash = hash
13
+ end
14
+
15
+ def [](key)
16
+ return unless @hash.key?(key)
17
+
18
+ (@read_keys ||= []) << key
19
+ @hash[key]
20
+ end
21
+
22
+ def additional_properties
23
+ if @hash.key?('additional_properties')
24
+ @hash['additional_properties']
25
+ elsif @hash.key?(:additional_properties)
26
+ @hash[:additional_properties]
27
+ elsif @read_keys
28
+ @hash.except(*@read_keys)
29
+ else
30
+ @hash
31
+ end
32
+ end
33
+ end
34
+
8
35
  def additional_properties
9
36
  AdditionalProperties.wrap(super, definitions)
10
37
  end
@@ -58,6 +85,53 @@ module Jsapi
58
85
  .new(schema.resolve(definitions), definitions)
59
86
  .resolve_schema(object, context: context)
60
87
  end
88
+
89
+ private
90
+
91
+ def jsonify_value(value, context:, omit:)
92
+ schema = resolve_schema(value, context: context)
93
+ additional_properties = schema.additional_properties
94
+ value = HashReader.new(value) if additional_properties && value.is_a?(Hash)
95
+
96
+ {}.tap do |properties|
97
+ # Add properties
98
+ schema.resolve_properties(context: context).each_value do |property|
99
+ property_schema = property.schema
100
+ property_value = property.reader.call(value)
101
+ property_value = property_schema.default if property_value.nil?
102
+
103
+ property_value = property_schema.jsonify(
104
+ property_value,
105
+ context: context,
106
+ omit: omit
107
+ )
108
+ if property_schema.omittable?
109
+ next if omit == :nil && property_value.nil?
110
+ next if omit == :empty && (property_value.nil? || property_value.try(:empty?))
111
+ end
112
+ properties[property.name] = property_value
113
+ rescue JsonifyError => e
114
+ raise e.prepend(".#{property.name}")
115
+ end
116
+ # Add additional properties
117
+ if additional_properties
118
+ additional_properties_schema = additional_properties.schema
119
+
120
+ additional_properties.source.call(value)&.each do |property_name, property_value|
121
+ property_name = property_name.to_s
122
+ next if properties.key?(property_name)
123
+
124
+ properties[property_name] = additional_properties_schema.jsonify(
125
+ property_value,
126
+ context: context,
127
+ omit: omit
128
+ )
129
+ rescue JsonifyError => e
130
+ raise e.prepend(".#{property_name}")
131
+ end
132
+ end
133
+ end
134
+ end
61
135
  end
62
136
 
63
137
  ##
@@ -6,6 +6,28 @@ module Jsapi
6
6
  class String < Base
7
7
  include Conversion
8
8
 
9
+ class Wrapper < Schema::Wrapper # :nodoc:
10
+ private
11
+
12
+ def jsonify_value(value, **)
13
+ convert(
14
+ case format
15
+ when 'date'
16
+ value.to_date.as_json
17
+ when 'date-time'
18
+ value.to_datetime.as_json
19
+ when 'duration'
20
+ value = ActiveSupport::Duration.parse(value) \
21
+ unless value.is_a?(ActiveSupport::Duration)
22
+
23
+ value.iso8601
24
+ else
25
+ value.to_s
26
+ end
27
+ )
28
+ end
29
+ end
30
+
9
31
  ##
10
32
  # :attr: format
11
33
  # The format of a string.
@@ -25,6 +25,20 @@ module Jsapi
25
25
 
26
26
  definitions.default_value(type, context: context)
27
27
  end
28
+
29
+ # Generates a JSON value representing +value+.
30
+ def jsonify(value, context: nil, omit: nil)
31
+ value = default_value(context: context) if value.nil?
32
+ raise JsonifyError, "can't be nil" if value.nil? && !nullable?
33
+
34
+ jsonify_value(value, context: context, omit: omit) unless value.nil?
35
+ end
36
+
37
+ private
38
+
39
+ def jsonify_value(value, **)
40
+ value
41
+ end
28
42
  end
29
43
  end
30
44
  end
@@ -65,14 +65,9 @@ module Jsapi
65
65
  return if schema.nil?
66
66
  return schema if schema.is_a?(Wrapper)
67
67
 
68
- case schema.resolve(definitions).type
69
- when 'array'
70
- Array::Wrapper
71
- when 'object'
72
- Object::Wrapper
73
- else
74
- Wrapper
75
- end.new(schema, definitions)
68
+ "#{schema.resolve(definitions).class.name}::Wrapper"
69
+ .constantize
70
+ .new(schema, definitions)
76
71
  end
77
72
  end
78
73
  end
data/lib/jsapi/version.rb CHANGED
@@ -5,6 +5,6 @@ module Jsapi
5
5
  # NOTE: See https://bundler.io/guides/creating_gem.html
6
6
 
7
7
  # The current GEM version.
8
- VERSION = '2.0.1'
8
+ VERSION = '2.1.1'
9
9
  end
10
10
  end
data/lib/jsapi.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'jsapi/messages'
4
+ require 'jsapi/jsonify_error'
4
5
  require 'jsapi/configuration'
5
6
  require 'jsapi/media'
6
7
  require 'jsapi/status'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jsapi
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.1
4
+ version: 2.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Denis Göller
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-04-20 00:00:00.000000000 Z
11
+ date: 2026-10-13 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email: denis@dmgoeller.de
@@ -58,6 +58,7 @@ files:
58
58
  - lib/jsapi/json/object.rb
59
59
  - lib/jsapi/json/string.rb
60
60
  - lib/jsapi/json/value.rb
61
+ - lib/jsapi/jsonify_error.rb
61
62
  - lib/jsapi/media.rb
62
63
  - lib/jsapi/media/range.rb
63
64
  - lib/jsapi/media/type.rb
@@ -76,6 +77,7 @@ files:
76
77
  - lib/jsapi/meta/definitions.rb
77
78
  - lib/jsapi/meta/example.rb
78
79
  - lib/jsapi/meta/example/base.rb
80
+ - lib/jsapi/meta/example/builder.rb
79
81
  - lib/jsapi/meta/example/reference.rb
80
82
  - lib/jsapi/meta/existence.rb
81
83
  - lib/jsapi/meta/external_documentation.rb