From fe3f92e53888ce25bec97710c66188e05e056ef1 Mon Sep 17 00:00:00 2001 From: Akira Matsuda Date: Tue, 13 Oct 2026 18:59:44 +0900 Subject: [PATCH 01/22] activejob: Copy hand-written RBS and test from 6.0 to 8.1 Co-Authored-By: Claude Opus 5.5 (1M context) --- gems/activejob/8.1/_test/activejob.rb | 25 ++++++++ gems/activejob/8.1/_test/metadata.yaml | 3 + gems/activejob/8.1/_test/send_message_job.rbs | 7 +++ gems/activejob/8.1/activejob.rbs | 62 +++++++++++++++++++ gems/activejob/8.1/patch.rbs | 29 +++++++++ 5 files changed, 126 insertions(+) create mode 100644 gems/activejob/8.1/_test/activejob.rb create mode 100644 gems/activejob/8.1/_test/metadata.yaml create mode 100644 gems/activejob/8.1/_test/send_message_job.rbs create mode 100644 gems/activejob/8.1/activejob.rbs create mode 100644 gems/activejob/8.1/patch.rbs diff --git a/gems/activejob/8.1/_test/activejob.rb b/gems/activejob/8.1/_test/activejob.rb new file mode 100644 index 000000000..d0521bf37 --- /dev/null +++ b/gems/activejob/8.1/_test/activejob.rb @@ -0,0 +1,25 @@ +class SendMessageJob < ActiveJob::Base + queue_as :default + self.queue_adapter = :sidekiq + + before_enqueue do |job| + # before enqueued + end + + around_perform :around_task + + def perform(*args) + # Do something later + end + + private + + def around_task + # Do something before + # yield + # Do something after + end +end + +SendMessageJob.perform_now +SendMessageJob.set(queue: :another_queue).perform_later(flag: false) diff --git a/gems/activejob/8.1/_test/metadata.yaml b/gems/activejob/8.1/_test/metadata.yaml new file mode 100644 index 000000000..45c31bed5 --- /dev/null +++ b/gems/activejob/8.1/_test/metadata.yaml @@ -0,0 +1,3 @@ +additional_gems: + - railties + - sidekiq \ No newline at end of file diff --git a/gems/activejob/8.1/_test/send_message_job.rbs b/gems/activejob/8.1/_test/send_message_job.rbs new file mode 100644 index 000000000..70b2b929b --- /dev/null +++ b/gems/activejob/8.1/_test/send_message_job.rbs @@ -0,0 +1,7 @@ +class SendMessageJob < ActiveJob::Base + def perform: (*untyped) -> void + + private + + def around_task: () -> void +end diff --git a/gems/activejob/8.1/activejob.rbs b/gems/activejob/8.1/activejob.rbs new file mode 100644 index 000000000..e5447b4a9 --- /dev/null +++ b/gems/activejob/8.1/activejob.rbs @@ -0,0 +1,62 @@ +module ActiveJob + class Base + extend ActiveJob::Callbacks::ClassMethods + extend ActiveJob::Core::ClassMethods + extend ActiveJob::Enqueuing::ClassMethods + extend ActiveJob::Exceptions::ClassMethods + extend ActiveJob::Execution::ClassMethods + extend ActiveJob::QueueAdapter::ClassMethods + extend ActiveJob::QueueName::ClassMethods + extend ActiveJob::QueuePriority::ClassMethods + + def self.queue_as: (*untyped) -> void + | ... + def self.queue_with_priority: (?untyped? priority) -> void + | ... + def self.before_perform: (*untyped filters) -> void + | ... + def self.after_perform: (*untyped filters) -> void + | ... + def self.around_perform: (*untyped filters) -> void + | ... + def self.before_enqueue: (*untyped filters) -> void + | ... + def self.after_enqueue: (*untyped filters) -> void + | ... + def self.around_enqueue: (*untyped filters) -> void + | ... + def self.retry_on: (*untyped) -> void + | ... + def self.discard_on: (*untyped) -> void + | ... + end +end + +module ActiveJob + module QueueAdapters + # == Sidekiq adapter for Active Job + # + # Simple, efficient background processing for Ruby. Sidekiq uses threads to + # handle many jobs at the same time in the same process. It does not + # require Rails but will integrate tightly with it to make background + # processing dead simple. + # + # Read more about Sidekiq {here}[http://sidekiq.org]. + # + # To use Sidekiq set the queue_adapter config to +:sidekiq+. + # + # Rails.application.config.active_job.queue_adapter = :sidekiq + class SidekiqAdapter + def enqueue: (untyped job) -> untyped + + def enqueue_at: (untyped job, untyped timestamp) -> untyped + + class JobWrapper + # nodoc: + include Sidekiq::Job + + def perform: (untyped job_data) -> untyped + end + end + end +end \ No newline at end of file diff --git a/gems/activejob/8.1/patch.rbs b/gems/activejob/8.1/patch.rbs new file mode 100644 index 000000000..0752327fd --- /dev/null +++ b/gems/activejob/8.1/patch.rbs @@ -0,0 +1,29 @@ +# Remove the fake type if the real type is available +module Que + class Job + end +end + +# Remove the fake type if the real type is available +class QC + class Queue + end +end + +# Glue code that could be made more detailed if Sidekiq is available. +module Sidekiq + module Job + end +end + +# Remove the fake type if the real type is available +module Sneakers + module Worker + end +end + +# Remove the fake type if the real type is available +module SuckerPunch + module Job + end +end From e49adbb94142f762ec459df3c1e944b9760acaab Mon Sep 17 00:00:00 2001 From: Akira Matsuda Date: Tue, 13 Oct 2026 19:03:18 +0900 Subject: [PATCH 02/22] activejob: Generate RBS from activejob 8.1.4 Generated with `rbs prototype rb` from the activejob 8.1.4 source and post-processed the same way as the other Rails 8.1 gems: drop members already defined elsewhere, fill missing type arguments, add accessors defined with class_attribute, mattr_accessor, and cattr_accessor, add methods defined with delegate, and carry over typed signatures from 6.0 where the method still has the same parameters. The job generator in lib/rails is left out, as its superclass comes from railties, which activejob doesn't depend on. Co-Authored-By: Claude Opus 5.5 (1M context) --- gems/activejob/8.1/activejob-generated.rbs | 2730 ++++++++++++++++++++ 1 file changed, 2730 insertions(+) create mode 100644 gems/activejob/8.1/activejob-generated.rbs diff --git a/gems/activejob/8.1/activejob-generated.rbs b/gems/activejob/8.1/activejob-generated.rbs new file mode 100644 index 000000000..ac0eef7a2 --- /dev/null +++ b/gems/activejob/8.1/activejob-generated.rbs @@ -0,0 +1,2730 @@ +# The generated code is based on Ruby on Rails source code +# You can find the license of Ruby on Rails from following. + +#Copyright (c) David Heinemeier Hansson +# +#Permission is hereby granted, free of charge, to any person obtaining +#a copy of this software and associated documentation files (the +#"Software"), to deal in the Software without restriction, including +#without limitation the rights to use, copy, modify, merge, publish, +#distribute, sublicense, and/or sell copies of the Software, and to +#permit persons to whom the Software is furnished to do so, subject to +#the following conditions: +# +#The above copyright notice and this permission notice shall be +#included in all copies or substantial portions of the Software. +# +#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +#EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +#MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +#NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +#LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +#OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +#WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + +# :markup: markdown +# :include: ../README.md +module ActiveJob + def self.verbose_enqueue_logs: () -> untyped + + def self.verbose_enqueue_logs=: (untyped value) -> untyped + + extend ::ActiveSupport::Autoload + + # Push many jobs onto the queue at once without running enqueue callbacks. + # Queue adapters may communicate the enqueue status of each job by setting + # successfully_enqueued and/or enqueue_error on the passed-in job instances. + def self.perform_all_later: (*untyped jobs) -> nil +end + +module ActiveJob + # Raised when an exception is raised during job arguments deserialization. + # + # Wraps the original exception raised as +cause+. + class DeserializationError < ::StandardError + def initialize: () -> void + end + + # Raised when an unsupported argument type is set as a job argument. We + # currently support String, Integer, Float, NilClass, TrueClass, FalseClass, + # BigDecimal, Symbol, Date, Time, DateTime, ActiveSupport::TimeWithZone, + # ActiveSupport::Duration, Hash, ActiveSupport::HashWithIndifferentAccess, + # Array, Range, or GlobalID::Identification instances, although this can be + # extended by adding custom serializers. + # Raised if you set the key for a Hash something else than a string or + # a symbol. Also raised when trying to serialize an object which can't be + # identified with a GlobalID - such as an unpersisted Active Record model. + class SerializationError < ::ArgumentError + end + + module Arguments + extend ::ActiveJob::Arguments + + # Serializes a set of arguments. Intrinsic types that can safely be + # serialized without mutation are returned as-is. Arrays/Hashes are + # serialized element by element. All other types are serialized using + # GlobalID. + def serialize: (untyped arguments) -> untyped + + def serialize_argument: (untyped argument) -> untyped + + # Deserializes a set of arguments. Intrinsic types that can safely be + # deserialized without mutation are returned as-is. Arrays/Hashes are + # deserialized element by element. All other types are deserialized using + # GlobalID. + def deserialize: (untyped arguments) -> untyped + + private + + # :nodoc: + GLOBALID_KEY: "_aj_globalid" + + # :nodoc: + SYMBOL_KEYS_KEY: "_aj_symbol_keys" + + # :nodoc: + RUBY2_KEYWORDS_KEY: "_aj_ruby2_keywords" + + # :nodoc: + WITH_INDIFFERENT_ACCESS_KEY: "_aj_hash_with_indifferent_access" + + # :nodoc: + RESERVED_KEYS: untyped + + def deserialize_argument: (untyped argument) -> untyped + + def serialized_global_id?: (untyped hash) -> untyped + + def deserialize_global_id: (untyped hash) -> untyped + + def custom_serialized?: (untyped hash) -> untyped + + def serialize_hash: (untyped argument) -> untyped + + def deserialize_hash: (untyped serialized_hash) -> untyped + + def serialize_hash_key: (untyped key) -> untyped + + def serialize_indifferent_hash: (untyped indifferent_hash) -> untyped + + def transform_symbol_keys: (untyped hash, untyped symbol_keys) -> untyped + + def convert_to_global_id_hash: (untyped argument) -> untyped + end +end + +module ActiveJob + # = Active Job \Base + # + # Active Job objects can be configured to work with different backend + # queuing frameworks. To specify a queue adapter to use: + # + # ActiveJob::Base.queue_adapter = :inline + # + # A list of supported adapters can be found in QueueAdapters. + # + # Active Job objects can be defined by creating a class that inherits + # from the ActiveJob::Base class. The only necessary method to + # implement is the "perform" method. + # + # To define an Active Job object: + # + # class ProcessPhotoJob < ActiveJob::Base + # def perform(photo) + # photo.watermark!('Rails') + # photo.rotate!(90.degrees) + # photo.resize_to_fit!(300, 300) + # photo.upload! + # end + # end + # + # Records that are passed in are serialized/deserialized using Global + # ID. More information can be found in ActiveJob::Arguments. + # + # To enqueue a job to be performed as soon as the queuing system is free: + # + # ProcessPhotoJob.perform_later(photo) + # + # To enqueue a job to be processed at some point in the future: + # + # ProcessPhotoJob.set(wait_until: Date.tomorrow.noon).perform_later(photo) + # + # More information can be found in ActiveJob::Core::ClassMethods#set. + # + # A job can also be processed immediately without sending to the queue: + # + # ProcessPhotoJob.perform_now(photo) + # + # == Exceptions + # + # * DeserializationError - Error class for deserialization errors. + # * SerializationError - Error class for serialization errors. + class Base + include ::ActiveJob::Core + + include ::ActiveJob::QueueAdapter + + include ::ActiveJob::QueueName + + include ::ActiveJob::QueuePriority + + include ::ActiveJob::Enqueuing + + include ::ActiveJob::Execution + + include ::ActiveJob::Callbacks + + include ::ActiveJob::Exceptions + + include ::ActiveJob::Instrumentation + + include ::ActiveJob::Logging + + include ::ActiveJob::ExecutionState + end +end + +module ActiveJob + # = Active Job \Callbacks + # + # Active Job provides hooks during the life cycle of a job. Callbacks allow you + # to trigger logic during this cycle. Available callbacks are: + # + # * before_enqueue + # * around_enqueue + # * after_enqueue + # * before_perform + # * around_perform + # * after_perform + module Callbacks + extend ::ActiveSupport::Concern + + include ::ActiveSupport::Callbacks + + extend ::ActiveSupport::Callbacks + + # These methods will be included into any Active Job object, adding + # callbacks for +perform+ and +enqueue+ methods. + module ClassMethods + # Defines a callback that will get called right before the + # job's perform method is executed. + # + # class VideoProcessJob < ActiveJob::Base + # queue_as :default + # + # before_perform do |job| + # UserMailer.notify_video_started_processing(job.arguments.first) + # end + # + # def perform(video_id) + # Video.find(video_id).process + # end + # end + # + def before_perform: (*untyped filters) { (?) -> untyped } -> untyped + + # Defines a callback that will get called right after the + # job's perform method has finished. + # + # class VideoProcessJob < ActiveJob::Base + # queue_as :default + # + # after_perform do |job| + # UserMailer.notify_video_processed(job.arguments.first) + # end + # + # def perform(video_id) + # Video.find(video_id).process + # end + # end + # + def after_perform: (*untyped filters) { (?) -> untyped } -> untyped + + # Defines a callback that will get called around the job's perform method. + # + # class VideoProcessJob < ActiveJob::Base + # queue_as :default + # + # around_perform do |job, block| + # UserMailer.notify_video_started_processing(job.arguments.first) + # block.call + # UserMailer.notify_video_processed(job.arguments.first) + # end + # + # def perform(video_id) + # Video.find(video_id).process + # end + # end + # + # You can access the return value of the job only if the execution wasn't halted. + # + # class VideoProcessJob < ActiveJob::Base + # around_perform do |job, block| + # value = block.call + # puts value # => "Hello World!" + # end + # + # def perform + # "Hello World!" + # end + # end + # + def around_perform: (*untyped filters) { (?) -> untyped } -> untyped + + # Defines a callback that will get called right before the + # job is enqueued. + # + # class VideoProcessJob < ActiveJob::Base + # queue_as :default + # + # before_enqueue do |job| + # $statsd.increment "enqueue-video-job.try" + # end + # + # def perform(video_id) + # Video.find(video_id).process + # end + # end + # + def before_enqueue: (*untyped filters) { (?) -> untyped } -> untyped + + # Defines a callback that will get called right after the + # job is enqueued. + # + # class VideoProcessJob < ActiveJob::Base + # queue_as :default + # + # after_enqueue do |job| + # result = job.successfully_enqueued? ? "success" : "failure" + # $statsd.increment "enqueue-video-job.#{result}" + # end + # + # def perform(video_id) + # Video.find(video_id).process + # end + # end + # + def after_enqueue: (*untyped filters) { (?) -> untyped } -> untyped + + # Defines a callback that will get called around the enqueuing + # of the job. + # + # class VideoProcessJob < ActiveJob::Base + # queue_as :default + # + # around_enqueue do |job, block| + # $statsd.time "video-job.process" do + # block.call + # end + # end + # + # def perform(video_id) + # Video.find(video_id).process + # end + # end + # + def around_enqueue: (*untyped filters) { (?) -> untyped } -> untyped + end + end +end + +module ActiveJob + class ConfiguredJob + def initialize: (untyped job_class, ?::Hash[untyped, untyped] options) -> void + + def perform_now: (*untyped, **untyped) ?{ (?) -> untyped } -> untyped + + def perform_later: (*untyped, **untyped) ?{ (untyped) -> untyped } -> untyped + end +end + +module ActiveJob + # = Active Job Continuable + # + # The Continuable module provides the ability to track the progress of your + # jobs, and continue from where they left off if interrupted. + # + # Mix ActiveJob::Continuable into your job to enable continuations. + # + # See {ActiveJob::Continuation}[rdoc-ref:ActiveJob::Continuation] for usage. + # + module Continuable + def max_resumptions: () -> untyped + + def max_resumptions?: () -> bool + + def resume_options: () -> untyped + + def resume_options?: () -> bool + + def resume_errors_after_advancing: () -> untyped + + def resume_errors_after_advancing?: () -> bool + + extend ::ActiveSupport::Concern + + # The number of times the job has been resumed. + attr_accessor resumptions: untyped + + attr_accessor continuation: untyped + + # Start a new continuation step + def step: (untyped step_name, ?start: untyped?, ?isolated: bool) ?{ (?) -> untyped } -> untyped + + def serialize: () -> untyped + + def deserialize: (untyped job_data) -> untyped + + def checkpoint!: () -> (untyped | nil) + + def interrupt!: (reason: untyped) -> untyped + + private + + def continue: () { (?) -> untyped } -> untyped + + def resume_job: (untyped exception) -> untyped + end +end + +module ActiveJob + # = Active Job \Continuation + # + # Continuations provide a mechanism for interrupting and resuming jobs. This allows + # long-running jobs to make progress across application restarts. + # + # Jobs should include the ActiveJob::Continuable module to enable continuations. + # \Continuable jobs are automatically retried when interrupted. + # + # Use the +step+ method to define the steps in your job. Steps can use an optional + # cursor to track progress in the step. + # + # Steps are executed as soon as they are encountered. If a job is interrupted, previously + # completed steps will be skipped. If a step is in progress, it will be resumed + # with the last recorded cursor. + # + # Code that is not part of a step will be executed on each job run. + # + # You can pass a block or a method name to the step method. The block will be called with + # the step object as an argument. Methods can either take no arguments or a single argument + # for the step object. + # + # class ProcessImportJob < ApplicationJob + # include ActiveJob::Continuable + # + # def perform(import_id) + # # This always runs, even if the job is resumed. + # @import = Import.find(import_id) + # + # step :validate do + # @import.validate! + # end + # + # step(:process_records) do |step| + # @import.records.find_each(start: step.cursor) do |record| + # record.process + # step.advance! from: record.id + # end + # end + # + # step :reprocess_records + # step :finalize + # end + # + # def reprocess_records(step) + # @import.records.find_each(start: step.cursor) do |record| + # record.reprocess + # step.advance! from: record.id + # end + # end + # + # def finalize + # @import.finalize! + # end + # end + # + # === Cursors + # + # Cursors are used to track progress within a step. The cursor can be any object that is + # serializable as an argument to +ActiveJob::Base.serialize+. It defaults to +nil+. + # + # When a step is resumed, the last cursor value is restored. The code in the step is responsible + # for using the cursor to continue from the right point. + # + # +set!+ sets the cursor to a specific value. + # + # step :iterate_items do |step| + # items[step.cursor..].each do |item| + # process(item) + # step.set! (step.cursor || 0) + 1 + # end + # end + # + # An starting value for the cursor can be set when defining the step: + # + # step :iterate_items, start: 0 do |step| + # items[step.cursor..].each do |item| + # process(item) + # step.set! step.cursor + 1 + # end + # end + # + # The cursor can be advanced with +advance!+. This calls +succ+ on the current cursor value. + # It raises an ActiveJob::Continuation::UnadvanceableCursorError if the cursor does not implement +succ+. + # + # step :iterate_items, start: 0 do |step| + # items[step.cursor..].each do |item| + # process(item) + # step.advance! + # end + # end + # + # You can optionally pass a +from+ argument to +advance!+. This is useful when iterating + # over a collection of records where IDs may not be contiguous. + # + # step :process_records do |step| + # import.records.find_each(start: step.cursor) do |record| + # record.process + # step.advance! from: record.id + # end + # end + # + # You can use an array to iterate over nested records: + # + # step :process_nested_records, start: [ 0, 0 ] do |step| + # Account.find_each(start: step.cursor[0]) do |account| + # account.records.find_each(start: step.cursor[1]) do |record| + # record.process + # step.set! [ account.id, record.id + 1 ] + # end + # step.set! [ account.id + 1, 0 ] + # end + # end + # + # Setting or advancing the cursor creates a checkpoint. You can also create a checkpoint + # manually by calling the +checkpoint!+ method on the step. This is useful if you want to + # allow interruptions, but don't need to update the cursor. + # + # step :destroy_records do |step| + # import.records.find_each do |record| + # record.destroy! + # step.checkpoint! + # end + # end + # + # === Checkpoints + # + # A checkpoint is where a job can be interrupted. At a checkpoint the job will call + # +queue_adapter.stopping?+. If it returns true, the job will raise an + # ActiveJob::Continuation::Interrupt exception. + # + # There is an automatic checkpoint before the start of each step except for the first for + # each job execution. Within a step one is created when calling +set!+, +advance!+ or +checkpoint!+. + # + # Jobs are not automatically interrupted when the queue adapter is marked as stopping - they + # will continue to run either until the next checkpoint, or when the process is stopped. + # + # This is to allow jobs to be interrupted at a safe point, but it also means that the jobs + # should checkpoint more frequently than the shutdown timeout to ensure a graceful restart. + # + # When interrupted, the job will automatically retry with the progress serialized + # in the job data under the +continuation+ key. + # + # The serialized progress contains: + # - a list of the completed steps + # - the current step and its cursor value (if one is in progress) + # + # === Isolated Steps + # + # Steps run sequentially in a single job execution, unless the job is interrupted. + # + # You can specify that a step should always run in its own execution by passing the +isolated: true+ option. + # + # This is useful for long-running steps where it may not be possible to checkpoint within + # the job grace period - it ensures that progress is serialized back into the job data before + # the step starts. + # + # step :quick_step1 + # step :slow_step, isolated: true + # step :quick_step2 + # step :quick_step3 + # + # === Errors + # + # If a job raises an error and is not retried via Active Job, it will be passed back to the underlying + # queue backend and any progress in this execution will be lost. + # + # To mitigate this, the job will be automatically retried if it raises an error after it has made progress. + # Making progress is defined as having completed a step or advanced the cursor within the current step. + # + # === Configuration + # + # Continuable jobs have several configuration options: + # * :max_resumptions - The maximum number of times a job can be resumed. Defaults to +nil+ which means + # unlimited resumptions. + # * :resume_options - Options to pass to +retry_job+ when resuming the job. + # Defaults to { wait: 5.seconds }. + # See {ActiveJob::Exceptions#retry_job}[rdoc-ref:ActiveJob::Exceptions#retry_job] for available options. + # * :resume_errors_after_advancing - Whether to resume errors after advancing the continuation. + # Defaults to +true+. + class Continuation + extend ::ActiveSupport::Autoload + + # Raised when a job is interrupted, allowing Active Job to requeue it. + # This inherits from +Exception+ rather than +StandardError+, so it's not + # caught by normal exception handling. + class Interrupt < ::Exception + end + + # Base class for all Continuation errors. + class Error < ::StandardError + end + + # Raised when a step is invalid. + class InvalidStepError < ::ActiveJob::Continuation::Error + end + + # Raised when there is an error with a checkpoint, such as open database transactions. + class CheckpointError < ::ActiveJob::Continuation::Error + end + + # Raised when attempting to advance a cursor that doesn't implement `succ`. + class UnadvanceableCursorError < ::ActiveJob::Continuation::Error + end + + # Raised when a job has reached its limit of the number of resumes. + # The limit is defined by the +max_resumptions+ class attribute. + class ResumeLimitError < ::ActiveJob::Continuation::Error + end + + include ::ActiveJob::Continuation::Validation + + def initialize: (untyped job, untyped serialized_progress) -> void + + def step: (untyped name, **untyped options) { (?) -> untyped } -> untyped + + def to_h: () -> untyped + + def description: () -> (untyped | ::String | "not started") + + def started?: () -> untyped + + def advanced?: () -> untyped + + def instrumentation: () -> { description: untyped, completed_steps: untyped, current_step: untyped } + + private + + attr_reader job: untyped + + attr_reader encountered: untyped + + attr_reader completed: untyped + + attr_reader current: untyped + + def running_step?: () -> untyped + + def isolating?: () -> untyped + + def completed?: (untyped name) -> untyped + + def new_step: (*untyped args, **untyped options) -> untyped + + def skip_step: (untyped name) -> untyped + + def run_step: (untyped name, start: untyped, isolated: untyped) { (?) -> untyped } -> untyped + + def run_step_inline: (untyped name, start: untyped, **untyped options) { (?) -> untyped } -> untyped + + def instrumenting_step: (untyped step) { (?) -> untyped } -> untyped + + def instrument: (*untyped, **untyped) ?{ (?) -> untyped } -> untyped + end +end + +module ActiveJob + class Continuation + # = Active Job Continuation Step + # + # Represents a step within a continuable job. + # + # When a step is completed, it is recorded in the job's continuation state. + # If the job is interrupted, it will be resumed from after the last completed step. + # + # Steps also have an optional cursor that can be used to track progress within the step. + # If a job is interrupted during a step, the cursor will be saved and passed back when + # the job is resumed. + # + # It is the responsibility of the code in the step to use the cursor correctly to resume + # from where it left off. + class Step + # The name of the step. + attr_reader name: untyped + + # The cursor for the step. + attr_reader cursor: untyped + + def initialize: (untyped name, untyped cursor, job: untyped, resumed: untyped) -> void + + # Check if the job should be interrupted, and if so raise an Interrupt exception. + # The job will be requeued for retry. + def checkpoint!: () -> untyped + + # Set the cursor and interrupt the job if necessary. + def set!: (untyped cursor) -> untyped + + # Advance the cursor from the current or supplied value + # + # The cursor will be advanced by calling the +succ+ method on the cursor. + # An UnadvanceableCursorError error will be raised if the cursor does not implement +succ+. + def advance!: (?from: untyped?) -> untyped + + # Has this step been resumed from a previous job execution? + def resumed?: () -> untyped + + # Has the cursor been advanced during this job execution? + def advanced?: () -> untyped + + def to_a: () -> ::Array[untyped] + + def description: () -> ::String + + private + + attr_reader initial_cursor: untyped + + attr_reader job: untyped + end + end +end + +module ActiveJob + class Continuation + # Test helper for ActiveJob::Continuable jobs. + # + module TestHelper + include ::ActiveJob::TestHelper + + # Interrupt a job during a step. + # + # class MyJob < ApplicationJob + # include ActiveJob::Continuable + # + # cattr_accessor :items, default: [] + # def perform + # step :my_step, start: 1 do |step| + # (step.cursor..10).each do |i| + # items << i + # step.advance! + # end + # end + # end + # end + # + # test "interrupt job during step" do + # MyJob.perform_later + # interrupt_job_during_step(MyJob, :my_step, cursor: 6) { perform_enqueued_jobs } + # assert_equal [1, 2, 3, 4, 5], MyJob.items + # perform_enqueued_jobs + # assert_equal [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], MyJob.items + # end + def interrupt_job_during_step: (untyped job, untyped step, ?cursor: untyped?) { (?) -> untyped } -> untyped + + # Interrupt a job after a step. + # + # Note that there's no checkpoint after the final step so it won't be interrupted. + # + # class MyJob < ApplicationJob + # include ActiveJob::Continuable + # + # cattr_accessor :items, default: [] + # + # def perform + # step(:step_one) { items << 1 } + # step(:step_two) { items << 2 } + # step(:step_three) { items << 3 } + # step(:step_four) { items << 4 } + # end + # end + # + # test "interrupt job after step" do + # MyJob.perform_later + # interrupt_job_after_step(MyJob, :step_two) { perform_enqueued_jobs } + # assert_equal [1, 2], MyJob.items + # perform_enqueued_jobs + # assert_equal [1, 2, 3, 4], MyJob.items + # end + def interrupt_job_after_step: (untyped job, untyped step) { (?) -> untyped } -> untyped + + private + + def continuation_for: (untyped klass) -> untyped + + def during_step?: (untyped job, untyped step, ?cursor: untyped?) -> (untyped | nil) + + def after_step?: (untyped job, untyped step) -> (untyped | nil) + end + end +end + +module ActiveJob + class Continuation + module Validation + private + + def validate_step!: (untyped name) -> untyped + + def validate_step_symbol!: (untyped name) -> (untyped | nil) + + def validate_step_not_encountered!: (untyped name) -> (untyped | nil) + + def validate_step_not_nested!: (untyped name) -> (untyped | nil) + + def validate_step_resume_expected!: (untyped name) -> (untyped | nil) + + def validate_step_expected_order!: (untyped name) -> (untyped | nil) + + def raise_step_error!: (untyped message) -> untyped + end + end +end + +module ActiveJob + # Raised during job payload deserialization when it references an uninitialized job class. + class UnknownJobClassError < ::NameError[untyped] + def initialize: (untyped job_class_name) -> void + end + + # = Active Job \Core + # + # Provides general behavior that will be included into every Active Job + # object that inherits from ActiveJob::Base. + module Core + extend ::ActiveSupport::Concern + + # Job arguments + attr_accessor arguments: untyped + + attr_writer serialized_arguments: untyped + + # Time when the job should be performed + attr_accessor scheduled_at: untyped + + # Job Identifier + attr_accessor job_id: untyped + + # Queue in which the job will reside. + attr_writer queue_name: untyped + + # Priority that the job will have (lower is more priority). + attr_writer priority: untyped + + # ID optionally provided by adapter + attr_accessor provider_job_id: untyped + + # Number of times this job has been executed (which increments on every retry, like after an exception). + attr_accessor executions: untyped + + # Hash that contains the number of times this job handled errors for each specific retry_on declaration. + # Keys are the string representation of the exceptions listed in the retry_on declaration, + # while its associated value holds the number of executions where the corresponding retry_on + # declaration handled one of its listed exceptions. + attr_accessor exception_executions: untyped + + # I18n.locale to be used during the job. + attr_accessor locale: untyped + + # Timezone to be used during the job. + attr_accessor timezone: untyped + + # Track when a job was enqueued + attr_accessor enqueued_at: untyped + + # Track whether the adapter received the job successfully. + attr_writer successfully_enqueued: untyped + + def successfully_enqueued?: () -> untyped + + # Track any exceptions raised by the backend so callers can inspect the errors. + attr_accessor enqueue_error: untyped + + # These methods will be included into any Active Job object, adding + # helpers for de/serialization and creation of job instances. + module ClassMethods + # Creates a new job instance from a hash created with +serialize+ + def deserialize: (untyped job_data) -> untyped + + # Creates a job preconfigured with the given options. You can call + # perform_later with the job arguments to enqueue the job with the + # preconfigured options + # + # ==== Options + # * :wait - Enqueues the job with the specified delay + # * :wait_until - Enqueues the job at the time specified + # * :queue - Enqueues the job on the specified queue + # * :priority - Enqueues the job with the specified priority + # + # ==== Examples + # + # VideoJob.set(queue: :some_queue).perform_later(Video.last) + # VideoJob.set(wait: 5.minutes).perform_later(Video.last) + # VideoJob.set(wait_until: Time.now.tomorrow).perform_later(Video.last) + # VideoJob.set(queue: :some_queue, wait: 5.minutes).perform_later(Video.last) + # VideoJob.set(queue: :some_queue, wait_until: Time.now.tomorrow).perform_later(Video.last) + # VideoJob.set(queue: :some_queue, wait: 5.minutes, priority: 10).perform_later(Video.last) + def set: (?::Hash[untyped, untyped] options) -> ConfiguredJob + end + + # Creates a new job instance. Takes the arguments that will be + # passed to the perform method. + def initialize: (*untyped arguments) -> void + + # Returns a hash with the job data that can safely be passed to the + # queuing adapter. + def serialize: () -> ::Hash[::String, untyped] + + # Attaches the stored job data to the current instance. Receives a hash + # returned from +serialize+ + # + # ==== Examples + # + # class DeliverWebhookJob < ActiveJob::Base + # attr_writer :attempt_number + # + # def attempt_number + # @attempt_number ||= 0 + # end + # + # def serialize + # super.merge('attempt_number' => attempt_number + 1) + # end + # + # def deserialize(job_data) + # super + # self.attempt_number = job_data['attempt_number'] + # end + # + # rescue_from(Timeout::Error) do |exception| + # raise exception if attempt_number > 5 + # retry_job(wait: 10) + # end + # end + def deserialize: (untyped job_data) -> untyped + + # Configures the job with the given options. + def set: (?::Hash[untyped, untyped] options) -> self + + private + + def serialize_arguments_if_needed: (untyped arguments) -> untyped + + def deserialize_arguments_if_needed: () -> (untyped | nil) + + def serialize_arguments: (untyped arguments) -> untyped + + def deserialize_arguments: (untyped serialized_args) -> untyped + + def arguments_serialized?: () -> untyped + + def deserialize_time: (untyped time) -> untyped + end +end + +module ActiveJob + def self.deprecator: () -> untyped +end + +module ActiveJob + module EnqueueAfterTransactionCommit + def self.included: (untyped base) -> untyped + + module ActiveJobMethods + # Ensures perform_all_later respects each job's enqueue_after_transaction_commit configuration. + # Jobs with enqueue_after_transaction_commit set to true are deferred and enqueued only after the transaction commits; + # other jobs are enqueued immediately. This ensures enqueuing timing matches the per-job setting. + def perform_all_later: (*untyped jobs) -> nil + end + + private + + def raw_enqueue: () -> (self | untyped) + end +end + +module ActiveJob + # Can be raised by adapters if they wish to communicate to the caller a reason + # why the adapter was unexpectedly unable to enqueue a job. + class EnqueueError < ::StandardError + end + + module Enqueuing + extend ::ActiveSupport::Concern + + # Includes the +perform_later+ method for job initialization. + module ClassMethods + def enqueue_after_transaction_commit: () -> untyped + + def enqueue_after_transaction_commit=: (untyped value) -> untyped + + # Push a job onto the queue. By default the arguments must be either String, + # Integer, Float, NilClass, TrueClass, FalseClass, BigDecimal, Symbol, Date, + # Time, DateTime, ActiveSupport::TimeWithZone, ActiveSupport::Duration, + # Hash, ActiveSupport::HashWithIndifferentAccess, Array, Range, or + # GlobalID::Identification instances, although this can be extended by adding + # custom serializers. + # + # Returns an instance of the job class queued with arguments available in + # Job#arguments or +false+ if the enqueue did not succeed. + # + # After the attempted enqueue, the job will be yielded to an optional block. + # + # If Active Job is used conjointly with Active Record, and #perform_later is called + # inside an Active Record transaction, then the enqueue is implicitly deferred to after + # the transaction is committed, or dropped if it's rolled back. In such case #perform_later + # will return the job instance like if it was successfully enqueued, but will still return + # +false+ if a callback prevented the job from being enqueued. + # + # This behavior can be changed on a per job basis: + # + # class NotificationJob < ApplicationJob + # self.enqueue_after_transaction_commit = false + # end + def perform_later: (*untyped, **untyped) ?{ (untyped) -> untyped } -> untyped + + private + + def job_or_instantiate: (*untyped args) { (?) -> untyped } -> untyped + end + + # Enqueues the job to be performed by the queue adapter. + # + # ==== Options + # * :wait - Enqueues the job with the specified delay + # * :wait_until - Enqueues the job at the time specified + # * :queue - Enqueues the job on the specified queue + # * :priority - Enqueues the job with the specified priority + # + # ==== Examples + # + # my_job_instance.enqueue + # my_job_instance.enqueue wait: 5.minutes + # my_job_instance.enqueue queue: :important + # my_job_instance.enqueue wait_until: Date.tomorrow.midnight + # my_job_instance.enqueue priority: 10 + def enqueue: (?::Hash[untyped, untyped] options) -> untyped + + private + + def raw_enqueue: () -> untyped + + def _raw_enqueue: () -> untyped + end +end + +module ActiveJob + # Provides behavior for retrying and discarding jobs on exceptions. + module Exceptions + def after_discard_procs: () -> untyped + + def after_discard_procs?: () -> bool + + def after_discard_procs=: (untyped value) -> untyped + + extend ::ActiveSupport::Concern + + module ClassMethods + def retry_jitter: () -> untyped + + def retry_jitter=: (untyped value) -> untyped + + def after_discard_procs: () -> untyped + + def after_discard_procs=: (untyped value) -> untyped + + def after_discard_procs?: () -> bool + + # Catch the exception and reschedule job for re-execution after so many seconds, for a specific number of attempts. + # If the exception keeps getting raised beyond the specified number of attempts, the exception is allowed to + # bubble up to the underlying queuing system, which may have its own retry mechanism or place it in a + # holding queue for inspection. + # + # You can also pass a block that'll be invoked if the retry attempts fail for custom logic rather than letting + # the exception bubble up. This block is yielded with the job instance as the first and the error instance as the second parameter. + # + # +retry_on+ and +discard_on+ handlers are searched from bottom to top, and up the class hierarchy. The handler of the first class for + # which exception.is_a?(klass) holds true is the one invoked, if any. + # + # ==== Options + # * :wait - Re-enqueues the job with a delay specified either in seconds (default: 3 seconds), + # as a computing proc that takes the number of executions so far as an argument, or as a symbol reference of + # :polynomially_longer, which applies the wait algorithm of ((executions**4) + (Kernel.rand * (executions**4) * jitter)) + 2 + # (first wait ~3s, then ~18s, then ~83s, etc) + # * :attempts - Enqueues the job the specified number of times (default: 5 attempts) or a symbol reference of :unlimited + # to retry the job until it succeeds. The number of attempts includes the original job execution. + # * :queue - Re-enqueues the job on a different queue + # * :priority - Re-enqueues the job with a different priority + # * :jitter - A random delay of wait time used when calculating backoff. The default is 15% (0.15) which represents the upper bound of possible wait time (expressed as a percentage) + # * :report - Errors will be reported to the Rails.error reporter before being retried + # + # ==== Examples + # + # class RemoteServiceJob < ActiveJob::Base + # retry_on CustomAppException # defaults to ~3s wait, 5 attempts + # retry_on AnotherCustomAppException, wait: ->(executions) { executions * 2 } + # retry_on CustomInfrastructureException, wait: 5.minutes, attempts: :unlimited + # + # retry_on ActiveRecord::Deadlocked, wait: 5.seconds, attempts: 3 + # retry_on Net::OpenTimeout, Timeout::Error, wait: :polynomially_longer, attempts: 10 # retries at most 10 times for Net::OpenTimeout and Timeout::Error combined + # # To retry at most 10 times for each individual exception: + # # retry_on Net::OpenTimeout, wait: :polynomially_longer, attempts: 10 + # # retry_on Net::ReadTimeout, wait: 5.seconds, jitter: 0.30, attempts: 10 + # # retry_on Timeout::Error, wait: :polynomially_longer, attempts: 10 + # + # retry_on YetAnotherCustomAppException, report: true + # retry_on EvenWorseCustomAppException do |job, error| + # CustomErrorHandlingCode.handle(job, error) + # end + # + # def perform(*args) + # # Might raise CustomAppException, AnotherCustomAppException, or YetAnotherCustomAppException for something domain specific + # # Might raise ActiveRecord::Deadlocked when a local db deadlock is detected + # # Might raise Net::OpenTimeout or Timeout::Error when the remote service is down + # end + # end + def retry_on: (*untyped exceptions, ?wait: untyped, ?attempts: ::Integer, ?queue: untyped?, ?priority: untyped?, ?jitter: untyped, ?report: bool) ?{ (untyped, untyped) -> untyped } -> untyped + + # Discard the job with no attempts to retry, if the exception is raised. This is useful when the subject of the job, + # like an Active Record, is no longer available, and the job is thus no longer relevant. + # + # Passing the :report option reports the error through the error reporter before discarding the job. + # + # You can also pass a block that'll be invoked. This block is yielded with the job instance as the first and the error instance as the second parameter. + # + # +retry_on+ and +discard_on+ handlers are searched from bottom to top, and up the class hierarchy. The handler of the first class for + # which exception.is_a?(klass) holds true is the one invoked, if any. + # + # ==== Example + # + # class SearchIndexingJob < ActiveJob::Base + # discard_on ActiveJob::DeserializationError + # discard_on CustomAppException, report: true + # discard_on(AnotherCustomAppException) do |job, error| + # CustomErrorHandlingCode.handle(job, error) + # end + # + # def perform(record) + # # Will raise ActiveJob::DeserializationError if the record can't be deserialized + # # Might raise CustomAppException for something domain specific + # end + # end + def discard_on: (*untyped exceptions, ?report: bool) ?{ (untyped, untyped) -> untyped } -> untyped + + # A block to run when a job is about to be discarded for any reason. + # + # ==== Example + # + # class WorkJob < ActiveJob::Base + # after_discard do |job, exception| + # ExceptionNotifier.report(exception) + # end + # + # ... + # + # end + def after_discard: () { (?) -> untyped } -> untyped + end + + # Reschedules the job to be re-executed. This is useful in combination with + # {rescue_from}[rdoc-ref:ActiveSupport::Rescuable::ClassMethods#rescue_from]. + # When you rescue an exception from your job you can ask Active Job to retry + # performing your job. + # + # ==== Options + # * :wait - Enqueues the job with the specified delay in seconds + # * :wait_until - Enqueues the job at the time specified + # * :queue - Enqueues the job on the specified queue + # * :priority - Enqueues the job with the specified priority + # + # ==== Examples + # + # class SiteScraperJob < ActiveJob::Base + # rescue_from(ErrorLoadingSite) do + # retry_job queue: :low_priority + # end + # + # def perform(*args) + # # raise ErrorLoadingSite if cannot scrape + # end + # end + def retry_job: (?::Hash[untyped, untyped] options) -> untyped + + private + + JITTER_DEFAULT: untyped + + def determine_delay: (seconds_or_duration_or_algorithm: untyped, executions: untyped, ?jitter: untyped) -> untyped + + def determine_jitter_for_delay: (untyped delay, untyped jitter) -> (::Float | untyped) + + def executions_for: (untyped exceptions) -> untyped + + def run_after_discard_procs: (untyped exception) -> untyped + end +end + +module ActiveJob + module ExecutionState + def perform_now: () -> untyped + end +end + +module ActiveJob + # = Active Job \Execution + # + # Provides methods to execute jobs immediately, and wraps job execution so + # that exceptions configured with + # {rescue_from}[rdoc-ref:ActiveSupport::Rescuable::ClassMethods#rescue_from] + # are handled. + module Execution + extend ::ActiveSupport::Concern + + include ::ActiveSupport::Rescuable + + # Includes methods for executing and performing jobs instantly. + module ClassMethods + # Performs the job immediately. + # + # MyJob.perform_now("mike") + # + def perform_now: (*untyped, **untyped) ?{ (?) -> untyped } -> untyped + + def execute: (untyped job_data) -> untyped + end + + # Performs the job immediately. The job is not sent to the queuing adapter + # but directly executed by blocking the execution of others until it's finished. + # +perform_now+ returns the value of your job's +perform+ method. + # + # class MyJob < ActiveJob::Base + # def perform + # "Hello World!" + # end + # end + # + # puts MyJob.new(*args).perform_now # => "Hello World!" + def perform_now: () -> untyped + + def perform: (*untyped) -> untyped + + private + + def _perform_job: () -> untyped + end +end + +module ActiveJob + # Returns the currently loaded version of Active Job as a +Gem::Version+. + def self.gem_version: () -> Gem::Version + + module VERSION + MAJOR: 8 + + MINOR: 1 + + TINY: 4 + + PRE: nil + + STRING: untyped + end +end + +module ActiveJob + def self.instrument_enqueue_all: (untyped queue_adapter, untyped jobs) { (untyped) -> untyped } -> untyped + + module Instrumentation + extend ::ActiveSupport::Concern + + def perform_now: () -> untyped + + def instrument: (untyped operation, ?::Hash[untyped, untyped] payload) { (?) -> untyped } -> untyped + + private + + def _perform_job: () -> untyped + + def halted_callback_hook: (*untyped) -> untyped + end +end + +module ActiveJob + class LogSubscriber < ::ActiveSupport::LogSubscriber + def self.backtrace_cleaner: () -> untyped + def self.backtrace_cleaner=: (untyped value) -> untyped + def self.backtrace_cleaner?: () -> bool + def backtrace_cleaner: () -> untyped + def backtrace_cleaner?: () -> bool + def backtrace_cleaner=: (untyped value) -> untyped + + def enqueue: (untyped event) -> untyped + + def enqueue_at: (untyped event) -> untyped + + def enqueue_all: (untyped event) -> untyped + + def perform_start: (untyped event) -> untyped + + def perform: (untyped event) -> untyped + + def enqueue_retry: (untyped event) -> untyped + + def retry_stopped: (untyped event) -> untyped + + def discard: (untyped event) -> untyped + + def interrupt: (untyped event) -> untyped + + def resume: (untyped event) -> untyped + + def step_skipped: (untyped event) -> untyped + + def step_started: (untyped event) -> untyped + + def step: (untyped event) -> untyped + + private + + def queue_name: (untyped event) -> untyped + + def args_info: (untyped job) -> (untyped | "") + + def format: (untyped arg) -> untyped + + def scheduled_at: (untyped event) -> untyped + + def logger: () -> untyped + + def info: (?untyped? progname) { (?) -> untyped } -> (nil | untyped) + + def error: (?untyped? progname) { (?) -> untyped } -> (nil | untyped) + + def log_enqueue_source: () -> untyped + + def enqueue_source_location: () -> untyped + + def enqueued_jobs_message: (untyped adapter, untyped enqueued_jobs) -> ::String + end +end + +module ActiveJob + module Logging + def logger: () -> untyped + + def logger=: (untyped value) -> untyped + + extend ::ActiveSupport::Concern + + def perform_now: () -> untyped + + private + + def tag_logger: (*untyped tags) { () -> untyped } -> untyped + + def logger_tagged_by_active_job?: () -> untyped + end +end + +module ActiveJob + def self.adapter_name: (untyped adapter) -> (untyped | ::String) + + # = Active Job Queue adapter + # + # The +ActiveJob::QueueAdapter+ module is used to load the + # correct adapter. The default queue adapter is +:async+, + # which loads the ActiveJob::QueueAdapters::AsyncAdapter. + module QueueAdapter + def queue_adapter: (*untyped, **untyped) ?{ (*untyped) -> untyped } -> untyped + + extend ::ActiveSupport::Concern + + # Includes the setter method for changing the active queue adapter. + module ClassMethods + def _queue_adapter_name: () -> untyped + + def _queue_adapter_name=: (untyped value) -> untyped + + def _queue_adapter: () -> untyped + + def _queue_adapter=: (untyped value) -> untyped + + # Returns the backend queue provider. The default queue adapter + # is +:async+. See QueueAdapters for more information. + def queue_adapter: () -> untyped + + # Returns string denoting the name of the configured queue adapter. + # By default returns "async". + def queue_adapter_name: () -> untyped + + # Specify the backend queue provider. The default queue adapter + # is the +:async+ queue. See QueueAdapters for more + # information. + def queue_adapter=: (untyped name_or_adapter) -> untyped + + private + + def assign_adapter: (untyped adapter_name, untyped queue_adapter) -> untyped + + QUEUE_ADAPTER_METHODS: ::Array[:enqueue | :enqueue_at] + + def queue_adapter?: (untyped object) -> untyped + end + end +end + +module ActiveJob + # = Active Job adapters + # + # Active Job has adapters for the following queuing backends: + # + # * {Backburner}[https://github.com/nesquena/backburner] + # * {Delayed Job}[https://github.com/collectiveidea/delayed_job] + # * {Que}[https://github.com/chanks/que] + # * {queue_classic}[https://github.com/QueueClassic/queue_classic] + # * {Resque}[https://github.com/resque/resque] + # * {Sidekiq}[https://sidekiq.org] + # * {Sneakers}[https://github.com/jondot/sneakers] + # * Please Note: We are not accepting pull requests for new adapters. See the {README}[link:files/activejob/README_md.html] for more details. + # + # For testing and development Active Job has three built-in adapters: + # + # * {Active Job Async}[https://api.rubyonrails.org/classes/ActiveJob/QueueAdapters/AsyncAdapter.html] + # * {Active Job Inline}[https://api.rubyonrails.org/classes/ActiveJob/QueueAdapters/InlineAdapter.html] + # * {Active Job Test}[https://api.rubyonrails.org/classes/ActiveJob/QueueAdapters/TestAdapter.html] + # + # === Backends Features + # + # | | Async | Queues | Delayed | Priorities | Timeout | Retries | + # |-------------------|-------|--------|------------|------------|---------|---------| + # | Backburner | Yes | Yes | Yes | Yes | Job | Global | + # | Delayed Job | Yes | Yes | Yes | Job | Global | Global | + # | Que | Yes | Yes | Yes | Job | No | Job | + # | queue_classic | Yes | Yes | Yes* | No | No | No | + # | Resque | Yes | Yes | Yes (Gem) | Queue | Global | Yes | + # | Sidekiq | Yes | Yes | Yes | Queue | No | Job | + # | Sneakers | Yes | Yes | No | Queue | Queue | No | + # | Active Job Async | Yes | Yes | Yes | No | No | No | + # | Active Job Inline | No | Yes | N/A | N/A | N/A | N/A | + # | Active Job Test | No | Yes | N/A | N/A | N/A | N/A | + # + # ==== Async + # + # Yes: The Queue Adapter has the ability to run the job in a non-blocking manner. + # It either runs on a separate or forked process, or on a different thread. + # + # No: The job is run in the same process. + # + # ==== Queues + # + # Yes: Jobs may set which queue they are run in with queue_as or by using the set + # method. + # + # ==== Delayed + # + # Yes: The adapter will run the job in the future through perform_later. + # + # (Gem): An additional gem is required to use perform_later with this adapter. + # + # No: The adapter will run jobs at the next opportunity and cannot use perform_later. + # + # N/A: The adapter does not support queuing. + # + # NOTE: + # queue_classic supports job scheduling since version 3.1. + # For older versions you can use the queue_classic-later gem. + # + # ==== Priorities + # + # The order in which jobs are processed can be configured differently depending + # on the adapter. + # + # Job: Any class inheriting from the adapter may set the priority on the job + # object relative to other jobs. + # + # Queue: The adapter can set the priority for job queues, when setting a queue + # with Active Job this will be respected. + # + # Yes: Allows the priority to be set on the job object, at the queue level or + # as default configuration option. + # + # No: The adapter does not allow the priority of jobs to be configured. + # + # N/A: The adapter does not support queuing, and therefore sorting them. + # + # ==== Timeout + # + # When a job will stop after the allotted time. + # + # Job: The timeout can be set for each instance of the job class. + # + # Queue: The timeout is set for all jobs on the queue. + # + # Global: The adapter is configured that all jobs have a maximum run time. + # + # No: The adapter does not allow the timeout of jobs to be configured. + # + # N/A: This adapter does not run in a separate process, and therefore timeout + # is unsupported. + # + # ==== Retries + # + # Job: The number of retries can be set per instance of the job class. + # + # Yes: The Number of retries can be configured globally, for each instance or + # on the queue. This adapter may also present failed instances of the job class + # that can be restarted. + # + # Global: The adapter has a global number of retries. + # + # No: The adapter does not allow the number of retries to be configured. + # + # N/A: The adapter does not run in a separate process, and therefore doesn't + # support retries. + # + module QueueAdapters + extend ::ActiveSupport::Autoload + + ADAPTER: "Adapter" + + # Returns adapter for specified name. + # + # ActiveJob::QueueAdapters.lookup(:sidekiq) + # # => ActiveJob::QueueAdapters::SidekiqAdapter + def self.lookup: (untyped name) -> untyped + end +end + +module ActiveJob + module QueueAdapters + # = Active Job Abstract Adapter + # + # Active Job supports multiple job queue systems. ActiveJob::QueueAdapters::AbstractAdapter + # forms the abstraction layer which makes this possible. + class AbstractAdapter + attr_accessor stopping: untyped + + def enqueue: (untyped job) -> untyped + + def enqueue_at: (untyped job, untyped timestamp) -> untyped + + def stopping?: () -> untyped + end + end +end + +module ActiveJob + module QueueAdapters + # = Active Job Async adapter + # + # The Async adapter runs jobs with an in-process thread pool. + # + # This is the default queue adapter. It's well-suited for dev/test since + # it doesn't need an external infrastructure, but it's a poor fit for + # production since it drops pending jobs on restart. + # + # To use this adapter, set queue adapter to +:async+: + # + # config.active_job.queue_adapter = :async + # + # To configure the adapter's thread pool, instantiate the adapter and + # pass your own config: + # + # config.active_job.queue_adapter = ActiveJob::QueueAdapters::AsyncAdapter.new \ + # min_threads: 1, + # max_threads: 2 * Concurrent.processor_count, + # idletime: 600.seconds + # + # The adapter uses a {Concurrent Ruby}[https://github.com/ruby-concurrency/concurrent-ruby] thread pool to schedule and execute + # jobs. Since jobs share a single thread pool, long-running jobs will block + # short-lived jobs. Fine for dev/test; bad for production. + class AsyncAdapter < ::ActiveJob::QueueAdapters::AbstractAdapter + # See {Concurrent::ThreadPoolExecutor}[https://ruby-concurrency.github.io/concurrent-ruby/master/Concurrent/ThreadPoolExecutor.html] for executor options. + def initialize: (**untyped executor_options) -> void + + def enqueue: (untyped job) -> untyped + + def enqueue_at: (untyped job, untyped timestamp) -> untyped + + # Gracefully stop processing jobs. Finishes in-progress work and handles + # any new jobs following the executor's fallback policy (`caller_runs`). + # Waits for termination by default. Pass `wait: false` to continue. + def shutdown: (?wait: bool) -> untyped + + # Used for our test suite. + def immediate=: (untyped immediate) -> untyped + + # Note that we don't actually need to serialize the jobs since we're + # performing them in-process, but we do so anyway for parity with other + # adapters and deployment environments. Otherwise, serialization bugs + # may creep in undetected. + class JobWrapper + def initialize: (untyped job) -> void + + def perform: () -> untyped + end + + class Scheduler + DEFAULT_EXECUTOR_OPTIONS: { min_threads: 0, max_threads: untyped, auto_terminate: true, idletime: 60, max_queue: 0, fallback_policy: :caller_runs } + + attr_accessor immediate: untyped + + def initialize: (**untyped options) -> void + + def enqueue: (untyped job, queue_name: untyped) -> untyped + + def enqueue_at: (untyped job, untyped timestamp, queue_name: untyped) -> untyped + + def shutdown: (?wait: bool) -> untyped + + def executor: () -> untyped + end + end + end +end + +module ActiveJob + module QueueAdapters + # = Backburner adapter for Active Job + # + # Backburner is a beanstalkd-powered job queue that can handle a very + # high volume of jobs. You create background jobs and place them on + # multiple work queues to be processed later. Read more about + # Backburner {here}[https://github.com/nesquena/backburner]. + # + # To use Backburner set the queue_adapter config to +:backburner+. + # + # Rails.application.config.active_job.queue_adapter = :backburner + class BackburnerAdapter < ::ActiveJob::QueueAdapters::AbstractAdapter + def enqueue: (untyped job) -> untyped + + def enqueue_at: (untyped job, untyped timestamp) -> untyped + + class JobWrapper + def self.perform: (untyped job_data) -> untyped + end + end + end +end + +module ActiveJob + module QueueAdapters + # = Delayed Job adapter for Active Job + # + # Delayed::Job (or DJ) encapsulates the common pattern of asynchronously + # executing longer tasks in the background. Although DJ can have many + # storage backends, one of the most used is based on Active Record. + # Read more about Delayed Job {here}[https://github.com/collectiveidea/delayed_job]. + # + # To use Delayed Job, set the queue_adapter config to +:delayed_job+. + # + # Rails.application.config.active_job.queue_adapter = :delayed_job + class DelayedJobAdapter < ::ActiveJob::QueueAdapters::AbstractAdapter + def enqueue: (untyped job) -> untyped + + def enqueue_at: (untyped job, untyped timestamp) -> untyped + + class JobWrapper + attr_accessor job_data: untyped + + def initialize: (untyped job_data) -> void + + def display_name: () -> ::String + + def perform: () -> untyped + + private + + def log_arguments?: () -> untyped + end + end + end +end + +module ActiveJob + module QueueAdapters + # = Active Job Inline adapter + # + # When enqueuing jobs with the Inline adapter the job will be executed + # immediately. + # + # To use the Inline set the queue_adapter config to +:inline+. + # + # Rails.application.config.active_job.queue_adapter = :inline + class InlineAdapter < ::ActiveJob::QueueAdapters::AbstractAdapter + def enqueue: (untyped job) -> untyped + + def enqueue_at: (*untyped) -> untyped + end + end +end + +module ActiveJob + module QueueAdapters + # = queue_classic adapter for Active Job + # + # queue_classic provides a simple interface to a PostgreSQL-backed message + # queue. queue_classic specializes in concurrent locking and minimizing + # database load while providing a simple, intuitive developer experience. + # queue_classic assumes that you are already using PostgreSQL in your + # production environment and that adding another dependency (e.g. redis, + # beanstalkd, 0mq) is undesirable. + # + # Read more about queue_classic {here}[https://github.com/QueueClassic/queue_classic]. + # + # To use queue_classic set the queue_adapter config to +:queue_classic+. + # + # Rails.application.config.active_job.queue_adapter = :queue_classic + class QueueClassicAdapter < ::ActiveJob::QueueAdapters::AbstractAdapter + def enqueue: (untyped job) -> untyped + + def enqueue_at: (untyped job, untyped timestamp) -> untyped + + # Builds a +QC::Queue+ object to schedule jobs on. + # + # If you have a custom +QC::Queue+ subclass you'll need to subclass + # +ActiveJob::QueueAdapters::QueueClassicAdapter+ and override the + # build_queue method. + def build_queue: (untyped queue_name) -> QC::Queue + + class JobWrapper + def self.perform: (untyped job_data) -> untyped + end + end + end +end + +module ActiveJob + module QueueAdapters + # = Resque adapter for Active Job + # + # Resque (pronounced like "rescue") is a Redis-backed library for creating + # background jobs, placing those jobs on multiple queues, and processing + # them later. + # + # Read more about Resque {here}[https://github.com/resque/resque]. + # + # To use Resque set the queue_adapter config to +:resque+. + # + # Rails.application.config.active_job.queue_adapter = :resque + class ResqueAdapter < ::ActiveJob::QueueAdapters::AbstractAdapter + def enqueue: (untyped job) -> untyped + + def enqueue_at: (untyped job, untyped timestamp) -> untyped + + class JobWrapper + def self.perform: (untyped job_data) -> untyped + end + end + end +end + +module ActiveJob + module QueueAdapters + # = Sidekiq adapter for Active Job + # + # Simple, efficient background processing for Ruby. Sidekiq uses threads to + # handle many jobs at the same time in the same process. It does not + # require \Rails but will integrate tightly with it to make background + # processing dead simple. + # + # Read more about Sidekiq {here}[http://sidekiq.org]. + # + # To use Sidekiq set the queue_adapter config to +:sidekiq+. + # + # Rails.application.config.active_job.queue_adapter = :sidekiq + class SidekiqAdapter < ::ActiveJob::QueueAdapters::AbstractAdapter + def initialize: (*untyped) -> void + + def check_adapter: () -> untyped + + def enqueue_all: (untyped jobs) -> untyped + + class JobWrapper + end + end + end +end + +module ActiveJob + module QueueAdapters + # = Sneakers adapter for Active Job + # + # A high-performance RabbitMQ background processing framework for Ruby. + # Sneakers is being used in production for both I/O and CPU intensive + # workloads, and have achieved the goals of high-performance and + # 0-maintenance, as designed. + # + # Read more about Sneakers {here}[https://github.com/jondot/sneakers]. + # + # To use Sneakers set the queue_adapter config to +:sneakers+. + # + # Rails.application.config.active_job.queue_adapter = :sneakers + class SneakersAdapter < ::ActiveJob::QueueAdapters::AbstractAdapter + def initialize: () -> void + + def enqueue: (untyped job) -> untyped + + def enqueue_at: (untyped job, untyped timestamp) -> untyped + + class JobWrapper + include ::Sneakers::Worker + + def work: (untyped msg) -> untyped + end + end + end +end + +module ActiveJob + module QueueAdapters + # = Test adapter for Active Job + # + # The test adapter should be used only in testing. Along with + # ActiveJob::TestCase and ActiveJob::TestHelper + # it makes a great tool to test your \Rails application. + # + # To use the test adapter set +queue_adapter+ config to +:test+. + # + # Rails.application.config.active_job.queue_adapter = :test + class TestAdapter < ::ActiveJob::QueueAdapters::AbstractAdapter + attr_accessor perform_enqueued_jobs: untyped + + attr_accessor perform_enqueued_at_jobs: untyped + + attr_accessor filter: untyped + + attr_accessor reject: untyped + + attr_accessor queue: untyped + + attr_accessor at: untyped + + attr_accessor stopping: untyped + + attr_writer enqueued_jobs: untyped + + attr_writer performed_jobs: untyped + + # Provides a store of all the enqueued jobs with the TestAdapter so you can check them. + def enqueued_jobs: () -> untyped + + # Provides a store of all the performed jobs with the TestAdapter so you can check them. + def performed_jobs: () -> untyped + + def enqueue: (untyped job) -> untyped + + def enqueue_at: (untyped job, untyped timestamp) -> untyped + + def stopping?: () -> untyped + + private + + def job_to_hash: (untyped job, ?::Hash[untyped, untyped] extras) -> untyped + + def perform_or_enqueue: (untyped perform, untyped job, untyped job_data) -> untyped + + def filtered?: (untyped job) -> untyped + + def filtered_time?: (untyped job) -> (untyped | nil) + + def filtered_queue?: (untyped job) -> (untyped | nil) + + def filtered_job_class?: (untyped job) -> untyped + + def filter_as_proc: (untyped filter) -> untyped + end + end +end + +module ActiveJob + module QueueName + def queue_name_prefix: () -> untyped + + def queue_name_prefix?: () -> bool + + def queue_name_prefix=: (untyped value) -> untyped + + extend ::ActiveSupport::Concern + + # Includes the ability to override the default queue name and prefix. + module ClassMethods + def self.default_queue_name: () -> untyped + + def self.default_queue_name=: (untyped value) -> untyped + + def default_queue_name: () -> untyped + + def default_queue_name=: (untyped value) -> untyped + + def queue_name: () -> untyped + + def queue_name=: (untyped value) -> untyped + + def queue_name?: () -> bool + + def queue_name_delimiter: () -> untyped + + def queue_name_delimiter=: (untyped value) -> untyped + + def queue_name_delimiter?: () -> bool + + def queue_name_prefix: () -> untyped + + def queue_name_prefix=: (untyped value) -> untyped + + def queue_name_prefix?: () -> bool + + # Specifies the name of the queue to process the job on. + # + # class PublishToFeedJob < ActiveJob::Base + # queue_as :feeds + # + # def perform(post) + # post.to_feed! + # end + # end + # + # Can be given a block that will evaluate in the context of the job + # so that a dynamic queue name can be applied: + # + # class PublishToFeedJob < ApplicationJob + # queue_as do + # post = self.arguments.first + # + # if post.paid? + # :paid_feeds + # else + # :feeds + # end + # end + # + # def perform(post) + # post.to_feed! + # end + # end + def queue_as: (?untyped? part_name) ?{ (?) -> untyped } -> untyped + + def queue_name_from_part: (untyped part_name) -> untyped + end + + # Returns the name of the queue the job will be run on. + def queue_name: () -> untyped + end +end + +module ActiveJob + module QueuePriority + extend ::ActiveSupport::Concern + + # Includes the ability to override the default queue priority. + module ClassMethods + def self.default_priority: () -> untyped + + def self.default_priority=: (untyped value) -> untyped + + def default_priority: () -> untyped + + def default_priority=: (untyped value) -> untyped + + def priority: () -> untyped + + def priority=: (untyped value) -> untyped + + def priority?: () -> bool + + # Specifies the priority of the queue to create the job with. + # + # class PublishToFeedJob < ActiveJob::Base + # queue_with_priority 50 + # + # def perform(post) + # post.to_feed! + # end + # end + # + # Can be given a block that will evaluate in the context of the job + # so that a dynamic priority can be applied: + # + # class PublishToFeedJob < ApplicationJob + # queue_with_priority do + # post = self.arguments.first + # + # if post.paid? + # 10 + # else + # 50 + # end + # end + # + # def perform(post) + # post.to_feed! + # end + # end + def queue_with_priority: (?untyped? priority) ?{ (?) -> untyped } -> untyped + end + + # Returns the priority that the job will be created with + def priority: () -> untyped + end +end + +module ActiveJob + # = Active Job Railtie + class Railtie < ::Rails::Railtie + end +end + +module ActiveJob + # = Active Job \Serializers + # + # The +ActiveJob::Serializers+ module is used to store a list of known serializers + # and to add new ones. It also has helpers to serialize/deserialize objects. + module Serializers + extend ::ActiveSupport::Autoload + + # Returns serialized representative of the passed object. + # Will look up through all known serializers. + # Raises ActiveJob::SerializationError if it can't find a proper serializer. + def self.serialize: (untyped argument) -> untyped + + # Returns deserialized object. + # Will look up through all known serializers. + # If no serializer found will raise ArgumentError. + def self.deserialize: (untyped argument) -> untyped + + # Returns list of known serializers. + attr_reader self.serializers: untyped + + def self.serializers=: (untyped serializers) -> untyped + + # Adds new serializers to a list of known serializers. + def self.add_serializers: (*untyped new_serializers) -> untyped + + def self.add_new_serializers: (untyped new_serializers) -> untyped + + def self.index_serializers: (untyped new_serializers) -> untyped + + # :nodoc: + OBJECT_SERIALIZER_KEY: "_aj_serialized" + end +end + +module ActiveJob + module Serializers + class ActionControllerParametersSerializer < ::ActiveJob::Serializers::ObjectSerializer + def serialize: (untyped argument) -> untyped + + def deserialize: (untyped hash) -> untyped + + def serialize?: (untyped argument) -> untyped + + def klass: () -> (untyped | nil) + end + end +end + +module ActiveJob + module Serializers + class BigDecimalSerializer < ::ActiveJob::Serializers::ObjectSerializer + def serialize: (untyped big_decimal) -> untyped + + def deserialize: (untyped hash) -> untyped + + def klass: () -> untyped + end + end +end + +module ActiveJob + module Serializers + class DateSerializer < ::ActiveJob::Serializers::ObjectSerializer + def serialize: (untyped date) -> untyped + + def deserialize: (untyped hash) -> untyped + + def klass: () -> untyped + end + end +end + +module ActiveJob + module Serializers + class DateTimeSerializer < ::ActiveJob::Serializers::TimeObjectSerializer + def deserialize: (untyped hash) -> untyped + + def klass: () -> untyped + end + end +end + +module ActiveJob + module Serializers + class DurationSerializer < ::ActiveJob::Serializers::ObjectSerializer + def serialize: (untyped duration) -> untyped + + def deserialize: (untyped hash) -> untyped + + def klass: () -> untyped + end + end +end + +module ActiveJob + module Serializers + class ModuleSerializer < ::ActiveJob::Serializers::ObjectSerializer + def serialize: (untyped constant) -> untyped + + def deserialize: (untyped hash) -> untyped + + def klass: () -> untyped + end + end +end + +module ActiveJob + module Serializers + # Base class for serializing and deserializing custom objects. + # + # Example: + # + # class MoneySerializer < ActiveJob::Serializers::ObjectSerializer + # def serialize(money) + # super("amount" => money.amount, "currency" => money.currency) + # end + # + # def deserialize(hash) + # Money.new(hash["amount"], hash["currency"]) + # end + # + # def klass + # Money + # end + # end + class ObjectSerializer + def self.serialize?: (*untyped, **untyped) ?{ (*untyped) -> untyped } -> untyped + def self.serialize: (*untyped, **untyped) ?{ (*untyped) -> untyped } -> untyped + def self.deserialize: (*untyped, **untyped) ?{ (*untyped) -> untyped } -> untyped + + include ::Singleton + + def initialize: () -> void + + # Determines if an argument should be serialized by a serializer. + def serialize?: (untyped argument) -> untyped + + # Serializes an argument to a JSON primitive type. + def serialize: (untyped hash) -> untyped + + # Deserializes an argument from a JSON primitive type. + def deserialize: (untyped hash) -> untyped + end + end +end + +module ActiveJob + module Serializers + class RangeSerializer < ::ActiveJob::Serializers::ObjectSerializer + def serialize: (untyped range) -> untyped + + def deserialize: (untyped hash) -> untyped + + def klass: () -> untyped + end + end +end + +module ActiveJob + module Serializers + class SymbolSerializer < ::ActiveJob::Serializers::ObjectSerializer + def serialize: (untyped argument) -> untyped + + def deserialize: (untyped argument) -> untyped + + def klass: () -> untyped + end + end +end + +module ActiveJob + module Serializers + class TimeObjectSerializer < ::ActiveJob::Serializers::ObjectSerializer + NANO_PRECISION: 9 + + def serialize: (untyped time) -> untyped + end + end +end + +module ActiveJob + module Serializers + class TimeSerializer < ::ActiveJob::Serializers::TimeObjectSerializer + def deserialize: (untyped hash) -> untyped + + def klass: () -> untyped + end + end +end + +module ActiveJob + module Serializers + class TimeWithZoneSerializer < ::ActiveJob::Serializers::ObjectSerializer + NANO_PRECISION: 9 + + def serialize: (untyped time_with_zone) -> untyped + + def deserialize: (untyped hash) -> untyped + + def klass: () -> untyped + end + end +end + +module ActiveJob + class StructuredEventSubscriber < ::ActiveSupport::StructuredEventSubscriber + def enqueue: (untyped event) -> untyped + + def enqueue_at: (untyped event) -> untyped + + def enqueue_all: (untyped event) -> untyped + + def perform_start: (untyped event) -> untyped + + def perform: (untyped event) -> untyped + + def enqueue_retry: (untyped event) -> untyped + + def retry_stopped: (untyped event) -> untyped + + def discard: (untyped event) -> untyped + + def interrupt: (untyped event) -> untyped + + def resume: (untyped event) -> untyped + + def step_skipped: (untyped event) -> untyped + + def step_started: (untyped event) -> untyped + + def step: (untyped event) -> untyped + end +end + +module ActiveJob + class TestCase < ::ActiveSupport::TestCase + include ::ActiveJob::TestHelper + end +end + +module ActiveJob + # Provides helper methods for testing Active Job + module TestHelper + def enqueued_jobs: (*untyped, **untyped) ?{ (*untyped) -> untyped } -> untyped + + def enqueued_jobs=: (*untyped, **untyped) ?{ (*untyped) -> untyped } -> untyped + + def performed_jobs: (*untyped, **untyped) ?{ (*untyped) -> untyped } -> untyped + + def performed_jobs=: (*untyped, **untyped) ?{ (*untyped) -> untyped } -> untyped + + include ::ActiveSupport::Testing::Assertions + + module TestQueueAdapter + extend ::ActiveSupport::Concern + + module ClassMethods + def _test_adapter: () -> untyped + + def _test_adapter=: (untyped value) -> untyped + + def queue_adapter: () -> untyped + + def disable_test_adapter: () -> untyped + + def enable_test_adapter: (untyped test_adapter) -> untyped + end + end + + def before_setup: () -> untyped + + def after_teardown: () -> untyped + + # Returns a queue adapter instance to use with all Active Job test helpers. + # By default, returns an instance of ActiveJob::QueueAdapters::TestAdapter. + # Override this method to specify a different adapter. The adapter must + # implement the same interface as ActiveJob::QueueAdapters::TestAdapter. + def queue_adapter_for_test: () -> nil + + # Asserts that the number of enqueued jobs matches the given number. + # + # def test_jobs + # assert_enqueued_jobs 0 + # HelloJob.perform_later('david') + # assert_enqueued_jobs 1 + # HelloJob.perform_later('abdelkader') + # assert_enqueued_jobs 2 + # end + # + # If a block is passed, asserts that the block will cause the specified number of + # jobs to be enqueued. + # + # def test_jobs_again + # assert_enqueued_jobs 1 do + # HelloJob.perform_later('cristian') + # end + # + # assert_enqueued_jobs 2 do + # HelloJob.perform_later('aaron') + # HelloJob.perform_later('rafael') + # end + # end + # + # Asserts the number of times a specific job was enqueued by passing +:only+ option. + # + # def test_logging_job + # assert_enqueued_jobs 1, only: LoggingJob do + # LoggingJob.perform_later + # HelloJob.perform_later('jeremy') + # end + # end + # + # Asserts the number of times a job except specific class was enqueued by passing +:except+ option. + # + # def test_logging_job + # assert_enqueued_jobs 1, except: HelloJob do + # LoggingJob.perform_later + # HelloJob.perform_later('jeremy') + # end + # end + # + # +:only+ and +:except+ options accept Class, Array of Class, or Proc. When passed a Proc, + # a hash containing the job's class and it's argument are passed as argument. + # + # Asserts the number of times a job is enqueued to a specific queue by passing +:queue+ option. + # + # def test_logging_job + # assert_enqueued_jobs 2, queue: 'default' do + # LoggingJob.perform_later + # HelloJob.perform_later('elfassy') + # end + # end + def assert_enqueued_jobs: (untyped number, ?only: untyped?, ?except: untyped?, ?queue: untyped?) ?{ (?) -> untyped } -> untyped + + # Asserts that no jobs have been enqueued. + # + # def test_jobs + # assert_no_enqueued_jobs + # HelloJob.perform_later('jeremy') + # assert_enqueued_jobs 1 + # end + # + # If a block is passed, asserts that the block will not cause any job to be enqueued. + # + # def test_jobs_again + # assert_no_enqueued_jobs do + # # No job should be enqueued from this block + # end + # end + # + # Asserts that no jobs of a specific kind are enqueued by passing +:only+ option. + # + # def test_no_logging + # assert_no_enqueued_jobs only: LoggingJob do + # HelloJob.perform_later('jeremy') + # end + # end + # + # Asserts that no jobs except specific class are enqueued by passing +:except+ option. + # + # def test_no_logging + # assert_no_enqueued_jobs except: HelloJob do + # HelloJob.perform_later('jeremy') + # end + # end + # + # +:only+ and +:except+ options accept Class, Array of Class, or Proc. When passed a Proc, + # a hash containing the job's class and it's argument are passed as argument. + # + # Asserts that no jobs are enqueued to a specific queue by passing +:queue+ option + # + # def test_no_logging + # assert_no_enqueued_jobs queue: 'default' do + # LoggingJob.set(queue: :some_queue).perform_later + # end + # end + # + # Note: This assertion is simply a shortcut for: + # + # assert_enqueued_jobs 0, &block + def assert_no_enqueued_jobs: (?only: untyped?, ?except: untyped?, ?queue: untyped?) { (?) -> untyped } -> untyped + + # Asserts that the number of performed jobs matches the given number. + # If no block is passed, perform_enqueued_jobs + # must be called around or after the job call. + # + # def test_jobs + # assert_performed_jobs 0 + # + # perform_enqueued_jobs do + # HelloJob.perform_later('xavier') + # end + # assert_performed_jobs 1 + # + # HelloJob.perform_later('yves') + # + # perform_enqueued_jobs + # + # assert_performed_jobs 2 + # end + # + # If a block is passed, asserts that the block will cause the specified number of + # jobs to be performed. + # + # def test_jobs_again + # assert_performed_jobs 1 do + # HelloJob.perform_later('robin') + # end + # + # assert_performed_jobs 2 do + # HelloJob.perform_later('carlos') + # HelloJob.perform_later('sean') + # end + # end + # + # This method also supports filtering. If the +:only+ option is specified, + # then only the listed job(s) will be performed. + # + # def test_hello_job + # assert_performed_jobs 1, only: HelloJob do + # HelloJob.perform_later('jeremy') + # LoggingJob.perform_later + # end + # end + # + # Also if the +:except+ option is specified, + # then the job(s) except specific class will be performed. + # + # def test_hello_job + # assert_performed_jobs 1, except: LoggingJob do + # HelloJob.perform_later('jeremy') + # LoggingJob.perform_later + # end + # end + # + # An array may also be specified, to support testing multiple jobs. + # + # def test_hello_and_logging_jobs + # assert_nothing_raised do + # assert_performed_jobs 2, only: [HelloJob, LoggingJob] do + # HelloJob.perform_later('jeremy') + # LoggingJob.perform_later('stewie') + # RescueJob.perform_later('david') + # end + # end + # end + # + # A proc may also be specified. When passed a Proc, the job's instance will be passed as argument. + # + # def test_hello_and_logging_jobs + # assert_nothing_raised do + # assert_performed_jobs(1, only: ->(job) { job.is_a?(HelloJob) }) do + # HelloJob.perform_later('jeremy') + # LoggingJob.perform_later('stewie') + # RescueJob.perform_later('david') + # end + # end + # end + # + # If the +:queue+ option is specified, + # then only the job(s) enqueued to a specific queue will be performed. + # + # def test_assert_performed_jobs_with_queue_option + # assert_performed_jobs 1, queue: :some_queue do + # HelloJob.set(queue: :some_queue).perform_later("jeremy") + # HelloJob.set(queue: :other_queue).perform_later("bogdan") + # end + # end + def assert_performed_jobs: (untyped number, ?only: untyped?, ?except: untyped?, ?queue: untyped?) ?{ (?) -> untyped } -> untyped + + # Asserts that no jobs have been performed. + # + # def test_jobs + # assert_no_performed_jobs + # + # perform_enqueued_jobs do + # HelloJob.perform_later('matthew') + # assert_performed_jobs 1 + # end + # end + # + # If a block is passed, asserts that the block will not cause any job to be performed. + # + # def test_jobs_again + # assert_no_performed_jobs do + # # No job should be performed from this block + # end + # end + # + # The block form supports filtering. If the +:only+ option is specified, + # then only the listed job(s) will not be performed. + # + # def test_no_logging + # assert_no_performed_jobs only: LoggingJob do + # HelloJob.perform_later('jeremy') + # end + # end + # + # Also if the +:except+ option is specified, + # then the job(s) except specific class will not be performed. + # + # def test_no_logging + # assert_no_performed_jobs except: HelloJob do + # HelloJob.perform_later('jeremy') + # end + # end + # + # +:only+ and +:except+ options accept Class, Array of Class, or Proc. When passed a Proc, + # an instance of the job will be passed as argument. + # + # If the +:queue+ option is specified, + # then only the job(s) enqueued to a specific queue will not be performed. + # + # def test_assert_no_performed_jobs_with_queue_option + # assert_no_performed_jobs queue: :some_queue do + # HelloJob.set(queue: :other_queue).perform_later("jeremy") + # end + # end + # + # Note: This assertion is simply a shortcut for: + # + # assert_performed_jobs 0, &block + def assert_no_performed_jobs: (?only: untyped?, ?except: untyped?, ?queue: untyped?) { (?) -> untyped } -> untyped + + # Asserts that the job has been enqueued with the given arguments. + # + # def test_assert_enqueued_with + # MyJob.perform_later(1,2,3) + # assert_enqueued_with(job: MyJob, args: [1,2,3]) + # + # MyJob.set(wait_until: Date.tomorrow.noon, queue: "my_queue").perform_later + # assert_enqueued_with(at: Date.tomorrow.noon, queue: "my_queue") + # end + # + # For keyword arguments, specify them as a hash inside an array: + # + # def test_assert_enqueued_with_keyword_arguments + # MyJob.perform_later(arg1: 'value1', arg2: 'value2') + # assert_enqueued_with(job: MyJob, args: [{ arg1: 'value1', arg2: 'value2' }]) + # end + # + # The given arguments may also be specified as matcher procs that return a + # boolean value indicating whether a job's attribute meets certain criteria. + # + # For example, a proc can be used to match a range of times: + # + # def test_assert_enqueued_with + # at_matcher = ->(job_at) { (Date.yesterday..Date.tomorrow).cover?(job_at) } + # + # MyJob.set(wait_until: Date.today.noon).perform_later + # + # assert_enqueued_with(job: MyJob, at: at_matcher) + # end + # + # A proc can also be used to match a subset of a job's args: + # + # def test_assert_enqueued_with + # args_matcher = ->(job_args) { job_args[0].key?(:foo) } + # + # MyJob.perform_later(foo: "bar", other_arg: "No need to check in the test") + # + # assert_enqueued_with(job: MyJob, args: args_matcher) + # end + # + # If a block is passed, asserts that the block will cause the job to be + # enqueued with the given arguments. + # + # def test_assert_enqueued_with + # assert_enqueued_with(job: MyJob, args: [1,2,3]) do + # MyJob.perform_later(1,2,3) + # end + # + # assert_enqueued_with(job: MyJob, at: Date.tomorrow.noon) do + # MyJob.set(wait_until: Date.tomorrow.noon).perform_later + # end + # end + def assert_enqueued_with: (?job: untyped?, ?args: untyped?, ?at: untyped?, ?queue: untyped?, ?priority: untyped?) ?{ (?) -> untyped } -> untyped + + # Asserts that the job has been performed with the given arguments. + # + # def test_assert_performed_with + # MyJob.perform_later(1,2,3) + # + # perform_enqueued_jobs + # + # assert_performed_with(job: MyJob, args: [1,2,3]) + # + # MyJob.set(wait_until: Date.tomorrow.noon, queue: "my_queue").perform_later + # + # perform_enqueued_jobs + # + # assert_performed_with(at: Date.tomorrow.noon, queue: "my_queue") + # end + # + # The given arguments may also be specified as matcher procs that return a + # boolean value indicating whether a job's attribute meets certain criteria. + # + # For example, a proc can be used to match a range of times: + # + # def test_assert_performed_with + # at_matcher = ->(job_at) { (Date.yesterday..Date.tomorrow).cover?(job_at) } + # + # MyJob.set(wait_until: Date.today.noon).perform_later + # + # perform_enqueued_jobs + # + # assert_performed_with(job: MyJob, at: at_matcher) + # end + # + # A proc can also be used to match a subset of a job's args: + # + # def test_assert_performed_with + # args_matcher = ->(job_args) { job_args[0].key?(:foo) } + # + # MyJob.perform_later(foo: "bar", other_arg: "No need to check in the test") + # + # perform_enqueued_jobs + # + # assert_performed_with(job: MyJob, args: args_matcher) + # end + # + # If a block is passed, that block performs all of the jobs that were + # enqueued throughout the duration of the block and asserts that + # the job has been performed with the given arguments in the block. + # + # def test_assert_performed_with + # assert_performed_with(job: MyJob, args: [1,2,3]) do + # MyJob.perform_later(1,2,3) + # end + # + # assert_performed_with(job: MyJob, at: Date.tomorrow.noon) do + # MyJob.set(wait_until: Date.tomorrow.noon).perform_later + # end + # end + def assert_performed_with: (?job: untyped?, ?args: untyped?, ?at: untyped?, ?queue: untyped?, ?priority: untyped?) ?{ (?) -> untyped } -> untyped + + # Performs all enqueued jobs. If a block is given, performs all of the jobs + # that were enqueued throughout the duration of the block. If a block is + # not given, performs all of the enqueued jobs up to this point in the test. + # + # def test_perform_enqueued_jobs + # perform_enqueued_jobs do + # MyJob.perform_later(1, 2, 3) + # end + # assert_performed_jobs 1 + # end + # + # def test_perform_enqueued_jobs_without_block + # MyJob.perform_later(1, 2, 3) + # + # perform_enqueued_jobs + # + # assert_performed_jobs 1 + # end + # + # This method also supports filtering. If the +:only+ option is specified, + # then only the listed job(s) will be performed. + # + # def test_perform_enqueued_jobs_with_only + # perform_enqueued_jobs(only: MyJob) do + # MyJob.perform_later(1, 2, 3) # will be performed + # HelloJob.perform_later(1, 2, 3) # will not be performed + # end + # assert_performed_jobs 1 + # end + # + # Also if the +:except+ option is specified, + # then the job(s) except specific class will be performed. + # + # def test_perform_enqueued_jobs_with_except + # perform_enqueued_jobs(except: HelloJob) do + # MyJob.perform_later(1, 2, 3) # will be performed + # HelloJob.perform_later(1, 2, 3) # will not be performed + # end + # assert_performed_jobs 1 + # end + # + # +:only+ and +:except+ options accept Class, Array of Class, or Proc. When passed a Proc, + # an instance of the job will be passed as argument. + # + # If the +:queue+ option is specified, + # then only the job(s) enqueued to a specific queue will be performed. + # + # def test_perform_enqueued_jobs_with_queue + # perform_enqueued_jobs queue: :some_queue do + # MyJob.set(queue: :some_queue).perform_later(1, 2, 3) # will be performed + # HelloJob.set(queue: :other_queue).perform_later(1, 2, 3) # will not be performed + # end + # assert_performed_jobs 1 + # end + # + # If the +:at+ option is specified, then only jobs that have been enqueued + # to run at or before the given time will be performed. This includes jobs + # that have been enqueued without a time. + # + # If queue_adapter_for_test is overridden to return a different adapter, + # +perform_enqueued_jobs+ will merely execute the block. + def perform_enqueued_jobs: (?only: untyped?, ?except: untyped?, ?queue: untyped?, ?at: untyped?) ?{ (?) -> untyped } -> untyped + + # Accesses the queue_adapter set by ActiveJob::Base. + # + # def test_assert_job_has_custom_queue_adapter_set + # assert_instance_of CustomQueueAdapter, HelloJob.queue_adapter + # end + def queue_adapter: () -> untyped + + private + + def require_active_job_test_adapter!: (untyped method) -> (untyped | nil) + + def using_test_adapter?: () -> untyped + + def clear_enqueued_jobs: () -> (untyped | nil) + + def clear_performed_jobs: () -> (untyped | nil) + + def jobs_with: (untyped jobs, ?only: untyped?, ?except: untyped?, ?queue: untyped?, ?at: untyped?) ?{ (untyped) -> untyped } -> untyped + + def filter_as_proc: (untyped filter) -> untyped + + def enqueued_jobs_with: (?only: untyped?, ?except: untyped?, ?queue: untyped?, ?at: untyped?) { (?) -> untyped } -> untyped + + def performed_jobs_with: (?only: untyped?, ?except: untyped?, ?queue: untyped?) { (?) -> untyped } -> untyped + + def flush_enqueued_jobs: (?only: untyped?, ?except: untyped?, ?queue: untyped?, ?at: untyped?) -> untyped + + def prepare_args_for_assertion: (untyped args) -> untyped + + def deserialize_args_for_assertion: (untyped job) -> untyped + + def instantiate_job: (untyped payload, ?skip_deserialize_arguments: bool) -> untyped + + def queue_adapter_changed_jobs: () -> untyped + + def validate_option: (?only: untyped?, ?except: untyped?) -> (untyped | nil) + end +end + +module ActiveJob + # Returns the currently loaded version of Active Job as a +Gem::Version+. + def self.version: () -> untyped +end From f7a72d36ed4ba93ee8b8152179fc8f5023993cae Mon Sep 17 00:00:00 2001 From: Akira Matsuda Date: Tue, 13 Oct 2026 19:04:24 +0900 Subject: [PATCH 03/22] activejob: Remove fake types for the Que and Sucker Punch adapters Rails no longer has QueAdapter or SuckerPunchAdapter, so nothing refers to Que::Job or SuckerPunch::Job. Co-Authored-By: Claude Opus 5.5 (1M context) --- gems/activejob/8.1/patch.rbs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/gems/activejob/8.1/patch.rbs b/gems/activejob/8.1/patch.rbs index 0752327fd..a9029291f 100644 --- a/gems/activejob/8.1/patch.rbs +++ b/gems/activejob/8.1/patch.rbs @@ -1,9 +1,3 @@ -# Remove the fake type if the real type is available -module Que - class Job - end -end - # Remove the fake type if the real type is available class QC class Queue @@ -21,9 +15,3 @@ module Sneakers module Worker end end - -# Remove the fake type if the real type is available -module SuckerPunch - module Job - end -end From d5e30999a62ff075fe2bd03090f8fd6085c984f8 Mon Sep 17 00:00:00 2001 From: Akira Matsuda Date: Tue, 13 Oct 2026 19:05:49 +0900 Subject: [PATCH 04/22] activejob: Add logger and log_arguments to ActiveJob::Base Logging defines them on the job class with cattr_accessor and class_attribute in its included block, which rbs prototype doesn't see. Co-Authored-By: Claude Opus 5.5 (1M context) --- gems/activejob/8.1/_test/activejob.rb | 3 +++ gems/activejob/8.1/activejob.rbs | 15 ++++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/gems/activejob/8.1/_test/activejob.rb b/gems/activejob/8.1/_test/activejob.rb index d0521bf37..f89e54697 100644 --- a/gems/activejob/8.1/_test/activejob.rb +++ b/gems/activejob/8.1/_test/activejob.rb @@ -23,3 +23,6 @@ def around_task SendMessageJob.perform_now SendMessageJob.set(queue: :another_queue).perform_later(flag: false) + +SendMessageJob.logger = SendMessageJob.logger +SendMessageJob.log_arguments = false diff --git a/gems/activejob/8.1/activejob.rbs b/gems/activejob/8.1/activejob.rbs index e5447b4a9..f3f4c5364 100644 --- a/gems/activejob/8.1/activejob.rbs +++ b/gems/activejob/8.1/activejob.rbs @@ -59,4 +59,17 @@ module ActiveJob end end end -end \ No newline at end of file +end + +# Methods rbs prototype doesn't see, which aren't generated from the source +module ActiveJob + class Base + # cattr_accessor and class_attribute in Logging's included block + def self.logger: () -> untyped + def self.logger=: (untyped value) -> untyped + + def self.log_arguments: () -> untyped + def self.log_arguments=: (untyped value) -> untyped + def self.log_arguments?: () -> bool + end +end From 976d9512da3dc1fdae763599909671805f14acdb Mon Sep 17 00:00:00 2001 From: Akira Matsuda Date: Tue, 13 Oct 2026 19:06:06 +0900 Subject: [PATCH 05/22] activejob: Type return values of commonly used job methods job_id, arguments, executions, queue_name, priority, and the other job attributes, enqueue, and perform_later, which returns the job or false when it couldn't be enqueued. Co-Authored-By: Claude Opus 5.5 (1M context) --- gems/activejob/8.1/_test/activejob.rb | 23 +++++++ gems/activejob/8.1/activejob-generated.rbs | 79 ---------------------- gems/activejob/8.1/activejob.rbs | 43 ++++++++++++ 3 files changed, 66 insertions(+), 79 deletions(-) diff --git a/gems/activejob/8.1/_test/activejob.rb b/gems/activejob/8.1/_test/activejob.rb index f89e54697..1e9739315 100644 --- a/gems/activejob/8.1/_test/activejob.rb +++ b/gems/activejob/8.1/_test/activejob.rb @@ -26,3 +26,26 @@ def around_task SendMessageJob.logger = SendMessageJob.logger SendMessageJob.log_arguments = false + +job = SendMessageJob.new(1, "two") +job.job_id.upcase +job.arguments.size +job.executions + 1 +job.exception_executions.each_value(&:succ) +job.queue_name.upcase +job.priority&.succ +job.locale&.upcase +job.timezone&.upcase +job.enqueued_at&.iso8601 +job.enqueue_error&.message +job.successfully_enqueued? + +if (enqueued = SendMessageJob.perform_later(1)) + enqueued.job_id.upcase +end +if (enqueued = SendMessageJob.set(wait: 1.minute).perform_later(1) { |j| j.job_id }) + enqueued.provider_job_id +end +if (enqueued = job.enqueue(queue: :low)) + enqueued.job_id.upcase +end diff --git a/gems/activejob/8.1/activejob-generated.rbs b/gems/activejob/8.1/activejob-generated.rbs index ac0eef7a2..bbc45df27 100644 --- a/gems/activejob/8.1/activejob-generated.rbs +++ b/gems/activejob/8.1/activejob-generated.rbs @@ -334,8 +334,6 @@ module ActiveJob def initialize: (untyped job_class, ?::Hash[untyped, untyped] options) -> void def perform_now: (*untyped, **untyped) ?{ (?) -> untyped } -> untyped - - def perform_later: (*untyped, **untyped) ?{ (untyped) -> untyped } -> untyped end end @@ -804,17 +802,11 @@ module ActiveJob module Core extend ::ActiveSupport::Concern - # Job arguments - attr_accessor arguments: untyped - attr_writer serialized_arguments: untyped # Time when the job should be performed attr_accessor scheduled_at: untyped - # Job Identifier - attr_accessor job_id: untyped - # Queue in which the job will reside. attr_writer queue_name: untyped @@ -824,32 +816,9 @@ module ActiveJob # ID optionally provided by adapter attr_accessor provider_job_id: untyped - # Number of times this job has been executed (which increments on every retry, like after an exception). - attr_accessor executions: untyped - - # Hash that contains the number of times this job handled errors for each specific retry_on declaration. - # Keys are the string representation of the exceptions listed in the retry_on declaration, - # while its associated value holds the number of executions where the corresponding retry_on - # declaration handled one of its listed exceptions. - attr_accessor exception_executions: untyped - - # I18n.locale to be used during the job. - attr_accessor locale: untyped - - # Timezone to be used during the job. - attr_accessor timezone: untyped - - # Track when a job was enqueued - attr_accessor enqueued_at: untyped - # Track whether the adapter received the job successfully. attr_writer successfully_enqueued: untyped - def successfully_enqueued?: () -> untyped - - # Track any exceptions raised by the backend so callers can inspect the errors. - attr_accessor enqueue_error: untyped - # These methods will be included into any Active Job object, adding # helpers for de/serialization and creation of job instances. module ClassMethods @@ -968,53 +937,11 @@ module ActiveJob def enqueue_after_transaction_commit=: (untyped value) -> untyped - # Push a job onto the queue. By default the arguments must be either String, - # Integer, Float, NilClass, TrueClass, FalseClass, BigDecimal, Symbol, Date, - # Time, DateTime, ActiveSupport::TimeWithZone, ActiveSupport::Duration, - # Hash, ActiveSupport::HashWithIndifferentAccess, Array, Range, or - # GlobalID::Identification instances, although this can be extended by adding - # custom serializers. - # - # Returns an instance of the job class queued with arguments available in - # Job#arguments or +false+ if the enqueue did not succeed. - # - # After the attempted enqueue, the job will be yielded to an optional block. - # - # If Active Job is used conjointly with Active Record, and #perform_later is called - # inside an Active Record transaction, then the enqueue is implicitly deferred to after - # the transaction is committed, or dropped if it's rolled back. In such case #perform_later - # will return the job instance like if it was successfully enqueued, but will still return - # +false+ if a callback prevented the job from being enqueued. - # - # This behavior can be changed on a per job basis: - # - # class NotificationJob < ApplicationJob - # self.enqueue_after_transaction_commit = false - # end - def perform_later: (*untyped, **untyped) ?{ (untyped) -> untyped } -> untyped - private def job_or_instantiate: (*untyped args) { (?) -> untyped } -> untyped end - # Enqueues the job to be performed by the queue adapter. - # - # ==== Options - # * :wait - Enqueues the job with the specified delay - # * :wait_until - Enqueues the job at the time specified - # * :queue - Enqueues the job on the specified queue - # * :priority - Enqueues the job with the specified priority - # - # ==== Examples - # - # my_job_instance.enqueue - # my_job_instance.enqueue wait: 5.minutes - # my_job_instance.enqueue queue: :important - # my_job_instance.enqueue wait_until: Date.tomorrow.midnight - # my_job_instance.enqueue priority: 10 - def enqueue: (?::Hash[untyped, untyped] options) -> untyped - private def raw_enqueue: () -> untyped @@ -1917,9 +1844,6 @@ module ActiveJob def queue_name_from_part: (untyped part_name) -> untyped end - - # Returns the name of the queue the job will be run on. - def queue_name: () -> untyped end end @@ -1973,9 +1897,6 @@ module ActiveJob # end def queue_with_priority: (?untyped? priority) ?{ (?) -> untyped } -> untyped end - - # Returns the priority that the job will be created with - def priority: () -> untyped end end diff --git a/gems/activejob/8.1/activejob.rbs b/gems/activejob/8.1/activejob.rbs index f3f4c5364..12bc91130 100644 --- a/gems/activejob/8.1/activejob.rbs +++ b/gems/activejob/8.1/activejob.rbs @@ -73,3 +73,46 @@ module ActiveJob def self.log_arguments?: () -> bool end end + +# Return types of commonly used methods +module ActiveJob + module Core + attr_accessor arguments: Array[untyped] + + attr_accessor job_id: String + + attr_accessor executions: Integer + + attr_accessor exception_executions: Hash[String, Integer] + + attr_accessor locale: String? + + attr_accessor timezone: String? + + attr_accessor enqueued_at: Time? + + def successfully_enqueued?: () -> bool? + + attr_accessor enqueue_error: EnqueueError? + end + + module QueueName + def queue_name: () -> String + end + + module QueuePriority + def priority: () -> Integer? + end + + module Enqueuing + def enqueue: (?Hash[Symbol, untyped] options) -> (self | false) + + module ClassMethods + def perform_later: (*untyped, **untyped) ?{ (ActiveJob::Base job) -> void } -> (ActiveJob::Base | false) + end + end + + class ConfiguredJob + def perform_later: (*untyped, **untyped) ?{ (ActiveJob::Base job) -> void } -> (ActiveJob::Base | false) + end +end From a33924345e72e1930c23a0a705fd935cf4b701bd Mon Sep 17 00:00:00 2001 From: Akira Matsuda Date: Tue, 13 Oct 2026 19:08:14 +0900 Subject: [PATCH 06/22] activejob: Make blocks optional where calling without one is valid rbs prototype marks a block as required unless the method checks it. These forward the block or don't use it: the callbacks with method names (before_perform :foo), assert_no_enqueued_jobs and assert_no_performed_jobs without a block, and a few internal methods. Co-Authored-By: Claude Opus 5.5 (1M context) --- gems/activejob/8.1/_test/activejob.rb | 9 ++++++ gems/activejob/8.1/_test/send_message_job.rbs | 4 +++ gems/activejob/8.1/activejob-generated.rbs | 28 +++++++++---------- 3 files changed, 27 insertions(+), 14 deletions(-) diff --git a/gems/activejob/8.1/_test/activejob.rb b/gems/activejob/8.1/_test/activejob.rb index 1e9739315..546c415a9 100644 --- a/gems/activejob/8.1/_test/activejob.rb +++ b/gems/activejob/8.1/_test/activejob.rb @@ -49,3 +49,12 @@ def around_task if (enqueued = job.enqueue(queue: :low)) enqueued.job_id.upcase end + +class SendMessageJobTest < ActiveJob::TestCase + def test_enqueue + assert_no_enqueued_jobs + SendMessageJob.perform_later + assert_enqueued_jobs 1 + assert_no_performed_jobs + end +end diff --git a/gems/activejob/8.1/_test/send_message_job.rbs b/gems/activejob/8.1/_test/send_message_job.rbs index 70b2b929b..67fd4d848 100644 --- a/gems/activejob/8.1/_test/send_message_job.rbs +++ b/gems/activejob/8.1/_test/send_message_job.rbs @@ -5,3 +5,7 @@ class SendMessageJob < ActiveJob::Base def around_task: () -> void end + +class SendMessageJobTest < ActiveJob::TestCase + def test_enqueue: () -> void +end diff --git a/gems/activejob/8.1/activejob-generated.rbs b/gems/activejob/8.1/activejob-generated.rbs index bbc45df27..8e068f5a5 100644 --- a/gems/activejob/8.1/activejob-generated.rbs +++ b/gems/activejob/8.1/activejob-generated.rbs @@ -222,7 +222,7 @@ module ActiveJob # end # end # - def before_perform: (*untyped filters) { (?) -> untyped } -> untyped + def before_perform: (*untyped filters) ?{ (?) -> untyped } -> untyped # Defines a callback that will get called right after the # job's perform method has finished. @@ -239,7 +239,7 @@ module ActiveJob # end # end # - def after_perform: (*untyped filters) { (?) -> untyped } -> untyped + def after_perform: (*untyped filters) ?{ (?) -> untyped } -> untyped # Defines a callback that will get called around the job's perform method. # @@ -270,7 +270,7 @@ module ActiveJob # end # end # - def around_perform: (*untyped filters) { (?) -> untyped } -> untyped + def around_perform: (*untyped filters) ?{ (?) -> untyped } -> untyped # Defines a callback that will get called right before the # job is enqueued. @@ -287,7 +287,7 @@ module ActiveJob # end # end # - def before_enqueue: (*untyped filters) { (?) -> untyped } -> untyped + def before_enqueue: (*untyped filters) ?{ (?) -> untyped } -> untyped # Defines a callback that will get called right after the # job is enqueued. @@ -305,7 +305,7 @@ module ActiveJob # end # end # - def after_enqueue: (*untyped filters) { (?) -> untyped } -> untyped + def after_enqueue: (*untyped filters) ?{ (?) -> untyped } -> untyped # Defines a callback that will get called around the enqueuing # of the job. @@ -324,7 +324,7 @@ module ActiveJob # end # end # - def around_enqueue: (*untyped filters) { (?) -> untyped } -> untyped + def around_enqueue: (*untyped filters) ?{ (?) -> untyped } -> untyped end end end @@ -939,7 +939,7 @@ module ActiveJob private - def job_or_instantiate: (*untyped args) { (?) -> untyped } -> untyped + def job_or_instantiate: (*untyped args) ?{ (?) -> untyped } -> untyped end private @@ -1176,7 +1176,7 @@ module ActiveJob def perform_now: () -> untyped - def instrument: (untyped operation, ?::Hash[untyped, untyped] payload) { (?) -> untyped } -> untyped + def instrument: (untyped operation, ?::Hash[untyped, untyped] payload) ?{ (?) -> untyped } -> untyped private @@ -1233,9 +1233,9 @@ module ActiveJob def logger: () -> untyped - def info: (?untyped? progname) { (?) -> untyped } -> (nil | untyped) + def info: (?untyped? progname) ?{ (?) -> untyped } -> (nil | untyped) - def error: (?untyped? progname) { (?) -> untyped } -> (nil | untyped) + def error: (?untyped? progname) ?{ (?) -> untyped } -> (nil | untyped) def log_enqueue_source: () -> untyped @@ -2287,7 +2287,7 @@ module ActiveJob # Note: This assertion is simply a shortcut for: # # assert_enqueued_jobs 0, &block - def assert_no_enqueued_jobs: (?only: untyped?, ?except: untyped?, ?queue: untyped?) { (?) -> untyped } -> untyped + def assert_no_enqueued_jobs: (?only: untyped?, ?except: untyped?, ?queue: untyped?) ?{ (?) -> untyped } -> untyped # Asserts that the number of performed jobs matches the given number. # If no block is passed, perform_enqueued_jobs @@ -2429,7 +2429,7 @@ module ActiveJob # Note: This assertion is simply a shortcut for: # # assert_performed_jobs 0, &block - def assert_no_performed_jobs: (?only: untyped?, ?except: untyped?, ?queue: untyped?) { (?) -> untyped } -> untyped + def assert_no_performed_jobs: (?only: untyped?, ?except: untyped?, ?queue: untyped?) ?{ (?) -> untyped } -> untyped # Asserts that the job has been enqueued with the given arguments. # @@ -2627,9 +2627,9 @@ module ActiveJob def filter_as_proc: (untyped filter) -> untyped - def enqueued_jobs_with: (?only: untyped?, ?except: untyped?, ?queue: untyped?, ?at: untyped?) { (?) -> untyped } -> untyped + def enqueued_jobs_with: (?only: untyped?, ?except: untyped?, ?queue: untyped?, ?at: untyped?) ?{ (?) -> untyped } -> untyped - def performed_jobs_with: (?only: untyped?, ?except: untyped?, ?queue: untyped?) { (?) -> untyped } -> untyped + def performed_jobs_with: (?only: untyped?, ?except: untyped?, ?queue: untyped?) ?{ (?) -> untyped } -> untyped def flush_enqueued_jobs: (?only: untyped?, ?except: untyped?, ?queue: untyped?, ?at: untyped?) -> untyped From 476eeb355e1ab80f6bf2b3577d163954e0c9a39f Mon Sep 17 00:00:00 2001 From: Akira Matsuda Date: Tue, 13 Oct 2026 19:16:20 +0900 Subject: [PATCH 07/22] activejob: Extend Callbacks and Rescuable class methods in ActiveJob::Base ActiveJob::Callbacks and ActiveJob::Exceptions include ActiveSupport::Callbacks and ActiveSupport::Rescuable, so job classes have rescue_from and set_callback. Co-Authored-By: Claude Opus 5.5 (1M context) --- gems/activejob/8.1/_test/activejob.rb | 2 ++ gems/activejob/8.1/activejob.rbs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/gems/activejob/8.1/_test/activejob.rb b/gems/activejob/8.1/_test/activejob.rb index 546c415a9..4905b10b1 100644 --- a/gems/activejob/8.1/_test/activejob.rb +++ b/gems/activejob/8.1/_test/activejob.rb @@ -8,6 +8,8 @@ class SendMessageJob < ActiveJob::Base around_perform :around_task + rescue_from(ActiveJob::DeserializationError) { |exception| exception.message } + def perform(*args) # Do something later end diff --git a/gems/activejob/8.1/activejob.rbs b/gems/activejob/8.1/activejob.rbs index 12bc91130..e86e99ec1 100644 --- a/gems/activejob/8.1/activejob.rbs +++ b/gems/activejob/8.1/activejob.rbs @@ -8,6 +8,8 @@ module ActiveJob extend ActiveJob::QueueAdapter::ClassMethods extend ActiveJob::QueueName::ClassMethods extend ActiveJob::QueuePriority::ClassMethods + extend ActiveSupport::Callbacks::ClassMethods + extend ActiveSupport::Rescuable::ClassMethods def self.queue_as: (*untyped) -> void | ... From 925b5c5d4d5787fd43f185a2a2b0ce2cd7a77464 Mon Sep 17 00:00:00 2001 From: Akira Matsuda Date: Tue, 13 Oct 2026 19:23:45 +0900 Subject: [PATCH 08/22] activestorage: Copy hand-written RBS and test from 7.0 to 8.1 Co-Authored-By: Claude Opus 5.5 (1M context) --- gems/activestorage/8.1/_test/test.rb | 8 + gems/activestorage/8.1/_test/test.rbs | 2 + .../app/models/active_storage/attachment.rbs | 12 ++ .../8.1/app/models/active_storage/blob.rbs | 186 ++++++++++++++++++ .../8.1/app/models/active_storage/current.rbs | 8 + .../8.1/app/models/active_storage/record.rbs | 2 + gems/activestorage/8.1/lib/attached.rbs | 11 ++ gems/activestorage/8.1/lib/attached/many.rbs | 38 ++++ gems/activestorage/8.1/lib/attached/model.rbs | 107 ++++++++++ gems/activestorage/8.1/lib/attached/one.rbs | 46 +++++ gems/activestorage/8.1/lib/engine.rbs | 10 + gems/activestorage/8.1/lib/reflection.rbs | 32 +++ gems/activestorage/8.1/manifest.yaml | 2 + 13 files changed, 464 insertions(+) create mode 100644 gems/activestorage/8.1/_test/test.rb create mode 100644 gems/activestorage/8.1/_test/test.rbs create mode 100644 gems/activestorage/8.1/app/models/active_storage/attachment.rbs create mode 100644 gems/activestorage/8.1/app/models/active_storage/blob.rbs create mode 100644 gems/activestorage/8.1/app/models/active_storage/current.rbs create mode 100644 gems/activestorage/8.1/app/models/active_storage/record.rbs create mode 100644 gems/activestorage/8.1/lib/attached.rbs create mode 100644 gems/activestorage/8.1/lib/attached/many.rbs create mode 100644 gems/activestorage/8.1/lib/attached/model.rbs create mode 100644 gems/activestorage/8.1/lib/attached/one.rbs create mode 100644 gems/activestorage/8.1/lib/engine.rbs create mode 100644 gems/activestorage/8.1/lib/reflection.rbs create mode 100644 gems/activestorage/8.1/manifest.yaml diff --git a/gems/activestorage/8.1/_test/test.rb b/gems/activestorage/8.1/_test/test.rb new file mode 100644 index 000000000..52fd70531 --- /dev/null +++ b/gems/activestorage/8.1/_test/test.rb @@ -0,0 +1,8 @@ +class User < ActiveRecord::Base + has_one_attached :one_image + has_many_attached :many_image +end + +user = User.new +one = ActiveStorage::Attached::One.new("one_image", user) +one.url diff --git a/gems/activestorage/8.1/_test/test.rbs b/gems/activestorage/8.1/_test/test.rbs new file mode 100644 index 000000000..4a57cf079 --- /dev/null +++ b/gems/activestorage/8.1/_test/test.rbs @@ -0,0 +1,2 @@ +class User < ActiveRecord::Base +end diff --git a/gems/activestorage/8.1/app/models/active_storage/attachment.rbs b/gems/activestorage/8.1/app/models/active_storage/attachment.rbs new file mode 100644 index 000000000..53f33eb8b --- /dev/null +++ b/gems/activestorage/8.1/app/models/active_storage/attachment.rbs @@ -0,0 +1,12 @@ +# Attachments associate records with blobs. Usually that's a one record-many blobs relationship, +# but it is possible to associate many different records with the same blob. A foreign-key constraint +# on the attachments table prevents blobs from being purged if they’re still attached to any records. +# +# Attachments also have access to all methods from {ActiveStorage::Blob}[rdoc-ref:ActiveStorage::Blob]. +class ActiveStorage::Attachment < ActiveStorage::Record + # Synchronously deletes the attachment and {purges the blob}[rdoc-ref:ActiveStorage::Blob#purge]. + def purge: () -> untyped + + # Deletes the attachment and {enqueues a background job}[rdoc-ref:ActiveStorage::Blob#purge_later] to purge the blob. + def purge_later: () -> untyped +end diff --git a/gems/activestorage/8.1/app/models/active_storage/blob.rbs b/gems/activestorage/8.1/app/models/active_storage/blob.rbs new file mode 100644 index 000000000..45a691258 --- /dev/null +++ b/gems/activestorage/8.1/app/models/active_storage/blob.rbs @@ -0,0 +1,186 @@ +# A blob is a record that contains the metadata about a file and a key for where that file resides on the service. +# Blobs can be created in two ways: +# +# 1. Ahead of the file being uploaded server-side to the service, via create_and_upload!. A rewindable +# io with the file contents must be available at the server for this operation. +# 2. Ahead of the file being directly uploaded client-side to the service, via create_before_direct_upload!. +# +# The first option doesn't require any client-side JavaScript integration, and can be used by any other back-end +# service that deals with files. The second option is faster, since you're not using your own server as a staging +# point for uploads, and can work with deployments like Heroku that do not provide large amounts of disk space. +# +# Blobs are intended to be immutable in as-so-far as their reference to a specific file goes. You're allowed to +# update a blob's metadata on a subsequent pass, but you should not update the key or change the uploaded file. +# If you need to create a derivative or otherwise change the blob, simply create a new blob and purge the old one. +class ActiveStorage::Blob < ActiveStorage::Record + # We use constant paths in the following include calls to avoid a gotcha of + # classic mode: If the parent application defines a top-level Analyzable, for + # example, and ActiveStorage::Blob::Analyzable is not yet loaded, a bare + # + # include Analyzable + # + # would resolve to the top-level one, const_missing would not be triggered, + # and therefore ActiveStorage::Blob::Analyzable would not be autoloaded. + # + # By using qualified names, we ensure const_missing is invoked if needed. + # Please, note that Ruby 2.5 or newer is required, so Object is not checked + # when looking up the ancestors of ActiveStorage::Blob. + # + # Zeitwerk mode does not have this gotcha. If we ever drop classic mode, this + # can be simplified, bare constant names would just work. + # include ActiveStorage::Blob::Analyzable + + # include ActiveStorage::Blob::Identifiable + + # include ActiveStorage::Blob::Representable + + MINIMUM_TOKEN_LENGTH: ::Integer + + # You can use the signed ID of a blob to refer to it on the client side without fear of tampering. + # This is particularly helpful for direct uploads where the client-side needs to refer to the blob + # that was created ahead of the upload itself on form submission. + # + # The signed ID is also used to create stable URLs for the blob through the BlobsController. + def self.find_signed: (untyped id, ?record: untyped? record, ?purpose: ::Symbol purpose) -> untyped + + # Works like +find_signed+, but will raise an +ActiveSupport::MessageVerifier::InvalidSignature+ + # exception if the +signed_id+ has either expired, has a purpose mismatch, is for another record, + # or has been tampered with. It will also raise an +ActiveRecord::RecordNotFound+ exception if + # the valid signed id can't find a record. + def self.find_signed!: (untyped id, ?record: untyped? record, ?purpose: ::Symbol purpose) -> untyped + + def self.build_after_upload: (io: untyped io, filename: untyped filename, ?content_type: untyped? content_type, ?metadata: untyped? metadata, ?service_name: untyped? service_name, ?identify: bool identify, ?record: untyped? record) -> untyped + + def self.build_after_unfurling: (io: untyped io, filename: untyped filename, ?key: untyped? key, ?content_type: untyped? content_type, ?metadata: untyped? metadata, ?service_name: untyped? service_name, ?identify: bool identify, ?record: untyped? record) -> untyped + + def self.create_after_unfurling!: (io: untyped io, filename: untyped filename, ?key: untyped? key, ?content_type: untyped? content_type, ?metadata: untyped? metadata, ?service_name: untyped? service_name, ?identify: bool identify, ?record: untyped? record) -> untyped + + # Creates a new blob instance and then uploads the contents of + # the given io to the service. The blob instance is going to + # be saved before the upload begins to prevent the upload clobbering another due to key collisions. + # When providing a content type, pass identify: false to bypass + # automatic content type inference. + def self.create_and_upload!: (io: untyped io, filename: untyped filename, ?key: untyped? key, ?content_type: untyped? content_type, ?metadata: untyped? metadata, ?service_name: untyped? service_name, ?identify: bool identify, ?record: untyped? record) -> untyped + + alias self.create_after_upload! self.create_and_upload! + + # Returns a saved blob _without_ uploading a file to the service. This blob will point to a key where there is + # no file yet. It's intended to be used together with a client-side upload, which will first create the blob + # in order to produce the signed URL for uploading. This signed URL points to the key generated by the blob. + # Once the form using the direct upload is submitted, the blob can be associated with the right record using + # the signed ID. + def self.create_before_direct_upload!: (filename: untyped filename, byte_size: untyped byte_size, checksum: untyped checksum, ?key: untyped? key, ?content_type: untyped? content_type, ?metadata: untyped? metadata, ?service_name: untyped? service_name, ?record: untyped? record) -> untyped + + # To prevent problems with case-insensitive filesystems, especially in combination + # with databases which treat indices as case-sensitive, all blob keys generated are going + # to only contain the base-36 character alphabet and will therefore be lowercase. To maintain + # the same or higher amount of entropy as in the base-58 encoding used by `has_secure_token` + # the number of bytes used is increased to 28 from the standard 24 + def self.generate_unique_secure_token: (?length: untyped length) -> untyped + + def self.combine_signed_id_purposes: (untyped purpose) -> untyped + + def self.signed_id_verifier: () -> untyped + + # Returns a signed ID for this blob that's suitable for reference on the client-side without fear of tampering. + def signed_id: () -> untyped + + # Returns the key pointing to the file on the service that's associated with this blob. The key is the + # secure-token format from Rails in lower case. So it'll look like: xtapjjcjiudrlk3tmwyjgpuobabd. + # This key is not intended to be revealed directly to the user. + # Always refer to blobs using the signed_id or a verified form of the key. + def key: () -> untyped + + # Returns an ActiveStorage::Filename instance of the filename that can be + # queried for basename, extension, and a sanitized version of the filename + # that's safe to use in URLs. + def filename: () -> untyped + + # Returns true if the content_type of this blob is in the image range, like image/png. + def image?: () -> untyped + + # Returns true if the content_type of this blob is in the audio range, like audio/mpeg. + def audio?: () -> untyped + + # Returns true if the content_type of this blob is in the video range, like video/mp4. + def video?: () -> untyped + + # Returns true if the content_type of this blob is in the text range, like text/plain. + def text?: () -> untyped + + # Returns the URL of the blob on the service. This returns a permanent URL for public files, and returns a + # short-lived URL for private files. Private files are signed, and not for public use. Instead, + # the URL should only be exposed as a redirect from a stable, possibly authenticated URL. Hiding the + # URL behind a redirect also allows you to change services without updating all URLs. + def url: (?expires_in: untyped expires_in, ?disposition: ::Symbol disposition, ?filename: untyped? filename, **untyped options) -> untyped + + alias service_url url + + # Returns a URL that can be used to directly upload a file for this blob on the service. This URL is intended to be + # short-lived for security and only generated on-demand by the client-side JavaScript responsible for doing the uploading. + def service_url_for_direct_upload: (?expires_in: untyped expires_in) -> untyped + + # Returns a Hash of headers for +service_url_for_direct_upload+ requests. + def service_headers_for_direct_upload: () -> untyped + + def content_type_for_serving: () -> untyped + + def forced_disposition_for_serving: () -> untyped + + # Uploads the +io+ to the service on the +key+ for this blob. Blobs are intended to be immutable, so you shouldn't be + # using this method after a file has already been uploaded to fit with a blob. If you want to create a derivative blob, + # you should instead simply create a new blob based on the old one. + # + # Prior to uploading, we compute the checksum, which is sent to the service for transit integrity validation. If the + # checksum does not match what the service receives, an exception will be raised. We also measure the size of the +io+ + # and store that in +byte_size+ on the blob record. The content type is automatically extracted from the +io+ unless + # you specify a +content_type+ and pass +identify+ as false. + # + # Normally, you do not have to call this method directly at all. Use the +create_and_upload!+ class method instead. + # If you do use this method directly, make sure you are using it on a persisted Blob as otherwise another blob's + # data might get overwritten on the service. + def upload: (untyped io, ?identify: bool identify) -> untyped + + def unfurl: (untyped io, ?identify: bool identify) -> untyped + + def upload_without_unfurling: (untyped io) -> untyped + + # Downloads the file associated with this blob. If no block is given, the entire file is read into memory and returned. + # That'll use a lot of RAM for very large files. If a block is given, then the download is streamed and yielded in chunks. + def download: () { (String) -> void } -> void + | () -> String + + # Downloads the blob to a tempfile on disk. Yields the tempfile. + # + # The tempfile's name is prefixed with +ActiveStorage-+ and the blob's ID. Its extension matches that of the blob. + # + # By default, the tempfile is created in Dir.tmpdir. Pass +tmpdir:+ to create it in a different directory: + # + # blob.open(tmpdir: "/path/to/tmp") do |file| + # # ... + # end + # + # The tempfile is automatically closed and unlinked after the given block is executed. + # + # Raises ActiveStorage::IntegrityError if the downloaded data does not match the blob's checksum. + def open: (?tmpdir: untyped? tmpdir) { (Tempfile) -> untyped } -> untyped + + def mirror_later: () -> untyped + + # Deletes the files on the service associated with the blob. This should only be done if the blob is going to be + # deleted as well or you will essentially have a dead reference. It's recommended to use #purge and #purge_later + # methods in most circumstances. + def delete: () -> untyped + + # Destroys the blob record and then deletes the file on the service. This is the recommended way to dispose of unwanted + # blobs. Note, though, that deleting the file off the service will initiate an HTTP connection to the service, which may + # be slow or prevented, so you should not use this method inside a transaction or in callbacks. Use #purge_later instead. + def purge: () -> untyped + + # Enqueues an ActiveStorage::PurgeJob to call #purge. This is the recommended way to purge blobs from a transaction, + # an Active Record callback, or in any other real-time scenario. + def purge_later: () -> untyped + + # Returns an instance of service, which can be configured globally or per attachment + def service: () -> untyped +end diff --git a/gems/activestorage/8.1/app/models/active_storage/current.rbs b/gems/activestorage/8.1/app/models/active_storage/current.rbs new file mode 100644 index 000000000..682a704ff --- /dev/null +++ b/gems/activestorage/8.1/app/models/active_storage/current.rbs @@ -0,0 +1,8 @@ +module ActiveStorage + class Current < ActiveSupport::CurrentAttributes + def self.url_options: () -> Hash[Symbol, untyped] + def self.url_options=: (Hash[Symbol, untyped]) -> Hash[Symbol, untyped] + def url_options: () -> Hash[Symbol, untyped] + def url_options=: (Hash[Symbol, untyped]) -> Hash[Symbol, untyped] + end +end diff --git a/gems/activestorage/8.1/app/models/active_storage/record.rbs b/gems/activestorage/8.1/app/models/active_storage/record.rbs new file mode 100644 index 000000000..db904304e --- /dev/null +++ b/gems/activestorage/8.1/app/models/active_storage/record.rbs @@ -0,0 +1,2 @@ +class ActiveStorage::Record < ActiveRecord::Base #:nodoc: +end diff --git a/gems/activestorage/8.1/lib/attached.rbs b/gems/activestorage/8.1/lib/attached.rbs new file mode 100644 index 000000000..b00f98e95 --- /dev/null +++ b/gems/activestorage/8.1/lib/attached.rbs @@ -0,0 +1,11 @@ +module ActiveStorage + # Abstract base class for the concrete ActiveStorage::Attached::One and ActiveStorage::Attached::Many + # classes that both provide proxy access to the blob association for a record. + class Attached + attr_reader name: String + + attr_reader record: untyped # Model + + def initialize: (String name, untyped record) -> void + end +end diff --git a/gems/activestorage/8.1/lib/attached/many.rbs b/gems/activestorage/8.1/lib/attached/many.rbs new file mode 100644 index 000000000..a47f08087 --- /dev/null +++ b/gems/activestorage/8.1/lib/attached/many.rbs @@ -0,0 +1,38 @@ +module ActiveStorage + # Decorated proxy object representing of multiple attachments to a model. + class Attached::Many < Attached + include _ActiveRecord_Relation[ActiveStorage::Attachment, Integer] + + # Returns all the associated attachment records. + # + # All methods called on this proxy object that aren't listed here will automatically be delegated to +attachments+. + def attachments: () -> Array[ActiveStorage::Attachment] + + # Returns all attached blobs. + def blobs: () -> untyped + + # Attaches one or more +attachables+ to the record. + # + # If the record is persisted and unchanged, the attachments are saved to + # the database immediately. Otherwise, they'll be saved to the DB when the + # record is next saved. + # + # document.images.attach(params[:images]) # Array of ActionDispatch::Http::UploadedFile objects + # document.images.attach(params[:signed_blob_id]) # Signed reference to blob from direct upload + # document.images.attach(io: File.open("/path/to/racecar.jpg"), filename: "racecar.jpg", content_type: "image/jpg") + # document.images.attach([ first_blob, second_blob ]) + def attach: (*untyped attachables) -> untyped + + # Returns true if any attachments has been made. + # + # class Gallery < ActiveRecord::Base + # has_many_attached :photos + # end + # + # Gallery.new.photos.attached? # => false + def attached?: () -> bool + + # Deletes associated attachments without purging them, leaving their respective blobs in place. + def detach: () -> untyped + end +end diff --git a/gems/activestorage/8.1/lib/attached/model.rbs b/gems/activestorage/8.1/lib/attached/model.rbs new file mode 100644 index 000000000..382d2c797 --- /dev/null +++ b/gems/activestorage/8.1/lib/attached/model.rbs @@ -0,0 +1,107 @@ +module ActiveStorage + # Provides the class-level DSL for declaring an Active Record model's attachments. + module Attached::Model + extend ActiveSupport::Concern + + # mutate class_methods + module ClassMethods + # Specifies the relation between a single attachment and the model. + # + # class User < ApplicationRecord + # has_one_attached :avatar + # end + # + # There is no column defined on the model side, Active Storage takes + # care of the mapping between your records and the attachment. + # + # To avoid N+1 queries, you can include the attached blobs in your query like so: + # + # User.with_attached_avatar + # + # Under the covers, this relationship is implemented as a +has_one+ association to a + # ActiveStorage::Attachment record and a +has_one-through+ association to a + # ActiveStorage::Blob record. These associations are available as +avatar_attachment+ + # and +avatar_blob+. But you shouldn't need to work with these associations directly in + # most circumstances. + # + # The system has been designed to having you go through the ActiveStorage::Attached::One + # proxy that provides the dynamic proxy to the associations and factory methods, like +attach+. + # + # If the +:dependent+ option isn't set, the attachment will be purged + # (i.e. destroyed) whenever the record is destroyed. + # + # If you need the attachment to use a service which differs from the globally configured one, + # pass the +:service+ option. For instance: + # + # class User < ActiveRecord::Base + # has_one_attached :avatar, service: :s3 + # end + # + # If you need to enable +strict_loading+ to prevent lazy loading of attachment, + # pass the +:strict_loading+ option. You can do: + # + # class User < ApplicationRecord + # has_one_attached :avatar, strict_loading: true + # end + # + def has_one_attached: ( + (::String | ::Symbol) name, + ?dependent: ::Symbol dependent, + ?service: (::String | ::Symbol | nil) service, + ?strict_loading: bool strict_loading + ) -> void + + # Specifies the relation between multiple attachments and the model. + # + # class Gallery < ApplicationRecord + # has_many_attached :photos + # end + # + # There are no columns defined on the model side, Active Storage takes + # care of the mapping between your records and the attachments. + # + # To avoid N+1 queries, you can include the attached blobs in your query like so: + # + # Gallery.where(user: Current.user).with_attached_photos + # + # Under the covers, this relationship is implemented as a +has_many+ association to a + # ActiveStorage::Attachment record and a +has_many-through+ association to a + # ActiveStorage::Blob record. These associations are available as +photos_attachments+ + # and +photos_blobs+. But you shouldn't need to work with these associations directly in + # most circumstances. + # + # The system has been designed to having you go through the ActiveStorage::Attached::Many + # proxy that provides the dynamic proxy to the associations and factory methods, like +#attach+. + # + # If the +:dependent+ option isn't set, all the attachments will be purged + # (i.e. destroyed) whenever the record is destroyed. + # + # If you need the attachment to use a service which differs from the globally configured one, + # pass the +:service+ option. For instance: + # + # class Gallery < ActiveRecord::Base + # has_many_attached :photos, service: :s3 + # end + # + # If you need to enable +strict_loading+ to prevent lazy loading of attachments, + # pass the +:strict_loading+ option. You can do: + # + # class Gallery < ApplicationRecord + # has_many_attached :photos, strict_loading: true + # end + # + def has_many_attached: ( + (::String | ::Symbol) name, + ?dependent: ::Symbol dependent, + ?service: (::String | ::Symbol | nil) service, + ?strict_loading: bool strict_loading + ) -> void + end + + def attachment_changes: () -> void + + def changed_for_autosave?: () -> bool + + def reload: (?lock: boolish) -> self + end +end diff --git a/gems/activestorage/8.1/lib/attached/one.rbs b/gems/activestorage/8.1/lib/attached/one.rbs new file mode 100644 index 000000000..84909021f --- /dev/null +++ b/gems/activestorage/8.1/lib/attached/one.rbs @@ -0,0 +1,46 @@ +module ActiveStorage + # Representation of a single attachment to a model. + class Attached::One < Attached + # delegated from Blob + def url: () -> String + + # Returns the associated attachment record. + # + # You don't have to call this method to access the attachment's methods as + # they are all available at the model level. + def attachment: () -> ActiveStorage::Attachment? + + def blank?: () -> bool + + # Attaches an +attachable+ to the record. + # + # If the record is persisted and unchanged, the attachment is saved to + # the database immediately. Otherwise, it'll be saved to the DB when the + # record is next saved. + # + # person.avatar.attach(params[:avatar]) # ActionDispatch::Http::UploadedFile object + # person.avatar.attach(params[:signed_blob_id]) # Signed reference to blob from direct upload + # person.avatar.attach(io: File.open("/path/to/face.jpg"), filename: "face.jpg", content_type: "image/jpg") + # person.avatar.attach(avatar_blob) # ActiveStorage::Blob object + def attach: (untyped attachable) -> untyped + + # Returns +true+ if an attachment has been made. + # + # class User < ActiveRecord::Base + # has_one_attached :avatar + # end + # + # User.new.avatar.attached? # => false + def attached?: () -> bool + + # Deletes the attachment without purging it, leaving its blob in place. + def detach: () -> untyped + + # Directly purges the attachment (i.e. destroys the blob and + # attachment and deletes the file on the service). + def purge: () -> untyped + + # Purges the attachment through the queuing system. + def purge_later: () -> untyped + end +end diff --git a/gems/activestorage/8.1/lib/engine.rbs b/gems/activestorage/8.1/lib/engine.rbs new file mode 100644 index 000000000..64e3053e4 --- /dev/null +++ b/gems/activestorage/8.1/lib/engine.rbs @@ -0,0 +1,10 @@ +module ActiveRecord + class Base + include ::ActiveStorage::Attached::Model + extend ::ActiveStorage::Attached::Model::ClassMethods + include ::ActiveStorage::Reflection::ActiveRecordExtensions + # included in ActiveRecordExtensions + attr_accessor self.attachment_reflections: Hash[String, ActiveStorage::Reflection::HasOneAttachedReflection | ActiveStorage::Reflection::HasManyAttachedReflection] + extend ::ActiveStorage::Reflection::ActiveRecordExtensions::ClassMethods + end +end diff --git a/gems/activestorage/8.1/lib/reflection.rbs b/gems/activestorage/8.1/lib/reflection.rbs new file mode 100644 index 000000000..033008310 --- /dev/null +++ b/gems/activestorage/8.1/lib/reflection.rbs @@ -0,0 +1,32 @@ +module ActiveStorage + module Reflection + # Holds all the metadata about a has_one_attached attachment as it was + # specified in the Active Record class. + class HasOneAttachedReflection < ActiveRecord::Reflection::MacroReflection #:nodoc: + def macro: () -> :has_one_attached + end + + # Holds all the metadata about a has_many_attached attachment as it was + # specified in the Active Record class. + class HasManyAttachedReflection < ActiveRecord::Reflection::MacroReflection #:nodoc: + def macro: () -> :has_many_attached + end + + module ActiveRecordExtensions + extend ActiveSupport::Concern + + module ClassMethods + # Returns an array of reflection objects for all the attachments in the + # class. + def reflect_on_all_attachments: () -> Array[ActiveStorage::Reflection::HasOneAttachedReflection | ActiveStorage::Reflection::HasManyAttachedReflection] + + # Returns the reflection object for the named +attachment+. + # + # User.reflect_on_attachment(:avatar) + # # => the avatar reflection + # + def reflect_on_attachment: ((String | Symbol) attachment) -> (ActiveStorage::Reflection::HasOneAttachedReflection | ActiveStorage::Reflection::HasManyAttachedReflection | nil) + end + end + end +end diff --git a/gems/activestorage/8.1/manifest.yaml b/gems/activestorage/8.1/manifest.yaml new file mode 100644 index 000000000..ab31da323 --- /dev/null +++ b/gems/activestorage/8.1/manifest.yaml @@ -0,0 +1,2 @@ +dependencies: + - name: tempfile From 1acc43b7f9a4fa93b5464b75f6010ff2d8ddd640 Mon Sep 17 00:00:00 2001 From: Akira Matsuda Date: Tue, 13 Oct 2026 19:25:39 +0900 Subject: [PATCH 09/22] activestorage: Generate RBS from activestorage 8.1.4 7.0 had only hand-written signatures. 8.1 adds signatures generated with `rbs prototype rb` from the activestorage 8.1.4 lib and app sources and post-processed the same way as the other Rails 8.1 gems: drop members already defined elsewhere, fill missing type arguments, and add accessors defined with class_attribute, mattr_accessor, and cattr_accessor and methods defined with delegate. Methods defined inside `class_methods do ... end` are generated as the concern's ClassMethods. ActiveStorage::Blobs and ActiveStorage::Representations, which Zeitwerk defines for the controllers in app, are declared as modules. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../8.1/activestorage-generated.rbs | 2505 +++++++++++++++++ 1 file changed, 2505 insertions(+) create mode 100644 gems/activestorage/8.1/activestorage-generated.rbs diff --git a/gems/activestorage/8.1/activestorage-generated.rbs b/gems/activestorage/8.1/activestorage-generated.rbs new file mode 100644 index 000000000..4fd46531a --- /dev/null +++ b/gems/activestorage/8.1/activestorage-generated.rbs @@ -0,0 +1,2505 @@ +# The generated code is based on Ruby on Rails source code +# You can find the license of Ruby on Rails from following. + +#Copyright (c) David Heinemeier Hansson, 37signals LLC +# +#Permission is hereby granted, free of charge, to any person obtaining +#a copy of this software and associated documentation files (the +#"Software"), to deal in the Software without restriction, including +#without limitation the rights to use, copy, modify, merge, publish, +#distribute, sublicense, and/or sell copies of the Software, and to +#permit persons to whom the Software is furnished to do so, subject to +#the following conditions: +# +#The above copyright notice and this permission notice shall be +#included in all copies or substantial portions of the Software. +# +#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +#EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +#MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +#NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +#LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +#OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +#WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +# The base class for all Active Storage controllers. +class ActiveStorage::BaseController < ::ActionController::Base + include ::ActiveStorage::SetCurrent +end + +# Finds a blob by a +signed_id+ and proxies the file through the application. +# The blob is streamed from storage directly to the response. This avoids having +# a redirect and makes files easier to cache. +# +# The +signed_id+s make URLs hard to guess but permanent by design, allowing the URLs to be cached. +# The response sets the HTTP cache to public and allows browsers and proxies to cache it indefinitely. +# +# The URLs created for this controller are set to never expire by default. +# To make URLs expire, pass the +expires_in+ option when generating the URL: +# +# rails_storage_proxy_url(blob, expires_in: 1.minute) +# +# Or set the default for all Active Storage URLs: +# +# config.active_storage.urls_expire_in = 1.day +# +# WARNING: All Active Storage controllers are publicly accessible by default. +# Anyone who knows the URL can access the file, even if the rest of your application requires +# authentication. If your files require access control consider implementing +# {Authenticated Controllers}[https://guides.rubyonrails.org/active_storage_overview.html#authenticated-controllers]. +class ActiveStorage::Blobs::ProxyController < ::ActiveStorage::BaseController + include ::ActiveStorage::SetBlob + + include ::ActiveStorage::Streaming + + include ::ActiveStorage::DisableSession + + def show: () -> untyped +end + +# Finds a blob by a +signed_id+ and redirects to a blob's expiring service URL. +# +# The +signed_id+s make URLs hard to guess but permanent by design, allowing the URLs to be cached. +# +# The URLs created for this controller are set to never expire by default. +# To make URLs expire, pass the +expires_in+ option when generating the URL: +# +# rails_storage_redirect_url(blob, expires_in: 1.minute) +# +# Or set the default for all Active Storage URLs: +# +# config.active_storage.urls_expire_in = 1.day +# +# The service URLs are set to expire in 5 minutes by default. +# The default can be changed for all service URLs: +# +# config.active_storage.service_urls_expire_in = 1.hour +# +# WARNING: All Active Storage controllers are publicly accessible by default. +# Anyone who knows the URL can access the file, even if the rest of your application requires +# authentication. If your files require access control consider implementing +# {Authenticated Controllers}[https://guides.rubyonrails.org/active_storage_overview.html#authenticated-controllers]. +class ActiveStorage::Blobs::RedirectController < ::ActiveStorage::BaseController + include ::ActiveStorage::SetBlob + + def show: () -> untyped +end + +# Creates a new blob on the server side in anticipation of a direct-to-service upload from the client side. +# When the client-side upload is completed, the signed_blob_id can be submitted as part of the form to reference +# the blob that was created up front. +class ActiveStorage::DirectUploadsController < ::ActiveStorage::BaseController + def create: () -> untyped + + private + + def blob_args: () -> untyped + + def direct_upload_json: (untyped blob) -> untyped +end + +# Serves files stored with the disk service in the same way that the cloud services do. +# This means using expiring, signed URLs that are meant for immediate access, not permanent linking. +# Always go through the BlobsController, or your own authenticated controller, rather than directly +# to the service URL. +class ActiveStorage::DiskController < ::ActiveStorage::BaseController + include ::ActiveStorage::FileServer + + def show: () -> untyped + + def update: () -> untyped + + private + + def named_disk_service: (untyped name) -> untyped + + def decode_verified_key: () -> untyped + + def decode_verified_token: () -> untyped + + def acceptable_content?: (untyped token) -> untyped +end + +class ActiveStorage::Representations::BaseController < ::ActiveStorage::BaseController + include ::ActiveStorage::SetBlob + + private + + def blob_scope: () -> untyped + + def set_representation: () -> untyped +end + +# Finds a representation by a +signed_id+ and a +variation_key+, and proxies the file through the application. +# The representation is streamed from storage directly to the response. This avoids having +# a redirect and makes files easier to cache. +# +# The +signed_id+s make URLs hard to guess but permanent by design, allowing the URLs to be cached. +# The response sets the HTTP cache to public and allows browsers and proxies to cache it indefinitely. +# +# The URLs created for this controller are set to never expire by default. +# To make URLs expire, pass the +expires_in+ option when generating the URL: +# +# rails_storage_proxy_url(representation, expires_in: 1.minute) +# +# Or set the default for all Active Storage URLs: +# +# config.active_storage.urls_expire_in = 1.day +# +# WARNING: All Active Storage controllers are publicly accessible by default. +# Anyone who knows the URL can access the file, even if the rest of your application requires +# authentication. If your files require access control consider implementing +# {Authenticated Controllers}[https://guides.rubyonrails.org/active_storage_overview.html#authenticated-controllers]. +class ActiveStorage::Representations::ProxyController < ::ActiveStorage::Representations::BaseController + include ::ActiveStorage::Streaming + + include ::ActiveStorage::DisableSession + + def show: () -> untyped +end + +# Finds a representation by a +signed_id+ and a +variation_key+, and redirects to a representation's expiring +# service URL. +# +# The +signed_id+s make URLs hard to guess but permanent by design, allowing the URLs to be cached. +# +# The URLs created for this controller are set to never expire by default. +# To make URLs expire, pass the +expires_in+ option when generating the URL: +# +# rails_storage_redirect_url(representation, expires_in: 1.minute) +# +# Or set the default for all Active Storage URLs: +# +# config.active_storage.urls_expire_in = 1.day +# +# The service URLs are set to expire in 5 minutes by default. +# The default can be changed for all service URLs: +# +# config.active_storage.service_urls_expire_in = 1.hour +# +# WARNING: All Active Storage controllers are publicly accessible by default. +# Anyone who knows the URL can access the file, even if the rest of your application requires +# authentication. If your files require access control consider implementing +# {Authenticated Controllers}[https://guides.rubyonrails.org/active_storage_overview.html#authenticated-controllers]. +class ActiveStorage::Representations::RedirectController < ::ActiveStorage::Representations::BaseController + def show: () -> untyped +end + +# This concern disables the session in order to allow caching by default in some CDNs as CloudFlare. +module ActiveStorage::DisableSession + extend ::ActiveSupport::Concern +end + +module ActiveStorage::FileServer + private + + def serve_file: (untyped path, content_type: untyped, disposition: untyped) -> untyped +end + +module ActiveStorage::SetBlob + extend ::ActiveSupport::Concern + + private + + def set_blob: () -> untyped + + def blob_scope: () -> untyped +end + +# Sets the ActiveStorage::Current.url_options attribute, which the disk service uses to generate URLs. +# Include this concern in custom controllers that call ActiveStorage::Blob#url, +# ActiveStorage::Variant#url, or ActiveStorage::Preview#url so the disk service can +# generate URLs using the same host, protocol, and port as the current request. +module ActiveStorage::SetCurrent + extend ::ActiveSupport::Concern +end + +module ActiveStorage::Streaming + extend ::ActiveSupport::Concern + + DEFAULT_BLOB_STREAMING_DISPOSITION: "inline" + + include ::ActionController::DataStreaming + + include ::ActionController::Live + + private + + # Stream the blob in byte ranges specified through the header + def send_blob_byte_range_data: (untyped blob, untyped range_header, ?disposition: untyped?) -> untyped + + def ranges_valid?: (untyped ranges) -> (false | untyped) + + # Stream the blob from storage directly to the response. The disposition can be controlled by setting +disposition+. + # The content type and filename is set directly from the +blob+. + def send_blob_stream: (untyped blob, ?disposition: untyped?) -> untyped +end + +# Provides asynchronous analysis of ActiveStorage::Blob records via ActiveStorage::Blob#analyze_later. +class ActiveStorage::AnalyzeJob < ::ActiveStorage::BaseJob + def perform: (untyped blob) -> untyped +end + +class ActiveStorage::BaseJob < ::ActiveJob::Base +end + +# Provides asynchronous mirroring of directly-uploaded blobs. +class ActiveStorage::MirrorJob < ::ActiveStorage::BaseJob + def perform: (untyped key, checksum: untyped) -> untyped +end + +class ActiveStorage::PreviewImageJob < ::ActiveStorage::BaseJob + def perform: (untyped blob, untyped variations) -> untyped +end + +# Provides asynchronous purging of ActiveStorage::Blob records via ActiveStorage::Blob#purge_later. +class ActiveStorage::PurgeJob < ::ActiveStorage::BaseJob + def perform: (untyped blob) -> untyped +end + +class ActiveStorage::TransformJob < ::ActiveStorage::BaseJob + def perform: (untyped blob, untyped transformations) -> untyped +end + +# = Active Storage \Attachment +# +# Attachments associate records with blobs. Usually that's a one record-many blobs relationship, +# but it is possible to associate many different records with the same blob. A foreign-key constraint +# on the attachments table prevents blobs from being purged if they’re still attached to any records. +# +# Attachments also have access to all methods from ActiveStorage::Blob. +# +# If you wish to preload attachments or blobs, you can use these scopes: +# +# # preloads attachments, their corresponding blobs, and variant records (if using `ActiveStorage.track_variants`) +# User.all.with_attached_avatars +# +# # preloads blobs and variant records (if using `ActiveStorage.track_variants`) +# User.first.avatars.with_all_variant_records +class ActiveStorage::Attachment + def signed_id: (*untyped, **untyped) ?{ (*untyped) -> untyped } -> untyped + + # Returns an ActiveStorage::Variant or ActiveStorage::VariantWithRecord + # instance for the attachment with the set of +transformations+ provided. + # Example: + # + # avatar.variant(resize_to_limit: [100, 100]).processed.url + # + # or if you are using pre-defined variants: + # + # avatar.variant(:thumb).processed.url + # + # See ActiveStorage::Blob::Representable#variant for more information. + # + # Raises an +ArgumentError+ if +transformations+ is a +Symbol+ which is an + # unknown pre-defined variant of the attachment. + def variant: (untyped transformations) -> untyped + + # Returns an ActiveStorage::Preview instance for the attachment with the set + # of +transformations+ provided. + # Example: + # + # video.preview(resize_to_limit: [100, 100]).processed.url + # + # or if you are using pre-defined variants: + # + # video.preview(:thumb).processed.url + # + # See ActiveStorage::Blob::Representable#preview for more information. + # + # Raises an +ArgumentError+ if +transformations+ is a +Symbol+ which is an + # unknown pre-defined variant of the attachment. + def preview: (untyped transformations) -> untyped + + # Returns an ActiveStorage::Preview or an ActiveStorage::Variant for the + # attachment with set of +transformations+ provided. + # Example: + # + # avatar.representation(resize_to_limit: [100, 100]).processed.url + # + # or if you are using pre-defined variants: + # + # avatar.representation(:thumb).processed.url + # + # See ActiveStorage::Blob::Representable#representation for more information. + # + # Raises an +ArgumentError+ if +transformations+ is a +Symbol+ which is an + # unknown pre-defined variant of the attachment. + def representation: (untyped transformations) -> untyped + + private + + def analyze_blob_later: () -> (untyped | nil) + + def mirror_blob_later: () -> untyped + + def transform_variants_later: () -> untyped + + def purge_dependent_blob: () -> (untyped | nil) + + def dependent: () -> untyped + + def named_variants: () -> untyped + + def transformations_by_name: (untyped transformations) -> untyped +end + +# = Active Storage \Blob +# +# A blob is a record that contains the metadata about a file and a key for where that file resides on the service. +# Blobs can be created in two ways: +# +# 1. Ahead of the file being uploaded server-side to the service, via create_and_upload!. A rewindable +# io with the file contents must be available at the server for this operation. +# 2. Ahead of the file being directly uploaded client-side to the service, via create_before_direct_upload!. +# +# The first option doesn't require any client-side JavaScript integration, and can be used by any other back-end +# service that deals with files. The second option is faster, since you're not using your own server as a staging +# point for uploads, and can work with deployments like Heroku that do not provide large amounts of disk space. +# +# Blobs are intended to be immutable in as-so-far as their reference to a specific file goes. You're allowed to +# update a blob's metadata on a subsequent pass, but you should not update the key or change the uploaded file. +# If you need to create a derivative or otherwise change the blob, simply create a new blob and purge the old one. +# +# When using a custom +key+, the value is treated as trusted. Using untrusted user input +# as the key may result in unexpected behavior. +class ActiveStorage::Blob + def self.services: () -> untyped + def self.services=: (untyped value) -> untyped + def self.services?: () -> bool + def services: () -> untyped + def services?: () -> bool + def services=: (untyped value) -> untyped + def self.service: () -> untyped + def self.service=: (untyped value) -> untyped + def self.service?: () -> bool + + # FIXME: these property should never have been stored in the metadata. + # The blob table should be migrated to have dedicated columns for theses. + PROTECTED_METADATA: ::Array["analyzed" | "identified" | "composed"] + + def self.scope_for_strict_loading: () -> untyped + + # Concatenate multiple blobs into a single "composed" blob. + def self.compose: (untyped blobs, filename: untyped, ?key: untyped?, ?content_type: untyped?, ?metadata: untyped?) -> untyped + + def self.filter_metadata: (untyped metadata) -> untyped + + include ::ActiveStorage::Blob::Analyzable + + include ::ActiveStorage::Blob::Identifiable + + include ::ActiveStorage::Blob::Representable + + include ::ActiveStorage::Blob::Servable + + def custom_metadata: () -> untyped + + def custom_metadata=: (untyped metadata) -> untyped + + def compose: (untyped keys) -> untyped + + def service_metadata: () -> ({ content_type: untyped, disposition: :attachment, filename: untyped, custom_metadata: untyped } | { content_type: untyped, custom_metadata: untyped }) + + # Downloads a part of the file associated with this blob. + def download_chunk: (untyped range) -> untyped + + private + + def compute_checksum_in_chunks: (untyped io) -> untyped + + def extract_content_type: (untyped io) -> untyped + + def web_image?: () -> untyped + + def touch_attachments: () -> untyped + + def update_service_metadata: () -> (untyped | nil) +end + +# = Active Storage \Blob \Analyzable +module ActiveStorage::Blob::Analyzable + # Extracts and stores metadata from the file associated with this blob using a relevant analyzer. Active Storage comes + # with built-in analyzers for images and videos. See ActiveStorage::Analyzer::ImageAnalyzer and + # ActiveStorage::Analyzer::VideoAnalyzer for information about the specific attributes they extract and the third-party + # libraries they require. + # + # To choose the analyzer for a blob, Active Storage calls +accept?+ on each registered analyzer in order. It uses the + # first analyzer for which +accept?+ returns true when given the blob. If no registered analyzer accepts the blob, no + # metadata is extracted from it. + # + # In a \Rails application, add or remove analyzers by manipulating +Rails.application.config.active_storage.analyzers+ + # in an initializer: + # + # # Add a custom analyzer for Microsoft Office documents: + # Rails.application.config.active_storage.analyzers.append DOCXAnalyzer + # + # # Remove the built-in video analyzer: + # Rails.application.config.active_storage.analyzers.delete ActiveStorage::Analyzer::VideoAnalyzer + # + # Outside of a \Rails application, manipulate +ActiveStorage.analyzers+ instead. + # + # You won't ordinarily need to call this method from a \Rails application. New blobs are automatically and asynchronously + # analyzed via #analyze_later when they're attached for the first time. + def analyze: () -> untyped + + # Enqueues an ActiveStorage::AnalyzeJob which calls #analyze, or calls #analyze inline based on analyzer class configuration. + # + # This method is automatically called for a blob when it's attached for the first time. You can call it to analyze a blob + # again (e.g. if you add a new analyzer or modify an existing one). + def analyze_later: () -> untyped + + # Returns true if the blob has been analyzed. + def analyzed?: () -> untyped + + private + + def extract_metadata_via_analyzer: () -> untyped + + def analyzer: () -> untyped + + def analyzer_class: () -> untyped +end + +# = Active Storage \Blob \Identifiable +module ActiveStorage::Blob::Identifiable + def identify: () -> untyped + + def identify_without_saving: () -> (untyped | nil) + + def identified?: () -> untyped + + private + + def identify_content_type: () -> untyped + + def download_identifiable_chunk: () -> (untyped | "") +end + +module ActiveStorage::Blob::Representable + extend ::ActiveSupport::Concern + + # Returns an ActiveStorage::Variant or ActiveStorage::VariantWithRecord instance with the set of +transformations+ provided. + # This is only relevant for image files, and it allows any image to be transformed for size, colors, and the like. Example: + # + # avatar.variant(resize_to_limit: [100, 100]).processed.url + # + # This will create and process a variant of the avatar blob that's constrained to a height and width of 100px. + # Then it'll upload said variant to the service according to a derivative key of the blob and the transformations. + # + # Frequently, though, you don't actually want to transform the variant right away. But rather simply refer to a + # specific variant that can be created by a controller on-demand. Like so: + # + # <%= image_tag Current.user.avatar.variant(resize_to_limit: [100, 100]) %> + # + # This will create a URL for that specific blob with that specific variant, which the ActiveStorage::Representations::ProxyController + # or ActiveStorage::Representations::RedirectController can then produce on-demand. + # + # Raises ActiveStorage::InvariableError if the variant processor cannot + # transform the blob. To determine whether a blob is variable, call + # ActiveStorage::Blob#variable?. + # + # ==== Options + # + # Options are defined by the {image_processing gem}[https://github.com/janko/image_processing], + # and depend on which variant processor you are using: + # {Vips}[https://github.com/janko/image_processing/blob/master/doc/vips.md] or + # {MiniMagick}[https://github.com/janko/image_processing/blob/master/doc/minimagick.md]. + # However, both variant processors support the following options: + # + # [+:resize_to_limit+] + # Downsizes the image to fit within the specified dimensions while retaining + # the original aspect ratio. Will only resize the image if it's larger than + # the specified dimensions. + # + # user.avatar.variant(resize_to_limit: [100, 100]) + # + # [+:resize_to_fit+] + # Resizes the image to fit within the specified dimensions while retaining + # the original aspect ratio. Will downsize the image if it's larger than the + # specified dimensions or upsize if it's smaller. + # + # user.avatar.variant(resize_to_fit: [100, 100]) + # + # [+:resize_to_fill+] + # Resizes the image to fill the specified dimensions while retaining the + # original aspect ratio. If necessary, will crop the image in the larger + # dimension. + # + # user.avatar.variant(resize_to_fill: [100, 100]) + # + # [+:resize_and_pad+] + # Resizes the image to fit within the specified dimensions while retaining + # the original aspect ratio. If necessary, will pad the remaining area with + # transparent color if source image has alpha channel, black otherwise. + # + # user.avatar.variant(resize_and_pad: [100, 100]) + # + # [+:crop+] + # Extracts an area from an image. The first two arguments are the left and + # top edges of area to extract, while the last two arguments are the width + # and height of the area to extract. + # + # user.avatar.variant(crop: [20, 50, 300, 300]) + # + # [+:rotate+] + # Rotates the image by the specified angle. + # + # user.avatar.variant(rotate: 90) + # + # Some options, including those listed above, can accept additional + # processor-specific values which can be passed as a trailing hash: + # + # + # <%= image_tag user.avatar.variant(resize_to_fill: [100, 100, { crop: :centre }]) %> + # + # If migrating an existing application between MiniMagick and Vips, you will + # need to update processor-specific options: + # + # + # <%= image_tag user.avatar.variant(resize_to_limit: [100, 100], format: :jpeg, + # sampling_factor: "4:2:0", strip: true, interlace: "JPEG", colorspace: "sRGB", quality: 80) %> + # + # + # <%= image_tag user.avatar.variant(resize_to_limit: [100, 100], format: :jpeg, + # saver: { subsample_mode: "on", strip: true, interlace: true, quality: 80 }) %> + # + def variant: (untyped transformations) -> untyped + + # Returns true if the variant processor can transform the blob (its content + # type is in +ActiveStorage.variable_content_types+). + def variable?: () -> untyped + + # Returns an ActiveStorage::Preview instance with the set of +transformations+ provided. A preview is an image generated + # from a non-image blob. Active Storage comes with built-in previewers for videos and PDF documents. The video previewer + # extracts the first frame from a video and the PDF previewer extracts the first page from a PDF document. + # + # blob.preview(resize_to_limit: [100, 100]).processed.url + # + # Avoid processing previews synchronously in views. Instead, link to a controller action that processes them on demand. + # Active Storage provides one, but you may want to create your own (for example, if you need authentication). Here’s + # how to use the built-in version: + # + # <%= image_tag video.preview(resize_to_limit: [100, 100]) %> + # + # This method raises ActiveStorage::UnpreviewableError if no previewer accepts the receiving blob. To determine + # whether a blob is accepted by any previewer, call ActiveStorage::Blob#previewable?. + def preview: (untyped transformations) -> untyped + + # Returns true if any registered previewer accepts the blob. By default, this will return true for videos and PDF documents. + def previewable?: () -> untyped + + # Returns an ActiveStorage::Preview for a previewable blob or an ActiveStorage::Variant for a variable image blob. + # + # blob.representation(resize_to_limit: [100, 100]).processed.url + # + # Raises ActiveStorage::UnrepresentableError if the receiving blob is neither variable nor previewable. Call + # ActiveStorage::Blob#representable? to determine whether a blob is representable. + # + # See ActiveStorage::Blob#preview and ActiveStorage::Blob#variant for more information. + def representation: (untyped transformations) -> untyped + + # Returns true if the blob is variable or previewable. + def representable?: () -> untyped + + def preview_image_needed_before_processing_variants?: () -> untyped + + def create_preview_image_later: (untyped variations) -> (untyped | nil) + + def preprocessed: (untyped transformations) -> (untyped | nil) + + private + + def default_variant_transformations: () -> { format: untyped } + + def default_variant_format: () -> (untyped | :png) + + def format: () -> untyped + + def variant_class: () -> untyped +end + +module ActiveStorage::Blob::Servable + def content_type_for_serving: () -> untyped + + def forced_disposition_for_serving: () -> (:attachment | nil) + + private + + def forcibly_serve_as_binary?: () -> untyped + + def allowed_inline?: () -> untyped +end + +class ActiveStorage::Current +end + +# = Active Storage \Filename +# +# Encapsulates a string representing a filename to provide convenient access to parts of it and sanitization. +# A Filename instance is returned by ActiveStorage::Blob#filename, and is comparable so it can be used for sorting. +class ActiveStorage::Filename + include ::Comparable + + # Returns a Filename instance based on the given filename. If the filename is a Filename, it is + # returned unmodified. If it is a String, it is passed to ActiveStorage::Filename.new. + def self.wrap: (untyped filename) -> untyped + + def initialize: (untyped filename) -> void + + # Returns the part of the filename preceding any extension. + # + # ActiveStorage::Filename.new("racecar.jpg").base # => "racecar" + # ActiveStorage::Filename.new("racecar").base # => "racecar" + # ActiveStorage::Filename.new(".gitignore").base # => ".gitignore" + def base: () -> untyped + + # Returns the extension of the filename (i.e. the substring following the last dot, excluding a dot at the + # beginning) with the dot that precedes it. If the filename has no extension, an empty string is returned. + # + # ActiveStorage::Filename.new("racecar.jpg").extension_with_delimiter # => ".jpg" + # ActiveStorage::Filename.new("racecar").extension_with_delimiter # => "" + # ActiveStorage::Filename.new(".gitignore").extension_with_delimiter # => "" + def extension_with_delimiter: () -> untyped + + # Returns the extension of the filename (i.e. the substring following the last dot, excluding a dot at + # the beginning). If the filename has no extension, an empty string is returned. + # + # ActiveStorage::Filename.new("racecar.jpg").extension_without_delimiter # => "jpg" + # ActiveStorage::Filename.new("racecar").extension_without_delimiter # => "" + # ActiveStorage::Filename.new(".gitignore").extension_without_delimiter # => "" + def extension_without_delimiter: () -> untyped + + alias extension extension_without_delimiter + + # Returns the sanitized filename. + # + # ActiveStorage::Filename.new("foo:bar.jpg").sanitized # => "foo-bar.jpg" + # ActiveStorage::Filename.new("foo/bar.jpg").sanitized # => "foo-bar.jpg" + # + # Characters considered unsafe for storage (e.g. \, $, and the RTL override character) are replaced with a dash. + def sanitized: () -> untyped + + # Returns the sanitized version of the filename. + def to_s: () -> untyped + + alias to_str to_s + + def as_json: (*untyped) -> untyped + + def <=>: (untyped other) -> untyped +end + +class ActiveStorage::NamedVariant + attr_reader transformations: untyped + + attr_reader preprocessed: untyped + + def initialize: (untyped transformations) -> void + + def preprocessed?: (untyped record) -> untyped +end + +# = Active Storage \Preview +# +# Some non-image blobs can be previewed: that is, they can be presented as images. A video blob can be previewed by +# extracting its first frame, and a PDF blob can be previewed by extracting its first page. +# +# A previewer extracts a preview image from a blob. Active Storage provides previewers for videos and PDFs. +# ActiveStorage::Previewer::VideoPreviewer is used for videos whereas ActiveStorage::Previewer::PopplerPDFPreviewer +# and ActiveStorage::Previewer::MuPDFPreviewer are used for PDFs. Build custom previewers by +# subclassing ActiveStorage::Previewer and implementing the requisite methods. Consult the ActiveStorage::Previewer +# documentation for more details on what's required of previewers. +# +# To choose the previewer for a blob, Active Storage calls +accept?+ on each registered previewer in order. It uses the +# first previewer for which +accept?+ returns true when given the blob. In a \Rails application, add or remove previewers +# by manipulating +Rails.application.config.active_storage.previewers+ in an initializer: +# +# Rails.application.config.active_storage.previewers +# # => [ ActiveStorage::Previewer::PopplerPDFPreviewer, ActiveStorage::Previewer::MuPDFPreviewer, ActiveStorage::Previewer::VideoPreviewer ] +# +# # Add a custom previewer for Microsoft Office documents: +# Rails.application.config.active_storage.previewers << DOCXPreviewer +# # => [ ActiveStorage::Previewer::PopplerPDFPreviewer, ActiveStorage::Previewer::MuPDFPreviewer, ActiveStorage::Previewer::VideoPreviewer, DOCXPreviewer ] +# +# Outside of a \Rails application, modify +ActiveStorage.previewers+ instead. +# +# The built-in previewers rely on third-party system libraries. Specifically, the built-in video previewer requires +# {FFmpeg}[https://www.ffmpeg.org]. Two PDF previewers are provided: one requires {Poppler}[https://poppler.freedesktop.org], +# and the other requires {muPDF}[https://mupdf.com] (version 1.8 or newer). To preview PDFs, install either Poppler or muPDF. +# +# These libraries are not provided by \Rails. You must install them yourself to use the built-in previewers. Before you +# install and use third-party software, make sure you understand the licensing implications of doing so. +class ActiveStorage::Preview + def filename: (*untyped, **untyped) ?{ (*untyped) -> untyped } -> untyped + def content_type: (*untyped, **untyped) ?{ (*untyped) -> untyped } -> untyped + + include ::ActiveStorage::Blob::Servable + + class UnprocessedError < ::StandardError + end + + attr_reader blob: untyped + + attr_reader variation: untyped + + def initialize: (untyped blob, untyped variation_or_variation_key) -> void + + # Processes the preview if it has not been processed yet. Returns the receiving +ActiveStorage::Preview+ instance for convenience: + # + # blob.preview(resize_to_limit: [100, 100]).processed.url + # + # Processing a preview generates an image from its blob and attaches the preview image to the blob. Because the preview + # image is stored with the blob, it is only generated once. + def processed: () -> self + + # Returns the blob's attached preview image. + def image: () -> untyped + + # Returns the URL of the preview's variant on the service. Raises ActiveStorage::Preview::UnprocessedError if the + # preview has not been processed yet. + # + # This method synchronously processes a variant of the preview image, so do not call it in views. Instead, generate + # a stable URL that redirects to the URL returned by this method. + def url: (**untyped options) -> untyped + + # Returns a combination key of the blob and the variation that together identifies a specific variant. + def key: () -> untyped + + # Downloads the file associated with this preview's variant. If no block is + # given, the entire file is read into memory and returned. That'll use a lot + # of RAM for very large files. If a block is given, then the download is + # streamed and yielded in chunks. Raises ActiveStorage::Preview::UnprocessedError + # if the preview has not been processed yet. + def download: () { (?) -> untyped } -> untyped + + private + + def processed?: () -> untyped + + def process: () -> untyped + + def variant: () -> untyped + + def variant?: () -> untyped + + def presentation: () -> untyped + + def previewer: () -> untyped + + def previewer_class: () -> untyped +end + +class ActiveStorage::Record +end + +class ActiveStorage::VariantRecord < ::ActiveStorage::Record +end + +# = Active Storage \Variant With Record +# +# Like an ActiveStorage::Variant, but keeps detail about the variant in the database as an +# ActiveStorage::VariantRecord. This is only used if +ActiveStorage.track_variants+ is enabled. +class ActiveStorage::VariantWithRecord + def service: (*untyped, **untyped) ?{ (*untyped) -> untyped } -> untyped + def content_type: (*untyped, **untyped) ?{ (*untyped) -> untyped } -> untyped + def key: (*untyped, **untyped) ?{ (*untyped) -> untyped } -> untyped + def url: (*untyped, **untyped) ?{ (*untyped) -> untyped } -> untyped + def download: (*untyped, **untyped) ?{ (*untyped) -> untyped } -> untyped + + include ::ActiveStorage::Blob::Servable + + attr_reader blob: untyped + + attr_reader variation: untyped + + def initialize: (untyped blob, untyped variation) -> void + + def processed: () -> self + + def image: () -> untyped + + def filename: () -> untyped + + # Destroys record and deletes file from service. + def destroy: () -> untyped + + private + + def processed?: () -> untyped + + def process: () -> untyped + + def transform_blob: () { (io: untyped, filename: untyped, content_type: untyped, service_name: untyped) -> untyped } -> untyped + + def create_or_find_record: (image: untyped) -> untyped + + def record: () -> untyped +end + +# = Active Storage \Variant +# +# Image blobs can have variants that are the result of a set of transformations applied to the original. +# These variants are used to create thumbnails, fixed-size avatars, or any other derivative image from the +# original. +# +# Variants rely on {ImageProcessing}[https://github.com/janko/image_processing] gem for the actual transformations +# of the file, so you must add gem "image_processing" to your Gemfile if you wish to use variants. By +# default, images will be processed with {libvips}[http://libvips.github.io/libvips/] using the +# {ruby-vips}[https://github.com/libvips/ruby-vips] gem, but you can also switch to the +# {ImageMagick}[http://imagemagick.org] processor operated by the {MiniMagick}[https://github.com/minimagick/minimagick] +# gem). +# +# Rails.application.config.active_storage.variant_processor +# # => :vips +# +# Rails.application.config.active_storage.variant_processor = :mini_magick +# # => :mini_magick +# +# Note that to create a variant it's necessary to download the entire blob file from the service. Because of this process, +# you also want to be considerate about when the variant is actually processed. You shouldn't be processing variants inline +# in a template, for example. Delay the processing to an on-demand controller, like the one provided in +# ActiveStorage::Representations::ProxyController and ActiveStorage::Representations::RedirectController. +# +# To refer to such a delayed on-demand variant, simply link to the variant through the resolved route provided +# by Active Storage like so: +# +# <%= image_tag Current.user.avatar.variant(resize_to_limit: [100, 100]) %> +# +# This will create a URL for that specific blob with that specific variant, which the ActiveStorage::Representations::ProxyController +# or ActiveStorage::Representations::RedirectController can then produce on-demand. +# +# When you do want to actually produce the variant needed, call +processed+. This will check that the variant +# has already been processed and uploaded to the service, and, if so, just return that. Otherwise it will perform +# the transformations, upload the variant to the service, and return itself again. Example: +# +# avatar.variant(resize_to_limit: [100, 100]).processed.url +# +# This will create and process a variant of the avatar blob that's constrained to a height and width of 100. +# Then it'll upload said variant to the service according to a derivative key of the blob and the transformations. +# +# You can combine any number of ImageMagick/libvips operations into a variant, as well as any macros provided by the +# ImageProcessing gem (such as +resize_to_limit+): +# +# avatar.variant(resize_to_limit: [800, 800], colourspace: "b-w", rotate: "-90") +# +# Visit the following links for a list of available ImageProcessing commands and ImageMagick/libvips operations: +# +# * {ImageProcessing::MiniMagick}[https://github.com/janko/image_processing/blob/master/doc/minimagick.md#methods] +# * {ImageMagick reference}[https://www.imagemagick.org/script/mogrify.php] +# * {ImageProcessing::Vips}[https://github.com/janko/image_processing/blob/master/doc/vips.md#methods] +# * {ruby-vips reference}[http://www.rubydoc.info/gems/ruby-vips/Vips/Image] +class ActiveStorage::Variant + def service: (*untyped, **untyped) ?{ (*untyped) -> untyped } -> untyped + def content_type: (*untyped, **untyped) ?{ (*untyped) -> untyped } -> untyped + + include ::ActiveStorage::Blob::Servable + + attr_reader blob: untyped + + attr_reader variation: untyped + + def initialize: (untyped blob, untyped variation_or_variation_key) -> void + + # Returns the variant instance itself after it's been processed or an existing processing has been found on the service. + def processed: () -> self + + # Returns a combination key of the blob and the variation that together identifies a specific variant. + def key: () -> ::String + + # Returns the URL of the blob variant on the service. See ActiveStorage::Blob#url for details. + # + # Use url_for(variant) (or the implied form, like link_to variant or redirect_to variant) to get the stable URL + # for a variant that points to the ActiveStorage::Representations::ProxyController or ActiveStorage::Representations::RedirectController, + # which in turn will use this +url+ method for its redirection. + def url: (?expires_in: untyped, ?disposition: ::Symbol) -> untyped + + # Downloads the file associated with this variant. If no block is given, the entire file is read into memory and returned. + # That'll use a lot of RAM for very large files. If a block is given, then the download is streamed and yielded in chunks. + def download: () { (?) -> untyped } -> untyped + + def filename: () -> untyped + + # Returns the receiving variant. Allows ActiveStorage::Variant and ActiveStorage::Preview instances to be used interchangeably. + def image: () -> self + + # Deletes variant file from service. + def destroy: () -> untyped + + private + + def processed?: () -> untyped + + def process: () -> untyped +end + +# = Active Storage \Variation +# +# A set of transformations that can be applied to a blob to create a variant. This class is exposed via +# the ActiveStorage::Blob#variant method and should rarely be used directly. +# +# In case you do need to use this directly, it's instantiated using a hash of transformations where +# the key is the command and the value is the arguments. Example: +# +# ActiveStorage::Variation.new(resize_to_limit: [100, 100], colourspace: "b-w", rotate: "-90", saver: { trim: true }) +# +# The options map directly to {ImageProcessing}[https://github.com/janko/image_processing] commands. +class ActiveStorage::Variation + attr_reader transformations: untyped + + # Returns a Variation instance based on the given variator. If the variator is a Variation, it is + # returned unmodified. If it is a String, it is passed to ActiveStorage::Variation.decode. Otherwise, + # it is assumed to be a transformations Hash and is passed directly to the constructor. + def self.wrap: (untyped variator) -> untyped + + # Returns a Variation instance with the transformations that were encoded by +encode+. + def self.decode: (untyped key) -> untyped + + # Returns a signed key for the +transformations+, which can be used to refer to a specific + # variation in a URL or combined key (like ActiveStorage::Variant#key). + def self.encode: (untyped transformations) -> untyped + + def initialize: (untyped transformations) -> void + + def default_to: (untyped defaults) -> untyped + + # Accepts a File object, performs the +transformations+ against it, and + # saves the transformed image into a temporary file. + def transform: (untyped file) { (?) -> untyped } -> untyped + + def format: () -> untyped + + def content_type: () -> untyped + + # Returns a signed key for all the +transformations+ that this variation was instantiated with. + def key: () -> untyped + + def digest: () -> untyped + + private + + def transformer: () -> untyped +end + +# :markup: markdown +# :include: ../README.md +module ActiveStorage + def self.logger: () -> untyped + + def self.logger=: (untyped value) -> untyped + + def logger: () -> untyped + + def logger=: (untyped value) -> untyped + + def self.verifier: () -> untyped + + def self.verifier=: (untyped value) -> untyped + + def verifier: () -> untyped + + def verifier=: (untyped value) -> untyped + + def self.variant_processor: () -> untyped + + def self.variant_processor=: (untyped value) -> untyped + + def variant_processor: () -> untyped + + def variant_processor=: (untyped value) -> untyped + + def self.variant_transformer: () -> untyped + + def self.variant_transformer=: (untyped value) -> untyped + + def variant_transformer: () -> untyped + + def variant_transformer=: (untyped value) -> untyped + + def self.queues: () -> untyped + + def self.queues=: (untyped value) -> untyped + + def queues: () -> untyped + + def queues=: (untyped value) -> untyped + + def self.previewers: () -> untyped + + def self.previewers=: (untyped value) -> untyped + + def previewers: () -> untyped + + def previewers=: (untyped value) -> untyped + + def self.analyzers: () -> untyped + + def self.analyzers=: (untyped value) -> untyped + + def analyzers: () -> untyped + + def analyzers=: (untyped value) -> untyped + + def self.paths: () -> untyped + + def self.paths=: (untyped value) -> untyped + + def paths: () -> untyped + + def paths=: (untyped value) -> untyped + + def self.variable_content_types: () -> untyped + + def self.variable_content_types=: (untyped value) -> untyped + + def variable_content_types: () -> untyped + + def variable_content_types=: (untyped value) -> untyped + + def self.web_image_content_types: () -> untyped + + def self.web_image_content_types=: (untyped value) -> untyped + + def web_image_content_types: () -> untyped + + def web_image_content_types=: (untyped value) -> untyped + + def self.binary_content_type: () -> untyped + + def self.binary_content_type=: (untyped value) -> untyped + + def binary_content_type: () -> untyped + + def binary_content_type=: (untyped value) -> untyped + + def self.content_types_to_serve_as_binary: () -> untyped + + def self.content_types_to_serve_as_binary=: (untyped value) -> untyped + + def content_types_to_serve_as_binary: () -> untyped + + def content_types_to_serve_as_binary=: (untyped value) -> untyped + + def self.content_types_allowed_inline: () -> untyped + + def self.content_types_allowed_inline=: (untyped value) -> untyped + + def content_types_allowed_inline: () -> untyped + + def content_types_allowed_inline=: (untyped value) -> untyped + + def self.supported_image_processing_methods: () -> untyped + + def self.supported_image_processing_methods=: (untyped value) -> untyped + + def supported_image_processing_methods: () -> untyped + + def supported_image_processing_methods=: (untyped value) -> untyped + + def self.unsupported_image_processing_arguments: () -> untyped + + def self.unsupported_image_processing_arguments=: (untyped value) -> untyped + + def unsupported_image_processing_arguments: () -> untyped + + def unsupported_image_processing_arguments=: (untyped value) -> untyped + + def self.streaming_chunk_max_size: () -> untyped + + def self.streaming_chunk_max_size=: (untyped value) -> untyped + + def streaming_chunk_max_size: () -> untyped + + def streaming_chunk_max_size=: (untyped value) -> untyped + + def self.service_urls_expire_in: () -> untyped + + def self.service_urls_expire_in=: (untyped value) -> untyped + + def service_urls_expire_in: () -> untyped + + def service_urls_expire_in=: (untyped value) -> untyped + + def self.touch_attachment_records: () -> untyped + + def self.touch_attachment_records=: (untyped value) -> untyped + + def touch_attachment_records: () -> untyped + + def touch_attachment_records=: (untyped value) -> untyped + + def self.urls_expire_in: () -> untyped + + def self.urls_expire_in=: (untyped value) -> untyped + + def urls_expire_in: () -> untyped + + def urls_expire_in=: (untyped value) -> untyped + + def self.routes_prefix: () -> untyped + + def self.routes_prefix=: (untyped value) -> untyped + + def routes_prefix: () -> untyped + + def routes_prefix=: (untyped value) -> untyped + + def self.draw_routes: () -> untyped + + def self.draw_routes=: (untyped value) -> untyped + + def draw_routes: () -> untyped + + def draw_routes=: (untyped value) -> untyped + + def self.resolve_model_to_route: () -> untyped + + def self.resolve_model_to_route=: (untyped value) -> untyped + + def resolve_model_to_route: () -> untyped + + def resolve_model_to_route=: (untyped value) -> untyped + + def self.track_variants: () -> untyped + + def self.track_variants=: (untyped value) -> untyped + + def track_variants: () -> untyped + + def track_variants=: (untyped value) -> untyped + + def self.checksum_implementation: () -> untyped + + def self.checksum_implementation=: (untyped value) -> untyped + + def self.streaming_max_ranges: () -> untyped + + def self.streaming_max_ranges=: (untyped value) -> untyped + + def self.video_preview_arguments: () -> untyped + + def self.video_preview_arguments=: (untyped value) -> untyped + + def video_preview_arguments: () -> untyped + + def video_preview_arguments=: (untyped value) -> untyped + + extend ::ActiveSupport::Autoload + + module Transformers + extend ::ActiveSupport::Autoload + end +end + +module ActiveStorage + # = Active Storage \Analyzer + # + # This is an abstract base class for analyzers, which extract metadata from blobs. See + # ActiveStorage::Analyzer::VideoAnalyzer for an example of a concrete subclass. + class Analyzer + attr_reader blob: untyped + + # Implement this method in a concrete subclass. Have it return true when given a blob from which + # the analyzer can extract metadata. + def self.accept?: (untyped blob) -> false + + # Implement this method in concrete subclasses. It will determine if blob analysis + # should be done in a job or performed inline. By default, analysis is enqueued in a job. + def self.analyze_later?: () -> true + + def initialize: (untyped blob) -> void + + # Override this method in a concrete subclass. Have it return a Hash of metadata. + def metadata: () -> untyped + + private + + # Downloads the blob to a tempfile on disk. Yields the tempfile. + def download_blob_to_tempfile: () { (?) -> untyped } -> untyped + + def logger: () -> untyped + + def tmpdir: () -> untyped + + def instrument: (untyped analyzer) { (?) -> untyped } -> untyped + end +end + +module ActiveStorage + # = Active Storage Audio \Analyzer + # + # Extracts duration (seconds), bit_rate (bits/s), sample_rate (hertz) and tags (internal metadata) from an audio blob. + # + # Example: + # + # ActiveStorage::Analyzer::AudioAnalyzer.new(blob).metadata + # # => { duration: 5.0, bit_rate: 320340, sample_rate: 44100, tags: { encoder: "Lavc57.64", ... } } + # + # This analyzer requires the {FFmpeg}[https://www.ffmpeg.org] system library, which is not provided by \Rails. + class Analyzer::AudioAnalyzer < ::ActiveStorage::Analyzer + def self.accept?: (untyped blob) -> untyped + + def metadata: () -> untyped + + private + + def duration: () -> untyped + + def bit_rate: () -> untyped + + def sample_rate: () -> untyped + + def tags: () -> untyped + + def audio_stream: () -> untyped + + def streams: () -> untyped + + def probe: () -> untyped + + def probe_from: (untyped file) -> untyped + + def ffprobe_path: () -> untyped + end +end + +module ActiveStorage + # = Active Storage Image \Analyzer + # + # This is an abstract base class for image analyzers, which extract width and height from an image blob. + # + # If the image contains EXIF data indicating its angle is 90 or 270 degrees, its width and height are swapped for convenience. + # + # Example: + # + # ActiveStorage::Analyzer::ImageAnalyzer::ImageMagick.new(blob).metadata + # # => { width: 4104, height: 2736 } + class Analyzer::ImageAnalyzer < ::ActiveStorage::Analyzer + extend ::ActiveSupport::Autoload + + def self.accept?: (untyped blob) -> untyped + + def metadata: () -> untyped + end +end + +module ActiveStorage + # This analyzer relies on the third-party {MiniMagick}[https://github.com/minimagick/minimagick] gem. MiniMagick requires + # the {ImageMagick}[http://www.imagemagick.org] system library. + class Analyzer::ImageAnalyzer::ImageMagick < ::ActiveStorage::Analyzer::ImageAnalyzer + def self.accept?: (untyped blob) -> untyped + + private + + def read_image: () { (untyped) -> untyped } -> (::Hash[untyped, untyped] | untyped) + + def rotated_image?: (untyped image) -> untyped + end +end + +module ActiveStorage + # This analyzer relies on the third-party {ruby-vips}[https://github.com/libvips/ruby-vips] gem. Ruby-vips requires + # the {libvips}[https://libvips.github.io/libvips/] system library. + class Analyzer::ImageAnalyzer::Vips < ::ActiveStorage::Analyzer::ImageAnalyzer + def self.accept?: (untyped blob) -> untyped + + private + + def read_image: () { (untyped) -> untyped } -> untyped + + ROTATIONS: ::Regexp + + def rotated_image?: (untyped image) -> untyped + end +end + +module ActiveStorage + class Analyzer::NullAnalyzer < ::ActiveStorage::Analyzer + def self.accept?: (untyped blob) -> true + + def self.analyze_later?: () -> false + + def metadata: () -> ::Hash[untyped, untyped] + end +end + +module ActiveStorage + # = Active Storage Video \Analyzer + # + # Extracts the following from a video blob: + # + # * Width (pixels) + # * Height (pixels) + # * Duration (seconds) + # * Angle (degrees) + # * Display aspect ratio + # * Audio (true if file has an audio channel, false if not) + # * Video (true if file has an video channel, false if not) + # + # Example: + # + # ActiveStorage::Analyzer::VideoAnalyzer.new(blob).metadata + # # => { width: 640.0, height: 480.0, duration: 5.0, angle: 0, display_aspect_ratio: [4, 3], audio: true, video: true } + # + # When a video's angle is 90, -90, 270 or -270 degrees, its width and height are automatically swapped for convenience. + # + # This analyzer requires the {FFmpeg}[https://www.ffmpeg.org] system library, which is not provided by \Rails. + class Analyzer::VideoAnalyzer < ::ActiveStorage::Analyzer + def self.accept?: (untyped blob) -> untyped + + def metadata: () -> untyped + + private + + def width: () -> untyped + + def height: () -> untyped + + def duration: () -> untyped + + def angle: () -> (untyped | nil) + + def display_matrix: () -> untyped + + def display_aspect_ratio: () -> (untyped | nil) + + def rotated?: () -> untyped + + def audio?: () -> untyped + + def video?: () -> untyped + + def computed_height: () -> (untyped | nil) + + def encoded_width: () -> (untyped | nil) + + def encoded_height: () -> (untyped | nil) + + def display_height_scale: () -> (untyped | nil) + + def tags: () -> untyped + + def side_data: () -> untyped + + def video_stream: () -> untyped + + def audio_stream: () -> untyped + + def streams: () -> untyped + + def container: () -> untyped + + def probe: () -> untyped + + def probe_from: (untyped file) -> untyped + + def ffprobe_path: () -> untyped + end +end + +module ActiveStorage + # = Active Storage \Attached + # + # Abstract base class for the concrete ActiveStorage::Attached::One and ActiveStorage::Attached::Many + # classes that both provide proxy access to the blob association for a record. + class Attached + private + + def change: () -> untyped + end +end + +module ActiveStorage + module Attached::Changes + extend ::ActiveSupport::Autoload + end +end + +module ActiveStorage + class Attached::Changes::CreateMany + attr_reader name: untyped + + attr_reader attachables: untyped + + attr_reader pending_uploads: untyped + + attr_accessor record: untyped + + def initialize: (untyped name, untyped record, untyped attachables, ?pending_uploads: untyped) -> void + + def attachments: () -> untyped + + def blobs: () -> untyped + + def upload: () -> untyped + + def save: () -> untyped + + private + + def subchanges: () -> untyped + + def build_subchange_from: (untyped attachable) -> untyped + + def subchanges_without_blobs: () -> untyped + + def assign_associated_attachments: () -> untyped + + def reset_associated_blobs: () -> untyped + + def persisted_or_new_attachments: () -> untyped + end +end + +module ActiveStorage + class Attached::Changes::CreateOneOfMany < ::ActiveStorage::Attached::Changes::CreateOne + private + + def find_attachment: () -> untyped + end +end + +module ActiveStorage + class Attached::Changes::CreateOne + attr_reader name: untyped + + attr_reader attachable: untyped + + attr_accessor record: untyped + + def initialize: (untyped name, untyped record, untyped attachable) -> void + + def attachment: () -> untyped + + def blob: () -> untyped + + def upload: () -> untyped + + def save: () -> untyped + + private + + def find_or_build_attachment: () -> untyped + + def find_attachment: () -> (untyped | nil) + + def build_attachment: () -> untyped + + def find_or_build_blob: () -> untyped + + def attachment_service_name: () -> untyped + end +end + +module ActiveStorage + class Attached::Changes::DeleteMany + attr_reader name: untyped + + attr_accessor record: untyped + + def initialize: (untyped name, untyped record) -> void + + def attachables: () -> ::Array[untyped] + + def attachments: () -> untyped + + def blobs: () -> untyped + + def save: () -> untyped + end +end + +module ActiveStorage + class Attached::Changes::DeleteOne + attr_reader name: untyped + + attr_accessor record: untyped + + def initialize: (untyped name, untyped record) -> void + + def attachment: () -> nil + + def save: () -> untyped + end +end + +module ActiveStorage + class Attached::Changes::DetachMany + attr_reader name: untyped + + attr_reader record: untyped + + attr_reader attachments: untyped + + def initialize: (untyped name, untyped record, untyped attachments) -> void + + def detach: () -> (untyped | nil) + end +end + +module ActiveStorage + class Attached::Changes::DetachOne + attr_reader name: untyped + + attr_reader record: untyped + + attr_reader attachment: untyped + + def initialize: (untyped name, untyped record, untyped attachment) -> void + + def detach: () -> (untyped | nil) + + private + + def reset: () -> untyped + end +end + +module ActiveStorage + class Attached::Changes::PurgeMany + attr_reader name: untyped + + attr_reader record: untyped + + attr_reader attachments: untyped + + def initialize: (untyped name, untyped record, untyped attachments) -> void + + def purge: () -> untyped + + def purge_later: () -> untyped + + private + + def reset: () -> untyped + end +end + +module ActiveStorage + class Attached::Changes::PurgeOne + attr_reader name: untyped + + attr_reader record: untyped + + attr_reader attachment: untyped + + def initialize: (untyped name, untyped record, untyped attachment) -> void + + def purge: () -> untyped + + def purge_later: () -> untyped + + private + + def reset: () -> untyped + end +end + +module ActiveStorage + # = Active Storage \Attached \Many + # + # Decorated proxy object representing of multiple attachments to a model. + class Attached::Many + def purge: (*untyped, **untyped) ?{ (*untyped) -> untyped } -> untyped + def purge_later: (*untyped, **untyped) ?{ (*untyped) -> untyped } -> untyped + + def as_json: (?untyped? options) -> untyped + + private + + def purge_many: () -> untyped + + def detach_many: () -> untyped + end +end + +module ActiveStorage + # = Active Storage \Attached \Model + # + # Provides the class-level DSL for declaring an Active Record model's attachments. + module Attached::Model + extend ::ActiveSupport::Concern + + def self.validate_service_configuration: (untyped service_name, untyped model_class, untyped association_name) -> untyped + + def self.validate_global_service_configuration: (untyped model_class) -> (untyped | nil) + + attr_writer attachment_changes: untyped + + def initialize_dup: (*untyped) -> untyped + + def becomes: (untyped klass) -> untyped + end +end + +module ActiveStorage + # = Active Storage \Attached \One + # + # Representation of a single attachment to a model. + class Attached::One + # Returns the attachment record's JSON representation, or +nil+ when no + # attachment is present. + def as_json: (?untyped? options) -> (untyped | nil) + + private + + def purge_one: () -> untyped + + def detach_one: () -> untyped + end +end + +module ActiveStorage + def self.deprecator: () -> untyped +end + +module ActiveStorage + class Downloader + attr_reader service: untyped + + def initialize: (untyped service) -> void + + def open: (untyped key, ?checksum: untyped?, ?verify: bool, ?name: ::String, ?tmpdir: untyped?) { (untyped) -> untyped } -> untyped + + private + + def open_tempfile: (untyped name, ?untyped? tmpdir) { (untyped) -> untyped } -> untyped + + def download: (untyped key, untyped file) -> untyped + + def verify_integrity_of: (untyped file, checksum: untyped) -> (untyped | nil) + end +end + +module ActiveStorage + class Engine < ::Rails::Engine + end +end + +module ActiveStorage + # Generic base class for all Active Storage exceptions. + class Error < ::StandardError + end + + # Raised when ActiveStorage::Blob#variant is called on a blob that isn't variable. + # Use ActiveStorage::Blob#variable? to determine whether a blob is variable. + class InvariableError < ::ActiveStorage::Error + end + + # Raised when ActiveStorage::Blob#preview is called on a blob that isn't previewable. + # Use ActiveStorage::Blob#previewable? to determine whether a blob is previewable. + class UnpreviewableError < ::ActiveStorage::Error + end + + # Raised when ActiveStorage::Blob#representation is called on a blob that isn't representable. + # Use ActiveStorage::Blob#representable? to determine whether a blob is representable. + class UnrepresentableError < ::ActiveStorage::Error + end + + # Raised when uploaded or downloaded data does not match a precomputed checksum. + # Indicates that a network error or a software bug caused data corruption. + class IntegrityError < ::ActiveStorage::Error + end + + # Raised when ActiveStorage::Blob#download is called on a blob where the + # backing file is no longer present in its service. + class FileNotFoundError < ::ActiveStorage::Error + end + + # Raised when a Previewer is unable to generate a preview image. + class PreviewError < ::ActiveStorage::Error + end + + # Raised when a storage key resolves to a path outside the service's root + # directory, indicating a potential path traversal attack. + class InvalidKeyError < ::ActiveStorage::Error + end +end + +module ActiveStorage + # = Active Storage \FixtureSet + # + # Fixtures are a way of organizing data that you want to test against; in + # short, sample data. + # + # To learn more about fixtures, read the ActiveRecord::FixtureSet documentation. + # + # === YAML + # + # Like other Active Record-backed models, ActiveStorage::Attachment and + # ActiveStorage::Blob records inherit from ActiveRecord::Base instances and + # therefore can be populated by fixtures. + # + # Consider a hypothetical Article model class, its related + # fixture data, as well as fixture data for related ActiveStorage::Attachment + # and ActiveStorage::Blob records: + # + # # app/models/article.rb + # class Article < ApplicationRecord + # has_one_attached :thumbnail + # end + # + # + # + # # fixtures/active_storage/blobs.yml + # first_thumbnail_blob: <%= ActiveStorage::FixtureSet.blob filename: "first.png" %> + # + # + # + # # fixtures/active_storage/attachments.yml + # first_thumbnail_attachment: + # name: thumbnail + # record: first (Article) + # blob: first_thumbnail_blob + # + # When processed, Active Record will insert database records for each fixture + # entry and will ensure the Active Storage relationship is intact. + class FixtureSet + include ::ActiveSupport::Testing::FileFixtures + + include ::ActiveRecord::SecureToken + + # Generate a YAML-encoded representation of an ActiveStorage::Blob + # instance's attributes, resolve the file relative to the directory mentioned + # by ActiveSupport::Testing::FileFixtures.file_fixture, and upload + # the file to the Service + # + # ==== Examples + # + # # tests/fixtures/active_storage/blobs.yml + # second_thumbnail_blob: <%= ActiveStorage::FixtureSet.blob( + # filename: "second.svg", + # ) %> + # + # third_thumbnail_blob: <%= ActiveStorage::FixtureSet.blob( + # filename: "third.svg", + # content_type: "image/svg+xml", + # service_name: "public" + # ) %> + # + def self.blob: (filename: untyped, **untyped attributes) -> untyped + + def prepare: (untyped instance, **untyped attributes) -> untyped + end +end + +module ActiveStorage + # Returns the currently loaded version of Active Storage as a +Gem::Version+. + def self.gem_version: () -> untyped + + module VERSION + MAJOR: 8 + + MINOR: 1 + + TINY: 4 + + PRE: nil + + STRING: untyped + end +end + +module ActiveStorage + class LogSubscriber < ::ActiveSupport::LogSubscriber + def service_upload: (untyped event) -> untyped + + def service_download: (untyped event) -> untyped + + alias service_streaming_download service_download + + def preview: (untyped event) -> untyped + + def service_delete: (untyped event) -> untyped + + def service_delete_prefixed: (untyped event) -> untyped + + def service_exist: (untyped event) -> untyped + + def service_url: (untyped event) -> untyped + + def service_mirror: (untyped event) -> untyped + + def logger: () -> untyped + + private + + def info: (untyped event, untyped colored_message) -> untyped + + def debug: (untyped event, untyped colored_message) -> untyped + + def log_prefix_for_service: (untyped event) -> untyped + + def key_in: (untyped event) -> untyped + end +end + +module ActiveStorage + # = Active Storage \Previewer + # + # This is an abstract base class for previewers, which generate images from blobs. See + # ActiveStorage::Previewer::MuPDFPreviewer and ActiveStorage::Previewer::VideoPreviewer for + # examples of concrete subclasses. + class Previewer + attr_reader blob: untyped + + # Implement this method in a concrete subclass. Have it return true when given a blob from which + # the previewer can generate an image. + def self.accept?: (untyped blob) -> false + + def initialize: (untyped blob) -> void + + # Override this method in a concrete subclass. Have it yield an attachable preview image (i.e. + # anything accepted by ActiveStorage::Attached::One#attach). Pass the additional options to + # the underlying blob that is created. + def preview: (**untyped options) -> untyped + + private + + # Downloads the blob to a tempfile on disk. Yields the tempfile. + def download_blob_to_tempfile: () { (?) -> untyped } -> untyped + + # Executes a system command, capturing its binary output in a tempfile. Yields the tempfile. + # + # Use this method to shell out to a system library (e.g. muPDF or FFmpeg) for preview image + # generation. The resulting tempfile can be used as the +:io+ value in an attachable Hash: + # + # def preview + # download_blob_to_tempfile do |input| + # draw "my-drawing-command", input.path, "--format", "png", "-" do |output| + # yield io: output, filename: "#{blob.filename.base}.png", content_type: "image/png" + # end + # end + # end + # + # The output tempfile is opened in the directory returned by #tmpdir. + def draw: (*untyped argv) { (untyped) -> untyped } -> untyped + + def open_tempfile: () { (untyped) -> untyped } -> untyped + + def instrument: (untyped operation, ?::Hash[untyped, untyped] payload) { (?) -> untyped } -> untyped + + def service_name: () -> untyped + + def capture: (*untyped argv, to: untyped) -> untyped + + def logger: () -> untyped + + def tmpdir: () -> untyped + end +end + +module ActiveStorage + class Previewer::MuPDFPreviewer < ::ActiveStorage::Previewer + def self.accept?: (untyped blob) -> untyped + + def self.pdf?: (untyped content_type) -> untyped + + def self.mutool_path: () -> untyped + + def self.mutool_exists?: () -> untyped + + def preview: (**untyped options) { (untyped) -> untyped } -> untyped + + private + + def draw_first_page_from: (untyped file) { (?) -> untyped } -> untyped + end +end + +module ActiveStorage + class Previewer::PopplerPDFPreviewer < ::ActiveStorage::Previewer + def self.accept?: (untyped blob) -> untyped + + def self.pdf?: (untyped content_type) -> untyped + + def self.pdftoppm_path: () -> untyped + + def self.pdftoppm_exists?: () -> untyped + + def preview: (**untyped options) { (untyped) -> untyped } -> untyped + + private + + def draw_first_page_from: (untyped file) { (?) -> untyped } -> untyped + end +end + +module ActiveStorage + class Previewer::VideoPreviewer < ::ActiveStorage::Previewer + def self.accept?: (untyped blob) -> untyped + + def self.ffmpeg_exists?: () -> untyped + + def self.ffmpeg_path: () -> untyped + + def preview: (**untyped options) { (untyped) -> untyped } -> untyped + + private + + def draw_relevant_frame_from: (untyped file) { (?) -> untyped } -> untyped + end +end + +module ActiveStorage + module Reflection + class HasAttachedReflection < ::ActiveRecord::Reflection::MacroReflection + def variant: (untyped name, untyped transformations) -> untyped + + def named_variants: () -> untyped + end + + # Holds all the metadata about a has_one_attached attachment as it was + # specified in the Active Record class. + class HasOneAttachedReflection + end + + # Holds all the metadata about a has_many_attached attachment as it was + # specified in the Active Record class. + class HasManyAttachedReflection + end + + module ReflectionExtension + def add_attachment_reflection: (untyped model, untyped name, untyped reflection) -> untyped + + private + + def reflection_class_for: (untyped macro) -> untyped + end + + module ActiveRecordExtensions + def attachment_reflections: () -> untyped + + def attachment_reflections?: () -> bool + + extend ::ActiveSupport::Concern + + module ClassMethods + def attachment_reflections: () -> untyped + + def attachment_reflections=: (untyped value) -> untyped + + def attachment_reflections?: () -> bool + end + end + end +end + +module ActiveStorage + # = Active Storage \Service + # + # Abstract class serving as an interface for concrete services. + # + # The available services are: + # + # * +Disk+, to manage attachments saved directly on the hard drive. + # * +GCS+, to manage attachments through Google Cloud Storage. + # * +S3+, to manage attachments through Amazon S3. + # * +Mirror+, to be able to use several services to manage attachments. + # + # Inside a \Rails application, you can set-up your services through the + # generated config/storage.yml file and reference one + # of the aforementioned constant under the +service+ key. For example: + # + # local: + # service: Disk + # root: <%= Rails.root.join("storage") %> + # + # You can checkout the service's constructor to know which keys are required. + # + # Then, in your application's configuration, you can specify the service to + # use like this: + # + # config.active_storage.service = :local + # + # If you are using Active Storage outside of a Ruby on \Rails application, you + # can configure the service to use like this: + # + # ActiveStorage::Blob.service = ActiveStorage::Service.configure( + # :local, + # { local: {service: "Disk", root: Pathname("/tmp/foo/storage") } } + # ) + class Service + extend ::ActiveSupport::Autoload + + attr_accessor name: untyped + + # Configure an Active Storage service by name from a set of configurations, + # typically loaded from a YAML file. The Active Storage engine uses this + # to set the global Active Storage service when the app boots. + def self.configure: (untyped service_name, untyped configurations) -> untyped + + # Override in subclasses that stitch together multiple services and hence + # need to build additional services using the configurator. + # + # Passes the configurator and all of the service's config as keyword args. + # + # See MirrorService for an example. + def self.build: (configurator: untyped, name: untyped, ?service: untyped?, **untyped service_config) -> untyped + + # Upload the +io+ to the +key+ specified. If a +checksum+ is provided, the service will + # ensure a match when the upload has completed or raise an ActiveStorage::IntegrityError. + def upload: (untyped key, untyped io, ?checksum: untyped?, **untyped options) -> untyped + + # Update metadata for the file identified by +key+ in the service. + # Override in subclasses only if the service needs to store specific + # metadata that has to be updated upon identification. + def update_metadata: (untyped key, **untyped metadata) -> nil + + # Return the content of the file at the +key+. + def download: (untyped key) -> untyped + + # Return the partial content in the byte +range+ of the file at the +key+. + def download_chunk: (untyped key, untyped range) -> untyped + + def open: (*untyped args, **untyped options) { (?) -> untyped } -> untyped + + # Concatenate multiple files into a single "composed" file. + def compose: (untyped source_keys, untyped destination_key, ?filename: untyped?, ?content_type: untyped?, ?disposition: untyped?, ?custom_metadata: ::Hash[untyped, untyped]) -> untyped + + # Delete the file at the +key+. + def delete: (untyped key) -> untyped + + # Delete files at keys starting with the +prefix+. + def delete_prefixed: (untyped prefix) -> untyped + + # Return +true+ if a file exists at the +key+. + def exist?: (untyped key) -> untyped + + # Returns the URL for the file at the +key+. This returns a permanent URL for public files, and returns a + # short-lived URL for private files. For private files you can provide the +disposition+ (+:inline+ or +:attachment+), + # +filename+, and +content_type+ that you wish the file to be served with on request. Additionally, you can also provide + # the amount of seconds the URL will be valid for, specified in +expires_in+. + def url: (untyped key, **untyped options) -> untyped + + # Returns a signed, temporary URL that a direct upload file can be PUT to on the +key+. + # The URL will be valid for the amount of seconds specified in +expires_in+. + # You must also provide the +content_type+, +content_length+, and +checksum+ of the file + # that will be uploaded. All these attributes will be validated by the service upon upload. + def url_for_direct_upload: (untyped key, expires_in: untyped, content_type: untyped, content_length: untyped, checksum: untyped, ?custom_metadata: ::Hash[untyped, untyped]) -> untyped + + # Returns a Hash of headers for +url_for_direct_upload+ requests. + def headers_for_direct_upload: (untyped key, filename: untyped, content_type: untyped, content_length: untyped, checksum: untyped, ?custom_metadata: ::Hash[untyped, untyped]) -> ::Hash[untyped, untyped] + + def public?: () -> untyped + + def inspect: () -> ::String + + private + + def private_url: (untyped key, expires_in: untyped, filename: untyped, disposition: untyped, content_type: untyped, **untyped) -> untyped + + def public_url: (untyped key, **untyped) -> untyped + + def custom_metadata_headers: (untyped metadata) -> untyped + + def instrument: (untyped operation, ?::Hash[untyped, untyped] payload) { (?) -> untyped } -> untyped + + def service_name: () -> untyped + + def content_disposition_with: (filename: untyped, ?type: ::String) -> untyped + end +end + +module ActiveStorage + class Service::Configurator + attr_reader configurations: untyped + + def self.build: (untyped service_name, untyped configurations) -> untyped + + def initialize: (untyped configurations) -> void + + def build: (untyped service_name) -> untyped + + def inspect: () -> ::String + + private + + def config_for: (untyped name) -> untyped + + def resolve: (untyped class_name) -> untyped + end +end + +module ActiveStorage + # = Active Storage \Disk \Service + # + # Wraps a local disk path as an Active Storage service. See ActiveStorage::Service for the generic API + # documentation that applies to all services. + class Service::DiskService < ::ActiveStorage::Service + attr_accessor root: untyped + + def initialize: (root: untyped, ?public: bool, **untyped options) -> void + + def upload: (untyped key, untyped io, ?checksum: untyped?, **untyped) -> untyped + + def download: (untyped key) ?{ (?) -> untyped } -> untyped + + def download_chunk: (untyped key, untyped range) -> untyped + + def delete: (untyped key) -> untyped + + def delete_prefixed: (untyped prefix) -> untyped + + def exist?: (untyped key) -> untyped + + def url_for_direct_upload: (untyped key, expires_in: untyped, content_type: untyped, content_length: untyped, checksum: untyped, ?custom_metadata: ::Hash[untyped, untyped]) -> untyped + + def headers_for_direct_upload: (untyped key, content_type: untyped, **untyped) -> { "Content-Type" => untyped } + + # Every filesystem operation in DiskService resolves paths through this method (or through + # make_path_for, which delegates here). This is the primary filesystem security check: all + # path-traversal protection is enforced here. New methods that touch the filesystem MUST use + # path_for or make_path_for -- never construct paths from +root+ directly. + def path_for: (untyped key) -> untyped + + def compose: (untyped source_keys, untyped destination_key, **untyped) -> untyped + + private + + def private_url: (untyped key, expires_in: untyped, filename: untyped, content_type: untyped, disposition: untyped, **untyped) -> untyped + + def public_url: (untyped key, filename: untyped, ?content_type: untyped?, ?disposition: ::Symbol, **untyped) -> untyped + + def generate_url: (untyped key, expires_in: untyped, filename: untyped, content_type: untyped, disposition: untyped) -> untyped + + def stream: (untyped key) { (untyped) -> untyped } -> untyped + + def folder_for: (untyped key) -> untyped + + def escape_glob_metacharacters: (untyped path) -> untyped + + def make_path_for: (untyped key) -> untyped + + def ensure_integrity_of: (untyped key, untyped checksum) -> (untyped | nil) + + def url_helpers: () -> untyped + + def url_options: () -> untyped + end +end + +module ActiveStorage + # = Active Storage \GCS \Service + # + # Wraps the Google Cloud Storage as an Active Storage service. See ActiveStorage::Service for the generic API + # documentation that applies to all services. + class Service::GCSService < ::ActiveStorage::Service + class MetadataServerError < ::ActiveStorage::Error + end + + class MetadataServerNotFoundError < ::ActiveStorage::Error + end + + def initialize: (?public: bool, **untyped config) -> void + + def upload: (untyped key, untyped io, ?checksum: untyped?, ?content_type: untyped?, ?disposition: untyped?, ?filename: untyped?, ?custom_metadata: ::Hash[untyped, untyped]) -> untyped + + def download: (untyped key) ?{ (?) -> untyped } -> untyped + + def update_metadata: (untyped key, content_type: untyped, ?disposition: untyped?, ?filename: untyped?, ?custom_metadata: ::Hash[untyped, untyped]) -> untyped + + def download_chunk: (untyped key, untyped range) -> untyped + + def delete: (untyped key) -> untyped + + def delete_prefixed: (untyped prefix) -> untyped + + def exist?: (untyped key) -> untyped + + def url_for_direct_upload: (untyped key, expires_in: untyped, checksum: untyped, ?custom_metadata: ::Hash[untyped, untyped], **untyped) -> untyped + + def headers_for_direct_upload: (untyped key, checksum: untyped, ?filename: untyped?, ?disposition: untyped?, ?custom_metadata: ::Hash[untyped, untyped], **untyped) -> untyped + + def compose: (untyped source_keys, untyped destination_key, ?filename: untyped?, ?content_type: untyped?, ?disposition: untyped?, ?custom_metadata: ::Hash[untyped, untyped]) -> untyped + + # Returns the IAM client used for direct uploads and signed URLs. + # By default, the authorization for the IAM client is set to + # {Application Default Credentials}[https://docs.cloud.google.com/docs/authentication/application-default-credentials], + # fetched once at instantiation time, then refreshed automatically when expired. + # This can be set to a different value to use other authorization methods. + # + # ActiveStorage::Blob.service.iam_client.authorization = Google::Auth::ImpersonatedServiceAccountCredentials.new(options) + def iam_client: () -> untyped + + private + + def private_url: (untyped key, expires_in: untyped, filename: untyped, content_type: untyped, disposition: untyped, **untyped) -> untyped + + def public_url: (untyped key, **untyped) -> untyped + + attr_reader config: untyped + + def file_for: (untyped key, ?skip_lookup: bool) -> untyped + + # Reads the file for the given key in chunks, yielding each to the block. + def stream: (untyped key) { (untyped) -> untyped } -> untyped + + def bucket: () -> untyped + + def client: () -> untyped + + def issuer: () -> untyped + + def email_from_metadata_server: () -> untyped + + def signer: () -> untyped + + def custom_metadata_headers: (untyped metadata) -> untyped + end +end + +module ActiveStorage + # = Active Storage Mirror \Service + # + # Wraps a set of mirror services and provides a single ActiveStorage::Service object that will all + # have the files uploaded to them. A +primary+ service is designated to answer calls to: + # * +download+ + # * +exist?+ + # * +url+ + # * +url_for_direct_upload+ + # * +headers_for_direct_upload+ + class Service::MirrorService < ::ActiveStorage::Service + def download: (*untyped, **untyped) ?{ (*untyped) -> untyped } -> untyped + def download_chunk: (*untyped, **untyped) ?{ (*untyped) -> untyped } -> untyped + def exist?: (*untyped, **untyped) ?{ (*untyped) -> untyped } -> untyped + def url: (*untyped, **untyped) ?{ (*untyped) -> untyped } -> untyped + def url_for_direct_upload: (*untyped, **untyped) ?{ (*untyped) -> untyped } -> untyped + def headers_for_direct_upload: (*untyped, **untyped) ?{ (*untyped) -> untyped } -> untyped + def path_for: (*untyped, **untyped) ?{ (*untyped) -> untyped } -> untyped + def compose: (*untyped, **untyped) ?{ (*untyped) -> untyped } -> untyped + + attr_reader primary: untyped + + attr_reader mirrors: untyped + + # Stitch together from named services. + def self.build: (primary: untyped, mirrors: untyped, name: untyped, configurator: untyped, **untyped options) -> untyped + + def initialize: (primary: untyped, mirrors: untyped) -> void + + # Upload the +io+ to the +key+ specified to all services. The upload to the primary service is done synchronously + # whereas the upload to the mirrors is done asynchronously. If a +checksum+ is provided, all services will + # ensure a match when the upload has completed or raise an ActiveStorage::IntegrityError. + def upload: (untyped key, untyped io, ?checksum: untyped?, **untyped options) -> untyped + + # Delete the file at the +key+ on all services. + def delete: (untyped key) -> untyped + + # Delete files at keys starting with the +prefix+ on all services. + def delete_prefixed: (untyped prefix) -> untyped + + def mirror_later: (untyped key, checksum: untyped) -> untyped + + # Copy the file at the +key+ from the primary service to each of the mirrors where it doesn't already exist. + def mirror: (untyped key, checksum: untyped) -> untyped + + private + + def each_service: () { (?) -> untyped } -> untyped + + def perform_across_services: (untyped method, *untyped args) -> untyped + end +end + +module ActiveStorage + class Service::Registry + def initialize: (untyped configurations) -> void + + def fetch: (untyped name) ?{ (untyped) -> untyped } -> untyped + + def inspect: () -> ::String + + private + + attr_reader configurations: untyped + + attr_reader services: untyped + + def configurator: () -> untyped + end +end + +module ActiveStorage + # = Active Storage \S3 \Service + # + # Wraps the Amazon Simple Storage Service (S3) as an Active Storage service. + # See ActiveStorage::Service for the generic API documentation that applies to all services. + class Service::S3Service < ::ActiveStorage::Service + attr_reader client: untyped + + attr_reader bucket: untyped + + attr_reader multipart_upload_threshold: untyped + + attr_reader upload_options: untyped + + def initialize: (bucket: untyped, ?upload: ::Hash[untyped, untyped], ?public: bool, **untyped options) -> void + + def upload: (untyped key, untyped io, ?checksum: untyped?, ?filename: untyped?, ?content_type: untyped?, ?disposition: untyped?, ?custom_metadata: ::Hash[untyped, untyped], **untyped) -> untyped + + def download: (untyped key) ?{ (?) -> untyped } -> untyped + + def download_chunk: (untyped key, untyped range) -> untyped + + def delete: (untyped key) -> untyped + + def delete_prefixed: (untyped prefix) -> untyped + + def exist?: (untyped key) -> untyped + + def url_for_direct_upload: (untyped key, expires_in: untyped, content_type: untyped, content_length: untyped, checksum: untyped, ?custom_metadata: ::Hash[untyped, untyped]) -> untyped + + def headers_for_direct_upload: (untyped key, content_type: untyped, checksum: untyped, ?filename: untyped?, ?disposition: untyped?, ?custom_metadata: ::Hash[untyped, untyped], **untyped) -> ::Hash["Content-Type" | "Content-MD5" | "Content-Disposition" | untyped, untyped] + + def compose: (untyped source_keys, untyped destination_key, ?filename: untyped?, ?content_type: untyped?, ?disposition: untyped?, ?custom_metadata: ::Hash[untyped, untyped]) -> untyped + + private + + def upload_stream: (key: untyped, **untyped options) { (?) -> untyped } -> untyped + + def private_url: (untyped key, expires_in: untyped, filename: untyped, disposition: untyped, content_type: untyped, **untyped client_opts) -> untyped + + def public_url: (untyped key, **untyped client_opts) -> untyped + + MAXIMUM_UPLOAD_PARTS_COUNT: 10000 + + MINIMUM_UPLOAD_PART_SIZE: untyped + + def upload_with_single_part: (untyped key, untyped io, ?checksum: untyped?, ?content_type: untyped?, ?content_disposition: untyped?, ?custom_metadata: ::Hash[untyped, untyped]) -> untyped + + def upload_with_multipart: (untyped key, untyped io, ?content_type: untyped?, ?content_disposition: untyped?, ?custom_metadata: ::Hash[untyped, untyped]) -> untyped + + def object_for: (untyped key) -> untyped + + # Reads the object for the given key in chunks, yielding each to the block. + def stream: (untyped key) { (untyped) -> untyped } -> untyped + + def custom_metadata_headers: (untyped metadata) -> untyped + end +end + +module ActiveStorage + class StructuredEventSubscriber < ::ActiveSupport::StructuredEventSubscriber + def service_upload: (untyped event) -> untyped + + def service_download: (untyped event) -> untyped + + def service_streaming_download: (untyped event) -> untyped + + def preview: (untyped event) -> untyped + + def service_delete: (untyped event) -> untyped + + def service_delete_prefixed: (untyped event) -> untyped + + def service_exist: (untyped event) -> untyped + + def service_url: (untyped event) -> untyped + + def service_mirror: (untyped event) -> untyped + end +end + +module ActiveStorage + module Transformers + class ImageMagick < ::ActiveStorage::Transformers::ImageProcessingTransformer + private + + def processor: () -> untyped + + def validate_transformation: (untyped name, untyped argument) -> untyped + + def validate_arg_string: (untyped argument) -> untyped + + def validate_arg_array: (untyped argument) -> untyped + + def validate_arg_hash: (untyped argument) -> untyped + end + end +end + +module ActiveStorage + module Transformers + class ImageProcessingTransformer < ::ActiveStorage::Transformers::Transformer + private + + class UnsupportedImageProcessingMethod < ::StandardError + end + + class UnsupportedImageProcessingArgument < ::StandardError + end + + def process: (untyped file, format: untyped) -> untyped + + def operations: () -> untyped + + def validate_transformation: (untyped name, untyped argument) -> (untyped | nil) + end + end +end + +module ActiveStorage + module Transformers + class NullTransformer < ::ActiveStorage::Transformers::Transformer + private + + def process: (untyped file, format: untyped) -> untyped + end + end +end + +module ActiveStorage + module Transformers + # = Active Storage \Transformers \Transformer + # + # A Transformer applies a set of transformations to an image. + # + # The following concrete subclasses are included in Active Storage: + # + # * ActiveStorage::Transformers::ImageProcessingTransformer: + # backed by ImageProcessing, a common interface for MiniMagick and ruby-vips + class Transformer + attr_reader transformations: untyped + + def initialize: (untyped transformations) -> void + + # Applies the transformations to the source image in +file+, producing a target image in the + # specified +format+. Yields an open Tempfile containing the target image. Closes and unlinks + # the output tempfile after yielding to the given block. Returns the result of the block. + def transform: (untyped file, format: untyped) { (untyped) -> untyped } -> untyped + + private + + # Returns an open Tempfile containing a transformed image in the given +format+. + # All subclasses implement this method. + def process: (untyped file, format: untyped) -> untyped + end + end +end + +module ActiveStorage + module Transformers + class Vips < ::ActiveStorage::Transformers::ImageProcessingTransformer + def processor: () -> untyped + end + end +end + +module ActiveStorage + # Returns the currently loaded version of Active Storage as a +Gem::Version+. + def self.version: () -> untyped +end + +module ActiveStorage + module Attached::Model + module ClassMethods + end + end +end + +module ActiveStorage::Blobs +end + +module ActiveStorage::Representations +end From 127b951a34c1fb4d414bbb13c71a696529b4e5f1 Mon Sep 17 00:00:00 2001 From: Akira Matsuda Date: Tue, 13 Oct 2026 19:26:17 +0900 Subject: [PATCH 10/22] activestorage: Remove Blob.build_after_upload and Blob#service_url They were deprecated in 6.1 and removed in 7.0. Co-Authored-By: Claude Opus 5.5 (1M context) --- gems/activestorage/8.1/app/models/active_storage/blob.rbs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/gems/activestorage/8.1/app/models/active_storage/blob.rbs b/gems/activestorage/8.1/app/models/active_storage/blob.rbs index 45a691258..fba81c207 100644 --- a/gems/activestorage/8.1/app/models/active_storage/blob.rbs +++ b/gems/activestorage/8.1/app/models/active_storage/blob.rbs @@ -49,8 +49,6 @@ class ActiveStorage::Blob < ActiveStorage::Record # the valid signed id can't find a record. def self.find_signed!: (untyped id, ?record: untyped? record, ?purpose: ::Symbol purpose) -> untyped - def self.build_after_upload: (io: untyped io, filename: untyped filename, ?content_type: untyped? content_type, ?metadata: untyped? metadata, ?service_name: untyped? service_name, ?identify: bool identify, ?record: untyped? record) -> untyped - def self.build_after_unfurling: (io: untyped io, filename: untyped filename, ?key: untyped? key, ?content_type: untyped? content_type, ?metadata: untyped? metadata, ?service_name: untyped? service_name, ?identify: bool identify, ?record: untyped? record) -> untyped def self.create_after_unfurling!: (io: untyped io, filename: untyped filename, ?key: untyped? key, ?content_type: untyped? content_type, ?metadata: untyped? metadata, ?service_name: untyped? service_name, ?identify: bool identify, ?record: untyped? record) -> untyped @@ -114,8 +112,6 @@ class ActiveStorage::Blob < ActiveStorage::Record # URL behind a redirect also allows you to change services without updating all URLs. def url: (?expires_in: untyped expires_in, ?disposition: ::Symbol disposition, ?filename: untyped? filename, **untyped options) -> untyped - alias service_url url - # Returns a URL that can be used to directly upload a file for this blob on the service. This URL is intended to be # short-lived for security and only generated on-demand by the client-side JavaScript responsible for doing the uploading. def service_url_for_direct_upload: (?expires_in: untyped expires_in) -> untyped From 30574f1cbb6be36061c18ac7dd1ec5cb3be28e05 Mon Sep 17 00:00:00 2001 From: Akira Matsuda Date: Tue, 13 Oct 2026 19:27:53 +0900 Subject: [PATCH 11/22] activestorage: Update signatures that changed in 8.1 - has_one_attached and has_many_attached take a block to declare named variants, which yields the attachment reflection - The reflections inherit HasAttachedReflection, which has variant and named_variants - Blob#signed_id takes purpose:, expires_in:, and expires_at:, and returns a String Co-Authored-By: Claude Opus 5.5 (1M context) --- gems/activestorage/8.1/_test/test.rb | 7 +++++++ gems/activestorage/8.1/app/models/active_storage/blob.rbs | 2 +- gems/activestorage/8.1/lib/attached/model.rbs | 4 ++-- gems/activestorage/8.1/lib/reflection.rbs | 4 ++-- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/gems/activestorage/8.1/_test/test.rb b/gems/activestorage/8.1/_test/test.rb index 52fd70531..f7c5deb8e 100644 --- a/gems/activestorage/8.1/_test/test.rb +++ b/gems/activestorage/8.1/_test/test.rb @@ -1,8 +1,15 @@ class User < ActiveRecord::Base has_one_attached :one_image has_many_attached :many_image + has_one_attached :avatar do |attachable| + attachable.variant :thumb, resize_to_limit: [100, 100] + end + has_many_attached :photos do |attachable| + attachable.named_variants + end end user = User.new one = ActiveStorage::Attached::One.new("one_image", user) one.url +ActiveStorage::Blob.new.signed_id(purpose: :download).size diff --git a/gems/activestorage/8.1/app/models/active_storage/blob.rbs b/gems/activestorage/8.1/app/models/active_storage/blob.rbs index fba81c207..382aa547f 100644 --- a/gems/activestorage/8.1/app/models/active_storage/blob.rbs +++ b/gems/activestorage/8.1/app/models/active_storage/blob.rbs @@ -81,7 +81,7 @@ class ActiveStorage::Blob < ActiveStorage::Record def self.signed_id_verifier: () -> untyped # Returns a signed ID for this blob that's suitable for reference on the client-side without fear of tampering. - def signed_id: () -> untyped + def signed_id: (?purpose: Symbol, ?expires_in: untyped, ?expires_at: untyped) -> String # Returns the key pointing to the file on the service that's associated with this blob. The key is the # secure-token format from Rails in lower case. So it'll look like: xtapjjcjiudrlk3tmwyjgpuobabd. diff --git a/gems/activestorage/8.1/lib/attached/model.rbs b/gems/activestorage/8.1/lib/attached/model.rbs index 382d2c797..b7b790405 100644 --- a/gems/activestorage/8.1/lib/attached/model.rbs +++ b/gems/activestorage/8.1/lib/attached/model.rbs @@ -49,7 +49,7 @@ module ActiveStorage ?dependent: ::Symbol dependent, ?service: (::String | ::Symbol | nil) service, ?strict_loading: bool strict_loading - ) -> void + ) ?{ (ActiveStorage::Reflection::HasOneAttachedReflection reflection) -> void } -> void # Specifies the relation between multiple attachments and the model. # @@ -95,7 +95,7 @@ module ActiveStorage ?dependent: ::Symbol dependent, ?service: (::String | ::Symbol | nil) service, ?strict_loading: bool strict_loading - ) -> void + ) ?{ (ActiveStorage::Reflection::HasManyAttachedReflection reflection) -> void } -> void end def attachment_changes: () -> void diff --git a/gems/activestorage/8.1/lib/reflection.rbs b/gems/activestorage/8.1/lib/reflection.rbs index 033008310..e8cdbd42d 100644 --- a/gems/activestorage/8.1/lib/reflection.rbs +++ b/gems/activestorage/8.1/lib/reflection.rbs @@ -2,13 +2,13 @@ module ActiveStorage module Reflection # Holds all the metadata about a has_one_attached attachment as it was # specified in the Active Record class. - class HasOneAttachedReflection < ActiveRecord::Reflection::MacroReflection #:nodoc: + class HasOneAttachedReflection < HasAttachedReflection #:nodoc: def macro: () -> :has_one_attached end # Holds all the metadata about a has_many_attached attachment as it was # specified in the Active Record class. - class HasManyAttachedReflection < ActiveRecord::Reflection::MacroReflection #:nodoc: + class HasManyAttachedReflection < HasAttachedReflection #:nodoc: def macro: () -> :has_many_attached end From e6a5d90f34c658e36a4c03c282411e0db2df5939 Mon Sep 17 00:00:00 2001 From: Akira Matsuda Date: Tue, 13 Oct 2026 19:29:17 +0900 Subject: [PATCH 12/22] activestorage: Add the columns and associations of Blob and Attachment They're defined by the active_storage_blobs and active_storage_attachments tables and belongs_to/has_many, which rbs prototype doesn't see, and the models are in the gem, so apps' generated model signatures don't have them. Co-Authored-By: Claude Opus 5.5 (1M context) --- gems/activestorage/8.1/_test/test.rb | 12 ++++++++++++ .../8.1/app/models/active_storage/attachment.rbs | 9 +++++++++ .../8.1/app/models/active_storage/blob.rbs | 15 +++++++++++++++ 3 files changed, 36 insertions(+) diff --git a/gems/activestorage/8.1/_test/test.rb b/gems/activestorage/8.1/_test/test.rb index f7c5deb8e..912cd17c8 100644 --- a/gems/activestorage/8.1/_test/test.rb +++ b/gems/activestorage/8.1/_test/test.rb @@ -13,3 +13,15 @@ class User < ActiveRecord::Base one = ActiveStorage::Attached::One.new("one_image", user) one.url ActiveStorage::Blob.new.signed_id(purpose: :download).size + +blob = ActiveStorage::Blob.new +blob.content_type&.start_with?("image/") +blob.byte_size + 1 +blob.checksum&.size +blob.metadata["width"] +blob.service_name.to_sym +blob.created_at.to_date +attachment = ActiveStorage::Attachment.new +attachment.name.to_sym +attachment.blob.byte_size +attachment.created_at.to_date diff --git a/gems/activestorage/8.1/app/models/active_storage/attachment.rbs b/gems/activestorage/8.1/app/models/active_storage/attachment.rbs index 53f33eb8b..40ddb3230 100644 --- a/gems/activestorage/8.1/app/models/active_storage/attachment.rbs +++ b/gems/activestorage/8.1/app/models/active_storage/attachment.rbs @@ -9,4 +9,13 @@ class ActiveStorage::Attachment < ActiveStorage::Record # Deletes the attachment and {enqueues a background job}[rdoc-ref:ActiveStorage::Blob#purge_later] to purge the blob. def purge_later: () -> untyped + + # Columns and associations, which rbs prototype doesn't see + def name: () -> String + + def record: () -> untyped + + def blob: () -> ActiveStorage::Blob + + def created_at: () -> ActiveSupport::TimeWithZone end diff --git a/gems/activestorage/8.1/app/models/active_storage/blob.rbs b/gems/activestorage/8.1/app/models/active_storage/blob.rbs index 382aa547f..2b42fbe4c 100644 --- a/gems/activestorage/8.1/app/models/active_storage/blob.rbs +++ b/gems/activestorage/8.1/app/models/active_storage/blob.rbs @@ -179,4 +179,19 @@ class ActiveStorage::Blob < ActiveStorage::Record # Returns an instance of service, which can be configured globally or per attachment def service: () -> untyped + + # Columns and associations, which rbs prototype doesn't see + attr_accessor content_type: String? + + attr_accessor metadata: Hash[String, untyped] + + attr_accessor service_name: String + + attr_accessor byte_size: Integer + + attr_accessor checksum: String? + + def created_at: () -> ActiveSupport::TimeWithZone + + def attachments: () -> untyped end From 42c8c9fb3e77b82e43f9354b3a4706eb6c692346 Mon Sep 17 00:00:00 2001 From: Akira Matsuda Date: Tue, 13 Oct 2026 19:30:06 +0900 Subject: [PATCH 13/22] activestorage: Type return values of Blob and Filename methods key, filename, url, the content type predicates, and the direct upload URL and headers of Blob, and the parts of Filename. Co-Authored-By: Claude Opus 5.5 (1M context) --- gems/activestorage/8.1/_test/test.rb | 9 +++++ .../8.1/activestorage-generated.rbs | 38 ------------------- .../8.1/app/models/active_storage/blob.rbs | 18 ++++----- .../app/models/active_storage/filename.rbs | 15 ++++++++ 4 files changed, 33 insertions(+), 47 deletions(-) create mode 100644 gems/activestorage/8.1/app/models/active_storage/filename.rbs diff --git a/gems/activestorage/8.1/_test/test.rb b/gems/activestorage/8.1/_test/test.rb index 912cd17c8..d8a26c504 100644 --- a/gems/activestorage/8.1/_test/test.rb +++ b/gems/activestorage/8.1/_test/test.rb @@ -25,3 +25,12 @@ class User < ActiveRecord::Base attachment.name.to_sym attachment.blob.byte_size attachment.created_at.to_date + +blob.key.size +blob.filename.extension.upcase +blob.filename.base.upcase +blob.filename.to_s.upcase +blob.url(expires_in: 5.minutes).size +blob.service_url_for_direct_upload.size +blob.service_headers_for_direct_upload.each_value(&:size) +blob.image? || blob.video? diff --git a/gems/activestorage/8.1/activestorage-generated.rbs b/gems/activestorage/8.1/activestorage-generated.rbs index 4fd46531a..4e8c37b20 100644 --- a/gems/activestorage/8.1/activestorage-generated.rbs +++ b/gems/activestorage/8.1/activestorage-generated.rbs @@ -647,44 +647,6 @@ class ActiveStorage::Filename def initialize: (untyped filename) -> void - # Returns the part of the filename preceding any extension. - # - # ActiveStorage::Filename.new("racecar.jpg").base # => "racecar" - # ActiveStorage::Filename.new("racecar").base # => "racecar" - # ActiveStorage::Filename.new(".gitignore").base # => ".gitignore" - def base: () -> untyped - - # Returns the extension of the filename (i.e. the substring following the last dot, excluding a dot at the - # beginning) with the dot that precedes it. If the filename has no extension, an empty string is returned. - # - # ActiveStorage::Filename.new("racecar.jpg").extension_with_delimiter # => ".jpg" - # ActiveStorage::Filename.new("racecar").extension_with_delimiter # => "" - # ActiveStorage::Filename.new(".gitignore").extension_with_delimiter # => "" - def extension_with_delimiter: () -> untyped - - # Returns the extension of the filename (i.e. the substring following the last dot, excluding a dot at - # the beginning). If the filename has no extension, an empty string is returned. - # - # ActiveStorage::Filename.new("racecar.jpg").extension_without_delimiter # => "jpg" - # ActiveStorage::Filename.new("racecar").extension_without_delimiter # => "" - # ActiveStorage::Filename.new(".gitignore").extension_without_delimiter # => "" - def extension_without_delimiter: () -> untyped - - alias extension extension_without_delimiter - - # Returns the sanitized filename. - # - # ActiveStorage::Filename.new("foo:bar.jpg").sanitized # => "foo-bar.jpg" - # ActiveStorage::Filename.new("foo/bar.jpg").sanitized # => "foo-bar.jpg" - # - # Characters considered unsafe for storage (e.g. \, $, and the RTL override character) are replaced with a dash. - def sanitized: () -> untyped - - # Returns the sanitized version of the filename. - def to_s: () -> untyped - - alias to_str to_s - def as_json: (*untyped) -> untyped def <=>: (untyped other) -> untyped diff --git a/gems/activestorage/8.1/app/models/active_storage/blob.rbs b/gems/activestorage/8.1/app/models/active_storage/blob.rbs index 2b42fbe4c..311548a22 100644 --- a/gems/activestorage/8.1/app/models/active_storage/blob.rbs +++ b/gems/activestorage/8.1/app/models/active_storage/blob.rbs @@ -87,37 +87,37 @@ class ActiveStorage::Blob < ActiveStorage::Record # secure-token format from Rails in lower case. So it'll look like: xtapjjcjiudrlk3tmwyjgpuobabd. # This key is not intended to be revealed directly to the user. # Always refer to blobs using the signed_id or a verified form of the key. - def key: () -> untyped + def key: () -> String # Returns an ActiveStorage::Filename instance of the filename that can be # queried for basename, extension, and a sanitized version of the filename # that's safe to use in URLs. - def filename: () -> untyped + def filename: () -> ActiveStorage::Filename # Returns true if the content_type of this blob is in the image range, like image/png. - def image?: () -> untyped + def image?: () -> bool? # Returns true if the content_type of this blob is in the audio range, like audio/mpeg. - def audio?: () -> untyped + def audio?: () -> bool? # Returns true if the content_type of this blob is in the video range, like video/mp4. - def video?: () -> untyped + def video?: () -> bool? # Returns true if the content_type of this blob is in the text range, like text/plain. - def text?: () -> untyped + def text?: () -> bool? # Returns the URL of the blob on the service. This returns a permanent URL for public files, and returns a # short-lived URL for private files. Private files are signed, and not for public use. Instead, # the URL should only be exposed as a redirect from a stable, possibly authenticated URL. Hiding the # URL behind a redirect also allows you to change services without updating all URLs. - def url: (?expires_in: untyped expires_in, ?disposition: ::Symbol disposition, ?filename: untyped? filename, **untyped options) -> untyped + def url: (?expires_in: untyped expires_in, ?disposition: ::Symbol disposition, ?filename: untyped? filename, **untyped options) -> String # Returns a URL that can be used to directly upload a file for this blob on the service. This URL is intended to be # short-lived for security and only generated on-demand by the client-side JavaScript responsible for doing the uploading. - def service_url_for_direct_upload: (?expires_in: untyped expires_in) -> untyped + def service_url_for_direct_upload: (?expires_in: untyped expires_in) -> String # Returns a Hash of headers for +service_url_for_direct_upload+ requests. - def service_headers_for_direct_upload: () -> untyped + def service_headers_for_direct_upload: () -> Hash[String, String] def content_type_for_serving: () -> untyped diff --git a/gems/activestorage/8.1/app/models/active_storage/filename.rbs b/gems/activestorage/8.1/app/models/active_storage/filename.rbs new file mode 100644 index 000000000..31c754d73 --- /dev/null +++ b/gems/activestorage/8.1/app/models/active_storage/filename.rbs @@ -0,0 +1,15 @@ +class ActiveStorage::Filename + def base: () -> String + + def extension_with_delimiter: () -> String + + def extension_without_delimiter: () -> String + + alias extension extension_without_delimiter + + def sanitized: () -> String + + def to_s: () -> String + + alias to_str to_s +end From 977cf10f7ba3794c7952d9fc3a413e3f38b242ae Mon Sep 17 00:00:00 2001 From: Akira Matsuda Date: Tue, 13 Oct 2026 19:31:14 +0900 Subject: [PATCH 14/22] activestorage: Make blocks optional where calling without one is valid rbs prototype marks a block as required unless the method checks it. Variant#download and Preview#download pass the block to the service, and return the content when called without one, like Blob#download. The instrument methods pass it to ActiveSupport::Notifications.instrument. Co-Authored-By: Claude Opus 5.5 (1M context) --- gems/activestorage/8.1/_test/test.rb | 3 +++ gems/activestorage/8.1/activestorage-generated.rbs | 10 +++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/gems/activestorage/8.1/_test/test.rb b/gems/activestorage/8.1/_test/test.rb index d8a26c504..54b205501 100644 --- a/gems/activestorage/8.1/_test/test.rb +++ b/gems/activestorage/8.1/_test/test.rb @@ -34,3 +34,6 @@ class User < ActiveRecord::Base blob.service_url_for_direct_upload.size blob.service_headers_for_direct_upload.each_value(&:size) blob.image? || blob.video? + +ActiveStorage::Variant.new(blob, { resize_to_limit: [100, 100] }).download +ActiveStorage::Preview.new(blob, { resize_to_limit: [100, 100] }).download diff --git a/gems/activestorage/8.1/activestorage-generated.rbs b/gems/activestorage/8.1/activestorage-generated.rbs index 4e8c37b20..e30fc0615 100644 --- a/gems/activestorage/8.1/activestorage-generated.rbs +++ b/gems/activestorage/8.1/activestorage-generated.rbs @@ -733,7 +733,7 @@ class ActiveStorage::Preview # of RAM for very large files. If a block is given, then the download is # streamed and yielded in chunks. Raises ActiveStorage::Preview::UnprocessedError # if the preview has not been processed yet. - def download: () { (?) -> untyped } -> untyped + def download: () ?{ (?) -> untyped } -> untyped private @@ -878,7 +878,7 @@ class ActiveStorage::Variant # Downloads the file associated with this variant. If no block is given, the entire file is read into memory and returned. # That'll use a lot of RAM for very large files. If a block is given, then the download is streamed and yielded in chunks. - def download: () { (?) -> untyped } -> untyped + def download: () ?{ (?) -> untyped } -> untyped def filename: () -> untyped @@ -1183,7 +1183,7 @@ module ActiveStorage def tmpdir: () -> untyped - def instrument: (untyped analyzer) { (?) -> untyped } -> untyped + def instrument: (untyped analyzer) ?{ (?) -> untyped } -> untyped end end @@ -1839,7 +1839,7 @@ module ActiveStorage def open_tempfile: () { (untyped) -> untyped } -> untyped - def instrument: (untyped operation, ?::Hash[untyped, untyped] payload) { (?) -> untyped } -> untyped + def instrument: (untyped operation, ?::Hash[untyped, untyped] payload) ?{ (?) -> untyped } -> untyped def service_name: () -> untyped @@ -2055,7 +2055,7 @@ module ActiveStorage def custom_metadata_headers: (untyped metadata) -> untyped - def instrument: (untyped operation, ?::Hash[untyped, untyped] payload) { (?) -> untyped } -> untyped + def instrument: (untyped operation, ?::Hash[untyped, untyped] payload) ?{ (?) -> untyped } -> untyped def service_name: () -> untyped From b5cd5795d2c65fb21b2b0bc5b48dde2dce11f66d Mon Sep 17 00:00:00 2001 From: Akira Matsuda Date: Tue, 13 Oct 2026 19:38:37 +0900 Subject: [PATCH 15/22] activestorage: Extend the class methods of the concerns the generated classes include The generated classes now extend the ClassMethods of the concerns they include, directly or through other concerns, as ActiveSupport::Concern does. FixtureSet gets generate_unique_secure_token, for example. Co-Authored-By: Claude Opus 5.5 (1M context) --- gems/activestorage/8.1/_test/test.rb | 2 ++ gems/activestorage/8.1/activestorage-generated.rbs | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/gems/activestorage/8.1/_test/test.rb b/gems/activestorage/8.1/_test/test.rb index 54b205501..a064d78aa 100644 --- a/gems/activestorage/8.1/_test/test.rb +++ b/gems/activestorage/8.1/_test/test.rb @@ -37,3 +37,5 @@ class User < ActiveRecord::Base ActiveStorage::Variant.new(blob, { resize_to_limit: [100, 100] }).download ActiveStorage::Preview.new(blob, { resize_to_limit: [100, 100] }).download + +ActiveStorage::FixtureSet.generate_unique_secure_token(length: 28) diff --git a/gems/activestorage/8.1/activestorage-generated.rbs b/gems/activestorage/8.1/activestorage-generated.rbs index e30fc0615..26c268407 100644 --- a/gems/activestorage/8.1/activestorage-generated.rbs +++ b/gems/activestorage/8.1/activestorage-generated.rbs @@ -48,6 +48,8 @@ end # authentication. If your files require access control consider implementing # {Authenticated Controllers}[https://guides.rubyonrails.org/active_storage_overview.html#authenticated-controllers]. class ActiveStorage::Blobs::ProxyController < ::ActiveStorage::BaseController + extend ::ActionController::Live::ClassMethods + include ::ActiveStorage::SetBlob include ::ActiveStorage::Streaming @@ -151,6 +153,8 @@ end # authentication. If your files require access control consider implementing # {Authenticated Controllers}[https://guides.rubyonrails.org/active_storage_overview.html#authenticated-controllers]. class ActiveStorage::Representations::ProxyController < ::ActiveStorage::Representations::BaseController + extend ::ActionController::Live::ClassMethods + include ::ActiveStorage::Streaming include ::ActiveStorage::DisableSession @@ -1717,6 +1721,8 @@ module ActiveStorage # When processed, Active Record will insert database records for each fixture # entry and will ensure the Active Storage relationship is intact. class FixtureSet + extend ::ActiveRecord::SecureToken::ClassMethods + include ::ActiveSupport::Testing::FileFixtures include ::ActiveRecord::SecureToken From d4039a0ed8020133a1fb073055fbfaeabbe70eef Mon Sep 17 00:00:00 2001 From: Akira Matsuda Date: Tue, 13 Oct 2026 19:31:28 +0900 Subject: [PATCH 16/22] actiontext: Copy hand-written RBS and test from 7.2 to 8.1 Co-Authored-By: Claude Opus 5.5 (1M context) --- gems/actiontext/8.1/_test/metadata.yaml | 2 ++ gems/actiontext/8.1/_test/test.rb | 5 +++++ gems/actiontext/8.1/_test/test.rbs | 2 ++ gems/actiontext/8.1/actiontext.rbs | 16 ++++++++++++++++ 4 files changed, 25 insertions(+) create mode 100644 gems/actiontext/8.1/_test/metadata.yaml create mode 100644 gems/actiontext/8.1/_test/test.rb create mode 100644 gems/actiontext/8.1/_test/test.rbs create mode 100644 gems/actiontext/8.1/actiontext.rbs diff --git a/gems/actiontext/8.1/_test/metadata.yaml b/gems/actiontext/8.1/_test/metadata.yaml new file mode 100644 index 000000000..2ec243bd6 --- /dev/null +++ b/gems/actiontext/8.1/_test/metadata.yaml @@ -0,0 +1,2 @@ +additional_gems: + - activerecord diff --git a/gems/actiontext/8.1/_test/test.rb b/gems/actiontext/8.1/_test/test.rb new file mode 100644 index 000000000..e6731ba31 --- /dev/null +++ b/gems/actiontext/8.1/_test/test.rb @@ -0,0 +1,5 @@ +require "actiontext" + +class Article < ActiveRecord::Base + has_rich_text :content +end diff --git a/gems/actiontext/8.1/_test/test.rbs b/gems/actiontext/8.1/_test/test.rbs new file mode 100644 index 000000000..b7ac9f734 --- /dev/null +++ b/gems/actiontext/8.1/_test/test.rbs @@ -0,0 +1,2 @@ +class Article < ActiveRecord::Base +end diff --git a/gems/actiontext/8.1/actiontext.rbs b/gems/actiontext/8.1/actiontext.rbs new file mode 100644 index 000000000..51e1af565 --- /dev/null +++ b/gems/actiontext/8.1/actiontext.rbs @@ -0,0 +1,16 @@ +module ActionText + module Attribute + module ClassMethods + def has_rich_text: (String | Symbol name, ?encrypted: bool, ?strict_loading: bool) -> void + def with_all_rich_text: () -> untyped + def rich_text_association_names: () -> Array[String] + end + end +end + +module ActiveRecord + class Base + include ActionText::Attribute + extend ActionText::Attribute::ClassMethods + end +end From 669bae54893695614706758e34aeb62166c2c097 Mon Sep 17 00:00:00 2001 From: Akira Matsuda Date: Tue, 13 Oct 2026 19:32:14 +0900 Subject: [PATCH 17/22] actiontext: Generate RBS from actiontext 8.1.4 7.2 had only hand-written signatures. 8.1 adds signatures generated with `rbs prototype rb` from the actiontext 8.1.4 lib and app sources and post-processed the same way as the other Rails 8.1 gems: drop members already defined elsewhere, fill missing type arguments, and add accessors defined with class_attribute, mattr_accessor, and cattr_accessor and methods defined with delegate. Methods defined inside `class_methods do ... end` are generated as the concern's ClassMethods. The generators in lib/generators and lib/rails are left out, as their superclasses come from railties, which actiontext doesn't depend on. Co-Authored-By: Claude Opus 5.5 (1M context) --- gems/actiontext/8.1/actiontext-generated.rbs | 1077 ++++++++++++++++++ 1 file changed, 1077 insertions(+) create mode 100644 gems/actiontext/8.1/actiontext-generated.rbs diff --git a/gems/actiontext/8.1/actiontext-generated.rbs b/gems/actiontext/8.1/actiontext-generated.rbs new file mode 100644 index 000000000..52a211c26 --- /dev/null +++ b/gems/actiontext/8.1/actiontext-generated.rbs @@ -0,0 +1,1077 @@ +# The generated code is based on Ruby on Rails source code +# You can find the license of Ruby on Rails from following. + +#MIT License +# +#Copyright (c) 37signals LLC +# +#Permission is hereby granted, free of charge, to any person obtaining a copy +#of this software and associated documentation files (the "Software"), to deal +#in the Software without restriction, including without limitation the rights +#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +#copies of the Software, and to permit persons to whom the Software is +#furnished to do so, subject to the following conditions: +# +#The above copyright notice and this permission notice shall be included in all +#copies or substantial portions of the Software. +# +#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +#SOFTWARE. + +module ActionText + module ContentHelper + def self.sanitizer: () -> untyped + + def self.sanitizer=: (untyped value) -> untyped + + def sanitizer: () -> untyped + + def sanitizer=: (untyped value) -> untyped + + def self.allowed_tags: () -> untyped + + def self.allowed_tags=: (untyped value) -> untyped + + def allowed_tags: () -> untyped + + def allowed_tags=: (untyped value) -> untyped + + def self.allowed_attributes: () -> untyped + + def self.allowed_attributes=: (untyped value) -> untyped + + def allowed_attributes: () -> untyped + + def allowed_attributes=: (untyped value) -> untyped + + def self.scrubber: () -> untyped + + def self.scrubber=: (untyped value) -> untyped + + def scrubber: () -> untyped + + def scrubber=: (untyped value) -> untyped + + def render_action_text_content: (untyped content) -> untyped + + def sanitize_content_attachment: (untyped content_attachment) -> untyped + + def sanitize_action_text_content: (untyped content) -> untyped + + def render_action_text_attachments: (untyped content) -> untyped + + def render_action_text_attachment: (untyped attachment, ?locals: ::Hash[untyped, untyped]) -> untyped + + def sanitizer_allowed_tags: () -> untyped + + def sanitizer_allowed_attributes: () -> untyped + end +end + +module ActionText + module TagHelper + def self.id: () -> untyped + + def self.id=: (untyped value) -> untyped + + # Returns a `trix-editor` tag that instantiates the Trix JavaScript editor as + # well as a hidden field that Trix will write to on changes, so the content will + # be sent on form submissions. + # + # #### Options + # * `:class` - Defaults to "trix-content" so that default styles will be + # applied. Setting this to a different value will prevent default styles + # from being applied. + # * `[:data][:direct_upload_url]` - Defaults to `rails_direct_uploads_url`. + # * `[:data][:blob_url_template]` - Defaults to + # `rails_service_blob_url(":signed_id", ":filename")`. + # + # + # #### Example + # + # rich_textarea_tag "content", message.content + # # + # # + # + # rich_textarea_tag "content", nil do + # "

