NsOptions
A Ruby DSL for defining, organizing and accessing options. Use namespaces to organize options. Read and write option values using accessors.
Usage
require 'ns-options'
module App
include NsOptions
options(:settings) do
option :root, Pathname
option :stage
end
end
App.settings.root = "/a/path/to/the/root"
App.settings.root.join("log", "test.log") #=> "/a/path/to/the/root/log/test.log" (a Pathname instance)
App.settings.stage = "development"
App.settings.stage #=> "development"The code above defines a settings reader on App. The options can be read and written to using their accessors
Namespaces
options(:settings) do
namespace :grouped_stuff do
option :something
option :something_else
end
endNamespaces allow you to organize your options. You access the namespace and its options through their accessors.
App.settings.grouped_stuff.something = 1
App.settings.grouped_stuff.something # => 1Less Verbose Definitions
As an alternative to the above definition syntax, you can use a less-verbose syntax:
-
optsforoptions -
optforoption -
nsfornamespace
require 'ns-options'
module App
include NsOptions
opts :settings do
opt :root, Pathname
opt :stage
ns :other_stuff do
opt :something
end
end
endDynamically Defined Options
Not all options have to be defined formally ahead of time. You can write any option value you like at any time.
App.settings.a_value #=> NoMethodError: undefined method `a_value'...
App.settings.a_value = 1
App.settings.a_value #=> 1Mass Assigning Options
Sometimes, it's convenient to be able to set many options at once. This can be done by calling the apply method and giving it a hash of option names with values. You can even give it keys that aren't pre-defined options - new options will be created for them
App.settings.apply({
:root => "/path/to/project",
:stage => "development"
:new_value => 1
})
App.settings.root #=> "/path/to/project"
App.settings.stage #=> "development"
App.settings.new_value #=> 1To get a hash of values for a namespace, just call its to_hash method.
Class Behavior
Using NsOptions on a Class uses namespaces to create separate sets of options for every instance of your class. This different instances to have different options values but share the same definition.
To illustrate:
class User
include NsOptions
options(:preferences) do
option :home_page
end
end
User.preferences # => NsOptions::Namespace instanceA preferences namespace is created for the User class. For each instiance of User created, NsOptions will setup an identical copy of their class's namespace. However, each instance sets and maintains unique option values.
user1 = User.new
user1.preferences.home_page = "/home"
user2 = User.new
user2.preferences.home_page = "/not_home"
user.preferences.home_page == user2.preferences.home_page #=> falseOptions
Type Classes
Options can be defined with a given "type class". If none is specified, Object is used.
options :settings do
option :opt1
option :opt2, MyCustomTypeClass
endUnderstanding what NsOptions will do with your type class is important. First, option values will be cast to your type class. If you write a value that is not of a matching type, NsOptions will try to coerce the value.
# no type coercion is done here, the value is of the right type
settings.opt2 = MyCustomTypeClass.new(123)
class BetterCustomTypeClass < MyCustomTypeClass; end
# again, no type coercion is done, as BetterCustomTypeClass is a kind of MyCustomTypeClass
settings.opt2 = BetterCustomTypeClass.new(456)
# here, type coercion is performed
# this is the equivalent of doing: `settings.opt2 = MyCustomTypeClass.new(789)`
settings.opt2 = 789
# nil is never coerced, if you set a value to nil, it's just nil
App.setting.stage = nilFor type coercion to work, your type class's initializer must work given only a single argument.
Default Type Classes
class MyCustomFixNum < Struct.new(:num); end
options :settings do
option_type_class Fixnum
option :opt1, :default => 1
option :opt2, :default => 2
option :opt3, MyCustomFixNum, :default => 3
end
settings.opt1.class #=> Fixnum
settings.opt3.class #=> Fixnum
settings.opt3.class #=> MyCustomFixNumBy default, NsOptions will use Object for an option's type class if none is specified. You can override this on a per-namespaces basis using the option_type_class method. Call this and all options will be defined using the given class by default.
Note, this setting applies recursively, so all child namespaces honor it as well. You can override this by specifying a new type class on your child namespaces.
# you can use an abbreviated syntax
#...
options :settings do
opt_type_class Fixnum
option :opt1, :default => 1
#...
# you can also pass in the option type class when defining the ns
#...
options :settings, Fixnum do
option :opt1, :default => 1
#...Ruby Classes As A Type Class
NsOptions will allow you to use many of Ruby's standard objects as type classes and still handle coercing values appropriately.
module Example
include NsOptions
options :stuff do
option :string, String
option :integer, Integer
option :float, Float
option :symbol, Symbol
option :hash, Hash
option :array, Array
end
end
Example.stuff.string = 1
Example.stuff.string #=> "1", the same as doing String(1)
Example.stuff.integer = 5.0
Example.stuff.integer # => 5, this time it's Integer(5.0)
Example.stuff.float = "5.0"
Example.stuff.float #=> 5.0, same as Float("5.0")
Example.stuff.symbol = "awesome"
Example.stuff.symbol #=> :awesome
Example.stuff.hash = { :a => 'b' }
Example.stuff.hash # => returns the same hash
Example.stuff.array = [ 1, 2, 3 ]
Example.stuff.array # => returns the same arrayRules
An option can be defined with certain rules that extend the behavior of the option.
Default
settings do
option :opt1, :default => "development"
end
settings.opt1 #=> 'development'
settings.opt1 = 'production' #=> 'production'A default value runs through the same logic as if you set the value manually, so it will be coerced if necessary.
Required
settings do
option :opt1, :required => true
end
settings.required_set? #=> false
settings.root = "/path/to/somewhere"
settings.required_set? #=> trueTo check if an option is set it will simply check if the value is not nil. If you are using a custom type class though, you can define an is_set? method and this will be used to check if an option is set.
The built in required_set? method checks to see if all the options for the namespace that have been marked :required => true are set. It will recursively check any sub-namespaces.
Args
Another rule that you can specify is args.
class MyCustomTypeClass
def initialize(value, arg1, arg2); end
end
settings do
option :opt1, MyCustomTypeClass, :args => lambda{ ["arg 1's value", "arg 2's value"] }
end
# equivalent to: `settings.opt1 = MyCustomTypeClass.new("a value", "arg 1's value", "arg 2's value")
settings.opt1 = 'a value'This allows you to pass additional arguments when coercing option values. The first argument will always be the value to coerce. Any additional arguments will be appended on after the value when calling the initializer.
Lazily eval'd options
Sometimes, you may want to set an option to a value that shouldn't be evaluated until the option is read. If you set an option equal to a Proc, the value of the option will be whatever the return value of the Proc is at the time the option is read.
Here are some examples:
# dynamic value
options(:dynamic) do
option :rand, :default => Proc.new { rand(1000) }
end
dynamic.rand #=> 347
dynamic.rand #=> 529
# self referential value
options(:selfref) do
option :something, :default => "123"
option :else, :default => Proc.new { self.something }
end
selfref.something #=> "123"
selfref.else #=> "123"
selfref.something = 456
selfref.else #=> 456If you really want your option to read and write Procs and not do this lazy eval behavior, just define the option with a Proc type class.
options(:explicit) do
option :a_proc, Proc, :default => Proc.new { rand(1000) }
end
explicit.a_proc #=> <the proc obj>NsOptions::Proxy
Mix in NsOptions::Proxy to any module/class to make it proxy a namespace. This essentially turns your receiver into a namespace - you can interact with it just as if it were a namespace object. For example:
module Something
include NsOptions::Proxy
# define options directly
option :foo
option :bar, :default => "Bar"
# define sub-namespaces
namespace :more do
option :another
end
end
# handle those options
Something.bar #=> "Bar"
Something.to_hash #=> {:foo => nil, :bar => "Bar"}
Something.each do |opt_name, opt_value|
...
endWhile your Something behaves like a namespace, you can still define methods and add to it just as you would normally in Ruby:
module Something
def self.awesome_bar
"Awesome #{bar}"
end
end
Something.awesome_bar # => "Awesome Bar"And remember, NsOptions is mixed in, so you can go ahead and create a root namespace as you normally would:
module Something
options(:else) do
option :baz
end
endProxy initialization
Mixing in Proxy will add a default initializer for you as well. This initializer allows you to call new on your proxy, passing it a hash of key-values. These key values will be applied to the proxy using the Namespace#apply logic. This allows you to use Proxy objects as option types and maintain the option type-casting and defaulting behavior.
module Things
include NsOptions::Proxy
option :one
option :two
end
# proxy defines a `new` method that takes a hash arg and
# applies it to the proxy
t = Thing.new(:one => 1, :two => 2, :three => 3)
# the values have been applied
t.to_hash # => {:one => 1, :two => 2, :three => 3}NsOptions::Struct
Much like a traditional ruby Struct, NsOptions::Struct is a class that creates other classes. However, NsOptions::Struct creates proxy classes. There are a number of ways to do this:
NsOptions::Struct.new #=> #<#<Class:0x1076166d0>:#<NsOptions::
Thing = NsOptions::Struct.new #=> #<Thing:#<NsOptions::
Thing = Class.new(NsOptions::Struct.new) #=> #<Thing:#<NsOptions::
class Thing < NsOptions::Struct.new; end #=> #<Thing:#<NsOptions::NsOptions::Struct objects, being proxies, can be created with a hash of values and support dynamic writers (much like an OpenStruct).
Thing = NsOptions::Struct.new
thing = Thing.new(:something => 1)
thing.something #=> 1
thing.otherthing #=> NoMethodError
thing.otherthing = 2
thing.otherthing #=> 2You can pre-define the structure, including default values and type-casting info.
Thing = NsOptions::Struct.new do
option :something, Integer, :default => 1
option :otherthing, String
end
thing = Thing.new(:yet_another => 12.5)
thing.something #=> 1
thing.otherthing #=> nil
thing.otherthing = 2
thing.otherthing #=> '2'
thing.yet_another #=> 12.5And since struct classes are proxies, you don't have to create instances of them if you don't need to.
thing = NsOptions::Struct.new(:yet_another => 12.5) do
option :something, Integer, :default => 1
option :otherthing, String
end
thing.something #=> 1
thing.otherthing #=> nil
thing.otherthing = 2
thing.otherthing #=> '2'
thing.yet_another #=> 12.5Installation
Add this line to your application's Gemfile:
gem 'ns-options'
And then execute:
$ bundle
Or install it yourself as:
$ gem install ns-options
Contributing
- Fork it
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Added some feature') - Push to the branch (
git push origin my-new-feature) - Create new Pull Request