ns-options 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,14 +3,21 @@ module NsOptions
3
3
  class Namespace
4
4
  attr_accessor :options, :metaclass
5
5
 
6
- def initialize(key, parent = nil)
6
+ # Every namespace tracks a metaclass to allow for individual reader/writers for their options,
7
+ # without any collisions. Since every namespace is of the same class, defining option reader and
8
+ # writer methods directly on the class would make multiple namespaces with different options
9
+ # impossible.
10
+ def initialize(key, parent = nil, &block)
7
11
  self.metaclass = (class << self; self; end)
8
12
  self.options = NsOptions::Options.new(key, parent)
13
+ self.define(&block)
9
14
  end
10
15
 
16
+ # This is a helper to check if options that were defined as :required have been set.
11
17
  def required_set?
12
18
  self.options.required_set?
13
19
  end
20
+ alias :valid? :required_set?
14
21
 
15
22
  # Define an option for this namespace. Add the option to the namespace's options collection
16
23
  # and then define accessors for the option. With the following:
@@ -25,25 +32,9 @@ module NsOptions
25
32
  #
26
33
  # The defined option is returned as well.
27
34
  def option(*args)
35
+ NsOptions::Helper.advisor(self).is_this_option_ok?(args[0], caller)
28
36
  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
-
37
+ NsOptions::Helper.define_option_methods(self, option)
47
38
  option
48
39
  end
49
40
 
@@ -64,19 +55,42 @@ module NsOptions
64
55
  # The defined namespaces is returned as well.
65
56
  def namespace(name, key = nil, &block)
66
57
  key = "#{self.options.key}:#{(key || name)}"
67
- namespace = self.options.namespaces.add(name, key, self, &block)
68
-
69
- self.metaclass.class_eval <<-DEFINE_METHOD
58
+ NsOptions::Helper.advisor(self).is_this_namespace_ok?(name, caller)
59
+ namespace = self.options.add_namespace(name, key, self, &block)
60
+ NsOptions::Helper.define_namespace_methods(self, name)
61
+ namespace
62
+ end
70
63
 
71
- def #{name}(&block)
72
- namespace = self.options.namespaces.get("#{name}")
73
- namespace.define(&block) if block
74
- namespace
64
+ # The opposite of #to_hash. Takes a hash representation of options and namespaces and mass
65
+ # assigns option values.
66
+ def apply(option_values = {})
67
+ option_values.each do |name, value|
68
+ namespace = self.options.namespaces[name]
69
+ if self.options[name] || !namespace
70
+ self.send("#{name}=", value)
71
+ elsif namespace && value.kind_of?(Hash)
72
+ namespace.apply(value)
75
73
  end
74
+ end
75
+ end
76
76
 
77
- DEFINE_METHOD
77
+ # return a hash representation of the namespace
78
+ # use symbols for the hash
79
+ def to_hash
80
+ Hash.new.tap do |out|
81
+ self.options.each do |name, opt|
82
+ out[name.to_sym] = opt.value
83
+ end
84
+ self.options.namespaces.each do |name, value|
85
+ out[name.to_sym] = value.to_hash
86
+ end
87
+ end
88
+ end
78
89
 
79
- namespace
90
+ # allow for iterating over the key/values of a namespace
91
+ # this uses #to_hash so you won't get option/namespace objs for the values
92
+ def each
93
+ self.to_hash.each { |k,v| yield k,v if block_given? }
80
94
  end
81
95
 
82
96
  # The define method is provided for convenience and commonization. The internal system
@@ -104,23 +118,27 @@ module NsOptions
104
118
  # 1. A reader of a 'known' option. This case is for an option that's been defined for an
105
119
  # ancestor of this namespace but not directly for this namespace. In this case we fetch
106
120
  # 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
121
+ # 2. TODO
122
+ # 3. A writer of a 'known' option. This case is similar to the above, but instead we are
108
123
  # wanting to write a value. We need to fetch the option definition, define it and then
109
124
  # 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
125
+ # 4. A dynamic writer. The option is not 'known' to the namespace, so we use the value and it's
111
126
  # class to define the option for this namespace. Then we just use the writer as we normally
112
127
  # would.
113
128
  def method_missing(method, *args, &block)
114
129
  option_name = method.to_s.gsub("=", "")
115
130
  value = args.size == 1 ? args[0] : args
116
131
  if args.empty? && self.respond_to?(option_name)
