settler 1.0.3 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -1,6 +1,6 @@
1
1
  = Settler
2
2
 
3
- Settler can be used for defining application wide settings in Rails. Settings are loaded from a YAML file and stored in the database using ActiveRecord to allow users to update settings on the fly. The YAML configuration allows you to not only specify defaults, but setting value validations as well!
3
+ Settler can be used for defining application wide settings in Rails. Settings are loaded from a YAML file and stored in the database using ActiveRecord to allow users to update settings on the fly. The YAML configuration allows you to not only specify defaults, but setting value validations and typecasts as well!
4
4
 
5
5
  == Requirements
6
6
 
@@ -48,19 +48,14 @@ The initial version of settler.yml contains example settings for every default R
48
48
  validations:
49
49
  presence: true
50
50
 
51
- In this case, 'google_analytics_key' is the key of the setting. Every nested property is either an attribute of the setting or a list of validations:
51
+ In this case, 'google_analytics_key' is the key of the setting. Every nested property is either an attribute of the setting or a list of validations. Alt, value, editable and deletable are attributes of the Setting model. If a Setting with a given key does not exist, it is created with the attributes found. Therefore, you can consider these attributes as the default values for the setting. See the validations section for more info on validations.
52
52
 
53
- * Alt, value, editable and deletable are attributes of the Setting model. If a Setting with a given key does not exist, it is created with the attributes found. Therefore, you can consider these attributes as the default values for the setting.
54
- * Validations are not part of every setting, but are loaded on validation of a Setting instance. They apply to the value of the setting. The following validations can be created:
55
- - 'presence', true/false.
56
- - 'inclusion', followed by a YAML array (e.g. ['a','b','c']). Accepts a comma separated string as well.
57
-
58
- Note that you can use ERB in the configuration file if you need to. For example:
53
+ Note that you can use ERB in the configuration file if you need to. For example:
59
54
 
60
55
  google_analytics_key:
61
56
  value: '<%= GOOGLE_ANALYTICS_KEY %>'
62
57
 
63
- will evaluate the GOOGLE_ANALYTICS_KEY constant.
58
+ will evaluate the GOOGLE_ANALYTICS_KEY constant.
64
59
 
65
60
  == Access settings
66
61
 
@@ -79,11 +74,60 @@ In this case, 'google_analytics_key' is the key of the setting. Every nested pro
79
74
  >> Settler[:google_analytics_key]
80
75
  Setting Load (0.7ms) SELECT * FROM "settings" WHERE ("settings"."key" = 'google_analytics_key') LIMIT 1
81
76
  => "UA-xxxxx-x"
77
+
78
+ * Some named scopes for finding setting are available that may be useful as well:
79
+ * Setting.editable: returns all editable settings.
80
+ * Setting.deletable: returns all deletable settings.
81
+ * Setting.deleted: returns all deleted settings.
82
+
83
+ == Typecasting
84
+
85
+ * Setting values can be typecasted to several types. This can easily be done by adding a 'typecast' property to the settler.yml. For example:
86
+
87
+ float_value:
88
+ alt: A float that should be typecasted
89
+ value: '0.25'
90
+ typecast: float
91
+
92
+ will ensure that the value returned by this setting will always be a float (0.25) instead if the string representation of a float.
93
+
94
+ * The following 'typecasters' are available by default:
95
+ * integer
96
+ * float
97
+ * boolean
98
+
99
+ Note: the boolean typecaster will yield true when the value is '1','t' or 'true' (case ignored), false otherwise.
100
+
101
+ * It is possible to create and use your own typecaster instead of the built in typecasters. A custom type caster should look like this:
102
+
103
+ class CustomTypecaster < Typecaster
104
+ def typecast(value)
105
+ # typecast and return value
106
+ end
107
+ end
108
+
109
+ When you require this class in your application, you can easily use your typecaster by specifying its class name in the settler configuration:
110
+
111
+ custom_value:
112
+ alt: An integer that should be custom typecasted
113
+ value: 1
114
+ typecast: CustomTypecaster
115
+
116
+ == Validations
117
+
118
+ * Validations are not stored in every setting, but are loaded on validation of a Setting instance. They apply to the value of the setting. The following validations can be created:
119
+ - 'presence', true/false.
120
+ - 'inclusion', followed by a YAML array (e.g. ['a','b','c']). Accepts a comma separated string as well.
121
+ - 'format', followed by a YAML regex (e.g. !ruby/regexp "/^UA-.{6}-.$/i"). String can be given as well, which will be converted to a regex. Note that you will not be able to pass regex modifiers in that case, therefore the YAML regex syntax is recommended.
82
122
 
