Last active
May 3, 2023 02:15
-
-
Save estum/9561636564f83f5c3f2267fca96e1ffa to your computer and use it in GitHub Desktop.
RW.rb - inverted-control accessor tool
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# frozen_string_literal: true | |
require 'dry/core/cache' | |
require 'dry/core/constants' | |
require 'dry/container' | |
require 'dry/types' | |
require 'dry/types/tuple' # → https://github.com/estum/dry-types-tuple | |
# External instance-level accessor wrapper in inverted control: | |
# once defined with an accessor name it could be used with different objects | |
# to get, set and predicate values. | |
# | |
# @example Instance variable accessor | |
# RW[@ivar_name].get(object) | |
# RW[@ivar_name].set(object, value) | |
# RW[@ivar_name].defined_on?(object) | |
# | |
# @example Instance accessor method | |
# RW[:accessor_method].get(object) | |
# RW[:accessor_method].set(object, value) | |
# RW[:accessor_method].defined_on?(object) | |
# | |
# @see RW.[] | |
# @see RW::Base#get | |
# @see RW::Base#set | |
# @see RW::Base#defined_on? | |
# | |
class RW | |
include Dry::Core::Constants | |
# @api private | |
module Repo | |
extend Dry::Container::Mixin | |
register :any, Dry::Types['any'] | |
register :str, Dry::Types['string'] | |
register :tuple, Dry::Types['tuple'] | |
register :hash, Dry::Types['hash'] | |
register :writer, proc { @r.is_a?(Symbol) ? :"#{@r}=" : Undefined }, call: false | |
register :ivar, resolve(:str).constrained(start_with: ?@).constructor(&:to_s) | |
register :not_ivar, resolve(:str).constrained(format: /^[^@]/).constructor(&:to_s) | |
register :api, proc { |any, name| any.constrained(respond_to: name) }.curry(2).(resolve(:any)) | |
register :fn, proc { |api| api[:call] | api[:to_proc].constructor(&:to_proc) }.(resolve(:api)) | |
register :curry_unbound do |fn, *args| | |
fn = resolve(:fn)[fn] | |
proc { |o, *rest, &blk| fn.(o, *args, *rest, &blk) } | |
end | |
end | |
# Implements typecasting methods and makes an extended class act like Dry::Types::Type. | |
# @api private | |
module ClassInterface | |
include Dry::Types::Type | |
include Dry::Types::Builder | |
include Dry::Types::Decorator | |
attr_accessor :type | |
# @param [Any] input | |
# @return [Object] | |
def call_safe(input, &block) | |
return input if input.is_a?(self) | |
result = @type.try(input) do | |
input = block.call(input) if block_given? | |
return input | |
end | |
new(*coerce_args(result.input)) | |
end | |
# @param [Any] input | |
# @return [Object] | |
def call_unsafe(input, &block) | |
return input if input.is_a?(self) | |
input = @type.call_unsafe(input) | |
new(*coerce_args(input)) | |
end | |
# @abstract | |
# Expected to be defined in an extended class | |
# @param [Any] input | |
# @return [Array(Any)] | |
# @see IVarSetter.coerce_args | |
# @see MethodCall.coerce_args | |
def coerce_args(input) | |
raise NotImplementedError | |
end | |
end | |
# @abstract | |
# When inherited, set a tuple type with <tt>self.type =</tt> and | |
# define <tt>self.coerce_args</tt> as a static class method. | |
class Base | |
send :extend, Dry::Initializer[undefined: false] | |
param :name | |
option :reader, type: Repo[:fn] | |
option :writer, type: Repo[:fn] | |
option :predicate, type: Repo[:fn] | |
# @!method initialize(name, **options) | |
# @abstract | |
# @api private | |
# Initializes an instance of {Base} | |
# @param [String, Symbol] name (required) | |
# @param [Hash] options | |
# @option options [#to_proc, #call] :reader (required) | |
# @option options [#to_proc, #call] :writer (required) | |
# @option options [#to_proc, #call] :predicate (required) | |
# @return [self] | |
send :include, Dry.Equalizer(:name, immutable: true) | |
extend ClassInterface | |
# Get value by calling +@reader+ proc | |
# @param [Any] receiver | |
# @param [Array(Any)] args | |
# passed to #call method of <tt>@reader</tt> | |
def get(receiver, *args, &block) | |
@reader.(receiver, *args, &block) | |
end | |
# Set value by calling +@writer+ proc | |
# @param [Any] receiver | |
# @param [Array(Any)] args | |
# passed to #call method of <tt>@writer</tt> | |
def set(receiver, *args, &block) | |
@writer.(receiver, *args, &block) | |
end | |
# Check if target is accessible on the receiver by calling +@predicate+ proc. | |
# @param [Any] receiver | |
# @return [Boolean] | |
def defined_on?(receiver) | |
@predicate.(receiver) | |
end | |
# Get value of ivar and, if it is nil, set it with the given | |
# @see #set | |
def set_if_absent(receiver, *args, &block) | |
set(receiver, *args, &block) if get(receiver).nil? | |
end | |
end | |
# Handles an instance variable access, when used with <tt>RW[:@variable_name]</tt>. | |
class IVarSetter < Base | |
self.type = Repo[:tuple].of(Repo[:ivar], [Repo[:hash].optional]) | |
# @!method initialize(name, **options) | |
# Initializes an instance of {IVarSetter} | |
# @param [String, Symbol] name (required) | |
# @param [Hash] options | |
# @option options [#to_proc, #call] :reader | |
# (Repo[:curry_unbound][:instance_variable_get, name]) | |
# @option options [#to_proc, #call] :writer | |
# (Repo[:curry_unbound][:instance_variable_set, name]) | |
# @option options [#to_proc, #call] :predicate | |
# (Repo[:curry_unbound][:instance_variable_defined?, name]) | |
# @return [self] | |
# @api private | |
def self.coerce_args((name, *rest)) | |
opts = rest.extract_options! | |
opts[:reader] ||= Repo[:curry_unbound][:instance_variable_get, name] | |
opts[:writer] ||= Repo[:curry_unbound][:instance_variable_set, name] | |
opts[:predicate] ||= Repo[:curry_unbound][:instance_variable_defined?, name] | |
[name, *rest, **opts] | |
end | |
end | |
# Handles an accessor method, when used with <tt>RW[:accessor_name]</tt. | |
class MethodCall < Base | |
self.type = Repo[:tuple].of(Repo[:not_ivar], [Repo[:hash].optional]) | |
# @!method initialize(name, **options) | |
# Initializes an instance of {MethodCall} | |
# @param [String, Symbol] name (required) | |
# @param [Hash] options | |
# @option options [#to_proc, #call] :reader (proc(&name.to_sym)) | |
# @option options [#to_proc, #call] :writer (proc(&"#{name}=")) | |
# @option options [#to_proc, #call] :predicate (Repo[:curry_unbound][:respond_to?, name]) | |
# @return [self] | |
# @api private | |
def self.coerce_args((name, *rest)) | |
opts = rest.extract_options! | |
opts[:reader] ||= proc(&name.to_sym) | |
opts[:writer] ||= proc(&:"#{name}=") | |
opts[:predicate] ||= Repo[:curry_unbound][:respond_to?, name] | |
[name, *rest, **opts] | |
end | |
end | |
extend Dry::Core::Cache | |
class << self | |
# @see ClassInterface#call_safe | |
# | |
# @param [Hash] opts | |
# custom procs to use instead of generated ones | |
# @option opts [#call] reader | |
# optional custom reader proc | |
# @option opts [#call] writer | |
# optional custom writer proc | |
# @option opts [#call] predicate | |
# optional custom predicate proc | |
# | |
# @overload [](ivar_name, **opts) | |
# @see IVarSetter.coerce_args | |
# @see IVarSetter#initialize | |
# @param [String, Symbol] ivar_name | |
# An instance variable name to be accessed, i.e. '@example' | |
# @example | |
# rw = RW[:@example] | |
# rw.get(object) # => object.instance_variable_get(:@example) | |
# rw.set(object, value) # => object.instance_variable_set(:@example, value) | |
# rw.defined_on?(object) # => object.instance_variable_defined?(:@example) | |
# @return [IVarSetter] | |
# | |
# @overload [](accessor_name, **opts) | |
# @see MethodCall.coerce_args | |
# @see MethodCall#initialize | |
# @example | |
# rw = RW[:example] | |
# @param [String, Symbol] accessor_name | |
# An accessor method name, i.e. `example` | |
# @example | |
# rw = RW[:example] | |
# rw.get(object) # => object.example | |
# rw.set(object, value) # => object.example = value | |
# rw.defined_on?(object) # => object.respond_to?(:example) | |
# @return [MethodCall] | |
def [](name, **opts) | |
fetch_or_store(name, opts) do | |
@sum[[name, **opts]] | |
end | |
end | |
attr_accessor :sum, :to_proc | |
end | |
# Composition type to handle both cases. | |
self.sum = IVarSetter | MethodCall | |
self.to_proc = method(:[]).to_proc | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment