Skip to content

Commit

Permalink
'with' caster
Browse files Browse the repository at this point in the history
  • Loading branch information
EugZol committed Oct 5, 2023
1 parent c209552 commit fea9889
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 5 deletions.
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ It is currently used in production in several projects (mainly as request parame
- [`pick(*keys)`](#pickkeys)
- [`remove`](#remove)
- [`responds_to(method, error_key = nil)`](#responds_tomethod-error_key--nil)
- [`with(key, caster)`](#withkey-caster)
- [`transform_to_value(value)`](#transform_to_valuevalue)
- ["Web-form" types](#web-form-types)
- [`iso8601(error_key = nil)`](#iso8601error_key--nil)
Expand Down Expand Up @@ -763,6 +764,41 @@ Returns ValidResult if and only if the value `#responds_to?(method)`. Doesn't tr

I18n keys: `error_key`, `'.responds_to'`, `'datacaster.errors.responds_to'`. Adds `reference` i18n variable, setting it to `method.to_s`.

#### `with(key, caster)`

Returns ValidResult if and only if value is enumerable and `caster` returns ValidResult. Transforms incoming hash, providing value described by `key` to `caster`, and putting its result back into the original hash.

```ruby
upcase_name =
Datacaster.schema do
with(:name, transform(&:upcase))
end

upcase_name.(name: 'Josh')
# => Datacaster::ValidResult({:name=>"JOSH"})
```

If an array is provided instead of string or Symbol for `key` argument, it is treated as array of key names for a deeply nested value:

```ruby
upcase_person_name =
Datacaster.schema do
with([:person, :name], transform(&:upcase))
end

upcase_person_name.(person: {name: 'Josh'})
# => Datacaster::ValidResult({:person=>{:name=>"JOSH"}})

upcase_person_name.({})
# => Datacaster::ErrorResult({:person=>["is not Enumerable"]})
```

Note that `Datacaster.absent` will be provided to `caster` if corresponding key is absent from the value.

I18n keys:

* is not enumerable – `'.must_be'`, `'datacaster.errors.must_be'`. Adds `reference` i18n variable, setting it to `"Enumerable"`.

#### `transform_to_value(value)`

Always returns ValidResult. The value is transformed to provided argument (disregarding the original value). See also [`default`](#defaultdefault_value-on-nil).
Expand Down
4 changes: 2 additions & 2 deletions lib/datacaster/hash_mapper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ def cast(object, runtime:)
unwrapped = new_value.valid? ? new_value.value : new_value.raw_errors

if key.length != unwrapped.length
raise TypeError.new("When using transform_to_hash([:a, :b, :c] => validator), validator should return Array "\
raise TypeError, "When using transform_to_hash([:a, :b, :c] => validator), validator should return Array "\
"with number of elements equal to the number of elements in left-hand-side array.\n" \
"Got the following (values or errors) instead: #{keys.inspect} => #{values_or_errors.inspect}.")
"Got the following (values or errors) instead: #{key.inspect} => #{unwrapped.inspect}."
end
end

Expand Down
26 changes: 24 additions & 2 deletions lib/datacaster/predefined.rb
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,12 @@ def pass_if(base)
end

def pick(*keys)
if keys.empty? || keys.any? { |k| !Datacaster::Utils.pickable?(k) }
raise RuntimeError, "each argument should be String, Symbol, Integer or an array thereof", caller
if keys.empty?
raise RuntimeError, "pick(key, ...) accepts at least one argument", caller
end

if wrong = keys.find { |k| !Datacaster::Utils.pickable?(k) }
raise RuntimeError, "each argument should be String, Symbol, Integer or an array thereof, instead got #{wrong.inspect}", caller
end

retrieve_key = -> (from, key) do
Expand Down Expand Up @@ -250,6 +254,10 @@ def responds_to(method, error_key = nil)
check { |x| x.respond_to?(method) }.i18n_key(*error_keys, reference: method.to_s)
end

def steps(*casters)
AndNode.new(*casters)
end

def switch(base = nil, **on_clauses)
switch = SwitchNode.new(base)
on_clauses.reduce(switch) do |result, (k, v)|
Expand All @@ -261,6 +269,20 @@ def transform_to_value(value)
transform { value }
end

def with(keys, caster)
keys = Array(keys)

unless Datacaster::Utils.pickable?(keys)
raise RuntimeError, "provide String, Symbol, Integer or an array thereof instead of #{keys.inspect}", caller
end

if keys.length == 1
return transform_to_hash(keys[0] => pick(keys[0]) & caster)
end

with(keys[0], must_be(Enumerable) & with(keys[1..-1], caster))
end

# Strict types

def decimal(digits = 8, error_key = nil)
Expand Down
30 changes: 30 additions & 0 deletions spec/datacaster_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1052,6 +1052,36 @@
end
end

describe "'with' typecasting" do
it "picks and puts back single key" do
schema = Datacaster.schema { with(:name, transform { |x| "#{x}-1" }) }
expect(schema.(name: 'Josh').to_dry_result).to eq Success(name: 'Josh-1')
end

it "puts ErrorResult under the same key" do
schema = Datacaster.schema { with(:name, integer) }
expect(schema.(name: 'Josh').to_dry_result).to eq Failure(name: ["is not an integer"])
end

it "picks and puts back deeply nested key" do
schema =
Datacaster.schema do
with([:person, :name], transform(&:upcase))
end

expect(schema.(person: {name: 'Josh'}).to_dry_result).to eq Success(person: {name: 'JOSH'})
end

it "returns absent when failed to pick" do
schema =
Datacaster.schema do
with([:person, :name, :first], any)
end

expect(schema.(person: {}).to_dry_result).to eq Failure(person: {name: ["is not Enumerable"]})
end
end

describe "attribute mapping" do
context "when non array keys are used" do
subject { Datacaster.schema { attribute(:test) } }
Expand Down
2 changes: 1 addition & 1 deletion spec/transaction_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
perform steps(
unwrap,
typecast,
transform_to_hash(email: pick(:email) & send_email)
with(:email, send_email)
)
end

Expand Down

0 comments on commit fea9889

Please sign in to comment.