cockpit 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
File without changes
data/README.markdown CHANGED
@@ -1,17 +1,25 @@
1
1
  <h1>Cockpit <img src='http://imgur.com/oXAb6.png' width='16' height='15'/></h1>
2
2
 
3
- > Super DRY Settings for Ruby, Rails, and Sinatra Apps. Thin layer above wycat's [Moneta](http://github.com/wycats/moneta) for pluggable backend support.
3
+ > Super DRY Settings for Ruby, Rails, and Sinatra Apps with pluggable backend support.
4
+
5
+ ## Install
6
+
7
+ sudo gem install cockpit
4
8
 
5
9
  ## How it works
6
10
 
7
- You can define arbitrarily nested key/value pairs of any type, and customize them from an Admin panel or the terminal, and save them to the MySQL, MongoDB, Redis, or even a File.
11
+ You can define arbitrarily nested key/value pairs of any type, and customize them from an Admin panel or the terminal, and save them to the MySQL, MongoDB, Redis, in memory, or even a File.
12
+
13
+ 1. Settings can be associated with a model class
14
+ 2. Settings can be associated with a model instance, which can use the model class settings as defaults
15
+ 3. Settings can be global and not reference a model at all (basic key/value store).
8
16
 
9
17
  You define settings like this:
10
18
 
11
- settings = Cockpit "mongo" do
19
+ Cockpit :active_record do
12
20
  site do
13
21
  title "My Site"
14
- time_zone lambda { "Hawaii" }
22
+ time_zone :default => lambda { "Hawaii" }
15
23
  feed do
16
24
  per_page 10
17
25
  formats %w(rss atom)
@@ -19,73 +27,93 @@ You define settings like this:
19
27
  end
20
28
  end
21
29
 
