A way to enforce contracts in Portrayal rubygem

portrayal code, portrayal gem

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.


Tags
ruby snippet

Date
October 31, 2022