# frozen_string_literal: true module Speculation # @private class HashSpec < SpecImpl include NamespacedSymbols S = Speculation attr_reader :id def initialize(req, opt, req_un, opt_un) @id = SecureRandom.uuid @req = req @opt = opt @req_un = req_un @opt_un = opt_un req_keys = req.flat_map(&method(:extract_keys)) req_un_specs = req_un.flat_map(&method(:extract_keys)) all_keys = req_keys + req_un_specs + opt + opt_un unless all_keys.all? { |s| s.is_a?(Symbol) && NamespacedSymbols.namespace(s) } raise "all keys must be namespaced Symbols" end req_specs = req_keys + req_un_specs req_keys += req_un_specs.map(&method(:unqualify_key)) pred_exprs = [Utils.method(:hash?)] pred_exprs.push(->(v) { parse_req(req, v, Utils.method(:itself)).empty? }) if req.any? pred_exprs.push(->(v) { parse_req(req_un, v, method(:unqualify_key)).empty? }) if req_un.any? @req_keys = req_keys @req_specs = req_specs @opt_keys = opt + opt_un.map(&method(:unqualify_key)) @opt_specs = opt + opt_un @keys_pred = ->(v) { pred_exprs.all? { |p| p.call(v) } } @key_to_spec_map = Hash[req_keys.concat(@opt_keys).zip(req_specs.concat(@opt_specs))] end def conform(value) return ns(S, :invalid) unless @keys_pred.call(value) reg = S.registry ret = value value.each do |key, v| spec_name = @key_to_spec_map.fetch(key, key) spec = reg[spec_name] next unless spec conformed_value = S.conform(spec, v) if S.invalid?(conformed_value) return ns(S, :invalid) else unless conformed_value.equal?(v) ret = ret.merge(key => conformed_value) end end end ret end def explain(path, via, inn, value) unless Utils.hash?(value) return [{ :path => path, :pred => [Utils.method(:hash?), [value]], :val => value, :via => via, :in => inn }] end problems = [] if @req.any? failures = parse_req(@req, value, Utils.method(:itself)) failures.each do |failure_sexp| # eww pred = [Utils.method(:key?), [sexp_to_rb(failure_sexp)]] problems << { :path => path, :pred => pred, :val => value, :via => via, :in => inn } end end if @req_un.any? failures = parse_req(@req_un, value, method(:unqualify_key)) failures.each do |failure_sexp| pred = [Utils.method(:key?), [sexp_to_rb(failure_sexp)]] problems << { :path => path, :pred => pred, :val => value, :via => via, :in => inn } end end problems += value.flat_map { |(k, v)| next unless S.registry.key?(@key_to_spec_map[k]) unless S.pvalid?(@key_to_spec_map.fetch(k), v) S.explain1(@key_to_spec_map.fetch(k), Utils.conj(path, k), via, Utils.conj(inn, k), v) end } problems.compact end def specize self end def gen(overrides, path, rmap) return @gen if @gen rmap = S.inck(rmap, @id) reqs = @req_keys.zip(@req_specs). reduce({}) { |m, (k, s)| m.merge(k => S.gensub(s, overrides, Utils.conj(path, k), rmap)) } opts = @opt_keys.zip(@opt_specs). reduce({}) { |m, (k, s)| if S.recur_limit?(rmap, @id, path, k) m else m.merge(k => Gen.delay { S.gensub(s, overrides, Utils.conj(path, k), rmap) }) end } ->(rantly) do count = rantly.range(0, opts.count) opts = Hash[opts.to_a.shuffle.take(count)] reqs.merge(opts).each_with_object({}) { |(k, spec_gen), h| h[k] = spec_gen.call(rantly) } end end private def sexp_to_rb(sexp, level = 0) if sexp.is_a?(Array) op, *keys = sexp rb_string = String.new rb_string << "(" unless level.zero? keys.each_with_index do |key, i| unless i.zero? rb_string << " #{NamespacedSymbols.name(op)} " end rb_string << sexp_to_rb(key, level + 1).to_s end rb_string << ")" unless level.zero? rb_string else sexp end end def extract_keys(symbol_or_arr) if symbol_or_arr.is_a?(Array) symbol_or_arr[1..-1].flat_map(&method(:extract_keys)) else symbol_or_arr end end def unqualify_key(x) NamespacedSymbols.name(x).to_sym end def parse_req(ks, v, f) key, *ks = ks ret = if key.is_a?(Array) op, *kks = key case op when ns(S, :or) if kks.one? { |k| parse_req([k], v, f).empty? } [] else [key] end when ns(S, :and) if kks.all? { |k| parse_req([k], v, f).empty? } [] else [key] end else raise "Expected or, and, got #{op}" end elsif v.key?(f.call(key)) [] else [key] end if ks.any? ret + parse_req(ks, v, f) else ret end end end end