Migration
This is my new public git source viewer. Source coming soon. Please contact with concerns/bugs.
Contact me
internal/api/api.go
554bcfc63721b22faf8bca78d5518d76e24a4d20
· 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 | } |