coverage~bigbes/sr-ht-doltede3b0bbweb/views.go

Coverage
84.6% 11/13 statements
Δ
Blob
705d3e6
Uncovered L64-L66
1 package web
2
3 import (
4 "context"
5 "net/url"
6
7 "sourcecraft.dev/bigbes/sr-ht-dolt/browse"
8 "sourcecraft.dev/bigbes/sr-ht-dolt/core"
9 )
10
11 // View is a specialized, read-only rendering of a repository whose table shape
12 // it recognizes. Views are registered at init time via RegisterView and chosen
13 // per-repo by Applies. The generic table browser is always available too, so a
14 // View never has to be exhaustive.
15 //
16 // A View pulls all of its data through the BrowseSession surface only (Tables,
17 // Rows, Branches, Log, ...); there is no SQL engine behind a bare store. Build
18 // returns an opaque value handed to the view's Template as its .Data field.
19 type View interface {
20 Name() string // URL slug, e.g. "beads"; must be a valid path segment, unique
21 Label() string // human tab label, e.g. "Beads"
22 Template() string // page template filename in templates/, e.g. "beads.html"
23 Applies(tables []browse.TableInfo) bool // fingerprint by table names/columns
24 // Build produces the opaque .Data value for the view's Template. query is the
25 // request URL's query string, so a view can offer sub-modes (e.g. beads'
26 // ?issue=<id> detail pane) without a new route.
27 Build(ctx context.Context, sess BrowseSession, repo *core.Repo, ref string, query url.Values) (any, error)
28 }
29
30 // registeredViews is the package-global registry populated at init time by
31 // RegisterView. It is read once per process: Register snapshots it into
32 // app.views so handlers (and tests, which set app.views directly) never touch
33 // the global at request time.
34 //
35 // Nothing here parses a view's Template() any more: pages discovers every page
36 // in templates/, so a view's page is registered by its file existing. A view
37 // whose Template() names no such file renders as a 500 with a log line, which
38 // is what it is.
39 var registeredViews []View
40
41 // init registers every view this service ships, in the order their tabs appear.
42 //
43 // The registration lives here, in one list, rather than in an init() beside each
44 // concrete view — which is where it used to be, and which made the tab order a
45 // consequence of file names. A package's init functions run in lexical file
46 // order, so "memory.go" registering itself would have put Memory between Beads
47 // and Milestones, and the only way to restore the intended order from inside
48 // memory.go was to register a view belonging to another file first. Tab order is
49 // a decision, and a decision that reads as a list of three lines cannot be
50 // changed by renaming a file.
51 1 func init() {
52 1 RegisterView(&beadsView{})
53 1 RegisterView(&milestonesView{})
54 1 RegisterView(&memoryView{})
55 1 }
56
57 // RegisterView adds v to the global registry, appending it after everything
58 // registered before it: registration order is tab order. If a view with the same
59 // Name() is already registered it is replaced in place, so re-registering (a
60 // test swapping an implementation, say) never reorders the bar.
61 3 func RegisterView(v View) {
62 3 for i, existing := range registeredViews {
63 3 if existing.Name() == v.Name() {
64 0 registeredViews[i] = v
65 0 return
66 0 }
67 }
68 3 registeredViews = append(registeredViews, v)
69 }
70
71 // applicableViews returns the subset of views whose Applies reports true for
72 // tables, preserving registration order. It is pure: handlers pass their own
73 // snapshot (app.views) so the result is deterministic and testable.
74 73 func applicableViews(views []View, tables []browse.TableInfo) []View {
75 73 out := make([]View, 0, len(views))
76 107 for _, v := range views {
77 107 if v.Applies(tables) {
78 94 out = append(out, v)
79 94 }
80 }
81 73 return out
82 }