ns-options 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.markdown CHANGED
@@ -16,7 +16,7 @@ The basic usage of Namespace Options is to be able to define options for a modul
16
16
 
17
17
  ```ruby
18
18
  module App
19
- include NsOptions::HasOptions
19
+ include NsOptions
20
20
  options(:settings) do
21
21
  option :root, Pathname
22
22
  option :stage
@@ -34,7 +34,7 @@ App.settings.stage = "development"
34
34
  App.settings.stage # => "development"
35
35
  ```
36
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:
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 an `Object` which allows it to accept any value. You can define you're own type classes as well and use them:
38
38
 
39
39
  ```ruby
40
40
  class Stage < String
@@ -60,11 +60,11 @@ App.settings.stage = "test"
60
60
  App.settings.stage.development? # => false
61
61
  ```
62
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.
63
+ This allows you to add extended functionality to your options and is where a lot of nice usability can be added. Defining your own type classes is explained in more detail later.
64
64
 
65
65
  ### Namespaces
66
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:
67
+ Namespaces allow you to organize your options. With the previously mentioned `App` module and it's options you could create a namespace for another library:
68
68
 
69
69
  ```ruby
70
70
  module Data # data is a library for retrieving persisted data from some resource
@@ -85,20 +85,17 @@ Now I can set a server option for data that is separate from the main `App` sett
85
85
  ```ruby
86
86
  Data.config.server = "127.0.0.1:1234"
87
87
 
88
- App.server # => NoMethodError
88
+ App.settings.server # => NoMethodError
89
+ App.settings.data.server # => 127.0.0.1:1234
89
90
  ```
90
91
 
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
+ #### With Classes
92
93
 
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:
94
+ Using `NsOptions` on a `Class` uses namespaces to create separate sets of options for every instance of your class created. This allows every instance to have different values for the set of options and not interfere with each other. For example with the following:
98
95
 
99
96
  ```ruby
100
97
  class User
101
- include NsOptions::HasOptions
98
+ include NsOptions
102
99
  options(:preferences) do
103
100
  option :home_page
104
101
  end
@@ -106,11 +103,43 @@ class User
106
103
  end
107
104
  ```
108
105
 
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:
106
+ A namespace is created for the `User` class in the same way it works for modules:
107
+
108
+ ```ruby
109
+ User.preferences # => NsOptions::Namespace instance
110
+ User.preferences.home_page = "/home" # you can set options at this level, though I'm not sure why you would
111
+ ```
112
+
113
+ Additionally, `NsOptions` will setup instances of a class to have a _copy_ of their class's namespace.
114
+
115
+ ```ruby
116
+ user = User.new
117
+ user.preferences.home_page = "/home" # makes a lot more sense to do this
118
+ user2 = User.new
119
+ user2.preferences.home_page = "/not_home"
120
+ user.preferences.home_page == user2.preferences.home_page # => false, they are completely separate
121
+ ```
122
+
123
+ The instance level namespaces are deep copies of the class one. This means every option and sub-namespaces will be included. Only values are not copied.
110
124
 
111
125
  ```ruby
126
+ class User
127
+ include NsOptions
128
+ options(:preferences) do
129
+ option :home_page
130
+ namespace :view do
131
+ option :background_color, ViewColor
132
+ end
133
+ end
134
+
135
+ end
136
+
137
+ User.preferences.home_page = "/home"
112
138
  user = User.new
113
- user.preferences.home_page = "/home"
139
+ user.preferences.home_page # => nil, does not return '/home'
140
+ user.preferences.object_id != User.preferences.object_id # => true, they are different objects, just with the same definition
141
+ user.preferences.view.background_color = "green"
142
+ user.preferences.view.background_color # => returns an instance of ViewColor
114
143
  ```
115
144
 
116
145
  ### Dynamically Defined Options
@@ -123,7 +152,235 @@ App.settings.logger = Logger.new(App.settings.root.join("log", "test.log"))
123
152
  App.settings.logger.info("Hello World")
124
153
  ```