83
123
  == Changing / Destroying settings
84
124
 
85
125
  * As settings are represented by a Setting ActiveRecord model, you can just update and destroy them like you would update or destroy any AR model.
86
126
 
127
+ * The 'key' attribute is read only as it should never be changed through your application.
128
+
129
+ * The 'editable', 'deletable' and 'deleted' attributes are protected from mass assignment using attr_protected.
130
+
87
131
  * By default, settings will only be editable or deletable iff the corresponding attribute is set to 'true'. This will be enforced before saving or destroying a record:
88
132
 
89
133
  >> Settler.google_analytics_key.destroy
@@ -128,10 +172,6 @@ In this case, 'google_analytics_key' is the key of the setting. Every nested pro
128
172
 
129
173
  Settler.raise_missing = true
130
174
 
131
- == To do
132
-
133
- * Add more validations, for now only 'presence' and 'inclusion' are supported.
134
-
135
175
  == Note on Patches/Pull Requests
136
176
 
137
177
  * Fork the project.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.0.3
1
+ 1.1.0
data/init.rb CHANGED
@@ -1,3 +1,4 @@
1
1
  require 'hash_extension'
2
2
  require 'setting'
3
- require 'settler'
3
+ require 'settler'
4
+ require 'type_casters'
data/lib/setting.rb CHANGED
@@ -1,5 +1,10 @@
1
+ require 'type_casters'
2
+
1
3
  # The Setting class is an AR model that encapsulates a Settler setting. The key if the setting is the only required attribute.\
2
4
  class Setting < ActiveRecord::Base
5
+ attr_readonly :key
6
+ attr_protected :editable, :deletable, :deleted
7
+
3
8
  before_update :ensure_editable
4
9
 
5
10
  validates_presence_of :key
@@ -8,6 +13,28 @@ class Setting < ActiveRecord::Base
8
13
  serialize :value
9
14
 
10
15
  default_scope :conditions => ['deleted = ? or deleted IS NULL', false]
16
+ named_scope :editable, :conditions => { :editable => true }
17
+ named_scope :deletable, :conditions => { :deletable => true }
18
+
19
+ # Returns the value, typecasted if a typecaster is available.
20
+ def value
21
+ typecast.present? ? typecasted_value : super
22
+ end
23
+
24
+ # Reads the raw, untypecasted value.
25
+ def untypecasted_value
26
+ read_attribute(:value)
27
+ end
28
+
29
+ # Returns the typecasted value or the raw value if a typecaster could not be found.
30
+ def typecasted_value
31
+ typecaster.present? ? typecaster.typecast(untypecasted_value) : untypecasted_value
32
+ end
33
+
34
+ # Finds the typecast for this key in the settler configuration.
35
+ def typecast
36
+ @typecast ||= Settler.typecast_for(key)
37
+ end
11
38
 
12
39
  # Returns all valid values for this setting, which is based on the presence of an inclusion validator.
13
40
  # Will return nil if no valid values could be determined.
@@ -38,12 +65,18 @@ class Setting < ActiveRecord::Base
38
65
  Setting.with_exclusive_scope(&block)
39
66
  end
40
67
 
68
+ # Deleted scope is specified as a method as it needs to be an exclusive scope
69
+ def self.deleted
70
+ Setting.without_default_scope{ Setting.all :conditions => { :deleted => true } }
71
+ end
72
+
41
73
  protected
42
74
 
43
75
  # Performs instance validations as defined in the settler configuration.
44
76
  def setting_validations
45
77
  errors.add(:value, I18n.t(:blank, :scope => 'activerecord.errors.messages')) if validators['presence'] && ['1','true',true].include?(validators['presence']) && self.value.nil?
46
78
  errors.add(:value, I18n.t(:inclusion, :scope => 'activerecord.errors.messages')) if valid_values && !valid_values.include?(self.value)
79
+ errors.add(:value, I18n.t(:format, :scope => 'activerecord.errors.messages')) if validators['format'] && !(Regexp.new(validators['format']) =~ self.value)
47
80
  end
48
81
 
49
82
  # Retrieves all validators defined for this setting.
@@ -51,7 +84,12 @@ protected
51
84
  @validators ||= Settler.validations_for(self.key)
52
85
  end
53
86
 
87
+ # Retrieves the typecaster instance that will typecast the value of this setting.
88
+ def typecaster
89
+ @typecaster ||= Typecaster.typecaster_for(typecast)
90
+ end
91
+
54
92
  # Before_update filter to ensure uneditable settings cannot be updated.
