ruby: calling a instance method without using instance - ruby

I know in ruby, when we call an instance method, we need to firstly instantiate a class object.
But when I see a open sourced code I got confused.
The code is like this:
File Message.rb
require 'json'
module Yora
module Message
def serialize(msg)
JSON.generate(msg)
end
def deserialize(raw, symbolized_key = true)
msg = JSON.parse(raw, create_additions: true)
if symbolized_key
Hash[msg.map { |k, v| [k.to_sym, v] }]
else
msg
end
end
end
end
File. Persistance.rb
require 'fileutils'
require_relative 'message'
module Yora
module Persistence
class SimpleFile
include Message
def initialize(node_id, node_address)
#node_id, #node_address = node_id, node_address
FileUtils.mkdir_p "data/#{node_id}"
#log_path = "data/#{node_id}/log.txt"
#metadata_path = "data/#{node_id}/metadata.txt"
#snapshot_path = "data/#{node_id}/snapshot.txt"
end
def read_metadata
metadata = {
current_term: 0,
voted_for: nil,
cluster: { #node_id => #node_address }
}
if File.exist?(#metadata_path)
metadata = deserialize(File.read(#metadata_path)) #<============
end
$stderr.puts "-- metadata = #{metadata}"
metadata
end
.....
You can see the line I marked with "<==="
It uses deserialize function that been defined in message class.
And from message class we can see that method is a instance method, not class method.
So why can we call it without instantiating anything like this?
thanks

Message ist an module. Your Class SimpleFile includes this module. so the module methods included in your class SimpleFile. that means, all module methods can now be used like as methods from SimpleFile
see http://ruby-doc.org/core-2.2.0/Module.html for more infos about module in ruby. it's a great feature.

It is being called on an instance. In Ruby, if you leave out the explicit receiver of the message send, an implicit receiver of self is assumed. So, deserialize is being called on an instance, namely self.
Note that this exact same phenomenon also occurs in other places in your code, much earlier (in line 1, in fact):
require 'fileutils'
require_relative 'message'
Here, you also have two method calls without an explicit receiver, which means that the implicit receiver is self.

Related

How can I DRY up SemanticLogger::Loggable and SemanticLogger::Loggable::ClassMethods by extending classes?

Summary
I'm trying to use SemanticLogger 4.10.0 as a part of a dynamic tracing solution in a Ruby 3.1.1 app. However, I seem to be misunderstanding something about how or where to access the logger instance that SemanticLogger::Loggable is supposed to create on the class, and am getting NameError exceptions (documented below) when calling #logger_measure_method from inside the extended class because I can't seem to implement the class extension properly.
Code That Demonstrates the Problem
Module with CustomLogger
require 'json'
require 'semantic_logger'
module SomeGem
module CustomLogger
include SemanticLogger
class MyFormat < SemanticLogger::Formatters::Default
def call (log, logger, machine_name: nil, json: nil)
self.log = log
self.logger = logger
message = "[#{machine name}] #{message}" if machine_name
payload = JSON.parse(json)
[time, level, process.info, tags, named.tags, duration, name, message, payload, exception].compact!
end
end
def self.extended(klass)
prepend SemanticLogger, SemanticLogger::Loggable, SemanticLogger::Loggable::ClassMethods
SemanticLogger.default_level = :debug
SemanticLogger.add_appender(file_name: "/dev/stderr", formatter: SomeGem::CustomLogger::MyFormat.new,
level: :warn, filter: proc { %i[warn fatal unknown].include? _1.level }
)
SemanticLogger.add_appender(file_name: "/dev/stdout", formatter: SomeGem::CustomLogger::MyFormat.new,
level: :trace, filter: proc { %i[trace debug info error].include? _1.level }
)
define_method(:auto_measure_method_calls) do
public_instance_methods(false).
reject { _1.to_s.match? /^logger/ }.
each { logger_measure_method _1, level: :trace }
end
end
end
end
Module Extended by CustomLogger
module SomeGem
class Foo
extend CustomLogger
def alpha = 1
def beta = 2
def charlie = 3
auto_measure_method_calls if ENV['LOG_LEVEL'] == 'trace'
end
end
Exercising the Code
ENV['LOG_LEVEL'] = 'trace'
f = SomeGem::Foo.new
pp f.alpha, f.beta, f.charlie
Problems and Issues with Code
My issues are:
The underlying (and possibly X/Y) issue is that I want to DRY up the code of including SemanticLogger::Loggable and SemanticLogger::Loggable::ClassMethods in every class I'm tracing, and auto-measuring the extended classes' methods in development.
When I try to extend a class with a module that is intended to pull in SemanticLogger::Loggable, I don't seem to always have logger available as an accessor throughout the class.
I'm also concerned that including the module in multiple classes would result in duplicate appenders being added to the #appenders array, wherever that's actually stored.
Most importantly though, when I try to automagically add logging and method measurement through extending a class, I get errors like the following:
~/.gem/ruby/3.1.1/gems/semantic_logger-4.10.0/lib/semantic_logger/loggable.rb:96:in `alpha': undefined local variable or method `logger' for #SomeGem::Foo:0x00000001138f6a38 (NameError) from foo.rb:51:in `<main>'
Am I missing something obvious about how to extend the classes with SemanticLogger? If there's a better way to accomplish what I'm trying to do, that's great! If I'm doing something wrong, understanding that would be great, too. If it's a bug, or I'm using the feature wrong, that's useful information as well.

Undefined method `collection' for #<Mongo::Client> Did you mean? collections (NoMethodError). Ruby

I am working with automated test. This is the first time I'm working with mongoDB.
So, I am trying to create a generic method to find a document in a desired collection that will be passed as parameter. I've found some examples and all of them use the .collection method. It doesn't seem to work in my project.
Here's my DB client code:
require 'mongo'
require 'singleton'
class DBClient
include Singleton
def initialize
#db_connection = Mongo::Client.new($env['database']['feature']['url'])
end
def find(collection, value)
coll = #db_connection.collection(collection)
coll.find(owner: 'value')
end
end
And here's how I instance my method
DBClient.instance.find('collectionTest', 'Jhon')
When I run my test I get the following message:
undefined method `collection' for #<Mongo::Client: cluster=localhost:>
Did you mean? collections (NoMethodError)
The gem I'm using is mongo (2.6.1).
What I am doing wrong?
Based on documentation, there is indeed no method collection in Mongo::Client. What you are looking for is the [] method. The code will then look like this:
require 'mongo'
require 'singleton'
class DBClient
include Singleton
def initialize
#db_connection = Mongo::Client.new($env['database']['feature']['url'])
end
def find(collection, value)
coll = #db_connection[collection]
coll.find(owner: value)
end
end
EDIT: I've also changed the line with the find itself. In your original code, it would find documents where owner is 'value' string. I presume you want the documents where owner matches the value send to the function.

How to stub class instantiated inside tested class in rspec

I have problem stubbing external api, following is the example
require 'rspec'
require 'google/apis/storage_v1'
module Google
class Storage
def upload file
puts '#' * 90
puts "File #{file} is uploaded to google cloud"
end
end
end
class UploadWorker
include Sidekiq::Worker
def perform
Google::Storage.new.upload 'test.txt'
end
end
RSpec.describe UploadWorker do
it 'uploads to google cloud' do
google_cloud_instance = double(Google::Storage, insert_object: nil)
expect(google_cloud_instance).to receive(:upload)
worker = UploadWorker.new
worker.perform
end
end
I'm trying to stub Google::Storage class. This class is instantiated inside the object being tested. How can I verify the message expectation on this instance?
When I run above example, I get following output, and it seems logical, my double is not used by tested object
(Double Google::Storage).upload(*(any args))
expected: 1 time with any arguments
received: 0 times with any arguments
I'm new to Rspec and having hard time with this, any help will be appreciated.
Thanks!
Reaching for DI is always a good idea (https://stackoverflow.com/a/51401376/299774) but there are sometimes reasons you can't so it, so here's another way to stub it without changing the "production" code.
1. expect_any_instance_of
it 'uploads to google cloud' do
expect_any_instance_of(Google::Storage).to receive(:insert_object)
worker = UploadWorker.new
worker.perform
end
In case you just want to test that the method calls the method on any such objects.
2. bit more elaborated setup
In case you want to control or set up more expectations, you can do this
it 'uploads to google cloud' do
the_double = instance_double(Google::Storage)
expect(Google::Storage).to receive(:new).and_return(the_double)
# + optional `.with` in case you wanna assert stuff passed to the constructor
expect(the_double).to receive(:insert_object)
worker = UploadWorker.new
worker.perform
end
Again - Dependency Injection is clearer, and you should aim for it. This is presented as another possibility.
I would consider reaching for dependency injection, such as:
class UploadWorker
def initialize(dependencies = {})
#storage = dependencies.fetch(:storage) { Google::Storage }
end
def perform
#storage.new.upload 'test.txt'
end
end
Then in the spec you can inject a double:
storage = double
expect(storage).to receive(...) # expection
worker = UploadWorker.new(storage: storage)
worker.perform
If using the initializer is not an option then you could use getter/setter method to inject the dependency:
def storage=(new_storage)
#storage = new_storage
end
def storage
#storage ||= Google::Storage
end
and in the specs:
storage = double
worker.storage = storage

ruby, no method error

I am receiving the following error when running my below ruby script:
s3parse.rb:12:in `block in <class:AccountLog>': undefined method `extract_account_id' for AccountLog:Class (NoMethodError)
I dont think it should be a class method, is there a reason its not taking my method into account?
class AccountLog
attr_accessor :bytes, :account_id, :date
def extract_account_id(line)
line.match(%r{accounts/(\d+)}).captures.join.to_i
end
s3log = File.open('vidcoder.txt').each do |line|
account_log = AccountLog.new
account_log.date = line.match(%r{\[[^:]*}).to_s.delete"[" #need to finish this regex to make it work
account_log.account_id = extract_account_id(line)
account_log.bytes = line.match(%r{^.*\s+HTTP.*\s+-\s+(\d+)\s+}).captures.join.to_i
puts "\n"
puts "The api request on #{account_log.date} was fromm account number #{account_log.account_id} and the bytes were #{account_log.bytes}"
end
end
def extract_account_id will define an instance method.
In the way you call it, you need a class method instead.
Define it like this:
def self.extract_account_id(line)
or, as you already have an AccountLog instance, use it to call extract_account_id:
account_log.account_id = account_log.extract_account_id(line)
Please note that with second way you do not need to alter method definition, just call extract_account_id via account_log instance.
And i guess you would want to put s3log = File... outside class definition.
Or use a constant instead: S3log = ...
Then you'll can access it as AccountLog::S3log
Is there any reason you don't think it should be a class method? You are using it in the context of a class method and that's why it it's saying no such method for class AccountLog.
If you name your method as self.extract_account_id(line) I'm sure it will work.
From what you are trying to do I think this is what you are looking for?
class AccountLog
attr_accessor :bytes, :account_id, :date
def self.extract_account_id(line)
line.match(%r{accounts/(\d+)}).captures.join.to_i
end
end
s3log = File.open('vidcoder.txt').each do |line|
account_log = AccountLog.new
account_log.date = line.match(%r{\[[^:]*}).to_s.delete"[" #need to finish this regex to make it work
account_log.account_id = extract_account_id(line)
account_log.bytes = line.match(%r{^.*\s+HTTP.*\s+-\s+(\d+)\s+}).captures.join.to_i
puts "\n"
puts "The api request on #{account_log.date} was fromm account number #{account_log.account_id} and the bytes were #{account_log.bytes}"
end
While you could take the class method approach, there seems to be a little more going on.
You should put the extraction logic in a method in itself rather than let it hangout in your class. Then outside of the class, have an instance of AccountLog where you can call on the methods for log and account id extraction. At that point you can do something with those values.
Class method or not is a detail we can explore after the class is a bit more clean I think.

Adding #to_yaml to DataMapper models

I am using DataMapper for Database access. My goal is to send the models to an webservice as read-only object. This is my current try:
class User
include DataMapper::Resource
def to_yaml(opts = {})
mini_me = OpenStruct.new
instance_variables.each do |var|
next if /^#_/ =~ var.to_s
mini_me.send("#{var.to_s.gsub(/^#/, '')}=", instance_variable_get(var))
end
mini_me.to_yaml(opts)
end
....
end
YAML::ENGINE.yamler = 'psych'
u = User.get("hulk")
p u.to_yaml
# => "--- !ruby/object:OpenStruct\ntable:\n :uid: hulk\n :uidNumber: 1000\n :gidNumber: 1001\n :email: hulk#example.com\n :dn: uid=hulk,ou=People,o=example\n :name: Hulk\n :displayName: Hulk\n :description: Hulk\n :homeDirectory: /home/hulk\n :accountFlags: ! '[U ]'\n :sambaSID: S-1-5-21-......\nmodifiable: true\n"
p [ u ].to_yaml # TypeError: can't dump anonymous class Class
Any ideas how to make this work and get rid of the exception?
Thanks,
krissi
Using to_yaml is deprecated in Psych, and from my testing it seems to be actually broken in cases like this.
When you call to_yaml directly on your object, your method gets called and you get the result you expect. When you call it on the array containing your object, Psych serializes it but doesn’t correctly handle your to_yaml method, and ends up falling back onto the default serialization. In your case this results in an attempt to serialize an anonymous Class which causes the error.
To fix this, you should use the encode_with method instead. If it’s important that the serialized form is tagged as an OpenStruct object in the generated yaml you can use the represent_object (that first nil parameter doesn’t seem to be used):
def encode_with(coder)
mini_me = OpenStruct.new
instance_variables.each do |var|
next if /^#_/ =~ var.to_s
mini_me.send("#{var.to_s.gsub(/^#/, '')}=", instance_variable_get(var))
end
coder.represent_object(nil, mini_me)
end
If you were just using OpenStruct for convenience, an alternative could be something like:
def encode_with(coder)
instance_variables.each do |var|
next if /^#_/ =~ var.to_s
coder[var.to_s.gsub(/^#/, '')]= instance_variable_get(var)
end
end
Note that Datamapper has its own serializer plugin that provides yaml serialization for models, it might be worth looking into.

Resources