~singpolyma/dhall-ruby

3dae10e1594374c8a64de89ce83278a1822e7758 — Stephen Paul Weber 2 years ago 2eb7061
implement as Location and use URI internally
M dhall.gemspec => dhall.gemspec +1 -0
@@ 27,6 27,7 @@ Gem::Specification.new do |spec|

	spec.add_dependency "cbor", "~> 0.5.9.3"
	spec.add_dependency "citrus", "~> 3.0"
	spec.add_dependency "lazy_object", "~> 0.0.3"
	spec.add_dependency "multihashes", "~> 0.1.3"
	spec.add_dependency "promise.rb", "~> 0.7.4"
	spec.add_dependency "value_semantics", "~> 3.0"

M lib/dhall/ast.rb => lib/dhall/ast.rb +78 -54
@@ 1,5 1,6 @@
# frozen_string_literal: true

require "lazy_object"
require "multihashes"
require "uri"
require "value_semantics"


@@ 1256,60 1257,43 @@ module Dhall
			end
		end

		Location = LazyObject.new do
			UnionType.new(
				alternatives: {
					"Local"       => Builtins[:Text],
					"Remote"      => Builtins[:Text],
					"Environment" => Builtins[:Text],
					"Missing"     => nil
				}
			)
		end

		class URI
			include(ValueSemantics.for_attributes do
				headers   Either(nil, Expression)
				authority ::String
				path      ArrayOf(::String)
				query     Either(nil, ::String)
				uri       ::URI
				headers   Either(nil, Expression), default: nil
			end)

			def initialize(headers, authority, *path, query)
				super(
					headers:   headers,
					authority: authority,
					path:      path,
					query:     query,
				)
			end

			def with(hash)
				self.class.new(
					hash.fetch(:headers, headers),
					hash.fetch(:authority, authority),
					*hash.fetch(:path, path),
					hash.fetch(:query, query)
				)
			end
			def with(attrs)
				if attrs.key?(:path)
					attrs[:uri] =
						uri + Util.path_components_to_uri(*attrs.delete(:path))
				end

			def self.from_uri(uri)
				(uri.scheme == "https" ? Https : Http).new(
					nil,
					"#{uri.host}:#{uri.port}",
					*uri.path.split(/\//)[1..-1],
					uri.query,
					nil
				)
				super
			end

			def headers
				header_type = RecordType.new(
					record: {
						"header" => Builtins[:Text],
						"value"  => Builtins[:Text]
						"mapKey"   => Builtins[:Text],
						"mapValue" => Builtins[:Text]
					}
				)

				super || EmptyList.new(element_type: header_type)
			end

			def uri
				escaped_path = path.map do |c|
					::URI.encode_www_form_component(c).gsub("+", "%20")
				end
				URI("#{scheme}://#{authority}/#{escaped_path.join("/")}?#{query}")
			end

			def chain_onto(relative_to)
				if headers.is_a?(Import)
					with(headers: headers.with(path: headers.real_path(relative_to)))


@@ 1320,23 1304,43 @@ module Dhall

			def canonical
				with(
					path: (path[1..-1] + [""])
					.reduce([[], path.first]) { |(pth, prev), c|
					path: (path[1..-1] + [""]).reduce([[], path.first]) { |(pth, prev), c|
						c == ".." ? [pth, prev] : [pth + [prev], c]
					}.first.reject { |c| c == "." }
				)
			end

			def port
				uri.port && uri.port != uri.default_port ? uri.port : nil
			end

			def authority
				[
					uri.userinfo,
					[uri.host, port].compact.join(":")
				].compact.join("@")
			end

			def origin
				"#{scheme}://#{authority}"
				"#{uri.scheme}://#{authority}"
			end

			def to_s
				uri.to_s
			end

			def location
				Union.from(Location, "Remote", to_s.as_dhall)
			end

			def path
				path = uri.path.split(/\//, -1).map(&::URI.method(:unescape))
				path = path[1..-1] if path.length > 1 && path.first.empty?
				path
			end

			def as_json
				[@headers&.as_json, authority, *path, query]
				[@headers&.as_json, authority, *path, uri.query]
			end
		end



@@ 1344,20 1348,12 @@ module Dhall
			def resolve(resolver)
				resolver.resolve_http(self)
			end

			def scheme
				"http"
			end
		end

		class Https < URI
			def resolve(resolver)
				resolver.resolve_https(self)
			end

			def scheme
				"https"
			end
		end

		class Path


@@ 1402,6 1398,10 @@ module Dhall
				pathname.to_s
			end

			def location
				Union.from(Location, "Local", to_s.as_dhall)
			end

			def as_json
				path
			end


@@ 1412,8 1412,8 @@ module Dhall
				Pathname.new("/").join(*path)
			end

			def to_uri(scheme, authority)
				scheme.new(nil, authority, *path, nil)
			def to_uri(scheme, base_uri)
				scheme.new(uri: base_uri + Util.path_components_to_uri(*path))
			end

			def chain_onto(relative_to)


@@ 1430,6 1430,10 @@ module Dhall
				Pathname.new(".").join(*path)
			end

			def to_s
				"./#{pathname}"
			end

			def chain_onto(relative_to)
				relative_to.with(
					path: relative_to.path[0..-2] + path


@@ 1509,6 1513,10 @@ module Dhall
				end}"
			end

			def location
				Union.from(Location, "Environment", to_s.as_dhall)
			end

			def hash
				@var.hash
			end


@@ 1516,7 1524,7 @@ module Dhall
			def eql?(other)
				other.is_a?(self.class) && other.var == var
			end
			alias eql? ==
			alias == eql?

			def as_json
				@var


@@ 1542,6 1550,15 @@ module Dhall
				"missing"
			end

			def location
				Union.from(Location, "Missing", nil)
			end

			def eql?(other)
				other.class == self.class
			end
			alias == eql?

			def as_json
				[]
			end


@@ 1561,9 1578,16 @@ module Dhall
			end
		end

		class AsLocation
			def self.call(*)
				raise "AsLocation is only a marker, you don't actually call it"
			end
		end

		IMPORT_TYPES = [
			Expression,
			Text
			Text,
			AsLocation
		].freeze

		PATH_TYPES = [

M lib/dhall/binary.rb => lib/dhall/binary.rb +35 -1
@@ 6,6 6,7 @@ require "multihashes"

require "dhall/ast"
require "dhall/builtins"
require "dhall/parser"

module Dhall
	def self.from_binary(cbor_binary)


@@ 210,13 211,46 @@ module Dhall
			end
		end

		class URI
			def self.decode(headers, authority, *path, query)
				new(
					headers: headers,
					uri:     ::URI.scheme_list[name.split(/::/).last.upcase].build(
						Parser.parse(authority, root: :authority).value.merge(
							path:  Util.path_components_to_uri(*path).path,
							query: query
						)
					)
				)
			end
		end

		class Path
			def self.decode(*args)
				new(*args)
			end
		end

		class EnvironmentVariable
			def self.decode(*args)
				new(*args)
			end
		end

		class MissingImport
			def self.decode(*args)
				new(*args)
			end
		end

		def self.decode(integrity_check, import_type, path_type, *parts)
			parts[0] = Dhall.decode(parts[0]) if path_type < 2 && !parts[0].nil?
			path_type = PATH_TYPES.fetch(path_type)

			new(
				IntegrityCheck.decode(integrity_check),
				IMPORT_TYPES[import_type],
				PATH_TYPES[path_type].new(*parts)
				path_type.decode(*parts)
			)
		end
	end

M lib/dhall/parser.rb => lib/dhall/parser.rb +48 -17
@@ 510,6 510,8 @@ module Dhall
			def value
				import_type = if captures.key?(:text)
					Dhall::Import::Text
				elsif captures.key?(:location)
					Dhall::Import::AsLocation
				else
					Dhall::Import::Expression
				end


@@ 538,29 540,45 @@ module Dhall
			end
		end

		module Scheme
			def value
				::URI.scheme_list[string.upcase]
			end
		end

		module Authority
			def value
				{
					userinfo: capture(:userinfo)&.value,
					host:     capture(:host).value,
					port:     capture(:port)&.value
				}
			end
		end

		module Http
			SCHEME = {
				"http"  => Dhall::Import::Http,
				"https" => Dhall::Import::Https
			}.freeze

			def http(key)
				@http ||= capture(:http_raw)
				@http.capture(key)&.value
			end

			def value
				http = capture(:http_raw)
				SCHEME.fetch(http.capture(:scheme).value).new(
					capture(:import_hashed)&.value(Dhall::Import::Expression),
					http.capture(:authority).value,
					*unescaped_components,
					http.capture(:query)&.value
				uri = http(:scheme).build(
					http(:authority).merge(
						path:  http(:url_path) || "/",
						query: http(:query)
					)
				)
			end

			def unescaped_components
				capture(:http_raw)
					.capture(:path)
					.captures(:path_component)
					.map do |pc|
						pc.value(URI.method(:unescape))
					end
				SCHEME.fetch(uri.scheme).new(
					headers: capture(:import_expression)&.value,
					uri:     uri
				)
			end
		end



@@ 637,15 655,28 @@ module Dhall
		end

		module PathComponent
			def value(unescaper=:itself.to_proc)
			def value(escaper=:itself.to_proc)
				if captures.key?(:quoted_path_component)
					capture(:quoted_path_component).value
					escaper.call(capture(:quoted_path_component).value)
				else
					unescaper.call(capture(:unquoted_path_component).value)
					capture(:unquoted_path_component).value
				end
			end
		end

		module UrlPath
			def value
				"/" + matches.map { |pc|
					if pc.captures.key?(:path_component)
						# We escape here because ruby stdlib URI just stores path unparsed
						pc.value(Util.method(:uri_escape))
					else
						pc.string[1..-1]
					end
				}.join("/")
			end
		end

		module Missing
			def value
				Dhall::Import::MissingImport.new

M lib/dhall/resolve.rb => lib/dhall/resolve.rb +16 -0
@@ 427,9 427,25 @@ module Dhall
			).then { |h| @expr.with(h) }
		end

		class ImportAsLocationResolver < ExpressionResolver
			def resolve(resolver:, relative_to:)
				Promise.resolve(nil).then do
					@expr.real_path(relative_to).location
				end
			end
		end

		class ImportResolver < ExpressionResolver
			register_for Import

			def self.new(expr)
				if expr.import_type == Import::AsLocation
					ImportAsLocationResolver.new(expr)
				else
					super
				end
			end

			def resolve(resolver:, relative_to:)
				Promise.resolve(nil).then do
					resolver.cache_fetch(@expr.cache_key(relative_to)) do

M lib/dhall/typecheck.rb => lib/dhall/typecheck.rb +1 -1
@@ 12,7 12,7 @@ module Dhall

		def self.assert_type(expr, assertion, message, context:)
			aexpr = self.for(expr).annotate(context)
			type = aexpr.type
			type = aexpr.type.normalize
			raise TypeError, "#{message}: #{type}" unless assertion === type
			aexpr
		end

M lib/dhall/util.rb => lib/dhall/util.rb +8 -0
@@ 192,5 192,13 @@ module Dhall
					.reduce(&method(:longest_common_prefix))&.length.to_i
			end
		end

		def self.path_components_to_uri(*components)
			URI("/#{components.map(&method(:uri_escape)).join("/")}")
		end

		def self.uri_escape(s)
			::URI.encode_www_form_component(s).gsub("+", "%20")
		end
	end
end

M test/test_resolve.rb => test/test_resolve.rb +38 -6
@@ 264,7 264,7 @@ class TestResolve < Minitest::Test
				digest: Dhall::Variable["_"].digest.digest
			),
			Dhall::Import::Expression,
			Dhall::Import::Http.new(nil, "example.com", "thing.dhall", nil)
			Dhall::Import::Http.new(uri: URI("http://example.com/thing.dhall"))
		)

		cache = Dhall::Resolvers::RamCache.new


@@ 315,19 315,51 @@ class TestResolve < Minitest::Test
		assert_equal Dhall::Variable["_"], expr.resolve.sync
	end

	DIRPATH = Pathname.new(File.dirname(__FILE__))
	TESTS = DIRPATH + "../dhall-lang/tests/import/"
	DIRPATH = Pathname.new(File.dirname(__FILE__)).realpath
	TESTS = (DIRPATH + "../dhall-lang/tests/import/").realpath

	Pathname.glob(TESTS + "success/**/*A.dhall").each do |path|
		Dir.chdir(path.dirname)
		ENV["XDG_CACHE_HOME"] = (TESTS + "cache").to_s

		test = path.relative_path_from(TESTS).to_s.sub(/A\.dhall$/, "")

		define_method("test_#{test}") do
			stub_request(:any, "https://httpbin.org/user-agent")
				.to_return do |req|
					{
						headers: { "Access-Control-Allow-Origin": "*" },
						body:    <<~JSON
							{
							  "user-agent": "#{req.headers["User-Agent"]}"
							}
						JSON
					}
				end

			stub_request(
				:get,
				"https://raw.githubusercontent.com/dhall-lang/dhall-lang/" \
				"master/tests/import/success/customHeadersA.dhall"
			).to_return(body: (TESTS + "success/customHeadersA.dhall").read)

			stub_request(:get, "https://test.dhall-lang.org/Bool/package.dhall")
				.with(headers: { "Test" => "Example" })
				.to_return(body: "./test.dhall")

			stub_request(:get, "https://test.dhall-lang.org/Bool/test.dhall")
				.with(headers: { "Test" => "Example" })
				.to_return(body: (TESTS + "#{test}B.dhall").read)

			Dhall::Function.disable_alpha_normalization!
			assert_equal(
				Dhall::Parser.parse_file(TESTS + "#{test}B.dhall").value,
				Dhall::Parser.parse_file(TESTS + "#{test}B.dhall").value.normalize,
				Dhall::Parser.parse_file(path).value.resolve(
					relative_to: Dhall::Import::Path.from_string(path)
				).sync
					resolver:    Dhall::Resolvers::Standard.new,
					relative_to: Dhall::Import::Path.from_string("./")
				).sync.normalize
			)
			Dhall::Function.enable_alpha_normalization!
		end
	end


M test/test_resolvers.rb => test/test_resolvers.rb +4 -4
@@ 36,7 36,7 @@ class TestResolvers < Minitest::Test
				sources.map { |source| Promise.resolve(source.to_s) }
			end
		)
		source = Dhall::Import::Http.new(nil, "example.com", "x.dhall", nil)
		source = Dhall::Import::Http.new(uri: URI("http://example.com/x.dhall"))
		promise = source.resolve(resolver)
		resolver.finish!
		assert_equal source.to_s, promise.sync


