ruby clone an object - ruby

I need to clone an existing object and change that cloned object.
The problem is that my changes change original object.
Here's the code:
require "httparty"
class Http
attr_accessor :options
attr_accessor :rescue_response
include HTTParty
def initialize(options)
options[:path] = '/' if options[:path].nil? == true
options[:verify] = false
self.options = options
self.rescue_response = {
:code => 500
}
end
def get
self.class.get(self.options[:path], self.options)
end
def post
self.class.post(self.options[:path], self.options)
end
def put
self.class.put(self.options[:path], self.options)
end
def delete
self.class.put(self.options[:path], self.options)
end
end
Scenario:
test = Http.new({})
test2 = test
test2.options[:path] = "www"
p test2
p test
Output:
#<Http:0x00007fbc958c5bc8 #options={:path=>"www", :verify=>false}, #rescue_response={:code=>500}>
#<Http:0x00007fbc958c5bc8 #options={:path=>"www", :verify=>false}, #rescue_response={:code=>500}>
Is there a way to fix this?

You don't even need to clone here, you just need to make a new instance.
Right here:
test = Http.new({})
test2 = test
you don't have two instances of Http, you have one. You just have two variables pointing to the same instance.
You could instead change it to this, and you wouldn't have the problem.
test = Http.new({})
test2 = Http.new({})
If, however, you used a shared options argument, that's where you'd encounter an issue:
options = { path: nil }
test = Http.new(options)
# options has been mutated, which may be undesirable
puts options[:path] # => "/"
To avoid this "side effect", you could change the initialize method to use a clone of the options:
def initialize(options)
options = options.clone
# ... do other stuff
end
You could also make use of the splat operator, which is a little more cryptic but possibly more idiomatic:
def initialize(**options)
# do stuff with options, no need to clone
end
You would then call the constructor like so:
options = { path: nil }
test = Http.new(**options)
puts test.options[:path] # => "/"
# the original hasn't been mutated
puts options[:path] # => nil

You want .clone or perhaps .dup
test2 = test.clone
But depending on your purposes, but in this case, you probably want .clone
see What's the difference between Ruby's dup and clone methods?
The main difference is that .clone also copies the objects singleton methods and frozen state.
On a side note, you can also change
options[:path] = '/' if options[:path].nil? # you don't need "== true"

Related

Metrics/AbcSize Too High: How do I decrease the ABC in this method?

