From 492051c6330e744edf58f8fc082d93cea8b8c679 Mon Sep 17 00:00:00 2001 From: Lewis Date: Mon, 29 Jun 2026 09:01:59 +0300 Subject: [PATCH] appview/models: issue/pull state record Lewis: May this revision serve well! --- appview/models/entity_state.go | 72 ++++++++++++++++++++++++++ appview/models/entity_state_test.go | 80 +++++++++++++++++++++++++++++ 2 files changed, 152 insertions(+) create mode 100644 appview/models/entity_state.go create mode 100644 appview/models/entity_state_test.go diff --git a/appview/models/entity_state.go b/appview/models/entity_state.go new file mode 100644 index 00000000..162a9946 --- /dev/null +++ b/appview/models/entity_state.go @@ -0,0 +1,72 @@ +package models + +import ( + "fmt" + + "github.com/bluesky-social/indigo/atproto/syntax" + "tangled.org/core/api/tangled" +) + +type StateValue string + +const ( + StateOpen StateValue = "open" + StateClosed StateValue = "closed" + StateMerged StateValue = "merged" +) + +type StateRecord struct { + Did string + Rkey string + Subject syntax.ATURI + Value StateValue + SortMicros int64 +} + +func sortMicros(createdAt, rkey string) int64 { + if t, err := syntax.ParseDatetimeTime(createdAt); err == nil { + return t.UnixMicro() + } + if tid, err := syntax.ParseTID(rkey); err == nil { + return tid.Time().UnixMicro() + } + return 0 +} + +func newStateRecord(did, rkey, subjectUri, createdAt string, value StateValue) (StateRecord, error) { + subject, err := syntax.ParseATURI(subjectUri) + if err != nil { + return StateRecord{}, fmt.Errorf("invalid subject uri: %w", err) + } + return StateRecord{ + Did: did, + Rkey: rkey, + Subject: subject, + Value: value, + SortMicros: sortMicros(createdAt, rkey), + }, nil +} + +func IssueStateFromRecord(did, rkey string, record tangled.RepoIssueState) (StateRecord, error) { + switch record.State { + case tangled.RepoIssueStateOpen: + return newStateRecord(did, rkey, record.Issue, record.CreatedAt, StateOpen) + case tangled.RepoIssueStateClosed: + return newStateRecord(did, rkey, record.Issue, record.CreatedAt, StateClosed) + default: + return StateRecord{}, fmt.Errorf("unknown issue state variant: %q", record.State) + } +} + +func PullStatusFromRecord(did, rkey string, record tangled.RepoPullStatus) (StateRecord, error) { + switch record.Status { + case tangled.RepoPullStatusOpen: + return newStateRecord(did, rkey, record.Pull, record.CreatedAt, StateOpen) + case tangled.RepoPullStatusClosed: + return newStateRecord(did, rkey, record.Pull, record.CreatedAt, StateClosed) + case tangled.RepoPullStatusMerged: + return newStateRecord(did, rkey, record.Pull, record.CreatedAt, StateMerged) + default: + return StateRecord{}, fmt.Errorf("unknown pull status variant: %q", record.Status) + } +} diff --git a/appview/models/entity_state_test.go b/appview/models/entity_state_test.go new file mode 100644 index 00000000..2f0b6850 --- /dev/null +++ b/appview/models/entity_state_test.go @@ -0,0 +1,80 @@ +package models + +import ( + "testing" + "time" + + "github.com/bluesky-social/indigo/atproto/syntax" + "tangled.org/core/api/tangled" +) + +const testIssueSubject = "at://did:plc:anemone/sh.tangled.repo.issue/i1" + +func TestIssueStateFromRecord(t *testing.T) { + const rkey = "3jzfcijpj2z2a" + + if _, err := IssueStateFromRecord("did:plc:akshay", rkey, tangled.RepoIssueState{ + Issue: testIssueSubject, + State: "sh.tangled.repo.issue.state.reopened", + CreatedAt: "2026-06-01T00:00:00Z", + }); err == nil { + t.Fatal("unknown state variant must be rejected") + } + + created := "2026-06-01T12:30:00Z" + rec, err := IssueStateFromRecord("did:plc:akshay", rkey, tangled.RepoIssueState{ + Issue: testIssueSubject, + State: tangled.RepoIssueStateClosed, + CreatedAt: created, + }) + if err != nil { + t.Fatalf("IssueStateFromRecord: %v", err) + } + want, _ := time.Parse(time.RFC3339, created) + if rec.Value != StateClosed || rec.SortMicros != want.UnixMicro() { + t.Fatalf("got value=%q micros=%d, want closed with createdAt as the sort key", rec.Value, rec.SortMicros) + } + + tidRec, err := IssueStateFromRecord("did:plc:akshay", rkey, tangled.RepoIssueState{ + Issue: testIssueSubject, + State: tangled.RepoIssueStateOpen, + CreatedAt: "not-a-datetime", + }) + if err != nil { + t.Fatalf("IssueStateFromRecord tid fallback: %v", err) + } + tid, _ := syntax.ParseTID(rkey) + if tidRec.SortMicros != tid.Time().UnixMicro() { + t.Fatalf("SortMicros = %d, want TID micros %d when createdAt is unparseable", tidRec.SortMicros, tid.Time().UnixMicro()) + } +} + +func TestPullStatusFromRecord_Variants(t *testing.T) { + subject := "at://did:plc:limpet/sh.tangled.repo.pull/p1" + cases := map[string]StateValue{ + tangled.RepoPullStatusOpen: StateOpen, + tangled.RepoPullStatusClosed: StateClosed, + tangled.RepoPullStatusMerged: StateMerged, + } + for wire, want := range cases { + rec, err := PullStatusFromRecord("did:plc:akshay", "3jzfcijpj2z2a", tangled.RepoPullStatus{ + Pull: subject, + Status: wire, + CreatedAt: "2026-06-01T00:00:00Z", + }) + if err != nil { + t.Fatalf("PullStatusFromRecord(%q): %v", wire, err) + } + if rec.Value != want { + t.Fatalf("PullStatusFromRecord(%q) value = %q, want %q", wire, rec.Value, want) + } + } + + if _, err := PullStatusFromRecord("did:plc:akshay", "3jzfcijpj2z2a", tangled.RepoPullStatus{ + Pull: subject, + Status: "sh.tangled.repo.pull.status.draft", + CreatedAt: "2026-06-01T00:00:00Z", + }); err == nil { + t.Fatal("unknown pull status variant must be rejected") + } +} -- 2.51.2