From 48234ba365013a0fbe7249e672aa1469fdf00e2d Mon Sep 17 00:00:00 2001 From: Ash Godfrey Date: Thu, 22 Oct 2026 10:52:39 +0100 Subject: [PATCH 1/2] feat(terraform): honor const values on array and map properties Array and map properties that declare a const value were parsed as ordinary fields: the schema parser only attached const/default metadata to containers when a default was present. Terraform therefore exposed them as configurable attributes and sent whatever was in state instead of the const value. - Parser: attach const metadata to arrays, sets and maps behind the new constArrays and constObjects features, and drop consts whose elements are not scalars with a validation warning. - Go models: emit a const struct tag carrying the JSON encoding for container fields, render Go literals for their getters, and compare received const values structurally when unmarshaling. - Terraform: declare the new features; the existing const handling already hides the fields from the resource schema and skips them when building requests, so the SDK model injects the value. Regenerates the review Go, CLI and Terraform fixtures and adds a ConstContainer entity with an acceptance test asserting the request body. --- .changesets/1792662097-11a83540.yaml | 10 + .changesets/1792662097-2435dcc7.yaml | 10 + .changesets/1792662097-4b2c17a5.yaml | 10 + internal/features/features.go | 6 + internal/features/features_generated.go | 14 + internal/schemas/handle_array_test.go | 100 +++++ internal/schemas/handle_object_test.go | 96 +++++ internal/schemas/schemas.go | 103 ++++- .../templates/common/common/features.d.ts | 2 + templates/templates/common/go/annotations.ts | 15 +- templates/templates/common/go/sanitization.ts | 33 ++ .../{{getUtilsLocation}}/json.go.stmpl | 13 +- templates/templates/terraform/features.ts | 2 + tests/specs/review-terraform.yaml | 122 ++++++ zSDKs/sdk-cli/.speakeasy/gen.lock | 2 +- .../internal/sdk/sdkinternal/utils/json.go | 13 +- zSDKs/sdk-go/.speakeasy/gen.lock | 2 +- zSDKs/sdk-go/internal/utils/json.go | 13 +- .../.speakeasy/gen.lock | 56 ++- .../.speakeasy/logs/naming.log | 18 + zSDKs/terraform-provider-testing/README.md | 2 + .../docs/data-sources/const_container.md | 27 ++ .../docs/resources/const_container.md | 49 +++ .../testing_const_container/data-source.tf | 3 + .../import-by-string-id.tf | 4 + .../testing_const_container/import.sh | 1 + .../testing_const_container/resource.tf | 3 + .../provider/constcontainer_data_source.go | 128 ++++++ .../constcontainer_data_source_sdk.go | 35 ++ .../provider/constcontainer_resource.go | 268 ++++++++++++ .../provider/constcontainer_resource_sdk.go | 61 +++ .../internal/provider/provider.go | 2 + .../internal/sdk/internal/utils/json.go | 13 +- .../models/operations/createconstcontainer.go | 47 +++ .../models/operations/deleteconstcontainer.go | 48 +++ .../models/operations/getconstcontainer.go | 58 +++ .../models/shared/constcontainerrequest.go | 84 ++++ .../models/shared/constcontainerresponse.go | 92 +++++ .../internal/sdk/sdk.go | 381 ++++++++++++++++++ 39 files changed, 1926 insertions(+), 20 deletions(-) create mode 100644 .changesets/1792662097-11a83540.yaml create mode 100644 .changesets/1792662097-2435dcc7.yaml create mode 100644 .changesets/1792662097-4b2c17a5.yaml create mode 100644 zSDKs/terraform-provider-testing/docs/data-sources/const_container.md create mode 100644 zSDKs/terraform-provider-testing/docs/resources/const_container.md create mode 100644 zSDKs/terraform-provider-testing/examples/data-sources/testing_const_container/data-source.tf create mode 100644 zSDKs/terraform-provider-testing/examples/resources/testing_const_container/import-by-string-id.tf create mode 100644 zSDKs/terraform-provider-testing/examples/resources/testing_const_container/import.sh create mode 100644 zSDKs/terraform-provider-testing/examples/resources/testing_const_container/resource.tf create mode 100644 zSDKs/terraform-provider-testing/internal/provider/constcontainer_data_source.go create mode 100644 zSDKs/terraform-provider-testing/internal/provider/constcontainer_data_source_sdk.go create mode 100644 zSDKs/terraform-provider-testing/internal/provider/constcontainer_resource.go create mode 100644 zSDKs/terraform-provider-testing/internal/provider/constcontainer_resource_sdk.go create mode 100644 zSDKs/terraform-provider-testing/internal/sdk/models/operations/createconstcontainer.go create mode 100644 zSDKs/terraform-provider-testing/internal/sdk/models/operations/deleteconstcontainer.go create mode 100644 zSDKs/terraform-provider-testing/internal/sdk/models/operations/getconstcontainer.go create mode 100644 zSDKs/terraform-provider-testing/internal/sdk/models/shared/constcontainerrequest.go create mode 100644 zSDKs/terraform-provider-testing/internal/sdk/models/shared/constcontainerresponse.go diff --git a/.changesets/1792662097-11a83540.yaml b/.changesets/1792662097-11a83540.yaml new file mode 100644 index 000000000..71464cfb3 --- /dev/null +++ b/.changesets/1792662097-11a83540.yaml @@ -0,0 +1,10 @@ +id: 1792662097-11a83540 +features: + - constArrays +targets: + - terraform +type: feat +bump: minor +description: array properties with a const value are hidden from the resource schema and the const value is sent in request bodies +author: AshGodfrey +date: "2026-10-22" diff --git a/.changesets/1792662097-2435dcc7.yaml b/.changesets/1792662097-2435dcc7.yaml new file mode 100644 index 000000000..ef6e758ff --- /dev/null +++ b/.changesets/1792662097-2435dcc7.yaml @@ -0,0 +1,10 @@ +id: 1792662097-2435dcc7 +features: + - constsAndDefaults +targets: + - go +type: fix +bump: patch +description: const values on array and map fields are emitted when marshaling and accepted when unmarshaling +author: AshGodfrey +date: "2026-10-22" diff --git a/.changesets/1792662097-4b2c17a5.yaml b/.changesets/1792662097-4b2c17a5.yaml new file mode 100644 index 000000000..af2bcc2df --- /dev/null +++ b/.changesets/1792662097-4b2c17a5.yaml @@ -0,0 +1,10 @@ +id: 1792662097-4b2c17a5 +features: + - constObjects +targets: + - terraform +type: feat +bump: minor +description: map properties with a const value are hidden from the resource schema and the const value is sent in request bodies +author: AshGodfrey +date: "2026-10-22" diff --git a/internal/features/features.go b/internal/features/features.go index ff07148e8..1bc3574f0 100644 --- a/internal/features/features.go +++ b/internal/features/features.go @@ -149,6 +149,12 @@ const ( // FeatureDefaultObjects relates to the ability to handle default values for objects // Tier: Experimental FeatureDefaultObjects + // FeatureConstArrays relates to the ability to handle const values for arrays + // Tier: Experimental + FeatureConstArrays + // FeatureConstObjects relates to the ability to handle const values for objects + // Tier: Experimental + FeatureConstObjects // FeatureAdditionalProperties relates to the ability to deserialize any unknown properties into a map available as an extra property on the generated models for both request bodies and parameters // Tier: Gold FeatureAdditionalProperties diff --git a/internal/features/features_generated.go b/internal/features/features_generated.go index a3bb2c2ec..1c7661d2e 100644 --- a/internal/features/features_generated.go +++ b/internal/features/features_generated.go @@ -20,6 +20,10 @@ func (f Feature) String() string { return "callbacks" case FeatureConfigurableModuleName: return "configurableModuleName" + case FeatureConstArrays: + return "constArrays" + case FeatureConstObjects: + return "constObjects" case FeatureConstsAndDefaults: return "constsAndDefaults" case FeatureContentMediaTypeApplicationJSON: @@ -198,6 +202,10 @@ func FeatureFromString(s string) Feature { return FeatureCallbacks case "configurableModuleName": return FeatureConfigurableModuleName + case "constArrays": + return FeatureConstArrays + case "constObjects": + return FeatureConstObjects case "constsAndDefaults": return FeatureConstsAndDefaults case "contentMediaTypeApplicationJSON": @@ -367,6 +375,8 @@ var featureList = []Feature{ FeatureCLICommands, FeatureCallbacks, FeatureConfigurableModuleName, + FeatureConstArrays, + FeatureConstObjects, FeatureConstsAndDefaults, FeatureContentMediaTypeApplicationJSON, FeatureCore, @@ -456,6 +466,8 @@ var featureTiers = map[Feature]string{ FeatureCLICommands: TierExperimental, FeatureCallbacks: TierCore, FeatureConfigurableModuleName: TierExperimental, + FeatureConstArrays: TierExperimental, + FeatureConstObjects: TierExperimental, FeatureConstsAndDefaults: TierBeta, FeatureContentMediaTypeApplicationJSON: TierExperimental, FeatureCore: TierCore, @@ -571,6 +583,8 @@ var tierCoreFeatures = []Feature{ var tierExperimentalFeatures = []Feature{ FeatureCLICommands, FeatureConfigurableModuleName, + FeatureConstArrays, + FeatureConstObjects, FeatureContentMediaTypeApplicationJSON, FeatureCustomCodeRegions, FeatureCustomSecuritySchemes, diff --git a/internal/schemas/handle_array_test.go b/internal/schemas/handle_array_test.go index 5f8943a9a..001150af7 100644 --- a/internal/schemas/handle_array_test.go +++ b/internal/schemas/handle_array_test.go @@ -69,6 +69,106 @@ func TestHandleArray(t *testing.T) { Build()). Build(), }, + { + name: "ArrayWithConst", + schemasYAML: `TestSchema: + type: array + items: + type: string + const: + - read + - write`, + schemaName: "TestSchema", + mockFeatures: func() *testutils.MockFeaturesConfig { + config := testutils.NewMockFeaturesConfig() + config.SupportedFeatures = map[features.Feature]bool{ + features.FeatureConstArrays: true, + features.FeatureConstsAndDefaults: true, + } + return config + }, + expected: testutils.NewFieldDef("strings", + testutils.NewTypeDef(ast.DataTypeArray). + WithItemType(testutils.NewTypeDef(ast.DataTypeString).WithLocation(&yaml.Node{Line: 11, Column: 9}).WithValidations(&ast.Validations{}).Build()). + WithLocation(&yaml.Node{Line: 9, Column: 7}). + WithValidations(&ast.Validations{}). + Build()). + WithConst(&ast.AnyValue{Value: []interface{}{"read", "write"}}). + Build(), + }, + { + name: "ArrayWithEmptyConst", + schemasYAML: `TestSchema: + type: array + items: + type: string + const: []`, + schemaName: "TestSchema", + mockFeatures: func() *testutils.MockFeaturesConfig { + config := testutils.NewMockFeaturesConfig() + config.SupportedFeatures = map[features.Feature]bool{ + features.FeatureConstArrays: true, + features.FeatureConstsAndDefaults: true, + } + return config + }, + expected: testutils.NewFieldDef("strings", + testutils.NewTypeDef(ast.DataTypeArray). + WithItemType(testutils.NewTypeDef(ast.DataTypeString).WithLocation(&yaml.Node{Line: 11, Column: 9}).WithValidations(&ast.Validations{}).Build()). + WithLocation(&yaml.Node{Line: 9, Column: 7}). + WithValidations(&ast.Validations{}). + Build()). + WithConst(&ast.AnyValue{Value: []interface{}{}}). + Build(), + }, + { + name: "ArrayWithConstFeatureUnsupported", + schemasYAML: `TestSchema: + type: array + items: + type: string + const: []`, + schemaName: "TestSchema", + mockFeatures: func() *testutils.MockFeaturesConfig { + config := testutils.NewMockFeaturesConfig() + config.SupportedFeatures = map[features.Feature]bool{ + features.FeatureConstsAndDefaults: true, + } + return config + }, + expected: testutils.NewFieldDef("strings", + testutils.NewTypeDef(ast.DataTypeArray). + WithItemType(testutils.NewTypeDef(ast.DataTypeString).WithLocation(&yaml.Node{Line: 11, Column: 9}).WithValidations(&ast.Validations{}).Build()). + WithLocation(&yaml.Node{Line: 9, Column: 7}). + WithValidations(&ast.Validations{}). + Build()). + Build(), + }, + { + name: "ArrayWithConstItemTypeMismatch", + schemasYAML: `TestSchema: + type: array + items: + type: string + const: + - 1`, + schemaName: "TestSchema", + mockFeatures: func() *testutils.MockFeaturesConfig { + config := testutils.NewMockFeaturesConfig() + config.SupportedFeatures = map[features.Feature]bool{ + features.FeatureConstArrays: true, + features.FeatureConstsAndDefaults: true, + } + return config + }, + expected: testutils.NewFieldDef("strings", + testutils.NewTypeDef(ast.DataTypeArray). + WithItemType(testutils.NewTypeDef(ast.DataTypeString).WithLocation(&yaml.Node{Line: 11, Column: 9}).WithValidations(&ast.Validations{}).Build()). + WithLocation(&yaml.Node{Line: 9, Column: 7}). + WithValidations(&ast.Validations{}). + Build()). + Build(), + }, { name: "ArrayWithMinMaxItems", schemasYAML: `TestSchema: diff --git a/internal/schemas/handle_object_test.go b/internal/schemas/handle_object_test.go index bce54b65d..7e5906855 100644 --- a/internal/schemas/handle_object_test.go +++ b/internal/schemas/handle_object_test.go @@ -245,6 +245,102 @@ func TestHandleObject(t *testing.T) { Build()). Build(), }, + { + name: "MapWithConst", + schemasYAML: `TestSchema: + type: object + additionalProperties: + type: string + const: + key: value`, + schemaName: "TestSchema", + mockFeatures: func() *testutils.MockFeaturesConfig { + config := testutils.NewMockFeaturesConfig() + config.SupportedFeatures = map[features.Feature]bool{ + features.FeatureConstObjects: true, + features.FeatureConstsAndDefaults: true, + } + return config + }, + expected: testutils.NewFieldDef("object", + testutils.NewTypeDef(ast.DataTypeMap). + WithLocation(&yaml.Node{Line: 9, Column: 7}). + WithItemType( + testutils.NewTypeDef(ast.DataTypeString). + WithLocation(&yaml.Node{Line: 11, Column: 9}). + WithValidations(&ast.Validations{}). + Build(), + ). + Build()). + WithConst(&ast.AnyValue{Value: map[string]interface{}{"key": "value"}}). + Build(), + }, + { + name: "MapWithConstFeatureUnsupported", + schemasYAML: `TestSchema: + type: object + additionalProperties: + type: string + const: {}`, + schemaName: "TestSchema", + mockFeatures: func() *testutils.MockFeaturesConfig { + config := testutils.NewMockFeaturesConfig() + config.SupportedFeatures = map[features.Feature]bool{ + features.FeatureConstsAndDefaults: true, + } + return config + }, + expected: testutils.NewFieldDef("object", + testutils.NewTypeDef(ast.DataTypeMap). + WithLocation(&yaml.Node{Line: 9, Column: 7}). + WithItemType( + testutils.NewTypeDef(ast.DataTypeString). + WithLocation(&yaml.Node{Line: 11, Column: 9}). + WithValidations(&ast.Validations{}). + Build(), + ). + Build()). + Build(), + }, + { + name: "ObjectWithUnsupportedConst", + schemasYAML: `TestSchema: + type: object + properties: + name: + type: string + const: + name: "fixed"`, + schemaName: "TestSchema", + mockFeatures: func() *testutils.MockFeaturesConfig { + config := testutils.NewMockFeaturesConfig() + config.SupportedFeatures = map[features.Feature]bool{ + features.FeatureConstObjects: true, + features.FeatureConstsAndDefaults: true, + } + return config + }, + expected: testutils.NewFieldDef("object", + testutils.NewTypeDef(ast.DataTypeClass). + WithLocation(&yaml.Node{Line: 9, Column: 7}). + WithScope(ast.ScopeShared). + WithRegistered(). + WithOriginalNameFrozen(). + WithContextStack(ast.ContextStack{}). + WithFields( + testutils.NewFieldDef("name", + testutils.NewTypeDef(ast.DataTypeString). + WithLocation(&yaml.Node{Line: 12, Column: 11}). + WithValidations(&ast.Validations{}). + Build()). + WithOriginalName("name"). + WithJSONAnnotation("name"). + WithOptional(true). + Build(), + ). + Build()). + Build(), + }, { name: "ObjectWithExample", schemasYAML: `TestSchema: diff --git a/internal/schemas/schemas.go b/internal/schemas/schemas.go index 26cec94b9..b735eaa14 100644 --- a/internal/schemas/schemas.go +++ b/internal/schemas/schemas.go @@ -562,8 +562,9 @@ func (s *Schemas) handleArray(ctx context.Context, params Params, nullable bool) Optional: nullable, Nullable: nullable, } - if s.Subsystem.Features.IsFeatureSupported(ctx, features.FeatureDefaultArrays) && schema.Default != nil { + if s.containerMetaDataApplies(ctx, schema, features.FeatureDefaultArrays, features.FeatureConstArrays) { fieldDef = s.handleFieldDefMetaData(ctx, fieldDef, nullable, schema) + s.recordContainerConstUsage(ctx, fieldDef, features.FeatureConstArrays) } // We need to inherit the multipart annotations @@ -818,12 +819,18 @@ func (s *Schemas) handleObject(ctx context.Context, params Params, nullable bool typeDef.Extensions = extensions - return &ast.FieldDef{ + mapField := &ast.FieldDef{ Name: fieldName, Type: ast.NewType(typeDef, nil), Optional: nullable, Nullable: nullable, - }, nil + } + if schema.Const != nil && s.Subsystem.Features.IsFeatureSupported(ctx, features.FeatureConstObjects) { + mapField = s.handleFieldDefMetaData(ctx, mapField, nullable, schema) + s.recordContainerConstUsage(ctx, mapField, features.FeatureConstObjects) + } + + return mapField, nil } case recurseAdditionalProperties: additionalPropertiesParams := params @@ -891,12 +898,18 @@ func (s *Schemas) handleObject(ctx context.Context, params Params, nullable bool typeDef.Extensions = extensions - return &ast.FieldDef{ + mapField := &ast.FieldDef{ Name: fieldName, Type: ast.NewType(typeDef, nil), Optional: nullable, Nullable: nullable, - }, nil + } + if schema.Const != nil && s.Subsystem.Features.IsFeatureSupported(ctx, features.FeatureConstObjects) { + mapField = s.handleFieldDefMetaData(ctx, mapField, nullable, schema) + s.recordContainerConstUsage(ctx, mapField, features.FeatureConstObjects) + } + + return mapField, nil } } } @@ -1298,8 +1311,9 @@ func (s *Schemas) handleObject(ctx context.Context, params Params, nullable bool Nullable: nullable, } - if s.Subsystem.Features.IsFeatureSupported(ctx, features.FeatureDefaultObjects) && schema.Default != nil { + if s.containerMetaDataApplies(ctx, schema, features.FeatureDefaultObjects, features.FeatureConstObjects) { fieldDef = s.handleFieldDefMetaData(ctx, fieldDef, nullable, schema) + s.recordContainerConstUsage(ctx, fieldDef, features.FeatureConstObjects) } s.setCachedType(fieldType, ¶ms) @@ -1660,6 +1674,17 @@ func (s *Schemas) checkDefaultConformsToType(ctx context.Context, field *ast.Fie continue } + if pair.label == "const" && (field.Type.IsContainer() || field.Type.Type == ast.DataTypeClass) { + if !constContainerValueSupported(v.Value, field.Type) { + logging.LogWarning(ctx, "validation warning", errors.NewValidationWarning( + fmt.Sprintf("const value %v is not supported for type %q, only arrays and maps of scalar values are supported, omitting const", v.Value, dataType), + schema.GetPropertyNode("Const"), nil, + )) + *pair.val = nil + } + continue + } + if !defaultValueMatchesType(v.Value, dataType) { // Before clearing, try to coerce string values to the target type. // Many real-world specs have string defaults on numeric fields (e.g. default: "0.00"). @@ -1681,6 +1706,56 @@ func (s *Schemas) checkDefaultConformsToType(ctx context.Context, field *ast.Fie } } +func constContainerValueSupported(val any, typ *ast.TypeDef) bool { + switch typ.Type { + case ast.DataTypeArray, ast.DataTypeSet: + items, ok := val.([]any) + if !ok { + return false + } + for _, item := range items { + if !constScalarMatchesType(item, typ.ItemType) { + return false + } + } + return true + case ast.DataTypeMap: + entries, ok := val.(map[string]any) + if !ok { + return false + } + for _, item := range entries { + if !constScalarMatchesType(item, typ.ItemType) { + return false + } + } + return true + default: + return false + } +} + +func constScalarMatchesType(val any, itemType *ast.TypeDef) bool { + if val == nil || itemType == nil { + return false + } + + switch itemType.Type { + case ast.DataTypeEnum: + return itemType.Enum != nil && slices.Contains(itemType.Enum.Values, fmt.Sprint(val)) + case ast.DataTypeBoolean, ast.DataTypeInteger, ast.DataTypeInt32, ast.DataTypeBigInt, + ast.DataTypeNumber, ast.DataTypeFloat32, ast.DataTypeDecimal, + ast.DataTypeString, ast.DataTypeDate, ast.DataTypeDateTime: + dataType := itemType.Type + if itemType.Format == "string" { + dataType = ast.DataTypeString + } + return defaultValueMatchesType(val, dataType) + default: + return false + } +} + // defaultValueMatchesType checks whether a decoded default/const value is compatible // with the given AST data type. func defaultValueMatchesType(val any, dataType ast.DataType) bool { @@ -2752,6 +2827,22 @@ func (s *Schemas) handleFieldDefMetaData(ctx context.Context, fieldDef *ast.Fiel return fieldDef } +func (s *Schemas) containerMetaDataApplies(ctx context.Context, schema *oas3.Schema, defaultFeature, constFeature features.Feature) bool { + if schema.Default != nil && s.Subsystem.Features.IsFeatureSupported(ctx, defaultFeature) { + return true + } + + return schema.Const != nil && s.Subsystem.Features.IsFeatureSupported(ctx, constFeature) +} + +func (s *Schemas) recordContainerConstUsage(ctx context.Context, fieldDef *ast.FieldDef, constFeature features.Feature) { + if fieldDef.Const == nil { + return + } + + s.Subsystem.Features.RecordFeatureUsage(ctx, constFeature) +} + func (s *Schemas) sanitizeDefaultConstValue(ctx context.Context, value values.Value) *ast.AnyValue { if value == nil { return nil diff --git a/templates/templates/common/common/features.d.ts b/templates/templates/common/common/features.d.ts index faa544746..773e3374e 100644 --- a/templates/templates/common/common/features.d.ts +++ b/templates/templates/common/common/features.d.ts @@ -15,6 +15,8 @@ declare global { | "cliCommands" | "callbacks" | "configurableModuleName" + | "constArrays" + | "constObjects" | "constsAndDefaults" | "contentMediaTypeApplicationJSON" | "core" diff --git a/templates/templates/common/go/annotations.ts b/templates/templates/common/go/annotations.ts index 91d4b6efb..77292fc16 100644 --- a/templates/templates/common/go/annotations.ts +++ b/templates/templates/common/go/annotations.ts @@ -295,10 +295,6 @@ function templateFormAnnotation( function templateConstsAndDefaultsAnnotation( fieldDef: FieldDef, ): TemplatedAnnotation | null { - if (fieldDef.Type.IsContainer()) { - return null; - } - function sanitizeValue(value: any): string { if (value === null) { return "null"; @@ -311,6 +307,17 @@ function templateConstsAndDefaultsAnnotation( return value; } + if (fieldDef.Type.IsContainer()) { + if (fieldDef.Const) { + return { + Tag: "const", + Value: sanitizeValue(JSON.stringify(fieldDef.Const.Value)), + }; + } + + return null; + } + if (fieldDef.Const) { return { Tag: "const", diff --git a/templates/templates/common/go/sanitization.ts b/templates/templates/common/go/sanitization.ts index 0fa062913..9f4d2e0c0 100644 --- a/templates/templates/common/go/sanitization.ts +++ b/templates/templates/common/go/sanitization.ts @@ -736,6 +736,10 @@ function templateConstValue( useTypesPackage: true, skipPointerWrapping, }); + case "array": + case "set": + case "map": + return templateConstContainerValue(fieldDef, usageLocation); case "bytes": default: throw new Error( @@ -746,6 +750,35 @@ function templateConstValue( registerTemplateFunc("templateConstValue", templateConstValue); +function templateConstContainerValue( + fieldDef: FieldDef, + usageLocation: string, +): string { + const containerType = sanitizeType(fieldDef.Type, false, usageLocation); + const renderItem = (value: any): string => + templateConstValue( + { + ...fieldDef, + Type: fieldDef.Type.ItemType, + Optional: false, + Nullable: false, + Const: { Value: value }, + } as FieldDef, + usageLocation, + true, + ); + + if (fieldDef.Type.Type.toString() === "map") { + const entries = Object.entries(fieldDef.Const.Value ?? {}).map( + ([key, value]) => `${JSON.stringify(key)}: ${renderItem(value)}`, + ); + return `${containerType}{${entries.join(", ")}}`; + } + + const items = ((fieldDef.Const.Value ?? []) as any[]).map(renderItem); + return `${containerType}{${items.join(", ")}}`; +} + // @ts-ignore function templateArrayValue(value: any): string { return value; diff --git a/templates/templates/go/auxiliary/{{getUtilsLocation}}/json.go.stmpl b/templates/templates/go/auxiliary/{{getUtilsLocation}}/json.go.stmpl index ffb6afffd..7c319ca06 100644 --- a/templates/templates/go/auxiliary/{{getUtilsLocation}}/json.go.stmpl +++ b/templates/templates/go/auxiliary/{{getUtilsLocation}}/json.go.stmpl @@ -215,7 +215,7 @@ func UnmarshalJSON(b []byte, v interface{}, tag reflect.StructTag, topLevel bool } } constValue := field.Tag.Get("const") - if val != constValue { + if val != constValue && !constJSONValuesEqual(r, constValue) { return fmt.Errorf("const field `%s` does not match expected value `%s` got `%s`", fieldName, constValue, val) } @@ -447,6 +447,17 @@ func implementsJSONMarshaler(v interface{}) bool { return reflect.PtrTo(vType).Implements(marshalerType) } +func constJSONValuesEqual(received json.RawMessage, constValue string) bool { + var got, want interface{} + if err := json.Unmarshal(received, &got); err != nil { + return false + } + if err := json.Unmarshal([]byte(constValue), &want); err != nil { + return false + } + return reflect.DeepEqual(got, want) +} + func handleDefaultConstValue(tagValue string, val interface{}, tag reflect.StructTag) json.RawMessage { if tagValue == "null" { return []byte("null") diff --git a/templates/templates/terraform/features.ts b/templates/templates/terraform/features.ts index ed42926a7..b249b429b 100644 --- a/templates/templates/terraform/features.ts +++ b/templates/templates/terraform/features.ts @@ -26,6 +26,8 @@ const supportedFeatures = { constsAndDefaults: "0.3.2", defaultArrays: "0.2.0", defaultObjects: "0.1.2", + constArrays: "0.1.0", + constObjects: "0.1.0", additionalProperties: "0.1.5", disallowCircularRefs: "0.0.0", oauth2ClientCredentials: "0.3.5", diff --git a/tests/specs/review-terraform.yaml b/tests/specs/review-terraform.yaml index 4c85fff0c..4651ffcb2 100644 --- a/tests/specs/review-terraform.yaml +++ b/tests/specs/review-terraform.yaml @@ -5697,6 +5697,54 @@ paths: responses: '200': description: OK + /v0/constcontainers: + post: + x-speakeasy-entity-operation: ConstContainer#create + description: | + Create a resource whose request schema declares const values on array + and map properties. Terraform must hide those properties from the + resource schema and send the const values in the request body. + operationId: create-const-container + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ConstContainerRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ConstContainerResponse' + /v0/constcontainer/{id}: + parameters: + - name: id + in: path + required: true + schema: + type: string + get: + x-speakeasy-entity-operation: ConstContainer#read + description: Get a const container resource + operationId: get-const-container + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ConstContainerResponse' + '404': + description: Not Found + delete: + x-speakeasy-entity-operation: ConstContainer#delete + description: Delete a const container resource + operationId: delete-const-container + responses: + '200': + description: OK components: parameters: GlobalBoolean: @@ -5917,6 +5965,80 @@ components: type: string default: DEFAULT schemas: + ConstContainerRequest: + type: object + additionalProperties: false + x-speakeasy-entity: ConstContainer + properties: + name: + type: string + empty_list: + type: array + items: + type: string + const: [] + fixed_list: + type: array + items: + type: string + enum: + - read + - write + - admin + const: + - read + - write + empty_map: + type: object + additionalProperties: + type: string + const: {} + fixed_map: + type: object + additionalProperties: + type: integer + const: + max: 10 + required: + - name + - empty_list + - empty_map + ConstContainerResponse: + type: object + additionalProperties: false + x-speakeasy-entity: ConstContainer + properties: + id: + type: string + name: + type: string + empty_list: + type: array + items: + type: string + const: [] + fixed_list: + type: array + items: + type: string + enum: + - read + - write + - admin + const: + - read + - write + empty_map: + type: object + additionalProperties: + type: string + const: {} + fixed_map: + type: object + additionalProperties: + type: integer + const: + max: 10 DiscriminatedUnionCreateRequest: x-speakeasy-entity: DiscriminatedUnion type: object diff --git a/zSDKs/sdk-cli/.speakeasy/gen.lock b/zSDKs/sdk-cli/.speakeasy/gen.lock index f9b725c8f..dbcc53eb4 100644 --- a/zSDKs/sdk-cli/.speakeasy/gen.lock +++ b/zSDKs/sdk-cli/.speakeasy/gen.lock @@ -598,7 +598,7 @@ trackedFiles: internal/sdk/sdkinternal/utils/headers.go: last_write_checksum: sha1:1b0bd68e1ea0bce8c5a42b5de9fd020fede7aecc internal/sdk/sdkinternal/utils/json.go: - last_write_checksum: sha1:cbddebcd3cd06c607a5c462eab1d490f62bcdc1a + last_write_checksum: sha1:156c99b390e19b755a3acda6f2f57dcdefcb6206 internal/sdk/sdkinternal/utils/oauth2_credentials.go: last_write_checksum: sha1:f9218b6b3dbd11494cc353bf22eb37f0ca654a7d internal/sdk/sdkinternal/utils/oauth2_sessions.go: diff --git a/zSDKs/sdk-cli/internal/sdk/sdkinternal/utils/json.go b/zSDKs/sdk-cli/internal/sdk/sdkinternal/utils/json.go index bf44ff6e5..c8f90dd87 100644 --- a/zSDKs/sdk-cli/internal/sdk/sdkinternal/utils/json.go +++ b/zSDKs/sdk-cli/internal/sdk/sdkinternal/utils/json.go @@ -216,7 +216,7 @@ func UnmarshalJSON(b []byte, v interface{}, tag reflect.StructTag, topLevel bool } } constValue := field.Tag.Get("const") - if val != constValue { + if val != constValue && !constJSONValuesEqual(r, constValue) { return fmt.Errorf("const field `%s` does not match expected value `%s` got `%s`", fieldName, constValue, val) } @@ -446,6 +446,17 @@ func implementsJSONMarshaler(v interface{}) bool { return reflect.PtrTo(vType).Implements(marshalerType) } +func constJSONValuesEqual(received json.RawMessage, constValue string) bool { + var got, want interface{} + if err := json.Unmarshal(received, &got); err != nil { + return false + } + if err := json.Unmarshal([]byte(constValue), &want); err != nil { + return false + } + return reflect.DeepEqual(got, want) +} + func handleDefaultConstValue(tagValue string, val interface{}, tag reflect.StructTag) json.RawMessage { if tagValue == "null" { return []byte("null") diff --git a/zSDKs/sdk-go/.speakeasy/gen.lock b/zSDKs/sdk-go/.speakeasy/gen.lock index 5032e8a95..7498b2596 100644 --- a/zSDKs/sdk-go/.speakeasy/gen.lock +++ b/zSDKs/sdk-go/.speakeasy/gen.lock @@ -713,7 +713,7 @@ trackedFiles: internal/utils/headers.go: last_write_checksum: sha1:e529ca3d7ab96a091021bcd471ba2cbef50286b3 internal/utils/json.go: - last_write_checksum: sha1:785278d21e06d3c0b820630fc9d5ca38d364f9ee + last_write_checksum: sha1:3ed843422cdd78c41282fe4ab6f22bef8b389c40 internal/utils/pathparams.go: last_write_checksum: sha1:440aadb18d886ea0a79af0772eb65cdddc9eee60 internal/utils/queryparams.go: diff --git a/zSDKs/sdk-go/internal/utils/json.go b/zSDKs/sdk-go/internal/utils/json.go index 5e6fb780d..8106a80db 100644 --- a/zSDKs/sdk-go/internal/utils/json.go +++ b/zSDKs/sdk-go/internal/utils/json.go @@ -216,7 +216,7 @@ func UnmarshalJSON(b []byte, v interface{}, tag reflect.StructTag, topLevel bool } } constValue := field.Tag.Get("const") - if val != constValue { + if val != constValue && !constJSONValuesEqual(r, constValue) { return fmt.Errorf("const field `%s` does not match expected value `%s` got `%s`", fieldName, constValue, val) } @@ -446,6 +446,17 @@ func implementsJSONMarshaler(v interface{}) bool { return reflect.PtrTo(vType).Implements(marshalerType) } +func constJSONValuesEqual(received json.RawMessage, constValue string) bool { + var got, want interface{} + if err := json.Unmarshal(received, &got); err != nil { + return false + } + if err := json.Unmarshal([]byte(constValue), &want); err != nil { + return false + } + return reflect.DeepEqual(got, want) +} + func handleDefaultConstValue(tagValue string, val interface{}, tag reflect.StructTag) json.RawMessage { if tagValue == "null" { return []byte("null") diff --git a/zSDKs/terraform-provider-testing/.speakeasy/gen.lock b/zSDKs/terraform-provider-testing/.speakeasy/gen.lock index 8fffcb37d..a9345b240 100644 --- a/zSDKs/terraform-provider-testing/.speakeasy/gen.lock +++ b/zSDKs/terraform-provider-testing/.speakeasy/gen.lock @@ -1,7 +1,7 @@ lockVersion: 2.0.0 id: review-sdk-test-id management: - docChecksum: fccaa1943b7d99936e3cf386d6aef8ac + docChecksum: b5e0ebee357a2bc4151a20fb322e722c docVersion: 0.0.1 speakeasyVersion: internal generationVersion: internal @@ -11,6 +11,8 @@ features: terraform: additionalDependencies: 0.1.0 additionalProperties: 0.1.5 + constArrays: 0.1.0 + constObjects: 0.1.0 constsAndDefaults: 0.3.2 contentMediaTypeApplicationJSON: 0.1.1 core: 3.50.2 @@ -51,6 +53,8 @@ trackedFiles: last_write_checksum: sha1:d84cd84fe166ec2d063ebb13a1845e744425693c examples/data-sources/testing_basic/data-source.tf: last_write_checksum: sha1:02bc159abd38449bcddcff2ed9eb7daf0b5cf5b4 + examples/data-sources/testing_const_container/data-source.tf: + last_write_checksum: sha1:a45dc527c9b2b2404f3ce3797f8a0c3009de47dd examples/data-sources/testing_discriminated_union/data-source.tf: last_write_checksum: sha1:a21b63c90ce67e7b78e103fb57e8b55ba8ab4073 examples/data-sources/testing_framework_type/data-source.tf: @@ -275,6 +279,12 @@ trackedFiles: last_write_checksum: sha1:056e42f5b27cbc4a7e5b4a14c154ad4084026594 examples/resources/testing_basic/resource.tf: last_write_checksum: sha1:60780e18fdeae0f7305502bd819092015c9e36b5 + examples/resources/testing_const_container/import-by-string-id.tf: + last_write_checksum: sha1:094ecf7d894ef2ef5a9a4669ec05ef7c75bdceaa + examples/resources/testing_const_container/import.sh: + last_write_checksum: sha1:dc82e240078029b62b56bb695067d4c5e9cb4264 + examples/resources/testing_const_container/resource.tf: + last_write_checksum: sha1:73c7ef301934066294a6aa619b7a7e4268581b95 examples/resources/testing_discriminated_union/import-by-string-id.tf: last_write_checksum: sha1:61473f31485ac280bfa6943e7580d1bdd1e43413 examples/resources/testing_discriminated_union/import.sh: @@ -763,6 +773,14 @@ trackedFiles: last_write_checksum: sha1:550b1c9370c6f1e389f6145f4ab522df9c855e76 internal/provider/basic_resource_sdk.go: last_write_checksum: sha1:c7327b7ec6b530ccb4ba940c83953e7b58b092a4 + internal/provider/constcontainer_data_source.go: + last_write_checksum: sha1:3af6b6a8ec4bc9b019d8c83c297577266be2cb05 + internal/provider/constcontainer_data_source_sdk.go: + last_write_checksum: sha1:a3d2f26a22da0b68da7f73523f16dec043d43c59 + internal/provider/constcontainer_resource.go: + last_write_checksum: sha1:1c82779f3e4d07d9145f3c49abc8ecbf21b390af + internal/provider/constcontainer_resource_sdk.go: + last_write_checksum: sha1:e8e60887a5d623a9cdaef5a5d0f38884b3cc56a5 internal/provider/discriminatedunion_data_source.go: last_write_checksum: sha1:dd97963488cb1994321bf5e3f813917c80a7d7b5 internal/provider/discriminatedunion_data_source_sdk.go: @@ -1066,7 +1084,7 @@ trackedFiles: internal/provider/patch_resource_sdk.go: last_write_checksum: sha1:9b583e2ac47191350373ce5a86408c118bd606b1 internal/provider/provider.go: - last_write_checksum: sha1:a333255d3d9585c5700d3059b03e9b0dbf388774 + last_write_checksum: sha1:8b1723df0915047021b8fd1b267f93dc19808e67 internal/provider/reflect/diags.go: last_write_checksum: sha1:ace8bc53054bb1d8ee8689acf3e4323de75a6297 internal/provider/reflect/doc.go: @@ -2404,7 +2422,7 @@ trackedFiles: internal/sdk/internal/utils/jq.go: last_write_checksum: sha1:2e75ed7a7db1f91e97dac1ac4ca7801b2beb69b5 internal/sdk/internal/utils/json.go: - last_write_checksum: sha1:bc34328ed29902c9f3bbeb04b5706a684e9bccc1 + last_write_checksum: sha1:e4afb6c5125227acd6366acac653831f4d01cd0c internal/sdk/internal/utils/oauth2_credentials.go: last_write_checksum: sha1:f9218b6b3dbd11494cc353bf22eb37f0ca654a7d internal/sdk/internal/utils/oauth2_sessions.go: @@ -2439,6 +2457,8 @@ trackedFiles: last_write_checksum: sha1:5b080b285280d5f8a012138b77c848c3118bddb0 internal/sdk/models/operations/createbasic.go: last_write_checksum: sha1:0fdbac8d615b203c4f6054f9f44176e1c4ce564a + internal/sdk/models/operations/createconstcontainer.go: + last_write_checksum: sha1:892a2934f9b5cc0d5e40335502d20c3750702b95 internal/sdk/models/operations/creatediscriminatedunion.go: last_write_checksum: sha1:68d916f8c709a15e2a564066dbc9317502aecf13 internal/sdk/models/operations/creatediscriminatedunionarray.go: @@ -2587,6 +2607,8 @@ trackedFiles: last_write_checksum: sha1:5b31482c4b581bc143b0403060004849179d8ffd internal/sdk/models/operations/deletebasic.go: last_write_checksum: sha1:2d991f9ce3b49547f88d3ed69a9a4e01ccc135a8 + internal/sdk/models/operations/deleteconstcontainer.go: + last_write_checksum: sha1:443d6380ef9a4f9405c655078371ab2c13e633fa internal/sdk/models/operations/deletecustomplanmodifiers.go: last_write_checksum: sha1:8bcb2a4f56d57265de023e41d21617f5b88a7bc6 internal/sdk/models/operations/deletediscriminatedunion.go: @@ -2727,6 +2749,8 @@ trackedFiles: last_write_checksum: sha1:3bcc0082028b5f01a4d13f84072629b010550da0 internal/sdk/models/operations/getbasic.go: last_write_checksum: sha1:73ada8ac1a2cec3987eb5b93f78f815934067b9d + internal/sdk/models/operations/getconstcontainer.go: + last_write_checksum: sha1:c4ac02b538bdf08901d302a4d4c07a6a18cb2536 internal/sdk/models/operations/getcustomplanmodifiers.go: last_write_checksum: sha1:c6f5938cb6330969450186336a3085558bcd6d1c internal/sdk/models/operations/getdiscriminatedunion.go: @@ -3073,6 +3097,10 @@ trackedFiles: last_write_checksum: sha1:34cb7c7fc9ab32b03e7c6a0875e7e6fec5805577 internal/sdk/models/shared/basicsearch.go: last_write_checksum: sha1:ebc8943a31148f7623c5f8ff940dd81c1b52f23d + internal/sdk/models/shared/constcontainerrequest.go: + last_write_checksum: sha1:3cdb41c7b838e1c15dd0483ce7f685fdc6e7d480 + internal/sdk/models/shared/constcontainerresponse.go: + last_write_checksum: sha1:d94a9a07729db01d47dd0ff890a363406d2169e5 internal/sdk/models/shared/discriminatedobject.go: last_write_checksum: sha1:9bb5641a1bcb27ec59c1f16d82d8a251ec733286 internal/sdk/models/shared/discriminatedunionarraycreaterequest.go: @@ -3578,7 +3606,7 @@ trackedFiles: internal/sdk/retry/config.go: last_write_checksum: sha1:102d1953fbd7e9f312c4442c71ccca2eaaeaa27d internal/sdk/sdk.go: - last_write_checksum: sha1:a2997f8af41cead3493768284c9cc8c950f8f57c + last_write_checksum: sha1:f390c541016ad47b4744f87a396fc36e8590d9f4 internal/sdk/types/bigint.go: last_write_checksum: sha1:49b004005d0461fb04b846eca062b070b0360b31 internal/sdk/types/date.go: @@ -6020,4 +6048,24 @@ examples: responses: "200": application/json: {} + create-const-container: + speakeasy-default-create-const-container: + requestBody: + application/json: {"name": "", "empty_list": [], "fixed_list": ["read", "write"], "empty_map": {}, "fixed_map": {"max": 10}} + responses: + "200": + application/json: {"empty_list": [], "fixed_list": ["read", "write"], "empty_map": {}, "fixed_map": {"max": 10}} + get-const-container: + speakeasy-default-get-const-container: + parameters: + path: + id: "" + responses: + "200": + application/json: {"empty_list": [], "fixed_list": ["read", "write"], "empty_map": {}, "fixed_map": {"max": 10}} + delete-const-container: + speakeasy-default-delete-const-container: + parameters: + path: + id: "" examplesVersion: 1.0.2 diff --git a/zSDKs/terraform-provider-testing/.speakeasy/logs/naming.log b/zSDKs/terraform-provider-testing/.speakeasy/logs/naming.log index 9f3e6a51d..73705617c 100644 --- a/zSDKs/terraform-provider-testing/.speakeasy/logs/naming.log +++ b/zSDKs/terraform-provider-testing/.speakeasy/logs/naming.log @@ -2238,6 +2238,15 @@ DEBUG discriminated: Renamed to "XUnknownValuesResponse_closed_enum_string" registrationID: "scope:shared refType:Schemas refName:XUnknownValuesResponse originalName:closed_enum_string" DEBUG +--- Renaming 2 types with name "fixed_list" --- +DEBUG discriminated: Renamed to "ConstContainerRequest_fixed_list" + labels: "original_name:fixed_list scope:shared data_type:enum refName:ConstContainerRequest" + registrationID: "scope:shared refType:Schemas refName:ConstContainerRequest originalName:fixed_list" +DEBUG discriminated: Renamed to "ConstContainerResponse_fixed_list" + labels: "original_name:fixed_list scope:shared data_type:enum refName:ConstContainerResponse" + registrationID: "scope:shared refType:Schemas refName:ConstContainerResponse originalName:fixed_list" +DEBUG + --- Renaming 3 types with name "tier" --- DEBUG discriminated: Renamed to "create_import_defaulted_id_tier" labels: "original_name:tier operation:create_import_defaulted_id data_type:enum parameter:pathParam" @@ -3813,6 +3822,15 @@ UpdateOasContentMediaTypeRequest (id: string, OASContentMediaTypeRequest: OASCon UpdateOasContentMediaTypeResponse (ContentType: string, StatusCode: int32, RawResponse: response ...) DeleteOasContentMediaTypeRequest (id: string) DeleteOasContentMediaTypeResponse (ContentType: string, StatusCode: int32, RawResponse: response) +ConstContainerRequest (name: string, empty_list: array, fixed_list: array ...) + ConstContainerRequestFixedList (enum: read, write, admin) +CreateConstContainerResponse (ContentType: string, StatusCode: int32, RawResponse: response ...) + ConstContainerResponse (id: string, name: string, empty_list: array ...) + ConstContainerResponseFixedList (enum: read, write, admin) +GetConstContainerRequest (id: string) +GetConstContainerResponse (ContentType: string, StatusCode: int32, RawResponse: response ...) +DeleteConstContainerRequest (id: string) +DeleteConstContainerResponse (ContentType: string, StatusCode: int32, RawResponse: response) Globals (globalBoolean: boolean, globalBooleanWithDefault: boolean, globalEnumFloat32: float32 ...) UpdateXGlobalsGlobals (globalBoolean: boolean, globalBooleanWithDefault: boolean, globalEnumFloat32: float32 ...) diff --git a/zSDKs/terraform-provider-testing/README.md b/zSDKs/terraform-provider-testing/README.md index f26d58fe8..62129d589 100644 --- a/zSDKs/terraform-provider-testing/README.md +++ b/zSDKs/terraform-provider-testing/README.md @@ -102,6 +102,7 @@ Available configuration: * [testing_api_create_and_update](docs/resources/api_create_and_update.md) * [testing_basic](docs/resources/basic.md) +* [testing_const_container](docs/resources/const_container.md) * [testing_discriminated_union](docs/resources/discriminated_union.md) * [testing_discriminated_union_array](docs/resources/discriminated_union_array.md) * [testing_framework_type](docs/resources/framework_type.md) @@ -179,6 +180,7 @@ Available configuration: * [testing_api_create_and_update](docs/data-sources/api_create_and_update.md) * [testing_basic](docs/data-sources/basic.md) +* [testing_const_container](docs/data-sources/const_container.md) * [testing_discriminated_union](docs/data-sources/discriminated_union.md) * [testing_framework_type](docs/data-sources/framework_type.md) * [testing_import_defaulted_id](docs/data-sources/import_defaulted_id.md) diff --git a/zSDKs/terraform-provider-testing/docs/data-sources/const_container.md b/zSDKs/terraform-provider-testing/docs/data-sources/const_container.md new file mode 100644 index 000000000..d32d01fab --- /dev/null +++ b/zSDKs/terraform-provider-testing/docs/data-sources/const_container.md @@ -0,0 +1,27 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "testing_const_container Data Source - terraform-provider-testing" +subcategory: "" +description: |- + ConstContainer DataSource +--- + +# testing_const_container (Data Source) + +ConstContainer DataSource + +## Example Usage + +```terraform +data "testing_const_container" "my_constcontainer" { + id = "...my_id..." +} +``` + + +## Schema + +### Read-Only + +- `id` (String) The ID of this resource. +- `name` (String) diff --git a/zSDKs/terraform-provider-testing/docs/resources/const_container.md b/zSDKs/terraform-provider-testing/docs/resources/const_container.md new file mode 100644 index 000000000..665204c33 --- /dev/null +++ b/zSDKs/terraform-provider-testing/docs/resources/const_container.md @@ -0,0 +1,49 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "testing_const_container Resource - terraform-provider-testing" +subcategory: "" +description: |- + ConstContainer Resource +--- + +# testing_const_container (Resource) + +ConstContainer Resource + +## Example Usage + +```terraform +resource "testing_const_container" "my_constcontainer" { + name = "...my_name..." +} +``` + + +## Schema + +### Required + +- `name` (String) Requires replacement if changed. + +### Read-Only + +- `id` (String) The ID of this resource. + +## Import + +Import is supported using the following syntax: + +In Terraform v1.5.0 and later, the [`import` block](https://developer.hashicorp.com/terraform/language/import) can be used with the `id` attribute, for example: + +```terraform +import { + to = testing_const_container.my_testing_const_container + id = "..." +} +``` + +The [`terraform import` command](https://developer.hashicorp.com/terraform/cli/commands/import) can be used, for example: + +```shell +terraform import testing_const_container.my_testing_const_container "..." +``` diff --git a/zSDKs/terraform-provider-testing/examples/data-sources/testing_const_container/data-source.tf b/zSDKs/terraform-provider-testing/examples/data-sources/testing_const_container/data-source.tf new file mode 100644 index 000000000..96013b05a --- /dev/null +++ b/zSDKs/terraform-provider-testing/examples/data-sources/testing_const_container/data-source.tf @@ -0,0 +1,3 @@ +data "testing_const_container" "my_constcontainer" { + id = "...my_id..." +} \ No newline at end of file diff --git a/zSDKs/terraform-provider-testing/examples/resources/testing_const_container/import-by-string-id.tf b/zSDKs/terraform-provider-testing/examples/resources/testing_const_container/import-by-string-id.tf new file mode 100644 index 000000000..51488fd18 --- /dev/null +++ b/zSDKs/terraform-provider-testing/examples/resources/testing_const_container/import-by-string-id.tf @@ -0,0 +1,4 @@ +import { + to = testing_const_container.my_testing_const_container + id = "..." +} diff --git a/zSDKs/terraform-provider-testing/examples/resources/testing_const_container/import.sh b/zSDKs/terraform-provider-testing/examples/resources/testing_const_container/import.sh new file mode 100644 index 000000000..1b394b9c7 --- /dev/null +++ b/zSDKs/terraform-provider-testing/examples/resources/testing_const_container/import.sh @@ -0,0 +1 @@ +terraform import testing_const_container.my_testing_const_container "..." diff --git a/zSDKs/terraform-provider-testing/examples/resources/testing_const_container/resource.tf b/zSDKs/terraform-provider-testing/examples/resources/testing_const_container/resource.tf new file mode 100644 index 000000000..871e0d3f3 --- /dev/null +++ b/zSDKs/terraform-provider-testing/examples/resources/testing_const_container/resource.tf @@ -0,0 +1,3 @@ +resource "testing_const_container" "my_constcontainer" { + name = "...my_name..." +} \ No newline at end of file diff --git a/zSDKs/terraform-provider-testing/internal/provider/constcontainer_data_source.go b/zSDKs/terraform-provider-testing/internal/provider/constcontainer_data_source.go new file mode 100644 index 000000000..4b2b78c30 --- /dev/null +++ b/zSDKs/terraform-provider-testing/internal/provider/constcontainer_data_source.go @@ -0,0 +1,128 @@ +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package provider + +import ( + "context" + "fmt" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-provider-testing/internal/sdk" +) + +// Ensure provider defined types fully satisfy framework interfaces. +var _ datasource.DataSource = &ConstContainerDataSource{} +var _ datasource.DataSourceWithConfigure = &ConstContainerDataSource{} + +func NewConstContainerDataSource() datasource.DataSource { + return &ConstContainerDataSource{} +} + +// ConstContainerDataSource is the data source implementation. +type ConstContainerDataSource struct { + // Provider configured SDK client. + client *sdk.SDK +} + +// ConstContainerDataSourceModel describes the data model. +type ConstContainerDataSourceModel struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` +} + +// Metadata returns the data source type name. +func (r *ConstContainerDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_const_container" +} + +// Schema defines the schema for the data source. +func (r *ConstContainerDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "ConstContainer DataSource", + + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Required: true, + }, + "name": schema.StringAttribute{ + Computed: true, + }, + }, + } +} + +func (r *ConstContainerDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + providerData, ok := req.ProviderData.(*TestingProviderConfigureData) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected DataSource Configure Type", + fmt.Sprintf("Expected *TestingProviderConfigureData, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + + return + } + + r.client = providerData.SDKClient +} + +func (r *ConstContainerDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var data *ConstContainerDataSourceModel + var item types.Object + + resp.Diagnostics.Append(req.Config.Get(ctx, &item)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(item.As(ctx, &data, basetypes.ObjectAsOptions{ + UnhandledNullAsEmpty: true, + UnhandledUnknownAsEmpty: true, + })...) + + if resp.Diagnostics.HasError() { + return + } + + request, requestDiags := data.ToOperationsGetConstContainerRequest(ctx) + resp.Diagnostics.Append(requestDiags...) + + if resp.Diagnostics.HasError() { + return + } + res, err := r.client.GetConstContainer(ctx, *request) + if err != nil { + resp.Diagnostics.AddError("failure to invoke API", redactSensitiveValues(ctx, err.Error())) + if res != nil && res.RawResponse != nil { + resp.Diagnostics.AddError("unexpected http request/response", debugResponse(res.RawResponse)) + } + return + } + if res == nil { + resp.Diagnostics.AddError("unexpected response from API", fmt.Sprintf("%v", res)) + return + } + if res.StatusCode != 200 { + resp.Diagnostics.AddError(fmt.Sprintf("unexpected response from API. Got an unexpected response code %v", res.StatusCode), debugResponse(res.RawResponse)) + return + } + if !(res.ConstContainerResponse != nil) { + resp.Diagnostics.AddError("unexpected response from API. Got an unexpected response body", debugResponse(res.RawResponse)) + return + } + resp.Diagnostics.Append(data.RefreshFromSharedConstContainerResponse(ctx, res.ConstContainerResponse)...) + + if resp.Diagnostics.HasError() { + return + } + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} diff --git a/zSDKs/terraform-provider-testing/internal/provider/constcontainer_data_source_sdk.go b/zSDKs/terraform-provider-testing/internal/provider/constcontainer_data_source_sdk.go new file mode 100644 index 000000000..0dfce4980 --- /dev/null +++ b/zSDKs/terraform-provider-testing/internal/provider/constcontainer_data_source_sdk.go @@ -0,0 +1,35 @@ +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package provider + +import ( + "context" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-provider-testing/internal/sdk/models/operations" + "github.com/hashicorp/terraform-provider-testing/internal/sdk/models/shared" +) + +func (r *ConstContainerDataSourceModel) RefreshFromSharedConstContainerResponse(ctx context.Context, resp *shared.ConstContainerResponse) diag.Diagnostics { + var diags diag.Diagnostics + + if resp != nil { + r.ID = types.StringPointerValue(resp.ID) + r.Name = types.StringPointerValue(resp.Name) + } + + return diags +} + +func (r *ConstContainerDataSourceModel) ToOperationsGetConstContainerRequest(ctx context.Context) (*operations.GetConstContainerRequest, diag.Diagnostics) { + var diags diag.Diagnostics + + var id string + id = r.ID.ValueString() + + out := operations.GetConstContainerRequest{ + ID: id, + } + + return &out, diags +} diff --git a/zSDKs/terraform-provider-testing/internal/provider/constcontainer_resource.go b/zSDKs/terraform-provider-testing/internal/provider/constcontainer_resource.go new file mode 100644 index 000000000..3cd47c03e --- /dev/null +++ b/zSDKs/terraform-provider-testing/internal/provider/constcontainer_resource.go @@ -0,0 +1,268 @@ +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package provider + +import ( + "context" + "fmt" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + speakeasy_stringplanmodifier "github.com/hashicorp/terraform-provider-testing/internal/planmodifiers/stringplanmodifier" + "github.com/hashicorp/terraform-provider-testing/internal/sdk" +) + +// Ensure provider defined types fully satisfy framework interfaces. +var _ resource.Resource = &ConstContainerResource{} +var _ resource.ResourceWithImportState = &ConstContainerResource{} + +func NewConstContainerResource() resource.Resource { + return &ConstContainerResource{} +} + +// ConstContainerResource defines the resource implementation. +type ConstContainerResource struct { + // Provider configured SDK client. + client *sdk.SDK +} + +// ConstContainerResourceModel describes the resource data model. +type ConstContainerResourceModel struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` +} + +func (r *ConstContainerResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_const_container" +} + +func (r *ConstContainerResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "ConstContainer Resource", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + }, + "name": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplaceIfConfigured(), + speakeasy_stringplanmodifier.SuppressDiff(speakeasy_stringplanmodifier.ExplicitSuppress), + }, + Description: `Requires replacement if changed.`, + }, + }, + } +} + +func (r *ConstContainerResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + providerData, ok := req.ProviderData.(*TestingProviderConfigureData) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *TestingProviderConfigureData, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + + return + } + + r.client = providerData.SDKClient +} + +func (r *ConstContainerResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data *ConstContainerResourceModel + var plan types.Object + + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(plan.As(ctx, &data, basetypes.ObjectAsOptions{ + UnhandledNullAsEmpty: true, + UnhandledUnknownAsEmpty: true, + })...) + + if resp.Diagnostics.HasError() { + return + } + + request, requestDiags := data.ToSharedConstContainerRequest(ctx) + resp.Diagnostics.Append(requestDiags...) + + if resp.Diagnostics.HasError() { + return + } + res, err := r.client.CreateConstContainer(ctx, *request) + if err != nil { + resp.Diagnostics.AddError("failure to invoke API", redactSensitiveValues(ctx, err.Error())) + if res != nil && res.RawResponse != nil { + resp.Diagnostics.AddError("unexpected http request/response", debugResponse(res.RawResponse)) + } + return + } + if res == nil { + resp.Diagnostics.AddError("unexpected response from API", fmt.Sprintf("%v", res)) + return + } + if res.StatusCode != 200 { + resp.Diagnostics.AddError(fmt.Sprintf("unexpected response from API. Got an unexpected response code %v", res.StatusCode), debugResponse(res.RawResponse)) + return + } + if !(res.ConstContainerResponse != nil) { + resp.Diagnostics.AddError("unexpected response from API. Got an unexpected response body", debugResponse(res.RawResponse)) + return + } + resp.Diagnostics.Append(data.RefreshFromSharedConstContainerResponse(ctx, res.ConstContainerResponse)...) + + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(refreshPlan(ctx, plan, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *ConstContainerResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data *ConstContainerResourceModel + var item types.Object + + resp.Diagnostics.Append(req.State.Get(ctx, &item)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(item.As(ctx, &data, basetypes.ObjectAsOptions{ + UnhandledNullAsEmpty: true, + UnhandledUnknownAsEmpty: true, + })...) + + if resp.Diagnostics.HasError() { + return + } + + request, requestDiags := data.ToOperationsGetConstContainerRequest(ctx) + resp.Diagnostics.Append(requestDiags...) + + if resp.Diagnostics.HasError() { + return + } + res, err := r.client.GetConstContainer(ctx, *request) + if err != nil { + resp.Diagnostics.AddError("failure to invoke API", redactSensitiveValues(ctx, err.Error())) + if res != nil && res.RawResponse != nil { + resp.Diagnostics.AddError("unexpected http request/response", debugResponse(res.RawResponse)) + } + return + } + if res == nil { + resp.Diagnostics.AddError("unexpected response from API", fmt.Sprintf("%v", res)) + return + } + if res.StatusCode == 404 { + resp.State.RemoveResource(ctx) + return + } + if res.StatusCode != 200 { + resp.Diagnostics.AddError(fmt.Sprintf("unexpected response from API. Got an unexpected response code %v", res.StatusCode), debugResponse(res.RawResponse)) + return + } + if !(res.ConstContainerResponse != nil) { + resp.Diagnostics.AddError("unexpected response from API. Got an unexpected response body", debugResponse(res.RawResponse)) + return + } + resp.Diagnostics.Append(data.RefreshFromSharedConstContainerResponse(ctx, res.ConstContainerResponse)...) + + if resp.Diagnostics.HasError() { + return + } + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *ConstContainerResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data *ConstContainerResourceModel + var plan types.Object + + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + merge(ctx, req, resp, &data) + if resp.Diagnostics.HasError() { + return + } + + // Not Implemented; all attributes marked as RequiresReplace + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *ConstContainerResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data *ConstContainerResourceModel + var item types.Object + + resp.Diagnostics.Append(req.State.Get(ctx, &item)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(item.As(ctx, &data, basetypes.ObjectAsOptions{ + UnhandledNullAsEmpty: true, + UnhandledUnknownAsEmpty: true, + })...) + + if resp.Diagnostics.HasError() { + return + } + + request, requestDiags := data.ToOperationsDeleteConstContainerRequest(ctx) + resp.Diagnostics.Append(requestDiags...) + + if resp.Diagnostics.HasError() { + return + } + res, err := r.client.DeleteConstContainer(ctx, *request) + if err != nil { + resp.Diagnostics.AddError("failure to invoke API", redactSensitiveValues(ctx, err.Error())) + if res != nil && res.RawResponse != nil { + resp.Diagnostics.AddError("unexpected http request/response", debugResponse(res.RawResponse)) + } + return + } + if res == nil { + resp.Diagnostics.AddError("unexpected response from API", fmt.Sprintf("%v", res)) + return + } + switch res.StatusCode { + case 200, 404: + break + default: + resp.Diagnostics.AddError(fmt.Sprintf("unexpected response from API. Got an unexpected response code %v", res.StatusCode), debugResponse(res.RawResponse)) + return + } + +} + +func (r *ConstContainerResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), req.ID)...) +} diff --git a/zSDKs/terraform-provider-testing/internal/provider/constcontainer_resource_sdk.go b/zSDKs/terraform-provider-testing/internal/provider/constcontainer_resource_sdk.go new file mode 100644 index 000000000..f6f4a0ed4 --- /dev/null +++ b/zSDKs/terraform-provider-testing/internal/provider/constcontainer_resource_sdk.go @@ -0,0 +1,61 @@ +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package provider + +import ( + "context" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-provider-testing/internal/sdk/models/operations" + "github.com/hashicorp/terraform-provider-testing/internal/sdk/models/shared" +) + +func (r *ConstContainerResourceModel) RefreshFromSharedConstContainerResponse(ctx context.Context, resp *shared.ConstContainerResponse) diag.Diagnostics { + var diags diag.Diagnostics + + if resp != nil { + r.ID = types.StringPointerValue(resp.ID) + r.Name = types.StringPointerValue(resp.Name) + } + + return diags +} + +func (r *ConstContainerResourceModel) ToOperationsDeleteConstContainerRequest(ctx context.Context) (*operations.DeleteConstContainerRequest, diag.Diagnostics) { + var diags diag.Diagnostics + + var id string + id = r.ID.ValueString() + + out := operations.DeleteConstContainerRequest{ + ID: id, + } + + return &out, diags +} + +func (r *ConstContainerResourceModel) ToOperationsGetConstContainerRequest(ctx context.Context) (*operations.GetConstContainerRequest, diag.Diagnostics) { + var diags diag.Diagnostics + + var id string + id = r.ID.ValueString() + + out := operations.GetConstContainerRequest{ + ID: id, + } + + return &out, diags +} + +func (r *ConstContainerResourceModel) ToSharedConstContainerRequest(ctx context.Context) (*shared.ConstContainerRequest, diag.Diagnostics) { + var diags diag.Diagnostics + + var name string + name = r.Name.ValueString() + + out := shared.ConstContainerRequest{ + Name: name, + } + + return &out, diags +} diff --git a/zSDKs/terraform-provider-testing/internal/provider/provider.go b/zSDKs/terraform-provider-testing/internal/provider/provider.go index c558c318a..06585ae95 100644 --- a/zSDKs/terraform-provider-testing/internal/provider/provider.go +++ b/zSDKs/terraform-provider-testing/internal/provider/provider.go @@ -808,6 +808,7 @@ func (p *TestingProvider) Resources(ctx context.Context) []func() resource.Resou return []func() resource.Resource{ NewAPICreateAndUpdateResource, NewBasicResource, + NewConstContainerResource, NewDiscriminatedUnionResource, NewDiscriminatedUnionArrayResource, NewFrameworkTypeResource, @@ -888,6 +889,7 @@ func (p *TestingProvider) DataSources(ctx context.Context) []func() datasource.D return []func() datasource.DataSource{ NewAPICreateAndUpdateDataSource, NewBasicDataSource, + NewConstContainerDataSource, NewDiscriminatedUnionDataSource, NewFrameworkTypeDataSource, NewImportDefaultedIDDataSource, diff --git a/zSDKs/terraform-provider-testing/internal/sdk/internal/utils/json.go b/zSDKs/terraform-provider-testing/internal/sdk/internal/utils/json.go index d49c56d68..13a99d20d 100644 --- a/zSDKs/terraform-provider-testing/internal/sdk/internal/utils/json.go +++ b/zSDKs/terraform-provider-testing/internal/sdk/internal/utils/json.go @@ -214,7 +214,7 @@ func UnmarshalJSON(b []byte, v interface{}, tag reflect.StructTag, topLevel bool } } constValue := field.Tag.Get("const") - if val != constValue { + if val != constValue && !constJSONValuesEqual(r, constValue) { return fmt.Errorf("const field `%s` does not match expected value `%s` got `%s`", fieldName, constValue, val) } @@ -433,6 +433,17 @@ func implementsJSONMarshaler(v interface{}) bool { return reflect.PtrTo(vType).Implements(marshalerType) } +func constJSONValuesEqual(received json.RawMessage, constValue string) bool { + var got, want interface{} + if err := json.Unmarshal(received, &got); err != nil { + return false + } + if err := json.Unmarshal([]byte(constValue), &want); err != nil { + return false + } + return reflect.DeepEqual(got, want) +} + func handleDefaultConstValue(tagValue string, val interface{}, tag reflect.StructTag) json.RawMessage { if tagValue == "null" { return []byte("null") diff --git a/zSDKs/terraform-provider-testing/internal/sdk/models/operations/createconstcontainer.go b/zSDKs/terraform-provider-testing/internal/sdk/models/operations/createconstcontainer.go new file mode 100644 index 000000000..7f223e99e --- /dev/null +++ b/zSDKs/terraform-provider-testing/internal/sdk/models/operations/createconstcontainer.go @@ -0,0 +1,47 @@ +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package operations + +import ( + "github.com/hashicorp/terraform-provider-testing/internal/sdk/models/shared" + "net/http" +) + +type CreateConstContainerResponse struct { + // HTTP response content type for this operation + ContentType string + // HTTP response status code for this operation + StatusCode int + // Raw HTTP response; suitable for custom response parsing + RawResponse *http.Response + // OK + ConstContainerResponse *shared.ConstContainerResponse +} + +func (c *CreateConstContainerResponse) GetContentType() string { + if c == nil { + return "" + } + return c.ContentType +} + +func (c *CreateConstContainerResponse) GetStatusCode() int { + if c == nil { + return 0 + } + return c.StatusCode +} + +func (c *CreateConstContainerResponse) GetRawResponse() *http.Response { + if c == nil { + return nil + } + return c.RawResponse +} + +func (c *CreateConstContainerResponse) GetConstContainerResponse() *shared.ConstContainerResponse { + if c == nil { + return nil + } + return c.ConstContainerResponse +} diff --git a/zSDKs/terraform-provider-testing/internal/sdk/models/operations/deleteconstcontainer.go b/zSDKs/terraform-provider-testing/internal/sdk/models/operations/deleteconstcontainer.go new file mode 100644 index 000000000..d2937f78e --- /dev/null +++ b/zSDKs/terraform-provider-testing/internal/sdk/models/operations/deleteconstcontainer.go @@ -0,0 +1,48 @@ +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package operations + +import ( + "net/http" +) + +type DeleteConstContainerRequest struct { + ID string `pathParam:"style=simple,explode=false,name=id"` +} + +func (d *DeleteConstContainerRequest) GetID() string { + if d == nil { + return "" + } + return d.ID +} + +type DeleteConstContainerResponse struct { + // HTTP response content type for this operation + ContentType string + // HTTP response status code for this operation + StatusCode int + // Raw HTTP response; suitable for custom response parsing + RawResponse *http.Response +} + +func (d *DeleteConstContainerResponse) GetContentType() string { + if d == nil { + return "" + } + return d.ContentType +} + +func (d *DeleteConstContainerResponse) GetStatusCode() int { + if d == nil { + return 0 + } + return d.StatusCode +} + +func (d *DeleteConstContainerResponse) GetRawResponse() *http.Response { + if d == nil { + return nil + } + return d.RawResponse +} diff --git a/zSDKs/terraform-provider-testing/internal/sdk/models/operations/getconstcontainer.go b/zSDKs/terraform-provider-testing/internal/sdk/models/operations/getconstcontainer.go new file mode 100644 index 000000000..b9d5e2846 --- /dev/null +++ b/zSDKs/terraform-provider-testing/internal/sdk/models/operations/getconstcontainer.go @@ -0,0 +1,58 @@ +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package operations + +import ( + "github.com/hashicorp/terraform-provider-testing/internal/sdk/models/shared" + "net/http" +) + +type GetConstContainerRequest struct { + ID string `pathParam:"style=simple,explode=false,name=id"` +} + +func (g *GetConstContainerRequest) GetID() string { + if g == nil { + return "" + } + return g.ID +} + +type GetConstContainerResponse struct { + // HTTP response content type for this operation + ContentType string + // HTTP response status code for this operation + StatusCode int + // Raw HTTP response; suitable for custom response parsing + RawResponse *http.Response + // OK + ConstContainerResponse *shared.ConstContainerResponse +} + +func (g *GetConstContainerResponse) GetContentType() string { + if g == nil { + return "" + } + return g.ContentType +} + +func (g *GetConstContainerResponse) GetStatusCode() int { + if g == nil { + return 0 + } + return g.StatusCode +} + +func (g *GetConstContainerResponse) GetRawResponse() *http.Response { + if g == nil { + return nil + } + return g.RawResponse +} + +func (g *GetConstContainerResponse) GetConstContainerResponse() *shared.ConstContainerResponse { + if g == nil { + return nil + } + return g.ConstContainerResponse +} diff --git a/zSDKs/terraform-provider-testing/internal/sdk/models/shared/constcontainerrequest.go b/zSDKs/terraform-provider-testing/internal/sdk/models/shared/constcontainerrequest.go new file mode 100644 index 000000000..b8e5b45f3 --- /dev/null +++ b/zSDKs/terraform-provider-testing/internal/sdk/models/shared/constcontainerrequest.go @@ -0,0 +1,84 @@ +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package shared + +import ( + "encoding/json" + "fmt" + "github.com/hashicorp/terraform-provider-testing/internal/sdk/internal/utils" +) + +type ConstContainerRequestFixedList string + +const ( + ConstContainerRequestFixedListRead ConstContainerRequestFixedList = "read" + ConstContainerRequestFixedListWrite ConstContainerRequestFixedList = "write" + ConstContainerRequestFixedListAdmin ConstContainerRequestFixedList = "admin" +) + +func (e ConstContainerRequestFixedList) ToPointer() *ConstContainerRequestFixedList { + return &e +} +func (e *ConstContainerRequestFixedList) UnmarshalJSON(data []byte) error { + var v string + if err := json.Unmarshal(data, &v); err != nil { + return err + } + switch v { + case "read": + fallthrough + case "write": + fallthrough + case "admin": + *e = ConstContainerRequestFixedList(v) + return nil + default: + return fmt.Errorf("invalid value for ConstContainerRequestFixedList: %v", v) + } +} + +type ConstContainerRequest struct { + Name string `json:"name"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + emptyList []string `const:"[]" json:"empty_list"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + fixedList []ConstContainerRequestFixedList `const:"[\"read\",\"write\"]" json:"fixed_list,omitempty"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + emptyMap map[string]string `const:"{}" json:"empty_map"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + fixedMap map[string]int64 `const:"{\"max\":10}" json:"fixed_map,omitempty"` +} + +func (c ConstContainerRequest) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(c, "", false) +} + +func (c *ConstContainerRequest) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &c, "", false, nil); err != nil { + return err + } + return nil +} + +func (c *ConstContainerRequest) GetName() string { + if c == nil { + return "" + } + return c.Name +} + +func (c *ConstContainerRequest) GetEmptyList() []string { + return []string{} +} + +func (c *ConstContainerRequest) GetFixedList() []ConstContainerRequestFixedList { + return []ConstContainerRequestFixedList{ConstContainerRequestFixedListRead, ConstContainerRequestFixedListWrite} +} + +func (c *ConstContainerRequest) GetEmptyMap() map[string]string { + return map[string]string{} +} + +func (c *ConstContainerRequest) GetFixedMap() map[string]int64 { + return map[string]int64{"max": 10} +} diff --git a/zSDKs/terraform-provider-testing/internal/sdk/models/shared/constcontainerresponse.go b/zSDKs/terraform-provider-testing/internal/sdk/models/shared/constcontainerresponse.go new file mode 100644 index 000000000..67ae54213 --- /dev/null +++ b/zSDKs/terraform-provider-testing/internal/sdk/models/shared/constcontainerresponse.go @@ -0,0 +1,92 @@ +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package shared + +import ( + "encoding/json" + "fmt" + "github.com/hashicorp/terraform-provider-testing/internal/sdk/internal/utils" +) + +type ConstContainerResponseFixedList string + +const ( + ConstContainerResponseFixedListRead ConstContainerResponseFixedList = "read" + ConstContainerResponseFixedListWrite ConstContainerResponseFixedList = "write" + ConstContainerResponseFixedListAdmin ConstContainerResponseFixedList = "admin" +) + +func (e ConstContainerResponseFixedList) ToPointer() *ConstContainerResponseFixedList { + return &e +} +func (e *ConstContainerResponseFixedList) UnmarshalJSON(data []byte) error { + var v string + if err := json.Unmarshal(data, &v); err != nil { + return err + } + switch v { + case "read": + fallthrough + case "write": + fallthrough + case "admin": + *e = ConstContainerResponseFixedList(v) + return nil + default: + return fmt.Errorf("invalid value for ConstContainerResponseFixedList: %v", v) + } +} + +type ConstContainerResponse struct { + ID *string `json:"id,omitempty"` + Name *string `json:"name,omitempty"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + emptyList []string `const:"[]" json:"empty_list,omitempty"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + fixedList []ConstContainerResponseFixedList `const:"[\"read\",\"write\"]" json:"fixed_list,omitempty"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + emptyMap map[string]string `const:"{}" json:"empty_map,omitempty"` + //lint:ignore U1000 accessed via reflection for JSON marshaling + fixedMap map[string]int64 `const:"{\"max\":10}" json:"fixed_map,omitempty"` +} + +func (c ConstContainerResponse) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(c, "", false) +} + +func (c *ConstContainerResponse) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &c, "", false, nil); err != nil { + return err + } + return nil +} + +func (c *ConstContainerResponse) GetID() *string { + if c == nil { + return nil + } + return c.ID +} + +func (c *ConstContainerResponse) GetName() *string { + if c == nil { + return nil + } + return c.Name +} + +func (c *ConstContainerResponse) GetEmptyList() []string { + return []string{} +} + +func (c *ConstContainerResponse) GetFixedList() []ConstContainerResponseFixedList { + return []ConstContainerResponseFixedList{ConstContainerResponseFixedListRead, ConstContainerResponseFixedListWrite} +} + +func (c *ConstContainerResponse) GetEmptyMap() map[string]string { + return map[string]string{} +} + +func (c *ConstContainerResponse) GetFixedMap() map[string]int64 { + return map[string]int64{"max": 10} +} diff --git a/zSDKs/terraform-provider-testing/internal/sdk/sdk.go b/zSDKs/terraform-provider-testing/internal/sdk/sdk.go index 461079173..a31c2eb1d 100644 --- a/zSDKs/terraform-provider-testing/internal/sdk/sdk.go +++ b/zSDKs/terraform-provider-testing/internal/sdk/sdk.go @@ -42192,3 +42192,384 @@ func (s *SDK) DeleteOasContentMediaType(ctx context.Context, request operations. return res, nil } + +// CreateConstContainer - Create a resource whose request schema declares const values on array +// and map properties. Terraform must hide those properties from the +// resource schema and send the const values in the request body. +func (s *SDK) CreateConstContainer(ctx context.Context, request shared.ConstContainerRequest, opts ...operations.Option) (*operations.CreateConstContainerResponse, error) { + o := operations.Options{} + supportedOptions := []string{ + operations.SupportedOptionTimeout, + } + + for _, opt := range opts { + if err := opt(&o, supportedOptions...); err != nil { + return nil, fmt.Errorf("error applying option: %w", err) + } + } + + var baseURL string + if o.ServerURL == nil { + baseURL = utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + } else { + baseURL = *o.ServerURL + } + opURL, err := url.JoinPath(baseURL, "/v0/constcontainers") + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + hookCtx := hooks.HookContext{ + SDK: s, + SDKConfiguration: s.sdkConfiguration, + BaseURL: baseURL, + Context: ctx, + OperationID: "create-const-container", + OAuth2Scopes: []string{}, + SecuritySource: s.sdkConfiguration.Security, + } + bodyReader, reqContentType, err := utils.SerializeRequestBody(ctx, request, false, false, "Request", "json", `request:"mediaType=application/json"`) + if err != nil { + return nil, err + } + + timeout := o.Timeout + if timeout == nil { + timeout = s.sdkConfiguration.Timeout + } + + if timeout != nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *timeout) + defer cancel() + } + + req, err := http.NewRequestWithContext(ctx, "POST", opURL, bodyReader) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + if reqContentType != "" { + req.Header.Set("Content-Type", reqContentType) + } + + if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil { + return nil, err + } + + for k, v := range o.SetHeaders { + req.Header.Set(k, v) + } + + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + return nil, err + } + + httpRes, err := s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{}, httpRes.StatusCode) { + _httpRes, err := s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } else if _httpRes != nil { + httpRes = _httpRes + } + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + + res := &operations.CreateConstContainerResponse{ + StatusCode: httpRes.StatusCode, + ContentType: httpRes.Header.Get("Content-Type"), + RawResponse: httpRes, + } + + switch { + case httpRes.StatusCode == 200: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + var out shared.ConstContainerResponse + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + res.ConstContainerResponse = &out + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, errors.NewSDKError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes) + } + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, errors.NewSDKError("unknown status code returned", httpRes.StatusCode, string(rawBody), httpRes) + } + + return res, nil + +} + +// GetConstContainer - Get a const container resource +func (s *SDK) GetConstContainer(ctx context.Context, request operations.GetConstContainerRequest, opts ...operations.Option) (*operations.GetConstContainerResponse, error) { + o := operations.Options{} + supportedOptions := []string{ + operations.SupportedOptionTimeout, + } + + for _, opt := range opts { + if err := opt(&o, supportedOptions...); err != nil { + return nil, fmt.Errorf("error applying option: %w", err) + } + } + + var baseURL string + if o.ServerURL == nil { + baseURL = utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + } else { + baseURL = *o.ServerURL + } + opURL, err := utils.GenerateURL(ctx, baseURL, "/v0/constcontainer/{id}", request, nil) + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + hookCtx := hooks.HookContext{ + SDK: s, + SDKConfiguration: s.sdkConfiguration, + BaseURL: baseURL, + Context: ctx, + OperationID: "get-const-container", + OAuth2Scopes: []string{}, + SecuritySource: s.sdkConfiguration.Security, + } + + timeout := o.Timeout + if timeout == nil { + timeout = s.sdkConfiguration.Timeout + } + + if timeout != nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *timeout) + defer cancel() + } + + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil { + return nil, err + } + + for k, v := range o.SetHeaders { + req.Header.Set(k, v) + } + + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + return nil, err + } + + httpRes, err := s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{}, httpRes.StatusCode) { + _httpRes, err := s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } else if _httpRes != nil { + httpRes = _httpRes + } + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + + res := &operations.GetConstContainerResponse{ + StatusCode: httpRes.StatusCode, + ContentType: httpRes.Header.Get("Content-Type"), + RawResponse: httpRes, + } + + switch { + case httpRes.StatusCode == 200: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + var out shared.ConstContainerResponse + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + res.ConstContainerResponse = &out + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, errors.NewSDKError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes) + } + case httpRes.StatusCode == 404: + utils.DrainBody(httpRes) + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, errors.NewSDKError("unknown status code returned", httpRes.StatusCode, string(rawBody), httpRes) + } + + return res, nil + +} + +// DeleteConstContainer - Delete a const container resource +func (s *SDK) DeleteConstContainer(ctx context.Context, request operations.DeleteConstContainerRequest, opts ...operations.Option) (*operations.DeleteConstContainerResponse, error) { + o := operations.Options{} + supportedOptions := []string{ + operations.SupportedOptionTimeout, + } + + for _, opt := range opts { + if err := opt(&o, supportedOptions...); err != nil { + return nil, fmt.Errorf("error applying option: %w", err) + } + } + + var baseURL string + if o.ServerURL == nil { + baseURL = utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + } else { + baseURL = *o.ServerURL + } + opURL, err := utils.GenerateURL(ctx, baseURL, "/v0/constcontainer/{id}", request, nil) + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + hookCtx := hooks.HookContext{ + SDK: s, + SDKConfiguration: s.sdkConfiguration, + BaseURL: baseURL, + Context: ctx, + OperationID: "delete-const-container", + OAuth2Scopes: []string{}, + SecuritySource: s.sdkConfiguration.Security, + } + + timeout := o.Timeout + if timeout == nil { + timeout = s.sdkConfiguration.Timeout + } + + if timeout != nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *timeout) + defer cancel() + } + + req, err := http.NewRequestWithContext(ctx, "DELETE", opURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Accept", "*/*") + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil { + return nil, err + } + + for k, v := range o.SetHeaders { + req.Header.Set(k, v) + } + + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + return nil, err + } + + httpRes, err := s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{}, httpRes.StatusCode) { + _httpRes, err := s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } else if _httpRes != nil { + httpRes = _httpRes + } + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + + res := &operations.DeleteConstContainerResponse{ + StatusCode: httpRes.StatusCode, + ContentType: httpRes.Header.Get("Content-Type"), + RawResponse: httpRes, + } + + switch { + case httpRes.StatusCode == 200: + utils.DrainBody(httpRes) + case httpRes.StatusCode == 404: + utils.DrainBody(httpRes) + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, errors.NewSDKError("unknown status code returned", httpRes.StatusCode, string(rawBody), httpRes) + } + + return res, nil + +} From df8db20a0774c426376b6550f48b74a250dbc3d5 Mon Sep 17 00:00:00 2001 From: Ash Godfrey Date: Thu, 22 Oct 2026 11:16:37 +0100 Subject: [PATCH 2/2] fix: address review findings for container const support - Gate container default and const metadata independently so a const cannot ride along on a target that only supports container defaults. - Reject consts on containers with nullable items, whose Go element type is a pointer the const renderer does not produce. - Accept uuid and duration items in container consts. - Compare received const values with exact JSON numbers instead of float64. - Record the cli target on the Go runtime changeset. --- .changesets/1792662097-2435dcc7.yaml | 1 + internal/schemas/handle_array_test.go | 27 ++++++++ internal/schemas/schemas.go | 62 ++++++++----------- .../{{getUtilsLocation}}/json.go.stmpl | 57 +++++++++++++++-- zSDKs/sdk-cli/.speakeasy/gen.lock | 2 +- .../internal/sdk/sdkinternal/utils/json.go | 57 +++++++++++++++-- zSDKs/sdk-go/.speakeasy/gen.lock | 2 +- zSDKs/sdk-go/internal/utils/json.go | 57 +++++++++++++++-- .../.speakeasy/gen.lock | 2 +- .../internal/sdk/internal/utils/json.go | 57 +++++++++++++++-- 10 files changed, 270 insertions(+), 54 deletions(-) diff --git a/.changesets/1792662097-2435dcc7.yaml b/.changesets/1792662097-2435dcc7.yaml index ef6e758ff..daeefe689 100644 --- a/.changesets/1792662097-2435dcc7.yaml +++ b/.changesets/1792662097-2435dcc7.yaml @@ -3,6 +3,7 @@ features: - constsAndDefaults targets: - go + - cli type: fix bump: patch description: const values on array and map fields are emitted when marshaling and accepted when unmarshaling diff --git a/internal/schemas/handle_array_test.go b/internal/schemas/handle_array_test.go index 001150af7..5d4e71f1e 100644 --- a/internal/schemas/handle_array_test.go +++ b/internal/schemas/handle_array_test.go @@ -265,6 +265,33 @@ func TestHandleArray(t *testing.T) { Build()). Build(), }, + { + name: "ArrayWithConstNullableItems", + schemasYAML: `TestSchema: + type: array + items: + type: string + nullable: true + const: + - read`, + schemaName: "TestSchema", + mockFeatures: func() *testutils.MockFeaturesConfig { + config := testutils.NewMockFeaturesConfig() + config.SupportedFeatures = map[features.Feature]bool{ + features.FeatureConstArrays: true, + features.FeatureConstsAndDefaults: true, + } + return config + }, + expected: testutils.NewFieldDef("strings", + testutils.NewTypeDef(ast.DataTypeArray). + WithItemType(testutils.NewTypeDef(ast.DataTypeString).WithLocation(&yaml.Node{Line: 11, Column: 9}).WithValidations(&ast.Validations{}).Build()). + WithContainsNull(true). + WithLocation(&yaml.Node{Line: 9, Column: 7}). + WithValidations(&ast.Validations{}). + Build()). + Build(), + }, { name: "ArrayWithExample", schemasYAML: `TestSchema: diff --git a/internal/schemas/schemas.go b/internal/schemas/schemas.go index b735eaa14..a63fc9414 100644 --- a/internal/schemas/schemas.go +++ b/internal/schemas/schemas.go @@ -562,10 +562,7 @@ func (s *Schemas) handleArray(ctx context.Context, params Params, nullable bool) Optional: nullable, Nullable: nullable, } - if s.containerMetaDataApplies(ctx, schema, features.FeatureDefaultArrays, features.FeatureConstArrays) { - fieldDef = s.handleFieldDefMetaData(ctx, fieldDef, nullable, schema) - s.recordContainerConstUsage(ctx, fieldDef, features.FeatureConstArrays) - } + fieldDef = s.handleContainerMetaData(ctx, fieldDef, nullable, schema, s.Subsystem.Features.IsFeatureSupported(ctx, features.FeatureDefaultArrays), features.FeatureConstArrays) // We need to inherit the multipart annotations if itemField != nil { @@ -819,18 +816,12 @@ func (s *Schemas) handleObject(ctx context.Context, params Params, nullable bool typeDef.Extensions = extensions - mapField := &ast.FieldDef{ + return s.handleContainerMetaData(ctx, &ast.FieldDef{ Name: fieldName, Type: ast.NewType(typeDef, nil), Optional: nullable, Nullable: nullable, - } - if schema.Const != nil && s.Subsystem.Features.IsFeatureSupported(ctx, features.FeatureConstObjects) { - mapField = s.handleFieldDefMetaData(ctx, mapField, nullable, schema) - s.recordContainerConstUsage(ctx, mapField, features.FeatureConstObjects) - } - - return mapField, nil + }, nullable, schema, false, features.FeatureConstObjects), nil } case recurseAdditionalProperties: additionalPropertiesParams := params @@ -898,18 +889,12 @@ func (s *Schemas) handleObject(ctx context.Context, params Params, nullable bool typeDef.Extensions = extensions - mapField := &ast.FieldDef{ + return s.handleContainerMetaData(ctx, &ast.FieldDef{ Name: fieldName, Type: ast.NewType(typeDef, nil), Optional: nullable, Nullable: nullable, - } - if schema.Const != nil && s.Subsystem.Features.IsFeatureSupported(ctx, features.FeatureConstObjects) { - mapField = s.handleFieldDefMetaData(ctx, mapField, nullable, schema) - s.recordContainerConstUsage(ctx, mapField, features.FeatureConstObjects) - } - - return mapField, nil + }, nullable, schema, false, features.FeatureConstObjects), nil } } } @@ -1311,10 +1296,7 @@ func (s *Schemas) handleObject(ctx context.Context, params Params, nullable bool Nullable: nullable, } - if s.containerMetaDataApplies(ctx, schema, features.FeatureDefaultObjects, features.FeatureConstObjects) { - fieldDef = s.handleFieldDefMetaData(ctx, fieldDef, nullable, schema) - s.recordContainerConstUsage(ctx, fieldDef, features.FeatureConstObjects) - } + fieldDef = s.handleContainerMetaData(ctx, fieldDef, nullable, schema, s.Subsystem.Features.IsFeatureSupported(ctx, features.FeatureDefaultObjects), features.FeatureConstObjects) s.setCachedType(fieldType, ¶ms) @@ -1707,6 +1689,10 @@ func (s *Schemas) checkDefaultConformsToType(ctx context.Context, field *ast.Fie } func constContainerValueSupported(val any, typ *ast.TypeDef) bool { + if typ.ContainsNull { + return false + } + switch typ.Type { case ast.DataTypeArray, ast.DataTypeSet: items, ok := val.([]any) @@ -1745,7 +1731,7 @@ func constScalarMatchesType(val any, itemType *ast.TypeDef) bool { return itemType.Enum != nil && slices.Contains(itemType.Enum.Values, fmt.Sprint(val)) case ast.DataTypeBoolean, ast.DataTypeInteger, ast.DataTypeInt32, ast.DataTypeBigInt, ast.DataTypeNumber, ast.DataTypeFloat32, ast.DataTypeDecimal, - ast.DataTypeString, ast.DataTypeDate, ast.DataTypeDateTime: + ast.DataTypeString, ast.DataTypeDate, ast.DataTypeDateTime, ast.DataTypeUUID, ast.DataTypeDuration: dataType := itemType.Type if itemType.Format == "string" { dataType = ast.DataTypeString @@ -2827,20 +2813,26 @@ func (s *Schemas) handleFieldDefMetaData(ctx context.Context, fieldDef *ast.Fiel return fieldDef } -func (s *Schemas) containerMetaDataApplies(ctx context.Context, schema *oas3.Schema, defaultFeature, constFeature features.Feature) bool { - if schema.Default != nil && s.Subsystem.Features.IsFeatureSupported(ctx, defaultFeature) { - return true +func (s *Schemas) handleContainerMetaData(ctx context.Context, fieldDef *ast.FieldDef, nullable bool, schema *oas3.Schema, defaultSupported bool, constFeature features.Feature) *ast.FieldDef { + applyDefault := schema.Default != nil && defaultSupported + applyConst := schema.Const != nil && s.Subsystem.Features.IsFeatureSupported(ctx, constFeature) + if !applyDefault && !applyConst { + return fieldDef } - return schema.Const != nil && s.Subsystem.Features.IsFeatureSupported(ctx, constFeature) -} - -func (s *Schemas) recordContainerConstUsage(ctx context.Context, fieldDef *ast.FieldDef, constFeature features.Feature) { - if fieldDef.Const == nil { - return + fieldDef = s.handleFieldDefMetaData(ctx, fieldDef, nullable, schema) + if !applyDefault { + fieldDef.Default = nil + fieldDef.Optional = nullable + } + if !applyConst { + fieldDef.Const = nil + } + if fieldDef.Const != nil { + s.Subsystem.Features.RecordFeatureUsage(ctx, constFeature) } - s.Subsystem.Features.RecordFeatureUsage(ctx, constFeature) + return fieldDef } func (s *Schemas) sanitizeDefaultConstValue(ctx context.Context, value values.Value) *ast.AnyValue { diff --git a/templates/templates/go/auxiliary/{{getUtilsLocation}}/json.go.stmpl b/templates/templates/go/auxiliary/{{getUtilsLocation}}/json.go.stmpl index 7c319ca06..e8da8c543 100644 --- a/templates/templates/go/auxiliary/{{getUtilsLocation}}/json.go.stmpl +++ b/templates/templates/go/auxiliary/{{getUtilsLocation}}/json.go.stmpl @@ -448,14 +448,63 @@ func implementsJSONMarshaler(v interface{}) bool { } func constJSONValuesEqual(received json.RawMessage, constValue string) bool { - var got, want interface{} - if err := json.Unmarshal(received, &got); err != nil { + got, err := decodeJSONWithNumbers(received) + if err != nil { return false } - if err := json.Unmarshal([]byte(constValue), &want); err != nil { + want, err := decodeJSONWithNumbers([]byte(constValue)) + if err != nil { return false } - return reflect.DeepEqual(got, want) + return jsonValuesEqual(got, want) +} + +func decodeJSONWithNumbers(data []byte) (interface{}, error) { + decoder := json.NewDecoder(bytes.NewReader(data)) + decoder.UseNumber() + var value interface{} + if err := decoder.Decode(&value); err != nil { + return nil, err + } + return value, nil +} + +func jsonValuesEqual(a, b interface{}) bool { + switch av := a.(type) { + case json.Number: + bv, ok := b.(json.Number) + if !ok { + return false + } + ar, aok := new(big.Rat).SetString(av.String()) + br, bok := new(big.Rat).SetString(bv.String()) + return aok && bok && ar.Cmp(br) == 0 + case map[string]interface{}: + bv, ok := b.(map[string]interface{}) + if !ok || len(av) != len(bv) { + return false + } + for key, value := range av { + other, ok := bv[key] + if !ok || !jsonValuesEqual(value, other) { + return false + } + } + return true + case []interface{}: + bv, ok := b.([]interface{}) + if !ok || len(av) != len(bv) { + return false + } + for i := range av { + if !jsonValuesEqual(av[i], bv[i]) { + return false + } + } + return true + default: + return reflect.DeepEqual(a, b) + } } func handleDefaultConstValue(tagValue string, val interface{}, tag reflect.StructTag) json.RawMessage { diff --git a/zSDKs/sdk-cli/.speakeasy/gen.lock b/zSDKs/sdk-cli/.speakeasy/gen.lock index dbcc53eb4..db5cded35 100644 --- a/zSDKs/sdk-cli/.speakeasy/gen.lock +++ b/zSDKs/sdk-cli/.speakeasy/gen.lock @@ -598,7 +598,7 @@ trackedFiles: internal/sdk/sdkinternal/utils/headers.go: last_write_checksum: sha1:1b0bd68e1ea0bce8c5a42b5de9fd020fede7aecc internal/sdk/sdkinternal/utils/json.go: - last_write_checksum: sha1:156c99b390e19b755a3acda6f2f57dcdefcb6206 + last_write_checksum: sha1:7230262498cf1d11a4ca8e4b12e3e9354f1db635 internal/sdk/sdkinternal/utils/oauth2_credentials.go: last_write_checksum: sha1:f9218b6b3dbd11494cc353bf22eb37f0ca654a7d internal/sdk/sdkinternal/utils/oauth2_sessions.go: diff --git a/zSDKs/sdk-cli/internal/sdk/sdkinternal/utils/json.go b/zSDKs/sdk-cli/internal/sdk/sdkinternal/utils/json.go index c8f90dd87..9c7d5d75f 100644 --- a/zSDKs/sdk-cli/internal/sdk/sdkinternal/utils/json.go +++ b/zSDKs/sdk-cli/internal/sdk/sdkinternal/utils/json.go @@ -447,14 +447,63 @@ func implementsJSONMarshaler(v interface{}) bool { } func constJSONValuesEqual(received json.RawMessage, constValue string) bool { - var got, want interface{} - if err := json.Unmarshal(received, &got); err != nil { + got, err := decodeJSONWithNumbers(received) + if err != nil { return false } - if err := json.Unmarshal([]byte(constValue), &want); err != nil { + want, err := decodeJSONWithNumbers([]byte(constValue)) + if err != nil { return false } - return reflect.DeepEqual(got, want) + return jsonValuesEqual(got, want) +} + +func decodeJSONWithNumbers(data []byte) (interface{}, error) { + decoder := json.NewDecoder(bytes.NewReader(data)) + decoder.UseNumber() + var value interface{} + if err := decoder.Decode(&value); err != nil { + return nil, err + } + return value, nil +} + +func jsonValuesEqual(a, b interface{}) bool { + switch av := a.(type) { + case json.Number: + bv, ok := b.(json.Number) + if !ok { + return false + } + ar, aok := new(big.Rat).SetString(av.String()) + br, bok := new(big.Rat).SetString(bv.String()) + return aok && bok && ar.Cmp(br) == 0 + case map[string]interface{}: + bv, ok := b.(map[string]interface{}) + if !ok || len(av) != len(bv) { + return false + } + for key, value := range av { + other, ok := bv[key] + if !ok || !jsonValuesEqual(value, other) { + return false + } + } + return true + case []interface{}: + bv, ok := b.([]interface{}) + if !ok || len(av) != len(bv) { + return false + } + for i := range av { + if !jsonValuesEqual(av[i], bv[i]) { + return false + } + } + return true + default: + return reflect.DeepEqual(a, b) + } } func handleDefaultConstValue(tagValue string, val interface{}, tag reflect.StructTag) json.RawMessage { diff --git a/zSDKs/sdk-go/.speakeasy/gen.lock b/zSDKs/sdk-go/.speakeasy/gen.lock index 7498b2596..40d67eac8 100644 --- a/zSDKs/sdk-go/.speakeasy/gen.lock +++ b/zSDKs/sdk-go/.speakeasy/gen.lock @@ -713,7 +713,7 @@ trackedFiles: internal/utils/headers.go: last_write_checksum: sha1:e529ca3d7ab96a091021bcd471ba2cbef50286b3 internal/utils/json.go: - last_write_checksum: sha1:3ed843422cdd78c41282fe4ab6f22bef8b389c40 + last_write_checksum: sha1:031133cd77b7fb3478635075efe7f830be401140 internal/utils/pathparams.go: last_write_checksum: sha1:440aadb18d886ea0a79af0772eb65cdddc9eee60 internal/utils/queryparams.go: diff --git a/zSDKs/sdk-go/internal/utils/json.go b/zSDKs/sdk-go/internal/utils/json.go index 8106a80db..906ea3072 100644 --- a/zSDKs/sdk-go/internal/utils/json.go +++ b/zSDKs/sdk-go/internal/utils/json.go @@ -447,14 +447,63 @@ func implementsJSONMarshaler(v interface{}) bool { } func constJSONValuesEqual(received json.RawMessage, constValue string) bool { - var got, want interface{} - if err := json.Unmarshal(received, &got); err != nil { + got, err := decodeJSONWithNumbers(received) + if err != nil { return false } - if err := json.Unmarshal([]byte(constValue), &want); err != nil { + want, err := decodeJSONWithNumbers([]byte(constValue)) + if err != nil { return false } - return reflect.DeepEqual(got, want) + return jsonValuesEqual(got, want) +} + +func decodeJSONWithNumbers(data []byte) (interface{}, error) { + decoder := json.NewDecoder(bytes.NewReader(data)) + decoder.UseNumber() + var value interface{} + if err := decoder.Decode(&value); err != nil { + return nil, err + } + return value, nil +} + +func jsonValuesEqual(a, b interface{}) bool { + switch av := a.(type) { + case json.Number: + bv, ok := b.(json.Number) + if !ok { + return false + } + ar, aok := new(big.Rat).SetString(av.String()) + br, bok := new(big.Rat).SetString(bv.String()) + return aok && bok && ar.Cmp(br) == 0 + case map[string]interface{}: + bv, ok := b.(map[string]interface{}) + if !ok || len(av) != len(bv) { + return false + } + for key, value := range av { + other, ok := bv[key] + if !ok || !jsonValuesEqual(value, other) { + return false + } + } + return true + case []interface{}: + bv, ok := b.([]interface{}) + if !ok || len(av) != len(bv) { + return false + } + for i := range av { + if !jsonValuesEqual(av[i], bv[i]) { + return false + } + } + return true + default: + return reflect.DeepEqual(a, b) + } } func handleDefaultConstValue(tagValue string, val interface{}, tag reflect.StructTag) json.RawMessage { diff --git a/zSDKs/terraform-provider-testing/.speakeasy/gen.lock b/zSDKs/terraform-provider-testing/.speakeasy/gen.lock index a9345b240..67cd62b22 100644 --- a/zSDKs/terraform-provider-testing/.speakeasy/gen.lock +++ b/zSDKs/terraform-provider-testing/.speakeasy/gen.lock @@ -2422,7 +2422,7 @@ trackedFiles: internal/sdk/internal/utils/jq.go: last_write_checksum: sha1:2e75ed7a7db1f91e97dac1ac4ca7801b2beb69b5 internal/sdk/internal/utils/json.go: - last_write_checksum: sha1:e4afb6c5125227acd6366acac653831f4d01cd0c + last_write_checksum: sha1:5d331461a0348861323c12b9489fefa388dd6a76 internal/sdk/internal/utils/oauth2_credentials.go: last_write_checksum: sha1:f9218b6b3dbd11494cc353bf22eb37f0ca654a7d internal/sdk/internal/utils/oauth2_sessions.go: diff --git a/zSDKs/terraform-provider-testing/internal/sdk/internal/utils/json.go b/zSDKs/terraform-provider-testing/internal/sdk/internal/utils/json.go index 13a99d20d..83cf65606 100644 --- a/zSDKs/terraform-provider-testing/internal/sdk/internal/utils/json.go +++ b/zSDKs/terraform-provider-testing/internal/sdk/internal/utils/json.go @@ -434,14 +434,63 @@ func implementsJSONMarshaler(v interface{}) bool { } func constJSONValuesEqual(received json.RawMessage, constValue string) bool { - var got, want interface{} - if err := json.Unmarshal(received, &got); err != nil { + got, err := decodeJSONWithNumbers(received) + if err != nil { return false } - if err := json.Unmarshal([]byte(constValue), &want); err != nil { + want, err := decodeJSONWithNumbers([]byte(constValue)) + if err != nil { return false } - return reflect.DeepEqual(got, want) + return jsonValuesEqual(got, want) +} + +func decodeJSONWithNumbers(data []byte) (interface{}, error) { + decoder := json.NewDecoder(bytes.NewReader(data)) + decoder.UseNumber() + var value interface{} + if err := decoder.Decode(&value); err != nil { + return nil, err + } + return value, nil +} + +func jsonValuesEqual(a, b interface{}) bool { + switch av := a.(type) { + case json.Number: + bv, ok := b.(json.Number) + if !ok { + return false + } + ar, aok := new(big.Rat).SetString(av.String()) + br, bok := new(big.Rat).SetString(bv.String()) + return aok && bok && ar.Cmp(br) == 0 + case map[string]interface{}: + bv, ok := b.(map[string]interface{}) + if !ok || len(av) != len(bv) { + return false + } + for key, value := range av { + other, ok := bv[key] + if !ok || !jsonValuesEqual(value, other) { + return false + } + } + return true + case []interface{}: + bv, ok := b.([]interface{}) + if !ok || len(av) != len(bv) { + return false + } + for i := range av { + if !jsonValuesEqual(av[i], bv[i]) { + return false + } + } + return true + default: + return reflect.DeepEqual(a, b) + } } func handleDefaultConstValue(tagValue string, val interface{}, tag reflect.StructTag) json.RawMessage {