Skip to content

Commit

Permalink
Use evaluate implementation from granite (#18)
Browse files Browse the repository at this point in the history
  • Loading branch information
andriusch authored Jan 16, 2023
1 parent 2163df6 commit 2103414
Show file tree
Hide file tree
Showing 10 changed files with 181 additions and 24 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# master

## Next

* Drop support for taking `model` as first argument in default/readonly/enum/normalize. This means that `default: -> (model) { model.other_field}` is no longer supported and should be replaced with `default: -> { other_field }`.
* Add support for evaluating `Symbol` for readonly/enum/normalize. If symbol is passed in one of those options, method with that name will be called when evaluating the value.

## v0.3.0

- [BREAKING] Stop automatically saving `references_one`/`references_many` when applying changes.
Expand Down
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,9 +172,10 @@ It is possible to provide default values for attributes and they will act in the

```ruby
attribute :check, Boolean, default: false # Simply false by default
attribute :today, Date, default: ->{ Time.zone.now.to_date } # Dynamic default value
attribute :today_wday, Integer, default: ->{ today.wday } # Default is evaluated in instance context
attribute :today_wday, Integer, default: ->(instance) { instance.today.wday } # The same as previous, but instance provided explicitly
attribute :wday, Integer, default: ->{ today.wday } # Default evaluated in instance context
def calculate_today
Time.zone.now.today
end
```

##### Enums
Expand All @@ -201,8 +202,8 @@ attribute :title, String, normalizers: [->(value) { value.strip }, trim: {length

```ruby
attribute :name, String, readonly: true # Readonly forever
attribute :name, String, readonly: -> { true } # Conditionally readonly
attribute :name, String, readonly: ->(instance) { instance.subject.present? } # Explicit instance
attribute :name, String, readonly: :name_changed? # Conditional with calling method
attribute :name, String, readonly: -> { subject.present? } # Conditional with lambda
```

#### Collection
Expand Down
1 change: 1 addition & 0 deletions lib/granite/form.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
require 'active_model'

require 'granite/form/version'
require 'granite/form/util'
require 'granite/form/errors'
require 'granite/form/extensions'
require 'granite/form/undefined_class'
Expand Down
1 change: 1 addition & 0 deletions lib/granite/form/model.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ module Model
include ActiveModel::Serialization
include ActiveModel::Serializers::JSON

include Util
include Conventions
include Attributes
include Validations
Expand Down
6 changes: 3 additions & 3 deletions lib/granite/form/model/attributes/attribute.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,15 @@ def read_before_type_cast
end

def default
defaultizer.is_a?(Proc) ? evaluate(&defaultizer) : defaultizer
owner.evaluate_if_proc(defaultizer)
end

def defaultize(value, default_value = nil)
!defaultizer.nil? && value.nil? ? default_value || default : value
end

def enum
source = enumerizer.is_a?(Proc) ? evaluate(&enumerizer) : enumerizer
source = owner.evaluate(enumerizer)

case source
when Range
Expand All @@ -57,7 +57,7 @@ def normalize(value)
normalizers.inject(value) do |val, normalizer|
case normalizer
when Proc
evaluate(val, &normalizer)
owner.evaluate(normalizer, val)
when Hash
normalizer.inject(val) do |v, (name, options)|
Granite::Form.normalizer(name).call(v, options, self)
Expand Down
11 changes: 1 addition & 10 deletions lib/granite/form/model/attributes/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def query
end

def readonly?
!!(readonly.is_a?(Proc) ? evaluate(&readonly) : readonly)
!!owner.evaluate(readonly)
end

def inspect_attribute
Expand Down Expand Up @@ -91,15 +91,6 @@ def pollute

private

def evaluate(*args, &block)
if block.arity >= 0 && block.arity <= args.length
owner.instance_exec(*args.first(block.arity), &block)
else
args = block.arity.negative? ? args : args.first(block.arity)
yield(*args, owner)
end
end

def remove_variable(*names)
names.flatten.each do |name|
name = :"@#{name}"
Expand Down
55 changes: 55 additions & 0 deletions lib/granite/form/util.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
module Granite
module Form
module Util
extend ActiveSupport::Concern

# Evaluates value and returns result based on what was passed:
# - if Proc was passed, then executes it in context of self
# - if Symbol was passed, then calls a method with that name and returns result
# - otherwise just returns the value itself
# @param value [Object] value to evaluate
# @return [Object] result of evaluation
def evaluate(value, *args)
value.is_a?(Symbol) ? evaluate_symbol(value, *args) : evaluate_if_proc(value, *args)
end

# Evaluates value and returns result based on what was passed:
# - if Proc was passed, then executes it in context of self
# - otherwise just returns the value itself
# @param value [Object] value to evaluate
# @return [Object] result of evaluation
def evaluate_if_proc(value, *args)
value.is_a?(Proc) ? evaluate_proc(value, *args) : value
end

# Evaluates `if` or `unless` conditions present in the supplied
# `options` being it a symbol or callable.
#
# @param [Hash] options The method options to evaluate.
# @option options :if method name or callable
# @option options :unless method name or callable
# @return [Boolean] whether conditions are satisfied
def conditions_satisfied?(**options)
raise ArgumentError, 'You cannot specify both if and unless' if options.key?(:if) && options.key?(:unless)

if options.key?(:if)
evaluate(options[:if])
elsif options.key?(:unless)
!evaluate(options[:unless])
else
true
end
end

private

def evaluate_proc(value, *args)
instance_exec(*args, &value)
end

def evaluate_symbol(value, *args)
__send__(value, *args)
end
end
end
end
5 changes: 0 additions & 5 deletions spec/granite/form/model/attributes/attribute_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,6 @@ def attribute(*args)
specify { expect(attribute.default).to eq(nil) }
specify { expect(attribute(default: 'hello').default).to eq('hello') }
specify { expect(attribute(default: -> { value }).default).to eq(42) }
specify { expect(attribute(default: ->(object) { object.value }).default).to eq(42) }
specify { expect(attribute(default: ->(*args) { args.first.value }).default).to eq(42) }
end

describe '#defaultize' do
Expand All @@ -70,7 +68,6 @@ def attribute(*args)
specify { expect(attribute(enum: -> { 'hello' }).enum).to eq(['hello'].to_set) }
specify { expect(attribute(enum: -> { ['hello', 42] }).enum).to eq(['hello', 42].to_set) }
specify { expect(attribute(enum: -> { value }).enum).to eq((1..5).to_a.to_set) }
specify { expect(attribute(enum: ->(object) { object.value }).enum).to eq((1..5).to_a.to_set) }
end

describe '#enumerize' do
Expand All @@ -93,8 +90,6 @@ def attribute(*args)
let(:other) { 'other' }

specify { expect(attribute(normalizer: ->(_v) { value }).normalize(' hello ')).to eq('value') }
specify { expect(attribute(normalizer: ->(_v, object) { object.value }).normalize(' hello ')).to eq('value') }
specify { expect(attribute(normalizer: ->(_v, _object) { other }).normalize(' hello ')).to eq('other') }
end

context 'integration' do
Expand Down
3 changes: 2 additions & 1 deletion spec/granite/form/model/attributes_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -247,13 +247,14 @@ def assign_attributes(attrs)
context 'attributes integration' do
let(:model) do
stub_class do
include Granite::Form::Util
include Granite::Form::Model::Attributes
include Granite::Form::Model::Associations
attr_accessor :name

attribute :id, Integer
attribute :hello, Object
attribute :string, String, default: ->(record) { record.name }
attribute :string, String, default: -> { name }
attribute :count, Integer, default: '10'
attribute(:calc, Integer) { 2 + 3 }
attribute :enum, Integer, enum: [1, 2, 3]
Expand Down
108 changes: 108 additions & 0 deletions spec/granite/form/util_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
require 'spec_helper'

RSpec.describe Granite::Form::Util do
subject(:dummy) { Dummy.new('John') }

before do
stub_class(:dummy, Object) do
attr_accessor :name

def initialize(name)
@name = name
end

def full_name(last_name)
[name, last_name].join(' ')
end
end

Dummy.include described_class
end

describe '#evaluate' do
subject { dummy.evaluate(target) }
let(:target) { 'Peter' }

it { is_expected.to eq('Peter') }

context 'when symbol is passed' do
let(:target) { :name }

it { is_expected.to eq('John') }
end

context 'when lambda is passed' do
let(:target) { -> { name } }

it { is_expected.to eq('John') }
end

context 'with extra arguments' do
subject { dummy.evaluate(target, 'Doe') }

context 'when symbol is passed' do
let(:target) { :full_name }

it { is_expected.to eq('John Doe') }
end

context 'when lambda is passed' do
let(:target) { ->(last_name) { ['John', last_name].join(' ') } }

it { is_expected.to eq('John Doe') }
end
end
end

describe '#evaluate_if_proc' do
subject { dummy.evaluate(target) }
let(:target) { 'Peter' }

it { is_expected.to eq('Peter') }

context 'when lambda is passed' do
let(:target) { -> { name } }

it { is_expected.to eq('John') }
end

context 'with extra arguments' do
subject { dummy.evaluate(target, 'Doe') }

let(:target) { ->(last_name) { ['John', last_name].join(' ') } }

it { is_expected.to eq('John Doe') }
end
end

describe '#conditions_satisfied?' do
subject { dummy.conditions_satisfied?(**conditions) }
let(:conditions) { {if: -> { name == 'John' }} }

it { is_expected.to be_truthy }

context 'when if condition is satisfied' do
before { dummy.name = 'Peter' }

it { is_expected.to be_falsey }
end

context 'when unless condition is passed' do
let(:conditions) { {unless: :name} }

it { is_expected.to be_falsey }
end

context 'when no condition is passed' do
let(:conditions) { {} }

it { is_expected.to be_truthy }
end

context 'when both if & unless are passed' do
let(:conditions) { {if: :name, unless: :name} }

it { expect { subject }.to raise_error(ArgumentError) }
end
end
end

0 comments on commit 2103414

Please sign in to comment.