I'm writing a gem which I would like to work with and without the Rails environment.
I have a Configuration class to allow configuration of the gem:
module NameChecker
class Configuration
attr_accessor :api_key, :log_level
def initialize
self.api_key = nil
self.log_level = 'info'
end
end
class << self
attr_accessor :configuration
end
def self.configure
self.configuration ||= Configuration.new
yield(configuration) if block_given?
end
end
This can now be used like so:
NameChecker.configure do |config|
config.api_key = 'dfskljkf'
end
However, I don't seem to be able to access my configuration variables from withing the other classes in my gem. For example, when I configure the gem in my spec_helper.rb like so:
# spec/spec_helper.rb
require "name_checker"
NameChecker.configure do |config|
config.api_key = 'dfskljkf'
end
and reference the configuration from my code:
# lib/name_checker/net_checker.rb
module NameChecker
class NetChecker
p NameChecker.configuration.api_key
end
end
I get an undefined method error:
`<class:NetChecker>': undefined method `api_key' for nil:NilClass (NoMethodError)
What is wrong with my code?
Try refactoring to:
def self.configuration
#configuration ||= Configuration.new
end
def self.configure
yield(configuration) if block_given?
end
The main issue is that you've applied too much indirection. Why don't you just do
module NameChecker
class << self
attr_accessor :api_key, :log_level
end
end
and be done with it? You could also override the two generated readers right afterwards so that they ensure the presence of the environment that you need...
module NameChecker
class << self
attr_accessor :api_key, :log_level
def api_key
raise "NameChecker really needs is't api_key set to work" unless #api_key
#api_key
end
DEFAULT_LOG_LEVEL = 'info'
def log_level
#log_level || DEFAULT_LOG_LEVEL
end
end
end
Now, the actual (technical) problem is that you are defining a class called NetChecker and while defining it you are trying to print the return value of the api_key call on an assumed Configuration object (so you are violating the law of Demeter here). This fails, because you are defining NetChecker before anyone really has had time to define any configuration. So you are in fact requesting api_key before the configure method has been called on NameChecker, so it has nil in it's configuration ivar.
My advice would be to remove the overengineering and try again ;-)
Related
I've got a class such as
# this has been simplified for the example
class MyClass
##private_attributes = []
def self.private_attributes(*args)
##private_attributes = args
end
def private_attributes
#private_attributes ||= ##private_attributes
end
end
It works great. I've this ##private_attributes set at class level which's then used at instance level in multiple ways.
I want to abstract this logic somewhere else to simplify my class, something like that
class MyClass
include PrivateAttributes
end
When I create the module PrivateAttributes, however I shape it, the ##private_attributes isn't understood at MyClass level.
I tried many things but here's the latest code attempt
module PrivateAttributes
include ProcessAttributes
def self.included(base)
base.extend(ClassMethods)
base.include(InstanceMethods)
end
module ClassMethods
##private_attributes = []
def private_attributes(*args)
##private_attributes = args
end
end
module InstanceMethods
def private_attributes
#private_attributes ||= process_attributes_from(##private_attributes)
end
def private_attributes?
instance_options[:scope] == :private
end
end
end
It crashes with this error
NameError:
uninitialized class variable ##private_attributes in PrivateAttributes::InstanceMethods
Did you mean? private_constant
In short, the ##private_attributes isn't transferred throughout the code, but looks like it stays at the module level.
What's the best way to abstract this logic from my original class ?
Working solution
An easy workaround is to use mattr_accessor on the class level or anything similar to communicate our data around. I preferred to write down my own methods in this case:
module PrivateAttributes
include ProcessAttributes
def self.included(base)
base.extend(ClassMethods)
base.include(InstanceMethods)
end
module ClassMethods
##private_attributes_memory = []
def private_attributes(*args)
##private_attributes_memory = args
end
def private_attributes_memory
##private_attributes_memory
end
end
module InstanceMethods
def private_attributes
#private_attributes ||= process_attributes_from private_attributes_memory
end
# you can add diverse methods here
# which will be used in MyClass once included
private
def private_attributes_memory
self.class.private_attributes_memory
end
end
end
I have a base module that has some logic in it and it gets included inside various classes. Now I need a configure block that sets some configuration how the class should behave.
I tried the following code:
class MyConfig
attr_accessor :foo, :bar
end
module BaseModule
def self.included base
base.include InstanceMethods
base.extend ClassMethods
end
module ClassMethods
attr_reader :config
def configure &block
#config = MyConfig.new
block.call(#config)
end
end
module InstanceMethods
def do_something
puts self.class.config.inspect
end
end
end
class MyClass1
include BaseModule
configure do |config|
config.foo = "foo"
end
end
class MyClass2
include BaseModule
configure do |config|
config.bar = "bar"
end
end
MyClass1.new.do_something
#<MyConfig:0x007fa052877ea0 #foo="foo">
MyClass2.new.do_something
#<MyConfig:0x007fa052877ce8 #bar="bar">
I'm unsure if an instance variable #config for a module / class is the best way to configure a class. Is this the right way, or are there any better solutions?
Is it good to also use base.extend ClassMethods when the module only gets included with include BaseModule? A developer could expect that only instance methods get included, but as a side effect there are also class methods extended.
Update
I've added a MyConfig class, from which a new instance is created. This has the benefit that you can use config.foo = "foo".
I've changed your code a bit to use an approach both me and a lot of popular gems (like devise) use for configuration. Hope you'll like it :)
module BaseModule
def self.included base
base.include InstanceMethods
base.extend ClassMethods
end
module ClassMethods
mattr_accessor :foo
##foo = 'initial value'
def configure &block
block.call(self)
end
end
module InstanceMethods
def do_something
puts foo
end
end
end
class MyClass1
include BaseModule
configure do |klass|
klass.foo = "foo"
end
end
class MyClass2
include BaseModule
configure do |klass|
klass.foo = "bar"
end
end
MyClass1.new.do_something
# => "foo"
MyClass2.new.do_something
# => "bar"
The changes are:
use mattr_accessor - it creates both getters and setters for your method, you can opt out some of them if you wish, more info here
let configure method return self, this way you can configure your class in a simple, readable, rails-like way
Example:
Kid.config do |k|
k.be_kind = false
k.munch_loudly = true
k.age = 12
end
My module:
# test.rb
module Database
# not used in this example, but illustrates how I intend to include the module in class
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
attr_accessor :db, :dbname
self.dbname = ENV['RACK_ENV'] == 'test' ? 'mydb_test' : 'mydb'
end
end
When I load it, I get this:
test.rb:7:in `<module:ClassMethods>': undefined method `dbname=' for Database::ClassMethods:Module (NoMethodError)
from bin/test:7:in `<module:Database>'
from bin/test:1:in `<main>'
Can't figure out why. If I check instance_methods, it's empty before attr_accessor, and has the appropriate four methods after. Yet when I call them, they don't exist.
attr_accessor defines instance methods on ClassMethods (such as ClassMethods#dbname=), but you are trying to call a class method (ClassMethods.dbname=).
With self.dbname = ENV..., you are calling a method #self.dbname=, which doesn't exist. Maybe this is what you are after?
# test.rb
module Database
# not used in this example, but illustrates how I intend to include the module in class
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
attr_accessor :db, :dbname
def self.dbname ; ENV['RACK_ENV'] == 'test' ? 'mydb_test' : 'mydb' ; end
puts self.dbname
end
end
I need to access the config variables from inside another class of a module.
In test.rb, how can I get the config values from client.rb? #config gives me an uninitialized var. It's in the same module but a different class.
Is the best bet to create a new instance of config? If so how do I get the argument passed in through run.rb?
Or, am I just structuring this all wrong or should I be using attr_accessor?
client.rb
module Cli
class Client
def initialize(config_file)
#config_file = config_file
#debug = false
end
def config
#config ||= Config.new(#config_file)
end
def startitup
Cli::Easy.test
end
end
end
config.rb
module Cli
class Config
def initialize(config_path)
#config_path = config_path
#config = {}
load
end
def load
begin
#config = YAML.load_file(#config_path)
rescue
nil
end
end
end
end
test.rb
module Cli
class Easy
def self.test
puts #config
end
end
end
run.rb
client = Cli::Client.new("path/to/my/config.yaml")
client.startitup
#config is a instance variable, if you want get it from outside you need to provide accessor, and give to Easy class self object.
client.rb:
attr_reader :config
#...
def startitup
Cli::Easy.test(self)
end
test.rb
def self.test(klass)
puts klass.config
end
If you use ##config, then you can acces to this variable without giving a self object, with class_variable_get.
class Lol
##lold = 0
def initialize(a)
##lold = a
end
end
x = Lol.new(4)
puts Lol.class_variable_get("##lold")
I recommend to you read metaprogramming ruby book.
I'm pretty curious about how this thing works.
after require 'sinatra'
then I can invoke get() in the top level scope.
after digging into the source code, I found this get() structure
module Sinatra
class << self
def get
...
end
end
end
know the class << self is open up the self object's singleton class definition and add get() inside, so it starts to make sense.
But the only thing left I can't figure out is it's within module Sinstra, how could get() be invoked without using Sinatra:: resolution operation or something?
It is spread out in a few places, but if you look in lib/sinatra/main.rb, you can see this line at the bottom:
include Sinatra::Delegator
If we go into lib/sinatra/base.rb we see this chunk of code around like 1470.
# Sinatra delegation mixin. Mixing this module into an object causes all
# methods to be delegated to the Sinatra::Application class. Used primarily
# at the top-level.
module Delegator #:nodoc:
def self.delegate(*methods)
methods.each do |method_name|
define_method(method_name) do |*args, &block|
return super(*args, &block) if respond_to? method_name
Delegator.target.send(method_name, *args, &block)
end
private method_name
end
end
delegate :get, :patch, :put, :post, :delete, :head, :options, :template, :layout,
:before, :after, :error, :not_found, :configure, :set, :mime_type,
:enable, :disable, :use, :development?, :test?, :production?,
:helpers, :settings
class << self
attr_accessor :target
end
self.target = Application
end
This code does what the comment says: if it is included, it delegates all calls to the list of delegated methods to Sinatra::Application class, which is a subclass of Sinatra::Base, which is where the get method is defined. When you write something like this:
require "sinatra"
get "foo" do
"Hello World"
end
Sinatra will end up calling the get method on Sinatra::Base due to the delegation it set up earlier.
I haven't looked at the source of Sinatra, but the gist of it should be something like
>> module Test
.. extend self
.. class << self
.. def get; "hi";end
.. end
.. end #=> nil
>> include Test #=> Object
>> get #=> "hi"