Skip to content
This repository was archived by the owner on Sep 30, 2025. It is now read-only.

Commit 5bbb4c9

Browse files
Merge pull request #30 from firehydrant/js/PDE-54321/notification-policy-support
PDE-5421: Normalize inline request schemas, update manual mappings
2 parents e7b7433 + d06a6b8 commit 5bbb4c9

File tree

7 files changed

+426
-20
lines changed

7 files changed

+426
-20
lines changed

scripts/Readme.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ go run ./scripts/normalize <input.json> [output.json]
2828
```
2929

3030
**What it does:**
31+
- Extracts inline request schemas: Moves inline `requestBody` schemas to `components/schemas` with `$ref` references
3132
- Replaces Terraform reserved keyword properties with empty objects (only for object/ref types)
3233
- Converts `additionalProperties` to `properties` for better Terraform compatibility
3334
- Normalizes path parameters (e.g., converts integer IDs to strings)
@@ -57,6 +58,7 @@ See speakeasy extensions: https://www.speakeasy.com/docs/speakeasy-reference/ext
5758
## Key Concepts
5859

5960
1. **During Normalization**:
61+
- Inline request parameter extraction into request schemas
6062
- Minimal structural changes to preserve original API design
6163
- Reserved keyword handling for Terraform compatibility
6264
- Schema cleanup without forced alignment
@@ -169,9 +171,10 @@ Given this input structure:
169171
```
170172

171173
The normalization process will:
172-
1. Leave schemas structurally unchanged
173-
2. Handle any reserved keyword properties
174-
3. Perform minimal cleanup operations
174+
1. Extract any inline request schemas to `components/schemas` with proper `$ref` references
175+
2. Leave existing schemas structurally unchanged
176+
3. Handle any reserved keyword properties
177+
4. Perform minimal cleanup operations
175178

176179
The overlay generation will:
177180
1. Mark `UserEntity` with `x-speakeasy-entity`
@@ -188,6 +191,8 @@ The overlay generation will:
188191
Run scripts locally. Use `speakeasy run` to attempt to generate the provider.
189192

190193
### Debugging Normalization
194+
- Verify that extracted schemas appear in `components/schemas`
195+
- Confirm inline request bodies now use `$ref` references
191196
- Check for reserved keyword replacements
192197
- Verify minimal structural changes were applied
193198
- Look for enum and parameter normalizations

