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

Commit b2bf0fe

Browse files
Merge pull request #38 from firehydrant/add-manual-mappings
Add manual mappings, ignores related to on call schedules, shifts
2 parents 4f5ce2f + a12295c commit b2bf0fe

File tree

3 files changed

+199
-27
lines changed

3 files changed

+199
-27
lines changed

scripts/overlay/manual-mappings.yaml

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,18 @@ operations:
5252
method: "get"
5353
action: "ignore"
5454

55+
# Ignoring create, update, delete operations for on call schedule shifts
56+
# These operations should not be supported in the provider, but read to enable the datasource is supported
57+
- path: "/v1/teams/{team_id}/on_call_schedules/{schedule_id}/shifts"
58+
method: "post"
59+
action: "ignore"
60+
- path: "/v1/teams/{team_id}/on_call_schedules/{schedule_id}/shifts/{shift_id}"
61+
method: "patch"
62+
action: "ignore"
63+
- path: "/v1/teams/{team_id}/on_call_schedules/{schedule_id}/shifts/{shift_id}"
64+
method: "delete"
65+
action: "ignore"
66+
5567
# Manual Entity Mappings
5668
# Signals Event Sources
5769
- path: "/v1/signals/event_sources/{transposer_slug}"
@@ -148,6 +160,37 @@ operations:
148160
action: "match"
149161
value: "team_id:handoff_step.target.id"
150162

163+
# Signals On Call Schedules
164+
- path: "/v1/teams/{team_id}/on_call_schedules/{schedule_id}"
165+
method: "get"
166+
action: "match"
167+
value: "schedule_id:id"
168+
169+
- path: "/v1/teams/{team_id}/on_call_schedules/{schedule_id}"
170+
method: "patch"
171+
action: "match"
172+
value: "schedule_id:id"
173+
174+
- path: "/v1/teams/{team_id}/on_call_schedules/{schedule_id}"
175+
method: "delete"
176+
action: "match"
177+
value: "schedule_id:id"
178+
179+
- path: "/v1/teams/{team_id}/on_call_schedules/{schedule_id}"
180+
method: "get"
181+
action: "match"
182+
value: "team_id:team.id"
183+
184+
- path: "/v1/teams/{team_id}/on_call_schedules/{schedule_id}"
185+
method: "patch"
186+
action: "match"
187+
value: "team_id:team.id"
188+
189+
- path: "/v1/teams/{team_id}/on_call_schedules/{schedule_id}"
190+
method: "delete"
191+
action: "match"
192+
value: "team_id:team.id"
193+
151194
# Task Lists
152195
- path: "/v1/task_lists"
153196
method: "get"

scripts/overlay/terraform-viable.go

