├── .devtools └── templates │ ├── changelog.erb │ └── release.erb ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug-report.md │ └── config.yml ├── SUPPORT.md └── workflows │ ├── ci.yml │ ├── docsite.yml │ ├── rubocop.yml │ └── sync_configs.yml ├── .gitignore ├── .repobot.yml ├── .rspec ├── .rubocop.yml ├── .yardopts ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Gemfile ├── Gemfile.devtools ├── LICENSE ├── README.md ├── Rakefile ├── bin ├── .gitkeep └── console ├── changelog.yml ├── docsite └── source │ ├── component-dirs.html.md │ ├── container.html.md │ ├── container │ └── hooks.html.md │ ├── dependency-auto-injection.html.md │ ├── external-provider-sources.html.md │ ├── index.html.md │ ├── plugins.html.md │ ├── providers.html.md │ ├── settings.html.md │ └── test-mode.html.md ├── dry-system.gemspec ├── examples ├── custom_configuration_auto_register │ ├── Gemfile │ ├── lib │ │ ├── entities │ │ │ └── user.rb │ │ └── user_repo.rb │ ├── run.rb │ └── system │ │ ├── boot │ │ └── persistence.rb │ │ ├── container.rb │ │ └── import.rb ├── standalone │ ├── Gemfile │ ├── lib │ │ ├── empty_service.rb │ │ ├── not_registered.rb │ │ ├── service_with_dependency.rb │ │ └── user_repo.rb │ ├── run.rb │ └── system │ │ ├── container.rb │ │ ├── import.rb │ │ └── providers │ │ └── persistence.rb └── zeitwerk │ ├── Gemfile │ ├── lib │ ├── service_with_dependency.rb │ └── user_repo.rb │ ├── run.rb │ └── system │ ├── container.rb │ └── import.rb ├── lib ├── dry-system.rb └── dry │ ├── system.rb │ └── system │ ├── auto_registrar.rb │ ├── component.rb │ ├── component_dir.rb │ ├── config │ ├── component_dir.rb │ ├── component_dirs.rb │ ├── namespace.rb │ └── namespaces.rb │ ├── constants.rb │ ├── container.rb │ ├── errors.rb │ ├── identifier.rb │ ├── importer.rb │ ├── indirect_component.rb │ ├── loader.rb │ ├── loader │ └── autoloading.rb │ ├── magic_comments_parser.rb │ ├── manifest_registrar.rb │ ├── plugins.rb │ ├── plugins │ ├── bootsnap.rb │ ├── dependency_graph.rb │ ├── dependency_graph │ │ └── strategies.rb │ ├── env.rb │ ├── logging.rb │ ├── monitoring.rb │ ├── monitoring │ │ └── proxy.rb │ ├── notifications.rb │ ├── plugin.rb │ ├── zeitwerk.rb │ └── zeitwerk │ │ └── compat_inflector.rb │ ├── provider.rb │ ├── provider │ ├── source.rb │ └── source_dsl.rb │ ├── provider_registrar.rb │ ├── provider_source_registry.rb │ ├── provider_sources.rb │ ├── provider_sources │ ├── settings.rb │ └── settings │ │ ├── config.rb │ │ └── loader.rb │ ├── stubs.rb │ └── version.rb ├── project.yml └── spec ├── fixtures ├── app │ ├── lib │ │ ├── ignored_spec_service.rb │ │ └── spec_service.rb │ └── system │ │ └── providers │ │ └── client.rb ├── autoloading │ └── lib │ │ └── test │ │ ├── entities │ │ └── foo_entity.rb │ │ └── foo.rb ├── components │ └── test │ │ ├── bar.rb │ │ ├── bar │ │ ├── abc.rb │ │ └── baz.rb │ │ ├── foo.rb │ │ └── no_register.rb ├── components_with_errors │ └── test │ │ └── constant_error.rb ├── deprecations │ └── bootable_dirs_config │ │ └── system │ │ ├── boot │ │ └── logger.rb │ │ └── custom_boot │ │ └── logger.rb ├── external_components │ ├── alt-components │ │ ├── db.rb │ │ └── logger.rb │ ├── components │ │ ├── logger.rb │ │ ├── mailer.rb │ │ └── notifier.rb │ └── lib │ │ └── external_components.rb ├── external_components_deprecated │ ├── components │ │ └── logger.rb │ └── lib │ │ └── external_components.rb ├── import_test │ ├── config │ │ └── application.yml │ └── lib │ │ └── test │ │ ├── bar.rb │ │ └── foo.rb ├── lazy_loading │ ├── auto_registration_disabled │ │ └── lib │ │ │ ├── entities │ │ │ └── kitten.rb │ │ │ └── fetch_kitten.rb │ └── shared_root_keys │ │ ├── lib │ │ └── kitten_service │ │ │ ├── fetch_kitten.rb │ │ │ └── submit_kitten.rb │ │ └── system │ │ └── providers │ │ └── kitten_service.rb ├── lazytest │ ├── config │ │ └── application.yml │ ├── lib │ │ └── test │ │ │ ├── dep.rb │ │ │ ├── foo.rb │ │ │ ├── models.rb │ │ │ └── models │ │ │ ├── book.rb │ │ │ └── user.rb │ └── system │ │ └── providers │ │ └── bar.rb ├── magic_comments │ └── comments.rb ├── manifest_registration │ ├── lib │ │ └── test │ │ │ └── foo.rb │ └── system │ │ └── registrations │ │ └── foo.rb ├── memoize_magic_comments │ └── test │ │ ├── memoize_false_comment.rb │ │ ├── memoize_no_comment.rb │ │ └── memoize_true_comment.rb ├── mixed_namespaces │ └── lib │ │ └── test │ │ ├── external │ │ └── external_component.rb │ │ └── my_app │ │ └── app_component.rb ├── multiple_namespaced_components │ └── multiple │ │ └── level │ │ ├── baz.rb │ │ └── foz.rb ├── multiple_provider_dirs │ ├── custom_bootables │ │ └── logger.rb │ └── default_bootables │ │ ├── inflector.rb │ │ └── logger.rb ├── namespaced_components │ └── namespaced │ │ ├── bar.rb │ │ └── foo.rb ├── other │ ├── config │ │ └── providers │ │ │ ├── bar.rb │ │ │ └── hell.rb │ └── lib │ │ └── test │ │ ├── dep.rb │ │ ├── foo.rb │ │ ├── models.rb │ │ └── models │ │ ├── book.rb │ │ └── user.rb ├── require_path │ └── lib │ │ └── test │ │ └── foo.rb ├── settings_test │ ├── .env │ └── types.rb ├── standard_container_with_default_namespace │ └── lib │ │ └── test │ │ ├── dep.rb │ │ └── example_with_dep.rb ├── standard_container_without_default_namespace │ └── lib │ │ └── test │ │ ├── dep.rb │ │ └── example_with_dep.rb ├── stubbing │ ├── lib │ │ └── test │ │ │ └── car.rb │ └── system │ │ └── providers │ │ └── db.rb ├── test │ ├── config │ │ ├── application.yml │ │ └── subapp.yml │ ├── lib │ │ └── test │ │ │ ├── dep.rb │ │ │ ├── foo.rb │ │ │ ├── models.rb │ │ │ ├── models │ │ │ ├── book.rb │ │ │ └── user.rb │ │ │ └── singleton_dep.rb │ ├── log │ │ └── .gitkeep │ └── system │ │ └── providers │ │ ├── bar.rb │ │ ├── client.rb │ │ ├── db.rb │ │ ├── hell.rb │ │ └── logger.rb ├── umbrella │ └── system │ │ └── providers │ │ └── db.rb └── unit │ ├── component │ ├── component_dir_1 │ │ ├── namespace │ │ │ └── nested │ │ │ │ ├── component_file.rb │ │ │ │ └── component_file_with_auto_register_false.rb │ │ └── outside_namespace │ │ │ └── component_file.rb │ └── component_dir_2 │ │ └── namespace │ │ └── nested │ │ └── component_file.rb │ └── component_dir │ └── component_file.rb ├── integration ├── boot_spec.rb ├── container │ ├── auto_registration │ │ ├── component_dir_namespaces │ │ │ ├── autoloading_loader_spec.rb │ │ │ ├── deep_namespace_paths_spec.rb │ │ │ ├── default_loader_spec.rb │ │ │ ├── multiple_namespaces_spec.rb │ │ │ └── namespaces_as_defaults_spec.rb │ │ ├── custom_auto_register_proc_spec.rb │ │ ├── custom_instance_proc_spec.rb │ │ ├── custom_loader_spec.rb │ │ ├── memoize_spec.rb │ │ └── mixed_namespaces_spec.rb │ ├── auto_registration_spec.rb │ ├── autoloading_spec.rb │ ├── importing │ │ ├── container_registration_spec.rb │ │ ├── exports_spec.rb │ │ ├── import_namespaces_spec.rb │ │ ├── imported_component_protection_spec.rb │ │ └── partial_imports_spec.rb │ ├── lazy_loading │ │ ├── auto_registration_disabled_spec.rb │ │ ├── bootable_components_spec.rb │ │ └── manifest_registration_spec.rb │ ├── plugins │ │ ├── bootsnap_spec.rb │ │ ├── dependency_graph_spec.rb │ │ ├── env_spec.rb │ │ ├── logging_spec.rb │ │ └── zeitwerk │ │ │ ├── eager_loading_spec.rb │ │ │ ├── namespaces_spec.rb │ │ │ ├── resolving_components_spec.rb │ │ │ └── user_configured_loader_spec.rb │ ├── plugins_spec.rb │ └── providers │ │ ├── conditional_providers_spec.rb │ │ ├── custom_provider_registrar_spec.rb │ │ ├── custom_provider_superclass_spec.rb │ │ ├── multiple_provider_dirs_spec.rb │ │ ├── provider_sources │ │ └── provider_options_spec.rb │ │ ├── registering_components_spec.rb │ │ └── resolving_root_key_spec.rb ├── did_you_mean_integration_spec.rb ├── external_components_spec.rb ├── import_spec.rb └── settings_component_spec.rb ├── spec_helper.rb ├── support ├── coverage.rb ├── loaded_constants_cleaning.rb ├── rspec_options.rb ├── tmp_directory.rb ├── warnings.rb └── zeitwerk_helpers.rb └── unit ├── auto_registrar_spec.rb ├── component_dir ├── component_for_identifier_key.rb └── each_component_spec.rb ├── component_dir_spec.rb ├── component_spec.rb ├── config ├── component_dirs_spec.rb └── namespaces_spec.rb ├── container ├── auto_register_spec.rb ├── boot_spec.rb ├── config │ └── root_spec.rb ├── configuration_spec.rb ├── decorate_spec.rb ├── hooks │ ├── after_hooks_spec.rb │ └── load_path_hook_spec.rb ├── hooks_spec.rb ├── import_spec.rb ├── injector_spec.rb ├── load_path_spec.rb ├── monitor_spec.rb └── notifications_spec.rb ├── container_spec.rb ├── errors_spec.rb ├── identifier_spec.rb ├── indirect_component_spec.rb ├── loader └── autoloading_spec.rb ├── loader_spec.rb ├── magic_comments_parser_spec.rb ├── provider └── source_spec.rb └── provider_sources └── settings └── loader_spec.rb /.devtools/templates/changelog.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <% releases.each_with_index do |r, idx| %> 5 | ## <%= r.version %> <%= r.date %> 6 | 7 | <% if r.summary %> 8 | <%= r.summary %> 9 | 10 | <% end %> 11 | 12 | <% if r.added? %> 13 | ### Added 14 | 15 | <% r.added.each do |log| %> 16 | - <%= log %> 17 | <% end %> 18 | 19 | <% end %> 20 | <% if r.fixed? %> 21 | ### Fixed 22 | 23 | <% r.fixed.each do |log| %> 24 | - <%= log %> 25 | <% end %> 26 | 27 | <% end %> 28 | <% if r.changed? %> 29 | ### Changed 30 | 31 | <% r.changed.each do |log| %> 32 | - <%= log %> 33 | <% end %> 34 | <% end %> 35 | <% curr_ver = r.date ? "v#{r.version}" : 'master' %> 36 | <% prev_rel = releases[idx + 1] %> 37 | <% if prev_rel %> 38 | <% ver_range = "v#{prev_rel.version}...#{curr_ver}" %> 39 | 40 | [Compare <%=ver_range%>](https://github.com/dry-rb/<%= project.name %>/compare/<%=ver_range%>) 41 | <% end %> 42 | 43 | <% end %> 44 | -------------------------------------------------------------------------------- /.devtools/templates/release.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <% if latest_release.summary %> 5 | <%= latest_release.summary %> 6 | 7 | <% end %> 8 | 9 | <% if latest_release.added? %> 10 | ### Added 11 | 12 | <% latest_release.added.each do |log| %> 13 | - <%= log %> 14 | <% end %> 15 | 16 | <% end %> 17 | <% if latest_release.fixed? %> 18 | ### Fixed 19 | 20 | <% latest_release.fixed.each do |log| %> 21 | - <%= log %> 22 | <% end %> 23 | 24 | <% end %> 25 | <% if latest_release.changed? %> 26 | ### Changed 27 | 28 | <% latest_release.changed.each do |log| %> 29 | - <%= log %> 30 | <% end %> 31 | <% end %> 32 | <% if previous_release %> 33 | <% ver_range = "v#{previous_release.version}...v#{latest_release.version}" %> 34 | 35 | [Compare <%=ver_range%>](https://github.com/dry-rb/<%= project.name %>/compare/<%=ver_range%>) 36 | <% end %> 37 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: hanami 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41B Bug report" 3 | about: See CONTRIBUTING.md for more information 4 | title: '' 5 | labels: bug, help wanted 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Describe the bug 11 | 12 | A clear and concise description of what the bug is. 13 | 14 | ## To Reproduce 15 | 16 | Provide detailed steps to reproduce, **an executable script would be best**. 17 | 18 | ## Expected behavior 19 | 20 | A clear and concise description of what you expected to happen. 21 | 22 | ## My environment 23 | 24 | - Affects my production application: **YES/NO** 25 | - Ruby version: ... 26 | - OS: ... 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Community Support 4 | url: https://discourse.dry-rb.org 5 | about: Please ask and answer questions here. 6 | -------------------------------------------------------------------------------- /.github/SUPPORT.md: -------------------------------------------------------------------------------- 1 | ## Support 2 | 3 | If you need help with any of the dry-rb libraries, feel free to ask questions on our [discussion forum](https://discourse.dry-rb.org/). This is the best place to seek help. Make sure to search for a potential solution in past threads before posting your question. Thanks! :heart: 4 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This file is synced from dry-rb/template-gem repo 2 | name: CI 3 | 4 | on: 5 | push: 6 | paths: 7 | - ".github/workflows/ci.yml" 8 | - "lib/**" 9 | - "*.gemspec" 10 | - "spec/**" 11 | - "Rakefile" 12 | - "Gemfile" 13 | - "Gemfile.devtools" 14 | - ".rubocop.yml" 15 | - "project.yml" 16 | pull_request: 17 | branches: 18 | - main 19 | create: 20 | 21 | jobs: 22 | tests: 23 | runs-on: ubuntu-latest 24 | name: Tests 25 | strategy: 26 | fail-fast: false 27 | matrix: 28 | ruby: 29 | - "3.4" 30 | - "3.3" 31 | # ensure 3.3.0 passes 32 | # see https://github.com/dry-rb/dry-system/issues/284 33 | - "3.3.0" 34 | - "3.2" 35 | - "3.1" 36 | include: 37 | - ruby: "3.4" 38 | coverage: "true" 39 | env: 40 | COVERAGE: ${{matrix.coverage}} 41 | COVERAGE_TOKEN: ${{secrets.CODACY_PROJECT_TOKEN}} 42 | steps: 43 | - name: Checkout 44 | uses: actions/checkout@v3 45 | - name: Install package dependencies 46 | run: "[ -e $APT_DEPS ] || sudo apt-get install -y --no-install-recommends $APT_DEPS" 47 | - name: Set up Ruby 48 | uses: ruby/setup-ruby@v1 49 | with: 50 | ruby-version: ${{matrix.ruby}} 51 | bundler-cache: true 52 | - name: Run all tests 53 | run: bundle exec rake 54 | release: 55 | runs-on: ubuntu-latest 56 | if: contains(github.ref, 'tags') && github.event_name == 'create' 57 | needs: tests 58 | env: 59 | GITHUB_LOGIN: dry-bot 60 | GITHUB_TOKEN: ${{secrets.GH_PAT}} 61 | steps: 62 | - uses: actions/checkout@v3 63 | - name: Install package dependencies 64 | run: "[ -e $APT_DEPS ] || sudo apt-get install -y --no-install-recommends $APT_DEPS" 65 | - name: Set up Ruby 66 | uses: ruby/setup-ruby@v1 67 | with: 68 | ruby-version: 3.4 69 | - name: Install dependencies 70 | run: gem install ossy --no-document 71 | - name: Trigger release workflow 72 | run: | 73 | tag=$(echo $GITHUB_REF | cut -d / -f 3) 74 | ossy gh w dry-rb/devtools release --payload "{\"tag\":\"$tag\",\"sha\":\"${{github.sha}}\",\"tag_creator\":\"$GITHUB_ACTOR\",\"repo\":\"$GITHUB_REPOSITORY\",\"repo_name\":\"${{github.event.repository.name}}\"}" 75 | -------------------------------------------------------------------------------- /.github/workflows/docsite.yml: -------------------------------------------------------------------------------- 1 | # This file is synced from dry-rb/template-gem repo 2 | 3 | name: docsite 4 | 5 | on: 6 | push: 7 | paths: 8 | - docsite/** 9 | - .github/workflows/docsite.yml 10 | branches: 11 | - main 12 | - release-** 13 | tags: 14 | 15 | jobs: 16 | update-docs: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v3 20 | with: 21 | fetch-depth: 0 22 | - run: | 23 | git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/* 24 | - name: Set up Ruby 25 | uses: ruby/setup-ruby@v1 26 | with: 27 | ruby-version: "3.0.5" 28 | - name: Set up git user 29 | run: | 30 | git config --local user.email "dry-bot@dry-rb.org" 31 | git config --local user.name "dry-bot" 32 | - name: Install dependencies 33 | run: gem install ossy --no-document 34 | - name: Update release branches 35 | run: | 36 | branches=`git log --format=%B -n 1 $GITHUB_SHA | grep "docsite:release-" || echo "nothing"` 37 | 38 | if [[ ! $branches -eq "nothing" ]]; then 39 | for b in $branches 40 | do 41 | name=`echo $b | ruby -e 'puts gets[/:(.+)/, 1].gsub(/\s+/, "")'` 42 | 43 | echo "merging $GITHUB_SHA to $name" 44 | 45 | git checkout -b $name --track origin/$name 46 | 47 | echo `git log -n 1` 48 | 49 | git cherry-pick $GITHUB_SHA -m 1 50 | done 51 | 52 | git push --all "https://dry-bot:${{secrets.GH_PAT}}@github.com/$GITHUB_REPOSITORY.git" 53 | 54 | git checkout main 55 | else 56 | echo "no need to update branches" 57 | fi 58 | - name: Trigger dry-rb.org deploy 59 | env: 60 | GITHUB_LOGIN: dry-bot 61 | GITHUB_TOKEN: ${{secrets.GH_PAT}} 62 | run: ossy github workflow dry-rb/dry-rb.org ci 63 | -------------------------------------------------------------------------------- /.github/workflows/rubocop.yml: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file is synced from dry-rb/template-gem repo 4 | 5 | name: RuboCop 6 | 7 | on: [push, pull_request] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | env: 16 | BUNDLE_ONLY: tools 17 | 18 | steps: 19 | - uses: actions/checkout@v3 20 | 21 | - name: Set up Ruby 3.2 22 | uses: ruby/setup-ruby@v1 23 | with: 24 | ruby-version: 3.2 25 | bundler-cache: true 26 | 27 | - name: Run RuboCop 28 | run: bundle exec rubocop --parallel 29 | -------------------------------------------------------------------------------- /.github/workflows/sync_configs.yml: -------------------------------------------------------------------------------- 1 | # This file is synced from dry-rb/template-gem repo 2 | 3 | name: sync 4 | 5 | on: 6 | repository_dispatch: 7 | push: 8 | branches: 9 | - "main" 10 | 11 | jobs: 12 | main: 13 | runs-on: ubuntu-latest 14 | if: (github.event_name == 'repository_dispatch' && github.event.action == 'sync_configs') || github.event_name != 'repository_dispatch' 15 | env: 16 | GITHUB_LOGIN: dry-bot 17 | GITHUB_TOKEN: ${{ secrets.GH_PAT }} 18 | steps: 19 | - name: Checkout ${{github.repository}} 20 | uses: actions/checkout@v3 21 | - name: Checkout devtools 22 | uses: actions/checkout@v3 23 | with: 24 | repository: dry-rb/devtools 25 | path: tmp/devtools 26 | - name: Setup git user 27 | run: | 28 | git config --local user.email "dry-bot@dry-rb.org" 29 | git config --local user.name "dry-bot" 30 | - name: Set up Ruby 31 | uses: ruby/setup-ruby@v1 32 | with: 33 | ruby-version: "3.1" 34 | - name: Install dependencies 35 | run: gem install ossy --no-document 36 | - name: Update changelog.yml from commit 37 | run: tmp/devtools/bin/update-changelog-from-commit $GITHUB_SHA 38 | - name: Compile CHANGELOG.md 39 | run: tmp/devtools/bin/compile-changelog 40 | - name: Commit 41 | run: | 42 | git add -A 43 | git commit -m "[devtools] sync" || echo "nothing to commit" 44 | - name: Push changes 45 | run: | 46 | git pull --rebase origin main 47 | git push https://dry-bot:${{secrets.GH_PAT}}@github.com/${{github.repository}}.git HEAD:main 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | *.log 4 | /.config 5 | /coverage/ 6 | /InstalledFiles 7 | /pkg/ 8 | /spec/reports/ 9 | /test/tmp/ 10 | /test/version_tmp/ 11 | /tmp/ 12 | 13 | ## Specific to RubyMotion: 14 | .dat* 15 | .repl_history 16 | build/ 17 | 18 | ## Documentation cache and generated files: 19 | /.yardoc/ 20 | /_yardoc/ 21 | /doc/ 22 | /rdoc/ 23 | 24 | ## Environment normalisation: 25 | /.bundle/ 26 | /vendor/bundle 27 | /lib/bundler/man/ 28 | 29 | # for a library or gem, you might want to ignore these files since the code is 30 | # intended to run in multiple environments; otherwise, check them in: 31 | # Gemfile.lock 32 | # .ruby-version 33 | # .ruby-gemset 34 | 35 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 36 | .rvmrc 37 | Gemfile.lock 38 | 39 | ## Specific to RubyMine 40 | .idea 41 | 42 | ## Tests 43 | /spec/examples.txt 44 | /spec/fixtures/test/tmp/cache 45 | -------------------------------------------------------------------------------- /.repobot.yml: -------------------------------------------------------------------------------- 1 | ########################################################### 2 | # DO NOT EDIT THIS FILE 3 | # 4 | # This is a config synced from dry-rb/template-gem repo 5 | ########################################################### 6 | 7 | sources: 8 | - repo: dry-rb/template-gem 9 | sync: 10 | - ".repobot.yml.erb" 11 | - ".devtools/templates/*.sync:${{dir}}/${{name}}" 12 | - ".github/**/*.*" 13 | - ".rspec" 14 | - ".rubocop.yml" 15 | - "gemspec.erb:dry-system.gemspec" 16 | - "spec/support/*" 17 | - "CODE_OF_CONDUCT.md" 18 | - "CONTRIBUTING.md" 19 | - "LICENSE.erb" 20 | - "README.md.erb" 21 | - "Gemfile.devtools" 22 | - repo: repobot-app/workflows 23 | sync: 24 | - ".github/workflows/*.yml" 25 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | --order random 4 | --warnings 5 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --no-private 2 | --hide-void-return 3 | 4 | --markup-provider=redcarpet 5 | --markup=markdown 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Issue Guidelines 2 | 3 | ## Reporting bugs 4 | 5 | If you found a bug, report an issue and describe what's the expected behavior versus what actually happens. If the bug causes a crash, attach a full backtrace. If possible, a reproduction script showing the problem is highly appreciated. 6 | 7 | ## Reporting feature requests 8 | 9 | Report a feature request **only after discussing it first on [discourse.dry-rb.org](https://discourse.dry-rb.org)** where it was accepted. Please provide a concise description of the feature. 10 | 11 | ## Reporting questions, support requests, ideas, concerns etc. 12 | 13 | **PLEASE DON'T** - use [discourse.dry-rb.org](https://discourse.dry-rb.org) instead. 14 | 15 | # Pull Request Guidelines 16 | 17 | A Pull Request will only be accepted if it addresses a specific issue that was reported previously, or fixes typos, mistakes in documentation etc. 18 | 19 | Other requirements: 20 | 21 | 1) Do not open a pull request if you can't provide tests along with it. If you have problems writing tests, ask for help in the related issue. 22 | 2) Follow the style conventions of the surrounding code. In most cases, this is standard ruby style. 23 | 3) Add API documentation if it's a new feature 24 | 4) Update API documentation if it changes an existing feature 25 | 5) Bonus points for sending a PR which updates user documentation in the `docsite` directory 26 | 27 | # Asking for help 28 | 29 | If these guidelines aren't helpful, and you're stuck, please post a message on [discourse.dry-rb.org](https://discourse.dry-rb.org). 30 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | eval_gemfile "Gemfile.devtools" 6 | 7 | gemspec 8 | 9 | # Remove verson constraint once latter versions release their -java packages 10 | gem "bootsnap" 11 | gem "dotenv" 12 | gem "dry-events" 13 | gem "dry-monitor" 14 | gem "dry-types" 15 | 16 | gem "zeitwerk" 17 | 18 | group :test do 19 | gem "ostruct" 20 | end 21 | -------------------------------------------------------------------------------- /Gemfile.devtools: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file is synced from dry-rb/template-gem repo 4 | 5 | gem "rake", ">= 12.3.3" 6 | 7 | group :test do 8 | gem "simplecov", require: false, platforms: :ruby 9 | gem "simplecov-cobertura", require: false, platforms: :ruby 10 | gem "rexml", require: false 11 | gem "rspec" 12 | 13 | gem "warning" 14 | end 15 | 16 | group :tools do 17 | gem "rubocop", "~> 1.69.2" 18 | gem "byebug" 19 | end 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2023 dry-rb team 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [gem]: https://rubygems.org/gems/dry-system 4 | [actions]: https://github.com/dry-rb/dry-system/actions 5 | 6 | # dry-system [![Gem Version](https://badge.fury.io/rb/dry-system.svg)][gem] [![CI Status](https://github.com/dry-rb/dry-system/workflows/CI/badge.svg)][actions] 7 | 8 | ## Links 9 | 10 | * [User documentation](https://dry-rb.org/gems/dry-system) 11 | * [API documentation](http://rubydoc.info/gems/dry-system) 12 | * [Forum](https://discourse.dry-rb.org) 13 | 14 | ## Supported Ruby versions 15 | 16 | This library officially supports the following Ruby versions: 17 | 18 | * MRI `>= 3.1` 19 | * jruby `>= 9.4` (not tested on CI) 20 | 21 | ## License 22 | 23 | See `LICENSE` file. 24 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | # frozen_string_literal: true 3 | 4 | require "bundler/gem_tasks" 5 | 6 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "lib")) 7 | 8 | require "rspec/core" 9 | require "rspec/core/rake_task" 10 | 11 | task default: :spec 12 | 13 | desc "Run all specs in spec directory" 14 | RSpec::Core::RakeTask.new(:spec) 15 | -------------------------------------------------------------------------------- /bin/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dry-rb/dry-system/dd4ad2f8c0d1821ae7414d47dfaa7482fae8a7d3/bin/.gitkeep -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | 6 | begin 7 | require "pry-byebug" 8 | Pry.start 9 | rescue LoadError 10 | require "irb" 11 | IRB.start 12 | end 13 | -------------------------------------------------------------------------------- /docsite/source/container.html.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Container 3 | layout: gem-single 4 | name: dry-system 5 | sections: 6 | - hooks 7 | --- 8 | 9 | The main API of dry-system is the abstract container that you inherit from. It allows you to configure basic settings and exposes APIs for requiring files easily. Container is the entry point to your application, and it encapsulates application's state. 10 | 11 | Let's say you want to define an application container that will provide a logger: 12 | 13 | ``` ruby 14 | require 'dry/system' 15 | 16 | class Application < Dry::System::Container 17 | configure do |config| 18 | config.root = Pathname('./my/app') 19 | end 20 | end 21 | 22 | # now you can register a logger 23 | require 'logger' 24 | Application.register('utils.logger', Logger.new($stdout)) 25 | 26 | # and access it 27 | Application['utils.logger'] 28 | ``` 29 | 30 | ### Auto-Registration 31 | 32 | By using simple naming conventions we can automatically register objects within our container. 33 | 34 | Let's provide a custom logger object and put it under a custom load-path that we will configure: 35 | 36 | ``` ruby 37 | require "dry/system" 38 | 39 | class Application < Dry::System::Container 40 | configure do |config| 41 | config.root = Pathname("./my/app") 42 | 43 | # Add a 'lib' component dir (relative to `root`), containing class definitions 44 | # that can be auto-registered 45 | config.component_dirs.add "lib" 46 | end 47 | end 48 | 49 | # under /my/app/lib/logger.rb we put 50 | class Logger 51 | # some neat logger implementation 52 | end 53 | 54 | # we can finalize the container which triggers auto-registration 55 | Application.finalize! 56 | 57 | # the logger becomes available 58 | Application["logger"] 59 | ``` 60 | -------------------------------------------------------------------------------- /docsite/source/container/hooks.html.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Hooks 3 | layout: gem-single 4 | name: dry-system 5 | --- 6 | 7 | There are a few lifecycle events that you can hook into if you need to ensure things happen in a particular order. 8 | 9 | Hooks are executed within the context of the container instance. 10 | 11 | ### `configure` Event 12 | 13 | You can register a callback to fire after the container is configured, which happens one of three ways: 14 | 15 | 1. The `configure` method is called on the container 16 | 2. The `configured!` method is called 17 | 3. The `finalize!` method is called when neither of the other two have been 18 | 19 | ```ruby 20 | class MyApp::Container < Dry::System::Container 21 | after(:configure) do 22 | # do something here 23 | end 24 | end 25 | ``` 26 | 27 | ### `register` Event 28 | 29 | Most of the time, you will know what keys you are working with ahead of time. But for certain cases you may want to 30 | react to keys dynamically. 31 | 32 | ```ruby 33 | class MyApp::Container < Dry::System::Container 34 | use :monitoring 35 | 36 | after(:register) do |key| 37 | next unless key.end_with?(".gateway") 38 | 39 | monitor(key) do |event| 40 | resolve(:logger).debug(key:, method: event[:method], time: event[:time]) 41 | end 42 | end 43 | end 44 | ``` 45 | 46 | Now let's say you register `api_client.gateway` into your container. Your API methods will be automatically monitored 47 | and their timing measured and logged. 48 | 49 | ### `finalize` Event 50 | 51 | Finalization is the point at which the container is made ready, such as booting a web application. 52 | 53 | The following keys are loaded in sequence: 54 | 55 | 1. Providers 56 | 2. Auto-registered components 57 | 3. Manually-registered components 58 | 4. Container imports 59 | 60 | At the conclusion of this process, the container is frozen thus preventing any further changes. This makes the 61 | `finalize` event quite important: it's the last call before your container will disallow mutation. 62 | 63 | Unlike the previous events, you can register before hooks in addition to after hooks. 64 | 65 | The after hooks will run immediately prior to the container freeze. This allows you to enumerate the container keys 66 | while they can still be mutated, such as with `decorate` or `monitor`. 67 | 68 | ```ruby 69 | class MyApp::Container < Dry::System::Container 70 | before(:finalize) do 71 | # Before system boot, no keys registered yet 72 | end 73 | 74 | after(:finalize) do 75 | # After system boot, all keys registered 76 | end 77 | end 78 | ``` 79 | -------------------------------------------------------------------------------- /docsite/source/dependency-auto-injection.html.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Dependency auto-injection 3 | layout: gem-single 4 | name: dry-system 5 | --- 6 | 7 | After defining your container, you can use its auto-injector as a mixin to declare a component's dependencies using their container keys. 8 | 9 | For example, if you have an `Application` container and an object that will need a logger: 10 | 11 | ``` ruby 12 | # system/import.rb 13 | require "system/container" 14 | Import = Application.injector 15 | 16 | # In a class definition you simply specify what it needs 17 | # lib/post_publisher.rb 18 | require "import" 19 | class PostPublisher 20 | include Import["logger"] 21 | 22 | def call(post) 23 | # some stuff 24 | logger.debug("post published: #{post}") 25 | end 26 | end 27 | ``` 28 | 29 | ### Auto-registered component keys 30 | 31 | When components are auto-registered, their default keys are based on their file paths and your [component dir](docs::component-dirs) configuration. For example, `lib/api/client.rb` will have the key `"api.client"` and will resolve an instance of `API::Client`. 32 | 33 | Resolving a component will also start a registered [provider](docs::providers) if it shares the same name as the root segment of its container key. This is useful in cases where a group of components require an additional dependency to be always made available. 34 | 35 | For example, if you have a group of repository objects that need a `persistence` provider to be started, all you need to do is to follow this naming convention: 36 | 37 | - `system/providers/persistence.rb` - where you register your `:persistence` provider 38 | - `lib/persistence/user_repo` - where you can define any components that need the components or setup established by the `persistence` provider 39 | 40 | Here's a sample setup for this scenario: 41 | 42 | ``` ruby 43 | # system/container.rb 44 | require "dry/system" 45 | 46 | class Application < Dry::System::Container 47 | configure do |config| 48 | config.root = Pathname("/my/app") 49 | config.component_dirs.add "lib" 50 | end 51 | end 52 | 53 | # system/import.rb 54 | require_relative "container" 55 | 56 | Import = Application.injector 57 | 58 | # system/providers/persistence.rb 59 | Application.register_provider(:persistence) do 60 | start do 61 | require "sequel" 62 | container.register("persistence.db", Sequel.connect(ENV['DB_URL'])) 63 | end 64 | 65 | stop do 66 | container["persistence.db"].disconnect 67 | end 68 | end 69 | 70 | # lib/persistence/user_repo.rb 71 | require "import" 72 | 73 | module Persistence 74 | class UserRepo 75 | include Import["persistence.db"] 76 | 77 | def find(conditions) 78 | db[:users].where(conditions) 79 | end 80 | end 81 | end 82 | ``` 83 | -------------------------------------------------------------------------------- /docsite/source/external-provider-sources.html.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: External provider sources 3 | layout: gem-single 4 | name: dry-system 5 | --- 6 | 7 | You can distribute your own components to other dry-system users via external provider sources, which can be used as the basis for providers within any dry-system container. 8 | 9 | Provider sources look and work the same as regular providers, which means allowing you to use their full lifecycle for creating, configuring, and registering your components. 10 | 11 | To distribute a group of provider sources (defined in their own files), register them with `Dry::System`: 12 | 13 | ``` ruby 14 | # my_gem 15 | # |- lib/my_gem/provider_sources.rb 16 | 17 | Dry::System.register_provider_sources(:common, boot_path: File.join(__dir__, "provider_sources")) 18 | ``` 19 | 20 | Then, define your provider source: 21 | 22 | ``` ruby 23 | # my_gem 24 | # |- lib/my_gem/provider_sources/exception_notifier.rb 25 | 26 | Dry::System.register_provider_source(:exception_notifier, group: :my_gem) do 27 | prepare do 28 | require "some_exception_notifier" 29 | end 30 | 31 | start do 32 | register(:exception_notifier, SomeExceptionNotifier.new) 33 | end 34 | end 35 | ``` 36 | 37 | Then you can use this provider source when you register a provider in a dry-system container: 38 | 39 | ``` ruby 40 | # system/app/container.rb 41 | 42 | require "dry/system" 43 | require "my_gem/provider_sources" 44 | 45 | module App 46 | class Container < Dry::System::Container 47 | register_provider(:exception_notifier, from: :my_gem) 48 | end 49 | end 50 | 51 | App::Container[:exception_notifier] 52 | ``` 53 | 54 | ### Customizing provider sources 55 | 56 | You can customize a provider source for your application via `before` and `after` callbacks for its lifecycle steps. 57 | 58 | For example, you can register additional components based on the provider source's own registrations via an `after(:start)` callback: 59 | 60 | ``` ruby 61 | module App 62 | class Container < Dry::System::Container 63 | register_provider(:exception_notifier, from: :my_gem) do 64 | after(:start) 65 | register(:my_notifier, container[:exception_notifier]) 66 | end 67 | end 68 | end 69 | end 70 | ``` 71 | 72 | The following callbacks are supported: 73 | 74 | - `before(:prepare)` 75 | - `after(:prepare)` 76 | - `before(:start)` 77 | - `after(:start)` 78 | 79 | ### Providing component configuration 80 | 81 | Provider sources can define their own settings using [dry-configurable’s](/gems/dry-configurable) `setting` API. These will be configured when the provider source is used by a provider. The other lifecycle steps in the provider souce can access the configured settings as `config`. 82 | 83 | For example, here’s an extended `:exception_notifier` provider source with settings: 84 | 85 | ``` ruby 86 | # my_gem 87 | # |- lib/my_gem/provider_sources/exception_notifier.rb 88 | 89 | Dry::System.register_component(:exception_notifier, provider: :common) do 90 | setting :environments, default: :production, constructor: Types::Strict::Array.of(Types::Strict::Symbol) 91 | setting :logger 92 | 93 | prepare do 94 | require "some_exception_notifier" 95 | end 96 | 97 | start do 98 | # Now we have access to `config` 99 | register(:exception_notifier, SomeExceptionNotifier.new(config.to_h)) 100 | end 101 | end 102 | ``` 103 | 104 | This defines two settings: 105 | 106 | - `:environments`, which is a list of environment identifiers with default value set to `[:production]` 107 | - `:logger`, an object that should be used as the logger, which must be configured 108 | 109 | To configure this provider source, you can use a `configure` block when defining your provider using the source: 110 | 111 | ``` ruby 112 | module App 113 | class Container < Dry::System::Container 114 | register_provider(:exception_notifier, from: :my_gem) do 115 | require "logger" 116 | 117 | configure do |config| 118 | config.logger = Logger.new($stdout) 119 | end 120 | end 121 | end 122 | end 123 | ``` 124 | -------------------------------------------------------------------------------- /docsite/source/index.html.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Introduction 3 | layout: gem-single 4 | name: dry-system 5 | type: gem 6 | sections: 7 | - container 8 | - component-dirs 9 | - providers 10 | - dependency-auto-injection 11 | - plugins 12 | - external-provider-sources 13 | - settings 14 | - test-mode 15 | --- 16 | 17 | Object dependency management system based on [dry-container](/gems/dry-container) and [dry-auto_inject](/gems/dry-auto_inject) allowing you to configure reusable components in any environment, set up their load-paths, require needed files and instantiate objects automatically with the ability to have them injected as dependencies. 18 | 19 | This library relies on very basic mechanisms provided by Ruby, specifically `require` and managing `$LOAD_PATH`. It doesn't use magic like automatic const resolution, it's pretty much the opposite and forces you to be explicit about dependencies in your applications. 20 | 21 | It does a couple of things for you: 22 | 23 | * Provides an abstract dependency container implementation 24 | * Integrates with an autoloader, or handles `$LOAD_PATH` for you and loads needed files using `require` 25 | * Resolves object dependencies automatically 26 | * Supports auto-registration of dependencies via file/dir naming conventions 27 | * Supports multi-system setups (ie your application is split into multiple sub-systems) 28 | * Supports configuring component providers, which can be used to share common components between many systems 29 | * Supports test-mode with convenient stubbing API 30 | 31 | To put it all together, this allows you to configure your system in a way where you have full control over dependencies and it's very easy to draw the boundaries between individual components. 32 | 33 | This comes with a bunch of nice benefits: 34 | 35 | * Your system relies on abstractions rather than concrete classes and modules 36 | * It helps in decoupling your code from 3rd party code 37 | * It makes it possible to load components in complete isolation. In example you can run a single test for a single component and only required files will be loaded, or you can run a rake task and it will only load the things it needs. 38 | * It opens up doors to better instrumentation and debugging tools 39 | 40 | You can use dry-system in a new application or add it to an existing application. It should Just Work™ but if it doesn't please [report an issue](https://github.com/dry-rb/dry-system/issues). 41 | 42 | ### Rails support 43 | 44 | If you want to use dry-system with Rails, it's recommended to use [dry-rails](/gems/dry-rails) which sets up application container for you and provides additional features on top of it. 45 | 46 | ### Credits 47 | 48 | * dry-system has been extracted from an experimental project called Rodakase created by [solnic](https://github.com/solnic). Later on Rodakase was renamed to [dry-web](https://github.com/dry-rb/dry-web). 49 | * System/Component and lifecycle triggers are inspired by Clojure's [component](https://github.com/stuartsierra/component) library by [Stuart Sierra](https://github.com/stuartsierra) 50 | 51 | -------------------------------------------------------------------------------- /docsite/source/providers.html.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Providers 3 | layout: gem-single 4 | name: dry-system 5 | --- 6 | 7 | Some components can be large, stateful, or requiring specific configuration as part of their setup (such as when dealing with third party code). You can use providers to manage and register these components across several distinct lifecycle steps. 8 | 9 | You can define your providers as individual source files in `system/providers/`, for example: 10 | 11 | ``` ruby 12 | # system/providers/persistence.rb 13 | 14 | Application.register_provider(:database) do 15 | prepare do 16 | require "third_party/db" 17 | end 18 | 19 | start do 20 | register(:database, ThirdParty::DB.new) 21 | end 22 | end 23 | ``` 24 | 25 | The provider’s lifecycle steps will not run until the provider is required by another component, is started directly, or when the container finalizes. 26 | 27 | This means you can require your container and ask it to start just that one provider: 28 | 29 | ``` ruby 30 | # system/application/container.rb 31 | class Application < Dry::System::Container 32 | configure do |config| 33 | config.root = Pathname("/my/app") 34 | end 35 | end 36 | 37 | Application.start(:database) 38 | 39 | # and now `database` becomes available 40 | Application["database"] 41 | ``` 42 | 43 | ### Provider lifecycle 44 | 45 | The provider lifecycle consists of three steps, each with a distinct purpose: 46 | 47 | * `prepare` - basic setup code, here you can require third party code and perform basic configuration 48 | * `start` - code that needs to run for a component to be usable at application's runtime 49 | * `stop` - code that needs to run to stop a component, ie close a database connection, clear some artifacts etc. 50 | 51 | Here's a simple example: 52 | 53 | ``` ruby 54 | # system/providers/db.rb 55 | 56 | Application.register_provider(:database) do 57 | prepare do 58 | require 'third_party/db' 59 | 60 | register(:database, ThirdParty::DB.configure(ENV['DB_URL'])) 61 | end 62 | 63 | start do 64 | container[:database].establish_connection 65 | end 66 | 67 | stop do 68 | container[:database].close_connection 69 | end 70 | end 71 | ``` 72 | 73 | ### Using other providers 74 | 75 | You can start one provider as a dependency of another by invoking the provider’s lifecycle directly on the `target` container (i.e. your application container): 76 | 77 | ``` ruby 78 | # system/providers/logger.rb 79 | Application.register_provider(:logger) do 80 | prepare do 81 | require "logger" 82 | end 83 | 84 | start do 85 | register(:logger, Logger.new($stdout)) 86 | end 87 | end 88 | 89 | # system/providers/db.rb 90 | Application.register_provider(:db) do 91 | start do 92 | target.start :logger 93 | 94 | register(DB.new(ENV['DB_URL'], logger: target[:logger])) 95 | end 96 | end 97 | ``` 98 | -------------------------------------------------------------------------------- /docsite/source/settings.html.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Settings 3 | layout: gem-single 4 | name: dry-system 5 | --- 6 | 7 | ## Basic usage 8 | 9 | dry-system provides a `:settings` provider source that you can use to load settings and share them throughout your application. To use this provider source, create your own `:settings` provider using the provider source from `:dry_system`, then declare your settings inside `settings` block (using [dry-configurable’s](/gems/dry-configurable) `setting` API): 10 | 11 | ```ruby 12 | # system/providers/settings.rb: 13 | 14 | require "dry/system" 15 | 16 | Application.register_provider(:settings, from: :dry_system) do 17 | before :prepare do 18 | # Change this to load your own `Types` module if you want type-checked settings 19 | require "your/types/module" 20 | end 21 | 22 | settings do 23 | setting :database_url, constructor: Types::String.constrained(filled: true) 24 | 25 | setting :logger_level, default: :info, constructor: Types::Symbol 26 | .constructor { |value| value.to_s.downcase.to_sym } 27 | .enum(:trace, :unknown, :error, :fatal, :warn, :info, :debug) 28 | end 29 | end 30 | ``` 31 | 32 | Your provider will then map `ENV` variables to a struct object giving access to your settings as their own methods, which you can use throughout your application: 33 | 34 | ```ruby 35 | Application[:settings].database_url # => "postgres://..." 36 | Application[:settings].logger_level # => :info 37 | ``` 38 | 39 | You can use this settings object in other providers: 40 | 41 | ```ruby 42 | Application.register_provider(:redis) do 43 | start do 44 | use :settings 45 | 46 | uri = URI.parse(target[:settings].redis_url) 47 | redis = Redis.new(host: uri.host, port: uri.port, password: uri.password) 48 | 49 | register('persistance.redis', redis) 50 | end 51 | end 52 | ``` 53 | 54 | Or as an injected dependency in your classes: 55 | 56 | ```ruby 57 | module Operations 58 | class CreateUser 59 | include Import[:settings] 60 | 61 | def call(params) 62 | settings # => your settings struct 63 | end 64 | end 65 | end 66 | end 67 | ``` 68 | -------------------------------------------------------------------------------- /docsite/source/test-mode.html.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Test Mode 3 | layout: gem-single 4 | name: dry-system 5 | --- 6 | 7 | In some cases it is useful to stub a component in your tests. To enable this, dry-system provides a test mode, 8 | in which a container will not be frozen during finalization. This allows you to use `stub` API to stub a given component. 9 | 10 | ``` ruby 11 | require 'dry/system' 12 | 13 | class Application < Dry::System::Container 14 | configure do |config| 15 | config.root = Pathname('./my/app') 16 | end 17 | end 18 | 19 | require 'dry/system/stubs' 20 | 21 | Application.enable_stubs! 22 | 23 | Application.stub('persistence.db', stubbed_db) 24 | ``` 25 | 26 | Typically, you want to use `enable_stubs!` in a test helper file, before booting your system. 27 | -------------------------------------------------------------------------------- /dry-system.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # this file is synced from dry-rb/template-gem project 4 | 5 | lib = File.expand_path("lib", __dir__) 6 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 7 | require "dry/system/version" 8 | 9 | Gem::Specification.new do |spec| 10 | spec.name = "dry-system" 11 | spec.authors = ["Piotr Solnica"] 12 | spec.email = ["piotr.solnica@gmail.com"] 13 | spec.license = "MIT" 14 | spec.version = Dry::System::VERSION.dup 15 | 16 | spec.summary = "Organize your code into reusable components" 17 | spec.description = spec.summary 18 | spec.homepage = "https://dry-rb.org/gems/dry-system" 19 | spec.files = Dir["CHANGELOG.md", "LICENSE", "README.md", "dry-system.gemspec", "lib/**/*"] 20 | spec.bindir = "bin" 21 | spec.executables = [] 22 | spec.require_paths = ["lib"] 23 | 24 | spec.metadata["allowed_push_host"] = "https://rubygems.org" 25 | spec.metadata["changelog_uri"] = "https://github.com/dry-rb/dry-system/blob/main/CHANGELOG.md" 26 | spec.metadata["source_code_uri"] = "https://github.com/dry-rb/dry-system" 27 | spec.metadata["bug_tracker_uri"] = "https://github.com/dry-rb/dry-system/issues" 28 | spec.metadata["rubygems_mfa_required"] = "true" 29 | 30 | spec.required_ruby_version = ">= 3.1" 31 | 32 | # to update dependencies edit project.yml 33 | spec.add_dependency "dry-auto_inject", "~> 1.1" 34 | spec.add_dependency "dry-configurable", "~> 1.3" 35 | spec.add_dependency "dry-core", "~> 1.1" 36 | spec.add_dependency "dry-inflector", "~> 1.1" 37 | end 38 | -------------------------------------------------------------------------------- /examples/custom_configuration_auto_register/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "dry-system", path: "../.." 6 | gem "sequel" 7 | gem "sqlite3" 8 | -------------------------------------------------------------------------------- /examples/custom_configuration_auto_register/lib/entities/user.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Entities 4 | class User 5 | include Import["persistence.db"] 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /examples/custom_configuration_auto_register/lib/user_repo.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UserRepo 4 | include Import["persistence.db"] 5 | end 6 | -------------------------------------------------------------------------------- /examples/custom_configuration_auto_register/run.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/setup" 4 | require_relative "system/container" 5 | require_relative "system/import" 6 | 7 | App.finalize! 8 | 9 | user_repo1 = App["user_repo"] 10 | user_repo2 = App["user_repo"] 11 | puts "User has not been loaded" unless App.key?("entities.user") 12 | puts user_repo1.db.inspect 13 | puts user_repo2.db.inspect 14 | puts "user_repo1 and user_repo2 reference the same instance" if user_repo1.equal?(user_repo2) 15 | -------------------------------------------------------------------------------- /examples/custom_configuration_auto_register/system/boot/persistence.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | App.boot(:persistence) do |persistence| 4 | init do 5 | require "sequel" 6 | end 7 | 8 | start do 9 | persistence.register("persistence.db", Sequel.connect("sqlite::memory")) 10 | end 11 | 12 | stop do 13 | db.close_connection 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /examples/custom_configuration_auto_register/system/container.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/system" 4 | 5 | class App < Dry::System::Container 6 | configure do |config| 7 | config.component_dirs.add "lib" do |dir| 8 | dir.memoize = true 9 | 10 | dir.auto_register = lambda do |component| 11 | !component.identifier.start_with?("entities") 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /examples/custom_configuration_auto_register/system/import.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "container" 4 | 5 | Import = App.injector 6 | -------------------------------------------------------------------------------- /examples/standalone/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "dry-events" 6 | gem "dry-monitor" 7 | gem "dry-system", path: "../.." 8 | gem "sequel" 9 | gem "sqlite3" 10 | -------------------------------------------------------------------------------- /examples/standalone/lib/empty_service.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class EmptyService 4 | end 5 | -------------------------------------------------------------------------------- /examples/standalone/lib/not_registered.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class NotRegistered 4 | end 5 | -------------------------------------------------------------------------------- /examples/standalone/lib/service_with_dependency.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ServiceWithDependency 4 | include Import["user_repo"] 5 | end 6 | -------------------------------------------------------------------------------- /examples/standalone/lib/user_repo.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UserRepo 4 | include Import["persistence.db"] 5 | end 6 | -------------------------------------------------------------------------------- /examples/standalone/run.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/setup" 4 | require_relative "system/container" 5 | require_relative "system/import" 6 | require "dry/events" 7 | require "dry/monitor/notifications" 8 | 9 | App[:notifications].subscribe(:resolved_dependency) do |event| 10 | puts "Event #{event.id}, payload: #{event.to_h}" 11 | end 12 | 13 | App[:notifications].subscribe(:registered_dependency) do |event| 14 | puts "Event #{event.id}, payload: #{event.to_h}" 15 | end 16 | 17 | App.finalize! 18 | p App.keys 19 | 20 | App["service_with_dependency"] 21 | user_repo = App["user_repo"] 22 | 23 | puts user_repo.db.inspect 24 | -------------------------------------------------------------------------------- /examples/standalone/system/container.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/events" 4 | require "dry/monitor/notifications" 5 | require "dry/system" 6 | 7 | class App < Dry::System::Container 8 | use :dependency_graph 9 | 10 | configure do |config| 11 | config.component_dirs.add "lib" do |dir| 12 | dir.add_to_load_path = true # defaults to true 13 | dir.auto_register = lambda do |component| 14 | !component.identifier.start_with?("not_registered") 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /examples/standalone/system/import.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "container" 4 | 5 | Import = App.injector 6 | -------------------------------------------------------------------------------- /examples/standalone/system/providers/persistence.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | App.boot(:persistence) do |persistence| 4 | init do 5 | require "sequel" 6 | end 7 | 8 | start do 9 | persistence.register("persistence.db", Sequel.connect("sqlite::memory")) 10 | end 11 | 12 | stop do 13 | db.close_connection 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /examples/zeitwerk/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "dry-events" 6 | gem "dry-monitor" 7 | gem "dry-system", path: "../.." 8 | gem "zeitwerk" 9 | -------------------------------------------------------------------------------- /examples/zeitwerk/lib/service_with_dependency.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ServiceWithDependency 4 | include Import["user_repo"] 5 | end 6 | -------------------------------------------------------------------------------- /examples/zeitwerk/lib/user_repo.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UserRepo 4 | end 5 | -------------------------------------------------------------------------------- /examples/zeitwerk/run.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/setup" 4 | require_relative "system/container" 5 | require_relative "system/import" 6 | 7 | App.finalize! 8 | 9 | service = App["service_with_dependency"] 10 | 11 | puts "Container keys: #{App.keys}" 12 | puts "User repo: #{service.user_repo.inspect}" 13 | puts "Loader: #{App.autoloader}" 14 | -------------------------------------------------------------------------------- /examples/zeitwerk/system/container.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/system" 4 | 5 | class App < Dry::System::Container 6 | use :env, inferrer: -> { ENV.fetch("RACK_ENV", :development).to_sym } 7 | use :zeitwerk, debug: true 8 | 9 | configure do |config| 10 | config.component_dirs.add "lib" 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /examples/zeitwerk/system/import.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "container" 4 | 5 | Import = App.injector 6 | -------------------------------------------------------------------------------- /lib/dry-system.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/system" 4 | -------------------------------------------------------------------------------- /lib/dry/system.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "zeitwerk" 4 | require "dry/core" 5 | 6 | module Dry 7 | module System 8 | # @api private 9 | def self.loader 10 | @loader ||= Zeitwerk::Loader.new.tap do |loader| 11 | root = File.expand_path("..", __dir__) 12 | loader.tag = "dry-system" 13 | loader.inflector = Zeitwerk::GemInflector.new("#{root}/dry-system.rb") 14 | loader.push_dir(root) 15 | loader.ignore( 16 | "#{root}/dry-system.rb", 17 | "#{root}/dry/system/{components,constants,errors,stubs,version}.rb" 18 | ) 19 | loader.inflector.inflect("source_dsl" => "SourceDSL") 20 | end 21 | end 22 | 23 | # Registers the provider sources in the files under the given path 24 | # 25 | # @api public 26 | def self.register_provider_sources(path) 27 | provider_sources.load_sources(path) 28 | end 29 | 30 | # Registers a provider source, which can be used as the basis for other providers 31 | # 32 | # @api public 33 | def self.register_provider_source(name, group:, source: nil, provider_options: {}, &) 34 | if source && block_given? 35 | raise ArgumentError, "You must supply only a `source:` option or a block, not both" 36 | end 37 | 38 | if source 39 | provider_sources.register( 40 | name: name, 41 | group: group, 42 | source: source, 43 | provider_options: provider_options 44 | ) 45 | else 46 | provider_sources.register_from_block( 47 | name: name, 48 | group: group, 49 | provider_options: provider_options, 50 | & 51 | ) 52 | end 53 | end 54 | 55 | # @api private 56 | def self.provider_sources 57 | @provider_sources ||= ProviderSourceRegistry.new 58 | end 59 | 60 | loader.setup 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/dry/system/auto_registrar.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/system/constants" 4 | 5 | module Dry 6 | module System 7 | # Default auto-registration implementation 8 | # 9 | # This is currently configured by default for every System::Container. 10 | # Auto-registrar objects are responsible for loading files from configured 11 | # auto-register paths and registering components automatically within the 12 | # container. 13 | # 14 | # @api private 15 | class AutoRegistrar 16 | attr_reader :container 17 | 18 | def initialize(container) 19 | @container = container 20 | end 21 | 22 | # @api private 23 | def finalize! 24 | container.component_dirs.each do |component_dir| 25 | call(component_dir) if component_dir.auto_register? 26 | end 27 | end 28 | 29 | # @api private 30 | def call(component_dir) 31 | component_dir.each_component do |component| 32 | next unless register_component?(component) 33 | 34 | container.register(component.key, memoize: component.memoize?) { component.instance } 35 | end 36 | end 37 | 38 | private 39 | 40 | def register_component?(component) 41 | !container.registered?(component.key) && component.auto_register? 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/dry/system/config/namespace.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/system/constants" 4 | 5 | module Dry 6 | module System 7 | module Config 8 | # A configured namespace for a component dir 9 | # 10 | # Namespaces consist of three elements: 11 | # 12 | # - The `path` within the component dir to which its namespace rules should apply. 13 | # - A `key`, which determines the leading part of the key used to register 14 | # each component in the container. 15 | # - A `const`, which is the Ruby namespace expected to contain the class constants 16 | # defined within each component's source file. This value is expected to be an 17 | # "underscored" string, intended to be run through the configured inflector to be 18 | # converted into a real constant (e.g. `"foo_bar/baz"` will become `FooBar::Baz`) 19 | # 20 | # Namespaces are added and configured for a component dir via {Namespaces#add}. 21 | # 22 | # @see Namespaces#add 23 | # 24 | # @api public 25 | class Namespace 26 | ROOT_PATH = nil 27 | 28 | include Dry::Equalizer(:path, :key, :const) 29 | 30 | # @api public 31 | attr_reader :path 32 | 33 | # @api public 34 | attr_reader :key 35 | 36 | # @api public 37 | attr_reader :const 38 | 39 | # Returns a namespace configured to serve as the default root namespace for a 40 | # component dir, ensuring that all code within the dir can be loaded, regardless 41 | # of any other explictly configured namespaces 42 | # 43 | # @return [Namespace] the root namespace 44 | # 45 | # @api private 46 | def self.default_root 47 | new( 48 | path: ROOT_PATH, 49 | key: nil, 50 | const: nil 51 | ) 52 | end 53 | 54 | # @api private 55 | def initialize(path:, key:, const:) 56 | @path = path 57 | # Default keys (i.e. when the user does not explicitly provide one) for non-root 58 | # paths will include path separators, which we must convert into key separators 59 | @key = key && key == path ? key.gsub(PATH_SEPARATOR, KEY_SEPARATOR) : key 60 | @const = const 61 | end 62 | 63 | # @api public 64 | def root? 65 | path == ROOT_PATH 66 | end 67 | 68 | # @api public 69 | def path? 70 | !root? 71 | end 72 | end 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/dry/system/constants.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dry 4 | module System 5 | include Dry::Core::Constants 6 | 7 | RB_EXT = ".rb" 8 | RB_GLOB = "*.rb" 9 | PATH_SEPARATOR = File::SEPARATOR 10 | KEY_SEPARATOR = "." 11 | WORD_REGEX = /\w+/ 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/dry/system/indirect_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dry 4 | module System 5 | # An indirect component is a component that cannot be directly from a source file 6 | # directly managed by the container. It may be component that needs to be loaded 7 | # indirectly, either via a registration manifest file or an imported container 8 | # 9 | # Indirect components are an internal abstraction and, unlike ordinary components, are 10 | # not exposed to users via component dir configuration hooks. 11 | # 12 | # @see Container#load_component 13 | # @see Container#find_component 14 | # 15 | # @api private 16 | class IndirectComponent 17 | include Dry::Equalizer(:identifier) 18 | 19 | # @!attribute [r] identifier 20 | # @return [String] the component's unique identifier 21 | attr_reader :identifier 22 | 23 | # @api private 24 | def initialize(identifier) 25 | @identifier = identifier 26 | end 27 | 28 | # Returns false, indicating that the component is not directly loadable from the 29 | # files managed by the container 30 | # 31 | # This is the inverse of {Component#loadable?} 32 | # 33 | # @return [FalseClass] 34 | # 35 | # @api private 36 | def loadable? 37 | false 38 | end 39 | 40 | # Returns the component's unique key 41 | # 42 | # @return [String] the key 43 | # 44 | # @see Identifier#key 45 | # 46 | # @api private 47 | def key 48 | identifier.to_s 49 | end 50 | 51 | # Returns the root namespace segment of the component's key, as a symbol 52 | # 53 | # @see Identifier#root_key 54 | # 55 | # @return [Symbol] the root key 56 | # 57 | # @api private 58 | def root_key 59 | identifier.root_key 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/dry/system/loader.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/system/errors" 4 | 5 | module Dry 6 | module System 7 | # Default component loader implementation 8 | # 9 | # This class is configured by default for every System::Container. You can 10 | # provide your own and use it in your containers too. 11 | # 12 | # @example 13 | # class MyLoader < Dry::System::Loader 14 | # def call(*args) 15 | # constant.build(*args) 16 | # end 17 | # end 18 | # 19 | # class MyApp < Dry::System::Container 20 | # configure do |config| 21 | # # ... 22 | # config.component_dirs.loader = MyLoader 23 | # end 24 | # end 25 | # 26 | # @api public 27 | class Loader 28 | class << self 29 | # Requires the component's source file 30 | # 31 | # @api public 32 | def require!(component) 33 | require(component.require_path) 34 | self 35 | end 36 | 37 | # Returns an instance of the component 38 | # 39 | # Provided optional args are passed to object's constructor 40 | # 41 | # @param [Array] args Optional constructor args 42 | # 43 | # @return [Object] 44 | # 45 | # @api public 46 | def call(component, *args, **kwargs) 47 | require!(component) 48 | 49 | constant = self.constant(component) 50 | 51 | if singleton?(constant) 52 | constant.instance(*args, **kwargs) 53 | else 54 | constant.new(*args, **kwargs) 55 | end 56 | end 57 | 58 | # Returns the component's class constant 59 | # 60 | # @return [Class] 61 | # 62 | # @api public 63 | def constant(component) 64 | inflector = component.inflector 65 | const_name = inflector.camelize(component.const_path) 66 | inflector.constantize(const_name) 67 | rescue NameError => e 68 | # Ensure it's this component's constant, not any other NameError within the component 69 | if e.message =~ /#{const_name}( |\n|$)/ 70 | raise ComponentNotLoadableError.new(component, e) 71 | else 72 | raise e 73 | end 74 | end 75 | 76 | private 77 | 78 | def singleton?(constant) 79 | constant.respond_to?(:instance) && !constant.respond_to?(:new) 80 | end 81 | end 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /lib/dry/system/loader/autoloading.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dry 4 | module System 5 | class Loader 6 | # Component loader for autoloading-enabled applications 7 | # 8 | # This behaves like the default loader, except instead of requiring the given path, 9 | # it loads the respective constant, allowing the autoloader to load the 10 | # corresponding file per its own configuration. 11 | # 12 | # @see Loader 13 | # @api public 14 | class Autoloading < Loader 15 | class << self 16 | def require!(component) 17 | constant(component) 18 | self 19 | end 20 | end 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/dry/system/magic_comments_parser.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dry 4 | module System 5 | class MagicCommentsParser 6 | VALID_LINE_RE = /^(#.*)?$/ 7 | COMMENT_RE = /^#\s+(?[A-Za-z]{1}[A-Za-z0-9_]+):\s+(?.+?)$/ 8 | 9 | COERCIONS = { 10 | "true" => true, 11 | "false" => false 12 | }.freeze 13 | 14 | def self.call(file_name) 15 | {}.tap do |options| 16 | File.foreach(file_name) do |line| 17 | break unless line =~ VALID_LINE_RE 18 | 19 | if (comment = line.match(COMMENT_RE)) 20 | options[comment[:name].to_sym] = coerce(comment[:value]) 21 | end 22 | end 23 | end 24 | end 25 | 26 | def self.coerce(value) 27 | COERCIONS.fetch(value) { value } 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/dry/system/manifest_registrar.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/system/constants" 4 | 5 | module Dry 6 | module System 7 | # Default manifest registration implementation 8 | # 9 | # This is configured by default for every System::Container. The manifest registrar is 10 | # responsible for loading manifest files that contain code to manually register 11 | # certain objects with the container. 12 | # 13 | # @api private 14 | class ManifestRegistrar 15 | # @api private 16 | attr_reader :container 17 | 18 | # @api private 19 | attr_reader :config 20 | 21 | # @api private 22 | def initialize(container) 23 | @container = container 24 | @config = container.config 25 | end 26 | 27 | # @api private 28 | def finalize! 29 | ::Dir[registrations_dir.join(RB_GLOB)].each do |file| 30 | call(Identifier.new(File.basename(file, RB_EXT))) 31 | end 32 | end 33 | 34 | # @api private 35 | def call(component) 36 | require(root.join(config.registrations_dir, component.root_key.to_s)) 37 | end 38 | 39 | # @api private 40 | def file_exists?(component) 41 | ::File.exist?(::File.join(registrations_dir, "#{component.root_key}#{RB_EXT}")) 42 | end 43 | 44 | private 45 | 46 | # @api private 47 | def registrations_dir 48 | root.join(config.registrations_dir) 49 | end 50 | 51 | # @api private 52 | def root 53 | container.root 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/dry/system/plugins.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dry 4 | module System 5 | module Plugins 6 | # Register a plugin 7 | # 8 | # @param [Symbol] name The name of a plugin 9 | # @param [Class] plugin Plugin module 10 | # 11 | # @return [Plugins] 12 | # 13 | # @api public 14 | def self.register(name, plugin, &) 15 | registry[name] = Plugin.new(name, plugin, &) 16 | end 17 | 18 | # @api private 19 | def self.registry 20 | @registry ||= {} 21 | end 22 | 23 | # @api private 24 | def self.loaded_dependencies 25 | @loaded_dependencies ||= [] 26 | end 27 | 28 | # Enables a plugin if not already enabled. 29 | # Raises error if plugin cannot be found in the plugin registry. 30 | # 31 | # @param [Symbol] name The plugin name 32 | # @param [Hash] options Plugin options 33 | # 34 | # @return [self] 35 | # 36 | # @api public 37 | def use(name, **options) 38 | return self if enabled_plugins.include?(name) 39 | 40 | raise PluginNotFoundError, name unless (plugin = Dry::System::Plugins.registry[name]) 41 | 42 | plugin.load_dependencies 43 | plugin.apply_to(self, **options) 44 | 45 | enabled_plugins << name 46 | 47 | self 48 | end 49 | 50 | # @api private 51 | def inherited(klass) 52 | klass.instance_variable_set(:@enabled_plugins, enabled_plugins.dup) 53 | super 54 | end 55 | 56 | # @api private 57 | def enabled_plugins 58 | @enabled_plugins ||= [] 59 | end 60 | 61 | register(:bootsnap, Plugins::Bootsnap) 62 | register(:logging, Plugins::Logging) 63 | register(:env, Plugins::Env) 64 | register(:notifications, Plugins::Notifications) 65 | register(:monitoring, Plugins::Monitoring) 66 | register(:dependency_graph, Plugins::DependencyGraph) 67 | register(:zeitwerk, Plugins::Zeitwerk) 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /lib/dry/system/plugins/bootsnap.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dry 4 | module System 5 | module Plugins 6 | module Bootsnap 7 | DEFAULT_OPTIONS = { 8 | load_path_cache: true, 9 | compile_cache_iseq: true, 10 | compile_cache_yaml: true 11 | }.freeze 12 | 13 | # @api private 14 | def self.extended(system) 15 | super 16 | 17 | system.use(:env) 18 | system.setting :bootsnap, default: DEFAULT_OPTIONS 19 | system.after(:configure, &:setup_bootsnap) 20 | end 21 | 22 | # @api private 23 | def self.dependencies 24 | {bootsnap: "bootsnap"} 25 | end 26 | 27 | # Set up bootsnap for faster booting 28 | # 29 | # @api public 30 | def setup_bootsnap 31 | return unless bootsnap_available? 32 | 33 | ::Bootsnap.setup(**config.bootsnap, cache_dir: root.join("tmp/cache").to_s) 34 | end 35 | 36 | # @api private 37 | def bootsnap_available? 38 | spec = Gem.loaded_specs["bootsnap"] or return false 39 | 40 | RUBY_ENGINE == "ruby" && 41 | spec.match_platform(RUBY_PLATFORM) && 42 | spec.required_ruby_version.satisfied_by?(Gem::Version.new(RUBY_VERSION)) 43 | end 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/dry/system/plugins/dependency_graph.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dry 4 | module System 5 | module Plugins 6 | # @api public 7 | module DependencyGraph 8 | # @api private 9 | def self.extended(system) 10 | super 11 | 12 | system.instance_eval do 13 | use(:notifications) 14 | 15 | setting :dependency_graph do 16 | setting :ignored_dependencies, default: [] 17 | end 18 | 19 | after(:configure) do 20 | self[:notifications].register_event(:resolved_dependency) 21 | self[:notifications].register_event(:registered_dependency) 22 | end 23 | end 24 | end 25 | 26 | # @api private 27 | def self.dependencies 28 | {"dry-events" => "dry/events/publisher"} 29 | end 30 | 31 | # @api private 32 | def injector(**options) 33 | super(**options, strategies: DependencyGraph::Strategies) 34 | end 35 | 36 | # @api private 37 | def register(key, contents = nil, options = {}, &) 38 | super.tap do 39 | key = key.to_s 40 | 41 | unless config.dependency_graph.ignored_dependencies.include?(key) 42 | self[:notifications].instrument( 43 | :registered_dependency, 44 | key: key, 45 | class: self[key].class 46 | ) 47 | end 48 | end 49 | end 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/dry/system/plugins/dependency_graph/strategies.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dry 4 | module System 5 | module Plugins 6 | module DependencyGraph 7 | # @api private 8 | class Strategies 9 | extend Core::Container::Mixin 10 | 11 | # @api private 12 | class Kwargs < Dry::AutoInject::Strategies::Kwargs 13 | private 14 | 15 | # @api private 16 | def define_initialize(klass) 17 | @container["notifications"].instrument( 18 | :resolved_dependency, 19 | dependency_map: dependency_map.to_h, 20 | target_class: klass 21 | ) 22 | 23 | super 24 | end 25 | end 26 | 27 | # @api private 28 | class Args < Dry::AutoInject::Strategies::Args 29 | private 30 | 31 | # @api private 32 | def define_initialize(klass) 33 | @container["notifications"].instrument( 34 | :resolved_dependency, 35 | dependency_map: dependency_map.to_h, 36 | target_class: klass 37 | ) 38 | 39 | super 40 | end 41 | end 42 | 43 | class Hash < Dry::AutoInject::Strategies::Hash 44 | private 45 | 46 | # @api private 47 | def define_initialize(klass) 48 | @container["notifications"].instrument( 49 | :resolved_dependency, 50 | dependency_map: dependency_map.to_h, 51 | target_class: klass 52 | ) 53 | 54 | super 55 | end 56 | end 57 | 58 | register :kwargs, Kwargs 59 | register :args, Args 60 | register :hash, Hash 61 | register :default, Kwargs 62 | end 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/dry/system/plugins/env.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dry 4 | module System 5 | module Plugins 6 | # @api public 7 | class Env < Module 8 | DEFAULT_INFERRER = -> { :development } 9 | 10 | attr_reader :options 11 | 12 | # @api private 13 | def initialize(**options) 14 | @options = options 15 | super() 16 | end 17 | 18 | def inferrer 19 | options.fetch(:inferrer, DEFAULT_INFERRER) 20 | end 21 | 22 | # @api private 23 | def extended(system) 24 | system.setting :env, default: inferrer.(), reader: true 25 | super 26 | end 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/dry/system/plugins/logging.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "logger" 4 | 5 | module Dry 6 | module System 7 | module Plugins 8 | module Logging 9 | # @api private 10 | def self.extended(system) 11 | system.instance_eval do 12 | setting :logger, reader: true 13 | 14 | setting :log_dir, default: "log" 15 | 16 | setting :log_levels, default: { 17 | development: Logger::DEBUG, 18 | test: Logger::DEBUG, 19 | production: Logger::ERROR 20 | } 21 | 22 | setting :logger_class, default: ::Logger, reader: true 23 | end 24 | 25 | system.after(:configure, &:register_logger) 26 | 27 | super 28 | end 29 | 30 | # Set a logger 31 | # 32 | # This is invoked automatically when a container is being configured 33 | # 34 | # @return [self] 35 | # 36 | # @api private 37 | def register_logger 38 | if registered?(:logger) 39 | self 40 | elsif config.logger 41 | register(:logger, config.logger) 42 | else 43 | config.logger = config.logger_class.new(log_file_path) 44 | config.logger.level = log_level 45 | 46 | register(:logger, config.logger) 47 | self 48 | end 49 | end 50 | 51 | # @api private 52 | def log_level 53 | config.log_levels.fetch(config.env, Logger::ERROR) 54 | end 55 | 56 | # @api private 57 | def log_dir_path 58 | root.join(config.log_dir).realpath 59 | end 60 | 61 | # @api private 62 | def log_file_path 63 | log_dir_path.join(log_file_name) 64 | end 65 | 66 | # @api private 67 | def log_file_name 68 | "#{config.env}.log" 69 | end 70 | end 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /lib/dry/system/plugins/monitoring.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/system/constants" 4 | 5 | module Dry 6 | module System 7 | module Plugins 8 | # @api public 9 | module Monitoring 10 | # @api private 11 | def self.extended(system) 12 | super 13 | 14 | system.use(:notifications) 15 | 16 | system.after(:configure) do 17 | self[:notifications].register_event(:monitoring) 18 | end 19 | end 20 | 21 | # @api private 22 | def self.dependencies 23 | {"dry-events": "dry/events/publisher"} 24 | end 25 | 26 | # @api private 27 | def monitor(key, **options, &block) 28 | notifications = self[:notifications] 29 | 30 | resolve(key).tap do |target| 31 | proxy = Proxy.for(target, **options, key: key) 32 | 33 | if block_given? 34 | proxy.monitored_methods.each do |meth| 35 | notifications.subscribe(:monitoring, target: key, method: meth, &block) 36 | end 37 | end 38 | 39 | decorate(key, with: -> tgt { proxy.new(tgt, notifications) }) 40 | end 41 | end 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/dry/system/plugins/monitoring/proxy.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "delegate" 4 | 5 | module Dry 6 | module System 7 | module Plugins 8 | module Monitoring 9 | # @api private 10 | class Proxy < SimpleDelegator 11 | # @api private 12 | def self.for(target, key:, methods: []) 13 | monitored_methods = 14 | if methods.empty? 15 | target.public_methods - Object.public_instance_methods 16 | else 17 | methods 18 | end 19 | 20 | Class.new(self) do 21 | extend Dry::Core::ClassAttributes 22 | include Dry::Events::Publisher[target.class.name] 23 | 24 | defines :monitored_methods 25 | 26 | attr_reader :__notifications__ 27 | 28 | monitored_methods(monitored_methods) 29 | 30 | monitored_methods.each do |meth| 31 | define_method(meth) do |*args, &block| 32 | object = __getobj__ 33 | opts = {target: key, object: object, method: meth, args: args} 34 | 35 | __notifications__.instrument(:monitoring, opts) do 36 | object.public_send(meth, *args, &block) 37 | end 38 | end 39 | end 40 | end 41 | end 42 | 43 | def initialize(target, notifications) 44 | super(target) 45 | @__notifications__ = notifications 46 | end 47 | end 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/dry/system/plugins/notifications.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dry 4 | module System 5 | module Plugins 6 | # @api public 7 | module Notifications 8 | # @api private 9 | def self.extended(system) 10 | system.after(:configure, &:register_notifications) 11 | end 12 | 13 | # @api private 14 | def self.dependencies 15 | {"dry-monitor": "dry/monitor"} 16 | end 17 | 18 | # @api private 19 | def register_notifications 20 | return self if registered?(:notifications) 21 | 22 | register(:notifications, Monitor::Notifications.new(config.name)) 23 | end 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/dry/system/plugins/plugin.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dry 4 | module System 5 | module Plugins 6 | # @api private 7 | class Plugin 8 | attr_reader :name 9 | 10 | attr_reader :mod 11 | 12 | attr_reader :block 13 | 14 | # @api private 15 | def initialize(name, mod, &block) 16 | @name = name 17 | @mod = mod 18 | @block = block 19 | end 20 | 21 | # @api private 22 | def apply_to(system, **options) 23 | system.extend(stateful? ? mod.new(**options) : mod) 24 | system.instance_eval(&block) if block 25 | system 26 | end 27 | 28 | # @api private 29 | def load_dependencies(dependencies = mod_dependencies, gem = nil) 30 | Array(dependencies).each do |dependency| 31 | if dependency.is_a?(Array) || dependency.is_a?(Hash) 32 | dependency.each { |value| load_dependencies(*Array(value).reverse) } 33 | elsif !Plugins.loaded_dependencies.include?(dependency.to_s) 34 | load_dependency(dependency, gem) 35 | end 36 | end 37 | end 38 | 39 | # @api private 40 | def load_dependency(dependency, gem) 41 | Kernel.require dependency 42 | Plugins.loaded_dependencies << dependency.to_s 43 | rescue LoadError => e 44 | raise PluginDependencyMissing.new(name, e.message, gem) 45 | end 46 | 47 | # @api private 48 | def stateful? 49 | mod < Module 50 | end 51 | 52 | # @api private 53 | def mod_dependencies 54 | return EMPTY_ARRAY unless mod.respond_to?(:dependencies) 55 | 56 | mod.dependencies.is_a?(Array) ? mod.dependencies : [mod.dependencies] 57 | end 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/dry/system/plugins/zeitwerk.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/system/constants" 4 | 5 | module Dry 6 | module System 7 | module Plugins 8 | # @api private 9 | class Zeitwerk < Module 10 | # @api private 11 | def self.dependencies 12 | [ 13 | "dry/system/loader/autoloading", 14 | "dry/system/plugins/zeitwerk/compat_inflector", 15 | {"zeitwerk" => "zeitwerk"} 16 | ] 17 | end 18 | 19 | # @api private 20 | attr_reader :loader, :run_setup, :eager_load, :debug 21 | 22 | # @api private 23 | def initialize(loader: nil, run_setup: true, eager_load: nil, debug: false) 24 | @loader = loader || ::Zeitwerk::Loader.new 25 | @run_setup = run_setup 26 | @eager_load = eager_load 27 | @debug = debug 28 | super() 29 | end 30 | 31 | # @api private 32 | def extended(system) 33 | system.setting :autoloader, reader: true 34 | 35 | system.config.autoloader = loader 36 | system.config.component_dirs.loader = Dry::System::Loader::Autoloading 37 | system.config.component_dirs.add_to_load_path = false 38 | 39 | system.after(:configure, &method(:setup_autoloader)) 40 | 41 | super 42 | end 43 | 44 | private 45 | 46 | def setup_autoloader(system) 47 | configure_loader(system.autoloader, system) 48 | 49 | push_component_dirs_to_loader(system, system.autoloader) 50 | 51 | system.autoloader.setup if run_setup 52 | 53 | system.after(:finalize) { system.autoloader.eager_load } if eager_load?(system) 54 | 55 | system 56 | end 57 | 58 | # Build a zeitwerk loader with the configured component directories 59 | # 60 | # @return [Zeitwerk::Loader] 61 | def configure_loader(loader, system) 62 | loader.tag = system.config.name || system.name unless loader.tag 63 | loader.inflector = CompatInflector.new(system.config) 64 | loader.logger = method(:puts) if debug 65 | end 66 | 67 | # Add component dirs to the zeitwerk loader 68 | # 69 | # @return [Zeitwerk::Loader] 70 | def push_component_dirs_to_loader(system, loader) 71 | system.config.component_dirs.each do |dir| 72 | dir.namespaces.each do |ns| 73 | loader.push_dir( 74 | system.root.join(dir.path, ns.path.to_s), 75 | namespace: module_for_namespace(ns, system.config.inflector) 76 | ) 77 | end 78 | end 79 | 80 | loader 81 | end 82 | 83 | def module_for_namespace(namespace, inflector) 84 | return Object unless namespace.const 85 | 86 | begin 87 | inflector.constantize(inflector.camelize(namespace.const)) 88 | rescue NameError 89 | namespace.const.split(PATH_SEPARATOR).reduce(Object) { |parent_mod, mod_path| 90 | get_or_define_module(parent_mod, inflector.camelize(mod_path)) 91 | } 92 | end 93 | end 94 | 95 | def get_or_define_module(parent_mod, name) 96 | parent_mod.const_get(name) 97 | rescue NameError 98 | parent_mod.const_set(name, Module.new) 99 | end 100 | 101 | def eager_load?(system) 102 | return eager_load unless eager_load.nil? 103 | 104 | system.config.respond_to?(:env) && system.config.env == :production 105 | end 106 | end 107 | end 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /lib/dry/system/plugins/zeitwerk/compat_inflector.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dry 4 | module System 5 | module Plugins 6 | class Zeitwerk < Module 7 | # @api private 8 | class CompatInflector 9 | attr_reader :config 10 | 11 | def initialize(config) 12 | @config = config 13 | end 14 | 15 | def camelize(string, _) 16 | config.inflector.camelize(string) 17 | end 18 | end 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/dry/system/provider/source_dsl.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dry 4 | module System 5 | class Provider 6 | # Configures a Dry::System::Provider::Source subclass using a DSL that makes it 7 | # nicer to define source behaviour via a single block. 8 | # 9 | # @see Dry::System::Container.register_provider 10 | # 11 | # @api private 12 | class SourceDSL 13 | def self.evaluate(source_class, &) 14 | new(source_class).instance_eval(&) 15 | end 16 | 17 | attr_reader :source_class 18 | 19 | def initialize(source_class) 20 | @source_class = source_class 21 | end 22 | 23 | def setting(...) 24 | source_class.setting(...) 25 | end 26 | 27 | def prepare(&) 28 | source_class.define_method(:prepare, &) 29 | end 30 | 31 | def start(&) 32 | source_class.define_method(:start, &) 33 | end 34 | 35 | def stop(&) 36 | source_class.define_method(:stop, &) 37 | end 38 | 39 | private 40 | 41 | def method_missing(name, ...) 42 | if source_class.respond_to?(name) 43 | source_class.public_send(name, ...) 44 | else 45 | super 46 | end 47 | end 48 | 49 | def respond_to_missing?(name, include_all = false) 50 | source_class.respond_to?(name, include_all) || super 51 | end 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/dry/system/provider_source_registry.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/system/constants" 4 | 5 | module Dry 6 | module System 7 | # @api private 8 | class ProviderSourceRegistry 9 | # @api private 10 | class Registration 11 | attr_reader :source 12 | attr_reader :provider_options 13 | 14 | def initialize(source:, provider_options:) 15 | @source = source 16 | @provider_options = provider_options 17 | end 18 | end 19 | 20 | attr_reader :sources 21 | 22 | def initialize 23 | @sources = {} 24 | end 25 | 26 | def load_sources(path) 27 | ::Dir[::File.join(path, "**/#{RB_GLOB}")].each do |file| 28 | require file 29 | end 30 | end 31 | 32 | def register(name:, group:, source:, provider_options:) 33 | sources[key(name, group)] = Registration.new( 34 | source: source, 35 | provider_options: provider_options 36 | ) 37 | end 38 | 39 | def register_from_block(name:, group:, provider_options:, &) 40 | register( 41 | name: name, 42 | group: group, 43 | source: Provider::Source.for(name: name, group: group, &), 44 | provider_options: provider_options 45 | ) 46 | end 47 | 48 | def resolve(name:, group:) 49 | sources[key(name, group)].tap { |source| 50 | unless source 51 | raise ProviderSourceNotFoundError.new( 52 | name: name, 53 | group: group, 54 | keys: sources.keys 55 | ) 56 | end 57 | } 58 | end 59 | 60 | private 61 | 62 | def key(name, group) 63 | {group: group, name: name} 64 | end 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/dry/system/provider_sources.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "pathname" 4 | require "dry/system" 5 | 6 | Dry::System.register_provider_sources Pathname(__dir__).join("provider_sources").realpath 7 | -------------------------------------------------------------------------------- /lib/dry/system/provider_sources/settings.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dry 4 | module System 5 | module ProviderSources 6 | module Settings 7 | class Source < Dry::System::Provider::Source 8 | setting :store 9 | 10 | def prepare 11 | require "dry/system/provider_sources/settings/config" 12 | end 13 | 14 | def start 15 | register(:settings, settings.load(root: target.root, env: target.config.env)) 16 | end 17 | 18 | def settings(&block) 19 | # Save the block and evaluate it lazily to allow a provider with this source 20 | # to `require` any necessary files for the block to evaluate correctly (e.g. 21 | # requiring an app-specific types module for setting constructors) 22 | if block 23 | @settings_block = block 24 | elsif defined? @settings_class 25 | @settings_class 26 | elsif @settings_block 27 | @settings_class = Class.new(Settings::Config, &@settings_block) 28 | end 29 | end 30 | end 31 | end 32 | end 33 | end 34 | end 35 | 36 | Dry::System.register_provider_source( 37 | :settings, 38 | group: :dry_system, 39 | source: Dry::System::ProviderSources::Settings::Source 40 | ) 41 | -------------------------------------------------------------------------------- /lib/dry/system/provider_sources/settings/config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dry 4 | module System 5 | module ProviderSources 6 | # @api private 7 | module Settings 8 | InvalidSettingsError = Class.new(ArgumentError) do 9 | # @api private 10 | def initialize(errors) 11 | message = <<~STR 12 | Could not load settings. The following settings were invalid: 13 | 14 | #{setting_errors(errors).join("\n")} 15 | STR 16 | 17 | super(message) 18 | end 19 | 20 | private 21 | 22 | def setting_errors(errors) 23 | errors.sort_by { |k, _| k }.map { |key, error| "#{key}: #{error}" } 24 | end 25 | end 26 | 27 | # @api private 28 | class Config 29 | # @api private 30 | def self.load(root:, env:, loader: Loader) 31 | loader = loader.new(root: root, env: env) 32 | 33 | new.tap do |settings_obj| 34 | errors = {} 35 | 36 | settings.to_a.each do |setting| 37 | value = loader[setting.name.to_s.upcase] 38 | 39 | begin 40 | if value 41 | settings_obj.config.public_send(:"#{setting.name}=", value) 42 | else 43 | settings_obj.config[setting.name] 44 | end 45 | rescue => e # rubocop:disable Style/RescueStandardError 46 | errors[setting.name] = e 47 | end 48 | end 49 | 50 | raise InvalidSettingsError, errors unless errors.empty? 51 | end 52 | end 53 | 54 | include Dry::Configurable 55 | 56 | private 57 | 58 | def method_missing(name, ...) 59 | if config.respond_to?(name) 60 | config.public_send(name, ...) 61 | else 62 | super 63 | end 64 | end 65 | 66 | def respond_to_missing?(name, include_all = false) 67 | config.respond_to?(name, include_all) || super 68 | end 69 | end 70 | end 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /lib/dry/system/provider_sources/settings/loader.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dry 4 | module System 5 | module ProviderSources 6 | module Settings 7 | # @api private 8 | class Loader 9 | # @api private 10 | attr_reader :store 11 | 12 | # @api private 13 | def initialize(root:, env:, store: ENV) 14 | @store = store 15 | load_dotenv(root, env.to_sym) 16 | end 17 | 18 | # @api private 19 | def [](key) 20 | store[key] 21 | end 22 | 23 | private 24 | 25 | def load_dotenv(root, env) 26 | require "dotenv" 27 | Dotenv.load(*dotenv_files(root, env)) if defined?(Dotenv) 28 | rescue LoadError 29 | # Do nothing if dotenv is unavailable 30 | end 31 | 32 | def dotenv_files(root, env) 33 | [ 34 | File.join(root, ".env.#{env}.local"), 35 | (File.join(root, ".env.local") unless env == :test), 36 | File.join(root, ".env.#{env}"), 37 | File.join(root, ".env") 38 | ].compact 39 | end 40 | end 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/dry/system/stubs.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/core/container/stub" 4 | 5 | module Dry 6 | module System 7 | class Container 8 | # @api private 9 | module Stubs 10 | # This overrides default finalize! just to disable automatic freezing 11 | # of the container 12 | # 13 | # @api private 14 | def finalize!(**, &) 15 | super(freeze: false, &) 16 | end 17 | end 18 | 19 | # Enables stubbing container's components 20 | # 21 | # @example 22 | # require 'dry/system/stubs' 23 | # 24 | # MyContainer.enable_stubs! 25 | # MyContainer.finalize! 26 | # 27 | # MyContainer.stub('some.component', some_stub_object) 28 | # 29 | # @return Container 30 | # 31 | # @api public 32 | def self.enable_stubs! 33 | super 34 | extend ::Dry::System::Container::Stubs 35 | self 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/dry/system/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dry 4 | module System 5 | VERSION = "1.2.2" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /project.yml: -------------------------------------------------------------------------------- 1 | name: dry-system 2 | codacy_id: 3a0e30d0ae2542c7ba047ba5f923c0bb 3 | gemspec: 4 | authors: ["Piotr Solnica"] 5 | email: ["piotr.solnica@gmail.com"] 6 | summary: "Organize your code into reusable components" 7 | runtime_dependencies: 8 | - [dry-auto_inject, "~> 1.1"] 9 | - [dry-core, "~> 1.1"] 10 | - [dry-configurable, "~> 1.3"] 11 | - [dry-inflector, "~> 1.1"] 12 | -------------------------------------------------------------------------------- /spec/fixtures/app/lib/ignored_spec_service.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class IgnoredSpecService 4 | end 5 | -------------------------------------------------------------------------------- /spec/fixtures/app/lib/spec_service.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class SpecService 4 | end 5 | -------------------------------------------------------------------------------- /spec/fixtures/app/system/providers/client.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Test::Container.register_provider(:client) do 4 | module Test 5 | class Client 6 | end 7 | end 8 | 9 | start do 10 | register(:client, Test::Client.new) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/fixtures/autoloading/lib/test/entities/foo_entity.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Test 4 | module Entities 5 | class FooEntity 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/fixtures/autoloading/lib/test/foo.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Test 4 | class Foo 5 | def call 6 | Entities::FooEntity.new 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/fixtures/components/test/bar.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Test 4 | class Bar 5 | def self.call 6 | "Welcome to my Moe's Tavern!" 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/fixtures/components/test/bar/abc.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Test 4 | class Bar 5 | class ABC 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/fixtures/components/test/bar/baz.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Test 4 | class Bar 5 | class Baz 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/fixtures/components/test/foo.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Test 4 | class Foo 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/fixtures/components/test/no_register.rb: -------------------------------------------------------------------------------- 1 | # auto_register: false 2 | # frozen_string_literal: true 3 | 4 | module Test 5 | class NoRegister 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/fixtures/components_with_errors/test/constant_error.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ConstantError 4 | const_get(:NotHere) 5 | end 6 | -------------------------------------------------------------------------------- /spec/fixtures/deprecations/bootable_dirs_config/system/boot/logger.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Test::Container.register_provider :logger do 4 | start do 5 | register "logger", "my logger" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/fixtures/deprecations/bootable_dirs_config/system/custom_boot/logger.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Test::Container.register_provider :logger do 4 | start do 5 | register "logger", "my logger" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/fixtures/external_components/alt-components/db.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/system" 4 | 5 | Dry::System.register_provider_source(:db, group: :alt) do 6 | prepare do 7 | module AltComponents 8 | class DbConn 9 | end 10 | end 11 | end 12 | 13 | start do 14 | register(:db_conn, AltComponents::DbConn.new) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/fixtures/external_components/alt-components/logger.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/system" 4 | 5 | Dry::System.register_provider_source(:logger, group: :alt) do 6 | prepare do 7 | module AltComponents 8 | class Logger 9 | end 10 | end 11 | end 12 | 13 | start do 14 | register(:logger, AltComponents::Logger.new) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/fixtures/external_components/components/logger.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/system" 4 | 5 | Dry::System.register_provider_source(:logger, group: :external_components) do 6 | setting :log_level, default: :scream, constructor: Types::Symbol 7 | 8 | prepare do 9 | unless defined?(ExternalComponents) 10 | module ExternalComponents 11 | class Logger 12 | class << self 13 | attr_accessor :default_level 14 | end 15 | 16 | self.default_level = :scream 17 | 18 | attr_reader :log_level 19 | 20 | def initialize(log_level = Logger.default_level) 21 | @log_level = log_level 22 | end 23 | end 24 | end 25 | end 26 | end 27 | 28 | start do 29 | logger = 30 | if config.log_level 31 | ExternalComponents::Logger.new(config.log_level) 32 | else 33 | ExternalComponents::Logger.new 34 | end 35 | 36 | register(:logger, logger) 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/fixtures/external_components/components/mailer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/system" 4 | 5 | Dry::System.register_provider_source(:mailer, group: :external_components) do 6 | prepare do 7 | module ExternalComponents 8 | class Mailer 9 | attr_reader :client 10 | 11 | def initialize(client) 12 | @client = client 13 | end 14 | end 15 | end 16 | end 17 | 18 | start do 19 | target.start :client 20 | 21 | register(:mailer, ExternalComponents::Mailer.new(target_container["client"])) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/fixtures/external_components/components/notifier.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/system" 4 | 5 | Dry::System.register_provider_source(:notifier, group: :external_components) do 6 | prepare do 7 | module ExternalComponents 8 | class Notifier 9 | attr_reader :monitor 10 | 11 | def initialize(monitor) 12 | @monitor = monitor 13 | end 14 | end 15 | end 16 | end 17 | 18 | start do 19 | register(:notifier, ExternalComponents::Notifier.new(target_container["monitor"])) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/fixtures/external_components/lib/external_components.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/system" 4 | 5 | Dry::System.register_provider_sources Pathname(__dir__).join("../components").realpath 6 | Dry::System.register_provider_sources Pathname(__dir__).join("../alt-components").realpath 7 | -------------------------------------------------------------------------------- /spec/fixtures/external_components_deprecated/components/logger.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/system" 4 | 5 | Dry::System.register_component(:logger, provider: :external_components) do 6 | setting :log_level, default: :scream, constructor: Types::Symbol 7 | 8 | prepare do 9 | unless defined?(ExternalComponents) 10 | module ExternalComponents 11 | class Logger 12 | class << self 13 | attr_accessor :default_level 14 | end 15 | 16 | self.default_level = :scream 17 | 18 | attr_reader :log_level 19 | 20 | def initialize(log_level = Logger.default_level) 21 | @log_level = log_level 22 | end 23 | end 24 | end 25 | end 26 | end 27 | 28 | start do 29 | logger = 30 | if config 31 | ExternalComponents::Logger.new(config.log_level) 32 | else 33 | ExternalComponents::Logger.new 34 | end 35 | 36 | register(:logger, logger) 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/fixtures/external_components_deprecated/lib/external_components.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/system" 4 | 5 | Dry::System.register_provider( 6 | :external_components, 7 | path: Pathname(__dir__).join("../components").realpath 8 | ) 9 | -------------------------------------------------------------------------------- /spec/fixtures/import_test/config/application.yml: -------------------------------------------------------------------------------- 1 | test: 2 | foo: 'bar' 3 | -------------------------------------------------------------------------------- /spec/fixtures/import_test/lib/test/bar.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Test 4 | class Bar 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/fixtures/import_test/lib/test/foo.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Test 4 | class Foo 5 | include Import["test.bar"] 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/fixtures/lazy_loading/auto_registration_disabled/lib/entities/kitten.rb: -------------------------------------------------------------------------------- 1 | # auto_register: false 2 | # frozen_string_literal: true 3 | 4 | module Entities 5 | class Kitten 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/fixtures/lazy_loading/auto_registration_disabled/lib/fetch_kitten.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class FetchKitten 4 | end 5 | -------------------------------------------------------------------------------- /spec/fixtures/lazy_loading/shared_root_keys/lib/kitten_service/fetch_kitten.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module KittenService 4 | class FetchKitten 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/fixtures/lazy_loading/shared_root_keys/lib/kitten_service/submit_kitten.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module KittenService 4 | class SubmitKitten 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/fixtures/lazy_loading/shared_root_keys/system/providers/kitten_service.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Test::Container.register_provider(:kitten_service, namespace: true) do 4 | prepare do 5 | module KittenService 6 | class Client 7 | end 8 | end 9 | end 10 | 11 | start do 12 | register "client", KittenService::Client.new 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/fixtures/lazytest/config/application.yml: -------------------------------------------------------------------------------- 1 | test: 2 | foo: 'bar' 3 | -------------------------------------------------------------------------------- /spec/fixtures/lazytest/lib/test/dep.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Test 4 | class Dep 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/fixtures/lazytest/lib/test/foo.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Test 4 | class Foo 5 | include Import["test.dep"] 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/fixtures/lazytest/lib/test/models.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Test 4 | module Models 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/fixtures/lazytest/lib/test/models/book.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Test 4 | module Models 5 | class Book 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/fixtures/lazytest/lib/test/models/user.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Test 4 | module Models 5 | class User 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/fixtures/lazytest/system/providers/bar.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Test::Container.namespace(:test) do |container| 4 | container.register_provider(:bar) do 5 | prepare do 6 | module Test 7 | module Bar 8 | # I shall be booted 9 | end 10 | end 11 | end 12 | 13 | start do 14 | container.register(:bar, "I was finalized") 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/fixtures/magic_comments/comments.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # rubocop:disable Layout/CommentIndentation 3 | # frozen_string_literal: true 4 | 5 | # This is a file with a mix of valid and invalid magic comments 6 | 7 | # valid_comment: hello 8 | # true_comment: true 9 | # false_comment: false 10 | # comment_123: alpha-numeric and underscores allowed 11 | # 123_will_not_match: will not match 12 | # not-using-underscores: value for comment using dashes 13 | 14 | # not_at_start_of_line: will not match 15 | 16 | module Test 17 | end 18 | 19 | # after_code: will not match 20 | # rubocop:enable Layout/CommentIndentation 21 | -------------------------------------------------------------------------------- /spec/fixtures/manifest_registration/lib/test/foo.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Test 4 | class Foo 5 | attr_reader :name 6 | 7 | def initialize(name) 8 | @name = name 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/fixtures/manifest_registration/system/registrations/foo.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Test::Container.namespace(:foo) do |container| 4 | container.register("special") do 5 | require "test/foo" 6 | Test::Foo.new("special") 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/fixtures/memoize_magic_comments/test/memoize_false_comment.rb: -------------------------------------------------------------------------------- 1 | # memoize: false 2 | # frozen_string_literal: true 3 | 4 | module Test 5 | class MemoizeFalseComment 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/fixtures/memoize_magic_comments/test/memoize_no_comment.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Test 4 | class MemoizeNoComment 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/fixtures/memoize_magic_comments/test/memoize_true_comment.rb: -------------------------------------------------------------------------------- 1 | # memoize: true 2 | # frozen_string_literal: true 3 | 4 | module Test 5 | class MemoizeTrueComment 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/fixtures/mixed_namespaces/lib/test/external/external_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Test 4 | module External 5 | class ExternalComponent 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/fixtures/mixed_namespaces/lib/test/my_app/app_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Test 4 | module MyApp 5 | class AppComponent 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/fixtures/multiple_namespaced_components/multiple/level/baz.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Multiple 4 | module Level 5 | class Baz 6 | include Test::Container.injector["foz"] 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/fixtures/multiple_namespaced_components/multiple/level/foz.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Multiple 4 | module Level 5 | class Foz 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/fixtures/multiple_provider_dirs/custom_bootables/logger.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Test::Container.register_provider(:logger) do 4 | start do 5 | register(:logger, "custom_logger") 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/fixtures/multiple_provider_dirs/default_bootables/inflector.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Test::Container.register_provider(:inflector) do 4 | start do 5 | register(:inflector, "default_inflector") 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/fixtures/multiple_provider_dirs/default_bootables/logger.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Test::Container.register_provider(:logger) do 4 | start do 5 | register(:logger, "default_logger") 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/fixtures/namespaced_components/namespaced/bar.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Namespaced 4 | class Bar 5 | include Test::Container.injector["foo"] 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/fixtures/namespaced_components/namespaced/foo.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Namespaced 4 | class Foo 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/fixtures/other/config/providers/bar.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Test::Container.register_provider(:bar, namespace: "test") do |_container| 4 | prepare do 5 | module Test 6 | module Bar 7 | # I shall be booted 8 | end 9 | end 10 | end 11 | 12 | start do 13 | register(:bar, "I was finalized") 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/fixtures/other/config/providers/hell.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Test::Container.register_provider(:heaven) do 4 | start do 5 | register("heaven", "string") 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/fixtures/other/lib/test/dep.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Test 4 | class Dep 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/fixtures/other/lib/test/foo.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Test 4 | class Foo 5 | include Import["test.dep"] 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/fixtures/other/lib/test/models.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Test 4 | module Models 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/fixtures/other/lib/test/models/book.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Test 4 | module Models 5 | class Book 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/fixtures/other/lib/test/models/user.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Test 4 | module Models 5 | class User 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/fixtures/require_path/lib/test/foo.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Test 4 | class Foo 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/fixtures/settings_test/.env: -------------------------------------------------------------------------------- 1 | SESSION_SECRET="super-secret" -------------------------------------------------------------------------------- /spec/fixtures/settings_test/types.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/types" 4 | 5 | module SettingsTest 6 | module Types 7 | include Dry::Types() 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/fixtures/standard_container_with_default_namespace/lib/test/dep.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Test 4 | class Dep 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/fixtures/standard_container_with_default_namespace/lib/test/example_with_dep.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Test 4 | class ExampleWithDep 5 | include Import["dep"] 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/fixtures/standard_container_without_default_namespace/lib/test/dep.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Test 4 | class Dep 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/fixtures/standard_container_without_default_namespace/lib/test/example_with_dep.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Test 4 | class ExampleWithDep 5 | include Import["test.dep"] 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/fixtures/stubbing/lib/test/car.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Test 4 | class Car 5 | def wheels_count 6 | 4 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/fixtures/stubbing/system/providers/db.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Test::Container.register_provider(:db) do 4 | module Test 5 | class DB 6 | end 7 | end 8 | 9 | start do 10 | register(:db, Test::DB.new) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/fixtures/test/config/application.yml: -------------------------------------------------------------------------------- 1 | test: 2 | foo: 'bar' 3 | -------------------------------------------------------------------------------- /spec/fixtures/test/config/subapp.yml: -------------------------------------------------------------------------------- 1 | test: 2 | bar: 'baz' 3 | -------------------------------------------------------------------------------- /spec/fixtures/test/lib/test/dep.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Test 4 | class Dep 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/fixtures/test/lib/test/foo.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Test 4 | class Foo 5 | include Import["test.dep"] 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/fixtures/test/lib/test/models.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Test 4 | module Models 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/fixtures/test/lib/test/models/book.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Test 4 | module Models 5 | class Book 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/fixtures/test/lib/test/models/user.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Test 4 | module Models 5 | class User 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/fixtures/test/lib/test/singleton_dep.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "singleton" 4 | 5 | module Test 6 | class SingletonDep 7 | include Singleton 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/fixtures/test/log/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dry-rb/dry-system/dd4ad2f8c0d1821ae7414d47dfaa7482fae8a7d3/spec/fixtures/test/log/.gitkeep -------------------------------------------------------------------------------- /spec/fixtures/test/system/providers/bar.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Test::Container.register_provider(:bar, namespace: "test") do 4 | prepare do 5 | module Test 6 | module Bar 7 | # I shall be booted 8 | end 9 | end 10 | end 11 | 12 | start do 13 | register(:bar, "I was finalized") 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/fixtures/test/system/providers/client.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Test::Container.register_provider(:client) do 4 | start do 5 | target.start :logger 6 | 7 | Test::Client = Struct.new(:logger) 8 | 9 | register(:client, Test::Client.new(target_container["logger"])) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/fixtures/test/system/providers/db.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # this is just for Container.finalize spec, actual finalization code is in the test 4 | -------------------------------------------------------------------------------- /spec/fixtures/test/system/providers/hell.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Test::Container.register_provider(:heaven) do 4 | start do 5 | register("heaven", "string") 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/fixtures/test/system/providers/logger.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Test 4 | class LoggerProvider < Dry::System::Provider::Source 5 | def prepare 6 | require "logger" 7 | end 8 | 9 | def start 10 | register(:logger, Logger.new(target_container.root.join("log/test.log"))) 11 | end 12 | end 13 | end 14 | 15 | Test::Container.register_provider(:logger, source: Test::LoggerProvider) 16 | -------------------------------------------------------------------------------- /spec/fixtures/umbrella/system/providers/db.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Test::Umbrella.register_provider(:db, namespace: "db") do 4 | prepare do 5 | module Db 6 | class Repo 7 | end 8 | end 9 | end 10 | 11 | start do 12 | register(:repo, Db::Repo.new) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/fixtures/unit/component/component_dir_1/namespace/nested/component_file.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | -------------------------------------------------------------------------------- /spec/fixtures/unit/component/component_dir_1/namespace/nested/component_file_with_auto_register_false.rb: -------------------------------------------------------------------------------- 1 | # auto_register: false 2 | # frozen_string_literal: true 3 | -------------------------------------------------------------------------------- /spec/fixtures/unit/component/component_dir_1/outside_namespace/component_file.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | -------------------------------------------------------------------------------- /spec/fixtures/unit/component/component_dir_2/namespace/nested/component_file.rb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dry-rb/dry-system/dd4ad2f8c0d1821ae7414d47dfaa7482fae8a7d3/spec/fixtures/unit/component/component_dir_2/namespace/nested/component_file.rb -------------------------------------------------------------------------------- /spec/fixtures/unit/component_dir/component_file.rb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dry-rb/dry-system/dd4ad2f8c0d1821ae7414d47dfaa7482fae8a7d3/spec/fixtures/unit/component_dir/component_file.rb -------------------------------------------------------------------------------- /spec/integration/boot_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "ostruct" 4 | 5 | RSpec.describe Dry::System::Container, ".register_provider" do 6 | subject(:system) { Test::Container } 7 | let(:setup_db) do 8 | system.register_provider(:db) do 9 | prepare do 10 | module Test 11 | class Db < OpenStruct 12 | end 13 | end 14 | end 15 | 16 | start do 17 | register("db.conn", Test::Db.new(established: true)) 18 | end 19 | 20 | stop do 21 | container["db.conn"].established = false 22 | end 23 | end 24 | end 25 | 26 | let(:setup_client) do 27 | system.register_provider(:client) do 28 | prepare do 29 | module Test 30 | class Client < OpenStruct 31 | end 32 | end 33 | end 34 | 35 | start do 36 | register("client.conn", Test::Client.new(connected: true)) 37 | end 38 | 39 | stop do 40 | container["client.conn"].connected = false 41 | end 42 | end 43 | end 44 | 45 | context "with a boot file" do 46 | before do 47 | class Test::Container < Dry::System::Container 48 | configure do |config| 49 | config.root = SPEC_ROOT.join("fixtures/test").realpath 50 | end 51 | end 52 | end 53 | 54 | it "auto-boots dependency of a bootable component" do 55 | system.start(:client) 56 | 57 | expect(system[:client]).to be_a(Test::Client) 58 | expect(system[:client].logger).to be_a(Logger) 59 | end 60 | end 61 | 62 | context "using predefined settings for configuration" do 63 | before do 64 | class Test::Container < Dry::System::Container 65 | end 66 | end 67 | 68 | it "uses defaults" do 69 | system.register_provider(:api) do 70 | setting :token, default: "xxx" 71 | 72 | start do 73 | register(:client, OpenStruct.new(config.to_h)) 74 | end 75 | end 76 | 77 | system.start(:api) 78 | 79 | client = system[:client] 80 | 81 | expect(client.token).to eql("xxx") 82 | end 83 | end 84 | 85 | context "inline booting" do 86 | before do 87 | class Test::Container < Dry::System::Container 88 | end 89 | end 90 | 91 | it "allows lazy-booting" do 92 | system.register_provider(:db) do 93 | prepare do 94 | module Test 95 | class Db < OpenStruct 96 | end 97 | end 98 | end 99 | 100 | start do 101 | register("db.conn", Test::Db.new(established?: true)) 102 | end 103 | 104 | stop do 105 | db.conn.established = false 106 | end 107 | end 108 | conn = system["db.conn"] 109 | 110 | expect(conn).to be_established 111 | end 112 | 113 | it "allows component to be stopped" do 114 | setup_db 115 | system.start(:db) 116 | 117 | conn = system["db.conn"] 118 | system.stop(:db) 119 | 120 | expect(conn.established).to eq false 121 | end 122 | 123 | xit "raises an error when trying to stop a component that has not been started" do 124 | setup_db 125 | 126 | expect { 127 | system.stop(:db) 128 | }.to raise_error(Dry::System::ProviderNotStartedError) 129 | end 130 | 131 | describe "#shutdown!" do 132 | it "allows container to stop all started components" do 133 | setup_db 134 | setup_client 135 | 136 | db = system["db.conn"] 137 | client = system["client.conn"] 138 | system.shutdown! 139 | 140 | expect(db.established).to eq false 141 | expect(client.connected).to eq false 142 | end 143 | 144 | it "skips components that has not been started" do 145 | setup_db 146 | setup_client 147 | 148 | db = system["db.conn"] 149 | system.shutdown! 150 | 151 | expect { 152 | system.shutdown! 153 | }.to_not raise_error 154 | 155 | expect(db.established).to eq false 156 | end 157 | end 158 | end 159 | end 160 | -------------------------------------------------------------------------------- /spec/integration/container/auto_registration/component_dir_namespaces/deep_namespace_paths_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe "Component dir namespaces / Deep namespace paths" do 4 | let(:container) { 5 | root = @dir 6 | dir_config = defined?(component_dir_config) ? component_dir_config : -> * {} 7 | 8 | Class.new(Dry::System::Container) { 9 | configure do |config| 10 | config.root = root 11 | config.component_dirs.add("lib", &dir_config) 12 | end 13 | } 14 | } 15 | 16 | context "key namespace not given" do 17 | let(:component_dir_config) { 18 | -> dir { 19 | dir.namespaces.add "ns/nested", const: nil 20 | } 21 | } 22 | 23 | before :context do 24 | @dir = make_tmp_directory 25 | 26 | with_directory(@dir) do 27 | write "lib/ns/nested/component.rb", <<~RUBY 28 | class Component 29 | end 30 | RUBY 31 | end 32 | end 33 | 34 | context "lazy loading" do 35 | it "registers components using the key namespace separator ('.'), not the path separator used for the namespace path" do 36 | expect(container["ns.nested.component"]).to be_an_instance_of Component 37 | end 38 | end 39 | 40 | context "finalized" do 41 | before do 42 | container.finalize! 43 | end 44 | 45 | it "registers components using the key namespace separator ('.'), not the path separator used for the namespace path" do 46 | expect(container["ns.nested.component"]).to be_an_instance_of Component 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /spec/integration/container/auto_registration/component_dir_namespaces/namespaces_as_defaults_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe "Component dir namespaces / Namespaces as component dir defaults" do 4 | let(:container) { 5 | root = @dir 6 | cont_config = defined?(container_config) ? container_config : -> * {} 7 | 8 | Class.new(Dry::System::Container) { 9 | configure do |config| 10 | config.root = root 11 | 12 | cont_config.(config) 13 | end 14 | } 15 | } 16 | 17 | let(:container_config) { 18 | -> config { 19 | config.component_dirs.add "lib" 20 | 21 | config.component_dirs.namespaces.add "top_level_const", const: nil 22 | config.component_dirs.namespaces.add "top_level_key", key: nil 23 | 24 | config.component_dirs.add "xyz" 25 | } 26 | } 27 | 28 | before :context do 29 | @dir = make_tmp_directory 30 | 31 | with_directory(@dir) do 32 | write "lib/top_level_const/top_level_lib_component.rb", <<~RUBY 33 | class TopLevelLibComponent 34 | end 35 | RUBY 36 | 37 | write "xyz/top_level_const/top_level_xyz_component.rb", <<~RUBY 38 | class TopLevelXyzComponent 39 | end 40 | RUBY 41 | 42 | write "lib/top_level_key/nested/lib_component.rb", <<~RUBY 43 | module TopLevelKey 44 | module Nested 45 | class LibComponent 46 | end 47 | end 48 | end 49 | RUBY 50 | 51 | write "xyz/top_level_key/nested/xyz_component.rb", <<~RUBY 52 | module TopLevelKey 53 | module Nested 54 | class XyzComponent 55 | end 56 | end 57 | end 58 | RUBY 59 | end 60 | end 61 | 62 | context "lazy loading" do 63 | it "resolves the components from multiple component dirs according to the default namespaces" do 64 | expect(container["top_level_const.top_level_lib_component"]).to be_an_instance_of TopLevelLibComponent 65 | expect(container["top_level_const.top_level_xyz_component"]).to be_an_instance_of TopLevelXyzComponent 66 | 67 | expect(container["nested.lib_component"]).to be_an_instance_of TopLevelKey::Nested::LibComponent 68 | expect(container["nested.xyz_component"]).to be_an_instance_of TopLevelKey::Nested::XyzComponent 69 | end 70 | end 71 | 72 | context "finalized" do 73 | before do 74 | container.finalize! 75 | end 76 | 77 | it "resolves the components from multiple component dirs according to the default namespaces" do 78 | expect(container["top_level_const.top_level_lib_component"]).to be_an_instance_of TopLevelLibComponent 79 | expect(container["top_level_const.top_level_xyz_component"]).to be_an_instance_of TopLevelXyzComponent 80 | 81 | expect(container["nested.lib_component"]).to be_an_instance_of TopLevelKey::Nested::LibComponent 82 | expect(container["nested.xyz_component"]).to be_an_instance_of TopLevelKey::Nested::XyzComponent 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /spec/integration/container/auto_registration/custom_auto_register_proc_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe "Auto-registration / Custom auto_register proc" do 4 | before do 5 | class Test::Container < Dry::System::Container 6 | configure do |config| 7 | config.root = SPEC_ROOT.join("fixtures").realpath 8 | 9 | config.component_dirs.add "components" do |dir| 10 | dir.namespaces.add "test", key: nil 11 | dir.auto_register = proc do |component| 12 | !component.key.match?(/bar/) 13 | end 14 | end 15 | end 16 | end 17 | end 18 | 19 | shared_examples "custom auto_register proc" do 20 | it "registers components according to the custom auto_register proc" do 21 | expect(Test::Container.key?("foo")).to be true 22 | expect(Test::Container.key?("bar")).to be false 23 | expect(Test::Container.key?("bar.baz")).to be false 24 | end 25 | end 26 | 27 | context "Finalized container" do 28 | before do 29 | Test::Container.finalize! 30 | end 31 | 32 | include_examples "custom auto_register proc" 33 | end 34 | 35 | context "Non-finalized container" do 36 | include_examples "custom auto_register proc" 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/integration/container/auto_registration/custom_instance_proc_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe "Auto-registration / Custom instance proc" do 4 | before :context do 5 | with_directory(@dir = make_tmp_directory) do 6 | write "lib/foo.rb", <<~RUBY 7 | module Test 8 | class Foo; end 9 | end 10 | RUBY 11 | end 12 | end 13 | 14 | let(:container) { 15 | root = @dir 16 | Class.new(Dry::System::Container) { 17 | configure do |config| 18 | config.root = root 19 | 20 | config.component_dirs.add "lib" do |dir| 21 | dir.namespaces.add_root const: "test" 22 | dir.instance = proc do |component, *args| 23 | # Return the component's string key as its instance 24 | component.key 25 | end 26 | end 27 | end 28 | } 29 | } 30 | 31 | shared_examples "custom instance proc" do 32 | it "registers the component using the custom loader" do 33 | expect(container["foo"]).to eq "foo" 34 | end 35 | end 36 | 37 | context "Non-finalized container" do 38 | include_examples "custom instance proc" 39 | end 40 | 41 | context "Finalized container" do 42 | before do 43 | container.finalize! 44 | end 45 | 46 | include_examples "custom instance proc" 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /spec/integration/container/auto_registration/custom_loader_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe "Auto-registration / Custom loader" do 4 | before do 5 | # A loader that simply returns the component's identifier string as its instance 6 | class Test::IdentifierLoader 7 | def self.call(component, *_args) 8 | component.identifier.to_s 9 | end 10 | end 11 | 12 | class Test::Container < Dry::System::Container 13 | configure do |config| 14 | config.root = SPEC_ROOT.join("fixtures").realpath 15 | 16 | config.component_dirs.add "components" do |dir| 17 | dir.namespaces.add "test", key: nil 18 | dir.loader = Test::IdentifierLoader 19 | end 20 | end 21 | end 22 | end 23 | 24 | shared_examples "custom loader" do 25 | it "registers the component using the custom loader" do 26 | expect(Test::Container["foo"]).to eq "foo" 27 | end 28 | end 29 | 30 | context "Finalized container" do 31 | before do 32 | Test::Container.finalize! 33 | end 34 | 35 | include_examples "custom loader" 36 | end 37 | 38 | context "Non-finalized container" do 39 | include_examples "custom loader" 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/integration/container/auto_registration/mixed_namespaces_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe "Auto-registration / Components with mixed namespaces" do 4 | before do 5 | class Test::Container < Dry::System::Container 6 | configure do |config| 7 | config.root = SPEC_ROOT.join("fixtures/mixed_namespaces").realpath 8 | 9 | config.component_dirs.add "lib" do |dir| 10 | dir.namespaces.add "test/my_app", key: nil 11 | end 12 | end 13 | end 14 | end 15 | 16 | it "loads components with and without the default namespace (lazy loading)" do 17 | aggregate_failures do 18 | expect(Test::Container["app_component"]).to be_an_instance_of Test::MyApp::AppComponent 19 | expect(Test::Container["test.external.external_component"]).to be_an_instance_of Test::External::ExternalComponent 20 | end 21 | end 22 | 23 | it "loads components with and without the default namespace (finalizing)" do 24 | Test::Container.finalize! 25 | 26 | aggregate_failures do 27 | expect(Test::Container["app_component"]).to be_an_instance_of Test::MyApp::AppComponent 28 | expect(Test::Container["test.external.external_component"]).to be_an_instance_of Test::External::ExternalComponent 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/integration/container/auto_registration_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/system/container" 4 | require "zeitwerk" 5 | 6 | RSpec.describe "Auto-registration" do 7 | specify "Resolving components from a non-finalized container, without a default namespace" do 8 | module Test 9 | class Container < Dry::System::Container 10 | configure do |config| 11 | config.root = SPEC_ROOT.join("fixtures/standard_container_without_default_namespace").realpath 12 | config.component_dirs.add "lib" 13 | end 14 | end 15 | 16 | Import = Container.injector 17 | end 18 | 19 | example_with_dep = Test::Container["test.example_with_dep"] 20 | 21 | expect(example_with_dep).to be_a Test::ExampleWithDep 22 | expect(example_with_dep.dep).to be_a Test::Dep 23 | end 24 | 25 | specify "Resolving components from a non-finalized container, with a default namespace" do 26 | module Test 27 | class Container < Dry::System::Container 28 | configure do |config| 29 | config.root = SPEC_ROOT.join("fixtures/standard_container_with_default_namespace").realpath 30 | config.component_dirs.add "lib" do |dir| 31 | dir.namespaces.add "test", key: nil 32 | end 33 | end 34 | end 35 | 36 | Import = Container.injector 37 | end 38 | 39 | example_with_dep = Test::Container["example_with_dep"] 40 | 41 | expect(example_with_dep).to be_a Test::ExampleWithDep 42 | expect(example_with_dep.dep).to be_a Test::Dep 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/integration/container/autoloading_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/system/container" 4 | require "dry/system/loader/autoloading" 5 | require "zeitwerk" 6 | 7 | RSpec.describe "Autoloading loader" do 8 | include ZeitwerkHelpers 9 | 10 | specify "Resolving components using Zeitwerk" do 11 | module Test 12 | class Container < Dry::System::Container 13 | config.root = SPEC_ROOT.join("fixtures/autoloading").realpath 14 | config.component_dirs.loader = Dry::System::Loader::Autoloading 15 | config.component_dirs.add "lib" do |dir| 16 | dir.add_to_load_path = false 17 | dir.namespaces.add "test", key: nil 18 | end 19 | end 20 | end 21 | 22 | loader = Zeitwerk::Loader.new 23 | loader.push_dir Test::Container.config.root.join("lib").realpath 24 | loader.setup 25 | 26 | foo = Test::Container["foo"] 27 | entity = foo.call 28 | 29 | expect(foo).to be_a Test::Foo 30 | expect(entity).to be_a Test::Entities::FooEntity 31 | 32 | teardown_zeitwerk 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/integration/container/importing/container_registration_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe "Container / Imports / Container registration" do 4 | let(:exporting_container) do 5 | Class.new(Dry::System::Container) { 6 | register "block_component" do 7 | Object.new 8 | end 9 | 10 | register "direct_component", Object.new 11 | 12 | register "memoized_component", memoize: true do 13 | Object.new 14 | end 15 | 16 | register "existing_component", "from exporting container" 17 | } 18 | end 19 | 20 | let(:importing_container) do 21 | Class.new(Dry::System::Container) { 22 | register "existing_component", "from importing container" 23 | } 24 | end 25 | 26 | before do 27 | importing_container.import(from: exporting_container, as: :other).finalize! 28 | end 29 | 30 | it "imports components with the same options as their original registration" do 31 | block_component_a = importing_container["other.block_component"] 32 | block_component_b = importing_container["other.block_component"] 33 | 34 | expect(block_component_a).to be_an_instance_of(block_component_b.class) 35 | expect(block_component_a).not_to be block_component_b 36 | 37 | direct_component_a = importing_container["other.direct_component"] 38 | direct_component_b = importing_container["other.direct_component"] 39 | 40 | expect(direct_component_a).to be direct_component_b 41 | 42 | memoized_component_a = importing_container["other.memoized_component"] 43 | memoized_component_b = importing_container["other.memoized_component"] 44 | 45 | expect(memoized_component_a).to be memoized_component_b 46 | 47 | expect(importing_container["existing_component"]).to eql("from importing container") 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /spec/integration/container/importing/import_namespaces_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe "Container / Imports / Import namespaces" do 4 | before :context do 5 | @dir = make_tmp_directory 6 | 7 | with_directory @dir do 8 | write "lib/exportable_component_a.rb", <<~RUBY 9 | module Test 10 | class ExportableComponentA; end 11 | end 12 | RUBY 13 | 14 | write "lib/nested/exportable_component_b.rb", <<~RUBY 15 | module Test 16 | module Nested 17 | class ExportableComponentB; end 18 | end 19 | end 20 | RUBY 21 | end 22 | end 23 | 24 | let(:exporting_container) { 25 | root = @dir 26 | exports = self.exports if respond_to?(:exports) 27 | 28 | Class.new(Dry::System::Container) { 29 | configure do |config| 30 | config.root = root 31 | config.component_dirs.add "lib" do |dir| 32 | dir.namespaces.add_root const: "test" 33 | end 34 | config.exports = exports if exports 35 | end 36 | } 37 | } 38 | 39 | context "nil namespace" do 40 | context "no keys specified" do 41 | let(:importing_container) { 42 | exporting_container = self.exporting_container 43 | 44 | Class.new(Dry::System::Container) { 45 | import from: exporting_container, as: nil 46 | } 47 | } 48 | 49 | context "importing container is lazy loading" do 50 | it "imports all the components" do 51 | expect(importing_container.key?("exportable_component_a")).to be true 52 | expect(importing_container.key?("nested.exportable_component_b")).to be true 53 | expect(importing_container.key?("non_existent")).to be false 54 | end 55 | end 56 | 57 | context "importing container is finalized" do 58 | before do 59 | importing_container.finalize! 60 | end 61 | 62 | it "imports all the components" do 63 | expect(importing_container.key?("exportable_component_a")).to be true 64 | expect(importing_container.key?("nested.exportable_component_b")).to be true 65 | expect(importing_container.key?("non_existent")).to be false 66 | end 67 | end 68 | end 69 | 70 | context "keys specified" do 71 | let(:importing_container) { 72 | exporting_container = self.exporting_container 73 | 74 | Class.new(Dry::System::Container) { 75 | import keys: ["exportable_component_a"], from: exporting_container, as: nil 76 | } 77 | } 78 | 79 | context "importing container is lazy loading" do 80 | it "imports the specified components only" do 81 | expect(importing_container.key?("exportable_component_a")).to be true 82 | expect(importing_container.key?("nested.exportable_component_b")).to be false 83 | end 84 | end 85 | 86 | context "importing container is finalized" do 87 | before do 88 | importing_container.finalize! 89 | end 90 | 91 | it "imports the specified components only" do 92 | expect(importing_container.key?("exportable_component_a")).to be true 93 | expect(importing_container.key?("nested.exportable_component_b")).to be false 94 | end 95 | end 96 | end 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /spec/integration/container/importing/imported_component_protection_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe "Container / Imports / Protection of imported components from export" do 4 | let(:source_container_1) { 5 | Class.new(Dry::System::Container) { 6 | register("component", Object.new) 7 | } 8 | } 9 | 10 | let(:source_container_2) { 11 | container_1 = source_container_1 12 | 13 | Class.new(Dry::System::Container) { 14 | register("component", Object.new) 15 | 16 | import from: container_1, as: :container_1 17 | } 18 | } 19 | 20 | let(:importing_container) { 21 | container_2 = source_container_2 22 | 23 | Class.new(Dry::System::Container) { 24 | import from: container_2, as: :container_2 25 | } 26 | } 27 | 28 | describe "no exports configured" do 29 | context "importing container lazy loading" do 30 | it "does not import components that were themselves imported" do 31 | expect(importing_container.key?("container_2.component")).to be true 32 | expect(importing_container.key?("container_2.container_1.component")).to be false 33 | end 34 | end 35 | 36 | context "importing container finalized" do 37 | before do 38 | importing_container.finalize! 39 | end 40 | 41 | it "does not import components that were themselves imported" do 42 | expect(importing_container.keys).to eq ["container_2.component"] 43 | end 44 | end 45 | end 46 | 47 | describe "exports configured with imported components included" do 48 | let(:source_container_2) { 49 | container_1 = source_container_1 50 | 51 | Class.new(Dry::System::Container) { 52 | configure do |config| 53 | config.exports = %w[component container_1.component] 54 | end 55 | 56 | register("component", Object.new) 57 | 58 | import from: container_1, as: :container_1 59 | } 60 | } 61 | 62 | context "importing container lazy loading" do 63 | it "imports the previously-imported component" do 64 | expect(importing_container.key?("container_2.component")).to be true 65 | expect(importing_container.key?("container_2.container_1.component")).to be true 66 | end 67 | end 68 | 69 | context "importing container finalized" do 70 | before do 71 | importing_container.finalize! 72 | end 73 | 74 | it "imports the previously-imported component" do 75 | expect(importing_container.keys).to eq %w[container_2.component container_2.container_1.component] 76 | end 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /spec/integration/container/lazy_loading/auto_registration_disabled_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe "Lazy loading components with auto-registration disabled" do 4 | before do 5 | module Test 6 | class Container < Dry::System::Container 7 | configure do |config| 8 | config.root = SPEC_ROOT.join("fixtures/lazy_loading/auto_registration_disabled").realpath 9 | config.component_dirs.add "lib" 10 | end 11 | end 12 | end 13 | end 14 | 15 | it "reports the component as absent" do 16 | expect(Test::Container.key?("entities.kitten")).to be false 17 | end 18 | 19 | it "does not load the component" do 20 | expect { Test::Container["entities.kitten"] }.to raise_error(Dry::Core::Container::KeyError) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/integration/container/lazy_loading/bootable_components_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe "Lazy loading bootable components" do 4 | describe "Booting component when resolving another components with bootable component as root key" do 5 | before do 6 | module Test 7 | class Container < Dry::System::Container 8 | configure do |config| 9 | config.root = SPEC_ROOT.join("fixtures/lazy_loading/shared_root_keys").realpath 10 | config.component_dirs.add "lib" 11 | end 12 | end 13 | end 14 | end 15 | 16 | context "Single container" do 17 | it "boots the component and can resolve multiple other components registered using the same root key" do 18 | expect(Test::Container["kitten_service.fetch_kitten"]).to be 19 | expect(Test::Container.keys).to include("kitten_service.client", "kitten_service.fetch_kitten") 20 | expect(Test::Container["kitten_service.submit_kitten"]).to be 21 | expect(Test::Container.keys).to include("kitten_service.client", "kitten_service.fetch_kitten", "kitten_service.submit_kitten") 22 | end 23 | end 24 | 25 | context "Bootable component in imported container" do 26 | before do 27 | module Test 28 | class AnotherContainer < Dry::System::Container 29 | import from: Container, as: :core 30 | end 31 | end 32 | end 33 | 34 | context "lazy loading" do 35 | it "boots the component and can resolve multiple other components registered using the same root key" do 36 | expect(Test::AnotherContainer["core.kitten_service.fetch_kitten"]).to be 37 | expect(Test::AnotherContainer.keys).to include("core.kitten_service.fetch_kitten") 38 | 39 | expect(Test::AnotherContainer["core.kitten_service.submit_kitten"]).to be 40 | expect(Test::AnotherContainer.keys).to include("core.kitten_service.submit_kitten") 41 | 42 | expect(Test::AnotherContainer["core.kitten_service.client"]).to be 43 | expect(Test::AnotherContainer.keys).to include("core.kitten_service.client") 44 | end 45 | end 46 | 47 | context "finalized" do 48 | before do 49 | Test::AnotherContainer.finalize! 50 | end 51 | 52 | it "boots the component in the imported container and imports the bootable component's registered components" do 53 | expect(Test::AnotherContainer.keys).to include("core.kitten_service.fetch_kitten", "core.kitten_service.submit_kitten", "core.kitten_service.client") 54 | end 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /spec/integration/container/lazy_loading/manifest_registration_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe "Lazy-loading registration manifest files" do 4 | before do 5 | module Test 6 | class Container < Dry::System::Container 7 | configure do |config| 8 | config.root = SPEC_ROOT.join("fixtures/manifest_registration").realpath 9 | end 10 | 11 | add_to_load_path!("lib") 12 | end 13 | end 14 | end 15 | 16 | shared_examples "manifest component" do 17 | it "loads a registration manifest file if the component could not be found" do 18 | expect(Test::Container["foo.special"]).to be_a(Test::Foo) 19 | expect(Test::Container["foo.special"].name).to eq "special" 20 | end 21 | end 22 | 23 | context "Non-finalized container" do 24 | include_examples "manifest component" 25 | end 26 | 27 | context "Finalized container" do 28 | before { Test::Container.finalize! } 29 | include_examples "manifest component" 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/integration/container/plugins/bootsnap_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe "Plugins / Bootsnap" do 4 | subject(:system) do 5 | Class.new(Dry::System::Container) do 6 | use :bootsnap 7 | 8 | configure do |config| 9 | config.root = SPEC_ROOT.join("fixtures/test") 10 | config.env = :development 11 | config.bootsnap = { 12 | load_path_cache: false, 13 | compile_cache_iseq: true, 14 | compile_cache_yaml: true 15 | } 16 | end 17 | end 18 | end 19 | 20 | let(:cache_dir) do 21 | system.root.join("tmp/cache") 22 | end 23 | 24 | let(:bootsnap_cache_file) do 25 | cache_dir.join("bootsnap") 26 | end 27 | 28 | before do 29 | FileUtils.rm_rf(cache_dir) 30 | FileUtils.mkdir_p(cache_dir) 31 | end 32 | 33 | after do 34 | FileUtils.rm_rf(cache_dir) 35 | end 36 | 37 | describe ".require_from_root" do 38 | if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.4.0") 39 | it "loads file" do 40 | system.require_from_root("lib/test/models") 41 | 42 | expect(Object.const_defined?("Test::Models")).to be(true) 43 | 44 | expect(bootsnap_cache_file.exist?).to be(true) 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /spec/integration/container/plugins/dependency_graph_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe "Plugins / Dependency Graph" do 4 | let(:container) { Test::Container } 5 | subject(:events) { [] } 6 | 7 | before :context do 8 | with_directory(@dir = make_tmp_directory) do 9 | write "system/providers/mailer.rb", <<~RUBY 10 | Test::Container.register_provider :mailer do 11 | start do 12 | register "mailer", Object.new 13 | end 14 | 15 | end 16 | RUBY 17 | 18 | write "lib/foo.rb", <<~RUBY 19 | module Test 20 | class Foo 21 | include Deps["mailer"] 22 | end 23 | end 24 | RUBY 25 | 26 | write "lib/bar.rb", <<~RUBY 27 | module Test 28 | class Bar 29 | include Deps["foo"] 30 | end 31 | end 32 | RUBY 33 | end 34 | end 35 | 36 | before do 37 | root = @dir 38 | Test::Container = Class.new(Dry::System::Container) { 39 | use :dependency_graph 40 | 41 | configure do |config| 42 | config.root = root 43 | config.component_dirs.add "lib" do |dir| 44 | dir.namespaces.add_root const: "test" 45 | end 46 | end 47 | } 48 | end 49 | 50 | before do 51 | container[:notifications].subscribe(:resolved_dependency) { events << _1 } 52 | container[:notifications].subscribe(:registered_dependency) { events << _1 } 53 | end 54 | 55 | shared_examples "dependency graph notifications" do 56 | context "lazy loading" do 57 | it "emits dependency notifications for the resolved component" do 58 | container["foo"] 59 | 60 | expect(events.map { [_1.id, _1.payload] }).to eq [ 61 | [:resolved_dependency, {dependency_map: {mailer: "mailer"}, target_class: Test::Foo}], 62 | [:registered_dependency, {class: Object, key: "mailer"}], 63 | [:registered_dependency, {class: Test::Foo, key: "foo"}] 64 | ] 65 | end 66 | end 67 | 68 | context "finalized" do 69 | before do 70 | container.finalize! 71 | end 72 | 73 | it "emits dependency notifications for all components" do 74 | expect(events.map { [_1.id, _1.payload] }).to eq [ 75 | [:registered_dependency, {key: "mailer", class: Object}], 76 | [:resolved_dependency, {dependency_map: {foo: "foo"}, target_class: Test::Bar}], 77 | [:resolved_dependency, {dependency_map: {mailer: "mailer"}, target_class: Test::Foo}], 78 | [:registered_dependency, {key: "foo", class: Test::Foo}], 79 | [:registered_dependency, {key: "bar", class: Test::Bar}] 80 | ] 81 | end 82 | end 83 | end 84 | 85 | describe "default (kwargs) injector" do 86 | before do 87 | Test::Deps = Test::Container.injector 88 | end 89 | 90 | specify "objects receive dependencies via keyword arguments" do 91 | expect(container["bar"].method(:initialize).parameters).to eq( 92 | [[:keyrest, :kwargs], [:block, :block]] 93 | ) 94 | end 95 | 96 | it_behaves_like "dependency graph notifications" 97 | end 98 | 99 | describe "hash injector" do 100 | before do 101 | Test::Deps = Test::Container.injector.hash 102 | end 103 | 104 | specify "objects receive dependencies via a single options hash argument" do 105 | expect(container["bar"].method(:initialize).parameters).to eq [[:req, :options]] 106 | end 107 | 108 | it_behaves_like "dependency graph notifications" 109 | end 110 | 111 | describe "args injector" do 112 | before do 113 | Test::Deps = Test::Container.injector.args 114 | end 115 | 116 | specify "objects receive dependencies via positional arguments" do 117 | expect(container["bar"].method(:initialize).parameters).to eq [[:req, :foo]] 118 | end 119 | 120 | it_behaves_like "dependency graph notifications" 121 | end 122 | end 123 | -------------------------------------------------------------------------------- /spec/integration/container/plugins/env_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe "Plugins / Env" do 4 | context "with default settings" do 5 | subject(:system) do 6 | Class.new(Dry::System::Container) do 7 | use :env 8 | end 9 | end 10 | 11 | describe ".env" do 12 | it "returns :development" do 13 | expect(system.env).to be(:development) 14 | end 15 | end 16 | end 17 | 18 | context "with a custom inferrer" do 19 | subject(:system) do 20 | Class.new(Dry::System::Container) do 21 | use :env, inferrer: -> { :test } 22 | end 23 | end 24 | 25 | describe ".env" do 26 | it "returns :test" do 27 | expect(system.env).to be(:test) 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/integration/container/plugins/logging_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe "Plugins / Logging" do 4 | before do 5 | system.configure do |config| 6 | config.root = SPEC_ROOT.join("fixtures/test") 7 | end 8 | end 9 | 10 | let(:logger) do 11 | system.logger 12 | end 13 | 14 | let(:log_file_content) do 15 | File.read(system.log_file_path) 16 | end 17 | 18 | context "with default logger settings" do 19 | subject(:system) do 20 | class Test::Container < Dry::System::Container 21 | use :env 22 | use :logging 23 | end 24 | end 25 | 26 | it "logs to development.log" do 27 | logger.info "info message" 28 | 29 | expect(log_file_content).to include("info message") 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/integration/container/plugins/zeitwerk/eager_loading_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # rubocop:disable Style/GlobalVars 4 | 5 | RSpec.describe "Zeitwerk plugin / Eager loading" do 6 | include ZeitwerkHelpers 7 | 8 | before do 9 | $eager_loaded = false 10 | end 11 | 12 | after { teardown_zeitwerk } 13 | 14 | it "Eager loads after finalization" do 15 | with_tmp_directory do |tmp_dir| 16 | write "lib/zeitwerk_eager.rb", <<~RUBY 17 | $eager_loaded = true 18 | 19 | module Test 20 | class ZeitwerkEager; end 21 | end 22 | RUBY 23 | 24 | container = Class.new(Dry::System::Container) do 25 | use :zeitwerk, eager_load: true 26 | 27 | configure do |config| 28 | config.root = tmp_dir 29 | config.component_dirs.add "lib" do |dir| 30 | dir.namespaces.add_root const: "test" 31 | end 32 | end 33 | end 34 | 35 | expect { container.finalize! }.to change { $eager_loaded }.to true 36 | end 37 | end 38 | 39 | it "Eager loads in production by default" do 40 | with_tmp_directory do |tmp_dir| 41 | write "lib/zeitwerk_eager.rb", <<~RUBY 42 | $eager_loaded = true 43 | 44 | module Test 45 | class ZeitwerkEager; end 46 | end 47 | RUBY 48 | 49 | container = Class.new(Dry::System::Container) do 50 | use :env, inferrer: -> { :production } 51 | use :zeitwerk 52 | 53 | configure do |config| 54 | config.root = tmp_dir 55 | config.component_dirs.add "lib" do |dir| 56 | dir.namespaces.add_root const: "test" 57 | end 58 | end 59 | end 60 | 61 | expect { container.finalize! }.to change { $eager_loaded }.to true 62 | end 63 | end 64 | end 65 | 66 | # rubocop:enable Style/GlobalVars 67 | -------------------------------------------------------------------------------- /spec/integration/container/plugins/zeitwerk/namespaces_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe "Zeitwerk plugin / Namespaces" do 4 | include ZeitwerkHelpers 5 | 6 | after { teardown_zeitwerk } 7 | 8 | it "loads components from a root namespace with a const namespace" do 9 | with_tmp_directory do |tmp_dir| 10 | write "lib/foo.rb", <<~RUBY 11 | module Test 12 | class Foo; end 13 | end 14 | RUBY 15 | 16 | container = Class.new(Dry::System::Container) do 17 | use :zeitwerk 18 | 19 | configure do |config| 20 | config.root = tmp_dir 21 | 22 | config.component_dirs.add "lib" do |dir| 23 | dir.namespaces.add_root const: "test" 24 | end 25 | end 26 | end 27 | 28 | expect(container["foo"]).to be_an_instance_of Test::Foo 29 | end 30 | end 31 | 32 | it "loads components from multiple namespace with distinct const namespaces" do 33 | with_tmp_directory do |tmp_dir| 34 | write "lib/foo.rb", <<~RUBY 35 | module Test 36 | class Foo; end 37 | end 38 | RUBY 39 | 40 | write "lib/nested/foo.rb", <<~RUBY 41 | module Test 42 | module Nested 43 | class Foo; end 44 | end 45 | end 46 | RUBY 47 | 48 | write "lib/adapters/bar.rb", <<~RUBY 49 | module My 50 | module Adapters 51 | class Bar; end 52 | end 53 | end 54 | RUBY 55 | 56 | container = Class.new(Dry::System::Container) do 57 | use :zeitwerk 58 | 59 | configure do |config| 60 | config.root = tmp_dir 61 | 62 | config.component_dirs.add "lib" do |dir| 63 | dir.namespaces.add "adapters", const: "my/adapters" 64 | dir.namespaces.add_root const: "test" 65 | end 66 | end 67 | end 68 | 69 | expect(container["foo"]).to be_an_instance_of Test::Foo 70 | expect(container["nested.foo"]).to be_an_instance_of Test::Nested::Foo 71 | expect(container["adapters.bar"]).to be_an_instance_of My::Adapters::Bar 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /spec/integration/container/plugins/zeitwerk/resolving_components_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe "Zeitwerk plugin / Resolving components" do 4 | include ZeitwerkHelpers 5 | 6 | after { teardown_zeitwerk } 7 | 8 | specify "Resolving components using Zeitwerk" do 9 | with_tmp_directory do |tmp_dir| 10 | write "lib/foo.rb", <<~RUBY 11 | module Test 12 | class Foo 13 | def call 14 | Entities::FooEntity.new 15 | end 16 | end 17 | end 18 | RUBY 19 | 20 | write "lib/entities/foo_entity.rb", <<~RUBY 21 | module Test 22 | module Entities 23 | class FooEntity; end 24 | end 25 | end 26 | RUBY 27 | 28 | container = Class.new(Dry::System::Container) do 29 | use :zeitwerk 30 | 31 | configure do |config| 32 | config.root = tmp_dir 33 | config.component_dirs.add "lib" do |dir| 34 | dir.namespaces.add_root const: "test" 35 | end 36 | end 37 | end 38 | 39 | foo = container["foo"] 40 | entity = foo.call 41 | 42 | expect(foo).to be_a Test::Foo 43 | expect(entity).to be_a Test::Entities::FooEntity 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /spec/integration/container/plugins/zeitwerk/user_configured_loader_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe "Zeitwerk plugin / User-configured loader" do 4 | include ZeitwerkHelpers 5 | 6 | after { teardown_zeitwerk } 7 | 8 | it "uses the user-configured loader and pushes component dirs to it" do 9 | with_tmp_directory do |tmp_dir| 10 | write "lib/foo.rb", <<~RUBY 11 | module Test 12 | class Foo;end 13 | end 14 | RUBY 15 | 16 | require "zeitwerk" 17 | 18 | logs = [] 19 | 20 | container = Class.new(Dry::System::Container) do 21 | use :zeitwerk 22 | 23 | configure do |config| 24 | config.root = tmp_dir 25 | config.component_dirs.add "lib" do |dir| 26 | dir.namespaces.add_root const: "test" 27 | end 28 | 29 | config.autoloader = Zeitwerk::Loader.new.tap do |loader| 30 | loader.tag = "custom_loader" 31 | loader.logger = -> str { logs << str } 32 | end 33 | end 34 | end 35 | 36 | expect(container["foo"]).to be_a Test::Foo 37 | expect(logs).not_to be_empty 38 | expect(logs[0]).to include "custom_loader" 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/integration/container/providers/conditional_providers_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe "Providers / Conditional providers" do 4 | let(:container) { 5 | provider_if = self.provider_if 6 | Class.new(Dry::System::Container) { 7 | register_provider :provided, if: provider_if do 8 | start do 9 | register "provided", Object.new 10 | end 11 | end 12 | } 13 | } 14 | 15 | shared_examples "loads the provider" do 16 | it "runs the provider when a related component is resolved" do 17 | expect(container["provided"]).to be 18 | expect(container.providers.key?(:provided)).to be true 19 | end 20 | end 21 | 22 | shared_examples "does not load the provider" do 23 | it "does not run the provider when a related component is resolved" do 24 | expect { container["provided"] }.to raise_error(Dry::Core::Container::KeyError, /key not found: "provided"/) 25 | expect(container.providers.key?(:provided)).to be false 26 | end 27 | end 28 | 29 | describe "true" do 30 | let(:provider_if) { true } 31 | 32 | context "lazy loading" do 33 | include_examples "loads the provider" 34 | end 35 | 36 | context "finalized" do 37 | before { container.finalize! } 38 | include_examples "loads the provider" 39 | end 40 | end 41 | 42 | describe "false" do 43 | let(:provider_if) { false } 44 | 45 | context "lazy loading" do 46 | include_examples "does not load the provider" 47 | end 48 | 49 | context "finalized" do 50 | before { container.finalize! } 51 | include_examples "does not load the provider" 52 | end 53 | end 54 | 55 | describe "provider file in provider dir" do 56 | let(:container) { 57 | root = @dir 58 | Test::Container = Class.new(Dry::System::Container) { 59 | configure do |config| 60 | config.root = root 61 | end 62 | } 63 | } 64 | 65 | describe "true" do 66 | before :context do 67 | with_directory(@dir = make_tmp_directory) do 68 | write "system/providers/provided.rb", <<~RUBY 69 | Test::Container.register_provider :provided, if: true do 70 | start do 71 | register "provided", Object.new 72 | end 73 | end 74 | RUBY 75 | end 76 | end 77 | 78 | context "lazy loading" do 79 | include_examples "loads the provider" 80 | end 81 | 82 | context "finalized" do 83 | before { container.finalize! } 84 | include_examples "loads the provider" 85 | end 86 | end 87 | 88 | describe "true" do 89 | before :context do 90 | with_directory(@dir = make_tmp_directory) do 91 | write "system/providers/provided.rb", <<~RUBY 92 | Test::Container.register_provider :provided, if: false do 93 | start do 94 | register "provided", Object.new 95 | end 96 | end 97 | RUBY 98 | end 99 | end 100 | 101 | context "lazy loading" do 102 | include_examples "does not load the provider" 103 | end 104 | 105 | context "finalized" do 106 | before { container.finalize! } 107 | include_examples "does not load the provider" 108 | end 109 | end 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /spec/integration/container/providers/custom_provider_registrar_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe "Providers / Custom provider registrar" do 4 | specify "Customizing the target_container for providers" do 5 | # Create a provider registrar that exposes a container _wrapper_ (i.e. something resembling a 6 | # Hanami slice) as the target_container. 7 | provider_registrar = Class.new(Dry::System::ProviderRegistrar) do 8 | def self.for_wrapper(wrapper) 9 | Class.new(self) do 10 | define_singleton_method(:new) do |container| 11 | super(container, wrapper) 12 | end 13 | end 14 | end 15 | 16 | attr_reader :wrapper 17 | 18 | def initialize(container, wrapper) 19 | super(container) 20 | @wrapper = wrapper 21 | end 22 | 23 | def target_container 24 | wrapper 25 | end 26 | end 27 | 28 | # Create the wrapper, which has an internal Dry::System::Container (configured with our custom 29 | # provider_registrar) that it then delegates to. 30 | container_wrapper = Class.new do 31 | define_singleton_method(:container) do 32 | @container ||= Class.new(Dry::System::Container).tap do |container| 33 | container.config.provider_registrar = provider_registrar.for_wrapper(self) 34 | end 35 | end 36 | 37 | def self.register_provider(...) 38 | container.register_provider(...) 39 | end 40 | 41 | def self.start(...) 42 | container.start(...) 43 | end 44 | end 45 | 46 | # Create a provider to expose its given `target` so we can make expecations about it 47 | exposed_target = nil 48 | container_wrapper.register_provider(:my_provider) do 49 | start do 50 | exposed_target = target 51 | end 52 | end 53 | container_wrapper.start(:my_provider) 54 | 55 | expect(exposed_target).to be container_wrapper 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /spec/integration/container/providers/custom_provider_superclass_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe "Providers / Custom provider superclass" do 4 | let!(:custom_superclass) do 5 | module Test 6 | class CustomSource < Dry::System::Provider::Source 7 | attr_reader :custom_setting 8 | 9 | def initialize(custom_setting:, **options, &) 10 | super(**options, &) 11 | @custom_setting = custom_setting 12 | end 13 | end 14 | end 15 | 16 | Test::CustomSource 17 | end 18 | 19 | let!(:custom_registrar) do 20 | module Test 21 | class CustomRegistrar < Dry::System::ProviderRegistrar 22 | def provider_source_class = Test::CustomSource 23 | def provider_source_options = {custom_setting: "hello"} 24 | end 25 | end 26 | 27 | Test::CustomRegistrar 28 | end 29 | 30 | subject(:system) do 31 | module Test 32 | class Container < Dry::System::Container 33 | configure do |config| 34 | config.root = SPEC_ROOT.join("fixtures/app").realpath 35 | config.provider_registrar = Test::CustomRegistrar 36 | end 37 | end 38 | end 39 | 40 | Test::Container 41 | end 42 | 43 | it "overrides the default Provider Source base class" do 44 | system.register_provider(:test) {} 45 | 46 | provider_source = system.providers[:test].source 47 | 48 | expect(provider_source.class).to be < custom_superclass 49 | expect(provider_source.class.name).to eq "Test::CustomSource[test]" 50 | expect(provider_source.custom_setting).to eq "hello" 51 | end 52 | 53 | context "Source class != provider_source_class" do 54 | let!(:custom_source) do 55 | module Test 56 | class OtherSource < Dry::System::Provider::Source 57 | attr_reader :options 58 | 59 | def initialize(**options, &block) 60 | @options = options.except(:provider_container, :target_container) 61 | super(**options.slice(:provider_container, :target_container), &block) 62 | end 63 | end 64 | end 65 | 66 | Test::OtherSource 67 | end 68 | 69 | specify "External source doesn't use provider_source_options" do 70 | Dry::System.register_provider_source(:test, group: :custom, source: custom_source) 71 | system.register_provider(:test, from: :custom) {} 72 | 73 | expect { 74 | provider_source = system.providers[:test].source 75 | expect(provider_source.class).to be < Dry::System::Provider::Source 76 | expect(provider_source.options).to be_empty 77 | }.to_not raise_error 78 | end 79 | 80 | specify "Class-based source doesn't use provider_source_options" do 81 | system.register_provider(:test, source: custom_source) 82 | 83 | expect { 84 | provider_source = system.providers[:test].source 85 | expect(provider_source.class).to be < Dry::System::Provider::Source 86 | expect(provider_source.options).to be_empty 87 | }.to_not raise_error 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /spec/integration/container/providers/multiple_provider_dirs_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe "Providers / Multiple provider dirs" do 4 | specify "Resolving provider files from multiple provider dirs" do 5 | module Test 6 | class Container < Dry::System::Container 7 | config.root = SPEC_ROOT.join("fixtures/multiple_provider_dirs").realpath 8 | 9 | config.provider_dirs = [ 10 | "custom_bootables", # Relative paths are appended to the container root 11 | SPEC_ROOT.join("fixtures/multiple_provider_dirs/default_bootables") 12 | ] 13 | end 14 | end 15 | 16 | expect(Test::Container[:inflector]).to eq "default_inflector" 17 | expect(Test::Container[:logger]).to eq "custom_logger" 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/integration/container/providers/provider_sources/provider_options_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe "Providers / Provider sources / Provider options" do 4 | let(:container) { Class.new(Dry::System::Container) } 5 | 6 | specify "provider_options registered with provider sources are used when creating corresponding providers" do 7 | Dry::System.register_provider_source(:db, group: :my_framework, provider_options: {namespace: true}) do 8 | start do 9 | register "config", "db_config_here" 10 | end 11 | end 12 | 13 | # Note no `namespace:` option when registering provider 14 | container.register_provider :db, from: :my_framework 15 | 16 | # Also works when using a different name for the provider 17 | container.register_provider :my_db, from: :my_framework, source: :db 18 | 19 | container.start :db 20 | container.start :my_db 21 | 22 | expect(container["db.config"]).to eq "db_config_here" 23 | expect(container["my_db.config"]).to eq "db_config_here" 24 | end 25 | 26 | specify "provider source provider_options can be overridden" do 27 | Dry::System.register_provider_source(:db, group: :my_framework, provider_options: {namespace: true}) do 28 | start do 29 | register "config", "db_config_here" 30 | end 31 | end 32 | 33 | container.register_provider :db, from: :my_framework, namespace: "custom_db" 34 | 35 | container.start :db 36 | 37 | expect(container["custom_db.config"]).to eq "db_config_here" 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/integration/container/providers/registering_components_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe "Providers / Registering components" do 4 | specify "Components registered with blocks in a provider are resolved as new objects each time in the target container" do 5 | module Test 6 | class Thing; end 7 | end 8 | 9 | container = Class.new(Dry::System::Container) do 10 | register_provider :thing, namespace: true do 11 | start do 12 | register :via_block do 13 | Test::Thing.new 14 | end 15 | 16 | register :direct, Test::Thing.new 17 | end 18 | end 19 | end 20 | 21 | container.start :thing 22 | 23 | thing_via_block_1 = container["thing.via_block"] 24 | thing_via_block_2 = container["thing.via_block"] 25 | 26 | thing_direct_1 = container["thing.direct"] 27 | thing_direct_2 = container["thing.direct"] 28 | 29 | expect(thing_via_block_1).to be_an_instance_of(thing_via_block_2.class) 30 | expect(thing_via_block_1).not_to be thing_via_block_2 31 | 32 | expect(thing_direct_1).to be thing_direct_2 33 | end 34 | 35 | specify "Components registered with options in a provider have those options set on the target container" do 36 | container = Class.new(Dry::System::Container) do 37 | register_provider :thing do 38 | start do 39 | register :thing, memoize: true do 40 | Object.new 41 | end 42 | end 43 | end 44 | end 45 | 46 | container.start :thing 47 | 48 | thing_1 = container["thing"] 49 | thing_2 = container["thing"] 50 | 51 | expect(thing_2).to be thing_1 52 | end 53 | 54 | specify "Components registered with keys that are already used on the target container are not applied" do 55 | container = Class.new(Dry::System::Container) do 56 | register_provider :thing, namespace: true do 57 | start do 58 | register :first, Object.new 59 | register :second, Object.new 60 | end 61 | end 62 | end 63 | 64 | already_registered = Object.new 65 | container.register "thing.second", already_registered 66 | 67 | container.start :thing 68 | 69 | expect(container["thing.first"]).to be 70 | expect(container["thing.second"]).to be already_registered 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /spec/integration/container/providers/resolving_root_key_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe "Providers / Resolving components with same root key as a running provider" do 4 | before :context do 5 | @dir = make_tmp_directory 6 | 7 | with_directory(@dir) do 8 | write "lib/animals/cat.rb", <<~RUBY 9 | module Test 10 | module Animals 11 | class Cat 12 | include Deps["animals.collar"] 13 | end 14 | end 15 | end 16 | RUBY 17 | 18 | write "lib/animals/collar.rb", <<~RUBY 19 | module Test 20 | module Animals 21 | class Collar; end 22 | end 23 | end 24 | RUBY 25 | 26 | write "system/providers/animals.rb", <<~RUBY 27 | Test::Container.register_provider :animals, namespace: true do 28 | start do 29 | require "animals/cat" 30 | register :cat, Test::Animals::Cat.new 31 | end 32 | end 33 | RUBY 34 | end 35 | end 36 | 37 | before do 38 | root = @dir 39 | Test::Container = Class.new(Dry::System::Container) do 40 | configure do |config| 41 | config.root = root 42 | config.component_dirs.add "lib" do |dir| 43 | dir.namespaces.add_root const: "test" 44 | end 45 | end 46 | end 47 | 48 | Test::Deps = Test::Container.injector 49 | end 50 | 51 | context "lazy loading" do 52 | it "resolves the component without attempting to re-run provider steps" do 53 | expect(Test::Container["animals.cat"]).to be 54 | end 55 | end 56 | 57 | context "finalized" do 58 | before do 59 | Test::Container.finalize! 60 | end 61 | 62 | it "resolves the component without attempting to re-run provider steps" do 63 | expect(Test::Container["animals.cat"]).to be 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /spec/integration/did_you_mean_integration_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "ostruct" 4 | 5 | RSpec.describe "DidYouMean integration" do 6 | subject(:system) { Test::Container } 7 | 8 | context "with a file with a syntax error in it" do 9 | before do 10 | class Test::Container < Dry::System::Container 11 | use :zeitwerk 12 | 13 | configure do |config| 14 | config.root = SPEC_ROOT.join("fixtures").join("components_with_errors").realpath 15 | config.component_dirs.add "test" 16 | end 17 | end 18 | end 19 | 20 | it "auto-boots dependency of a bootable component" do 21 | expect { system["constant_error"] } 22 | .to raise_error(NameError, "uninitialized constant ConstantError::NotHere") 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/integration/import_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/system/stubs" 4 | 5 | RSpec.describe "Lazy-booting external deps" do 6 | before do 7 | module Test 8 | class Umbrella < Dry::System::Container 9 | configure do |config| 10 | config.name = :core 11 | config.root = SPEC_ROOT.join("fixtures/umbrella").realpath 12 | end 13 | end 14 | 15 | class App < Dry::System::Container 16 | configure do |config| 17 | config.name = :main 18 | end 19 | end 20 | end 21 | end 22 | 23 | shared_examples_for "lazy booted dependency" do 24 | it "lazy boots an external dep provided by top-level container" do 25 | expect(user_repo.repo).to be_instance_of(Db::Repo) 26 | end 27 | 28 | it "loads an external dep during finalization" do 29 | system.finalize! 30 | expect(user_repo.repo).to be_instance_of(Db::Repo) 31 | end 32 | end 33 | 34 | context "when top-level container provides the dependency" do 35 | let(:user_repo) do 36 | Class.new { include Test::Import["db.repo"] }.new 37 | end 38 | 39 | let(:system) { Test::Umbrella } 40 | 41 | before do 42 | module Test 43 | Umbrella.import(from: App, as: :main) 44 | Import = Umbrella.injector 45 | end 46 | end 47 | 48 | it_behaves_like "lazy booted dependency" 49 | 50 | context "when stubs are enabled" do 51 | before do 52 | system.enable_stubs! 53 | end 54 | 55 | it_behaves_like "lazy booted dependency" 56 | end 57 | end 58 | 59 | context "when top-level container provides the dependency through import" do 60 | let(:user_repo) do 61 | Class.new { include Test::Import["core.db.repo"] }.new 62 | end 63 | 64 | let(:system) { Test::App } 65 | 66 | before do 67 | module Test 68 | App.import(from: Umbrella, as: :core) 69 | Import = App.injector 70 | end 71 | end 72 | 73 | it_behaves_like "lazy booted dependency" 74 | 75 | context "when stubs are enabled" do 76 | before do 77 | system.enable_stubs! 78 | end 79 | 80 | it_behaves_like "lazy booted dependency" 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/setup" 4 | 5 | require "pathname" 6 | require "warning" 7 | 8 | begin 9 | require "byebug" 10 | require "pry-byebug" 11 | rescue LoadError; end 12 | SPEC_ROOT = Pathname(__FILE__).dirname 13 | 14 | Dir[SPEC_ROOT.join("support/*.rb").to_s].each { |f| require f } 15 | Dir[SPEC_ROOT.join("shared/*.rb").to_s].each { |f| require f } 16 | 17 | require "dry/system" 18 | require "dry/system/stubs" 19 | require "dry/events" 20 | require "dry/types" 21 | 22 | # For specs that rely on `settings` DSL 23 | module Types 24 | include Dry::Types() 25 | end 26 | 27 | RSpec.configure do |config| 28 | config.after do 29 | Dry::System.provider_sources.sources.delete_if { |k, _| k[:group] != :dry_system } 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/support/coverage.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # this file is managed by dry-rb/devtools 4 | 5 | if ENV["COVERAGE"] == "true" 6 | require "simplecov" 7 | require "simplecov-cobertura" 8 | 9 | SimpleCov.formatter = SimpleCov::Formatter::CoberturaFormatter 10 | 11 | SimpleCov.start do 12 | add_filter "/spec/" 13 | enable_coverage :branch 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/support/loaded_constants_cleaning.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "tmpdir" 4 | 5 | module TestCleanableNamespace 6 | def remove_constants 7 | constants.each do |name| 8 | remove_const(name) 9 | end 10 | end 11 | end 12 | 13 | RSpec.shared_context "Loaded constants cleaning" do 14 | let(:cleanable_modules) { %i[Test] } 15 | let(:cleanable_constants) { [] } 16 | end 17 | 18 | RSpec.configure do |config| 19 | config.include_context "Loaded constants cleaning" 20 | 21 | config.before do 22 | @load_path = $LOAD_PATH.dup 23 | @loaded_features = $LOADED_FEATURES.dup 24 | 25 | cleanable_modules.each do |mod| 26 | if Object.const_defined?(mod) 27 | Object.const_get(mod).extend(TestCleanableNamespace) 28 | else 29 | Object.const_set(mod, Module.new { |m| m.extend(TestCleanableNamespace) }) 30 | end 31 | end 32 | end 33 | 34 | config.after do 35 | $LOAD_PATH.replace(@load_path) 36 | 37 | # We want to delete only newly loaded features within spec/, otherwise we're removing 38 | # files that may have been additionally loaded for rspec et al 39 | new_features_to_keep = ($LOADED_FEATURES - @loaded_features).tap do |feats| 40 | feats.delete_if { |path| path.include?(SPEC_ROOT.to_s) || path.include?(Dir.tmpdir) } 41 | end 42 | $LOADED_FEATURES.replace(@loaded_features + new_features_to_keep) 43 | 44 | cleanable_modules.each do |mod| 45 | next unless Object.const_defined?(mod) 46 | 47 | Object.const_get(mod).remove_constants 48 | Object.send :remove_const, mod 49 | end 50 | 51 | cleanable_constants.each do |const| 52 | Object.send :remove_const, const if Object.const_defined?(const) 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/support/tmp_directory.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "fileutils" 4 | require "pathname" 5 | require "tmpdir" 6 | 7 | module RSpec 8 | module Support 9 | module TmpDirectory 10 | private 11 | 12 | def with_tmp_directory(&) 13 | with_directory(make_tmp_directory, &) 14 | end 15 | 16 | def with_directory(dir, &) 17 | Dir.chdir(dir, &) 18 | end 19 | 20 | def make_tmp_directory 21 | Pathname(::Dir.mktmpdir).tap do |dir| 22 | (@made_tmp_dirs ||= []) << dir 23 | end 24 | end 25 | 26 | def write(path, *content) 27 | ::Pathname.new(path).dirname.mkpath 28 | 29 | File.open(path, ::File::CREAT | ::File::WRONLY) do |file| 30 | file.write(Array(content).flatten.join) 31 | end 32 | end 33 | end 34 | end 35 | end 36 | 37 | RSpec.configure do |config| 38 | config.include RSpec::Support::TmpDirectory 39 | 40 | config.after :all do 41 | if instance_variable_defined?(:@made_tmp_dirs) 42 | Array(@made_tmp_dirs).each do |dir| 43 | FileUtils.remove_entry dir 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/support/warnings.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # this file is managed by dry-rb/devtools project 4 | 5 | require "warning" 6 | 7 | Warning.ignore(%r{rspec/core}) 8 | Warning.ignore(%r{rspec/mocks}) 9 | Warning.ignore(/codacy/) 10 | Warning[:experimental] = false if Warning.respond_to?(:[]) 11 | -------------------------------------------------------------------------------- /spec/support/zeitwerk_helpers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ZeitwerkHelpers 4 | def teardown_zeitwerk 5 | ObjectSpace.each_object(Zeitwerk::Loader) do |loader| 6 | if loader.dirs.any? { |dir| dir.include?("/spec/") || dir.include?(Dir.tmpdir) } 7 | loader.unregister 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/unit/auto_registrar_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/system/auto_registrar" 4 | require "dry/system/errors" 5 | 6 | RSpec.describe Dry::System::AutoRegistrar, "#finalize!" do 7 | let(:auto_registrar) { described_class.new(container) } 8 | 9 | let(:container) { 10 | Class.new(Dry::System::Container) { 11 | configure do |config| 12 | config.root = SPEC_ROOT.join("fixtures").realpath 13 | config.component_dirs.add "components" do |dir| 14 | dir.namespaces.add "test", key: nil 15 | end 16 | end 17 | } 18 | } 19 | 20 | it "registers components in the configured component dirs" do 21 | auto_registrar.finalize! 22 | 23 | expect(container["foo"]).to be_an_instance_of(Test::Foo) 24 | expect(container["bar"]).to be_an_instance_of(Test::Bar) 25 | expect(container["bar.baz"]).to be_an_instance_of(Test::Bar::Baz) 26 | 27 | expect { container["bar.abc"] }.to raise_error( 28 | Dry::System::ComponentNotLoadableError 29 | ).with_message( 30 | <<~ERROR_MESSAGE 31 | Component 'bar.abc' is not loadable. 32 | Looking for Test::Bar::Abc. 33 | 34 | You likely need to add: 35 | 36 | acronym('ABC') 37 | 38 | to your container's inflector, since we found a Test::Bar::ABC class. 39 | ERROR_MESSAGE 40 | ) 41 | end 42 | 43 | it "doesn't re-register components previously registered via lazy loading" do 44 | expect(container["foo"]).to be_an_instance_of(Test::Foo) 45 | 46 | expect { auto_registrar.finalize! }.not_to raise_error 47 | 48 | expect(container["bar"]).to be_an_instance_of(Test::Bar) 49 | expect(container["bar.baz"]).to be_an_instance_of(Test::Bar::Baz) 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/unit/component_dir/component_for_identifier_key.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/system/component_dir" 4 | require "dry/system/config/component_dir" 5 | require "dry/system/container" 6 | 7 | RSpec.describe Dry::System::ComponentDir, "#component_for_key" do 8 | subject(:component) { component_dir.component_for_key(key) } 9 | 10 | let(:component_dir) { 11 | Dry::System::ComponentDir.new( 12 | config: Dry::System::Config::ComponentDir.new("component_dir_1") { |config| 13 | config.namespaces.add "namespace", key: nil 14 | component_dir_options.each do |key, val| 15 | config.send :"#{key}=", val 16 | end 17 | }, 18 | container: container 19 | ) 20 | } 21 | let(:component_dir_options) { {} } 22 | let(:container) { 23 | container_root = root 24 | Class.new(Dry::System::Container) { 25 | configure do |config| 26 | config.root = container_root 27 | end 28 | } 29 | } 30 | let(:root) { SPEC_ROOT.join("fixtures/unit/component").realpath } 31 | 32 | context "component file located" do 33 | let(:key) { "nested.component_file" } 34 | 35 | it "returns a component" do 36 | expect(component).to be_a Dry::System::Component 37 | end 38 | 39 | it "has a matching key" do 40 | expect(component.key).to eq key 41 | end 42 | 43 | it "has the component dir's namespace" do 44 | expect(component.namespace.path).to eq "namespace" 45 | end 46 | 47 | context "options given as component dir config" do 48 | let(:component_dir_options) { {memoize: true} } 49 | 50 | it "has the component dir's options" do 51 | expect(component.memoize?).to be true 52 | end 53 | end 54 | 55 | context "options given as magic comments in file" do 56 | let(:key) { "nested.component_file_with_auto_register_false" } 57 | 58 | it "loads options specified within the file's magic comments" do 59 | expect(component.options).to include(auto_register: false) 60 | end 61 | end 62 | 63 | context "options given as both component dir config and as magic comments in file" do 64 | let(:component_dir_options) { {auto_register: true} } 65 | let(:key) { "nested.component_file_with_auto_register_false" } 66 | 67 | it "prefers the options specified as magic comments" do 68 | expect(component.options).to include(auto_register: false) 69 | end 70 | end 71 | end 72 | 73 | context "component file not located" do 74 | let(:key) { "nested.missing_component" } 75 | 76 | it "returns nil" do 77 | expect(component).to be_nil 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /spec/unit/component_dir_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/system/component_dir" 4 | 5 | require "dry/system/container" 6 | require "dry/system/config/component_dir" 7 | 8 | RSpec.describe Dry::System::ComponentDir do 9 | subject(:component_dir) { described_class.new(config: config, container: container) } 10 | 11 | let(:config) { Dry::System::Config::ComponentDir.new(dir_path) } 12 | let(:dir_path) { "component_dir" } 13 | let(:container) { 14 | container_root = root 15 | 16 | Class.new(Dry::System::Container) { 17 | configure do |config| 18 | config.root = container_root 19 | end 20 | } 21 | } 22 | let(:root) { SPEC_ROOT.join("fixtures/unit").realpath } 23 | 24 | describe "config" do 25 | it "delegates config methods to the config" do 26 | expect(component_dir.path).to eql config.path 27 | expect(component_dir.auto_register).to eql config.auto_register 28 | expect(component_dir.add_to_load_path).to eql config.add_to_load_path 29 | end 30 | 31 | # TODO 32 | xit "provides a default root namespace if none is specified" 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/unit/component_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/system/component" 4 | require "dry/system/identifier" 5 | require "dry/system/loader" 6 | require "dry/system/config/namespace" 7 | 8 | RSpec.describe Dry::System::Component do 9 | subject(:component) { 10 | described_class.new( 11 | identifier, 12 | file_path: file_path, 13 | namespace: namespace, 14 | loader: loader 15 | ) 16 | } 17 | 18 | let(:identifier) { Dry::System::Identifier.new("test.foo") } 19 | let(:file_path) { "/path/to/test/foo.rb" } 20 | let(:namespace) { Dry::System::Config::Namespace.default_root } 21 | let(:loader) { class_spy(Dry::System::Loader) } 22 | 23 | it "is loadable" do 24 | expect(component).to be_loadable 25 | end 26 | 27 | describe "#identifier" do 28 | it "is the given identifier" do 29 | expect(component.identifier).to be identifier 30 | end 31 | end 32 | 33 | describe "#key" do 34 | it "returns the identifier's key" do 35 | expect(component.key).to eq "test.foo" 36 | end 37 | end 38 | 39 | describe "#root_key" do 40 | it "returns the identifier's root_key" do 41 | expect(component.root_key).to eq :test 42 | end 43 | end 44 | 45 | describe "#instance" do 46 | it "builds and returns an instance via the loader" do 47 | loaded_instance = double(:instance) 48 | allow(loader).to receive(:call).with(component) { loaded_instance } 49 | 50 | expect(component.instance).to eql loaded_instance 51 | end 52 | 53 | it "forwards additional arguments to the loader" do 54 | loaded_instance = double(:instance) 55 | allow(loader).to receive(:call).with(component, "extra", "args") { loaded_instance } 56 | 57 | expect(component.instance("extra", "args")).to eql loaded_instance 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /spec/unit/container/config/root_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/system/container" 4 | 5 | RSpec.describe Dry::System::Container, ".config" do 6 | subject(:config) { Test::Container.config } 7 | let(:configuration) { proc {} } 8 | 9 | before do 10 | class Test::Container < Dry::System::Container 11 | end 12 | Test::Container.configure(&configuration) 13 | end 14 | 15 | describe "#root" do 16 | subject(:root) { config.root } 17 | 18 | context "no value" do 19 | it "defaults to pwd" do 20 | expect(root).to eq Pathname.pwd 21 | end 22 | end 23 | 24 | context "string provided" do 25 | let(:configuration) { proc { |config| config.root = "/tmp" } } 26 | 27 | it "coerces string paths to pathname" do 28 | expect(root).to eq Pathname("/tmp") 29 | end 30 | end 31 | 32 | context "pathname provided" do 33 | let(:configuration) { proc { |config| config.root = Pathname("/tmp") } } 34 | 35 | it "accepts the pathname" do 36 | expect(root).to eq Pathname("/tmp") 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/unit/container/decorate_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "delegate" 4 | 5 | RSpec.describe Dry::System::Container do 6 | subject(:system) do 7 | Class.new(Dry::System::Container) 8 | end 9 | 10 | describe ".decorate" do 11 | it "decorates registered singleton object with provided decorator API" do 12 | system.register(:foo, "foo") 13 | 14 | system.decorate(:foo, with: SimpleDelegator) 15 | 16 | expect(system[:foo]).to be_instance_of(SimpleDelegator) 17 | end 18 | 19 | it "decorates registered object with provided decorator API" do 20 | system.register(:foo) { "foo" } 21 | 22 | system.decorate(:foo, with: SimpleDelegator) 23 | 24 | expect(system[:foo]).to be_instance_of(SimpleDelegator) 25 | expect(system[:foo].__getobj__).to eql("foo") 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/unit/container/hooks/after_hooks_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Dry::System::Container do 4 | subject(:system) do 5 | Class.new(described_class) 6 | end 7 | 8 | describe "after_register hook" do 9 | it "executes after a new key is registered" do 10 | expect { |hook| 11 | system.after(:register, &hook) 12 | system.register(:foo) { "bar" } 13 | }.to yield_with_args(:foo) 14 | end 15 | 16 | it "provides the fully-qualified key" do 17 | expect { |hook| 18 | system.after(:register, &hook) 19 | system.namespace :foo do 20 | register(:bar) { "baz" } 21 | end 22 | }.to yield_with_args("foo.bar") 23 | end 24 | end 25 | 26 | describe "after_finalize hook" do 27 | it "executes after finalization" do 28 | expect { |hook| 29 | system.after(:finalize, &hook) 30 | system.finalize! 31 | }.to yield_control 32 | end 33 | 34 | it "executes before the container is frozen" do 35 | is_frozen = nil 36 | 37 | system.after(:finalize) { is_frozen = frozen? } 38 | system.finalize! 39 | 40 | expect(is_frozen).to eq false 41 | expect(system).to be_frozen 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/unit/container/hooks/load_path_hook_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Dry::System::Container, "Default hooks / Load path" do 4 | let(:container) { 5 | Class.new(Dry::System::Container) { 6 | config.root = SPEC_ROOT.join("fixtures/test") 7 | } 8 | } 9 | 10 | before do 11 | @load_path_before = $LOAD_PATH 12 | end 13 | 14 | after do 15 | $LOAD_PATH.replace(@load_path_before) 16 | end 17 | 18 | context "component_dirs configured with add_to_load_path = true" do 19 | before do 20 | container.config.component_dirs.add "lib" do |dir| 21 | dir.add_to_load_path = true 22 | end 23 | end 24 | 25 | it "adds the component dirs to the load path" do 26 | expect { 27 | container.configure do 28 | end 29 | }.to change { $LOAD_PATH.include?(SPEC_ROOT.join("fixtures/test/lib").to_s) } 30 | .from(false).to(true) 31 | end 32 | end 33 | 34 | context "component_dirs configured with add_to_load_path = false" do 35 | before do 36 | container.config.component_dirs.add "lib" do |dir| 37 | dir.add_to_load_path = false 38 | end 39 | end 40 | 41 | it "does not change the load path" do 42 | expect { 43 | container.configure do 44 | end 45 | }.not_to(change { $LOAD_PATH }) 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /spec/unit/container/hooks_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Dry::System::Container do 4 | subject(:system) do 5 | Class.new(Dry::System::Container) 6 | end 7 | 8 | describe ".after" do 9 | it "registers an after hook" do 10 | system.after(:configure) do 11 | register(:test, true) 12 | end 13 | 14 | system.configure {} 15 | 16 | expect(system[:test]).to be(true) 17 | end 18 | 19 | it "inherits hooks from superclass" do 20 | system.after(:configure) do 21 | register(:test_1, true) 22 | end 23 | 24 | descendant = Class.new(system) do 25 | after(:configure) do 26 | register(:test_2, true) 27 | end 28 | end 29 | 30 | descendant.configure {} 31 | 32 | expect(descendant[:test_1]).to be(true) 33 | expect(descendant[:test_2]).to be(true) 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/unit/container/import_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/system/container" 4 | 5 | RSpec.describe Dry::System::Container, ".import" do 6 | subject(:app) { Class.new(Dry::System::Container) } 7 | 8 | let(:db) do 9 | Class.new(Dry::System::Container) do 10 | register(:users, %w[jane joe]) 11 | end 12 | end 13 | 14 | it "imports one container into another" do 15 | app.import(from: db, as: :persistence) 16 | 17 | expect(app.registered?("persistence.users")).to be(false) 18 | 19 | app.finalize! 20 | 21 | expect(app["persistence.users"]).to eql(%w[jane joe]) 22 | end 23 | 24 | context "when container has been finalized" do 25 | it "raises an error" do 26 | app.finalize! 27 | 28 | expect do 29 | app.import(from: db, as: :persistence) 30 | end.to raise_error(Dry::System::ContainerAlreadyFinalizedError) 31 | end 32 | end 33 | 34 | describe "import module" do 35 | it "loads system when it was not loaded in the imported container yet" do 36 | class Test::Other < Dry::System::Container 37 | configure do |config| 38 | config.root = SPEC_ROOT.join("fixtures/import_test").realpath 39 | config.component_dirs.add "lib" 40 | end 41 | end 42 | 43 | class Test::Container < Dry::System::Container 44 | configure do |config| 45 | config.root = SPEC_ROOT.join("fixtures/test").realpath 46 | config.component_dirs.add "lib" 47 | end 48 | 49 | import from: Test::Other, as: :other 50 | end 51 | 52 | module Test 53 | Import = Container.injector 54 | end 55 | 56 | class Test::Foo 57 | include Test::Import["other.test.bar"] 58 | end 59 | 60 | expect(Test::Foo.new.bar).to be_instance_of(Test::Bar) 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /spec/unit/container/injector_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/system/container" 4 | 5 | RSpec.describe Dry::System::Container, ".injector" do 6 | context "default injector" do 7 | it "works correct" do 8 | Test::Foo = Class.new 9 | 10 | Test::Container = Class.new(Dry::System::Container) do 11 | register "foo", Test::Foo.new 12 | end 13 | 14 | Test::Inject = Test::Container.injector 15 | 16 | injected_class = Class.new do 17 | include Test::Inject["foo"] 18 | end 19 | 20 | obj = injected_class.new 21 | expect(obj.foo).to be_a Test::Foo 22 | 23 | another = Object.new 24 | obj = injected_class.new(foo: another) 25 | expect(obj.foo).to eq another 26 | end 27 | end 28 | 29 | context "injector_options provided" do 30 | it "builds an injector with the provided options" do 31 | Test::Foo = Class.new 32 | 33 | Test::Container = Class.new(Dry::System::Container) do 34 | register "foo", Test::Foo.new 35 | end 36 | 37 | Test::Inject = Test::Container.injector(strategies: { 38 | default: Dry::AutoInject::Strategies::Args, 39 | australian: Dry::AutoInject::Strategies::Args 40 | }) 41 | 42 | injected_class = Class.new do 43 | include Test::Inject.australian["foo"] 44 | end 45 | 46 | obj = injected_class.new 47 | expect(obj.foo).to be_a Test::Foo 48 | 49 | another = Object.new 50 | obj = injected_class.new(another) 51 | expect(obj.foo).to eq another 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /spec/unit/container/load_path_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/system/container" 4 | 5 | RSpec.describe Dry::System::Container, "Load path handling" do 6 | let(:container) { 7 | class Test::Container < Dry::System::Container 8 | config.root = SPEC_ROOT.join("fixtures/test") 9 | config.component_dirs.add "lib" 10 | end 11 | 12 | Test::Container 13 | } 14 | 15 | before do 16 | @load_path_before = $LOAD_PATH 17 | end 18 | 19 | after do 20 | $LOAD_PATH.replace(@load_path_before) 21 | end 22 | 23 | describe ".add_to_load_path!" do 24 | it "adds the given directories, relative to the container's root, to the beginning of the $LOAD_PATH" do 25 | expect { container.add_to_load_path!("lib", "system") } 26 | .to change { $LOAD_PATH.include?(SPEC_ROOT.join("fixtures/test/lib").to_s) } 27 | .from(false).to(true) 28 | .and change { $LOAD_PATH.include?(SPEC_ROOT.join("fixtures/test/system").to_s) } 29 | .from(false).to(true) 30 | 31 | expect($LOAD_PATH[0..1]).to eq [ 32 | SPEC_ROOT.join("fixtures/test/lib").to_s, 33 | SPEC_ROOT.join("fixtures/test/system").to_s 34 | ] 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/unit/container/monitor_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Dry::System::Container do 4 | subject(:system) do 5 | Class.new(Dry::System::Container) do 6 | use :monitoring 7 | end 8 | end 9 | 10 | describe ".monitor" do 11 | let(:klass) do 12 | Class.new do 13 | def self.name 14 | "Test::Class_#{__id__}" 15 | end 16 | 17 | def say(word, &block) 18 | block&.call 19 | word 20 | end 21 | 22 | def other; end 23 | end 24 | end 25 | 26 | let(:object) do 27 | klass.new 28 | end 29 | 30 | before do 31 | system.configure {} 32 | system.register(:object, klass.new) 33 | end 34 | 35 | it "monitors object public method calls" do 36 | captured = [] 37 | 38 | system.monitor(:object) do |event| 39 | captured << [event.id, event[:target], event[:method], event[:args]] 40 | end 41 | 42 | object = system[:object] 43 | block_result = [] 44 | block = proc { block_result << true } 45 | 46 | result = object.say("hi", &block) 47 | 48 | expect(block_result).to eql([true]) 49 | expect(result).to eql("hi") 50 | 51 | expect(captured).to eql([[:monitoring, :object, :say, ["hi"]]]) 52 | end 53 | 54 | it "monitors specified object method calls" do 55 | captured = [] 56 | 57 | system.monitor(:object, methods: [:say]) do |event| 58 | captured << [event.id, event[:target], event[:method], event[:args]] 59 | end 60 | 61 | object = system[:object] 62 | 63 | object.say("hi") 64 | object.other 65 | 66 | expect(captured).to eql([[:monitoring, :object, :say, ["hi"]]]) 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /spec/unit/container/notifications_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Dry::System::Container do 4 | subject(:system) do 5 | Class.new(Dry::System::Container) do 6 | use :notifications 7 | end 8 | end 9 | 10 | describe ".notifications" do 11 | it "returns configured notifications" do 12 | system.configure {} 13 | 14 | expect(system[:notifications]).to be_instance_of(Dry::Monitor::Notifications) 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/unit/errors_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/system/errors" 4 | 5 | module Dry 6 | module System 7 | RSpec.describe "Errors" do 8 | describe ComponentNotLoadableError do 9 | let(:component) { instance_double(Dry::System::Component, key: key) } 10 | let(:error) { instance_double(NameError, name: "Foo", receiver: "Test") } 11 | subject { described_class.new(component, error, corrections: corrections) } 12 | 13 | describe "without corrections" do 14 | let(:corrections) { [] } 15 | let(:key) { "test.foo" } 16 | 17 | it do 18 | expect(subject.message).to eq( 19 | "Component 'test.foo' is not loadable.\n" \ 20 | "Looking for Test::Foo." 21 | ) 22 | end 23 | end 24 | 25 | describe "with corrections" do 26 | describe "acronym" do 27 | describe "single class name correction" do 28 | let(:corrections) { ["Test::FOO"] } 29 | let(:key) { "test.foo" } 30 | 31 | it do 32 | expect(subject.message).to eq( 33 | <<~ERROR_MESSAGE 34 | Component 'test.foo' is not loadable. 35 | Looking for Test::Foo. 36 | 37 | You likely need to add: 38 | 39 | acronym('FOO') 40 | 41 | to your container's inflector, since we found a Test::FOO class. 42 | ERROR_MESSAGE 43 | ) 44 | end 45 | end 46 | 47 | describe "module and class name correction" do 48 | let(:error) { instance_double(NameError, name: "Foo", receiver: "Test::Api") } 49 | let(:corrections) { ["Test::API::FOO"] } 50 | let(:key) { "test.api.foo" } 51 | 52 | it do 53 | expect(subject.message).to eq( 54 | <<~ERROR_MESSAGE 55 | Component 'test.api.foo' is not loadable. 56 | Looking for Test::Api::Foo. 57 | 58 | You likely need to add: 59 | 60 | acronym('API', 'FOO') 61 | 62 | to your container's inflector, since we found a Test::API::FOO class. 63 | ERROR_MESSAGE 64 | ) 65 | end 66 | end 67 | end 68 | 69 | describe "typo" do 70 | let(:corrections) { ["Test::Fon", "Test::Flo"] } 71 | let(:key) { "test.foo" } 72 | 73 | it do 74 | expect(subject.message).to eq( 75 | <<~ERROR_MESSAGE.chomp 76 | Component 'test.foo' is not loadable. 77 | Looking for Test::Foo. 78 | 79 | Did you mean? Test::Fon 80 | Test::Flo 81 | ERROR_MESSAGE 82 | ) 83 | end 84 | end 85 | end 86 | end 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /spec/unit/indirect_component_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/system/identifier" 4 | require "dry/system/indirect_component" 5 | 6 | RSpec.describe Dry::System::IndirectComponent do 7 | subject(:component) { described_class.new(identifier) } 8 | let(:identifier) { Dry::System::Identifier.new("test.foo") } 9 | 10 | it "is not loadable" do 11 | expect(component).not_to be_loadable 12 | end 13 | 14 | describe "#identifier" do 15 | it "is the given identifier" do 16 | expect(component.identifier).to be identifier 17 | end 18 | end 19 | 20 | describe "#key" do 21 | it "returns the identifier's key" do 22 | expect(component.key).to eq "test.foo" 23 | end 24 | end 25 | 26 | describe "#root_key" do 27 | it "returns the identifier's root key" do 28 | expect(component.root_key).to eq :test 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/unit/loader/autoloading_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/system/loader/autoloading" 4 | require "dry/system/component" 5 | require "dry/system/config/namespace" 6 | require "dry/system/identifier" 7 | 8 | RSpec.describe Dry::System::Loader::Autoloading do 9 | describe "#require!" do 10 | subject(:loader) { described_class } 11 | let(:component) { 12 | Dry::System::Component.new( 13 | Dry::System::Identifier.new("test.not_loaded_const"), 14 | file_path: "/path/to/test/not_loaded_const.rb", 15 | namespace: Dry::System::Config::Namespace.default_root 16 | ) 17 | } 18 | 19 | before do 20 | allow(loader).to receive(:require) 21 | allow(Test).to receive(:const_missing) 22 | end 23 | 24 | it "loads the constant " do 25 | loader.require!(component) 26 | expect(loader).not_to have_received(:require) 27 | expect(Test).to have_received(:const_missing).with :NotLoadedConst 28 | end 29 | 30 | it "returns self" do 31 | expect(loader.require!(component)).to eql loader 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/unit/magic_comments_parser_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/system/magic_comments_parser" 4 | 5 | RSpec.describe Dry::System::MagicCommentsParser, ".call" do 6 | let(:file_name) { SPEC_ROOT.join("fixtures/magic_comments/comments.rb") } 7 | let(:comments) { described_class.(file_name) } 8 | 9 | it "makes comment names available as symbols" do 10 | expect(comments.key?(:valid_comment)).to eql true 11 | end 12 | 13 | it "finds magic comments after other commented lines or blank lines" do 14 | expect(comments[:valid_comment]).to eq "hello" 15 | end 16 | 17 | it "does not match comments with invalid names" do 18 | expect(comments.values).not_to include "value for comment using dashes" 19 | end 20 | 21 | it "supports comment names with alpha-numeric characters and underscores (numbers not allowed for first character)" do 22 | expect(comments[:comment_123]).to eq "alpha-numeric and underscores allowed" 23 | expect(comments.keys).not_to include(:"123_will_not_match") 24 | end 25 | 26 | it "only matches comments at the start of the line" do 27 | expect(comments.key?(:not_at_start_of_line)).to eql false 28 | end 29 | 30 | it "does not match any comments after any lines of code" do 31 | expect(comments.key?(:after_code)).to eql false 32 | end 33 | 34 | describe "coercions" do 35 | it 'coerces "true" to true' do 36 | expect(comments[:true_comment]).to eq true 37 | end 38 | 39 | it 'coerces "false" to false' do 40 | expect(comments[:false_comment]).to eq false 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/unit/provider/source_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Dry::System::Provider::Source do 4 | let(:target_container) do 5 | Dry::Core::Container.new 6 | end 7 | 8 | let(:provider_container) do 9 | Dry::Core::Container.new 10 | end 11 | 12 | shared_examples_for "a provider class" do 13 | let(:provider) do 14 | provider_class.new( 15 | provider_container: provider_container, target_container: target_container 16 | ) 17 | end 18 | 19 | it "exposes start callback" do 20 | expect(provider.provider_container.key?("persistence")).to be(false) 21 | 22 | provider.start 23 | 24 | expect(provider.provider_container.key?("persistence")).to be(true) 25 | end 26 | end 27 | 28 | context "using a base class" do 29 | it_behaves_like "a provider class" do 30 | let(:provider_class) do 31 | described_class.for(name: "Persistence") do 32 | start do 33 | register(:persistence, {}) 34 | end 35 | end 36 | end 37 | end 38 | end 39 | 40 | context "using a sub-class" do 41 | it_behaves_like "a provider class" do 42 | let(:parent_class) do 43 | described_class.for(name: "Persistence") do 44 | start do 45 | register(:persistence, {}) 46 | end 47 | end 48 | end 49 | 50 | let(:provider_class) do 51 | Class.new(parent_class) 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/unit/provider_sources/settings/loader_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/system/provider_sources/settings/loader" 4 | 5 | RSpec.describe Dry::System::ProviderSources::Settings::Loader do 6 | subject(:loader) { described_class.new(root: root, env: env) } 7 | let(:root) { "/system/root" } 8 | subject(:env) { :development } 9 | 10 | before do 11 | allow_any_instance_of(described_class).to receive(:require).with("dotenv") 12 | end 13 | 14 | describe "#initialize" do 15 | context "dotenv available" do 16 | let(:dotenv) { spy(:dotenv) } 17 | 18 | before do 19 | stub_const "Dotenv", dotenv 20 | end 21 | 22 | context "non-test environment" do 23 | let(:env) { :development } 24 | 25 | it "requires dotenv and loads a range of .env files" do 26 | loader 27 | 28 | expect(loader).to have_received(:require).with("dotenv").ordered 29 | expect(dotenv).to have_received(:load).ordered.with( 30 | "/system/root/.env.development.local", 31 | "/system/root/.env.local", 32 | "/system/root/.env.development", 33 | "/system/root/.env" 34 | ) 35 | end 36 | end 37 | 38 | context "test environment" do 39 | let(:env) { :test } 40 | 41 | it "loads a range of .env files, not including .env.local" do 42 | loader 43 | 44 | expect(dotenv).to have_received(:load).ordered.with( 45 | "/system/root/.env.test.local", 46 | "/system/root/.env.test", 47 | "/system/root/.env" 48 | ) 49 | end 50 | end 51 | end 52 | 53 | context "dotenv unavailable" do 54 | it "attempts to require dotenv" do 55 | loader 56 | expect(loader).to have_received(:require).with("dotenv") 57 | end 58 | 59 | it "does not raise any error" do 60 | expect { loader }.not_to raise_error 61 | end 62 | end 63 | end 64 | 65 | describe "#[]" do 66 | it "returns a values from ENV" do 67 | allow(ENV).to receive(:[]).and_call_original 68 | allow(ENV).to receive(:[]).with("SOME_KEY").and_return "some value" 69 | 70 | expect(loader["SOME_KEY"]).to eq "some value" 71 | end 72 | end 73 | end 74 | --------------------------------------------------------------------------------