Default content

" + # end + # # + # # + def rich_textarea_tag: (untyped name, ?untyped? value, ?::Hash[untyped, untyped] options) ?{ (?) -> untyped } -> untyped + + alias rich_text_area_tag rich_textarea_tag + end +end + +module ActionView::Helpers + class Tags::ActionText < ::ActionView::Helpers::Tags::Base + def dom_id: (*untyped, **untyped) ?{ (*untyped) -> untyped } -> untyped + + include ::ActionView::Helpers::Tags::Placeholderable + + def render: () { (?) -> untyped } -> untyped + end + + module FormHelper + # Returns a `trix-editor` tag that instantiates the Trix JavaScript editor as + # well as a hidden field that Trix will write to on changes, so the content will + # be sent on form submissions. + # + # #### Options + # * `:class` - Defaults to "trix-content" which ensures default styling is + # applied. + # * `:value` - Adds a default value to the HTML input tag. + # * `[:data][:direct_upload_url]` - Defaults to `rails_direct_uploads_url`. + # * `[:data][:blob_url_template]` - Defaults to + # `rails_service_blob_url(":signed_id", ":filename")`. + # + # + # #### Example + # rich_textarea :message, :content + # # + # # + # + # rich_textarea :message, :content, value: "

Default message

" + # # + # # + # + # rich_textarea :message, :content do + # "

