Skip to content

Commit e799b57

Browse files
authored
Merge pull request #40 from PDOK/wr/validation
Wr/validation
2 parents 33a645b + 4ef8fa3 commit e799b57

File tree

18 files changed

+262
-144
lines changed

18 files changed

+262
-144
lines changed

api/v2beta1/atom_conversion.go

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ SOFTWARE.
2525
package v2beta1
2626

2727
import (
28-
"fmt"
2928
"log"
3029
"strconv"
3130
"time"
@@ -60,9 +59,14 @@ func (a *Atom) ToV3(dst *pdoknlv3.Atom) error {
6059
}
6160
}
6261

62+
baseURL, err := createBaseURL(pdoknlv3.GetBaseURL(), a.Spec.General)
63+
if err != nil {
64+
return err
65+
}
66+
6367
// Service
6468
dst.Spec.Service = pdoknlv3.Service{
65-
BaseURL: createBaseURL(pdoknlv3.GetBaseURL(), a.Spec.General),
69+
BaseURL: *baseURL,
6670
Lang: "nl",
6771
Title: a.Spec.Service.Title,
6872
Subtitle: a.Spec.Service.Subtitle,
@@ -91,10 +95,14 @@ func (a *Atom) ToV3(dst *pdoknlv3.Atom) error {
9195

9296
// Map the links
9397
for _, srcLink := range srcDataset.Links {
98+
href, err := smoothoperatormodel.ParseURL(srcLink.URI)
99+
if err != nil {
100+
return err
101+
}
94102
dstLink := pdoknlv3.Link{
95103
Rel: "describedby",
96104
Title: &srcLink.Type,
97-
Href: srcLink.URI,
105+
Href: smoothoperatormodel.URL{URL: href},
98106
}
99107
if srcLink.ContentType != nil {
100108
dstLink.Type = *srcLink.ContentType
@@ -108,11 +116,17 @@ func (a *Atom) ToV3(dst *pdoknlv3.Atom) error {
108116

109117
// Map the entries
110118
for _, srcDownload := range srcDataset.Downloads {
119+
120+
uri, err := smoothoperatormodel.ParseURL(srcDownload.Srs.URI)
121+
if err != nil {
122+
return err
123+
}
124+
111125
dstEntry := pdoknlv3.Entry{
112126
TechnicalName: srcDownload.Name,
113127
Content: srcDownload.Content,
114128
SRS: pdoknlv3.SRS{
115-
URI: srcDownload.Srs.URI,
129+
URI: smoothoperatormodel.URL{URL: uri},
116130
Name: srcDownload.Srs.Code,
117131
},
118132
Polygon: pdoknlv3.Polygon{
@@ -234,7 +248,7 @@ func (a *Atom) ConvertFrom(srcRaw conversion.Hub) error {
234248
for _, srcLink := range srcDatasetFeed.Links {
235249
dstDataset.Links = append(dstDataset.Links, OtherLink{
236250
Type: smoothutil.PointerVal(srcLink.Title, ""),
237-
URI: srcLink.Href,
251+
URI: srcLink.Href.String(),
238252
ContentType: &srcLink.Type,
239253
Language: srcLink.Hreflang,
240254
})
@@ -263,7 +277,7 @@ func (a *Atom) ConvertFrom(srcRaw conversion.Hub) error {
263277
dstDownload.Updated = &updatedString
264278

265279
dstDownload.Srs = Srs{
266-
URI: srcEntry.SRS.URI,
280+
URI: srcEntry.SRS.URI.String(),
267281
Code: srcEntry.SRS.Name,
268282
}
269283

@@ -307,19 +321,22 @@ func (a *Atom) ConvertFrom(srcRaw conversion.Hub) error {
307321
return nil
308322
}
309323

310-
func createBaseURL(host string, general General) (baseURL string) {
311-
atomURI := fmt.Sprintf("%s/%s", general.DatasetOwner, general.Dataset)
324+
func createBaseURL(host string, general General) (*smoothoperatormodel.URL, error) {
325+
baseURL, err := smoothoperatormodel.ParseURL(host)
326+
if err != nil {
327+
return nil, err
328+
}
329+
baseURL = baseURL.JoinPath(general.DatasetOwner, general.Dataset)
312330
if general.Theme != nil {
313-
atomURI += "/" + *general.Theme
331+
baseURL = baseURL.JoinPath(*general.Theme)
314332
}
315-
atomURI += "/atom"
333+
baseURL = baseURL.JoinPath("atom")
316334

317335
if general.ServiceVersion != nil {
318-
atomURI += "/" + *general.ServiceVersion
336+
baseURL = baseURL.JoinPath(*general.ServiceVersion)
319337
}
320338

321-
baseURL = fmt.Sprintf("%s/%s/", host, atomURI)
322-
return
339+
return &smoothoperatormodel.URL{URL: baseURL}, nil
323340
}
324341

325342
func GetInt32Pointer(value int32) *int32 {

api/v2beta1/atom_conversion_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ func TestAtom_ConvertTo(t *testing.T) {
1414
convertFromAtom := getTestAtomV2()
1515
convertToAtom := &pdoknlv3.Atom{}
1616
dstRaw := conversion.Hub(convertToAtom)
17+
pdoknlv3.SetBaseURL("https://test.com/test")
1718
err := convertFromAtom.ConvertTo(dstRaw)
1819
if err != nil {
1920
t.Errorf("ConvertTo() error = %v", err)

api/v3/atom_types.go

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ SOFTWARE.
2525
package v3
2626

2727
import (
28-
"net/url"
2928
"strings"
3029

3130
smoothoperatormodel "github.com/pdok/smooth-operator/model"
@@ -47,16 +46,15 @@ type AtomSpec struct {
4746
// Service defines the service configuration for the Atom feed
4847
type Service struct {
4948
// BaseURL of the Atom service. Will be suffixed with index.xml for the index.
50-
// +kubebuilder:validation:Pattern:="https?://.*"
51-
BaseURL string `json:"baseUrl"` // TODO use URL type
49+
BaseURL smoothoperatormodel.URL `json:"baseUrl"`
5250

5351
// Language of the service
5452
// +kubebuilder:default:="nl"
5553
// +kubebuilder:validation:MinLength:=2
56-
Lang string `json:"lang"`
54+
Lang string `json:"lang,omitempty"`
5755

5856
// Optional link to a stylesheet used in pages generated by the service.
59-
Stylesheet *string `json:"stylesheet,omitempty"`
57+
Stylesheet *smoothoperatormodel.URL `json:"stylesheet,omitempty"`
6058

6159
// Title of the service
6260
// +kubebuilder:validation:MinLength:=1
@@ -88,8 +86,7 @@ type Service struct {
8886
// Link represents a link in the service or dataset feed
8987
type Link struct {
9088
// Actual href of the link
91-
// +kubebuilder:validation:Pattern:="https?://.*"
92-
Href string `json:"href"`
89+
Href smoothoperatormodel.URL `json:"href"`
9390

9491
// Relation (type) of the link, for example: describedby, self or alternate
9592
// +kubebuilder:validation:MinLength:=1
@@ -136,6 +133,7 @@ type DatasetFeed struct {
136133
// +kubebuilder:validation:Pattern:=`^[0-9a-zA-Z]{8}\-[0-9a-zA-Z]{4}\-[0-9a-zA-Z]{4}\-[0-9a-zA-Z]{4}\-[0-9a-zA-Z]{12}$`
137134
SpatialDatasetIdentifierCode *string `json:"spatialDatasetIdentifierCode,omitempty"`
138135

136+
// TODO user URL type
139137
// SpatialDatasetIdentifierNamespace
140138
// +kubebuilder:validation:Format:=uri
141139
SpatialDatasetIdentifierNamespace *string `json:"spatialDatasetIdentifierNamespace,omitempty"`
@@ -153,6 +151,7 @@ type MetadataLink struct {
153151

154152
// Metadata templates to use
155153
// +kubebuilder:validation:MinItems:=1
154+
// +kubebuilder:validation:items:Enum:=csw;opensearch;html
156155
Templates []string `json:"templates"`
157156
}
158157

@@ -189,7 +188,7 @@ type Entry struct {
189188
// DownloadLink specifies download information for entries
190189
type DownloadLink struct {
191190
// URL to the data
192-
// +kubebuilder:validation:MinLength:=1
191+
// +kubebuilder:validation:Pattern:=^[^\/]+\/.+\/.+
193192
Data string `json:"data"`
194193

195194
// Optional relation if the link, for example: describedby, self or alternate
@@ -212,8 +211,7 @@ type Polygon struct {
212211
// SRS describes the Spatial Reference System for an entry
213212
type SRS struct {
214213
// URI of the SRS
215-
// +kubebuilder:validation:Pattern:="https?://.*"
216-
URI string `json:"uri"`
214+
URI smoothoperatormodel.URL `json:"uri"`
217215

218216
// Name of the SRS
219217
// +kubebuilder:validation:MinLength:=1
@@ -265,14 +263,9 @@ func GetBlobEndpoint() string {
265263
return blobEndpoint
266264
}
267265

268-
func (r *Atom) GetBaseURL() url.URL {
269-
baseURL, _ := url.Parse(r.Spec.Service.BaseURL)
270-
return *baseURL
271-
}
272-
273266
//nolint:revive
274267
func (r *Atom) GetBaseUrl() string {
275-
return r.Spec.Service.BaseURL
268+
return r.Spec.Service.BaseURL.String()
276269
}
277270

278271
func (r *Atom) GetDownloadLinks() (downloadLinks []DownloadLink) {

api/v3/atom_validation.go

Lines changed: 82 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@ package v3
22

33
import (
44
"fmt"
5+
"slices"
56

67
smoothoperatorv1 "github.com/pdok/smooth-operator/api/v1"
78
smoothoperatorvalidation "github.com/pdok/smooth-operator/pkg/validation"
9+
apierrors "k8s.io/apimachinery/pkg/api/errors"
10+
"k8s.io/apimachinery/pkg/runtime/schema"
11+
"k8s.io/apimachinery/pkg/util/validation/field"
812

913
"strings"
1014

@@ -13,79 +17,121 @@ import (
1317
)
1418

1519
func (atom *Atom) ValidateCreate(c client.Client) ([]string, error) {
16-
warnings := []string{}
17-
reasons := []string{}
20+
var warnings []string
21+
var allErrs field.ErrorList
1822

1923
err := smoothoperatorvalidation.ValidateLabelsOnCreate(atom.Labels)
2024
if err != nil {
21-
reasons = append(reasons, fmt.Sprintf("%v", err))
25+
allErrs = append(allErrs, err)
2226
}
2327

24-
ValidateAtom(c, atom, &warnings, &reasons)
28+
ValidateAtom(c, atom, &warnings, &allErrs)
2529

26-
if len(reasons) > 0 {
27-
return warnings, fmt.Errorf("%s", strings.Join(reasons, ". "))
30+
if len(allErrs) == 0 {
31+
return warnings, nil
2832
}
2933

30-
return warnings, nil
34+
return warnings, apierrors.NewInvalid(
35+
schema.GroupKind{Group: "pdok.nl", Kind: "Atom"},
36+
atom.Name, allErrs)
3137
}
3238

3339
func (atom *Atom) ValidateUpdate(c client.Client, atomOld *Atom) ([]string, error) {
34-
warnings := []string{}
35-
reasons := []string{}
40+
var warnings []string
41+
var allErrs field.ErrorList
42+
smoothoperatorvalidation.ValidateLabelsOnUpdate(atomOld.Labels, atom.Labels, &allErrs)
3643

37-
// Check labels did not change
38-
err := smoothoperatorvalidation.ValidateLabelsOnUpdate(atomOld.Labels, atom.Labels)
39-
if err != nil {
40-
reasons = append(reasons, fmt.Sprintf("%v", err))
41-
}
42-
43-
smoothoperatorvalidation.CheckBaseUrlImmutability(atomOld, atom, &reasons)
44+
smoothoperatorvalidation.CheckBaseUrlImmutability(atomOld, atom, &allErrs)
4445

45-
ValidateAtom(c, atom, &warnings, &reasons)
46+
ValidateAtom(c, atom, &warnings, &allErrs)
4647

47-
if len(reasons) > 0 {
48-
return warnings, fmt.Errorf("%s", strings.Join(reasons, ". "))
48+
if len(allErrs) == 0 {
49+
return warnings, nil
4950
}
5051

51-
return warnings, nil
52+
return warnings, apierrors.NewInvalid(
53+
schema.GroupKind{Group: "pdok.nl", Kind: "Atom"},
54+
atom.Name, allErrs)
5255
}
5356

54-
func ValidateAtom(c client.Client, atom *Atom, warnings *[]string, reasons *[]string) {
55-
ValidateAtomWithoutClusterChecks(atom, warnings, reasons)
57+
func ValidateAtom(c client.Client, atom *Atom, warnings *[]string, allErrs *field.ErrorList) {
58+
ValidateAtomWithoutClusterChecks(atom, warnings, allErrs)
5659

60+
ownerInfoRef := atom.Spec.Service.OwnerInfoRef
5761
ownerInfo := &smoothoperatorv1.OwnerInfo{}
5862
objectKey := client.ObjectKey{
5963
Namespace: atom.Namespace,
60-
Name: atom.Spec.Service.OwnerInfoRef,
64+
Name: ownerInfoRef,
6165
}
6266
ctx := context.Background()
6367
err := c.Get(ctx, objectKey, ownerInfo)
68+
fieldPath := field.NewPath("spec").Child("service").Child("ownerInfoRef")
6469
if err != nil {
65-
*reasons = append(*reasons, fmt.Sprintf("%v", err))
70+
*allErrs = append(*allErrs, field.NotFound(fieldPath, ownerInfoRef))
71+
return
6672
}
6773

6874
if ownerInfo.Spec.Atom == nil {
69-
*reasons = append(*reasons, "no atom settings in ownerInfo: "+ownerInfo.Name)
75+
*allErrs = append(*allErrs, field.Required(fieldPath, "spec.Atom missing in "+ownerInfo.Name))
7076
}
7177
}
7278

73-
func ValidateAtomWithoutClusterChecks(atom *Atom, warnings *[]string, reasons *[]string) {
79+
func ValidateAtomWithoutClusterChecks(atom *Atom, warnings *[]string, allErrs *field.ErrorList) {
80+
var fieldPath *field.Path
7481
if strings.Contains(atom.GetName(), "atom") {
75-
*warnings = append(*warnings, smoothoperatorvalidation.FormatValidationWarning("name should not contain atom", atom.GroupVersionKind(), atom.GetName()))
82+
fieldPath = field.NewPath("metadata").Child("name")
83+
smoothoperatorvalidation.AddWarning(warnings, *fieldPath, "should not contain atom", atom.GroupVersionKind(), atom.GetName())
7684
}
85+
var feedNames []string
86+
for i, datasetFeed := range atom.Spec.Service.DatasetFeeds {
87+
fieldPath = field.NewPath("spec").Child("service").Child("datasetFeeds").Index(i)
7788

78-
for _, datasetFeed := range atom.Spec.Service.DatasetFeeds {
79-
for _, entry := range datasetFeed.Entries {
80-
if linkCount := len(entry.DownloadLinks); linkCount > 1 && entry.Content == nil {
81-
*reasons = append(*reasons, "content is required for an Entry with more than 1 DownloadLink")
89+
if slices.Contains(feedNames, datasetFeed.TechnicalName) {
90+
*allErrs = append(*allErrs, field.Duplicate(fieldPath.Child("technicalName"), datasetFeed.TechnicalName))
91+
}
92+
93+
feedNames = append(feedNames, datasetFeed.TechnicalName)
94+
95+
if datasetFeed.DatasetMetadataLinks != nil && atom.Spec.Service.ServiceMetadataLinks != nil {
96+
if datasetFeed.DatasetMetadataLinks.MetadataIdentifier == atom.Spec.Service.ServiceMetadataLinks.MetadataIdentifier {
97+
*allErrs = append(*allErrs, field.Invalid(
98+
fieldPath.Child("datasetMetadataLinks").Child("metadataIdentifier"),
99+
datasetFeed.DatasetMetadataLinks.MetadataIdentifier,
100+
fmt.Sprintf("should not be the same as %s", field.NewPath("spec").
101+
Child("service").Child("serviceMetadataLinks").Child("metadataIdentifier")),
102+
))
82103
}
83104
}
84-
}
85105

86-
service := atom.Spec.Service
87-
err := smoothoperatorvalidation.ValidateBaseURL(service.BaseURL)
88-
if err != nil {
89-
*reasons = append(*reasons, fmt.Sprintf("%v", err))
106+
if datasetFeed.DatasetMetadataLinks != nil && datasetFeed.SpatialDatasetIdentifierCode == nil {
107+
*allErrs = append(*allErrs, field.Required(
108+
fieldPath.Child("spatialDatasetIdentifierCode"),
109+
fmt.Sprintf("when %s exists", fieldPath.Child("datasetMetadataLinks").String()),
110+
))
111+
}
112+
113+
if datasetFeed.SpatialDatasetIdentifierCode != nil && datasetFeed.SpatialDatasetIdentifierNamespace == nil {
114+
*allErrs = append(*allErrs, field.Required(
115+
fieldPath.Child("spatialDatasetIdentifierNamespace"),
116+
fmt.Sprintf("when %s exists", fieldPath.Child("spatialDatasetIdentifierCode").String()),
117+
))
118+
}
119+
120+
var entryNames []string
121+
for in, entry := range datasetFeed.Entries {
122+
fieldPath = fieldPath.Child("entries").Index(in)
123+
if linkCount := len(entry.DownloadLinks); linkCount > 1 && entry.Content == nil {
124+
*allErrs = append(*allErrs, field.Required(
125+
fieldPath.Child("content"),
126+
fmt.Sprintf("when %s has 2 or more elements", fieldPath.Child("downloadlinks").String()),
127+
))
128+
}
129+
130+
if slices.Contains(entryNames, entry.TechnicalName) {
131+
*allErrs = append(*allErrs, field.Duplicate(fieldPath.Child("technicalName"), entry.TechnicalName))
132+
}
133+
134+
entryNames = append(entryNames, entry.TechnicalName)
135+
}
90136
}
91137
}

0 commit comments

Comments
 (0)