Search code examples
rubyregexruby-on-rails-3mongoidformtastic

Regex put in via formtastic gets altered (maybe by the controller) before it's put into Mongoid


I have a form where I put in hashes with regular expression values. My problem is that they gets messed up when travelling from my view, through my controller and into MongoDB with Mongoid. How do I preserve the regex'es?

Input examples:

{:regex1 => "^Something \(#\d*\)$"}
{:regex2 => "\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z"}

My formtastic view form looks like this:

= semantic_form_for resource, :html => {:class => "form-vertical"} do |r|
  = r.inputs do
    = r.input :value, :as => :text
  = r.actions do
    = r.action :submit

My controller create action takes in the params and handles it like this:

class EmailTypesController < InheritedResources::Base

  def create
    puts params[:email_type][:value]          # =>  {:regex1 => "^Something \(#\d*\)$"} and
                                              #     {:regex2 => "\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z"}
    puts params[:email_type][:value].inspect  # =>  "{:regex1 => \"^Something \\(#\\d*\\)$\"}" and
                                              #     "{:regex2 => \"\\A[\\w+\\-.]+@[a-z\\d\\-.]+\\.[a-z]+\\z\"}"

    params[:email_type][:value] = convert_to_hash(params[:email_type][:value])

    puts params[:email_type][:value]          # =>  {"regex1"=>"^Something (#d*)$"} and
                                              #     {"regex2"=>"A[w+-.]+@[a-zd-.]+.[a-z]+z"}
    create! do |success, failure|

      success.html {
        redirect_to resource
      }

      failure.html {
        render :action => :new
      }
    end    
  end

  def convert_to_hash(string)
    if string.match(/(.*?)=>(.*)\n*/)
      string = eval(string)
    else
      string = string_to_hash(string)
    end
  end

  def string_to_hash(string)
    values = string.split("\r\n")
    output = {}
    values.each do |v|
      val = v.split("=")
      output[val[0].to_sym] = val[1]
    end
    output
  end
end

Firing up the console and inspecting the values put in through Mongoid:

Loading development environment (Rails 3.2.12)
1.9.3p385 :001 > EmailType.all.each do |email_type|
1.9.3p385 :002 >     puts email_type.value
1.9.3p385 :003?>   end
{"regex1"=>"^Something (#d*)$"}
{"regex2"=>"A[w+-.]+@[a-zd-.]+.[a-z]+z"}
 => true 
1.9.3p385 :004 > 

Solution

  • The problem lies in ruby's evaluation of strings, which ignores useless escapes:

    puts "^Something \(#\d*\)$".inspect
    =>"^Something (#d*)$"
    

    That is to say the eval simply ignores the backslash. Note that typically in ruby regexes aren't created using strings but through their own regex literal, so that

    /^Something \(#\d*\)$/.inspect
    =>"/^Something \\(#\\d*\\)$/"
    

    Notice the double backslash instead of single. This means that eval has to receive two backslashes instead of one in the string, as it has to be eval'd into a single backslash character.

    A quick and easy way to do this is to simply run a sub ob the string before the convert_to_hash call:

    # A little confusing due to escapes, but single backslashes are replaced with double.
    # The second parameter is confusing, but it appears that String#sub requires a few
    # extra escapes due to backslashes also being used to backreference.
    # i.e. \n is replaced with the nth regex group, so to replace something with the string
    # "\n" yet another escape for backslash is required, so "\\n" is replaced with "\n".
    # Therefore the input of 8 blackslashes is eval'd to a string of 4 backslashes, which
    # sub interprets as 2 backslashes.
    params[:email_type][:value].gsub!('\\', '\\\\\\\\')
    

    this shouldn't be a problem unless you are using backslashes in the hash keys at some point, in which case more advanced matching would be needed to extract only the regex's and perform the substitution on them.