Skip to content
quarry Artifact [a8e0ee8ee5]
Login

Artifact [a8e0ee8ee5]

Artifact a8e0ee8ee5d0891b5f7f89b2adb8386363f6f137dde76771f2a8eefe64c6cbba:


// Package api owns RegisterAll — the one explicit wiring point where every
// op joins the registry (0003 §2: no init() magic; the wiring stays
// greppable) — plus the HTTP router that serves the registered ops. This
// is the sanctioned place Run closures over the data planes are built;
// the arch gate keeps every other consumer on the op side of the seam.
package api

import (
	"context"
	"fmt"
	"html"

	"craftdesign.group/quarry/internal/index"
	"craftdesign.group/quarry/internal/markup"
	"craftdesign.group/quarry/internal/ops"
)

// Deps carries what op implementations close over. Index may be nil (no
// store configured — `quarry spec` runs this way); ops that need it
// answer with an error at run time, and the spec stays complete either
// way.
type Deps struct {
	Version string
	Index   *index.Index
}

type versionIn struct{}

type versionOut struct {
	Version string `json:"version"`
}

type timelineIn struct {
	Limit int `json:"limit" validate:"min=0,max=200"`
}

type timelineOut struct {
	Entries []index.TimelineEntry `json:"entries"`
	Count   int                   `json:"count"`
}

type wikiListIn struct{}

type wikiListOut struct {
	Pages []index.WikiTitle `json:"pages"`
	Count int               `json:"count"`
}

type wikiGetIn struct {
	Name string `json:"name" validate:"required,max=100"`
}

type wikiGetOut struct {
	Title    string `json:"title"`
	Date     string `json:"date"`
	User     string `json:"user"`
	Mimetype string `json:"mimetype"`
	Source   string `json:"source"`
	HTML     string `json:"html"`
}

// resolver implements markup.Resolver over the index. Wiki names resolve
// to existing pages; hash resolution stays empty until a check-in page
// exists to link to — an unresolvable reference renders as plain text,
// never as a dead link.
type resolver struct{ ix *index.Index }

func (r resolver) ResolveHash(string) string { return "" }

func (r resolver) ResolveWiki(name string) string {
	if r.ix == nil {
		return ""
	}
	if _, ok, err := r.ix.WikiPage(name); err == nil && ok {
		return "/wiki/" + name
	}
	return ""
}

// RegisterAll wires every op.
func RegisterAll(r *ops.Registry, d Deps) error {
	if err := ops.Register(r, ops.Op[versionIn, versionOut]{
		Name:  "meta.version",
		Doc:   "Report the quarry build version.",
		Kind:  ops.Query,
		Auth:  ops.Anonymous,
		Route: "GET /api/v1/version",
		Run: func(_ context.Context, _ versionIn) (versionOut, error) {
			return versionOut{Version: d.Version}, nil
		},
	}); err != nil {
		return err
	}
	if err := ops.Register(r, ops.Op[timelineIn, timelineOut]{
		Name:  "timeline.list",
		Doc:   "List check-ins, newest first.",
		Kind:  ops.Query,
		Auth:  ops.Anonymous,
		Route: "GET /api/v1/timeline",
		Run: func(_ context.Context, in timelineIn) (timelineOut, error) {
			if d.Index == nil {
				return timelineOut{}, fmt.Errorf("no store is being served")
			}
			limit := in.Limit
			if limit == 0 {
				limit = 50
			}
			entries, err := d.Index.Timeline(limit)
			if err != nil {
				return timelineOut{}, err
			}
			return timelineOut{Entries: entries, Count: len(entries)}, nil
		},
	}); err != nil {
		return err
	}
	if err := ops.Register(r, ops.Op[wikiListIn, wikiListOut]{
		Name:  "wiki.list",
		Doc:   "List wiki pages with their latest edit.",
		Kind:  ops.Query,
		Auth:  ops.Anonymous,
		Route: "GET /api/v1/wiki",
		Run: func(_ context.Context, _ wikiListIn) (wikiListOut, error) {
			if d.Index == nil {
				return wikiListOut{}, fmt.Errorf("no store is being served")
			}
			pages, err := d.Index.WikiTitles()
			if err != nil {
				return wikiListOut{}, err
			}
			return wikiListOut{Pages: pages, Count: len(pages)}, nil
		},
	}); err != nil {
		return err
	}
	return ops.Register(r, ops.Op[wikiGetIn, wikiGetOut]{
		Name:  "wiki.get",
		Doc:   "Fetch one wiki page, rendered.",
		Kind:  ops.Query,
		Auth:  ops.Anonymous,
		Route: "GET /api/v1/wiki/{name}",
		Run: func(_ context.Context, in wikiGetIn) (wikiGetOut, error) {
			if d.Index == nil {
				return wikiGetOut{}, fmt.Errorf("no store is being served")
			}
			page, ok, err := d.Index.WikiPage(in.Name)
			if err != nil {
				return wikiGetOut{}, err
			}
			if !ok {
				return wikiGetOut{}, fmt.Errorf("no wiki page %q", in.Name)
			}
			var rendered []byte
			switch page.Mimetype {
			case "", "text/x-markdown", "text/markdown":
				rendered, err = markup.Render(page.Body, resolver{ix: d.Index})
				if err != nil {
					return wikiGetOut{}, err
				}
			default:
				// Non-markdown wiki content renders as escaped text.
				rendered = []byte("<pre>" + html.EscapeString(string(page.Body)) + "</pre>")
			}
			return wikiGetOut{
				Title: page.Title, Date: page.Date, User: page.User,
				Mimetype: page.Mimetype, Source: string(page.Body),
				HTML: string(rendered),
			}, nil
		},
	})
}