Configure a class with a block - ruby

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

Related

How to best abstract class variables in Ruby

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

How can I use class instance variables in an extended module in Ruby?

I want to create a DSL which stores various blocks, and then can call them. I want this to be a reusable module that I can include in multiple classes.
I figured out a way to do this using class variables, but rubocop complains and says that I should use class instance variables instead. I can't figure out a way to do this though. Is it possible?
module MyModule
def self.included(base)
base.extend(ClassMethods)
end
def run_fixers
ClassMethods.class_variable_get(:##fixers).each(&:call)
end
module ClassMethods
def fix(_name, &block)
##fixers ||= []
##fixers << block
end
end
end
class MyClass
include MyModule
def initialize
run_fixers
end
fix 'test' do
puts 'testing'
end
end
Rubocop complains about ##class_variable style. Class variables usage can be refactored into using instance variable of a class:
class SomeClass
#some_variable ||= []
end
instead of
class SomeClass
##some_variable ||= []
end
You can define instance variable of a class in any place where self equals to the class object. For example, in a class method:
module MyModule
def self.included(base)
base.extend(ClassMethods)
end
def run_fixers
ClassMethods.fixers.each(&:call)
end
module ClassMethods
def self.fixers
#fixers ||= []
end
def fix(_name, &block)
ClassMethods.fixers << block
end
end
end

how to write a ruby module to allow methods in class definition

Having a hard time figuring this out. Suppose I wanted to write a module, and when included, it would allow classes to define methods by calling a method with symbols
class Anything
include Foo
initializers :hello, :goodbye
end
module Foo
# What goes in here? Its not
# def self.initializers(*symbols)
end
Same syntax idea as attr_accessible. Tried finding it in the Rails source, but, well..
module Foo
def self.included(base)
base.extend ClassMethods
end
module ClassMethods
def initializers *names
names.each do |name|
define_method name do
'ok'
end
end
end
end
def self.included(base)
base.extend ClassMethods
end
end
class Anything
include Foo
initializers :hello, :goodbye
end
puts Anything.new.hello #=> ok
for example:
module Foo
def self.included(base)
block = Proc.new do |*symbols|
puts symbols.inspect
end
base.class.send(:define_method, :initializers, block)
end
end
class Anything
include Foo
initializers :one, :two , :three
end

Add class methods through include

I have Ruby class into which I want to include both class and instance methods. Following the pattern described here, I'm currently using the following:
class SomeObject
include SomeObject::Ability
def self.some_builder_method(params)
# use some_class_method ...
end
end
module SomeObject::Ability
module ClassMethods
def some_class_method(param)
# ...
end
end
def self.included(klass)
klass.extend(ClassMethods)
end
def some_instance_method
# ...
end
end
I'd rather not make two separate modules (one being included and the other being extended), because all the methods in my module logically fit together. On the other hand, this pattern a) requires me to define an additional ClassMethods module and b) requires me to write a boilerplate self.included method for every module.
Is there a better way to do this?
Edit 1: I've found another way, but I'm unsure if this is better than the first.
module Concern
def included(base)
# Define instance methods.
instance_methods.each do |m|
defn = instance_method(m)
base.class_eval { define_method(m, defn) }
end
# Define class methods.
(self.methods - Module.methods).each do |m|
unless m == __method__
base.define_singleton_method(m, &method(m))
end
end
end
end
module SomeModule
extend Concern
def self.class_m
puts "Class"
end
def instance_m
puts "Instance"
end
end
class Allo
include SomeModule
end
Allo.class_m # => "Class"
Allo.new.instance_m # => "Instance"
If I understand you correctly, you really just want to use ActiveSupport::Concern:
module PetWorthy
extend ActiveSupport::Concern
included do
validates :was_pet, inclusion: [true, 'yes']
end
def pet #instance method
end
module ClassMethods
def find_petworthy_animal
# ...
end
end
end
class Kitty
include PetWorthy
end
Kitty.find_petworthy_animal.pet
You (hopefully obviously) don't need to use the included method if you don't have any behavior to trigger on include, but I put it in just to demonstrate.

Class variables and extending a module

I have a module like the following
module MyModule
def self.included(base)
base.extend ClassMethods
end
module ClassMethods
def foo
##var = 1
end
def bar
puts ##var
end
end
end
class A
include MyModule
foo
end
class B < A; end
so that
B.bar outputs '1'.
However, I would like to have .bar only be defined if .foo is called. I tried
module MyModule
def self.included(base)
base.extend ClassMethods
end
module ClassMethods
def foo
##var = 1
extend SingletonMethods
end
module SingletonMethods
def bar
puts ##var
end
end
end
The problem is that
B.bar
returns the error "uninitialized class variable ##var in MyModule::SingletonMethods". How can I make it so that a variable defined in .foo is available to .bar?
use mattr_accessor instead
I was able to access class variables from a module using the self syntax
class User
include Base
##attributes = [:slug, :email, :crypted_password]
end
module Base
def self.included(base)
base.extend ClassMethods
end
def headers
if defined? self.attributes
self.attributes
end
end
end
Now calling User.headers gives me the expected result of
[:slug, :email, :crypted_password]
If anyone can shed more light on why this works exactly so in ruby, please let me know!

Resources