Skip to content

PDE-5218: Update Terraform Provider Generation #24

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 16 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 53 additions & 8 deletions .github/workflows/sdk_generation.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,56 @@ permissions:
- cron: 0 0 * * *
jobs:
generate:
uses: speakeasy-api/sdk-generation-action/.github/workflows/workflow-executor.yaml@v15
with:
force: ${{ github.event.inputs.force }}
mode: pr
set_version: ${{ github.event.inputs.set_version }}
secrets:
github_access_token: ${{ secrets.GITHUB_TOKEN }}
speakeasy_api_key: ${{ secrets.SPEAKEASY_API_KEY }}
runs-on: ubuntu-latest
steps:
- name: Clone SDK repo
uses: actions/checkout@v3

- name: Set up SSH
env:
SSH_AUTH_SOCK: /tmp/ssh_agent.sock
run: |
mkdir -p ~/.ssh
ssh-keyscan github.com >> ~/.ssh/known_hosts
echo "${{ secrets.FH_OPS_SSH_KEY }}" > ~/.ssh/id_ed25519
chmod 400 ~/.ssh/id_ed25519
ssh-agent -a $SSH_AUTH_SOCK > /dev/null
ssh-add ~/.ssh/id_ed25519

- name: Clone developers repo
env:
SSH_AUTH_SOCK: /tmp/ssh_agent.sock
run: |
git clone git@github.com:firehydrant/developers.git /tmp/dev-repo
cd /tmp/dev-repo
git checkout main

- name: Copy OpenAPI spec
run: |
cp /tmp/dev-repo/docs/public/openapi3_doc.json ${GITHUB_WORKSPACE}/openapi-raw.json

- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: 'stable'

- name: Process OpenAPI spec
run: |
# Normalize the schema
go run ./scripts/normalize openapi-raw.json openapi.json

# Generate Terraform overlay
go run ./scripts/overlay openapi.json ./scripts/overlay/manual-mappings.yaml

# Move overlay to Speakeasy directory
mkdir -p .speakeasy
mv terraform-overlay.yaml .speakeasy/

- name: Generate SDK
uses: speakeasy-api/sdk-generation-action@v15
with:
force: ${{ github.event.inputs.force }}
mode: pr
set_version: ${{ github.event.inputs.set_version }}
github_access_token: ${{ secrets.GITHUB_TOKEN }}
SPEAKEASY_API_KEY: ${{ secrets.SPEAKEASY_API_KEY }}
20 changes: 19 additions & 1 deletion .speakeasy/gen.yaml
Original file line number Diff line number Diff line change
@@ -1,24 +1,42 @@
configVersion: 2.0.0
generation:
devContainers:
enabled: true
schemaPath: registry.speakeasyapi.dev/firehydrant/firehydrant/firehydrant-oas:main
sdkClassName: firehydrant
maintainOpenAPIOrder: true
usageSnippets:
optionalPropertyRendering: withExample
sdkInitStyle: constructor
useClassNamesForArrayFields: true
fixes:
nameResolutionDec2023: true
nameResolutionFeb2025: true
parameterOrderingFeb2024: true
requestResponseComponentNamesFeb2024: true
securityFeb2025: false
sharedErrorComponentsApr2025: false
auth:
oAuth2ClientCredentialsEnabled: true
oAuth2PasswordEnabled: true
terraform:
version: 0.1.5
version: 0.2.6
additionalDataSources: []
additionalDependencies: {}
additionalEphemeralResources: []
additionalProviderAttributes:
httpHeaders: ""
tlsSkipVerify: ""
additionalResources: []
allowUnknownFieldsInWeakUnions: false
author: firehydrant
defaultErrorName: APIError
enableTypeDeduplication: false
environmentVariables: []
operationIdNaming: preserve
packageName: firehydrant
providerGeneration:
enabled: true
resourceDetection:
strategy: operationId
tagStrategy: ignore
4 changes: 2 additions & 2 deletions .speakeasy/workflow.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ speakeasyVersion: latest
sources:
firehydrant-oas:
inputs:
- location: ./openapi.yaml
- location: ${GITHUB_WORKSPACE}/openapi.json
overlays:
- location: .speakeasy/speakeasy-suggestions.yaml
- location: ./.speakeasy/terraform-overlay.yaml
registry:
location: registry.speakeasyapi.dev/firehydrant/firehydrant/firehydrant-oas
targets:
Expand Down
230 changes: 230 additions & 0 deletions scripts/normalize/enums.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
package main

import (
"fmt"
"strings"
"unicode"
)

type EnumNormalizationInfo struct {
SchemaName string
PropertyPath string
PropertyName string
EnumValues []string
Target map[string]interface{}
}

func normalizeEnums(schemas map[string]interface{}) []ConflictDetail {
conflicts := make([]ConflictDetail, 0)

fmt.Printf("\n=== Normalizing Enum Properties ===\n")

allEnums := findAllEnumProperties(schemas)

if len(allEnums) == 0 {
fmt.Printf("No enum properties found to normalize\n")
return conflicts
}

fmt.Printf("Found %d enum properties to normalize\n", len(allEnums))

for _, enumInfo := range allEnums {
conflict := transformEnumProperty(enumInfo)
if conflict != nil {
conflicts = append(conflicts, *conflict)
}
}

fmt.Printf("Successfully normalized %d enum properties\n", len(conflicts))
return conflicts
}

