dtos, new services, serializers
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
class Api::EventsController < ApiController
|
||||
def index
|
||||
render json: EventSerializer.serialize_all(Event.upcoming)
|
||||
render json: EventService.new.index
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
class Api::ImagesController < ApplicationController
|
||||
def show
|
||||
image = Image.find(params[:id])
|
||||
image = ImageService.new.show(ImageShowInput.new(id: params[:id]))
|
||||
send_file image.file_path, type: image.content_type, disposition: "inline"
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render plain: "Not Found", status: :not_found
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
class Api::MembersController < ApiController
|
||||
def index
|
||||
render json: MemberSerializer.serialize_all(Member.includes(:image).order(:id))
|
||||
render json: MemberService.new.index
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
class Api::SoftwareController < ApiController
|
||||
def index
|
||||
render json: SoftwareListService.new.call
|
||||
render json: SoftwareService.new.index
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
class Api::SoftwareHighlightedController < ApiController
|
||||
def index
|
||||
result = SoftwareHighlightedService.new.call
|
||||
result = SoftwareHighlightedService.new.index
|
||||
if result
|
||||
render json: result
|
||||
else
|
||||
|
||||
@@ -1,21 +1,12 @@
|
||||
class FilesController < ApplicationController
|
||||
skip_forgery_protection
|
||||
|
||||
BASE_PATH = Pathname.new(ENV.fetch("FILE_CONTAINER_PATH", "/softwares")).realpath
|
||||
def show
|
||||
requested = params[:path].to_s
|
||||
full_path = BASE_PATH.join(requested)
|
||||
|
||||
if File.directory?(full_path)
|
||||
index_path = full_path.join('index.html')
|
||||
return head(:not_found) unless File.file?(index_path)
|
||||
return redirect_to("/file/#{requested.chomp('/')}/index.html", status: :moved_permanently)
|
||||
result = FileService.new.show(FileShowInput.new(path: params[:path]))
|
||||
case result.type
|
||||
when :redirect then redirect_to result.url, status: :moved_permanently
|
||||
when :file then send_file result.path, disposition: "inline"
|
||||
when :not_found then head :not_found
|
||||
end
|
||||
|
||||
if File.file?(full_path)
|
||||
send_file full_path, disposition: 'inline'
|
||||
else
|
||||
head :not_found
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,24 +1,18 @@
|
||||
class UpdateController < ApiController
|
||||
PLATFORM_SERVICES = {
|
||||
"tic80" => "SoftwareUpdater::Tic80Service",
|
||||
"ebitengine" => "SoftwareUpdater::EbitengineService",
|
||||
"love" => "SoftwareUpdater::LoveService"
|
||||
}.freeze
|
||||
|
||||
def update
|
||||
return render plain: "Unauthorized", status: :unauthorized unless authorized?
|
||||
return render plain: "Version not provided", status: :bad_request if params[:version].blank?
|
||||
|
||||
platform = params[:platform]
|
||||
name = params[:name]
|
||||
version = params[:version]
|
||||
input = UpdateInput.new(
|
||||
platform: params[:platform],
|
||||
name: params[:name],
|
||||
version: params[:version]
|
||||
)
|
||||
|
||||
return render plain: "Version not provided", status: :bad_request if version.blank?
|
||||
|
||||
service_class_name = PLATFORM_SERVICES[platform]
|
||||
return render plain: "Unsupported platform: #{platform}", status: :bad_request unless service_class_name
|
||||
|
||||
service_class_name.constantize.new.update(name, version)
|
||||
UpdateService.new.update(input)
|
||||
render plain: "Updated"
|
||||
rescue ArgumentError => e
|
||||
render plain: e.message, status: :bad_request
|
||||
rescue => e
|
||||
Rails.logger.error("[UpdateController] #{e.class}: #{e.message}\n#{e.backtrace.first(5).join("\n")}")
|
||||
render plain: e.message, status: :internal_server_error
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
class FileResult
|
||||
attr_reader :type, :path, :url
|
||||
|
||||
def initialize(type:, path: nil, url: nil)
|
||||
@type = type
|
||||
@path = path
|
||||
@url = url
|
||||
end
|
||||
|
||||
def self.file(path) = new(type: :file, path: path)
|
||||
def self.redirect(url) = new(type: :redirect, url: url)
|
||||
def self.not_found = new(type: :not_found)
|
||||
end
|
||||
@@ -0,0 +1 @@
|
||||
FileShowInput = Struct.new(:path, keyword_init: true)
|
||||
@@ -0,0 +1 @@
|
||||
ImageShowInput = Struct.new(:id, keyword_init: true)
|
||||
@@ -0,0 +1 @@
|
||||
UpdateInput = Struct.new(:platform, :name, :version, keyword_init: true)
|
||||
@@ -1,7 +1,6 @@
|
||||
class EventSerializer
|
||||
class EventSerializer < Blueprinter::Base
|
||||
DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
|
||||
|
||||
def self.serialize_all(events)
|
||||
events.map { |e| { name: e.name, date: e.date.utc.strftime(DATE_FORMAT) } }
|
||||
end
|
||||
field :name
|
||||
field(:date) { |event| event.date.utc.strftime(DATE_FORMAT) }
|
||||
end
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
class ExternalLinkSerializer < Blueprinter::Base
|
||||
GO_ZERO_TIME = "0001-01-01T00:00:00Z"
|
||||
TS_FORMAT = "%Y-%m-%dT%H:%M:%S.%3NZ"
|
||||
|
||||
field(:ID) { |el| el.id }
|
||||
field(:CreatedAt) { |el| el.created_at&.utc&.strftime(TS_FORMAT) || GO_ZERO_TIME }
|
||||
field(:UpdatedAt) { |el| el.updated_at&.utc&.strftime(TS_FORMAT) || GO_ZERO_TIME }
|
||||
field(:DeletedAt) { |el| el.deleted_at&.utc&.strftime(TS_FORMAT) }
|
||||
field(:softwareId) { |el| el.software_id }
|
||||
field :label
|
||||
field :url
|
||||
end
|
||||
@@ -1,15 +1,5 @@
|
||||
class MemberSerializer
|
||||
def self.serialize_all(members)
|
||||
members.map { |m| serialize(m) }
|
||||
end
|
||||
class MemberSerializer < Blueprinter::Base
|
||||
fields :nick, :real_nick, :motto, :avatar_filename
|
||||
|
||||
def self.serialize(m)
|
||||
{
|
||||
nick: m.nick,
|
||||
real_nick: m.real_nick,
|
||||
motto: m.motto,
|
||||
avatar_filename: m.avatar_filename,
|
||||
image_url: m.image_id ? "/api/image/#{m.image_id}" : nil
|
||||
}
|
||||
end
|
||||
field(:image_url) { |member| member.image_id ? "/api/image/#{member.image_id}" : nil }
|
||||
end
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
class ReleaseSerializer < Blueprinter::Base
|
||||
GO_ZERO_TIME = "0001-01-01T00:00:00Z"
|
||||
TS_FORMAT = "%Y-%m-%dT%H:%M:%S.%3NZ"
|
||||
FILE_PATH_FROM = "/softwares/"
|
||||
FILE_PATH_TO = "/file/"
|
||||
|
||||
field(:ID) { |r| r.id }
|
||||
field(:CreatedAt) { |r| r.created_at&.utc&.strftime(TS_FORMAT) || GO_ZERO_TIME }
|
||||
field(:UpdatedAt) { |r| r.updated_at&.utc&.strftime(TS_FORMAT) || GO_ZERO_TIME }
|
||||
field(:DeletedAt) { |r| r.deleted_at&.utc&.strftime(TS_FORMAT) }
|
||||
field(:softwareId) { |r| r.software_id }
|
||||
field :version
|
||||
field(:cartridgePath) { |r| r.cartridge_path.blank? ? "" : r.cartridge_path.gsub(FILE_PATH_FROM, FILE_PATH_TO) }
|
||||
field(:sourcePath) { |r| r.source_path.blank? ? "" : r.source_path.gsub(FILE_PATH_FROM, FILE_PATH_TO) }
|
||||
field(:htmlFolderPath) { |r| r.html_folder_path.blank? ? "" : r.html_folder_path.gsub(FILE_PATH_FROM, FILE_PATH_TO) }
|
||||
field(:docsFolderPath) { |r| r.docs_folder_path.blank? ? "" : r.docs_folder_path.gsub(FILE_PATH_FROM, FILE_PATH_TO) }
|
||||
end
|
||||
@@ -1,83 +1,20 @@
|
||||
class SoftwareSerializer
|
||||
GO_ZERO_TIME = "0001-01-01T00:00:00Z"
|
||||
FILE_PATH_FROM = "/softwares/"
|
||||
FILE_PATH_TO = "/file/"
|
||||
class SoftwareSerializer < Blueprinter::Base
|
||||
GO_ZERO_TIME = "0001-01-01T00:00:00Z"
|
||||
TS_FORMAT = "%Y-%m-%dT%H:%M:%S.%3NZ"
|
||||
|
||||
def self.show_data(software, releases)
|
||||
sorted = releases.sort_by { |r| r.created_at || Time.at(0) }.reverse
|
||||
|
||||
latest = sorted.reject { |r| r.version.to_s.start_with?("dev-") }.first
|
||||
web_playable = sorted.find { |r| r.html_folder_path.present? }
|
||||
|
||||
{
|
||||
software: serialize_software(software),
|
||||
releases: sorted.map { |r| serialize_release(r) },
|
||||
latestRelease: latest ? serialize_release(latest) : nil,
|
||||
webPlayableRelease: web_playable ? serialize_release(web_playable) : nil
|
||||
}
|
||||
end
|
||||
|
||||
def self.serialize_software(sw)
|
||||
{
|
||||
"ID" => sw.id,
|
||||
"CreatedAt" => fmt(sw.created_at),
|
||||
"UpdatedAt" => fmt(sw.updated_at),
|
||||
"DeletedAt" => fmt_nullable(sw.deleted_at),
|
||||
"name" => sw.name,
|
||||
"title" => sw.title,
|
||||
"author" => sw.author,
|
||||
"desc" => sw.desc.to_s,
|
||||
"story" => sw.story.to_s,
|
||||
"license" => sw.license.to_s,
|
||||
"platform" => sw.platform,
|
||||
"status" => sw.status,
|
||||
"highlighted" => sw.highlighted ? true : false,
|
||||
"externalLinks" => sw.external_links.map { |el| serialize_external_link(el) },
|
||||
"imageUrl" => sw.image_id ? "/api/image/#{sw.image_id}" : nil
|
||||
}
|
||||
end
|
||||
|
||||
def self.serialize_release(r)
|
||||
{
|
||||
"ID" => r.id,
|
||||
"CreatedAt" => fmt(r.created_at),
|
||||
"UpdatedAt" => fmt(r.updated_at),
|
||||
"DeletedAt" => fmt_nullable(r.deleted_at),
|
||||
"softwareId" => r.software_id,
|
||||
"version" => r.version,
|
||||
"cartridgePath" => rewrite(r.cartridge_path),
|
||||
"sourcePath" => rewrite(r.source_path),
|
||||
"htmlFolderPath" => rewrite(r.html_folder_path),
|
||||
"docsFolderPath" => rewrite(r.docs_folder_path)
|
||||
}
|
||||
end
|
||||
|
||||
def self.serialize_external_link(el)
|
||||
{
|
||||
"ID" => el.id,
|
||||
"CreatedAt" => fmt(el.created_at),
|
||||
"UpdatedAt" => fmt(el.updated_at),
|
||||
"DeletedAt" => fmt_nullable(el.deleted_at),
|
||||
"softwareId" => el.software_id,
|
||||
"label" => el.label,
|
||||
"url" => el.url
|
||||
}
|
||||
end
|
||||
|
||||
# null → Go zero-time string (matches GORM behavior)
|
||||
def self.fmt(t)
|
||||
return GO_ZERO_TIME if t.nil?
|
||||
t.utc.strftime("%Y-%m-%dT%H:%M:%S.%3NZ")
|
||||
end
|
||||
|
||||
# null → JSON null (matches gorm.DeletedAt behavior)
|
||||
def self.fmt_nullable(t)
|
||||
return nil if t.nil?
|
||||
t.utc.strftime("%Y-%m-%dT%H:%M:%S.%3NZ")
|
||||
end
|
||||
|
||||
def self.rewrite(path)
|
||||
return "" if path.blank?
|
||||
path.gsub(FILE_PATH_FROM, FILE_PATH_TO)
|
||||
end
|
||||
field(:ID) { |sw| sw.id }
|
||||
field(:CreatedAt) { |sw| sw.created_at&.utc&.strftime(TS_FORMAT) || GO_ZERO_TIME }
|
||||
field(:UpdatedAt) { |sw| sw.updated_at&.utc&.strftime(TS_FORMAT) || GO_ZERO_TIME }
|
||||
field(:DeletedAt) { |sw| sw.deleted_at&.utc&.strftime(TS_FORMAT) }
|
||||
field :name
|
||||
field :title
|
||||
field :author
|
||||
field(:desc) { |sw| sw.desc.to_s }
|
||||
field(:story) { |sw| sw.story.to_s }
|
||||
field(:license) { |sw| sw.license.to_s }
|
||||
field :platform
|
||||
field :status
|
||||
field(:highlighted) { |sw| sw.highlighted ? true : false }
|
||||
field(:externalLinks) { |sw| ExternalLinkSerializer.render_as_hash(sw.external_links) }
|
||||
field(:imageUrl) { |sw| sw.image_id ? "/api/image/#{sw.image_id}" : nil }
|
||||
end
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
class EventService
|
||||
def index
|
||||
EventSerializer.render_as_hash(Event.upcoming)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,19 @@
|
||||
class FileService
|
||||
BASE_PATH = Pathname.new(ENV.fetch("FILE_CONTAINER_PATH", "/softwares")).realpath
|
||||
|
||||
def show(input)
|
||||
full_path = BASE_PATH.join(input.path.to_s)
|
||||
|
||||
if File.directory?(full_path)
|
||||
index_path = full_path.join("index.html")
|
||||
return FileResult.not_found unless File.file?(index_path)
|
||||
return FileResult.redirect("/file/#{input.path.to_s.chomp("/")}/index.html")
|
||||
end
|
||||
|
||||
if File.file?(full_path)
|
||||
FileResult.file(full_path)
|
||||
else
|
||||
FileResult.not_found
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,5 @@
|
||||
class ImageService
|
||||
def show(input)
|
||||
Image.find(input.id)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,5 @@
|
||||
class MemberService
|
||||
def index
|
||||
MemberSerializer.render_as_hash(Member.includes(:image).order(:id))
|
||||
end
|
||||
end
|
||||
@@ -1,5 +1,5 @@
|
||||
class SoftwareHighlightedService
|
||||
def call
|
||||
def index
|
||||
software = Software.includes(:external_links, :image)
|
||||
.where(highlighted: true)
|
||||
.order(id: :desc)
|
||||
@@ -7,6 +7,21 @@ class SoftwareHighlightedService
|
||||
return nil unless software
|
||||
|
||||
releases = Release.where(software_id: software.id).to_a
|
||||
SoftwareSerializer.show_data(software, releases)
|
||||
build_response(software, releases)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_response(software, releases)
|
||||
sorted = releases.sort_by { |r| r.created_at || Time.at(0) }.reverse
|
||||
latest = sorted.reject { |r| r.version.to_s.start_with?("dev-") }.first
|
||||
web_playable = sorted.find { |r| r.html_folder_path.present? }
|
||||
|
||||
{
|
||||
software: SoftwareSerializer.render_as_hash(software),
|
||||
releases: ReleaseSerializer.render_as_hash(sorted),
|
||||
latestRelease: latest ? ReleaseSerializer.render_as_hash(latest) : nil,
|
||||
webPlayableRelease: web_playable ? ReleaseSerializer.render_as_hash(web_playable) : nil
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
class SoftwareListService
|
||||
def call
|
||||
softwares = Software.includes(:releases, :external_links, :image).all
|
||||
items = softwares.map do |sw|
|
||||
SoftwareSerializer.show_data(sw, sw.releases.to_a)
|
||||
end
|
||||
{ softwares: items }
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,21 @@
|
||||
class SoftwareService
|
||||
def index
|
||||
softwares = Software.includes(:releases, :external_links, :image).all
|
||||
{ softwares: softwares.map { |sw| build_response(sw, sw.releases.to_a) } }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_response(software, releases)
|
||||
sorted = releases.sort_by { |r| r.created_at || Time.at(0) }.reverse
|
||||
latest = sorted.reject { |r| r.version.to_s.start_with?("dev-") }.first
|
||||
web_playable = sorted.find { |r| r.html_folder_path.present? }
|
||||
|
||||
{
|
||||
software: SoftwareSerializer.render_as_hash(software),
|
||||
releases: ReleaseSerializer.render_as_hash(sorted),
|
||||
latestRelease: latest ? ReleaseSerializer.render_as_hash(latest) : nil,
|
||||
webPlayableRelease: web_playable ? ReleaseSerializer.render_as_hash(web_playable) : nil
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,13 @@
|
||||
class UpdateService
|
||||
PLATFORM_SERVICES = {
|
||||
"tic80" => "SoftwareUpdater::Tic80Service",
|
||||
"ebitengine" => "SoftwareUpdater::EbitengineService",
|
||||
"love" => "SoftwareUpdater::LoveService"
|
||||
}.freeze
|
||||
|
||||
def update(input)
|
||||
service_class = PLATFORM_SERVICES[input.platform]
|
||||
raise ArgumentError, "Unsupported platform: #{input.platform}" unless service_class
|
||||
service_class.constantize.new.update(input.name, input.version)
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user