Making rails do variable replacement from a db string - ruby

I have a string in a db that contains a local variable reference and I want Ruby to parse and replace it.
For example, the string in the db is "Hello #{classname.name}" and it is stored in classname.description
and my code reads:
<%=h #classname.description %>
Put that just prints the exact value from the db:
Hello #{name}
and not the (assume classname.name is Bob):
Hello Bob
How do I get Ruby to parse the string from the db?

You can use eval() to do this. For example:
>> a = {:name => 'bob'}
=> {:name=>"bob"}
>> eval('"Hello #{a[:name]}"')
=> "Hello bob"
However, what you are doing can be very dangerous and is almost never necessary. I can not be sure that this is or isn't the right way to do things for your project, but in general storing code to be executed in your database is bad practice.

Why don't you use a safe template engine like Liquid, to get around the eval problem?
template_string = "Hello {{name}}" #actually get from database
template = Liquid::Template.parse(template_string) #compile template
name = 'Bob'
text = template.render( 'name' => name )

Related

postgres avoiding extra quotes inside a string

I have following select query which I will be passing to the database to get results back,
sql = "select * from movies where title = #{movie_title};"
movie_title contains a value that can sometimes contain single quotes and other chars that need escaping. I have come across dollar quoted string which is working well when used inside a INSERT statement but SELECT is not behaving the same, if I use $$#{movie_title}$$ like this it just doesn't get converted to a value inside movie_title. Is there any solution for this?
I am using postgres 9.5.0 and I am programming using ruby.
Bad idea. Don't do that, as you are making your code vulnerable to SQL injection attacks, and also making your life harder. Read more about prepared SQL statements, SQL injection etc.
In short, unless you are using some ORM, you should do something like:
#!/usr/bin/ruby
require 'pg'
if ARGV.length != 1 then
puts "Usage: prepared_statement.rb rowId"
exit
end
rowId = ARGV[0]
begin
con = PG.connect :dbname => 'testdb', :user => 'janbodnar'
con.prepare 'stm1', "SELECT * FROM Cars WHERE Id=$1"
rs = con.exec_prepared 'stm1', [rowId]
puts rs.values
rescue PG::Error => e
puts e.message
ensure
rs.clear if rs
con.close if con
end
(an example taken from http://zetcode.com/db/postgresqlruby/)
Edit: You don't need to use prepared statements, you can also use your DB lib's methods which provide proper parameter binding:
require 'pg'
conn = PG::Connection.open(:dbname => 'test')
res = conn.exec_params('SELECT $1 AS a, $2 AS b, $3 AS c', [1, 2, nil])
Take a look at docs for PG#exec_params

How do I pass a hash from commandline?

I have a ruby script that has a hash.
Example:
animal_sound = { 'dog' => 'bark', 'cat' => 'meow' }
I want to add 'snake' => 'hiss'
Example:
myscript.rb --addsound "'snake' => 'hiss'"
Then in my script have it add it to animal_sound.
Example:
animal_sound.merge! 'snake' => 'hiss'
=> {"dog"=>"bark", "cat"=>"meow", "snake"=>"hiss"}
Is there a way to do this?
Here is the whole script:
#!/usr/bin/env ruby
require 'rubygems'
require 'micro-optparse'
options = Parser.new do |p|
p.option :addsound, "add sound"
end.process!
animal_sound = { 'dog' => 'bark', 'cat' => 'meow' }
if options[:add_sound]
newsound = options[:add_sound]
animal_sound.merge! newsound
end
puts animal_sound
When I run my script I get:
$ bin/myscript.rb --addsound "'snake' => 'hiss'"
bin/myscript.rb:14:in `merge!': can't convert true into Hash (TypeError)
from bin/myscript.rb:14:in `<main>'
SOLVED:
Using PSkocik's solution I got the script to work using animal, sound = options[:addsound].split(' => '); animal_sound[animal] = sound
I also used Simone Carletti's idea to simplify the CLI command. FYI it also works if I want to pass in hash format, like myscript.rb --addsound "'snake' => 'hiss'". Of course the split has to be changed back to split(' => '). I like the simpler CLI using the :.
Example:
myscript.rb --addsound snake:hiss
Final Code:
#!/usr/bin/env ruby
require 'rubygems'
require 'micro-optparse'
options = Parser.new do |p|
p.option :addsound, "add sound", default: ""
end.process!
animal_sound = { 'dog' => 'bark', 'cat' => 'meow' }
if options[:addsound]
animal, sound = options[:addsound].split(':')
animal_sound[animal] = sound
end
puts animal_sound
Command line:
$ bin/myscript.rb --addsound snake:hiss
{"dog"=>"bark", "cat"=>"meow", "snake"=>"hiss"}
I never could get the merge to work.
Each post was helpful. Thanks.
It's a good idea to keep the CLI interface detached from the underlying implementation. In fact, you may decide to switch the script in the future from Ruby to another language, and you don't really want to change the way the code is invoked.
My suggestion is to pass a serialized value, for example
myscript.rb --addsound snake:hiss
In the code, simply decompose the content and merge it.
if options[:add_sound]
animal, sound = options[:add_sound].split(":")
animal_sound.merge!(animal => sound)
end
p.option :addsound, "add sound"
^ this makes it a flag (true or false)
What you want is make it into a switch whose value is the next argument:
p.option :addsound, "add sound", default: ""
^ this makes it a switch, the string value will be assigned to options[:addsound]
newsound = options[:addsound]
^ Here you need to drop the underscore and parse the string into a hash.
Eval is evil.
For example, you could split it on ' => ' and forget about quoting:
newsound = [ options[:addsound].split(' => ') ].to_h #and then merge it
(Passing the argument like so --addsound snake:hiss and then splitting on ':' instead of ' => ' is another good option.)
^splitting on ' => ' should yield a two-member array. Here I put it into another array (arrays of two-member arrays are convertible to hashes) to make it convertible into a hash.
Or you do completely without merging and constructing another hash:
animal, sound = options[:addsound].split(' => ')
animal_sound[animal] = sound
In regards to your error
Notice the line if options[:add_sound]. That basically evaluates to if true. You are getting your error because you are setting newsound to true, and trying to merge a Boolean into a hash. To my knowledge, the .merge only works like so: hash1.merge(hash2).
Passing command line argument
Rather than passing the argument "'snake' => 'hiss'", I suggest making this a comma-delineated list, like so: "snake,hiss". From there, in your if options[:add_sound] block, you can split the string into an array, using a comma as a splitter. Finally, rather than using .merge, you can add your key:value as you normally would for any hash in Ruby. animal_sound[arr[0]] = arr[1].
Mind you, this method will work best with a single key:value pair. I am sure you can submit multiple pairs, but you would need to (by this method) split into more arrays by an additional character(like / maybe).

Ruby parsing XML: no implicit conversion of String into Integer

I'm working on a Sinatra application that pulls in a list of dates through an XML file and then creates an hash of all the dates.
I'm running into a strange issue that's happening when I'm pulling the id and assigning it to a variable.
The error I'm getting is:
no implicit conversion of String into Integer and it's being thrown on the event_date_id = event_date["date_id"] line. I have almost identical method in my code and it's working just fine. When I puts event_date['date_id'] it gives me the correct numerical date_id.
Just in case it helps, the class of the event_date['date_id'] is REXMLUtiliyNodeString, same as the id field in the other method. If I try to event_date['date_id'].to_i it gives breaks at that point.
def get_dates(event_id)
url = "some_url_to_some_xml"
puts '==================='
puts "Pulling in #{url}"
puts '==================='
date_xml = Crack::XML.parse(open(url))
dates = {}
date_xml['document']['date'].each do | event_date |
event_date_id = event_date['date_id']
single_date = {
'date_id' => event_date_id,
'date_start' => event_date['datestart'],
'date_end' => event_date['dateend'],
'date_live' => event_date['live'],
'time_start' => event_date['timestart'],
'time_end' => event_date['timestart'],
'date_available' => event_date['date_available']
}
dates.merge!( event_date_id => single_date )
end
return dates
end
Here is the xml format:
<document>
<date>
<date_id>881908</date_id>
<live>y</live>
<datestart>2017-08-14</datestart>
<dateend>2017-08-15</dateend>
<timestart>13:00</timestart>
<timeend>0:00</timeend>
<date_available>10000</date_available>
</date>
<document>
I have a feeling this is something really simple but I'm wracking my brains trying to figure it out. If anyone could shed some light on this, I'd definitely appreciate it.
Edit 1: When I run the code in irb, it does indeed work without error. Somewhere, somehow it seems Sinatra, Rack or Shotgun are getting in the way.
I have found the cause of my issue. It only occurs when I'm parsing an xml file with one entry for date/event whatever.
This question explains the exact issue I was having and the answer includes a work around that worked for me.
if(!date_xml['document']['date'].is_a?(Array))
date_xml['document']['date'] = [ date_xml['document']['date'] ]
end
This message occurs when you try to use a string index to look up a value in an array.
2.0.0p353 :001 > results = [""]
=> [""]
2.0.0p353 :002 > results["x"]
TypeError: no implicit conversion of String into Integer
from (irb):2:in `[]'
from (irb):2
from /home/jeff/.rvm/rubies/ruby-2.0.0-p353/bin/irb:12:in `<main>'
Since arrays can only be accessed by integer indexes, Ruby attempts to transform your key name into an integer, and fails because it doesn't know what number should be used to represent arbitrary string data. Hence the message "no implicit conversion of String into Integer".
If you're experiencing this, the answer is to fix your code so that it doesn't try to access an array like a hash. If you're importing from XML or JSON data on the assumption that the key desired is always there and will always be imported, but you're still getting this, your assumption is wrong; the data is not formatted as expected. Either fix the data or fix the code to handle the differing format.
I randomly stumbled upon what may be a better answer to this question, although I am very inexperienced so I think it needs to be verified.
I had an identical issue and saw that much of the trouble seems to be because I was returning a somewhat confusing array of one object.
Once I added ".first" to my query, I was able to retrieve my intended attribute.
response = File.open('ncaa_bb_schedule.xml')
doc = Nokogiri::XML(response)
doc.remove_namespaces!
doc.xpath('//game').each do |game|
h = game.xpath('home').first
p h['id']
end
But my original query, shown here
response = File.open('ncaa_bb_schedule.xml')
doc = Nokogiri::XML(response)
doc.remove_namespaces!
doc.xpath('//game').each do |game|
h = game.xpath('home')
p h['id']
end
end
was giving me the same error: "TypeError: no implicit conversion of String into Integer." Hope that helps somebody as it is much shorter than the aforementioned workaround.

Tabbed text file to MultiDimensional hash using Ruby?

I'm having a bit of trouble figuring about how I'd go about this for a part of my project. Basically I need to take a normal tabbed text file and convert it into a Multi Dimensional hash in Ruby so I can cycle through and detect which parts have children. An example of the file:
hello
world
how
are
you
today
Would become:
{'hello' => ['world', 'how'], 'are' => {'you' => ['today']}}
Since your input format is up to you, I really don't understand why you're not using YAML:
puts { 'hello' => ['world', 'how'], 'are' => { 'you' => ['today'] } }.to_yaml
yields:
---
hello:
- world
- how
are:
you:
- today
Calling YAML.load with that string, of course, returns the original data structure. Contrary to what you believe, YAML does not require a "key value syntax".

Ruby unable to use require

This is a newbie question as I am attempting to learn Ruby by myself, so apologies if it sounds like a silly question!
I am reading through the examples of why's (poignant) guide to ruby and am in chapter 4. I typed the code_words Hash into a file called wordlist.rb
I opened another file and typed the first line as require 'wordlist.rb' and the rest of the code as below
#Get evil idea and swap in code
print "Enter your ideas "
idea = gets
code_words.each do |real, code|
idea.gsub!(real, code)
end
#Save the gibberish to a new file
print "File encoded, please enter a name to save the file"
ideas_name = gets.strip
File::open( 'idea-' + ideas_name + '.txt', 'w' ) do |f|
f << idea
end
When I execute this code, it fails with the following error message:
C:/MyCode/MyRubyCode/filecoder.rb:5: undefined local variable or method `code_words' for main:Object (NameError)
I use Windows XP and Ruby version ruby 1.8.6
I know I should be setting something like a ClassPath, but not sure where/how to do so!
Many thanks in advance!
While the top-level of all files are executed in the same context, each file has its own script context for local variables. In other words, each file has its own set of local variables that can be accessed throughout that file, but not in other files.
On the other hand, constants (CodeWords), globals ($code_words) and methods (def code_words) would be accessible across files.
Some solutions:
CodeWords = {:real => "code"}
$code_words = {:real => "code"}
def code_words
{:real => "code"}
end
An OO solution that is definitely too complex for this case:
# first file
class CodeWords
DEFAULT = {:real => "code"}
attr_reader :words
def initialize(words = nil)
#words = words || DEFAULT
end
end
# second file
print "Enter your ideas "
idea = gets
code_words = CodeWords.new
code_words.words.each do |real, code|
idea.gsub!(real, code)
end
#Save the gibberish to a new file
print "File encoded, please enter a name to save the file"
ideas_name = gets.strip
File::open( 'idea-' + ideas_name + '.txt', 'w' ) do |f|
f << idea
end
I think the problem might be that the require executes the code in another context, so the runtime variable is no longer available after the require.
What you could try is making it a constant:
CodeWords = { :real => 'code' }
That will be available everywhere.
Here is some background on variable scopes etc.
I was just looking at the same example and was having the same problem.
What I did was change the variable name in both files from code_words to $code_words .
This would make it a global variable and thus accesible by both files right?
My question is: wouldn't this be a simpler solution than making it a constant and having to write CodeWords = { :real => 'code' } or is there a reason not to do it ?
A simpler way would be to use the Marshal.dump feature to save the code words.
# Save to File
code_words = {
'starmonkeys' => 'Phil and Pete, those prickly chancellors of the New Reich',
'catapult' => 'chucky go-go', 'firebomb' => 'Heat-Assisted Living',
'Nigeria' => "Ny and Jerry's Dry Cleaning (with Donuts)",
'Put the kabosh on' => 'Put the cable box on'
}
# Serialize
f = File.open('codewords','w')
Marshal.dump(code_words, f)
f.close
Now at the beginning of your file you would put this:
# Load the Serialized Data
code_words = Marshal.load(File.open('codewords','r'))
Here's the easy way to make sure you can always include a file that's in the same directory as your app, put this before the require statement
$:.unshift File.dirname(__FILE__)
$: is the global variable representing the "CLASSPATH"

Resources