cockpit 0.0.1.7 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.markdown CHANGED
@@ -1,225 +1,126 @@
1
- # Cockpit
1
+ <h1>Cockpit <img src='http://imgur.com/oXAb6.png' width='16' height='15'/></h1>
2
2
 
3
- <q>Super DRY Settings for Ruby, Rails, and Sinatra Apps.</q>
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.
4
4
 
5
- I am going to use this in all future gems that need configurable variables. Reason being, every gem uses _some_ configurable variables, and you end up writing the same code over and over again.
5
+ ## How it works
6
6
 
7
- This will make it so if say 10 gems have custom settings, you can change this:
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.
8
8
 
9
- Paperclip.config = ...
10
- S3.configure = ...
11
- Authlogic.setup = ...
12
- MyApp.settings = ....
13
-
14
- to this:
9
+ You define settings like this:
15
10
 
16
- Settings do
17
- paperclip ...
18
- s3 ...
19
- authentication ...
20
- app ...
11
+ settings = Cockpit "mongo" do
12
+ site do
13
+ title "My Site"
14
+ time_zone lambda { "Hawaii" }
15
+ feed do
16
+ per_page 10
17
+ formats %w(rss atom)
18
+ end
19
+ end
21
20
  end
22
21
 
23
- Paperclip.config = Settings(:paperclip)
24
- S3.configure = Settings(:s3)
25
- Authlogic.setup = Settings(:authentication)
26
- MyApp.settings = Settings(:app)
27
-
28
- Which translates to 1, uniform, clean, configuration file.
29
-
30
- ## Install
22
+ That gives you [this data structure](http://gist.github.com/558480), which is accessed internally as a flat hash with keys like this:
31
23
 
32
- sudo gem install cockpit
33
-
34
- ## Usage
24
+ ["site.feed.formats", "site.time_zone", "site.feed.per_page", "site", "site.feed", "site.title"]
25
+
26
+ ## Global and Instance Settings
35
27
 
36
- ### Migration
28
+ By default you will have 1 set of global settings, accessible via `Cockpit::Settings.root` which is populated in this call:
37
29
 
38
- create_table :settings, :force => true do |t|
39
- t.string :key
40
- t.string :value
41
- t.string :cast_as
42
- t.string :configurable_type
43
- t.integer :configurable_id
30
+ Cockpit "mongo" do
31
+ site do
32
+ author "Lance"
33
+ end
44
34
  end
35
+
36
+ If you want to have settings encapsulated in an independent scope, you can just assign that to a variable:
45
37
 
46
- ### Setup (`config/initializers/settings.rb`)
47
-
48
- Cockpit do
38
+ site_settings = Cockpit "mongo" do
49
39
  site do
50
- title "Martini", :tooltip => "Set your title!"
51
- tagline "Developer Friendly, Client Ready Blog with Rails 3"
52
- keywords "Rails 3, Heroku, JQuery, HTML 5, Blog Engine, CSS3"
53
- copyright "© 2010 Viatropos. All rights reserved."
54
- timezones :value => lambda { TimeZone.first }, :options => lambda { TimeZone.all }
55
- date_format "%m %d, %Y"
56
- time_format "%H"
57
- week_starts_on "Monday", :options => ["Monday", "Sunday", "Friday"]
58
- language "en-US", :options => ["en-US", "de"]
59
- touch_enabled true
60
- touch_as_subdomain false
61
- google_analytics ""
62
- teasers :title => "Teasers" do
63
- disable false
64
- left 1, :title => "Left Teaser"
65
- right 2
66
- center 3
67
- end
68
- main_quote 1
40
+ author "Lance"
69
41
  end
70
- asset :title => "Asset (and related) Settings" do
71
- thumb do
72
- width 100, :tip => "Thumb's width"
73
- height 100, :tip => "Thumb's height"
74
- end
75
- medium do
76
- width 600, :tip => "Thumb's width"
77
- height 250, :tip => "Thumb's height"
42
+ end
43
+
44
+ ## Associated Settings with Models
45
+
46
+ You can also associate settings with any object (plain Object, ActiveRecord, MongoMapper::Document, etc.):
47
+
48
+ class User < ActiveRecord::Base
49
+ include Cockpit
50
+
51
+ cockpit "mongo" do
52
+ preferences do
53
+ favorite_color "red"
78
54
  end
79
- large do
80
- width 600, :tip => "Large's width"
81
- height 295, :tip => "Large's height"
55
+ settings do
56
+ google_analytics "123123123"
82
57
  end
83
58
  end
84
- authentication :title => "Authentication Settings" do
85
- use_open_id true
86
- use_oauth true
87
- end
88
- front_page do
89
- slideshow_tag "slideshow"
90
- slideshow_effect "fade"
91
- end
92
- page do
93
- per_page 10
94
- feed_per_page 10
95
- end
96
- people do
97
- show_avatars true
98
- default_avatar "/images/missing-person.png"
99
- end
100
- social do
101
- facebook "http://facebook.com/viatropos"
102
- twitter "http://twitter.com/viatropos"
103
- end
104
- s3 do
105
- key "my_key"
106
- secret "my_secret"
107
- end
108
59
  end
60
+
61
+ And access them like this:
109
62
 
110
- #### Get
63
+ user = User.new
64
+ user.cockpit["settings.google_analytics"] #=> "123123123"
65
+ user.cockpit["preferences.favorite_color"] = "green"
66
+
67
+ ## Swappable Backend
111
68
 
112
- Settings.get("site.title").value #=> "Martini"
113
- Settings.get("site.title.value") #=> "Martini"
114
- Settings("site.title").value #=> "Martini"
115
- Settings("site.title.value") #=> "Martini"
116
- Settings["site.title"].value #=> "Martini"
117
- Settings["site.title.value"] #=> "Martini"
118
- Settings.site.title.value #=> "Martini" # doesn't pass through store yet
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).
70
+
71
+ The current backends supported are these keys:
72
+
73
+ - mongodb (or 'mongo')
74
+ - redis
75
+ - active_record
76
+ - file
77
+ - memory
78
+ - yaml
79
+
80
+ It should be easy enough to wrap the rest of the Moneta adapters.
81
+
82
+ This is specified as the first DSL attribute:
83
+
84
+ Cockpit "redis" do
85
+ site do
86
+ author "Lance"
87
+ end
88
+ end
119
89
 
