Builder's XmlMarkup object loses constants? - ruby

I try to upgrade a legacy application from Ruby 1.8.7 to 2.2.3. Afterwards the rendering of builder templates raises errors about unknown classes.
uninitialized constant Builder::XmlMarkup::BigDecimal (NameError)
It seem that within the Builder::XmlMarkup constants like classes disappear.
### example.xml.builder (template) ###
BigDecimal.new('23') # no error
class << xml
def some
data(BigDecimal.new('23')) # raises an error in 2.2.3
end
end
xml.test { xml.some }
### main script ###
require 'rubygems'
require 'builder'
require 'bigdecimal'
def eval_script(file)
xml = Builder::XmlMarkup.new
binding.eval(File.read(file), file)
xml.target!
end
template = File.join(File.dirname(__FILE__), 'example.xml.builder')
puts eval_script(template)
# Ruby 1.8.7 / builder 3.2.0 => <test><data>0.23E2</data></test>
# Ruby 2.2.3 / builder 3.2.2 => ./eval_script.rb:5:in `some': uninitialized constant Builder::XmlMarkup::BigDecimal (NameError)
I found no reason for the behavior. How can I fix the problem?
BTW: I have the same problem with the method lookup, e.g format('%d', 42) which returns the full XML document but doesn't call Kernel.format in Ruby 2.2.3.

I found a workaround overriding const_missing which has to be applied to every template file. It works so far for the legacy application.
### example.xml.builder (template) ###
class << xml
def self.const_missing(name)
super rescue ::Object.const_get(name)
end
def some
data(BigDecimal.new('23'))
end
end
xml.test { xml.some }
But every time the constant BigDecimal is used, it triggers const_missing and then raises a NameError and calls the Object method.

Related

Upgade to Ruby 3.1 breaks code when using CSV class from standard library

I'm upgrading a Project written for JRuby 1.7 (corresponding on the language level to Ruby 1.9) to JRuby 9.4 (corresponding to Ruby 3.1.0). In this code, we have
require 'csv'
....
CSV.parse(string, csv_options) { .... }
where string is of class String and csv_optionsis of class Hash. This statement produces, when run under the new Ruby version, the error
ArgumentError:
wrong number of arguments (given 2, expected 1)
I found in the Ruby docs the following difference in the definition of parse:
Old version:
def parse(str, options={}, &block)
New version
def parse(str, **options, &block)
I understand that in the new Ruby, I would have to invoke parse as
CSV.parse(string, **csv_options) {....}
However, I would like to keep the code compatible for both versions, at least for some transition period, but the old JRuby does not understand **variable (I would get a syntax error, unexpected tPOW).
Is there a way to write the invocation of CSV.parse in such a way, that it preserves the original semantics and can run under Ruby 1.9 and Ruby 3.1? Currently the best solution for this problem which I can think of, is to write something like turning the block argument into a proc and writing
if RUBY_VERSION < '2'
CSV.parse(string, csv_options, &myproc)
else
# Without the eval, the compiler would complain about
# the ** when compiled with Ruby 1.9
eval "CSV.parse(string, **csv_options, &myproc)"
end
which looks pretty awful.
Not sure exactly what you are passing as csv_options but all versions can handle this using an a combination of implicit Hash/kwargs. e.g.
CSV.parse(string, col_sep: '|', write_headers: false, quote_empty: true) { ... }
If this is not an option then you going to need to patch the CSV class to make it backwards compatible e.g.
csv_shim.rb
# Shim CSV::parse to mimic old method signature
# while supporting Ruby 3 kwargs argument passing
module CSVShim
def parse(string, options={}, &block)
super(string, **options, &block)
end
end
CSV.singleton_class.prepend(CSVShim)
Then you can modify as:
require 'csv'
require 'csv_shim.rb' if RUBY_VERSION > '2.6.0'
#...
CSV.parse(string, csv_options) { .... }

Can't call module method

