<commit_before>require "erb"
require "fileutils"

class Rails < RubyGenerator
  attr_accessor :app, :opts, :generator
  def initialize(app, opts = { })
    @app = app
    @opts = opts
    @generator = self
  end

  # returns the rails string defining an association.  Supports belongs_to, has_many, hmt
  def association_string(assoc_name, assoc_hash)
    option_str = ""
    option_str << ", :through => :#{assoc_hash[:through].p}" if assoc_hash[:through]
    "#{assoc_hash[:type]} :#{assoc_name + option_str}"
  end

  # wraps an array of lines in a method call, with a given name, using a given indentation.
  def wrap_method(name, arr, indent_string = "  ")
    indent_string + "def #{name}\n" + arr.map { |s| indent_string + "  " + s }.join("\n") + "\n#{indent_string}end"
  end
  
  # TODO: right now this doesn't handle routes that have an multiply routed resource in the chain somewhere
  # eg route :blogs, [:users, [:blogs, :posts]]  It's obvious in posts that blogs is the nested bit.  
  def find_method(goal)
    wrap_method("find_#{goal.s}",
                goal.resource_tuple[0..-2].map { |var| finder_string(var.to_s.singularize.camelize.constantize) } +
                [finder_string(goal, "id")] + 
               goal.nested_resources.map { |k,v| new_object_string(v)})
  end
  
  # this returns @foo = Foo.find(params[:param_name]) or @foo = @bar.foos.find(params[:param_name])
  def finder_string(goal, id_str = nil)
    id_str ||= "#{goal.s}_id"
    if goal.nested?
      enclosing_resource = @app.fetch_goal(goal.resource_tuple[-2])
      "@#{goal.s} = @#{enclosing_resource.s}.#{goal.p}.find(params[:#{id_str}])"
    else
      "@#{goal.s} = #{goal.cs}.find(params[:#{id_str}])"
    end
  end
  
  # returns the string necessary to assign a newly created instance of goal to an instance variable.  
  def new_object_string(goal)
    if goal.nested?
      enclosing_resource = @app.fetch_goal(goal.resource_tuple[-2])
      "@#{goal.s} = @#{enclosing_resource.s}.#{goal.p}.new(params[:#{goal.s}])"
    else
      "@#{goal.s} = #{goal.cs}.new(params[:#{goal.s}])"
    end
  end

  #returns a string assigning a collection of goal elements to an instance variable.
  def collection_finder_string(goal)
    if goal.nested?
      enclosing_resource = @app.fetch_goal(goal.resource_tuple[-2])
      "@#{goal.p} = @#{enclosing_resource.s}.#{goal.p}"
    else
      "@#{goal.p} = #{goal.cs}.find(:all)"
    end
  end
  
  def test_var_string(sym)
    goal = app.fetch_goal(sym)
    if goal.nested?
      enclosing_resource = app.fetch_goal(goal.resource_tuple[-2])
      "@#{goal.s} = @#{enclosing_resource.s}.#{goal.p}.find(:first)"
    else
      "@#{goal.s} = #{goal.cs}.find(:first)"
    end
  end

  # returns a list of all the params that are required for a goal but not inferrable from the path in which it is encountered.  
  def required_nonpath_params(goal)
    goal.associations.reject { |k,v| v[:type] != :belongs_to }.keys.reject {|x| goal.resource_tuple.map { |y| y.to_s.singularize }.member?(x) }
  end
  
  def required_nonpath_param_string(goal)
    required_nonpath_params(goal).map { |z| ":#{ z.singularize }_id => 1" }.join(", ")
  end
  
  def new_object_method(goal)
    wrap_method("new_#{goal.s}", (goal.resource_tuple[0..-2].map { |var| finder_string(@app.fetch_goal(var)) } +
                                  [new_object_string(goal)]))
  end

  def find_collection_method(goal)
    wrap_method("find_#{goal.p}", (goal.resource_tuple[0..-2].map { |var| finder_string(@app.fetch_goal(var)) } +
                                   [collection_finder_string(goal), new_object_string(goal)]))
  end

  def object_path(goal, str = "@")
    goal.underscore_tuple.join("_") + '_path(' + (goal.ivar_tuple[0..-2]  + ["#{str.to_s + goal.s})"]).join(', ')
  end

  #TODO: see if these can be removed.
#   def edit_path(goal, str = nil)
#     if str
#       "edit_" + object_path(goal, str)
#     else
#       "edit_" + object_path(goal)
#     end
#   end
  
#   def new_path(goal)
#     "new_" + (goal.rails_underscore_tuple[0..-2] + ["#{goal.s}"]).join("_") + "_path(" + goal.rails_ivar_tuple(-2).join(', ') + ')'
#   end
  
  def collection_path(goal)
    (goal.underscore_tuple[0..-2] + [goal.p]).join("_") +  "_path(" + goal.ivar_tuple[0..-2].join(', ') + ')'
  end

  # the core method that generates the whole app.  
  def generate
    gen_app
    gen_routes
     @app.goals.values.each_with_index do |goal, i|
       gen_migration(goal, i)
       gen_model(goal)
       gen_controller(goal)
       gen_view(goal)
       gen_tests(goal)
       gen_misc
    end
    self
  end

  # this currently shells out to rails, and perhaps that's the right thing to do.  
  def gen_app
    unless File.exists?(app_dir) #TODO: figure out what to do when there is a directory there.
      original_dir = FileUtils.pwd
      Dir.mkdir(root_dir) unless (!root_dir or File.exists?(root_dir))
      FileUtils.cd(root_dir) if root_dir
      `#{rails_str}`<commit_msg>split out a new method that will generate everything for a given goal.<commit_after>require "erb"
require "fileutils"

class Rails < RubyGenerator
  attr_accessor :app, :opts, :generator
  def initialize(app, opts = { })
    @app = app
    @opts = opts
    @generator = self
  end

  # returns the rails string defining an association.  Supports belongs_to, has_many, hmt
  def association_string(assoc_name, assoc_hash)
    option_str = ""
    option_str << ", :through => :#{assoc_hash[:through].p}" if assoc_hash[:through]
    "#{assoc_hash[:type]} :#{assoc_name + option_str}"
  end

  # wraps an array of lines in a method call, with a given name, using a given indentation.
  def wrap_method(name, arr, indent_string = "  ")
    indent_string + "def #{name}\n" + arr.map { |s| indent_string + "  " + s }.join("\n") + "\n#{indent_string}end"
  end
  
  # TODO: right now this doesn't handle routes that have an multiply routed resource in the chain somewhere
  # eg route :blogs, [:users, [:blogs, :posts]]  It's obvious in posts that blogs is the nested bit.  
  def find_method(goal)
    wrap_method("find_#{goal.s}",
                goal.resource_tuple[0..-2].map { |var| finder_string(var.to_s.singularize.camelize.constantize) } +
                [finder_string(goal, "id")] + 
               goal.nested_resources.map { |k,v| new_object_string(v)})
  end
  
  # this returns @foo = Foo.find(params[:param_name]) or @foo = @bar.foos.find(params[:param_name])
  def finder_string(goal, id_str = nil)
    id_str ||= "#{goal.s}_id"
    if goal.nested?
      enclosing_resource = @app.fetch_goal(goal.resource_tuple[-2])
      "@#{goal.s} = @#{enclosing_resource.s}.#{goal.p}.find(params[:#{id_str}])"
    else
      "@#{goal.s} = #{goal.cs}.find(params[:#{id_str}])"
    end
  end
  
  # returns the string necessary to assign a newly created instance of goal to an instance variable.  
  def new_object_string(goal)
    if goal.nested?
      enclosing_resource = @app.fetch_goal(goal.resource_tuple[-2])
      "@#{goal.s} = @#{enclosing_resource.s}.#{goal.p}.new(params[:#{goal.s}])"
    else
      "@#{goal.s} = #{goal.cs}.new(params[:#{goal.s}])"
    end
  end

  #returns a string assigning a collection of goal elements to an instance variable.
  def collection_finder_string(goal)
    if goal.nested?
      enclosing_resource = @app.fetch_goal(goal.resource_tuple[-2])
      "@#{goal.p} = @#{enclosing_resource.s}.#{goal.p}"
    else
      "@#{goal.p} = #{goal.cs}.find(:all)"
    end
  end
  
  def test_var_string(sym)
    goal = app.fetch_goal(sym)
    if goal.nested?
      enclosing_resource = app.fetch_goal(goal.resource_tuple[-2])
      "@#{goal.s} = @#{enclosing_resource.s}.#{goal.p}.find(:first)"
    else
      "@#{goal.s} = #{goal.cs}.find(:first)"
    end
  end

  # returns a list of all the params that are required for a goal but not inferrable from the path in which it is encountered.  
  def required_nonpath_params(goal)
    goal.associations.reject { |k,v| v[:type] != :belongs_to }.keys.reject {|x| goal.resource_tuple.map { |y| y.to_s.singularize }.member?(x) }
  end
  
  def required_nonpath_param_string(goal)
    required_nonpath_params(goal).map { |z| ":#{ z.singularize }_id => 1" }.join(", ")
  end
  
  def new_object_method(goal)
    wrap_method("new_#{goal.s}", (goal.resource_tuple[0..-2].map { |var| finder_string(@app.fetch_goal(var)) } +
                                  [new_object_string(goal)]))
  end

  def find_collection_method(goal)
    wrap_method("find_#{goal.p}", (goal.resource_tuple[0..-2].map { |var| finder_string(@app.fetch_goal(var)) } +
                                   [collection_finder_string(goal), new_object_string(goal)]))
  end

  def object_path(goal, str = "@")
    goal.underscore_tuple.join("_") + '_path(' + (goal.ivar_tuple[0..-2]  + ["#{str.to_s + goal.s})"]).join(', ')
  end

  #TODO: see if these can be removed.