120
- #### Set
121
-
122
- Settings.set("site.title" => "Martini") #=> {:site => {:title => {:value => "Martini"}}}
123
- Settings("site.title" => "Martini") #=> {:site => {:title => {:value => "Martini"}}}
124
- Settings["site.title"] = "Martini" #=> {:site => {:title => {:value => "Martini"}}}
125
- Settings.site.title = "Martini" #=> {:site => {:title => {:value => "Martini"}}} # doesn't pass through store yet
126
-
127
- ### Key points
128
-
129
- - Each node is any word you want
130
- - You can nest them arbitrarily deep
131
- - You can use Procs
132
- - Values are type casted
133
- - Settings can be defined in yaml or using the DSL.
134
- - The preferred way to _get_ values is `Settings("path.to.value").value`
135
- - You can add custom properties to each setting:
136
- - `Settings("site.title").tooltip #=> "Set your title!"`
137
- - You have multiple storage options:
138
- - `Settings.store = :db`: Syncs setting to/from ActiveRecord
139
- - `Settings.store = :memory`: Stores everything in a Hash (memoized, super fast)
140
- - You can specify them on a per-model basis.
141
-
142
- Example:
90
+ ## Use Cases
143
91
 
144
- class User < ActiveRecord::Base
145
- acts_as_configurable :settings do
146
- name "Lance", :title => "First Name", :options => ["Lance", "viatropos"]
147
- favorite do
148
- color "red"
149
- end
92
+ This makes it really easy to edit random settings from an interface, such as an admin panel. Next goal is to add callbacks around save/destroy so you can run processes when settings are changed (such as changing your google_analytics, which would require re-rendering views if they were cached).
93
+
94
+ The goal is to make this [enormous configuration dsl work](http://gist.github.com/558432), so I can define an entire site in a DSL.
95
+
96
+ ### Other API Notes
97
+
98
+ When you specify the DSL, that creates a flat tree of defaults, which aren't saved to the database. Then when you update the setting, it saves to the database, otherwise when the value is read and is null, it will use the default from the in-memory/dsl-defined tree.
99
+
100
+ 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
+
102
+ Cockpit "active_record" do
103
+ site do
104
+ time_zones "MST", :options => Proc.new { TZInfo::Timezone.all.map(&:name) }
150
105
  end
151
106
  end
152
107
 
153
- User.new.settings #=> <#Settings @tree={
154
- :favorite => {
155
- :color => {:type=>:string, :value=>"red"}
156
- },
157
- :name => {:type=>:string, :title=>"First Name", :value=>"Lance", :options=>["Lance", "Viatropos"]}
158
- }/>
108
+ assert_equal TZInfo::Timezone.all.map(&:name), Cockpit::Settings["site.time_zones"][:options].call
109
+
110
+ And you can access the definition object directly:
111
+
112
+ Cockpit::Settings.definition("site.time_zones").attributes[:options]
113
+
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:
119
+
120
+ class Bookmark < ActsAsJoinable::Relationship; end
121
+
122
+ class User < ActiveRecord::Base
123
+ joins :posts, :as => :parent, :through => :bookmark
124
+ end
159
125
 
