Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow builder DSL to accept implicit arrays and hashes #3

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 25 additions & 13 deletions lib/keyword_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,20 @@ class KeywordBuilder
class BuilderError < ArgumentError; end

class << self
def create(clazz, constructor: :new)
def create(clazz, constructor: :new, &finalizer)
keywords, wildcard = parse_constructor_parameters(clazz, constructor)

Class.new(self) do
define_singleton_method(:clazz) { clazz }
define_singleton_method(:constructor) { constructor }
define_singleton_method(:keywords) { keywords }
define_singleton_method(:wildcard?) { wildcard }

finalizer ||= proc { |_attrs| }
define_singleton_method(:finalizer, &finalizer)

keywords.each do |keyword|
define_method(keyword) { |*args, &block| _set_attribute(keyword, *args, &block) }
define_method(keyword) { |*args, **kwargs, &block| _set_attribute(keyword, *args, **kwargs, &block) }
end
end
end
Expand All @@ -27,6 +31,7 @@ def valid_keyword?(param)
def build!(**initial_attrs, &block)
builder = self.new(initial_attrs)
builder.instance_eval(&block) if block_given?
finalizer(builder.attrs)
clazz.public_send(constructor, **builder.attrs)
end

Expand Down Expand Up @@ -66,28 +71,35 @@ def initialize(initial_attrs)
@attrs = initial_attrs.dup
end

def _set_attribute(attr, *args, &block)
def _set_attribute(attr, *args, **kwargs, &block)
if attrs.has_key?(attr)
raise BuilderError.new("Invalid builder state: #{attr} already provided")
end

value =
if block_given?
raise ArgumentError.new('Cannot provide both immediate and block value') unless args.blank?
if kwargs.empty?
value = args.dup

value << block if block_given?

if value.empty?
raise ArgumentError.new('Wrong number of arguments: expected at least one argument or block')
end

block
elsif args.size == 1
args[0]
else
raise ArgumentError.new('Wrong number of arguments: expected 1 or block')
value = value[0] if value.size == 1
else
unless args.empty? && block.nil?
raise ArgumentError.new('Invalid arguments: cannot provide both keyword and positional arguments')
end

value = kwargs.dup
end

attrs[attr] = value
end

def method_missing(attr, *args, &block)
def method_missing(attr, *args, **kwargs, &block)
if self.class.wildcard?
_set_attribute(attr, *args, &block)
_set_attribute(attr, *args, **kwargs, &block)
else
super
end
Expand Down
2 changes: 1 addition & 1 deletion lib/keyword_builder/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

class KeywordBuilder
VERSION = '1.0.0'
VERSION = '1.1.0'
end
51 changes: 48 additions & 3 deletions spec/unit/keyword_builder_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,32 @@ def initialize(a:, b:, c:)
expect(result).to have_attributes(a: 1, b: 2, c: 3)
end

it 'constructs an array when builder is given more than one argument' do
result = builder.build!(a: 1, b: 2) do
c(1, 2, 3)
end

expect(result).to have_attributes(a: 1, b: 2, c: [1, 2, 3])
end

it 'constructs an array when builder is given arguments and a block' do
p = proc { 1 }

result = builder.build!(a: 1, b: 2) do
c(3, &p)
end

expect(result).to have_attributes(a: 1, b: 2, c: [3, p])
end

it 'constructs an hash when builder is given keyword arguments' do
result = builder.build!(a: 1, b: 2) do
c(x: 3, y: 4)
end

expect(result).to have_attributes(a: 1, b: 2, c: { x: 3, y: 4 })
end

it 'rejects arguments colliding with builder' do
expect {
builder.build!(a: 1, b: 2, c: 3) { a 1 }
Expand All @@ -55,10 +81,16 @@ def initialize(a:, b:, c:)
}.to raise_error(ArgumentError, /Wrong number of arguments/)
end

it 'requires only one argument' do
it 'rejects mixed keyword and positional arguments' do
expect {
builder.build!(a: 1, b: 2) { c(1, 2, 3) }
}.to raise_error(ArgumentError, /Wrong number of arguments/)
builder.build!(a: 1, b: 2) { c(3, x: 4) }
}.to raise_error(ArgumentError, /Invalid arguments/)
end

it 'rejects mixed keyword and block arguments' do
expect {
builder.build!(a: 1, b: 2) { c(x: 3) { 4 } }
}.to raise_error(ArgumentError, /Invalid arguments/)
end

it 'rejects unknown arguments' do
Expand All @@ -71,6 +103,19 @@ def initialize(a:, b:, c:)
expect(builder).not_to be_wildcard
end

context 'with finalizer' do
let(:builder) do
KeywordBuilder.create(Record) do |attrs|
attrs[:a] += 1
end
end

it 'invokes the finalizer on the builder' do
result = builder.build!(a: 1, b: 2, c: 3)
expect(result).to have_attributes(a: 2, b: 2, c: 3)
end
end

context 'with wildcard arguments' do
WildcardRecord = Struct.new(:a, :b) do
def initialize(**rest)
Expand Down
Loading