Skip to content

Commit 3de8209

Browse files
fix: handle git ref paths in join() for $ref resolution
When resolving relative $ref paths against a base path that uses git ref syntax (e.g. "origin/main:openapi.yaml"), path.Dir() treats the colon as a regular character and returns "origin" instead of recognizing it as the git ref separator. This causes $ref resolution to produce invalid paths like "origin/schemas/pet.yaml" instead of "origin/main:schemas/pet.yaml", breaking multi-file OpenAPI specs loaded via git refs. The fix detects the colon separator and applies path.Dir/path.Join only to the file path portion after the colon, preserving the git ref prefix. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 22cc09f commit 3de8209

2 files changed

Lines changed: 64 additions & 1 deletion

File tree

openapi3/loader.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,16 @@ func join(basePath *url.URL, relativePath *url.URL) *url.URL {
278278
return relativePath
279279
}
280280
newPath := *basePath
281-
newPath.Path = path.Join(path.Dir(newPath.Path), relativePath.Path)
281+
// Handle git ref paths like "origin/main:openapi.yaml" where ":"
282+
// separates the ref from the file path. path.Dir does not understand
283+
// this syntax and would treat the colon as a regular character.
284+
if i := strings.IndexByte(newPath.Path, ':'); i >= 0 {
285+
prefix := newPath.Path[:i+1] // e.g. "origin/main:"
286+
filePath := newPath.Path[i+1:]
287+
newPath.Path = prefix + path.Join(path.Dir(filePath), relativePath.Path)
288+
} else {
289+
newPath.Path = path.Join(path.Dir(newPath.Path), relativePath.Path)
290+
}
282291
return &newPath
283292
}
284293

openapi3/loader_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -672,3 +672,57 @@ func TestReadFromIoReader_Nil(t *testing.T) {
672672
_, err := loader.LoadFromIoReader(nil)
673673
require.EqualError(t, err, "invalid reader: <nil>")
674674
}
675+
676+
func TestJoinGitRefPath(t *testing.T) {
677+
tests := []struct {
678+
name string
679+
base string
680+
rel string
681+
expected string
682+
}{
683+
{
684+
name: "git ref with relative path",
685+
base: "origin/main:openapi.yaml",
686+
rel: "schemas/pet.yaml",
687+
expected: "origin/main:schemas/pet.yaml",
688+
},
689+
{
690+
name: "git ref with dot-slash relative path",
691+
base: "origin/main:openapi.yaml",
692+
rel: "./schemas/pet.yaml",
693+
expected: "origin/main:schemas/pet.yaml",
694+
},
695+
{
696+
name: "git ref with nested base path",
697+
base: "origin/main:api/v1/openapi.yaml",
698+
rel: "../common/types.yaml",
699+
expected: "origin/main:api/common/types.yaml",
700+
},
701+
{
702+
name: "regular path without colon",
703+
base: "/home/user/openapi.yaml",
704+
rel: "schemas/pet.yaml",
705+
expected: "/home/user/schemas/pet.yaml",
706+
},
707+
{
708+
name: "nil base returns relative",
709+
base: "",
710+
rel: "schemas/pet.yaml",
711+
expected: "schemas/pet.yaml",
712+
},
713+
}
714+
715+
for _, tt := range tests {
716+
t.Run(tt.name, func(t *testing.T) {
717+
rel := &url.URL{Path: tt.rel}
718+
if tt.base == "" {
719+
result := join(nil, rel)
720+
require.Equal(t, tt.expected, result.Path)
721+
return
722+
}
723+
base := &url.URL{Path: tt.base}
724+
result := join(base, rel)
725+
require.Equal(t, tt.expected, result.Path)
726+
})
727+
}
728+
}

0 commit comments

Comments
 (0)