Skip to content

Commit

Permalink
Merge pull request #12 from Kaligo/feature/option-transform
Browse files Browse the repository at this point in the history
Add new transform option to OptionsDeclaration
  • Loading branch information
Drenmi authored May 28, 2021
2 parents 7242baf + 583c31e commit 5f6f1f7
Show file tree
Hide file tree
Showing 8 changed files with 118 additions and 23 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## 0.6.0

### New features

- Add support for transformation methods to `OptionsDeclaration`.

## 0.5.6

### Bug fixes
Expand Down
6 changes: 3 additions & 3 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
PATH
remote: .
specs:
stimpack (0.5.5)
stimpack (0.6.0)
activesupport (~> 6.1)

GEM
remote: https://rubygems.org/
specs:
activesupport (6.1.3)
activesupport (6.1.3.2)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
Expand All @@ -16,7 +16,7 @@ GEM
ast (2.4.2)
concurrent-ruby (1.1.8)
diff-lcs (1.4.4)
i18n (1.8.9)
i18n (1.8.10)
concurrent-ruby (~> 1.0)
minitest (5.14.4)
parallel (1.20.1)
Expand Down
61 changes: 56 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,11 +152,62 @@ Foo.new(bar: "Hello!")

When declaring an option, the following configuration kets are available:

| Configuration | Type | Default | Notes |
| --------------- | ------------ | ------- | ----- |
| `default` | `any` | `nil` | Can be a literal or a callable object. Arrays and hashes will not be shared across instances. |
| `required` | `boolean` | `true` | |
| `private_reader` | `boolean` | `true` | |
| Configuration | Type | Default | Notes |
| --------------- | --------------- | ------- | ----- |
| `default` | `any` | `nil` | Can be a literal or a callable object. Arrays and hashes will not be shared across instances. |
| `required` | `boolean` | `true` | |
| `transform` | `symbol`/`proc` | `noop` | Can be a symbol that is a method on the value, or a callable object that takes the value as argument. |
| `private_reader` | `boolean` | `true` | |

### Transformations

You can declare transformations which will be performed on the value when
assigned. This also works with default values. (The transformation will be
applied to the default value.)

**Example:**

Given the following declaration:

```ruby
class Foo
include Stimpack::OptionsDeclaration

option :bar, transform: ->(value) { value.upcase }
end
```

values assigned to `bar` will now be upcased:

```ruby
foo = Foo.new(bar: "baz")

foo.bar
#=> "BAZ"
```

You can also use the name of method on the value, passed as a symbol.

**Example:**

Given the following declaration:

```ruby
class Foo
include Stimpack::OptionsDeclaration

option :bar, transform: :symbolize_keys
end
```

hashes assigned to `bar` will now have their keys symbolized:

```ruby
foo = Foo.new(bar: { "baz" => "qux" })

foo.bar
#=> { baz: "qux" }
```

## ResultMonad

