coverage~bigbes/sr-ht-spec3cb1c03dweb/reader.go

Coverage
0.0% 0/32 statements
Δ
Blob
e5fee44
1 package web
2
3 import (
4 "context"
5 "time"
6
7 "sourcecraft.dev/bigbes/sr-ht-spec/authn"
8 "sourcecraft.dev/bigbes/sr-ht-spec/core"
9 "sourcecraft.dev/bigbes/sr-ht-spec/doc"
10 "sourcecraft.dev/bigbes/sr-ht-spec/search"
11 "sourcecraft.dev/bigbes/sr-ht-spec/service"
12 )
13
14 // Snapshot is one space at one *resolved* revision: the addressable document
15 // set plus every document's bytes.
16 //
17 // Rev is always a commit sha, never a branch name, even when the request
18 // carried no ?rev= at all. The read plane resolves first and renders second, so
19 // a merge landing mid-request cannot make one page describe two revisions —
20 // and the sha it resolved to is the value the page offers as its permalink.
21 //
22 // Bodies is keyed by tree path, the same key doc.Page.Path carries. It comes
23 // off the same tree walk as the archive, so a document listed here and a
24 // document in the archive are the same bytes at the same revision.
25 type Snapshot struct {
26 Ref core.SpaceRef
27 Rev string
28 Archive *doc.Archive
29 Bodies map[string][]byte
30 }
31
32 // Reader is the service surface this package needs. *service.Service provides
33 // it through NewReader.
34 //
35 // It is an interface for the same reason compare.sr.ht's authz.Authorizer is:
36 // so the handlers can be tested against a document set rather than against a
37 // Postgres instance and a tree of bare repositories. It is named Reader for the
38 // read plane it began as; Phase 4's review page adds the proposal reads and the
39 // two approve/reject writes, because the review page is where this package first
40 // mutates anything and one seam is simpler than two.
41 type Reader interface {
42 // ListSpaces returns every space on the instance. Single-user with no
43 // visibility levels means there is nothing to filter — the list is the
44 // whole corpus.
45 ListSpaces(ctx context.Context) ([]core.SpaceRef, error)
46
47 // Snapshot resolves rev (service.ApprovedRev for the approved head) and
48 // returns the space at that revision.
49 Snapshot(ctx context.Context, ref core.SpaceRef, rev string) (*Snapshot, error)
50
51 // ReadDocument reads one document by tree path at a revision.
52 ReadDocument(ctx context.Context, ref core.SpaceRef, rev, path string) (service.Document, error)
53
54 // GetProposal resolves one proposal by id, in any state — the stable
55 // proposal URL still resolves after merge or rejection.
56 GetProposal(ctx context.Context, id int) (service.Proposal, error)
57
58 // ListProposals returns a space's proposals in one state, newest first: the
59 // inbox (open) and the digest (merged) are the same call with a different
60 // state.
61 ListProposals(ctx context.Context, ref core.SpaceRef, state core.ProposalState) ([]service.Proposal, error)
62
63 // ProposalDiff returns each document a proposal changes, with the base and
64 // proposed content the page diffs.
65 ProposalDiff(ctx context.Context, p service.Proposal) ([]service.ProposalDoc, error)
66
67 // Approve merges a proposal on the owner's approval — always human, never
68 // policy. Reject resolves it to rejected. Both return the proposal as it now
69 // stands.
70 Approve(ctx context.Context, ref core.SpaceRef, id int) (service.Proposal, error)
71 Reject(ctx context.Context, ref core.SpaceRef, id int) (service.Proposal, error)
72
73 // Threads returns a proposal's review conversations with their replies, and
74 // the three comment writes the review page makes.
75 //
76 // These carry the principal explicitly rather than reading it from the
77 // context the way the read plane does, because the authority they turn on is
78 // not the read ACL: only the owner may open or resolve a thread, while owner
79 // and agent alike may reply. service/ decides that and answers ErrForbidden;
80 // this package passes the principal through and surfaces the refusal.
81 Threads(ctx context.Context, p authn.Principal, proposalID int) ([]service.Thread, error)
82 CommentOn(ctx context.Context, req service.CommentRequest) (service.Thread, error)
83 ReplyTo(ctx context.Context, p authn.Principal, threadID int, body string) (service.Comment, error)
84 ResolveThread(ctx context.Context, p authn.Principal, threadID int, resolved bool) error
85
86 // There is no token surface here. /tokens used to mint, list and revoke
87 // spec's own agent credential; issuance is tokens.sr.ht's now, so the route
88 // is a redirect and this interface has nothing to serve it.
89
90 // Inbox is every open proposal on the instance, newest first — the review
91 // queue. Digest is the recently policy-merged proposals, the firehose a human
92 // sees after the fact.
93 Inbox(ctx context.Context) ([]service.Proposal, error)
94 Digest(ctx context.Context) ([]service.Proposal, error)
95
96 // DigestMark reports when the owner last marked the digest seen, and whether
97 // a mark exists yet; the inbox reads it to divide new auto-merges from seen
98 // ones and itself never writes. MarkDigestSeen advances it — the one write
99 // the review queue makes, behind an explicit POST so the GET stays pure.
100 DigestMark(ctx context.Context) (time.Time, bool, error)
101 MarkDigestSeen(ctx context.Context, seenAt time.Time) error
102 }
103
104 // Searcher is the keyword index. *search.Index satisfies it as declared.
105 type Searcher interface {
106 Search(ctx context.Context, q search.Query) (search.Results, error)
107 }
108
109 // NewReader adapts a *service.Service to Reader.
110 0 func NewReader(svc *service.Service) Reader { return serviceReader{svc: svc} }
111
112 // serviceReader is the production Reader: everything goes through service/,
113 // which is the layer that is allowed to touch gitx and db.
114 type serviceReader struct{ svc *service.Service }
115
116 0 func (r serviceReader) ListSpaces(ctx context.Context) ([]core.SpaceRef, error) {
117 0 spaces, err := r.svc.ListSpaces(ctx)
118 0 if err != nil {
119 0 return nil, err
120 0 }
121 0 refs := make([]core.SpaceRef, 0, len(spaces))
122 0 for _, sp := range spaces {
123 0 refs = append(refs, sp.Ref)
124 0 }
125 0 return refs, nil
126 }
127
128 0 func (r serviceReader) Snapshot(ctx context.Context, ref core.SpaceRef, rev string) (*Snapshot, error) {
129 0 sp, err := r.svc.OpenSpace(ctx, ref)
130 0 if err != nil {
131 0 return nil, err
132 0 }
133 // One call, one tree walk, and the link graph filled in. This package used
134 // to scan the git tree itself and then list the documents a second time for
135 // their bodies — two walks per page view, and a reach past service/ into
136 // gitx that the layering rule forbids.
137 0 arc, bodies, err := r.svc.Archive(ctx, sp, rev)
138 0 if err != nil {
139 0 return nil, err
140 0 }
141 0 return &Snapshot{Ref: ref, Rev: arc.Rev, Archive: arc, Bodies: bodies}, nil
142 }
143
144 0 func (r serviceReader) ReadDocument(ctx context.Context, ref core.SpaceRef, rev, p string) (service.Document, error) {
145 0 sp, err := r.svc.OpenSpace(ctx, ref)
146 0 if err != nil {
147 0 return service.Document{}, err
148 0 }
149 0 return r.svc.ReadDocument(ctx, sp, rev, p)
150 }
151
152 0 func (r serviceReader) GetProposal(ctx context.Context, id int) (service.Proposal, error) {
153 0 return r.svc.GetProposal(ctx, id)
154 0 }
155
156 0 func (r serviceReader) ListProposals(ctx context.Context, ref core.SpaceRef, state core.ProposalState) ([]service.Proposal, error) {
157 0 return r.svc.ListProposals(ctx, ref, state)
158 0 }
159
160 0 func (r serviceReader) ProposalDiff(ctx context.Context, p service.Proposal) ([]service.ProposalDoc, error) {
161 0 return r.svc.ProposalDiff(ctx, p)
162 0 }
163
164 0 func (r serviceReader) Approve(ctx context.Context, ref core.SpaceRef, id int) (service.Proposal, error) {
165 0 return r.svc.MergeHuman(ctx, ref, id)
166 0 }
167
168 0 func (r serviceReader) Reject(ctx context.Context, ref core.SpaceRef, id int) (service.Proposal, error) {
169 0 return r.svc.Reject(ctx, ref, id)
170 0 }
171
172 0 func (r serviceReader) Threads(ctx context.Context, p authn.Principal, proposalID int) ([]service.Thread, error) {
173 0 return r.svc.Threads(ctx, p, proposalID)
174 0 }
175
176 0 func (r serviceReader) CommentOn(ctx context.Context, req service.CommentRequest) (service.Thread, error) {
177 0 return r.svc.CommentOn(ctx, req)
178 0 }
179
180 0 func (r serviceReader) ReplyTo(ctx context.Context, p authn.Principal, threadID int, body string) (service.Comment, error) {
181 0 return r.svc.ReplyTo(ctx, p, threadID, body)
182 0 }
183
184 0 func (r serviceReader) ResolveThread(ctx context.Context, p authn.Principal, threadID int, resolved bool) error {
185 0 return r.svc.ResolveThread(ctx, p, threadID, resolved)
186 0 }
187
188 0 func (r serviceReader) Inbox(ctx context.Context) ([]service.Proposal, error) {
189 0 return r.svc.InboxProposals(ctx)
190 0 }
191
192 0 func (r serviceReader) Digest(ctx context.Context) ([]service.Proposal, error) {
193 0 return r.svc.DigestProposals(ctx, 0)
194 0 }
195
196 0 func (r serviceReader) DigestMark(ctx context.Context) (time.Time, bool, error) {
197 0 return r.svc.DigestMark(ctx)
198 0 }
199
200 0 func (r serviceReader) MarkDigestSeen(ctx context.Context, seenAt time.Time) error {
201 0 return r.svc.MarkDigestSeen(ctx, seenAt)
202 0 }