ultra_settings 0.0.1.rc1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -5,51 +5,41 @@ module UltraSettings
5
5
  class Field
6
6
  attr_reader :name
7
7
  attr_reader :type
8
+ attr_reader :description
8
9
  attr_reader :default
9
10
  attr_reader :default_if
10
11
  attr_reader :env_var
11
- attr_reader :setting_name
12
+ attr_reader :runtime_setting
12
13
  attr_reader :yaml_key
13
- attr_reader :env_var_prefix
14
- attr_reader :env_var_upcase
15
- attr_reader :setting_prefix
16
- attr_reader :setting_upcase
17
14
 
18
15
  # @param name [String, Symbol] The name of the field.
19
16
  # @param type [Symbol] The type of the field.
17
+ # @param description [String] The description of the field.
20
18
  # @param default [Object] The default value of the field.
21
19
  # @param default_if [Proc] A proc that returns true if the default value should be used.
22
20
  # @param env_var [String, Symbol] The name of the environment variable to use for the field.
23
- # @param setting_name [String, Symbol] The name of the setting to use for the field.
21
+ # @param runtime_setting [String, Symbol] The name of the setting to use for the field.
24
22
  # @param yaml_key [String, Symbol] The name of the YAML key to use for the field.
25
- # @param env_var_prefix [String, Symbol] The prefix to use for the environment variable name.
26
- # @param env_var_upcase [Boolean] Whether or not to upcase the environment variable name.
27
- # @param setting_prefix [String, Symbol] The prefix to use for the setting name.
28
- # @param setting_upcase [Boolean] Whether or not to upcase the setting name.
29
23
  def initialize(
30
24
  name:,
31
25
  type: :string,
26
+ description: nil,
32
27
  default: nil,
33
28
  default_if: nil,
34
29
  env_var: nil,
35
- setting_name: nil,
30
+ runtime_setting: nil,
36
31
  yaml_key: nil,
37
- env_var_prefix: nil,
38
- env_var_upcase: true,
39
- setting_prefix: nil,
40
- setting_upcase: false
32
+ static: false
41
33
  )
42
- @name = frozen_string(name)
34
+ @name = name.to_s.freeze
43
35
  @type = type.to_sym
44
- @default = coerce_value(default).freeze
36
+ @description = description&.to_s&.freeze
37
+ @default = Coerce.coerce_value(default, @type).freeze
45
38
  @default_if = default_if
46
- @env_var = frozen_string(env_var)
47
- @setting_name = frozen_string(setting_name)
48
- @yaml_key = frozen_string(yaml_key)
49
- @env_var_prefix = frozen_string(env_var_prefix)
50
- @env_var_upcase = !!env_var_upcase
51
- @setting_prefix = frozen_string(setting_prefix)
52
- @setting_upcase = !!setting_upcase
39
+ @env_var = env_var&.to_s&.freeze
40
+ @runtime_setting = runtime_setting&.to_s&.freeze
41
+ @yaml_key = yaml_key&.to_s&.freeze
42
+ @static = !!static
53
43
  end
54
44
 
55
45
  # Get the value for the field from the passed in state.
@@ -58,85 +48,64 @@ module UltraSettings
58
48
  # @param settings [#[]] The runtime settings.
59
49
  # @param yaml_config [#[]] The YAML configuration.
60
50
  def value(env: nil, settings: nil, yaml_config: nil)
61
- val = fetch_value(env: env, settings: settings, yaml_config: yaml_config)
62
- val = coerce_value(val).freeze
63
- val = @default if use_default?(val)
64
- val
51
+ fetch_value_and_source(env: env, settings: settings, yaml_config: yaml_config).first
65
52
  end
66
53
 
