From 57fd07886bee07fdc92dfe681d64015084b895b4 Mon Sep 17 00:00:00 2001 From: Matt Moore Date: Wed, 1 May 2019 18:40:34 -0700 Subject: [PATCH] Add support for a structured URL type. (#400) * Add support for a structured URL type. This type can be used to accept `url: http://asdf.com` where in code we get a `url.URL` to interact with. * Propagate errors parsing, cast pointer instead of copying. --- apis/url.go | 73 ++++++++++++ apis/url_test.go | 204 ++++++++++++++++++++++++++++++++++ apis/zz_generated.deepcopy.go | 25 +++++ 3 files changed, 302 insertions(+) create mode 100644 apis/url.go create mode 100644 apis/url_test.go diff --git a/apis/url.go b/apis/url.go new file mode 100644 index 000000000..c0402016f --- /dev/null +++ b/apis/url.go @@ -0,0 +1,73 @@ +/* +Copyright 2019 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package apis + +import ( + "encoding/json" + "fmt" + "net/url" +) + +// URL is an alias of url.URL. +// It has custom json marshal methods that enable it to be used in K8s CRDs +// such that the CRD resource will have the URL but operator code can can work with url.URL struct +type URL url.URL + +// ParseURL attempts to parse the given string as a URL. +func ParseURL(u string) (*URL, error) { + if u == "" { + return nil, nil + } + pu, err := url.Parse(u) + if err != nil { + return nil, err + } + return (*URL)(pu), nil +} + +// MarshalJSON implements a custom json marshal method used when this type is +// marshaled using json.Marshal. +// json.Marshaler impl +func (u URL) MarshalJSON() ([]byte, error) { + b := fmt.Sprintf("%q", u.String()) + return []byte(b), nil +} + +// UnmarshalJSON implements the json unmarshal method used when this type is +// unmarsheled using json.Unmarshal. +// json.Unmarshaler impl +func (u *URL) UnmarshalJSON(b []byte) error { + var ref string + if err := json.Unmarshal(b, &ref); err != nil { + return err + } + r, err := ParseURL(ref) + if err != nil { + return err + } + *u = *r + return nil +} + +// String returns the full string representation of the URL. +func (u *URL) String() string { + if u == nil { + return "" + } + uu := url.URL(*u) + return uu.String() +} diff --git a/apis/url_test.go b/apis/url_test.go new file mode 100644 index 000000000..b51f66f80 --- /dev/null +++ b/apis/url_test.go @@ -0,0 +1,204 @@ +/* +Copyright 2019 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package apis + +import ( + "net/url" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestParseURL(t *testing.T) { + testCases := map[string]struct { + t string + want *URL + wantErr bool + }{ + "empty": { + want: nil, + }, + "empty string": { + t: "", + want: nil, + }, + "invalid format": { + t: "💩://error", + want: nil, + wantErr: true, + }, + "relative": { + t: "/path/to/something", + want: func() *URL { + uu, _ := url.Parse("/path/to/something") + u := URL(*uu) + return &u + }(), + }, + "url": { + t: "http://path/to/something", + want: func() *URL { + uu, _ := url.Parse("http://path/to/something") + u := URL(*uu) + return &u + }(), + }, + } + for n, tc := range testCases { + t.Run(n, func(t *testing.T) { + + got, err := ParseURL(tc.t) + if err != nil { + if !tc.wantErr { + t.Fatalf("ParseURL() = %v", err) + } + return + } else if tc.wantErr { + t.Fatalf("ParseURL() = %v, wanted error", got) + } + + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("unexpected object (-want, +got) = %v", diff) + } + }) + } +} + +func TestJsonMarshalURL(t *testing.T) { + testCases := map[string]struct { + t string + want []byte + }{ + "empty": {}, + "empty string": { + t: "", + }, + "invalid url": { + t: "not a url", + want: []byte(`"not%20a%20url"`), + }, + "relative format": { + t: "/path/to/something", + want: []byte(`"/path/to/something"`), + }, + } + for n, tc := range testCases { + t.Run(n, func(t *testing.T) { + + var got []byte + tt, err := ParseURL(tc.t) + if err != nil { + t.Fatalf("ParseURL() = %v", err) + } + if tt != nil { + got, _ = tt.MarshalJSON() + } + + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Logf("got: %s", string(got)) + t.Errorf("unexpected object (-want, +got) = %v", diff) + } + }) + } +} + +func TestJsonUnmarshalURL(t *testing.T) { + testCases := map[string]struct { + b []byte + want *URL + wantErr string + }{ + "empty": { + wantErr: "unexpected end of JSON input", + }, + "invalid format": { + b: []byte("%"), + wantErr: "invalid character '%' looking for beginning of value", + }, + "relative": { + b: []byte(`"/path/to/something"`), + want: func() *URL { + uu, _ := url.Parse("/path/to/something") + u := URL(*uu) + return &u + }(), + }, + "url": { + b: []byte(`"http://path/to/something"`), + want: func() *URL { + uu, _ := url.Parse("http://path/to/something") + u := URL(*uu) + return &u + }(), + }, + } + for n, tc := range testCases { + t.Run(n, func(t *testing.T) { + + got := &URL{} + err := got.UnmarshalJSON(tc.b) + + if tc.wantErr != "" || err != nil { + var gotErr string + if err != nil { + gotErr = err.Error() + } + if diff := cmp.Diff(tc.wantErr, gotErr); diff != "" { + t.Errorf("unexpected error (-want, +got) = %v", diff) + } + return + } + + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("unexpected object (-want, +got) = %v", diff) + } + }) + } +} + +func TestURLString(t *testing.T) { + testCases := map[string]struct { + t string + want string + }{ + "empty": { + want: "", + }, + "relative": { + t: "/path/to/something", + want: "/path/to/something", + }, + "url": { + t: "http://path/to/something", + want: "http://path/to/something", + }, + } + for n, tc := range testCases { + t.Run(n, func(t *testing.T) { + + tt, err := ParseURL(tc.t) + if err != nil { + t.Fatalf("ParseURL() = %v", err) + } + got := tt.String() + + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Logf("got: %s", string(got)) + t.Errorf("unexpected string (-want, +got) = %v", diff) + } + }) + } +} diff --git a/apis/zz_generated.deepcopy.go b/apis/zz_generated.deepcopy.go index f32afcd0f..be670d4a8 100644 --- a/apis/zz_generated.deepcopy.go +++ b/apis/zz_generated.deepcopy.go @@ -20,6 +20,10 @@ limitations under the License. package apis +import ( + url "net/url" +) + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Condition) DeepCopyInto(out *Condition) { *out = *in @@ -87,6 +91,27 @@ func (in *FieldError) DeepCopy() *FieldError { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *URL) DeepCopyInto(out *URL) { + *out = *in + if in.User != nil { + in, out := &in.User, &out.User + *out = new(url.Userinfo) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new URL. +func (in *URL) DeepCopy() *URL { + if in == nil { + return nil + } + out := new(URL) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *VolatileTime) DeepCopyInto(out *VolatileTime) { *out = *in