From 2d0aa30c0d1559dcbaffb552bd47079700e4408f Mon Sep 17 00:00:00 2001 From: Scott Miller Date: Wed, 15 Jan 2025 16:53:40 -0800 Subject: [PATCH] Add backwards compatible changes to ParsePath for extra behaviors (#154) * 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 --- parseutil/parsepath.go | 78 +++++++++++++++++++++++++++++++------ parseutil/parsepath_test.go | 39 +++++++++++++++++-- 2 files changed, 102 insertions(+), 15 deletions(-) diff --git a/parseutil/parsepath.go b/parseutil/parsepath.go index ec81236..46ccbe7 100644 --- a/parseutil/parsepath.go +++ b/parseutil/parsepath.go @@ -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: // @@ -34,31 +43,60 @@ 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 @@ -66,3 +104,21 @@ func parsePath(path string, mustParse bool) (string, error) { 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 + }) + } +} diff --git a/parseutil/parsepath_test.go b/parseutil/parsepath_test.go index baa9935..df4e0c0 100644 --- a/parseutil/parsepath_test.go +++ b/parseutil/parsepath_test.go @@ -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 @@ -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()), @@ -50,17 +57,36 @@ 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", @@ -68,6 +94,11 @@ func TestParsePath(t *testing.T) { must: true, notParsed: true, }, + { + name: "escaped", + inPath: "string://env://foo", + outStr: "env://foo", + }, { name: "no file", inPath: "file:///dev/nullface", @@ -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)