Skip to content

Instantly share code, notes, and snippets.

@maxim
Created May 13, 2023 22:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save maxim/12e086f23f7ae9d230a2895bbb519483 to your computer and use it in GitHub Desktop.
Save maxim/12e086f23f7ae9d230a2895bbb519483 to your computer and use it in GitHub Desktop.
Portrayal::Guards Proof of Concept
#!/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