Skip to content

Commit

Permalink
Add ability to define custom data types (#235)
Browse files Browse the repository at this point in the history
* move attributes converting into separate files

* remove redundant file import

* move saving inside field

* fix initialization

* fix styles

* add ability to serialize and deserialize fields separately

* add test for custom item
  • Loading branch information
MrKirat authored May 29, 2023
1 parent fd4d99c commit bbb7a67
Show file tree
Hide file tree
Showing 13 changed files with 242 additions and 93 deletions.
11 changes: 11 additions & 0 deletions lib/rails-settings-cached.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
# frozen_string_literal: true

require_relative "rails-settings/fields/base"
require_relative "rails-settings/fields/array"
require_relative "rails-settings/fields/big_decimal"
require_relative "rails-settings/fields/boolean"
require_relative "rails-settings/fields/float"
require_relative "rails-settings/fields/hash"
require_relative "rails-settings/fields/integer"
require_relative "rails-settings/fields/string"

require_relative "rails-settings/base"
require_relative "rails-settings/request_cache"
require_relative "rails-settings/middleware"
require_relative "rails-settings/railtie"
require_relative "rails-settings/version"

module RailsSettings
module Fields
end
end
104 changes: 14 additions & 90 deletions lib/rails-settings/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ def initialize(key)
end

class Base < ActiveRecord::Base
SEPARATOR_REGEXP = /[\n,;]+/
PROTECTED_KEYS = %w[var value]
self.table_name = table_name_prefix + "settings"

Expand Down Expand Up @@ -55,7 +54,7 @@ def scope(*args, &block)
end

def get_field(key)
@defined_fields.find { |field| field[:key] == key.to_s } || {}
@defined_fields.find { |field| field.key == key.to_s }.to_h || {}
end

def cache_prefix(&block)
Expand All @@ -69,15 +68,15 @@ def cache_key
end

def keys
@defined_fields.map { |field| field[:key] }
@defined_fields.map(&:key)
end

def editable_keys
@defined_fields.reject { |field| field[:readonly] }.map { |field| field[:key] }
@defined_fields.reject(&:readonly).map(&:key)
end

def readonly_keys
@defined_fields.select { |field| field[:readonly] }.map { |field| field[:key] }
@defined_fields.select(&:readonly).map(&:key)
end

attr_reader :defined_fields
Expand All @@ -89,56 +88,24 @@ def _define_field(key, default: nil, type: :string, readonly: false, separator:

raise ProtectedKeyError.new(key) if PROTECTED_KEYS.include?(key)

field = ::RailsSettings::Fields::Base.generate(
scope: @scope, key: key, default: default,
type: type, readonly: readonly, options: opts,
separator: separator, parent: self
)
@defined_fields ||= []
@defined_fields << {
scope: @scope,
key: key,
default: default,
type: type || :string,
readonly: readonly.nil? ? false : readonly,
options: opts
}
@defined_fields << field

if readonly
define_singleton_method(key) do
result = default.is_a?(Proc) ? default.call : default
send(:_convert_string_to_typeof_value, type, result, separator: separator)
end
define_singleton_method(key) { field.read }
else
define_singleton_method(key) do
val = send(:_value_of, key)
result = nil
if !val.nil?
result = val
else
result = default
result = default.call if default.is_a?(Proc)
end

result = send(:_convert_string_to_typeof_value, type, result, separator: separator)

result
end

define_singleton_method("#{key}=") do |value|
var_name = key

record = find_by(var: var_name) || new(var: var_name)
value = send(:_convert_string_to_typeof_value, type, value, separator: separator)

record.value = value
record.save!

value
end
define_singleton_method(key) { field.read }
define_singleton_method("#{key}=") { |value| field.save!(value: value) }

if validates
validates[:if] = proc { |item| item.var.to_s == key }
send(:validates, key, **validates)

define_method(:read_attribute_for_validation) do |_key|
self.value
end
define_method(:read_attribute_for_validation) { |_key| self.value }
end
end

Expand All @@ -156,49 +123,6 @@ def _define_field(key, default: nil, type: :string, readonly: false, separator:
end
end

def _convert_string_to_typeof_value(type, value, separator: nil)
return value unless [String, Integer, Float, BigDecimal].include?(value.class)

case type
when :boolean
["true", "1", 1, true].include?(value)
when :array
value.split(separator || SEPARATOR_REGEXP).reject { |str| str.empty? }.map(&:strip)
when :hash
value = begin
YAML.safe_load(value).to_h
rescue
{}
end
value.deep_stringify_keys!
ActiveSupport::HashWithIndifferentAccess.new(value)
when :integer
value.to_i
when :float
value.to_f
when :big_decimal
value.to_d
else
value
end
end

def _value_of(var_name)
unless _table_exists?
# Fallback to default value if table was not ready (before migrate)
puts "WARNING: table: \"#{table_name}\" does not exist or not database connection, `#{name}.#{var_name}` fallback to returns the default value."
return nil
end

_all_settings[var_name]
end

def _table_exists?
table_exists?
rescue
false
end

def rails_initialized?
Rails.application&.initialized?
end
Expand Down
15 changes: 15 additions & 0 deletions lib/rails-settings/fields/array.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module RailsSettings
module Fields
class Array < ::RailsSettings::Fields::Base
def deserialize(value)
return value unless value.is_a?(::String)

value.split(separator).reject(&:empty?).map(&:strip)
end

def serialize(value)
deserialize(value)
end
end
end
end
71 changes: 71 additions & 0 deletions lib/rails-settings/fields/base.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
module RailsSettings
module Fields
class Base < Struct.new(:scope, :key, :default, :parent, :readonly, :separator, :type, :options, keyword_init: true)
SEPARATOR_REGEXP = /[\n,;]+/

def initialize(scope:, key:, default:, parent:, readonly:, separator:, type:, options:)
super
self.readonly = !!readonly
self.type ||= :string
self.separator ||= SEPARATOR_REGEXP
end

def save!(value:)
serialized_value = serialize(value)
parent_record = parent.find_by(var: key) || parent.new(var: key)
parent_record.value = serialized_value
parent_record.save!
parent_record.value
end

def saved_value
return parent.send(:_all_settings)[key] if table_exists?

# Fallback to default value if table was not ready (before migrate)
puts "WARNING: table: \"#{parent.table_name}\" does not exist or not database connection, `#{parent.name}.#{key}` fallback to returns the default value."
nil
end

def default_value
default.is_a?(Proc) ? default.call : default
end

def read
return deserialize(default_value) if readonly || saved_value.nil?

deserialize(saved_value)
end

def deserialize(value)
raise NotImplementedError
end

def serialize(value)
raise NotImplementedError
end

def to_h
super.slice(:scope, :key, :default, :type, :readonly, :options)
end

def table_exists?
parent.table_exists?
rescue StandardError
false
end

class << self
def generate(**args)
fetch_field_class(args[:type]).new(**args)
end

private

def fetch_field_class(type)
field_class_name = type.to_s.split('_').map(&:capitalize).join('')
const_get("::RailsSettings::Fields::#{field_class_name}")
end
end
end
end
end
13 changes: 13 additions & 0 deletions lib/rails-settings/fields/big_decimal.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module RailsSettings
module Fields
class BigDecimal < ::RailsSettings::Fields::Base
def deserialize(value)
value.to_d
end

def serialize(value)
deserialize(value)
end
end
end
end
13 changes: 13 additions & 0 deletions lib/rails-settings/fields/boolean.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module RailsSettings
module Fields
class Boolean < ::RailsSettings::Fields::Base
def deserialize(value)
["true", "1", 1, true].include?(value)
end

def serialize(value)
deserialize(value)
end
end
end
end
13 changes: 13 additions & 0 deletions lib/rails-settings/fields/float.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module RailsSettings
module Fields
class Float < ::RailsSettings::Fields::Base
def deserialize(value)
value.to_f
end

def serialize(value)
deserialize(value)
end
end
end
end
21 changes: 21 additions & 0 deletions lib/rails-settings/fields/hash.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
module RailsSettings
module Fields
class Hash < ::RailsSettings::Fields::Base
def deserialize(value)
return value unless value.is_a?(::String)

load_value(value).deep_stringify_keys.with_indifferent_access
end

def serialize(value)
deserialize(value)
end

def load_value(value)
YAML.safe_load(value).to_h
rescue StandardError
{}
end
end
end
end
13 changes: 13 additions & 0 deletions lib/rails-settings/fields/integer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module RailsSettings
module Fields
class Integer < ::RailsSettings::Fields::Base
def deserialize(value)
value.to_i
end

def serialize(value)
deserialize(value)
end
end
end
end
13 changes: 13 additions & 0 deletions lib/rails-settings/fields/string.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module RailsSettings
module Fields
class String < ::RailsSettings::Fields::Base
def deserialize(value)
value
end

def serialize(value)
deserialize(value)
end
end
end
end
Loading

0 comments on commit bbb7a67

Please sign in to comment.