jbuilder-schema 2.0.4 → 2.1.0

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: 2aea259ba4965ec62874b56ab4267ddc2f4cd2967babe536726397c53599c922
4
- data.tar.gz: 71c9f200fb4cc481055ec0790e514b9a46108a6ae4cb0c0aad1ed09eba5f384e
3
+ metadata.gz: 8e69ae6e6726679110d135c34a461a2bfc176f967bfa1777b73989ae107fdabc
4
+ data.tar.gz: 63fc234f84fe96195c6cc849bf26f4bea6d5e60808a0363e4fdddfe778497292
5
5
  SHA512:
6
- metadata.gz: d0ac95a76f5630daad73debfd74f6056dd3e6b4f30e4075779a41306585d211c918d16140de6f32b01a60db3303ac21e1ccdf8c52b9ea4292e32c38a134e1e21
7
- data.tar.gz: d88181a3d13d3748cfca995bbcc9de741bb9d5ecf98bd34af452b499aaeae2c0a6c44c5fc57ea88ee3892a2b6c2579f65a3310f1b59a83805067422ea20956c3
6
+ metadata.gz: 2fcc3260655ff07670c2a8be5175c801f29af27790e0675c680fc861bae3a2a48dcd30170cb5468e595ec647d58c9abe92d2177f7c5dc77acdb8569f8ab7997e
7
+ data.tar.gz: ca344a595d3ed5c43c602e5d24ca154c4e0b9872388bd055422c957af93891be133618651e423f560ccc419b66c8b068828202f68929161d0790c4efbf71c8cb
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- jbuilder-schema (2.0.4)
4
+ jbuilder-schema (2.1.0)
5
5
  jbuilder
6
6
  rails (>= 5.0.0)
7
7
 
@@ -77,7 +77,7 @@ GEM
77
77
  builder (3.2.4)
78
78
  concurrent-ruby (1.1.10)
79
79
  crass (1.0.6)
80
- erubi (1.11.0)
80
+ erubi (1.12.0)
81
81
  globalid (1.0.0)
82
82
  activesupport (>= 5.0)
83
83
  i18n (1.12.0)
@@ -86,7 +86,7 @@ GEM
86
86
  actionview (>= 5.0.0)
87
87
  activesupport (>= 5.0.0)
88
88
  json (2.6.2)
89
- loofah (2.19.0)
89
+ loofah (2.19.1)
90
90
  crass (~> 1.0.2)
91
91
  nokogiri (>= 1.5.9)
92
92
  mail (2.8.0)
@@ -98,13 +98,13 @@ GEM
98
98
  method_source (1.0.0)
99
99
  mini_mime (1.1.2)
100
100
  mini_portile2 (2.8.0)
101
- minitest (5.16.3)
101
+ minitest (5.17.0)
102
102
  mocha (1.14.0)
103
103
  net-imap (0.3.1)
104
104
  net-protocol
105
105
  net-pop (0.1.2)
106
106
  net-protocol
107
- net-protocol (0.2.0)
107
+ net-protocol (0.2.1)
108
108
  timeout
109
109
  net-smtp (0.3.3)
110
110
  net-protocol
@@ -115,8 +115,8 @@ GEM
115
115
  parallel (1.22.1)
116
116
  parser (3.1.2.0)
117
117
  ast (~> 2.4.1)
118
- racc (1.6.1)
119
- rack (2.2.4)
118
+ racc (1.6.2)
119
+ rack (2.2.5)
120
120
  rack-test (2.0.2)
121
121
  rack (>= 1.3)
122
122
  rails (7.0.4)
@@ -136,8 +136,8 @@ GEM
136
136
  rails-dom-testing (2.0.3)
137
137
  activesupport (>= 4.2.0)
138
138
  nokogiri (>= 1.6)
139
- rails-html-sanitizer (1.4.3)
140
- loofah (~> 2.3)
139
+ rails-html-sanitizer (1.4.4)
140
+ loofah (~> 2.19, >= 2.19.1)
141
141
  railties (7.0.4)
