From b5d1d5272246dd0404a0c43c02d46b5f17034d4c Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Fri, 23 Jun 2023 16:08:40 -0400 Subject: [PATCH 01/15] Squashed commit of the following: commit dd5627e7fca3c7200da588215d7d8a7bc06622fd Author: Austin Valle Date: Fri Jun 23 16:01:17 2023 -0400 match openapi spec to name commit 46967909124805805ab8c39f49797b0805d6bbcf Author: Austin Valle Date: Fri Jun 23 15:46:11 2023 -0400 update to latest commit commit b41fedc6f8d6f5361e7d8ad85655c41e8a9f4e32 Author: Austin Valle Date: Fri Jun 23 15:41:12 2023 -0400 update design doc for sets commit bc38b31c2fae9491752f653e9210696cac5cec0e Author: Austin Valle Date: Fri Jun 23 15:30:54 2023 -0400 implement set and run `make testdata` commit 6315b97024e2ec1e61557aff3db82cf4e447fedd Author: Austin Valle Date: Fri Jun 23 15:28:37 2023 -0400 add resource example commit 2aa0bce2fb03c2eee5de31ee75cfa870f4eb348f Author: Austin Valle Date: Fri Jun 23 15:27:48 2023 -0400 update test to include resources commit 60b134e18a3cd79c363759d5631a19d9987d6bea Author: Austin Valle Date: Fri Jun 23 12:26:01 2023 -0400 Add test before changes commit 9a8bdd37a9efe665f54cfe2fe3449985ddd74cba Author: Austin Valle Date: Fri Jun 23 12:04:07 2023 -0400 update go mod with latest `codegen-spec` --- DESIGN.md | 11 +- go.mod | 2 +- go.sum | 4 +- .../edgecase/generated_framework_ir.json | 90 ++++- .../cmd/testdata/edgecase/openapi_spec.yml | 67 +++- .../testdata/edgecase/tfopenapigen_config.yml | 12 +- internal/mapper/oas/attribute.go | 4 +- .../mapper/oas/{list.go => collection.go} | 66 +++- .../oas/{list_test.go => collection_test.go} | 340 +++++++++++++++++- internal/mapper/oas/element_type.go | 2 +- internal/mapper/util/oas_constants.go | 3 + 11 files changed, 576 insertions(+), 25 deletions(-) rename internal/mapper/oas/{list.go => collection.go} (62%) rename internal/mapper/oas/{list_test.go => collection_test.go} (52%) diff --git a/DESIGN.md b/DESIGN.md index ec6ef428..5ac34cd9 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -69,6 +69,8 @@ The response body schema found will be deep merged with the query/path `paramete For a given [OAS type](https://spec.openapis.org/oas/v3.1.0#data-types) and format combination, the following rules will be applied for mapping to Framework attribute types. Not all Framework types are represented natively with OAS, those types are noted below in [Unsupported Attribute Types](#unsupported-attribute-types). +> **NOTE:** All `Type` and `Format` fields below are native to OpenAPI Spec 3.x, with the exception of the format `set`, which is a custom field that only this generator tool is expected to support. + | Type (OAS) | Format (OAS) | Items type (OAS `array`) | Plugin Framework Attribute Type | |------------|---------------------|--------------------------|---------------------------------------------------------------------------------------------| | `integer` | - | - | `Int64Attribute` | @@ -77,7 +79,9 @@ For a given [OAS type](https://spec.openapis.org/oas/v3.1.0#data-types) and form | `string` | - | - | `StringAttribute` | | `boolean` | - | - | `BoolAttribute` | | `array` | - | `object` | `ListNestedAttribute` | -| `array` | - | (all others) | `ListAttribute` (nests with [element types](#oas-to-plugin-framework-element-types)) | +| `array` | - | (all others) | `ListAttribute` (nests with [element types](#oas-to-plugin-framework-element-types)) | +| `array` | `set` | `object` | `SetNestedAttribute` | +| `array` | `set` | (all others) | `SetAttribute` (nests with [element types](#oas-to-plugin-framework-element-types)) | | `object` | - | - | `SingleNestedAttribute` | #### Unsupported Attribute Types @@ -85,12 +89,12 @@ For a given [OAS type](https://spec.openapis.org/oas/v3.1.0#data-types) and form - While the Plugin Framework supports blocks, the Plugin Framework team encourages provider developers to prefer `ListNestedAttribute`, `SetNestedAttribute`, and `SingleNestedAttribute` for new provider development. - `ObjectAttribute` - The generator will default to `SingleNestedAttribute` for object types to provide the additional schema information. -- `SetNestedAttribute`, `SetAttribute`, `MapNestedAttribute`, and `MapAttribute` +- `MapNestedAttribute`, and `MapAttribute` - Mapping for these types is currently not supported, but will be considered in future versions. ### OAS to Plugin Framework Element Types -For attributes that don’t have additional schema information (currently only `ListAttribute`), the following rules will be applied for mapping from OAS type and format combinations, into Framework element types. +For attributes that don't have additional schema information (currently only `ListAttribute` and `SetAttribute`), the following rules will be applied for mapping from OAS type and format combinations, into Framework element types. | Type (OAS) | Format (OAS) | Items type (OAS `array`) | Plugin Framework Element Type | |------------|---------------------|--------------------------|---------------------------------| @@ -100,6 +104,7 @@ For attributes that don’t have additional schema information (currently only ` | `string` | - | - | `StringType` | | `boolean` | - | - | `BoolType` | | `array` | - | (all) | `ListType` | +| `array` | `set` | (all) | `SetType` | | `object` | - | - | `ObjectType` | ### Required, Computed, and Optional diff --git a/go.mod b/go.mod index d2d3dc4a..c6cf2783 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.20 require ( github.com/google/go-cmp v0.5.9 - github.com/hashicorp/terraform-plugin-codegen-spec v0.0.0-20230622133831-8ed4d77d321e + github.com/hashicorp/terraform-plugin-codegen-spec v0.0.0-20230623193314-2a16a9d0b6f3 github.com/mattn/go-colorable v0.1.13 github.com/mitchellh/cli v1.1.5 github.com/pb33f/libopenapi v0.8.5 diff --git a/go.sum b/go.sum index 3597f030..8df99f25 100644 --- a/go.sum +++ b/go.sum @@ -54,8 +54,8 @@ github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brv github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/terraform-plugin-codegen-spec v0.0.0-20230622133831-8ed4d77d321e h1:+BPglv17monTwovwckVW/ZXbi3ZzGLanhQvBccSdWSQ= -github.com/hashicorp/terraform-plugin-codegen-spec v0.0.0-20230622133831-8ed4d77d321e/go.mod h1:4aZFKiOI2xiQFOpeQMMyDL8vRmKAZXHUY4kWol7Fdco= +github.com/hashicorp/terraform-plugin-codegen-spec v0.0.0-20230623193314-2a16a9d0b6f3 h1:6IiMR/31Wu/HvEGhLNzx1wSWvYCU2gkRv39sB24Cpis= +github.com/hashicorp/terraform-plugin-codegen-spec v0.0.0-20230623193314-2a16a9d0b6f3/go.mod h1:4aZFKiOI2xiQFOpeQMMyDL8vRmKAZXHUY4kWol7Fdco= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= diff --git a/internal/cmd/testdata/edgecase/generated_framework_ir.json b/internal/cmd/testdata/edgecase/generated_framework_ir.json index d561271d..9758be2e 100644 --- a/internal/cmd/testdata/edgecase/generated_framework_ir.json +++ b/internal/cmd/testdata/edgecase/generated_framework_ir.json @@ -20,9 +20,97 @@ } ] } + }, + { + "name": "set_collections", + "schema": { + "attributes": [ + { + "name": "set_prop", + "set": { + "computed_optional_required": "computed_optional", + "element_type": { + "string": {} + }, + "description": "This is a set of strings" + } + }, + { + "name": "setnested_prop", + "set_nested": { + "computed_optional_required": "computed_optional", + "nested_object": { + "attributes": [ + { + "name": "bool_prop", + "bool": { + "computed_optional_required": "computed_optional", + "description": "Bool inside a set!" + } + }, + { + "name": "string_prop", + "string": { + "computed_optional_required": "computed_optional", + "description": "String inside a set!", + "sensitive": false + } + } + ] + }, + "description": "This is a set with a nested object" + } + } + ] + } } ], "provider": { "name": "edgecase" - } + }, + "resources": [ + { + "name": "set_collections", + "schema": { + "attributes": [ + { + "name": "set_prop", + "set": { + "computed_optional_required": "computed_optional", + "element_type": { + "string": {} + }, + "description": "This is a set of strings" + } + }, + { + "name": "setnested_prop", + "set_nested": { + "computed_optional_required": "required", + "nested_object": { + "attributes": [ + { + "name": "bool_prop", + "bool": { + "computed_optional_required": "computed_optional", + "description": "Bool inside a set!" + } + }, + { + "name": "string_prop", + "string": { + "computed_optional_required": "computed_optional", + "description": "String inside a set!", + "sensitive": false + } + } + ] + }, + "description": "This is a set with a nested object" + } + } + ] + } + } + ] } diff --git a/internal/cmd/testdata/edgecase/openapi_spec.yml b/internal/cmd/testdata/edgecase/openapi_spec.yml index f21bd138..f5d167f8 100644 --- a/internal/cmd/testdata/edgecase/openapi_spec.yml +++ b/internal/cmd/testdata/edgecase/openapi_spec.yml @@ -8,7 +8,7 @@ paths: get: summary: Test for nested lists (list within a list) responses: - '200': + "200": description: OK content: application/json: @@ -16,11 +16,64 @@ paths: type: object properties: main_list: - description: This list has another list nested underneath! + $ref: "#/components/schemas/multidimensional_array_schema" + /set_collections: + get: + summary: Test for SetNested attributes and Set attributes in a data source + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + setnested_prop: + $ref: "#/components/schemas/setnested_schema" + set_prop: + description: This is a set of strings type: array + format: set items: - type: array - items: - type: string - required: - - main_list \ No newline at end of file + type: string + post: + summary: Test for SetNested attributes and Set attributes in a resource + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - setnested_prop + properties: + setnested_prop: + $ref: "#/components/schemas/setnested_schema" + set_prop: + description: This is a set of strings + type: array + format: set + items: + type: string +components: + schemas: + multidimensional_array_schema: + description: This list has another list nested underneath! + type: array + items: + type: array + items: + type: string + setnested_schema: + description: This is a set with a nested object + type: array + format: set + items: + type: object + properties: + string_prop: + description: String inside a set! + type: string + bool_prop: + description: Bool inside a set! + type: boolean diff --git a/internal/cmd/testdata/edgecase/tfopenapigen_config.yml b/internal/cmd/testdata/edgecase/tfopenapigen_config.yml index aae4315e..baa648c4 100644 --- a/internal/cmd/testdata/edgecase/tfopenapigen_config.yml +++ b/internal/cmd/testdata/edgecase/tfopenapigen_config.yml @@ -1,11 +1,21 @@ -# Ref: https://registry.terraform.io/providers/integrations/github/latest/docs provider: name: edgecase resources: + set_collections: + create: + path: /set_collections + method: POST + read: + path: /set_collections + method: GET data_sources: nested_lists: read: path: /nested_lists + method: GET + set_collections: + read: + path: /set_collections method: GET \ No newline at end of file diff --git a/internal/mapper/oas/attribute.go b/internal/mapper/oas/attribute.go index bcc65a4f..6ad41bdc 100644 --- a/internal/mapper/oas/attribute.go +++ b/internal/mapper/oas/attribute.go @@ -49,7 +49,7 @@ func (s *OASSchema) BuildResourceAttribute(name string, computability schema.Com case util.OAS_type_boolean: return s.BuildBoolResource(name, computability) case util.OAS_type_array: - return s.BuildListResource(name, computability) + return s.BuildCollectionResource(name, computability) case util.OAS_type_object: return s.BuildSingleNestedResource(name, computability) default: @@ -94,7 +94,7 @@ func (s *OASSchema) BuildDataSourceAttribute(name string, computability schema.C case util.OAS_type_boolean: return s.BuildBoolDataSource(name, computability) case util.OAS_type_array: - return s.BuildListDataSource(name, computability) + return s.BuildCollectionDataSource(name, computability) case util.OAS_type_object: return s.BuildSingleNestedDataSource(name, computability) default: diff --git a/internal/mapper/oas/list.go b/internal/mapper/oas/collection.go similarity index 62% rename from internal/mapper/oas/list.go rename to internal/mapper/oas/collection.go index ad124403..97b26a55 100644 --- a/internal/mapper/oas/list.go +++ b/internal/mapper/oas/collection.go @@ -12,7 +12,7 @@ import ( "github.com/hashicorp/terraform-plugin-codegen-spec/schema" ) -func (s *OASSchema) BuildListResource(name string, computability schema.ComputedOptionalRequired) (*resource.Attribute, error) { +func (s *OASSchema) BuildCollectionResource(name string, computability schema.ComputedOptionalRequired) (*resource.Attribute, error) { if !s.Schema.Items.IsA() { return nil, fmt.Errorf("invalid array type for '%s', doesn't have a schema", name) } @@ -28,6 +28,19 @@ func (s *OASSchema) BuildListResource(name string, computability schema.Computed return nil, fmt.Errorf("failed to map nested object schema proxy - %w", err) } + if s.Schema.Format == util.TF_format_set { + return &resource.Attribute{ + Name: name, + SetNested: &resource.SetNestedAttribute{ + NestedObject: resource.NestedAttributeObject{ + Attributes: *objectAttributes, + }, + ComputedOptionalRequired: computability, + Description: s.GetDescription(), + }, + }, nil + } + return &resource.Attribute{ Name: name, ListNested: &resource.ListNestedAttribute{ @@ -42,7 +55,18 @@ func (s *OASSchema) BuildListResource(name string, computability schema.Computed elemType, err := itemSchema.BuildElementType() if err != nil { - return nil, fmt.Errorf("failed to create list elem type - %w", err) + return nil, fmt.Errorf("failed to create collection elem type - %w", err) + } + + if s.Schema.Format == util.TF_format_set { + return &resource.Attribute{ + Name: name, + Set: &resource.SetAttribute{ + ElementType: elemType, + ComputedOptionalRequired: computability, + Description: s.GetDescription(), + }, + }, nil } return &resource.Attribute{ @@ -55,7 +79,7 @@ func (s *OASSchema) BuildListResource(name string, computability schema.Computed }, nil } -func (s *OASSchema) BuildListDataSource(name string, computability schema.ComputedOptionalRequired) (*datasource.Attribute, error) { +func (s *OASSchema) BuildCollectionDataSource(name string, computability schema.ComputedOptionalRequired) (*datasource.Attribute, error) { if !s.Schema.Items.IsA() { return nil, fmt.Errorf("invalid array type for '%s', doesn't have a schema", name) } @@ -71,6 +95,19 @@ func (s *OASSchema) BuildListDataSource(name string, computability schema.Comput return nil, fmt.Errorf("failed to map nested object schema proxy - %w", err) } + if s.Schema.Format == util.TF_format_set { + return &datasource.Attribute{ + Name: name, + SetNested: &datasource.SetNestedAttribute{ + NestedObject: datasource.NestedAttributeObject{ + Attributes: *objectAttributes, + }, + ComputedOptionalRequired: computability, + Description: s.GetDescription(), + }, + }, nil + } + return &datasource.Attribute{ Name: name, ListNested: &datasource.ListNestedAttribute{ @@ -85,7 +122,18 @@ func (s *OASSchema) BuildListDataSource(name string, computability schema.Comput elemType, err := itemSchema.BuildElementType() if err != nil { - return nil, fmt.Errorf("failed to create list elem type - %w", err) + return nil, fmt.Errorf("failed to create collection elem type - %w", err) + } + + if s.Schema.Format == util.TF_format_set { + return &datasource.Attribute{ + Name: name, + Set: &datasource.SetAttribute{ + ElementType: elemType, + ComputedOptionalRequired: computability, + Description: s.GetDescription(), + }, + }, nil } return &datasource.Attribute{ @@ -98,7 +146,7 @@ func (s *OASSchema) BuildListDataSource(name string, computability schema.Comput }, nil } -func (s *OASSchema) BuildListElementType() (schema.ElementType, error) { +func (s *OASSchema) BuildCollectionElementType() (schema.ElementType, error) { if !s.Schema.Items.IsA() { return schema.ElementType{}, fmt.Errorf("invalid array type for nested elem array, doesn't have a schema") } @@ -112,6 +160,14 @@ func (s *OASSchema) BuildListElementType() (schema.ElementType, error) { return schema.ElementType{}, err } + if s.Schema.Format == util.TF_format_set { + return schema.ElementType{ + Set: &schema.SetType{ + ElementType: elemType, + }, + }, nil + } + return schema.ElementType{ List: &schema.ListType{ ElementType: elemType, diff --git a/internal/mapper/oas/list_test.go b/internal/mapper/oas/collection_test.go similarity index 52% rename from internal/mapper/oas/list_test.go rename to internal/mapper/oas/collection_test.go index c7702a3c..eb13c3e5 100644 --- a/internal/mapper/oas/list_test.go +++ b/internal/mapper/oas/collection_test.go @@ -17,7 +17,7 @@ import ( // TODO: add error tests -func TestBuildListResource(t *testing.T) { +func TestBuildCollectionResource(t *testing.T) { t.Parallel() testCases := map[string]struct { @@ -81,6 +81,64 @@ func TestBuildListResource(t *testing.T) { }, }, }, + "set nested attributes": { + schema: &base.Schema{ + Type: []string{"object"}, + Required: []string{"nested_set_prop_required"}, + Properties: map[string]*base.SchemaProxy{ + "nested_set_prop_required": base.CreateSchemaProxy(&base.Schema{ + Type: []string{"array"}, + Format: "set", + Description: "hey there! I'm a set nested array type, required.", + Items: &base.DynamicValue[*base.SchemaProxy, bool]{ + A: base.CreateSchemaProxy(&base.Schema{ + Type: []string{"object"}, + Required: []string{"nested_int64_required"}, + Properties: map[string]*base.SchemaProxy{ + "nested_float64": base.CreateSchemaProxy(&base.Schema{ + Type: []string{"number"}, + Format: "double", + Description: "hey there! I'm a nested float64 type.", + }), + "nested_int64_required": base.CreateSchemaProxy(&base.Schema{ + Type: []string{"integer"}, + Format: "int64", + Description: "hey there! I'm a nested int64 type, required.", + }), + }, + }), + }, + }), + }, + }, + expectedAttributes: &[]resource.Attribute{ + { + Name: "nested_set_prop_required", + SetNested: &resource.SetNestedAttribute{ + ComputedOptionalRequired: schema.Required, + Description: pointer("hey there! I'm a set nested array type, required."), + NestedObject: resource.NestedAttributeObject{ + Attributes: []resource.Attribute{ + { + Name: "nested_float64", + Float64: &resource.Float64Attribute{ + ComputedOptionalRequired: schema.ComputedOptional, + Description: pointer("hey there! I'm a nested float64 type."), + }, + }, + { + Name: "nested_int64_required", + Int64: &resource.Int64Attribute{ + ComputedOptionalRequired: schema.Required, + Description: pointer("hey there! I'm a nested int64 type, required."), + }, + }, + }, + }, + }, + }, + }, + }, "list attributes with list and nested object element type": { schema: &base.Schema{ Type: []string{"object"}, @@ -187,6 +245,116 @@ func TestBuildListResource(t *testing.T) { }, }, }, + "set attributes with set and nested object element type": { + schema: &base.Schema{ + Type: []string{"object"}, + Required: []string{"nested_set_prop_required"}, + Properties: map[string]*base.SchemaProxy{ + "nested_set_prop": base.CreateSchemaProxy(&base.Schema{ + Type: []string{"array"}, + Format: "set", + Description: "hey there! I'm a set of sets.", + Items: &base.DynamicValue[*base.SchemaProxy, bool]{ + A: base.CreateSchemaProxy(&base.Schema{ + Type: []string{"array"}, + Format: "set", + Items: &base.DynamicValue[*base.SchemaProxy, bool]{ + A: base.CreateSchemaProxy(&base.Schema{ + Type: []string{"object"}, + Properties: map[string]*base.SchemaProxy{ + "float64_prop": base.CreateSchemaProxy(&base.Schema{ + Type: []string{"number"}, + Format: "double", + }), + "int64_prop": base.CreateSchemaProxy(&base.Schema{ + Type: []string{"integer"}, + Format: "int64", + }), + }, + }), + }, + }), + }, + }), + "nested_set_prop_required": base.CreateSchemaProxy(&base.Schema{ + Type: []string{"array"}, + Format: "set", + Description: "hey there! I'm a set of sets, required.", + Items: &base.DynamicValue[*base.SchemaProxy, bool]{ + A: base.CreateSchemaProxy(&base.Schema{ + Type: []string{"array"}, + Format: "set", + Items: &base.DynamicValue[*base.SchemaProxy, bool]{ + A: base.CreateSchemaProxy(&base.Schema{ + Type: []string{"object"}, + Properties: map[string]*base.SchemaProxy{ + "bool_prop": base.CreateSchemaProxy(&base.Schema{ + Type: []string{"boolean"}, + }), + "string_prop": base.CreateSchemaProxy(&base.Schema{ + Type: []string{"string"}, + }), + }, + }), + }, + }), + }, + }), + }, + }, + expectedAttributes: &[]resource.Attribute{ + { + Name: "nested_set_prop", + Set: &resource.SetAttribute{ + ComputedOptionalRequired: schema.ComputedOptional, + Description: pointer("hey there! I'm a set of sets."), + ElementType: schema.ElementType{ + Set: &schema.SetType{ + ElementType: schema.ElementType{ + Object: &schema.ObjectType{ + AttributeTypes: []schema.ObjectAttributeType{ + { + Name: "float64_prop", + Float64: &schema.Float64Type{}, + }, + { + Name: "int64_prop", + Int64: &schema.Int64Type{}, + }, + }, + }, + }, + }, + }, + }, + }, + { + Name: "nested_set_prop_required", + Set: &resource.SetAttribute{ + ComputedOptionalRequired: schema.Required, + Description: pointer("hey there! I'm a set of sets, required."), + ElementType: schema.ElementType{ + Set: &schema.SetType{ + ElementType: schema.ElementType{ + Object: &schema.ObjectType{ + AttributeTypes: []schema.ObjectAttributeType{ + { + Name: "bool_prop", + Bool: &schema.BoolType{}, + }, + { + Name: "string_prop", + String: &schema.StringType{}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, } for name, testCase := range testCases { @@ -208,7 +376,7 @@ func TestBuildListResource(t *testing.T) { } } -func TestBuildListDataSource(t *testing.T) { +func TestBuildCollectionDataSource(t *testing.T) { t.Parallel() testCases := map[string]struct { @@ -272,6 +440,64 @@ func TestBuildListDataSource(t *testing.T) { }, }, }, + "set nested attributes": { + schema: &base.Schema{ + Type: []string{"object"}, + Required: []string{"nested_set_prop_required"}, + Properties: map[string]*base.SchemaProxy{ + "nested_set_prop_required": base.CreateSchemaProxy(&base.Schema{ + Type: []string{"array"}, + Format: "set", + Description: "hey there! I'm a set nested array type, required.", + Items: &base.DynamicValue[*base.SchemaProxy, bool]{ + A: base.CreateSchemaProxy(&base.Schema{ + Type: []string{"object"}, + Required: []string{"nested_int64_required"}, + Properties: map[string]*base.SchemaProxy{ + "nested_float64": base.CreateSchemaProxy(&base.Schema{ + Type: []string{"number"}, + Format: "double", + Description: "hey there! I'm a nested float64 type.", + }), + "nested_int64_required": base.CreateSchemaProxy(&base.Schema{ + Type: []string{"integer"}, + Format: "int64", + Description: "hey there! I'm a nested int64 type, required.", + }), + }, + }), + }, + }), + }, + }, + expectedAttributes: &[]datasource.Attribute{ + { + Name: "nested_set_prop_required", + SetNested: &datasource.SetNestedAttribute{ + ComputedOptionalRequired: schema.Required, + Description: pointer("hey there! I'm a set nested array type, required."), + NestedObject: datasource.NestedAttributeObject{ + Attributes: []datasource.Attribute{ + { + Name: "nested_float64", + Float64: &datasource.Float64Attribute{ + ComputedOptionalRequired: schema.ComputedOptional, + Description: pointer("hey there! I'm a nested float64 type."), + }, + }, + { + Name: "nested_int64_required", + Int64: &datasource.Int64Attribute{ + ComputedOptionalRequired: schema.Required, + Description: pointer("hey there! I'm a nested int64 type, required."), + }, + }, + }, + }, + }, + }, + }, + }, "list attributes with list and nested object element type": { schema: &base.Schema{ Type: []string{"object"}, @@ -378,6 +604,116 @@ func TestBuildListDataSource(t *testing.T) { }, }, }, + "set attributes with set and nested object element type": { + schema: &base.Schema{ + Type: []string{"object"}, + Required: []string{"nested_set_prop_required"}, + Properties: map[string]*base.SchemaProxy{ + "nested_set_prop": base.CreateSchemaProxy(&base.Schema{ + Type: []string{"array"}, + Format: "set", + Description: "hey there! I'm a set of sets.", + Items: &base.DynamicValue[*base.SchemaProxy, bool]{ + A: base.CreateSchemaProxy(&base.Schema{ + Type: []string{"array"}, + Format: "set", + Items: &base.DynamicValue[*base.SchemaProxy, bool]{ + A: base.CreateSchemaProxy(&base.Schema{ + Type: []string{"object"}, + Properties: map[string]*base.SchemaProxy{ + "float64_prop": base.CreateSchemaProxy(&base.Schema{ + Type: []string{"number"}, + Format: "double", + }), + "int64_prop": base.CreateSchemaProxy(&base.Schema{ + Type: []string{"integer"}, + Format: "int64", + }), + }, + }), + }, + }), + }, + }), + "nested_set_prop_required": base.CreateSchemaProxy(&base.Schema{ + Type: []string{"array"}, + Format: "set", + Description: "hey there! I'm a set of sets, required.", + Items: &base.DynamicValue[*base.SchemaProxy, bool]{ + A: base.CreateSchemaProxy(&base.Schema{ + Type: []string{"array"}, + Format: "set", + Items: &base.DynamicValue[*base.SchemaProxy, bool]{ + A: base.CreateSchemaProxy(&base.Schema{ + Type: []string{"object"}, + Properties: map[string]*base.SchemaProxy{ + "bool_prop": base.CreateSchemaProxy(&base.Schema{ + Type: []string{"boolean"}, + }), + "string_prop": base.CreateSchemaProxy(&base.Schema{ + Type: []string{"string"}, + }), + }, + }), + }, + }), + }, + }), + }, + }, + expectedAttributes: &[]datasource.Attribute{ + { + Name: "nested_set_prop", + Set: &datasource.SetAttribute{ + ComputedOptionalRequired: schema.ComputedOptional, + Description: pointer("hey there! I'm a set of sets."), + ElementType: schema.ElementType{ + Set: &schema.SetType{ + ElementType: schema.ElementType{ + Object: &schema.ObjectType{ + AttributeTypes: []schema.ObjectAttributeType{ + { + Name: "float64_prop", + Float64: &schema.Float64Type{}, + }, + { + Name: "int64_prop", + Int64: &schema.Int64Type{}, + }, + }, + }, + }, + }, + }, + }, + }, + { + Name: "nested_set_prop_required", + Set: &datasource.SetAttribute{ + ComputedOptionalRequired: schema.Required, + Description: pointer("hey there! I'm a set of sets, required."), + ElementType: schema.ElementType{ + Set: &schema.SetType{ + ElementType: schema.ElementType{ + Object: &schema.ObjectType{ + AttributeTypes: []schema.ObjectAttributeType{ + { + Name: "bool_prop", + Bool: &schema.BoolType{}, + }, + { + Name: "string_prop", + String: &schema.StringType{}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, } for name, testCase := range testCases { diff --git a/internal/mapper/oas/element_type.go b/internal/mapper/oas/element_type.go index becf239f..2f5ef02d 100644 --- a/internal/mapper/oas/element_type.go +++ b/internal/mapper/oas/element_type.go @@ -21,7 +21,7 @@ func (s *OASSchema) BuildElementType() (schema.ElementType, error) { case util.OAS_type_boolean: return s.BuildBoolElementType() case util.OAS_type_array: - return s.BuildListElementType() + return s.BuildCollectionElementType() case util.OAS_type_object: return s.BuildObjectElementType() diff --git a/internal/mapper/util/oas_constants.go b/internal/mapper/util/oas_constants.go index 8ab26f32..66e59f04 100644 --- a/internal/mapper/util/oas_constants.go +++ b/internal/mapper/util/oas_constants.go @@ -21,6 +21,9 @@ const ( OAS_format_float = "float" OAS_format_password = "password" + // Custom format for SetNested and Set attributes + TF_format_set = "set" + OAS_mediatype_json = "application/json" OAS_response_code_ok = "200" From f10f0ee19c6a25afd302b288ac9f0998855083c4 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Fri, 23 Jun 2023 16:36:07 -0400 Subject: [PATCH 02/15] map for edge case test --- .../edgecase/generated_framework_ir.json | 42 ++++++++++++++++ .../cmd/testdata/edgecase/openapi_spec.yml | 48 +++++++++++++++++++ .../testdata/edgecase/tfopenapigen_config.yml | 11 +++++ 3 files changed, 101 insertions(+) diff --git a/internal/cmd/testdata/edgecase/generated_framework_ir.json b/internal/cmd/testdata/edgecase/generated_framework_ir.json index 9758be2e..ddf7b667 100644 --- a/internal/cmd/testdata/edgecase/generated_framework_ir.json +++ b/internal/cmd/testdata/edgecase/generated_framework_ir.json @@ -1,5 +1,26 @@ { "datasources": [ + { + "name": "map_test", + "schema": { + "attributes": [ + { + "name": "map_prop", + "single_nested": { + "computed_optional_required": "computed_optional", + "description": "This is a map of strings" + } + }, + { + "name": "mapnested_prop", + "single_nested": { + "computed_optional_required": "computed_optional", + "description": "This is a map with a nested object" + } + } + ] + } + }, { "name": "nested_lists", "schema": { @@ -69,6 +90,27 @@ "name": "edgecase" }, "resources": [ + { + "name": "map_test", + "schema": { + "attributes": [ + { + "name": "map_prop", + "single_nested": { + "computed_optional_required": "computed_optional", + "description": "This is a map of strings" + } + }, + { + "name": "mapnested_prop", + "single_nested": { + "computed_optional_required": "required", + "description": "This is a map with a nested object" + } + } + ] + } + }, { "name": "set_collections", "schema": { diff --git a/internal/cmd/testdata/edgecase/openapi_spec.yml b/internal/cmd/testdata/edgecase/openapi_spec.yml index f5d167f8..065341e1 100644 --- a/internal/cmd/testdata/edgecase/openapi_spec.yml +++ b/internal/cmd/testdata/edgecase/openapi_spec.yml @@ -17,6 +17,42 @@ paths: properties: main_list: $ref: "#/components/schemas/multidimensional_array_schema" + /map_test: + get: + summary: Test for MapNested attributes and Map attributes in a data source + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + mapnested_prop: + $ref: "#/components/schemas/mapnested_schema" + map_prop: + description: This is a map of strings + type: object + additionalProperties: + type: string + post: + summary: Test for MapNested attributes and Map attributes in a resource + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - mapnested_prop + properties: + mapnested_prop: + $ref: "#/components/schemas/mapnested_schema" + map_prop: + description: This is a map of strings + type: object + additionalProperties: + type: string /set_collections: get: summary: Test for SetNested attributes and Set attributes in a data source @@ -77,3 +113,15 @@ components: bool_prop: description: Bool inside a set! type: boolean + mapnested_schema: + description: This is a map with a nested object + type: object + additionalProperties: + type: object + properties: + string_prop: + description: String inside a map! + type: string + bool_prop: + description: Bool inside a map! + type: boolean diff --git a/internal/cmd/testdata/edgecase/tfopenapigen_config.yml b/internal/cmd/testdata/edgecase/tfopenapigen_config.yml index baa648c4..68caa2bf 100644 --- a/internal/cmd/testdata/edgecase/tfopenapigen_config.yml +++ b/internal/cmd/testdata/edgecase/tfopenapigen_config.yml @@ -9,6 +9,13 @@ resources: read: path: /set_collections method: GET + map_test: + create: + path: /map_test + method: POST + read: + path: /map_test + method: GET data_sources: nested_lists: @@ -18,4 +25,8 @@ data_sources: set_collections: read: path: /set_collections + method: GET + map_test: + read: + path: /map_test method: GET \ No newline at end of file From 2b49e1fb0d71d8a4df0667d418be87384a1e8cf3 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Fri, 23 Jun 2023 17:22:11 -0400 Subject: [PATCH 03/15] rename to nested collections --- .../cmd/testdata/edgecase/generated_framework_ir.json | 2 +- internal/cmd/testdata/edgecase/openapi_spec.yml | 8 ++++---- internal/cmd/testdata/edgecase/tfopenapigen_config.yml | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/internal/cmd/testdata/edgecase/generated_framework_ir.json b/internal/cmd/testdata/edgecase/generated_framework_ir.json index ddf7b667..462e70f5 100644 --- a/internal/cmd/testdata/edgecase/generated_framework_ir.json +++ b/internal/cmd/testdata/edgecase/generated_framework_ir.json @@ -22,7 +22,7 @@ } }, { - "name": "nested_lists", + "name": "nested_collections", "schema": { "attributes": [ { diff --git a/internal/cmd/testdata/edgecase/openapi_spec.yml b/internal/cmd/testdata/edgecase/openapi_spec.yml index 065341e1..369e1081 100644 --- a/internal/cmd/testdata/edgecase/openapi_spec.yml +++ b/internal/cmd/testdata/edgecase/openapi_spec.yml @@ -4,9 +4,9 @@ info: description: This is a fake API spec that was built to test some of the less common API schema structures and their mapping in the OpenAPI to Framework code generator version: 1.0.0 paths: - /nested_lists: + /nested_collections: get: - summary: Test for nested lists (list within a list) + summary: Test for nested collections (list within a list, set within a list, map within a list) responses: "200": description: OK @@ -16,7 +16,7 @@ paths: type: object properties: main_list: - $ref: "#/components/schemas/multidimensional_array_schema" + $ref: "#/components/schemas/nested_collection_schema" /map_test: get: summary: Test for MapNested attributes and Map attributes in a data source @@ -93,7 +93,7 @@ paths: type: string components: schemas: - multidimensional_array_schema: + nested_collection_schema: description: This list has another list nested underneath! type: array items: diff --git a/internal/cmd/testdata/edgecase/tfopenapigen_config.yml b/internal/cmd/testdata/edgecase/tfopenapigen_config.yml index 68caa2bf..65e87dce 100644 --- a/internal/cmd/testdata/edgecase/tfopenapigen_config.yml +++ b/internal/cmd/testdata/edgecase/tfopenapigen_config.yml @@ -18,9 +18,9 @@ resources: method: GET data_sources: - nested_lists: + nested_collections: read: - path: /nested_lists + path: /nested_collections method: GET set_collections: read: From f750729c9795f5a4d9d2166dda2075941cc8c62d Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Fri, 23 Jun 2023 17:24:25 -0400 Subject: [PATCH 04/15] change name --- internal/cmd/testdata/edgecase/generated_framework_ir.json | 2 +- internal/cmd/testdata/edgecase/openapi_spec.yml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/cmd/testdata/edgecase/generated_framework_ir.json b/internal/cmd/testdata/edgecase/generated_framework_ir.json index 462e70f5..b1719231 100644 --- a/internal/cmd/testdata/edgecase/generated_framework_ir.json +++ b/internal/cmd/testdata/edgecase/generated_framework_ir.json @@ -26,7 +26,7 @@ "schema": { "attributes": [ { - "name": "main_list", + "name": "double_nested_list", "list": { "computed_optional_required": "computed_optional", "element_type": { diff --git a/internal/cmd/testdata/edgecase/openapi_spec.yml b/internal/cmd/testdata/edgecase/openapi_spec.yml index 369e1081..4a5c9220 100644 --- a/internal/cmd/testdata/edgecase/openapi_spec.yml +++ b/internal/cmd/testdata/edgecase/openapi_spec.yml @@ -15,8 +15,8 @@ paths: schema: type: object properties: - main_list: - $ref: "#/components/schemas/nested_collection_schema" + double_nested_list: + $ref: "#/components/schemas/double_nested_list_schema" /map_test: get: summary: Test for MapNested attributes and Map attributes in a data source @@ -93,7 +93,7 @@ paths: type: string components: schemas: - nested_collection_schema: + double_nested_list_schema: description: This list has another list nested underneath! type: array items: From 91475649beea86b72098775c345d1f20a0a70400 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Fri, 23 Jun 2023 18:11:53 -0400 Subject: [PATCH 05/15] add support for mapnested and map attribute --- .../edgecase/generated_framework_ir.json | 117 +++++++++++++++-- .../cmd/testdata/edgecase/openapi_spec.yml | 45 ++++++- .../testdata/edgecase/tfopenapigen_config.yml | 10 +- internal/mapper/oas/attribute.go | 11 ++ internal/mapper/oas/collection.go | 13 +- internal/mapper/oas/element_type.go | 6 + internal/mapper/oas/map.go | 122 ++++++++++++++++++ 7 files changed, 307 insertions(+), 17 deletions(-) create mode 100644 internal/mapper/oas/map.go diff --git a/internal/cmd/testdata/edgecase/generated_framework_ir.json b/internal/cmd/testdata/edgecase/generated_framework_ir.json index b1719231..701fc134 100644 --- a/internal/cmd/testdata/edgecase/generated_framework_ir.json +++ b/internal/cmd/testdata/edgecase/generated_framework_ir.json @@ -6,15 +6,37 @@ "attributes": [ { "name": "map_prop", - "single_nested": { + "map": { "computed_optional_required": "computed_optional", + "element_type": { + "string": {} + }, "description": "This is a map of strings" } }, { "name": "mapnested_prop", - "single_nested": { + "map_nested": { "computed_optional_required": "computed_optional", + "nested_object": { + "attributes": [ + { + "name": "bool_prop", + "bool": { + "computed_optional_required": "computed_optional", + "description": "Bool inside a map!" + } + }, + { + "name": "string_prop", + "string": { + "computed_optional_required": "computed_optional", + "description": "String inside a map!", + "sensitive": false + } + } + ] + }, "description": "This is a map with a nested object" } } @@ -36,14 +58,71 @@ } } }, - "description": "This list has another list nested underneath!" + "description": "This list has a list nested underneath!" + } + }, + { + "name": "double_nested_map", + "list": { + "computed_optional_required": "computed_optional", + "element_type": { + "map": { + "element_type": { + "string": {} + } + } + }, + "description": "This list has a map nested underneath!" + } + }, + { + "name": "double_nested_set", + "set": { + "computed_optional_required": "computed_optional", + "element_type": { + "set": { + "element_type": { + "string": {} + } + } + }, + "description": "This set has a set nested underneath!" + } + }, + { + "name": "triple_nested_map", + "list": { + "computed_optional_required": "computed_optional", + "element_type": { + "set": { + "element_type": { + "map": { + "element_type": { + "object": { + "attribute_types": [ + { + "name": "bool_prop", + "bool": {} + }, + { + "name": "string_prop", + "string": {} + } + ] + } + } + } + } + } + }, + "description": "This list has a set of maps nested underneath!" } } ] } }, { - "name": "set_collections", + "name": "set_test", "schema": { "attributes": [ { @@ -96,15 +175,37 @@ "attributes": [ { "name": "map_prop", - "single_nested": { + "map": { "computed_optional_required": "computed_optional", + "element_type": { + "string": {} + }, "description": "This is a map of strings" } }, { "name": "mapnested_prop", - "single_nested": { - "computed_optional_required": "required", + "map_nested": { + "computed_optional_required": "computed_optional", + "nested_object": { + "attributes": [ + { + "name": "bool_prop", + "bool": { + "computed_optional_required": "computed_optional", + "description": "Bool inside a map!" + } + }, + { + "name": "string_prop", + "string": { + "computed_optional_required": "computed_optional", + "description": "String inside a map!", + "sensitive": false + } + } + ] + }, "description": "This is a map with a nested object" } } @@ -112,7 +213,7 @@ } }, { - "name": "set_collections", + "name": "set_test", "schema": { "attributes": [ { diff --git a/internal/cmd/testdata/edgecase/openapi_spec.yml b/internal/cmd/testdata/edgecase/openapi_spec.yml index 4a5c9220..5a61d051 100644 --- a/internal/cmd/testdata/edgecase/openapi_spec.yml +++ b/internal/cmd/testdata/edgecase/openapi_spec.yml @@ -17,6 +17,12 @@ paths: properties: double_nested_list: $ref: "#/components/schemas/double_nested_list_schema" + double_nested_set: + $ref: "#/components/schemas/double_nested_set_schema" + double_nested_map: + $ref: "#/components/schemas/double_nested_map_schema" + triple_nested_map: + $ref: "#/components/schemas/triple_nested_map_schema" /map_test: get: summary: Test for MapNested attributes and Map attributes in a data source @@ -53,7 +59,7 @@ paths: type: object additionalProperties: type: string - /set_collections: + /set_test: get: summary: Test for SetNested attributes and Set attributes in a data source responses: @@ -94,12 +100,47 @@ paths: components: schemas: double_nested_list_schema: - description: This list has another list nested underneath! + description: This list has a list nested underneath! type: array items: type: array items: type: string + double_nested_set_schema: + description: This set has a set nested underneath! + type: array + format: set + items: + type: array + format: set + items: + type: string + double_nested_map_schema: + description: This list has a map nested underneath! + type: array + items: + type: object + description: This is a double nested map of strings + additionalProperties: + type: string + triple_nested_map_schema: + description: This list has a set of maps nested underneath! + type: array + items: + type: array + format: set + items: + type: object + description: This is a triple nested map of objects + additionalProperties: + type: object + properties: + string_prop: + description: String inside a map! + type: string + bool_prop: + description: Bool inside a map! + type: boolean setnested_schema: description: This is a set with a nested object type: array diff --git a/internal/cmd/testdata/edgecase/tfopenapigen_config.yml b/internal/cmd/testdata/edgecase/tfopenapigen_config.yml index 65e87dce..18bd1c07 100644 --- a/internal/cmd/testdata/edgecase/tfopenapigen_config.yml +++ b/internal/cmd/testdata/edgecase/tfopenapigen_config.yml @@ -2,12 +2,12 @@ provider: name: edgecase resources: - set_collections: + set_test: create: - path: /set_collections + path: /set_test method: POST read: - path: /set_collections + path: /set_test method: GET map_test: create: @@ -22,9 +22,9 @@ data_sources: read: path: /nested_collections method: GET - set_collections: + set_test: read: - path: /set_collections + path: /set_test method: GET map_test: read: diff --git a/internal/mapper/oas/attribute.go b/internal/mapper/oas/attribute.go index 6ad41bdc..11df8ce5 100644 --- a/internal/mapper/oas/attribute.go +++ b/internal/mapper/oas/attribute.go @@ -10,6 +10,7 @@ import ( "github.com/hashicorp/terraform-plugin-codegen-spec/datasource" "github.com/hashicorp/terraform-plugin-codegen-spec/resource" "github.com/hashicorp/terraform-plugin-codegen-spec/schema" + "github.com/pb33f/libopenapi/datamodel/high/base" ) func (s *OASSchema) BuildResourceAttributes() (*[]resource.Attribute, error) { @@ -51,6 +52,11 @@ func (s *OASSchema) BuildResourceAttribute(name string, computability schema.Com case util.OAS_type_array: return s.BuildCollectionResource(name, computability) case util.OAS_type_object: + // Maps are defined as `type: object`, with an additionalProperties field that is a schema (can also be a boolean, hence the type assertion) + _, ok := s.Schema.AdditionalProperties.(*base.SchemaProxy) + if ok { + return s.BuildMapResource(name, computability) + } return s.BuildSingleNestedResource(name, computability) default: return nil, fmt.Errorf("invalid schema type '%s'", s.Type) @@ -96,6 +102,11 @@ func (s *OASSchema) BuildDataSourceAttribute(name string, computability schema.C case util.OAS_type_array: return s.BuildCollectionDataSource(name, computability) case util.OAS_type_object: + // Maps are defined as `type: object`, with an additionalProperties field that is a schema (can also be a boolean, hence the type assertion) + _, ok := s.Schema.AdditionalProperties.(*base.SchemaProxy) + if ok { + return s.BuildMapDataSource(name, computability) + } return s.BuildSingleNestedDataSource(name, computability) default: return nil, fmt.Errorf("invalid schema type '%s'", s.Type) diff --git a/internal/mapper/oas/collection.go b/internal/mapper/oas/collection.go index 97b26a55..edd102e0 100644 --- a/internal/mapper/oas/collection.go +++ b/internal/mapper/oas/collection.go @@ -10,6 +10,7 @@ import ( "github.com/hashicorp/terraform-plugin-codegen-spec/datasource" "github.com/hashicorp/terraform-plugin-codegen-spec/resource" "github.com/hashicorp/terraform-plugin-codegen-spec/schema" + "github.com/pb33f/libopenapi/datamodel/high/base" ) func (s *OASSchema) BuildCollectionResource(name string, computability schema.ComputedOptionalRequired) (*resource.Attribute, error) { @@ -22,7 +23,11 @@ func (s *OASSchema) BuildCollectionResource(name string, computability schema.Co return nil, fmt.Errorf("failed to build array items schema for '%s'", name) } - if itemSchema.Type == util.OAS_type_object { + // Maps are defined as `type: object`, with an additionalProperties field that is a schema (can also be a boolean, hence the type assertion) + // If a list has a map as an item, then we can only handle that scenario with a ListAttribute + _, hasAdditionalProperties := itemSchema.Schema.AdditionalProperties.(*base.SchemaProxy) + + if itemSchema.Type == util.OAS_type_object && !hasAdditionalProperties { objectAttributes, err := itemSchema.BuildResourceAttributes() if err != nil { return nil, fmt.Errorf("failed to map nested object schema proxy - %w", err) @@ -89,7 +94,11 @@ func (s *OASSchema) BuildCollectionDataSource(name string, computability schema. return nil, fmt.Errorf("failed to build array items schema for '%s'", name) } - if itemSchema.Type == util.OAS_type_object { + // Maps are defined as `type: object`, with an additionalProperties field that is a schema (can also be a boolean, hence the type assertion) + // If a list has a map as an item, then we can only handle that scenario with a ListAttribute + _, hasAdditionalProperties := itemSchema.Schema.AdditionalProperties.(*base.SchemaProxy) + + if itemSchema.Type == util.OAS_type_object && !hasAdditionalProperties { objectAttributes, err := itemSchema.BuildDataSourceAttributes() if err != nil { return nil, fmt.Errorf("failed to map nested object schema proxy - %w", err) diff --git a/internal/mapper/oas/element_type.go b/internal/mapper/oas/element_type.go index 2f5ef02d..d38a4138 100644 --- a/internal/mapper/oas/element_type.go +++ b/internal/mapper/oas/element_type.go @@ -8,6 +8,7 @@ import ( "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/mapper/util" "github.com/hashicorp/terraform-plugin-codegen-spec/schema" + "github.com/pb33f/libopenapi/datamodel/high/base" ) func (s *OASSchema) BuildElementType() (schema.ElementType, error) { @@ -23,6 +24,11 @@ func (s *OASSchema) BuildElementType() (schema.ElementType, error) { case util.OAS_type_array: return s.BuildCollectionElementType() case util.OAS_type_object: + // Maps are defined as `type: object`, with an additionalProperties field that is a schema (can also be a boolean, hence the type assertion) + _, ok := s.Schema.AdditionalProperties.(*base.SchemaProxy) + if ok { + return s.BuildMapElementType() + } return s.BuildObjectElementType() default: diff --git a/internal/mapper/oas/map.go b/internal/mapper/oas/map.go new file mode 100644 index 00000000..17b3978f --- /dev/null +++ b/internal/mapper/oas/map.go @@ -0,0 +1,122 @@ +package oas + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/mapper/util" + "github.com/hashicorp/terraform-plugin-codegen-spec/datasource" + "github.com/hashicorp/terraform-plugin-codegen-spec/resource" + "github.com/hashicorp/terraform-plugin-codegen-spec/schema" + "github.com/pb33f/libopenapi/datamodel/high/base" +) + +func (s *OASSchema) BuildMapResource(name string, computability schema.ComputedOptionalRequired) (*resource.Attribute, error) { + mapSchemaProxy, ok := s.Schema.AdditionalProperties.(*base.SchemaProxy) + if !ok { + return nil, fmt.Errorf("invalid map schema, expected type *base.SchemaProxy, got: %T", s.Schema.AdditionalProperties) + } + + mapSchema, err := BuildSchema(mapSchemaProxy, SchemaOpts{}, s.GlobalSchemaOpts) + if err != nil { + return nil, fmt.Errorf("error building map schema proxy - %w", err) + } + + if mapSchema.Type == util.OAS_type_object { + mapAttributes, err := mapSchema.BuildResourceAttributes() + if err != nil { + return nil, fmt.Errorf("failed to build nested object schema proxy - %w", err) + } + return &resource.Attribute{ + Name: name, + MapNested: &resource.MapNestedAttribute{ + NestedObject: resource.NestedAttributeObject{ + Attributes: *mapAttributes, + }, + ComputedOptionalRequired: s.GetComputability(name), + Description: s.GetDescription(), + }, + }, nil + } + + elemType, err := mapSchema.BuildElementType() + if err != nil { + return nil, fmt.Errorf("failed to create map elem type - %w", err) + } + + return &resource.Attribute{ + Name: name, + Map: &resource.MapAttribute{ + ElementType: elemType, + ComputedOptionalRequired: s.GetComputability(name), + Description: s.GetDescription(), + }, + }, nil +} + +func (s *OASSchema) BuildMapDataSource(name string, computability schema.ComputedOptionalRequired) (*datasource.Attribute, error) { + // This should be done at a higher level as well, so this is just for safety + mapSchemaProxy, ok := s.Schema.AdditionalProperties.(*base.SchemaProxy) + if !ok { + return nil, fmt.Errorf("invalid map schema, expected type *base.SchemaProxy, got: %T", s.Schema.AdditionalProperties) + } + + mapSchema, err := BuildSchema(mapSchemaProxy, SchemaOpts{}, s.GlobalSchemaOpts) + if err != nil { + return nil, fmt.Errorf("error building map schema proxy - %w", err) + } + + if mapSchema.Type == util.OAS_type_object { + mapAttributes, err := mapSchema.BuildDataSourceAttributes() + if err != nil { + return nil, fmt.Errorf("failed to build nested object schema proxy - %w", err) + } + return &datasource.Attribute{ + Name: name, + MapNested: &datasource.MapNestedAttribute{ + NestedObject: datasource.NestedAttributeObject{ + Attributes: *mapAttributes, + }, + ComputedOptionalRequired: s.GetComputability(name), + Description: s.GetDescription(), + }, + }, nil + } + + elemType, err := mapSchema.BuildElementType() + if err != nil { + return nil, fmt.Errorf("failed to create map elem type - %w", err) + } + + return &datasource.Attribute{ + Name: name, + Map: &datasource.MapAttribute{ + ElementType: elemType, + ComputedOptionalRequired: s.GetComputability(name), + Description: s.GetDescription(), + }, + }, nil +} + +func (s *OASSchema) BuildMapElementType() (schema.ElementType, error) { + // This should be done at a higher level as well, so this is just for safety + mapSchemaProxy, ok := s.Schema.AdditionalProperties.(*base.SchemaProxy) + if !ok { + return schema.ElementType{}, fmt.Errorf("invalid map schema, expected type *base.SchemaProxy, got: %T", s.Schema.AdditionalProperties) + } + + mapSchema, err := BuildSchema(mapSchemaProxy, SchemaOpts{}, s.GlobalSchemaOpts) + if err != nil { + return schema.ElementType{}, fmt.Errorf("error building map schema proxy - %w", err) + } + + elemType, err := mapSchema.BuildElementType() + if err != nil { + return schema.ElementType{}, fmt.Errorf("failed to create map elem type - %w", err) + } + + return schema.ElementType{ + Map: &schema.MapType{ + ElementType: elemType, + }, + }, nil +} From 9c33612a5907aebdb21a638f38e60c2e0b3ee406 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Mon, 26 Jun 2023 10:38:01 -0400 Subject: [PATCH 06/15] change to floats --- internal/cmd/testdata/edgecase/generated_framework_ir.json | 4 ++-- internal/cmd/testdata/edgecase/openapi_spec.yml | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/internal/cmd/testdata/edgecase/generated_framework_ir.json b/internal/cmd/testdata/edgecase/generated_framework_ir.json index 701fc134..15237cda 100644 --- a/internal/cmd/testdata/edgecase/generated_framework_ir.json +++ b/internal/cmd/testdata/edgecase/generated_framework_ir.json @@ -9,9 +9,9 @@ "map": { "computed_optional_required": "computed_optional", "element_type": { - "string": {} + "float64": {} }, - "description": "This is a map of strings" + "description": "This is a map of floats" } }, { diff --git a/internal/cmd/testdata/edgecase/openapi_spec.yml b/internal/cmd/testdata/edgecase/openapi_spec.yml index 5a61d051..894885a7 100644 --- a/internal/cmd/testdata/edgecase/openapi_spec.yml +++ b/internal/cmd/testdata/edgecase/openapi_spec.yml @@ -37,10 +37,11 @@ paths: mapnested_prop: $ref: "#/components/schemas/mapnested_schema" map_prop: - description: This is a map of strings + description: This is a map of floats type: object additionalProperties: - type: string + type: number + format: float post: summary: Test for MapNested attributes and Map attributes in a resource requestBody: From c01f5565dfb8e5bb15d926558864a06e2812a313 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Mon, 26 Jun 2023 10:54:01 -0400 Subject: [PATCH 07/15] moved comments to map.go --- internal/mapper/oas/attribute.go | 2 -- internal/mapper/oas/element_type.go | 1 - internal/mapper/oas/map.go | 8 ++++++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/internal/mapper/oas/attribute.go b/internal/mapper/oas/attribute.go index 11df8ce5..0b3a63a4 100644 --- a/internal/mapper/oas/attribute.go +++ b/internal/mapper/oas/attribute.go @@ -52,7 +52,6 @@ func (s *OASSchema) BuildResourceAttribute(name string, computability schema.Com case util.OAS_type_array: return s.BuildCollectionResource(name, computability) case util.OAS_type_object: - // Maps are defined as `type: object`, with an additionalProperties field that is a schema (can also be a boolean, hence the type assertion) _, ok := s.Schema.AdditionalProperties.(*base.SchemaProxy) if ok { return s.BuildMapResource(name, computability) @@ -102,7 +101,6 @@ func (s *OASSchema) BuildDataSourceAttribute(name string, computability schema.C case util.OAS_type_array: return s.BuildCollectionDataSource(name, computability) case util.OAS_type_object: - // Maps are defined as `type: object`, with an additionalProperties field that is a schema (can also be a boolean, hence the type assertion) _, ok := s.Schema.AdditionalProperties.(*base.SchemaProxy) if ok { return s.BuildMapDataSource(name, computability) diff --git a/internal/mapper/oas/element_type.go b/internal/mapper/oas/element_type.go index d38a4138..2984f22b 100644 --- a/internal/mapper/oas/element_type.go +++ b/internal/mapper/oas/element_type.go @@ -24,7 +24,6 @@ func (s *OASSchema) BuildElementType() (schema.ElementType, error) { case util.OAS_type_array: return s.BuildCollectionElementType() case util.OAS_type_object: - // Maps are defined as `type: object`, with an additionalProperties field that is a schema (can also be a boolean, hence the type assertion) _, ok := s.Schema.AdditionalProperties.(*base.SchemaProxy) if ok { return s.BuildMapElementType() diff --git a/internal/mapper/oas/map.go b/internal/mapper/oas/map.go index 17b3978f..02ddb8d5 100644 --- a/internal/mapper/oas/map.go +++ b/internal/mapper/oas/map.go @@ -11,6 +11,8 @@ import ( ) func (s *OASSchema) BuildMapResource(name string, computability schema.ComputedOptionalRequired) (*resource.Attribute, error) { + // Maps are detected as `type: object`, with an `additionalProperties` field that is a schema `additionalProperties` can + // also be a boolean (which we should ignore and map to an SingleNestedAttribute), so calling functions should perform this type assertion as well. mapSchemaProxy, ok := s.Schema.AdditionalProperties.(*base.SchemaProxy) if !ok { return nil, fmt.Errorf("invalid map schema, expected type *base.SchemaProxy, got: %T", s.Schema.AdditionalProperties) @@ -54,7 +56,8 @@ func (s *OASSchema) BuildMapResource(name string, computability schema.ComputedO } func (s *OASSchema) BuildMapDataSource(name string, computability schema.ComputedOptionalRequired) (*datasource.Attribute, error) { - // This should be done at a higher level as well, so this is just for safety + // Maps are detected as `type: object`, with an `additionalProperties` field that is a schema `additionalProperties` can + // also be a boolean (which we should ignore and map to an SingleNestedAttribute), so calling functions should perform this type assertion as well. mapSchemaProxy, ok := s.Schema.AdditionalProperties.(*base.SchemaProxy) if !ok { return nil, fmt.Errorf("invalid map schema, expected type *base.SchemaProxy, got: %T", s.Schema.AdditionalProperties) @@ -98,7 +101,8 @@ func (s *OASSchema) BuildMapDataSource(name string, computability schema.Compute } func (s *OASSchema) BuildMapElementType() (schema.ElementType, error) { - // This should be done at a higher level as well, so this is just for safety + // Maps are detected as `type: object`, with an `additionalProperties` field that is a schema `additionalProperties` can + // also be a boolean (which we should ignore and map to an ObjectType), so calling functions should perform this type assertion as well. mapSchemaProxy, ok := s.Schema.AdditionalProperties.(*base.SchemaProxy) if !ok { return schema.ElementType{}, fmt.Errorf("invalid map schema, expected type *base.SchemaProxy, got: %T", s.Schema.AdditionalProperties) From 8c9fb8b63d233c2c77723d5661bee323f66c4cc2 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Mon, 26 Jun 2023 10:56:45 -0400 Subject: [PATCH 08/15] remove comments in collection --- internal/mapper/oas/collection.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/internal/mapper/oas/collection.go b/internal/mapper/oas/collection.go index edd102e0..37f7ec63 100644 --- a/internal/mapper/oas/collection.go +++ b/internal/mapper/oas/collection.go @@ -23,8 +23,6 @@ func (s *OASSchema) BuildCollectionResource(name string, computability schema.Co return nil, fmt.Errorf("failed to build array items schema for '%s'", name) } - // Maps are defined as `type: object`, with an additionalProperties field that is a schema (can also be a boolean, hence the type assertion) - // If a list has a map as an item, then we can only handle that scenario with a ListAttribute _, hasAdditionalProperties := itemSchema.Schema.AdditionalProperties.(*base.SchemaProxy) if itemSchema.Type == util.OAS_type_object && !hasAdditionalProperties { @@ -94,8 +92,6 @@ func (s *OASSchema) BuildCollectionDataSource(name string, computability schema. return nil, fmt.Errorf("failed to build array items schema for '%s'", name) } - // Maps are defined as `type: object`, with an additionalProperties field that is a schema (can also be a boolean, hence the type assertion) - // If a list has a map as an item, then we can only handle that scenario with a ListAttribute _, hasAdditionalProperties := itemSchema.Schema.AdditionalProperties.(*base.SchemaProxy) if itemSchema.Type == util.OAS_type_object && !hasAdditionalProperties { From 5f505620fa5b17ffbef8de26f4b17992ebb91911 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Mon, 26 Jun 2023 11:12:20 -0400 Subject: [PATCH 09/15] move to shared function and update docs --- internal/mapper/oas/attribute.go | 7 ++----- internal/mapper/oas/collection.go | 11 ++++------- internal/mapper/oas/element_type.go | 4 +--- internal/mapper/oas/map.go | 6 +++--- internal/mapper/oas/oas_schema.go | 7 +++++++ 5 files changed, 17 insertions(+), 18 deletions(-) diff --git a/internal/mapper/oas/attribute.go b/internal/mapper/oas/attribute.go index 0b3a63a4..ba60328c 100644 --- a/internal/mapper/oas/attribute.go +++ b/internal/mapper/oas/attribute.go @@ -10,7 +10,6 @@ import ( "github.com/hashicorp/terraform-plugin-codegen-spec/datasource" "github.com/hashicorp/terraform-plugin-codegen-spec/resource" "github.com/hashicorp/terraform-plugin-codegen-spec/schema" - "github.com/pb33f/libopenapi/datamodel/high/base" ) func (s *OASSchema) BuildResourceAttributes() (*[]resource.Attribute, error) { @@ -52,8 +51,7 @@ func (s *OASSchema) BuildResourceAttribute(name string, computability schema.Com case util.OAS_type_array: return s.BuildCollectionResource(name, computability) case util.OAS_type_object: - _, ok := s.Schema.AdditionalProperties.(*base.SchemaProxy) - if ok { + if s.IsMap() { return s.BuildMapResource(name, computability) } return s.BuildSingleNestedResource(name, computability) @@ -101,8 +99,7 @@ func (s *OASSchema) BuildDataSourceAttribute(name string, computability schema.C case util.OAS_type_array: return s.BuildCollectionDataSource(name, computability) case util.OAS_type_object: - _, ok := s.Schema.AdditionalProperties.(*base.SchemaProxy) - if ok { + if s.IsMap() { return s.BuildMapDataSource(name, computability) } return s.BuildSingleNestedDataSource(name, computability) diff --git a/internal/mapper/oas/collection.go b/internal/mapper/oas/collection.go index 37f7ec63..c31f9786 100644 --- a/internal/mapper/oas/collection.go +++ b/internal/mapper/oas/collection.go @@ -10,7 +10,6 @@ import ( "github.com/hashicorp/terraform-plugin-codegen-spec/datasource" "github.com/hashicorp/terraform-plugin-codegen-spec/resource" "github.com/hashicorp/terraform-plugin-codegen-spec/schema" - "github.com/pb33f/libopenapi/datamodel/high/base" ) func (s *OASSchema) BuildCollectionResource(name string, computability schema.ComputedOptionalRequired) (*resource.Attribute, error) { @@ -23,9 +22,8 @@ func (s *OASSchema) BuildCollectionResource(name string, computability schema.Co return nil, fmt.Errorf("failed to build array items schema for '%s'", name) } - _, hasAdditionalProperties := itemSchema.Schema.AdditionalProperties.(*base.SchemaProxy) - - if itemSchema.Type == util.OAS_type_object && !hasAdditionalProperties { + // If the items schema is a map (i.e. additionalProperties set to a schema), it cannot be a NestedAttribute + if itemSchema.Type == util.OAS_type_object && !itemSchema.IsMap() { objectAttributes, err := itemSchema.BuildResourceAttributes() if err != nil { return nil, fmt.Errorf("failed to map nested object schema proxy - %w", err) @@ -92,9 +90,8 @@ func (s *OASSchema) BuildCollectionDataSource(name string, computability schema. return nil, fmt.Errorf("failed to build array items schema for '%s'", name) } - _, hasAdditionalProperties := itemSchema.Schema.AdditionalProperties.(*base.SchemaProxy) - - if itemSchema.Type == util.OAS_type_object && !hasAdditionalProperties { + // If the items schema is a map (i.e. additionalProperties set to a schema), it cannot be a NestedAttribute + if itemSchema.Type == util.OAS_type_object && !itemSchema.IsMap() { objectAttributes, err := itemSchema.BuildDataSourceAttributes() if err != nil { return nil, fmt.Errorf("failed to map nested object schema proxy - %w", err) diff --git a/internal/mapper/oas/element_type.go b/internal/mapper/oas/element_type.go index 2984f22b..64a8d4a4 100644 --- a/internal/mapper/oas/element_type.go +++ b/internal/mapper/oas/element_type.go @@ -8,7 +8,6 @@ import ( "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/mapper/util" "github.com/hashicorp/terraform-plugin-codegen-spec/schema" - "github.com/pb33f/libopenapi/datamodel/high/base" ) func (s *OASSchema) BuildElementType() (schema.ElementType, error) { @@ -24,8 +23,7 @@ func (s *OASSchema) BuildElementType() (schema.ElementType, error) { case util.OAS_type_array: return s.BuildCollectionElementType() case util.OAS_type_object: - _, ok := s.Schema.AdditionalProperties.(*base.SchemaProxy) - if ok { + if s.IsMap() { return s.BuildMapElementType() } return s.BuildObjectElementType() diff --git a/internal/mapper/oas/map.go b/internal/mapper/oas/map.go index 02ddb8d5..cf1b9f7d 100644 --- a/internal/mapper/oas/map.go +++ b/internal/mapper/oas/map.go @@ -12,7 +12,7 @@ import ( func (s *OASSchema) BuildMapResource(name string, computability schema.ComputedOptionalRequired) (*resource.Attribute, error) { // Maps are detected as `type: object`, with an `additionalProperties` field that is a schema `additionalProperties` can - // also be a boolean (which we should ignore and map to an SingleNestedAttribute), so calling functions should perform this type assertion as well. + // also be a boolean (which we should ignore and map to an SingleNestedAttribute), so calling functions should call s.IsMap() first. mapSchemaProxy, ok := s.Schema.AdditionalProperties.(*base.SchemaProxy) if !ok { return nil, fmt.Errorf("invalid map schema, expected type *base.SchemaProxy, got: %T", s.Schema.AdditionalProperties) @@ -57,7 +57,7 @@ func (s *OASSchema) BuildMapResource(name string, computability schema.ComputedO func (s *OASSchema) BuildMapDataSource(name string, computability schema.ComputedOptionalRequired) (*datasource.Attribute, error) { // Maps are detected as `type: object`, with an `additionalProperties` field that is a schema `additionalProperties` can - // also be a boolean (which we should ignore and map to an SingleNestedAttribute), so calling functions should perform this type assertion as well. + // also be a boolean (which we should ignore and map to an SingleNestedAttribute), so calling functions should call s.IsMap() first. mapSchemaProxy, ok := s.Schema.AdditionalProperties.(*base.SchemaProxy) if !ok { return nil, fmt.Errorf("invalid map schema, expected type *base.SchemaProxy, got: %T", s.Schema.AdditionalProperties) @@ -102,7 +102,7 @@ func (s *OASSchema) BuildMapDataSource(name string, computability schema.Compute func (s *OASSchema) BuildMapElementType() (schema.ElementType, error) { // Maps are detected as `type: object`, with an `additionalProperties` field that is a schema `additionalProperties` can - // also be a boolean (which we should ignore and map to an ObjectType), so calling functions should perform this type assertion as well. + // also be a boolean (which we should ignore and map to an ObjectType), so calling functions should call s.IsMap() first. mapSchemaProxy, ok := s.Schema.AdditionalProperties.(*base.SchemaProxy) if !ok { return schema.ElementType{}, fmt.Errorf("invalid map schema, expected type *base.SchemaProxy, got: %T", s.Schema.AdditionalProperties) diff --git a/internal/mapper/oas/oas_schema.go b/internal/mapper/oas/oas_schema.go index 83e6c01f..d7bb8e5d 100644 --- a/internal/mapper/oas/oas_schema.go +++ b/internal/mapper/oas/oas_schema.go @@ -40,6 +40,13 @@ type SchemaOpts struct { OverrideDescription string } +// IsMap will perform a type assertion on the `additionalProperties` field to determine if a map type +// is appropriate. See: https://json-schema.org/understanding-json-schema/reference/object.html#additional-properties +func (s *OASSchema) IsMap() bool { + _, isMap := s.Schema.AdditionalProperties.(*base.SchemaProxy) + return isMap +} + func (s *OASSchema) GetDescription() *string { if s.SchemaOpts.OverrideDescription != "" { return &s.SchemaOpts.OverrideDescription From 138f58cc780144b630fd864d6af32059d250f4ec Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Mon, 26 Jun 2023 11:33:59 -0400 Subject: [PATCH 10/15] add collection tests --- internal/mapper/oas/collection_test.go | 212 +++++++++++++++++++++++++ 1 file changed, 212 insertions(+) diff --git a/internal/mapper/oas/collection_test.go b/internal/mapper/oas/collection_test.go index eb13c3e5..ee3082c4 100644 --- a/internal/mapper/oas/collection_test.go +++ b/internal/mapper/oas/collection_test.go @@ -355,6 +355,112 @@ func TestBuildCollectionResource(t *testing.T) { }, }, }, + "list and set attribute - nested map results in element type": { + schema: &base.Schema{ + Type: []string{"object"}, + Properties: map[string]*base.SchemaProxy{ + "list_with_map": base.CreateSchemaProxy(&base.Schema{ + Type: []string{"array"}, + Description: "hey there! I'm a list with a nested map of objects.", + Items: &base.DynamicValue[*base.SchemaProxy, bool]{ + A: base.CreateSchemaProxy(&base.Schema{ + Type: []string{"object"}, + AdditionalProperties: base.CreateSchemaProxy(&base.Schema{ + Type: []string{"object"}, + Properties: map[string]*base.SchemaProxy{ + "nested_boolean": base.CreateSchemaProxy(&base.Schema{ + Type: []string{"boolean"}, + Description: "this won't be added, since it will map to element type", + }), + "nested_string": base.CreateSchemaProxy(&base.Schema{ + Type: []string{"string"}, + Description: "this won't be added, since it will map to element type", + }), + }, + }), + }), + }, + }), + "set_with_map": base.CreateSchemaProxy(&base.Schema{ + Type: []string{"array"}, + Format: "set", + Description: "hey there! I'm a set with a nested map of objects.", + Items: &base.DynamicValue[*base.SchemaProxy, bool]{ + A: base.CreateSchemaProxy(&base.Schema{ + Type: []string{"object"}, + AdditionalProperties: base.CreateSchemaProxy(&base.Schema{ + Type: []string{"object"}, + Properties: map[string]*base.SchemaProxy{ + "nested_float64": base.CreateSchemaProxy(&base.Schema{ + Type: []string{"number"}, + Format: "double", + Description: "this won't be added, since it will map to element type", + }), + "nested_int64": base.CreateSchemaProxy(&base.Schema{ + Type: []string{"integer"}, + Format: "int64", + Description: "this won't be added, since it will map to element type", + }), + }, + }), + }), + }, + }), + }, + }, + expectedAttributes: &[]resource.Attribute{ + { + Name: "list_with_map", + List: &resource.ListAttribute{ + ComputedOptionalRequired: schema.ComputedOptional, + Description: pointer("hey there! I'm a list with a nested map of objects."), + ElementType: schema.ElementType{ + Map: &schema.MapType{ + ElementType: schema.ElementType{ + Object: &schema.ObjectType{ + AttributeTypes: []schema.ObjectAttributeType{ + { + Name: "nested_boolean", + Bool: &schema.BoolType{}, + }, + { + Name: "nested_string", + String: &schema.StringType{}, + }, + }, + }, + }, + }, + }, + }, + }, + { + Name: "set_with_map", + Set: &resource.SetAttribute{ + ComputedOptionalRequired: schema.ComputedOptional, + Description: pointer("hey there! I'm a set with a nested map of objects."), + ElementType: schema.ElementType{ + Map: &schema.MapType{ + ElementType: schema.ElementType{ + Object: &schema.ObjectType{ + AttributeTypes: []schema.ObjectAttributeType{ + { + Name: "nested_float64", + Float64: &schema.Float64Type{}, + }, + { + Name: "nested_int64", + Int64: &schema.Int64Type{}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, } for name, testCase := range testCases { @@ -714,6 +820,112 @@ func TestBuildCollectionDataSource(t *testing.T) { }, }, }, + "list and set attribute - nested map results in element type": { + schema: &base.Schema{ + Type: []string{"object"}, + Properties: map[string]*base.SchemaProxy{ + "list_with_map": base.CreateSchemaProxy(&base.Schema{ + Type: []string{"array"}, + Description: "hey there! I'm a list with a nested map of objects.", + Items: &base.DynamicValue[*base.SchemaProxy, bool]{ + A: base.CreateSchemaProxy(&base.Schema{ + Type: []string{"object"}, + AdditionalProperties: base.CreateSchemaProxy(&base.Schema{ + Type: []string{"object"}, + Properties: map[string]*base.SchemaProxy{ + "nested_boolean": base.CreateSchemaProxy(&base.Schema{ + Type: []string{"boolean"}, + Description: "this won't be added, since it will map to element type", + }), + "nested_string": base.CreateSchemaProxy(&base.Schema{ + Type: []string{"string"}, + Description: "this won't be added, since it will map to element type", + }), + }, + }), + }), + }, + }), + "set_with_map": base.CreateSchemaProxy(&base.Schema{ + Type: []string{"array"}, + Format: "set", + Description: "hey there! I'm a set with a nested map of objects.", + Items: &base.DynamicValue[*base.SchemaProxy, bool]{ + A: base.CreateSchemaProxy(&base.Schema{ + Type: []string{"object"}, + AdditionalProperties: base.CreateSchemaProxy(&base.Schema{ + Type: []string{"object"}, + Properties: map[string]*base.SchemaProxy{ + "nested_float64": base.CreateSchemaProxy(&base.Schema{ + Type: []string{"number"}, + Format: "double", + Description: "this won't be added, since it will map to element type", + }), + "nested_int64": base.CreateSchemaProxy(&base.Schema{ + Type: []string{"integer"}, + Format: "int64", + Description: "this won't be added, since it will map to element type", + }), + }, + }), + }), + }, + }), + }, + }, + expectedAttributes: &[]datasource.Attribute{ + { + Name: "list_with_map", + List: &datasource.ListAttribute{ + ComputedOptionalRequired: schema.ComputedOptional, + Description: pointer("hey there! I'm a list with a nested map of objects."), + ElementType: schema.ElementType{ + Map: &schema.MapType{ + ElementType: schema.ElementType{ + Object: &schema.ObjectType{ + AttributeTypes: []schema.ObjectAttributeType{ + { + Name: "nested_boolean", + Bool: &schema.BoolType{}, + }, + { + Name: "nested_string", + String: &schema.StringType{}, + }, + }, + }, + }, + }, + }, + }, + }, + { + Name: "set_with_map", + Set: &datasource.SetAttribute{ + ComputedOptionalRequired: schema.ComputedOptional, + Description: pointer("hey there! I'm a set with a nested map of objects."), + ElementType: schema.ElementType{ + Map: &schema.MapType{ + ElementType: schema.ElementType{ + Object: &schema.ObjectType{ + AttributeTypes: []schema.ObjectAttributeType{ + { + Name: "nested_float64", + Float64: &schema.Float64Type{}, + }, + { + Name: "nested_int64", + Int64: &schema.Int64Type{}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, } for name, testCase := range testCases { From 412b66f51d48f6ec63a0a8ce31dd9c6a9198697a Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Mon, 26 Jun 2023 11:56:26 -0400 Subject: [PATCH 11/15] fix computability bug --- .../cmd/testdata/edgecase/generated_framework_ir.json | 2 +- internal/mapper/oas/map.go | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/cmd/testdata/edgecase/generated_framework_ir.json b/internal/cmd/testdata/edgecase/generated_framework_ir.json index 15237cda..76c766f5 100644 --- a/internal/cmd/testdata/edgecase/generated_framework_ir.json +++ b/internal/cmd/testdata/edgecase/generated_framework_ir.json @@ -186,7 +186,7 @@ { "name": "mapnested_prop", "map_nested": { - "computed_optional_required": "computed_optional", + "computed_optional_required": "required", "nested_object": { "attributes": [ { diff --git a/internal/mapper/oas/map.go b/internal/mapper/oas/map.go index cf1b9f7d..834a7e81 100644 --- a/internal/mapper/oas/map.go +++ b/internal/mapper/oas/map.go @@ -34,7 +34,7 @@ func (s *OASSchema) BuildMapResource(name string, computability schema.ComputedO NestedObject: resource.NestedAttributeObject{ Attributes: *mapAttributes, }, - ComputedOptionalRequired: s.GetComputability(name), + ComputedOptionalRequired: computability, Description: s.GetDescription(), }, }, nil @@ -49,7 +49,7 @@ func (s *OASSchema) BuildMapResource(name string, computability schema.ComputedO Name: name, Map: &resource.MapAttribute{ ElementType: elemType, - ComputedOptionalRequired: s.GetComputability(name), + ComputedOptionalRequired: computability, Description: s.GetDescription(), }, }, nil @@ -79,7 +79,7 @@ func (s *OASSchema) BuildMapDataSource(name string, computability schema.Compute NestedObject: datasource.NestedAttributeObject{ Attributes: *mapAttributes, }, - ComputedOptionalRequired: s.GetComputability(name), + ComputedOptionalRequired: computability, Description: s.GetDescription(), }, }, nil @@ -94,7 +94,7 @@ func (s *OASSchema) BuildMapDataSource(name string, computability schema.Compute Name: name, Map: &datasource.MapAttribute{ ElementType: elemType, - ComputedOptionalRequired: s.GetComputability(name), + ComputedOptionalRequired: computability, Description: s.GetDescription(), }, }, nil From 81d1c1dd4d4eab36a8fc66119d9f46b5cd16b404 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Mon, 26 Jun 2023 11:58:43 -0400 Subject: [PATCH 12/15] add map resource and data source unit tests --- internal/mapper/oas/map_test.go | 326 ++++++++++++++++++++++++++++++++ 1 file changed, 326 insertions(+) diff --git a/internal/mapper/oas/map_test.go b/internal/mapper/oas/map_test.go index 0989363a..d33ea92d 100644 --- a/internal/mapper/oas/map_test.go +++ b/internal/mapper/oas/map_test.go @@ -1 +1,327 @@ package oas_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/mapper/oas" + "github.com/hashicorp/terraform-plugin-codegen-spec/datasource" + "github.com/hashicorp/terraform-plugin-codegen-spec/resource" + "github.com/hashicorp/terraform-plugin-codegen-spec/schema" + "github.com/pb33f/libopenapi/datamodel/high/base" +) + +// TODO: add error tests + +func TestBuildMapResource(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema *base.Schema + expectedAttributes *[]resource.Attribute + }{ + "map nested attribute with props": { + schema: &base.Schema{ + Type: []string{"object"}, + Properties: map[string]*base.SchemaProxy{ + "nested_map_prop": base.CreateSchemaProxy(&base.Schema{ + Type: []string{"object"}, + Description: "hey there! I'm a map nested type.", + AdditionalProperties: base.CreateSchemaProxy(&base.Schema{ + Type: []string{"object"}, + Required: []string{"nested_password_required"}, + Properties: map[string]*base.SchemaProxy{ + "nested_obj_prop": base.CreateSchemaProxy(&base.Schema{ + Type: []string{"object"}, + Required: []string{"nested_int64_required"}, + Description: "hey there! I'm a single nested object type.", + Properties: map[string]*base.SchemaProxy{ + "nested_float64": base.CreateSchemaProxy(&base.Schema{ + Type: []string{"number"}, + Format: "double", + Description: "hey there! I'm a nested float64 type.", + }), + "nested_int64_required": base.CreateSchemaProxy(&base.Schema{ + Type: []string{"integer"}, + Format: "int64", + Description: "hey there! I'm a nested int64 type, required.", + }), + }, + }), + "nested_password_required": base.CreateSchemaProxy(&base.Schema{ + Type: []string{"string"}, + Format: "password", + Description: "hey there! I'm a nested string type, required.", + }), + }, + }), + }), + }, + }, + expectedAttributes: &[]resource.Attribute{ + { + Name: "nested_map_prop", + MapNested: &resource.MapNestedAttribute{ + NestedObject: resource.NestedAttributeObject{ + Attributes: []resource.Attribute{ + { + Name: "nested_obj_prop", + SingleNested: &resource.SingleNestedAttribute{ + Attributes: []resource.Attribute{ + { + Name: "nested_float64", + Float64: &resource.Float64Attribute{ + ComputedOptionalRequired: schema.ComputedOptional, + Description: pointer("hey there! I'm a nested float64 type."), + }, + }, + { + Name: "nested_int64_required", + Int64: &resource.Int64Attribute{ + ComputedOptionalRequired: schema.Required, + Description: pointer("hey there! I'm a nested int64 type, required."), + }, + }, + }, + ComputedOptionalRequired: schema.ComputedOptional, + Description: pointer("hey there! I'm a single nested object type."), + }, + }, + { + Name: "nested_password_required", + String: &resource.StringAttribute{ + ComputedOptionalRequired: schema.Required, + Sensitive: pointer(true), + Description: pointer("hey there! I'm a nested string type, required."), + }, + }, + }, + }, + ComputedOptionalRequired: schema.ComputedOptional, + Description: pointer("hey there! I'm a map nested type."), + }, + }, + }, + }, + "map attributes with element types": { + schema: &base.Schema{ + Type: []string{"object"}, + Required: []string{"map_with_strings_required"}, + Properties: map[string]*base.SchemaProxy{ + "map_with_floats": base.CreateSchemaProxy(&base.Schema{ + Type: []string{"object"}, + Description: "hey there! I'm a map type with floats.", + AdditionalProperties: base.CreateSchemaProxy(&base.Schema{ + Type: []string{"number"}, + Format: "float", + }), + }), + "map_with_strings_required": base.CreateSchemaProxy(&base.Schema{ + Type: []string{"object"}, + Description: "hey there! I'm a map type with strings, required.", + AdditionalProperties: base.CreateSchemaProxy(&base.Schema{ + Type: []string{"string"}, + }), + }), + }, + }, + expectedAttributes: &[]resource.Attribute{ + { + Name: "map_with_floats", + Map: &resource.MapAttribute{ + ElementType: schema.ElementType{ + Float64: &schema.Float64Type{}, + }, + ComputedOptionalRequired: schema.ComputedOptional, + Description: pointer("hey there! I'm a map type with floats."), + }, + }, + { + Name: "map_with_strings_required", + Map: &resource.MapAttribute{ + ElementType: schema.ElementType{ + String: &schema.StringType{}, + }, + ComputedOptionalRequired: schema.Required, + Description: pointer("hey there! I'm a map type with strings, required."), + }, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + schema := oas.OASSchema{Schema: testCase.schema} + attributes, err := schema.BuildResourceAttributes() + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if diff := cmp.Diff(attributes, testCase.expectedAttributes); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBuildMapDataSource(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema *base.Schema + expectedAttributes *[]datasource.Attribute + }{ + + "map nested attribute with props": { + schema: &base.Schema{ + Type: []string{"object"}, + Properties: map[string]*base.SchemaProxy{ + "nested_map_prop": base.CreateSchemaProxy(&base.Schema{ + Type: []string{"object"}, + Description: "hey there! I'm a map nested type.", + AdditionalProperties: base.CreateSchemaProxy(&base.Schema{ + Type: []string{"object"}, + Required: []string{"nested_password_required"}, + Properties: map[string]*base.SchemaProxy{ + "nested_obj_prop": base.CreateSchemaProxy(&base.Schema{ + Type: []string{"object"}, + Required: []string{"nested_int64_required"}, + Description: "hey there! I'm a single nested object type.", + Properties: map[string]*base.SchemaProxy{ + "nested_float64": base.CreateSchemaProxy(&base.Schema{ + Type: []string{"number"}, + Format: "double", + Description: "hey there! I'm a nested float64 type.", + }), + "nested_int64_required": base.CreateSchemaProxy(&base.Schema{ + Type: []string{"integer"}, + Format: "int64", + Description: "hey there! I'm a nested int64 type, required.", + }), + }, + }), + "nested_password_required": base.CreateSchemaProxy(&base.Schema{ + Type: []string{"string"}, + Format: "password", + Description: "hey there! I'm a nested string type, required.", + }), + }, + }), + }), + }, + }, + expectedAttributes: &[]datasource.Attribute{ + { + Name: "nested_map_prop", + MapNested: &datasource.MapNestedAttribute{ + NestedObject: datasource.NestedAttributeObject{ + Attributes: []datasource.Attribute{ + { + Name: "nested_obj_prop", + SingleNested: &datasource.SingleNestedAttribute{ + Attributes: []datasource.Attribute{ + { + Name: "nested_float64", + Float64: &datasource.Float64Attribute{ + ComputedOptionalRequired: schema.ComputedOptional, + Description: pointer("hey there! I'm a nested float64 type."), + }, + }, + { + Name: "nested_int64_required", + Int64: &datasource.Int64Attribute{ + ComputedOptionalRequired: schema.Required, + Description: pointer("hey there! I'm a nested int64 type, required."), + }, + }, + }, + ComputedOptionalRequired: schema.ComputedOptional, + Description: pointer("hey there! I'm a single nested object type."), + }, + }, + { + Name: "nested_password_required", + String: &datasource.StringAttribute{ + ComputedOptionalRequired: schema.Required, + Sensitive: pointer(true), + Description: pointer("hey there! I'm a nested string type, required."), + }, + }, + }, + }, + ComputedOptionalRequired: schema.ComputedOptional, + Description: pointer("hey there! I'm a map nested type."), + }, + }, + }, + }, + "map attributes with element types": { + schema: &base.Schema{ + Type: []string{"object"}, + Required: []string{"map_with_strings_required"}, + Properties: map[string]*base.SchemaProxy{ + "map_with_floats": base.CreateSchemaProxy(&base.Schema{ + Type: []string{"object"}, + Description: "hey there! I'm a map type with floats.", + AdditionalProperties: base.CreateSchemaProxy(&base.Schema{ + Type: []string{"number"}, + Format: "float", + }), + }), + "map_with_strings_required": base.CreateSchemaProxy(&base.Schema{ + Type: []string{"object"}, + Description: "hey there! I'm a map type with strings, required.", + AdditionalProperties: base.CreateSchemaProxy(&base.Schema{ + Type: []string{"string"}, + }), + }), + }, + }, + expectedAttributes: &[]datasource.Attribute{ + { + Name: "map_with_floats", + Map: &datasource.MapAttribute{ + ElementType: schema.ElementType{ + Float64: &schema.Float64Type{}, + }, + ComputedOptionalRequired: schema.ComputedOptional, + Description: pointer("hey there! I'm a map type with floats."), + }, + }, + { + Name: "map_with_strings_required", + Map: &datasource.MapAttribute{ + ElementType: schema.ElementType{ + String: &schema.StringType{}, + }, + ComputedOptionalRequired: schema.Required, + Description: pointer("hey there! I'm a map type with strings, required."), + }, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + schema := oas.OASSchema{Schema: testCase.schema} + attributes, err := schema.BuildDataSourceAttributes() + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if diff := cmp.Diff(attributes, testCase.expectedAttributes); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} From cf86e49dacec78d2173d4eb5addbf81689fc5c38 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Mon, 26 Jun 2023 12:27:18 -0400 Subject: [PATCH 13/15] add map attribute and type to design doc --- DESIGN.md | 53 +++++++++++++++++++++++++++-------------------------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/DESIGN.md b/DESIGN.md index 5ac34cd9..79be00ae 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -71,41 +71,42 @@ For a given [OAS type](https://spec.openapis.org/oas/v3.1.0#data-types) and form > **NOTE:** All `Type` and `Format` fields below are native to OpenAPI Spec 3.x, with the exception of the format `set`, which is a custom field that only this generator tool is expected to support. -| Type (OAS) | Format (OAS) | Items type (OAS `array`) | Plugin Framework Attribute Type | -|------------|---------------------|--------------------------|---------------------------------------------------------------------------------------------| -| `integer` | - | - | `Int64Attribute` | -| `number` | `double` or `float` | - | `Float64Attribute` | -| `number` | (all others) | - | `NumberAttribute` | -| `string` | - | - | `StringAttribute` | -| `boolean` | - | - | `BoolAttribute` | -| `array` | - | `object` | `ListNestedAttribute` | -| `array` | - | (all others) | `ListAttribute` (nests with [element types](#oas-to-plugin-framework-element-types)) | -| `array` | `set` | `object` | `SetNestedAttribute` | -| `array` | `set` | (all others) | `SetAttribute` (nests with [element types](#oas-to-plugin-framework-element-types)) | -| `object` | - | - | `SingleNestedAttribute` | +| Type (OAS) | Format (OAS) | Other Criteria | Plugin Framework Attribute Type | +|------------|---------------------|----------------------------------------------|---------------------------------------------------------------------------------------------| +| `integer` | - | - | `Int64Attribute` | +| `number` | `double` or `float` | - | `Float64Attribute` | +| `number` | - | - | `NumberAttribute` | +| `string` | - | - | `StringAttribute` | +| `boolean` | - | - | `BoolAttribute` | +| `array` | - | `items.type == object` | `ListNestedAttribute` | +| `array` | - | `items.type == (any)` | `ListAttribute` (nests with [element types](#oas-to-plugin-framework-element-types)) | +| `array` | `set` | `items.type == object` | `SetNestedAttribute` | +| `array` | `set` | `items.type == (any)` | `SetAttribute` (nests with [element types](#oas-to-plugin-framework-element-types)) | +| `object` | - | `additionalProperties.type == object` | `MapNestedAttribute` | +| `object` | - | `additionalProperties.type == (any)` | `MapAttribute` (nests with [element types](#oas-to-plugin-framework-element-types)) | +| `object` | - | - | `SingleNestedAttribute` | #### Unsupported Attribute Types - `ListNestedBlock`, `SetNestedBlock`, and `SingleNestedBlock` - While the Plugin Framework supports blocks, the Plugin Framework team encourages provider developers to prefer `ListNestedAttribute`, `SetNestedAttribute`, and `SingleNestedAttribute` for new provider development. - `ObjectAttribute` - The generator will default to `SingleNestedAttribute` for object types to provide the additional schema information. -- `MapNestedAttribute`, and `MapAttribute` - - Mapping for these types is currently not supported, but will be considered in future versions. ### OAS to Plugin Framework Element Types -For attributes that don't have additional schema information (currently only `ListAttribute` and `SetAttribute`), the following rules will be applied for mapping from OAS type and format combinations, into Framework element types. - -| Type (OAS) | Format (OAS) | Items type (OAS `array`) | Plugin Framework Element Type | -|------------|---------------------|--------------------------|---------------------------------| -| `integer` | - | - | `Int64Type` | -| `number` | `double` or `float` | - | `Float64Type` | -| `number` | (all others) | - | `NumberType` | -| `string` | - | - | `StringType` | -| `boolean` | - | - | `BoolType` | -| `array` | - | (all) | `ListType` | -| `array` | `set` | (all) | `SetType` | -| `object` | - | - | `ObjectType` | +For attributes that don't have additional schema information (`ListAttribute`, `SetAttribute`, and `MapAttribute`), the following rules will be applied for mapping from OAS type and format combinations, into Framework element types. + +| Type (OAS) | Format (OAS) | Other Criteria | Plugin Framework Element Type | +|------------|---------------------|---------------------------------------|---------------------------------| +| `integer` | - | - | `Int64Type` | +| `number` | `double` or `float` | - | `Float64Type` | +| `number` | - | - | `NumberType` | +| `string` | - | - | `StringType` | +| `boolean` | - | - | `BoolType` | +| `array` | - | - | `ListType` | +| `array` | `set` | - | `SetType` | +| `object` | - | `additionalProperties.type == (any)` | `MapType` | +| `object` | - | - | `ObjectType` | ### Required, Computed, and Optional From 330b3f582937faede49dd539a5653cee38c0bccc Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Mon, 26 Jun 2023 12:48:23 -0400 Subject: [PATCH 14/15] add copywrite --- internal/mapper/oas/map.go | 3 +++ internal/mapper/oas/map_test.go | 3 +++ 2 files changed, 6 insertions(+) diff --git a/internal/mapper/oas/map.go b/internal/mapper/oas/map.go index 834a7e81..41c5f236 100644 --- a/internal/mapper/oas/map.go +++ b/internal/mapper/oas/map.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package oas import ( diff --git a/internal/mapper/oas/map_test.go b/internal/mapper/oas/map_test.go index d33ea92d..f756fe4f 100644 --- a/internal/mapper/oas/map_test.go +++ b/internal/mapper/oas/map_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package oas_test import ( From 6212d783c6b54614536a9623e8e50015b619a63d Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Mon, 26 Jun 2023 12:52:53 -0400 Subject: [PATCH 15/15] add dot --- internal/mapper/oas/map.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/mapper/oas/map.go b/internal/mapper/oas/map.go index 41c5f236..55b4ff13 100644 --- a/internal/mapper/oas/map.go +++ b/internal/mapper/oas/map.go @@ -14,7 +14,7 @@ import ( ) func (s *OASSchema) BuildMapResource(name string, computability schema.ComputedOptionalRequired) (*resource.Attribute, error) { - // Maps are detected as `type: object`, with an `additionalProperties` field that is a schema `additionalProperties` can + // Maps are detected as `type: object`, with an `additionalProperties` field that is a schema. `additionalProperties` can // also be a boolean (which we should ignore and map to an SingleNestedAttribute), so calling functions should call s.IsMap() first. mapSchemaProxy, ok := s.Schema.AdditionalProperties.(*base.SchemaProxy) if !ok { @@ -59,7 +59,7 @@ func (s *OASSchema) BuildMapResource(name string, computability schema.ComputedO } func (s *OASSchema) BuildMapDataSource(name string, computability schema.ComputedOptionalRequired) (*datasource.Attribute, error) { - // Maps are detected as `type: object`, with an `additionalProperties` field that is a schema `additionalProperties` can + // Maps are detected as `type: object`, with an `additionalProperties` field that is a schema. `additionalProperties` can // also be a boolean (which we should ignore and map to an SingleNestedAttribute), so calling functions should call s.IsMap() first. mapSchemaProxy, ok := s.Schema.AdditionalProperties.(*base.SchemaProxy) if !ok { @@ -104,7 +104,7 @@ func (s *OASSchema) BuildMapDataSource(name string, computability schema.Compute } func (s *OASSchema) BuildMapElementType() (schema.ElementType, error) { - // Maps are detected as `type: object`, with an `additionalProperties` field that is a schema `additionalProperties` can + // Maps are detected as `type: object`, with an `additionalProperties` field that is a schema. `additionalProperties` can // also be a boolean (which we should ignore and map to an ObjectType), so calling functions should call s.IsMap() first. mapSchemaProxy, ok := s.Schema.AdditionalProperties.(*base.SchemaProxy) if !ok {