Default message

" + # end + # # + # # + def rich_textarea: (untyped object_name, untyped method, ?::Hash[untyped, untyped] options) { (?) -> untyped } -> untyped + + alias rich_text_area rich_textarea + end + + class FormBuilder + # Wraps ActionView::Helpers::FormHelper#rich_textarea for form builders: + # + # <%= form_with model: @message do |f| %> + # <%= f.rich_textarea :content %> + # <% end %> + # + # Please refer to the documentation of the base helper for details. + def rich_textarea: (untyped method, ?::Hash[untyped, untyped] options) { (?) -> untyped } -> untyped + + alias rich_text_area rich_textarea + end +end + +module ActionText + class EncryptedRichText < ::ActionText::RichText + end +end + +module ActionText + class Record < ::ActiveRecord::Base + end +end + +module ActionText + # # Action Text RichText + # + # The RichText record holds the content produced by the Trix editor in a + # serialized `body` attribute. It also holds all the references to the embedded + # files, which are stored using Active Storage. This record is then associated + # with the Active Record model the application desires to have rich text content + # using the `has_rich_text` class method. + # + # class Message < ActiveRecord::Base + # has_rich_text :content + # end + # + # message = Message.create!(content: "

Funny times!

") + # message.content #=> # "

Funny times!

" + # message.content.to_plain_text # => "Funny times!" + # + # message = Message.create!(content: "
safe
") + # message.content #=> # "
safeunsafe
" + # message.content.to_plain_text # => "safeunsafe" + class RichText < ::ActionText::Record + def to_s: (*untyped, **untyped) ?{ (*untyped) -> untyped } -> untyped + def nil?: (*untyped, **untyped) ?{ (*untyped) -> untyped } -> untyped + def blank?: (*untyped, **untyped) ?{ (*untyped) -> untyped } -> untyped + def empty?: (*untyped, **untyped) ?{ (*untyped) -> untyped } -> untyped + def present?: (*untyped, **untyped) ?{ (*untyped) -> untyped } -> untyped + + # Returns a plain-text version of the markup contained by the `body` attribute, + # with tags removed but HTML entities encoded. + # + # message = Message.create!(content: "

