Skip to content

Repository files navigation

SmartCore::Initializer · Supported by Cado Labs · Gem Version

A simple and convenient way to declare complex constructors with a support for various commonly used type systems. (in active development).


Supported by Cado Labs


Installation

gem'smart_initializer'
bundle install
# --- or ---
gem install smart_initializer
require'smart_core/initializer'

Table of contents


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:

# without :defaultclassUserincludeSmartCore::Initializeroption:age,:string,optional: true,finalize: ->(val){"#{val}_years"}endUser.new.age# => nil
# with :defaultclassUserincludeSmartCore::Initializeroption:age,:string,optional: true,default: '0',finalize: ->(val){"#{val}_years"}endUser.new.age# => '0_years'

Constructor definition DSL

NOTE: last Hash argument will be treated as kwargs;

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 duplicate 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:

param <attribute_name>,
<type=SmartCore::Types::Value::Any>,# Any by defaultcast: false,# false by defaultprivacy: :public,# :public by defaultfinalize: proc{ |value| value},# no finalization by defaultfinalize: :some_method,# use this apporiach in order to finalize by `some_method(value)` instance methodas: :some_alias,# define attribute aliasmutable: true,# (false by default) generate type-validated attr_writer in addition to attr_readertype_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 defaultcast: false,# false by defaultprivacy: :public,# :public by defaultfinalize: proc{ |value| value},# no finalization by defaultfinalize: :some_method,# use this apporiach in order to finalize by `some_method(value)` instance methoddefault: 123,# no default value by defaultdefault: proc{123},# use proc/lambda object for dynamic initializationas: :some_alias,# define attribute aliasmutable: true,# (false by default) generate type-validated attr_writer in addition to attr_readeroptional: 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)classMyStructureincludeSmartCore::Initializerend
# with manually chosen settingsclassMyStructureincludeSmartCore::Initializer(type_system: :smart_types,# use smart_typesauto_cast: true,# type-cast all values by defaultstrict_options: false# ignore extra kwargs passed to the constructor)endclassAnotherStructureincludeSmartCore::Initializer(type_system: :thy_types)# use thy_types and global defaultsend

Basic Example:

classUserincludeSmartCore::Initializer# --- or ---includeSmartCore::Initializer(type_system: :smart_types)param:user_id,SmartCore::Types::Value::Integer,cast: false,privacy: :publicparam:login,:string,mutable: trueoption: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,:passwordoptions:metadata,:enabledend# 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# => 1object.login# => 'kek123'object.name# => 'John'object.password# => 'test123'object.role# => :adminobject.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;
classUserincludeSmartCore::Initializerparam:first_name,'string'param:second_name,'string'option:age,'numeric'option:is_admin,'boolean',default: trueenduser=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.configuredo |config|
config.default_type_system=:smart_types# default setting valueconfig.strict_options=true# default setting valueconfig.auto_cast=false# default setting valueend
# 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:classParametersincludeSmartCore::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)endclassUserincludeSmartCore::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:classSomeClassincludeSmartCore::Initializer(type_system: :thy_types)endSomeClass.__initializer_settings__[:type_system]# => :thy_typesSomeClass.__initializer_settings__[:auto_cast]# => falseSomeClass.__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)classUserincludeSmartCore::Initializerparam:data,'hsh'# use your new defined type aliasoption:metadata,:hsh# use your new defined type aliasparam:age,'int',type_system: :thy_typesend
  • 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:
classOrderincludeSmartCore::Initializerparam:manager,'string'# cast: false is used by defaultparam:amount,'float',cast: trueoption:status,:symbol# cast: false is used by defaultoption:is_processed,'boolean',cast: trueoption:processed_at,'time',cast: trueendorder=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# => :pendingorder.is_processed# => false (type casted)order.processed_at# => 2021-01-01 00:00:00 +0300 (type casted)
  • configure automatic type casting:
# per classclassUserincludeSmartCore::Initializer(auto_cast: true)# auto type cast every attributeparam:x,'string'param:y,'numeric',cast: false# disable type-castingoption:b,'integer',cast: false# disable type-castingoption:c,'boolean'end
# globallySmartCore::Initializer::Configuration.configuredo |config|
config.auto_cast=true# false by defaultend

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;
classUserincludeSmartCore::Initializeroption:name,:nameoption:age,:integerext_init{ |instance| instance.define_singleton_method(:extra){:ext1}}ext_init{ |instance| instance.define_singleton_method(:extra2){:ext2}}enduser=User.new(name: 'keka',age: 123)user.name# => 'keka'user.age# => 123user.extra# => :ext1user.extra2# => :ext2

Plugins


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:
classUserincludeSmartCore::Initializer(type_system: :thy_types)param:nickname,'string'param:email,'value.text',type_system: :smart_types# mixing with smart_typesoption:admin,Thy::Types::Boolean,default: falseoption:age,(Thy::Type.new{ |value| value > 18})# custom thy type is supported tooend# 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;
  • 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;
  • an ability to define nested-option (or param) for structure-like object (for object with "nested" nature like a.b.c or a[: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.

Supporting

Supported by Cado Labs

Authors

Rustam Ibragimov

About

A simple and convenient way to declare complex constructors with a support for various commonly used type systems.

Topics

Resources

Code of conduct

Stars

18 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages