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
end
Namespaces 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 # => 1
Less Verbose Definitions
As an alternative to the above definition syntax, you can use a less-verbose syntax:
-
opts
foroptions
-
opt
foroption
-
ns
fornamespace
require 'ns-options'
module App
include NsOptions
opts :settings do
opt :root, Pathname
opt :stage
ns :other_stuff do
opt :something
end
end
end
Dynamically 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 #=> 1
Mass 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 #=> 1
To 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 instance
A 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 #=> false
Options
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
end
Understanding 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 = nil
For 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 #=> MyCustomFixNum
By 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 array
Rules
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? #=> true
To 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 #=> 456
If 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|
...
end
While 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
end
Proxy 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 #=> 2
You 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.5
And 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.5
Installation
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