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.
@@ -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
- if valid_blueprint?(blueprint)
14
- true
15
- else
16
- raise(
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 :association_default, :datetime_format, :deprecations, :field_default, :generator, :if, :method,
10
- :sort_fields_by, :unless, :extractor_default, :default_transformers, :custom_array_like_classes
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
- end
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
- @array_like_classes ||= [
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Blueprinter
4
4
  module Errors
5
- class InvalidBlueprint < Blueprinter::BlueprinterError; end
5
+ class InvalidBlueprint < BlueprinterError; end
6
6
  end
7
7
  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.extractor_default.new
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.except(:default, :default_if)
17
- # Merge in assocation options hash
18
- local_options = local_options.merge(options[:options]) if options[:options].is_a?(Hash)
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.prepare(value, view_name: view, local_options: 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].call(object, local_options)
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
@@ -26,15 +26,11 @@ module Blueprinter
26
26
  private
27
27
 
28
28
  def if_callable
29
- return @if_callable if defined?(@if_callable)
30
-
31
- @if_callable = callable_from(:if)
29
+ @_if_callable ||= callable_from(:if)
32
30
  end
33
31
 
34
32
  def unless_callable
35
- return @unless_callable if defined?(@unless_callable)
36
-
37
- @unless_callable = callable_from(:unless)
33
+ @_unless_callable ||= callable_from(:unless)
38
34
  end
39
35
 
40
36
  def callable_from(condition)
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Blueprinter
4
4
  class DateTimeFormatter
5
- InvalidDateTimeFormatterError = Class.new(BlueprinterError)
5
+ class InvalidDateTimeFormatterError < BlueprinterError; end
6
6
 
7
7
  def format(value, options)
8
8
  return value if value.nil?
@@ -23,7 +23,7 @@ module Blueprinter
23
23
  # @return [Hash<Symbol, Blueprinter::Reflection::View>]
24
24
  #
25
25
  def reflections
26
- @reflections ||= view_collection.views.transform_values do |view|
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
- @fields ||= @view_collection.fields_for(name).each_with_object({}) do |field, obj|
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
- @associations ||= @view_collection.fields_for(name).each_with_object({}) do |field, obj|
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Blueprinter
4
- VERSION = '1.1.2'
4
+ VERSION = '1.3.0'
5
5
  end
@@ -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
- return identifier_fields if view_name == :identifier
59
+ ensure_cached!
30
60
 
31
- fields, excluded_fields = sortable_fields(view_name)
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
- included_transformers = gather_transformers_from_included_views(view_name).reverse
41
- all_transformers = [*views[:default].view_transformers, *included_transformers].uniq
42
- all_transformers.empty? ? Blueprinter.configuration.default_transformers : all_transformers
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] ||= View.new(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
- @configuration ||= Configuration.new
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
- @configuration = nil
24
+ @_configuration = nil
24
25
  end
25
26
  end
26
27
  end
@@ -36,7 +36,7 @@ module Blueprinter
36
36
  end
37
37
 
38
38
  def create_blueprint
39
- template 'blueprint.rb', File.join(path, "#{file_path}_blueprint.rb")
39
+ template 'blueprint.erb', File.join(path, "#{file_path}_blueprint.rb")
40
40
  end
41
41
 
42
42
  private