Lines changed: 90 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -164,15 +164,16 @@ func validateOperationParameters(resource *ResourceInfo, primaryID string, schem
164164
}
165165
} else if isEntityID(param) {
166166
// This is another ID-like parameter without manual mapping
167-
// Check if it maps to a field in the entity (not the primary id field)
168-
if checkFieldExistsInEntityWithRefResolution(param, entityProps, schemas) {
169-
// This parameter maps to a real entity field - it's valid
170-
continue
171-
} else {
172-
// This ID parameter doesn't map to any entity field
167+
// Only consider it problematic if it conflicts with the primary ID or claims to be this entity's ID
168+
if param != primaryID && mapsToEntityID(param, resource.EntityName) {
169+
// This parameter claims to be this entity's ID but isn't the primary ID - that's a conflict
173170
hasConflictingEntityIDs = true
174171
break
175172
}
173+
174+
// For other ID parameters (like team_id, parent_id, etc.), they're just foreign keys
175+
// and don't need to exist in the entity schema - they're fine
176+
// we weren't able to map them but they don't conflict with the primary ID
176177
}
177178
// Non-ID parameters are always OK
178179
}
@@ -276,42 +277,48 @@ func checkFieldExistsInEntityWithRefResolution(fieldPath string, entityProps map
276277
currentLevel := entityProps
277278

278279
for i, part := range parts {
280+
279281
if prop, exists := currentLevel[part]; exists {
282+
280283
if i == len(parts)-1 {
281284
return true
282285
}
283286

284287
if propMap, ok := prop.(map[string]interface{}); ok {
288+
// Check if it has direct properties
285289
if nestedProps, hasProps := propMap["properties"].(map[string]interface{}); hasProps {
286290
currentLevel = nestedProps
287291
continue
288292
}
289293

294+
// Check if it has a $ref that needs resolution
290295
if ref, hasRef := propMap["$ref"].(string); hasRef {
291-
if strings.HasPrefix(ref, "#/components/schemas/") {
292-
schemaName := strings.TrimPrefix(ref, "#/components/schemas/")
293-
294-
if referencedSchema, exists := schemas[schemaName]; exists {
295-
specData, _ := json.Marshal(referencedSchema)
296-
var schemaMap map[string]interface{}
297-
json.Unmarshal(specData, &schemaMap)
298-
299-
if refProps, hasRefProps := schemaMap["properties"].(map[string]interface{}); hasRefProps {
300-
currentLevel = refProps
301-
continue
302-
}
296+
297+
resolvedSchema := resolveSchemaRef(ref, schemas)
298+
if resolvedSchema != nil {
299+
300+
if refProps, hasRefProps := resolvedSchema["properties"].(map[string]interface{}); hasRefProps {
301+
currentLevel = refProps
302+
continue
303+
} else {
304+
fmt.Printf(" Debug: Resolved schema has no properties\n")
303305
}
306+
} else {
307+
fmt.Printf(" Debug: Failed to resolve $ref '%s'\n", ref)
304308
}
305309

306310
fmt.Printf(" Warning: Cannot resolve $ref for nested property validation: %s (ref: %s)\n", fieldPath, ref)
307-
return false // We don't want to assume the ref is valid if we can't resolve it
311+
return false
308312
}
309313
}
310314

311315
// If we can't navigate deeper but haven't reached the end, the path is invalid
316+
fmt.Printf(" Debug: Cannot navigate deeper from part '%s' - not a valid object structure\n", part)
312317
fmt.Printf(" Cannot navigate to nested property: %s at part: %s\n", fieldPath, part)
313318
return false
314319
} else {
320+
fmt.Printf(" Debug: Part '%s' not found in current level\n", part)
321+
fmt.Printf(" Debug: Available keys in current level: %v\n", getKeys(currentLevel))
315322
fmt.Printf(" Field does not exist: %s at part: %s\n", fieldPath, part)
316323
return false
317324
}
@@ -320,6 +327,55 @@ func checkFieldExistsInEntityWithRefResolution(fieldPath string, entityProps map
320327
return false
321328
}
322329

330+
func getKeys(m map[string]interface{}) []string {
331+
keys := make([]string, 0, len(m))
332+
for k := range m {
333+
keys = append(keys, k)
334+
}
335+
return keys
336+
}
337+
338+
func resolveSchemaRef(ref string, schemas map[string]interface{}) map[string]interface{} {
339+
if !strings.HasPrefix(ref, "#/components/schemas/") {
340+
return nil
341+
}
342+
343+
schemaName := strings.TrimPrefix(ref, "#/components/schemas/")
344+
345+
if referencedSchema, exists := schemas[schemaName]; exists {
346+
specData, _ := json.Marshal(referencedSchema)
347+
var schemaMap map[string]interface{}
348+
json.Unmarshal(specData, &schemaMap)
349+
350+
// At time of writing we have validation rules which prevent oneOfs in our swagger generation within laddertruck.
351+
// So we only have allOf patterns to work with, these are generally created by our NullableWrappers
352+
// NullableWrappers are used to allow nullable properties in the API (also created in laddertruck), so we need to resolve them
353+
if allOf, hasAllOf := schemaMap["allOf"].([]interface{}); hasAllOf {
354+
for _, item := range allOf {
355+
if itemMap, ok := item.(map[string]interface{}); ok {
356+
if innerRef, hasInnerRef := itemMap["$ref"].(string); hasInnerRef {
357+
resolved := resolveSchemaRef(innerRef, schemas)
358+
if resolved != nil {
359+
return resolved
360+
}
361+
}
362+
if _, hasProps := itemMap["properties"].(map[string]interface{}); hasProps {
363+
return itemMap
364+
}
365+
}
366+
}
367+
}
368+
369+
if _, hasProps := schemaMap["properties"].(map[string]interface{}); hasProps {
370+
return schemaMap
371+
}
372+
373+
return schemaMap
374+
}
375+
376+
return nil
377+
}
378+
323379
func identifyEntityPrimaryID(resource *ResourceInfo, schemas map[string]interface{}) (string, bool) {
324380
allParams := make(map[string]bool)
325381

@@ -380,14 +436,26 @@ func identifyEntityPrimaryID(resource *ResourceInfo, schemas map[string]interfac
380436
}
381437
}
382438
}
383-
}
384439

385-
if allParams["id"] {
386-
return "id", true
440+
if allParams["id"] {
441+
return "id", true
442+
}
387443
}
388444

389445
if len(allParams) == 1 && (hasID || hasSlug) {
390446
for param := range allParams {
447+
if !strings.Contains(param, "team_id") && !strings.Contains(param, "parent_id") {
448+
return param, true
449+
}
450+
}
451+
}
452+
453+
// For complex cases with multiple parameters, try to identify the most likely primary ID
454+
// Look for parameters that end with the entity name or are simple "id"
455+
entityBase := strings.ToLower(strings.TrimSuffix(resource.EntityName, "Entity"))
456+
for param := range allParams {
457+
lowerParam := strings.ToLower(param)
458+
if lowerParam == entityBase+"_id" || lowerParam == "id" {
391459
return param, true
392460
}
393461
}

scripts/overlay/terraform-viable_test.go

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,13 @@ func TestValidateOperationParameters(t *testing.T) {
129129
"name": map[string]interface{}{"type": "string"},
130130
},
131131
},
132+
"TeamEntity": map[string]interface{}{
133+
"type": "object",
134+
"properties": map[string]interface{}{
135+
"id": map[string]interface{}{"type": "string"},
136+
"name": map[string]interface{}{"type": "string"},
137+
},
138+
},
132139
}
133140

