Class: Toys::Settings
- Inherits:
-
Object
- Object
- Toys::Settings
- Defined in:
- lib/toys/settings.rb
Overview
A settings class defines the structure of application settings, i.e. the various fields that can be set, and their types. You can define a settings structure by subclassing this base class, and using the provided methods.
Attributes
To define an attribute, use the Settings.settings_attr declaration.
Example:
class ServiceSettings < Toys::Settings
settings_attr :endpoint, default: "api.example.com"
end
my_settings = ServiceSettings.new
my_settings.endpoint_set? # => false
my_settings.endpoint # => "api.example.com"
my_settings.endpoint = "rest.example.com"
my_settings.endpoint_set? # => true
my_settings.endpoint # => "rest.example.com"
my_settings.endpoint_unset!
my_settings.endpoint_set? # => false
my_settings.endpoint # => "api.example.com"
An attribute has a name, a default value, and a type specification. The name is used to define methods for getting and setting the attribute. The default is returned if no value is set. (See the section below on parents and defaults for more information.) The type specification governs what values are allowed. (See the section below on type specifications.)
Attribute names must start with an ascii letter, and may contain only ascii
letters, digits, and underscores. Unlike method names, they may not include
non-ascii unicode characters, nor may they end with !
or ?
.
Additionally, the name method_missing
is not allowed because of its
special behavior in Ruby.
Each attribute defines four methods: a getter, a setter, an unsetter, and a
set detector. In the above example, the attribute named :endpoint
creates
the following four methods:
-
endpoint
- retrieves the attribute value, or a default if not set. -
endpoint=(value)
- sets a new attribute value. -
endpoint_unset!
- unsets the attribute, reverting to a default. -
endpoint_set?
- returns a boolean, whether the attribute is set.
Groups
A group is a settings field that itself is a Settings object. You can use it to group settings fields in a hierarchy.
Example:
class ServiceSettings < Toys::Settings
settings_attr :endpoint, default: "api.example.com"
settings_group :service_flags do
settings_attr :verbose, default: false
settings_attr :use_proxy, default: false
end
end
my_settings = ServiceSettings.new
my_settings.service_flags.verbose # => false
my_settings.service_flags.verbose = true
my_settings.service_flags.verbose # => true
my_settings.endpoint # => "api.example.com"
You can define a group inline, as in the example above, or create an explicit settings class and use it for the group. For example:
class Flags < Toys::Settings
settings_attr :verbose, default: false
settings_attr :use_proxy, default: false
end
class ServiceSettings < Toys::Settings
settings_attr :endpoint, default: "api.example.com"
settings_group :service_flags, Flags
end
my_settings = ServiceSettings.new
my_settings.service_flags.verbose = true
If the module enclosing a subclass of Settings
is itself a subclass of
Settings
, then the class is automatically added to its enclosing class as
a group. For example:
class ServiceSettings < Toys::Settings
settings_attr :endpoint, default: "api.example.com"
# Automatically adds this as the group service_flags.
# The name is inferred (snake_cased) from the class name.
class ServiceFlags < Toys::Settings
settings_attr :verbose, default: false
settings_attr :use_proxy, default: false
end
end
my_settings = ServiceSettings.new
my_settings.service_flags.verbose = true
Type specifications
A type specification is a restriction on the types of values allowed for a
settings field. Every attribute has a type specification. You can set it
explicitly by providing a :type
argument or a block. If a type
specification is not provided explicitly, it is inferred from the default
value of the attribute.
Type specifications can be any of the following:
A Module, restricting values to those that include the module.
For example, a type specification of
Enumerable
would accept[123]
but not123
.A Class, restricting values to that class or any subclass.
For example, a type specification of
Time
would acceptTime.now
but notDateTime.now
.Note that some classes will convert (i.e. parse) strings. For example, a type specification of
Integer
will accept the string"-123"
and convert it to the value
-123`. Classes that support parsing include:-
Date
-
DateTime
-
Float
-
Integer
-
Regexp
-
Symbol
-
Time
-
A Regexp, restricting values to strings matching the regexp.
For example, a type specification of
/^\w+$/
would match"abc"
but not"abc!"
.A Range, restricting values to objects that fall in the range and are of the same class (or a subclass) as the endpoints. String values are accepted if they can be converted to the endpoint class as specified by a class type specification.
For example, a type specification of
(1..5)
would match5
but not6
. It would also match"5"
because the String can be parsed into an Integer in the range.A specific value, any Symbol, String, Numeric, or the values
nil
,true
, orfalse
, restricting the value to only that given value.For example, a type specification of
:foo
would match:foo
but not:bar
.(It might not seem terribly useful to have an attribute that can take only one value, but this type is generally used as part of a union type, described below, to implement an enumeration.)
An Array representing a union type, each of whose elements is one of the above types. Values are accepted if they match any of the elements.
For example, a type specification of
[:a, :b :c]
would match:a
but not"a"
. Similarly, a type specification of[String, Integer, nil]
would match"hello"
,123
, ornil
, but not123.4
.A Proc that takes the proposed value and returns either the value if it is legal, the converted value if it can be converted to a legal value, or the constant ILLEGAL_VALUE if it cannot be converted to a legal value. You may also pass a block to
settings_attr
to set a Proc type specification.A Type that checks and converts values.
If you do not explicitly provide a type specification, one is inferred from the attribute's default value. The rules are:
If the default value is
true
orfalse
, then the type specification inferred is[true, false]
.If the default value is
nil
or not provided, then the type specification allows any object (i.e. is equivalent toObject
).Otherwise, the type specification allows any value of the same class as the default value. For example, if the default value is
""
, the effective type specification isString
.
Examples:
class ServiceSettings < Toys::Settings
# Allows only strings because the default is a string.
settings_attr :endpoint, default: "example.com"
end
class ServiceSettings < Toys::Settings
# Allows strings or nil.
settings_attr :endpoint, default: "example.com", type: [String, nil]
end
class ServiceSettings < Toys::Settings
# Raises ArgumentError because the default is nil, which does not
# match the type specification. (You should either allow nil
# explicitly with `type: [String, nil]` or set the default to a
# suitable string such as the empty string "".)
settings_attr :endpoint, type: String
end
Settings parents
A settings object can have a "parent" which provides the values if they are not set in the settings object. This lets you organize settings as "defaults" and "overrides". A parent settings object provides the defaults, and a child can selectively override certain values.
To set the parent for a settings object, pass it as the argument to the Settings constructor. When a field in a settings object is queried, it looks up the value as follows:
- If a field value is explicitly set in the settings object, that value is returned.
- If the field is not set in the settings object, but the settings object has a parent, the parent is queried. If that parent also does not have a value for the field, it may query its parent in turn, and so forth.
- If we encounter a root settings with no parent, and still no value is set for the field, the default is returned.
Example:
class MySettings < Toys::Settings
settings_attr :str, default: "default"
end
root_settings = MySettings.new
child_settings = MySettings.new(root_settings)
child_settings.str # => "default"
root_settings.str = "value_from_root"
child_settings.str # => "value_from_root"
child_settings.str = "value_from_child"
child_settings.str # => "value_from_child"
child_settings.str_unset!
child_settings.str # => "value_from_root"
root_settings.str_unset!
child_settings.str # => "default"
Parents are honored through groups as well. For example:
class MySettings < Toys::Settings
settings_group :flags do
settings_attr :verbose, default: false
settings_attr :force, default: false
end
end
root_settings = MySettings.new
child_settings = MySettings.new(root_settings)
child_settings.flags.verbose # => false
root_settings.flags.verbose = true
child_settings.flags.verbose # => true
Usually, a settings and its parent (and its parent, and so forth) should have the same class. This guarantees that they define the same fields with the same type specifications. However, this is not required. If a parent does not define a particular field, it is treated as if that field is unset, and lookup proceeds to its parent. To illustrate:
class Settings1 < Toys::Settings
settings_attr :str, default: "default"
end
class Settings2 < Toys::Settings
end
root_settings = Settings1.new
child_settings = Settings2.new(root_settings) # does not have str
grandchild_settings = Settings1.new(child_settings)
grandchild_settings.str # => "default"
root_settings.str = "value_from_root"
grandchild_settings.str # => "value_from_root"
Type specifications are enforced when falling back to parent values. If a parent provides a value that is not allowed, it is treated as if the field is unset, and lookup proceeds to its parent.
class Settings1 < Toys::Settings
settings_attr :str, default: "default" # type spec is String
end
class Settings2 < Toys::Settings
settings_attr :str, default: 0 # type spec is Integer
end
root_settings = Settings1.new
child_settings = Settings2.new(root_settings)
grandchild_settings = Settings1.new(child_settings)
grandchild_settings.str # => "default"
child_settings.str = 123 # does not match grandchild's type
root_settings.str = "value_from_root"
grandchild_settings.str # => "value_from_root"
Direct Known Subclasses
Defined Under Namespace
Classes: FieldError, Type
Constant Summary collapse
- ILLEGAL_VALUE =
A special value indicating a type check failure.
::Object.new.freeze
- DEFAULT_TYPE =
A special type specification indicating infer from the default value.
::Object.new.freeze
Class Method Summary collapse
-
.settings_attr(name, default: nil, type: DEFAULT_TYPE, &block) ⇒ Object
Add an attribute field.
-
.settings_group(name, klass = nil, &block) ⇒ Object
Add a group field.
Instance Method Summary collapse
-
#initialize(parent: nil) ⇒ Settings
constructor
Create a settings instance.
-
#load_data!(data, raise_on_failure: false) ⇒ Array<FieldError>
Load the given hash of data into this settings object.
-
#load_json!(str, raise_on_failure: false, **json_opts) ⇒ Array<FieldError>
Parse the given JSON string and load the data into this settings object.
-
#load_json_file!(filename, raise_on_failure: false, **json_opts) ⇒ Array<FieldError>
Parse the given JSON file and load the data into this settings object.
-
#load_yaml!(str, raise_on_failure: false) ⇒ Array<FieldError>
Parse the given YAML string and load the data into this settings object.
-
#load_yaml_file!(filename, raise_on_failure: false) ⇒ Array<FieldError>
Parse the given YAML file and load the data into this settings object.
Constructor Details
#initialize(parent: nil) ⇒ Settings
Create a settings instance.
582 583 584 585 586 587 588 589 590 |
# File 'lib/toys/settings.rb', line 582 def initialize(parent: nil) unless parent.nil? || parent.is_a?(Settings) raise ::ArgumentError, "parent must be a Settings object, if given" end @parent = parent @fields = self.class.fields @mutex = ::Mutex.new @values = {} end |
Class Method Details
.settings_attr(name, default: nil, type: DEFAULT_TYPE, &block) ⇒ Object
Add an attribute field.
798 799 800 801 802 803 804 805 806 807 |
# File 'lib/toys/settings.rb', line 798 def settings_attr(name, default: nil, type: DEFAULT_TYPE, &block) name = interpret_name(name) type = block if type == DEFAULT_TYPE && block @fields[name] = field = Field.new(self, name, type, default) create_getter(field) create_setter(field) create_set_detect(field) create_unsetter(field) self end |
.settings_group(name, klass = nil, &block) ⇒ Object
Add a group field.
Specify the group's structure by passing either a class (which must subclass Settings) or a block (which will be called on the group's class.)
821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 |
# File 'lib/toys/settings.rb', line 821 def settings_group(name, klass = nil, &block) name = interpret_name(name) if klass.nil? == block.nil? raise ::ArgumentError, "A group field requires a class or a block, but not both." end unless klass klass = ::Class.new(Settings) klass_name = to_class_name(name.to_s) const_set(klass_name, klass) klass.class_eval(&block) end @fields[name] = field = Field.new(self, name, SETTINGS_TYPE, klass) create_getter(field) self end |
Instance Method Details
#load_data!(data, raise_on_failure: false) ⇒ Array<FieldError>
Load the given hash of data into this settings object.
601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 |
# File 'lib/toys/settings.rb', line 601 def load_data!(data, raise_on_failure: false) errors = [] data.each do |name, value| name = name.to_sym field = @fields[name] begin raise FieldError.new(value, self.class, name, nil) unless field if field.group? raise FieldError.new(value, self.class, name, "Hash") unless value.is_a?(::Hash) get!(field).load_data!(value) else set!(field, value) end rescue FieldError => e raise e if raise_on_failure errors << e end end errors end |
#load_json!(str, raise_on_failure: false, **json_opts) ⇒ Array<FieldError>
Parse the given JSON string and load the data into this settings object.
658 659 660 661 |
# File 'lib/toys/settings.rb', line 658 def load_json!(str, raise_on_failure: false, **json_opts) require "json" load_data!(::JSON.parse(str, json_opts), raise_on_failure: raise_on_failure) end |
#load_json_file!(filename, raise_on_failure: false, **json_opts) ⇒ Array<FieldError>
Parse the given JSON file and load the data into this settings object.
672 673 674 |
# File 'lib/toys/settings.rb', line 672 def load_json_file!(filename, raise_on_failure: false, **json_opts) load_json!(File.read(filename), raise_on_failure: raise_on_failure, **json_opts) end |
#load_yaml!(str, raise_on_failure: false) ⇒ Array<FieldError>
Parse the given YAML string and load the data into this settings object.
631 632 633 634 |
# File 'lib/toys/settings.rb', line 631 def load_yaml!(str, raise_on_failure: false) require "psych" load_data!(::Psych.load(str), raise_on_failure: raise_on_failure) end |
#load_yaml_file!(filename, raise_on_failure: false) ⇒ Array<FieldError>
Parse the given YAML file and load the data into this settings object.
645 646 647 |
# File 'lib/toys/settings.rb', line 645 def load_yaml_file!(filename, raise_on_failure: false) load_yaml!(File.read(filename), raise_on_failure: raise_on_failure) end |