├── .changes ├── unreleased │ └── .gitkeep ├── 0.1.0.md ├── 0.2.1.md ├── 0.3.0.md └── 0.2.0.md ├── .github ├── CODEOWNERS ├── workflows │ ├── compliance.yml │ ├── ci-github-actions.yml │ ├── ci-goreleaser.yml │ ├── lock.yml │ ├── ci-changie.yml │ ├── ci-go.yml │ ├── issue-comment-triage.yml │ └── release.yml ├── dependabot.yml └── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.yml │ └── bug_report.yml ├── internal ├── log │ ├── doc.go │ └── warn.go ├── config │ └── doc.go ├── explorer │ ├── doc.go │ ├── explorer_utils.go │ ├── explorer.go │ └── guesstimator_explorer_test.go ├── mapper │ ├── doc.go │ ├── oas │ │ ├── doc.go │ │ ├── element_type.go │ │ ├── object.go │ │ ├── bool.go │ │ ├── single_nested.go │ │ ├── schema_error.go │ │ ├── integer.go │ │ ├── string.go │ │ ├── number.go │ │ ├── oas_schema_test.go │ │ ├── attribute.go │ │ └── oas_schema.go │ ├── util │ │ ├── sort.go │ │ ├── oas_constants.go │ │ ├── framework_identifier.go │ │ ├── attribute_type.go │ │ └── framework_identifier_test.go │ ├── attrmapper │ │ ├── doc.go │ │ ├── provider_attributes.go │ │ ├── types.go │ │ ├── bool.go │ │ ├── int64.go │ │ ├── number.go │ │ ├── string.go │ │ ├── float64.go │ │ ├── map.go │ │ ├── set.go │ │ ├── list.go │ │ ├── resource_attributes.go │ │ ├── data_source_attributes.go │ │ ├── single_nested.go │ │ ├── map_nested.go │ │ ├── set_nested.go │ │ └── list_nested.go │ ├── frameworkvalidators │ │ ├── doc.go │ │ ├── code_import.go │ │ ├── float64validator.go │ │ ├── float64validator_test.go │ │ ├── mapvalidator.go │ │ ├── setvalidator.go │ │ ├── mapvalidator_test.go │ │ ├── setvalidator_test.go │ │ ├── listvalidator.go │ │ ├── int64validator.go │ │ ├── listvalidator_test.go │ │ ├── int64validator_test.go │ │ ├── stringvalidator.go │ │ └── stringvalidator_test.go │ ├── provider_mapper.go │ ├── datasource_mapper.go │ └── resource_mapper.go └── cmd │ ├── testdata │ ├── kubernetes │ │ └── generator_config.yml │ ├── edgecase │ │ └── generator_config.yml │ ├── github │ │ └── generator_config.yml │ ├── scaleway │ │ └── generator_config.yml │ └── petstore3 │ │ └── generator_config.yml │ └── generate_test.go ├── META.d └── _summary.yaml ├── tools ├── copywrite.go └── go.mod ├── .gitignore ├── .golangci.yml ├── .goreleaser.yml ├── .changie.yaml ├── .copywrite.hcl ├── cmd └── tfplugingen-openapi │ ├── version.go │ └── main.go ├── Makefile ├── CHANGELOG.md ├── go.mod └── README.md /.changes/unreleased/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @hashicorp/terraform-devex -------------------------------------------------------------------------------- /internal/log/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | // Package log contains helper functions for structured logging 5 | package log 6 | -------------------------------------------------------------------------------- /internal/config/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | // Package config contains the types, parsing, and validation logic for the YAML generator configuration 5 | package config 6 | -------------------------------------------------------------------------------- /META.d/_summary.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | schema: 1.1 3 | 4 | partition: tf-ecosystem 5 | 6 | summary: 7 | owner: team-tf-core-plugins 8 | description: | 9 | OpenAPI to Terraform Provider Code Generation Specification 10 | visibility: public 11 | -------------------------------------------------------------------------------- /.changes/0.1.0.md: -------------------------------------------------------------------------------- 1 | ## 0.1.0 (October 17, 2023) 2 | 3 | NOTES: 4 | 5 | * Initial release of `tfplugingen-openapi` CLI for Terraform Provider Code Generation tech preview ([#68](https://github.com/hashicorp/terraform-plugin-codegen-openapi/issues/68)) 6 | 7 | -------------------------------------------------------------------------------- /internal/explorer/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | // Package explorer contains the types and methods for relating OpenAPI operations to a set of Terraform Provider resource/data source actions (CRUD) 5 | package explorer 6 | -------------------------------------------------------------------------------- /internal/mapper/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | // Package mapper includes the types and methods for mapping provider, resource, and data source 5 | // types (explorer package) from an OpenAPI specification into Provider Code Specification. 6 | package mapper 7 | -------------------------------------------------------------------------------- /internal/mapper/oas/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | // Package oas contains the logic that determines how to map OpenAPI schemas to the intermediate attrmapper 5 | // types. OpenAPI 3.1 schemas are compatible with JSON schema, and OpenAPI 3.0 are "mostly" compatible. 6 | package oas 7 | -------------------------------------------------------------------------------- /tools/copywrite.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | //go:build generate 5 | 6 | package tools 7 | 8 | import ( 9 | // copywrite header generation 10 | _ "github.com/hashicorp/copywrite" 11 | ) 12 | 13 | //go:generate go run github.com/hashicorp/copywrite headers -d .. --config ../.copywrite.hcl 14 | -------------------------------------------------------------------------------- /.changes/0.2.1.md: -------------------------------------------------------------------------------- 1 | ## 0.2.1 (December 13, 2023) 2 | 3 | BUG FIXES: 4 | 5 | * Fixed a bug where schemas that used `additionalProperties` with schema composition (allOf/anyOf/oneOf) would return an empty single nested attribute. Will now return map or map nested attribute. ([#100](https://github.com/hashicorp/terraform-plugin-codegen-openapi/issues/100)) 6 | 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # OS generated files 15 | .DS_Store 16 | 17 | # JetBrains IDEs files 18 | .idea/ 19 | *.iws 20 | 21 | # VSCode files 22 | .vscode/ 23 | -------------------------------------------------------------------------------- /internal/mapper/util/sort.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package util 5 | 6 | import "sort" 7 | 8 | // Generics? ☜(ಠ_ಠ☜) 9 | func SortedKeys[V any](m map[string]V) []string { 10 | keys := make([]string, 0) 11 | 12 | for key := range m { 13 | keys = append(keys, key) 14 | } 15 | sort.Strings(keys) 16 | 17 | return keys 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/compliance.yml: -------------------------------------------------------------------------------- 1 | name: compliance 2 | 3 | on: 4 | pull_request: 5 | 6 | permissions: 7 | contents: read 8 | 9 | jobs: 10 | # Reference: ENGSRV-059 11 | copywrite: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 15 | - uses: hashicorp/setup-copywrite@32638da2d4e81d56a0764aa1547882fc4d209636 # v1.1.3 16 | - run: copywrite headers --plan 17 | - run: copywrite license --plan -------------------------------------------------------------------------------- /internal/mapper/attrmapper/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | // Package attrmapper contains types and methods that provide an intermediate step between the OpenAPI schema 5 | // types (libopenapi) and the Provider Code Specification types (terraform-plugin-codegen-spec). This intermediate 6 | // step enables merging of attributes, overriding of specific properties, and converting into a provider code spec type 7 | // to be marshalled to JSON. 8 | package attrmapper 9 | -------------------------------------------------------------------------------- /.changes/0.3.0.md: -------------------------------------------------------------------------------- 1 | ## 0.3.0 (January 19, 2024) 2 | 3 | ENHANCEMENTS: 4 | 5 | * Added data source and resource support for query and path parameters specified in the [OAS Path Item](https://spec.openapis.org/oas/v3.1.0#path-item-object) ([#114](https://github.com/hashicorp/terraform-plugin-codegen-openapi/issues/114)) 6 | 7 | BUG FIXES: 8 | 9 | * Fixed a bug where schema defaults were not detected for integer/int32 properties ([#111](https://github.com/hashicorp/terraform-plugin-codegen-openapi/issues/111)) 10 | 11 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | issues: 2 | max-issues-per-linter: 0 3 | max-same-issues: 0 4 | 5 | linters: 6 | disable-all: true 7 | enable: 8 | - copyloopvar 9 | - durationcheck 10 | - errcheck 11 | - forcetypeassert 12 | - gofmt 13 | - gosimple 14 | - govet 15 | - ineffassign 16 | - makezero 17 | - misspell 18 | - nilerr 19 | - paralleltest 20 | - predeclared 21 | - staticcheck 22 | - unconvert 23 | - unparam 24 | - unused 25 | - usetesting 26 | 27 | run: 28 | # Prevent false positive timeouts in CI 29 | timeout: 5m -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | - package-ecosystem: "gomod" 8 | directory: "/tools" 9 | schedule: 10 | interval: "daily" 11 | - package-ecosystem: "github-actions" 12 | directory: "/" 13 | groups: 14 | "github-actions": 15 | patterns: 16 | - "*" # Group all GitHub Actions dependencies together 17 | schedule: 18 | interval: "weekly" 19 | day: "monday" 20 | time: "09:00" 21 | timezone: "Etc/UTC" 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Code Generation Feedback 4 | url: https://discuss.hashicorp.com/c/terraform-providers/tf-plugin-sdk 5 | about: Please share any overall feedback relating to provider code generation by creating a new topic in the Plugin Development Community Forum. 6 | - name: ❓ Code Generation Questions 7 | url: https://discuss.hashicorp.com/c/terraform-providers/tf-plugin-sdk 8 | about: Please ask any general questions about provider code generation by creating a new topic in the Plugin Development Community Forum. -------------------------------------------------------------------------------- /internal/mapper/frameworkvalidators/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | // Package frameworkvalidators contains functionality for mapping validations 5 | // onto specification that uses terraform-plugin-framework-validators. 6 | // 7 | // Currently, the specification requires all schema validations to be written 8 | // as "custom" validations. Over time, the specification may begin to support 9 | // "native" validations for very common use cases as specific properties, which 10 | // would alleviate some of the need of this package. 11 | package frameworkvalidators 12 | -------------------------------------------------------------------------------- /.changes/0.2.0.md: -------------------------------------------------------------------------------- 1 | ## 0.2.0 (October 30, 2023) 2 | 3 | FEATURES: 4 | 5 | * Added schema.ignores option to generator config for resources, data sources, and providers. Allows excluding attributes from OAS mapping ([#81](https://github.com/hashicorp/terraform-plugin-codegen-openapi/issues/81)) 6 | 7 | ENHANCEMENTS: 8 | 9 | * Added data source support for response body arrays ([#16](https://github.com/hashicorp/terraform-plugin-codegen-openapi/issues/16)) 10 | * Schemas that have the `properties` keyword defined with no type will now default to `object` ([#79](https://github.com/hashicorp/terraform-plugin-codegen-openapi/issues/79)) 11 | 12 | -------------------------------------------------------------------------------- /internal/mapper/frameworkvalidators/code_import.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package frameworkvalidators 5 | 6 | import "github.com/hashicorp/terraform-plugin-codegen-spec/code" 7 | 8 | const ( 9 | // CodeImportBasePath is the base code import path for framework validators. 10 | CodeImportBasePath = "github.com/hashicorp/terraform-plugin-framework-validators" 11 | ) 12 | 13 | // CodeImport returns the framework validators code import for the given path. 14 | func CodeImport(packagePath string) code.Import { 15 | return code.Import{ 16 | Path: CodeImportBasePath + "/" + packagePath, 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/ci-github-actions.yml: -------------------------------------------------------------------------------- 1 | # Continuous integration handling for GitHub Actions workflows 2 | name: ci-github-actions 3 | 4 | on: 5 | pull_request: 6 | paths: 7 | - .github/workflows/*.yml 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | actionlint: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 17 | - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 18 | with: 19 | go-version-file: 'go.mod' 20 | - run: go install github.com/rhysd/actionlint/cmd/actionlint@latest 21 | - run: actionlint -------------------------------------------------------------------------------- /internal/cmd/testdata/kubernetes/generator_config.yml: -------------------------------------------------------------------------------- 1 | # Ref: https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/deployment_v1 2 | provider: 3 | name: kubernetes 4 | 5 | resources: 6 | deployment_v1: 7 | create: 8 | path: /apis/apps/v1/namespaces/{namespace}/deployments 9 | method: POST 10 | read: 11 | path: /apis/apps/v1/namespaces/{namespace}/deployments/{name} 12 | method: GET 13 | update: 14 | path: /apis/apps/v1/namespaces/{namespace}/deployments/{name} 15 | method: PUT 16 | delete: 17 | path: /apis/apps/v1/namespaces/{namespace}/deployments/{name} 18 | method: DELETE 19 | -------------------------------------------------------------------------------- /internal/mapper/attrmapper/provider_attributes.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package attrmapper 5 | 6 | import ( 7 | "github.com/hashicorp/terraform-plugin-codegen-spec/provider" 8 | ) 9 | 10 | type ProviderAttribute interface { 11 | ToSpec() provider.Attribute 12 | } 13 | 14 | type ProviderAttributes []ProviderAttribute 15 | 16 | func (attributes ProviderAttributes) ToSpec() []provider.Attribute { 17 | specAttributes := make([]provider.Attribute, 0, len(attributes)) 18 | for _, attribute := range attributes { 19 | specAttributes = append(specAttributes, attribute.ToSpec()) 20 | } 21 | 22 | return specAttributes 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/ci-goreleaser.yml: -------------------------------------------------------------------------------- 1 | # Continuous integration handling for GoReleaser 2 | name: ci-goreleaser 3 | 4 | on: 5 | pull_request: 6 | paths: 7 | - .github/workflows/ci-goreleaser.yml 8 | - .goreleaser.yml 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | check: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 18 | - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 19 | with: 20 | go-version-file: 'go.mod' 21 | - uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0 22 | with: 23 | args: check -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | project_name: tfplugingen-openapi 3 | builds: 4 | - main: ./cmd/tfplugingen-openapi 5 | env: 6 | - CGO_ENABLED=0 7 | mod_timestamp: '{{ .CommitTimestamp }}' 8 | flags: 9 | - -trimpath 10 | ldflags: 11 | - '-s -w -X main.version={{.Version}} -X main.commit={{.Commit}}' 12 | goos: 13 | - windows 14 | - linux 15 | - darwin 16 | goarch: 17 | - amd64 18 | - arm64 19 | binary: '{{ .ProjectName }}' 20 | archives: 21 | - formats: [ 'zip' ] 22 | name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}' 23 | checksum: 24 | name_template: '{{ .ProjectName }}_{{ .Version }}_SHA256SUMS' 25 | algorithm: sha256 26 | milestones: 27 | - close: true 28 | release: -------------------------------------------------------------------------------- /.changie.yaml: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT - This GitHub Workflow is managed by automation 2 | # https://github.com/hashicorp/terraform-devex-repos 3 | changesDir: .changes 4 | unreleasedDir: unreleased 5 | changelogPath: CHANGELOG.md 6 | versionExt: md 7 | versionFormat: '## {{.Version}} ({{.Time.Format "January 02, 2006"}})' 8 | kindFormat: '{{.Kind}}:' 9 | changeFormat: '* {{.Body}} ([#{{.Custom.Issue}}](https://github.com/hashicorp/terraform-plugin-codegen-openapi/issues/{{.Custom.Issue}}))' 10 | custom: 11 | - key: Issue 12 | label: Issue/PR Number 13 | type: int 14 | minInt: 1 15 | kinds: 16 | - label: BREAKING CHANGES 17 | - label: NOTES 18 | - label: FEATURES 19 | - label: ENHANCEMENTS 20 | - label: BUG FIXES 21 | newlines: 22 | afterKind: 1 23 | beforeKind: 1 24 | endOfVersion: 2 25 | -------------------------------------------------------------------------------- /internal/log/warn.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package log 5 | 6 | import ( 7 | "errors" 8 | "log/slog" 9 | 10 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/mapper/oas" 11 | ) 12 | 13 | // WarnLogOnError inspects the error type and extracts additional information for structured logging if possible 14 | func WarnLogOnError(logger *slog.Logger, err error, message string) { 15 | if err == nil { 16 | return 17 | } 18 | 19 | var schemaErr *oas.SchemaError 20 | if errors.As(err, &schemaErr) { 21 | if schemaErr.Path() != "" { 22 | logger = logger.With("oas_path", schemaErr.Path()) 23 | } 24 | if schemaErr.LineNumber() != 0 { 25 | logger = logger.With("oas_line_number", schemaErr.LineNumber()) 26 | } 27 | } 28 | 29 | logger.Warn(message, "err", err) 30 | } 31 | -------------------------------------------------------------------------------- /.copywrite.hcl: -------------------------------------------------------------------------------- 1 | schema_version = 1 2 | 3 | project { 4 | license = "MPL-2.0" 5 | copyright_year = 2023 6 | 7 | header_ignore = [ 8 | # internal catalog metadata (prose) 9 | "META.d/**/*.yaml", 10 | 11 | # changie tooling configuration and CHANGELOG entries (prose) 12 | ".changes/unreleased/*.yaml", 13 | ".changie.yaml", 14 | 15 | # GitHub issue template configuration 16 | ".github/ISSUE_TEMPLATE/*.yml", 17 | 18 | # GitHub Actions workflow-specific configurations 19 | ".github/labeler-*.yml", 20 | 21 | # golangci-lint tooling configuration 22 | ".golangci.yml", 23 | 24 | # GoReleaser tooling configuration 25 | ".goreleaser.yml", 26 | 27 | # Release Engineering tooling configuration 28 | ".release/*.hcl", 29 | 30 | # Unit test data files 31 | "internal/cmd/testdata/**", 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/lock.yml: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT - This GitHub Workflow is managed by automation 2 | # https://github.com/hashicorp/terraform-devex-repos 3 | name: 'Lock Threads' 4 | 5 | on: 6 | schedule: 7 | - cron: '49 18 * * *' 8 | 9 | jobs: 10 | lock: 11 | runs-on: ubuntu-latest 12 | steps: 13 | # NOTE: When TSCCR updates the GitHub action version, update the template workflow file to avoid drift: 14 | # https://github.com/hashicorp/terraform-devex-repos/blob/main/modules/repo/workflows/lock.tftpl 15 | - uses: dessant/lock-threads@7266a7ce5c1df01b1c6db85bf8cd86c737dadbe7 # v6.0.0 16 | with: 17 | process-only: 'issues, prs' 18 | github-token: ${{ github.token }} 19 | issue-inactive-days: '30' 20 | issue-lock-reason: resolved 21 | pr-inactive-days: '30' 22 | pr-lock-reason: resolved 23 | -------------------------------------------------------------------------------- /.github/workflows/ci-changie.yml: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT - This GitHub Workflow is managed by automation 2 | # https://github.com/hashicorp/terraform-devex-repos 3 | 4 | # Continuous integration handling for changie 5 | name: ci-changie 6 | 7 | on: 8 | pull_request: 9 | paths: 10 | - .changes/unreleased/*.yaml 11 | - .changie.yaml 12 | - .github/workflows/ci-changie.yml 13 | 14 | permissions: 15 | contents: read 16 | 17 | jobs: 18 | check: 19 | runs-on: ubuntu-latest 20 | steps: 21 | # Ensure terraform-devex-repos is updated on version changes. 22 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 23 | # Ensure terraform-devex-repos is updated on version changes. 24 | - uses: miniscruff/changie-action@5036dffa79ffc007110dc7f75eca7ef72780e147 # v2.1.0 25 | with: 26 | version: latest 27 | args: batch patch --dry-run 28 | -------------------------------------------------------------------------------- /.github/workflows/ci-go.yml: -------------------------------------------------------------------------------- 1 | # Continuous integration handling for Go 2 | name: ci-go 3 | 4 | on: 5 | pull_request: 6 | paths: 7 | - .github/workflows/ci-go.yml 8 | - go.mod 9 | - '**.go' 10 | - 'internal/cmd/testdata/**' 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | test: 17 | runs-on: ubuntu-latest 18 | env: 19 | GOPRIVATE: github.com/hashicorp/terraform-plugin-codegen-spec 20 | steps: 21 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 22 | - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 23 | with: 24 | go-version-file: 'go.mod' 25 | - run: go mod download 26 | - run: go test -coverprofile=coverage.out ./... 27 | - run: go tool cover -html=coverage.out -o coverage.html 28 | - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 29 | with: 30 | name: go-coverage 31 | path: coverage.html -------------------------------------------------------------------------------- /cmd/tfplugingen-openapi/version.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "runtime/debug" 9 | ) 10 | 11 | var ( 12 | // These vars will be set by goreleaser. 13 | version string 14 | commit string 15 | ) 16 | 17 | func getVersion() string { 18 | // Prefer global version as it's set by goreleaser via ldflags 19 | // https://goreleaser.com/cookbooks/using-main.version/ 20 | if version != "" { 21 | if commit != "" { 22 | version = fmt.Sprintf("%s from commit: %s", version, commit) 23 | } 24 | return version 25 | } 26 | 27 | // If not built with goreleaser, check the binary for VCS revision/module version info 28 | if info, ok := debug.ReadBuildInfo(); ok { 29 | for _, setting := range info.Settings { 30 | if setting.Key == "vcs.revision" { 31 | return fmt.Sprintf("commit: %s", setting.Value) 32 | } 33 | } 34 | 35 | return fmt.Sprintf("module: %s", info.Main.Version) 36 | } 37 | 38 | return "local" 39 | } 40 | -------------------------------------------------------------------------------- /.github/workflows/issue-comment-triage.yml: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT - This GitHub Workflow is managed by automation 2 | # https://github.com/hashicorp/terraform-devex-repos 3 | name: Issue Comment Triage 4 | 5 | on: 6 | issue_comment: 7 | types: [created] 8 | 9 | jobs: 10 | issue_comment_triage: 11 | runs-on: ubuntu-latest 12 | env: 13 | # issue_comment events are triggered by comments on issues and pull requests. Checking the 14 | # value of github.event.issue.pull_request tells us whether the issue is an issue or is 15 | # actually a pull request, allowing us to dynamically set the gh subcommand: 16 | # https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#issue_comment-on-issues-only-or-pull-requests-only 17 | COMMAND: ${{ github.event.issue.pull_request && 'pr' || 'issue' }} 18 | GH_TOKEN: ${{ github.token }} 19 | steps: 20 | - name: 'Remove waiting-response on comment' 21 | run: gh ${{ env.COMMAND }} edit ${{ github.event.issue.html_url }} --remove-label waiting-response 22 | -------------------------------------------------------------------------------- /internal/mapper/oas/element_type.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package oas 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/mapper/util" 10 | "github.com/hashicorp/terraform-plugin-codegen-spec/schema" 11 | ) 12 | 13 | func (s *OASSchema) BuildElementType() (schema.ElementType, *SchemaError) { 14 | switch s.Type { 15 | case util.OAS_type_string: 16 | return s.BuildStringElementType() 17 | case util.OAS_type_integer: 18 | return s.BuildIntegerElementType() 19 | case util.OAS_type_number: 20 | return s.BuildNumberElementType() 21 | case util.OAS_type_boolean: 22 | return s.BuildBoolElementType() 23 | case util.OAS_type_array: 24 | return s.BuildCollectionElementType() 25 | case util.OAS_type_object: 26 | if s.IsMap() { 27 | return s.BuildMapElementType() 28 | } 29 | return s.BuildObjectElementType() 30 | 31 | default: 32 | return schema.ElementType{}, SchemaErrorFromNode(fmt.Errorf("invalid schema type '%s'", s.Type), s.Schema, Type) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /internal/cmd/testdata/edgecase/generator_config.yml: -------------------------------------------------------------------------------- 1 | provider: 2 | name: edgecase 3 | schema_ref: '#/components/schemas/edgecase_provider' 4 | ignores: 5 | - triple_nested_map.ignore_me_1 6 | - triple_nested_map.obj_prop.ignore_me_2 7 | 8 | resources: 9 | set_test: 10 | create: 11 | path: /set_test 12 | method: POST 13 | read: 14 | path: /set_test 15 | method: GET 16 | schema: 17 | ignores: 18 | - setnested_prop.string_prop 19 | 20 | map_test: 21 | create: 22 | path: /map_test 23 | method: POST 24 | read: 25 | path: /map_test 26 | method: GET 27 | 28 | data_sources: 29 | nested_collections: 30 | read: 31 | path: /nested_collections 32 | method: GET 33 | schema: 34 | ignores: 35 | - triple_nested_map.ignore_me_1 36 | - triple_nested_map.obj_prop.ignore_me_2 37 | 38 | set_test: 39 | read: 40 | path: /set_test 41 | method: GET 42 | map_test: 43 | read: 44 | path: /map_test 45 | method: GET 46 | obj_no_type: 47 | read: 48 | path: /obj_no_type 49 | method: GET -------------------------------------------------------------------------------- /internal/cmd/testdata/github/generator_config.yml: -------------------------------------------------------------------------------- 1 | # Ref: https://registry.terraform.io/providers/integrations/github/latest/docs 2 | provider: 3 | name: github 4 | 5 | resources: 6 | # Ref: https://registry.terraform.io/providers/integrations/github/latest/docs/resources/repository 7 | repository: 8 | create: 9 | path: /user/repos 10 | method: POST 11 | read: 12 | path: /repos/{owner}/{repo} 13 | method: GET 14 | update: 15 | path: /repos/{owner}/{repo} 16 | method: PATCH 17 | delete: 18 | path: /repos/{owner}/{repo} 19 | method: DELETE 20 | schema: 21 | attributes: 22 | aliases: 23 | repo: name 24 | 25 | data_sources: 26 | gists: 27 | read: 28 | path: /gists/{gist_id} 29 | method: GET 30 | schema: 31 | ignores: 32 | - fork_of.forks 33 | - fork_of.history 34 | - fork_of.files.language 35 | - forks.user.plan.collaborators 36 | 37 | repository: 38 | read: 39 | path: /repos/{owner}/{repo} 40 | method: GET 41 | schema: 42 | attributes: 43 | aliases: 44 | repo: name -------------------------------------------------------------------------------- /internal/explorer/explorer_utils.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package explorer 5 | 6 | import high "github.com/pb33f/libopenapi/datamodel/high/v3" 7 | 8 | func mergeParameters(commonParameters []*high.Parameter, operation *high.Operation) []*high.Parameter { 9 | mergedParameters := make([]*high.Parameter, len(commonParameters)) 10 | copy(mergedParameters, commonParameters) 11 | if operation != nil { 12 | for _, operationParameter := range operation.Parameters { 13 | found := false 14 | for i, mergedParameter := range mergedParameters { 15 | if operationParameter.Name == mergedParameter.Name { 16 | found = true 17 | mergedParameters[i] = operationParameter 18 | break 19 | } 20 | } 21 | if !found { 22 | mergedParameters = append(mergedParameters, operationParameter) // nolint: makezero 23 | } 24 | } 25 | } 26 | return mergedParameters 27 | } 28 | 29 | func (e *Resource) ReadOpParameters() []*high.Parameter { 30 | return mergeParameters(e.CommonParameters, e.ReadOp) 31 | } 32 | 33 | func (e *DataSource) ReadOpParameters() []*high.Parameter { 34 | return mergeParameters(e.CommonParameters, e.ReadOp) 35 | } 36 | -------------------------------------------------------------------------------- /internal/mapper/util/oas_constants.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package util 5 | 6 | // Reference links: 7 | // - [JSON Schema - types] 8 | // - [JSON Schema - format] 9 | // - [JSON schema - custom format] 10 | // - [OAS - format] 11 | // 12 | // [JSON Schema - types]: https://json-schema.org/draft/2020-12/json-schema-core.html#name-instance-data-model 13 | // [JSON Schema - format]: https://json-schema.org/draft/2020-12/json-schema-validation.html#name-defined-formats 14 | // [JSON schema - custom format]: https://json-schema.org/draft/2020-12/json-schema-validation.html#name-custom-format-attributes 15 | // [OAS - format]: https://spec.openapis.org/oas/latest.html#data-types 16 | const ( 17 | OAS_type_string = "string" 18 | OAS_type_integer = "integer" 19 | OAS_type_number = "number" 20 | OAS_type_boolean = "boolean" 21 | OAS_type_array = "array" 22 | OAS_type_object = "object" 23 | OAS_type_null = "null" 24 | 25 | OAS_format_double = "double" 26 | OAS_format_float = "float" 27 | OAS_format_password = "password" 28 | 29 | OAS_param_path = "path" 30 | OAS_param_query = "query" 31 | 32 | // Custom format for SetNested and Set attributes 33 | TF_format_set = "set" 34 | 35 | OAS_mediatype_json = "application/json" 36 | 37 | OAS_response_code_ok = "200" 38 | OAS_response_code_created = "201" 39 | ) 40 | -------------------------------------------------------------------------------- /internal/mapper/oas/object.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package oas 5 | 6 | import ( 7 | "context" 8 | 9 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/mapper/util" 10 | "github.com/hashicorp/terraform-plugin-codegen-spec/schema" 11 | "github.com/pb33f/libopenapi/orderedmap" 12 | ) 13 | 14 | func (s *OASSchema) BuildObjectElementType() (schema.ElementType, *SchemaError) { 15 | objectElemTypes := []schema.ObjectAttributeType{} 16 | 17 | sortedProperties := orderedmap.SortAlpha(s.Schema.Properties) 18 | for pair := range orderedmap.Iterate(context.TODO(), sortedProperties) { 19 | name := pair.Key() 20 | 21 | if s.IsPropertyIgnored(name) { 22 | continue 23 | } 24 | 25 | pProxy := pair.Value() 26 | schemaOpts := SchemaOpts{ 27 | Ignores: s.GetIgnoresForNested(name), 28 | } 29 | 30 | pSchema, err := BuildSchema(pProxy, schemaOpts, s.GlobalSchemaOpts) 31 | if err != nil { 32 | return schema.ElementType{}, s.NestSchemaError(err, name) 33 | } 34 | 35 | elemType, err := pSchema.BuildElementType() 36 | if err != nil { 37 | return schema.ElementType{}, s.NestSchemaError(err, name) 38 | } 39 | 40 | objectElemTypes = append(objectElemTypes, util.CreateObjectAttributeType(name, elemType)) 41 | } 42 | 43 | return schema.ElementType{ 44 | Object: &schema.ObjectType{ 45 | AttributeTypes: objectElemTypes, 46 | }, 47 | }, nil 48 | } 49 | -------------------------------------------------------------------------------- /internal/mapper/frameworkvalidators/float64validator.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package frameworkvalidators 5 | 6 | import ( 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/hashicorp/terraform-plugin-codegen-spec/code" 11 | "github.com/hashicorp/terraform-plugin-codegen-spec/schema" 12 | ) 13 | 14 | const ( 15 | // Float64ValidatorPackage is the name of the float64 validation package in 16 | // the framework validators module. 17 | Float64ValidatorPackage = "float64validator" 18 | ) 19 | 20 | var ( 21 | // Float64ValidatorCodeImport is a single allocation of the framework 22 | // validators module float64validator package import. 23 | Float64ValidatorCodeImport code.Import = CodeImport(Float64ValidatorPackage) 24 | ) 25 | 26 | // Float64ValidatorOneOf returns a custom validator mapped to the Float64validator 27 | // package OneOf function. If the values are nil or empty, nil is returned. 28 | func Float64ValidatorOneOf(values []float64) *schema.CustomValidator { 29 | if len(values) == 0 { 30 | return nil 31 | } 32 | 33 | var schemaDefinition strings.Builder 34 | 35 | schemaDefinition.WriteString(Float64ValidatorPackage) 36 | schemaDefinition.WriteString(".OneOf(\n") 37 | 38 | for _, value := range values { 39 | schemaDefinition.WriteString(strconv.FormatFloat(value, 'f', -1, 64) + ",\n") 40 | } 41 | 42 | schemaDefinition.WriteString(")") 43 | 44 | return &schema.CustomValidator{ 45 | Imports: []code.Import{ 46 | Float64ValidatorCodeImport, 47 | }, 48 | SchemaDefinition: schemaDefinition.String(), 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /internal/explorer/explorer.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package explorer 5 | 6 | import ( 7 | "github.com/pb33f/libopenapi/datamodel/high/base" 8 | high "github.com/pb33f/libopenapi/datamodel/high/v3" 9 | ) 10 | 11 | // Explorer implements methods that relate OpenAPI operations to a set of Terraform Provider resource/data source actions (CRUD) 12 | type Explorer interface { 13 | FindProvider() (Provider, error) 14 | FindResources() (map[string]Resource, error) 15 | FindDataSources() (map[string]DataSource, error) 16 | } 17 | 18 | // Resource contains CRUD operations and schema options for configuration. 19 | type Resource struct { 20 | CreateOp *high.Operation 21 | ReadOp *high.Operation 22 | UpdateOp *high.Operation 23 | DeleteOp *high.Operation 24 | CommonParameters []*high.Parameter 25 | SchemaOptions SchemaOptions 26 | } 27 | 28 | // DataSource contains a Read operation and schema options for configuration. 29 | type DataSource struct { 30 | ReadOp *high.Operation 31 | CommonParameters []*high.Parameter 32 | SchemaOptions SchemaOptions 33 | } 34 | 35 | // Provider contains a name and a schema. 36 | type Provider struct { 37 | Name string 38 | SchemaProxy *base.SchemaProxy 39 | Ignores []string 40 | } 41 | 42 | type SchemaOptions struct { 43 | Ignores []string 44 | AttributeOptions AttributeOptions 45 | } 46 | 47 | type AttributeOptions struct { 48 | Aliases map[string]string 49 | Overrides map[string]Override 50 | } 51 | 52 | type Override struct { 53 | Description string 54 | } 55 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | go build ./cmd/tfplugingen-openapi 3 | 4 | lint: 5 | golangci-lint run 6 | 7 | fmt: 8 | gofmt -s -w -e . 9 | 10 | test: 11 | go test $$(go list ./... | grep -v /output) -v -cover -timeout=120s -parallel=4 12 | 13 | # Generate copywrite headers 14 | generate: 15 | cd tools; go generate ./... 16 | 17 | # Regenerate testdata folder 18 | testdata: 19 | go run ./cmd/tfplugingen-openapi generate \ 20 | --config ./internal/cmd/testdata/petstore3/generator_config.yml \ 21 | --output ./internal/cmd/testdata/petstore3/provider_code_spec.json \ 22 | ./internal/cmd/testdata/petstore3/openapi_spec.json 23 | 24 | go run ./cmd/tfplugingen-openapi generate \ 25 | --config ./internal/cmd/testdata/github/generator_config.yml \ 26 | --output ./internal/cmd/testdata/github/provider_code_spec.json \ 27 | ./internal/cmd/testdata/github/openapi_spec.json 28 | 29 | go run ./cmd/tfplugingen-openapi generate \ 30 | --config ./internal/cmd/testdata/scaleway/generator_config.yml \ 31 | --output ./internal/cmd/testdata/scaleway/provider_code_spec.json \ 32 | ./internal/cmd/testdata/scaleway/openapi_spec.yml 33 | 34 | go run ./cmd/tfplugingen-openapi generate \ 35 | --config ./internal/cmd/testdata/edgecase/generator_config.yml \ 36 | --output ./internal/cmd/testdata/edgecase/provider_code_spec.json \ 37 | ./internal/cmd/testdata/edgecase/openapi_spec.yml 38 | 39 | go run ./cmd/tfplugingen-openapi generate \ 40 | --config ./internal/cmd/testdata/kubernetes/generator_config.yml \ 41 | --output ./internal/cmd/testdata/kubernetes/provider_code_spec.json \ 42 | ./internal/cmd/testdata/kubernetes/openapi_spec.json 43 | 44 | .PHONY: lint fmt test -------------------------------------------------------------------------------- /internal/cmd/testdata/scaleway/generator_config.yml: -------------------------------------------------------------------------------- 1 | # Ref: https://registry.terraform.io/providers/scaleway/scaleway/latest/docs 2 | provider: 3 | name: scaleway 4 | 5 | resources: 6 | # Ref: https://registry.terraform.io/providers/scaleway/scaleway/latest/docs/resources/instance_image 7 | instance_image: 8 | create: 9 | path: /instance/v1/zones/{zone}/images 10 | method: POST 11 | read: 12 | path: /instance/v1/zones/{zone}/images/{image_id} 13 | method: GET 14 | 15 | update: 16 | path: /instance/v1/zones/{zone}/images/{id} 17 | method: PUT 18 | delete: 19 | path: /instance/v1/zones/{zone}/images/{image_id} 20 | method: DELETE 21 | 22 | # Ref: https://registry.terraform.io/providers/scaleway/scaleway/latest/docs/resources/instance_ip 23 | instance_ip: 24 | create: 25 | path: /instance/v1/zones/{zone}/ips 26 | method: POST 27 | read: 28 | path: /instance/v1/zones/{zone}/ips/{ip} 29 | method: GET 30 | update: 31 | path: /instance/v1/zones/{zone}/ips/{ip} 32 | method: PATCH 33 | delete: 34 | path: /instance/v1/zones/{zone}/ips/{ip} 35 | method: DELETE 36 | 37 | data_sources: 38 | # Ref: https://registry.terraform.io/providers/scaleway/scaleway/latest/docs/data-sources/instance_servers 39 | instance_servers: 40 | read: 41 | path: /instance/v1/zones/{zone}/servers 42 | method: GET 43 | 44 | # Ref: https://registry.terraform.io/providers/scaleway/scaleway/latest/docs/data-sources/instance_server 45 | instance_server: 46 | read: 47 | path: /instance/v1/zones/{zone}/servers/{server_id} 48 | method: GET 49 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.3.0 (January 19, 2024) 2 | 3 | ENHANCEMENTS: 4 | 5 | * Added data source and resource support for query and path parameters specified in the [OAS Path Item](https://spec.openapis.org/oas/v3.1.0#path-item-object) ([#114](https://github.com/hashicorp/terraform-plugin-codegen-openapi/issues/114)) 6 | 7 | BUG FIXES: 8 | 9 | * Fixed a bug where schema defaults were not detected for integer/int32 properties ([#111](https://github.com/hashicorp/terraform-plugin-codegen-openapi/issues/111)) 10 | 11 | ## 0.2.1 (December 13, 2023) 12 | 13 | BUG FIXES: 14 | 15 | * Fixed a bug where schemas that used `additionalProperties` with schema composition (allOf/anyOf/oneOf) would return an empty single nested attribute. Will now return map or map nested attribute. ([#100](https://github.com/hashicorp/terraform-plugin-codegen-openapi/issues/100)) 16 | 17 | ## 0.2.0 (October 30, 2023) 18 | 19 | FEATURES: 20 | 21 | * Added schema.ignores option to generator config for resources, data sources, and providers. Allows excluding attributes from OAS mapping ([#81](https://github.com/hashicorp/terraform-plugin-codegen-openapi/issues/81)) 22 | 23 | ENHANCEMENTS: 24 | 25 | * Added data source support for response body arrays ([#16](https://github.com/hashicorp/terraform-plugin-codegen-openapi/issues/16)) 26 | * Schemas that have the `properties` keyword defined with no type will now default to `object` ([#79](https://github.com/hashicorp/terraform-plugin-codegen-openapi/issues/79)) 27 | 28 | ## 0.1.0 (October 17, 2023) 29 | 30 | NOTES: 31 | 32 | * Initial release of `tfplugingen-openapi` CLI for Terraform Provider Code Generation tech preview ([#68](https://github.com/hashicorp/terraform-plugin-codegen-openapi/issues/68)) 33 | 34 | -------------------------------------------------------------------------------- /cmd/tfplugingen-openapi/main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "io" 9 | "os" 10 | 11 | "github.com/hashicorp/cli" 12 | "github.com/mattn/go-colorable" 13 | 14 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/cmd" 15 | ) 16 | 17 | // version will be set by goreleaser via ldflags 18 | // https://goreleaser.com/cookbooks/using-main.version/ 19 | func main() { 20 | name := "tfplugingen-openapi" 21 | versionOutput := fmt.Sprintf("%s %s", name, getVersion()) 22 | 23 | os.Exit(runCLI( 24 | name, 25 | versionOutput, 26 | os.Args[1:], 27 | os.Stdin, 28 | colorable.NewColorableStdout(), 29 | colorable.NewColorableStderr(), 30 | )) 31 | } 32 | 33 | func initCommands(ui cli.Ui) map[string]cli.CommandFactory { 34 | 35 | generateFactory := func() (cli.Command, error) { 36 | return &cmd.GenerateCommand{ 37 | UI: ui, 38 | }, nil 39 | } 40 | 41 | return map[string]cli.CommandFactory{ 42 | "generate": generateFactory, 43 | } 44 | } 45 | 46 | func runCLI(name, versionOutput string, args []string, stdin io.Reader, stdout, stderr io.Writer) int { 47 | ui := &cli.ColoredUi{ 48 | ErrorColor: cli.UiColorRed, 49 | WarnColor: cli.UiColorYellow, 50 | 51 | Ui: &cli.BasicUi{ 52 | Reader: stdin, 53 | Writer: stdout, 54 | ErrorWriter: stderr, 55 | }, 56 | } 57 | 58 | commands := initCommands(ui) 59 | openAPIGen := cli.CLI{ 60 | Name: name, 61 | Args: args, 62 | Commands: commands, 63 | HelpFunc: cli.BasicHelpFunc(name), 64 | HelpWriter: stderr, 65 | Version: versionOutput, 66 | } 67 | exitCode, err := openAPIGen.Run() 68 | if err != nil { 69 | return 1 70 | } 71 | 72 | return exitCode 73 | } 74 | -------------------------------------------------------------------------------- /internal/mapper/util/framework_identifier.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package util 5 | 6 | import ( 7 | "fmt" 8 | "regexp" 9 | "strings" 10 | "unicode/utf8" 11 | ) 12 | 13 | var ( 14 | // lowerToUpper will match a sequence of a lowercase letter followed by an uppercase letter 15 | lowerToUpper = regexp.MustCompile(`([a-z])[A-Z]`) 16 | 17 | // unsupportedCharacters matches any characters that are NOT alphanumeric or underscores 18 | unsupportedCharacters = regexp.MustCompile(`[^a-zA-Z0-9_]+`) 19 | 20 | // leadingNumbers matches all numbers that are at the beginning of a string 21 | leadingNumbers = regexp.MustCompile(`^(\d+)`) 22 | ) 23 | 24 | // TerraformIdentifier attempts to convert the given string to a valid Terraform identifier for usage in a Provider Code Specification. 25 | func TerraformIdentifier(original string) string { 26 | if len(original) == 0 { 27 | return original 28 | } 29 | 30 | // Remove any characters that are either not supported in a Terraform indentifier, or can't be automatically converted 31 | removedUnsupported := unsupportedCharacters.ReplaceAllString(original, "") 32 | 33 | // Remove leading numbers 34 | noLeadingNumbers := leadingNumbers.ReplaceAllString(removedUnsupported, "") 35 | 36 | // Insert an underscore between lowercase letter followed by uppercase letter 37 | insertedUnderscores := lowerToUpper.ReplaceAllStringFunc(noLeadingNumbers, func(s string) string { 38 | firstRune, size := utf8.DecodeRuneInString(s) 39 | if firstRune == utf8.RuneError && size <= 1 { 40 | // The string is empty, return it 41 | return s 42 | } 43 | 44 | return fmt.Sprintf("%s_%s", string(firstRune), strings.ToLower(s[size:])) 45 | }) 46 | 47 | // Lowercase the final string 48 | return strings.ToLower(insertedUnderscores) 49 | } 50 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Something is missing or could be improved. 3 | labels: ["enhancement"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thank you for taking the time to fill out this feature request! Please note that this issue tracker is only used for bug reports and feature requests. Other issues will be closed. 9 | 10 | If you have a question or want to provide general feedback about code generation, please go back to the issue chooser and select one of the discuss board links. 11 | - type: textarea 12 | id: use-case 13 | attributes: 14 | label: Use Cases or Problem Statement 15 | description: What use cases or problems are you trying to solve? 16 | placeholder: Description of use cases or problems. 17 | validations: 18 | required: true 19 | - type: textarea 20 | id: proposal 21 | attributes: 22 | label: Proposal 23 | description: What solutions would you prefer? 24 | placeholder: Description of proposed solutions. 25 | validations: 26 | required: true 27 | - type: textarea 28 | id: additional-information 29 | attributes: 30 | label: Additional Information 31 | description: Are there any additional details about your environment, workflow, or recent changes that might be relevant? Have you discovered a workaround? Are there links to other related issues? 32 | validations: 33 | required: false 34 | - type: checkboxes 35 | id: terms 36 | attributes: 37 | label: Code of Conduct 38 | description: By submitting this issue, you agree to follow our [Community Guidelines](https://www.hashicorp.com/community-guidelines). 39 | options: 40 | - label: I agree to follow this project's Code of Conduct 41 | required: true -------------------------------------------------------------------------------- /internal/mapper/frameworkvalidators/float64validator_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package frameworkvalidators_test 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/google/go-cmp/cmp" 10 | "github.com/hashicorp/terraform-plugin-codegen-spec/code" 11 | "github.com/hashicorp/terraform-plugin-codegen-spec/schema" 12 | 13 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/mapper/frameworkvalidators" 14 | ) 15 | 16 | func TestFloat64ValidatorOneOf(t *testing.T) { 17 | t.Parallel() 18 | 19 | testCases := map[string]struct { 20 | values []float64 21 | expected *schema.CustomValidator 22 | }{ 23 | "nil": { 24 | values: nil, 25 | expected: nil, 26 | }, 27 | "empty": { 28 | values: []float64{}, 29 | expected: nil, 30 | }, 31 | "one": { 32 | values: []float64{1.2}, 33 | expected: &schema.CustomValidator{ 34 | Imports: []code.Import{ 35 | { 36 | Path: "github.com/hashicorp/terraform-plugin-framework-validators/float64validator", 37 | }, 38 | }, 39 | SchemaDefinition: "float64validator.OneOf(\n1.2,\n)", 40 | }, 41 | }, 42 | "multiple": { 43 | values: []float64{1.2, 2.3}, 44 | expected: &schema.CustomValidator{ 45 | Imports: []code.Import{ 46 | { 47 | Path: "github.com/hashicorp/terraform-plugin-framework-validators/float64validator", 48 | }, 49 | }, 50 | SchemaDefinition: "float64validator.OneOf(\n1.2,\n2.3,\n)", 51 | }, 52 | }, 53 | } 54 | 55 | for name, testCase := range testCases { 56 | 57 | t.Run(name, func(t *testing.T) { 58 | t.Parallel() 59 | 60 | got := frameworkvalidators.Float64ValidatorOneOf(testCase.values) 61 | 62 | if diff := cmp.Diff(got, testCase.expected); diff != "" { 63 | t.Errorf("unexpected difference: %s", diff) 64 | } 65 | }) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hashicorp/terraform-plugin-codegen-openapi 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.0 6 | 7 | require ( 8 | github.com/google/go-cmp v0.7.0 9 | github.com/hashicorp/cli v1.1.7 10 | github.com/hashicorp/terraform-plugin-codegen-spec v0.2.0 11 | github.com/mattn/go-colorable v0.1.14 12 | github.com/pb33f/libopenapi v0.21.8 13 | gopkg.in/yaml.v3 v3.0.1 14 | ) 15 | 16 | require ( 17 | github.com/Masterminds/goutils v1.1.1 // indirect 18 | github.com/Masterminds/semver/v3 v3.2.0 // indirect 19 | github.com/Masterminds/sprig/v3 v3.2.3 // indirect 20 | github.com/armon/go-radix v1.0.0 // indirect 21 | github.com/bahlo/generic-list-go v0.2.0 // indirect 22 | github.com/bgentry/speakeasy v0.1.0 // indirect 23 | github.com/buger/jsonparser v1.1.1 // indirect 24 | github.com/fatih/color v1.16.0 // indirect 25 | github.com/google/uuid v1.3.0 // indirect 26 | github.com/hashicorp/errwrap v1.1.0 // indirect 27 | github.com/hashicorp/go-multierror v1.1.1 // indirect 28 | github.com/huandu/xstrings v1.4.0 // indirect 29 | github.com/imdario/mergo v0.3.15 // indirect 30 | github.com/mailru/easyjson v0.7.7 // indirect 31 | github.com/mattn/go-isatty v0.0.20 // indirect 32 | github.com/mitchellh/copystructure v1.2.0 // indirect 33 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 34 | github.com/posener/complete v1.2.3 // indirect 35 | github.com/shopspring/decimal v1.3.1 // indirect 36 | github.com/speakeasy-api/jsonpath v0.6.1 // indirect 37 | github.com/spf13/cast v1.5.0 // indirect 38 | github.com/wk8/go-ordered-map/v2 v2.1.9-0.20240815153524-6ea36470d1bd // indirect 39 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect 40 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect 41 | github.com/xeipuuv/gojsonschema v1.2.0 // indirect 42 | golang.org/x/crypto v0.32.0 // indirect 43 | golang.org/x/sys v0.29.0 // indirect 44 | ) 45 | -------------------------------------------------------------------------------- /internal/mapper/util/attribute_type.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package util 5 | 6 | import "github.com/hashicorp/terraform-plugin-codegen-spec/schema" 7 | 8 | // CreateObjectAttributeType is a helper function that assists in mapping between ElementType and ObjectAttributeType 9 | // as their structs share the same underlying types 10 | func CreateObjectAttributeType(name string, elemType schema.ElementType) schema.ObjectAttributeType { 11 | attrType := schema.ObjectAttributeType{Name: name} 12 | 13 | switch { 14 | case elemType.Bool != nil: 15 | attrType.Bool = elemType.Bool 16 | case elemType.Float64 != nil: 17 | attrType.Float64 = elemType.Float64 18 | case elemType.Int64 != nil: 19 | attrType.Int64 = elemType.Int64 20 | case elemType.List != nil: 21 | attrType.List = elemType.List 22 | case elemType.Map != nil: 23 | attrType.Map = elemType.Map 24 | case elemType.Number != nil: 25 | attrType.Number = elemType.Number 26 | case elemType.Object != nil: 27 | attrType.Object = elemType.Object 28 | case elemType.Set != nil: 29 | attrType.Set = elemType.Set 30 | case elemType.String != nil: 31 | attrType.String = elemType.String 32 | } 33 | 34 | return attrType 35 | } 36 | 37 | // CreateElementType is a helper function that assists in mapping between ObjectAttributeType and ElementType 38 | // as their structs share the same underlying types 39 | func CreateElementType(attrType schema.ObjectAttributeType) schema.ElementType { 40 | elemType := schema.ElementType{} 41 | 42 | switch { 43 | case attrType.Bool != nil: 44 | elemType.Bool = attrType.Bool 45 | case attrType.Float64 != nil: 46 | elemType.Float64 = attrType.Float64 47 | case attrType.Int64 != nil: 48 | elemType.Int64 = attrType.Int64 49 | case attrType.List != nil: 50 | elemType.List = attrType.List 51 | case attrType.Map != nil: 52 | elemType.Map = attrType.Map 53 | case attrType.Number != nil: 54 | elemType.Number = attrType.Number 55 | case attrType.Object != nil: 56 | elemType.Object = attrType.Object 57 | case attrType.Set != nil: 58 | elemType.Set = attrType.Set 59 | case attrType.String != nil: 60 | elemType.String = attrType.String 61 | } 62 | 63 | return elemType 64 | } 65 | -------------------------------------------------------------------------------- /internal/mapper/provider_mapper.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package mapper 5 | 6 | import ( 7 | "fmt" 8 | "log/slog" 9 | 10 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/config" 11 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/explorer" 12 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/log" 13 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/mapper/oas" 14 | "github.com/hashicorp/terraform-plugin-codegen-spec/provider" 15 | ) 16 | 17 | var _ ProviderMapper = providerMapper{} 18 | 19 | type ProviderMapper interface { 20 | MapToIR(*slog.Logger) (*provider.Provider, error) 21 | } 22 | 23 | type providerMapper struct { 24 | provider explorer.Provider 25 | //nolint:unused // Might be useful later! 26 | cfg config.Config 27 | } 28 | 29 | func NewProviderMapper(exploredProvider explorer.Provider, cfg config.Config) ProviderMapper { 30 | return providerMapper{ 31 | provider: exploredProvider, 32 | cfg: cfg, 33 | } 34 | } 35 | 36 | func (m providerMapper) MapToIR(logger *slog.Logger) (*provider.Provider, error) { 37 | providerIR := provider.Provider{ 38 | Name: m.provider.Name, 39 | } 40 | 41 | if m.provider.SchemaProxy == nil { 42 | return &providerIR, nil 43 | } 44 | 45 | pLogger := logger.With("provider", providerIR.Name) 46 | 47 | providerSchema, err := generateProviderSchema(pLogger, m.provider) 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | providerIR.Schema = providerSchema 53 | return &providerIR, nil 54 | } 55 | 56 | func generateProviderSchema(logger *slog.Logger, exploredProvider explorer.Provider) (*provider.Schema, error) { 57 | providerSchema := &provider.Schema{} 58 | 59 | schemaOpts := oas.SchemaOpts{ 60 | Ignores: exploredProvider.Ignores, 61 | } 62 | s, err := oas.BuildSchema(exploredProvider.SchemaProxy, schemaOpts, oas.GlobalSchemaOpts{}) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | attributes, err := s.BuildProviderAttributes() 68 | if err != nil { 69 | log.WarnLogOnError(logger, err, "error mapping provider schema") 70 | 71 | return nil, fmt.Errorf("error mapping provider schema: %w", err) 72 | } 73 | 74 | providerSchema.Attributes = attributes.ToSpec() 75 | 76 | return providerSchema, nil 77 | } 78 | -------------------------------------------------------------------------------- /internal/mapper/oas/bool.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package oas 5 | 6 | import ( 7 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/mapper/attrmapper" 8 | "github.com/hashicorp/terraform-plugin-codegen-spec/datasource" 9 | "github.com/hashicorp/terraform-plugin-codegen-spec/provider" 10 | "github.com/hashicorp/terraform-plugin-codegen-spec/resource" 11 | "github.com/hashicorp/terraform-plugin-codegen-spec/schema" 12 | ) 13 | 14 | func (s *OASSchema) BuildBoolResource(name string, computability schema.ComputedOptionalRequired) (attrmapper.ResourceAttribute, *SchemaError) { 15 | result := &attrmapper.ResourceBoolAttribute{ 16 | Name: name, 17 | BoolAttribute: resource.BoolAttribute{ 18 | ComputedOptionalRequired: computability, 19 | DeprecationMessage: s.GetDeprecationMessage(), 20 | Description: s.GetDescription(), 21 | }, 22 | } 23 | 24 | if s.Schema.Default != nil { 25 | var staticDefault bool 26 | if err := s.Schema.Default.Decode(&staticDefault); err == nil { 27 | if computability == schema.Required { 28 | result.ComputedOptionalRequired = schema.ComputedOptional 29 | } 30 | 31 | result.Default = &schema.BoolDefault{ 32 | Static: &staticDefault, 33 | } 34 | } 35 | } 36 | 37 | return result, nil 38 | } 39 | 40 | func (s *OASSchema) BuildBoolDataSource(name string, computability schema.ComputedOptionalRequired) (attrmapper.DataSourceAttribute, *SchemaError) { 41 | result := &attrmapper.DataSourceBoolAttribute{ 42 | Name: name, 43 | BoolAttribute: datasource.BoolAttribute{ 44 | ComputedOptionalRequired: computability, 45 | DeprecationMessage: s.GetDeprecationMessage(), 46 | Description: s.GetDescription(), 47 | }, 48 | } 49 | 50 | return result, nil 51 | } 52 | 53 | func (s *OASSchema) BuildBoolProvider(name string, optionalOrRequired schema.OptionalRequired) (attrmapper.ProviderAttribute, *SchemaError) { 54 | return &attrmapper.ProviderBoolAttribute{ 55 | Name: name, 56 | BoolAttribute: provider.BoolAttribute{ 57 | OptionalRequired: optionalOrRequired, 58 | DeprecationMessage: s.GetDeprecationMessage(), 59 | Description: s.GetDescription(), 60 | }, 61 | }, nil 62 | } 63 | 64 | func (s *OASSchema) BuildBoolElementType() (schema.ElementType, *SchemaError) { 65 | return schema.ElementType{ 66 | Bool: &schema.BoolType{}, 67 | }, nil 68 | } 69 | -------------------------------------------------------------------------------- /internal/mapper/oas/single_nested.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package oas 5 | 6 | import ( 7 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/mapper/attrmapper" 8 | "github.com/hashicorp/terraform-plugin-codegen-spec/datasource" 9 | "github.com/hashicorp/terraform-plugin-codegen-spec/provider" 10 | "github.com/hashicorp/terraform-plugin-codegen-spec/resource" 11 | "github.com/hashicorp/terraform-plugin-codegen-spec/schema" 12 | ) 13 | 14 | func (s *OASSchema) BuildSingleNestedResource(name string, computability schema.ComputedOptionalRequired) (attrmapper.ResourceAttribute, *SchemaError) { 15 | objectAttributes, err := s.BuildResourceAttributes() 16 | if err != nil { 17 | return nil, s.NestSchemaError(err, name) 18 | } 19 | 20 | return &attrmapper.ResourceSingleNestedAttribute{ 21 | Name: name, 22 | Attributes: objectAttributes, 23 | SingleNestedAttribute: resource.SingleNestedAttribute{ 24 | ComputedOptionalRequired: computability, 25 | DeprecationMessage: s.GetDeprecationMessage(), 26 | Description: s.GetDescription(), 27 | }, 28 | }, nil 29 | } 30 | 31 | func (s *OASSchema) BuildSingleNestedDataSource(name string, computability schema.ComputedOptionalRequired) (attrmapper.DataSourceAttribute, *SchemaError) { 32 | objectAttributes, err := s.BuildDataSourceAttributes() 33 | if err != nil { 34 | return nil, s.NestSchemaError(err, name) 35 | } 36 | 37 | return &attrmapper.DataSourceSingleNestedAttribute{ 38 | Name: name, 39 | Attributes: objectAttributes, 40 | SingleNestedAttribute: datasource.SingleNestedAttribute{ 41 | ComputedOptionalRequired: computability, 42 | DeprecationMessage: s.GetDeprecationMessage(), 43 | Description: s.GetDescription(), 44 | }, 45 | }, nil 46 | } 47 | 48 | func (s *OASSchema) BuildSingleNestedProvider(name string, optionalOrRequired schema.OptionalRequired) (attrmapper.ProviderAttribute, *SchemaError) { 49 | objectAttributes, err := s.BuildProviderAttributes() 50 | if err != nil { 51 | return nil, s.NestSchemaError(err, name) 52 | } 53 | 54 | return &attrmapper.ProviderSingleNestedAttribute{ 55 | Name: name, 56 | Attributes: objectAttributes, 57 | SingleNestedAttribute: provider.SingleNestedAttribute{ 58 | OptionalRequired: optionalOrRequired, 59 | DeprecationMessage: s.GetDeprecationMessage(), 60 | Description: s.GetDescription(), 61 | }, 62 | }, nil 63 | } 64 | -------------------------------------------------------------------------------- /internal/mapper/attrmapper/types.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package attrmapper 5 | 6 | import ( 7 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/mapper/util" 8 | "github.com/hashicorp/terraform-plugin-codegen-spec/schema" 9 | ) 10 | 11 | type ResourceNestedAttributeObject struct { 12 | Attributes ResourceAttributes 13 | } 14 | 15 | type DataSourceNestedAttributeObject struct { 16 | Attributes DataSourceAttributes 17 | } 18 | 19 | type ProviderNestedAttributeObject struct { 20 | Attributes ProviderAttributes 21 | } 22 | 23 | func mergeElementType(target schema.ElementType, merge schema.ElementType) schema.ElementType { 24 | // Handle collection type 25 | // https://developer.hashicorp.com/terraform/plugin/framework/handling-data/types#collection-types 26 | if target.List != nil && merge.List != nil { 27 | target.List.ElementType = mergeElementType(target.List.ElementType, merge.List.ElementType) 28 | } else if target.Map != nil && merge.Map != nil { 29 | target.Map.ElementType = mergeElementType(target.Map.ElementType, merge.Map.ElementType) 30 | } else if target.Set != nil && merge.Set != nil { 31 | target.Set.ElementType = mergeElementType(target.Set.ElementType, merge.Set.ElementType) 32 | } 33 | 34 | // Handle object type 35 | // https://developer.hashicorp.com/terraform/plugin/framework/handling-data/types#object-type 36 | if target.Object != nil && merge.Object != nil { 37 | target.Object.AttributeTypes = mergeObjectAttributeTypes(target.Object.AttributeTypes, merge.Object.AttributeTypes) 38 | } 39 | 40 | return target 41 | } 42 | 43 | func mergeObjectAttributeTypes(targetAttrTypes []schema.ObjectAttributeType, mergeAttrTypes []schema.ObjectAttributeType) []schema.ObjectAttributeType { 44 | for _, mergeAttrType := range mergeAttrTypes { 45 | // As we compare attribute types, if we don't find a match, we should add this attribute type to the slice after 46 | isNewAttrType := true 47 | 48 | for i, targetAttrType := range targetAttrTypes { 49 | if targetAttrType.Name == mergeAttrType.Name { 50 | mergedElementType := mergeElementType(util.CreateElementType(targetAttrType), util.CreateElementType(mergeAttrType)) 51 | targetAttrTypes[i] = util.CreateObjectAttributeType(targetAttrType.Name, mergedElementType) 52 | 53 | isNewAttrType = false 54 | break 55 | } 56 | } 57 | 58 | if isNewAttrType { 59 | // Add this back to the original slice to avoid adding duplicate attributes from different mergeSlices 60 | targetAttrTypes = append(targetAttrTypes, mergeAttrType) 61 | } 62 | } 63 | 64 | return targetAttrTypes 65 | } 66 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Let us know about an unexpected error, a crash, or an incorrect behavior. 3 | labels: ["bug"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thank you for taking the time to fill out this bug report! Please note that this issue tracker is only used for bug reports and feature requests. Other issues will be closed. 9 | 10 | If you have a question or want to provide general feedback about code generation, please go back to the issue chooser and select one of the discuss board links. 11 | - type: textarea 12 | id: version 13 | attributes: 14 | label: tfplugingen-openapi CLI version 15 | description: What is the version of the OpenAPI Provider Spec Generator CLI? 16 | placeholder: Output of `tfplugingen-openapi --version` 17 | validations: 18 | required: true 19 | - type: textarea 20 | id: openapi-spec 21 | attributes: 22 | label: OpenAPI Spec File 23 | description: Please copy and paste any relevant content of the OpenAPI specification used 24 | render: YAML 25 | validations: 26 | required: true 27 | - type: textarea 28 | id: generator-config 29 | attributes: 30 | label: Generator Config 31 | description: Please copy and paste the content of the generator config used 32 | render: YAML 33 | validations: 34 | required: true 35 | - type: textarea 36 | id: expected-behavior 37 | attributes: 38 | label: Expected Behavior 39 | description: What did you expect to happen? 40 | placeholder: Description of what should have happened. 41 | validations: 42 | required: true 43 | - type: textarea 44 | id: actual-behavior 45 | attributes: 46 | label: Actual Behavior 47 | description: What actually happened? 48 | placeholder: Description of what actually happened. 49 | validations: 50 | required: true 51 | - type: textarea 52 | id: additional-information 53 | attributes: 54 | label: Additional Information 55 | description: Are there any additional details about your environment, workflow, or recent changes that might be relevant? Have you discovered a workaround? Are there links to other related issues? 56 | validations: 57 | required: false 58 | - type: checkboxes 59 | id: terms 60 | attributes: 61 | label: Code of Conduct 62 | description: By submitting this issue, you agree to follow our [Community Guidelines](https://www.hashicorp.com/community-guidelines). 63 | options: 64 | - label: I agree to follow this project's Code of Conduct 65 | required: true -------------------------------------------------------------------------------- /internal/cmd/testdata/petstore3/generator_config.yml: -------------------------------------------------------------------------------- 1 | provider: 2 | name: petstore 3 | 4 | resources: 5 | pet: 6 | create: 7 | path: /pet 8 | method: POST 9 | read: 10 | path: /pet/{petId} 11 | method: GET 12 | update: 13 | path: /pet 14 | method: PUT 15 | delete: 16 | path: /pet/{petId} 17 | method: DELETE 18 | schema: 19 | attributes: 20 | overrides: 21 | name: 22 | description: The pet's full name 23 | category: 24 | description: Category containing classification info about the pet 25 | "category.name": 26 | description: The category name, possible values - 'dog', 'cat', 'bird', or 'other' 27 | aliases: 28 | petId: id 29 | 30 | order: 31 | create: 32 | path: /store/order 33 | method: POST 34 | read: 35 | path: /store/order/{orderId} 36 | method: GET 37 | delete: 38 | path: /store/order/{orderId} 39 | method: DELETE 40 | schema: 41 | attributes: 42 | overrides: 43 | status: 44 | description: Order status, possible values - 'placed', 'approved', or 'delivered' 45 | shipDate: 46 | description: A field representing the date and time an order will be shipped by 47 | aliases: 48 | orderId: id 49 | 50 | user: 51 | create: 52 | path: /user 53 | method: POST 54 | read: 55 | path: /user/{username} 56 | method: GET 57 | schema: 58 | ignores: 59 | - username 60 | 61 | data_sources: 62 | pet: 63 | read: 64 | path: /pet/{petId} 65 | method: GET 66 | schema: 67 | attributes: 68 | overrides: 69 | name: 70 | description: The pet's full name 71 | category: 72 | description: Category containing classification info about the pet 73 | "category.name": 74 | description: The category name, possible values - 'dog', 'cat', 'bird', or 'other' 75 | aliases: 76 | petId: id 77 | 78 | pets: 79 | read: 80 | path: /pet/findByStatus 81 | method: GET 82 | schema: 83 | ignores: 84 | - status 85 | 86 | order: 87 | read: 88 | path: /store/order/{orderId} 89 | method: GET 90 | schema: 91 | attributes: 92 | overrides: 93 | status: 94 | description: Order status, possible values - 'placed', 'approved', or 'delivered' 95 | shipDate: 96 | description: A field representing the date and time an order will be shipped by 97 | aliases: 98 | orderId: id 99 | -------------------------------------------------------------------------------- /internal/mapper/attrmapper/bool.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package attrmapper 5 | 6 | import ( 7 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/explorer" 8 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/mapper/util" 9 | "github.com/hashicorp/terraform-plugin-codegen-spec/datasource" 10 | "github.com/hashicorp/terraform-plugin-codegen-spec/provider" 11 | "github.com/hashicorp/terraform-plugin-codegen-spec/resource" 12 | ) 13 | 14 | type ResourceBoolAttribute struct { 15 | resource.BoolAttribute 16 | 17 | Name string 18 | } 19 | 20 | func (a *ResourceBoolAttribute) GetName() string { 21 | return a.Name 22 | } 23 | 24 | func (a *ResourceBoolAttribute) Merge(mergeAttribute ResourceAttribute) (ResourceAttribute, error) { 25 | boolAttribute, ok := mergeAttribute.(*ResourceBoolAttribute) 26 | // TODO: return error if types don't match? 27 | if ok && (a.Description == nil || *a.Description == "") { 28 | a.Description = boolAttribute.Description 29 | } 30 | 31 | return a, nil 32 | } 33 | 34 | func (a *ResourceBoolAttribute) ApplyOverride(override explorer.Override) (ResourceAttribute, error) { 35 | a.Description = &override.Description 36 | 37 | return a, nil 38 | } 39 | 40 | func (a *ResourceBoolAttribute) ToSpec() resource.Attribute { 41 | return resource.Attribute{ 42 | Name: util.TerraformIdentifier(a.Name), 43 | Bool: &a.BoolAttribute, 44 | } 45 | } 46 | 47 | type DataSourceBoolAttribute struct { 48 | datasource.BoolAttribute 49 | 50 | Name string 51 | } 52 | 53 | func (a *DataSourceBoolAttribute) GetName() string { 54 | return a.Name 55 | } 56 | 57 | func (a *DataSourceBoolAttribute) Merge(mergeAttribute DataSourceAttribute) (DataSourceAttribute, error) { 58 | boolAttribute, ok := mergeAttribute.(*DataSourceBoolAttribute) 59 | // TODO: return error if types don't match? 60 | if ok && (a.Description == nil || *a.Description == "") { 61 | a.Description = boolAttribute.Description 62 | } 63 | 64 | return a, nil 65 | } 66 | 67 | func (a *DataSourceBoolAttribute) ApplyOverride(override explorer.Override) (DataSourceAttribute, error) { 68 | a.Description = &override.Description 69 | 70 | return a, nil 71 | } 72 | 73 | func (a *DataSourceBoolAttribute) ToSpec() datasource.Attribute { 74 | return datasource.Attribute{ 75 | Name: util.TerraformIdentifier(a.Name), 76 | Bool: &a.BoolAttribute, 77 | } 78 | } 79 | 80 | type ProviderBoolAttribute struct { 81 | provider.BoolAttribute 82 | 83 | Name string 84 | } 85 | 86 | func (a *ProviderBoolAttribute) ToSpec() provider.Attribute { 87 | return provider.Attribute{ 88 | Name: util.TerraformIdentifier(a.Name), 89 | Bool: &a.BoolAttribute, 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /internal/cmd/generate_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package cmd_test 5 | 6 | import ( 7 | "os" 8 | "path" 9 | "testing" 10 | 11 | "github.com/google/go-cmp/cmp" 12 | "github.com/hashicorp/cli" 13 | 14 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/cmd" 15 | ) 16 | 17 | func TestGenerate_WithConfig(t *testing.T) { 18 | t.Parallel() 19 | 20 | testCases := map[string]struct { 21 | oasSpecPath string 22 | configPath string 23 | goldenFilePath string 24 | }{ 25 | "GitHub v3 REST API": { 26 | oasSpecPath: "testdata/github/openapi_spec.json", 27 | configPath: "testdata/github/generator_config.yml", 28 | goldenFilePath: "testdata/github/provider_code_spec.json", 29 | }, 30 | "Swagger Petstore - OpenAPI 3.0": { 31 | oasSpecPath: "testdata/petstore3/openapi_spec.json", 32 | configPath: "testdata/petstore3/generator_config.yml", 33 | goldenFilePath: "testdata/petstore3/provider_code_spec.json", 34 | }, 35 | "Scaleway - Instance API": { 36 | oasSpecPath: "testdata/scaleway/openapi_spec.yml", 37 | configPath: "testdata/scaleway/generator_config.yml", 38 | goldenFilePath: "testdata/scaleway/provider_code_spec.json", 39 | }, 40 | "EdgeCase API": { 41 | oasSpecPath: "testdata/edgecase/openapi_spec.yml", 42 | configPath: "testdata/edgecase/generator_config.yml", 43 | goldenFilePath: "testdata/edgecase/provider_code_spec.json", 44 | }, 45 | "Kubernetes API": { 46 | oasSpecPath: "testdata/kubernetes/openapi_spec.json", 47 | configPath: "testdata/kubernetes/generator_config.yml", 48 | goldenFilePath: "testdata/kubernetes/provider_code_spec.json", 49 | }, 50 | } 51 | for name, testCase := range testCases { 52 | 53 | t.Run(name, func(t *testing.T) { 54 | t.Parallel() 55 | 56 | tempProviderSpecPath := path.Join(t.TempDir(), "provider_code_spec.json") 57 | 58 | mockUi := cli.NewMockUi() 59 | c := cmd.GenerateCommand{UI: mockUi} 60 | args := []string{ 61 | "--config", testCase.configPath, 62 | "--output", tempProviderSpecPath, 63 | testCase.oasSpecPath, 64 | } 65 | 66 | exitCode := c.Run(args) 67 | if exitCode != 0 { 68 | t.Fatalf("unexpected error running generate cmd: %s", mockUi.ErrorWriter.String()) 69 | } 70 | 71 | goldenFileBytes, err := os.ReadFile(testCase.goldenFilePath) 72 | if err != nil { 73 | t.Fatal(err) 74 | } 75 | 76 | tempProviderSpecBytes, err := os.ReadFile(tempProviderSpecPath) 77 | if err != nil { 78 | t.Fatal(err) 79 | } 80 | 81 | if diff := cmp.Diff(tempProviderSpecBytes, goldenFileBytes); diff != "" { 82 | t.Errorf("unexpected difference: %s", diff) 83 | } 84 | }) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /internal/mapper/attrmapper/int64.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package attrmapper 5 | 6 | import ( 7 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/explorer" 8 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/mapper/util" 9 | "github.com/hashicorp/terraform-plugin-codegen-spec/datasource" 10 | "github.com/hashicorp/terraform-plugin-codegen-spec/provider" 11 | "github.com/hashicorp/terraform-plugin-codegen-spec/resource" 12 | ) 13 | 14 | type ResourceInt64Attribute struct { 15 | resource.Int64Attribute 16 | 17 | Name string 18 | } 19 | 20 | func (a *ResourceInt64Attribute) GetName() string { 21 | return a.Name 22 | } 23 | 24 | func (a *ResourceInt64Attribute) Merge(mergeAttribute ResourceAttribute) (ResourceAttribute, error) { 25 | int64Attribute, ok := mergeAttribute.(*ResourceInt64Attribute) 26 | // TODO: return error if types don't match? 27 | if ok && (a.Description == nil || *a.Description == "") { 28 | a.Description = int64Attribute.Description 29 | } 30 | 31 | return a, nil 32 | } 33 | 34 | func (a *ResourceInt64Attribute) ApplyOverride(override explorer.Override) (ResourceAttribute, error) { 35 | a.Description = &override.Description 36 | 37 | return a, nil 38 | } 39 | 40 | func (a *ResourceInt64Attribute) ToSpec() resource.Attribute { 41 | return resource.Attribute{ 42 | Name: util.TerraformIdentifier(a.Name), 43 | Int64: &a.Int64Attribute, 44 | } 45 | } 46 | 47 | type DataSourceInt64Attribute struct { 48 | datasource.Int64Attribute 49 | 50 | Name string 51 | } 52 | 53 | func (a *DataSourceInt64Attribute) GetName() string { 54 | return a.Name 55 | } 56 | 57 | func (a *DataSourceInt64Attribute) Merge(mergeAttribute DataSourceAttribute) (DataSourceAttribute, error) { 58 | int64Attribute, ok := mergeAttribute.(*DataSourceInt64Attribute) 59 | // TODO: return error if types don't match? 60 | if ok && (a.Description == nil || *a.Description == "") { 61 | a.Description = int64Attribute.Description 62 | } 63 | 64 | return a, nil 65 | } 66 | 67 | func (a *DataSourceInt64Attribute) ApplyOverride(override explorer.Override) (DataSourceAttribute, error) { 68 | a.Description = &override.Description 69 | 70 | return a, nil 71 | } 72 | 73 | func (a *DataSourceInt64Attribute) ToSpec() datasource.Attribute { 74 | return datasource.Attribute{ 75 | Name: util.TerraformIdentifier(a.Name), 76 | Int64: &a.Int64Attribute, 77 | } 78 | } 79 | 80 | type ProviderInt64Attribute struct { 81 | provider.Int64Attribute 82 | 83 | Name string 84 | } 85 | 86 | func (a *ProviderInt64Attribute) ToSpec() provider.Attribute { 87 | return provider.Attribute{ 88 | Name: util.TerraformIdentifier(a.Name), 89 | Int64: &a.Int64Attribute, 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /internal/mapper/frameworkvalidators/mapvalidator.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package frameworkvalidators 5 | 6 | import ( 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/hashicorp/terraform-plugin-codegen-spec/code" 11 | "github.com/hashicorp/terraform-plugin-codegen-spec/schema" 12 | ) 13 | 14 | const ( 15 | // MapValidatorPackage is the name of the set validation package in 16 | // the framework validators module. 17 | MapValidatorPackage = "mapvalidator" 18 | ) 19 | 20 | var ( 21 | // MapValidatorCodeImport is a single allocation of the framework 22 | // validators module mapvalidator package import. 23 | MapValidatorCodeImport code.Import = CodeImport(MapValidatorPackage) 24 | ) 25 | 26 | // MapValidatorSizeAtLeast returns a custom validator mapped to the 27 | // Mapvalidator package SizeAtLeast function. 28 | func MapValidatorSizeAtLeast(minimum int64) *schema.CustomValidator { 29 | var schemaDefinition strings.Builder 30 | 31 | schemaDefinition.WriteString(MapValidatorPackage) 32 | schemaDefinition.WriteString(".SizeAtLeast(") 33 | schemaDefinition.WriteString(strconv.FormatInt(minimum, 10)) 34 | schemaDefinition.WriteString(")") 35 | 36 | return &schema.CustomValidator{ 37 | Imports: []code.Import{ 38 | MapValidatorCodeImport, 39 | }, 40 | SchemaDefinition: schemaDefinition.String(), 41 | } 42 | } 43 | 44 | // MapValidatorSizeAtMost returns a custom validator mapped to the 45 | // Mapvalidator package SizeAtMost function. 46 | func MapValidatorSizeAtMost(maximum int64) *schema.CustomValidator { 47 | var schemaDefinition strings.Builder 48 | 49 | schemaDefinition.WriteString(MapValidatorPackage) 50 | schemaDefinition.WriteString(".SizeAtMost(") 51 | schemaDefinition.WriteString(strconv.FormatInt(maximum, 10)) 52 | schemaDefinition.WriteString(")") 53 | 54 | return &schema.CustomValidator{ 55 | Imports: []code.Import{ 56 | MapValidatorCodeImport, 57 | }, 58 | SchemaDefinition: schemaDefinition.String(), 59 | } 60 | } 61 | 62 | // MapValidatorSizeBetween returns a custom validator mapped to the 63 | // Mapvalidator package SizeBetween function. 64 | func MapValidatorSizeBetween(minimum, maximum int64) *schema.CustomValidator { 65 | var schemaDefinition strings.Builder 66 | 67 | schemaDefinition.WriteString(MapValidatorPackage) 68 | schemaDefinition.WriteString(".SizeBetween(") 69 | schemaDefinition.WriteString(strconv.FormatInt(minimum, 10)) 70 | schemaDefinition.WriteString(", ") 71 | schemaDefinition.WriteString(strconv.FormatInt(maximum, 10)) 72 | schemaDefinition.WriteString(")") 73 | 74 | return &schema.CustomValidator{ 75 | Imports: []code.Import{ 76 | MapValidatorCodeImport, 77 | }, 78 | SchemaDefinition: schemaDefinition.String(), 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /internal/mapper/frameworkvalidators/setvalidator.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package frameworkvalidators 5 | 6 | import ( 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/hashicorp/terraform-plugin-codegen-spec/code" 11 | "github.com/hashicorp/terraform-plugin-codegen-spec/schema" 12 | ) 13 | 14 | const ( 15 | // SetValidatorPackage is the name of the set validation package in 16 | // the framework validators module. 17 | SetValidatorPackage = "setvalidator" 18 | ) 19 | 20 | var ( 21 | // SetValidatorCodeImport is a single allocation of the framework 22 | // validators module setvalidator package import. 23 | SetValidatorCodeImport code.Import = CodeImport(SetValidatorPackage) 24 | ) 25 | 26 | // SetValidatorSizeAtLeast returns a custom validator mapped to the 27 | // Setvalidator package SizeAtLeast function. 28 | func SetValidatorSizeAtLeast(minimum int64) *schema.CustomValidator { 29 | var schemaDefinition strings.Builder 30 | 31 | schemaDefinition.WriteString(SetValidatorPackage) 32 | schemaDefinition.WriteString(".SizeAtLeast(") 33 | schemaDefinition.WriteString(strconv.FormatInt(minimum, 10)) 34 | schemaDefinition.WriteString(")") 35 | 36 | return &schema.CustomValidator{ 37 | Imports: []code.Import{ 38 | SetValidatorCodeImport, 39 | }, 40 | SchemaDefinition: schemaDefinition.String(), 41 | } 42 | } 43 | 44 | // SetValidatorSizeAtMost returns a custom validator mapped to the 45 | // Setvalidator package SizeAtMost function. 46 | func SetValidatorSizeAtMost(maximum int64) *schema.CustomValidator { 47 | var schemaDefinition strings.Builder 48 | 49 | schemaDefinition.WriteString(SetValidatorPackage) 50 | schemaDefinition.WriteString(".SizeAtMost(") 51 | schemaDefinition.WriteString(strconv.FormatInt(maximum, 10)) 52 | schemaDefinition.WriteString(")") 53 | 54 | return &schema.CustomValidator{ 55 | Imports: []code.Import{ 56 | SetValidatorCodeImport, 57 | }, 58 | SchemaDefinition: schemaDefinition.String(), 59 | } 60 | } 61 | 62 | // SetValidatorSizeBetween returns a custom validator mapped to the 63 | // Setvalidator package SizeBetween function. 64 | func SetValidatorSizeBetween(minimum, maximum int64) *schema.CustomValidator { 65 | var schemaDefinition strings.Builder 66 | 67 | schemaDefinition.WriteString(SetValidatorPackage) 68 | schemaDefinition.WriteString(".SizeBetween(") 69 | schemaDefinition.WriteString(strconv.FormatInt(minimum, 10)) 70 | schemaDefinition.WriteString(", ") 71 | schemaDefinition.WriteString(strconv.FormatInt(maximum, 10)) 72 | schemaDefinition.WriteString(")") 73 | 74 | return &schema.CustomValidator{ 75 | Imports: []code.Import{ 76 | SetValidatorCodeImport, 77 | }, 78 | SchemaDefinition: schemaDefinition.String(), 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /internal/mapper/attrmapper/number.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package attrmapper 5 | 6 | import ( 7 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/explorer" 8 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/mapper/util" 9 | "github.com/hashicorp/terraform-plugin-codegen-spec/datasource" 10 | "github.com/hashicorp/terraform-plugin-codegen-spec/provider" 11 | "github.com/hashicorp/terraform-plugin-codegen-spec/resource" 12 | ) 13 | 14 | type ResourceNumberAttribute struct { 15 | resource.NumberAttribute 16 | 17 | Name string 18 | } 19 | 20 | func (a *ResourceNumberAttribute) GetName() string { 21 | return a.Name 22 | } 23 | 24 | func (a *ResourceNumberAttribute) Merge(mergeAttribute ResourceAttribute) (ResourceAttribute, error) { 25 | numberAttribute, ok := mergeAttribute.(*ResourceNumberAttribute) 26 | // TODO: return error if types don't match? 27 | if ok && (a.Description == nil || *a.Description == "") { 28 | a.Description = numberAttribute.Description 29 | } 30 | 31 | return a, nil 32 | } 33 | 34 | func (a *ResourceNumberAttribute) ApplyOverride(override explorer.Override) (ResourceAttribute, error) { 35 | a.Description = &override.Description 36 | 37 | return a, nil 38 | } 39 | 40 | func (a *ResourceNumberAttribute) ToSpec() resource.Attribute { 41 | return resource.Attribute{ 42 | Name: util.TerraformIdentifier(a.Name), 43 | Number: &a.NumberAttribute, 44 | } 45 | } 46 | 47 | type DataSourceNumberAttribute struct { 48 | datasource.NumberAttribute 49 | 50 | Name string 51 | } 52 | 53 | func (a *DataSourceNumberAttribute) GetName() string { 54 | return a.Name 55 | } 56 | 57 | func (a *DataSourceNumberAttribute) Merge(mergeAttribute DataSourceAttribute) (DataSourceAttribute, error) { 58 | numberAttribute, ok := mergeAttribute.(*DataSourceNumberAttribute) 59 | // TODO: return error if types don't match? 60 | if ok && (a.Description == nil || *a.Description == "") { 61 | a.Description = numberAttribute.Description 62 | } 63 | 64 | return a, nil 65 | } 66 | 67 | func (a *DataSourceNumberAttribute) ApplyOverride(override explorer.Override) (DataSourceAttribute, error) { 68 | a.Description = &override.Description 69 | 70 | return a, nil 71 | } 72 | 73 | func (a *DataSourceNumberAttribute) ToSpec() datasource.Attribute { 74 | return datasource.Attribute{ 75 | Name: util.TerraformIdentifier(a.Name), 76 | Number: &a.NumberAttribute, 77 | } 78 | } 79 | 80 | type ProviderNumberAttribute struct { 81 | provider.NumberAttribute 82 | 83 | Name string 84 | } 85 | 86 | func (a *ProviderNumberAttribute) ToSpec() provider.Attribute { 87 | return provider.Attribute{ 88 | Name: util.TerraformIdentifier(a.Name), 89 | Number: &a.NumberAttribute, 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /internal/mapper/attrmapper/string.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package attrmapper 5 | 6 | import ( 7 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/explorer" 8 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/mapper/util" 9 | "github.com/hashicorp/terraform-plugin-codegen-spec/datasource" 10 | "github.com/hashicorp/terraform-plugin-codegen-spec/provider" 11 | "github.com/hashicorp/terraform-plugin-codegen-spec/resource" 12 | ) 13 | 14 | type ResourceStringAttribute struct { 15 | resource.StringAttribute 16 | 17 | Name string 18 | } 19 | 20 | func (a *ResourceStringAttribute) GetName() string { 21 | return a.Name 22 | } 23 | 24 | func (a *ResourceStringAttribute) Merge(mergeAttribute ResourceAttribute) (ResourceAttribute, error) { 25 | stringAttribute, ok := mergeAttribute.(*ResourceStringAttribute) 26 | // TODO: return error if types don't match? 27 | if ok && (a.Description == nil || *a.Description == "") { 28 | a.Description = stringAttribute.Description 29 | } 30 | 31 | return a, nil 32 | } 33 | 34 | func (a *ResourceStringAttribute) ApplyOverride(override explorer.Override) (ResourceAttribute, error) { 35 | a.Description = &override.Description 36 | 37 | return a, nil 38 | } 39 | 40 | func (a *ResourceStringAttribute) ToSpec() resource.Attribute { 41 | return resource.Attribute{ 42 | Name: util.TerraformIdentifier(a.Name), 43 | String: &a.StringAttribute, 44 | } 45 | } 46 | 47 | type DataSourceStringAttribute struct { 48 | datasource.StringAttribute 49 | 50 | Name string 51 | } 52 | 53 | func (a *DataSourceStringAttribute) GetName() string { 54 | return a.Name 55 | } 56 | 57 | func (a *DataSourceStringAttribute) Merge(mergeAttribute DataSourceAttribute) (DataSourceAttribute, error) { 58 | stringAttribute, ok := mergeAttribute.(*DataSourceStringAttribute) 59 | // TODO: return error if types don't match? 60 | if ok && (a.Description == nil || *a.Description == "") { 61 | a.Description = stringAttribute.Description 62 | } 63 | 64 | return a, nil 65 | } 66 | 67 | func (a *DataSourceStringAttribute) ApplyOverride(override explorer.Override) (DataSourceAttribute, error) { 68 | a.Description = &override.Description 69 | 70 | return a, nil 71 | } 72 | 73 | func (a *DataSourceStringAttribute) ToSpec() datasource.Attribute { 74 | return datasource.Attribute{ 75 | Name: util.TerraformIdentifier(a.Name), 76 | String: &a.StringAttribute, 77 | } 78 | } 79 | 80 | type ProviderStringAttribute struct { 81 | provider.StringAttribute 82 | 83 | Name string 84 | } 85 | 86 | func (a *ProviderStringAttribute) ToSpec() provider.Attribute { 87 | return provider.Attribute{ 88 | Name: util.TerraformIdentifier(a.Name), 89 | String: &a.StringAttribute, 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /internal/mapper/attrmapper/float64.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package attrmapper 5 | 6 | import ( 7 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/explorer" 8 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/mapper/util" 9 | "github.com/hashicorp/terraform-plugin-codegen-spec/datasource" 10 | "github.com/hashicorp/terraform-plugin-codegen-spec/provider" 11 | "github.com/hashicorp/terraform-plugin-codegen-spec/resource" 12 | ) 13 | 14 | type ResourceFloat64Attribute struct { 15 | resource.Float64Attribute 16 | 17 | Name string 18 | } 19 | 20 | func (a *ResourceFloat64Attribute) GetName() string { 21 | return a.Name 22 | } 23 | 24 | func (a *ResourceFloat64Attribute) Merge(mergeAttribute ResourceAttribute) (ResourceAttribute, error) { 25 | float64Attribute, ok := mergeAttribute.(*ResourceFloat64Attribute) 26 | // TODO: return error if types don't match? 27 | if ok && (a.Description == nil || *a.Description == "") { 28 | a.Description = float64Attribute.Description 29 | } 30 | 31 | return a, nil 32 | } 33 | 34 | func (a *ResourceFloat64Attribute) ApplyOverride(override explorer.Override) (ResourceAttribute, error) { 35 | a.Description = &override.Description 36 | 37 | return a, nil 38 | } 39 | 40 | func (a *ResourceFloat64Attribute) ToSpec() resource.Attribute { 41 | return resource.Attribute{ 42 | Name: util.TerraformIdentifier(a.Name), 43 | Float64: &a.Float64Attribute, 44 | } 45 | } 46 | 47 | type DataSourceFloat64Attribute struct { 48 | datasource.Float64Attribute 49 | 50 | Name string 51 | } 52 | 53 | func (a *DataSourceFloat64Attribute) GetName() string { 54 | return a.Name 55 | } 56 | 57 | func (a *DataSourceFloat64Attribute) Merge(mergeAttribute DataSourceAttribute) (DataSourceAttribute, error) { 58 | float64Attribute, ok := mergeAttribute.(*DataSourceFloat64Attribute) 59 | // TODO: return error if types don't match? 60 | if ok && (a.Description == nil || *a.Description == "") { 61 | a.Description = float64Attribute.Description 62 | } 63 | 64 | return a, nil 65 | } 66 | 67 | func (a *DataSourceFloat64Attribute) ApplyOverride(override explorer.Override) (DataSourceAttribute, error) { 68 | a.Description = &override.Description 69 | 70 | return a, nil 71 | } 72 | 73 | func (a *DataSourceFloat64Attribute) ToSpec() datasource.Attribute { 74 | return datasource.Attribute{ 75 | Name: util.TerraformIdentifier(a.Name), 76 | Float64: &a.Float64Attribute, 77 | } 78 | } 79 | 80 | type ProviderFloat64Attribute struct { 81 | provider.Float64Attribute 82 | 83 | Name string 84 | } 85 | 86 | func (a *ProviderFloat64Attribute) ToSpec() provider.Attribute { 87 | return provider.Attribute{ 88 | Name: util.TerraformIdentifier(a.Name), 89 | Float64: &a.Float64Attribute, 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /internal/mapper/util/framework_identifier_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package util_test 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/mapper/util" 10 | ) 11 | 12 | func TestFrameworkIdentifier(t *testing.T) { 13 | t.Parallel() 14 | 15 | testCases := map[string]struct { 16 | original string 17 | want string 18 | }{ 19 | "no change - empty string": { 20 | original: "", 21 | want: "", 22 | }, 23 | "no change - lowercase alphabet": { 24 | original: "thing", 25 | want: "thing", 26 | }, 27 | "no change - one letter lowercase": { 28 | original: "f", 29 | want: "f", 30 | }, 31 | "no change - leading underscore": { 32 | original: "_thing", 33 | want: "_thing", 34 | }, 35 | "no change - middle underscore": { 36 | original: "fake_thing", 37 | want: "fake_thing", 38 | }, 39 | "no change - alphanumeric": { 40 | original: "thing123", 41 | want: "thing123", 42 | }, 43 | "no change - alphanumeric with underscores": { 44 | original: "fake_thing_123", 45 | want: "fake_thing_123", 46 | }, 47 | "change - middle hyphen removed": { 48 | original: "fake-thing", 49 | want: "fakething", 50 | }, 51 | "change - middle hyphen removed and separated": { 52 | original: "Fake-Thing", 53 | want: "fake_thing", 54 | }, 55 | "change - special symbols": { 56 | original: "", 57 | want: "fake_thing", 58 | }, 59 | "change - remove leading number": { 60 | original: "123fakeThing", 61 | want: "fake_thing", 62 | }, 63 | "change - one letter uppercase": { 64 | original: "F", 65 | want: "f", 66 | }, 67 | "change - capitalized with underscore": { 68 | original: "Fake_Thing", 69 | want: "fake_thing", 70 | }, 71 | "change - lower camelCase": { 72 | original: "fakeThing", 73 | want: "fake_thing", 74 | }, 75 | "change - PascalCase": { 76 | original: "FakeThing", 77 | want: "fake_thing", 78 | }, 79 | "change - lower camelCase with initialism": { 80 | original: "fakeID", 81 | want: "fake_id", 82 | }, 83 | "change - PascalCase with initialism": { 84 | original: "FakeURL", 85 | want: "fake_url", 86 | }, 87 | "change - all uppercase": { 88 | original: "FAKETHING", 89 | want: "fakething", 90 | }, 91 | } 92 | for name, testCase := range testCases { 93 | 94 | t.Run(name, func(t *testing.T) { 95 | t.Parallel() 96 | 97 | got := util.TerraformIdentifier(testCase.original) 98 | if got != testCase.want { 99 | t.Fatalf("expected %s, got %s", testCase.want, got) 100 | } 101 | }) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /tools/go.mod: -------------------------------------------------------------------------------- 1 | module tools 2 | 3 | go 1.22.7 4 | toolchain go1.24.1 5 | 6 | require github.com/hashicorp/copywrite v0.22.0 7 | 8 | require ( 9 | github.com/AlecAivazis/survey/v2 v2.3.7 // indirect 10 | github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect 11 | github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef // indirect 12 | github.com/bmatcuk/doublestar/v4 v4.6.0 // indirect 13 | github.com/bradleyfalzon/ghinstallation/v2 v2.5.0 // indirect 14 | github.com/cli/go-gh/v2 v2.11.2 // indirect 15 | github.com/cli/safeexec v1.0.0 // indirect 16 | github.com/cloudflare/circl v1.3.7 // indirect 17 | github.com/fatih/color v1.13.0 // indirect 18 | github.com/fsnotify/fsnotify v1.5.4 // indirect 19 | github.com/go-openapi/errors v0.20.2 // indirect 20 | github.com/go-openapi/strfmt v0.21.3 // indirect 21 | github.com/golang-jwt/jwt/v4 v4.5.2 // indirect 22 | github.com/golang/protobuf v1.5.2 // indirect 23 | github.com/google/go-github/v45 v45.2.0 // indirect 24 | github.com/google/go-github/v53 v53.0.0 // indirect 25 | github.com/google/go-querystring v1.1.0 // indirect 26 | github.com/hashicorp/go-hclog v1.5.0 // indirect 27 | github.com/hashicorp/hcl v1.0.0 // indirect 28 | github.com/inconshreveable/mousetrap v1.0.1 // indirect 29 | github.com/jedib0t/go-pretty v4.3.0+incompatible // indirect 30 | github.com/jedib0t/go-pretty/v6 v6.4.6 // indirect 31 | github.com/joho/godotenv v1.3.0 // indirect 32 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect 33 | github.com/knadh/koanf v1.5.0 // indirect 34 | github.com/mattn/go-colorable v0.1.13 // indirect 35 | github.com/mattn/go-isatty v0.0.20 // indirect 36 | github.com/mattn/go-runewidth v0.0.15 // indirect 37 | github.com/mergestat/timediff v0.0.3 // indirect 38 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect 39 | github.com/mitchellh/copystructure v1.2.0 // indirect 40 | github.com/mitchellh/go-homedir v1.1.0 // indirect 41 | github.com/mitchellh/mapstructure v1.5.0 // indirect 42 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 43 | github.com/oklog/ulid v1.3.1 // indirect 44 | github.com/rivo/uniseg v0.4.7 // indirect 45 | github.com/rogpeppe/go-internal v1.10.0 // indirect 46 | github.com/samber/lo v1.37.0 // indirect 47 | github.com/spf13/cobra v1.6.1 // indirect 48 | github.com/spf13/pflag v1.0.5 // indirect 49 | github.com/thanhpk/randstr v1.0.4 // indirect 50 | go.mongodb.org/mongo-driver v1.10.0 // indirect 51 | golang.org/x/crypto v0.35.0 // indirect 52 | golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect 53 | golang.org/x/net v0.36.0 // indirect 54 | golang.org/x/oauth2 v0.8.0 // indirect 55 | golang.org/x/sync v0.11.0 // indirect 56 | golang.org/x/sys v0.30.0 // indirect 57 | golang.org/x/term v0.29.0 // indirect 58 | golang.org/x/text v0.22.0 // indirect 59 | google.golang.org/appengine v1.6.7 // indirect 60 | google.golang.org/protobuf v1.33.0 // indirect 61 | gopkg.in/yaml.v3 v3.0.1 // indirect 62 | ) 63 | -------------------------------------------------------------------------------- /internal/mapper/attrmapper/map.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package attrmapper 5 | 6 | import ( 7 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/explorer" 8 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/mapper/util" 9 | "github.com/hashicorp/terraform-plugin-codegen-spec/datasource" 10 | "github.com/hashicorp/terraform-plugin-codegen-spec/provider" 11 | "github.com/hashicorp/terraform-plugin-codegen-spec/resource" 12 | ) 13 | 14 | type ResourceMapAttribute struct { 15 | resource.MapAttribute 16 | 17 | Name string 18 | } 19 | 20 | func (a *ResourceMapAttribute) GetName() string { 21 | return a.Name 22 | } 23 | 24 | func (a *ResourceMapAttribute) Merge(mergeAttribute ResourceAttribute) (ResourceAttribute, error) { 25 | mapAttribute, ok := mergeAttribute.(*ResourceMapAttribute) 26 | // TODO: return error if types don't match? 27 | if !ok { 28 | return a, nil 29 | } 30 | 31 | if a.Description == nil || *a.Description == "" { 32 | a.Description = mapAttribute.Description 33 | } 34 | a.ElementType = mergeElementType(a.ElementType, mapAttribute.ElementType) 35 | 36 | return a, nil 37 | } 38 | 39 | func (a *ResourceMapAttribute) ApplyOverride(override explorer.Override) (ResourceAttribute, error) { 40 | a.Description = &override.Description 41 | 42 | return a, nil 43 | } 44 | 45 | func (a *ResourceMapAttribute) ToSpec() resource.Attribute { 46 | return resource.Attribute{ 47 | Name: util.TerraformIdentifier(a.Name), 48 | Map: &a.MapAttribute, 49 | } 50 | } 51 | 52 | type DataSourceMapAttribute struct { 53 | datasource.MapAttribute 54 | 55 | Name string 56 | } 57 | 58 | func (a *DataSourceMapAttribute) GetName() string { 59 | return a.Name 60 | } 61 | 62 | func (a *DataSourceMapAttribute) Merge(mergeAttribute DataSourceAttribute) (DataSourceAttribute, error) { 63 | mapAttribute, ok := mergeAttribute.(*DataSourceMapAttribute) 64 | // TODO: return error if types don't match? 65 | if !ok { 66 | return a, nil 67 | } 68 | 69 | if a.Description == nil || *a.Description == "" { 70 | a.Description = mapAttribute.Description 71 | } 72 | a.ElementType = mergeElementType(a.ElementType, mapAttribute.ElementType) 73 | 74 | return a, nil 75 | } 76 | 77 | func (a *DataSourceMapAttribute) ApplyOverride(override explorer.Override) (DataSourceAttribute, error) { 78 | a.Description = &override.Description 79 | 80 | return a, nil 81 | } 82 | 83 | func (a *DataSourceMapAttribute) ToSpec() datasource.Attribute { 84 | return datasource.Attribute{ 85 | Name: util.TerraformIdentifier(a.Name), 86 | Map: &a.MapAttribute, 87 | } 88 | } 89 | 90 | type ProviderMapAttribute struct { 91 | provider.MapAttribute 92 | 93 | Name string 94 | } 95 | 96 | func (a *ProviderMapAttribute) ToSpec() provider.Attribute { 97 | return provider.Attribute{ 98 | Name: util.TerraformIdentifier(a.Name), 99 | Map: &a.MapAttribute, 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /internal/mapper/attrmapper/set.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package attrmapper 5 | 6 | import ( 7 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/explorer" 8 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/mapper/util" 9 | "github.com/hashicorp/terraform-plugin-codegen-spec/datasource" 10 | "github.com/hashicorp/terraform-plugin-codegen-spec/provider" 11 | "github.com/hashicorp/terraform-plugin-codegen-spec/resource" 12 | ) 13 | 14 | type ResourceSetAttribute struct { 15 | resource.SetAttribute 16 | 17 | Name string 18 | } 19 | 20 | func (a *ResourceSetAttribute) GetName() string { 21 | return a.Name 22 | } 23 | 24 | func (a *ResourceSetAttribute) Merge(mergeAttribute ResourceAttribute) (ResourceAttribute, error) { 25 | setAttribute, ok := mergeAttribute.(*ResourceSetAttribute) 26 | // TODO: return error if types don't match? 27 | if !ok { 28 | return a, nil 29 | } 30 | 31 | if a.Description == nil || *a.Description == "" { 32 | a.Description = setAttribute.Description 33 | } 34 | a.ElementType = mergeElementType(a.ElementType, setAttribute.ElementType) 35 | 36 | return a, nil 37 | } 38 | 39 | func (a *ResourceSetAttribute) ApplyOverride(override explorer.Override) (ResourceAttribute, error) { 40 | a.Description = &override.Description 41 | 42 | return a, nil 43 | } 44 | 45 | func (a *ResourceSetAttribute) ToSpec() resource.Attribute { 46 | return resource.Attribute{ 47 | Name: util.TerraformIdentifier(a.Name), 48 | Set: &a.SetAttribute, 49 | } 50 | } 51 | 52 | type DataSourceSetAttribute struct { 53 | datasource.SetAttribute 54 | 55 | Name string 56 | } 57 | 58 | func (a *DataSourceSetAttribute) GetName() string { 59 | return a.Name 60 | } 61 | 62 | func (a *DataSourceSetAttribute) Merge(mergeAttribute DataSourceAttribute) (DataSourceAttribute, error) { 63 | setAttribute, ok := mergeAttribute.(*DataSourceSetAttribute) 64 | // TODO: return error if types don't match? 65 | if !ok { 66 | return a, nil 67 | } 68 | 69 | if a.Description == nil || *a.Description == "" { 70 | a.Description = setAttribute.Description 71 | } 72 | a.ElementType = mergeElementType(a.ElementType, setAttribute.ElementType) 73 | 74 | return a, nil 75 | } 76 | 77 | func (a *DataSourceSetAttribute) ApplyOverride(override explorer.Override) (DataSourceAttribute, error) { 78 | a.Description = &override.Description 79 | 80 | return a, nil 81 | } 82 | 83 | func (a *DataSourceSetAttribute) ToSpec() datasource.Attribute { 84 | return datasource.Attribute{ 85 | Name: util.TerraformIdentifier(a.Name), 86 | Set: &a.SetAttribute, 87 | } 88 | } 89 | 90 | type ProviderSetAttribute struct { 91 | provider.SetAttribute 92 | 93 | Name string 94 | } 95 | 96 | func (a *ProviderSetAttribute) ToSpec() provider.Attribute { 97 | return provider.Attribute{ 98 | Name: util.TerraformIdentifier(a.Name), 99 | Set: &a.SetAttribute, 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /internal/mapper/attrmapper/list.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package attrmapper 5 | 6 | import ( 7 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/explorer" 8 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/mapper/util" 9 | "github.com/hashicorp/terraform-plugin-codegen-spec/datasource" 10 | "github.com/hashicorp/terraform-plugin-codegen-spec/provider" 11 | "github.com/hashicorp/terraform-plugin-codegen-spec/resource" 12 | ) 13 | 14 | type ResourceListAttribute struct { 15 | resource.ListAttribute 16 | 17 | Name string 18 | } 19 | 20 | func (a *ResourceListAttribute) GetName() string { 21 | return a.Name 22 | } 23 | 24 | func (a *ResourceListAttribute) Merge(mergeAttribute ResourceAttribute) (ResourceAttribute, error) { 25 | listAttribute, ok := mergeAttribute.(*ResourceListAttribute) 26 | // TODO: return error if types don't match? 27 | if !ok { 28 | return a, nil 29 | } 30 | 31 | if a.Description == nil || *a.Description == "" { 32 | a.Description = listAttribute.Description 33 | } 34 | a.ElementType = mergeElementType(a.ElementType, listAttribute.ElementType) 35 | 36 | return a, nil 37 | } 38 | 39 | func (a *ResourceListAttribute) ApplyOverride(override explorer.Override) (ResourceAttribute, error) { 40 | a.Description = &override.Description 41 | 42 | return a, nil 43 | } 44 | 45 | func (a *ResourceListAttribute) ToSpec() resource.Attribute { 46 | return resource.Attribute{ 47 | Name: util.TerraformIdentifier(a.Name), 48 | List: &a.ListAttribute, 49 | } 50 | } 51 | 52 | type DataSourceListAttribute struct { 53 | datasource.ListAttribute 54 | 55 | Name string 56 | } 57 | 58 | func (a *DataSourceListAttribute) GetName() string { 59 | return a.Name 60 | } 61 | 62 | func (a *DataSourceListAttribute) Merge(mergeAttribute DataSourceAttribute) (DataSourceAttribute, error) { 63 | listAttribute, ok := mergeAttribute.(*DataSourceListAttribute) 64 | // TODO: return error if types don't match? 65 | if !ok { 66 | return a, nil 67 | } 68 | 69 | if a.Description == nil || *a.Description == "" { 70 | a.Description = listAttribute.Description 71 | } 72 | a.ElementType = mergeElementType(a.ElementType, listAttribute.ElementType) 73 | 74 | return a, nil 75 | } 76 | 77 | func (a *DataSourceListAttribute) ApplyOverride(override explorer.Override) (DataSourceAttribute, error) { 78 | a.Description = &override.Description 79 | 80 | return a, nil 81 | } 82 | 83 | func (a *DataSourceListAttribute) ToSpec() datasource.Attribute { 84 | return datasource.Attribute{ 85 | Name: util.TerraformIdentifier(a.Name), 86 | List: &a.ListAttribute, 87 | } 88 | } 89 | 90 | type ProviderListAttribute struct { 91 | provider.ListAttribute 92 | 93 | Name string 94 | } 95 | 96 | func (a *ProviderListAttribute) ToSpec() provider.Attribute { 97 | return provider.Attribute{ 98 | Name: util.TerraformIdentifier(a.Name), 99 | List: &a.ListAttribute, 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /internal/mapper/frameworkvalidators/mapvalidator_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package frameworkvalidators_test 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/google/go-cmp/cmp" 10 | "github.com/hashicorp/terraform-plugin-codegen-spec/code" 11 | "github.com/hashicorp/terraform-plugin-codegen-spec/schema" 12 | 13 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/mapper/frameworkvalidators" 14 | ) 15 | 16 | func TestMapValidatorSizeAtLeast(t *testing.T) { 17 | t.Parallel() 18 | 19 | testCases := map[string]struct { 20 | min int64 21 | expected *schema.CustomValidator 22 | }{ 23 | "test": { 24 | min: 123, 25 | expected: &schema.CustomValidator{ 26 | Imports: []code.Import{ 27 | { 28 | Path: "github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator", 29 | }, 30 | }, 31 | SchemaDefinition: "mapvalidator.SizeAtLeast(123)", 32 | }, 33 | }, 34 | } 35 | 36 | for name, testCase := range testCases { 37 | 38 | t.Run(name, func(t *testing.T) { 39 | t.Parallel() 40 | 41 | got := frameworkvalidators.MapValidatorSizeAtLeast(testCase.min) 42 | 43 | if diff := cmp.Diff(got, testCase.expected); diff != "" { 44 | t.Errorf("unexpected difference: %s", diff) 45 | } 46 | }) 47 | } 48 | } 49 | 50 | func TestMapValidatorSizeAtMost(t *testing.T) { 51 | t.Parallel() 52 | 53 | testCases := map[string]struct { 54 | max int64 55 | expected *schema.CustomValidator 56 | }{ 57 | "test": { 58 | max: 123, 59 | expected: &schema.CustomValidator{ 60 | Imports: []code.Import{ 61 | { 62 | Path: "github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator", 63 | }, 64 | }, 65 | SchemaDefinition: "mapvalidator.SizeAtMost(123)", 66 | }, 67 | }, 68 | } 69 | 70 | for name, testCase := range testCases { 71 | 72 | t.Run(name, func(t *testing.T) { 73 | t.Parallel() 74 | 75 | got := frameworkvalidators.MapValidatorSizeAtMost(testCase.max) 76 | 77 | if diff := cmp.Diff(got, testCase.expected); diff != "" { 78 | t.Errorf("unexpected difference: %s", diff) 79 | } 80 | }) 81 | } 82 | } 83 | 84 | func TestMapValidatorSizeBetween(t *testing.T) { 85 | t.Parallel() 86 | 87 | testCases := map[string]struct { 88 | min int64 89 | max int64 90 | expected *schema.CustomValidator 91 | }{ 92 | "test": { 93 | min: 123, 94 | max: 456, 95 | expected: &schema.CustomValidator{ 96 | Imports: []code.Import{ 97 | { 98 | Path: "github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator", 99 | }, 100 | }, 101 | SchemaDefinition: "mapvalidator.SizeBetween(123, 456)", 102 | }, 103 | }, 104 | } 105 | 106 | for name, testCase := range testCases { 107 | 108 | t.Run(name, func(t *testing.T) { 109 | t.Parallel() 110 | 111 | got := frameworkvalidators.MapValidatorSizeBetween(testCase.min, testCase.max) 112 | 113 | if diff := cmp.Diff(got, testCase.expected); diff != "" { 114 | t.Errorf("unexpected difference: %s", diff) 115 | } 116 | }) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /internal/mapper/frameworkvalidators/setvalidator_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package frameworkvalidators_test 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/google/go-cmp/cmp" 10 | "github.com/hashicorp/terraform-plugin-codegen-spec/code" 11 | "github.com/hashicorp/terraform-plugin-codegen-spec/schema" 12 | 13 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/mapper/frameworkvalidators" 14 | ) 15 | 16 | func TestSetValidatorSizeAtLeast(t *testing.T) { 17 | t.Parallel() 18 | 19 | testCases := map[string]struct { 20 | min int64 21 | expected *schema.CustomValidator 22 | }{ 23 | "test": { 24 | min: 123, 25 | expected: &schema.CustomValidator{ 26 | Imports: []code.Import{ 27 | { 28 | Path: "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator", 29 | }, 30 | }, 31 | SchemaDefinition: "setvalidator.SizeAtLeast(123)", 32 | }, 33 | }, 34 | } 35 | 36 | for name, testCase := range testCases { 37 | 38 | t.Run(name, func(t *testing.T) { 39 | t.Parallel() 40 | 41 | got := frameworkvalidators.SetValidatorSizeAtLeast(testCase.min) 42 | 43 | if diff := cmp.Diff(got, testCase.expected); diff != "" { 44 | t.Errorf("unexpected difference: %s", diff) 45 | } 46 | }) 47 | } 48 | } 49 | 50 | func TestSetValidatorSizeAtMost(t *testing.T) { 51 | t.Parallel() 52 | 53 | testCases := map[string]struct { 54 | max int64 55 | expected *schema.CustomValidator 56 | }{ 57 | "test": { 58 | max: 123, 59 | expected: &schema.CustomValidator{ 60 | Imports: []code.Import{ 61 | { 62 | Path: "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator", 63 | }, 64 | }, 65 | SchemaDefinition: "setvalidator.SizeAtMost(123)", 66 | }, 67 | }, 68 | } 69 | 70 | for name, testCase := range testCases { 71 | 72 | t.Run(name, func(t *testing.T) { 73 | t.Parallel() 74 | 75 | got := frameworkvalidators.SetValidatorSizeAtMost(testCase.max) 76 | 77 | if diff := cmp.Diff(got, testCase.expected); diff != "" { 78 | t.Errorf("unexpected difference: %s", diff) 79 | } 80 | }) 81 | } 82 | } 83 | 84 | func TestSetValidatorSizeBetween(t *testing.T) { 85 | t.Parallel() 86 | 87 | testCases := map[string]struct { 88 | min int64 89 | max int64 90 | expected *schema.CustomValidator 91 | }{ 92 | "test": { 93 | min: 123, 94 | max: 456, 95 | expected: &schema.CustomValidator{ 96 | Imports: []code.Import{ 97 | { 98 | Path: "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator", 99 | }, 100 | }, 101 | SchemaDefinition: "setvalidator.SizeBetween(123, 456)", 102 | }, 103 | }, 104 | } 105 | 106 | for name, testCase := range testCases { 107 | 108 | t.Run(name, func(t *testing.T) { 109 | t.Parallel() 110 | 111 | got := frameworkvalidators.SetValidatorSizeBetween(testCase.min, testCase.max) 112 | 113 | if diff := cmp.Diff(got, testCase.expected); diff != "" { 114 | t.Errorf("unexpected difference: %s", diff) 115 | } 116 | }) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /internal/mapper/frameworkvalidators/listvalidator.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package frameworkvalidators 5 | 6 | import ( 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/hashicorp/terraform-plugin-codegen-spec/code" 11 | "github.com/hashicorp/terraform-plugin-codegen-spec/schema" 12 | ) 13 | 14 | const ( 15 | // ListValidatorPackage is the name of the list validation package in 16 | // the framework validators module. 17 | ListValidatorPackage = "listvalidator" 18 | ) 19 | 20 | var ( 21 | // ListValidatorCodeImport is a single allocation of the framework 22 | // validators module listvalidator package import. 23 | ListValidatorCodeImport code.Import = CodeImport(ListValidatorPackage) 24 | ) 25 | 26 | // ListValidatorSizeAtLeast returns a custom validator mapped to the 27 | // listvalidator package SizeAtLeast function. 28 | func ListValidatorSizeAtLeast(minimum int64) *schema.CustomValidator { 29 | var schemaDefinition strings.Builder 30 | 31 | schemaDefinition.WriteString(ListValidatorPackage) 32 | schemaDefinition.WriteString(".SizeAtLeast(") 33 | schemaDefinition.WriteString(strconv.FormatInt(minimum, 10)) 34 | schemaDefinition.WriteString(")") 35 | 36 | return &schema.CustomValidator{ 37 | Imports: []code.Import{ 38 | ListValidatorCodeImport, 39 | }, 40 | SchemaDefinition: schemaDefinition.String(), 41 | } 42 | } 43 | 44 | // ListValidatorSizeAtMost returns a custom validator mapped to the 45 | // listvalidator package SizeAtMost function. 46 | func ListValidatorSizeAtMost(maximum int64) *schema.CustomValidator { 47 | var schemaDefinition strings.Builder 48 | 49 | schemaDefinition.WriteString(ListValidatorPackage) 50 | schemaDefinition.WriteString(".SizeAtMost(") 51 | schemaDefinition.WriteString(strconv.FormatInt(maximum, 10)) 52 | schemaDefinition.WriteString(")") 53 | 54 | return &schema.CustomValidator{ 55 | Imports: []code.Import{ 56 | ListValidatorCodeImport, 57 | }, 58 | SchemaDefinition: schemaDefinition.String(), 59 | } 60 | } 61 | 62 | // ListValidatorSizeBetween returns a custom validator mapped to the 63 | // listvalidator package SizeBetween function. 64 | func ListValidatorSizeBetween(minimum, maximum int64) *schema.CustomValidator { 65 | var schemaDefinition strings.Builder 66 | 67 | schemaDefinition.WriteString(ListValidatorPackage) 68 | schemaDefinition.WriteString(".SizeBetween(") 69 | schemaDefinition.WriteString(strconv.FormatInt(minimum, 10)) 70 | schemaDefinition.WriteString(", ") 71 | schemaDefinition.WriteString(strconv.FormatInt(maximum, 10)) 72 | schemaDefinition.WriteString(")") 73 | 74 | return &schema.CustomValidator{ 75 | Imports: []code.Import{ 76 | ListValidatorCodeImport, 77 | }, 78 | SchemaDefinition: schemaDefinition.String(), 79 | } 80 | } 81 | 82 | // ListValidatorUniqueValues returns a custom validator mapped to the 83 | // listvalidator package UniqueValues function. 84 | func ListValidatorUniqueValues() *schema.CustomValidator { 85 | var schemaDefinition strings.Builder 86 | 87 | schemaDefinition.WriteString(ListValidatorPackage) 88 | schemaDefinition.WriteString(".UniqueValues()") 89 | 90 | return &schema.CustomValidator{ 91 | Imports: []code.Import{ 92 | ListValidatorCodeImport, 93 | }, 94 | SchemaDefinition: schemaDefinition.String(), 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenAPI Provider Spec Generator 2 | 3 | > _[Terraform Provider Code Generation](https://developer.hashicorp.com/terraform/plugin/code-generation) is currently in tech preview. If you have general questions or feedback about provider code generation, please create a new topic in the [Plugin Development Community Forum](https://discuss.hashicorp.com/c/terraform-providers/tf-plugin-sdk)._ 4 | 5 | ## Overview 6 | 7 | The OpenAPI Provider Spec Generator is a CLI tool that transforms an [OpenAPI 3.x](https://www.openapis.org/) Specification (OAS) into a [Provider Code Specification](https://developer.hashicorp.com/terraform/plugin/code-generation/specification). 8 | 9 | A Provider Code Specification can be used to generate [Terraform Provider](https://developer.hashicorp.com/terraform/plugin) code, for example, with the [Plugin Framework Code Generator](https://developer.hashicorp.com/terraform/plugin/code-generation/framework-generator). 10 | 11 | ## Documentation 12 | 13 | Full usage info, examples, and config file documentation live on the HashiCorp developer site: https://developer.hashicorp.com/terraform/plugin/code-generation/openapi-generator 14 | 15 | For more in-depth details about the design and internals of the OpenAPI Provider Spec Generator, see [`DESIGN.md`](./DESIGN.md). 16 | 17 | ## Usage 18 | 19 | ### Installation 20 | 21 | You install a copy of the binary manually from the [releases](https://github.com/hashicorp/terraform-plugin-codegen-openapi/releases) tab, or install via the Go toolchain: 22 | 23 | ```shell-session 24 | go install github.com/hashicorp/terraform-plugin-codegen-openapi/cmd/tfplugingen-openapi@latest 25 | ``` 26 | 27 | ### Generate 28 | 29 | The primary `generate` command requires a [generator config](https://developer.hashicorp.com/terraform/plugin/code-generation/openapi-generator#generator-config) and an OpenAPI 3.x specification: 30 | 31 | ```shell-session 32 | tfplugingen-openapi generate \ 33 | --config \ 34 | --output \ 35 | 36 | ``` 37 | 38 | ### Examples 39 | 40 | Example generator configs, OpenAPI specifications, and Provider Code Specification output can be found in the [`./internal/cmd/testdata/`](./internal/cmd/testdata/) folder. Here is an example running `petstore3`, built from source: 41 | 42 | ```shell-session 43 | go run ./cmd/tfplugingen-openapi generate \ 44 | --config ./internal/cmd/testdata/petstore3/generator_config.yml \ 45 | --output ./internal/cmd/testdata/petstore3/provider_code_spec.json \ 46 | ./internal/cmd/testdata/petstore3/openapi_spec.json 47 | ``` 48 | 49 | ## License 50 | 51 | Refer to [Mozilla Public License v2.0](./LICENSE). 52 | 53 | ## Experimental Status 54 | 55 | By using the software in this repository (the "Software"), you acknowledge that: (1) the Software is still in development, may change, and has not been released as a commercial product by HashiCorp and is not currently supported in any way by HashiCorp; (2) the Software is provided on an "as-is" basis, and may include bugs, errors, or other issues; (3) the Software is NOT INTENDED FOR PRODUCTION USE, use of the Software may result in unexpected results, loss of data, or other unexpected results, and HashiCorp disclaims any and all liability resulting from use of the Software; and (4) HashiCorp reserves all rights to make all decisions about the features, functionality and commercial release (or non-release) of the Software, at any time and without any obligation or liability whatsoever. 56 | -------------------------------------------------------------------------------- /internal/mapper/frameworkvalidators/int64validator.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package frameworkvalidators 5 | 6 | import ( 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/hashicorp/terraform-plugin-codegen-spec/code" 11 | "github.com/hashicorp/terraform-plugin-codegen-spec/schema" 12 | ) 13 | 14 | const ( 15 | // Int64ValidatorPackage is the name of the int64 validation package in 16 | // the framework validators module. 17 | Int64ValidatorPackage = "int64validator" 18 | ) 19 | 20 | var ( 21 | // Int64ValidatorCodeImport is a single allocation of the framework 22 | // validators module int64validator package import. 23 | Int64ValidatorCodeImport code.Import = CodeImport(Int64ValidatorPackage) 24 | ) 25 | 26 | // Int64ValidatorAtLeast returns a custom validator mapped to the 27 | // int64validator package AtLeast function. 28 | func Int64ValidatorAtLeast(minimum int64) *schema.CustomValidator { 29 | var schemaDefinition strings.Builder 30 | 31 | schemaDefinition.WriteString(Int64ValidatorPackage) 32 | schemaDefinition.WriteString(".AtLeast(") 33 | schemaDefinition.WriteString(strconv.FormatInt(minimum, 10)) 34 | schemaDefinition.WriteString(")") 35 | 36 | return &schema.CustomValidator{ 37 | Imports: []code.Import{ 38 | Int64ValidatorCodeImport, 39 | }, 40 | SchemaDefinition: schemaDefinition.String(), 41 | } 42 | } 43 | 44 | // Int64ValidatorAtMost returns a custom validator mapped to the 45 | // int64validator package AtMost function. 46 | func Int64ValidatorAtMost(maximum int64) *schema.CustomValidator { 47 | var schemaDefinition strings.Builder 48 | 49 | schemaDefinition.WriteString(Int64ValidatorPackage) 50 | schemaDefinition.WriteString(".AtMost(") 51 | schemaDefinition.WriteString(strconv.FormatInt(maximum, 10)) 52 | schemaDefinition.WriteString(")") 53 | 54 | return &schema.CustomValidator{ 55 | Imports: []code.Import{ 56 | Int64ValidatorCodeImport, 57 | }, 58 | SchemaDefinition: schemaDefinition.String(), 59 | } 60 | } 61 | 62 | // Int64ValidatorBetween returns a custom validator mapped to the 63 | // int64validator package Between function. 64 | func Int64ValidatorBetween(minimum, maximum int64) *schema.CustomValidator { 65 | var schemaDefinition strings.Builder 66 | 67 | schemaDefinition.WriteString(Int64ValidatorPackage) 68 | schemaDefinition.WriteString(".Between(") 69 | schemaDefinition.WriteString(strconv.FormatInt(minimum, 10)) 70 | schemaDefinition.WriteString(", ") 71 | schemaDefinition.WriteString(strconv.FormatInt(maximum, 10)) 72 | schemaDefinition.WriteString(")") 73 | 74 | return &schema.CustomValidator{ 75 | Imports: []code.Import{ 76 | Int64ValidatorCodeImport, 77 | }, 78 | SchemaDefinition: schemaDefinition.String(), 79 | } 80 | } 81 | 82 | // Int64ValidatorOneOf returns a custom validator mapped to the int64validator 83 | // package OneOf function. If the values are nil or empty, nil is returned. 84 | func Int64ValidatorOneOf(values []int64) *schema.CustomValidator { 85 | if len(values) == 0 { 86 | return nil 87 | } 88 | 89 | var schemaDefinition strings.Builder 90 | 91 | schemaDefinition.WriteString(Int64ValidatorPackage) 92 | schemaDefinition.WriteString(".OneOf(\n") 93 | 94 | for _, value := range values { 95 | schemaDefinition.WriteString(strconv.FormatInt(value, 10) + ",\n") 96 | } 97 | 98 | schemaDefinition.WriteString(")") 99 | 100 | return &schema.CustomValidator{ 101 | Imports: []code.Import{ 102 | Int64ValidatorCodeImport, 103 | }, 104 | SchemaDefinition: schemaDefinition.String(), 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /internal/mapper/attrmapper/resource_attributes.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package attrmapper 5 | 6 | import ( 7 | "errors" 8 | "strings" 9 | 10 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/explorer" 11 | "github.com/hashicorp/terraform-plugin-codegen-spec/resource" 12 | ) 13 | 14 | type ResourceAttribute interface { 15 | GetName() string 16 | Merge(ResourceAttribute) (ResourceAttribute, error) 17 | ApplyOverride(explorer.Override) (ResourceAttribute, error) 18 | ToSpec() resource.Attribute 19 | } 20 | 21 | type ResourceNestedAttribute interface { 22 | ApplyNestedOverride([]string, explorer.Override) (ResourceAttribute, error) 23 | } 24 | 25 | type ResourceAttributes []ResourceAttribute 26 | 27 | func (targetSlice ResourceAttributes) Merge(mergeSlices ...ResourceAttributes) (ResourceAttributes, error) { 28 | var errResult error 29 | 30 | for _, mergeSlice := range mergeSlices { 31 | for _, mergeAttribute := range mergeSlice { 32 | // As we compare attributes, if we don't find a match, we should add this attribute to the slice after 33 | isNewAttribute := true 34 | 35 | for i, targetAttribute := range targetSlice { 36 | if targetAttribute.GetName() == mergeAttribute.GetName() { 37 | mergedAttribute, err := targetAttribute.Merge(mergeAttribute) 38 | if err != nil { 39 | // TODO: consider how best to surface this error 40 | // Currently, if the merge fails we should just keep the original target attribute for now 41 | errResult = errors.Join(errResult, err) 42 | } else { 43 | targetSlice[i] = mergedAttribute 44 | } 45 | 46 | isNewAttribute = false 47 | break 48 | } 49 | } 50 | 51 | if isNewAttribute { 52 | // Add this back to the original slice to avoid adding duplicate attributes from different mergeSlices 53 | targetSlice = append(targetSlice, mergeAttribute) 54 | } 55 | } 56 | } 57 | 58 | return targetSlice, errResult 59 | } 60 | 61 | func (attributes ResourceAttributes) ToSpec() []resource.Attribute { 62 | specAttributes := make([]resource.Attribute, 0, len(attributes)) 63 | for _, attribute := range attributes { 64 | specAttributes = append(specAttributes, attribute.ToSpec()) 65 | } 66 | 67 | return specAttributes 68 | } 69 | 70 | func (attributes ResourceAttributes) ApplyOverrides(overrideMap map[string]explorer.Override) (ResourceAttributes, error) { 71 | var errResult error 72 | for key, override := range overrideMap { 73 | var err error 74 | attributes, err = attributes.ApplyOverride(strings.Split(key, "."), override) 75 | errResult = errors.Join(errResult, err) 76 | } 77 | 78 | return attributes, errResult 79 | } 80 | 81 | func (attributes ResourceAttributes) ApplyOverride(path []string, override explorer.Override) (ResourceAttributes, error) { 82 | var errResult error 83 | if len(path) == 0 { 84 | return attributes, errResult 85 | } 86 | for i, attribute := range attributes { 87 | if attribute.GetName() == path[0] { 88 | 89 | if len(path) > 1 { 90 | nestedAttribute, ok := attribute.(ResourceNestedAttribute) 91 | if !ok { 92 | // TODO: error? there is a nested override for an attribute that is not a nested type 93 | break 94 | } 95 | 96 | // The attribute we need to override is deeper nested, move up 97 | nextPath := path[1:] 98 | 99 | overriddenAttribute, err := nestedAttribute.ApplyNestedOverride(nextPath, override) 100 | errResult = errors.Join(errResult, err) 101 | 102 | attributes[i] = overriddenAttribute 103 | 104 | } else { 105 | // No more path to traverse, apply override 106 | overriddenAttribute, err := attribute.ApplyOverride(override) 107 | errResult = errors.Join(errResult, err) 108 | 109 | attributes[i] = overriddenAttribute 110 | } 111 | 112 | break 113 | } 114 | } 115 | 116 | return attributes, errResult 117 | } 118 | -------------------------------------------------------------------------------- /internal/mapper/attrmapper/data_source_attributes.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package attrmapper 5 | 6 | import ( 7 | "errors" 8 | "strings" 9 | 10 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/explorer" 11 | "github.com/hashicorp/terraform-plugin-codegen-spec/datasource" 12 | ) 13 | 14 | type DataSourceAttribute interface { 15 | GetName() string 16 | Merge(DataSourceAttribute) (DataSourceAttribute, error) 17 | ApplyOverride(explorer.Override) (DataSourceAttribute, error) 18 | ToSpec() datasource.Attribute 19 | } 20 | 21 | type DataSourceNestedAttribute interface { 22 | ApplyNestedOverride([]string, explorer.Override) (DataSourceAttribute, error) 23 | } 24 | 25 | type DataSourceAttributes []DataSourceAttribute 26 | 27 | func (targetSlice DataSourceAttributes) Merge(mergeSlices ...DataSourceAttributes) (DataSourceAttributes, error) { 28 | var errResult error 29 | 30 | for _, mergeSlice := range mergeSlices { 31 | for _, mergeAttribute := range mergeSlice { 32 | // As we compare attributes, if we don't find a match, we should add this attribute to the slice after 33 | isNewAttribute := true 34 | 35 | for i, targetAttribute := range targetSlice { 36 | if targetAttribute.GetName() == mergeAttribute.GetName() { 37 | mergedAttribute, err := targetAttribute.Merge(mergeAttribute) 38 | if err != nil { 39 | // TODO: consider how best to surface this error 40 | // Currently, if the merge fails we should just keep the original target attribute for now 41 | errResult = errors.Join(errResult, err) 42 | } else { 43 | targetSlice[i] = mergedAttribute 44 | } 45 | 46 | isNewAttribute = false 47 | break 48 | } 49 | } 50 | 51 | if isNewAttribute { 52 | // Add this back to the original slice to avoid adding duplicate attributes from different mergeSlices 53 | targetSlice = append(targetSlice, mergeAttribute) 54 | } 55 | } 56 | } 57 | 58 | return targetSlice, errResult 59 | } 60 | 61 | func (attributes DataSourceAttributes) ToSpec() []datasource.Attribute { 62 | specAttributes := make([]datasource.Attribute, 0, len(attributes)) 63 | for _, attribute := range attributes { 64 | specAttributes = append(specAttributes, attribute.ToSpec()) 65 | } 66 | 67 | return specAttributes 68 | } 69 | 70 | func (attributes DataSourceAttributes) ApplyOverrides(overrideMap map[string]explorer.Override) (DataSourceAttributes, error) { 71 | var errResult error 72 | for key, override := range overrideMap { 73 | var err error 74 | attributes, err = attributes.ApplyOverride(strings.Split(key, "."), override) 75 | errResult = errors.Join(errResult, err) 76 | } 77 | 78 | return attributes, errResult 79 | } 80 | 81 | func (attributes DataSourceAttributes) ApplyOverride(path []string, override explorer.Override) (DataSourceAttributes, error) { 82 | var errResult error 83 | if len(path) == 0 { 84 | return attributes, errResult 85 | } 86 | for i, attribute := range attributes { 87 | if attribute.GetName() == path[0] { 88 | 89 | if len(path) > 1 { 90 | nestedAttribute, ok := attribute.(DataSourceNestedAttribute) 91 | if !ok { 92 | // TODO: error? there is a nested override for an attribute that is not a nested type 93 | break 94 | } 95 | 96 | // The attribute we need to override is deeper nested, move up 97 | nextPath := path[1:] 98 | 99 | overriddenAttribute, err := nestedAttribute.ApplyNestedOverride(nextPath, override) 100 | errResult = errors.Join(errResult, err) 101 | 102 | attributes[i] = overriddenAttribute 103 | 104 | } else { 105 | // No more path to traverse, apply override 106 | overriddenAttribute, err := attribute.ApplyOverride(override) 107 | errResult = errors.Join(errResult, err) 108 | 109 | attributes[i] = overriddenAttribute 110 | } 111 | 112 | break 113 | } 114 | } 115 | 116 | return attributes, errResult 117 | } 118 | -------------------------------------------------------------------------------- /internal/mapper/frameworkvalidators/listvalidator_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package frameworkvalidators_test 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/google/go-cmp/cmp" 10 | "github.com/hashicorp/terraform-plugin-codegen-spec/code" 11 | "github.com/hashicorp/terraform-plugin-codegen-spec/schema" 12 | 13 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/mapper/frameworkvalidators" 14 | ) 15 | 16 | func TestListValidatorSizeAtLeast(t *testing.T) { 17 | t.Parallel() 18 | 19 | testCases := map[string]struct { 20 | min int64 21 | expected *schema.CustomValidator 22 | }{ 23 | "test": { 24 | min: 123, 25 | expected: &schema.CustomValidator{ 26 | Imports: []code.Import{ 27 | { 28 | Path: "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator", 29 | }, 30 | }, 31 | SchemaDefinition: "listvalidator.SizeAtLeast(123)", 32 | }, 33 | }, 34 | } 35 | 36 | for name, testCase := range testCases { 37 | 38 | t.Run(name, func(t *testing.T) { 39 | t.Parallel() 40 | 41 | got := frameworkvalidators.ListValidatorSizeAtLeast(testCase.min) 42 | 43 | if diff := cmp.Diff(got, testCase.expected); diff != "" { 44 | t.Errorf("unexpected difference: %s", diff) 45 | } 46 | }) 47 | } 48 | } 49 | 50 | func TestListValidatorSizeAtMost(t *testing.T) { 51 | t.Parallel() 52 | 53 | testCases := map[string]struct { 54 | max int64 55 | expected *schema.CustomValidator 56 | }{ 57 | "test": { 58 | max: 123, 59 | expected: &schema.CustomValidator{ 60 | Imports: []code.Import{ 61 | { 62 | Path: "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator", 63 | }, 64 | }, 65 | SchemaDefinition: "listvalidator.SizeAtMost(123)", 66 | }, 67 | }, 68 | } 69 | 70 | for name, testCase := range testCases { 71 | 72 | t.Run(name, func(t *testing.T) { 73 | t.Parallel() 74 | 75 | got := frameworkvalidators.ListValidatorSizeAtMost(testCase.max) 76 | 77 | if diff := cmp.Diff(got, testCase.expected); diff != "" { 78 | t.Errorf("unexpected difference: %s", diff) 79 | } 80 | }) 81 | } 82 | } 83 | 84 | func TestListValidatorSizeBetween(t *testing.T) { 85 | t.Parallel() 86 | 87 | testCases := map[string]struct { 88 | min int64 89 | max int64 90 | expected *schema.CustomValidator 91 | }{ 92 | "test": { 93 | min: 123, 94 | max: 456, 95 | expected: &schema.CustomValidator{ 96 | Imports: []code.Import{ 97 | { 98 | Path: "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator", 99 | }, 100 | }, 101 | SchemaDefinition: "listvalidator.SizeBetween(123, 456)", 102 | }, 103 | }, 104 | } 105 | 106 | for name, testCase := range testCases { 107 | 108 | t.Run(name, func(t *testing.T) { 109 | t.Parallel() 110 | 111 | got := frameworkvalidators.ListValidatorSizeBetween(testCase.min, testCase.max) 112 | 113 | if diff := cmp.Diff(got, testCase.expected); diff != "" { 114 | t.Errorf("unexpected difference: %s", diff) 115 | } 116 | }) 117 | } 118 | } 119 | 120 | func TestListValidatorUniqueValues(t *testing.T) { 121 | t.Parallel() 122 | 123 | testCases := map[string]struct { 124 | expected *schema.CustomValidator 125 | }{ 126 | "test": { 127 | expected: &schema.CustomValidator{ 128 | Imports: []code.Import{ 129 | { 130 | Path: "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator", 131 | }, 132 | }, 133 | SchemaDefinition: "listvalidator.UniqueValues()", 134 | }, 135 | }, 136 | } 137 | 138 | for name, testCase := range testCases { 139 | 140 | t.Run(name, func(t *testing.T) { 141 | t.Parallel() 142 | 143 | got := frameworkvalidators.ListValidatorUniqueValues() 144 | 145 | if diff := cmp.Diff(got, testCase.expected); diff != "" { 146 | t.Errorf("unexpected difference: %s", diff) 147 | } 148 | }) 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /internal/mapper/attrmapper/single_nested.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package attrmapper 5 | 6 | import ( 7 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/explorer" 8 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/mapper/util" 9 | "github.com/hashicorp/terraform-plugin-codegen-spec/datasource" 10 | "github.com/hashicorp/terraform-plugin-codegen-spec/provider" 11 | "github.com/hashicorp/terraform-plugin-codegen-spec/resource" 12 | ) 13 | 14 | type ResourceSingleNestedAttribute struct { 15 | resource.SingleNestedAttribute 16 | 17 | Name string 18 | Attributes ResourceAttributes 19 | } 20 | 21 | func (a *ResourceSingleNestedAttribute) GetName() string { 22 | return a.Name 23 | } 24 | 25 | func (a *ResourceSingleNestedAttribute) Merge(mergeAttribute ResourceAttribute) (ResourceAttribute, error) { 26 | singleNestedAttribute, ok := mergeAttribute.(*ResourceSingleNestedAttribute) 27 | // TODO: return error if types don't match? 28 | if !ok { 29 | return a, nil 30 | } 31 | 32 | if a.Description == nil || *a.Description == "" { 33 | a.Description = singleNestedAttribute.Description 34 | } 35 | a.Attributes, _ = a.Attributes.Merge(singleNestedAttribute.Attributes) 36 | 37 | return a, nil 38 | } 39 | 40 | func (a *ResourceSingleNestedAttribute) ApplyOverride(override explorer.Override) (ResourceAttribute, error) { 41 | a.Description = &override.Description 42 | 43 | return a, nil 44 | } 45 | 46 | func (a *ResourceSingleNestedAttribute) ApplyNestedOverride(path []string, override explorer.Override) (ResourceAttribute, error) { 47 | var err error 48 | a.Attributes, err = a.Attributes.ApplyOverride(path, override) 49 | 50 | return a, err 51 | } 52 | 53 | func (a *ResourceSingleNestedAttribute) ToSpec() resource.Attribute { 54 | a.SingleNestedAttribute.Attributes = a.Attributes.ToSpec() 55 | 56 | return resource.Attribute{ 57 | Name: util.TerraformIdentifier(a.Name), 58 | SingleNested: &a.SingleNestedAttribute, 59 | } 60 | } 61 | 62 | type DataSourceSingleNestedAttribute struct { 63 | datasource.SingleNestedAttribute 64 | 65 | Name string 66 | Attributes DataSourceAttributes 67 | } 68 | 69 | func (a *DataSourceSingleNestedAttribute) GetName() string { 70 | return a.Name 71 | } 72 | 73 | func (a *DataSourceSingleNestedAttribute) Merge(mergeAttribute DataSourceAttribute) (DataSourceAttribute, error) { 74 | singleNestedAttribute, ok := mergeAttribute.(*DataSourceSingleNestedAttribute) 75 | // TODO: return error if types don't match? 76 | if !ok { 77 | return a, nil 78 | } 79 | 80 | if a.Description == nil || *a.Description == "" { 81 | a.Description = singleNestedAttribute.Description 82 | } 83 | a.Attributes, _ = a.Attributes.Merge(singleNestedAttribute.Attributes) 84 | 85 | return a, nil 86 | } 87 | 88 | func (a *DataSourceSingleNestedAttribute) ApplyOverride(override explorer.Override) (DataSourceAttribute, error) { 89 | a.Description = &override.Description 90 | 91 | return a, nil 92 | } 93 | 94 | func (a *DataSourceSingleNestedAttribute) ApplyNestedOverride(path []string, override explorer.Override) (DataSourceAttribute, error) { 95 | var err error 96 | a.Attributes, err = a.Attributes.ApplyOverride(path, override) 97 | 98 | return a, err 99 | } 100 | 101 | func (a *DataSourceSingleNestedAttribute) ToSpec() datasource.Attribute { 102 | a.SingleNestedAttribute.Attributes = a.Attributes.ToSpec() 103 | 104 | return datasource.Attribute{ 105 | Name: util.TerraformIdentifier(a.Name), 106 | SingleNested: &a.SingleNestedAttribute, 107 | } 108 | } 109 | 110 | type ProviderSingleNestedAttribute struct { 111 | provider.SingleNestedAttribute 112 | 113 | Name string 114 | Attributes ProviderAttributes 115 | } 116 | 117 | func (a *ProviderSingleNestedAttribute) ToSpec() provider.Attribute { 118 | a.SingleNestedAttribute.Attributes = a.Attributes.ToSpec() 119 | 120 | return provider.Attribute{ 121 | Name: util.TerraformIdentifier(a.Name), 122 | SingleNested: &a.SingleNestedAttribute, 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /internal/mapper/oas/schema_error.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package oas 5 | 6 | import ( 7 | "strings" 8 | 9 | "github.com/pb33f/libopenapi/datamodel/high/base" 10 | "gopkg.in/yaml.v3" 11 | ) 12 | 13 | // SchemaError contains additional details about an error that occurred when processing an OpenAPI schema, 14 | // such as the line number of the invalid schema or nested path information. 15 | type SchemaError struct { 16 | err error 17 | path []string 18 | lineNumber int 19 | } 20 | 21 | // Error implements the error interface by returning the original error string 22 | func (e *SchemaError) Error() string { 23 | return e.err.Error() 24 | } 25 | 26 | // NestedSchemaError creates a new SchemaError, appending the parent name to the path. This allows a parent 27 | // OpenAPI schema to preserve the error and line number from a child schema, while creating a path name that is an absolute reference. 28 | // 29 | // If no line number exists for the child schema, the parent schema line number will be added. 30 | func (e *SchemaError) NestedSchemaError(parentName string, lineNumber int) *SchemaError { 31 | newErr := &SchemaError{ 32 | err: e.err, 33 | path: append([]string{parentName}, e.path...), 34 | lineNumber: e.lineNumber, 35 | } 36 | 37 | if newErr.lineNumber == 0 { 38 | newErr.lineNumber = lineNumber 39 | } 40 | 41 | return newErr 42 | } 43 | 44 | // Path returns an absolute reference to the schema where the error occurred. 45 | func (e *SchemaError) Path() string { 46 | return strings.Join(e.path, ".") 47 | } 48 | 49 | // LineNumber returns the line number closest to the schema where the error occurred. 50 | func (e *SchemaError) LineNumber() int { 51 | return e.lineNumber 52 | } 53 | 54 | // NewSchemaError returns a new SchemaError error struct 55 | func NewSchemaError(err error, lineNumber int, path ...string) *SchemaError { 56 | return &SchemaError{ 57 | err: err, 58 | path: path, 59 | lineNumber: lineNumber, 60 | } 61 | } 62 | 63 | type NodeType int 64 | 65 | const ( 66 | None NodeType = iota 67 | Type 68 | AdditionalProperties 69 | Items 70 | AllOf 71 | AnyOf 72 | OneOf 73 | ) 74 | 75 | // SchemaErrorFromNode returns a new SchemaError error struct that has no path information, using a schema node to get the line number if available. 76 | func SchemaErrorFromNode(err error, schema *base.Schema, nodeType NodeType) *SchemaError { 77 | // If there is no low information, then we can't retrieve any line numbers 78 | low := schema.GoLow() 79 | if low == nil { 80 | return emptySchemaError(err) 81 | } 82 | 83 | var valueNode *yaml.Node 84 | switch nodeType { 85 | case Type: 86 | valueNode = low.Type.ValueNode 87 | case AdditionalProperties: 88 | valueNode = low.AdditionalProperties.ValueNode 89 | case Items: 90 | valueNode = low.Items.ValueNode 91 | case AllOf: 92 | valueNode = low.AllOf.ValueNode 93 | case AnyOf: 94 | valueNode = low.AnyOf.ValueNode 95 | case OneOf: 96 | valueNode = low.OneOf.ValueNode 97 | } 98 | 99 | lineNumber := 0 100 | if valueNode != nil { 101 | lineNumber = valueNode.Line 102 | } 103 | 104 | return &SchemaError{ 105 | err: err, 106 | path: make([]string, 0), 107 | lineNumber: lineNumber, 108 | } 109 | } 110 | 111 | // SchemaErrorFromProxy returns a new SchemaError error struct that has no path information, using a schema proxy to get the line number. 112 | func SchemaErrorFromProxy(err error, proxy *base.SchemaProxy) *SchemaError { 113 | // If there is no low information, then we can't retrieve any line numbers 114 | if proxy == nil || proxy.GoLow() == nil || proxy.GoLow().GetValueNode() == nil { 115 | return emptySchemaError(err) 116 | } 117 | 118 | return &SchemaError{ 119 | err: err, 120 | path: make([]string, 0), 121 | lineNumber: proxy.GoLow().GetValueNode().Line, 122 | } 123 | } 124 | 125 | // emptySchemaError will return a simple SchemaError struct that contains no additional OAS information 126 | func emptySchemaError(err error) *SchemaError { 127 | return &SchemaError{ 128 | err: err, 129 | path: make([]string, 0), 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /internal/mapper/oas/integer.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package oas 5 | 6 | import ( 7 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/mapper/attrmapper" 8 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/mapper/frameworkvalidators" 9 | "github.com/hashicorp/terraform-plugin-codegen-spec/datasource" 10 | "github.com/hashicorp/terraform-plugin-codegen-spec/provider" 11 | "github.com/hashicorp/terraform-plugin-codegen-spec/resource" 12 | "github.com/hashicorp/terraform-plugin-codegen-spec/schema" 13 | ) 14 | 15 | func (s *OASSchema) BuildIntegerResource(name string, computability schema.ComputedOptionalRequired) (attrmapper.ResourceAttribute, *SchemaError) { 16 | result := &attrmapper.ResourceInt64Attribute{ 17 | Name: name, 18 | Int64Attribute: resource.Int64Attribute{ 19 | ComputedOptionalRequired: computability, 20 | DeprecationMessage: s.GetDeprecationMessage(), 21 | Description: s.GetDescription(), 22 | }, 23 | } 24 | 25 | if s.Schema.Default != nil { 26 | var staticDefault int64 27 | if err := s.Schema.Default.Decode(&staticDefault); err == nil { 28 | if computability == schema.Required { 29 | result.ComputedOptionalRequired = schema.ComputedOptional 30 | } 31 | 32 | result.Default = &schema.Int64Default{ 33 | Static: &staticDefault, 34 | } 35 | } 36 | } 37 | 38 | if computability != schema.Computed { 39 | result.Validators = s.GetIntegerValidators() 40 | } 41 | 42 | return result, nil 43 | } 44 | 45 | func (s *OASSchema) BuildIntegerDataSource(name string, computability schema.ComputedOptionalRequired) (attrmapper.DataSourceAttribute, *SchemaError) { 46 | result := &attrmapper.DataSourceInt64Attribute{ 47 | Name: name, 48 | Int64Attribute: datasource.Int64Attribute{ 49 | ComputedOptionalRequired: computability, 50 | DeprecationMessage: s.GetDeprecationMessage(), 51 | Description: s.GetDescription(), 52 | }, 53 | } 54 | 55 | if computability != schema.Computed { 56 | result.Validators = s.GetIntegerValidators() 57 | } 58 | 59 | return result, nil 60 | } 61 | 62 | func (s *OASSchema) BuildIntegerProvider(name string, optionalOrRequired schema.OptionalRequired) (attrmapper.ProviderAttribute, *SchemaError) { 63 | result := &attrmapper.ProviderInt64Attribute{ 64 | Name: name, 65 | Int64Attribute: provider.Int64Attribute{ 66 | OptionalRequired: optionalOrRequired, 67 | DeprecationMessage: s.GetDeprecationMessage(), 68 | Description: s.GetDescription(), 69 | Validators: s.GetIntegerValidators(), 70 | }, 71 | } 72 | 73 | return result, nil 74 | } 75 | 76 | func (s *OASSchema) BuildIntegerElementType() (schema.ElementType, *SchemaError) { 77 | return schema.ElementType{ 78 | Int64: &schema.Int64Type{}, 79 | }, nil 80 | } 81 | 82 | func (s *OASSchema) GetIntegerValidators() []schema.Int64Validator { 83 | var result []schema.Int64Validator 84 | 85 | if len(s.Schema.Enum) > 0 { 86 | var enum []int64 87 | 88 | for _, valueNode := range s.Schema.Enum { 89 | var value int64 90 | if err := valueNode.Decode(&value); err != nil { 91 | // could consider error/panic here to notify developers 92 | continue 93 | } 94 | 95 | enum = append(enum, value) 96 | } 97 | 98 | customValidator := frameworkvalidators.Int64ValidatorOneOf(enum) 99 | 100 | if customValidator != nil { 101 | result = append(result, schema.Int64Validator{ 102 | Custom: customValidator, 103 | }) 104 | } 105 | } 106 | 107 | minimum := s.Schema.Minimum 108 | maximum := s.Schema.Maximum 109 | 110 | if minimum != nil && maximum != nil { 111 | result = append(result, schema.Int64Validator{ 112 | Custom: frameworkvalidators.Int64ValidatorBetween(int64(*minimum), int64(*maximum)), 113 | }) 114 | } else if minimum != nil { 115 | result = append(result, schema.Int64Validator{ 116 | Custom: frameworkvalidators.Int64ValidatorAtLeast(int64(*minimum)), 117 | }) 118 | } else if maximum != nil { 119 | result = append(result, schema.Int64Validator{ 120 | Custom: frameworkvalidators.Int64ValidatorAtMost(int64(*maximum)), 121 | }) 122 | } 123 | 124 | return result 125 | } 126 | -------------------------------------------------------------------------------- /internal/mapper/attrmapper/map_nested.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package attrmapper 5 | 6 | import ( 7 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/explorer" 8 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/mapper/util" 9 | "github.com/hashicorp/terraform-plugin-codegen-spec/datasource" 10 | "github.com/hashicorp/terraform-plugin-codegen-spec/provider" 11 | "github.com/hashicorp/terraform-plugin-codegen-spec/resource" 12 | ) 13 | 14 | type ResourceMapNestedAttribute struct { 15 | resource.MapNestedAttribute 16 | 17 | Name string 18 | NestedObject ResourceNestedAttributeObject 19 | } 20 | 21 | func (a *ResourceMapNestedAttribute) GetName() string { 22 | return a.Name 23 | } 24 | 25 | func (a *ResourceMapNestedAttribute) Merge(mergeAttribute ResourceAttribute) (ResourceAttribute, error) { 26 | mapNestedAttribute, ok := mergeAttribute.(*ResourceMapNestedAttribute) 27 | // TODO: return error if types don't match? 28 | if !ok { 29 | return a, nil 30 | } 31 | 32 | if a.Description == nil || *a.Description == "" { 33 | a.Description = mapNestedAttribute.Description 34 | } 35 | a.NestedObject.Attributes, _ = a.NestedObject.Attributes.Merge(mapNestedAttribute.NestedObject.Attributes) 36 | 37 | return a, nil 38 | } 39 | 40 | func (a *ResourceMapNestedAttribute) ApplyOverride(override explorer.Override) (ResourceAttribute, error) { 41 | a.Description = &override.Description 42 | 43 | return a, nil 44 | } 45 | 46 | func (a *ResourceMapNestedAttribute) ApplyNestedOverride(path []string, override explorer.Override) (ResourceAttribute, error) { 47 | var err error 48 | a.NestedObject.Attributes, err = a.NestedObject.Attributes.ApplyOverride(path, override) 49 | 50 | return a, err 51 | } 52 | 53 | func (a *ResourceMapNestedAttribute) ToSpec() resource.Attribute { 54 | a.MapNestedAttribute.NestedObject = resource.NestedAttributeObject{ 55 | Attributes: a.NestedObject.Attributes.ToSpec(), 56 | } 57 | 58 | return resource.Attribute{ 59 | Name: util.TerraformIdentifier(a.Name), 60 | MapNested: &a.MapNestedAttribute, 61 | } 62 | } 63 | 64 | type DataSourceMapNestedAttribute struct { 65 | datasource.MapNestedAttribute 66 | 67 | Name string 68 | NestedObject DataSourceNestedAttributeObject 69 | } 70 | 71 | func (a *DataSourceMapNestedAttribute) GetName() string { 72 | return a.Name 73 | } 74 | 75 | func (a *DataSourceMapNestedAttribute) Merge(mergeAttribute DataSourceAttribute) (DataSourceAttribute, error) { 76 | mapNestedAttribute, ok := mergeAttribute.(*DataSourceMapNestedAttribute) 77 | // TODO: return error if types don't match? 78 | if !ok { 79 | return a, nil 80 | } 81 | 82 | if a.Description == nil || *a.Description == "" { 83 | a.Description = mapNestedAttribute.Description 84 | } 85 | a.NestedObject.Attributes, _ = a.NestedObject.Attributes.Merge(mapNestedAttribute.NestedObject.Attributes) 86 | 87 | return a, nil 88 | } 89 | 90 | func (a *DataSourceMapNestedAttribute) ApplyOverride(override explorer.Override) (DataSourceAttribute, error) { 91 | a.Description = &override.Description 92 | 93 | return a, nil 94 | } 95 | 96 | func (a *DataSourceMapNestedAttribute) ApplyNestedOverride(path []string, override explorer.Override) (DataSourceAttribute, error) { 97 | var err error 98 | a.NestedObject.Attributes, err = a.NestedObject.Attributes.ApplyOverride(path, override) 99 | 100 | return a, err 101 | } 102 | 103 | func (a *DataSourceMapNestedAttribute) ToSpec() datasource.Attribute { 104 | a.MapNestedAttribute.NestedObject = datasource.NestedAttributeObject{ 105 | Attributes: a.NestedObject.Attributes.ToSpec(), 106 | } 107 | 108 | return datasource.Attribute{ 109 | Name: util.TerraformIdentifier(a.Name), 110 | MapNested: &a.MapNestedAttribute, 111 | } 112 | } 113 | 114 | type ProviderMapNestedAttribute struct { 115 | provider.MapNestedAttribute 116 | 117 | Name string 118 | NestedObject ProviderNestedAttributeObject 119 | } 120 | 121 | func (a *ProviderMapNestedAttribute) ToSpec() provider.Attribute { 122 | a.MapNestedAttribute.NestedObject = provider.NestedAttributeObject{ 123 | Attributes: a.NestedObject.Attributes.ToSpec(), 124 | } 125 | 126 | return provider.Attribute{ 127 | Name: util.TerraformIdentifier(a.Name), 128 | MapNested: &a.MapNestedAttribute, 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /internal/mapper/attrmapper/set_nested.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package attrmapper 5 | 6 | import ( 7 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/explorer" 8 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/mapper/util" 9 | "github.com/hashicorp/terraform-plugin-codegen-spec/datasource" 10 | "github.com/hashicorp/terraform-plugin-codegen-spec/provider" 11 | "github.com/hashicorp/terraform-plugin-codegen-spec/resource" 12 | ) 13 | 14 | type ResourceSetNestedAttribute struct { 15 | resource.SetNestedAttribute 16 | 17 | Name string 18 | NestedObject ResourceNestedAttributeObject 19 | } 20 | 21 | func (a *ResourceSetNestedAttribute) GetName() string { 22 | return a.Name 23 | } 24 | 25 | func (a *ResourceSetNestedAttribute) Merge(mergeAttribute ResourceAttribute) (ResourceAttribute, error) { 26 | setNestedAttribute, ok := mergeAttribute.(*ResourceSetNestedAttribute) 27 | // TODO: return error if types don't match? 28 | if !ok { 29 | return a, nil 30 | } 31 | 32 | if a.Description == nil || *a.Description == "" { 33 | a.Description = setNestedAttribute.Description 34 | } 35 | a.NestedObject.Attributes, _ = a.NestedObject.Attributes.Merge(setNestedAttribute.NestedObject.Attributes) 36 | 37 | return a, nil 38 | } 39 | 40 | func (a *ResourceSetNestedAttribute) ApplyOverride(override explorer.Override) (ResourceAttribute, error) { 41 | a.Description = &override.Description 42 | 43 | return a, nil 44 | } 45 | 46 | func (a *ResourceSetNestedAttribute) ApplyNestedOverride(path []string, override explorer.Override) (ResourceAttribute, error) { 47 | var err error 48 | a.NestedObject.Attributes, err = a.NestedObject.Attributes.ApplyOverride(path, override) 49 | 50 | return a, err 51 | } 52 | 53 | func (a *ResourceSetNestedAttribute) ToSpec() resource.Attribute { 54 | a.SetNestedAttribute.NestedObject = resource.NestedAttributeObject{ 55 | Attributes: a.NestedObject.Attributes.ToSpec(), 56 | } 57 | 58 | return resource.Attribute{ 59 | Name: util.TerraformIdentifier(a.Name), 60 | SetNested: &a.SetNestedAttribute, 61 | } 62 | } 63 | 64 | type DataSourceSetNestedAttribute struct { 65 | datasource.SetNestedAttribute 66 | 67 | Name string 68 | NestedObject DataSourceNestedAttributeObject 69 | } 70 | 71 | func (a *DataSourceSetNestedAttribute) GetName() string { 72 | return a.Name 73 | } 74 | 75 | func (a *DataSourceSetNestedAttribute) Merge(mergeAttribute DataSourceAttribute) (DataSourceAttribute, error) { 76 | setNestedAttribute, ok := mergeAttribute.(*DataSourceSetNestedAttribute) 77 | // TODO: return error if types don't match? 78 | if !ok { 79 | return a, nil 80 | } 81 | 82 | if a.Description == nil || *a.Description == "" { 83 | a.Description = setNestedAttribute.Description 84 | } 85 | a.NestedObject.Attributes, _ = a.NestedObject.Attributes.Merge(setNestedAttribute.NestedObject.Attributes) 86 | 87 | return a, nil 88 | } 89 | 90 | func (a *DataSourceSetNestedAttribute) ApplyOverride(override explorer.Override) (DataSourceAttribute, error) { 91 | a.Description = &override.Description 92 | 93 | return a, nil 94 | } 95 | 96 | func (a *DataSourceSetNestedAttribute) ApplyNestedOverride(path []string, override explorer.Override) (DataSourceAttribute, error) { 97 | var err error 98 | a.NestedObject.Attributes, err = a.NestedObject.Attributes.ApplyOverride(path, override) 99 | 100 | return a, err 101 | } 102 | 103 | func (a *DataSourceSetNestedAttribute) ToSpec() datasource.Attribute { 104 | a.SetNestedAttribute.NestedObject = datasource.NestedAttributeObject{ 105 | Attributes: a.NestedObject.Attributes.ToSpec(), 106 | } 107 | 108 | return datasource.Attribute{ 109 | Name: util.TerraformIdentifier(a.Name), 110 | SetNested: &a.SetNestedAttribute, 111 | } 112 | } 113 | 114 | type ProviderSetNestedAttribute struct { 115 | provider.SetNestedAttribute 116 | 117 | Name string 118 | NestedObject ProviderNestedAttributeObject 119 | } 120 | 121 | func (a *ProviderSetNestedAttribute) ToSpec() provider.Attribute { 122 | a.SetNestedAttribute.NestedObject = provider.NestedAttributeObject{ 123 | Attributes: a.NestedObject.Attributes.ToSpec(), 124 | } 125 | 126 | return provider.Attribute{ 127 | Name: util.TerraformIdentifier(a.Name), 128 | SetNested: &a.SetNestedAttribute, 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /internal/mapper/attrmapper/list_nested.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package attrmapper 5 | 6 | import ( 7 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/explorer" 8 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/mapper/util" 9 | "github.com/hashicorp/terraform-plugin-codegen-spec/datasource" 10 | "github.com/hashicorp/terraform-plugin-codegen-spec/provider" 11 | "github.com/hashicorp/terraform-plugin-codegen-spec/resource" 12 | ) 13 | 14 | type ResourceListNestedAttribute struct { 15 | resource.ListNestedAttribute 16 | 17 | Name string 18 | NestedObject ResourceNestedAttributeObject 19 | } 20 | 21 | func (a *ResourceListNestedAttribute) GetName() string { 22 | return a.Name 23 | } 24 | 25 | func (a *ResourceListNestedAttribute) Merge(mergeAttribute ResourceAttribute) (ResourceAttribute, error) { 26 | listNestedAttribute, ok := mergeAttribute.(*ResourceListNestedAttribute) 27 | // TODO: return error if types don't match? 28 | if !ok { 29 | return a, nil 30 | } 31 | 32 | if a.Description == nil || *a.Description == "" { 33 | a.Description = listNestedAttribute.Description 34 | } 35 | a.NestedObject.Attributes, _ = a.NestedObject.Attributes.Merge(listNestedAttribute.NestedObject.Attributes) 36 | 37 | return a, nil 38 | } 39 | 40 | func (a *ResourceListNestedAttribute) ApplyOverride(override explorer.Override) (ResourceAttribute, error) { 41 | a.Description = &override.Description 42 | 43 | return a, nil 44 | } 45 | 46 | func (a *ResourceListNestedAttribute) ApplyNestedOverride(path []string, override explorer.Override) (ResourceAttribute, error) { 47 | var err error 48 | a.NestedObject.Attributes, err = a.NestedObject.Attributes.ApplyOverride(path, override) 49 | 50 | return a, err 51 | } 52 | 53 | func (a *ResourceListNestedAttribute) ToSpec() resource.Attribute { 54 | a.ListNestedAttribute.NestedObject = resource.NestedAttributeObject{ 55 | Attributes: a.NestedObject.Attributes.ToSpec(), 56 | } 57 | 58 | return resource.Attribute{ 59 | Name: util.TerraformIdentifier(a.Name), 60 | ListNested: &a.ListNestedAttribute, 61 | } 62 | } 63 | 64 | type DataSourceListNestedAttribute struct { 65 | datasource.ListNestedAttribute 66 | 67 | Name string 68 | NestedObject DataSourceNestedAttributeObject 69 | } 70 | 71 | func (a *DataSourceListNestedAttribute) GetName() string { 72 | return a.Name 73 | } 74 | 75 | func (a *DataSourceListNestedAttribute) Merge(mergeAttribute DataSourceAttribute) (DataSourceAttribute, error) { 76 | listNestedAttribute, ok := mergeAttribute.(*DataSourceListNestedAttribute) 77 | // TODO: return error if types don't match? 78 | if !ok { 79 | return a, nil 80 | } 81 | 82 | if a.Description == nil || *a.Description == "" { 83 | a.Description = listNestedAttribute.Description 84 | } 85 | a.NestedObject.Attributes, _ = a.NestedObject.Attributes.Merge(listNestedAttribute.NestedObject.Attributes) 86 | 87 | return a, nil 88 | } 89 | 90 | func (a *DataSourceListNestedAttribute) ApplyOverride(override explorer.Override) (DataSourceAttribute, error) { 91 | a.Description = &override.Description 92 | 93 | return a, nil 94 | } 95 | 96 | func (a *DataSourceListNestedAttribute) ApplyNestedOverride(path []string, override explorer.Override) (DataSourceAttribute, error) { 97 | var err error 98 | a.NestedObject.Attributes, err = a.NestedObject.Attributes.ApplyOverride(path, override) 99 | 100 | return a, err 101 | } 102 | 103 | func (a *DataSourceListNestedAttribute) ToSpec() datasource.Attribute { 104 | a.ListNestedAttribute.NestedObject = datasource.NestedAttributeObject{ 105 | Attributes: a.NestedObject.Attributes.ToSpec(), 106 | } 107 | 108 | return datasource.Attribute{ 109 | Name: util.TerraformIdentifier(a.Name), 110 | ListNested: &a.ListNestedAttribute, 111 | } 112 | } 113 | 114 | type ProviderListNestedAttribute struct { 115 | provider.ListNestedAttribute 116 | 117 | Name string 118 | NestedObject ProviderNestedAttributeObject 119 | } 120 | 121 | func (a *ProviderListNestedAttribute) ToSpec() provider.Attribute { 122 | a.ListNestedAttribute.NestedObject = provider.NestedAttributeObject{ 123 | Attributes: a.NestedObject.Attributes.ToSpec(), 124 | } 125 | 126 | return provider.Attribute{ 127 | Name: util.TerraformIdentifier(a.Name), 128 | ListNested: &a.ListNestedAttribute, 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /internal/mapper/frameworkvalidators/int64validator_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package frameworkvalidators_test 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/google/go-cmp/cmp" 10 | "github.com/hashicorp/terraform-plugin-codegen-spec/code" 11 | "github.com/hashicorp/terraform-plugin-codegen-spec/schema" 12 | 13 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/mapper/frameworkvalidators" 14 | ) 15 | 16 | func TestInt64ValidatorAtLeast(t *testing.T) { 17 | t.Parallel() 18 | 19 | testCases := map[string]struct { 20 | min int64 21 | expected *schema.CustomValidator 22 | }{ 23 | "test": { 24 | min: 123, 25 | expected: &schema.CustomValidator{ 26 | Imports: []code.Import{ 27 | { 28 | Path: "github.com/hashicorp/terraform-plugin-framework-validators/int64validator", 29 | }, 30 | }, 31 | SchemaDefinition: "int64validator.AtLeast(123)", 32 | }, 33 | }, 34 | } 35 | 36 | for name, testCase := range testCases { 37 | 38 | t.Run(name, func(t *testing.T) { 39 | t.Parallel() 40 | 41 | got := frameworkvalidators.Int64ValidatorAtLeast(testCase.min) 42 | 43 | if diff := cmp.Diff(got, testCase.expected); diff != "" { 44 | t.Errorf("unexpected difference: %s", diff) 45 | } 46 | }) 47 | } 48 | } 49 | 50 | func TestInt64ValidatorAtMost(t *testing.T) { 51 | t.Parallel() 52 | 53 | testCases := map[string]struct { 54 | max int64 55 | expected *schema.CustomValidator 56 | }{ 57 | "test": { 58 | max: 123, 59 | expected: &schema.CustomValidator{ 60 | Imports: []code.Import{ 61 | { 62 | Path: "github.com/hashicorp/terraform-plugin-framework-validators/int64validator", 63 | }, 64 | }, 65 | SchemaDefinition: "int64validator.AtMost(123)", 66 | }, 67 | }, 68 | } 69 | 70 | for name, testCase := range testCases { 71 | 72 | t.Run(name, func(t *testing.T) { 73 | t.Parallel() 74 | 75 | got := frameworkvalidators.Int64ValidatorAtMost(testCase.max) 76 | 77 | if diff := cmp.Diff(got, testCase.expected); diff != "" { 78 | t.Errorf("unexpected difference: %s", diff) 79 | } 80 | }) 81 | } 82 | } 83 | 84 | func TestInt64ValidatorBetween(t *testing.T) { 85 | t.Parallel() 86 | 87 | testCases := map[string]struct { 88 | min int64 89 | max int64 90 | expected *schema.CustomValidator 91 | }{ 92 | "test": { 93 | min: 123, 94 | max: 456, 95 | expected: &schema.CustomValidator{ 96 | Imports: []code.Import{ 97 | { 98 | Path: "github.com/hashicorp/terraform-plugin-framework-validators/int64validator", 99 | }, 100 | }, 101 | SchemaDefinition: "int64validator.Between(123, 456)", 102 | }, 103 | }, 104 | } 105 | 106 | for name, testCase := range testCases { 107 | 108 | t.Run(name, func(t *testing.T) { 109 | t.Parallel() 110 | 111 | got := frameworkvalidators.Int64ValidatorBetween(testCase.min, testCase.max) 112 | 113 | if diff := cmp.Diff(got, testCase.expected); diff != "" { 114 | t.Errorf("unexpected difference: %s", diff) 115 | } 116 | }) 117 | } 118 | } 119 | 120 | func TestInt64ValidatorOneOf(t *testing.T) { 121 | t.Parallel() 122 | 123 | testCases := map[string]struct { 124 | values []int64 125 | expected *schema.CustomValidator 126 | }{ 127 | "nil": { 128 | values: nil, 129 | expected: nil, 130 | }, 131 | "empty": { 132 | values: []int64{}, 133 | expected: nil, 134 | }, 135 | "one": { 136 | values: []int64{1}, 137 | expected: &schema.CustomValidator{ 138 | Imports: []code.Import{ 139 | { 140 | Path: "github.com/hashicorp/terraform-plugin-framework-validators/int64validator", 141 | }, 142 | }, 143 | SchemaDefinition: "int64validator.OneOf(\n1,\n)", 144 | }, 145 | }, 146 | "multiple": { 147 | values: []int64{1, 2}, 148 | expected: &schema.CustomValidator{ 149 | Imports: []code.Import{ 150 | { 151 | Path: "github.com/hashicorp/terraform-plugin-framework-validators/int64validator", 152 | }, 153 | }, 154 | SchemaDefinition: "int64validator.OneOf(\n1,\n2,\n)", 155 | }, 156 | }, 157 | } 158 | 159 | for name, testCase := range testCases { 160 | 161 | t.Run(name, func(t *testing.T) { 162 | t.Parallel() 163 | 164 | got := frameworkvalidators.Int64ValidatorOneOf(testCase.values) 165 | 166 | if diff := cmp.Diff(got, testCase.expected); diff != "" { 167 | t.Errorf("unexpected difference: %s", diff) 168 | } 169 | }) 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | versionNumber: 7 | description: 'Release version number (v#.#.#)' 8 | type: string 9 | required: true 10 | 11 | permissions: 12 | contents: read # Changelog commit operations use service account PAT 13 | 14 | jobs: 15 | changelog-version: 16 | runs-on: ubuntu-latest 17 | outputs: 18 | version: ${{ steps.changelog-version.outputs.version }} 19 | steps: 20 | - id: changelog-version 21 | run: echo "version=$(echo "${{ inputs.versionNumber }}" | cut -c 2-)" >> "$GITHUB_OUTPUT" 22 | 23 | changelog: 24 | needs: changelog-version 25 | runs-on: ubuntu-latest 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 29 | with: 30 | fetch-depth: 0 31 | # Avoid persisting GITHUB_TOKEN credentials as they take priority over our service account PAT for `git push` operations 32 | # More details: https://github.com/actions/checkout/blob/b4626ce19ce1106186ddf9bb20e706842f11a7c3/adrs/0153-checkout-v2.md#persist-credentials 33 | persist-credentials: false 34 | - name: Batch changes 35 | uses: miniscruff/changie-action@6dcc2533cac0495148ed4046c438487e4dceaa23 # v2.0.0 36 | with: 37 | version: latest 38 | args: batch ${{ needs.changelog-version.outputs.version }} 39 | - name: Merge changes 40 | uses: miniscruff/changie-action@6dcc2533cac0495148ed4046c438487e4dceaa23 # v2.0.0 41 | with: 42 | version: latest 43 | args: merge 44 | - name: Git push changelog 45 | run: | 46 | git config --global user.name "${{ vars.TF_DEVEX_CI_COMMIT_AUTHOR }}" 47 | git config --global user.email "${{ vars.TF_DEVEX_CI_COMMIT_EMAIL }}" 48 | git add . 49 | git commit -a -m "Update changelog" 50 | git push "https://${{ vars.TF_DEVEX_CI_COMMIT_AUTHOR }}:${{ secrets.TF_DEVEX_COMMIT_GITHUB_TOKEN }}@github.com/${{ github.repository }}.git" 51 | 52 | release-tag: 53 | needs: changelog 54 | runs-on: ubuntu-latest 55 | steps: 56 | - name: Checkout 57 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 58 | with: 59 | fetch-depth: 0 60 | # Default input is the SHA that initially triggered the workflow. As we created a new commit in the previous job, 61 | # to ensure we get the latest commit we use the ref for checkout: 'refs/heads/' 62 | ref: ${{ github.ref }} 63 | # Avoid persisting GITHUB_TOKEN credentials as they take priority over our service account PAT for `git push` operations 64 | # More details: https://github.com/actions/checkout/blob/b4626ce19ce1106186ddf9bb20e706842f11a7c3/adrs/0153-checkout-v2.md#persist-credentials 65 | persist-credentials: false 66 | 67 | - name: Git push release tag 68 | run: | 69 | git config --global user.name "${{ vars.TF_DEVEX_CI_COMMIT_AUTHOR }}" 70 | git config --global user.email "${{ vars.TF_DEVEX_CI_COMMIT_EMAIL }}" 71 | 72 | git tag "${{ inputs.versionNumber }}" 73 | git push "https://${{ vars.TF_DEVEX_CI_COMMIT_AUTHOR }}:${{ secrets.TF_DEVEX_COMMIT_GITHUB_TOKEN }}@github.com/${{ github.repository }}.git" "${{ inputs.versionNumber }}" 74 | 75 | goreleaser: 76 | needs: [ changelog-version, changelog, release-tag ] 77 | runs-on: ubuntu-latest 78 | permissions: 79 | contents: write # Needed for goreleaser to create GitHub release 80 | issues: write # Needed for goreleaser to close associated milestone 81 | steps: 82 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 83 | with: 84 | ref: ${{ inputs.versionNumber }} 85 | fetch-depth: 0 86 | 87 | - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 88 | with: 89 | go-version-file: 'go.mod' 90 | 91 | - name: Generate Release Notes 92 | run: | 93 | cd .changes 94 | sed -e "1{/# /d;}" -e "2{/^$/d;}" ${{ needs.changelog-version.outputs.version }}.md > /tmp/release-notes.txt 95 | 96 | - uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0 97 | env: 98 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 99 | with: 100 | args: release --release-notes /tmp/release-notes.txt --clean -------------------------------------------------------------------------------- /internal/mapper/frameworkvalidators/stringvalidator.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package frameworkvalidators 5 | 6 | import ( 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/hashicorp/terraform-plugin-codegen-spec/code" 11 | "github.com/hashicorp/terraform-plugin-codegen-spec/schema" 12 | ) 13 | 14 | const ( 15 | // StringValidatorPackage is the name of the string validation package in 16 | // the framework validators module. 17 | StringValidatorPackage = "stringvalidator" 18 | ) 19 | 20 | var ( 21 | // StringValidatorCodeImport is a single allocation of the framework 22 | // validators module stringvalidator package import. 23 | StringValidatorCodeImport code.Import = CodeImport(StringValidatorPackage) 24 | ) 25 | 26 | // StringValidatorLengthAtLeast returns a custom validator mapped to the 27 | // stringvalidator package LengthAtLeast function. 28 | func StringValidatorLengthAtLeast(minimum int64) *schema.CustomValidator { 29 | var schemaDefinition strings.Builder 30 | 31 | schemaDefinition.WriteString(StringValidatorPackage) 32 | schemaDefinition.WriteString(".LengthAtLeast(") 33 | schemaDefinition.WriteString(strconv.FormatInt(minimum, 10)) 34 | schemaDefinition.WriteString(")") 35 | 36 | return &schema.CustomValidator{ 37 | Imports: []code.Import{ 38 | StringValidatorCodeImport, 39 | }, 40 | SchemaDefinition: schemaDefinition.String(), 41 | } 42 | } 43 | 44 | // StringValidatorLengthAtMost returns a custom validator mapped to the 45 | // stringvalidator package LengthAtMost function. 46 | func StringValidatorLengthAtMost(maximum int64) *schema.CustomValidator { 47 | var schemaDefinition strings.Builder 48 | 49 | schemaDefinition.WriteString(StringValidatorPackage) 50 | schemaDefinition.WriteString(".LengthAtMost(") 51 | schemaDefinition.WriteString(strconv.FormatInt(maximum, 10)) 52 | schemaDefinition.WriteString(")") 53 | 54 | return &schema.CustomValidator{ 55 | Imports: []code.Import{ 56 | StringValidatorCodeImport, 57 | }, 58 | SchemaDefinition: schemaDefinition.String(), 59 | } 60 | } 61 | 62 | // StringValidatorLengthBetween returns a custom validator mapped to the 63 | // stringvalidator package LengthBetween function. 64 | func StringValidatorLengthBetween(minimum, maximum int64) *schema.CustomValidator { 65 | var schemaDefinition strings.Builder 66 | 67 | schemaDefinition.WriteString(StringValidatorPackage) 68 | schemaDefinition.WriteString(".LengthBetween(") 69 | schemaDefinition.WriteString(strconv.FormatInt(minimum, 10)) 70 | schemaDefinition.WriteString(", ") 71 | schemaDefinition.WriteString(strconv.FormatInt(maximum, 10)) 72 | schemaDefinition.WriteString(")") 73 | 74 | return &schema.CustomValidator{ 75 | Imports: []code.Import{ 76 | StringValidatorCodeImport, 77 | }, 78 | SchemaDefinition: schemaDefinition.String(), 79 | } 80 | } 81 | 82 | // StringValidatorOneOf returns a custom validator mapped to the stringvalidator 83 | // package OneOf function. If the values are nil or empty, nil is returned. 84 | func StringValidatorOneOf(values []string) *schema.CustomValidator { 85 | if len(values) == 0 { 86 | return nil 87 | } 88 | 89 | var schemaDefinition strings.Builder 90 | 91 | schemaDefinition.WriteString(StringValidatorPackage) 92 | schemaDefinition.WriteString(".OneOf(\n") 93 | 94 | for _, value := range values { 95 | schemaDefinition.WriteString(strconv.Quote(value) + ",\n") 96 | } 97 | 98 | schemaDefinition.WriteString(")") 99 | 100 | return &schema.CustomValidator{ 101 | Imports: []code.Import{ 102 | StringValidatorCodeImport, 103 | }, 104 | SchemaDefinition: schemaDefinition.String(), 105 | } 106 | } 107 | 108 | // StringValidatorRegexMatches returns a custom validator mapped to the 109 | // stringvalidator package RegexMatches function. 110 | func StringValidatorRegexMatches(pattern, message string) *schema.CustomValidator { 111 | var schemaDefinition strings.Builder 112 | 113 | schemaDefinition.WriteString(StringValidatorPackage) 114 | schemaDefinition.WriteString(".RegexMatches(") 115 | schemaDefinition.WriteString("regexp.MustCompile(") 116 | schemaDefinition.WriteString(strconv.Quote(pattern)) 117 | schemaDefinition.WriteString(")") 118 | schemaDefinition.WriteString(", ") 119 | schemaDefinition.WriteString(strconv.Quote(message)) 120 | schemaDefinition.WriteString(")") 121 | 122 | return &schema.CustomValidator{ 123 | Imports: []code.Import{ 124 | { 125 | Path: "regexp", 126 | }, 127 | StringValidatorCodeImport, 128 | }, 129 | SchemaDefinition: schemaDefinition.String(), 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /internal/mapper/oas/string.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package oas 5 | 6 | import ( 7 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/mapper/attrmapper" 8 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/mapper/frameworkvalidators" 9 | "github.com/hashicorp/terraform-plugin-codegen-spec/datasource" 10 | "github.com/hashicorp/terraform-plugin-codegen-spec/provider" 11 | "github.com/hashicorp/terraform-plugin-codegen-spec/resource" 12 | "github.com/hashicorp/terraform-plugin-codegen-spec/schema" 13 | ) 14 | 15 | func (s *OASSchema) BuildStringResource(name string, computability schema.ComputedOptionalRequired) (attrmapper.ResourceAttribute, *SchemaError) { 16 | result := &attrmapper.ResourceStringAttribute{ 17 | Name: name, 18 | StringAttribute: resource.StringAttribute{ 19 | ComputedOptionalRequired: computability, 20 | DeprecationMessage: s.GetDeprecationMessage(), 21 | Description: s.GetDescription(), 22 | Sensitive: s.IsSensitive(), 23 | }, 24 | } 25 | 26 | if s.Schema.Default != nil { 27 | var staticDefault string 28 | if err := s.Schema.Default.Decode(&staticDefault); err == nil { 29 | if computability == schema.Required { 30 | result.ComputedOptionalRequired = schema.ComputedOptional 31 | } 32 | 33 | result.Default = &schema.StringDefault{ 34 | Static: &staticDefault, 35 | } 36 | } 37 | } 38 | 39 | if computability != schema.Computed { 40 | result.Validators = s.GetStringValidators() 41 | } 42 | 43 | return result, nil 44 | } 45 | 46 | func (s *OASSchema) BuildStringDataSource(name string, computability schema.ComputedOptionalRequired) (attrmapper.DataSourceAttribute, *SchemaError) { 47 | result := &attrmapper.DataSourceStringAttribute{ 48 | Name: name, 49 | StringAttribute: datasource.StringAttribute{ 50 | ComputedOptionalRequired: computability, 51 | DeprecationMessage: s.GetDeprecationMessage(), 52 | Description: s.GetDescription(), 53 | Sensitive: s.IsSensitive(), 54 | }, 55 | } 56 | 57 | if computability != schema.Computed { 58 | result.Validators = s.GetStringValidators() 59 | } 60 | 61 | return result, nil 62 | } 63 | 64 | func (s *OASSchema) BuildStringProvider(name string, optionalOrRequired schema.OptionalRequired) (attrmapper.ProviderAttribute, *SchemaError) { 65 | result := &attrmapper.ProviderStringAttribute{ 66 | Name: name, 67 | StringAttribute: provider.StringAttribute{ 68 | OptionalRequired: optionalOrRequired, 69 | DeprecationMessage: s.GetDeprecationMessage(), 70 | Description: s.GetDescription(), 71 | Sensitive: s.IsSensitive(), 72 | Validators: s.GetStringValidators(), 73 | }, 74 | } 75 | 76 | return result, nil 77 | } 78 | 79 | func (s *OASSchema) BuildStringElementType() (schema.ElementType, *SchemaError) { 80 | return schema.ElementType{ 81 | String: &schema.StringType{}, 82 | }, nil 83 | } 84 | 85 | func (s *OASSchema) GetStringValidators() []schema.StringValidator { 86 | var result []schema.StringValidator 87 | 88 | if len(s.Schema.Enum) > 0 { 89 | var enum []string 90 | 91 | for _, valueNode := range s.Schema.Enum { 92 | var value string 93 | if err := valueNode.Decode(&value); err != nil { 94 | // could consider error/panic here to notify developers 95 | continue 96 | } 97 | 98 | enum = append(enum, value) 99 | } 100 | 101 | customValidator := frameworkvalidators.StringValidatorOneOf(enum) 102 | 103 | if customValidator != nil { 104 | result = append(result, schema.StringValidator{ 105 | Custom: customValidator, 106 | }) 107 | } 108 | } 109 | 110 | minLength := s.Schema.MinLength 111 | maxLength := s.Schema.MaxLength 112 | 113 | if minLength != nil && maxLength != nil { 114 | result = append(result, schema.StringValidator{ 115 | Custom: frameworkvalidators.StringValidatorLengthBetween(*minLength, *maxLength), 116 | }) 117 | } else if minLength != nil { 118 | result = append(result, schema.StringValidator{ 119 | Custom: frameworkvalidators.StringValidatorLengthAtLeast(*minLength), 120 | }) 121 | } else if maxLength != nil { 122 | result = append(result, schema.StringValidator{ 123 | Custom: frameworkvalidators.StringValidatorLengthAtMost(*maxLength), 124 | }) 125 | } 126 | 127 | if s.Schema.Pattern != "" { 128 | result = append(result, schema.StringValidator{ 129 | // Friendly regex message could be added later via configuration or 130 | // custom annotation. 131 | Custom: frameworkvalidators.StringValidatorRegexMatches(s.Schema.Pattern, ""), 132 | }) 133 | } 134 | 135 | return result 136 | } 137 | -------------------------------------------------------------------------------- /internal/mapper/oas/number.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package oas 5 | 6 | import ( 7 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/mapper/attrmapper" 8 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/mapper/frameworkvalidators" 9 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/mapper/util" 10 | "github.com/hashicorp/terraform-plugin-codegen-spec/datasource" 11 | "github.com/hashicorp/terraform-plugin-codegen-spec/provider" 12 | "github.com/hashicorp/terraform-plugin-codegen-spec/resource" 13 | "github.com/hashicorp/terraform-plugin-codegen-spec/schema" 14 | ) 15 | 16 | func (s *OASSchema) BuildNumberResource(name string, computability schema.ComputedOptionalRequired) (attrmapper.ResourceAttribute, *SchemaError) { 17 | if s.Format == util.OAS_format_double || s.Format == util.OAS_format_float { 18 | result := &attrmapper.ResourceFloat64Attribute{ 19 | Name: name, 20 | Float64Attribute: resource.Float64Attribute{ 21 | ComputedOptionalRequired: computability, 22 | DeprecationMessage: s.GetDeprecationMessage(), 23 | Description: s.GetDescription(), 24 | }, 25 | } 26 | 27 | if s.Schema.Default != nil { 28 | var staticDefault float64 29 | if err := s.Schema.Default.Decode(&staticDefault); err == nil { 30 | if computability == schema.Required { 31 | result.ComputedOptionalRequired = schema.ComputedOptional 32 | } 33 | 34 | result.Default = &schema.Float64Default{ 35 | Static: &staticDefault, 36 | } 37 | } 38 | } 39 | 40 | if computability != schema.Computed { 41 | result.Validators = s.GetFloatValidators() 42 | } 43 | 44 | return result, nil 45 | } 46 | 47 | return &attrmapper.ResourceNumberAttribute{ 48 | Name: name, 49 | NumberAttribute: resource.NumberAttribute{ 50 | ComputedOptionalRequired: computability, 51 | DeprecationMessage: s.GetDeprecationMessage(), 52 | Description: s.GetDescription(), 53 | }, 54 | }, nil 55 | } 56 | 57 | func (s *OASSchema) BuildNumberDataSource(name string, computability schema.ComputedOptionalRequired) (attrmapper.DataSourceAttribute, *SchemaError) { 58 | if s.Format == util.OAS_format_double || s.Format == util.OAS_format_float { 59 | result := &attrmapper.DataSourceFloat64Attribute{ 60 | Name: name, 61 | Float64Attribute: datasource.Float64Attribute{ 62 | ComputedOptionalRequired: computability, 63 | DeprecationMessage: s.GetDeprecationMessage(), 64 | Description: s.GetDescription(), 65 | }, 66 | } 67 | 68 | if computability != schema.Computed { 69 | result.Validators = s.GetFloatValidators() 70 | } 71 | 72 | return result, nil 73 | } 74 | result := &attrmapper.DataSourceNumberAttribute{ 75 | Name: name, 76 | NumberAttribute: datasource.NumberAttribute{ 77 | ComputedOptionalRequired: computability, 78 | DeprecationMessage: s.GetDeprecationMessage(), 79 | Description: s.GetDescription(), 80 | }, 81 | } 82 | 83 | return result, nil 84 | } 85 | 86 | func (s *OASSchema) BuildNumberProvider(name string, optionalOrRequired schema.OptionalRequired) (attrmapper.ProviderAttribute, *SchemaError) { 87 | if s.Format == util.OAS_format_double || s.Format == util.OAS_format_float { 88 | result := &attrmapper.ProviderFloat64Attribute{ 89 | Name: name, 90 | Float64Attribute: provider.Float64Attribute{ 91 | OptionalRequired: optionalOrRequired, 92 | DeprecationMessage: s.GetDeprecationMessage(), 93 | Description: s.GetDescription(), 94 | Validators: s.GetFloatValidators(), 95 | }, 96 | } 97 | 98 | return result, nil 99 | } 100 | result := &attrmapper.ProviderNumberAttribute{ 101 | Name: name, 102 | NumberAttribute: provider.NumberAttribute{ 103 | OptionalRequired: optionalOrRequired, 104 | DeprecationMessage: s.GetDeprecationMessage(), 105 | Description: s.GetDescription(), 106 | }, 107 | } 108 | 109 | return result, nil 110 | } 111 | 112 | func (s *OASSchema) BuildNumberElementType() (schema.ElementType, *SchemaError) { 113 | if s.Format == util.OAS_format_double || s.Format == util.OAS_format_float { 114 | return schema.ElementType{ 115 | Float64: &schema.Float64Type{}, 116 | }, nil 117 | } 118 | 119 | return schema.ElementType{ 120 | Number: &schema.NumberType{}, 121 | }, nil 122 | } 123 | 124 | func (s *OASSchema) GetFloatValidators() []schema.Float64Validator { 125 | var result []schema.Float64Validator 126 | 127 | if len(s.Schema.Enum) > 0 { 128 | var enum []float64 129 | 130 | for _, valueNode := range s.Schema.Enum { 131 | var value float64 132 | if err := valueNode.Decode(&value); err != nil { 133 | // could consider error/panic here to notify developers 134 | continue 135 | } 136 | 137 | enum = append(enum, value) 138 | } 139 | 140 | customValidator := frameworkvalidators.Float64ValidatorOneOf(enum) 141 | 142 | if customValidator != nil { 143 | result = append(result, schema.Float64Validator{ 144 | Custom: customValidator, 145 | }) 146 | } 147 | } 148 | 149 | return result 150 | } 151 | -------------------------------------------------------------------------------- /internal/mapper/datasource_mapper.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package mapper 5 | 6 | import ( 7 | "fmt" 8 | "log/slog" 9 | 10 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/config" 11 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/explorer" 12 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/log" 13 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/mapper/attrmapper" 14 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/mapper/oas" 15 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/mapper/util" 16 | "github.com/hashicorp/terraform-plugin-codegen-spec/datasource" 17 | "github.com/hashicorp/terraform-plugin-codegen-spec/schema" 18 | ) 19 | 20 | var _ DataSourceMapper = dataSourceMapper{} 21 | 22 | type DataSourceMapper interface { 23 | MapToIR(*slog.Logger) ([]datasource.DataSource, error) 24 | } 25 | 26 | type dataSourceMapper struct { 27 | dataSources map[string]explorer.DataSource 28 | //nolint:unused // Might be useful later! 29 | cfg config.Config 30 | } 31 | 32 | func NewDataSourceMapper(dataSources map[string]explorer.DataSource, cfg config.Config) DataSourceMapper { 33 | return dataSourceMapper{ 34 | dataSources: dataSources, 35 | cfg: cfg, 36 | } 37 | } 38 | 39 | func (m dataSourceMapper) MapToIR(logger *slog.Logger) ([]datasource.DataSource, error) { 40 | dataSourceSchemas := []datasource.DataSource{} 41 | 42 | // Guarantee the order of processing 43 | dataSourceNames := util.SortedKeys(m.dataSources) 44 | for _, name := range dataSourceNames { 45 | dataSource := m.dataSources[name] 46 | dLogger := logger.With("data_source", name) 47 | 48 | schema, err := generateDataSourceSchema(dLogger, name, dataSource) 49 | if err != nil { 50 | log.WarnLogOnError(dLogger, err, "skipping data source schema mapping") 51 | continue 52 | } 53 | 54 | dataSourceSchemas = append(dataSourceSchemas, datasource.DataSource{ 55 | Name: name, 56 | Schema: schema, 57 | }) 58 | } 59 | 60 | return dataSourceSchemas, nil 61 | } 62 | 63 | func generateDataSourceSchema(logger *slog.Logger, name string, dataSource explorer.DataSource) (*datasource.Schema, error) { 64 | dataSourceSchema := &datasource.Schema{ 65 | Attributes: []datasource.Attribute{}, 66 | } 67 | 68 | // ******************** 69 | // READ Response Body (required) 70 | // ******************** 71 | logger.Debug("searching for read operation response body") 72 | 73 | schemaOpts := oas.SchemaOpts{ 74 | Ignores: dataSource.SchemaOptions.Ignores, 75 | } 76 | globalSchemaOpts := oas.GlobalSchemaOpts{ 77 | OverrideComputability: schema.Computed, 78 | } 79 | readResponseSchema, err := oas.BuildSchemaFromResponse(dataSource.ReadOp, schemaOpts, globalSchemaOpts) 80 | if err != nil { 81 | return nil, err 82 | } 83 | 84 | readResponseAttributes := attrmapper.DataSourceAttributes{} 85 | if readResponseSchema.Type == util.OAS_type_array { 86 | logger.Debug(fmt.Sprintf("response body is an array, building '%s' set attribute", name)) 87 | 88 | // API's generally don't guarantee ordering of results for collection/query responses, default mapping to set 89 | readResponseSchema.Format = util.TF_format_set 90 | 91 | collectionAttribute, schemaErr := readResponseSchema.BuildDataSourceAttribute(name, schema.Computed) 92 | if schemaErr != nil { 93 | return nil, schemaErr 94 | } 95 | 96 | readResponseAttributes = append(readResponseAttributes, collectionAttribute) 97 | } else { 98 | attributes, schemaErr := readResponseSchema.BuildDataSourceAttributes() 99 | if schemaErr != nil { 100 | return nil, schemaErr 101 | } 102 | 103 | readResponseAttributes = attributes 104 | } 105 | 106 | // **************** 107 | // READ Parameters (optional) 108 | // **************** 109 | readParameterAttributes := attrmapper.DataSourceAttributes{} 110 | for _, param := range dataSource.ReadOpParameters() { 111 | if param.In != util.OAS_param_path && param.In != util.OAS_param_query { 112 | continue 113 | } 114 | 115 | pLogger := logger.With("param", param.Name) 116 | schemaOpts := oas.SchemaOpts{ 117 | Ignores: dataSource.SchemaOptions.Ignores, 118 | OverrideDescription: param.Description, 119 | } 120 | 121 | s, schemaErr := oas.BuildSchema(param.Schema, schemaOpts, oas.GlobalSchemaOpts{}) 122 | if schemaErr != nil { 123 | log.WarnLogOnError(pLogger, schemaErr, "skipping mapping of read operation parameter") 124 | continue 125 | } 126 | 127 | computability := schema.ComputedOptional 128 | if param.Required != nil && *param.Required { 129 | computability = schema.Required 130 | } 131 | 132 | // Check for any aliases and replace the paramater name if found 133 | paramName := param.Name 134 | if aliasedName, ok := dataSource.SchemaOptions.AttributeOptions.Aliases[param.Name]; ok { 135 | pLogger = pLogger.With("param_alias", aliasedName) 136 | paramName = aliasedName 137 | } 138 | 139 | if s.IsPropertyIgnored(paramName) { 140 | continue 141 | } 142 | 143 | parameterAttribute, schemaErr := s.BuildDataSourceAttribute(paramName, computability) 144 | if schemaErr != nil { 145 | log.WarnLogOnError(pLogger, schemaErr, "skipping mapping of read operation parameter") 146 | continue 147 | } 148 | 149 | readParameterAttributes = append(readParameterAttributes, parameterAttribute) 150 | } 151 | 152 | // TODO: currently, no errors can be returned from merging, but in the future we should consider raising errors/warnings for unexpected scenarios, like type mismatches between attribute schemas 153 | dataSourceAttributes, _ := readParameterAttributes.Merge(readResponseAttributes) 154 | 155 | // TODO: handle error for overrides 156 | dataSourceAttributes, _ = dataSourceAttributes.ApplyOverrides(dataSource.SchemaOptions.AttributeOptions.Overrides) 157 | 158 | dataSourceSchema.Attributes = dataSourceAttributes.ToSpec() 159 | return dataSourceSchema, nil 160 | } 161 | -------------------------------------------------------------------------------- /internal/mapper/oas/oas_schema_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package oas_test 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/google/go-cmp/cmp" 10 | "github.com/pb33f/libopenapi/datamodel/high/base" 11 | 12 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/mapper/oas" 13 | ) 14 | 15 | func pointer[T any](value T) *T { 16 | return &value 17 | } 18 | 19 | func TestOASSchemaGetDeprecationMessage(t *testing.T) { 20 | t.Parallel() 21 | 22 | testCases := map[string]struct { 23 | schema oas.OASSchema 24 | expected *string 25 | }{ 26 | "deprecated-nil": { 27 | schema: oas.OASSchema{ 28 | Schema: &base.Schema{ 29 | Deprecated: nil, 30 | }, 31 | }, 32 | expected: nil, 33 | }, 34 | "deprecated-false": { 35 | schema: oas.OASSchema{ 36 | Schema: &base.Schema{ 37 | Deprecated: pointer(false), 38 | }, 39 | }, 40 | expected: nil, 41 | }, 42 | "deprecated-true": { 43 | schema: oas.OASSchema{ 44 | Schema: &base.Schema{ 45 | Deprecated: pointer(true), 46 | }, 47 | }, 48 | expected: pointer("This attribute is deprecated."), 49 | }, 50 | "deprecated-true-override-empty": { 51 | schema: oas.OASSchema{ 52 | Schema: &base.Schema{ 53 | Deprecated: pointer(true), 54 | }, 55 | SchemaOpts: oas.SchemaOpts{ 56 | OverrideDeprecationMessage: "", 57 | }, 58 | }, 59 | expected: pointer("This attribute is deprecated."), 60 | }, 61 | "deprecated-true-override-non-empty": { 62 | schema: oas.OASSchema{ 63 | Schema: &base.Schema{ 64 | Deprecated: pointer(true), 65 | }, 66 | SchemaOpts: oas.SchemaOpts{ 67 | OverrideDeprecationMessage: "Use test attribute instead.", 68 | }, 69 | }, 70 | expected: pointer("Use test attribute instead."), 71 | }, 72 | } 73 | 74 | for name, testCase := range testCases { 75 | 76 | t.Run(name, func(t *testing.T) { 77 | t.Parallel() 78 | 79 | got := testCase.schema.GetDeprecationMessage() 80 | 81 | if diff := cmp.Diff(got, testCase.expected); diff != "" { 82 | t.Errorf("unexpected difference: %s", diff) 83 | } 84 | }) 85 | } 86 | } 87 | 88 | func TestGetDescription_Override(t *testing.T) { 89 | t.Parallel() 90 | 91 | testCases := map[string]struct { 92 | schema oas.OASSchema 93 | expectedDescription string 94 | }{ 95 | "override description": { 96 | schema: oas.OASSchema{ 97 | SchemaOpts: oas.SchemaOpts{ 98 | OverrideDescription: "this is the correct description!", 99 | }, 100 | Schema: &base.Schema{ 101 | Description: "this shouldn't show up!", 102 | }, 103 | }, 104 | expectedDescription: "this is the correct description!", 105 | }, 106 | "no override of description": { 107 | schema: oas.OASSchema{ 108 | SchemaOpts: oas.SchemaOpts{}, 109 | Schema: &base.Schema{ 110 | Description: "this is the correct description!", 111 | }, 112 | }, 113 | expectedDescription: "this is the correct description!", 114 | }, 115 | } 116 | 117 | for name, testCase := range testCases { 118 | 119 | t.Run(name, func(t *testing.T) { 120 | t.Parallel() 121 | 122 | got := testCase.schema.GetDescription() 123 | if *got != testCase.expectedDescription { 124 | t.Fatalf("unexpected difference, got: %s, wanted: %s", *got, testCase.expectedDescription) 125 | } 126 | }) 127 | } 128 | } 129 | 130 | func TestIsPropertyIgnored(t *testing.T) { 131 | t.Parallel() 132 | 133 | testCases := map[string]struct { 134 | schema oas.OASSchema 135 | propertyName string 136 | want bool 137 | }{ 138 | "propery is ignored": { 139 | propertyName: "ignored_prop", 140 | schema: oas.OASSchema{ 141 | SchemaOpts: oas.SchemaOpts{ 142 | Ignores: []string{ 143 | "ignored_prop", 144 | }, 145 | }, 146 | }, 147 | want: true, 148 | }, 149 | "propery not ignored": { 150 | propertyName: "prop", 151 | schema: oas.OASSchema{ 152 | SchemaOpts: oas.SchemaOpts{ 153 | Ignores: []string{ 154 | "ignored_prop", 155 | }, 156 | }, 157 | }, 158 | want: false, 159 | }, 160 | "nested propery is not ignored": { 161 | propertyName: "prop", 162 | schema: oas.OASSchema{ 163 | SchemaOpts: oas.SchemaOpts{ 164 | Ignores: []string{ 165 | "prop.ignored_prop", 166 | }, 167 | }, 168 | }, 169 | want: false, 170 | }, 171 | } 172 | 173 | for name, testCase := range testCases { 174 | 175 | t.Run(name, func(t *testing.T) { 176 | t.Parallel() 177 | 178 | got := testCase.schema.IsPropertyIgnored(testCase.propertyName) 179 | if got != testCase.want { 180 | t.Fatalf("unexpected difference, got: %t, wanted: %t", got, testCase.want) 181 | } 182 | }) 183 | } 184 | } 185 | 186 | func TestGetIgnoresForNested(t *testing.T) { 187 | t.Parallel() 188 | 189 | testCases := map[string]struct { 190 | schema oas.OASSchema 191 | propertyName string 192 | want []string 193 | }{ 194 | "ignores are empty": { 195 | propertyName: "prop", 196 | schema: oas.OASSchema{ 197 | SchemaOpts: oas.SchemaOpts{ 198 | Ignores: make([]string, 0), 199 | }, 200 | }, 201 | want: make([]string, 0), 202 | }, 203 | "ignores are invalid": { 204 | propertyName: "prop", 205 | schema: oas.OASSchema{ 206 | SchemaOpts: oas.SchemaOpts{ 207 | Ignores: []string{ 208 | ".prop", 209 | "prop.", 210 | ".", 211 | "", 212 | }, 213 | }, 214 | }, 215 | want: make([]string, 0), 216 | }, 217 | "nested ignores exist": { 218 | propertyName: "prop", 219 | schema: oas.OASSchema{ 220 | SchemaOpts: oas.SchemaOpts{ 221 | Ignores: []string{ 222 | "prop.ignore_me_1", 223 | "not_me.prop", 224 | "prop.nested.ignore_me_2", 225 | "prop.ignore_me_3", 226 | }, 227 | }, 228 | }, 229 | want: []string{ 230 | "ignore_me_1", 231 | "nested.ignore_me_2", 232 | "ignore_me_3", 233 | }, 234 | }, 235 | } 236 | 237 | for name, testCase := range testCases { 238 | 239 | t.Run(name, func(t *testing.T) { 240 | t.Parallel() 241 | 242 | got := testCase.schema.GetIgnoresForNested(testCase.propertyName) 243 | if diff := cmp.Diff(got, testCase.want); diff != "" { 244 | t.Errorf("unexpected difference: %s", diff) 245 | } 246 | }) 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /internal/mapper/frameworkvalidators/stringvalidator_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package frameworkvalidators_test 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/google/go-cmp/cmp" 10 | "github.com/hashicorp/terraform-plugin-codegen-spec/code" 11 | "github.com/hashicorp/terraform-plugin-codegen-spec/schema" 12 | 13 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/mapper/frameworkvalidators" 14 | ) 15 | 16 | func TestStringValidatorLengthAtLeast(t *testing.T) { 17 | t.Parallel() 18 | 19 | testCases := map[string]struct { 20 | min int64 21 | expected *schema.CustomValidator 22 | }{ 23 | "test": { 24 | min: 123, 25 | expected: &schema.CustomValidator{ 26 | Imports: []code.Import{ 27 | { 28 | Path: "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator", 29 | }, 30 | }, 31 | SchemaDefinition: "stringvalidator.LengthAtLeast(123)", 32 | }, 33 | }, 34 | } 35 | 36 | for name, testCase := range testCases { 37 | 38 | t.Run(name, func(t *testing.T) { 39 | t.Parallel() 40 | 41 | got := frameworkvalidators.StringValidatorLengthAtLeast(testCase.min) 42 | 43 | if diff := cmp.Diff(got, testCase.expected); diff != "" { 44 | t.Errorf("unexpected difference: %s", diff) 45 | } 46 | }) 47 | } 48 | } 49 | 50 | func TestStringValidatorLengthAtMost(t *testing.T) { 51 | t.Parallel() 52 | 53 | testCases := map[string]struct { 54 | max int64 55 | expected *schema.CustomValidator 56 | }{ 57 | "test": { 58 | max: 123, 59 | expected: &schema.CustomValidator{ 60 | Imports: []code.Import{ 61 | { 62 | Path: "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator", 63 | }, 64 | }, 65 | SchemaDefinition: "stringvalidator.LengthAtMost(123)", 66 | }, 67 | }, 68 | } 69 | 70 | for name, testCase := range testCases { 71 | 72 | t.Run(name, func(t *testing.T) { 73 | t.Parallel() 74 | 75 | got := frameworkvalidators.StringValidatorLengthAtMost(testCase.max) 76 | 77 | if diff := cmp.Diff(got, testCase.expected); diff != "" { 78 | t.Errorf("unexpected difference: %s", diff) 79 | } 80 | }) 81 | } 82 | } 83 | 84 | func TestStringValidatorLengthBetween(t *testing.T) { 85 | t.Parallel() 86 | 87 | testCases := map[string]struct { 88 | min int64 89 | max int64 90 | expected *schema.CustomValidator 91 | }{ 92 | "test": { 93 | min: 123, 94 | max: 456, 95 | expected: &schema.CustomValidator{ 96 | Imports: []code.Import{ 97 | { 98 | Path: "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator", 99 | }, 100 | }, 101 | SchemaDefinition: "stringvalidator.LengthBetween(123, 456)", 102 | }, 103 | }, 104 | } 105 | 106 | for name, testCase := range testCases { 107 | 108 | t.Run(name, func(t *testing.T) { 109 | t.Parallel() 110 | 111 | got := frameworkvalidators.StringValidatorLengthBetween(testCase.min, testCase.max) 112 | 113 | if diff := cmp.Diff(got, testCase.expected); diff != "" { 114 | t.Errorf("unexpected difference: %s", diff) 115 | } 116 | }) 117 | } 118 | } 119 | 120 | func TestStringValidatorOneOf(t *testing.T) { 121 | t.Parallel() 122 | 123 | testCases := map[string]struct { 124 | values []string 125 | expected *schema.CustomValidator 126 | }{ 127 | "nil": { 128 | values: nil, 129 | expected: nil, 130 | }, 131 | "empty": { 132 | values: []string{}, 133 | expected: nil, 134 | }, 135 | "one": { 136 | values: []string{"one"}, 137 | expected: &schema.CustomValidator{ 138 | Imports: []code.Import{ 139 | { 140 | Path: "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator", 141 | }, 142 | }, 143 | SchemaDefinition: "stringvalidator.OneOf(\n\"one\",\n)", 144 | }, 145 | }, 146 | "multiple": { 147 | values: []string{"one", "two"}, 148 | expected: &schema.CustomValidator{ 149 | Imports: []code.Import{ 150 | { 151 | Path: "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator", 152 | }, 153 | }, 154 | SchemaDefinition: "stringvalidator.OneOf(\n\"one\",\n\"two\",\n)", 155 | }, 156 | }, 157 | } 158 | 159 | for name, testCase := range testCases { 160 | 161 | t.Run(name, func(t *testing.T) { 162 | t.Parallel() 163 | 164 | got := frameworkvalidators.StringValidatorOneOf(testCase.values) 165 | 166 | if diff := cmp.Diff(got, testCase.expected); diff != "" { 167 | t.Errorf("unexpected difference: %s", diff) 168 | } 169 | }) 170 | } 171 | } 172 | 173 | func TestStringValidatorRegexMatches(t *testing.T) { 174 | t.Parallel() 175 | 176 | testCases := map[string]struct { 177 | pattern string 178 | message string 179 | expected *schema.CustomValidator 180 | }{ 181 | "empty pattern": { 182 | pattern: "", 183 | message: "", 184 | expected: &schema.CustomValidator{ 185 | Imports: []code.Import{ 186 | { 187 | Path: "regexp", 188 | }, 189 | { 190 | Path: "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator", 191 | }, 192 | }, 193 | SchemaDefinition: "stringvalidator.RegexMatches(regexp.MustCompile(\"\"), \"\")", 194 | }, 195 | }, 196 | "pattern": { 197 | pattern: "^[a-zA-Z0-9]*$", 198 | message: "", 199 | expected: &schema.CustomValidator{ 200 | Imports: []code.Import{ 201 | { 202 | Path: "regexp", 203 | }, 204 | { 205 | Path: "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator", 206 | }, 207 | }, 208 | SchemaDefinition: "stringvalidator.RegexMatches(regexp.MustCompile(\"^[a-zA-Z0-9]*$\"), \"\")", 209 | }, 210 | }, 211 | "message": { 212 | pattern: "^[a-zA-Z0-9]*$", 213 | message: "must contain alphanumeric characters", 214 | expected: &schema.CustomValidator{ 215 | Imports: []code.Import{ 216 | { 217 | Path: "regexp", 218 | }, 219 | { 220 | Path: "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator", 221 | }, 222 | }, 223 | SchemaDefinition: "stringvalidator.RegexMatches(regexp.MustCompile(\"^[a-zA-Z0-9]*$\"), \"must contain alphanumeric characters\")", 224 | }, 225 | }, 226 | } 227 | 228 | for name, testCase := range testCases { 229 | 230 | t.Run(name, func(t *testing.T) { 231 | t.Parallel() 232 | 233 | got := frameworkvalidators.StringValidatorRegexMatches(testCase.pattern, testCase.message) 234 | 235 | if diff := cmp.Diff(got, testCase.expected); diff != "" { 236 | t.Errorf("unexpected difference: %s", diff) 237 | } 238 | }) 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /internal/mapper/oas/attribute.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package oas 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | 10 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/mapper/attrmapper" 11 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/mapper/util" 12 | "github.com/hashicorp/terraform-plugin-codegen-spec/schema" 13 | "github.com/pb33f/libopenapi/orderedmap" 14 | ) 15 | 16 | func (s *OASSchema) BuildResourceAttributes() (attrmapper.ResourceAttributes, *SchemaError) { 17 | objectAttributes := attrmapper.ResourceAttributes{} 18 | 19 | sortedProperties := orderedmap.SortAlpha(s.Schema.Properties) 20 | for pair := range orderedmap.Iterate(context.TODO(), sortedProperties) { 21 | name := pair.Key() 22 | 23 | if s.IsPropertyIgnored(name) { 24 | continue 25 | } 26 | 27 | pProxy := pair.Value() 28 | schemaOpts := SchemaOpts{ 29 | Ignores: s.GetIgnoresForNested(name), 30 | } 31 | 32 | pSchema, err := BuildSchema(pProxy, schemaOpts, s.GlobalSchemaOpts) 33 | if err != nil { 34 | return nil, s.NestSchemaError(err, name) 35 | } 36 | 37 | attribute, err := pSchema.BuildResourceAttribute(name, s.GetComputability(name)) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | objectAttributes = append(objectAttributes, attribute) 43 | } 44 | 45 | return objectAttributes, nil 46 | } 47 | 48 | func (s *OASSchema) BuildResourceAttribute(name string, computability schema.ComputedOptionalRequired) (attrmapper.ResourceAttribute, *SchemaError) { 49 | if util.TerraformIdentifier(name) == "" { 50 | return nil, s.SchemaErrorFromProperty(fmt.Errorf("'%s' cannot be converted to a valid Terraform identifier", name), name) 51 | } 52 | 53 | switch s.Type { 54 | case util.OAS_type_string: 55 | return s.BuildStringResource(name, computability) 56 | case util.OAS_type_integer: 57 | return s.BuildIntegerResource(name, computability) 58 | case util.OAS_type_number: 59 | return s.BuildNumberResource(name, computability) 60 | case util.OAS_type_boolean: 61 | return s.BuildBoolResource(name, computability) 62 | case util.OAS_type_array: 63 | return s.BuildCollectionResource(name, computability) 64 | case util.OAS_type_object: 65 | if s.IsMap() { 66 | return s.BuildMapResource(name, computability) 67 | } 68 | return s.BuildSingleNestedResource(name, computability) 69 | default: 70 | return nil, s.SchemaErrorFromProperty(fmt.Errorf("invalid schema type '%s'", s.Type), name) 71 | } 72 | } 73 | 74 | func (s *OASSchema) BuildDataSourceAttributes() (attrmapper.DataSourceAttributes, *SchemaError) { 75 | objectAttributes := attrmapper.DataSourceAttributes{} 76 | 77 | sortedProperties := orderedmap.SortAlpha(s.Schema.Properties) 78 | for pair := range orderedmap.Iterate(context.TODO(), sortedProperties) { 79 | name := pair.Key() 80 | 81 | if s.IsPropertyIgnored(name) { 82 | continue 83 | } 84 | 85 | pProxy := pair.Value() 86 | schemaOpts := SchemaOpts{ 87 | Ignores: s.GetIgnoresForNested(name), 88 | } 89 | 90 | pSchema, err := BuildSchema(pProxy, schemaOpts, s.GlobalSchemaOpts) 91 | if err != nil { 92 | return nil, s.NestSchemaError(err, name) 93 | } 94 | 95 | attribute, err := pSchema.BuildDataSourceAttribute(name, s.GetComputability(name)) 96 | if err != nil { 97 | return nil, err 98 | } 99 | 100 | objectAttributes = append(objectAttributes, attribute) 101 | } 102 | 103 | return objectAttributes, nil 104 | } 105 | 106 | func (s *OASSchema) BuildDataSourceAttribute(name string, computability schema.ComputedOptionalRequired) (attrmapper.DataSourceAttribute, *SchemaError) { 107 | if util.TerraformIdentifier(name) == "" { 108 | return nil, s.SchemaErrorFromProperty(fmt.Errorf("'%s' cannot be converted to a valid Terraform identifier", name), name) 109 | } 110 | 111 | switch s.Type { 112 | case util.OAS_type_string: 113 | return s.BuildStringDataSource(name, computability) 114 | case util.OAS_type_integer: 115 | return s.BuildIntegerDataSource(name, computability) 116 | case util.OAS_type_number: 117 | return s.BuildNumberDataSource(name, computability) 118 | case util.OAS_type_boolean: 119 | return s.BuildBoolDataSource(name, computability) 120 | case util.OAS_type_array: 121 | return s.BuildCollectionDataSource(name, computability) 122 | case util.OAS_type_object: 123 | if s.IsMap() { 124 | return s.BuildMapDataSource(name, computability) 125 | } 126 | return s.BuildSingleNestedDataSource(name, computability) 127 | default: 128 | return nil, s.SchemaErrorFromProperty(fmt.Errorf("invalid schema type '%s'", s.Type), name) 129 | } 130 | } 131 | 132 | func (s *OASSchema) BuildProviderAttributes() (attrmapper.ProviderAttributes, *SchemaError) { 133 | objectAttributes := attrmapper.ProviderAttributes{} 134 | 135 | sortedProperties := orderedmap.SortAlpha(s.Schema.Properties) 136 | for pair := range orderedmap.Iterate(context.TODO(), sortedProperties) { 137 | name := pair.Key() 138 | 139 | if s.IsPropertyIgnored(name) { 140 | continue 141 | } 142 | 143 | pProxy := pair.Value() 144 | schemaOpts := SchemaOpts{ 145 | Ignores: s.GetIgnoresForNested(name), 146 | } 147 | 148 | pSchema, err := BuildSchema(pProxy, schemaOpts, s.GlobalSchemaOpts) 149 | if err != nil { 150 | return nil, s.NestSchemaError(err, name) 151 | } 152 | 153 | attribute, err := pSchema.BuildProviderAttribute(name, s.GetOptionalOrRequired(name)) 154 | if err != nil { 155 | return nil, err 156 | } 157 | 158 | objectAttributes = append(objectAttributes, attribute) 159 | } 160 | 161 | return objectAttributes, nil 162 | } 163 | 164 | func (s *OASSchema) BuildProviderAttribute(name string, optionalOrRequired schema.OptionalRequired) (attrmapper.ProviderAttribute, *SchemaError) { 165 | if util.TerraformIdentifier(name) == "" { 166 | return nil, s.SchemaErrorFromProperty(fmt.Errorf("'%s' cannot be converted to a valid Terraform identifier", name), name) 167 | } 168 | 169 | switch s.Type { 170 | case util.OAS_type_string: 171 | return s.BuildStringProvider(name, optionalOrRequired) 172 | case util.OAS_type_integer: 173 | return s.BuildIntegerProvider(name, optionalOrRequired) 174 | case util.OAS_type_number: 175 | return s.BuildNumberProvider(name, optionalOrRequired) 176 | case util.OAS_type_boolean: 177 | return s.BuildBoolProvider(name, optionalOrRequired) 178 | case util.OAS_type_array: 179 | return s.BuildCollectionProvider(name, optionalOrRequired) 180 | case util.OAS_type_object: 181 | if s.IsMap() { 182 | return s.BuildMapProvider(name, optionalOrRequired) 183 | } 184 | return s.BuildSingleNestedProvider(name, optionalOrRequired) 185 | default: 186 | return nil, s.SchemaErrorFromProperty(fmt.Errorf("invalid schema type '%s'", s.Type), name) 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /internal/mapper/oas/oas_schema.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package oas 5 | 6 | import ( 7 | "context" 8 | "strings" 9 | 10 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/mapper/util" 11 | "github.com/hashicorp/terraform-plugin-codegen-spec/schema" 12 | 13 | "github.com/pb33f/libopenapi/datamodel/high/base" 14 | "github.com/pb33f/libopenapi/orderedmap" 15 | ) 16 | 17 | type OASSchema struct { 18 | Type string 19 | Format string 20 | Schema *base.Schema 21 | 22 | GlobalSchemaOpts GlobalSchemaOpts 23 | SchemaOpts SchemaOpts 24 | } 25 | 26 | // GlobalSchemaOpts is passed recursively through built OASSchema structs. This is used for options that need to control 27 | // the entire of a schema and it's potential nested schemas, like overriding computability. (Required, Optional, Computed) 28 | type GlobalSchemaOpts struct { 29 | // OverrideComputability will set all attribute and nested attribute `ComputedOptionalRequired` fields 30 | // to this value. This ensures that an optional attribute from a higher precedence operation, such as a 31 | // create request for a resource, does not become required from a lower precedence operation, such as an 32 | // read response for a resource. 33 | OverrideComputability schema.ComputedOptionalRequired 34 | } 35 | 36 | // SchemaOpts is NOT passed recursively through built OASSchema structs, and will only be available to the top level schema. This is used 37 | // for options that need to control just the top level schema, like overriding descriptions. 38 | type SchemaOpts struct { 39 | // Ignores contains all potentially relevant ignores for a schema and it's potential nested schemas 40 | Ignores []string 41 | 42 | // OverrideDeprecationMessage will set the attribute deprecation message to 43 | // this field if populated, otherwise the attribute deprecation message will 44 | // be set to a default "This attribute is deprecated." message when the 45 | // deprecated property is enabled. 46 | OverrideDeprecationMessage string 47 | 48 | // OverrideDescription will set the attribute description to this field if populated, otherwise the attribute description 49 | // will be set to the description field of the `schema`. 50 | OverrideDescription string 51 | } 52 | 53 | // IsMap checks the `additionalProperties` field to determine if a map type is appropriate (refer to [JSON Schema - additionalProperties]). 54 | // 55 | // [JSON Schema - additionalProperties]: https://json-schema.org/understanding-json-schema/reference/object.html#additional-properties 56 | func (s *OASSchema) IsMap() bool { 57 | return s.Schema.AdditionalProperties != nil && s.Schema.AdditionalProperties.IsA() 58 | } 59 | 60 | // SchemaErrorFromProperty is a helper function for creating an SchemaError struct for a property. 61 | func (s *OASSchema) SchemaErrorFromProperty(err error, propName string) *SchemaError { 62 | return NewSchemaError(err, s.getPropertyLineNumber(propName), propName) 63 | } 64 | 65 | // NestSchemaError is a helper function for creating a nested SchemaError struct for a property. 66 | func (s *OASSchema) NestSchemaError(err *SchemaError, propName string) *SchemaError { 67 | return err.NestedSchemaError(propName, s.getPropertyLineNumber(propName)) 68 | } 69 | 70 | // getPropertyLineNumber looks in the low-level schema instance for line information. Returns 0 if not found. 71 | func (s *OASSchema) getPropertyLineNumber(propName string) int { 72 | low := s.Schema.GoLow() 73 | if low == nil { 74 | return 0 75 | } 76 | 77 | // Check property nodes first for a line number 78 | for pair := range orderedmap.Iterate(context.TODO(), low.Properties.Value) { 79 | if pair.Key().Value == propName { 80 | return pair.Value().NodeLineNumber() 81 | } 82 | } 83 | 84 | // If it's not found in properties, default to the line number from the parent node 85 | if low.ParentProxy != nil && low.ParentProxy.GetValueNode() != nil { 86 | return low.ParentProxy.GetValueNode().Line 87 | } 88 | 89 | return 0 90 | } 91 | 92 | // GetDeprecationMessage returns a deprecation message if the deprecated 93 | // property is enabled. It defaults the message to "This attribute is 94 | // deprecated" unless the SchemaOpts.OverrideDeprecationMessage is set. 95 | func (s *OASSchema) GetDeprecationMessage() *string { 96 | if s.Schema.Deprecated == nil || !(*s.Schema.Deprecated) { 97 | return nil 98 | } 99 | 100 | if s.SchemaOpts.OverrideDeprecationMessage != "" { 101 | return &s.SchemaOpts.OverrideDeprecationMessage 102 | } 103 | 104 | deprecationMessage := "This attribute is deprecated." 105 | 106 | return &deprecationMessage 107 | } 108 | 109 | func (s *OASSchema) GetDescription() *string { 110 | if s.SchemaOpts.OverrideDescription != "" { 111 | return &s.SchemaOpts.OverrideDescription 112 | } 113 | 114 | if s.Schema.Description == "" { 115 | return nil 116 | } 117 | 118 | return &s.Schema.Description 119 | } 120 | 121 | func (s *OASSchema) IsSensitive() *bool { 122 | isSensitive := s.Format == util.OAS_format_password 123 | 124 | if !isSensitive { 125 | return nil 126 | } 127 | 128 | return &isSensitive 129 | } 130 | 131 | // TODO: Figure out a better way to handle computability, since it differs with provider vs. datasource/resource 132 | func (s *OASSchema) GetComputability(name string) schema.ComputedOptionalRequired { 133 | if s.GlobalSchemaOpts.OverrideComputability != "" { 134 | return s.GlobalSchemaOpts.OverrideComputability 135 | } 136 | 137 | for _, prop := range s.Schema.Required { 138 | if name == prop { 139 | return schema.Required 140 | } 141 | } 142 | 143 | return schema.ComputedOptional 144 | } 145 | 146 | func (s *OASSchema) GetOptionalOrRequired(name string) schema.OptionalRequired { 147 | for _, prop := range s.Schema.Required { 148 | if name == prop { 149 | return schema.Required 150 | } 151 | } 152 | 153 | return schema.Optional 154 | } 155 | 156 | // IsPropertyIgnored checks if a property should be ignored 157 | func (s *OASSchema) IsPropertyIgnored(name string) bool { 158 | for _, ignore := range s.SchemaOpts.Ignores { 159 | if name == ignore { 160 | return true 161 | } 162 | } 163 | return false 164 | } 165 | 166 | // GetIgnoresForNested is a helper function that will return all nested ignores for a property. If no ignores 167 | // or nested ignores are found, returns an empty string slice. 168 | func (s *OASSchema) GetIgnoresForNested(name string) []string { 169 | newIgnores := make([]string, 0) 170 | 171 | for _, ignore := range s.SchemaOpts.Ignores { 172 | ignoreParts := strings.Split(ignore, ".") 173 | 174 | if len(ignoreParts) > 1 && name == ignoreParts[0] { 175 | newIgnore := strings.Join(ignoreParts[1:], ".") 176 | 177 | if newIgnore != "" { 178 | newIgnores = append(newIgnores, newIgnore) 179 | } 180 | } 181 | } 182 | 183 | return newIgnores 184 | } 185 | -------------------------------------------------------------------------------- /internal/mapper/resource_mapper.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package mapper 5 | 6 | import ( 7 | "errors" 8 | "log/slog" 9 | 10 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/config" 11 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/explorer" 12 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/log" 13 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/mapper/attrmapper" 14 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/mapper/oas" 15 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/mapper/util" 16 | "github.com/hashicorp/terraform-plugin-codegen-spec/resource" 17 | "github.com/hashicorp/terraform-plugin-codegen-spec/schema" 18 | ) 19 | 20 | var _ ResourceMapper = resourceMapper{} 21 | 22 | type ResourceMapper interface { 23 | MapToIR(*slog.Logger) ([]resource.Resource, error) 24 | } 25 | 26 | type resourceMapper struct { 27 | resources map[string]explorer.Resource 28 | //nolint:unused // Might be useful later! 29 | cfg config.Config 30 | } 31 | 32 | func NewResourceMapper(resources map[string]explorer.Resource, cfg config.Config) ResourceMapper { 33 | return resourceMapper{ 34 | resources: resources, 35 | cfg: cfg, 36 | } 37 | } 38 | 39 | func (m resourceMapper) MapToIR(logger *slog.Logger) ([]resource.Resource, error) { 40 | resourceSchemas := []resource.Resource{} 41 | 42 | // Guarantee the order of processing 43 | resourceNames := util.SortedKeys(m.resources) 44 | for _, name := range resourceNames { 45 | explorerResource := m.resources[name] 46 | rLogger := logger.With("resource", name) 47 | 48 | schema, err := generateResourceSchema(rLogger, explorerResource) 49 | if err != nil { 50 | log.WarnLogOnError(rLogger, err, "skipping resource schema mapping") 51 | continue 52 | } 53 | 54 | resourceSchemas = append(resourceSchemas, resource.Resource{ 55 | Name: name, 56 | Schema: schema, 57 | }) 58 | } 59 | 60 | return resourceSchemas, nil 61 | } 62 | 63 | func generateResourceSchema(logger *slog.Logger, explorerResource explorer.Resource) (*resource.Schema, error) { 64 | resourceSchema := &resource.Schema{ 65 | Attributes: []resource.Attribute{}, 66 | } 67 | 68 | // ******************** 69 | // Create Request Body (required) 70 | // ******************** 71 | logger.Debug("searching for create operation request body") 72 | 73 | schemaOpts := oas.SchemaOpts{ 74 | Ignores: explorerResource.SchemaOptions.Ignores, 75 | } 76 | createRequestSchema, err := oas.BuildSchemaFromRequest(explorerResource.CreateOp, schemaOpts, oas.GlobalSchemaOpts{}) 77 | if err != nil { 78 | return nil, err 79 | } 80 | createRequestAttributes, schemaErr := createRequestSchema.BuildResourceAttributes() 81 | if schemaErr != nil { 82 | return nil, schemaErr 83 | } 84 | 85 | // ********************* 86 | // Create Response Body (optional) 87 | // ********************* 88 | logger.Debug("searching for create operation response body") 89 | 90 | createResponseAttributes := attrmapper.ResourceAttributes{} 91 | schemaOpts = oas.SchemaOpts{ 92 | Ignores: explorerResource.SchemaOptions.Ignores, 93 | } 94 | globalSchemaOpts := oas.GlobalSchemaOpts{ 95 | OverrideComputability: schema.Computed, 96 | } 97 | createResponseSchema, err := oas.BuildSchemaFromResponse(explorerResource.CreateOp, schemaOpts, globalSchemaOpts) 98 | if err != nil { 99 | if errors.Is(err, oas.ErrSchemaNotFound) { 100 | // Demote log to INFO if there was no schema found 101 | logger.Info("skipping mapping of create operation response body", "err", err) 102 | } else { 103 | logger.Warn("skipping mapping of create operation response body", "err", err) 104 | } 105 | } else { 106 | createResponseAttributes, schemaErr = createResponseSchema.BuildResourceAttributes() 107 | if schemaErr != nil { 108 | log.WarnLogOnError(logger, schemaErr, "skipping mapping of create operation response body") 109 | } 110 | } 111 | 112 | // ******************* 113 | // READ Response Body (optional) 114 | // ******************* 115 | logger.Debug("searching for read operation response body") 116 | 117 | readResponseAttributes := attrmapper.ResourceAttributes{} 118 | 119 | schemaOpts = oas.SchemaOpts{ 120 | Ignores: explorerResource.SchemaOptions.Ignores, 121 | } 122 | globalSchemaOpts = oas.GlobalSchemaOpts{ 123 | OverrideComputability: schema.Computed, 124 | } 125 | readResponseSchema, err := oas.BuildSchemaFromResponse(explorerResource.ReadOp, schemaOpts, globalSchemaOpts) 126 | if err != nil { 127 | if errors.Is(err, oas.ErrSchemaNotFound) { 128 | // Demote log to INFO if there was no schema found 129 | logger.Info("skipping mapping of read operation response body", "err", err) 130 | } else { 131 | logger.Warn("skipping mapping of read operation response body", "err", err) 132 | } 133 | } else { 134 | readResponseAttributes, schemaErr = readResponseSchema.BuildResourceAttributes() 135 | if schemaErr != nil { 136 | log.WarnLogOnError(logger, schemaErr, "skipping mapping of read operation response body") 137 | } 138 | } 139 | 140 | // **************** 141 | // READ Parameters (optional) 142 | // **************** 143 | readParameterAttributes := attrmapper.ResourceAttributes{} 144 | for _, param := range explorerResource.ReadOpParameters() { 145 | if param.In != util.OAS_param_path && param.In != util.OAS_param_query { 146 | continue 147 | } 148 | 149 | pLogger := logger.With("param", param.Name) 150 | schemaOpts := oas.SchemaOpts{ 151 | Ignores: explorerResource.SchemaOptions.Ignores, 152 | OverrideDescription: param.Description, 153 | } 154 | globalSchemaOpts := oas.GlobalSchemaOpts{OverrideComputability: schema.ComputedOptional} 155 | 156 | s, schemaErr := oas.BuildSchema(param.Schema, schemaOpts, globalSchemaOpts) 157 | if schemaErr != nil { 158 | log.WarnLogOnError(pLogger, schemaErr, "skipping mapping of read operation parameter") 159 | continue 160 | } 161 | 162 | // Check for any aliases and replace the paramater name if found 163 | paramName := param.Name 164 | if aliasedName, ok := explorerResource.SchemaOptions.AttributeOptions.Aliases[param.Name]; ok { 165 | pLogger = pLogger.With("param_alias", aliasedName) 166 | paramName = aliasedName 167 | } 168 | 169 | if s.IsPropertyIgnored(paramName) { 170 | continue 171 | } 172 | 173 | parameterAttribute, schemaErr := s.BuildResourceAttribute(paramName, schema.ComputedOptional) 174 | if schemaErr != nil { 175 | log.WarnLogOnError(pLogger, schemaErr, "skipping mapping of read operation parameter") 176 | continue 177 | } 178 | 179 | readParameterAttributes = append(readParameterAttributes, parameterAttribute) 180 | } 181 | 182 | // TODO: currently, no errors can be returned from merging, but in the future we should consider raising errors/warnings for unexpected scenarios, like type mismatches between attribute schemas 183 | resourceAttributes, _ := createRequestAttributes.Merge(createResponseAttributes, readResponseAttributes, readParameterAttributes) 184 | 185 | // TODO: handle error for overrides 186 | resourceAttributes, _ = resourceAttributes.ApplyOverrides(explorerResource.SchemaOptions.AttributeOptions.Overrides) 187 | 188 | resourceSchema.Attributes = resourceAttributes.ToSpec() 189 | return resourceSchema, nil 190 | } 191 | -------------------------------------------------------------------------------- /internal/explorer/guesstimator_explorer_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package explorer_test 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/hashicorp/terraform-plugin-codegen-openapi/internal/explorer" 10 | 11 | high "github.com/pb33f/libopenapi/datamodel/high/v3" 12 | "github.com/pb33f/libopenapi/orderedmap" 13 | ) 14 | 15 | func Test_GuesstimatorExplorer_FindResources(t *testing.T) { 16 | t.Parallel() 17 | 18 | testCases := map[string]struct { 19 | pathItems *orderedmap.Map[string, *high.PathItem] 20 | expectedResources []string 21 | }{ 22 | "valid flat resource combo": { 23 | pathItems: orderedmap.ToOrderedMap(map[string]*high.PathItem{ 24 | "/resources": { 25 | Post: &high.Operation{}, 26 | }, 27 | "/resources/{resource_id}": { 28 | Get: &high.Operation{}, 29 | Delete: &high.Operation{}, 30 | }, 31 | }), 32 | expectedResources: []string{"resources"}, 33 | }, 34 | "valid nested resource combo": { 35 | pathItems: orderedmap.ToOrderedMap(map[string]*high.PathItem{ 36 | "/verycool/verynice/resources": { 37 | Post: &high.Operation{}, 38 | }, 39 | "/verycool/verynice/resources/{resource_id}": { 40 | Get: &high.Operation{}, 41 | Delete: &high.Operation{}, 42 | }, 43 | }), 44 | expectedResources: []string{"verycool_verynice_resources"}, 45 | }, 46 | "valid nested with id resource combo": { 47 | pathItems: orderedmap.ToOrderedMap(map[string]*high.PathItem{ 48 | "/verycool/{id}/verynice/resources": { 49 | Post: &high.Operation{}, 50 | }, 51 | "/verycool/{id}/verynice/resources/{resource_id}": { 52 | Get: &high.Operation{}, 53 | Delete: &high.Operation{}, 54 | }, 55 | }), 56 | expectedResources: []string{"verycool_verynice_resources"}, 57 | }, 58 | "invalid resource combo - POST,DELETEbyID": { 59 | pathItems: orderedmap.ToOrderedMap(map[string]*high.PathItem{ 60 | "/resources": { 61 | Post: &high.Operation{}, 62 | }, 63 | "/resources/{resource_id}": { 64 | Delete: &high.Operation{}, 65 | }, 66 | }), 67 | expectedResources: []string{}, 68 | }, 69 | "invalid resource combo - GETbyID,DELETEbyID": { 70 | pathItems: orderedmap.ToOrderedMap(map[string]*high.PathItem{ 71 | "/resources/{resource_id}": { 72 | Get: &high.Operation{}, 73 | Delete: &high.Operation{}, 74 | }, 75 | }), 76 | expectedResources: []string{}, 77 | }, 78 | "invalid resource combo - GETbyID,POST": { 79 | pathItems: orderedmap.ToOrderedMap(map[string]*high.PathItem{ 80 | "/resources": { 81 | Post: &high.Operation{}, 82 | }, 83 | "/resources/{resource_id}": { 84 | Get: &high.Operation{}, 85 | }, 86 | }), 87 | expectedResources: []string{}, 88 | }, 89 | "invalid resource combo - no ops": { 90 | pathItems: orderedmap.ToOrderedMap(map[string]*high.PathItem{ 91 | "/resources": {}, 92 | "/resources/{resource_id}": {}, 93 | }), 94 | expectedResources: []string{}, 95 | }, 96 | } 97 | 98 | for name, testCase := range testCases { 99 | 100 | t.Run(name, func(t *testing.T) { 101 | t.Parallel() 102 | explorer := explorer.NewGuesstimatorExplorer(high.Document{Paths: &high.Paths{PathItems: testCase.pathItems}}) 103 | resources, err := explorer.FindResources() 104 | 105 | if err != nil { 106 | t.Fatalf("was not expecting error, got: %s", err) 107 | } 108 | 109 | if len(resources) != len(testCase.expectedResources) { 110 | t.Fatalf("expected %d resources, found %d resources", len(testCase.expectedResources), len(resources)) 111 | } 112 | 113 | for _, expectedResource := range testCase.expectedResources { 114 | _, ok := resources[expectedResource] 115 | if !ok { 116 | t.Fatalf("%s resource not found", expectedResource) 117 | } 118 | } 119 | }) 120 | } 121 | } 122 | 123 | func Test_GuesstimatorExplorer_FindDataSources(t *testing.T) { 124 | t.Parallel() 125 | 126 | testCases := map[string]struct { 127 | pathItems *orderedmap.Map[string, *high.PathItem] 128 | expectedDataSources []string 129 | }{ 130 | "valid flat data source combo": { 131 | pathItems: orderedmap.ToOrderedMap(map[string]*high.PathItem{ 132 | "/resources": { 133 | Get: &high.Operation{}, 134 | }, 135 | "/resources/{resource_id}": { 136 | Get: &high.Operation{}, 137 | }, 138 | }), 139 | expectedDataSources: []string{"resources_collection", "resources_by_id"}, 140 | }, 141 | "valid nested data source combo": { 142 | pathItems: orderedmap.ToOrderedMap(map[string]*high.PathItem{ 143 | "/verycool/verynice/resources": { 144 | Post: &high.Operation{}, 145 | }, 146 | "/verycool/verynice/resources/{resource_id}": { 147 | Get: &high.Operation{}, 148 | Delete: &high.Operation{}, 149 | }, 150 | }), 151 | expectedDataSources: []string{"verycool_verynice_resources_by_id"}, 152 | }, 153 | "valid nested with id data source combo": { 154 | pathItems: orderedmap.ToOrderedMap(map[string]*high.PathItem{ 155 | "/verycool/{id}/verynice/resources": { 156 | Get: &high.Operation{}, 157 | Post: &high.Operation{}, 158 | }, 159 | "/verycool/{id}/verynice/resources/{resource_id}": { 160 | Delete: &high.Operation{}, 161 | }, 162 | }), 163 | expectedDataSources: []string{"verycool_verynice_resources_collection"}, 164 | }, 165 | "invalid data source combo - no matching ops": { 166 | pathItems: orderedmap.ToOrderedMap(map[string]*high.PathItem{ 167 | "/resources": { 168 | Put: &high.Operation{}, 169 | Post: &high.Operation{}, 170 | Delete: &high.Operation{}, 171 | Options: &high.Operation{}, 172 | Head: &high.Operation{}, 173 | Patch: &high.Operation{}, 174 | Trace: &high.Operation{}, 175 | }, 176 | "/resources/{resource_id}": { 177 | Put: &high.Operation{}, 178 | Post: &high.Operation{}, 179 | Delete: &high.Operation{}, 180 | Options: &high.Operation{}, 181 | Head: &high.Operation{}, 182 | Patch: &high.Operation{}, 183 | Trace: &high.Operation{}, 184 | }, 185 | }), 186 | expectedDataSources: []string{}, 187 | }, 188 | "invalid data source combo - no ops": { 189 | pathItems: orderedmap.ToOrderedMap(map[string]*high.PathItem{ 190 | "/resources": {}, 191 | "/resources/{resource_id}": {}, 192 | }), 193 | expectedDataSources: []string{}, 194 | }, 195 | } 196 | 197 | for name, testCase := range testCases { 198 | 199 | t.Run(name, func(t *testing.T) { 200 | t.Parallel() 201 | explorer := explorer.NewGuesstimatorExplorer(high.Document{Paths: &high.Paths{PathItems: testCase.pathItems}}) 202 | dataSources, err := explorer.FindDataSources() 203 | 204 | if err != nil { 205 | t.Fatalf("was not expecting error, got: %s", err) 206 | } 207 | 208 | if len(dataSources) != len(testCase.expectedDataSources) { 209 | t.Fatalf("expected %d data sources, found %d data sources", len(testCase.expectedDataSources), len(dataSources)) 210 | } 211 | 212 | for _, expectedDataSource := range testCase.expectedDataSources { 213 | _, ok := dataSources[expectedDataSource] 214 | if !ok { 215 | t.Fatalf("%s data sources not found", expectedDataSource) 216 | } 217 | } 218 | }) 219 | } 220 | } 221 | --------------------------------------------------------------------------------