@@ 48,7 48,7 @@ class TestResolvers < Minitest::Test
				sources.map { |source| Promise.resolve(source.to_s) }
			end
		)
		source = Dhall::Import::Https.new(nil, "example.com", "x.dhall", nil)
		source = Dhall::Import::Https.new(uri: URI("https://example.com/x.dhall"))
		promise = source.resolve(resolver)
		resolver.finish!
		assert_equal source.to_s, promise.sync


@@ 60,7 60,7 @@ class TestResolvers < Minitest::Test
				sources.map { |source| Promise.resolve(source.to_s) }
			end
		)
		source = Dhall::Import::Https.new(nil, "example.com", "x.dhall", nil)
		source = Dhall::Import::Https.new(uri: URI("https://example.com/x.dhall"))
		promise = source.resolve(resolver)
		resolver.finish!
		assert_equal source.to_s, promise.sync


@@ 68,7 68,7 @@ class TestResolvers < Minitest::Test

	def test_local_only_resolver_rejects_http
		resolver = Dhall::Resolvers::LocalOnly.new
		source = Dhall::Import::Http.new(nil, "example.com", "x.dhall", nil)
		source = Dhall::Import::Http.new(uri: URI("http://example.com/x.dhall"))
		promise = source.resolve(resolver)
		resolver.finish!
		assert_raises Dhall::ImportBannedException do