How do these 'proxy' types work exactly?

Hi, I’m trying to learn to use ruby debug/steep. Take the case of ActiveStorage.

I have an ActiveStorage::Attached::One called ‘content’. If I do “content.methods” or “content.inspect” I won’t see the symbol ‘blob’ or ‘attachment’, but I can do “content.blob” or “content.attachment” and get valid results. I can also do “content.blob_size” and get a blob size, even though it’s only listed explicitly if I do “content.blob.inspect”, and the type checker steep complains when I use ‘content.blob_size’ in my code.

How does this work under the hood? How do I get steep and my IDE to recognize what’s happening?

1 Like

It’s delegate_missing_to, twice. In activestorage 8.1:

# lib/active_storage/attached/one.rb
delegate_missing_to :attachment, allow_nil: true

# app/models/active_storage/attachment.rb
delegate_missing_to :blob

So anything Attached::One doesn’t define itself goes to the ActiveStorage::Attachment record, and anything the attachment doesn’t define goes on to its ActiveStorage::Blob. That’s why content.byte_size, content.filename, content.content_type etc. all work from the top.

delegate_missing_to is built on method_missing plus respond_to_missing?, so the methods never exist on the object. content.methods won’t list them, but content.respond_to?(:blob) returns true. Quick way to see it outside Rails:

require "active_support/all"
class Att; def blob = "B"; end
class One; delegate_missing_to :att; def att = Att.new; end
o = One.new
o.methods.include?(:blob) # => false
o.respond_to?(:blob)      # => true
o.blob                    # => "B"

Steep can’t follow method_missing, and the activestorage signatures in gem_rbs_collection only declare a handful of methods on Attached::One (attachment, attach, attached?, url, purge…). Two ways out:

  1. Be explicit in code: content.attachment&.blob&.byte_size. Every hop is typed, and it’s arguably clearer to read anyway.
  2. Reopen the class in your own sig/ with just the delegated methods you use:
module ActiveStorage
  class Attached::One
    def blob: () -> ActiveStorage::Blob?
    def byte_size: () -> Integer?
    def filename: () -> ActiveStorage::Filename?
    def content_type: () -> String?
  end
end

RBS merges that with the collection’s definition, so you only add what’s missing. The IDE side (ruby-lsp / Steep’s LSP) picks it up from the same files.

The behavior comes from Rails’ delegation/proxy layer rather than content actually having every method as a normal instance method. ActiveStorage::Attached::One exposes attachment/blob-related behavior through generated delegation methods, so methods/inspect can be misleading compared with what you can actually call at runtime.

For Steep and IDE support, I’d look at the Rails/RBS signatures for ActiveStorage::Attached::One and the generated delegation rather than trying to infer the API from methods. If a valid runtime method such as blob_size is missing from the type information, that’s likely a typing/signature gap that should be addressed in the Rails RBS definitions.