# frozen_string_literal: true
require "pathname"
require "promise.rb"
require "set"
require "dhall/ast"
require "dhall/binary"
require "dhall/util"
module Dhall
class ImportFailedException < StandardError; end
class ImportBannedException < ImportFailedException; end
class ImportLoopException < ImportBannedException; end
module Resolvers
ReadPathSources = lambda do |sources|
sources.map do |source|
Promise.resolve(nil).then { source.pathname.binread }
end
end
PreflightCORS = lambda do |source, parent_origin|
uri = source.uri
if parent_origin != "localhost" && parent_origin != source.origin
req = Net::HTTP::Options.new(uri)
req["Origin"] = parent_origin
req["Access-Control-Request-Method"] = "GET"
req["Access-Control-Request-Headers"] =
source.headers.map { |h| h.fetch("header").to_s }.join(",")
r = Net::HTTP.start(
uri.hostname,
uri.port,
use_ssl: uri.scheme == "https"
) { |http| http.request(req) }
raise ImportFailedException, source if r.code != "200"
unless r["Access-Control-Allow-Origin"] == parent_origin ||
r["Access-Control-Allow-Origin"] == "*"
raise ImportBannedException, source
end
end
end
ReadHttpSources = lambda do |sources, parent_origin|
sources.map do |source|
Promise.resolve(nil).then do
PreflightCORS.call(source, parent_origin)
uri = source.uri
req = Net::HTTP::Get.new(uri)
source.headers.each do |header|
req[header.fetch("header").to_s] = header.fetch("value").to_s
end
r = Net::HTTP.start(
uri.hostname,
uri.port,
use_ssl: uri.scheme == "https"
) { |http| http.request(req) }
raise ImportFailedException, source if r.code != "200"
r.body
end
end
end
RejectSources = lambda do |sources|
sources.map do |source|
Promise.new.reject(ImportBannedException.new(source))
end
end
class ReadPathAndIPFSSources
def initialize(
path_reader: ReadPathSources,
http_reader: ReadHttpSources,
https_reader: http_reader,
public_gateway: "cloudflare-ipfs.com"
)
@path_reader = path_reader
@http_reader = http_reader
@https_reader = https_reader
@public_gateway = public_gateway
end
def arity
1
end
def call(sources)
@path_reader.call(sources).map.with_index do |promise, idx|
source = sources[idx]
if source.is_a?(Import::AbsolutePath) &&
["ipfs", "ipns"].include?(source.path.first)
gateway_fallback(source, promise)
else
promise
end
end
end
def to_proc
method(:call).to_proc
end
protected
def gateway_fallback(source, promise)
promise.catch {
@http_reader.call([
source.to_uri(Import::Http, "localhost:8000")
], "localhost").first
}.catch do
@https_reader.call([
source.to_uri(Import::Https, @public_gateway)
], "localhost").first
end
end
end
class ResolutionSet
def initialize(reader)
@reader = reader
@parents = []
@set = Hash.new { |h, k| h[k] = [] }
end
def register(source)
p = Promise.new
if @parents.include?(source)
p.reject(ImportLoopException.new(source))
else
@set[source] << p
end
p
end
def resolutions
sources, promises = @set.to_a.transpose
[Array(sources), Array(promises)]
end
def reader
lambda do |sources|
if @reader.arity == 2
@reader.call(sources, @parents.last&.origin || "localhost")
else
@reader.call(sources)
end
end
end
def child(parent_source)
dup.tap do |c|
c.instance_eval do
@parents = @parents.dup + [parent_source]
@set = Hash.new { |h, k| h[k] = [] }
end
end
end
end
class Standard
def initialize(
path_reader: ReadPathSources,
http_reader: ReadHttpSources,
https_reader: http_reader
)
@path_resolutions = ResolutionSet.new(path_reader)
@http_resolutions = ResolutionSet.new(http_reader)
@https_resolutions = ResolutionSet.new(https_reader)
@cache = {}
end
def cache_fetch(key, &fallback)
@cache.fetch(key) do
Promise.resolve(nil).then(&fallback).then do |result|
@cache[key] = result
end
end
end
def resolve_path(path_source)
@path_resolutions.register(path_source)
end
def resolve_http(http_source)
http_source.headers.resolve(
resolver: self,
relative_to: Dhall::Import::RelativePath.new
).then do |headers|
@http_resolutions.register(
http_source.with(headers: headers.normalize)
)
end
end
def resolve_https(https_source)
https_source.headers.resolve(
resolver: self,
relative_to: Dhall::Import::RelativePath.new
).then do |headers|
@https_resolutions.register(
https_source.with(headers: headers.normalize)
)
end
end
def finish!
[
@path_resolutions,
@http_resolutions,
@https_resolutions
].each do |rset|
Util.match_result_promises(*rset.resolutions, &rset.reader)
end
freeze
end
def child(parent_source)
dup.tap do |c|
c.instance_eval do
@path_resolutions = @path_resolutions.child(parent_source)
@http_resolutions = @http_resolutions.child(parent_source)
@https_resolutions = @https_resolutions.child(parent_source)
end
end
end
end
class Default < Standard
def initialize(
path_reader: ReadPathSources,
http_reader: ReadHttpSources,
https_reader: http_reader,
ipfs_public_gateway: "cloudflare-ipfs.com"
)
super(
path_reader: ReadPathAndIPFSSources.new(
path_reader: path_reader,
http_reader: http_reader,
https_reader: https_reader,
public_gateway: ipfs_public_gateway
),
http_reader: http_reader,
https_reader: https_reader
)
end
end
class LocalOnly < Standard
def initialize(path_reader: ReadPathSources)
super(
path_reader: path_reader,
http_reader: RejectSources,
https_reader: RejectSources
)
end
end
class None < Default
def initialize
super(
path_reader: RejectSources,
http_reader: RejectSources,
https_reader: RejectSources
)
end
end
end
class ExpressionResolver
@@registry = {}
def self.for(expr)
@@registry.find { |k, _| k === expr }.last.new(expr)
end
def self.register_for(kase)
@@registry[kase] = self
end
def initialize(expr)
@expr = expr
end
def resolve(**kwargs)
Util.promise_all_hash(
@expr.to_h.each_with_object({}) { |(attr, value), h|
h[attr] = ExpressionResolver.for(value).resolve(**kwargs)
}
).then { |h| @expr.with(h) }
end
class ImportResolver < ExpressionResolver
register_for Import
def resolve(resolver:, relative_to:)
Promise.resolve(nil).then do
resolver.cache_fetch(@expr.cache_key(relative_to)) do
resolve_raw(resolver: resolver, relative_to: relative_to)
end
end
end
def resolve_raw(resolver:, relative_to:)
real_path = @expr.real_path(relative_to)
real_path.resolve(resolver).then do |result|
@expr.parse_and_check(result).resolve(
resolver: resolver.child(real_path),
relative_to: real_path
)
end
end
end
class FallbackResolver < ExpressionResolver
register_for Operator::ImportFallback
def resolve(**kwargs)
ExpressionResolver.for(@expr.lhs).resolve(**kwargs).catch do
ExpressionResolver.for(@expr.rhs).resolve(**kwargs)
end
end
end
class ArrayResolver < ExpressionResolver
register_for Util::ArrayOf.new(Expression)
def resolve(**kwargs)
Promise.all(
@expr.map { |e| ExpressionResolver.for(e).resolve(**kwargs) }
)
end
end
class HashResolver < ExpressionResolver
register_for Util::HashOf.new(
ValueSemantics::Anything,
ValueSemantics::Either.new([Expression, nil])
)
def resolve(**kwargs)
Util.promise_all_hash(Hash[@expr.map do |k, v|
[k, ExpressionResolver.for(v).resolve(**kwargs)]
end])
end
end
register_for Expression
class IdentityResolver < ExpressionResolver
register_for Object
def resolve(*)
Promise.resolve(@expr)
end
end
end
class Expression
def resolve(
resolver: Resolvers::Default.new,
relative_to: Import::Path.from_string(Pathname.pwd + "file")
)
p = ExpressionResolver.for(self).resolve(
resolver: resolver,
relative_to: relative_to
)
resolver.finish!
p
end
end
end