Skip to content

Commit e92e85d

Browse files
authored
⚠️ Add support for encoding.TextMarshaler (#1015)
* ⚠️ Add support for encoding.TextMarshaler Whenever a type is encountered that implements encoding.TextMarshaler but not encoding/json.Marshaler, assume that it will be encoded as a string. This is a breaking change, as types that implement TextMarshaler are now handled differently. On the other hand, this probably restores reality for all but the most customized setups, as Go's JSON package will also check for the presence of this interface, and output a JSON string. Signed-off-by: Tom Wieczorek <[email protected]> * 🌱 More tests for encoding.TextMarshaler Test that json.Marshaler has precedence over encoding.TextMarshaler. Signed-off-by: Tom Wieczorek <[email protected]> --------- Signed-off-by: Tom Wieczorek <[email protected]>
1 parent d62a67b commit e92e85d

File tree

3 files changed

+104
-8
lines changed

3 files changed

+104
-8
lines changed

pkg/crd/schema.go

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -113,12 +113,26 @@ func (c *schemaContext) requestSchema(pkgPath, typeName string) {
113113

114114
// infoToSchema creates a schema for the type in the given set of type information.
115115
func infoToSchema(ctx *schemaContext) *apiext.JSONSchemaProps {
116-
// If the obj implements a JSON marshaler and has a marker, use the markers value and do not traverse as
117-
// the marshaler could be doing anything. If there is no marker, fall back to traversing.
118-
if obj := ctx.pkg.Types.Scope().Lookup(ctx.info.Name); obj != nil && implementsJSONMarshaler(obj.Type()) {
119-
schema := &apiext.JSONSchemaProps{}
120-
applyMarkers(ctx, ctx.info.Markers, schema, ctx.info.RawSpec.Type)
121-
if schema.Type != "" {
116+
if obj := ctx.pkg.Types.Scope().Lookup(ctx.info.Name); obj != nil {
117+
switch {
118+
// If the obj implements a JSON marshaler and has a marker, use the
119+
// markers value and do not traverse as the marshaler could be doing
120+
// anything. If there is no marker, fall back to traversing.
121+
case implements(obj.Type(), jsonMarshaler):
122+
schema := &apiext.JSONSchemaProps{}
123+
applyMarkers(ctx, ctx.info.Markers, schema, ctx.info.RawSpec.Type)
124+
if schema.Type != "" {
125+
return schema
126+
}
127+
128+
// If the obj implements a text marshaler, encode it as a string.
129+
case implements(obj.Type(), textMarshaler):
130+
schema := &apiext.JSONSchemaProps{Type: "string"}
131+
applyMarkers(ctx, ctx.info.Markers, schema, ctx.info.RawSpec.Type)
132+
if schema.Type != "string" {
133+
err := fmt.Errorf("%q implements encoding.TextMarshaler but schema type is not string: %q", ctx.info.RawSpec.Name, schema.Type)
134+
ctx.pkg.AddError(loader.ErrFromNode(err, ctx.info.RawSpec.Type))
135+
}
122136
return schema
123137
}
124138
}
@@ -521,6 +535,15 @@ var jsonMarshaler = types.NewInterfaceType([]*types.Func{
521535
types.NewVar(token.NoPos, nil, "", types.Universe.Lookup("error").Type())), false)),
522536
}, nil).Complete()
523537

524-
func implementsJSONMarshaler(typ types.Type) bool {
525-
return types.Implements(typ, jsonMarshaler) || types.Implements(types.NewPointer(typ), jsonMarshaler)
538+
// Open coded go/types representation of encoding.TextMarshaler
539+
var textMarshaler = types.NewInterfaceType([]*types.Func{
540+
types.NewFunc(token.NoPos, nil, "MarshalText",
541+
types.NewSignatureType(nil, nil, nil, nil,
542+
types.NewTuple(
543+
types.NewVar(token.NoPos, nil, "text", types.NewSlice(types.Universe.Lookup("byte").Type())),
544+
types.NewVar(token.NoPos, nil, "err", types.Universe.Lookup("error").Type())), false)),
545+
}, nil).Complete()
546+
547+
func implements(typ types.Type, iface *types.Interface) bool {
548+
return types.Implements(typ, iface) || types.Implements(types.NewPointer(typ), iface)
526549
}

pkg/crd/testdata/cronjob_types.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,11 @@ limitations under the License.
2323
package cronjob
2424

2525
import (
26+
"encoding"
2627
"encoding/json"
2728
"fmt"
2829
"net/url"
30+
"strconv"
2931
"time"
3032

3133
batchv1beta1 "k8s.io/api/batch/v1beta1"
@@ -592,6 +594,52 @@ func (u *URL2) String() string {
592594
return (*url.URL)(u).String()
593595
}
594596

597+
// URL3 wraps [net/url.URL]. It implements [encoding.TextMarshaler] so that it
598+
// can be used in K8s CRDs such that the CRD resource will have the URL but
599+
// operator code can can work with the URL struct.
600+
type URL3 struct{ url.URL }
601+
602+
var _ encoding.TextMarshaler = (*URL3)(nil)
603+
604+
// MarshalText implements [encoding.TextMarshaler].
605+
func (u *URL3) MarshalText() (text []byte, err error) {
606+
return u.MarshalBinary()
607+
}
608+
609+
// URL4 is newtype around [net/url.URL]. It implements [encoding.TextMarshaler]
610+
// so that it can be used in K8s CRDs such that the CRD resource will have the
611+
// URL but operator code can can work with the URL struct.
612+
type URL4 url.URL
613+
614+
var _ encoding.TextMarshaler = (*URL4)(nil)
615+
616+
// MarshalText implements [encoding.TextMarshaler].
617+
func (u *URL4) MarshalText() (text []byte, err error) {
618+
return (*url.URL)(u).MarshalBinary()
619+
}
620+
621+
// +kubebuilder:validation:Type=integer
622+
// +kubebuilder:validation:Format=int64
623+
// Time2 is a newtype around [metav1.Time].
624+
// It implements both [encoding.TextMarshaler] and [json.Marshaler].
625+
// The latter is authoritative for the CRD generation.
626+
type Time2 time.Time
627+
628+
var _ interface {
629+
encoding.TextMarshaler
630+
json.Marshaler
631+
} = (*Time2)(nil)
632+
633+
// MarshalText implements [encoding.TextMarshaler].
634+
func (t *Time2) MarshalText() (text []byte, err error) {
635+
return []byte((*time.Time)(t).String()), nil
636+
}
637+
638+
// MarshalJSON implements [json.Marshaler].
639+
func (t *Time2) MarshalJSON() ([]byte, error) {
640+
return strconv.AppendInt(nil, (*time.Time)(t).UnixMilli(), 10), nil
641+
}
642+
595643
// Duration has a custom Marshaler but no markers.
596644
// We want the CRD generation to infer type information
597645
// from the go types and ignore the presense of the Marshaler.
@@ -642,6 +690,10 @@ type CronJobStatus struct {
642690
// +optional
643691
LastScheduleTime *metav1.Time `json:"lastScheduleTime,omitempty"`
644692

693+
// Information when was the last time the job was successfully scheduled.
694+
// +optional
695+
LastScheduleTime2 Time2 `json:"lastScheduleTime2,omitempty"`
696+
645697
// Information about the last time the job was successfully scheduled,
646698
// with microsecond precision.
647699
// +optional
@@ -655,6 +707,14 @@ type CronJobStatus struct {
655707
// +optional
656708
LastActiveLogURL2 *URL2 `json:"lastActiveLogURL2,omitempty"`
657709

710+
// LastActiveLogURL3 specifies the logging url for the last started job
711+
// +optional
712+
LastActiveLogURL3 *URL3 `json:"lastActiveLogURL3,omitempty"`
713+
714+
// LastActiveLogURL4 specifies the logging url for the last started job
715+
// +optional
716+
LastActiveLogURL4 *URL4 `json:"lastActiveLogURL4,omitempty"`
717+
658718
Runtime *Duration `json:"duration,omitempty"`
659719
}
660720

pkg/crd/testdata/testdata.kubebuilder.io_cronjobs.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9221,6 +9221,14 @@ spec:
92219221
description: LastActiveLogURL2 specifies the logging url for the last
92229222
started job
92239223
type: string
9224+
lastActiveLogURL3:
9225+
description: LastActiveLogURL3 specifies the logging url for the last
9226+
started job
9227+
type: string
9228+
lastActiveLogURL4:
9229+
description: LastActiveLogURL4 specifies the logging url for the last
9230+
started job
9231+
type: string
92249232
lastScheduleMicroTime:
92259233
description: |-
92269234
Information about the last time the job was successfully scheduled,
@@ -9232,6 +9240,11 @@ spec:
92329240
scheduled.
92339241
format: date-time
92349242
type: string
9243+
lastScheduleTime2:
9244+
description: Information when was the last time the job was successfully
9245+
scheduled.
9246+
format: int64
9247+
type: integer
92359248
type: object
92369249
type: object
92379250
selectableFields:

0 commit comments

Comments
 (0)