├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── iter.go ├── iter_test.go └── link.go /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Enrico Candino 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gh-iter 2 | 3 | > **Note:** this package leverages the new [`iter`](https://pkg.go.dev/iter) package, and it needs Go [`1.23`](https://go.dev/dl/#go1.23). 4 | 5 | The `gh-iter` package provides an iterator that can be used with the [`google/go-github`](https://github.com/google/go-github) client. 6 | It supports automatic pagination with generic types. 7 | 8 | ## Quickstart 9 | 10 | ```go 11 | package main 12 | 13 | import ( 14 | "fmt" 15 | 16 | ghiter "github.com/enrichman/gh-iter" 17 | "github.com/google/go-github/v69/github" 18 | ) 19 | 20 | func main() { 21 | // init your Github client 22 | client := github.NewClient(nil) 23 | 24 | // create an iterator, and start looping! 🎉 25 | users := ghiter.NewFromFn(client.Users.ListAll) 26 | for u := range users.All() { 27 | fmt.Println(*u.Login) 28 | } 29 | 30 | // check if the loop stopped because of an error 31 | if err := users.Err(); err != nil { 32 | // something happened :( 33 | panic(err) 34 | } 35 | } 36 | ``` 37 | 38 | ## Usage 39 | 40 | Depending of the API you need to use you can create an iterator from one of the three provided constructor: 41 | 42 | ### No args 43 | 44 | ```go 45 | ghiter.NewFromFn(client.Users.ListAll) 46 | ``` 47 | 48 | ### One string arg 49 | 50 | ```go 51 | ghiter.NewFromFn1(client.Repositories.ListByUser, "enrichman") 52 | ``` 53 | 54 | ### Two string args 55 | 56 | ```go 57 | ghiter.NewFromFn2(client.Issues.ListByRepo, "enrichman", "gh-iter") 58 | ``` 59 | 60 | Then you can simply loop through the objects with the `All()` method. 61 | 62 | 63 | ### Customize options 64 | 65 | You can tweak the iteration providing your own options. They will be updated during the loop. 66 | 67 | For example if you want to request only 5 repositories per request: 68 | 69 | ```go 70 | ghiter.NewFromFn1(client.Repositories.ListByUser, "enrichman"). 71 | Opts(&github.RepositoryListByUserOptions{ 72 | ListOptions: github.ListOptions{PerPage: 5}, 73 | }) 74 | ``` 75 | 76 | ### Context 77 | 78 | If you don't provide a context with the `Ctx()` func an empty `context.Background` will be used. You can use a custom context to have a more granular control, for example if you want to close the iteration from a timeout, or with a manual cancellation. 79 | 80 | You can check if the int 81 | 82 | 83 | ```go 84 | ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) 85 | 86 | repos := ghiter.NewFromFn1(client.Repositories.ListByUser, "enrichman").Ctx(ctx) 87 | for repo := range repos.All() { 88 | if *repo.Name == "myrepo" { 89 | fmt.Println(*repo.Name) 90 | cancel() 91 | } 92 | } 93 | ``` 94 | 95 | ## Advanced usage 96 | 97 | Some APIs do not match the "standard" string arguments, or the returned type is not an array. In these cases you can still use this package, but you will need to provide a "custom func" to the `ghiter.NewFromFn` constructor. 98 | 99 | For example the [`client.Teams.ListTeamReposByID`](https://pkg.go.dev/github.com/google/go-github/v69/github#TeamsService.ListTeamReposByID) needs the `orgID, teamID int64` arguments: 100 | 101 | ```go 102 | repos := ghiter.NewFromFn(func(ctx context.Context, opts *github.ListOptions) ([]*github.Repository, *github.Response, error) { 103 | return client.Teams.ListTeamReposByID(ctx, 123, 456, opts) 104 | }) 105 | ``` 106 | 107 | In case the returned object is not an array you will have to "unwrap" it. 108 | For example the [`client.Teams.ListIDPGroupsInOrganization`](https://pkg.go.dev/github.com/google/go-github/v69/github#TeamsService.ListIDPGroupsInOrganization) returns a [IDPGroupList](https://pkg.go.dev/github.com/google/go-github/v69/github#IDPGroupList), and not a slice. 109 | 110 | ```go 111 | idpGroups := ghiter.NewFromFn(func(ctx context.Context, opts *github.ListCursorOptions) ([]*github.IDPGroup, *github.Response, error) { 112 | groups, resp, err := client.Teams.ListIDPGroupsInOrganization(ctx, "myorg", opts) 113 | // remember to check for nil! 114 | if groups != nil { 115 | return groups.Groups, resp, err 116 | } 117 | return nil, resp, err 118 | }) 119 | ``` 120 | 121 | 122 | # Feedback 123 | 124 | If you like the project please star it on Github 🌟, and feel free to drop me a note, or [open an issue](https://github.com/enrichman/gh-iter/issues/new)! 125 | 126 | [Twitter](https://twitter.com/enrichmann) 127 | 128 | # License 129 | 130 | [MIT](LICENSE) 131 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/enrichman/gh-iter/v69 2 | 3 | go 1.23 4 | 5 | require github.com/google/go-github/v69 v69.0.0 6 | 7 | require github.com/google/go-querystring v1.1.0 // indirect 8 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 2 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 3 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 4 | github.com/google/go-github/v69 v69.0.0 h1:YnFvZ3pEIZF8KHmI8xyQQe3mYACdkhnaTV2hr7CP2/w= 5 | github.com/google/go-github/v69 v69.0.0/go.mod h1:xne4jymxLR6Uj9b7J7PyTpkMYstEMMwGZa0Aehh1azM= 6 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= 7 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= 8 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 9 | -------------------------------------------------------------------------------- /iter.go: -------------------------------------------------------------------------------- 1 | package ghiter 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "iter" 8 | "net/url" 9 | "reflect" 10 | "strconv" 11 | "strings" 12 | "time" 13 | 14 | "github.com/google/go-github/v69/github" 15 | ) 16 | 17 | type Iterator[T, O any] struct { 18 | opt O 19 | 20 | ctx context.Context 21 | args []string 22 | 23 | fn func(ctx context.Context, opt O) ([]T, *github.Response, error) 24 | fn1 func(ctx context.Context, arg1 string, opt O) ([]T, *github.Response, error) 25 | fn2 func(ctx context.Context, arg1, arg2 string, opt O) ([]T, *github.Response, error) 26 | 27 | raw *github.Response 28 | err error 29 | } 30 | 31 | func NewFromFn[T, O any]( 32 | fn func(ctx context.Context, opt O) ([]T, *github.Response, error), 33 | ) *Iterator[T, O] { 34 | return &Iterator[T, O]{ 35 | fn: fn, 36 | } 37 | } 38 | 39 | func NewFromFn1[T, O any]( 40 | fn1 func(ctx context.Context, arg1 string, opt O) ([]T, *github.Response, error), 41 | arg1 string, 42 | ) *Iterator[T, O] { 43 | return &Iterator[T, O]{ 44 | fn1: fn1, 45 | args: []string{arg1}, 46 | } 47 | } 48 | 49 | func NewFromFn2[T, O any]( 50 | fn2 func(ctx context.Context, arg1, arg2 string, opt O) ([]T, *github.Response, error), 51 | arg1 string, 52 | arg2 string, 53 | ) *Iterator[T, O] { 54 | return &Iterator[T, O]{ 55 | fn2: fn2, 56 | args: []string{arg1, arg2}, 57 | } 58 | } 59 | 60 | func (it *Iterator[T, O]) Ctx(ctx context.Context) *Iterator[T, O] { 61 | it.ctx = ctx 62 | return it 63 | } 64 | 65 | func (it *Iterator[T, O]) Args(args ...string) *Iterator[T, O] { 66 | it.args = args 67 | return it 68 | } 69 | 70 | func (it *Iterator[T, O]) Opts(opt O) *Iterator[T, O] { 71 | it.opt = opt 72 | return it 73 | } 74 | 75 | func (it *Iterator[T, O]) Raw() *github.Response { 76 | return it.raw 77 | } 78 | 79 | func (it *Iterator[T, O]) Err() error { 80 | return it.err 81 | } 82 | 83 | func (it *Iterator[T, O]) All() iter.Seq[T] { 84 | initialize(it) 85 | 86 | return func(yield func(T) bool) { 87 | if err := validate(it); err != nil { 88 | it.err = err 89 | return 90 | } 91 | 92 | for { 93 | parts, resp, err := it.do() 94 | it.raw = resp 95 | 96 | if err != nil { 97 | it.err = err 98 | return 99 | } 100 | 101 | // push the result until yield, or an error occurs 102 | for _, p := range parts { 103 | if !yield(p) || contextErr(it) { 104 | return 105 | } 106 | } 107 | 108 | // no more results, break 109 | if resp.NextPage == 0 { 110 | return 111 | } 112 | 113 | // get the next page from the link header 114 | links := ParseLinkHeader(resp.Header.Get("link")) 115 | if next, found := links.FindByRel("next"); found { 116 | nextURL, err := url.Parse(next.URL) 117 | if err != nil { 118 | it.err = err 119 | return 120 | } 121 | 122 | vals := make(map[string]string) 123 | for k, v := range nextURL.Query() { 124 | vals[k] = v[0] 125 | } 126 | 127 | if err := updateOptions(it.opt, vals); err != nil { 128 | it.err = err 129 | return 130 | } 131 | } 132 | } 133 | } 134 | } 135 | 136 | func initialize[T, O any](it *Iterator[T, O]) { 137 | // initialize context if nil 138 | if it.ctx == nil { 139 | it.ctx = context.Background() 140 | } 141 | 142 | // initialize options if nil 143 | if reflect.ValueOf(it.opt).IsNil() { 144 | optionPointerType := reflect.TypeOf(it.opt) 145 | optionValue := reflect.New(optionPointerType.Elem()) 146 | if opt, ok := optionValue.Interface().(O); ok { 147 | it.opt = opt 148 | } 149 | } 150 | } 151 | 152 | func validate[T, O any](it *Iterator[T, O]) error { 153 | if it.fn == nil && it.fn1 == nil && it.fn2 == nil { 154 | return errors.New("no func provided") 155 | } 156 | 157 | numOfArgs := len(it.args) 158 | if it.fn1 != nil { 159 | if numOfArgs != 1 { 160 | args := strings.Join(it.args, ",") 161 | return fmt.Errorf("wrong number of arguments: expected 1, got %d [%s]", numOfArgs, args) 162 | } 163 | 164 | if it.args[0] == "" { 165 | return errors.New("empty argument[0]") 166 | } 167 | } 168 | 169 | if it.fn2 != nil { 170 | if numOfArgs != 2 { 171 | args := strings.Join(it.args, ",") 172 | return fmt.Errorf("wrong number of arguments: expected 2, got %d [%s]", numOfArgs, args) 173 | } 174 | 175 | if it.args[0] == "" { 176 | return errors.New("empty argument[0]") 177 | } 178 | 179 | if it.args[1] == "" { 180 | return errors.New("empty argument[1]") 181 | } 182 | } 183 | 184 | return nil 185 | } 186 | 187 | func contextErr[T, O any](it *Iterator[T, O]) bool { 188 | if err := it.ctx.Err(); err != nil { 189 | it.err = err 190 | return true 191 | } 192 | return false 193 | } 194 | 195 | func (it *Iterator[T, O]) do() ([]T, *github.Response, error) { 196 | if it.fn != nil { 197 | return it.fn(it.ctx, it.opt) 198 | } else if it.fn1 != nil { 199 | return it.fn1(it.ctx, it.args[0], it.opt) 200 | } else if it.fn2 != nil { 201 | return it.fn2(it.ctx, it.args[0], it.args[1], it.opt) 202 | } 203 | 204 | return nil, nil, errors.New("no func provided") 205 | } 206 | 207 | var ( 208 | stringTypePtr *string 209 | intTypePtr *int 210 | int64TypePtr *int64 211 | boolTypePtr *bool 212 | ) 213 | 214 | // updateOptions will update the github options based on the provided map and the `url` tag. 215 | // If the field in the struct has a `url` tag it tries to set the value of the field from the one 216 | // found in the map, if any. 217 | func updateOptions(v any, m map[string]string) error { 218 | valueOf := reflect.ValueOf(v) 219 | typeOf := reflect.TypeOf(v) 220 | 221 | if valueOf.Kind() == reflect.Pointer { 222 | valueOf = valueOf.Elem() 223 | typeOf = typeOf.Elem() 224 | } 225 | 226 | for i := 0; i < valueOf.NumField(); i++ { 227 | structField := typeOf.Field(i) 228 | fieldValue := valueOf.Field(i) 229 | 230 | // if field is of type struct then iterate over the pointer 231 | if structField.Type.Kind() == reflect.Struct { 232 | if fieldValue.CanAddr() { 233 | if err := updateOptions(fieldValue.Addr().Interface(), m); err != nil { 234 | return err 235 | } 236 | } 237 | } 238 | 239 | // otherwise check if it has a 'url' tag 240 | urlTag := structField.Tag.Get("url") 241 | if urlTag == "" { 242 | continue 243 | } 244 | 245 | if !fieldValue.IsValid() || !fieldValue.CanSet() { 246 | continue 247 | } 248 | 249 | urlParam := strings.Split(urlTag, ",")[0] 250 | v, found := m[urlParam] 251 | if !found { 252 | continue 253 | } 254 | 255 | switch fieldValue.Kind() { 256 | 257 | // handle string 258 | case reflect.String: 259 | fieldValue.Set(reflect.ValueOf(v)) 260 | 261 | // handle numeric types (int, int8, int16, int32, int64) 262 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 263 | if i, err := strconv.Atoi(v); err == nil { 264 | fieldValue.SetInt(int64(i)) 265 | } 266 | 267 | // handle bool 268 | case reflect.Bool: 269 | parsedBool, err := strconv.ParseBool(v) 270 | if err != nil { 271 | return fmt.Errorf("error while parsing string '%s' as bool: %s", v, err) 272 | } 273 | fieldValue.Set(reflect.ValueOf(parsedBool)) 274 | 275 | // handle pointers (*string, *int, *int64, *bool, *time.Time) 276 | case reflect.Pointer: 277 | switch fieldValue.Type() { 278 | 279 | // handle *string 280 | case reflect.TypeOf(stringTypePtr): 281 | fieldValue.Set(reflect.ValueOf(&v)) 282 | 283 | // handle *int 284 | case reflect.TypeOf(intTypePtr): 285 | parsedInt, err := strconv.Atoi(v) 286 | if err != nil { 287 | return fmt.Errorf("error while parsing string '%s' as int: %s", v, err) 288 | } 289 | fieldValue.Set(reflect.ValueOf(&parsedInt)) 290 | 291 | // handle *int64 292 | case reflect.TypeOf(int64TypePtr): 293 | parsedInt64, err := strconv.ParseInt(v, 10, 64) 294 | if err != nil { 295 | return fmt.Errorf("error while parsing string '%s' as int64: %s", v, err) 296 | } 297 | fieldValue.Set(reflect.ValueOf(&parsedInt64)) 298 | 299 | // handle *bool 300 | case reflect.TypeOf(boolTypePtr): 301 | parsedBool, err := strconv.ParseBool(v) 302 | if err != nil { 303 | return fmt.Errorf("error while parsing string '%s' as bool: %s", v, err) 304 | } 305 | fieldValue.Set(reflect.ValueOf(&parsedBool)) 306 | 307 | // handle *time.Time 308 | case reflect.TypeOf(&time.Time{}): 309 | layout := time.RFC3339 310 | if len(v) == len(time.DateOnly) { 311 | layout = time.DateOnly 312 | } 313 | 314 | result, err := time.Parse(layout, v) 315 | if err != nil { 316 | return fmt.Errorf("error while parsing string '%s' as time.Time: %s", v, err) 317 | } 318 | 319 | fieldValue.Set(reflect.ValueOf(&result)) 320 | 321 | default: 322 | return fmt.Errorf("cannot set '%s' value to unknown pointer of '%s'", v, fieldValue.Type()) 323 | } 324 | 325 | case reflect.Struct: 326 | // handle time.Time 327 | if fieldValue.Type() == reflect.TypeOf(time.Time{}) { 328 | layout := time.RFC3339 329 | if len(v) == len(time.DateOnly) { 330 | layout = time.DateOnly 331 | } 332 | 333 | result, err := time.Parse(layout, v) 334 | if err != nil { 335 | return fmt.Errorf("error while parsing string '%s' as time.Time: %s", v, err) 336 | } 337 | 338 | fieldValue.Set(reflect.ValueOf(result)) 339 | } else { 340 | return fmt.Errorf("cannot set '%s' value to unknown struct '%s'", v, fieldValue.Type()) 341 | } 342 | 343 | default: 344 | return fmt.Errorf("cannot set '%s' value to unknown type '%s'", v, fieldValue.Type()) 345 | } 346 | } 347 | 348 | return nil 349 | } 350 | -------------------------------------------------------------------------------- /iter_test.go: -------------------------------------------------------------------------------- 1 | package ghiter 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | "time" 7 | 8 | "github.com/google/go-github/v69/github" 9 | ) 10 | 11 | func Test_updateOptions(t *testing.T) { 12 | tt := []struct { 13 | name string 14 | opts any 15 | queryParams map[string]string 16 | expectedOpts any 17 | expectedErr bool 18 | }{ 19 | { 20 | name: "Simple Opts with ListOptions", 21 | opts: &github.RepositoryListByOrgOptions{}, 22 | queryParams: map[string]string{ 23 | "page": "1123323", 24 | }, 25 | expectedOpts: &github.RepositoryListByOrgOptions{ 26 | ListOptions: github.ListOptions{ 27 | Page: 1123323, 28 | }, 29 | }, 30 | }, 31 | { 32 | name: "Simple Opts with ListOptions with multiple params", 33 | opts: &github.RepositoryListByOrgOptions{}, 34 | queryParams: map[string]string{ 35 | "page": "1123323", 36 | "direction": "desc", 37 | }, 38 | expectedOpts: &github.RepositoryListByOrgOptions{ 39 | Direction: "desc", 40 | ListOptions: github.ListOptions{ 41 | Page: 1123323, 42 | }, 43 | }, 44 | }, 45 | { 46 | name: "Opts with Since", 47 | opts: &github.RepositoryListAllOptions{}, 48 | queryParams: map[string]string{ 49 | "page": "1123323", 50 | "direction": "desc", 51 | "since": "111", 52 | }, 53 | expectedOpts: &github.RepositoryListAllOptions{ 54 | Since: 111, 55 | }, 56 | }, 57 | { 58 | name: "Opts with pointers and multiple ListOptions", 59 | opts: &github.ListAlertsOptions{}, 60 | queryParams: map[string]string{ 61 | "page": "1123323", 62 | "direction": "desc", 63 | "since": "111", 64 | "sort": "", 65 | }, 66 | expectedOpts: &github.ListAlertsOptions{ 67 | Direction: func() *string { 68 | direction := "desc" 69 | return &direction 70 | }(), 71 | Sort: func() *string { 72 | var sort string 73 | return &sort 74 | }(), 75 | ListOptions: github.ListOptions{ 76 | Page: 1123323, 77 | }, 78 | ListCursorOptions: github.ListCursorOptions{ 79 | Page: "1123323", 80 | }, 81 | }, 82 | }, 83 | { 84 | name: "date RFC parse", 85 | opts: &github.IssueListCommentsOptions{}, 86 | queryParams: map[string]string{ 87 | "since": "1989-10-02T00:00:00Z", 88 | }, 89 | expectedOpts: &github.IssueListCommentsOptions{ 90 | Since: func() *time.Time { 91 | since := time.Date(1989, time.October, 2, 0, 0, 0, 0, time.UTC) 92 | return &since 93 | }(), 94 | }, 95 | }, 96 | { 97 | name: "date DateTime parse", 98 | opts: &github.IssueListCommentsOptions{}, 99 | queryParams: map[string]string{ 100 | "since": "1989-10-02", 101 | }, 102 | expectedOpts: &github.IssueListCommentsOptions{ 103 | Since: func() *time.Time { 104 | since := time.Date(1989, time.October, 2, 0, 0, 0, 0, time.UTC) 105 | return &since 106 | }(), 107 | }, 108 | }, 109 | { 110 | name: "wrong Date", 111 | opts: &github.IssueListCommentsOptions{}, 112 | queryParams: map[string]string{ 113 | "since": "1923230Z", 114 | }, 115 | expectedErr: true, 116 | }, 117 | { 118 | name: "Opts with pointers and multiple ListOptions", 119 | opts: &github.CommitsListOptions{}, 120 | queryParams: map[string]string{ 121 | "since": "1989-10-02T00:00:00Z", 122 | }, 123 | expectedOpts: &github.CommitsListOptions{ 124 | Since: time.Date(1989, time.October, 2, 0, 0, 0, 0, time.UTC), 125 | }, 126 | }, 127 | { 128 | name: "Opts with bool", 129 | opts: &github.ListWorkflowRunsOptions{}, 130 | queryParams: map[string]string{ 131 | "exclude_pull_requests": "true", 132 | }, 133 | expectedOpts: &github.ListWorkflowRunsOptions{ 134 | ExcludePullRequests: true, 135 | }, 136 | }, 137 | { 138 | name: "Opts with bool pointers", 139 | opts: &github.WorkflowRunAttemptOptions{}, 140 | queryParams: map[string]string{ 141 | "exclude_pull_requests": "true", 142 | }, 143 | expectedOpts: &github.WorkflowRunAttemptOptions{ 144 | ExcludePullRequests: func() *bool { 145 | exclude := true 146 | return &exclude 147 | }(), 148 | }, 149 | }, 150 | { 151 | name: "Opts with int pointers", 152 | opts: &github.ListSCIMProvisionedIdentitiesOptions{}, 153 | queryParams: map[string]string{ 154 | "count": "12", 155 | }, 156 | expectedOpts: &github.ListSCIMProvisionedIdentitiesOptions{ 157 | Count: func() *int { 158 | count := 12 159 | return &count 160 | }(), 161 | }, 162 | }, 163 | { 164 | name: "Opts with int64 pointers", 165 | opts: &github.ListCheckRunsOptions{}, 166 | queryParams: map[string]string{ 167 | "app_id": "12", 168 | }, 169 | expectedOpts: &github.ListCheckRunsOptions{ 170 | AppID: func() *int64 { 171 | count := int64(12) 172 | return &count 173 | }(), 174 | }, 175 | }, 176 | } 177 | 178 | for _, tc := range tt { 179 | err := updateOptions(tc.opts, tc.queryParams) 180 | 181 | if tc.expectedErr { 182 | if err == nil { 183 | t.Fatal("missing expected err\n\n") 184 | } 185 | continue 186 | } 187 | 188 | if !reflect.DeepEqual(tc.expectedOpts, tc.opts) { 189 | t.Fatalf("structs are not equal:\nexpected:\t%+v\ngot:\t\t%+v\n", tc.expectedOpts, tc.opts) 190 | } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /link.go: -------------------------------------------------------------------------------- 1 | package ghiter 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | type Links []Link 8 | 9 | func (l Links) FindByRel(rel string) (Link, bool) { 10 | for _, link := range l { 11 | if link.Rel == rel { 12 | return link, true 13 | } 14 | } 15 | return Link{}, false 16 | } 17 | 18 | type Link struct { 19 | URL string 20 | Rel string 21 | Params map[string]string 22 | } 23 | 24 | func ParseLinkHeader(header string) Links { 25 | var links Links 26 | 27 | header = strings.TrimSpace(header) 28 | rawLinks := strings.Split(header, ",") 29 | for _, l := range rawLinks { 30 | links = append(links, parseLink(l)) 31 | } 32 | 33 | return links 34 | } 35 | 36 | func parseLink(header string) Link { 37 | header = strings.TrimSpace(header) 38 | 39 | attrs := strings.Split(header, ";") 40 | 41 | rawURL := attrs[0] 42 | rawURL = strings.TrimSpace(rawURL) 43 | rawURL = strings.Trim(rawURL, "<>") 44 | 45 | link := Link{ 46 | URL: rawURL, 47 | Params: map[string]string{}, 48 | } 49 | 50 | for i := 1; i < len(attrs); i++ { 51 | attr := strings.TrimSpace(attrs[i]) 52 | keyVal := strings.Split(attr, "=") 53 | if len(keyVal) > 1 { 54 | link.Params[keyVal[0]] = strings.Trim(keyVal[1], `"`) 55 | } 56 | } 57 | link.Rel = link.Params["rel"] 58 | 59 | return link 60 | } 61 | --------------------------------------------------------------------------------