# frozen_string_literal: true module Dry # rubocop:disable Metrics/AbcSize # rubocop:disable Metrics/PerceivedComplexity module Types # @api private class Printer MAPPING = { # rubocop:disable Style/MutableConstant Nominal => :visit_nominal, Constructor => :visit_constructor, Constrained => :visit_constrained, Constrained::Coercible => :visit_constrained, Hash => :visit_hash, Schema => :visit_schema, Schema::Key => :visit_key, Map => :visit_map, Array => :visit_array, Array::Member => :visit_array_member, Lax => :visit_lax, Enum => :visit_enum, Default => :visit_default, Default::Callable => :visit_default, Sum => :visit_sum, Sum::Constrained => :visit_sum, Any.class => :visit_any } def call(type) output = "".dup visit(type) { |str| output << str } "#<Dry::Types[#{output}]>" end def visit(type, &block) print_with = MAPPING.fetch(type.class) do if type.class < Constructor :visit_constructor elsif type.is_a?(Type) return yield type.inspect else raise ArgumentError, "Do not know how to print #{type.class}" end end send(print_with, type, &block) end def visit_any(_) yield "Any" end def visit_array(type) visit_options(EMPTY_HASH, type.meta) do |opts| yield "Array#{opts}" end end def visit_array_member(array) visit(array.member) do |type| visit_options(EMPTY_HASH, array.meta) do |opts| yield "Array<#{type}#{opts}>" end end end def visit_constructor(constructor) visit(constructor.type) do |type| visit_callable(constructor.fn.fn) do |fn| options = constructor.options.dup options.delete(:fn) visit_options(options) do |opts| yield "Constructor<#{type} fn=#{fn}#{opts}>" end end end end def visit_constrained(constrained) visit(constrained.type) do |type| options = constrained.options.dup rule = options.delete(:rule) visit_options(options) do |_opts| yield "Constrained<#{type} rule=[#{rule}]>" end end end def visit_schema(schema) options = schema.options.dup size = schema.count key_fn_str = "" type_fn_str = "" strict_str = "" strict_str = "strict " if options.delete(:strict) if (key_fn = options.delete(:key_transform_fn)) visit_callable(key_fn) do |fn| key_fn_str = "key_fn=#{fn} " end end if (type_fn = options.delete(:type_transform_fn)) visit_callable(type_fn) do |fn| type_fn_str = "type_fn=#{fn} " end end keys = options.delete(:keys) visit_options(options, schema.meta) do |opts| opts = "#{opts[1..]} " unless opts.empty? schema_parameters = "#{key_fn_str}#{type_fn_str}#{strict_str}#{opts}" header = "Schema<#{schema_parameters}keys={" if size.zero? yield "#{header}}>" else yield header.dup << keys.map { |key| visit(key) { |type| type } }.join(" ") << "}>" end end end def visit_map(map) visit(map.key_type) do |key| visit(map.value_type) do |value| options = map.options.dup options.delete(:key_type) options.delete(:value_type) visit_options(options) do |_opts| yield "Map<#{key} => #{value}>" end end end end def visit_key(key) visit(key.type) do |type| if key.required? yield "#{key.name}: #{type}" else yield "#{key.name}?: #{type}" end end end def visit_sum(sum) visit_sum_constructors(sum) do |constructors| visit_options(sum.options, sum.meta) do |opts| yield "Sum<#{constructors}#{opts}>" end end end def visit_sum_constructors(sum) case sum.left when Sum visit_sum_constructors(sum.left) do |left| case sum.right when Sum visit_sum_constructors(sum.right) do |right| yield "#{left} | #{right}" end else visit(sum.right) do |right| yield "#{left} | #{right}" end end end else visit(sum.left) do |left| case sum.right when Sum visit_sum_constructors(sum.right) do |right| yield "#{left} | #{right}" end else visit(sum.right) do |right| yield "#{left} | #{right}" end end end end end def visit_enum(enum) visit(enum.type) do |type| options = enum.options.dup mapping = options.delete(:mapping) visit_options(options) do |opts| if mapping == enum.inverted_mapping values = mapping.values.map(&:inspect).join(", ") yield "Enum<#{type} values={#{values}}#{opts}>" else mapping_str = mapping.map { |key, value| "#{key.inspect}=>#{value.inspect}" }.join(", ") yield "Enum<#{type} mapping={#{mapping_str}}#{opts}>" end end end end def visit_default(default) visit(default.type) do |type| visit_options(default.options) do |opts| if default.is_a?(Default::Callable) visit_callable(default.value) do |fn| yield "Default<#{type} value_fn=#{fn}#{opts}>" end else yield "Default<#{type} value=#{default.value.inspect}#{opts}>" end end end end def visit_nominal(type) visit_options(type.options, type.meta) do |opts| yield "Nominal<#{type.primitive}#{opts}>" end end def visit_lax(lax) visit(lax.type) do |type| yield "Lax<#{type}>" end end def visit_hash(hash) options = hash.options.dup type_fn_str = "" if (type_fn = options.delete(:type_transform_fn)) visit_callable(type_fn) do |fn| type_fn_str = "type_fn=#{fn}" end end visit_options(options, hash.meta) do |opts| if opts.empty? && type_fn_str.empty? yield "Hash" else yield "Hash<#{type_fn_str}#{opts}>" end end end def visit_callable(callable) fn = callable.is_a?(String) ? FnContainer[callable] : callable case fn when ::Method yield "#{fn.receiver}.#{fn.name}" when ::Proc path, line = fn.source_location if line&.zero? yield ".#{path}" elsif path yield "#{path.sub("#{Dir.pwd}/", EMPTY_STRING)}:#{line}" else match = fn.to_s.match(/\A#<Proc:0x\h+\(&:(?<name>\w+)\)(:? \(lambda\))?>\z/) # rubocop:disable Lint/MixedRegexpCaptureTypes if match yield ".#{match[:name]}" elsif fn.lambda? yield "(lambda)" else yield "(proc)" end end else call = fn.method(:call) if call.owner == fn.class yield "#{fn.class}#call" else yield "#{fn}.call" end end end def visit_options(options, meta = EMPTY_HASH) # rubocop:disable Metrics/PerceivedComplexity if options.empty? && meta.empty? yield "" else opts = options.empty? ? "" : " options=#{options.inspect}" if meta.empty? yield opts else values = meta.map do |key, value| case key when Symbol "#{key}: #{value.inspect}" else "#{key.inspect}=>#{value.inspect}" end end yield "#{opts} meta={#{values.join(", ")}}" end end end end PRINTER = Printer.new.freeze end # rubocop:enable Metrics/AbcSize # rubocop:enable Metrics/PerceivedComplexity end