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?