Sobrescreva os métodos existentes do Discourse em plugins

Tenho encontrado várias situações recentemente em que preciso sobrescrever métodos Ruby existentes de plugins, e pensei em compartilhar minhas melhores práticas aqui.

Sobrescrevendo um método de instância

class ::TopicQuery
  module BabbleDefaultResults
    def default_results(options={})
      super(options).where('archetype <> ?', Archetype.chat)
    end
  end
  prepend BabbleDefaultResults
end
  • Aqui estou removendo tópicos de chat de um método de instância que retorna uma lista de tópicos.
  • O nome do módulo BabbleDefaultResults pode ser qualquer coisa que você desejar; geralmente faço com que corresponda ao nome do método, mais o nome do meu plugin, para minimizar qualquer risco de conflito de nomes (embora eles já sejam bastante baixos).
  • Module#prepend é super legal e você deve conhecê-lo se estiver escrevendo plugins para qualquer coisa em Ruby. Note que é o fato de estarmos prependendo um módulo que nos permite chamar super dentro do método de sobrescrita.
  • PS, sempre chame super! Isso torna seu plugin muito menos propenso a quebrar quando a implementação subjacente muda. A menos que você esteja realmente, realmente certo de que sua funcionalidade substitui tudo no método subjacente, você deve chamar super e modificar os resultados a partir daí, para que alterações nesse método no núcleo do Discourse não façam seu plugin quebrar mais tarde.
  • O :: em ::TopicQuery garante que estou me referindo à classe TopicQuery de nível superior para sobrescrever, e não a alguma versão modularizada dela (como Babble::TopicQuery).
  • Isso pode ser inserido diretamente em plugin.rb como está, ou se seu plugin for grande, você pode considerar separar cada sobrescrita em um arquivo diferente.

Sobrescrevendo um método de classe

class ::Topic
  module BabbleForDigest
    def for_digest(user)
      super(user).where('archetype <> ?', Archetype.chat)
    end
  end
  singleton_class.prepend BabbleForDigest
end
  • Aqui estou pegando um método self.for_digest existente na classe Topic e removendo tópicos de chat do resultado.
  • Muito semelhante à sobrescrita de método de instância, observe a diferença: estamos chamando singleton_class.prepend em vez de apenas prepend. singleton_class é uma maneira um pouco estranha de dizer “quero anexar isso ao nível da classe, não ao nível da instância”, leitura adicional se você estiver procurando um buraco de coelho relacionado a Ruby.

Sobrescrevendo um escopo

class ::Topic
  @@babble_listable_topics = method(:listable_topics).clone
  scope :listable_topics, ->(user) {
    @@babble_listable_topics.call(user).where('archetype <> ?', Archetype.chat)
  }
end
  • Esta é um pouco mais complicada porque os escopos não funcionam bem com super (ou, pelo menos, eu não consegui fazê-los funcionar). Então, em vez disso, estamos pegando uma definição de método existente, clonando-a, armazenando-a e depois chamando-a mais tarde.
  • Novamente, @@babble_listable_topics pode ser qualquer coisa que você desejar, mas usar o nome do seu plugin como um namespace provavelmente é uma boa ideia.
  • Mais sobre a função method, que também é super legal, embora os momentos em que você realmente precise dela sejam bastante raros. Bônus: fato divertido relacionado a isso; ao depurar, se você estiver com dificuldade para descobrir qual código está sendo executado para uma chamada de método específica (geralmente “Qual gem está definindo este método?”), você pode usar source_location para obter a linha exata do código-fonte onde o método está definido.
[7] pry(main)> Topic.new.method(:best_post).source_location
=> ["/Users/gdpelican/workspace/discourse/app/models/topic.rb", 282]

( ^^ isso está dizendo que o método best_post em um novo tópico está definido em /app/models/topic.rb, na linha 282)

Bem, é tudo o que tenho. Deixe-me saber se devo corrigir, expandir ou esclarecer algo :slight_smile:

35 curtidas

This is great, thanks for sharing James!

Overriding instance and class methods

My fav resource on this is: https://stackoverflow.com/a/4471202

I use basically the same structure, except I tend to seperate out the module and the prepend.

As you pointed out, this pattern is “super” :wink: useful when trying to avoid overriding core logic.

module InviteMailerEventExtension
  def send_invite(invite)
     ## stuff
     super(invite)
  end
end

require_dependency 'invite_mailer'
class ::InviteMailer
  prepend InviteMailerEventExtension
end

One small tip here is that when overriding private or protected methods, your overriding method also needs to be private or protected, e.g.

module UserNotificationsEventExtension
  protected def send_notification_email(opts)
    ## stuff
    super(opts)
  end
end
20 curtidas

@angus @gdpelican Thanks for this. This is great stuff. :smiley: . This would be really essential all the (especially newbies like me) plugin developers out there.

This is what I really really needed to be aware of. I use to think that if you override a method only to make a few changes to it, you’d probably copy the code to your new method and make changes to it which by the very thought of it sounded hacky.

3 curtidas

Hi, I was wondering about this part of the code? why is this require_dependency needed? It seems like the code is working without it as well.

Yes, indeed. Since that post, updates to Discourse’s use of rails have made require_dependency unecessary. I’m unable to edit the post to address that. See further:

3 curtidas

Alguma dica para sobrescrever classes de módulos? Quero fazer algumas alterações no GroupGuardian (algumas condições especiais para um tipo especial de grupo).

Obrigado.

1 curtida

Você pode simplesmente redefinir o módulo e redefinir a função. Use alias_method como este RubyDoc.info: Method: Module#alias_method – Documentation for core (3.4.3) – RubyDoc.info se você quiser manter o acesso ao método antigo.

3 curtidas

Novo no desenvolvimento de discourse e Rails. Estou usando o ambiente Dev Container (no VS Code) localmente. Os guias e a documentação têm sido úteis.

Gostaria de saber se alguém tem alguma dica sobre como substituir classes principais do discourse, especificamente para que isso persista em um ambiente de desenvolvimento local.

No meu plugin, estou tentando substituir um método na classe principal do discourse TopicEmbed. (usando a abordagem geral bem documentada por @angus acima.) Funciona uma vez quando reconstruo e recarrego o VS Code, mas em requisições http subsequentes, meu override nunca é invocado.

Meu override está definido em /plugins/my-plugin/app/models/override.rb e eu uso require_relative para incluir este arquivo no meu plugin.rb.

#override.rb:
class ::TopicEmbed

  # um módulo que será prepended na singleton_class do TopicEmbed.
  module TopicEmbedOverrideModule
    # método em TopicEmbed
    def first_paragraph_from(html)
      Rails.logger.info(“meu override está acontecendo! ”)

      # continuar com a implementação original fornecida por TopicEmbed.
      super

    end
  end

  # fazer o prepend aqui
  singleton_class.prepend TopicEmbedOverrideModule
end

Suspeito que este desafio de persistência possa ser devido ao meu ambiente de desenvolvimento e como o código ruby é compilado/cacheado.
Também tentei rm -rf tmp; bin/ember-cli -u e bundle exec rake tmp:cache:clear.

consegui fazer funcionar para uma classe singleton desta forma:

# meus overrides.rb

# Um módulo que será anteposto à singleton_class de TopicEmbed
module TopicEmbedOverrides

  # Sobrescreve o método parse_html
  def parse_html(html, url) # nota: eu não uso self. aqui
    # minhas novas coisas aqui
    # então executa a implementação original
    super
  end

end

# faz a sobrescrita aqui
class ::TopicEmbed
  singleton_class.prepend TopicEmbedOverrides
end