[codegen] Add type name generation tests. (#7470)

The inputs and expected outputs for the tests are encoded using a
schema. Each property present in the schema forms a testcase; the
expected outputs for each language are stored in each property's
`Language` field with the language name "test". Expected outputs can be
regenerated using `PULUMI_ACCEPT`.
This commit is contained in:
Pat Gavlin 2021-07-09 10:23:10 -07:00 committed by GitHub
parent 8f000c0ca1
commit d07b325138
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 1085 additions and 2 deletions

View file

@ -7,6 +7,7 @@ import (
"github.com/pulumi/pulumi/pkg/v3/codegen/schema"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGeneratePackage(t *testing.T) {
@ -56,3 +57,17 @@ func TestGenerateType(t *testing.T) {
})
}
}
func TestGenerateTypeNames(t *testing.T) {
test.TestTypeNameCodegen(t, "dotnet", func(pkg *schema.Package) test.TypeNameGeneratorFunc {
modules, _, err := generateModuleContextMap("test", pkg)
require.NoError(t, err)
root, ok := modules[""]
require.True(t, ok)
return func(t schema.Type) string {
return root.typeString(t, "", false, false, false)
}
})
}

View file

@ -60,6 +60,26 @@ func TestGeneratePackage(t *testing.T) {
test.TestSDKCodegen(t, "go", generatePackage)
}
func TestGenerateTypeNames(t *testing.T) {
test.TestTypeNameCodegen(t, "go", func(pkg *schema.Package) test.TypeNameGeneratorFunc {
err := pkg.ImportLanguages(map[string]schema.Language{"go": Importer})
require.NoError(t, err)
var goPkgInfo GoPackageInfo
if goInfo, ok := pkg.Language["go"].(GoPackageInfo); ok {
goPkgInfo = goInfo
}
packages := generatePackageContextMap("test", pkg, goPkgInfo)
root, ok := packages[""]
require.True(t, ok)
return func(t schema.Type) string {
return root.typeString(t)
}
})
}
type mocks int
func (mocks) NewResource(args pulumi.MockResourceArgs) (string, resource.PropertyMap, error) {

View file

@ -0,0 +1,429 @@
{
"name": "typetests",
"version": "0.0.1",
"meta": {
"moduleFormat": "(.*)"
},
"config": {},
"types": {
"typetests::primitives": {
"description": "Tests name generation for the primitive schame types. See https://pkg.go.dev/github.com/pulumi/pulumi/pkg/v3/codegen/schema#Type for a complete list",
"properties": {
"any": {
"$ref": "pulumi.json#/Any",
"language": {
"test": {
"expected": {
"dotnet": {
"input": "Input<object>?",
"plain": "object?"
},
"go": {
"input": "pulumi.Input",
"plain": "interface{}"
},
"nodejs": {
"input": "any | undefined",
"plain": "any | undefined"
},
"python": {
"input": "Optional[Any]",
"plain": "Optional[Any]"
}
}
}
}
},
"archive": {
"$ref": "pulumi.json#/Archive",
"language": {
"test": {
"expected": {
"dotnet": {
"input": "Input<Archive>?",
"plain": "Archive?"
},
"go": {
"input": "pulumi.ArchiveInput",
"plain": "pulumi.Archive"
},
"nodejs": {
"input": "pulumi.Input<pulumi.asset.Archive> | undefined",
"plain": "pulumi.asset.Archive | undefined"
},
"python": {
"input": "Optional[pulumi.Input[pulumi.Archive]]",
"plain": "Optional[pulumi.Archive]"
}
}
}
}
},
"asset": {
"$ref": "pulumi.json#/Asset",
"language": {
"test": {
"expected": {
"dotnet": {
"input": "Input<AssetOrArchive>?",
"plain": "AssetOrArchive?"
},
"go": {
"input": "pulumi.AssetOrArchiveInput",
"plain": "pulumi.AssetOrArchive"
},
"nodejs": {
"input": "pulumi.Input<pulumi.asset.Asset | pulumi.asset.Archive> | undefined",
"plain": "pulumi.asset.Asset | pulumi.asset.Archive | undefined"
},
"python": {
"input": "Optional[pulumi.Input[Union[pulumi.Asset, pulumi.Archive]]]",
"plain": "Optional[Union[pulumi.Asset, pulumi.Archive]]"
}
}
}
}
},
"boolean": {
"type": "boolean",
"language": {
"test": {
"expected": {
"dotnet": {
"input": "Input<bool>?",
"plain": "bool?"
},
"go": {
"input": "pulumi.BoolPtrInput",
"plain": "*bool"
},
"nodejs": {
"input": "pulumi.Input<boolean> | undefined",
"plain": "boolean | undefined"
},
"python": {
"input": "Optional[pulumi.Input[bool]]",
"plain": "Optional[bool]"
}
}
}
}
},
"integer": {
"type": "integer",
"language": {
"test": {
"expected": {
"dotnet": {
"input": "Input<int>?",
"plain": "int?"
},
"go": {
"input": "pulumi.IntPtrInput",
"plain": "*int"
},
"nodejs": {
"input": "pulumi.Input<number> | undefined",
"plain": "number | undefined"
},
"python": {
"input": "Optional[pulumi.Input[int]]",
"plain": "Optional[int]"
}
}
}
}
},
"json": {
"$ref": "pulumi.json#/Json",
"language": {
"test": {
"expected": {
"dotnet": {
"input": "InputJson?",
"plain": "System.Text.Json.JsonElement?"
},
"go": {
"input": "pulumi.Input",
"plain": "interface{}"
},
"nodejs": {
"input": "any | undefined",
"plain": "any | undefined"
},
"python": {
"input": "Optional[Any]",
"plain": "Optional[Any]"
}
}
}
}
},
"number": {
"type": "number",
"language": {
"test": {
"expected": {
"dotnet": {
"input": "Input<double>?",
"plain": "double?"
},
"go": {
"input": "pulumi.Float64PtrInput",
"plain": "*float64"
},
"nodejs": {
"input": "pulumi.Input<number> | undefined",
"plain": "number | undefined"
},
"python": {
"input": "Optional[pulumi.Input[float]]",
"plain": "Optional[float]"
}
}
}
}
},
"requiredAny": {
"$ref": "pulumi.json#/Any",
"language": {
"test": {
"expected": {
"dotnet": {
"input": "Input<object>",
"plain": "object"
},
"go": {
"input": "pulumi.Input",
"plain": "interface{}"
},
"nodejs": {
"input": "any",
"plain": "any"
},
"python": {
"input": "Any",
"plain": "Any"
}
}
}
}
},
"requiredArchive": {
"$ref": "pulumi.json#/Archive",
"language": {
"test": {
"expected": {
"dotnet": {
"input": "Input<Archive>",
"plain": "Archive"
},
"go": {
"input": "pulumi.ArchiveInput",
"plain": "pulumi.Archive"
},
"nodejs": {
"input": "pulumi.Input<pulumi.asset.Archive>",
"plain": "pulumi.asset.Archive"
},
"python": {
"input": "pulumi.Input[pulumi.Archive]",
"plain": "pulumi.Archive"
}
}
}
}
},
"requiredAsset": {
"$ref": "pulumi.json#/Asset",
"language": {
"test": {
"expected": {
"dotnet": {
"input": "Input<AssetOrArchive>",
"plain": "AssetOrArchive"
},
"go": {
"input": "pulumi.AssetOrArchiveInput",
"plain": "pulumi.AssetOrArchive"
},
"nodejs": {
"input": "pulumi.Input<pulumi.asset.Asset | pulumi.asset.Archive>",
"plain": "pulumi.asset.Asset | pulumi.asset.Archive"
},
"python": {
"input": "pulumi.Input[Union[pulumi.Asset, pulumi.Archive]]",
"plain": "Union[pulumi.Asset, pulumi.Archive]"
}
}
}
}
},
"requiredBoolean": {
"type": "boolean",
"language": {
"test": {
"expected": {
"dotnet": {
"input": "Input<bool>",
"plain": "bool"
},
"go": {
"input": "pulumi.BoolInput",
"plain": "bool"
},
"nodejs": {
"input": "pulumi.Input<boolean>",
"plain": "boolean"
},
"python": {
"input": "pulumi.Input[bool]",
"plain": "bool"
}
}
}
}
},
"requiredInteger": {
"type": "integer",
"language": {
"test": {
"expected": {
"dotnet": {
"input": "Input<int>",
"plain": "int"
},
"go": {
"input": "pulumi.IntInput",
"plain": "int"
},
"nodejs": {
"input": "pulumi.Input<number>",
"plain": "number"
},
"python": {
"input": "pulumi.Input[int]",
"plain": "int"
}
}
}
}
},
"requiredJson": {
"$ref": "pulumi.json#/Json",
"language": {
"test": {
"expected": {
"dotnet": {
"input": "InputJson",
"plain": "System.Text.Json.JsonElement"
},
"go": {
"input": "pulumi.Input",
"plain": "interface{}"
},
"nodejs": {
"input": "any",
"plain": "any"
},
"python": {
"input": "Any",
"plain": "Any"
}
}
}
}
},
"requiredNumber": {
"type": "number",
"language": {
"test": {
"expected": {
"dotnet": {
"input": "Input<double>",
"plain": "double"
},
"go": {
"input": "pulumi.Float64Input",
"plain": "float64"
},
"nodejs": {
"input": "pulumi.Input<number>",
"plain": "number"
},
"python": {
"input": "pulumi.Input[float]",
"plain": "float"
}
}
}
}
},
"requiredString": {
"type": "string",
"language": {
"test": {
"expected": {
"dotnet": {
"input": "Input<string>",
"plain": "string"
},
"go": {
"input": "pulumi.StringInput",
"plain": "string"
},
"nodejs": {
"input": "pulumi.Input<string>",
"plain": "string"
},
"python": {
"input": "pulumi.Input[str]",
"plain": "str"
}
}
}
}
},
"string": {
"type": "string",
"language": {
"test": {
"expected": {
"dotnet": {
"input": "Input<string>?",
"plain": "string?"
},
"go": {
"input": "pulumi.StringPtrInput",
"plain": "*string"
},
"nodejs": {
"input": "pulumi.Input<string> | undefined",
"plain": "string | undefined"
},
"python": {
"input": "Optional[pulumi.Input[str]]",
"plain": "Optional[str]"
}
}
}
}
}
},
"type": "object",
"required": [
"requiredAny",
"requiredArchive",
"requiredAsset",
"requiredBoolean",
"requiredInteger",
"requiredJson",
"requiredNumber",
"requiredString"
]
}
},
"provider": {
"type": "object"
}
}

View file

@ -0,0 +1,196 @@
package test
import (
"encoding/json"
"os"
"path/filepath"
"testing"
"github.com/pulumi/pulumi/pkg/v3/codegen/schema"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type typeTestCase struct {
Expected map[string]interface{} `json:"expected"`
}
type typeTestImporter int
func (typeTestImporter) ImportDefaultSpec(def *schema.DefaultValue, bytes json.RawMessage) (interface{}, error) {
return bytes, nil
}
func (typeTestImporter) ImportPropertySpec(property *schema.Property, bytes json.RawMessage) (interface{}, error) {
var test typeTestCase
if err := json.Unmarshal([]byte(bytes), &test); err != nil {
return nil, err
}
return &test, nil
}
func (typeTestImporter) ImportObjectTypeSpec(object *schema.ObjectType, bytes json.RawMessage) (interface{}, error) {
return bytes, nil
}
func (typeTestImporter) ImportResourceSpec(resource *schema.Resource, bytes json.RawMessage) (interface{}, error) {
return bytes, nil
}
func (typeTestImporter) ImportFunctionSpec(function *schema.Function, bytes json.RawMessage) (interface{}, error) {
return bytes, nil
}
func (typeTestImporter) ImportPackageSpec(pkg *schema.Package, bytes json.RawMessage) (interface{}, error) {
return bytes, nil
}
type NewTypeNameGeneratorFunc func(pkg *schema.Package) TypeNameGeneratorFunc
type TypeNameGeneratorFunc func(t schema.Type) string
func TestTypeNameCodegen(t *testing.T, language string, newTypeNameGenerator NewTypeNameGeneratorFunc) {
// Read in, decode, and import the schema.
schemaBytes, err := os.ReadFile(filepath.FromSlash("../internal/test/testdata/types.json"))
require.NoError(t, err)
var pkgSpec schema.PackageSpec
err = json.Unmarshal(schemaBytes, &pkgSpec)
require.NoError(t, err)
pkg, err := schema.ImportSpec(pkgSpec, map[string]schema.Language{"test": typeTestImporter(0)})
require.NoError(t, err)
typeName := newTypeNameGenerator(pkg)
if os.Getenv("PULUMI_ACCEPT") == "" {
runTests := func(where string, props []*schema.Property, inputShape bool) {
for _, p := range props {
if testCase, ok := p.Language["test"].(*typeTestCase); ok {
if expected, ok := testCase.Expected[language]; ok {
typ := p.Type
t.Run(where+"/"+p.Name, func(t *testing.T) {
var expectedName string
switch expected := expected.(type) {
case string:
expectedName = expected
case map[string]interface{}:
if inputShape {
expectedName = expected["input"].(string)
} else {
expectedName = expected["plain"].(string)
}
}
assert.Equal(t, expectedName, typeName(typ))
})
}
}
}
}
runTests("#/config", pkg.Config, false)
runTests("#/provider/properties", pkg.Provider.Properties, false)
runTests("#/provider/inputProperties", pkg.Provider.InputProperties, false)
if pkg.Provider.StateInputs != nil {
runTests("#/provider/stateInputs/properties", pkg.Provider.StateInputs.Properties, false)
}
for _, typ := range pkg.Types {
if o, ok := typ.(*schema.ObjectType); ok {
if o.IsInputShape() {
continue
}
runTests("#/types/"+o.Token+"/properties", o.Properties, false)
runTests("#/types/"+o.InputShape.Token+"/properties", o.InputShape.Properties, true)
}
}
for _, r := range pkg.Resources {
runTests("#/resources/"+r.Token+"/properties", r.Properties, false)
runTests("#/resources/"+r.Token+"/inputProperties", r.InputProperties, false)
if r.StateInputs != nil {
runTests("#/resources/"+r.Token+"/properties", r.StateInputs.Properties, false)
}
}
for _, f := range pkg.Functions {
if f.Inputs != nil {
runTests("/functions/"+f.Token+"/inputs/properties", f.Inputs.Properties, false)
}
if f.Outputs != nil {
runTests("/functions/"+f.Token+"/outputs/properties", f.Outputs.Properties, false)
}
}
return
}
updateTests := func(props []*schema.Property) {
for _, p := range props {
testCase, _ := p.Language["test"].(*typeTestCase)
if testCase == nil {
testCase = &typeTestCase{}
p.Language["test"] = testCase
}
if testCase.Expected == nil {
testCase.Expected = map[string]interface{}{}
}
testCase.Expected[language] = typeName(p.Type)
}
}
updateTests(pkg.Config)
updateTests(pkg.Provider.Properties)
updateTests(pkg.Provider.InputProperties)
if pkg.Provider.StateInputs != nil {
updateTests(pkg.Provider.StateInputs.Properties)
}
for _, typ := range pkg.Types {
if o, ok := typ.(*schema.ObjectType); ok {
if o.IsInputShape() {
continue
}
updateTests(o.Properties)
updateTests(o.InputShape.Properties)
for i, p := range o.Properties {
testCase := p.Language["test"].(*typeTestCase)
plain := testCase.Expected[language].(string)
input := o.InputShape.Properties[i].Language["test"].(*typeTestCase).Expected[language].(string)
testCase.Expected[language] = map[string]interface{}{
"plain": plain,
"input": input,
}
}
}
}
for _, r := range pkg.Resources {
updateTests(r.Properties)
updateTests(r.InputProperties)
if r.StateInputs != nil {
updateTests(r.StateInputs.Properties)
}
}
for _, f := range pkg.Functions {
if f.Inputs != nil {
updateTests(f.Inputs.Properties)
}
if f.Outputs != nil {
updateTests(f.Outputs.Properties)
}
}
f, err := os.Create(filepath.FromSlash("../internal/test/testdata/types.json"))
require.NoError(t, err)
defer f.Close()
encoder := json.NewEncoder(f)
encoder.SetEscapeHTML(false)
encoder.SetIndent("", " ")
err = encoder.Encode(pkg)
require.NoError(t, err)
}

View file

@ -5,8 +5,32 @@ import (
"testing"
"github.com/pulumi/pulumi/pkg/v3/codegen/internal/test"
"github.com/pulumi/pulumi/pkg/v3/codegen/schema"
"github.com/stretchr/testify/require"
)
func TestGeneratePackage(t *testing.T) {
test.TestSDKCodegen(t, "nodejs", GeneratePackage)
}
func TestGenerateTypeNames(t *testing.T) {
test.TestTypeNameCodegen(t, "nodejs", func(pkg *schema.Package) test.TypeNameGeneratorFunc {
// Decode node-specific info
err := pkg.ImportLanguages(map[string]schema.Language{"nodejs": Importer})
require.NoError(t, err)
info, _ := pkg.Language["nodejs"].(NodePackageInfo)
modules, info, err := generateModuleContextMap("test", pkg, info, nil)
require.NoError(t, err)
pkg.Language["nodejs"] = info
root, ok := modules[""]
require.True(t, ok)
return func(t schema.Type) string {
return root.typeString(t, false, nil)
}
})
}

View file

@ -4,6 +4,8 @@ import (
"testing"
"github.com/pulumi/pulumi/pkg/v3/codegen/internal/test"
"github.com/pulumi/pulumi/pkg/v3/codegen/schema"
"github.com/stretchr/testify/require"
)
var pathTests = []struct {
@ -34,3 +36,23 @@ func TestRelPathToRelImport(t *testing.T) {
func TestGeneratePackage(t *testing.T) {
test.TestSDKCodegen(t, "python", GeneratePackage)
}
func TestGenerateTypeNames(t *testing.T) {
test.TestTypeNameCodegen(t, "python", func(pkg *schema.Package) test.TypeNameGeneratorFunc {
// Decode python-specific info
err := pkg.ImportLanguages(map[string]schema.Language{"python": Importer})
require.NoError(t, err)
info, _ := pkg.Language["python"].(PackageInfo)
modules, err := generateModuleContextMap("test", pkg, info, nil)
require.NoError(t, err)
root, ok := modules[""]
require.True(t, ok)
return func(t schema.Type) string {
return root.typeString(t, false, false)
}
})
}

View file

@ -15,6 +15,7 @@
package schema
import (
"bytes"
"encoding/json"
"fmt"
"math"
@ -697,6 +698,371 @@ func (pkg *Package) GetType(token string) (Type, bool) {
return t, ok
}
func (pkg *Package) MarshalJSON() (bytes []byte, err error) {
version := ""
if pkg.Version != nil {
version = pkg.Version.String()
}
var metadata *MetadataSpec
if pkg.moduleFormat != nil {
metadata = &MetadataSpec{ModuleFormat: pkg.moduleFormat.String()}
}
spec := PackageSpec{
Name: pkg.Name,
Version: version,
Description: pkg.Description,
Keywords: pkg.Keywords,
Homepage: pkg.Homepage,
License: pkg.License,
Attribution: pkg.Attribution,
Repository: pkg.Repository,
LogoURL: pkg.LogoURL,
PluginDownloadURL: pkg.PluginDownloadURL,
Meta: metadata,
Types: map[string]ComplexTypeSpec{},
Resources: map[string]ResourceSpec{},
Functions: map[string]FunctionSpec{},
}
spec.Config.Required, spec.Config.Variables, err = pkg.marshalProperties(pkg.Config)
if err != nil {
return nil, fmt.Errorf("marshaling package config: %w", err)
}
spec.Provider, err = pkg.marshalResource(pkg.Provider)
if err != nil {
return nil, fmt.Errorf("marshaling provider: %w", err)
}
for _, t := range pkg.Types {
switch t := t.(type) {
case *ObjectType:
if t.IsInputShape() {
continue
}
o, err := pkg.marshalObject(t)
if err != nil {
return nil, fmt.Errorf("marshaling type '%v': %w", t.Token, err)
}
spec.Types[t.Token] = o
case *EnumType:
spec.Types[t.Token] = pkg.marshalEnum(t)
}
}
for _, res := range pkg.Resources {
r, err := pkg.marshalResource(res)
if err != nil {
return nil, fmt.Errorf("marshaling resource '%v': %w", res.Token, err)
}
spec.Resources[res.Token] = r
}
for _, fn := range pkg.Functions {
f, err := pkg.marshalFunction(fn)
if err != nil {
return nil, fmt.Errorf("marshaling function '%v': %w", fn.Token, err)
}
spec.Functions[fn.Token] = f
}
return jsonMarshal(spec)
}
func (pkg *Package) marshalObjectData(
comment string, properties []*Property, language map[string]interface{}) (ObjectTypeSpec, error) {
required, props, err := pkg.marshalProperties(properties)
if err != nil {
return ObjectTypeSpec{}, err
}
lang, err := marshalLanguage(language)
if err != nil {
return ObjectTypeSpec{}, err
}
return ObjectTypeSpec{
Description: comment,
Properties: props,
Type: "object",
Required: required,
Language: lang,
}, nil
}
func (pkg *Package) marshalObject(t *ObjectType) (ComplexTypeSpec, error) {
data, err := pkg.marshalObjectData(t.Comment, t.Properties, t.Language)
if err != nil {
return ComplexTypeSpec{}, err
}
return ComplexTypeSpec{ObjectTypeSpec: data}, nil
}
func (pkg *Package) marshalEnum(t *EnumType) ComplexTypeSpec {
values := make([]EnumValueSpec, len(t.Elements))
for i, el := range t.Elements {
values[i] = EnumValueSpec{
Name: el.Name,
Description: el.Comment,
Value: el.Value,
DeprecationMessage: el.DeprecationMessage,
}
}
return ComplexTypeSpec{
ObjectTypeSpec: ObjectTypeSpec{Type: pkg.marshalType(t.ElementType).Type},
Enum: values,
}
}
func (pkg *Package) marshalResource(r *Resource) (ResourceSpec, error) {
object, err := pkg.marshalObjectData(r.Comment, r.Properties, r.Language)
if err != nil {
return ResourceSpec{}, fmt.Errorf("marshaling properties: %w", err)
}
requiredInputs, inputs, err := pkg.marshalProperties(r.InputProperties)
if err != nil {
return ResourceSpec{}, fmt.Errorf("marshaling input properties: %w", err)
}
var stateInputs *ObjectTypeSpec
if r.StateInputs != nil {
o, err := pkg.marshalObject(r.StateInputs)
if err != nil {
return ResourceSpec{}, fmt.Errorf("marshaling state inputs: %w", err)
}
stateInputs = &o.ObjectTypeSpec
}
var aliases []AliasSpec
for _, a := range r.Aliases {
aliases = append(aliases, AliasSpec{
Name: a.Name,
Project: a.Project,
Type: a.Type,
})
}
var methods map[string]string
if len(r.Methods) != 0 {
methods = map[string]string{}
for _, m := range r.Methods {
methods[m.Name] = m.Function.Token
}
}
return ResourceSpec{
ObjectTypeSpec: object,
InputProperties: inputs,
RequiredInputs: requiredInputs,
StateInputs: stateInputs,
Aliases: aliases,
DeprecationMessage: r.DeprecationMessage,
Language: object.Language,
IsComponent: r.IsComponent,
Methods: methods,
}, nil
}
func (pkg *Package) marshalFunction(f *Function) (FunctionSpec, error) {
var inputs *ObjectTypeSpec
if f.Inputs != nil {
ins, err := pkg.marshalObject(f.Inputs)
if err != nil {
return FunctionSpec{}, fmt.Errorf("marshaling inputs: %w", err)
}
inputs = &ins.ObjectTypeSpec
}
var outputs *ObjectTypeSpec
if f.Outputs != nil {
outs, err := pkg.marshalObject(f.Outputs)
if err != nil {
return FunctionSpec{}, fmt.Errorf("marshaloutg outputs: %w", err)
}
outputs = &outs.ObjectTypeSpec
}
lang, err := marshalLanguage(f.Language)
if err != nil {
return FunctionSpec{}, err
}
return FunctionSpec{
Description: f.Comment,
Inputs: inputs,
Outputs: outputs,
Language: lang,
}, nil
}
func (pkg *Package) marshalProperties(props []*Property) (required []string, specs map[string]PropertySpec, err error) {
if len(props) == 0 {
return
}
specs = make(map[string]PropertySpec, len(props))
for _, p := range props {
typ := p.Type
if t, optional := typ.(*OptionalType); optional {
typ = t.ElementType
} else {
required = append(required, p.Name)
}
var defaultValue interface{}
var defaultSpec *DefaultSpec
if p.DefaultValue != nil {
defaultValue = p.DefaultValue.Value
if len(p.DefaultValue.Environment) != 0 || len(p.DefaultValue.Language) != 0 {
lang, err := marshalLanguage(p.DefaultValue.Language)
if err != nil {
return nil, nil, fmt.Errorf("property '%v': %w", p.Name, err)
}
defaultSpec = &DefaultSpec{
Environment: p.DefaultValue.Environment,
Language: lang,
}
}
}
lang, err := marshalLanguage(p.Language)
if err != nil {
return nil, nil, fmt.Errorf("property '%v': %w", p.Name, err)
}
specs[p.Name] = PropertySpec{
TypeSpec: pkg.marshalType(typ),
Description: p.Comment,
Const: p.ConstValue,
Default: defaultValue,
DefaultInfo: defaultSpec,
DeprecationMessage: p.DeprecationMessage,
Language: lang,
Secret: p.Secret,
}
}
return required, specs, nil
}
func (pkg *Package) marshalType(t Type) TypeSpec {
switch t := t.(type) {
case *InputType:
el := pkg.marshalType(t.ElementType)
el.Plain = false
return el
case *ArrayType:
el := pkg.marshalType(t.ElementType)
return TypeSpec{
Type: "array",
Items: &el,
Plain: true,
}
case *MapType:
el := pkg.marshalType(t.ElementType)
return TypeSpec{
Type: "object",
AdditionalProperties: &el,
Plain: true,
}
case *UnionType:
oneOf := make([]TypeSpec, len(t.ElementTypes))
for i, el := range t.ElementTypes {
oneOf[i] = pkg.marshalType(el)
}
defaultType := ""
if t.DefaultType != nil {
defaultType = pkg.marshalType(t.DefaultType).Type
}
var discriminator *DiscriminatorSpec
if t.Discriminator != "" {
discriminator = &DiscriminatorSpec{
PropertyName: t.Discriminator,
Mapping: t.Mapping,
}
}
return TypeSpec{
Type: defaultType,
OneOf: oneOf,
Discriminator: discriminator,
Plain: true,
}
case *ObjectType:
return TypeSpec{Ref: pkg.marshalTypeRef(t.Package, "types", t.Token)}
case *EnumType:
return TypeSpec{Ref: pkg.marshalTypeRef(t.Package, "types", t.Token)}
case *ResourceType:
return TypeSpec{Ref: pkg.marshalTypeRef(t.Resource.Package, "resources", t.Token)}
case *TokenType:
var defaultType string
if t.UnderlyingType != nil {
defaultType = pkg.marshalType(t.UnderlyingType).Type
}
return TypeSpec{
Type: defaultType,
Ref: t.Token,
}
default:
switch t {
case BoolType:
return TypeSpec{Type: "boolean"}
case StringType:
return TypeSpec{Type: "string"}
case IntType:
return TypeSpec{Type: "integer"}
case NumberType:
return TypeSpec{Type: "number"}
case AnyType:
return TypeSpec{Ref: "pulumi.json#/Any"}
case ArchiveType:
return TypeSpec{Ref: "pulumi.json#/Archive"}
case AssetType:
return TypeSpec{Ref: "pulumi.json#/Asset"}
case JSONType:
return TypeSpec{Ref: "pulumi.json#/Json"}
default:
panic(fmt.Errorf("unexepcted type %v (%T)", t, t))
}
}
}
func (pkg *Package) marshalTypeRef(container *Package, section, token string) string {
token = url.PathEscape(token)
if container == pkg {
return fmt.Sprintf("#/%s/%s", section, token)
}
// TODO(schema): this isn't quite right--it doesn't handle schemas sourced from URLs--but it's good enough for now.
return fmt.Sprintf("/%s/%v/schema.json#/%s/%s", container.Name, container.Version, section, token)
}
func marshalLanguage(lang map[string]interface{}) (map[string]json.RawMessage, error) {
if len(lang) == 0 {
return nil, nil
}
result := map[string]json.RawMessage{}
for name, data := range lang {
bytes, err := jsonMarshal(data)
if err != nil {
return nil, fmt.Errorf("marshaling %v language data: %w", name, err)
}
result[name] = json.RawMessage(bytes)
}
return result, nil
}
// TypeSpec is the serializable form of a reference to a type.
type TypeSpec struct {
// Type is the primitive or composite type, if any. May be "bool", "integer", "number", "string", "array", or
@ -784,7 +1150,7 @@ type ComplexTypeSpec struct {
ObjectTypeSpec
// Enum, if present, is the list of possible values for an enum type.
Enum []*EnumValueSpec `json:"enum,omitempty"`
Enum []EnumValueSpec `json:"enum,omitempty"`
}
// EnumValuesSpec is the serializable form of the values metadata associated with an enum type.
@ -1642,7 +2008,7 @@ func (t *types) bindEnumTypeDetails(enum *EnumType, token string, spec ComplexTy
return nil
}
func (t *types) bindEnumValues(values []*EnumValueSpec, typ Type) ([]*Enum, error) {
func (t *types) bindEnumValues(values []EnumValueSpec, typ Type) ([]*Enum, error) {
var enums []*Enum
errorMessage := func(val interface{}, expectedType string) error {
@ -1977,3 +2343,14 @@ func bindFunctions(specs map[string]FunctionSpec, types *types) ([]*Function, ma
return functions, functionTable, nil
}
func jsonMarshal(v interface{}) ([]byte, error) {
var b bytes.Buffer
enc := json.NewEncoder(&b)
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
if err := enc.Encode(v); err != nil {
return nil, err
}
return b.Bytes(), nil
}