# SmartCore::Initializer · · [![Gem Version](https://badge.fury.io/rb/smart_initializer.svg)](https://badge.fury.io/rb/smart_initializer)
A simple and convenient way to declare complex constructors with a support for various commonly used type systems.
(**in active development**).
---
---
## Installation
```ruby
gem 'smart_initializer'
```
```shell
bundle install
# --- or ---
gem install smart_initializer
```
```ruby
require 'smart_core/initializer'
```
---
## Table of contents
- [Synopsis](#synopsis)
- [Initialization flow](#initialization-flow)
- [Attribute value definition flow](#attribute-value-definition-flow-during-object-allocation-and-construction)
- [Constructor definition DSL](#constructor-definition-dsl)
- [param](#param)
- [option](#option)
- [params](#params)
- [options](#options)
- [param and params signature](#param-and-params-signautre)
- [option and options signature](#option-and-options-signature)
- [Initializer integration](#initializer-integration)
- [Basic Example](#basic-example)
- [Access to the instance attributes](#access-to-the-instance-attributes)
- [Configuration](#configuration)
- [Type aliasing](#type-aliasing)
- [Type casting](#type-casting)
- [Initialization extension](#initialization-extension)
- [Plugins](#plugins)
- [thy-types](#plugin-thy-types)
- [Roadmap](#roadmap)
- [Build](#build)
---
## Synopsis
#### Initialization flow
1. Parameter + Option definitioning and initialization (custom object allocator and constructor);
2. Original **#initialize** invokation;
3. 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):
1. `original value`
2. *(if defined)*: `default value` (default value is used when `original value` is not defined)
3. *(if defined)*: `finalize`;
- if `default`-object is a proc-object - this proc-object will be invoked in the `outer scope` of block definition;
- if `finalize`-object is a proc-object - this proc-object will be invoked in the `isntance` 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:
```ruby
# without :default
class User
include SmartCore::Initializer
option :age, :string, optional: true, finalize: -> (val) { "#{val}_years" }
end
User.new.age # => nil
```
```ruby
# 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 with `nil` or by `default:` parameter);
- `finalize` (optional) - value post-processing (receives method name or proc) (the result value type is also validate);
- expects `Proc` object or `symbol`/`string` isntance method;
- `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;
- `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:
```ruby
param ,
, # 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
```
```ruby
params , , , ...,
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:
```ruby
option ,
, # 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
```
```ruby
options , , , ...,
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);
```ruby
# with pre-configured type system (:smart_types, see Configuration doc)
class MyStructure
include SmartCore::Initializer
end
```
```ruby
# 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:
```ruby
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;
```ruby
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;
```ruby
# 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
```
```ruby
# Read configs:
SmartCore::Initializer::Configuration[:default_type_system]
SmartCore::Initializer::Configuration.config[:default_type_system]
SmartCore::Initializer::Configuration.config.settings.default_type_system
```
```ruby
# 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
```
```ruby
# 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:
```ruby
# 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:
```ruby
# 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:
```ruby
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:
```ruby
# 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
```
```ruby
# 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`;
```ruby
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)
---
## Plugin: thy-types
Support for `Thy::Types` type system ([gem](https://github.com/akxcv/thy))
- install `thy` types (`gem install thy`):
```ruby
gem 'thy'
```
```shell
bundle install
```
- enable `thy_types` plugin:
```ruby
require 'thy'
SmartCore::Initializer::Configuration.plugin(:thy_types)
```
- usage:
```ruby
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
- More semantic attribute declaration errors (more domain-related attribute error objects);
- incorrect `:finalize` argument type: `ArgumentError` => `FinalizeArgumentError`;
- incorrect `:as` argument type: `ArguemntError` => `AsArgumentError`;
- etc;
- Support for `RSpec` doubles and instance_doubles inside the type system integration;
- Specs restructuring;
- Migrate from `TravisCI` to `GitHub Actions`;
- Extract `Type Interop` system to `smart_type-system`;
---
## Build
### Tests Running
- with plugin tests:
```shell
bin/rspec -w
```
- without plugin tests:
```shell
bin/rspec -n
```
- help message:
```shell
bin/rspec -h
```
### Code Style Checking
- without auto-correction:
```shell
bundle exec rake rubocop
```
- with auto-correction:
```shell
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.
## Supporting
## Authors
[Rustam Ibragimov](https://github.com/0exp)