setting_accessors 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +29 -0
  3. data/.ruby-gemset +1 -0
  4. data/.ruby-version +1 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +254 -0
  8. data/Rakefile +2 -0
  9. data/lib/generators/setting_accessors/install_generator.rb +38 -0
  10. data/lib/generators/setting_accessors/templates/migration.rb.erb +12 -0
  11. data/lib/generators/setting_accessors/templates/model.rb.erb +47 -0
  12. data/lib/generators/setting_accessors/templates/settings.yml +24 -0
  13. data/lib/setting_accessors.rb +26 -0
  14. data/lib/setting_accessors/accessor.rb +148 -0
  15. data/lib/setting_accessors/converter.rb +67 -0
  16. data/lib/setting_accessors/integration.rb +79 -0
  17. data/lib/setting_accessors/integration_validator.rb +15 -0
  18. data/lib/setting_accessors/internal.rb +108 -0
  19. data/lib/setting_accessors/setting_scaffold.rb +252 -0
  20. data/lib/setting_accessors/validator.rb +144 -0
  21. data/lib/setting_accessors/version.rb +3 -0
  22. data/lib/tasks/setting_accessors_tasks.rake +4 -0
  23. data/setting_accessors.gemspec +30 -0
  24. data/test/dummy/README.rdoc +28 -0
  25. data/test/dummy/Rakefile +6 -0
  26. data/test/dummy/app/assets/images/.keep +0 -0
  27. data/test/dummy/app/assets/javascripts/application.js +13 -0
  28. data/test/dummy/app/assets/stylesheets/application.css +15 -0
  29. data/test/dummy/app/controllers/application_controller.rb +5 -0
  30. data/test/dummy/app/controllers/concerns/.keep +0 -0
  31. data/test/dummy/app/helpers/application_helper.rb +2 -0
  32. data/test/dummy/app/mailers/.keep +0 -0
  33. data/test/dummy/app/models/.keep +0 -0
  34. data/test/dummy/app/models/concerns/.keep +0 -0
  35. data/test/dummy/app/models/setting.rb +59 -0
  36. data/test/dummy/app/models/user.rb +15 -0
  37. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  38. data/test/dummy/bin/bundle +3 -0
  39. data/test/dummy/bin/rails +4 -0
  40. data/test/dummy/bin/rake +4 -0
  41. data/test/dummy/config.ru +4 -0
  42. data/test/dummy/config/application.rb +25 -0
  43. data/test/dummy/config/boot.rb +5 -0
  44. data/test/dummy/config/database.yml +25 -0
  45. data/test/dummy/config/environment.rb +5 -0
  46. data/test/dummy/config/environments/development.rb +37 -0
  47. data/test/dummy/config/environments/production.rb +83 -0
  48. data/test/dummy/config/environments/test.rb +34 -0
  49. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  50. data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
  51. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  52. data/test/dummy/config/initializers/inflections.rb +16 -0
  53. data/test/dummy/config/initializers/mime_types.rb +4 -0
  54. data/test/dummy/config/initializers/session_store.rb +3 -0
  55. data/test/dummy/config/initializers/setting_accessors.rb +1 -0
  56. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  57. data/test/dummy/config/locales/en.yml +23 -0
  58. data/test/dummy/config/routes.rb +56 -0
  59. data/test/dummy/config/secrets.yml +22 -0
  60. data/test/dummy/config/settings.yml +23 -0
  61. data/test/dummy/db/migrate/20150102112106_create_users.rb +9 -0
  62. data/test/dummy/db/migrate/20150102115329_create_settings.rb +12 -0
  63. data/test/dummy/db/schema.rb +32 -0
  64. data/test/dummy/db/test.sqlite3 +0 -0
  65. data/test/dummy/lib/assets/.keep +0 -0
  66. data/test/dummy/log/.keep +0 -0
  67. data/test/dummy/public/404.html +67 -0
  68. data/test/dummy/public/422.html +67 -0
  69. data/test/dummy/public/500.html +66 -0
  70. data/test/dummy/public/favicon.ico +0 -0
  71. data/test/dummy/test/models/setting_test.rb +131 -0
  72. data/test/dummy/test/models/user_test.rb +5 -0
  73. data/test/generators/install_generator_test.rb +15 -0
  74. data/test/setting_accessors_test.rb +4 -0
  75. data/test/test_helper.rb +23 -0
  76. metadata +270 -0