117
- option = NsOptions::Helper.fetch_and_define_option(self, option_name)
132
+ option = NsOptions::Helper.find_and_define_option(self, option_name)
118
133
  self.send(option.name)
134
+ elsif args.empty? && (namespace = self.options.get_namespace(option_name))
135
+ NsOptions::Helper.find_and_define_namespace(self, option_name)
136
+ self.send(option_name)
119
137
  elsif !args.empty? && self.respond_to?(option_name)
120
- option = NsOptions::Helper.fetch_and_define_option(self, option_name)
138
+ option = NsOptions::Helper.find_and_define_option(self, option_name)
121
139
  self.send("#{option.name}=", value)
122
140
  elsif !args.empty?
123
- option = self.option(option_name, value.class)
141
+ option = self.option(option_name)
124
142
  self.send("#{option.name}=", value)
125
143
  else
126
144
  super
@@ -131,6 +149,10 @@ module NsOptions
131
149
  super || self.options.is_defined?(method.to_s.gsub("=", ""))
132
150
  end
133
151
 
152
+ def inspect(*args)
153
+ "#<#{self.class}:#{'0x%x' % (self.object_id << 1)}:#{self.options.key} #{self.to_hash.inspect}>"
154
+ end
155
+
134
156
  end
135
157
 
136
158
  end
@@ -10,7 +10,7 @@ module NsOptions
10
10
  end
11
11
 
12
12
  def add(name, key, parent = nil, &block)
13
- self[name] = NsOptions::Helper.new_namespace(key, parent, &block)
13
+ self[name] = NsOptions::Namespace.new(key, parent, &block)
14
14
  end
15
15
 
16
16
  def get(name)
@@ -1,31 +1,30 @@
1
1
  module NsOptions
2
2
 
3
3
  class Option
4
- autoload :Boolean, 'ns-options/option/boolean'
5
-
6
4
  attr_accessor :name, :value, :type_class, :rules
7
5
 
8
- def initialize(name, type_class, rules = {})
6
+ def initialize(name, type_class, rules={})
9
7
  self.name = name.to_s
10
- self.type_class = self.usable_type_class(type_class)
8
+
9
+ # if a nil type_class is given, just use Object
10
+ # this makes the option accept any value with no type coercion
11
+ self.type_class = (type_class || Object)
12
+
11
13
  self.rules = rules
14
+ self.rules[:args] = (self.rules[:args] ? [*self.rules[:args]] : [])
12
15
  self.value = rules[:default]
13
16
  end
14
17
 
18
+ # if reading a lazy_proc, call the proc and return its coerced return val
19
+ # otherwise, just return the stored value
15
20
  def value
16
- if self.type_class == NsOptions::Option::Boolean
17
- @value and @value.actual
18
- else
19
- @value
20
- end
21
+ self.lazy_proc?(@value) ? self.coerce(@value.call) : @value
21
22
  end
22
23
 
24
+ # if setting a lazy_proc, just store the proc off to be called when read
25
+ # otherwise, coerce and store the value being set
23
26
  def value=(new_value)
24
- @value = if (new_value.kind_of?(self.type_class)) || new_value.nil?
25
- new_value
26
- else
27
- self.coerce(new_value)
28
- end
27
+ @value = self.lazy_proc?(new_value) ? new_value : self.coerce(new_value)
29
28
  end
30
29
 
31
30
  def is_set?
@@ -44,24 +43,27 @@ module NsOptions
44
43
 
45
44
  protected
46
45
 
47
- def coerce(new_value)
46
+ # a value is considered to by a lazy eval proc if it some kind of a of
47
+ # Proc and the option it is being set on is not explicitly defined as some
48
+ # kind of Proc
49
+ # The allows you to set option values that should't be evaluated until they
50
+ # are being read
51
+ def lazy_proc?(value)
52
+ value.kind_of?(::Proc) && !self.type_class.ancestors.include?(::Proc)
53
+ end
54
+
55
+ def coerce(value)
56
+ return value if (value.kind_of?(self.type_class)) || value.nil?
57
+
48
58
  if [ Integer, Float, String ].include?(self.type_class)
49
59
  # ruby type conversion, i.e. String(1)
50
- Object.send(self.type_class.to_s.to_sym, new_value)
60
+ Object.send(self.type_class.to_s.to_sym, value)
61
+ elsif self.type_class == Symbol
62
+ value.to_sym
51
63
  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
64
+ {}.merge(value)
63
65
  else
