Skip to content

Commit 7a388ca

Browse files
authored
docs: Update CONTRIBUTING.md (#4341)
1 parent aa311aa commit 7a388ca

1 file changed

Lines changed: 152 additions & 62 deletions

File tree

CONTRIBUTING.md

Lines changed: 152 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,6 @@ And the returned type `Repository` will have comments like this:
180180
// Repository represents a GitHub repository.
181181
type Repository struct {
182182
ID *int64 `json:"id,omitempty"`
183-
NodeID *string `json:"node_id,omitempty"`
184183
Owner *User `json:"owner,omitempty"`
185184
// ...
186185
}
@@ -222,6 +221,28 @@ For example, methods defined at <https://docs.github.com/en/rest/webhooks/repos>
222221

223222
[GitHub API documentation]: https://docs.github.com/en/rest
224223

224+
### Services
225+
226+
The API is split into services, one per logical area of the GitHub API
227+
(e.g. `RepositoriesService`, `UsersService`). Each service is a named type
228+
over the shared `service` struct, which holds a back-reference to the `Client`:
229+
230+
```go
231+
type service struct {
232+
client *Client
233+
}
234+
235+
type RepositoriesService service
236+
```
237+
238+
All services share a single `service` value embedded in the `Client` as
239+
`common`, so adding a service does not allocate. To add a new service:
240+
241+
1. Add a field for it on the `Client` struct, keeping the list alphabetical
242+
2. Wire it up in `newClient` (alongside the other `c.X = (*XService)(&c.common)` lines, also kept alphabetical).
243+
3. Declare the service type in the service's file (e.g. `repos.go`) and hang the
244+
methods off it, using `s` as the receiver (see [Receiver Names](#receiver-names)).
245+
225246
### Naming Conventions
226247

227248
#### Receiver Names
@@ -263,6 +284,7 @@ Common method name prefixes:
263284
- `result` - Result from API call
264285
- `err` - Error
265286
- `opts` - Options parameter
287+
- `body` - Body parameter
266288
- `owner` - Repository owner (username or organization)
267289
- `repo` - Repository name
268290
- `org` - Organization name
@@ -283,30 +305,45 @@ See [#3644][] and [#3887][] for background discussion.
283305
[#3644]: https://github.com/google/go-github/pull/3644
284306
[#3887]: https://github.com/google/go-github/pull/3887
285307

308+
#### Creating Pointers
309+
310+
Pointer fields are common because many request and response fields are
311+
optional. Use the generic `Ptr` helper to take the address of a literal
312+
instead of declaring an intermediate variable:
313+
314+
```go
315+
repo := &Repository{
316+
Name: Ptr("go-github"),
317+
Private: Ptr(true),
318+
ID: Ptr(int64(1)),
319+
}
320+
```
321+
286322
#### Request Option Types
287323

288-
Request option types (for query parameters) are named after the method they
324+
Request option types should be used for query parameters and named after the method they
289325
modify, with the suffix `Options`:
290326

291327
```go
292328
type RepositoryListOptions struct {
293-
Type string `url:"type,omitempty"`
294-
Sort string `url:"sort,omitempty"`
329+
Type string `url:"type,omitempty"`
330+
Sort string `url:"sort,omitempty"`
295331
Direction string `url:"direction,omitempty"`
296332
ListOptions
297333
}
298334
```
299335

300336
#### Request Body Types
301337

302-
Request body types (for POST/PUT/PATCH requests) should use the `Request`
338+
Request body types for POST/PUT/PATCH requests should use the `Request`
303339
suffix for create and update operations:
304340

305341
```go
306-
type CreateSelfHostedRunnerGroupRequest struct {
307-
Name string `json:"name"`
308-
RunnerGroupId int64 `json:"runner_group_id"`
309-
Visibility string `json:"visibility,omitempty"`
342+
type CreateHostedRunnerRequest struct {
343+
Name string `json:"name"`
344+
RunnerGroupID int64 `json:"runner_group_id"`
345+
MaximumRunners *int64 `json:"maximum_runners,omitempty"`
346+
// ...
310347
}
311348
```
312349

@@ -317,9 +354,9 @@ any suffix:
317354

318355
```go
319356
type Repository struct {
320-
ID *int64 `json:"id,omitempty"`
321-
Name *string `json:"name,omitempty"`
322-
FullName *string `json:"full_name,omitempty"`
357+
ID *int64 `json:"id,omitempty"`
358+
Name *string `json:"name,omitempty"`
359+
FullName *string `json:"full_name,omitempty"`
323360
Description *string `json:"description,omitempty"`
324361
// ...
325362
}
@@ -336,31 +373,34 @@ type Repository struct {
336373

337374
#### Request Bodies
338375

339-
Required fields should be non-pointer types without `omitempty`:
376+
Required fields should be non-pointer types without `omitempty`.
377+
Optional fields should be pointer types with `omitempty`.
378+
Use `omitzero` for structs and `time.Time` where you want to omit
379+
empty values (not just nil). For slices and maps, `omitzero` has the
380+
opposite behavior: it keeps empty (non-nil) values and only omits nil
381+
values.
340382

341383
```go
342-
type SecretScanningAlertUpdateOptions struct {
343-
State string `json:"state"`
344-
// ...
345-
}
346-
```
384+
type RepositoryRuleset struct {
385+
// ID is optional.
386+
ID *int64 `json:"id,omitempty"`
347387

348-
Optional fields should be pointer types with `omitempty`:
388+
// Name is required.
389+
Name string `json:"name"`
349390

350-
```go
351-
type SecretScanningAlertUpdateOptions struct {
352-
State string `json:"state"`
353-
Resolution *string `json:"resolution,omitempty"`
354-
ResolutionComment *string `json:"resolution_comment,omitempty"`
391+
// Target is optional struct.
392+
Target *RulesetTarget `json:"target,omitempty"`
393+
394+
// BypassActors is optional.
395+
BypassActors []*BypassActor `json:"bypass_actors,omitzero"`
396+
397+
// CreatedAt is optional.
398+
CreatedAt *Timestamp `json:"created_at,omitempty"`
399+
400+
// ...
355401
}
356402
```
357403

358-
Use `omitzero` for structs and `time.Time` where you want to omit
359-
empty values (not just nil). For slices and maps, `omitzero` has the
360-
opposite behavior: it keeps empty (non-nil) values and only omits nil
361-
values. See `RepositoryRuleset` for examples of `omitzero` usage with
362-
both slices and structs.
363-
364404
For optional boolean fields where you need to distinguish between `false`
365405
and "not set", use `*bool` with `omitzero`.
366406

@@ -369,22 +409,17 @@ and "not set", use `*bool` with `omitzero`.
369409
Follow the same conventions as request bodies for `omitempty` and
370410
`omitzero` usage. Optional fields should use pointer types with
371411
`omitempty`, and required fields should prefer non-pointer types.
372-
See [Common Types](#common-types) for conventions on ID, Node ID,
373-
Timestamp, and Boolean fields.
412+
See [Common Types](#common-types) for conventions on ID, Node ID, and Timestamp.
374413

375414
### URL Tags for Query Parameters
376415

377416
All fields should use `url` tags with `omitempty` to omit zero values
378417
from the query string:
379418

380419
```go
381-
type RepositoryContentGetOptions struct {
382-
Ref string `url:"ref,omitempty"`
383-
}
384-
385420
type RepositoryListOptions struct {
386-
Type string `url:"type,omitempty"`
387-
Sort string `url:"sort,omitempty"`
421+
Type string `url:"type,omitempty"`
422+
Sort string `url:"sort,omitempty"`
388423
Direction string `url:"direction,omitempty"`
389424
ListOptions
390425
}
@@ -400,7 +435,7 @@ Use `ListOptions` for APIs that use page/per_page parameters:
400435

401436
```go
402437
type ListOptions struct {
403-
Page int `url:"page,omitempty"`
438+
Page int `url:"page,omitempty"`
404439
PerPage int `url:"per_page,omitempty"`
405440
}
406441
```
@@ -411,13 +446,13 @@ Use `ListCursorOptions` for APIs that use cursor-based pagination:
411446

412447
```go
413448
type ListCursorOptions struct {
414-
Page string `url:"page,omitempty"`
415-
PerPage int `url:"per_page,omitempty"`
416-
First int `url:"first,omitempty"`
417-
Last int `url:"last,omitempty"`
418-
After string `url:"after,omitempty"`
419-
Before string `url:"before,omitempty"`
420-
Cursor string `url:"cursor,omitempty"`
449+
Page string `url:"page,omitempty"`
450+
PerPage int `url:"per_page,omitempty"`
451+
First int `url:"first,omitempty"`
452+
Last int `url:"last,omitempty"`
453+
After string `url:"after,omitempty"`
454+
Before string `url:"before,omitempty"`
455+
Cursor string `url:"cursor,omitempty"`
421456
}
422457
```
423458

@@ -436,32 +471,23 @@ in `gen-iterators.go` can be adjusted — including `useCursorPagination`,
436471

437472
#### ID Fields
438473

439-
GitHub API IDs are always `int64`. In request bodies, use non-pointer `int64`
440-
if the ID is required and `*int64` if the ID is optional. In response bodies,
441-
use non-pointer `int64` if the ID is required and `*int64` with `omitempty`
442-
if the ID is optional:
474+
GitHub API IDs are usually `int64`. Use non-pointer `int64`
475+
if the ID is required and `*int64` if the ID is optional.
443476

444477
```go
445-
// Request body — required ID
446478
type CreateHostedRunnerRequest struct {
447479
RunnerGroupID int64 `json:"runner_group_id"`
448-
}
449-
450-
// Response body — optional ID
451-
type Repository struct {
452-
ID *int64 `json:"id,omitempty"`
453480
// ...
454481
}
455482
```
456483

457484
#### Node ID Fields
458485

459-
Node IDs are always `string`. In response bodies, `NodeID` is typically
460-
required and should use a non-pointer type:
486+
Node IDs are usually `string`:
461487

462488
```go
463-
type Repository struct {
464-
NodeID *string `json:"node_id,omitempty"`
489+
type IssueFieldValue struct {
490+
NodeID string `json:"node_id"`
465491
// ...
466492
}
467493
```
@@ -473,12 +499,76 @@ Use the `Timestamp` type for all date/time fields:
473499
```go
474500
type Repository struct {
475501
CreatedAt *Timestamp `json:"created_at,omitempty"`
476-
UpdatedAt *Timestamp `json:"updated_at,omitempty"`
477-
PushedAt *Timestamp `json:"pushed_at,omitempty"`
478502
// ...
479503
}
480504
```
481505

506+
### Generated Code
507+
508+
Some files are generated and must never be edited by hand.
509+
When you add or change a struct, run `script/generate.sh` to regenerate them.
510+
511+
So after adding a field you typically only write the struct field itself;
512+
the accessor and stringify code follow from `script/generate.sh`.
513+
`script/lint.sh` will fail if these files are out of date.
514+
Documentation links and `//meta:operation` directives are updated
515+
by the same script (see [Code Comments](#code-comments)).
516+
517+
### Testing
518+
519+
Tests use a real `httptest` server rather than mocks. Call `setup` to get a
520+
`Client` pointed at a test server plus the `mux` to register handlers on, then
521+
assert on the request and the decoded response. A typical test looks like this:
522+
523+
```go
524+
func TestRepositoriesService_GetByID(t *testing.T) {
525+
t.Parallel()
526+
client, mux, _ := setup(t)
527+
528+
mux.HandleFunc("/repositories/1", func(w http.ResponseWriter, r *http.Request) {
529+
testMethod(t, r, "GET")
530+
fmt.Fprint(w, `{"id":1,"name":"n"}`)
531+
})
532+
533+
ctx := t.Context()
534+
got, _, err := client.Repositories.GetByID(ctx, 1)
535+
if err != nil {
536+
t.Fatalf("Repositories.GetByID returned error: %v", err)
537+
}
538+
539+
want := &Repository{ID: Ptr(int64(1)), Name: Ptr("n")}
540+
if !cmp.Equal(got, want) {
541+
t.Errorf("Repositories.GetByID returned %+v, want %+v", got, want)
542+
}
543+
544+
const methodName = "GetByID"
545+
testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
546+
got, resp, err := client.Repositories.GetByID(ctx, 1)
547+
if got != nil {
548+
t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got)
549+
}
550+
return resp, err
551+
})
552+
}
553+
```
554+
555+
Conventions to follow:
556+
557+
- Call `t.Parallel()` and use `t.Context()` for the request context.
558+
- Register handlers on `mux` with relative paths (a leading `/`); the test
559+
server fails requests built from absolute URLs to catch that mistake.
560+
- Use the shared helpers instead of reimplementing assertions:
561+
- `testMethod` - asserts the HTTP method.
562+
- `testFormValues` - asserts query parameters.
563+
- `testHeader` - asserts a request header.
564+
- `testJSONBody` / `testPlainBody` - assert the request body.
565+
- `testNewRequestAndDoFailure` - exercises the request-building and
566+
request-doing error paths for a method.
567+
- `testBadOptions` - asserts that invalid options return an error.
568+
- Use `testJSONMarshal` to verify a type round-trips to and from the expected JSON.
569+
- Compare values with `cmp.Equal` and construct optional fields with `Ptr`
570+
(see [Creating Pointers](#creating-pointers)).
571+
482572
## Metadata
483573

484574
GitHub publishes [OpenAPI descriptions of their API][]. We use these

0 commit comments

Comments
 (0)