diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c51f5a..78385b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog All notable changes to this project will be documented in this file. +## [0.4.1] - 2020-07-12 +### Added +- Plugin for gem `dry-types` + ## [0.4.0] - 2020-07-12 ### Added - Attribue Definition DSL diff --git a/Gemfile.lock b/Gemfile.lock index 215bc39..a86bddd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -22,15 +22,20 @@ GEM rubocop-rake (= 0.5.1) rubocop-rspec (= 1.42.0) ast (2.4.1) + coderay (1.1.3) concurrent-ruby (1.1.6) diff-lcs (1.4.4) docile (1.3.2) i18n (1.8.3) concurrent-ruby (~> 1.0) + method_source (1.0.0) minitest (5.14.1) parallel (1.19.2) parser (2.7.1.4) ast (~> 2.4.1) + pry (0.13.1) + coderay (~> 1.1) + method_source (~> 1.0) qonfig (0.24.1) rack (2.2.3) rainbow (3.0.0) @@ -91,6 +96,7 @@ PLATFORMS DEPENDENCIES armitage-rubocop (~> 0.87) bundler (~> 2.1) + pry rake (~> 13.0) rspec (~> 3.9) simplecov (~> 0.18) diff --git a/README.md b/README.md index e43e583..493e2cb 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ require 'smart_core/initializer' - [Initialization extension](#initialization-extension) - [Plugins](#plugins) - [thy-types](#plugin-thy-types) + - [dry-types](#plugin-dry-types) - [Roadmap](#roadmap) - [How to run tests](#how-to-run-tests) @@ -280,13 +281,57 @@ User.new('daiver', 'iamdaiver@gmail.com', { admin: true, age: 19 }) User.new('daiver', 'iamdaiver@gmail.com', { age: 17 }) # SmartCore::Initializer::ThyTypeValidationError -# invaldi case (invalid nickname) +# invalid case (invalid nickname) User.new(123, 'test', { admin: true, age: 22 }) # => SmartCore::Initializer::ThyTypeValidationError ``` --- +## Plugin: dry-types + +Support for `Dry::Types` type system ([gem](https://dry-rb.org/gems/dry-types)) + +- install `dry` types (`gem install dry-types`): + +```ruby +gem 'dry-types' +``` + +```shell +bundle install +``` + +- enable `dry_types` plugin: + +```ruby +require 'dry-types' +SmartCore::Initializer::Configuration.plugin(:dry_types) +``` + +- usage: + +```ruby +class User + include SmartCore::Initializer(type_system: :dry_types) + + param :nickname, 'string' + param :email, 'value.text', type_system: :smart_types # mixing with smart_types + option :admin, Dry::Types['bool'], default: false + option :age, Dry::Types['integer'] +end + +# valid case: +User.new('daiver', 'iamdaiver@gmail.com', { admin: true, age: 19 }) +# => new user object + +# invalid case (invalid nickname) +User.new(123, 'test', { admin: true, age: 22 }) +# => SmartCore::Initializer::DryTypeValidationError +``` + +--- + ## Roadmap - Attribue Definition DSL diff --git a/gemfiles/with_external_deps.gemfile b/gemfiles/with_external_deps.gemfile index 1ae0861..b3289a5 100644 --- a/gemfiles/with_external_deps.gemfile +++ b/gemfiles/with_external_deps.gemfile @@ -3,5 +3,6 @@ source 'https://rubygems.org' gem 'thy', '~> 0.1.4' +gem 'dry-types', '~> 1.4.0' gemspec path: '..' diff --git a/gemfiles/with_external_deps.gemfile.lock b/gemfiles/with_external_deps.gemfile.lock index 5c3a53a..33fe9ea 100644 --- a/gemfiles/with_external_deps.gemfile.lock +++ b/gemfiles/with_external_deps.gemfile.lock @@ -22,15 +22,42 @@ GEM rubocop-rake (= 0.5.1) rubocop-rspec (= 1.42.0) ast (2.4.1) + coderay (1.1.3) concurrent-ruby (1.1.6) diff-lcs (1.3) docile (1.3.2) + dry-configurable (0.11.6) + concurrent-ruby (~> 1.0) + dry-core (~> 0.4, >= 0.4.7) + dry-equalizer (~> 0.2) + dry-container (0.7.2) + concurrent-ruby (~> 1.0) + dry-configurable (~> 0.1, >= 0.1.3) + dry-core (0.4.10) + concurrent-ruby (~> 1.0) + dry-equalizer (0.3.0) + dry-inflector (0.2.0) + dry-logic (1.0.8) + concurrent-ruby (~> 1.0) + dry-core (~> 0.2) + dry-equalizer (~> 0.2) + dry-types (1.4.0) + concurrent-ruby (~> 1.0) + dry-container (~> 0.3) + dry-core (~> 0.4, >= 0.4.4) + dry-equalizer (~> 0.3) + dry-inflector (~> 0.1, >= 0.1.2) + dry-logic (~> 1.0, >= 1.0.2) i18n (1.8.3) concurrent-ruby (~> 1.0) + method_source (1.0.0) minitest (5.14.1) parallel (1.19.2) parser (2.7.1.4) ast (~> 2.4.1) + pry (0.13.1) + coderay (~> 1.1) + method_source (~> 1.0) qonfig (0.24.1) rack (2.2.3) rainbow (3.0.0) @@ -92,6 +119,8 @@ PLATFORMS DEPENDENCIES armitage-rubocop (~> 0.87) bundler (~> 2.1) + dry-types (~> 1.4.0) + pry rake (~> 13.0) rspec (~> 3.9) simplecov (~> 0.18) diff --git a/gemfiles/without_external_deps.gemfile.lock b/gemfiles/without_external_deps.gemfile.lock index 2010745..94acc64 100644 --- a/gemfiles/without_external_deps.gemfile.lock +++ b/gemfiles/without_external_deps.gemfile.lock @@ -1,41 +1,46 @@ PATH remote: .. specs: - smart_initializer (0.2.0) + smart_initializer (0.3.2) qonfig (~> 0.24) - smart_engine (~> 0.6) + smart_engine (~> 0.7) smart_types (~> 0.1.0) GEM remote: https://rubygems.org/ specs: - activesupport (6.0.3.1) + activesupport (6.0.3.4) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 0.7, < 2) minitest (~> 5.1) tzinfo (~> 1.1) zeitwerk (~> 2.2, >= 2.2.2) - armitage-rubocop (0.85.0) - rubocop (= 0.85.0) - rubocop-performance (= 1.6.1) - rubocop-rails (= 2.5.2) + armitage-rubocop (0.93.1) + rubocop (= 0.93.1) + rubocop-performance (= 1.8.1) + rubocop-rails (= 2.8.1) rubocop-rake (= 0.5.1) - rubocop-rspec (= 1.39.0) - ast (2.4.0) - concurrent-ruby (1.1.6) + rubocop-rspec (= 1.43.2) + ast (2.4.1) + coderay (1.1.3) + concurrent-ruby (1.1.7) diff-lcs (1.3) docile (1.3.2) - i18n (1.8.3) + i18n (1.8.5) concurrent-ruby (~> 1.0) - minitest (5.14.1) - parallel (1.19.1) - parser (2.7.1.3) - ast (~> 2.4.0) - qonfig (0.24.1) - rack (2.2.2) + method_source (1.0.0) + minitest (5.14.2) + parallel (1.20.1) + parser (2.7.2.0) + ast (~> 2.4.1) + pry (0.13.1) + coderay (~> 1.1) + method_source (~> 1.0) + qonfig (0.25.0) + rack (2.2.3) rainbow (3.0.0) rake (13.0.1) - regexp_parser (1.7.1) + regexp_parser (2.0.0) rexml (3.2.4) rspec (3.9.0) rspec-core (~> 3.9.0) @@ -50,47 +55,49 @@ GEM diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.9.0) rspec-support (3.9.3) - rubocop (0.85.0) + rubocop (0.93.1) parallel (~> 1.10) - parser (>= 2.7.0.1) + parser (>= 2.7.1.5) rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 1.7) + regexp_parser (>= 1.8) rexml - rubocop-ast (>= 0.0.3) + rubocop-ast (>= 0.6.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 1.4.0, < 2.0) - rubocop-ast (0.0.3) - parser (>= 2.7.0.1) - rubocop-performance (1.6.1) - rubocop (>= 0.71.0) - rubocop-rails (2.5.2) - activesupport + rubocop-ast (1.2.0) + parser (>= 2.7.1.5) + rubocop-performance (1.8.1) + rubocop (>= 0.87.0) + rubocop-ast (>= 0.4.0) + rubocop-rails (2.8.1) + activesupport (>= 4.2.0) rack (>= 1.1) - rubocop (>= 0.72.0) + rubocop (>= 0.87.0) rubocop-rake (0.5.1) rubocop - rubocop-rspec (1.39.0) - rubocop (>= 0.68.1) + rubocop-rspec (1.43.2) + rubocop (~> 0.87) ruby-progressbar (1.10.1) simplecov (0.18.5) docile (~> 1.1) simplecov-html (~> 0.11) simplecov-html (0.12.2) - smart_engine (0.6.0) + smart_engine (0.8.0) smart_types (0.1.0) smart_engine (~> 0.6) thread_safe (0.3.6) - tzinfo (1.2.7) + tzinfo (1.2.8) thread_safe (~> 0.1) unicode-display_width (1.7.0) - zeitwerk (2.3.0) + zeitwerk (2.4.1) PLATFORMS ruby DEPENDENCIES - armitage-rubocop (~> 0.85) + armitage-rubocop (~> 0.87) bundler (~> 2.1) + pry rake (~> 13.0) rspec (~> 3.9) simplecov (~> 0.18) diff --git a/lib/smart_core/initializer/constructor.rb b/lib/smart_core/initializer/constructor.rb index d8a729b..bc0d5c2 100644 --- a/lib/smart_core/initializer/constructor.rb +++ b/lib/smart_core/initializer/constructor.rb @@ -117,7 +117,7 @@ def initialize_parameters(instance) parameter_definitions = Hash[klass.__params__.zip(parameters)] parameter_definitions.each_pair do |attribute, parameter_value| - if !attribute.type.valid?(parameter_value) && attribute.cast? + if attribute.type.force_cast? || (!attribute.type.valid?(parameter_value) && attribute.cast?) parameter_value = attribute.type.cast(parameter_value) end @@ -138,7 +138,7 @@ def initialize_options(instance) klass.__options__.each do |attribute| option_value = options.fetch(attribute.name) { attribute.default } - if !attribute.type.valid?(option_value) && attribute.cast? + if attribute.type.force_cast? || (!attribute.type.valid?(option_value) && attribute.cast?) option_value = attribute.type.cast(option_value) end diff --git a/lib/smart_core/initializer/plugins.rb b/lib/smart_core/initializer/plugins.rb index 1912095..6b10cd1 100644 --- a/lib/smart_core/initializer/plugins.rb +++ b/lib/smart_core/initializer/plugins.rb @@ -8,10 +8,12 @@ module SmartCore::Initializer::Plugins require_relative 'plugins/registry_interface.rb' require_relative 'plugins/access_mixin' require_relative 'plugins/thy_types' + require_relative 'plugins/dry_types' # @since 0.1.0 extend SmartCore::Initializer::Plugins::RegistryInterface # @since 0.1.0 register_plugin('thy_types', SmartCore::Initializer::Plugins::ThyTypes) + register_plugin('dry_types', SmartCore::Initializer::Plugins::DryTypes) end diff --git a/lib/smart_core/initializer/plugins/dry_types.rb b/lib/smart_core/initializer/plugins/dry_types.rb new file mode 100644 index 0000000..c47133b --- /dev/null +++ b/lib/smart_core/initializer/plugins/dry_types.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +# @api private +# @since 0.4.1 +class SmartCore::Initializer::Plugins::DryTypes < SmartCore::Initializer::Plugins::Abstract + class << self + # @return [void] + # + # @api private + # @since 0.4.1 + def install! + raise( + SmartCore::Initializer::UnresolvedPluginDependencyError, + '::Dry::Types does not exist or "dry-types" gem is not loaded' + ) unless const_defined?('::Dry::Types') + + # NOTE: require necessary dependencies + require 'date' + + # NOTE: add dry-types type system implementation + require_relative 'dry_types/errors' + require_relative 'dry_types/dry_types' + + # NOTE: register dry-types type system + SmartCore::Initializer::TypeSystem.register( + :dry_types, SmartCore::Initializer::TypeSystem::DryTypes + ) + end + end +end diff --git a/lib/smart_core/initializer/plugins/dry_types/dry_types.rb b/lib/smart_core/initializer/plugins/dry_types/dry_types.rb new file mode 100644 index 0000000..0605764 --- /dev/null +++ b/lib/smart_core/initializer/plugins/dry_types/dry_types.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module SmartCore::Initializer::TypeSystem + # @api public + # @since 0.4.1 + class DryTypes < Interop + require_relative 'dry_types/abstract_factory' + require_relative 'dry_types/operation' + + type_alias(:any, AbstractFactory.generic_type_object) + type_alias(:nil, ::Dry::Types["nil"]) + type_alias(:string, ::Dry::Types["string"]) + type_alias(:symbol, ::Dry::Types["symbol"]) + type_alias(:integer, ::Dry::Types["integer"]) + type_alias(:float, ::Dry::Types["float"]) + type_alias(:decimal, ::Dry::Types["decimal"]) + type_alias(:boolean, ::Dry::Types["bool"]) + type_alias(:time, ::Dry::Types["time"]) + type_alias(:date_time, ::Dry::Types["date_time"]) + type_alias(:array, ::Dry::Types["array"]) + type_alias(:hash, ::Dry::Types["hash"]) + end +end diff --git a/lib/smart_core/initializer/plugins/dry_types/dry_types/abstract_factory.rb b/lib/smart_core/initializer/plugins/dry_types/dry_types/abstract_factory.rb new file mode 100644 index 0000000..245f9b1 --- /dev/null +++ b/lib/smart_core/initializer/plugins/dry_types/dry_types/abstract_factory.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +module SmartCore::Initializer::TypeSystem + # @api private + # @since 0.4.1 + class DryTypes::AbstractFactory < Interop::AbstractFactory + # @return [Dry::Types::AnyClass] + # + # @api private + # @since 0.4.1 + GENERIC_TYPE = ::Dry::Types["any"] + + class << self + # @param type [#valid?, #call] + # @return [void] + # + # @raise [SmartCore::Initializer::IncorrectTypeObjectError] + # + # @api private + # @since 0.4.1 + def prevent_incompatible_type!(type) + unless type.respond_to?(:valid?) || type.respond_to?(:call) + raise( + SmartCore::Initializer::IncorrectTypeObjectError, + 'Incorrect Dry::Type primitive ' \ + '(type object should respond to :valid? and :call methods)' + ) + end + end + + # @param type [#valid?, #call] + # @return [SmartCore::Initializer::TypeSystem::DryTypes::Operation::Valid] + # + # @api private + # @since 0.4.1 + def build_valid_operation(type) + DryTypes::Operation::Valid.new(type) + end + + # @return [#valid?, #call] + # + # @api private + # @since 0.4.1 + def generic_type_object + GENERIC_TYPE + end + + # @param type [#valid?, #call] + # @return [SmartCore::Initializer::TypeSystem::DryTypes::Operation::Validate] + # + # @api private + # @since 0.4.1 + def build_validate_operation(type) + DryTypes::Operation::Validate.new(type) + end + + # @param type [#valid?, #call] + # @return [SmartCore::Initializer::TypeSystem::DryTypes::Operation::Cast] + # + # @api private + # @since 0.4.1 + def build_cast_operation(type) + DryTypes::Operation::Cast.new(type) + end + + # @param type [Any] + # @return [Boolean] + # + # @api private + # @since 0.4.1 + def force_cast_for?(type) + type.is_a?(Dry::Types::Constructor) + end + + # @param valid_op [SmartCore::Initializer::TypeSystem::DryTypes::Operation::Valid] + # @param valid_op [SmartCore::Initializer::TypeSystem::DryTypes::Operation::Validate] + # @param valid_op [SmartCore::Initializer::TypeSystem::DryTypes::Operation::Cast] + # @param force_cast [Boolean] + # @return [SmartCore::Initializer::TypeSystem::DryTypes] + # + # @api private + # @since 0.4.1 + def build_interop(valid_op, validate_op, cast_op, force_cast) + DryTypes.new(valid_op, validate_op, cast_op, force_cast) + end + end + end +end diff --git a/lib/smart_core/initializer/plugins/dry_types/dry_types/operation.rb b/lib/smart_core/initializer/plugins/dry_types/dry_types/operation.rb new file mode 100644 index 0000000..770fffd --- /dev/null +++ b/lib/smart_core/initializer/plugins/dry_types/dry_types/operation.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module SmartCore::Initializer::TypeSystem + # @api private + # @since 0.4.1 + module DryTypes::Operation + require_relative 'operation/base' + require_relative 'operation/valid' + require_relative 'operation/validate' + require_relative 'operation/cast' + end +end diff --git a/lib/smart_core/initializer/plugins/dry_types/dry_types/operation/base.rb b/lib/smart_core/initializer/plugins/dry_types/dry_types/operation/base.rb new file mode 100644 index 0000000..b77d3c0 --- /dev/null +++ b/lib/smart_core/initializer/plugins/dry_types/dry_types/operation/base.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module SmartCore::Initializer::TypeSystem + # @abstract + # @api private + # @since 0.4.1 + class DryTypes::Operation::Base < Interop::Operation + end +end diff --git a/lib/smart_core/initializer/plugins/dry_types/dry_types/operation/cast.rb b/lib/smart_core/initializer/plugins/dry_types/dry_types/operation/cast.rb new file mode 100644 index 0000000..a923e07 --- /dev/null +++ b/lib/smart_core/initializer/plugins/dry_types/dry_types/operation/cast.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module SmartCore::Initializer::TypeSystem::DryTypes::Operation + # @api private + # @since 0.4.1 + class Cast < Base + # @param value [Any] + # @return [Any] + # + # @api private + # @since 0.4.1 + def call(value) + type[value] + rescue Dry::Types::CoercionError + value + end + end +end diff --git a/lib/smart_core/initializer/plugins/dry_types/dry_types/operation/valid.rb b/lib/smart_core/initializer/plugins/dry_types/dry_types/operation/valid.rb new file mode 100644 index 0000000..a2f3708 --- /dev/null +++ b/lib/smart_core/initializer/plugins/dry_types/dry_types/operation/valid.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module SmartCore::Initializer::TypeSystem::DryTypes::Operation + # @api private + # @since 0.4.1 + class Valid < Base + # @param value [Any] + # @return [Boolean] + # + # @api private + # @since 0.4.1 + def call(value) + type.valid?(value) + end + end +end diff --git a/lib/smart_core/initializer/plugins/dry_types/dry_types/operation/validate.rb b/lib/smart_core/initializer/plugins/dry_types/dry_types/operation/validate.rb new file mode 100644 index 0000000..dae7422 --- /dev/null +++ b/lib/smart_core/initializer/plugins/dry_types/dry_types/operation/validate.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module SmartCore::Initializer::TypeSystem::DryTypes::Operation + # @api private + # @since 0.4.1 + class Validate < Base + # @param value [Any] + # @return [void] + # + # @api private + # @since 0.4.1 + def call(value) + raise( + SmartCore::Initializer::DryTypeValidationError, + "Dry validation error: (get #{value.inspect} for type #{type.inspect}" + ) unless type.valid?(value) + end + end +end diff --git a/lib/smart_core/initializer/plugins/dry_types/errors.rb b/lib/smart_core/initializer/plugins/dry_types/errors.rb new file mode 100644 index 0000000..1d350d3 --- /dev/null +++ b/lib/smart_core/initializer/plugins/dry_types/errors.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module SmartCore::Initializer + # @api public + # @since 0.4.1 + DryTypeValidationError = Class.new(SmartCore::Initializer::Error) +end diff --git a/lib/smart_core/initializer/plugins/thy_types/thy_types/abstract_factory.rb b/lib/smart_core/initializer/plugins/thy_types/thy_types/abstract_factory.rb index d0986e2..6419e65 100644 --- a/lib/smart_core/initializer/plugins/thy_types/thy_types/abstract_factory.rb +++ b/lib/smart_core/initializer/plugins/thy_types/thy_types/abstract_factory.rb @@ -66,12 +66,13 @@ def build_cast_operation(type) # @param valid_op [SmartCore::Initializer::TypeSystem::ThyTypes::Operation::Valid] # @param valid_op [SmartCore::Initializer::TypeSystem::ThyTypes::Operation::Validate] # @param valid_op [SmartCore::Initializer::TypeSystem::ThyTypes::Operation::Cast] + # @param force_cast [Boolean] # @return [SmartCore::Initializer::TypeSystem::ThyTypes] # # @api private # @since 0.1.0 - def build_interop(valid_op, validate_op, cast_op) - ThyTypes.new(valid_op, validate_op, cast_op) + def build_interop(valid_op, validate_op, cast_op, force_cast) + ThyTypes.new(valid_op, validate_op, cast_op, force_cast) end end end diff --git a/lib/smart_core/initializer/type_system/interop.rb b/lib/smart_core/initializer/type_system/interop.rb index 15ab186..cf1df27 100644 --- a/lib/smart_core/initializer/type_system/interop.rb +++ b/lib/smart_core/initializer/type_system/interop.rb @@ -48,10 +48,11 @@ def prevent_incompatible_type!(type_object) # # @api private # @since 0.1.0 - def initialize(valid_op, validate_op, cast_op) + def initialize(valid_op, validate_op, cast_op, force_cast) @valid_op = valid_op @validate_op = validate_op @cast_op = cast_op + @force_cast = force_cast end # @param value [Any] @@ -81,6 +82,14 @@ def cast(value) cast_op.call(value) end + # @return [Boolean] + # + # @api private + # @since 0.4.1 + def force_cast? + @force_cast + end + private # @return [SmartCore::Initializer::TypeSystem::Interop::Operation] diff --git a/lib/smart_core/initializer/type_system/interop/abstract_factory.rb b/lib/smart_core/initializer/type_system/interop/abstract_factory.rb index f138798..f1847b4 100644 --- a/lib/smart_core/initializer/type_system/interop/abstract_factory.rb +++ b/lib/smart_core/initializer/type_system/interop/abstract_factory.rb @@ -17,7 +17,9 @@ def create(type) validate_op = build_validate_operation(type) cast_op = build_cast_operation(type) - build_interop(valid_op, validate_op, cast_op) + force_cast = force_cast_for?(type) + + build_interop(valid_op, validate_op, cast_op, force_cast) end # @return [Any] @@ -58,13 +60,23 @@ def build_validate_operation(type); end # @since 0.1.0 def build_cast_operation(type); end + # @param type [Any] + # @return [Boolean] + # + # @api private + # @since 0.4.1 + def force_cast_for?(type) + false + end + # @param valid_op [SmartCore::Initializer::TypeSystem::Interop::Operation] # @param validate_op [SmartCore::Initializer::TypeSystem::Interop::Operation] # @param cast_op [SmartCore::Initializer::TypeSystem::Interop::Operation] + # @param force_cast [Boolean] # @return [SmartCore::Initializer::TypeSystem::Interop] # # @api private # @since 0.1.0 - def build_interop(valid_op, validate_op, cast_op); end + def build_interop(valid_op, validate_op, cast_op, force_cast); end end end diff --git a/lib/smart_core/initializer/type_system/smart_types/abstract_factory.rb b/lib/smart_core/initializer/type_system/smart_types/abstract_factory.rb index b7ac93c..4198525 100644 --- a/lib/smart_core/initializer/type_system/smart_types/abstract_factory.rb +++ b/lib/smart_core/initializer/type_system/smart_types/abstract_factory.rb @@ -60,12 +60,13 @@ def build_cast_operation(type) # @param valid_op [SmartCore::Initializer::TypeSystem::SmartTypes::Operation::Valid] # @param valid_op [SmartCore::Initializer::TypeSystem::SmartTypes::Operation::Validate] # @param valid_op [SmartCore::Initializer::TypeSystem::SmartTypes::Operation::Cast] + # @param force_cast [Boolean] # @return [SmartCore::Initializer::TypeSystem::SmartTypes] # # @api private # @since 0.1.0 - def build_interop(valid_op, validate_op, cast_op) - SmartTypes.new(valid_op, validate_op, cast_op) + def build_interop(valid_op, validate_op, cast_op, force_cast) + SmartTypes.new(valid_op, validate_op, cast_op, force_cast) end end end diff --git a/smart_initializer.gemspec b/smart_initializer.gemspec index 7c9795d..d97a22c 100644 --- a/smart_initializer.gemspec +++ b/smart_initializer.gemspec @@ -39,4 +39,5 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'rspec', '~> 3.9' spec.add_development_dependency 'armitage-rubocop', '~> 0.87' spec.add_development_dependency 'simplecov', '~> 0.18' + spec.add_development_dependency 'pry' end diff --git a/spec/features/plugins/dry_types_spec.rb b/spec/features/plugins/dry_types_spec.rb new file mode 100644 index 0000000..7d6a538 --- /dev/null +++ b/spec/features/plugins/dry_types_spec.rb @@ -0,0 +1,168 @@ +# frozen_string_literal: true + +RSpec.describe 'Plugins: dry_types', plugin: :dry_types do + specify 'aliases' do + expect(SmartCore::Initializer::TypeSystem::DryTypes.type_aliases).to contain_exactly( + 'any', + 'nil', + 'string', + 'symbol', + 'integer', + 'float', + 'decimal', + 'boolean', + 'time', + 'date_time', + 'array', + 'hash' + ) + + expect(SmartCore::Initializer::TypeSystem::DryTypes.type_from_alias('any')).to eq( + Dry::Types["any"] + ) + expect(SmartCore::Initializer::TypeSystem::DryTypes.type_from_alias('nil')).to eq( + Dry::Types["nil"] + ) + expect(SmartCore::Initializer::TypeSystem::DryTypes.type_from_alias('string')).to eq( + Dry::Types["string"] + ) + expect(SmartCore::Initializer::TypeSystem::DryTypes.type_from_alias('symbol')).to eq( + Dry::Types["symbol"] + ) + expect(SmartCore::Initializer::TypeSystem::DryTypes.type_from_alias('integer')).to eq( + Dry::Types["integer"] + ) + expect(SmartCore::Initializer::TypeSystem::DryTypes.type_from_alias('float')).to eq( + Dry::Types["float"] + ) + expect(SmartCore::Initializer::TypeSystem::DryTypes.type_from_alias('decimal')).to eq( + Dry::Types["decimal"] + ) + expect(SmartCore::Initializer::TypeSystem::DryTypes.type_from_alias('boolean')).to eq( + Dry::Types["bool"] + ) + expect(SmartCore::Initializer::TypeSystem::DryTypes.type_from_alias('time')).to eq( + Dry::Types["time"] + ) + expect(SmartCore::Initializer::TypeSystem::DryTypes.type_from_alias('date_time')).to eq( + Dry::Types["date_time"] + ) + expect(SmartCore::Initializer::TypeSystem::DryTypes.type_from_alias('array')).to eq( + Dry::Types["array"] + ) + expect(SmartCore::Initializer::TypeSystem::DryTypes.type_from_alias('hash')).to eq( + Dry::Types["hash"] + ) + end + + describe 'usage' do + specify 'initializer with dry-types' do + data_klass = Class.new do + include SmartCore::Initializer(type_system: :dry_types) + + param :login, Dry::Types['string'] + param :password, 'string' + + option :age, 'integer' + option :as_admin, Dry::Types['bool'] + end + + instance = data_klass.new('vasia', 'pupkin', { age: 23, as_admin: true }) + + expect(instance.login).to eq('vasia') + expect(instance.password).to eq('pupkin') + expect(instance.age).to eq(23) + expect(instance.as_admin).to eq(true) + end + + specify 'mixing dry-types with smart-types' do + data_klass = Class.new do + include SmartCore::Initializer(type_system: :dry_types) + + param :nickname, SmartCore::Types::Value::String, type_system: :smart_types + param :password, Dry::Types['string'] + + option :balance, 'value.float', type_system: :smart_types + option :version, 'float' # dry-types + end + + instance = data_klass.new('over', 'watch', { balance: 12.34, version: 15.707 }) + + expect(instance.nickname).to eq('over') + expect(instance.password).to eq('watch') + expect(instance.balance).to eq(12.34) + expect(instance.version).to eq(15.707) + end + + specify 'support for type casting with coercible types' do + mod = Module.new do + include Dry.Types(:coercible) + end + data_class = Class.new do + include SmartCore::Initializer(type_system: :dry_types) + param :nickname, mod::Coercible::String + end + + instance = data_class.new(123) + + expect(instance.nickname).to eq('123') + end + + specify 'validation' do + data_klass = Class.new do + include SmartCore::Initializer(type_system: :dry_types) + param :nickname, 'string' + option :age, 'integer' + end + + # NOTE: invalid + expect { data_klass.new('test', { age: '123' }) }.to raise_error( + SmartCore::Initializer::DryTypeValidationError + ) + + # NOTE: invalid + expect { data_klass.new(123, { age: 123 }) }.to raise_error( + SmartCore::Initializer::DryTypeValidationError + ) + + # NOTE: valid + expect { data_klass.new('123', { age: 123 }) }.not_to raise_error + end + + specify 'validation check for smart-types mixed with dry-types' do + data_klass = Class.new do + include SmartCore::Initializer(type_system: :dry_types) + + param :nickname, 'string', type_system: :smart_types + param :email, 'string' + end + + expect { data_klass.new(123, 'iamdaiver@gmail.com') }.to raise_error( + SmartCore::Types::TypeError + ) + + expect { data_klass.new('123', :email) }.to raise_error( + SmartCore::Initializer::DryTypeValidationError + ) + + expect { data_klass.new('123', 'iamdaiver@gmail.com') }.not_to raise_error + end + + specify 'support for the common attribute definition functionality' do + data_klass = Class.new do + include SmartCore::Initializer(type_system: :dry_types) + + option :email, 'string', default: 'no@email.com', finalize: -> (value) { "1#{value}" } + option :admin, 'boolean', default: false, finalize: -> (value) { true } + end + + instance = data_klass.new + expect(instance.email).to eq('1no@email.com') + expect(instance.admin).to eq(true) + + instance = data_klass.new(email: 'iamdaiver@gmail.com', admin: false) + expect(instance.email).to eq('1iamdaiver@gmail.com') + expect(instance.admin).to eq(true) + end + end +end diff --git a/spec/features/plugins/thy_types_spec.rb b/spec/features/plugins/thy_types_spec.rb index 7043b4e..82012ba 100644 --- a/spec/features/plugins/thy_types_spec.rb +++ b/spec/features/plugins/thy_types_spec.rb @@ -1,11 +1,6 @@ # frozen_string_literal: true RSpec.describe 'Plugins: thy_types', plugin: :thy_types do - before do - require 'thy' - SmartCore::Initializer::Configuration.plugin(:thy_types) - end - specify 'aliases' do expect(SmartCore::Initializer::TypeSystem::ThyTypes.type_aliases).to contain_exactly( 'any', diff --git a/spec/support/meta_scopes.rb b/spec/support/meta_scopes.rb index 8c5ae3c..5edb985 100644 --- a/spec/support/meta_scopes.rb +++ b/spec/support/meta_scopes.rb @@ -7,6 +7,9 @@ when :thy_types require 'thy' SmartCore::Initializer::Configuration.plugin(:thy_types) + when :dry_types + require "dry-types" + SmartCore::Initializer::Configuration.plugin(:dry_types) end example.call end