I have recently started using Rubocop to "standardise" my code, and it has helped me optimise a lot of my code, as well as help me learn a lot of Ruby "tricks". I understand that I should use my own judgement and disable Cops where necessary, but I have found myself quite stuck with the below code:
def index
if params[:filters].present?
if params[:filters][:deleted].blank? || params[:filters][:deleted] == "false"
# if owned is true, then we don't need to filter by admin
params[:filters][:admin] = nil if params[:filters][:admin].present? && params[:filters][:owned] == "true"
# if admin is true, then must not filter by owned if false
params[:filters][:owned] = nil if params[:filters][:owned].present? && params[:filters][:admin] == "false"
companies_list =
case params[:filters][:admin]&.to_b
when true
current_user.admin_companies
when false
current_user.non_admin_companies
end
if params[:filters][:owned].present?
companies_list ||= current_user.companies
if params[:filters][:owned].to_b
companies_list = companies_list.where(owner: current_user)
else
companies_list = companies_list.where.not(owner: current_user)
end
end
else
# Filters for deleted companies
companies_list = {}
end
end
companies_list ||= current_user.companies
response = { data: companies_list.alphabetical.as_json(current_user: current_user) }
json_response(response)
end
Among others, the error that I'm getting is the following:
C: Metrics/AbcSize: Assignment Branch Condition size for index is too high. [<13, 57, 16> 60.61/15]
I understand the maths behind it, but I don't know how to simplify this code to achieve the same result.
Could someone please give me some guidance on this?
Thanks in advance.
Well first and foremost, is this code fully tested, including all the myriad conditions? It's so complex that refactoring will surely be disastrous unless the test suite is rigorous. So, write a comprehensive test suite if you don't already have one. If there's already a test suite, make sure it tests all the conditions.
Second, apply the "fat model skinny controller" paradigm. So move all the complexity into a model, let's call it CompanyFilter
def index
companies_list = CompanyFilter.new(current_user, params).list
response = { data: companies_list.alphabetical.as_json(current_user: current_user) }
json_response(response)
end
and move all those if/then/else statements into the CompanyFilter#list method
tests still pass? great, you'll still get the Rubocop warnings, but related to the CompanyFilter class.
Now you need to untangle all the conditions. It's a bit hard for me to understand what's going on, but it looks as if it should be reducible to a single case statement, with 5 possible outcomes. So the CompanyFilter class might look something like this:
class CompanyFilter
attr_accessors :current_user, :params
def initialize(current_user, params)
#current_user = current_user
#params = params
end
def list
case
when no_filter_specified
{}
when user_is_admin
#current_user.admin_companies
when user_is_owned
# etc
when # other condition
# etc
end
end
private
def no_filter_specified
#params[:filter].blank?
end
def user_is_admin
# returns boolean based on params hash
end
def user_is_owned
# returns boolean based on params hash
end
end
tests still passing? perfect! [Edit] Now you can move most of your controller tests into a model test for the CompanyFilter class.
Finally I would define all the different companies_list queries as scopes on the Company model, e.g.
class Company < ApplicationRecord
# some examples, I don't know what's appropriate in this app
scope :for_user, ->(user){ where("...") }
scope :administered_by, ->(user){ where("...") }
end
When composing database scopes ActiveRecord::SpawnMethods#merge is your friend.
Post.where(title: 'How to use .merge')
.merge(Post.where(published: true))
While it doesn't look like much it lets you programatically compose scopes without overelying on mutating assignment and if/else trees. You can for example compose an array of conditions and merge them together into a single ActiveRecord::Relation object with Array#reduce:
[Post.where(title: 'foo'), Post.where(author: 'bar')].reduce(&:merge)
# => SELECT "posts".* FROM "posts" WHERE "posts"."title" = $1 AND "posts"."author" = $2 LIMIT $3
So lets combine that with a skinny controllers approach where you handle filtering in a seperate object:
class ApplicationFilter
include ActiveModel::Attributes
include ActiveModel::AttributeAssignment
attr_accessor :user
def initialize(**attributes)
super()
assign_attributes(attributes)
end
# A convenience method to both instanciate and apply the filters
def self.call(user, params, scope: model_class.all)
return scope unless params[:filters].present?
scope.merge(
new(
permit_params(params).merge(user: user)
).to_scope
)
end
def to_scope
filters.map { |filter| apply_filter(filter) }
.compact
.select {|f| f.respond_to?(:merge) }
.reduce(&:merge)
end
private
# calls a filter_by_foo method if present or
# defaults to where(key => value)
def apply_filter(attribute)
if respond_to? "filter_by_#{attribute}"
send("filter_by_#{attribute}")
else
self.class.model_class.where(
attribute => send(attribute)
)
end
end
# Convention over Configuration is sexy.
def self.model_class
name.chomp("Filter").constantize
end
# filters the incoming params hash based on the attributes of this filter class
def self.permit_params
params.permit(filters).reject{ |k,v| v.blank? }
end
# provided for modularity
def self.filters
attribute_names
end
end
This uses some of the goodness provided by Rails to setup objects with attributes that will dynamically handle filtering attributes. It looks at the list of attributes you have declared and then slices those off the params and applies a method for that filter if present.
We can then write a concrete implementation:
class CompanyFilter < ApplicationFilter
attribute :admin, :boolean, default: false
attribute :owned, :boolean
private
def filter_by_admin
if admin
user.admin_companies
else
user.non_admin_companies
end
end
# this should be refactored to use an assocation on User
def filter_by_owned
case owned
when nil
nil
when true
Company.where(owner: user)
when false
Company.where.not(owner: user)
end
end
end
And you can call it with:
# scope is optional
#companies = CompanyFilter.call(current_user, params), scope: current_user.companies)

"undefined method 'becomes' for nil:NilClass" where it shouldn't

