# frozen_string_literal: true
require "ostruct"
require "psych"
module Dhall
module AsDhall
TAGS = {
::Integer => "Integer",
::FalseClass => "Bool",
::Integer => "Integer",
::Float => "Double",
::NilClass => "None",
::String => "Text",
::TrueClass => "Bool"
}.freeze
def self.tag_for(o)
return "Natural" if o.is_a?(::Integer) && !o.negative?
TAGS.fetch(o.class) do
o.class.name
end
end
class AnnotatedExpressionList
attr_reader :type
attr_reader :exprs
def self.from(type_annotation)
if type_annotation.nil?
new(nil, [nil])
else
new(type_annotation.type, [type_annotation.value])
end
end
def initialize(type, exprs)
@type = type
@exprs = exprs
end
def +(other)
raise "#{type} != #{other.type}" if type != other.type
self.class.new(type, exprs + other.exprs)
end
end
class UnionInferer
def initialize(tagged={})
@tagged = tagged
end
def union_type
UnionType.new(alternatives: Hash[@tagged.map { |k, v| [k, v.type] }])
end
def union_for(expr)
if expr.is_a?(Enum)
tag = expr.tag
expr = nil
else
tag = @tagged.keys.find { |k| @tagged[k].exprs.include?(expr) }
end
expr = expr.extract if expr.is_a?(Union)
Union.from(union_type, tag, expr)
end
def with(tag, type_annotation)
anno = AnnotatedExpressionList.from(type_annotation)
if @tagged.key?(tag) && @tagged[tag].type != anno.type
disambiguate_against(tag, anno)
else
self.class.new(@tagged.merge(tag => anno) { |_, x, y| x + y })
end
end
def disambiguate_against(tag, anno)
self.class.new(
@tagged.reject { |k, _| k == tag }.merge(
"#{tag}_#{@tagged[tag].type.digest.hexdigest}" => @tagged[tag],
"#{tag}_#{anno.type.digest.hexdigest}" => anno
)
)
end
end
refine ::String do
def as_dhall
if encoding == Encoding::BINARY
bytes.as_dhall
else
Text.new(value: self)
end
end
end
refine ::Symbol do
def as_dhall
Dhall::Enum.new(
tag: to_s,
alternatives: Dhall::UnionType.new(alternatives: {})
)
end
end
refine ::Integer do
def as_dhall
if negative?
Integer.new(value: self)
else
Natural.new(value: self)
end
end
end
refine ::Float do
def as_dhall
Double.new(value: self)
end
end
refine ::TrueClass do
def as_dhall
Bool.new(value: true)
end
end
refine ::FalseClass do
def as_dhall
Bool.new(value: false)
end
end
refine ::NilClass do
def as_dhall
raise(
"Cannot call NilClass#as_dhall directly, " \
"you probably want to create a Dhall::OptionalNone yourself."
)
end
end
module ExpressionList
def self.for(values, exprs)
types = exprs.map(&TypeChecker.method(:type_of))
if types.empty?
Empty
elsif types.include?(nil) && types.uniq.length <= 2
Optional
elsif types.uniq.length == 1
Mono
else
Union
end.new(values, exprs, types)
end
class Empty
def initialize(*); end
def list
EmptyList.new(element_type: UnionType.new(alternatives: {}))
end
end
class Optional
def initialize(_, exprs, types)
@type = types.compact.first
@exprs = exprs
end
def list
List.new(elements: @exprs.map do |x|
if x.nil?
Dhall::OptionalNone.new(value_type: @type)
else
Dhall::Optional.new(value: x)
end
end)
end
end
class Mono
def initialize(_, exprs, _)
@exprs = exprs
end
def list
List.new(elements: @exprs)
end
end
class Union
def initialize(values, exprs, types)
@tags, @types = values.zip(types).map { |(value, type)|
if type.is_a?(UnionType) && type.alternatives.length == 1
type.alternatives.to_a.first
else
[AsDhall.tag_for(value), type]
end
}.transpose
@exprs = exprs
@inferer = UnionInferer.new
end
def list
final_inferer =
@tags
.zip(@exprs, @types)
.reduce(@inferer) do |inferer, (tag, expr, type)|
inferer.with(
tag,
type.nil? ? nil : TypeAnnotation.new(value: expr, type: type)
)
end
List.new(elements: @exprs.map(&final_inferer.method(:union_for)))
end
end
end
refine ::Array do
def as_dhall
ExpressionList.for(self, map { |x| x&.as_dhall }).list
end
end
refine ::Hash do
def as_dhall
if empty?
EmptyRecord.new
else
Record.new(record: Hash[
reject { |_, v| v.nil? }
.map { |k, v| [k.to_s, v.as_dhall] }
.sort
])
end
end
end
refine ::OpenStruct do
def as_dhall
expr = to_h.as_dhall
type = TypeChecker.for(expr).annotate(TypeChecker::Context.new).type
Union.from(
UnionType.new(alternatives: { "OpenStruct" => type }),
"OpenStruct",
expr
)
end
end
refine ::Psych::Coder do
def as_dhall
case type
when :seq
seq
when :map
map
else
scalar
end.as_dhall
end
end
refine ::Object do
def as_dhall
tag = self.class.name
expr = Util.psych_coder_from(tag, self).as_dhall
type = TypeChecker.for(expr).annotate(TypeChecker::Context.new).type
Union.from(
UnionType.new(alternatives: { tag => type }),
tag,
expr
)
end
end
refine ::Proc do
def as_dhall
FunctionProxy.new(self)
end
end
end
end