#   def edit_path(goal, str = nil)
#     if str
#       "edit_" + object_path(goal, str)
#     else
#       "edit_" + object_path(goal)
#     end
#   end
  
#   def new_path(goal)
#     "new_" + (goal.rails_underscore_tuple[0..-2] + ["#{goal.s}"]).join("_") + "_path(" + goal.rails_ivar_tuple(-2).join(', ') + ')'
#   end
  
  def collection_path(goal)
    (goal.underscore_tuple[0..-2] + [goal.p]).join("_") +  "_path(" + goal.ivar_tuple[0..-2].join(', ') + ')'
  end

  # the core method that generates the whole app.  
  def generate
    gen_app
    gen_routes
    @app.goals.values.each_with_index do |goal, i|
      gen_goal(goal, i)
    end
    gen_misc
    self
  end

  # this does all of the generation for a given goal
  def gen_goal(goal, index)
    gen_migration(goal, index)
    gen_model(goal)
    gen_controller(goal)
    gen_view(goal)
    gen_tests(goal)
  end

  # this currently shells out to rails, and perhaps that's the right thing to do.  
  def gen_app
    unless File.exists?(app_dir) #TODO: figure out what to do when there is a directory there.
      original_dir = FileUtils.pwd
      Dir.mkdir(root_dir) unless (!root_dir or File.exists?(root_dir))
      FileUtils.cd(root_dir) if root_dir
      `#{rails_str}`
{'fraction_non_alphanumeric': 0.15301085883514315, 'fraction_numerical': 0.003948667324777887, 'mean_word_length': 3.1742892459826946, 'pattern_counts': {'":': 2, '<': 9, '<?xml version=': 0, '>': 7, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 4, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1053515', 'n_tokens_mistral': 3769, 'n_tokens_neox': 3607, 'n_words': 807}