Expand Down
13 changes: 11 additions & 2 deletions lib/stimpack/options_declaration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,23 @@ def self.extended(klass)
# option :user
# end
#
def option(*identifiers, required: true, default: Option::MISSING_VALUE, private_reader: true) # rubocop:disable Metrics/MethodLength
# rubocop:disable Metrics/MethodLength
def option(
*identifiers,
required: true,
default: Option::MISSING_VALUE,
transform: Option::NO_TRANSFORM,
private_reader: true
)
self.options_configuration = options_configuration.merge(
identifiers.map do |identifier|
[
identifier.to_sym,
Option.new(
identifier.to_sym,
required: required,
default: default
default: default,
transform: transform
)
]
end.to_h
Expand All @@ -78,6 +86,7 @@ def option(*identifiers, required: true, default: Option::MISSING_VALUE, private
end
end
end
# rubocop:enable Metrics/MethodLength

def options
options_configuration.keys
Expand Down
4 changes: 3 additions & 1 deletion lib/stimpack/options_declaration/initializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,11 @@ def check_for_missing_options!
def assign_option(option)
assigned_value = options[option.name]

value = assigned_value.nil? ? option.default_value : assigned_value

service.instance_variable_set(
"@#{option.name}",
assigned_value.nil? ? option.default_value : assigned_value
option.transformed_value(value)
)
end

Expand Down
14 changes: 12 additions & 2 deletions lib/stimpack/options_declaration/option.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ class Option
# omitted and one that was explicitly set to `nil`.
#
MISSING_VALUE = "__missing__"
NO_TRANSFORM = "__noop__"

def initialize(name, required:, default:)
def initialize(name, required:, default:, transform:)
@name = name
@default = default
@required = required
@transform = transform
end

attr_reader :name
Expand All @@ -24,6 +26,10 @@ def default_value
default.respond_to?(:call) ? default.() : default
end

def transformed_value(value)
transform? ? transform.to_proc.(value) : value
end

def required?
required && !default?
end
Expand All @@ -36,9 +42,13 @@ def default?
default != MISSING_VALUE
end

def transform?
transform != NO_TRANSFORM
end

private

attr_reader :default, :required
attr_reader :default, :required, :transform
end
end
end
2 changes: 1 addition & 1 deletion lib/stimpack/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module Stimpack
VERSION = "0.5.6"
VERSION = "0.6.0"
end
35 changes: 26 additions & 9 deletions spec/stimpack/options_declaration_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,36 +15,39 @@
option :qux, default: "Foo"
option :quuz, default: nil
option :quux, default: -> { "Bar" }
option :corge, transform: ->(value) { value.upcase }
option :grault, default: "bar", transform: ->(value) { value.upcase }
option :garply, default: "baz", transform: :upcase
end
end

describe ".option" do
it { expect(service.options_configuration.size).to eq(6) }
it { expect(service.options_configuration.size).to eq(9) }
it { expect(service.options_configuration.values).to all(be_a(described_class::Option)) }

describe "private_reader (option)" do
let(:public_instance_methods) { service.public_instance_methods(false) }
let(:private_instance_methods) { service.private_instance_methods(false) }

it { expect(public_instance_methods).to contain_exactly(:baz) }
it { expect(private_instance_methods).to contain_exactly(:foo, :bar, :qux, :quux, :quuz) }
it { expect(private_instance_methods).to contain_exactly(:foo, :bar, :qux, :quux, :quuz, :corge, :grault, :garply) } # rubocop:disable Layout/LineLength
end
end

describe ".options" do
it { expect(service.options).to contain_exactly(:foo, :bar, :baz, :qux, :quux, :quuz) }
it { expect(service.options).to contain_exactly(:foo, :bar, :baz, :qux, :quux, :quuz, :corge, :grault, :garply) }
end

describe ".required_options" do
it { expect(service.required_options).to contain_exactly(:foo, :baz) }
it { expect(service.required_options).to contain_exactly(:foo, :baz, :corge) }
end

describe ".optional_options" do
it { expect(service.optional_options).to contain_exactly(:bar, :qux, :quux, :quuz) }
it { expect(service.optional_options).to contain_exactly(:bar, :qux, :quux, :quuz, :grault, :garply) }
end

describe ".default_options" do
it { expect(service.default_options).to contain_exactly(:qux, :quux, :quuz) }
it { expect(service.default_options).to contain_exactly(:qux, :quux, :quuz, :grault, :garply) }
end

describe "#initialize" do
Expand All @@ -56,7 +59,8 @@
let(:options) do
{
foo: 1,
baz: 2
baz: 2,
corge: "foo"
}
end

Expand All @@ -69,7 +73,8 @@
let(:options) do
{
foo: 1,
baz: 2
baz: 2,
corge: "foo"
}
end

Expand All @@ -93,6 +98,18 @@
it { expect(instance.send(:quux)).to eq("Bar") }
it { expect(instance.send(:quuz)).to eq(nil) }
end

context "when transform is applied to user input" do
it { expect(instance.send(:corge)).to eq("FOO") }
end

context "when transform is applied to default" do
it { expect(instance.send(:grault)).to eq("BAR") }
end

context "when using a method name for transform" do
it { expect(instance.send(:garply)).to eq("BAZ") }
end
end
end

Expand All @@ -114,7 +131,7 @@ def initialize(hello, world:, **options)
end

let(:instance) do
sub_klass.new("hello", world: "world", foo: 1, baz: 2, lorem: 3, ipsum: 4) do |instance|
sub_klass.new("hello", world: "world", foo: 1, baz: 2, corge: "foo", lorem: 3, ipsum: 4) do |instance|
instance.qux = instance.lorem + instance.ipsum
end
end
Expand Down

0 comments on commit 5f6f1f7

Please sign in to comment.