code-generator: allow to customize generated verbs and add custom verb

parent c026b62d
...@@ -32,6 +32,7 @@ var supportedTags = []string{ ...@@ -32,6 +32,7 @@ var supportedTags = []string{
"genclient:skipVerbs", "genclient:skipVerbs",
"genclient:noStatus", "genclient:noStatus",
"genclient:readonly", "genclient:readonly",
"genclient:method",
} }
// SupportedVerbs is a list of supported verbs for +onlyVerbs and +skipVerbs. // SupportedVerbs is a list of supported verbs for +onlyVerbs and +skipVerbs.
...@@ -54,6 +55,96 @@ var ReadonlyVerbs = []string{ ...@@ -54,6 +55,96 @@ var ReadonlyVerbs = []string{
"watch", "watch",
} }
// genClientPrefix is the default prefix for all genclient tags.
const genClientPrefix = "genclient:"
// unsupportedExtensionVerbs is a list of verbs we don't support generating
// extension client functions for.
var unsupportedExtensionVerbs = []string{
"updateStatus",
"deleteCollection",
"watch",
"delete",
}
// inputTypeSupportedVerbs is a list of verb types that supports overriding the
// input argument type.
var inputTypeSupportedVerbs = []string{
"create",
"update",
}
// resultTypeSupportedVerbs is a list of verb types that supports overriding the
// resulting type.
var resultTypeSupportedVerbs = []string{
"create",
"update",
"get",
"list",
"patch",
}
// Extensions allows to extend the default set of client verbs
// (CRUD+watch+patch+list+deleteCollection) for a given type with custom defined
// verbs. Custom verbs can have custom input and result types and also allow to
// use a sub-resource in a request instead of top-level resource type.
//
// Example:
//
// +genclient:method=UpdateScale,verb=update,subresource=scale,input=Scale,result=Scale
//
// type ReplicaSet struct { ... }
//
// The 'method=UpdateScale' is the name of the client function.
// The 'verb=update' here means the client function will use 'PUT' action.
// The 'subresource=scale' means we will use SubResource template to generate this client function.
// The 'input' is the input type used for creation (function argument).
// The 'result' (not needed in this case) is the result type returned from the
// client function.
//
type extension struct {
// VerbName is the name of the custom verb (Scale, Instantiate, etc..)
VerbName string
// VerbType is the type of the verb (only verbs from SupportedVerbs are
// supported)
VerbType string
// SubResourcePath defines a path to a sub-resource to use in the request.
// (optional)
SubResourcePath string
// InputTypeOverride overrides the input parameter type for the verb. By
// default the original type is used. Overriding the input type only works for
// "create" and "update" verb types. The given type must exists in the same
// package as the original type.
// (optional)
InputTypeOverride string
// ResultTypeOverride overrides the resulting object type for the verb. By
// default the original type is used. Overriding the result type works.
// (optional)
ResultTypeOverride string
}
// IsSubresource indicates if this extension should generate the sub-resource.
func (e *extension) IsSubresource() bool {
return len(e.SubResourcePath) > 0
}
// HasVerb checks if the extension matches the given verb.
func (e *extension) HasVerb(verb string) bool {
return e.VerbType == verb
}
// Input returns the input override package path and the type.
func (e *extension) Input() (string, string) {
parts := strings.Split(e.InputTypeOverride, ".")
return parts[len(parts)-1], strings.Join(parts[0:len(parts)-1], ".")
}
// Result returns the result override package path and the type.
func (e *extension) Result() (string, string) {
parts := strings.Split(e.ResultTypeOverride, ".")
return parts[len(parts)-1], strings.Join(parts[0:len(parts)-1], ".")
}
// Tags represents a genclient configuration for a single type. // Tags represents a genclient configuration for a single type.
type Tags struct { type Tags struct {
// +genclient // +genclient
...@@ -67,6 +158,8 @@ type Tags struct { ...@@ -67,6 +158,8 @@ type Tags struct {
// +genclient:skipVerbs=get,update // +genclient:skipVerbs=get,update
// +genclient:onlyVerbs=create,delete // +genclient:onlyVerbs=create,delete
SkipVerbs []string SkipVerbs []string
// +genclient:method=UpdateScale,verb=update,subresource=scale,input=Scale,result=Scale
Extensions []extension
} }
// HasVerb returns true if we should include the given verb in final client interface and // HasVerb returns true if we should include the given verb in final client interface and
...@@ -103,25 +196,25 @@ func ParseClientGenTags(lines []string) (Tags, error) { ...@@ -103,25 +196,25 @@ func ParseClientGenTags(lines []string) (Tags, error) {
if len(value) > 0 && len(value[0]) > 0 { if len(value) > 0 && len(value[0]) > 0 {
return ret, fmt.Errorf("+genclient=%s is invalid, use //+genclient if you want to generate client or omit it when you want to disable generation", value) return ret, fmt.Errorf("+genclient=%s is invalid, use //+genclient if you want to generate client or omit it when you want to disable generation", value)
} }
_, ret.NonNamespaced = values["genclient:nonNamespaced"] _, ret.NonNamespaced = values[genClientPrefix+"nonNamespaced"]
// Check the old format and error when used // Check the old format and error when used
if value := values["nonNamespaced"]; len(value) > 0 && len(value[0]) > 0 { if value := values["nonNamespaced"]; len(value) > 0 && len(value[0]) > 0 {
return ret, fmt.Errorf("+nonNamespaced=%s is invalid, use //+genclient:nonNamespaced instead", value[0]) return ret, fmt.Errorf("+nonNamespaced=%s is invalid, use //+genclient:nonNamespaced instead", value[0])
} }
_, ret.NoVerbs = values["genclient:noVerbs"] _, ret.NoVerbs = values[genClientPrefix+"noVerbs"]
_, ret.NoStatus = values["genclient:noStatus"] _, ret.NoStatus = values[genClientPrefix+"noStatus"]
onlyVerbs := []string{} onlyVerbs := []string{}
if _, isReadonly := values["genclient:readonly"]; isReadonly { if _, isReadonly := values[genClientPrefix+"readonly"]; isReadonly {
onlyVerbs = ReadonlyVerbs onlyVerbs = ReadonlyVerbs
} }
// Check the old format and error when used // Check the old format and error when used
if value := values["readonly"]; len(value) > 0 && len(value[0]) > 0 { if value := values["readonly"]; len(value) > 0 && len(value[0]) > 0 {
return ret, fmt.Errorf("+readonly=%s is invalid, use //+genclient:readonly instead", value[0]) return ret, fmt.Errorf("+readonly=%s is invalid, use //+genclient:readonly instead", value[0])
} }
if v, exists := values["genclient:skipVerbs"]; exists { if v, exists := values[genClientPrefix+"skipVerbs"]; exists {
ret.SkipVerbs = strings.Split(v[0], ",") ret.SkipVerbs = strings.Split(v[0], ",")
} }
if v, exists := values["genclient:onlyVerbs"]; exists || len(onlyVerbs) > 0 { if v, exists := values[genClientPrefix+"onlyVerbs"]; exists || len(onlyVerbs) > 0 {
if len(v) > 0 { if len(v) > 0 {
onlyVerbs = append(onlyVerbs, strings.Split(v[0], ",")...) onlyVerbs = append(onlyVerbs, strings.Split(v[0], ",")...)
} }
...@@ -146,16 +239,101 @@ func ParseClientGenTags(lines []string) (Tags, error) { ...@@ -146,16 +239,101 @@ func ParseClientGenTags(lines []string) (Tags, error) {
} }
ret.SkipVerbs = skipVerbs ret.SkipVerbs = skipVerbs
} }
var err error
if ret.Extensions, err = parseClientExtensions(values); err != nil {
return ret, err
}
return ret, validateClientGenTags(values) return ret, validateClientGenTags(values)
} }
func parseClientExtensions(tags map[string][]string) ([]extension, error) {
var ret []extension
for name, values := range tags {
if !strings.HasPrefix(name, genClientPrefix+"method") {
continue
}
for _, value := range values {
// the value comes in this form: "Foo,verb=create"
ext := extension{}
parts := strings.Split(value, ",")
if len(parts) == 0 {
return nil, fmt.Errorf("invalid of empty extension verb name: %q", value)
}
// The first part represents the name of the extension
ext.VerbName = parts[0]
if len(ext.VerbName) == 0 {
return nil, fmt.Errorf("must specify a verb name (// +genclient:method=Foo,verb=create)")
}
// Parse rest of the arguments
params := parts[1:]
for _, p := range params {
parts := strings.Split(p, "=")
if len(parts) != 2 {
return nil, fmt.Errorf("invalid extension tag specification %q", p)
}
key, val := strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])
if len(val) == 0 {
return nil, fmt.Errorf("empty value of %q for %q extension", key, ext.VerbName)
}
switch key {
case "verb":
ext.VerbType = val
case "subresource":
ext.SubResourcePath = val
case "input":
ext.InputTypeOverride = val
case "result":
ext.ResultTypeOverride = val
default:
return nil, fmt.Errorf("unknown extension configuration key %q", key)
}
}
// Validate resulting extension configuration
if len(ext.VerbType) == 0 {
return nil, fmt.Errorf("verb type must be specified (use '// +genclient:method=%s,verb=create')", ext.VerbName)
}
if len(ext.ResultTypeOverride) > 0 {
supported := false
for _, v := range resultTypeSupportedVerbs {
if ext.VerbType == v {
supported = true
break
}
}
if !supported {
return nil, fmt.Errorf("%s: result type is not supported for %q verbs (supported verbs: %#v)", ext.VerbName, ext.VerbType, resultTypeSupportedVerbs)
}
}
if len(ext.InputTypeOverride) > 0 {
supported := false
for _, v := range inputTypeSupportedVerbs {
if ext.VerbType == v {
supported = true
break
}
}
if !supported {
return nil, fmt.Errorf("%s: input type is not supported for %q verbs (supported verbs: %#v)", ext.VerbName, ext.VerbType, inputTypeSupportedVerbs)
}
}
for _, t := range unsupportedExtensionVerbs {
if ext.VerbType == t {
return nil, fmt.Errorf("verb %q is not supported by extension generator", ext.VerbType)
}
}
ret = append(ret, ext)
}
}
return ret, nil
}
// validateTags validates that only supported genclient tags were provided. // validateTags validates that only supported genclient tags were provided.
func validateClientGenTags(values map[string][]string) error { func validateClientGenTags(values map[string][]string) error {
for _, k := range supportedTags { for _, k := range supportedTags {
delete(values, k) delete(values, k)
} }
for key := range values { for key := range values {
if strings.HasPrefix(key, "genclient") { if strings.HasPrefix(key, strings.TrimSuffix(genClientPrefix, ":")) {
return errors.New("unknown tag detected: " + key) return errors.New("unknown tag detected: " + key)
} }
} }
......
...@@ -82,3 +82,67 @@ func TestParseTags(t *testing.T) { ...@@ -82,3 +82,67 @@ func TestParseTags(t *testing.T) {
} }
} }
} }
func TestParseTagsExtension(t *testing.T) {
testCases := map[string]struct {
lines []string
expectedExtensions []extension
expectError bool
}{
"simplest extension": {
lines: []string{`+genclient:method=Foo,verb=create`},
expectedExtensions: []extension{{VerbName: "Foo", VerbType: "create"}},
},
"multiple extensions": {
lines: []string{`+genclient:method=Foo,verb=create`, `+genclient:method=Bar,verb=get`},
expectedExtensions: []extension{{VerbName: "Foo", VerbType: "create"}, {VerbName: "Bar", VerbType: "get"}},
},
"extension without verb": {
lines: []string{`+genclient:method`},
expectError: true,
},
"extension without verb type": {
lines: []string{`+genclient:method=Foo`},
expectError: true,
},
"sub-resource extension": {
lines: []string{`+genclient:method=Foo,verb=create,subresource=bar`},
expectedExtensions: []extension{{VerbName: "Foo", VerbType: "create", SubResourcePath: "bar"}},
},
"output type extension": {
lines: []string{`+genclient:method=Foos,verb=list,result=Bars`},
expectedExtensions: []extension{{VerbName: "Foos", VerbType: "list", ResultTypeOverride: "Bars"}},
},
"input type extension": {
lines: []string{`+genclient:method=Foo,verb=update,input=Bar`},
expectedExtensions: []extension{{VerbName: "Foo", VerbType: "update", InputTypeOverride: "Bar"}},
},
"unknown verb type extension": {
lines: []string{`+genclient:method=Foo,verb=explode`},
expectedExtensions: nil,
expectError: true,
},
"invalid verb extension": {
lines: []string{`+genclient:method=Foo,unknown=bar`},
expectedExtensions: nil,
expectError: true,
},
"empty verb extension subresource": {
lines: []string{`+genclient:method=Foo,verb=get,subresource=`},
expectedExtensions: nil,
expectError: true,
},
}
for key, c := range testCases {
result, err := ParseClientGenTags(c.lines)
if err != nil && !c.expectError {
t.Fatalf("[%s] unexpected error: %v", key, err)
}
if err != nil && c.expectError {
t.Logf("[%s] got expected error: %+v", key, err)
}
if !c.expectError && !reflect.DeepEqual(result.Extensions, c.expectedExtensions) {
t.Errorf("[%s] expected %#+v to be %#+v", key, result.Extensions, c.expectedExtensions)
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment