将 Ruby 源文件中的函数替换为 Ruby

Replacing a function in a Ruby source file with Ruby

首先,我使用的是 Ruby 2.2.0 和 Rails 4.2.0(所以我安装了 ActiveSupport,如果其中有任何对这项任务有帮助的话)。

什么

我想要做的是读取一个 Ruby 文件并采用其中声明的任何方法,然后将它们写入/覆盖到另一个可能已经有这些的 Ruby 文件中声明的方法。

我想在源文件中做这个,它是作为一种预处理器的事情来做的,而不是在 运行 的时候。我不是在谈论 Open Classes,我想永久编辑源文件。

如果什么都没有,那么我会做一些读取文件的事情,逐行查找 defend 语句,但我想知道是否已经存在这样的东西?

为什么

部分是为了工作,部分是为了看看我是否可以,我在 Ruby 中编写了一个脚本,它解析由 ArgoXML 生成的 XMI 文件,在我的绘图中使用一些进一步的约束,以便我的脚本知道我什么时候我正在使用 has_manyhas_many :through,后者我用于我所有的 habtm。

然后它在 shell 脚本中为我的所有模型等创建 Rails 生成器命令,并使用 awk 注入任何需要手动设置的关系(如不符合发电机)。

这为大约 100 个表快速创建了模型和数据库条目。显然,虽然 运行再次使用我的脚本会删除任何现有文件(或者问我 100 次我想做什么,考虑到大部分时间都被销毁,我宁愿不这样做)。

示例:

我很高兴只有一个带有函数声明的 class,或者我可以将它放在一个命名的 class 或模块中,这都没有关系。文件中永远只有一个 class。

我想要发生的是读取文件 A 的内容,从文件中获取每个方法。然后我想将这些方法覆盖到文件 B 的源代码中,以便我得到文件 C。

文件A(方法需要插入文件B的文件)

def show
  puts params[:rule_set_edition_id]
  @description = @rule.description
  @input_lines = @description.split("\n")
  @output_lines = []
  for input_line in @input_lines do
    if input_line != "" then
      @output_lines << input_line
    end
  end

  @rule_set_edition 
end

def some_other_method
  puts "FooBar"
end

文件 B(部分 - 标准 Rails 脚手架,覆盖前)

class RulesController < ApplicationController
  before_action :set_rule, only: [:show, :edit, :update, :destroy]

  # GET /rules
  # GET /rules.json
  def index
    @rules = Rule.all
  end

  # GET /rules/1
  # GET /rules/1.json
  def show
  end

  # GET /rules/new
  def new
    @rule = Rule.new
  end

  # GET /rules/1/edit
  def edit
  end

  . . . etc

end

文件C(部分-我想要的输出文件)

class RulesController < ApplicationController
  before_action :set_rule, only: [:show, :edit, :update, :destroy]

  # GET /rules
  # GET /rules.json
  def index
    @rules = Rule.all
  end

  # GET /rules/1
  # GET /rules/1.json
  def show
    puts params[:rule_set_edition_id]
    @description = @rule.description
    @input_lines = @description.split("\n")
    @output_lines = []
    for input_line in @input_lines do
      if input_line != "" then
        @output_lines << input_line
      end
    end

    @rule_set_edition 
  end

  def some_other_method
    puts "FooBar"
  end

  # GET /rules/new
  def new
    @rule = Rule.new
  end

  # GET /rules/1/edit
  def edit
  end
end

更新 注意:我意识到这是错误的 atm 并且正在修复它。

由于我的 XMI 解析器 class 中已经有一个变量 @models,它以简洁的格式包含了我的 XMI 上的所有信息,我可以向我的解析器添加一个函数来破解模板。

我使用了@ChrisHeald 提供的代码,将它放在一个名为 hack_templates.rb 的文件中,并使其接受命令行 ARGV 作为实现和模板文件位置。

我的新函数然后在我的输出中添加额外的 shell 命令行。

    def hack_templates
      hacks = []
      for idref, model in @models do
        # Controllers
        controller_name = "#{model['name'].tableize}_controller.rb"
        controller_implementation = "./db/init/controllers/#{controller_name}"
        countroller_template = "./app/controllers/#{controller_name}"
        if File.exist?(countroller_template) and File.exist?(controller_implementation) then
          puts "ruby hack_template.rb #{controller_implementation} #{countroller_template}"
        end
        # Models

        # Views
      end
      hacks.join("\n")
    end

@ChrisHeald 代码中的 hack_templates.rb:

require 'method_source'
include MethodSource::CodeHelpers

$implementation = open(ARGV[0])
$template = open(ARGV[1])

module Implementation
  class << self
    eval $implementation.read
  end
end

@template_by_line = $template.each_line.to_a

