Skip to content

Latest commit

 

History

History
601 lines (442 loc) · 17.2 KB

README.md

File metadata and controls

601 lines (442 loc) · 17.2 KB

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


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', '[email protected]', { admin: true, age: 19 })
# => new user object

# invalid case (invalid age)
User.new('daiver', '[email protected]', { 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