67
- private
68
-
69
- def fetch_value(env:, settings:, yaml_config:)
70
- value = env_value(env) if env
71
- value = nil if value == ""
72
-
73
- if value.nil? && settings
74
- value = runtime_value(settings)
75
- value = nil if value == ""
76
- end
54
+ # Get the source for the field from the passed in state.
55
+ #
56
+ # @param env [Hash] The environment variables.
57
+ # @param settings [Hash] The runtime settings.
58
+ # @param yaml_config [Hash] The YAML configuration.
59
+ # @return [Symbol, nil] The source of the value (:env, :settings, or :yaml).
60
+ def source(env: nil, settings: nil, yaml_config: nil)
61
+ fetch_value_and_source(env: env, settings: settings, yaml_config: yaml_config).last
62
+ end
77
63
 
78
- if value.nil? && yaml_config
79
- value = yaml_value(yaml_config)
80
- value = nil if value == ""
81
- end
64
+ # Coerce the passed in value to the type of the field.
65
+ #
66
+ # @param value [Object] The value to coerce.
67
+ # @return [Object] The coerced value.
68
+ def coerce(value)
69
+ Coerce.coerce_value(value, @type)
70
+ end
82
71
 
83
- value
72
+ # Returns true if the field is static.
73
+ #
74
+ # @return [Boolean]
75
+ def static?
76
+ @static
84
77
  end
85
78
 
86
- def coerce_value(value)
87
- return nil if value.nil?
79
+ private
80
+
81
+ def fetch_value_and_source(env:, settings:, yaml_config:)
82
+ source = nil
88
83
 
89
- case type
90
- when :integer
91
- value.is_a?(Integer) ? value : value.to_s&.to_i
92
- when :float
93
- value.is_a?(Float) ? value : value.to_s&.to_f
94
- when :boolean
95
- SuperSettings::Coerce.boolean(value)
96
- when :datetime
97
- SuperSettings::Coerce.time(value)
98
- when :array
99
- Array(value).map(&:to_s)
100
- when :symbol
101
- value.to_s.to_sym
84
+ value = env[env_var] if env && env_var
85
+ value = nil if value == ""
86
+ if value.nil?
87
+ value = settings[runtime_setting] if settings && runtime_setting
88
+ value = nil if value == ""
89
+ if value.nil?
90
+ value = yaml_value(yaml_config)
91
+ value = nil if value == ""
92
+ source = :yaml unless value.nil?
93
+ else
94
+ source = :settings
95
+ end
102
96
  else
103
- value.to_s
97
+ source = :env
104
98
  end
105
- end
106
99
 
107
- def env_value(env)
108
- var_name = env_var
109
- if var_name.nil?
110
- var_name = "#{env_var_prefix}#{name}"
111
- var_name = var_name.upcase if env_var_upcase
112
- end
113
- env[var_name.to_s]
114
- end
100
+ value = coerce(value).freeze
115
101
 
116
- def runtime_value(settings)
117
- var_name = setting_name
118
- if var_name.nil?
119
- var_name = "#{setting_prefix}#{name}"
120
- var_name = var_name.upcase if setting_upcase
121
- end
122
- settings[var_name.to_s]
102
+ [value, source]
123
103
  end
124
104
 
125
105
  def yaml_value(yaml_config)
126
- key = (yaml_key || name)
127
- yaml_config[key.to_s]
128
- end
129
-
130
- def use_default?(value)
131
- if value && @default_if
132
- @default_if.call(value)
133
- else
134
- value.nil?
135
- end
136
- end
106
+ return nil unless yaml_config && yaml_key
137
107
 
138
- def frozen_string(value)
139
- value&.to_s&.dup&.freeze
108
+ yaml_config[yaml_key]
140
109
  end
141
110
  end
142
111
  end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UltraSettings
