ns-options 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source 'http://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in ns-options.gemspec
4
+ gemspec
5
+
6
+ gem 'rake', '~>0.9.2'
7
+
8
+ gem 'assert-mocha', :git => "git@github.com:jcredding/assert-mocha.git"
data/README.markdown ADDED
@@ -0,0 +1,151 @@
1
+ # Namespace Options
2
+
3
+ Namespace Options provides a clean interface for defining, organizing and using options. Options are defined through a clean syntax that clearly shows what can be set and read. Options can be organized into namespaces as well. This allows multiple libraries to share common options but still have their own specific options separated. Reading and writing options is as simple as if they were accessors. Furthermore, you can provide your own _type_ class that allows you to provide extended functionality to a simple option.
4
+
5
+ ## Installation
6
+
7
+ Add the following to your Gemfile and `bundle install`:
8
+
9
+ gem 'ns-options'
10
+
11
+ ## Usage
12
+
13
+ ### Defining Options
14
+
15
+ The basic usage of Namespace Options is to be able to define options for a module or class:
16
+
17
+ ```ruby
18
+ module App
19
+ include NsOptions::HasOptions
20
+ options(:settings) do
21
+ option :root, Pathname
22
+ option :stage
23
+ end
24
+ end
25
+ ```
26
+
27
+ The code above makes the `App` module now have options, accessible through the settings method. The options can be read and written to like a normal accessor:
28
+
29
+ ```ruby
30
+ App.settings.root = "/a/path/to/the/root"
31
+ App.settings.root.join("log", "test.log") # => "/a/path/to/the/root/log/test.log" (a Pathname instance)
32
+
33
+ App.settings.stage = "development"
34
+ App.settings.stage # => "development"
35
+ ```
36
+
37
+ Because the `root` option specified `Pathname` as it's type, the option will always return an instance of `Pathname`. Since the `stage` option did not specify a type, it defaulted to a `String`. You can define you're own type classes as well and use them:
38
+
39
+ ```ruby
40
+ class Stage < String
41
+
42
+ def initialize(value)
43
+ super(value.to_s)
44
+ end
45
+
46
+ def development?
47
+ self == "development"
48
+ end
49
+
50
+ end
51
+
52
+
53
+ App.settings.define do
54
+ option :stage, Stage
55
+ end
56
+
57
+ App.settings.stage = "development"
58
+ App.settings.stage.development? # => true
59
+ App.settings.stage = "test"
60
+ App.settings.stage.development? # => false
61
+ ```
62
+
63
+ This allows you to add extended functionality to your options. The only condition is that the `initialize` accepts a single argument. This argument is always the value that was used with the writer. For the above, the `Stage` class received `"development"` and `"test"` in it's initialize method.
64
+
65
+ ### Namespaces
66
+
67
+ Namespaces allow you to organize and share options. With the previously mentioned `App` module and it's options you could create a namespace for another library:
68
+
69
+ ```ruby
70
+ module Data # data is a library for retrieving persisted data from some resource
71
+
72
+ App.settings.namespace(:data) do
73
+ option :server
74
+ end
75
+
76
+ def self.config
77
+ App.settings.data
78
+ end
79
+
80
+ end
81
+ ```
82
+
83
+ Now I can set a server option for data that is separate from the main `App` settings:
84
+
85
+ ```ruby
86
+ Data.config.server = "127.0.0.1:1234"
87
+
88
+ App.server # => NoMethodError
89
+ ```
90
+
91
+ Since the data namespace was created from the `App` settings (which is also a namespace) it can access it's parent's options:
92
+
93
+ ```ruby
94
+ Data.config.stage # => "test", or whatever App.settings.stage would return
95
+ ```
96
+
97
+ Namespaces and their ability to read their parent's options is internally used by Namespace Options. When you add options to a class like so:
98
+
99
+ ```ruby
100
+ class User
101
+ include NsOptions::HasOptions
102
+ options(:preferences) do
103
+ option :home_page
104
+ end
105
+
106
+ end
107
+ ```
108
+
109
+ A namespace will be created for the `User` class itself. Which can have options added and even set. Once a user instance is created, it will create a child namespace from the classes. Thus, it will be able to access and use any options on the class:
110
+
111
+ ```ruby
112
+ user = User.new
113
+ user.preferences.home_page = "/home"
114
+ ```
115
+
116
+ ### Dynamically Defined Options
117
+
118
+ Not all options have to be defined formally. Though defining an option through the `option` method allows the most functionality and allows for quickly seeing what can be used, Namespace Options allows you to write options that have not been defined:
119
+
120
+ ```ruby
121
+ App.settings.logger = Logger.new(App.settings.root.join("log", "test.log"))
122
+
123
+ App.settings.logger.info("Hello World")
124
+ ```
125
+
126
+ Writing to a namespace with a previously undefined option will create a new option. The type class will be pulled from whatever object you write with. In the above case, the option defined would have it's type class set to `Logger` and would try to convert any new values to an instance of `Logger`.
127
+
128
+ ## License
129
+
130
+ Copyright (c) 2011 Collin Redding and Team Insight
131
+
132
+ Permission is hereby granted, free of charge, to any person
133
+ obtaining a copy of this software and associated documentation
134
+ files (the "Software"), to deal in the Software without
135
+ restriction, including without limitation the rights to use,
136
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
137
+ copies of the Software, and to permit persons to whom the
138
+ Software is furnished to do so, subject to the following
139
+ conditions:
140
+
141
+ The above copyright notice and this permission notice shall be
142
+ included in all copies or substantial portions of the Software.
143
+
144
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
145
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
146
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
147
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
148
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
149
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
150
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
151
+ OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+
4
+ require 'assert/rake_tasks'
5
+ Assert::RakeTasks.for :test
@@ -0,0 +1,36 @@
1
+ module NsOptions
2
+
3
+ module HasOptions
4
+ class << self
5
+
6
+ def included(klass)
7
+ klass.class_eval do
8
+ extend NsOptions::HasOptions::DSL
9
+ end
10
+ end
11
+
12
+ end
13
+
14
+ module DSL
15
+
16
+ def options(name, key = nil, &block)
17
+ key ||= name.to_s
18
+ self.class_eval <<-DEFINE_METHOD
19
+
20
+ def #{name}(&block)
21
+ @#{name} ||= NsOptions::Helper.new_child_namespace(self, '#{name}', &block)
22
+ end
23
+
24
+ def self.#{name}(&block)
25
+ @#{name} ||= NsOptions::Helper.new_namespace('#{key}', &block)
26
+ end
27
+
28
+ DEFINE_METHOD
29
+ self.send(name, &block)
30
+ end
31
+
32
+ end
33
+
34
+ end
35
+
36
+ end
@@ -0,0 +1,34 @@
1
+ module NsOptions
2
+
3
+ module Helper
4
+ module_function
5
+
6
+ # Common method for creating a new namespace
7
+ def new_namespace(key, parent = nil, &block)
8
+ namespace = NsOptions::Namespace.new(key, parent)
9
+ namespace.define(&block)
10
+ end
11
+
12
+ # Common method for creating a new child namespace, using the owner's class's options as the
13
+ # parent.
14
+ def new_child_namespace(owner, name, &block)
15
+ parent = owner.class.send(name)
16
+ method = "#{name}_key"
17
+ key = if owner.respond_to?(method)
18
+ owner.send(method)
19
+ else
20
+ "#{owner.class.to_s.split('::').last.downcase}_#{owner.object_id}"
21
+ end
22
+ namespace = parent.namespace(name, key)
23
+ namespace.define(&block)
24
+ end
25
+
26
+ def fetch_and_define_option(namespace, option_name)
27
+ option = namespace.options.fetch(option_name)
28
+ namespace.option(option.name, option.type_class, option.rules)
29
+ option
30
+ end
31
+
32
+ end
33
+
34
+ end
@@ -0,0 +1,136 @@
1
+ module NsOptions
2
+
3
+ class Namespace
4
+ attr_accessor :options, :metaclass
5
+
6
+ def initialize(key, parent = nil)
7
+ self.metaclass = (class << self; self; end)
8
+ self.options = NsOptions::Options.new(key, parent)
9
+ end
10
+
11
+ def required_set?
12
+ self.options.required_set?
13
+ end
14
+
15
+ # Define an option for this namespace. Add the option to the namespace's options collection
16
+ # and then define accessors for the option. With the following:
17
+ #
18
+ # namespace.option(:root, String, { :some_option => true })
19
+ #
20
+ # you will get accessors for root:
21
+ #
22
+ # namespace.root = "something" # set's the root option to 'something'
23
+ # namespace.root # => "something"
24
+ # namespace.root("something else") # set's the root option to `something-else`
25
+ #
26
+ # The defined option is returned as well.
27
+ def option(*args)
28
+ option = self.options.add(*args)
29
+
30
+ self.metaclass.class_eval <<-DEFINE_METHOD
31
+
32
+ def #{option.name}(*args)
33
+ if !args.empty?
34
+ self.send("#{option.name}=", *args)
35
+ else
36
+ self.options.get(:#{option.name})
37
+ end
38
+ end
39
+
40
+ def #{option.name}=(*args)
41
+ value = args.size == 1 ? args.first : args
42
+ self.options.set(:#{option.name}, value)
43
+ end
44
+
45
+ DEFINE_METHOD
46
+
47
+ option
48
+ end
49
+
50
+ # Define a namespace under this namespace. Firstly, a new key is constructured from this current
51
+ # namespace's key and the name for the new namespace. The namespace is then added to the
52
+ # options collection. Finally a reader method is defined for accessing the namespace. With the
53
+ # following:
54
+ #
55
+ # parent_namespace.namespace(:specific) do
56
+ # option :root
57
+ # end
58
+ #
59
+ # you will get a reader for the namespace:
60
+ #
61
+ # parent_namespace.specific # => returns the namespace
62
+ # parent_namespace.specific.root = "something" # => options are accessed in the same way
63
+ #
64
+ # The defined namespaces is returned as well.
65
+ def namespace(name, key = nil, &block)
66
+ key = "#{self.options.key}:#{(key || name)}"
67
+ namespace = self.options.namespaces.add(name, key, self, &block)
68
+
69
+ self.metaclass.class_eval <<-DEFINE_METHOD
70
+
71
+ def #{name}(&block)
72
+ namespace = self.options.namespaces.get("#{name}")
73
+ namespace.define(&block) if block
74
+ namespace
75
+ end
76
+
77
+ DEFINE_METHOD
78
+
79
+ namespace
80
+ end
81
+
82
+ # The define method is provided for convenience and commonization. The internal system
83
+ # uses it to commonly use a block with a namespace. The method can be used externally when
84
+ # a namespace is created separately from where options are added/set on it. For example:
85
+ #
86
+ # parent_namespace.namespace(:specific)
87
+ #
88
+ # parent_namespace.specific.define do
89
+ # option :root
90
+ # end
91
+ #
92
+ # Will define a new namespace under the parent namespace and then will later on add options to
93
+ # it.
94
+ def define(&block)
95
+ if block && block.arity > 0
96
+ yield self
97
+ elsif block
98
+ self.instance_eval(&block)
99
+ end
100
+ self
101
+ end
102
+
103
+ # There are a number of cases we want to watch for:
104
+ # 1. A reader of a 'known' option. This case is for an option that's been defined for an
105
+ # ancestor of this namespace but not directly for this namespace. In this case we fetch
106
+ # the options definition and use it to define the option directly for this namespace.
107
+ # 2. A writer of a 'known' option. This case is similar to the above, but instead we are
108
+ # wanting to write a value. We need to fetch the option definition, define it and then
109
+ # we write the option as we normally would.
110
+ # 3. A dynamic writer. The option is not 'known' to the namespace, so we use the value and it's
111
+ # class to define the option for this namespace. Then we just use the writer as we normally
112
+ # would.
113
+ def method_missing(method, *args, &block)
114
+ option_name = method.to_s.gsub("=", "")
115
+ value = args.size == 1 ? args[0] : args
116
+ if args.empty? && self.respond_to?(option_name)
117
+ option = NsOptions::Helper.fetch_and_define_option(self, option_name)
118
+ self.send(option.name)
119
+ elsif !args.empty? && self.respond_to?(option_name)
120
+ option = NsOptions::Helper.fetch_and_define_option(self, option_name)
121
+ self.send("#{option.name}=", value)
122
+ elsif !args.empty?
123
+ option = self.option(option_name, value.class)
124
+ self.send("#{option.name}=", value)
125
+ else
126
+ super
127
+ end
128
+ end
129
+
130
+ def respond_to?(method)
131
+ super || self.options.is_defined?(method.to_s.gsub("=", ""))
132
+ end
133
+
134
+ end
135
+
136
+ end
@@ -0,0 +1,22 @@
1
+ module NsOptions
2
+
3
+ class Namespaces < Hash
4
+
5
+ def [](name)
6
+ super(name.to_sym)
7
+ end
8
+ def []=(name, value)
9
+ super(name.to_sym, value)
10
+ end
11
+
12
+ def add(name, key, parent = nil, &block)
13
+ self[name] = NsOptions::Helper.new_namespace(key, parent, &block)
14
+ end
15
+
16
+ def get(name)
17
+ self[name]
18
+ end
19
+
20
+ end
21
+
22
+ end
@@ -0,0 +1,27 @@
1
+ module NsOptions
2
+ class Option
3
+
4
+ class Boolean
5
+ attr_accessor :actual
6
+
7
+ def initialize(value)
8
+ self.actual = value
9
+ end
10
+
11
+ def actual=(new_value)
12
+ @actual = self.convert(new_value)
13
+ end
14
+
15
+ protected
16
+
17
+ def convert(value)
18
+ if [ nil, 0, '0', false, 'false' ].include?(value)
19
+ false
20
+ elsif value
21
+ true
22
+ end
23
+ end
24
+ end
25
+
26
+ end
27
+ end
@@ -0,0 +1,70 @@
1
+ module NsOptions
2
+
3
+ class Option
4
+ autoload :Boolean, 'ns-options/option/boolean'
5
+
6
+ attr_accessor :name, :value, :type_class, :rules
7
+
8
+ def initialize(name, type_class, rules = {})
9
+ self.name = name.to_s
10
+ self.type_class = self.usable_type_class(type_class)
11
+ self.rules = rules
12
+ self.value = rules[:default]
13
+ end
14
+
15
+ def value
16
+ if self.type_class == NsOptions::Option::Boolean
17
+ @value and @value.actual
18
+ else
19
+ @value
20
+ end
21
+ end
22
+
23
+ def value=(new_value)
24
+ @value = if (new_value.class == self.type_class) || new_value.nil?
25
+ new_value
26
+ else
27
+ self.coerce(new_value)
28
+ end
29
+ end
30
+
31
+ def is_set?
32
+ self.value.respond_to?(:is_set?) ? self.value.is_set? : !self.value.nil?
33
+ end
34
+
35
+ def required?
36
+ !!self.rules[:required] || !!self.rules[:require]
37
+ end
38
+
39
+ def ==(other)
40
+ [ :name, :type_class, :rules, :value ].inject(true) do |bool, attribute|
41
+ bool && (self.send(attribute) == other.send(attribute))
42
+ end
43
+ end
44
+
45
+ protected
46
+
47
+ def coerce(new_value)
48
+ if [ Integer, Float, String ].include?(self.type_class)
49
+ # ruby type conversion, i.e. String(1)
50
+ Object.send(self.type_class.to_s.to_sym, new_value)
51
+ elsif self.type_class == Hash
52
+ {}.merge(new_value)
53
+ else
54
+ self.type_class.new(new_value)
55
+ end
56
+ end
57
+
58
+ def usable_type_class(type_class)
59
+ if type_class == Fixnum
60
+ Integer
61
+ elsif [ TrueClass, FalseClass ].include?(type_class)
62
+ NsOptions::Option::Boolean
63
+ else
64
+ (type_class || String)
65
+ end
66
+ end
67
+
68
+ end
69
+
70
+ end
@@ -0,0 +1,65 @@
1
+ module NsOptions
2
+
3
+ class Options < Hash
4
+ attr_accessor :key, :parent, :children
5
+ alias :namespaces :children
6
+
7
+ def initialize(key, parent = nil)
8
+ self.key = key.to_s
9
+ self.parent = parent
10
+ self.children = NsOptions::Namespaces.new
11
+ end
12
+
13
+ def [](name)
14
+ super(name.to_sym)
15
+ end
16
+ def []=(name, value)
17
+ super(name.to_sym, value)
18
+ end
19
+
20
+ def add(*args)
21
+ options = args.last.kind_of?(Hash) ? args.pop : {}
22
+ option = NsOptions::Option.new(args[0], args[1], options)
23
+ self[option.name] = option
24
+ end
25
+
26
+ def del(name)
27
+ self[name] = nil
28
+ end
29
+ alias :remove :del
30
+
31
+ def get(name)
32
+ option = self[name]
33
+ if option && !option.value.nil?
34
+ option.value
35
+ elsif self.parent_options
36
+ self.parent_options.get(name)
37
+ end
38
+ end
39
+
40
+ def set(name, new_value)
41
+ self[name].value = new_value
42
+ self[name]
43
+ end
44
+
45
+ def fetch(name)
46
+ self[name] || (self.parent_options && self.parent_options.fetch(name))
47
+ end
48
+
49
+ def is_defined?(name)
50
+ !!self[name] || !!(self.parent_options && self.parent_options.is_defined?(name))
51
+ end
52
+
53
+ def parent_options
54
+ self.parent and self.parent.options
55
+ end
56
+
57
+ def required_set?
58
+ self.values.reject{|option| !option.required? }.inject(true) do |bool, option|
59
+ bool && option.is_set?
60
+ end
61
+ end
62
+
63
+ end
64
+
65
+ end
@@ -0,0 +1,3 @@
1
+ module NsOptions
2
+ VERSION = "0.1.0"
3
+ end
data/lib/ns-options.rb ADDED
@@ -0,0 +1,9 @@
1
+ module NsOptions
2
+ autoload :HasOptions, 'ns-options/has_options'
3
+ autoload :Helper, 'ns-options/helper'
4
+ autoload :Namespace, 'ns-options/namespace'
5
+ autoload :Namespaces, 'ns-options/namespaces'
6
+ autoload :Option, 'ns-options/option'
7
+ autoload :Options, 'ns-options/options'
8
+ autoload :VERSION, 'ns-options/version'
9
+ end
@@ -0,0 +1,19 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/ns-options/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Collin Redding"]
6
+ gem.email = ["collin.redding@reelfx.com"]
7
+ gem.description = %q{Define and use namespaced options with a clean interface.}
8
+ gem.summary = %q{Define and use namespaced options with a clean interface.}
9
+
10
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
11
+ gem.files = `git ls-files`.split("\n")
12
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
13
+ gem.name = "ns-options"
14
+ gem.require_paths = ["lib"]
15
+ gem.version = NsOptions::VERSION
16
+
17
+ gem.add_development_dependency("assert", ["~>0.6.0"])
18
+ #gem.add_development_dependency("assert-mocha", ["~>0.1.0"])
19
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,14 @@
1
+ require 'logger'
2
+
3
+ root_path = File.expand_path("../..", __FILE__)
4
+ if !$LOAD_PATH.include?(root_path)
5
+ $LOAD_PATH.unshift(root_path)
6
+ end
7
+ require 'ns-options'
8
+
9
+ require 'test/support/app'
10
+ require 'test/support/user'
11
+
12
+ if defined?(Assert)
13
+ require 'assert-mocha'
14
+ end
@@ -0,0 +1,63 @@
1
+ require 'assert'
2
+
3
+ module App
4
+
5
+ class BaseTest < Assert::Context
6
+ desc "the App module"
7
+ setup do
8
+ @module = App
9
+ end
10
+ subject{ @module }
11
+
12
+ should have_instance_methods :options, :settings
13
+
14
+ end
15
+
16
+ class DefineTest < BaseTest
17
+ desc "defined"
18
+ setup do
19
+ stage = @stage = "test"
20
+ root_path = @root_path = File.expand_path("../../..", __FILE__)
21
+ logger = @logger = Logger.new(File.join(@root_path, "log", "test.log"))
22
+ run = @run = true
23
+ @module.settings.define do |namespace|
24
+ namespace.stage stage
25
+ namespace.root = root_path
26
+ namespace.logger = logger
27
+
28
+ namespace.sub do
29
+ run_commands run
30
+ end
31
+ end
32
+ end
33
+ subject{ @module.settings }
34
+
35
+ should have_instance_methods :namespace, :option, :define, :options, :metaclass
36
+ should have_accessors :stage, :root, :logger
37
+ should have_instance_methods :sub
38
+
39
+ should "have set the stage to 'test'" do
40
+ assert_equal @stage, subject.stage
41
+ end
42
+ should "have set the root to this gem's dir" do
43
+ assert_equal Pathname.new(@root_path), subject.root
44
+ end
45
+ should "have set the logger to the passed logger" do
46
+ assert_equal @logger, subject.logger
47
+ assert_same @logger, subject.logger
48
+ end
49
+ end
50
+
51
+ class SubNamespaceTest < DefineTest
52
+ desc "the sub namespace"
53
+ subject{ @module.settings.sub }
54
+
55
+ should "have set the run_commands option" do
56
+ assert_equal @run, subject.run_commands
57
+ end
58
+ should "have access to it's parent's options" do
59
+ assert_equal @stage, subject.stage
60
+ end
61
+ end
62
+
63
+ end