Skip to content

Commit 15861bc

Browse files
committed
Add k8s-short-name and k8s-long-name formats.
1 parent c8a335a commit 15861bc

File tree

4 files changed

+299
-9
lines changed

4 files changed

+299
-9
lines changed

pkg/validation/strfmt/default_test.go

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,10 @@ type testableFormat interface {
317317
}
318318

319319
func testStringFormat(t *testing.T, what testableFormat, format, with string, validSamples, invalidSamples []string) {
320+
testStringFormatWithRegistry(t, Default, what, format, with, validSamples, invalidSamples)
321+
}
322+
323+
func testStringFormatWithRegistry(t *testing.T, registry Registry, what testableFormat, format, with string, validSamples, invalidSamples []string) {
320324
// text encoding interface
321325
b := []byte(with)
322326
err := what.UnmarshalText(b)
@@ -347,24 +351,39 @@ func testStringFormat(t *testing.T, what testableFormat, format, with string, va
347351
assert.Equalf(t, bj, b, "[%s]MarshalJSON: expected %v and %v to be value equal as []byte", format, string(b), with)
348352

349353
// validation with Registry
350-
for _, valid := range append(validSamples, with) {
351-
testValid(t, format, valid)
352-
}
353-
354-
for _, invalid := range invalidSamples {
355-
testInvalid(t, format, invalid)
356-
}
354+
t.Run("valid", func(t *testing.T) {
355+
for _, valid := range append(validSamples, with) {
356+
t.Run(valid, func(t *testing.T) {
357+
testValidWithRegistry(t, registry, format, valid)
358+
})
359+
}
360+
})
361+
t.Run("invalid", func(t *testing.T) {
362+
for _, invalid := range invalidSamples {
363+
t.Run(invalid, func(t *testing.T) {
364+
testInvalidWithRegistry(t, registry, format, invalid)
365+
})
366+
}
367+
})
357368
}
358369

359370
func testValid(t *testing.T, name, value string) {
360-
ok := Default.Validates(name, value)
371+
testValidWithRegistry(t, Default, name, value)
372+
}
373+
374+
func testValidWithRegistry(t *testing.T, registry Registry, name, value string) {
375+
ok := registry.Validates(name, value)
361376
if !ok {
362377
t.Errorf("expected %q of type %s to be valid", value, name)
363378
}
364379
}
365380

366381
func testInvalid(t *testing.T, name, value string) {
367-
ok := Default.Validates(name, value)
382+
testInvalidWithRegistry(t, Default, name, value)
383+
}
384+
385+
func testInvalidWithRegistry(t *testing.T, registry Registry, name, value string) {
386+
ok := registry.Validates(name, value)
368387
if ok {
369388
t.Errorf("expected %q of type %s to be invalid", value, name)
370389
}

pkg/validation/strfmt/format.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ package strfmt
1616

1717
import (
1818
"encoding"
19+
"encoding/json"
20+
"fmt"
1921
"reflect"
2022
"strings"
2123
"sync"
@@ -231,3 +233,40 @@ func (f *defaultFormats) Parse(name, data string) (interface{}, error) {
231233
}
232234
return nil, errors.InvalidTypeName(name)
233235
}
236+
237+
// scan provides a generic implementation of sql.Scanner interface's Scan function for basic string formats.
238+
func scan[T ~string](r *T, raw interface{}) error {
239+
switch v := raw.(type) {
240+
case []byte:
241+
*r = T(v)
242+
case string:
243+
*r = T(v)
244+
default:
245+
return fmt.Errorf("cannot sql.Scan() strfmt.StringFormat from: %#v", v)
246+
}
247+
248+
return nil
249+
}
250+
251+
// unmarshalJSON provides a generic implementation of json.Unmarshaler interface's UnmarshalJSON function for basic string formats.
252+
func unmarshalJSON[T ~string](r *T, data []byte) error {
253+
if string(data) == jsonNull {
254+
return nil
255+
}
256+
var ustr string
257+
if err := json.Unmarshal(data, &ustr); err != nil {
258+
return err
259+
}
260+
*r = T(ustr)
261+
return nil
262+
}
263+
264+
// deepCopy provides a generic implementation of DeepCopy for basic string formats.
265+
func deepCopy[T ~string](r *T) *T {
266+
if r == nil {
267+
return nil
268+
}
269+
out := new(T)
270+
*out = *r
271+
return out
272+
}
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
// Copyright 2024 go-swagger maintainers
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package strfmt
16+
17+
import (
18+
"encoding/json"
19+
"regexp"
20+
)
21+
22+
// KubernetesExtensions is the formats registry for JSON Schema formats
23+
// extensions defined by the Kubernetes project.
24+
var KubernetesExtensions = NewSeededFormats(nil, nil)
25+
26+
const k8sPrefix = "k8s-"
27+
28+
func init() {
29+
// register formats in the KubernetesExtensions registry:
30+
// - k8s-short-name
31+
// - k8s-long-name
32+
shortName := ShortName("")
33+
KubernetesExtensions.Add(k8sPrefix+"short-name", &shortName, IsShortName)
34+
35+
longName := LongName("")
36+
KubernetesExtensions.Add(k8sPrefix+"long-name", &longName, IsLongName)
37+
}
38+
39+
// ShortName is a name, up to 63 characters long, composed of alphanumeric
40+
// characters and dashes, which cannot begin or end with a dash.
41+
//
42+
// ShortName almost conforms to the definition of a label in DNS (RFC 1123),
43+
// except that uppercase letters are not allowed.
44+
//
45+
// xref: https://github.com/kubernetes/kubernetes/issues/71140
46+
//
47+
// swagger:strfmt k8s-short-name
48+
type ShortName string
49+
50+
func (r ShortName) MarshalText() ([]byte, error) {
51+
return []byte(string(r)), nil
52+
}
53+
54+
func (r *ShortName) UnmarshalText(data []byte) error { // validation is performed later on
55+
*r = ShortName(data)
56+
return nil
57+
}
58+
59+
func (r *ShortName) Scan(raw interface{}) error {
60+
return scan[ShortName](r, raw)
61+
}
62+
63+
func (r ShortName) String() string {
64+
return string(r)
65+
}
66+
67+
func (r ShortName) MarshalJSON() ([]byte, error) {
68+
return json.Marshal(string(r))
69+
}
70+
71+
func (r *ShortName) UnmarshalJSON(data []byte) error {
72+
return unmarshalJSON(r, data)
73+
}
74+
75+
func (r *ShortName) DeepCopyInto(out *ShortName) {
76+
*out = *r
77+
}
78+
79+
func (r *ShortName) DeepCopy() *ShortName {
80+
return deepCopy(r)
81+
}
82+
83+
const shortNameFmt string = "[a-z0-9]([-a-z0-9]*[a-z0-9])?"
84+
85+
// ShortNameMaxLength is a label's max length in DNS (RFC 1123)
86+
const ShortNameMaxLength int = 63
87+
88+
var shortNameRegexp = regexp.MustCompile("^" + shortNameFmt + "$")
89+
90+
// IsShortName checks if a string is a valid ShortName.
91+
func IsShortName(value string) bool {
92+
return len(value) <= ShortNameMaxLength &&
93+
shortNameRegexp.MatchString(value)
94+
}
95+
96+
// LongName is a name, up to 253 characters long, composed of dot-separated
97+
// segments; each segment uses only alphanumerics and dashes (no
98+
// leading/trailing).
99+
//
100+
// LongName almost conforms to the definition of a subdomain in DNS (RFC 1123),
101+
// except that uppercase letters are not allowed. and there is no max length
102+
// limit of 63 for each of the dot-separated DNS Labels that make up the
103+
// subdomain.
104+
//
105+
// xref: https://github.com/kubernetes/kubernetes/issues/71140
106+
// xref: https://github.com/kubernetes/kubernetes/issues/79351
107+
//
108+
// swagger:strfmt k8s-long-name
109+
type LongName string
110+
111+
func (r LongName) MarshalText() ([]byte, error) {
112+
return []byte(string(r)), nil
113+
}
114+
115+
func (r *LongName) UnmarshalText(data []byte) error { // validation is performed later on
116+
*r = LongName(data)
117+
return nil
118+
}
119+
120+
func (r *LongName) Scan(raw interface{}) error {
121+
return scan[LongName](r, raw)
122+
}
123+
124+
func (r LongName) String() string {
125+
return string(r)
126+
}
127+
128+
func (r LongName) MarshalJSON() ([]byte, error) {
129+
return json.Marshal(string(r))
130+
}
131+
132+
func (r *LongName) UnmarshalJSON(data []byte) error {
133+
return unmarshalJSON(r, data)
134+
}
135+
136+
func (r *LongName) DeepCopyInto(out *LongName) {
137+
*out = *r
138+
}
139+
140+
func (r *LongName) DeepCopy() *LongName {
141+
return deepCopy(r)
142+
}
143+
144+
const longNameFmt string = shortNameFmt + "(\\." + shortNameFmt + ")*"
145+
146+
// LongNameMaxLength is a subdomain's max length in DNS (RFC 1123)
147+
const LongNameMaxLength int = 253
148+
149+
var longNameRegexp = regexp.MustCompile("^" + longNameFmt + "$")
150+
151+
// IsLongName checks if a string is a valid LongName.
152+
func IsLongName(value string) bool {
153+
return len(value) <= LongNameMaxLength &&
154+
longNameRegexp.MatchString(value)
155+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Copyright 2024 go-swagger maintainers
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package strfmt
16+
17+
import (
18+
"strings"
19+
"testing"
20+
)
21+
22+
var goodShortName = []string{
23+
"a", "ab", "abc", "a1", "a-1", "a--1--2--b",
24+
"0", "01", "012", "1a", "1-a", "1--a--b--2",
25+
strings.Repeat("a", 63),
26+
}
27+
var badShortName = []string{
28+
"", "A", "ABC", "aBc", "A1", "A-1", "1-A",
29+
"-", "-a", "-1",
30+
"_", "a_", "_a", "a_b", "1_", "_1", "1_2",
31+
".", "a.", ".a", "a.b", "1.", ".1", "1.2",
32+
" ", "a ", " a", "a b", "1 ", " 1", "1 2",
33+
strings.Repeat("a", 64),
34+
}
35+
36+
var prefixOnlyShortName = []string{
37+
"a-", "1-",
38+
}
39+
40+
func TestIsShortName(t *testing.T) {
41+
v := ShortName("a")
42+
testStringFormatWithRegistry(t, KubernetesExtensions, &v, "k8s-short-name", "a", goodShortName, append(badShortName, prefixOnlyShortName...))
43+
}
44+
45+
var goodLongName = []string{
46+
"a", "ab", "abc", "a1", "a-1", "a--1--2--b",
47+
"0", "01", "012", "1a", "1-a", "1--a--b--2",
48+
"a.a", "ab.a", "abc.a", "a1.a", "a-1.a", "a--1--2--b.a",
49+
"a.1", "ab.1", "abc.1", "a1.1", "a-1.1", "a--1--2--b.1",
50+
"0.a", "01.a", "012.a", "1a.a", "1-a.a", "1--a--b--2",
51+
"0.1", "01.1", "012.1", "1a.1", "1-a.1", "1--a--b--2.1",
52+
"a.b.c.d.e", "aa.bb.cc.dd.ee", "1.2.3.4.5", "11.22.33.44.55",
53+
strings.Repeat("a", 253),
54+
}
55+
var badLongName = []string{
56+
"", "A", "ABC", "aBc", "A1", "A-1", "1-A",
57+
"-", "-a", "-1",
58+
"_", "a_", "_a", "a_b", "1_", "_1", "1_2",
59+
".", "a.", ".a", "a..b", "1.", ".1", "1..2",
60+
" ", "a ", " a", "a b", "1 ", " 1", "1 2",
61+
"A.a", "aB.a", "ab.A", "A1.a", "a1.A",
62+
"A.1", "aB.1", "A1.1", "1A.1",
63+
"0.A", "01.A", "012.A", "1A.a", "1a.A",
64+
"A.B.C.D.E", "AA.BB.CC.DD.EE", "a.B.c.d.e", "aa.bB.cc.dd.ee",
65+
"a@b", "a,b", "a_b", "a;b",
66+
"a:b", "a%b", "a?b", "a$b",
67+
strings.Repeat("a", 254),
68+
}
69+
70+
var prefixOnlyLongName = []string{
71+
"a-", "1-",
72+
}
73+
74+
func TestFormatLongName(t *testing.T) {
75+
v := LongName("a")
76+
testStringFormatWithRegistry(t, KubernetesExtensions, &v, "k8s-long-name", "a", goodLongName, append(badLongName, prefixOnlyLongName...))
77+
}

0 commit comments

Comments
 (0)