cockpit 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/{MIT-LICENSE → MIT-LICENSE.markdown} +0 -0
- data/README.markdown +70 -47
- data/lib/cockpit.rb +1 -0
- data/lib/cockpit/core/definition.rb +187 -76
- data/lib/cockpit/core/global.rb +35 -0
- data/lib/cockpit/core/helpers.rb +12 -0
- data/lib/cockpit/core/include.rb +29 -74
- data/lib/cockpit/core/scope.rb +46 -0
- data/lib/cockpit/core/settings.rb +113 -74
- data/lib/cockpit/core/spec.rb +65 -0
- data/lib/cockpit/core/store.rb +34 -38
- data/lib/cockpit/stores/active_record.rb +128 -0
- data/lib/cockpit/stores/file_system.rb +11 -0
- data/lib/cockpit/stores/memory.rb +16 -0
- data/lib/cockpit/stores/mongo.rb +29 -0
- data/lib/cockpit/stores/redis.rb +11 -0
- metadata +16 -19
- data/Rakefile +0 -79
- data/lib/cockpit/core/definitions.rb +0 -55
- data/lib/cockpit/many/include.rb +0 -18
- data/lib/cockpit/many/settings.rb +0 -21
- data/lib/cockpit/moneta/active_record.rb +0 -120
- data/lib/cockpit/moneta/simple_active_record.rb +0 -93
- data/test/lib/database.rb +0 -22
- data/test/lib/user.rb +0 -10
- data/test/test_active_record.rb +0 -80
- data/test/test_helper.rb +0 -88
- data/test/test_mongo.rb +0 -82
- data/test/test_stores.rb +0 -125
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
|
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
|
-
|
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
|
30
|
+
That gives you an instance of `Cockpit::Settings`, a tree data structure.
|
23
31
|
|
24
|
-
|
25
|
-
|
26
|
-
## Global and Instance Settings
|
32
|
+
## Global Settings API
|
27
33
|
|
28
|
-
|
34
|
+
If you've defined your `:active_record` settings like above, which are _global_ settings, you can use them like this:
|
29
35
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
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
|
-
|
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
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|
-
|
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
|
73
|
+
cockpit do
|
52
74
|
preferences do
|
53
75
|
favorite_color "red"
|
54
76
|
end
|
55
77
|
settings do
|
56
|
-
|
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.
|
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
|
-
|
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
|
-
|
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
|
-
-
|
109
|
+
- mongo
|
77
110
|
- memory
|
78
|
-
- yaml
|
79
111
|
|
80
|
-
|
112
|
+
Soon, or as need be, I'll support redis, files, couchdb, etc. Haven't needed them yet.
|
81
113
|
|
82
|
-
|
114
|
+
## Caching
|
83
115
|
|
84
|
-
|
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
|
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
|
-
|
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
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
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,92 +1,203 @@
|
|
1
1
|
module Cockpit
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
-
|
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
|
-
|
23
|
-
|
24
|
-
|
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
|
-
|
31
|
-
|
32
|
-
|
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
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
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
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
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
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
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
|
-
|
195
|
+
@keys = [key]
|
73
196
|
end
|
74
197
|
end
|
75
|
-
|
76
|
-
|
77
|
-
|
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
|