diff --git a/Gemfile b/Gemfile index 4f99b18..32be448 100644 --- a/Gemfile +++ b/Gemfile @@ -10,6 +10,4 @@ gem 'rspec', '~> 3.0' gem 'activesupport' gem 'combustion' -gem 'dry-container' -gem 'dry-system' gem 'rubocop-gp', github: 'corp-gp/rubocop-gp', require: false diff --git a/Gemfile.lock b/Gemfile.lock index d2f1c00..a5ed47b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -37,44 +37,31 @@ GEM tzinfo (~> 2.0) ast (2.4.2) base64 (0.1.1) - builder (3.2.4) - combustion (1.3.7) + builder (3.3.0) + combustion (1.5.0) activesupport (>= 3.0.0) railties (>= 3.0.0) thor (>= 0.14.6) concurrent-ruby (1.2.2) crass (1.0.6) diff-lcs (1.5.0) - dry-auto_inject (1.0.1) - dry-core (~> 1.0) - zeitwerk (~> 2.6) dry-configurable (1.1.0) dry-core (~> 1.0, < 2) zeitwerk (~> 2.6) - dry-container (0.11.0) - concurrent-ruby (~> 1.0) dry-core (1.0.1) concurrent-ruby (~> 1.0) zeitwerk (~> 2.6) - dry-inflector (1.0.0) - dry-system (1.0.1) - dry-auto_inject (~> 1.0, < 2) - dry-configurable (~> 1.0, < 2) - dry-core (~> 1.0, < 2) - dry-inflector (~> 1.0, < 2) - erubi (1.12.0) + erubi (1.13.0) i18n (1.14.1) concurrent-ruby (~> 1.0) json (2.6.3) language_server-protocol (3.17.0.3) - loofah (2.21.3) + loofah (2.23.1) crass (~> 1.0.2) nokogiri (>= 1.12.0) - method_source (1.0.0) + method_source (1.1.0) minitest (5.19.0) - nokogiri (1.15.4-arm64-darwin) - racc (~> 1.4) - nokogiri (1.15.4-x86_64-linux) + nokogiri (1.16.8-x86_64-linux) racc (~> 1.4) parallel (1.23.0) parser (3.2.2.3) @@ -88,9 +75,9 @@ GEM activesupport (>= 5.0.0) minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.6.0) + rails-html-sanitizer (1.6.1) loofah (~> 2.21) - nokogiri (~> 1.14) + nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) railties (7.0.7) actionpack (= 7.0.7) activesupport (= 7.0.7) @@ -145,7 +132,7 @@ GEM rubocop-capybara (~> 2.17) rubocop-factory_bot (~> 2.22) ruby-progressbar (1.13.0) - thor (1.2.2) + thor (1.3.2) tzinfo (2.0.6) concurrent-ruby (~> 1.0) unicode-display_width (2.4.2) @@ -159,8 +146,6 @@ DEPENDENCIES active_dry_deps! activesupport combustion - dry-container - dry-system rake (~> 13.0) rspec (~> 3.0) rubocop-gp! diff --git a/active_dry_deps.gemspec b/active_dry_deps.gemspec index 02ac80d..7d185d6 100644 --- a/active_dry_deps.gemspec +++ b/active_dry_deps.gemspec @@ -9,9 +9,11 @@ Gem::Specification.new do |spec| spec.email = ['andruhafirst@yandex.ru'] spec.summary = 'Dependency injection and resolution support for classes and modules.' - spec.description = 'ActiveDryDeps not modify constructor and support Dependency Injection for modules. - Also you can import method from any object in your container. - Adding extra dependencies is easy and improve readability your code.' + spec.description = <<~DESCRIPTION + ActiveDryDeps does not modify constructor and supports Dependency Injection for modules. + Also you can import method from any object in your container. + Adding extra dependencies is easy and improve readability your code. + DESCRIPTION spec.homepage = 'https://github.com/corp-gp/active_dry_deps' spec.license = 'MIT' spec.required_ruby_version = '>= 3.0.0' diff --git a/lib/active_dry_deps.rb b/lib/active_dry_deps.rb index 43f6cc4..eceecb0 100644 --- a/lib/active_dry_deps.rb +++ b/lib/active_dry_deps.rb @@ -6,7 +6,9 @@ module ActiveDryDeps - autoload :Deps, 'active_dry_deps/deps' + autoload :Deps, 'active_dry_deps/deps' + autoload :Dependency, 'active_dry_deps/dependency' + autoload :Container, 'active_dry_deps/container' class Error < StandardError; end class DependencyNameInvalid < Error; end diff --git a/lib/active_dry_deps/configuration.rb b/lib/active_dry_deps/configuration.rb index 4bf674a..ef61efc 100644 --- a/lib/active_dry_deps/configuration.rb +++ b/lib/active_dry_deps/configuration.rb @@ -6,7 +6,6 @@ module ActiveDryDeps extend Dry::Configurable - setting :container setting :inflector, default: ActiveSupport::Inflector setting :inject_global_constant, default: 'Deps' diff --git a/lib/active_dry_deps/container.rb b/lib/active_dry_deps/container.rb new file mode 100644 index 0000000..b778e02 --- /dev/null +++ b/lib/active_dry_deps/container.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module ActiveDryDeps + class Container < Hash + + def resolve(const_name) + unless key?(const_name) + self[const_name] = Object.const_get(const_name) + end + + self[const_name] + end + + end +end diff --git a/lib/active_dry_deps/dependency.rb b/lib/active_dry_deps/dependency.rb new file mode 100644 index 0000000..5447a94 --- /dev/null +++ b/lib/active_dry_deps/dependency.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +module ActiveDryDeps + class Dependency + + VALID_METHOD_NAME = /^[a-zA-Z_0-9]+$/ + VALID_CONST_NAME = /^[[:upper:]][a-zA-Z_0-9\:]*$/ + METHODS_AS_KLASS = %w[perform_later call].freeze + + attr_reader :receiver_method_name, :receiver_method, :const_name, :method_name + + def initialize(resolver, receiver_method_alias: nil) + if resolver.respond_to?(:call) + receiver_method_by_proc(resolver, receiver_method_alias: receiver_method_alias) + else + receiver_method_by_const_name(resolver, receiver_method_alias: receiver_method_alias) + end + end + + def callable? + !@receiver_method.nil? + end + + def receiver_method_string + if @method_name + <<~RUBY + # def create_order(*args, **options, &block) + # ::ActiveDryDeps::Deps::CONTAINER.resolve("OrderService::Create").call(*args, **options, &block) + # end + + def #{@receiver_method_name}(*args, **options, &block) + ::ActiveDryDeps::Deps::CONTAINER.resolve("#{@const_name}").#{@method_name}(*args, **options, &block) + end + RUBY + else + <<~RUBY + # def create_order_service + # ::ActiveDryDeps::Deps::CONTAINER.resolve("OrderService::Create") + # end + + def #{@receiver_method_name} + ::ActiveDryDeps::Deps::CONTAINER.resolve("#{@const_name}") + end + RUBY + end + end + + private def receiver_method_by_proc(resolver, receiver_method_alias: nil) + unless receiver_method_alias + raise MissingAlias, "Alias is required while injecting with Proc" + end + + @receiver_method_name = receiver_method_alias + + unless VALID_METHOD_NAME.match?(@receiver_method_name.to_s) + raise DependencyNameInvalid, "name +#{@receiver_method_name}+ is not a valid Ruby identifier" + end + + @receiver_method = resolver + end + + private def receiver_method_by_const_name(resolver, receiver_method_alias: nil) + @const_name, @method_name = resolver.to_s.split('.', 2) + + unless VALID_CONST_NAME.match?(@const_name) + raise DependencyNameInvalid, "+#{resolver}+ must contains valid constant name" + end + + if @method_name && !VALID_METHOD_NAME.match?(@method_name) + raise DependencyNameInvalid, "name +#{@method_name}+ is not a valid Ruby identifier" + end + + @receiver_method_name = + if receiver_method_alias + receiver_method_alias + elsif @method_name && METHODS_AS_KLASS.exclude?(@method_name) + @method_name + elsif @const_name + @const_name.split('::').last + else + resolver + end + + unless VALID_METHOD_NAME.match?(@receiver_method_name.to_s) + raise DependencyNameInvalid, "name +#{@receiver_method_name}+ is not a valid Ruby identifier" + end + end + + end +end \ No newline at end of file diff --git a/lib/active_dry_deps/deps.rb b/lib/active_dry_deps/deps.rb index bd17fed..e3ca62b 100644 --- a/lib/active_dry_deps/deps.rb +++ b/lib/active_dry_deps/deps.rb @@ -3,8 +3,7 @@ module ActiveDryDeps module Deps - VALID_NAME = /([a-zA-Z_0-9]*)$/ - METHODS_AS_KLASS = %w[perform_later call].freeze + CONTAINER = Container.new module_function @@ -12,53 +11,31 @@ module Deps # include Deps['Lib::Routes.admin'] use as `admin` # include Deps['Lib::Routes'] use as `Routes()` # include Deps['OrderService::Recalculate.call'] use as `Recalculate()` + # include Deps[send_email: -> { 'email-sent' }] use as `send_email` def [](*keys, **aliases) - str_methods = +'' - - keys.each { |resolver| str_methods << str_method(resolver, nil) } - aliases.each { |alias_method, resolver| str_methods << str_method(resolver, alias_method) } - m = Module.new - m.module_eval(str_methods) - m - end - - private def str_method(resolve, alias_method) - resolve_klass, extract_method = resolve.split('.') - alias_method ||= - if extract_method && METHODS_AS_KLASS.exclude?(extract_method) - extract_method - else - resolve_klass.split('::').last - end + dependencies = [] + dependencies += keys.map { |resolver| Dependency.new(resolver) } + dependencies += aliases.map { |alias_method, resolver| Dependency.new(resolver, receiver_method_alias: alias_method) } - if alias_method && !VALID_NAME.match?(alias_method.to_s) - raise DependencyNameInvalid, "name +#{alias_method}+ is not a valid Ruby identifier" - end + call_dependencies, constant_dependencies = dependencies.partition(&:callable?) - key = resolve_key(resolve_klass) + m.module_eval(constant_dependencies.map(&:receiver_method_string).join("\n")) - if extract_method - %(def #{alias_method}(...); ::#{ActiveDryDeps.config.container}['#{key}'].#{extract_method}(...) end\n) - else - %(def #{alias_method}; ::#{ActiveDryDeps.config.container}['#{key}'] end\n) + call_dependencies.each do |dependency| + m.define_method(dependency.receiver_method_name, &dependency.receiver_method) end - end - def resolve_key(key) - ActiveDryDeps.config.inflector.underscore(key).tr('/', '.') + m end - instance_eval <<~RUBY, __FILE__, __LINE__ + 1 - # def resolve(key) - # ::MyApp::Container[resolve_key(key)] - # end - - def resolve(key) - ::#{ActiveDryDeps.config.container}[resolve_key(key)] - end - RUBY + # TODO: необходимость сомнительна + def resolve(resolver) + dependency = Dependency.new(resolver) + m = Module.new { module_function module_eval(dependency.receiver_method_string) } + m.public_send(dependency.receiver_method_name) + end end end diff --git a/lib/active_dry_deps/railtie.rb b/lib/active_dry_deps/railtie.rb index f0a9a80..3e49d39 100644 --- a/lib/active_dry_deps/railtie.rb +++ b/lib/active_dry_deps/railtie.rb @@ -4,9 +4,6 @@ module ActiveDryDeps class Railtie < ::Rails::Railtie config.to_prepare do - app_namespace = ::Rails.application.class.to_s.split('::').first - ActiveDryDeps.config.container ||= "#{app_namespace}::Container" - Object.const_set(ActiveDryDeps.config.inject_global_constant, ::ActiveDryDeps::Deps) ActiveDryDeps.config.finalize!(freeze_values: true) end diff --git a/lib/active_dry_deps/stub.rb b/lib/active_dry_deps/stub.rb index 5fd9fd8..0742064 100644 --- a/lib/active_dry_deps/stub.rb +++ b/lib/active_dry_deps/stub.rb @@ -2,16 +2,34 @@ module ActiveDryDeps - module Stub + module StubDeps - CONTAINER_CONST = Object.const_get(ActiveDryDeps.config.container) - - def stub(path, ...) - CONTAINER_CONST.stub(Deps.resolve_key(path), ...) + def stub(const_name, proxy_object, &block) + self::CONTAINER.stub(const_name, proxy_object, &block) end def unstub(*keys) - CONTAINER_CONST.unstub(*keys.map { resolve_key(_1) }) + self::CONTAINER.unstub(*keys) + end + + end + + module StubContainer + + def stub(const_name, proxy_object) + if block_given? + begin + self[const_name] = proxy_object + ensure + delete(const_name) + end + else + self[const_name] = proxy_object + end + end + + def unstub(*unstub_keys) + (unstub_keys.empty? ? keys : unstub_keys).each { |const_name| delete(const_name) } end end @@ -19,7 +37,8 @@ def unstub(*keys) module Deps def self.enable_stubs! - extend Stub + Deps::CONTAINER.extend(StubContainer) + Deps.extend StubDeps end end diff --git a/spec/active_dry_deps_spec.rb b/spec/active_dry_deps_spec.rb index 8a887a1..f0d5826 100644 --- a/spec/active_dry_deps_spec.rb +++ b/spec/active_dry_deps_spec.rb @@ -2,20 +2,20 @@ RSpec.describe ActiveDryDeps do it 'all dependencies works' do - expect(CreateOrder.call).to eq %w[CreateDeparture CreateDeparture perform_later message-ok] - end - - it 'dependencies resolved' do - expect(Deps.resolve('CreateDeparture')).to eq CreateDeparture - expect(Deps.resolve('SupplierSync::ReserveJob')).to eq SupplierSync::ReserveJob - expect(Deps.resolve('supplier_sync.reserve_job')).to eq SupplierSync::ReserveJob + expect(CreateOrder.call).to eq %w[CreateDeparture CreateDeparture job-performed message-ok email-sent] end it 'stub dependencies with `deps`' do service = CreateOrder.new - expect(service).to deps(CreateDepartureCallable: '1', CreateDeparture: double(call: '2'), ReserveJob: '3', message: '4') - expect(service.call).to eq %w[1 2 3 4] + expect(service).to deps( + CreateDepartureCallable: '1', + CreateDeparture: double(call: '2'), + ReserveJob: '3', + message: '4', + send_mail: '5', + ) + expect(service.call).to eq %w[1 2 3 4 5] end it 'stub dependency with `deps` not runnable' do @@ -25,17 +25,40 @@ service.call(is_message: false) end - it 'direct stub with `Deps.sub`' do - Deps.stub('create_departure', double(call: '1')) - - expect(CreateOrder.call).to eq %w[1 1 perform_later message-ok] + it 'invalid method identifier not allowed' do + expect { + Class.new { include Deps['CreateOrder.!invalid_identifier'] } + }.to raise_error(ActiveDryDeps::DependencyNameInvalid, 'name +!invalid_identifier+ is not a valid Ruby identifier') + end - Deps.unstub + describe '#resolve' do + it 'dependencies resolved' do + expect(Deps.resolve('CreateDeparture')).to eq CreateDeparture + expect(Deps.resolve('SupplierSync::ReserveJob')).to eq SupplierSync::ReserveJob + end end - it 'direct stub with `Deps.sub` with block' do - Deps.stub('create_departure', double(call: '1')) do - expect(CreateOrder.call).to eq %w[1 1 perform_later message-ok] + describe '#stub' do + def expect_call_orig + expect(CreateOrder.call).to eq %w[CreateDeparture CreateDeparture job-performed message-ok email-sent] + end + + it 'direct stub with `Deps.stub`' do + Deps.stub('CreateDeparture', double(call: '1')) + + expect(CreateOrder.call).to eq %w[1 1 job-performed message-ok email-sent] + + Deps.unstub + + expect_call_orig + end + + it 'direct stub with `Deps.sub` with block' do + Deps.stub('CreateDeparture', double(call: '1')) do + expect(CreateOrder.call).to eq %w[1 1 job-performed message-ok email-sent] + end + + expect_call_orig end end end diff --git a/spec/app/create_order.rb b/spec/app/create_order.rb index b6d41fa..a744a61 100644 --- a/spec/app/create_order.rb +++ b/spec/app/create_order.rb @@ -10,6 +10,7 @@ def self.call 'CreateDeparture', 'Utils.message', 'SupplierSync::ReserveJob.perform_later', + send_mail: -> { 'email-sent' }, CreateDepartureCallable: 'CreateDeparture.call', ] @@ -19,6 +20,7 @@ def call(is_message: true) CreateDeparture().call, ReserveJob(), (message('ok') if is_message), + send_mail, ] end diff --git a/spec/app/supplier_sync/reserve_job.rb b/spec/app/supplier_sync/reserve_job.rb index a0db795..96e86b2 100644 --- a/spec/app/supplier_sync/reserve_job.rb +++ b/spec/app/supplier_sync/reserve_job.rb @@ -4,7 +4,7 @@ module SupplierSync class ReserveJob def self.perform_later - 'perform_later' + 'job-performed' end end diff --git a/spec/support/system.rb b/spec/support/system.rb index 58dece0..e7cb6bb 100644 --- a/spec/support/system.rb +++ b/spec/support/system.rb @@ -1,36 +1,4 @@ # frozen_string_literal: true -LOADER = - Class.new(Dry::System::Loader) do - def self.call(component, *args) - constant = self.constant(component) - - if singleton?(constant) - constant.instance(*args) - else - constant # constant.new(*args) - THIS LINE REWRITED from Dry::System::Loader - end - end - end - -require 'dry/system/container' -module Combustion - class Container < Dry::System::Container - - configure do |config| - config.root = Pathname('./spec') - config.component_dirs.add 'app' do |dir| - dir.loader = LOADER - end - end - - end -end - -require 'dry/core/container/stub' -Combustion::Container.enable_stubs! - require 'active_dry_deps/stub' -Deps.enable_stubs! - -Combustion::Container.finalize!(freeze: false) +Deps.enable_stubs! \ No newline at end of file