A simple and convenient way to declare complex constructors with a support for various commonly used type systems. (in active development).
Installation
gem 'smart_initializer'
bundle install
# --- or ---
gem install smart_initializer
require 'smart_core/initializer'
Table of contents
- Synopsis
- Initialization flow
- Attribute value definition flow
- Constructor definition DSL
- param
- option
- params
- options
- param and params signature
- option and options signature
- Initializer integration
- Basic Example
- Access to the instance attributes
- Configuration
- Type aliasing
- Type casting
- Initialization extension
- Plugins
- thy-types
- Roadmap
- Build
Synopsis
Initialization flow
- Parameter + Option definitioning and initialization (custom object allocator and constructor);
- Original #initialize invokation;
- Initialization extensions invokation;
NOTE!: SmarteCore::Initializer's constructor is invoked first
in order to guarantee the validity of the SmartCore::Initializer's functionality
(such as attribute overlap chek
, instant type checking
, value post-processing by finalize
, etc)
Attribute value definition flow (during object allocation and construction):
original value
-
(if defined):
default value
(default value is used whenoriginal value
is not defined) -
(if defined):
finalize
;
- if
default
-object is a proc-object - this proc-object will be invoked in theouter scope
of block definition; - if
finalize
-object is a proc-object - this proc-object will be invoked in theisntance
context (class instance);
NOTE: :finalize
block are not invoked on omitted optional: true
attributes
which has no :default
definition bock and which are not passed to the constructor. Example:
# without :default
class User
include SmartCore::Initializer
option :age, :string, optional: true, finalize: -> (val) { "#{val}_years" }
end
User.new.age # => nil
# with :default
class User
include SmartCore::Initializer
option :age, :string, optional: true, default: '0', finalize: -> (val) { "#{val}_years" }
end
User.new.age # => '0_years'
Constructor definition DSL
NOTE: last Hash
argument will be treated as kwarg
s;
param
-
param
- defines name-like attribute:-
cast
(optional) - type-cast received value if value has invalid type; -
privacy
(optional) - reader incapsulation level; -
finalize
(optional) - value post-processing (receives method name or proc) (the result value type is also validate); -
type_system
(optional) - differently chosen type system for the current attribute; -
as
(optional)- attribute alias (be careful with naming aliases that overlap the names of other attributes); -
mutable
(optional) - generate type-validated attr_writer in addition to attr_reader (false
by default) - (limitation) param has no
:default
option;
-
option
-
option
- defines kwarg-like attribute:-
cast
(optional) - type-cast received value if value has invalid type; -
privacy
(optional) - reader incapsulation level; -
as
(optional) - attribute alias (be careful with naming aliases that overlap the names of other attributes); -
mutable
(optional) - generate type-validated attr_writer in addition to attr_reader (false
by default) -
optional
(optional) - mark attribut as optional (you can may not initialize optional attributes, their values will be initialized withnil
or bydefault:
parameter); -
finalize
(optional) - value post-processing (receives method name or proc) (the result value type is also validate);- expects
Proc
object orsymbol
/string
isntance method;
- expects
-
default
(optional) - defalut value (if an attribute is not provided);- expects
Proc
object or a simple value of any type; - non-proc values will be
dup
licate during initialization;
- expects
-
type_system
(optional) - differently chosen type system for the current attribute;
-
params
-
params
- defines a series of parameters;-
:mutable
(optional) - (false
by default); -
:privacy
(optional) - (:public
by default);
-
options
-
options
- defines a series of options;-
:mutable
(optional) - (false
by default); -
:privacy
(optional) - (:public
by default);
-
param
and params
signautre:
param <attribute_name>,
<type=SmartCore::Types::Value::Any>, # Any by default
cast: false, # false by default
privacy: :public, # :public by default
finalize: proc { |value| value }, # no finalization by default
finalize: :some_method, # use this apporiach in order to finalize by `some_method(value)` instance method
as: :some_alias, # define attribute alias
mutable: true, # (false by default) generate type-validated attr_writer in addition to attr_reader
type_system: :smart_types # used by default
params <atribute_name1>, <attribute_name2>, <attribute_name3>, ...,
mutable: true, # generate type-validated attr_writer in addition to attr_reader (false by default);
privacy: :private # incapsulate all attributes as private
option
and options
signature:
option <attribute_name>,
<type=SmartCore::Types::Value::Any>, # Any by default
cast: false, # false by default
privacy: :public, # :public by default
finalize: proc { |value| value }, # no finalization by default
finalize: :some_method, # use this apporiach in order to finalize by `some_method(value)` instance method
default: 123, # no default value by default
default: proc { 123 }, # use proc/lambda object for dynamic initialization
as: :some_alias, # define attribute alias
mutable: true, # (false by default) generate type-validated attr_writer in addition to attr_reader
optional: true # (false by default) mark attribute as optional (attribute will be defined with `nil` or by `default:` value)
type_system: :smart_types # used by default
options <attribute_name1>, <attribute_name2>, <attribute_name3>, ...,
mutable: true, # generate type-validated attr_writer in addition to attr_reader (false by default);
privacy: :private # incapsulate all attributes as private
Initializer integration
- supports per-class configurations;
- possible configurations:
-
:type_system
- chosen type-system (smart_types
by default); -
:strict_options
- fail extra kwarg-attributes, passed to the constructor (true
by default); -
:auto_cast
- type-cast all values to the declared attribute type (false
by default);
-
# with pre-configured type system (:smart_types, see Configuration doc)
class MyStructure
include SmartCore::Initializer
end
# with manually chosen settings
class MyStructure
include SmartCore::Initializer(
type_system: :smart_types, # use smart_types
auto_cast: true, # type-cast all values by default
strict_options: false # ignore extra kwargs passed to the constructor
)
end
class AnotherStructure
include SmartCore::Initializer(type_system: :thy_types) # use thy_types and global defaults
end
Basic Example:
class User
include SmartCore::Initializer
# --- or ---
include SmartCore::Initializer(type_system: :smart_types)
param :user_id, SmartCore::Types::Value::Integer, cast: false, privacy: :public
param :login, :string, mutable: true
option :role, default: :user, finalize: -> { |value| Role.find(name: value) }
# NOTE: for method-based finalizetion use `your_method(value)` isntance method of your class;
# NOTE: for dynamic default values use `proc` objects and `lambda` objects;
params :name, :password
options :metadata, :enabled
end
# with correct types (incorrect types will raise SmartCore::Initializer::IncorrectTypeError)
object = User.new(1, 'kek123', 'John', 'test123', role: :admin, metadata: {}, enabled: false)
# attribute accessing:
object.user_id # => 1
object.login # => 'kek123'
object.name # => 'John'
object.password # => 'test123'
object.role # => :admin
object.metadata # => {}
object.enabled # => false
# attribute mutation (only mutable attributes have a mutator):
object.login = 123 # => (type vlaidation error) raises SmartCore::Initializer::IncorrectTypeError (expected String, got Integer)
object.login # => 'kek123'
object.login = 'pek456'
object.login # => 'pek456'
Access to the instance attributes
-
#__params__
- returns a list of initialized params; -
#__options__
- returns a list of initialized options; -
#__attributes__
- returns a list of merged params and options;
class User
include SmartCore::Initializer
param :first_name, 'string'
param :second_name, 'string'
option :age, 'numeric'
option :is_admin, 'boolean', default: true
end
user = User.new('Rustam', 'Ibragimov', age: 28)
user.__params__ # => { first_name: 'Rustam', second_name: 'Ibragimov' }
user.__options__ # => { age: 28, is_admin: true }
user.__attributes__ # => { first_name: 'Rustam', second_name: 'Ibragimov', age: 28, is_admin: true }
Configuration
-
configuration setitngs:
-
:default_type_system
- default type system (smart_types
by default); -
:strict_options
- fail on extra kwarg-attributes passed to the constructor (true
by default); -
:auto_cast
- type-cast all values to the declared attribute type (false
by default);
-
- by default, all classes uses and inherits the Global configuration;
- you can read config values via
[]
or.config.settings
or.config[key]
; - each class can be configured separately (in
include
invocation); - global configuration affects classes used the default global configs in run-time;
- each class can be re-configured separately in run-time;
- based on
Qonfig
gem;
# Global configuration:
SmartCore::Initializer::Configuration.configure do |config|
config.default_type_system = :smart_types # default setting value
config.strict_options = true # default setting value
config.auto_cast = false # default setting value
end
# Read configs:
SmartCore::Initializer::Configuration[:default_type_system]
SmartCore::Initializer::Configuration.config[:default_type_system]
SmartCore::Initializer::Configuration.config.settings.default_type_system
# per-class configuration:
class Parameters
include SmartCore::Initializer(auto_cast: true, strict_options: false)
# 1. use globally configured `smart_types` (default value)
# 2. type-cast all attributes by default (auto_cast: true)
# 3. ignore extra kwarg-attributes passed to the constructor (strict_options: false)
end
class User
include SmartCore::Initializer(type_system: :thy_types)
# 1. use :thy_types isntead of pre-configured :smart_types
# 2. use pre-configured auto_cast (false by default above)
# 3. use pre-configured strict_options ()
end
# debug class-related configurations:
class SomeClass
include SmartCore::Initializer(type_system: :thy_types)
end
SomeClass.__initializer_settings__[:type_system] # => :thy_types
SomeClass.__initializer_settings__[:auto_cast] # => false
SomeClass.__initializer_settings__[:strict_options] # => true
Type aliasing
- Usage:
# for smart_types:
SmartCore::Initializer::TypeSystem::SmartTypes.type_alias('hsh', SmartCore::Types::Value::Hash)
# for thy:
SmartCore::Initializer::TypeSystem::ThyTypes.type_alias('int', Thy::Tyhes::Integer)
class User
include SmartCore::Initializer
param :data, 'hsh' # use your new defined type alias
option :metadata, :hsh # use your new defined type alias
param :age, 'int', type_system: :thy_types
end
- Predefined aliases:
# for smart_types:
SmartCore::Initializer::TypeSystem::SmartTypes.type_aliases
# for thy_types:
SmartCore::Initializer::TypeSystem::ThyTypes.type_aliases
Type-casting
- make param/option as type-castable:
class Order
include SmartCore::Initializer
param :manager, 'string' # cast: false is used by default
param :amount, 'float', cast: true
option :status, :symbol # cast: false is used by default
option :is_processed, 'boolean', cast: true
option :processed_at, 'time', cast: true
end
order = Order.new(
'Daiver',
'123.456',
status: :pending,
is_processed: nil,
processed_at: '2021-01-01'
)
order.manager # => 'Daiver'
order.amount # => 123.456 (type casted)
order.status # => :pending
order.is_processed # => false (type casted)
order.processed_at # => 2021-01-01 00:00:00 +0300 (type casted)
- configure automatic type casting:
# per class
class User
include SmartCore::Initializer(auto_cast: true) # auto type cast every attribute
param :x, 'string'
param :y, 'numeric', cast: false # disable type-casting
option :b, 'integer', cast: false # disable type-casting
option :c, 'boolean'
end
# globally
SmartCore::Initializer::Configuration.configure do |config|
config.auto_cast = true # false by default
end
Initialization extension
-
ext_init(&block)
:- you can define as many extensions as you want;
- extensions are invoked in the order they are defined;
- alias method:
extend_initialization_flow
;
class User
include SmartCore::Initializer
option :name, :name
option :age, :integer
ext_init { |instance| instance.define_singleton_method(:extra) { :ext1 } }
ext_init { |instance| instance.define_singleton_method(:extra2) { :ext2 } }
end
user = User.new(name: 'keka', age: 123)
user.name # => 'keka'
user.age # => 123
user.extra # => :ext1
user.extra2 # => :ext2
Plugins
- thy-types
Plugin: thy-types
Support for Thy::Types
type system (gem)
- install
thy
types (gem install thy
):
gem 'thy'
bundle install
- enable
thy_types
plugin:
require 'thy'
SmartCore::Initializer::Configuration.plugin(:thy_types)
- usage:
class User
include SmartCore::Initializer(type_system: :thy_types)
param :nickname, 'string'
param :email, 'value.text', type_system: :smart_types # mixing with smart_types
option :admin, Thy::Types::Boolean, default: false
option :age, (Thy::Type.new { |value| value > 18 }) # custom thy type is supported too
end
# valid case:
User.new('daiver', 'iamdaiver@gmail.com', { admin: true, age: 19 })
# => new user object
# invalid case (invalid age)
User.new('daiver', 'iamdaiver@gmail.com', { age: 17 })
# SmartCore::Initializer::ThyTypeValidationError
# invaldi case (invalid nickname)
User.new(123, 'test', { admin: true, age: 22 })
# => SmartCore::Initializer::ThyTypeValidationError
Roadmap
- an ability to re-define existing options and parameters in children classes;
- More semantic attribute declaration errors (more domain-related attribute error objects);
- incorrect
:finalize
argument type:ArgumentError
=>FinalizeArgumentError
; - incorrect
:as
argument type:ArguemntError
=>AsArgumentError
; - etc;
- incorrect
- Support for
RSpec
doubles and instance_doubles inside the type system integration; - Specs restructuring;
- Migrate from
TravisCI
toGitHub Actions
; - Extract
Type Interop
system tosmart_type-system
; - an ability to define nested-
option
(orparam
) for structure-like object (for object with "nested" nature likea.b.c
ora[:b][:c]
) with data type validaitons and with a support of (almost) full attribute DSL;
Build
Tests Running
- with plugin tests:
bin/rspec -w
- without plugin tests:
bin/rspec -n
- help message:
bin/rspec -h
Code Style Checking
- without auto-correction:
bundle exec rake rubocop
- with auto-correction:
bundle exec rake rubocop -A
Contributing
- Fork it ( https://github.com/smart-rb/smart_initializer )
- Create your feature branch (
git checkout -b feature/my-new-feature
) - Commit your changes (
git commit -am '[feature_context] Add some feature'
) - Push to the branch (
git push origin feature/my-new-feature
) - Create new Pull Request
License
Released under MIT License.