jbuilder-schema 2.0.4 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
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