assembler 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
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