142
142
  actionpack (= 7.0.4)
143
143
  activesupport (= 7.0.4)
@@ -3,6 +3,7 @@ require_relative "template"
3
3
 
4
4
  class Jbuilder::Schema::Renderer
5
5
  @@view_renderer = ActionView::Base.with_empty_template_cache
6
+ @@view_renderer.prefix_partial_path_with_controller_namespace = false
6
7
 
7
8
  def initialize(paths, default_locals = nil)
8
9
  @view_renderer = @@view_renderer.with_view_paths(paths)
@@ -18,19 +19,25 @@ class Jbuilder::Schema::Renderer
18
19
  end
19
20
 
20
21
  def render(object = nil, title: nil, description: nil, assigns: nil, **options)
21
- if object
22
- partial_path = object.respond_to?(:to_partial_path_for_jbuilder_schema) ? object.to_partial_path_for_jbuilder_schema : object.to_partial_path
23
- options.merge! partial: partial_path, object: object
24
- end
22
+ @view_renderer.assign assigns if assigns
23
+
24
+ json = original_render(object || options.dup, options.dup)
25
+
26
+ partial_path = %i[to_partial_path_for_jbuilder_schema to_partial_path].map { object.public_send(_1) if object.respond_to?(_1) }.compact.first
27
+ options.merge! partial: partial_path, object: object if partial_path
25
28
 
26
29
  options[:locals] ||= {}
27
30
  options[:locals].merge! @default_locals if @default_locals
28
- options[:locals][:__jbuilder_schema_options] = { model: object&.class, title: title, description: description }
31
+ options[:locals][:__jbuilder_schema_options] = { json: json, object: object, title: title, description: description }
29
32
 
30
- @view_renderer.assign assigns if assigns
31
33
  @view_renderer.render(options)
32
34
  end
33
35
 
36
+ # Thin wrapper around the regular Jbuilder JSON output render, which also parses it into a hash.
37
+ def original_render(options = {}, locals = {})
38
+ JSON.parse @view_renderer.render(options, locals)
39
+ end
40
+
34
41
  private
35
42
 
36
43
  def normalize(schema)
@@ -5,9 +5,6 @@ require "active_support/inflections"
5
5
 
6
6
  class Jbuilder::Schema
7
7
  class Template < ::JbuilderTemplate
8
- attr_reader :attributes, :type
9
- attr_reader :model_scope
10
-
11
8
  class Handler < ::JbuilderHandler
12
9
  def self.call(template, source = nil)
13
10
  super.sub("JbuilderTemplate.new(self", "Jbuilder::Schema::Template.build(self, local_assigns")
@@ -26,34 +23,33 @@ class Jbuilder::Schema
26
23
  end
27
24
  end
28
25
 
29
- ModelScope = ::Struct.new(:model, :title, :description, keyword_init: true) do
30
- def initialize(**)
31
- super
32
- @scope = model&.name&.underscore&.pluralize
26
+ class Configuration < ::Struct.new(:object, :title, :description, keyword_init: true)
27
+ def self.build(object: nil, object_title: nil, object_description: nil, **)
28
+ new(object: object, title: object_title, description: object_description)
33
29
  end
34
30
 
35
- def i18n_title
36
- title || ::I18n.t(::Jbuilder::Schema.title_name, scope: @scope)
31
+ def title
32
+ super || translate(Jbuilder::Schema.title_name)
37
33
  end
38
34
 
39
- def i18n_description
40
- description || ::I18n.t(::Jbuilder::Schema.description_name, scope: @scope)
35
+ def description
36
+ super || translate(Jbuilder::Schema.description_name)
41
37
  end
42
38
 
43
39
  def translate_field(key)
44
- ::I18n.t("fields.#{key}.#{::Jbuilder::Schema.description_name}", scope: @scope)
40
+ translate("fields.#{key}.#{Jbuilder::Schema.description_name}")
45
41
  end
46
- end
47
42
 
