Ruby Access Class Variables Easily - ruby

I've created a class that I'm using to store configuration data. Currently the class looks like this:
class Configed
##username = "test#gmail.com"
##password = "password"
##startpage = "http://www.example.com/login"
##nextpage = "http://www.example.com/product"
##loginfield = "username"
##passwordfield = "password"
##parser = "button"
##testpage = "http://www.example.com/product/1"
##button1 = "button1"
def self.username
##username
end
def self.password
##password
end
def self.startpage
##startpage
end
def self.nextpage
##nextpage
end
def self.loginfield
##loginfield
end
def self.passwordfield
##passwordfield
end
def self.parser
##parser
end
def self.testpage
##testpage
end
def self.button1
##button1
end
end
To access the variables I'm using:
# Config file
require_relative 'Configed'
# Parse config
startpage = Configed.startpage
loginfield = Configed.loginfield
passwordfield = Configed.passwordfield
username = Configed.username
password = Configed.password
nextpage = Configed.nextpage
parser = Configed.parser
testpage = Configed.testpage
This is not very modular. Adding additional configuration data needs to be referenced in three places.
Is there a better way of accomplishing this?

You can make class level instance variables...
class Configed
#username = "test#gmail.com"
#password = "password"
#startpage = "http://www.example.com/login"
# ...
class << self
attr_reader :username, :password, :startpage # ...
end
end
It's somewhat more compact, and still gives you
username = Configed.username
# ...
NOTE: there's a lot of good ideas in #philomory 's answer that deserves consideration. The use of YAML in particular would allow you to set up different constants for different environemnts test, development, production etc, and you can load the current environment's configuration options into an OpenStruct created in an initializer. Makes for a more flexible solution.

There are a lot of potential improvements. First of all, no reason to use class variables if you don't want their weird specific inheritance-related behavior, and no reason to use a class at all if you're not going to instantiate it.
You could use a module:
module Configed
module_function
def username
'username'
end
# etc
end
Configed.username
But frankly, you're almost certainly better off using a hash:
Config = {
username: 'username'
# etc
}.freeze
Config[:username]
or, if you prefer method-style access, an OpenStruct:
require 'openstruct' # from standard library
Config = OpenStruct.new(
username: 'username'
# etc
).freeze
Config.username
If they need to be modifiable, just don't freeze them. Also, typically a constant which is not a class or a module (such as a hash) would have a name in ALL_CAPS, e.g. CONFIGED, but, that's a stylistic decision with no actual impact on the code.
Your question refers to 'parsing' the config, but of course, you're not; the config data in your setup (and in my examples so far) is just Ruby code. If you'd rather load it from a non-code file, there's always YAML:
config.yaml:
username: username
password: password
config.rb:
require 'yaml' # from Standard Library
Configed = YAML.load_file('config.yaml')
Configed['username']
or JSON:
config.json:
{
"username": "username",
"password": "password"
}
config.rb:
require 'json' # from Standard Library
Configed = JSON.parse(File.read('config.json'))
Configed['username']

Related

Using YAML data in Ruby App

I want to load data from my YAML file and use the it in my ruby application. The contents available in YAML will be used in different places. Therefore, I want to read it initially and use the data whenever required.
I would like to know what is the best practice for this?
What I tried is
config.yaml
db:
username: admin
password: admin
config.rb
class Config
class << self
attr_accessor :uname, :pwd
def load
config_data = YAML.load_file("c:\config.yml")
#uname = config_data['db']['username']
#pwd = config_data['db']['password']
end
end
end
my_app.rb
Config.load
puts Config.uname
puts Config.pwd
Please let me know whether this is a right way to load and use YAML data. If not please share the best practice.
It hardly depends on your intentions, but generally speaking, yes, it is mostly correct approach. What do you want to do more, is to cache once read data:
class Config
class << self
attr_accessor :uname, :pwd
def load force_reload = false
return #config_data if #config_data && !force_reload
#config_data = YAML.load_file("c:\config.yml")
#uname = #config_data['db']['username']
#pwd = #config_data['db']['password']
end
end
end
Here we return immediately, if the #config was already loaded, allowing fast subsequent calls to Config.config.
Whether one wants to explicitly reload the config, it should pass the parameter to load:
Config.load true # true means reload YAML explicitly

How do I dynamically set object attributes from a YAML file?