@@ -0,0 +1,26 @@
1
+ require 'setting_accessors/version'
2
+ require 'setting_accessors/accessor'
3
+ require 'setting_accessors/converter'
4
+ require 'setting_accessors/integration'
5
+ require 'setting_accessors/integration_validator'
6
+ require 'setting_accessors/internal'
7
+ require 'setting_accessors/setting_scaffold'
8
+ require 'setting_accessors/validator'
9
+
10
+ ActiveRecord::Base.class_eval do
11
+ include SettingAccessors::Integration
12
+ end
13
+
14
+ module SettingAccessors
15
+ def self.setting_class
16
+ self.setting_class_name.constantize
17
+ end
18
+
19
+ def self.setting_class=(klass)
20
+ @@setting_class = klass.to_s
21
+ end
22
+
23
+ def self.setting_class_name
24
+ (@@setting_class ||= 'Setting').camelize
25
+ end
26
+ end
@@ -0,0 +1,148 @@
1
+ #
2
+ # Helper class to make accessing record specific settings easier
3
+ #
4
+
5
+ class SettingAccessors::Accessor
6
+
7
+ def initialize(record)
8
+ @record = record
9
+ @temp_settings = {}
10
+ end
11
+
12
+ #
13
+ # Gets a setting's value
14
+ #
15
+ def [](key)
16
+ @temp_settings[key.to_sym] || SettingAccessors.setting_class.get(key, @record)
17
+ end
18
+
19
+ def has_key?(key)
20
+ @temp_settings.has_key?(key.to_sym)
21
+ end
22
+
23
+ #
24
+ # Writes a setting's value
25
+ #
26
+ def []=(key, val)
27
+ set_value_before_type_cast(key, val)
28
+ @temp_settings[key.to_sym] = SettingAccessors::Internal.converter(value_type(key)).convert(val)
29
+ end
30
+
31
+ #
32
+ # Tries to find a setting for this record.
33
+ # If none is found, will return the default setting value
34
+ # specified in the setting config file.
35
+ #
36
+ def get_or_default(key)
37
+ self[key] || SettingAccessors.setting_class.get_or_default(key, @record)
38
+ end
39
+
40
+ #
41
+ # Tries to find a setting for this record first.
42
+ # If none is found, tries to find a global setting with the same name
43
+ #
44
+ def get_or_global(key)
45
+ self[key] || SettingAccessors.setting_class.get(key)
46
+ end
47
+
48
+ #
49
+ # Tries to find a setting for this record first,
50
+ # if none is found, it will return the given value instead.
51
+ #
52
+ def get_or_value(key, value)
53
+ self.has_key?(key) ? self[key] : value
54
+ end
55
+
56
+ def get_with_fallback(key, fallback = nil)
57
+ return self[key] if fallback.nil?
58
+
59
+ case fallback.to_s
60
+ when 'default' then get_or_default(key)
61
+ when 'global' then get_or_global(key)
62
+ else get_or_value(key, fallback)
63
+ end
64
+ end
65
+
66
+ #
67
+ # @return [String] the setting's value type in the +@record+'s context
68
+ #
69
+ def value_type(key)
70
+ SettingAccessors::Internal.setting_value_type(key, @record)
71
+ end
72
+
73
+ #----------------------------------------------------------------
74
+ # ActiveRecord Helper Methods Emulation
75
+ #----------------------------------------------------------------
76
+
77
+ def value_was(key, fallback = nil)
78
+ return SettingAccessors.setting_class.get(key, @record) if fallback.nil?
79
+
80
+ case fallback.to_s
81
+ when 'default' then SettingAccessors.setting_class.get_or_default(key, @record)
82
+ when 'global' then SettingAccessors.setting_class.get(key)
83
+ else fallback
84
+ end
85
+ end
86
+
87
+ def value_changed?(key)
88
+ self[key] != value_was(key)
89
+ end
90
+
91
+ def value_before_type_cast(key)
92
+ SettingAccessors::Internal.lookup_nested_hash(@values_before_type_casts, key.to_s) || self[key]
93
+ end
94
+
95
+ protected
96
+
97
+ #
98
+ # Keeps a record of the originally set value for a setting before it was
99
+ # automatically converted.
100
+ #
101
+ def set_value_before_type_cast(key, value)
102
+ @values_before_type_casts ||= {}
103
+ @values_before_type_casts[key.to_s] = value
104
+ end
105
+
106
+ #
107
+ # Validates the new setting values.
108
+ # If there is an accessor for the setting, the errors will be
109
+ # directly forwarded to it, otherwise to :base
110
+ #
111
+ # Please do not call this method directly, use the IntegrationValidator
112
+ # class instead, e.g.
113
+ #
114
+ # validates_with SettingAccessors::IntegrationValidator
115
+ #
116
+ def validate!
117
+ @temp_settings.each do |key, value|
118
+ validation_errors = SettingAccessors.setting_class.validation_errors(key, value, @record)
119
+ validation_errors.each do |message|
120
+ if @record.respond_to?("#{key}=")
121
+ @record.errors.add(key, message)
122
+ else
123
+ @record.errors.add :base, :invalid_setting, :name => key, :message => message
124
+ end
125
+ end
126
+ end
127
+ end
128
+
129
+ #
130
+ # Saves the new setting values into the database
131
+ # Please note that there is no check if the values changed their
132
+ # in the meantime.
133
+ #
134
+ # Also, this method expects that the settings were validated
135
+ # before using #validate! and will therefore not perform
136
+ # validations itself.
137
+ #
138
+ def persist!
139
+ @temp_settings.each do |key, value|
140
+ Setting.create_or_update(key, value, @record)
141
+ end
142
+ flush!
143
+ end
144
+
145
+ def flush!
146
+ @temp_settings = {}
147
+ end
148
+ end
@@ -0,0 +1,67 @@
1
+ #
2
+ # This class hopefully will hopefully one day mimic ActiveRecord's
3
+ # attribute assigning methods, meaning that a conversion to the column type
4
+ # is done as soon as a new value is assigned by the programmer.
5
+ #
6
+ # If the value cannot be parsed in the required type, +nil+ is assigned.
7
+ # Please make sure that you specify the correct validations in settings.yml
8
+ # to avoid this.
9
+ #
10
+ # Currently supported types:
11
+ # - Fixnum
12
+ # - String
13
+ # - Boolean
14
+ #
15
+ # If the type is 'polymorphic', it is not converted at all.
16
+ #
17
+ class SettingAccessors::Converter
18
+
19
+ def initialize(value_type)
20
+ @value_type = value_type
21
+ end
22
+
23
+ #
24
+ # Converts the setting's value to the correct type
25
+ #
26
+ def convert(new_value)
27
+ #If the value is set to be polymorphic, we don't have to convert anything.
28
+ return new_value if @value_type == 'polymorphic'
29
+
30
+ parse_method = :"parse_#{@value_type}"
31
+
32
+ if private_methods.include?(parse_method)
33
+ send(parse_method, new_value)
34
+ else
35
+ Rails.logger.warn("Invalid Setting type: #{@value_type}")
36
+ new_value
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def parse_boolean(value)
43
+ case value
44
+ when TrueClass, FalseClass
45
+ value
46
+ when String
47
+ return true if %w[true 1].include?(value.downcase)
48
+ return false if %w[false 0].include?(value.downcase)
49
+ nil
50
+ when Fixnum
51
+ return true if value == 1
52
+ return false if value.zero?
53
+ nil
54
+ else
55
+ nil
56
+ end
57
+ end
58
+
59
+ def parse_integer(value)
60
+ value.to_i
61
+ end
62
+
63
+ def parse_string(value)
64
+ value.to_s
65
+ end
66
+
67
+ end
@@ -0,0 +1,79 @@
1
+ module SettingAccessors::Integration
2
+ def self.included(base)
3
+ base.validates_with SettingAccessors::IntegrationValidator
4
+
5
+ #After the main record was saved, we can save its settings.
6
+ #This is necessary as the record might have been a new record
7
+ #without an ID yet
8
+ base.after_save do
9
+ settings.send(:persist!)
10
+ end
11
+
12
+ base.extend ClassMethods
13
+ end
14
+
15
+ module ClassMethods
16
+
17
+ #
18
+ # Generates a new accessor (=getter and setter) for the given setting
19
+ #
20
+ # @param [String, Symbol] setting_name
21
+ # The setting's name
22
+ #
23
+ # @param [Hash] options
24
+ # Options to customize the behaviour of the generated accessor
25
+ #
26
+ # @option options [Symbol, Object] :fallback (nil)
27
+ # If set to +:default+, the getter will return the setting's default
28
+ # value if no own value was specified for this record
29
+ #
30
+ # If set to +:global+, the getter will try to find a global
31
+ # setting if no record specific setting was found
32
+ #
33
+ # If set to another value, this value is used by default
34
+ #
35
+ # If not set at all or to +nil+, the getter will only search for a record specific
36
+ # setting and return +nil+ if none was specified previously.
37
+ #
38
+ def setting_accessor(setting_name, options = {})
39
+ fallback = options.delete(:fallback)
40
+
41
+ SettingAccessors::Internal.set_class_setting(self, setting_name, options)
42
+
43
+ #Create a virtual column in the models column hash.
44
+ #This is currently not absolutely necessary, but will become important once
45
+ #Time etc. are supported. Otherwise, Rails won't be able to e.g. automatically
46
+ #create multi-param fields in forms.
47
+ self.columns_hash[setting_name.to_s] = OpenStruct.new(type: SettingAccessors::Internal.setting_value_type(setting_name, self.new).to_sym)
48
+
49
+ #Getter
50
+ define_method(setting_name) do
51
+ settings.get_with_fallback(setting_name, fallback)
52
+ end
53
+
54
+ # Setter
55
+ define_method("#{setting_name}=") do |new_value|
56
+ settings[setting_name] = new_value
57
+ end
58
+
59
+ #NAME_was
60
+ define_method("#{setting_name}_was") do
61
+ settings.value_was(setting_name, fallback)
62
+ end
63
+
64
+ #NAME_before_type_cast
65
+ define_method("#{setting_name}_before_type_cast") do
66
+ settings.value_before_type_cast(setting_name)
67
+ end
68
+
69
+ #NAME_changed?
70
+ define_method("#{setting_name}_changed?") do
71
+ settings.value_changed?(setting_name)
72
+ end
73
+ end
74
+ end
75
+
76
+ def settings
77
+ @settings_accessor ||= SettingAccessors::Accessor.new(self)
78
+ end
79
+ end
@@ -0,0 +1,15 @@
1
+ #
2
+ # This class handles model validations for assigned records, e.g.
3
+ # if the settings are accessed using the Accessor class in this module.
4
+ # Only the new temp values are validated using the setting config.
5
+ #
6
+ # The main work is still done in the Accessor class, so we don't have
7
+ # to access its instance variables here, this class acts as a wrapper
8
+ # for Rails' validation chain
9
+ #
10
+
11
+ class SettingAccessors::IntegrationValidator < ActiveModel::Validator
12
+ def validate(record)
13
+ record.settings.send(:validate!)
14
+ end
15
+ end
@@ -0,0 +1,108 @@
1
+ #
2
+ # This module contains class methods used internally.
3
+ #
4
+ module SettingAccessors
5
+ module Internal
6
+
7
+ def self.ensure_nested_hash!(hash, *keys)
8
+ h = hash
9
+ keys.each do |key|
10
+ h[key] ||= {}
11
+ h = h[key]
12
+ end
13
+ end
14
+
15
+ def self.lookup_nested_hash(hash, *keys)
16
+ return nil if hash.nil?
17
+
18
+ h = hash
19
+ keys.each do |key|
20
+ return nil if h[key].nil?
21
+ h = h[key]
22
+ end
23
+ h
24
+ end
25
+
26
+ #
27
+ # Loads information about all settings from YAML file
28
+ # These are cached in the class so they don't have to be reloaded
29
+ # every time.
30
+ #
31
+ # Note: For development / test, this is flushed every time
32
+ #
33
+ def self.global_config
34
+ if Rails.env.test? || Rails.env.development?
35
+ YAML.load(File.open(Rails.root.join('config/settings.yml'))).deep_stringify_keys
36
+ else
37
+ @@config ||= YAML.load(File.open(Rails.root.join('config/settings.yml'))).deep_stringify_keys
38
+ end
39
+ end
40
+
41
+ #
42
+ # @return [TrueClass, FalseClass] +true+ if the setting is defined in config/settings.yml
43
+ #
44
+ def self.globally_defined_setting?(setting_name)
45
+ self.global_config[setting_name.to_s].present?
46
+ end
47
+
48
+ #
49
+ # Sets a class-specific setting
50
+ # For global settings, this is done in config/settings.yml
51
+ # Please do not call this method yourself, it is done automatically
52
+ # by using setting_accessor in your model class
53
+ #
54
+ def self.set_class_setting(klass, setting_name, options = {})
55
+ @@class_settings ||= {}
56
+
57
+ #If there are no options given, the setting *has* to be defined globally.
58
+ if options.empty? && !self.globally_defined_setting?(setting_name)
59
+ raise ArgumentError.new "The setting '#{setting_name}' in model '#{klass.to_s}' is neither globally defined nor did it receive options"
60
+
61
+ #A setting which is already globally defined, may not be re-defined on class base
62
+ elsif self.globally_defined_setting?(setting_name) && options.any?
63
+ raise ArgumentError.new("The setting #{setting_name} is already defined in config/settings.yml and may not be redefined in #{klass}")
64
+
65
+ #If the setting is defined on class base, we have to store its options
66
+ elsif options.any? && !self.globally_defined_setting?(setting_name)
67
+ self.ensure_nested_hash!(@@class_settings, klass.to_s)
68
+ @@class_settings[klass.to_s][setting_name.to_s] = options.deep_stringify_keys
69
+ end
70
+ end
71
+
72
+
73
+ #
74
+ # @return [Hash] configuration data regarding this setting
75
+ #
76
+ # - If it's a globally defined setting, the value is taken from config/settings.yml
77
+ # - If it's a setting defined in a setting_accessor call, the information is taken from this call
78
+ # - Otherwise, an empty hash is returned
79
+ #
80
+ def self.setting_data(setting_name, assignable = nil)
81
+ (assignable && self.get_class_setting(assignable.class, setting_name)) ||
82
+ self.global_config[setting_name.to_s] ||
83
+ {}
84
+ end
85
+
86
+ #
87
+ # @return [String] the given setting's value type
88
+ #
89
+ def self.setting_value_type(*args)
90
+ self.setting_data(*args)['type'] || 'polymorphic'
91
+ end
92
+
93
+ #
94
+ # @return [SettingAccessors::Converter] A value converter for the given type
95
+ #
96
+ def self.converter(value_type)
97
+ @@converters ||= {}
98
+ @@converters[value_type.to_sym] ||= SettingAccessors::Converter.new(value_type)
99
+ end
100
+
101
+ #
102
+ # @return [Hash, NilClass] Information about a class specific setting or +nil+ if it wasn't set before
103
+ #
104
+ def self.get_class_setting(klass, setting_name)
105
+ self.lookup_nested_hash(@@class_settings, klass.to_s, setting_name.to_s)
106
+ end
107
+ end
108
+ end