Funny times!

") + # message.content.to_plain_text # => "Funny times!" + # + # NOTE: that the returned string is not HTML safe and should not be rendered in + # browsers. + # + # message = Message.create!(content: "<script>alert()</script>") + # message.content.to_plain_text # => "" + def to_plain_text: () -> untyped + + # Returns the `body` attribute in a format that makes it editable in the Trix + # editor. Previews of attachments are rendered inline. + # + # content = "

Funny Times!

" + # message = Message.create!(content: content) + # message.content.to_trix_html # => + # #
+ # #

Funny times!

+ # #
+ # # + # #
+ # #
+ def to_trix_html: () -> untyped + end +end + +# :markup: markdown +# :include: ../README.md +module ActionText + extend ::ActiveSupport::Autoload + + module Attachables + extend ::ActiveSupport::Autoload + end + + module Attachments + extend ::ActiveSupport::Autoload + end + + def self.html_document_class: () -> untyped + + def self.html_document_fragment_class: () -> untyped +end + +module ActionText + # # Action Text Attachable + # + # Include this module to make a record attachable to an ActionText::Content. + # + # class Person < ApplicationRecord + # include ActionText::Attachable + # end + # + # person = Person.create! name: "Javan" + # html = %Q() + # content = ActionText::Content.new(html) + # content.attachables # => [person] + module Attachable + extend ::ActiveSupport::Concern + + LOCATOR_NAME: "attachable" + + # Extracts the `ActionText::Attachable` from the attachment HTML node: + # + # person = Person.create! name: "Javan" + # html = %Q() + # fragment = ActionText::Fragment.wrap(html) + # attachment_node = fragment.find_all(ActionText::Attachment.tag_name).first + # ActionText::Attachable.from_node(attachment_node) # => person + def self.from_node: (untyped node) -> untyped + + def self.from_attachable_sgid: (untyped sgid, ?::Hash[untyped, untyped] options) -> untyped + + def self.attachable_from_sgid: (untyped sgid) -> untyped + + # Returns the Signed Global ID for the attachable. The purpose of the ID is set + # to 'attachable' so it can't be reused for other purposes. + def attachable_sgid: () -> untyped + + def attachable_content_type: () -> untyped + + def attachable_filename: () -> (untyped | nil) + + def attachable_filesize: () -> untyped + + def attachable_metadata: () -> untyped + + def previewable_attachable?: () -> false + + # Returns the path to the partial that is used for rendering the attachable in + # Trix. Defaults to `to_partial_path`. + # + # Override to render a different partial: + # + # class User < ApplicationRecord + # def to_trix_content_attachment_partial_path + # "users/trix_content_attachment" + # end + # end + def to_trix_content_attachment_partial_path: () -> untyped + + # Returns the path to the partial that is used for rendering the attachable. + # Defaults to `to_partial_path`. + # + # Override to render a different partial: + # + # class User < ApplicationRecord + # def to_attachable_partial_path + # "users/attachable" + # end + # end + def to_attachable_partial_path: () -> untyped + + def to_rich_text_attributes: (?::Hash[untyped, untyped] attributes) -> untyped + + private + + def attribute_names_for_serialization: () -> untyped + + def read_attribute_for_serialization: (untyped key) -> (untyped | nil) + end +end + +module ActionText + module Attachables + class ContentAttachment + include ::ActiveModel::Model + + def self.from_node: (untyped node) -> untyped + + attr_accessor content_type: untyped + + attr_accessor content: untyped + + def attachable_plain_text_representation: (untyped caption) -> untyped + + def to_html: () -> untyped + + def to_s: () -> untyped + + def to_partial_path: () -> "action_text/attachables/content_attachment" + + private + + def content_instance: () -> untyped + end + end +end + +module ActionText + module Attachables + class MissingAttachable + extend ::ActiveModel::Naming + + DEFAULT_PARTIAL_PATH: "action_text/attachables/missing_attachable" + + def initialize: (untyped sgid) -> void + + def to_partial_path: () -> untyped + + def model: () -> untyped + end + end +end + +module ActionText + module Attachables + class RemoteImage + extend ::ActiveModel::Naming + + def self.from_node: (untyped node) -> (untyped | nil) + + def self.content_type_is_image?: (untyped content_type) -> untyped + + def self.attributes_from_node: (untyped node) -> { url: untyped, content_type: untyped, width: untyped, height: untyped } + + attr_reader url: untyped + + attr_reader content_type: untyped + + attr_reader width: untyped + + attr_reader height: untyped + + def initialize: (?::Hash[untyped, untyped] attributes) -> void + + def attachable_plain_text_representation: (untyped caption) -> ::String + + def to_partial_path: () -> "action_text/attachables/remote_image" + end + end +end + +module ActionText + class AttachmentGallery + include ::ActiveModel::Model + + TAG_NAME: "div" + + def self.fragment_by_canonicalizing_attachment_galleries: (untyped content) -> untyped + + def self.fragment_by_replacing_attachment_gallery_nodes: (untyped content) { (untyped) -> untyped } -> untyped + + def self.find_attachment_gallery_nodes: (untyped content) -> untyped + + def self.from_node: (untyped node) -> untyped + + def self.attachment_selector: () -> ::String + + def self.selector: () -> ::String + + attr_reader node: untyped + + def initialize: (untyped node) -> void + + def attachments: () -> untyped + + def size: () -> untyped + + def inspect: () -> ::String + end +end + +module ActionText + # # Action Text Attachment + # + # Attachments serialize attachables to HTML or plain text. + # + # class Person < ApplicationRecord + # include ActionText::Attachable + # end + # + # attachable = Person.create! name: "Javan" + # attachment = ActionText::Attachment.from_attachable(attachable) + # attachment.to_html # => " untyped + def self.tag_name=: (untyped value) -> untyped + def tag_name: () -> untyped + def tag_name=: (untyped value) -> untyped + def to_param: (*untyped, **untyped) ?{ (*untyped) -> untyped } -> untyped + + include ::ActionText::Attachments::TrixConversion + + include ::ActionText::Attachments::Minification + + include ::ActionText::Attachments::Caching + + ATTRIBUTES: ::Array["sgid" | "content-type" | "url" | "href" | "filename" | "filesize" | "width" | "height" | "previewable" | "presentation" | "caption" | "content"] + + def self.fragment_by_canonicalizing_attachments: (untyped content) -> untyped + + def self.from_node: (untyped node, ?untyped? attachable) -> untyped + + def self.from_attachables: (untyped attachables) -> untyped + + def self.from_attachable: (untyped attachable, ?::Hash[untyped, untyped] attributes) -> (untyped | nil) + + def self.from_attributes: (untyped attributes, ?untyped? attachable) -> (untyped | nil) + + def self.node_from_attributes: (untyped attributes) -> (untyped | nil) + + def self.process_attributes: (untyped attributes) -> untyped + + attr_reader node: untyped + + attr_reader attachable: untyped + + def initialize: (untyped node, untyped attachable) -> void + + def caption: () -> untyped + + def full_attributes: () -> untyped + + def with_full_attributes: () -> untyped + + # Converts the attachment to plain text. + # + # attachable = ActiveStorage::Blob.find_by filename: "racecar.jpg" + # attachment = ActionText::Attachment.from_attachable(attachable) + # attachment.to_plain_text # => "[racecar.jpg]" + # + # Use the `caption` when set: + # + # attachment = ActionText::Attachment.from_attachable(attachable, caption: "Vroom vroom") + # attachment.to_plain_text # => "[Vroom vroom]" + # + # The presentation can be overridden by implementing the + # `attachable_plain_text_representation` method: + # + # class Person < ApplicationRecord + # include ActionText::Attachable + # + # def attachable_plain_text_representation + # "[#{name}]" + # end + # end + # + # attachable = Person.create! name: "Javan" + # attachment = ActionText::Attachment.from_attachable(attachable) + # attachment.to_plain_text # => "[Javan]" + def to_plain_text: () -> untyped + + # Converts the attachment to HTML. + # + # attachable = Person.create! name: "Javan" + # attachment = ActionText::Attachment.from_attachable(attachable) + # attachment.to_html # => " untyped + + def to_s: () -> untyped + + def inspect: () -> ::String + + private + + def node_attributes: () -> untyped + + def attachable_attributes: () -> untyped + + def sgid_attributes: () -> untyped + end +end + +module ActionText + module Attachments + module Caching + def cache_key: (*untyped args) -> untyped + + private + + def cache_digest: () -> untyped + end + end +end + +module ActionText + module Attachments + module Minification + extend ::ActiveSupport::Concern + end + end +end + +module ActionText + module Attachments + module TrixConversion + extend ::ActiveSupport::Concern + + def to_trix_attachment: (?untyped content) -> untyped + + private + + def trix_attachment_content: () -> (untyped | nil) + end + end +end + +module ActionText + module Attribute + extend ::ActiveSupport::Concern + end +end + +module ActionText + # # Action Text Content + # + # The `ActionText::Content` class wraps an HTML fragment to add support for + # parsing, rendering and serialization. It can be used to extract links and + # attachments, convert the fragment to plain text, or serialize the fragment to + # the database. + # + # The ActionText::RichText record serializes the `body` attribute as + # `ActionText::Content`. + # + # class Message < ActiveRecord::Base + # has_rich_text :content + # end + # + # message = Message.create!(content: "

Funny times!

") + # body = message.content.body # => # + # body.to_s # => "

Funny times!

" + # body.to_plain_text # => "Funny times!" + class Content + def deconstruct: (*untyped, **untyped) ?{ (*untyped) -> untyped } -> untyped + def blank?: (*untyped, **untyped) ?{ (*untyped) -> untyped } -> untyped + def empty?: (*untyped, **untyped) ?{ (*untyped) -> untyped } -> untyped + def html_safe: (*untyped, **untyped) ?{ (*untyped) -> untyped } -> untyped + def present?: (*untyped, **untyped) ?{ (*untyped) -> untyped } -> untyped + + include ::ActionText::Rendering + + include ::ActionText::Serialization + + include ::ActionText::ContentHelper + + attr_reader fragment: untyped + + def self.fragment_by_canonicalizing_content: (untyped content) -> untyped + + def initialize: (?untyped? content, ?::Hash[untyped, untyped] options) -> void + + # Extracts links from the HTML fragment: + # + # html = 'Example' + # content = ActionText::Content.new(html) + # content.links # => ["http://example.com/"] + def links: () -> untyped + + # Extracts ActionText::Attachment objects from the HTML fragment: + # + # attachable = ActiveStorage::Blob.first + # html = %Q() + # content = ActionText::Content.new(html) + # content.attachments # => [# untyped + + def attachment_galleries: () -> untyped + + def gallery_attachments: () -> untyped + + # Extracts ActionText::Attachable objects from the HTML fragment: + # + # attachable = ActiveStorage::Blob.first + # html = %Q() + # content = ActionText::Content.new(html) + # content.attachables # => [attachable] + def attachables: () -> untyped + + def append_attachables: (untyped attachables) -> untyped + + def render_attachments: (**untyped options) { (?) -> untyped } -> untyped + + def render_attachment_galleries: () { (?) -> untyped } -> untyped + + # Returns a plain-text version of the markup contained by the content, with tags + # removed but HTML entities encoded. + # + # content = ActionText::Content.new("

Funny times!

") + # content.to_plain_text # => "Funny times!" + # + # content = ActionText::Content.new("
safe
") + # content.to_plain_text # => "safe" + # + # NOTE: that the returned string is not HTML safe and should not be rendered in + # browsers without additional sanitization. + # + # content = ActionText::Content.new("<script>alert()</script>") + # content.to_plain_text # => "" + # ActionText::ContentHelper.sanitizer.sanitize(content.to_plain_text) # => "" + def to_plain_text: () -> untyped + + def to_trix_html: () -> untyped + + def to_html: () -> untyped + + def to_rendered_html_with_layout: () -> untyped + + def to_partial_path: () -> "action_text/contents/content" + + # Safely transforms Content into an HTML String. + # + # content = ActionText::Content.new("

Funny times!

") + # content.to_s # => "

Funny times!

" + # + # content = ActionText::Content.new("
safe
") + # content.to_s # => "
safeunsafe
" + def to_s: () -> untyped + + def as_json: (*untyped) -> untyped + + def inspect: () -> ::String + + def ==: (untyped other) -> (untyped | nil) + + private + + def attachment_nodes: () -> untyped + + def attachment_gallery_nodes: () -> untyped + + def attachment_for_node: (untyped node, ?with_full_attributes: bool) -> untyped + + def attachment_gallery_for_node: (untyped node) -> untyped + end +end + +module ActionText + def self.deprecator: () -> untyped +end + +module ActionText + module Encryption + def encrypt: () -> untyped + + def decrypt: () -> untyped + + private + + def encrypt_rich_texts: () -> untyped + + def decrypt_rich_texts: () -> untyped + + def has_encrypted_rich_texts?: () -> untyped + + def encryptable_rich_texts: () -> untyped + end +end + +module ActionText + class Engine < ::Rails::Engine + end +end + +module ActionText + # # Action Text FixtureSet + # + # Fixtures are a way of organizing data that you want to test against; in short, + # sample data. + # + # To learn more about fixtures, read the ActiveRecord::FixtureSet documentation. + # + # ### YAML + # + # Like other Active Record-backed models, ActionText::RichText records inherit + # from ActiveRecord::Base instances and can therefore be populated by fixtures. + # + # Consider an `Article` class: + # + # class Article < ApplicationRecord + # has_rich_text :content + # end + # + # To declare fixture data for the related `content`, first declare fixture data + # for `Article` instances in `test/fixtures/articles.yml`: + # + # first: + # title: An Article + # + # Then declare the ActionText::RichText fixture data in + # `test/fixtures/action_text/rich_texts.yml`, making sure to declare each + # entry's `record:` key as a polymorphic relationship: + # + # first: + # record: first (Article) + # name: content + # body:
Hello, world.
+ # + # When processed, Active Record will insert database records for each fixture + # entry and will ensure the Action Text relationship is intact. + class FixtureSet + # Fixtures support Action Text attachments as part of their `body` HTML. + # + # ### Examples + # + # For example, consider a second `Article` fixture declared in + # `test/fixtures/articles.yml`: + # + # second: + # title: Another Article + # + # You can attach a mention of `articles(:first)` to `second`'s `content` by + # embedding a call to `ActionText::FixtureSet.attachment` in the `body:` value + # in `test/fixtures/action_text/rich_texts.yml`: + # + # second: + # record: second (Article) + # name: content + # body:
Hello, <%= ActionText::FixtureSet.attachment("articles", :first) %>
+ # + def self.attachment: (untyped fixture_set_name, untyped label, ?column_type: ::Symbol) -> ::String + end +end + +module ActionText + class Fragment + def deconstruct: (*untyped, **untyped) ?{ (*untyped) -> untyped } -> untyped + + def self.wrap: (untyped fragment_or_html) -> untyped + + def self.from_html: (untyped html) -> untyped + + attr_reader source: untyped + + def initialize: (untyped source) -> void + + def find_all: (untyped selector) -> untyped + + def update: () { (untyped) -> untyped } -> untyped + + def replace: (untyped selector) { (untyped) -> untyped } -> untyped + + def to_plain_text: () -> untyped + + def to_html: () -> untyped + + def to_s: () -> untyped + end +end + +module ActionText + # Returns the currently loaded version of Action Text as a `Gem::Version`. + def self.gem_version: () -> untyped + + module VERSION + MAJOR: 8 + + MINOR: 1 + + TINY: 4 + + PRE: nil + + STRING: untyped + end +end + +module ActionText + module HtmlConversion + extend ::ActionText::HtmlConversion + + def node_to_html: (untyped node) -> untyped + + def fragment_for_html: (untyped html) -> untyped + + def create_element: (untyped tag_name, ?::Hash[untyped, untyped] attributes) -> untyped + + private + + def document: () -> untyped + end +end + +module ActionText + module PlainTextConversion + extend ::ActionText::PlainTextConversion + + def node_to_plain_text: (untyped node) -> untyped + + private + + def plain_text_for_node: (untyped node, untyped child_values) -> untyped + + def plain_text_method_for_node: (untyped node) -> ::Symbol + + def plain_text_for_child_values: (untyped child_values) -> untyped + + def plain_text_for_unsupported_node: (untyped node, untyped _child_values) -> "" + + def plain_text_for_block: (untyped node, untyped child_values) -> ::String + + def plain_text_for_list: (untyped node, untyped child_values) -> ::String + + def plain_text_for_br_node: (untyped node, untyped _child_values) -> "\n" + + def plain_text_for_text_node: (untyped node, untyped _child_values) -> untyped + + def plain_text_for_div_node: (untyped node, untyped child_values) -> ::String + + def plain_text_for_figcaption_node: (untyped node, untyped child_values) -> ::String + + def plain_text_for_blockquote_node: (untyped node, untyped child_values) -> (::String | untyped) + + def plain_text_for_li_node: (untyped node, untyped child_values) -> ::String + + def remove_trailing_newlines: (untyped text) -> untyped + + def bullet_for_li_node: (untyped node) -> ::String + + def list_node_name_for_li_node: (untyped node) -> untyped + + def indentation_for_li_node: (untyped node) -> untyped + + def list_node_depth_for_node: (untyped node) -> untyped + + def break_if_nested_list: (untyped node, untyped text) -> (::String | untyped) + + class BottomUpReducer + def initialize: (untyped node) -> void + + def reduce: () { (?) -> untyped } -> untyped + + private + + def traverse_bottom_up: (untyped node) { (?) -> untyped } -> untyped + end + end +end + +module ActionText + module Rendering + def render: (*untyped, **untyped) ?{ (*untyped) -> untyped } -> untyped + + extend ::ActiveSupport::Concern + end +end + +module ActionText + module Serialization + extend ::ActiveSupport::Concern + + def _dump: (*untyped) -> untyped + end +end + +module ActionText + module SystemTestHelper + # Locates a Trix editor and fills it in with the given HTML. + # + # The editor can be found by: + # + # * its `id` + # * its `placeholder` + # * the text from its `label` element + # * its `aria-label` + # * the `name` of its input + # + # Additional options are forwarded to Capybara as filters + # + # Examples: + # + # # + # fill_in_rich_textarea "message_content", with: "Hello world!" + # + # # + # fill_in_rich_textarea "Your message here", with: "Hello world!" + # + # # + # # + # fill_in_rich_textarea "Message content", with: "Hello world!" + # + # # + # fill_in_rich_textarea "Message content", with: "Hello world!" + # + # # + # # + # fill_in_rich_textarea "message[content]", with: "Hello world!" + def fill_in_rich_textarea: (?untyped? locator, with: untyped, **untyped) -> untyped + + alias fill_in_rich_text_area fill_in_rich_textarea + end +end + +module ActionText + class TrixAttachment + TAG_NAME: "figure" + + SELECTOR: "[data-trix-attachment]" + + COMPOSED_ATTRIBUTES: ::Array["caption" | "presentation"] + + ATTRIBUTES: untyped + + ATTRIBUTE_TYPES: { "previewable" => untyped, "filesize" => untyped, "width" => untyped, "height" => untyped, default: untyped } + + def self.from_attributes: (untyped attributes) -> untyped + + def self.process_attributes: (untyped attributes) -> untyped + + def self.transform_attribute_keys: (untyped attributes) -> untyped + + def self.typecast_attribute_values: (untyped attributes) -> untyped + + attr_reader node: untyped + + def initialize: (untyped node) -> void + + def attributes: () -> untyped + + def to_html: () -> untyped + + def to_s: () -> untyped + + private + + def attachment_attributes: () -> untyped + + def composed_attributes: () -> untyped + + def read_json_object_attribute: (untyped name) -> untyped + + def read_json_attribute: (untyped name) -> (untyped | nil) + end +end + +module ActionText + # Returns the currently loaded version of Action Text as a `Gem::Version`. + def self.version: () -> untyped +end + +module ActionText + module Attachable + module ClassMethods + def from_attachable_sgid: (untyped sgid) -> untyped + + # Returns the path to the partial that is used for rendering missing + # attachables. Defaults to "action_text/attachables/missing_attachable". + # + # Override to render a different partial: + # + # class User < ApplicationRecord + # def self.to_missing_attachable_partial_path + # "users/missing_attachable" + # end + # end + def to_missing_attachable_partial_path: () -> untyped + end + end +end + +module ActionText + module Attachments + module Minification + module ClassMethods + def fragment_by_minifying_attachments: (untyped content) -> untyped + end + end + end +end + +module ActionText + module Attachments + module TrixConversion + module ClassMethods + def fragment_by_converting_trix_attachments: (untyped content) -> untyped + + def from_trix_attachment: (untyped trix_attachment) -> untyped + end + end + end +end + +module ActionText + module Attribute + module ClassMethods + end + end +end + +module ActionText + module Rendering + module ClassMethods + def action_controller_renderer: () -> untyped + + def with_renderer: (untyped renderer) { () -> untyped } -> untyped + + def render: (*untyped args) { (?) -> untyped } -> untyped + end + end +end + +module ActionText + module Serialization + module ClassMethods + def load: (untyped content) -> (untyped | nil) + + def dump: (untyped content) -> untyped + end + end +end + +module ActionText + module Serialization + module ClassMethods + alias _load load + end + end +end From 4f313ea1c82abd3a396cb33407a4fc30ed1d0866 Mon Sep 17 00:00:00 2001 From: Akira Matsuda Date: Tue, 13 Oct 2026 19:32:48 +0900 Subject: [PATCH 18/22] actiontext: Add store_if_blank: to has_rich_text It was added in 8.0. Co-Authored-By: Claude Opus 5.5 (1M context) --- gems/actiontext/8.1/_test/test.rb | 4 ++++ gems/actiontext/8.1/_test/test.rbs | 3 +++ gems/actiontext/8.1/actiontext.rbs | 2 +- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/gems/actiontext/8.1/_test/test.rb b/gems/actiontext/8.1/_test/test.rb index e6731ba31..e0daf298b 100644 --- a/gems/actiontext/8.1/_test/test.rb +++ b/gems/actiontext/8.1/_test/test.rb @@ -3,3 +3,7 @@ class Article < ActiveRecord::Base has_rich_text :content end + +class Comment < ActiveRecord::Base + has_rich_text :body, store_if_blank: false +end diff --git a/gems/actiontext/8.1/_test/test.rbs b/gems/actiontext/8.1/_test/test.rbs index b7ac9f734..ca4d100de 100644 --- a/gems/actiontext/8.1/_test/test.rbs +++ b/gems/actiontext/8.1/_test/test.rbs @@ -1,2 +1,5 @@ class Article < ActiveRecord::Base end + +class Comment < ActiveRecord::Base +end diff --git a/gems/actiontext/8.1/actiontext.rbs b/gems/actiontext/8.1/actiontext.rbs index 51e1af565..883307939 100644 --- a/gems/actiontext/8.1/actiontext.rbs +++ b/gems/actiontext/8.1/actiontext.rbs @@ -1,7 +1,7 @@ module ActionText module Attribute module ClassMethods - def has_rich_text: (String | Symbol name, ?encrypted: bool, ?strict_loading: bool) -> void + def has_rich_text: (String | Symbol name, ?encrypted: bool, ?strict_loading: bool, ?store_if_blank: bool) -> void def with_all_rich_text: () -> untyped def rich_text_association_names: () -> Array[String] end From 74a8e108b2aecc42198752cc175b0740a19d50c0 Mon Sep 17 00:00:00 2001 From: Akira Matsuda Date: Tue, 13 Oct 2026 19:40:30 +0900 Subject: [PATCH 19/22] actiontext: Make blocks optional where calling without one is valid rbs prototype marks a block as required unless the method checks it. rich_textarea passes the block to rich_textarea_tag, which uses it for the value only when given, and ActionText::Content.render passes it to render_to_string. Co-Authored-By: Claude Opus 5.5 (1M context) --- gems/actiontext/8.1/_test/test.rb | 2 ++ gems/actiontext/8.1/actiontext-generated.rbs | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/gems/actiontext/8.1/_test/test.rb b/gems/actiontext/8.1/_test/test.rb index e0daf298b..27266e7f0 100644 --- a/gems/actiontext/8.1/_test/test.rb +++ b/gems/actiontext/8.1/_test/test.rb @@ -7,3 +7,5 @@ class Article < ActiveRecord::Base class Comment < ActiveRecord::Base has_rich_text :body, store_if_blank: false end + +ActionView::Helpers::FormBuilder.new(:article, Article.new, ActionView::Base.empty, {}).rich_textarea(:content, class: "editor") diff --git a/gems/actiontext/8.1/actiontext-generated.rbs b/gems/actiontext/8.1/actiontext-generated.rbs index 52a211c26..ae7dfd5f8 100644 --- a/gems/actiontext/8.1/actiontext-generated.rbs +++ b/gems/actiontext/8.1/actiontext-generated.rbs @@ -115,7 +115,7 @@ module ActionView::Helpers include ::ActionView::Helpers::Tags::Placeholderable - def render: () { (?) -> untyped } -> untyped + def render: () ?{ (?) -> untyped } -> untyped end module FormHelper @@ -146,7 +146,7 @@ module ActionView::Helpers # end # # # # - def rich_textarea: (untyped object_name, untyped method, ?::Hash[untyped, untyped] options) { (?) -> untyped } -> untyped + def rich_textarea: (untyped object_name, untyped method, ?::Hash[untyped, untyped] options) ?{ (?) -> untyped } -> untyped alias rich_text_area rich_textarea end @@ -159,7 +159,7 @@ module ActionView::Helpers # <% end %> # # Please refer to the documentation of the base helper for details. - def rich_textarea: (untyped method, ?::Hash[untyped, untyped] options) { (?) -> untyped } -> untyped + def rich_textarea: (untyped method, ?::Hash[untyped, untyped] options) ?{ (?) -> untyped } -> untyped alias rich_text_area rich_textarea end @@ -1053,7 +1053,7 @@ module ActionText def with_renderer: (untyped renderer) { () -> untyped } -> untyped - def render: (*untyped args) { (?) -> untyped } -> untyped + def render: (*untyped args) ?{ (?) -> untyped } -> untyped end end end From 7e60ef6e6e6c1a926623f270db0a35fd9a7dbe02 Mon Sep 17 00:00:00 2001 From: Akira Matsuda Date: Tue, 13 Oct 2026 19:42:07 +0900 Subject: [PATCH 20/22] actiontext: Extend the class methods of the concerns the generated classes include The generated classes now extend the ClassMethods of the concerns they include, directly or through other concerns, as ActiveSupport::Concern does. ActionText::Content gets render, load, and dump, and ActionText::Attachment gets the minification and Trix conversion methods, for example. Co-Authored-By: Claude Opus 5.5 (1M context) --- gems/actiontext/8.1/_test/test.rb | 2 ++ gems/actiontext/8.1/actiontext-generated.rbs | 16 ++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/gems/actiontext/8.1/_test/test.rb b/gems/actiontext/8.1/_test/test.rb index 27266e7f0..c3a2f09f5 100644 --- a/gems/actiontext/8.1/_test/test.rb +++ b/gems/actiontext/8.1/_test/test.rb @@ -9,3 +9,5 @@ class Comment < ActiveRecord::Base end ActionView::Helpers::FormBuilder.new(:article, Article.new, ActionView::Base.empty, {}).rich_textarea(:content, class: "editor") +ActionText::Content.render(partial: "action_text/contents/content") +ActionText::Attachment.fragment_by_minifying_attachments("