I'm writing a Chef cookbook to deploy and application and create users. It doesn't have an API, and uses an odd hashing method, so I've written a short library module for it. I've included only the makeSalt() method below for the sake of brevity.
module Foo_packagist
module Password
def makeSalt(len=31)
require 'securerandom'
return Digest.hexencode(SecureRandom.random_bytes((len*6/8.0).ceil)).to_i(16).to_s(36)[0..len-1]
end
end
end
The trouble is that in every Chef run I get:
NoMethodError
-------------
undefined method `makeSalt' for Foo_packagist::Password:Module
and debugging in chef-shell I get:
chef (12.4.0)> puts ::Foo_packagist::Password.instance_methods()
makeSalt
encodePassword
chef (12.4.0)> puts ::Foo_packagist::Password.makeSalt()
NoMethodError: undefined method `makeSalt' for Foo_packagist::Password:Module
chef (12.4.0)> puts ::Foo_packagist::Password::makeSalt()
NoMethodError: undefined method `makeSalt' for Foo_packagist::Password:Module
What is the right way to call this method?
Change that to def self.makeSalt. That's the Ruby syntax for a module-level method.
Try this ->
module Foo_packagist
module Password
def self.makeSalt(len=31)
require 'securerandom'
return Digest.hexencode(SecureRandom.random_bytes((len*6/8.0).ceil)).to_i(16).to_s(36)[0..len-1]
end
end
end
Then to call it would be this->
Foo_packagist::Password.makeSalt()

'Error: Cannot open "/home/<...>/billy-bones/=" for reading' while using pry and DataMapper

So, I'm trying to build a quick console program for my development needs, akin to rails console (I'm using Sinatra + DataMapper + pry).
I run it and launch cat = Category.new(name: 'TestCat', type: :referential). It gives me the following error:
Error: Cannot open "/home/art-solopov/Projects/by-language/Ruby/billy-bones/=" for reading.
What could be the cause of the problem?
console:
#!/usr/bin/env ruby
$LOAD_PATH << 'lib'
require 'pry'
require 'config'
binding.pry
lib/config.rb:
# Configuration files and app-wide requires go here
require 'sinatra'
require 'data_mapper'
require 'model/bill'
require 'model/category'
configure :production do
DataMapper::Logger.new('db-log', :debug)
DataMapper.setup(:default,
'postgres://billy-bones:billy#localhost/billy-bones')
DataMapper.finalize
end
configure :development do
DataMapper::Logger.new($stderr, :debug)
DataMapper.setup(:default,
'postgres://billy-bones:billy#localhost/billy-bones-dev')
DataMapper.finalize
DataMapper.auto_upgrade!
end
configure :test do
require 'dm_migrations'
DataMapper::Logger.new($stderr, :debug)
DataMapper.setup(:default,
'postgres://billy-bones:billy#localhost/billy-bones-test')
DataMapper.finalize
DataMapper.auto_migrate!
end
lib/model/category.rb:
require 'data_mapper'
class Category
include DataMapper::Resource
property :id, Serial
property :name, String
property :type, Enum[:referential, :predefined, :computable]
has n, :bills
# has n, :tariffs TODO uncomment when tariff ready
def create_bill(params)
# A bill factory for current category type
case type
when :referential
ReferentialBill.new params
when :predefined
PredefinedBill.new params
when :computable
ComputableBill.new params
end
end
end
If I substitute pry with irb in the console script, it goes fine.
Thank you very much!
P. S.
Okay, yesterday I tried this script again, and it worked perfectly. I didn't change anything. I'm not sure whether I should remove the question now or not.
P. P. S.
Or actually not... Today I've encountered it again. Still completely oblivious to what could cause it.
** SOLVED **
DAMN YOU PRY!
Okay, so here's the difference.
When I tested it the second time, I actually entered a = Category.new(name: 'TestCat', type: :referential) and it worked. Looks like pry just thinks cat is a Unix command, not a valid variable name.
Not answer to the pry question I just generally hate case statements in ruby.
Why not change:
def create_bill(params)
# A bill factory for current category type
case type
when :referential
ReferentialBill.new params
when :predefined
PredefinedBill.new params
when :computable
ComputableBill.new params
end
end
to:
def create_bill(params)
# A bill factory for current category type
self.send("new_#{type}_bill",params)
end
def new_referential_bill(params)
ReferentialBill.new params
end
def new_predefined_bill(params)
PredefinedBill.new params
end
def new_computable_bill(params)
ComputableBill.new params
end
You could make this more dynamic but I think that would take away from readability in this case but if you'd like in rails this should do the trick
def create_bill(params)
if [:referential, :predefined, :computable].include?(type)
"#{type}_bill".classify.constantize.new(params)
else
#Some Kind of Handling for non Defined Bill Types
end
end
Or this will work inside or outside rails
def create_bill(params)
if [:referential, :predefined, :computable].include?(type)
Object.const_get("#{type.to_s.capitalize}Bill").new(params)
else
#Some Kind of Handling for non Defined Bill Types
end
end

Why do I get the error uninitialized constant Stuff::HTTParty?

I have the HTTParty gem on my system and I can use it from within rails.
Now I want to use it standalone.
I am trying:
class Stuff
include HTTParty
def self.y
HTTParty.get('http://www.google.com')
end
end
Stuff.y
but I get
$ ruby test_httparty.rb
test_httparty.rb:2:in `<class:Stuff>': uninitialized constant Stuff::HTTParty (NameError)
from test_httparty.rb:1:in `<main>'
07:46:52 durrantm Castle2012 /home/durrantm/Dropnot/_/rails_apps/linker 73845718_get_method
$
You have to require 'httparty':
require 'httparty'
class Stuff
include HTTParty
# ...
end
Its all because of the include which exists with in the class
If you include a class with a module, that means you're "bringing in" the module's methods as instance methods.
If you need more clarity on include and require
I request you to refer to this wonderful SO Posting
What is the difference between include and require in Ruby?
Here is an example which I have taken from the same posting
module A
def say
puts "this is module A"
end
end
class B
include A
end
class C
extend A
end
B.say => undefined method 'say' for B:Class
B.new.say => this is module A
C.say => this is module A
C.new.say => undefined method 'say' for C:Class

cattr_accessor outside of rails

I'm trying to use the google_search ruby library (code follows) but it complains that 'cattr_accessor is an undefined method' - any ideas why this might be or how I could fix it?
require 'rubygems'
require 'google_search'
GoogleSearch.web :q => "pink floyd"
cattr_accessor seems to be a Rails extension that acts like attr_accessor, but is accessible on both the class and its instances.
If you want to copy the source of the cattr_accessor method, check out this documentation:
# File vendor/rails/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb, line 46
def cattr_accessor(*syms)
cattr_reader(*syms)
cattr_writer(*syms)
end
# File vendor/rails/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb, line 4
def cattr_reader(*syms)
syms.flatten.each do |sym|
next if sym.is_a?(Hash)
class_eval("unless defined? ##\#{sym}\n##\#{sym} = nil\nend\n\ndef self.\#{sym}\n##\#{sym}\nend\n\ndef \#{sym}\n##\#{sym}\nend\n", __FILE__, __LINE__)
end
end
# File vendor/rails/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb, line 24
def cattr_writer(*syms)
options = syms.extract_options!
syms.flatten.each do |sym|
class_eval("unless defined? ##\#{sym}\n##\#{sym} = nil\nend\n\ndef self.\#{sym}=(obj)\n##\#{sym} = obj\nend\n\n\#{\"\ndef \#{sym}=(obj)\n##\#{sym} = obj\nend\n\" unless options[:instance_writer] == false }\n", __FILE__, __LINE__)
end
end
You can get this functionality by including the Ruby Facets gem. Reference the source here:
https://github.com/rubyworks/facets/blob/master/lib/core/facets/cattr.rb
You generally don't need to require all code from the gem. You can selectively require what you want. There are quite a few useful extensions in the gem though.

Resources