A way to enforce contracts in Portrayal rubygem
New way (based on version 0.9.0): https://gist.github.com/maxim/4dc1765d9f81ac849325d166d14d2dfa Blog post: https://max.engineer/portrayal-guards-poc
Old way:
module Portrayal
class Contract
attr_reader :clauses
def initialize(block, mod = nil)
@block = block
@clauses =
Describe.new(mod).tap { |c| c.instance_exec({}, &block) }.clauses.freeze
@enforce = Enforce.new(mod)
end
def inspect
"<#{self.class}:#{object_id} @clauses=#{@clauses}>"
end
def find_violation(attrs)
catch(:portrayal_violation) { @enforce.instance_exec(attrs, &@block) }
end
class Describe
attr_reader :clauses
def initialize(mod = nil)
extend(mod) if mod
@clauses = []
end
def clause(message); @clauses << message end
end
class Enforce
def initialize(mod = nil)
extend(mod) if mod
end
def clause(message)
throw :portrayal_violation, message unless yield
end
end
end
class Contracts
include Enumerable
def initialize; @contracts = [] end
def add(block, mod); @contracts << Contract.new(block, mod) end
def violations(attrs); each_violation(attrs).to_a end
def enforce(attrs)
violation = each_violation(attrs).first
return unless violation
raise ArgumentError, violation
end
def each_violation(attrs)
return enum_for(__method__, attrs) unless block_given?
each do |contract|
violation = contract.find_violation(attrs)
yield(violation) if violation
end
end
def human
map { |contract|
head, *tail = contract.clauses
"* #{head}\n" + tail.map { |c| " + #{c}\n" }.join
}.join
end
def each
return @contracts.each unless block_given?
@contracts.each { |contract| yield(contract) }
end
end
class Schema
attr_reader :contracts
def add_contract(block, mod)
@contracts ||= Contracts.new
@contracts.add(block, mod)
end
def enforce_contracts(obj, changes = {})
return unless @contracts
attrs = attributes(obj).merge(changes)
@contracts.enforce(attrs)
end
alias _base_definition_of_initialize definition_of_initialize
def definition_of_initialize
_base_definition_of_initialize
.sub(/ end\z/, '; self.class.portrayal.enforce_contracts(self) end')
end
end
def keyword_contract(name, mod = nil, &block)
return contract(mod, &block) unless portrayal.schema[name][:default]
default = portrayal.get_default(name)
contract(mod) do |kw|
kw.key?(name) ? (next if kw[name] == default) : (next if default.nil?)
instance_exec(kw, &block)
end
name
end
def contract(mod = nil, &block)
unless respond_to?(:portrayal)
class << self
attr_reader :portrayal
def inherited(base)
base.instance_variable_set('@portrayal', portrayal.dup)
end
end
@portrayal = Schema.new
class_eval(portrayal.definition_of_object_enhancements)
end
portrayal.add_contract(block, mod)
end
alias _raw_keyword keyword
def keyword(name, *args, **kwargs, &block)
_raw_keyword(name, *args, **kwargs, &block).tap {
define_method "#{name}=" do |value|
self.class.portrayal.enforce_contracts(self, name => value)
instance_variable_set("@#{name}", value)
end
protected "#{name}="
}
end
end
Code snippets in this post are covered by 0BSD License.