I have a YAML file which maps a bunch of properties to a class. I'd like to be able to loop through all of the properties from my YAML file and set them on an object dynamically.
How can I do this?
Here's the gist of what I have so far:
YAML contents:
my_obj:
:username: 'myuser'
:password: 'mypass'
...
Ruby:
settings = YAML::load_file SETTINGS_FILE
settings = settings['my_obj']
settings.each do |s|
#psuedo-code
#example: my_obj.username = settings[:username] if my_obj.has_property?(:username)
#my_obj.[s] = settings[:s] if my_obj.has_property?(:s)
end
I'm not sure if doing this is necessarily a best practice, but there are a lot of properties and I thought this would be a cleaner way than manually setting each property directly.
Depending on what you actually need, you may be able to use Ruby’s default YAML dump and load behaviour. When dumping an arbitrary object to YAML, Psych (the Yaml parser/emitter in Ruby) will look at all the instance variables of the object and write out a Yaml mapping with them. For example:
class MyObj
def initialize(name, password)
#name = name
#password = password
end
end
my_obj = MyObj.new('myuser', 'mypass')
puts YAML.dump my_obj
will print out:
--- !ruby/object:MyObj
name: myuser
password: mypass
You can then load this back with YAML.load and you will get an instance of MyObj with the same instance variables as your original.
You can override or customise this behaviour by implementing encode_with or init_with methods on your class.
As per your sample yaml file content,you would get the below Hash.
require 'yaml'
str = <<-end
my_obj:
:username: 'myuser'
:password: 'mypass'
end
settings = YAML.load(str)
# => {"my_obj"=>{:username=>"myuser", :password=>"mypass"}}
settings['my_obj'][:username] # => "myuser"
settings['my_obj'][:password] # => "mypass"
You'll see this used a lot in Ruby gems and Rails. Here's a dummy class:
require 'yaml'
class MyClass
attr_accessor :username, :password
def initialize(params)
#username, #password = params.values_at(*[:username, :password])
end
end
We can load the data in using YAML's load_file:
data = YAML.load_file('test.yaml') # => {"my_obj"=>{:username=>"myuser", :password=>"mypass"}}
Passing the structure just read, which in this case is a hash, and which, oddly enough, or maybe even magically, is what the class initializer takes, creates an instance of the class:
my_class = MyClass.new(data['my_obj'])
We can see the instance variables are initialized correctly from the contents of the YAML file:
my_class.username # => "myuser"
my_class.password # => "mypass"
This appears to be working. It allows me to set attributes on my_obj dynamically from the YAML settings hash.
settings.each do |key, value|
my_obj.instance_variable_set("##{key}", value) if my_obj.method_defined?("#{key}")
end
my_obj has attr_accessor :username, :password, etc.
The snippet above appears to let me set these attributes dynamically using instance_variable_set, while also ensuring that my_obj has the attribute I'm trying to assign with method_defined?.

Access module Configuration constant from another class

