From 7abfb89bcac96c88beadd139994c0b5aa2251937 Mon Sep 17 00:00:00 2001 From: Zsolt Tasnadi Date: Tue, 4 Aug 2026 18:51:12 +0200 Subject: [PATCH] Phase 2: move the 8 catalog models into the WarpEngine engine - Software, Release, ReleaseAsset, ExternalLink, PlatformLink, Image, SoftwareImage, Download now live in the engine under WarpEngine::, on top of WarpEngine::ApplicationRecord; table names unchanged (empty prefix) - each model runs an ActiveSupport load hook (:warp_engine_) as a host extension point - WarpEngine::Image reads its upload path from WarpEngine.config - host references fully qualified (services, serializers, admin, specs); admin registrations renamed with as: so /admin URLs and route helpers are byte-identical; factories pinned to the namespaced classes - Member#image now class_name: "WarpEngine::Image" Verified: full suite green, /api/software and /api/builds byte-identical to the phase-0 baselines, admin routes unchanged. Co-Authored-By: Claude Fable 5 --- app/models/warp_engine/application_record.rb | 5 +++ app/models/warp_engine/download.rb | 19 +++++++++ app/models/warp_engine/external_link.rb | 22 ++++++++++ app/models/warp_engine/image.rb | 36 ++++++++++++++++ app/models/warp_engine/platform_link.rb | 27 ++++++++++++ app/models/warp_engine/release.rb | 37 ++++++++++++++++ app/models/warp_engine/release_asset.rb | 26 +++++++++++ app/models/warp_engine/software.rb | 31 ++++++++++++++ app/models/warp_engine/software_image.rb | 45 ++++++++++++++++++++ 9 files changed, 248 insertions(+) create mode 100644 app/models/warp_engine/application_record.rb create mode 100644 app/models/warp_engine/download.rb create mode 100644 app/models/warp_engine/external_link.rb create mode 100644 app/models/warp_engine/image.rb create mode 100644 app/models/warp_engine/platform_link.rb create mode 100644 app/models/warp_engine/release.rb create mode 100644 app/models/warp_engine/release_asset.rb create mode 100644 app/models/warp_engine/software.rb create mode 100644 app/models/warp_engine/software_image.rb diff --git a/app/models/warp_engine/application_record.rb b/app/models/warp_engine/application_record.rb new file mode 100644 index 0000000..9c62512 --- /dev/null +++ b/app/models/warp_engine/application_record.rb @@ -0,0 +1,5 @@ +module WarpEngine + class ApplicationRecord < ActiveRecord::Base + self.abstract_class = true + end +end diff --git a/app/models/warp_engine/download.rb b/app/models/warp_engine/download.rb new file mode 100644 index 0000000..0247998 --- /dev/null +++ b/app/models/warp_engine/download.rb @@ -0,0 +1,19 @@ +module WarpEngine + class Download < ApplicationRecord + belongs_to :release, optional: true + + validates :file_path, presence: true + + default_scope { where(deleted_at: nil) } + + def self.ransackable_attributes(auth_object = nil) + %w[created_at deleted_at file_path id ip_address referer release_id updated_at user_agent] + end + + def self.ransackable_associations(auth_object = nil) + %w[release] + end + + ActiveSupport.run_load_hooks(:warp_engine_download, self) + end +end diff --git a/app/models/warp_engine/external_link.rb b/app/models/warp_engine/external_link.rb new file mode 100644 index 0000000..b582f01 --- /dev/null +++ b/app/models/warp_engine/external_link.rb @@ -0,0 +1,22 @@ +module WarpEngine + class ExternalLink < ApplicationRecord + self.table_name = "external_links" + + belongs_to :software + + validates :label, presence: true + validates :url, presence: true + + default_scope { where(deleted_at: nil) } + + def self.ransackable_attributes(auth_object = nil) + %w[created_at deleted_at id label software_id updated_at url] + end + + def self.ransackable_associations(auth_object = nil) + %w[software] + end + + ActiveSupport.run_load_hooks(:warp_engine_external_link, self) + end +end diff --git a/app/models/warp_engine/image.rb b/app/models/warp_engine/image.rb new file mode 100644 index 0000000..5b2212f --- /dev/null +++ b/app/models/warp_engine/image.rb @@ -0,0 +1,36 @@ +module WarpEngine + class Image < ApplicationRecord + def self.upload_path + WarpEngine.config.image_container_path + end + + has_many :software_images, dependent: :restrict_with_error + + default_scope { where(deleted_at: nil) } + + attr_accessor :file_upload + + before_save :process_upload, if: -> { file_upload.present? } + + def self.ransackable_attributes(auth_object = nil) + %w[content_type created_at deleted_at filename id original_filename updated_at] + end + + def file_path + File.join(self.class.upload_path, filename.to_s) + end + + ActiveSupport.run_load_hooks(:warp_engine_image, self) + + private + + def process_upload + FileUtils.mkdir_p(self.class.upload_path) + self.original_filename = file_upload.original_filename + self.content_type = file_upload.content_type.presence || "application/octet-stream" + ext = File.extname(file_upload.original_filename) + self.filename = "#{SecureRandom.uuid}#{ext}" + IO.copy_stream(file_upload.to_io, file_path) + end + end +end diff --git a/app/models/warp_engine/platform_link.rb b/app/models/warp_engine/platform_link.rb new file mode 100644 index 0000000..c2de668 --- /dev/null +++ b/app/models/warp_engine/platform_link.rb @@ -0,0 +1,27 @@ +module WarpEngine + class PlatformLink < ApplicationRecord + self.table_name = "platform_links" + + SUPPORTED_PLATFORMS = %w[tic80 ebitengine love c64 godot bevy phaser].freeze + + validates :name, presence: true + validates :url, presence: true + validates :platform, presence: true, inclusion: { in: SUPPORTED_PLATFORMS } + + default_scope { where(deleted_at: nil).order(:position) } + + def self.ransackable_attributes(auth_object = nil) + %w[created_at deleted_at id name platform position updated_at url] + end + + def self.ransackable_associations(auth_object = nil) + [] + end + + def self.for_platform(platform) + where(platform: platform).to_a + end + + ActiveSupport.run_load_hooks(:warp_engine_platform_link, self) + end +end diff --git a/app/models/warp_engine/release.rb b/app/models/warp_engine/release.rb new file mode 100644 index 0000000..8938887 --- /dev/null +++ b/app/models/warp_engine/release.rb @@ -0,0 +1,37 @@ +module WarpEngine + class Release < ApplicationRecord + self.table_name = "releases" + + belongs_to :software + has_many :downloads + has_many :release_assets + + accepts_nested_attributes_for :release_assets, allow_destroy: true + + validates :version, presence: true + validates :version, uniqueness: { scope: :software_id } + + default_scope { where(deleted_at: nil) } + + validate :c64_cannot_be_web_playable + + def c64_cannot_be_web_playable + return unless software&.platform == "c64" + + has_html = release_assets.reject(&:marked_for_destruction?).any? { |a| a.kind == "html" } + if has_html + errors.add(:base, "C64 releases cannot be web playable (download only)") + end + end + + def self.ransackable_attributes(auth_object = nil) + %w[created_at deleted_at id software_id updated_at version] + end + + def self.ransackable_associations(auth_object = nil) + %w[downloads release_assets software] + end + + ActiveSupport.run_load_hooks(:warp_engine_release, self) + end +end diff --git a/app/models/warp_engine/release_asset.rb b/app/models/warp_engine/release_asset.rb new file mode 100644 index 0000000..001d69e --- /dev/null +++ b/app/models/warp_engine/release_asset.rb @@ -0,0 +1,26 @@ +module WarpEngine + class ReleaseAsset < ApplicationRecord + KINDS = %w[cartridge source html docs + win_x86 win_x64 linux_x86 linux_x64 + mac_x64 mac_arm64 mac_universal].freeze + + belongs_to :release + + enum :kind, KINDS.index_by(&:itself) + + validates :path, presence: true + validates :kind, uniqueness: { scope: :release_id } + + default_scope { where(deleted_at: nil) } + + def self.ransackable_attributes(auth_object = nil) + %w[created_at deleted_at id kind path release_id updated_at] + end + + def self.ransackable_associations(auth_object = nil) + %w[release] + end + + ActiveSupport.run_load_hooks(:warp_engine_release_asset, self) + end +end diff --git a/app/models/warp_engine/software.rb b/app/models/warp_engine/software.rb new file mode 100644 index 0000000..b6d4d32 --- /dev/null +++ b/app/models/warp_engine/software.rb @@ -0,0 +1,31 @@ +module WarpEngine + class Software < ApplicationRecord + self.table_name = "softwares" + + has_many :software_images, foreign_key: :software_id, dependent: :destroy + has_many :images, through: :software_images + has_many :releases, foreign_key: :software_id + has_many :downloads, through: :releases + has_many :external_links, foreign_key: :software_id + + accepts_nested_attributes_for :software_images, allow_destroy: true + accepts_nested_attributes_for :external_links, allow_destroy: true + accepts_nested_attributes_for :releases, allow_destroy: true + + validates :name, presence: true, uniqueness: true + validates :title, presence: true + validates :platform, presence: true + + default_scope { where(deleted_at: nil) } + + def self.ransackable_attributes(auth_object = nil) + %w[author created_at desc highlighted id license name platform site status story title updated_at] + end + + def self.ransackable_associations(auth_object = nil) + %w[releases external_links software_images images] + end + + ActiveSupport.run_load_hooks(:warp_engine_software, self) + end +end diff --git a/app/models/warp_engine/software_image.rb b/app/models/warp_engine/software_image.rb new file mode 100644 index 0000000..3a0165c --- /dev/null +++ b/app/models/warp_engine/software_image.rb @@ -0,0 +1,45 @@ +module WarpEngine + class SoftwareImage < ApplicationRecord + belongs_to :software + belongs_to :image, optional: true + + attr_accessor :file_upload + + before_validation :create_image_from_upload, if: -> { file_upload.present? } + + validates :image_id, presence: true + validates :image_id, uniqueness: { scope: :software_id } + validates :position, numericality: { only_integer: true, greater_than_or_equal_to: 0 } + + before_save :unset_other_defaults, if: -> { is_default? && is_default_changed? } + + default_scope { order(:position) } + + def self.ransackable_attributes(auth_object = nil) + %w[created_at id image_id is_default position software_id updated_at] + end + + def self.ransackable_associations(auth_object = nil) + %w[image software] + end + + ActiveSupport.run_load_hooks(:warp_engine_software_image, self) + + private + + def create_image_from_upload + img = Image.new(file_upload: file_upload) + if img.save + self.image = img + else + errors.add(:file_upload, img.errors.full_messages.join(", ")) + end + end + + def unset_other_defaults + SoftwareImage.where(software_id: software_id, is_default: true) + .where.not(id: id) + .update_all(is_default: false) + end + end +end