Skip to content

Commit 2d0aa30

Browse files
authored
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
1 parent 48acf69 commit 2d0aa30

File tree

2 files changed

+102
-15
lines changed

2 files changed

+102
-15
lines changed

parseutil/parsepath.go

Lines changed: 67 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,15 @@ var (
1717
ErrNotParsed = errors.New("not a parsed value")
1818
)
1919

20+
type options struct {
21+
errorOnMissingEnv bool
22+
noTrimSpaces bool
23+
}
24+
25+
type option func() optionFunc
26+
27+
type optionFunc func(*options)
28+
2029
// ParsePath parses a URL with schemes file://, env://, or any other. Depending
2130
// on the scheme it will return specific types of data:
2231
//
@@ -34,35 +43,82 @@ var (
3443
// step that errored or something else (such as a file not found). This is
3544
// useful to attempt to read a non-URL string from some resource, but where the
3645
// original input may simply be a valid string of that type.
37-
func ParsePath(path string) (string, error) {
38-
return parsePath(path, false)
46+
func ParsePath(path string, options ...option) (string, error) {
47+
return parsePath(path, false, options)
3948
}
4049

4150
// MustParsePath behaves like ParsePath but will return ErrNotAUrl if the value
4251
// is not a URL with a scheme that can be parsed by this function.
43-
func MustParsePath(path string) (string, error) {
44-
return parsePath(path, true)
52+
func MustParsePath(path string, options ...option) (string, error) {
53+
return parsePath(path, true, options)
4554
}
4655

47-
func parsePath(path string, mustParse bool) (string, error) {
48-
path = strings.TrimSpace(path)
49-
parsed, err := url.Parse(path)
56+
func parsePath(path string, mustParse bool, passedOptions []option) (string, error) {
57+
var opts options
58+
for _, o := range passedOptions {
59+
of := o()
60+
of(&opts)
61+
}
62+
63+
trimmedPath := strings.TrimSpace(path)
64+
parsed, err := url.Parse(trimmedPath)
5065
if err != nil {
51-
return path, fmt.Errorf("error parsing url (%q): %w", err.Error(), ErrNotAUrl)
66+
err = fmt.Errorf("error parsing url (%q): %w", err.Error(), ErrNotAUrl)
67+
if opts.noTrimSpaces {
68+
return path, err
69+
}
70+
return trimmedPath, err
5271
}
5372
switch parsed.Scheme {
5473
case "file":
55-
contents, err := ioutil.ReadFile(strings.TrimPrefix(path, "file://"))
74+
contents, err := ioutil.ReadFile(strings.TrimPrefix(trimmedPath, "file://"))
5675
if err != nil {
57-
return path, fmt.Errorf("error reading file at %s: %w", path, err)
76+
return trimmedPath, fmt.Errorf("error reading file at %s: %w", trimmedPath, err)
77+
}
78+
if opts.noTrimSpaces {
79+
return string(contents), nil
5880
}
5981
return strings.TrimSpace(string(contents)), nil
6082
case "env":
61-
return strings.TrimSpace(os.Getenv(strings.TrimPrefix(path, "env://"))), nil
83+
envKey := strings.TrimPrefix(trimmedPath, "env://")
84+
envVal, ok := os.LookupEnv(envKey)
85+
if opts.errorOnMissingEnv && !ok {
86+
return "", fmt.Errorf("environment variable %s unset", envKey)
87+
}
88+
if opts.noTrimSpaces {
89+
return envVal, nil
90+
}
91+
return strings.TrimSpace(envVal), nil
92+
case "string":
93+
// 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,
94+
// e.g. "string://env://foo", in order to get the value "env://foo"
95+
val := strings.TrimPrefix(trimmedPath, "string://")
96+
if opts.noTrimSpaces {
97+
return val, nil
98+
}
99+
return strings.TrimSpace(val), nil
62100
default:
63101
if mustParse {
64102
return "", ErrNotParsed
65103
}
66104
return path, nil
67105
}
68106
}
107+
108+
// When true, values returned from ParsePath won't have leading/trailing spaces trimmed.
109+
func WithNoTrimSpaces(noTrim bool) option {
110+
return func() optionFunc {
111+
return optionFunc(func(o *options) {
112+
o.noTrimSpaces = noTrim
113+
})
114+
}
115+
}
116+
117+
// When true, if an environment variable is unset, an error will be returned rather than the empty string.
118+
func WithErrorOnMissingEnv(errorOnMissingEnv bool) option {
119+
return func() optionFunc {
120+
return optionFunc(func(o *options) {
121+
o.errorOnMissingEnv = errorOnMissingEnv
122+
})
123+
}
124+
}

parseutil/parsepath_test.go

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@ func TestParsePath(t *testing.T) {
1818

1919
file, err := os.CreateTemp("", "")
2020
require.NoError(t, err)
21-
_, err = file.WriteString("foo")
21+
_, err = file.WriteString(" foo ")
2222
require.NoError(t, err)
2323
require.NoError(t, file.Close())
2424
defer os.Remove(file.Name())
2525

26-
require.NoError(t, os.Setenv("PATHTEST", "bar"))
26+
require.NoError(t, os.Setenv("PATHTEST", " bar "))
2727

2828
cases := []struct {
2929
name string
@@ -33,12 +33,19 @@ func TestParsePath(t *testing.T) {
3333
must bool
3434
notParsed bool
3535
expErrorContains string
36+
options []option
3637
}{
3738
{
3839
name: "file",
3940
inPath: fmt.Sprintf("file://%s", file.Name()),
4041
outStr: "foo",
4142
},
43+
{
44+
name: "file-untrimmed",
45+
inPath: fmt.Sprintf("file://%s", file.Name()),
46+
outStr: " foo ",
47+
options: []option{WithNoTrimSpaces(true)},
48+
},
4249
{
4350
name: "file-mustparse",
4451
inPath: fmt.Sprintf("file://%s", file.Name()),
@@ -50,24 +57,48 @@ func TestParsePath(t *testing.T) {
5057
inPath: "env://PATHTEST",
5158
outStr: "bar",
5259
},
60+
{
61+
name: "env-untrimmed",
62+
inPath: "env://PATHTEST",
63+
outStr: " bar ",
64+
options: []option{WithNoTrimSpaces(true)},
65+
},
5366
{
5467
name: "env-mustparse",
5568
inPath: "env://PATHTEST",
5669
outStr: "bar",
5770
must: true,
5871
},
72+
{
73+
name: "env-error-missing",
74+
inPath: "env://PATHTEST2",
75+
outStr: "bar",
76+
expErrorContains: "environment variable PATHTEST2 unset",
77+
options: []option{WithErrorOnMissingEnv(true)},
78+
},
5979
{
6080
name: "plain",
6181
inPath: "zipzap",
6282
outStr: "zipzap",
6383
},
84+
{
85+
name: "plan-untrimmed",
86+
inPath: " zipzap ",
87+
outStr: " zipzap ",
88+
options: []option{WithNoTrimSpaces(true)},
89+
},
6490
{
6591
name: "plain-mustparse",
6692
inPath: "zipzap",
6793
outStr: "zipzap",
6894
must: true,
6995
notParsed: true,
7096
},
97+
{
98+
name: "escaped",
99+
inPath: "string://env://foo",
100+
outStr: "env://foo",
101+
},
71102
{
72103
name: "no file",
73104
inPath: "file:///dev/nullface",
@@ -88,9 +119,9 @@ func TestParsePath(t *testing.T) {
88119
var err error
89120
switch tt.must {
90121
case false:
91-
out, err = ParsePath(tt.inPath)
122+
out, err = ParsePath(tt.inPath, tt.options...)
92123
default:
93-
out, err = MustParsePath(tt.inPath)
124+
out, err = MustParsePath(tt.inPath, tt.options...)
94125
}
95126
if tt.expErrorContains != "" {
96127
require.Error(err)

0 commit comments

Comments
 (0)