From 1d8091ec945c9931460269ca7157335f31450bac Mon Sep 17 00:00:00 2001 From: Aly Raffauf Date: Sat, 25 Jul 2026 17:22:31 -0400 Subject: [PATCH] migrate to generated types + validation --- atproto/records.go | 7 + atproto/records_test.go | 63 + flake/treefmt.nix | 16 + go.mod | 5 + go.sum | 11 + internal/app/git.go | 2 +- internal/app/issues.go | 27 +- internal/app/items.go | 34 +- internal/app/pulls.go | 64 +- internal/app/pulls_test.go | 2 +- internal/app/records.go | 79 +- internal/app/records_test.go | 48 + internal/app/repos.go | 58 +- internal/app/service_test.go | 14 +- internal/app/sshkeys.go | 14 +- internal/app/strings.go | 24 +- internal/app/strings_test.go | 37 +- internal/app/target.go | 4 +- internal/tangledlex/README.md | 30 + internal/tangledlex/cbor_gen.go | 3726 +++++++++++++++++ internal/tangledlex/cborgen/main.go | 28 + internal/tangledlex/generate-lexicons.sh | 16 + internal/tangledlex/generate.go | 3 + internal/tangledlex/issuecomment.go | 23 + internal/tangledlex/issuestate.go | 21 + internal/tangledlex/lexgen.json | 8 + internal/tangledlex/lock.json | 5 + internal/tangledlex/pullcomment.go | 22 + internal/tangledlex/pullstatus.go | 21 + internal/tangledlex/repogetRepo.go | 34 + internal/tangledlex/repoissue.go | 23 + internal/tangledlex/repolistIssues.go | 65 + internal/tangledlex/repolistPulls.go | 65 + internal/tangledlex/repolistRepos.go | 51 + internal/tangledlex/repopull.go | 46 + internal/tangledlex/schemas.txt | 13 + .../tangledlex/schemas/issue/comment.json | 47 + internal/tangledlex/schemas/issue/issue.json | 46 + internal/tangledlex/schemas/issue/state.json | 35 + internal/tangledlex/schemas/publicKey.json | 32 + .../tangledlex/schemas/pulls/comment.json | 43 + internal/tangledlex/schemas/pulls/pull.json | 102 + internal/tangledlex/schemas/pulls/state.json | 36 + internal/tangledlex/schemas/repo/getRepo.json | 41 + .../tangledlex/schemas/repo/listIssues.json | 98 + .../tangledlex/schemas/repo/listPulls.json | 98 + .../tangledlex/schemas/repo/listRepos.json | 62 + internal/tangledlex/schemas/repo/repo.json | 72 + .../tangledlex/schemas/string/string.json | 35 + internal/tangledlex/tangledpublicKey.go | 23 + internal/tangledlex/tangledrepo.go | 35 + internal/tangledlex/tangledstring.go | 21 + internal/tangledlex/update-schemas.sh | 43 + internal/tangledlex/validate.go | 339 ++ internal/tangledlex/validate_test.go | 181 + nix/tg.nix | 2 +- tangled/get_repo.go | 46 +- tangled/lex_client.go | 29 + tangled/lex_client_test.go | 28 + tangled/lex_decode.go | 24 + tangled/list.go | 17 +- tangled/list_issues.go | 31 +- tangled/list_pulls.go | 58 +- tangled/list_repos.go | 30 +- tangled/records.go | 47 - 65 files changed, 6123 insertions(+), 287 deletions(-) create mode 100644 atproto/records_test.go create mode 100644 internal/app/records_test.go create mode 100644 internal/tangledlex/README.md create mode 100644 internal/tangledlex/cbor_gen.go create mode 100644 internal/tangledlex/cborgen/main.go create mode 100755 internal/tangledlex/generate-lexicons.sh create mode 100644 internal/tangledlex/generate.go create mode 100644 internal/tangledlex/issuecomment.go create mode 100644 internal/tangledlex/issuestate.go create mode 100644 internal/tangledlex/lexgen.json create mode 100644 internal/tangledlex/lock.json create mode 100644 internal/tangledlex/pullcomment.go create mode 100644 internal/tangledlex/pullstatus.go create mode 100644 internal/tangledlex/repogetRepo.go create mode 100644 internal/tangledlex/repoissue.go create mode 100644 internal/tangledlex/repolistIssues.go create mode 100644 internal/tangledlex/repolistPulls.go create mode 100644 internal/tangledlex/repolistRepos.go create mode 100644 internal/tangledlex/repopull.go create mode 100644 internal/tangledlex/schemas.txt create mode 100644 internal/tangledlex/schemas/issue/comment.json create mode 100644 internal/tangledlex/schemas/issue/issue.json create mode 100644 internal/tangledlex/schemas/issue/state.json create mode 100644 internal/tangledlex/schemas/publicKey.json create mode 100644 internal/tangledlex/schemas/pulls/comment.json create mode 100644 internal/tangledlex/schemas/pulls/pull.json create mode 100644 internal/tangledlex/schemas/pulls/state.json create mode 100644 internal/tangledlex/schemas/repo/getRepo.json create mode 100644 internal/tangledlex/schemas/repo/listIssues.json create mode 100644 internal/tangledlex/schemas/repo/listPulls.json create mode 100644 internal/tangledlex/schemas/repo/listRepos.json create mode 100644 internal/tangledlex/schemas/repo/repo.json create mode 100644 internal/tangledlex/schemas/string/string.json create mode 100644 internal/tangledlex/tangledpublicKey.go create mode 100644 internal/tangledlex/tangledrepo.go create mode 100644 internal/tangledlex/tangledstring.go create mode 100755 internal/tangledlex/update-schemas.sh create mode 100644 internal/tangledlex/validate.go create mode 100644 internal/tangledlex/validate_test.go create mode 100644 tangled/lex_client.go create mode 100644 tangled/lex_client_test.go create mode 100644 tangled/lex_decode.go diff --git a/atproto/records.go b/atproto/records.go index e25a99f..07ed402 100644 --- a/atproto/records.go +++ b/atproto/records.go @@ -7,7 +7,9 @@ import ( "fmt" "io" "net/http" + "strings" + "github.com/alyraffauf/tg/internal/tangledlex" "github.com/bluesky-social/indigo/atproto/atclient" "github.com/bluesky-social/indigo/atproto/syntax" ) @@ -69,6 +71,11 @@ const maxRecordPages = 1000 // PutRecord writes a record to the PDS, returning its at:// URI and CID. func (a *ATProto) PutRecord(ctx context.Context, in PutRecordInput) (uri, cid string, err error) { + if strings.HasPrefix(in.Collection, "sh.tangled.") { + if err := tangledlex.ValidateRecord(in.Collection, in.Record); err != nil { + return "", "", fmt.Errorf("validate %s record: %w", in.Collection, err) + } + } var out struct { URI string `json:"uri"` CID string `json:"cid,omitempty"` diff --git a/atproto/records_test.go b/atproto/records_test.go new file mode 100644 index 0000000..32cd75a --- /dev/null +++ b/atproto/records_test.go @@ -0,0 +1,63 @@ +package atproto + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/alyraffauf/tg/internal/tangledlex" + "github.com/bluesky-social/indigo/atproto/atclient" +) + +func TestPutRecordRejectsInvalidTangledRecordBeforeRequest(t *testing.T) { + client := &ATProto{} + _, _, err := client.PutRecord(context.Background(), PutRecordInput{ + Collection: "sh.tangled.string", + Record: tangledlex.String{ + LexiconTypeID: "sh.tangled.string", + }, + }) + if err == nil || !strings.Contains(err.Error(), "filename") { + t.Fatalf("PutRecord() error = %v, want filename validation error", err) + } +} + +func TestPutRecordSendsValidatedTangledRecord(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + defer request.Body.Close() + var input PutRecordInput + if err := json.NewDecoder(request.Body).Decode(&input); err != nil { + t.Fatal(err) + } + record, ok := input.Record.(map[string]any) + if !ok || record["$type"] != "sh.tangled.string" { + t.Fatalf("record = %#v, want serialized Tangled string", input.Record) + } + writer.Header().Set("Content-Type", "application/json") + _, _ = writer.Write([]byte(`{"uri":"at://did:plc:abc123/sh.tangled.string/3k2abc"}`)) + })) + defer server.Close() + + client := &ATProto{Client: &atclient.APIClient{Client: server.Client(), Host: server.URL}} + uri, _, err := client.PutRecord(context.Background(), PutRecordInput{ + Repo: "did:plc:abc123", + Collection: "sh.tangled.string", + Rkey: "3k2abc", + Record: tangledlex.String{ + LexiconTypeID: "sh.tangled.string", + Filename: "note.md", + Description: "note", + Contents: "text", + CreatedAt: "2026-07-25T12:00:00Z", + }, + }) + if err != nil { + t.Fatalf("PutRecord() error = %v", err) + } + if uri != "at://did:plc:abc123/sh.tangled.string/3k2abc" { + t.Fatalf("PutRecord() URI = %q", uri) + } +} diff --git a/flake/treefmt.nix b/flake/treefmt.nix index 53fdca8..2643688 100644 --- a/flake/treefmt.nix +++ b/flake/treefmt.nix @@ -1,6 +1,22 @@ _: { perSystem = _: { treefmt.config = { + settings.global.excludes = [ + "internal/tangledlex/cbor_gen.go" + "internal/tangledlex/issuecomment.go" + "internal/tangledlex/issuestate.go" + "internal/tangledlex/pullcomment.go" + "internal/tangledlex/pullstatus.go" + "internal/tangledlex/repogetRepo.go" + "internal/tangledlex/repoissue.go" + "internal/tangledlex/repolistIssues.go" + "internal/tangledlex/repolistPulls.go" + "internal/tangledlex/repolistRepos.go" + "internal/tangledlex/repopull.go" + "internal/tangledlex/tangledpublicKey.go" + "internal/tangledlex/tangledrepo.go" + "internal/tangledlex/tangledstring.go" + ]; programs = { alejandra.enable = true; deadnix.enable = true; diff --git a/go.mod b/go.mod index f482cad..98216ad 100644 --- a/go.mod +++ b/go.mod @@ -49,14 +49,19 @@ require ( github.com/spf13/cast v1.10.0 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/subosito/gotenv v1.6.0 // indirect + github.com/urfave/cli/v2 v2.25.7 // indirect github.com/whyrusleeping/cbor-gen v0.3.1 // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.54.0 // indirect + golang.org/x/mod v0.37.0 // indirect + golang.org/x/sync v0.22.0 // indirect golang.org/x/sys v0.47.0 // indirect golang.org/x/text v0.40.0 // indirect golang.org/x/time v0.15.0 // indirect + golang.org/x/tools v0.47.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect diff --git a/go.sum b/go.sum index 38f4049..934bff9 100644 --- a/go.sum +++ b/go.sum @@ -111,8 +111,13 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/urfave/cli v1.22.17 h1:SYzXoiPfQjHBbkYxbew5prZHS1TOLT3ierW8SYLqtVQ= +github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= +github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= github.com/whyrusleeping/cbor-gen v0.3.1 h1:82ioxmhEYut7LBVGhGq8xoRkXPLElVuh5mV67AFfdv0= github.com/whyrusleeping/cbor-gen v0.3.1/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= github.com/zalando/go-keyring v0.2.8 h1:6sD/Ucpl7jNq10rM2pgqTs0sZ9V3qMrqfIIy5YPccHs= github.com/zalando/go-keyring v0.2.8/go.mod h1:tsMo+VpRq5NGyKfxoBVjCuMrG47yj8cmakZDO5QGii0= gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b h1:CzigHMRySiX3drau9C6Q5CAbNIApmLdat5jPMqChvDA= @@ -127,12 +132,18 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= +golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= +golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= +golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= +golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= diff --git a/internal/app/git.go b/internal/app/git.go index 78dbe7e..8afe825 100644 --- a/internal/app/git.go +++ b/internal/app/git.go @@ -64,7 +64,7 @@ func (s *Service) CheckoutPull(ctx context.Context, in CheckoutPullInput) (*PRCh return nil, err } } - if targetRecord.Value.RepoDid != localRecord.Value.RepoDid { + if stringValue(targetRecord.Value.RepoDid) != stringValue(localRecord.Value.RepoDid) { return nil, fmt.Errorf("pull request target %s does not match the current repository", target) } diff --git a/internal/app/issues.go b/internal/app/issues.go index 6488522..90920ff 100644 --- a/internal/app/issues.go +++ b/internal/app/issues.go @@ -6,6 +6,7 @@ import ( "time" "github.com/alyraffauf/tg/atproto" + "github.com/alyraffauf/tg/internal/tangledlex" "github.com/alyraffauf/tg/tangled" "github.com/bluesky-social/indigo/atproto/syntax" ) @@ -69,12 +70,12 @@ func (s *Service) CreateIssue(ctx context.Context, t Target, title, body string) Repo: did, Collection: issueCollection, Rkey: rkey, - Record: tangled.IssueRecord{ - Type: issueCollection, - Repo: repoDid, - Title: title, - Body: body, - CreatedAt: time.Now().UTC().Format(time.RFC3339), + Record: tangledlex.RepoIssue{ + LexiconTypeID: issueCollection, + Repo: repoDid, + Title: title, + Body: optionalString(body), + CreatedAt: time.Now().UTC().Format(time.RFC3339), }, }) if err != nil { @@ -112,11 +113,11 @@ func (s *Service) createIssueComment(ctx context.Context, issueURI, body string) Repo: did, Collection: issueCollection + ".comment", Rkey: rkey, - Record: tangled.IssueCommentRecord{ - Type: issueCollection + ".comment", - Issue: issueURI, - Body: body, - CreatedAt: time.Now().UTC().Format(time.RFC3339), + Record: tangledlex.RepoIssueComment{ + LexiconTypeID: issueCollection + ".comment", + Issue: issueURI, + Body: body, + CreatedAt: time.Now().UTC().Format(time.RFC3339), }, }) if err != nil { @@ -163,14 +164,14 @@ func (s *Service) targetRecord(ctx context.Context, t Target, collection, rkey s var items []tangled.ListItem var recordType string if collection == issueCollection { - issues, err := s.appview.ListIssues(ctx, repoRecord.Value.RepoDid, tangled.ListOpts{Limit: defaultListLimit}) + issues, err := s.appview.ListIssues(ctx, stringValue(repoRecord.Value.RepoDid), tangled.ListOpts{Limit: defaultListLimit}) if err != nil { return "", "", fmt.Errorf("list issues for %s: %w", t, err) } items = issues.Items recordType = "issue" } else { - pulls, err := s.appview.ListPulls(ctx, repoRecord.Value.RepoDid, tangled.ListOpts{Limit: defaultListLimit}) + pulls, err := s.appview.ListPulls(ctx, stringValue(repoRecord.Value.RepoDid), tangled.ListOpts{Limit: defaultListLimit}) if err != nil { return "", "", fmt.Errorf("list pull requests for %s: %w", t, err) } diff --git a/internal/app/items.go b/internal/app/items.go index c64d0f7..8b432ed 100644 --- a/internal/app/items.go +++ b/internal/app/items.go @@ -6,6 +6,7 @@ import ( "fmt" "strings" + "github.com/alyraffauf/tg/internal/tangledlex" "github.com/alyraffauf/tg/tangled" ) @@ -24,27 +25,48 @@ type recordView struct { } func decodeIssue(raw json.RawMessage) (recordView, error) { - var r tangled.IssueRecord + var r tangledlex.RepoIssue if err := json.Unmarshal(raw, &r); err != nil { return recordView{}, err } - return recordView{Title: r.Title, Body: r.Body, CreatedAt: r.CreatedAt}, nil + return recordView{Title: r.Title, Body: stringValue(r.Body), CreatedAt: r.CreatedAt}, nil } func decodePull(raw json.RawMessage) (recordView, error) { - var r tangled.PullRecord + var r tangledlex.RepoPull if err := json.Unmarshal(raw, &r); err != nil { return recordView{}, err } return recordView{ Title: r.Title, - Body: r.Body, + Body: stringValue(r.Body), CreatedAt: r.CreatedAt, - SourceBranch: r.Source.Branch, - TargetBranch: r.Target.Branch, + SourceBranch: pullSourceBranch(r.Source), + TargetBranch: pullTargetBranch(r.Target), }, nil } +func stringValue(value *string) string { + if value == nil { + return "" + } + return *value +} + +func pullSourceBranch(source *tangledlex.RepoPull_Source) string { + if source == nil { + return "" + } + return source.Branch +} + +func pullTargetBranch(target *tangledlex.RepoPull_Target) string { + if target == nil { + return "" + } + return target.Branch +} + // resolveAuthor resolves a DID to an author, falling back to the raw DID // string for Handle if resolution fails. func (s *Service) resolveAuthor(ctx context.Context, did string) Author { diff --git a/internal/app/pulls.go b/internal/app/pulls.go index 02da073..a3e1e25 100644 --- a/internal/app/pulls.go +++ b/internal/app/pulls.go @@ -11,9 +11,11 @@ import ( "time" "github.com/alyraffauf/tg/atproto" + "github.com/alyraffauf/tg/internal/tangledlex" "github.com/alyraffauf/tg/knot" "github.com/alyraffauf/tg/tangled" "github.com/bluesky-social/indigo/atproto/syntax" + lexutil "github.com/bluesky-social/indigo/lex/util" ) // maxPullPatchSize caps a downloaded pull-request patch. @@ -131,7 +133,7 @@ func (s *Service) CreatePull(ctx context.Context, in CreatePullInput) (*PRCreate return nil, fmt.Errorf("resolve source repository: %w", err) } } - if source.Value.RepoDid == "" { + if stringValue(source.Value.RepoDid) == "" { return nil, fmt.Errorf("source repository has no repo DID") } @@ -147,8 +149,8 @@ func (s *Service) CreatePull(ctx context.Context, in CreatePullInput) (*PRCreate uri, err := createPullRecord(ctx, atClient, did, pullRecordInput{ Title: in.Title, Body: in.Body, - TargetRepoDid: target.Value.RepoDid, - SourceRepoDid: source.Value.RepoDid, + TargetRepoDid: stringValue(target.Value.RepoDid), + SourceRepoDid: stringValue(source.Value.RepoDid), Base: base, Head: head, Patch: blob, @@ -178,43 +180,43 @@ func createPullRecord(ctx context.Context, atClient pdsClient, did string, input return uri, nil } -func newPullRecord(input pullRecordInput, createdAt time.Time) (tangled.PullRecord, error) { +func newPullRecord(input pullRecordInput, createdAt time.Time) (tangledlex.RepoPull, error) { now := createdAt.Format(time.RFC3339) patchBlob, err := patchBlob(input.Patch) if err != nil { - return tangled.PullRecord{}, err + return tangledlex.RepoPull{}, err } - return tangled.PullRecord{ - Type: pullCollection, - Title: input.Title, - Body: input.Body, - CreatedAt: now, - Target: tangled.PullTarget{ + return tangledlex.RepoPull{ + LexiconTypeID: pullCollection, + Title: input.Title, + Body: optionalString(input.Body), + CreatedAt: now, + Target: &tangledlex.RepoPull_Target{ Repo: input.TargetRepoDid, Branch: input.Base, }, - Source: tangled.PullSource{ - Repo: input.SourceRepoDid, + Source: &tangledlex.RepoPull_Source{ + Repo: optionalString(input.SourceRepoDid), Branch: input.Head, }, - Rounds: []tangled.PullRound{{ + Rounds: []*tangledlex.RepoPull_Round{{ CreatedAt: now, - PatchBlob: patchBlob, + PatchBlob: &patchBlob, }}, }, nil } -func patchBlob(blob *atproto.Blob) (tangled.PatchBlob, error) { +func patchBlob(blob *atproto.Blob) (lexutil.LexBlob, error) { if blob == nil || blob.Ref == nil { - return tangled.PatchBlob{}, nil + return lexutil.LexBlob{}, nil } data, err := json.Marshal(blob) if err != nil { - return tangled.PatchBlob{}, fmt.Errorf("encode pull patch blob: %w", err) + return lexutil.LexBlob{}, fmt.Errorf("encode pull patch blob: %w", err) } - var result tangled.PatchBlob + var result lexutil.LexBlob if err := json.Unmarshal(data, &result); err != nil { - return tangled.PatchBlob{}, fmt.Errorf("decode pull patch blob: %w", err) + return lexutil.LexBlob{}, fmt.Errorf("decode pull patch blob: %w", err) } return result, nil } @@ -248,11 +250,11 @@ func (s *Service) createPullComment(ctx context.Context, pullURI, body string) ( Repo: did, Collection: pullCollection + ".comment", Rkey: rkey, - Record: tangled.PullCommentRecord{ - Type: pullCollection + ".comment", - Pull: pullURI, - Body: body, - CreatedAt: time.Now().UTC().Format(time.RFC3339), + Record: tangledlex.RepoPullComment{ + LexiconTypeID: pullCollection + ".comment", + Pull: pullURI, + Body: body, + CreatedAt: time.Now().UTC().Format(time.RFC3339), }, }) if err != nil { @@ -286,18 +288,22 @@ func (s *Service) PullPatch(ctx context.Context, t Target, rkey string) (*PullPa if err != nil { return nil, err } - return &PullPatch{URI: pull.URI, TargetBranch: record.Target.Branch, Patch: patch}, nil + return &PullPatch{URI: pull.URI, TargetBranch: pullTargetBranch(record.Target), Patch: patch}, nil } -func latestPullPatch(pull *tangled.ListItem, rkey string) (tangled.PullRecord, string, error) { - var record tangled.PullRecord +func latestPullPatch(pull *tangled.ListItem, rkey string) (tangledlex.RepoPull, string, error) { + var record tangledlex.RepoPull if err := json.Unmarshal(pull.Value, &record); err != nil { return record, "", fmt.Errorf("decode pull request %q: %w", rkey, err) } if len(record.Rounds) == 0 { return record, "", fmt.Errorf("pull request %q has no rounds", rkey) } - patchCID := record.Rounds[len(record.Rounds)-1].PatchBlob.Ref.String() + lastRound := record.Rounds[len(record.Rounds)-1] + if lastRound == nil || lastRound.PatchBlob == nil { + return record, "", fmt.Errorf("pull request %q has no patch blob", rkey) + } + patchCID := lastRound.PatchBlob.Ref.String() if patchCID == "" { return record, "", fmt.Errorf("pull request %q has no patch blob", rkey) } diff --git a/internal/app/pulls_test.go b/internal/app/pulls_test.go index ee2086e..576dc9b 100644 --- a/internal/app/pulls_test.go +++ b/internal/app/pulls_test.go @@ -23,7 +23,7 @@ func TestNewPullRecordUsesDistinctSourceAndTarget(t *testing.T) { if record.Target.Repo != "did:plc:upstream" { t.Fatalf("unexpected target: %+v", record.Target) } - if record.Source.Repo != "did:plc:fork" { + if stringValue(record.Source.Repo) != "did:plc:fork" { t.Fatalf("unexpected source: %+v", record.Source) } } diff --git a/internal/app/records.go b/internal/app/records.go index afad669..1fb2f86 100644 --- a/internal/app/records.go +++ b/internal/app/records.go @@ -4,8 +4,10 @@ import ( "context" "encoding/json" "fmt" + "time" "github.com/alyraffauf/tg/atproto" + "github.com/alyraffauf/tg/internal/tangledlex" "github.com/alyraffauf/tg/tangled" ) @@ -37,37 +39,47 @@ func editRecord(ctx context.Context, atClient pdsClient, did, collection, rkey s return fmt.Errorf("get existing record: %w", err) } - record, err := preserveRecord(found.Value) + record, err := editLexiconRecord(collection, found.Value, title, body) if err != nil { return err } - if title != nil { - record["title"] = *title - } - if body != nil { - record["body"] = *body - } - _, _, err = atClient.PutRecord(ctx, atproto.PutRecordInput{ - Repo: did, Collection: collection, Rkey: rkey, Record: record, - }) - return err + return putRecord(ctx, atClient, did, collection, rkey, record) } -// preserveRecord marshals a record value into a map so individual fields can -// be patched without losing fields this client does not model. -func preserveRecord(value any) (map[string]any, error) { +func editLexiconRecord(collection string, value any, title, body *string) (any, error) { data, err := json.Marshal(value) if err != nil { return nil, fmt.Errorf("encode existing record: %w", err) } - var record map[string]any - if err := json.Unmarshal(data, &record); err != nil { - return nil, fmt.Errorf("decode existing record: %w", err) - } - if record == nil { - return nil, fmt.Errorf("existing record is not an object") + + switch collection { + case issueCollection: + var record tangledlex.RepoIssue + if err := json.Unmarshal(data, &record); err != nil { + return nil, fmt.Errorf("decode existing issue: %w", err) + } + if title != nil { + record.Title = *title + } + if body != nil { + record.Body = body + } + return record, nil + case pullCollection: + var record tangledlex.RepoPull + if err := json.Unmarshal(data, &record); err != nil { + return nil, fmt.Errorf("decode existing pull request: %w", err) + } + if title != nil { + record.Title = *title + } + if body != nil { + record.Body = body + } + return record, nil + default: + return nil, fmt.Errorf("editing records in collection %q is not supported", collection) } - return record, nil } // putState writes an issue.state or pull.status record keyed by rkey. state @@ -76,16 +88,25 @@ func preserveRecord(value any) (map[string]any, error) { func putState(ctx context.Context, atClient pdsClient, did, rkey, collection, target, state string) error { if collection == tangled.IssueCollection { state = tangled.IssueCollection + tangled.IssueStateSuffix + "." + state - return putRecord(ctx, atClient, did, tangled.IssueCollection+tangled.IssueStateSuffix, rkey, tangled.IssueStateRecord{ - Type: tangled.IssueCollection + tangled.IssueStateSuffix, - Issue: target, - State: state, + return putRecord(ctx, atClient, did, tangled.IssueCollection+tangled.IssueStateSuffix, rkey, tangledlex.RepoIssueState{ + LexiconTypeID: tangled.IssueCollection + tangled.IssueStateSuffix, + CreatedAt: time.Now().UTC().Format(time.RFC3339), + Issue: target, + State: state, }) } state = tangled.PullCollection + tangled.PullStatusSuffix + "." + state - return putRecord(ctx, atClient, did, tangled.PullCollection+tangled.PullStatusSuffix, rkey, tangled.PullStatusRecord{ - Type: tangled.PullCollection + tangled.PullStatusSuffix, - Pull: target, - Status: state, + return putRecord(ctx, atClient, did, tangled.PullCollection+tangled.PullStatusSuffix, rkey, tangledlex.RepoPullStatus{ + LexiconTypeID: tangled.PullCollection + tangled.PullStatusSuffix, + CreatedAt: time.Now().UTC().Format(time.RFC3339), + Pull: target, + Status: state, }) } + +func optionalString(value string) *string { + if value == "" { + return nil + } + return &value +} diff --git a/internal/app/records_test.go b/internal/app/records_test.go new file mode 100644 index 0000000..a17011d --- /dev/null +++ b/internal/app/records_test.go @@ -0,0 +1,48 @@ +package app + +import ( + "strings" + "testing" + + "github.com/alyraffauf/tg/internal/tangledlex" +) + +func TestEditLexiconRecordKeepsValidIssueWritable(t *testing.T) { + title := "Updated title" + body := "Updated body" + record, err := editLexiconRecord(issueCollection, map[string]any{ + "$type": issueCollection, + "repo": "did:plc:abc123", + "title": "Original title", + "createdAt": "2026-07-25T12:00:00Z", + }, &title, &body) + if err != nil { + t.Fatalf("editLexiconRecord() error = %v", err) + } + + issue, ok := record.(tangledlex.RepoIssue) + if !ok { + t.Fatalf("editLexiconRecord() record = %T, want tangledlex.RepoIssue", record) + } + if issue.Title != title || issue.Body == nil || *issue.Body != body { + t.Fatalf("edited issue = %+v", issue) + } + if err := tangledlex.ValidateRecord(issueCollection, issue); err != nil { + t.Fatalf("ValidateRecord() error = %v", err) + } +} + +func TestEditLexiconRecordRejectsHistoricalRecordAtWriteBoundary(t *testing.T) { + title := "Updated title" + record, err := editLexiconRecord(issueCollection, map[string]any{ + "repo": "did:plc:abc123", + "title": "Original title", + "createdAt": "2026-07-25T12:00:00Z", + }, &title, nil) + if err != nil { + t.Fatalf("editLexiconRecord() error = %v", err) + } + if err := tangledlex.ValidateRecord(issueCollection, record); err == nil || !strings.Contains(err.Error(), "$type") { + t.Fatalf("ValidateRecord() error = %v, want type validation error", err) + } +} diff --git a/internal/app/repos.go b/internal/app/repos.go index 767b22f..8219033 100644 --- a/internal/app/repos.go +++ b/internal/app/repos.go @@ -10,6 +10,7 @@ import ( "github.com/alyraffauf/tg/atproto" "github.com/alyraffauf/tg/internal/gitutil" + "github.com/alyraffauf/tg/internal/tangledlex" "github.com/alyraffauf/tg/knot" "github.com/alyraffauf/tg/tangled" ) @@ -25,7 +26,7 @@ func (s *Service) ViewRepo(ctx context.Context, t Target) (*RepoItem, error) { if err != nil { return nil, fmt.Errorf("get repo %s: %w", t, err) } - name := tangledRepo.Value.Name + name := stringValue(tangledRepo.Value.Name) if name == "" { name = t.Repo } @@ -34,9 +35,9 @@ func (s *Service) ViewRepo(ctx context.Context, t Target) (*RepoItem, error) { Author: t.Handle, URI: repoURI, Knot: tangledRepo.Value.Knot, - Description: tangledRepo.Value.Description, + Description: stringValue(tangledRepo.Value.Description), CreatedAt: tangledRepo.Value.CreatedAt, - RepoDid: tangledRepo.Value.RepoDid, + RepoDid: stringValue(tangledRepo.Value.RepoDid), }, nil } @@ -56,7 +57,7 @@ func (s *Service) ListRepos(ctx context.Context, handle string) ([]RepoItem, err func buildRepoItems(items []tangled.Repo, author string) []RepoItem { result := make([]RepoItem, 0, len(items)) for _, tangledRepo := range items { - name := tangledRepo.Value.Name + name := stringValue(tangledRepo.Value.Name) if name == "" { // Fall back to the rkey segment of the at:// URI. if idx := strings.LastIndex(tangledRepo.URI, "/"); idx != -1 { @@ -68,9 +69,9 @@ func buildRepoItems(items []tangled.Repo, author string) []RepoItem { URI: tangledRepo.URI, Author: author, Knot: tangledRepo.Value.Knot, - Description: tangledRepo.Value.Description, + Description: stringValue(tangledRepo.Value.Description), CreatedAt: tangledRepo.Value.CreatedAt, - RepoDid: tangledRepo.Value.RepoDid, + RepoDid: stringValue(tangledRepo.Value.RepoDid), }) } return result @@ -142,14 +143,14 @@ func (s *Service) provisionRepo(ctx context.Context, in ProvisionRepoInput) (uri if err != nil { return "", "", err } - record := tangled.RepoRecord{ - Type: repoCollection, - Knot: in.KnotHost, - CreatedAt: time.Now().UTC().Format(time.RFC3339), - RepoDid: repoDid, + record := tangledlex.Repo{ + LexiconTypeID: repoCollection, + Knot: in.KnotHost, + CreatedAt: time.Now().UTC().Format(time.RFC3339), + RepoDid: optionalString(repoDid), } if in.Description != "" { - record.Description = in.Description + record.Description = optionalString(in.Description) } uri, _, err = atClient.PutRecord(ctx, atproto.PutRecordInput{ Repo: did, @@ -358,6 +359,19 @@ func (s *Service) DeleteRepo(ctx context.Context, t Target) (*RepoDeleteResult, } rkey := extractRKey(repo.URI) existingRecord, getErr := atClient.GetRecord(ctx, did, repoCollection, rkey) + var recordToRestore tangledlex.Repo + if getErr == nil { + data, err := json.Marshal(existingRecord.Value) + if err != nil { + return nil, fmt.Errorf("encode repository record for restore: %w", err) + } + if err := json.Unmarshal(data, &recordToRestore); err != nil { + return nil, fmt.Errorf("decode repository record for restore: %w", err) + } + if err := tangledlex.ValidateRecord(repoCollection, recordToRestore); err != nil { + return nil, fmt.Errorf("validate repository record for restore: %w", err) + } + } // getErr is non-fatal: the record may already be deleted. Only call // DeleteRecord if it still exists. @@ -381,7 +395,7 @@ func (s *Service) DeleteRepo(ctx context.Context, t Target) (*RepoDeleteResult, }); err != nil { if getErr == nil { if _, _, restoreErr := atClient.PutRecord(ctx, atproto.PutRecordInput{ - Repo: did, Collection: repoCollection, Rkey: rkey, Record: existingRecord.Value, + Repo: did, Collection: repoCollection, Rkey: rkey, Record: recordToRestore, }); restoreErr != nil { return nil, fmt.Errorf("delete knot repository: %w; restore repository record: %v", err, restoreErr) } @@ -419,13 +433,13 @@ func (s *Service) ForkRepo(ctx context.Context, source Target, name string) (*Re Repo: ownerDID, Collection: repoCollection, Rkey: name, - Record: tangled.RepoRecord{ - Type: repoCollection, - Name: name, - Knot: src.Knot, - CreatedAt: time.Now().UTC().Format(time.RFC3339), - RepoDid: repoDID, - Source: src.URI, + Record: tangledlex.Repo{ + LexiconTypeID: repoCollection, + Name: optionalString(name), + Knot: src.Knot, + CreatedAt: time.Now().UTC().Format(time.RFC3339), + RepoDid: optionalString(repoDID), + Source: optionalString(src.URI), }, }) if err != nil { @@ -465,13 +479,13 @@ func (s *Service) getForkSource(ctx context.Context, t Target) (forkSource, erro if repo.Value.Knot == "" { return forkSource{}, fmt.Errorf("source repository %s has no knot", t) } - if repo.Value.RepoDid == "" { + if stringValue(repo.Value.RepoDid) == "" { return forkSource{}, fmt.Errorf("source repository %s has no repo DID", t) } if repo.URI != "" { uri = repo.URI } - return forkSource{URI: uri, Knot: repo.Value.Knot, RepoDID: repo.Value.RepoDid}, nil + return forkSource{URI: uri, Knot: repo.Value.Knot, RepoDID: stringValue(repo.Value.RepoDid)}, nil } func (s *Service) deleteFork(ctx context.Context, atClient pdsClient, knotHost, did, name string) error { diff --git a/internal/app/service_test.go b/internal/app/service_test.go index 08b71a8..060a01a 100644 --- a/internal/app/service_test.go +++ b/internal/app/service_test.go @@ -15,6 +15,7 @@ import ( "github.com/alyraffauf/tg/atproto" "github.com/alyraffauf/tg/internal/gitutil" + "github.com/alyraffauf/tg/internal/tangledlex" "github.com/alyraffauf/tg/knot" "github.com/alyraffauf/tg/tangled" "github.com/bluesky-social/indigo/atproto/atclient" @@ -63,12 +64,12 @@ func TestCreateRepoRecordsDefaultBranchOutcome(t *testing.T) { } func TestDeleteRepoRestoresRecordWhenKnotDeleteFails(t *testing.T) { - pds := &testPDS{record: &atproto.GetRecordOutput{Value: map[string]any{"$type": repoCollection, "knot": "knot.example"}}} + pds := &testPDS{record: &atproto.GetRecordOutput{Value: map[string]any{"$type": repoCollection, "knot": "knot.example", "createdAt": "2026-07-25T12:00:00Z"}}} knotClient := &testKnot{deleteErr: errors.New("knot unavailable")} service := testService(pds, &testGit{}, knotClient) service.appview = testAppview{repo: &tangled.Repo{ URI: "at://did:plc:owner/sh.tangled.repo/example", - Value: tangled.RepoRecord{Knot: "knot.example"}, + Value: tangledlex.Repo{Knot: "knot.example"}, }} _, err := service.DeleteRepo(context.Background(), Target{Handle: "owner.test", Repo: "example"}) @@ -89,7 +90,7 @@ func TestForkRepoCleansUpWhenRecordWriteFails(t *testing.T) { service := testService(pds, &testGit{}, knotClient) service.appview = testAppview{repo: &tangled.Repo{ URI: "at://did:plc:source/sh.tangled.repo/source", - Value: tangled.RepoRecord{Knot: "knot.example", RepoDid: "did:plc:source-repo"}, + Value: tangledlex.Repo{Knot: "knot.example", RepoDid: optionalString("did:plc:source-repo")}, }} _, err := service.ForkRepo(context.Background(), Target{Handle: "source.test", Repo: "source"}, "fork") @@ -108,7 +109,7 @@ func TestMergePullReportsStatusWriteFailureAfterMerge(t *testing.T) { service.appview = testAppview{ repo: &tangled.Repo{ URI: "at://did:plc:owner/sh.tangled.repo/example", - Value: tangled.RepoRecord{Knot: "knot.example", RepoDid: "did:plc:repo"}, + Value: tangledlex.Repo{Knot: "knot.example", RepoDid: optionalString("did:plc:repo")}, }, pulls: &tangled.List{Items: []tangled.ListItem{{ URI: "at://did:plc:owner/sh.tangled.repo.pull/pr-1", @@ -269,6 +270,11 @@ func (p *testPDS) PutRecord(_ context.Context, input atproto.PutRecordInput) (st if p.putErr != nil { return "", "", p.putErr } + if strings.HasPrefix(input.Collection, "sh.tangled.") { + if err := tangledlex.ValidateRecord(input.Collection, input.Record); err != nil { + return "", "", err + } + } return fmt.Sprintf("at://%s/%s/%s", input.Repo, input.Collection, input.Rkey), "", nil } diff --git a/internal/app/sshkeys.go b/internal/app/sshkeys.go index 87a62c8..dda8d43 100644 --- a/internal/app/sshkeys.go +++ b/internal/app/sshkeys.go @@ -7,7 +7,7 @@ import ( "time" "github.com/alyraffauf/tg/atproto" - "github.com/alyraffauf/tg/tangled" + "github.com/alyraffauf/tg/internal/tangledlex" "github.com/bluesky-social/indigo/atproto/syntax" ) @@ -21,11 +21,11 @@ func (s *Service) AddSSHKey(ctx context.Context, name, key string) (*SSHKeyAddRe Repo: did, Collection: sshKeyCollection, Rkey: string(syntax.NewTIDNow(0)), - Record: tangled.SSHKeyRecord{ - Type: sshKeyCollection, - Key: key, - Name: name, - CreatedAt: time.Now().UTC().Format(time.RFC3339), + Record: tangledlex.PublicKey{ + LexiconTypeID: sshKeyCollection, + Key: key, + Name: name, + CreatedAt: time.Now().UTC().Format(time.RFC3339), }, }) if err != nil { @@ -66,7 +66,7 @@ func (s *Service) DeleteSSHKey(ctx context.Context, rkey string) (*DeletedRecord func buildSSHKeyItems(records []atproto.RecordItem) []SSHKeyItem { items := make([]SSHKeyItem, 0, len(records)) for _, rec := range records { - var key tangled.SSHKeyRecord + var key tangledlex.PublicKey data, err := json.Marshal(rec.Value) if err != nil { continue diff --git a/internal/app/strings.go b/internal/app/strings.go index 12915c1..8653357 100644 --- a/internal/app/strings.go +++ b/internal/app/strings.go @@ -7,7 +7,7 @@ import ( "time" "github.com/alyraffauf/tg/atproto" - "github.com/alyraffauf/tg/tangled" + "github.com/alyraffauf/tg/internal/tangledlex" "github.com/bluesky-social/indigo/atproto/syntax" ) @@ -29,12 +29,12 @@ func (s *Service) CreateString(ctx context.Context, in CreateStringInput) (*Crea Repo: did, Collection: stringCollection, Rkey: rkey, - Record: tangled.StringRecord{ - Type: stringCollection, - Filename: in.Filename, - Description: in.Description, - Contents: in.Contents, - CreatedAt: time.Now().UTC().Format(time.RFC3339), + Record: tangledlex.String{ + LexiconTypeID: stringCollection, + Filename: in.Filename, + Description: in.Description, + Contents: in.Contents, + CreatedAt: time.Now().UTC().Format(time.RFC3339), }, }) if err != nil { @@ -100,7 +100,7 @@ func (s *Service) DeleteString(ctx context.Context, rkey string) (*DeletedRecord func buildStringItems(records []atproto.RecordItem) []StringItem { items := make([]StringItem, 0, len(records)) for _, rec := range records { - var str tangled.StringRecord + var str tangledlex.String data, err := json.Marshal(rec.Value) if err != nil { continue @@ -124,14 +124,14 @@ func buildStringItems(records []atproto.RecordItem) []StringItem { return items } -func decodeStringRecord(value any) (tangled.StringRecord, error) { +func decodeStringRecord(value any) (tangledlex.String, error) { data, err := json.Marshal(value) if err != nil { - return tangled.StringRecord{}, fmt.Errorf("encode record: %w", err) + return tangledlex.String{}, fmt.Errorf("encode record: %w", err) } - var record tangled.StringRecord + var record tangledlex.String if err := json.Unmarshal(data, &record); err != nil { - return tangled.StringRecord{}, fmt.Errorf("decode record: %w", err) + return tangledlex.String{}, fmt.Errorf("decode record: %w", err) } return record, nil } diff --git a/internal/app/strings_test.go b/internal/app/strings_test.go index 53c71dd..861eb83 100644 --- a/internal/app/strings_test.go +++ b/internal/app/strings_test.go @@ -3,16 +3,15 @@ package app import ( "testing" - "github.com/alyraffauf/tg/tangled" - "github.com/alyraffauf/tg/atproto" + "github.com/alyraffauf/tg/internal/tangledlex" ) func TestDecodeStringRecord(t *testing.T) { tests := []struct { name string value any - want tangled.StringRecord + want tangledlex.String wantErr bool }{ { @@ -24,12 +23,12 @@ func TestDecodeStringRecord(t *testing.T) { "contents": "# hello", "createdAt": "2026-07-18T23:15:54+03:00", }, - want: tangled.StringRecord{ - Type: "sh.tangled.string", - Filename: "hello.md", - Description: "a greeting", - Contents: "# hello", - CreatedAt: "2026-07-18T23:15:54+03:00", + want: tangledlex.String{ + LexiconTypeID: "sh.tangled.string", + Filename: "hello.md", + Description: "a greeting", + Contents: "# hello", + CreatedAt: "2026-07-18T23:15:54+03:00", }, }, { @@ -40,11 +39,11 @@ func TestDecodeStringRecord(t *testing.T) { "contents": "no description", "createdAt": "2026-07-18T12:00:00Z", }, - want: tangled.StringRecord{ - Type: "sh.tangled.string", - Filename: "bare.md", - Contents: "no description", - CreatedAt: "2026-07-18T12:00:00Z", + want: tangledlex.String{ + LexiconTypeID: "sh.tangled.string", + Filename: "bare.md", + Contents: "no description", + CreatedAt: "2026-07-18T12:00:00Z", }, }, { @@ -55,11 +54,11 @@ func TestDecodeStringRecord(t *testing.T) { "contents": "empty filename", "createdAt": "2026-07-18T12:00:00Z", }, - want: tangled.StringRecord{ - Type: "sh.tangled.string", - Filename: "", - Contents: "empty filename", - CreatedAt: "2026-07-18T12:00:00Z", + want: tangledlex.String{ + LexiconTypeID: "sh.tangled.string", + Filename: "", + Contents: "empty filename", + CreatedAt: "2026-07-18T12:00:00Z", }, }, { diff --git a/internal/app/target.go b/internal/app/target.go index eff1bed..4a5c451 100644 --- a/internal/app/target.go +++ b/internal/app/target.go @@ -65,7 +65,7 @@ func (s *Service) resolveRepo(ctx context.Context, t Target) (*tangled.Repo, err } for index := range repos.Items { repo := &repos.Items[index] - if repo.Value.Name == t.Repo || extractRKey(repo.URI) == t.Repo { + if stringValue(repo.Value.Name) == t.Repo || extractRKey(repo.URI) == t.Repo { return repo, nil } } @@ -78,7 +78,7 @@ func (s *Service) repoDID(ctx context.Context, t Target) (string, error) { if err != nil { return "", err } - return record.Value.RepoDid, nil + return stringValue(record.Value.RepoDid), nil } // requireOwnedRepo resolves a target and verifies it is owned by did. diff --git a/internal/tangledlex/README.md b/internal/tangledlex/README.md new file mode 100644 index 0000000..97e6fad --- /dev/null +++ b/internal/tangledlex/README.md @@ -0,0 +1,30 @@ +# Tangled lexicons + +This package contains the generated Go representations of the Tangled record +and Bobbin query lexicons that `tg` consumes. It is deliberately internal: +`tangled/` remains the handwritten Bobbin client and turns these generated +types into the CLI's stable application types. + +`lock.json` pins the upstream Tangled Core commit used for the checked-in +schemas. `schemas.txt` is the explicit allowlist of upstream lexicons that tg +depends on. Keeping both means an upstream schema addition cannot silently +become part of tg's wire contract. + +## Updating + +Choose a reviewed, full 40-character Tangled Core commit and run: + +```sh +cd internal/tangledlex +./update-schemas.sh +``` + +The script fetches only that commit, replaces exactly the paths listed in +`schemas.txt`, updates `lock.json`, and regenerates the Go and CBOR code. +Review the resulting schema and generated-code diff, then run `nix fmt` and +`nix build path:.#tg` from the repository root. + +To add support for a new lexicon, add its path (relative to Tangled Core's +`lexicons/`) to `schemas.txt`. If it defines a record type, also add that +type to the CBOR generator's list in `cborgen/main.go`; then run the update +script. diff --git a/internal/tangledlex/cbor_gen.go b/internal/tangledlex/cbor_gen.go new file mode 100644 index 0000000..42db667 --- /dev/null +++ b/internal/tangledlex/cbor_gen.go @@ -0,0 +1,3726 @@ +// Code generated by github.com/whyrusleeping/cbor-gen. DO NOT EDIT. + +package tangledlex + +import ( + "fmt" + "io" + "math" + "sort" + + util "github.com/bluesky-social/indigo/lex/util" + cid "github.com/ipfs/go-cid" + cbg "github.com/whyrusleeping/cbor-gen" + xerrors "golang.org/x/xerrors" +) + +var _ = xerrors.Errorf +var _ = cid.Undef +var _ = math.E +var _ = sort.Sort + +func (t *PublicKey) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{164}); err != nil { + return err + } + + // t.Key (string) (string) + if len("key") > 1000000 { + return xerrors.Errorf("Value in field \"key\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("key"))); err != nil { + return err + } + if _, err := cw.WriteString(string("key")); err != nil { + return err + } + + if len(t.Key) > 1000000 { + return xerrors.Errorf("Value in field t.Key was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Key))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.Key)); err != nil { + return err + } + + // t.Name (string) (string) + if len("name") > 1000000 { + return xerrors.Errorf("Value in field \"name\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("name"))); err != nil { + return err + } + if _, err := cw.WriteString(string("name")); err != nil { + return err + } + + if len(t.Name) > 1000000 { + return xerrors.Errorf("Value in field t.Name was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Name))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.Name)); err != nil { + return err + } + + // t.LexiconTypeID (string) (string) + if len("$type") > 1000000 { + return xerrors.Errorf("Value in field \"$type\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil { + return err + } + if _, err := cw.WriteString(string("$type")); err != nil { + return err + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.publicKey"))); err != nil { + return err + } + if _, err := cw.WriteString(string("sh.tangled.publicKey")); err != nil { + return err + } + + // t.CreatedAt (string) (string) + if len("createdAt") > 1000000 { + return xerrors.Errorf("Value in field \"createdAt\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil { + return err + } + if _, err := cw.WriteString(string("createdAt")); err != nil { + return err + } + + if len(t.CreatedAt) > 1000000 { + return xerrors.Errorf("Value in field t.CreatedAt was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.CreatedAt))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.CreatedAt)); err != nil { + return err + } + return nil +} + +func (t *PublicKey) UnmarshalCBOR(r io.Reader) (err error) { + *t = PublicKey{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("PublicKey: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 9) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Key (string) (string) + case "key": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.Key = string(sval) + } + // t.Name (string) (string) + case "name": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.Name = string(sval) + } + // t.LexiconTypeID (string) (string) + case "$type": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.LexiconTypeID = string(sval) + } + // t.CreatedAt (string) (string) + case "createdAt": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.CreatedAt = string(sval) + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *Repo) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + fieldCount := 11 + + if t.Description == nil { + fieldCount-- + } + + if t.Labels == nil { + fieldCount-- + } + + if t.Name == nil { + fieldCount-- + } + + if t.RepoDid == nil { + fieldCount-- + } + + if t.Source == nil { + fieldCount-- + } + + if t.Spindle == nil { + fieldCount-- + } + + if t.Topics == nil { + fieldCount-- + } + + if t.Website == nil { + fieldCount-- + } + + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { + return err + } + + // t.Knot (string) (string) + if len("knot") > 1000000 { + return xerrors.Errorf("Value in field \"knot\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("knot"))); err != nil { + return err + } + if _, err := cw.WriteString(string("knot")); err != nil { + return err + } + + if len(t.Knot) > 1000000 { + return xerrors.Errorf("Value in field t.Knot was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Knot))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.Knot)); err != nil { + return err + } + + // t.Name (string) (string) + if t.Name != nil { + + if len("name") > 1000000 { + return xerrors.Errorf("Value in field \"name\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("name"))); err != nil { + return err + } + if _, err := cw.WriteString(string("name")); err != nil { + return err + } + + if t.Name == nil { + if _, err := cw.Write(cbg.CborNull); err != nil { + return err + } + } else { + if len(*t.Name) > 1000000 { + return xerrors.Errorf("Value in field t.Name was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Name))); err != nil { + return err + } + if _, err := cw.WriteString(string(*t.Name)); err != nil { + return err + } + } + } + + // t.LexiconTypeID (string) (string) + if len("$type") > 1000000 { + return xerrors.Errorf("Value in field \"$type\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil { + return err + } + if _, err := cw.WriteString(string("$type")); err != nil { + return err + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.repo"))); err != nil { + return err + } + if _, err := cw.WriteString(string("sh.tangled.repo")); err != nil { + return err + } + + // t.Labels ([]string) (slice) + if t.Labels != nil { + + if len("labels") > 1000000 { + return xerrors.Errorf("Value in field \"labels\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("labels"))); err != nil { + return err + } + if _, err := cw.WriteString(string("labels")); err != nil { + return err + } + + if len(t.Labels) > 8192 { + return xerrors.Errorf("Slice value in field t.Labels was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Labels))); err != nil { + return err + } + for _, v := range t.Labels { + if len(v) > 1000000 { + return xerrors.Errorf("Value in field v was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil { + return err + } + if _, err := cw.WriteString(string(v)); err != nil { + return err + } + + } + } + + // t.Source (string) (string) + if t.Source != nil { + + if len("source") > 1000000 { + return xerrors.Errorf("Value in field \"source\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("source"))); err != nil { + return err + } + if _, err := cw.WriteString(string("source")); err != nil { + return err + } + + if t.Source == nil { + if _, err := cw.Write(cbg.CborNull); err != nil { + return err + } + } else { + if len(*t.Source) > 1000000 { + return xerrors.Errorf("Value in field t.Source was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Source))); err != nil { + return err + } + if _, err := cw.WriteString(string(*t.Source)); err != nil { + return err + } + } + } + + // t.Topics ([]string) (slice) + if t.Topics != nil { + + if len("topics") > 1000000 { + return xerrors.Errorf("Value in field \"topics\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("topics"))); err != nil { + return err + } + if _, err := cw.WriteString(string("topics")); err != nil { + return err + } + + if len(t.Topics) > 8192 { + return xerrors.Errorf("Slice value in field t.Topics was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Topics))); err != nil { + return err + } + for _, v := range t.Topics { + if len(v) > 1000000 { + return xerrors.Errorf("Value in field v was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil { + return err + } + if _, err := cw.WriteString(string(v)); err != nil { + return err + } + + } + } + + // t.RepoDid (string) (string) + if t.RepoDid != nil { + + if len("repoDid") > 1000000 { + return xerrors.Errorf("Value in field \"repoDid\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("repoDid"))); err != nil { + return err + } + if _, err := cw.WriteString(string("repoDid")); err != nil { + return err + } + + if t.RepoDid == nil { + if _, err := cw.Write(cbg.CborNull); err != nil { + return err + } + } else { + if len(*t.RepoDid) > 1000000 { + return xerrors.Errorf("Value in field t.RepoDid was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.RepoDid))); err != nil { + return err + } + if _, err := cw.WriteString(string(*t.RepoDid)); err != nil { + return err + } + } + } + + // t.Spindle (string) (string) + if t.Spindle != nil { + + if len("spindle") > 1000000 { + return xerrors.Errorf("Value in field \"spindle\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("spindle"))); err != nil { + return err + } + if _, err := cw.WriteString(string("spindle")); err != nil { + return err + } + + if t.Spindle == nil { + if _, err := cw.Write(cbg.CborNull); err != nil { + return err + } + } else { + if len(*t.Spindle) > 1000000 { + return xerrors.Errorf("Value in field t.Spindle was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Spindle))); err != nil { + return err + } + if _, err := cw.WriteString(string(*t.Spindle)); err != nil { + return err + } + } + } + + // t.Website (string) (string) + if t.Website != nil { + + if len("website") > 1000000 { + return xerrors.Errorf("Value in field \"website\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("website"))); err != nil { + return err + } + if _, err := cw.WriteString(string("website")); err != nil { + return err + } + + if t.Website == nil { + if _, err := cw.Write(cbg.CborNull); err != nil { + return err + } + } else { + if len(*t.Website) > 1000000 { + return xerrors.Errorf("Value in field t.Website was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Website))); err != nil { + return err + } + if _, err := cw.WriteString(string(*t.Website)); err != nil { + return err + } + } + } + + // t.CreatedAt (string) (string) + if len("createdAt") > 1000000 { + return xerrors.Errorf("Value in field \"createdAt\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil { + return err + } + if _, err := cw.WriteString(string("createdAt")); err != nil { + return err + } + + if len(t.CreatedAt) > 1000000 { + return xerrors.Errorf("Value in field t.CreatedAt was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.CreatedAt))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.CreatedAt)); err != nil { + return err + } + + // t.Description (string) (string) + if t.Description != nil { + + if len("description") > 1000000 { + return xerrors.Errorf("Value in field \"description\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("description"))); err != nil { + return err + } + if _, err := cw.WriteString(string("description")); err != nil { + return err + } + + if t.Description == nil { + if _, err := cw.Write(cbg.CborNull); err != nil { + return err + } + } else { + if len(*t.Description) > 1000000 { + return xerrors.Errorf("Value in field t.Description was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Description))); err != nil { + return err + } + if _, err := cw.WriteString(string(*t.Description)); err != nil { + return err + } + } + } + return nil +} + +func (t *Repo) UnmarshalCBOR(r io.Reader) (err error) { + *t = Repo{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("Repo: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 11) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Knot (string) (string) + case "knot": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.Knot = string(sval) + } + // t.Name (string) (string) + case "name": + + { + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.Name = (*string)(&sval) + } + } + // t.LexiconTypeID (string) (string) + case "$type": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.LexiconTypeID = string(sval) + } + // t.Labels ([]string) (slice) + case "labels": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 8192 { + return fmt.Errorf("t.Labels: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.Labels = make([]string, extra) + } + + for i := 0; i < int(extra); i++ { + { + var maj byte + var extra uint64 + var err error + _ = maj + _ = extra + _ = err + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.Labels[i] = string(sval) + } + + } + } + // t.Source (string) (string) + case "source": + + { + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.Source = (*string)(&sval) + } + } + // t.Topics ([]string) (slice) + case "topics": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 8192 { + return fmt.Errorf("t.Topics: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.Topics = make([]string, extra) + } + + for i := 0; i < int(extra); i++ { + { + var maj byte + var extra uint64 + var err error + _ = maj + _ = extra + _ = err + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.Topics[i] = string(sval) + } + + } + } + // t.RepoDid (string) (string) + case "repoDid": + + { + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.RepoDid = (*string)(&sval) + } + } + // t.Spindle (string) (string) + case "spindle": + + { + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.Spindle = (*string)(&sval) + } + } + // t.Website (string) (string) + case "website": + + { + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.Website = (*string)(&sval) + } + } + // t.CreatedAt (string) (string) + case "createdAt": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.CreatedAt = string(sval) + } + // t.Description (string) (string) + case "description": + + { + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.Description = (*string)(&sval) + } + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *RepoIssue) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + fieldCount := 7 + + if t.Body == nil { + fieldCount-- + } + + if t.Mentions == nil { + fieldCount-- + } + + if t.References == nil { + fieldCount-- + } + + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { + return err + } + + // t.Body (string) (string) + if t.Body != nil { + + if len("body") > 1000000 { + return xerrors.Errorf("Value in field \"body\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("body"))); err != nil { + return err + } + if _, err := cw.WriteString(string("body")); err != nil { + return err + } + + if t.Body == nil { + if _, err := cw.Write(cbg.CborNull); err != nil { + return err + } + } else { + if len(*t.Body) > 1000000 { + return xerrors.Errorf("Value in field t.Body was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Body))); err != nil { + return err + } + if _, err := cw.WriteString(string(*t.Body)); err != nil { + return err + } + } + } + + // t.Repo (string) (string) + if len("repo") > 1000000 { + return xerrors.Errorf("Value in field \"repo\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("repo"))); err != nil { + return err + } + if _, err := cw.WriteString(string("repo")); err != nil { + return err + } + + if len(t.Repo) > 1000000 { + return xerrors.Errorf("Value in field t.Repo was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Repo))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.Repo)); err != nil { + return err + } + + // t.LexiconTypeID (string) (string) + if len("$type") > 1000000 { + return xerrors.Errorf("Value in field \"$type\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil { + return err + } + if _, err := cw.WriteString(string("$type")); err != nil { + return err + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.repo.issue"))); err != nil { + return err + } + if _, err := cw.WriteString(string("sh.tangled.repo.issue")); err != nil { + return err + } + + // t.Title (string) (string) + if len("title") > 1000000 { + return xerrors.Errorf("Value in field \"title\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("title"))); err != nil { + return err + } + if _, err := cw.WriteString(string("title")); err != nil { + return err + } + + if len(t.Title) > 1000000 { + return xerrors.Errorf("Value in field t.Title was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Title))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.Title)); err != nil { + return err + } + + // t.Mentions ([]string) (slice) + if t.Mentions != nil { + + if len("mentions") > 1000000 { + return xerrors.Errorf("Value in field \"mentions\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("mentions"))); err != nil { + return err + } + if _, err := cw.WriteString(string("mentions")); err != nil { + return err + } + + if len(t.Mentions) > 8192 { + return xerrors.Errorf("Slice value in field t.Mentions was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Mentions))); err != nil { + return err + } + for _, v := range t.Mentions { + if len(v) > 1000000 { + return xerrors.Errorf("Value in field v was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil { + return err + } + if _, err := cw.WriteString(string(v)); err != nil { + return err + } + + } + } + + // t.CreatedAt (string) (string) + if len("createdAt") > 1000000 { + return xerrors.Errorf("Value in field \"createdAt\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil { + return err + } + if _, err := cw.WriteString(string("createdAt")); err != nil { + return err + } + + if len(t.CreatedAt) > 1000000 { + return xerrors.Errorf("Value in field t.CreatedAt was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.CreatedAt))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.CreatedAt)); err != nil { + return err + } + + // t.References ([]string) (slice) + if t.References != nil { + + if len("references") > 1000000 { + return xerrors.Errorf("Value in field \"references\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("references"))); err != nil { + return err + } + if _, err := cw.WriteString(string("references")); err != nil { + return err + } + + if len(t.References) > 8192 { + return xerrors.Errorf("Slice value in field t.References was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.References))); err != nil { + return err + } + for _, v := range t.References { + if len(v) > 1000000 { + return xerrors.Errorf("Value in field v was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil { + return err + } + if _, err := cw.WriteString(string(v)); err != nil { + return err + } + + } + } + return nil +} + +func (t *RepoIssue) UnmarshalCBOR(r io.Reader) (err error) { + *t = RepoIssue{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("RepoIssue: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 10) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Body (string) (string) + case "body": + + { + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.Body = (*string)(&sval) + } + } + // t.Repo (string) (string) + case "repo": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.Repo = string(sval) + } + // t.LexiconTypeID (string) (string) + case "$type": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.LexiconTypeID = string(sval) + } + // t.Title (string) (string) + case "title": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.Title = string(sval) + } + // t.Mentions ([]string) (slice) + case "mentions": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 8192 { + return fmt.Errorf("t.Mentions: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.Mentions = make([]string, extra) + } + + for i := 0; i < int(extra); i++ { + { + var maj byte + var extra uint64 + var err error + _ = maj + _ = extra + _ = err + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.Mentions[i] = string(sval) + } + + } + } + // t.CreatedAt (string) (string) + case "createdAt": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.CreatedAt = string(sval) + } + // t.References ([]string) (slice) + case "references": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 8192 { + return fmt.Errorf("t.References: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.References = make([]string, extra) + } + + for i := 0; i < int(extra); i++ { + { + var maj byte + var extra uint64 + var err error + _ = maj + _ = extra + _ = err + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.References[i] = string(sval) + } + + } + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *RepoIssueComment) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + fieldCount := 7 + + if t.Mentions == nil { + fieldCount-- + } + + if t.References == nil { + fieldCount-- + } + + if t.ReplyTo == nil { + fieldCount-- + } + + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { + return err + } + + // t.Body (string) (string) + if len("body") > 1000000 { + return xerrors.Errorf("Value in field \"body\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("body"))); err != nil { + return err + } + if _, err := cw.WriteString(string("body")); err != nil { + return err + } + + if len(t.Body) > 1000000 { + return xerrors.Errorf("Value in field t.Body was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Body))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.Body)); err != nil { + return err + } + + // t.LexiconTypeID (string) (string) + if len("$type") > 1000000 { + return xerrors.Errorf("Value in field \"$type\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil { + return err + } + if _, err := cw.WriteString(string("$type")); err != nil { + return err + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.repo.issue.comment"))); err != nil { + return err + } + if _, err := cw.WriteString(string("sh.tangled.repo.issue.comment")); err != nil { + return err + } + + // t.Issue (string) (string) + if len("issue") > 1000000 { + return xerrors.Errorf("Value in field \"issue\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("issue"))); err != nil { + return err + } + if _, err := cw.WriteString(string("issue")); err != nil { + return err + } + + if len(t.Issue) > 1000000 { + return xerrors.Errorf("Value in field t.Issue was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Issue))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.Issue)); err != nil { + return err + } + + // t.ReplyTo (string) (string) + if t.ReplyTo != nil { + + if len("replyTo") > 1000000 { + return xerrors.Errorf("Value in field \"replyTo\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("replyTo"))); err != nil { + return err + } + if _, err := cw.WriteString(string("replyTo")); err != nil { + return err + } + + if t.ReplyTo == nil { + if _, err := cw.Write(cbg.CborNull); err != nil { + return err + } + } else { + if len(*t.ReplyTo) > 1000000 { + return xerrors.Errorf("Value in field t.ReplyTo was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.ReplyTo))); err != nil { + return err + } + if _, err := cw.WriteString(string(*t.ReplyTo)); err != nil { + return err + } + } + } + + // t.Mentions ([]string) (slice) + if t.Mentions != nil { + + if len("mentions") > 1000000 { + return xerrors.Errorf("Value in field \"mentions\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("mentions"))); err != nil { + return err + } + if _, err := cw.WriteString(string("mentions")); err != nil { + return err + } + + if len(t.Mentions) > 8192 { + return xerrors.Errorf("Slice value in field t.Mentions was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Mentions))); err != nil { + return err + } + for _, v := range t.Mentions { + if len(v) > 1000000 { + return xerrors.Errorf("Value in field v was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil { + return err + } + if _, err := cw.WriteString(string(v)); err != nil { + return err + } + + } + } + + // t.CreatedAt (string) (string) + if len("createdAt") > 1000000 { + return xerrors.Errorf("Value in field \"createdAt\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil { + return err + } + if _, err := cw.WriteString(string("createdAt")); err != nil { + return err + } + + if len(t.CreatedAt) > 1000000 { + return xerrors.Errorf("Value in field t.CreatedAt was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.CreatedAt))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.CreatedAt)); err != nil { + return err + } + + // t.References ([]string) (slice) + if t.References != nil { + + if len("references") > 1000000 { + return xerrors.Errorf("Value in field \"references\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("references"))); err != nil { + return err + } + if _, err := cw.WriteString(string("references")); err != nil { + return err + } + + if len(t.References) > 8192 { + return xerrors.Errorf("Slice value in field t.References was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.References))); err != nil { + return err + } + for _, v := range t.References { + if len(v) > 1000000 { + return xerrors.Errorf("Value in field v was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil { + return err + } + if _, err := cw.WriteString(string(v)); err != nil { + return err + } + + } + } + return nil +} + +func (t *RepoIssueComment) UnmarshalCBOR(r io.Reader) (err error) { + *t = RepoIssueComment{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("RepoIssueComment: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 10) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Body (string) (string) + case "body": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.Body = string(sval) + } + // t.LexiconTypeID (string) (string) + case "$type": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.LexiconTypeID = string(sval) + } + // t.Issue (string) (string) + case "issue": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.Issue = string(sval) + } + // t.ReplyTo (string) (string) + case "replyTo": + + { + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.ReplyTo = (*string)(&sval) + } + } + // t.Mentions ([]string) (slice) + case "mentions": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 8192 { + return fmt.Errorf("t.Mentions: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.Mentions = make([]string, extra) + } + + for i := 0; i < int(extra); i++ { + { + var maj byte + var extra uint64 + var err error + _ = maj + _ = extra + _ = err + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.Mentions[i] = string(sval) + } + + } + } + // t.CreatedAt (string) (string) + case "createdAt": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.CreatedAt = string(sval) + } + // t.References ([]string) (slice) + case "references": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 8192 { + return fmt.Errorf("t.References: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.References = make([]string, extra) + } + + for i := 0; i < int(extra); i++ { + { + var maj byte + var extra uint64 + var err error + _ = maj + _ = extra + _ = err + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.References[i] = string(sval) + } + + } + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *RepoIssueState) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{164}); err != nil { + return err + } + + // t.LexiconTypeID (string) (string) + if len("$type") > 1000000 { + return xerrors.Errorf("Value in field \"$type\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil { + return err + } + if _, err := cw.WriteString(string("$type")); err != nil { + return err + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.repo.issue.state"))); err != nil { + return err + } + if _, err := cw.WriteString(string("sh.tangled.repo.issue.state")); err != nil { + return err + } + + // t.Issue (string) (string) + if len("issue") > 1000000 { + return xerrors.Errorf("Value in field \"issue\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("issue"))); err != nil { + return err + } + if _, err := cw.WriteString(string("issue")); err != nil { + return err + } + + if len(t.Issue) > 1000000 { + return xerrors.Errorf("Value in field t.Issue was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Issue))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.Issue)); err != nil { + return err + } + + // t.State (string) (string) + if len("state") > 1000000 { + return xerrors.Errorf("Value in field \"state\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("state"))); err != nil { + return err + } + if _, err := cw.WriteString(string("state")); err != nil { + return err + } + + if len(t.State) > 1000000 { + return xerrors.Errorf("Value in field t.State was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.State))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.State)); err != nil { + return err + } + + // t.CreatedAt (string) (string) + if len("createdAt") > 1000000 { + return xerrors.Errorf("Value in field \"createdAt\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil { + return err + } + if _, err := cw.WriteString(string("createdAt")); err != nil { + return err + } + + if len(t.CreatedAt) > 1000000 { + return xerrors.Errorf("Value in field t.CreatedAt was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.CreatedAt))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.CreatedAt)); err != nil { + return err + } + return nil +} + +func (t *RepoIssueState) UnmarshalCBOR(r io.Reader) (err error) { + *t = RepoIssueState{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("RepoIssueState: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 9) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.LexiconTypeID (string) (string) + case "$type": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.LexiconTypeID = string(sval) + } + // t.Issue (string) (string) + case "issue": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.Issue = string(sval) + } + // t.State (string) (string) + case "state": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.State = string(sval) + } + // t.CreatedAt (string) (string) + case "createdAt": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.CreatedAt = string(sval) + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *RepoPull) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + fieldCount := 10 + + if t.Body == nil { + fieldCount-- + } + + if t.DependentOn == nil { + fieldCount-- + } + + if t.Mentions == nil { + fieldCount-- + } + + if t.References == nil { + fieldCount-- + } + + if t.Source == nil { + fieldCount-- + } + + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { + return err + } + + // t.Body (string) (string) + if t.Body != nil { + + if len("body") > 1000000 { + return xerrors.Errorf("Value in field \"body\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("body"))); err != nil { + return err + } + if _, err := cw.WriteString(string("body")); err != nil { + return err + } + + if t.Body == nil { + if _, err := cw.Write(cbg.CborNull); err != nil { + return err + } + } else { + if len(*t.Body) > 1000000 { + return xerrors.Errorf("Value in field t.Body was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Body))); err != nil { + return err + } + if _, err := cw.WriteString(string(*t.Body)); err != nil { + return err + } + } + } + + // t.LexiconTypeID (string) (string) + if len("$type") > 1000000 { + return xerrors.Errorf("Value in field \"$type\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil { + return err + } + if _, err := cw.WriteString(string("$type")); err != nil { + return err + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.repo.pull"))); err != nil { + return err + } + if _, err := cw.WriteString(string("sh.tangled.repo.pull")); err != nil { + return err + } + + // t.Title (string) (string) + if len("title") > 1000000 { + return xerrors.Errorf("Value in field \"title\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("title"))); err != nil { + return err + } + if _, err := cw.WriteString(string("title")); err != nil { + return err + } + + if len(t.Title) > 1000000 { + return xerrors.Errorf("Value in field t.Title was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Title))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.Title)); err != nil { + return err + } + + // t.Rounds ([]*tangledlex.RepoPull_Round) (slice) + if len("rounds") > 1000000 { + return xerrors.Errorf("Value in field \"rounds\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("rounds"))); err != nil { + return err + } + if _, err := cw.WriteString(string("rounds")); err != nil { + return err + } + + if len(t.Rounds) > 8192 { + return xerrors.Errorf("Slice value in field t.Rounds was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Rounds))); err != nil { + return err + } + for _, v := range t.Rounds { + if err := v.MarshalCBOR(cw); err != nil { + return err + } + + } + + // t.Source (tangledlex.RepoPull_Source) (struct) + if t.Source != nil { + + if len("source") > 1000000 { + return xerrors.Errorf("Value in field \"source\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("source"))); err != nil { + return err + } + if _, err := cw.WriteString(string("source")); err != nil { + return err + } + + if err := t.Source.MarshalCBOR(cw); err != nil { + return err + } + } + + // t.Target (tangledlex.RepoPull_Target) (struct) + if len("target") > 1000000 { + return xerrors.Errorf("Value in field \"target\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("target"))); err != nil { + return err + } + if _, err := cw.WriteString(string("target")); err != nil { + return err + } + + if err := t.Target.MarshalCBOR(cw); err != nil { + return err + } + + // t.Mentions ([]string) (slice) + if t.Mentions != nil { + + if len("mentions") > 1000000 { + return xerrors.Errorf("Value in field \"mentions\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("mentions"))); err != nil { + return err + } + if _, err := cw.WriteString(string("mentions")); err != nil { + return err + } + + if len(t.Mentions) > 8192 { + return xerrors.Errorf("Slice value in field t.Mentions was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Mentions))); err != nil { + return err + } + for _, v := range t.Mentions { + if len(v) > 1000000 { + return xerrors.Errorf("Value in field v was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil { + return err + } + if _, err := cw.WriteString(string(v)); err != nil { + return err + } + + } + } + + // t.CreatedAt (string) (string) + if len("createdAt") > 1000000 { + return xerrors.Errorf("Value in field \"createdAt\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil { + return err + } + if _, err := cw.WriteString(string("createdAt")); err != nil { + return err + } + + if len(t.CreatedAt) > 1000000 { + return xerrors.Errorf("Value in field t.CreatedAt was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.CreatedAt))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.CreatedAt)); err != nil { + return err + } + + // t.References ([]string) (slice) + if t.References != nil { + + if len("references") > 1000000 { + return xerrors.Errorf("Value in field \"references\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("references"))); err != nil { + return err + } + if _, err := cw.WriteString(string("references")); err != nil { + return err + } + + if len(t.References) > 8192 { + return xerrors.Errorf("Slice value in field t.References was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.References))); err != nil { + return err + } + for _, v := range t.References { + if len(v) > 1000000 { + return xerrors.Errorf("Value in field v was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil { + return err + } + if _, err := cw.WriteString(string(v)); err != nil { + return err + } + + } + } + + // t.DependentOn (string) (string) + if t.DependentOn != nil { + + if len("dependentOn") > 1000000 { + return xerrors.Errorf("Value in field \"dependentOn\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("dependentOn"))); err != nil { + return err + } + if _, err := cw.WriteString(string("dependentOn")); err != nil { + return err + } + + if t.DependentOn == nil { + if _, err := cw.Write(cbg.CborNull); err != nil { + return err + } + } else { + if len(*t.DependentOn) > 1000000 { + return xerrors.Errorf("Value in field t.DependentOn was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.DependentOn))); err != nil { + return err + } + if _, err := cw.WriteString(string(*t.DependentOn)); err != nil { + return err + } + } + } + return nil +} + +func (t *RepoPull) UnmarshalCBOR(r io.Reader) (err error) { + *t = RepoPull{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("RepoPull: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 11) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Body (string) (string) + case "body": + + { + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.Body = (*string)(&sval) + } + } + // t.LexiconTypeID (string) (string) + case "$type": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.LexiconTypeID = string(sval) + } + // t.Title (string) (string) + case "title": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.Title = string(sval) + } + // t.Rounds ([]*tangledlex.RepoPull_Round) (slice) + case "rounds": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 8192 { + return fmt.Errorf("t.Rounds: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.Rounds = make([]*RepoPull_Round, extra) + } + + for i := 0; i < int(extra); i++ { + { + var maj byte + var extra uint64 + var err error + _ = maj + _ = extra + _ = err + + { + + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + t.Rounds[i] = new(RepoPull_Round) + if err := t.Rounds[i].UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Rounds[i] pointer: %w", err) + } + } + + } + + } + } + // t.Source (tangledlex.RepoPull_Source) (struct) + case "source": + + { + + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + t.Source = new(RepoPull_Source) + if err := t.Source.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Source pointer: %w", err) + } + } + + } + // t.Target (tangledlex.RepoPull_Target) (struct) + case "target": + + { + + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + t.Target = new(RepoPull_Target) + if err := t.Target.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Target pointer: %w", err) + } + } + + } + // t.Mentions ([]string) (slice) + case "mentions": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 8192 { + return fmt.Errorf("t.Mentions: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.Mentions = make([]string, extra) + } + + for i := 0; i < int(extra); i++ { + { + var maj byte + var extra uint64 + var err error + _ = maj + _ = extra + _ = err + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.Mentions[i] = string(sval) + } + + } + } + // t.CreatedAt (string) (string) + case "createdAt": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.CreatedAt = string(sval) + } + // t.References ([]string) (slice) + case "references": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 8192 { + return fmt.Errorf("t.References: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.References = make([]string, extra) + } + + for i := 0; i < int(extra); i++ { + { + var maj byte + var extra uint64 + var err error + _ = maj + _ = extra + _ = err + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.References[i] = string(sval) + } + + } + } + // t.DependentOn (string) (string) + case "dependentOn": + + { + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.DependentOn = (*string)(&sval) + } + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *RepoPullComment) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + fieldCount := 6 + + if t.Mentions == nil { + fieldCount-- + } + + if t.References == nil { + fieldCount-- + } + + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { + return err + } + + // t.Body (string) (string) + if len("body") > 1000000 { + return xerrors.Errorf("Value in field \"body\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("body"))); err != nil { + return err + } + if _, err := cw.WriteString(string("body")); err != nil { + return err + } + + if len(t.Body) > 1000000 { + return xerrors.Errorf("Value in field t.Body was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Body))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.Body)); err != nil { + return err + } + + // t.Pull (string) (string) + if len("pull") > 1000000 { + return xerrors.Errorf("Value in field \"pull\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("pull"))); err != nil { + return err + } + if _, err := cw.WriteString(string("pull")); err != nil { + return err + } + + if len(t.Pull) > 1000000 { + return xerrors.Errorf("Value in field t.Pull was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Pull))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.Pull)); err != nil { + return err + } + + // t.LexiconTypeID (string) (string) + if len("$type") > 1000000 { + return xerrors.Errorf("Value in field \"$type\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil { + return err + } + if _, err := cw.WriteString(string("$type")); err != nil { + return err + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.repo.pull.comment"))); err != nil { + return err + } + if _, err := cw.WriteString(string("sh.tangled.repo.pull.comment")); err != nil { + return err + } + + // t.Mentions ([]string) (slice) + if t.Mentions != nil { + + if len("mentions") > 1000000 { + return xerrors.Errorf("Value in field \"mentions\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("mentions"))); err != nil { + return err + } + if _, err := cw.WriteString(string("mentions")); err != nil { + return err + } + + if len(t.Mentions) > 8192 { + return xerrors.Errorf("Slice value in field t.Mentions was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Mentions))); err != nil { + return err + } + for _, v := range t.Mentions { + if len(v) > 1000000 { + return xerrors.Errorf("Value in field v was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil { + return err + } + if _, err := cw.WriteString(string(v)); err != nil { + return err + } + + } + } + + // t.CreatedAt (string) (string) + if len("createdAt") > 1000000 { + return xerrors.Errorf("Value in field \"createdAt\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil { + return err + } + if _, err := cw.WriteString(string("createdAt")); err != nil { + return err + } + + if len(t.CreatedAt) > 1000000 { + return xerrors.Errorf("Value in field t.CreatedAt was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.CreatedAt))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.CreatedAt)); err != nil { + return err + } + + // t.References ([]string) (slice) + if t.References != nil { + + if len("references") > 1000000 { + return xerrors.Errorf("Value in field \"references\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("references"))); err != nil { + return err + } + if _, err := cw.WriteString(string("references")); err != nil { + return err + } + + if len(t.References) > 8192 { + return xerrors.Errorf("Slice value in field t.References was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.References))); err != nil { + return err + } + for _, v := range t.References { + if len(v) > 1000000 { + return xerrors.Errorf("Value in field v was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil { + return err + } + if _, err := cw.WriteString(string(v)); err != nil { + return err + } + + } + } + return nil +} + +func (t *RepoPullComment) UnmarshalCBOR(r io.Reader) (err error) { + *t = RepoPullComment{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("RepoPullComment: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 10) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Body (string) (string) + case "body": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.Body = string(sval) + } + // t.Pull (string) (string) + case "pull": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.Pull = string(sval) + } + // t.LexiconTypeID (string) (string) + case "$type": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.LexiconTypeID = string(sval) + } + // t.Mentions ([]string) (slice) + case "mentions": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 8192 { + return fmt.Errorf("t.Mentions: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.Mentions = make([]string, extra) + } + + for i := 0; i < int(extra); i++ { + { + var maj byte + var extra uint64 + var err error + _ = maj + _ = extra + _ = err + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.Mentions[i] = string(sval) + } + + } + } + // t.CreatedAt (string) (string) + case "createdAt": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.CreatedAt = string(sval) + } + // t.References ([]string) (slice) + case "references": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 8192 { + return fmt.Errorf("t.References: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.References = make([]string, extra) + } + + for i := 0; i < int(extra); i++ { + { + var maj byte + var extra uint64 + var err error + _ = maj + _ = extra + _ = err + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.References[i] = string(sval) + } + + } + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *RepoPull_Round) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{162}); err != nil { + return err + } + + // t.CreatedAt (string) (string) + if len("createdAt") > 1000000 { + return xerrors.Errorf("Value in field \"createdAt\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil { + return err + } + if _, err := cw.WriteString(string("createdAt")); err != nil { + return err + } + + if len(t.CreatedAt) > 1000000 { + return xerrors.Errorf("Value in field t.CreatedAt was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.CreatedAt))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.CreatedAt)); err != nil { + return err + } + + // t.PatchBlob (util.LexBlob) (struct) + if len("patchBlob") > 1000000 { + return xerrors.Errorf("Value in field \"patchBlob\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("patchBlob"))); err != nil { + return err + } + if _, err := cw.WriteString(string("patchBlob")); err != nil { + return err + } + + if err := t.PatchBlob.MarshalCBOR(cw); err != nil { + return err + } + return nil +} + +func (t *RepoPull_Round) UnmarshalCBOR(r io.Reader) (err error) { + *t = RepoPull_Round{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("RepoPull_Round: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 9) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.CreatedAt (string) (string) + case "createdAt": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.CreatedAt = string(sval) + } + // t.PatchBlob (util.LexBlob) (struct) + case "patchBlob": + + { + + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + t.PatchBlob = new(util.LexBlob) + if err := t.PatchBlob.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.PatchBlob pointer: %w", err) + } + } + + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *RepoPull_Source) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + fieldCount := 2 + + if t.Repo == nil { + fieldCount-- + } + + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { + return err + } + + // t.Repo (string) (string) + if t.Repo != nil { + + if len("repo") > 1000000 { + return xerrors.Errorf("Value in field \"repo\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("repo"))); err != nil { + return err + } + if _, err := cw.WriteString(string("repo")); err != nil { + return err + } + + if t.Repo == nil { + if _, err := cw.Write(cbg.CborNull); err != nil { + return err + } + } else { + if len(*t.Repo) > 1000000 { + return xerrors.Errorf("Value in field t.Repo was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Repo))); err != nil { + return err + } + if _, err := cw.WriteString(string(*t.Repo)); err != nil { + return err + } + } + } + + // t.Branch (string) (string) + if len("branch") > 1000000 { + return xerrors.Errorf("Value in field \"branch\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("branch"))); err != nil { + return err + } + if _, err := cw.WriteString(string("branch")); err != nil { + return err + } + + if len(t.Branch) > 1000000 { + return xerrors.Errorf("Value in field t.Branch was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Branch))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.Branch)); err != nil { + return err + } + return nil +} + +func (t *RepoPull_Source) UnmarshalCBOR(r io.Reader) (err error) { + *t = RepoPull_Source{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("RepoPull_Source: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 6) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Repo (string) (string) + case "repo": + + { + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.Repo = (*string)(&sval) + } + } + // t.Branch (string) (string) + case "branch": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.Branch = string(sval) + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *RepoPullStatus) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{164}); err != nil { + return err + } + + // t.Pull (string) (string) + if len("pull") > 1000000 { + return xerrors.Errorf("Value in field \"pull\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("pull"))); err != nil { + return err + } + if _, err := cw.WriteString(string("pull")); err != nil { + return err + } + + if len(t.Pull) > 1000000 { + return xerrors.Errorf("Value in field t.Pull was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Pull))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.Pull)); err != nil { + return err + } + + // t.LexiconTypeID (string) (string) + if len("$type") > 1000000 { + return xerrors.Errorf("Value in field \"$type\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil { + return err + } + if _, err := cw.WriteString(string("$type")); err != nil { + return err + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.repo.pull.status"))); err != nil { + return err + } + if _, err := cw.WriteString(string("sh.tangled.repo.pull.status")); err != nil { + return err + } + + // t.Status (string) (string) + if len("status") > 1000000 { + return xerrors.Errorf("Value in field \"status\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("status"))); err != nil { + return err + } + if _, err := cw.WriteString(string("status")); err != nil { + return err + } + + if len(t.Status) > 1000000 { + return xerrors.Errorf("Value in field t.Status was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Status))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.Status)); err != nil { + return err + } + + // t.CreatedAt (string) (string) + if len("createdAt") > 1000000 { + return xerrors.Errorf("Value in field \"createdAt\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil { + return err + } + if _, err := cw.WriteString(string("createdAt")); err != nil { + return err + } + + if len(t.CreatedAt) > 1000000 { + return xerrors.Errorf("Value in field t.CreatedAt was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.CreatedAt))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.CreatedAt)); err != nil { + return err + } + return nil +} + +func (t *RepoPullStatus) UnmarshalCBOR(r io.Reader) (err error) { + *t = RepoPullStatus{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("RepoPullStatus: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 9) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Pull (string) (string) + case "pull": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.Pull = string(sval) + } + // t.LexiconTypeID (string) (string) + case "$type": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.LexiconTypeID = string(sval) + } + // t.Status (string) (string) + case "status": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.Status = string(sval) + } + // t.CreatedAt (string) (string) + case "createdAt": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.CreatedAt = string(sval) + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *RepoPull_Target) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{162}); err != nil { + return err + } + + // t.Repo (string) (string) + if len("repo") > 1000000 { + return xerrors.Errorf("Value in field \"repo\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("repo"))); err != nil { + return err + } + if _, err := cw.WriteString(string("repo")); err != nil { + return err + } + + if len(t.Repo) > 1000000 { + return xerrors.Errorf("Value in field t.Repo was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Repo))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.Repo)); err != nil { + return err + } + + // t.Branch (string) (string) + if len("branch") > 1000000 { + return xerrors.Errorf("Value in field \"branch\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("branch"))); err != nil { + return err + } + if _, err := cw.WriteString(string("branch")); err != nil { + return err + } + + if len(t.Branch) > 1000000 { + return xerrors.Errorf("Value in field t.Branch was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Branch))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.Branch)); err != nil { + return err + } + return nil +} + +func (t *RepoPull_Target) UnmarshalCBOR(r io.Reader) (err error) { + *t = RepoPull_Target{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("RepoPull_Target: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 6) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Repo (string) (string) + case "repo": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.Repo = string(sval) + } + // t.Branch (string) (string) + case "branch": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.Branch = string(sval) + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *String) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{165}); err != nil { + return err + } + + // t.LexiconTypeID (string) (string) + if len("$type") > 1000000 { + return xerrors.Errorf("Value in field \"$type\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil { + return err + } + if _, err := cw.WriteString(string("$type")); err != nil { + return err + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.string"))); err != nil { + return err + } + if _, err := cw.WriteString(string("sh.tangled.string")); err != nil { + return err + } + + // t.Contents (string) (string) + if len("contents") > 1000000 { + return xerrors.Errorf("Value in field \"contents\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("contents"))); err != nil { + return err + } + if _, err := cw.WriteString(string("contents")); err != nil { + return err + } + + if len(t.Contents) > 1000000 { + return xerrors.Errorf("Value in field t.Contents was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Contents))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.Contents)); err != nil { + return err + } + + // t.Filename (string) (string) + if len("filename") > 1000000 { + return xerrors.Errorf("Value in field \"filename\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("filename"))); err != nil { + return err + } + if _, err := cw.WriteString(string("filename")); err != nil { + return err + } + + if len(t.Filename) > 1000000 { + return xerrors.Errorf("Value in field t.Filename was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Filename))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.Filename)); err != nil { + return err + } + + // t.CreatedAt (string) (string) + if len("createdAt") > 1000000 { + return xerrors.Errorf("Value in field \"createdAt\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil { + return err + } + if _, err := cw.WriteString(string("createdAt")); err != nil { + return err + } + + if len(t.CreatedAt) > 1000000 { + return xerrors.Errorf("Value in field t.CreatedAt was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.CreatedAt))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.CreatedAt)); err != nil { + return err + } + + // t.Description (string) (string) + if len("description") > 1000000 { + return xerrors.Errorf("Value in field \"description\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("description"))); err != nil { + return err + } + if _, err := cw.WriteString(string("description")); err != nil { + return err + } + + if len(t.Description) > 1000000 { + return xerrors.Errorf("Value in field t.Description was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Description))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.Description)); err != nil { + return err + } + return nil +} + +func (t *String) UnmarshalCBOR(r io.Reader) (err error) { + *t = String{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("String: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 11) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.LexiconTypeID (string) (string) + case "$type": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.LexiconTypeID = string(sval) + } + // t.Contents (string) (string) + case "contents": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.Contents = string(sval) + } + // t.Filename (string) (string) + case "filename": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.Filename = string(sval) + } + // t.CreatedAt (string) (string) + case "createdAt": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.CreatedAt = string(sval) + } + // t.Description (string) (string) + case "description": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.Description = string(sval) + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} diff --git a/internal/tangledlex/cborgen/main.go b/internal/tangledlex/cborgen/main.go new file mode 100644 index 0000000..9ddf812 --- /dev/null +++ b/internal/tangledlex/cborgen/main.go @@ -0,0 +1,28 @@ +package main + +import ( + lex "github.com/alyraffauf/tg/internal/tangledlex" + cbg "github.com/whyrusleeping/cbor-gen" +) + +func main() { + generator := cbg.Gen{MaxStringLength: 1_000_000} + if err := generator.WriteMapEncodersToFile( + "cbor_gen.go", + "tangledlex", + lex.PublicKey{}, + lex.Repo{}, + lex.RepoIssue{}, + lex.RepoIssueComment{}, + lex.RepoIssueState{}, + lex.RepoPull{}, + lex.RepoPullComment{}, + lex.RepoPull_Round{}, + lex.RepoPull_Source{}, + lex.RepoPullStatus{}, + lex.RepoPull_Target{}, + lex.String{}, + ); err != nil { + panic(err) + } +} diff --git a/internal/tangledlex/generate-lexicons.sh b/internal/tangledlex/generate-lexicons.sh new file mode 100755 index 0000000..6779e78 --- /dev/null +++ b/internal/tangledlex/generate-lexicons.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env sh +set -eu + +go run github.com/bluesky-social/indigo/cmd/lexgen --build-file lexgen.json schemas + +# cbor-gen needs to compile the generated package before it can emit its CBOR +# methods. Temporarily suppress the registry calls, which require those methods. +sed -i.bak '/lexutil.RegisterType/s/^/\/\//' ./*.go +sed -i.bak '/func init() {/a\ + _ = lexutil.ErrUnrecognizedType +' ./*.go +go run ./cborgen + +# Restore the generated sources, including type registration, after CBOR methods exist. +go run github.com/bluesky-social/indigo/cmd/lexgen --build-file lexgen.json schemas +rm -f ./*.bak diff --git a/internal/tangledlex/generate.go b/internal/tangledlex/generate.go new file mode 100644 index 0000000..2fc5e64 --- /dev/null +++ b/internal/tangledlex/generate.go @@ -0,0 +1,3 @@ +package tangledlex + +//go:generate ./generate-lexicons.sh diff --git a/internal/tangledlex/issuecomment.go b/internal/tangledlex/issuecomment.go new file mode 100644 index 0000000..792ee66 --- /dev/null +++ b/internal/tangledlex/issuecomment.go @@ -0,0 +1,23 @@ +// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. + +// Lexicon schema: sh.tangled.repo.issue.comment + +package tangledlex + +import ( + lexutil "github.com/bluesky-social/indigo/lex/util" +) + +func init() { + lexutil.RegisterType("sh.tangled.repo.issue.comment", &RepoIssueComment{}) +} + +type RepoIssueComment struct { + LexiconTypeID string `json:"$type" cborgen:"$type,const=sh.tangled.repo.issue.comment"` + Body string `json:"body" cborgen:"body"` + CreatedAt string `json:"createdAt" cborgen:"createdAt"` + Issue string `json:"issue" cborgen:"issue"` + Mentions []string `json:"mentions,omitempty" cborgen:"mentions,omitempty"` + References []string `json:"references,omitempty" cborgen:"references,omitempty"` + ReplyTo *string `json:"replyTo,omitempty" cborgen:"replyTo,omitempty"` +} diff --git a/internal/tangledlex/issuestate.go b/internal/tangledlex/issuestate.go new file mode 100644 index 0000000..fe971c0 --- /dev/null +++ b/internal/tangledlex/issuestate.go @@ -0,0 +1,21 @@ +// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. + +// Lexicon schema: sh.tangled.repo.issue.state + +package tangledlex + +import ( + lexutil "github.com/bluesky-social/indigo/lex/util" +) + +func init() { + lexutil.RegisterType("sh.tangled.repo.issue.state", &RepoIssueState{}) +} + +type RepoIssueState struct { + LexiconTypeID string `json:"$type" cborgen:"$type,const=sh.tangled.repo.issue.state"` + CreatedAt string `json:"createdAt" cborgen:"createdAt"` + Issue string `json:"issue" cborgen:"issue"` + // state: state of the issue + State string `json:"state" cborgen:"state"` +} diff --git a/internal/tangledlex/lexgen.json b/internal/tangledlex/lexgen.json new file mode 100644 index 0000000..eec788b --- /dev/null +++ b/internal/tangledlex/lexgen.json @@ -0,0 +1,8 @@ +[ + { + "package": "tangledlex", + "prefix": "sh.tangled", + "outdir": ".", + "import": "github.com/alyraffauf/tg/internal/tangledlex" + } +] diff --git a/internal/tangledlex/lock.json b/internal/tangledlex/lock.json new file mode 100644 index 0000000..10e5c69 --- /dev/null +++ b/internal/tangledlex/lock.json @@ -0,0 +1,5 @@ +{ + "source": "https://tangled.org/tangled.org/core.git", + "commit": "566697c68dfd519f745fe12862707c898695166b", + "schemaPaths": "schemas.txt" +} diff --git a/internal/tangledlex/pullcomment.go b/internal/tangledlex/pullcomment.go new file mode 100644 index 0000000..7451592 --- /dev/null +++ b/internal/tangledlex/pullcomment.go @@ -0,0 +1,22 @@ +// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. + +// Lexicon schema: sh.tangled.repo.pull.comment + +package tangledlex + +import ( + lexutil "github.com/bluesky-social/indigo/lex/util" +) + +func init() { + lexutil.RegisterType("sh.tangled.repo.pull.comment", &RepoPullComment{}) +} + +type RepoPullComment struct { + LexiconTypeID string `json:"$type" cborgen:"$type,const=sh.tangled.repo.pull.comment"` + Body string `json:"body" cborgen:"body"` + CreatedAt string `json:"createdAt" cborgen:"createdAt"` + Mentions []string `json:"mentions,omitempty" cborgen:"mentions,omitempty"` + Pull string `json:"pull" cborgen:"pull"` + References []string `json:"references,omitempty" cborgen:"references,omitempty"` +} diff --git a/internal/tangledlex/pullstatus.go b/internal/tangledlex/pullstatus.go new file mode 100644 index 0000000..f4cdb49 --- /dev/null +++ b/internal/tangledlex/pullstatus.go @@ -0,0 +1,21 @@ +// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. + +// Lexicon schema: sh.tangled.repo.pull.status + +package tangledlex + +import ( + lexutil "github.com/bluesky-social/indigo/lex/util" +) + +func init() { + lexutil.RegisterType("sh.tangled.repo.pull.status", &RepoPullStatus{}) +} + +type RepoPullStatus struct { + LexiconTypeID string `json:"$type" cborgen:"$type,const=sh.tangled.repo.pull.status"` + CreatedAt string `json:"createdAt" cborgen:"createdAt"` + Pull string `json:"pull" cborgen:"pull"` + // status: status of the pull request + Status string `json:"status" cborgen:"status"` +} diff --git a/internal/tangledlex/repogetRepo.go b/internal/tangledlex/repogetRepo.go new file mode 100644 index 0000000..c8620f6 --- /dev/null +++ b/internal/tangledlex/repogetRepo.go @@ -0,0 +1,34 @@ +// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. + +// Lexicon schema: sh.tangled.repo.getRepo + +package tangledlex + +import ( + "context" + + lexutil "github.com/bluesky-social/indigo/lex/util" +) + +// RepoGetRepo_Output is the output of a sh.tangled.repo.getRepo call. +type RepoGetRepo_Output struct { + Cid *string `json:"cid,omitempty" cborgen:"cid,omitempty"` + Uri string `json:"uri" cborgen:"uri"` + // value: Embedded sh.tangled.repo record. + Value *lexutil.LexiconTypeDecoder `json:"value" cborgen:"value"` +} + +// RepoGetRepo calls the XRPC method "sh.tangled.repo.getRepo". +// +// repo: AT-URI of the sh.tangled.repo record to fetch. +func RepoGetRepo(ctx context.Context, c lexutil.LexClient, repo string) (*RepoGetRepo_Output, error) { + var out RepoGetRepo_Output + + params := map[string]interface{}{} + params["repo"] = repo + if err := c.LexDo(ctx, lexutil.Query, "", "sh.tangled.repo.getRepo", params, nil, &out); err != nil { + return nil, err + } + + return &out, nil +} diff --git a/internal/tangledlex/repoissue.go b/internal/tangledlex/repoissue.go new file mode 100644 index 0000000..90844aa --- /dev/null +++ b/internal/tangledlex/repoissue.go @@ -0,0 +1,23 @@ +// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. + +// Lexicon schema: sh.tangled.repo.issue + +package tangledlex + +import ( + lexutil "github.com/bluesky-social/indigo/lex/util" +) + +func init() { + lexutil.RegisterType("sh.tangled.repo.issue", &RepoIssue{}) +} + +type RepoIssue struct { + LexiconTypeID string `json:"$type" cborgen:"$type,const=sh.tangled.repo.issue"` + Body *string `json:"body,omitempty" cborgen:"body,omitempty"` + CreatedAt string `json:"createdAt" cborgen:"createdAt"` + Mentions []string `json:"mentions,omitempty" cborgen:"mentions,omitempty"` + References []string `json:"references,omitempty" cborgen:"references,omitempty"` + Repo string `json:"repo" cborgen:"repo"` + Title string `json:"title" cborgen:"title"` +} diff --git a/internal/tangledlex/repolistIssues.go b/internal/tangledlex/repolistIssues.go new file mode 100644 index 0000000..95bfc40 --- /dev/null +++ b/internal/tangledlex/repolistIssues.go @@ -0,0 +1,65 @@ +// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. + +// Lexicon schema: sh.tangled.repo.listIssues + +package tangledlex + +import ( + "context" + + lexutil "github.com/bluesky-social/indigo/lex/util" +) + +// RepoListIssues_IssueListItem is a "issueListItem" in the sh.tangled.repo.listIssues schema. +type RepoListIssues_IssueListItem struct { + Cid *string `json:"cid,omitempty" cborgen:"cid,omitempty"` + // commentCount: Count of sh.tangled.repo.issue.comment records targeting this issue. + CommentCount int64 `json:"commentCount" cborgen:"commentCount"` + // state: Latest derived state. + State string `json:"state" cborgen:"state"` + // stateUpdatedAt: TID-derived timestamp of the latest state record. + StateUpdatedAt *string `json:"stateUpdatedAt,omitempty" cborgen:"stateUpdatedAt,omitempty"` + Uri string `json:"uri" cborgen:"uri"` + // value: Embedded sh.tangled.repo.issue record + Value *lexutil.LexiconTypeDecoder `json:"value" cborgen:"value"` +} + +// RepoListIssues_Output is the output of a sh.tangled.repo.listIssues call. +type RepoListIssues_Output struct { + Cursor *string `json:"cursor,omitempty" cborgen:"cursor,omitempty"` + Items []*RepoListIssues_IssueListItem `json:"items" cborgen:"items"` +} + +// RepoListIssues calls the XRPC method "sh.tangled.repo.listIssues". +// +// author: Restrict to issues authored by this user DID. +// cursor: Pagination cursor +// order: Sort direction by createdAt. +// state: Restrict to issues whose latest derived state matches. +// subject: Repo DID to list issues for +func RepoListIssues(ctx context.Context, c lexutil.LexClient, author string, cursor string, limit int64, order string, state string, subject string) (*RepoListIssues_Output, error) { + var out RepoListIssues_Output + + params := map[string]interface{}{} + if author != "" { + params["author"] = author + } + if cursor != "" { + params["cursor"] = cursor + } + if limit != 0 { + params["limit"] = limit + } + if order != "" { + params["order"] = order + } + if state != "" { + params["state"] = state + } + params["subject"] = subject + if err := c.LexDo(ctx, lexutil.Query, "", "sh.tangled.repo.listIssues", params, nil, &out); err != nil { + return nil, err + } + + return &out, nil +} diff --git a/internal/tangledlex/repolistPulls.go b/internal/tangledlex/repolistPulls.go new file mode 100644 index 0000000..06afb3f --- /dev/null +++ b/internal/tangledlex/repolistPulls.go @@ -0,0 +1,65 @@ +// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. + +// Lexicon schema: sh.tangled.repo.listPulls + +package tangledlex + +import ( + "context" + + lexutil "github.com/bluesky-social/indigo/lex/util" +) + +// RepoListPulls_Output is the output of a sh.tangled.repo.listPulls call. +type RepoListPulls_Output struct { + Cursor *string `json:"cursor,omitempty" cborgen:"cursor,omitempty"` + Items []*RepoListPulls_PullListItem `json:"items" cborgen:"items"` +} + +// RepoListPulls_PullListItem is a "pullListItem" in the sh.tangled.repo.listPulls schema. +type RepoListPulls_PullListItem struct { + Cid *string `json:"cid,omitempty" cborgen:"cid,omitempty"` + // commentCount: Count of sh.tangled.repo.pull.comment records targeting this pull. + CommentCount int64 `json:"commentCount" cborgen:"commentCount"` + // state: Latest derived state. + State string `json:"state" cborgen:"state"` + // stateUpdatedAt: TID-derived timestamp of the latest pull status record. + StateUpdatedAt *string `json:"stateUpdatedAt,omitempty" cborgen:"stateUpdatedAt,omitempty"` + Uri string `json:"uri" cborgen:"uri"` + // value: Embedded sh.tangled.repo.pull record + Value *lexutil.LexiconTypeDecoder `json:"value" cborgen:"value"` +} + +// RepoListPulls calls the XRPC method "sh.tangled.repo.listPulls". +// +// author: Restrict to pulls authored by this user DID. +// cursor: Pagination cursor +// order: Sort direction by createdAt. +// status: Restrict to pulls whose latest derived status matches. +// subject: Repo DID to list pulls for +func RepoListPulls(ctx context.Context, c lexutil.LexClient, author string, cursor string, limit int64, order string, status string, subject string) (*RepoListPulls_Output, error) { + var out RepoListPulls_Output + + params := map[string]interface{}{} + if author != "" { + params["author"] = author + } + if cursor != "" { + params["cursor"] = cursor + } + if limit != 0 { + params["limit"] = limit + } + if order != "" { + params["order"] = order + } + if status != "" { + params["status"] = status + } + params["subject"] = subject + if err := c.LexDo(ctx, lexutil.Query, "", "sh.tangled.repo.listPulls", params, nil, &out); err != nil { + return nil, err + } + + return &out, nil +} diff --git a/internal/tangledlex/repolistRepos.go b/internal/tangledlex/repolistRepos.go new file mode 100644 index 0000000..a775374 --- /dev/null +++ b/internal/tangledlex/repolistRepos.go @@ -0,0 +1,51 @@ +// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. + +// Lexicon schema: sh.tangled.repo.listRepos + +package tangledlex + +import ( + "context" + + lexutil "github.com/bluesky-social/indigo/lex/util" +) + +// RepoListRepos_ListItem is a "listItem" in the sh.tangled.repo.listRepos schema. +type RepoListRepos_ListItem struct { + Cid *string `json:"cid,omitempty" cborgen:"cid,omitempty"` + Uri string `json:"uri" cborgen:"uri"` + // value: Embedded sh.tangled.repo record + Value *lexutil.LexiconTypeDecoder `json:"value" cborgen:"value"` +} + +// RepoListRepos_Output is the output of a sh.tangled.repo.listRepos call. +type RepoListRepos_Output struct { + Cursor *string `json:"cursor,omitempty" cborgen:"cursor,omitempty"` + Items []*RepoListRepos_ListItem `json:"items" cborgen:"items"` +} + +// RepoListRepos calls the XRPC method "sh.tangled.repo.listRepos". +// +// cursor: Pagination cursor +// order: Sort direction by createdAt. +// subject: Owner DID whose repo records to list. +func RepoListRepos(ctx context.Context, c lexutil.LexClient, cursor string, limit int64, order string, subject string) (*RepoListRepos_Output, error) { + var out RepoListRepos_Output + + params := map[string]interface{}{} + if cursor != "" { + params["cursor"] = cursor + } + if limit != 0 { + params["limit"] = limit + } + if order != "" { + params["order"] = order + } + params["subject"] = subject + if err := c.LexDo(ctx, lexutil.Query, "", "sh.tangled.repo.listRepos", params, nil, &out); err != nil { + return nil, err + } + + return &out, nil +} diff --git a/internal/tangledlex/repopull.go b/internal/tangledlex/repopull.go new file mode 100644 index 0000000..13e8def --- /dev/null +++ b/internal/tangledlex/repopull.go @@ -0,0 +1,46 @@ +// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. + +// Lexicon schema: sh.tangled.repo.pull + +package tangledlex + +import ( + lexutil "github.com/bluesky-social/indigo/lex/util" +) + +func init() { + lexutil.RegisterType("sh.tangled.repo.pull", &RepoPull{}) +} + +type RepoPull struct { + LexiconTypeID string `json:"$type" cborgen:"$type,const=sh.tangled.repo.pull"` + Body *string `json:"body,omitempty" cborgen:"body,omitempty"` + CreatedAt string `json:"createdAt" cborgen:"createdAt"` + DependentOn *string `json:"dependentOn,omitempty" cborgen:"dependentOn,omitempty"` + Mentions []string `json:"mentions,omitempty" cborgen:"mentions,omitempty"` + References []string `json:"references,omitempty" cborgen:"references,omitempty"` + Rounds []*RepoPull_Round `json:"rounds" cborgen:"rounds"` + Source *RepoPull_Source `json:"source,omitempty" cborgen:"source,omitempty"` + Target *RepoPull_Target `json:"target" cborgen:"target"` + Title string `json:"title" cborgen:"title"` +} + +// RepoPull_Round is a "round" in the sh.tangled.repo.pull schema. +// +// revisions of this pull request, newer rounds are appended to this array. appviews may reject records do not treat this field as append-only. the blob format is gzipped text-based git-format-patches. +type RepoPull_Round struct { + CreatedAt string `json:"createdAt" cborgen:"createdAt"` + PatchBlob *lexutil.LexBlob `json:"patchBlob" cborgen:"patchBlob"` +} + +// RepoPull_Source is a "source" in the sh.tangled.repo.pull schema. +type RepoPull_Source struct { + Branch string `json:"branch" cborgen:"branch"` + Repo *string `json:"repo,omitempty" cborgen:"repo,omitempty"` +} + +// RepoPull_Target is a "target" in the sh.tangled.repo.pull schema. +type RepoPull_Target struct { + Branch string `json:"branch" cborgen:"branch"` + Repo string `json:"repo" cborgen:"repo"` +} diff --git a/internal/tangledlex/schemas.txt b/internal/tangledlex/schemas.txt new file mode 100644 index 0000000..dc1bc12 --- /dev/null +++ b/internal/tangledlex/schemas.txt @@ -0,0 +1,13 @@ +issue/comment.json +issue/issue.json +issue/state.json +publicKey.json +pulls/comment.json +pulls/pull.json +pulls/state.json +repo/getRepo.json +repo/listIssues.json +repo/listPulls.json +repo/listRepos.json +repo/repo.json +string/string.json diff --git a/internal/tangledlex/schemas/issue/comment.json b/internal/tangledlex/schemas/issue/comment.json new file mode 100644 index 0000000..92cb0e5 --- /dev/null +++ b/internal/tangledlex/schemas/issue/comment.json @@ -0,0 +1,47 @@ +{ + "lexicon": 1, + "id": "sh.tangled.repo.issue.comment", + "needsCbor": true, + "needsType": true, + "defs": { + "main": { + "type": "record", + "key": "tid", + "record": { + "type": "object", + "required": ["issue", "body", "createdAt"], + "properties": { + "issue": { + "type": "string", + "format": "at-uri" + }, + "body": { + "type": "string" + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "replyTo": { + "type": "string", + "format": "at-uri" + }, + "mentions": { + "type": "array", + "items": { + "type": "string", + "format": "did" + } + }, + "references": { + "type": "array", + "items": { + "type": "string", + "format": "at-uri" + } + } + } + } + } + } +} diff --git a/internal/tangledlex/schemas/issue/issue.json b/internal/tangledlex/schemas/issue/issue.json new file mode 100644 index 0000000..cd484fe --- /dev/null +++ b/internal/tangledlex/schemas/issue/issue.json @@ -0,0 +1,46 @@ +{ + "lexicon": 1, + "id": "sh.tangled.repo.issue", + "needsCbor": true, + "needsType": true, + "defs": { + "main": { + "type": "record", + "key": "tid", + "record": { + "type": "object", + "required": ["repo", "title", "createdAt"], + "properties": { + "repo": { + "type": "string", + "format": "did" + }, + "title": { + "type": "string" + }, + "body": { + "type": "string" + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "mentions": { + "type": "array", + "items": { + "type": "string", + "format": "did" + } + }, + "references": { + "type": "array", + "items": { + "type": "string", + "format": "at-uri" + } + } + } + } + } + } +} diff --git a/internal/tangledlex/schemas/issue/state.json b/internal/tangledlex/schemas/issue/state.json new file mode 100644 index 0000000..5a36223 --- /dev/null +++ b/internal/tangledlex/schemas/issue/state.json @@ -0,0 +1,35 @@ +{ + "lexicon": 1, + "id": "sh.tangled.repo.issue.state", + "needsCbor": true, + "needsType": true, + "defs": { + "main": { + "type": "record", + "key": "tid", + "record": { + "type": "object", + "required": ["issue", "state", "createdAt"], + "properties": { + "issue": { + "type": "string", + "format": "at-uri" + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "state": { + "type": "string", + "description": "state of the issue", + "knownValues": [ + "sh.tangled.repo.issue.state.open", + "sh.tangled.repo.issue.state.closed" + ], + "default": "sh.tangled.repo.issue.state.open" + } + } + } + } + } +} diff --git a/internal/tangledlex/schemas/publicKey.json b/internal/tangledlex/schemas/publicKey.json new file mode 100644 index 0000000..6698868 --- /dev/null +++ b/internal/tangledlex/schemas/publicKey.json @@ -0,0 +1,32 @@ +{ + "lexicon": 1, + "id": "sh.tangled.publicKey", + "needsCbor": true, + "needsType": true, + "defs": { + "main": { + "type": "record", + "key": "tid", + "record": { + "type": "object", + "required": ["key", "name", "createdAt"], + "properties": { + "key": { + "type": "string", + "maxLength": 4096, + "description": "public key contents" + }, + "name": { + "type": "string", + "description": "human-readable name for this key" + }, + "createdAt": { + "type": "string", + "format": "datetime", + "description": "key upload timestamp" + } + } + } + } + } +} diff --git a/internal/tangledlex/schemas/pulls/comment.json b/internal/tangledlex/schemas/pulls/comment.json new file mode 100644 index 0000000..b7ffecd --- /dev/null +++ b/internal/tangledlex/schemas/pulls/comment.json @@ -0,0 +1,43 @@ +{ + "lexicon": 1, + "id": "sh.tangled.repo.pull.comment", + "needsCbor": true, + "needsType": true, + "defs": { + "main": { + "type": "record", + "key": "tid", + "record": { + "type": "object", + "required": ["pull", "body", "createdAt"], + "properties": { + "pull": { + "type": "string", + "format": "at-uri" + }, + "body": { + "type": "string" + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "mentions": { + "type": "array", + "items": { + "type": "string", + "format": "did" + } + }, + "references": { + "type": "array", + "items": { + "type": "string", + "format": "at-uri" + } + } + } + } + } + } +} diff --git a/internal/tangledlex/schemas/pulls/pull.json b/internal/tangledlex/schemas/pulls/pull.json new file mode 100644 index 0000000..728e86a --- /dev/null +++ b/internal/tangledlex/schemas/pulls/pull.json @@ -0,0 +1,102 @@ +{ + "lexicon": 1, + "id": "sh.tangled.repo.pull", + "needsCbor": true, + "needsType": true, + "defs": { + "main": { + "type": "record", + "key": "tid", + "record": { + "type": "object", + "required": ["target", "title", "createdAt", "rounds"], + "properties": { + "title": { + "type": "string" + }, + "body": { + "type": "string" + }, + "rounds": { + "type": "array", + "items": { + "type": "ref", + "ref": "#round" + } + }, + "source": { + "type": "ref", + "ref": "#source" + }, + "target": { + "type": "ref", + "ref": "#target" + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "mentions": { + "type": "array", + "items": { + "type": "string", + "format": "did" + } + }, + "references": { + "type": "array", + "items": { + "type": "string", + "format": "at-uri" + } + }, + "dependentOn": { + "type": "string", + "format": "at-uri" + } + } + } + }, + "target": { + "type": "object", + "required": ["repo", "branch"], + "properties": { + "repo": { + "type": "string", + "format": "did" + }, + "branch": { + "type": "string" + } + } + }, + "source": { + "type": "object", + "required": ["branch"], + "properties": { + "branch": { + "type": "string" + }, + "repo": { + "type": "string", + "format": "did" + } + } + }, + "round": { + "type": "object", + "required": ["patchBlob", "createdAt"], + "description": "revisions of this pull request, newer rounds are appended to this array. appviews may reject records do not treat this field as append-only. the blob format is gzipped text-based git-format-patches.", + "properties": { + "createdAt": { + "type": "string", + "format": "datetime" + }, + "patchBlob": { + "type": "blob", + "accept": ["application/gzip"] + } + } + } + } +} diff --git a/internal/tangledlex/schemas/pulls/state.json b/internal/tangledlex/schemas/pulls/state.json new file mode 100644 index 0000000..94687c6 --- /dev/null +++ b/internal/tangledlex/schemas/pulls/state.json @@ -0,0 +1,36 @@ +{ + "lexicon": 1, + "id": "sh.tangled.repo.pull.status", + "needsCbor": true, + "needsType": true, + "defs": { + "main": { + "type": "record", + "key": "tid", + "record": { + "type": "object", + "required": ["pull", "status", "createdAt"], + "properties": { + "pull": { + "type": "string", + "format": "at-uri" + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "status": { + "type": "string", + "description": "status of the pull request", + "knownValues": [ + "sh.tangled.repo.pull.status.open", + "sh.tangled.repo.pull.status.closed", + "sh.tangled.repo.pull.status.merged" + ], + "default": "sh.tangled.repo.pull.status.open" + } + } + } + } + } +} diff --git a/internal/tangledlex/schemas/repo/getRepo.json b/internal/tangledlex/schemas/repo/getRepo.json new file mode 100644 index 0000000..bf715ab --- /dev/null +++ b/internal/tangledlex/schemas/repo/getRepo.json @@ -0,0 +1,41 @@ +{ + "lexicon": 1, + "id": "sh.tangled.repo.getRepo", + "defs": { + "main": { + "type": "query", + "parameters": { + "type": "params", + "required": ["repo"], + "properties": { + "repo": { + "type": "string", + "format": "at-uri", + "description": "AT-URI of the sh.tangled.repo record to fetch." + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["uri", "value"], + "properties": { + "uri": { + "type": "string", + "format": "at-uri" + }, + "cid": { + "type": "string", + "format": "cid" + }, + "value": { + "type": "unknown", + "description": "Embedded sh.tangled.repo record." + } + } + } + } + } + } +} diff --git a/internal/tangledlex/schemas/repo/listIssues.json b/internal/tangledlex/schemas/repo/listIssues.json new file mode 100644 index 0000000..c89bdd0 --- /dev/null +++ b/internal/tangledlex/schemas/repo/listIssues.json @@ -0,0 +1,98 @@ +{ + "lexicon": 1, + "id": "sh.tangled.repo.listIssues", + "defs": { + "main": { + "type": "query", + "parameters": { + "type": "params", + "required": ["subject"], + "properties": { + "subject": { + "type": "string", + "format": "did", + "description": "Repo DID to list issues for" + }, + "author": { + "type": "string", + "format": "did", + "description": "Restrict to issues authored by this user DID." + }, + "state": { + "type": "string", + "knownValues": ["open", "closed"], + "description": "Restrict to issues whose latest derived state matches." + }, + "cursor": { + "type": "string", + "description": "Pagination cursor" + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 1000, + "default": 50 + }, + "order": { + "type": "string", + "knownValues": ["asc", "desc"], + "default": "desc", + "description": "Sort direction by createdAt." + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["items"], + "properties": { + "items": { + "type": "array", + "items": { + "type": "ref", + "ref": "#issueListItem" + } + }, + "cursor": { + "type": "string" + } + } + } + } + }, + "issueListItem": { + "type": "object", + "required": ["uri", "value", "state", "commentCount"], + "properties": { + "uri": { + "type": "string", + "format": "at-uri" + }, + "cid": { + "type": "string", + "format": "cid" + }, + "value": { + "type": "unknown", + "description": "Embedded sh.tangled.repo.issue record" + }, + "state": { + "type": "string", + "knownValues": ["open", "closed"], + "description": "Latest derived state." + }, + "stateUpdatedAt": { + "type": "string", + "format": "datetime", + "description": "TID-derived timestamp of the latest state record." + }, + "commentCount": { + "type": "integer", + "minimum": 0, + "description": "Count of sh.tangled.repo.issue.comment records targeting this issue." + } + } + } + } +} diff --git a/internal/tangledlex/schemas/repo/listPulls.json b/internal/tangledlex/schemas/repo/listPulls.json new file mode 100644 index 0000000..626639e --- /dev/null +++ b/internal/tangledlex/schemas/repo/listPulls.json @@ -0,0 +1,98 @@ +{ + "lexicon": 1, + "id": "sh.tangled.repo.listPulls", + "defs": { + "main": { + "type": "query", + "parameters": { + "type": "params", + "required": ["subject"], + "properties": { + "subject": { + "type": "string", + "format": "did", + "description": "Repo DID to list pulls for" + }, + "author": { + "type": "string", + "format": "did", + "description": "Restrict to pulls authored by this user DID." + }, + "status": { + "type": "string", + "knownValues": ["open", "closed", "merged"], + "description": "Restrict to pulls whose latest derived status matches." + }, + "cursor": { + "type": "string", + "description": "Pagination cursor" + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 1000, + "default": 50 + }, + "order": { + "type": "string", + "knownValues": ["asc", "desc"], + "default": "desc", + "description": "Sort direction by createdAt." + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["items"], + "properties": { + "items": { + "type": "array", + "items": { + "type": "ref", + "ref": "#pullListItem" + } + }, + "cursor": { + "type": "string" + } + } + } + } + }, + "pullListItem": { + "type": "object", + "required": ["uri", "value", "state", "commentCount"], + "properties": { + "uri": { + "type": "string", + "format": "at-uri" + }, + "cid": { + "type": "string", + "format": "cid" + }, + "value": { + "type": "unknown", + "description": "Embedded sh.tangled.repo.pull record" + }, + "state": { + "type": "string", + "knownValues": ["open", "closed", "merged"], + "description": "Latest derived state." + }, + "stateUpdatedAt": { + "type": "string", + "format": "datetime", + "description": "TID-derived timestamp of the latest pull status record." + }, + "commentCount": { + "type": "integer", + "minimum": 0, + "description": "Count of sh.tangled.repo.pull.comment records targeting this pull." + } + } + } + } +} diff --git a/internal/tangledlex/schemas/repo/listRepos.json b/internal/tangledlex/schemas/repo/listRepos.json new file mode 100644 index 0000000..4897f67 --- /dev/null +++ b/internal/tangledlex/schemas/repo/listRepos.json @@ -0,0 +1,62 @@ +{ + "lexicon": 1, + "id": "sh.tangled.repo.listRepos", + "defs": { + "main": { + "type": "query", + "parameters": { + "type": "params", + "required": ["subject"], + "properties": { + "subject": { + "type": "string", + "format": "did", + "description": "Owner DID whose repo records to list." + }, + "cursor": { + "type": "string", + "description": "Pagination cursor" + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 1000, + "default": 50 + }, + "order": { + "type": "string", + "knownValues": ["asc", "desc"], + "default": "desc", + "description": "Sort direction by createdAt." + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["items"], + "properties": { + "items": { + "type": "array", + "items": { "type": "ref", "ref": "#listItem" } + }, + "cursor": { "type": "string" } + } + } + } + }, + "listItem": { + "type": "object", + "required": ["uri", "value"], + "properties": { + "uri": { "type": "string", "format": "at-uri" }, + "cid": { "type": "string", "format": "cid" }, + "value": { + "type": "unknown", + "description": "Embedded sh.tangled.repo record" + } + } + } + } +} diff --git a/internal/tangledlex/schemas/repo/repo.json b/internal/tangledlex/schemas/repo/repo.json new file mode 100644 index 0000000..c874e1a --- /dev/null +++ b/internal/tangledlex/schemas/repo/repo.json @@ -0,0 +1,72 @@ +{ + "lexicon": 1, + "id": "sh.tangled.repo", + "needsCbor": true, + "needsType": true, + "defs": { + "main": { + "type": "record", + "key": "any", + "record": { + "type": "object", + "required": ["knot", "createdAt"], + "properties": { + "name": { + "type": "string", + "description": "Cosmetic name of the repo." + }, + "knot": { + "type": "string", + "description": "knot where the repo was created" + }, + "spindle": { + "type": "string", + "description": "CI runner to send jobs to and receive results from" + }, + "description": { + "type": "string", + "minGraphemes": 1, + "maxGraphemes": 140 + }, + "website": { + "type": "string", + "format": "uri", + "description": "Any URI related to the repo" + }, + "topics": { + "type": "array", + "description": "Topics related to the repo", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 50 + }, + "maxLength": 50 + }, + "source": { + "type": "string", + "format": "uri", + "description": "source of the repo" + }, + "labels": { + "type": "array", + "description": "List of labels that this repo subscribes to", + "items": { + "type": "string", + "format": "at-uri" + } + }, + "repoDid": { + "type": "string", + "format": "did", + "description": "DID of the repo itself, if assigned" + }, + "createdAt": { + "type": "string", + "format": "datetime" + } + } + } + } + } +} diff --git a/internal/tangledlex/schemas/string/string.json b/internal/tangledlex/schemas/string/string.json new file mode 100644 index 0000000..437167e --- /dev/null +++ b/internal/tangledlex/schemas/string/string.json @@ -0,0 +1,35 @@ +{ + "lexicon": 1, + "id": "sh.tangled.string", + "needsCbor": true, + "needsType": true, + "defs": { + "main": { + "type": "record", + "key": "tid", + "record": { + "type": "object", + "required": ["filename", "description", "createdAt", "contents"], + "properties": { + "filename": { + "type": "string", + "maxGraphemes": 140, + "minGraphemes": 1 + }, + "description": { + "type": "string", + "maxGraphemes": 280 + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "contents": { + "type": "string", + "minGraphemes": 1 + } + } + } + } + } +} diff --git a/internal/tangledlex/tangledpublicKey.go b/internal/tangledlex/tangledpublicKey.go new file mode 100644 index 0000000..2e38f4c --- /dev/null +++ b/internal/tangledlex/tangledpublicKey.go @@ -0,0 +1,23 @@ +// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. + +// Lexicon schema: sh.tangled.publicKey + +package tangledlex + +import ( + lexutil "github.com/bluesky-social/indigo/lex/util" +) + +func init() { + lexutil.RegisterType("sh.tangled.publicKey", &PublicKey{}) +} + +type PublicKey struct { + LexiconTypeID string `json:"$type" cborgen:"$type,const=sh.tangled.publicKey"` + // createdAt: key upload timestamp + CreatedAt string `json:"createdAt" cborgen:"createdAt"` + // key: public key contents + Key string `json:"key" cborgen:"key"` + // name: human-readable name for this key + Name string `json:"name" cborgen:"name"` +} diff --git a/internal/tangledlex/tangledrepo.go b/internal/tangledlex/tangledrepo.go new file mode 100644 index 0000000..f67e980 --- /dev/null +++ b/internal/tangledlex/tangledrepo.go @@ -0,0 +1,35 @@ +// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. + +// Lexicon schema: sh.tangled.repo + +package tangledlex + +import ( + lexutil "github.com/bluesky-social/indigo/lex/util" +) + +func init() { + lexutil.RegisterType("sh.tangled.repo", &Repo{}) +} + +type Repo struct { + LexiconTypeID string `json:"$type" cborgen:"$type,const=sh.tangled.repo"` + CreatedAt string `json:"createdAt" cborgen:"createdAt"` + Description *string `json:"description,omitempty" cborgen:"description,omitempty"` + // knot: knot where the repo was created + Knot string `json:"knot" cborgen:"knot"` + // labels: List of labels that this repo subscribes to + Labels []string `json:"labels,omitempty" cborgen:"labels,omitempty"` + // name: Cosmetic name of the repo. + Name *string `json:"name,omitempty" cborgen:"name,omitempty"` + // repoDid: DID of the repo itself, if assigned + RepoDid *string `json:"repoDid,omitempty" cborgen:"repoDid,omitempty"` + // source: source of the repo + Source *string `json:"source,omitempty" cborgen:"source,omitempty"` + // spindle: CI runner to send jobs to and receive results from + Spindle *string `json:"spindle,omitempty" cborgen:"spindle,omitempty"` + // topics: Topics related to the repo + Topics []string `json:"topics,omitempty" cborgen:"topics,omitempty"` + // website: Any URI related to the repo + Website *string `json:"website,omitempty" cborgen:"website,omitempty"` +} diff --git a/internal/tangledlex/tangledstring.go b/internal/tangledlex/tangledstring.go new file mode 100644 index 0000000..71b7c89 --- /dev/null +++ b/internal/tangledlex/tangledstring.go @@ -0,0 +1,21 @@ +// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. + +// Lexicon schema: sh.tangled.string + +package tangledlex + +import ( + lexutil "github.com/bluesky-social/indigo/lex/util" +) + +func init() { + lexutil.RegisterType("sh.tangled.string", &String{}) +} + +type String struct { + LexiconTypeID string `json:"$type" cborgen:"$type,const=sh.tangled.string"` + Contents string `json:"contents" cborgen:"contents"` + CreatedAt string `json:"createdAt" cborgen:"createdAt"` + Description string `json:"description" cborgen:"description"` + Filename string `json:"filename" cborgen:"filename"` +} diff --git a/internal/tangledlex/update-schemas.sh b/internal/tangledlex/update-schemas.sh new file mode 100755 index 0000000..ad4385f --- /dev/null +++ b/internal/tangledlex/update-schemas.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env sh +set -eu + +if [ "$#" -ne 1 ]; then + echo "usage: $0 " >&2 + exit 2 +fi + +commit="$1" +case "$commit" in +*[!0123456789abcdef]*) + echo "commit must be a 40-character lowercase hexadecimal SHA" >&2 + exit 2 + ;; +esac +if [ "${#commit}" -ne 40 ]; then + echo "commit must be a 40-character lowercase hexadecimal SHA" >&2 + exit 2 +fi +source_url="https://tangled.org/tangled.org/core.git" +temporary_directory=$(mktemp -d) +trap 'rm -rf "$temporary_directory"' EXIT + +git init --quiet "$temporary_directory" +git -C "$temporary_directory" remote add origin "$source_url" +git -C "$temporary_directory" fetch --quiet --depth=1 origin "$commit" +git -C "$temporary_directory" checkout --quiet --detach FETCH_HEAD + +resolved_commit=$(git -C "$temporary_directory" rev-parse HEAD) +if [ "$resolved_commit" != "$commit" ]; then + echo "resolved $resolved_commit, expected $commit" >&2 + exit 1 +fi + +while IFS= read -r schema_path; do + [ -n "$schema_path" ] || continue + destination="schemas/$schema_path" + mkdir -p "$(dirname "$destination")" + git -C "$temporary_directory" show "HEAD:lexicons/$schema_path" >"$destination" +done lock.json +go generate . diff --git a/internal/tangledlex/validate.go b/internal/tangledlex/validate.go new file mode 100644 index 0000000..71b0987 --- /dev/null +++ b/internal/tangledlex/validate.go @@ -0,0 +1,339 @@ +package tangledlex + +import ( + "fmt" + "net/url" + "time" + "unicode/utf8" + + "github.com/bluesky-social/indigo/atproto/syntax" +) + +// ValidateRecord validates a Tangled record immediately before it is written +// to a PDS. Generated types supply the wire shape; this checks constraints +// that Go's type system cannot express. +func ValidateRecord(collection string, record any) error { + switch value := record.(type) { + case Repo: + return validateRepo(collection, value) + case RepoIssue: + return validateIssue(collection, value) + case RepoIssueComment: + return validateIssueComment(collection, value) + case RepoIssueState: + return validateIssueState(collection, value) + case RepoPull: + return validatePull(collection, value) + case RepoPullComment: + return validatePullComment(collection, value) + case RepoPullStatus: + return validatePullStatus(collection, value) + case PublicKey: + return validatePublicKey(collection, value) + case String: + return validateString(collection, value) + default: + return fmt.Errorf("%s record has unsupported generated type %T", collection, record) + } +} + +func validateRepo(collection string, record Repo) error { + if err := validateType(collection, record.LexiconTypeID, "sh.tangled.repo"); err != nil { + return err + } + if err := required("knot", record.Knot); err != nil { + return err + } + if err := datetime(record.CreatedAt); err != nil { + return fieldError("createdAt", err) + } + if record.Description != nil && !lengthBetween(*record.Description, 1, 140) { + return fmt.Errorf("description must contain 1 to 140 characters") + } + if record.RepoDid != nil { + if err := did(*record.RepoDid); err != nil { + return fieldError("repoDid", err) + } + } + for name, value := range map[string]*string{"source": record.Source, "website": record.Website} { + if value != nil { + if err := uri(*value); err != nil { + return fieldError(name, err) + } + } + } + for _, label := range record.Labels { + if err := atURI(label); err != nil { + return fieldError("labels", err) + } + } + if len(record.Topics) > 50 { + return fmt.Errorf("topics must not contain more than 50 items") + } + for _, topic := range record.Topics { + if !lengthBetween(topic, 1, 50) { + return fmt.Errorf("topics must contain 1 to 50 characters per item") + } + } + return nil +} + +func validateIssue(collection string, record RepoIssue) error { + if err := validateType(collection, record.LexiconTypeID, "sh.tangled.repo.issue"); err != nil { + return err + } + if err := did(record.Repo); err != nil { + return fieldError("repo", err) + } + if err := required("title", record.Title); err != nil { + return err + } + if err := validateMentions(record.Mentions); err != nil { + return err + } + if err := validateReferences(record.References); err != nil { + return err + } + return datetimeField(record.CreatedAt) +} + +func validateIssueComment(collection string, record RepoIssueComment) error { + if err := validateType(collection, record.LexiconTypeID, "sh.tangled.repo.issue.comment"); err != nil { + return err + } + if err := atURI(record.Issue); err != nil { + return fieldError("issue", err) + } + if err := required("body", record.Body); err != nil { + return err + } + if record.ReplyTo != nil { + if err := atURI(*record.ReplyTo); err != nil { + return fieldError("replyTo", err) + } + } + if err := validateMentions(record.Mentions); err != nil { + return err + } + if err := validateReferences(record.References); err != nil { + return err + } + return datetimeField(record.CreatedAt) +} + +func validateIssueState(collection string, record RepoIssueState) error { + if err := validateType(collection, record.LexiconTypeID, "sh.tangled.repo.issue.state"); err != nil { + return err + } + if err := atURI(record.Issue); err != nil { + return fieldError("issue", err) + } + if record.State != "sh.tangled.repo.issue.state.open" && record.State != "sh.tangled.repo.issue.state.closed" { + return fmt.Errorf("state must be an issue state value") + } + return datetimeField(record.CreatedAt) +} + +func validatePull(collection string, record RepoPull) error { + if err := validateType(collection, record.LexiconTypeID, "sh.tangled.repo.pull"); err != nil { + return err + } + if err := required("title", record.Title); err != nil { + return err + } + if record.Target == nil { + return fmt.Errorf("target is required") + } + if err := did(record.Target.Repo); err != nil { + return fieldError("target.repo", err) + } + if err := required("target.branch", record.Target.Branch); err != nil { + return err + } + if len(record.Rounds) == 0 { + return fmt.Errorf("rounds is required") + } + for _, round := range record.Rounds { + if round == nil || round.PatchBlob == nil { + return fmt.Errorf("rounds must contain a patch blob") + } + if !round.PatchBlob.Ref.Defined() { + return fmt.Errorf("rounds.patchBlob must reference a blob") + } + if err := datetime(round.CreatedAt); err != nil { + return fieldError("rounds.createdAt", err) + } + if round.PatchBlob.MimeType != "application/gzip" { + return fmt.Errorf("rounds.patchBlob must have MIME type application/gzip") + } + } + if record.Source != nil { + if err := required("source.branch", record.Source.Branch); err != nil { + return err + } + if record.Source.Repo != nil { + if err := did(*record.Source.Repo); err != nil { + return fieldError("source.repo", err) + } + } + } + if record.DependentOn != nil { + if err := atURI(*record.DependentOn); err != nil { + return fieldError("dependentOn", err) + } + } + if err := validateMentions(record.Mentions); err != nil { + return err + } + if err := validateReferences(record.References); err != nil { + return err + } + return datetimeField(record.CreatedAt) +} + +func validatePullComment(collection string, record RepoPullComment) error { + if err := validateType(collection, record.LexiconTypeID, "sh.tangled.repo.pull.comment"); err != nil { + return err + } + if err := atURI(record.Pull); err != nil { + return fieldError("pull", err) + } + if err := required("body", record.Body); err != nil { + return err + } + if err := validateMentions(record.Mentions); err != nil { + return err + } + if err := validateReferences(record.References); err != nil { + return err + } + return datetimeField(record.CreatedAt) +} + +func validatePullStatus(collection string, record RepoPullStatus) error { + if err := validateType(collection, record.LexiconTypeID, "sh.tangled.repo.pull.status"); err != nil { + return err + } + if err := atURI(record.Pull); err != nil { + return fieldError("pull", err) + } + if record.Status != "sh.tangled.repo.pull.status.open" && record.Status != "sh.tangled.repo.pull.status.closed" && record.Status != "sh.tangled.repo.pull.status.merged" { + return fmt.Errorf("status must be a pull status value") + } + return datetimeField(record.CreatedAt) +} + +func validatePublicKey(collection string, record PublicKey) error { + if err := validateType(collection, record.LexiconTypeID, "sh.tangled.publicKey"); err != nil { + return err + } + if err := required("key", record.Key); err != nil { + return err + } + if utf8.RuneCountInString(record.Key) > 4096 { + return fmt.Errorf("key must not exceed 4096 characters") + } + if err := required("name", record.Name); err != nil { + return err + } + return datetimeField(record.CreatedAt) +} + +func validateString(collection string, record String) error { + if err := validateType(collection, record.LexiconTypeID, "sh.tangled.string"); err != nil { + return err + } + if !lengthBetween(record.Filename, 1, 140) { + return fmt.Errorf("filename must contain 1 to 140 characters") + } + if utf8.RuneCountInString(record.Description) > 280 { + return fmt.Errorf("description must not exceed 280 characters") + } + if err := required("contents", record.Contents); err != nil { + return err + } + return datetimeField(record.CreatedAt) +} + +func validateType(collection, actual, expected string) error { + if collection != expected { + return fmt.Errorf("record type %q does not match collection %q", expected, collection) + } + if actual != expected { + return fmt.Errorf("$type must be %q", expected) + } + return nil +} + +func datetimeField(value string) error { + if err := datetime(value); err != nil { + return fieldError("createdAt", err) + } + return nil +} + +func required(name, value string) error { + if value == "" { + return fmt.Errorf("%s is required", name) + } + return nil +} + +func lengthBetween(value string, minimum, maximum int) bool { + length := utf8.RuneCountInString(value) + return length >= minimum && length <= maximum +} + +func validateMentions(mentions []string) error { + for _, mention := range mentions { + if err := did(mention); err != nil { + return fieldError("mentions", err) + } + } + return nil +} + +func validateReferences(references []string) error { + for _, reference := range references { + if err := atURI(reference); err != nil { + return fieldError("references", err) + } + } + return nil +} + +func datetime(value string) error { + if value == "" { + return fmt.Errorf("is required") + } + if _, err := time.Parse(time.RFC3339, value); err != nil { + return fmt.Errorf("must be RFC 3339: %w", err) + } + return nil +} + +func did(value string) error { + if _, err := syntax.ParseDID(value); err != nil { + return fmt.Errorf("must be a DID: %w", err) + } + return nil +} + +func atURI(value string) error { + if _, err := syntax.ParseATURI(value); err != nil { + return fmt.Errorf("must be an at:// URI: %w", err) + } + return nil +} + +func fieldError(name string, err error) error { + return fmt.Errorf("%s %w", name, err) +} + +func uri(value string) error { + parsed, err := url.ParseRequestURI(value) + if err != nil || parsed.Scheme == "" { + return fmt.Errorf("must be a URI") + } + return nil +} diff --git a/internal/tangledlex/validate_test.go b/internal/tangledlex/validate_test.go new file mode 100644 index 0000000..e656139 --- /dev/null +++ b/internal/tangledlex/validate_test.go @@ -0,0 +1,181 @@ +package tangledlex + +import ( + "strings" + "testing" + + lexutil "github.com/bluesky-social/indigo/lex/util" + "github.com/ipfs/go-cid" +) + +const ( + testDID = "did:plc:abc123" + testATURI = "at://did:plc:abc123/sh.tangled.repo.issue/3k2abc" + testTime = "2026-07-25T12:00:00Z" +) + +func TestValidateRecordAcceptsEveryWritableRecord(t *testing.T) { + for collection, record := range validRecords() { + t.Run(collection, func(t *testing.T) { + if err := ValidateRecord(collection, record); err != nil { + t.Fatalf("ValidateRecord() error = %v", err) + } + }) + } +} + +func TestValidateRecordRejectsEveryWritableRecord(t *testing.T) { + tests := []struct { + name string + collection string + record any + contains string + }{ + {"repo", "sh.tangled.repo", Repo{LexiconTypeID: "sh.tangled.repo", CreatedAt: testTime}, "knot"}, + {"issue", "sh.tangled.repo.issue", RepoIssue{LexiconTypeID: "sh.tangled.repo.issue", Title: "title", CreatedAt: testTime}, "repo"}, + {"issue comment", "sh.tangled.repo.issue.comment", RepoIssueComment{LexiconTypeID: "sh.tangled.repo.issue.comment", Body: "body", CreatedAt: testTime}, "issue"}, + {"issue state", "sh.tangled.repo.issue.state", RepoIssueState{LexiconTypeID: "sh.tangled.repo.issue.state", Issue: testATURI, State: "invalid", CreatedAt: testTime}, "state"}, + {"pull", "sh.tangled.repo.pull", RepoPull{LexiconTypeID: "sh.tangled.repo.pull", Title: "title", CreatedAt: testTime}, "target"}, + {"pull comment", "sh.tangled.repo.pull.comment", RepoPullComment{LexiconTypeID: "sh.tangled.repo.pull.comment", Body: "body", CreatedAt: testTime}, "pull"}, + {"pull status", "sh.tangled.repo.pull.status", RepoPullStatus{LexiconTypeID: "sh.tangled.repo.pull.status", Pull: testATURI, Status: "invalid", CreatedAt: testTime}, "status"}, + {"public key", "sh.tangled.publicKey", PublicKey{LexiconTypeID: "sh.tangled.publicKey", Name: "key", CreatedAt: testTime}, "key"}, + {"string", "sh.tangled.string", String{LexiconTypeID: "sh.tangled.string", Contents: "text", CreatedAt: testTime}, "filename"}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := ValidateRecord(test.collection, test.record) + if err == nil || !strings.Contains(err.Error(), test.contains) { + t.Fatalf("ValidateRecord() error = %v, want %q", err, test.contains) + } + }) + } +} + +func TestValidateRecordRejectsMismatchedType(t *testing.T) { + err := ValidateRecord("sh.tangled.repo.issue", RepoIssue{ + LexiconTypeID: "sh.tangled.repo.pull", + Repo: testDID, + Title: "title", + CreatedAt: testTime, + }) + if err == nil || !strings.Contains(err.Error(), "$type") { + t.Fatalf("ValidateRecord() error = %v, want type error", err) + } +} + +func TestValidateRecordRejectsNestedConstraints(t *testing.T) { + tests := []struct { + name string + collection string + record func() any + contains string + }{ + { + name: "repo website", + collection: "sh.tangled.repo", + record: func() any { + record := validRecords()["sh.tangled.repo"].(Repo) + record.Website = stringPointer(":not-a-uri") + return record + }, + contains: "website", + }, + { + name: "issue mention", + collection: "sh.tangled.repo.issue", + record: func() any { + record := validRecords()["sh.tangled.repo.issue"].(RepoIssue) + record.Mentions = []string{"not-a-did"} + return record + }, + contains: "mentions", + }, + { + name: "issue comment reply", + collection: "sh.tangled.repo.issue.comment", + record: func() any { + record := validRecords()["sh.tangled.repo.issue.comment"].(RepoIssueComment) + record.ReplyTo = stringPointer("not-an-at-uri") + return record + }, + contains: "replyTo", + }, + { + name: "pull blob reference", + collection: "sh.tangled.repo.pull", + record: func() any { + record := validRecords()["sh.tangled.repo.pull"].(RepoPull) + record.Rounds[0].PatchBlob.Ref = lexutil.LexLink{} + return record + }, + contains: "reference", + }, + { + name: "pull source repository", + collection: "sh.tangled.repo.pull", + record: func() any { + record := validRecords()["sh.tangled.repo.pull"].(RepoPull) + record.Source = &RepoPull_Source{Branch: "main", Repo: stringPointer("not-a-did")} + return record + }, + contains: "source.repo", + }, + { + name: "pull reference", + collection: "sh.tangled.repo.pull", + record: func() any { + record := validRecords()["sh.tangled.repo.pull"].(RepoPull) + record.DependentOn = stringPointer("not-an-at-uri") + return record + }, + contains: "dependentOn", + }, + { + name: "pull comment reference", + collection: "sh.tangled.repo.pull.comment", + record: func() any { + record := validRecords()["sh.tangled.repo.pull.comment"].(RepoPullComment) + record.References = []string{"not-an-at-uri"} + return record + }, + contains: "references", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := ValidateRecord(test.collection, test.record()) + if err == nil || !strings.Contains(err.Error(), test.contains) { + t.Fatalf("ValidateRecord() error = %v, want %q", err, test.contains) + } + }) + } +} + +func validRecords() map[string]any { + return map[string]any{ + "sh.tangled.repo": Repo{LexiconTypeID: "sh.tangled.repo", Knot: "knot.example", CreatedAt: testTime}, + "sh.tangled.repo.issue": RepoIssue{LexiconTypeID: "sh.tangled.repo.issue", Repo: testDID, Title: "title", CreatedAt: testTime}, + "sh.tangled.repo.issue.comment": RepoIssueComment{LexiconTypeID: "sh.tangled.repo.issue.comment", Issue: testATURI, Body: "body", CreatedAt: testTime}, + "sh.tangled.repo.issue.state": RepoIssueState{LexiconTypeID: "sh.tangled.repo.issue.state", Issue: testATURI, State: "sh.tangled.repo.issue.state.open", CreatedAt: testTime}, + "sh.tangled.repo.pull": RepoPull{ + LexiconTypeID: "sh.tangled.repo.pull", + Title: "title", + CreatedAt: testTime, + Target: &RepoPull_Target{Repo: testDID, Branch: "main"}, + Rounds: []*RepoPull_Round{{ + CreatedAt: testTime, + PatchBlob: &lexutil.LexBlob{Ref: lexutil.LexLink(cid.MustParse("bafybeigdyrzt5m6b5nkn55vsgzzfw5cfs2tidw6zqugycdkyybf2z7kz4q")), MimeType: "application/gzip"}, + }}, + }, + "sh.tangled.repo.pull.comment": RepoPullComment{LexiconTypeID: "sh.tangled.repo.pull.comment", Pull: testATURI, Body: "body", CreatedAt: testTime}, + "sh.tangled.repo.pull.status": RepoPullStatus{LexiconTypeID: "sh.tangled.repo.pull.status", Pull: testATURI, Status: "sh.tangled.repo.pull.status.open", CreatedAt: testTime}, + "sh.tangled.publicKey": PublicKey{LexiconTypeID: "sh.tangled.publicKey", Key: "ssh-ed25519 AAAA", Name: "key", CreatedAt: testTime}, + "sh.tangled.string": String{LexiconTypeID: "sh.tangled.string", Filename: "note.md", Description: "note", Contents: "text", CreatedAt: testTime}, + } +} + +func stringPointer(value string) *string { + return &value +} diff --git a/nix/tg.nix b/nix/tg.nix index cdf8db7..df80b8e 100644 --- a/nix/tg.nix +++ b/nix/tg.nix @@ -9,7 +9,7 @@ buildGoModule { pname = "tg"; version = "dev"; src = ../.; - vendorHash = "sha256-vAjC3nyJvXVvsBj+JXPN7dNPAdhwY64lqHLsOhTuVKc="; + vendorHash = "sha256-PQWuNj8xBORLOm9b7KCfORRbJFffOeDmk5hvhS4KoQA="; subPackages = ["cmd/tg"]; nativeBuildInputs = [installShellFiles]; diff --git a/tangled/get_repo.go b/tangled/get_repo.go index 9f36848..b11e22e 100644 --- a/tangled/get_repo.go +++ b/tangled/get_repo.go @@ -2,39 +2,39 @@ package tangled import ( "context" + "encoding/json" "fmt" - "github.com/bluesky-social/indigo/atproto/syntax" + "github.com/alyraffauf/tg/internal/tangledlex" ) -// RepoRecord is the value of a sh.tangled.repo lexicon record. -type RepoRecord struct { - Type string `json:"$type"` - Name string `json:"name,omitempty"` - Description string `json:"description,omitempty"` - Knot string `json:"knot"` - CreatedAt string `json:"createdAt"` - Owner string `json:"owner,omitempty"` - AddedAt string `json:"addedAt,omitempty"` - RepoDid string `json:"repoDid,omitempty"` - Source string `json:"source,omitempty"` - Spindle string `json:"spindle,omitempty"` - Website string `json:"website,omitempty"` - Labels []string `json:"labels,omitempty"` -} - type Repo struct { - URI string `json:"uri"` - CID string `json:"cid"` - Value RepoRecord `json:"value"` + URI string `json:"uri"` + CID string `json:"cid"` + Value tangledlex.Repo `json:"value"` } func (t *Tangled) GetRepo(ctx context.Context, repoURI string) (*Repo, error) { - var repo Repo - err := t.Client.Get(ctx, syntax.NSID("sh.tangled.repo.getRepo"), map[string]any{"repo": repoURI}, &repo) + response, err := tangledlex.RepoGetRepo(ctx, t.lexClient(), repoURI) if err != nil { return nil, fmt.Errorf("get tangled repo %q: %w", repoURI, err) } + value, err := recordJSON(response.Value, &tangledlex.Repo{}) + if err != nil { + return nil, fmt.Errorf("decode tangled repo %q: %w", repoURI, err) + } - return &repo, nil + var record tangledlex.Repo + if err := json.Unmarshal(value, &record); err != nil { + return nil, fmt.Errorf("decode tangled repo %q: %w", repoURI, err) + } + + return &Repo{URI: response.Uri, CID: dereference(response.Cid), Value: record}, nil +} + +func dereference(value *string) string { + if value == nil { + return "" + } + return *value } diff --git a/tangled/lex_client.go b/tangled/lex_client.go new file mode 100644 index 0000000..58f2dea --- /dev/null +++ b/tangled/lex_client.go @@ -0,0 +1,29 @@ +package tangled + +import ( + "context" + "fmt" + + "github.com/bluesky-social/indigo/atproto/atclient" + "github.com/bluesky-social/indigo/atproto/syntax" +) + +type lexClient struct { + client *atclient.APIClient +} + +func (c lexClient) LexDo(ctx context.Context, method, inputEncoding, endpoint string, params map[string]any, bodyData, out any) error { + if method != "GET" || inputEncoding != "" || bodyData != nil { + return fmt.Errorf("unsupported Bobbin lexicon request %s %s", method, endpoint) + } + + nsid, err := syntax.ParseNSID(endpoint) + if err != nil { + return fmt.Errorf("parse Bobbin endpoint: %w", err) + } + return c.client.Get(ctx, nsid, params, out) +} + +func (t *Tangled) lexClient() lexClient { + return lexClient{client: t.Client} +} diff --git a/tangled/lex_client_test.go b/tangled/lex_client_test.go new file mode 100644 index 0000000..c214926 --- /dev/null +++ b/tangled/lex_client_test.go @@ -0,0 +1,28 @@ +package tangled + +import ( + "context" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/bluesky-social/indigo/atproto/atclient" +) + +func TestListIssuesRejectsUnexpectedRecordType(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) { + if request.URL.Path != "/xrpc/sh.tangled.repo.listIssues" { + t.Fatalf("unexpected request path %q", request.URL.Path) + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"items":[{"uri":"at://did:plc:repo/sh.tangled.repo.issue/abc","value":{"$type":"sh.tangled.repo.pull"},"state":"open","commentCount":0}]}`)) + })) + defer server.Close() + + client := Tangled{Client: &atclient.APIClient{Client: server.Client(), Host: server.URL}} + _, err := client.ListIssues(context.Background(), "did:plc:repo", ListOpts{}) + if err == nil || !strings.Contains(err.Error(), "expected *tangledlex.RepoIssue") { + t.Fatalf("ListIssues error = %v, want unexpected record type", err) + } +} diff --git a/tangled/lex_decode.go b/tangled/lex_decode.go new file mode 100644 index 0000000..9390077 --- /dev/null +++ b/tangled/lex_decode.go @@ -0,0 +1,24 @@ +package tangled + +import ( + "encoding/json" + "fmt" + "reflect" + + "github.com/bluesky-social/indigo/lex/util" +) + +func recordJSON(value *util.LexiconTypeDecoder, expected any) (json.RawMessage, error) { + if value == nil || value.Val == nil { + return nil, fmt.Errorf("Bobbin response is missing a record value") + } + if reflect.TypeOf(value.Val) != reflect.TypeOf(expected) { + return nil, fmt.Errorf("Bobbin response has record type %T; expected %T", value.Val, expected) + } + + encoded, err := json.Marshal(value) + if err != nil { + return nil, fmt.Errorf("encode lexicon record: %w", err) + } + return encoded, nil +} diff --git a/tangled/list.go b/tangled/list.go index a2d8c48..d32d11a 100644 --- a/tangled/list.go +++ b/tangled/list.go @@ -34,21 +34,22 @@ type ListOpts struct { Order string // "asc" or "desc" } -// params builds the XRPC query parameters for subject, requesting the page -// after cursor (the first page if cursor is empty). +func (o ListOpts) limit() int64 { + if o.Limit > 0 { + return o.Limit + } + return 50 +} + +// params builds query parameters for a paginated issue or pull request list. func (o ListOpts) params(subject, cursor string) map[string]any { - params := map[string]any{"subject": subject} + params := map[string]any{"subject": subject, "limit": o.limit()} if o.Author != "" { params["author"] = o.Author } if o.State != "" { params["state"] = o.State } - if o.Limit > 0 { - params["limit"] = o.Limit - } else { - params["limit"] = int64(50) - } if o.Order != "" { params["order"] = o.Order } diff --git a/tangled/list_issues.go b/tangled/list_issues.go index 1626174..1be1121 100644 --- a/tangled/list_issues.go +++ b/tangled/list_issues.go @@ -4,31 +4,34 @@ import ( "context" "fmt" - "github.com/bluesky-social/indigo/atproto/syntax" + "github.com/alyraffauf/tg/internal/tangledlex" ) -type IssueRecord struct { - Type string `json:"$type"` - Repo string `json:"repo"` - Title string `json:"title"` - Body string `json:"body,omitempty"` - CreatedAt string `json:"createdAt"` - Mentions []string `json:"mentions,omitempty"` - References []string `json:"references,omitempty"` -} - // ListIssues fetches every issue for repoDid, following pagination // cursors until the listing is exhausted. func (t *Tangled) ListIssues(ctx context.Context, repoDid string, opts ListOpts) (*List, error) { items, err := fetchAllPages(ctx, func(ctx context.Context, cursor string) ([]ListItem, *string, error) { - var page List - if err := t.Client.Get(ctx, syntax.NSID("sh.tangled.repo.listIssues"), opts.params(repoDid, cursor), &page); err != nil { + page, err := tangledlex.RepoListIssues(ctx, t.lexClient(), opts.Author, cursor, opts.limit(), opts.Order, opts.State, repoDid) + if err != nil { return nil, nil, err } - return page.Items, page.Cursor, nil + items, err := issueListItems(page.Items) + return items, page.Cursor, err }) if err != nil { return nil, fmt.Errorf("list issues for %q: %w", repoDid, err) } return &List{Items: items}, nil } + +func issueListItems(items []*tangledlex.RepoListIssues_IssueListItem) ([]ListItem, error) { + decoded := make([]ListItem, 0, len(items)) + for _, item := range items { + value, err := recordJSON(item.Value, &tangledlex.RepoIssue{}) + if err != nil { + return nil, err + } + decoded = append(decoded, ListItem{URI: item.Uri, CID: dereference(item.Cid), Value: value, State: item.State, StateUpdatedAt: dereference(item.StateUpdatedAt), CommentCount: item.CommentCount}) + } + return decoded, nil +} diff --git a/tangled/list_pulls.go b/tangled/list_pulls.go index 4983b5a..524c598 100644 --- a/tangled/list_pulls.go +++ b/tangled/list_pulls.go @@ -4,58 +4,34 @@ import ( "context" "fmt" - "github.com/bluesky-social/indigo/atproto/atdata" - "github.com/bluesky-social/indigo/atproto/syntax" + "github.com/alyraffauf/tg/internal/tangledlex" ) -type PullRecord struct { - Type string `json:"$type"` - Title string `json:"title"` - Body string `json:"body,omitempty"` - CreatedAt string `json:"createdAt"` - Mentions []string `json:"mentions,omitempty"` - References []string `json:"references,omitempty"` - DependentOn string `json:"dependentOn,omitempty"` - Target PullTarget `json:"target"` - Source PullSource `json:"source"` - Rounds []PullRound `json:"rounds"` -} - -type PullTarget struct { - Repo string `json:"repo"` - Branch string `json:"branch"` -} - -type PullSource struct { - Repo string `json:"repo,omitempty"` - Branch string `json:"branch"` -} - -type PullRound struct { - CreatedAt string `json:"createdAt"` - PatchBlob PatchBlob `json:"patchBlob"` -} - -// PatchBlob is a gzipped git patch referenced by CID. -type PatchBlob struct { - Type string `json:"$type"` - Ref atdata.CIDLink `json:"ref"` - MimeType string `json:"mimeType"` - Size int64 `json:"size"` -} - // ListPulls fetches every pull request for repoDid, following pagination // cursors until the listing is exhausted. func (t *Tangled) ListPulls(ctx context.Context, repoDid string, opts ListOpts) (*List, error) { items, err := fetchAllPages(ctx, func(ctx context.Context, cursor string) ([]ListItem, *string, error) { - var page List - if err := t.Client.Get(ctx, syntax.NSID("sh.tangled.repo.listPulls"), opts.params(repoDid, cursor), &page); err != nil { + page, err := tangledlex.RepoListPulls(ctx, t.lexClient(), opts.Author, cursor, opts.limit(), opts.Order, opts.State, repoDid) + if err != nil { return nil, nil, err } - return page.Items, page.Cursor, nil + items, err := pullListItems(page.Items) + return items, page.Cursor, err }) if err != nil { return nil, fmt.Errorf("list PRs for %q: %w", repoDid, err) } return &List{Items: items}, nil } + +func pullListItems(items []*tangledlex.RepoListPulls_PullListItem) ([]ListItem, error) { + decoded := make([]ListItem, 0, len(items)) + for _, item := range items { + value, err := recordJSON(item.Value, &tangledlex.RepoPull{}) + if err != nil { + return nil, err + } + decoded = append(decoded, ListItem{URI: item.Uri, CID: dereference(item.Cid), Value: value, State: item.State, StateUpdatedAt: dereference(item.StateUpdatedAt), CommentCount: item.CommentCount}) + } + return decoded, nil +} diff --git a/tangled/list_repos.go b/tangled/list_repos.go index c8c1fc9..639cfcb 100644 --- a/tangled/list_repos.go +++ b/tangled/list_repos.go @@ -2,9 +2,10 @@ package tangled import ( "context" + "encoding/json" "fmt" - "github.com/bluesky-social/indigo/atproto/syntax" + "github.com/alyraffauf/tg/internal/tangledlex" ) type RepoList struct { @@ -16,15 +17,12 @@ type RepoList struct { // cursors until the listing is exhausted. func (t *Tangled) ListRepos(ctx context.Context, ownerDid string) (*RepoList, error) { items, err := fetchAllPages(ctx, func(ctx context.Context, cursor string) ([]Repo, *string, error) { - params := map[string]any{"subject": ownerDid, "limit": 100} - if cursor != "" { - params["cursor"] = cursor - } - var page RepoList - if err := t.Client.Get(ctx, syntax.NSID("sh.tangled.repo.listRepos"), params, &page); err != nil { + page, err := tangledlex.RepoListRepos(ctx, t.lexClient(), cursor, 100, "", ownerDid) + if err != nil { return nil, nil, err } - return page.Items, page.Cursor, nil + items, err := repoListItems(page.Items) + return items, page.Cursor, err }) if err != nil { return nil, fmt.Errorf("list tangled repos for %q: %w", ownerDid, err) @@ -32,3 +30,19 @@ func (t *Tangled) ListRepos(ctx context.Context, ownerDid string) (*RepoList, er return &RepoList{Items: items}, nil } + +func repoListItems(items []*tangledlex.RepoListRepos_ListItem) ([]Repo, error) { + decoded := make([]Repo, 0, len(items)) + for _, item := range items { + value, err := recordJSON(item.Value, &tangledlex.Repo{}) + if err != nil { + return nil, err + } + var record tangledlex.Repo + if err := json.Unmarshal(value, &record); err != nil { + return nil, fmt.Errorf("decode repository record: %w", err) + } + decoded = append(decoded, Repo{URI: item.Uri, CID: dereference(item.Cid), Value: record}) + } + return decoded, nil +} diff --git a/tangled/records.go b/tangled/records.go index 79c7adb..14122e4 100644 --- a/tangled/records.go +++ b/tangled/records.go @@ -9,50 +9,3 @@ const ( IssueStateSuffix = ".state" PullStatusSuffix = ".status" ) - -// IssueCommentRecord is the value of a sh.tangled.repo.issue.comment record. -type IssueCommentRecord struct { - Type string `json:"$type"` - Issue string `json:"issue"` - Body string `json:"body"` - CreatedAt string `json:"createdAt"` -} - -// PullCommentRecord is the value of a sh.tangled.repo.pull.comment record. -type PullCommentRecord struct { - Type string `json:"$type"` - Pull string `json:"pull"` - Body string `json:"body"` - CreatedAt string `json:"createdAt"` -} - -// IssueStateRecord is the value of a sh.tangled.repo.issue.state record. -type IssueStateRecord struct { - Type string `json:"$type"` - Issue string `json:"issue"` - State string `json:"state"` -} - -// PullStatusRecord is the value of a sh.tangled.repo.pull.status record. -type PullStatusRecord struct { - Type string `json:"$type"` - Pull string `json:"pull"` - Status string `json:"status"` -} - -// SSHKeyRecord is the value of a sh.tangled.publicKey record. -type SSHKeyRecord struct { - Type string `json:"$type"` - Key string `json:"key"` - Name string `json:"name"` - CreatedAt string `json:"createdAt"` -} - -// StringRecord is the value of a sh.tangled.string record. -type StringRecord struct { - Type string `json:"$type"` - Filename string `json:"filename"` - Description string `json:"description"` - Contents string `json:"contents"` - CreatedAt string `json:"createdAt"` -} -- 2.51.2