Repository: kubernetes/kube-openapi

Issue:
Drop scan implementations from formats
Some, but not all strfmt types implement the `sql.Scanner` interface.  For example duration implements `Scan` but date does not.

We should be consistent.  I'm in favor of removing scan support rather than exposing incomplete support for a capability we don't need or use.

Hints from issue comments:
@k8s-triage-robot: The Kubernetes project currently lacks enough contributors to adequately respond to all issues.

This bot triages un-triaged issues according to the following rules:
- After 90d of inactivity, `lifecycle/stale` is applied
- After 30d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
- After 30d of inactivity since `lifecycle/rotten` was applied, the issue is closed

You can:
- Mark this issue as fresh with `/remove-lifecycle stale`
- Close this issue with `/close`
- Offer to help out with [Issue Triage][1]

Please send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).

/lifecycle stale

[1]: https://www.kubernetes.dev/docs/guide/issue-triage/

Solution patch:
diff --git a/pkg/validation/strfmt/format.go b/pkg/validation/strfmt/format.go
index c85067a..6981a40 100644
--- a/pkg/validation/strfmt/format.go
+++ b/pkg/validation/strfmt/format.go
@@ -16,6 +16,7 @@ package strfmt
 
 import (
 	"encoding"
+	"encoding/json"
 	"reflect"
 	"strings"
 	"sync"
@@ -231,3 +232,26 @@ func (f *defaultFormats) Parse(name, data string) (interface{}, error) {
 	}
 	return nil, errors.InvalidTypeName(name)
 }
+
+// unmarshalJSON provides a generic implementation of json.Unmarshaler interface's UnmarshalJSON function for basic string formats.
+func unmarshalJSON[T ~string](r *T, data []byte) error {
+	if string(data) == jsonNull {
+		return nil
+	}
+	var ustr string
+	if err := json.Unmarshal(data, &ustr); err != nil {
+		return err
+	}
+	*r = T(ustr)
+	return nil
+}
+
+// deepCopy provides a generic implementation of DeepCopy for basic string formats.
+func deepCopy[T ~string](r *T) *T {
+	if r == nil {
+		return nil
+	}
+	out := new(T)
+	*out = *r
+	return out
+}
diff --git a/pkg/validation/strfmt/kubernetes-extensions.go b/pkg/validation/strfmt/kubernetes-extensions.go
new file mode 100644
index 0000000..64d6654
--- /dev/null
+++ b/pkg/validation/strfmt/kubernetes-extensions.go
@@ -0,0 +1,143 @@
+// Copyright 2024 go-swagger maintainers
+//
+// 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 strfmt
+
+import (
+	"encoding/json"
+	"regexp"
+)
+
+const k8sPrefix = "k8s-"
+
+func init() {
+	// register formats in the KubernetesExtensions registry:
+	//   - k8s-short-name
+	//   - k8s-long-name
+	shortName := ShortName("")
+	Default.Add(k8sPrefix+"short-name", &shortName, IsShortName)
+
+	longName := LongName("")
+	Default.Add(k8sPrefix+"long-name", &longName, IsLongName)
+}
+
+// ShortName is a name, up to 63 characters long, composed of alphanumeric
+// characters and dashes, which cannot begin or end with a dash.
+//
+// ShortName almost conforms to the definition of a label in DNS (RFC 1123),
+// except that uppercase letters are not allowed.
+//
+// xref: https://github.com/kubernetes/kubernetes/issues/71140
+//
+// swagger:strfmt k8s-short-name
+type ShortName string
+
+func (r ShortName) MarshalText() ([]byte, error) {
+	return []byte(string(r)), nil
+}
+
+func (r *ShortName) UnmarshalText(data []byte) error { // validation is performed later on
+	*r = ShortName(data)
+	return nil
+}
+
+func (r ShortName) String() string {
+	return string(r)
+}
+
+func (r ShortName) MarshalJSON() ([]byte, error) {
+	return json.Marshal(string(r))
+}
+
+func (r *ShortName) UnmarshalJSON(data []byte) error {
+	return unmarshalJSON(r, data)
+}
+
+func (r *ShortName) DeepCopyInto(out *ShortName) {
+	*out = *r
+}
+
+func (r *ShortName) DeepCopy() *ShortName {
+	return deepCopy(r)
+}
+
+const shortNameFmt string = "[a-z0-9]([-a-z0-9]*[a-z0-9])?"
+
+// ShortNameMaxLength is a label's max length in DNS (RFC 1123)
+const ShortNameMaxLength int = 63
+
+var shortNameRegexp = regexp.MustCompile("^" + shortNameFmt + "$")
+
+// IsShortName checks if a string is a valid ShortName.
+func IsShortName(value string) bool {
+	return len(value) <= ShortNameMaxLength &&
+		shortNameRegexp.MatchString(value)
+}
+
+// LongName is a name, up to 253 characters long, composed of dot-separated
+// segments; each segment uses only alphanumerics and dashes (no
+// leading/trailing).
+//
+// LongName almost conforms to the definition of a subdomain in DNS (RFC 1123),
+// except that uppercase letters are not allowed, and there is no max length
+// limit of 63 for each of the dot-separated DNS Labels that make up the
+// subdomain.
+//
+// xref: https://github.com/kubernetes/kubernetes/issues/71140
+// xref: https://github.com/kubernetes/kubernetes/issues/79351
+//
+// swagger:strfmt k8s-long-name
+type LongName string
+
+func (r LongName) MarshalText() ([]byte, error) {
+	return []byte(string(r)), nil
+}
+
+func (r *LongName) UnmarshalText(data []byte) error { // validation is performed later on
+	*r = LongName(data)
+	return nil
+}
+
+func (r LongName) String() string {
+	return string(r)
+}
+
+func (r LongName) MarshalJSON() ([]byte, error) {
+	return json.Marshal(string(r))
+}
+
+func (r *LongName) UnmarshalJSON(data []byte) error {
+	return unmarshalJSON(r, data)
+}
+
+func (r *LongName) DeepCopyInto(out *LongName) {
+	*out = *r
+}
+
+func (r *LongName) DeepCopy() *LongName {
+	return deepCopy(r)
+}
+
+const longNameFmt string = shortNameFmt + "(\\." + shortNameFmt + ")*"
+
+// LongNameMaxLength is a subdomain's max length in DNS (RFC 1123)
+const LongNameMaxLength int = 253
+
+var longNameRegexp = regexp.MustCompile("^" + longNameFmt + "$")
+
+// IsLongName checks if a string is a valid LongName.
+func IsLongName(value string) bool {
+	return len(value) <= LongNameMaxLength &&
+		longNameRegexp.MatchString(value)
+}



