Last active
August 29, 2015 13:56
-
-
Save nilium/9152452 to your computer and use it in GitHub Desktop.
Fun little key path thing for Ruby.
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
# | |
# Key path constructor object. Builds up a key path for most instance methods | |
# called against itself. | |
# | |
class KeyPathCtor | |
def self.forwarding_method(name, old_name = nil) | |
old_name ||= name | |
define_method(name, -> (*args) do | |
if args.empty? | |
method_missing(old_name) | |
else | |
method_missing(-> (obj) { obj.__send__(name, *args) }) | |
end | |
end) | |
end | |
def initialize(paths = nil, &block) | |
@paths = paths | |
@defined = !paths.nil? | |
instance_exec(&block) if block | |
end | |
forwarding_method :object_id | |
forwarding_method :type, :class | |
forwarding_method :nil? | |
forwarding_method :frozen? | |
forwarding_method :hash | |
forwarding_method :inspect | |
forwarding_method :to_s | |
forwarding_method :singleton_type, :singleton_class | |
forwarding_method :tainted? | |
forwarding_method :untrusted? | |
def this(&block) | |
kp = method_missing(::KeyPath::SelfKey) | |
kp = kp.method_missing(block) if block | |
kp | |
end | |
def map(&block) | |
raise ArgumentError, "No block given" unless block | |
this( | |
&-> (obj) do | |
if obj.respond_to?(:map) | |
obj.map(&block) | |
else | |
block[obj] | |
end | |
end | |
) | |
end | |
def select(&block) | |
raise ArgumentError, "No block given" unless block | |
this( | |
&-> (obj) do | |
if obj.respond_to?(:select) | |
obj.select(&block) | |
elsif block[obj] | |
obj | |
end | |
end | |
) | |
end | |
alias_method :filter, :select | |
def reject(&block) | |
raise ArgumentError, "No block given" unless block | |
this( | |
&-> (obj) do | |
if obj.respond_to?(:reject) | |
obj.reject(&block) | |
elsif !block[obj] | |
obj | |
end | |
end | |
) | |
end | |
# Operators | |
def +(x) | |
method_missing(-> (obj) { obj + x }) | |
end | |
def *(x) | |
method_missing(-> (obj) { obj * x }) | |
end | |
def **(x) | |
method_missing(-> (obj) { obj * x }) | |
end | |
def /(x) | |
method_missing(-> (obj) { obj / x }) | |
end | |
def -(x) | |
method_missing(-> (obj) { obj - x }) | |
end | |
def %(x) | |
method_missing(-> (obj) { obj % x }) | |
end | |
def <=>(x) | |
method_missing(-> (obj) { obj <=> x }) | |
end | |
def >=(x) | |
method_missing(-> (obj) { obj >= x }) | |
end | |
def <=(x) | |
method_missing(-> (obj) { obj <= x }) | |
end | |
def >(x) | |
method_missing(-> (obj) { obj > x }) | |
end | |
def <(x) | |
method_missing(-> (obj) { obj < x }) | |
end | |
def ===(x) | |
method_missing(-> (obj) { obj === x }) | |
end | |
def ==(x) | |
method_missing(-> (obj) { obj == x }) | |
end | |
def !=(x) | |
method_missing(-> (obj) { obj != x }) | |
end | |
def =~(x) | |
method_missing(-> (obj) { obj =~ x }) | |
end | |
def !~(x) | |
method_missing(-> (obj) { obj !~ x }) | |
end | |
def ! | |
method_missing(-> (obj) { !obj }) | |
end | |
def +@ | |
method_missing(-> (obj) { +obj }) | |
end | |
def -@ | |
method_missing(-> (obj) { -obj }) | |
end | |
def <<(x) | |
method_missing(-> (obj) { obj << x }) | |
end | |
def >>(x) | |
method_missing(-> (obj) { obj >> x }) | |
end | |
def &(x) | |
method_missing(-> (obj) { obj & x }) | |
end | |
def |(x) | |
method_missing(-> (obj) { obj | x }) | |
end | |
def ^(x) | |
method_missing(-> (obj) { obj ^ x }) | |
end | |
def ~(x) | |
method_missing(-> (obj) { obj ~ x }) | |
end | |
def method_missing(msg, *args, &block) | |
if (!args.empty? || block) && msg.kind_of?(Symbol) | |
name = msg | |
if block | |
msg = -> (obj) { obj.__send__(name, *args, &block) } | |
else | |
msg = -> (obj) { obj.__send__(name, *args) } | |
end | |
end | |
if @paths && @defined | |
@paths << msg unless msg == ::KeyPath::SelfKey && @paths.last == ::KeyPath::SelfKey | |
elsif @paths && !@defined | |
raise "Cannot define multiple paths" | |
elsif !@paths | |
@paths = [msg] | |
end | |
self.__send__(:class).new(@paths) | |
end | |
# Subscript operator (subscript-assign not provided) | |
def [](*args) | |
method_missing(::KeyPath::Subscript.new(*args)) | |
end | |
end | |
# | |
# Key path class -- can be initialized with any set of valid keys (see | |
# PATH_TYPES for valid types) and applied to any object. Provides kp[obj] | |
# as a convenience method for kp.apply(obj). | |
# | |
# May be constructed using the global keypath function below. | |
# | |
class KeyPath | |
class Subscript | |
attr_reader :args | |
def initialize(*args) | |
@args = args.freeze | |
end | |
end | |
module SelfKey ; end | |
PATH_TYPES = [ | |
::KeyPath::Subscript, | |
::KeyPath::SelfKey, | |
Symbol, | |
Proc | |
].freeze | |
def initialize(path) | |
unless path.kind_of?(Array) && path.all? do |k| | |
PATH_TYPES.any? { |c| k.kind_of?(c) || k == c } | |
end | |
raise ArgumentError, "Invalid path" | |
end | |
@path = path.freeze | |
end | |
def to_proc | |
kp = self | |
-> (obj) { kp.apply(obj) } | |
end | |
def apply(object) | |
result = object | |
index = 0 | |
keys = @path | |
len = keys.length | |
while index < len | |
key = keys[index] | |
no_enum = (key == SelfKey) | |
if no_enum | |
index += 1 | |
break unless index < len | |
key = keys[index] | |
end | |
result = | |
case key | |
when Subscript then apply_subscript_key(result, key, no_enum) | |
when Proc then apply_proc_key(result, key, no_enum) | |
else apply_key(result, key, no_enum) | |
end | |
index += 1 | |
end | |
result | |
end | |
alias_method :[], :apply | |
private | |
def apply_key(object, key, no_enum) | |
case | |
when no_enum || !object.respond_to?(:map) | |
object.__send__(key) | |
when object.kind_of?(Array) || object.include?(Enumerable) || object.respond_to?(:map) | |
r = object.map(&key) | |
if r.kind_of?(Array) | |
if r.respond_to?(:flatten!) | |
r.flatten!(1) | |
elsif r.respond_to?(:flatten) | |
r = r.flatten(1) | |
end | |
end | |
r | |
end | |
end | |
def apply_subscript_key(object, key, no_enum) | |
args = key.args | |
case | |
when no_enum || !object.respond_to?(:map) | |
object[*args] | |
when object.kind_of?(Array) || object.include?(Enumerable) || object.respond_to?(:map) | |
r = object.map { |item| item[*args] } | |
if r.kind_of?(Array) | |
if r.respond_to?(:flatten!) | |
r.flatten!(1) | |
elsif r.respond_to?(:flatten) | |
r = r.flatten(1) | |
end | |
end | |
r | |
end | |
end | |
def apply_proc_key(object, proc, no_enum) | |
case | |
when no_enum || !object.respond_to?(:map) | |
proc[object] | |
when object.kind_of?(Array) || object.include?(Enumerable) || object.respond_to?(:map) | |
r = object.map(&proc) | |
if r.kind_of?(Array) | |
if r.respond_to?(:flatten!) | |
r.flatten!(1) | |
elsif r.respond_to?(:flatten) | |
r = r.flatten(1) | |
end | |
end | |
r | |
end | |
end | |
end | |
class Object | |
# | |
# Object extension for compiling and running a key path against the receiver. | |
# | |
def value_of(kp = nil, &block) | |
if kp && block_given? | |
raise ArgumentError, "Must provide either a keypath or a block -- not both" | |
elsif kp.nil? && !block | |
raise ArgumentError, "No block given" | |
end | |
if block | |
keypath(&block)[self] | |
else | |
kp[self] | |
end | |
end | |
end | |
# | |
# Creates a key path and returns it. If an object is provided, it will apply | |
# the resulting key path to the object and return the result. | |
# | |
def keypath(obj = nil, &block) | |
kp = KeyPath.new(KeyPathCtor.new(nil, &block).instance_variable_get(:@paths)) | |
if obj | |
kp[obj] | |
else | |
kp | |
end | |
end | |
#============================================================================= | |
# TEST CODE # | |
#============================================================================= | |
# A | |
kp = keypath { (self ** 2).select { |k| k < 5 } } | |
puts kp[[1, 2, 3, 4]].inspect | |
# B | |
puts [1, 2, 3, 4].value_of(kp).inspect | |
# C | |
puts [1, 2, 3, 4].value_of { (self ** 2).select { |k| k < 5 } }.inspect | |
puts [:foo, :bar, :baz].value_of { ((hash ** 2) & 0xFF).this[1] } | |
nesting = { | |
a: { e: { i: 1, m: 2 }, q: { u: 3, y: 4 }}, | |
b: { f: { j: 5, n: 6 }, r: { v: 7, z: 8 }}, | |
c: { g: { k: 9, o: 10 }, s: { w: 11, aa: 12 }}, | |
d: { h: { l: 13, p: 14 }, t: { x: 15, ab: 16 }} | |
} | |
puts nesting.value_of { this.values.values.each_key.to_a.this.sort { |a, b| a <=> b }.to_s.rjust(2, ' ') } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment