├── .gitignore ├── .rspec ├── .rubocop.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console ├── rspec └── setup ├── gemfiles ├── with_external_deps.gemfile ├── with_external_deps.gemfile.lock ├── without_external_deps.gemfile └── without_external_deps.gemfile.lock ├── lib └── smart_core │ ├── initializer.rb │ └── initializer │ ├── attribute.rb │ ├── attribute │ ├── factory.rb │ ├── factory │ │ ├── base.rb │ │ ├── option.rb │ │ └── param.rb │ ├── finalizer.rb │ ├── finalizer │ │ ├── abstract.rb │ │ ├── anonymous_block.rb │ │ └── instance_method.rb │ ├── list.rb │ ├── value.rb │ └── value │ │ ├── base.rb │ │ ├── option.rb │ │ └── param.rb │ ├── configuration.rb │ ├── constructor.rb │ ├── constructor │ ├── dsl.rb │ ├── dsl │ └── inheritance.rb │ ├── errors.rb │ ├── extensions.rb │ ├── extensions │ ├── abstract.rb │ ├── ext_init.rb │ └── list.rb │ ├── functionality.rb │ ├── instance_attribute_accessing.rb │ ├── plugins.rb │ ├── plugins │ ├── abstract.rb │ ├── access_mixin.rb │ ├── registry.rb │ ├── registry_interface.rb │ ├── thy_types.rb │ └── thy_types │ │ ├── errors.rb │ │ ├── thy_types.rb │ │ └── thy_types │ │ ├── abstract_factory.rb │ │ ├── operation.rb │ │ └── operation │ │ ├── base.rb │ │ ├── cast.rb │ │ ├── valid.rb │ │ └── validate.rb │ ├── settings.rb │ ├── settings │ ├── auto_cast.rb │ ├── base.rb │ ├── duplicator.rb │ ├── strict_options.rb │ └── type_system.rb │ ├── type_system.rb │ ├── type_system │ ├── interop.rb │ ├── interop │ │ ├── abstract_factory.rb │ │ ├── aliasing.rb │ │ ├── aliasing │ │ │ └── alias_list.rb │ │ └── operation.rb │ ├── registry.rb │ ├── registry_interface.rb │ ├── smart_types.rb │ └── smart_types │ │ ├── abstract_factory.rb │ │ ├── operation.rb │ │ └── operation │ │ ├── base.rb │ │ ├── cast.rb │ │ ├── valid.rb │ │ └── validate.rb │ └── version.rb ├── smart_initializer.gemspec └── spec ├── features ├── configuration_spec.rb ├── instance_attribute_list_access_spec.rb ├── integration_spec.rb ├── plugin_system │ ├── load_spec.rb │ └── registration_spec.rb ├── plugins │ └── thy_types_spec.rb └── type_system │ ├── custom_type_system_integration_spec.rb │ └── type_aliasing_spec.rb ├── smoke_spec.rb ├── spec_helper.rb └── support ├── meta_scopes.rb └── spec_support.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | .rspec_status 10 | /.idea 11 | .ruby-version 12 | *.gem 13 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format progress 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_gem: 2 | armitage-rubocop: 3 | - lib/rubocop.general.yml 4 | - lib/rubocop.rake.yml 5 | - lib/rubocop.rspec.yml 6 | 7 | AllCops: 8 | TargetRubyVersion: 3.1 9 | NewCops: enable 10 | Include: 11 | - lib/**/*.rb 12 | - spec/**/*.rb 13 | - Gemfile 14 | - Rakefile 15 | - smart_initializer.gemspec 16 | - gemfiles/*.gemfile 17 | - bin/console 18 | 19 | # NOTE: It is not suitable for infrastracture-level frameworks 20 | Metrics/ParameterLists: 21 | Enabled: false 22 | 23 | # NOTE: It is ok to use empty blocks in specs (inside simple test cases) 24 | Lint/EmptyBlock: 25 | Exclude: 26 | - spec/**/*.rb 27 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | ## [0.11.0] - 2022-11-25 5 | ### Changed 6 | - Support for *Ruby@2.5* and *Ruby@2.6* has ended; 7 | - Reduced object allocation count during type checking process; 8 | - Removed useless `Incorrect type` text part from `SmartCore::Initializer::IncorrectTypeError` exception message; 9 | - Updated development dependencies; 10 | 11 | ## [0.10.0] - 2022-10-04 12 | ### Changed 13 | - `SmartCore::Engine::ReadWriteLock` is used instead `SmartCore::Engine::Lock` in order to decrease the count of RubyVM's context switching and useless Mutexes usage; 14 | - reduced `KeyError`-exception flow use cases in some cases inside the framework internals (in order to reduce object allocations under the hood); 15 | - bumped development dependencies; 16 | - bumped core dependencies (in order to use `ReadWiteLock` (and actualize available inner-framework features); 17 | 18 | ## [0.9.1] - 2022-03-06 19 | ### Fixed 20 | - `finalize` now accepts lambdas with arity `-2`. For example `->(a, *b) {}` or `:freeze.to_proc` 21 | which in `Ruby >= 3` returns lambda with arity equal to `-2`. 22 | 23 | ## [0.9.0] - 2021-12-19 24 | ### Changed 25 | - `:finalize` block is not invoked on the `option` with `optional: true` flag; 26 | 27 | ## [0.8.0] - 2021-12-04 28 | ### Added 29 | - New options for option and param attributes: 30 | - Support for attribute **aliasing** (`:as` parameter); 31 | - supports: `option`, `param`; 32 | - Support for attribute **auto-casting** (`:auto_cast` parameter); 33 | - supports: `option`, `param`; 34 | - Support for **mutable** attributes (`:mutable` parameter) with type-validation inside; 35 | - supports: `option`, `options`, `param`, `params`; 36 | - Support for **optional** attributes (`:optional` parameter); 37 | - supports: `option`; 38 | - **SmartCore::Initializer::Configuration**: 39 | - Configuration setting `strict_options` now works separately "per-class" in all configs manner 40 | (each class shares the global state but has own state too) 41 | - Support for per-class/global `:auto_cast` configuration; 42 | - provides a global/per-class behavior for `:cast` option (`false` by default); 43 | - `options` declaration now supports `privacy` option; 44 | - `params` declaration now supports `privacy` option; 45 | 46 | ### Changed 47 | - Drop support of **Ruby@2.4**; 48 | - `:default` attribute parameter now duplicates the passed value during object instantiation; 49 | - `:default` attribute parameter only works with `option` attribute (`param` can't have `default` parameter now); 50 | - Attribute values generated by `:finalize` are type-validated now too; 51 | - Now lambda-based `:finalize` attributes are being checked for the signature during attribute declaration; 52 | - Some error messages have become more readable; 53 | 54 | ## Fixed 55 | - `send` method overlaping/rewriting: otions and params named as `send` breaks the internal 56 | framework-related invocations of the attribute definitioning inside the custom object constructor 57 | (this name rewrites internal Ruby's `send` method and brokes some things); 58 | 59 | ## [0.7.0] - 2021-06-23 60 | ### Added 61 | - `strict_options` config option for non-strict checking of passed options; 62 | 63 | ## [0.6.0] - 2021-06-23 64 | ### Added 65 | - Validation messages for incorrect attribute types; 66 | 67 | ## [0.5.0] - 2021-01-18 68 | ### Changed 69 | - Updated `smart_types` dependency (`~> 0.4.0`) to guarantee **Ruby@3** compatibility; 70 | - Updated development dependencies; 71 | 72 | ## [0.4.0] - 2021-01-18 73 | ### Added 74 | - Support for **Ruby 3**; 75 | 76 | ### Changed 77 | - Moved from `TravisCI` to nothing (todo: migrate to `GitHub Actions`). 78 | Temporary: we must run test cases locally. 79 | - Updated development dependencies; 80 | 81 | ## [0.3.2] - 2020-07-12 82 | ### Fixed 83 | - Deeply inherited entities lose their `__initializer_settings__` entitiy; 84 | 85 | ## [0.3.1] - 2020-07-12 86 | ### Fixed 87 | - Deeply inherited entities lose their class attribute definers; 88 | 89 | ## [0.3.0] - 2020-07-11 90 | ### Added 91 | - `extend_initialization_flow` alias method for `SmartCore::Initializer.ext_init`; 92 | - Access methods to the instance attribute lists (`#__params`, `#__options__`, `#__attributes__`); 93 | 94 | ### Changed 95 | - Updated development dependencies; 96 | 97 | ## [0.2.0] - 2020-05-16 98 | ### Changed 99 | - **Constructor**: disallow unknown option attributes; 100 | 101 | ## [0.1.0] - 2020-05-10 102 | - Release :) 103 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at iamdaiver@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [https://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: https://contributor-covenant.org 74 | [version]: https://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | gemspec 6 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | smart_initializer (0.12.0) 5 | qonfig (~> 0.24) 6 | smart_engine (~> 0.16) 7 | smart_types (~> 0.8) 8 | 9 | GEM 10 | remote: https://rubygems.org/ 11 | specs: 12 | activesupport (7.0.4) 13 | concurrent-ruby (~> 1.0, >= 1.0.2) 14 | i18n (>= 1.6, < 2) 15 | minitest (>= 5.1) 16 | tzinfo (~> 2.0) 17 | armitage-rubocop (1.36.0) 18 | rubocop (= 1.36.0) 19 | rubocop-performance (= 1.15.0) 20 | rubocop-rails (= 2.16.1) 21 | rubocop-rake (= 0.6.0) 22 | rubocop-rspec (= 2.13.2) 23 | ast (2.4.2) 24 | bigdecimal (3.1.8) 25 | coderay (1.1.3) 26 | concurrent-ruby (1.1.10) 27 | diff-lcs (1.5.0) 28 | docile (1.4.0) 29 | i18n (1.12.0) 30 | concurrent-ruby (~> 1.0) 31 | json (2.6.2) 32 | method_source (1.0.0) 33 | minitest (5.16.3) 34 | ostruct (0.6.0) 35 | parallel (1.22.1) 36 | parser (3.1.2.1) 37 | ast (~> 2.4.1) 38 | pry (0.14.1) 39 | coderay (~> 1.1) 40 | method_source (~> 1.0) 41 | qonfig (0.28.0) 42 | rack (3.0.1) 43 | rainbow (3.1.1) 44 | rake (13.0.6) 45 | regexp_parser (2.6.1) 46 | rexml (3.2.5) 47 | rspec (3.12.0) 48 | rspec-core (~> 3.12.0) 49 | rspec-expectations (~> 3.12.0) 50 | rspec-mocks (~> 3.12.0) 51 | rspec-core (3.12.0) 52 | rspec-support (~> 3.12.0) 53 | rspec-expectations (3.12.0) 54 | diff-lcs (>= 1.2.0, < 2.0) 55 | rspec-support (~> 3.12.0) 56 | rspec-mocks (3.12.0) 57 | diff-lcs (>= 1.2.0, < 2.0) 58 | rspec-support (~> 3.12.0) 59 | rspec-support (3.12.0) 60 | rubocop (1.36.0) 61 | json (~> 2.3) 62 | parallel (~> 1.10) 63 | parser (>= 3.1.2.1) 64 | rainbow (>= 2.2.2, < 4.0) 65 | regexp_parser (>= 1.8, < 3.0) 66 | rexml (>= 3.2.5, < 4.0) 67 | rubocop-ast (>= 1.20.1, < 2.0) 68 | ruby-progressbar (~> 1.7) 69 | unicode-display_width (>= 1.4.0, < 3.0) 70 | rubocop-ast (1.23.0) 71 | parser (>= 3.1.1.0) 72 | rubocop-performance (1.15.0) 73 | rubocop (>= 1.7.0, < 2.0) 74 | rubocop-ast (>= 0.4.0) 75 | rubocop-rails (2.16.1) 76 | activesupport (>= 4.2.0) 77 | rack (>= 1.1) 78 | rubocop (>= 1.33.0, < 2.0) 79 | rubocop-rake (0.6.0) 80 | rubocop (~> 1.0) 81 | rubocop-rspec (2.13.2) 82 | rubocop (~> 1.33) 83 | ruby-progressbar (1.11.0) 84 | simplecov (0.21.2) 85 | docile (~> 1.1) 86 | simplecov-html (~> 0.11) 87 | simplecov_json_formatter (~> 0.1) 88 | simplecov-html (0.12.3) 89 | simplecov_json_formatter (0.1.4) 90 | smart_engine (0.17.0) 91 | smart_types (0.8.0) 92 | smart_engine (~> 0.11) 93 | tzinfo (2.0.5) 94 | concurrent-ruby (~> 1.0) 95 | unicode-display_width (2.3.0) 96 | 97 | PLATFORMS 98 | arm64-darwin-21 99 | arm64-darwin-23 100 | 101 | DEPENDENCIES 102 | armitage-rubocop (~> 1.30) 103 | bigdecimal 104 | bundler (~> 2.3) 105 | ostruct 106 | pry (~> 0.14) 107 | rake (~> 13.0) 108 | rspec (~> 3.11) 109 | simplecov (~> 0.21) 110 | smart_initializer! 111 | 112 | BUNDLED WITH 113 | 2.5.19 114 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020-2024 Rustam Ibragimov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SmartCore::Initializer · Supported by Cado Labs · [![Gem Version](https://badge.fury.io/rb/smart_initializer.svg)](https://badge.fury.io/rb/smart_initializer) 2 | 3 | A simple and convenient way to declare complex constructors with a support for various commonly used type systems. 4 | (**in active development**). 5 | 6 | --- 7 | 8 | Supported by Cado Labs 9 | 10 | --- 11 | 12 | ## Installation 13 | 14 | ```ruby 15 | gem 'smart_initializer' 16 | ``` 17 | 18 | ```shell 19 | bundle install 20 | # --- or --- 21 | gem install smart_initializer 22 | ``` 23 | 24 | ```ruby 25 | require 'smart_core/initializer' 26 | ``` 27 | 28 | --- 29 | 30 | ## Table of contents 31 | 32 | - [Synopsis](#synopsis) 33 | - [Initialization flow](#initialization-flow) 34 | - [Attribute value definition flow](#attribute-value-definition-flow-during-object-allocation-and-construction) 35 | - [Constructor definition DSL](#constructor-definition-dsl) 36 | - [param](#param) 37 | - [option](#option) 38 | - [params](#params) 39 | - [options](#options) 40 | - [param and params signature](#param-and-params-signautre) 41 | - [option and options signature](#option-and-options-signature) 42 | - [Initializer integration](#initializer-integration) 43 | - [Basic Example](#basic-example) 44 | - [Access to the instance attributes](#access-to-the-instance-attributes) 45 | - [Configuration](#configuration) 46 | - [Type aliasing](#type-aliasing) 47 | - [Type casting](#type-casting) 48 | - [Initialization extension](#initialization-extension) 49 | - [Plugins](#plugins) 50 | - [thy-types](#plugin-thy-types) 51 | - [Roadmap](#roadmap) 52 | - [Build](#build) 53 | 54 | --- 55 | 56 | ## Synopsis 57 | 58 | #### Initialization flow 59 | 60 | 1. Parameter + Option definitioning and initialization (custom object allocator and constructor); 61 | 2. Original **#initialize** invokation; 62 | 3. Initialization extensions invokation; 63 | 64 | **NOTE!**: **SmarteCore::Initializer**'s constructor is invoked first 65 | in order to guarantee the validity of the SmartCore::Initializer's functionality 66 | (such as `attribute overlap chek`, `instant type checking`, `value post-processing by finalize`, etc) 67 | 68 | #### Attribute value definition flow (during object allocation and construction): 69 | 70 | 1. `original value` 71 | 2. *(if defined)*: `default value` (default value is used when `original value` is not defined) 72 | 3. *(if defined)*: `finalize`; 73 | 74 | - if `default`-object is a proc-object - this proc-object will be invoked in the `outer scope` of block definition; 75 | - if `finalize`-object is a proc-object - this proc-object will be invoked in the `isntance` context (class instance); 76 | 77 | **NOTE**: `:finalize` block are not invoked on omitted `optional: true` attributes 78 | which has no `:default` definition bock and which are not passed to the constructor. Example: 79 | 80 | ```ruby 81 | # without :default 82 | 83 | class User 84 | include SmartCore::Initializer 85 | option :age, :string, optional: true, finalize: -> (val) { "#{val}_years" } 86 | end 87 | 88 | User.new.age # => nil 89 | ``` 90 | 91 | ```ruby 92 | # with :default 93 | 94 | class User 95 | include SmartCore::Initializer 96 | option :age, :string, optional: true, default: '0', finalize: -> (val) { "#{val}_years" } 97 | end 98 | 99 | User.new.age # => '0_years' 100 | ``` 101 | 102 | --- 103 | 104 | ### Constructor definition DSL 105 | 106 | **NOTE**: last `Hash` argument will be treated as `kwarg`s; 107 | 108 | #### param 109 | 110 | - `param` - defines name-like attribute: 111 | - `cast` (optional) - type-cast received value if value has invalid type; 112 | - `privacy` (optional) - reader incapsulation level; 113 | - `finalize` (optional) - value post-processing (receives method name or proc) (the result value type is also validate); 114 | - `type_system` (optional) - differently chosen type system for the current attribute; 115 | - `as` (optional)- attribute alias (be careful with naming aliases that overlap the names of other attributes); 116 | - `mutable` (optional) - generate type-validated attr_writer in addition to attr_reader (`false` by default) 117 | - (**limitation**) param has no `:default` option; 118 | 119 | #### option 120 | 121 | - `option` - defines kwarg-like attribute: 122 | - `cast` (optional) - type-cast received value if value has invalid type; 123 | - `privacy` (optional) - reader incapsulation level; 124 | - `as` (optional) - attribute alias (be careful with naming aliases that overlap the names of other attributes); 125 | - `mutable` (optional) - generate type-validated attr_writer in addition to attr_reader (`false` by default) 126 | - `optional` (optional) - mark attribut as optional (you can may not initialize optional attributes, 127 | their values will be initialized with `nil` or by `default:` parameter); 128 | - `finalize` (optional) - value post-processing (receives method name or proc) (the result value type is also validate); 129 | - expects `Proc` object or `symbol`/`string` isntance method; 130 | - `default` (optional) - defalut value (if an attribute is not provided); 131 | - expects `Proc` object or a simple value of any type; 132 | - non-proc values will be `dup`licate during initialization; 133 | - `type_system` (optional) - differently chosen type system for the current attribute; 134 | 135 | #### params 136 | 137 | - `params` - defines a series of parameters; 138 | - `:mutable` (optional) - (`false` by default); 139 | - `:privacy` (optional) - (`:public` by default); 140 | 141 | #### options 142 | 143 | - `options` - defines a series of options; 144 | - `:mutable` (optional) - (`false` by default); 145 | - `:privacy` (optional) - (`:public` by default); 146 | 147 | 148 | #### `param` and `params` signautre: 149 | 150 | ```ruby 151 | param , 152 | , # Any by default 153 | cast: false, # false by default 154 | privacy: :public, # :public by default 155 | finalize: proc { |value| value }, # no finalization by default 156 | finalize: :some_method, # use this apporiach in order to finalize by `some_method(value)` instance method 157 | as: :some_alias, # define attribute alias 158 | mutable: true, # (false by default) generate type-validated attr_writer in addition to attr_reader 159 | type_system: :smart_types # used by default 160 | ``` 161 | 162 | ```ruby 163 | params , , , ..., 164 | mutable: true, # generate type-validated attr_writer in addition to attr_reader (false by default); 165 | privacy: :private # incapsulate all attributes as private 166 | ``` 167 | 168 | #### `option` and `options` signature: 169 | 170 | ```ruby 171 | option , 172 | , # Any by default 173 | cast: false, # false by default 174 | privacy: :public, # :public by default 175 | finalize: proc { |value| value }, # no finalization by default 176 | finalize: :some_method, # use this apporiach in order to finalize by `some_method(value)` instance method 177 | default: 123, # no default value by default 178 | default: proc { 123 }, # use proc/lambda object for dynamic initialization 179 | as: :some_alias, # define attribute alias 180 | mutable: true, # (false by default) generate type-validated attr_writer in addition to attr_reader 181 | optional: true # (false by default) mark attribute as optional (attribute will be defined with `nil` or by `default:` value) 182 | type_system: :smart_types # used by default 183 | ``` 184 | 185 | ```ruby 186 | options , , , ..., 187 | mutable: true, # generate type-validated attr_writer in addition to attr_reader (false by default); 188 | privacy: :private # incapsulate all attributes as private 189 | ``` 190 | 191 | --- 192 | 193 | ## Initializer integration 194 | 195 | - supports per-class configurations; 196 | - possible configurations: 197 | - `:type_system` - chosen type-system (`smart_types` by default); 198 | - `:strict_options` - fail extra kwarg-attributes, passed to the constructor (`true` by default); 199 | - `:auto_cast` - type-cast all values to the declared attribute type (`false` by default); 200 | 201 | ```ruby 202 | # with pre-configured type system (:smart_types, see Configuration doc) 203 | 204 | class MyStructure 205 | include SmartCore::Initializer 206 | end 207 | ``` 208 | 209 | ```ruby 210 | # with manually chosen settings 211 | 212 | class MyStructure 213 | include SmartCore::Initializer( 214 | type_system: :smart_types, # use smart_types 215 | auto_cast: true, # type-cast all values by default 216 | strict_options: false # ignore extra kwargs passed to the constructor 217 | ) 218 | end 219 | 220 | class AnotherStructure 221 | include SmartCore::Initializer(type_system: :thy_types) # use thy_types and global defaults 222 | end 223 | ``` 224 | 225 | --- 226 | 227 | ### Basic Example: 228 | 229 | 230 | ```ruby 231 | class User 232 | include SmartCore::Initializer 233 | # --- or --- 234 | include SmartCore::Initializer(type_system: :smart_types) 235 | 236 | param :user_id, SmartCore::Types::Value::Integer, cast: false, privacy: :public 237 | param :login, :string, mutable: true 238 | 239 | option :role, default: :user, finalize: -> { |value| Role.find(name: value) } 240 | 241 | # NOTE: for method-based finalizetion use `your_method(value)` isntance method of your class; 242 | # NOTE: for dynamic default values use `proc` objects and `lambda` objects; 243 | 244 | params :name, :password 245 | options :metadata, :enabled 246 | end 247 | 248 | # with correct types (incorrect types will raise SmartCore::Initializer::IncorrectTypeError) 249 | object = User.new(1, 'kek123', 'John', 'test123', role: :admin, metadata: {}, enabled: false) 250 | 251 | # attribute accessing: 252 | object.user_id # => 1 253 | object.login # => 'kek123' 254 | object.name # => 'John' 255 | object.password # => 'test123' 256 | object.role # => :admin 257 | object.metadata # => {} 258 | object.enabled # => false 259 | 260 | # attribute mutation (only mutable attributes have a mutator): 261 | object.login = 123 # => (type vlaidation error) raises SmartCore::Initializer::IncorrectTypeError (expected String, got Integer) 262 | object.login # => 'kek123' 263 | object.login = 'pek456' 264 | object.login # => 'pek456' 265 | ``` 266 | 267 | --- 268 | 269 | ## Access to the instance attributes 270 | 271 | - `#__params__` - returns a list of initialized params; 272 | - `#__options__` - returns a list of initialized options; 273 | - `#__attributes__` - returns a list of merged params and options; 274 | 275 | ```ruby 276 | class User 277 | include SmartCore::Initializer 278 | 279 | param :first_name, 'string' 280 | param :second_name, 'string' 281 | option :age, 'numeric' 282 | option :is_admin, 'boolean', default: true 283 | end 284 | 285 | user = User.new('Rustam', 'Ibragimov', age: 28) 286 | 287 | user.__params__ # => { first_name: 'Rustam', second_name: 'Ibragimov' } 288 | user.__options__ # => { age: 28, is_admin: true } 289 | user.__attributes__ # => { first_name: 'Rustam', second_name: 'Ibragimov', age: 28, is_admin: true } 290 | ``` 291 | 292 | --- 293 | 294 | ## Configuration 295 | 296 | - **configuration setitngs**: 297 | - `:default_type_system` - default type system (`smart_types` by default); 298 | - `:strict_options` - fail on extra kwarg-attributes passed to the constructor (`true` by default); 299 | - `:auto_cast` - type-cast all values to the declared attribute type (`false` by default); 300 | - by default, all classes uses and inherits the Global configuration; 301 | - you can read config values via `[]` or `.config.settings` or `.config[key]`; 302 | - each class can be configured separately (in `include` invocation); 303 | - global configuration affects classes used the default global configs in run-time; 304 | - each class can be re-configured separately in run-time; 305 | - based on `Qonfig` gem; 306 | 307 | ```ruby 308 | # Global configuration: 309 | 310 | SmartCore::Initializer::Configuration.configure do |config| 311 | config.default_type_system = :smart_types # default setting value 312 | config.strict_options = true # default setting value 313 | config.auto_cast = false # default setting value 314 | end 315 | ``` 316 | 317 | ```ruby 318 | # Read configs: 319 | 320 | SmartCore::Initializer::Configuration[:default_type_system] 321 | SmartCore::Initializer::Configuration.config[:default_type_system] 322 | SmartCore::Initializer::Configuration.config.settings.default_type_system 323 | ``` 324 | 325 | ```ruby 326 | # per-class configuration: 327 | 328 | class Parameters 329 | include SmartCore::Initializer(auto_cast: true, strict_options: false) 330 | # 1. use globally configured `smart_types` (default value) 331 | # 2. type-cast all attributes by default (auto_cast: true) 332 | # 3. ignore extra kwarg-attributes passed to the constructor (strict_options: false) 333 | end 334 | 335 | class User 336 | include SmartCore::Initializer(type_system: :thy_types) 337 | # 1. use :thy_types isntead of pre-configured :smart_types 338 | # 2. use pre-configured auto_cast (false by default above) 339 | # 3. use pre-configured strict_options () 340 | end 341 | ``` 342 | 343 | ```ruby 344 | # debug class-related configurations: 345 | 346 | class SomeClass 347 | include SmartCore::Initializer(type_system: :thy_types) 348 | end 349 | 350 | SomeClass.__initializer_settings__[:type_system] # => :thy_types 351 | SomeClass.__initializer_settings__[:auto_cast] # => false 352 | SomeClass.__initializer_settings__[:strict_options] # => true 353 | ``` 354 | 355 | --- 356 | 357 | ## Type aliasing 358 | 359 | - Usage: 360 | 361 | ```ruby 362 | # for smart_types: 363 | SmartCore::Initializer::TypeSystem::SmartTypes.type_alias('hsh', SmartCore::Types::Value::Hash) 364 | 365 | # for thy: 366 | SmartCore::Initializer::TypeSystem::ThyTypes.type_alias('int', Thy::Tyhes::Integer) 367 | 368 | class User 369 | include SmartCore::Initializer 370 | 371 | param :data, 'hsh' # use your new defined type alias 372 | option :metadata, :hsh # use your new defined type alias 373 | 374 | param :age, 'int', type_system: :thy_types 375 | end 376 | ``` 377 | 378 | - Predefined aliases: 379 | 380 | ```ruby 381 | # for smart_types: 382 | SmartCore::Initializer::TypeSystem::SmartTypes.type_aliases 383 | 384 | # for thy_types: 385 | SmartCore::Initializer::TypeSystem::ThyTypes.type_aliases 386 | ``` 387 | 388 | --- 389 | 390 | ## Type-casting 391 | 392 | - make param/option as type-castable: 393 | 394 | ```ruby 395 | class Order 396 | include SmartCore::Initializer 397 | 398 | param :manager, 'string' # cast: false is used by default 399 | param :amount, 'float', cast: true 400 | 401 | option :status, :symbol # cast: false is used by default 402 | option :is_processed, 'boolean', cast: true 403 | option :processed_at, 'time', cast: true 404 | end 405 | 406 | order = Order.new( 407 | 'Daiver', 408 | '123.456', 409 | status: :pending, 410 | is_processed: nil, 411 | processed_at: '2021-01-01' 412 | ) 413 | 414 | order.manager # => 'Daiver' 415 | order.amount # => 123.456 (type casted) 416 | order.status # => :pending 417 | order.is_processed # => false (type casted) 418 | order.processed_at # => 2021-01-01 00:00:00 +0300 (type casted) 419 | ``` 420 | 421 | - configure automatic type casting: 422 | 423 | ```ruby 424 | # per class 425 | 426 | class User 427 | include SmartCore::Initializer(auto_cast: true) # auto type cast every attribute 428 | 429 | param :x, 'string' 430 | param :y, 'numeric', cast: false # disable type-casting 431 | 432 | option :b, 'integer', cast: false # disable type-casting 433 | option :c, 'boolean' 434 | end 435 | ``` 436 | 437 | ```ruby 438 | # globally 439 | 440 | SmartCore::Initializer::Configuration.configure do |config| 441 | config.auto_cast = true # false by default 442 | end 443 | ``` 444 | 445 | --- 446 | 447 | ## Initialization extension 448 | 449 | - `ext_init(&block)`: 450 | - you can define as many extensions as you want; 451 | - extensions are invoked in the order they are defined; 452 | - alias method: `extend_initialization_flow`; 453 | 454 | ```ruby 455 | class User 456 | include SmartCore::Initializer 457 | 458 | option :name, :name 459 | option :age, :integer 460 | 461 | ext_init { |instance| instance.define_singleton_method(:extra) { :ext1 } } 462 | ext_init { |instance| instance.define_singleton_method(:extra2) { :ext2 } } 463 | end 464 | 465 | user = User.new(name: 'keka', age: 123) 466 | user.name # => 'keka' 467 | user.age # => 123 468 | user.extra # => :ext1 469 | user.extra2 # => :ext2 470 | ``` 471 | 472 | --- 473 | 474 | ## Plugins 475 | 476 | - [thy-types](#plugin-thy-types) 477 | 478 | --- 479 | 480 | ## Plugin: thy-types 481 | 482 | Support for `Thy::Types` type system ([gem](https://github.com/akxcv/thy)) 483 | 484 | - install `thy` types (`gem install thy`): 485 | 486 | ```ruby 487 | gem 'thy' 488 | ``` 489 | 490 | ```shell 491 | bundle install 492 | ``` 493 | 494 | - enable `thy_types` plugin: 495 | 496 | ```ruby 497 | require 'thy' 498 | SmartCore::Initializer::Configuration.plugin(:thy_types) 499 | ``` 500 | 501 | - usage: 502 | 503 | ```ruby 504 | class User 505 | include SmartCore::Initializer(type_system: :thy_types) 506 | 507 | param :nickname, 'string' 508 | param :email, 'value.text', type_system: :smart_types # mixing with smart_types 509 | option :admin, Thy::Types::Boolean, default: false 510 | option :age, (Thy::Type.new { |value| value > 18 }) # custom thy type is supported too 511 | end 512 | 513 | # valid case: 514 | User.new('daiver', 'iamdaiver@gmail.com', { admin: true, age: 19 }) 515 | # => new user object 516 | 517 | # invalid case (invalid age) 518 | User.new('daiver', 'iamdaiver@gmail.com', { age: 17 }) 519 | # SmartCore::Initializer::ThyTypeValidationError 520 | 521 | # invaldi case (invalid nickname) 522 | User.new(123, 'test', { admin: true, age: 22 }) 523 | # => SmartCore::Initializer::ThyTypeValidationError 524 | ``` 525 | 526 | --- 527 | 528 | ## Roadmap 529 | 530 | - an ability to re-define existing options and parameters in children classes; 531 | - More semantic attribute declaration errors (more domain-related attribute error objects); 532 | - incorrect `:finalize` argument type: `ArgumentError` => `FinalizeArgumentError`; 533 | - incorrect `:as` argument type: `ArguemntError` => `AsArgumentError`; 534 | - etc; 535 | - Support for `RSpec` doubles and instance_doubles inside the type system integration; 536 | - Specs restructuring; 537 | - Migrate from `TravisCI` to `GitHub Actions`; 538 | - Extract `Type Interop` system to `smart_type-system`; 539 | - 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; 540 | 541 | --- 542 | 543 | ## Build 544 | 545 | ### Tests Running 546 | 547 | - with plugin tests: 548 | 549 | ```shell 550 | bin/rspec -w 551 | ``` 552 | 553 | - without plugin tests: 554 | 555 | ```shell 556 | bin/rspec -n 557 | ``` 558 | 559 | - help message: 560 | 561 | ```shell 562 | bin/rspec -h 563 | ``` 564 | 565 | ### Code Style Checking 566 | 567 | - without auto-correction: 568 | 569 | ```shell 570 | bundle exec rake rubocop 571 | ``` 572 | 573 | - with auto-correction: 574 | 575 | ```shell 576 | bundle exec rake rubocop -A 577 | ``` 578 | 579 | --- 580 | 581 | ## Contributing 582 | 583 | - Fork it ( https://github.com/smart-rb/smart_initializer ) 584 | - Create your feature branch (`git checkout -b feature/my-new-feature`) 585 | - Commit your changes (`git commit -am '[feature_context] Add some feature'`) 586 | - Push to the branch (`git push origin feature/my-new-feature`) 587 | - Create new Pull Request 588 | 589 | ## License 590 | 591 | Released under MIT License. 592 | 593 | ## Supporting 594 | 595 | 596 | Supported by Cado Labs 597 | 598 | 599 | ## Authors 600 | 601 | [Rustam Ibragimov](https://github.com/0exp) 602 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/gem_tasks' 4 | require 'rspec/core/rake_task' 5 | require 'rubocop' 6 | require 'rubocop/rake_task' 7 | require 'rubocop-performance' 8 | require 'rubocop-rspec' 9 | require 'rubocop-rake' 10 | 11 | RuboCop::RakeTask.new(:rubocop) do |t| 12 | config_path = File.expand_path(File.join('.rubocop.yml'), __dir__) 13 | t.options = ['--config', config_path] 14 | t.requires << 'rubocop-rspec' 15 | t.requires << 'rubocop-performance' 16 | t.requires << 'rubocop-rake' 17 | end 18 | 19 | RSpec::Core::RakeTask.new(:rspec) 20 | 21 | task default: :rspec 22 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'bundler/setup' 5 | require 'smart_core/initializer' 6 | require 'pry' 7 | 8 | Pry.start 9 | -------------------------------------------------------------------------------- /bin/rspec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'pathname' 5 | require 'optparse' 6 | 7 | module SmartCoreInitializerRSpecRunner 8 | expand_gemfile_path = lambda do |gemfile| 9 | File.expand_path(File.join('..', 'gemfiles', gemfile), __dir__) 10 | end 11 | 12 | GEMFILES = { 13 | with_external_deps: expand_gemfile_path.call('with_external_deps.gemfile'), 14 | without_external_deps: expand_gemfile_path.call('without_external_deps.gemfile') 15 | }.freeze 16 | 17 | class << self 18 | def run! 19 | OptionParser.new do |opts| 20 | opts.banner = 'Usage: bin/rspec [options]' 21 | 22 | opts.on('-w', '--with-plugins', 'Run general tests and plugin tests') do 23 | run_with_plugin_tests! 24 | end 25 | 26 | opts.on('-n', '--without-plugins', 'Run general tests (without plugin tests') do 27 | run_without_plugin_tests! 28 | end 29 | end.parse! 30 | end 31 | 32 | private 33 | 34 | def run_with_plugin_tests! 35 | ENV['TEST_PLUGINS'] = 'true' 36 | ENV['FULL_TEST_COVERAGE_CHECK'] = 'true' 37 | ENV['BUNDLE_GEMFILE'] = GEMFILES[:with_external_deps] 38 | run_tests! 39 | end 40 | 41 | def run_without_plugin_tests! 42 | ENV['BUNDLE_GEMFILE'] = GEMFILES[:without_external_deps] 43 | run_tests! 44 | end 45 | 46 | def run_tests! 47 | require 'rubygems' 48 | require 'bundler/setup' 49 | load Gem.bin_path('rspec-core', 'rspec') 50 | end 51 | end 52 | end 53 | 54 | SmartCoreInitializerRSpecRunner.run! 55 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /gemfiles/with_external_deps.gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | gem 'thy', '~> 0.1.4' 6 | 7 | gemspec path: '..' 8 | -------------------------------------------------------------------------------- /gemfiles/with_external_deps.gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: .. 3 | specs: 4 | smart_initializer (0.11.1) 5 | qonfig (~> 0.24) 6 | smart_engine (~> 0.16) 7 | smart_types (~> 0.8) 8 | 9 | GEM 10 | remote: https://rubygems.org/ 11 | specs: 12 | activesupport (7.0.4) 13 | concurrent-ruby (~> 1.0, >= 1.0.2) 14 | i18n (>= 1.6, < 2) 15 | minitest (>= 5.1) 16 | tzinfo (~> 2.0) 17 | armitage-rubocop (1.30.1.1) 18 | rubocop (= 1.30.1) 19 | rubocop-performance (= 1.14.2) 20 | rubocop-rails (= 2.15.0) 21 | rubocop-rake (= 0.6.0) 22 | rubocop-rspec (= 2.11.1) 23 | ast (2.4.2) 24 | coderay (1.1.3) 25 | concurrent-ruby (1.1.10) 26 | diff-lcs (1.5.0) 27 | docile (1.4.0) 28 | i18n (1.12.0) 29 | concurrent-ruby (~> 1.0) 30 | method_source (1.0.0) 31 | minitest (5.16.3) 32 | parallel (1.22.1) 33 | parser (3.1.2.1) 34 | ast (~> 2.4.1) 35 | pry (0.14.1) 36 | coderay (~> 1.1) 37 | method_source (~> 1.0) 38 | qonfig (0.28.0) 39 | rack (3.0.0) 40 | rainbow (3.1.1) 41 | rake (13.0.6) 42 | regexp_parser (2.6.0) 43 | rexml (3.2.5) 44 | rspec (3.11.0) 45 | rspec-core (~> 3.11.0) 46 | rspec-expectations (~> 3.11.0) 47 | rspec-mocks (~> 3.11.0) 48 | rspec-core (3.11.0) 49 | rspec-support (~> 3.11.0) 50 | rspec-expectations (3.11.1) 51 | diff-lcs (>= 1.2.0, < 2.0) 52 | rspec-support (~> 3.11.0) 53 | rspec-mocks (3.11.1) 54 | diff-lcs (>= 1.2.0, < 2.0) 55 | rspec-support (~> 3.11.0) 56 | rspec-support (3.11.1) 57 | rubocop (1.30.1) 58 | parallel (~> 1.10) 59 | parser (>= 3.1.0.0) 60 | rainbow (>= 2.2.2, < 4.0) 61 | regexp_parser (>= 1.8, < 3.0) 62 | rexml (>= 3.2.5, < 4.0) 63 | rubocop-ast (>= 1.18.0, < 2.0) 64 | ruby-progressbar (~> 1.7) 65 | unicode-display_width (>= 1.4.0, < 3.0) 66 | rubocop-ast (1.21.0) 67 | parser (>= 3.1.1.0) 68 | rubocop-performance (1.14.2) 69 | rubocop (>= 1.7.0, < 2.0) 70 | rubocop-ast (>= 0.4.0) 71 | rubocop-rails (2.15.0) 72 | activesupport (>= 4.2.0) 73 | rack (>= 1.1) 74 | rubocop (>= 1.7.0, < 2.0) 75 | rubocop-rake (0.6.0) 76 | rubocop (~> 1.0) 77 | rubocop-rspec (2.11.1) 78 | rubocop (~> 1.19) 79 | ruby-progressbar (1.11.0) 80 | simplecov (0.21.2) 81 | docile (~> 1.1) 82 | simplecov-html (~> 0.11) 83 | simplecov_json_formatter (~> 0.1) 84 | simplecov-html (0.12.3) 85 | simplecov_json_formatter (0.1.4) 86 | smart_engine (0.17.0) 87 | smart_types (0.8.0) 88 | smart_engine (~> 0.11) 89 | thy (0.1.4) 90 | tzinfo (2.0.5) 91 | concurrent-ruby (~> 1.0) 92 | unicode-display_width (2.3.0) 93 | 94 | PLATFORMS 95 | arm64-darwin-21 96 | 97 | DEPENDENCIES 98 | armitage-rubocop (~> 1.30) 99 | bundler (~> 2.3) 100 | pry (~> 0.14) 101 | rake (~> 13.0) 102 | rspec (~> 3.11) 103 | simplecov (~> 0.21) 104 | smart_initializer! 105 | thy (~> 0.1.4) 106 | 107 | BUNDLED WITH 108 | 2.3.17 109 | -------------------------------------------------------------------------------- /gemfiles/without_external_deps.gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | gemspec path: '..' 6 | -------------------------------------------------------------------------------- /gemfiles/without_external_deps.gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: .. 3 | specs: 4 | smart_initializer (0.11.0) 5 | qonfig (~> 0.24) 6 | smart_engine (~> 0.16) 7 | smart_types (~> 0.7) 8 | 9 | GEM 10 | remote: https://rubygems.org/ 11 | specs: 12 | activesupport (7.0.4) 13 | concurrent-ruby (~> 1.0, >= 1.0.2) 14 | i18n (>= 1.6, < 2) 15 | minitest (>= 5.1) 16 | tzinfo (~> 2.0) 17 | armitage-rubocop (1.30.1.1) 18 | rubocop (= 1.30.1) 19 | rubocop-performance (= 1.14.2) 20 | rubocop-rails (= 2.15.0) 21 | rubocop-rake (= 0.6.0) 22 | rubocop-rspec (= 2.11.1) 23 | ast (2.4.2) 24 | coderay (1.1.3) 25 | concurrent-ruby (1.1.10) 26 | diff-lcs (1.5.0) 27 | docile (1.4.0) 28 | i18n (1.12.0) 29 | concurrent-ruby (~> 1.0) 30 | method_source (1.0.0) 31 | minitest (5.16.3) 32 | parallel (1.22.1) 33 | parser (3.1.2.1) 34 | ast (~> 2.4.1) 35 | pry (0.14.1) 36 | coderay (~> 1.1) 37 | method_source (~> 1.0) 38 | qonfig (0.28.0) 39 | rack (3.0.0) 40 | rainbow (3.1.1) 41 | rake (13.0.6) 42 | regexp_parser (2.6.0) 43 | rexml (3.2.5) 44 | rspec (3.11.0) 45 | rspec-core (~> 3.11.0) 46 | rspec-expectations (~> 3.11.0) 47 | rspec-mocks (~> 3.11.0) 48 | rspec-core (3.11.0) 49 | rspec-support (~> 3.11.0) 50 | rspec-expectations (3.11.1) 51 | diff-lcs (>= 1.2.0, < 2.0) 52 | rspec-support (~> 3.11.0) 53 | rspec-mocks (3.11.1) 54 | diff-lcs (>= 1.2.0, < 2.0) 55 | rspec-support (~> 3.11.0) 56 | rspec-support (3.11.1) 57 | rubocop (1.30.1) 58 | parallel (~> 1.10) 59 | parser (>= 3.1.0.0) 60 | rainbow (>= 2.2.2, < 4.0) 61 | regexp_parser (>= 1.8, < 3.0) 62 | rexml (>= 3.2.5, < 4.0) 63 | rubocop-ast (>= 1.18.0, < 2.0) 64 | ruby-progressbar (~> 1.7) 65 | unicode-display_width (>= 1.4.0, < 3.0) 66 | rubocop-ast (1.21.0) 67 | parser (>= 3.1.1.0) 68 | rubocop-performance (1.14.2) 69 | rubocop (>= 1.7.0, < 2.0) 70 | rubocop-ast (>= 0.4.0) 71 | rubocop-rails (2.15.0) 72 | activesupport (>= 4.2.0) 73 | rack (>= 1.1) 74 | rubocop (>= 1.7.0, < 2.0) 75 | rubocop-rake (0.6.0) 76 | rubocop (~> 1.0) 77 | rubocop-rspec (2.11.1) 78 | rubocop (~> 1.19) 79 | ruby-progressbar (1.11.0) 80 | simplecov (0.21.2) 81 | docile (~> 1.1) 82 | simplecov-html (~> 0.11) 83 | simplecov_json_formatter (~> 0.1) 84 | simplecov-html (0.12.3) 85 | simplecov_json_formatter (0.1.4) 86 | smart_engine (0.17.0) 87 | smart_types (0.8.0) 88 | smart_engine (~> 0.11) 89 | tzinfo (2.0.5) 90 | concurrent-ruby (~> 1.0) 91 | unicode-display_width (2.3.0) 92 | 93 | PLATFORMS 94 | arm64-darwin-21 95 | 96 | DEPENDENCIES 97 | armitage-rubocop (~> 1.30) 98 | bundler (~> 2.3) 99 | pry (~> 0.14) 100 | rake (~> 13.0) 101 | rspec (~> 3.11) 102 | simplecov (~> 0.21) 103 | smart_initializer! 104 | 105 | BUNDLED WITH 106 | 2.3.17 107 | -------------------------------------------------------------------------------- /lib/smart_core/initializer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'smart_core' 4 | require 'smart_core/types' 5 | require 'forwardable' 6 | 7 | # @api public 8 | # @since 0.1.0 9 | module SmartCore 10 | # @api public 11 | # @since 0.1.0 12 | module Initializer 13 | require_relative 'initializer/version' 14 | require_relative 'initializer/errors' 15 | require_relative 'initializer/plugins' 16 | require_relative 'initializer/settings' 17 | require_relative 'initializer/configuration' 18 | require_relative 'initializer/type_system' 19 | require_relative 'initializer/attribute' 20 | require_relative 'initializer/extensions' 21 | require_relative 'initializer/constructor' 22 | require_relative 'initializer/dsl' 23 | require_relative 'initializer/instance_attribute_accessing' 24 | require_relative 'initializer/functionality' 25 | 26 | class << self 27 | # @param base_klass [Class] 28 | # @return [void] 29 | # 30 | # @api private 31 | # @since 0.1.0 32 | # @version 0.3.0 33 | def included(base_klass) 34 | ::SmartCore::Initializer::Functionality.seed_to(base_klass) 35 | end 36 | end 37 | 38 | # @return [void] 39 | # 40 | # @api private 41 | # @since 0.1.0 42 | def initialize(*); end 43 | end 44 | 45 | class << self 46 | # @option type_system [String, Symbol] 47 | # @option strict_options [Boolean] 48 | # @option auto_cast [Boolean] 49 | # @return [Module] 50 | # 51 | # @api public 52 | # @since 0.1.0 53 | # @version 0.8.0 54 | # rubocop:disable Naming/MethodName 55 | def Initializer( 56 | type_system: SmartCore::Initializer::Configuration[:default_type_system], 57 | strict_options: SmartCore::Initializer::Configuration[:strict_options], 58 | auto_cast: SmartCore::Initializer::Configuration[:auto_cast] 59 | ) 60 | SmartCore::Initializer::Functionality.includable_module( 61 | type_system: type_system, 62 | strict_options: strict_options, 63 | auto_cast: auto_cast 64 | ) 65 | end 66 | # rubocop:enable Naming/MethodName 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/attribute.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # @api private 4 | # @since 0.1.0 5 | # @version 0.8.0 6 | module SmartCore::Initializer::Attribute 7 | require_relative 'attribute/value' 8 | require_relative 'attribute/list' 9 | require_relative 'attribute/finalizer' 10 | require_relative 'attribute/factory' 11 | end 12 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/attribute/factory.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # @api private 4 | # @since 0.1.0 5 | # @version 0.8.0 6 | module SmartCore::Initializer::Attribute::Factory 7 | require_relative 'factory/base' 8 | require_relative 'factory/option' 9 | require_relative 'factory/param' 10 | end 11 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/attribute/factory/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # @api private 4 | # @since 0.8.0 5 | class SmartCore::Initializer::Attribute::Factory::Base 6 | class << self 7 | # @!method create(*params) 8 | # @param params [Any] List of appropriated attributes for the corresponding factory 9 | # @return [SmartCore::Initializer::Attribute::Base] 10 | 11 | private 12 | 13 | # @param name [String, Symbol] 14 | # @return [Symbol] 15 | # 16 | # @api private 17 | # @since 0.8.0 18 | def prepare_name_param(name) 19 | unless name.is_a?(::String) || name.is_a?(::Symbol) 20 | raise(SmartCore::Initializer::ArgumentError, <<~ERROR_MESSAGE) 21 | Attribute name should be a type of String or Symbol 22 | ERROR_MESSAGE 23 | end 24 | 25 | name.to_sym 26 | end 27 | 28 | # @param type [String, Symbol, Any] 29 | # @param type_system [Class] 30 | # @return [SmartCore::Initializer::TypeSystem::Interop] 31 | # 32 | # @api private 33 | # @since 0.8.0 34 | def prepare_type_param(type, type_system) 35 | type_primitive = 36 | if type.is_a?(::String) || type.is_a?(::Symbol) 37 | type_system.type_from_alias(type) 38 | else 39 | type 40 | end 41 | 42 | type_system.create(type_primitive) 43 | end 44 | 45 | # @param cast [Boolean] 46 | # @return [Boolean] 47 | # 48 | # @api private 49 | # @since 0.8.0 50 | def prepare_cast_param(cast) 51 | unless cast.is_a?(::TrueClass) || cast.is_a?(::FalseClass) 52 | raise(SmartCore::Initializer::ArgumentError, <<~ERROR_MESSAGE) 53 | Attribute cast should be a type of boolean 54 | ERROR_MESSAGE 55 | end 56 | 57 | cast 58 | end 59 | 60 | # @param privacy [String, Symbol] 61 | # @return [Symbol] 62 | # 63 | # @api private 64 | # @since 0.8.0 65 | def prepare_privacy_param(privacy) 66 | unless privacy.is_a?(::String) || privacy.is_a?(::Symbol) 67 | raise(SmartCore::Initializer::ArgumentError, <<~ERROR_MESSAGE) 68 | Attribute privacy should be a type of String or Symbol 69 | ERROR_MESSAGE 70 | end 71 | 72 | SmartCore::Initializer::Attribute::Value::Base::PRIVACY_MODES.fetch(privacy.to_sym) do 73 | raise(SmartCore::Initializer::ArgumentError, <<~ERROR_MESSAGE) 74 | Incorrect attribute privacy identifier "#{privacy}" 75 | ERROR_MESSAGE 76 | end 77 | end 78 | 79 | # @param finalize [String, Symbol, Proc] 80 | # @return [SmartCore::Initializer::Attribute::Finalizer::AnonymousBlock/InstanceMethod] 81 | # 82 | # @api private 83 | # @since 0.8.0 84 | # @version 0.9.1 85 | def prepare_finalize_param(finalize) 86 | unless finalize.is_a?(::String) || finalize.is_a?(::Symbol) || finalize.is_a?(::Proc) 87 | raise(SmartCore::Initializer::ArgumentError, <<~ERROR_MESSAGE) 88 | Attribute finalizer should be a type of String, Symbol or Proc 89 | ERROR_MESSAGE 90 | end 91 | 92 | # rubocop:disable Style/SoleNestedConditional 93 | if finalize.is_a?(::Proc) && finalize.lambda? 94 | unless allowed_lambda_arity.bsearch { |element| finalize.arity <=> element } 95 | raise( 96 | SmartCore::Initializer::ArgumentError, 97 | "Lambda-based finalizer should have arity " \ 98 | "equal to #{allowed_lambda_arity.join(', ')} " \ 99 | "(your lambda object should require one attribute)" 100 | ) # TODO: show the name of attribute in error message (if the name is a valid, of course) 101 | end 102 | end 103 | # rubocop:enable Style/SoleNestedConditional 104 | 105 | SmartCore::Initializer::Attribute::Finalizer.create(finalize) 106 | end 107 | 108 | # return [Array] 109 | # @api private 110 | # @since 0.9.1 111 | def allowed_lambda_arity 112 | @allowed_lambda_arity ||= [-2, -1, 1].freeze 113 | end 114 | 115 | # @param mutable [Boolean] 116 | # @return [Boolean] 117 | # 118 | # @api private 119 | # @since 0.4.0 120 | def prepare_mutable_param(mutable) 121 | unless mutable.is_a?(::FalseClass) || mutable.is_a?(::TrueClass) 122 | raise(SmartCore::Initializer::ArgumentError, <<~ERROR_MESSAGE) 123 | :mutable attribute should be a type of boolean 124 | ERROR_MESSAGE 125 | end 126 | 127 | mutable 128 | end 129 | 130 | # @param as [String, Symbol, NilClass] 131 | # @return [String, Symbol, NilClass] 132 | # 133 | # @api private 134 | # @since 0.4.0 135 | def prepare_as_param(as) 136 | unless as.is_a?(::NilClass) || as.is_a?(::String) || as.is_a?(::Symbol) 137 | raise(SmartCore::Initializer::ArgumentError, <<~ERROR_MESSAGE) 138 | Attribute alias should be a type of String or Symbol 139 | ERROR_MESSAGE 140 | end 141 | 142 | as 143 | end 144 | 145 | # @param type_system [String, Symbol] 146 | # @return [Class] 147 | # 148 | # @api private 149 | # @since 0.8.0 150 | def prepare_type_system_param(type_system) 151 | SmartCore::Initializer::TypeSystem.resolve(type_system) 152 | end 153 | end 154 | end 155 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/attribute/factory/option.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SmartCore::Initializer::Attribute::Factory 4 | # @api private 5 | # @since 0.8.0 6 | class Option < Base 7 | class << self 8 | # @param klass [Class] 9 | # @param name [String, Symbol] 10 | # @param type [String, Symbol, Any] 11 | # @param type_system [String, Symbol] 12 | # @param privacy [String, Symbol] 13 | # @param finalize [String, Symbol, Proc] 14 | # @param cast [Boolean] 15 | # @param mutable [Boolean] 16 | # @param as [String, Symbol, NilClass] 17 | # @param default [Proc, Any] 18 | # @param optional [Boolean] 19 | # @return [SmartCore::Initializer::Attribute::Value::Option] 20 | # 21 | # @api private 22 | # @since 0.8.0 23 | def create(klass, name, type, type_system, privacy, finalize, cast, mutable, as, default, optional) 24 | prepared_name = prepare_name_param(name) 25 | prepared_privacy = prepare_privacy_param(privacy) 26 | prepared_finalize = prepare_finalize_param(finalize) 27 | prepared_cast = prepare_cast_param(cast) 28 | prepared_type_system = prepare_type_system_param(type_system) 29 | prepared_type = prepare_type_param(type, prepared_type_system) 30 | prepared_mutable = prepare_mutable_param(mutable) 31 | prepared_as = prepare_as_param(as) 32 | prepared_default = prepare_default_param(default) 33 | prepared_optional = prepare_optional_param(optional) 34 | 35 | create_attribute( 36 | klass, 37 | prepared_name, 38 | prepared_type, 39 | prepared_type_system, 40 | prepared_privacy, 41 | prepared_finalize, 42 | prepared_cast, 43 | prepared_mutable, 44 | prepared_as, 45 | prepared_default, 46 | prepared_optional 47 | ) 48 | end 49 | 50 | private 51 | 52 | # @param klass [Class] 53 | # @param name [String] 54 | # @param type [SmartCore::Initializer::TypeSystem::Interop] 55 | # @param type_system [Class] 56 | # @param privacy [Symbol] 57 | # @param finalize [SmartCore::Initializer::Attribute::Finalizer::Abstract] 58 | # @param cast [Boolean] 59 | # @param mutable [Boolean] 60 | # @param as [String, Symbol] 61 | # @param default [Proc, Any] 62 | # @param optional [Boolean] 63 | # @return [SmartCore::Initializer::Attribute::Value::Option] 64 | # 65 | # @api private 66 | # @since 0.8.0 67 | def create_attribute( 68 | klass, 69 | name, 70 | type, 71 | type_system, 72 | privacy, 73 | finalize, 74 | cast, 75 | mutable, 76 | as, 77 | default, 78 | optional 79 | ) 80 | SmartCore::Initializer::Attribute::Value::Option.new( 81 | klass, name, type, type_system, privacy, finalize, cast, mutable, as, default, optional 82 | ) 83 | end 84 | 85 | # @param default [Proc, Any] 86 | # @return [Proc, Any] 87 | # 88 | # @api private 89 | # @since 0.8.0 90 | def prepare_default_param(default) 91 | # NOTE: this "strange" approach is used to comply the default Factory methods approach here 92 | default 93 | end 94 | 95 | # @param optional [Boolean] 96 | # @return [Boolean] 97 | # 98 | # @api private 99 | # @since 0.8.0 100 | def prepare_optional_param(optional) 101 | unless optional.is_a?(::TrueClass) || optional.is_a?(::FalseClass) 102 | raise(SmartCore::Initializer::ArgumentError, <<~ERROR_MESSAGE) 103 | :optional attribute should be a type of boolean 104 | ERROR_MESSAGE 105 | end 106 | 107 | optional 108 | end 109 | end 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/attribute/factory/param.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SmartCore::Initializer::Attribute::Factory 4 | # @api private 5 | # @since 0.8.0 6 | class Param < Base 7 | class << self 8 | # @param klass [Class] 9 | # @param name [String, Symbol] 10 | # @param type [String, Symbol, Any] 11 | # @param type_system [String, Symbol] 12 | # @param privacy [String, Symbol] 13 | # @param finalize [String, Symbol, Proc] 14 | # @param cast [Boolean] 15 | # @param mutable [Boolean] 16 | # @param as [String, Symbol, NilClass] 17 | # @return [SmartCore::Initializer::Attribute::Value::Param] 18 | # 19 | # @api private 20 | # @since 0.8.0 21 | def create(klass, name, type, type_system, privacy, finalize, cast, mutable, as) 22 | prepared_name = prepare_name_param(name) 23 | prepared_privacy = prepare_privacy_param(privacy) 24 | prepared_finalize = prepare_finalize_param(finalize) 25 | prepared_cast = prepare_cast_param(cast) 26 | prepared_type_system = prepare_type_system_param(type_system) 27 | prepared_type = prepare_type_param(type, prepared_type_system) 28 | prepared_mutable = prepare_mutable_param(mutable) 29 | prepared_as = prepare_as_param(as) 30 | 31 | create_attribute( 32 | klass, 33 | prepared_name, 34 | prepared_type, 35 | prepared_type_system, 36 | prepared_privacy, 37 | prepared_finalize, 38 | prepared_cast, 39 | prepared_mutable, 40 | prepared_as 41 | ) 42 | end 43 | 44 | private 45 | 46 | # @param name [String] 47 | # @param type [SmartCore::Initializer::TypeSystem::Interop] 48 | # @param type_system [Class] 49 | # @param privacy [Symbol] 50 | # @param finalize [SmartCore::Initializer::Attribute::Finalizer::Abstract] 51 | # @param cast [Boolean] 52 | # @param mutable [Boolean] 53 | # @param as [String, Symbol] 54 | # @return [SmartCore::Initializer::Attribute::Value::Param] 55 | # 56 | # @api private 57 | # @since 0.8.0 58 | def create_attribute(klass, name, type, type_system, privacy, finalize, cast, mutable, as) 59 | SmartCore::Initializer::Attribute::Value::Param.new( 60 | klass, name, type, type_system, privacy, finalize, cast, mutable, as 61 | ) 62 | end 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/attribute/finalizer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # @api private 4 | # @since 0.1.0 5 | module SmartCore::Initializer::Attribute::Finalizer 6 | require_relative 'finalizer/abstract' 7 | require_relative 'finalizer/instance_method' 8 | require_relative 'finalizer/anonymous_block' 9 | 10 | class << self 11 | # @param finalization_approach [String, Symbol, Proc] 12 | # @return [SmartCore::Initializer::Attribute::Finalizer::InstanceMethod] 13 | # @return [SmartCore::Initializer::Attribute::Finalizer::AnonymousBlock] 14 | # 15 | # @api private 16 | # @since 0.1.0 17 | # @version 0.8.0 18 | def create(finalization_approach) 19 | case finalization_approach 20 | when String, Symbol 21 | InstanceMethod.new(finalization_approach) 22 | when Proc 23 | AnonymousBlock.new(finalization_approach) 24 | else 25 | raise( 26 | SmartCore::Initializer::ArgumentError, 27 | 'Finalization approach object should be a type of Proc, Symbol or String' 28 | ) 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/attribute/finalizer/abstract.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # @api private 4 | # @since 0.1.0 5 | class SmartCore::Initializer::Attribute::Finalizer::Abstract 6 | # @param finalizer [Any] 7 | # @return [void] 8 | # 9 | # @api private 10 | # @since 0.1.0 11 | def initialize(finalizer) 12 | @finalizer = finalizer 13 | end 14 | 15 | # @param value [Any] 16 | # @param instance [Any] 17 | # @return [Any] 18 | # 19 | # @api private 20 | # @since 0.1.0 21 | def call(value, instance) 22 | # :nocov: 23 | raise NoMethodError 24 | # :nocov: 25 | end 26 | 27 | # @return [SmartCore::Initializer::Attribute::Finalizer::Abstract] 28 | def dup 29 | self.class.new(finalizer) 30 | end 31 | 32 | private 33 | 34 | # @return [Any] 35 | # 36 | # @api private 37 | # @since 0.1.0 38 | attr_reader :finalizer 39 | end 40 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/attribute/finalizer/anonymous_block.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SmartCore::Initializer::Attribute::Finalizer 4 | # @pai private 5 | # @since 0.1.0 6 | class AnonymousBlock < Abstract 7 | # @param finalizer [Proc] 8 | # @return [void] 9 | # 10 | # @api private 11 | # @since 0.1.0 12 | def initialize(finalizer) 13 | @finalizer = finalizer 14 | end 15 | 16 | # @param value [Any] 17 | # @param instance [Any] 18 | # @return [value] 19 | # 20 | # @pai private 21 | # @since 0.1.0 22 | def call(value, instance) 23 | instance.instance_exec(value, &finalizer) 24 | end 25 | 26 | private 27 | 28 | # @return [NilClass, Any] 29 | # 30 | # @api private 31 | # @since 0.1.0 32 | attr_reader :finalizer 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/attribute/finalizer/instance_method.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SmartCore::Initializer::Attribute::Finalizer 4 | # @pai private 5 | # @since 0.1.0 6 | class InstanceMethod < Abstract 7 | # @param finalizer [String, Symbol] 8 | # @return [void] 9 | # 10 | # @api private 11 | # @since 0.1.0 12 | def initialize(finalizer) 13 | @finalizer = finalizer 14 | end 15 | 16 | # @param value [Any] 17 | # @param instance [Any] 18 | # @return [value] 19 | # 20 | # @pai private 21 | # @since 0.1.0 22 | def call(value, instance) 23 | instance.__send__(finalizer, value) 24 | end 25 | 26 | private 27 | 28 | # @return [NilClass, Any] 29 | # 30 | # @api private 31 | # @since 0.1.0 32 | attr_reader :finalizer 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/attribute/list.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # @api private 4 | # @since 0.1.0 5 | # @version 0.10.0 6 | class SmartCore::Initializer::Attribute::List 7 | # @since 0.1.0 8 | include Enumerable 9 | 10 | # @return [void] 11 | # 12 | # @api private 13 | # @since 0.1.0 14 | # @version 0.10.0 15 | def initialize 16 | @attributes = {} 17 | @lock = SmartCore::Engine::ReadWriteLock.new 18 | end 19 | 20 | # @param attribute_name [Symbol] 21 | # @return [SmartCore::Initializer::Atribute] 22 | # 23 | # @raise [SmartCore::Initializer::UndefinedAttributeError] 24 | # 25 | # @api private 26 | # @since 0.8.0 27 | # @version 0.10.0 28 | def fetch(attribute_name) 29 | @lock.read_sync do 30 | raise( 31 | ::SmartCore::Initializer::UndefinedAttributeError, 32 | "Attribute with `#{attribute_name}` name is not defined in your constructor. " \ 33 | "Please, check your attribute definitions inside your class." 34 | ) unless attributes.key?(attribute_name) 35 | 36 | attributes[attribute_name] 37 | end 38 | end 39 | alias_method :[], :fetch 40 | 41 | # @param attribute [SmartCore::Initializer::Attribute] 42 | # @return [void] 43 | # 44 | # @api private 45 | # @since 0.1.0 46 | # @version 0.10.0 47 | def add(attribute) 48 | @lock.write_sync { attributes[attribute.name] = attribute } 49 | end 50 | alias_method :<<, :add 51 | 52 | # @param list [SmartCore::Initializer::Attribute::List] 53 | # @return [void] 54 | # 55 | # @api private 56 | # @since 0.1.0 57 | # @version 0.10.0 58 | def concat(list) 59 | @lock.write_sync do 60 | list.each { |attribute| add(attribute.dup) } 61 | end 62 | end 63 | 64 | # @param attribute [SmartCore::Initializer::Attribute] 65 | # @return [void] 66 | # 67 | # @api private 68 | # @since 0.1.0 69 | def include?(attribute) 70 | @lock.read_sync { attributes.key?(attribute.name) } 71 | end 72 | 73 | # @param block [Block] 74 | # @return [Enumerable] 75 | # 76 | # @api private 77 | # @since 0.1.0 78 | # @version 0.10.0 79 | def each(&block) 80 | @lock.read_sync do 81 | block_given? ? attributes.values.each(&block) : attributes.values.each 82 | end 83 | end 84 | 85 | # @return [Integer] 86 | # 87 | # @api private 88 | # @since 0.1.0 89 | # @version 0.10.0 90 | def size 91 | @lock.read_sync { attributes.size } 92 | end 93 | 94 | # @param block [Block] 95 | # @return [Integer] 96 | # 97 | # @api private 98 | # @since 0.1.0 99 | # @version 0.10.0 100 | def count(&block) 101 | @lock.read_sync { attributes.values.count(&block) } 102 | end 103 | 104 | private 105 | 106 | # @return [Hash] 107 | # 108 | # @api private 109 | # @since 0.1.0 110 | attr_reader :attributes 111 | end 112 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/attribute/value.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # @api private 4 | # @since 0.8.0 5 | module SmartCore::Initializer::Attribute::Value 6 | require_relative 'value/base' 7 | require_relative 'value/param' 8 | require_relative 'value/option' 9 | end 10 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/attribute/value/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # @api private 4 | # @since 0.8.0 5 | # @version 0.11.0 6 | class SmartCore::Initializer::Attribute::Value::Base 7 | # @return [Hash] 8 | # 9 | # @api private 10 | # @since 0.8.0 11 | PRIVACY_MODES = { 12 | public: :public, 13 | protected: :protected, 14 | private: :private 15 | }.freeze 16 | 17 | # @return [Symbol] 18 | # 19 | # @api private 20 | # @since 0.8.0 21 | DEFAULT_PRIVACY_MODE = PRIVACY_MODES[:public] 22 | 23 | # @return [Proc] 24 | # 25 | # @api private 26 | # @since 0.8.0 27 | DEFAULT_FINALIZER = proc { |value| value }.freeze 28 | 29 | # @return [NilClass] 30 | # 31 | # @api private 32 | # @since 0.8.0 33 | DEFAULT_AS = nil 34 | 35 | # @return [Boolean] 36 | # 37 | # @api private 38 | # @since 0.8.0 39 | DEFAULT_MUTABLE = false 40 | 41 | # @return [Class] 42 | # 43 | # @api private 44 | # @since 0.12.0 45 | attr_reader :klass 46 | 47 | # @return [Symbol] 48 | # 49 | # @api private 50 | # @since 0.8.0 51 | attr_reader :name 52 | 53 | # @return [SmartCore::Initializer::TypeSystem::Interop] 54 | # 55 | # @api private 56 | # @since 0.8.0 57 | attr_reader :type 58 | 59 | # @return [Class] 60 | # 61 | # @api private 62 | # @since 0.8.0 63 | attr_reader :type_system 64 | 65 | # @return [Symbol] 66 | # 67 | # @api private 68 | # @since 0.8.0 69 | attr_reader :privacy 70 | 71 | # @return [SmartCore::Initializer::Attribute::Finalizer::AnonymousBlock] 72 | # @return [SmartCore::Initializer::Attribute::Finalizer::InstanceMethod] 73 | # 74 | # @api private 75 | # @since 0.8.0 76 | attr_reader :finalizer 77 | 78 | # @return [Boolean] 79 | # 80 | # @api private 81 | # @since 0.8.0 82 | attr_reader :cast 83 | alias_method :cast?, :cast 84 | 85 | # @return [Boolean] 86 | # 87 | # @api private 88 | # @since 0.8.0 89 | attr_reader :mutable 90 | alias_method :mutable?, :mutable 91 | 92 | # @return [String, Symbol, NilClass] 93 | # 94 | # @api private 95 | # @since 0.8.0 96 | attr_reader :as 97 | 98 | # @param klass [Class] 99 | # @param name [Symbol] 100 | # @param type [SmartCore::Initializer::TypeSystem::Interop] 101 | # @param type_system [Class] 102 | # @param privacy [Symbol] 103 | # @param finalizer [SmartCore::Initializer::Attribute::AnonymousBlock/InstanceMethod] 104 | # @param cast [Boolean] 105 | # @param mutable [Boolean] 106 | # @param as [NilClass, Symbol, String] 107 | # @return [void] 108 | # 109 | # @api private 110 | # @since 0.8.0 111 | def initialize(klass, name, type, type_system, privacy, finalizer, cast, mutable, as) 112 | @klass = klass 113 | @name = name 114 | @type = type 115 | @type_system = type_system 116 | @privacy = privacy 117 | @finalizer = finalizer 118 | @cast = cast 119 | @mutable = mutable 120 | @as = as 121 | end 122 | 123 | # @param value [Any] 124 | # @return [void] 125 | # 126 | # @raise [SmartCore::Initializer::IncorrectTypeError] 127 | # 128 | # @api private 129 | # @since 0.8.0 130 | # @version 0.11.0 131 | def validate!(value) 132 | unless type.valid?(value) 133 | raise( 134 | SmartCore::Initializer::IncorrectTypeError, 135 | "Validation of attribute `#{klass}##{name}` failed. " \ 136 | "Expected: #{type.identifier}, got: #{truncate(value.inspect, 100)}", 137 | ) 138 | end 139 | end 140 | 141 | private 142 | 143 | def truncate(string, max_length) 144 | return string unless string.size > max_length 145 | omission = "..." 146 | "#{string[0, max_length - omission.size]}#{omission}" 147 | end 148 | end 149 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/attribute/value/option.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SmartCore::Initializer::Attribute::Value 4 | # @api private 5 | # @since 0.8.0 6 | class Option < Base 7 | # @return [Object] 8 | # 9 | # @api private 10 | # @since 0.8.0 11 | UNDEFINED_DEFAULT = ::Object.new.tap(&:freeze) 12 | 13 | # @return [Boolean] 14 | # 15 | # @api private 16 | # @since 0.8.0 17 | DEFAULT_OPTIONAL = false 18 | 19 | # @return [Boolean] 20 | # 21 | # @api private 22 | # @since 0.8.0 23 | attr_reader :optional 24 | alias_method :optional?, :optional 25 | 26 | # @param klass [Class] 27 | # @param name [Symbol] 28 | # @param type [SmartCore::Initializer::TypeSystem::Interop] 29 | # @param type_system [Class] 30 | # @param privacy [Symbol] 31 | # @param finalizer [SmartCore::Initializer::Attribute::AnonymousBlock/InstanceMethod] 32 | # @param cast [Boolean] 33 | # @param mutable [Boolean] 34 | # @param as [NilClass, Symbol, String] 35 | # @param default [Proc, Any] 36 | # @param optional [Boolean] 37 | # @return [void] 38 | # 39 | # @api private 40 | # @since 0.8.0 41 | def initialize( 42 | klass, 43 | name, 44 | type, 45 | type_system, 46 | privacy, 47 | finalizer, 48 | cast, 49 | mutable, 50 | as, 51 | default, 52 | optional 53 | ) 54 | super(klass, name, type, type_system, privacy, finalizer, cast, mutable, as) 55 | @default = default 56 | @optional = optional 57 | end 58 | 59 | # @return [Boolean] 60 | # 61 | # @api private 62 | # @since 0.8.0 63 | def has_default? 64 | !@default.equal?(UNDEFINED_DEFAULT) 65 | end 66 | 67 | # @return [Any] 68 | # 69 | # @raise [SmartCore::Initializer::NoDefaultValueError] 70 | # 71 | # @api private 72 | # @since 0.8.0 73 | def default 74 | raise( 75 | SmartCore::Initializer::NoDefaultValueError, 76 | "Attribute #{name} has no default value" 77 | ) if @default.equal?(UNDEFINED_DEFAULT) 78 | 79 | @default.is_a?(Proc) ? @default.call : @default.dup 80 | end 81 | 82 | # @return [SmartCore::Initializer::Attribute::Value::Option] 83 | # 84 | # @api private 85 | # @since 0.8.0 86 | def dup 87 | default = @default.equal?(UNDEFINED_DEFAULT) ? @default : @default.dup 88 | 89 | self.class.new( 90 | klass, 91 | name.dup, 92 | type, 93 | type_system, 94 | privacy, 95 | finalizer.dup, 96 | cast, 97 | mutable, 98 | as, 99 | default, 100 | optional 101 | ) 102 | end 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/attribute/value/param.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SmartCore::Initializer::Attribute::Value 4 | # @api private 5 | # @since 0.8.0 6 | class Param < Base 7 | # @return [SmartCore::Initializer::Attribute::Value::Param] 8 | # 9 | # @api private 10 | # @since 0.8.0 11 | def dup 12 | self.class.new( 13 | klass, 14 | name.dup, 15 | type, 16 | type_system, 17 | privacy, 18 | finalizer.dup, 19 | cast, 20 | mutable, 21 | as 22 | ) 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/configuration.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'qonfig' 4 | 5 | # @api public 6 | # @since 0.1.0 7 | module SmartCore::Initializer::Configuration 8 | # @since 0.1.0 9 | include Qonfig::Configurable 10 | 11 | # @api public 12 | # @since 0.1.0 13 | extend SmartCore::Initializer::Plugins::AccessMixin 14 | 15 | class << self 16 | # @param setting_key [String, Symbol] 17 | # @return [Qonfig::Settings, Any] 18 | # 19 | # @api private 20 | # @since 0.1.0 21 | def [](setting_key) 22 | config[setting_key] 23 | end 24 | end 25 | 26 | # @since 0.1.0 27 | configuration do 28 | # @since 0.?.0 29 | setting :default_type_system, :smart_types 30 | validate :default_type_system do |value| 31 | SmartCore::Initializer::TypeSystem.resolve(value) rescue false 32 | end 33 | 34 | # @since 0.?.0 35 | setting :strict_options, true 36 | validate :strict_options, :boolean 37 | 38 | # @since 0.8.0 39 | setting :auto_cast, false 40 | validate :auto_cast, :boolean 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/constructor.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # @api private 4 | # @since 0.1.0 5 | class SmartCore::Initializer::Constructor 6 | require_relative 'constructor/definer' 7 | 8 | # @param klass [Class] 9 | # @param arguments [Array] 10 | # @param block [Proc, NilClass] 11 | # @return [void] 12 | # 13 | # @api private 14 | # @since 0.1.0 15 | def initialize(klass, arguments, block) 16 | @klass = klass 17 | @arguments = arguments 18 | @parameters, @options = extract_attributes(arguments) 19 | @block = block 20 | end 21 | 22 | # @return [Any] 23 | # 24 | # @api private 25 | # @since 0.1.0 26 | def construct 27 | allocate_klass_instance.tap do |instance| 28 | prevent_attribute_insufficiency 29 | initialize_parameters(instance) 30 | initialize_options(instance) 31 | process_original_initializer(instance) 32 | process_init_extensions(instance) 33 | end 34 | end 35 | 36 | private 37 | 38 | # @return [Class] 39 | # 40 | # @api private 41 | # @since 0.1.0 42 | attr_reader :klass 43 | 44 | # @return [Array] 45 | # 46 | # @api private 47 | # @since 0.1.0 48 | attr_reader :arguments 49 | 50 | # @return [Array] 51 | # 52 | # @api private 53 | # @since 0.1.0 54 | attr_reader :parameters 55 | 56 | # @return [Hash] 57 | # 58 | # @api private 59 | # @since 0.1.0 60 | attr_reader :options 61 | 62 | # @return [Proc, NilClass] 63 | # 64 | # @api private 65 | # @since 0.1.0 66 | attr_reader :block 67 | 68 | # @return [void] 69 | # 70 | # @raise [SmartCore::Initializer::ParameterArgumentError] 71 | # @raise [SmartCore::Initializer::OptionArgumentError] 72 | # 73 | # @api private 74 | # @since 0.1.0 75 | # @version 0.8.0 76 | # rubocop:disable Metrics/AbcSize 77 | def prevent_attribute_insufficiency 78 | required_parameter_count = klass.__params__.size 79 | 80 | raise( 81 | SmartCore::Initializer::ParameterArgumentError, 82 | "Wrong number of parameters " \ 83 | "(given #{parameters.size}, expected #{required_parameter_count})" 84 | ) unless parameters.size == required_parameter_count 85 | 86 | required_options = klass.__options__.reject(&:has_default?).reject(&:optional?).map(&:name) 87 | missing_options = required_options.reject { |option_name| options.key?(option_name) } 88 | 89 | raise( 90 | SmartCore::Initializer::OptionArgumentError, 91 | "Missing options: #{missing_options.join(', ')}" 92 | ) if missing_options.any? 93 | 94 | possible_options = klass.__options__.map(&:name) 95 | unknown_options = options.keys - possible_options 96 | 97 | raise( 98 | SmartCore::Initializer::OptionArgumentError, 99 | "Unknown options: #{unknown_options.join(', ')}" 100 | ) if klass.__initializer_settings__.strict_options && unknown_options.any? 101 | end 102 | # rubocop:enable Metrics/AbcSize 103 | 104 | # @return [Any] 105 | # 106 | # @api private 107 | # @since 0.1.0 108 | def allocate_klass_instance 109 | klass.allocate 110 | end 111 | 112 | # @param instance [Any] 113 | # @return [void] 114 | # 115 | # @api private 116 | # @since 0.1.0 117 | # @version 0.5.1 118 | def initialize_parameters(instance) 119 | parameter_definitions = klass.__params__.zip(parameters).to_h 120 | 121 | parameter_definitions.each_pair do |attribute, parameter_value| 122 | if !attribute.type.valid?(parameter_value) && attribute.cast? 123 | parameter_value = attribute.type.cast(parameter_value) 124 | end 125 | 126 | attribute.validate!(parameter_value) 127 | final_value = attribute.finalizer.call(parameter_value, instance) 128 | attribute.validate!(final_value) 129 | 130 | instance.instance_variable_set("@#{attribute.name}", final_value) 131 | end 132 | end 133 | 134 | # @param instance [Any] 135 | # @return [void] 136 | # 137 | # @api private 138 | # @since 0.1.0 139 | # @version 0.8.0 140 | # rubocop:disable Metrics/AbcSize 141 | def initialize_options(instance) 142 | klass.__options__.each do |attribute| 143 | option_value = options.fetch(attribute.name) do 144 | # NOTE: `nil` case is a case when an option is `optional` 145 | attribute.has_default? ? attribute.default : nil 146 | end 147 | 148 | # NOTE: (if-block: what if `if` receives `false`?): 149 | # For other case passed `attribute` is optional and 150 | # should not be type-checked/type-casted/etc. 151 | # But optional attributes with defined `default` setting should be 152 | # type-checked and type-casted. 153 | if options.key?(attribute.name) || attribute.has_default? 154 | if !attribute.type.valid?(option_value) && attribute.cast? 155 | option_value = attribute.type.cast(option_value) 156 | end 157 | 158 | attribute.validate!(option_value) 159 | option_value = attribute.finalizer.call(option_value, instance) 160 | attribute.validate!(option_value) 161 | end 162 | 163 | instance.instance_variable_set("@#{attribute.name}", option_value) 164 | end 165 | end 166 | # rubocop:enable Metrics/AbcSize 167 | 168 | # @param instance [Any] 169 | # @return [void] 170 | # 171 | # @api private 172 | # @since 0.1.0 173 | def process_original_initializer(instance) 174 | instance.__send__(:initialize, *arguments, &block) 175 | end 176 | 177 | # @param instance [Any] 178 | # @return [void] 179 | # 180 | # @api private 181 | # @since 0.1.0 182 | def process_init_extensions(instance) 183 | klass.__init_extensions__.each do |extension| 184 | extension.call(instance) 185 | end 186 | end 187 | 188 | # @param arguments [Array] 189 | # @return [Array,Hash>] 190 | # 191 | # @api private 192 | # @since 0.1.0 193 | def extract_attributes(arguments) 194 | extracted_parameters = [] 195 | extracted_options = {} 196 | 197 | if ( 198 | klass.__options__.size >= 1 && 199 | arguments.last.is_a?(Hash) && 200 | arguments.last.keys.all? { |key| key.is_a?(Symbol) } 201 | ) 202 | extracted_parameters = arguments[0..-2] 203 | extracted_options = arguments.last 204 | else 205 | extracted_parameters = arguments 206 | end 207 | 208 | [extracted_parameters, extracted_options] 209 | end 210 | end 211 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/constructor/definer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # @api private 4 | # @since 0.1.0 5 | # @version 0.10.0 6 | # rubocop:disable Metrics/ClassLength 7 | class SmartCore::Initializer::Constructor::Definer 8 | # @param klass [Class] 9 | # @return [void] 10 | # 11 | # @api private 12 | # @since 0.1.0 13 | # @version 0.10.0 14 | def initialize(klass) 15 | @klass = klass 16 | @lock = SmartCore::Engine::ReadWriteLock.new 17 | end 18 | 19 | # @param block [Proc] 20 | # @return [void] 21 | # 22 | # @api private 23 | # @since 0.1.0 24 | # @version 0.10.0 25 | def define_init_extension(block) 26 | @lock.write_sync do 27 | add_init_extension(build_init_extension(block)) 28 | end 29 | end 30 | 31 | # @param name [String, Symbol] 32 | # @param type [String, Symbol, Any] 33 | # @param type_system [String, Symbol] 34 | # @param privacy [String, Symbol] 35 | # @param finalize [String, Symbol, Proc] 36 | # @param cast [Boolean] 37 | # @param mutable [Boolean] 38 | # @param as [String, Symbol, NilClass] 39 | # @return [void] 40 | # 41 | # @api private 42 | # @since 0.1.0 43 | # @version 0.10.0 44 | def define_parameter( 45 | name, 46 | type, 47 | type_system, 48 | privacy, 49 | finalize, 50 | cast, 51 | mutable, 52 | as 53 | ) 54 | @lock.write_sync do 55 | attribute = build_param_attribute( 56 | name, 57 | type, 58 | type_system, 59 | privacy, 60 | finalize, 61 | cast, 62 | mutable, 63 | as 64 | ) 65 | prevent_option_overlap(attribute) 66 | add_parameter(attribute) 67 | end 68 | end 69 | 70 | # @param names [Array] 71 | # @option mutable [Boolean] 72 | # @option privacy [String, Symbol] 73 | # @return [void] 74 | # 75 | # @api private 76 | # @since 0.1.0 77 | # @version 0.10.0 78 | def define_parameters(*names, mutable:, privacy:) 79 | @lock.write_sync do 80 | names.map do |name| 81 | build_param_attribute( 82 | name, 83 | klass.__initializer_settings__.generic_type_object, 84 | klass.__initializer_settings__.type_system, 85 | privacy, 86 | SmartCore::Initializer::Attribute::Value::Param::DEFAULT_FINALIZER, 87 | klass.__initializer_settings__.auto_cast, 88 | mutable, 89 | SmartCore::Initializer::Attribute::Value::Param::DEFAULT_AS 90 | ).tap do |attribute| 91 | prevent_option_overlap(attribute) 92 | end 93 | end.each do |attribute| 94 | add_parameter(attribute) 95 | end 96 | end 97 | end 98 | 99 | # @param name [String, Symbol] 100 | # @param type [String, Symbol, Any] 101 | # @param type_system [String, Symbol] 102 | # @param privacy [String, Symbol] 103 | # @param finalize [String, Symbol, Proc] 104 | # @param cast [Boolean] 105 | # @param mutable [Boolean] 106 | # @param as [String, Symbol, NilClass] 107 | # @param default [Proc, Any] 108 | # @param optional [Boolean] 109 | # @return [void] 110 | # 111 | # @api private 112 | # @since 0.1.0 113 | # @version 0.10.0 114 | def define_option( 115 | name, 116 | type, 117 | type_system, 118 | privacy, 119 | finalize, 120 | cast, 121 | mutable, 122 | as, 123 | default, 124 | optional 125 | ) 126 | @lock.write_sync do 127 | attribute = build_option_attribute( 128 | name, 129 | type, 130 | type_system, 131 | privacy, 132 | finalize, 133 | cast, 134 | mutable, 135 | as, 136 | default, 137 | optional 138 | ) 139 | prevent_parameter_overlap(attribute) 140 | add_option(attribute) 141 | end 142 | end 143 | 144 | # @param names [Array] 145 | # @option mutable [Boolean] 146 | # @option privacy [String, Symbol] 147 | # @return [void] 148 | # 149 | # @api private 150 | # @since 0.1.0 151 | # @version 0.10.0 152 | def define_options(*names, mutable:, privacy:) 153 | @lock.write_sync do 154 | names.map do |name| 155 | build_option_attribute( 156 | name, 157 | klass.__initializer_settings__.generic_type_object, 158 | klass.__initializer_settings__.type_system, 159 | privacy, 160 | SmartCore::Initializer::Attribute::Value::Option::DEFAULT_FINALIZER, 161 | klass.__initializer_settings__.auto_cast, 162 | mutable, 163 | SmartCore::Initializer::Attribute::Value::Option::DEFAULT_AS, 164 | SmartCore::Initializer::Attribute::Value::Option::UNDEFINED_DEFAULT, 165 | SmartCore::Initializer::Attribute::Value::Option::DEFAULT_OPTIONAL 166 | ).tap do |attribute| 167 | prevent_parameter_overlap(attribute) 168 | end 169 | end.each do |attribute| 170 | add_option(attribute) 171 | end 172 | end 173 | end 174 | 175 | private 176 | 177 | # @return [Class] 178 | # 179 | # @api private 180 | # @since 0.1.0 181 | attr_reader :klass 182 | 183 | # @param name [String, Symbol] 184 | # @param type [String, Symbol, Any] 185 | # @param type_system [String, Symbol] 186 | # @param privacy [String, Symbol] 187 | # @param finalize [String, Symbol, Proc] 188 | # @param cast [Boolean] 189 | # @param mutable [Boolean] 190 | # @param as [String, Symbol, NilClass] 191 | # @return [SmartCore::Initializer::Attribute::Value::Param] 192 | # 193 | # @api private 194 | # @since 0.1.0 195 | # @version 0.8.0 196 | def build_param_attribute( 197 | name, 198 | type, 199 | type_system, 200 | privacy, 201 | finalize, 202 | cast, 203 | mutable, 204 | as 205 | ) 206 | SmartCore::Initializer::Attribute::Factory::Param.create( 207 | klass, name, type, type_system, privacy, finalize, cast, mutable, as 208 | ) 209 | end 210 | 211 | # @param name [String, Symbol] 212 | # @param type [String, Symbol, Any] 213 | # @param type_system [String, Symbol] 214 | # @param privacy [String, Symbol] 215 | # @param finalize [String, Symbol, Proc] 216 | # @param cast [Boolean] 217 | # @param mutable [Boolean] 218 | # @param as [String, Symbol, NilClass] 219 | # @param default [Proc, Any] 220 | # @param optional [Boolean] 221 | # @return [SmartCore::Initializer::Attribute::Value::Option] 222 | # 223 | # @api private 224 | # @since 0.1.0 225 | # @version 0.8.0 226 | def build_option_attribute( 227 | name, 228 | type, 229 | type_system, 230 | privacy, 231 | finalize, 232 | cast, 233 | mutable, 234 | as, 235 | default, 236 | optional 237 | ) 238 | SmartCore::Initializer::Attribute::Factory::Option.create( 239 | klass, name, type, type_system, privacy, finalize, cast, mutable, as, default, optional 240 | ) 241 | end 242 | 243 | # @param block [Proc] 244 | # @return [SmartCore::Initializer::Extensions::ExtInit] 245 | # 246 | # @api private 247 | # @since 0.1.0 248 | def build_init_extension(block) 249 | SmartCore::Initializer::Extensions::ExtInit.new(block) 250 | end 251 | 252 | # @param parameter [SmartCore::Initializer::Attribute::Value::Param] 253 | # @return [void] 254 | # 255 | # @api private 256 | # @since 0.1.0 257 | # @version 0.8.0 258 | # rubocop:disable Metrics/AbcSize 259 | def add_parameter(parameter) 260 | klass.__params__ << parameter 261 | klass.__send__(:attr_reader, parameter.name) 262 | klass.__send__(parameter.privacy, parameter.name) 263 | 264 | if parameter.mutable? 265 | # NOTE: 266 | # code evaluation approach is used instead of `define_method` approach in order 267 | # to avoid the `clojure`-context binding inside the new method (this context can 268 | # access the current context or the current variable set and the way to avoid this by 269 | # ruby method is more diffcult to support and read insead of the real `code` evaluation) 270 | klass.class_eval(<<~METHOD_CODE, __FILE__, __LINE__ + 1) 271 | #{parameter.privacy} def #{parameter.name}=(new_value) 272 | self.class.__params__[:#{parameter.name}].validate!(new_value) 273 | @#{parameter.name} = new_value 274 | end 275 | METHOD_CODE 276 | end 277 | 278 | if parameter.as 279 | klass.__send__(:alias_method, parameter.as, parameter.name) 280 | klass.__send__(:alias_method, "#{parameter.as}=", "#{parameter.name}=") if parameter.mutable? 281 | 282 | klass.__send__(parameter.privacy, parameter.as) 283 | klass.__send__(parameter.privacy, "#{parameter.as}=") if parameter.mutable? 284 | end 285 | end 286 | # rubocop:enable Metrics/AbcSize 287 | 288 | # @param option [SmartCore::Initializer::Attribute::Value::Option] 289 | # @return [void] 290 | # 291 | # @api private 292 | # @since 0.1.0 293 | # @version 0.8.0 294 | # rubocop:disable Metrics/AbcSize 295 | def add_option(option) 296 | klass.__options__ << option 297 | klass.__send__(:attr_reader, option.name) 298 | klass.__send__(option.privacy, option.name) 299 | 300 | if option.mutable? 301 | # NOTE: 302 | # code evaluation approach is used instead of `define_method` approach in order 303 | # to avoid the `clojure`-context binding inside the new method (this context can 304 | # access the current context or the current variable set and the way to avoid this by 305 | # ruby method is more diffcult to support and read insead of the real `code` evaluation) 306 | klass.class_eval(<<~METHOD_CODE, __FILE__, __LINE__ + 1) 307 | #{option.privacy} def #{option.name}=(new_value) 308 | self.class.__options__[:#{option.name}].validate!(new_value) 309 | @#{option.name} = new_value 310 | end 311 | METHOD_CODE 312 | end 313 | 314 | if option.as 315 | klass.__send__(:alias_method, option.as, option.name) 316 | klass.__send__(:alias_method, "#{option.as}=", "#{option.name}=") if option.mutable? 317 | 318 | klass.__send__(option.privacy, option.as) 319 | klass.__send__(option.privacy, "#{option.as}=") if option.mutable? 320 | end 321 | end 322 | # rubocop:enable Metrics/AbcSize 323 | 324 | # @param extension [SmartCore::Initializer::Extensions::ExtInit] 325 | # @return [void] 326 | # 327 | # @api private 328 | # @since 0.1.0 329 | def add_init_extension(extension) 330 | klass.__init_extensions__ << extension 331 | end 332 | 333 | # @param parameter [SmartCore::Initializer::Attribute] 334 | # @return [void] 335 | # 336 | # @api private 337 | # @since 0.1.0 338 | def prevent_option_overlap(parameter) 339 | if klass.__options__.include?(parameter) 340 | raise(SmartCore::Initializer::OptionOverlapError, <<~ERROR_MESSAGE) 341 | You have already defined option with name :#{parameter.name} 342 | ERROR_MESSAGE 343 | end 344 | end 345 | 346 | # @param option [SmartCore::Initializer::Attribute] 347 | # @return [void] 348 | # 349 | # @api private 350 | # @since 0.1.0 351 | def prevent_parameter_overlap(option) 352 | if klass.__params__.include?(option) 353 | raise(SmartCore::Initializer::ParameterOverlapError, <<~ERROR_MESSAGE) 354 | You have already defined parameter with name :#{option.name} 355 | ERROR_MESSAGE 356 | end 357 | end 358 | end 359 | # rubocop:enable Metrics/ClassLength 360 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/dsl.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # @api private 4 | # @since 0.1.0 5 | # @version 0.10.0 6 | module SmartCore::Initializer::DSL 7 | require_relative 'dsl/inheritance' 8 | 9 | class << self 10 | # @param base_klass [Class] 11 | # @return [void] 12 | # 13 | # @api private 14 | # @since 0.1.0 15 | # @version 0.10.0 16 | def extended(base_klass) 17 | base_klass.instance_eval do 18 | instance_variable_set(:@__params__, SmartCore::Initializer::Attribute::List.new) 19 | instance_variable_set(:@__options__, SmartCore::Initializer::Attribute::List.new) 20 | instance_variable_set(:@__init_extensions__, SmartCore::Initializer::Extensions::List.new) 21 | instance_variable_set(:@__definer__, SmartCore::Initializer::Constructor::Definer.new(self)) 22 | instance_variable_set(:@__initializer_settings__, SmartCore::Initializer::Settings.new) 23 | end 24 | base_klass.extend(ClassMethods) 25 | base_klass.singleton_class.prepend(ClassInheritance) 26 | end 27 | end 28 | 29 | # @api private 30 | # @since 0.1.0 31 | # @version 0.10.0 32 | module ClassInheritance 33 | # @param child_klass [Class] 34 | # @return [void] 35 | # 36 | # @api private 37 | # @since 0.1.0 38 | # @version 0.10.0 39 | def inherited(child_klass) 40 | child_klass.instance_exec(__initializer_settings__) do |init_settings| 41 | instance_variable_set(:@__params__, SmartCore::Initializer::Attribute::List.new) 42 | instance_variable_set(:@__options__, SmartCore::Initializer::Attribute::List.new) 43 | instance_variable_set(:@__init_extensions__, SmartCore::Initializer::Extensions::List.new) 44 | instance_variable_set(:@__definer__, SmartCore::Initializer::Constructor::Definer.new(self)) 45 | instance_variable_set(:@__initializer_settings__, init_settings.dup) 46 | end 47 | child_klass.extend(ClassMethods) 48 | SmartCore::Initializer::DSL::Inheritance.inherit(base: self, child: child_klass) 49 | child_klass.singleton_class.prepend(ClassInheritance) 50 | super 51 | end 52 | end 53 | 54 | # @api private 55 | # @since 0.1.0 56 | module ClassMethods 57 | # @return [SmartCore::Initializer::Attribute::List] 58 | # 59 | # @api private 60 | # @since 0.1.0 61 | def __params__ 62 | @__params__ 63 | end 64 | 65 | # @return [SmartCore::Initializer::Attribute::List] 66 | # 67 | # @api private 68 | # @since 0.1.0 69 | def __options__ 70 | @__options__ 71 | end 72 | 73 | # @return [SmartCore::Initializer::Extentions::List] 74 | # 75 | # @api private 76 | # @since 0.1.0 77 | def __init_extensions__ 78 | @__init_extensions__ 79 | end 80 | 81 | # @return [SmartCore::Initializer::Constructor::Definer] 82 | # 83 | # @api private 84 | # @since 0.1.0 85 | def __definer__ 86 | @__definer__ 87 | end 88 | 89 | # @return [SmartCore::Initializer::Settings] 90 | # 91 | # @api private 92 | # @since 0.1.0 93 | def __initializer_settings__ 94 | @__initializer_settings__ 95 | end 96 | 97 | # @param arguments [Array] 98 | # @param block [Block] 99 | # @return [Any] 100 | # 101 | # @api public 102 | # @since 0.1.0 103 | def new(*arguments, &block) 104 | SmartCore::Initializer::Constructor.new(self, arguments, block).construct 105 | end 106 | 107 | # @param name [String, Symbol] 108 | # @param type [String, Symbol, Any] 109 | # @option cast [Boolean] 110 | # @option privacy [String, Symbol] 111 | # @option finalize [String, Symbol, Proc] 112 | # @option mutable [Boolean] 113 | # @option as [NilClass, String, Symbol] 114 | # @return [void] 115 | # 116 | # @api public 117 | # @since 0.1.0 118 | # @version 0.8.0 119 | def param( 120 | name, 121 | type = __initializer_settings__.generic_type_object, 122 | privacy: SmartCore::Initializer::Attribute::Value::Param::DEFAULT_PRIVACY_MODE, 123 | finalize: SmartCore::Initializer::Attribute::Value::Param::DEFAULT_FINALIZER, 124 | cast: __initializer_settings__.auto_cast, 125 | type_system: __initializer_settings__.type_system, 126 | mutable: SmartCore::Initializer::Attribute::Value::Param::DEFAULT_MUTABLE, 127 | as: SmartCore::Initializer::Attribute::Value::Param::DEFAULT_AS 128 | ) 129 | __definer__.define_parameter( 130 | name, type, type_system, privacy, finalize, cast, mutable, as 131 | ) 132 | end 133 | 134 | # @param names [Array] 135 | # @option mutable [Boolean] 136 | # @option privacy [String, Symbol] 137 | # @return [void] 138 | # 139 | # @api public 140 | # @since 0.1.0 141 | # @verison 0.8.0 142 | def params( 143 | *names, 144 | mutable: SmartCore::Initializer::Attribute::Value::Param::DEFAULT_MUTABLE, 145 | privacy: SmartCore::Initializer::Attribute::Value::Param::DEFAULT_PRIVACY_MODE 146 | ) 147 | __definer__.define_parameters(*names, mutable: mutable, privacy: privacy) 148 | end 149 | 150 | # @param name [String, Symbol] 151 | # @param type [String, Symbol, Any] 152 | # @option cast [Boolean] 153 | # @option privacy [String, Symbol] 154 | # @option finalize [String, Symbol, Proc] 155 | # @option type_system [String, Symbol] 156 | # @option mutable [Boolean] 157 | # @option as [NilClass, String, Symbol] 158 | # @option default [Proc, Any] 159 | # @option optional [Boolean] 160 | # @return [void] 161 | # 162 | # @api public 163 | # @since 0.1.0 164 | # @version 0.8.0 165 | def option( 166 | name, 167 | type = __initializer_settings__.generic_type_object, 168 | privacy: SmartCore::Initializer::Attribute::Value::Option::DEFAULT_PRIVACY_MODE, 169 | finalize: SmartCore::Initializer::Attribute::Value::Option::DEFAULT_FINALIZER, 170 | cast: __initializer_settings__.auto_cast, 171 | type_system: __initializer_settings__.type_system, 172 | mutable: SmartCore::Initializer::Attribute::Value::Option::DEFAULT_MUTABLE, 173 | as: SmartCore::Initializer::Attribute::Value::Option::DEFAULT_AS, 174 | default: SmartCore::Initializer::Attribute::Value::Option::UNDEFINED_DEFAULT, 175 | optional: SmartCore::Initializer::Attribute::Value::Option::DEFAULT_OPTIONAL 176 | ) 177 | __definer__.define_option( 178 | name, type, type_system, privacy, finalize, cast, mutable, as, default, optional 179 | ) 180 | end 181 | 182 | # @param names [Array] 183 | # @option mutable [Boolean] 184 | # @option privacy [String, Symbol] 185 | # @return [void] 186 | # 187 | # @api public 188 | # @since 0.1.0 189 | # @version 0.8.0 190 | def options( 191 | *names, 192 | mutable: SmartCore::Initializer::Attribute::Value::Option::DEFAULT_MUTABLE, 193 | privacy: SmartCore::Initializer::Attribute::Value::Option::DEFAULT_PRIVACY_MODE 194 | ) 195 | __definer__.define_options(*names, mutable: mutable, privacy: privacy) 196 | end 197 | 198 | # @param block [Block] 199 | # @return [void] 200 | # 201 | # @api public 202 | # @since 0.1.0 203 | def ext_init(&block) 204 | __definer__.define_init_extension(block) 205 | end 206 | alias_method :extend_initialization_flow, :ext_init 207 | end 208 | end 209 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/dsl/inheritance.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # @api private 4 | # @since 0.1.0 5 | module SmartCore::Initializer::DSL::Inheritance 6 | class << self 7 | # @param base [Class] 8 | # @param child [Class] 9 | # @return [void] 10 | # 11 | # @api private 12 | # @since 0.1.0 13 | def inherit(base:, child:) 14 | child.__params__.concat(base.__params__) 15 | child.__options__.concat(base.__options__) 16 | child.__init_extensions__.concat(base.__init_extensions__) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/errors.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SmartCore::Initializer 4 | # @api public 5 | # @since 0.1.0 6 | Error = Class.new(SmartCore::Error) 7 | 8 | # @api public 9 | # @since 0.1.0 10 | ArgumentError = Class.new(SmartCore::ArgumentError) 11 | 12 | # @api public 13 | # @since 0.8.0 14 | AttributeError = Class.new(Error) 15 | 16 | # @api public 17 | # @since 0.8.0 18 | UndefinedAttributeError = Class.new(AttributeError) 19 | 20 | # @api public 21 | # @since 0.1.0 22 | ParameterArgumentError = Class.new(ArgumentError) 23 | 24 | # @api public 25 | # @since 0.1.0 26 | OptionArgumentError = Class.new(ArgumentError) 27 | 28 | # @api public 29 | # @since 0.8.0 30 | AliasArgumentError = Class.new(ArgumentError) 31 | 32 | # @api public 33 | # @since 0.8.0 34 | SettingArgumentError = Class.new(ArgumentError) 35 | 36 | # @api public 37 | # @since 0.1.0 38 | NoDefaultValueError = Class.new(Error) 39 | 40 | # @api public 41 | # @since 0.1.0 42 | OptionOverlapError = Class.new(ArgumentError) 43 | 44 | # @api public 45 | # @since 0.1.0 46 | ParameterOverlapError = Class.new(ArgumentError) 47 | 48 | # @api public 49 | # @since 0.1.0 50 | NoTypeAliasError = Class.new(Error) 51 | 52 | # @api public 53 | # @since 0.1.0 54 | PluginError = Class.new(Error) 55 | 56 | # @api public 57 | # @since 0.1.0 58 | UnresolvedPluginDependencyError = Class.new(PluginError) 59 | 60 | # @api public 61 | # @since 0.1.0 62 | AlreadyRegisteredPluginError = Class.new(PluginError) 63 | 64 | # @api public 65 | # @since 0.1.0 66 | UnregisteredPluginError = Class.new(PluginError) 67 | 68 | # @api public 69 | # @since 0.1.0 70 | TypeSystemError = Class.new(Error) 71 | 72 | # @api public 73 | # @since 0.5.1 74 | IncorrectTypeError = Class.new(TypeSystemError) 75 | 76 | # @api public 77 | # @since 0.1.0 78 | TypeAliasNotFoundError = Class.new(TypeSystemError) 79 | 80 | # @api public 81 | # @since 0.1.0 82 | IncorrectTypeSystemInteropError = Class.new(TypeSystemError) 83 | 84 | # @api public 85 | # @since 0.1.0 86 | IncorrectTypeObjectError = Class.new(TypeSystemError) 87 | 88 | # @api public 89 | # @since 0.1.0 90 | UnsupportedTypeSystemError = Class.new(TypeSystemError) 91 | 92 | # @api public 93 | # @since 0.1.0 94 | UnsupportedTypeOperationError = Class.new(TypeSystemError) 95 | 96 | # @api public 97 | # @since 0.1.0 98 | TypeCastingUnsupportedError = Class.new(UnsupportedTypeOperationError) 99 | end 100 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/extensions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # @api private 4 | # @since 0.1.0 5 | module SmartCore::Initializer::Extensions 6 | require_relative 'extensions/abstract' 7 | require_relative 'extensions/ext_init' 8 | require_relative 'extensions/list' 9 | end 10 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/extensions/abstract.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # @api private 4 | # @since 0.1.0 5 | class SmartCore::Initializer::Extensions::Abstract 6 | # @!method dup 7 | # @return [SmartCore::Initializer::Extensions::Abstract] 8 | end 9 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/extensions/ext_init.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # @api private 4 | # @since 0.1.0 5 | class SmartCore::Initializer::Extensions::ExtInit < SmartCore::Initializer::Extensions::Abstract 6 | # @param extender [Proc] 7 | # @return [void] 8 | # 9 | # @api private 10 | # @since 0.1.0 11 | def initialize(extender) 12 | @extender = extender 13 | end 14 | 15 | # @param instance [Any] 16 | # @return [void] 17 | # 18 | # @api private 19 | # @since 0.1.0 20 | def call(instance) 21 | extender.call(instance) 22 | end 23 | 24 | private 25 | 26 | # @return [Proc] 27 | # 28 | # @api private 29 | # @since0 0.1.0 30 | attr_reader :extender 31 | end 32 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/extensions/list.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # @api private 4 | # @since 0.1.0 5 | # @version 0.10.0 6 | class SmartCore::Initializer::Extensions::List 7 | # @since 0.1.0 8 | include Enumerable 9 | 10 | # @return [void] 11 | # 12 | # @api private 13 | # @since 0.1.0 14 | # @version 0.10.0 15 | def initialize 16 | @extensions = [] 17 | @lock = SmartCore::Engine::ReadWriteLock.new 18 | end 19 | 20 | # @param extension [SmartCore::Initializer::Extensions::Abstract] 21 | # @return [void] 22 | # 23 | # @api private 24 | # @since 0.1.0 25 | # @version 0.10.0 26 | def add(extension) 27 | @lock.write_sync { extensions << extension } 28 | end 29 | alias_method :<<, :add 30 | 31 | # @param list [SmartCore::Initializer::Extensions::List] 32 | # @return [void] 33 | # 34 | # @api private 35 | # @since 0.1.0 36 | # @version 0.10.0 37 | def concat(list) 38 | @lock.write_sync do 39 | list.each { |extension| add(extension.dup) } 40 | end 41 | end 42 | 43 | # @param block [Block] 44 | # @return [Enumerable] 45 | # 46 | # @api private 47 | # @since 0.1.0 48 | # @version 0.10.0 49 | def each(&block) 50 | @lock.read_sync do 51 | block_given? ? extensions.each(&block) : extensions.each 52 | end 53 | end 54 | 55 | # @return [Integer] 56 | # 57 | # @api private 58 | # @since 0.1.0 59 | # @version 0.10.0 60 | def size 61 | @lock.read_sync { extensions.size } 62 | end 63 | 64 | private 65 | 66 | # @return [Array] 67 | # 68 | # @api private 69 | # @since 0.1.0 70 | attr_reader :extensions 71 | end 72 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/functionality.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # @api private 4 | # @since 0.3.0 5 | module SmartCore::Initializer::Functionality 6 | class << self 7 | # @option type_system [String, Symbol] 8 | # @option strict_option [Boolean] 9 | # @option auto_cast [Boolean] 10 | # @return [Module] 11 | # 12 | # @api private 13 | # @since 0.3.0 14 | # @version 0.8.0 15 | def includable_module(type_system:, strict_options:, auto_cast:) 16 | Module.new.tap do |extension| 17 | extension.singleton_class.define_method(:included) do |base_klass| 18 | base_klass.include(::SmartCore::Initializer) 19 | base_klass.__initializer_settings__.type_system = type_system 20 | base_klass.__initializer_settings__.strict_options = strict_options 21 | base_klass.__initializer_settings__.auto_cast = auto_cast 22 | end 23 | end 24 | end 25 | 26 | # @param base_klass [Class] 27 | # @return [void] 28 | # 29 | # @api private 30 | # @since 0.3.0 31 | def seed_to(base_klass) 32 | base_klass.extend(SmartCore::Initializer::DSL) 33 | base_klass.include(SmartCore::Initializer::InstanceAttributeAccessing) 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/instance_attribute_accessing.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # @api private 4 | # @since 0.3.0 5 | module SmartCore::Initializer::InstanceAttributeAccessing 6 | # @return [Hash] 7 | # 8 | # @api public 9 | # @since 0.3.0 10 | def __params__ 11 | __collect_params__ 12 | end 13 | 14 | # @return [Hash] 15 | # 16 | # @api public 17 | # @since 0.3.0 18 | def __options__ 19 | __collect_options__ 20 | end 21 | 22 | # @return [Hash] 23 | # 24 | # @api public 25 | # @since 0.3.0 26 | def __attributes__ 27 | __collect_params__.merge(__collect_options__) 28 | end 29 | 30 | private 31 | 32 | # @return [Hash] 33 | # 34 | # @api private 35 | # @since 0.3.0 36 | def __collect_params__ 37 | self.class.__params__.each_with_object({}) do |param, memo| 38 | memo[param.name] = instance_variable_get("@#{param.name}") 39 | end 40 | end 41 | 42 | # @return [Hash] 43 | # 44 | # @api private 45 | # @since 0.3.0 46 | def __collect_options__ 47 | self.class.__options__.each_with_object({}) do |option, memo| 48 | memo[option.name] = instance_variable_get("@#{option.name}") 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/plugins.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # @api private 4 | # @since 0.1.0 5 | module SmartCore::Initializer::Plugins 6 | require_relative 'plugins/abstract' 7 | require_relative 'plugins/registry' 8 | require_relative 'plugins/registry_interface' 9 | require_relative 'plugins/access_mixin' 10 | require_relative 'plugins/thy_types' 11 | 12 | # @since 0.1.0 13 | extend SmartCore::Initializer::Plugins::RegistryInterface 14 | 15 | # @since 0.1.0 16 | register_plugin('thy_types', SmartCore::Initializer::Plugins::ThyTypes) 17 | end 18 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/plugins/abstract.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # @api private 4 | # @since 0.1.0 5 | # @version 0.10.0 6 | class SmartCore::Initializer::Plugins::Abstract 7 | class << self 8 | # @param child_klass [Class] 9 | # @return [void] 10 | # 11 | # @api private 12 | # @since 0.1.0 13 | # @version 0.10.0 14 | def inherited(child_klass) 15 | child_klass.instance_variable_set(:@__loaded__, false) 16 | child_klass.instance_variable_set(:@__lock__, SmartCore::Engine::Lock.new) 17 | super 18 | end 19 | 20 | # @return [void] 21 | # 22 | # @api private 23 | # @since 0.1.0 24 | def load! 25 | __thread_safe__ do 26 | unless @__loaded__ 27 | @__loaded__ = true 28 | install! 29 | end 30 | end 31 | end 32 | 33 | # @return [Boolean] 34 | # 35 | # @api private 36 | # @since 0.1.0 37 | def loaded? 38 | __thread_safe__ { @__loaded__ } 39 | end 40 | 41 | private 42 | 43 | # @return [void] 44 | # 45 | # @api private 46 | # @since 0.1.0 47 | def install!; end 48 | 49 | # @return [Any] 50 | # 51 | # @api private 52 | # @since 0.1.0 53 | def __thread_safe__(&block) 54 | @__lock__.synchronize(&block) 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/plugins/access_mixin.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # @api private 4 | # @since 0.1.0 5 | module SmartCore::Initializer::Plugins::AccessMixin 6 | # @param plugin_name [Symbol, String] 7 | # @return [void] 8 | # 9 | # @see SmartCore::Initializer::Plugins 10 | # 11 | # @api public 12 | # @since 0.1.0 13 | def plugin(plugin_name) 14 | SmartCore::Initializer::Plugins.load(plugin_name) 15 | end 16 | 17 | # @return [Array] 18 | # 19 | # @see SmartCore::Initializer::Plugins 20 | # 21 | # @api public 22 | # @since 0.1.0 23 | def plugins 24 | SmartCore::Initializer::Plugins.names 25 | end 26 | 27 | # @return [Hash>] 28 | # 29 | # @api private 30 | # @since 0.1.0 31 | def loaded_plugins 32 | SmartCore::Initializer::Plugins.loaded_plugins 33 | end 34 | alias_method :enabled_plugins, :loaded_plugins 35 | 36 | # @param plugin_name [String, Symbol] 37 | # @param plugin_klass [Class] 38 | # @return [void] 39 | # 40 | # @see SmartCore::Initializer::Plugins 41 | # 42 | # @api public 43 | # @since 0.1.0 44 | def register_plugin(plugin_name, plugin_klass) 45 | SmartCore::Initializer::Plugins.register_plugin(plugin_name, plugin_klass) 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/plugins/registry.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # @api private 4 | # @since 0.1.0 5 | # @version 0.10.0 6 | class SmartCore::Initializer::Plugins::Registry 7 | # @since 0.1.0 8 | include Enumerable 9 | 10 | # @return [void] 11 | # 12 | # @api private 13 | # @since 0.1.0 14 | # @version 0.10.0 15 | def initialize 16 | @plugin_set = {} 17 | @access_lock = SmartCore::Engine::ReadWriteLock.new 18 | end 19 | 20 | # @param plugin_name [Symbol, String] 21 | # @return [SmartCore::Initializer::Plugins::Abstract] 22 | # 23 | # @api private 24 | # @since 0.1.0 25 | # @version 0.10.0 26 | def [](plugin_name) 27 | @access_lock.read_sync { fetch(plugin_name) } 28 | end 29 | 30 | # @param plugin_name [Symbol, String] 31 | # @param plugin_module [SmartCore::Initializer::Plugins::Abstract] 32 | # @return [void] 33 | # 34 | # @api private 35 | # @since 0.1.0 36 | # @version 0.10.0 37 | def register(plugin_name, plugin_module) 38 | @access_lock.write_sync { apply(plugin_name, plugin_module) } 39 | end 40 | alias_method :[]=, :register 41 | 42 | # @return [Array] 43 | # 44 | # @api private 45 | # @since 0.1.0 46 | # @version 0.10.0 47 | def names 48 | @access_lock.read_sync { plugin_names } 49 | end 50 | 51 | # @return [Hash>] 52 | # 53 | # @api private 54 | # @since 0.1.0 55 | # @version 0.10.0 56 | def loaded 57 | @access_lock.read_sync { loaded_plugins } 58 | end 59 | 60 | # @param block [Block] 61 | # @return [Enumerable] 62 | # 63 | # @api private 64 | # @since 0.1.0 65 | # @version 0.10.0 66 | def each(&block) 67 | @access_lock.read_sync { iterate(&block) } 68 | end 69 | 70 | private 71 | 72 | # @return [Hash] 73 | # 74 | # @api private 75 | # @since 0.1.0 76 | attr_reader :plugin_set 77 | 78 | # @return [Mutex] 79 | # 80 | # @api private 81 | # @since 0.1.0 82 | attr_reader :access_lock 83 | 84 | # @return [Array] 85 | # 86 | # @api private 87 | # @since 0.1.0 88 | def plugin_names 89 | plugin_set.keys 90 | end 91 | 92 | # @param block [Block] 93 | # @return [Enumerable] 94 | # 95 | # @api private 96 | # @since 0.1.0 97 | def iterate(&block) 98 | block_given? ? plugin_set.each_pair(&block) : plugin_set.each_pair 99 | end 100 | 101 | # @param plugin_name [String] 102 | # @return [Boolean] 103 | # 104 | # @api private 105 | # @since 0.1.0 106 | def registered?(plugin_name) 107 | plugin_set.key?(plugin_name) 108 | end 109 | 110 | # @return [Array] 111 | # 112 | # @api private 113 | # @since 0.1.0 114 | def loaded_plugins 115 | plugin_set.select { |_plugin_name, plugin_module| plugin_module.loaded? } 116 | end 117 | 118 | # @param plugin_name [Symbol, String] 119 | # @param plugin_module [SmartCore::Initializer::Plugins::Abstract] 120 | # @return [void] 121 | # 122 | # @raise [SmartCore::Initializer::AlreadyRegisteredPluginError] 123 | # 124 | # @api private 125 | # @since 0.1.0 126 | def apply(plugin_name, plugin_module) 127 | plugin_name = indifferently_accessible_plugin_name(plugin_name) 128 | 129 | if registered?(plugin_name) 130 | raise(SmartCore::Initializer::AlreadyRegisteredPluginError, <<~ERROR_MESSAGE) 131 | #{plugin_name} plugin already exists 132 | ERROR_MESSAGE 133 | end 134 | 135 | plugin_set[plugin_name] = plugin_module 136 | end 137 | 138 | # @param plugin_name [Symbol, String] 139 | # @return [SmartCore::Initializer::Plugins::Abstract] 140 | # 141 | # @raise [SmartCore::Initializer::UnregisteredPluginError] 142 | # 143 | # @api private 144 | # @since 0.1.0 145 | def fetch(plugin_name) 146 | plugin_name = indifferently_accessible_plugin_name(plugin_name) 147 | 148 | unless registered?(plugin_name) 149 | raise(SmartCore::Initializer::UnregisteredPluginError, <<~ERROR_MESSAGE) 150 | #{plugin_name} plugin is not registered 151 | ERROR_MESSAGE 152 | end 153 | 154 | plugin_set[plugin_name] 155 | end 156 | 157 | # @param key [Symbol, String] 158 | # @return [String] 159 | # 160 | # @api private 161 | # @since 0.1.0 162 | def indifferently_accessible_plugin_name(plugin_name) 163 | plugin_name.to_s 164 | end 165 | end 166 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/plugins/registry_interface.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # @api private 4 | # @since 0.1.0 5 | # @version 0.10.0 6 | module SmartCore::Initializer::Plugins::RegistryInterface 7 | class << self 8 | # @param base_module [Class, Module] 9 | # @return [void] 10 | # 11 | # @api private 12 | # @since 0.1.0 13 | # @version 0.10.0 14 | def extended(base_module) 15 | base_module.instance_variable_set( 16 | :@plugin_registry, SmartCore::Initializer::Plugins::Registry.new 17 | ) 18 | base_module.instance_variable_set( 19 | :@access_lock, SmartCore::Engine::ReadWriteLock.new 20 | ) 21 | end 22 | end 23 | 24 | # @param plugin_name [Symbol, String] 25 | # @return [void] 26 | # 27 | # @api public 28 | # @since 0.1.0 29 | # @version 0.10.0 30 | def load(plugin_name) 31 | @access_lock.read_sync { plugin_registry[plugin_name].load! } 32 | end 33 | 34 | # @return [Array] 35 | # 36 | # @api public 37 | # @since 0.1.0 38 | # @version 0.10.0 39 | def loaded_plugins 40 | @access_lock.read_sync { plugin_registry.loaded.keys } 41 | end 42 | 43 | # @return [Array] 44 | # 45 | # @api public 46 | # @since 0.1.0 47 | # @version 0.10.0 48 | def names 49 | @access_lock.read_sync { plugin_registry.names } 50 | end 51 | 52 | # @param plugin_name [Symbol, String] 53 | # @return [void] 54 | # 55 | # @api private 56 | # @since 0.1.0 57 | # @version 0.10.0 58 | def register_plugin(plugin_name, plugin_module) 59 | @access_lock.write_sync { plugin_registry[plugin_name] = plugin_module } 60 | end 61 | 62 | private 63 | 64 | # @return [SmartCore::Initializer::Plugins::Registry] 65 | # 66 | # @api private 67 | # @since 0.1.0 68 | attr_reader :plugin_registry 69 | end 70 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/plugins/thy_types.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # @api private 4 | # @since 0.1.0 5 | class SmartCore::Initializer::Plugins::ThyTypes < SmartCore::Initializer::Plugins::Abstract 6 | class << self 7 | # @return [void] 8 | # 9 | # @api private 10 | # @since 0.1.0 11 | def install! 12 | raise( 13 | SmartCore::Initializer::UnresolvedPluginDependencyError, 14 | '::Thy does not exist or "thy" gem is not loaded' 15 | ) unless const_defined?('::Thy') 16 | 17 | # NOTE: require necessary dependencies 18 | require 'date' 19 | 20 | # NOTE: add thy-types type system implementation 21 | require_relative 'thy_types/errors' 22 | require_relative 'thy_types/thy_types' 23 | 24 | # NOTE: register thy-types type system 25 | SmartCore::Initializer::TypeSystem.register( 26 | :thy_types, SmartCore::Initializer::TypeSystem::ThyTypes 27 | ) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/plugins/thy_types/errors.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SmartCore::Initializer 4 | # @api public 5 | # @since 0.1.0 6 | ThyTypesError = Class.new(SmartCore::Initializer::Error) 7 | 8 | # @api public 9 | # @since 0.1.0 10 | ThyTypeValidationError = Class.new(ThyTypesError) 11 | end 12 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/plugins/thy_types/thy_types.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SmartCore::Initializer::TypeSystem 4 | # @api public 5 | # @since 0.1.0 6 | class ThyTypes < Interop 7 | require_relative 'thy_types/abstract_factory' 8 | require_relative 'thy_types/operation' 9 | 10 | type_alias(:any, AbstractFactory.generic_type_object) 11 | type_alias(:nil, ::Thy::Types::Nil) 12 | type_alias(:string, ::Thy::Types::String) 13 | type_alias(:symbol, ::Thy::Types::Symbol) 14 | type_alias(:integer, ::Thy::Types::Integer) 15 | type_alias(:float, ::Thy::Types::Float) 16 | type_alias(:numeric, ::Thy::Types::Numeric) 17 | type_alias(:boolean, ::Thy::Types::Boolean) 18 | type_alias(:time, ::Thy::Types::Time) 19 | type_alias(:date_time, ::Thy::Types::DateTime) 20 | type_alias(:untyped_array, ::Thy::Types::UntypedArray) 21 | type_alias(:untyped_hash, ::Thy::Types::UntypedHash) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/plugins/thy_types/thy_types/abstract_factory.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SmartCore::Initializer::TypeSystem 4 | # @api private 5 | # @since 0.1.0 6 | class ThyTypes::AbstractFactory < Interop::AbstractFactory 7 | # @return [Thy::Type] 8 | # 9 | # @api private 10 | # @since 0.1.0 11 | GENERIC_TYPE = ::Thy::Type.new { true } 12 | 13 | class << self 14 | # @param type [Thy::Type, #check] 15 | # @return [void] 16 | # 17 | # @raise [SmartCore::Initializer::IncorrectTypeObjectError] 18 | # 19 | # @api private 20 | # @since 0.1.0 21 | def prevent_incompatible_type!(type) 22 | unless type.respond_to?(:check) || type.is_a?(::Thy::Type) 23 | raise( 24 | SmartCore::Initializer::IncorrectTypeObjectError, 25 | 'Incorrect Thy::Type primitive ' \ 26 | '(type object should respond to :check method)' 27 | ) 28 | end 29 | end 30 | 31 | # @param type [Any] 32 | # @return [String] 33 | # 34 | # @api private 35 | # @since 0.5.1 36 | def build_identifier(type) 37 | type.name 38 | end 39 | 40 | # @param type [Thy::Type, #check] 41 | # @return [SmartCore::Initializer::TypeSystem::ThyTypes::Operation::Valid] 42 | # 43 | # @api private 44 | # @since 0.1.0 45 | def build_valid_operation(type) 46 | ThyTypes::Operation::Valid.new(type) 47 | end 48 | 49 | # @return [Thy::Type, #check] 50 | # 51 | # @api private 52 | # @since 0.1.0 53 | def generic_type_object 54 | GENERIC_TYPE 55 | end 56 | 57 | # @param type [Thy::Type, #check] 58 | # @return [SmartCore::Initializer::TypeSystem::ThyTypes::Operation::Validate] 59 | # 60 | # @api private 61 | # @since 0.1.0 62 | def build_validate_operation(type) 63 | ThyTypes::Operation::Validate.new(type) 64 | end 65 | 66 | # @param type [Thy::Type, #check] 67 | # @return [SmartCore::Initializer::TypeSystem::ThyTypes::Operation::Cast] 68 | # 69 | # @api private 70 | # @since 0.1.0 71 | def build_cast_operation(type) 72 | ThyTypes::Operation::Cast.new(type) 73 | end 74 | 75 | # @param identifier [String] 76 | # @param valid_op [SmartCore::Initializer::TypeSystem::ThyTypes::Operation::Valid] 77 | # @param valid_op [SmartCore::Initializer::TypeSystem::ThyTypes::Operation::Validate] 78 | # @param valid_op [SmartCore::Initializer::TypeSystem::ThyTypes::Operation::Cast] 79 | # @return [SmartCore::Initializer::TypeSystem::ThyTypes] 80 | # 81 | # @api private 82 | # @since 0.1.0 83 | # @version 0.5.1 84 | def build_interop(identifier, valid_op, validate_op, cast_op) 85 | ThyTypes.new(identifier, valid_op, validate_op, cast_op) 86 | end 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/plugins/thy_types/thy_types/operation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SmartCore::Initializer::TypeSystem 4 | # @api private 5 | # @since 0.1.0 6 | module ThyTypes::Operation 7 | require_relative 'operation/base' 8 | require_relative 'operation/valid' 9 | require_relative 'operation/validate' 10 | require_relative 'operation/cast' 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/plugins/thy_types/thy_types/operation/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SmartCore::Initializer::TypeSystem 4 | # @abstract 5 | # @api private 6 | # @since 0.1.0 7 | class ThyTypes::Operation::Base < Interop::Operation 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/plugins/thy_types/thy_types/operation/cast.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SmartCore::Initializer::TypeSystem::ThyTypes::Operation 4 | # @api private 5 | # @since 0.1.0 6 | class Cast < Base 7 | # @param value [Any] 8 | # @return [void] 9 | # 10 | # @raise [SmartCore::Initializer::TypeCastingUnsupportedError] 11 | # 12 | # @api private 13 | # @since 0.1.0 14 | def call(value) 15 | raise( 16 | SmartCore::Initializer::TypeCastingUnsupportedError, 17 | 'ThyTypes type system has no support for type casting.' 18 | ) 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/plugins/thy_types/thy_types/operation/valid.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SmartCore::Initializer::TypeSystem::ThyTypes::Operation 4 | # @api private 5 | # @since 0.1.0 6 | class Valid < Base 7 | # @param value [Any] 8 | # @return [Boolean] 9 | # 10 | # @api private 11 | # @since 0.1.0 12 | def call(value) 13 | type.check(value).success? 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/plugins/thy_types/thy_types/operation/validate.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SmartCore::Initializer::TypeSystem::ThyTypes::Operation 4 | # @api private 5 | # @since 0.1.0 6 | class Validate < Base 7 | # @param value [Any] 8 | # @return [void] 9 | # 10 | # @api private 11 | # @since 0.1.0 12 | def call(value) 13 | raise( 14 | SmartCore::Initializer::ThyTypeValidationError, 15 | "Thy::Types validation error: (get #{value.inspect} for type #{type.inspect}" 16 | ) unless type.check(value).success? 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/settings.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # @api private 4 | # @since 0.1.0 5 | class SmartCore::Initializer::Settings 6 | require_relative 'settings/base' 7 | require_relative 'settings/type_system' 8 | require_relative 'settings/strict_options' 9 | require_relative 'settings/auto_cast' 10 | require_relative 'settings/duplicator' 11 | 12 | # @return [void] 13 | # 14 | # @api private 15 | # @since 0.1.0 16 | # @version 0.8.0 17 | def initialize 18 | @type_system = TypeSystem.new 19 | @strict_options = StrictOptions.new 20 | @auto_cast = AutoCast.new 21 | end 22 | 23 | # @return [Any] 24 | # 25 | # @api private 26 | # @since 0.1.0 27 | def generic_type_object 28 | @type_system.generic_type_object 29 | end 30 | 31 | # @return [Symbol] 32 | # 33 | # @api private 34 | # @since 0.1.0 35 | def type_system 36 | @type_system.resolve 37 | end 38 | 39 | # @return [Boolean] 40 | # 41 | # @api private 42 | # @since 0.8.0 43 | def strict_options 44 | @strict_options.resolve 45 | end 46 | 47 | # @return [Boolean] 48 | # 49 | # @api private 50 | # @since 0.8.0 51 | def auto_cast 52 | @auto_cast.resolve 53 | end 54 | 55 | # @param value [String, System] 56 | # @return [void] 57 | # 58 | # @api private 59 | # @since 0.1.0 60 | def type_system=(value) 61 | @type_system.assign(value) 62 | end 63 | 64 | # @param value [Boolean] 65 | # @return [void] 66 | # 67 | # @api private 68 | # @since 0.8.0 69 | def strict_options=(value) 70 | @strict_options.assign(value) 71 | end 72 | 73 | # @param value [Boolean] 74 | # @return [void] 75 | # 76 | # @api private 77 | # @since 0.8.0 78 | def auto_cast=(value) 79 | @auto_cast.assign(value) 80 | end 81 | 82 | # @return [SmartCore::Initializer::Settings] 83 | # 84 | # @api private 85 | # @since 0.1.0 86 | def dup 87 | Duplicator.duplicate(self) 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/settings/auto_cast.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # @api private 4 | # @since 0.8.0 5 | # @version 0.10.0 6 | class SmartCore::Initializer::Settings::AutoCast < SmartCore::Initializer::Settings::Base 7 | # @return [Boolean] 8 | # 9 | # @api private 10 | # @since 0.8.0 11 | # @version 0.10.0 12 | def resolve 13 | @lock.read_sync do 14 | (@value == nil) ? SmartCore::Initializer::Configuration[:auto_cast] : @value 15 | end 16 | end 17 | 18 | # @param value [Boolean] 19 | # @return [void] 20 | # 21 | # @api private 22 | # @since 0.8.0 23 | # @version 0.10.0 24 | def assign(value) 25 | @lock.write_sync do 26 | raise( 27 | SmartCore::Initializer::SettingArgumentError, 28 | ":auto_cast setting should be a type of boolean (got: `#{value.class}`)" 29 | ) unless value.is_a?(::TrueClass) || value.is_a?(::FalseClass) 30 | 31 | @value = value 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/settings/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # @api private 4 | # @since 0.8.0 5 | # @version 0.10.0 6 | class SmartCore::Initializer::Settings::Base 7 | # @return [void] 8 | # 9 | # @api private 10 | # @since 0.8.0 11 | # @version 0.10.0 12 | def initialize 13 | @value = nil 14 | @lock = SmartCore::Engine::ReadWriteLock.new 15 | end 16 | 17 | # @!method resolve 18 | # @return [Any] 19 | # @api private 20 | # @since 0.8.0 21 | 22 | # @!method assign(value) 23 | # @param value [Any] 24 | # @return [void] 25 | # @raise [SmartCore::Initializer::SettingArgumentError] 26 | # @api private 27 | # @since 0.8.0 28 | 29 | # @return [SmartCore::Initializer::Settings::Base] 30 | # 31 | # @api private 32 | # @since 0.8.0 33 | # @version 0.10.0 34 | def dup 35 | @lock.write_sync do 36 | self.class.new.tap do |duplicate| 37 | duplicate.instance_variable_set(:@value, @value) 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/settings/duplicator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # @api private 4 | # @since 0.1.0 5 | module SmartCore::Initializer::Settings::Duplicator 6 | class << self 7 | # @param settings [SmartCore::Initializer::Settings] 8 | # @return [SmartCore::Initializer::Settings] 9 | # 10 | # @api private 11 | # @since 0.1.0 12 | # @version 0.8.0 13 | def duplicate(settings) 14 | SmartCore::Initializer::Settings.new.tap do |new_instance| 15 | type_system = settings.instance_variable_get(:@type_system).dup 16 | strict_options = settings.instance_variable_get(:@strict_options).dup 17 | auto_cast = settings.instance_variable_get(:@auto_cast).dup 18 | 19 | new_instance.instance_variable_set(:@type_system, type_system) 20 | new_instance.instance_variable_set(:@strict_options, strict_options) 21 | new_instance.instance_variable_set(:@auto_cast, auto_cast) 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/settings/strict_options.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # @api private 4 | # @since 0.8.0 5 | # @version 0.10.0 6 | class SmartCore::Initializer::Settings::StrictOptions < SmartCore::Initializer::Settings::Base 7 | # @return [Boolean] 8 | # 9 | # @api private 10 | # @since 0.8.0 11 | # @version 0.10.0 12 | def resolve 13 | @lock.read_sync do 14 | (@value == nil) ? SmartCore::Initializer::Configuration[:strict_options] : @value 15 | end 16 | end 17 | 18 | # @param value [Boolean] 19 | # @return [void] 20 | # 21 | # @api private 22 | # @since 0.8.0 23 | # @version 0.10.0 24 | def assign(value) 25 | @lock.write_sync do 26 | raise( 27 | SmartCore::Initializer::SettingArgumentError, 28 | ":strict_options setting should be a type of boolean (got: `#{value.class}`)" 29 | ) unless value.is_a?(::TrueClass) || value.is_a?(::FalseClass) 30 | 31 | @value = value 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/settings/type_system.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # @api private 4 | # @since 0.1.0 5 | # @version 0.10.0 6 | class SmartCore::Initializer::Settings::TypeSystem < SmartCore::Initializer::Settings::Base 7 | # @return [Any] 8 | # 9 | # @api private 10 | # @since 0.1.0 11 | # @version 0.10.0 12 | def generic_type_object 13 | @lock.read_sync do 14 | SmartCore::Initializer::TypeSystem.resolve(resolve).generic_type_object 15 | end 16 | end 17 | 18 | # @return [Symbol] 19 | # 20 | # @api private 21 | # @since 0.1.0 22 | # @version 0.10.0 23 | def resolve 24 | @lock.read_sync do 25 | (@value == nil) ? SmartCore::Initializer::Configuration[:default_type_system] : @value 26 | end 27 | end 28 | 29 | # @param value [String, Symbol] 30 | # @return [void] 31 | # 32 | # @api private 33 | # @since 0.1.0 34 | # @version 0.10.0 35 | def assign(value) 36 | @lock.write_sync do 37 | # NOTE: type system existence validation 38 | SmartCore::Initializer::TypeSystem.resolve(value) 39 | @value = value 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/type_system.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # @api public 4 | # @since 0.1.0 5 | module SmartCore::Initializer::TypeSystem 6 | require_relative 'type_system/interop' 7 | require_relative 'type_system/registry' 8 | require_relative 'type_system/smart_types' 9 | require_relative 'type_system/registry_interface' 10 | 11 | # @since 0.1.0 12 | extend SmartCore::Initializer::TypeSystem::RegistryInterface 13 | 14 | # @since 0.1.0 15 | register(:smart_types, SmartCore::Initializer::TypeSystem::SmartTypes) 16 | end 17 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/type_system/interop.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # @abstract 4 | # @api private 5 | # @since 0.1.0 6 | class SmartCore::Initializer::TypeSystem::Interop 7 | require_relative 'interop/operation' 8 | require_relative 'interop/abstract_factory' 9 | require_relative 'interop/aliasing' 10 | 11 | # @since 0.1.0 12 | include SmartCore::Initializer::TypeSystem::Interop::Aliasing 13 | 14 | class << self 15 | # @param type_object [Any] 16 | # @return [SmartCore::Initializer::TypeSystem::Interop] 17 | # 18 | # @api private 19 | # @since 0.1.0 20 | def create(type_object) 21 | self::AbstractFactory.create(type_object) 22 | end 23 | 24 | # @return [SmartCore::Initialiezr::TypeSystem::Interop] 25 | # 26 | # @api private 27 | # @since 0.1.0 28 | def generic_type_object 29 | self::AbstractFactory.generic_type_object 30 | end 31 | 32 | # @param type_object [Any] 33 | # @return [void] 34 | # 35 | # @raise [SmartCore::Initializer::IncorrectTypeObjectError] 36 | # 37 | # @api private 38 | # @since 0.1.0 39 | def prevent_incompatible_type!(type_object) 40 | self::AbstractFactory.prevent_incompatible_type!(type_object) 41 | end 42 | end 43 | 44 | # @return [String] 45 | # 46 | # @api private 47 | # @since 0.5.1 48 | attr_reader :identifier 49 | 50 | # @param identifier [String] 51 | # @param valid_op [SmartCore::Initializer::TypeSystem::Interop::Operation] 52 | # @param validate_op [SmartCore::Initializer::TypeSystem::Interop::Operation] 53 | # @param cast_op [SmartCore::Initializer::TypeSystem::Interop::Operation] 54 | # @return [void] 55 | # 56 | # @api private 57 | # @since 0.1.0 58 | # @version 0.5.1 59 | def initialize(identifier, valid_op, validate_op, cast_op) 60 | @identifier = identifier 61 | @valid_op = valid_op 62 | @validate_op = validate_op 63 | @cast_op = cast_op 64 | end 65 | 66 | # @param value [Any] 67 | # @return [Boolean] 68 | # 69 | # @api private 70 | # @since 0.1.0 71 | def valid?(value) 72 | valid_op.call(value) 73 | end 74 | 75 | # @param value [Any] 76 | # @return [void] 77 | # 78 | # @api private 79 | # @since 0.1.0 80 | def validate!(value) 81 | validate_op.call(value) 82 | end 83 | 84 | # @param value [Any] 85 | # @return [Any] 86 | # 87 | # @api private 88 | # @since 0.1.0 89 | def cast(value) 90 | cast_op.call(value) 91 | end 92 | 93 | private 94 | 95 | # @return [SmartCore::Initializer::TypeSystem::Interop::Operation] 96 | # 97 | # @api private 98 | # @since 0.1.0 99 | attr_reader :valid_op 100 | 101 | # @return [SmartCore::Initializer::TypeSystem::Interop::Operation] 102 | # 103 | # @api private 104 | # @since 0.1.0 105 | attr_reader :validate_op 106 | 107 | # @return [SmartCore::Initializer::TypeSystem::Interop::Operation] 108 | # 109 | # @api private 110 | # @since 0.1.0 111 | attr_reader :cast_op 112 | end 113 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/type_system/interop/abstract_factory.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # @abstract 4 | # @api private 5 | # @since 0.1.0 6 | class SmartCore::Initializer::TypeSystem::Interop::AbstractFactory 7 | class << self 8 | # @param type [Any] 9 | # @return [SmartCore::Initializer::TypeSystem::Interop] 10 | # 11 | # @api private 12 | # @since 0.1.0 13 | def create(type) 14 | prevent_incompatible_type!(type) 15 | 16 | identifier = build_identifier(type) 17 | valid_op = build_valid_operation(type) 18 | validate_op = build_validate_operation(type) 19 | cast_op = build_cast_operation(type) 20 | 21 | build_interop(identifier, valid_op, validate_op, cast_op) 22 | end 23 | 24 | # @return [Any] 25 | # 26 | # @api private 27 | # @since 0.1.0 28 | def generic_type_object; end 29 | 30 | # @param type [Any] 31 | # @return [String] 32 | # 33 | # @api private 34 | # @since 0.5.1 35 | def build_identifier(type); end 36 | 37 | # @param type [Any] 38 | # @return [void] 39 | # 40 | # @raise [SmartCore::Initializer::IncorrectTypeObjectError] 41 | # 42 | # @api private 43 | # @since 0.1.0 44 | def prevent_incompatible_type!(type); end 45 | 46 | private 47 | 48 | # @param type [Any] 49 | # @return [SmartCore::Initializer::TypeSystem::Interop::Operation] 50 | # 51 | # @api private 52 | # @since 0.1.0 53 | def build_valid_operation(type); end 54 | 55 | # @param type [Any] 56 | # @return [SmartCore::Initializer::TypeSystem::Interop::Operation] 57 | # 58 | # @api private 59 | # @since 0.1.0 60 | def build_validate_operation(type); end 61 | 62 | # @param type [Any] 63 | # @return [SmartCore::Initializer::TypeSystem::Interop::Operation] 64 | # 65 | # @api private 66 | # @since 0.1.0 67 | def build_cast_operation(type); end 68 | 69 | # @param identifier [String] 70 | # @param valid_op [SmartCore::Initializer::TypeSystem::Interop::Operation] 71 | # @param validate_op [SmartCore::Initializer::TypeSystem::Interop::Operation] 72 | # @param cast_op [SmartCore::Initializer::TypeSystem::Interop::Operation] 73 | # @return [SmartCore::Initializer::TypeSystem::Interop] 74 | # 75 | # @api private 76 | # @since 0.1.0 77 | # @version 0.5.1 78 | def build_interop(identifier, valid_op, validate_op, cast_op); end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/type_system/interop/aliasing.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # @api private 4 | # @since 0.1.0 5 | module SmartCore::Initializer::TypeSystem::Interop::Aliasing 6 | require_relative 'aliasing/alias_list' 7 | 8 | class << self 9 | # @param base_klass [Class] 10 | # @return [void] 11 | # 12 | # @api private 13 | # @since 0.1.0 14 | def included(base_klass) 15 | base_klass.extend(ClassMethods) 16 | base_klass.singleton_class.prepend(ClassInheritance) 17 | end 18 | end 19 | 20 | # @api private 21 | # @since 0.1.0 22 | module ClassInheritance 23 | # @param child_klass [Class] 24 | # @return [void] 25 | # 26 | # @api private 27 | # @since 0.1.0 28 | def inherited(child_klass) 29 | child_klass.instance_variable_set(:@__type_aliases__, AliasList.new(child_klass)) 30 | super 31 | end 32 | end 33 | 34 | # @api private 35 | # @since 0.1.0 36 | module ClassMethods 37 | # @return [SmartCore::Initializer::TypeSystem::Interop::Aliasing::AliasList] 38 | # 39 | # @api private 40 | # @since 0.1.0 41 | def __type_aliases__ 42 | @__type_aliases__ 43 | end 44 | 45 | # @return [Array] 46 | # 47 | # @api public 48 | # @since 0.1.0 49 | def type_aliases 50 | __type_aliases__.keys 51 | end 52 | 53 | # @param alias_name [String, Symbol] 54 | # @param type [Any] 55 | # @return [void] 56 | # 57 | # @api public 58 | # @since 0.1.0 59 | def type_alias(alias_name, type) 60 | __type_aliases__.associate(alias_name, type) 61 | end 62 | 63 | # @param alias_name [String, Symbol] 64 | # @return [Any] 65 | # 66 | # @api public 67 | # @since 0.1.0 68 | def type_from_alias(alias_name) 69 | __type_aliases__.resolve(alias_name) 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/type_system/interop/aliasing/alias_list.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # @api private 4 | # @since 0.1.0 5 | # @version 0.10.0 6 | class SmartCore::Initializer::TypeSystem::Interop::Aliasing::AliasList 7 | # @param interop_klass [Class] 8 | # @return [void] 9 | # 10 | # @api private 11 | # @since 0.1.0 12 | # @version 0.10.0 13 | def initialize(interop_klass) 14 | @interop_klass = interop_klass 15 | @list = {} 16 | @lock = SmartCore::Engine::ReadWriteLock.new 17 | end 18 | 19 | # @return [Array] 20 | # 21 | # @api private 22 | # @since 0.1.0 23 | # @version 0.10.0 24 | def keys 25 | @lock.read_sync { registered_aliases } 26 | end 27 | 28 | # @return [Hash] 29 | # 30 | # @api private 31 | # @since 0.1.0 32 | # @version 0.10.0 33 | def to_h 34 | @lock.read_sync { transform_to_hash } 35 | end 36 | alias_method :to_hash, :to_h 37 | 38 | # @param alias_name [String, Symbol] 39 | # @param type [Any] 40 | # @return [void] 41 | # 42 | # @api private 43 | # @since 0.1.0 44 | # @version 0.10.0 45 | def associate(alias_name, type) 46 | interop_klass.prevent_incompatible_type!(type) 47 | @lock.write_sync { set_alias(alias_name, type) } 48 | end 49 | 50 | # @param alias_name [String, Symbol] 51 | # @return [Any] 52 | # 53 | # @api private 54 | # @since 0.1.0 55 | # @version 0.10.0 56 | def resolve(alias_name) 57 | @lock.read_sync { get_alias(alias_name) } 58 | end 59 | 60 | private 61 | 62 | # @return [Hash] 63 | # 64 | # @api private 65 | # @since 0.1.0 66 | attr_reader :list 67 | 68 | # @return [Class] 69 | # 70 | # @api private 71 | # @since 0.1.0 72 | attr_reader :interop_klass 73 | 74 | # @param alias_name [String, Symbol] 75 | # @param type [Any] 76 | # @return [void] 77 | # 78 | # @api private 79 | # @since 0.1.0 80 | def set_alias(alias_name, type) 81 | alias_name = normalized_alias(alias_name) 82 | 83 | if list.key?(alias_name) 84 | ::Warning.warn( 85 | "[#{interop_klass.name}] Shadowing of the already existing \"#{alias_name}\" type alias." 86 | ) 87 | end 88 | 89 | list[alias_name] = type 90 | end 91 | 92 | # @param alias_name [String, Symbol] 93 | # @return [Any] 94 | # 95 | # @api private 96 | # @since 0.1.0 97 | # @version 0.10.0 98 | def get_alias(alias_name) 99 | alias_name = normalized_alias(alias_name) 100 | 101 | raise( 102 | SmartCore::Initializer::TypeAliasNotFoundError, 103 | "Alias with name `#{alias_name}` not found." 104 | ) unless list.key?(alias_name) 105 | 106 | list.fetch(alias_name) 107 | end 108 | 109 | # @param alias_name [String, Symbol] 110 | # @return [String] 111 | # 112 | # @api private 113 | # @since 0.1.0 114 | def normalized_alias(alias_name) 115 | raise( 116 | SmartCore::Initializer::ArgumentError, 117 | 'Type alias should be a type of string or symbol' 118 | ) unless alias_name.is_a?(String) || alias_name.is_a?(Symbol) 119 | 120 | alias_name.to_s 121 | end 122 | 123 | # @return [Hash] 124 | # 125 | # @api private 126 | # @since 0.1.0 127 | def transform_to_hash 128 | list.to_h 129 | end 130 | 131 | # @return [Array] 132 | # 133 | # @api private 134 | # @since 0.1.0 135 | def registered_aliases 136 | list.keys 137 | end 138 | end 139 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/type_system/interop/operation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # @abstract 4 | # @api private 5 | # @since 0.1.0 6 | class SmartCore::Initializer::TypeSystem::Interop::Operation 7 | # @param type [Any] 8 | # @return [void] 9 | # 10 | # @api private 11 | # @since 0.1.0 12 | def initialize(type) 13 | @type = type 14 | end 15 | 16 | # @param value [Any] 17 | # @return [Any] 18 | # 19 | # @api private 20 | # @since 0.1.0 21 | def call(value); end 22 | 23 | private 24 | 25 | # @return [Any] 26 | # 27 | # @api private 28 | # @since 0.1.0 29 | attr_reader :type 30 | end 31 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/type_system/registry.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # @api private 4 | # @since 0.1.0 5 | # @version 0.10.0 6 | class SmartCore::Initializer::TypeSystem::Registry 7 | # @since 0.1.0 8 | include Enumerable 9 | 10 | # @return [void] 11 | # 12 | # @api private 13 | # @since 0.1.0 14 | # @version 0.10.0 15 | def initialize 16 | @systems = {} 17 | @lock = SmartCore::Engine::ReadWriteLock.new 18 | end 19 | 20 | # @param system_identifier [String, Symbol] 21 | # @param interop_klass [Class] 22 | # @return [void] 23 | # 24 | # @api private 25 | # @since 0.1.0 26 | # @version 0.10.0 27 | def register(system_identifier, interop_klass) 28 | @lock.write_sync { apply(system_identifier, interop_klass) } 29 | end 30 | 31 | # @param system_identifier [String, Symbol] 32 | # @return [Class] 33 | # 34 | # @api private 35 | # @since 0.1.0 36 | # @version 0.10.0 37 | def resolve(system_identifier) 38 | @lock.read_sync { fetch(system_identifier) } 39 | end 40 | 41 | # @return [Array] 42 | # 43 | # @api private 44 | # @since 0.1.0 45 | # @version 0.10.0 46 | def names 47 | @lock.read_sync { system_names } 48 | end 49 | 50 | # @return [Array>] 51 | # 52 | # @api private 53 | # @since 0.1.0 54 | # @version 0.10.0 55 | def interops 56 | @lock.read_sync { system_interops } 57 | end 58 | 59 | # @param block [Block] 60 | # @yield [system_name, system_interop] 61 | # @yieldparam system_name [String] 62 | # @yieldparam system_interop [Class] 63 | # @return [Enumerable] 64 | # 65 | # @api private 66 | # @since 0.1.0 67 | # @version 0.10.0 68 | def each(&block) 69 | @lock.read_sync { iterate(&block) } 70 | end 71 | 72 | # @return [Hash] 73 | # 74 | # @api private 75 | # @since 0.1.0 76 | # @version 0.10.0 77 | def to_h 78 | @lock.write_sync { systems.dup } 79 | end 80 | alias_method :to_hash, :to_h 81 | 82 | private 83 | 84 | # @return [Hash] 85 | # 86 | # @api private 87 | # @since 0.1.0 88 | attr_reader :systems 89 | 90 | # @return [Array] 91 | # 92 | # @pai private 93 | # @since 0.1.0 94 | def system_names 95 | systems.keys 96 | end 97 | 98 | # @return [Array>] 99 | # 100 | # @api private 101 | # @since 0.1.0 102 | def system_interops 103 | systems.values 104 | end 105 | 106 | # @param block [Block] 107 | # @yield [system_name, system_interop] 108 | # @yieldparam system_name [String] 109 | # @yieldparam system_interop [Class] 110 | # @return [Enumerable] 111 | # 112 | # @api private 113 | # @since 0.1.0 114 | def iterate(&block) 115 | block_given? ? systems.each_pair(&block) : systems.each_pair 116 | end 117 | 118 | # @param system_identifier [String, Symbol] 119 | # @param interop_klass [Class] 120 | # @return [void] 121 | # 122 | # @api private 123 | # @since 0.1.0 124 | def apply(system_identifier, interop_klass) 125 | prevent_incorrect_system_interop!(interop_klass) 126 | identifier = indifferently_accessible_identifier(system_identifier) 127 | systems[identifier] = interop_klass 128 | end 129 | 130 | # @param system_identifier [String, Symbol] 131 | # @return [Class] 132 | # 133 | # @raise [SmartCore::Initializer::UnsupportedTypeSystemError] 134 | # 135 | # @api private 136 | # @since 0.1.0 137 | # @version 0.10.0 138 | def fetch(system_identifier) 139 | identifier = indifferently_accessible_identifier(system_identifier) 140 | 141 | raise( 142 | SmartCore::Initializer::UnsupportedTypeSystemError, 143 | "`#{identifier}` type system is not supported." 144 | ) unless systems.key?(identifier) 145 | 146 | systems.fetch(identifier) 147 | end 148 | 149 | # @param interop_klass [Class] 150 | # @return [void] 151 | # 152 | # @raise [SmartCore::Initializer::IncorrectTypeSystemInteropError] 153 | # 154 | # @api private 155 | # @since 0.1.0 156 | def prevent_incorrect_system_interop!(interop_klass) 157 | unless interop_klass.is_a?(Class) && interop_klass < SmartCore::Initializer::TypeSystem::Interop 158 | raise( 159 | SmartCore::Initializer::IncorrectTypeSystemInteropError, 160 | 'Incorrect type system interop class.' 161 | ) 162 | end 163 | end 164 | 165 | # @param system_identifier [String, Symbol] 166 | # @return [String] 167 | # 168 | # @api private 169 | # @since 0.1.0 170 | def indifferently_accessible_identifier(system_identifier) 171 | system_identifier.to_s.clone.tap(&:freeze) 172 | end 173 | end 174 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/type_system/registry_interface.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # @api private 4 | # @since 0.1.0 5 | # @version 0.10.0 6 | module SmartCore::Initializer::TypeSystem::RegistryInterface 7 | class << self 8 | # @param base_module [Class, Module] 9 | # @return [void] 10 | # 11 | # @api private 12 | # @since 0.1.0 13 | # @version 0.10.0 14 | def extended(base_module) 15 | base_module.instance_variable_set( 16 | :@registry, SmartCore::Initializer::TypeSystem::Registry.new 17 | ) 18 | base_module.instance_variable_set( 19 | :@access_lock, SmartCore::Engine::ReadWriteLock.new 20 | ) 21 | end 22 | end 23 | 24 | # @option system_identifier [String, Symbol] 25 | # @option type [Any] 26 | # @return [SmartCore::Initializer::TypeSystem::Interop] 27 | # 28 | # @api private 29 | # @since 0.1.0 30 | # @version 0.10.0 31 | def build_interop(system: system_identifier, type: type_object) 32 | @access_lock.read_sync { registry.resolve(system_identifier).create(type_object) } 33 | end 34 | 35 | # @param identifier [String, Symbol] 36 | # @param interop_klass [Class] 37 | # @return [void] 38 | # 39 | # @api private 40 | # @since 0.1.0 41 | # @version 0.10.0 42 | def register(identifier, interop_klass) 43 | @access_lock.write_sync { registry.register(identifier, interop_klass) } 44 | end 45 | 46 | # @param identifier [String, Symbol] 47 | # @return [Class] 48 | # 49 | # @api private 50 | # @since 0.1.0 51 | # @version 0.10.0 52 | def resolve(identifier) 53 | @access_lock.read_sync { registry.resolve(identifier) } 54 | end 55 | alias_method :[], :resolve 56 | 57 | # @return [Array] 58 | # 59 | # @api public 60 | # @since 0.1.0 61 | # @version 0.10.0 62 | def names 63 | @access_lock.read_sync { registry.names } 64 | end 65 | 66 | # @return [Array>] 67 | # 68 | # @api public 69 | # @since 0.1.0 70 | # @version 0.10.0 71 | def systems 72 | @access_lock.read_sync { registry.to_h } 73 | end 74 | 75 | # @param block [Block] 76 | # @yield [system_name, system_interop] 77 | # @yieldparam system_name [String] 78 | # @yieldparam system_interop [Class] 79 | # @return [Enumerable] 80 | # 81 | # @api public 82 | # @since 0.1.0 83 | # @version 0.10.0 84 | def each(&block) 85 | @access_lock.read_sync { registry.each(&block) } 86 | end 87 | 88 | private 89 | 90 | # @return [SmartCore::Initializer::TypeSystem::Registry] 91 | # 92 | # @api private 93 | # @since 0.1.0 94 | attr_reader :registry 95 | end 96 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/type_system/smart_types.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # @api private 4 | # @since 0.1.0 5 | class SmartCore::Initializer::TypeSystem::SmartTypes < SmartCore::Initializer::TypeSystem::Interop 6 | require_relative 'smart_types/abstract_factory' 7 | require_relative 'smart_types/operation' 8 | 9 | type_alias('value.any', SmartCore::Types::Value::Any) 10 | type_alias('value.nil', SmartCore::Types::Value::Nil) 11 | type_alias('value.string', SmartCore::Types::Value::String) 12 | type_alias('value.symbol', SmartCore::Types::Value::Symbol) 13 | type_alias('value.text', SmartCore::Types::Value::Text) 14 | type_alias('value.integer', SmartCore::Types::Value::Integer) 15 | type_alias('value.float', SmartCore::Types::Value::Float) 16 | type_alias('value.numeric', SmartCore::Types::Value::Numeric) 17 | type_alias('value.big_decimal', SmartCore::Types::Value::BigDecimal) 18 | type_alias('value.boolean', SmartCore::Types::Value::Boolean) 19 | type_alias('value.array', SmartCore::Types::Value::Array) 20 | type_alias('value.hash', SmartCore::Types::Value::Hash) 21 | type_alias('value.proc', SmartCore::Types::Value::Proc) 22 | type_alias('value.class', SmartCore::Types::Value::Class) 23 | type_alias('value.module', SmartCore::Types::Value::Module) 24 | type_alias('value.time', SmartCore::Types::Value::Time) 25 | type_alias('value.date_time', SmartCore::Types::Value::DateTime) 26 | type_alias('value.date', SmartCore::Types::Value::Date) 27 | type_alias('value.time_based', SmartCore::Types::Value::TimeBased) 28 | 29 | type_alias(:any, SmartCore::Types::Value::Any) 30 | type_alias(:nil, SmartCore::Types::Value::Nil) 31 | type_alias(:string, SmartCore::Types::Value::String) 32 | type_alias(:symbol, SmartCore::Types::Value::Symbol) 33 | type_alias(:text, SmartCore::Types::Value::Text) 34 | type_alias(:integer, SmartCore::Types::Value::Integer) 35 | type_alias(:float, SmartCore::Types::Value::Float) 36 | type_alias(:numeric, SmartCore::Types::Value::Numeric) 37 | type_alias(:big_decimal, SmartCore::Types::Value::BigDecimal) 38 | type_alias(:boolean, SmartCore::Types::Value::Boolean) 39 | type_alias(:array, SmartCore::Types::Value::Array) 40 | type_alias(:hash, SmartCore::Types::Value::Hash) 41 | type_alias(:proc, SmartCore::Types::Value::Proc) 42 | type_alias(:class, SmartCore::Types::Value::Class) 43 | type_alias(:module, SmartCore::Types::Value::Module) 44 | type_alias(:time, SmartCore::Types::Value::Time) 45 | type_alias(:date_time, SmartCore::Types::Value::DateTime) 46 | type_alias(:date, SmartCore::Types::Value::Date) 47 | type_alias(:time_based, SmartCore::Types::Value::TimeBased) 48 | end 49 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/type_system/smart_types/abstract_factory.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SmartCore::Initializer::TypeSystem 4 | # @api private 5 | # @since 0.1.0 6 | class SmartTypes::AbstractFactory < Interop::AbstractFactory 7 | class << self 8 | # @param type [SmartCore::Types::Primitive] 9 | # @return [void] 10 | # 11 | # @raise [SmartCore::Initializer::IncorrectTypeObjectError] 12 | # 13 | # @api private 14 | # @since 0.1.0 15 | def prevent_incompatible_type!(type) 16 | unless type.is_a?(SmartCore::Types::Primitive) 17 | raise( 18 | SmartCore::Initializer::IncorrectTypeObjectError, 19 | 'Incorrect SmartCore::Types primitive ' \ 20 | '(type object should be a type of SmartCore::Types::Primitive)' 21 | ) 22 | end 23 | end 24 | 25 | # @param type [Any] 26 | # @return [String] 27 | # 28 | # @api private 29 | # @since 0.5.1 30 | def build_identifier(type) 31 | type.name 32 | end 33 | 34 | # @param type [SmartCore::Types::Primitive] 35 | # @return [SmartCore::Initializer::TypeSystem::SmartTypes::Operation::Valid] 36 | # 37 | # @api private 38 | # @since 0.1.0 39 | def build_valid_operation(type) 40 | SmartTypes::Operation::Valid.new(type) 41 | end 42 | 43 | # @return [SmartCore::Types::Value::Any] 44 | # 45 | # @api private 46 | # @since 0.1.0 47 | def generic_type_object 48 | SmartCore::Types::Value::Any 49 | end 50 | 51 | # @param type [SmartCore::Types::Primitive] 52 | # @return [SmartCore::Initializer::TypeSystem::SmartTypes::Operation::Validate] 53 | # 54 | # @api private 55 | # @since 0.1.0 56 | def build_validate_operation(type) 57 | SmartTypes::Operation::Validate.new(type) 58 | end 59 | 60 | # @param type [SmartCore::Types::Primitive] 61 | # @return [SmartCore::Initializer::TypeSystem::SmartTypes::Operation::Cast] 62 | # 63 | # @api private 64 | # @since 0.1.0 65 | def build_cast_operation(type) 66 | SmartTypes::Operation::Cast.new(type) 67 | end 68 | 69 | # @param identifier [String] 70 | # @param valid_op [SmartCore::Initializer::TypeSystem::SmartTypes::Operation::Valid] 71 | # @param valid_op [SmartCore::Initializer::TypeSystem::SmartTypes::Operation::Validate] 72 | # @param valid_op [SmartCore::Initializer::TypeSystem::SmartTypes::Operation::Cast] 73 | # @return [SmartCore::Initializer::TypeSystem::SmartTypes] 74 | # 75 | # @api private 76 | # @since 0.1.0 77 | # @version 0.5.1 78 | def build_interop(identifier, valid_op, validate_op, cast_op) 79 | SmartTypes.new(identifier, valid_op, validate_op, cast_op) 80 | end 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/type_system/smart_types/operation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SmartCore::Initializer::TypeSystem 4 | # @abstract 5 | # @api private 6 | # @since 0.1.0 7 | module SmartTypes::Operation 8 | require_relative 'operation/base' 9 | require_relative 'operation/valid' 10 | require_relative 'operation/validate' 11 | require_relative 'operation/cast' 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/type_system/smart_types/operation/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SmartCore::Initializer::TypeSystem 4 | # @api private 5 | # @since 0.1.0 6 | class SmartTypes::Operation::Base < Interop::Operation 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/type_system/smart_types/operation/cast.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SmartCore::Initializer::TypeSystem::SmartTypes::Operation 4 | # @api private 5 | # @since 0.1.0 6 | class Cast < Base 7 | # @param value [Any] 8 | # @return [Any] 9 | # 10 | # @api private 11 | # @since 0.1.0 12 | def call(value) 13 | type.cast(value) 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/type_system/smart_types/operation/valid.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SmartCore::Initializer::TypeSystem::SmartTypes::Operation 4 | # @api private 5 | # @since 0.1.0 6 | class Valid < Base 7 | # @param value [Any] 8 | # @return [Boolean] 9 | # 10 | # @api private 11 | # @since 0.1.0 12 | def call(value) 13 | type.valid?(value) 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/type_system/smart_types/operation/validate.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SmartCore::Initializer::TypeSystem::SmartTypes::Operation 4 | # @api private 5 | # @since 0.1.0 6 | class Validate < Base 7 | # @param value [Any] 8 | # @return [void] 9 | # 10 | # @api private 11 | # @since 0.1.0 12 | def call(value) 13 | type.validate!(value) 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/smart_core/initializer/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SmartCore 4 | module Initializer 5 | # @return [String] 6 | # 7 | # @api public 8 | # @since 0.1.0 9 | # @version 0.11.1 10 | VERSION = '0.12.0' 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /smart_initializer.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'lib/smart_core/initializer/version' 4 | 5 | Gem::Specification.new do |spec| 6 | spec.required_ruby_version = Gem::Requirement.new('>= 2.7') 7 | 8 | spec.name = 'smart_initializer' 9 | spec.version = SmartCore::Initializer::VERSION 10 | spec.authors = ['Rustam Ibragimov'] 11 | spec.email = ['iamdaiver@gmail.com'] 12 | 13 | spec.summary = 'Initializer DSL' 14 | spec.description = 'A simple and convenient way to declare complex constructors' 15 | spec.homepage = 'https://github.com/smart-rb/smart_initializer' 16 | spec.license = 'MIT' 17 | 18 | spec.metadata['homepage_uri'] = 19 | spec.homepage 20 | spec.metadata['source_code_uri'] = 21 | 'https://github.com/smart-rb/smart_initializer' 22 | spec.metadata['changelog_uri'] = 23 | 'https://github.com/smart-rb/smart_initializer/blob/master/CHANGELOG.md' 24 | 25 | spec.files = Dir.chdir(File.expand_path(__dir__)) do 26 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 27 | end 28 | 29 | spec.bindir = 'exe' 30 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 31 | spec.require_paths = ['lib'] 32 | 33 | spec.add_dependency 'smart_engine', '~> 0.16' 34 | spec.add_dependency 'smart_types', '~> 0.8' 35 | spec.add_dependency 'qonfig', '~> 0.24' 36 | 37 | spec.add_development_dependency 'bundler', '~> 2.3' 38 | spec.add_development_dependency 'rake', '~> 13.0' 39 | spec.add_development_dependency 'rspec', '~> 3.11' 40 | spec.add_development_dependency 'armitage-rubocop', '~> 1.30' 41 | spec.add_development_dependency 'simplecov', '~> 0.21' 42 | spec.add_development_dependency 'pry', '~> 0.14' 43 | spec.add_development_dependency 'ostruct' 44 | spec.add_development_dependency 'bigdecimal' 45 | end 46 | -------------------------------------------------------------------------------- /spec/features/configuration_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe 'Initializer configuration' do 4 | describe 'type system' do 5 | specify 'default type system' do 6 | expect(SmartCore::Initializer::Configuration[:default_type_system]).to eq(:smart_types) 7 | end 8 | 9 | specify 'unsupported type system - fail' do 10 | expect(SmartCore::Initializer::Configuration.config.valid_with?({ 11 | default_type_system: :kek_pek 12 | })).to eq(false) 13 | end 14 | 15 | specify 'you can choose any supported type system' do 16 | expect(SmartCore::Initializer::Configuration.config.valid_with?({ 17 | default_type_system: :smart_types 18 | })).to eq(true) 19 | end 20 | end 21 | 22 | describe 'strict_options' do 23 | specify 'default value' do 24 | expect(SmartCore::Initializer::Configuration[:strict_options]).to eq(true) 25 | end 26 | 27 | specify 'unsupported value => fail' do 28 | expect(SmartCore::Initializer::Configuration.config.valid_with?({ 29 | strict_options: :kek_pek 30 | })).to eq(false) 31 | end 32 | 33 | specify 'you choose any supported config-switching' do 34 | expect(SmartCore::Initializer::Configuration.config.valid_with?({ 35 | strict_options: true 36 | })).to eq(true) 37 | 38 | expect(SmartCore::Initializer::Configuration.config.valid_with?({ 39 | strict_options: false 40 | })).to eq(true) 41 | end 42 | end 43 | 44 | describe 'auto_cast' do 45 | specify 'default value' do 46 | expect(SmartCore::Initializer::Configuration[:auto_cast]).to eq(false) 47 | end 48 | 49 | specify 'unsupported value => fail' do 50 | expect(SmartCore::Initializer::Configuration.config.valid_with?({ 51 | auto_cast: :kek_pek 52 | })).to eq(false) 53 | end 54 | 55 | specify 'you choose any supported config-switching' do 56 | expect(SmartCore::Initializer::Configuration.config.valid_with?({ 57 | auto_cast: true 58 | })).to eq(true) 59 | 60 | expect(SmartCore::Initializer::Configuration.config.valid_with?({ 61 | auto_cast: false 62 | })).to eq(true) 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /spec/features/instance_attribute_list_access_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe 'Instance attribute list access' do 4 | specify '#__params__ / #__options__ / #__attributes__' do 5 | instance = Class.new do 6 | include SmartCore::Initializer 7 | param :first_name, 'string', as: :first 8 | param :second_name, 'string', as: :last 9 | option :age, 'numeric', as: :years 10 | option :is_admin, 'boolean', default: true 11 | end.new('Rustam', 'Ibragimov', age: 27) 12 | 13 | aggregate_failures 'attribute access (result without aliases)' do 14 | expect(instance.__params__).to eq({ 15 | first_name: 'Rustam', 16 | second_name: 'Ibragimov' 17 | }) 18 | 19 | expect(instance.__options__).to eq({ 20 | age: 27, 21 | is_admin: true 22 | }) 23 | 24 | expect(instance.__attributes__).to eq({ 25 | first_name: 'Rustam', 26 | second_name: 'Ibragimov', 27 | age: 27, 28 | is_admin: true 29 | }) 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/features/integration_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe 'SmartCore::Initializer integration' do 4 | describe 'configurable integration' do 5 | describe 'type_system config' do 6 | specify 'default config value (:smart_types)' do 7 | custom_klass = Class.new { include SmartCore::Initializer } 8 | expect(custom_klass.__initializer_settings__.type_system).to eq(:smart_types) 9 | end 10 | 11 | specify 'custom type_system definition' do 12 | custom_klass = Class.new { include SmartCore::Initializer(type_system: :smart_types) } 13 | expect(custom_klass.__initializer_settings__.type_system).to eq(:smart_types) 14 | end 15 | 16 | specify 'fails when the chosen type system is incorrect or unsupporeted' do 17 | expect do 18 | Class.new { include SmartCore::Initializer(type_system: :kek_pek) } 19 | end.to raise_error(SmartCore::Initializer::UnsupportedTypeSystemError) 20 | end 21 | end 22 | end 23 | 24 | describe 'strict_options config' do 25 | specify 'default config value (true)' do 26 | custom_klass = Class.new { include SmartCore::Initializer } 27 | expect(custom_klass.__initializer_settings__.strict_options).to eq(true) 28 | end 29 | 30 | specify 'custom strict_options definition' do 31 | # setup local config 32 | custom_klass = Class.new { include SmartCore::Initializer(strict_options: false) } 33 | expect(custom_klass.__initializer_settings__.strict_options).to eq(false) 34 | # check that global config has not been changed 35 | expect(SmartCore::Initializer::Configuration[:strict_options]).to eq(true) 36 | 37 | # setup another local config 38 | custom_klass = Class.new { include SmartCore::Initializer(strict_options: true) } 39 | expect(custom_klass.__initializer_settings__.strict_options).to eq(true) 40 | # check that global config has not been changed 41 | expect(SmartCore::Initializer::Configuration[:strict_options]).to eq(true) 42 | end 43 | 44 | specify 'fails when the strict_options value has incorrect type' do 45 | expect do 46 | Class.new { include SmartCore::Initializer(strict_options: 123) } 47 | end.to raise_error(SmartCore::Initializer::SettingArgumentError) 48 | end 49 | end 50 | 51 | describe 'auto_cast config' do 52 | specify 'default config value (true)' do 53 | custom_klass = Class.new { include SmartCore::Initializer } 54 | expect(custom_klass.__initializer_settings__.auto_cast).to eq(false) 55 | end 56 | 57 | specify 'custom auto_cast definition' do 58 | # setup local config 59 | custom_klass = Class.new { include SmartCore::Initializer(auto_cast: true) } 60 | expect(custom_klass.__initializer_settings__.auto_cast).to eq(true) 61 | # check that global config has not been changed 62 | expect(SmartCore::Initializer::Configuration[:auto_cast]).to eq(false) 63 | 64 | # setup another local config 65 | custom_klass = Class.new { include SmartCore::Initializer(auto_cast: false) } 66 | expect(custom_klass.__initializer_settings__.auto_cast).to eq(false) 67 | # check that global config has not been changed 68 | expect(SmartCore::Initializer::Configuration[:auto_cast]).to eq(false) 69 | end 70 | 71 | specify 'fails when the auto_cast value has incorrect type' do 72 | expect do 73 | Class.new { include SmartCore::Initializer(auto_cast: 123) } 74 | end.to raise_error(SmartCore::Initializer::SettingArgumentError) 75 | end 76 | end 77 | 78 | describe 'mixed configuration' do 79 | specify 'you can configure your own configs per class with inherited globals' do 80 | expect(SmartCore::Initializer::Configuration[:auto_cast]).to eq(false) 81 | expect(SmartCore::Initializer::Configuration[:default_type_system]).to eq(:smart_types) 82 | expect(SmartCore::Initializer::Configuration[:strict_options]).to eq(true) 83 | 84 | aggregate_failures 'inheritable defaults' do 85 | klass = Class.new { include SmartCore::Initializer } 86 | 87 | expect(klass.__initializer_settings__.auto_cast).to eq(false) 88 | expect(klass.__initializer_settings__.type_system).to eq(:smart_types) 89 | expect(klass.__initializer_settings__.strict_options).to eq(true) 90 | end 91 | 92 | aggregate_failures 'local configs has higher priority over the global configs' do 93 | klass = Class.new do 94 | include SmartCore::Initializer(auto_cast: true, strict_options: false) 95 | end 96 | 97 | expect(klass.__initializer_settings__.auto_cast).to eq(true) 98 | expect(klass.__initializer_settings__.strict_options).to eq(false) 99 | expect(klass.__initializer_settings__.type_system).to eq(:smart_types) 100 | end 101 | end 102 | 103 | context 'global config affect' do 104 | specify 'global configs affects local configs' do 105 | klass = Class.new { include SmartCore::Initializer } 106 | # initial global config 107 | expect(klass.__initializer_settings__.auto_cast).to eq(false) 108 | # initial global config 109 | expect(klass.__initializer_settings__.strict_options).to eq(true) 110 | 111 | # temporary change the global config 112 | SmartCore::Initializer::Configuration.config.with({ 113 | auto_cast: true, 114 | strict_options: false 115 | }) do 116 | klass = Class.new { include SmartCore::Initializer } 117 | 118 | # new global config 119 | expect(klass.__initializer_settings__.auto_cast).to eq(true) 120 | # new global config 121 | expect(klass.__initializer_settings__.strict_options).to eq(false) 122 | end 123 | end 124 | end 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /spec/features/plugin_system/load_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class SmartCore::Initializer::ALoadTest < SmartCore::Initializer::Plugins::Abstract 4 | def self.install!; end 5 | end 6 | 7 | class SmartCore::Initializer::BLoadTest < SmartCore::Initializer::Plugins::Abstract 8 | def self.install!; end 9 | end 10 | 11 | class SmartCore::Initializer::CLoadTest < SmartCore::Initializer::Plugins::Abstract 12 | def self.install! 13 | C_TEST_INTERCEPTOR.invoke 14 | end 15 | end 16 | 17 | class SmartCore::Initializer::DLoadTest < SmartCore::Initializer::Plugins::Abstract 18 | def self.install! 19 | D_TEST_INTERCEPTOR.invoke 20 | end 21 | end 22 | 23 | # rubocop:disable Layout/LineLength 24 | SmartCore::Initializer::Configuration.register_plugin(:a_load_test, SmartCore::Initializer::ALoadTest) 25 | SmartCore::Initializer::Configuration.register_plugin(:b_load_test, SmartCore::Initializer::BLoadTest) 26 | SmartCore::Initializer::Configuration.register_plugin(:c_load_test, SmartCore::Initializer::CLoadTest) 27 | SmartCore::Initializer::Configuration.register_plugin(:d_load_test, SmartCore::Initializer::DLoadTest) 28 | # rubocop:enable Layout/LineLength 29 | 30 | RSpec.describe 'SmartCore::Initializer::Plugins' do 31 | before do 32 | interceptor = Class.new { def invoke; end } 33 | stub_const('C_TEST_INTERCEPTOR', interceptor.new) 34 | stub_const('D_TEST_INTERCEPTOR', interceptor.new) 35 | end 36 | 37 | describe 'installation' do 38 | specify 'plugin loading interface' do 39 | expect(SmartCore::Initializer::ALoadTest).to receive(:load!).exactly(2).times 40 | expect(SmartCore::Initializer::BLoadTest).to receive(:load!).exactly(2).times 41 | 42 | SmartCore::Initializer::Configuration.plugin('a_load_test') 43 | SmartCore::Initializer::Configuration.plugin(:a_load_test) 44 | SmartCore::Initializer::Configuration.plugin('b_load_test') 45 | SmartCore::Initializer::Configuration.plugin(:b_load_test) 46 | end 47 | 48 | specify 'loaded plugins' do 49 | expect(SmartCore::Initializer::Configuration.loaded_plugins).not_to include( 50 | 'a_load_test', 'b_load_test' 51 | ) 52 | 53 | SmartCore::Initializer::Configuration.plugin('a_load_test') 54 | expect(SmartCore::Initializer::Configuration.loaded_plugins).to include('a_load_test') 55 | expect(SmartCore::Initializer::Configuration.loaded_plugins).not_to include('b_load_test') 56 | 57 | SmartCore::Initializer::Configuration.plugin('b_load_test') 58 | expect(SmartCore::Initializer::Configuration.loaded_plugins).to include( 59 | 'a_load_test', 'b_load_test' 60 | ) 61 | end 62 | end 63 | 64 | specify 'loading (loads only one time)' do 65 | expect(C_TEST_INTERCEPTOR).to receive(:invoke).exactly(1).time 66 | expect(D_TEST_INTERCEPTOR).to receive(:invoke).exactly(1).time 67 | 68 | SmartCore::Initializer::Configuration.plugin('c_load_test') 69 | SmartCore::Initializer::Configuration.plugin(:c_load_test) 70 | SmartCore::Initializer::Configuration.plugin('d_load_test') 71 | SmartCore::Initializer::Configuration.plugin(:d_load_test) 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /spec/features/plugin_system/registration_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # rubocop:disable Layout/LineLength 4 | SmartCore::Initializer::Plugins::ExistenceTest = Class.new(SmartCore::Initializer::Plugins::Abstract) 5 | SmartCore::Initializer::Configuration.register_plugin(:existence_test, SmartCore::Initializer::Plugins::ExistenceTest) 6 | # rubocop:enable Layout/LineLength 7 | 8 | SmartCore::Initializer::Plugins::ARegTest = Class.new(SmartCore::Initializer::Plugins::Abstract) 9 | SmartCore::Initializer::Plugins::BRegTest = Class.new(SmartCore::Initializer::Plugins::Abstract) 10 | 11 | RSpec.describe 'SmartCore::Initializer::Plugins' do 12 | specify 'plugin registration' do 13 | expect(SmartCore::Initializer::Plugins.names).not_to include('a_reg_test', 'b_reg_test') 14 | expect(SmartCore::Initializer::Configuration.plugins).not_to include('a_reg_test', 'b_reg_test') 15 | 16 | SmartCore::Initializer::Configuration.register_plugin( 17 | :a_reg_test, SmartCore::Initializer::Plugins::ARegTest 18 | ) 19 | 20 | expect(SmartCore::Initializer::Plugins.names).to include('a_reg_test') 21 | expect(SmartCore::Initializer::Configuration.plugins).to include('a_reg_test') 22 | expect(SmartCore::Initializer::Plugins.names).not_to include('b_reg_test') 23 | expect(SmartCore::Initializer::Configuration.plugins).not_to include('b_reg_test') 24 | 25 | SmartCore::Initializer::Configuration.register_plugin( 26 | :b_reg_test, SmartCore::Initializer::Plugins::BRegTest 27 | ) 28 | 29 | expect(SmartCore::Initializer::Plugins.names).to include('a_reg_test', 'b_reg_test') 30 | expect(SmartCore::Initializer::Configuration.plugins).to include('a_reg_test', 'b_reg_test') 31 | end 32 | 33 | describe 'incompatability-related failures' do 34 | specify 'plugin registration which name is in conflict with already registered plugin' do 35 | expect do 36 | SmartCore::Initializer::Plugins.register_plugin(:existence_test, Object) 37 | end.to raise_error(SmartCore::Initializer::AlreadyRegisteredPluginError) 38 | expect do 39 | SmartCore::Initializer::Configuration.register_plugin(:existence_test, Object) 40 | end.to raise_error(SmartCore::Initializer::AlreadyRegisteredPluginError) 41 | end 42 | 43 | specify 'loading of unregistered plugin' do 44 | expect do 45 | SmartCore::Initializer::Configuration.plugin(:kek_test_plugin) 46 | end.to raise_error(SmartCore::Initializer::UnregisteredPluginError) 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /spec/features/plugins/thy_types_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe 'Plugins: thy_types', plugin: :thy_types do 4 | before do 5 | require 'thy' 6 | SmartCore::Initializer::Configuration.plugin(:thy_types) 7 | end 8 | 9 | specify 'aliases' do 10 | expect(SmartCore::Initializer::TypeSystem::ThyTypes.type_aliases).to contain_exactly( 11 | 'any', 12 | 'nil', 13 | 'string', 14 | 'symbol', 15 | 'integer', 16 | 'float', 17 | 'numeric', 18 | 'boolean', 19 | 'time', 20 | 'date_time', 21 | 'untyped_array', 22 | 'untyped_hash' 23 | ) 24 | 25 | expect(SmartCore::Initializer::TypeSystem::ThyTypes.type_from_alias('any')).to( 26 | respond_to(:check) 27 | ) 28 | expect(SmartCore::Initializer::TypeSystem::ThyTypes.type_from_alias('nil')).to eq( 29 | Thy::Types::Nil 30 | ) 31 | expect(SmartCore::Initializer::TypeSystem::ThyTypes.type_from_alias('string')).to eq( 32 | Thy::Types::String 33 | ) 34 | expect(SmartCore::Initializer::TypeSystem::ThyTypes.type_from_alias('symbol')).to eq( 35 | Thy::Types::Symbol 36 | ) 37 | expect(SmartCore::Initializer::TypeSystem::ThyTypes.type_from_alias('integer')).to eq( 38 | Thy::Types::Integer 39 | ) 40 | expect(SmartCore::Initializer::TypeSystem::ThyTypes.type_from_alias('float')).to eq( 41 | Thy::Types::Float 42 | ) 43 | expect(SmartCore::Initializer::TypeSystem::ThyTypes.type_from_alias('numeric')).to eq( 44 | Thy::Types::Numeric 45 | ) 46 | expect(SmartCore::Initializer::TypeSystem::ThyTypes.type_from_alias('boolean')).to eq( 47 | Thy::Types::Boolean 48 | ) 49 | expect(SmartCore::Initializer::TypeSystem::ThyTypes.type_from_alias('time')).to eq( 50 | Thy::Types::Time 51 | ) 52 | expect(SmartCore::Initializer::TypeSystem::ThyTypes.type_from_alias('date_time')).to eq( 53 | Thy::Types::DateTime 54 | ) 55 | expect(SmartCore::Initializer::TypeSystem::ThyTypes.type_from_alias('untyped_array')).to eq( 56 | Thy::Types::UntypedArray 57 | ) 58 | expect(SmartCore::Initializer::TypeSystem::ThyTypes.type_from_alias('untyped_hash')).to eq( 59 | Thy::Types::UntypedHash 60 | ) 61 | end 62 | 63 | describe 'usage' do 64 | specify 'initializer with thy-types' do 65 | data_klass = Class.new do 66 | include SmartCore::Initializer(type_system: :thy_types) 67 | 68 | param :login, Thy::Types::String 69 | param :password, 'string' 70 | 71 | option :age, 'numeric' 72 | option :as_admin, Thy::Types::Boolean 73 | end 74 | 75 | instance = data_klass.new('vasia', 'pupkin', { age: 23, as_admin: true }) 76 | 77 | expect(instance.login).to eq('vasia') 78 | expect(instance.password).to eq('pupkin') 79 | expect(instance.age).to eq(23) 80 | expect(instance.as_admin).to eq(true) 81 | end 82 | 83 | specify 'mixing thy-types with smart-types' do 84 | data_klass = Class.new do 85 | include SmartCore::Initializer(type_system: :thy_types) 86 | 87 | param :nickname, SmartCore::Types::Value::String, type_system: :smart_types 88 | param :password, Thy::Types::String 89 | 90 | option :balance, 'value.float', type_system: :smart_types 91 | option :version, 'float' # thy-types 92 | end 93 | 94 | instance = data_klass.new('over', 'watch', { balance: 12.34, version: 15.707 }) 95 | 96 | expect(instance.nickname).to eq('over') 97 | expect(instance.password).to eq('watch') 98 | expect(instance.balance).to eq(12.34) 99 | expect(instance.version).to eq(15.707) 100 | end 101 | 102 | specify 'no support for type cast' do 103 | data_klass = Class.new do 104 | include SmartCore::Initializer(type_system: :thy_types) 105 | param :nickname, 'string', cast: true 106 | end 107 | 108 | expect do 109 | data_klass.new(123) 110 | end.to raise_error(SmartCore::Initializer::TypeCastingUnsupportedError) 111 | 112 | expect { data_klass.new('123') }.not_to raise_error 113 | end 114 | 115 | specify 'validation' do 116 | data_klass = Class.new do 117 | include SmartCore::Initializer(type_system: :thy_types) 118 | param :nickname, 'string' 119 | option :age, 'integer' 120 | end 121 | 122 | # NOTE: invalid 123 | expect { data_klass.new('test', { age: '123' }) }.to raise_error( 124 | # NOTE: old version => SmartCore::Initializer::ThyTypeValidationError 125 | SmartCore::Initializer::IncorrectTypeError 126 | ) 127 | 128 | # NOTE: invalid 129 | expect { data_klass.new(123, { age: 123 }) }.to raise_error( 130 | # NOTE: old version => SmartCore::Initializer::ThyTypeValidationError 131 | SmartCore::Initializer::IncorrectTypeError 132 | ) 133 | 134 | # NOTE: valid 135 | expect { data_klass.new('123', { age: 123 }) }.not_to raise_error 136 | end 137 | 138 | specify 'validation check for smart-types mixed with thy-types' do 139 | data_klass = Class.new do 140 | include SmartCore::Initializer(type_system: :thy_types) 141 | 142 | param :nickname, 'string', type_system: :smart_types 143 | param :email, 'string' 144 | end 145 | 146 | expect { data_klass.new(123, 'iamdaiver@gmail.com') }.to raise_error( 147 | # NOTE: old version => SmartCore::Types::TypeError 148 | SmartCore::Initializer::IncorrectTypeError 149 | ) 150 | 151 | expect { data_klass.new('123', :email) }.to raise_error( 152 | # NOTE: old version => SmartCore::Initializer::ThyTypeValidationError 153 | SmartCore::Initializer::IncorrectTypeError 154 | ) 155 | 156 | expect { data_klass.new('123', 'iamdaiver@gmail.com') }.not_to raise_error 157 | end 158 | 159 | specify 'support for the common attribute definition functionality' do 160 | data_klass = Class.new do 161 | include SmartCore::Initializer(type_system: :thy_types) 162 | 163 | option :email, 'string', default: 'no@email.com', finalize: -> (value) { "1#{value}" } 164 | option :admin, 'boolean', default: false, finalize: -> (value) { true } 165 | end 166 | 167 | instance = data_klass.new 168 | expect(instance.email).to eq('1no@email.com') 169 | expect(instance.admin).to eq(true) 170 | 171 | instance = data_klass.new(email: 'iamdaiver@gmail.com', admin: false) 172 | expect(instance.email).to eq('1iamdaiver@gmail.com') 173 | expect(instance.admin).to eq(true) 174 | end 175 | end 176 | end 177 | -------------------------------------------------------------------------------- /spec/features/type_system/custom_type_system_integration_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe 'Type System: custom type system integration' do 4 | end 5 | -------------------------------------------------------------------------------- /spec/features/type_system/type_aliasing_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe 'Type System: type aliasing' do 4 | end 5 | -------------------------------------------------------------------------------- /spec/smoke_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe 'Smoke Test' do 4 | specify 'full definition' do 5 | class User 6 | include SmartCore::Initializer 7 | # include SmartCore::Types::System(:T) 8 | # param :test, T::Value::Integer 9 | 10 | param :user_id, SmartCore::Types::Value::Integer, cast: true, privacy: :private 11 | option :password, :integer, cast: true, default: 'test', privacy: :private 12 | option :keka, finalize: (-> (value) { value.to_s }) 13 | option :send, :string, default: 'yes!' # you can use `send` as attribtue name 14 | 15 | params :creds, :nickname 16 | options :metadata, :datameta 17 | 18 | def initialize(*attrs, &block) 19 | @attrs = attrs 20 | @block = block 21 | end 22 | 23 | attr_reader :attrs 24 | attr_reader :block 25 | end 26 | 27 | random_block_result = rand(1...1000) 28 | user = User.new( 29 | 1, { admin: true }, '0exp', password: 'kek', metadata: {}, datameta: {}, keka: 123 30 | ) { random_block_result } 31 | 32 | expect(user).to be_a(User) 33 | 34 | expect { user.user_id }.to raise_error(NoMethodError) 35 | expect(user.__send__(:user_id)).to eq(1) 36 | expect(user.creds).to eq(admin: true) 37 | expect(user.nickname).to eq('0exp') 38 | expect { user.password }.to raise_error(NoMethodError) 39 | expect(user.__send__(:password)).to eq(0) 40 | expect(user.metadata).to eq({}) 41 | expect(user.datameta).to eq({}) 42 | expect(user.keka).to eq('123') 43 | expect(user.send).to eq('yes!') 44 | 45 | expect(user.attrs).to eq( 46 | [1, { admin: true }, '0exp', 47 | { password: 'kek', metadata: {}, datameta: {}, keka: 123 }] 48 | ) 49 | 50 | expect(user.block).to be_a(Proc) 51 | expect(user.block.call).to eq(random_block_result) 52 | end 53 | 54 | specify 'incompatible param/params/option/options names should raise an error' do 55 | expect do 56 | Class.new do 57 | include SmartCore::Initializer 58 | param 123 59 | end 60 | end.to raise_error(SmartCore::Initializer::ArgumentError) 61 | 62 | expect do 63 | Class.new do 64 | include SmartCore::Initializer 65 | option 123 66 | end 67 | end.to raise_error(SmartCore::Initializer::ArgumentError) 68 | 69 | expect do 70 | Class.new do 71 | include SmartCore::Initializer 72 | params :a, :b, 123, Object.new, 'test' 73 | end 74 | end.to raise_error(SmartCore::Initializer::ArgumentError) 75 | 76 | expect do 77 | Class.new do 78 | include SmartCore::Initializer 79 | options :a, :b, 123, Object.new, 'test' 80 | end 81 | end.to raise_error(SmartCore::Initializer::ArgumentError) 82 | end 83 | 84 | specify 'type alias shadowing warn' do 85 | expect do 86 | SmartCore::Initializer::TypeSystem::SmartTypes.type_alias( 87 | :string, SmartCore::Types::Value::String 88 | ) 89 | end.to output( 90 | '[SmartCore::Initializer::TypeSystem::SmartTypes] ' \ 91 | 'Shadowing of the already existing "string" type alias.' 92 | ).to_stderr 93 | 94 | expect do 95 | SmartCore::Initializer::TypeSystem::SmartTypes.type_alias( 96 | :integer, SmartCore::Types::Value::Integer 97 | ) 98 | end.to output( 99 | '[SmartCore::Initializer::TypeSystem::SmartTypes] ' \ 100 | 'Shadowing of the already existing "integer" type alias.' 101 | ).to_stderr 102 | end 103 | 104 | specify 'invalid/unsupported type attribute' do 105 | expect do 106 | Class.new do 107 | include SmartCore::Initializer 108 | param :name, Object.new 109 | end 110 | end.to raise_error(SmartCore::Initializer::IncorrectTypeObjectError) 111 | 112 | expect do 113 | Class.new do 114 | include SmartCore::Initializer 115 | option :name, Object.new 116 | end 117 | end.to raise_error(SmartCore::Initializer::IncorrectTypeObjectError) 118 | 119 | expect do 120 | Class.new do 121 | include SmartCore::Initializer 122 | option :name, :my_custom_type_alias 123 | end 124 | end.to raise_error(SmartCore::Initializer::TypeAliasNotFoundError) 125 | 126 | expect do 127 | Class.new do 128 | include SmartCore::Initializer 129 | param :name, :my_custom_type_alias 130 | end 131 | end.to raise_error(SmartCore::Initializer::TypeAliasNotFoundError) 132 | end 133 | 134 | specify ':privacy attribute functionality' do 135 | aggregate_failures 'correct values amd fuctionality' do 136 | instance = Class.new do 137 | include SmartCore::Initializer 138 | 139 | param :a, privacy: :private 140 | param :b, privacy: :protected 141 | param :c, privacy: :public 142 | 143 | param :d, privacy: 'private' 144 | param :e, privacy: 'protected' 145 | param :g, privacy: 'public' 146 | 147 | params :a1, :a2, privacy: :private 148 | params :c1, :c2, privacy: :protected 149 | params :d1, :d2, privacy: :public 150 | 151 | params :a3, :a4, privacy: 'private' 152 | params :c3, :c4, privacy: 'protected' 153 | params :d3, :d4, privacy: 'public' 154 | 155 | option :o, privacy: :private 156 | option :p, privacy: :protected 157 | option :q, privacy: :public 158 | 159 | option :r, privacy: 'private' 160 | option :s, privacy: 'protected' 161 | option :t, privacy: 'public' 162 | 163 | options :x1, :x2, privacy: :private 164 | options :y1, :y2, privacy: :protected 165 | options :z1, :z2, privacy: :public 166 | 167 | options :x3, :x4, privacy: 'private' 168 | options :y3, :y4, privacy: 'protected' 169 | options :z3, :z4, privacy: 'public' 170 | end.new( 171 | '', '', '', '', '', '', 172 | '', '', '', '', '', '', 173 | '', '', '', '', '', '', 174 | o: 1, p: 1, q: 1, r: 1, s: 1, t: 1, 175 | x1: 1, x2: 1, x3: 1, x4: 1, 176 | y1: 1, y2: 1, y3: 1, y4: 1, 177 | z1: 1, z2: 1, z3: 1, z4: 1 178 | ) 179 | 180 | expect(instance.private_methods(false)).to contain_exactly(*%i[ 181 | a d a1 a2 a3 a4 o r x1 x2 x3 x4 182 | ]) 183 | 184 | expect(instance.protected_methods(false)).to contain_exactly(*%i[ 185 | b e c1 c2 c3 c4 p s y1 y2 y3 y4 186 | ]) 187 | 188 | expect(instance.public_methods(false)).to contain_exactly(*%i[ 189 | c g d1 d2 d3 d4 q t z1 z2 z3 z4 190 | ]) 191 | end 192 | 193 | aggregate_failures 'incorrect values' do 194 | expect do 195 | Class.new do 196 | include SmartCore::Initializer 197 | param :name, privacy: 123 198 | end 199 | end.to raise_error(SmartCore::Initializer::ArgumentError) # TODO: PrivacyArgumentError 200 | 201 | expect do 202 | Class.new do 203 | include SmartCore::Initializer 204 | option :name, privacy: 123 205 | end 206 | end.to raise_error(SmartCore::Initializer::ArgumentError) # TODO: PrivacyArgumentError 207 | end 208 | end 209 | describe ':finalize' do 210 | specify 'finalize the result value of attribute via instance method' do 211 | klass = Class.new do 212 | include SmartCore::Initializer 213 | 214 | param :x, 'numeric', finalize: :double_up 215 | param :y, 'boolean', finalize: :switch 216 | option :a, 'integer', finalize: :change_amount 217 | option :b, 'string', finalize: :post_process 218 | 219 | def double_up(attr_value) 220 | attr_value * 2 221 | end 222 | 223 | def switch(attr_value) 224 | !attr_value 225 | end 226 | 227 | def change_amount(attr_value) 228 | attr_value + 1000 229 | end 230 | 231 | def post_process(attr_value) 232 | "#{attr_value}_post_processed!" 233 | end 234 | end 235 | 236 | instance = klass.new(5.6, true, a: 20, b: 'daiver') 237 | expect(instance.x).to eq(11.2) 238 | expect(instance.y).to eq(false) 239 | expect(instance.a).to eq(1020) 240 | expect(instance.b).to eq('daiver_post_processed!') 241 | end 242 | 243 | specify 'finalize the result value of attribute via proc/lambda object' do 244 | klass = Class.new do 245 | include SmartCore::Initializer 246 | 247 | param :x, 'numeric', finalize: -> (val) { val * val } 248 | param :y, 'boolean', finalize: proc { |val| !val } 249 | option :a, 'integer', finalize: -> (val) { val + 1 } 250 | option :b, 'string', finalize: proc { 'NOPE! :D' } 251 | end 252 | 253 | instance = klass.new(10, false, a: 1, b: 'pek') 254 | 255 | expect(instance.x).to eq(100) 256 | expect(instance.y).to eq(true) 257 | expect(instance.a).to eq(2) 258 | expect(instance.b).to eq('NOPE! :D') 259 | end 260 | 261 | specify 'expects proc/labmda/string/symbol' do 262 | expect do 263 | Class.new do 264 | include SmartCore::Initializer 265 | param :a, finalize: 123 266 | end 267 | end.to raise_error(SmartCore::Initializer::ArgumentError) # TODO: FinalizeArgumentError 268 | 269 | expect do 270 | Class.new do 271 | include SmartCore::Initializer 272 | option :a, finalize: 123 273 | end 274 | end.to raise_error(SmartCore::Initializer::ArgumentError) # TODO: FinalizeArgumentError 275 | 276 | expect do 277 | Class.new do 278 | include SmartCore::Initializer 279 | param :a, finalize: :test 280 | end 281 | end.not_to raise_error 282 | 283 | expect do 284 | Class.new do 285 | include SmartCore::Initializer 286 | option :a, finalize: 'test' 287 | end 288 | end.not_to raise_error 289 | 290 | expect do 291 | Class.new do 292 | include SmartCore::Initializer 293 | param :a, finalize: proc {} 294 | end 295 | end.not_to raise_error 296 | 297 | expect do 298 | Class.new do 299 | include SmartCore::Initializer 300 | option :a, finalize: -> (val) {} 301 | end 302 | end.not_to raise_error 303 | end 304 | 305 | specify 'lambda objects used for finalize should have an argument in their signature' do 306 | expect do 307 | Class.new do 308 | include SmartCore::Initializer 309 | option :a, finalize: -> {} 310 | end 311 | end.to raise_error(SmartCore::Initializer::ArgumentError) # TODO: FinalizeArgumentError 312 | 313 | expect do 314 | Class.new do 315 | include SmartCore::Initializer 316 | param :a, finalize: -> {} 317 | end 318 | end.to raise_error(SmartCore::Initializer::ArgumentError) # TODO: FinalizeArgumentError 319 | 320 | expect do 321 | Class.new do 322 | include SmartCore::Initializer 323 | option :a, finalize: -> (val) {} 324 | end 325 | end.not_to raise_error 326 | 327 | expect do 328 | Class.new do 329 | include SmartCore::Initializer 330 | param :a, finalize: -> (val) {} 331 | end 332 | end.not_to raise_error 333 | 334 | expect do 335 | Class.new do 336 | include SmartCore::Initializer 337 | option :a, finalize: -> (*val) {} 338 | end 339 | end.not_to raise_error 340 | 341 | expect do 342 | Class.new do 343 | include SmartCore::Initializer 344 | param :a, finalize: -> (*val) {} 345 | end 346 | end.not_to raise_error 347 | 348 | expect do 349 | Class.new do 350 | include SmartCore::Initializer 351 | param :a, finalize: -> (req_arg, *val) {} 352 | end 353 | end.not_to raise_error 354 | 355 | # NOTE: proc objects can omit attribute in their signature 356 | expect do 357 | Class.new do 358 | include SmartCore::Initializer 359 | option :a, finalize: proc {} 360 | end 361 | end.not_to raise_error 362 | end 363 | 364 | specify 'finalized return value is type-validated too' do 365 | expect do 366 | Class.new do 367 | include SmartCore::Initializer 368 | param :a, 'string', finalize: -> (val) { 1 } # returns incorrect value 369 | end.new('111') 370 | end.to raise_error(SmartCore::Initializer::IncorrectTypeError) 371 | 372 | expect do 373 | Class.new do 374 | include SmartCore::Initializer 375 | option :b, 'string', finalize: proc { 1 } # returns incorrect value 376 | end.new(b: '111') 377 | end.to raise_error(SmartCore::Initializer::IncorrectTypeError) 378 | 379 | expect do 380 | Class.new do 381 | include SmartCore::Initializer 382 | param :c, :string, finalize: :post_process 383 | 384 | def post_process(attr_value) 385 | 123 386 | end 387 | end.new('test') 388 | end.to raise_error(SmartCore::Initializer::IncorrectTypeError) 389 | 390 | expect do 391 | Class.new do 392 | include SmartCore::Initializer 393 | option :d, :string, finalize: :post_process 394 | 395 | def post_process(attr_value) 396 | 123 397 | end 398 | end.new(d: 'test') 399 | end.to raise_error(SmartCore::Initializer::IncorrectTypeError) 400 | end 401 | end 402 | 403 | specify 'inheritance' do 404 | class NanoBase 405 | include SmartCore::Initializer 406 | 407 | param :a 408 | option :b 409 | 410 | params :c, :d, :e 411 | options :f, :g 412 | end 413 | 414 | Concrete = Class.new(NanoBase) 415 | SubConcrete = Class.new(Concrete) 416 | sub_concrete = SubConcrete.new(1, 2, 3, 4, b: 6, f: 7, g: 8) 417 | 418 | expect(sub_concrete.a).to eq(1) 419 | expect(sub_concrete.b).to eq(6) 420 | expect(sub_concrete.c).to eq(2) 421 | expect(sub_concrete.d).to eq(3) 422 | expect(sub_concrete.e).to eq(4) 423 | expect(sub_concrete.f).to eq(7) 424 | expect(sub_concrete.g).to eq(8) 425 | end 426 | 427 | specify 'type aliases' do 428 | class Animal 429 | include SmartCore::Initializer 430 | 431 | param :name, :string 432 | option :age, :integer 433 | end 434 | 435 | expect { Animal.new(123, age: 123) } 436 | .to raise_error(SmartCore::Initializer::IncorrectTypeError) 437 | 438 | expect { Animal.new('test', age: 'test') } 439 | .to raise_error(SmartCore::Initializer::IncorrectTypeError) 440 | 441 | expect { Animal.new('test', age: 123) }.not_to raise_error 442 | end 443 | 444 | specify 'param and option overlapping' do 445 | aggregate_failures 'overlap failures' do 446 | expect do 447 | Class.new do 448 | include SmartCore::Initializer 449 | 450 | param :user_id 451 | option :user_id 452 | end 453 | end.to raise_error(SmartCore::Initializer::ParameterOverlapError) 454 | 455 | expect do 456 | Class.new do 457 | include SmartCore::Initializer 458 | 459 | option :user_id 460 | param :user_id 461 | end 462 | end.to raise_error(SmartCore::Initializer::OptionOverlapError) 463 | end 464 | end 465 | 466 | context 'instantiation' do 467 | describe 'strict_options option' do 468 | context "when it's true" do 469 | let(:klass) do 470 | Class.new do 471 | include SmartCore::Initializer(strict_options: true) 472 | option :user_id 473 | option :role_id, default: 123 474 | end 475 | end 476 | 477 | specify 'fails on unknown options' do 478 | expect do 479 | klass.new(user_id: 7, lol_kek: 123) 480 | end.to raise_error(SmartCore::Initializer::OptionArgumentError) 481 | 482 | expect do 483 | klass.new(user_id: 7, role_id: 55, lol_kek: 123) 484 | end.to raise_error(SmartCore::Initializer::OptionArgumentError) 485 | 486 | expect { klass.new(user_id: 7) }.not_to raise_error 487 | 488 | expect { klass.new(user_id: 7, role_id: 7) }.not_to raise_error 489 | end 490 | end 491 | 492 | context "when it's false" do 493 | let(:klass) do 494 | Class.new do 495 | include SmartCore::Initializer(strict_options: false) 496 | option :user_id 497 | option :role_id, default: 123 498 | end 499 | end 500 | 501 | specify 'skips unknown options' do 502 | expect { klass.new(user_id: 7, lol_kek: 123) }.not_to raise_error 503 | end 504 | end 505 | end 506 | end 507 | 508 | specify 'initializer behavior extension' do 509 | instance = Class.new { include SmartCore::Initializer } 510 | expect(instance).not_to respond_to(:kek) 511 | expect(instance).not_to respond_to(:lol) 512 | 513 | instance = Class.new do 514 | include SmartCore::Initializer 515 | ext_init { |object| object.define_singleton_method(:kek) { 'pek' } } 516 | ext_init { |object| object.define_singleton_method(:lol) { 'rofl' } } 517 | end.new 518 | 519 | expect(instance).to respond_to(:kek) 520 | expect(instance).to respond_to(:lol) 521 | expect(instance.kek).to eq('pek') 522 | expect(instance.lol).to eq('rofl') 523 | end 524 | 525 | describe 'attribute aliases (:as)' do 526 | specify ':as option makes an alias method for attribute' do 527 | klass = Class.new do 528 | include SmartCore::Initializer 529 | 530 | param :name, as: :first_name 531 | option :nick, as: :login 532 | end 533 | 534 | instance = klass.new('Rustam', nick: :daiver) 535 | 536 | expect(instance.name).to eq('Rustam') 537 | expect(instance.first_name).to eq('Rustam') 538 | 539 | expect(instance.nick).to eq(:daiver) 540 | expect(instance.login).to eq(:daiver) 541 | end 542 | 543 | specify 'takes into original attribute incapsulation (private/protected/public)' do 544 | instance = Class.new do 545 | include SmartCore::Initializer 546 | 547 | param :a, as: :a1, privacy: :private, mutable: true 548 | param :b, as: :b1, privacy: :protected, mutable: true 549 | param :c, as: :c1, privacy: :public, mutable: true 550 | 551 | option :x, as: :x1, privacy: :private, mutable: true 552 | option :y, as: :y1, privacy: :protected, mutable: true 553 | option :z, as: :z1, privacy: :public, mutable: true 554 | end.new(1, 2, 3, x: 4, y: 5, z: 6) 555 | 556 | # rubocop:disable Layout/LineLength 557 | expect(instance.private_methods(false)).to contain_exactly(:a, :a=, :a1, :a1=, :x, :x=, :x1, :x1=) 558 | expect(instance.protected_methods(false)).to contain_exactly(:b, :b=, :b1, :b1=, :y, :y=, :y1, :y1=) 559 | expect(instance.public_methods(false)).to contain_exactly(:c, :c=, :c1, :c1=, :z, :z=, :z1, :z1=) 560 | # rubocop:enable Layout/LineLength 561 | end 562 | specify 'aliased mutable attributes gives type-validated mutator too' do 563 | klass = Class.new do 564 | include SmartCore::Initializer 565 | 566 | param :role, :symbol, as: :access_level, mutable: true 567 | option :nick, :string, as: :login, mutable: true 568 | end 569 | 570 | instance = klass.new(:admin, nick: 'D@iVeR') 571 | 572 | aggregate_failures 'instance repsonds to aliased mutators' do 573 | expect(instance).to respond_to(:access_level=) 574 | expect(instance).to respond_to(:login=) 575 | end 576 | 577 | aggregate_failures 'aliased mutator mutates attributes correctly' do 578 | expect(instance.access_level).to eq(:admin) 579 | expect(instance.role).to eq(:admin) 580 | expect(instance.login).to eq('D@iVeR') 581 | expect(instance.nick).to eq('D@iVeR') 582 | 583 | instance.access_level = :user 584 | instance.login = 'Rustam' 585 | 586 | expect(instance.access_level).to eq(:user) 587 | expect(instance.role).to eq(:user) 588 | expect(instance.login).to eq('Rustam') 589 | expect(instance.nick).to eq('Rustam') 590 | end 591 | 592 | aggregate_failures "incopmatible types raises error and does not mutate object's state" do 593 | expect do 594 | instance.access_level = 123 595 | end.to raise_error(SmartCore::Initializer::IncorrectTypeError) 596 | expect do 597 | instance.login = Object.new 598 | end.to raise_error(SmartCore::Initializer::IncorrectTypeError) 599 | 600 | # aliases 601 | expect(instance.access_level).to eq(:user) 602 | expect(instance.login).to eq('Rustam') 603 | 604 | # originals 605 | expect(instance.role).to eq(:user) 606 | expect(instance.nick).to eq('Rustam') 607 | end 608 | end 609 | 610 | specify 'expects strings and symbols' do 611 | expect do 612 | Class.new do 613 | include SmartCore::Initializer 614 | option :a, as: 123 615 | end 616 | end.to raise_error(SmartCore::Initializer::ArgumentError) # TODO: use AliasArgumentError 617 | 618 | expect do 619 | Class.new do 620 | include SmartCore::Initializer 621 | option :a, as: Object.new 622 | end 623 | end.to raise_error(SmartCore::Initializer::ArgumentError) # TODO: use AliasArgumentError 624 | 625 | expect do 626 | Class.new do 627 | include SmartCore::Initializer 628 | option :a, as: :test 629 | end 630 | end.not_to raise_error 631 | 632 | expect do 633 | Class.new do 634 | include SmartCore::Initializer 635 | option :a, as: 'test' 636 | end 637 | end.not_to raise_error 638 | end 639 | end 640 | 641 | describe 'optional `option` attributes' do 642 | specify 'developer can omit passing optional `option` attributes' do 643 | klass = Class.new do 644 | include SmartCore::Initializer 645 | 646 | param :name 647 | 648 | option :age, 'integer' 649 | option :role, 'symbol', optional: true 650 | option :nick, 'string', optional: true 651 | option :last_login_at, 'time' 652 | end 653 | 654 | # you can omit :role and :nick options 655 | instance = klass.new('Rustam', age: 30, last_login_at: Time.parse('2020-01-01')) 656 | 657 | expect(instance.name).to eq('Rustam') 658 | expect(instance.age).to eq(30) 659 | expect(instance.last_login_at).to eq(Time.parse('2020-01-01')) 660 | 661 | expect(instance.role).to eq(nil) 662 | expect(instance.nick).to eq(nil) 663 | end 664 | 665 | specify 'omitted attributes are initialized by `nil` by default' do 666 | instance = Class.new do 667 | include SmartCore::Initializer 668 | 669 | params :x, :y 670 | 671 | option :a, 'integer' 672 | option :b, 'symbol', optional: true 673 | option :c, 'string', optional: true 674 | option :d, 'time' 675 | option :f, optional: true 676 | end.new(1, 2, a: 3, b: :kek, d: Time.parse('2021-01-01')) 677 | 678 | expect(instance.x).to eq(1) 679 | expect(instance.y).to eq(2) 680 | expect(instance.b).to eq(:kek) 681 | expect(instance.c).to eq(nil) 682 | expect(instance.d).to eq(Time.parse('2021-01-01')) 683 | expect(instance.f).to eq(nil) 684 | end 685 | 686 | specify 'omitted :optional attributes uses the declared `:default` value' do 687 | instance = Class.new do 688 | include SmartCore::Initializer 689 | 690 | params :x, :y 691 | 692 | option :a, 'numeric' 693 | option :b, 'symbol', default: -> { :test_b }, optional: true 694 | option :c, 'string', default: -> { 'test_c' }, optional: true 695 | option :d, 'string', default: 'Test_D', optional: true 696 | option :e, 'time', default: -> { Time.parse('2022-01-01') } 697 | option :f, 'time', optional: true 698 | end.new(555, 666, a: 888) 699 | 700 | expect(instance.x).to eq(555) 701 | expect(instance.y).to eq(666) 702 | expect(instance.a).to eq(888) 703 | 704 | expect(instance.b).to eq(:test_b) 705 | expect(instance.c).to eq('test_c') 706 | expect(instance.d).to eq('Test_D') 707 | expect(instance.e).to eq(Time.parse('2022-01-01')) 708 | expect(instance.f).to eq(nil) 709 | end 710 | 711 | specify 'optional attributes without :default should not be finalized' do 712 | aggregate_failures 'without :default => without finalization' do 713 | instance = Class.new do 714 | include SmartCore::Initializer 715 | option :a, :numeric, optional: true, finalize: -> (val) { 123 } 716 | end.new 717 | expect(instance.a).to eq(nil) 718 | end 719 | 720 | aggregate_failures 'with :default => with finalization' do 721 | instance = Class.new do 722 | include SmartCore::Initializer 723 | option :a, :numeric, default: 3, optional: true, finalize: -> (val) { val * 2 } 724 | end.new 725 | expect(instance.a).to eq(6) 726 | end 727 | end 728 | 729 | specify 'expects boolean value' do 730 | expect do 731 | Class.new do 732 | include SmartCore::Initializer 733 | option :a, optional: 123 734 | end 735 | end.to raise_error(SmartCore::Initializer::ArgumentError) # TODO: use OptionalArgumentError 736 | 737 | expect do 738 | Class.new do 739 | include SmartCore::Initializer 740 | option :a, optional: Object.new 741 | end 742 | end.to raise_error(SmartCore::Initializer::ArgumentError) # TODO: use OptionalArgumentError 743 | 744 | expect do 745 | Class.new do 746 | include SmartCore::Initializer 747 | option :a, optional: true 748 | end 749 | end.not_to raise_error 750 | 751 | expect do 752 | Class.new do 753 | include SmartCore::Initializer 754 | option :a, optional: false 755 | end 756 | end.not_to raise_error 757 | end 758 | end 759 | 760 | describe ':default value' do 761 | specify 'dynamic value initializetion (usage of a proc/lambda)' do 762 | klass = Class.new do 763 | include SmartCore::Initializer 764 | 765 | option :a, default: proc { '123' } 766 | option :b, default: -> { Time.parse('2011-01-01') } 767 | end 768 | 769 | aggregate_failures 'pass only one attribute (first option)' do 770 | instance = klass.new(a: 10) 771 | expect(instance.a).to eq(10) 772 | expect(instance.b).to eq(Time.parse('2011-01-01')) 773 | end 774 | 775 | aggregate_failures 'pass only one attribute (second option)' do 776 | instance = klass.new(b: Time.parse('2088-01-01')) 777 | expect(instance.a).to eq('123') 778 | expect(instance.b).to eq(Time.parse('2088-01-01')) 779 | end 780 | 781 | aggregate_failures 'use defaults' do 782 | instance = klass.new 783 | expect(instance.a).to eq('123') 784 | expect(instance.b).to eq(Time.parse('2011-01-01')) 785 | end 786 | end 787 | 788 | specify 'duplicatable values (non-dynamic values should be duplicated during initialization' do 789 | some_static_value = 'test_123' 790 | 791 | klass = Class.new do 792 | include SmartCore::Initializer 793 | option :nickname, default: 'D@iVeR' # non-dynamic value (should be duplicated) 794 | option :age, default: -> { some_static_value } # dynamic value (should be returned "as-is") 795 | end 796 | 797 | first_instance = klass.new 798 | second_instance = klass.new 799 | 800 | # non-dynamic attributes are different 801 | expect(first_instance.nickname.object_id).not_to eq(second_instance.nickname.object_id) 802 | 803 | # dynamic attribute is static in our case - should have no difference 804 | expect(first_instance.age.object_id).to eq(second_instance.age.object_id) 805 | end 806 | 807 | specify 'default value should be type validated too' do 808 | klass = Class.new do 809 | include SmartCore::Initializer 810 | option :nickname, 'string', default: -> { 123 } # default value has invalid type 811 | end 812 | 813 | expect { klass.new }.to raise_error(SmartCore::Initializer::IncorrectTypeError) 814 | end 815 | end 816 | 817 | describe 'attribute type casting' do 818 | specify 'expects boolean' do 819 | expect do 820 | Class.new do 821 | include SmartCore::Initializer 822 | param :a, :integer, cast: 123 823 | end 824 | end.to raise_error(SmartCore::Initializer::ArgumentError) # TODO: CastArgumentError 825 | 826 | expect do 827 | Class.new do 828 | include SmartCore::Initializer 829 | option :a, :integer, cast: 'kek' 830 | end 831 | end.to raise_error(SmartCore::Initializer::ArgumentError) # TODO: CastArgumentError 832 | 833 | expect do 834 | Class.new do 835 | include SmartCore::Initializer 836 | param :a, :integer, cast: true 837 | end 838 | end.not_to raise_error 839 | 840 | expect do 841 | Class.new do 842 | include SmartCore::Initializer 843 | param :a, :integer, cast: false 844 | end 845 | end.not_to raise_error 846 | 847 | expect do 848 | Class.new do 849 | include SmartCore::Initializer 850 | option :a, :integer, cast: true 851 | end 852 | end.not_to raise_error 853 | 854 | expect do 855 | Class.new do 856 | include SmartCore::Initializer 857 | option :a, :integer, cast: false 858 | end 859 | end.not_to raise_error 860 | end 861 | 862 | specify 'attribte can be marked as type-castable (non-marked by default)' do 863 | klass = Class.new do 864 | include SmartCore::Initializer(auto_cast: true) 865 | 866 | param :a, 'integer', cast: true 867 | param :b, 'boolean', cast: true 868 | param :c, 'string' 869 | 870 | option :x, 'string', cast: true 871 | option :y, 'integer', cast: true 872 | option :z, 'array' 873 | end 874 | 875 | instance = klass.new('123', 456, 'test', x: :phone, y: '123.456', z: []) 876 | 877 | expect(instance.a).to eq(123) 878 | expect(instance.b).to eq(true) 879 | expect(instance.c).to eq('test') 880 | expect(instance.x).to eq('phone') 881 | expect(instance.y).to eq(123) 882 | expect(instance.z).to eq([]) 883 | end 884 | 885 | specify 'automatic type casting' do 886 | klass = Class.new do 887 | include SmartCore::Initializer(auto_cast: true) # set the automatic type casting 888 | 889 | param :a, 'integer' 890 | option :b, 'string' 891 | param :c, 'boolean', cast: false # do not cast automaticaly 892 | option :d, 'string', cast: false # do not cast automaticaly 893 | end 894 | 895 | instance = klass.new('123', true, b: 456.789, d: '12000') 896 | expect(instance.a).to eq(123) # auto-casted from string to integer 897 | expect(instance.b).to eq('456.789') # auto-casted from float to string 898 | expect(instance.c).to eq(true) 899 | expect(instance.d).to eq('12000') 900 | end 901 | end 902 | 903 | describe 'mutable attributes' do 904 | specify 'attribute definition' do 905 | klass = Class.new do 906 | include SmartCore::Initializer 907 | 908 | param :a, 'string', mutable: true, cast: true 909 | param :b, 'integer', mutable: false 910 | param :c, 'array' # mutable: false by default 911 | 912 | option :d, :string, mutable: true 913 | option :e, :integer, mutable: false 914 | option :f, :array # mutable: false by default 915 | 916 | params :z1, :z2, mutable: true 917 | params :z3, :z4, mutable: false 918 | params :z5, :z6 # mutable: false by default 919 | 920 | options :o1, :o2, mutable: true 921 | options :o3, :o4, mutable: false 922 | options :o5, :o6 # mutable: false by default 923 | end 924 | 925 | instance = klass.new( 926 | 1, 2, [], 927 | 'z1', 'z2', 'z3', 'z4', 'z5', 'z6', 928 | d: '3', e: 4, f: [], 929 | o1: 'o1', o2: 'o2', o3: 'o3', o4: 'o4', o5: 'o5', o6: 'o6' 930 | ) 931 | 932 | # check param behavior 933 | aggregate_failures 'param mutator behavior' do 934 | expect { instance.a = '2' }.not_to raise_error 935 | # test that value was changed 936 | expect(instance.a).to eq('2') 937 | # test the type checking of mutable methods 938 | expect { instance.a = 2 }.to raise_error(SmartCore::Initializer::IncorrectTypeError) 939 | # NOTE: old version of error => SmartCore::Types::TypeError 940 | 941 | # non-mutable option 942 | expect { instance.b = 3 }.to raise_error(::NoMethodError) 943 | # non-mutable by default 944 | expect { instance.c = [123] }.to raise_error(::NoMethodError) 945 | 946 | # after all manipulations the state is correct 947 | expect(instance.a).to eq('2') 948 | expect(instance.b).to eq(2) 949 | expect(instance.c).to eq([]) 950 | end 951 | 952 | # check option behavior 953 | aggregate_failures 'otpion mutator behavior' do 954 | expect { instance.d = '2' }.not_to raise_error 955 | # test that value was changed 956 | expect(instance.d).to eq('2') 957 | # test the type checking of mutable methods 958 | expect { instance.d = 2 }.to raise_error(SmartCore::Initializer::IncorrectTypeError) 959 | # NOTE: old version of error => SmartCore::Types::TypeError 960 | 961 | # non-mutable option 962 | expect { instance.e = 3 }.to raise_error(::NoMethodError) 963 | # non-mutable by default 964 | expect { instance.f = [123] }.to raise_error(::NoMethodError) 965 | 966 | # after all manipulations the state is correct 967 | expect(instance.d).to eq('2') 968 | expect(instance.e).to eq(4) 969 | expect(instance.f).to eq([]) 970 | end 971 | 972 | # check optionS behavior 973 | aggregate_failures 'optionS mutator behavior' do 974 | # mutable options 975 | expect { instance.o1 = 'kek_o1' }.not_to raise_error 976 | expect { instance.o2 = 'kek_o2' }.not_to raise_error 977 | expect(instance.o1).to eq('kek_o1') 978 | expect(instance.o2).to eq('kek_o2') 979 | 980 | # non-mutable options 981 | expect { instance.o3 = 'kek_o1' }.to raise_error(::NoMethodError) 982 | expect { instance.o4 = 'kek_o2' }.to raise_error(::NoMethodError) 983 | expect(instance.o3).to eq('o3') 984 | expect(instance.o4).to eq('o4') 985 | 986 | # non-mutable by default 987 | expect { instance.o5 = 'kek_05' }.to raise_error(::NoMethodError) 988 | expect { instance.o6 = 'kek_06' }.to raise_error(::NoMethodError) 989 | expect(instance.o5).to eq('o5') 990 | expect(instance.o6).to eq('o6') 991 | end 992 | 993 | # check paramS behavior 994 | aggregate_failures 'paramS mutator behavior' do 995 | # mutable options 996 | expect { instance.z1 = 'lel_z1' }.not_to raise_error 997 | expect { instance.z2 = 'lel_z2' }.not_to raise_error 998 | expect(instance.z1).to eq('lel_z1') 999 | expect(instance.z2).to eq('lel_z2') 1000 | 1001 | # non-mutable options 1002 | expect { instance.z3 = 'lel_z3' }.to raise_error(::NoMethodError) 1003 | expect { instance.z4 = 'lel_z4' }.to raise_error(::NoMethodError) 1004 | expect(instance.z3).to eq('z3') 1005 | expect(instance.z4).to eq('z4') 1006 | 1007 | # non-mutable by default 1008 | expect { instance.z5 = 'lel_z5' }.to raise_error(::NoMethodError) 1009 | expect { instance.z6 = 'lel_z6' }.to raise_error(::NoMethodError) 1010 | expect(instance.z5).to eq('z5') 1011 | expect(instance.z6).to eq('z6') 1012 | end 1013 | end 1014 | specify 'expects boolean value' do 1015 | expect do 1016 | Class.new do 1017 | include SmartCore::Initializer 1018 | option :a, mutable: 123 1019 | end 1020 | end.to raise_error(SmartCore::Initializer::ArgumentError) # TODO: use MutableArgumentError 1021 | 1022 | expect do 1023 | Class.new do 1024 | include SmartCore::Initializer 1025 | option :a, mutable: Object.new 1026 | end 1027 | end.to raise_error(SmartCore::Initializer::ArgumentError) # TODO: use MutableArgumentError 1028 | 1029 | expect do 1030 | Class.new do 1031 | include SmartCore::Initializer 1032 | option :a, mutable: true 1033 | end 1034 | end.not_to raise_error 1035 | 1036 | expect do 1037 | Class.new do 1038 | include SmartCore::Initializer 1039 | option :a, mutable: false 1040 | end 1041 | end.not_to raise_error 1042 | end 1043 | end 1044 | end 1045 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'simplecov' 4 | 5 | SimpleCov.formatter = SimpleCov::Formatter::HTMLFormatter 6 | SimpleCov.minimum_coverage(100) if !!ENV['FULL_TEST_COVERAGE_CHECK'] 7 | SimpleCov.enable_coverage(:branch) 8 | SimpleCov.enable_coverage(:line) 9 | SimpleCov.primary_coverage(:line) 10 | SimpleCov.add_filter('spec') 11 | SimpleCov.start 12 | 13 | require 'bundler/setup' 14 | require 'smart_core/initializer' 15 | require 'pry' 16 | 17 | require_relative 'support/spec_support' 18 | require_relative 'support/meta_scopes' 19 | 20 | RSpec.configure do |config| 21 | Kernel.srand config.seed 22 | config.disable_monkey_patching! 23 | config.filter_run_when_matching :focus 24 | config.order = :random 25 | config.shared_context_metadata_behavior = :apply_to_host_groups 26 | config.expect_with(:rspec) { |c| c.syntax = :expect } 27 | Thread.abort_on_exception = true 28 | end 29 | -------------------------------------------------------------------------------- /spec/support/meta_scopes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.configure do |config| 4 | config.around(:example, :plugin) do |example| 5 | if SpecSupport.test_plugins? 6 | case example.metadata[:plugin] 7 | when :thy_types 8 | require 'thy' 9 | SmartCore::Initializer::Configuration.plugin(:thy_types) 10 | end 11 | example.call 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/support/spec_support.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SpecSupport 4 | class << self 5 | def test_plugins? 6 | !!ENV['TEST_PLUGINS'] 7 | end 8 | end 9 | end 10 | --------------------------------------------------------------------------------