lib/super_settings/setting.rb in super_settings-0.0.0.rc1 vs lib/super_settings/setting.rb in super_settings-0.0.1.rc1
- old
+ new
@@ -3,11 +3,11 @@
module SuperSettings
# This is the model for interacting with settings. This class provides methods for finding, validating, and
# updating settings.
#
# This class does not deal with actually persisting settings to and fetching them from a data store.
- # You need to specify the storage engine you want to use with the `storage` class method. This gem
+ # You need to specify the storage engine you want to use with the +storage+ class method. This gem
# ships with storage engines for ActiveRecord, Redis, and HTTP (microservice). See the SuperSettings::Storage
# class for more details.
class Setting
LAST_UPDATED_CACHE_KEY = "SuperSettings.last_updated_at"
@@ -15,13 +15,12 @@
INTEGER = "integer"
FLOAT = "float"
BOOLEAN = "boolean"
DATETIME = "datetime"
ARRAY = "array"
- SECRET = "secret"
- VALUE_TYPES = [STRING, INTEGER, FLOAT, BOOLEAN, DATETIME, ARRAY, SECRET].freeze
+ VALUE_TYPES = [STRING, INTEGER, FLOAT, BOOLEAN, DATETIME, ARRAY].freeze
ARRAY_DELIMITER = /[\n\r]+/.freeze
# Exception raised if you try to save with invalid data.
class InvalidRecordError < StandardError
@@ -34,12 +33,12 @@
# and is cleared after the record is saved.
attr_accessor :changed_by
class << self
# Set a cache to use for caching values. This feature is optional. The cache must respond
- # to `delete(key)` and `fetch(key, &block)`. If you are running in a Rails environment,
- # you can use `Rails.cache` or any ActiveSupport::Cache::Store object.
+ # to +delete(key)+ and +fetch(key, &block)+. If you are running in a Rails environment,
+ # you can use +Rails.cache+ or any ActiveSupport::Cache::Store object.
attr_accessor :cache
# Set the storage class to use for persisting data.
attr_writer :storage
@@ -54,10 +53,11 @@
raise ArgumentError.new("No storage class defined for #{name}")
end
end
# Create a new setting with the specified attributes.
+ #
# @param attributes [Hash] hash of attribute names and values
# @return [Setting]
def create!(attributes)
setting = new(attributes)
storage.with_connection do
@@ -66,79 +66,82 @@
setting
end
# Get all the settings. This will even return settings that have been marked as deleted.
# If you just want current settings, then call #active instead.
+ #
# @return [Array<Setting>]
def all
storage.with_connection do
storage.all.collect { |record| new(record) }
end
end
# Get all the current settings.
+ #
# @return [Array<Setting>]
def active
storage.with_connection do
storage.active.collect { |record| new(record) }
end
end
# Get all settings that have been updated since the specified time stamp.
+ #
# @param time [Time]
# @return [Array<Setting>]
def updated_since(time)
storage.with_connection do
storage.updated_since(time).collect { |record| new(record) }
end
end
# Get a setting by its unique key.
+ #
# @return Setting
def find_by_key(key)
record = storage.with_connection { storage.find_by_key(key) }
if record
new(record)
end
end
# Return the maximum updated at value from all the rows. This is used in the caching
# scheme to determine if data needs to be reloaded from the database.
+ #
# @return [Time]
def last_updated_at
fetch_from_cache(LAST_UPDATED_CACHE_KEY) do
storage.with_connection { storage.last_updated_at }
end
end
# Bulk update settings in a single database transaction. No changes will be saved
# if there are any invalid records.
#
- # Example:
+ # @example
#
- # ```
- # SuperSettings.bulk_update([
- # {
- # key: "setting-key",
- # value: "foobar",
- # value_type: "string",
- # description: "A sample setting"
- # },
- # {
- # key: "setting-to-delete",
- # deleted: true
- # }
- # ])
- # ```
+ # SuperSettings.bulk_update([
+ # {
+ # key: "setting-key",
+ # value: "foobar",
+ # value_type: "string",
+ # description: "A sample setting"
+ # },
+ # {
+ # key: "setting-to-delete",
+ # deleted: true
+ # }
+ # ])
#
# @param params [Array] Array of hashes with setting attributes. Each hash must include
# a "key" element to identify the setting. To update a key, it must also include at least
# one of "value", "value_type", or "description". If one of these attributes is present in
# the hash, it will be updated. If a setting with the given key does not exist, it will be created.
# A setting may also be deleted by providing the attribute "deleted: true".
# @return [Array] Boolean indicating if update succeeded, Array of settings affected by the update;
- # if the settings were not updated, the `errors` on the settings that failed validation will be filled.
+ # if the settings were not updated, the +errors+ on the settings that failed validation will be filled.
def bulk_update(params, changed_by = nil)
all_valid, settings = update_settings(params, changed_by)
if all_valid
storage.with_connection do
storage.transaction do
@@ -151,10 +154,12 @@
end
[all_valid, settings]
end
# Determine the value type from a value.
+ #
+ # @return [String]
def value_type(value)
case value
when Integer
INTEGER
when Numeric
@@ -169,20 +174,22 @@
STRING
end
end
# Clear the last updated timestamp from the cache.
+ #
# @api private
def clear_last_updated_cache
cache&.delete(Setting::LAST_UPDATED_CACHE_KEY)
end
private
# Updates settings in memory from an array of parameters.
- # @param params [Array<Hash>] Each hash must contain a `key` element and may contain elements
- # for `value`, `value_type`, `description`, and `deleted`.
+ #
+ # @param params [Array<Hash>] Each hash must contain a "key" element and may contain elements
+ # for "value", "value_type", "description", and "deleted".
# @param changed_by [String] Value to be stored in the history for each setting
# @return [Array] The first value is a boolean indicating if all the settings are valid,
# the second is an array of settings with their attributes updated in memory and ready to be saved.
def update_settings(params, changed_by)
changed = {}
@@ -245,145 +252,173 @@
self.attributes = attributes
self.value_type ||= STRING
end
end
- # @return [String] the unique key for the setting.
+ # Get the unique key for the setting.
+ #
+ # @return [String]
def key
@record.key
end
- # Set the value of the setting.
- # @param val [String]
+ # Set the value of the setting. The value will be coerced to a string for storage.
+ #
+ # @param val [Object]
def key=(val)
val = val&.to_s
will_change!(:key, val) unless key == val
@record.key = val
end
- # @return [Object] the value of a setting coerced to the appropriate class depending on its value type.
+ # The value of a setting coerced to the appropriate class depending on its value type.
+ #
+ # @return [Object]
def value
if deleted?
nil
else
coerce(raw_value)
end
end
# Set the value of the setting.
+ #
# @param val [Object]
def value=(val)
val = serialize(val) unless val.is_a?(Array)
val = val.join("\n") if val.is_a?(Array)
self.raw_value = val
end
+ # Get the type of value being stored in the setting.
+ #
+ # @return [String] one of string, integer, float, boolean, datetime, or array.
def value_type
@record.value_type
end
# Set the value type of the setting.
- # @param val [String] one of string, integer, float, boolean, datetime, array, or secret.
+ #
+ # @param val [String] one of string, integer, float, boolean, datetime, or array.
def value_type=(val)
val = val&.to_s
will_change!(:value_type, val) unless value_type == val
@record.value_type = val
end
+ # Get the description for the setting.
+ #
+ # @return [String]
def description
@record.description
end
+ # Set the description of the setting.
+ #
# @param val [String]
def description=(val)
val = val&.to_s
val = nil if val&.empty?
will_change!(:description, val) unless description == val
@record.description = val
end
+ # Return true if the setting has been marked as deleted.
+ #
+ # @return [Boolean]
def deleted?
@record.deleted?
end
alias_method :deleted, :deleted?
# Set the deleted flag on the setting. Deleted settings are not visible but are not actually
# removed from the data store.
+ #
# @param val [Boolean]
def deleted=(val)
val = Coerce.boolean(val)
will_change!(:deleted, val) unless deleted? == val
@record.deleted = val
end
+ # Get the time the setting was first created.
+ #
+ # @return [Time]
def created_at
@record.created_at
end
+ # Set the time when the setting was created.
+ #
# @param val [Time, DateTime]
def created_at=(val)
val = Coerce.time(val)
will_change!(:created_at, val) unless created_at == val
@record.created_at = val
end
+ # Get the time the setting was last updated.
+ #
+ # @return [Time]
def updated_at
@record.updated_at
end
+ # Set the time when the setting was last updated.
+ #
# @param val [Time, DateTime]
def updated_at=(val)
val = Coerce.time(val)
will_change!(:updated_at, val) unless updated_at == val
@record.updated_at = val
end
- # @return [true] if the setting has a string value type.
+ # Return true if the setting has a string value type.
+ #
+ # @return [Boolean]
def string?
value_type == STRING
end
- # @return [true] if the setting has an integer value type.
+ # Return true if the setting has an integer value type.
+ #
+ # @return [Boolean]
def integer?
value_type == INTEGER
end
- # @return [true] if the setting has a float value type.
+ # Return true if the setting has a float value type.
+ # @return [Boolean]
def float?
value_type == FLOAT
end
- # @return [true] if the setting has a boolean value type.
+ # Return true if the setting has a boolean value type.
+ # @return [Boolean]
def boolean?
value_type == BOOLEAN
end
- # @return [true] if the setting has a datetime value type.
+ # Return true if the setting has a datetime value type.
+ # @return [Boolean]
def datetime?
value_type == DATETIME
end
- # @return [true] if the setting has an array value type.
+ # Return true if the setting has an array value type.
+ # @return [Boolean]
def array?
value_type == ARRAY
end
- # @return [true] if the setting has a secret value type.
- def secret?
- value_type == SECRET
- end
-
- # @return [true] if the setting is a secret setting and the value is encrypted in the database.
- def encrypted?
- secret? && Encryption.encrypted?(raw_value)
- end
-
# Save the setting to the data storage engine.
+ #
# @return [void]
def save!
- set_raw_value
+ record_value_change
unless valid?
raise InvalidRecordError.new(errors.values.join("; "))
end
@@ -396,70 +431,80 @@
@record.save!
end
begin
self.class.clear_last_updated_cache
- redact_history! if history_needs_redacting?
ensure
clear_changes
end
end
nil
end
- # @return [Boolean] true if the record has been stored in the data storage engine.
+ # Return true if the record has been stored in the data storage engine.
+ #
+ # @return [Boolean]
def persisted?
@record.persisted?
end
- # @return [Boolean] true if the record has valid data.
+ # Return true if the record has valid data.
+ #
+ # @return [Boolean]
def valid?
validate!
@errors.empty?
end
- # @return [Hash<String, Array<String>>] hash of errors generated from the last call to `valid?`
+ # Return hash of errors generated from the last call to +valid?+
+ #
+ # @return [Hash<String, Array<String>>]
attr_reader :errors
# Mark the record as deleted. The record will not actually be deleted since it's still needed
# for caching purposes, but it will no longer be returned by queries.
+ #
+ # @return [void]
def delete!
update!(deleted: true)
end
# Update the setting attributes and save it.
+ #
# @param attributes [Hash]
# @return [void]
def update!(attributes)
self.attributes = attributes
save!
end
# Return array of history items reflecting changes made to the setting over time. Items
# should be returned in reverse chronological order so that the most recent changes are first.
+ #
# @return [Array<SuperSettings::History>]
def history(limit: nil, offset: 0)
@record.history(limit: limit, offset: offset)
end
# Serialize to a hash that is used for rendering JSON responses.
+ #
# @return [Hash]
def as_json(options = nil)
attributes = {
key: key,
value: value,
value_type: value_type,
description: description,
created_at: created_at,
updated_at: updated_at
}
- attributes[:encrypted] = encrypted? if secret?
attributes[:deleted] = true if deleted?
attributes
end
# Serialize to a JSON string.
+ #
# @return [String]
def to_json(options = nil)
as_json.to_json(options)
end
@@ -484,16 +529,10 @@
if value.is_a?(String)
value.split(Setting::ARRAY_DELIMITER).map(&:freeze).freeze
else
Array(value).reject { |v| v.respond_to?(:empty?) ? v.empty? : v.to_s.empty? }.collect { |v| v.to_s.freeze }.freeze
end
- when Setting::SECRET
- begin
- Encryption.decrypt(value).freeze
- rescue Encryption::InvalidSecretError
- nil
- end
else
value.freeze
end
rescue ArgumentError
nil
@@ -508,22 +547,14 @@
else
coerce(value.to_s)
end
end
- # Set the raw string value that will be persisted to the data store.
- def set_raw_value
- if value_type == Setting::SECRET && !raw_value.to_s.empty? && (changed?(:raw_value) || !Encryption.encrypted?(raw_value))
- self.raw_value = Encryption.encrypt(raw_value)
- end
- record_value_change
- end
-
# Update the histories association whenever the value or key is changed.
def record_value_change
return unless changed?(:raw_value) || changed?(:deleted) || changed?(:key)
- recorded_value = (deleted? || value_type == Setting::SECRET ? nil : raw_value)
+ recorded_value = (deleted? ? nil : raw_value)
@record.create_history(value: recorded_value, deleted: deleted?, changed_by: changed_by, created_at: Time.now)
end
def clear_changes
@changes = {}
@@ -540,17 +571,9 @@
change[1] = value # rubocop:disable Lint/UselessSetterCall
end
def changed?(attribute)
@changes.include?(attribute.to_s)
- end
-
- def history_needs_redacting?
- value_type == Setting::SECRET && changed?(:value_type)
- end
-
- def redact_history!
- @record.send(:redact_history!)
end
def raw_value=(val)
val = val&.to_s
val = nil if val&.empty?