4
+ # Rack application for displaying the current settings in an HTML page.
5
+ # No setting values are displayed, but you should still add some
6
+ # sort of authentication if you want to use this in production.
7
+ class RackApp
8
+ def call(env)
9
+ [200, {"content-type" => "text/html; charset=utf8"}, [webview.render_settings]]
10
+ end
11
+
12
+ private
13
+
14
+ def webview
15
+ if ENV.fetch("RAILS_ENV", ENV.fetch("RACK_ENV", "development")) == "development"
16
+ @webview = nil
17
+ end
18
+ @webview ||= WebView.new
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UltraSettings
4
+ # Railtie to automatically configure settings for Rails applications.
5
+ # By default this will automatically load any configuration classes in the
6
+ # app/configurations directory. This can be customized by setting the
7
+ # `config.ultra_settings.auto_load_directories` option.
8
+ class Railtie < Rails::Railtie
9
+ config.ultra_settings = ActiveSupport::OrderedOptions.new
10
+ config.ultra_settings.auto_load_directories = [File.join("app", "configurations")]
11
+
12
+ config.before_configuration do
13
+ UltraSettings.yaml_config_env = Rails.env
14
+ UltraSettings.yaml_config_path = Rails.root.join("config")
15
+ end
16
+
17
+ # Automatically register any configuration classes in the app/configurations
18
+ # directory. The path to load can be customized by setting the
19
+ # `config.ultra_settings.auto_load_directory` option.
20
+ config.after_initialize do
21
+ Array(Rails.application.config.ultra_settings.auto_load_directories).each do |directory|
22
+ next unless directory
23
+
24
+ app_config_dir = Rails.root.join(directory)
25
+ app_config_dir.glob("**/*_configuration.rb").each do |file_path|
26
+ config_name = file_path.basename("_configuration.rb")
27
+ class_name = file_path.relative_path_from(app_config_dir).to_s.chomp(".rb").classify
28
+ UltraSettings.add(config_name, class_name)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UltraSettings
4
+ VERSION = File.read(File.join(__dir__, "..", "..", "VERSION")).strip
5
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UltraSettings
4
+ # Helper class for rendering the settings information in an HTML page.
5
+ class WebView
6
+ attr_reader :css
7
+
8
+ def initialize
9
+ @index_template = erb_template("index.html.erb")
10
+ @layout_template = erb_template("layout.html.erb")
11
+ @layout_css = read_app_file("layout.css")
12
+ @css = read_app_file("application.css")
13
+ @javascript = read_app_file("application.js")
14
+ end
15
+
16
+ def render_settings
17
+ @layout_template.result(binding)
18
+ end
19
+
20
+ def content
21
+ @index_template.result(binding)
22
+ end
23
+
24
+ private
25
+
26
+ def erb_template(path)
27
+ ERB.new(read_app_file(path))
28
+ end
29
+
30
+ def read_app_file(path)
31
+ File.read(File.join(app_dir, path))
32
+ end
33
+
34
+ def app_dir
35
+ File.expand_path(File.join("..", "..", "app"), __dir__)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UltraSettings
4
+ # Helper class to load YAML configuration files. Any ERB markup in the YAML
5
+ # file will be evaluated. The YAML file should be structured like this:
6
+ #
7
+ # ```yaml
8
+ # shared:
9
+ # foo: bar
10
+ # bar: baz
11
+ #
12
+ # development:
13
+ # bar: qux
14
+ # biz: buz
15
+ #
16
+ # test:
17
+ # bar: qix
18
+ # biz: biz
19
+ # ```
20
+ #
21
+ # The section with the key matching the environment name is merged into
22
+ # the shared section. In this example, the development environment would
23
+ # have the following configuration:
24
+ #
25
+ # ```ruby
26
+ # {
27
+ # "foo" => "bar",
28
+ # "bar" => "qux",
29
+ # "biz" => "buz"
30
+ # }
31
+ # ```
32
+ #
33
+ # In addition, the keys are flattened into a one level deep hash with dots
34
+ # separating the keys.
35
+ class YamlConfig
36
+ def initialize(path, environment)
37
+ yaml = load_yaml(path)
38
+ @config = environment_config(yaml, environment)
39
+ end
40
+
41
+ def to_h
42
+ @config
43
+ end
44
+
45
+ private
46
+
47
+ def load_yaml(path)
48
+ yaml = File.read(path)
49
+
50
+ if yaml.include?("<%")
51
+ yaml = ERB.new(yaml).result
52
+ end
53
+
54
+ hash = YAML.load(yaml) # rubocop:disable Security/YAMLLoad
55
+ hash = {} unless hash.is_a?(Hash)
56
+ hash
57
+ end
58
+
59
+ def environment_config(yaml, environment)
60
+ shared = flatten_hash(yaml.fetch("shared", {}))
61
+ env = flatten_hash(yaml.fetch(environment, {}))
62
+ shared.merge(env)
63
+ end
64
+
65
+ def flatten_hash(hash, prefix = nil)
66
+ hash.each_with_object({}) do |(key, value), result|
67
+ key = key.to_s
68
+ key = "#{prefix}.#{key}" if prefix
69
+
70
+ if value.is_a?(Hash)
71
+ result.merge!(flatten_hash(value, key))
72
+ else
73
+ result[key] = value
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -1,9 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "super_settings"
3
+ require "erb"
4
+ require "yaml"
5
+ require "time"
6
+ require "pathname"
7
+ require "singleton"
4
8
 