160
- ### Why
161
-
162
- There's no standard yet for organizing random properties in Rails apps. And settings should be able to be modified through an interface (think Admin panel).
163
-
164
- Cockpit encapsulates the logic common to:
165
-
166
- - Options
167
- - Preferences
168
- - Settings
169
- - Configuration
170
- - Properties and Attributes
171
- - Key/Value stores
172
-
173
- Sometimes you need a global store, sometimes that global store needs to be customizable by the user, sometimes each user has their own set of configurations. This handles all of those cases.
174
-
175
- ## Todo
176
-
177
- - Add ability to `freeze` certain branches of the tree (so plugins can use it and know `Settings.clear` won't remove it)
178
- - Settings should be sorted by the way they were constructed
179
- - Check type, so when it is saved it knows what to do.
180
- - Store global declarations in memory
181
- - Create "context" for each set of settings, giving it its own `tree`. Allows mimicking subclasses.
182
- - `Settings` should be a collection of trees or `contexts`:
183
- Settings
184
- user
185
- global
186
- default
187
- user_a
188
- user_b
189
- widget
190
- global
191
- default
192
- widget_a
193
- widget_b
194
- text
195
- default
196
- widget_a
197
- widget_b
198
- social
199
- default
200
- widget_a
201
- widget_b
202
- Settings.for(:widget, :social) #=> default social widget settings.
203
-
204
- This ended up being very similar to i18n:
205
-
206
- - [http://guides.rubyonrails.org/i18n.html](http://guides.rubyonrails.org/i18n.html)
207
- - [I asked about this on the i18n lighthouse](http://i18n.lighthouseapp.com/projects/14947/tickets/21-abstract-out-configuration-functionality-from-i18n-into-separate-gem#ticket-21-1)
208
-
209
- I think the i18n gem should be broken down into two parts: Configuration (key/value store), and Translation.
210
-
211
- #### End Goal
212
-
213
- - Base key-value functionality gem, which allows you to store arbitrary key values in any database (similar to moneta). Should store settings in MongoDB by default.
214
- - i18n and Cockpit build on top of that
215
-
216
- ### Alternatives
217
-
218
- - [Preferences](http://github.com/pluginaweek/preferences)
219
- - [SettingsGoo](http://rubygems.org/gems/settings-goo)
220
- - [RailsSettings](http://github.com/Squeegy/rails-settings)
221
- - [SimpleConfig](http://github.com/lukeredpath/simpleconfig)
222
- - [Configatron](http://github.com/markbates/configatron)
223
- - [RConfig](http://github.com/rahmal/rconfig)
224
- - [Serenity](http://github.com/progressions/serenity)
225
- - [ApplicationSettings](http://github.com/bradhaydon/application_settings)
126
+ <cite>copyright [@viatropos](http://viatropos.com) 2010</cite>
data/Rakefile CHANGED
@@ -5,16 +5,17 @@ require 'rake/gempackagetask'
5
5
  spec = Gem::Specification.new do |s|
6
6
  s.name = "cockpit"
7
7
  s.authors = ["Lance Pollard"]
8
- s.version = "0.0.1.7"
9
- s.summary = "Cockpit: Super DRY Configuration for Ruby, Rails, and Sinatra Apps"
8
+ s.version = "0.1.1"
9
+ s.summary = "Super DRY Configuration Management for Ruby, Rails, and Sinatra Apps. With Pluggable NoSQL/SQL backends using Moneta"
10
10
  s.homepage = "http://github.com/viatropos/cockpit"
11
11
  s.email = "lancejpollard@gmail.com"
12
- s.description = "Super DRY Configuration for Ruby, Rails, and Sinatra Apps"
12
+ s.description = "Super DRY Configuration for Ruby, Rails, and Sinatra Apps. With Pluggable NoSQL/SQL backends using Moneta"
13
13
  s.has_rdoc = false
14
14
  s.rubyforge_project = "cockpit"
15
15
  s.platform = Gem::Platform::RUBY
16
- s.files = %w(README.markdown Rakefile init.rb MIT-LICENSE) + Dir["{lib,rails,test,app}/**/*"] - Dir["test/tmp"]
16
+ s.files = %w(README.markdown Rakefile MIT-LICENSE) + Dir["{lib,test,app}/**/*"] - Dir["test/tmp"]
17
17
  s.require_path = "lib"
18
+ s.add_dependency("moneta")
18
19
  end
19
20
 
20
21
  Rake::GemPackageTask.new(spec) do |pkg|
@@ -0,0 +1,93 @@
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
16
+ else
17
+ self.attributes = {}
18
+ end
19
+ else
20
+ self.attributes ||= {}
21
+ 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
29
+ 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)
39
+ 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
52
+ end
53
+ hash.merge(sub_definition)
54
+ end
55
+ else
56
+ {key => self}
57
+ 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
65
+ 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)
71
+ else
72
+ self.value << Cockpit::Definition.new(method, *args, &block)
73
+ end
74
+ 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)
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,55 @@
1
+ module Cockpit
2
+ # settings have one direct definition and many child definitions
3
+ class Definitions < Hash
4
+ attr_accessor :name, :scope
5
+
6
+ def initialize(*args, &block)
7
+ define!(*args, &block)
8
+ end
9
+
10
+ def define!(*args, &block)
11
+ options = args.extract_options!
12
+ options[:store] ||= args.first
13
+ options.each do |k, v|
14
+ send("#{k}=", v) if respond_to?("#{k}=")
15
+ end
16
+ raise ArgumentError.new("pass a :name to Cockpit::Setting.define!") if self.name.blank?
17
+ if block_given?
18
+ self << Cockpit::Definition.define!(&block)
19
+ end
20
+ self
21
+ end
22
+
23
+ def <<(value)
24
+ ([value] + self.values).flatten.uniq.each do |definition|
25
+ self.merge!(definition.keys)
26
+ end
27
+ self
28
+ end
29
+
30
+ def []=(key, value)
31
+ self << Cockpit::Definition.new(key, value) unless has_key?(key)
32
+ super(key.to_s, value)
33
+ end
34
+
35
+ def [](key)
36
+ super(key.to_s)
37
+ end
38
+
39
+ def to_hash
40
+ keys.inject({}) do |hash, key|
41
+ hash[key] = self[key].value
42
+ hash
43
+ end
44
+ end
45
+
46
+ def method_missing(method, *args, &block)
47
+ if has_key?(method)
48
+ self[method]
49
+ else
50
+ super(method, *args, &block)
51
+ end
52
+ end
53
+
54
+ end
55
+ end
@@ -0,0 +1,90 @@
1
+ module Cockpit
2
+ def self.included(base)
3
+ base.send(:include, ObjectInclude)
4
+ if defined?(::ActiveRecord::Base) && base.ancestors.include?(::ActiveRecord::Base)
5
+ base.send(:include, ActiveRecordInclude)
6
+ end
7
+ end
8
+
9
+ module ActiveRecordInclude
10
+ def self.included(base)
11
+ base.class_eval do
12
+ def self.cockpit(*args, &block)
13
+ if block_given? || @cockpit.nil?
14
+ @cockpit = Cockpit::Settings.new(
15
+ :name => self.name.underscore.gsub(/[^a-z0-9]/, "_").squeeze("_"),
16
+ :store => :active_record,
17
+ &block
18
+ )
19
+
20
+ @cockpit.keys.each do |key|
21
+ next if key =~ /\./
22
+
23
+ define_method key do
24
+ send(:cockpit)[key]
25
+ end
26
+
27
+ define_method "#{key}?" do
28
+ !send(key).blank?
29
+ end
30
+ end
31
+
32
+ else
33
+ @cockpit
34
+ end
35
+ end
36
+
37
+ def cockpit
38
+ unless @cockpit
39
+ @cockpit = Cockpit::Settings.new(
40
+ :name => self.class.cockpit.name,
41
+ :store => :active_record,
42
+ :record => self
43
+ )
44
+ end
45
+
46
+ @cockpit
47
+ end
48
+
49
+ def get(key)
50
+ cockpit[key]
51
+ end unless respond_to?(:get)
52
+
53
+ def set(*args)
54
+ if args.last.is_a?(Hash)
55
+ cockpit.set(args.last)
56
+ else
57
+ cockpit[args.first] = args.last
58
+ end
59
+ end unless respond_to?(:set)
60
+ end
61
+ end
62
+ end
63
+
64
+ module ObjectInclude
65
+ def self.included(base)
66
+ base.class_eval do
67
+ def self.cockpit(*args, &block)
68
+ if block_given?
69
+ @cockpit = Cockpit::Settings.new(
70
+ :name => self.name.underscore.gsub(/[^a-z0-9]/, "_").squeeze("_"),
71
+ :scope => "default",
72
+ :store => args.first || "memory",
73
+ &block
74
+ )
75
+ else
76
+ @cockpit
77
+ end
78
+ end
79
+
80
+ def cockpit
81
+ unless @cockpit
82
+ @cockpit = self.class.cockpit.dup
83
+ end
84
+
85
+ @cockpit
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end