# frozen_string_literal: true require 'concurrent/map' require 'dry/core/constants' require 'dry/equalizer' require 'dry/logic/operations' require 'dry/logic/result' require 'dry/logic/rule/interface' module Dry module Logic def self.Rule(*args, **options, &block) if args.any? Rule.build(*args, **options) elsif block Rule.build(block, **options) end end class Rule include Core::Constants include Dry::Equalizer(:predicate, :options) include Operators attr_reader :predicate attr_reader :options attr_reader :args attr_reader :arity def self.interfaces @interfaces ||= ::Concurrent::Map.new end def self.specialize(arity, curried, base = Rule) base.interfaces.fetch_or_store([arity, curried]) do interface = Interface.new(arity, curried) klass = Class.new(base) { include interface } base.const_set("#{base.name.split('::').last}#{interface.name}", klass) klass end end def self.build(predicate, args: EMPTY_ARRAY, arity: predicate.arity, **options) specialize(arity, args.size).new(predicate, { args: args, arity: arity, **options }) end def initialize(predicate, options = EMPTY_HASH) @predicate = predicate @options = options @args = options[:args] || EMPTY_ARRAY @arity = options[:arity] || predicate.arity end def type :rule end def id options[:id] end def curry(*new_args) with(args: args + new_args) end def bind(object) if predicate.respond_to?(:bind) self.class.build(predicate.bind(object), options) else self.class.build( -> *args { object.instance_exec(*args, &predicate) }, options.merge(arity: arity, parameters: parameters) ) end end def eval_args(object) with(args: args.map { |arg| UnboundMethod === arg ? arg.bind(object).() : arg }) end def with(new_opts) self.class.build(predicate, options.merge(new_opts)) end def parameters options[:parameters] || predicate.parameters end def ast(input = Undefined) [:predicate, [id, args_with_names(input)]] end private def args_with_names(*input) parameters.map(&:last).zip(args + input) end end end end