5
9
  require_relative "ultra_settings/configuration"
10
+ require_relative "ultra_settings/coerce"
6
11
  require_relative "ultra_settings/field"
12
+ require_relative "ultra_settings/rack_app"
13
+ require_relative "ultra_settings/web_view"
14
+ require_relative "ultra_settings/yaml_config"
15
+ require_relative "ultra_settings/version"
16
+
17
+ if defined?(Rails::Railtie)
18
+ require_relative "ultra_settings/railtie"
19
+ end
7
20
 
8
21
  # This is the root namespace for UltraSettings. You can add configurations to
9
22
  # this namespace using the add method.
@@ -12,12 +25,11 @@ require_relative "ultra_settings/field"
12
25
  # UltraSettings.add(:test)
13
26
  # UltraSettings.test # => TestConfiguration.instance
14
27
  module UltraSettings
28
+ VALID_NAME__PATTERN = /\A[a-z_][a-zA-Z0-9_]*\z/
29
+
15
30
  @configurations = {}
16
31
  @mutex = Mutex.new
17
32
 
18
- class NonStaticValueError < StandardError
19
- end
20
-
21
33
  class << self
22
34
  # Adds a configuration to the root namespace. The configuration will be
23
35
  # available as a method on the UltraSettings module with the provide name.
@@ -29,15 +41,15 @@ module UltraSettings
29
41
  # @return [void]
30
42
  def add(name, klass = nil)
31
43
  name = name.to_s
32
- unless name.match?(/\A[a-z_][a-zA-Z0-9_]*\z/)
44
+ unless name.match?(VALID_NAME__PATTERN)
33
45
  raise ArgementError.new("Invalid configuration name: #{name.inspect}")
34
46
  end
35
47
 
36
48
  class_name = klass&.to_s
37
- class_name ||= "#{name.classify}Configuration"
49
+ class_name ||= "#{classify(name)}Configuration"
38
50
 
39
51
  @mutex.synchronize do
40
- @configurations.delete(name)
52
+ @configurations[name] = class_name
41
53
 
42
54
  eval <<-RUBY, binding, __FILE__, __LINE__ + 1 # rubocop:disable Security/Eval
43
55
  def #{name}
@@ -45,93 +57,158 @@ module UltraSettings
45
57
  end
46
58
  RUBY
47
59
  end
60
+ end
48
61
 
49
- # Control if settings can be loaded from environment variables. By default
50
- # environment variables are enabled. This can also be disabled on
51
- # individual Configuration classes.
52
- #
53
- # @param value [Boolean] Whether or not to load settings from environment variables.
54
- # @return [void]
55
- def environment_variables_disabled=(value)
56
- Configuration.environment_variables_disabled = !!value
57
- end
62
+ # Returns true if the provided class has been added as a configuration.
63
+ #
64
+ # @param class_name [Class, String] The name of the configuration class.
65
+ # @return [Boolean]
66
+ def include?(class_name)
67
+ @configurations.values.collect(&:to_s).include?(class_name.to_s)
68
+ end
58
69
 