(Implementation.methods - Module.new.methods).each do |method|
  $implementation.rewind
  # Get the source code of our implementation method
  impl_source = expression_at $implementation, Implementation.method(method).source_location[1]

  # Get the line that this method starts on in the template
  template_line = @template_by_line.index {|line| line.match(/^\s*def #{method}/) }

  if template_line
    # If we found a match, replace it
    # Get the source code for the template method
    tmpl_source = expression_at @template_by_line, template_line + 1

    # Replace it with the implemetation method
    @template_by_line[template_line] = impl_source

    # Remove any extra lines from the template method
    tmpl_source.split(/\n/).length.times do |len|
      @template_by_line.delete_at template_line + len + 1
    end
  else
    # find the last `end` in the template array and insert the implementation source before it
    last_end = @template_by_line.rindex {|line| line.match(/^\s*end/)}
    @template_by_line.insert(last_end, "\n" + impl_source + "\n")
  end

end

File.open($template, "w+") do |f|
   f.syswrite(@template_by_line.join)
end

这只会从第二个 运行 开始起作用,因为控制器在我调用此代码的地方不存在,除非那里有旧控制器。对我来说,只 运行 两次可能很容易,但我也可以将 hack_template.rb 命令发送到与主 output.sh

不同的文件

进一步更新 最后我最终使用了 Rails 关注点。我按照 ClassNameConcern 的命名约定为每个 class 创建了一个新关注点。然后我将我所有的 class 方法放在一个问题中,让我的模型只剩下自动生成的东西。

因为我有大约 100 个模型,到目前为止我已经编辑了 4 个模型,所以效果很好。不过,@ChrisHeald 的回答仍然是我问题的正确答案。

发电机

最简单的方法就是使用自定义生成器模板。

In Rails 3.0 and above, generators don't just look in the source root for templates, they also search for templates in other paths. And one of them is lib/templates. Since we want to customize Rails::Generators::HelperGenerator, we can do that by simply making a template copy inside lib/templates/rails/helper with the name helper.rb.

http://guides.rubyonrails.org/generators.html#customizing-your-workflow-by-changing-generators-templates

控制器模板在这里:

https://github.com/rails/rails/blob/master/railties/lib/rails/generators/rails/controller/templates/controller.rb

所以您可以在 lib/templates/rails/controller/controller.rb 中创建一个副本,然后将其自定义为:

<% if namespaced? -%>
require_dependency "<%= namespaced_path %>/application_controller"
<% end -%>
<% module_namespacing do -%>
class <%= class_name %>Controller < ApplicationController

  def show
    puts params[:rule_set_edition_id]
    @description = @rule.description
    @input_lines = @description.split("\n")
    @output_lines = []
    for input_line in @input_lines do
      if input_line != "" then
        @output_lines << input_line
      end
    end

    @rule_set_edition
  end

  def some_other_method
    puts "FooBar"
  end

<% (actions - [:show, :some_other_method]).each do |action| -%>
  def <%= action %>
  end
<%= "\n" unless action == actions.last -%>
<% end -%>
end
<% end -%>

然后,当您生成控制器时,Rails 将使用您的模板(与您的实现)。

就是说,如果这些方法在您生成的所有控制器中都是通用的,您应该考虑简单地从通用超类继承您的控制器,然后让 show 的默认实现类似于:

def show
  super
end

还有 post 的选项 - 使用 source_location 处理生成的模板以检测方法边界,但这很繁琐。我目前正在为此进行概念验证。

实际上是在修改模板

我们将使用 method_source gem,它使用 Method#source_location 来查找方法的起始位置,然后计算行直到找到完整的表达式。一旦我们有了它,那么我们就可以用我们的实现替换给定的方法。

这太糟糕了,如果可行,我建议不要这样做。

implementation.rb

  def show
    puts params[:rule_set_edition_id]
    @description = @rule.description
    @input_lines = @description.split("\n")
    @output_lines = []
    for input_line in @input_lines do
      if input_line != "" then
        @output_lines << input_line
      end
    end

    @rule_set_edition
  end

  def some_other_method
    puts "FooBar"
  end

template.rb

class RulesController < ApplicationController
  before_action :set_rule, only: [:show, :edit, :update, :destroy]

  # GET /rules
  # GET /rules.json
  def index
    @rules = Rule.all
  end

  # GET /rules/1
  # GET /rules/1.json
  def show
  end

  # GET /rules/new
  def new
    @rule = Rule.new
  end

  # GET /rules/1/edit
  def edit
  end

  # . . . etc
end

converter.rb

require 'method_source'
include MethodSource::CodeHelpers

$implementation = open("implementation.rb")
$template = open("template.rb")

module Implementation
  class << self
    eval $implementation.read
  end
end

@template_by_line = $template.each_line.to_a

(Implementation.methods - Module.new.methods).each do |method|
  $implementation.rewind
  # Get the source code of our implementation method
  impl_source = expression_at $implementation, Implementation.method(method).source_location[1]

  # Get the line that this method starts on in the template
  template_line = @template_by_line.index {|line| line.match(/^\s*def #{method}/) }

  if template_line
    # If we found a match, replace it
    # Get the source code for the template method
    tmpl_source = expression_at @template_by_line, template_line + 1

    # Replace it with the implemetation method
    @template_by_line[template_line] = impl_source

    # Remove any extra lines from the template method
    tmpl_source.split(/\n/).length.times do |len|
      @template_by_line.delete_at template_line + len + 1
    end
  else
    # find the last `end` in the template array and insert the implementation source before it
    last_end = @template_by_line.rindex {|line| line.match(/^\s*end/)}
    @template_by_line.insert(last_end, "\n" + impl_source + "\n")
  end

end
puts @template_by_line.join

还有一些输出:

$ ruby converter.rb
class RulesController < ApplicationController
  before_action :set_rule, only: [:show, :edit, :update, :destroy]

  # GET /rules
  # GET /rules.json
  def index
    @rules = Rule.all
  end

  # GET /rules/1
  # GET /rules/1.json
  def show
    puts params[:rule_set_edition_id]
    @description = @rule.description
    @input_lines = @description.split("\n")
    @output_lines = []
    for input_line in @input_lines do
      if input_line != "" then
        @output_lines << input_line
      end
    end

    @rule_set_edition
  end

  def new
    @rule = Rule.new
  end

  # GET /rules/1/edit
  def edit

  def some_other_method
    puts "FooBar"
  end  end

  # . . . etc
end