Skip to content

Commit

Permalink
Merge pull request #48 from pabloh/remove_support_for_ruby_2
Browse files Browse the repository at this point in the history
Remove support for Ruby 2.x and 3.0
  • Loading branch information
pabloh authored Dec 17, 2024
2 parents 5740c12 + e958290 commit 58bc78f
Show file tree
Hide file tree
Showing 22 changed files with 227 additions and 918 deletions.
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

0 comments on commit 58bc78f

Please sign in to comment.