48
- def initialize(context, **options)
49
- @type = :object
50
- @inline_array = false
51
- @collection = false
52
-
53
- @model_scope = ModelScope.new(**options)
43
+ private
44
+ def translate(key)
45
+ I18n.t(key, scope: @scope ||= object&.class&.name&.underscore&.pluralize)
46
+ end
47
+ end
54
48
 
49
+ def initialize(context, json: nil, **options)
50
+ @json = json
51
+ @configuration = Configuration.new(**options)
55
52
  super(context)
56
-
57
53
  @ignore_nil = false
58
54
  end
59
55
 
@@ -62,176 +58,112 @@ class Jbuilder::Schema
62
58
  end
63
59
 
64
60
  def schema!
65
- {type: type}.merge(type == :object ? _object(**attributes.merge) : attributes)
61
+ if ([@attributes] + @attributes.each_value.grep(::Hash)).any? { _1[:type] == :array && _1.key?(:items) }
62
+ @attributes
63
+ else
64
+ _object(@attributes)
65
+ end.merge(example: @json).compact
66
66
  end
67
67
 
68
- def set!(key, value = BLANK, *args, schema: {}, **options, &block)
69
- result = if block
70
- if !_blank?(value)
71
- # OBJECTS ARRAY:
72
- # json.comments @article.comments { |comment| ... }
73
- # { "comments": [ { ... }, { ... } ] }
74
- _scope { array! value, &block }
75
- else
76
- # BLOCK:
77
- # json.comments { ... }
78
- # { "comments": ... }
79
- @inline_array = true
80
-
81
- _with_model_scope(**schema) do
82
- _merge_block(key) { yield self }
83
- end
84
- end
85
- elsif args.empty?
86
- if ::Jbuilder === value
87
- # ATTRIBUTE1:
88
- # json.age 32
89
- # json.person another_jbuilder
90
- # { "age": 32, "person": { ... }
91
- _schema(key, _format_keys(value.attributes!), **schema)
92
- elsif _is_collection_array?(value)
93
- # ATTRIBUTE2:
94
- _scope { array! value }
95
- # json.articles @articles
96
- else
97
- # json.age 32
98
- # { "age": 32 }
99
- _schema(key, _format_keys(value), **schema)
100
- end
101
- elsif _is_collection?(value)
102
- # COLLECTION:
103
- # json.comments @article.comments, :content, :created_at
104
- # { "comments": [ { "content": "hello", "created_at": "..." }, { "content": "world", "created_at": "..." } ] }
105
- @inline_array = true
106
- @collection = true
107
-
108
- _scope { array! value, *args }
109
- else
110
- # EXTRACT!:
111
- # json.author @article.creator, :name, :email_address
112
- # { "author": { "name": "David", "email_address": "david@loudthinking.com" } }
113
- _with_model_scope(**schema) do
114
- _merge_block(key) { extract! value, *args, schema: schema }
115
- end
116
- end
68
+ def set!(key, value = BLANK, *args, schema: nil, **options, &block)
69
+ old_configuration, @configuration = @configuration, Configuration.build(**schema) if schema&.dig(:object)
117
70
 
118
- result = _set_description key, result if model_scope.model
119
- _set_value key, result
120
- end
71
+ _with_schema_overrides(key => schema) do
72
+ keys = args.presence || _extract_possible_keys(value)
121
73
 
122
- def extract!(object, *attributes, schema: {})
123
- if ::Hash === object
124
- _extract_hash_values(object, attributes, schema: schema)
125
- else
126
- _extract_method_values(object, attributes, schema: schema)
74
+ # Detect `json.articles user.articles` to override Jbuilder's logic, which wouldn't hit `array!` and set a `type: :array, items: {"$ref": "#/components/schemas/article"}` ref.
75
+ if block.nil? && keys.blank? && _is_collection?(value) && (value.empty? || value.all? { _is_active_model?(_1) })
76
+ _set_value(key, _scope { _set_ref(key.to_s.singularize, array: true) })
77
+ else
78
+ super(key, value, *keys, **options, &block)
79
+ end
127
80
  end