text

") diff --git a/gems/actiontext/8.1/actiontext-generated.rbs b/gems/actiontext/8.1/actiontext-generated.rbs index ae7dfd5f8..a58cc6cc5 100644 --- a/gems/actiontext/8.1/actiontext-generated.rbs +++ b/gems/actiontext/8.1/actiontext-generated.rbs @@ -333,6 +333,10 @@ end module ActionText module Attachables class ContentAttachment + extend ::ActiveModel::Conversion::ClassMethods + + extend ::ActiveModel::Validations::ClassMethods + include ::ActiveModel::Model def self.from_node: (untyped node) -> untyped @@ -402,6 +406,10 @@ end module ActionText class AttachmentGallery + extend ::ActiveModel::Conversion::ClassMethods + + extend ::ActiveModel::Validations::ClassMethods + include ::ActiveModel::Model TAG_NAME: "div" @@ -443,6 +451,10 @@ module ActionText # attachment = ActionText::Attachment.from_attachable(attachable) # attachment.to_html # => " untyped def self.tag_name=: (untyped value) -> untyped def tag_name: () -> untyped @@ -591,6 +603,10 @@ module ActionText # body.to_s # => "

Funny times!

" # body.to_plain_text # => "Funny times!" class Content + extend ::ActionText::Serialization::ClassMethods + + extend ::ActionText::Rendering::ClassMethods + def deconstruct: (*untyped, **untyped) ?{ (*untyped) -> untyped } -> untyped def blank?: (*untyped, **untyped) ?{ (*untyped) -> untyped } -> untyped def empty?: (*untyped, **untyped) ?{ (*untyped) -> untyped } -> untyped From e69598f1118e17c66020bbb2b13898aafb3f29b6 Mon Sep 17 00:00:00 2001 From: Akira Matsuda Date: Tue, 13 Oct 2026 19:43:07 +0900 Subject: [PATCH 21/22] actiontext: Add the columns and associations of ActionText::RichText They're defined by the action_text_rich_texts table, belongs_to, and has_many_attached, which rbs prototype doesn't see, and the model is in the gem, so apps' generated model signatures don't have them. Co-Authored-By: Claude Opus 5.5 (1M context) --- gems/actiontext/8.1/_test/test.rb | 6 ++++++ gems/actiontext/8.1/actiontext.rbs | 17 +++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/gems/actiontext/8.1/_test/test.rb b/gems/actiontext/8.1/_test/test.rb index c3a2f09f5..16b9c6b92 100644 --- a/gems/actiontext/8.1/_test/test.rb +++ b/gems/actiontext/8.1/_test/test.rb @@ -11,3 +11,9 @@ class Comment < ActiveRecord::Base ActionView::Helpers::FormBuilder.new(:article, Article.new, ActionView::Base.empty, {}).rich_textarea(:content, class: "editor") ActionText::Content.render(partial: "action_text/contents/content") ActionText::Attachment.fragment_by_minifying_attachments("

