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

Support keyword arguments for functions. #8

Open
wants to merge 4 commits into
base: main
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
39 changes: 31 additions & 8 deletions lib/dry/transformer/function.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ class Function
# @api private
attr_reader :args

# Additional keyword arguments that will be passed to the wrapped proc
#
# @return [Hash]
#
# @api private
attr_reader :kwargs

# @!attribute [r] name
#
# @return [<type] The name of the function
Expand All @@ -36,6 +43,7 @@ class Function
def initialize(fn, options = {})
@fn = fn
@args = options.fetch(:args, [])
@kwargs = options.fetch(:kwargs, {})
@name = options.fetch(:name, fn)
end

Expand All @@ -46,8 +54,8 @@ def initialize(fn, options = {})
# @alias []
#
# @api public
def call(*value)
fn.call(*value, *args)
def call(*value, **additional_kwargs)
fn.call(*value, *args, **(kwargs).merge(additional_kwargs))
end
alias_method :[], :call

Expand All @@ -71,15 +79,15 @@ def compose(other)
# @return [Function]
#
# @api private
def with(*args)
self.class.new(fn, name: name, args: args)
def with(*args, **additional_kwargs)
self.class.new(fn, name: name, args: args, kwargs: kwargs.merge(additional_kwargs))
end

# @api public
def ==(other)
return false unless other.instance_of?(self.class)

[fn, name, args] == [other.fn, other.name, other.args]
[fn, name, args, kwargs] == [other.fn, other.name, other.args, other.kwargs]
end
alias_method :eql?, :==

Expand All @@ -89,8 +97,7 @@ def ==(other)
#
# @api public
def to_ast
args_ast = args.map { |arg| arg.respond_to?(:to_ast) ? arg.to_ast : arg }
[name, args_ast]
[name, object_to_ast(args), object_to_ast(kwargs)]
end

# Converts a transproc to a simple proc
Expand All @@ -99,11 +106,27 @@ def to_ast
#
def to_proc
if !args.empty?
proc { |*value| fn.call(*value, *args) }
proc { |*value| fn.call(*value, *args, **kwargs) }
else
fn.to_proc
end
end

private

# @api private
def object_to_ast(object)
case object
when Array
object.map { |item| object_to_ast(item) }
when Hash
object.each_with_object({}) { |(key, value), hash|
hash[object_to_ast(key)] = object_to_ast(value)
}
else
object.respond_to?(:to_ast) ? object.to_ast : object
end
end
end
end
end
4 changes: 2 additions & 2 deletions lib/dry/transformer/pipe/class_interface.rb
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,8 @@ def new(*)
# @return [Transproc::Function]
#
# @api public
def t(fn, *args)
container[fn, *args]
def t(fn, *args, **kwargs)
container[fn, *args, **kwargs]
end
end
end
Expand Down
4 changes: 2 additions & 2 deletions lib/dry/transformer/registry.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ module Registry
#
# @alias :t
#
def [](fn, *args)
def [](fn, *args, **kwargs)
fetched = fetch(fn)

return Function.new(fetched, args: args, name: fn) unless already_wrapped?(fetched)
return Function.new(fetched, args: args, kwargs: kwargs, name: fn) unless already_wrapped?(fetched)

args.empty? ? fetched : fetched.with(*args)
end
Expand Down
4 changes: 2 additions & 2 deletions spec/unit/class_transformations_spec.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# frozen_string_literal: true

require "dry/core/equalizer"
require "dry/equalizer"

RSpec.describe Dry::Transformer::ClassTransformations do
describe ".constructor_inject" do
Expand Down Expand Up @@ -39,7 +39,7 @@ def initialize(name:, age:)
set_ivars = described_class.t(:set_ivars, klass)

input = { name: "Jane", age: 25 }
output = klass.new(input)
output = klass.new(**input)
result = set_ivars[input]

expect(result).to eql(output)
Expand Down
40 changes: 28 additions & 12 deletions spec/unit/function_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@

expect(f3.to_ast).to eql(
[
:symbolize_keys, [],
:symbolize_keys, [], {},
[
:rename_keys, [user_name: :name]
:rename_keys, [], {user_name: :name}
]
]
)
Expand All @@ -47,12 +47,12 @@

expect(f4.to_ast).to eql(
[
:symbolize_keys, [],
:symbolize_keys, [], {},
[
:rename_keys, [user_name: :name]
:rename_keys, [], {user_name: :name}
],
[
:nest, [:details, [:name]]
:nest, [:details, [:name]], {}
]
]
)
Expand All @@ -68,9 +68,9 @@

expect(f3.to_ast).to eql(
[
f1.fn, [2],
f1.fn, [2], {},
[
f2.fn, []
f2.fn, [], {}
]
]
)
Expand All @@ -83,9 +83,9 @@
expect(f["user_name" => "Jane"]).to eql(name: "Jane")
expect(f.to_ast).to eql(
[
:symbolize_keys, [],
:symbolize_keys, [], {},
[
:rename_keys, [user_name: :name]
:rename_keys, [], {user_name: :name}
]
]
)
Expand All @@ -96,7 +96,7 @@
fn = container.t(:to_string)

expect(fn[:ok]).to eql("ok")
expect(fn.to_ast).to eql([:to_string, []])
expect(fn.to_ast).to eql([:to_string, [], {}])
end

it "plays well with functions as arguments" do
Expand All @@ -108,11 +108,27 @@
expect(fn.to_ast).to eql(
[
:map_array, [
[:to_symbol, []]
]
[:to_symbol, [], {}]
], {}
]
)
end

it "plays well with keyword arguments" do
container = Module.new do
extend Dry::Transformer::Registry

def self.trim(string, limit:)
string[0..(limit - 1)]
end
end

fn = container[:trim, limit: 3]

expect(fn.to_ast).to eql(
[:trim, [], {limit: 3}]
)
end
end

describe "#==" do
Expand Down
14 changes: 14 additions & 0 deletions spec/unit/registry_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
def self.prefix(value, prefix)
"#{prefix}_#{value}"
end

def self.trim(value, limit:)
value[0..(limit - 1)]
end
end
end

Expand All @@ -32,6 +36,16 @@ def self.prefix(value, prefix)
expect(transproc["qux"]).to eql "baz_qux"
end
end

context "with keyword arguments" do
it "passes keywords through" do
expect(foo[:trim]["string", limit: 3]).to eql("str")
end

it "works with a transproc approach" do
expect(foo[:trim, limit: 2]["string"]).to eql("st")
end
end
end

describe ".t" do
Expand Down
4 changes: 2 additions & 2 deletions spec/unit/store_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
it "returns false if requested proc is unknown" do
expect(store.contain?(:bar)).to be false
end
end # describe #fetch
end # describe #contain?

describe "#register" do
subject { new_store }
Expand All @@ -73,7 +73,7 @@
end
end

describe "#import", :focus do
describe "#import" do
before do
module Bar
def self.bar
Expand Down
4 changes: 2 additions & 2 deletions spec/unit/transformer/class_interface_spec.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

require 'ostruct'
require 'dry/core/equalizer'
require 'dry/equalizer'

RSpec.describe Dry::Transformer do
let(:container) { Module.new { extend Dry::Transformer::Registry } }
Expand Down Expand Up @@ -166,7 +166,7 @@ def self.to_symbol(v)
map_value(:attr, t(:to_symbol))
end

expect(transproc.new.transproc[attr: 'abc']).to eq(attr: :abc)
expect(transproc.new.transproc[{attr: 'abc'}]).to eq(attr: :abc)
end

it 'does not affect original transformer' do
Expand Down