Sinatra: params hash cannot be merged - ruby

I want to merge a hash with default parameters and the actual parameters given in a request. When I call this seemingly innocent script:
#!/usr/bin/env ruby
require 'sinatra'
get '/' do
defaults = { 'p1' => 'default1', 'p2' => 'default2' }
# params = request.params
params = defaults.merge(params)
params
end
with curl http://localhost:4567?p0=request then it crashes with
Listening on localhost:4567, CTRL+C to stop
2016-06-17 11:10:34 - TypeError - no implicit conversion of nil into Hash:
sinatrabug:8:in `merge'
sinatrabug:8:in `block in <main>'
When I access the Rack request.params directly it works. I looked into the Sinatra sources but I couldn't figure it out.
So I have a solution for my actual problem. But I don't know why it works.
My question is: Why can I assign param to a parameter, why is the class Hash but in defaults.merge params it throws an exception?
Any idea?

This is caused by the way Ruby handles local variables and setter methods (i.e. methods that end in =) with the same name. When Ruby reaches the line
params = defaults.merge(params)
it assumes you want to create a new local variable named params, rather than use the method. The initial value of this variable will be nil, and this is the value that the merge method sees.
If you want to refer to the method, you need to refer to it as self.params=. This is for any object that has such a method, not just Sinatra.
A better solution, to avoid this confusion altogether, might be to use a different name. Something like:
get '/' do
defaults = { 'p1' => 'default1', 'p2' => 'default2' }
normalized_params = defaults.merge(params)
normalized_params.inspect
end

Your code is throwing an error because params is nil when you make this call defaults.merge(params). I assume you are trying to merge defaults with request.params, which should contain the parameters from your GET.
Change this line
params = defaults.merge(params)
to this
params = defaults.merge(request.params)

I found this in rack gem
http://www.rubydoc.info/gems/rack/Rack/Request#params-instance_method
It seems you can retrieve GET and POST data by params method but you can't write in it. You have to use update_param and delete_param instead.

Related

Ruby how to use method parameter embedded in call to another method other than just puts

