dry-mutations 0.8.1 → 0.8.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.rubocop.yml +6 -0
- data/.rubocop_todo.yml +34 -5
- data/Gemfile +3 -0
- data/README.md +71 -4
- data/dry-mutations.gemspec +4 -1
- data/lib/dry/mutations.rb +7 -2
- data/lib/dry/mutations/dsl/schema.rb +1 -1
- data/lib/dry/mutations/extensions.rb +2 -0
- data/lib/dry/mutations/extensions/command.rb +123 -0
- data/lib/dry/mutations/extensions/outcome.rb +79 -0
- data/lib/dry/mutations/transactions.rb +14 -0
- data/lib/dry/mutations/transactions/container.rb +11 -0
- data/lib/dry/mutations/transactions/dsl.rb +23 -0
- data/lib/dry/mutations/transactions/step_adapters.rb +32 -0
- data/lib/dry/mutations/transactions/step_adapters/mutate.rb +11 -0
- data/lib/dry/mutations/transactions/step_adapters/transform.rb +11 -0
- data/lib/dry/mutations/transactions/step_adapters/validate.rb +11 -0
- data/lib/dry/mutations/transactions/wrapper.rb +32 -0
- data/lib/dry/mutations/utils.rb +40 -3
- data/lib/dry/mutations/version.rb +1 -1
- metadata +63 -11
- data/lib/dry/mutations/command.rb +0 -49
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3b9a36a11f7ac423bc0a287e56f7955f8e55bfd3
|
4
|
+
data.tar.gz: c3e7009846e8dcbfe5c898f8332540fad3d48e93
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 72eff94bdc14331b0fa44fabc3ee0db5bd04ba2bceca112cedb452a35f04a2a587b39988988f3e3faf38677ffada2d445a1cf719b203d20100747adb3be21b72
|
7
|
+
data.tar.gz: 2ec5d6cfa1e2d7668faf340b5c32aa69706d175caa1f1c40d19b5b88daede7f54e25cad3a741b58f9d06b6f3ddd9104109746975d2f1727a1f3c0df95aa10442
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
@@ -27,6 +27,7 @@ Style/SignalException:
|
|
27
27
|
Style/MethodName:
|
28
28
|
Exclude:
|
29
29
|
- 'lib/dry/mutations/utils.rb'
|
30
|
+
- 'lib/dry/transactions/*.rb'
|
30
31
|
|
31
32
|
################################################################################
|
32
33
|
|
@@ -35,9 +36,14 @@ Style/Alias:
|
|
35
36
|
- 'lib/**/*'
|
36
37
|
- 'spec/**/*'
|
37
38
|
|
39
|
+
Style/Lambda:
|
40
|
+
Exclude:
|
41
|
+
- 'lib/**/*'
|
42
|
+
|
38
43
|
Style/LambdaCall:
|
39
44
|
Exclude:
|
40
45
|
- 'lib/**/*'
|
46
|
+
- 'spec/**/*'
|
41
47
|
|
42
48
|
Style/ParallelAssignment:
|
43
49
|
Exclude:
|
data/.rubocop_todo.yml
CHANGED
@@ -1,14 +1,43 @@
|
|
1
1
|
# This configuration was generated by
|
2
2
|
# `rubocop --auto-gen-config`
|
3
|
-
# on 2016-07-
|
3
|
+
# on 2016-07-13 12:03:55 +0200 using RuboCop version 0.40.0.
|
4
4
|
# The point is for the user to remove these configuration records
|
5
5
|
# one by one as the offenses are removed from the code base.
|
6
6
|
# Note that changes in the inspected code, or installation of new
|
7
7
|
# versions of RuboCop, may require this file to be generated again.
|
8
8
|
|
9
9
|
# Offense count: 1
|
10
|
-
|
10
|
+
# Cop supports --auto-correct.
|
11
|
+
# Configuration parameters: AllowUnusedKeywordArguments, IgnoreEmptyMethods.
|
12
|
+
Lint/UnusedMethodArgument:
|
11
13
|
Exclude:
|
12
|
-
- '
|
13
|
-
|
14
|
-
|
14
|
+
- 'lib/dry/mutations/transactions/dsl.rb'
|
15
|
+
|
16
|
+
# Offense count: 2
|
17
|
+
# Cop supports --auto-correct.
|
18
|
+
# Configuration parameters: EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle, SupportedLastArgumentHashStyles.
|
19
|
+
# SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit
|
20
|
+
Style/AlignHash:
|
21
|
+
Exclude:
|
22
|
+
- 'spec/dry/mutations/command_spec.rb'
|
23
|
+
|
24
|
+
# Offense count: 1
|
25
|
+
# Cop supports --auto-correct.
|
26
|
+
# Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth.
|
27
|
+
# SupportedStyles: consistent, special_for_inner_method_call, special_for_inner_method_call_in_parentheses
|
28
|
+
Style/FirstParameterIndentation:
|
29
|
+
Exclude:
|
30
|
+
- 'spec/dry/mutations/command_spec.rb'
|
31
|
+
|
32
|
+
# Offense count: 3
|
33
|
+
# Cop supports --auto-correct.
|
34
|
+
# Configuration parameters: EnforcedStyle, SupportedStyles, ConsistentQuotesInMultiline.
|
35
|
+
# SupportedStyles: single_quotes, double_quotes
|
36
|
+
Style/StringLiterals:
|
37
|
+
Enabled: false
|
38
|
+
|
39
|
+
# Offense count: 2
|
40
|
+
# Configuration parameters: EnforcedStyle, SupportedStyles.
|
41
|
+
# SupportedStyles: snake_case, camelCase
|
42
|
+
Style/VariableName:
|
43
|
+
Enabled: false
|
data/Gemfile
CHANGED
@@ -4,5 +4,8 @@ source 'https://rubygems.org'
|
|
4
4
|
gemspec
|
5
5
|
|
6
6
|
gem 'dry-validation', github: 'dry-rb/dry-validation'
|
7
|
+
gem 'dry-monads', github: 'dry-rb/dry-monads'
|
8
|
+
gem 'dry-matcher', github: 'dry-rb/dry-matcher'
|
9
|
+
gem 'dry-transaction', github: 'dry-rb/dry-transaction'
|
7
10
|
|
8
11
|
gem 'codeclimate-test-reporter', group: :test, require: nil
|
data/README.md
CHANGED
@@ -30,11 +30,11 @@ Or install it yourself as:
|
|
30
30
|
|
31
31
|
### Enable extensions for the specific mutation’s command
|
32
32
|
|
33
|
-
Prepend a `::Dry::Mutations::Command` module to your `Mutation::Command` instance:
|
33
|
+
Prepend a `::Dry::Mutations::Extensions::Command` module to your `Mutation::Command` instance:
|
34
34
|
|
35
35
|
```ruby
|
36
36
|
class MyMutation < Mutations::Command
|
37
|
-
prepend ::Dry::Mutations::Command
|
37
|
+
prepend ::Dry::Mutations::Extensions::Command
|
38
38
|
|
39
39
|
required do
|
40
40
|
model :company, class: 'Profile'
|
@@ -59,7 +59,7 @@ It is possible to mix standard mutations’ syntax with `dry-rb` schemas:
|
|
59
59
|
|
60
60
|
```ruby
|
61
61
|
class MyMutation < Mutations::Command
|
62
|
-
prepend ::Dry::Mutations::Command
|
62
|
+
prepend ::Dry::Mutations::Extensions::Command
|
63
63
|
|
64
64
|
required do
|
65
65
|
model :company, class: 'Profile'
|
@@ -93,7 +93,74 @@ required do
|
|
93
93
|
end
|
94
94
|
```
|
95
95
|
|
96
|
-
|
96
|
+
## Dealing `outcome`
|
97
|
+
|
98
|
+
### Command
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
let!(:command) do
|
102
|
+
Class.new(::Mutations::Command) do
|
103
|
+
prepend ::Dry::Mutations::Extensions::Command
|
104
|
+
|
105
|
+
required { string :name, max_length: 5 }
|
106
|
+
schema { required(:amount).filled(:int?, gt?: 0) }
|
107
|
+
|
108
|
+
def execute
|
109
|
+
@inputs
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
```
|
114
|
+
|
115
|
+
### Using `Either` monad
|
116
|
+
|
117
|
+
```ruby
|
118
|
+
outcome = command.new(name: 'John', amount: 42).run
|
119
|
+
outcome.right?
|
120
|
+
#⇒ true
|
121
|
+
outcome.either.value
|
122
|
+
#⇒ { 'name' => 'John', 'amount' => 42 }
|
123
|
+
|
124
|
+
outcome = command.new(name: 'John Donne', amount: -500).run
|
125
|
+
outcome.right?
|
126
|
+
#⇒ false
|
127
|
+
outcome.left?
|
128
|
+
#⇒ true
|
129
|
+
outcome.either
|
130
|
+
#⇒ Left({
|
131
|
+
# "name"=>#<Dry::Mutations::Errors::ErrorAtom:0x00000003b4e7b0
|
132
|
+
# @key="name",
|
133
|
+
# @symbol=:max_length,
|
134
|
+
# @message="size cannot be greater than 5",
|
135
|
+
# @index=0,
|
136
|
+
# @dry_message=#<Dry::Validation::Message
|
137
|
+
# predicate=:max_size?
|
138
|
+
# path=[:name]
|
139
|
+
# text="size cannot be greater than 5"
|
140
|
+
# options={:args=>[5], :rule=>:name, :each=>false}>>,
|
141
|
+
# "amount"=>#<Dry::Mutations::Errors::ErrorAtom:0x00000003b4e508
|
142
|
+
# @key="amount",
|
143
|
+
# @symbol=:gt?,
|
144
|
+
# @message="must be greater than 0",
|
145
|
+
# @index=1,
|
146
|
+
# @dry_message=#<Dry::Validation::Message
|
147
|
+
# predicate=:gt?
|
148
|
+
# path=[:amount]
|
149
|
+
# text="must be greater than 0"
|
150
|
+
# options={:args=>[0], :rule=>:amount, :each=>false}>>
|
151
|
+
# })
|
152
|
+
outcome.either.value
|
153
|
+
#⇒ the hash ⇑ above
|
154
|
+
```
|
155
|
+
|
156
|
+
### Using `Matcher`
|
157
|
+
|
158
|
+
```ruby
|
159
|
+
expect(outcome.match { |m| m.success(&:keys) }).to match_array(%w(amount name))
|
160
|
+
expect(outcome.match { |m| m.failure(&:keys) }).to be_nil
|
161
|
+
```
|
162
|
+
|
163
|
+
## Turn On Globally (use with caution!)
|
97
164
|
|
98
165
|
ENV['GLOBAL_DRY_MUTATIONS'] = 'true' && rake
|
99
166
|
|
data/dry-mutations.gemspec
CHANGED
@@ -34,6 +34,9 @@ Gem::Specification.new do |spec|
|
|
34
34
|
|
35
35
|
spec.add_dependency 'activesupport', '~> 3.2' # prevent mutations to require activesupport 5
|
36
36
|
spec.add_dependency 'mutations', '~> 0.8'
|
37
|
-
spec.add_dependency 'dry-validation', '~> 0.8'
|
38
37
|
spec.add_dependency 'hashie', '~> 3'
|
38
|
+
spec.add_dependency 'dry-validation'
|
39
|
+
spec.add_dependency 'dry-matcher'
|
40
|
+
spec.add_dependency 'dry-monads'
|
41
|
+
spec.add_dependency 'dry-transaction'
|
39
42
|
end
|
data/lib/dry/mutations.rb
CHANGED
@@ -1,5 +1,9 @@
|
|
1
1
|
require 'mutations'
|
2
|
+
|
2
3
|
require 'dry-validation'
|
4
|
+
require 'dry-transaction'
|
5
|
+
require 'dry-matcher'
|
6
|
+
require 'dry-monads'
|
3
7
|
|
4
8
|
require 'dry/mutations/version'
|
5
9
|
require 'dry/mutations/utils'
|
@@ -7,7 +11,8 @@ require 'dry/mutations/monkeypatches'
|
|
7
11
|
require 'dry/mutations/predicates'
|
8
12
|
require 'dry/mutations/errors'
|
9
13
|
require 'dry/mutations/dsl'
|
10
|
-
require 'dry/mutations/
|
14
|
+
require 'dry/mutations/extensions'
|
15
|
+
require 'dry/mutations/transactions'
|
11
16
|
|
12
17
|
module Dry
|
13
18
|
# A dry implementation of mutations interface introduced by
|
@@ -25,6 +30,6 @@ module Dry
|
|
25
30
|
end
|
26
31
|
|
27
32
|
DSL::Types::Nested.extend DSL::Module
|
28
|
-
::Mutations::Command.prepend Command if Utils.Truthy?(ENV['GLOBAL_DRY_MUTATIONS'])
|
33
|
+
::Mutations::Command.prepend Extensions::Command if Utils.Truthy?(ENV['GLOBAL_DRY_MUTATIONS'])
|
29
34
|
end
|
30
35
|
end
|
@@ -15,7 +15,7 @@ module Dry
|
|
15
15
|
this = is_a?(Class) ? self : self.class
|
16
16
|
|
17
17
|
parent_with_schema = this.ancestors.drop(1).detect do |klazz|
|
18
|
-
next if [this, ::Mutations::Command, ::Dry::Mutations::Command].include?(klazz)
|
18
|
+
next if [this, ::Mutations::Command, ::Dry::Mutations::Extensions::Command].include?(klazz)
|
19
19
|
klazz.respond_to?(:schema) && klazz.schema.is_a?(Validation::Schema)
|
20
20
|
end
|
21
21
|
parent_with_schema ? Class.new(parent_with_schema.schema.class).new : empty_schema
|
@@ -0,0 +1,123 @@
|
|
1
|
+
module Dry
|
2
|
+
module Mutations
|
3
|
+
module Extensions
|
4
|
+
module Command # :nodoc:
|
5
|
+
def self.prepended base
|
6
|
+
fail ArgumentError, "Can not prepend #{self.class} to #{base.class}: base class must be a ::Mutations::Command descendant." unless base < ::Mutations::Command
|
7
|
+
base.extend(DSL::Module) unless base.ancestors.include?(DSL::Module)
|
8
|
+
base.extend(Module.new do
|
9
|
+
def call(*args)
|
10
|
+
new(*args).call
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_proc
|
14
|
+
->(*args) { new(*args).call }
|
15
|
+
end
|
16
|
+
|
17
|
+
if base.name && !::Kernel.methods.include?(base_name = base.name.split('::').last.to_sym)
|
18
|
+
::Kernel.class_eval <<-FACTORY, __FILE__, __LINE__ + 1
|
19
|
+
def #{base_name}(*args)
|
20
|
+
puts "Gonna call [#{base}.call(*args)] with \#{args.inspect}"
|
21
|
+
#{base}.call(*args)
|
22
|
+
end
|
23
|
+
FACTORY
|
24
|
+
end
|
25
|
+
end)
|
26
|
+
|
27
|
+
base.singleton_class.prepend(Module.new do
|
28
|
+
def respond_to_missing?(method_name, include_private = false)
|
29
|
+
[:call, :to_proc].include?(method_name) || super
|
30
|
+
end
|
31
|
+
end)
|
32
|
+
end
|
33
|
+
|
34
|
+
attr_reader :validation
|
35
|
+
|
36
|
+
def initialize(*args)
|
37
|
+
@raw_inputs = args.inject(Utils.Hash({})) do |h, arg|
|
38
|
+
fail ArgumentError.new("All arguments must be hashes. Given: #{args.inspect}.") unless arg.is_a?(Hash)
|
39
|
+
h.merge!(arg)
|
40
|
+
end
|
41
|
+
|
42
|
+
@validation_result = schema.(@raw_inputs)
|
43
|
+
|
44
|
+
@inputs = Utils.Hash @validation_result.output
|
45
|
+
|
46
|
+
# dry: {:name=>["size cannot be greater than 10"],
|
47
|
+
# :properties=>{:first_arg=>["must be a string", "is in invalid format"]},
|
48
|
+
# :second_arg=>{:second_sub_arg=>["must be one of: 42"]},
|
49
|
+
# :amount=>["must be one of: 42"]}}
|
50
|
+
# mut: {:name=>#<Mutations::ErrorAtom:0x00000009534e50 @key=:name, @symbol=:max_length, @message=nil, @index=nil>,
|
51
|
+
# :properties=>{
|
52
|
+
# :second_arg=>{:second_sub_arg=>#<Mutations::ErrorAtom:0x000000095344a0 @key=:second_sub_arg, @symbol=:in, @message=nil, @index=nil>}
|
53
|
+
# :amount=>#<Mutations::ErrorAtom:0x00000009534068 @key=:amount, @symbol=:in, @message=nil, @index=nil>}
|
54
|
+
|
55
|
+
@errors = Errors::ErrorAtom.patch_message_set(
|
56
|
+
Errors::ErrorCompiler.new(schema).(@validation_result.to_ast.last)
|
57
|
+
)
|
58
|
+
|
59
|
+
# Run a custom validation method if supplied:
|
60
|
+
validate unless has_errors?
|
61
|
+
end
|
62
|
+
|
63
|
+
########################################################################
|
64
|
+
### Functional helpers
|
65
|
+
########################################################################
|
66
|
+
|
67
|
+
def call
|
68
|
+
run.either
|
69
|
+
end
|
70
|
+
|
71
|
+
########################################################################
|
72
|
+
### Overrides
|
73
|
+
########################################################################
|
74
|
+
|
75
|
+
def validation_outcome(result = nil)
|
76
|
+
# Outcome.new(!has_errors?, has_errors? ? nil : result, @errors, @inputs)
|
77
|
+
super.tap do |outcome|
|
78
|
+
outcome.singleton_class.tap do |klazz|
|
79
|
+
klazz.prepend Outcome unless klazz.ancestors.include?(Outcome)
|
80
|
+
end
|
81
|
+
outcome.eitherify!
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def execute
|
86
|
+
super
|
87
|
+
rescue => e
|
88
|
+
add_error(:♻, :runtime_exception, e.message)
|
89
|
+
end
|
90
|
+
|
91
|
+
def add_error(key, kind, message = nil, dry_message = nil)
|
92
|
+
fail ArgumentError.new("Invalid kind #{kind}") unless kind.is_a?(Symbol)
|
93
|
+
|
94
|
+
path = key.to_s.split('.')
|
95
|
+
# ["#<struct Dry::Validation::Message
|
96
|
+
# predicate=:int?,
|
97
|
+
# path=[:maturity_set, :maturity_days_set, :days],
|
98
|
+
# text=\"must be an integer\",
|
99
|
+
# options={:args=>[], :rule=>:days, :each=>false}>"
|
100
|
+
dry_message ||= ::Dry::Validation::Message.new(kind, *path.map(&:to_sym), message, rule: :♻)
|
101
|
+
atom = Errors::ErrorAtom.new(key, kind, dry_message, message: message)
|
102
|
+
|
103
|
+
last = path.pop
|
104
|
+
(@errors ||= ::Mutations::ErrorHash.new).tap do |errs|
|
105
|
+
path.inject(errs) do |cur_errors, part|
|
106
|
+
cur_errors[part.to_sym] ||= ::Mutations::ErrorHash.new
|
107
|
+
end[last] = atom
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def messages
|
112
|
+
@messages ||= @errors && @errors.values.map(&:dry_message)
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
|
117
|
+
def schema
|
118
|
+
@schema ||= self.class.schema
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module Dry
|
2
|
+
module Mutations
|
3
|
+
module Extensions
|
4
|
+
module Outcome # :nodoc:
|
5
|
+
include Dry::Monads::Either::Mixin
|
6
|
+
|
7
|
+
class EitherCalculator # :nodoc:
|
8
|
+
include Dry::Monads::Either::Mixin
|
9
|
+
|
10
|
+
attr_accessor :outcome
|
11
|
+
|
12
|
+
def calculate
|
13
|
+
Right(outcome).bind do |value|
|
14
|
+
value.success? ? Right(value.result) : Left(value.errors)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class Matcher # :nodoc:
|
20
|
+
SUCCESS = Dry::Matcher::Case.new(
|
21
|
+
match: ->(value) { value.right? },
|
22
|
+
resolve: ->(value) { value.either.value }
|
23
|
+
)
|
24
|
+
|
25
|
+
# rubocop:disable Style/BlockDelimiters
|
26
|
+
FAILURE = Dry::Matcher::Case.new(
|
27
|
+
match: -> (value, *patterns) {
|
28
|
+
value.left? && (patterns.none? || (patterns & value.either.value.keys).any?)
|
29
|
+
},
|
30
|
+
resolve: -> (value) { value.either.value }
|
31
|
+
)
|
32
|
+
# rubocop:enable Style/BlockDelimiters
|
33
|
+
|
34
|
+
# Build the matcher
|
35
|
+
def self.!
|
36
|
+
Dry::Matcher.new(success: SUCCESS, failure: FAILURE)
|
37
|
+
end
|
38
|
+
|
39
|
+
private_constant :SUCCESS
|
40
|
+
private_constant :FAILURE
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.prepended base
|
44
|
+
fail ArgumentError, "Can not prepend #{self.class} to #{base.class}: base class must be a ::Mutations::Outcome descendant." unless base < ::Mutations::Outcome
|
45
|
+
end
|
46
|
+
|
47
|
+
attr_reader :either
|
48
|
+
|
49
|
+
def initialize(is_success, result, errors, inputs)
|
50
|
+
super is_success, result, errors, inputs
|
51
|
+
etherify!
|
52
|
+
end
|
53
|
+
|
54
|
+
def eitherify!
|
55
|
+
calc = EitherCalculator.new
|
56
|
+
calc.outcome = self
|
57
|
+
@either = calc.calculate
|
58
|
+
end
|
59
|
+
|
60
|
+
def right?
|
61
|
+
@either.is_a?(Right)
|
62
|
+
end
|
63
|
+
|
64
|
+
def left?
|
65
|
+
@either.is_a?(Left)
|
66
|
+
end
|
67
|
+
|
68
|
+
def value
|
69
|
+
@either.value
|
70
|
+
end
|
71
|
+
|
72
|
+
def match
|
73
|
+
fail 'Call to Outcome#match requires a block passed.' unless block_given?
|
74
|
+
Matcher.!.(self, &Proc.new)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'dry-transaction'
|
2
|
+
|
3
|
+
require 'dry/mutations/transactions/wrapper'
|
4
|
+
require 'dry/mutations/transactions/container'
|
5
|
+
require 'dry/mutations/transactions/step_adapters'
|
6
|
+
require 'dry/mutations/transactions/dsl'
|
7
|
+
|
8
|
+
module Dry
|
9
|
+
module Mutations # :nodoc:
|
10
|
+
def self.Transaction(**params, &cb)
|
11
|
+
# ::Dry::Transaction(container: )
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Dry
|
2
|
+
module Mutations
|
3
|
+
module Transactions # :nodoc:
|
4
|
+
Container = lambda do |whatever|
|
5
|
+
whatever.respond_to?(:call) ? whatever : Utils.Constant(whatever).tap do |p|
|
6
|
+
fail ArgumentError, "The argument must respond to :call, though #{k.inspect} passed." unless p.respond_to? :call
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Dry
|
2
|
+
module Mutations
|
3
|
+
module Transactions # :nodoc:
|
4
|
+
# http://dry-rb.org/gems/dry-transaction/basic-usage/
|
5
|
+
# save_user = Dry.Transaction(container: Container) do
|
6
|
+
# step :process
|
7
|
+
# step :validate
|
8
|
+
# step :persist
|
9
|
+
# end
|
10
|
+
module DSL # :nodoc:
|
11
|
+
def chain **params
|
12
|
+
return enum_for(:chain) unless block_given? # FIXME: Needed? Works? Remove?
|
13
|
+
|
14
|
+
λ = Proc.new
|
15
|
+
|
16
|
+
::Dry.Transaction(container: ::Dry::Mutations::Transactions::Container, step_adapters: StepAdapters) do
|
17
|
+
instance_eval(&λ)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Dry
|
2
|
+
module Mutations
|
3
|
+
module Transactions # :nodoc:
|
4
|
+
# http://dry-rb.org/gems/dry-transaction/custom-step-adapters/
|
5
|
+
# step adapters must provide a single `#call(step, *args, input)` method,
|
6
|
+
# which should return the step’s result wrapped in an `Either` object.
|
7
|
+
class StepAdapters < ::Dry::Transaction::StepAdapters # :nodoc:
|
8
|
+
class Move # :nodoc:
|
9
|
+
def self.inherited(sub)
|
10
|
+
name = Utils.Snake(sub, short: true, symbolize: true)
|
11
|
+
StepAdapters.register name, sub.new
|
12
|
+
adapters[name] = sub
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.adapters
|
16
|
+
@adapters ||= Utils.Hash
|
17
|
+
end
|
18
|
+
|
19
|
+
def call(step, *args, input)
|
20
|
+
binding.pry unless args.empty? # FIXME
|
21
|
+
step.operation.(input)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# preload predefined step adapters
|
26
|
+
Dir[File.expand_path('step_adapters', __dir__) << '/*'].each do |f|
|
27
|
+
require_relative f
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Dry
|
2
|
+
module Mutations
|
3
|
+
module Transactions # :nodoc:
|
4
|
+
# http://dry-rb.org/gems/dry-transaction/custom-step-adapters/
|
5
|
+
# step adapters must provide a single `#call(step, *args, input)` method,
|
6
|
+
# which should return the step’s result wrapped in an `Either` object.
|
7
|
+
class Mutate < StepAdapters::Move # :nodoc:
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Dry
|
2
|
+
module Mutations
|
3
|
+
module Transactions # :nodoc:
|
4
|
+
# http://dry-rb.org/gems/dry-transaction/custom-step-adapters/
|
5
|
+
# step adapters must provide a single `#call(step, *args, input)` method,
|
6
|
+
# which should return the step’s result wrapped in an `Either` object.
|
7
|
+
class Transform < StepAdapters::Move # :nodoc:
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Dry
|
2
|
+
module Mutations
|
3
|
+
module Transactions # :nodoc:
|
4
|
+
# http://dry-rb.org/gems/dry-transaction/custom-step-adapters/
|
5
|
+
# step adapters must provide a single `#call(step, *args, input)` method,
|
6
|
+
# which should return the step’s result wrapped in an `Either` object.
|
7
|
+
class Validate < StepAdapters::Move # :nodoc:
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Dry
|
2
|
+
module Mutations
|
3
|
+
module Transactions # :nodoc:
|
4
|
+
class Wrapper # :nodoc:
|
5
|
+
def initialize(**params)
|
6
|
+
(@wrappers = params).each do |name, λ|
|
7
|
+
fail ArgumentError, "Wrapper’s constructor requires hash of { name ⇒ λ(proc, value) }" unless name.is_a?(Symbol) && λ.is_a?(Proc)
|
8
|
+
singleton_class.send :define_method, name do |value, naked|
|
9
|
+
-> { λ.(value, naked.()) }
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def wrap λ, **params
|
15
|
+
end
|
16
|
+
|
17
|
+
private_class_method :new
|
18
|
+
end
|
19
|
+
|
20
|
+
class Options < Wrapper
|
21
|
+
OPTIONS = {
|
22
|
+
failure: ->(value, λ) do
|
23
|
+
result = λ.()
|
24
|
+
Utils.Falsey?(value) ? result : Right(nil)
|
25
|
+
end
|
26
|
+
}.freeze
|
27
|
+
def initialize
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/lib/dry/mutations/utils.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
module Dry
|
2
2
|
module Mutations
|
3
3
|
module Utils # :nodoc:
|
4
|
-
FALSEY = /\A#{Regexp.union(%w(0 false falsey no n)).source}\z/i
|
5
|
-
TRUTHY = /\A#{Regexp.union(%w(1 true truthy yes y)).source}\z/i
|
4
|
+
FALSEY = /\A#{Regexp.union(%w(0 skip false falsey no n)).source}\z/i
|
5
|
+
TRUTHY = /\A#{Regexp.union(%w(1 use true truthy yes y)).source}\z/i
|
6
6
|
|
7
7
|
def self.Falsey? input, explicit: true
|
8
8
|
explicit ? input.to_s =~ FALSEY : input.to_s !~ TRUTHY
|
@@ -12,6 +12,43 @@ module Dry
|
|
12
12
|
explicit ? input.to_s =~ TRUTHY : input.to_s !~ FALSEY
|
13
13
|
end
|
14
14
|
|
15
|
+
def self.Snake(whatever, short: false, symbolize: false)
|
16
|
+
result = whatever.to_s.split('::').map do |e|
|
17
|
+
e.gsub(/(?<=[^\W_])(?=[A-Z])/, '_').downcase
|
18
|
+
end
|
19
|
+
result = short ? result.last : result.join('__')
|
20
|
+
symbolize ? result.to_sym : result
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.SnakeSafe(whatever, existing = [], update_existing: true, short: false, symbolize: false)
|
24
|
+
result = Snake(whatever, short: short)
|
25
|
+
str = loop do
|
26
|
+
break result unless existing.include? result
|
27
|
+
suffix = result[/(?<=_)\d+(?=\z)/]
|
28
|
+
suffix.nil? ? result << '_1' : result[-suffix.length..-1] = (suffix.to_i + 1).to_s
|
29
|
+
end.tap { |r| existing << r if update_existing }
|
30
|
+
symbolize ? str.to_sym : str
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.Camel(whatever)
|
34
|
+
whatever.to_s.split('__').map do |s|
|
35
|
+
s.gsub(/(?:\A|_)(?<letter>\w)/) { $~[:letter].upcase }
|
36
|
+
end.join('::')
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.Constant(whatever)
|
40
|
+
::Kernel.const_get(Camel(whatever))
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.Λ input, **params
|
44
|
+
case
|
45
|
+
when params[:method] then input.method(params.delete[:method].to_sym).to_proc
|
46
|
+
when input.respond_to?(:to_proc) then input.to_proc
|
47
|
+
when input.respond_to?(:call) then input.method(:call).to_proc
|
48
|
+
else fail ArgumentError, "The executor given can not be executed (forgot to specify :method param?)"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
15
52
|
# Lazy detector for Hashie::Mash
|
16
53
|
# TODO: Make it possible to choose friendly hash implementation
|
17
54
|
USE_HASHIE_MASH = Falsey?(ENV['PLAIN_HASHES'], explicit: false) && begin
|
@@ -28,7 +65,7 @@ module Dry
|
|
28
65
|
# Converts a hash to a best available hash implementation
|
29
66
|
# with stringified keys, since `Mutations` expect hash
|
30
67
|
# keys to be strings.
|
31
|
-
def self.Hash hash
|
68
|
+
def self.Hash hash = {}
|
32
69
|
case
|
33
70
|
when USE_HASHIE_MASH
|
34
71
|
Kernel.const_get('::Hashie::Mash').new(hash)
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dry-mutations
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.8.
|
4
|
+
version: 0.8.8
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Aleksei Matiushkin
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-07-
|
11
|
+
date: 2016-07-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -109,33 +109,75 @@ dependencies:
|
|
109
109
|
- !ruby/object:Gem::Version
|
110
110
|
version: '0.8'
|
111
111
|
- !ruby/object:Gem::Dependency
|
112
|
-
name:
|
112
|
+
name: hashie
|
113
113
|
requirement: !ruby/object:Gem::Requirement
|
114
114
|
requirements:
|
115
115
|
- - "~>"
|
116
116
|
- !ruby/object:Gem::Version
|
117
|
-
version: '
|
117
|
+
version: '3'
|
118
118
|
type: :runtime
|
119
119
|
prerelease: false
|
120
120
|
version_requirements: !ruby/object:Gem::Requirement
|
121
121
|
requirements:
|
122
122
|
- - "~>"
|
123
123
|
- !ruby/object:Gem::Version
|
124
|
-
version: '
|
124
|
+
version: '3'
|
125
125
|
- !ruby/object:Gem::Dependency
|
126
|
-
name:
|
126
|
+
name: dry-validation
|
127
127
|
requirement: !ruby/object:Gem::Requirement
|
128
128
|
requirements:
|
129
|
-
- - "
|
129
|
+
- - ">="
|
130
130
|
- !ruby/object:Gem::Version
|
131
|
-
version: '
|
131
|
+
version: '0'
|
132
132
|
type: :runtime
|
133
133
|
prerelease: false
|
134
134
|
version_requirements: !ruby/object:Gem::Requirement
|
135
135
|
requirements:
|
136
|
-
- - "
|
136
|
+
- - ">="
|
137
137
|
- !ruby/object:Gem::Version
|
138
|
-
version: '
|
138
|
+
version: '0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: dry-matcher
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
type: :runtime
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: dry-monads
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - ">="
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '0'
|
160
|
+
type: :runtime
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - ">="
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '0'
|
167
|
+
- !ruby/object:Gem::Dependency
|
168
|
+
name: dry-transaction
|
169
|
+
requirement: !ruby/object:Gem::Requirement
|
170
|
+
requirements:
|
171
|
+
- - ">="
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: '0'
|
174
|
+
type: :runtime
|
175
|
+
prerelease: false
|
176
|
+
version_requirements: !ruby/object:Gem::Requirement
|
177
|
+
requirements:
|
178
|
+
- - ">="
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
version: '0'
|
139
181
|
description: |2
|
140
182
|
Mutations gem interface implemented with `dry-rb`’s validation schemas.
|
141
183
|
email:
|
@@ -162,7 +204,6 @@ files:
|
|
162
204
|
- config/messages.yml
|
163
205
|
- dry-mutations.gemspec
|
164
206
|
- lib/dry/mutations.rb
|
165
|
-
- lib/dry/mutations/command.rb
|
166
207
|
- lib/dry/mutations/dsl.rb
|
167
208
|
- lib/dry/mutations/dsl/blocks.rb
|
168
209
|
- lib/dry/mutations/dsl/module.rb
|
@@ -172,8 +213,19 @@ files:
|
|
172
213
|
- lib/dry/mutations/errors.rb
|
173
214
|
- lib/dry/mutations/errors/error_atom.rb
|
174
215
|
- lib/dry/mutations/errors/error_compiler.rb
|
216
|
+
- lib/dry/mutations/extensions.rb
|
217
|
+
- lib/dry/mutations/extensions/command.rb
|
218
|
+
- lib/dry/mutations/extensions/outcome.rb
|
175
219
|
- lib/dry/mutations/monkeypatches.rb
|
176
220
|
- lib/dry/mutations/predicates.rb
|
221
|
+
- lib/dry/mutations/transactions.rb
|
222
|
+
- lib/dry/mutations/transactions/container.rb
|
223
|
+
- lib/dry/mutations/transactions/dsl.rb
|
224
|
+
- lib/dry/mutations/transactions/step_adapters.rb
|
225
|
+
- lib/dry/mutations/transactions/step_adapters/mutate.rb
|
226
|
+
- lib/dry/mutations/transactions/step_adapters/transform.rb
|
227
|
+
- lib/dry/mutations/transactions/step_adapters/validate.rb
|
228
|
+
- lib/dry/mutations/transactions/wrapper.rb
|
177
229
|
- lib/dry/mutations/utils.rb
|
178
230
|
- lib/dry/mutations/version.rb
|
179
231
|
homepage: http://github.com/am-kantox/dry-mutations
|
@@ -1,49 +0,0 @@
|
|
1
|
-
module Dry
|
2
|
-
module Mutations
|
3
|
-
module Command # :nodoc:
|
4
|
-
def self.prepended base
|
5
|
-
fail ArgumentError, "Can not prepend #{self.class} to #{base.class}: base class must be a ::Mutations::Command descendant." unless base < ::Mutations::Command
|
6
|
-
base.extend(DSL::Module) unless base.ancestors.include?(DSL::Module)
|
7
|
-
end
|
8
|
-
|
9
|
-
attr_reader :validation
|
10
|
-
|
11
|
-
def initialize(*args)
|
12
|
-
@raw_inputs = args.inject(Utils.Hash({})) do |h, arg|
|
13
|
-
fail ArgumentError.new('All arguments must be hashes') unless arg.is_a?(Hash)
|
14
|
-
h.merge!(arg)
|
15
|
-
end
|
16
|
-
|
17
|
-
@validation_result = schema.(@raw_inputs)
|
18
|
-
|
19
|
-
@inputs = Utils.Hash @validation_result.output
|
20
|
-
|
21
|
-
# dry: {:name=>["size cannot be greater than 10"],
|
22
|
-
# :properties=>{:first_arg=>["must be a string", "is in invalid format"]},
|
23
|
-
# :second_arg=>{:second_sub_arg=>["must be one of: 42"]},
|
24
|
-
# :amount=>["must be one of: 42"]}}
|
25
|
-
# mut: {:name=>#<Mutations::ErrorAtom:0x00000009534e50 @key=:name, @symbol=:max_length, @message=nil, @index=nil>,
|
26
|
-
# :properties=>{
|
27
|
-
# :second_arg=>{:second_sub_arg=>#<Mutations::ErrorAtom:0x000000095344a0 @key=:second_sub_arg, @symbol=:in, @message=nil, @index=nil>}
|
28
|
-
# :amount=>#<Mutations::ErrorAtom:0x00000009534068 @key=:amount, @symbol=:in, @message=nil, @index=nil>}
|
29
|
-
|
30
|
-
@errors = Errors::ErrorAtom.patch_message_set(
|
31
|
-
Errors::ErrorCompiler.new(schema).(@validation_result.to_ast.last)
|
32
|
-
)
|
33
|
-
|
34
|
-
# Run a custom validation method if supplied:
|
35
|
-
validate unless has_errors?
|
36
|
-
end
|
37
|
-
|
38
|
-
def messages
|
39
|
-
@messages ||= @errors && @errors.values.map(&:dry_message)
|
40
|
-
end
|
41
|
-
|
42
|
-
private
|
43
|
-
|
44
|
-
def schema
|
45
|
-
@schema ||= self.class.schema
|
46
|
-
end
|
47
|
-
end
|
48
|
-
end
|
49
|
-
end
|