55
- def ensure_editable; return false unless editable? end
93
+ def ensure_editable; return false unless editable? end
56
94
 
57
95
  end
data/lib/settler.rb CHANGED
@@ -1,6 +1,6 @@
1
- require "yaml"
2
- require "erb"
3
- require "hash_extension"
1
+ require 'yaml'
2
+ require 'erb'
3
+ require 'hash_extension'
4
4
 
5
5
  # Settler loads and manages application wide settings and provides an interface for retrieving settings. The Settler
6
6
  # object cannot be instantiated; all functionality is available on class level.
@@ -13,9 +13,17 @@ class Settler
13
13
  # Loads the settler configuration from settler.yml and defines methods for retrieving the found settings.
14
14
  def load!
15
15
  raise "Source settler.yml not set. Please create one and set it by using Settler.source = <file>. When using Rails, please create a settler.yml file in the config directory." unless source
16
+
16
17
  self.config = YAML.load(ERB.new(File.read(source)).result).to_hash
17
18
  self.config = config[namespace] if namespace
18
- self.config.each{ |key, attributes| Setting.without_default_scope { Setting.find_or_create_by_key(attributes.only(:alt, :value, :editable, :deletable).merge(:key => key)) } }
19
+ self.config.each do |key, attributes|
20
+ Setting.without_default_scope do
21
+ Setting.find_or_create_by_key(attributes.only(:alt, :value).merge(:key => key)) do |s|
22
+ s.editable = attributes['editable']
23
+ s.deletable = attributes['deletable']
24
+ end
25
+ end
26
+ end
19
27
  Setting.all.each{ |s| key = s.key; Settler.class.send(:define_method, key){ Setting.find_by_key(key) } }
20
28
  end
21
29
 
@@ -26,9 +34,9 @@ class Settler
26
34
  end
27
35
 
28
36
  # Returns an array of all setting keys
29
- def settings
37
+ def settings(options = {})
30
38
  Settler.load! if config.nil?
31
- Setting.all.map(&:key)
39
+ Setting.all(:order => options[:order]).map(&:key)
32
40
  end
33
41
 
34
42
  # Returns a list of validations to perform on a setting.
@@ -38,6 +46,12 @@ class Settler
38
46
  (setting = config[key.to_s]) ? setting['validations'] || {} : {}
39
47
  end
40
48
 
49
+ # Returns the typecast for a setting (if any).
50
+ def typecast_for(key)
51
+ Settler.load! if config.nil?
52
+ (setting = config[key.to_s]) ? setting['typecast'] || nil : nil
53
+ end
54
+
41
55
  # Overrides the normal method_missing to return nil for non-existant settings. The behaviour of this method
42
56
  # depends on the boolean attributes raise_missing and report_missing.
43
57
  def method_missing(name, *args, &block)
@@ -0,0 +1,55 @@
1
+ require 'singleton'
2
+
3
+ # The TypeClaster class maintains a list of typecasters and retrieves the appropriate typecaster
4
+ # for a given data type.
5
+ class Typecaster
6
+ include Singleton
7
+
8
+ # Returns the list of available typecasters
9
+ def self.registered_typecasters
10
+ @@registered_typecasters ||= [self.instance, IntegerTypecaster.instance, FloatTypecaster.instance, BooleanTypecaster.instance]
11
+ end
12
+
13
+ # Returns the first typecaster from the list of typecasters that is able to typecast the passed type.
14
+ def self.typecaster_for(typecast)
15
+ typecaster = typecast.constantize.instance rescue nil
16
+ typecaster.present? && typecaster.is_a?(Typecaster) ? typecaster : registered_typecasters.detect{|tc| tc.type == typecast } || self.new
17
+ end
18
+
19
+ # Subclasses should implement this method and return the type of data it can typecast.
20
+ def type; nil end
21
+
22
+ # Subclasses should implement this method and return the typecasted value of the passed value.
23
+ # By default, the untypecasted value will be returned.
24
+ def typecast(value)
25
+ value
26
+ end
27
+ end
28
+
29
+ # Casts a value to an integer
30
+ class IntegerTypecaster < Typecaster
31
+ def type; 'integer' end
32
+
33
+ def typecast(value)
34
+ value.to_i
35
+ end
36
+ end
37
+
38
+ # Casts a value to a float
39
+ class FloatTypecaster < Typecaster
40
+ def type; 'float' end
41
+
42
+ def typecast(value)
43
+ value.to_f
44
+ end
45
+ end
46
+
47
+ # Casts a value to a boolean. '1', 't' or 'true' (case ignored) will evaluate to
48
+ # true, otherwise the result will be false.
49
+ class BooleanTypecaster < Typecaster
50
+ def type; 'boolean' end
51
+
52
+ def typecast(value)
53
+ !(/^(1|t|true)$/i =~ value.to_s).nil?
54
+ end
55
+ end
data/settler.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{settler}
8
- s.version = "1.0.3"
8
+ s.version = "1.1.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Reinier de Lange"]
12
- s.date = %q{2010-09-23}
12
+ s.date = %q{2010-09-26}
13
13
  s.description = %q{This gem is a combination of the Squeegy's rails-settings and Binarylogic's settingslogic gem, meaning it reads its configuration from a YAML file, but stores all settings in the database as well for on the fly changes.}