64
- (type_class || String)
66
+ self.type_class.new(value, *self.rules[:args])
65
67
  end
66
68
  end
67
69
 
@@ -1,13 +1,12 @@
1
1
  module NsOptions
2
2
 
3
3
  class Options < Hash
4
- attr_accessor :key, :parent, :children
5
- alias :namespaces :children
4
+ attr_accessor :key, :parent, :namespaces
6
5
 
7
6
  def initialize(key, parent = nil)
8
7
  self.key = key.to_s
9
8
  self.parent = parent
10
- self.children = NsOptions::Namespaces.new
9
+ self.namespaces = NsOptions::Namespaces.new
11
10
  end
12
11
 
13
12
  def [](name)
@@ -30,11 +29,7 @@ module NsOptions
30
29
 
31
30
  def get(name)
32
31
  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
32
+ option and option.value
38
33
  end
39
34
 
40
35
  def set(name, new_value)
@@ -42,16 +37,8 @@ module NsOptions
42
37
  self[name]
43
38
  end
44
39
 
45
- def fetch(name)
46
- self[name] || (self.parent_options && self.parent_options.fetch(name))
47
- end
48
-
49
40
  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
41
+ !!self[name]
55
42
  end
56
43
 
57
44
  def required_set?
@@ -60,6 +47,31 @@ module NsOptions
60
47
  end
61
48
  end
62
49
 
50
+ def add_namespace(name, key = nil, parent = nil, &block)
51
+ key ||= name
52
+ self.namespaces.add(name, key, parent, &block)
53
+ end
54
+
55
+ def get_namespace(name)
56
+ self.namespaces[name]
57
+ end
58
+
59
+ def is_namespace_defined?(name)
60
+ !!self.get_namespace(name)
61
+ end
62
+
63
+ def build_from(options, namespace)
64
+ options.each do |key, option|
65
+ self.add(option.name, option.type_class, option.rules)
66
+ NsOptions::Helper.find_and_define_option(namespace, option.name)
67
+ end
68
+ options.namespaces.each do |name, ns|
69
+ new_namespace = self.add_namespace(name, ns.options.key, ns.options.parent)
70
+ NsOptions::Helper.find_and_define_namespace(namespace, name)
71
+ new_namespace.options.build_from(ns.options, new_namespace)
72
+ end
73
+ end
74
+
63
75
  end
64
76
 
65
77
  end
@@ -1,3 +1,3 @@
1
1
  module NsOptions
2
- VERSION = "0.1.1"
2
+ VERSION = "0.2.0"
3
3
  end
data/lib/ns-options.rb CHANGED
@@ -6,4 +6,13 @@ module NsOptions
6
6
  autoload :Option, 'ns-options/option'
7
7
  autoload :Options, 'ns-options/options'
8
8
  autoload :VERSION, 'ns-options/version'
9
+
10
+ module Errors
11
+ autoload :InvalidName, 'ns-options/errors/invalid_name'
12
+ end
13
+
14
+ def self.included(receiver)
15
+ receiver.send(:include, HasOptions)
16
+ end
17
+
9
18
  end
data/log/.gitkeep ADDED
File without changes
data/ns-options.gemspec CHANGED
@@ -14,6 +14,6 @@ Gem::Specification.new do |gem|
14
14
  gem.require_paths = ["lib"]
15
15
  gem.version = NsOptions::VERSION
16
16
 
17
- gem.add_development_dependency("assert", ["~>0.6"])
17
+ gem.add_development_dependency("assert", ["~>0.7"])
18
18
  gem.add_development_dependency("assert-mocha", ["~>0.1"])
19
19
  end
data/test/helper.rb CHANGED
@@ -1,14 +1,9 @@
1
- require 'logger'
1
+ $LOAD_PATH.unshift(File.expand_path("../..", __FILE__))
2
2
 
3
- root_path = File.expand_path("../..", __FILE__)
4
- if !$LOAD_PATH.include?(root_path)
5
- $LOAD_PATH.unshift(root_path)
6
- end
3
+ require 'logger'
7
4
  require 'ns-options'
8
5
 
9
6
  require 'test/support/app'
10
7
  require 'test/support/user'
11
8
 
12
- if defined?(Assert)
13
- require 'assert-mocha'
14
- end
9
+ require 'assert-mocha'
@@ -39,24 +39,30 @@ module App
39
39
  should "have set the stage to 'test'" do
40
40
  assert_equal @stage, subject.stage