I was testing a snippet today
unless resource.nil?
resource = resource.becomes(Accounts::Admin)
end
this raises an error
undefined method `becomes' for nil:NilClass
if I do this
unless resource.nil?
a = resource.becomes(Accounts::Admin)
resource = a
end
Everything goes right.
What is the difference if the right part of the =operator is executed first ?
EDIT :
Something nasty is happening, the last line under if false is being executed, but "ALOHA" is never printed.
<%
puts "AAAA #{resource.inspect}"
if false
puts "ALOHA"
# this line is being executed !
# if I comment it out the BBBB output is correct
resource = nil
end
puts "BBBB #{resource.inspect}"
%>
it prints
AAAA User id: nil, nome: nil, endereco_id: nil, created_at: nil,
updated_at: nil, email: ""
BBBB nil
but if I do this
<%
res = resource
puts "AAAA #{res.inspect}"
if false
puts "ALOHA"
res = nil
end
puts "BBBB #{res.inspect}"
%>
it print correctly
AAAA User id: nil, nome: nil, endereco_id: nil, created_at: nil,
updated_at: nil, email: ""
BBBB User id: nil, nome: nil,
endereco_id: nil, created_at: nil, updated_at: nil, email: ""
I already have tried to restart the server. This snippet is at devise/registrations/new.html.erb. The resourcevariable is supposed to be an instance of User, created by devise's RegistrationController
I have checked the text for hidden characters, the snippets I pasted here are the whole text for the file being tested.
This is the content of the controller at ~/.rvm/gems/ruby-2.3.3#mygem/gems/devise-4.2.0/app/controllers/devise/registrations_controller.rb
class Devise::RegistrationsController < DeviseController
prepend_before_action :require_no_authentication, only: [:new, :create, :cancel]
prepend_before_action :authenticate_scope!, only: [:edit, :update, :destroy]
prepend_before_action :set_minimum_password_length, only: [:new, :edit]
# GET /resource/sign_up
def new
build_resource({})
yield resource if block_given?
respond_with resource
end
# POST /resource
def create
build_resource(sign_up_params)
resource.save
yield resource if block_given?
if resource.persisted?
if resource.active_for_authentication?
set_flash_message! :notice, :signed_up
sign_up(resource_name, resource)
respond_with resource, location: after_sign_up_path_for(resource)
else
set_flash_message! :notice, :"signed_up_but_#{resource.inactive_message}"
expire_data_after_sign_in!
respond_with resource, location: after_inactive_sign_up_path_for(resource)
end
else
clean_up_passwords resource
set_minimum_password_length
respond_with resource
end
end
# GET /resource/edit
def edit
render :edit
end
# PUT /resource
# We need to use a copy of the resource because we don't want to change
# the current user in place.
def update
self.resource = resource_class.to_adapter.get!(send(:"current_#{resource_name}").to_key)
prev_unconfirmed_email = resource.unconfirmed_email if resource.respond_to?(:unconfirmed_email)
resource_updated = update_resource(resource, account_update_params)
yield resource if block_given?
if resource_updated
if is_flashing_format?
flash_key = update_needs_confirmation?(resource, prev_unconfirmed_email) ?
:update_needs_confirmation : :updated
set_flash_message :notice, flash_key
end
bypass_sign_in resource, scope: resource_name
respond_with resource, location: after_update_path_for(resource)
else
clean_up_passwords resource
respond_with resource
end
end
# DELETE /resource
def destroy
resource.destroy
Devise.sign_out_all_scopes ? sign_out : sign_out(resource_name)
set_flash_message! :notice, :destroyed
yield resource if block_given?
respond_with_navigational(resource){ redirect_to after_sign_out_path_for(resource_name) }
end
# GET /resource/cancel
# Forces the session data which is usually expired after sign
# in to be expired now. This is useful if the user wants to
# cancel oauth signing in/up in the middle of the process,
# removing all OAuth session data.
def cancel
expire_data_after_sign_in!
redirect_to new_registration_path(resource_name)
end
protected
def update_needs_confirmation?(resource, previous)
resource.respond_to?(:pending_reconfirmation?) &&
resource.pending_reconfirmation? &&
previous != resource.unconfirmed_email
end
# By default we want to require a password checks on update.
# You can overwrite this method in your own RegistrationsController.
def update_resource(resource, params)
resource.update_with_password(params)
end
# Build a devise resource passing in the session. Useful to move
# temporary session data to the newly created user.
def build_resource(hash=nil)
self.resource = resource_class.new_with_session(hash || {}, session)
end
# Signs in a user on sign up. You can overwrite this method in your own
# RegistrationsController.
def sign_up(resource_name, resource)
sign_in(resource_name, resource)
end
# The path used after sign up. You need to overwrite this method
# in your own RegistrationsController.
def after_sign_up_path_for(resource)
after_sign_in_path_for(resource)
end
# The path used after sign up for inactive accounts. You need to overwrite
# this method in your own RegistrationsController.
def after_inactive_sign_up_path_for(resource)
scope = Devise::Mapping.find_scope!(resource)
router_name = Devise.mappings[scope].router_name
context = router_name ? send(router_name) : self
context.respond_to?(:root_path) ? context.root_path : "/"
end
# The default url to be used after updating a resource. You need to overwrite
# this method in your own RegistrationsController.
def after_update_path_for(resource)
signed_in_root_path(resource)
end
# Authenticates the current scope and gets the current resource from the session.
def authenticate_scope!
send(:"authenticate_#{resource_name}!", force: true)
self.resource = send(:"current_#{resource_name}")
end
def sign_up_params
devise_parameter_sanitizer.sanitize(:sign_up)
end
def account_update_params
devise_parameter_sanitizer.sanitize(:account_update)
end
def translation_scope
'devise.registrations'
end
end
ruby-2.3.3
rails (4.2.7.1)
devise (4.2.0)
Look at this ruby snippet:
if true
foo = "hello"
end
puts foo
#=> hello
And this one:
if false
foo = "hello"
end
puts foo
#=> nil
In many languages, if-statements have their own scope, but in ruby they share the scope of the surrounding function. This means variables declared inside if-statements are accessible outside of if-statements.
The issue here is that variables are declared at compile-time, before ruby knows whether the if-statement is true or false. So in ruby, all local variables are declared and initialized as nil, even if they're in a conditional statement.
This code:
unless resource.nil?
resource = resource.becomes(Accounts::Admin)
end
Causes problems because of another rule in ruby which gives local variables priority over methods. So when you say resource = resource you're actually calling the method resource and saving its value to the local variable resource, which then overshadows the method by the same name.
Ultimately you're getting the error:
undefined method `becomes' for nil:NilClass
because at compile-time, the local variable resource is being created, overshadowing the method. Then, at run-time, the condition is being executed because resource is not yet nil. But at the line where you create the local variable, it instantly comes into scope, making resource = nil and causing this the error.
The error can be reproduced in this generalized example:
def blah
"foo"
end
unless blah.nil?
blah = blah.size
end
puts blah
And the fix for it is to specify the method itself:
def blah
"foo"
end
def blah= value
#do nothing
end
unless blah.nil?
self.blah = blah.size
end
puts blah
That being said, I'm not sure whether devise actually implements resource=(). If it doesn't, then your best solution is the one you already came up with- use a local variable:
unless resource.nil?
res = resource.becomes(Accounts::Admin)
end
puts res
After doing some research, I found that local variables in ruby are defined from top to bottom and left to right, based on position in the file, not their position in program flow. For example:
if x="foo"
puts x
end
#=> "foo"
puts y if y="foo"
#NameError: undefined variable or method 'y'
This is part of the ruby spec, according to matz.
If using ruby Ruby 2.3.0 and above use Safe Navigation Operator (&.)
resource = resource&.becomes(Accounts::Admin)
Try this in IRb, it should be fine:
a = 'hello'
unless a.nil?
a = a.upcase
end
a
# => "HELLO"
The second example is equivalent to the first, the expression on the right side of the = is assigned to the var on the left.
Maybe some hidden characters or a mis-spelling?
Are you sure the stacktrace points to that line, are you using becomes elsewhere in the file?

