Migration This is my new public git source viewer. Source coming soon. Please contact with concerns/bugs. Contact me

internal/api/api.go

4bda28df3ab3dbfcc789dcbbb184cabc8cac9398 · 3.5 KB · 133 lines raw

1 // Package api defines the JSON surface.
2 //
3 // DESIGN RULE: endpoints return purpose-built DTOs with explicit json
4 // tags. They never marshal a module's return value directly. Go marshals
5 // every exported field by default, so serializing a module value means one
6 // added field silently becomes public. The allowlist lives here, in types
7 // you can read in one screen, not in struct tags scattered across modules.
8 package api
9
10 import (
11 "fmt"
12 "time"
13
14 "gsg/internal/core"
15 "gsg/internal/gitx"
16 "gsg/internal/modules"
17 )
18
19 // Repo is the per-repository document. Deliberately minimal.
20 type Repo struct {
21 Slug string `json:"slug"`
22 Title string `json:"title"`
23 Description string `json:"description,omitempty"`
24 Owner string `json:"owner,omitempty"`
25 Section string `json:"section,omitempty"`
26 CloneURL string `json:"clone_url,omitempty"`
27 DefaultBranch string `json:"default_branch,omitempty"`
28 LastCommit *string `json:"last_commit,omitempty"` // RFC3339, null when empty repo
29 }
30
31 func repoDoc(info core.RepoInfo, defaultRef string, last time.Time) Repo {
32 d := Repo{
33 Slug: info.Slug,
34 Title: info.Title,
35 Description: info.Description,
36 Owner: info.Owner,
37 Section: info.Section,
38 CloneURL: info.Clone,
39 DefaultBranch: defaultRef,
40 }
41 if !last.IsZero() {
42 s := last.UTC().Format(time.RFC3339)
43 d.LastCommit = &s
44 }
45 return d
46 }
47
48 // RepoEndpoint -> /{repo}/api/repo.json
49 func RepoEndpoint(c *core.Ctx) (any, error) {
50 v, err := c.Mod("meta")
51 if err != nil {
52 return nil, err
53 }
54 m, ok := v.(modules.Meta)
55 if !ok {
56 return nil, fmt.Errorf("api: meta returned %T", v)
57 }
58 return repoDoc(m.RepoInfo, m.DefaultRef, m.LastCommit), nil
59 }
60
61 // Commit is the per-commit document.
62 //
63 // NOTE: no Subject, no Body. You said commit messages stay out of the JSON
64 // surface, and because this is an explicit DTO rather than a marshalled
65 // module value, that decision is enforced by the type -- adding a field to
66 // gitx.Commit cannot leak it here.
67 type Commit struct {
68 SHA string `json:"sha"`
69 Short string `json:"short"`
70 When string `json:"authored"`
71 Parents []string `json:"parents,omitempty"`
72 Files int `json:"files_changed"`
73 Added int `json:"lines_added"`
74 Deleted int `json:"lines_deleted"`
75 }
76
77 // CommitEndpoint -> /{repo}/api/commit/{sha}.json
78 func CommitEndpoint(c *core.Ctx) (any, error) {
79 cv, err := c.Mod("commit")
80 if err != nil {
81 return nil, err
82 }
83 cm, ok := cv.(gitx.Commit)
84 if !ok {
85 return nil, fmt.Errorf("api: commit returned %T", cv)
86 }
87 doc := Commit{
88 SHA: cm.SHA,
89 Short: cm.Short,
90 When: cm.When.UTC().Format(time.RFC3339),
91 Parents: cm.Parents,
92 }
93 dv, err := c.Mod("diff")
94 if err != nil {
95 return nil, err
96 }
97 if d, ok := dv.(gitx.Diff); ok {
98 doc.Files = len(d.Stats)
99 for _, f := range d.Stats {
100 doc.Added += f.Added
101 doc.Deleted += f.Deleted
102 }
103 }
104 return doc, nil
105 }
106
107 // Index is the site-wide document.
108 type Index struct {
109 Site string `json:"site"`
110 Generated string `json:"generated"`
111 Repos []Repo `json:"repos"`
112 }
113
114 // IndexEndpoint -> /api/repos.json
115 func IndexEndpoint(c *core.Ctx) (any, error) {
116 v, err := c.Mod("repos")
117 if err != nil {
118 return nil, err
119 }
120 list, ok := v.([]modules.RepoSummary)
121 if !ok {
122 return nil, fmt.Errorf("api: repos returned %T", v)
123 }
124 idx := Index{
125 Site: c.Site.Name,
126 Generated: time.Now().UTC().Format(time.RFC3339),
127 Repos: make([]Repo, 0, len(list)),
128 }
129 for _, s := range list {
130 idx.Repos = append(idx.Repos, repoDoc(s.RepoInfo, "", s.LastCommit))
131 }
132 return idx, nil
133 }