ultra_settings 0.0.1.rc1 → 1.0.0

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.
@@ -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