func findAllEnumProperties(schemas map[string]interface{}) []EnumNormalizationInfo {
var allEnums []EnumNormalizationInfo

for schemaName, schema := range schemas {
schemaMap, ok := schema.(map[string]interface{})
if !ok {
continue
}

schemaEnums := findEnumsInSchemaRecursive(schemaName, schemaMap, "", schemaMap)
allEnums = append(allEnums, schemaEnums...)
}

return allEnums
}

func findEnumsInSchemaRecursive(schemaName string, currentObj map[string]interface{}, path string, rootSchema map[string]interface{}) []EnumNormalizationInfo {
var enums []EnumNormalizationInfo

if enumValues, hasEnum := currentObj["enum"]; hasEnum {
if enumArray, ok := enumValues.([]interface{}); ok && len(enumArray) > 0 {
var stringValues []string
for _, val := range enumArray {
if str, ok := val.(string); ok {
stringValues = append(stringValues, str)
}
}

if len(stringValues) > 0 {
propertyName := extractPropertyNameFromNormalizationPath(path, schemaName)

enumInfo := EnumNormalizationInfo{
SchemaName: schemaName,
PropertyPath: path,
PropertyName: propertyName,
EnumValues: stringValues,
Target: currentObj,
}

enums = append(enums, enumInfo)
}
}
}

if properties, hasProps := currentObj["properties"].(map[string]interface{}); hasProps {
for propName, propValue := range properties {
if propMap, ok := propValue.(map[string]interface{}); ok {
newPath := propName
if path != "" {
newPath = fmt.Sprintf("%s.properties.%s", path, propName)
} else {
newPath = fmt.Sprintf("properties.%s", propName)
}

propEnums := findEnumsInSchemaRecursive(schemaName, propMap, newPath, rootSchema)
enums = append(enums, propEnums...)
}
}
}

if items, hasItems := currentObj["items"].(map[string]interface{}); hasItems {
newPath := path
if path != "" {
newPath = fmt.Sprintf("%s.items", path)
} else {
newPath = "items"
}

itemEnums := findEnumsInSchemaRecursive(schemaName, items, newPath, rootSchema)
enums = append(enums, itemEnums...)
}

return enums
}

func transformEnumProperty(enumInfo EnumNormalizationInfo) *ConflictDetail {
var members []map[string]interface{}

for _, value := range enumInfo.EnumValues {
memberName := generateEnumMemberName(value, enumInfo.PropertyName, enumInfo.SchemaName)
members = append(members, map[string]interface{}{
"name": memberName,
"value": value,
})
}

delete(enumInfo.Target, "enum")
enumInfo.Target["x-speakeasy-enums"] = members

targetPath := enumInfo.SchemaName
if enumInfo.PropertyPath != "" {
targetPath = fmt.Sprintf("%s.%s", enumInfo.SchemaName, enumInfo.PropertyPath)
}

return &ConflictDetail{
Schema: enumInfo.SchemaName,
Property: enumInfo.PropertyName,
ConflictType: "enum-normalization",
Resolution: fmt.Sprintf("Replaced enum array with x-speakeasy-enums at %s (%d values)", targetPath, len(members)),
}
}

func generateEnumMemberName(value, fieldName, entityName string) string {
cleanEntityName := convertToEnumMemberName(strings.TrimSuffix(entityName, "Entity"))
cleanFieldName := convertToEnumMemberName(fieldName)
cleanValue := convertToEnumMemberName(value)

memberName := cleanEntityName + cleanFieldName + cleanValue

// Ensure it starts with a letter
if len(memberName) > 0 && unicode.IsDigit(rune(memberName[0])) {
memberName = "Value" + memberName
}

// Handle empty result after cleaning
if memberName == "" {
memberName = cleanEntityName + cleanFieldName + "Unknown"
}

return memberName
}

func extractPropertyNameFromNormalizationPath(path, schemaName string) string {
if path == "" {
return strings.TrimSuffix(schemaName, "Entity")
}

parts := strings.Split(path, ".")

for i := len(parts) - 1; i >= 0; i-- {
part := parts[i]

if part == "properties" || part == "items" {
continue
}

return part
}

return strings.TrimSuffix(schemaName, "Entity")
}

func convertToEnumMemberName(value string) string {
if strings.TrimSpace(value) == "" {
return "Empty"
}

cleaned := strings.Map(func(r rune) rune {
if unicode.IsLetter(r) || unicode.IsDigit(r) {
return r
}
return '_'
}, value)

cleaned = strings.Trim(cleaned, "_")
for strings.Contains(cleaned, "__") {
cleaned = strings.ReplaceAll(cleaned, "__", "_")
}

if cleaned == "" {
return "Empty"
}

parts := strings.Split(cleaned, "_")
var result strings.Builder

for _, part := range parts {
if len(part) > 0 {
result.WriteString(strings.ToUpper(string(part[0])))
if len(part) > 1 {
result.WriteString(strings.ToLower(part[1:]))
}
}
}

memberName := result.String()

// Ensure it starts with a letter
if len(memberName) > 0 && unicode.IsDigit(rune(memberName[0])) {
memberName = "Value" + memberName
}

// Handle empty result after cleaning
if memberName == "" {
memberName = "Empty"
}

return memberName
}
Loading