22
- That gives you [this data structure](http://gist.github.com/558480), which is accessed internally as a flat hash with keys like this:
30
+ That gives you an instance of `Cockpit::Settings`, a tree data structure.
23
31
 
24
- ["site.feed.formats", "site.time_zone", "site.feed.per_page", "site", "site.feed", "site.title"]
25
-
26
- ## Global and Instance Settings
32
+ ## Global Settings API
27
33
 
28
- By default you will have 1 set of global settings, accessible via `Cockpit::Settings.root` which is populated in this call:
34
+ If you've defined your `:active_record` settings like above, which are _global_ settings, you can use them like this:
29
35
 
30
- Cockpit "mongo" do
31
- site do
32
- author "Lance"
33
- end
34
- end
36
+ ### Get Methods
37
+
38
+ Cockpit::Settings["site.feed.per_page"] #=> 10
39
+ Cockpit::Settings("site.feed.per_page") #=> 10
40
+ Cockpit::Settings.site.feed.per_page.value #=> 10
35
41
 
36
- If you want to have settings encapsulated in an independent scope, you can just assign that to a variable:
42
+ Everything ultimately passes through the hash form of the method, `Cockpit::Settings["path"]`, so that's the most optimized way to do it.
37
43
 
38
- site_settings = Cockpit "mongo" do
39
- site do
40
- author "Lance"
41
- end
44
+ You can also check to see if these settings exist:
45
+
46
+ Cockpit::Settings.site.feed.per_page? #=> true
47
+
48
+ ### Set Methods
49
+
50
+ Cockpit::Settings["site.feed.per_page"] = 20
51
+ Cockpit::Settings("site.feed.per_page", 20)
52
+ Cockpit::Settings.site.feed.per_page.value = 20
53
+
54
+ ### Behind the Scenes
55
+
56
+ When you define settings using the DSL, they get stored as `Cockpit::Spec` objects into a global hash in the `Cockpit::Settings` class, which is a dictionary of `specs[class][name] = spec`. Global specs aren't associated with a class (e.g. model class), so the `class` is `NilClass`. You can have multiple global settings classes if you'd like, just give them names:
57
+
58
+ Cockpit :store => :active_record, :name => :more_settings do
59
+ hello "world"
42
60
  end
43
61
 
44
- ## Associated Settings with Models
62
+ You can access specific global settings like this:
63
+
64
+ Cockpit::Settings.find(:more_settings).hello.value #=> "world"
65
+
66
+ ## Instance Settings API
45
67
 
46
68
  You can also associate settings with any object (plain Object, ActiveRecord, MongoMapper::Document, etc.):
47
69
 
48
70
  class User < ActiveRecord::Base
49
71
  include Cockpit
50
72
 
51
- cockpit "mongo" do
73
+ cockpit do
52
74
  preferences do
53
75
  favorite_color "red"
54
76
  end
55
77
  settings do
56
- google_analytics "123123123"
78
+ birthday :after => :queue_birthday_message, :if => lambda { |key, value|
79
+ value =~ /\d\d\/\d\d\/\d\d\d\d/ # 10/03/1986
80
+ }
81
+ number_of_children, Integer
57
82
  end
58
83
  end
84
+
85
+ def queue_birthday_message(key, value)
86
+ BirthdayMailer.enqueue(value # ,...)
87
+ end
59
88
  end
60
89
 
61
90
  And access them like this:
62
91
 
63
92
  user = User.new
64
- user.cockpit["settings.google_analytics"] #=> "123123123"
93
+ user.cockpit["settings.number_of_children"] #=> 300
65
94
  user.cockpit["preferences.favorite_color"] = "green"
95
+ user.cockpit.settings.number_of_children.value = 0
96
+ user.cockpit.preferences? #=> true
66
97
 
67
- ## Swappable Backend
98
+ If your model class doesn't have methods named after the cockpit keys, it will generate methods for you and delegate them to the `cockpit`:
68
99
 
69
- Thanks to the work behind Moneta, there's a clear interface to key/value stores (and some people have added ActiveRecord support which I've included in this).
100
+ user.preferences.favorite_color? #=> true
101
+ user.settings? #=> true
102
+ user.preferences.favorite_color.value = "turquoise"
103
+
104
+ ## Swappable Backend
70
105
 
71
106
  The current backends supported are these keys:
72
107
 
73
- - mongodb (or 'mongo')
74
- - redis
75
108
  - active_record
76
- - file
109
+ - mongo
77
110
  - memory
78
- - yaml
79
111
 
80
- It should be easy enough to wrap the rest of the Moneta adapters.
112
+ Soon, or as need be, I'll support redis, files, couchdb, etc. Haven't needed them yet.
81
113
 
82
- This is specified as the first DSL attribute:
114
+ ## Caching
83
115
 
84
- Cockpit "redis" do
85
- site do
86
- author "Lance"
87
- end
88
- end
116
+ For Active Record, Cockpit just adds a `has_many :settings` declaration to your model, and loads all of the settings on the first call, caching any further gets to settings for that model. This means basically that settings are extendable database attributes for your model.
89
117
 
90
118
  ## Use Cases
91
119
 
@@ -99,7 +127,7 @@ When you specify the DSL, that creates a flat tree of defaults, which aren't sav
99
127
 
100
128
  You can also associate a hash with each setting definition. This is great for say options, defaults, titles and tooltips, etc. Here's an example:
101
129
 
102
- Cockpit "active_record" do
130
+ Cockpit :active_record do
103
131
  site do
104
132
  time_zones "MST", :options => Proc.new { TZInfo::Timezone.all.map(&:name) }
105
133
  end
@@ -111,16 +139,11 @@ And you can access the definition object directly:
111
139
 
112
140
  Cockpit::Settings.definition("site.time_zones").attributes[:options]
113
141
 
114
- ## Customizations
115
-
116
- Not yet implemented, just ideas.
117
-
118
- If you want to extend the Relationship model and reference that in your child/parent classes, you can do that like so:
142
+ You can even do this in the terminal:
119
143
 
120
- class Bookmark < ActsAsJoinable::Relationship; end
121
-
122
- class User < ActiveRecord::Base
123
- joins :posts, :as => :parent, :through => :bookmark
124
- end
144
+ irb -r 'rubygems'
145
+ require 'cockpit'
146
+ Cockpit { site { title "Lance" } }
147
+ puts Cockpit::Settings["site.title"] #=> "Lance"
125
148
 
126
149
  <cite>copyright [@viatropos](http://viatropos.com) 2010</cite>
data/lib/cockpit.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require 'rubygems'
2
+ require 'defined-by'
2
3
 
3
4
  this = File.expand_path(File.dirname(__FILE__))
4
5
  Dir["#{this}/cockpit/*"].each { |c| require c unless File.directory?(c) }
@@ -1,92 +1,203 @@
1
1
  module Cockpit
2
- # This class defines default properties for a setting object, based on the DSL
3
- class Definition
4
- # keys is the nested keys associated with child values
5
- attr_accessor :key, :value, :keys, :nested, :parent, :attributes, :type
6
-
7
- def initialize(key, *args, &block)
8
- process(key, *args, &block)
9
- end
10
-
11
- def process(key, *args, &block)
12
- self.key = key.to_s
13
- if args.length >= 1
14
- if args.last.is_a?(Hash)
15
- self.attributes = args.pop
2
+ class Settings
3
+ # This class defines default properties for a setting object, based on the DSL
4
+ class Definition
5
+
6
+ class << self
7
+ def define!(options = {}, &block)
8
+ DefinedBy::DSL(&block).map do |key, value, dsl_block|
9
+ Cockpit::Settings::Definition.new(key, value, &dsl_block)
10
+ end
11
+ end
12
+ end
13
+
14
+ # keys is the nested keys associated with child values
15
+ attr_reader :key, :value
16
+ attr_reader :attributes, :type, :callbacks, :validation
17
+ attr_reader :nested
18
+
19
+ def initialize(key, *args, &block)
20
+ @key = key.to_s
21
+ @attributes = {}
22
+
23
+ if block_given?
24
+ @value = self.class.define!(&block)
25
+ @nested = true
16
26
  else
17
- self.attributes = {}
27
+ args = args.pop
28
+ if args.is_a?(Array)
29
+ if args.last.is_a?(Hash)
30
+ @attributes.merge!(args.pop)
31
+ end
32
+ if args.last.is_a?(Class)
33
+ @type = args.pop
34
+ end
35
+
36
+ args = args.pop if (args.length == 1)
37
+ end
38
+
39
+ if attributes.has_key?(:default)
40
+ @value = attributes.delete(:default)
41
+ else
42
+ if args.is_a?(Class)
43
+ @type = args
44
+ else
45
+ @value = args
46
+ end
47
+ end
48
+
49
+ @validation = attributes.delete(:if)
50
+ @callbacks = {
51
+ :before => attributes.delete(:before),
52
+ :after => attributes.delete(:after)
53
+ }
54
+
55
+ @type ||= @value.class
56
+ @nested = false
18
57
  end
19
- else
20
- self.attributes ||= {}
21
58
  end
22
- if block_given?
23
- self.value ||= []
24
- self.nested = true
25
- instance_eval(&block)
26
- else
27
- self.value = *args.first
28
- self.nested = false
59
+
60
+ def each(&block)
61
+ iterate(:each, &block)
29
62
  end
30
- end
31
-
32
- def [](key)
33
- if attributes.has_key?(key.to_sym)
34
- attributes[key.to_sym]
35
- elsif attributes.has_key?(key.to_s)
36
- attributes[key.to_s]
37
- else
38
- method_missing(key)
63
+
64
+ def map(&block)
65
+ iterate(:map, &block)
39
66
  end
40
- end
41
-
42
- def nested?
43
- self.nested == true
44
- end
45
-
46
- def keys(separator = ".")
47
- if nested?
48
- value.inject({key => self}) do |hash, definition|
49
- sub_definition = definition.keys.keys.inject({}) do |sub_hash, sub_key|
50
- sub_hash["#{key}#{separator}#{sub_key}"] = definition.keys[sub_key]
51
- sub_hash
67
+
68
+ def iterate(method, &block)
69
+ keys.send(method) do |key|
70
+ case block.arity
71
+ when 1
72
+ yield(key)
73
+ when 2
74
+ yield(key, value_for(key))
52
75
  end
53
- hash.merge(sub_definition)
54
76
  end
55
- else
56
- {key => self}
57
77
  end
58
- end
59
-
60
- def method_missing(method, *args, &block)
61
- method = method.to_s.gsub("=", "").to_sym
62
- if args.blank? && !block_given?
63
- result = self.value.detect do |definition|
64
- definition.key == method.to_s
78
+
79
+ def keys
80
+ @keys ||= get_keys(false, :keys)
81
+ end
82
+
83
+ def all_keys
84
+ @all_keys ||= get_keys(true, :all_keys)
85
+ end
86
+
87
+ def child(key)
88
+ flatten[key.to_s]
89
+ end
90
+
91
+ def value_for(key)
92
+ child(key).value rescue nil
93
+ end
94
+
95
+ def [](key)
96
+ value_for(key)
97
+ end
98
+
99
+ # map of nested key to definition
100
+ def flatten(separator = ".")
101
+ unless @flattened
102
+ if nested?
103
+ @flattened = value.inject({key => self}) do |hash, definition|
104
+ sub_definition = definition.keys.inject({}) do |sub_hash, sub_key|
105
+ sub_hash["#{key}#{separator}#{sub_key}"] = definition.child(sub_key)
106
+ sub_hash
107
+ end
108
+ hash.merge(sub_definition)
109
+ end
110
+ else
111
+ @flattened = {key => self}
112
+ end
65
113
  end
66
- result ? result.value : nil
67
- else
68
- old_value = self.value.detect { |definition| definition.key == method.to_s }
69
- if old_value
70
- old_value.process(method, *args, &block)
114
+
115
+ @flattened
116
+ end
117
+
118
+ def to_hash
119
+ flatten.inject({}) do |hash, key, definition|
120
+ hash[key] = definition.value unless definition.nested?
121
+ hash
122
+ end
123
+ end
124
+
125
+ def to_tree
126
+ {key => nested? ? value.map(&:to_tree) : value}
127
+ end
128
+
129
+ def nested?
130
+ self.nested == true
131
+ end
132
+
133
+ # callbacks
134
+ def with_callbacks(record, new_value, &block)
135
+ validate(record, new_value) do
136
+ callback(:before, record, new_value)
137
+ yield(new_value)
138
+ callback(:after, record, new_value)
139
+ end
140
+ end
141
+
142
+ def validate(record, new_value, &block)
143
+ yield if execute(validation, record, new_value)
144
+ end
145
+
146
+ def callback(name, record, new_value)
147
+ execute(callbacks[name], record, new_value)
148
+ end
149
+
150
+ def execute(executable, record, new_value)
151
+ return true unless executable
152
+
153
+ case executable
154
+ when String, Symbol
155
+ if record.respond_to?(executable)
156
+ case record.method(executable).arity
157
+ when 0
158
+ record.send(executable)
159
+ when 1
160
+ record.send(executable, key)
161
+ when 2
162
+ record.send(executable, key, new_value)
163
+ end
164
+ end
165
+ when Proc
166
+ case executable.arity
167
+ when 0, -1
168
+ if record
169
+ record.instance_eval(&executable)
170
+ else
171
+ executable.call
172
+ end
173
+ when 1
174
+ if record
175
+ record.instance_exec(key, &executable)
176
+ else
177
+ executable.call(key)
178
+ end
179
+ when 2
180
+ if record
181
+ record.instance_exec(key, new_value, &executable)
182
+ else
183
+ executable.call(key, new_value)
184
+ end
185
+ end
186
+ end
187
+ end
188
+
189
+ protected
190
+ def get_keys(include_self, method)
191
+ if nested?
192
+ @keys = include_self ? [key] : []
193
+ @keys += value.map(&method).flatten.map {|key| "#{self.key}.#{key}"}
71
194
  else
72
- self.value << Cockpit::Definition.new(method, *args, &block)
195
+ @keys = [key]
73
196
  end
74
197
  end
75
- end
76
-
77
- class << self
78
- # top-level declaration are the first keys in the chain
79
- def define!(*args, &block)
80
- @definitions = []
81
- instance_eval(&block) if block_given?
82
- definitions = @definitions
83
- @definitions = nil
84
- definitions
85
- end
86
-
87
- def method_missing(method, *args, &block)
88
- method = method.to_s.gsub("=", "").to_sym
89
- @definitions << Cockpit::Definition.new(method, *args, &block)
198
+
199
+ def call_proc(proc, record)
200
+
90
201
  end
91
202
  end
92
203
  end