41
41
  end
42
+
42
43
  should "have set the root to this gem's dir" do
43
44
  assert_equal Pathname.new(@root_path), subject.root
44
45
  end
46
+
45
47
  should "have set the logger to the passed logger" do
46
48
  assert_equal @logger, subject.logger
47
49
  assert_same @logger, subject.logger
48
50
  end
51
+
52
+ should "have set its self_stage option to its stage" do
53
+ assert_equal @stage, subject.self_stage
54
+ assert_same @stage, subject.self_stage
55
+ end
56
+
49
57
  end
50
-
58
+
51
59
  class SubNamespaceTest < DefineTest
52
60
  desc "the sub namespace"
53
61
  subject{ @module.settings.sub }
54
-
62
+
55
63
  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
64
+ assert_kind_of NsOptions::Boolean, subject.run_commands
65
+ assert_equal @run, subject.run_commands.actual
60
66
  end
61
67
  end
62
68
 
@@ -10,6 +10,7 @@ class User
10
10
  subject{ @class }
11
11
 
12
12
  should have_instance_methods :options, :preferences
13
+
13
14
  end
14
15
 
15
16
  class ClassPreferencesTest < BaseTest
@@ -18,16 +19,44 @@ class User
18
19
 
19
20
  should have_instance_methods :namespace, :option, :define, :options, :metaclass
20
21
  should have_accessors :home_url, :show_messages, :font_size
22
+
21
23
  end
22
24
 
23
25
  class InstanceTest < BaseTest
24
26
  desc "instance"
25
27
  setup do
26
28
  @instance = @class.new
29
+ @class_preferences = @class.preferences
30
+ @class_preferences.home_url = "/something"
31
+ @preferences = @instance.preferences
27
32
  end
28
33
  subject{ @instance }
29
34
 
30
35
  should have_instance_methods :preferences
36
+
37
+ should "have a new namespace that is a different object than the class namespace" do
38
+ assert_equal @class_preferences.options.key, @preferences.options.key
39
+ assert_equal @class_preferences.options.parent, @preferences.options.parent
40
+ assert_not_same @class_preferences, @preferences
41
+ end
42
+ should "have the same options as the class namespace, but different objects" do
43
+ @class_preferences.options.each do |key, class_option|
44
+ option = @preferences.options[key]
45
+ assert_equal class_option.name, option.name
46
+ assert_equal class_option.type_class, option.type_class
47
+ assert_equal class_option.rules, option.rules
48
+ assert_not_same class_option, option
49
+ end
50
+ assert_not_equal @class_preferences.home_url, @preferences.home_url
51
+ end
52
+ should "have the same namespaces as the class namespace, but different objects" do
53
+ @class_preferences.options.namespaces.each do |name, class_namespace|
54
+ namespace = @preferences.options.namespaces[name]
55
+ assert_equal class_namespace.options.key, namespace.options.key
56
+ assert_equal class_namespace.options.parent, namespace.options.parent
57
+ assert_not_same class_namespace, namespace
58
+ end
59
+ end
31
60
  end
32
61
 
33
62
  class PreferencesTest < InstanceTest
@@ -37,21 +66,30 @@ class User
37
66
  @preferences.home_url = "/home"
38
67
  @preferences.show_messages = false
39
68
  @preferences.font_size = 15
69
+ @preferences.view.color = "green"
40
70
  end
41
71
  subject{ @preferences }
42
72
 
43
- should have_instance_methods :namespace, :option, :define, :options, :metaclass
44
73
  should have_accessors :home_url, :show_messages, :font_size
74
+ should have_instance_methods :namespace, :option, :define, :options, :metaclass
75
+ should have_instance_methods :view
45
76
 
46
77
  should "have set the home_url" do
47
78
  assert_equal "/home", subject.home_url
48
79
  end
80
+
49
81
  should "have set show_messages" do
50
- assert_equal false, subject.show_messages
82
+ assert_kind_of NsOptions::Boolean, subject.show_messages
83
+ assert_equal false, subject.show_messages.actual
51
84
  end
85
+
52
86
  should "have set the font_size" do
53
87
  assert_equal 15, subject.font_size
54
88
  end
89
+
90
+ should "have set the color option on the view sub namespace" do
91
+ assert_equal "green", subject.view.color
92
+ end
55
93
  end
56
94
 
57
95
  end
