From 0a7e5c7287f9a877114020eed75efd1d649c3a0d Mon Sep 17 00:00:00 2001 From: kayahk <80452889+kayahk@users.noreply.github.com> Date: Mon, 2 Nov 2026 19:02:11 +0100 Subject: [PATCH] Add optional issue listing operation --- gitprovider/conformance/conformance.go | 32 +++++ gitprovider/fakeprovider/issue_list_test.go | 76 ++++++++++ gitprovider/fakeprovider/mutants_test.go | 20 +++ gitprovider/fakeprovider/variants.go | 48 ++++++- gitprovider/gitprovider_test.go | 12 +- gitprovider/issue_list_test.go | 137 +++++++++++++++++++ gitprovider/provider.go | 6 + gitprovider/serve/serve_test.go | 19 +++ gitprovider/testdata/wire-shapes.golden.json | 40 ++++++ gitprovider/types.go | 16 +++ gitprovider/wire.go | 1 + gitprovider/wiretypes.go | 2 +- 12 files changed, 401 insertions(+), 8 deletions(-) create mode 100644 gitprovider/fakeprovider/issue_list_test.go create mode 100644 gitprovider/issue_list_test.go diff --git a/gitprovider/conformance/conformance.go b/gitprovider/conformance/conformance.go index 164c01c..4791ade 100644 --- a/gitprovider/conformance/conformance.go +++ b/gitprovider/conformance/conformance.go @@ -313,6 +313,7 @@ func testDescribe(t *testing.T, h Harness) { t.Errorf("optional interface for %s is implemented but the capability is not declared (core would never call it)", name) } } + check("issue_list", c.IssueList, impl.IssueList) check("resolve_threads", c.ResolveThreads, impl.ResolveThreads) check("file_read", c.FileRead, impl.FileRead) check("code_search", c.CodeSearch, impl.CodeSearch) @@ -885,6 +886,37 @@ func testReads(t *testing.T, h Harness) { // Optional reads, only when declared. caps := p.Describe().Capabilities + if lister, ok := p.(gp.IssueLister); ok && caps.IssueList { + seen, cursors := map[string]bool{}, map[string]bool{} + cursor := "" + for { + page, err := lister.ListIssues(ctx(), gp.ListIssuesRequest{Target: tg, State: "all", Cursor: cursor}) + if err != nil { + t.Fatalf("ListIssues: %v", err) + } + for _, issue := range page.Items { + if issue.Number == "" || seen[issue.Number] || issue.Title == "" || issue.URL == "" || (issue.State != "open" && issue.State != "closed") { + t.Errorf("invalid or duplicate issue: %+v", issue) + } + seen[issue.Number] = true + } + if page.NextCursor == "" { + break + } + if cursors[page.NextCursor] || len(cursors) >= 100 { + t.Fatal("ListIssues pagination did not terminate") + } + cursors[page.NextCursor] = true + cursor = page.NextCursor + } + if f.IssueNumber != "" && !seen[f.IssueNumber] { + t.Errorf("ListIssues did not return fixture issue %q", f.IssueNumber) + } + if f.InvisibleRepo.FullPath != "" { + _, err := lister.ListIssues(ctx(), gp.ListIssuesRequest{Target: gp.Target{Identity: f.Identity, Repo: f.InvisibleRepo}}) + mustCode(t, f, err, gp.CodeNotFound, gp.CodeForbidden) + } + } if fr, ok := p.(gp.FileReader); ok && caps.FileRead && f.File.Path != "" { file, err := fr.GetFile(ctx(), gp.FileRequest{Target: tg, Ref: f.File.Ref, Path: f.File.Path}) if err != nil || len(file.Content) == 0 { diff --git a/gitprovider/fakeprovider/issue_list_test.go b/gitprovider/fakeprovider/issue_list_test.go new file mode 100644 index 0000000..24e0774 --- /dev/null +++ b/gitprovider/fakeprovider/issue_list_test.go @@ -0,0 +1,76 @@ +package fakeprovider + +import ( + "context" + "errors" + "testing" + + gp "github.com/Paca-AI/plugin-sdk-go/gitprovider" +) + +func TestListIssuesStatesAndPages(t *testing.T) { + p := New() + target := gp.Target{Identity: gp.IdentityRef{ID: IdentityID}, Repo: gp.RepoRef{FullPath: RepoPath}} + r := p.repos[RepoPath] + r.issues["8"] = &gp.Issue{Number: "8", State: "closed"} + r.issues["9"] = &gp.Issue{Number: "9", State: "open"} + r.issues["10"] = &gp.Issue{Number: "10", State: "open"} + for _, tc := range []struct { + state string + want map[string]bool + }{ + {"", map[string]bool{"7": true, "9": true, "10": true}}, + {"open", map[string]bool{"7": true, "9": true, "10": true}}, + {"closed", map[string]bool{"8": true}}, + {"all", map[string]bool{"7": true, "8": true, "9": true, "10": true}}, + } { + t.Run(tc.state, func(t *testing.T) { + seen := map[string]bool{} + cursor := "" + for i := 0; i < 10; i++ { + page, err := p.ListIssues(context.Background(), gp.ListIssuesRequest{Target: target, State: tc.state, Cursor: cursor}) + if err != nil { + t.Fatal(err) + } + for _, issue := range page.Items { + if !tc.want[issue.Number] || seen[issue.Number] { + t.Fatalf("unexpected or duplicate issue %q", issue.Number) + } + seen[issue.Number] = true + } + if page.NextCursor == "" { + break + } + if page.NextCursor == cursor { + t.Fatal("cursor did not advance") + } + cursor = page.NextCursor + if i == 9 { + t.Fatal("pagination did not terminate") + } + } + if len(seen) != len(tc.want) { + t.Fatalf("got %v, want %v", seen, tc.want) + } + }) + } +} + +func TestListIssuesRejectsInvalidInput(t *testing.T) { + p := New() + target := gp.Target{Identity: gp.IdentityRef{ID: IdentityID}, Repo: gp.RepoRef{FullPath: RepoPath}} + for _, req := range []gp.ListIssuesRequest{ + {Target: target, State: "merged"}, + {Target: target, Cursor: "-1"}, + {Target: target, Cursor: "garbage"}, + } { + _, err := p.ListIssues(context.Background(), req) + if !errors.Is(err, gp.Errorf(gp.CodeValidation, "")) { + t.Errorf("request %+v: got %v, want validation", req, err) + } + } + _, err := p.ListIssues(context.Background(), gp.ListIssuesRequest{Target: gp.Target{}}) + if !errors.Is(err, gp.Errorf(gp.CodeNotFound, "")) { + t.Errorf("bad target: got %v, want not_found", err) + } +} diff --git a/gitprovider/fakeprovider/mutants_test.go b/gitprovider/fakeprovider/mutants_test.go index d288fb5..8ab7956 100644 --- a/gitprovider/fakeprovider/mutants_test.go +++ b/gitprovider/fakeprovider/mutants_test.go @@ -73,6 +73,26 @@ func TestMutant_DishonestCapability(t *testing.T) { run(t, func() gp.Provider { return dishonest{fakeprovider.New()} }) } +type noIssueList struct{ *fakeprovider.Full } + +func (n noIssueList) Describe() gp.Info { + i := n.Full.Describe() + i.Capabilities.IssueList = false + return i +} +func TestMutant_DishonestIssueList(t *testing.T) { + run(t, func() gp.Provider { return noIssueList{fakeprovider.New()} }) +} + +type emptyIssueList struct{ *fakeprovider.Full } + +func (n emptyIssueList) ListIssues(context.Context, gp.ListIssuesRequest) (gp.IssuePage, error) { + return gp.IssuePage{Items: []gp.Issue{}}, nil +} +func TestMutant_EmptyIssueList(t *testing.T) { + run(t, func() gp.Provider { return emptyIssueList{fakeprovider.New()} }) +} + type noExpiry struct{ *fakeprovider.Full } func (n noExpiry) MintCredential(ctx context.Context, r gp.MintRequest) (gp.Credential, error) { diff --git a/gitprovider/fakeprovider/variants.go b/gitprovider/fakeprovider/variants.go index 33e5985..668566a 100644 --- a/gitprovider/fakeprovider/variants.go +++ b/gitprovider/fakeprovider/variants.go @@ -3,6 +3,7 @@ package fakeprovider import ( "context" "net/url" + "sort" "strconv" "strings" @@ -33,8 +34,8 @@ func New() *Full { IdentityKinds: []gp.IdentityKind{gp.IdentityApp, gp.IdentityBotUser}, CredentialModel: gp.CredentialEphemeral, WebhookScope: gp.WebhookApp, StatusKind: gp.StatusCheckRun, Triggers: gp.AllTriggerKinds, - MultipleIdentities: true, - ResolveThreads: true, FileRead: true, CodeSearch: true, Labels: true, ReviewerRequests: true, Assignees: true, + MultipleIdentities: true, IssueList: true, + ResolveThreads: true, FileRead: true, CodeSearch: true, Labels: true, ReviewerRequests: true, Assignees: true, Merge: true, RerunChecks: true, Reactions: true, Branches: true, WebhookManagement: true, IdentityDeletion: true, AccessGrants: true, BatchReview: true, IdempotentWrites: true, HiddenMarkers: true, ForkChangeRequests: true, DraftChangeRequests: true, CIConfigGlobs: []string{".fake-ci/**"}, @@ -113,6 +114,49 @@ func (f *Full) CreateBranch(_ context.Context, req gp.CreateBranchRequest) (gp.B }) } +// ListIssues implements gp.IssueLister. +func (f *Full) ListIssues(_ context.Context, req gp.ListIssuesRequest) (gp.IssuePage, error) { + f.mu.Lock() + defer f.mu.Unlock() + r, err := f.repo(req.Target) + if err != nil { + return gp.IssuePage{}, err + } + state := req.State + if state == "" { + state = "open" + } + if state != "open" && state != "closed" && state != "all" { + return gp.IssuePage{}, gp.Errorf(gp.CodeValidation, "invalid issue state") + } + start := 0 + if req.Cursor != "" { + var err error + start, err = strconv.Atoi(req.Cursor) + if err != nil || start < 0 { + return gp.IssuePage{}, gp.Errorf(gp.CodeValidation, "invalid cursor") + } + } + items := []gp.Issue{} + for _, issue := range r.issues { + if state == "all" || issue.State == state { + items = append(items, *issue) + } + } + sort.Slice(items, func(i, j int) bool { return items[i].Number < items[j].Number }) + page := gp.IssuePage{Items: []gp.Issue{}} + if start < len(items) { + end := start + 2 + if end < len(items) { + page.NextCursor = strconv.Itoa(end) + } else { + end = len(items) + } + page.Items = items[start:end] + } + return page, nil +} + // ResolveThread implements gp.ThreadResolver. func (f *Full) ResolveThread(_ context.Context, req gp.ResolveThreadRequest) (gp.Empty, error) { f.mu.Lock() diff --git a/gitprovider/gitprovider_test.go b/gitprovider/gitprovider_test.go index 91e30f6..db8e137 100644 --- a/gitprovider/gitprovider_test.go +++ b/gitprovider/gitprovider_test.go @@ -2,9 +2,9 @@ package gitprovider_test import ( "context" - "encoding/json" "errors" "fmt" + "reflect" "strings" "testing" @@ -41,9 +41,11 @@ func TestAsError_NeverLeaksInternals(t *testing.T) { func TestOperationsTable(t *testing.T) { // Every optional operation names a capability that exists in Capabilities. - raw, _ := json.Marshal(gp.Capabilities{}) - var caps map[string]any - _ = json.Unmarshal(raw, &caps) + caps := map[string]bool{} + typ := reflect.TypeOf(gp.Capabilities{}) + for i := 0; i < typ.NumField(); i++ { + caps[strings.Split(typ.Field(i).Tag.Get("json"), ",")[0]] = true + } seen := map[string]bool{} for _, op := range gp.Operations() { if seen[op.Name] { @@ -58,7 +60,7 @@ func TestOperationsTable(t *testing.T) { } // Every capability that gates an operation is covered by the table, so a new // optional interface cannot be added without a wire operation. - for _, flag := range []string{"resolve_threads", "file_read", "code_search", "labels", "reviewer_requests", "assignees", "merge", "rerun_checks", "reactions", "webhook_management", "identity_deletion", "access_grants"} { + for _, flag := range []string{"issue_list", "resolve_threads", "file_read", "code_search", "labels", "reviewer_requests", "assignees", "merge", "rerun_checks", "reactions", "webhook_management", "identity_deletion", "access_grants"} { found := false for _, op := range gp.Operations() { if op.Capability == flag { diff --git a/gitprovider/issue_list_test.go b/gitprovider/issue_list_test.go new file mode 100644 index 0000000..ae8057c --- /dev/null +++ b/gitprovider/issue_list_test.go @@ -0,0 +1,137 @@ +package gitprovider_test + +import ( + "context" + "encoding/json" + "errors" + "reflect" + "testing" + + gp "github.com/Paca-AI/plugin-sdk-go/gitprovider" + "github.com/Paca-AI/plugin-sdk-go/gitprovider/fakeprovider" +) + +func TestListIssuesContract(t *testing.T) { + if !gp.Implemented(fakeprovider.New()).IssueList || gp.Implemented(fakeprovider.NewMinimal()).IssueList { + t.Fatal("IssueList implementation detection is wrong") + } + operations := gp.Operations() + found := false + for _, op := range operations { + if op.Name == "list_issues" && op.Capability == "issue_list" { + found = true + } + } + if !found { + t.Fatal("list_issues must be gated by issue_list") + } + for name, typ := range map[string]reflect.Type{"ListIssuesRequest": reflect.TypeOf(gp.ListIssuesRequest{}), "IssuePage": reflect.TypeOf(gp.IssuePage{})} { + if gp.WireTypes()[name] != typ { + t.Errorf("WireTypes missing %s", name) + } + } + for _, tc := range []struct { + value any + want string + }{ + {gp.ListIssuesRequest{}, `{"target":{"identity":{"id":""},"repo":{"host":"","full_path":""}}}`}, + {gp.ListIssuesRequest{State: "closed", Cursor: "next"}, `{"target":{"identity":{"id":""},"repo":{"host":"","full_path":""}},"state":"closed","cursor":"next"}`}, + {gp.IssuePage{Items: []gp.Issue{}}, `{"items":[]}`}, + {gp.IssuePage{Items: []gp.Issue{}, NextCursor: "next"}, `{"items":[],"next_cursor":"next"}`}, + } { + got, err := json.Marshal(tc.value) + if err != nil || string(got) != tc.want { + t.Errorf("marshal: %s, %v; want %s", got, err, tc.want) + } + } +} + +type missingIssueLister struct{ gp.Provider } + +func (p missingIssueLister) Describe() gp.Info { + i := p.Provider.Describe() + i.Capabilities.IssueList = true + return i +} + +type hiddenIssueLister struct{ *fakeprovider.Full } + +func (p hiddenIssueLister) Describe() gp.Info { + i := p.Full.Describe() + i.Capabilities.IssueList = false + return i +} + +func TestListIssuesWireGuards(t *testing.T) { + for _, p := range []gp.Provider{fakeprovider.NewMinimal(), missingIssueLister{fakeprovider.NewMinimal()}, hiddenIssueLister{fakeprovider.New()}} { + status, raw := gp.Dispatch(context.Background(), p, "list_issues", []byte(`{}`)) + var env gp.Envelope + if err := json.Unmarshal(raw, &env); err != nil || status != 501 || env.Error == nil || !errors.Is(env.Error, gp.ErrUnsupported) { + t.Errorf("unsupported: status=%d body=%s err=%v", status, raw, err) + } + } + for _, body := range [][]byte{nil, []byte(`{`)} { + status, raw := gp.Dispatch(context.Background(), fakeprovider.New(), "list_issues", body) + var env gp.Envelope + if err := json.Unmarshal(raw, &env); err != nil || status != 400 || env.Error == nil || env.Error.Code != gp.CodeValidation { + t.Errorf("invalid body: status=%d body=%s err=%v", status, raw, err) + } + } +} + +func TestListIssuesDispatchFiltersAndValidates(t *testing.T) { + f := fakeprovider.Fixtures() + target := gp.Target{Identity: f.Identity, Repo: f.Repo} + for _, tc := range []struct { + state, cursor string + status int + code gp.ErrorCode + items int + }{ + {"closed", "", 200, "", 0}, + {"all", "", 200, "", 1}, + {"all", "2", 200, "", 0}, + {"invalid", "", 400, gp.CodeValidation, 0}, + {"all", "bad", 400, gp.CodeValidation, 0}, + } { + body, _ := json.Marshal(gp.ListIssuesRequest{Target: target, State: tc.state, Cursor: tc.cursor}) + status, raw := gp.Dispatch(context.Background(), fakeprovider.New(), "list_issues", body) + var env gp.Envelope + if err := json.Unmarshal(raw, &env); err != nil { + t.Fatal(err) + } + if status != tc.status { + t.Fatalf("%+v: status=%d body=%s", tc, status, raw) + } + if tc.code != "" { + if env.Error == nil || env.Error.Code != tc.code { + t.Fatalf("%+v: body=%s", tc, raw) + } + continue + } + var page gp.IssuePage + if err := json.Unmarshal(env.Result, &page); err != nil || !env.OK || len(page.Items) != tc.items { + t.Fatalf("%+v: body=%s err=%v", tc, raw, err) + } + } +} + +func TestListIssuesDispatch(t *testing.T) { + p := fakeprovider.New() + f := fakeprovider.Harness(false).Fixtures + body, err := json.Marshal(map[string]any{"target": gp.Target{Identity: f.Identity, Repo: f.Repo}}) + if err != nil { + t.Fatal(err) + } + status, raw := gp.Dispatch(context.Background(), p, "list_issues", body) + var env gp.Envelope + if err := json.Unmarshal(raw, &env); err != nil || status != 200 || !env.OK { + t.Fatalf("list_issues: status=%d body=%s err=%v", status, raw, err) + } + var page struct { + Items []gp.Issue `json:"items"` + } + if err := json.Unmarshal(env.Result, &page); err != nil || len(page.Items) != 1 || page.Items[0].Number != f.IssueNumber { + t.Fatalf("issue page: %+v err=%v", page, err) + } +} diff --git a/gitprovider/provider.go b/gitprovider/provider.go index ef324ed..633aa40 100644 --- a/gitprovider/provider.go +++ b/gitprovider/provider.go @@ -52,6 +52,11 @@ type Provider interface { // set the matching [Capabilities] flag; the conformance suite checks that the // two agree. +// IssueLister lists repository issues (capability IssueList). +type IssueLister interface { + ListIssues(ctx context.Context, req ListIssuesRequest) (IssuePage, error) +} + // ThreadResolver resolves and reopens review threads (capability ResolveThreads). type ThreadResolver interface { ResolveThread(ctx context.Context, req ResolveThreadRequest) (Empty, error) @@ -129,6 +134,7 @@ type BranchManager interface { // tooling can compare them with what [Info] declares. func Implemented(p Provider) Capabilities { var c Capabilities + _, c.IssueList = p.(IssueLister) _, c.ResolveThreads = p.(ThreadResolver) _, c.FileRead = p.(FileReader) _, c.CodeSearch = p.(CodeSearcher) diff --git a/gitprovider/serve/serve_test.go b/gitprovider/serve/serve_test.go index 2facbcb..b468bea 100644 --- a/gitprovider/serve/serve_test.go +++ b/gitprovider/serve/serve_test.go @@ -42,6 +42,25 @@ func TestRegister_DescribeOverRoute(t *testing.T) { } } +func TestRegister_ListIssuesOverRoute(t *testing.T) { + f := fakeprovider.Fixtures() + body, err := json.Marshal(gitprovider.ListIssuesRequest{Target: gitprovider.Target{Identity: f.Identity, Repo: f.Repo}}) + if err != nil { + t.Fatal(err) + } + res := setup(t, fakeprovider.New()).Call("POST", "/_git/list_issues", plugintest.Request{ + PathParams: map[string]string{"op": "list_issues"}, Body: body, + }) + var env gitprovider.Envelope + if err := json.Unmarshal(res.Body, &env); err != nil || res.StatusCode != 200 || !env.OK { + t.Fatalf("list issues: status %d body %s err %v", res.StatusCode, res.BodyString(), err) + } + var page gitprovider.IssuePage + if err := json.Unmarshal(env.Result, &page); err != nil || len(page.Items) != 1 || page.Items[0].Number != f.IssueNumber { + t.Fatalf("issue page: %+v err=%v", page, err) + } +} + func TestRegister_ErrorsCarryHTTPStatus(t *testing.T) { tc := setup(t, fakeprovider.New()) res := tc.Call("POST", "/_git/get_change_request", plugintest.Request{ diff --git a/gitprovider/testdata/wire-shapes.golden.json b/gitprovider/testdata/wire-shapes.golden.json index b8b96a5..3e22dcd 100644 --- a/gitprovider/testdata/wire-shapes.golden.json +++ b/gitprovider/testdata/wire-shapes.golden.json @@ -125,6 +125,7 @@ "identity_kinds": [ "string" ], + "issue_list": "bool", "kinds": [ { "access_grants": "bool", @@ -612,6 +613,7 @@ "identity_kinds": [ "string" ], + "issue_list": "bool", "kinds": [ { "access_grants": "bool", @@ -669,6 +671,25 @@ "title": "string", "url": "string" }, + "IssuePage": { + "items": [ + { + "assignees": [ + "string" + ], + "author": "string", + "body": "string", + "labels": [ + "string" + ], + "number": "string", + "state": "string", + "title": "string", + "url": "string" + } + ], + "next_cursor": "string" + }, "IssueRequest": { "number": "string", "target": { @@ -733,6 +754,25 @@ } } }, + "ListIssuesRequest": { + "cursor": "string", + "state": "string", + "target": { + "identity": { + "handle": "string", + "id": "string", + "provider_ref": "bytes" + }, + "repo": { + "clone_url": "string", + "default_branch": "string", + "external_id": "string", + "full_path": "string", + "host": "string", + "provider_ref": "bytes" + } + } + }, "ListReposRequest": { "cursor": "string", "identity": { diff --git a/gitprovider/types.go b/gitprovider/types.go index 5b71711..3360c6d 100644 --- a/gitprovider/types.go +++ b/gitprovider/types.go @@ -210,6 +210,7 @@ type Capabilities struct { Kinds []KindProfile `json:"kinds,omitempty"` // Optional operations (each backed by an optional interface). + IssueList bool `json:"issue_list,omitempty"` ResolveThreads bool `json:"resolve_threads"` FileRead bool `json:"file_read"` CodeSearch bool `json:"code_search"` @@ -698,6 +699,21 @@ type IssueRequest struct { Number string `json:"number"` } +// ListIssuesRequest pages repository issues. State is open, closed or all; +// providers treat an empty state as open and reject other values as validation +// errors. Cursor is an opaque continuation token from the preceding page. +type ListIssuesRequest struct { + Target Target `json:"target"` + State string `json:"state,omitempty"` + Cursor string `json:"cursor,omitempty"` +} + +// IssuePage is one page of issues. +type IssuePage struct { + Items []Issue `json:"items"` + NextCursor string `json:"next_cursor,omitempty"` +} + // Issue is an issue. type Issue struct { Number string `json:"number"` diff --git a/gitprovider/wire.go b/gitprovider/wire.go index 4f9c208..6c943c9 100644 --- a/gitprovider/wire.go +++ b/gitprovider/wire.go @@ -83,6 +83,7 @@ var operations = []opEntry{ required("reply_thread", Provider.ReplyThread), required("submit_review", Provider.SubmitReview), required("set_status", Provider.SetStatus), + optional("list_issues", "issue_list", IssueLister.ListIssues), optional("resolve_thread", "resolve_threads", ThreadResolver.ResolveThread), optional("get_file", "file_read", FileReader.GetFile), optional("search_code", "code_search", CodeSearcher.SearchCode), diff --git a/gitprovider/wiretypes.go b/gitprovider/wiretypes.go index fc3d607..8382972 100644 --- a/gitprovider/wiretypes.go +++ b/gitprovider/wiretypes.go @@ -26,7 +26,7 @@ func WireTypes() map[string]reflect.Type { "ChangedFile": t(ChangedFile{}), "ChangedFilePage": t(ChangedFilePage{}), "DiffRequest": t(DiffRequest{}), "Diff": t(Diff{}), "Comment": t(Comment{}), "Thread": t(Thread{}), "ThreadPage": t(ThreadPage{}), "ListCommentsRequest": t(ListCommentsRequest{}), "CommentPage": t(CommentPage{}), - "IssueRequest": t(IssueRequest{}), "Issue": t(Issue{}), + "IssueRequest": t(IssueRequest{}), "Issue": t(Issue{}), "ListIssuesRequest": t(ListIssuesRequest{}), "IssuePage": t(IssuePage{}), "Check": t(Check{}), "ChecksRequest": t(ChecksRequest{}), "CheckLogsRequest": t(CheckLogsRequest{}), "Logs": t(Logs{}), "FileRequest": t(FileRequest{}), "File": t(File{}), "CodeSearchRequest": t(CodeSearchRequest{}), "CodeHit": t(CodeHit{}), "CodeHits": t(CodeHits{}), "OpenCRRequest": t(OpenCRRequest{}), "UpdateCRRequest": t(UpdateCRRequest{}),