59
- # Control if settings can be loaded from runtime settings. By default
60
- # runtime settings are enabled. This can also be disabled on individual
61
- # Configuration classes.
62
- #
63
- # @param value [Boolean] Whether or not to load settings from runtime settings.
64
- # @return [void]
65
- def runtime_settings_disabled=(value)
66
- Configuration.runtime_settings_disabled = !!value
67
- end
70
+ # Control if settings can be loaded from environment variables. By default
71
+ # environment variables are enabled. This can also be disabled on
72
+ # individual Configuration classes.
73
+ #
74
+ # @param value [Boolean] Whether or not to load settings from environment variables.
75
+ # @return [void]
76
+ def environment_variables_disabled=(value)
77
+ Configuration.environment_variables_disabled = !!value
78
+ end
68
79
 
69
- # Control if settings can be loaded from YAML configuration files. By
70
- # default YAML configuration is enabled. This can also be disabled on
71
- # individual Configuration classes.
72
- #
73
- # @param value [Boolean] Whether or not to load settings from YAML configuration.
74
- # @return [void]
75
- def yaml_config_disabled=(value)
76
- Configuration.yaml_config_disabled = !!value
77
- end
80
+ # Control if settings can be loaded from runtime settings. By default
81
+ # runtime settings are enabled. This can also be disabled on individual
82
+ # Configuration classes.
83
+ #
84
+ # @param value [Boolean] Whether or not to load settings from runtime settings.
85
+ # @return [void]
86
+ def runtime_settings_disabled=(value)
87
+ Configuration.runtime_settings_disabled = !!value
88
+ end
78
89
 
79
- # Set the delimiter to use when determining environment variable names.
80
- # By default this is an underscore.
81
- #
82
- # @param value [String] The delimiter to use.
83
- # @return [void]
84
- def env_var_delimiter=(value)
85
- Configuration.env_var_delimiter = value.to_s
86
- end
90
+ # Control if settings can be loaded from YAML configuration files. By
91
+ # default YAML configuration is enabled. This can also be disabled on
92
+ # individual Configuration classes.
93
+ #
94
+ # @param value [Boolean] Whether or not to load settings from YAML configuration.
95
+ # @return [void]
96
+ def yaml_config_disabled=(value)
97
+ Configuration.yaml_config_disabled = !!value
98
+ end
87
99
 
88
- # Set the delimiter to use when determining setting names. By default
89
- # this is a period.
90
- #
91
- # @param value [String] The delimiter to use.
92
- def setting_delimiter=(value)
93
- Configuration.setting_delimiter = value.to_s
94
- end
100
+ # Set the environment to use when loading YAML configuration files.
101
+ # In a Rails application this will be the current Rails environment.
102
+ # Defaults to "development".
103
+ #
104
+ # @param value [String] The environment name to use.
105
+ def yaml_config_env=(value)
106
+ Configuration.yaml_config_env = value
107
+ end
95
108
 
96
- # Control if environment variable names should be upcased. By default
97
- # this is true.
98
- #
99
- # @param value [Boolean] Whether or not to upcase environment variable names.
100
- # @return [void]
101
- def env_var_upcase=(value)
102
- Configuration.env_var_upcase = !!value
103
- end
109
+ # Set the delimiter to use when determining environment variable names.
110
+ # By default this is an underscore.
111
+ #
112
+ # @param value [String] The delimiter to use.
113
+ # @return [void]
114
+ def env_var_delimiter=(value)
115
+ Configuration.env_var_delimiter = value.to_s
116
+ end
117
+
118
+ # Set the delimiter to use when determining setting names. By default
119
+ # this is a period.
120
+ #
121
+ # @param value [String] The delimiter to use.
122
+ def runtime_setting_delimiter=(value)
123
+ Configuration.runtime_setting_delimiter = value.to_s
124
+ end
125
+
126
+ # Control if environment variable names should be upcased. By default
127
+ # this is true.
128
+ #
129
+ # @param value [Boolean] Whether or not to upcase environment variable names.
130
+ # @return [void]
131
+ def env_var_upcase=(value)
132
+ Configuration.env_var_upcase = !!value
133
+ end
134
+
135
+ # Control if setting names should be upcased. By default this is false.
136
+ #
137
+ # @param value [Boolean] Whether or not to upcase setting names.
138
+ # @return [void]
139
+ def runtime_setting_upcase=(value)
140
+ Configuration.runtime_setting_upcase = !!value
141
+ end
142
+
143
+ # Set the directory to use when loading YAML configuration files.
144
+ # In a Rails application this will be the config directory.
145
+ # Otherwise it will be the current working directory.
146
+ #
147
+ # @param value [String, Pathname] The directory to use.
148
+ # @return [void]
149
+ def yaml_config_path=(value)
150
+ Configuration.yaml_config_path = value.to_s
151
+ end
152
+
153
+ # Set the object to use for runtime settings. This can be any object that
154
+ # responds to the [] method. If you are using the `super_settings` gem,
155
+ # you can set this to `SuperSettings`.
156
+ attr_writer :runtime_settings
104
157
 
