blueprinter 1.2.1 โ 1.3.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 +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +45 -5
- data/lib/blueprinter/association.rb +2 -2
- data/lib/blueprinter/base.rb +21 -3
- data/lib/blueprinter/configuration.rb +19 -2
- data/lib/blueprinter/extractors/association_extractor.rb +23 -5
- data/lib/blueprinter/extractors/block_extractor.rb +25 -1
- data/lib/blueprinter/formatters/date_time_formatter.rb +1 -1
- data/lib/blueprinter/rendering.rb +21 -12
- data/lib/blueprinter/version.rb +1 -1
- data/lib/blueprinter/view_collection.rb +87 -11
- data/lib/blueprinter.rb +1 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 72b769b814788e3b67f5cf11fc9e15322f20240cf436eb81004d7014b1a89755
|
|
4
|
+
data.tar.gz: a975d061a805def418f281ef7012f3aa6031448c15e092578cab127a8c06e3bc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2ff9c8a8af880ac84e7352b7e989dc84b05573d494a105879d65661a0f2db6441b69ff799531e4ccd2cc4f14e31cfb9e6acd416f43d2f70f8a6f9edde4dc4fe8
|
|
7
|
+
data.tar.gz: f46db4bfc8eaa1c3d60132e4c69872bc20c116c59033e36916e5dbaeebdc2ecac0c68d3c4abab80d7f45e6adb835602f0540d6c3a4e0582296a1d5ec075c0883
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
## Unreleased
|
|
2
2
|
--
|
|
3
3
|
|
|
4
|
+
## 1.3.0 - 2026/04/14
|
|
5
|
+
- ๐ [FEATURE] Adds support for `Symbol#to_proc` syntax in fields and identifiers. See [#546](https://github.com/procore-oss/blueprinter/pull/546). Thanks to [@tob1k](https://github.com/tob1k).
|
|
6
|
+
- ๐ [FEATURE] Adds support for Proc in association `:options` parameter, enabling dynamic options based on the parent object. See [#579](https://github.com/procore-oss/blueprinter/pull/579). Thanks to [@lessthanjacob](https://github.com/lessthanjacob).
|
|
7
|
+
- ๐ [BUGFIX] Passes options as keywords during block extraction. See [#521](https://github.com/procore-oss/blueprinter/pull/521). Thanks to [@tylerhunt](https://github.com/tylerhunt).
|
|
8
|
+
- ๐ [BUGFIX] Ensures `Deprecation` module gets loaded before it's used. See [#549](https://github.com/procore-oss/blueprinter/pull/549). Thanks to [@jhollinger](https://github.com/jhollinger).
|
|
9
|
+
- ๐ [BUGFIX] Fixes `ViewCollection` cache invalidation after view block evaluation, preventing silent loss of mutations when `reflections` triggers early cache compilation. See [#579](https://github.com/procore-oss/blueprinter/pull/579). Thanks to [@lessthanjacob](https://github.com/lessthanjacob).
|
|
10
|
+
- ๐ [REFACTOR] Caches `ViewCollection` and transformers to reduce object allocations and improve rendering performance. See [#566](https://github.com/procore-oss/blueprinter/pull/566). Thanks to [@lessthanjacob](https://github.com/lessthanjacob).
|
|
11
|
+
|
|
4
12
|
## 1.2.1 - 2025/09/11
|
|
5
13
|
- ๐ [BUGFIX] Adds back `Blueprinter.prepare` method with a deprecated warning. This method was previously public, but was removed as part of **1.2.0**.
|
|
6
14
|
|
data/README.md
CHANGED
|
@@ -1051,7 +1051,7 @@ assoc[:category].options
|
|
|
1051
1051
|
<details>
|
|
1052
1052
|
<summary>Extensions</summary>
|
|
1053
1053
|
|
|
1054
|
-
Blueprinter
|
|
1054
|
+
Blueprinter provides an extension system that enables certain behavior to be modified as needed.
|
|
1055
1055
|
|
|
1056
1056
|
```ruby
|
|
1057
1057
|
Blueprinter.configure do |config|
|
|
@@ -1060,13 +1060,53 @@ Blueprinter.configure do |config|
|
|
|
1060
1060
|
end
|
|
1061
1061
|
```
|
|
1062
1062
|
|
|
1063
|
-
|
|
1063
|
+
#### Available Hooks
|
|
1064
1064
|
|
|
1065
|
-
*
|
|
1065
|
+
* **pre_render** - Intercept the object before rendering begins. This allows you to modify, transform, or replace the object that will be serialized.
|
|
1066
1066
|
|
|
1067
|
-
|
|
1067
|
+
#### Creating Extensions
|
|
1068
|
+
|
|
1069
|
+
To create an extension, simply subclass `Blueprinter::Extension` and override the method representing the desired hook:
|
|
1070
|
+
|
|
1071
|
+
```ruby
|
|
1072
|
+
class ObfuscateNameExtension < Blueprinter::Extension
|
|
1073
|
+
def pre_render(object, blueprint, view, options)
|
|
1074
|
+
return object unless object.respond_to?(:name)
|
|
1075
|
+
|
|
1076
|
+
modified_object = object.dup
|
|
1077
|
+
modified_object.name = ObsfuscateName.call(modified_object.name)
|
|
1078
|
+
|
|
1079
|
+
modified_object
|
|
1080
|
+
end
|
|
1081
|
+
end
|
|
1082
|
+
```
|
|
1083
|
+
|
|
1084
|
+
#### Extension Execution Order
|
|
1085
|
+
|
|
1086
|
+
Extensions are executed in the order they are added to the configuration. Each extension receives the result from the previous extension, allowing for chained transformations:
|
|
1087
|
+
|
|
1088
|
+
```ruby
|
|
1089
|
+
Blueprinter.configure do |config|
|
|
1090
|
+
config.extensions << SecurityExtension.new # Runs first
|
|
1091
|
+
config.extensions << AssociationLoaderExtension.new # Runs second
|
|
1092
|
+
config.extensions << UserEnrichmentExtension.new # Runs third
|
|
1093
|
+
end
|
|
1094
|
+
```
|
|
1095
|
+
|
|
1096
|
+
#### Gem Extensions
|
|
1097
|
+
|
|
1098
|
+
Gems can be created that enrich `blueprinter`'s core functionality via extensions.
|
|
1099
|
+
|
|
1100
|
+
##### Procore Supported Gems
|
|
1101
|
+
|
|
1102
|
+
* [blueprinter-activerecord](https://github.com/procore-oss/blueprinter-activerecord) - Provides ActiveRecord-specific optimizations and features
|
|
1103
|
+
|
|
1104
|
+
##### Community Gems
|
|
1105
|
+
|
|
1106
|
+
> **_NOTE:_** The following are not officially maintained/supported by Procore OSS.
|
|
1107
|
+
|
|
1108
|
+
* [blueprinter_schema](https://github.com/thisismydesign/blueprinter_schema) - Create JSON Schemas from Blueprinter Serializers
|
|
1068
1109
|
|
|
1069
|
-
* [blueprinter-activerecord](https://github.com/procore-oss/blueprinter-activerecord)
|
|
1070
1110
|
</details>
|
|
1071
1111
|
|
|
1072
1112
|
<details>
|
data/lib/blueprinter/base.rb
CHANGED
|
@@ -45,7 +45,7 @@ module Blueprinter
|
|
|
45
45
|
# end
|
|
46
46
|
#
|
|
47
47
|
# @return [Field] A Field object
|
|
48
|
-
def identifier(method, name: method, extractor: Blueprinter.configuration.
|
|
48
|
+
def identifier(method, name: method, extractor: Blueprinter.configuration.default_extractor, &block)
|
|
49
49
|
view_collection[:identifier] << Field.new(
|
|
50
50
|
method,
|
|
51
51
|
name,
|
|
@@ -116,7 +116,7 @@ module Blueprinter
|
|
|
116
116
|
current_view << Field.new(
|
|
117
117
|
method,
|
|
118
118
|
options.fetch(:name) { method },
|
|
119
|
-
options.fetch(:extractor) { Blueprinter.configuration.
|
|
119
|
+
options.fetch(:extractor) { Blueprinter.configuration.default_extractor },
|
|
120
120
|
self,
|
|
121
121
|
options.merge(block:)
|
|
122
122
|
)
|
|
@@ -133,6 +133,9 @@ module Blueprinter
|
|
|
133
133
|
# JSON output.
|
|
134
134
|
# @option options [Symbol] :view Specify the view to use or fall back to
|
|
135
135
|
# to the :default view.
|
|
136
|
+
# @option options [Hash, Proc] :options Additional options to merge into the
|
|
137
|
+
# options hash passed to the nested blueprint. Can be a static Hash or a Proc
|
|
138
|
+
# that receives the parent object and returns a Hash.
|
|
136
139
|
# @yield [object, options] The object and the options passed to render are
|
|
137
140
|
# also yielded to the block.
|
|
138
141
|
#
|
|
@@ -150,6 +153,16 @@ module Blueprinter
|
|
|
150
153
|
# end
|
|
151
154
|
# end
|
|
152
155
|
#
|
|
156
|
+
# @example Passing static options to the nested blueprint.
|
|
157
|
+
# class UserBlueprint < Blueprinter::Base
|
|
158
|
+
# association :vehicles, blueprint: VehiclesBlueprint, options: { show_owner: true }
|
|
159
|
+
# end
|
|
160
|
+
#
|
|
161
|
+
# @example Passing dynamic options based on the parent object.
|
|
162
|
+
# class UserBlueprint < Blueprinter::Base
|
|
163
|
+
# association :vehicles, blueprint: VehiclesBlueprint, options: ->(user) { { owner_name: user.name } }
|
|
164
|
+
# end
|
|
165
|
+
#
|
|
153
166
|
# @return [Association] An object
|
|
154
167
|
# @raise [Blueprinter::Errors::InvalidBlueprint] if provided blueprint is not valid
|
|
155
168
|
def association(method, options = {}, &block)
|
|
@@ -159,7 +172,7 @@ module Blueprinter
|
|
|
159
172
|
current_view << Association.new(
|
|
160
173
|
method:,
|
|
161
174
|
name: options.fetch(:name) { method },
|
|
162
|
-
extractor: options.fetch(:extractor) {
|
|
175
|
+
extractor: options.fetch(:extractor) { association_extractor },
|
|
163
176
|
blueprint: options.fetch(:blueprint),
|
|
164
177
|
parent_blueprint: self,
|
|
165
178
|
view: options.fetch(:view, :default),
|
|
@@ -329,6 +342,7 @@ module Blueprinter
|
|
|
329
342
|
self.view_scope = view_collection[view_name]
|
|
330
343
|
view_collection[:default].track_definition_order(view_name)
|
|
331
344
|
yield
|
|
345
|
+
view_collection.invalidate_cache!
|
|
332
346
|
self.view_scope = view_collection[:default]
|
|
333
347
|
end
|
|
334
348
|
|
|
@@ -369,6 +383,10 @@ module Blueprinter
|
|
|
369
383
|
def inherited(subclass)
|
|
370
384
|
subclass.send(:view_collection).inherit(view_collection)
|
|
371
385
|
end
|
|
386
|
+
|
|
387
|
+
def association_extractor
|
|
388
|
+
@_association_extractor ||= AssociationExtractor.new
|
|
389
|
+
end
|
|
372
390
|
end
|
|
373
391
|
end
|
|
374
392
|
end
|
|
@@ -12,7 +12,6 @@ module Blueprinter
|
|
|
12
12
|
:datetime_format,
|
|
13
13
|
:default_transformers,
|
|
14
14
|
:deprecations,
|
|
15
|
-
:extractor_default,
|
|
16
15
|
:field_default,
|
|
17
16
|
:generator,
|
|
18
17
|
:if,
|
|
@@ -20,7 +19,7 @@ module Blueprinter
|
|
|
20
19
|
:sort_fields_by,
|
|
21
20
|
:unless
|
|
22
21
|
)
|
|
23
|
-
attr_reader :extensions
|
|
22
|
+
attr_reader :extensions, :extractor_default
|
|
24
23
|
|
|
25
24
|
VALID_CALLABLES = %i[if unless].freeze
|
|
26
25
|
|
|
@@ -59,5 +58,23 @@ module Blueprinter
|
|
|
59
58
|
def valid_callable?(callable_name)
|
|
60
59
|
VALID_CALLABLES.include?(callable_name)
|
|
61
60
|
end
|
|
61
|
+
|
|
62
|
+
# @param extractor [Class<Blueprinter::AutoExtractor>]
|
|
63
|
+
def extractor_default=(extractor)
|
|
64
|
+
reset_default_extractor!
|
|
65
|
+
|
|
66
|
+
@extractor_default = extractor
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# @return [Blueprinter::AutoExtractor]
|
|
70
|
+
def default_extractor
|
|
71
|
+
@_default_extractor ||= extractor_default.new
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def reset_default_extractor!
|
|
77
|
+
@_default_extractor = nil
|
|
78
|
+
end
|
|
62
79
|
end
|
|
63
80
|
end
|
|
@@ -9,23 +9,41 @@ module Blueprinter
|
|
|
9
9
|
include EmptyTypes
|
|
10
10
|
|
|
11
11
|
def initialize
|
|
12
|
-
@extractor = Blueprinter.configuration.
|
|
12
|
+
@extractor = Blueprinter.configuration.default_extractor
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
def extract(association_name, object, local_options, options = {})
|
|
16
|
-
options_without_default = options.
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
options_without_default = if options.key?(:default) || options.key?(:default_if)
|
|
17
|
+
options.except(:default, :default_if)
|
|
18
|
+
else
|
|
19
|
+
options
|
|
20
|
+
end
|
|
21
|
+
|
|
19
22
|
value = @extractor.extract(association_name, object, local_options, options_without_default)
|
|
20
23
|
return default_value(options) if use_default_value?(value, options[:default_if])
|
|
21
24
|
|
|
25
|
+
# Merge in association options - supports both static Hash and dynamic Proc
|
|
26
|
+
local_options = merge_association_options(local_options, options[:options], object)
|
|
27
|
+
|
|
22
28
|
view = options[:view] || :default
|
|
23
29
|
blueprint = association_blueprint(options[:blueprint], value)
|
|
24
|
-
blueprint.hashify(value, view_name: view, local_options:
|
|
30
|
+
blueprint.hashify(value, view_name: view, local_options:)
|
|
25
31
|
end
|
|
26
32
|
|
|
27
33
|
private
|
|
28
34
|
|
|
35
|
+
def merge_association_options(local_options, association_options, object)
|
|
36
|
+
return local_options unless association_options
|
|
37
|
+
|
|
38
|
+
additional_options = if association_options.respond_to?(:call)
|
|
39
|
+
association_options.call(object)
|
|
40
|
+
else
|
|
41
|
+
association_options
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
local_options.merge(additional_options)
|
|
45
|
+
end
|
|
46
|
+
|
|
29
47
|
def default_value(association_options)
|
|
30
48
|
return association_options.fetch(:default) if association_options.key?(:default)
|
|
31
49
|
|
|
@@ -6,7 +6,31 @@ module Blueprinter
|
|
|
6
6
|
# @api private
|
|
7
7
|
class BlockExtractor < Extractor
|
|
8
8
|
def extract(_field_name, object, local_options, options = {})
|
|
9
|
-
options[:block]
|
|
9
|
+
block = options[:block]
|
|
10
|
+
|
|
11
|
+
# Symbol#to_proc creates procs with signature [[:req], [:rest]]
|
|
12
|
+
# These procs forward ALL arguments to the method, which causes
|
|
13
|
+
# issues when we call block.call(object, local_options) because
|
|
14
|
+
# it becomes object.method_name(local_options), and most methods
|
|
15
|
+
# don't accept extra arguments.
|
|
16
|
+
#
|
|
17
|
+
# For Symbol#to_proc, we only pass the object.
|
|
18
|
+
# For regular blocks, we pass both object and local_options.
|
|
19
|
+
if symbol_to_proc?(block)
|
|
20
|
+
block.call(object)
|
|
21
|
+
else
|
|
22
|
+
block.call(object, **local_options)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def symbol_to_proc?(block)
|
|
29
|
+
# Symbol#to_proc has a characteristic signature:
|
|
30
|
+
# - Parameters: [[:req], [:rest]] (one required + rest args)
|
|
31
|
+
# - This is different from regular blocks which typically have
|
|
32
|
+
# optional parameters like [[:opt, :obj], [:opt, :options]]
|
|
33
|
+
block.parameters == [[:req], [:rest]]
|
|
10
34
|
end
|
|
11
35
|
end
|
|
12
36
|
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'blueprinter/errors/invalid_root'
|
|
4
4
|
require 'blueprinter/errors/meta_requires_root'
|
|
5
|
+
require 'blueprinter/deprecation'
|
|
5
6
|
|
|
6
7
|
module Blueprinter
|
|
7
8
|
# Encapsulates the rendering logic for Blueprinter.
|
|
@@ -29,7 +30,7 @@ module Blueprinter
|
|
|
29
30
|
#
|
|
30
31
|
# @return [String] JSON formatted String
|
|
31
32
|
def render(object, options = {})
|
|
32
|
-
jsonify(build_result(object
|
|
33
|
+
jsonify(build_result(object:, options:))
|
|
33
34
|
end
|
|
34
35
|
|
|
35
36
|
# Generates a Hash representation of the provided object.
|
|
@@ -54,7 +55,7 @@ module Blueprinter
|
|
|
54
55
|
#
|
|
55
56
|
# @return [Hash]
|
|
56
57
|
def render_as_hash(object, options = {})
|
|
57
|
-
build_result(object
|
|
58
|
+
build_result(object:, options:)
|
|
58
59
|
end
|
|
59
60
|
|
|
60
61
|
# Generates a JSONified hash.
|
|
@@ -79,7 +80,7 @@ module Blueprinter
|
|
|
79
80
|
#
|
|
80
81
|
# @return [Hash]
|
|
81
82
|
def render_as_json(object, options = {})
|
|
82
|
-
build_result(object
|
|
83
|
+
build_result(object:, options:).as_json
|
|
83
84
|
end
|
|
84
85
|
|
|
85
86
|
# Converts an object into a Hash representation based on provided view.
|
|
@@ -121,35 +122,43 @@ module Blueprinter
|
|
|
121
122
|
attr_reader :blueprint, :options
|
|
122
123
|
|
|
123
124
|
def prepare_data(object, view_name, local_options)
|
|
125
|
+
# Since we're currently providing the current view in the local_options hash when we extract fields, we can merge
|
|
126
|
+
# it in ahead of time to avoid allocating a new hash for every field extraction.
|
|
127
|
+
local_options_with_view = local_options.merge(view: view_name)
|
|
128
|
+
|
|
124
129
|
if array_like?(object)
|
|
125
130
|
object.map do |obj|
|
|
126
131
|
object_to_hash(
|
|
127
132
|
obj,
|
|
128
|
-
view_name
|
|
129
|
-
local_options:
|
|
133
|
+
view_name:,
|
|
134
|
+
local_options: local_options_with_view
|
|
130
135
|
)
|
|
131
136
|
end
|
|
132
137
|
else
|
|
133
138
|
object_to_hash(
|
|
134
139
|
object,
|
|
135
|
-
view_name
|
|
136
|
-
local_options:
|
|
140
|
+
view_name:,
|
|
141
|
+
local_options: local_options_with_view
|
|
137
142
|
)
|
|
138
143
|
end
|
|
139
144
|
end
|
|
140
145
|
|
|
141
146
|
def object_to_hash(object, view_name:, local_options:)
|
|
142
|
-
result_hash =
|
|
147
|
+
result_hash = {}
|
|
148
|
+
|
|
149
|
+
view_collection.fields_for(view_name).each do |field|
|
|
143
150
|
next if field.skip?(field.name, object, local_options)
|
|
144
151
|
|
|
145
|
-
value = field.extract(object, local_options
|
|
152
|
+
value = field.extract(object, local_options)
|
|
146
153
|
next if value.nil? && field.options[:exclude_if_nil]
|
|
147
154
|
|
|
148
|
-
|
|
155
|
+
result_hash[field.name] = value
|
|
149
156
|
end
|
|
157
|
+
|
|
150
158
|
view_collection.transformers(view_name).each do |transformer|
|
|
151
159
|
transformer.transform(result_hash, object, local_options)
|
|
152
160
|
end
|
|
161
|
+
|
|
153
162
|
result_hash
|
|
154
163
|
end
|
|
155
164
|
|
|
@@ -172,11 +181,11 @@ module Blueprinter
|
|
|
172
181
|
end
|
|
173
182
|
|
|
174
183
|
def build_result(object:, options:)
|
|
175
|
-
view_name = options
|
|
184
|
+
view_name = options[:view] || :default
|
|
176
185
|
|
|
177
186
|
prepared_object = hashify(
|
|
178
187
|
object,
|
|
179
|
-
view_name
|
|
188
|
+
view_name:,
|
|
180
189
|
local_options: options.except(:view, :root, :meta)
|
|
181
190
|
)
|
|
182
191
|
object_with_root = apply_root_key(
|
data/lib/blueprinter/version.rb
CHANGED
|
@@ -4,6 +4,21 @@ require 'blueprinter/view'
|
|
|
4
4
|
|
|
5
5
|
module Blueprinter
|
|
6
6
|
# @api private
|
|
7
|
+
#
|
|
8
|
+
# ViewCollection manages the views defined in a Blueprint, along with their fields and transformers.
|
|
9
|
+
#
|
|
10
|
+
# To improve performance, the "structure" of each view is cached. Once the first view is accessed, we prewarm the
|
|
11
|
+
# cache for _all_ views (while this isn't the _most_ optimal approach, the overhead is negligible, and allows the
|
|
12
|
+
# caching logic to be quite simple).
|
|
13
|
+
#
|
|
14
|
+
# Thread Safety: view data is lazily compiled into a frozen snapshot on first access, and is assigned atomically. A
|
|
15
|
+
# mutex serializes the compilation itself to prevent duplicate work. After compilation, reads bypass the mutex entirely.
|
|
16
|
+
#
|
|
17
|
+
# Future optimization: a public compile! method could allow eager compilation at boot time
|
|
18
|
+
# (e.g. in a Rails after_initialize hook) to move the first-render compilation cost (~10ยตs per
|
|
19
|
+
# blueprint) out of the request path entirely.
|
|
20
|
+
#
|
|
21
|
+
# rubocop:disable Metrics/ClassLength
|
|
7
22
|
class ViewCollection
|
|
8
23
|
attr_reader :views, :sort_by_definition
|
|
9
24
|
|
|
@@ -13,37 +28,60 @@ module Blueprinter
|
|
|
13
28
|
default: View.new(:default)
|
|
14
29
|
}
|
|
15
30
|
@sort_by_definition = Blueprinter.configuration.sort_fields_by.eql?(:definition)
|
|
31
|
+
@cache_mutex = Mutex.new
|
|
32
|
+
@cache = nil
|
|
16
33
|
end
|
|
17
34
|
|
|
18
35
|
def inherit(view_collection)
|
|
19
36
|
view_collection.views.each do |view_name, view|
|
|
20
37
|
self[view_name].inherit(view)
|
|
21
38
|
end
|
|
39
|
+
|
|
40
|
+
invalidate_cache!
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Clears the compiled field/transformer cache. This should be called after any mutation to a view's fields,
|
|
44
|
+
# exclusions, or transformers to ensure the cache reflects the current state.
|
|
45
|
+
def invalidate_cache!
|
|
46
|
+
@cache = nil
|
|
22
47
|
end
|
|
23
48
|
|
|
49
|
+
# @param [String] view_name
|
|
50
|
+
# @return [Boolean] true if the view exists, false otherwise
|
|
24
51
|
def view?(view_name)
|
|
25
52
|
views.key? view_name
|
|
26
53
|
end
|
|
27
54
|
|
|
55
|
+
# Returns an array of Field objects for the provided View.
|
|
56
|
+
# @param [String] view_name
|
|
57
|
+
# @return [Array<Field>]
|
|
28
58
|
def fields_for(view_name)
|
|
29
|
-
|
|
59
|
+
ensure_cached!
|
|
30
60
|
|
|
31
|
-
fields
|
|
32
|
-
sorted_fields = sort_by_definition ? sort_by_def(view_name, fields) : fields.values.sort_by(&:name)
|
|
33
|
-
|
|
34
|
-
(identifier_fields + sorted_fields).tap do |fields_array|
|
|
35
|
-
fields_array.reject! { |field| excluded_fields.include?(field.name) }
|
|
36
|
-
end
|
|
61
|
+
@cache[:fields][view_name]
|
|
37
62
|
end
|
|
38
63
|
|
|
64
|
+
# Returns an array of Transformer objects for the provided View.
|
|
65
|
+
# @param [String] view_name
|
|
66
|
+
# @return [Array<Transformer>]
|
|
39
67
|
def transformers(view_name)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
68
|
+
ensure_cached!
|
|
69
|
+
|
|
70
|
+
@cache[:transformers][view_name]
|
|
43
71
|
end
|
|
44
72
|
|
|
73
|
+
# @param [String] view_name
|
|
74
|
+
# @return [View]
|
|
45
75
|
def [](view_name)
|
|
46
|
-
@views[view_name]
|
|
76
|
+
return @views[view_name] if @views.key?(view_name)
|
|
77
|
+
|
|
78
|
+
@cache_mutex.synchronize do
|
|
79
|
+
unless @views.key?(view_name)
|
|
80
|
+
@views[view_name] = View.new(view_name)
|
|
81
|
+
invalidate_cache!
|
|
82
|
+
end
|
|
83
|
+
@views[view_name]
|
|
84
|
+
end
|
|
47
85
|
end
|
|
48
86
|
|
|
49
87
|
private
|
|
@@ -52,6 +90,43 @@ module Blueprinter
|
|
|
52
90
|
views[:identifier].fields.values
|
|
53
91
|
end
|
|
54
92
|
|
|
93
|
+
def ensure_cached!
|
|
94
|
+
# Fast path: no lock needed once the cache is populated (atomic reference read).
|
|
95
|
+
return if @cache
|
|
96
|
+
|
|
97
|
+
@cache_mutex.synchronize do
|
|
98
|
+
# Re-check after acquiring the lock; another thread may have built the cache first.
|
|
99
|
+
return if @cache
|
|
100
|
+
|
|
101
|
+
fields = {}
|
|
102
|
+
transformers = {}
|
|
103
|
+
|
|
104
|
+
views.each_key do |view_name|
|
|
105
|
+
fields[view_name] = build_fields_for(view_name).freeze
|
|
106
|
+
transformers[view_name] = build_transformers(view_name).freeze
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
@cache = { fields: fields.freeze, transformers: transformers.freeze }.freeze
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def build_fields_for(view_name)
|
|
114
|
+
return identifier_fields if view_name == :identifier
|
|
115
|
+
|
|
116
|
+
fields, excluded_fields = sortable_fields(view_name)
|
|
117
|
+
sorted_fields = sort_by_definition ? sort_by_def(view_name, fields) : fields.values.sort_by(&:name)
|
|
118
|
+
|
|
119
|
+
(identifier_fields + sorted_fields).tap do |fields_array|
|
|
120
|
+
fields_array.reject! { |field| excluded_fields.include?(field.name) }
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def build_transformers(view_name)
|
|
125
|
+
included_transformers = gather_transformers_from_included_views(view_name).reverse
|
|
126
|
+
all_transformers = [*views[:default].view_transformers, *included_transformers].uniq
|
|
127
|
+
all_transformers.empty? ? Blueprinter.configuration.default_transformers : all_transformers
|
|
128
|
+
end
|
|
129
|
+
|
|
55
130
|
# @param [String] view_name
|
|
56
131
|
# @return [Array<(Hash, Hash<String, NilClass>)>] fields, excluded_fields
|
|
57
132
|
def sortable_fields(view_name)
|
|
@@ -104,4 +179,5 @@ module Blueprinter
|
|
|
104
179
|
[*already_included_transformers, *current_view.view_transformers].uniq
|
|
105
180
|
end
|
|
106
181
|
end
|
|
182
|
+
# rubocop:enable Metrics/ClassLength
|
|
107
183
|
end
|
data/lib/blueprinter.rb
CHANGED
|
@@ -4,6 +4,7 @@ module Blueprinter
|
|
|
4
4
|
autoload :Base, 'blueprinter/base'
|
|
5
5
|
autoload :BlueprinterError, 'blueprinter/blueprinter_error'
|
|
6
6
|
autoload :Configuration, 'blueprinter/configuration'
|
|
7
|
+
autoload :Deprecation, 'blueprinter/deprecation'
|
|
7
8
|
autoload :Errors, 'blueprinter/errors'
|
|
8
9
|
autoload :Extension, 'blueprinter/extension'
|
|
9
10
|
autoload :Transformer, 'blueprinter/transformer'
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: blueprinter
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Procore Technologies, Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-04-17 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: Blueprinter is a JSON Object Presenter for Ruby that takes business objects
|
|
14
14
|
and breaks them down into simple hashes and serializes them to JSON. It can be used
|