Phase 6: polish — apipie matcher, install generator, engine migrations, docs

- host apipie matcher now globs the engine controllers, so /api/docs and
  /api/swagger keep documenting the catalog endpoints
- rails g warp_engine:install: initializer template + a clean
  create_warp_engine_tables migration (signed bigint PKs) for new hosts;
  TTG never runs it
- engine append_migrations initializer: future catalog migrations in the
  engine's db/migrate run via the host's rails db:migrate
- README documents the updater contract, config surface, host expectations
  (admin JS picker, apipie matcher) and the soft-delete/resurrection behavior
- make api-test runs both suites plus a production zeitwerk:check

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-04 19:21:40 +02:00
co-authored by Claude Fable 5
parent d2b4b67e44
commit 4d1e04afdf
7 changed files with 230 additions and 19 deletions
+6
View File
@@ -18,3 +18,9 @@ stop:
$(call docker_compose,down --remove-orphans)
restart: stop start
# Teljes API-teszt: host suite + warp_engine suite + production eager-load check
api-test:
docker exec -e RAILS_ENV=test api bundle exec rspec
docker exec -w /libs/ruby/warp_engine -e RAILS_ENV=test api bundle exec rspec
docker exec -e RAILS_ENV=production -e SECRET_KEY_BASE=zeitwerkcheck api bundle exec rails zeitwerk:check
+1 -2
View File
@@ -4,8 +4,7 @@ Apipie.configure do |config|
config.doc_base_url = "/api/docs"
config.api_controllers_matcher = [
"#{Rails.root}/app/controllers/api/**/*.rb",
"#{Rails.root}/app/controllers/update_controller.rb",
"#{Rails.root}/app/controllers/files_controller.rb"
"#{WarpEngine::Engine.root}/app/controllers/**/*.rb"
]
config.validate = false
config.translate = false
+65 -17
View File
@@ -3,32 +3,80 @@
Mountable Rails engine: retro szoftverkatalógus CI-pipeline-ból hívható release-updaterrel,
publikus read-only JSON API-val és a host adminjába betöltődő ActiveAdmin erőforrásokkal.
## Használat
## Mit ad
- **Modellek**: `Software`, `Release`, `ReleaseAsset`, `ExternalLink`, `PlatformLink`,
`Image`, `SoftwareImage`, `Download` (mind `WarpEngine::` alatt, prefix nélküli táblákkal)
- **Updater**: `GET /update?platform=&name=&version=` (`X-Update-Secret` fejléc vagy
`?secret=`) — a CI a build-artifactokat a `file_container_path` alá másolja
(`<name>-<version>*` konvencióval), majd meghívja az endpointot; az updater kicsomagol,
metadatát parse-ol, és upserteli a Software/Release/ReleaseAsset/ExternalLink rekordokat.
Támogatott platformok: tic80, ebitengine, love, c64, godot, bevy, phaser.
- **Publikus API**: `/api/software`, `/api/software/highlighted`, `/api/builds`,
`/api/softwares/:name/builds`, `/api/image/:id`, `/api/download?path=`, `/file/*path`
- **Admin**: ActiveAdmin resource-fájlok (softwares a 3 szintű beágyazott formmal,
releases, external links, platform links, images orphan-kezeléssel, Files fájlkezelő
picker móddal, download stats) — a host ActiveAdmin példányába töltődnek be.
## Telepítés
```ruby
# Gemfile
gem "warp_engine", path: "../../libs/ruby/warp_engine"
```
```sh
rails g warp_engine:install # initializer + create_warp_engine_tables migráció
rails db:migrate
```
```ruby
# config/routes.rb — utolsó sorként, hogy a host route-jai nyerjenek
mount WarpEngine::Engine => "/"
```
# config/initializers/warp_engine.rb
WarpEngine.configure do |c|
c.file_container_path = ENV.fetch("FILE_CONTAINER_PATH", "/softwares")
c.image_container_path = ENV.fetch("IMAGE_CONTAINER_PATH", "/images")
c.update_secret = ENV["UPDATE_SECRET"]
# Ha a host modelljei is használnak katalógus-képeket:
c.image_owners << {
label: "member",
image_ids: -> { Member.where.not(image_id: nil).distinct.pluck(:image_id) },
usage_label: ->(image) { "member" if Member.where(image_id: image.id).exists? }
}
## Konfiguráció
```ruby
Rails.application.config.to_prepare do
WarpEngine.configure do |c|
c.file_container_path = ENV.fetch("FILE_CONTAINER_PATH", "/softwares")
c.image_container_path = ENV.fetch("IMAGE_CONTAINER_PATH", "/images")
c.update_secret = ENV["UPDATE_SECRET"] # nil => /update mindig elutasít
# Ha a host modelljei is használnak katalógus-képeket:
c.image_owners = [
{
label: "member",
image_ids: -> { Member.where.not(image_id: nil).distinct.pluck(:image_id) },
usage_label: ->(image) { "member" if Member.where(image_id: image.id).exists? }
}
]
end
end
```
## Megjegyzések
## Host-elvárások
- A modellek soft-delete-et használnak (`default_scope { where(deleted_at: nil) }`);
az updater `.unscoped`-dal "feltámasztja" az újra beküldött, korábban törölt rekordokat.
- A táblanevek prefix nélküliek (`softwares`, `releases`, ...).
- Az admin erőforrások a host ActiveAdmin példányába töltődnek be; auth (Devise) a host dolga.
- **ActiveAdmin + Devise a hostban él**: auth, téma, assetek és a `/admin` route-ok a
host dolga; az engine csak resource-fájlokat ad a `ActiveAdmin.application.load_paths`-hoz.
- **Files picker JS**: a release-asset path-mezők melletti fájlkiválasztó a host
`active_admin.js`-ében élő pár soros JS-re támaszkodik (iframe a
`/admin/files?picker=1&field=<dom_id>` címre) — új hostba ezt is át kell venni.
- **apipie**: ha a host apipie-dokut generál, vegye fel a matcherbe:
`"#{WarpEngine::Engine.root}/app/controllers/**/*.rb"`.
## Viselkedési megjegyzések
- Minden modell soft-delete-es (`default_scope { where(deleted_at: nil) }`); az updater
`.unscoped`-dal "feltámasztja" az újra beküldött, korábban törölt rekordokat.
- A JSON-formátum szándékosan bug-kompatibilis az egykori Go backenddel
(Go zero-time timestampek, camelCase kulcsok, legacy flat path mezők).
- Modell-kiterjesztési pontok: `ActiveSupport.on_load(:warp_engine_<model>)` hookok.
## Tesztek
```sh
bundle install
bundle exec rake app:db:prepare RAILS_ENV=test # warp_engine_test DB a dummy apphoz
bundle exec rspec
```
@@ -0,0 +1,35 @@
require "rails/generators"
require "rails/generators/migration"
require "rails/generators/active_record"
module WarpEngine
module Generators
class InstallGenerator < Rails::Generators::Base
include Rails::Generators::Migration
source_root File.expand_path("templates", __dir__)
def self.next_migration_number(dirname)
ActiveRecord::Generators::Base.next_migration_number(dirname)
end
def copy_initializer
template "initializer.rb", "config/initializers/warp_engine.rb"
end
def copy_migration
migration_template "create_warp_engine_tables.rb", "db/migrate/create_warp_engine_tables.rb"
end
def show_readme
say <<~MSG
WarpEngine telepítve. Következő lépések:
1. rails db:migrate
2. mount WarpEngine::Engine => "/" a config/routes.rb végére
3. állítsd be a config/initializers/warp_engine.rb-t
MSG
end
end
end
end
@@ -0,0 +1,91 @@
class CreateWarpEngineTables < ActiveRecord::Migration[8.0]
def change
create_table :softwares do |t|
t.string :name, limit: 128
t.string :title
t.string :author
t.text :desc
t.text :story
t.string :license, limit: 128
t.string :platform, limit: 128
t.string :site
t.string :status, limit: 20, default: "development"
t.boolean :highlighted, default: false
t.datetime :deleted_at, precision: 3
t.timestamps precision: 3, null: true
t.index :name, unique: true
t.index :deleted_at
end
create_table :releases do |t|
t.references :software, foreign_key: { on_delete: :cascade }
t.string :version, limit: 64
t.datetime :deleted_at, precision: 3
t.timestamps precision: 3, null: true
t.index :deleted_at
end
create_table :release_assets do |t|
t.references :release, null: false, foreign_key: { on_delete: :cascade }
t.string :kind, limit: 32, null: false
t.string :path, null: false
t.datetime :deleted_at, precision: 3
t.timestamps precision: 3
t.index [ :release_id, :kind ], unique: true
t.index :path
t.index :deleted_at
end
create_table :external_links do |t|
t.references :software, foreign_key: { on_delete: :cascade }
t.string :label, limit: 128
t.string :url
t.datetime :deleted_at, precision: 3
t.timestamps precision: 3, null: true
t.index :deleted_at
end
create_table :platform_links do |t|
t.string :name, limit: 128
t.string :platform, limit: 128
t.string :url
t.integer :position, default: 0, null: false
t.datetime :deleted_at, precision: 3
t.timestamps precision: 3, null: true
t.index :platform
t.index :deleted_at
end
create_table :images do |t|
t.string :filename, null: false
t.string :original_filename, null: false
t.string :content_type, default: "application/octet-stream", null: false
t.datetime :deleted_at, precision: 3
t.timestamps
t.index :deleted_at
end
create_table :software_images do |t|
t.references :software, null: false, foreign_key: { on_delete: :cascade }
t.references :image, null: false, foreign_key: true, index: false
t.boolean :is_default, default: false, null: false
t.integer :position, default: 0, null: false
t.timestamps
t.index [ :software_id, :image_id ], unique: true
t.index [ :software_id, :position ]
end
create_table :downloads do |t|
t.string :file_path, null: false
t.references :release, foreign_key: { on_delete: :nullify }, index: false
t.string :ip_address
t.string :user_agent, limit: 500
t.string :referer, limit: 500
t.datetime :deleted_at, precision: 3
t.timestamps precision: 3, null: true
t.index :file_path
t.index :release_id
t.index :deleted_at
end
end
end
@@ -0,0 +1,22 @@
Rails.application.config.to_prepare do
WarpEngine.configure do |c|
# A build-artifactok és képek tárolási helye (defaultok az env-ből:
# FILE_CONTAINER_PATH ill. IMAGE_CONTAINER_PATH).
# c.file_container_path = "/softwares"
# c.image_container_path = "/images"
# A /update endpoint shared secretje (default: ENV["UPDATE_SECRET"]).
# Beállítatlan secret esetén az endpoint minden kérést elutasít.
# c.update_secret = ENV["UPDATE_SECRET"]
# Ha a host modelljei is hivatkoznak katalógus-képekre, regisztráld őket,
# hogy az admin Images oldal orphan-detektálása figyelembe vegye:
# c.image_owners = [
# {
# label: "member",
# image_ids: -> { Member.where.not(image_id: nil).distinct.pluck(:image_id) },
# usage_label: ->(image) { "member" if Member.where(image_id: image.id).exists? }
# }
# ]
end
end
@@ -8,6 +8,16 @@ module WarpEngine
Rails.autoloaders.main.ignore(Engine.root.join("app/admin"))
end
# A jövőbeli katalógus-migrációk az engine db/migrate-jéből futnak a host
# rails db:migrate-jével, másolás nélkül.
initializer "warp_engine.append_migrations" do |app|
unless app.root.to_s.start_with?(root.to_s)
config.paths["db/migrate"].expanded.each do |path|
app.config.paths["db/migrate"] << path
end
end
end
# Az admin erőforrásokat a HOST ActiveAdmin példánya tölti be; az engine csak
# regisztrálja a saját app/admin könyvtárát. Auth/téma/route-ok a host dolga.
initializer "warp_engine.active_admin" do |app|