Skip to content

Commit

Permalink
more fetchable docs
Browse files Browse the repository at this point in the history
Also, remove Fetchable.Name() because Fetchable.String() was more than
enough.
  • Loading branch information
philandstuff committed Feb 14, 2020
1 parent 95476cd commit 726a652
Show file tree
Hide file tree
Showing 2 changed files with 100 additions and 20 deletions.
2 changes: 1 addition & 1 deletion imports/imports.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func LoadWith(cache DhallCache, e Term, ancestors ...Fetchable) (Term, error) {
expr = TextLit{Suffix: content}
} else {
// dynamicExpr may contain more imports
dynamicExpr, err := parser.Parse(here.Name(), []byte(content))
dynamicExpr, err := parser.Parse(here.String(), []byte(content))
if err != nil {
return nil, err
}
Expand Down
118 changes: 99 additions & 19 deletions term/fetchable.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,27 +11,40 @@ import (
"strings"
)

// An EnvVar is a Fetchable which represents fetching the value of an
// environment variable.
type EnvVar string

// A LocalFile is a Fetchable which represents fetching the content of
// a local file. It is defined to be one of four classes:
// here-relative (ie starts with ./), parent-relative (starts with
// ../), home-relative (starts with ~/), or absolute (starts with /).
type LocalFile string

// A RemoteFile is a Fetchable which represents fetching the content
// of a remote file (over HTTP or HTTPS).
type RemoteFile struct{ url *url.URL }

// Missing is a Fetchable which cannot be Fetched.
type Missing struct{}

// NullOrigin is used in Fetchable.Fetch() to indicate no origin.
const NullOrigin = "null"

var LocationType = UnionType{
var locationType = UnionType{
"Local": Text,
"Remote": Text,
"Environment": Text,
"Missing": nil,
}

// A Fetchable is the target of a Dhall import: a remote file, local
// file, environment variable, or the special value `missing`.
//
// Fetch(origin) is the key method on this interface; it fetches the
// underlying resource, with authority from the given origin.
type Fetchable interface {
Name() string
Origin() string
//Fetch
// fetches the import
// the `origin` parameter should be `scheme://authority` or NullOrigin
Fetch(origin string) (string, error)
ChainOnto(base Fetchable) (Fetchable, error)
String() string
Expand All @@ -43,11 +56,15 @@ var _ Fetchable = LocalFile("")
var _ Fetchable = RemoteFile{}
var _ Fetchable = Missing{}

func (e EnvVar) Name() string { return string(e) }
// Origin returns NullOrigin, since EnvVars do not have an origin.
func (EnvVar) Origin() string { return NullOrigin }
func (e EnvVar) String() string {
return "env:" + string(e)
}

// Fetch reads the environment variable. If origin is not NullOrigin,
// an error is returned, to prevent remote imports from importing
// environment variables.
func (e EnvVar) Fetch(origin string) (string, error) {
if origin != NullOrigin {
return "", errors.New("Can't access environment variable from remote import")
Expand All @@ -58,29 +75,49 @@ func (e EnvVar) Fetch(origin string) (string, error) {
}
return val, nil
}

// ChainOnto returns e.
func (e EnvVar) ChainOnto(base Fetchable) (Fetchable, error) {
return e, nil
}

// AsLocation returns the EnvVar as a Dhall Term. This implements the
// `env:FOO as Location` Dhall feature.
func (e EnvVar) AsLocation() Term {
return Apply(Field{LocationType, "Environment"}, TextLit{Suffix: e.String()})
return Apply(Field{locationType, "Environment"}, TextLit{Suffix: e.String()})
}

func (l LocalFile) Name() string { return string(l) }
// Origin returns NullOrigin, since LocalFiles do not have an origin.
func (LocalFile) Origin() string { return NullOrigin }
func (l LocalFile) String() string {
if l.IsAbs() || l.IsRelativeToHome() || l.IsRelativeToParent() {
return string(l)
} else {
return "./" + string(l)
}
return "./" + string(l)
}

// Fetch reads the local file. If origin is not NullOrigin, an error
// is returned, to prevent remote imports from importing local files.
func (l LocalFile) Fetch(origin string) (string, error) {
if origin != NullOrigin {
return "", fmt.Errorf("Can't get %s from remote import at %s", l, origin)
}
bytes, err := ioutil.ReadFile(string(l))
return string(bytes), err
}

// ChainOnto chains l onto the base Fetchable, according to the Dhall
// definition of import chaining:
// https://github.com/dhall-lang/dhall-lang/blob/master/standard/imports.md#chaining-imports
//
// For here- or parent-relative LocalFiles, they chain onto
// RemoteFiles using the URL reference resolution algorithm; they
// chain onto LocalFiles using filesystem path joining; they chain
// onto Missing or EnvVar by just returning the LocalFile unmodified.
//
// For home-relative or absolute LocalFiles, chaining them onto a
// RemoteFile is an error; all other cases return the LocalFile
// unmodified.
func (l LocalFile) ChainOnto(base Fetchable) (Fetchable, error) {
switch r := base.(type) {
case LocalFile:
Expand All @@ -102,9 +139,14 @@ func (l LocalFile) ChainOnto(base Fetchable) (Fetchable, error) {
}
}

func (l LocalFile) IsAbs() bool { return path.IsAbs(string(l)) }
// IsAbs returns true if the LocalFile is an absolute path.
func (l LocalFile) IsAbs() bool { return path.IsAbs(string(l)) }

// IsRelativeToParent returns true if the LocalFile starts with "../"
func (l LocalFile) IsRelativeToParent() bool { return strings.HasPrefix(string(l), "..") }
func (l LocalFile) IsRelativeToHome() bool { return string(l)[0] == '~' }

// IsRelativeToHome returns true if the LocalFile starts with "~/"
func (l LocalFile) IsRelativeToHome() bool { return string(l)[0] == '~' }

//asRelativeRef converts a local path to a relative reference
func (l LocalFile) asRelativeRef() *url.URL {
Expand All @@ -128,25 +170,38 @@ func (l LocalFile) asRelativeRef() *url.URL {
return u
}

// PathComponents returns a slice of strings, one for each component
// of the given path. It excludes any leading ".", ".." or "~".
func (l LocalFile) PathComponents() []string {
if l.IsAbs() || l.IsRelativeToHome() || l.IsRelativeToParent() {
return strings.Split(string(l), "/")[1:]
} else {
return strings.Split(string(l), "/")
}
return strings.Split(string(l), "/")
}

// AsLocation returns the LocalFile as a Dhall Term. This implements the
// `./file as Location` Dhall feature.
func (l LocalFile) AsLocation() Term {
return Apply(Field{LocationType, "Local"}, TextLit{Suffix: l.String()})
return Apply(Field{locationType, "Local"}, TextLit{Suffix: l.String()})
}

// NewRemoteFile constructs a RemoteFile from a *url.URL.
func NewRemoteFile(u *url.URL) RemoteFile {
return RemoteFile{url: u}
}

var client http.Client

func (r RemoteFile) Name() string { return r.url.String() }
// Origin returns the scheme and authority of the underlying URL of a
// RemoteFile. For example, the Origin of
// "https://example.com/foo/bar" is "https://example.com".
func (r RemoteFile) Origin() string { return fmt.Sprintf("%s://%s", r.url.Scheme, r.Authority()) }
func (r RemoteFile) String() string { return fmt.Sprintf("%v", r.url) }

// Fetch makes an HTTP request to fetch the RemoteFile. If origin is
// neither NullOrigin nor the same origin as this RemoteFile, this is
// considered a cross-origin request and so appropriate CORS checks
// are made; if these fail, an error is returned with no content.
func (r RemoteFile) Fetch(origin string) (string, error) {
req, err := http.NewRequest("GET", r.url.String(), nil)
if err != nil {
Expand Down Expand Up @@ -175,41 +230,66 @@ func (r RemoteFile) Fetch(origin string) (string, error) {
bodyBytes, err := ioutil.ReadAll(resp.Body)
return string(bodyBytes), err
}

// ChainOnto returns the RemoteFile unmodified.
func (r RemoteFile) ChainOnto(base Fetchable) (Fetchable, error) {
return r, nil
}

// IsPlainHTTP returns true if this is an "http://" URL, and false if
// it an "https://" URL.
func (r RemoteFile) IsPlainHTTP() bool { return r.url.Scheme == "http" }

// Authority returns the authority of the URL; that is, the bit
// between the first "//" and the next "/", which includes optional
// userinfo, remote host, and optional port number.
func (r RemoteFile) Authority() string {
if r.url.User != nil {
return fmt.Sprintf("%s@%s", r.url.User.String(), r.url.Host)
}
return r.url.Host
}

// PathComponents returns a slice of strings, one for each path
// component of the given URL.
func (r RemoteFile) PathComponents() []string {
if r.url.Path == "" || r.url.Path == "/" {
return []string{""}
}
return strings.Split(r.url.EscapedPath()[1:], "/")
}

// Query returns the query string, or nil if no query string is
// present.
func (r RemoteFile) Query() *string {
if r.url.RawQuery == "" && !r.url.ForceQuery {
return nil
}
return &r.url.RawQuery
}

// AsLocation returns the RemoteFile as a Dhall Term. This implements the
// `https://example.com/foo/bar as Location` feature.
func (r RemoteFile) AsLocation() Term {
return Apply(Field{LocationType, "Remote"}, TextLit{Suffix: r.String()})
return Apply(Field{locationType, "Remote"}, TextLit{Suffix: r.String()})
}

func (Missing) Name() string { return "" }
// Origin returns NullOrigin, since Missing does not have an origin.
func (Missing) Origin() string { return NullOrigin }
func (Missing) String() string { return "missing" }

// Fetch always returns an error, because Missing cannot be fetched.
func (Missing) Fetch(origin string) (string, error) {
return "", errors.New("Cannot resolve missing import")
}

// ChainOnto returns a Missing.
func (Missing) ChainOnto(base Fetchable) (Fetchable, error) {
return Missing{}, nil
}

// AsLocation returns Missing as a Dhall Term. This implements the
// `missing as Location` feature.
func (Missing) AsLocation() Term {
return Field{LocationType, "Missing"}
return Field{locationType, "Missing"}
}

0 comments on commit 726a652

Please sign in to comment.