105
- # Control if setting names should be upcased. By default this is false.
106
- #
107
- # @param value [Boolean] Whether or not to upcase setting names.
108
- # @return [void]
109
- def setting_upcase=(value)
110
- Configuration.setting_upcase = !!value
158
+ # Get the object to use for runtime settings.
159
+ #
160
+ # @return [Object, nil]
161
+ # @api private
162
+ def __runtime_settings__
163
+ @runtime_settings ||= nil
164
+ end
165
+
166
+ # Explicitly set setting values within a block. This is useful for testing
167
+ # or other situations where you want hard code a specific set of values.
168
+ #
169
+ # @param settings [Hash] The settings to set.
170
+ # @return [Object] The result of the block.
171
+ def override!(settings, &block)
172
+ settings = settings.to_a
173
+ config_name, values = settings.first
174
+ config_name = config_name.to_s
175
+ other_settings = settings[1..-1]
176
+
177
+ unless @configurations.include?(config_name)
178
+ raise ArgumentError.new("Unknown configuration: #{config_name.inspect}")
111
179
  end
112
180
 
113
- # Set the directory to use when loading YAML configuration files. By
114
- # default this is the config directory in the Rails root.
115
- #
116
- # @param value [String, Pathname] The directory to use.
117
- # @return [void]
118
- def yaml_config_directory=(value)
119
- Configuration.yaml_config_directory = value.to_s
181
+ config = send(config_name)
182
+ config.override!(values) do
183
+ if other_settings.empty?
184
+ yield
185
+ else
186
+ override!(other_settings, &block)
187
+ end
120
188
  end
121
189
  end
122
190
 
191
+ # Get the names of all of the configurations that have been added.
192
+ #
193
+ # @return [Array<String>] The names of the configurations.
194
+ # @api private
195
+ def __configuration_names__
196
+ @configurations.keys
197
+ end
198
+
123
199
  private
124
200
 
125
201
  # Load a configuration class.
126
202
  def __load_config__(name, class_name)
127
203
  klass = @configurations[name]
128
204
 
129
- if klass && !Rails.configuration.cache_classes
130
- klass = nil if klass != class_name.constantize
205
+ # Hook for Rails development mode to reload the configuration class.
206
+ if klass && defined?(Rails.configuration.cache_classes) && !Rails.configuration.cache_classes
207
+ klass = class_name if klass != constantize(class_name)
131
208
  end
132
209
 
133
- unless klass
134
- klass = class_name.constantize
210
+ if klass.is_a?(String)
211
+ klass = constantize(class_name)
135
212
  @mutex.synchronize do
136
213
  unless klass < Configuration
137
214
  raise TypeError.new("Configuration class #{class_name} does not inherit from UltraSettings::Configuration")
@@ -142,5 +219,21 @@ module UltraSettings
142
219
 
143
220
  klass.instance
144
221
  end
222
+
223
+ def classify(name)
224
+ # Use the Rails classify method if it is available since it will
225
+ # handle custom inflections.
226
+ if name.respond_to?(:classify)
227
+ name.classify
228
+ else
229
+ name.split("_").map(&:capitalize).join.gsub("/", "::")
230
+ end
231
+ end
232
+
233
+ def constantize(class_name)
234
+ class_name.split("::").reduce(Object) do |mod, name|
235
+ mod.const_get(name)
236
+ end
237
+ end
145
238
  end
146
239
  end