assembler 1.1.0 → 1.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 459053f97aea6e47c8993857b8bdb82695c39354
4
- data.tar.gz: eb0b24fda83cfe8b556a97f0cdcd5108af2970b7
3
+ metadata.gz: e1d3eb4e375c75519e76037b6ac633be9901d78d
4
+ data.tar.gz: 0fd01f2626ac0d88104703fa5ea2545d7c581f85
5
5
  SHA512:
6
- metadata.gz: 6a52ab6904aec79e71a8725aded5372880c75172c52a3101c6794b50a8bd5e4520a12f20d8e7ae4fe4c369376b1d6e5d2c8a4440679e5a62611aa0bf8cb4d3ec
7
- data.tar.gz: 4566485a70fc557fb3321bf0586d58e11f1f69f041f946ce166b7e34d7188e5df3d2ab664683142e54def16f3b6e014912e94d5479aec5e18c07836ba97979a0
6
+ metadata.gz: 4ff23dd635deec43014f704eabbc1282de5f5a7cd1fa6bd584da667eba048ecb39e2b895989e321dfb70c7acb515d219402e15ed983dc8650f262e166bb46842
7
+ data.tar.gz: d833e8aba1907888013f186e4afc2f22a42edea19d81ff6893791f6dc05f60ca8781396c27a97f8b8e6cc8931081383f8dddf6b509dc5df7b576fab5b8f1b25d
data/.rspec CHANGED
@@ -1,2 +1 @@
1
- --color
2
- --format documentation
1
+ --require spec_helper
data/CHANGES.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changes
2
2
 
3
+ ## 1.2.0
4
+
5
+ * Add ability to access (optionally) coerced values on the builder object, for
6
+ example Foo.new(some: 'value') {|b| puts b.some}. (Ryan Michael)
7
+ * Add `assemble_from_options` DSL method for defining coercions and aliases. (Ryan Michael)
8
+ * Add `before_assembly` and `after_assembly` hooks. (Ben Hamill)
9
+ * Make the `before_assembly` and `after_assembly` hooks additive, in the case
10
+ that those methods are called more than once. (Ben Hamill)
11
+ * Change up spec set up and move project documentation around a little. (Ben
12
+ Hamill)
13
+
3
14
  ## 1.1.0
4
15
 
