Thor Reading config yaml file to override options - ruby

I am trying to create a executable ruby script using Thor.
I have defined the options for my task. So far I have something like this
class Command < Thor
desc "csv2strings CSV_FILENAME", "convert CSV file to '.strings' file"
method_option :langs, :type => :hash, :required => true, :aliases => "-L", :desc => "languages to convert"
...
def csv2strings(filename)
...
end
...
def config
args = options.dup
args[:file] ||= '.csvconverter.yaml'
config = YAML::load File.open(args[:file], 'r')
end
end
When csv2strings is called without arguments, I would like the config task to be invoked, which would set the option :langs.
I haven't yet found a good way to do it.
Any help will be appreciated.

I think you are looking for a way to set configuration options via the command line and via a configuration file.
Here is an example from the foreman gem.
def options
original_options = super
return original_options unless File.exists?(".foreman")
defaults = ::YAML::load_file(".foreman") || {}
Thor::CoreExt::HashWithIndifferentAccess.new(defaults.merge(original_options))
end
It overrides the options method and merges values from a configuration file into the original options hash.
In your case, the following might work:
def csv2strings(name)
# do something with options
end
private
def options
original_options = super
filename = original_options[:file] || '.csvconverter.yaml'
return original_options unless File.exists?(filename)
defaults = ::YAML::load_file(filename) || {}
defaults.merge(original_options)
# alternatively, set original_options[:langs] and then return it
end
(I recently wrote a post about Foreman on my blog that explains this in more detail.)

Related

ruby clone an object

I need to clone an existing object and change that cloned object.
The problem is that my changes change original object.
Here's the code:
require "httparty"
class Http
attr_accessor :options
attr_accessor :rescue_response
include HTTParty
def initialize(options)
options[:path] = '/' if options[:path].nil? == true
options[:verify] = false
self.options = options
self.rescue_response = {
:code => 500
}
end
def get
self.class.get(self.options[:path], self.options)
end
def post
self.class.post(self.options[:path], self.options)
end
def put
self.class.put(self.options[:path], self.options)
end
def delete
self.class.put(self.options[:path], self.options)
end
end
Scenario:
test = Http.new({})
test2 = test
test2.options[:path] = "www"
p test2
p test
Output:
#<Http:0x00007fbc958c5bc8 #options={:path=>"www", :verify=>false}, #rescue_response={:code=>500}>
#<Http:0x00007fbc958c5bc8 #options={:path=>"www", :verify=>false}, #rescue_response={:code=>500}>
Is there a way to fix this?
You don't even need to clone here, you just need to make a new instance.
Right here:
test = Http.new({})
test2 = test
you don't have two instances of Http, you have one. You just have two variables pointing to the same instance.
You could instead change it to this, and you wouldn't have the problem.
test = Http.new({})
test2 = Http.new({})
If, however, you used a shared options argument, that's where you'd encounter an issue:
options = { path: nil }
test = Http.new(options)
# options has been mutated, which may be undesirable
puts options[:path] # => "/"
To avoid this "side effect", you could change the initialize method to use a clone of the options:
def initialize(options)
options = options.clone
# ... do other stuff
end
You could also make use of the splat operator, which is a little more cryptic but possibly more idiomatic:
def initialize(**options)
# do stuff with options, no need to clone
end
You would then call the constructor like so:
options = { path: nil }
test = Http.new(**options)
puts test.options[:path] # => "/"
# the original hasn't been mutated
puts options[:path] # => nil
You want .clone or perhaps .dup
test2 = test.clone
But depending on your purposes, but in this case, you probably want .clone
see What's the difference between Ruby's dup and clone methods?
The main difference is that .clone also copies the objects singleton methods and frozen state.
On a side note, you can also change
options[:path] = '/' if options[:path].nil? # you don't need "== true"

Ruby Access Class Variables Easily

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']

Ruby, properly error handling class constructor options

I am working on an application in Ruby, and I am trying to write a clean and efficient API and would need properly error handle options passed to a class constructor.
For instance:
class SomeClass
def initialize(options = {})
#some_opt = options[:some_opt]
#some_other_opt = options[:some_other_opt]
end
end
sc = SomeClass.new(:some_opt => 'foo', :some_other_opt => 'bar')
How would I make sure, that if the user adds an option wich the application does not accept, the application will raise an error?
sc = SomeClass.new(:some_opt => 'foo', :some_new_opt => 'foobar') # => Unknown option 'some_new_opt'
Would it be better to only use the options that you need to, and disregard any other options passed to the class?
For Ruby versions 1.8 and 1.9 disregarding the extra options is generally how that situation is handled. You can raise an error if you want, but I wouldn't recommend it:
require "set"
class SomeClass
ACCEPTED_KEYWORDS = Set[:some_opt, :some_other_opt]
def initialize(options = {})
raise ArgumentError unless options.keys.all? {|k| ACCEPTED_KEYWORDS.include? k }
# Rest of the code...
end
end
For Ruby version 2.0 and above you can use keyword arguments, and you can make them required from version 2.1 onwards.

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?.

Commands in ruby terminal application

I have just written my first terminal application in ruby. I use OptionParser to parse the options and their arguments. However I want to create commands. For example:
git add .
In the above line, add is the command which cannot occur anywhere else than immediately after the application. How do I create these.
I will appreciate if anyone could point me in the right direction. However, please do not reference any gems such as Commander. I already know about these. I want to understand how it is done.
The OptionParser's parse! takes an array of arguments. By default, it will take ARGV, but you can override this behaviour like so:
Basic Approach
def build_option_parser(command)
# depending on `command`, build your parser
OptionParser.new do |opt|
# ...
end
end
args = ARGV
command = args.shift # pick and remove the first option, do some validation...
#options = build_option_parser(command).parse!(args) # parse the rest of it
Advanced Approach
Instead of a build_option_parser method with a huge case-statement, consider an OO approach:
class AddCommand
attr_reader :options
def initialize(args)
#options = {}
#parser = OptionParser.new #...
#parser.parse(args)
end
end
class MyOptionParser
def initialize(command, args)
#parser = {
'add' => AddCommand,
'...' => DotsCommand
}[command.to_s].new(args)
end
def options
#parser.options
end
end
Alternatives
For sure, there exist tons of Rubygems (well, 20 in that list), which will take care of your problem. I'd like to mention Thor which powers, e.g. the rails command line tool.
You can retrieve the command with Array#shift prior invoking OptionParser.

Resources