M Makefile => Makefile +1 -1
@@ 15,7 15,7 @@ test: lib/dhall/parser.citrus
bundle exec ruby -E UTF-8 test/test_suite.rb
unit: lib/dhall/parser.citrus
- bundle exec ruby -E UTF-8 test/test_suite.rb -n'/unit|import|TestReadme|TestLoad|TestAsDhall|TestResolvers|TestBinary|TestAsJson/'
+ bundle exec ruby -E UTF-8 test/test_suite.rb -n'/unit|import|TestReadme|TestLoad|TestAsDhall|TestResolvers|TestBinary|TestAsJson|TestCoder/'
clean:
$(RM) lib/dhall/parser.citrus
M lib/dhall.rb => lib/dhall.rb +1 -0
@@ 4,6 4,7 @@ require "dhall/as_dhall"
require "dhall/ast"
require "dhall/binary"
require "dhall/builtins"
+require "dhall/coder"
require "dhall/normalize"
require "dhall/parser"
require "dhall/resolve"
A lib/dhall/coder.rb => lib/dhall/coder.rb +189 -0
@@ 0,0 1,189 @@
+# 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)
+ to_a.map(&decode)
+ end
+ end
+
+ refine Optional do
+ def to_ruby(&decode)
+ reduce(nil, &decode)
+ end
+ end
+
+ refine Function do
+ def to_ruby(&decode)
+ ->(*args) { decode[expr.call(*args)] }
+ end
+ end
+
+ refine Union do
+ def to_ruby
+ if !value.nil? && tag.match(/\A\p{Upper}/) &&
+ Object.const_defined?(tag)
+ yield extract, Object.const_get(tag)
+ elsif extract == :None
+ nil
+ 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
M lib/dhall/util.rb => lib/dhall/util.rb +18 -0
@@ 85,6 85,18 @@ module Dhall
end
end
+ def self.psych_coder_for(tag, v)
+ c = Psych::Coder.new(tag)
+ case v
+ when Hash
+ c.map = v
+ when Array
+ c.seq = v
+ else
+ c.scalar = v
+ end
+ end
+
def self.psych_coder_from(tag, o)
coder = Psych::Coder.new(tag)
@@ 98,5 110,11 @@ module Dhall
coder
end
+
+ def self.transform_keys(hash_or_not)
+ return hash_or_not unless hash_or_not.is_a?(Hash)
+
+ Hash[hash_or_not.map { |k, v| [(yield k), v] }]
+ end
end
end
A test/test_coder.rb => test/test_coder.rb +289 -0
@@ 0,0 1,289 @@
+# frozen_string_literal: true
+
+require "minitest/autorun"
+
+require "dhall"
+
+class TestCoder < Minitest::Test
+ def test_dump_integer
+ assert_equal "\x82\x0F\x01".b, Dhall::Coder.dump(1)
+ end
+
+ def test_dump_integer_negative
+ assert_equal "\x82\x10 ".b, Dhall::Coder.dump(-1)
+ end
+
+ def test_dump_float
+ assert_equal "\xFA?\x80\x00\x00".b, Dhall::Coder.dump(1.0)
+ end
+
+ def test_dump_string
+ assert_equal "\x82\x12ehello".b, Dhall::Coder.dump("hello")
+ end
+
+ def test_dump_nil
+ assert_nil Dhall::Coder.dump(nil)
+ end
+
+ def test_dump_array
+ assert_equal(
+ "\x84\x04\xF6\x82\x0F\x01\x82\x0F\x02".b,
+ Dhall::Coder.dump([1, 2])
+ )
+ end
+
+ def test_dump_array_with_nil
+ assert_equal(
+ "\x84\x04\xF6\x83\x05\xF6\x82\x0F\x01\x83\x00dNonegNatural".b,
+ Dhall::Coder.dump([1, nil])
+ )
+ end
+
+ def test_dump_array_heterogenous
+ assert_equal(
+ "\x86\x04\xF6\x83\x00\x83\t\x82\v\xA4fDoublefDoublegNatural" \
+ "gNaturaldNone\xF6dTextdTextgNatural\x82\x0F\x01\x83\t\x82\v\xA4" \
+ "fDoublefDoublegNaturalgNaturaldNone\xF6dTextdTextdNone\x83\x00\x83" \
+ "\t\x82\v\xA4fDoublefDoublegNaturalgNaturaldNone\xF6dTextdText" \
+ "fDouble\xFA?\x80\x00\x00\x83\x00\x83\t\x82\v\xA4fDoublefDoubleg" \
+ "NaturalgNaturaldNone\xF6dTextdTextdText\x82\x12ehello".b,
+ Dhall::Coder.dump([1, nil, 1.0, "hello"])
+ )
+ end
+
+ def test_dump_hash
+ assert_equal(
+ "\x82\b\xA2aa\x82\x0F\x01ab\xFA?\x80\x00\x00".b,
+ Dhall::Coder.dump(a: 1, b: 1.0)
+ )
+ end
+
+ def test_dump_object
+ assert_raises ArgumentError do
+ Dhall::Coder.dump(Object.new)
+ end
+ end
+
+ def test_load_loaded
+ assert_equal 1, Dhall::Coder.load(1)
+ end
+
+ def test_load_integer
+ assert_equal 1, Dhall::Coder.load("\x82\x0F\x01".b)
+ end
+
+ def test_load_integer_negative
+ assert_equal(-1, Dhall::Coder.load("\x82\x10 ".b))
+ end
+
+ def test_load_float
+ assert_equal 1.0, Dhall::Coder.load("\xFA?\x80\x00\x00".b)
+ end
+
+ def test_load_string
+ assert_equal "hello", Dhall::Coder.load("\x82\x12ehello".b)
+ end
+
+ def test_load_nil
+ assert_nil Dhall::Coder.load(nil)
+ end
+
+ def test_load_array
+ assert_equal(
+ [1, 2],
+ Dhall::Coder.load("\x84\x04\xF6\x82\x0F\x01\x82\x0F\x02".b)
+ )
+ end
+
+ def test_load_array_with_nil
+ assert_equal(
+ [1, nil],
+ Dhall::Coder.load(
+ "\x84\x04\xF6\x83\x05\xF6\x82\x0F\x01\x83\x00dNonegNatural".b
+ )
+ )
+ end
+
+ def test_load_array_heterogenous
+ assert_equal(
+ [1, nil, 1.0, "hello"],
+ Dhall::Coder.load(
+ "\x86\x04\xF6\x83\x00\x83\t\x82\v\xA4fDoublefDoublegNatural" \
+ "gNaturaldNone\xF6dTextdTextgNatural\x82\x0F\x01\x83\t\x82\v\xA4" \
+ "fDoublefDoublegNaturalgNaturaldNone\xF6dTextdTextdNone\x83\x00\x83" \
+ "\t\x82\v\xA4fDoublefDoublegNaturalgNaturaldNone\xF6dTextdText" \
+ "fDouble\xFA?\x80\x00\x00\x83\x00\x83\t\x82\v\xA4fDoublefDoubleg" \
+ "NaturalgNaturaldNone\xF6dTextdTextdText\x82\x12ehello".b
+ )
+ )
+ end
+
+ def test_load_hash
+ assert_equal(
+ { "a" => 1, "b" => 1.0 },
+ Dhall::Coder.load("\x82\b\xA2aa\x82\x0F\x01ab\xFA?\x80\x00\x00".b)
+ )
+ end
+
+ def test_load_hash_symbolize
+ assert_equal(
+ { a: 1, b: 1.0 },
+ Dhall::Coder.load(
+ "\x82\b\xA2aa\x82\x0F\x01ab\xFA?\x80\x00\x00".b,
+ transform_keys: :to_sym
+ )
+ )
+ end
+
+ def test_load_object
+ assert_raises ArgumentError do
+ Dhall::Coder.load(
+ "\x83\x00\x83\t\x82\v\xA1fObject\x82\a\xA0fObject\x82\b\xA0".b
+ )
+ end
+ end
+
+ class Custom
+ attr_reader :a, :b
+
+ def initialize
+ @a = true
+ @b = "true"
+ end
+
+ def ==(other)
+ a == other.a && b == other.b
+ end
+ end
+
+ def test_bad_default
+ assert_raises ArgumentError do
+ Dhall::Coder.new(safe: Custom)
+ end
+ end
+
+ def test_dump_custom
+ assert_equal(
+ "\x83\x00\x83\t\x82\v\xA1qTestCoder::Custom\x82\a\xA2aadBoolabd" \
+ "TextqTestCoder::Custom\x82\b\xA2aa\xF5ab\x82\x12dtrue".b,
+ Dhall::Coder.new(default: Custom.new, safe: Custom).dump(Custom.new)
+ )
+ end
+
+ def test_load_custom
+ coder = Dhall::Coder.new(
+ default: Custom.new,
+ safe: Dhall::Coder::JSON_LIKE + [Custom]
+ )
+ assert_equal(
+ Custom.new,
+ coder.load(
+ "\x83\x00\x83\t\x82\v\xA1qTestCoder::Custom\x82\a\xA2aadBoolabd" \
+ "TextqTestCoder::Custom\x82\b\xA2aa\xF5ab\x82\x12dtrue".b
+ )
+ )
+ end
+
+ class CustomCoding
+ attr_reader :a, :b
+
+ def initialize
+ @a = true
+ @b = "true"
+ end
+
+ def ==(other)
+ a == other.a && b == other.b
+ end
+
+ def init_with(coder)
+ @a = coder["abool"]
+ @b = coder["astring"]
+ end
+
+ def encode_with(coder)
+ coder["abool"] = @a
+ coder["astring"] = @b
+ end
+ end
+
+ def test_dump_custom_coding
+ assert_equal(
+ "\x83\x00\x83\t\x82\v\xA1wTestCoder::CustomCoding\x82\a\xA2" \
+ "eabooldBoolgastringdTextwTestCoder::CustomCoding\x82\b\xA2" \
+ "eabool\xF5gastring\x82\x12dtrue".b,
+ Dhall::Coder.new(
+ default: CustomCoding.new,
+ safe: CustomCoding
+ ).dump(CustomCoding.new)
+ )
+ end
+
+ def test_load_custom_coding
+ coder = Dhall::Coder.new(
+ default: CustomCoding.new,
+ safe: Dhall::Coder::JSON_LIKE + [CustomCoding]
+ )
+ assert_equal(
+ CustomCoding.new,
+ coder.load(
+ "\x83\x00\x83\t\x82\v\xA1wTestCoder::CustomCoding\x82\a\xA2" \
+ "eabooldBoolgastringdTextwTestCoder::CustomCoding\x82\b\xA2" \
+ "eabool\xF5gastring\x82\x12dtrue".b
+ )
+ )
+ end
+
+ class CustomDhall
+ using Dhall::AsDhall
+
+ attr_reader :str
+
+ def self.from_dhall(expr)
+ new(expr.to_s)
+ end
+
+ def initialize(str="test")
+ @str = str
+ end
+
+ def ==(other)
+ str == other.str
+ end
+
+ def as_dhall
+ Dhall::Union.from(
+ Dhall::UnionType.new(
+ alternatives: { self.class.name => Dhall::Text.as_dhall }
+ ),
+ self.class.name,
+ @str.as_dhall
+ )
+ end
+ end
+
+ def test_dump_custom_dhall
+ assert_equal(
+ "\x83\x00\x83\t\x82\v\xA1vTestCoder::CustomDhalldText" \
+ "vTestCoder::CustomDhall\x82\x12dtest".b,
+ Dhall::Coder.new(
+ default: CustomDhall.new,
+ safe: CustomDhall
+ ).dump(CustomDhall.new)
+ )
+ end
+
+ def test_load_custom_dhall
+ coder = Dhall::Coder.new(
+ default: CustomDhall.new,
+ safe: Dhall::Coder::JSON_LIKE + [CustomDhall]
+ )
+ assert_equal(
+ CustomDhall.new,
+ coder.load(
+ "\x83\x00\x83\t\x82\v\xA1vTestCoder::CustomDhalldText" \
+ "vTestCoder::CustomDhall\x82\x12dtest".b
+ )
+ )
+ end
+end