scripts/normalize/main.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ func printNormalizationReport(report NormalizationReport) {
8484
parameterFixes := 0
8585
enumFixes := 0
8686
terraformKeywordFixes := 0
87+
requestSchemaFixes := 0
8788
otherFixes := 0
8889

8990
for _, detail := range report.ConflictDetails {
@@ -96,6 +97,8 @@ func printNormalizationReport(report NormalizationReport) {
9697
enumFixes++
9798
case "terraform-keyword":
9899
terraformKeywordFixes++
100+
case "request-schema-extraction":
101+
requestSchemaFixes++
99102
default:
100103
otherFixes++
101104
}
@@ -105,6 +108,7 @@ func printNormalizationReport(report NormalizationReport) {
105108
fmt.Printf("Parameter type fixes: %d\n", parameterFixes)
106109
fmt.Printf("Enum normalization fixes: %d\n", enumFixes)
107110
fmt.Printf("Terraform keyword fixes: %d\n", terraformKeywordFixes)
111+
fmt.Printf("Request schema extraction fixes: %d\n", requestSchemaFixes)
108112
fmt.Printf("Other fixes: %d\n", otherFixes)
109113

110114
// Helpful for debugging

scripts/normalize/normalize.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,30 @@ func normalizeSpec(spec map[string]interface{}) NormalizationReport {
1212
components, ok := spec["components"].(map[string]interface{})
1313
if !ok {
1414
fmt.Println("Warning: No components found in spec")
15-
return report
15+
// Create components if it doesn't exist
16+
components = map[string]interface{}{}
17+
spec["components"] = components
1618
}
1719

1820
schemas, ok := components["schemas"].(map[string]interface{})
1921
if !ok {
2022
fmt.Println("Warning: No schemas found in components")
21-
return report
23+
// Create schemas if it doesn't exist
24+
schemas = map[string]interface{}{}
25+
components["schemas"] = schemas
2226
}
2327

2428
paths, pathsOk := spec["paths"].(map[string]interface{})
2529
if !pathsOk {
2630
fmt.Println("Warning: No paths found in spec")
2731
}
2832

33+
// We need to normalize request schemas first to ensure that extracted schemas are available for other normalization steps
34+
if pathsOk {
35+
requestSchemaFixes := normalizeRequestSchemasWithPaths(paths, schemas)
36+
report.ConflictDetails = append(report.ConflictDetails, requestSchemaFixes...)
37+
}
38+
2939
terraformFixes := normalizeTerraformKeywords(schemas)
3040
report.ConflictDetails = append(report.ConflictDetails, terraformFixes...)
3141

@@ -51,6 +61,8 @@ func normalizeSpec(spec map[string]interface{}) NormalizationReport {
5161
report.MapClassFixes++
5262
case "terraform-keyword":
5363
report.TerraformKeywordFixes++
64+
case "request-schema-extraction":
65+
report.PropertyFixes++
5466
default:
5567
report.PropertyFixes++
5668
}
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
)
7+
8+
type RequestSchemaInfo struct {
9+
Path string
10+
Method string
11+
OperationId string
12+
ContentType string
13+
Schema map[string]interface{}
14+
SchemaName string
15+
IsInline bool
16+
}
17+
18+
func findInlineRequestSchemas(paths map[string]interface{}) []RequestSchemaInfo {
19+
var inlineSchemas []RequestSchemaInfo
20+
21+
for pathName, pathItem := range paths {
22+
pathMap, ok := pathItem.(map[string]interface{})
23+
if !ok {
24+
continue
25+
}
26+
27+
// Check POST, PUT, PATCH methods for request bodies
28+
methods := []string{"post", "put", "patch"}
29+
for _, method := range methods {
30+
if operation, exists := pathMap[method]; exists {
31+
opMap, ok := operation.(map[string]interface{})
32+
if !ok {
33+
continue
34+
}
35+
36+
operationId, _ := opMap["operationId"].(string)
37+
38+
if requestBody, hasReqBody := opMap["requestBody"]; hasReqBody {
39+
reqBodyMap, ok := requestBody.(map[string]interface{})
40+
if !ok {
41+
continue
42+
}
43+
44+
if content, hasContent := reqBodyMap["content"].(map[string]interface{}); hasContent {
45+
for contentType, contentSchema := range content {
46+
if contentMap, ok := contentSchema.(map[string]interface{}); ok {
47+
if schema, hasSchema := contentMap["schema"].(map[string]interface{}); hasSchema {
48+
// Check if it's an inline schema (no $ref)
49+
if _, hasRef := schema["$ref"]; !hasRef {
50+
// Generate schema name
51+
schemaName := generateRequestSchemaName(operationId, contentType)
52+
53+
inlineSchemas = append(inlineSchemas, RequestSchemaInfo{
54+
Path: pathName,
55+
Method: method,
56+
OperationId: operationId,
57+
ContentType: contentType,
58+
Schema: schema,
59+
SchemaName: schemaName,
60+
IsInline: true,
61+
})
62+
}
63+
}
64+
}
65+
}
66+
}
67+
}
68+
}
69+
}
70+
}
71+
72+
return inlineSchemas
73+
}
74+
75+
func generateRequestSchemaName(operationId, contentType string) string {
76+
// Tests in laddertruck required operationIds to be added to routes
77+
// so we will rely on these here
78+
schemaName := operationId
79+
80+
// Only add suffix for non-JSON content types
81+
if contentType != "application/json" {
82+
schemaName = schemaName + convertContentTypeToSuffix(contentType)
83+
}
84+
85+
return schemaName
86+
}
87+
88+
func convertContentTypeToSuffix(contentType string) string {
89+
switch contentType {
90+
case "multipart/form-data":
91+
return "_form"
92+
case "application/x-www-form-urlencoded":
93+
return "_form_encoded"
94+
case "text/plain":
95+
return "_text"
96+
case "application/xml":
97+
return "_xml"
98+
default:
99+
suffix := strings.ReplaceAll(contentType, "/", "_")
100+
suffix = strings.ReplaceAll(suffix, "-", "_")
101+
suffix = strings.ReplaceAll(suffix, ".", "_")
102+
suffix = strings.ReplaceAll(suffix, "+", "_")
103+
return "_" + suffix
104+
}
105+
}
106+
107+
func ensureUniqueSchemaName(baseName string, schemas map[string]interface{}) string {
108+
if _, exists := schemas[baseName]; !exists {
109+
return baseName
110+
}
111+
112+
counter := 1
113+
for {
114+
candidate := fmt.Sprintf("%s_%d", baseName, counter)
115+
if _, exists := schemas[candidate]; !exists {
116+
return candidate
117+
}
118+
counter++
119+
}
120+
}
121+
122+
// normalizeRequestSchemasWithPaths extracts inline request body schemas and moves them to components/schemas
123+
func normalizeRequestSchemasWithPaths(paths map[string]interface{}, schemas map[string]interface{}) []ConflictDetail {
124+
conflicts := make([]ConflictDetail, 0)
125+
126+
fmt.Printf("\n=== Normalizing Request Schemas ===\n")
127+
128+
inlineSchemas := findInlineRequestSchemas(paths)
129+
130+
if len(inlineSchemas) == 0 {
131+
fmt.Printf("No inline request schemas found to normalize\n")
132+
return conflicts
133+
}
134+
135+
fmt.Printf("Found %d inline request schemas to normalize\n", len(inlineSchemas))
136+
137+
for _, schemaInfo := range inlineSchemas {
138+
finalSchemaName := ensureUniqueSchemaName(schemaInfo.SchemaName, schemas)
139+
140+
schemaCopy := make(map[string]interface{})
141+
for k, v := range schemaInfo.Schema {
142+
schemaCopy[k] = v
143+
}
144+
145+
schemas[finalSchemaName] = schemaCopy
146+
147+
// Replace the original inline schema with $ref
148+
ref := fmt.Sprintf("#/components/schemas/%s", finalSchemaName)
149+
150+
// Clear the original schema and replace with $ref
151+
for key := range schemaInfo.Schema {
152+
delete(schemaInfo.Schema, key)
153+
}
154+
schemaInfo.Schema["$ref"] = ref
155+
156+
conflicts = append(conflicts, ConflictDetail{
157+
Schema: finalSchemaName,
158+
Property: fmt.Sprintf("%s.%s", schemaInfo.Method, schemaInfo.Path),
159+
ConflictType: "request-schema-extraction",
160+
Resolution: fmt.Sprintf("Extracted inline request schema to %s", finalSchemaName),
161+
})
162+
}
163+
164+
fmt.Printf("Successfully normalized %d request schemas\n", len(conflicts))
165+
return conflicts
166+
}

0 commit comments

Comments
 (0)