README.md in super_settings-0.0.1.rc3 vs README.md in super_settings-1.0.0

- old
+ new

@@ -17,10 +17,14 @@ SuperSettings provides a simple interface for accessing settings backed by a thread-safe caching mechanism, which provides in-memory performance while significantly limiting any load on the database. You can tune how frequently the cache is refreshed and each refresh call is tuned to be highly efficient. There is also an out of the box Web UI and REST API for managing settings. You can specify data types for your settings (string, integer, float, boolean, datetime, or array) to ensure that values will be valid. You can also supply documentation for each setting so that it's clear what each one does and how it is used. +Changes to settings are stored whenever a setting is changed to give you an audit trail if you need it for compliance reasons. In addition, you can provide your own callbacks to execute whenever a setting is changed. + +There is a companion gem [ultra_settings](https://github.com/bdurand/ultra_settings) that can be used to integrate SuperSettings into a combined configuration system alongside YAML files and environment variables. + ## Usage - [Getting Value](#getting-values) - [Hashes](#hashes) - [Defaults](#defaults) @@ -49,70 +53,11 @@ SuperSettings.datetime("key") # -> returns a `Time` object SuperSettings.array("key") # -> returns an array of strings ``` -#### Hashes -There is also a method to get multiple settings at once structured as a hash. -```ruby -SuperSettings.structured("parent") # -> returns an hash -``` - -The key provided to the `SuperSettings.structured` method indicates the key prefix and constructs the hash from settings that have keys beginning with that prefix. Keys are also broken down by a delimiter so you can create nested hashes. The delimiter defaults to `"."`, but you can specify a different one with the `delimiter` keyword argument. - -You can also set a maximum depth to the returned hash with the `max_depth` keyword argument. - -So, if you have the following settings: - -``` -vendors.company_1.path = "/co1" -vendors.company_1.timeout = 5 -vendors.company_2.path = "/co2" -page_size = 20 -``` - -You would get these results: - -```ruby -SuperSettings.structured("vendors") -# { -# "company_1" => { -# "path" => "/co1", -# "timeout" => 5 -# }, -# "company_2" => { -# "path" => "/co2" -# } -# } - -SuperSettings.structured("vendors.company_1") -# {"path" => "/co1", "timeout" => 5} - -SuperSettings.structured("vendors.company_2") -# {"path" => "/co2"} - -# Get all the settings by omitting the key -SuperSettings.structured -# { -# "vendors" => { -# "company_1" => {"path" => "/co1", "timeout" => 5}, -# "company_2" => {"path" => "/co2"} -# }, -# "page_size" => 20 -# } - -# Limit the nesting depth of the returned hash to one level -SuperSettings.structured(max_depth: 1) -# { -# "vendors.company_1.path => "/co1", -# "vendors.company_1.timeout" => 5, -# "vendors.company_2.path" => "/co2", -# "page_size" => 20 -# } -``` - #### Defaults When you request a setting, you can also specify a default value to use if the setting does not have a value. ```ruby @@ -130,27 +75,62 @@ # BAD: this will create an entry in the cache for every id SuperSettings.enabled?("enabled_users.#{id}") # GOOD: use an array if there are a limited number of values SuperSettings.array("enabled_users", []).include?(id) - -# GOOD: use a hash if you need to scale to any number of values -SuperSettings.structured("enabled_users", {})["id"] ``` The cache will scale without issue to handle hundreds of settings. However, you should avoid creating thousands of settings. Because all settings are read into memory, having too many settings records can lead to performance or memory issues. +#### Request Context + +You can ensure that settings won't change in a block of code by surrounding it with a `SuperSettings.context` block. Inside a `context` block, a setting will always return the same value. This can prevent race conditions where you code may branch based on a setting value. + +```ruby +# This code could be unsafe since the value of the "threshold" setting could +# change after the if statement is checked. +if SuperSettings.integer("threshold") > 0 + do_something(SuperSettings.integer("threshold")) +end + +# With a context block, the value for the "threshold setting will always +# return the same value +SuperSettings.context do + if SuperSettings.integer("threshold") > 0 + do_something(SuperSettings.integer("threshold")) + end +end +``` + +It's a good idea to add a `context` block around your main unit of work: + +- Rack application: add `SuperSettings::Context::RackMiddleware` to your middleware stack +- Sidekiq: add `SuperSettings::Context::SidekiqMiddleware` to your server middleware +- ActiveJob: add an `around_perform` callback that calls `SuperSettings.context` + +In a Rails application all of these will be done automatically. + ### Data Model Each setting has a key, value, value type, and optional description. The key must be unique. The value type can be one of "string", "integer", "float", "boolean", "datetime", or "array". The array value type will always return an array of strings. You can request a setting using one of the accessor methods on `SuperSettings` regardless of its defined value type. For instance, you can call `SuperSettings.get("integer_key")` on an integer setting and it will return the value as a string. The value type of a setting is only used for validating input values and does not limit how you can request the value at runtime. It is not possible to store an empty string in a setting; empty strings will be always be returned as `nil`. A history of all settings changes is updated every time the value is changed in the `histories` association. You can also record who made the changes. +#### Callbacks + +You can define custom callbacks on the `SuperSettings::Setting` model that will be called whenever a setting is changed. For example, if you needed to log all changes to you settings in your application logs, you could do something like this: + +```ruby +SuperSettings::Setting.after_save do |setting| + Application.logger.info("Setting #{setting.key} changed: #{setting.changes.inspect}) +end +``` + #### Storage Engines This gem abstracts out the storage engine and can support multiple storage mechanisms. It has built in support for ActiveRecord, Redis, and HTTP storage. * `SuperSettings::Storage::ActiveRecordStorage` - Stores the settings in a relational database using ActiveRecord. This is the default storage engine for Rails applications. @@ -218,11 +198,11 @@ The gem ships with a Rails engine that provides easy integration with a Rails application. The default storage engine for a Rails application will be the ActiveRecord storage. You need to install the database migrations first with: ```bash -rails app:super_settings:install:migrations +rails super_settings:install:migrations ``` You also need to mount the engine routes in your application's `config/routes.rb` file. The routes can be mounted under any prefix you'd like. ```ruby @@ -268,23 +248,33 @@ redirect_to access_denied_url, status: 403 end end end - # Define a method that returns the value that will be stored in the settings history in + # Define a block that returns the value that will be stored in the settings history in # the `changed_by` column. config.controller.define_changed_by do - current_user.name + current_user.id end + # Define a block that determines how to display the `changed_by`` value in the setting history. + config.model.define_changed_by_display do |changed_by_id| + User.find_by(id: changed_by_id)&.name + end + # You can define the storage engine for the model. This can be either done either with a Class # object or with a symbol matching the underscored class name of a storage class defined under # the SuperSettings::Storage namespace. # config.model.storage = :active_record # You can also specify a cache implementation to use to cache the last updated timestamp # for model changes. By default this will use `Rails.cache`. # config.model.cache = Rails.cache + + # You can define after_save callbacks for the model. + # config.model.after_save do |setting| + # Rail.logger.info("Setting #{setting.key} changed to #{setting.value.inspect}") + # end end ``` ## Installation