Monorepo for Tangled
0

Configure Feed

Select the types of activity you want to include in your feed.

appview/models: issue/pull state record

Lewis: May this revision serve well! <lewis@tangled.org>

authored by

Lewis and committed by
Tangled
(Jun 29, 2026, 8:20 PM +0300) 492051c6 02c365f3

+152
+72
appview/models/entity_state.go
··· 1 + package models 2 + 3 + import ( 4 + "fmt" 5 + 6 + "github.com/bluesky-social/indigo/atproto/syntax" 7 + "tangled.org/core/api/tangled" 8 + ) 9 + 10 + type StateValue string 11 + 12 + const ( 13 + StateOpen StateValue = "open" 14 + StateClosed StateValue = "closed" 15 + StateMerged StateValue = "merged" 16 + ) 17 + 18 + type StateRecord struct { 19 + Did string 20 + Rkey string 21 + Subject syntax.ATURI 22 + Value StateValue 23 + SortMicros int64 24 + } 25 + 26 + func sortMicros(createdAt, rkey string) int64 { 27 + if t, err := syntax.ParseDatetimeTime(createdAt); err == nil { 28 + return t.UnixMicro() 29 + } 30 + if tid, err := syntax.ParseTID(rkey); err == nil { 31 + return tid.Time().UnixMicro() 32 + } 33 + return 0 34 + } 35 + 36 + func newStateRecord(did, rkey, subjectUri, createdAt string, value StateValue) (StateRecord, error) { 37 + subject, err := syntax.ParseATURI(subjectUri) 38 + if err != nil { 39 + return StateRecord{}, fmt.Errorf("invalid subject uri: %w", err) 40 + } 41 + return StateRecord{ 42 + Did: did, 43 + Rkey: rkey, 44 + Subject: subject, 45 + Value: value, 46 + SortMicros: sortMicros(createdAt, rkey), 47 + }, nil 48 + } 49 + 50 + func IssueStateFromRecord(did, rkey string, record tangled.RepoIssueState) (StateRecord, error) { 51 + switch record.State { 52 + case tangled.RepoIssueStateOpen: 53 + return newStateRecord(did, rkey, record.Issue, record.CreatedAt, StateOpen) 54 + case tangled.RepoIssueStateClosed: 55 + return newStateRecord(did, rkey, record.Issue, record.CreatedAt, StateClosed) 56 + default: 57 + return StateRecord{}, fmt.Errorf("unknown issue state variant: %q", record.State) 58 + } 59 + } 60 + 61 + func PullStatusFromRecord(did, rkey string, record tangled.RepoPullStatus) (StateRecord, error) { 62 + switch record.Status { 63 + case tangled.RepoPullStatusOpen: 64 + return newStateRecord(did, rkey, record.Pull, record.CreatedAt, StateOpen) 65 + case tangled.RepoPullStatusClosed: 66 + return newStateRecord(did, rkey, record.Pull, record.CreatedAt, StateClosed) 67 + case tangled.RepoPullStatusMerged: 68 + return newStateRecord(did, rkey, record.Pull, record.CreatedAt, StateMerged) 69 + default: 70 + return StateRecord{}, fmt.Errorf("unknown pull status variant: %q", record.Status) 71 + } 72 + }
+80
appview/models/entity_state_test.go
··· 1 + package models 2 + 3 + import ( 4 + "testing" 5 + "time" 6 + 7 + "github.com/bluesky-social/indigo/atproto/syntax" 8 + "tangled.org/core/api/tangled" 9 + ) 10 + 11 + const testIssueSubject = "at://did:plc:anemone/sh.tangled.repo.issue/i1" 12 + 13 + func TestIssueStateFromRecord(t *testing.T) { 14 + const rkey = "3jzfcijpj2z2a" 15 + 16 + if _, err := IssueStateFromRecord("did:plc:akshay", rkey, tangled.RepoIssueState{ 17 + Issue: testIssueSubject, 18 + State: "sh.tangled.repo.issue.state.reopened", 19 + CreatedAt: "2026-06-01T00:00:00Z", 20 + }); err == nil { 21 + t.Fatal("unknown state variant must be rejected") 22 + } 23 + 24 + created := "2026-06-01T12:30:00Z" 25 + rec, err := IssueStateFromRecord("did:plc:akshay", rkey, tangled.RepoIssueState{ 26 + Issue: testIssueSubject, 27 + State: tangled.RepoIssueStateClosed, 28 + CreatedAt: created, 29 + }) 30 + if err != nil { 31 + t.Fatalf("IssueStateFromRecord: %v", err) 32 + } 33 + want, _ := time.Parse(time.RFC3339, created) 34 + if rec.Value != StateClosed || rec.SortMicros != want.UnixMicro() { 35 + t.Fatalf("got value=%q micros=%d, want closed with createdAt as the sort key", rec.Value, rec.SortMicros) 36 + } 37 + 38 + tidRec, err := IssueStateFromRecord("did:plc:akshay", rkey, tangled.RepoIssueState{ 39 + Issue: testIssueSubject, 40 + State: tangled.RepoIssueStateOpen, 41 + CreatedAt: "not-a-datetime", 42 + }) 43 + if err != nil { 44 + t.Fatalf("IssueStateFromRecord tid fallback: %v", err) 45 + } 46 + tid, _ := syntax.ParseTID(rkey) 47 + if tidRec.SortMicros != tid.Time().UnixMicro() { 48 + t.Fatalf("SortMicros = %d, want TID micros %d when createdAt is unparseable", tidRec.SortMicros, tid.Time().UnixMicro()) 49 + } 50 + } 51 + 52 + func TestPullStatusFromRecord_Variants(t *testing.T) { 53 + subject := "at://did:plc:limpet/sh.tangled.repo.pull/p1" 54 + cases := map[string]StateValue{ 55 + tangled.RepoPullStatusOpen: StateOpen, 56 + tangled.RepoPullStatusClosed: StateClosed, 57 + tangled.RepoPullStatusMerged: StateMerged, 58 + } 59 + for wire, want := range cases { 60 + rec, err := PullStatusFromRecord("did:plc:akshay", "3jzfcijpj2z2a", tangled.RepoPullStatus{ 61 + Pull: subject, 62 + Status: wire, 63 + CreatedAt: "2026-06-01T00:00:00Z", 64 + }) 65 + if err != nil { 66 + t.Fatalf("PullStatusFromRecord(%q): %v", wire, err) 67 + } 68 + if rec.Value != want { 69 + t.Fatalf("PullStatusFromRecord(%q) value = %q, want %q", wire, rec.Value, want) 70 + } 71 + } 72 + 73 + if _, err := PullStatusFromRecord("did:plc:akshay", "3jzfcijpj2z2a", tangled.RepoPullStatus{ 74 + Pull: subject, 75 + Status: "sh.tangled.repo.pull.status.draft", 76 + CreatedAt: "2026-06-01T00:00:00Z", 77 + }); err == nil { 78 + t.Fatal("unknown pull status variant must be rejected") 79 + } 80 + }