Module: Plutonium::Attachments

Defined in:
lib/plutonium/attachments.rb

Overview

Staging and revival for attachments that travel as PLAIN STRINGS — backend-agnostic and model-free.

Two subsystems need this and neither has a model to hang an attachment off while the work is in flight: a wizard stages a file across steps, and an async interaction has to carry one from the request into a job. Both hold a bare attribute :photo, :string, so the value has to BE a string.

Two shapes exist, and they are distinguishable — ActiveStorage's signed_id is opaque, Shrine's cached-file data parses as JSON — so each revives through its own backend with no model and no per-field configuration.

Revival is deliberately separate from staging. Attachments.stage_upload runs in the request, where the file is; Attachments.resolve runs wherever the value is read, which may be a render or a job. Assigning a token straight to a model attachment (both backends accept one) needs neither.

Backend objects answer DIFFERENT method names (filename/content_type vs original_filename/mime_type), so a resolved token is wrapped in Resolved, a uniform view over both.

Defined Under Namespace

Classes: Resolved

Constant Summary collapse

STAGING_ONLY_INPUT_OPTIONS =

Options on a file input that are consumed SERVER-SIDE to stage the upload, and are never form or HTML concerns.

They have to be kept out of what the form renders: Phlex rejects a Class-valued uploader: as an attribute outright, and backend: would leak as a stray one. Both the wizard (Wizard::StepAdapter) and an interaction's form read this list, so the two cannot drift.

%i[backend uploader].freeze

Class Method Summary collapse

Class Method Details

.default_backendObject

The default staging backend when a caller names none: the globally configured one, else auto-detected (active_shrine loaded → Shrine, else ActiveStorage).

Subsystems layer their own override on top of this rather than replacing it — see Wizard::Attachments.attachment_backend.



81
82
83
84
# File 'lib/plutonium/attachments.rb', line 81

def default_backend
  Plutonium.configuration.attachment_backend ||
    (defined?(ActiveShrine) ? :shrine : :active_storage)
end

.resolve(value) ⇒ Array<Resolved>

Resolve a staged attachment token (or array of them) into uniform Resolved view(s).

Parameters:

  • value (String, Array, nil)

    the staged token(s).

Returns:

  • (Array<Resolved>)

    resolved attachments; blank, tampered, or unrecognized tokens are dropped (never raised), so a bad token can't 500 the form or the review.



43
44
45
# File 'lib/plutonium/attachments.rb', line 43

def resolve(value)
  Array(value).filter_map { |token| resolve_token(token) }
end

.stage_upload(value, backend: nil, uploader: nil) ⇒ Object

SERVER-SIDE staging: turn a submitted attachment value into a token string to stage in data, minting one from an uploaded file when needed.

Handles every shape a step POST can carry for an attachment field:

  • an already-minted token String (direct upload, or the hidden preview field on re-submit) → kept verbatim;
  • an uploaded file (IO-like) → uploaded to the backend's cache, returning its token (an AS signed_id, or Shrine cached-file JSON);
  • blank / no selection → nil (the caller drops the key so the previously staged token survives a Back/re-submit);
  • an Array (multiple) → each element mapped, blanks dropped.

Parameters:

  • backend (Symbol, nil) (defaults to: nil)

    per-field override; nil → the configured default.

  • uploader (Class, String, nil) (defaults to: nil)

    a Shrine uploader to cache through (:shrine backend only) — its cache-stage plugins (mime/dimension extraction, generate_location, validations) run instead of base Shrine's. The minted token stays uploader-agnostic, so display + execute promotion are unaffected. Ignored shape for ActiveStorage (raises if given).



65
66
67
68
69
70
71
72
73
# File 'lib/plutonium/attachments.rb', line 65

def stage_upload(value, backend: nil, uploader: nil)
  if value.is_a?(Array)
    value.filter_map { |v| stage_upload(v, backend:, uploader:) }.presence
  elsif value.is_a?(String)
    value.presence
  elsif value.respond_to?(:read)
    upload_to_cache(value, backend || default_backend, uploader:)
  end
end

.validation_errors(value, backend: nil, uploader: nil) ⇒ Array<String>

Run the EFFECTIVE Shrine uploader's attacher validations against a staged token (or array of them), returning the validation messages — so a file that violates the uploader's validate_* rules is rejected at the STEP (stage phase), not deferred to execute's model assignment.

The effective uploader is the field's uploader: if given, else base Shrine — both of which may carry Attacher.validate rules. Returns [] when the field isn't Shrine-backed (ActiveStorage has no attacher here), when nothing is staged, or when the effective uploader declares no validations.

Parameters:

  • value (String, Array, nil)

    the staged token(s).

  • backend (Symbol, nil) (defaults to: nil)

    per-field override; nil → the configured default.

  • uploader (Class, String, nil) (defaults to: nil)

    the field's uploader: option.

Returns:

  • (Array<String>)

    validation messages (empty ⇒ valid).



100
101
102
103
104
105
106
107
108
109
110
# File 'lib/plutonium/attachments.rb', line 100

def validation_errors(value, backend: nil, uploader: nil)
  return [] unless (backend || default_backend).to_sym == :shrine

  klass = shrine_uploader(uploader)
  # Shrine's `validation` plugin is OPTIONAL — without it (or `validation_helpers`)
  # the Attacher has no `#errors` and nothing to enforce. Detect it up front so a
  # plugin-less app is a clean no-op, not a per-step rescued NoMethodError.
  return [] unless klass::Attacher.method_defined?(:errors)

  Array(value).flat_map { |token| token_validation_errors(klass, token) }
end