How to extract tasks and variables from a Rakefile?

I need to:
Open a Rakefile
Find if a certain task is defined
Find if a certain variable is defined
This works to find tasks defined inside a Rakefile, but it pollutes the global namespace (i.e. if you run it twice, all tasks defined in first one will show up in the second one):
sub_rake = Rake::DefaultLoader.new
sub_rake.load("Rakefile")
puts Rake.application.tasks
In Rake, here is where it loads the Makefile:
https://github.com/ruby/rake/blob/master/lib/rake/rake_module.rb#L28
How do I get access to the variables that are loaded there?
Here is an example Rakefile I am parsing:
load '../common.rake'
#source_dir = 'source'
desc "Run all build and deployment tasks, for continuous delivery"
task :deliver => ['git:pull', 'jekyll:build', 'rsync:push']
Here's some things I tried that didn't work. Using eval on the Rakefile:
safe_object = Object.new
safe_object.instance_eval("Dir.chdir('" + f + "')\n" + File.read(folder_rakefile))
if safe_object.instance_variable_defined?("#staging_dir")
puts " Staging directory is " + f.yellow + safe_object.instance_variable_get("#staging_dir").yellow
else
puts " Staging directory is not specified".red
end
This failed when parsing desc parts of the Rakefile. I also tried things like
puts Rake.instance_variables
puts Rake.class_variables
But these are not getting the #source_dir that I am looking for.
rakefile_body = <<-RUBY
load '../common.rake'
#source_dir = 'some/source/dir'
desc "Run all build and deployment tasks, for continuous delivery"
task :deliver => ['git:pull', 'jekyll:build', 'rsync:push']
RUBY
def source_dir(ast)
return nil unless ast.kind_of? AST::Node
if ast.type == :ivasgn && ast.children[0] == :#source_dir
rhs = ast.children[1]
if rhs.type != :str
raise "#source_dir is not a string literal! #{rhs.inspect}"
else
return rhs.children[0]
end
end
ast.children.each do |child|
value = source_dir(child)
return value if value
end
nil
end
require 'parser/ruby22'
body = Parser::Ruby22.parse(rakefile_body)
source_dir body # => "some/source/dir"
Rake runs load() on the Rakefile inside load_rakefile in the Rake module. And you can easily get the tasks with the public API.
Rake.load_rakefile("Rakefile")
puts Rake.application.tasks
Apparently that load() invocation causes the loaded variables to be captured into the main Object. This is the top-level Object of Ruby. (I expected it to be captured into Rake since the load call is made in the context of the Rake module.)
Therefore, it is possible to access instance variables from the main object using this ugly code:
main = eval 'self', TOPLEVEL_BINDING
puts main.instance_variable_get('#staging_dir')
Here is a way to encapsulate the parsing of the Rakefile so that opening two files will not have all the things from the first one show up when you are analyzing the second one:
class RakeBrowser
attr_reader :tasks
attr_reader :variables
include Rake::DSL
def task(*args, &block)
if args.first.respond_to?(:id2name)
#tasks << args.first.id2name
elsif args.first.keys.first.respond_to?(:id2name)
#tasks << args.first.keys.first.id2name
end
end
def initialize(file)
#tasks = []
Dir.chdir(File.dirname(file)) do
eval(File.read(File.basename(file)))
end
#variables = Hash.new
instance_variables.each do |name|
#variables[name] = instance_variable_get(name)
end
end
end
browser = RakeBrowser.new(f + "Rakefile")
puts browser.tasks
puts browser.variables[:#staging_dir]

How to make Class.clone or Class.dup work in ruby?

I have a model called Client and I would like to clone it in certain cases where clients (the real world kind) have modifications that require class level changes.
For example, if I have:
class Client
set_table_name :clients
def some_method
puts "Hello"
end
end
Then if I have the following:
module ClientA
def some_method
puts "World"
end
end
I would expect that I could clone (or dup) the class, then include the module to overwrite the method some_method.
But here's what happens in my production console:
> CA = Client.dup
> CA.singleton_class.send(:include, ClientA) # Or just CA.send(:include, ClientA)
> client = CA.new
> client.some_method
=> "Hello" # Expected "World"
Is there a trick to this?
Instead of Client.dup use Class.new(Client), which subclasses Client.
If you're trying to avoid that, this seems to work with Client.dup:
CA.send(:define_method, :some_method) do
puts "World"
end
If you're wanting to override specific classes with a module's data, you want to extend it.
client = Client.new
client.extend(ClientA)
client.some_method
=> "World"
You can see it work here: http://rubyfiddle.com/riddles/7621e

Ruby - Method call to object in array

I'm working with a Ruby project for school, and have sadly not been able to find an answer to this question in my literature.
I have an array of camping lots, each containing a guest. I initialize the lots like this:
lots = Array.new
for i in (1..36)
lots[i] = Lot.new(i)
end
Further down I create a Guest object, initialize it, and now I want to add the Guest to my Lot. The method in the class Lot looks like this:
def AddGuest(guest)
#guest = guest
end
The problem comes when I want to call the method, as the Lot is in an Array.
lots[lotnumber].AddGuest(guest)
This call gives me the error:
undefined method `+#' for #<Guest:0x2c1ff14> (NoMethodError)
I have used require, so the classes know about each other. I've had quite a hard time understanding Ruby, could my error be that I try to access the AddGuest method in the Array class? I'm used to doing things like this in C++.
Below is the full source (the relevant parts at least).
Entire Lot class:
class Lot
def initialize(number)
#gauge = rand(2000) + 2000
#number = number
#guest = false
end
def Occupied()
return #guest
end
def AddGuest(guest)
#guest = guest
end
def RemoveGuest()
#guest = false
end
end
Parts of main.rb
#includes
require 'guest'
require 'lot'
#initiate comparison variables
userInput = "0"
numberOfGuests = 0
foundLot = false
guests = Array.new
lots = Array.new
#initialize lot list
for i in (1..36)
lots[i] = Lot.new(i)
end
Player input omitted
#make sure lot is not taken
while foundLot == false do
lotnumber = rand(35)+1
if lots[lotnumber].Occupied() == false then
foundLot = "true"
end
end
foundLot = false
guest = Guest.new(firstName, lastName, adress, phone, arrival, lotnumber)
guests.insert(numberOfGuests, guest)
numberOfGuests++
lots[lotnumber].AddGuest(guest) #this is where error hits
end
end
end
The error appears to be related to your use of the ++ operator, which is, quite naturally, supported in C++, but is not supported in Ruby.
The equivalent is:
numberOfGuests += 1
A couple little tips...
[1]
A slightly more idiomatic way to write this...
for i in (1..36)
lots[i] = Lot.new(i)
end
would be...
(1..36).each { |i| lots[i] << Lot.new(i) }
[2]
To remove a Guest from a Lot, you might want to set it to nil rather than false. This would be my suggestion...
class Lot
def initialize(number)
#gauge = rand(2000) + 2000
#number = number
# Don't need to set #guest -- it's nil by default.
end
# In Ruby, methods that return a boolean often have a "?".
# Makes it "read better" when you call the method. (See
# usage sample.)
def occupied?
! #guest.nil?
end
# There's a more commonplace way to do this. See below...
def add_guest(guest)
#guest = guest
end
def remove_guest()
#guest = nil
end
end
Example of usage:
>> lot = Lot.new(2)
=> #<Lot:0x1300920 #number=2, #gauge=3444>
>> lot.occupied
=> false
>> lot.add_guest('A guest')
=> "A guest"
>> lot.occupied?
=> true
>> lot.remove_guest
=> nil
>> lot.occupied?
=> false
Take two...
It's conventional to use attr_accessor methods in your class definition. They automatically add getter and setter methods to your class. You could do that instead of add_guest and remove_guest if you wanted to follow the common Ruby pattern...
class Lot
attr_accessor :number, :gauge, :guest
def initialize(number)
#gauge = rand(2000) + 2000
#number = number
end
def occupied?
! #guest.nil?
end
end
Usage...
irb(main):017:0> lot = Lot.new(3)
=> #<Lot:0xb7f7fca8 #gauge=3186, #number=3>
Set the Guest of a Lot (like add_guest)...
irb(main):019:0> lot.guest = 'A guest'
=> "A guest"
irb(main):020:0> lot.occupied?
=> true
Get the Guest for a Lot...
irb(main):025:0> lot.guest
=> "A guest"
Remove the Guest...
irb(main):021:0> lot.guest = nil
=> nil
irb(main):023:0> lot.occupied?
=> false
Generally Ruby method names are not capitalized. The convention are simply: ClassName, CONSTANT, method_name.
Since you have an Array of Lot objects, the following should be true:
lots.class # => Array
lots[1].class # => Lot
The method called should be defined for Lot.

Resources