Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove support for Ruby 2.x and 3.0 #48

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 }}
4 changes: 0 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
89 changes: 35 additions & 54 deletions lib/pathway.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# frozen_string_literal: true

require 'ruby2_keywords'
require 'forwardable'
require 'dry/inflector'
require 'contextualizer'
Expand All @@ -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))
Expand All @@ -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)
Expand All @@ -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

Expand All @@ -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'
Expand All @@ -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
Expand All @@ -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

Expand All @@ -168,62 +153,58 @@ 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
alias_method :guard, :if_true

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
Expand Down
18 changes: 14 additions & 4 deletions lib/pathway/plugins/auto_deconstruct_state.rb
Original file line number Diff line number Diff line change
@@ -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
22 changes: 0 additions & 22 deletions lib/pathway/plugins/auto_deconstruct_state/ruby3.rb

This file was deleted.

85 changes: 73 additions & 12 deletions lib/pathway/plugins/dry_validation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading