A store registry record needs no repository
The client only ever took identity from a store repository — a slug, a name, a catalog — and the store engine's own defaults cover everything else: the host-to-asset mapping, the install modes, the platforms, the behaviour. So `store_repository_url` is now optional: nullable in the schema, no presence validation, the format check only when a value is given, and the serializer answers null rather than an empty string, because the client branches on its absence. Adding a store is therefore a row with two fields filled in. Given a repository the client still reads its config.json, and that file remains the authority on how the store behaves — the admin form and the endpoint's documentation say so. The frontend's /stores page gains a section of its own for the graphical client on the desktop tab: what it does, that it sets the store up itself, that it is the way in on Windows where `curl … | sh` does not exist, and links to the releases, the repository and the documentation — now under stores/warp-engine-client, which is where that repository lives after the rename. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -35,8 +35,10 @@ ActiveAdmin.register Store do
|
||||
row :updated_at
|
||||
end
|
||||
para do
|
||||
"Listed by GET /api/stores, which the desktop client reads on first run: it " \
|
||||
"takes the config from the store repository and points it at the catalog above."
|
||||
"Listed by GET /api/stores, which the desktop client reads on first run. With a " \
|
||||
"store repository the client takes that config and points it at the catalog " \
|
||||
"above; without one it uses the store engine's defaults, which need nothing " \
|
||||
"but the name and the catalog."
|
||||
end
|
||||
end
|
||||
|
||||
@@ -45,8 +47,10 @@ ActiveAdmin.register Store do
|
||||
f.input :name, hint: "What the client shows in its store picker"
|
||||
f.input :catalog_url, hint: "Base URL of the WarpEngine catalog, e.g. https://teletypegames.org"
|
||||
f.input :store_repository_url,
|
||||
hint: "Repository holding the store's config.json, e.g. " \
|
||||
"https://git.teletypegames.org/stores/ttg-desktop-store"
|
||||
hint: "Optional. A repository holding the store's config.json, e.g. " \
|
||||
"https://git.teletypegames.org/stores/ttg-desktop-store. Leave it " \
|
||||
"empty and the client uses the engine's defaults with the name and " \
|
||||
"catalog above"
|
||||
end
|
||||
f.actions
|
||||
end
|
||||
|
||||
@@ -6,13 +6,19 @@ class Api::StoresController < ApiController
|
||||
api :GET, "/api/stores", "List the stores a client can install from"
|
||||
desc <<~DESC
|
||||
The registry the graphical desktop client reads on first run: which catalogs
|
||||
exist, and where each one's store configuration lives. Public on purpose —
|
||||
a client has nobody to log in as.
|
||||
exist, and — where there is one — the repository holding a store's own
|
||||
configuration. Public on purpose: a client has nobody to log in as.
|
||||
|
||||
A record needs a name and a catalog URL. `storeRepositoryUrl` is optional and
|
||||
answers `null` when there is none; a client then takes identity from this
|
||||
record and everything else from the store engine's built-in defaults, so no
|
||||
repository has to exist for a store to be installable.
|
||||
DESC
|
||||
returns code: 200, desc: "Array of stores" do
|
||||
property :name, String, desc: "Display name of the store"
|
||||
property :catalogUrl, String, desc: "Base URL of the WarpEngine catalog it serves"
|
||||
property :storeRepositoryUrl, String, desc: "Repository holding the store's config.json"
|
||||
property :storeRepositoryUrl, [String, nil],
|
||||
desc: "Repository holding the store's config.json, or null when it has none"
|
||||
end
|
||||
def index
|
||||
render json: StoreService.new.index
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
# A store a client can install from: a WarpEngine catalog, and the repository
|
||||
# holding the store's own configuration.
|
||||
# A store a client can install from: a WarpEngine catalog, and — optionally — a
|
||||
# repository holding that store's own configuration.
|
||||
#
|
||||
# This is deliberately not part of WarpEngine. The engine serves one catalog and
|
||||
# has no business knowing which stores exist for it; the registry is a property of
|
||||
# this site, which is what the graphical client asks.
|
||||
#
|
||||
# The repository is optional because a store does not need one. A client takes
|
||||
# identity from this record — the name, the catalog and a slug derived from it —
|
||||
# and everything else from the store engine's built-in defaults. A repository is
|
||||
# still honoured when given: it remains the authority on how that store behaves,
|
||||
# which platforms it offers and where things land.
|
||||
class Store < ApplicationRecord
|
||||
URL = %r{\Ahttps?://\S+\z}
|
||||
|
||||
validates :name, presence: true
|
||||
validates :catalog_url, presence: true, format: { with: URL, message: "must be an http(s) URL" }
|
||||
validates :store_repository_url, presence: true, format: { with: URL, message: "must be an http(s) URL" }
|
||||
validates :store_repository_url, format: { with: URL, message: "must be an http(s) URL" },
|
||||
allow_blank: true
|
||||
|
||||
default_scope { where(deleted_at: nil) }
|
||||
|
||||
|
||||
@@ -5,5 +5,7 @@ class StoreSerializer < Blueprinter::Base
|
||||
# camelCase, as the catalog's own payloads use — one convention for a client
|
||||
# that reads both.
|
||||
field(:catalogUrl) { |store| store.catalog_url }
|
||||
field(:storeRepositoryUrl) { |store| store.store_repository_url }
|
||||
# Null rather than an empty string when there is no repository: the client
|
||||
# branches on its absence, and "" is not an absence a JSON reader can trust.
|
||||
field(:storeRepositoryUrl) { |store| store.store_repository_url.presence }
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user