text

") + +rich_text = ActionText::RichText.new +rich_text.name.to_sym +rich_text.body&.to_html +rich_text.embeds.attached? +rich_text.updated_at.to_date diff --git a/gems/actiontext/8.1/actiontext.rbs b/gems/actiontext/8.1/actiontext.rbs index 883307939..dd5603900 100644 --- a/gems/actiontext/8.1/actiontext.rbs +++ b/gems/actiontext/8.1/actiontext.rbs @@ -14,3 +14,20 @@ module ActiveRecord extend ActionText::Attribute::ClassMethods end end + +module ActionText + class RichText + # Columns and associations, which rbs prototype doesn't see + attr_accessor name: String + + attr_accessor body: ActionText::Content? + + def record: () -> untyped + + def embeds: () -> ActiveStorage::Attached::Many + + def created_at: () -> ActiveSupport::TimeWithZone + + def updated_at: () -> ActiveSupport::TimeWithZone + end +end From eec20c3e596fda5bb3b7e28952d6ff4559f05d64 Mon Sep 17 00:00:00 2001 From: Akira Matsuda Date: Tue, 13 Oct 2026 19:43:37 +0900 Subject: [PATCH 22/22] actiontext: Type return values of RichText and Content methods to_s, to_plain_text, and to_trix_html of RichText, and the links, attachments, and HTML and plain text conversions of Content. Co-Authored-By: Claude Opus 5.5 (1M context) --- gems/actiontext/8.1/_test/test.rb | 12 ++++ gems/actiontext/8.1/actiontext-generated.rbs | 75 -------------------- gems/actiontext/8.1/actiontext.rbs | 27 +++++++ 3 files changed, 39 insertions(+), 75 deletions(-) diff --git a/gems/actiontext/8.1/_test/test.rb b/gems/actiontext/8.1/_test/test.rb index 16b9c6b92..0ddd90f29 100644 --- a/gems/actiontext/8.1/_test/test.rb +++ b/gems/actiontext/8.1/_test/test.rb @@ -17,3 +17,15 @@ class Comment < ActiveRecord::Base rich_text.body&.to_html rich_text.embeds.attached? rich_text.updated_at.to_date +rich_text.to_s.size +rich_text.to_plain_text.size +rich_text.to_trix_html&.size +if (content = rich_text.body) + content.links.first&.size + content.attachments.first&.to_html + content.gallery_attachments.size + content.to_plain_text.size + content.to_trix_html.size + content.to_html.size + content.to_s.size +end diff --git a/gems/actiontext/8.1/actiontext-generated.rbs b/gems/actiontext/8.1/actiontext-generated.rbs index a58cc6cc5..c8453da30 100644 --- a/gems/actiontext/8.1/actiontext-generated.rbs +++ b/gems/actiontext/8.1/actiontext-generated.rbs @@ -198,38 +198,10 @@ module ActionText # message.content.to_s # => "
safeunsafe
" # message.content.to_plain_text # => "safeunsafe" class RichText < ::ActionText::Record - def to_s: (*untyped, **untyped) ?{ (*untyped) -> untyped } -> untyped def nil?: (*untyped, **untyped) ?{ (*untyped) -> untyped } -> untyped def blank?: (*untyped, **untyped) ?{ (*untyped) -> untyped } -> untyped def empty?: (*untyped, **untyped) ?{ (*untyped) -> untyped } -> untyped def present?: (*untyped, **untyped) ?{ (*untyped) -> untyped } -> untyped - - # Returns a plain-text version of the markup contained by the `body` attribute, - # with tags removed but HTML entities encoded. - # - # message = Message.create!(content: "

Funny times!

") - # message.content.to_plain_text # => "Funny times!" - # - # NOTE: that the returned string is not HTML safe and should not be rendered in - # browsers. - # - # message = Message.create!(content: "<script>alert()</script>") - # message.content.to_plain_text # => "" - def to_plain_text: () -> untyped - - # Returns the `body` attribute in a format that makes it editable in the Trix - # editor. Previews of attachments are rendered inline. - # - # content = "

Funny Times!

" - # message = Message.create!(content: content) - # message.content.to_trix_html # => - # #
- # #

Funny times!

- # #
- # # - # #
- # #
- def to_trix_html: () -> untyped end end @@ -625,25 +597,8 @@ module ActionText def initialize: (?untyped? content, ?::Hash[untyped, untyped] options) -> void - # Extracts links from the HTML fragment: - # - # html = 'Example' - # content = ActionText::Content.new(html) - # content.links # => ["http://example.com/"] - def links: () -> untyped - - # Extracts ActionText::Attachment objects from the HTML fragment: - # - # attachable = ActiveStorage::Blob.first - # html = %Q() - # content = ActionText::Content.new(html) - # content.attachments # => [# untyped - def attachment_galleries: () -> untyped - def gallery_attachments: () -> untyped - # Extracts ActionText::Attachable objects from the HTML fragment: # # attachable = ActiveStorage::Blob.first @@ -658,40 +613,10 @@ module ActionText def render_attachment_galleries: () { (?) -> untyped } -> untyped - # Returns a plain-text version of the markup contained by the content, with tags - # removed but HTML entities encoded. - # - # content = ActionText::Content.new("

Funny times!

") - # content.to_plain_text # => "Funny times!" - # - # content = ActionText::Content.new("
safe
") - # content.to_plain_text # => "safe" - # - # NOTE: that the returned string is not HTML safe and should not be rendered in - # browsers without additional sanitization. - # - # content = ActionText::Content.new("<script>alert()</script>") - # content.to_plain_text # => "" - # ActionText::ContentHelper.sanitizer.sanitize(content.to_plain_text) # => "" - def to_plain_text: () -> untyped - - def to_trix_html: () -> untyped - - def to_html: () -> untyped - def to_rendered_html_with_layout: () -> untyped def to_partial_path: () -> "action_text/contents/content" - # Safely transforms Content into an HTML String. - # - # content = ActionText::Content.new("

Funny times!

") - # content.to_s # => "

Funny times!

" - # - # content = ActionText::Content.new("
safe
") - # content.to_s # => "
safeunsafe
" - def to_s: () -> untyped - def as_json: (*untyped) -> untyped def inspect: () -> ::String diff --git a/gems/actiontext/8.1/actiontext.rbs b/gems/actiontext/8.1/actiontext.rbs index dd5603900..951cc05fc 100644 --- a/gems/actiontext/8.1/actiontext.rbs +++ b/gems/actiontext/8.1/actiontext.rbs @@ -31,3 +31,30 @@ module ActionText def updated_at: () -> ActiveSupport::TimeWithZone end end + +# Return types of commonly used methods +module ActionText + class RichText + def to_s: () -> String + + def to_plain_text: () -> String + + def to_trix_html: () -> String? + end + + class Content + def links: () -> Array[String] + + def attachments: () -> Array[ActionText::Attachment] + + def gallery_attachments: () -> Array[ActionText::Attachment] + + def to_plain_text: () -> String + + def to_trix_html: () -> String + + def to_html: () -> String + + def to_s: () -> String + end +end