125
154
 
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`.
155
+ Writing to a namespace with a previously undefined option will create a new option. The type class will be defaulted to `Object` as if you didn't provide it. This will allow you to set any value for the option so you have no guarantee on what it's value is and how it can be used.
156
+
157
+ ### Mass Assigning Options
158
+
159
+ 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:
160
+
161
+ ```ruby
162
+ class Project
163
+ include NsOptions
164
+ options(:settings) do
165
+ option :file_path
166
+ option :home_page
167
+
168
+ namespace(:movie_resolution) do
169
+ option :height, Integer
170
+ option :width, Integer
171
+ end
172
+ end
173
+ end
174
+
175
+ project = Project.new
176
+ project.settings.apply({
177
+ :file_path => "/path/to/project",
178
+ :movie_resolution => { :height => 800, :width => 600 }
179
+ })
180
+
181
+ project.settings.file_path # => "/path/to/project"
182
+ project.settings.movie_resolution.height # => 800
183
+ project.settings.movie_resolution.width # => 600
184
+ ```
185
+
186
+ As the example shows, if you have a namespace and have a matching hash, it will automatically apply those values to that namespace. Also, if you include keys that are not defined options for your namespace, new options will be created for the values:
187
+
188
+ ```ruby
189
+ project = Project.new
190
+ project.settings.apply({ :stereoscopic => true, :not_a_namespace => { :yes => true } })
191
+
192
+ project.settings.stereoscopic # => true
193
+ project.settings.not_a_namespace # => { :yes => true }
194
+ ```
195
+
196
+ The reverse is also supported, so if you want a `Hash` version of your namespace, just ask your options for it.
197
+
198
+ ```ruby
199
+ # a continuation of the previous block of code...
200
+ project.settings.to_hash # => { :stereoscopic => true, :not_a_namespace => { :yes => true } }
201
+ project.settings.each do |name, value|
202
+ # iterating over your options works as well
203
+ end
204
+ ```
205
+
206
+ ### Lazily eval'd options
207
+
208
+ Sometimes, you may want to set an option to a value that shouldn't (couldn'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:
209
+
210
+ ```ruby
211
+ # dynamic value
212
+ options(:dynamic) do
213
+ option :rand, :default => Proc.new { rand(1000) }
214
+ end
215
+
216
+ dynamic.rand #=> 347
217
+ dynamic.rand #=> 529
218
+
219
+ # same goes for dynamically defined options
220
+ dynamic.not_originally_defined = Proc.new { rand(1000) }
221
+ dynamic.not_originally_defined #=> 110
222
+ dynamic.not_originally_defined #=> 931
223
+ ```
224
+
225
+ ```ruby
226
+ # self referential value
227
+ options(:selfref) do
228
+ option :something, :default => "123"
229
+ option :else, :default => Proc.new { self.something }
230
+ end
231
+
232
+ selfref.something #=> "123"
233
+ selfref.else #=> "123"
234
+ ```
235
+
236
+ If you really want your option to read and write Procs and not do this lazy eval behavior, just define the option as a Proc option
237
+
238
+ ```ruby
239
+ options(:explicit) do
240
+ option :a_proc, Proc, :default => Proc.new { rand(1000) }
241
+ end
242
+
243
+ explicit.a_proc #=> <the proc obj>
244
+ ```
245
+
246
+ ### Custom Type Classes
247
+
248
+ As stated previously, type classes is where you can add a lot of functionality and usability to your options. To do this though, understanding what `NsOptions` will do with your type class is important. First, it's important to understand when `NsOptions` will try to _coerce_ a value. This is only done when a value is not a _kind of_ the option's type class or when the value is nil. For example:
249
+
250
+ ```ruby
251
+ module App
252
+ include NsOptions
253
+ options :settings do
254
+ option :stage, Stage
255
+ end
256
+ end
257
+
258
+ App.settings.stage = Stage.new("development") # no type coercion is done here, the value is already a Stage
259
+
260
+ class BetterStage < Stage
261
+ # do something better
262
+ end
263
+
264
+ App.settings.stage = BetterStage.new("test") # again, no type coercion is done, as BetterStage is a kind of Stage
265
+
266
+ App.setting.stage = nil # nil is never coerced, if you set a value to nil, it's just nil
267
+ ```
268
+
269
+ Next, when `NsOptions` chooses to coerce a value with your class, it will always create a new instance of your type class and pass the value as the first argument. Your `initialize` method needs to be defined to handle this:
270
+
271
+ ```ruby
272
+ class Root < Pathname
273
+ def initialize(path, app_name)
274
+ super("#{path}/#{app_name}")
275
+ end
276
+ end
277
+ ```
278
+
279
+ `Root`'s `initialize` method will not work for type coercion. The `app_name` argument will not be provided and Ruby will get angry. To solve this, make the `app_name` not required:
280
+
281
+ ```ruby
282
+ class Root < Pathname
283
+ def initialize(path, app_name = nil)
284
+ app_name ||= App.settings.name # this might be one way to solve this
285
+ super("#{path}/#{app_name}")
286
+ end
287
+ end
288
+ ```
289
+
290
+ With the revised `initialize` method, `NsOptions` will have no problems coercing values for your the type class. In some cases the above solution may not work for you, but don't worry. See the _Option Rules_ section for another way to solve this, specifically about the args rule. For an example of a custom type class, the included `NsOptions::Boolean` can be looked at. This is a special case, but it works as a type class with `NsOptions`.
291
+
292
+ ### Ruby Classes As A Type Class
293
+
294
+ `NsOptions` will allow you to use many of Ruby's standard objects as type classes and still handle coercing values appropriately. Typically this is done with ruby's type casting:
295
+
296
+ ```ruby
297
+ module Example
298
+ include NsOptions
299
+ options :stuff do
300
+ option :string, String
301
+ option :integer, Integer
302
+ option :float, Float
303
+ option :symbol, Symbol
304
+ option :hash, Hash
305
+ option :array, Array
306
+ end
307
+ end
308
+
309
+ Example.stuff.string = 1
310
+ Example.stuff.string # => "1", the same as doing String(1)
311
+ Example.stuff.integer = 5.0
312
+ Example.stuff.integer # => 5, this time it's Integer(5.0)
313
+ Example.stuff.float = "5.0"
314
+ Example.stuff.float # => 5.0, same as Float("5.0")
315
+ ```
316
+
317
+ `Symbol`, `Hash` and `Array` work, but ruby doesn't provide a built in type casting for these.
318
+
319
+ ```ruby
320
+ Example.stuff.symbol = "awesome"
321
+ Example.stuff.symbol # => :awesome, watch out, this will try calling to_sym on the passed value, so it can error
322
+ Example.stuff.hash = { :a => 'b' }
323
+ Example.stuff.hash # => returns the same hash, does Hash.new.merge(value)
324
+ Example.stuff.array = [ 1, 2, 3 ]
325
+ Example.stuff.array # => returns the same array, Array is the only one that works without anything special, Array.new(value)
326
+ ```
327
+
328
+ ### Option Rules
329
+
330
+ An option can be defined with certain rules (through a hash) that will extend the behavior of the option.
331
+
332
+ #### Default Value
333
+
334
+ The first rule is setting a default value.
335
+
336
+ ```ruby
337
+ App.settings do
338
+ option :stage, Stage, :default => "development"
339
+ end
340
+ App.settings.stage # => instead of nil this will be 'development'
341
+ ```
342
+
343
+ A default value runs through the same logic as if you set the value manually, so it will be coerced if necessary.
344
+
345
+ #### Required
346
+
347
+ It's also possible to flag an option as _required_.
348
+
349
+ ```ruby
350
+ App.settings do
351
+ option :root, :required => true
352
+ end
353
+
354
+ App.settings.required_set? # => false, asking if the required options are set
355
+ App.settings.root = "/path/to/somewhere"
356
+ App.settings.required_set? # => true
357
+ ```
358
+
359
+ 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.
360
+
361
+ #### Args
362
+
363
+ Another rule that you can specify is args. This allows you to pass more arguments to a type class.
364
+
365
+ ```ruby
366
+ class Root < Pathname
367
+ def initialize(path, app_name = nil)
368
+ app_name = app_name.respond_to?(:call) ? app_name.call : app_name
369
+ super("#{path}/#{app_name}")
370
+ end
371
+ end
372
+
373
+ App.settings do
374
+ option :name
375
+ option :root, Root, :args => lambda{ App.settings.name }
376
+ end
377
+
378
+ App.settings.name = "example"
379
+ App.settings.root = "/path/to"
380
+ App.settings.root # => /path/to/example, uses the args rule to build the path
381
+ ```
382
+
383
+ With the args rule, you can have a type class accept more than one argument. The first argument will always be the value to coerce. Any more arguments will be appended on after the value.
127
384
 
128
385
  ## License
129
386
 
@@ -148,4 +405,4 @@ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
148
405
  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
149
406
  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
150
407
  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
151
- OTHER DEALINGS IN THE SOFTWARE.
408
+ OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,25 @@
1
+ module NsOptions
2
+ class Boolean
3
+
4
+ attr_accessor :actual
5
+
6
+ def initialize(value)
7
+ self.actual = value
8
+ end
9
+
10
+ def actual=(new_value)
11
+ @actual = self.convert(new_value)
12
+ end
13
+
14
+ protected
15
+
16
+ def convert(value)
17
+ if [ nil, 0, '0', false, 'false', 'f', 'F' ].include?(value)
18
+ false
19
+ elsif value
20
+ true
21
+ end
22
+ end
23
+
24
+ end
25
+ end
@@ -0,0 +1,15 @@
1
+ module NsOptions
2
+ module Errors
3
+
4
+ class InvalidName < StandardError
5
+ attr_accessor :message
6
+
7
+ def initialize(message, backtrace)
8
+ self.message = message
9
+ self.set_backtrace(backtrace)
10
+ end
11
+
12
+ end
13
+
14
+ end
15
+ end
@@ -13,19 +13,45 @@ module NsOptions
13
13
 
14
14
  module DSL
15
15
 
16
+ # This is the main DSL method for creating a namespace of options for your class/module. This
17
+ # will define a class method for both classes and modules and an additional instance method
18
+ # for classes. The namespace is then created and returned by calling the class method version.
19
+ # For classes, the instance method will build an entirely new namespace from the class level
20
+ # namespace. This is so when you define options at the class level:
21
+ #
22
+ # class Something
23
+ # include NsOptions
24
+ # options(:settings) do
25
+ # option :root
26
+ # end
27
+ # end
28
+ #
29
+ # the namespaces at the instance level still get all the defined options, but are completely
30
+ # separate objects from the class and other instances. Modules only deal with a single
31
+ # namespace at the module level.
16
32
  def options(name, key = nil, &block)
17
33
  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
34
+ method_definitions = <<-CLASS_METHOD
23
35
 
24
36
  def self.#{name}(&block)
25
- @#{name} ||= NsOptions::Helper.new_namespace('#{key}', &block)
37
+ @#{name} ||= NsOptions::Namespace.new('#{key}', &block)
26
38
  end
27
39
 
28
- DEFINE_METHOD
40
+ CLASS_METHOD
41
+ if self.kind_of?(Class)
42
+ method_definitions += <<-INSTANCE_METHOD
43
+
44
+ def #{name}(&block)
45
+ unless @#{name}
46
+ @#{name} = NsOptions::Namespace.new('#{key}', &block)
47
+ @#{name}.options.build_from(self.class.#{name}.options, @#{name})
48
+ end
49
+ @#{name}
50
+ end
51
+
52
+ INSTANCE_METHOD
53
+ end
54
+ self.class_eval(method_definitions)
29
55
  self.send(name, &block)
30
56
  end
31
57
 
@@ -0,0 +1,68 @@
1
+ module NsOptions
2
+ module Helper
3
+
4
+ class Advisor
5
+ attr_accessor :namespace
6
+
7
+ def initialize(namespace)
8
+ self.namespace = namespace
9
+ end
10
+
11
+ def is_this_ok?(kind, name, from)
12
+ display = (kind == :option ? "option" : "sub-namespace")
13
+ if self.bad_methods.include?(name.to_sym)
14
+ message = self.bad_method_message(display, name)
15
+ exception = NsOptions::Errors::InvalidName.new(message, from)
16
+ raise(exception)
17
+ elsif self.is_already_defined?(name)
18
+ puts self.duplicate_message(name)
19
+ elsif self.not_recommended_methods.include?(name.to_sym)
20
+ puts self.not_recommended_method_message(display, name)
21
+ else
22
+ return false
23
+ end
24
+ puts "From: #{from.first}"
25
+ true
26
+ end
27
+
28
+ def is_this_option_ok?(name, from = nil)
29
+ self.is_this_ok?(:option, name, (from || caller))
30
+ end
31
+
32
+ def is_this_namespace_ok?(name, from = nil)
33
+ self.is_this_ok?(:namespace, name, (from || caller))
34
+ end
35
+
36
+ def is_already_defined?(name)
37
+ self.namespace.options.is_defined?(name) ||
38
+ self.namespace.options.is_namespace_defined?(name)
39
+ end
40
+
41
+ def bad_methods
42
+ @bad_methods ||= [ :option, :namespace, :define, :options ]
43
+ end
44
+
45
+ def not_recommended_methods
46
+ @not_recommended_methods ||= NsOptions::Namespace.instance_methods(false).map(&:to_sym)
47
+ end
48
+
49
+ def bad_method_message(kind, name)
50
+ [ "The #{kind} '#{name}' overwrites a namespace method that NsOptions depends on.",
51
+ "Please choose a different name for your #{kind}."
52
+ ].join(" ")
53
+ end
54
+ def duplicate_message(name)
55
+ [ "WARNING! '#{name}' has already been defined and will be overwritten.",
56
+ "It's likely that it will not behave as expected."
57
+ ].join(" ")
58
+ end
59
+ def not_recommended_method_message(kind, name)
60
+ [ "WARNING! The #{kind} '#{name}' overwrites a namespace method.",
61
+ "This will limit some of the functionality of NsOptions."
62
+ ].join(" ")
63
+ end
64
+
65
+ end
66
+
67
+ end
68
+ end
@@ -1,34 +1,57 @@
1
1
  module NsOptions
2
2
 
3
3
  module Helper
4
+ autoload :Advisor, 'ns-options/helper/advisor'
5
+
4
6
  module_function
5
7
 
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)
8
+ def find_and_define_namespace(namespace, name)
9
+ sub_namespace = namespace.options.get_namespace(name)
10
+ self.define_namespace_methods(namespace, name)
11
+ sub_namespace
10
12
  end
11
13
 
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)
14
+ def define_namespace_methods(namespace, name)
15
+ namespace.metaclass.class_eval <<-DEFINE_METHOD
16
+
17
+ def #{name}(&block)
18
+ namespace = self.options.namespaces.get("#{name}")
19
+ namespace.define(&block) if block
20
+ namespace
21
+ end
22
+
23
+ DEFINE_METHOD
24
24
  end
25
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)
26
+ def find_and_define_option(namespace, option_name)
27
+ option = namespace.options[option_name]
28
+ self.define_option_methods(namespace, option)
29
29
  option
30
30
  end
31
31
 
32
+ def define_option_methods(namespace, option)
33
+ namespace.metaclass.class_eval <<-DEFINE_METHOD
34
+
35
+ def #{option.name}(*args)
36
+ if !args.empty?
37
+ self.send("#{option.name}=", *args)
38
+ else
39
+ self.options.get(:#{option.name})
40
+ end
41
+ end
42
+
43
+ def #{option.name}=(*args)
44
+ value = args.size == 1 ? args.first : args
45
+ self.options.set(:#{option.name}, value)
46
+ end
47
+
48
+ DEFINE_METHOD
49
+ end
50
+
51
+ def advisor(namespace)
52
+ NsOptions::Helper::Advisor.new(namespace)
53
+ end
54
+
32
55
  end
33
56
 
34
57
  end