Test patch:
diff --git a/pkg/validation/strfmt/default_test.go b/pkg/validation/strfmt/default_test.go
index b31bf0d..0b718da 100644
--- a/pkg/validation/strfmt/default_test.go
+++ b/pkg/validation/strfmt/default_test.go
@@ -317,6 +317,10 @@ type testableFormat interface {
 }
 
 func testStringFormat(t *testing.T, what testableFormat, format, with string, validSamples, invalidSamples []string) {
+	testStringFormatWithRegistry(t, Default, what, format, with, validSamples, invalidSamples)
+}
+
+func testStringFormatWithRegistry(t *testing.T, registry Registry, what testableFormat, format, with string, validSamples, invalidSamples []string) {
 	// text encoding interface
 	b := []byte(with)
 	err := what.UnmarshalText(b)
@@ -347,24 +351,39 @@ func testStringFormat(t *testing.T, what testableFormat, format, with string, va
 	assert.Equalf(t, bj, b, "[%s]MarshalJSON: expected %v and %v to be value equal as []byte", format, string(b), with)
 
 	// validation with Registry
-	for _, valid := range append(validSamples, with) {
-		testValid(t, format, valid)
-	}
-
-	for _, invalid := range invalidSamples {
-		testInvalid(t, format, invalid)
-	}
+	t.Run("valid", func(t *testing.T) {
+		for _, valid := range append(validSamples, with) {
+			t.Run(valid, func(t *testing.T) {
+				testValidWithRegistry(t, registry, format, valid)
+			})
+		}
+	})
+	t.Run("invalid", func(t *testing.T) {
+		for _, invalid := range invalidSamples {
+			t.Run(invalid, func(t *testing.T) {
+				testInvalidWithRegistry(t, registry, format, invalid)
+			})
+		}
+	})
 }
 
 func testValid(t *testing.T, name, value string) {
-	ok := Default.Validates(name, value)
+	testValidWithRegistry(t, Default, name, value)
+}
+
+func testValidWithRegistry(t *testing.T, registry Registry, name, value string) {
+	ok := registry.Validates(name, value)
 	if !ok {
 		t.Errorf("expected %q of type %s to be valid", value, name)
 	}
 }
 
 func testInvalid(t *testing.T, name, value string) {
-	ok := Default.Validates(name, value)
+	testInvalidWithRegistry(t, Default, name, value)
+}
+
+func testInvalidWithRegistry(t *testing.T, registry Registry, name, value string) {
+	ok := registry.Validates(name, value)
 	if ok {
 		t.Errorf("expected %q of type %s to be invalid", value, name)
 	}
diff --git a/pkg/validation/strfmt/kubernetes-extensions_test.go b/pkg/validation/strfmt/kubernetes-extensions_test.go
new file mode 100644
index 0000000..d58bc17
--- /dev/null
+++ b/pkg/validation/strfmt/kubernetes-extensions_test.go
@@ -0,0 +1,77 @@
+// Copyright 2024 go-swagger maintainers
+//
+// 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 strfmt
+
+import (
+	"strings"
+	"testing"
+)
+
+var goodShortName = []string{
+	"a", "ab", "abc", "a1", "a-1", "a--1--2--b",
+	"0", "01", "012", "1a", "1-a", "1--a--b--2",
+	strings.Repeat("a", 63),
+}
+var badShortName = []string{
+	"", "A", "ABC", "aBc", "A1", "A-1", "1-A",
+	"-", "-a", "-1",
+	"_", "a_", "_a", "a_b", "1_", "_1", "1_2",
+	".", "a.", ".a", "a.b", "1.", ".1", "1.2",
+	" ", "a ", " a", "a b", "1 ", " 1", "1 2",
+	strings.Repeat("a", 64),
+}
+
+var prefixOnlyShortName = []string{
+	"a-", "1-",
+}
+
+func TestIsShortName(t *testing.T) {
+	v := ShortName("a")
+	testStringFormatWithRegistry(t, Default, &v, "k8s-short-name", "a", goodShortName, append(badShortName, prefixOnlyShortName...))
+}
+
+var goodLongName = []string{
+	"a", "ab", "abc", "a1", "a-1", "a--1--2--b",
+	"0", "01", "012", "1a", "1-a", "1--a--b--2",
+	"a.a", "ab.a", "abc.a", "a1.a", "a-1.a", "a--1--2--b.a",
+	"a.1", "ab.1", "abc.1", "a1.1", "a-1.1", "a--1--2--b.1",
+	"0.a", "01.a", "012.a", "1a.a", "1-a.a", "1--a--b--2",
+	"0.1", "01.1", "012.1", "1a.1", "1-a.1", "1--a--b--2.1",
+	"a.b.c.d.e", "aa.bb.cc.dd.ee", "1.2.3.4.5", "11.22.33.44.55",
+	strings.Repeat("a", 253),
+}
+var badLongName = []string{
+	"", "A", "ABC", "aBc", "A1", "A-1", "1-A",
+	"-", "-a", "-1",
+	"_", "a_", "_a", "a_b", "1_", "_1", "1_2",
+	".", "a.", ".a", "a..b", "1.", ".1", "1..2",
+	" ", "a ", " a", "a b", "1 ", " 1", "1 2",
+	"A.a", "aB.a", "ab.A", "A1.a", "a1.A",
+	"A.1", "aB.1", "A1.1", "1A.1",
+	"0.A", "01.A", "012.A", "1A.a", "1a.A",
+	"A.B.C.D.E", "AA.BB.CC.DD.EE", "a.B.c.d.e", "aa.bB.cc.dd.ee",
+	"a@b", "a,b", "a_b", "a;b",
+	"a:b", "a%b", "a?b", "a$b",
+	strings.Repeat("a", 254),
+}
+
+var prefixOnlyLongName = []string{
+	"a-", "1-",
+}
+
+func TestFormatLongName(t *testing.T) {
+	v := LongName("a")
+	testStringFormatWithRegistry(t, Default, &v, "k8s-long-name", "a", goodLongName, append(badLongName, prefixOnlyLongName...))
+}