From 420b2f90ed6e3aba28c5a5a352d890b029391b72 Mon Sep 17 00:00:00 2001 From: AnotherRegularDude Date: Sat, 14 Dec 2024 08:42:42 +0300 Subject: [PATCH 01/21] Add kind of factory for different initializers --- .rubocop.yml | 2 +- Gemfile | 2 + Gemfile.lock | 153 +++++++++++++++++++------------------ lib/resol/configuration.rb | 6 +- lib/resol/initializers.rb | 48 ++++++++++++ lib/resol/plugins.rb | 46 +++++++++++ lib/resol/service.rb | 12 ++- resol.gemspec | 3 +- 8 files changed, 190 insertions(+), 82 deletions(-) create mode 100644 lib/resol/initializers.rb create mode 100644 lib/resol/plugins.rb diff --git a/.rubocop.yml b/.rubocop.yml index c118ba5..e6b375e 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -3,7 +3,7 @@ inherit_gem: AllCops: DisplayCopNames: true - TargetRubyVersion: 2.7 + TargetRubyVersion: 3.2 Naming/MethodParameterName: AllowedNames: ["x", "y", "z"] diff --git a/Gemfile b/Gemfile index a30e2f2..57bab9c 100644 --- a/Gemfile +++ b/Gemfile @@ -6,9 +6,11 @@ gemspec gem "bundler-audit" gem "ci-helper" +gem "dry-initializer" gem "pry" gem "rake" gem "rspec" gem "rubocop-config-umbrellio" gem "simplecov" gem "simplecov-lcov" +gem "smart_initializer" diff --git a/Gemfile.lock b/Gemfile.lock index db5ee30..531abc5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,117 +2,123 @@ PATH remote: . specs: resol (0.9.0) + dry-initializer (~> 3.1) smart_initializer (~> 0.7) GEM remote: https://rubygems.org/ specs: - activesupport (7.1.2) + activesupport (8.0.1) base64 + benchmark (>= 0.3) bigdecimal - concurrent-ruby (~> 1.0, >= 1.0.2) + concurrent-ruby (~> 1.0, >= 1.3.1) connection_pool (>= 2.2.5) drb i18n (>= 1.6, < 2) + logger (>= 1.4.2) minitest (>= 5.1) - mutex_m - tzinfo (~> 2.0) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) ast (2.4.2) base64 (0.2.0) - bigdecimal (3.1.4) - bundler-audit (0.9.1) + benchmark (0.4.0) + bigdecimal (3.1.8) + bundler-audit (0.9.2) bundler (>= 1.2.0, < 3) thor (~> 1.0) - ci-helper (0.5.0) - colorize (~> 0.8) - dry-inflector (~> 0.2) - umbrellio-sequel-plugins (~> 0.4) + ci-helper (0.7.0) + colorize (~> 1.1) + dry-inflector (~> 1.0) + umbrellio-sequel-plugins (~> 0.14) coderay (1.1.3) - colorize (0.8.1) - concurrent-ruby (1.2.2) + colorize (1.1.0) + concurrent-ruby (1.3.4) connection_pool (2.4.1) - diff-lcs (1.5.0) - docile (1.4.0) - drb (2.2.0) - ruby2_keywords - dry-inflector (0.3.0) - i18n (1.14.1) + diff-lcs (1.5.1) + docile (1.4.1) + drb (2.2.1) + dry-inflector (1.1.0) + dry-initializer (3.1.1) + i18n (1.14.6) concurrent-ruby (~> 1.0) - json (2.6.3) - method_source (1.0.0) - minitest (5.20.0) - mutex_m (0.2.0) - parallel (1.23.0) - parser (3.2.2.4) + json (2.9.0) + language_server-protocol (3.17.0.3) + logger (1.6.3) + method_source (1.1.0) + minitest (5.25.4) + parallel (1.26.3) + parser (3.3.6.0) ast (~> 2.4.1) racc - pry (0.14.2) + pry (0.15.0) coderay (~> 1.1) method_source (~> 1.0) - qonfig (0.28.0) - racc (1.7.3) - rack (3.0.8) + qonfig (0.29.0) + racc (1.8.1) + rack (3.1.8) rainbow (3.1.1) - rake (13.1.0) - regexp_parser (2.8.2) - rexml (3.2.6) - rspec (3.12.0) - rspec-core (~> 3.12.0) - rspec-expectations (~> 3.12.0) - rspec-mocks (~> 3.12.0) - rspec-core (3.12.2) - rspec-support (~> 3.12.0) - rspec-expectations (3.12.3) + rake (13.2.1) + regexp_parser (2.9.3) + rspec (3.13.0) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) + rspec-core (3.13.2) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.3) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.12.0) - rspec-mocks (3.12.6) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.2) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.12.0) - rspec-support (3.12.1) - rubocop (1.50.2) + rspec-support (~> 3.13.0) + rspec-support (3.13.2) + rubocop (1.66.1) json (~> 2.3) + language_server-protocol (>= 3.17.0) parallel (~> 1.10) - parser (>= 3.2.0.0) + parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 1.8, < 3.0) - rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.28.0, < 2.0) + regexp_parser (>= 2.4, < 3.0) + rubocop-ast (>= 1.32.2, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.30.0) - parser (>= 3.2.1.0) - rubocop-capybara (2.19.0) - rubocop (~> 1.41) - rubocop-config-umbrellio (1.50.0.85) - rubocop (~> 1.50.0) - rubocop-performance (~> 1.17.0) - rubocop-rails (~> 2.19.0) + rubocop-ast (1.37.0) + parser (>= 3.3.1.0) + rubocop-config-umbrellio (1.66.0.99) + rubocop (~> 1.66.0) + rubocop-factory_bot (~> 2.26.0) + rubocop-performance (~> 1.22.0) + rubocop-rails (~> 2.26.0) rubocop-rake (~> 0.6.0) - rubocop-rspec (~> 2.20.0) + rubocop-rspec (~> 3.0.0) rubocop-sequel (~> 0.3.3) - rubocop-performance (1.17.1) - rubocop (>= 1.7.0, < 2.0) - rubocop-ast (>= 0.4.0) - rubocop-rails (2.19.1) + rubocop-factory_bot (2.26.1) + rubocop (~> 1.61) + rubocop-performance (1.22.1) + rubocop (>= 1.48.1, < 2.0) + rubocop-ast (>= 1.31.1, < 2.0) + rubocop-rails (2.26.2) activesupport (>= 4.2.0) rack (>= 1.1) - rubocop (>= 1.33.0, < 2.0) + rubocop (>= 1.52.0, < 2.0) + rubocop-ast (>= 1.31.1, < 2.0) rubocop-rake (0.6.0) rubocop (~> 1.0) - rubocop-rspec (2.20.0) - rubocop (~> 1.33) - rubocop-capybara (~> 2.17) - rubocop-sequel (0.3.4) + rubocop-rspec (3.0.5) + rubocop (~> 1.61) + rubocop-sequel (0.3.7) rubocop (~> 1.0) ruby-progressbar (1.13.0) - ruby2_keywords (0.0.5) - sequel (5.74.0) + securerandom (0.4.0) + sequel (5.87.0) bigdecimal simplecov (0.22.0) docile (~> 1.1) simplecov-html (~> 0.11) simplecov_json_formatter (~> 0.1) - simplecov-html (0.12.3) + simplecov-html (0.13.1) simplecov-lcov (0.8.0) simplecov_json_formatter (0.1.4) smart_engine (0.17.0) @@ -122,14 +128,13 @@ GEM smart_types (~> 0.8) smart_types (0.8.0) smart_engine (~> 0.11) - symbiont-ruby (0.7.0) - thor (1.3.0) + thor (1.3.2) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - umbrellio-sequel-plugins (0.14.0.189) + umbrellio-sequel-plugins (0.17.0) sequel - symbiont-ruby - unicode-display_width (2.5.0) + unicode-display_width (2.6.0) + uri (1.0.2) PLATFORMS arm64-darwin-21 @@ -142,6 +147,7 @@ PLATFORMS DEPENDENCIES bundler-audit ci-helper + dry-initializer pry rake resol! @@ -149,6 +155,7 @@ DEPENDENCIES rubocop-config-umbrellio simplecov simplecov-lcov + smart_initializer BUNDLED WITH 2.4.10 diff --git a/lib/resol/configuration.rb b/lib/resol/configuration.rb index 378047f..3a7f8d7 100644 --- a/lib/resol/configuration.rb +++ b/lib/resol/configuration.rb @@ -25,13 +25,13 @@ def return_engine=(engine) attr_accessor :smartcore_config - def method_missing(meth, *args, &block) + def method_missing(meth, *, &) # rubocop:disable Style/SafeNavigation if smartcore_config && smartcore_config.respond_to?(meth) # rubocop:enable Style/SafeNavigation - smartcore_config.__send__(meth, *args, &block) + smartcore_config.__send__(meth, *, &) else - super(meth, *args, &block) + super end end diff --git a/lib/resol/initializers.rb b/lib/resol/initializers.rb new file mode 100644 index 0000000..7fbadbe --- /dev/null +++ b/lib/resol/initializers.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module Resol + module Initializers + extend self + + INITIALIZER_ANCESTOR_REGEX = /(Dry|SmartCore)::Initializer/ + + def apply!(service_class, initializer_name) + validate_state!(service_class) + + case initializer_name + when :smartcore + require "smart_core/initializer" + service_class.include(Object.const_get("SmartCore::Initializer")) + when :dry + require "dry/initializer" + service_class.extend(Object.const_get("Dry::Initializer")) + else + raise ArgumentError, "unknown initializer #{initializer_name}" + end + + (self.applied_classes ||= []) << service_class.name + end + + private + + attr_accessor :applied_classes + + def validate_state!(service_class) + applied_parent = nil + service_class.ancestors.any? { |klass| klass.name.start_with?(INITIALIZER_ANCESTOR_REGEX) } + + loop do + applied_parent = service_class.superclass or break + + break if applied_classes.key?(applied_parent.name) + end + + if applied_parent.nil? + err_message = "#{service_class.name} or his superclasses manually include initializer dsl" + raise ArgumentError, err_message + else + raise ArgumentError, "initializer dsl already applied to #{applied_parent.name}" + end + end + end +end diff --git a/lib/resol/plugins.rb b/lib/resol/plugins.rb new file mode 100644 index 0000000..0aa771a --- /dev/null +++ b/lib/resol/plugins.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require "pathname" + +module Resol + module Plugins + class Manager + include Singleton + + def initialize + self.patched_class = Resol::Service + self.plugins = [] + self.plugin_lib_path = PathName("resol/plugins") + end + + def plugin(plugin_name, ...) + plugin_module = find_plugin_module(plugin_name) + plugin_module.apply(target_class, ...) if plugin_module.respond_to?(:apply) + + if defined?(plugin_module::InstanceMethods) + target_class.include(plugin_module::InstanceMethods) + end + + if defined?(plugin_module::ClassMethods) + target_class.extend(plugin_module::ClassMethods) + end + + plugins << plugin_name + end + + private + + attr_accessor :target_class, :plugins, :plugin_lib_path + + def find_plugin_module(plugin_name) + require plugin_lib_path.join(plugin_name) + rescue LoadError, NameError => e + raise "Failed to load plugin '#{plugin_name}': #{e.message}" + end + + def camel_case(string) + string.split("_").map(&:capitalize).join + end + end + end +end diff --git a/lib/resol/service.rb b/lib/resol/service.rb index 5132dc5..a462e04 100644 --- a/lib/resol/service.rb +++ b/lib/resol/service.rb @@ -3,6 +3,7 @@ require_relative "builder" require_relative "callbacks" require_relative "result" +require_relative "initializers" module Resol class Service @@ -27,7 +28,6 @@ def message end end - include SmartCore::Initializer include Resol::Builder include Resol::Callbacks @@ -39,13 +39,17 @@ def inherited(klass) super end - def call(*args, **kwargs, &block) - service = build(*args, **kwargs) + def use_initializer!(initializer_lib) + Resol::Initializers.apply!(self, initializer_lib) + end + + def call(*, **, &) + service = build(*, **) result = return_engine.wrap_call(service) do service.instance_variable_set(:@__performing__, true) __run_callbacks__(service) - service.call(&block) + service.call(&) end if return_engine.uncaught_call?(result) diff --git a/resol.gemspec b/resol.gemspec index eb29428..91b6e24 100644 --- a/resol.gemspec +++ b/resol.gemspec @@ -13,9 +13,10 @@ Gem::Specification.new do |spec| spec.homepage = "https://github.com/umbrellio/resol" spec.license = "MIT" - spec.required_ruby_version = Gem::Requirement.new(">= 2.7.0") + spec.required_ruby_version = Gem::Requirement.new(">= 3.2.0") spec.files = `git ls-files -z`.split("\x0").reject { |f| f.include?("spec") } spec.require_paths = ["lib"] + spec.add_dependency "dry-initializer", "~> 3.1" spec.add_dependency "smart_initializer", "~> 0.7" end From 21128f43629f2745bd50fa794a31c70c258d2661 Mon Sep 17 00:00:00 2001 From: AnotherRegularDude Date: Sat, 14 Dec 2024 08:59:09 +0300 Subject: [PATCH 02/21] Small fixes and specs fix --- lib/resol/initializers.rb | 21 ++++++++------------- spec/building_spec.rb | 2 +- spec/service_spec.rb | 18 +++++++++--------- spec/spec_helper.rb | 4 ++++ 4 files changed, 22 insertions(+), 23 deletions(-) diff --git a/lib/resol/initializers.rb b/lib/resol/initializers.rb index 7fbadbe..0548c0e 100644 --- a/lib/resol/initializers.rb +++ b/lib/resol/initializers.rb @@ -4,9 +4,10 @@ module Resol module Initializers extend self - INITIALIZER_ANCESTOR_REGEX = /(Dry|SmartCore)::Initializer/ + MOD_MATCH_REGEX = /(Dry|SmartCore)::Initializer/ def apply!(service_class, initializer_name) + self.applied_classes ||= [] validate_state!(service_class) case initializer_name @@ -19,8 +20,6 @@ def apply!(service_class, initializer_name) else raise ArgumentError, "unknown initializer #{initializer_name}" end - - (self.applied_classes ||= []) << service_class.name end private @@ -28,21 +27,17 @@ def apply!(service_class, initializer_name) attr_accessor :applied_classes def validate_state!(service_class) - applied_parent = nil - service_class.ancestors.any? { |klass| klass.name.start_with?(INITIALIZER_ANCESTOR_REGEX) } + applied_parent = service_class + return if service_class.ancestors.none? { |klass| klass.name.start_with?(MOD_MATCH_REGEX) } loop do - applied_parent = service_class.superclass or break + applied_parent = applied_parent.superclass or break - break if applied_classes.key?(applied_parent.name) + break if applied_classes.include?(applied_parent.name) end - if applied_parent.nil? - err_message = "#{service_class.name} or his superclasses manually include initializer dsl" - raise ArgumentError, err_message - else - raise ArgumentError, "initializer dsl already applied to #{applied_parent.name}" - end + err_message = "#{applied_parent.name} or his superclasses manually include initializer dsl" + raise ArgumentError, err_message end end end diff --git a/spec/building_spec.rb b/spec/building_spec.rb index 767cfe6..698671f 100644 --- a/spec/building_spec.rb +++ b/spec/building_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class BaseService < Resol::Service +class BaseService < SmartService param :type option :option diff --git a/spec/service_spec.rb b/spec/service_spec.rb index 1d1bab0..97052f9 100644 --- a/spec/service_spec.rb +++ b/spec/service_spec.rb @@ -18,25 +18,25 @@ def transaction end end -class SuccessService < Resol::Service +class SuccessService < SmartService def call success!(:success_result) end end -class FailureService < Resol::Service +class FailureService < SmartService def call fail!(:failure_result, { data: 123 }) end end -class EmptyService < Resol::Service +class EmptyService < SmartService def call "some_string" end end -class AbstractService < Resol::Service +class AbstractService < SmartService end class InheritedService < AbstractService @@ -45,7 +45,7 @@ def call end end -class ServiceWithCall < Resol::Service +class ServiceWithCall < SmartService def call success!(:success_result) end @@ -54,7 +54,7 @@ def call class SubService < ServiceWithCall end -class ServiceWithCallbacks < Resol::Service +class ServiceWithCallbacks < SmartService before_call :define_instance_var def call @@ -78,19 +78,19 @@ def set_other_value end end -class ServiceWithTransaction < Resol::Service +class ServiceWithTransaction < SmartService def call DB.transaction { success! } end end -class ServiceWithFailInTransaction < Resol::Service +class ServiceWithFailInTransaction < SmartService def call DB.transaction { fail!(:failed) } end end -class HackyService < Resol::Service +class HackyService < SmartService param :count def call diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index c458ad9..a0596a6 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -25,6 +25,10 @@ require "resol" require "pry" +class SmartService < Resol::Service + use_initializer! :smartcore +end + RSpec.configure do |config| config.example_status_persistence_file_path = ".rspec_status" config.disable_monkey_patching! From 72d2929bcb05b057557e9f8054bf1e0e62042eba Mon Sep 17 00:00:00 2001 From: AnotherRegularDude Date: Sat, 14 Dec 2024 10:57:55 +0300 Subject: [PATCH 03/21] Write specs --- lib/resol.rb | 11 +++++-- lib/resol/configuration.rb | 63 ++++++++++++++++---------------------- resol.gemspec | 2 +- spec/configuration_spec.rb | 41 ++++++++----------------- spec/spec_helper.rb | 9 ++++++ 5 files changed, 58 insertions(+), 68 deletions(-) diff --git a/lib/resol.rb b/lib/resol.rb index 383a066..1f73abb 100644 --- a/lib/resol.rb +++ b/lib/resol.rb @@ -1,11 +1,18 @@ # frozen_string_literal: true -require "smart_core/initializer" - require_relative "resol/version" require_relative "resol/return_engine" require_relative "resol/configuration" require_relative "resol/service" module Resol + extend self + + def config + Configuration + end + + def configure + yield config + end end diff --git a/lib/resol/configuration.rb b/lib/resol/configuration.rb index 3a7f8d7..cb4318f 100644 --- a/lib/resol/configuration.rb +++ b/lib/resol/configuration.rb @@ -1,43 +1,32 @@ # frozen_string_literal: true module Resol - class Configuration - DEFAULT_RETURN_ENGINE = ReturnEngine::Catch - - class << self - def configure - SmartCore::Initializer::Configuration.configure do |c| - self.smartcore_config = c - yield self - self.smartcore_config = nil - end - end - - def return_engine - @return_engine || DEFAULT_RETURN_ENGINE - end - - def return_engine=(engine) - @return_engine = engine - end - - private - - attr_accessor :smartcore_config - - def method_missing(meth, *, &) - # rubocop:disable Style/SafeNavigation - if smartcore_config && smartcore_config.respond_to?(meth) - # rubocop:enable Style/SafeNavigation - smartcore_config.__send__(meth, *, &) - else - super - end - end - - def respond_to_missing?(meth, include_private) - smartcore_config.respond_to?(meth, include_private) - end + module Configuration + extend self + + DEFAULTS = { return_engine: Resol::ReturnEngine::Catch }.freeze + + DEFAULTS.each_key do |attr_name| + define_method(attr_name) { values[attr_name] } + define_method(:"#{attr_name}=") { |value| values[attr_name] = value } + end + + def smart_config + return nil if smart_not_loaded? + + SmartCore::Initializer::Configuration.config + end + + def to_h = values.dup + + private + + def smart_not_loaded? + !defined?(SmartCore::Initializer::Configuration) + end + + def values + @values ||= DEFAULTS.dup end end end diff --git a/resol.gemspec b/resol.gemspec index 91b6e24..c25335e 100644 --- a/resol.gemspec +++ b/resol.gemspec @@ -17,6 +17,6 @@ Gem::Specification.new do |spec| spec.files = `git ls-files -z`.split("\x0").reject { |f| f.include?("spec") } spec.require_paths = ["lib"] - spec.add_dependency "dry-initializer", "~> 3.1" + spec.add_dependency "dry-initializer", "~> 3.1" spec.add_dependency "smart_initializer", "~> 0.7" end diff --git a/spec/configuration_spec.rb b/spec/configuration_spec.rb index 472c234..0d9432c 100644 --- a/spec/configuration_spec.rb +++ b/spec/configuration_spec.rb @@ -1,42 +1,27 @@ # frozen_string_literal: true RSpec.describe Resol::Configuration do - around do |example| - example.call + let(:cfg_values) { described_class.instance_variable_get(:@values) } - described_class.configure do |c| - c.auto_cast = true - c.return_engine = described_class::DEFAULT_RETURN_ENGINE - end - end + it "properly configures" do + expect(described_class.return_engine).to eq(described_class::DEFAULTS[:return_engine]) + expect(described_class.smart_config).to eq(SmartCore::Initializer::Configuration.config) - it "delegates configuration" do - described_class.configure do |c| - c.auto_cast = false - c.return_engine = Resol::ReturnEngine::Return - end + described_class.return_engine = "kek" - expect(SmartCore::Initializer::Configuration.config[:auto_cast]).to eq(false) - expect(described_class.return_engine).to eq(Resol::ReturnEngine::Return) + expect(described_class.return_engine).to eq("kek") + expect(described_class.to_h.equal?(cfg_values)).to eq(false) end context "when undefined method is called" do - let(:called_block) do - proc do - described_class.configure do |c| - c.not_exist = true - end - end - end - - it "raises error" do - expect(&called_block).to raise_error(NoMethodError) + specify do + expect { described_class.kekpek }.to raise_error(NoMethodError) end end - context "with undefined method" do - it "respond_to? returns false" do - expect(described_class.respond_to?(:not_exist)).to eq(false) - end + context "when smartcore not loaded" do + before { allow(described_class).to receive(:smart_not_loaded?).and_return(true) } + + specify { expect(described_class.smart_config).to eq(nil) } end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index a0596a6..c20f9ef 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -25,6 +25,9 @@ require "resol" require "pry" +require "smart_core/initializer" +require "dry/initializer" + class SmartService < Resol::Service use_initializer! :smartcore end @@ -36,4 +39,10 @@ class SmartService < Resol::Service config.order = :random Kernel.srand config.seed + + config.around do |ex| + old_settings = Resol::Configuration.to_h + ex.call + Resol::Configuration.instance_variable_set(:@values, old_settings) + end end From 5777ce49f921f3e45937d2de40db5f55ac45ee9a Mon Sep 17 00:00:00 2001 From: AnotherRegularDude Date: Sat, 14 Dec 2024 15:12:58 +0300 Subject: [PATCH 04/21] Refacor, plugins feature, initializer feature Now supports plugins like sequel, can choose initializer, remove return engine --- Gemfile | 1 + Gemfile.lock | 3 +- Steepfile | 10 +++++ lib/resol.rb | 13 +++++- lib/resol/configuration.rb | 13 ------ lib/resol/plugins.rb | 28 ++++++------ lib/resol/plugins/return_in_service.rb | 25 +++++++++++ lib/resol/result.rb | 61 ++++++++++++-------------- lib/resol/return_engine/catch.rb | 24 ---------- lib/resol/return_engine/return.rb | 21 --------- lib/resol/service.rb | 55 ++++++++++++++++++----- spec/initializers_spec.rb | 4 ++ 12 files changed, 142 insertions(+), 116 deletions(-) create mode 100644 Steepfile create mode 100644 lib/resol/plugins/return_in_service.rb delete mode 100644 lib/resol/return_engine/catch.rb delete mode 100644 lib/resol/return_engine/return.rb create mode 100644 spec/initializers_spec.rb diff --git a/Gemfile b/Gemfile index 57bab9c..be40a33 100644 --- a/Gemfile +++ b/Gemfile @@ -14,3 +14,4 @@ gem "rubocop-config-umbrellio" gem "simplecov" gem "simplecov-lcov" gem "smart_initializer" +gem "qonfig", "0.28.0" diff --git a/Gemfile.lock b/Gemfile.lock index 531abc5..440ee56 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -55,7 +55,7 @@ GEM pry (0.15.0) coderay (~> 1.1) method_source (~> 1.0) - qonfig (0.29.0) + qonfig (0.28.0) racc (1.8.1) rack (3.1.8) rainbow (3.1.1) @@ -149,6 +149,7 @@ DEPENDENCIES ci-helper dry-initializer pry + qonfig (= 0.28.0) rake resol! rspec diff --git a/Steepfile b/Steepfile new file mode 100644 index 0000000..6d21d44 --- /dev/null +++ b/Steepfile @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +D = Steep::Diagnostic + +target :lib do + signature "sig" + check "lib/resol/result.rb" + + configure_code_diagnostics(D::Ruby.default) +end diff --git a/lib/resol.rb b/lib/resol.rb index 1f73abb..3b35961 100644 --- a/lib/resol.rb +++ b/lib/resol.rb @@ -1,9 +1,10 @@ # frozen_string_literal: true require_relative "resol/version" -require_relative "resol/return_engine" require_relative "resol/configuration" +require_relative "resol/initializers" require_relative "resol/service" +require_relative "resol/plugins" module Resol extend self @@ -15,4 +16,14 @@ def config def configure yield config end + + # rubocop:disable Naming/MethodName + def Success(...) + Success.new(...) + end + + def Failure(...) + Failure.new(...) + end + # rubocop:enable Naming/MethodName end diff --git a/lib/resol/configuration.rb b/lib/resol/configuration.rb index cb4318f..a724604 100644 --- a/lib/resol/configuration.rb +++ b/lib/resol/configuration.rb @@ -4,29 +4,16 @@ module Resol module Configuration extend self - DEFAULTS = { return_engine: Resol::ReturnEngine::Catch }.freeze - - DEFAULTS.each_key do |attr_name| - define_method(attr_name) { values[attr_name] } - define_method(:"#{attr_name}=") { |value| values[attr_name] = value } - end - def smart_config return nil if smart_not_loaded? SmartCore::Initializer::Configuration.config end - def to_h = values.dup - private def smart_not_loaded? !defined?(SmartCore::Initializer::Configuration) end - - def values - @values ||= DEFAULTS.dup - end end end diff --git a/lib/resol/plugins.rb b/lib/resol/plugins.rb index 0aa771a..396273e 100644 --- a/lib/resol/plugins.rb +++ b/lib/resol/plugins.rb @@ -4,25 +4,22 @@ module Resol module Plugins + PLUGINS_PATH = Pathname("resol/plugins") class Manager - include Singleton - def initialize - self.patched_class = Resol::Service self.plugins = [] - self.plugin_lib_path = PathName("resol/plugins") end - def plugin(plugin_name, ...) - plugin_module = find_plugin_module(plugin_name) - plugin_module.apply(target_class, ...) if plugin_module.respond_to?(:apply) + def plugin(plugin_name) + return if plugins.include?(plugin_name) + plugin_module = find_plugin_module(plugin_name) if defined?(plugin_module::InstanceMethods) - target_class.include(plugin_module::InstanceMethods) + target_class.prepend(plugin_module::InstanceMethods) end if defined?(plugin_module::ClassMethods) - target_class.extend(plugin_module::ClassMethods) + target_class.singleton_class.prepend(plugin_module::ClassMethods) end plugins << plugin_name @@ -30,16 +27,21 @@ def plugin(plugin_name, ...) private - attr_accessor :target_class, :plugins, :plugin_lib_path + attr_accessor :plugins def find_plugin_module(plugin_name) - require plugin_lib_path.join(plugin_name) + require PLUGINS_PATH.join(plugin_name) + Plugins.const_get(classify_plugin_name(plugin_name)) rescue LoadError, NameError => e raise "Failed to load plugin '#{plugin_name}': #{e.message}" end - def camel_case(string) - string.split("_").map(&:capitalize).join + def classify_plugin_name(string) + string.split(/_|-/).map!(&:capitalize).join + end + + def target_class + Resol::Service end end end diff --git a/lib/resol/plugins/return_in_service.rb b/lib/resol/plugins/return_in_service.rb new file mode 100644 index 0000000..be375a2 --- /dev/null +++ b/lib/resol/plugins/return_in_service.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Resol + module Plugins + module ReturnInService + module ClassMethods + private + + def handle_catch(_service) + yield + end + + def call_service(service) + service.call.tap { |res| return unless res.is_a?(Service::Result) } + end + end + + module InstanceMethods + private + + def proceed_return(_service, data) = data + end + end + end +end diff --git a/lib/resol/result.rb b/lib/resol/result.rb index 029a3ca..0fe8a48 100644 --- a/lib/resol/result.rb +++ b/lib/resol/result.rb @@ -3,26 +3,11 @@ module Resol class UnwrapError < StandardError; end - class Result - # @!method success? - # @!method failure? - # @!method value_or - # @!method value! - - def initialize(*); end - - def or - yield(@value) if failure? - end - - def either(success_proc, failure_proc) - success? ? success_proc.call(@value) : failure_proc.call(@value) - end - end + class Result; end class Success < Result def initialize(value) - super + super() @value = value end @@ -34,7 +19,7 @@ def failure? false end - def value_or(*) + def value_or(_other_value = nil) @value end @@ -42,14 +27,26 @@ def value! @value end - def error - nil + def error = nil + + def or = nil + + def either(success_proc, _failure_proc) + success_proc.call(@value) + end + + def bind + yield @value + end + + def fmap(&) + Resol.Success(bind(&)) end end class Failure < Result def initialize(error) - super + super() @value = error end @@ -62,11 +59,7 @@ def failure? end def value_or(other_value = nil) - if block_given? - yield(@value) - else - other_value - end + block_given? ? yield(@value) : other_value end def value! @@ -76,13 +69,17 @@ def value! def error @value end - end - def self.Success(...) - Success.new(...) - end + def or + yield @value + end + + def either(_success_proc, failure_proc) + failure_proc.call(@value) + end + + def bind = self - def self.Failure(...) - Failure.new(...) + alias fmap bind end end diff --git a/lib/resol/return_engine/catch.rb b/lib/resol/return_engine/catch.rb deleted file mode 100644 index 6407728..0000000 --- a/lib/resol/return_engine/catch.rb +++ /dev/null @@ -1,24 +0,0 @@ -# frozen_string_literal: true - -module Resol - module ReturnEngine - module Catch - extend self - - def wrap_call(service) - catch(service) do - yield - NOT_EXITED - end - end - - def uncaught_call?(return_obj) - return_obj == NOT_EXITED - end - - def handle_return(service, data) - throw(service, data) - end - end - end -end diff --git a/lib/resol/return_engine/return.rb b/lib/resol/return_engine/return.rb deleted file mode 100644 index a724cb6..0000000 --- a/lib/resol/return_engine/return.rb +++ /dev/null @@ -1,21 +0,0 @@ -# frozen_string_literal: true - -module Resol - module ReturnEngine - module Return - extend self - - def wrap_call(_service) - yield - end - - def uncaught_call?(return_obj) - !return_obj.is_a?(Resol::Service::Result) - end - - def handle_return(_service, data) - data - end - end - end -end diff --git a/lib/resol/service.rb b/lib/resol/service.rb index a462e04..7974bfb 100644 --- a/lib/resol/service.rb +++ b/lib/resol/service.rb @@ -3,7 +3,6 @@ require_relative "builder" require_relative "callbacks" require_relative "result" -require_relative "initializers" module Resol class Service @@ -28,31 +27,47 @@ def message end end + module ChildMethodRestriction + def plugin(*) + raise NoMethodError + end + + def manager + raise NoMethodError + end + end + include Resol::Builder include Resol::Callbacks Result = Struct.new(:data) + NOT_EXITED = Object.new.freeze class << self def inherited(klass) klass.const_set(:Failure, Class.new(klass::Failure)) + klass.extend(ChildMethodRestriction) super end def use_initializer!(initializer_lib) - Resol::Initializers.apply!(self, initializer_lib) + Initializers.apply!(self, initializer_lib) end - def call(*, **, &) + def plugin(...) + manager.plugin(...) + end + + def call(*, **) service = build(*, **) - result = return_engine.wrap_call(service) do + result = handle_catch(service) do service.instance_variable_set(:@__performing__, true) __run_callbacks__(service) - service.call(&) + call_service(service) end - if return_engine.uncaught_call?(result) + if result == NOT_EXITED error_message = "No `#success!` or `#fail!` called in `#call` method in #{service.class}." raise InvalidCommandImplementation, error_message else @@ -62,13 +77,27 @@ def call(*, **, &) Resol::Failure(e) end - def return_engine - Resol::Configuration.return_engine - end - def call!(...) call(...).value_or { |error| raise error } end + + private + + def manager + @manager ||= Plugins::Manager.new + end + + def handle_catch(service) + catch(service) do + yield + NOT_EXITED + end + end + + def call_service(service) + service.call + NOT_EXITED + end end # @!method call @@ -85,7 +114,7 @@ def fail!(code, data = nil) def success!(data = nil) check_performing do - self.class.return_engine.handle_return(self, Result.new(data)) + proceed_return(self, Result.new(data)) end end @@ -100,5 +129,9 @@ def check_performing raise InvalidCommandCall, error_message end end + + def proceed_return(data) + throw(self, data) + end end end diff --git a/spec/initializers_spec.rb b/spec/initializers_spec.rb new file mode 100644 index 0000000..6f82e86 --- /dev/null +++ b/spec/initializers_spec.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +RSpec.describe Resol::Initializers do +end From 3a674f9ddddf14c98f1bde96e710a87748b58bf5 Mon Sep 17 00:00:00 2001 From: AnotherRegularDude Date: Sat, 14 Dec 2024 15:18:36 +0300 Subject: [PATCH 05/21] Some fixes, processing specs --- Gemfile.lock | 2 +- lib/resol/return_engine.rb | 11 ----------- lib/resol/version.rb | 2 +- spec/configuration_spec.rb | 23 ----------------------- spec/service_spec.rb | 4 ---- spec/spec_helper.rb | 6 ------ 6 files changed, 2 insertions(+), 46 deletions(-) delete mode 100644 lib/resol/return_engine.rb diff --git a/Gemfile.lock b/Gemfile.lock index 440ee56..e0123aa 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - resol (0.9.0) + resol (1.0.0) dry-initializer (~> 3.1) smart_initializer (~> 0.7) diff --git a/lib/resol/return_engine.rb b/lib/resol/return_engine.rb deleted file mode 100644 index 5af8772..0000000 --- a/lib/resol/return_engine.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -module Resol - module ReturnEngine - NOT_EXITED = Object.new.freeze - DataWrapper = Struct.new(:data) - end -end - -require_relative "return_engine/catch" -require_relative "return_engine/return" diff --git a/lib/resol/version.rb b/lib/resol/version.rb index a03808a..8886a86 100644 --- a/lib/resol/version.rb +++ b/lib/resol/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Resol - VERSION = "0.9.0" + VERSION = "1.0.0" end diff --git a/spec/configuration_spec.rb b/spec/configuration_spec.rb index 0d9432c..fb6badb 100644 --- a/spec/configuration_spec.rb +++ b/spec/configuration_spec.rb @@ -1,27 +1,4 @@ # frozen_string_literal: true RSpec.describe Resol::Configuration do - let(:cfg_values) { described_class.instance_variable_get(:@values) } - - it "properly configures" do - expect(described_class.return_engine).to eq(described_class::DEFAULTS[:return_engine]) - expect(described_class.smart_config).to eq(SmartCore::Initializer::Configuration.config) - - described_class.return_engine = "kek" - - expect(described_class.return_engine).to eq("kek") - expect(described_class.to_h.equal?(cfg_values)).to eq(false) - end - - context "when undefined method is called" do - specify do - expect { described_class.kekpek }.to raise_error(NoMethodError) - end - end - - context "when smartcore not loaded" do - before { allow(described_class).to receive(:smart_not_loaded?).and_return(true) } - - specify { expect(described_class.smart_config).to eq(nil) } - end end diff --git a/spec/service_spec.rb b/spec/service_spec.rb index 97052f9..37f75dc 100644 --- a/spec/service_spec.rb +++ b/spec/service_spec.rb @@ -101,8 +101,6 @@ def call RSpec.describe Resol::Service do context "with Catch return engine" do - before { Resol::Configuration.return_engine = Resol::ReturnEngine::Catch } - it "returns a success result" do expect(SuccessService.call!).to eq(:success_result) end @@ -167,8 +165,6 @@ def call end context "with Return return engine" do - before { Resol::Configuration.return_engine = Resol::ReturnEngine::Return } - it "returns a success result" do expect(SuccessService.call!).to eq(:success_result) end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index c20f9ef..c6af524 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -39,10 +39,4 @@ class SmartService < Resol::Service config.order = :random Kernel.srand config.seed - - config.around do |ex| - old_settings = Resol::Configuration.to_h - ex.call - Resol::Configuration.instance_variable_set(:@values, old_settings) - end end From c179520dce6519f8dbe21b6bad86de5aec3a2bd8 Mon Sep 17 00:00:00 2001 From: AnotherRegularDude Date: Sat, 14 Dec 2024 20:46:14 +0300 Subject: [PATCH 06/21] Fix and write specs, fix bugs, change minimal ruby version --- .github/workflows/test.yml | 4 +- .rubocop.yml | 2 +- Gemfile | 5 +- Gemfile.lock | 8 +- lib/resol/initializers.rb | 16 ++- lib/resol/plugins.rb | 18 ++-- lib/resol/plugins/dummy.rb | 7 ++ lib/resol/plugins/return_in_service.rb | 4 +- lib/resol/result.rb | 2 + lib/resol/service.rb | 27 ++--- resol.gemspec | 5 +- spec/configuration_spec.rb | 23 +++++ spec/initializers_spec.rb | 79 ++++++++++++++ spec/manager_spec.rb | 55 ++++++++++ spec/resol_spec.rb | 14 +++ spec/result_spec.rb | 67 +++++++++--- spec/service_spec.rb | 137 ++++++++++++++++++++++--- spec/spec_helper.rb | 24 +++++ 18 files changed, 425 insertions(+), 72 deletions(-) create mode 100644 lib/resol/plugins/dummy.rb create mode 100644 spec/manager_spec.rb diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0560e13..e5349c3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,7 +16,7 @@ jobs: - uses: actions/checkout@v2 - uses: ruby/setup-ruby@v1 with: - ruby-version: "3.1" + ruby-version: "3.3" bundler-cache: true - name: Run Linter run: bundle exec ci-helper RubocopLint @@ -43,7 +43,7 @@ jobs: strategy: fail-fast: false matrix: - ruby: ["2.7", "3.0"] + ruby: ["3.1", "3.2"] experimental: [false] include: - ruby: head diff --git a/.rubocop.yml b/.rubocop.yml index e6b375e..c1a4a51 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -3,7 +3,7 @@ inherit_gem: AllCops: DisplayCopNames: true - TargetRubyVersion: 3.2 + TargetRubyVersion: 3.1 Naming/MethodParameterName: AllowedNames: ["x", "y", "z"] diff --git a/Gemfile b/Gemfile index be40a33..6e80bcd 100644 --- a/Gemfile +++ b/Gemfile @@ -6,12 +6,13 @@ gemspec gem "bundler-audit" gem "ci-helper" -gem "dry-initializer" gem "pry" +gem "qonfig" gem "rake" gem "rspec" gem "rubocop-config-umbrellio" gem "simplecov" gem "simplecov-lcov" + +gem "dry-initializer" gem "smart_initializer" -gem "qonfig", "0.28.0" diff --git a/Gemfile.lock b/Gemfile.lock index e0123aa..39a63fa 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,13 +2,11 @@ PATH remote: . specs: resol (1.0.0) - dry-initializer (~> 3.1) - smart_initializer (~> 0.7) GEM remote: https://rubygems.org/ specs: - activesupport (8.0.1) + activesupport (7.2.2.1) base64 benchmark (>= 0.3) bigdecimal @@ -20,7 +18,6 @@ GEM minitest (>= 5.1) securerandom (>= 0.3) tzinfo (~> 2.0, >= 2.0.5) - uri (>= 0.13.1) ast (2.4.2) base64 (0.2.0) benchmark (0.4.0) @@ -134,7 +131,6 @@ GEM umbrellio-sequel-plugins (0.17.0) sequel unicode-display_width (2.6.0) - uri (1.0.2) PLATFORMS arm64-darwin-21 @@ -149,7 +145,7 @@ DEPENDENCIES ci-helper dry-initializer pry - qonfig (= 0.28.0) + qonfig rake resol! rspec diff --git a/lib/resol/initializers.rb b/lib/resol/initializers.rb index 0548c0e..0deff38 100644 --- a/lib/resol/initializers.rb +++ b/lib/resol/initializers.rb @@ -20,6 +20,8 @@ def apply!(service_class, initializer_name) else raise ArgumentError, "unknown initializer #{initializer_name}" end + + self.applied_classes << service_class.name end private @@ -28,7 +30,7 @@ def apply!(service_class, initializer_name) def validate_state!(service_class) applied_parent = service_class - return if service_class.ancestors.none? { |klass| klass.name.start_with?(MOD_MATCH_REGEX) } + return if service_class.ancestors.none? { |klass| klass.inspect.start_with?(MOD_MATCH_REGEX) } loop do applied_parent = applied_parent.superclass or break @@ -36,8 +38,16 @@ def validate_state!(service_class) break if applied_classes.include?(applied_parent.name) end - err_message = "#{applied_parent.name} or his superclasses manually include initializer dsl" - raise ArgumentError, err_message + if applied_parent.nil? + error!("use ::use_initializer! method on desired service class") + end + + err_message = "#{applied_parent.name} or his superclasses already used initialize lib" + error!(err_message) + end + + def error!(message) + raise ArgumentError, message end end end diff --git a/lib/resol/plugins.rb b/lib/resol/plugins.rb index 396273e..a7c8b12 100644 --- a/lib/resol/plugins.rb +++ b/lib/resol/plugins.rb @@ -6,11 +6,13 @@ module Resol module Plugins PLUGINS_PATH = Pathname("resol/plugins") class Manager - def initialize + def initialize(target_class = nil) self.plugins = [] + self.target_class = target_class || Resol::Service end def plugin(plugin_name) + plugin_name = plugin_name.to_s return if plugins.include?(plugin_name) plugin_module = find_plugin_module(plugin_name) @@ -27,21 +29,21 @@ def plugin(plugin_name) private - attr_accessor :plugins + attr_accessor :plugins, :target_class def find_plugin_module(plugin_name) require PLUGINS_PATH.join(plugin_name) - Plugins.const_get(classify_plugin_name(plugin_name)) + resolve_module(classify_plugin_name(plugin_name)) rescue LoadError, NameError => e - raise "Failed to load plugin '#{plugin_name}': #{e.message}" + raise ArgumentError, "Failed to load plugin '#{plugin_name}': #{e.message}" end - def classify_plugin_name(string) - string.split(/_|-/).map!(&:capitalize).join + def resolve_module(module_name) + Plugins.const_get(module_name) end - def target_class - Resol::Service + def classify_plugin_name(string) + string.split(/_|-/).map!(&:capitalize).join end end end diff --git a/lib/resol/plugins/dummy.rb b/lib/resol/plugins/dummy.rb new file mode 100644 index 0000000..03fe03f --- /dev/null +++ b/lib/resol/plugins/dummy.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Resol + module Plugins + module Dummy; end + end +end diff --git a/lib/resol/plugins/return_in_service.rb b/lib/resol/plugins/return_in_service.rb index be375a2..8a3d37e 100644 --- a/lib/resol/plugins/return_in_service.rb +++ b/lib/resol/plugins/return_in_service.rb @@ -11,7 +11,9 @@ def handle_catch(_service) end def call_service(service) - service.call.tap { |res| return unless res.is_a?(Service::Result) } + service.call.tap do |res| + return Resol::Service::NOT_EXITED unless res.is_a?(Service::Result) + end end end diff --git a/lib/resol/result.rb b/lib/resol/result.rb index 0fe8a48..0d1facd 100644 --- a/lib/resol/result.rb +++ b/lib/resol/result.rb @@ -3,7 +3,9 @@ module Resol class UnwrapError < StandardError; end + # rubocop:disable Lint/EmptyClass class Result; end + # rubocop:enable Lint/EmptyClass class Success < Result def initialize(value) diff --git a/lib/resol/service.rb b/lib/resol/service.rb index 7974bfb..07b8f37 100644 --- a/lib/resol/service.rb +++ b/lib/resol/service.rb @@ -27,26 +27,17 @@ def message end end - module ChildMethodRestriction - def plugin(*) - raise NoMethodError - end - - def manager - raise NoMethodError - end - end - include Resol::Builder include Resol::Callbacks - Result = Struct.new(:data) NOT_EXITED = Object.new.freeze + BASE_CLASS = self + + Result = Struct.new(:data) class << self def inherited(klass) klass.const_set(:Failure, Class.new(klass::Failure)) - klass.extend(ChildMethodRestriction) super end @@ -55,11 +46,15 @@ def use_initializer!(initializer_lib) end def plugin(...) + if self::BASE_CLASS != self + raise ArgumentError, "can load plugins only on base Resol::Service" + end + manager.plugin(...) end - def call(*, **) - service = build(*, **) + def call(...) + service = build(...) result = handle_catch(service) do service.instance_variable_set(:@__performing__, true) @@ -130,8 +125,8 @@ def check_performing end end - def proceed_return(data) - throw(self, data) + def proceed_return(service, data) + throw(service, data) end end end diff --git a/resol.gemspec b/resol.gemspec index c25335e..46b1418 100644 --- a/resol.gemspec +++ b/resol.gemspec @@ -13,10 +13,7 @@ Gem::Specification.new do |spec| spec.homepage = "https://github.com/umbrellio/resol" spec.license = "MIT" - spec.required_ruby_version = Gem::Requirement.new(">= 3.2.0") + spec.required_ruby_version = Gem::Requirement.new(">= 3.1.0") spec.files = `git ls-files -z`.split("\x0").reject { |f| f.include?("spec") } spec.require_paths = ["lib"] - - spec.add_dependency "dry-initializer", "~> 3.1" - spec.add_dependency "smart_initializer", "~> 0.7" end diff --git a/spec/configuration_spec.rb b/spec/configuration_spec.rb index fb6badb..020ff9f 100644 --- a/spec/configuration_spec.rb +++ b/spec/configuration_spec.rb @@ -1,4 +1,27 @@ # frozen_string_literal: true RSpec.describe Resol::Configuration do + before { allow(described_class).to receive(:smart_not_loaded?).and_return(const_not_loaded?) } + + let(:const_not_loaded?) { true } + + it "#smart_config returns nil" do + expect(described_class.smart_config).to eq(nil) + end + + context "with loaded const" do + let(:const_not_loaded?) { false } + + it "returns config" do + expect(described_class.smart_config).to eq(SmartCore::Initializer::Configuration.config) + end + end + + context "with original method" do + before { allow(described_class).to receive(:smart_not_loaded?).and_call_original } + + it "returns smartcore config" do + expect(described_class.smart_config).to eq(SmartCore::Initializer::Configuration.config) + end + end end diff --git a/spec/initializers_spec.rb b/spec/initializers_spec.rb index 6f82e86..84a3a8c 100644 --- a/spec/initializers_spec.rb +++ b/spec/initializers_spec.rb @@ -1,4 +1,83 @@ # frozen_string_literal: true RSpec.describe Resol::Initializers do + def apply_initializer(forced_service_class = nil) + described_class.apply!(forced_service_class || service_class, initializer_name) + end + + before { stub_const("InitializerTestClass", service_class) } + + let(:service_class) do + Class.new(Resol::Service) do + def call + success! + end + end + end + + let(:initializer_name) { :dry } + let(:dry_modules) do + ["Dry::Initializer::Mixin::Root", start_with("Dry::Initializer::Mixin::Local")] + end + + it "properly extend service class with initializer" do + apply_initializer + expect(service_class.ancestors.map(&:to_s)).to include(*dry_modules) + end + + context "with unknown initializer lib" do + let(:initializer_name) { :kek } + + specify do + expect { apply_initializer }.to raise_error(ArgumentError, "unknown initializer kek") + end + end + + context "with already prepared parent" do + before { stub_const("SecondChildService", second_child_service) } + + let(:service_class) do + Class.new(ReturnEngineService) do + def call + success! + end + end + end + + let(:second_child_service) do + Class.new(InitializerTestClass) + end + + let(:error_message) do + "ReturnEngineService or his superclasses already used initialize lib" + end + + specify do + expect { apply_initializer(SecondChildService) }.to raise_error(ArgumentError, error_message) + end + end + + context "with manually extended service" do + before { stub_const("SecondChildService", second_child_service) } + + let(:service_class) do + Class.new(Resol::Service) do + extend Dry::Initializer + + def call + success! + end + end + end + + let(:second_child_service) do + Class.new(InitializerTestClass) + end + + let(:error_message) { "use ::use_initializer! method on desired service class" } + + specify do + expect { apply_initializer(SecondChildService) }.to raise_error(ArgumentError, error_message) + end + end end diff --git a/spec/manager_spec.rb b/spec/manager_spec.rb new file mode 100644 index 0000000..f56b11e --- /dev/null +++ b/spec/manager_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +RSpec.describe Resol::Plugins::Manager do + let(:manager) { described_class.new(service_double) } + + let(:service_double) do + class_double( + Resol::Service, :DummyService, { prepend: true, singleton_class: singleton_double } + ) + end + + let(:singleton_double) { double(prepend: true) } + + it "skips all prepends" do + manager.plugin(:dummy) + + expect(service_double).not_to receive(:prepend) + expect(singleton_double).not_to receive(:prepend) + end + + context "when uses same plugin few times" do + before { allow(manager).to receive(:resolve_module).and_return(Resol::Plugins::Dummy) } + + before { manager.plugin(:dummy) } + + let(:manager_plugins) { manager.instance_variable_get(:@plugins) } + + it "doesn't load plugin second time" do + manager.plugin(:dummy) + + expect(manager).to have_received(:resolve_module).once + expect(manager_plugins).to eq(["dummy"]) + end + end + + context "when can't require plugin" do + specify do + expect { manager.plugin(:not_existed_plugin) }.to raise_error do |error| + expect(error).to be_an_instance_of(ArgumentError) + expect(error.message).to include("Failed to load plugin 'not_existed_plugin': ") + end + end + end + + context "when can't resolve module" do + before { allow(manager).to receive(:resolve_module).and_raise(NameError, "msg") } + + specify do + expect { manager.plugin(:dummy) }.to raise_error do |error| + expect(error).to be_an_instance_of(ArgumentError) + expect(error.message).to include("Failed to load plugin 'dummy': msg") + end + end + end +end diff --git a/spec/resol_spec.rb b/spec/resol_spec.rb index 46f84fe..d419f6d 100644 --- a/spec/resol_spec.rb +++ b/spec/resol_spec.rb @@ -4,4 +4,18 @@ it "has a version number" do expect(Resol::VERSION).not_to be nil end + + describe "#config" do + specify do + expect(described_class.config).to eq(Resol::Configuration) + end + end + + describe "#configure" do + specify do + Resol.configure do |config| + expect(config.smart_config).to eq(SmartCore::Initializer::Configuration.config) + end + end + end end diff --git a/spec/result_spec.rb b/spec/result_spec.rb index 1589dc2..73920c1 100644 --- a/spec/result_spec.rb +++ b/spec/result_spec.rb @@ -4,41 +4,78 @@ describe Resol::Success do let(:result) { Resol::Success(:success_value) } - it { expect(result.success?).to be_truthy } - it { expect(result.failure?).to be_falsey } - it { expect(result.value_or(:other_value)).to eq(:success_value) } - it { expect(result.value!).to eq(:success_value) } - it { expect(result.error).to be_nil } - it { expect { result.or { raise "Some Error" } }.not_to raise_error } - - it do + specify { expect(result.success?).to be_truthy } + specify { expect(result.failure?).to be_falsey } + specify { expect(result.value_or(:other_value)).to eq(:success_value) } + specify { expect(result.value!).to eq(:success_value) } + specify { expect(result.error).to be_nil } + specify { expect { result.or { raise "Some Error" } }.not_to raise_error } + + specify do success_proc = instance_double(Proc) failure_proc = instance_double(Proc) allow(success_proc).to receive(:call).and_return("result") expect(result.either(success_proc, failure_proc)).to eq("result") end + + specify do + final = result.bind(&:to_s) + expect(final).to eq("success_value") + end + + specify do + final = result.bind do |val| + Resol::Success("success").bind do |to_exclude| + Resol::Success(val.to_s.gsub(to_exclude, "")) + end + end + + expect(final.success?).to eq(true) + expect(final.value!).to eq("_value") + end + + specify do + final = result.fmap(&:to_s) + + expect(final.success?).to eq(true) + expect(final.value!).to eq("success_value") + end end describe Resol::Failure do let(:result) { Resol::Failure(:failure_value) } - it { expect(result.success?).to be_falsey } - it { expect(result.failure?).to be_truthy } - it { expect(result.value_or(:other_value)).to eq(:other_value) } - it { expect(result.error).to eq(:failure_value) } - it { expect { result.or { raise "Some Error" } }.to raise_error("Some Error") } + specify { expect(result.success?).to be_falsey } + specify { expect(result.failure?).to be_truthy } + specify { expect(result.value_or(:other_value)).to eq(:other_value) } + specify { expect(result.error).to eq(:failure_value) } + specify { expect { result.or { raise "Some Error" } }.to raise_error("Some Error") } - it do + specify do expect { result.value! }.to raise_error(Resol::UnwrapError, "Failure result :failure_value") end - it do + specify do success_proc = instance_double(Proc) failure_proc = instance_double(Proc) allow(failure_proc).to receive(:call).and_return("result") expect(result.either(success_proc, failure_proc)).to eq("result") end + + specify do + final = result.bind(&:to_s) + + expect(final.failure?).to eq(true) + expect(final.error).to eq(:failure_value) + end + + specify do + final = result.fmap { |val| val + 1 } + + expect(final.failure?).to eq(true) + expect(final.error).to eq(:failure_value) + end end end diff --git a/spec/service_spec.rb b/spec/service_spec.rb index 37f75dc..314b665 100644 --- a/spec/service_spec.rb +++ b/spec/service_spec.rb @@ -99,6 +99,87 @@ def call end end +class PluginSuccessService < ReturnEngineService + def call + success!(:success_result) + end +end + +class PluginFailureService < ReturnEngineService + def call + fail!(:failure_result, { data: 123 }) + end +end + +class PluginEmptyService < ReturnEngineService + def call + "some_string" + end +end + +class PluginAbstractService < ReturnEngineService +end + +class PluginInheritedService < PluginAbstractService + def call + success!(:success_result) + end +end + +class PluginServiceWithCall < ReturnEngineService + def call + success!(:success_result) + end +end + +class PluginSubService < PluginServiceWithCall +end + +class PluginServiceWithCallbacks < ReturnEngineService + before_call :define_instance_var + + def call + success!(@some_var) + end + + private + + def define_instance_var + @some_var = "some_value" + end +end + +class PluginSubServiceWithCallbacks < PluginServiceWithCallbacks + before_call :set_other_value + + private + + def set_other_value + @some_var += "_postfix" + end +end + +class PluginServiceWithTransaction < ReturnEngineService + def call + DB.transaction { return success! } + end +end + +class PluginServiceWithFailInTransaction < ReturnEngineService + def call + DB.transaction { fail!(:failed) } + end +end + +class PluginHackyService < ReturnEngineService + param :count + + def call + return success! unless count.zero? + PluginHackyService.build(count + 1).call + end +end + RSpec.describe Resol::Service do context "with Catch return engine" do it "returns a success result" do @@ -164,41 +245,48 @@ def call end end - context "with Return return engine" do + context "with Return on success plugin" do it "returns a success result" do - expect(SuccessService.call!).to eq(:success_result) + expect(PluginSuccessService.call!).to eq(:success_result) end it "raises a failure result error" do - expect { FailureService.call! }.to raise_error do |error| - expect(error).to be_a(FailureService::Failure) + expect { PluginFailureService.call! }.to raise_error do |error| + expect(error).to be_a(PluginFailureService::Failure) expect(error.code).to eq(:failure_result) expect(error.data).to eq(data: 123) end end it "raises an InvalidCommandImplementation error" do - expect { EmptyService.call! }.to raise_error do |error| - expect(error).to be_a(EmptyService::InvalidCommandImplementation) + expect { PluginEmptyService.call! }.to raise_error do |error| + expect(error).to be_a(PluginEmptyService::InvalidCommandImplementation) expect(error.message).to eq( - "No `#success!` or `#fail!` called in `#call` method in EmptyService.", + "No `#success!` or `#fail!` called in `#call` method in PluginEmptyService.", ) end end it "properly works with inherited services" do - expect(InheritedService.call!).to eq(:success_result) - expect(SubService.call!).to eq(:success_result) + expect(PluginInheritedService.call!).to eq(:success_result) + expect(PluginSubService.call!).to eq(:success_result) end it "properly executes callbacks" do - expect(SubServiceWithCallbacks.call!).to eq("some_value_postfix") - expect(ServiceWithCallbacks.call!).to eq("some_value") + expect(PluginSubServiceWithCallbacks.call!).to eq("some_value_postfix") + expect(PluginServiceWithCallbacks.call!).to eq("some_value") + end + + it "doesn't rollback transaction" do + result = PluginServiceWithTransaction.call + expect(result.success?).to eq(true) + expect(result.value!).to eq(nil) + expect(DB.rollbacked).to eq(false) end context "when service failed" do it "rollbacks transaction" do - result = ServiceWithFailInTransaction.call + result = PluginServiceWithFailInTransaction.call expect(result.failure?).to eq(true) result.or do |error| expect(error.code).to eq(:failed) @@ -210,14 +298,35 @@ def call context "when using instance #call" do it "raises error" do - expect { SuccessService.build.call }.to raise_error(Resol::Service::InvalidCommandCall) + expect do + PluginSuccessService.build.call + end.to raise_error(Resol::Service::InvalidCommandCall) end end context "when using instance #call inside other service" do it "raises error" do - expect { HackyService.call!(0) }.to raise_error(Resol::Service::InvalidCommandCall) + expect { PluginHackyService.call!(0) }.to raise_error(Resol::Service::InvalidCommandCall) end end end + + context "when install plugin on the child service class" do + let(:child_service) { Class.new(Resol::Service) } + + let(:error_message) { "can load plugins only on base Resol::Service" } + + it "raises error" do + expect { child_service.plugin(:dump) }.to raise_error(ArgumentError, error_message) + end + end + + context "when access plugin manager more then one time" do + let(:first_manager) { Resol::Service.send(:manager) } + let(:second_manager) { Resol::Service.send(:manager) } + + it "memoize manager instance" do + expect(first_manager).to eq(second_manager) + end + end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index c6af524..95c209a 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -28,10 +28,28 @@ require "smart_core/initializer" require "dry/initializer" +require "resol/plugins/dummy" + class SmartService < Resol::Service use_initializer! :smartcore end +class ReturnEngineService < Resol::Service + BASE_CLASS = self + + use_initializer! :dry + + class << self + private + + def manager + @manager ||= Resol::Plugins::Manager.new(self) + end + end +end + +ReturnEngineService.plugin(:return_in_service) + RSpec.configure do |config| config.example_status_persistence_file_path = ".rspec_status" config.disable_monkey_patching! @@ -39,4 +57,10 @@ class SmartService < Resol::Service config.order = :random Kernel.srand config.seed + + config.around do |ex| + applied_classes = Resol::Initializers.send(:applied_classes).dup + ex.call + Resol::Initializers.send(:applied_classes=, applied_classes) + end end From 42b0969960a7c9d3726316df2e52bddd07dbd603 Mon Sep 17 00:00:00 2001 From: AnotherRegularDude Date: Sat, 14 Dec 2024 21:19:30 +0300 Subject: [PATCH 07/21] Remove rbs because it so painful --- Steepfile | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 Steepfile diff --git a/Steepfile b/Steepfile deleted file mode 100644 index 6d21d44..0000000 --- a/Steepfile +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true - -D = Steep::Diagnostic - -target :lib do - signature "sig" - check "lib/resol/result.rb" - - configure_code_diagnostics(D::Ruby.default) -end From b17da806334757898dad3dce94188634c9b14e4a Mon Sep 17 00:00:00 2001 From: AnotherRegularDude Date: Sat, 14 Dec 2024 21:20:12 +0300 Subject: [PATCH 08/21] Remove qonfig from Gemfile --- Gemfile | 1 - Gemfile.lock | 1 - 2 files changed, 2 deletions(-) diff --git a/Gemfile b/Gemfile index 6e80bcd..dd28cb3 100644 --- a/Gemfile +++ b/Gemfile @@ -7,7 +7,6 @@ gemspec gem "bundler-audit" gem "ci-helper" gem "pry" -gem "qonfig" gem "rake" gem "rspec" gem "rubocop-config-umbrellio" diff --git a/Gemfile.lock b/Gemfile.lock index 39a63fa..9b9df53 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -145,7 +145,6 @@ DEPENDENCIES ci-helper dry-initializer pry - qonfig rake resol! rspec From 9ee51955d97d94d14056dc9b8416c158aa4816cf Mon Sep 17 00:00:00 2001 From: AnotherRegularDude Date: Sat, 14 Dec 2024 23:21:28 +0300 Subject: [PATCH 09/21] Add some description for the new func --- README.md | 53 +++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 45 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index d4cca75..98ad809 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,37 @@ and method `.call!` returns a value or throws an error (in case of `fail!` has b #### Params defining -All incoming params and options can be defined using a [smart_initializer](https://github.com/smart-rb/smart_initializer) gem interface. +`Resol` supports two gems, which provide abstract initialization flow for classes: + +1. [smart_initializer](https://github.com/smart-rb/smart_initializer), which was default provider for a very long time. +2. [dry-initializer](https://dry-rb.org/gems/dry-initializer/3.1), additional provider with DSL almost identical to the smart_core's DSL. + +There is an _important restriction_ on using different initializers in different services. +Descendants of a parent, into which initializer logic has already been imported, cannot override the provider + +You can use both providers for a different services: + +```ruby + +# Types is a namespace for all types, defined by smart_types. +class FirstService < Resol::Service + use_initializer! :smartcore + + param :first, Types::String + param :second, Types::Integer +end + +# Types is a namespace for all types, defined by dry-types. +class SecondService < Resol::Service + use_initializer! :dry + + param :first, Types::Strict::String + param :second, Types::Strict::Integer +end +``` + +Both initializers support inheritance. And base features for initialization flow +like default value, arguments accessors visibility level, coercible attributes and so on. #### Return a result @@ -95,13 +125,15 @@ end Methods: -- `success?` – returns `true` for success result and `false` for failure result -- `failure?` – returns `true` for failure result and `false` for success result -- `value!` – unwraps a result object, returns the value for success result, and throws an error for failure result -- `value_or(other_value, &block)` – returns a value for success result or `other_value` for failure result (either calls `block` in case it given) -- `error` – returns `nil` for success result and error object (with code and data) for failure result -- `or(&block)` – calls block for failure result, for success result does nothing -- `either(success_proc, failure_proc)` – for success result calls success_proc with result value in args, for failure result calls failure_proc with error in args. +- `success?` — returns `true` for success result and `false` for failure result +- `failure?` — returns `true` for failure result and `false` for success result +- `value!` — unwraps a result object, returns the value for success result, and throws an error for failure result +- `value_or(other_value, &block)` — returns a value for success result or `other_value` for failure result (either calls `block` in case it given) +- `error` — returns `nil` for success result and error object (with code and data) for failure result +- `or(&block)` — calls block for failure result, for success result does nothing +- `either(success_proc, failure_proc)` — for success result calls success_proc with result value in args, for failure result calls failure_proc with error in args. +- `bind` — using with `block` for success result resolve value and pass it to the `block`, used to chain multiple monads. Block can return anything. Failure result ignore block and return `self`. +- `fmap` — like the `bind`, but wraps value returned by block by success monad. ### Error object @@ -115,6 +147,11 @@ methods `code` and `data`. Configuration constant references to `SmartCore::Initializer::Configuration`. You can read about available configuration options [here](https://github.com/smart-rb/smart_initializer#configuration). +### Plugin System + +Resol implements the basic logic of using plugins to extend and change the base service class. +You can write your own plugin and applie it by calling `Resol::Service#plugin(plugin_name)`. + ## Development After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/rspec` to run the tests. From e66738484d3b23834ae28e32b7e12d7584641607 Mon Sep 17 00:00:00 2001 From: AnotherRegularDude Date: Sun, 15 Dec 2024 22:40:43 +0300 Subject: [PATCH 10/21] Remove some classes, use new ones, add di --- Gemfile.lock | 3 ++ lib/resol.rb | 12 ++--- lib/resol/configuration.rb | 16 +++--- lib/resol/dependency_handler.rb | 27 ++++++++++ lib/resol/initializers.rb | 53 ------------------- lib/resol/injector.rb | 26 ++++++++++ lib/resol/plugins.rb | 18 ++++--- lib/resol/plugins/return_in_service.rb | 6 --- lib/resol/service.rb | 72 +++++++++++--------------- resol.gemspec | 2 + spec/spec_helper.rb | 16 +++--- 11 files changed, 119 insertions(+), 132 deletions(-) create mode 100644 lib/resol/dependency_handler.rb delete mode 100644 lib/resol/initializers.rb create mode 100644 lib/resol/injector.rb diff --git a/Gemfile.lock b/Gemfile.lock index 9b9df53..bf40452 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,6 +2,7 @@ PATH remote: . specs: resol (1.0.0) + dry-container GEM remote: https://rubygems.org/ @@ -36,6 +37,8 @@ GEM diff-lcs (1.5.1) docile (1.4.1) drb (2.2.1) + dry-container (0.11.0) + concurrent-ruby (~> 1.0) dry-inflector (1.1.0) dry-initializer (3.1.1) i18n (1.14.6) diff --git a/lib/resol.rb b/lib/resol.rb index 3b35961..3b0e9bb 100644 --- a/lib/resol.rb +++ b/lib/resol.rb @@ -1,20 +1,20 @@ # frozen_string_literal: true +require "dry-container" + require_relative "resol/version" require_relative "resol/configuration" -require_relative "resol/initializers" + +require_relative "resol/injector" require_relative "resol/service" require_relative "resol/plugins" +require_relative "resol/dependency_container" module Resol extend self def config - Configuration - end - - def configure - yield config + @config ||= Configuration.new end # rubocop:disable Naming/MethodName diff --git a/lib/resol/configuration.rb b/lib/resol/configuration.rb index a724604..f9c9923 100644 --- a/lib/resol/configuration.rb +++ b/lib/resol/configuration.rb @@ -1,19 +1,19 @@ # frozen_string_literal: true module Resol - module Configuration - extend self + class Configuration + DEFAULT_CONFIG_VALUES = { classes_allowed_to_patch: ["Resol::Service"] }.freeze - def smart_config - return nil if smart_not_loaded? + def initialize + self.data = DEFAULT_CONFIG_VALUES.deep_dup + end - SmartCore::Initializer::Configuration.config + DEFAULT_CONFIG_VALUES.each_key do |setting_name| + define_method(setting_name) { data.fetch(setting_name) } end private - def smart_not_loaded? - !defined?(SmartCore::Initializer::Configuration) - end + attr_accessor :data end end diff --git a/lib/resol/dependency_handler.rb b/lib/resol/dependency_handler.rb new file mode 100644 index 0000000..9718fc2 --- /dev/null +++ b/lib/resol/dependency_handler.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Resol + class DependencyHandler + extend Dry::Container::Mixin + + namespace(:external_libs) do + register(:smartcore_injector, memoize: true) do + require "smart_core/initializer" + + installer_proc = proc { include SmartCore::Initializer } + Resol::Injector.new(installer_proc) + end + + register(:dry_injector, memoize: true) do + require "dry/initializer" + + installer_proc = proc { extend Dry::Initializer } + Resol::Injector.new(installer_proc) + end + end + + namespace(:lib) do + register(:plugin_manager) { Plugins::Manager.new } + end + end +end diff --git a/lib/resol/initializers.rb b/lib/resol/initializers.rb deleted file mode 100644 index 0deff38..0000000 --- a/lib/resol/initializers.rb +++ /dev/null @@ -1,53 +0,0 @@ -# frozen_string_literal: true - -module Resol - module Initializers - extend self - - MOD_MATCH_REGEX = /(Dry|SmartCore)::Initializer/ - - def apply!(service_class, initializer_name) - self.applied_classes ||= [] - validate_state!(service_class) - - case initializer_name - when :smartcore - require "smart_core/initializer" - service_class.include(Object.const_get("SmartCore::Initializer")) - when :dry - require "dry/initializer" - service_class.extend(Object.const_get("Dry::Initializer")) - else - raise ArgumentError, "unknown initializer #{initializer_name}" - end - - self.applied_classes << service_class.name - end - - private - - attr_accessor :applied_classes - - def validate_state!(service_class) - applied_parent = service_class - return if service_class.ancestors.none? { |klass| klass.inspect.start_with?(MOD_MATCH_REGEX) } - - loop do - applied_parent = applied_parent.superclass or break - - break if applied_classes.include?(applied_parent.name) - end - - if applied_parent.nil? - error!("use ::use_initializer! method on desired service class") - end - - err_message = "#{applied_parent.name} or his superclasses already used initialize lib" - error!(err_message) - end - - def error!(message) - raise ArgumentError, message - end - end -end diff --git a/lib/resol/injector.rb b/lib/resol/injector.rb new file mode 100644 index 0000000..ebcaa5f --- /dev/null +++ b/lib/resol/injector.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Resol + class Injector + InjectMarker = Module.new + + def initialize(proc_register) + self.proc_register = proc_register + end + + def inject!(service_class) + error!("parent or this class already injected") if service_class.is_a?(InjectMarker) + + service_class.instance_eval(&proc_register) + service_class.include(InjectMarker) + end + + private + + attr_accessor :proc_register + + def error!(msg) + raise msg + end + end +end diff --git a/lib/resol/plugins.rb b/lib/resol/plugins.rb index a7c8b12..b74cb1f 100644 --- a/lib/resol/plugins.rb +++ b/lib/resol/plugins.rb @@ -6,22 +6,24 @@ module Resol module Plugins PLUGINS_PATH = Pathname("resol/plugins") class Manager - def initialize(target_class = nil) + def initialize + self.allowed_classes = resolve_allowed_classes self.plugins = [] - self.target_class = target_class || Resol::Service end - def plugin(plugin_name) + def plugin(caller_class, plugin_name) plugin_name = plugin_name.to_s + + return if allowed_classes.exclude?(caller_class) return if plugins.include?(plugin_name) plugin_module = find_plugin_module(plugin_name) if defined?(plugin_module::InstanceMethods) - target_class.prepend(plugin_module::InstanceMethods) + caller_class.prepend(plugin_module::InstanceMethods) end if defined?(plugin_module::ClassMethods) - target_class.singleton_class.prepend(plugin_module::ClassMethods) + caller_class.singleton_class.prepend(plugin_module::ClassMethods) end plugins << plugin_name @@ -29,7 +31,11 @@ def plugin(plugin_name) private - attr_accessor :plugins, :target_class + attr_accessor :plugins + + def resolve_allowed_classes + Resol.config.classes_allowed_to_patch.map { |name| Object.const_get(name) } + end def find_plugin_module(plugin_name) require PLUGINS_PATH.join(plugin_name) diff --git a/lib/resol/plugins/return_in_service.rb b/lib/resol/plugins/return_in_service.rb index 8a3d37e..d04a72b 100644 --- a/lib/resol/plugins/return_in_service.rb +++ b/lib/resol/plugins/return_in_service.rb @@ -9,12 +9,6 @@ module ClassMethods def handle_catch(_service) yield end - - def call_service(service) - service.call.tap do |res| - return Resol::Service::NOT_EXITED unless res.is_a?(Service::Result) - end - end end module InstanceMethods diff --git a/lib/resol/service.rb b/lib/resol/service.rb index 07b8f37..67483df 100644 --- a/lib/resol/service.rb +++ b/lib/resol/service.rb @@ -30,9 +30,6 @@ def message include Resol::Builder include Resol::Callbacks - NOT_EXITED = Object.new.freeze - BASE_CLASS = self - Result = Struct.new(:data) class << self @@ -41,16 +38,13 @@ def inherited(klass) super end - def use_initializer!(initializer_lib) - Initializers.apply!(self, initializer_lib) + def inject_initializer!(injector_name) + injector = DependencyContainer.resolve("libs.#{injector_name}") + injector.inject!(self) end def plugin(...) - if self::BASE_CLASS != self - raise ArgumentError, "can load plugins only on base Resol::Service" - end - - manager.plugin(...) + manager.plugin(self, ...) end def call(...) @@ -59,15 +53,12 @@ def call(...) result = handle_catch(service) do service.instance_variable_set(:@__performing__, true) __run_callbacks__(service) - call_service(service) + service.call end + return Resol::Success(result.data) if service.__result_method__called__ - if result == NOT_EXITED - error_message = "No `#success!` or `#fail!` called in `#call` method in #{service.class}." - raise InvalidCommandImplementation, error_message - else - Resol::Success(result.data) - end + error_message = "No `#success!` or `#fail!` called in `#call` method in #{service.class}." + raise InvalidCommandImplementation, error_message rescue self::Failure => e Resol::Failure(e) end @@ -79,50 +70,45 @@ def call!(...) private def manager - @manager ||= Plugins::Manager.new - end - - def handle_catch(service) - catch(service) do - yield - NOT_EXITED - end + @manager ||= DependencyContainer.resolve(:base_plugin_manager) end - def call_service(service) - service.call - NOT_EXITED + def handle_catch(service, &) + catch(service, &) end end # @!method call + attr_accessor :__result_method__called__ + private attr_reader :__performing__ def fail!(code, data = nil) - check_performing do - raise self.class::Failure.new(code, data) - end + check_performing! + raise self.class::Failure.new(code, data) end def success!(data = nil) - check_performing do - proceed_return(self, Result.new(data)) - end + check_performing! + result_method_called! + proceed_return(self, Result.new(data)) end - def check_performing - if __performing__ - yield - else - error_message = - "It looks like #call instance method was called directly in #{self.class}. " \ - "You must always use class-level `.call` or `.call!` method." + def check_performing! + return if __performing__ - raise InvalidCommandCall, error_message - end + error_message = + "It looks like #call instance method was called directly in #{self.class}. " \ + "You must always use class-level `.call` or `.call!` method." + + raise InvalidCommandCall, error_message + end + + def result_method_called! + self.__result_method__called__ = true end def proceed_return(service, data) diff --git a/resol.gemspec b/resol.gemspec index 46b1418..b2957ed 100644 --- a/resol.gemspec +++ b/resol.gemspec @@ -16,4 +16,6 @@ Gem::Specification.new do |spec| spec.required_ruby_version = Gem::Requirement.new(">= 3.1.0") spec.files = `git ls-files -z`.split("\x0").reject { |f| f.include?("spec") } spec.require_paths = ["lib"] + + spec.add_dependency "dry-container" end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 95c209a..d9c25e7 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -22,6 +22,7 @@ end end +require "dry/container/stub" require "resol" require "pry" @@ -30,22 +31,17 @@ require "resol/plugins/dummy" +Resol::DependencyContainer.enable_stubs! +Resol::DependencyContainer.stub("tools.base_login_manager", ) + class SmartService < Resol::Service - use_initializer! :smartcore + inject_initializer! :smartcore_injector end class ReturnEngineService < Resol::Service BASE_CLASS = self - use_initializer! :dry - - class << self - private - - def manager - @manager ||= Resol::Plugins::Manager.new(self) - end - end + inject_initializer! :dry_injector end ReturnEngineService.plugin(:return_in_service) From 9c27c025c72247b3dad708422995205917efae87 Mon Sep 17 00:00:00 2001 From: AnotherRegularDude Date: Sun, 15 Dec 2024 22:55:54 +0300 Subject: [PATCH 11/21] Fix oops --- lib/resol/configuration.rb | 2 +- lib/resol/{dependency_handler.rb => dependency_container.rb} | 2 +- lib/resol/plugins.rb | 2 +- lib/resol/service.rb | 4 ++-- spec/spec_helper.rb | 1 - 5 files changed, 5 insertions(+), 6 deletions(-) rename lib/resol/{dependency_handler.rb => dependency_container.rb} (95%) diff --git a/lib/resol/configuration.rb b/lib/resol/configuration.rb index f9c9923..a05a63c 100644 --- a/lib/resol/configuration.rb +++ b/lib/resol/configuration.rb @@ -5,7 +5,7 @@ class Configuration DEFAULT_CONFIG_VALUES = { classes_allowed_to_patch: ["Resol::Service"] }.freeze def initialize - self.data = DEFAULT_CONFIG_VALUES.deep_dup + self.data = DEFAULT_CONFIG_VALUES.dup end DEFAULT_CONFIG_VALUES.each_key do |setting_name| diff --git a/lib/resol/dependency_handler.rb b/lib/resol/dependency_container.rb similarity index 95% rename from lib/resol/dependency_handler.rb rename to lib/resol/dependency_container.rb index 9718fc2..7263bbe 100644 --- a/lib/resol/dependency_handler.rb +++ b/lib/resol/dependency_container.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Resol - class DependencyHandler + class DependencyContainer extend Dry::Container::Mixin namespace(:external_libs) do diff --git a/lib/resol/plugins.rb b/lib/resol/plugins.rb index b74cb1f..936a0de 100644 --- a/lib/resol/plugins.rb +++ b/lib/resol/plugins.rb @@ -31,7 +31,7 @@ def plugin(caller_class, plugin_name) private - attr_accessor :plugins + attr_accessor :allowed_classes, :plugins def resolve_allowed_classes Resol.config.classes_allowed_to_patch.map { |name| Object.const_get(name) } diff --git a/lib/resol/service.rb b/lib/resol/service.rb index 67483df..58617cb 100644 --- a/lib/resol/service.rb +++ b/lib/resol/service.rb @@ -39,7 +39,7 @@ def inherited(klass) end def inject_initializer!(injector_name) - injector = DependencyContainer.resolve("libs.#{injector_name}") + injector = DependencyContainer.resolve("external_libs.#{injector_name}") injector.inject!(self) end @@ -70,7 +70,7 @@ def call!(...) private def manager - @manager ||= DependencyContainer.resolve(:base_plugin_manager) + @manager ||= DependencyContainer.resolve("lib.plugin_manager") end def handle_catch(service, &) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index d9c25e7..a99c4f4 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -32,7 +32,6 @@ require "resol/plugins/dummy" Resol::DependencyContainer.enable_stubs! -Resol::DependencyContainer.stub("tools.base_login_manager", ) class SmartService < Resol::Service inject_initializer! :smartcore_injector From ad5c7ffd23fdd1d1d402f9f770a23fd1f59b8c7e Mon Sep 17 00:00:00 2001 From: AnotherRegularDude Date: Mon, 16 Dec 2024 00:44:44 +0300 Subject: [PATCH 12/21] Fixing specs --- lib/resol/plugins.rb | 12 +++--- lib/resol/result.rb | 2 +- spec/initializers_spec.rb | 83 --------------------------------------- spec/service_spec.rb | 7 ++-- spec/spec_helper.rb | 7 +--- 5 files changed, 12 insertions(+), 99 deletions(-) delete mode 100644 spec/initializers_spec.rb diff --git a/lib/resol/plugins.rb b/lib/resol/plugins.rb index 936a0de..15e606c 100644 --- a/lib/resol/plugins.rb +++ b/lib/resol/plugins.rb @@ -6,6 +6,10 @@ module Resol module Plugins PLUGINS_PATH = Pathname("resol/plugins") class Manager + def self.resolve_module(module_name) + Plugins.const_get(module_name) + end + def initialize self.allowed_classes = resolve_allowed_classes self.plugins = [] @@ -14,7 +18,7 @@ def initialize def plugin(caller_class, plugin_name) plugin_name = plugin_name.to_s - return if allowed_classes.exclude?(caller_class) + return unless allowed_classes.include?(caller_class) return if plugins.include?(plugin_name) plugin_module = find_plugin_module(plugin_name) @@ -39,15 +43,11 @@ def resolve_allowed_classes def find_plugin_module(plugin_name) require PLUGINS_PATH.join(plugin_name) - resolve_module(classify_plugin_name(plugin_name)) + self.class.resolve_module(classify_plugin_name(plugin_name)) rescue LoadError, NameError => e raise ArgumentError, "Failed to load plugin '#{plugin_name}': #{e.message}" end - def resolve_module(module_name) - Plugins.const_get(module_name) - end - def classify_plugin_name(string) string.split(/_|-/).map!(&:capitalize).join end diff --git a/lib/resol/result.rb b/lib/resol/result.rb index 0d1facd..d5d65cf 100644 --- a/lib/resol/result.rb +++ b/lib/resol/result.rb @@ -1,4 +1,4 @@ -# frozen_string_literal: true +1# frozen_string_literal: true module Resol class UnwrapError < StandardError; end diff --git a/spec/initializers_spec.rb b/spec/initializers_spec.rb deleted file mode 100644 index 84a3a8c..0000000 --- a/spec/initializers_spec.rb +++ /dev/null @@ -1,83 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe Resol::Initializers do - def apply_initializer(forced_service_class = nil) - described_class.apply!(forced_service_class || service_class, initializer_name) - end - - before { stub_const("InitializerTestClass", service_class) } - - let(:service_class) do - Class.new(Resol::Service) do - def call - success! - end - end - end - - let(:initializer_name) { :dry } - let(:dry_modules) do - ["Dry::Initializer::Mixin::Root", start_with("Dry::Initializer::Mixin::Local")] - end - - it "properly extend service class with initializer" do - apply_initializer - expect(service_class.ancestors.map(&:to_s)).to include(*dry_modules) - end - - context "with unknown initializer lib" do - let(:initializer_name) { :kek } - - specify do - expect { apply_initializer }.to raise_error(ArgumentError, "unknown initializer kek") - end - end - - context "with already prepared parent" do - before { stub_const("SecondChildService", second_child_service) } - - let(:service_class) do - Class.new(ReturnEngineService) do - def call - success! - end - end - end - - let(:second_child_service) do - Class.new(InitializerTestClass) - end - - let(:error_message) do - "ReturnEngineService or his superclasses already used initialize lib" - end - - specify do - expect { apply_initializer(SecondChildService) }.to raise_error(ArgumentError, error_message) - end - end - - context "with manually extended service" do - before { stub_const("SecondChildService", second_child_service) } - - let(:service_class) do - Class.new(Resol::Service) do - extend Dry::Initializer - - def call - success! - end - end - end - - let(:second_child_service) do - Class.new(InitializerTestClass) - end - - let(:error_message) { "use ::use_initializer! method on desired service class" } - - specify do - expect { apply_initializer(SecondChildService) }.to raise_error(ArgumentError, error_message) - end - end -end diff --git a/spec/service_spec.rb b/spec/service_spec.rb index 314b665..f248b85 100644 --- a/spec/service_spec.rb +++ b/spec/service_spec.rb @@ -314,10 +314,11 @@ def call context "when install plugin on the child service class" do let(:child_service) { Class.new(Resol::Service) } - let(:error_message) { "can load plugins only on base Resol::Service" } + it "just skips installation" do + child_service.plugin(:dummy) + manager = child_service.send(:manager) - it "raises error" do - expect { child_service.plugin(:dump) }.to raise_error(ArgumentError, error_message) + expect(manager.send(:plugins)).to eq([]) end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index a99c4f4..778689e 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -32,6 +32,7 @@ require "resol/plugins/dummy" Resol::DependencyContainer.enable_stubs! +Resol.config.send(:data=, { classes_allowed_to_patch: ["Resol::Service", "ReturnEngineService"] }) class SmartService < Resol::Service inject_initializer! :smartcore_injector @@ -52,10 +53,4 @@ class ReturnEngineService < Resol::Service config.order = :random Kernel.srand config.seed - - config.around do |ex| - applied_classes = Resol::Initializers.send(:applied_classes).dup - ex.call - Resol::Initializers.send(:applied_classes=, applied_classes) - end end From 31dd7d698348de46a0e06738a1a982095ffd7c90 Mon Sep 17 00:00:00 2001 From: AnotherRegularDude Date: Mon, 16 Dec 2024 00:45:48 +0300 Subject: [PATCH 13/21] Fix --- lib/resol/result.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/resol/result.rb b/lib/resol/result.rb index d5d65cf..0d1facd 100644 --- a/lib/resol/result.rb +++ b/lib/resol/result.rb @@ -1,4 +1,4 @@ -1# frozen_string_literal: true +# frozen_string_literal: true module Resol class UnwrapError < StandardError; end From 70cb4394f6d81ff85836341597b4df7ebc9ff491 Mon Sep 17 00:00:00 2001 From: AnotherRegularDude Date: Mon, 16 Dec 2024 02:11:05 +0300 Subject: [PATCH 14/21] Finish refactoring and fixing after refactoring --- Gemfile.lock | 9 +++++++++ README.md | 4 ++-- lib/resol.rb | 9 +++++---- lib/resol/configuration.rb | 19 ------------------- lib/resol/injector.rb | 4 ++-- resol.gemspec | 1 + spec/configuration_spec.rb | 27 --------------------------- spec/manager_spec.rb | 24 ++++++++++++++---------- spec/resol_spec.rb | 14 -------------- spec/service_spec.rb | 12 ++++++++++++ spec/spec_helper.rb | 9 ++++++++- 11 files changed, 53 insertions(+), 79 deletions(-) delete mode 100644 lib/resol/configuration.rb delete mode 100644 spec/configuration_spec.rb diff --git a/Gemfile.lock b/Gemfile.lock index bf40452..6ebd048 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,6 +2,7 @@ PATH remote: . specs: resol (1.0.0) + dry-configurable dry-container GEM @@ -37,8 +38,15 @@ GEM diff-lcs (1.5.1) docile (1.4.1) drb (2.2.1) + dry-configurable (1.2.0) + dry-core (~> 1.0, < 2) + zeitwerk (~> 2.6) dry-container (0.11.0) concurrent-ruby (~> 1.0) + dry-core (1.0.2) + concurrent-ruby (~> 1.0) + logger + zeitwerk (~> 2.6) dry-inflector (1.1.0) dry-initializer (3.1.1) i18n (1.14.6) @@ -134,6 +142,7 @@ GEM umbrellio-sequel-plugins (0.17.0) sequel unicode-display_width (2.6.0) + zeitwerk (2.7.1) PLATFORMS arm64-darwin-21 diff --git a/README.md b/README.md index 98ad809..472eebe 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ You can use both providers for a different services: # Types is a namespace for all types, defined by smart_types. class FirstService < Resol::Service - use_initializer! :smartcore + inject_initializer :smartcore param :first, Types::String param :second, Types::Integer @@ -76,7 +76,7 @@ end # Types is a namespace for all types, defined by dry-types. class SecondService < Resol::Service - use_initializer! :dry + inject_initializer :dry param :first, Types::Strict::String param :second, Types::Strict::Integer diff --git a/lib/resol.rb b/lib/resol.rb index 3b0e9bb..bba380e 100644 --- a/lib/resol.rb +++ b/lib/resol.rb @@ -1,21 +1,22 @@ # frozen_string_literal: true +require "dry-configurable" require "dry-container" require_relative "resol/version" -require_relative "resol/configuration" require_relative "resol/injector" require_relative "resol/service" require_relative "resol/plugins" + require_relative "resol/dependency_container" module Resol extend self - def config - @config ||= Configuration.new - end + extend Dry::Configurable + + setting :classes_allowed_to_patch, default: ["Resol::Service"] # rubocop:disable Naming/MethodName def Success(...) diff --git a/lib/resol/configuration.rb b/lib/resol/configuration.rb deleted file mode 100644 index a05a63c..0000000 --- a/lib/resol/configuration.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -module Resol - class Configuration - DEFAULT_CONFIG_VALUES = { classes_allowed_to_patch: ["Resol::Service"] }.freeze - - def initialize - self.data = DEFAULT_CONFIG_VALUES.dup - end - - DEFAULT_CONFIG_VALUES.each_key do |setting_name| - define_method(setting_name) { data.fetch(setting_name) } - end - - private - - attr_accessor :data - end -end diff --git a/lib/resol/injector.rb b/lib/resol/injector.rb index ebcaa5f..b48589a 100644 --- a/lib/resol/injector.rb +++ b/lib/resol/injector.rb @@ -9,7 +9,7 @@ def initialize(proc_register) end def inject!(service_class) - error!("parent or this class already injected") if service_class.is_a?(InjectMarker) + error!("parent or this class already injected") if service_class.include?(InjectMarker) service_class.instance_eval(&proc_register) service_class.include(InjectMarker) @@ -20,7 +20,7 @@ def inject!(service_class) attr_accessor :proc_register def error!(msg) - raise msg + raise msg end end end diff --git a/resol.gemspec b/resol.gemspec index b2957ed..ad56eef 100644 --- a/resol.gemspec +++ b/resol.gemspec @@ -17,5 +17,6 @@ Gem::Specification.new do |spec| spec.files = `git ls-files -z`.split("\x0").reject { |f| f.include?("spec") } spec.require_paths = ["lib"] + spec.add_dependency "dry-configurable" spec.add_dependency "dry-container" end diff --git a/spec/configuration_spec.rb b/spec/configuration_spec.rb deleted file mode 100644 index 020ff9f..0000000 --- a/spec/configuration_spec.rb +++ /dev/null @@ -1,27 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe Resol::Configuration do - before { allow(described_class).to receive(:smart_not_loaded?).and_return(const_not_loaded?) } - - let(:const_not_loaded?) { true } - - it "#smart_config returns nil" do - expect(described_class.smart_config).to eq(nil) - end - - context "with loaded const" do - let(:const_not_loaded?) { false } - - it "returns config" do - expect(described_class.smart_config).to eq(SmartCore::Initializer::Configuration.config) - end - end - - context "with original method" do - before { allow(described_class).to receive(:smart_not_loaded?).and_call_original } - - it "returns smartcore config" do - expect(described_class.smart_config).to eq(SmartCore::Initializer::Configuration.config) - end - end -end diff --git a/spec/manager_spec.rb b/spec/manager_spec.rb index f56b11e..797f489 100644 --- a/spec/manager_spec.rb +++ b/spec/manager_spec.rb @@ -1,7 +1,12 @@ # frozen_string_literal: true RSpec.describe Resol::Plugins::Manager do - let(:manager) { described_class.new(service_double) } + before do + Resol.config.classes_allowed_to_patch = %w[DummyService Resol::Service ReturnEngineService] + end + before { stub_const("DummyService", service_double) } + + let(:manager) { described_class.new } let(:service_double) do class_double( @@ -12,30 +17,29 @@ let(:singleton_double) { double(prepend: true) } it "skips all prepends" do - manager.plugin(:dummy) + manager.plugin(DummyService, :dummy) expect(service_double).not_to receive(:prepend) expect(singleton_double).not_to receive(:prepend) end context "when uses same plugin few times" do - before { allow(manager).to receive(:resolve_module).and_return(Resol::Plugins::Dummy) } - - before { manager.plugin(:dummy) } + before { allow(described_class).to receive(:resolve_module).and_return(Resol::Plugins::Dummy) } let(:manager_plugins) { manager.instance_variable_get(:@plugins) } it "doesn't load plugin second time" do - manager.plugin(:dummy) + manager.plugin(DummyService, :dummy) + manager.plugin(DummyService, :dummy) - expect(manager).to have_received(:resolve_module).once + expect(described_class).to have_received(:resolve_module).once expect(manager_plugins).to eq(["dummy"]) end end context "when can't require plugin" do specify do - expect { manager.plugin(:not_existed_plugin) }.to raise_error do |error| + expect { manager.plugin(DummyService, :not_existed_plugin) }.to raise_error do |error| expect(error).to be_an_instance_of(ArgumentError) expect(error.message).to include("Failed to load plugin 'not_existed_plugin': ") end @@ -43,10 +47,10 @@ end context "when can't resolve module" do - before { allow(manager).to receive(:resolve_module).and_raise(NameError, "msg") } + before { allow(described_class).to receive(:resolve_module).and_raise(NameError, "msg") } specify do - expect { manager.plugin(:dummy) }.to raise_error do |error| + expect { manager.plugin(DummyService, :dummy) }.to raise_error do |error| expect(error).to be_an_instance_of(ArgumentError) expect(error.message).to include("Failed to load plugin 'dummy': msg") end diff --git a/spec/resol_spec.rb b/spec/resol_spec.rb index d419f6d..46f84fe 100644 --- a/spec/resol_spec.rb +++ b/spec/resol_spec.rb @@ -4,18 +4,4 @@ it "has a version number" do expect(Resol::VERSION).not_to be nil end - - describe "#config" do - specify do - expect(described_class.config).to eq(Resol::Configuration) - end - end - - describe "#configure" do - specify do - Resol.configure do |config| - expect(config.smart_config).to eq(SmartCore::Initializer::Configuration.config) - end - end - end end diff --git a/spec/service_spec.rb b/spec/service_spec.rb index f248b85..1d102b6 100644 --- a/spec/service_spec.rb +++ b/spec/service_spec.rb @@ -330,4 +330,16 @@ def call expect(first_manager).to eq(second_manager) end end + + context "when inherited from already injected service" do + let(:child_service) { Class.new(SmartService) } + let(:injecting_proc) { proc { inject_initializer!(:dry_injector) } } + + it "tries to inject initializer" do + expect { child_service.class_eval(&injecting_proc) }.to raise_error do |error| + expect(error).to be_instance_of(RuntimeError) + expect(error.message).to eq("parent or this class already injected") + end + end + end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 778689e..5472d2d 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -23,6 +23,7 @@ end require "dry/container/stub" +require "dry/configurable/test_interface" require "resol" require "pry" @@ -31,8 +32,12 @@ require "resol/plugins/dummy" +module Resol + enable_test_interface +end + Resol::DependencyContainer.enable_stubs! -Resol.config.send(:data=, { classes_allowed_to_patch: ["Resol::Service", "ReturnEngineService"] }) +Resol.config.classes_allowed_to_patch = %w[Resol::Service ReturnEngineService] class SmartService < Resol::Service inject_initializer! :smartcore_injector @@ -53,4 +58,6 @@ class ReturnEngineService < Resol::Service config.order = :random Kernel.srand config.seed + + config.before { Resol.reset_config } end From 6740cfe15dcd31aa6eb5a1bf824c02dced274517 Mon Sep 17 00:00:00 2001 From: AnotherRegularDude Date: Mon, 16 Dec 2024 02:12:41 +0300 Subject: [PATCH 15/21] Install gems with 3.1 --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 6ebd048..ad65ea0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -142,7 +142,7 @@ GEM umbrellio-sequel-plugins (0.17.0) sequel unicode-display_width (2.6.0) - zeitwerk (2.7.1) + zeitwerk (2.6.18) PLATFORMS arm64-darwin-21 From ce03d811567cfe3eb6efd2467ca8c32498ef9c6b Mon Sep 17 00:00:00 2001 From: AnotherRegularDude Date: Mon, 16 Dec 2024 02:22:14 +0300 Subject: [PATCH 16/21] Limiting versions of gems in gemspec --- Gemfile.lock | 4 ++-- resol.gemspec | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index ad65ea0..624438c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,8 +2,8 @@ PATH remote: . specs: resol (1.0.0) - dry-configurable - dry-container + dry-configurable (~> 1.2.0) + dry-container (~> 0.11) GEM remote: https://rubygems.org/ diff --git a/resol.gemspec b/resol.gemspec index ad56eef..8868ce3 100644 --- a/resol.gemspec +++ b/resol.gemspec @@ -17,6 +17,6 @@ Gem::Specification.new do |spec| spec.files = `git ls-files -z`.split("\x0").reject { |f| f.include?("spec") } spec.require_paths = ["lib"] - spec.add_dependency "dry-configurable" - spec.add_dependency "dry-container" + spec.add_dependency "dry-configurable", "~>1.2.0" + spec.add_dependency "dry-container", "~>0.11" end From 5f9edf5f40c505f04ac47e711b6aeff1fb33e650 Mon Sep 17 00:00:00 2001 From: AnotherRegularDude Date: Mon, 16 Dec 2024 02:42:42 +0300 Subject: [PATCH 17/21] require all lib files after module definition --- README.md | 7 ++++--- lib/resol/result.rb | 12 ++++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 472eebe..a496ca9 100644 --- a/README.md +++ b/README.md @@ -65,10 +65,9 @@ Descendants of a parent, into which initializer logic has already been imported, You can use both providers for a different services: ```ruby - # Types is a namespace for all types, defined by smart_types. class FirstService < Resol::Service - inject_initializer :smartcore + inject_initializer :smartcore_injector param :first, Types::String param :second, Types::Integer @@ -76,7 +75,7 @@ end # Types is a namespace for all types, defined by dry-types. class SecondService < Resol::Service - inject_initializer :dry + inject_initializer :dry_injector param :first, Types::Strict::String param :second, Types::Strict::Integer @@ -86,6 +85,8 @@ end Both initializers support inheritance. And base features for initialization flow like default value, arguments accessors visibility level, coercible attributes and so on. +List of all supported initializers you can see at `DependencyContainer` definition. + #### Return a result **Note** – calling `success!`/`fail!` methods interrupts `call` method execution. diff --git a/lib/resol/result.rb b/lib/resol/result.rb index 0d1facd..7dd7368 100644 --- a/lib/resol/result.rb +++ b/lib/resol/result.rb @@ -84,4 +84,16 @@ def bind = self alias fmap bind end + + # TODO: Should be in a module, which includes in classes. + # Example; + # rubocop:disable Naming/MethodName + def Success(...) + Success.new(...) + end + + def Failure(...) + Failure.new(...) + end + # rubocop:enable Naming/MethodName end From eb9d721e32d0c842c50e431022d61896a443d4e1 Mon Sep 17 00:00:00 2001 From: AnotherRegularDude Date: Mon, 16 Dec 2024 03:08:19 +0300 Subject: [PATCH 18/21] Fix --- lib/resol.rb | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/lib/resol.rb b/lib/resol.rb index bba380e..dcbf33e 100644 --- a/lib/resol.rb +++ b/lib/resol.rb @@ -17,14 +17,4 @@ module Resol extend Dry::Configurable setting :classes_allowed_to_patch, default: ["Resol::Service"] - - # rubocop:disable Naming/MethodName - def Success(...) - Success.new(...) - end - - def Failure(...) - Failure.new(...) - end - # rubocop:enable Naming/MethodName end From cb905b5619ffeac28f4cd7fcdcf77416a4eb8e73 Mon Sep 17 00:00:00 2001 From: AnotherRegularDude Date: Mon, 16 Dec 2024 04:35:35 +0300 Subject: [PATCH 19/21] Fix bug, where the block was not transferred when the instance was split --- lib/resol/plugins.rb | 1 + lib/resol/service.rb | 6 +++--- spec/service_spec.rb | 13 +++++++++++++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/lib/resol/plugins.rb b/lib/resol/plugins.rb index 15e606c..75b1c57 100644 --- a/lib/resol/plugins.rb +++ b/lib/resol/plugins.rb @@ -5,6 +5,7 @@ module Resol module Plugins PLUGINS_PATH = Pathname("resol/plugins") + class Manager def self.resolve_module(module_name) Plugins.const_get(module_name) diff --git a/lib/resol/service.rb b/lib/resol/service.rb index 58617cb..1f07c78 100644 --- a/lib/resol/service.rb +++ b/lib/resol/service.rb @@ -47,13 +47,13 @@ def plugin(...) manager.plugin(self, ...) end - def call(...) - service = build(...) + def call(*args, **kwargs, &) + service = build(*args, **kwargs) result = handle_catch(service) do service.instance_variable_set(:@__performing__, true) __run_callbacks__(service) - service.call + service.call(&) end return Resol::Success(result.data) if service.__result_method__called__ diff --git a/spec/service_spec.rb b/spec/service_spec.rb index 1d102b6..4197aec 100644 --- a/spec/service_spec.rb +++ b/spec/service_spec.rb @@ -99,6 +99,12 @@ def call end end +class YieldingService < SmartService + def call + success!(yield) + end +end + class PluginSuccessService < ReturnEngineService def call success!(:success_result) @@ -243,6 +249,13 @@ def call expect { HackyService.call!(0) }.to raise_error(Resol::Service::InvalidCommandCall) end end + + context "when block passed to the service" do + it "yields block" do + result = YieldingService.call! { "kek" } + expect(result).to eq("kek") + end + end end context "with Return on success plugin" do From a4eebfc1b80dd0cc1ce83d9c865ec7f5b920be83 Mon Sep 17 00:00:00 2001 From: AnotherRegularDude Date: Mon, 16 Dec 2024 05:53:56 +0300 Subject: [PATCH 20/21] Move all requirements below first module definition --- lib/resol.rb | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/resol.rb b/lib/resol.rb index dcbf33e..e75638b 100644 --- a/lib/resol.rb +++ b/lib/resol.rb @@ -3,14 +3,6 @@ require "dry-configurable" require "dry-container" -require_relative "resol/version" - -require_relative "resol/injector" -require_relative "resol/service" -require_relative "resol/plugins" - -require_relative "resol/dependency_container" - module Resol extend self @@ -18,3 +10,11 @@ module Resol setting :classes_allowed_to_patch, default: ["Resol::Service"] end + +require_relative "resol/version" + +require_relative "resol/injector" +require_relative "resol/plugins" +require_relative "resol/service" + +require_relative "resol/dependency_container" From 52567c44f11a76eeeae683567443381b508835dc Mon Sep 17 00:00:00 2001 From: AnotherRegularDude Date: Sat, 11 Jan 2025 11:48:04 +0300 Subject: [PATCH 21/21] Remove DI, small refactoring --- .github/workflows/test.yml | 4 +- Gemfile.lock | 62 +++++++++++++------------- lib/resol.rb | 14 +++--- lib/resol/dependency_container.rb | 27 ----------- lib/resol/injector.rb | 29 ++++++++++-- lib/resol/plugins.rb | 4 +- lib/resol/plugins/return_in_service.rb | 10 ++++- lib/resol/service.rb | 40 ++++++++++++++--- resol.gemspec | 1 - spec/service_spec.rb | 2 +- spec/spec_helper.rb | 6 +-- 11 files changed, 110 insertions(+), 89 deletions(-) delete mode 100644 lib/resol/dependency_container.rb diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e5349c3..809cc6b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,7 +16,7 @@ jobs: - uses: actions/checkout@v2 - uses: ruby/setup-ruby@v1 with: - ruby-version: "3.3" + ruby-version: "3.4" bundler-cache: true - name: Run Linter run: bundle exec ci-helper RubocopLint @@ -43,7 +43,7 @@ jobs: strategy: fail-fast: false matrix: - ruby: ["3.1", "3.2"] + ruby: ["3.1", "3.2", "3.3"] experimental: [false] include: - ruby: head diff --git a/Gemfile.lock b/Gemfile.lock index 624438c..5cc8f56 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,7 +3,6 @@ PATH specs: resol (1.0.0) dry-configurable (~> 1.2.0) - dry-container (~> 0.11) GEM remote: https://rubygems.org/ @@ -23,7 +22,7 @@ GEM ast (2.4.2) base64 (0.2.0) benchmark (0.4.0) - bigdecimal (3.1.8) + bigdecimal (3.1.9) bundler-audit (0.9.2) bundler (>= 1.2.0, < 3) thor (~> 1.0) @@ -34,41 +33,40 @@ GEM coderay (1.1.3) colorize (1.1.0) concurrent-ruby (1.3.4) - connection_pool (2.4.1) + connection_pool (2.5.0) diff-lcs (1.5.1) docile (1.4.1) drb (2.2.1) dry-configurable (1.2.0) dry-core (~> 1.0, < 2) zeitwerk (~> 2.6) - dry-container (0.11.0) - concurrent-ruby (~> 1.0) - dry-core (1.0.2) + dry-core (1.1.0) concurrent-ruby (~> 1.0) logger zeitwerk (~> 2.6) - dry-inflector (1.1.0) - dry-initializer (3.1.1) + dry-inflector (1.2.0) + dry-initializer (3.2.0) i18n (1.14.6) concurrent-ruby (~> 1.0) - json (2.9.0) + json (2.9.1) language_server-protocol (3.17.0.3) - logger (1.6.3) + logger (1.6.5) method_source (1.1.0) minitest (5.25.4) parallel (1.26.3) parser (3.3.6.0) ast (~> 2.4.1) racc - pry (0.15.0) + pry (0.15.2) coderay (~> 1.1) method_source (~> 1.0) - qonfig (0.28.0) + qonfig (0.30.0) + base64 (>= 0.2) racc (1.8.1) rack (3.1.8) rainbow (3.1.1) rake (13.2.1) - regexp_parser (2.9.3) + regexp_parser (2.10.0) rspec (3.13.0) rspec-core (~> 3.13.0) rspec-expectations (~> 3.13.0) @@ -82,45 +80,45 @@ GEM diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) rspec-support (3.13.2) - rubocop (1.66.1) + rubocop (1.69.2) json (~> 2.3) language_server-protocol (>= 3.17.0) parallel (~> 1.10) parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 2.4, < 3.0) - rubocop-ast (>= 1.32.2, < 2.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.36.2, < 2.0) ruby-progressbar (~> 1.7) - unicode-display_width (>= 2.4.0, < 3.0) + unicode-display_width (>= 2.4.0, < 4.0) rubocop-ast (1.37.0) parser (>= 3.3.1.0) - rubocop-config-umbrellio (1.66.0.99) - rubocop (~> 1.66.0) + rubocop-config-umbrellio (1.69.0.101) + rubocop (~> 1.69.0) rubocop-factory_bot (~> 2.26.0) - rubocop-performance (~> 1.22.0) - rubocop-rails (~> 2.26.0) + rubocop-performance (~> 1.23.0) + rubocop-rails (~> 2.28.0) rubocop-rake (~> 0.6.0) - rubocop-rspec (~> 3.0.0) - rubocop-sequel (~> 0.3.3) + rubocop-rspec (~> 3.3.0) + rubocop-sequel (~> 0.3.0) rubocop-factory_bot (2.26.1) rubocop (~> 1.61) - rubocop-performance (1.22.1) + rubocop-performance (1.23.1) rubocop (>= 1.48.1, < 2.0) rubocop-ast (>= 1.31.1, < 2.0) - rubocop-rails (2.26.2) + rubocop-rails (2.28.0) activesupport (>= 4.2.0) rack (>= 1.1) rubocop (>= 1.52.0, < 2.0) rubocop-ast (>= 1.31.1, < 2.0) rubocop-rake (0.6.0) rubocop (~> 1.0) - rubocop-rspec (3.0.5) + rubocop-rspec (3.3.0) rubocop (~> 1.61) - rubocop-sequel (0.3.7) + rubocop-sequel (0.3.8) rubocop (~> 1.0) ruby-progressbar (1.13.0) - securerandom (0.4.0) - sequel (5.87.0) + securerandom (0.4.1) + sequel (5.88.0) bigdecimal simplecov (0.22.0) docile (~> 1.1) @@ -141,7 +139,9 @@ GEM concurrent-ruby (~> 1.0) umbrellio-sequel-plugins (0.17.0) sequel - unicode-display_width (2.6.0) + unicode-display_width (3.1.3) + unicode-emoji (~> 4.0, >= 4.0.4) + unicode-emoji (4.0.4) zeitwerk (2.6.18) PLATFORMS @@ -166,4 +166,4 @@ DEPENDENCIES smart_initializer BUNDLED WITH - 2.4.10 + 2.6.2 diff --git a/lib/resol.rb b/lib/resol.rb index e75638b..8d3fc6a 100644 --- a/lib/resol.rb +++ b/lib/resol.rb @@ -1,7 +1,11 @@ # frozen_string_literal: true require "dry-configurable" -require "dry-container" +require_relative "resol/version" + +require_relative "resol/injector" +require_relative "resol/plugins" +require_relative "resol/service" module Resol extend self @@ -10,11 +14,3 @@ module Resol setting :classes_allowed_to_patch, default: ["Resol::Service"] end - -require_relative "resol/version" - -require_relative "resol/injector" -require_relative "resol/plugins" -require_relative "resol/service" - -require_relative "resol/dependency_container" diff --git a/lib/resol/dependency_container.rb b/lib/resol/dependency_container.rb deleted file mode 100644 index 7263bbe..0000000 --- a/lib/resol/dependency_container.rb +++ /dev/null @@ -1,27 +0,0 @@ -# frozen_string_literal: true - -module Resol - class DependencyContainer - extend Dry::Container::Mixin - - namespace(:external_libs) do - register(:smartcore_injector, memoize: true) do - require "smart_core/initializer" - - installer_proc = proc { include SmartCore::Initializer } - Resol::Injector.new(installer_proc) - end - - register(:dry_injector, memoize: true) do - require "dry/initializer" - - installer_proc = proc { extend Dry::Initializer } - Resol::Injector.new(installer_proc) - end - end - - namespace(:lib) do - register(:plugin_manager) { Plugins::Manager.new } - end - end -end diff --git a/lib/resol/injector.rb b/lib/resol/injector.rb index b48589a..3a2ed62 100644 --- a/lib/resol/injector.rb +++ b/lib/resol/injector.rb @@ -1,23 +1,44 @@ # frozen_string_literal: true module Resol + # The Injector class is responsible for injecting initialization logic provider. + # + # Supported providers: + # - :smart -> uses the `smart_core/initializer` + # - :dry -> uses the `dry-initializer` class Injector InjectMarker = Module.new + NAME_TO_METHOD_MAPPING = { + smart: :inject_smart, + dry: :inject_dry, + }.freeze - def initialize(proc_register) - self.proc_register = proc_register + def self.inject_smart(service) + require "smart_core/initializer" + + service.include(SmartCore::Initializer) + end + + def self.inject_dry(service) + require "dry/initializer" + + service.extend(Dry::Initializer) + end + + def initialize(initializer_name) + self.called_method = NAME_TO_METHOD_MAPPING.fetch(initializer_name.to_sym) end def inject!(service_class) error!("parent or this class already injected") if service_class.include?(InjectMarker) - service_class.instance_eval(&proc_register) + self.class.public_send(called_method, service_class) service_class.include(InjectMarker) end private - attr_accessor :proc_register + attr_accessor :called_method def error!(msg) raise msg diff --git a/lib/resol/plugins.rb b/lib/resol/plugins.rb index 75b1c57..c4531c5 100644 --- a/lib/resol/plugins.rb +++ b/lib/resol/plugins.rb @@ -24,11 +24,11 @@ def plugin(caller_class, plugin_name) plugin_module = find_plugin_module(plugin_name) if defined?(plugin_module::InstanceMethods) - caller_class.prepend(plugin_module::InstanceMethods) + caller_class.include(plugin_module::InstanceMethods) end if defined?(plugin_module::ClassMethods) - caller_class.singleton_class.prepend(plugin_module::ClassMethods) + caller_class.extend(plugin_module::ClassMethods) end plugins << plugin_name diff --git a/lib/resol/plugins/return_in_service.rb b/lib/resol/plugins/return_in_service.rb index d04a72b..d336b03 100644 --- a/lib/resol/plugins/return_in_service.rb +++ b/lib/resol/plugins/return_in_service.rb @@ -12,9 +12,15 @@ def handle_catch(_service) end module InstanceMethods - private + module PrependedMethods + private + + def proceed_return(_service, data) = data + end - def proceed_return(_service, data) = data + def self.included(service) + service.prepend(PrependedMethods) + end end end end diff --git a/lib/resol/service.rb b/lib/resol/service.rb index 1f07c78..9536159 100644 --- a/lib/resol/service.rb +++ b/lib/resol/service.rb @@ -38,15 +38,33 @@ def inherited(klass) super end - def inject_initializer!(injector_name) - injector = DependencyContainer.resolve("external_libs.#{injector_name}") - injector.inject!(self) + # Allows you to configure an initializer library for this service, + # such as `dry-initializer` or `smart_core/initializer`. + # + # @param initializer_name [String, Symbol] + # The name of the initializer (e.g., `:dry` or `:smart`). + # @return [void] + def use_initializer!(initializer_name) + Resol::Injector.new(initializer_name).inject!(self) end def plugin(...) manager.plugin(self, ...) end + # Calls the service using class-level invocation. Builds the service object, + # runs any callbacks, and invokes the `#call` instance method. + # + # @param args [Array] Positional arguments for building service instance. + # @param kwargs [Hash] Keyword arguments for building service instance. + # @yield [block] An optional block passed into service's `#call`. + # + # @return [Resol::Success, Resol::Failure] + # Returns a `Resol::Success` if the service completed via `success!`, + # or a `Resol::Failure` if exited with`fail!` calling. + # + # @raise [InvalidCommandImplementation] + # If neither `#success!` nor `#fail!` is called inside the `#call` method. def call(*args, **kwargs, &) service = build(*args, **kwargs) @@ -63,6 +81,18 @@ def call(*args, **kwargs, &) Resol::Failure(e) end + # Same as {call}, but attempts to resolve value from monad. + # If the result is a failure, it raises the error instead of returning a `Resol::Failure`. + # + # @param args [Array] Positional arguments for building the service instance. + # @param kwargs [Hash] Keyword arguments for building the service instance. + # @yield [block] An optional block passed to the service's `#call`. + # + # @return [Object] + # Returns a value, with which instance of service interrupts with {#success!}. + # + # @raise [self::Failure] + # Raises an error if the service fails. def call!(...) call(...).value_or { |error| raise error } end @@ -70,7 +100,7 @@ def call!(...) private def manager - @manager ||= DependencyContainer.resolve("lib.plugin_manager") + @manager ||= Plugins::Manager.new end def handle_catch(service, &) @@ -78,8 +108,6 @@ def handle_catch(service, &) end end - # @!method call - attr_accessor :__result_method__called__ private diff --git a/resol.gemspec b/resol.gemspec index 8868ce3..398eefc 100644 --- a/resol.gemspec +++ b/resol.gemspec @@ -18,5 +18,4 @@ Gem::Specification.new do |spec| spec.require_paths = ["lib"] spec.add_dependency "dry-configurable", "~>1.2.0" - spec.add_dependency "dry-container", "~>0.11" end diff --git a/spec/service_spec.rb b/spec/service_spec.rb index 4197aec..b476971 100644 --- a/spec/service_spec.rb +++ b/spec/service_spec.rb @@ -346,7 +346,7 @@ def call context "when inherited from already injected service" do let(:child_service) { Class.new(SmartService) } - let(:injecting_proc) { proc { inject_initializer!(:dry_injector) } } + let(:injecting_proc) { proc { use_initializer!(:dry) } } it "tries to inject initializer" do expect { child_service.class_eval(&injecting_proc) }.to raise_error do |error| diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 5472d2d..84f889e 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -22,7 +22,6 @@ end end -require "dry/container/stub" require "dry/configurable/test_interface" require "resol" require "pry" @@ -36,17 +35,16 @@ module Resol enable_test_interface end -Resol::DependencyContainer.enable_stubs! Resol.config.classes_allowed_to_patch = %w[Resol::Service ReturnEngineService] class SmartService < Resol::Service - inject_initializer! :smartcore_injector + use_initializer! :smart end class ReturnEngineService < Resol::Service BASE_CLASS = self - inject_initializer! :dry_injector + use_initializer! :dry end ReturnEngineService.plugin(:return_in_service)