settingcrazy 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. data/.gitignore +17 -0
  2. data/.rvmrc +1 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE +22 -0
  5. data/README.md +245 -0
  6. data/Rakefile +2 -0
  7. data/TODO +10 -0
  8. data/db/safeattributes.db +0 -0
  9. data/lib/generators/settingcrazy/setting_values_migration/setting_values_migration_generator.rb +18 -0
  10. data/lib/generators/settingcrazy/setting_values_migration/templates/migration.rb +15 -0
  11. data/lib/settingcrazy.rb +23 -0
  12. data/lib/settingcrazy/class_methods.rb +32 -0
  13. data/lib/settingcrazy/inheritor.rb +31 -0
  14. data/lib/settingcrazy/instance_methods.rb +20 -0
  15. data/lib/settingcrazy/namespace.rb +14 -0
  16. data/lib/settingcrazy/namespaced_settings_proxy.rb +18 -0
  17. data/lib/settingcrazy/setting_value.rb +11 -0
  18. data/lib/settingcrazy/settings_proxy.rb +107 -0
  19. data/lib/settingcrazy/settings_validator.rb +43 -0
  20. data/lib/settingcrazy/template.rb +2 -0
  21. data/lib/settingcrazy/template/base.rb +66 -0
  22. data/lib/settingcrazy/template/enum.rb +19 -0
  23. data/lib/settingcrazy/version.rb +3 -0
  24. data/settingcrazy.gemspec +21 -0
  25. data/spec/inheritance_spec.rb +76 -0
  26. data/spec/mass_assignment_spec.rb +69 -0
  27. data/spec/namespaced_settings_proxy_spec.rb +70 -0
  28. data/spec/settingcrazy_spec.rb +78 -0
  29. data/spec/settings_proxy_spec.rb +55 -0
  30. data/spec/settings_validator_spec.rb +121 -0
  31. data/spec/spec_helper.rb +8 -0
  32. data/spec/support/models.rb +10 -0
  33. data/spec/support/models/campaign.rb +12 -0
  34. data/spec/support/models/case.rb +11 -0
  35. data/spec/support/models/clever_campaign.rb +13 -0
  36. data/spec/support/models/duck.rb +13 -0
  37. data/spec/support/models/farm.rb +10 -0
  38. data/spec/support/models/note.rb +12 -0
  39. data/spec/support/models/scenario.rb +13 -0
  40. data/spec/support/models/setting_value.rb +8 -0
  41. data/spec/support/models/templated_campaign.rb +11 -0
  42. data/spec/support/models/templated_scenario.rb +11 -0
  43. data/spec/support/models/vendor_instance.rb +9 -0
  44. data/spec/support/templates.rb +3 -0
  45. data/spec/support/templates/example_campaign_template.rb +24 -0
  46. data/spec/support/templates/example_template.rb +12 -0
  47. metadata +161 -0
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use 1.9.3@settingcrazy --create
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in settingcrazy.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Dan Draper
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,245 @@
1
+ # Settingcrazy
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'settingcrazy'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install settingcrazy
18
+
19
+ Finally, create a migration and run:
20
+
21
+ $ rails g settingcrazy:setting_values_migration
22
+ $ rake db:migrate
23
+
24
+ ## Usage
25
+
26
+ To use simply include the SettingCrazy module in any ActiveRecord model that you want to have settings:
27
+
28
+ class User < ActiveRecord::Base
29
+ include SettingCrazy
30
+ end
31
+
32
+ Your model will now have a settings method which you can use to get and set values.
33
+
34
+ user = User.first
35
+ user.settings => {}
36
+ user.settings.my_setting = "foo"
37
+ user.settings.my_setting => "foo"
38
+ user.settings => { "my_setting" => "foo" }
39
+
40
+ To persist, call save or save! on the parent. Eg;
41
+
42
+ user.save
43
+
44
+ ### Serializable Values
45
+
46
+ TODO
47
+
48
+ ### Setting Inheritance
49
+
50
+ Your settings can inherit from the settings of a parent.
51
+
52
+ class House < ActiveRecord::Base
53
+ include SettingCrazy
54
+ has_many :rooms
55
+ end
56
+
57
+ class Room < ActiveRecord::Base
58
+ include SettingCrazy
59
+ belongs_to :house
60
+ settings_inherit_via :house
61
+ end
62
+
63
+ house = House.create(...)
64
+ house.settings.color = "blue"
65
+ house.save!
66
+
67
+ room = house.rooms.create
68
+ room.settings.color => "blue"
69
+
70
+ ### Setting Templates
71
+
72
+ Available settings are specified through the use of setting templates. These tell settingcrazy the options that exist, and what possible values they can take.
73
+
74
+ class House < ActiveRecord::Base
75
+ include SettingCrazy
76
+ has_many :rooms
77
+ use_setting_template Settings::House
78
+ end
79
+
80
+ use_setting_template can also be passed a block, in case your model needs to use different templates, depending on the record itself.
81
+
82
+ class Room < ActiveRecord::Base
83
+ include SettingCrazy
84
+ belongs_to :house
85
+ settings_inherit_via :house
86
+
87
+ # Use the template associated with the room type of this room
88
+ use_setting_template do |record|
89
+ record.room_type.template
90
+ end
91
+ end
92
+
93
+ ####Enums
94
+
95
+ The template itself is a collection of enums. Any attempt to get or set a setting that is not defined in a template will result in an ActiveRecord::UnknownAttributeError.
96
+ The basic structure of an enum is:
97
+
98
+ enum :key, 'name', validation_options do
99
+ value 'value', 'key'
100
+ ...
101
+ end
102
+
103
+ class Settings::House < SettingCrazy::Template::Base
104
+ enum :bedroom_count, 'Room Count', {} do
105
+ value 1, 'One'
106
+ value 2, 'Two'
107
+ value 3, 'Three'
108
+ end
109
+ end
110
+
111
+ house = House.create(...)
112
+ house.settings.bedroom_count => nil
113
+ house.settings.bedroom_count = 1
114
+ house.save!
115
+
116
+ ####Validation
117
+
118
+ Note: Validation does not work with namespaces yet.
119
+
120
+ Settingcrazy always validates whether the value for an option is defined in the enums. There are a number of additional validation options available.
121
+
122
+ multiple (boolean) - Whether it is valid to save more than one entry for a single key
123
+ required (boolean) - Whether a value must be set for this enum
124
+ dependent ({ enum_key: setting_value }) - A value may only be set for this option if all of the options it is dependent on are set to the specified values
125
+ type (string) - Only current available value is 'text'. This causes settingcrazy to skip the range validation, so any value for this option will be valid.
126
+
127
+ class Settings::House < SettingCrazy::Template::Base
128
+ enum :is_furnished, 'Furnished', { multiple: false, required: true } do
129
+ value false, 'Not Furnished'
130
+ value true, 'Is Furnished'
131
+ end
132
+
133
+ enum :has_dining_table, 'Has Dining Table', { dependent: { is_furnished: true } } do
134
+ value false, 'No Dining Table'
135
+ value true, 'Dining Table'
136
+ end
137
+ end
138
+
139
+ house = House.create(...)
140
+ house.valid? => false
141
+ house.errors => { :is_furnished => ["Setting, 'Furnished', is required"] }
142
+ house.settings.is_furnished = false
143
+ house.valid? => true
144
+ house.settings.has_dining_table = false
145
+ house.valid? => false ("'Has Dining Table' can only be specified if 'Furnished' is set to 'Not Furnished'")
146
+ house.settings.is_furnished = true
147
+ house.valid? => true
148
+ house.settings.has_dining_table = 3
149
+ house.valid? => false ("'3' is not a valid setting for 'Has Dining Table'")
150
+
151
+
152
+ #####Defaults
153
+
154
+ Defaults enable both the ability to ensure the user starts with a valid object, and that the most common values are set on creation. To define defaults, create a class method in your template that returns a hash of default settings.
155
+
156
+ class Settings::House < SettingCrazy::Template::Base
157
+ # Assuming the enums for all these settings are defined below
158
+ def self.defaults
159
+ {
160
+ bedroom_count: 2,
161
+ is_furnished: false
162
+ }
163
+ end
164
+ end
165
+
166
+ house = House.create(...)
167
+ house.settings => {}
168
+ house.settings.bedroom_count => 2
169
+ house.settings.bedroom_count = 3
170
+ house.settings => { :bedroom_count => 3 }
171
+ house.is_furnished => false
172
+ house.has_dining_table => nil
173
+
174
+ ### Mass Assignment and Usage in Forms
175
+
176
+ You can easily bulk set settings using a hash.
177
+
178
+ house.settings = { :foo => "bar", :fruit => "apple" }
179
+
180
+ This extends to mass assignment:
181
+
182
+ house.attributes = {
183
+ :settings => { :foo => "bar", :fruit => "apple" }
184
+ }
185
+
186
+ If using in a form you can use fields for (eg in Haml):
187
+
188
+ = form.fields_for :settings, @house.settings do |setting|
189
+ = setting.text_field :foo
190
+
191
+ ### Setting Namespaces
192
+
193
+ If you have a model that needs settings to be divided in some way (perhaps by functional area) you can use a namespace.
194
+
195
+ class Scenario < ActiveRecord::Base
196
+ include SettingCrazy
197
+
198
+ setting_namespace :weekdays
199
+ setting_namespace :weekends
200
+ end
201
+
202
+ Now you can access your settings like so:
203
+
204
+ scenario = Scenario.find(...)
205
+ scenario.settings.weekends.foo = "bar"
206
+ scenario.settings.weekends.foo => "bar"
207
+ scenario.settings.weekdays.foo => nil
208
+
209
+ scenario.settings.weekends => { :foo => "bar" }
210
+
211
+ Not providing a namespace still works and will access all settings:
212
+
213
+ scenario.settings.foo => "bar"
214
+
215
+ Setting namespaces work with bulk setting and mass assignment too:
216
+
217
+ scenario.settings.weekends = { :foo => "bar" }
218
+
219
+ scenario.attributes = {
220
+ :settings => {
221
+ :weekends => {
222
+ :foo => "bar
223
+ },
224
+ },
225
+ }
226
+
227
+ You can also set templates to work per namespace:
228
+
229
+ setting_namespace :weekdays, :template => WeekDayTemplate
230
+
231
+ When inheriting via a parent with namespaces you may like to specify a namespace to inherit from instead of just the settings as a whole.
232
+
233
+ settings_inherit_via :house, :namespace => :a_namespace
234
+
235
+ Or, you can even use a proc
236
+
237
+ settings_inherit_via :house, :namespace => Proc.new { |room| room.parent_setting_namespace }
238
+
239
+ ## Contributing
240
+
241
+ 1. Fork it
242
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
243
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
244
+ 4. Push to the branch (`git push origin my-new-feature`)
245
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
data/TODO ADDED
@@ -0,0 +1,10 @@
1
+ * think some more about saving and record consistency. Settings don't get saved until we save the parent record. That may be OK but calling a getter should perhaps return the correct (albeit unsaved) value?
2
+ - implement new_record? changed? (maybe even changes) methods
3
+ - active record makes this hard to do in current form. We would actually need to build a second setting value when we change something and then replace the entire collection when we save the owner
4
+ * RDoc
5
+ * Serialize only for arrays (reject blanks too) - may need to add a serialize column
6
+ * Template Validators
7
+ * Template docs
8
+ * More validations in validator
9
+ - an enum_value is only available if another enum_value matches certain criteria, e.g, delivery_method can be set to 'ACCELERATED' only if bid_setting is 'manualCPC'
10
+ - validates format, e.g daily_budget and initial_key_cpc can only be decimals
Binary file
@@ -0,0 +1,18 @@
1
+ require 'rails'
2
+
3
+ module Settingcrazy
4
+ module Generators
5
+ class SettingValuesMigrationGenerator < Rails::Generators::Base
6
+ include Rails::Generators::Migration
7
+ source_root File.expand_path('../../../../../lib/generators/settingcrazy/setting_values_migration/templates/', __FILE__)
8
+
9
+ def self.next_migration_number(path)
10
+ Time.now.utc.strftime("%Y%m%d%H%M%S")
11
+ end
12
+
13
+ def create_setting_values_migration_file
14
+ migration_template 'migration.rb', 'db/migrate/create_settings_values'
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,15 @@
1
+ class CreateSettingsValues < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :setting_values do |t|
4
+ t.string :value
5
+ t.string :key
6
+ t.string :namespace
7
+ t.references :settable, :polymorphic => true
8
+ t.timestamps
9
+ end
10
+ end
11
+
12
+ def self.down
13
+ drop_table :setting_values
14
+ end
15
+ end
@@ -0,0 +1,23 @@
1
+ autoload :ActiveRecord, 'active_record'
2
+
3
+ require "settingcrazy/version"
4
+ require "settingcrazy/namespace"
5
+ require "settingcrazy/settings_proxy"
6
+ require "settingcrazy/namespaced_settings_proxy"
7
+ require "settingcrazy/setting_value"
8
+ require "settingcrazy/class_methods"
9
+ require "settingcrazy/instance_methods"
10
+ require "settingcrazy/inheritor"
11
+ require "settingcrazy/template"
12
+ require "settingcrazy/settings_validator"
13
+
14
+ module SettingCrazy
15
+ def self.included(base)
16
+ base.class_eval <<-EVAL
17
+ has_many :setting_values, :class_name => "SettingCrazy::SettingValue", :as => :settable, :autosave => true
18
+ validates_with SettingsValidator
19
+ EVAL
20
+ base.extend ClassMethods
21
+ base.send(:include, InstanceMethods)
22
+ end
23
+ end
@@ -0,0 +1,32 @@
1
+ module SettingCrazy
2
+ module ClassMethods
3
+ def use_setting_template(template = nil, &block)
4
+ @template = template || block
5
+ end
6
+
7
+ def settings_inherit_via(reflection, options = {})
8
+ @inheritor = Inheritor.new(reflection, options)
9
+ end
10
+
11
+ def setting_namespace(name, options = {})
12
+ @setting_namespaces ||= {}
13
+ @setting_namespaces[name.to_sym] = Namespace.new(name, options)
14
+ end
15
+
16
+ def _inheritor
17
+ @inheritor
18
+ end
19
+
20
+ def _setting_namespaces
21
+ @setting_namespaces
22
+ end
23
+
24
+ def setting_template(record)
25
+ case @template
26
+ when String then @template.constantize
27
+ when Class then @template
28
+ when Proc then @template.call(record)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,31 @@
1
+
2
+ module SettingCrazy
3
+ class Inheritor
4
+ def initialize(via, options = {})
5
+ # TODO: Do a reflection look up to make sure its valid
6
+ @via = via
7
+ @parent_namespace = options[:namespace]
8
+ end
9
+
10
+ # Returns the appropriate parent settings
11
+ def parent_settings_for(model)
12
+ return nil if model.send(@via).blank?
13
+ association = model.send(@via)
14
+ if pn = parent_namespace(model)
15
+ association.settings.send(pn)
16
+ else
17
+ association.settings
18
+ end
19
+ end
20
+
21
+ protected
22
+ def parent_namespace(model)
23
+ case @parent_namespace
24
+ when nil then nil
25
+ when String, Symbol then @parent_namespace
26
+ when Proc
27
+ @parent_namespace.call(model)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,20 @@
1
+
2
+ module SettingCrazy
3
+ module InstanceMethods
4
+ def assign_attributes(values, options = {})
5
+ if values.has_key?(:settings)
6
+ self.settings = values.delete(:settings)
7
+ end
8
+
9
+ super(values, options)
10
+ end
11
+
12
+ def settings
13
+ @settings ||= SettingsProxy.new(self, self.class.setting_template(self))
14
+ end
15
+
16
+ def settings=(attributes)
17
+ settings.bulk_assign(attributes)
18
+ end
19
+ end
20
+ end