The build endpoints state their input rules once
`POST /build/publish` opened with three guard clauses and `POST /build/upload`
with eight, each one a `return render json: { error: ... }` — name present,
version present, name format, version format, file present, filename prefix,
size, digest. That is a validation layer written by hand, in a place where it
cannot be unit tested: exercising it needs a request.
`PublishInputDto` was already there and was a bare `Struct` with no rules at
all, so the controller carried them. It is an `ActiveModel::Model` now, with
the presence and platform-inclusion validations on it, and `UploadInputDto`
joins it with the name and version formats and the `<name>-<version>` filename
convention. Each controller reads:
return render json: { error: input.error_message }, status: :bad_request unless input.valid?
Size and digest keep their own explicit checks, because they are not the same
answer: 413 tells a caller to stop, 422 tells it to retry a truncated upload,
and a single error bag cannot say which. Every status code the endpoints
answered before, they answer now — build_publish_controller_spec and
build_uploads_controller_spec pin all of them, unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,14 @@
|
||||
module WarpEngine
|
||||
PublishInputDto = Struct.new(:platform, :name, :version, keyword_init: true) do
|
||||
def initialize(platform:, name:, version: nil)
|
||||
super
|
||||
end
|
||||
class PublishInputDto
|
||||
include ActiveModel::Model
|
||||
|
||||
attr_accessor :platform, :name, :version
|
||||
|
||||
validates :name, presence: true
|
||||
validates :version, presence: true
|
||||
validates :platform, presence: true
|
||||
validates :platform, inclusion: { in: WarpEngine::Platform::NAMES }, allow_blank: true
|
||||
|
||||
def error_message = errors.full_messages.to_sentence
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
module WarpEngine
|
||||
class UploadInputDto
|
||||
include ActiveModel::Model
|
||||
|
||||
NAME_FORMAT = /\A[A-Za-z0-9._-]+\z/
|
||||
|
||||
attr_accessor :name, :version, :file, :sha256
|
||||
|
||||
validates :name, format: { with: NAME_FORMAT }
|
||||
validates :version, format: { with: NAME_FORMAT }
|
||||
validate :file_present
|
||||
validate :filename_prefixed
|
||||
|
||||
def filename
|
||||
return nil unless file.respond_to?(:original_filename)
|
||||
|
||||
File.basename(file.original_filename.to_s)
|
||||
end
|
||||
|
||||
def error_message = errors.full_messages.to_sentence
|
||||
|
||||
private
|
||||
|
||||
def file_present
|
||||
return if file.respond_to?(:original_filename)
|
||||
|
||||
errors.add(:file, "not provided")
|
||||
end
|
||||
|
||||
def filename_prefixed
|
||||
return if filename.nil? || errors.include?(:name) || errors.include?(:version)
|
||||
return if filename.start_with?("#{name}-#{version}.", "#{name}-#{version}-")
|
||||
|
||||
errors.add(:file, "name must be prefixed with #{name}-#{version}")
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user