A platform is named once: WarpEngine::Platform

The list of platforms lived on `PlatformLink::SUPPORTED_PLATFORMS` — a model
about links to a platform's website — and was copied around it four times in
the admin (`%w[tic80 ebitengine love c64 godot bevy phaser]`), spelled out in
three apipie descriptions, and turned into a class name by string
interpolation in three services:

    "WarpEngine::Platforms::#{platform.camelize}::Service".constantize

Seven places that had to agree, and nothing that made them.

`WarpEngine::Platform` is now that one place. `Platform.names` is the list,
`Platform.find!("godot")` answers with a value object that knows its `label`,
its `expected_kinds` and its updater `service`, and the constantize is gone —
the registry holds the reference. `PublishService` and `BuildsService` ask it,
the admin selects read `Platform.names`, and
`PlatformLink::SUPPORTED_PLATFORMS` stays as an alias of `Platform::NAMES` so
a host pinned to 0.7 keeps working.

`Software` also defends its own value sets now. It validated neither `status`
nor `platform`, so a mistyped platform only surfaced later, at publish time,
as "Unsupported platform" — after the row existed. And `status` had three
different answers depending on where you looked: the admin offered
development/demo/released/archived, the API documentation claimed
"active, inactive", and the database holds the first four. `Software::STATUSES`
is the list, the inclusion validations enforce both, and the documentation
names the values the database actually has.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-23 09:00:55 +02:00
co-authored by Claude Opus 5
parent 19d142ef64
commit f1ad80ed0c
10 changed files with 86 additions and 33 deletions
+1
View File
@@ -5,6 +5,7 @@ require "apipie-rails"
require "warp_engine/version"
require "warp_engine/configuration"
require "warp_engine/platform"
require "warp_engine/storage"
require "warp_engine/access"
require "warp_engine/images"
+48
View File
@@ -0,0 +1,48 @@
module WarpEngine
class Platform
NAMES = %w[tic80 ebitengine love c64 godot bevy phaser].freeze
class << self
def names = NAMES
def all = NAMES.map { |name| new(name) }
def exists?(name) = NAMES.include?(name.to_s)
def find(name)
return nil unless exists?(name)
new(name.to_s)
end
def find!(name)
find(name) || raise(ArgumentError, "Unsupported platform: #{name}")
end
end
attr_reader :name
def initialize(name)
@name = name.to_s
end
def service_class
"WarpEngine::Platforms::#{name.camelize}::Service".constantize
end
def service = service_class.new
def label = service_class.label
def expected_kinds = service_class.expected_kinds
def to_s = name
def ==(other)
other.is_a?(Platform) ? name == other.name : name == other.to_s
end
alias eql? ==
def hash = name.hash
end
end