5
16
  * Make Ruby 1.9-compatible. (Ryan Michael)
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,19 @@
1
+ # Contributing
2
+
3
+ Help is gladly welcomed. If you have a feature you'd like to add, it's much more
4
+ likely to get in (or get in faster) the closer you stick to these steps:
5
+
6
+ 1. Open an Issue to talk about it. We can discuss whether it's the right
7
+ direction or maybe help track down a bug, etc.
8
+ 1. Fork the project, and make a branch to work on your feature/fix. Master is
9
+ where you'll want to start from.
10
+ 1. Turn the Issue into a Pull Request. There are several ways to do this, but
11
+ [hub](https://github.com/defunkt/hub) is probably the easiest.
12
+ 1. Make sure your Pull Request includes tests.
13
+ 1. Bonus points if your Pull Request updates `CHANGES.md` to include a summary
14
+ of your changes and your name like the other entries. If the last entry is
15
+ the last release, add a new `## Unreleased` heading.
16
+ 1. *Do not* rev the version number in your pull request.
17
+
18
+ If you don't know how to fix something, even just a Pull Request that includes a
19
+ failing test can be helpful. If in doubt, make an Issue to discuss.
data/README.md CHANGED
@@ -7,14 +7,26 @@ bunch of nanomachines that can build [almost anything](http://en.wikipedia.org/w
7
7
 
8
8
  Assembler is a library that gives you a DSL to describe a super-handy
9
9
  initializer pattern. You specify the parameters your object should take and
10
- Assembler give you an initializer that takes an options hash as well as yielding
11
- a [builder object](http://c2.com/cgi/wiki?BuilderPattern) to a block. It takes
12
- care of storing the parameters and gives you private accessors, too.
10
+ Assembler gives you an initializer that takes an options hash as well as
11
+ yielding a [builder object](http://c2.com/cgi/wiki?BuilderPattern) to a block.
12
+ It takes care of storing the parameters and gives you private accessors, too.
13
+
14
+ ## Contents
15
+
16
+ * [Usage](#usage)
17
+ * [assemble_from](#assemble_from)
18
+ * [assemble_from_options](#assemble_from_options)
19
+ * [Before and After Hooks](#before-and-after-hooks)
20
+ * [Contributing](#contributing)
13
21
 
14
22
 
15
23
  ## Usage
16
24
 
17
- You use it like this:
25
+ ### `assemble_from`
26
+
27
+ The `assemble_from` method is the core of Assembler. It's also aliased as
28
+ `assemble_with`. The basic use case is to pass in required parameters followed
29
+ by optional parameters (and their defaults). Take this simple example:
18
30
 
19
31
  ```ruby
20
32
  class IMAPConnection
@@ -26,8 +38,8 @@ class IMAPConnection
26
38
  end
27
39
  ```
28
40
 
29
- Then you can instantiate your object with either an options hash or via a block.
30
- For example:
41
+ This enables you to instantiate your object with either an options hash or via a
42
+ block. For example:
31
43
 
32
44
  ```ruby
33
45
  # These two are equivalent:
@@ -42,9 +54,14 @@ IMAPConnection.new do |aw|
42
54
  aw.hostname = 'imap.example.com'
43
55
  aw.use_ssl = false
44
56
  end
57
+
58
+ # Or you can do a combination, if you need to:
59
+ IMAPConnection.new(hostname: 'imap.example.com') do |aw|
60
+ aw.use_ssl = false
61
+ end
45
62
  ```
46
63
 
47
- Note that when we set `use_ssl` to `false`, the code respects that, rather than
64
+ Note that when you set `use_ssl` to `false`, the code respects that, rather than
48
65
  over-writing anything falsey with the default. If you don't want that, override
49
66
  it like with `port`, below.
50
67
 
@@ -69,12 +86,14 @@ end
69
86
  These various syntaxes enable some trickery when you're dealing with a world of
70
87
  uncertainty. Let's look at a more complicated example.
71
88
 
72
- Say we've got a class that lets us describe an Elastic Load Balancer for Amazon
89
+ Say you want a class that lets us describe an Elastic Load Balancer for Amazon
73
90
  Web Services. There's a lot of complexity in what each of these arguments might
74
- be, but the key thing for our example is this: If you have `subnets`, you
91
+ be, but the key thing for this example is this: If you have `subnets`, you
75
92
  shouldn't have `availability_zones` and if you have `availability_zones`, you
76
93
  shouldn't have `subnets`. And, importantly, you shouldn't send in extraneous
77
- keys.
94
+ keys; you need to be able to differentiate callers sending `nil` explicitly from
95
+ not sending in anything when you make whatever API calls you're going to make to
96
+ Amazon.
78
97
 
79
98
  ```ruby
80
99
  class AmazonELB
@@ -95,9 +114,9 @@ end
95
114
  ```
96
115
 
97
116
  Now, since there's a lot of complexity in what each of these arguments might be,
98
- say we've developed some best-practices about what each of them should be. And
99
- we want to make it easy to pop off slight variations on what we consider to be a
100
- "standard" ELB.
117
+ say you've developed some best-practices about what each of them should be. And
118
+ you want to make it easy to pop off slight variations on what you consider to be
119
+ a "standard" ELB.
101
120
 
102
121
  ``` ruby
103
122
  module ELBFactory
@@ -108,7 +127,7 @@ module ELBFactory
108
127
  elb.security_groups = security_groups
109
128
  elb.instance_ids = instance_ids
110
129
 
111
- elb.health_check = HealthCheck.new (
130
+ elb.health_check = HealthCheck.new(
112
131
  target: 'HTTP:8000/',
113
132
  healthy_threshold: '3',
114
133
  unhealthy_threshold: '5',
@@ -143,21 +162,125 @@ would look similar to the above, but require you to assign an intermediate
143
162
  variable for no semantic benefit.
144
163
 
145
164
 
165
+ ### `assemble_from_options`
166
+
167
+ If you need to do something more complicated than what's provided by
168
+ `assemble_from`, you can specify per-argument options using
169
+ `assemble_from_options`. Like `assemble_from`, it's also aliased as
170
+ `assemble_with_options`.
171
+
172
+ Default values can be specified using the `:default` option, and work the same
173
+ as using hash-syntax with `assemble_from`.
174
+
175
+ If you would like to do some type of value coercion you can specify either a
176
+ symbol or a callable using the `:coerce` option. Symbols will be passed as
177
+ messages to the input object, and anything that responds to `#call` will be
178
+ called with the input object as an argument.
179
+
180
+ If you need to accept aliased key names you can use the `:aliases` option to
181
+ specify a list of keys. Aliases only apply to input processing; instance
182
+ variables aren't set and accessors aren't be provided.
183
+
184
+ ```ruby
185
+ class IMAPConnection
186
+ extend Assembler
187
+
188
+ # Here we want to assign an IP address so we only do DNS lookup once.
189
+ assemble_from_options :hostname, coerce: ->(h) { Resolv.getaddress(h) }
190
+
191
+ # Defaults must be specified explicitly; arguments with no default are required.
192
+ assemble_from_options :use_ssl, default: false
193
+
194
+ # We'll accept values named 'port' or 'host_port' (but we'll only assign '@port').
195
+ # Symbols can also be passed for coercions.
196
+ assemble_from_options :port, default: nil, coerce: :to_i, aliases: [:host_port]
197
+ end
198
+
199
+ instance = IMAPConnection.new(hostname: 'localhost') do |b|
200
+ puts b.hostname # => '127.0.0.0' - i.e. the accessor returns the coerced value.
201
+ puts b.use_ssl # => false - i.e. the accessor returns the default value if none is specified.
202
+ b.port = '100' # Will be coerced to the integer 100.
203
+ end
204
+
205
+ instance.host_port # => MethodMissing error - accessors aren't defined for aliases.
206
+ ```
207
+
208
+ ### Before and After Hooks
209
+
210
+ In some cases, you might need to take care of some extra things during object
211
+ initialization. One simple case would be if you're inheriting from another class
212
+ and need to call `super` to make sure it initializes correctly. Enter
213
+ `before_assembly` and `after_assembly`.
214
+
215
+ They both take a block and that block gets evaluated in the scope of your
216
+ instance before or after the rest of Assembler's initializer runs. This means
217
+ instance variables and private methods are available to you and that `self` is
218
+ the object being created. Nothing is `yield`ed to the blocks.
219
+
220
+ If you don't need to pass arguments or always pass the same arguments, you could
221
+ do something like this:
222
+
223
+ ```ruby
224
+ class Professor < Employee
225
+ extend Assembler
226
+
227
+ before_assembly do
228
+ super('teaching')
229
+ end
230
+ end
231
+ ```
232
+
233
+ If, however, you need to react to or interact with options that are passed in,
234
+ you can do something like this:
235
+
236
+ ```ruby
237
+ class Professor < Employee
238
+ extend Assembler
239
+
240
+ attr_reader :title
241
+
242
+ assemble_with :department_name, :degree_subject
243
+ after_assembly do
244
+ @title = "PhD of #{degree_subject}, #{department_name}"
245
+ end
246
+ end
247
+ ```
248
+
249
+ If you call these methods more than once, each block will be run in the order
250
+ declared. The most common case would be a child class adding more before or
251
+ after functionality to that declared by a parent.
252
+
253
+ ```ruby
254
+ class Employee
255
+ extend Assembler
256
+
257
+ assemble_with :manager_id, department_name: nil
258
+
259
+ attr_reader :manager
260
+
261
+ after_assembly do
262
+ @manager = Manager.find(manager_id)
263
+ end
264
+ end
265
+
266
+ class Professor < Employee
267
+ assemble_with :department_chair
268
+
269
+ attr_reader :people_answerable_to
270
+
271
+ after_assembly do
272
+ @people_answerable_to = [manager, department_name]
273
+ end
274
+ end
275
+ ```
276
+
277
+
146
278
  ## Contributing
147
279
 
148
- Help is gladly welcomed. If you have a feature you'd like to add, it's much more
149
- likely to get in (or get in faster) the closer you stick to these steps:
150
-
151
- 1. Open an Issue to talk about it. We can discuss whether it's the right
152
- direction or maybe help track down a bug, etc.
153
- 1. Fork the project, and make a branch to work on your feature/fix. Master is
154
- where you'll want to start from.
155
- 1. Turn the Issue into a Pull Request. There are several ways to do this, but
156
- [hub](https://github.com/defunkt/hub) is probably the easiest.
157
- 1. Make sure your Pull Request includes tests.
158
- 1. Bonus points if your Pull Request updates `CHANGES.md` to include a summary
159
- of your changes and your name like the other entries. If the last entry is
160
- the last release, add a new `## Unreleased` heading at the top.
161
-
162
- If you don't know how to fix something, even just a Pull Request that includes a
163
- failing test can be helpful. If in doubt, make an Issue to discuss.
280
+ If you'd like to contribute, please see the [contribution guidelines](CONTRIBUTING.md).
281
+
282
+
283
+ ## Releasing
284
+
285
+ Maintainers: Please make sure to follow the [release steps](RELEASING.md) when
286
+ it's time to cut a new release.
data/RELEASING.md ADDED
@@ -0,0 +1,15 @@
1
+ # Releasing
2
+
3
+ If you want to push a new version of this gem, do this:
4
+
5
+ 1. Ideally, every Pull Request should already have included an addition to the
6
+ `CHANGES.md` file summarizing the changes and crediting the author(s). It
7
+ doesn't hurt to review this to see if anything needs adding.
8
+ 1. Commit any changes you make.
9
+ 1. Go into version.rb and bump the version number
10
+ [as appopriate](http://semver.org/).
11
+ 1. Go into CHANGES.md and change the "Unlreleased" heading to match the new
12
+ version number.
13
+ 1. Commit these changes with a message like, "Minor version bump," or similar.
14
+ 1. Run `rake release`.
15
+ 1. High five someone nearby.
data/lib/assembler.rb CHANGED
@@ -2,19 +2,27 @@ require "assembler/version"
2
2
  require "assembler/initializer"
3
3
 
4
4
  module Assembler
5
- attr_writer :required_params, :optional_params
5
+ def assemble_from_options(*args)
6
+ assembly_setup do
7
+ options = args.last.is_a?(Hash) ? args.pop : {}
8
+ param_names = args
6
9
 
7
- def assemble_from(*args)
8
- optional = args.last.is_a?(Hash) ? args.pop : {}
9
- required = args
10
-
11
- include Assembler::Initializer
10
+ param_names.each do |param_name|
11
+ param = Parameter.new(param_name, options)
12
+ assembly_parameters_hash[param.name] = param
13
+ end
14
+ end
15
+ end
16
+ alias_method :assemble_with_options, :assemble_from_options
12
17
 
13
- self.required_params += required
14
- self.optional_params = optional_params.merge(optional)
18
+ def assemble_from(*args)
19
+ assembly_setup do
20
+ optional = args.last.is_a?(Hash) ? args.pop : {}
21
+ required = args
15
22
 
16
- attr_reader *all_param_names
17
- private *all_param_names
23
+ optional.each { |k,v| assemble_from_options(k, default: v) }
24
+ required.each { |k| assemble_from_options(k) }
25
+ end
18
26
  end
19
27
  alias_method :assemble_with, :assemble_from
20
28
 
@@ -24,15 +32,39 @@ module Assembler
24
32
  assemble_from(*args)
25
33
  end
26
34
 
27
- def required_params
28
- @required_params ||= []
35
+ def before_assembly(&block)
36
+ assembly_setup do
37
+ before_assembly_blocks << block
38
+ end
39
+ end
40
+
41
+ def before_assembly_blocks
42
+ @before_assembly_blocks ||= []
29
43
  end
30
44
 
31
- def optional_params
32
- @optional_params ||= {}
45
+ def after_assembly(&block)
46
+ assembly_setup do
47
+ after_assembly_blocks << block
48
+ end
33
49
  end
34
50
 
35
- def all_param_names
36
- (required_params + optional_params.keys).map(&:to_sym)
51
+ def after_assembly_blocks
52
+ @after_assembly_blocks ||= []
53
+ end
54
+
55
+ def assembly_parameters
56
+ assembly_parameters_hash.values
57
+ end
58
+
59
+ def assembly_parameters_hash
60
+ @assembly_parameters_hash ||= {}
61
+ end
62
+
63
+ def assembly_setup
64
+ yield
65
+ ensure
66
+ include Assembler::Initializer
67
+ attr_reader *assembly_parameters.map(&:name)
68
+ private *assembly_parameters.map(&:name)
37
69
  end
38
70
  end
@@ -1,27 +1,30 @@
1
1
  module Assembler
2
2
  class Builder
3
- def initialize(*parameter_names)
4
- @parameter_names = parameter_names
3
+ def initialize(parameters_hash, options = {})
4
+ @options = options
5
+ @parameters_hash = parameters_hash
5
6
 
6
- parameter_names.each do |parameter_name|
7
- self.singleton_class.class_eval(<<-RUBY)
8
- def #{parameter_name}=(value)
9
- parameters[:#{parameter_name.to_sym}] = value
10
- end
11
- RUBY
7
+ parameters_hash.each do |parameter_name, parameter|
8
+ parameter.name_and_aliases.each do |name_or_alias|
9
+ self.singleton_class.class_eval(<<-RUBY)
10
+ def #{name_or_alias}=(value)
11
+ options[:#{parameter_name.to_sym}] = value
12
+ end
13
+
14
+ def #{name_or_alias}
15
+ parameters_hash[:#{parameter_name}].value_from(options)
16
+ end
17
+ RUBY
18
+ end
12
19
  end
13
20
  end
14
21
 
15
22
  def to_h
16
- parameters
23
+ options
17
24
  end
18
25
 
19
26
  private
20
27
 
21
- attr_reader :parameter_names
22
-
23
- def parameters
24
- @parameters ||= {}
25
- end
28
+ attr_reader :parameters_hash, :options
26
29
  end
27
30
  end
@@ -1,39 +1,36 @@
1
1
  require "assembler/builder"
2
- require "assembler/parameters"
2
+ require "assembler/parameter"
3
3
 
4
4
  module Assembler
5
5
  module Initializer
6
6
  def initialize(options={})
7
- builder = Assembler::Builder.new(*self.class.all_param_names)
7
+ if self.class.before_assembly_blocks.any?
8
+ self.class.before_assembly_blocks.each do |block|
9
+ instance_eval(&block)
10
+ end
11
+ end
12
+
13
+ builder = Assembler::Builder.new(self.class.assembly_parameters_hash, options)
8
14
 
9
15
  yield builder if block_given?
10
16
 
11
- @full_options = Assembler::Parameters.new(options.merge(builder.to_h))
17
+ missing_required_parameters = []
12
18
 
13
- missing_required_params = []
19
+ self.class.assembly_parameters.each do |param|
20
+ if_required_and_missing = -> { missing_required_parameters << param.name }
14
21
 
15
- self.class.required_params.each do |param_name|
16
- remember_value_or(param_name) { missing_required_params << param_name }
17
- end
22
+ value = param.value_from(builder.to_h, &if_required_and_missing)
18
23
 
19
- self.class.optional_params.each do |param_name, default_value|
20
- remember_value_or(param_name) { default_value }
24
+ instance_variable_set(:"@#{param.name}", value)
21
25
  end
22
26
 
23
- raise(ArgumentError, "missing keywords: #{missing_required_params.join(', ')}") if missing_required_params.any?
24
- end
25
-
26
- private
27
+ raise(ArgumentError, "missing keywords: #{missing_required_parameters.join(', ')}") if missing_required_parameters.any?
27
28
 
28
- attr_reader :full_options
29
-
30
- def remember_value_or(param_name, &block)
31
- instance_variable_set(
32
- :"@#{param_name}",
33
- full_options.fetch(param_name) do
34
- block.call
29
+ if self.class.after_assembly_blocks.any?
30
+ self.class.after_assembly_blocks.each do |block|
31
+ instance_eval(&block)
35
32
  end
36
- )
33
+ end
37
34
  end
38
35
  end
39
36
  end
@@ -0,0 +1,79 @@
1
+ module Assembler
2
+ class Parameter
3
+ attr_reader :name
4
+
5
+ def initialize(name, options = {})
6
+ @name = name.to_sym
7
+ @options = options
8
+ end
9
+
10
+ def name_and_aliases
11
+ @name_and_aliases ||= [name] + aliases.map(&:to_sym)
12
+ end
13
+
14
+ def value_from(hash, &if_required_and_missing)
15
+ @memoized_value_from ||= {}
16
+
17
+ # NOTE: Jruby's Hash#hash implementation is BS:
18
+ # {:foo => :foo}.hash => 1
19
+ # {:bar => :bar}.hash => 1
20
+ # {:foo => :foo}.to_a.hash => 806614226
21
+ # {:bar => :bar}.to_a.hash => 3120054328
22
+ # Go figure...
23
+ memoization_key = hash.to_a.hash
24
+
25
+ return @memoized_value_from[memoization_key] if @memoized_value_from[memoization_key]
26
+
27
+ first_key = key_names.find { |name_or_alias| hash.has_key?(name_or_alias) }
28
+
29
+ raw_value = hash.fetch(first_key) do
30
+ options.fetch(:default) do
31
+ if_required_and_missing.call unless if_required_and_missing.nil?
32
+
33
+ # Returning here so we don't call coerce_value(nil)
34
+ return
35
+ end
36
+ end
37
+
38
+ @memoized_value_from[memoization_key] = coerce_value(raw_value)
39
+ end
40
+
41
+ private
42
+
43
+ attr_reader :options
44
+
45
+ def key_names
46
+ name_and_aliases.flat_map do |name_or_alias|
47
+ [name_or_alias.to_sym, name_or_alias.to_s]
48
+ end
49
+ end
50
+
51
+ def has_default?
52
+ options.has_key?(:default)
53
+ end
54
+
55
+ def default
56
+ options[:default]
57
+ end
58
+
59
+ def coercion
60
+ options[:coerce]
61
+ end
62
+
63
+ def coerce_value(value)
64
+ if !coercion
65
+ value
66
+ elsif coercion.kind_of?(Symbol)
67
+ value.send(coercion)
68
+ elsif coercion.respond_to?(:call)
69
+ coercion.call(value)
70
+ else
71
+ raise ArgumentError, "don't know how to handle coerce value #{coercion}"
72
+ end
73
+ end
74
+
75
+ def aliases
76
+ @aliases ||= Array(options[:aliases]) + Array(options[:alias])
77
+ end
78
+ end
79
+ end
@@ -1,3 +1,3 @@
1
1
  module Assembler
2
- VERSION = "1.1.0"
2
+ VERSION = "1.2.0"
3
3
  end
@@ -0,0 +1,91 @@
1
+ describe Assembler do
2
+ describe "#after_assembly" do
3
+ context "with no other assembler helpers called" do
4
+ let(:klass) do
5
+ Class.new do
6
+ extend Assembler
7
+
8
+ after_assembly do
9
+ @after = true
10
+ end
11
+
12
+ attr_reader :after
13
+ end
14
+ end
15
+
16
+ subject { klass.new }
17
+
18
+ it "calls the after block" do
19
+ expect(subject.after).to be_true
20
+ end
21
+ end
22
+
23
+ context "with a more complex declaration" do
24
+ let(:klass) do
25
+ Class.new do
26
+ extend Assembler
27
+
28
+ assemble_with middle: 'middle'
29
+
30
+ after_assembly do
31
+ a_private_method
32
+ @after = true
33
+ @middle = 'after'
34
+ end
35
+
36
+ attr_reader :after, :middle
37
+
38
+ private
39
+
40
+ def a_private_method
41
+ "Shhhhhhh! it's a secret!"
42
+ end
43
+ end
44
+ end
45
+
46
+ subject do
47
+ klass.new
48
+ end
49
+
50
+ it "calls the after block" do
51
+ expect(subject.after).to be_true
52
+ end
53
+
54
+ it "calls the block after running the rest of the initializer" do
55
+ expect(subject.middle).to eq('after')
56
+ end
57
+
58
+ it "allows access to private methods" do
59
+ expect { klass.new }.to_not raise_error
60
+ end
61
+ end
62
+
63
+ context "with two after hooks defined" do
64
+ let(:klass) do
65
+ Class.new do
66
+ extend Assembler
67
+
68
+ after_assembly do
69
+ hooks << :first
70
+ end
71
+
72
+ after_assembly do
73
+ hooks << :second
74
+ end
75
+
76
+ def hooks
77
+ @hooks ||= []
78
+ end
79
+ end
80
+ end
81
+
82
+ subject do
83
+ klass.new
84
+ end
85
+
86
+ it "calls the after hooks in order of declaration" do
87
+ expect(subject.hooks).to eq([:first, :second])
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,384 @@
1
+ describe Assembler do
2
+ describe "#assemble_from_options" do
3
+ context "without parameters" do
4
+ subject do
5
+ Class.new do
6
+ extend Assembler
7
+
8
+ assemble_from_options
9
+ end
10
+ end
11
+
12
+ it "throws away all input parameters (method arguments)" do
13
+ built_object = subject.new(foo: 'foo', bar: 'bar')
14
+
15
+ expect(built_object.instance_variables).to_not include(:@foo, :@bar)
16
+ end
17
+
18
+ it "doesn't have methods on the builder object" do
19
+ subject.new do |builder|
20
+ expect(builder).to_not respond_to(:foo=)
21
+ expect(builder).to_not respond_to(:foo)
22
+ end
23
+ end
24
+ end
25
+
26
+ context "with no default parameter" do
27
+ subject do
28
+ Class.new do
29
+ extend Assembler
30
+
31
+ assemble_from_options :foo, :bar
32
+ end
33
+ end
34
+
35
+ it "barfs from missing required parameters" do
36
+ expect { subject.new }.to raise_error(ArgumentError)
37
+ end
38
+
39
+ it "holds onto the parameters (method arguments)" do
40
+ built_object = subject.new(foo: 'baz', bar: 'qux')
41
+
42
+ expect(built_object.instance_variable_get(:@foo)).to eq('baz')
43
+ expect(built_object.instance_variable_get(:@bar)).to eq('qux')
44
+ end
45
+
46
+ it "holds onto the parameters (block)" do
47
+ built_object = subject.new do |builder|
48
+ builder.foo = 'baz'
49
+ builder.bar = 'qux'
50
+ end
51
+
52
+ expect(built_object.instance_variable_get(:@foo)).to eq('baz')
53
+ expect(built_object.instance_variable_get(:@bar)).to eq('qux')
54
+ end
55
+
56
+ it "ignores un-named parameters in method arguments" do
57
+ built_object = subject.new(foo: 'foo', bar: 'bar', baz: 'baz')
58
+
59
+ expect(built_object.instance_variables).to_not include(:@baz)
60
+ end
61
+
62
+ it "doesn't create builder methods for un-named parameters" do
63
+ expect {
64
+ subject.new do |builder|
65
+ builder.baz = 'baz'
66
+ end
67
+ }.to raise_error(NoMethodError)
68
+ end
69
+
70
+ it "provides accessors in the builder object" do
71
+ subject.new do |builder|
72
+ builder.foo = :new_foo
73
+ builder.bar = :new_bar
74
+
75
+ expect(builder.foo).to eq(:new_foo)
76
+ end
77
+ end
78
+
79
+ it "returns nil for un-assigned parameters" do
80
+ subject.new do |builder|
81
+ expect(builder.foo).to be_nil
82
+
83
+ builder.foo = :new_foo
84
+ builder.bar = :new_bar
85
+ end
86
+ end
87
+
88
+ it "incorporates constructor args in the builder accessors" do
89
+ subject.new(foo: 'new_foo', bar: 'new_bar') do |builder|
90
+ expect(builder.foo).to eq('new_foo')
91
+ end
92
+ end
93
+ end
94
+
95
+ context "with default parameter" do
96
+ subject do
97
+ Class.new do
98
+ extend Assembler
99
+
100
+ assemble_from_options :foo, :bar, default: 'default'
101
+ assemble_from_options :baz, :qux, default: nil
102
+ end
103
+ end
104
+
105
+ it "uses default values for missing parameters (method arguments)" do
106
+ built_object = subject.new
107
+
108
+ expect(built_object.instance_variable_get(:@foo)).to eq('default')
109
+ expect(built_object.instance_variable_get(:@bar)).to eq('default')
110
+ expect(built_object.instance_variable_get(:@baz)).to eq(nil)
111
+ expect(built_object.instance_variable_get(:@qux)).to eq(nil)
112
+ end
113
+
114
+ it "uses default values for missing parameters (block)" do
115
+ built_object = subject.new do |builder|
116
+ end
117
+
118
+ expect(built_object.instance_variable_get(:@foo)).to eq('default')
119
+ expect(built_object.instance_variable_get(:@bar)).to eq('default')
120
+ expect(built_object.instance_variable_get(:@baz)).to eq(nil)
121
+ expect(built_object.instance_variable_get(:@qux)).to eq(nil)
122
+ end
123
+
124
+ it "holds onto the parameters (method arguments)" do
125
+ built_object = subject.new(foo: 'foo', bar: 'bar', baz: 'baz', qux: 'qux')
126
+
127
+ expect(built_object.instance_variable_get(:@foo)).to eq('foo')
128
+ expect(built_object.instance_variable_get(:@bar)).to eq('bar')
129
+ expect(built_object.instance_variable_get(:@baz)).to eq('baz')
130
+ expect(built_object.instance_variable_get(:@qux)).to eq('qux')
131
+ end
132
+
133
+ it "holds onto the parameters (block)" do
134
+ built_object = subject.new do |builder|
135
+ builder.foo = 'foo'
136
+ builder.bar = 'bar'
137
+ builder.baz = 'baz'
138
+ builder.qux = 'qux'
139
+ end
140
+
141
+ expect(built_object.instance_variable_get(:@foo)).to eq('foo')
142
+ expect(built_object.instance_variable_get(:@bar)).to eq('bar')
143
+ expect(built_object.instance_variable_get(:@baz)).to eq('baz')
144
+ expect(built_object.instance_variable_get(:@qux)).to eq('qux')
145
+ end
146
+
147
+ it "ignores un-named parameters in method arguments" do
148
+ built_object = subject.new(nope: 'nope')
149
+
150
+ expect(built_object.instance_variables).to_not include(:@nope)
151
+ end
152
+
153
+ it "doesn't create builder methods for un-named parameters" do
154
+ expect {
155
+ subject.new do |builder|
156
+ builder.nope = 'nope'
157
+ end
158
+ }.to raise_error(NoMethodError)
159
+ end
160
+
161
+ it "provides accessors in the builder object" do
162
+ subject.new do |builder|
163
+ builder.foo = :new_foo
164
+ expect(builder.foo).to eq(:new_foo)
165
+ end
166
+ end
167
+
168
+ it "pre-fills default values in the builder accessors" do
169
+ subject.new do |builder|
170
+ expect(builder.foo).to eq('default')
171
+ end
172
+ end
173
+
174
+ it "incorporates constructor args in the builder accessors" do
175
+ subject.new(foo: 'new_foo') do |builder|
176
+ expect(builder.foo).to eq('new_foo')
177
+ end
178
+ end
179
+ end
180
+
181
+ context "with symbol coerce parameter" do
182
+ subject do
183
+ Class.new do
184
+ extend Assembler
185
+
186
+ assemble_from_options :foo, coerce: :to_set
187
+ assemble_from_options :bar, coerce: :to_set, default: [:coerced]
188
+ end
189
+ end
190
+
191
+ let(:argument) { double('argument', to_set: Set.new([:coerced])) }
192
+
193
+ it "sends parameter value to constructor argument (method arguments)" do
194
+ expect(argument).to receive(:to_set).and_return(Set.new([:coerced]))
195
+ subject.new(foo: argument)
196
+ end
197
+
198
+ it "assigns the output of the coercion (method arguments)" do
199
+ built_object = subject.new(foo: argument)
200
+
201
+ expect(built_object.instance_variable_get(:@foo)).to eq(Set.new([:coerced]))
202
+ end
203
+
204
+ it "sends parameter value to constructor argument (block)" do
205
+ expect(argument).to receive(:to_set).and_return(Set.new([:coerced]))
206
+ subject.new { |b| b.foo = argument }
207
+ end
208
+
209
+ it "assigns the output of the coercion (block)" do
210
+ built_object = subject.new { |b| b.foo = argument }
211
+
212
+ expect(built_object.instance_variable_get(:@foo)).to eq(Set.new([:coerced]))
213
+ end
214
+
215
+ it "coerces default values in the builder accessor" do
216
+ subject.new(foo: [:foo]) do |builder|
217
+ expect(builder.bar).to eq(Set.new([:coerced]))
218
+ end
219
+ end
220
+
221
+ it "coerces constructor args in the builder accessors" do
222
+ subject.new(foo: [:not_default]) do |builder|
223
+ expect(builder.foo).to eq(Set.new([:not_default]))
224
+ end
225
+ end
226
+
227
+ it "coerces assigned values in the buidler accessors" do
228
+ subject.new do |builder|
229
+ builder.foo = [:not_default]
230
+ expect(builder.foo).to eq(Set.new([:not_default]))
231
+ end
232
+ end
233
+ end
234
+
235
+ context "with callable coerce parameter" do
236
+ subject do
237
+ callable = double('callable')
238
+ allow(callable).to receive(:call) do |s|
239
+ [s, :called]
240
+ end
241
+
242
+ Class.new do
243
+ extend Assembler
244
+
245
+ assemble_from_options :lambda, default: nil, coerce: ->(s) { [s, :called] }
246
+ assemble_from_options :proc, default: nil, coerce: Proc.new { |s| [s, :called] }
247
+ assemble_from_options :callable, default: nil, coerce: callable
248
+ end
249
+ end
250
+
251
+ let(:argument) { double('argument') }
252
+
253
+ it "assigns the output of the coercion (method arguments)" do
254
+ expect(subject.new(:lambda => :original).instance_variable_get(:@lambda)).to eq([:original, :called])
255
+ expect(subject.new(:proc => :original).instance_variable_get(:@proc)).to eq([:original, :called])
256
+ expect(subject.new(:callable => :original).instance_variable_get(:@callable)).to eq([:original, :called])
257
+ end
258
+
259
+ it "assigns the output of the coercion (block)" do
260
+ expect(subject.new {|b| b.lambda = :original}.instance_variable_get(:@lambda)).to eq([:original, :called])
261
+ expect(subject.new {|b| b.proc = :original}.instance_variable_get(:@proc)).to eq([:original, :called])
262
+ expect(subject.new {|b| b.callable = :original}.instance_variable_get(:@callable)).to eq([:original, :called])
263
+ end
264
+
265
+ it "coerces default values in the builder accessor" do
266
+ subject.new do |builder|
267
+ expect(builder.lambda).to eq([nil, :called])
268
+ expect(builder.proc).to eq([nil, :called])
269
+ expect(builder.callable).to eq([nil, :called])
270
+ end
271
+ end
272
+
273
+ it "coerces constructor args in the builder accessors" do
274
+ subject.new(:lambda => :original, :proc => :original, :callable => :original) do |builder|
275
+ expect(builder.lambda).to eq([:original, :called])
276
+ expect(builder.proc).to eq([:original, :called])
277
+ expect(builder.callable).to eq([:original, :called])
278
+ end
279
+ end
280
+
281
+ it "coerces assigned values in the buidler accessors" do
282
+ subject.new do |builder|
283
+ builder.lambda = :original
284
+ builder.proc = :original
285
+ builder.callable = :original
286
+
287
+ expect(builder.lambda).to eq([:original, :called])
288
+ expect(builder.proc).to eq([:original, :called])
289
+ expect(builder.callable).to eq([:original, :called])
290
+ end
291
+ end
292
+ end
293
+
294
+ context "with singular alias parameter" do
295
+ subject do
296
+ Class.new do
297
+ extend Assembler
298
+
299
+ assemble_from_options :foo, :alias => :bar
300
+ end
301
+ end
302
+
303
+ it "creates an alias" do
304
+ expect(subject.new(bar: :bar).instance_variable_get(:@foo)).to eq(:bar)
305
+ end
306
+ end
307
+
308
+ context "with enumerable alias parameter" do
309
+ subject do
310
+ Class.new do
311
+ extend Assembler
312
+
313
+ assemble_from_options :foo, :alias => [:bar, :baz]
314
+ end
315
+ end
316
+
317
+ it "creates aliases" do
318
+ expect(subject.new(bar: :bar).instance_variable_get(:@foo)).to eq(:bar)
319
+ expect(subject.new(baz: :baz).instance_variable_get(:@foo)).to eq(:baz)
320
+ end
321
+ end
322
+
323
+ context "with singular aliases parameter" do
324
+ subject do
325
+ Class.new do
326
+ extend Assembler
327
+
328
+ assemble_from_options :foo, aliases: :bar
329
+ end
330
+ end
331
+
332
+ it "creates an alias" do
333
+ expect(subject.new(bar: :bar).instance_variable_get(:@foo)).to eq(:bar)
334
+ end
335
+ end
336
+
337
+ context "with enumerable aliases parameter" do
338
+ subject do
339
+ Class.new do
340
+ extend Assembler
341
+
342
+ assemble_from_options :foo, aliases: [:bar, :baz]
343
+ end
344
+ end
345
+
346
+ it "creates aliases" do
347
+ expect(subject.new(bar: :bar).instance_variable_get(:@foo)).to eq(:bar)
348
+ expect(subject.new(baz: :baz).instance_variable_get(:@foo)).to eq(:baz)
349
+ end
350
+ end
351
+
352
+ context "when called more than once for the same key" do
353
+ subject do
354
+ Class.new do
355
+ extend Assembler
356
+
357
+ assemble_from_options :foo
358
+ assemble_from_options :foo, default: :foo, coerce: :to_sym, aliases: [:bar]
359
+ end
360
+ end
361
+
362
+ it "re-writes default" do
363
+ expect(subject.new.instance_variable_get(:@foo)).to eq(:foo)
364
+ end
365
+
366
+ it "re-writes aliases (method)" do
367
+ expect(subject.new(bar: :bar).instance_variable_get(:@foo)).to eq(:bar)
368
+ end
369
+
370
+ it "re-writes aliases (block)" do
371
+ expect(subject.new { |s| s.bar = :bar}.instance_variable_get(:@foo)).to eq(:bar)
372
+ end
373
+
374
+ it "re-writes coercions (method)" do
375
+ expect(subject.new(foo: 'foo').instance_variable_get(:@foo)).to eq(:foo)
376
+ end
377
+
378
+ it "re-writes coercions (block)" do
379
+ expect(subject.new { |s| s.foo = 'foo'}.instance_variable_get(:@foo)).to eq(:foo)
380
+ end
381
+ end
382
+ end
383
+ end
384
+
@@ -1,6 +1,3 @@
1
- require 'spec_helper'
2
- require 'assembler'
3
-
4
1
  describe Assembler do
5
2
  describe "#assemble_from" do
6
3
  context "without parameters" do
@@ -82,6 +79,25 @@ describe Assembler do
82
79
  end
83
80
  }.to raise_error(NoMethodError)
84
81
  end
82
+
83
+ it "provides accessors in the builder object" do
84
+ subject.new do |builder|
85
+ builder.foo = :new_foo
86
+ expect(builder.foo).to eq(:new_foo)
87
+ end
88
+ end
89
+
90
+ it "pre-fills default values in the builder accessors" do
91
+ subject.new(foo: 'foo') do |builder|
92
+ expect(builder.bar).to eq('bar')
93
+ end
94
+ end
95
+
96
+ it "incorporates contructor args in the builder accessors" do
97
+ subject.new(foo: 'foo', bar: 'new_bar') do |builder|
98
+ expect(builder.bar).to eq('new_bar')
99
+ end
100
+ end
85
101
  end
86
102
 
87
103
  context "when called more than once" do
@@ -0,0 +1,80 @@
1
+ describe Assembler do
2
+ describe "#before_assembly" do
3
+ context "with no other assembler helpers called" do
4
+ let(:klass) do
5
+ Class.new do
6
+ extend Assembler
7
+
8
+ before_assembly do
9
+ @before = true
10
+ end
11
+
12
+ attr_reader :before
13
+ end
14
+ end
15
+
16
+ subject { klass.new }
17
+
18
+ it "calls the before block" do
19
+ expect(subject.before).to be_true
20
+ end
21
+ end
22
+
23
+ context "with a more complex declaration" do
24
+ let(:klass) do
25
+ Class.new do
26
+ extend Assembler
27
+
28
+ assemble_with :middle
29
+
30
+ before_assembly do
31
+ @before = true
32
+ @middle = 'before'
33
+ end
34
+
35
+ attr_reader :before, :middle
36
+ end
37
+ end
38
+
39
+ subject do
40
+ klass.new(middle: 'middle')
41
+ end
42
+
43
+ it "calls the before block" do
44
+ expect(subject.before).to be_true
45
+ end
46
+
47
+ it "calls the block before running the rest of the initializer" do
48
+ expect(subject.middle).to eq('middle')
49
+ end
50
+ end
51
+
52
+ context "with two before hooks defined" do
53
+ let(:klass) do
54
+ Class.new do
55
+ extend Assembler
56
+
57
+ before_assembly do
58
+ hooks << :first
59
+ end
60
+
61
+ before_assembly do
62
+ hooks << :second
63
+ end
64
+
65
+ def hooks
66
+ @hooks ||= []
67
+ end
68
+ end
69
+ end
70
+
71
+ subject do
72
+ klass.new
73
+ end
74
+
75
+ it "calls the before hooks in order of declaration" do
76
+ expect(subject.hooks).to eq([:first, :second])
77
+ end
78
+ end
79
+ end
80
+ end
data/spec/spec_helper.rb CHANGED
@@ -25,3 +25,4 @@ RSpec.configure do |config|
25
25
  end
26
26
 
27
27
  require 'pry'
28
+ require 'assembler'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: assembler
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben Hamill
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-03-13 00:00:00.000000000 Z
11
+ date: 2014-04-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -79,17 +79,22 @@ files:
79
79
  - ".rspec"
80
80
  - ".travis.yml"
81
81
  - CHANGES.md
82
+ - CONTRIBUTING.md
82
83
  - Gemfile
83
84
  - LICENSE.txt
84
85
  - README.md
86
+ - RELEASING.md
85
87
  - Rakefile
86
88
  - assembler.gemspec
87
89
  - lib/assembler.rb
88
90
  - lib/assembler/builder.rb
89
91
  - lib/assembler/initializer.rb
90
- - lib/assembler/parameters.rb
92
+ - lib/assembler/parameter.rb
91
93
  - lib/assembler/version.rb
92
- - spec/assembler_spec.rb
94
+ - spec/after_assembly_spec.rb
95
+ - spec/assemble_from_options_spec.rb
96
+ - spec/assemble_from_spec.rb
97
+ - spec/before_assembly_spec.rb
93
98
  - spec/spec_helper.rb
94
99
  homepage: https://github.com/benhamill/assembler#readme
95
100
  licenses:
@@ -116,5 +121,8 @@ signing_key:
116
121
  specification_version: 4
117
122
  summary: Block-based initializers for your objects.
118
123
  test_files:
119
- - spec/assembler_spec.rb
124
+ - spec/after_assembly_spec.rb
125
+ - spec/assemble_from_options_spec.rb
126
+ - spec/assemble_from_spec.rb
127
+ - spec/before_assembly_spec.rb
120
128
  - spec/spec_helper.rb
@@ -1,19 +0,0 @@
1
- module Assembler
2
- class Parameters
3
- def initialize(params={})
4
- @params = params
5
- end
6
-
7
- def fetch(key, &block)
8
- params.fetch(key.to_sym) do
9
- params.fetch(key.to_s) do
10
- block.call
11
- end
12
- end
13
- end
14
-
15
- private
16
-
17
- attr_reader :params
18
- end
19
- end