package core import ( "bytes" "encoding/json" "fmt" "html/template" "net/http" ) // TemplateSet holds one independently-parsed template tree per view. // Views are parsed separately because html/template has a single namespace // for defined blocks -- two views both defining "main" would collide. type TemplateSet map[string]*template.Template // Renderer turns a Ctx into bytes plus a content type. HTML pages and JSON // endpoints are the same shape of thing; this is where they unify. type Renderer interface { Render(c *Ctx, ts TemplateSet) ([]byte, error) ContentType() string } // View renders an HTML template. The template calls modules via `mod`. type View struct{ Template string } func (v View) ContentType() string { return "text/html; charset=utf-8" } func (v View) Render(c *Ctx, ts TemplateSet) ([]byte, error) { t, ok := ts[v.Template] if !ok { return nil, fmt.Errorf("no such view %q", v.Template) } // Clone per render so the funcmap can close over this Ctx. tt, err := t.Clone() if err != nil { return nil, err } tt = tt.Funcs(FuncMap(c)) var buf bytes.Buffer if err := tt.ExecuteTemplate(&buf, v.Template, c); err != nil { return nil, fmt.Errorf("view %s: %w", v.Template, err) } return buf.Bytes(), nil } // Endpoint builds the value that will be serialized. It must return a // purpose-built DTO, never a raw module value -- see internal/api. type Endpoint func(c *Ctx) (any, error) // JSON renders an API endpoint. type JSON struct{ Build Endpoint } func (j JSON) ContentType() string { return "application/json; charset=utf-8" } func (j JSON) Render(c *Ctx, _ TemplateSet) ([]byte, error) { v, err := j.Build(c) if err != nil { return nil, err } b, err := json.MarshalIndent(v, "", " ") if err != nil { return nil, err } return append(b, '\n'), nil } // ------------------------------------------------------------------ routes // Route binds a renderer to a net/http pattern (Go 1.22 wildcards). // // /{repo}/tree/{rest...} -> r.PathValue("repo"), r.PathValue("rest") // // Scope decides how the Ctx is built before Render runs. type Route struct { Name string Pattern string // e.g. "GET /{repo}/commit/{rev}" Render Renderer Scope Scope // Immutable marks responses whose content cannot change for a given // URL -- anything addressed by a commit SHA. Those get a year-long // Cache-Control; ref-addressed URLs get revalidation instead. Immutable bool } type Scope int const ( ScopeRepo Scope = iota // resolves {repo}, and {rest...}/{rev} if present ScopeSite // no repository in the URL ) // httpError lets a module or endpoint choose the status code. type httpError struct { Code int Err error } func (e httpError) Error() string { return e.Err.Error() } func (e httpError) Unwrap() error { return e.Err } func Errorf(code int, format string, a ...any) error { return httpError{Code: code, Err: fmt.Errorf(format, a...)} } func statusOf(err error) int { var he httpError if ok := asHTTPError(err, &he); ok { return he.Code } return http.StatusInternalServerError } // HeaderSetter lets a renderer add response headers. Optional: the server // type-asserts for it after Render succeeds. type HeaderSetter interface { Headers(c *Ctx, h http.Header) } // Raw serves file bytes straight out of the object database. // // SECURITY: a repository can contain an .html file with a