I am trying to understand how the following code is able to do this:
attr_accessor *Configuration::VALID_CONFIG_KEYS
Without requiring the Configuration file. Here is part of the code:
require 'openamplify/analysis/context'
require 'openamplify/connection'
require 'openamplify/request'
module OpenAmplify
# Provides access to the OpenAmplify API http://portaltnx20.openamplify.com/AmplifyWeb_v20/
#
# Basic usage of the library is to call supported methods via the Client class.
#
# text = "After getting the MX1000 laser mouse and the Z-5500 speakers i fell in love with logitech"
# OpenAmplify::Client.new.amplify(text)
class Client
include OpenAmplify::Connection
include OpenAmplify::Request
attr_accessor *Configuration::VALID_CONFIG_KEYS
def initialize(options={})
merged_options = OpenAmplify.options.merge(options)
Configuration::VALID_CONFIG_KEYS.each do |key|
send("#{key}=", merged_options[key])
end
end
....
end
And this is the Configuration module:
require 'openamplify/version'
# TODO: output_format, analysis, scoring can be specied in the client and becomes the default unless overriden
module OpenAmplify
# Defines constants and methods for configuring a client
module Configuration
VALID_CONNECTION_KEYS = [:endpoint, :user_agent, :method, :adapter].freeze
VALID_OPTIONS_KEYS = [:api_key, :analysis, :output_format, :scoring].freeze
VALID_CONFIG_KEYS = VALID_CONNECTION_KEYS + VALID_OPTIONS_KEYS
DEFAULT_ENDPOINT = 'http://portaltnx20.openamplify.com/AmplifyWeb_v21/AmplifyThis'
DEFAULT_HTTP_METHOD = :get
DEFAULT_HTTP_ADAPTER = :net_http
DEFAULT_USER_AGENT = "OpenAmplify Ruby Gem #{OpenAmplify::VERSION}".freeze
DEFAULT_API_KEY = nil
DEFAULT_ANALYSIS = :all
DEFAULT_OUTPUT_FORMAT = :xml
DEFAULT_SCORING = :standard
DEFAULT_SOURCE_URL = nil
DEFAULT_INPUT_TEXT = nil
attr_accessor *VALID_CONFIG_KEYS
....
end
This is from this repository: OpenAmplify
First of all, in both configuration.rb and client.rb, they're using the same naming space, which is module OpenAmplify.
Even though configuration.rb is not required in client.rb, the convention of Ruby project usually requires all necessary files in one file (normally the same name as the name space, and placed in {ProjectName}/lib/, in this case the file is openamplify/lib/openamplify.rb).
So if you go to openamplify/lib/openamplify.rb, you'll notice it actually requires all those two files:
require 'openamplify/configuration'
require 'openamplify/client'
And since constants are already defined in configuration.rb:
module OpenAmplify
module Configuration
VALID_CONFIG_KEYS = ...
end
end
Then obviously constant VALID_CONFIG_KEYS is visible in the same module (re-opened by client.rb) by Configuration::VALID_CONFIG_KEYS (and the * in front just means exploding array, because VALID_CONFIG_KEYS is an array of symbols)
module OpenAmplify
class Client
attr_accessor *Configuration::VALID_CONFIG_KEYS
end
end

Ruby not allowing dynamic strings as an argument

I have a class already mapped out and in a database through DataMapper and now I'm trying to make my first resource into the database.
I have a class that handles the form data and file stuff. In that class, I'm creating the first resource with #variables passed in from the params. All other args passed into this resource come from #variables that have values from the form. In this case, #url, the variable in question, is set to a value only a few lines before. Now when I put in the URL:
rec = Post.new(
# more args
:filename_ogg => #url
)
rec.save
This is the killer: Every other line of code in this file is able to access #url, through a global variable ($upload = Upload.new(file)), except for this resource creator. When it comes to saving the resource, it doesn't go through. BUT, when I replace #url with a static string like "RANDOM URL.", it works perfectly. Why?
This had been tested under both MRI 1.9.3 and JRuby 1.6.7.2 (1.9 mode) under Ubuntu 12.04:
# #{user} edited out
class Upload
attr_accessor :file, :filename, :filename_ogg, :status, :title, :desc, :url
def initialize(file)
#file = file
#filename = #file[:filename].gsub(" ", "")
#filename_ogg = "#{#filename}.ogg"
##url = "http://s3.amazonaws.com/#{user}/#{#filename_ogg}"
end
def downandup
# code
end
def convert(file, file_ogg)
# code
end
def upload(file_ogg)
# code
#url = "http://s3.amazonaws.com/#{user}/#{file_ogg}"
# title and desc are accessed through $upload.title/$upload.desc
rec = Post.new(
:title => #title,
:description => #desc,
:author_id => Random.rand(5),
:time_uploaded => Time.now,
:filename_ogg => #url,
:comments_table => Random.rand(10),
)
rec.save
end
end
The file runs through fine, but when it comes for DataMapper to put it in the database, it won't go in, but when replaced with the static string, the data gets stored.

Ruby: Allowing a module to have settings

If I'm building a library in ruby, what's the best way to allow users of the library to set module-wide settings that will be available to all sub classes etc of the library?
A good example would be if I'm writing a library for posting to a webservice:
TheService::File.upload("myfile.txt") # Uploads anonymously
TheService::Settings.login("myuser","mypass") # Or any other similar way of doing this
TheService::File.upload("myfile.txt") # Uploads as 'myuser'
The idea would be that unless TheService::Settings.logout is called then all TheService operations would be conducted under myuser's account.
Any ideas?
store the data in class variables (or static varibales). You can do something like this:
module TheService
class Settings
def self.login(username,password)
##username = username
##password = password
end
def username
##username
end
def password
##password
end
def self.logout
##username = ##password = nil
end
end
end
Now you can access these setting from Everywhere via TheService::Settings.username or TheService::Settings.password

Resources