diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d9ee418..13222cc 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - ruby-version: [2.6, 2.7, 3.0, 3.1, 3.2, 3.3] + ruby-version: [3.1, 3.2, 3.3] steps: - uses: actions/checkout@v3 - name: Set up Ruby @@ -23,7 +23,7 @@ jobs: - name: Run tests run: bundle exec rake - name: Coveralls GitHub Action - if: matrix.ruby-version == '3.2' + if: matrix.ruby-version == '3.3' uses: coverallsapp/github-action@v2 with: github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index 2db8e47..df197c5 100644 --- a/README.md +++ b/README.md @@ -474,10 +474,6 @@ end The `with:` parameter can always be specified for `step :validate`, and allows you to override the default mapping regardless if auto-wiring is active or not. -##### Older versions of `dry-validation` - -Pathway supports the `dry-validation` gem down to version `0.11` (inclusive) in case you still have unmigrated code. When using versions below `1.0` the concept of contract is not present and instead of calling the `contract` method to set up your validation logic, you must use the `form` method. Everything else remains the same except, obviously, that you would have to use `dry-definition`'s [old API](https://dry-rb.org/gems/dry-validation/0.13/) which is a bit different from the current one. - #### `SimpleAuth` plugin This very simple plugin adds a custom step called `:authorize`, that can be used to check for permissions and halt the operation with a `:forbidden` error when they aren't fulfilled. diff --git a/lib/pathway.rb b/lib/pathway.rb index 8a8393d..fb302b5 100644 --- a/lib/pathway.rb +++ b/lib/pathway.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -require 'ruby2_keywords' require 'forwardable' require 'dry/inflector' require 'contextualizer' @@ -11,7 +10,7 @@ module Pathway Inflector = Dry::Inflector.new class Operation class << self - ruby2_keywords def plugin(name, *args) + def plugin(name,...) require "pathway/plugins/#{Inflector.underscore(name)}" if name.is_a?(Symbol) plugin = name.is_a?(Module) ? name : Plugins.const_get(Inflector.camelize(name)) @@ -20,7 +19,7 @@ class << self self.include plugin::InstanceMethods if plugin.const_defined? :InstanceMethods self::DSL.include plugin::DSLMethods if plugin.const_defined? :DSLMethods - plugin.apply(self, *args) if plugin.respond_to?(:apply) + plugin.apply(self,...) if plugin.respond_to?(:apply) end def inherited(subclass) @@ -45,13 +44,8 @@ def initialize(type:, message: nil, details: nil) @details = details || {} end - def deconstruct - [type, message, details] - end - - def deconstruct_keys(_) - { type: type, message: message, details: details } - end + def deconstruct = [type, message, details] + def deconstruct_keys(_) = { type:, message:, details: } private @@ -62,35 +56,29 @@ def default_message_for(type) class State extend Forwardable + delegate %i([] []= fetch store include? values_at deconstruct_keys) => :@hash def initialize(operation, values = {}) @hash = operation.context.merge(values) @result_key = operation.result_key end - delegate %i([] []= fetch store include? values_at deconstruct_keys) => :@hash - def update(kargs) @hash.update(kargs) self end - def result - @hash[@result_key] - end - - def to_hash - @hash - end + def result = @hash[@result_key] + def to_hash = @hash def use(&bl) raise ArgumentError, 'a block must be provided' if !block_given? - if bl.parameters.any? {|(type,_)| type == :keyrest || type == :rest } + if bl.parameters in [*, [:rest|:keyrest,], *] raise ArgumentError, 'rest arguments are not supported' end - keys = bl.parameters.select {|(type,_)| type == :key || type == :keyreq }.map(&:last) - names = bl.parameters.select {|(type,_)| type == :req || type == :opt }.map(&:last) + keys = bl.parameters.select { _1 in :key|:keyreq, }.map(&:last) + names = bl.parameters.select { _1 in :req|:opt, }.map(&:last) if keys.any? && names.any? raise ArgumentError, 'cannot mix positional and keyword arguments' @@ -110,20 +98,19 @@ module Plugins module Base module ClassMethods attr_accessor :result_key - alias :result_at :result_key= + + alias_method :result_at, :result_key= def process(&bl) dsl = self::DSL define_method(:call) do |input| - dsl.new(State.new(self, input: input), self) + dsl.new(State.new(self, input:), self) .run(&bl) .then(&:result) end end - ruby2_keywords def call(ctx, *params) - new(ctx).call(*params) - end + def call(ctx,...) = new(ctx).call(...) def inherited(subclass) super @@ -137,18 +124,16 @@ module InstanceMethods delegate :result_key => 'self.class' delegate %i[result success failure] => Result - alias :wrap :result + alias_method :wrap, :result - def call(*) - fail 'must implement at subclass' - end + def call(*) = raise 'must implement at subclass' def error(type, message: nil, details: nil) - failure(Error.new(type: type, message: message, details: details)) + failure(Error.new(type:, message:, details:)) end def wrap_if_present(value, type: :not_found, message: nil, details: {}) - value.nil? ? error(type, message: message, details: details) : success(value) + value.nil? ? error(type, message:, details:) : success(value) end end @@ -168,45 +153,43 @@ def run(&bl) end # Execute step and preserve the former state - ruby2_keywords def step(callable, *args) + def step(callable,...) bl = _callable(callable) - - @result = @result.tee { |state| bl.call(state, *args) } + @result = @result.tee { |state| bl.call(state,...) } end # Execute step and modify the former state setting the key - def set(callable, *args, to: @operation.result_key) + def set(callable, *args, to: @operation.result_key, **kwargs, &bl) bl = _callable(callable) @result = @result.then do |state| - wrap(bl.call(state, *args)) + wrap(bl.call(state, *args, **kwargs, &bl)) .then { |value| state.update(to => value) } end end # Execute step and replace the current state completely - def map(callable) + def map(callable,...) bl = _callable(callable) - @result = @result.then(bl) + @result = @result.then { |state| bl.call(state,...) } end - def around(wrapper, &steps) + def around(execution_strategy, &dsl_block) @result.then do |state| - seq = -> (dsl = self) { @result = dsl.run(&steps) } - _callable(wrapper).call(seq, state) + dsl_runner = ->(dsl = self) { @result = dsl.run(&dsl_block) } + + _callable(execution_strategy).call(dsl_runner, state) end end - def if_true(cond, &steps) + def if_true(cond, &dsl_block) cond = _callable(cond) - around(-> seq, state { - seq.call if cond.call(state) - }, &steps) + around(->(dsl_runner, state) { dsl_runner.call if cond.call(state) }, &dsl_block) end - def if_false(cond, &steps) + def if_false(cond, &dsl_block) cond = _callable(cond) - if_true(-> state { !cond.call(state) }, &steps) + if_true(->(state) { !cond.call(state) }, &dsl_block) end alias_method :sequence, :around @@ -214,16 +197,14 @@ def if_false(cond, &steps) private - def wrap(obj) - Result.result(obj) - end + def wrap(obj) = Result.result(obj) def _callable(callable) case callable when Proc - -> *args { @operation.instance_exec(*args, &callable) }.ruby2_keywords + ->(*args, **kwargs) { @operation.instance_exec(*args, **kwargs, &callable) } when Symbol - -> *args { @operation.send(callable, *args) }.ruby2_keywords + ->(*args, **kwargs) { @operation.send(callable, *args, **kwargs) } else callable end diff --git a/lib/pathway/plugins/auto_deconstruct_state.rb b/lib/pathway/plugins/auto_deconstruct_state.rb index 40fd59e..1578616 100644 --- a/lib/pathway/plugins/auto_deconstruct_state.rb +++ b/lib/pathway/plugins/auto_deconstruct_state.rb @@ -1,12 +1,22 @@ # frozen_string_literal: true -if RUBY_VERSION =~ /^3\./ - require 'pathway/plugins/auto_deconstruct_state/ruby3' -end - module Pathway module Plugins module AutoDeconstructState + module DSLMethods + private + + def _callable(callable) + if callable.is_a?(Symbol) && @operation.respond_to?(callable, true) && + @operation.method(callable).arity != 0 && + @operation.method(callable).parameters.all? { _1 in [:key|:keyreq|:keyrest|:block,*] } + + -> state { @operation.send(callable, **state) } + else + super + end + end + end end end end diff --git a/lib/pathway/plugins/auto_deconstruct_state/ruby3.rb b/lib/pathway/plugins/auto_deconstruct_state/ruby3.rb deleted file mode 100644 index 1578616..0000000 --- a/lib/pathway/plugins/auto_deconstruct_state/ruby3.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -module Pathway - module Plugins - module AutoDeconstructState - module DSLMethods - private - - def _callable(callable) - if callable.is_a?(Symbol) && @operation.respond_to?(callable, true) && - @operation.method(callable).arity != 0 && - @operation.method(callable).parameters.all? { _1 in [:key|:keyreq|:keyrest|:block,*] } - - -> state { @operation.send(callable, **state) } - else - super - end - end - end - end - end -end diff --git a/lib/pathway/plugins/dry_validation.rb b/lib/pathway/plugins/dry_validation.rb index 76be08c..d3fd402 100644 --- a/lib/pathway/plugins/dry_validation.rb +++ b/lib/pathway/plugins/dry_validation.rb @@ -5,21 +5,82 @@ module Pathway module Plugins module DryValidation - def self.apply(operation, **kwargs) + module ClassMethods + attr_reader :contract_class, :contract_options + attr_accessor :auto_wire + + alias_method :auto_wire_options, :auto_wire + alias_method :auto_wire_options=, :auto_wire= + + def contract(base = nil, &block) + if block_given? + base ||= _base_contract + self.contract_class = Class.new(base, &block) + elsif base + self.contract_class = base + else + raise ArgumentError, 'Either a contract class or a block must be provided' + end + end + + def params(*args, **kwargs, &block) + contract { params(*args, **kwargs, &block) } + end + + def contract_class= klass + @contract_class = klass + @contract_options = (klass.dry_initializer.options - Dry::Validation::Contract.dry_initializer.options).map(&:target) + @builded_contract = @contract_options.empty? && klass.schema ? klass.new : nil + end + + def build_contract(**opts) + @builded_contract || contract_class.new(**opts) + end + + def inherited(subclass) + super + subclass.auto_wire = auto_wire + subclass.contract_class = contract_class + end + + private + + def _base_contract + superclass.respond_to?(:contract_class) ? superclass.contract_class : Dry::Validation::Contract + end + end + + module InstanceMethods + extend Forwardable + + delegate %i[build_contract contract_options auto_wire_options auto_wire] => 'self.class' + alias_method :contract, :build_contract + + def validate(state, with: nil) + if auto_wire && contract_options.any? + with ||= contract_options.zip(contract_options).to_h + end + opts = Hash(with).map { |to, from| [to, state[from]] }.to_h + validate_with(state[:input], **opts) + .then { |params| state.update(params:) } + end + + def validate_with(input, **opts) + result = contract(**opts).call(input) + + result.success? ? wrap(result.values.to_h) : error(:validation, details: result.errors.to_h) + end + end + + def self.apply(operation, auto_wire_options: (auto_wire_options_was_not_used=true; false), auto_wire: auto_wire_options) #:nocov: - if Gem.loaded_specs['dry-validation'].version < Gem::Version.new('0.11') - fail 'unsupported dry-validation gem version' - elsif Gem.loaded_specs['dry-validation'].version < Gem::Version.new('0.12') - require 'pathway/plugins/dry_validation/v0_11' - operation.plugin(Plugins::DryValidation::V0_11, **kwargs) - elsif Gem.loaded_specs['dry-validation'].version < Gem::Version.new('1.0') - require 'pathway/plugins/dry_validation/v0_12' - operation.plugin(Plugins::DryValidation::V0_12, **kwargs) - else - require 'pathway/plugins/dry_validation/v1_0' - operation.plugin(Plugins::DryValidation::V1_0, **kwargs) + unless auto_wire_options_was_not_used + warn "[DEPRECATION] `auto_wire_options` is deprecated. Please use `auto_wire` instead" end #:nocov: + + operation.auto_wire = auto_wire + operation.contract_class = Dry::Validation::Contract end end end diff --git a/lib/pathway/plugins/dry_validation/v0_11.rb b/lib/pathway/plugins/dry_validation/v0_11.rb deleted file mode 100644 index a939d15..0000000 --- a/lib/pathway/plugins/dry_validation/v0_11.rb +++ /dev/null @@ -1,96 +0,0 @@ -# frozen_string_literal: true - -module Pathway - module Plugins - module DryValidation - module V0_11 - module ClassMethods - attr_reader :form_class, :form_options - attr_accessor :auto_wire - - alias_method :auto_wire_options, :auto_wire - alias_method :auto_wire_options=, :auto_wire= - - def form(base = nil, **opts, &block) - if block_given? - base ||= _base_form - self.form_class = _block_definition(base, opts, &block) - elsif base - self.form_class = _form_class(base) - else - raise ArgumentError, 'Either a form class or a block must be provided' - end - end - - def form_class= klass - @builded_form = klass.options.empty? ? klass.new : nil - @form_class = klass - @form_options = klass.options.keys - end - - def build_form(opts = {}) - @builded_form || form_class.new(opts) - end - - def inherited(subclass) - super - subclass.form_class = form_class - subclass.auto_wire = auto_wire - end - - private - - def _base_form - superclass.respond_to?(:form_class) ? superclass.form_class : Dry::Validation::Schema::Form - end - - def _form_class(form) - form.is_a?(Class) ? form : form.class - end - - def _form_opts(opts = {}) - opts.merge(build: false) - end - - def _block_definition(base, opts, &block) - Dry::Validation.Form(_form_class(base), _form_opts(opts), &block) - end - end - - module InstanceMethods - extend Forwardable - - delegate %i[build_form form_options auto_wire_options] => 'self.class' - delegate %i[build_form form_options auto_wire_options auto_wire] => 'self.class' - alias_method :form, :build_form - - def validate(state, with: nil) - if auto_wire && form_options.any? - with ||= form_options.zip(form_options).to_h - end - opts = Hash(with).map { |opt, key| [opt, state[key]] }.to_h - validate_with(state[:input], opts) - .then { |params| state.update(params: params) } - end - - def validate_with(params, opts = {}) - val = form(opts).call(params) - - val.success? ? wrap(val.output) : error(:validation, details: val.messages) - end - end - - def self.apply(operation, auto_wire_options: (auto_wire_options_was_not_used=true; false), auto_wire: auto_wire_options) - #:nocov: - unless auto_wire_options_was_not_used - warn "[DEPRECATION] `auto_wire_options` is deprecated. Please use `auto_wire` instead" - end - #:nocov: - - operation.auto_wire = auto_wire - operation.form_class = Dry::Validation::Schema::Form - end - end - end - end -end diff --git a/lib/pathway/plugins/dry_validation/v0_12.rb b/lib/pathway/plugins/dry_validation/v0_12.rb deleted file mode 100644 index 98cccbd..0000000 --- a/lib/pathway/plugins/dry_validation/v0_12.rb +++ /dev/null @@ -1,95 +0,0 @@ -# frozen_string_literal: true - -module Pathway - module Plugins - module DryValidation - module V0_12 - module ClassMethods - attr_reader :form_class, :form_options - attr_accessor :auto_wire - - alias_method :auto_wire_options, :auto_wire - alias_method :auto_wire_options=, :auto_wire= - - def form(base = nil, **opts, &block) - if block_given? - base ||= _base_form - self.form_class = _block_definition(base, opts, &block) - elsif base - self.form_class = _form_class(base) - else - raise ArgumentError, 'Either a form class or a block must be provided' - end - end - - def form_class= klass - @builded_form = klass.options.empty? ? klass.new : nil - @form_class = klass - @form_options = klass.options.keys - end - - def build_form(opts = {}) - @builded_form || form_class.new(opts) - end - - def inherited(subclass) - super - subclass.form_class = form_class - subclass.auto_wire = auto_wire - end - - private - - def _base_form - superclass.respond_to?(:form_class) ? superclass.form_class : Dry::Validation::Schema::Params - end - - def _form_class(form) - form.is_a?(Class) ? form : form.class - end - - def _form_opts(opts = {}) - opts.merge(build: false) - end - - def _block_definition(base, opts, &block) - Dry::Validation.Params(_form_class(base), _form_opts(opts), &block) - end - end - - module InstanceMethods - extend Forwardable - - delegate %i[build_form form_options auto_wire_options auto_wire] => 'self.class' - alias_method :form, :build_form - - def validate(state, with: nil) - if auto_wire && form_options.any? - with ||= form_options.zip(form_options).to_h - end - opts = Hash(with).map { |opt, key| [opt, state[key]] }.to_h - validate_with(state[:input], opts) - .then { |params| state.update(params: params) } - end - - def validate_with(params, opts = {}) - val = form(opts).call(params) - - val.success? ? wrap(val.output) : error(:validation, details: val.messages) - end - end - - def self.apply(operation, auto_wire_options: (auto_wire_options_was_not_used=true; false), auto_wire: auto_wire_options) - #:nocov: - unless auto_wire_options_was_not_used - warn "[DEPRECATION] `auto_wire_options` is deprecated. Please use `auto_wire` instead" - end - #:nocov: - - operation.auto_wire = auto_wire - operation.form_class = Dry::Validation::Schema::Params - end - end - end - end -end diff --git a/lib/pathway/plugins/dry_validation/v1_0.rb b/lib/pathway/plugins/dry_validation/v1_0.rb deleted file mode 100644 index c60c3db..0000000 --- a/lib/pathway/plugins/dry_validation/v1_0.rb +++ /dev/null @@ -1,87 +0,0 @@ -# frozen_string_literal: true - -module Pathway - module Plugins - module DryValidation - module V1_0 - module ClassMethods - attr_reader :contract_class, :contract_options - attr_accessor :auto_wire - - alias_method :auto_wire_options, :auto_wire - alias_method :auto_wire_options=, :auto_wire= - - def contract(base = nil, &block) - if block_given? - base ||= _base_contract - self.contract_class = Class.new(base, &block) - elsif base - self.contract_class = base - else - raise ArgumentError, 'Either a contract class or a block must be provided' - end - end - - ruby2_keywords def params(*args, &block) - contract { params(*args, &block) } - end - - def contract_class= klass - @contract_class = klass - @contract_options = (klass.dry_initializer.options - Dry::Validation::Contract.dry_initializer.options).map(&:target) - @builded_contract = @contract_options.empty? && klass.schema ? klass.new : nil - end - - def build_contract(**opts) - @builded_contract || contract_class.new(**opts) - end - - def inherited(subclass) - super - subclass.auto_wire = auto_wire - subclass.contract_class = contract_class - end - - private - - def _base_contract - superclass.respond_to?(:contract_class) ? superclass.contract_class : Dry::Validation::Contract - end - end - - module InstanceMethods - extend Forwardable - - delegate %i[build_contract contract_options auto_wire_options auto_wire] => 'self.class' - alias_method :contract, :build_contract - - def validate(state, with: nil) - if auto_wire && contract_options.any? - with ||= contract_options.zip(contract_options).to_h - end - opts = Hash(with).map { |to, from| [to, state[from]] }.to_h - validate_with(state[:input], **opts) - .then { |params| state.update(params: params) } - end - - def validate_with(input, **opts) - result = contract(**opts).call(input) - - result.success? ? wrap(result.values.to_h) : error(:validation, details: result.errors.to_h) - end - end - - def self.apply(operation, auto_wire_options: (auto_wire_options_was_not_used=true; false), auto_wire: auto_wire_options) - #:nocov: - unless auto_wire_options_was_not_used - warn "[DEPRECATION] `auto_wire_options` is deprecated. Please use `auto_wire` instead" - end - #:nocov: - - operation.auto_wire = auto_wire - operation.contract_class = Dry::Validation::Contract - end - end - end - end -end diff --git a/lib/pathway/plugins/responder.rb b/lib/pathway/plugins/responder.rb index c6c8ee2..2b20e1f 100644 --- a/lib/pathway/plugins/responder.rb +++ b/lib/pathway/plugins/responder.rb @@ -4,8 +4,8 @@ module Pathway module Plugins module Responder module ClassMethods - ruby2_keywords def call(*args, &bl) - result = super(*args) + def call(*args, **kwargs, &bl) + result = super(*args, **kwargs) block_given? ? Responder.respond(result, &bl) : result end end @@ -21,9 +21,7 @@ def initialize(result, &bl) instance_eval(&bl) end - def success(&bl) - @ok = bl - end + def success(&bl)= @ok = bl def failure(type = nil, &bl) if type.nil? diff --git a/lib/pathway/plugins/sequel_models.rb b/lib/pathway/plugins/sequel_models.rb index 4128179..cbf31db 100644 --- a/lib/pathway/plugins/sequel_models.rb +++ b/lib/pathway/plugins/sequel_models.rb @@ -6,33 +6,33 @@ module Pathway module Plugins module SequelModels module DSLMethods - def transaction(step_name = nil, &bl) - fail 'must provide a step or a block but not both' if !step_name.nil? == block_given? + def transaction(step_name = nil, &dsl_bl) + raise 'must provide a step or a block but not both' if !step_name.nil? == block_given? if step_name transaction { step step_name } else - around(-> steps, _ { + around(->(runner, _) { db.transaction(savepoint: true) do - raise Sequel::Rollback if steps.call.failure? + raise Sequel::Rollback if runner.call.failure? end - }, &bl) + }, &dsl_bl) end end - def after_commit(step_name = nil, &bl) - fail 'must provide a step or a block but not both' if !step_name.nil? == block_given? + def after_commit(step_name = nil, &dsl_bl) + raise 'must provide a step or a block but not both' if !step_name.nil? == block_given? if step_name after_commit { step step_name } else - around(-> steps, state { - dsl = self.class::DSL.new(State.new(self, state.to_h.dup), self) + around(->(runner, state) { + dsl_copy = self.class::DSL.new(State.new(self, state.to_h.dup), self) db.after_commit do - steps.call(dsl) + runner.call(dsl_copy) end - }, &bl) + }, &dsl_bl) end end end @@ -41,10 +41,10 @@ module ClassMethods attr_accessor :model_class, :search_field, :model_not_found def model(model_class, search_by: model_class.primary_key, set_result_key: true, set_context_param: true, error_message: nil) - self.model_class = model_class - self.search_field = search_by - self.result_key = Inflector.underscore(Inflector.demodulize(model_class.name)).to_sym if set_result_key - self.model_not_found = error_message || "#{Inflector.humanize(Inflector.underscore(Inflector.demodulize(model_class.name)))} not found".freeze + self.model_class = model_class + self.search_field = search_by + self.result_key = Inflector.underscore(Inflector.demodulize(model_class.name)).to_sym if set_result_key + self.model_not_found = error_message || "#{Inflector.humanize(Inflector.underscore(Inflector.demodulize(model_class.name)))} not found".freeze self.context(result_key => Contextualizer::OPTIONAL) if set_result_key && set_context_param end diff --git a/lib/pathway/plugins/simple_auth.rb b/lib/pathway/plugins/simple_auth.rb index a69e55f..6d6402f 100644 --- a/lib/pathway/plugins/simple_auth.rb +++ b/lib/pathway/plugins/simple_auth.rb @@ -26,9 +26,7 @@ def authorize_with(*objs) authorized?(*objs) ? wrap(objs) : error(:forbidden) end - def authorized?(*) - true - end + def authorized?(*) = true end end end diff --git a/lib/pathway/result.rb b/lib/pathway/result.rb index b32f0d9..b1ac967 100644 --- a/lib/pathway/result.rb +++ b/lib/pathway/result.rb @@ -6,13 +6,8 @@ class Result attr_reader :value, :error class Success < Result - def initialize(value) - @value = value - end - - def success? - true - end + def initialize(value) = @value = value + def success? = true def then(bl=nil) result(block_given? ? yield(value): bl.call(value)) @@ -25,29 +20,18 @@ def tee(bl=nil, &block) private - alias :value_for_deconstruct :value + alias_method :value_for_deconstruct, :value end class Failure < Result - def initialize(error) - @error = error - end - - def success? - false - end - - def then(_=nil) - self - end - - def tee(_=nil) - self - end + def initialize(error) = @error = error + def success? = false + def then(_=nil) = self + def tee(_=nil) = self private - alias :value_for_deconstruct :error + alias_method :value_for_deconstruct, :error end module Mixin @@ -55,10 +39,16 @@ module Mixin Failure = Result::Failure end - def deconstruct - [value_for_deconstruct] + def self.success(value) = Success.new(value) + def self.failure(error) = Failure.new(error) + + def self.result(object) + object.is_a?(Result) ? object : success(object) end + def failure? = !success? + def deconstruct = [value_for_deconstruct] + def deconstruct_keys(keys) if value_for_deconstruct.respond_to?(:deconstruct_keys) value_for_deconstruct.deconstruct_keys(keys) @@ -67,22 +57,6 @@ def deconstruct_keys(keys) end end - def failure? - !success? - end - - def self.success(value) - Success.new(value) - end - - def self.failure(error) - Failure.new(error) - end - - def self.result(object) - object.is_a?(Result) ? object : success(object) - end - delegate :result => 'self.class' end end diff --git a/lib/pathway/rspec/matchers/fail_on.rb b/lib/pathway/rspec/matchers/fail_on.rb index a634f27..c36344a 100644 --- a/lib/pathway/rspec/matchers/fail_on.rb +++ b/lib/pathway/rspec/matchers/fail_on.rb @@ -22,22 +22,22 @@ @type = type end - alias :with_type :type - alias :and_type :type + alias_method :with_type, :type + alias_method :and_type, :type chain :message do |message| @message = message end - alias :with_message :message - alias :and_message :message + alias_method :with_message, :message + alias_method :and_message, :message chain :details do |details| @details = details end - alias :with_details :details - alias :and_details :details + alias_method :with_details, :details + alias_method :and_details, :details description do 'fail' + (@type ? " with :#@type error" : '') diff --git a/lib/pathway/version.rb b/lib/pathway/version.rb index e557127..59b9f10 100644 --- a/lib/pathway/version.rb +++ b/lib/pathway/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Pathway - VERSION = '0.12.3' + VERSION = '1.0.0b1' end diff --git a/pathway.gemspec b/pathway.gemspec index bf6fe98..78e2f13 100644 --- a/pathway.gemspec +++ b/pathway.gemspec @@ -27,13 +27,12 @@ Gem::Specification.new do |spec| spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ["lib"] - spec.required_ruby_version = ">= 2.4.0" + spec.required_ruby_version = ">= 3.1.0" spec.add_dependency "dry-inflector", ">= 0.1.0" - spec.add_dependency "contextualizer", "~> 0.0.4" - spec.add_dependency "ruby2_keywords" + spec.add_dependency "contextualizer", "~> 0.1.0" - spec.add_development_dependency "dry-validation", ">= 0.11" + spec.add_development_dependency "dry-validation", ">= 1.0" spec.add_development_dependency "bundler", ">= 2.4.10" spec.add_development_dependency "sequel", "~> 5.0" spec.add_development_dependency "rake", "~> 13.0" @@ -41,6 +40,7 @@ Gem::Specification.new do |spec| spec.add_development_dependency "simplecov-lcov", '~> 0.8.0' spec.add_development_dependency "simplecov" spec.add_development_dependency "pry" + spec.add_development_dependency "byebug" spec.add_development_dependency "pry-byebug" spec.add_development_dependency "pry-doc" spec.add_development_dependency "pry-stack" diff --git a/spec/plugins/base_spec.rb b/spec/plugins/base_spec.rb index ee54ed2..ca08feb 100644 --- a/spec/plugins/base_spec.rb +++ b/spec/plugins/base_spec.rb @@ -12,9 +12,10 @@ class OperationWithSteps < Operation process do step :custom_validate + map :add_misc set :get_value set :get_aux_value, to: :aux_value - around(-> seq, st { seq.call if cond.call(st) }) do + around(-> run, st { run.call if cond.call(st) }) do set ->_ { 99 }, to: :aux_value set ->_ { :UPDATED } end @@ -34,6 +35,10 @@ def custom_validate(state) state[:params] = @validator.call(state) end + def add_misc(state) + State.new(self, state.to_h.merge(misc: -1)) + end + def get_value(state) @back_end.call(state[:params]) end @@ -42,8 +47,8 @@ def get_aux_value(state) state[result_key] end - def if_zero(seq, state) - seq.call if state[:result_value] == 0 + def if_zero(run, state) + run.call if state[:result_value] == 0 end def negative?(state) @@ -112,6 +117,26 @@ def notify(state) end end + describe "#map" do + it "defines a step that replaces the current state" do + old_state = nil + + expect(validator).to receive(:call) do |state| + expect(state.to_h).to_not include(:misc) + + old_state = state + state[:input] + end + + expect(notifier).to receive(:call) do |state| + expect(state.to_h).to include(misc: -1) + expect(state).to_not be_equal(old_state) + end + + operation.call(input) + end + end + describe "#set" do it "defines an updating step which sets the result key if no key is specified" do expect(back_end).to receive(:call).and_return(:SOME_VALUE) diff --git a/spec/plugins/dry_validation/0_11_spec.rb b/spec/plugins/dry_validation/0_11_spec.rb deleted file mode 100644 index 9300ada..0000000 --- a/spec/plugins/dry_validation/0_11_spec.rb +++ /dev/null @@ -1,225 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' -require 'dry/validation/version' - -return unless Dry::Validation::VERSION =~ /^0\.11/ - -module Pathway - module Plugins - describe 'DryValidation::V0_11' do - class SimpleOperation < Operation - plugin :dry_validation - - context :user, :repository - - form do - required(:name).filled(:str?) - optional(:email).maybe(:str?) - end - - process do - step :validate - set :fetch_profile, to: :profile - set :create_model - end - - private - - def fetch_profile(params:,**) - wrap_if_present(repository.fetch(params)) - end - - def create_model(params:, profile:,**) - SimpleModel.new(*params.values, user.role, profile) - end - end - - SimpleModel = Struct.new(:name, :email, :role, :profile) - - SimpleForm = Dry::Validation.Form do - required(:age).filled(:int?) - end - - class OperationWithOpt < Operation - plugin :dry_validation - - context :quz - - form do - configure { option :foo } - - required(:qux).filled(eql?: foo) - end - - process do - step :validate, with: { foo: :quz } - end - end - - class OperationWithAutoWire < Operation - plugin :dry_validation, auto_wire_options: true - - context :baz - - form do - configure { option :baz } - - required(:qux).filled(eql?: baz) - end - - process do - step :validate - end - end - - describe ".form_class" do - subject(:operation_class) { Class.new(Operation) { plugin :dry_validation } } - - context "when no form's been setup" do - it "returns a default empty form" do - expect(operation_class.form_class).to eq(Dry::Validation::Schema::Form) - end - end - - context "when a form's been set" do - it "returns the form" do - operation_class.form_class = SimpleForm - expect(operation_class.form_class).to eq(SimpleForm) - end - end - end - - describe ".build_form" do - let(:form) { OperationWithOpt.build_form(foo: "XXXXX") } - - it "uses passed the option from the context to the form" do - expect(form.call(qux: "XXXXX")).to be_a_success - end - end - - describe ".form_options" do - it "returns the option names defined for the form" do - expect(SimpleOperation.form_options).to eq([]) - expect(OperationWithOpt.form_options).to eq([:foo]) - end - end - - describe ".form" do - context "when called with a form" do - subject(:operation_class) do - Class.new(Operation) do - plugin :dry_validation - form SimpleForm - end - end - - it "uses the passed form's class" do - expect(operation_class.form_class).to eq(SimpleForm.class) - end - - context "and a block" do - subject(:operation_class) do - Class.new(Operation) do - plugin :dry_validation - form(SimpleForm) { required(:gender).filled } - end - end - - it "extend from the form's class" do - expect(operation_class.form_class).to be < SimpleForm.class - end - - it "extends the form rules with the block's rules" do - expect(operation_class.form_class.rules.map(&:name)) - .to include(:age, :gender) - end - end - end - - context "when called with a form class" do - subject(:operation_class) do - Class.new(Operation) do - plugin :dry_validation - form SimpleForm.class - end - end - - it "uses the passed class as is" do - expect(operation_class.form_class).to eq(SimpleForm.class) - end - end - - context "when called with a block" do - subject(:operation_class) do - Class.new(Operation) do - plugin :dry_validation - form { required(:gender).filled } - end - end - - it "extends from the default form class" do - expect(operation_class.form_class).to be < Dry::Validation::Schema::Form - end - - it "uses the rules defined at the passed block" do - expect(operation_class.form_class.rules.map(&:name)) - .to include(:gender) - end - end - end - - describe "#call" do - subject(:operation) { SimpleOperation.new(ctx) } - - let(:ctx) { { user: double("User", role: role), repository: repository } } - let(:role) { :root } - let(:params) { { name: "Paul Smith", email: "psmith@email.com" } } - let(:result) { operation.call(params) } - let(:repository) { double.tap { |repo| allow(repo).to receive(:fetch).and_return(double) } } - - context "when calling with valid params" do - it "returns a successful result", :aggregate_failures do - expect(result).to be_a_success - expect(result.value).to_not be_nil - end - end - - context "when finding model fails" do - let(:repository) { double.tap { |repo| allow(repo).to receive(:fetch).and_return(nil) } } - it "returns a a failed result", :aggregate_failures do - expect(result).to be_a_failure - expect(result.error.type).to eq(:not_found) - end - end - - context "when calling with invalid params" do - let(:params) { { email: "psmith@email.com" } } - it "returns a failed result", :aggregate_failures do - expect(result).to be_a_failure - expect(result.error.type).to eq(:validation) - expect(result.error.details).to eq(name: ['is missing']) - end - end - - context "when form requires options for validation" do - subject(:operation) { OperationWithOpt.new(quz: 'XXXXX') } - - it "sets then passing a hash through the :with argument" do - expect(operation.call(qux: 'XXXXX')).to be_a_success - expect(operation.call(qux: 'OTHER')).to be_a_failure - end - - context "and is using auto_wire_options" do - subject(:operation) { OperationWithAutoWire.new(baz: 'XXXXX') } - - it "sets the options directly from the context using the keys with the same name" do - expect(operation.call(qux: 'XXXXX')).to be_a_success - expect(operation.call(qux: 'OTHER')).to be_a_failure - end - end - end - end - end - end -end diff --git a/spec/plugins/dry_validation/0_12_spec.rb b/spec/plugins/dry_validation/0_12_spec.rb deleted file mode 100644 index 37a1e7e..0000000 --- a/spec/plugins/dry_validation/0_12_spec.rb +++ /dev/null @@ -1,225 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' -require 'dry/validation/version' - -return unless Dry::Validation::VERSION =~ /^0\.1[23]/ - -module Pathway - module Plugins - describe 'DryValidation::V0_12' do - class SimpleOperation < Operation - plugin :dry_validation - - context :user, :repository - - form do - required(:name).filled(:str?) - optional(:email).maybe(:str?) - end - - process do - step :validate - set :fetch_profile, to: :profile - set :create_model - end - - private - - def fetch_profile(params:,**) - wrap_if_present(repository.fetch(params)) - end - - def create_model(params:, profile:,**) - SimpleModel.new(*params.values, user.role, profile) - end - end - - SimpleModel = Struct.new(:name, :email, :role, :profile) - - SimpleForm = Dry::Validation.Params do - required(:age).filled(:int?) - end - - class OperationWithOpt < Operation - plugin :dry_validation - - context :quz - - form do - configure { option :foo } - - required(:qux).filled(eql?: foo) - end - - process do - step :validate, with: { foo: :quz } - end - end - - class OperationWithAutoWire < Operation - plugin :dry_validation, auto_wire_options: true - - context :baz - - form do - configure { option :baz } - - required(:qux).filled(eql?: baz) - end - - process do - step :validate - end - end - - describe ".form_class" do - subject(:operation_class) { Class.new(Operation) { plugin :dry_validation } } - - context "when no form's been setup" do - it "returns a default empty form" do - expect(operation_class.form_class).to eq(Dry::Validation::Params) - end - end - - context "when a form's been set" do - it "returns the form" do - operation_class.form_class = SimpleForm - expect(operation_class.form_class).to eq(SimpleForm) - end - end - end - - describe ".build_form" do - let(:form) { OperationWithOpt.build_form(foo: "XXXXX") } - - it "uses passed the option from the context to the form" do - expect(form.call(qux: "XXXXX")).to be_a_success - end - end - - describe ".form_options" do - it "returns the option names defined for the form" do - expect(SimpleOperation.form_options).to eq([]) - expect(OperationWithOpt.form_options).to eq([:foo]) - end - end - - describe ".form" do - context "when called with a form" do - subject(:operation_class) do - Class.new(Operation) do - plugin :dry_validation - form SimpleForm - end - end - - it "uses the passed form's class" do - expect(operation_class.form_class).to eq(SimpleForm.class) - end - - context "and a block" do - subject(:operation_class) do - Class.new(Operation) do - plugin :dry_validation - form(SimpleForm) { required(:gender).filled } - end - end - - it "extend from the form's class" do - expect(operation_class.form_class).to be < SimpleForm.class - end - - it "extends the form rules with the block's rules" do - expect(operation_class.form_class.rules.map(&:name)) - .to include(:age, :gender) - end - end - end - - context "when called with a form class" do - subject(:operation_class) do - Class.new(Operation) do - plugin :dry_validation - form SimpleForm.class - end - end - - it "uses the passed class as is" do - expect(operation_class.form_class).to eq(SimpleForm.class) - end - end - - context "when called with a block" do - subject(:operation_class) do - Class.new(Operation) do - plugin :dry_validation - form { required(:gender).filled } - end - end - - it "extends from the default form class" do - expect(operation_class.form_class).to be < Dry::Validations::Params - end - - it "uses the rules defined at the passed block" do - expect(operation_class.form_class.rules.map(&:name)) - .to include(:gender) - end - end - end - - describe "#call" do - subject(:operation) { SimpleOperation.new(ctx) } - - let(:ctx) { { user: double("User", role: role), repository: repository } } - let(:role) { :root } - let(:params) { { name: "Paul Smith", email: "psmith@email.com" } } - let(:result) { operation.call(params) } - let(:repository) { double.tap { |repo| allow(repo).to receive(:fetch).and_return(double) } } - - context "when calling with valid params" do - it "returns a successful result", :aggregate_failures do - expect(result).to be_a_success - expect(result.value).to_not be_nil - end - end - - context "when finding model fails" do - let(:repository) { double.tap { |repo| allow(repo).to receive(:fetch).and_return(nil) } } - it "returns a a failed result", :aggregate_failures do - expect(result).to be_a_failure - expect(result.error.type).to eq(:not_found) - end - end - - context "when calling with invalid params" do - let(:params) { { email: "psmith@email.com" } } - it "returns a failed result", :aggregate_failures do - expect(result).to be_a_failure - expect(result.error.type).to eq(:validation) - expect(result.error.details).to eq(name: ['is missing']) - end - end - - context "when form requires options for validation" do - subject(:operation) { OperationWithOpt.new(quz: 'XXXXX') } - - it "sets then passing a hash through the :with argument" do - expect(operation.call(qux: 'XXXXX')).to be_a_success - expect(operation.call(qux: 'OTHER')).to be_a_failure - end - - context "and is using auto_wire_options" do - subject(:operation) { OperationWithAutoWire.new(baz: 'XXXXX') } - - it "sets the options directly from the context using the keys with the same name" do - expect(operation.call(qux: 'XXXXX')).to be_a_success - expect(operation.call(qux: 'OTHER')).to be_a_failure - end - end - end - end - end - end -end diff --git a/spec/plugins/dry_validation/1_0_spec.rb b/spec/plugins/dry_validation_spec.rb similarity index 89% rename from spec/plugins/dry_validation/1_0_spec.rb rename to spec/plugins/dry_validation_spec.rb index 73e22f5..c30e99c 100644 --- a/spec/plugins/dry_validation/1_0_spec.rb +++ b/spec/plugins/dry_validation_spec.rb @@ -1,13 +1,10 @@ # frozen_string_literal: true require 'spec_helper' -require 'dry/validation/version' - -return unless Dry::Validation::VERSION =~ /^1\./ module Pathway module Plugins - describe 'DryValidation::V1_0' do + describe 'DryValidation' do class SimpleOperation < Operation plugin :dry_validation @@ -177,6 +174,25 @@ class OperationWithAutoWire < Operation .to include(:gender) end end + + context "when called with no block nor contract" do + subject(:opr_class) { Class.new(Operation) { plugin :dry_validation } } + + it 'raises an error' do + expect { opr_class.contract } + .to raise_error(ArgumentError, 'Either a contract class or a block must be provided') + end + end + + context 'when the operation is inherited' do + let(:opr_class) { OperationWithAutoWire } + subject(:opr_subclass) { Class.new(OperationWithAutoWire) } + + it "sets 'contract_class' and 'auto_wire' from the superclass", :aggregate_failures do + expect(opr_subclass.auto_wire).to eq(opr_class.auto_wire) + expect(opr_subclass.contract_class).to eq(opr_class.contract_class) + end + end end describe "#call" do diff --git a/spec/plugins/sequel_models_spec.rb b/spec/plugins/sequel_models_spec.rb index 1be2a36..29d4e25 100644 --- a/spec/plugins/sequel_models_spec.rb +++ b/spec/plugins/sequel_models_spec.rb @@ -58,8 +58,6 @@ def chain_operation(state) end end - class SubOperation < MyOperation; end - describe 'DSL' do let(:result) { operation.call(params) } let(:params) { { email: 'asd@fgh.net' } } @@ -274,13 +272,14 @@ class InvalidOperation < MyOperation end context 'when the operation is inherited' do - it "sets 'result_key', 'search_field', 'model_class' and 'model_not_found' from the superclass" do - aggregate_failures do - expect(SubOperation.result_key).to eq(MyOperation.result_key) - expect(SubOperation.search_field).to eq(MyOperation.search_field) - expect(SubOperation.model_class).to eq(MyOperation.model_class) - expect(SubOperation.model_not_found).to eq(MyOperation.model_not_found) - end + let(:opr_class) { MyOperation } + subject(:opr_subclass) { Class.new(opr_class) } + + it "sets 'result_key', 'search_field', 'model_class' and 'model_not_found' from the superclass", :aggregate_failures do + expect(opr_subclass.result_key).to eq(opr_class.result_key) + expect(opr_subclass.search_field).to eq(opr_class.search_field) + expect(opr_subclass.model_class).to eq(opr_class.model_class) + expect(opr_subclass.model_not_found).to eq(opr_class.model_not_found) end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 5f0d4be..3c569d8 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -20,6 +20,7 @@ require 'pathway' require 'sequel' require 'pry' +require 'pry-byebug' # Load testing support files Dir[__dir__ + '/support/**/*.rb'].each { |support| require support }