# frozen_string_literal: true require "psych" module Dhall class Coder JSON_LIKE = [ ::Array, ::Hash, ::TrueClass, ::FalseClass, ::NilClass, ::Integer, ::Float, ::String ].freeze class Verifier def initialize(*classes) @classes = classes @matcher = ValueSemantics::Either.new(classes) end def verify_class(klass, op) if @classes.any? { |safe| klass <= safe } klass else raise ArgumentError, "#{op} does not match "\ "#{@classes.inspect}: #{klass}" end end def verify(obj, op) if @matcher === obj obj else raise ArgumentError, "#{op} does not match "\ "#{@classes.inspect}: #{obj.inspect}" end end end def self.load(source, transform_keys: :to_s) new.load(source, transform_keys: transform_keys) end def self.dump(obj) new.dump(obj) end def initialize(default: nil, safe: JSON_LIKE) @default = default @verifier = Verifier.new(*Array(safe)) @verifier.verify(default, "default value") end def load_async(source, op="load_async", transform_keys: :to_s) return Promise.resolve(@default) if source.nil? return Promise.resolve(source) unless source.is_a?(String) Dhall.load(source).then do |expr| decode(expr, op, transform_keys: transform_keys) end end def load(source, transform_keys: :to_s) load_async(source, "load", transform_keys: transform_keys).sync end module ToRuby refine Expression do def to_ruby self end end refine Natural do alias_method :to_ruby, :to_i end refine Integer do alias_method :to_ruby, :to_i end refine Double do alias_method :to_ruby, :to_f end refine Text do alias_method :to_ruby, :to_s end refine Bool do def to_ruby self === true end end refine Record do def to_ruby(&decode) Hash[to_h.map { |k, v| [k, decode[v]] }] end end refine EmptyRecord do def to_ruby {} end end refine List do def to_ruby(&decode) unless element_type.is_a?(RecordType) && element_type.keys == ["mapKey", "mapValue"] return to_a.map(&decode) end Hash[to_a.map { |r| [r["mapKey"].to_s, decode[r["mapValue"]]] }] end end refine Optional do def to_ruby(&decode) reduce(nil, &decode) end end refine Function do def to_ruby(&decode) ->(*args) { decode[call(*args)] } end end refine Enum do def to_ruby extract == :None ? nil : extract end end refine Union do def to_ruby rtag = tag.sub(/_[0-9a-f]{64}\Z/, "") if tag.match(/\A\p{Upper}/) && Object.const_defined?(rtag) && !Dhall.const_defined?(rtag, false) yield extract, Object.const_get(rtag) else yield extract end end end refine TypeAnnotation do def to_ruby yield value end end end using ToRuby module InitWith refine Object do def init_with(coder) coder.map.each do |k, v| instance_variable_set(:"@#{k}", v) end end end end using InitWith def revive(klass, expr, op="revive", transform_keys: :to_s) @verifier.verify_class(klass, op) return klass.from_dhall(expr) if klass.respond_to?(:from_dhall) klass.allocate.tap do |o| o.init_with(Util.psych_coder_for( klass.name, decode(expr, op, transform_keys: transform_keys) )) end end def decode(expr, op="decode", klass: nil, transform_keys: :to_s) return revive(klass, expr, op, transform_keys: transform_keys) if klass @verifier.verify( Util.transform_keys( expr.to_ruby { |dexpr, dklass| decode(dexpr, op, klass: dklass, transform_keys: transform_keys) }, &transform_keys ), op ) end def dump(obj) return if obj.nil? Dhall.dump(@verifier.verify(obj, "dump")) end end end