data/test/irb.rb ADDED
@@ -0,0 +1,9 @@
1
+ require 'assert/setup'
2
+
3
+ # this file is required in when the 'irb' rake test is run.
4
+ # b/c 'assert/setup' is required above, the test helper will be
5
+ # required in as well.
6
+
7
+ # put any IRB setup code here
8
+
9
+ require 'ns-options'
data/test/support/app.rb CHANGED
@@ -1,12 +1,18 @@
1
+ require 'ns-options/boolean'
2
+
1
3
  module App
2
- include NsOptions::HasOptions
4
+
5
+ # mixin on just the top-level NsOptions variant
6
+ include NsOptions
7
+
3
8
  options(:settings, "settings:app") do
4
9
  option :root, Pathname
5
10
  option :stage
6
11
  option :logger, Logger
12
+ option :self_stage, :default => Proc.new { self.stage }
7
13
 
8
14
  namespace :sub do
9
- option :run_commands, NsOptions::Option::Boolean
15
+ option :run_commands, NsOptions::Boolean
10
16
  end
11
17
  end
12
18
  end
data/test/support/user.rb CHANGED
@@ -1,13 +1,22 @@
1
+ require 'ns-options/boolean'
2
+
1
3
  class User
4
+
5
+ # mixin using the specific HasOptions variant
2
6
  include NsOptions::HasOptions
7
+
3
8
  options(:preferences, 'user-preferences') do
4
9
  option :home_url
5
- option :show_messages, NsOptions::Option::Boolean, :require => true
6
- option :font_size, Integer, :default => 12
10
+ option :show_messages, NsOptions::Boolean, :require => true
11
+ option :font_size, Integer, :default => 12
12
+
13
+ namespace :view do
14
+ option :color
15
+ end
7
16
  end
8
17
 
9
18
  def preferences_key
10
19
  "user_#{self.object_id}"
11
20
  end
12
21
 
13
- end
22
+ end
@@ -1,11 +1,11 @@
1
1
  require 'assert'
2
2
 
3
- class NsOptions::Option::Boolean
3
+ class NsOptions::Boolean
4
4
 
5
5
  class BaseTest < Assert::Context
6
- desc "NsOptions::Option::Boolean"
6
+ desc "NsOptions::Boolean"
7
7
  setup do
8
- @boolean = NsOptions::Option::Boolean.new(true)
8
+ @boolean = NsOptions::Boolean.new(true)
9
9
  end
10
10
  subject{ @boolean }
11
11
 
@@ -15,7 +15,7 @@ class NsOptions::Option::Boolean
15
15
  class WithTruthyValuesTest < BaseTest
16
16
  desc "with truthy values"
17
17
  setup do
18
- @boolean = NsOptions::Option::Boolean.new(false)
18
+ @boolean = NsOptions::Boolean.new(nil)
19
19
  end
20
20
 
21
21
  should "have set actual to true with true" do
@@ -26,6 +26,14 @@ class NsOptions::Option::Boolean
26
26
  subject.actual = 'true'
27
27
  assert_equal true, subject.actual
28
28
  end
29
+ should "have set actual to true with 't'" do
30
+ subject.actual = 't'
31
+ assert_equal true, subject.actual
32
+ end
33
+ should "have set actual to true with 'T'" do
34
+ subject.actual = 'T'
35
+ assert_equal true, subject.actual
36
+ end
29
37
  should "have set actual to true with 1" do
30
38
  subject.actual = 1
31
39
  assert_equal true, subject.actual
@@ -39,7 +47,7 @@ class NsOptions::Option::Boolean
39
47
  class WithFalsyValuesTest < BaseTest
40
48
  desc "with falsy values"
41
49
  setup do
42
- @boolean = NsOptions::Option::Boolean.new(true)
50
+ @boolean = NsOptions::Boolean.new(nil)
43
51
  end
44
52
 
45
53
  should "have set actual to false with false" do
@@ -50,6 +58,14 @@ class NsOptions::Option::Boolean
50
58
  subject.actual = 'false'
51
59
  assert_equal false, subject.actual
52
60
  end
61
+ should "have set actual to false with 'f'" do
62
+ subject.actual = 'f'
63
+ assert_equal false, subject.actual
64
+ end
65
+ should "have set actual to false with 'F'" do
66
+ subject.actual = 'F'
67
+ assert_equal false, subject.actual
68
+ end
53
69
  should "have set actual to false with 0" do
54
70
  subject.actual = 0
55
71
  assert_equal false, subject.actual