81
+ ensure
82
+ @configuration = old_configuration if old_configuration
128
83
  end
84
+ alias_method :method_missing, :set! # TODO: Remove once Jbuilder passes keyword arguments along to `set!` in its `method_missing`.
129
85
 
130
- def array!(collection = [], *args, schema: {}, **options, &block)
86
+ def array!(collection = [], *args, schema: nil, **options, &block)
131
87
  if _partial_options?(options)
132
- @collection = true
133
- _set_ref(options[:partial].split("/").last)
88
+ partial!(collection: collection, **options)
134
89
  else
135
- array = _make_array(collection, *args, schema: schema, &block)
136
-
137
- if @inline_array
138
- @attributes = {}
139
- _set_value(:type, :array)
140
- _set_value(:items, array)
141
- elsif _is_collection_array?(array)
142
- @attributes = {}
143
- @inline_array = true
144
- @collection = true
145
- array! array, *array.first&.attribute_names(&:to_sym)
146
- else
147
- @type = :array
148
- @attributes = {}
149
- _set_value(:items, array)
90
+ _with_schema_overrides(schema) do
91
+ _attributes.merge! type: :array, items: _scope { super(collection, *args, &block) }
150
92
  end
151
93
  end
152
94
  end
153
95
 
154
- def partial!(*args)
155
- if args.one? && _is_active_model?(args.first)
96
+ def extract!(object, *attributes, schema: nil)
97
+ _with_schema_overrides(schema) { super(object, *attributes) }
98
+ end
99
+
100
+ def partial!(model = nil, *args, partial: nil, collection: nil, **options)
101
+ if args.none? && _is_active_model?(model)
156
102
  # TODO: Find where it is being used
157
- _render_active_model_partial args.first
158
- elsif args.first.is_a?(::Hash)
159
- _set_ref(args.first[:partial].split("/").last)
103
+ _render_active_model_partial model
160
104
  else
161
- @collection = true if args[1].key?(:collection)
162
- _set_ref(args.first&.split("/")&.last)
105
+ _set_ref(partial || model, array: collection&.any?)
163
106
  end
164
107
  end
165
108
 
166
109
  def merge!(object)
167
- hash_or_array = ::Jbuilder === object ? object.attributes! : object
168
- hash_or_array = _format_keys(hash_or_array)
169
- if hash_or_array.is_a?(::Hash)
170
- hash_or_array = hash_or_array.each_with_object({}) do |(key, value), a|
171
- result = _schema(key, value)
172
- result = _set_description(key, result) if model_scope.model
173
- a[key] = result
174
- end
175
- end
176
- @attributes = _merge_values(@attributes, hash_or_array)
110
+ object = object.to_h { [_1, _schema(_1, _2)] } if object.is_a?(::Hash)
111
+ super
177
112
  end
178
113
 
179
114
  def cache!(key = nil, **options)
180
115
  yield # TODO: Our schema generation breaks Jbuilder's fragment caching.
181
116
  end
182
117
 
183
- def method_missing(*args, **options, &block) # standard:disable Style/MissingRespondToMissing
184
- # TODO: Remove once Jbuilder passes keyword arguments along to `set!` in its `method_missing`.
185
- set!(*args, **options, &block)
186
- end
187
-
188
118
  private
189
119
 
190
- def _with_model_scope(object: nil, object_title: nil, object_description: nil, **)
191
- old_model_scope, @model_scope = @model_scope, ModelScope.new(model: object.class, title: object_title, description: object_description) if object
120
+ def _extract_possible_keys(value)
121
+ value.first.as_json.keys if _is_collection?(value) && _is_active_model?(value.first)
122
+ end
123
+
124
+ def _with_schema_overrides(overrides)
125
+ old_schema_overrides, @schema_overrides = @schema_overrides, overrides if overrides
192
126
  yield
193
127
  ensure
194
- @model_scope = old_model_scope if object
128
+ @schema_overrides = old_schema_overrides if overrides
195
129
  end
