# 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