In Ruby I want to use an input parameter for REST verb (like 'Post') in a call to Net::HTTP new but I can't work out how to embed the parameter in the statement. I have the param called restVerb which prints fine in puts "Verb is #{restVerb}" but not in request = Net::HTTP::#{restVerb}.new(uri) - I get undefined method request' for Net::HTTP:Class (NoMethodError)` so it clearly doesn't recognise the parameter's value in the statement. I could use a case statement but wanted to make it more generic. What am I doing wrong?
I've tried the above syntax and a few others like request = Net::HTTP::restVerb.new(uri) or request = Net::HTTP::$restVerb.new(uri) I'm new to Ruby so be gentle with me please.
Use Object.const_get to convert a string to an actual constant:
klass = Object.const_get("Net::HTTP::#{restVerb}")
=> Net::HTTP::Post
klass.new(uri)
=> #<Net::HTTP::Post POST>

Ruby's Faraday - Send optional parameters in get method

I have an endpoint with multiple optional parameters.
def get_customers(params=nil)
if params.nil?
customer_url = "#{#url}/customers"
# call api
response = connection.get(customer_url)
else
# I do not know how to write this part
end
end
Could you please help me in order to write a call to and endpoint with optional parameters. The params argument is a hash (key, pair value). The query can have 8 parameters. I do not know how to concatenate the params to the url. I am stack in this section. I am a rookie at ruby and faraday.
Thanks in advance
You don't have to concatenate params with the url on your own. Faraday can accept a hash of params (or nil). You can pass them to the get method like so:
response = connection.get(customer_url, params)
Have into the "GET, HEAD, DELETE, TRACE" section of the documentation for more examples.
Side note: you don't event have to concatenate url and the path. You can pass them as separate arguments.

Ruby syntax error unexpected tLABEL

I have the following configuration for using redis as a rails cache, adapted closely from the rails doc:
Rails.application.configure do
...
config.cache_store = :redis_cache_store, url: "redis://example.com:6379/0"
...
end
Using rails 5.2.0.beta2 and ruby 2.3.1p112, my IDE is telling me the following things about the config.cache_store = line:
unused literal ignored
syntax error, unexpected tLABEL
If I enclose the hash in {}, the errors go away, i.e.:
config.cache_store = :redis_cache_store, { url: "redis://example.com:6379/0" }
Is the documentation in error, or is it a ruby version thing (e.g. maybe the doc is assuming ruby >= 2.4)?
In any case, I don't understand what the assignment is actually doing - how can two things be assigned to one?
When the config block hits cache_store and a symbol is given, it assumes it is a cache store class and does things like:
:redis_cache_store.to_s.classify.constantize
# => RedisCacheStore
which is presumably a class. You can infer that Rails does this by looking at the docs and seeing that you can also pass a class.
RedisCacheStore is defined inside the ActiveSupport::Cache namespace, which inherits from ActiveSupport::Cache::Store, which is defined in cache.rb. This store class only initializes with a single argument of options = nil:
def initialize(options = nil)
#options = options ? options.dup : {}
end
which is indicative of a requirement to pass a hash. For one reason or another, it doesn't initialize with options = {}, which would make your initial code correct. Hence, passing a hash like in your second example resolved the error.

Cannot deserialize object from a JSON string (but only to Hash)?

I wrote the dictation gem on my Mac, and deserialization works fine. When I installed it on another Mac it would not work because it "fails" to deserialize object, because it can only deserialize to a Hash.
Private Mac Ruby version: ruby-1.9.3-p0, json v1.8.0
Another Mac Ruby version: ruby-1.9.3-p448, json v1.8.0
I also tried different Ruby versions and Gem versions on both, but none of them works, only the initial one where I first wrote it.
When I try this code in the working environment:
require 'json'
class Word
attr_accessor :value, :translation
def initialize(value, translation)
#value = value
#translation = translation
end
def to_json(*args)
{
'json_class' => self.class.name,
'data' => [ #value, #translation ]
}.to_json(*args)
end
class << self
def json_create(object)
new(*object['data'])
end
end
end
str = '{"json_class":"Word","data":["Morgen","Tomorrow"]}'
p JSON.parse(str)
It prints a Word object, which is expected:
#<Word:0x007fcce22c9c58 #translation="Tomorrow", #value="Morgen">
With the other environment, it always prints a Hash:
{"json_class"=>"Word", "data"=>["Morgen", "Tomorrow"]}
I also tried to pass :object_class key, it throws another exception:
p JSON.parse(str, :object_class => Word)
# => ArgumentError: wrong number of arguments (0 for 2)
I could not figure out the require 'json' version during runtime using:
puts Gem.loaded_specs['json'].version
because Gem.loaded_specs.keys doesn't contain it.
Thanks for any hint.
Replied from the author of JSON lib - on newer version, due to security reason, to deserialize custom object, either you can:
JSON.parse(str, :create_additions => true)
or you can:
JSON.load(str)
So, I overlooked the JSON#load part in ruby-doc:
load(source, proc = nil, options = {})
Load a ruby data structure from a JSON source and return it. A source
can either be a string-like object, an IO-like object, or an object
responding to the read method. If proc was given, it will be called
with any nested Ruby object as an argument recursively in depth first
order. To modify the default options pass in the optional options
argument as well.
BEWARE: This method is meant to serialise data from trusted user
input, like from your own database server or clients under your
control, it could be dangerous to allow untrusted users to pass JSON
sources into it. The default options for the parser can be changed via
the ::load_default_options method.
This method is part of the implementation of the load/dump interface
of Marshal and YAML.
Deserializing directly into a rich object (especially if your JSON comes from an unknown source) can be a pretty serious attack vector (recent Rails vulnerabilities are related to that).
I would guess that this ability was disabled between Ruby versions, or, at least changed to a whitelist-based approach. I wasn't able to find any links to support this claim though, so I might be wrong.
Anyway, you might find it simpler and more compatible to initialize your class from the deserialized hash instead:
class Word
def self.from_json(json)
args = JSON.parse(json)["data"];
new(*args)
end
end
Here is another workaround, because my code is not used in web communication, vulnerability is not a problem here.
Before I was doing:
JSON.parse(str)
Now just need to add few lines:
obj = JSON.parse(str)
if obj.is_a?(Hash)
class_name = obj['json_class'].split('::').inject(Kernel) { |namespace, const_name| namespace.const_get(const_name) }
args = obj['data']
word = class_name.new(*args)
else
word = obj
end

How to get to_json work on BSON::Code object as well in mongo-ruby-driver

I am trying 'mongo-ruby-driver' for some project. It's working fine except when I call to_json on mongo object. It gives well formed json but it's not converting BSON::Code into readable value i resulting JSON.
Instead of showing code text, it show something like
#<BSON::Code:0x00000100af6fa8>
Did anyone tried it. Any help id highly appreciated.
UPDATE
here is some code snippet:
#records is variable that contains Array of MongoDB documents in hash.
#records.to_json
When I call to_json on it it gives everything as expected. except for the key that contain BSON::Code (means javascript code). for example consider following doc:
{
"_id" : "contains",
"value" : function( obj, target ) { return obj.indexOf(target) != -1; };
}
Querying same doc from ruby gives output like:
{
"_id"=>"contains",
"value"=><BSON::Code:2160165280 #data="function( obj, target ) { return obj.indexOf(target) != -1; };" #scope="{}">
}
and calling to_json on this gives following:
{"_id":"contains","value":"#<BSON::Code:0x00000100b54658>"}
this is what the problem is. Instead of getting actual code for 'value' key I am getting ruby object as string.
Calling code method on BSON::Code we can get it converted into code. But for that I need to loop mongo docs, check values for each key, calling code on it if it is an object of BSON::Code and then assigning it back to key. And at last we can call to_json on it. But I dont want this much overhead. I need to_json itself should take care of it.
The serializer that is iterating over the attributes of the objects in the array is probably calling the to_s method which would cause the output you are seeing.
You can either monkey patch the BSON::Code class to include a to_s method which calls inspect (the method that produces the output you want) or modify the serializer to detect when it encounters a BSON::Code instance and call inspect on it rather than to_s.
The code to mokey patch the BSON::Code class would look like this:
module BSON
class Code
def to_s
inspect
end
end
end
This would have the same behavior as inspect. If you just wanted the code you could monkey patch this in:
module BSON
class Code
def to_s
#code
end
end
end
I might make this the default behavior for to_s in the driver but for now just include that in your code and it should work like a champ.
Which Mongo object, do you mean the module? If you could, please post the code and what you are trying to do.
The only to_json I see in the driver is BSON::ObjectId#to_json which (from the docs) is described as a method that does the following:
Convert to MongoDB extended JSON format. Since JSON includes type information, but lacks an ObjectId type, this JSON format encodes the type using an $oid key.
I get the same results when I use it:
1.9.3-p0 :001 > require 'mongo'
=> true
1.9.3-p0 :002 > BSON::ObjectId
=> BSON::ObjectId
1.9.3-p0 :003 > BSON::ObjectId.new()
=> BSON::ObjectId('4f17350eadd361e91d000001')
1.9.3-p0 :004 > BSON::ObjectId.new().to_json
=> "{\"$oid\": \"4f173512add361e91d000002\"}"
BSON ( http://bsonspec.org/ ) is a binary representation of JSON ( http://www.json.org/ ) . JSON is not meant to be used to describe functions, as it is meant to be portable. Functions in this manner are not portable to other systems. So there is no way to serialize it. There is some hacks defined here that may get you what you need, but ultimately, BSON/JSON may be a weird tool for the job if your trying to serialize functions.

Resources