196
130
 
197
- def _object(**attributes)
131
+ def _object(attributes)
198
132
  {
199
133
  type: :object,
200
- title: model_scope.i18n_title,
201
- description: model_scope.i18n_description,
134
+ title: @configuration.title,
135
+ description: @configuration.description,
202
136
  required: _required!(attributes.keys),
203
137
  properties: attributes
204
138
  }
205
139
  end
206
140
 
207
141
  def _set_description(key, value)
208
- unless value.key?(:description)
209
- description = model_scope.translate_field(key)
210
- value = {description: description}.merge! value
142
+ if !value.key?(:description) && @configuration.object
143
+ value[:description] = @configuration.translate_field(key)
211
144
  end
212
- value
213
145
  end
214
146
 
215
- def _set_ref(component)
216
- component_path = "#/#{::Jbuilder::Schema.components_path}/#{component}"
147
+ def _set_ref(part, array: false)
148
+ ref = {"$ref": "#/#{::Jbuilder::Schema.components_path}/#{part.split("/").last}"}
217
149
 
218
- if @inline_array
219
- if @collection
220
- _set_value(:type, :array)
221
- _set_value(:items, {:$ref => component_path})
222
- else
223
- _set_value(:type, :object)
224
- _set_value(:$ref, component_path)
225
- end
150
+ if array
151
+ _attributes.merge! type: :array, items: ref
226
152
  else
227
- @type = :array
228
- _set_value(:items, {:$ref => component_path})
153
+ _attributes.merge! type: :object, **ref
229
154
  end
230
155
  end
231
156
 
157
+ def _attributes
158
+ @attributes = {} if _blank?
159
+ @attributes
160
+ end
161
+
232
162
  FORMATS = {::DateTime => "date-time", ::ActiveSupport::TimeWithZone => "date-time", ::Date => "date", ::Time => "time"}
233
163
 
234
164
  def _schema(key, value, **options)
165
+ options = @schema_overrides&.dig(key).to_h if options.empty?
166
+
235
167
  unless options[:type]
236
168
  options[:type] = _primitive_type value
237
169
 
@@ -243,10 +175,11 @@ class Jbuilder::Schema
243
175
  format = FORMATS[value.class] and options[:format] ||= format
244
176
  end
245
177
 
246
- if (model = model_scope.model) && (defined_enum = model.try(:defined_enums)&.dig(key.to_s))
178
+ if (klass = @configuration.object&.class) && (defined_enum = klass.try(:defined_enums)&.dig(key.to_s))
247
179
  options[:enum] = defined_enum.keys
248
180
  end
249
181
 
182
+ _set_description key, options
250
183
  options
251
184
  end
252
185
 
@@ -261,24 +194,14 @@ class Jbuilder::Schema
261
194
  end
262
195
  end
263
196
 
264
- def _make_array(collection, *args, schema: {}, &block)
265
- if collection.nil?
266
- []
267
- elsif block
268
- _map_collection(collection, &block)
269
- elsif args.any?
270
- _map_collection(collection) { |element| extract! element, *args, schema: schema }
271
- else
272
- _format_keys(collection.to_a)
273
- end
274
- end
275
-
276
- def _is_collection_array?(object)
277
- object.is_a?(::Array) && object.all? { _is_active_model? _1 }
197
+ def _set_value(key, value)
198
+ value = _schema(key, value) unless value.is_a?(::Hash) && value.key?(:type)
199
+ _set_description(key, value)
200
+ super
278
201
  end
279
202
 
280
203
  def _required!(keys)
281
- presence_validated_attributes = model_scope.model.try(:validators).to_a.flat_map { _1.attributes if _1.is_a?(::ActiveRecord::Validations::PresenceValidator) }
204
+ presence_validated_attributes = @configuration.object&.class.try(:validators).to_a.flat_map { _1.attributes if _1.is_a?(::ActiveRecord::Validations::PresenceValidator) }
282
205
  keys & [_key(:id), *presence_validated_attributes.map { _key _1 }]
