Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add backwards compatible changes to ParsePath for extra behaviors #154

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 67 additions & 11 deletions parseutil/parsepath.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@ var (
ErrNotParsed = errors.New("not a parsed value")
)

type options struct {
errorOnMissingEnv bool
noTrimSpaces bool
}

type option func() optionFunc

type optionFunc func(*options)

// ParsePath parses a URL with schemes file://, env://, or any other. Depending
// on the scheme it will return specific types of data:
//
Expand All @@ -34,35 +43,82 @@ var (
// step that errored or something else (such as a file not found). This is
// useful to attempt to read a non-URL string from some resource, but where the
// original input may simply be a valid string of that type.
func ParsePath(path string) (string, error) {
return parsePath(path, false)
func ParsePath(path string, options ...option) (string, error) {
return parsePath(path, false, options)
}

// MustParsePath behaves like ParsePath but will return ErrNotAUrl if the value
// is not a URL with a scheme that can be parsed by this function.
func MustParsePath(path string) (string, error) {
return parsePath(path, true)
func MustParsePath(path string, options ...option) (string, error) {
return parsePath(path, true, options)
}

func parsePath(path string, mustParse bool) (string, error) {
path = strings.TrimSpace(path)
parsed, err := url.Parse(path)
func parsePath(path string, mustParse bool, passedOptions []option) (string, error) {
var opts options
for _, o := range passedOptions {
of := o()
of(&opts)
}

trimmedPath := strings.TrimSpace(path)
parsed, err := url.Parse(trimmedPath)
if err != nil {
return path, fmt.Errorf("error parsing url (%q): %w", err.Error(), ErrNotAUrl)
err = fmt.Errorf("error parsing url (%q): %w", err.Error(), ErrNotAUrl)
if opts.noTrimSpaces {
return path, err
}
return trimmedPath, err
}
switch parsed.Scheme {
case "file":
contents, err := ioutil.ReadFile(strings.TrimPrefix(path, "file://"))
contents, err := ioutil.ReadFile(strings.TrimPrefix(trimmedPath, "file://"))
if err != nil {
return path, fmt.Errorf("error reading file at %s: %w", path, err)
return trimmedPath, fmt.Errorf("error reading file at %s: %w", trimmedPath, err)
}
if opts.noTrimSpaces {
return string(contents), nil
}
return strings.TrimSpace(string(contents)), nil
case "env":
return strings.TrimSpace(os.Getenv(strings.TrimPrefix(path, "env://"))), nil
envKey := strings.TrimPrefix(trimmedPath, "env://")
envVal, ok := os.LookupEnv(envKey)
if opts.errorOnMissingEnv && !ok {
return "", fmt.Errorf("environment variable %s unset", envKey)
}
if opts.noTrimSpaces {
return envVal, nil
}
return strings.TrimSpace(envVal), nil
case "string":
// Meant if there is a need to provide a string literal that is prefixed by one of these URL schemes but want to "escape" it,
// e.g. "string://env://foo", in order to get the value "env://foo"
val := strings.TrimPrefix(trimmedPath, "string://")
if opts.noTrimSpaces {
return val, nil
}
return strings.TrimSpace(val), nil
default:
if mustParse {
return "", ErrNotParsed
}
return path, nil
}
}

// When true, values returned from ParsePath won't have leading/trailing spaces trimmed.
func WithNoTrimSpaces(noTrim bool) option {
return func() optionFunc {
return optionFunc(func(o *options) {
o.noTrimSpaces = noTrim
})
}
}

// When true, if an environment variable is unset, an error will be returned rather than the empty string.
func WithErrorOnMissingEnv(errorOnMissingEnv bool) option {
return func() optionFunc {
return optionFunc(func(o *options) {
o.errorOnMissingEnv = errorOnMissingEnv
})
}
}
39 changes: 35 additions & 4 deletions parseutil/parsepath_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ func TestParsePath(t *testing.T) {

file, err := os.CreateTemp("", "")
require.NoError(t, err)
_, err = file.WriteString("foo")
_, err = file.WriteString(" foo ")
require.NoError(t, err)
require.NoError(t, file.Close())
defer os.Remove(file.Name())

require.NoError(t, os.Setenv("PATHTEST", "bar"))
require.NoError(t, os.Setenv("PATHTEST", " bar "))

cases := []struct {
name string
Expand All @@ -33,12 +33,19 @@ func TestParsePath(t *testing.T) {
must bool
notParsed bool
expErrorContains string
options []option
}{
{
name: "file",
inPath: fmt.Sprintf("file://%s", file.Name()),
outStr: "foo",
},
{
name: "file-untrimmed",
inPath: fmt.Sprintf("file://%s", file.Name()),
outStr: " foo ",
options: []option{WithNoTrimSpaces(true)},
},
{
name: "file-mustparse",
inPath: fmt.Sprintf("file://%s", file.Name()),
Expand All @@ -50,24 +57,48 @@ func TestParsePath(t *testing.T) {
inPath: "env://PATHTEST",
outStr: "bar",
},
{
name: "env-untrimmed",
inPath: "env://PATHTEST",
outStr: " bar ",
options: []option{WithNoTrimSpaces(true)},
},
{
name: "env-mustparse",
inPath: "env://PATHTEST",
outStr: "bar",
must: true,
},
{
name: "env-error-missing",
inPath: "env://PATHTEST2",
outStr: "bar",
expErrorContains: "environment variable PATHTEST2 unset",
options: []option{WithErrorOnMissingEnv(true)},
},
{
name: "plain",
inPath: "zipzap",
outStr: "zipzap",
},
{
name: "plan-untrimmed",
inPath: " zipzap ",
outStr: " zipzap ",
options: []option{WithNoTrimSpaces(true)},
},
{
name: "plain-mustparse",
inPath: "zipzap",
outStr: "zipzap",
must: true,
notParsed: true,
},
{
name: "escaped",
inPath: "string://env://foo",
outStr: "env://foo",
},
{
name: "no file",
inPath: "file:///dev/nullface",
Expand All @@ -88,9 +119,9 @@ func TestParsePath(t *testing.T) {
var err error
switch tt.must {
case false:
out, err = ParsePath(tt.inPath)
out, err = ParsePath(tt.inPath, tt.options...)
default:
out, err = MustParsePath(tt.inPath)
out, err = MustParsePath(tt.inPath, tt.options...)
}
if tt.expErrorContains != "" {
require.Error(err)
Expand Down
Loading