I created small API library, everything worked fine, until I realized, that I need multiple configurations.
It looks like this:
module Store
class Api
class << self
attr_accessor :configuration
def configure
self.configuration ||= Configuration.new
yield configuration
end
def get options = {}
url = "#{configuration.domain}/#{options[:resource]}"
# ResClient url ...
end
end
end
class Configuration
attr_accessor :domain
def initialize options = {}
#domain = options[:domain]
end
end
class Product
def self.get
sleep 5
Api.get resource: 'products'
end
end
end
When I run it simultaneously, it override module configuration.
Thread.new do
10.times do
Store::Api.configure do |c|
c.domain = "apple2.com"
end
p Store::Product.get
end
end
10.times do
Store::Api.configure do |c|
c.domain = "apple.com"
end
p Store::Product.get
end
I can't figure out, how make this module better. Thanks for your advise
Well, if you don't want multiple threads to compete for one resource, you shouldn't have made it a singleton. Try moving object configuration from class to its instances, then instantiate and configure them separately.
There is more to refactor here, but this solves your problem:
module Store
class API
attr_reader :domain
def initialize(options = {})
#domain = options[:domain]
end
def products
sleep 5
get resource: 'products'
end
private
def get(options = {})
url = "#{configuration.domain}/#{options[:resource]}"
# ResClient url ...
end
end
end
Thread.new do
10.times do
api = Store::API.new(domain: 'apple2.com')
p api.products
end
end
10.times do
api = Store::API.new(domain: 'apple.com')
p api.products
end
Related
I'm working to create a few Ruby builder objects, and thinking on how I could reuse some of Ruby's magic to reduce the logic of the builder to a single class/module. It's been ~10 years since my last dance with the language, so a bit rusty.
For example, I have this builder:
class Person
PROPERTIES = [:name, :age]
attr_accessor(*PROPERTIES)
def initialize(**kwargs)
kwargs.each do |k, v|
self.send("#{k}=", v) if self.respond_to?(k)
end
end
def build
output = {}
PROPERTIES.each do |prop|
if self.respond_to?(prop) and !self.send(prop).nil?
value = self.send(prop)
# if value itself is a builder, evalute it
output[prop] = value.respond_to?(:build) ? value.build : value
end
end
output
end
def method_missing(m, *args, &block)
if m.to_s.start_with?("set_")
mm = m.to_s.gsub("set_", "")
if PROPERTIES.include?(mm.to_sym)
self.send("#{mm}=", *args)
return self
end
end
end
end
Which can be used like so:
Person.new(name: "Joe").set_age(30).build
# => {name: "Joe", age: 30}
I would like to be able to refactor everything to a class and/or module so that I could create multiple such builders that'll only need to define attributes and inherit or include the rest (and possibly extend each other).
class BuilderBase
# define all/most relevant methods here for initialization,
# builder attributes and object construction
end
module BuilderHelper
# possibly throw some of the methods here for better scope access
end
class Person < BuilderBase
include BuilderHelper
PROPERTIES = [:name, :age, :email, :address]
attr_accessor(*PROPERTIES)
end
# Person.new(name: "Joe").set_age(30).set_email("joe#mail.com").set_address("NYC").build
class Server < BuilderBase
include BuilderHelper
PROPERTIES = [:cpu, :memory, :disk_space]
attr_accessor(*PROPERTIES)
end
# Server.new.set_cpu("i9").set_memory("32GB").set_disk_space("1TB").build
I've been able to get this far:
class BuilderBase
def initialize(**kwargs)
kwargs.each do |k, v|
self.send("#{k}=", v) if self.respond_to?(k)
end
end
end
class Person < BuilderBase
PROPERTIES = [:name, :age]
attr_accessor(*PROPERTIES)
def build
...
end
def method_missing(m, *args, &block)
...
end
end
Trying to extract method_missing and build into the base class or a module keeps throwing an error at me saying something like:
NameError: uninitialized constant BuilderHelper::PROPERTIES
OR
NameError: uninitialized constant BuilderBase::PROPERTIES
Essentially the neither the parent class nor the mixin are able to access the child class' attributes. For the parent this makes sense, but not sure why the mixin can't read the values inside the class it was included into. This being Ruby I'm sure there's some magical way to do this that I have missed.
Help appreciated - thanks!
I reduced your sample to the required parts and came up with:
module Mixin
def say_mixin
puts "Mixin: Value defined in #{self.class::VALUE}"
end
end
class Parent
def say_parent
puts "Parent: Value defined in #{self.class::VALUE}"
end
end
class Child < Parent
include Mixin
VALUE = "CHILD"
end
child = Child.new
child.say_mixin
child.say_parent
This is how you could access a CONSTANT that lives in the child/including class from the parent/included class.
But I don't see why you want to have this whole Builder thing in the first place. Would an OpenStruct not work for your case?
Interesting question. As mentioned by #Pascal, an OpenStruct might already do what you're looking for.
Still, it might be more concise to explicitly define the setter methods. It might also be clearer to replace the PROPERTIES constants by methods calls. And since I'd expect a build method to return a complete object and not just a Hash, I renamed it to to_h:
class BuilderBase
def self.properties(*ps)
ps.each do |property|
attr_reader property
define_method :"set_#{property}" do |value|
instance_variable_set(:"##{property}", value)
#hash[property] = value
self
end
end
end
def initialize(**kwargs)
#hash = {}
kwargs.each do |k, v|
self.send("set_#{k}", v) if self.respond_to?(k)
end
end
def to_h
#hash
end
end
class Person < BuilderBase
properties :name, :age, :email, :address
end
p Person.new(name: "Joe").set_age(30).set_email("joe#mail.com").set_address("NYC").to_h
# {:name=>"Joe", :age=>30, :email=>"joe#mail.com", :address=>"NYC"}
class Server < BuilderBase
properties :cpu, :memory, :disk_space
end
p Server.new.set_cpu("i9").set_memory("32GB").set_disk_space("1TB").to_h
# {:cpu=>"i9", :memory=>"32GB", :disk_space=>"1TB"}
I think no need to declare PROPERTIES, we can create a general builder like this:
class Builder
attr_reader :build
def initialize(clazz)
#build = clazz.new
end
def self.build(clazz, &block)
builder = Builder.new(clazz)
builder.instance_eval(&block)
builder.build
end
def set(attr, val)
#build.send("#{attr}=", val)
self
end
def method_missing(m, *args, &block)
if #build.respond_to?("#{m}=")
set(m, *args)
else
#build.send("#{m}", *args, &block)
end
self
end
def respond_to_missing?(method_name, include_private = false)
#build.respond_to?(method_name) || super
end
end
Using
class Test
attr_accessor :x, :y, :z
attr_reader :w, :u, :v
def set_w(val)
#w = val&.even? ? val : 0
end
def add_u(val)
#u = val if val&.odd?
end
end
test1 = Builder.build(Test) {
x 1
y 2
z 3
} # <Test:0x000055b6b0fb2888 #x=1, #y=2, #z=3>
test2 = Builder.new(Test).set(:x, 1988).set_w(6).add_u(2).build
# <Test:0x000055b6b0fb23b0 #x=1988, #w=6>
I'm trying to create a method that passes the caller as the default last argument. According to this, I only need:
class A
def initialize(object = self)
# work with object
end
end
so that in:
class B
def initialize
A.new # self is a B instance here
end
end
self will be B rather than A;
However, this doesn't seem to work. Here's some test code:
class A
def self.test test, t=self
puts t
end
end
class B
def test test,t=self
puts t
end
end
class T
def a
A.test 'hey'
end
def b
B.new.test 'hey'
end
def self.a
A.test 'hey'
end
def self.b
B.new.test'hey'
end
end
and I get:
T.new.a # => A
T.new.b # => #<B:0x000000015fef00>
T.a # => A
T.b # => #<B:0x000000015fed98>
whereas I expect it to be T or #<T:0x000000015fdf08>. Is there a way to set the default last argument to the caller?
EDIT:
class Registry
class << self
def add(component, base=self)
self.send(component).update( base.to_s.split('::').last => base)
end
end
end
The idea is pretty simple, you would use it like this
class Asset_Manager
Registry.add :utilities
end
and you access it like:
include Registry.utilities 'Debugger'
I'm trying to de-couple classes by having a middle-man management type class that takes care of inter-class communications, auto-loading of missing classes and erroring when it doesn't exist, it works but I just want to be able to use the above rather than:
class Asset_Manager
Registry.add :utilities, self
end
It just feels cleaner, that and I wanted to know if such a thing was possible.
You can't escape the explicit self. But you can hide it with some ruby magic.
class Registry
def self.add(group, klass)
puts "registering #{klass} in #{group}"
end
end
module Registrable
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def register_in(group)
Registry.add(group, self)
end
end
end
class AssetManager
include Registrable
register_in :utilities
end
# >> registering AssetManager in utilities
In short, you can't.
Ruby resolves the default arguments in the context of the receiver. That is, the object before the . in a method call. What you called the receiver should be the caller, actually.
class A
def test1(value = a)
puts a
end
def test2(value = b)
puts b
end
def a
"a"
end
end
a = A.new
a.test1 #=> a
def a.b; "b" end
a.test2 #=> b
If I were you, I would use the extended (or included) hook, where both the extending class and the extended module can be accessed. You can program what ever logic you want based on the information.
module Registry
module Utilities
def self.extended(cls)
#puts cls
::Registry.send(component).update( cls.to_s.split('::').last => cls)
end
end
end
class Asset_Manager
extend Registry::Utilities
end
I have an issue that seems to be specific to my test suite.
I have a module that holds some defaults in a constant like so:
module MyModule
DEFAULTS = {
pool: 15
}
def self.options
#options ||= DEFAULTS
end
def self.options=(opts)
#options = opts
end
end
module MyModule
class MyClass
def options
MyModule.options
end
def import_options(opts)
MyModule.options = opts
end
end
end
I allow the program to boot with no options or a user can specify options. If no options are given, we use the defaults but if options are given we use that instead. An example test suite looks like this:
RSpec.describe MyModule::MyClass do
context "with deafults" do
let(:my) { MyModule::MyClass.new }
it 'has a pool of 15' do
expect(my.options[:pool]).to eq 15
end
end
context "imported options" do
let(:my) { MyModule::MyClass.new }
it 'has optional pool size' do
my.import_options(pool: 30)
expect(my.options[:pool]).to eq 30
end
end
end
If those tests run in order, great, everything passes. If it runs in reverse (where the second test goes first), the first test gets a pool size of 30.
I don't have a 'real world' scenario where this would happen, the program boots once and that's it but I'd like to test for this accordingly. Any ideas?
#options is a class variable in that module. I'm not sure that is technically the right name for it, but that's how it is acting. As an experiment, print out #options.object_id right before you access it in self.options. Then run your tests. You'll see that in both cases it prints out the same id. This is why when your tests are flipped you get 30. #options is already defined so #options ||= DEFAULTS is not setting #options to DEFAULTS.
$ cat foo.rb
module MyModule
DEFAULTS = {
pool: 15
}
def self.options
puts "options_id: #{#options.object_id}"
#options ||= DEFAULTS
end
def self.options=(opts)
#options = opts
end
end
module MyModule
class MyClass
def options
MyModule.options
end
def import_options(opts)
MyModule.options = opts
end
end
end
puts "pool 30"
my = MyModule::MyClass.new
my.import_options(pool: 30)
my.options[:pool]
puts
puts "defaults"
my = MyModule::MyClass.new
my.options[:pool]
And running it...
$ ruby foo.rb
pool 30
options_id: 70260665635400
defaults
options_id: 70260665635400
You could always use a before(:each)
before(:each) do
MyModule.options = MyModule::DEFAULTS
end
Side note - maybe a class for the configuration.
Something like:
module MyModule
class Configuration
def initialize
#foo = 'default'
#bar = 'default'
#baz = 'default'
end
def load_from_yaml(path)
# :)
end
attr_accessor :foo, :bar, :baz
end
end
And then you could add something like this:
module MyModule
class << self
attr_accessor :configuration
end
# MyModule.configure do |config|
# config.baz = 123
# end
def self.configure
self.configuration ||= Configuration.new
yield(configuration)
end
end
And finally you will reset the configuration in a more meaningful manner
before(:each) do
MyModule.configuration = MyModule::Configuration.new
end
There has to be a better way to do this:
require 'log4r'
class PaddedLogger
attr_accessor :logger, :padding
def initialize(args)
#logger = Log4r::Logger.new args[:name]
#padding = args[:padding]
end
def debug(system, message)
system = "[#{system}]"
#logger.debug "#{system.ljust(#padding)}#{message}"
end
def info(system, message)
system = "[#{system}]"
#logger.info "#{system.ljust(#padding)}#{message}"
end
def warn(system, message)
system = "[#{system}]"
#logger.warn "#{system.ljust(#padding)}#{message}"
end
def error(system, message)
system = "[#{system}]"
#logger.error "#{system.ljust(#padding + 3)}#{message}"
end
def fatal(system, message)
system = "[#{system}]"
#logger.fatal "#{system.ljust(#padding + 3)}#{message}"
end
end
It's just a wrapping class to help me get my log4r logs appearing in a specific format, and it works exactly as I need it too, but surely Ruby's meta-programming magic can simplify this so I don't have to constantly repeat myself.
Something like this maybe?
require 'log4r'
class PaddedLogger
attr_accessor :logger, :padding
def initialize(args)
#logger = Log4r::Logger.new args[:name]
#padding = args[:padding]
end
[:debug, :info, :warn, :error, :fatal].each do |reason|
define_method reason do |system, message|
system = "[#{system}]"
#logger.send reason, "#{system.ljust(#padding)}#{message}"
end
end
end
And while you're at it, why not collapse the two lines in the define_method block to one?
#logger.send reason, "[#{system}]".ljust(#padding) + message
Here is and alternative solution using method_missing:
class TestClass
##methods = [:upcase, :other_methods_to_catch]
def initialize
#message = 'hello'
end
def method_missing name, *args
super unless ##methods.include? name
#message.send(name, *args)
end
end
x = TestClass.new
p x.upcase #=> HELLO
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.