Skip to content

Commit

Permalink
Add backwards compatible changes to ParsePath for extra behaviors (#154)
Browse files Browse the repository at this point in the history
* Add backwards compatible changes to ParsePath for extra behaviors

* No need to export these

* overzealous search/replace

* remove test code

* handle trim in the ErrNotAURL case
  • Loading branch information
sgmiller authored Jan 16, 2025
1 parent 48acf69 commit 2d0aa30
Show file tree
Hide file tree
Showing 2 changed files with 102 additions and 15 deletions.
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

0 comments on commit 2d0aa30

Please sign in to comment.