14
14
  s.email = %q{r.j.delange@nedforce.nl}
15
15
  s.extra_rdoc_files = [
@@ -30,7 +30,9 @@ Gem::Specification.new do |s|
30
30
  "lib/hash_extension.rb",
31
31
  "lib/setting.rb",
32
32
  "lib/settler.rb",
33
+ "lib/type_casters.rb",
33
34
  "settler.gemspec",
35
+ "test/custom_typecaster.rb",
34
36
  "test/database.yml",
35
37
  "test/helper.rb",
36
38
  "test/schema.rb",
@@ -43,7 +45,8 @@ Gem::Specification.new do |s|
43
45
  s.rubygems_version = %q{1.3.6}
44
46
  s.summary = %q{Settler manages global application settings in Rails}
45
47
  s.test_files = [
46
- "test/helper.rb",
48
+ "test/custom_typecaster.rb",
49
+ "test/helper.rb",
47
50
  "test/schema.rb",
48
51
  "test/test_settler.rb"
49
52
  ]
@@ -0,0 +1,5 @@
1
+ class CustomTypecaster < Typecaster
2
+ def typecast(value)
3
+ 'custom value'
4
+ end
5
+ end
data/test/settler.yml CHANGED
@@ -1,10 +1,11 @@
1
1
  settings:
2
2
  google_analytics_key:
3
- alt: Google analytics key
3
+ alt: Google analytics key
4
4
  value: 'UA-xxxxxx-x'
5
5
  editable: true
6
- deletable: false
6
+ deletable: false
7
7
  validations:
8
+ format: !ruby/regexp "/^UA-.{6}-.$/i"
8
9
  presence: true
9
10
  search_algorithm:
10
11
  alt: Default search engine
@@ -13,4 +14,20 @@ settings:
13
14
  deletable: true
14
15
  validations:
15
16
  inclusion: [ 'ferret' , 'sphinx' ]
17
+ bool_value:
18
+ alt: A boolean that should be typecasted
19
+ value: true
20
+ typecast: boolean
21
+ float_value:
22
+ alt: A float that should be typecasted
23
+ value: 0.25
24
+ typecast: float
25
+ integer_value:
26
+ alt: An integer that should be typecasted
27
+ value: 3
28
+ typecast: integer
29
+ custom_value:
30
+ alt: An integer that should be custom typecasted
31
+ value: 1
32
+ typecast: CustomTypecaster
16
33
 
data/test/test_settler.rb CHANGED
@@ -11,7 +11,7 @@ class TestSettler < Test::Unit::TestCase
11
11
  end
12
12
 
13
13
  def test_should_load_settings
14
- assert_equal ['google_analytics_key', 'search_algorithm'], Settler.settings
14
+ assert_equal ["bool_value", "custom_value", "float_value", "google_analytics_key", "integer_value", "search_algorithm"], Settler.settings(:order => :key)
15
15
  end
16
16
 
17
17
  def test_should_find_setting_value
@@ -20,7 +20,7 @@ class TestSettler < Test::Unit::TestCase
20
20
  end
21
21
 
22
22
  def test_should_get_validations_for_setting
23
- assert_equal({'presence' => true}, Settler.validations_for(:google_analytics_key))
23
+ assert Settler.validations_for(:google_analytics_key).keys.include?('presence')
24
24
  assert_equal({"inclusion"=>["ferret", "sphinx"]}, Settler.validations_for(:search_algorithm))
25
25
  end
26
26
 
@@ -31,7 +31,7 @@ class TestSettler < Test::Unit::TestCase
31
31
  end
32
32
 
33
33
  def test_should_report_or_raise_missing
34
- Settler.report_missing = true
34
+ #Settler.report_missing = true
35
35
  Settler.raise_missing = true
36
36
 
37
37
  assert_raise RuntimeError do
@@ -61,8 +61,68 @@ class TestSettler < Test::Unit::TestCase
61
61
 
62
62
  def test_should_update_editable_setting
63
63
  editable_setting = Settler.google_analytics_key
64
- assert editable_setting.update_attributes(:value => 'new_value')
65
- assert_equal 'new_value', Settler[:google_analytics_key]
66
- end
64
+ assert editable_setting.update_attributes(:value => 'UA-xxxxxx-1')
65
+ assert_equal 'UA-xxxxxx-1', Settler[:google_analytics_key]
66
+ end
67
+
68
+ def test_key_should_be_readonly_attribute
69
+ setting = Settler.google_analytics_key
70
+ setting.update_attribute(:key, 'new_key')
71
+ assert_equal 'google_analytics_key', setting.reload.key
72
+ end
73
+
74
+ def test_should_not_update_protected_attributes
75
+ setting = Settler.google_analytics_key
76
+ assert setting.update_attributes(:key => 'new_key', :alt => 'new_alt', :value => 'UA-xxxxxx-1', :editable => false, :deletable => true, :deleted => true)
77
+ setting.reload
78
+ assert_equal 'google_analytics_key', setting.key
79
+ assert_equal 'new_alt', setting.alt
80
+ assert_equal 'UA-xxxxxx-1', setting.value
81
+ assert setting.editable?
82
+ assert !setting.deletable?
83
+ assert_nil setting.deleted
84
+ end
85
+
86
+ def test_should_get_scopes
87
+ assert_equal [Settler.google_analytics_key], Setting.editable
88
+ assert_equal [Settler.search_algorithm], Setting.deletable
89
+
90
+ assert_equal [], Setting.deleted
91
+ deletable_setting = Settler.search_algorithm
92
+ assert deletable_setting.destroy
93
+ assert_equal [deletable_setting], Setting.deleted
94
+ end
95
+
96
+ def test_should_typecast
97
+ require 'custom_typecaster'
98
+ assert_equal 3, Settler.integer_value.value
99
+ assert_equal 0.25, Settler.float_value.value
100
+ assert_equal true, Settler.bool_value.value
101
+ assert_equal 'custom value', Settler.custom_value.value
102
+ end
103
+
104
+ def test_boolean_typecaster
105
+ bool_setting = Settler.bool_value
106
+ assert_equal true, bool_setting.value
107
+ bool_setting.update_attribute(:value, false)
108
+ assert_equal false, bool_setting.value
109
+ bool_setting.update_attribute(:value, 'f')
110
+ assert_equal false, bool_setting.value
111
+ bool_setting.update_attribute(:value, 'bla')
112
+ assert_equal false, bool_setting.value
113
+ bool_setting.update_attribute(:value, 't')
114
+ assert_equal true, bool_setting.value
115
+ bool_setting.update_attribute(:value, 'true')
116
+ assert_equal true, bool_setting.value
117
+ bool_setting.update_attribute(:value, 'TrUe')
118
+ assert_equal true, bool_setting.value
119
+ bool_setting.update_attribute(:value, 'tr')
120
+ assert_equal false, bool_setting.value
121
+ end
122
+
123
+ def test_should_validate_format
124
+ setting = Settler.google_analytics_key
125
+ assert !setting.update_attributes(:value => 'invalid_format')
126
+ end
67
127
 
68
128
  end
metadata CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
4
4
  prerelease: false
5
5
  segments:
6
6
  - 1
7
+ - 1
7
8
  - 0
8
- - 3
9
- version: 1.0.3
9
+ version: 1.1.0
10
10
  platform: ruby
11
11
  authors:
12
12
  - Reinier de Lange
@@ -14,7 +14,7 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2010-09-23 00:00:00 +02:00
17
+ date: 2010-09-26 00:00:00 +02:00
18
18
  default_executable:
19
19
  dependencies: []
20
20
 
@@ -41,7 +41,9 @@ files:
41
41
  - lib/hash_extension.rb
42
42
  - lib/setting.rb
43
43
  - lib/settler.rb
44
+ - lib/type_casters.rb
44
45
  - settler.gemspec
46
+ - test/custom_typecaster.rb
45
47
  - test/database.yml
46
48
  - test/helper.rb
47
49
  - test/schema.rb
@@ -78,6 +80,7 @@ signing_key:
78
80
  specification_version: 3
79
81
  summary: Settler manages global application settings in Rails
80
82
  test_files:
83
+ - test/custom_typecaster.rb
81
84
  - test/helper.rb
82
85
  - test/schema.rb
83
86
  - test/test_settler.rb