blueprinter 1.1.2 → 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 +25 -5
- data/README.md +83 -5
- data/lib/blueprinter/association.rb +2 -2
- data/lib/blueprinter/base.rb +363 -419
- data/lib/blueprinter/blueprint_validator.rb +4 -8
- data/lib/blueprinter/configuration.rb +34 -7
- data/lib/blueprinter/errors/invalid_blueprint.rb +1 -1
- data/lib/blueprinter/errors/invalid_root.rb +13 -0
- data/lib/blueprinter/errors/meta_requires_root.rb +13 -0
- data/lib/blueprinter/extractors/association_extractor.rb +23 -5
- data/lib/blueprinter/extractors/block_extractor.rb +25 -1
- data/lib/blueprinter/field.rb +2 -6
- data/lib/blueprinter/formatters/date_time_formatter.rb +1 -1
- data/lib/blueprinter/reflection.rb +3 -3
- data/lib/blueprinter/rendering.rb +202 -0
- data/lib/blueprinter/version.rb +1 -1
- data/lib/blueprinter/view_collection.rb +87 -11
- data/lib/blueprinter.rb +3 -2
- data/lib/generators/blueprinter/blueprint_generator.rb +1 -1
- metadata +10 -8
- data/lib/blueprinter/helpers/base_helpers.rb +0 -104
- /data/lib/generators/blueprinter/templates/{blueprint.rb → blueprint.erb} +0 -0
|
@@ -10,14 +10,10 @@ module Blueprinter
|
|
|
10
10
|
# @return [Boolean] true if object is a valid Blueprint
|
|
11
11
|
# @raise [Blueprinter::Errors::InvalidBlueprint] if the object is not a valid Blueprint.
|
|
12
12
|
def validate!(blueprint)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
Errors::InvalidBlueprint,
|
|
18
|
-
"#{blueprint} is not a valid blueprint. Please ensure it subclasses Blueprinter::Base or is a Proc."
|
|
19
|
-
)
|
|
20
|
-
end
|
|
13
|
+
valid_blueprint?(blueprint) || raise(
|
|
14
|
+
Errors::InvalidBlueprint,
|
|
15
|
+
"#{blueprint} is not a valid blueprint. Please ensure it subclasses Blueprinter::Base or is a Proc."
|
|
16
|
+
)
|
|
21
17
|
end
|
|
22
18
|
|
|
23
19
|
private
|
|
@@ -6,8 +6,20 @@ require 'blueprinter/extractors/auto_extractor'
|
|
|
6
6
|
|
|
7
7
|
module Blueprinter
|
|
8
8
|
class Configuration
|
|
9
|
-
attr_accessor
|
|
10
|
-
|
|
9
|
+
attr_accessor(
|
|
10
|
+
:association_default,
|
|
11
|
+
:custom_array_like_classes,
|
|
12
|
+
:datetime_format,
|
|
13
|
+
:default_transformers,
|
|
14
|
+
:deprecations,
|
|
15
|
+
:field_default,
|
|
16
|
+
:generator,
|
|
17
|
+
:if,
|
|
18
|
+
:method,
|
|
19
|
+
:sort_fields_by,
|
|
20
|
+
:unless
|
|
21
|
+
)
|
|
22
|
+
attr_reader :extensions, :extractor_default
|
|
11
23
|
|
|
12
24
|
VALID_CALLABLES = %i[if unless].freeze
|
|
13
25
|
|
|
@@ -24,10 +36,7 @@ module Blueprinter
|
|
|
24
36
|
@extractor_default = AutoExtractor
|
|
25
37
|
@default_transformers = []
|
|
26
38
|
@custom_array_like_classes = []
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def extensions
|
|
30
|
-
@extensions ||= Extensions.new
|
|
39
|
+
@extensions = Extensions.new
|
|
31
40
|
end
|
|
32
41
|
|
|
33
42
|
def extensions=(list)
|
|
@@ -35,7 +44,7 @@ module Blueprinter
|
|
|
35
44
|
end
|
|
36
45
|
|
|
37
46
|
def array_like_classes
|
|
38
|
-
@
|
|
47
|
+
@_array_like_classes ||= [
|
|
39
48
|
Array,
|
|
40
49
|
defined?(ActiveRecord::Relation) && ActiveRecord::Relation,
|
|
41
50
|
*custom_array_like_classes
|
|
@@ -49,5 +58,23 @@ module Blueprinter
|
|
|
49
58
|
def valid_callable?(callable_name)
|
|
50
59
|
VALID_CALLABLES.include?(callable_name)
|
|
51
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
|
|
52
79
|
end
|
|
53
80
|
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'blueprinter/blueprinter_error'
|
|
4
|
+
|
|
5
|
+
module Blueprinter
|
|
6
|
+
module Errors
|
|
7
|
+
class InvalidRoot < BlueprinterError
|
|
8
|
+
def initialize(message = 'root key must be a Symbol or a String')
|
|
9
|
+
super
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'blueprinter/blueprinter_error'
|
|
4
|
+
|
|
5
|
+
module Blueprinter
|
|
6
|
+
module Errors
|
|
7
|
+
class MetaRequiresRoot < BlueprinterError
|
|
8
|
+
def initialize(message = 'adding metadata requires that a root key is set')
|
|
9
|
+
super
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
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.
|
|
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
|
data/lib/blueprinter/field.rb
CHANGED
|
@@ -26,15 +26,11 @@ module Blueprinter
|
|
|
26
26
|
private
|
|
27
27
|
|
|
28
28
|
def if_callable
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
@if_callable = callable_from(:if)
|
|
29
|
+
@_if_callable ||= callable_from(:if)
|
|
32
30
|
end
|
|
33
31
|
|
|
34
32
|
def unless_callable
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
@unless_callable = callable_from(:unless)
|
|
33
|
+
@_unless_callable ||= callable_from(:unless)
|
|
38
34
|
end
|
|
39
35
|
|
|
40
36
|
def callable_from(condition)
|
|
@@ -23,7 +23,7 @@ module Blueprinter
|
|
|
23
23
|
# @return [Hash<Symbol, Blueprinter::Reflection::View>]
|
|
24
24
|
#
|
|
25
25
|
def reflections
|
|
26
|
-
@
|
|
26
|
+
@_reflections ||= view_collection.views.transform_values do |view|
|
|
27
27
|
View.new(view.name, view_collection)
|
|
28
28
|
end
|
|
29
29
|
end
|
|
@@ -45,7 +45,7 @@ module Blueprinter
|
|
|
45
45
|
# @return [Hash<Symbol, Blueprinter::Reflection::Field>]
|
|
46
46
|
#
|
|
47
47
|
def fields
|
|
48
|
-
@
|
|
48
|
+
@_fields ||= @view_collection.fields_for(name).each_with_object({}) do |field, obj|
|
|
49
49
|
next if field.options[:association]
|
|
50
50
|
|
|
51
51
|
obj[field.name] = Field.new(field.method, field.name, field.options)
|
|
@@ -58,7 +58,7 @@ module Blueprinter
|
|
|
58
58
|
# @return [Hash<Symbol, Blueprinter::Reflection::Association>]
|
|
59
59
|
#
|
|
60
60
|
def associations
|
|
61
|
-
@
|
|
61
|
+
@_associations ||= @view_collection.fields_for(name).each_with_object({}) do |field, obj|
|
|
62
62
|
next unless field.options[:association]
|
|
63
63
|
|
|
64
64
|
blueprint = field.options.fetch(:blueprint)
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'blueprinter/errors/invalid_root'
|
|
4
|
+
require 'blueprinter/errors/meta_requires_root'
|
|
5
|
+
require 'blueprinter/deprecation'
|
|
6
|
+
|
|
7
|
+
module Blueprinter
|
|
8
|
+
# Encapsulates the rendering logic for Blueprinter.
|
|
9
|
+
module Rendering
|
|
10
|
+
include TypeHelpers
|
|
11
|
+
|
|
12
|
+
# Generates a JSON formatted String represantation of the provided object.
|
|
13
|
+
#
|
|
14
|
+
# @param object [Object] the Object to serialize.
|
|
15
|
+
# @param options [Hash] the options hash which requires a :view. Any
|
|
16
|
+
# additional key value pairs will be exposed during serialization.
|
|
17
|
+
# @option options [Symbol] :view Defaults to :default.
|
|
18
|
+
# The view name that corresponds to the group of
|
|
19
|
+
# fields to be serialized.
|
|
20
|
+
# @option options [Symbol|String] :root Defaults to nil.
|
|
21
|
+
# Render the json/hash with a root key if provided.
|
|
22
|
+
# @option options [Any] :meta Defaults to nil.
|
|
23
|
+
# Render the json/hash with a meta attribute with provided value
|
|
24
|
+
# if both root and meta keys are provided in the options hash.
|
|
25
|
+
#
|
|
26
|
+
# @example Generating JSON with an extended view
|
|
27
|
+
# post = Post.all
|
|
28
|
+
# Blueprinter::Base.render post, view: :extended
|
|
29
|
+
# # => "[{\"id\":1,\"title\":\"Hello\"},{\"id\":2,\"title\":\"My Day\"}]"
|
|
30
|
+
#
|
|
31
|
+
# @return [String] JSON formatted String
|
|
32
|
+
def render(object, options = {})
|
|
33
|
+
jsonify(build_result(object:, options:))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Generates a Hash representation of the provided object.
|
|
37
|
+
# Takes a required object and an optional view.
|
|
38
|
+
#
|
|
39
|
+
# @param object [Object] the Object to serialize upon.
|
|
40
|
+
# @param options [Hash] the options hash which requires a :view. Any
|
|
41
|
+
# additional key value pairs will be exposed during serialization.
|
|
42
|
+
# @option options [Symbol] :view Defaults to :default.
|
|
43
|
+
# The view name that corresponds to the group of
|
|
44
|
+
# fields to be serialized.
|
|
45
|
+
# @option options [Symbol|String] :root Defaults to nil.
|
|
46
|
+
# Render the json/hash with a root key if provided.
|
|
47
|
+
# @option options [Any] :meta Defaults to nil.
|
|
48
|
+
# Render the json/hash with a meta attribute with provided value
|
|
49
|
+
# if both root and meta keys are provided in the options hash.
|
|
50
|
+
#
|
|
51
|
+
# @example Generating a hash with an extended view
|
|
52
|
+
# post = Post.all
|
|
53
|
+
# Blueprinter::Base.render_as_hash post, view: :extended
|
|
54
|
+
# # => [{id:1, title: Hello},{id:2, title: My Day}]
|
|
55
|
+
#
|
|
56
|
+
# @return [Hash]
|
|
57
|
+
def render_as_hash(object, options = {})
|
|
58
|
+
build_result(object:, options:)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Generates a JSONified hash.
|
|
62
|
+
# Takes a required object and an optional view.
|
|
63
|
+
#
|
|
64
|
+
# @param object [Object] the Object to serialize upon.
|
|
65
|
+
# @param options [Hash] the options hash which requires a :view. Any
|
|
66
|
+
# additional key value pairs will be exposed during serialization.
|
|
67
|
+
# @option options [Symbol] :view Defaults to :default.
|
|
68
|
+
# The view name that corresponds to the group of
|
|
69
|
+
# fields to be serialized.
|
|
70
|
+
# @option options [Symbol|String] :root Defaults to nil.
|
|
71
|
+
# Render the json/hash with a root key if provided.
|
|
72
|
+
# @option options [Any] :meta Defaults to nil.
|
|
73
|
+
# Render the json/hash with a meta attribute with provided value
|
|
74
|
+
# if both root and meta keys are provided in the options hash.
|
|
75
|
+
#
|
|
76
|
+
# @example Generating a hash with an extended view
|
|
77
|
+
# post = Post.all
|
|
78
|
+
# Blueprinter::Base.render_as_json post, view: :extended
|
|
79
|
+
# # => [{"id" => "1", "title" => "Hello"},{"id" => "2", "title" => "My Day"}]
|
|
80
|
+
#
|
|
81
|
+
# @return [Hash]
|
|
82
|
+
def render_as_json(object, options = {})
|
|
83
|
+
build_result(object:, options:).as_json
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Converts an object into a Hash representation based on provided view.
|
|
87
|
+
#
|
|
88
|
+
# @param object [Object] the Object to convert into a Hash.
|
|
89
|
+
# @param view_name [Symbol] the view
|
|
90
|
+
# @param local_options [Hash] the options hash which requires a :view. Any
|
|
91
|
+
# additional key value pairs will be exposed during serialization.
|
|
92
|
+
# @return [Hash]
|
|
93
|
+
def hashify(object, view_name:, local_options:)
|
|
94
|
+
raise BlueprinterError, "View '#{view_name}' is not defined" unless view_collection.view?(view_name)
|
|
95
|
+
|
|
96
|
+
object = Blueprinter.configuration.extensions.pre_render(object, self, view_name, local_options)
|
|
97
|
+
prepare_data(object, view_name, local_options)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# @deprecated This method is no longer supported, and was not originally intended to be public. This will be removed
|
|
101
|
+
# in the next minor release. If similar functionality is needed, use `.render_as_hash` instead.
|
|
102
|
+
#
|
|
103
|
+
# This is the magic method that converts complex objects into a simple hash
|
|
104
|
+
# ready for JSON conversion.
|
|
105
|
+
#
|
|
106
|
+
# Note: we accept view (public interface) that is in reality a view_name,
|
|
107
|
+
# so we rename it for clarity
|
|
108
|
+
#
|
|
109
|
+
# @api private
|
|
110
|
+
def prepare(object, view_name:, local_options:)
|
|
111
|
+
Blueprinter::Deprecation.report(
|
|
112
|
+
<<~MESSAGE
|
|
113
|
+
The `prepare` method is no longer supported will be removed in the next minor release.
|
|
114
|
+
If similar functionality is needed, use `.render_as_hash` instead.
|
|
115
|
+
MESSAGE
|
|
116
|
+
)
|
|
117
|
+
render_as_hash(object, view_name:, local_options:)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
private
|
|
121
|
+
|
|
122
|
+
attr_reader :blueprint, :options
|
|
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
|
+
|
|
129
|
+
if array_like?(object)
|
|
130
|
+
object.map do |obj|
|
|
131
|
+
object_to_hash(
|
|
132
|
+
obj,
|
|
133
|
+
view_name:,
|
|
134
|
+
local_options: local_options_with_view
|
|
135
|
+
)
|
|
136
|
+
end
|
|
137
|
+
else
|
|
138
|
+
object_to_hash(
|
|
139
|
+
object,
|
|
140
|
+
view_name:,
|
|
141
|
+
local_options: local_options_with_view
|
|
142
|
+
)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def object_to_hash(object, view_name:, local_options:)
|
|
147
|
+
result_hash = {}
|
|
148
|
+
|
|
149
|
+
view_collection.fields_for(view_name).each do |field|
|
|
150
|
+
next if field.skip?(field.name, object, local_options)
|
|
151
|
+
|
|
152
|
+
value = field.extract(object, local_options)
|
|
153
|
+
next if value.nil? && field.options[:exclude_if_nil]
|
|
154
|
+
|
|
155
|
+
result_hash[field.name] = value
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
view_collection.transformers(view_name).each do |transformer|
|
|
159
|
+
transformer.transform(result_hash, object, local_options)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
result_hash
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def jsonify(data)
|
|
166
|
+
Blueprinter.configuration.jsonify(data)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def apply_root_key(object:, root:)
|
|
170
|
+
return object unless root
|
|
171
|
+
return { root => object } if root.is_a?(String) || root.is_a?(Symbol)
|
|
172
|
+
|
|
173
|
+
raise(Errors::InvalidRoot)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def add_metadata(object:, metadata:, root:)
|
|
177
|
+
return object unless metadata
|
|
178
|
+
return object.merge(meta: metadata) if root
|
|
179
|
+
|
|
180
|
+
raise(Errors::MetaRequiresRoot)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def build_result(object:, options:)
|
|
184
|
+
view_name = options[:view] || :default
|
|
185
|
+
|
|
186
|
+
prepared_object = hashify(
|
|
187
|
+
object,
|
|
188
|
+
view_name:,
|
|
189
|
+
local_options: options.except(:view, :root, :meta)
|
|
190
|
+
)
|
|
191
|
+
object_with_root = apply_root_key(
|
|
192
|
+
object: prepared_object,
|
|
193
|
+
root: options[:root]
|
|
194
|
+
)
|
|
195
|
+
add_metadata(
|
|
196
|
+
object: object_with_root,
|
|
197
|
+
metadata: options[:meta],
|
|
198
|
+
root: options[:root]
|
|
199
|
+
)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
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'
|
|
@@ -11,7 +12,7 @@ module Blueprinter
|
|
|
11
12
|
class << self
|
|
12
13
|
# @return [Configuration]
|
|
13
14
|
def configuration
|
|
14
|
-
@
|
|
15
|
+
@_configuration ||= Configuration.new
|
|
15
16
|
end
|
|
16
17
|
|
|
17
18
|
def configure
|
|
@@ -20,7 +21,7 @@ module Blueprinter
|
|
|
20
21
|
|
|
21
22
|
# Resets global configuration.
|
|
22
23
|
def reset_configuration!
|
|
23
|
-
@
|
|
24
|
+
@_configuration = nil
|
|
24
25
|
end
|
|
25
26
|
end
|
|
26
27
|
end
|