-
Notifications
You must be signed in to change notification settings - Fork 2
/
buildable.rb
156 lines (132 loc) · 5.47 KB
/
buildable.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
module Blueprints
class Buildable
delegate :namespace, :dependencies, :to => :@context
attr_reader :name
# Initializes new Buildable object by name and context which it belongs to.
# @param [#to_sym, Hash] name Name of buildable. If hash is passed then first key is assumed name, and
# value(s) of that key are assumed as dependencies.
# @param [Blueprints::Context] context Context of buildable that later might get updated.
# @raise [TypeError] If name is invalid.
def initialize(name, context)
@context = context
name = self.class.infer_name(attributes) if name.nil?
@name, parents = parse_name(name)
depends_on(*parents)
namespace.add_child(self) if namespace
end
# Returns class, name, attributes and dependencies of buildable in nice formatted string.
# @return [String] Inspected properties of buildable.
def inspect
"<##{self.class} name: #{full_name.inspect}, attributes: #{attributes.inspect}, dependencies: #{dependencies.inspect}>"
end
# Defines dependencies of buildable by updating it's context.
# @param [Array<String, Symbol>] dependencies List of dependencies.
def depends_on(*dependencies)
update_context(:dependencies => dependencies)
end
# @overload attributes
# Returns attributes of buildable
# @return [Hash] Attributes of buildable
# @overload attributes(value)
# Merges attributes of buildable with new attributes by updating context
# @param [Hash] Updated attributes
def attributes(value = nil)
if value
update_context(:attributes => value)
else
@context.attributes
end
end
# Builds dependencies of buildable and then buildable itself.
# @param [Blueprints::EvalContext] eval_context Context to build buildable object in.
# @param [Hash] options List of options to build this buildable with.
# @option options [Hash] :options ({}) List of options to be accessible in the body of a blueprint.
# @option options [true, false] :rebuild (false) If true this buildable is treated as not built yet and is rebuilt even if it was built before.
# @option options [Symbol] :strategy (:default) Strategy to use when building.
def build(eval_context, options = {})
return result(eval_context) if @building or (built? and not options[:rebuild] and options[:options].blank?)
@building = true
each_namespace { |namespace| namespace.build_parents(eval_context) }
build_parents(eval_context)
result = build_self(eval_context, options)
Namespace.root.executed_blueprints << self
@building = false
result
end
# Returns if blueprint has been built
# @return [true, false] true if was built, false otherwise
def built?
Namespace.root.executed_blueprints.include?(self)
end
# Marks blueprint as not built
def undo!
Namespace.root.executed_blueprints.delete self
end
# Returns full path to this buildable
# @param [String] join_with Separator used to join names of parent namespaces and buildable itself.
# @return [String] full path to this buildable joined with separator
def path(join_with = '_')
(namespace.path(join_with) + join_with unless namespace.nil? or namespace.path.empty?).to_s + @name.to_s
end
# Returns full name for this buildable
# @return [String] full buildable name
def full_name
path('.')
end
# Builds all dependencies. Should be called before building itself. Searches dependencies first in parent then in root namespace.
# @param [Blueprints::EvalContext] eval_context Context to build parents against.
# @raise [Blueprints::BlueprintNotFoundError] If one of dependencies can't be found.
def build_parents(eval_context)
@context.dependencies.each do |name|
parent = begin
namespace[name]
rescue BlueprintNotFoundError
Namespace.root[name]
end
parent.build(eval_context)
end
end
# Infers name of buildable using default attributes from Blueprints.config
# @param [Hash] attributes Attributes of buildable object to infer the name from.
# @return [String] Inferred name
def self.infer_name(attributes)
default_attribute = Blueprints.config.default_attributes.detect { |attribute| attributes.has_key?(attribute) }
attributes[default_attribute].parameterize('_') if default_attribute
end
protected
def each_namespace
namespace = self
yield(namespace) while namespace = namespace.namespace
end
def variable_name
:"@#{path}"
end
def parse_name(name)
if name.is_a?(Hash)
return name.keys.first.to_sym, [name.values.first].flatten.map { |sc| parse_name(sc).first }
elsif name.respond_to?(:to_sym)
name = name.to_sym unless name == ''
return name, []
else
raise TypeError, "Pass blueprint names as strings or symbols only, cannot define blueprint #{name.inspect}"
end
end
def update_context(options)
@context = @context.with_context(options)
self
end
private
def result(eval_context)
if block_given?
yield.tap do |result|
if @auto_variable or not eval_context.instance_variable_defined?(variable_name)
eval_context.instance_variable_set(variable_name, result)
@auto_variable = true
end
end
else
eval_context.instance_variable_get(variable_name)
end
end
end
end