From b7d8dc6f8ee0aeee0f938284361e5a5f24af1e89 Mon Sep 17 00:00:00 2001 From: Thomas Kappler Date: Fri, 21 Jun 2024 12:18:52 +0200 Subject: [PATCH] Extend `infer` with annotations for aliases and resource deprecations (#245) This allows authors of providers built on `infer` to [alias](https://www.pulumi.com/docs/using-pulumi/pulumi-packages/schema/#alias) and [deprecate](https://www.pulumi.com/docs/using-pulumi/pulumi-packages/schema/#resource) resources. --- infer/resource.go | 4 +++ infer/schema.go | 20 ++++++++--- infer/schema_test.go | 46 ++++++++++++++++++++++++++ internal/introspect/annotator.go | 26 ++++++++++++--- internal/introspect/introspect_test.go | 4 +++ 5 files changed, 90 insertions(+), 10 deletions(-) create mode 100644 infer/schema_test.go diff --git a/infer/resource.go b/infer/resource.go index f5890a5c..5ee53a93 100644 --- a/infer/resource.go +++ b/infer/resource.go @@ -280,6 +280,10 @@ type Annotator interface { // mypkg:mymodule:MyResource // SetToken(module, name string) + + AddAlias(module, name string) + + SetResourceDeprecationMessage(message string) } // Annotated is used to describe the fields of an object or a resource. Annotated can be diff --git a/infer/schema.go b/infer/schema.go index 14616a86..6e8afb46 100644 --- a/infer/schema.go +++ b/infer/schema.go @@ -58,6 +58,8 @@ func getAnnotated(t reflect.Type) introspect.Annotator { (*dst).DefaultEnvs[k] = v } dst.Token = src.Token + dst.Aliases = append(dst.Aliases, src.Aliases...) + dst.DeprecationMessage = src.DeprecationMessage } ret := introspect.Annotator{ @@ -86,7 +88,7 @@ func getAnnotated(t reflect.Type) introspect.Annotator { func getResourceSchema[R, I, O any](isComponent bool) (schema.ResourceSpec, multierror.Error) { var r R var errs multierror.Error - descriptions := getAnnotated(reflect.TypeOf(r)) + annotations := getAnnotated(reflect.TypeOf(r)) properties, required, err := propertyListFromType(reflect.TypeOf(new(O)), isComponent) if err != nil { @@ -100,15 +102,23 @@ func getResourceSchema[R, I, O any](isComponent bool) (schema.ResourceSpec, mult errs.Errors = append(errs.Errors, fmt.Errorf("could not serialize input type %T: %w", i, err)) } + var aliases []schema.AliasSpec + for _, alias := range annotations.Aliases { + a := alias + aliases = append(aliases, schema.AliasSpec{Type: &a}) + } + return schema.ResourceSpec{ ObjectTypeSpec: schema.ObjectTypeSpec{ Properties: properties, - Description: descriptions.Descriptions[""], + Description: annotations.Descriptions[""], Required: required, }, - InputProperties: inputProperties, - RequiredInputs: requiredInputs, - IsComponent: isComponent, + InputProperties: inputProperties, + RequiredInputs: requiredInputs, + IsComponent: isComponent, + Aliases: aliases, + DeprecationMessage: annotations.DeprecationMessage, }, errs } diff --git a/infer/schema_test.go b/infer/schema_test.go new file mode 100644 index 00000000..06452367 --- /dev/null +++ b/infer/schema_test.go @@ -0,0 +1,46 @@ +// Copyright 2024, Pulumi Corporation. +// +// 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 infer + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type TestResource struct { +} + +func (r *TestResource) Annotate(a Annotator) { + a.Describe(&r, "This is a test resource.") + a.AddAlias("myMod", "MyAlias") + a.SetResourceDeprecationMessage("This resource is deprecated.") + a.SetToken("myMod", "TheResource") +} + +func TestResourceAnnotations(t *testing.T) { + t.Parallel() + + spec, err := getResourceSchema[TestResource, TestResource, TestResource](false /* isComponent */) + require.NoError(t, err.ErrorOrNil()) + + require.Len(t, spec.Aliases, 1) + assert.Equal(t, "pkg:myMod:MyAlias", *spec.Aliases[0].Type) + + require.Equal(t, "This is a test resource.", spec.Description) + + require.Equal(t, "This resource is deprecated.", spec.DeprecationMessage) +} diff --git a/internal/introspect/annotator.go b/internal/introspect/annotator.go index 354de70d..198455fb 100644 --- a/internal/introspect/annotator.go +++ b/internal/introspect/annotator.go @@ -32,10 +32,12 @@ func NewAnnotator(resource any) Annotator { // Implements the Annotator interface as defined in resource/resource.go type Annotator struct { - Descriptions map[string]string - Defaults map[string]any - DefaultEnvs map[string][]string - Token string + Descriptions map[string]string + Defaults map[string]any + DefaultEnvs map[string][]string + Token string + Aliases []string + DeprecationMessage string matcher FieldMatcher } @@ -88,11 +90,25 @@ func (a *Annotator) SetDefault(i any, defaultValue any, env ...string) { } func (a *Annotator) SetToken(module, token string) { + a.Token = formatToken(module, token) +} + +func (a *Annotator) AddAlias(module, token string) { + a.Aliases = append(a.Aliases, formatToken(module, token)) +} + +func (a *Annotator) SetResourceDeprecationMessage(message string) { + a.DeprecationMessage = message +} + +// Formates module and token into a valid token string. +// Panics when module or token are invalid. +func formatToken(module, token string) string { if !tokens.IsQName(module) { panic(fmt.Sprintf("Module (%q) must comply with %s, but does not", module, tokens.QNameRegexp)) } if !tokens.IsName(token) { panic(fmt.Sprintf("Token (%q) must comply with %s, but does not", token, tokens.NameRegexp)) } - a.Token = fmt.Sprintf("pkg:%s:%s", module, token) + return fmt.Sprintf("pkg:%s:%s", module, token) } diff --git a/internal/introspect/introspect_test.go b/internal/introspect/introspect_test.go index de4ad190..5ca4b518 100644 --- a/internal/introspect/introspect_test.go +++ b/internal/introspect/introspect_test.go @@ -38,6 +38,8 @@ func (m *MyStruct) Annotate(a infer.Annotator) { a.Describe(&m.Fizz, "Fizz is not MyStruct.Foo.") a.SetDefault(&m.Foo, "Fizz") a.SetToken("myMod", "MyToken") + a.SetResourceDeprecationMessage("This resource is deprecated.") + a.AddAlias("myMod", "MyAlias") } func TestParseTag(t *testing.T) { @@ -110,6 +112,8 @@ func TestAnnotate(t *testing.T) { assert.Equal(t, "Fizz is not MyStruct.Foo.", a.Descriptions["fizz"]) assert.Equal(t, "This is MyStruct, but also your struct.", a.Descriptions[""]) assert.Equal(t, "pkg:myMod:MyToken", a.Token) + assert.Equal(t, "This resource is deprecated.", a.DeprecationMessage) + assert.Equal(t, []string{"pkg:myMod:MyAlias"}, a.Aliases) } func TestSetTokenValidation(t *testing.T) {