283
206
  end
284
207
 
@@ -286,22 +209,6 @@ class Jbuilder::Schema
286
209
  # Jbuilder methods
287
210
  ###
288
211
 
289
- def _extract_hash_values(object, attributes, schema:)
290
- attributes.each do |key|
291
- result = _schema(key, _format_keys(object.fetch(key)), **schema[key] || {})
292
- result = _set_description(key, result) if model_scope.model
293
- _set_value key, result
294
- end
295
- end
296
-
297
- def _extract_method_values(object, attributes, schema:)
298
- attributes.each do |key|
299
- result = _schema(key, _format_keys(object.public_send(key)), **schema[key] || {})
300
- result = _set_description(key, result) if model_scope.model
301
- _set_value key, result
302
- end
303
- end
304
-
305
212
  def _map_collection(collection)
306
213
  super.first
307
214
  end
@@ -311,20 +218,8 @@ class Jbuilder::Schema
311
218
  raise NullError.build(key) if current_value.nil?
312
219
 
313
220
  value = _scope { yield self }
314
- value = _object(**value) unless value.values_at("type", :type).any?(:array) || value.key?(:$ref) || value.key?("$ref")
221
+ value = _object(value) unless value[:type] == :array || value.key?(:$ref)
315
222
  _merge_values(current_value, value)
316
223
  end
317
224
  end
318
225
  end
319
-
320
- class Jbuilder
321
- module SkipFormatting
322
- SCHEMA_KEYS = %i[type items properties]
323
-
324
- def format(key)
325
- SCHEMA_KEYS.include?(key) ? key : super
326
- end
327
- end
328
-
329
- KeyFormatter.prepend SkipFormatting
330
- end
@@ -1,4 +1,4 @@
1
1
  # We can't use the standard `Jbuilder::Schema::VERSION =` because
2
2
  # `Jbuilder` isn't a regular module namespace, but a class …which also loads Active Support.
3
3
  # So we use trickery, and assign the proper version once `jbuilder/schema.rb` is loaded.
4
- JBUILDER_SCHEMA_VERSION = "2.0.4"
4
+ JBUILDER_SCHEMA_VERSION = "2.1.0"
@@ -1,10 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/core_ext/module/delegation"
3
+ require "jbuilder"
4
4
  require "jbuilder/schema/version"
5
+ require "active_support/core_ext/module/delegation"
5
6
 
6
7
  class Jbuilder::Schema
7
- VERSION = "2.0.3" # TODO Fix this. It's throwing errors when including the Ruby gem in downstream projects.
8
+ VERSION = JBUILDER_SCHEMA_VERSION # See `jbuilder/schema/version.rb`
8
9
 
9
10
  module IgnoreSchemaMeta
10
11
  ::Jbuilder.prepend self
@@ -12,6 +13,18 @@ class Jbuilder::Schema
12
13
  def method_missing(*args, schema: nil, **options, &block) # standard:disable Style/MissingRespondToMissing
13
14
  super(*args, **options, &block)
14
15
  end
16
+
17
+ def set!(*args, schema: nil, **options, &block)
18
+ super(*args, **options, &block)
19
+ end
20
+
21
+ def array!(*args, schema: nil, **options, &block)
22
+ super(*args, **options, &block)
23
+ end
24
+
25
+ def extract!(*args, schema: nil, **options, &block)
26
+ super(*args, **options, &block)
27
+ end
15
28
  end
16
29
 
17
30
  singleton_class.alias_method :configure, :tap
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jbuilder-schema
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.4
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yuri Sidorov
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-12-07 00:00:00.000000000 Z
11
+ date: 2023-01-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: jbuilder
@@ -76,7 +76,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
76
76
  - !ruby/object:Gem::Version
77
77
  version: '0'
78
78
  requirements: []
79
- rubygems_version: 3.3.16
79
+ rubygems_version: 3.4.1
80
80
  signing_key:
81
81
  specification_version: 4
82
82
  summary: Generate JSON Schema from Jbuilder files