134141
tests := []struct {
@@ -203,6 +210,28 @@ func TestValidateOperationParameters(t *testing.T) {
203210
primaryID: "id",
204211
expected: 1, // Only create should be valid
205212
},
213+
{
214+
name: "operations with foreign keys (should be valid)",
215+
resource: &ResourceInfo{
216+
EntityName: "UserEntity",
217+
Operations: map[string]OperationInfo{
218+
"create": {
219+
Path: "/teams/{team_id}/users",
220+
Method: "post",
221+
},
222+
"read": {
223+
Path: "/teams/{team_id}/users/{id}",
224+
Method: "get",
225+
},
226+
"update": {
227+
Path: "/teams/{team_id}/users/{id}",
228+
Method: "put",
229+
},
230+
},
231+
},
232+
primaryID: "id",
233+
expected: 3,
234+
},
206235
}
207236

208237
for _, tt := range tests {
@@ -440,6 +469,9 @@ func TestCheckFieldExistsInEntityWithRefResolution(t *testing.T) {
440469
"settings": map[string]interface{}{
441470
"$ref": "#/components/schemas/UserSettings",
442471
},
472+
"team": map[string]interface{}{
473+
"$ref": "#/components/schemas/NullableSuccinctEntity",
474+
},
443475
}
444476

445477
schemas := map[string]interface{}{
@@ -449,6 +481,20 @@ func TestCheckFieldExistsInEntityWithRefResolution(t *testing.T) {
449481
"theme": map[string]interface{}{"type": "string"},
450482
},
451483
},
484+
"NullableSuccinctEntity": map[string]interface{}{
485+
"allOf": []interface{}{
486+
map[string]interface{}{
487+
"$ref": "#/components/schemas/SuccinctEntity",
488+
},
489+
},
490+
},
491+
"SuccinctEntity": map[string]interface{}{
492+
"type": "object",
493+
"properties": map[string]interface{}{
494+
"id": map[string]interface{}{"type": "string"},
495+
"name": map[string]interface{}{"type": "string"},
496+
},
497+
},
452498
}
453499

454500
tests := []struct {
@@ -486,6 +532,21 @@ func TestCheckFieldExistsInEntityWithRefResolution(t *testing.T) {
486532
fieldPath: "settings.nonexistent",
487533
expected: false,
488534
},
535+
{
536+
name: "nullable ref field exists",
537+
fieldPath: "team.id",
538+
expected: true,
539+
},
540+
{
541+
name: "nullable ref field exists - name",
542+
fieldPath: "team.name",
543+
expected: true,
544+
},
545+
{
546+
name: "nullable ref field doesn't exist",
547+
fieldPath: "team.nonexistent",
548+
expected: false,
549+
},
489550
{
490551
name: "invalid path",
491552
fieldPath: "name.invalid.path",
@@ -589,20 +650,20 @@ func TestIdentifyEntityPrimaryID(t *testing.T) {
589650
expectID: false,
590651
},
591652
{
592-
name: "multiple conflicting parameters - fixed",
653+
name: "single parameter with foreign key context",
593654
resource: &ResourceInfo{
594655
EntityName: "UserEntity",
595656
Operations: map[string]OperationInfo{
596657
"read": {
597-
Path: "/users/{user_id}",
658+
Path: "/teams/{team_id}/users/{id}",
598659
},
599660
"update": {
600-
Path: "/users/{user_id}", // Fixed: make them the same to not conflict
661+
Path: "/teams/{team_id}/users/{id}",
601662
},
602663
},
603664
},
604-
expected: "user_id", // Fixed: now they should match
605-
expectID: true, // Fixed: now should find ID
665+
expected: "id",
666+
expectID: true,
606667
},
607668
}
608669

0 commit comments

Comments
 (0)