A máquina real por trás do config.middleware
O desafio final do curso passado pedia uma função run_chain(env, middlewares, final_app). Ela começa por final_app, percorre a lista de middlewares de trás para frente e envolve cada um em volta da aplicação atual. Se a sua solução ficou parecida com middlewares.reverse_each { |mw| app = mw.new(app) }, ela fez mais do que passar nos testes. Você escreveu a mesma linha que o Rails roda a cada boot, em produção.
O código-fonte
ActionDispatch::MiddlewareStack é a classe que o config.middleware monta no fim. Ela fica em lib/action_dispatch/middleware/stack.rb, dentro da actionpack-8.1.3.1, e tem um método build:
def build(app = nil, &block)
instrumenting = ActiveSupport::Notifications.notifier.listening?(InstrumentationProxy::EVENT_NAME)
middlewares.freeze.reverse.inject(app || block) do |a, e|
if instrumenting
e.build_instrumented(a)
else
e.build(a)
end
end
end
Deixe de lado o branch de instrumentação. Ele envolve cada middleware numa classe que dispara um evento de ActiveSupport::Notifications em volta de call. Isso ajuda no profiling, mas não tem nada a ver com o encadeamento. O que sobra é middlewares.freeze.reverse.inject(app) { |a, e| e.build(a) }: a mesma ideia de inverter a lista e envolver do run_chain, escrita com inject no lugar de um loop que reatribui uma variável.
O que e.build(a) faz
Cada e aqui é uma instância de Middleware, uma classe pequena dentro de MiddlewareStack, e não a sua classe de middleware. Ela guarda a classe, os argumentos do construtor e o bloco passado para use, insert_after e os demais:
class Middleware
attr_reader :args, :block, :klass
def initialize(klass, args, block)
@klass = klass
@args = args
@block = block
end
def build(app)
klass.new(app, *args, &block)
end
end
build(app) tem uma linha só: klass.new(app, *args, &block). Num config.middleware.use RequestTimingLogger simples, isso vira RequestTimingLogger.new(app), o mesmo initialize(app) do curso passado. O *args, &block serve para as chamadas que passam mais coisas. O config/initializers/omniauth.rb desta aplicação chama config.middleware.use OmniAuth::Builder do ... end, e o bloco segue direto para OmniAuth::Builder.new(app, &block).
Por que essa coincidência importa
Chegar ao mesmo código era o objetivo do desafio do curso passado. ActionDispatch::MiddlewareStack não é uma caixa-preta. É a ideia pequena e legível que você já construiu à mão, com uma classe pequena Middleware e um inject no lugar do loop. Sempre que esta aplicação sobe, em desenvolvimento ou em produção, esse build roda uma vez. Ele percorre de trás para frente a lista do config.middleware (29 entradas em desenvolvimento, 26 em produção) e devolve um único objeto aninhado, com run NewbieOnRails::Application.routes no centro. Não há outra camada por baixo.
Pontos-chave
ActionDispatch::MiddlewareStack#build, naactionpack-8.1.3.1, se resume amiddlewares.freeze.reverse.inject(app) { |a, e| e.build(a) }: a mesma lógica de inverter e envolver do desafiorun_chaindo curso passado.- Cada entrada da pilha é um objeto
Middlewareque guarda a classe, os argumentos do construtor e o bloco. Obuild(app)dela tem uma linha:klass.new(app, *args, &block). - É por repassar
*args, &blockque oconfig.middleware.use OmniAuth::Builder do ... enddesta aplicação funciona: o bloco dado aousechega ao construtor do próprio middleware. - Isso roda uma vez por boot, na pilha da aplicação (29 entradas em desenvolvimento, 26 em produção), e gera a cadeia aninhada que o
bin/rails middlewaremostra. Não há mais nada escondido por baixo.
Continue com a aula 2
Gravar em vez de aplicar: o MiddlewareStackProxy
As chamadas a config.middleware guardam lambdas em arrays, e o merge_into as aplica no fim do boot sobre a pilha padrão.
Com uma conta grátis você também tem os quizzes, seu progresso e suas anotações, e a discussão.