-
-
Save maxim/12e086f23f7ae9d230a2895bbb519483 to your computer and use it in GitHub Desktop.
Portrayal::Guards Proof of Concept
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
#!/usr/bin/env ruby | |
require 'bundler/setup' | |
require 'portrayal' | |
require 'pry' | |
module Portrayal::Guards | |
class Guard | |
attr_reader :name | |
def initialize(type, name, blk); @type, @name, @block = type, name, blk end | |
def failure(obj); @name unless obj.instance_exec(&@block) end | |
def pass?; @type == :pass! end | |
def initialize_dup(src) | |
@type = src.instance_variable_get(:@type).dup | |
@name = src.instance_variable_get(:@name).dup | |
@block = src.instance_variable_get(:@block).dup | |
end | |
end | |
class Group | |
include Enumerable | |
def initialize; @passes = []; @guards = [] end | |
def failure(obj); each_failure(obj).first end | |
def <<(guard); (guard.pass? ? @passes : @guards) << guard end | |
def initialize_dup(src) | |
@passes = src.instance_variable_get(:@passes).map(&:dup) | |
@guards = src.instance_variable_get(:@guards).map(&:dup) | |
end | |
def each | |
return enum_for(__method__) unless block_given? | |
@passes.each { |p| yield(p) } | |
@guards.each { |g| yield(g) } | |
end | |
def each_failure(obj) | |
return enum_for(__method__, obj) unless block_given? | |
return if @passes.any? { |p| !p.failure(obj) } | |
@guards.each { |g| failure = g.failure(obj); yield(failure) if failure } | |
end | |
end | |
class Failure | |
attr_reader :topic, :message | |
def initialize(topic, message); @topic, @message = topic, message end | |
def explode!; raise ArgumentError, @message end | |
end | |
module SchemaExt | |
attr_reader :guards, :guard_module | |
def initialize(*); @guards = {}; @guard_module = Module.new; super end | |
def list_guards; guards.transform_values { |group| group.map(&:name) } end | |
def each_failure(obj) | |
return enum_for(__method__, obj) unless block_given? | |
guards.each do |topic, group| | |
group.each_failure(obj) do |failure| | |
yield Failure.new(topic, failure) | |
end | |
end | |
end | |
def add_guard(topic, type, name, blk) | |
guard = Guard.new(type, name, blk) | |
(@guards[topic] ||= Group.new) << guard | |
topic | |
end | |
def set_changes(obj, hash) | |
obj.instance_variable_set(:@__portrayal_cskip, true) | |
hash.each { |k, v| obj.send("#{k}=", v) } | |
ensure | |
obj.remove_instance_variable(:@__portrayal_cskip) | |
end | |
def initialize_dup(other) | |
@guards = other.guards.transform_values(&:dup) | |
@guard_module = other.guard_module.dup | |
super | |
end | |
def update_guard_module! | |
@guard_module.module_eval(render_guard_module_code) | |
end | |
def try_changes(obj, changes) | |
sandbox = obj.dup | |
set_changes(sandbox, changes) | |
each_failure(sandbox) | |
end | |
def render_guard_module_code | |
writers = keywords.map { |k| | |
<<-RUBY | |
protected def #{k}=(v) | |
return super if @__portrayal_cskip | |
failure = self.class.portrayal.try_changes(self, #{k}: v).first | |
failure ? failure.explode! : super | |
end | |
RUBY | |
}.join("\n") | |
<<-RUBY | |
def initialize(*, **) | |
super | |
return if @__portrayal_cskip | |
self.class.portrayal.each_failure(self) { |f| f.explode! } | |
end | |
def update(changes) | |
if (failures = self.class.portrayal.try_changes(self, changes).to_a).any? | |
return failures.group_by(&:topic).transform_values { |a| a.map(&:message) } | |
end | |
self.class.portrayal.set_changes(self, changes) | |
nil | |
end | |
#{writers} | |
RUBY | |
end | |
end | |
module ClassExt | |
def inherited(c); super(c); c.include(c.portrayal.guard_module) end | |
def guard(topic = :base, name, &block) | |
portrayal.add_guard(topic, __callee__, name, block) | |
end | |
alias pass! guard | |
def keyword(*, **) | |
name = super | |
include portrayal.guard_module | |
portrayal.update_guard_module! | |
name | |
end | |
end | |
::Portrayal::Schema.prepend(SchemaExt) | |
::Portrayal.prepend(ClassExt) | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment