Skip to content

Commit e7b662c

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

File tree

4 files changed

+272
-9
lines changed

4 files changed

+272
-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: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package strfmt
1616

1717
import (
1818
"encoding"
19+
"encoding/json"
1920
"reflect"
2021
"strings"
2122
"sync"
@@ -231,3 +232,26 @@ func (f *defaultFormats) Parse(name, data string) (interface{}, error) {
231232
}
232233
return nil, errors.InvalidTypeName(name)
233234
}
235+
236+
// unmarshalJSON provides a generic implementation of json.Unmarshaler interface's UnmarshalJSON function for basic string formats.
237+
func unmarshalJSON[T ~string](r *T, data []byte) error {
238+
if string(data) == jsonNull {
239+
return nil
240+
}
241+
var ustr string
242+
if err := json.Unmarshal(data, &ustr); err != nil {
243+
return err
244+
}
245+
*r = T(ustr)
246+
return nil
247+
}
248+
249+
// deepCopy provides a generic implementation of DeepCopy for basic string formats.
250+
func deepCopy[T ~string](r *T) *T {
251+
if r == nil {
252+
return nil
253+
}
254+
out := new(T)
255+
*out = *r
256+
return out
257+
}
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
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+
const k8sPrefix = "k8s-"
23+
24+
func init() {
25+
// register formats in the KubernetesExtensions registry:
26+
// - k8s-short-name
27+
// - k8s-long-name
28+
shortName := ShortName("")
29+
Default.Add(k8sPrefix+"short-name", &shortName, IsShortName)
30+
31+
longName := LongName("")
32+
Default.Add(k8sPrefix+"long-name", &longName, IsLongName)
33+
}
34+
35+
// ShortName is a name, up to 63 characters long, composed of alphanumeric
36+
// characters and dashes, which cannot begin or end with a dash.
37+
//
38+
// ShortName almost conforms to the definition of a label in DNS (RFC 1123),
39+
// except that uppercase letters are not allowed.
40+
//
41+
// xref: https://github.com/kubernetes/kubernetes/issues/71140
42+
//
43+
// swagger:strfmt k8s-short-name
44+
type ShortName string
45+
46+
func (r ShortName) MarshalText() ([]byte, error) {
47+
return []byte(string(r)), nil
48+
}
49+
50+
func (r *ShortName) UnmarshalText(data []byte) error { // validation is performed later on
51+
*r = ShortName(data)
52+
return nil
53+
}
54+
55+
func (r ShortName) String() string {
56+
return string(r)
57+
}
58+
59+
func (r ShortName) MarshalJSON() ([]byte, error) {
60+
return json.Marshal(string(r))
61+
}
62+
63+
func (r *ShortName) UnmarshalJSON(data []byte) error {
64+
return unmarshalJSON(r, data)
65+
}
66+
67+
func (r *ShortName) DeepCopyInto(out *ShortName) {
68+
*out = *r
69+
}
70+
71+
func (r *ShortName) DeepCopy() *ShortName {
72+
return deepCopy(r)
73+
}
74+
75+
const shortNameFmt string = "[a-z0-9]([-a-z0-9]*[a-z0-9])?"
76+
77+
// ShortNameMaxLength is a label's max length in DNS (RFC 1123)
78+
const ShortNameMaxLength int = 63
79+
80+
var shortNameRegexp = regexp.MustCompile("^" + shortNameFmt + "$")
81+
82+
// IsShortName checks if a string is a valid ShortName.
83+
func IsShortName(value string) bool {
84+
return len(value) <= ShortNameMaxLength &&
85+
shortNameRegexp.MatchString(value)
86+
}
87+
88+
// LongName is a name, up to 253 characters long, composed of dot-separated
89+
// segments; each segment uses only alphanumerics and dashes (no
90+
// leading/trailing).
91+
//
92+
// LongName almost conforms to the definition of a subdomain in DNS (RFC 1123),
93+
// except that uppercase letters are not allowed, and there is no max length
94+
// limit of 63 for each of the dot-separated DNS Labels that make up the
95+
// subdomain.
96+
//
97+
// xref: https://github.com/kubernetes/kubernetes/issues/71140
98+
// xref: https://github.com/kubernetes/kubernetes/issues/79351
99+
//
100+
// swagger:strfmt k8s-long-name
101+
type LongName string
102+
103+
func (r LongName) MarshalText() ([]byte, error) {
104+
return []byte(string(r)), nil
105+
}
106+
107+
func (r *LongName) UnmarshalText(data []byte) error { // validation is performed later on
108+
*r = LongName(data)
109+
return nil
110+
}
111+
112+
func (r LongName) String() string {
113+
return string(r)
114+
}
115+
116+
func (r LongName) MarshalJSON() ([]byte, error) {
117+
return json.Marshal(string(r))
118+
}
119+
120+
func (r *LongName) UnmarshalJSON(data []byte) error {
121+
return unmarshalJSON(r, data)
122+
}
123+
124+
func (r *LongName) DeepCopyInto(out *LongName) {
125+
*out = *r
126+
}
127+
128+
func (r *LongName) DeepCopy() *LongName {
129+
return deepCopy(r)
130+
}
131+
132+
const longNameFmt string = shortNameFmt + "(\\." + shortNameFmt + ")*"
133+
134+
// LongNameMaxLength is a subdomain's max length in DNS (RFC 1123)
135+
const LongNameMaxLength int = 253
136+
137+
var longNameRegexp = regexp.MustCompile("^" + longNameFmt + "$")
138+
139+
// IsLongName checks if a string is a valid LongName.
140+
func IsLongName(value string) bool {
141+
return len(value) <= LongNameMaxLength &&
142+
longNameRegexp.MatchString(value)
143+
}
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, Default, &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, Default, &v, "k8s-long-name", "a", goodLongName, append(badLongName, prefixOnlyLongName...))
77+
}

0 commit comments

Comments
 (0)