77 "fmt"
88 "io"
99 "net/http"
10+ "os"
1011 "path/filepath"
1112 "regexp"
1213 "strings"
@@ -23,8 +24,7 @@ type protoFile struct {
2324}
2425
2526type ProtoSchemaSet struct {
26- Owner string `toml:"owner"`
27- Repository string `toml:"repository"`
27+ URI string `toml:"uri"`
2828 Ref string `toml:"ref"` // ref or tag or commit SHA
2929 Folders []string `toml:"folders"` // if not provided, all protos will be fetched, otherwise only protos in these folders will be fetched
3030 SubjectPrefix string `toml:"subject_prefix"` // optional prefix for subjects
@@ -36,16 +36,31 @@ type SubjectNamingStrategyFn func(subjectPrefix string, protoFile protoFile, rep
3636// RepositoryToSubjectNamingStrategyFn is a map of repository names to SubjectNamingStrategyFn functions
3737type RepositoryToSubjectNamingStrategyFn map [string ]SubjectNamingStrategyFn
3838
39- func ValidateRepoConfiguration (repoConfig ProtoSchemaSet ) error {
40- if repoConfig .Owner == "" {
41- return errors .New ("owner is required" )
39+ func validateRepoConfiguration (repoConfig ProtoSchemaSet ) error {
40+ if repoConfig .URI == "" {
41+ return errors .New ("uri is required" )
4242 }
43- if repoConfig .Repository == "" {
44- return errors .New ("repo is required" )
43+
44+ if ! strings .HasPrefix (repoConfig .URI , "https://" ) && ! strings .HasPrefix (repoConfig .URI , "file://" ) {
45+ return errors .New ("uri has to start with either 'file://' or 'https://'" )
46+ }
47+
48+ trimmedURI := strings .TrimPrefix (repoConfig .URI , "https://" )
49+ if ! strings .HasPrefix (trimmedURI , "github.com" ) {
50+ return fmt .Errorf ("only repositories hosted at github.com are supported, but %s was found" , repoConfig .URI )
51+ }
52+
53+ parts := strings .Split (trimmedURI , "/" )
54+ if len (parts ) < 3 {
55+ return fmt .Errorf ("URI should have following format: 'https://github.com/<OWNER>/<REPOSITORY>', but %s was found" , repoConfig .URI )
56+ }
57+
58+ if repoConfig .Ref == "" && strings .HasPrefix (repoConfig .URI , "https://" ) {
59+ return errors .New ("ref is required, when fetching protos from Github" )
4560 }
4661
47- if repoConfig .Ref == "" {
48- return errors .New ("ref is required " )
62+ if repoConfig .Ref != "" && strings . HasPrefix ( repoConfig . URI , "file://" ) {
63+ return errors .New ("ref is not supported with local protos " )
4964 }
5065
5166 return nil
@@ -59,9 +74,15 @@ func RegisterAndFetchProtos(ctx context.Context, client *github.Client, protoSch
5974 framework .L .Debug ().Msgf ("Registering and fetching protos from %d repositories" , len (protoSchemaSets ))
6075
6176 for _ , protoSchemaSet := range protoSchemaSets {
62- protos , protosErr := fetchProtoFilesInFolders (ctx , client , protoSchemaSet .Owner , protoSchemaSet .Repository , protoSchemaSet .Ref , protoSchemaSet .Folders )
77+ if valErr := validateRepoConfiguration (protoSchemaSet ); valErr != nil {
78+ return errors .Wrapf (valErr , "invalid repo configuration for schema set: %v" , protoSchemaSet )
79+ }
80+ }
81+
82+ for _ , protoSchemaSet := range protoSchemaSets {
83+ protos , protosErr := fetchProtoFilesInFolders (ctx , client , protoSchemaSet .URI , protoSchemaSet .Ref , protoSchemaSet .Folders )
6384 if protosErr != nil {
64- return errors .Wrapf (protosErr , "failed to fetch protos from %s/%s " , protoSchemaSet .Owner , protoSchemaSet . Repository )
85+ return errors .Wrapf (protosErr , "failed to fetch protos from %s" , protoSchemaSet .URI )
6586 }
6687
6788 protoMap := make (map [string ]string )
@@ -71,7 +92,7 @@ func RegisterAndFetchProtos(ctx context.Context, client *github.Client, protoSch
7192 protoMap [proto .Path ] = proto .Content
7293
7394 var subjectStrategy SubjectNamingStrategyFn
74- if strategy , ok := repoToSubjectNamingStrategy [protoSchemaSet .Owner + "/" + protoSchemaSet . Repository ]; ok {
95+ if strategy , ok := repoToSubjectNamingStrategy [protoSchemaSet .URI ]; ok {
7596 subjectStrategy = strategy
7697 } else {
7798 subjectStrategy = DefaultSubjectNamingStrategy
@@ -86,7 +107,7 @@ func RegisterAndFetchProtos(ctx context.Context, client *github.Client, protoSch
86107
87108 registerErr := registerAllWithTopologicalSortingByTrial (schemaRegistryURL , protoMap , subjects )
88109 if registerErr != nil {
89- return errors .Wrapf (registerErr , "failed to register protos from %s/%s " , protoSchemaSet .Owner , protoSchemaSet . Repository )
110+ return errors .Wrapf (registerErr , "failed to register protos from %s" , protoSchemaSet .URI )
90111 }
91112 }
92113
@@ -142,8 +163,22 @@ func extractTopLevelMessageNamesWithRegex(protoSrc string) ([]string, error) {
142163}
143164
144165// Fetches .proto files from a GitHub repo optionally scoped to specific folders. It is recommended to use `*github.Client` with auth token to avoid rate limiting.
145- func fetchProtoFilesInFolders (ctx context.Context , client * github.Client , owner , repository , ref string , folders []string ) ([]protoFile , error ) {
146- framework .L .Debug ().Msgf ("Fetching proto files from %s/%s in folders: %s" , owner , repository , strings .Join (folders , ", " ))
166+ func fetchProtoFilesInFolders (ctx context.Context , client * github.Client , uri , ref string , folders []string ) ([]protoFile , error ) {
167+ framework .L .Debug ().Msgf ("Fetching proto files from %s in folders: %s" , uri , strings .Join (folders , ", " ))
168+
169+ if strings .HasPrefix (uri , "file://" ) {
170+ return fetchProtosFromFilesystem (uri , folders )
171+ }
172+
173+ parts := strings .Split (strings .TrimPrefix (uri , "https://" ), "/" )
174+
175+ return fetchProtosFromGithub (ctx , client , parts [1 ], parts [2 ], ref , folders )
176+ }
177+
178+ func fetchProtosFromGithub (ctx context.Context , client * github.Client , owner , repository , ref string , folders []string ) ([]protoFile , error ) {
179+ if client == nil {
180+ return nil , errors .New ("github client cannot be nil" )
181+ }
147182
148183 var files []protoFile
149184
@@ -210,7 +245,7 @@ searchLoop:
210245 })
211246 }
212247
213- framework .L .Debug ().Msgf ("Fetched %d proto files from %s/%s" , len (files ), owner , repository )
248+ framework .L .Debug ().Msgf ("Fetched %d proto files from Github's %s/%s" , len (files ), owner , repository )
214249
215250 if len (files ) == 0 {
216251 return nil , fmt .Errorf ("no proto files found in %s/%s in folders %s" , owner , repository , strings .Join (folders , ", " ))
@@ -219,6 +254,73 @@ searchLoop:
219254 return files , nil
220255}
221256
257+ func fetchProtosFromFilesystem (uri string , folders []string ) ([]protoFile , error ) {
258+ var files []protoFile
259+
260+ protoDirPath := strings .TrimPrefix (uri , "file://" )
261+ walkErr := filepath .Walk (protoDirPath , func (path string , info os.FileInfo , err error ) error {
262+ if err != nil {
263+ return err
264+ }
265+
266+ if info .IsDir () {
267+ return nil
268+ }
269+
270+ var folderFound string
271+ if len (folders ) > 0 {
272+ matched := false
273+ for _ , folder := range folders {
274+ if strings .HasPrefix (strings .TrimPrefix (strings .TrimPrefix (path , protoDirPath ), "/" ), folder ) {
275+ matched = true
276+ folderFound = folder
277+ break
278+ }
279+ }
280+
281+ if ! matched {
282+ return nil
283+ }
284+ }
285+
286+ if ! strings .HasSuffix (path , ".proto" ) {
287+ return nil
288+ }
289+
290+ content , contentErr := os .ReadFile (path )
291+ if contentErr != nil {
292+ return errors .Wrapf (contentErr , "failed to read file at %s" , path )
293+ }
294+
295+ // subtract the folder from the path if it was provided, because if it is imported by some other protos
296+ // most probably it will be imported as a relative path, so we need to remove the folder from the path
297+ protoPath := strings .TrimPrefix (strings .TrimPrefix (path , protoDirPath ), "/" )
298+ if folderFound != "" {
299+ protoPath = strings .TrimPrefix (strings .TrimPrefix (protoPath , folderFound ), strings .TrimSuffix (folderFound , "/" ))
300+ protoPath = strings .TrimPrefix (protoPath , "/" )
301+ }
302+
303+ files = append (files , protoFile {
304+ Name : filepath .Base (path ),
305+ Path : protoPath ,
306+ Content : string (content ),
307+ })
308+
309+ return nil
310+ })
311+ if walkErr != nil {
312+ return nil , errors .Wrapf (walkErr , "failed to walk through directory %s" , protoDirPath )
313+ }
314+
315+ framework .L .Debug ().Msgf ("Fetched %d proto files from local %s" , len (files ), protoDirPath )
316+
317+ if len (files ) == 0 {
318+ return nil , fmt .Errorf ("no proto files found in '%s' in folders %s" , protoDirPath , strings .Join (folders , ", " ))
319+ }
320+
321+ return files , nil
322+ }
323+
222324func resolveRefSHA (ctx context.Context , client * github.Client , owner , repository , ref string ) (string , error ) {
223325 if refObj , _ , err := client .Git .GetRef (ctx , owner , repository , "refs/tags/" + ref ); err == nil {
224326 return refObj .GetObject ().GetSHA (), nil
0 commit comments