Files
summercms/modules/kiosk/registry.go
Jakub Zych 4916ee3d43 feat(kiosk): registry v1 wire types and version rules
- RegistryAPIPath, RegistryPlugin, RegistryPluginDetail, RegistryVersion, envelopes, errors, ingest types
- ValidVersion, CompareVersions, BaseVersion, Satisfies, NewestCompatible
- docs: Registry API v1 reference in plugins/marketplace.md
2026-10-29 17:05:41 +01:00

134 lines
4.2 KiB
Go

package kiosk
import (
"encoding/json"
"time"
)
// The registry API v1 contract. The summercms.io registry encodes its
// responses with these types and the summer tool and the admin decode
// them, so no other copy of the shape exists. Rules: every response is an
// envelope object (data, meta or error); clients ignore unknown fields;
// changes within v1 are additive only. The API is GET-only for clients,
// needs no credentials and answers with open CORS and
// Cache-Control: public, max-age=300 plus an ETag.
// RegistryAPIPath is the base path of the registry API v1:
//
// GET {base}/plugins?q=&category= ListResponse
// GET {base}/plugins/{id} DetailResponse
// GET {base}/plugins/{id}/versions VersionsResponse, newest first
// POST {base}/ingest IngestRequest -> 202 IngestResponse (authenticated)
//
// Errors answer with an ErrorResponse: 404 not_found, 400 invalid_query.
const RegistryAPIPath = "/api/registry/v1"
// Error codes of a RegistryError.
const (
// RegistryErrorNotFound is the code of a 404: no such plugin.
RegistryErrorNotFound = "not_found"
// RegistryErrorInvalidQuery is the code of a 400: a bad query parameter.
RegistryErrorInvalidQuery = "invalid_query"
)
// RegistryPlugin is one plugin in the registry list.
type RegistryPlugin struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Module string `json:"module"`
Package string `json:"package"`
Repository string `json:"repository"`
Category string `json:"category"`
Featured bool `json:"featured"`
IconURL string `json:"icon_url"`
LatestVersion string `json:"latest_version"`
LatestFramework string `json:"latest_framework"`
UpdatedAt time.Time `json:"updated_at"`
}
// RegistryPluginDetail is one plugin with its README, screenshots and the
// requirements of its latest version.
type RegistryPluginDetail struct {
RegistryPlugin
ReadmeMarkdown string `json:"readme_markdown"`
ReadmeHTML string `json:"readme_html"`
Screenshots []RegistryScreenshot `json:"screenshots"`
Requires Requires `json:"requires"`
}
// RegistryScreenshot is one screenshot of a plugin listing.
type RegistryScreenshot struct {
URL string `json:"url"`
Caption string `json:"caption"`
}
// RegistryVersion is one tagged release of a plugin, with the minimums
// its plugin.yaml declared at that tag.
type RegistryVersion struct {
Version string `json:"version"`
Framework string `json:"framework"`
Requires Requires `json:"requires"`
Module string `json:"module"`
Package string `json:"package"`
Commit string `json:"commit"`
TaggedAt time.Time `json:"tagged_at"`
}
// ListResponse answers GET {base}/plugins.
type ListResponse struct {
Data []RegistryPlugin `json:"data"`
Meta ListMeta `json:"meta"`
}
// ListMeta carries the number of plugins that matched.
type ListMeta struct {
Total int `json:"total"`
}
// DetailResponse answers GET {base}/plugins/{id}.
type DetailResponse struct {
Data RegistryPluginDetail `json:"data"`
}
// VersionsResponse answers GET {base}/plugins/{id}/versions, newest first.
type VersionsResponse struct {
Data []RegistryVersion `json:"data"`
}
// ErrorResponse is the body of every registry error.
type ErrorResponse struct {
Error RegistryError `json:"error"`
}
// RegistryError is a stable code and a human-readable message.
type RegistryError struct {
Code string `json:"code"`
Message string `json:"message"`
}
// IngestRequest is the body of POST {base}/ingest.
type IngestRequest struct {
Repository string `json:"repository"`
}
// IngestResponse answers an accepted ingest request (202).
type IngestResponse struct {
Data IngestResult `json:"data"`
}
// IngestResult names the repository queued for ingestion.
type IngestResult struct {
Repository string `json:"repository"`
Queued bool `json:"queued"`
}
// MarshalJSON encodes the map as an object, {} when it is empty, so the
// registry never sends null for requires.
func (r Requires) MarshalJSON() ([]byte, error) {
if r == nil {
return []byte("{}"), nil
}
return json.Marshal(map[string]string(r))
}