Repository: dry-rb/dry-system Branch: main Commit: 37df3d71bebc Files: 262 Total size: 424.6 KB Directory structure: gitextract_5feq0jid/ ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug-report.md │ │ └── config.yml │ ├── SUPPORT.md │ └── workflows/ │ ├── ci-lint.yml │ ├── ci.yml │ ├── pr-comments.yml │ ├── repo-sync-preview.yml │ └── rubocop.yml ├── .gitignore ├── .rspec ├── .yardopts ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Gemfile ├── Gemfile.devtools ├── LICENSE ├── README.md ├── Rakefile ├── bin/ │ ├── .gitkeep │ └── console ├── docsite/ │ └── source/ │ ├── component-dirs.html.md │ ├── container/ │ │ └── hooks.html.md │ ├── container.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/ │ │ │ ├── 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/ │ │ │ │ └── autoloading.rb │ │ │ ├── loader.rb │ │ │ ├── magic_comments_parser.rb │ │ │ ├── manifest_registrar.rb │ │ │ ├── plugins/ │ │ │ │ ├── bootsnap.rb │ │ │ │ ├── dependency_graph/ │ │ │ │ │ └── strategies.rb │ │ │ │ ├── dependency_graph.rb │ │ │ │ ├── env.rb │ │ │ │ ├── logging.rb │ │ │ │ ├── monitoring/ │ │ │ │ │ └── proxy.rb │ │ │ │ ├── monitoring.rb │ │ │ │ ├── notifications.rb │ │ │ │ ├── plugin.rb │ │ │ │ ├── zeitwerk/ │ │ │ │ │ └── compat_inflector.rb │ │ │ │ └── zeitwerk.rb │ │ │ ├── plugins.rb │ │ │ ├── provider/ │ │ │ │ ├── source.rb │ │ │ │ └── source_dsl.rb │ │ │ ├── provider.rb │ │ │ ├── provider_registrar.rb │ │ │ ├── provider_source_registry.rb │ │ │ ├── provider_sources/ │ │ │ │ ├── settings/ │ │ │ │ │ ├── config.rb │ │ │ │ │ └── loader.rb │ │ │ │ └── settings.rb │ │ │ ├── provider_sources.rb │ │ │ ├── stubs.rb │ │ │ └── version.rb │ │ └── system.rb │ └── dry-system.rb ├── repo-sync.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/ │ │ │ │ ├── abc.rb │ │ │ │ └── baz.rb │ │ │ ├── bar.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/ │ │ │ │ │ ├── book.rb │ │ │ │ │ └── user.rb │ │ │ │ └── models.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/ │ │ │ │ ├── book.rb │ │ │ │ └── user.rb │ │ │ └── models.rb │ │ ├── require_path/ │ │ │ └── lib/ │ │ │ └── test/ │ │ │ └── foo.rb │ │ ├── settings_test/ │ │ │ └── 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/ │ │ │ │ │ ├── book.rb │ │ │ │ │ └── user.rb │ │ │ │ ├── models.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.rb │ │ ├── tmp_directory.rb │ │ ├── warnings.rb │ │ └── zeitwerk_loader_registry.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 └── zizmor.yml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ github: hanami ================================================ FILE: .github/ISSUE_TEMPLATE/bug-report.md ================================================ --- name: "\U0001F41B Bug report" about: See CONTRIBUTING.md for more information title: '' labels: bug, help wanted assignees: '' --- ## Describe the bug A clear and concise description of what the bug is. ## To Reproduce Provide detailed steps to reproduce, **an executable script would be best**. ## Expected behavior A clear and concise description of what you expected to happen. ## My environment - Ruby version: ... - OS: ... ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: Community support url: https://discourse.hanamirb.org about: Please ask and answer questions here. ================================================ FILE: .github/SUPPORT.md ================================================ ## Support If you need help with any of the Hanami, Dry or Rom libraries, feel free to ask questions on our [discussion forum](https://discourse.hanamirb.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: ================================================ FILE: .github/workflows/ci-lint.yml ================================================ name: CI lint on: push: branches: ["main", "release-*", "ci/*"] tags: ["v*"] paths: - ".github/**" pull_request: branches: ["main", "release-*"] paths: - ".github/**" schedule: - cron: "0 0 * * 0" # every Sunday at midnight permissions: {} jobs: zizmor: name: Run zizmor runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: persist-credentials: false - name: Run zizmor uses: zizmorcore/zizmor-action@0dce2577a4760a2749d8cfb7a84b7d5585ebcb7d with: advanced-security: false ================================================ FILE: .github/workflows/ci.yml ================================================ # This file is synced from hanakai-rb/repo-sync name: CI run-name: ${{ github.ref_type == 'tag' && format('Release {0}', github.ref_name) || 'CI' }} on: push: branches: ["main", "release-*", "ci/*"] tags: ["v*"] pull_request: branches: ["main", "release-*"] schedule: - cron: "30 4 * * *" permissions: contents: read jobs: tests: name: Tests (Ruby ${{ matrix.ruby }}) runs-on: ubuntu-latest continue-on-error: ${{ matrix.optional || false }} strategy: fail-fast: false matrix: ruby: - "4.0" - "3.4" - "3.3" - "3.2" - "3.3.0" - "jruby" include: - ruby: "4.0" coverage: "true" env: COVERAGE: ${{ matrix.coverage }} steps: - name: Checkout uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 with: persist-credentials: false - name: Install package dependencies run: "[ -e $APT_DEPS ] || sudo apt-get install -y --no-install-recommends $APT_DEPS" - name: Set up Ruby uses: ruby/setup-ruby@e65c17d16e57e481586a6a5a0282698790062f92 # zizmor: ignore[cache-poisoning] with: ruby-version: ${{ matrix.ruby }} bundler-cache: true - name: Run all tests id: test run: | status=0 bundle exec rake || status=$? if [ ${status} -ne 0 ] && [ "${{ matrix.optional }}" == "true" ]; then echo "::warning::Optional matrix job failed." echo "optional_fail=true" >> "${GITHUB_OUTPUT}" echo "optional_fail_status=${status}" >> "${GITHUB_OUTPUT}" exit 0 # Ignore error here to keep the green checkmark fi exit ${status} - name: Create optional failure comment if: ${{ matrix.optional && github.event.pull_request }} uses: hanakai-rb/repo-sync/pr-comment-artifact@main with: name: ci-ruby-${{ matrix.ruby }} pr-number: ${{ github.event.pull_request.number }} comment-tag: ruby-${{ matrix.ruby }}-optional-failure message: "ℹ️ Optional job failed: Ruby ${{ matrix.ruby }}" mode: ${{ steps.test.outputs.optional_fail == 'true' && 'upsert' || 'delete' }} workflow-keepalive: if: github.event_name == 'schedule' runs-on: ubuntu-latest permissions: actions: write steps: - uses: liskin/gh-workflow-keepalive@7a9194bad497f0b993708eeaf10fc0a2d726eb71 release: runs-on: ubuntu-latest if: github.ref_type == 'tag' needs: tests steps: - name: Trigger release workflow uses: actions/github-script@450193c5abd4cdb17ba9f3ffcfe8f635c4bb6c2a with: github-token: ${{ secrets.RELEASE_MACHINE_DISPATCH_TOKEN }} script: | const tag = context.ref.replace("refs/tags/", ""); const repo = context.repo.owner + "/" + context.repo.repo; const tagMessage = await github.rest.git.getTag({ owner: context.repo.owner, repo: context.repo.repo, tag_sha: context.sha }).then(res => res.data.message).catch(() => ""); const announce = /(skip-announce|no-announce)/i.test(tagMessage) ? "false" : "true"; await github.rest.actions.createWorkflowDispatch({ owner: "hanakai-rb", repo: "release-machine", workflow_id: "release.yml", ref: "main", inputs: { repo, tag, announce } }); const workflowUrl = "https://github.com/hanakai-rb/release-machine/actions/workflows/release.yml"; await core.summary .addHeading("Release Triggered") .addRaw(`Triggered release workflow for ${tag}`) .addLink("View release workflow", workflowUrl) .write(); ================================================ FILE: .github/workflows/pr-comments.yml ================================================ # This file is synced from hanakai-rb/repo-sync # Downloads comment artifacts from completed workflows and posts them to PRs. This allows source # workflows to run with read-only permissions on fork PRs while still posting comments via this # privileged workflow that runs in the base repo context. # # Comment artifacts should be generated using the `hanakai-rb/repo-sync/pr-comment-artifact@main` # action. name: PR comments on: # zizmor: ignore[dangerous-triggers] workflow_run: workflows: ["CI"] types: - completed permissions: pull-requests: write jobs: post-comments: runs-on: ubuntu-latest permissions: pull-requests: write if: github.event.workflow_run.event == 'pull_request' steps: - name: Post comments uses: hanakai-rb/repo-sync/pr-comments-from-artifacts@main with: workflow-run-id: ${{ github.event.workflow_run.id }} ================================================ FILE: .github/workflows/repo-sync-preview.yml ================================================ name: Repo-sync preview on: # zizmor: ignore[dangerous-triggers] workflow_run: workflows: ["CI", "RuboCop", "CI lint"] types: [completed] branches: - "ci/repo-sync-preview-*" jobs: report: runs-on: ubuntu-latest permissions: {} if: > github.event.workflow_run.event == 'push' && github.event.workflow_run.head_repository.fork == false steps: - name: Dispatch status to repo-sync uses: actions/github-script@450193c5abd4cdb17ba9f3ffcfe8f635c4bb6c2a with: github-token: ${{ secrets.REPO_SYNC_DISPATCH_TOKEN }} script: | const { BRANCH, REPO, WORKFLOW, STATUS, RUN_URL } = process.env; await github.rest.actions.createWorkflowDispatch({ owner: "hanakai-rb", repo: "repo-sync", workflow_id: "aggregate-preview-status.yml", ref: "main", inputs: { pr_number: BRANCH.replace("ci/repo-sync-preview-", ""), repo_name: REPO, workflow_name: WORKFLOW, status: STATUS, run_url: RUN_URL } }); env: BRANCH: ${{ github.event.workflow_run.head_branch }} REPO: ${{ github.repository }} WORKFLOW: ${{ github.event.workflow_run.name }} STATUS: ${{ github.event.workflow_run.conclusion }} RUN_URL: ${{ github.event.workflow_run.html_url }} ================================================ FILE: .github/workflows/rubocop.yml ================================================ # frozen_string_literal: true # This file is synced from hanakai-rb/repo-sync name: RuboCop on: push: branches: ["main", "release-*", "ci/*"] tags: ["v*"] pull_request: branches: ["main", "release-*"] permissions: contents: read jobs: build: runs-on: ubuntu-latest env: BUNDLE_ONLY: tools steps: - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 with: persist-credentials: false - name: Set up Ruby 4.0 uses: ruby/setup-ruby@e65c17d16e57e481586a6a5a0282698790062f92 # zizmor: ignore[cache-poisoning] with: ruby-version: 4.0 bundler-cache: true - name: Run RuboCop run: bundle exec rubocop --parallel ================================================ FILE: .gitignore ================================================ *.gem *.rbc *.log /.rubocop* /.config /coverage/ /InstalledFiles /pkg/ /spec/reports/ /test/tmp/ /test/version_tmp/ /tmp/ ## Specific to RubyMotion: .dat* .repl_history build/ ## Documentation cache and generated files: /.yardoc/ /_yardoc/ /doc/ /rdoc/ ## Environment normalisation: /.bundle/ /vendor/bundle /lib/bundler/man/ # for a library or gem, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: # Gemfile.lock # .ruby-version # .ruby-gemset # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: .rvmrc Gemfile.lock ## Specific to RubyMine .idea ## Tests /spec/examples.txt /spec/fixtures/test/tmp/cache ================================================ FILE: .rspec ================================================ --color --require spec_helper --order random --warnings ================================================ FILE: .yardopts ================================================ --no-private --hide-void-return --markup-provider=redcarpet --markup=markdown ================================================ FILE: CHANGELOG.md ================================================ # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Break Versioning](https://www.taoensso.com/break-versioning). ## [Unreleased] [Unreleased]: https://github.com/dry-rb/dry-system/compare/v1.2.5...main ## [1.2.5] - 2025-12-01 ### Fixed - Pass through keyword arguments to monitored objects in `monitoring` plugin. (@yuszuv in #290) [1.2.5]: https://github.com/dry-rb/dry-system/compare/v1.2.4...v1.2.5 ## [1.2.4] - 2025-08-14 ### Fixed - Allow imported components to be lazy loaded when both strings and symbols are given as the namespace to `Container.import` (@timriley in #287) [Compare v1.2.3...v1.2.4](https://github.com/dry-rb/dry-system/compare/v1.2.3...v1.2.4) ## [1.2.3] - 2025-07-29 ### Added - Add :register after-hook to detect container key registration dynamically. (via #274, @alassek) ### Fixed - Re-register components from manifest registrars in apps that reload the container (e.g. when using dry-rails and Rails development mode) (via #286, @alassek) ### Changed - :finalize after-hook now executes before container freeze to allow mutation. (via #274, @alassek) [Compare v1.2.2...v1.2.3](https://github.com/dry-rb/dry-system/compare/v1.2.2...v1.2.3) ## [1.2.2] - 2025-01-31 ### Fixed - Syntax errors on 3.3.0 (@flash-gordon, see #284) [Compare v1.2.1...v1.2.2](https://github.com/dry-rb/dry-system/compare/v1.2.1...v1.2.2) ## [1.2.1] - 2025-01-08 ### Fixed - `eager_load` was removed from `finalize!`. It was introduced with `true` by default that wasn't the intention #281 (via #282) (@flash-gordon) [Compare v1.2.0...v1.2.1](https://github.com/dry-rb/dry-system/compare/v1.2.0...v1.2.1) ## [1.2.0] - 2025-01-07 ### Added - Option to skip eager loading during finalize with `eager_load: false` (via #276) (@cllns) ### Changed - Update required Ruby version to 3.1 (@flash-gordon) [Compare v1.1.1...v1.2.0](https://github.com/dry-rb/dry-system/compare/v1.1.1...v1.2.0) ## [1.1.1] - 2024-11-03 ### Fixed - Restore `ProviderRegistrar#find_and_load_provider` as an alias of `#[]` [Compare v1.1.0...v1.1.1](https://github.com/dry-rb/dry-system/compare/v1.1.0...v1.1.1) ## [1.1.0] - 2024-10-31 [Compare v1.1.0.beta2...v1.1.0](https://github.com/dry-rb/dry-system/compare/v1.1.0.beta2...v1.1.0) ## [1.1.0.beta2] - 2024-09-25 ### Changed - Allow provider sources to use a custom superclass. This requires a custom provider registrar to be configured, with its own implementations of `#provider_source_class` (the superclass to use) and `#provider_source_options` (custom initialization args to pass to the provider source). (via #275) (@alassek, @timriley) [Compare v1.1.0.beta1...v1.1.0.beta2](https://github.com/dry-rb/dry-system/compare/v1.1.0.beta1...v1.1.0.beta2) ## [1.1.0.beta1] - 2024-07-03 ### Added - Add `Dry::System::ProviderRegistrar#target_container`, to be passed when initializing providers. By default this is an alias of `#container`. This allows for custom provider registrars to override `#target_container` to provide a custom `#target` within providers. An overridden value **MUST** still wrap the original `#target_container` to ensure components are registered in the right place. (via #270) (@timriley) ### Changed - Make `Dry::System::ProviderRegistrar` public API (via #270) (@timriley) - When registering a provider source, you can now provide a `provider_options:` hash of default options for providers to be registered using that source. The one provider option currently supported is `namespace:`. (via #271) (@timriley) - Load providers when accessing them via `Dry::System::ProviderRegistrar#[]`. The previous, behavior of `#[]` returning `nil` if a provider had not been explicitly loaded was a potential source of confusion. Now `#[]` can serve as the one and only interface for fetching a provider. (via #273) (@timriley) [Compare v1.0.1...v1.1.0.beta1](https://github.com/dry-rb/dry-system/compare/v1.0.1...v1.1.0.beta1) ## [1.0.1] - 2022-11-18 ### Changed - Bumped dry-auto_inject dependency to its 1.0.0 final release (@solnic) [Compare v1.0.0...v1.0.1](https://github.com/dry-rb/dry-system/compare/v1.0.0...v1.0.1) ## [1.0.0] - 2022-11-18 ### Fixed - Only use DidYouMean-integrated Error for Component loading failure (via #261) (@cllns + @solnic) ### Changed - This version uses dry-core 1.0 and dry-configurable 1.0 (@solnic + @flash-gordon) - Raise error on import after finalize (via #254) (@timriley + @tak1n) - Validate settings even if loader does not set value (via #246) (@oeoeaio) - Remove all deprecated functionality and deprecation messages (via #255) (@timriley) - Use main dry/monitor entrypoint for autoloading (via #257) (@timriley) - Use dry-configurable 1.0 (via 43c79095ccf54c6251e825ae20c97a9415e78209) (@flash-gordon) - Use dry-core 1.0 (via 3d0cf95aef120601e67f3e8fbbf16d004017d376) (@flash-gordon) - Remove dry-container dependency and update to use `Dry::Core::Container` (via 2b76554e5925fc92614627d5c1e0a9177cecf12f) (@solnic) [Compare v0.27.2...v1.0.0](https://github.com/dry-rb/dry-system/compare/v0.27.2...v1.0.0) ## [0.27.2] - 2022-10-17 ### Fixed - Removed remaining manual require left-overs (@solnic) [Compare v0.27.1...v0.27.2](https://github.com/dry-rb/dry-system/compare/v0.27.1...v0.27.2) ## [0.27.1] - 2022-10-15 ### Fixed - Tweak for zeitwerk loader (@flash-gordon) [Compare v0.27.0...v0.27.1](https://github.com/dry-rb/dry-system/compare/v0.27.0...v0.27.1) ## [0.27.0] - 2022-10-15 ### Changed - [BREAKING] Use zeitwerk for auto-loading dry-system (@flash-gordon + @solnic) From now on you need to do `require "dry/system"` as it sets up its Zeitwerk loader and from there, everything else will be auto-loaded. [Compare v0.26.0...v0.27.0](https://github.com/dry-rb/dry-system/compare/v0.26.0...v0.27.0) ## [0.26.0] - 2022-10-08 ### Changed - Update dry-configurable dependency to 0.16.0 and make internal adjustments to suit (@timriley in #249) - Remove now-unused concurrent-ruby gem dependency (@timriley in #250) [Compare v0.25.0...v0.26.0](https://github.com/dry-rb/dry-system/compare/v0.25.0...v0.26.0) ## [0.25.0] - 2022-07-10 ### Fixed - Fix incorrect type in `ManifestRegistrar#finalize!` (@alassek) ### Changed - Import root components via `nil` import namespace (via #236) (@timriley) - Allow deeper `Provider::Source` hierarchies (via #240) (@timriley + @solnic) - Prefer local components when importing (via #241) (@timriley + @solnic) [Compare v0.24.0...v0.25.0](https://github.com/dry-rb/dry-system/compare/v0.24.0...v0.25.0) ## [0.24.0] - ### Changed - dry-struct depedency was removed (@flash-gordon) [Compare v0.23.0...master](https://github.com/dry-rb/dry-system/compare/v0.23.0...master) ## [0.23.0] - 2022-02-08 This is a major overhaul of bootable components (now known as “Providers”), and brings major advancements to other areas, including container imports and exports. Deprecations are in place for otherwise breaking changes to commonly used parts of dry-system, though some breaking changes remain. This prepares the way for dry-system 1.0, which will be released in the coming months. ### Added - Containers can configure specific components for export using `config.exports` (@timriley in #209). ```ruby class MyContainer < Dry::System::Container configure do |config| config.exports = %w[component_a component_b] end end ``` Containers importing another container with configured exports will import only those components. When importing a specific set of components (see the note in the “Changed” section below), only those components whose keys intersect with the configured exports will be imported. - A `:zeitwerk` plugin, to set up [Zeitwerk](https://github.com/fxn/zeitwerk) and integrate it with your container configuration (@ianks and @timriley in #197, #222, 13f8c87, #223) This makes it possible to enable Zeitwerk with a one-liner: ```ruby class MyContainer < Dry::System::Container use :zeitwerk configure do |config| config.component_dirs.add "lib" # ... end end ``` The plugin makes a `Zeitwerk::Loader` instance available at `config.autoloader`, and then in an after-`:configure` hook, the plugin will set up the loader to work with all of your configured component dirs and their namespaces. It will also enable the `Dry::System::Loader::Autoloading` loader for all component dirs, plus disable those dirs from being added to the `$LOAD_PATH`. The plugin accepts the following options: - `loader:` - (optional) to use a pre-initialized loader, if required. - `run_setup:` - (optional) a bool to determine whether to run `Zeitwerk::Loader#setup` as part of the after-`:configure` hook. This may be useful to disable in advanced cases when integrating with an externally managed loader. - `eager_load:` - (optional) a bool to determine whether to run `Zeitwerk::Loader#eager_load` as part of an after-`:finalize` hook. When not provided, it will default to true if the `:env` plugin is enabled and the env is set to `:production`. - `debug:` - (optional) a bool to set whether Zeitwerk should log to `$stdout`. - New `Identifier#end_with?` and `Identifier#include?` predicates (@timriley in #219) These are key segment-aware predicates that can be useful when checking components as part of container configuration. ```ruby identifier.key # => "articles.operations.create" identifier.end_with?("create") # => true identifier.end_with?("operations.create") # => true identifier.end_with?("ate") # => false, not a whole segment identifier.end_with?("nope") # => false, not part of the key at all identifier.include?("operations") # => true identifier.include?("articles.operations") # => true identifier.include?("operations.create") # => true identifier.include?("article") # false, not a whole segment identifier.include?("update") # => false, not part of the key at all ``` - An `instance` setting for component dirs allows simpler per-dir control over component instantiation (@timriley in #215) This optional setting should be provided a proc that receives a single `Dry::System::Component` instance as an argument, and should return the instance for the given component. ```ruby configure do |config| config.component_dirs.add "lib" do |dir| dir.instance = proc do |component| if component.identifier.include?("workers") # Register classes for jobs component.loader.constant(component) else # Otherwise register regular instances per default loader component.loader.call(component) end end end end ``` For complete control of component loading, you should continue to configure the component dir’s `loader` instead. - A new `ComponentNotLoadableError` error and helpful message is raised when resolving a component and an unexpected class is defined in the component’s source file (@cllns in #217). The error shows expected and found class names, and inflector configuration that may be required in the case of class names containing acronyms. ### Fixed - Registrations made in providers (by calling `register` inside a provider step) have all their registration options preserved (such as a block-based registration, or the `memoize:` option) when having their registration merged into the target container after the provider lifecycle steps complete (@timriley in #212). - Providers can no longer implicitly re-start themselves while in the process of starting and cause an infinite loop (@timriley #213). This was possible before when a provider resolved a component from the target container that auto-injected dependencies with container keys sharing the same base key as the provider name. ### Changed - “Bootable components” (also referred to in some places simply as “components”) have been renamed to “Providers” (@timriley in #200). Register a provider with `Dry::System::Container.register_provider` (`Dry::System::Container.boot` has been deprecated): ```ruby MyContainer.register_provider(:mailer) do # ... end ``` - Provider `init` lifecycle step has been deprecated and renamed to `prepare` (@timriley in #200). ```ruby MyContainer.reigster_provider(:mailer) do # Rename `init` to `prepare` prepare do require "some/third_party/mailer" end end ``` - Provider behavior is now backed by a class per provider, known as the “Provider source” (@timriley in #202). The provider source class is created for each provider as a subclass of `Dry::System::Provider::Source`. You can still register simple providers using the block-based DSL, but the class backing means you can share state between provider steps using regular instance variables: ```ruby MyContainer.reigster_provider(:mailer) do prepare do require "some/third_party/mailer" @some_config = ThirdParty::Mailer::Config.new end start do # Since the `prepare` step will always run before start, we can access # @some_config here register "mailer", ThirdParty::Mailer.new(@some_config) end end ``` Inside this `register_provider` block, `self` is the source subclass itself, and inside each of the step blocks (i.e. `prepare do`), `self` will be the _instance_ of that provider source. For more complex providers, you can define your own source subclass and register it directly with the `source:` option for `register_provider`. This allows you to more readily use standard arrangements for factoring your logic within a class, such as extraction to another method: ```ruby MyContainer.register_provider(:mailer, source: Class.new(Dry::System::Provider::Source) { # The provider lifecycle steps are ordinary methods def prepare end def start mailer = some_complex_logic_to_build_the_mailer(some: "config") register(:mailer, mailer) end private def some_complex_logic_to_build_the_mailer(**options) # ... end }) ``` - The block argument to `Dry::System::Container.register_provider` (previously `.boot`) has been deprecated. (@timriley in #202). This argument was used to give you access to the provider's target container (i.e. the container on which you were registering the provider). To access the target container, you can use `#target_container` (or `#target` as a convenience alias) instead. You can also access the provider's own container (which is where the provider's components are registered when you call `register` directly inside a provider step) as `#provider_container` (or `#container` as a convenience alias). - `use(provider_name)` inside a provider step has been deprecated. Use `target_container.start(provider_name)` instead (@timriley in #211 and #224) Now that you can access `target_container` consistently within all provider steps, you can use it to also start any other providers as you require without any special additional method. This also allows you to invoke other provider lifecycle steps, like `target_container.prepare(provider_name)`. - `method_missing`-based delegation within providers to target container registrations has been removed (**BREAKING**) (@timriley in #202) Delegation to registrations with the provider's own container has been kept, since it can be a convenient way to access registrations made in a prior lifecycle step: ```ruby MyContainer.register_provider(:mailer, namespace: true) do prepare do register :config, "mailer config here" end start do config # => "mailer config here" end end ``` - The previous "external component" and "provider" concepts have been renamed to "external provider sources", in keeping with the new provider terminology outlined above (@timriley in #200 and #202). You can register a collection of external provider sources defined in their own source files via `Dry::System.register_provider_sources` (`Dry::System.register_provider` has been deprecated): ```ruby require "dry/system" Dry::System.register_provider_sources(path) ``` You can register an individual external provider source via `Dry::System.register_provider_source` (`Dry::System.register_component` has been deprecated): ```ruby Dry::System.register_provider_source(:something, group: :my_gem) do start do # ... end end ``` Just like providers, you can also register a class as an external provider source: ```ruby module MyGem class MySource < Dry::System::Provider::Source def start # ... end end end Dry::System.register_provider_source(:something, group: :my_gem, source: MyGem::MySource) ``` The `group:` argument when registering an external provider sources is for preventing name clashes between provider sources. You should use an underscored version of your gem name or namespace when registering your own provider sources. - Registering a provider using an explicitly named external provider source via `key:` argument is deprecated, use the `source:` argument instead (@timriley in #202). You can register a provider using the same name as an external provider source by specifying the `from:` argument only, as before: ```ruby # Elsewhere Dry::System.register_provider_source(:something, group: :my_gem) { ... } # In your app: MyContainer.register_provider(:something, from: :my_gem) ``` When you wish the name your provider differently, this is when you need to use the `source:` argument: ```ruby MyContainer.register_provider(:differently_named, from: :my_gem, source: :something) ``` When you're registering a provider using an external provider source, you cannot provie your own `Dry::System::Provider::Source` subclass as the `source:`, since that source class is being provided by the external provider source. - Provider source settings are now defined using dry-configurable’s `setting` API at the top-level scope (@timriley in #202). Use the top-level `setting` method to define your settings (the `settings` block and settings defined inside the block using `key` is deprecated). Inside the provider steps, the configured settings can be accessed as `config`: ```ruby # In the external provider source Dry::System.register_provider_source(:something, group: :my_gem) do setting :my_option start do # Do something with `config.my_option` here end end ``` When using an external provider source, configure the source via the `#configure`: ```ruby # In your application's provider using the external source MyContainer.register_provider(:something, from: :my_gem) do configure do |config| config.my_option = "some value" end end ``` To provide default values and type checking or constraints for your settings, use the dry-configurable’s `default:` and `constructor:` arguments: ```ruby # Constructor can take any proc being passed the provided value setting :my_option, default: "hello", constructor: -> (v) { v.to_s.upcase } # Constructor will also work with dry-types objects setting :my_option, default: "hello", constructor: Types::String.constrained(min_size: 3) ``` - External provider sources can define their own methods for use by the providers alongside lifecycle steps (@timriley in #202). Now that provider sources are class-backed, external provider sources can define their own methods to be made available when that provider source is used. This makes it possible to define your own extended API for interacting with the provider source: ```ruby # In the external provider source module MyGem class MySource < Dry::System::Provider::Source # Standard lifecycle steps def start # Do something with @on_start here end # Custom behavior available when this provider source is used in a provider def on_start(&block) @on_start = block end end end Dry::System.register_provider_source(:something, group: :my_gem, source: MyGem::MySource) # In your application's provider using the external source MyContainer.register_provider(:something, from: :my_gem) do # Use the custom method! on_start do # ... end end ``` - Providers can be registered conditionally using the `if:` option (@timriley in #218). You should provide a simple truthy or falsey value to `if:`, and in the case of falsey value, the provider will not be registered. This is useful in cases where you have providers that are loaded explicitly for specific runtime configurations of your app (e.g. when they are needed for specific tasks or processes only), but you do not need them for your primaary app process, for which you may finalize your container. - `bootable_dirs` container setting has been deprecated and replaced by `provider_dirs` (@timriley in #200). The default value for `provider_dirs` is now `"system/providers`". - Removed the unused `system_dir` container setting (**BREAKING**) (@timriley in #200) If you’ve configured this inside your container, you can remove it. - dry-system’s first-party external provider sources now available via `require "dry/system/provider_sources"`, with the previous `require "dry/system/components"` deprecated (@timriley in #202). - When using registering a provider using a first-party dry-system provider source, `from: :dry_system` instead of `from: :system` (which is now deprecated) (@timriley in #202). ```ruby MyContainer.register_provider(:settings, from: :dry_system) do # ... end - When registering a provider using the `:settings` provider source, settings are now defined using `setting` inside a `settings` block, rather than `key`, which is deprecated (@timriley in #202). This `setting` method uses the dry-configurable setting API: ```ruby MyContainer.register_provider(:settings, from: :dry_system) do settings do # Previously: # key :my_int_setting, MyTypes::Coercible::Integer # Now: setting :my_setting, default: 0, constructor: MyTypes::Coercible::Integer end end ``` - The `:settings` provider source now requires the dotenv gem to load settings from `.env*` files (**BREAKING**) (@timriley in #204) To ensure you can load your settings from these `.env*` files, add `gem "dotenv"` to your `Gemfile`. - `Dry::System::Container` can be now be configured direclty using the setting writer methods on the class-level `.config` object, without going the `.configure(&block)` API (@timriley in #207). If configuring via the class-level `.config` object, you should call `.configured!` after you're completed your configuration, which will finalize (freeze) the `config` object and then run any after-`:configure` hooks. - `Dry::System::Container.configure(&block)` will now finalize (freeze) the `config` object by default, before returning (@timriley in #207). You can opt out of this behavior by passing the `finalize_config: false` option: ```ruby class MyContainer < Dry::System::Container configure(finalize_config: false) do |config| # ... end # `config` is still non-finalized here end ``` - `Dry::System::Container.finalize!` will call `.configured!` (if it has not yet been called) before doing its work (@timriley in #207) This ensures config finalization is an intrinsic part of the overall container finalization process. - The `Dry::System::Container` `before(:configure)` hook has been removed (**BREAKING**) (@timriley in #207). This was previously used for plugins to register their own settings, but this was not necessary given that plugins are modules, and can use their ordinary `.extended(container_class)` hook to register their settings. Essentially, any time after container subclass definition is "before configure" in nature. - Container plugins should define their settings on the container using their module `.extended` hook, no longer in a `before(:configure)` hook (as above) (**BREAKING**) (@timriley in #207). This ensures the plugin settings are available immediately after you’ve enabled the plugin via `Dry::System::Container.use`. - The `Dry::System::Container` key `namespace_separator` setting is no longer expected to be user-configured. A key namespace separator of "." is hard-coded and expected to remain the separator string. (@timriley in #206) - Containers can import a specific subset of another container’s components via changes to `.import`, which is now `.import(keys: nil, from:, as:)` (with prior API deprecated) (@timriley in #209) To import specific components: ```ruby class MyContainer < Dry::System::Container # config, etc. # Will import components with keys "other.component_a", "other.component_b" import( keys: %w[component_a component_b], from: OtherContainer, as: :other ) ``` Omitting `keys:` will import all the components available from the other container. - Components imported into a container from another will be protected from subsequent export unless explicitly configured in `config.exports` (@timriley in #209) Imported components are considered “private” by default because they did not originate in container that imported them. This ensures there are no redundant imports in arrangements where multiple all containers import a common “base” container, and then some of those containers then import each other. - Container imports are now made without finalizing the exporting container in most cases, ensuring more efficient imports (@timriley in #209) Now, the only time the exporting container will be finalized is when a container is importing all components, and the exporting container has not declared any components in `config.exports`. - [Internal] The `manual_registrar` container setting and associated `ManualRegistrar` class have been renamed to `manifest_registrar` and `ManifestRegistrar` respectively (**BREAKING**) (@timriley in #208). - The default value for the container `registrations_dir` setting has been changed from `"container"` to `"system/registrations"` (**BREAKING**) (@timriley in #208) - The `:dependency_graph` plugin now supports all dry-auto_inject injector strategies (@davydovanton and @timriley in #214) [Compare v0.22.0...v0.23.0](https://github.com/dry-rb/dry-system/compare/v0.22.0...v0.23.0) ## [0.22.0] - 2022-01-06 ### Added - Expanded public interfaces for `Dry::System::Config::ComponentDirs` and `Dry::System::Config::Namespaces` to better support programmatic construction and inspection of these configs (@timriley in #195) ### Changed - Deprecated `Dry::System::Config::Namespaces#root` as the way to add and configure a root namespace. Use `#add_root` instead (@timriley in #195) - Allow bootsnap plugin to use bootsnap on Ruby versions up to 3.0 (pusewicz in #196) [Compare v0.21.0...v0.22.0](https://github.com/dry-rb/dry-system/compare/v0.21.0...v0.22.0) ## [0.21.0] - 2021-11-01 ### Added - Added **component dir namespaces** as a way to specify multiple, ordered, independent namespace rules within a given component dir. This replaces and expands upon the namespace support we previously provided via the singular `default_namespace` component dir setting (@timriley in #181) ### Changed - `default_namespace` setting on component dirs has been deprecated. Add a component dir namespace instead, e.g. instead of: ```ruby # Inside Dry::System::Container.configure config.component_dirs.add "lib" do |dir| dir.default_namespace = "admin" end ``` Add this: ```ruby config.component_dirs.add "lib" do |dir| dir.namespaces.add "admin", key: nil end ``` (@timriley in #181) - `Dry::System::Component#path` has been removed and replaced by `Component#require_path` and `Component#const_path` (@timriley in #181) - Unused `Dry::System::FileNotFoundError` and `Dry::System::InvalidComponentIdentifierTypeError` errors have been removed (@timriley in #194) - Allow bootsnap for Rubies up to 3.0.x (via #196) (@pusewicz) [Compare v0.20.0...v0.21.0](https://github.com/dry-rb/dry-system/compare/v0.20.0...v0.21.0) ## [0.20.0] - 2021-09-12 ### Fixed - Fixed dependency graph plugin to work with internal changes introduced in 0.19.0 (@wuarmin in #173) - Fixed behavior of `Dry::System::Identifier#start_with?` for components identified by a single segment, or if all matching segments are provided (@wuarmin in #177) - Fixed compatibility of `finalize!` signature provided in `Container::Stubs` (@mpokrywka in #178) ### Changed - [internal] Upgraded to new `setting` API provided in dry-configurable 0.13.0 (@timriley in #179) [Compare v0.19.2...v0.20.0](https://github.com/dry-rb/dry-system/compare/v0.19.2...v0.20.0) ## [0.19.2] - 2021-08-30 ### Changed - [internal] Improved compatibility with upcoming dry-configurable 0.13.0 release (@timriley in #186) [Compare v0.18.2...v0.19.2](https://github.com/dry-rb/dry-system/compare/v0.18.2...v0.19.2) ## [0.18.2] - 2021-08-30 ### Changed - [internal] Improved compatibility with upcoming dry-configurable 0.13.0 release (@timriley in #187) [Compare v0.19.1...v0.18.2](https://github.com/dry-rb/dry-system/compare/v0.19.1...v0.18.2) ## [0.19.1] - 2021-07-11 ### Fixed - Check for registered components (@timriley in #175) [Compare v0.19.0...v0.19.1](https://github.com/dry-rb/dry-system/compare/v0.19.0...v0.19.1) ## [0.19.0] - 2021-04-22 This release marks a huge step forward for dry-system, bringing support for Zeitwerk and other autoloaders, plus clearer configuration and improved consistency around component resolution for both finalized and lazy loading containers. [Read the announcement post](https://dry-rb.org/news/2021/04/22/dry-system-0-19-released-with-zeitwerk-support-and-more-leading-the-way-for-hanami-2-0/) for a high-level tour of the new features. ### Added - New `component_dirs` setting on `Dry::System::Container`, which must be used for specifying the directories which dry-system will search for component source files. Each added component dir is relative to the container's `root`, and can have its own set of settings configured: ```ruby class MyApp::Container < Dry::System::Container configure do |config| config.root = __dir__ # Defaults for all component dirs can be configured separately config.component_dirs.auto_register = true # default is already true # Component dirs can be added and configured independently config.component_dirs.add "lib" do |dir| dir.add_to_load_path = true # defaults to true dir.default_namespace = "my_app" end # All component dir settings are optional. Component dirs relying on default # settings can be added like so: config.component_dirs.add "custom_components" end end ``` The following settings are available for configuring added `component_dirs`: - `auto_register`, a boolean, or a proc accepting a `Dry::System::Component` instance and returning a truthy or falsey value. Providing a proc allows an auto-registration policy to apply on a per-component basis - `add_to_load_path`, a boolean - `default_namespace`, a string representing the leading namespace segments to be stripped from the component's identifier (given the identifier is derived from the component's fully qualified class name) - `loader`, a custom replacement for the default `Dry::System::Loader` to be used for the component dir - `memoize`, a boolean, to enable/disable memoizing all components in the directory, or a proc accepting a `Dry::System::Component` instance and returning a truthy or falsey value. Providing a proc allows a memoization policy to apply on a per-component basis _All component dir settings are optional._ (@timriley in #155, #157, and #162) - A new autoloading-friendly `Dry::System::Loader::Autoloading` is available, which is tested to work with [Zeitwerk](https://github.com/fxn/zeitwerk) 🎉 Configure this on the container (via a component dir `loader` setting), and the loader will no longer `require` any components, instead allowing missing constant resolution to trigger the loading of the required file. This loader presumes an autoloading system like Zeitwerk has already been enabled and appropriately configured. A recommended setup is as follows: ```ruby require "dry/system/container" require "dry/system/loader/autoloading" require "zeitwerk" class MyApp::Container < Dry::System::Container configure do |config| config.root = __dir__ config.component_dirs.loader = Dry::System::Loader::Autoloading config.component_dirs.add_to_load_path = false config.component_dirs.add "lib" do |dir| # ... end end end loader = Zeitwerk::Loader.new loader.push_dir MyApp::Container.config.root.join("lib").realpath loader.setup ``` (@timriley in #153) - [BREAKING] `Dry::System::Component` instances (which users of dry-system will interact with via custom loaders, as well as via the `auto_register` and `memoize` component dir settings described above) now return a `Dry::System::Identifier` from their `#identifier` method. The raw identifier string may be accessed via the identifier's own `#key` or `#to_s` methods. `Identifier` also provides a helpful namespace-aware `#start_with?` method for returning whether the identifier begins with the provided namespace(s) (@timriley in #158) ### Changed - Components with `# auto_register: false` magic comments in their source files are now properly ignored when lazy loading (@timriley in #155) - `# memoize: true` and `# memoize: false` magic comments at top of component files are now respected (@timriley in #155) - [BREAKING] `Dry::System::Container.load_paths!` has been renamed to `.add_to_load_path!`. This method now exists as a mere convenience only. Calling this method is no longer required for any configured `component_dirs`; these are now added to the load path automatically (@timriley in #153 and #155) - [BREAKING] `auto_register` container setting has been removed. Configured directories to be auto-registered by adding `component_dirs` instead (@timriley in #155) - [BREAKING] `default_namespace` container setting has been removed. Set it when adding `component_dirs` instead (@timriley in #155) - [BREAKING] `loader` container setting has been nested under `component_dirs`, now available as `component_dirs.loader` to configure a default loader for all component dirs, as well as on individual component dirs when being added (@timriley in #162) - [BREAKING] `Dry::System::ComponentLoadError` is no longer raised when a component could not be lazy loaded; this was only raised in a single specific failure condition. Instead, a `Dry::Container::Error` is raised in all cases of components failing to load (@timriley in #155) - [BREAKING] `Dry::System::Container.auto_register!` has been removed. Configure `component_dirs` instead. (@timriley in #157) - [BREAKING] The `Dry::System::Loader` interface has changed. It is now a static interface, no longer initialized with a component. The component is instead passed to each method as an argument: `.require!(component)`, `.call(component, *args)`, `.constant(component)` (@timriley in #157) - [BREAKING] `Dry::System::Container.require_path` has been removed. Provide custom require behavior by configuring your own `loader` (@timriley in #153) [Compare v0.18.1...v0.19.0](https://github.com/dry-rb/dry-system/compare/v0.18.1...v0.19.0) ## [0.18.1] - 2020-08-26 ### Fixed - Made `Booter#boot_files` a public method again, since it was required by dry-rails (@timriley) [Compare v0.18.0...v0.18.1](https://github.com/dry-rb/dry-system/compare/v0.18.0...v0.18.1) ## [0.18.0] - 2020-08-24 ### Added - New `bootable_dirs` setting on `Dry::System::Container`, which accepts paths to multiple directories for looking up bootable component files. (@timriley in PR #151) For each entry in the `bootable_dirs` array, relative directories will be appended to the container's `root`, and absolute directories will be left unchanged. When searching for bootable files, the first match will win, and any subsequent same-named files will not be loaded. In this way, the `bootable_dirs` act similarly to the `$PATH` in a shell environment. [Compare v0.17.0...v0.18.0](https://github.com/dry-rb/dry-system/compare/v0.17.0...v0.18.0) ## [0.17.0] - 2020-02-19 ### Fixed - Works with the latest dry-configurable version (issue #141) (@solnic) ### Changed - Depends on dry-configurable `=> 0.11.1` now (@solnic) [Compare v0.16.0...v0.17.0](https://github.com/dry-rb/dry-system/compare/v0.16.0...v0.17.0) ## [0.16.0] - 2020-02-15 ### Changed - Plugins can now define their own settings which are available in the `before(:configure)` hook (@solnic) - Dependency on dry-configurable was bumped to `~> 0.11` (@solnic) [Compare v0.15.0...v0.16.0](https://github.com/dry-rb/dry-system/compare/v0.15.0...v0.16.0) ## [0.15.0] - 2020-01-30 ### Added - New hook - `before(:configure)` which a plugin should use if it needs to declare new settings (@solnic) ```ruby # in your plugin code before(:configure) { setting :my_new_setting } after(:configure) { config.my_new_setting = "awesome" } ``` ### Changed - Centralize error definitions in `lib/dry/system/errors.rb` (@cgeorgii) - All built-in plugins use `before(:configure)` now to declare their settings (@solnic) [Compare v0.14.1...v0.15.0](https://github.com/dry-rb/dry-system/compare/v0.14.1...v0.15.0) ## [0.14.1] - 2020-01-22 ### Changed - Use `Kernel.require` explicitly to avoid issues with monkey-patched `require` from ActiveSupport (@solnic) [Compare v0.14.0...v0.14.1](https://github.com/dry-rb/dry-system/compare/v0.14.0...v0.14.1) ## [0.14.0] - 2020-01-21 ### Fixed - Misspelled plugin name raises meaningful error (issue #132) (@cgeorgii) - Fail fast if auto_registrar config contains incorrect path (@cutalion) [Compare v0.13.2...v0.14.0](https://github.com/dry-rb/dry-system/compare/v0.13.2...v0.14.0) ## [0.13.2] - 2019-12-28 ### Fixed - More keyword warnings (flash-gordon) [Compare v0.13.1...v0.13.2](https://github.com/dry-rb/dry-system/compare/v0.13.1...v0.13.2) ## [0.13.1] - 2019-11-07 ### Fixed - Fixed keyword warnings reported by Ruby 2.7 (flash-gordon) - Duplicates in `Dry::System::Plugins.loaded_dependencies` (AMHOL) [Compare v0.13.0...v0.13.1](https://github.com/dry-rb/dry-system/compare/v0.13.0...v0.13.1) ## [0.13.0] - 2019-10-13 ### Added - `Container.resolve` accepts and optional block parameter which will be called if component cannot be found. This makes dry-system consistent with dry-container 0.7.2 (flash-gordon) ```ruby App.resolve('missing.dep') { :fallback } # => :fallback ``` ### Changed - [BREAKING] `Container.key?` triggers lazy-loading for not finalized containers. If component wasn't found it returns `false` without raising an error. This is a breaking change, if you seek the previous behavior, use `Container.registered?` (flash-gordon) [Compare v0.12.0...v0.13.0](https://github.com/dry-rb/dry-system/compare/v0.12.0...v0.13.0) ## [0.12.0] - 2019-04-24 ### Changed - Compatibility with dry-struct 1.0 and dry-types 1.0 (flash-gordon) [Compare v0.11.0...v0.12.0](https://github.com/dry-rb/dry-system/compare/v0.11.0...v0.12.0) ## [0.11.0] - 2019-03-22 ### Changed - [BREAKING] `:decorate` plugin was moved from dry-system to dry-container (available in 0.7.0+). To upgrade remove `use :decorate` and change `decorate` calls from `decorate(key, decorator: something)` to `decorate(key, with: something)` (flash-gordon) - [internal] Compatibility with dry-struct 0.7.0 and dry-types 0.15.0 [Compare v0.10.1...v0.11.0](https://github.com/dry-rb/dry-system/compare/v0.10.1...v0.11.0) ## [0.10.1] - 2018-07-05 ### Added - Support for stopping bootable components with `Container.stop(component_name)` (GustavoCaso) ### Fixed - When using a non-finalized container, you can now resolve multiple different container objects registered using the same root key as a bootable component (timriley) [Compare v0.10.0...v0.10.1](https://github.com/dry-rb/dry-system/compare/v0.10.0...v0.10.1) ## [0.10.0] - 2018-06-07 ### Added - You can now set a custom inflector on the container level. As a result, the `Loader`'s constructor accepts two arguments: `path` and `inflector`, update your custom loaders accordingly (flash-gordon) ```ruby class MyContainer < Dry::System::Container configure do |config| config.inflector = Dry::Inflector.new do |inflections| inflections.acronym('API') end end end ``` ### Changed - A helpful error will be raised if an invalid setting value is provided (GustavoCaso) - When using setting plugin, will use default values from types (GustavoCaso) - Minimal supported ruby version was bumped to `2.3` (flash-gordon) - `dry-struct` was updated to `~> 0.5` (flash-gordon) [Compare v0.9.2...v0.10.0](https://github.com/dry-rb/dry-system/compare/v0.9.2...v0.10.0) ## [0.9.2] - 2018-02-08 ### Fixed - Default namespace no longer breaks resolving dependencies with identifier that includes part of the namespace (ie `mail.mailer`) (GustavoCaso) [Compare v0.9.1...v0.9.2](https://github.com/dry-rb/dry-system/compare/v0.9.1...v0.9.2) ## [0.9.1] - 2018-01-03 ### Fixed - Plugin dependencies are now auto-required and a meaningful error is raised when a dep failed to load (solnic) [Compare v0.9.0...v0.9.1](https://github.com/dry-rb/dry-system/compare/v0.9.0...v0.9.1) ## [0.9.0] - 2018-01-02 ### Added - Plugin API (solnic) - `:env` plugin which adds support for setting `env` config value (solnic) - `:logging` plugin which adds a default logger (solnic) - `:decorate` plugin for decorating registered objects (solnic) - `:notifications` plugin adding pub/sub bus to containers (solnic) - `:monitoring` plugin which adds `monitor` method for monitoring object method calls (solnic) - `:bootsnap` plugin which adds support for bootsnap (solnic) ### Changed - [BREAKING] renamed `Container.{require=>require_from_root}` (GustavoCaso) [Compare v0.8.1...v0.9.0](https://github.com/dry-rb/dry-system/compare/v0.8.1...v0.9.0) ## [0.8.1] - 2017-10-17 ### Fixed - Aliasing an external component works correctly (solnic) - Manually calling `:init` will also finalize a component (solnic) [Compare v0.8.0...v0.8.1](https://github.com/dry-rb/dry-system/compare/v0.8.0...v0.8.1) ## [0.8.0] - 2017-10-16 ### Added - Support for external bootable components (solnic) - Built-in `:system` components including `:settings` component (solnic) ### Fixed - Lazy-loading components work when a container has `default_namespace` configured (GustavoCaso) ### Changed - [BREAKING] Improved boot DSL with support for namespacing and lifecycle before/after callbacks (solnic) [Compare v0.7.3...v0.8.0](https://github.com/dry-rb/dry-system/compare/v0.7.3...v0.8.0) ## [0.7.3] - 2017-08-02 ### Fixed - `Container.enable_stubs!` calls super too, which actually adds `stub` API (solnic) - Issues with lazy-loading and import in stub mode are gone (solnic) [Compare v0.7.2...v0.7.3](https://github.com/dry-rb/dry-system/compare/v0.7.2...v0.7.3) ## [0.7.2] - 2017-08-02 ### Added - `Container.enable_stubs!` for test environments which enables stubbing components (GustavoCaso) ### Changed - Component identifiers can now include same name more than once ie `foo.stuff.foo` (GustavoCaso) - `Container#boot!` was renamed to `Container#start` (davydovanton) - `Container#boot` was renamed to `Container#init` (davydovanton) [Compare v0.7.1...v0.7.2](https://github.com/dry-rb/dry-system/compare/v0.7.1...v0.7.2) ## [0.7.1] - 2017-06-16 ### Changed - Accept string values for Container's `root` config (timriley) [Compare v0.7.0...v0.7.1](https://github.com/dry-rb/dry-system/compare/v0.7.0...v0.7.1) ## [0.7.0] - 2017-06-15 ### Added - Added `manual_registrar` container setting (along with default `ManualRegistrar` implementation), and `registrations_dir` setting. These provide support for a well-established place for keeping files with manual container registrations (timriley) - AutoRegistrar parses initial lines of Ruby source files for "magic comments" when auto-registering components. An `# auto_register: false` magic comment will prevent a Ruby file from being auto-registered (timriley) - `Container.auto_register!`, when called with a block, yields a configuration object to control the auto-registration behavior for that path, with support for configuring 2 different aspects of auto-registration behavior (both optional): ```ruby class MyContainer < Dry::System::Container auto_register!('lib') do |config| config.instance do |component| # custom logic for initializing a component end config.exclude do |component| # return true to skip auto-registration of the component, e.g. # component.path =~ /entities/ end end end ``` - A helpful error will be raised if a bootable component's finalize block name doesn't match its boot file name (GustavoCaso) ### Changed - The `default_namespace` container setting now supports multi-level namespaces (GustavoCaso) - `Container.auto_register!` yields a configuration block instead of a block for returning a custom instance (see above) (GustavoCaso) - `Container.import` now requires an explicit local name for the imported container (e.g. `import(local_name: AnotherContainer)`) (timriley) [Compare v0.6.0...v0.7.0](https://github.com/dry-rb/dry-system/compare/v0.6.0...v0.7.0) ## [0.6.0] - 2016-02-02 ### Changed - Lazy load components as they are resolved, rather than on injection (timriley) - Perform registration even though component already required (blelump) [Compare v0.5.1...v0.6.0](https://github.com/dry-rb/dry-system/compare/v0.5.1...v0.6.0) ## [0.5.1] - 2016-08-23 ### Fixed - Undefined locals or method calls will raise proper exceptions in Lifecycle DSL (aradunovic) [Compare v0.5.0...v0.5.1](https://github.com/dry-rb/dry-system/compare/v0.5.0...v0.5.1) ## [0.5.0] - 2016-08-15 for multi-container setups. As part of this release `dry-system` has been renamed to `dry-system`. ### Added - Boot DSL with: - Lifecycle triggers: `init`, `start` and `stop` (solnic) - `use` method which auto-boots a dependency and makes it available in the booting context (solnic) - When a component relies on a bootable component, and is being loaded in isolation, the component will be booted automatically (solnic) ### Changed - [BREAKING] `Dry::Component::Container` is now `Dry::System::Container` (solnic) - [BREAKING] Configurable `loader` is now a class that accepts container's config and responds to `#constant` and `#instance` (solnic) - [BREAKING] `core_dir` renameda to `system_dir` and defaults to `system` (solnic) - [BREAKING] `auto_register!` yields `Component` objects (solnic) [Compare v0.4.3...v0.5.0](https://github.com/dry-rb/dry-system/compare/v0.4.3...v0.5.0) ## [0.4.3] - 2016-08-01 ### Fixed - Return immediately from `Container.load_component` if the requested component key already exists in the container. This fixes a crash when requesting to load a manually registered component with a name that doesn't map to a filename (timriley in [#24](https://github.com/dry-rb/dry-system/pull/24)) [Compare v0.4.2...v0.4.3](https://github.com/dry-rb/dry-system/compare/v0.4.2...v0.4.3) ## [0.4.2] - 2016-07-26 ### Fixed - Ensure file components can be loaded when they're requested for the first time using their shorthand container identifier (i.e. with the container's default namespace removed) (timriley) [Compare v0.4.1...v0.4.2](https://github.com/dry-rb/dry-system/compare/v0.4.1...v0.4.2) ## [0.4.1] - 2016-07-26 ### Fixed - Require the 0.4.0 release of dry-auto_inject for the features below (in 0.4.0) to work properly (timriley) [Compare v0.4.0...v0.4.1](https://github.com/dry-rb/dry-system/compare/v0.4.0...v0.4.1) ## [0.4.0] - 2016-07-26 ### Added - Support for supplying a default namespace to a container, which is passed to the container's injector to allow for convenient shorthand access to registered objects in the same namespace (timriley in [#20](https://github.com/dry-rb/dry-system/pull/20)) ```ruby # Set up container with default namespace module Admin class Container < Dry::Component::Container configure do |config| config.root = Pathname.new(__dir__).join("../..") config.default_namespace = "admin" end end Import = Container.injector end module Admin class CreateUser # "users.repository" will resolve an Admin::Users::Repository instance, # where previously you had to identify it as "admin.users.repository" include Admin::Import["users.repository"] end end ``` - Support for supplying to options directly to dry-auto_inject's `Builder` via `Dry::Component::Container#injector(options)`. This allows you to provide dry-auto_inject customizations like your own container of injection strategies (timriley in [#20](https://github.com/dry-rb/dry-system/pull/20)) - Support for accessing all available injector strategies, not just the defaults (e.g. `MyContainer.injector.some_custom_strategy`) (timriley in [#19](https://github.com/dry-rb/dry-system/pull/19)) ### Changed - Subclasses of `Dry::Component::Container` no longer have an `Injector` constant automatically defined within them. The recommended approach is to save your own injector object to a constant, which allows you to pass options to it at the same time, e.g. `MyApp::Import = MyApp::Container.injector(my_options)` (timriley in [#19](https://github.com/dry-rb/dry-system/pull/19)) [Compare v0.3.0...v0.4.0](https://github.com/dry-rb/dry-system/compare/v0.3.0...v0.4.0) ## [0.3.0] - 2016-06-18 Removed two pieces that are moving to dry-web: ### Changed - Removed two pieces that are moving to dry-web: - Removed `env` setting from `Container` (timriley) - Removed `Dry::Component::Config` and `options` setting from `Container` (timriley) - Changed `Component#configure` behavior so it can be run multiple times for configuration to be applied in multiple passes (timriley) [Compare v0.2.0...v0.3.0](https://github.com/dry-rb/dry-system/compare/v0.2.0...v0.3.0) ## [0.2.0] - 2016-06-13 ### Fixed - Fixed bug where specified auto-inject strategies were not respected (timriley) ### Changed - Component core directory is now `component/` by default (timriley) - Injector default stragegy is now whatever dry-auto_inject's default is (rather than hard-coding a particular default strategy for dry-system) (timriley) [Compare v0.1.0...v0.2.0](https://github.com/dry-rb/dry-system/compare/v0.1.0...v0.2.0) ## [0.1.0] - 2016-06-07 ### Added - Provide a dependency injector as an `Inject` constant inside any subclass of `Dry::Component::Container`. This injector supports all of `dry-auto_inject`'s default injection strategies, and will lazily load any dependencies as they are injected. It also supports arbitrarily switching strategies, so they can be used in different classes as required (e.g. `include MyComponent::Inject.args["dep"]`) (timriley) - Support aliased dependency names when calling the injector object (e.g. `MyComponent::Inject[foo: "my_app.foo", bar: "another.thing"]`) (timriley) - Allow a custom dependency loader to be set on a container via its config (AMHOL) ```ruby class MyContainer < Dry::Component::Container configure do |config| # other config config.loader = MyLoader end end ``` ### Changed - `Container.boot` now only makes a simple `require` for the boot file (solnic) - Container object is passed to `Container.finalize` blocks (solnic) - Allow `Pathname` objects passed to `Container.require` (solnic) - Support lazily loading missing dependencies from imported containers (solnic) - `Container.import_module` renamed to `.injector` (timriley) - Default injection strategy is now `kwargs`, courtesy of the new dry-auto_inject default (timriley) [Compare v0.0.2...v0.1.0](https://github.com/dry-rb/dry-system/compare/v0.0.2...v0.1.0) ## [0.0.2] - 2015-12-24 ### Added - Containers have a `name` setting (solnic) - Containers can be imported into one another (solnic) ### Changed - Container name is used to determine the name of its config file (solnic) [Compare v0.0.1...v0.0.2](https://github.com/dry-rb/dry-system/compare/v0.0.1...v0.0.2) ## [0.0.1] - 2015-12-24 First public release, extracted from rodakase project ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge We pledge to make our community welcoming, safe, and equitable for all. We are committed to fostering an environment that respects and promotes the dignity, rights, and contributions of all individuals, regardless of characteristics including race, ethnicity, caste, color, age, physical characteristics, neurodiversity, disability, sex or gender, gender identity or expression, sexual orientation, language, philosophy or religion, national or social origin, socio-economic position, level of education, or other status. The same privileges of participation are extended to everyone who participates in good faith and in accordance with this Covenant. ## Encouraged Behaviors While acknowledging differences in social norms, we all strive to meet our community's expectations for positive behavior. We also understand that our words and actions may be interpreted differently than we intend based on culture, background, or native language. With these considerations in mind, we agree to behave mindfully toward each other and act in ways that center our shared values, including: 1. Respecting the **purpose of our community**, our activities, and our ways of gathering. 2. Engaging **kindly and honestly** with others. 3. Respecting **different viewpoints** and experiences. 4. **Taking responsibility** for our actions and contributions. 5. Gracefully giving and accepting **constructive feedback**. 6. Committing to **repairing harm** when it occurs. 7. Behaving in other ways that promote and sustain the **well-being of our community**. ## Restricted Behaviors We agree to restrict the following behaviors in our community. Instances, threats, and promotion of these behaviors are violations of this Code of Conduct. 1. **Harassment.** Violating explicitly expressed boundaries or engaging in unnecessary personal attention after any clear request to stop. 2. **Character attacks.** Making insulting, demeaning, or pejorative comments directed at a community member or group of people. 3. **Stereotyping or discrimination.** Characterizing anyone’s personality or behavior on the basis of immutable identities or traits. 4. **Sexualization.** Behaving in a way that would generally be considered inappropriately intimate in the context or purpose of the community. 5. **Violating confidentiality**. Sharing or acting on someone's personal or private information without their permission. 6. **Endangerment.** Causing, encouraging, or threatening violence or other harm toward any person or group. 7. Behaving in other ways that **threaten the well-being** of our community. ### Other Restrictions 1. **Misleading identity.** Impersonating someone else for any reason, or pretending to be someone else to evade enforcement actions. 2. **Failing to credit sources.** Not properly crediting the sources of content you contribute. 3. **Promotional materials**. Sharing marketing or other commercial content in a way that is outside the norms of the community. 4. **Irresponsible communication.** Failing to responsibly present content which includes, links or describes any other restricted behaviors. ## Reporting an Issue Tensions can occur between community members even when they are trying their best to collaborate. Not every conflict represents a code of conduct violation, and this Code of Conduct reinforces encouraged behaviors and norms that can help avoid conflicts and minimize harm. When an incident does occur, it is important to report it promptly. To report a possible violation, email conduct@hanakai.org. Community Moderators take reports of violations seriously and will make every effort to respond in a timely manner. They will investigate all reports of code of conduct violations, reviewing messages, logs, and recordings, or interviewing witnesses and other participants. Community Moderators will keep investigation and enforcement actions as transparent as possible while prioritizing safety and confidentiality. In order to honor these values, enforcement actions are carried out in private with the involved parties, but communicating to the whole community may be part of a mutually agreed upon resolution. ## Addressing and Repairing Harm If an investigation by the Community Moderators finds that this Code of Conduct has been violated, the following enforcement ladder may be used to determine how best to repair harm, based on the incident's impact on the individuals involved and the community as a whole. Depending on the severity of a violation, lower rungs on the ladder may be skipped. 1) Warning 1) Event: A violation involving a single incident or series of incidents. 2) Consequence: A private, written warning from the Community Moderators. 3) Repair: Examples of repair include a private written apology, acknowledgement of responsibility, and seeking clarification on expectations. 2) Temporarily Limited Activities 1) Event: A repeated incidence of a violation that previously resulted in a warning, or the first incidence of a more serious violation. 2) Consequence: A private, written warning with a time-limited cooldown period designed to underscore the seriousness of the situation and give the community members involved time to process the incident. The cooldown period may be limited to particular communication channels or interactions with particular community members. 3) Repair: Examples of repair may include making an apology, using the cooldown period to reflect on actions and impact, and being thoughtful about re-entering community spaces after the period is over. 3) Temporary Suspension 1) Event: A pattern of repeated violation which the Community Moderators have tried to address with warnings, or a single serious violation. 2) Consequence: A private written warning with conditions for return from suspension. In general, temporary suspensions give the person being suspended time to reflect upon their behavior and possible corrective actions. 3) Repair: Examples of repair include respecting the spirit of the suspension, meeting the specified conditions for return, and being thoughtful about how to reintegrate with the community when the suspension is lifted. 4) Permanent Ban 1) Event: A pattern of repeated code of conduct violations that other steps on the ladder have failed to resolve, or a violation so serious that the Community Moderators determine there is no way to keep the community safe with this person as a member. 2) Consequence: Access to all community spaces, tools, and communication channels is removed. In general, permanent bans should be rarely used, should have strong reasoning behind them, and should only be resorted to if working through other remedies has failed to change the behavior. 3) Repair: There is no possible repair in cases of this severity. This enforcement ladder is intended as a guideline. It does not limit the ability of Community Managers to use their discretion and judgment, in keeping with the best interests of our community. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public or other spaces. Examples of representing our community include using an official email address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Attribution This Code of Conduct is adapted from the Contributor Covenant, version 3.0, permanently available at [https://www.contributor-covenant.org/version/3/0/](https://www.contributor-covenant.org/version/3/0/). Contributor Covenant is stewarded by the Organization for Ethical Source and licensed under CC BY-SA 4.0. To view a copy of this license, visit [https://creativecommons.org/licenses/by-sa/4.0/](https://creativecommons.org/licenses/by-sa/4.0/) For answers to common questions about Contributor Covenant, see the FAQ at [https://www.contributor-covenant.org/faq](https://www.contributor-covenant.org/faq). Translations are provided at [https://www.contributor-covenant.org/translations](https://www.contributor-covenant.org/translations). Additional enforcement and community guideline resources can be found at [https://www.contributor-covenant.org/resources](https://www.contributor-covenant.org/resources). The enforcement ladder was inspired by the work of [Mozilla’s code of conduct team](https://github.com/mozilla/inclusion). ================================================ FILE: CONTRIBUTING.md ================================================ # Issue guidelines ## Reporting bugs If you’ve found a bug, please report an issue and describe 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. ## Reporting feature requests Report a feature request **only after discussing it first on [our forum](https://discourse.hanamirb.org)** and having it accepted. Please provide a concise description of the feature. ## Reporting questions, support requests, ideas, concerns etc. **Please don’t.** Use [our forum](https://discourse.hanamirb.org) instead. # Pull request guidelines A pull request will only be accepted if it addresses a specific issue that was reported previously, or fixes typos, mistakes in documentation etc. Other requirements: 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. 2. Follow the style conventions of the surrounding code. In most cases, this is standard ruby style. 3. Add API documentation if it's a new feature. 4. Update API documentation if it changes an existing feature. 5. Bonus points for sending a PR which updates user documentation in our [site repository](https://github.com/hanakai-rb/site). # Asking for help If these guidelines aren't helpful, and you're stuck, please post a message on [our forum](https://discourse.dry-rb.org) or [find us in chat](https://discord.gg/KFCxDmk3JQ). ================================================ FILE: Gemfile ================================================ # frozen_string_literal: true source "https://rubygems.org" eval_gemfile "Gemfile.devtools" gemspec # Remove verson constraint once latter versions release their -java packages gem "bootsnap" gem "dotenv" gem "dry-events" gem "dry-monitor" gem "dry-types" gem "zeitwerk" group :test do gem "ostruct" end ================================================ FILE: Gemfile.devtools ================================================ # frozen_string_literal: true # This file is synced from hanakai-rb/repo-sync gem "rake", ">= 12.3.3" group :test do gem "simplecov", require: false, platforms: :ruby gem "simplecov-cobertura", require: false, platforms: :ruby gem "rexml", require: false gem "warning" end group :tools do gem "rubocop" end ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2015-2026 Hanakai team Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ [actions]: https://github.com/dry-rb/dry-system/actions [chat]: https://discord.gg/naQApPAsZB [forum]: https://discourse.hanamirb.org [rubygem]: https://rubygems.org/gems/dry-system # dry-system [![Gem Version](https://badge.fury.io/rb/dry-system.svg)][rubygem] [![CI Status](https://github.com/dry-rb/dry-system/workflows/CI/badge.svg)][actions] [![Forum](https://img.shields.io/badge/Forum-dc360f?logo=discourse&logoColor=white)][forum] [![Chat](https://img.shields.io/badge/Chat-717cf8?logo=discord&logoColor=white)][chat] ## Links - [User documentation](https://dry-rb.org/gems/dry-system) - [API documentation](http://rubydoc.info/gems/dry-system) - [Forum](https://discourse.dry-rb.org) ## License See `LICENSE` file. ================================================ FILE: Rakefile ================================================ #!/usr/bin/env rake # frozen_string_literal: true require "bundler/gem_tasks" $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "lib")) require "rspec/core" require "rspec/core/rake_task" task default: :spec desc "Run all specs in spec directory" RSpec::Core::RakeTask.new(:spec) ================================================ FILE: bin/.gitkeep ================================================ ================================================ FILE: bin/console ================================================ #!/usr/bin/env ruby # frozen_string_literal: true require "bundler/setup" begin require "pry-byebug" Pry.start rescue LoadError require "irb" IRB.start end ================================================ FILE: docsite/source/component-dirs.html.md ================================================ --- title: Component dirs layout: gem-single name: dry-system --- The container auto-registers its components from one or more component dirs, the directories holding the Ruby source files for your classes. You can configure one or more component dirs: ```ruby class Application < Dry::System::Container configure do |config| config.root = __dir__ config.component_dirs.add "lib" config.component_dirs.add "app" end end ``` Component dirs will be searched in the order you add them. A component found in the first added dir will be registered in preference to a component with the same name in a later dir. ### Component dir configuration You can configure many aspects of component auto-registration via component dirs. #### auto_register `auto_register` sets the auto-registration policy for the component dir. This may be a simple boolean to enable or disable auto-registration for all components, or a proc accepting a `Dry::Sytem::Component` and returning a boolean to configure auto-registration on a per-component basis. `auto_register` defaults to `true`. ```ruby config.component_dirs.add "lib" do |dir| dir.auto_register = false end ``` ```ruby config.component_dirs.add "lib" do |dir| dir.auto_register = proc do |component| !component.identifier.start_with?("entities") end end ``` #### memoize `memoize` sets whether to memoize components from the dir when registered in the container (ordinarily, components are initialized every time they are resolved). This may be a simple boolean to enable or disable memoization for all components, or a proc accepting a `Dry::Sytem::Component` and returning a boolean to configure memoization on a per-component basis. `memoize` defaults to `false`. ```ruby config.component_dirs.add "lib" do |dir| dir.memoize = true end ``` ```ruby config.component_dirs.add "lib" do |dir| dir.memoize = proc do |component| component.identifier.start_with?("providers") end end ``` #### namespaces `namespaces` allows one or more namespaces to be added for paths within the component dir. For the given path, the namespace determines: 1. The leading segments of its components' registered identifiers, and 2. The expected constant namespace of their class constants. When adding a namespace, you can specify: - A `key:` namespace, which determines the leading part of the key used to register each component in the container. It can be: - Omitted, in which case it defaults to the value of `path` - A string, which will become the leading part of the registered keys - `nil`, which will make the registered keys top-level, with no additional leading parts - A `const:` namespace, which is the Ruby namespace expected to contain the class constants defined within each component's source file. This value is provided as an "underscored" string, and will be run through the container inflector's `#constantize`, to be converted in to a real constant (e.g. `"foo_bar/baz"` will become `FooBar::Baz`). Accordingly, `const:` can be: - Omitted, in which case it defaults to the value of `path` - A string, which will be constantized to the expected constant namespace per the rules above - `nil`, to indicate the class constants will be in the top-level constant namespace Only a single namespace can be added for any distinct path. To illustrate these options: **Top-level key namespace** ```ruby config.component_dirs.add "lib" do |dir| dir.namespaces.add "admin", key: nil end ``` - `admin/foo.rb` is expected to define `Admin::Foo`, will be registered as `"foo"` - `admin/bar/baz.rb` is expected to define `Admin::Bar::Baz`, will be registered as `"bar.baz"` **Top-level const namespace** ```ruby config.component_dirs.add "lib" do |dir| dir.namespaces.add "admin/top", const: nil end ``` - `admin/top/foo.rb` is expected to define `Foo`, will be registered as `"admin.top.foo"` - `admin/top/bar/baz.rb` is expected to define `Bar::Baz`, will be registered as `"admin.top.bar.baz"` **Distinct const namespace** ```ruby config.component_dirs.add "lib" do |dir| dir.namespaces.add "admin", key: nil, const: "super/admin" end ``` - `admin/foo.rb` is expected to define `Super::Admin::Foo`, will be registered as `"foo"` - `admin/bar/baz.rb` is expected to define `Super::Admin::Bar::Baz`, will be registered as `"bar.baz"` **Omitted key namespace, with keys keeping their natural prefix** ```ruby config.component_dirs.add "lib" do |dir| dir.namespaces.add "admin", const: "super/admin" end ``` - `admin/foo.rb` is expected to define `Super::Admin::Foo`, will be registered as `"admin.foo"` - `admin/bar/baz.rb` is expected to define `Super::Admin::Bar::Baz`, will be registered as `"admin.bar.baz"` ##### Each component dir may have multiple namespaces The examples above show a component dir with a single configured namespace, but component dir may have any number of namespaces: ```ruby config.component_dirs.add "lib" do |dir| dir.namespaces.add "admin/system_adapters", key: nil, const: nil dir.namespaces.add "admin", key: nil dir.namespaces.add "elsewhere", key: "stuff.and.things" end ``` When the container loads its components, namespaces are searched and evaluated in order of definition. So for the example above: - Files within `lib/admin/system_adapters/` will have the `key: nil, const: nil` namespace rules applied - All other files in `lib/admin/` will have the `key: nil` namespace rules applied - Files in `lib/elsewhere/` will have the `key: "stuff.and.things"` namespace rules applied ##### A root namespace is implicitly appended to a component dir's configured namespaces To ensure that all the the files within a component dir remain loadable, a "root namespace" is implicitly appended to the list of configured namespaces on a component dir. A root namespace, as the name implies, encompasses all files in the component dir. In the example above, the root namespace would be used when loading files _not_ in the `admin/` or `elsewhere/` paths. The default root namespace is effectively the following: ```ruby namespaces.add nil, key: nil, const: nil ``` It has `nil` path (the root of the component dir), a `nil` leading key namespace (all keys will be determined based on the full file path from the root of the dir), and a `nil` const namespace (implying that the root of the component dir will hold top-level constants). These assumptions tend to hold true for typically organised projects, and they ensure that the component dirs can load code usefully even when no namespaces are configured at all. ##### The root namespace may be explicitly configured There may be cases where you want different namespace rules to apply when loading components from the root of the component dir. To support this, you can configure the root namespace explicitly via `namespaces.root`. In this example, files in `lib/` are all expected to provide class constants in the `Admin` namespace: ```ruby config.component_dirs.add "lib" do |dir| dir.namespaces.root const: "admin" end ``` Root namespaces can be configured alongside other namespaces. The same namespace ordering preferences apply to root namespaces as to all others. #### add_to_load_path `add_to_load_path` sets whether the component dir should be added to the `$LOAD_PATH` after the container is configured. Set this to false if you’re using dry-container with an autoloader. `add_to_load_path` defaults to `true`. #### loader `loader` sets the loader to use when registering components from the dir in the container. `loader` defaults to `Dry::System::Loader`. When using a class autoloader, consider setting this to `Dry::System::Loader::Autoloading`: ```ruby require "dry/system" class Application < Dry::System::Container configure do |config| config.root = __dir__ config.component_dirs.add "lib" do |dir| dir.loader = Dry::System::Loader::Autoloading end end end ``` To provide a custom loader, you must implement the same interface as `Dry::System::Loader`. ### Component dir defaults configuration If you are adding multiple component dirs to your container, and would like common configuration to be applied to all of them, you can configure the `component_dirs` collection directly. Configuration set on `component_dirs` will be applied to all added component dirs. Any configuration applied directly to an individual component dir will override the defaults. ```ruby class MyApp::Container < Dry::System::Container configure do |config| config.root = __dir__ # Configure defaults for all component dirs config.component_dirs.auto_register = proc do |component| !component.identifier.start_with?("entities") end config.component_dirs.namespaces.add "admin", key: nil config.component_dirs.add "lib" config.component_dirs.add "app" end end ``` ### Inline component configuration with magic comments You can override certain aspects of the component dir configuration on a per-component basis by adding “magic comments” to the top of your source files. The following settings can be configured by magic comments: - `auto_register` - `memoize` In the magic comments, you can set `true` or `false` values only. For example, to disable auto-registration of a particular component: ```ruby # auto_register: false # frozen_string_literal: true class MyClass end ``` Or to enable memoization of a particular component: ```ruby # memoize: true # frozen_string_literal: true class MyClass end ``` ================================================ FILE: docsite/source/container/hooks.html.md ================================================ --- title: Hooks layout: gem-single name: dry-system --- There are a few lifecycle events that you can hook into if you need to ensure things happen in a particular order. Hooks are executed within the context of the container instance. ### `configure` Event You can register a callback to fire after the container is configured, which happens one of three ways: 1. The `configure` method is called on the container 2. The `configured!` method is called 3. The `finalize!` method is called when neither of the other two have been ```ruby class MyApp::Container < Dry::System::Container after(:configure) do # do something here end end ``` ### `register` Event Most of the time, you will know what keys you are working with ahead of time. But for certain cases you may want to react to keys dynamically. ```ruby class MyApp::Container < Dry::System::Container use :monitoring after(:register) do |key| next unless key.end_with?(".gateway") monitor(key) do |event| resolve(:logger).debug(key:, method: event[:method], time: event[:time]) end end end ``` Now let's say you register `api_client.gateway` into your container. Your API methods will be automatically monitored and their timing measured and logged. ### `finalize` Event Finalization is the point at which the container is made ready, such as booting a web application. The following keys are loaded in sequence: 1. Providers 2. Auto-registered components 3. Manually-registered components 4. Container imports At the conclusion of this process, the container is frozen thus preventing any further changes. This makes the `finalize` event quite important: it's the last call before your container will disallow mutation. Unlike the previous events, you can register before hooks in addition to after hooks. The after hooks will run immediately prior to the container freeze. This allows you to enumerate the container keys while they can still be mutated, such as with `decorate` or `monitor`. ```ruby class MyApp::Container < Dry::System::Container before(:finalize) do # Before system boot, no keys registered yet end after(:finalize) do # After system boot, all keys registered end end ``` ================================================ FILE: docsite/source/container.html.md ================================================ --- title: Container layout: gem-single name: dry-system sections: - hooks --- 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. Let's say you want to define an application container that will provide a logger: ``` ruby require 'dry/system' class Application < Dry::System::Container configure do |config| config.root = Pathname('./my/app') end end # now you can register a logger require 'logger' Application.register('utils.logger', Logger.new($stdout)) # and access it Application['utils.logger'] ``` ### Auto-Registration By using simple naming conventions we can automatically register objects within our container. Let's provide a custom logger object and put it under a custom load-path that we will configure: ``` ruby require "dry/system" class Application < Dry::System::Container configure do |config| config.root = Pathname("./my/app") # Add a 'lib' component dir (relative to `root`), containing class definitions # that can be auto-registered config.component_dirs.add "lib" end end # under /my/app/lib/logger.rb we put class Logger # some neat logger implementation end # we can finalize the container which triggers auto-registration Application.finalize! # the logger becomes available Application["logger"] ``` ================================================ FILE: docsite/source/dependency-auto-injection.html.md ================================================ --- title: Dependency auto-injection layout: gem-single name: dry-system --- After defining your container, you can use its auto-injector as a mixin to declare a component's dependencies using their container keys. For example, if you have an `Application` container and an object that will need a logger: ``` ruby # system/import.rb require "system/container" Import = Application.injector # In a class definition you simply specify what it needs # lib/post_publisher.rb require "import" class PostPublisher include Import["logger"] def call(post) # some stuff logger.debug("post published: #{post}") end end ``` ### Auto-registered component keys 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`. 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. 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: - `system/providers/persistence.rb` - where you register your `:persistence` provider - `lib/persistence/user_repo` - where you can define any components that need the components or setup established by the `persistence` provider Here's a sample setup for this scenario: ``` ruby # system/container.rb require "dry/system" class Application < Dry::System::Container configure do |config| config.root = Pathname("/my/app") config.component_dirs.add "lib" end end # system/import.rb require_relative "container" Import = Application.injector # system/providers/persistence.rb Application.register_provider(:persistence) do start do require "sequel" container.register("persistence.db", Sequel.connect(ENV['DB_URL'])) end stop do container["persistence.db"].disconnect end end # lib/persistence/user_repo.rb require "import" module Persistence class UserRepo include Import["persistence.db"] def find(conditions) db[:users].where(conditions) end end end ``` ================================================ FILE: docsite/source/external-provider-sources.html.md ================================================ --- title: External provider sources layout: gem-single name: dry-system --- 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. 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. To distribute a group of provider sources (defined in their own files), register them with `Dry::System`: ``` ruby # my_gem # |- lib/my_gem/provider_sources.rb Dry::System.register_provider_sources(:common, boot_path: File.join(__dir__, "provider_sources")) ``` Then, define your provider source: ``` ruby # my_gem # |- lib/my_gem/provider_sources/exception_notifier.rb Dry::System.register_provider_source(:exception_notifier, group: :my_gem) do prepare do require "some_exception_notifier" end start do register(:exception_notifier, SomeExceptionNotifier.new) end end ``` Then you can use this provider source when you register a provider in a dry-system container: ``` ruby # system/app/container.rb require "dry/system" require "my_gem/provider_sources" module App class Container < Dry::System::Container register_provider(:exception_notifier, from: :my_gem) end end App::Container[:exception_notifier] ``` ### Customizing provider sources You can customize a provider source for your application via `before` and `after` callbacks for its lifecycle steps. For example, you can register additional components based on the provider source's own registrations via an `after(:start)` callback: ``` ruby module App class Container < Dry::System::Container register_provider(:exception_notifier, from: :my_gem) do after(:start) register(:my_notifier, container[:exception_notifier]) end end end end ``` The following callbacks are supported: - `before(:prepare)` - `after(:prepare)` - `before(:start)` - `after(:start)` ### Providing component configuration 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`. For example, here’s an extended `:exception_notifier` provider source with settings: ``` ruby # my_gem # |- lib/my_gem/provider_sources/exception_notifier.rb Dry::System.register_component(:exception_notifier, provider: :common) do setting :environments, default: :production, constructor: Types::Strict::Array.of(Types::Strict::Symbol) setting :logger prepare do require "some_exception_notifier" end start do # Now we have access to `config` register(:exception_notifier, SomeExceptionNotifier.new(config.to_h)) end end ``` This defines two settings: - `:environments`, which is a list of environment identifiers with default value set to `[:production]` - `:logger`, an object that should be used as the logger, which must be configured To configure this provider source, you can use a `configure` block when defining your provider using the source: ``` ruby module App class Container < Dry::System::Container register_provider(:exception_notifier, from: :my_gem) do require "logger" configure do |config| config.logger = Logger.new($stdout) end end end end ``` ================================================ FILE: docsite/source/index.html.md ================================================ --- title: Introduction layout: gem-single name: dry-system type: gem sections: - container - component-dirs - providers - dependency-auto-injection - plugins - external-provider-sources - settings - test-mode --- 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. 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. It does a couple of things for you: * Provides an abstract dependency container implementation * Integrates with an autoloader, or handles `$LOAD_PATH` for you and loads needed files using `require` * Resolves object dependencies automatically * Supports auto-registration of dependencies via file/dir naming conventions * Supports multi-system setups (ie your application is split into multiple sub-systems) * Supports configuring component providers, which can be used to share common components between many systems * Supports test-mode with convenient stubbing API 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. This comes with a bunch of nice benefits: * Your system relies on abstractions rather than concrete classes and modules * It helps in decoupling your code from 3rd party code * 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. * It opens up doors to better instrumentation and debugging tools 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). ### Rails support 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. ### Credits * 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). * System/Component and lifecycle triggers are inspired by Clojure's [component](https://github.com/stuartsierra/component) library by [Stuart Sierra](https://github.com/stuartsierra) ================================================ FILE: docsite/source/plugins.html.md ================================================ --- title: Plugins layout: gem-single name: dry-system --- dry-system has already built-in plugins that you can enable, and it’s very easy to write your own. ## Zeitwerk With the `:zeitwerk` plugin you can easily use [Zeitwerk](https://github.com/fxn/zeitwerk) as your applications's code loader: > Given a conventional file structure, Zeitwerk is able to load your project's classes and modules on demand (autoloading), or upfront (eager loading). You don't need to write require calls for your own files, rather, you can streamline your programming knowing that your classes and modules are available everywhere. This feature is efficient, thread-safe, and matches Ruby's semantics for constants. (Zeitwerk docs) ### Example Here is an example of using Zeitwerk plugin: ```ruby class App < Dry::System::Container use :env, inferrer: -> { ENV.fetch("RACK_ENV", :development).to_sym } use :zeitwerk configure do |config| config.component_dirs.add "lib" end end ``` For a more in depth and runnable example, [see here](https://github.com/dry-rb/dry-system/tree/master/examples/zeitwerk). ### Inflections The plugin passes the container's inflector to the Zeitwerk loader for resolving constants from file names. If Zeitwerk has trouble resolving some constants, you can update the container's inflector like so: ```ruby class App < Dry::System::Container use :zeitwerk configure do |config| config.inflector = Dry::Inflector.new do |inflections| inflections.acronym('REST') end # ... end end ``` ### Eager Loading By default, the plugin will have Zeitwerk eager load when using the `:env` plugin sets the environment to `:production`. However, you can change this behavior by passing `:eager_load` option to the plugin: ```ruby class App < Dry::System::Container use :zeitwerk, eager_load: true end ``` ### Debugging When you are developing your application, you can enable the plugin's debugging mode by passing `debug: true` option to the plugin, which will print Zeitwerk's logs to the standard output. ```ruby class App < Dry::System::Container use :zeitwerk, debug: true end ``` ### Advanced Configuration If you need to adjust the Zeitwerk configuration, you can do so by accessing the `Zeitwerk::Loader` instance directly on the container, as `.autoloader`: ```ruby # After you have configured the container but before you have finalized it MyContainer.autoloader.ignore("./some_path.rb) ``` ## Application environment You can use the `:env` plugin to set and configure an `env` setting for your application. ```ruby class App < Dry::System::Container use :env configure do |config| config.env = :staging end end ``` You can provide environment inferrer, which is probably something you want to do, here’s how dry-web sets up its environment: ```ruby module Dry module Web class Container < Dry::System::Container use :env, inferrer: -> { ENV.fetch("RACK_ENV", :development).to_sym } end end end ``` ## Logging You can now enable a default system logger by simply enabling `:logging` plugin, you can also configure log dir, level and provide your own logger class. ```ruby class App < Dry::System::Container use :logging end # default logger is registered as a standard object, so you can inject it via auto-injection App[:logger] # short-cut method is provided too, which is convenient in some cases App.logger ``` ## Monitoring Another plugin is called `:monitoring` which allows you to enable object monitoring, which is built on top of dry-monitor’s instrumentation API. Let’s say you have an object registered under `"users.operations.create",` and you’d like to add additional logging: ```ruby class App < Dry::System::Container use :logging use :monitoring end App.monitor("users.operations.create") do |event| App.logger.debug "user created: #{event.payload} in #{event[:time]}ms" end ``` You can also provide specific methods that should be monitored, let’s say we’re only interested in `#call` method: ```ruby App.monitor("users.operations.create", methods: %i[call]) do |event| App.logger.debug "user created: #{event.payload} in #{event[:time]}ms" end ``` ## Experimental bootsnap support dry-system is already pretty fast, but in a really big apps, it can take some seconds to boot. You can now speed it up significantly by using `:bootsnap` plugin, which simply configures bootsnap for you: ```ruby class App < Dry::System::Container use :bootsnap # that's it end ``` We’ve noticed a ~30% speed boost during booting the entire app, unfortunately there are some problems with bootsnap + byebug, so it is now recommended to turn it off if you’re debugging something. ================================================ FILE: docsite/source/providers.html.md ================================================ --- title: Providers layout: gem-single name: dry-system --- 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. You can define your providers as individual source files in `system/providers/`, for example: ``` ruby # system/providers/persistence.rb Application.register_provider(:database) do prepare do require "third_party/db" end start do register(:database, ThirdParty::DB.new) end end ``` The provider’s lifecycle steps will not run until the provider is required by another component, is started directly, or when the container finalizes. This means you can require your container and ask it to start just that one provider: ``` ruby # system/application/container.rb class Application < Dry::System::Container configure do |config| config.root = Pathname("/my/app") end end Application.start(:database) # and now `database` becomes available Application["database"] ``` ### Provider lifecycle The provider lifecycle consists of three steps, each with a distinct purpose: * `prepare` - basic setup code, here you can require third party code and perform basic configuration * `start` - code that needs to run for a component to be usable at application's runtime * `stop` - code that needs to run to stop a component, ie close a database connection, clear some artifacts etc. Here's a simple example: ``` ruby # system/providers/db.rb Application.register_provider(:database) do prepare do require 'third_party/db' register(:database, ThirdParty::DB.configure(ENV['DB_URL'])) end start do container[:database].establish_connection end stop do container[:database].close_connection end end ``` ### Using other providers 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): ``` ruby # system/providers/logger.rb Application.register_provider(:logger) do prepare do require "logger" end start do register(:logger, Logger.new($stdout)) end end # system/providers/db.rb Application.register_provider(:db) do start do target.start :logger register(DB.new(ENV['DB_URL'], logger: target[:logger])) end end ``` ================================================ FILE: docsite/source/settings.html.md ================================================ --- title: Settings layout: gem-single name: dry-system --- ## Basic usage 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): ```ruby # system/providers/settings.rb: require "dry/system" Application.register_provider(:settings, from: :dry_system) do before :prepare do # Change this to load your own `Types` module if you want type-checked settings require "your/types/module" end settings do setting :database_url, constructor: Types::String.constrained(filled: true) setting :logger_level, default: :info, constructor: Types::Symbol .constructor { |value| value.to_s.downcase.to_sym } .enum(:trace, :unknown, :error, :fatal, :warn, :info, :debug) end end ``` 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: ```ruby Application[:settings].database_url # => "postgres://..." Application[:settings].logger_level # => :info ``` You can use this settings object in other providers: ```ruby Application.register_provider(:redis) do start do use :settings uri = URI.parse(target[:settings].redis_url) redis = Redis.new(host: uri.host, port: uri.port, password: uri.password) register('persistance.redis', redis) end end ``` Or as an injected dependency in your classes: ```ruby module Operations class CreateUser include Import[:settings] def call(params) settings # => your settings struct end end end end ``` ================================================ FILE: docsite/source/test-mode.html.md ================================================ --- title: Test Mode layout: gem-single name: dry-system --- In some cases it is useful to stub a component in your tests. To enable this, dry-system provides a test mode, in which a container will not be frozen during finalization. This allows you to use `stub` API to stub a given component. ``` ruby require 'dry/system' class Application < Dry::System::Container configure do |config| config.root = Pathname('./my/app') end end require 'dry/system/stubs' Application.enable_stubs! Application.stub('persistence.db', stubbed_db) ``` Typically, you want to use `enable_stubs!` in a test helper file, before booting your system. ================================================ FILE: dry-system.gemspec ================================================ # frozen_string_literal: true # This file is synced from hanakai-rb/repo-sync. To update it, edit repo-sync.yml. lib = File.expand_path("lib", __dir__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require "dry/system/version" Gem::Specification.new do |spec| spec.name = "dry-system" spec.authors = ["Hanakai team"] spec.email = ["info@hanakai.org"] spec.license = "MIT" spec.version = Dry::System::VERSION.dup spec.summary = "Organize your code into reusable components" spec.description = spec.summary spec.homepage = "https://dry-rb.org/gems/dry-system" spec.files = Dir["CHANGELOG.md", "LICENSE", "README.md", "dry-system.gemspec", "lib/**/*"] spec.bindir = "exe" spec.executables = Dir["exe/*"].map { |f| File.basename(f) } spec.require_paths = ["lib"] spec.extra_rdoc_files = ["README.md", "CHANGELOG.md", "LICENSE"] spec.metadata["changelog_uri"] = "https://github.com/dry-rb/dry-system/blob/main/CHANGELOG.md" spec.metadata["source_code_uri"] = "https://github.com/dry-rb/dry-system" spec.metadata["bug_tracker_uri"] = "https://github.com/dry-rb/dry-system/issues" spec.metadata["funding_uri"] = "https://github.com/sponsors/hanami" spec.required_ruby_version = ">= 3.1.0" spec.add_runtime_dependency "dry-auto_inject", "~> 1.1" spec.add_runtime_dependency "dry-configurable", "~> 1.3" spec.add_runtime_dependency "dry-core", "~> 1.1" spec.add_runtime_dependency "dry-inflector", "~> 1.1" spec.add_development_dependency "bundler" spec.add_development_dependency "rake" spec.add_development_dependency "rspec" end ================================================ FILE: examples/custom_configuration_auto_register/Gemfile ================================================ # frozen_string_literal: true source "https://rubygems.org" gem "dry-system", path: "../.." gem "sequel" gem "sqlite3" ================================================ FILE: examples/custom_configuration_auto_register/lib/entities/user.rb ================================================ # frozen_string_literal: true module Entities class User include Import["persistence.db"] end end ================================================ FILE: examples/custom_configuration_auto_register/lib/user_repo.rb ================================================ # frozen_string_literal: true class UserRepo include Import["persistence.db"] end ================================================ FILE: examples/custom_configuration_auto_register/run.rb ================================================ # frozen_string_literal: true require "bundler/setup" require_relative "system/container" require_relative "system/import" App.finalize! user_repo1 = App["user_repo"] user_repo2 = App["user_repo"] puts "User has not been loaded" unless App.key?("entities.user") puts user_repo1.db.inspect puts user_repo2.db.inspect puts "user_repo1 and user_repo2 reference the same instance" if user_repo1.equal?(user_repo2) ================================================ FILE: examples/custom_configuration_auto_register/system/boot/persistence.rb ================================================ # frozen_string_literal: true App.boot(:persistence) do |persistence| init do require "sequel" end start do persistence.register("persistence.db", Sequel.connect("sqlite::memory")) end stop do db.close_connection end end ================================================ FILE: examples/custom_configuration_auto_register/system/container.rb ================================================ # frozen_string_literal: true require "dry/system" class App < Dry::System::Container configure do |config| config.component_dirs.add "lib" do |dir| dir.memoize = true dir.auto_register = lambda do |component| !component.identifier.start_with?("entities") end end end end ================================================ FILE: examples/custom_configuration_auto_register/system/import.rb ================================================ # frozen_string_literal: true require_relative "container" Import = App.injector ================================================ FILE: examples/standalone/Gemfile ================================================ # frozen_string_literal: true source "https://rubygems.org" gem "dry-events" gem "dry-monitor" gem "dry-system", path: "../.." gem "sequel" gem "sqlite3" ================================================ FILE: examples/standalone/lib/empty_service.rb ================================================ # frozen_string_literal: true class EmptyService end ================================================ FILE: examples/standalone/lib/not_registered.rb ================================================ # frozen_string_literal: true class NotRegistered end ================================================ FILE: examples/standalone/lib/service_with_dependency.rb ================================================ # frozen_string_literal: true class ServiceWithDependency include Import["user_repo"] end ================================================ FILE: examples/standalone/lib/user_repo.rb ================================================ # frozen_string_literal: true class UserRepo include Import["persistence.db"] end ================================================ FILE: examples/standalone/run.rb ================================================ # frozen_string_literal: true require "bundler/setup" require_relative "system/container" require_relative "system/import" require "dry/events" require "dry/monitor/notifications" App[:notifications].subscribe(:resolved_dependency) do |event| puts "Event #{event.id}, payload: #{event.to_h}" end App[:notifications].subscribe(:registered_dependency) do |event| puts "Event #{event.id}, payload: #{event.to_h}" end App.finalize! p App.keys App["service_with_dependency"] user_repo = App["user_repo"] puts user_repo.db.inspect ================================================ FILE: examples/standalone/system/container.rb ================================================ # frozen_string_literal: true require "dry/events" require "dry/monitor/notifications" require "dry/system" class App < Dry::System::Container use :dependency_graph configure do |config| config.component_dirs.add "lib" do |dir| dir.add_to_load_path = true # defaults to true dir.auto_register = lambda do |component| !component.identifier.start_with?("not_registered") end end end end ================================================ FILE: examples/standalone/system/import.rb ================================================ # frozen_string_literal: true require_relative "container" Import = App.injector ================================================ FILE: examples/standalone/system/providers/persistence.rb ================================================ # frozen_string_literal: true App.boot(:persistence) do |persistence| init do require "sequel" end start do persistence.register("persistence.db", Sequel.connect("sqlite::memory")) end stop do db.close_connection end end ================================================ FILE: examples/zeitwerk/Gemfile ================================================ # frozen_string_literal: true source "https://rubygems.org" gem "dry-events" gem "dry-monitor" gem "dry-system", path: "../.." gem "zeitwerk" ================================================ FILE: examples/zeitwerk/lib/service_with_dependency.rb ================================================ # frozen_string_literal: true class ServiceWithDependency include Import["user_repo"] end ================================================ FILE: examples/zeitwerk/lib/user_repo.rb ================================================ # frozen_string_literal: true class UserRepo end ================================================ FILE: examples/zeitwerk/run.rb ================================================ # frozen_string_literal: true require "bundler/setup" require_relative "system/container" require_relative "system/import" App.finalize! service = App["service_with_dependency"] puts "Container keys: #{App.keys}" puts "User repo: #{service.user_repo.inspect}" puts "Loader: #{App.autoloader}" ================================================ FILE: examples/zeitwerk/system/container.rb ================================================ # frozen_string_literal: true require "dry/system" class App < Dry::System::Container use :env, inferrer: -> { ENV.fetch("RACK_ENV", :development).to_sym } use :zeitwerk, debug: true configure do |config| config.component_dirs.add "lib" end end ================================================ FILE: examples/zeitwerk/system/import.rb ================================================ # frozen_string_literal: true require_relative "container" Import = App.injector ================================================ FILE: lib/dry/system/auto_registrar.rb ================================================ # frozen_string_literal: true require "dry/system/constants" module Dry module System # Default auto-registration implementation # # This is currently configured by default for every System::Container. # Auto-registrar objects are responsible for loading files from configured # auto-register paths and registering components automatically within the # container. # # @api private class AutoRegistrar attr_reader :container def initialize(container) @container = container end # @api private def finalize! container.component_dirs.each do |component_dir| call(component_dir) if component_dir.auto_register? end end # @api private def call(component_dir) component_dir.each_component do |component| next unless register_component?(component) container.register(component.key, memoize: component.memoize?) { component.instance } end end private def register_component?(component) !container.registered?(component.key) && component.auto_register? end end end end ================================================ FILE: lib/dry/system/component.rb ================================================ # frozen_string_literal: true require "pathname" require "dry/inflector" require "dry/system/errors" require "dry/system/constants" module Dry module System # Components are objects providing information about auto-registered files. # They expose an API to query this information and use a configurable # loader object to initialize class instances. # # @api public class Component include Dry::Equalizer(:identifier, :file_path, :namespace, :options) DEFAULT_OPTIONS = { inflector: Dry::Inflector.new, loader: Loader }.freeze # @!attribute [r] identifier # @return [String] the component's unique identifier attr_reader :identifier # @!attribute [r] file_path # @return [Pathname] the component's source file path attr_reader :file_path # @!attribute [r] namespace # @return [Dry::System::Config::Namespace] the component's namespace attr_reader :namespace # @!attribute [r] options # @return [Hash] the component's options attr_reader :options # @api private def initialize(identifier, file_path:, namespace:, **options) @identifier = identifier @file_path = Pathname(file_path) @namespace = namespace @options = DEFAULT_OPTIONS.merge(options) end # Returns true, indicating that the component is directly loadable from the files # managed by the container # # This is the inverse of {IndirectComponent#loadable?} # # @return [TrueClass] # # @api private def loadable? true end # Returns the component's instance # # @return [Object] component's class instance # @api public def instance(*args, **kwargs) options[:instance]&.call(self, *args, **kwargs) || loader.call(self, *args, **kwargs) end # Returns the component's unique key # # @return [String] the key # # @see Identifier#key # # @api public def key identifier.key end # Returns the root namespace segment of the component's key, as a symbol # # @see Identifier#root_key # # @return [Symbol] the root key # # @api public def root_key identifier.root_key end # Returns a path-delimited representation of the compnent, appropriate for passing # to `Kernel#require` to require its source file # # The path takes into account the rules of the namespace used to load the component. # # @example Component from a root namespace # component.key # => "articles.create" # component.require_path # => "articles/create" # # @example Component from an "admin/" path namespace (with `key: nil`) # component.key # => "articles.create" # component.require_path # => "admin/articles/create" # # @see Config::Namespaces#add # @see Config::Namespace # # @return [String] the require path # # @api public def require_path if namespace.path "#{namespace.path}#{PATH_SEPARATOR}#{path_in_namespace}" else path_in_namespace end end # Returns an "underscored", path-delimited representation of the component, # appropriate for passing to the inflector for constantizing # # The const path takes into account the rules of the namespace used to load the # component. # # @example Component from a namespace with `const: nil` # component.key # => "articles.create_article" # component.const_path # => "articles/create_article" # component.inflector.constantize(component.const_path) # => Articles::CreateArticle # # @example Component from a namespace with `const: "admin"` # component.key # => "articles.create_article" # component.const_path # => "admin/articles/create_article" # component.inflector.constantize(component.const_path) # => Admin::Articles::CreateArticle # # @see Config::Namespaces#add # @see Config::Namespace # # @return [String] the const path # # @api public def const_path namespace_const_path = namespace.const&.gsub(KEY_SEPARATOR, PATH_SEPARATOR) if namespace_const_path "#{namespace_const_path}#{PATH_SEPARATOR}#{path_in_namespace}" else path_in_namespace end end # @api private def loader options.fetch(:loader) end # @api private def inflector options.fetch(:inflector) end # @api private def auto_register? callable_option?(options[:auto_register]) end # @api private def memoize? callable_option?(options[:memoize]) end private def path_in_namespace identifier_in_namespace = if namespace.key identifier.namespaced(from: namespace.key, to: nil) else identifier end identifier_in_namespace.key_with_separator(PATH_SEPARATOR) end def callable_option?(value) if value.respond_to?(:call) !!value.call(self) else !!value end end end end end ================================================ FILE: lib/dry/system/component_dir.rb ================================================ # frozen_string_literal: true require "pathname" require "dry/system/constants" module Dry module System # A configured component directory within the container's root. Provides access to the # component directory's configuration, as well as methods for locating component files # within the directory # # @see Dry::System::Config::ComponentDir # @api private class ComponentDir # @!attribute [r] config # @return [Dry::System::Config::ComponentDir] the component directory configuration # @api private attr_reader :config # @!attribute [r] container # @return [Dry::System::Container] the container managing the component directory # @api private attr_reader :container # @api private def initialize(config:, container:) @config = config @container = container end # Returns a component for the given key if a matching source file is found within # the component dir # # This searches according to the component dir's configured namespaces, in order of # definition, with the first match returned as the component. # # @param key [String] the component's key # @return [Dry::System::Component, nil] the component, if found # # @api private def component_for_key(key) config.namespaces.each do |namespace| identifier = Identifier.new(key) next unless identifier.start_with?(namespace.key) if (file_path = find_component_file(identifier, namespace)) return build_component(identifier, namespace, file_path) end end nil end def each_component return enum_for(:each_component) unless block_given? each_file do |file_path, namespace| yield component_for_path(file_path, namespace) end end private def each_file return enum_for(:each_file) unless block_given? raise ComponentDirNotFoundError, full_path unless Dir.exist?(full_path) config.namespaces.each do |namespace| files(namespace).each do |file| yield file, namespace end end end def files(namespace) if namespace.path? ::Dir[::File.join(full_path, namespace.path, "**", RB_GLOB)] else non_root_paths = config.namespaces.to_a.reject(&:root?).map(&:path) ::Dir[::File.join(full_path, "**", RB_GLOB)].reject { |file_path| Pathname(file_path).relative_path_from(full_path).to_s.start_with?(*non_root_paths) } end end # Returns the full path of the component directory # # @return [Pathname] def full_path container.root.join(path) end # Returns a component for a full path to a Ruby source file within the component dir # # @param path [String] the full path to the file # @return [Dry::System::Component] the component def component_for_path(path, namespace) key = Pathname(path).relative_path_from(full_path).to_s .sub(RB_EXT, EMPTY_STRING) .scan(WORD_REGEX) .join(KEY_SEPARATOR) identifier = Identifier.new(key) .namespaced( from: namespace.path&.gsub(PATH_SEPARATOR, KEY_SEPARATOR), to: namespace.key ) build_component(identifier, namespace, path) end def find_component_file(identifier, namespace) # To properly find the file within a namespace with a key, we should strip the key # from beginning of our given identifier if namespace.key identifier = identifier.namespaced(from: namespace.key, to: nil) end file_name = "#{identifier.key_with_separator(PATH_SEPARATOR)}#{RB_EXT}" component_file = if namespace.path? full_path.join(namespace.path, file_name) else full_path.join(file_name) end component_file if component_file.exist? end def build_component(identifier, namespace, file_path) options = { inflector: container.config.inflector, **component_options, **MagicCommentsParser.(file_path) } Component.new( identifier, namespace: namespace, file_path: file_path, **options ) end def component_options { auto_register: auto_register, loader: loader, instance: instance, memoize: memoize } end def method_missing(name, ...) if config.respond_to?(name) config.public_send(name, ...) else super end end def respond_to_missing?(name, include_all = false) config.respond_to?(name) || super end end end end ================================================ FILE: lib/dry/system/config/component_dir.rb ================================================ # frozen_string_literal: true require "dry/system/constants" module Dry module System module Config # @api public class ComponentDir include ::Dry::Configurable # @!group Settings # @!method auto_register=(policy) # # Sets the auto-registration policy for the component dir. # # This may be a simple boolean to enable or disable auto-registration for all # components, or a proc accepting a {Dry::System::Component} and returning a # boolean to configure auto-registration on a per-component basis # # Defaults to `true`. # # @param policy [Boolean, Proc] # @return [Boolean, Proc] # # @example # dir.auto_register = false # # @example # dir.auto_register = proc do |component| # !component.identifier.start_with?("entities") # end # # @see auto_register # @see Component # @api public # # @!method auto_register # # Returns the configured auto-registration policy. # # @return [Boolean, Proc] the configured policy # # @see auto_register= # @api public setting :auto_register, default: true # @!method instance=(instance_proc) # # Sets a proc used to return the instance of any component within the component # dir. # # This proc should accept a {Dry::System::Component} and return the object to # serve as the component's instance. # # When you provide an instance proc, it will be used in preference to the # {loader} (either the default loader or an explicitly configured one). Provide # an instance proc when you want a simple way to customize the instance for # certain components. For complete control, provide a replacement loader via # {loader=}. # # Defaults to `nil`. # # @param instance_proc [Proc, nil] # @return [Proc] # # @example # dir.instance = proc do |component| # if component.key.match?(/workers\./) # # Register classes for jobs # component.loader.constant(component) # else # # Otherwise register regular instances per default loader # component.loader.call(component) # end # end # # @see Component, Loader # @api public # # @!method instance # # Returns the configured instance proc. # # @return [Proc, nil] # # @see instance= # @api public setting :instance # @!method loader=(loader) # # Sets the loader to use when registering components from the dir in the # container. # # Defaults to `Dry::System::Loader`. # # When using an autoloader like Zeitwerk, consider using # `Dry::System::Loader::Autoloading` # # @param loader [#call] the loader # @return [#call] the configured loader # # @see loader # @see Loader # @see Loader::Autoloading # @api public # # @!method loader # # Returns the configured loader. # # @return [#call] # # @see loader= # @api public setting :loader, default: Dry::System::Loader # @!method memoize=(policy) # # Sets whether to memoize components from the dir when registered in the # container. # # This may be a simple boolean to enable or disable memoization for all # components, or a proc accepting a `Dry::Sytem::Component` and returning a # boolean to configure memoization on a per-component basis # # Defaults to `false`. # # @param policy [Boolean, Proc] # @return [Boolean, Proc] the configured memoization policy # # @example # dir.memoize = true # # @example # dir.memoize = proc do |component| # !component.identifier.start_with?("providers") # end # # @see memoize # @see Component # @api public # # @!method memoize # # Returns the configured memoization policy. # # @return [Boolean, Proc] the configured memoization policy # # @see memoize= # @api public setting :memoize, default: false # @!method namespaces # # Returns the configured namespaces for the component dir. # # Allows namespaces to added on the returned object via {Namespaces#add}. # # @return [Namespaces] the namespaces # # @see Namespaces#add # @api public setting :namespaces, default: Namespaces.new, cloneable: true # @!method add_to_load_path=(policy) # # Sets whether the dir should be added to the `$LOAD_PATH` after the container # is configured. # # Defaults to `true`. This may need to be set to `false` when using a class # autoloading system. # # @param policy [Boolean] # @return [Boolean] # # @see add_to_load_path # @see Container.configure # @api public # # @!method add_to_load_path # # Returns the configured value. # # @return [Boolean] # # @see add_to_load_path= # @api public setting :add_to_load_path, default: true # @!endgroup # Returns the component dir path, relative to the configured container root # # @return [String] the path attr_reader :path # @api public def initialize(path) super() @path = path yield self if block_given? end # @api private def auto_register? !!config.auto_register end private def method_missing(name, ...) if config.respond_to?(name) config.public_send(name, ...) else super end end def respond_to_missing?(name, include_all = false) config.respond_to?(name) || super end end end end end ================================================ FILE: lib/dry/system/config/component_dirs.rb ================================================ # frozen_string_literal: true require "dry/system/constants" require "dry/system/errors" module Dry module System module Config # The configured component dirs for a container # # @api public class ComponentDirs # @!group Settings # @!method auto_register=(value) # # Sets a default `auto_register` for all added component dirs # # @see ComponentDir.auto_register= # @see auto_register # # @!method auto_register # # Returns the configured default `auto_register` # # @see auto_register= # @!method instance=(value) # # Sets a default `instance` for all added component dirs # # @see ComponentDir.instance= # @see auto_register # # @!method auto_register # # Returns the configured default `instance` # # @see instance= # @!method loader=(value) # # Sets a default `loader` value for all added component dirs # # @see ComponentDir.loader= # @see loader # # @!method loader # # Returns the configured default `loader` # # @see loader= # @!method memoize=(value) # # Sets a default `memoize` value for all added component dirs # # @see ComponentDir.memoize= # @see memoize # # @!method memoize # # Returns the configured default `memoize` # # @see memoize= # @!method namespaces # # Returns the default configured namespaces for all added component dirs # # Allows namespaces to added on the returned object via {Dry::System::Config::Namespaces#add}. # # @see Dry::System::Config::Namespaces#add # # @return [Namespaces] the namespaces # @!method add_to_load_path=(value) # # Sets a default `add_to_load_path` value for all added component dirs # # @see ComponentDir.add_to_load_path= # @see add_to_load_path # # @!method add_to_load_path # # Returns the configured default `add_to_load_path` # # @see add_to_load_path= # @!endgroup # A ComponentDir for configuring the default values to apply to all added # component dirs # # @see #method_missing # @api private attr_reader :defaults # Creates a new component dirs # # @api private def initialize @dirs = {} @defaults = ComponentDir.new(nil) end # @api private def initialize_copy(source) @dirs = source.dirs.transform_values(&:dup) @defaults = source.defaults.dup end # Returns and optionally yields a previously added component dir # # @param path [String] the path for the component dir # @yieldparam dir [ComponentDir] the component dir # # @return [ComponentDir] the component dir # # @api public def dir(path) dirs[path].tap do |dir| # Defaults can be (re-)applied first, since the dir has already been added apply_defaults_to_dir(dir) if dir yield dir if block_given? end end alias_method :[], :dir # @overload add(path) # Adds and configures a component dir for the given path # # @param path [String] the path for the component dir, relative to the configured # container root # @yieldparam dir [ComponentDir] the component dir to configure # # @return [ComponentDir] the added component dir # # @example # component_dirs.add "lib" do |dir| # dir.default_namespace = "my_app" # end # # @see ComponentDir # @api public # # @overload add(dir) # Adds a configured component dir # # @param dir [ComponentDir] the configured component dir # # @return [ComponentDir] the added component dir # # @example # dir = Dry::System::ComponentDir.new("lib") # component_dirs.add dir # # @see ComponentDir # @api public def add(path_or_dir) path, dir_to_add = path_and_dir(path_or_dir) raise ComponentDirAlreadyAddedError, path if dirs.key?(path) dirs[path] = dir_to_add.tap do |dir| # Defaults must be applied after yielding, since the dir is being newly added, # and must have its configuration fully in place before we can know which # defaults to apply yield dir if path_or_dir == path && block_given? apply_defaults_to_dir(dir) end end # Deletes and returns a previously added component dir # # @param path [String] the path for the component dir # # @return [ComponentDir] the removed component dir # # @api public def delete(path) dirs.delete(path) end # Returns the paths of the component dirs # # @return [Array] the component dir paths # # @api public def paths dirs.keys end # Returns the count of component dirs # # @return [Integer] # # @api public def length dirs.length end alias_method :size, :length # Returns the added component dirs, with default settings applied # # @return [Array] # # @api public def to_a dirs.each { |_, dir| apply_defaults_to_dir(dir) } dirs.values end # Calls the given block once for each added component dir, passing the dir as an # argument. # # @yieldparam dir [ComponentDir] the yielded component dir # # @api public def each(&) to_a.each(&) end protected # Returns the hash of component dirs, keyed by their paths # # Recently changed default configuration may not be applied to these dirs. Use # #to_a or #each to access dirs with default configuration fully applied. # # This method exists to encapsulate the instance variable and to serve the needs # of #initialize_copy # # @return [Hash{String => ComponentDir}] # # @api private attr_reader :dirs private # Converts a path string or pre-built component dir into a path and dir tuple # # @param path_or_dir [String,ComponentDir] # # @return [Array<(String, ComponentDir)>] # # @see #add def path_and_dir(path_or_dir) if path_or_dir.is_a?(ComponentDir) dir = path_or_dir [dir.path, dir] else path = path_or_dir [path, ComponentDir.new(path)] end end # Applies default settings to a component dir. This is run every time the dirs are # accessed to ensure defaults are applied regardless of when new component dirs # are added. This method must be idempotent. # # @return [void] def apply_defaults_to_dir(dir) defaults.config.values.each do |key, _| if defaults.configured?(key) && !dir.configured?(key) dir.public_send(:"#{key}=", defaults.public_send(key).dup) end end end def method_missing(name, ...) if defaults.respond_to?(name) defaults.public_send(name, ...) else super end end def respond_to_missing?(name, include_all = false) defaults.respond_to?(name) || super end end end end end ================================================ FILE: lib/dry/system/config/namespace.rb ================================================ # frozen_string_literal: true require "dry/system/constants" module Dry module System module Config # A configured namespace for a component dir # # Namespaces consist of three elements: # # - The `path` within the component dir to which its namespace rules should apply. # - A `key`, which determines the leading part of the key used to register # each component in the container. # - A `const`, which is the Ruby namespace expected to contain the class constants # defined within each component's source file. This value is expected to be an # "underscored" string, intended to be run through the configured inflector to be # converted into a real constant (e.g. `"foo_bar/baz"` will become `FooBar::Baz`) # # Namespaces are added and configured for a component dir via {Namespaces#add}. # # @see Namespaces#add # # @api public class Namespace ROOT_PATH = nil include Dry::Equalizer(:path, :key, :const) # @api public attr_reader :path # @api public attr_reader :key # @api public attr_reader :const # Returns a namespace configured to serve as the default root namespace for a # component dir, ensuring that all code within the dir can be loaded, regardless # of any other explictly configured namespaces # # @return [Namespace] the root namespace # # @api private def self.default_root new( path: ROOT_PATH, key: nil, const: nil ) end # @api private def initialize(path:, key:, const:) @path = path # Default keys (i.e. when the user does not explicitly provide one) for non-root # paths will include path separators, which we must convert into key separators @key = key && key == path ? key.gsub(PATH_SEPARATOR, KEY_SEPARATOR) : key @const = const end # @api public def root? path == ROOT_PATH end # @api public def path? !root? end end end end end ================================================ FILE: lib/dry/system/config/namespaces.rb ================================================ # frozen_string_literal: true require "dry/system/errors" module Dry module System module Config # The configured namespaces for a ComponentDir # # @see Config::ComponentDir#namespaces # # @api private class Namespaces include Dry::Equalizer(:namespaces) # @api private attr_reader :namespaces # @api private def initialize @namespaces = {} end # @api private def initialize_copy(source) super @namespaces = source.namespaces.dup end # Returns the namespace configured for the path, or nil if no such namespace has # been configured # # @return [Namespace, nil] the namespace, if configured # # @api public def namespace(path) namespaces[path] end alias_method :[], :namespace # Returns the namespace configured for the root path, or nil if the root namespace # has not been configured # # @return [Namespace, nil] the root namespace, if configured # # @api public def root namespaces[Namespace::ROOT_PATH] end # Adds a component dir namespace # # A namespace encompasses a given sub-directory of the component dir, and # determines (1) the leading segments of its components' registered identifiers, # and (2) the expected constant namespace of their class constants. # # A namespace for a path can only be added once. # # @example Adding a namespace with top-level identifiers # # Components defined within admin/ (e.g. admin/my_component.rb) will be: # # # # - Registered with top-level identifiers ("my_component") # # - Expected to have constants in `Admin`, matching the namespace's path (Admin::MyComponent) # # namespaces.add "admin", key: nil # # @example Adding a namespace with top-level class constants # # Components defined within adapters/ (e.g. adapters/my_adapter.rb) will be: # # # # - Registered with leading identifiers matching the namespace's path ("adapters.my_adapter") # # - Expected to have top-level constants (::MyAdapter) # # namespaces.add "adapters", const: nil # # @example Adding a namespace with distinct identifiers and class constants # # Components defined within `bananas/` (e.g. bananas/banana_split.rb) will be: # # # # - Registered with the given leading identifier ("desserts.banana_split") # # - Expected to have constants within the given namespace (EatMe::Now::BananaSplit) # # namespaces.add "bananas", key: "desserts", const: "eat_me/now" # # @param path [String] the path to the sub-directory of source files to which this # namespace should apply, relative to the component dir # @param key [String, nil] the leading namespace to apply to the container keys # for the components. Set `nil` for the keys to be top-level. # @param const [String, nil] the Ruby constant namespace to expect for constants # defined within the components. This should be provided in underscored string # form, e.g. "hello_there/world" for a Ruby constant of `HelloThere::World`. Set # `nil` for the constants to be top-level. # # @return [Namespace] the added namespace # # @see Namespace # # @api public def add(path, key: path, const: path) raise NamespaceAlreadyAddedError, path if namespaces.key?(path) namespaces[path] = Namespace.new(path: path, key: key, const: const) end # Adds a root component dir namespace # # @see #add # # @api public def add_root(key: nil, const: nil) add(Namespace::ROOT_PATH, key: key, const: const) end # Deletes the configured namespace for the given path and returns the namespace # # If no namespace was previously configured for the given path, returns nil # # @param path [String] the path for the namespace # # @return [Namespace, nil] # # @api public def delete(path) namespaces.delete(path) end # Deletes the configured root namespace and returns the namespace # # If no root namespace was previously configured, returns nil # # @return [Namespace, nil] # # @api public def delete_root delete(Namespace::ROOT_PATH) end # Returns the paths of the configured namespaces # # @return [Array] the namespace paths, with nil representing the root # namespace # # @api public def paths namespaces.keys end # Returns the count of configured namespaces # # @return [Integer] # # @api public def length namespaces.length end alias_method :size, :length # Returns true if there are no configured namespaces # # @return [Boolean] # # @api public def empty? namespaces.empty? end # Returns the configured namespaces as an array # # Adds a default root namespace to the end of the array if one was not added # explicitly. This fallback ensures that all components in the component dir can # be loaded. # # @return [Array] the namespaces # # @api public def to_a namespaces.values.tap do |arr| arr << Namespace.default_root unless arr.any?(&:root?) end end # Calls the given block once for each configured namespace, passing the namespace # as an argument. # # @yieldparam namespace [Namespace] the yielded namespace # # @api public def each(&) to_a.each(&) end end end end end ================================================ FILE: lib/dry/system/constants.rb ================================================ # frozen_string_literal: true module Dry module System include Dry::Core::Constants RB_EXT = ".rb" RB_GLOB = "*.rb" PATH_SEPARATOR = File::SEPARATOR KEY_SEPARATOR = "." WORD_REGEX = /\w+/ end end ================================================ FILE: lib/dry/system/container.rb ================================================ # frozen_string_literal: true require "pathname" require "dry/configurable" require "dry/auto_inject" require "dry/inflector" module Dry module System # Abstract container class to inherit from # # Container class is treated as a global registry with all system components. # Container can also import dependencies from other containers, which is # useful in complex systems that are split into sub-systems. # # Container can be finalized, which triggers loading of all the defined # components within a system, after finalization it becomes frozen. This # typically happens in cases like booting a web application. # # Before finalization, Container can lazy-load components on demand. A # component can be a simple class defined in a single file, or a complex # component which has init/start/stop lifecycle, and it's defined in a boot # file. Components which specify their dependencies using Import module can # be safely required in complete isolation, and Container will resolve and # load these dependencies automatically. # # Furthermore, Container supports auto-registering components based on # dir/file naming conventions. This reduces a lot of boilerplate code as all # you have to do is to put your classes under configured directories and # their instances will be automatically registered within a container. # # Every container needs to be configured with following settings: # # * `:name` - a unique container name # * `:root` - a system root directory (defaults to `pwd`) # # @example # class MyApp < Dry::System::Container # configure do |config| # config.name = :my_app # # # this will auto-register classes from 'lib/components'. ie if you add # # `lib/components/repo.rb` which defines `Repo` class, then it's # # instance will be automatically available as `MyApp['repo']` # config.auto_register = %w(lib/components) # end # # # this will configure $LOAD_PATH to include your `lib` dir # add_dirs_to_load_paths!('lib') # end # # @api public class Container extend Dry::Core::Container::Mixin extend Dry::System::Plugins setting :name setting :root, default: Pathname.pwd.freeze, constructor: ->(path) { Pathname(path) } setting :provider_dirs, default: ["system/providers"] setting :registrations_dir, default: "system/registrations" setting :component_dirs, default: Config::ComponentDirs.new, cloneable: true setting :exports, reader: true setting :inflector, default: Dry::Inflector.new setting :auto_registrar, default: Dry::System::AutoRegistrar setting :manifest_registrar, default: Dry::System::ManifestRegistrar setting :provider_registrar, default: Dry::System::ProviderRegistrar setting :importer, default: Dry::System::Importer # Expect "." as key namespace separator. This is not intended to be user-configurable. config.namespace_separator = KEY_SEPARATOR class << self # @!method config # Returns the configuration for the container # # @example # container.config.root = "/path/to/app" # container.config.root # => # # # @return [Dry::Configurable::Config] # # @api public # Yields a configuration object for the container, which you can use to modify the # configuration, then runs the after-`configured` hooks and finalizes (freezes) # the {config}. # # Does not finalize the config when given `finalize_config: false` # # @example # class MyApp < Dry::System::Container # configure do |config| # config.root = Pathname("/path/to/app") # config.name = :my_app # end # end # # @param finalize_config [Boolean] # # @return [self] # # @see after # # @api public def configure(finalize_config: true, &) super(&) configured!(finalize_config: finalize_config) end # Marks the container as configured, runs the after-`configured` hooks, then # finalizes (freezes) the {config}. # # This method is useful to call if you're modifying the container's {config} # directly, rather than via the config object yielded when calling {configure}. # # Does not finalize the config if given `finalize_config: false`. # # @param finalize_config [Boolean] # # @return [self] # # @see after # # @api public def configured!(finalize_config: true) return self if configured? hooks[:after_configure].each { |hook| instance_eval(&hook) } _configurable_finalize! if finalize_config @__configured__ = true self end # Finalizes the config for this container # # @api private alias_method :_configurable_finalize!, :finalize! def configured? @__configured__.equal?(true) end # Registers another container for import # # @example # # system/container.rb # require "dry/system" # require "logger" # # class Core < Dry::System::Container # register("logger", Logger.new($stdout)) # end # # # apps/my_app/system/container.rb # require 'system/container' # # class MyApp < Dry::System::Container # import(from: Core, as: :core) # end # # MyApp.import(keys: ["logger"], from: Core, as: :core2) # # MyApp["core.logger"].info("Test") # MyApp["core2.logger"].info("Test2") # # @param keys [Array] Keys for the components to import # @param from [Class] The container to import from # @param as [Symbol] Namespace to use for the components of the imported container # # @raise [Dry::System::ContainerAlreadyFinalizedError] if the container has already # been finalized # # @api public def import(from:, as:, keys: nil) raise Dry::System::ContainerAlreadyFinalizedError if finalized? importer.register(container: from, namespace: as, keys: keys) self end # @overload register_provider(name, namespace: nil, from: nil, source: nil, if: true, &block) # Registers a provider and its lifecycle hooks # # By convention, you should place a file for each provider in one of the # configured `provider_dirs`, and they will be loaded on demand when components # are loaded in isolation, or during container finalization. # # @example # # system/container.rb # class MyApp < Dry::System::Container # configure do |config| # config.root = Pathname("/path/to/app") # end # end # # # system/providers/db.rb # # # # Simple provider registration # MyApp.register_provider(:db) do # start do # require "db" # register("db", DB.new) # end # end # # # system/providers/db.rb # # # # Provider registration with lifecycle triggers # MyApp.register_provider(:db) do |container| # init do # require "db" # DB.configure(ENV["DB_URL"]) # container.register("db", DB.new) # end # # start do # container["db"].establish_connection # end # # stop do # container["db"].close_connection # end # end # # # system/providers/db.rb # # # # Provider registration which uses another provider # MyApp.register_provider(:db) do |container| # start do # use :logger # # require "db" # DB.configure(ENV['DB_URL'], logger: logger) # container.register("db", DB.new) # end # end # # # system/providers/db.rb # # # # Provider registration under a namespace. This will register the # # db object with the "persistence.db" key # MyApp.register_provider(:persistence, namespace: "db") do # start do # require "db" # DB.configure(ENV["DB_URL"]) # register("db", DB.new) # end # end # # @param name [Symbol] a unique name for the provider # @param namespace [String, nil] the key namespace to use for any registrations # made during the provider's lifecycle # @param from [Symbol, nil] the group for the external provider source (with the # provider source name inferred from `name` or passsed explicitly as # `source:`) # @param source [Symbol, nil] the name of the external provider source to use # (if different from the value provided as `name`) # @param if [Boolean] a boolean to determine whether to register the provider # # @see Provider # @see Provider::Source # # @return [self] # # @api public def register_provider(...) providers.register_provider(...) end # Return if a container was finalized # # @return [TrueClass, FalseClass] # # @api public def finalized? @__finalized__.equal?(true) end # Finalizes the container # # This triggers importing components from other containers, booting # registered components and auto-registering components. It should be # called only in places where you want to finalize your system as a # whole, ie when booting a web application # # @example # # system/container.rb # class MyApp < Dry::System::Container # configure do |config| # config.root = Pathname("/path/to/app") # config.name = :my_app # config.auto_register = %w(lib/apis lib/core) # end # end # # # You can put finalization file anywhere you want, ie system/boot.rb # MyApp.finalize! # # # If you need last-moment adjustments just before the finalization # # you can pass a block and do it there # MyApp.finalize! do |container| # # stuff that only needs to happen for finalization # end # # @return [self] frozen container # # @api public def finalize!(freeze: true, &) return self if finalized? configured! run_hooks(:finalize) do yield(self) if block_given? [providers, auto_registrar, manifest_registrar, importer].each(&:finalize!) @__finalized__ = true end self.freeze if freeze self end # Starts a provider # # As a result, the provider's `prepare` and `start` lifecycle triggers are called # # @example # MyApp.start(:persistence) # # @param name [Symbol] the name of a registered provider to start # # @return [self] # # @api public def start(name) providers.start(name) self end # Prepares a provider using its `prepare` lifecycle trigger # # Preparing (as opposed to starting) a provider is useful in places where some # aspects of a heavier dependency are needed, but its fully started environment # # @example # MyApp.prepare(:persistence) # # @param name [Symbol] The name of the registered provider to prepare # # @return [self] # # @api public def prepare(name) providers.prepare(name) self end # Stop a specific component but calls only `stop` lifecycle trigger # # @example # MyApp.stop(:persistence) # # @param name [Symbol] The name of a registered bootable component # # @return [self] # # @api public def stop(name) providers.stop(name) self end # @api public def shutdown! providers.shutdown self end # Adds the directories (relative to the container's root) to the Ruby load path # # @example # class MyApp < Dry::System::Container # configure do |config| # # ... # end # # add_to_load_path!('lib') # end # # @param dirs [Array] # # @return [self] # # @api public def add_to_load_path!(*dirs) dirs.reverse.map(&root.method(:join)).each do |path| $LOAD_PATH.prepend(path.to_s) unless $LOAD_PATH.include?(path.to_s) end self end # @api public def load_registrations!(name) manifest_registrar.(name) self end # Builds injector for this container # # An injector is a useful mixin which injects dependencies into # automatically defined constructor. # # @example # # Define an injection mixin # # # # system/import.rb # Import = MyApp.injector # # # Use it in your auto-registered classes # # # # lib/user_repo.rb # require 'import' # # class UserRepo # include Import['persistence.db'] # end # # MyApp['user_repo].db # instance under 'persistence.db' key # # @param options [Hash] injector options # # @api public def injector(**options) Dry::AutoInject(self, **options) end # Requires one or more files relative to the container's root # # @example # # single file # MyApp.require_from_root('lib/core') # # # glob # MyApp.require_from_root('lib/**/*') # # @param paths [Array] one or more paths, supports globs too # # @api public def require_from_root(*paths) paths.flat_map { |path| path.to_s.include?("*") ? ::Dir[root.join(path)] : root.join(path) }.each { |path| Kernel.require path.to_s } end # Returns container's root path # # @example # class MyApp < Dry::System::Container # configure do |config| # config.root = Pathname('/my/app') # end # end # # MyApp.root # returns '/my/app' pathname # # @return [Pathname] # # @api public def root config.root end # @api public def register(key, *) super hooks[:after_register].each { |hook| instance_exec(key, &hook) } self end # @api public def resolve(key) load_component(key) unless finalized? super end alias_method :registered?, :key? # # @!method registered?(key) # Whether a +key+ is registered (doesn't trigger loading) # @param key [String,Symbol] The key # @return [Boolean] # @api public # # Check if identifier is registered. # If not, try to load the component # # @param key [String,Symbol] Identifier # @return [Boolean] # # @api public def key?(key) if finalized? registered?(key) else registered?(key) || resolve(key) { return false } true end end # @api private def component_dirs config.component_dirs.to_a.map { |dir| ComponentDir.new(config: dir, container: self) } end # @api private def providers @providers ||= config.provider_registrar.new(self) end # @api private def auto_registrar @auto_registrar ||= config.auto_registrar.new(self) end # @api private def manifest_registrar @manifest_registrar ||= config.manifest_registrar.new(self) end # @api private def importer @importer ||= config.importer.new(self) end # Registers a callback hook to run before container lifecycle events. # # Currently, the only supported event is `:finalized`. This hook is called when # you run `{finalize!}`. # # When the given block is called, `self` is the container class, and no block # arguments are given. # # @param event [Symbol] the event name # @param block [Proc] the callback hook to run # # @return [self] # # @api public def before(event, &block) hooks[:"before_#{event}"] << block self end # Registers a callback hook to run after container lifecycle events. # # The supported events are: # # - `:configured`, called when you run {configure} or {configured!}, or when # running {finalize!} and neither of the prior two methods have been called. # - `:finalized`, called when you run {finalize!}. # # When the given block is called, `self` is the container class, and no block # arguments are given. # # @param event [Symbol] the event name # @param block [Proc] the callback hook to run # # @return [self] # # @api public def after(event, &block) hooks[:"after_#{event}"] << block self end # @api private def hooks @hooks ||= Hash.new { |h, k| h[k] = [] } end # @api private def inherited(klass) hooks.each do |event, blocks| klass.hooks[event].concat blocks.dup end klass.instance_variable_set(:@__configured__, false) klass.instance_variable_set(:@__finalized__, false) super end protected # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity # @api private def load_component(key) return self if registered?(key) if (provider = providers[key]) provider.start return self end component = find_component(key) providers[component.root_key]&.start return self if registered?(key) if component.loadable? load_local_component(component) elsif manifest_registrar.file_exists?(component) manifest_registrar.(component) elsif importer.namespace?(component.root_key) load_imported_component(component.identifier, namespace: component.root_key) elsif importer.namespace?(nil) load_imported_component(component.identifier, namespace: nil) end self end # rubocop:enable Metrics/AbcSize, Metrics/PerceivedComplexity private def load_local_component(component) if component.auto_register? register(component.identifier, memoize: component.memoize?) { component.instance } end end def load_imported_component(identifier, namespace:) return unless importer.namespace?(namespace) import_key = identifier.namespaced(from: namespace, to: nil).key importer.import(namespace, keys: [import_key]) end def find_component(key) # Find the first matching component from within the configured component dirs. # If no matching component is found, return a null component; this fallback is # important because the component may still be loadable via the manifest # registrar or an imported container. component_dirs.detect { |dir| if (component = dir.component_for_key(key)) break component end } || IndirectComponent.new(Identifier.new(key)) end def run_hooks(event) hooks[:"before_#{event}"].each { instance_eval(&_1) } yield hooks[:"after_#{event}"].each { instance_eval(&_1) } end end # Default hooks after :configure do # Add appropriately configured component dirs to the load path # # Do this in a single pass to preserve ordering (i.e. earliest dirs win) paths = config.component_dirs.to_a.each_with_object([]) { |dir, arr| arr << dir.path if dir.add_to_load_path } add_to_load_path!(*paths) end end end end ================================================ FILE: lib/dry/system/errors.rb ================================================ # frozen_string_literal: true module Dry module System # Error raised when import is called on an already finalized container # # @api public ContainerAlreadyFinalizedError = Class.new(StandardError) # Error raised when a component dir is added to configuration more than once # # @api public ComponentDirAlreadyAddedError = Class.new(StandardError) do def initialize(dir) super("Component directory #{dir.inspect} already added") end end # Error raised when a configured component directory could not be found # # @api public ComponentDirNotFoundError = Class.new(StandardError) do def initialize(dir) super("Component dir '#{dir}' not found") end end # Error raised when a namespace for a component dir is added to configuration more # than once # # @api public NamespaceAlreadyAddedError = Class.new(StandardError) do def initialize(path) path_label = path ? "path #{path.inspect}" : "root path" super("Namespace for #{path_label} already added") end end # Error raised when attempting to register provider using a name that has already been # registered # # @api public ProviderAlreadyRegisteredError = Class.new(ArgumentError) do def initialize(provider_name) super("Provider #{provider_name.inspect} has already been registered") end end # Error raised when a named provider could not be found # # @api public ProviderNotFoundError = Class.new(ArgumentError) do def initialize(name) super("Provider #{name.inspect} not found") end end # Error raised when a named provider source could not be found # # @api public ProviderSourceNotFoundError = Class.new(StandardError) do def initialize(name:, group:, keys:) msg = "Provider source not found: #{name.inspect}, group: #{group.inspect}" key_list = keys.map { |key| "- #{key[:name].inspect}, group: #{key[:group].inspect}" } msg += "Available provider sources:\n\n#{key_list}" super(msg) end end # Error raised when trying to use a plugin that does not exist. # # @api public PluginNotFoundError = Class.new(StandardError) do def initialize(plugin_name) super("Plugin #{plugin_name.inspect} does not exist") end end # Exception raise when a plugin dependency failed to load # # @api public PluginDependencyMissing = Class.new(StandardError) do # @api private def initialize(plugin, message, gem = nil) details = gem ? "#{message} - add #{gem} to your Gemfile" : message super("dry-system plugin #{plugin.inspect} failed to load its dependencies: #{details}") end end # Exception raised when auto-registerable component is not loadable # # @api public ComponentNotLoadableError = Class.new(NameError) do # @api private def initialize(component, error, corrections: DidYouMean::ClassNameChecker.new(error).corrections) full_class_name = [error.receiver, error.name].join("::") message = [ "Component '#{component.key}' is not loadable.", "Looking for #{full_class_name}." ] if corrections.any? case_correction = corrections.find { |correction| correction.casecmp?(full_class_name) } if case_correction acronyms_needed = case_correction.split("::").difference(full_class_name.split("::")) stringified_acronyms_needed = acronyms_needed.map { |acronym| "'#{acronym}'" } .join(", ") message << <<~ERROR_MESSAGE You likely need to add: acronym(#{stringified_acronyms_needed}) to your container's inflector, since we found a #{case_correction} class. ERROR_MESSAGE else message << DidYouMean.formatter.message_for(corrections) end end super(message.join("\n")) end end end end ================================================ FILE: lib/dry/system/identifier.rb ================================================ # frozen_string_literal: true require "dry/system/constants" module Dry module System # An identifier representing a component to be registered. # # Components are eventually registered in the container using plain string # identifiers, available as the `identifier` or `key` attribute here. Additional # methods are provided to make it easier to evaluate or manipulate these identifiers. # # @api public class Identifier include Dry::Equalizer(:key) # @return [String] the identifier's string key # @api public attr_reader :key # @api private def initialize(key) @key = key.to_s end # @!method to_s # Returns the identifier string key # # @return [String] # @see #key # @api public alias_method :to_s, :key # Returns the root namespace segment of the identifier string, as a symbol # # @example # identifier.key # => "articles.operations.create" # identifier.root_key # => :articles # # @return [Symbol] the root key # @api public def root_key segments.first.to_sym end # Returns true if the given leading segments string is a leading part of the {key}. # # Also returns true if nil or an empty string is given. # # @example # identifier.key # => "articles.operations.create" # # identifier.start_with?("articles.operations") # => true # identifier.start_with?("articles") # => true # identifier.start_with?("article") # => false # identifier.start_with?(nil) # => true # # @param leading_segments [String] the one or more leading segments to check # @return [Boolean] # @api public def start_with?(leading_segments) leading_segments.to_s.empty? || key.start_with?("#{leading_segments}#{KEY_SEPARATOR}") || key.eql?(leading_segments) end # Returns true if the given trailing segments string is the end part of the {key}. # # Also returns true if nil or an empty string is given. # # @example # identifier.key # => "articles.operations.create" # # identifier.end_with?("create") # => true # identifier.end_with?("operations.create") # => true # identifier.end_with?("ate") # => false, not a whole segment # identifier.end_with?("nup") # => false, not in key at all # # @param trailing_segments [String] the one or more trailing key segments to check # @return [Boolean] # @api public def end_with?(trailing_segments) trailing_segments.to_s.empty? || key.end_with?("#{KEY_SEPARATOR}#{trailing_segments}") || key.eql?(trailing_segments) end # Returns true if the given segments string matches whole segments within the {key}. # # @example # identifier.key # => "articles.operations.create" # # identifier.include?("operations") # => true # identifier.include?("articles.operations") # => true # identifier.include?("operations.create") # => true # # identifier.include?("article") # => false, not a whole segment # identifier.include?("update") # => false, not in key at all # # @param segments [String] the one of more key segments to check # @return [Boolean] # @api public def include?(segments) return false if segments.to_s.empty? sep_re = Regexp.escape(KEY_SEPARATOR) key.match?( / (\A|#{sep_re}) #{Regexp.escape(segments)} (\Z|#{sep_re}) /x ) end # Returns the key with its segments separated by the given separator # # @example # identifier.key # => "articles.operations.create" # identifier.key_with_separator("/") # => "articles/operations/create" # # @return [String] the key using the separator # @api private def key_with_separator(separator) segments.join(separator) end # Returns a copy of the identifier with the key's leading namespace(s) replaced # # @example Changing a namespace # identifier.key # => "articles.operations.create" # identifier.namespaced(from: "articles", to: "posts").key # => "posts.commands.create" # # @example Removing a namespace # identifier.key # => "articles.operations.create" # identifier.namespaced(from: "articles", to: nil).key # => "operations.create" # # @example Adding a namespace # identifier.key # => "articles.operations.create" # identifier.namespaced(from: nil, to: "admin").key # => "admin.articles.operations.create" # # @param from [String, nil] the leading namespace(s) to replace # @param to [String, nil] the replacement for the leading namespace # # @return [Dry::System::Identifier] the copy of the identifier # # @see #initialize # @api private def namespaced(from:, to:) return self if from == to separated_to = "#{to}#{KEY_SEPARATOR}" if to new_key = if from.nil? "#{separated_to}#{key}" else key.sub( /^#{Regexp.escape(from.to_s)}#{Regexp.escape(KEY_SEPARATOR)}/, separated_to || EMPTY_STRING ) end return self if new_key == key self.class.new(new_key) end private def segments @segments ||= key.split(KEY_SEPARATOR) end end end end ================================================ FILE: lib/dry/system/importer.rb ================================================ # frozen_string_literal: true require "dry/system/constants" module Dry module System # Default importer implementation # # This is currently configured by default for every System::Container. # Importer objects are responsible for importing components from one # container to another. This is used in cases where an application is split # into multiple sub-systems. # # @api private class Importer # @api private class Item attr_reader :namespace, :container, :import_keys def initialize(namespace:, container:, import_keys:) @namespace = namespace @container = container @import_keys = import_keys end end attr_reader :container attr_reader :registry # @api private def initialize(container) @container = container @registry = {} end # @api private def register(namespace:, container:, keys: nil) registry[namespace_key(namespace)] = Item.new( namespace: namespace, container: container, import_keys: keys ) end # @api private def [](name) registry.fetch(namespace_key(name)) end # @api private def key?(name) registry.key?(namespace_key(name)) end alias_method :namespace?, :key? # @api private def finalize! registry.each_key { import(_1) } self end # @api private def import(namespace, keys: Undefined) item = self[namespace] keys = Undefined.default(keys, item.import_keys) if keys import_keys(item.container, namespace, keys_to_import(keys, item)) else import_all(item.container, namespace) end self end private # Returns nil if given a nil (i.e. root) namespace. Otherwise, converts the namespace to a # string. # # This ensures imported components are found when either symbols or strings to given as the # namespace in {Container.import}. def namespace_key(namespace) return nil if namespace.nil? namespace.to_s end def keys_to_import(keys, item) keys .then { (arr = item.import_keys) ? _1 & arr : _1 } .then { (arr = item.container.exports) ? _1 & arr : _1 } end def import_keys(other, namespace, keys) merge(container, build_merge_container(other, keys), namespace: namespace) end def import_all(other, namespace) merge_container = if other.exports build_merge_container(other, other.exports) else build_merge_container(other.finalize!, other.keys) end merge(container, merge_container, namespace: namespace) end # Merges `other` into `container`, favoring the container's existing registrations def merge(container, other, namespace:) container.merge(other, namespace: namespace) { |_key, old_item, new_item| old_item || new_item } end def build_merge_container(other, keys) keys.each_with_object(Core::Container.new) { |key, ic| next unless other.key?(key) # Access the other container's items directly so that we can preserve all their # options when we merge them with the target container (e.g. if a component in # the provider container was registered with a block, we want block registration # behavior to be exhibited when later resolving that component from the target # container). TODO: Make this part of dry-system's public API. item = other._container[key] # By default, we "protect" components that were themselves imported into the # other container from being implicitly exported; imported components are # considered "private" and must be explicitly included in `exports` to be # exported. next if item.options[:imported] && !other.exports if item.callable? ic.register(key, **item.options, imported: true, &item.item) else ic.register(key, item.item, **item.options, imported: true) end } end end end end ================================================ FILE: lib/dry/system/indirect_component.rb ================================================ # frozen_string_literal: true module Dry module System # An indirect component is a component that cannot be directly from a source file # directly managed by the container. It may be component that needs to be loaded # indirectly, either via a registration manifest file or an imported container # # Indirect components are an internal abstraction and, unlike ordinary components, are # not exposed to users via component dir configuration hooks. # # @see Container#load_component # @see Container#find_component # # @api private class IndirectComponent include Dry::Equalizer(:identifier) # @!attribute [r] identifier # @return [String] the component's unique identifier attr_reader :identifier # @api private def initialize(identifier) @identifier = identifier end # Returns false, indicating that the component is not directly loadable from the # files managed by the container # # This is the inverse of {Component#loadable?} # # @return [FalseClass] # # @api private def loadable? false end # Returns the component's unique key # # @return [String] the key # # @see Identifier#key # # @api private def key identifier.to_s end # Returns the root namespace segment of the component's key, as a symbol # # @see Identifier#root_key # # @return [Symbol] the root key # # @api private def root_key identifier.root_key end end end end ================================================ FILE: lib/dry/system/loader/autoloading.rb ================================================ # frozen_string_literal: true module Dry module System class Loader # Component loader for autoloading-enabled applications # # This behaves like the default loader, except instead of requiring the given path, # it loads the respective constant, allowing the autoloader to load the # corresponding file per its own configuration. # # @see Loader # @api public class Autoloading < Loader class << self def require!(component) constant(component) self end end end end end end ================================================ FILE: lib/dry/system/loader.rb ================================================ # frozen_string_literal: true require "dry/system/errors" module Dry module System # Default component loader implementation # # This class is configured by default for every System::Container. You can # provide your own and use it in your containers too. # # @example # class MyLoader < Dry::System::Loader # def call(*args) # constant.build(*args) # end # end # # class MyApp < Dry::System::Container # configure do |config| # # ... # config.component_dirs.loader = MyLoader # end # end # # @api public class Loader class << self # Requires the component's source file # # @api public def require!(component) require(component.require_path) self end # Returns an instance of the component # # Provided optional args are passed to object's constructor # # @param [Array] args Optional constructor args # # @return [Object] # # @api public def call(component, *args, **kwargs) require!(component) constant = self.constant(component) if singleton?(constant) constant.instance(*args, **kwargs) else constant.new(*args, **kwargs) end end # Returns the component's class constant # # @return [Class] # # @api public def constant(component) inflector = component.inflector const_name = inflector.camelize(component.const_path) inflector.constantize(const_name) rescue NameError => exception # Ensure it's this component's constant, not any other NameError within the component if exception.message =~ /#{const_name}( |\n|$)/ raise ComponentNotLoadableError.new(component, exception) else raise exception end end private def singleton?(constant) constant.respond_to?(:instance) && !constant.respond_to?(:new) end end end end end ================================================ FILE: lib/dry/system/magic_comments_parser.rb ================================================ # frozen_string_literal: true module Dry module System class MagicCommentsParser VALID_LINE_RE = /^(#.*)?$/ COMMENT_RE = /^#\s+(?[A-Za-z]{1}[A-Za-z0-9_]+):\s+(?.+?)$/ COERCIONS = { "true" => true, "false" => false }.freeze def self.call(file_name) {}.tap do |options| File.foreach(file_name) do |line| break unless line =~ VALID_LINE_RE if (comment = line.match(COMMENT_RE)) options[comment[:name].to_sym] = coerce(comment[:value]) end end end end def self.coerce(value) COERCIONS.fetch(value) { value } end end end end ================================================ FILE: lib/dry/system/manifest_registrar.rb ================================================ # frozen_string_literal: true require "dry/system/constants" module Dry module System # Default manifest registration implementation # # This is configured by default for every System::Container. The manifest registrar is # responsible for loading manifest files that contain code to manually register # certain objects with the container. # # @api private class ManifestRegistrar # @api private attr_reader :container # @api private attr_reader :config # @api private def initialize(container) @container = container @config = container.config end # @api private def finalize! ::Dir[registrations_dir.join(RB_GLOB)].each do |file| call(Identifier.new(File.basename(file, RB_EXT))) end end # @api private def call(component) load(root.join(config.registrations_dir, "#{component.root_key}#{RB_EXT}")) end # @api private def file_exists?(component) ::File.exist?(::File.join(registrations_dir, "#{component.root_key}#{RB_EXT}")) end private # @api private def registrations_dir root.join(config.registrations_dir) end # @api private def root container.root end end end end ================================================ FILE: lib/dry/system/plugins/bootsnap.rb ================================================ # frozen_string_literal: true module Dry module System module Plugins module Bootsnap DEFAULT_OPTIONS = { load_path_cache: true, compile_cache_iseq: true, compile_cache_yaml: true }.freeze # @api private def self.extended(system) super system.use(:env) system.setting :bootsnap, default: DEFAULT_OPTIONS system.after(:configure, &:setup_bootsnap) end # @api private def self.dependencies {bootsnap: "bootsnap"} end # Set up bootsnap for faster booting # # @api public def setup_bootsnap return unless bootsnap_available? ::Bootsnap.setup(**config.bootsnap, cache_dir: root.join("tmp/cache").to_s) end # @api private def bootsnap_available? spec = Gem.loaded_specs["bootsnap"] or return false RUBY_ENGINE == "ruby" && spec.match_platform(RUBY_PLATFORM) && spec.required_ruby_version.satisfied_by?(Gem::Version.new(RUBY_VERSION)) end end end end end ================================================ FILE: lib/dry/system/plugins/dependency_graph/strategies.rb ================================================ # frozen_string_literal: true module Dry module System module Plugins module DependencyGraph # @api private class Strategies extend Core::Container::Mixin # @api private class Kwargs < Dry::AutoInject::Strategies::Kwargs private # @api private def define_initialize(klass) @container["notifications"].instrument( :resolved_dependency, dependency_map: dependency_map.to_h, target_class: klass ) super end end # @api private class Args < Dry::AutoInject::Strategies::Args private # @api private def define_initialize(klass) @container["notifications"].instrument( :resolved_dependency, dependency_map: dependency_map.to_h, target_class: klass ) super end end class Hash < Dry::AutoInject::Strategies::Hash private # @api private def define_initialize(klass) @container["notifications"].instrument( :resolved_dependency, dependency_map: dependency_map.to_h, target_class: klass ) super end end register :kwargs, Kwargs register :args, Args register :hash, Hash register :default, Kwargs end end end end end ================================================ FILE: lib/dry/system/plugins/dependency_graph.rb ================================================ # frozen_string_literal: true module Dry module System module Plugins # @api public module DependencyGraph # @api private def self.extended(system) super system.instance_eval do use(:notifications) setting :dependency_graph do setting :ignored_dependencies, default: [] end after(:configure) do self[:notifications].register_event(:resolved_dependency) self[:notifications].register_event(:registered_dependency) end end end # @api private def self.dependencies {"dry-events" => "dry/events/publisher"} end # @api private def injector(**options) super(**options, strategies: DependencyGraph::Strategies) end # @api private def register(key, contents = nil, options = {}, &) super.tap do key = key.to_s unless config.dependency_graph.ignored_dependencies.include?(key) self[:notifications].instrument( :registered_dependency, key: key, class: self[key].class ) end end end end end end end ================================================ FILE: lib/dry/system/plugins/env.rb ================================================ # frozen_string_literal: true module Dry module System module Plugins # @api public class Env < Module DEFAULT_INFERRER = -> { :development } attr_reader :options # @api private def initialize(**options) @options = options super() end def inferrer options.fetch(:inferrer, DEFAULT_INFERRER) end # @api private def extended(system) system.setting :env, default: inferrer.(), reader: true super end end end end end ================================================ FILE: lib/dry/system/plugins/logging.rb ================================================ # frozen_string_literal: true require "logger" module Dry module System module Plugins module Logging # @api private def self.extended(system) system.instance_eval do setting :logger, reader: true setting :log_dir, default: "log" setting :log_levels, default: { development: Logger::DEBUG, test: Logger::DEBUG, production: Logger::ERROR } setting :logger_class, default: ::Logger, reader: true end system.after(:configure, &:register_logger) super end # Set a logger # # This is invoked automatically when a container is being configured # # @return [self] # # @api private def register_logger if registered?(:logger) self elsif config.logger register(:logger, config.logger) else config.logger = config.logger_class.new(log_file_path) config.logger.level = log_level register(:logger, config.logger) self end end # @api private def log_level config.log_levels.fetch(config.env, Logger::ERROR) end # @api private def log_dir_path root.join(config.log_dir).realpath end # @api private def log_file_path log_dir_path.join(log_file_name) end # @api private def log_file_name "#{config.env}.log" end end end end end ================================================ FILE: lib/dry/system/plugins/monitoring/proxy.rb ================================================ # frozen_string_literal: true require "delegate" module Dry module System module Plugins module Monitoring # @api private class Proxy < SimpleDelegator # @api private def self.for(target, key:, methods: []) monitored_methods = if methods.empty? target.public_methods - Object.public_instance_methods else methods end Class.new(self) do extend Dry::Core::ClassAttributes include Dry::Events::Publisher[target.class.name] defines :monitored_methods attr_reader :__notifications__ monitored_methods(monitored_methods) monitored_methods.each do |meth| define_method(meth) do |*args, **kwargs, &block| object = __getobj__ opts = {target: key, object: object, method: meth, args: args, kwargs: kwargs} __notifications__.instrument(:monitoring, opts) do object.public_send(meth, *args, **kwargs, &block) end end end end end def initialize(target, notifications) super(target) @__notifications__ = notifications end end end end end end ================================================ FILE: lib/dry/system/plugins/monitoring.rb ================================================ # frozen_string_literal: true require "dry/system/constants" module Dry module System module Plugins # @api public module Monitoring # @api private def self.extended(system) super system.use(:notifications) system.after(:configure) do self[:notifications].register_event(:monitoring) end end # @api private def self.dependencies {"dry-events": "dry/events/publisher"} end # @api private def monitor(key, **options, &block) notifications = self[:notifications] resolve(key).tap do |target| proxy = Proxy.for(target, **options, key: key) if block_given? proxy.monitored_methods.each do |meth| notifications.subscribe(:monitoring, target: key, method: meth, &block) end end decorate(key, with: -> tgt { proxy.new(tgt, notifications) }) end end end end end end ================================================ FILE: lib/dry/system/plugins/notifications.rb ================================================ # frozen_string_literal: true module Dry module System module Plugins # @api public module Notifications # @api private def self.extended(system) system.after(:configure, &:register_notifications) end # @api private def self.dependencies {"dry-monitor": "dry/monitor"} end # @api private def register_notifications return self if registered?(:notifications) register(:notifications, Monitor::Notifications.new(config.name)) end end end end end ================================================ FILE: lib/dry/system/plugins/plugin.rb ================================================ # frozen_string_literal: true module Dry module System module Plugins # @api private class Plugin attr_reader :name attr_reader :mod attr_reader :block # @api private def initialize(name, mod, &block) @name = name @mod = mod @block = block end # @api private def apply_to(system, **options) system.extend(stateful? ? mod.new(**options) : mod) system.instance_eval(&block) if block system end # @api private def load_dependencies(dependencies = mod_dependencies, gem = nil) Array(dependencies).each do |dependency| if dependency.is_a?(Array) || dependency.is_a?(Hash) dependency.each { |value| load_dependencies(*Array(value).reverse) } elsif !Plugins.loaded_dependencies.include?(dependency.to_s) load_dependency(dependency, gem) end end end # @api private def load_dependency(dependency, gem) Kernel.require dependency Plugins.loaded_dependencies << dependency.to_s rescue LoadError => exception raise PluginDependencyMissing.new(name, exception.message, gem) end # @api private def stateful? mod < Module end # @api private def mod_dependencies return EMPTY_ARRAY unless mod.respond_to?(:dependencies) mod.dependencies.is_a?(Array) ? mod.dependencies : [mod.dependencies] end end end end end ================================================ FILE: lib/dry/system/plugins/zeitwerk/compat_inflector.rb ================================================ # frozen_string_literal: true module Dry module System module Plugins class Zeitwerk < Module # @api private class CompatInflector attr_reader :config def initialize(config) @config = config end def camelize(string, _) config.inflector.camelize(string) end end end end end end ================================================ FILE: lib/dry/system/plugins/zeitwerk.rb ================================================ # frozen_string_literal: true require "dry/system/constants" module Dry module System module Plugins # @api private class Zeitwerk < Module # @api private def self.dependencies [ "dry/system/loader/autoloading", "dry/system/plugins/zeitwerk/compat_inflector", {"zeitwerk" => "zeitwerk"} ] end # @api private attr_reader :loader, :run_setup, :eager_load, :debug # @api private def initialize(loader: nil, run_setup: true, eager_load: nil, debug: false) @loader = loader || ::Zeitwerk::Loader.new @run_setup = run_setup @eager_load = eager_load @debug = debug super() end # @api private def extended(system) system.setting :autoloader, reader: true system.config.autoloader = loader system.config.component_dirs.loader = Dry::System::Loader::Autoloading system.config.component_dirs.add_to_load_path = false system.after(:configure, &method(:setup_autoloader)) super end private def setup_autoloader(system) configure_loader(system.autoloader, system) push_component_dirs_to_loader(system, system.autoloader) system.autoloader.setup if run_setup system.after(:finalize) { system.autoloader.eager_load } if eager_load?(system) system end # Build a zeitwerk loader with the configured component directories # # @return [Zeitwerk::Loader] def configure_loader(loader, system) loader.tag = system.config.name || system.name unless loader.tag loader.inflector = CompatInflector.new(system.config) loader.logger = method(:puts) if debug end # Add component dirs to the zeitwerk loader # # @return [Zeitwerk::Loader] def push_component_dirs_to_loader(system, loader) system.config.component_dirs.each do |dir| dir.namespaces.each do |ns| loader.push_dir( system.root.join(dir.path, ns.path.to_s), namespace: module_for_namespace(ns, system.config.inflector) ) end end loader end def module_for_namespace(namespace, inflector) return Object unless namespace.const begin inflector.constantize(inflector.camelize(namespace.const)) rescue NameError namespace.const.split(PATH_SEPARATOR).reduce(Object) { |parent_mod, mod_path| get_or_define_module(parent_mod, inflector.camelize(mod_path)) } end end def get_or_define_module(parent_mod, name) parent_mod.const_get(name) rescue NameError parent_mod.const_set(name, Module.new) end def eager_load?(system) return eager_load unless eager_load.nil? system.config.respond_to?(:env) && system.config.env == :production end end end end end ================================================ FILE: lib/dry/system/plugins.rb ================================================ # frozen_string_literal: true module Dry module System module Plugins # Register a plugin # # @param [Symbol] name The name of a plugin # @param [Class] plugin Plugin module # # @return [Plugins] # # @api public def self.register(name, plugin, &) registry[name] = Plugin.new(name, plugin, &) end # @api private def self.registry @registry ||= {} end # @api private def self.loaded_dependencies @loaded_dependencies ||= [] end # Enables a plugin if not already enabled. # Raises error if plugin cannot be found in the plugin registry. # # @param [Symbol] name The plugin name # @param [Hash] options Plugin options # # @return [self] # # @api public def use(name, **options) return self if enabled_plugins.include?(name) raise PluginNotFoundError, name unless (plugin = Dry::System::Plugins.registry[name]) plugin.load_dependencies plugin.apply_to(self, **options) enabled_plugins << name self end # @api private def inherited(klass) klass.instance_variable_set(:@enabled_plugins, enabled_plugins.dup) super end # @api private def enabled_plugins @enabled_plugins ||= [] end register(:bootsnap, Plugins::Bootsnap) register(:logging, Plugins::Logging) register(:env, Plugins::Env) register(:notifications, Plugins::Notifications) register(:monitoring, Plugins::Monitoring) register(:dependency_graph, Plugins::DependencyGraph) register(:zeitwerk, Plugins::Zeitwerk) end end end ================================================ FILE: lib/dry/system/provider/source.rb ================================================ # frozen_string_literal: true module Dry module System class Provider # A provider's source provides the specific behavior for a given provider to serve # its purpose. # # Sources should be subclasses of `Dry::System::Source::Provider`, with instance # methods for each lifecycle step providing their behavior: {#prepare}, {#start}, # and {#stop}. # # Inside each of these methods, you should create and configure your provider's # objects as required, and then {#register} them with the {#provider_container}. # When the provider's lifecycle steps are run (via {Dry::System::Provider}), these # registered components will be merged into the target container. # # You can prepare a provider's source in two ways: # # 1. Passing a bock when registering the provider, which is then evaluated via # {Dry::System::Provider::SourceDSL} to prepare the provider subclass. This # approach is easiest for simple providers. # 2. Manually creare your own subclass of {Dry::System::Provider} and implement your # own instance methods for the lifecycle steps (you should not implement your own # `#initialize`). This approach may be useful for more complex providers. # # @see Dry::System::Container.register_provider # @see Dry::System.register_provider_source # @see Dry::System::Source::ProviderDSL # # @api public class Source class << self # Returns a new Dry::System::Provider::Source subclass with its behavior supplied by the # given block, which is evaluated using Dry::System::Provider::SourceDSL. # # @see Dry::System::Provider::SourceDSL # # @api private def for(name:, group: nil, superclass: nil, &block) superclass ||= self ::Class.new(superclass) { |klass| klass.source_name name klass.source_group group name_with_group = group ? "#{group}->#{name}" : name klass.instance_eval(<<~RUBY, __FILE__, __LINE__ + 1) def name # def name "#{superclass.name}[#{name_with_group}]" # "CustomSource[custom]" end # end RUBY SourceDSL.evaluate(klass, &block) if block_given? } end def inherited(subclass) super # Include Dry::Configurable only when first subclassing to ensure that # distinct Source subclasses do not share settings. # # The superclass check here allows deeper Source class hierarchies to be # created without running into a Dry::Configurable::AlreadyIncluded error. if subclass.superclass == Source subclass.include Dry::Configurable end end # @api private def to_s "#<#{name}>" end # @api private def inspect to_s end end CALLBACK_MAP = Hash.new { |h, k| h[k] = [] }.freeze extend Dry::Core::ClassAttributes defines :source_name, :source_group # @api private attr_reader :callbacks # Returns the provider's own container for the provider. # # This container is namespaced based on the provider's `namespace:` configuration. # # Registered components in this container will be merged into the target container # after the `prepare` and `start` lifecycle steps. # # @return [Dry::Container] # # @see #target_container # @see Dry::System::Provider # # @api public attr_reader :provider_container alias_method :container, :provider_container # Returns the target container for the provider. # # This is the container with which the provider is registered (via # {Dry::System::Container.register_provider}). # # Registered components from the provider's container will be merged into this # container after the `prepare` and `start` lifecycle steps. # # @return [Dry::System::Container] # # @see #provider_container # @see Dry::System::Provider # # @api public attr_reader :target_container # @see #target_container # @api public def target = target_container # @api private def initialize(provider_container:, target_container:, &) super() @callbacks = {before: CALLBACK_MAP.dup, after: CALLBACK_MAP.dup} @provider_container = provider_container @target_container = target_container instance_exec(&) if block_given? end # Returns a string containing a human-readable representation of the provider. # # @return [String] # # @api private def inspect ivars = instance_variables.map { |ivar| "#{ivar}=#{instance_variable_get(ivar).inspect}" }.join(" ") "#<#{self.class.name} #{ivars}>" end # Runs the behavior for the "prepare" lifecycle step. # # This should be implemented by your source subclass or specified by # `SourceDSL#prepare` when registering a provider using a block. # # @return [void] # # @see SourceDSL#prepare # # @api public def prepare; end # Runs the behavior for the "start" lifecycle step. # # This should be implemented by your source subclass or specified by # `SourceDSL#start` when registering a provider using a block. # # You can presume that {#prepare} has already run by the time this method is # called. # # @return [void] # # @see SourceDSL#start # # @api public def start; end # Runs the behavior for the "stop" lifecycle step. # # This should be implemented by your source subclass or specified by # `SourceDSL#stop` when registering a provider using a block. # # You can presume that {#prepare} and {#start} have already run by the time this # method is called. # # @return [void] # # @see SourceDSL#stop # # @api public def stop; end # Registers a "before" callback for the given lifecycle step. # # The given block will be run before the lifecycle step method is run. The block # will be evaluated in the context of the instance of this source. # # @param step_name [Symbol] # @param block [Proc] the callback block # # @return [self] # # @see #after # # @api public def before(step_name, &block) callbacks[:before][step_name] << block self end # Registers an "after" callback for the given lifecycle step. # # The given block will be run after the lifecycle step method is run. The block # will be evaluated in the context of the instance of this source. # # @param step_name [Symbol] # @param block [Proc] the callback block # # @return [self] # # @see #before # # @api public def after(step_name, &block) callbacks[:after][step_name] << block self end # @api private def run_callback(hook, step) callbacks[hook][step].each do |callback| instance_eval(&callback) end end private # Registers a component in the provider container. # # When the provider's lifecycle steps are run (via {Dry::System::Provider}), these # registered components will be merged into the target container. # # @return [Dry::Container] the provider container # # @api public def register(...) provider_container.register(...) end # Resolves a previously registered component from the provider container. # # @param key [String] the key for the component to resolve # # @return [Object] the previously registered component # # @api public def resolve(key) provider_container.resolve(key) end # @api private def run_step_block(step_name) step_block = self.class.step_blocks[step_name] instance_eval(&step_block) if step_block end # @api private def method_missing(name, *args, &) if container.key?(name) container[name] else super end end # @api private def respond_to_missing?(name, include_all = false) container.key?(name) || super end end end end end ================================================ FILE: lib/dry/system/provider/source_dsl.rb ================================================ # frozen_string_literal: true module Dry module System class Provider # Configures a Dry::System::Provider::Source subclass using a DSL that makes it # nicer to define source behaviour via a single block. # # @see Dry::System::Container.register_provider # # @api private class SourceDSL def self.evaluate(source_class, &) new(source_class).instance_eval(&) end attr_reader :source_class def initialize(source_class) @source_class = source_class end def setting(...) source_class.setting(...) end def prepare(&) source_class.define_method(:prepare, &) end def start(&) source_class.define_method(:start, &) end def stop(&) source_class.define_method(:stop, &) end private def method_missing(name, ...) if source_class.respond_to?(name) source_class.public_send(name, ...) else super end end def respond_to_missing?(name, include_all = false) source_class.respond_to?(name, include_all) || super end end end end end ================================================ FILE: lib/dry/system/provider.rb ================================================ # frozen_string_literal: true require "dry/system/constants" module Dry module System # Providers can prepare and register one or more objects and typically work with third # party code. A typical provider might be for a database library, or an API client. # # The particular behavior for any provider is defined in a {Provider::Source}, which # is a subclass created when you run {Container.register_provider} or # {Dry::System.register_provider_source}. The Source provides this behavior through # methods for each of the steps in the provider lifecycle: `prepare`, `start`, and # `run`. These methods typically create and configure various objects, then register # them with the {#provider_container}. # # The Provider manages this lifecycle by implementing common behavior around the # lifecycle steps, such as running step callbacks, and only running steps when # appropriate for the current status of the lifecycle. # # Providers can be registered via {Container.register_provider}. # # @example Simple provider # class App < Dry::System::Container # register_provider(:logger) do # prepare do # require "logger" # end # # start do # register(:logger, Logger.new($stdout)) # end # end # end # # App[:logger] # returns configured logger # # @example Using an external Provider Source # class App < Dry::System::Container # register_provider(:logger, from: :some_external_provider_source) do # configure do |config| # config.log_level = :debug # end # # after :start do # register(:my_extra_logger, resolve(:logger)) # end # end # end # # App[:my_extra_logger] # returns the extra logger registered in the callback # # @api public class Provider # Returns the provider's unique name. # # @return [Symbol] # # @api public attr_reader :name # Returns the default namespace for the provider's container keys. # # @return [Symbol,String] # # @api public attr_reader :namespace # Returns an array of lifecycle steps that have been run. # # @return [Array] # # @example # provider.statuses # => [:prepare, :start] # # @api public attr_reader :statuses # Returns the name of the currently running step, if any. # # @return [Symbol, nil] # # @api private attr_reader :step_running private :step_running # Returns the container for the provider. # # This is where the provider's source will register its components, which are then # later marged into the target container after the `prepare` and `start` lifecycle # steps. # # @return [Dry::Core::Container] # # @api public attr_reader :provider_container alias_method :container, :provider_container # Returns the target container for the provider. # # This is the container with which the provider is registered (via # {Dry::System::Container.register_provider}). # # Registered components from the provider's container will be merged into this # container after the `prepare` and `start` lifecycle steps. # # @return [Dry::System::Container] # # @api public attr_reader :target_container alias_method :target, :target_container # Returns the provider's source # # The source provides the specific behavior for the provider via methods # implementing the lifecycle steps. # # The provider's source is defined when registering a provider with the container, # or an external provider source. # # @see Dry::System::Container.register_provider # @see Dry::System.register_provider_source # # @return [Dry::System::Provider::Source] # # @api private attr_reader :source # @api private # rubocop:disable Style/KeywordParametersOrder def initialize(name:, namespace: nil, target_container:, source_class:, source_options: {}, &) @name = name @namespace = namespace @target_container = target_container @provider_container = build_provider_container @statuses = [] @step_running = nil @source = source_class.new( **source_options, provider_container: provider_container, target_container: target_container, & ) end # rubocop:enable Style/KeywordParametersOrder # Runs the `prepare` lifecycle step. # # Also runs any callbacks for the step, and then merges any registered components # from the provider container into the target container. # # @return [self] # # @api public def prepare run_step(:prepare) end # Runs the `start` lifecycle step. # # Also runs any callbacks for the step, and then merges any registered components # from the provider container into the target container. # # @return [self] # # @api public def start run_step(:prepare) run_step(:start) end # Runs the `stop` lifecycle step. # # Also runs any callbacks for the step. # # @return [self] # # @api public def stop return self unless started? run_step(:stop) end # Returns true if the provider's `prepare` lifecycle step has run # # @api public def prepared? statuses.include?(:prepare) end # Returns true if the provider's `start` lifecycle step has run # # @api public def started? statuses.include?(:start) end # Returns true if the provider's `stop` lifecycle step has run # # @api public def stopped? statuses.include?(:stop) end private # @api private def build_provider_container container = Core::Container.new case namespace when String, Symbol container.namespace(namespace) { |c| return c } when true container.namespace(name) { |c| return c } when nil container else raise ArgumentError, "+namespace:+ must be true, string or symbol: #{namespace.inspect} given." end end # @api private def run_step(step_name) return self if step_running? || statuses.include?(step_name) @step_running = step_name source.run_callback(:before, step_name) source.public_send(step_name) source.run_callback(:after, step_name) statuses << step_name apply @step_running = nil self end # Returns true if a step is currenly running. # # This is important for short-circuiting the invocation of {#run_step} and avoiding # infinite loops if a provider step happens to result in resolution of a component # with the same root key as the provider's own name (which ordinarily results in # that provider being started). # # @return [Boolean] # # @see {#run_step} # # @api private def step_running? !!step_running end # Registers any components from the provider's container in the main container. # # Called after each lifecycle step runs. # # @return [self] # # @api private def apply provider_container.each_key do |key| next if target_container.registered?(key) # Access the provider's container items directly so that we can preserve all # their options when we merge them with the target container (e.g. if a # component in the provider container was registered with a block, we want block # registration behavior to be exhibited when later resolving that component from # the target container). TODO: Make this part of dry-system's public API. item = provider_container._container[key] if item.callable? target_container.register(key, **item.options, &item.item) else target_container.register(key, item.item, **item.options) end end self end end end end ================================================ FILE: lib/dry/system/provider_registrar.rb ================================================ # frozen_string_literal: true require "pathname" require "dry/system/errors" require "dry/system/constants" module Dry module System # Default provider registrar implementation # # This is currently configured by default for every Dry::System::Container. The # provider registrar is responsible for loading provider files and exposing an API for # running the provider lifecycle steps. # # @api public # @since 1.1.0 class ProviderRegistrar # @api private attr_reader :providers # @api private attr_reader :container # Returns the container exposed to providers as `target_container`. # # @return [Dry::System::Container] # # @api public # @since 1.1.0 alias_method :target_container, :container # @api private def initialize(container) @providers = {} @container = container end # @api private def freeze providers.freeze super end # rubocop:disable Metrics/PerceivedComplexity # @see Container.register_provider # @api private def register_provider(name, from: nil, source: nil, if: true, **provider_options, &) raise ProviderAlreadyRegisteredError, name if providers.key?(name) if from && source.is_a?(Class) raise ArgumentError, "You must supply a block when using a provider source" end if block_given? && source.is_a?(Class) raise ArgumentError, "You must supply only a `source:` option or a block, not both" end return self unless binding.local_variable_get(:if) provider = if from build_provider_from_source( name, source: source || name, group: from, options: provider_options, & ) else build_provider( name, source: source, options: provider_options, & ) end providers[provider.name] = provider self end # rubocop:enable Metrics/PerceivedComplexity # Returns a provider if it can be found or loaded, otherwise nil # # @return [Dry::System::Provider, nil] # # @api public def [](provider_name) provider_name = provider_name.to_sym if (provider = providers[provider_name]) return provider end return if finalized? require_provider_file(provider_name) providers[provider_name] end # @api public alias_method :find_and_load_provider, :[] # @api private def key?(provider_name) providers.key?(provider_name) end # Returns all provider files within the configured provider_paths. # # Searches for files in the order of the configured provider_paths. In the case of multiple # identically-named boot files within different provider_paths, the file found first will be # returned, and other matching files will be discarded. # # This method is public to allow other tools extending dry-system (like dry-rails) # to access a canonical list of real, in-use provider files. # # @see Container.provider_paths # # @return [Array] # @api public def provider_files @provider_files ||= provider_paths.each_with_object([[], []]) { |path, (provider_files, loaded)| files = ::Dir["#{path}/#{RB_GLOB}"] files.each do |file| basename = ::File.basename(file) unless loaded.include?(basename) provider_files << Pathname(file) loaded << basename end end }.first end # Extension point for subclasses to customize their # provider source superclass. Expected to be a subclass # of Dry::System::Provider::Source # # @api public # @since 1.1.0 def provider_source_class = Dry::System::Provider::Source # Extension point for subclasses to customize initialization # params for provider_source_class # # @api public # @since 1.1.0 def provider_source_options = {} # @api private def finalize! provider_files.each do |path| load_provider(path) end providers.each_value(&:start) freeze end # @!method finalized? # Returns true if the booter has been finalized # # @return [Boolean] # @api private alias_method :finalized?, :frozen? # @api private def shutdown providers.each_value(&:stop) self end # @api private def prepare(provider_name) with_provider(provider_name, &:prepare) self end # @api private def start(provider_name) with_provider(provider_name, &:start) self end # @api private def stop(provider_name) with_provider(provider_name, &:stop) self end private # @api private def provider_paths provider_dirs = container.config.provider_dirs provider_dirs.map { |dir| dir = Pathname(dir) if dir.relative? container.root.join(dir) else dir end } end def build_provider(name, options:, source: nil, &) source_class = source || Provider::Source.for( name: name, superclass: provider_source_class, & ) source_options = if source_class < provider_source_class provider_source_options else {} end Provider.new( **options, name: name, target_container: target_container, source_class: source_class, source_options: source_options ) end def build_provider_from_source(name, source:, group:, options:, &) provider_source = System.provider_sources.resolve(name: source, group: group) source_options = if provider_source.source <= provider_source_class provider_source_options else {} end Provider.new( **provider_source.provider_options, **options, name: name, target_container: target_container, source_class: provider_source.source, source_options: source_options, & ) end def with_provider(provider_name) require_provider_file(provider_name) unless providers.key?(provider_name) provider = providers[provider_name] raise ProviderNotFoundError, provider_name unless provider yield(provider) end def load_provider(path) name = Pathname(path).basename(RB_EXT).to_s.to_sym Kernel.require path unless providers.key?(name) self end def require_provider_file(name) provider_file = find_provider_file(name) Kernel.require provider_file if provider_file end def find_provider_file(name) provider_files.detect { |file| File.basename(file, RB_EXT) == name.to_s } end end end end ================================================ FILE: lib/dry/system/provider_source_registry.rb ================================================ # frozen_string_literal: true require "dry/system/constants" module Dry module System # @api private class ProviderSourceRegistry # @api private class Registration attr_reader :source attr_reader :provider_options def initialize(source:, provider_options:) @source = source @provider_options = provider_options end end attr_reader :sources def initialize @sources = {} end def load_sources(path) ::Dir[::File.join(path, "**/#{RB_GLOB}")].each do |file| require file end end def register(name:, group:, source:, provider_options:) sources[key(name, group)] = Registration.new( source: source, provider_options: provider_options ) end def register_from_block(name:, group:, provider_options:, &) register( name: name, group: group, source: Provider::Source.for(name: name, group: group, &), provider_options: provider_options ) end def resolve(name:, group:) sources[key(name, group)].tap { |source| unless source raise ProviderSourceNotFoundError.new( name: name, group: group, keys: sources.keys ) end } end private def key(name, group) {group: group, name: name} end end end end ================================================ FILE: lib/dry/system/provider_sources/settings/config.rb ================================================ # frozen_string_literal: true module Dry module System module ProviderSources # @api private module Settings InvalidSettingsError = Class.new(ArgumentError) do # @api private def initialize(errors) message = <<~STR Could not load settings. The following settings were invalid: #{setting_errors(errors).join("\n")} STR super(message) end private def setting_errors(errors) errors.sort_by { |k, _| k }.map { |key, error| "#{key}: #{error}" } end end # @api private class Config # @api private def self.load(root:, env:, loader: Loader) loader = loader.new(root: root, env: env) new.tap do |settings_obj| errors = {} settings.to_a.each do |setting| value = loader[setting.name.to_s.upcase] begin if value settings_obj.config.public_send(:"#{setting.name}=", value) else settings_obj.config[setting.name] end rescue => exception # rubocop:disable Style/RescueStandardError errors[setting.name] = exception end end raise InvalidSettingsError, errors unless errors.empty? end end include Dry::Configurable private def method_missing(name, ...) if config.respond_to?(name) config.public_send(name, ...) else super end end def respond_to_missing?(name, include_all = false) config.respond_to?(name, include_all) || super end end end end end end ================================================ FILE: lib/dry/system/provider_sources/settings/loader.rb ================================================ # frozen_string_literal: true module Dry module System module ProviderSources module Settings # @api private class Loader # @api private attr_reader :store # @api private def initialize(root:, env:, store: ENV) @store = store load_dotenv(root, env.to_sym) end # @api private def [](key) store[key] end private def load_dotenv(root, env) require "dotenv" Dotenv.load(*dotenv_files(root, env)) if defined?(Dotenv) rescue LoadError # Do nothing if dotenv is unavailable end def dotenv_files(root, env) [ File.join(root, ".env.#{env}.local"), (File.join(root, ".env.local") unless env == :test), File.join(root, ".env.#{env}"), File.join(root, ".env") ].compact end end end end end end ================================================ FILE: lib/dry/system/provider_sources/settings.rb ================================================ # frozen_string_literal: true module Dry module System module ProviderSources module Settings class Source < Dry::System::Provider::Source setting :store def prepare require "dry/system/provider_sources/settings/config" end def start register(:settings, settings.load(root: target.root, env: target.config.env)) end def settings(&block) # Save the block and evaluate it lazily to allow a provider with this source # to `require` any necessary files for the block to evaluate correctly (e.g. # requiring an app-specific types module for setting constructors) if block @settings_block = block elsif defined? @settings_class @settings_class elsif @settings_block @settings_class = Class.new(Settings::Config, &@settings_block) end end end end end end end Dry::System.register_provider_source( :settings, group: :dry_system, source: Dry::System::ProviderSources::Settings::Source ) ================================================ FILE: lib/dry/system/provider_sources.rb ================================================ # frozen_string_literal: true require "pathname" require "dry/system" Dry::System.register_provider_sources Pathname(__dir__).join("provider_sources").realpath ================================================ FILE: lib/dry/system/stubs.rb ================================================ # frozen_string_literal: true require "dry/core/container/stub" module Dry module System class Container # @api private module Stubs # This overrides default finalize! just to disable automatic freezing # of the container # # @api private def finalize!(**, &) super(freeze: false, &) end end # Enables stubbing container's components # # @example # require 'dry/system/stubs' # # MyContainer.enable_stubs! # MyContainer.finalize! # # MyContainer.stub('some.component', some_stub_object) # # @return Container # # @api public def self.enable_stubs! super extend ::Dry::System::Container::Stubs self end end end end ================================================ FILE: lib/dry/system/version.rb ================================================ # frozen_string_literal: true module Dry module System VERSION = "1.2.5" end end ================================================ FILE: lib/dry/system.rb ================================================ # frozen_string_literal: true require "zeitwerk" require "dry/core" module Dry module System # @api private def self.loader @loader ||= Zeitwerk::Loader.new.tap do |loader| root = File.expand_path("..", __dir__) loader.tag = "dry-system" loader.inflector = Zeitwerk::GemInflector.new("#{root}/dry-system.rb") loader.push_dir(root) loader.ignore( "#{root}/dry-system.rb", "#{root}/dry/system/{components,constants,errors,stubs,version}.rb" ) loader.inflector.inflect("source_dsl" => "SourceDSL") end end # Registers the provider sources in the files under the given path # # @api public def self.register_provider_sources(path) provider_sources.load_sources(path) end # Registers a provider source, which can be used as the basis for other providers # # @api public def self.register_provider_source(name, group:, source: nil, provider_options: {}, &) if source && block_given? raise ArgumentError, "You must supply only a `source:` option or a block, not both" end if source provider_sources.register( name: name, group: group, source: source, provider_options: provider_options ) else provider_sources.register_from_block( name: name, group: group, provider_options: provider_options, & ) end end # @api private def self.provider_sources @provider_sources ||= ProviderSourceRegistry.new end loader.setup end end ================================================ FILE: lib/dry-system.rb ================================================ # frozen_string_literal: true require "dry/system" ================================================ FILE: repo-sync.yml ================================================ name: gem: dry-system constant: Dry::System github_org: dry-rb ci: rubies: ["3.3.0"] gemspec: authors: ["Hanakai team"] email: ["info@hanakai.org"] summary: "Organize your code into reusable components" homepage: "https://dry-rb.org/gems/dry-system" required_ruby_version: ">= 3.1.0" development_dependencies: - bundler - rake - rspec runtime_dependencies: - [dry-auto_inject, "~> 1.1"] - [dry-configurable, "~> 1.3"] - [dry-core, "~> 1.1"] - [dry-inflector, "~> 1.1"] ================================================ FILE: spec/fixtures/app/lib/ignored_spec_service.rb ================================================ # frozen_string_literal: true class IgnoredSpecService end ================================================ FILE: spec/fixtures/app/lib/spec_service.rb ================================================ # frozen_string_literal: true class SpecService end ================================================ FILE: spec/fixtures/app/system/providers/client.rb ================================================ # frozen_string_literal: true Test::Container.register_provider(:client) do module Test class Client end end start do register(:client, Test::Client.new) end end ================================================ FILE: spec/fixtures/autoloading/lib/test/entities/foo_entity.rb ================================================ # frozen_string_literal: true module Test module Entities class FooEntity end end end ================================================ FILE: spec/fixtures/autoloading/lib/test/foo.rb ================================================ # frozen_string_literal: true module Test class Foo def call Entities::FooEntity.new end end end ================================================ FILE: spec/fixtures/components/test/bar/abc.rb ================================================ # frozen_string_literal: true module Test class Bar class ABC end end end ================================================ FILE: spec/fixtures/components/test/bar/baz.rb ================================================ # frozen_string_literal: true module Test class Bar class Baz end end end ================================================ FILE: spec/fixtures/components/test/bar.rb ================================================ # frozen_string_literal: true module Test class Bar def self.call "Welcome to my Moe's Tavern!" end end end ================================================ FILE: spec/fixtures/components/test/foo.rb ================================================ # frozen_string_literal: true module Test class Foo end end ================================================ FILE: spec/fixtures/components/test/no_register.rb ================================================ # auto_register: false # frozen_string_literal: true module Test class NoRegister end end ================================================ FILE: spec/fixtures/components_with_errors/test/constant_error.rb ================================================ # frozen_string_literal: true class ConstantError const_get(:NotHere) end ================================================ FILE: spec/fixtures/deprecations/bootable_dirs_config/system/boot/logger.rb ================================================ # frozen_string_literal: true Test::Container.register_provider :logger do start do register "logger", "my logger" end end ================================================ FILE: spec/fixtures/deprecations/bootable_dirs_config/system/custom_boot/logger.rb ================================================ # frozen_string_literal: true Test::Container.register_provider :logger do start do register "logger", "my logger" end end ================================================ FILE: spec/fixtures/external_components/alt-components/db.rb ================================================ # frozen_string_literal: true require "dry/system" Dry::System.register_provider_source(:db, group: :alt) do prepare do module AltComponents class DbConn end end end start do register(:db_conn, AltComponents::DbConn.new) end end ================================================ FILE: spec/fixtures/external_components/alt-components/logger.rb ================================================ # frozen_string_literal: true require "dry/system" Dry::System.register_provider_source(:logger, group: :alt) do prepare do module AltComponents class Logger end end end start do register(:logger, AltComponents::Logger.new) end end ================================================ FILE: spec/fixtures/external_components/components/logger.rb ================================================ # frozen_string_literal: true require "dry/system" Dry::System.register_provider_source(:logger, group: :external_components) do setting :log_level, default: :scream, constructor: Types::Symbol prepare do unless defined?(ExternalComponents) module ExternalComponents class Logger class << self attr_accessor :default_level end self.default_level = :scream attr_reader :log_level def initialize(log_level = Logger.default_level) @log_level = log_level end end end end end start do logger = if config.log_level ExternalComponents::Logger.new(config.log_level) else ExternalComponents::Logger.new end register(:logger, logger) end end ================================================ FILE: spec/fixtures/external_components/components/mailer.rb ================================================ # frozen_string_literal: true require "dry/system" Dry::System.register_provider_source(:mailer, group: :external_components) do prepare do module ExternalComponents class Mailer attr_reader :client def initialize(client) @client = client end end end end start do target.start :client register(:mailer, ExternalComponents::Mailer.new(target_container["client"])) end end ================================================ FILE: spec/fixtures/external_components/components/notifier.rb ================================================ # frozen_string_literal: true require "dry/system" Dry::System.register_provider_source(:notifier, group: :external_components) do prepare do module ExternalComponents class Notifier attr_reader :monitor def initialize(monitor) @monitor = monitor end end end end start do register(:notifier, ExternalComponents::Notifier.new(target_container["monitor"])) end end ================================================ FILE: spec/fixtures/external_components/lib/external_components.rb ================================================ # frozen_string_literal: true require "dry/system" Dry::System.register_provider_sources Pathname(__dir__).join("../components").realpath Dry::System.register_provider_sources Pathname(__dir__).join("../alt-components").realpath ================================================ FILE: spec/fixtures/external_components_deprecated/components/logger.rb ================================================ # frozen_string_literal: true require "dry/system" Dry::System.register_component(:logger, provider: :external_components) do setting :log_level, default: :scream, constructor: Types::Symbol prepare do unless defined?(ExternalComponents) module ExternalComponents class Logger class << self attr_accessor :default_level end self.default_level = :scream attr_reader :log_level def initialize(log_level = Logger.default_level) @log_level = log_level end end end end end start do logger = if config ExternalComponents::Logger.new(config.log_level) else ExternalComponents::Logger.new end register(:logger, logger) end end ================================================ FILE: spec/fixtures/external_components_deprecated/lib/external_components.rb ================================================ # frozen_string_literal: true require "dry/system" Dry::System.register_provider( :external_components, path: Pathname(__dir__).join("../components").realpath ) ================================================ FILE: spec/fixtures/import_test/config/application.yml ================================================ test: foo: 'bar' ================================================ FILE: spec/fixtures/import_test/lib/test/bar.rb ================================================ # frozen_string_literal: true module Test class Bar end end ================================================ FILE: spec/fixtures/import_test/lib/test/foo.rb ================================================ # frozen_string_literal: true module Test class Foo include Import["test.bar"] end end ================================================ FILE: spec/fixtures/lazy_loading/auto_registration_disabled/lib/entities/kitten.rb ================================================ # auto_register: false # frozen_string_literal: true module Entities class Kitten end end ================================================ FILE: spec/fixtures/lazy_loading/auto_registration_disabled/lib/fetch_kitten.rb ================================================ # frozen_string_literal: true class FetchKitten end ================================================ FILE: spec/fixtures/lazy_loading/shared_root_keys/lib/kitten_service/fetch_kitten.rb ================================================ # frozen_string_literal: true module KittenService class FetchKitten end end ================================================ FILE: spec/fixtures/lazy_loading/shared_root_keys/lib/kitten_service/submit_kitten.rb ================================================ # frozen_string_literal: true module KittenService class SubmitKitten end end ================================================ FILE: spec/fixtures/lazy_loading/shared_root_keys/system/providers/kitten_service.rb ================================================ # frozen_string_literal: true Test::Container.register_provider(:kitten_service, namespace: true) do prepare do module KittenService class Client end end end start do register "client", KittenService::Client.new end end ================================================ FILE: spec/fixtures/lazytest/config/application.yml ================================================ test: foo: 'bar' ================================================ FILE: spec/fixtures/lazytest/lib/test/dep.rb ================================================ # frozen_string_literal: true module Test class Dep end end ================================================ FILE: spec/fixtures/lazytest/lib/test/foo.rb ================================================ # frozen_string_literal: true module Test class Foo include Import["test.dep"] end end ================================================ FILE: spec/fixtures/lazytest/lib/test/models/book.rb ================================================ # frozen_string_literal: true module Test module Models class Book end end end ================================================ FILE: spec/fixtures/lazytest/lib/test/models/user.rb ================================================ # frozen_string_literal: true module Test module Models class User end end end ================================================ FILE: spec/fixtures/lazytest/lib/test/models.rb ================================================ # frozen_string_literal: true module Test module Models end end ================================================ FILE: spec/fixtures/lazytest/system/providers/bar.rb ================================================ # frozen_string_literal: true Test::Container.namespace(:test) do |container| container.register_provider(:bar) do prepare do module Test module Bar # I shall be booted end end end start do container.register(:bar, "I was finalized") end end end ================================================ FILE: spec/fixtures/magic_comments/comments.rb ================================================ #!/usr/bin/env ruby # rubocop:disable Layout/CommentIndentation # frozen_string_literal: true # This is a file with a mix of valid and invalid magic comments # valid_comment: hello # true_comment: true # false_comment: false # comment_123: alpha-numeric and underscores allowed # 123_will_not_match: will not match # not-using-underscores: value for comment using dashes # not_at_start_of_line: will not match module Test end # after_code: will not match # rubocop:enable Layout/CommentIndentation ================================================ FILE: spec/fixtures/manifest_registration/lib/test/foo.rb ================================================ # frozen_string_literal: true module Test class Foo attr_reader :name def initialize(name) @name = name end end end ================================================ FILE: spec/fixtures/manifest_registration/system/registrations/foo.rb ================================================ # frozen_string_literal: true Test::Container.namespace(:foo) do |container| container.register("special") do require "test/foo" Test::Foo.new("special") end end ================================================ FILE: spec/fixtures/memoize_magic_comments/test/memoize_false_comment.rb ================================================ # memoize: false # frozen_string_literal: true module Test class MemoizeFalseComment end end ================================================ FILE: spec/fixtures/memoize_magic_comments/test/memoize_no_comment.rb ================================================ # frozen_string_literal: true module Test class MemoizeNoComment end end ================================================ FILE: spec/fixtures/memoize_magic_comments/test/memoize_true_comment.rb ================================================ # memoize: true # frozen_string_literal: true module Test class MemoizeTrueComment end end ================================================ FILE: spec/fixtures/mixed_namespaces/lib/test/external/external_component.rb ================================================ # frozen_string_literal: true module Test module External class ExternalComponent end end end ================================================ FILE: spec/fixtures/mixed_namespaces/lib/test/my_app/app_component.rb ================================================ # frozen_string_literal: true module Test module MyApp class AppComponent end end end ================================================ FILE: spec/fixtures/multiple_namespaced_components/multiple/level/baz.rb ================================================ # frozen_string_literal: true module Multiple module Level class Baz include Test::Container.injector["foz"] end end end ================================================ FILE: spec/fixtures/multiple_namespaced_components/multiple/level/foz.rb ================================================ # frozen_string_literal: true module Multiple module Level class Foz end end end ================================================ FILE: spec/fixtures/multiple_provider_dirs/custom_bootables/logger.rb ================================================ # frozen_string_literal: true Test::Container.register_provider(:logger) do start do register(:logger, "custom_logger") end end ================================================ FILE: spec/fixtures/multiple_provider_dirs/default_bootables/inflector.rb ================================================ # frozen_string_literal: true Test::Container.register_provider(:inflector) do start do register(:inflector, "default_inflector") end end ================================================ FILE: spec/fixtures/multiple_provider_dirs/default_bootables/logger.rb ================================================ # frozen_string_literal: true Test::Container.register_provider(:logger) do start do register(:logger, "default_logger") end end ================================================ FILE: spec/fixtures/namespaced_components/namespaced/bar.rb ================================================ # frozen_string_literal: true module Namespaced class Bar include Test::Container.injector["foo"] end end ================================================ FILE: spec/fixtures/namespaced_components/namespaced/foo.rb ================================================ # frozen_string_literal: true module Namespaced class Foo end end ================================================ FILE: spec/fixtures/other/config/providers/bar.rb ================================================ # frozen_string_literal: true Test::Container.register_provider(:bar, namespace: "test") do |_container| prepare do module Test module Bar # I shall be booted end end end start do register(:bar, "I was finalized") end end ================================================ FILE: spec/fixtures/other/config/providers/hell.rb ================================================ # frozen_string_literal: true Test::Container.register_provider(:heaven) do start do register("heaven", "string") end end ================================================ FILE: spec/fixtures/other/lib/test/dep.rb ================================================ # frozen_string_literal: true module Test class Dep end end ================================================ FILE: spec/fixtures/other/lib/test/foo.rb ================================================ # frozen_string_literal: true module Test class Foo include Import["test.dep"] end end ================================================ FILE: spec/fixtures/other/lib/test/models/book.rb ================================================ # frozen_string_literal: true module Test module Models class Book end end end ================================================ FILE: spec/fixtures/other/lib/test/models/user.rb ================================================ # frozen_string_literal: true module Test module Models class User end end end ================================================ FILE: spec/fixtures/other/lib/test/models.rb ================================================ # frozen_string_literal: true module Test module Models end end ================================================ FILE: spec/fixtures/require_path/lib/test/foo.rb ================================================ # frozen_string_literal: true module Test class Foo end end ================================================ FILE: spec/fixtures/settings_test/types.rb ================================================ # frozen_string_literal: true require "dry/types" module SettingsTest module Types include Dry::Types() end end ================================================ FILE: spec/fixtures/standard_container_with_default_namespace/lib/test/dep.rb ================================================ # frozen_string_literal: true module Test class Dep end end ================================================ FILE: spec/fixtures/standard_container_with_default_namespace/lib/test/example_with_dep.rb ================================================ # frozen_string_literal: true module Test class ExampleWithDep include Import["dep"] end end ================================================ FILE: spec/fixtures/standard_container_without_default_namespace/lib/test/dep.rb ================================================ # frozen_string_literal: true module Test class Dep end end ================================================ FILE: spec/fixtures/standard_container_without_default_namespace/lib/test/example_with_dep.rb ================================================ # frozen_string_literal: true module Test class ExampleWithDep include Import["test.dep"] end end ================================================ FILE: spec/fixtures/stubbing/lib/test/car.rb ================================================ # frozen_string_literal: true module Test class Car def wheels_count 4 end end end ================================================ FILE: spec/fixtures/stubbing/system/providers/db.rb ================================================ # frozen_string_literal: true Test::Container.register_provider(:db) do module Test class DB end end start do register(:db, Test::DB.new) end end ================================================ FILE: spec/fixtures/test/config/application.yml ================================================ test: foo: 'bar' ================================================ FILE: spec/fixtures/test/config/subapp.yml ================================================ test: bar: 'baz' ================================================ FILE: spec/fixtures/test/lib/test/dep.rb ================================================ # frozen_string_literal: true module Test class Dep end end ================================================ FILE: spec/fixtures/test/lib/test/foo.rb ================================================ # frozen_string_literal: true module Test class Foo include Import["test.dep"] end end ================================================ FILE: spec/fixtures/test/lib/test/models/book.rb ================================================ # frozen_string_literal: true module Test module Models class Book end end end ================================================ FILE: spec/fixtures/test/lib/test/models/user.rb ================================================ # frozen_string_literal: true module Test module Models class User end end end ================================================ FILE: spec/fixtures/test/lib/test/models.rb ================================================ # frozen_string_literal: true module Test module Models end end ================================================ FILE: spec/fixtures/test/lib/test/singleton_dep.rb ================================================ # frozen_string_literal: true require "singleton" module Test class SingletonDep include Singleton end end ================================================ FILE: spec/fixtures/test/log/.gitkeep ================================================ ================================================ FILE: spec/fixtures/test/system/providers/bar.rb ================================================ # frozen_string_literal: true Test::Container.register_provider(:bar, namespace: "test") do prepare do module Test module Bar # I shall be booted end end end start do register(:bar, "I was finalized") end end ================================================ FILE: spec/fixtures/test/system/providers/client.rb ================================================ # frozen_string_literal: true Test::Container.register_provider(:client) do start do target.start :logger Test::Client = Struct.new(:logger) register(:client, Test::Client.new(target_container["logger"])) end end ================================================ FILE: spec/fixtures/test/system/providers/db.rb ================================================ # frozen_string_literal: true # this is just for Container.finalize spec, actual finalization code is in the test ================================================ FILE: spec/fixtures/test/system/providers/hell.rb ================================================ # frozen_string_literal: true Test::Container.register_provider(:heaven) do start do register("heaven", "string") end end ================================================ FILE: spec/fixtures/test/system/providers/logger.rb ================================================ # frozen_string_literal: true module Test class LoggerProvider < Dry::System::Provider::Source def prepare require "logger" end def start register(:logger, Logger.new(target_container.root.join("log/test.log"))) end end end Test::Container.register_provider(:logger, source: Test::LoggerProvider) ================================================ FILE: spec/fixtures/umbrella/system/providers/db.rb ================================================ # frozen_string_literal: true Test::Umbrella.register_provider(:db, namespace: "db") do prepare do module Db class Repo end end end start do register(:repo, Db::Repo.new) end end ================================================ FILE: spec/fixtures/unit/component/component_dir_1/namespace/nested/component_file.rb ================================================ # frozen_string_literal: true ================================================ FILE: spec/fixtures/unit/component/component_dir_1/namespace/nested/component_file_with_auto_register_false.rb ================================================ # auto_register: false # frozen_string_literal: true ================================================ FILE: spec/fixtures/unit/component/component_dir_1/outside_namespace/component_file.rb ================================================ # frozen_string_literal: true ================================================ FILE: spec/fixtures/unit/component/component_dir_2/namespace/nested/component_file.rb ================================================ ================================================ FILE: spec/fixtures/unit/component_dir/component_file.rb ================================================ ================================================ FILE: spec/integration/boot_spec.rb ================================================ # frozen_string_literal: true require "ostruct" RSpec.describe Dry::System::Container, ".register_provider" do subject(:system) { Test::Container } let(:setup_db) do system.register_provider(:db) do prepare do module Test class Db < OpenStruct end end end start do register("db.conn", Test::Db.new(established: true)) end stop do container["db.conn"].established = false end end end let(:setup_client) do system.register_provider(:client) do prepare do module Test class Client < OpenStruct end end end start do register("client.conn", Test::Client.new(connected: true)) end stop do container["client.conn"].connected = false end end end context "with a boot file" do before do class Test::Container < Dry::System::Container configure do |config| config.root = SPEC_ROOT.join("fixtures/test").realpath end end end it "auto-boots dependency of a bootable component" do system.start(:client) expect(system[:client]).to be_a(Test::Client) expect(system[:client].logger).to be_a(Logger) end end context "using predefined settings for configuration" do before do class Test::Container < Dry::System::Container end end it "uses defaults" do system.register_provider(:api) do setting :token, default: "xxx" start do register(:client, OpenStruct.new(config.to_h)) end end system.start(:api) client = system[:client] expect(client.token).to eql("xxx") end end context "inline booting" do before do class Test::Container < Dry::System::Container end end it "allows lazy-booting" do system.register_provider(:db) do prepare do module Test class Db < OpenStruct end end end start do register("db.conn", Test::Db.new(established?: true)) end stop do db.conn.established = false end end conn = system["db.conn"] expect(conn).to be_established end it "allows component to be stopped" do setup_db system.start(:db) conn = system["db.conn"] system.stop(:db) expect(conn.established).to eq false end xit "raises an error when trying to stop a component that has not been started" do setup_db expect { system.stop(:db) }.to raise_error(Dry::System::ProviderNotStartedError) end describe "#shutdown!" do it "allows container to stop all started components" do setup_db setup_client db = system["db.conn"] client = system["client.conn"] system.shutdown! expect(db.established).to eq false expect(client.connected).to eq false end it "skips components that has not been started" do setup_db setup_client db = system["db.conn"] system.shutdown! expect { system.shutdown! }.to_not raise_error expect(db.established).to eq false end end end end ================================================ FILE: spec/integration/container/auto_registration/component_dir_namespaces/autoloading_loader_spec.rb ================================================ # frozen_string_literal: true require "dry/system/loader/autoloading" require "zeitwerk" RSpec.describe "Component dir namespaces / Autoloading loader" do let(:container) { root = @dir dir_config = defined?(component_dir_config) ? component_dir_config : -> * {} Class.new(Dry::System::Container) { configure do |config| config.root = root config.component_dirs.add "lib" do |dir| dir.loader = Dry::System::Loader::Autoloading dir_config.(dir) end end } } let(:loader) { Zeitwerk::Loader.new } before do allow(Zeitwerk::Loader).to receive(:new).and_return(ZeitwerkLoaderRegistry.new_loader) end after { ZeitwerkLoaderRegistry.clear } context "top-level constant namespace" do let(:component_dir_config) { -> dir { dir.namespaces.add "ns", const: nil } } before :context do @dir = make_tmp_directory with_directory(@dir) do write "lib/ns/component.rb", <<~RUBY class Component end RUBY write "lib/ns/nested/component.rb", <<~RUBY module Nested class Component end end RUBY end end before do loader.push_dir @dir.join("lib/ns").realpath loader.setup end let(:cleanable_constants) { %i[Component] } context "lazy loading" do it "resolves the component as an instance of a top-level class" do expect(container["ns.component"]).to be_an_instance_of Component expect(container["ns.nested.component"]).to be_an_instance_of Nested::Component end end context "finalized" do before do container.finalize! end it "resolves the component as an instance of a top-level class" do expect(container["ns.component"]).to be_an_instance_of Component expect(container["ns.nested.component"]).to be_an_instance_of Nested::Component end end end context "distinct constant namespace" do let(:component_dir_config) { -> dir { dir.namespaces.add "ns", const: "my_namespace" } } before :context do @dir = make_tmp_directory with_directory(@dir) do write "lib/ns/component.rb", <<~RUBY module MyNamespace class Component end end RUBY write "lib/ns/nested/component.rb", <<~RUBY module MyNamespace module Nested class Component end end end RUBY end end before do module MyNamespace; end loader.push_dir @dir.join("lib", "ns").realpath, namespace: MyNamespace loader.setup end let(:cleanable_modules) { super() + %i[MyNamespace] } context "lazy loading" do it "resolves the component as an instance of a class in the given constant namespace" do expect(container["ns.component"]).to be_an_instance_of MyNamespace::Component expect(container["ns.nested.component"]).to be_an_instance_of MyNamespace::Nested::Component end end context "finalized" do before do container.finalize! end it "resolves the component as an instance of a class in the given constant namespace" do expect(container["ns.component"]).to be_an_instance_of MyNamespace::Component expect(container["ns.nested.component"]).to be_an_instance_of MyNamespace::Nested::Component end end end context "distinct constant namespace for root" do let(:component_dir_config) { -> dir { dir.namespaces.add_root const: "my_namespace" } } before :context do @dir = make_tmp_directory with_directory(@dir) do write "lib/component.rb", <<~RUBY module MyNamespace class Component end end RUBY write "lib/nested/component.rb", <<~RUBY module MyNamespace module Nested class Component end end end RUBY end end before do module MyNamespace; end loader.push_dir @dir.join("lib").realpath, namespace: MyNamespace loader.setup end let(:cleanable_modules) { super() + %i[MyNamespace] } context "lazy loading" do it "resolves the component as an instance of a class in the given constant namespace" do expect(container["component"]).to be_an_instance_of MyNamespace::Component expect(container["nested.component"]).to be_an_instance_of MyNamespace::Nested::Component end end context "finalized" do before do container.finalize! end it "resolves the component as an instance of a class in the given constant namespace" do expect(container["component"]).to be_an_instance_of MyNamespace::Component expect(container["nested.component"]).to be_an_instance_of MyNamespace::Nested::Component end end end end ================================================ FILE: spec/integration/container/auto_registration/component_dir_namespaces/deep_namespace_paths_spec.rb ================================================ # frozen_string_literal: true RSpec.describe "Component dir namespaces / Deep namespace paths" do let(:container) { root = @dir dir_config = defined?(component_dir_config) ? component_dir_config : -> * {} Class.new(Dry::System::Container) { configure do |config| config.root = root config.component_dirs.add("lib", &dir_config) end } } context "key namespace not given" do let(:component_dir_config) { -> dir { dir.namespaces.add "ns/nested", const: nil } } before :context do @dir = make_tmp_directory with_directory(@dir) do write "lib/ns/nested/component.rb", <<~RUBY class Component end RUBY end end context "lazy loading" do it "registers components using the key namespace separator ('.'), not the path separator used for the namespace path" do expect(container["ns.nested.component"]).to be_an_instance_of Component end end context "finalized" do before do container.finalize! end it "registers components using the key namespace separator ('.'), not the path separator used for the namespace path" do expect(container["ns.nested.component"]).to be_an_instance_of Component end end end end ================================================ FILE: spec/integration/container/auto_registration/component_dir_namespaces/default_loader_spec.rb ================================================ # frozen_string_literal: true RSpec.describe "Component dir namespaces / Default loader" do let(:container) { root = @dir dir_config = defined?(component_dir_config) ? component_dir_config : -> * {} Class.new(Dry::System::Container) { configure do |config| config.root = root config.component_dirs.add("lib", &dir_config) end } } describe "constant namespaces" do context "top-level constant namespace" do let(:component_dir_config) { -> dir { dir.namespaces.add "ns", const: nil } } before :context do @dir = make_tmp_directory with_directory(@dir) do write "lib/ns/component.rb", <<~RUBY class Component end RUBY end end let(:cleanable_constants) { %i[Component] } context "lazy loading" do it "resolves the component as an instance of a top-level class" do expect(container["ns.component"]).to be_an_instance_of Component end end context "finalized" do before do container.finalize! end it "resolves the component as an instance of a top-level class" do expect(container["ns.component"]).to be_an_instance_of Component end end end context "distinct constant namespace" do let(:component_dir_config) { -> dir { dir.namespaces.add "ns", const: "my_namespace" } } before :context do @dir = make_tmp_directory with_directory(@dir) do write "lib/ns/component.rb", <<~RUBY module MyNamespace class Component end end RUBY write "lib/ns/nested/component.rb", <<~RUBY module MyNamespace module Nested class Component end end end RUBY end end let(:cleanable_modules) { super() + %i[MyNamespace] } context "lazy loading" do it "resolves the component as an instance of a class in the given constant namespace" do expect(container["ns.component"]).to be_an_instance_of MyNamespace::Component expect(container["ns.nested.component"]).to be_an_instance_of MyNamespace::Nested::Component end end context "finalized" do before do container.finalize! end it "resolves the component as an instance of a class in the given constant namespace" do expect(container["ns.component"]).to be_an_instance_of MyNamespace::Component expect(container["ns.nested.component"]).to be_an_instance_of MyNamespace::Nested::Component end end end context "distinct constant namespace for root" do let(:component_dir_config) { -> dir { dir.namespaces.add_root const: "my_namespace" } } before :context do @dir = make_tmp_directory with_directory(@dir) do write "lib/component.rb", <<~RUBY module MyNamespace class Component end end RUBY write "lib/nested/component.rb", <<~RUBY module MyNamespace module Nested class Component end end end RUBY end end let(:cleanable_modules) { super() + %i[MyNamespace] } context "lazy loading" do it "resolves the component as an instance of a class in the given constant namespace" do expect(container["component"]).to be_an_instance_of MyNamespace::Component expect(container["nested.component"]).to be_an_instance_of MyNamespace::Nested::Component end end context "finalized" do before do container.finalize! end it "resolves the component as an instance of a class in the given constant namespace" do expect(container["component"]).to be_an_instance_of MyNamespace::Component expect(container["nested.component"]).to be_an_instance_of MyNamespace::Nested::Component end end end end describe "key namespaces" do describe "top-level key namespace" do let(:component_dir_config) { -> dir { dir.namespaces.add "ns", key: nil } } before :context do @dir = make_tmp_directory with_directory(@dir) do write "lib/ns/component.rb", <<~RUBY module Ns class Component end end RUBY end end let(:cleanable_modules) { super() + %i[Ns] } context "lazy loading" do it "resolves the component via a top-level key" do expect(container["component"]).to be_an_instance_of Ns::Component end end context "finalized" do before do container.finalize! end it "resolves the component as an instance of a top-level class" do expect(container["component"]).to be_an_instance_of Ns::Component end end end describe "distinct key namespace" do let(:component_dir_config) { -> dir { dir.namespaces.add "ns", key: "my_ns" } } before :context do @dir = make_tmp_directory with_directory(@dir) do write "lib/ns/component.rb", <<~RUBY module Ns class Component end end RUBY write "lib/ns/nested/component.rb", <<~RUBY module Ns module Nested class Component end end end RUBY end end let(:cleanable_modules) { super() + %i[Ns] } context "lazy loading" do it "resolves the component via the given key namespace" do expect(container["my_ns.component"]).to be_an_instance_of Ns::Component expect(container["my_ns.nested.component"]).to be_an_instance_of Ns::Nested::Component end end context "finalized" do before do container.finalize! end it "resolves the component via the given key namespace" do expect(container["my_ns.component"]).to be_an_instance_of Ns::Component expect(container["my_ns.nested.component"]).to be_an_instance_of Ns::Nested::Component end end end describe "distinct key namespace for root" do let(:component_dir_config) { -> dir { dir.namespaces.add_root key: "my_ns" } } before :context do @dir = make_tmp_directory with_directory(@dir) do write "lib/component.rb", <<~RUBY class Component end RUBY write "lib/nested/component.rb", <<~RUBY module Nested class Component end end RUBY end end let(:cleanable_modules) { super() + %i[Nested] } let(:cleanable_constants) { %i[Component] } context "lazy loading" do it "resolves the component via the given key namespace" do expect(container["my_ns.component"]).to be_an_instance_of Component expect(container["my_ns.nested.component"]).to be_an_instance_of Nested::Component end end context "finalized" do before do container.finalize! end it "resolves the component via the given key namespace" do expect(container["my_ns.component"]).to be_an_instance_of Component expect(container["my_ns.nested.component"]).to be_an_instance_of Nested::Component end end end end describe "mixed constant and key namespaces" do let(:component_dir_config) { -> dir { dir.namespaces.add "ns", key: "my_ns", const: "my_namespace" } } before :context do @dir = make_tmp_directory with_directory(@dir) do write "lib/ns/component.rb", <<~RUBY module MyNamespace class Component end end RUBY write "lib/ns/nested/component.rb", <<~RUBY module MyNamespace module Nested class Component end end end RUBY end end let(:cleanable_modules) { super() + %i[MyNamespace] } context "lazy loading" do it "resolves the component via the given key namespace and returns an instance of a class in the given constant namespace" do expect(container["my_ns.component"]).to be_an_instance_of MyNamespace::Component expect(container["my_ns.nested.component"]).to be_an_instance_of MyNamespace::Nested::Component end end context "finalized" do before do container.finalize! end it "resolves the component via the given key namespace and returns an instance of a class in the given constant namespace" do expect(container["my_ns.component"]).to be_an_instance_of MyNamespace::Component expect(container["my_ns.nested.component"]).to be_an_instance_of MyNamespace::Nested::Component end end end end ================================================ FILE: spec/integration/container/auto_registration/component_dir_namespaces/multiple_namespaces_spec.rb ================================================ # frozen_string_literal: true RSpec.describe "Component dir namespaces / Multiple namespaces" do let(:cleanable_constants) { %i[Component RootComponent] } let(:cleanable_modules) { %i[Admin Test] } let(:container) { root = @dir dir_config = defined?(component_dir_config) ? component_dir_config : -> * {} Class.new(Dry::System::Container) { configure do |config| config.root = root config.component_dirs.add("lib", &dir_config) end } } context "single configured path namespace" do let(:component_dir_config) { -> dir { dir.namespaces.add "test", key: nil } } before :context do @dir = make_tmp_directory with_directory(@dir) do write "lib/test/component.rb", <<~RUBY module Test class Component end end RUBY end end context "lazy loading" do it "resolves the compoment via the namespace" do expect(container["component"]).to be_an_instance_of Test::Component end end context "finalized" do before do container.finalize! end it "resolves the compoment via the namespace" do expect(container["component"]).to be_an_instance_of Test::Component end end end context "mixed path and root namespace" do before :context do @dir = make_tmp_directory with_directory(@dir) do write "lib/test/component.rb", <<~RUBY module Test class Component end end RUBY write "lib/component.rb", <<~RUBY class Component end RUBY write "lib/root_component.rb", <<~RUBY class RootComponent end RUBY end end context "configured path namespace before implicit trailing root namespace" do let(:component_dir_config) { -> dir { dir.namespaces.add "test", key: nil } } context "lazy loading" do it "prefers the configured namespace when resolving components" do expect(container["component"]).to be_an_instance_of Test::Component expect(container["root_component"]).to be_an_instance_of RootComponent end end context "finalized" do before do container.finalize! end it "prefers the configured namespace when resolving components" do expect(container["component"]).to be_an_instance_of Test::Component expect(container["root_component"]).to be_an_instance_of RootComponent end end end context "leading root namespace before configured path namespace" do let(:component_dir_config) { -> dir { dir.namespaces.add_root dir.namespaces.add "test", key: nil } } context "lazy loading" do it "prefers the root namespace when resolving components" do expect(container["component"]).to be_an_instance_of Component expect(container["root_component"]).to be_an_instance_of RootComponent end end context "finalized" do before do container.finalize! end it "prefers the root namespace when resolving components" do expect(container["component"]).to be_an_instance_of Component expect(container["root_component"]).to be_an_instance_of RootComponent end end end end context "multiple configured path namespaces" do before :context do @dir = make_tmp_directory with_directory(@dir) do write "lib/admin/admin_component.rb", <<~RUBY module Admin class AdminComponent end end RUBY write "lib/admin/component.rb", <<~RUBY module Admin class Component end end RUBY write "lib/test/test_component.rb", <<~RUBY module Test class TestComponent end end RUBY write "lib/test/component.rb", <<~RUBY module Test class Component end end RUBY write "lib/component.rb", <<~RUBY class Component end RUBY write "lib/root_component.rb", <<~RUBY class RootComponent end RUBY end end context "ordered one way" do let(:component_dir_config) { -> dir { dir.namespaces.add "admin", key: nil dir.namespaces.add "test", key: nil } } context "lazy loading" do it "prefers the earlier configured namespaces when resolving components" do expect(container["component"]).to be_an_instance_of Admin::Component expect(container["admin_component"]).to be_an_instance_of Admin::AdminComponent expect(container["test_component"]).to be_an_instance_of Test::TestComponent end end context "finalized" do before do container.finalize! end it "prefers the earlier configured namespaces when resolving components" do expect(container["component"]).to be_an_instance_of Admin::Component expect(container["admin_component"]).to be_an_instance_of Admin::AdminComponent expect(container["test_component"]).to be_an_instance_of Test::TestComponent end end end context "ordered the other way" do let(:component_dir_config) { -> dir { dir.namespaces.add "test", key: nil dir.namespaces.add "admin", key: nil } } context "lazy loading" do it "prefers the earlier configured namespaces when resolving components" do expect(container["component"]).to be_an_instance_of Test::Component expect(container["admin_component"]).to be_an_instance_of Admin::AdminComponent expect(container["test_component"]).to be_an_instance_of Test::TestComponent end end context "finalized" do before do container.finalize! end it "prefers the earlier configured namespaces when resolving components" do expect(container["component"]).to be_an_instance_of Test::Component expect(container["admin_component"]).to be_an_instance_of Admin::AdminComponent expect(container["test_component"]).to be_an_instance_of Test::TestComponent end end end context "leading root namespace" do let(:component_dir_config) { -> dir { dir.namespaces.add_root dir.namespaces.add "admin", key: nil dir.namespaces.add "test", key: nil } } context "lazy loading" do it "prefers the earlier configured namespaces when resolving components" do expect(container["component"]).to be_an_instance_of Component expect(container["admin_component"]).to be_an_instance_of Admin::AdminComponent expect(container["test_component"]).to be_an_instance_of Test::TestComponent expect(container["root_component"]).to be_an_instance_of RootComponent end end context "finalized" do before do container.finalize! end it "prefers the earlier configured namespaces when resolving components" do expect(container["component"]).to be_an_instance_of Component expect(container["admin_component"]).to be_an_instance_of Admin::AdminComponent expect(container["test_component"]).to be_an_instance_of Test::TestComponent expect(container["root_component"]).to be_an_instance_of RootComponent end end end context "root namespace between path namespaces" do let(:component_dir_config) { -> dir { dir.namespaces.add "admin", key: nil dir.namespaces.add_root dir.namespaces.add "test", key: nil } } context "lazy loading" do it "prefers the earlier configured namespaces when resolving components" do expect(container["component"]).to be_an_instance_of Admin::Component expect(container["admin_component"]).to be_an_instance_of Admin::AdminComponent expect(container["test_component"]).to be_an_instance_of Test::TestComponent expect(container["root_component"]).to be_an_instance_of RootComponent end end context "finalized" do before do container.finalize! end it "prefers the earlier configured namespaces when resolving components" do expect(container["component"]).to be_an_instance_of Admin::Component expect(container["admin_component"]).to be_an_instance_of Admin::AdminComponent expect(container["test_component"]).to be_an_instance_of Test::TestComponent expect(container["root_component"]).to be_an_instance_of RootComponent end end end end end ================================================ FILE: spec/integration/container/auto_registration/component_dir_namespaces/namespaces_as_defaults_spec.rb ================================================ # frozen_string_literal: true RSpec.describe "Component dir namespaces / Namespaces as component dir defaults" do let(:container) { root = @dir cont_config = defined?(container_config) ? container_config : -> * {} Class.new(Dry::System::Container) { configure do |config| config.root = root cont_config.(config) end } } let(:container_config) { -> config { config.component_dirs.add "lib" config.component_dirs.namespaces.add "top_level_const", const: nil config.component_dirs.namespaces.add "top_level_key", key: nil config.component_dirs.add "xyz" } } before :context do @dir = make_tmp_directory with_directory(@dir) do write "lib/top_level_const/top_level_lib_component.rb", <<~RUBY class TopLevelLibComponent end RUBY write "xyz/top_level_const/top_level_xyz_component.rb", <<~RUBY class TopLevelXyzComponent end RUBY write "lib/top_level_key/nested/lib_component.rb", <<~RUBY module TopLevelKey module Nested class LibComponent end end end RUBY write "xyz/top_level_key/nested/xyz_component.rb", <<~RUBY module TopLevelKey module Nested class XyzComponent end end end RUBY end end context "lazy loading" do it "resolves the components from multiple component dirs according to the default namespaces" do expect(container["top_level_const.top_level_lib_component"]).to be_an_instance_of TopLevelLibComponent expect(container["top_level_const.top_level_xyz_component"]).to be_an_instance_of TopLevelXyzComponent expect(container["nested.lib_component"]).to be_an_instance_of TopLevelKey::Nested::LibComponent expect(container["nested.xyz_component"]).to be_an_instance_of TopLevelKey::Nested::XyzComponent end end context "finalized" do before do container.finalize! end it "resolves the components from multiple component dirs according to the default namespaces" do expect(container["top_level_const.top_level_lib_component"]).to be_an_instance_of TopLevelLibComponent expect(container["top_level_const.top_level_xyz_component"]).to be_an_instance_of TopLevelXyzComponent expect(container["nested.lib_component"]).to be_an_instance_of TopLevelKey::Nested::LibComponent expect(container["nested.xyz_component"]).to be_an_instance_of TopLevelKey::Nested::XyzComponent end end end ================================================ FILE: spec/integration/container/auto_registration/custom_auto_register_proc_spec.rb ================================================ # frozen_string_literal: true RSpec.describe "Auto-registration / Custom auto_register proc" do before do class Test::Container < Dry::System::Container configure do |config| config.root = SPEC_ROOT.join("fixtures").realpath config.component_dirs.add "components" do |dir| dir.namespaces.add "test", key: nil dir.auto_register = proc do |component| !component.key.match?(/bar/) end end end end end shared_examples "custom auto_register proc" do it "registers components according to the custom auto_register proc" do expect(Test::Container.key?("foo")).to be true expect(Test::Container.key?("bar")).to be false expect(Test::Container.key?("bar.baz")).to be false end end context "Finalized container" do before do Test::Container.finalize! end include_examples "custom auto_register proc" end context "Non-finalized container" do include_examples "custom auto_register proc" end end ================================================ FILE: spec/integration/container/auto_registration/custom_instance_proc_spec.rb ================================================ # frozen_string_literal: true RSpec.describe "Auto-registration / Custom instance proc" do before :context do with_directory(@dir = make_tmp_directory) do write "lib/foo.rb", <<~RUBY module Test class Foo; end end RUBY end end let(:container) { root = @dir Class.new(Dry::System::Container) { configure do |config| config.root = root config.component_dirs.add "lib" do |dir| dir.namespaces.add_root const: "test" dir.instance = proc do |component, *args| # Return the component's string key as its instance component.key end end end } } shared_examples "custom instance proc" do it "registers the component using the custom loader" do expect(container["foo"]).to eq "foo" end end context "Non-finalized container" do include_examples "custom instance proc" end context "Finalized container" do before do container.finalize! end include_examples "custom instance proc" end end ================================================ FILE: spec/integration/container/auto_registration/custom_loader_spec.rb ================================================ # frozen_string_literal: true RSpec.describe "Auto-registration / Custom loader" do before do # A loader that simply returns the component's identifier string as its instance class Test::IdentifierLoader def self.call(component, *_args) component.identifier.to_s end end class Test::Container < Dry::System::Container configure do |config| config.root = SPEC_ROOT.join("fixtures").realpath config.component_dirs.add "components" do |dir| dir.namespaces.add "test", key: nil dir.loader = Test::IdentifierLoader end end end end shared_examples "custom loader" do it "registers the component using the custom loader" do expect(Test::Container["foo"]).to eq "foo" end end context "Finalized container" do before do Test::Container.finalize! end include_examples "custom loader" end context "Non-finalized container" do include_examples "custom loader" end end ================================================ FILE: spec/integration/container/auto_registration/memoize_spec.rb ================================================ # frozen_string_literal: true RSpec::Matchers.define :have_memoized_component do |key| match do |container| container[key].eql?(container[key]) end end RSpec.describe "Auto-registration / Memoizing components" do describe "Memoizing all components in a component directory" do before do class Test::Container < Dry::System::Container configure do |config| config.root = SPEC_ROOT.join("fixtures").realpath config.component_dirs.add "components" do |dir| dir.namespaces.add "test", key: nil dir.memoize = true end end end end shared_examples "memoizing components" do it "memoizes the components" do expect(Test::Container["foo"]).to be Test::Container["foo"] end end context "Finalized container" do before do Test::Container.finalize! end include_examples "memoizing components" end context "Non-finalized container" do include_examples "memoizing components" end end describe "Memoizing specific components in a component directory with a memoize proc" do before do class Test::Container < Dry::System::Container configure do |config| config.root = SPEC_ROOT.join("fixtures").realpath config.component_dirs.add "components" do |dir| dir.namespaces.add "test", key: nil dir.memoize = proc do |component| !component.key.match?(/bar/) end end end end end shared_examples "memoizing components" do it "memoizes the components matching the memoize proc" do expect(Test::Container["foo"]).to be Test::Container["foo"] expect(Test::Container["bar"]).not_to be Test::Container["bar"] expect(Test::Container["bar.baz"]).not_to be Test::Container["bar.baz"] end end context "Finalized container" do before do Test::Container.finalize! end include_examples "memoizing components" end context "Non-finalized container" do include_examples "memoizing components" end end describe "Memoizing specific components via magic comments" do shared_examples "memoizing components based on magic comments" do it "memoizes components with memoize: true" do expect(Test::Container).to have_memoized_component "memoize_true_comment" end it "does not memoize components with memoize: false" do expect(Test::Container).not_to have_memoized_component "memoize_false_comment" end end context "No memoizing config for component_dir" do before do class Test::Container < Dry::System::Container configure do |config| config.root = SPEC_ROOT.join("fixtures").realpath config.component_dirs.add "memoize_magic_comments" do |dir| dir.namespaces.add "test", key: nil end end end end include_examples "memoizing components based on magic comments" it "does not memoize components without magic comments" do expect(Test::Container).not_to have_memoized_component "memoize_no_comment" end end context "Memoize config 'false' for component_dir" do before do class Test::Container < Dry::System::Container configure do |config| config.root = SPEC_ROOT.join("fixtures").realpath config.component_dirs.add "memoize_magic_comments" do |dir| dir.namespaces.add "test", key: nil dir.memoize = false end end end end include_examples "memoizing components based on magic comments" it "does not memoize components without magic comments" do expect(Test::Container).not_to have_memoized_component "memoize_no_comment" end end context "Memoize config 'true' for component_dir" do before do class Test::Container < Dry::System::Container configure do |config| config.root = SPEC_ROOT.join("fixtures").realpath config.component_dirs.add "memoize_magic_comments" do |dir| dir.namespaces.add "test", key: nil dir.memoize = true end end end end include_examples "memoizing components based on magic comments" it "memoizes components without magic comments" do expect(Test::Container).to have_memoized_component "memoize_no_comment" end end end end ================================================ FILE: spec/integration/container/auto_registration/mixed_namespaces_spec.rb ================================================ # frozen_string_literal: true RSpec.describe "Auto-registration / Components with mixed namespaces" do before do class Test::Container < Dry::System::Container configure do |config| config.root = SPEC_ROOT.join("fixtures/mixed_namespaces").realpath config.component_dirs.add "lib" do |dir| dir.namespaces.add "test/my_app", key: nil end end end end it "loads components with and without the default namespace (lazy loading)" do aggregate_failures do expect(Test::Container["app_component"]).to be_an_instance_of Test::MyApp::AppComponent expect(Test::Container["test.external.external_component"]).to be_an_instance_of Test::External::ExternalComponent end end it "loads components with and without the default namespace (finalizing)" do Test::Container.finalize! aggregate_failures do expect(Test::Container["app_component"]).to be_an_instance_of Test::MyApp::AppComponent expect(Test::Container["test.external.external_component"]).to be_an_instance_of Test::External::ExternalComponent end end end ================================================ FILE: spec/integration/container/auto_registration_spec.rb ================================================ # frozen_string_literal: true require "dry/system/container" require "zeitwerk" RSpec.describe "Auto-registration" do specify "Resolving components from a non-finalized container, without a default namespace" do module Test class Container < Dry::System::Container configure do |config| config.root = SPEC_ROOT.join("fixtures/standard_container_without_default_namespace").realpath config.component_dirs.add "lib" end end Import = Container.injector end example_with_dep = Test::Container["test.example_with_dep"] expect(example_with_dep).to be_a Test::ExampleWithDep expect(example_with_dep.dep).to be_a Test::Dep end specify "Resolving components from a non-finalized container, with a default namespace" do module Test class Container < Dry::System::Container configure do |config| config.root = SPEC_ROOT.join("fixtures/standard_container_with_default_namespace").realpath config.component_dirs.add "lib" do |dir| dir.namespaces.add "test", key: nil end end end Import = Container.injector end example_with_dep = Test::Container["example_with_dep"] expect(example_with_dep).to be_a Test::ExampleWithDep expect(example_with_dep.dep).to be_a Test::Dep end end ================================================ FILE: spec/integration/container/autoloading_spec.rb ================================================ # frozen_string_literal: true require "dry/system/container" require "dry/system/loader/autoloading" require "zeitwerk" RSpec.describe "Autoloading loader" do specify "Resolving components using Zeitwerk" do module Test class Container < Dry::System::Container config.root = SPEC_ROOT.join("fixtures/autoloading").realpath config.component_dirs.loader = Dry::System::Loader::Autoloading config.component_dirs.add "lib" do |dir| dir.add_to_load_path = false dir.namespaces.add "test", key: nil end end end loader = ZeitwerkLoaderRegistry.new_loader loader.push_dir Test::Container.config.root.join("lib").realpath loader.setup foo = Test::Container["foo"] entity = foo.call expect(foo).to be_a Test::Foo expect(entity).to be_a Test::Entities::FooEntity ZeitwerkLoaderRegistry.clear end end ================================================ FILE: spec/integration/container/importing/container_registration_spec.rb ================================================ # frozen_string_literal: true RSpec.describe "Container / Imports / Container registration" do let(:exporting_container) do Class.new(Dry::System::Container) { register "block_component" do Object.new end register "direct_component", Object.new register "memoized_component", memoize: true do Object.new end register "existing_component", "from exporting container" } end let(:importing_container) do Class.new(Dry::System::Container) { register "existing_component", "from importing container" } end before do importing_container.import(from: exporting_container, as: :other).finalize! end it "imports components with the same options as their original registration" do block_component_a = importing_container["other.block_component"] block_component_b = importing_container["other.block_component"] expect(block_component_a).to be_an_instance_of(block_component_b.class) expect(block_component_a).not_to be block_component_b direct_component_a = importing_container["other.direct_component"] direct_component_b = importing_container["other.direct_component"] expect(direct_component_a).to be direct_component_b memoized_component_a = importing_container["other.memoized_component"] memoized_component_b = importing_container["other.memoized_component"] expect(memoized_component_a).to be memoized_component_b expect(importing_container["existing_component"]).to eql("from importing container") end end ================================================ FILE: spec/integration/container/importing/exports_spec.rb ================================================ # frozen_string_literal: true RSpec.describe "Container / Imports / Exports" do before :context do @dir = make_tmp_directory with_directory @dir do write "lib/private_component.rb", <<~RUBY module Test class PrivateComponent; end end RUBY write "lib/exportable_component_a.rb", <<~RUBY module Test class ExportableComponentA; end end RUBY write "lib/nested/exportable_component_b.rb", <<~RUBY module Test module Nested class ExportableComponentB; end end end RUBY end end let(:exporting_container) { root = @dir exports = self.exports if respond_to?(:exports) Class.new(Dry::System::Container) { configure do |config| config.root = root config.component_dirs.add "lib" do |dir| dir.namespaces.add_root const: "test" end config.exports = exports if exports end } } let(:importing_container) { exporting_container = self.exporting_container Class.new(Dry::System::Container) { import from: exporting_container, as: :other } } context "exports configured as a list of keys" do let(:exports) { %w[ exportable_component_a nested.exportable_component_b ] } context "importing container is lazy loading" do it "can import only the components marked as exports" do expect(importing_container.key?("other.exportable_component_a")).to be true expect(importing_container.key?("other.nested.exportable_component_b")).to be true expect(importing_container.key?("other.private_component")).to be false end it "only loads imported components as required (in both containers)" do importing_container["other.exportable_component_a"] expect(importing_container.keys).to eq ["other.exportable_component_a"] expect(exporting_container.keys).to eq ["exportable_component_a"] end it "does not finalize either container" do importing_container["other.exportable_component_a"] expect(importing_container).not_to be_finalized expect(exporting_container).not_to be_finalized end end context "importing container is finalized" do before do importing_container.finalize! end it "can import only the components explicitly marked as exports" do expect(importing_container.key?("other.exportable_component_a")).to be true expect(importing_container.key?("other.nested.exportable_component_b")).to be true expect(importing_container.key?("other.private_component")).to be false end it "does not finalize the exporting container" do expect(importing_container.key?("other.exportable_component_a")).to be true expect(exporting_container).not_to be_finalized end it "does not load components not marked for export" do expect(exporting_container.keys).to eq [ "exportable_component_a", "nested.exportable_component_b" ] end end end context "exports configured with non-existent components included" do let(:exports) { %w[ exportable_component_a non_existent_component ] } context "importing container is lazy loading" do it "ignores the non-existent keys" do expect(importing_container.key?("other.exportable_component_a")).to be true expect(importing_container.key?("other.non_existent_component")).to be false end end context "importing container is finalized" do before do importing_container.finalize! end it "ignores the non-existent keys" do expect(importing_container.key?("other.exportable_component_a")).to be true expect(importing_container.key?("other.non_existent_component")).to be false end end end context "exports configured as an empty array" do let(:exports) { [] } context "importing container is lazy loading" do it "cannot import anything" do expect(importing_container.key?("other.exportable_component_a")).to be false expect(importing_container.key?("other.nested.exportable_component_b")).to be false expect(importing_container.key?("other.private_component")).to be false end it "does not finalize the exporting container" do expect(importing_container.key?("other.exportable_component_a")).to be false expect(exporting_container).not_to be_finalized end it "does not load any components in the exporting container" do expect(exporting_container.keys).to be_empty end end context "importing container is finalized" do before do importing_container.finalize! end it "cannot import anything" do expect(importing_container.key?("other.exportable_component_a")).to be false expect(importing_container.key?("other.nested.exportable_component_b")).to be false expect(importing_container.key?("other.private_component")).to be false end it "does not finalize the exporting container" do expect(importing_container.key?("other.exportable_component_a")).to be false expect(exporting_container).not_to be_finalized end it "does not load any components in the exporting container" do expect(exporting_container.keys).to be_empty end end end context "exports not configured (defaulting to nil)" do context "importing container is lazy loading" do it "can import all components" do expect(importing_container.key?("other.exportable_component_a")).to be true expect(importing_container.key?("other.nested.exportable_component_b")).to be true expect(importing_container.key?("other.private_component")).to be true end it "only loads imported components as required (in both containers)" do importing_container["other.exportable_component_a"] expect(importing_container.keys).to eq ["other.exportable_component_a"] expect(exporting_container.keys).to eq ["exportable_component_a"] end it "does not finalize either container" do importing_container["other.exportable_component_a"] expect(importing_container).not_to be_finalized expect(exporting_container).not_to be_finalized end end context "importing container is finalized" do before do importing_container.finalize! end it "imports all components" do expect(importing_container.key?("other.exportable_component_a")).to be true expect(importing_container.key?("other.nested.exportable_component_b")).to be true expect(importing_container.key?("other.private_component")).to be true end it "finalizes the exporting container" do expect(exporting_container).to be_finalized end end end end ================================================ FILE: spec/integration/container/importing/import_namespaces_spec.rb ================================================ # frozen_string_literal: true RSpec.describe "Container / Imports / Import namespaces" do before :context do @dir = make_tmp_directory with_directory @dir do write "lib/exportable_component_a.rb", <<~RUBY module Test class ExportableComponentA; end end RUBY write "lib/nested/exportable_component_b.rb", <<~RUBY module Test module Nested class ExportableComponentB; end end end RUBY end end let(:exporting_container) { root = @dir exports = self.exports if respond_to?(:exports) Class.new(Dry::System::Container) { configure do |config| config.root = root config.component_dirs.add "lib" do |dir| dir.namespaces.add_root const: "test" end config.exports = exports if exports end } } context "nil namespace" do context "no keys specified" do let(:importing_container) { exporting_container = self.exporting_container Class.new(Dry::System::Container) { import from: exporting_container, as: nil } } context "importing container is lazy loading" do it "imports all the components" do expect(importing_container.key?("exportable_component_a")).to be true expect(importing_container.key?("nested.exportable_component_b")).to be true expect(importing_container.key?("non_existent")).to be false end end context "importing container is finalized" do before do importing_container.finalize! end it "imports all the components" do expect(importing_container.key?("exportable_component_a")).to be true expect(importing_container.key?("nested.exportable_component_b")).to be true expect(importing_container.key?("non_existent")).to be false end end end context "keys specified" do let(:importing_container) { exporting_container = self.exporting_container Class.new(Dry::System::Container) { import keys: ["exportable_component_a"], from: exporting_container, as: nil } } context "importing container is lazy loading" do it "imports the specified components only" do expect(importing_container.key?("exportable_component_a")).to be true expect(importing_container.key?("nested.exportable_component_b")).to be false end end context "importing container is finalized" do before do importing_container.finalize! end it "imports the specified components only" do expect(importing_container.key?("exportable_component_a")).to be true expect(importing_container.key?("nested.exportable_component_b")).to be false end end end end end ================================================ FILE: spec/integration/container/importing/imported_component_protection_spec.rb ================================================ # frozen_string_literal: true RSpec.describe "Container / Imports / Protection of imported components from export" do let(:source_container_1) { Class.new(Dry::System::Container) { register("component", Object.new) } } let(:source_container_2) { container_1 = source_container_1 Class.new(Dry::System::Container) { register("component", Object.new) import from: container_1, as: :container_1 } } let(:importing_container) { container_2 = source_container_2 Class.new(Dry::System::Container) { import from: container_2, as: :container_2 } } describe "no exports configured" do context "importing container lazy loading" do it "does not import components that were themselves imported" do expect(importing_container.key?("container_2.component")).to be true expect(importing_container.key?("container_2.container_1.component")).to be false end end context "importing container finalized" do before do importing_container.finalize! end it "does not import components that were themselves imported" do expect(importing_container.keys).to eq ["container_2.component"] end end end describe "exports configured with imported components included" do let(:source_container_2) { container_1 = source_container_1 Class.new(Dry::System::Container) { configure do |config| config.exports = %w[component container_1.component] end register("component", Object.new) import from: container_1, as: :container_1 } } context "importing container lazy loading" do it "imports the previously-imported component" do expect(importing_container.key?("container_2.component")).to be true expect(importing_container.key?("container_2.container_1.component")).to be true end end context "importing container finalized" do before do importing_container.finalize! end it "imports the previously-imported component" do expect(importing_container.keys).to eq %w[container_2.component container_2.container_1.component] end end end end ================================================ FILE: spec/integration/container/importing/partial_imports_spec.rb ================================================ # frozen_string_literal: true RSpec.describe "Container / Imports / Partial imports" do before :context do @dir = make_tmp_directory with_directory @dir do write "lib/exportable_component_a.rb", <<~RUBY module Test class ExportableComponentA; end end RUBY write "lib/exportable_component_b.rb", <<~RUBY module Test module Nested class ExportableComponentB; end end end RUBY end end let(:exporting_container) { root = @dir exports = self.exports if respond_to?(:exports) Class.new(Dry::System::Container) { configure do |config| config.root = root config.component_dirs.add "lib" do |dir| dir.namespaces.add_root const: "test" end config.exports = exports if exports end } } let(:importing_container) { exporting_container = self.exporting_container import_keys = self.import_keys Class.new(Dry::System::Container) { import keys: import_keys, from: exporting_container, as: :other } } let(:import_keys) { %w[exportable_component_a] } context "no exports configured (whole container export)" do context "lazy loading" do it "imports the specified components only" do expect(importing_container.key?("other.exportable_component_a")).to be true expect(importing_container.key?("other.exportable_component_b")).to be false end end context "finalized" do before do importing_container.finalize! end it "imports the specified components only" do expect(importing_container.key?("other.exportable_component_a")).to be true expect(importing_container.key?("other.exportable_component_b")).to be false end end end context "exports configured (with import keys included)" do let(:exports) { %w[exportable_component_a exportable_component_b] } context "lazy loading" do it "imports the specified components only" do expect(importing_container.key?("other.exportable_component_a")).to be true expect(importing_container.key?("other.exportable_component_b")).to be false end end context "finalized" do before do importing_container.finalize! end it "imports the specified components only" do expect(importing_container.key?("other.exportable_component_a")).to be true expect(importing_container.key?("other.exportable_component_b")).to be false end end end context "exports configured (with import keys not included)" do let(:exports) { %w[exportable_component_b] } context "lazy loading" do it "does not import any components" do expect(importing_container.key?("other.exportable_component_a")).to be false end end context "finalized" do before do importing_container.finalize! end it "does not import any components" do expect(importing_container.key?("other.exportable_component_a")).to be false end end end context "import keys specified that do not exist in exporting container" do let(:import_keys) { %w[exportable_component_a non_existent_key] } context "lazy loading" do it "imports the existent components only" do expect(importing_container.key?("other.exportable_component_a")).to be true expect(importing_container.key?("other.non_existent_key")).to be false end end context "finalized" do before do importing_container.finalize! end it "imports the existent components only" do expect(importing_container.key?("other.exportable_component_a")).to be true expect(importing_container.key?("other.non_existent_key")).to be false end end end end ================================================ FILE: spec/integration/container/lazy_loading/auto_registration_disabled_spec.rb ================================================ # frozen_string_literal: true RSpec.describe "Lazy loading components with auto-registration disabled" do before do module Test class Container < Dry::System::Container configure do |config| config.root = SPEC_ROOT.join("fixtures/lazy_loading/auto_registration_disabled").realpath config.component_dirs.add "lib" end end end end it "reports the component as absent" do expect(Test::Container.key?("entities.kitten")).to be false end it "does not load the component" do expect { Test::Container["entities.kitten"] }.to raise_error(Dry::Core::Container::KeyError) end end ================================================ FILE: spec/integration/container/lazy_loading/bootable_components_spec.rb ================================================ # frozen_string_literal: true RSpec.describe "Lazy loading bootable components" do describe "Booting component when resolving another components with bootable component as root key" do before do module Test class Container < Dry::System::Container configure do |config| config.root = SPEC_ROOT.join("fixtures/lazy_loading/shared_root_keys").realpath config.component_dirs.add "lib" end end end end context "Single container" do it "boots the component and can resolve multiple other components registered using the same root key" do expect(Test::Container["kitten_service.fetch_kitten"]).to be expect(Test::Container.keys).to include("kitten_service.client", "kitten_service.fetch_kitten") expect(Test::Container["kitten_service.submit_kitten"]).to be expect(Test::Container.keys).to include("kitten_service.client", "kitten_service.fetch_kitten", "kitten_service.submit_kitten") end end context "Bootable component in imported container" do before do module Test class AnotherContainer < Dry::System::Container import from: Container, as: :core end end end context "lazy loading" do it "boots the component and can resolve multiple other components registered using the same root key" do expect(Test::AnotherContainer["core.kitten_service.fetch_kitten"]).to be expect(Test::AnotherContainer.keys).to include("core.kitten_service.fetch_kitten") expect(Test::AnotherContainer["core.kitten_service.submit_kitten"]).to be expect(Test::AnotherContainer.keys).to include("core.kitten_service.submit_kitten") expect(Test::AnotherContainer["core.kitten_service.client"]).to be expect(Test::AnotherContainer.keys).to include("core.kitten_service.client") end end context "finalized" do before do Test::AnotherContainer.finalize! end it "boots the component in the imported container and imports the bootable component's registered components" do expect(Test::AnotherContainer.keys).to include("core.kitten_service.fetch_kitten", "core.kitten_service.submit_kitten", "core.kitten_service.client") end end end end end ================================================ FILE: spec/integration/container/lazy_loading/manifest_registration_spec.rb ================================================ # frozen_string_literal: true RSpec.describe "Lazy-loading registration manifest files" do module Test; end def build_container Class.new(Dry::System::Container) do configure do |config| config.root = SPEC_ROOT.join("fixtures/manifest_registration").realpath end end end shared_examples "manifest component" do before do Test::Container = build_container Test::Container.add_to_load_path!("lib") end it "loads a registration manifest file if the component could not be found" do expect(Test::Container["foo.special"]).to be_a(Test::Foo) expect(Test::Container["foo.special"].name).to eq "special" end end context "Non-finalized container" do include_examples "manifest component" end context "Finalized container" do include_examples "manifest component" before { Test::Container.finalize! } end context "Autoloaded container" do let :autoloader do Zeitwerk::Loader.new.tap do |loader| loader.enable_reloading # This is a simulacrum of the Dry::Rails container reset # that happens on every reload loader.on_setup do if Test.const_defined?(:Container) Test.__send__(:remove_const, :Container) end Test.const_set :Container, build_container Test::Container.finalize! loader.push_dir(Test::Container.root) end end end it "reloads manifest keys" do autoloader.setup expect(Test::Container.keys).to include("foo.special") autoloader.reload expect(Test::Container.keys).to include("foo.special") end end end ================================================ FILE: spec/integration/container/plugins/bootsnap_spec.rb ================================================ # frozen_string_literal: true RSpec.describe "Plugins / Bootsnap" do subject(:system) do Class.new(Dry::System::Container) do use :bootsnap configure do |config| config.root = SPEC_ROOT.join("fixtures/test") config.env = :development config.bootsnap = { load_path_cache: false, compile_cache_iseq: true, compile_cache_yaml: true } end end end let(:cache_dir) do system.root.join("tmp/cache") end let(:bootsnap_cache_file) do cache_dir.join("bootsnap") end before do FileUtils.rm_rf(cache_dir) FileUtils.mkdir_p(cache_dir) end after do FileUtils.rm_rf(cache_dir) end describe ".require_from_root" do if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.4.0") it "loads file" do system.require_from_root("lib/test/models") expect(Object.const_defined?("Test::Models")).to be(true) expect(bootsnap_cache_file.exist?).to be(true) end end end end ================================================ FILE: spec/integration/container/plugins/dependency_graph_spec.rb ================================================ # frozen_string_literal: true RSpec.describe "Plugins / Dependency Graph" do let(:container) { Test::Container } subject(:events) { [] } before :context do with_directory(@dir = make_tmp_directory) do write "system/providers/mailer.rb", <<~RUBY Test::Container.register_provider :mailer do start do register "mailer", Object.new end end RUBY write "lib/foo.rb", <<~RUBY module Test class Foo include Deps["mailer"] end end RUBY write "lib/bar.rb", <<~RUBY module Test class Bar include Deps["foo"] end end RUBY end end before do root = @dir Test::Container = Class.new(Dry::System::Container) { use :dependency_graph configure do |config| config.root = root config.component_dirs.add "lib" do |dir| dir.namespaces.add_root const: "test" end end } end before do container[:notifications].subscribe(:resolved_dependency) { events << _1 } container[:notifications].subscribe(:registered_dependency) { events << _1 } end shared_examples "dependency graph notifications" do context "lazy loading" do it "emits dependency notifications for the resolved component" do container["foo"] expect(events.map { [_1.id, _1.payload] }).to eq [ [:resolved_dependency, {dependency_map: {mailer: "mailer"}, target_class: Test::Foo}], [:registered_dependency, {class: Object, key: "mailer"}], [:registered_dependency, {class: Test::Foo, key: "foo"}] ] end end context "finalized" do before do container.finalize! end it "emits dependency notifications for all components" do expect(events.map { [_1.id, _1.payload] }).to eq [ [:registered_dependency, {key: "mailer", class: Object}], [:resolved_dependency, {dependency_map: {foo: "foo"}, target_class: Test::Bar}], [:resolved_dependency, {dependency_map: {mailer: "mailer"}, target_class: Test::Foo}], [:registered_dependency, {key: "foo", class: Test::Foo}], [:registered_dependency, {key: "bar", class: Test::Bar}] ] end end end describe "default (kwargs) injector" do before do Test::Deps = Test::Container.injector end specify "objects receive dependencies via keyword arguments" do expect(container["bar"].method(:initialize).parameters).to eq( [[:keyrest, :kwargs], [:block, :block]] ) end it_behaves_like "dependency graph notifications" end describe "hash injector" do before do Test::Deps = Test::Container.injector.hash end specify "objects receive dependencies via a single options hash argument" do expect(container["bar"].method(:initialize).parameters).to eq [[:req, :options]] end it_behaves_like "dependency graph notifications" end describe "args injector" do before do Test::Deps = Test::Container.injector.args end specify "objects receive dependencies via positional arguments" do expect(container["bar"].method(:initialize).parameters).to eq [[:req, :foo]] end it_behaves_like "dependency graph notifications" end end ================================================ FILE: spec/integration/container/plugins/env_spec.rb ================================================ # frozen_string_literal: true RSpec.describe "Plugins / Env" do context "with default settings" do subject(:system) do Class.new(Dry::System::Container) do use :env end end describe ".env" do it "returns :development" do expect(system.env).to be(:development) end end end context "with a custom inferrer" do subject(:system) do Class.new(Dry::System::Container) do use :env, inferrer: -> { :test } end end describe ".env" do it "returns :test" do expect(system.env).to be(:test) end end end end ================================================ FILE: spec/integration/container/plugins/logging_spec.rb ================================================ # frozen_string_literal: true RSpec.describe "Plugins / Logging" do before do system.configure do |config| config.root = SPEC_ROOT.join("fixtures/test") end end let(:logger) do system.logger end let(:log_file_content) do File.read(system.log_file_path) end context "with default logger settings" do subject(:system) do class Test::Container < Dry::System::Container use :env use :logging end end it "logs to development.log" do logger.info "info message" expect(log_file_content).to include("info message") end end end ================================================ FILE: spec/integration/container/plugins/zeitwerk/eager_loading_spec.rb ================================================ # frozen_string_literal: true # rubocop:disable Style/GlobalVars RSpec.describe "Zeitwerk plugin / Eager loading" do before do $eager_loaded = false allow(Zeitwerk::Loader).to receive(:new).and_return(ZeitwerkLoaderRegistry.new_loader) end after { ZeitwerkLoaderRegistry.clear } it "Eager loads after finalization" do with_tmp_directory do |tmp_dir| write "lib/zeitwerk_eager.rb", <<~RUBY $eager_loaded = true module Test class ZeitwerkEager; end end RUBY container = Class.new(Dry::System::Container) do use :zeitwerk, eager_load: true configure do |config| config.root = tmp_dir config.component_dirs.add "lib" do |dir| dir.namespaces.add_root const: "test" end end end expect { container.finalize! }.to change { $eager_loaded }.to true end end it "Eager loads in production by default" do with_tmp_directory do |tmp_dir| write "lib/zeitwerk_eager.rb", <<~RUBY $eager_loaded = true module Test class ZeitwerkEager; end end RUBY container = Class.new(Dry::System::Container) do use :env, inferrer: -> { :production } use :zeitwerk configure do |config| config.root = tmp_dir config.component_dirs.add "lib" do |dir| dir.namespaces.add_root const: "test" end end end expect { container.finalize! }.to change { $eager_loaded }.to true end end end # rubocop:enable Style/GlobalVars ================================================ FILE: spec/integration/container/plugins/zeitwerk/namespaces_spec.rb ================================================ # frozen_string_literal: true RSpec.describe "Zeitwerk plugin / Namespaces" do before do allow(Zeitwerk::Loader).to receive(:new).and_return(ZeitwerkLoaderRegistry.new_loader) end after { ZeitwerkLoaderRegistry.clear } it "loads components from a root namespace with a const namespace" do with_tmp_directory do |tmp_dir| write "lib/foo.rb", <<~RUBY module Test class Foo; end end RUBY container = Class.new(Dry::System::Container) do use :zeitwerk configure do |config| config.root = tmp_dir config.component_dirs.add "lib" do |dir| dir.namespaces.add_root const: "test" end end end expect(container["foo"]).to be_an_instance_of Test::Foo end end it "loads components from multiple namespace with distinct const namespaces" do with_tmp_directory do |tmp_dir| write "lib/foo.rb", <<~RUBY module Test class Foo; end end RUBY write "lib/nested/foo.rb", <<~RUBY module Test module Nested class Foo; end end end RUBY write "lib/adapters/bar.rb", <<~RUBY module My module Adapters class Bar; end end end RUBY container = Class.new(Dry::System::Container) do use :zeitwerk configure do |config| config.root = tmp_dir config.component_dirs.add "lib" do |dir| dir.namespaces.add "adapters", const: "my/adapters" dir.namespaces.add_root const: "test" end end end expect(container["foo"]).to be_an_instance_of Test::Foo expect(container["nested.foo"]).to be_an_instance_of Test::Nested::Foo expect(container["adapters.bar"]).to be_an_instance_of My::Adapters::Bar end end end ================================================ FILE: spec/integration/container/plugins/zeitwerk/resolving_components_spec.rb ================================================ # frozen_string_literal: true RSpec.describe "Zeitwerk plugin / Resolving components" do before do allow(Zeitwerk::Loader).to receive(:new).and_return(ZeitwerkLoaderRegistry.new_loader) end after { ZeitwerkLoaderRegistry.clear } specify "Resolving components using Zeitwerk" do with_tmp_directory do |tmp_dir| write "lib/foo.rb", <<~RUBY module Test class Foo def call Entities::FooEntity.new end end end RUBY write "lib/entities/foo_entity.rb", <<~RUBY module Test module Entities class FooEntity; end end end RUBY container = Class.new(Dry::System::Container) do use :zeitwerk configure do |config| config.root = tmp_dir config.component_dirs.add "lib" do |dir| dir.namespaces.add_root const: "test" end end end foo = container["foo"] entity = foo.call expect(foo).to be_a Test::Foo expect(entity).to be_a Test::Entities::FooEntity end end end ================================================ FILE: spec/integration/container/plugins/zeitwerk/user_configured_loader_spec.rb ================================================ # frozen_string_literal: true RSpec.describe "Zeitwerk plugin / User-configured loader" do before do allow(Zeitwerk::Loader).to receive(:new).and_return(ZeitwerkLoaderRegistry.new_loader) end after { ZeitwerkLoaderRegistry.clear } it "uses the user-configured loader and pushes component dirs to it" do with_tmp_directory do |tmp_dir| write "lib/foo.rb", <<~RUBY module Test class Foo;end end RUBY require "zeitwerk" logs = [] container = Class.new(Dry::System::Container) do use :zeitwerk configure do |config| config.root = tmp_dir config.component_dirs.add "lib" do |dir| dir.namespaces.add_root const: "test" end config.autoloader = Zeitwerk::Loader.new.tap do |loader| loader.tag = "custom_loader" loader.logger = -> str { logs << str } end end end expect(container["foo"]).to be_a Test::Foo expect(logs).not_to be_empty expect(logs[0]).to include "custom_loader" end end end ================================================ FILE: spec/integration/container/plugins_spec.rb ================================================ # frozen_string_literal: true RSpec.describe Dry::System::Container, ".use" do subject(:system) do Class.new(Dry::System::Container) end before do # Store globals Test::PluginRegistry = Dry::System::Plugins.registry.dup Test::LoadedDependencies = Dry::System::Plugins.loaded_dependencies.dup end after do # Restore globals Dry::System::Plugins.instance_eval do @registry = Test::PluginRegistry @loaded_dependencies = Test::LoadedDependencies end end context "with a plugin which has dependencies" do let(:plugin) do Module.new do def self.dependencies SPEC_ROOT.join("fixtures/test/lib/test/dep") end end end before do Dry::System::Plugins.register(:test_plugin, plugin) end context "when another plugin has the same dependency" do context "and dependencies are defined with the same type" do before do Dry::System::Plugins.register(:test_plugin_2, plugin) end it "loads plugin and does not duplicate loaded_dependencies" do system.use(:test_plugin) system.use(:test_plugin_2) expect(Object.const_defined?("Test::Dep")).to be(true) expect( Dry::System::Plugins.loaded_dependencies.count { |dep| dep == SPEC_ROOT.join("fixtures/test/lib/test/dep").to_s } ).to be(1) end end context "and dependencies are not defined with the same type" do let(:plugin_2) do Module.new do def self.dependencies SPEC_ROOT.join("fixtures/test/lib/test/dep").to_s end end end before do Dry::System::Plugins.register(:test_plugin_2, plugin_2) end it "loads plugin and does not duplicate loaded_dependencies" do system.use(:test_plugin) system.use(:test_plugin_2) expect(Object.const_defined?("Test::Dep")).to be(true) expect( Dry::System::Plugins.loaded_dependencies.count { |dep| dep == SPEC_ROOT.join("fixtures/test/lib/test/dep").to_s } ).to be(1) end end end context "when dependency is available" do it "auto-requires dependency" do system.use(:test_plugin) expect(Object.const_defined?("Test::Dep")).to be(true) end end context "when dependency gem is not available" do let(:plugin) do Module.new do def self.dependencies {gem_name: "this-does-not-exist"} end end end it "raises exception" do msg = "dry-system plugin :test_plugin failed to load its dependencies: " \ "cannot load such file -- this-does-not-exist - add gem_name to your Gemfile" expect { system.use(:test_plugin) } .to raise_error(Dry::System::PluginDependencyMissing, msg) end end context "when dependency is not available" do let(:plugin) do Module.new do def self.dependencies "this-does-not-exist" end end end it "raises exception" do msg = "dry-system plugin :test_plugin failed to load its dependencies: " \ "cannot load such file -- this-does-not-exist" expect { system.use(:test_plugin) } .to raise_error(Dry::System::PluginDependencyMissing, msg) end end end context "with a stateless plugin" do let(:plugin) do Module.new do def plugin_enabled? true end end end context "plugin without a block" do before do Dry::System::Plugins.register(:test_plugin, plugin) end it "enables a plugin" do system.use(:test_plugin) expect(system).to be_plugin_enabled end end context "plugin with a block" do before do Dry::System::Plugins.register(:test_plugin, plugin) do setting :foo, default: "bar" end end it "enables a plugin which evaluates its block" do system.use(:test_plugin) expect(system).to be_plugin_enabled expect(system.config.foo).to eql("bar") end end context "inheritance" do before do Dry::System::Plugins.register(:test_plugin, plugin) do setting :trace, default: [] after(:configure) do config.trace << :test_plugin_configured end end end it "enables plugin for a class and its descendant" do system.use(:test_plugin) descendant = Class.new(system) system.configure {} descendant.configure {} expect(system.config.trace).to eql([:test_plugin_configured]) expect(descendant.config.trace).to eql([:test_plugin_configured]) end end context "calling multiple times" do before do Dry::System::Plugins.register(:test_plugin, plugin) do setting :trace, default: [] after(:configure) do config.trace << :test_plugin_configured end end end it "enables the plugin only once" do system.use(:test_plugin).use(:test_plugin).configure {} expect(system.config.trace).to eql([:test_plugin_configured]) end end end context "with a stateful plugin" do let(:plugin) do Class.new(Module) do def initialize(options) super() @options = options define_method(:plugin_test) do options[:value] end end end end before do Dry::System::Plugins.register(:test_plugin, plugin) end it "enables a plugin" do system.use(:test_plugin, value: "bar") expect(system.plugin_test).to eql("bar") end end context "misspeled plugin name" do it "raises meaningful error" do expect { system.use :wrong_name } .to raise_error(Dry::System::PluginNotFoundError, "Plugin :wrong_name does not exist") end end end ================================================ FILE: spec/integration/container/providers/conditional_providers_spec.rb ================================================ # frozen_string_literal: true RSpec.describe "Providers / Conditional providers" do let(:container) { provider_if = self.provider_if Class.new(Dry::System::Container) { register_provider :provided, if: provider_if do start do register "provided", Object.new end end } } shared_examples "loads the provider" do it "runs the provider when a related component is resolved" do expect(container["provided"]).to be expect(container.providers.key?(:provided)).to be true end end shared_examples "does not load the provider" do it "does not run the provider when a related component is resolved" do expect { container["provided"] }.to raise_error(Dry::Core::Container::KeyError, /key not found: "provided"/) expect(container.providers.key?(:provided)).to be false end end describe "true" do let(:provider_if) { true } context "lazy loading" do include_examples "loads the provider" end context "finalized" do before { container.finalize! } include_examples "loads the provider" end end describe "false" do let(:provider_if) { false } context "lazy loading" do include_examples "does not load the provider" end context "finalized" do before { container.finalize! } include_examples "does not load the provider" end end describe "provider file in provider dir" do let(:container) { root = @dir Test::Container = Class.new(Dry::System::Container) { configure do |config| config.root = root end } } describe "true" do before :context do with_directory(@dir = make_tmp_directory) do write "system/providers/provided.rb", <<~RUBY Test::Container.register_provider :provided, if: true do start do register "provided", Object.new end end RUBY end end context "lazy loading" do include_examples "loads the provider" end context "finalized" do before { container.finalize! } include_examples "loads the provider" end end describe "true" do before :context do with_directory(@dir = make_tmp_directory) do write "system/providers/provided.rb", <<~RUBY Test::Container.register_provider :provided, if: false do start do register "provided", Object.new end end RUBY end end context "lazy loading" do include_examples "does not load the provider" end context "finalized" do before { container.finalize! } include_examples "does not load the provider" end end end end ================================================ FILE: spec/integration/container/providers/custom_provider_registrar_spec.rb ================================================ # frozen_string_literal: true RSpec.describe "Providers / Custom provider registrar" do specify "Customizing the target_container for providers" do # Create a provider registrar that exposes a container _wrapper_ (i.e. something resembling a # Hanami slice) as the target_container. provider_registrar = Class.new(Dry::System::ProviderRegistrar) do def self.for_wrapper(wrapper) Class.new(self) do define_singleton_method(:new) do |container| super(container, wrapper) end end end attr_reader :wrapper def initialize(container, wrapper) super(container) @wrapper = wrapper end def target_container wrapper end end # Create the wrapper, which has an internal Dry::System::Container (configured with our custom # provider_registrar) that it then delegates to. container_wrapper = Class.new do define_singleton_method(:container) do @container ||= Class.new(Dry::System::Container).tap do |container| container.config.provider_registrar = provider_registrar.for_wrapper(self) end end def self.register_provider(...) container.register_provider(...) end def self.start(...) container.start(...) end end # Create a provider to expose its given `target` so we can make expecations about it exposed_target = nil container_wrapper.register_provider(:my_provider) do start do exposed_target = target end end container_wrapper.start(:my_provider) expect(exposed_target).to be container_wrapper end end ================================================ FILE: spec/integration/container/providers/custom_provider_superclass_spec.rb ================================================ # frozen_string_literal: true RSpec.describe "Providers / Custom provider superclass" do let!(:custom_superclass) do module Test class CustomSource < Dry::System::Provider::Source attr_reader :custom_setting def initialize(custom_setting:, **options, &) super(**options, &) @custom_setting = custom_setting end end end Test::CustomSource end let!(:custom_registrar) do module Test class CustomRegistrar < Dry::System::ProviderRegistrar def provider_source_class = Test::CustomSource def provider_source_options = {custom_setting: "hello"} end end Test::CustomRegistrar end subject(:system) do module Test class Container < Dry::System::Container configure do |config| config.root = SPEC_ROOT.join("fixtures/app").realpath config.provider_registrar = Test::CustomRegistrar end end end Test::Container end it "overrides the default Provider Source base class" do system.register_provider(:test) {} provider_source = system.providers[:test].source expect(provider_source.class).to be < custom_superclass expect(provider_source.class.name).to eq "Test::CustomSource[test]" expect(provider_source.custom_setting).to eq "hello" end context "Source class != provider_source_class" do let!(:custom_source) do module Test class OtherSource < Dry::System::Provider::Source attr_reader :options def initialize(**options, &block) @options = options.except(:provider_container, :target_container) super(**options.slice(:provider_container, :target_container), &block) end end end Test::OtherSource end specify "External source doesn't use provider_source_options" do Dry::System.register_provider_source(:test, group: :custom, source: custom_source) system.register_provider(:test, from: :custom) {} expect { provider_source = system.providers[:test].source expect(provider_source.class).to be < Dry::System::Provider::Source expect(provider_source.options).to be_empty }.to_not raise_error end specify "Class-based source doesn't use provider_source_options" do system.register_provider(:test, source: custom_source) expect { provider_source = system.providers[:test].source expect(provider_source.class).to be < Dry::System::Provider::Source expect(provider_source.options).to be_empty }.to_not raise_error end end end ================================================ FILE: spec/integration/container/providers/multiple_provider_dirs_spec.rb ================================================ # frozen_string_literal: true RSpec.describe "Providers / Multiple provider dirs" do specify "Resolving provider files from multiple provider dirs" do module Test class Container < Dry::System::Container config.root = SPEC_ROOT.join("fixtures/multiple_provider_dirs").realpath config.provider_dirs = [ "custom_bootables", # Relative paths are appended to the container root SPEC_ROOT.join("fixtures/multiple_provider_dirs/default_bootables") ] end end expect(Test::Container[:inflector]).to eq "default_inflector" expect(Test::Container[:logger]).to eq "custom_logger" end end ================================================ FILE: spec/integration/container/providers/provider_sources/provider_options_spec.rb ================================================ # frozen_string_literal: true RSpec.describe "Providers / Provider sources / Provider options" do let(:container) { Class.new(Dry::System::Container) } specify "provider_options registered with provider sources are used when creating corresponding providers" do Dry::System.register_provider_source(:db, group: :my_framework, provider_options: {namespace: true}) do start do register "config", "db_config_here" end end # Note no `namespace:` option when registering provider container.register_provider :db, from: :my_framework # Also works when using a different name for the provider container.register_provider :my_db, from: :my_framework, source: :db container.start :db container.start :my_db expect(container["db.config"]).to eq "db_config_here" expect(container["my_db.config"]).to eq "db_config_here" end specify "provider source provider_options can be overridden" do Dry::System.register_provider_source(:db, group: :my_framework, provider_options: {namespace: true}) do start do register "config", "db_config_here" end end container.register_provider :db, from: :my_framework, namespace: "custom_db" container.start :db expect(container["custom_db.config"]).to eq "db_config_here" end end ================================================ FILE: spec/integration/container/providers/registering_components_spec.rb ================================================ # frozen_string_literal: true RSpec.describe "Providers / Registering components" do specify "Components registered with blocks in a provider are resolved as new objects each time in the target container" do module Test class Thing; end end container = Class.new(Dry::System::Container) do register_provider :thing, namespace: true do start do register :via_block do Test::Thing.new end register :direct, Test::Thing.new end end end container.start :thing thing_via_block_1 = container["thing.via_block"] thing_via_block_2 = container["thing.via_block"] thing_direct_1 = container["thing.direct"] thing_direct_2 = container["thing.direct"] expect(thing_via_block_1).to be_an_instance_of(thing_via_block_2.class) expect(thing_via_block_1).not_to be thing_via_block_2 expect(thing_direct_1).to be thing_direct_2 end specify "Components registered with options in a provider have those options set on the target container" do container = Class.new(Dry::System::Container) do register_provider :thing do start do register :thing, memoize: true do Object.new end end end end container.start :thing thing_1 = container["thing"] thing_2 = container["thing"] expect(thing_2).to be thing_1 end specify "Components registered with keys that are already used on the target container are not applied" do container = Class.new(Dry::System::Container) do register_provider :thing, namespace: true do start do register :first, Object.new register :second, Object.new end end end already_registered = Object.new container.register "thing.second", already_registered container.start :thing expect(container["thing.first"]).to be expect(container["thing.second"]).to be already_registered end end ================================================ FILE: spec/integration/container/providers/resolving_root_key_spec.rb ================================================ # frozen_string_literal: true RSpec.describe "Providers / Resolving components with same root key as a running provider" do before :context do @dir = make_tmp_directory with_directory(@dir) do write "lib/animals/cat.rb", <<~RUBY module Test module Animals class Cat include Deps["animals.collar"] end end end RUBY write "lib/animals/collar.rb", <<~RUBY module Test module Animals class Collar; end end end RUBY write "system/providers/animals.rb", <<~RUBY Test::Container.register_provider :animals, namespace: true do start do require "animals/cat" register :cat, Test::Animals::Cat.new end end RUBY end end before do root = @dir Test::Container = Class.new(Dry::System::Container) do configure do |config| config.root = root config.component_dirs.add "lib" do |dir| dir.namespaces.add_root const: "test" end end end Test::Deps = Test::Container.injector end context "lazy loading" do it "resolves the component without attempting to re-run provider steps" do expect(Test::Container["animals.cat"]).to be end end context "finalized" do before do Test::Container.finalize! end it "resolves the component without attempting to re-run provider steps" do expect(Test::Container["animals.cat"]).to be end end end ================================================ FILE: spec/integration/did_you_mean_integration_spec.rb ================================================ # frozen_string_literal: true require "ostruct" RSpec.describe "DidYouMean integration" do subject(:system) { Test::Container } context "with a file with a syntax error in it" do before do class Test::Container < Dry::System::Container use :zeitwerk configure do |config| config.root = SPEC_ROOT.join("fixtures").join("components_with_errors").realpath config.component_dirs.add "test" end end end it "auto-boots dependency of a bootable component" do expect { system["constant_error"] } .to raise_error(NameError, "uninitialized constant ConstantError::NotHere") end end end ================================================ FILE: spec/integration/external_components_spec.rb ================================================ # frozen_string_literal: true RSpec.describe "External Components" do before do Object.send(:remove_const, :ExternalComponents) if defined? ExternalComponents end subject(:container) do module Test class Container < Dry::System::Container configure do |config| config.root = SPEC_ROOT.join("fixtures/app").realpath end register_provider(:logger, from: :external_components) register_provider(:my_logger, from: :external_components, source: :logger) do configure do |config| config.log_level = :debug end after(:start) do register(:my_logger, container[:logger]) end end register_provider(:notifier, from: :external_components) register_provider(:mailer, from: :external_components) register(:monitor, "a monitor") end end Test::Container end before do require SPEC_ROOT.join("fixtures/external_components/lib/external_components") end context "with default behavior" do it "boots external logger component" do container.finalize! expect(container[:logger]).to be_instance_of(ExternalComponents::Logger) end it "boots external logger component with customized booting process" do container.finalize! my_logger = container[:my_logger] expect(my_logger).to be_instance_of(ExternalComponents::Logger) expect(my_logger.log_level).to eq(:debug) end it "boots external notifier component which needs a local component" do container.finalize! notifier = container[:notifier] expect(notifier.monitor).to be(container[:monitor]) end it "boots external mailer component which needs a local bootable component" do container.finalize! mailer = container[:mailer] expect(mailer.client).to be(container[:client]) end end context "with customized booting" do it "allows aliasing external components" do container.register_provider(:error_logger, from: :external_components, source: :logger) do after(:start) do register(:error_logger, container[:logger]) end end container.finalize! expect(container[:error_logger]).to be_instance_of(ExternalComponents::Logger) end it "allows calling :prepare manually" do container.register_provider(:error_logger, from: :external_components, source: :logger) do after(:prepare) do ExternalComponents::Logger.default_level = :error end after(:start) do register(:error_logger, container[:logger]) end end container.prepare(:error_logger) expect(container[:error_logger]).to be_instance_of(ExternalComponents::Logger) expect(container[:error_logger].class.default_level).to be(:error) end end context "customized registration from an alternative provider" do subject(:container) do Class.new(Dry::System::Container) do register_provider(:logger, from: :external_components) register_provider(:conn, from: :alt, source: :db) do after(:start) do register(:conn, container[:db_conn]) end end end end before do require SPEC_ROOT.join("fixtures/external_components/lib/external_components") end context "with default behavior" do it "boots external logger component from the specified provider" do container.finalize! expect(container[:logger]).to be_instance_of(ExternalComponents::Logger) expect(container[:conn]).to be_instance_of(AltComponents::DbConn) end it "lazy-boots external logger components" do expect(container[:logger]).to be_instance_of(ExternalComponents::Logger) end end end it "raises an error when specifying an unknown provider sourse" do msgs = [ "Provider source not found: :logger, group: :not_found_components", "Available provider sources:", "- :logger, group: :external_components" ] error_re = /#{msgs.join(".*")}/m expect { Class.new(Dry::System::Container) { register_provider(:logger, from: :not_found_components) } }.to raise_error Dry::System::ProviderSourceNotFoundError, error_re end end ================================================ FILE: spec/integration/import_spec.rb ================================================ # frozen_string_literal: true require "dry/system/stubs" RSpec.describe "Lazy-booting external deps" do before do module Test class Umbrella < Dry::System::Container configure do |config| config.name = :core config.root = SPEC_ROOT.join("fixtures/umbrella").realpath end end class App < Dry::System::Container configure do |config| config.name = :main end end end end shared_examples_for "lazy booted dependency" do it "lazy boots an external dep provided by top-level container" do expect(user_repo.repo).to be_instance_of(Db::Repo) end it "loads an external dep during finalization" do system.finalize! expect(user_repo.repo).to be_instance_of(Db::Repo) end end context "when top-level container provides the dependency" do let(:user_repo) do Class.new { include Test::Import["db.repo"] }.new end let(:system) { Test::Umbrella } before do module Test Umbrella.import(from: App, as: :main) Import = Umbrella.injector end end it_behaves_like "lazy booted dependency" context "when stubs are enabled" do before do system.enable_stubs! end it_behaves_like "lazy booted dependency" end end context "when top-level container provides the dependency through import" do let(:user_repo) do Class.new { include Test::Import["core.db.repo"] }.new end let(:system) { Test::App } before do module Test App.import(from: Umbrella, as: :core) Import = App.injector end end it_behaves_like "lazy booted dependency" context "when stubs are enabled" do before do system.enable_stubs! end it_behaves_like "lazy booted dependency" end end end ================================================ FILE: spec/integration/settings_component_spec.rb ================================================ # frozen_string_literal: true require "dry/system/provider_sources" RSpec.describe "Settings component" do subject(:system) do Class.new(Dry::System::Container) do setting :env configure do |config| config.root = SPEC_ROOT.join("fixtures").join("settings_test") config.env = :test end register_provider(:settings, from: :dry_system) do before(:prepare) do target_container.require_from_root "types" end settings do setting :database_url, constructor: SettingsTest::Types::String.constrained(filled: true) setting :session_secret, constructor: SettingsTest::Types::String.constrained(filled: true) end end end end let(:settings) do system[:settings] end before do ENV["DATABASE_URL"] = "sqlite::memory" end after do ENV.delete("DATABASE_URL") end it "sets up system settings component via ENV and .env" do expect(settings.database_url).to eql("sqlite::memory") expect(settings.session_secret).to eql("super-secret") end context "Invalid setting value" do subject(:system) do Class.new(Dry::System::Container) do setting :env configure do |config| config.root = SPEC_ROOT.join("fixtures").join("settings_test") config.env = :test end register_provider(:settings, from: :dry_system) do before(:prepare) do target_container.require_from_root "types" end settings do setting :integer_value, constructor: SettingsTest::Types::Integer setting :coercible_value, constructor: SettingsTest::Types::Coercible::Integer end end end end before do ENV["INTEGER_VALUE"] = "foo" ENV["COERCIBLE_VALUE"] = "foo" end after do ENV.delete("INTEGER_VALUE") ENV.delete("COERCIBLE_VALUE") end it "raises InvalidSettingsError with meaningful message" do expect { settings.integer_value }.to raise_error( Dry::System::ProviderSources::Settings::InvalidSettingsError, <<~TEXT Could not load settings. The following settings were invalid: coercible_value: invalid value for Integer(): "foo" integer_value: "foo" violates constraints (type?(Integer, "foo") failed) TEXT ) end end context "Missing setting value" do subject(:system) do Class.new(Dry::System::Container) do setting :env configure do |config| config.root = SPEC_ROOT.join("fixtures").join("settings_test") config.env = :test end register_provider(:settings, from: :dry_system) do before(:prepare) do target_container.require_from_root "types" end settings do setting :missing_value_designated_optional, constructor: SettingsTest::Types::String.optional setting :missing_value_with_default, constructor: SettingsTest::Types::String, default: "blah" setting :missing_value_without_default, constructor: SettingsTest::Types::String end end end end it "raises InvalidSettingsError with meaningful message for settings where the constructor cannot accept nil" do expect { settings.to_h }.to raise_error( Dry::System::ProviderSources::Settings::InvalidSettingsError, <<~TEXT Could not load settings. The following settings were invalid: missing_value_without_default: nil violates constraints (type?(String, nil) failed) TEXT ) end end context "With default values" do subject(:system) do Class.new(Dry::System::Container) do setting :env configure do |config| config.root = SPEC_ROOT.join("fixtures").join("settings_test") config.env = :test end register_provider(:settings, from: :dry_system) do after(:prepare) do target_container.require_from_root "types" end settings do setting :number_of_workers, default: 14, constructor: SettingsTest::Types::Coercible::Integer end end end end it "uses the default value" do expect(settings.number_of_workers).to eql(14) end context "ENV variables take precedence before defaults" do before do ENV["NUMBER_OF_WORKERS"] = "4" end after do ENV.delete("NUMBER_OF_WORKERS") end it "uses the ENV value" do expect(settings.number_of_workers).to eql(4) end end end end ================================================ FILE: spec/spec_helper.rb ================================================ # frozen_string_literal: true require "bundler/setup" require "pathname" require "warning" begin require "byebug" require "pry-byebug" rescue LoadError; end SPEC_ROOT = Pathname(__dir__) Dir[SPEC_ROOT.join("support", "**", "*.rb")].each { |f| require f } Dir[SPEC_ROOT.join("shared", "**", "*.rb")].each { |f| require f } require "dry/system" require "dry/system/stubs" require "dry/events" require "dry/types" # For specs that rely on `settings` DSL module Types include Dry::Types() end RSpec.configure do |config| config.after do Dry::System.provider_sources.sources.delete_if { |k, _| k[:group] != :dry_system } end end ================================================ FILE: spec/support/coverage.rb ================================================ # frozen_string_literal: true # This file is synced from hanakai-rb/repo-sync if ENV["COVERAGE"] == "true" require "simplecov" require "simplecov-cobertura" SimpleCov.formatter = SimpleCov::Formatter::CoberturaFormatter SimpleCov.start do add_filter "/spec/" enable_coverage :branch end end ================================================ FILE: spec/support/loaded_constants_cleaning.rb ================================================ # frozen_string_literal: true require "tmpdir" module TestCleanableNamespace def remove_constants constants.each do |name| remove_const(name) end end end RSpec.shared_context "Loaded constants cleaning" do let(:cleanable_modules) { %i[Test] } let(:cleanable_constants) { [] } end RSpec.configure do |config| config.include_context "Loaded constants cleaning" config.before do @load_path = $LOAD_PATH.dup @loaded_features = $LOADED_FEATURES.dup cleanable_modules.each do |mod| if Object.const_defined?(mod) Object.const_get(mod).extend(TestCleanableNamespace) else Object.const_set(mod, Module.new { |m| m.extend(TestCleanableNamespace) }) end end end config.after do $LOAD_PATH.replace(@load_path) # We want to delete only newly loaded features within spec/, otherwise we're removing # files that may have been additionally loaded for rspec et al new_features_to_keep = ($LOADED_FEATURES - @loaded_features).tap do |feats| feats.delete_if { |path| path.include?(SPEC_ROOT.to_s) || path.include?(Dir.tmpdir) } end $LOADED_FEATURES.replace(@loaded_features + new_features_to_keep) cleanable_modules.each do |mod| next unless Object.const_defined?(mod) Object.const_get(mod).remove_constants Object.send :remove_const, mod end cleanable_constants.each do |const| Object.send :remove_const, const if Object.const_defined?(const) end end end ================================================ FILE: spec/support/rspec.rb ================================================ # frozen_string_literal: true # This file is synced from hanakai-rb/repo-sync RSpec.configure do |config| # When no filter given, search and run focused tests config.filter_run_when_matching :focus # Disables rspec monkey patches (no reason for their existence tbh) config.disable_monkey_patching! # Run ruby in verbose mode config.warnings = true # Collect all failing expectations automatically, # without calling aggregate_failures everywhere config.define_derived_metadata do |meta| meta[:aggregate_failures] = true end if ENV["CI"] # No focused specs should be committed. This ensures # builds fail when this happens. config.before(:each, :focus) do raise StandardError, "You've committed a focused spec!" end end end ================================================ FILE: spec/support/tmp_directory.rb ================================================ # frozen_string_literal: true require "fileutils" require "pathname" require "tmpdir" module RSpec module Support module TmpDirectory private def with_tmp_directory(&) with_directory(make_tmp_directory, &) end def with_directory(dir, &) Dir.chdir(dir, &) end def make_tmp_directory Pathname(::Dir.mktmpdir).tap do |dir| (@made_tmp_dirs ||= []) << dir end end def write(path, *content) ::Pathname.new(path).dirname.mkpath File.open(path, ::File::CREAT | ::File::WRONLY) do |file| file.write(Array(content).flatten.join) end end end end end RSpec.configure do |config| config.include RSpec::Support::TmpDirectory config.after :all do if instance_variable_defined?(:@made_tmp_dirs) Array(@made_tmp_dirs).each do |dir| FileUtils.remove_entry dir end end end end ================================================ FILE: spec/support/warnings.rb ================================================ # frozen_string_literal: true # This file is synced from hanakai-rb/repo-sync require "warning" # Ignore warnings for experimental features Warning[:experimental] = false if Warning.respond_to?(:[]) # Ignore all warnings coming from gem dependencies Gem.path.each do |path| Warning.ignore(//, path) end ================================================ FILE: spec/support/zeitwerk_loader_registry.rb ================================================ # frozen_string_literal: true module ZeitwerkLoaderRegistry class << self def new_loader Zeitwerk::Loader.new.tap do |loader| loaders << loader end end def clear loaders.each do |loader| loader.unregister end end private def loaders @loaders ||= [] end end end ================================================ FILE: spec/unit/auto_registrar_spec.rb ================================================ # frozen_string_literal: true require "dry/system/auto_registrar" require "dry/system/errors" RSpec.describe Dry::System::AutoRegistrar, "#finalize!" do let(:auto_registrar) { described_class.new(container) } let(:container) { Class.new(Dry::System::Container) { configure do |config| config.root = SPEC_ROOT.join("fixtures").realpath config.component_dirs.add "components" do |dir| dir.namespaces.add "test", key: nil end end } } it "registers components in the configured component dirs" do auto_registrar.finalize! expect(container["foo"]).to be_an_instance_of(Test::Foo) expect(container["bar"]).to be_an_instance_of(Test::Bar) expect(container["bar.baz"]).to be_an_instance_of(Test::Bar::Baz) expect { container["bar.abc"] }.to raise_error( Dry::System::ComponentNotLoadableError ).with_message( <<~ERROR_MESSAGE Component 'bar.abc' is not loadable. Looking for Test::Bar::Abc. You likely need to add: acronym('ABC') to your container's inflector, since we found a Test::Bar::ABC class. ERROR_MESSAGE ) end it "doesn't re-register components previously registered via lazy loading" do expect(container["foo"]).to be_an_instance_of(Test::Foo) expect { auto_registrar.finalize! }.not_to raise_error expect(container["bar"]).to be_an_instance_of(Test::Bar) expect(container["bar.baz"]).to be_an_instance_of(Test::Bar::Baz) end end ================================================ FILE: spec/unit/component_dir/component_for_identifier_key.rb ================================================ # frozen_string_literal: true require "dry/system/component_dir" require "dry/system/config/component_dir" require "dry/system/container" RSpec.describe Dry::System::ComponentDir, "#component_for_key" do subject(:component) { component_dir.component_for_key(key) } let(:component_dir) { Dry::System::ComponentDir.new( config: Dry::System::Config::ComponentDir.new("component_dir_1") { |config| config.namespaces.add "namespace", key: nil component_dir_options.each do |key, val| config.send :"#{key}=", val end }, container: container ) } let(:component_dir_options) { {} } let(:container) { container_root = root Class.new(Dry::System::Container) { configure do |config| config.root = container_root end } } let(:root) { SPEC_ROOT.join("fixtures/unit/component").realpath } context "component file located" do let(:key) { "nested.component_file" } it "returns a component" do expect(component).to be_a Dry::System::Component end it "has a matching key" do expect(component.key).to eq key end it "has the component dir's namespace" do expect(component.namespace.path).to eq "namespace" end context "options given as component dir config" do let(:component_dir_options) { {memoize: true} } it "has the component dir's options" do expect(component.memoize?).to be true end end context "options given as magic comments in file" do let(:key) { "nested.component_file_with_auto_register_false" } it "loads options specified within the file's magic comments" do expect(component.options).to include(auto_register: false) end end context "options given as both component dir config and as magic comments in file" do let(:component_dir_options) { {auto_register: true} } let(:key) { "nested.component_file_with_auto_register_false" } it "prefers the options specified as magic comments" do expect(component.options).to include(auto_register: false) end end end context "component file not located" do let(:key) { "nested.missing_component" } it "returns nil" do expect(component).to be_nil end end end ================================================ FILE: spec/unit/component_dir/each_component_spec.rb ================================================ # frozen_string_literal: true require "dry/system/component_dir" require "dry/system/config/component_dir" require "dry/system/container" RSpec.describe Dry::System::ComponentDir, "#each_component" do subject(:components) { component_dir.each_component.to_a } let(:component_dir) { described_class.new( config: Dry::System::Config::ComponentDir.new(@dir.join("lib")) { |config| component_dir_config.(config) if defined?(component_dir_config) }, container: container ) } let(:container) { container_root = @dir Class.new(Dry::System::Container) { configure do |config| config.root = container_root end } } before :all do @dir = make_tmp_directory with_directory(@dir) do write "lib/test/component_file.rb" write "lib/test/component_file_with_auto_register_true.rb", <<~RUBY # auto_register: false RUBY write "lib/outside_namespace/component_file.rb" end end it "finds the components" do expect(components.length).to eq 3 end it "returns components as Dry::System::Component" do expect(components).to all be_a Dry::System::Component end it "yields the components when called with a block" do expect { |b| component_dir.each_component(&b) }.to yield_successive_args( an_object_satisfying { |c| c.is_a?(Dry::System::Component) }, an_object_satisfying { |c| c.is_a?(Dry::System::Component) }, an_object_satisfying { |c| c.is_a?(Dry::System::Component) } ) end it "prepares a matching key for each component" do expect(components.map(&:key)).to eq %w[ outside_namespace.component_file test.component_file test.component_file_with_auto_register_true ] end context "component options given as component dir config" do let(:component_dir_config) { -> config { config.memoize = true } } it "includes the options with all components" do expect(components).to all satisfy(&:memoize?) end end context "component options given as magic comments" do let(:component) { components.detect { |c| c.key == "test.component_file_with_auto_register_true" } } it "loads the options specified within the magic comments" do expect(component.options).to include(auto_register: false) end end context "component options given as both component dir config and magic comments" do let(:component_dir_config) { -> config { config.auto_register = true } } let(:component) { components.detect { |c| c.key == "test.component_file_with_auto_register_true" } } it "prefers the options given as magic comments" do expect(component.options).to include(auto_register: false) end end context "namespaces configured" do let(:component_dir_config) { -> config { config.namespaces.add "test", key: nil } } it "loads the components in the order of the configured namespaces" do expect(components.map(&:key)).to eq %w[ component_file component_file_with_auto_register_true outside_namespace.component_file ] end it "provides the namespace to each component" do expect(components[0].namespace.path).to eq "test" expect(components[1].namespace.path).to eq "test" expect(components[2].namespace.path).to be nil end end context "clashing component names in multiple namespaces" do before :all do @dir = make_tmp_directory end before :all do with_directory(@dir) do write "lib/ns1/component_file.rb" write "lib/ns2/component_file.rb" end end let(:component_dir_config) { -> config { config.namespaces.add "ns1", key: nil config.namespaces.add "ns2", key: nil } } it "returns all components, in order of configured namespaces, even with clashing keys" do expect(components.map(&:key)).to eq %w[ component_file component_file ] expect(components[0].namespace.path).to eq "ns1" expect(components[1].namespace.path).to eq "ns2" end end end ================================================ FILE: spec/unit/component_dir_spec.rb ================================================ # frozen_string_literal: true require "dry/system/component_dir" require "dry/system/container" require "dry/system/config/component_dir" RSpec.describe Dry::System::ComponentDir do subject(:component_dir) { described_class.new(config: config, container: container) } let(:config) { Dry::System::Config::ComponentDir.new(dir_path) } let(:dir_path) { "component_dir" } let(:container) { container_root = root Class.new(Dry::System::Container) { configure do |config| config.root = container_root end } } let(:root) { SPEC_ROOT.join("fixtures/unit").realpath } describe "config" do it "delegates config methods to the config" do expect(component_dir.path).to eql config.path expect(component_dir.auto_register).to eql config.auto_register expect(component_dir.add_to_load_path).to eql config.add_to_load_path end # TODO xit "provides a default root namespace if none is specified" end end ================================================ FILE: spec/unit/component_spec.rb ================================================ # frozen_string_literal: true require "dry/system/component" require "dry/system/identifier" require "dry/system/loader" require "dry/system/config/namespace" RSpec.describe Dry::System::Component do subject(:component) { described_class.new( identifier, file_path: file_path, namespace: namespace, loader: loader ) } let(:identifier) { Dry::System::Identifier.new("test.foo") } let(:file_path) { "/path/to/test/foo.rb" } let(:namespace) { Dry::System::Config::Namespace.default_root } let(:loader) { class_spy(Dry::System::Loader) } it "is loadable" do expect(component).to be_loadable end describe "#identifier" do it "is the given identifier" do expect(component.identifier).to be identifier end end describe "#key" do it "returns the identifier's key" do expect(component.key).to eq "test.foo" end end describe "#root_key" do it "returns the identifier's root_key" do expect(component.root_key).to eq :test end end describe "#instance" do it "builds and returns an instance via the loader" do loaded_instance = double(:instance) allow(loader).to receive(:call).with(component) { loaded_instance } expect(component.instance).to eql loaded_instance end it "forwards additional arguments to the loader" do loaded_instance = double(:instance) allow(loader).to receive(:call).with(component, "extra", "args") { loaded_instance } expect(component.instance("extra", "args")).to eql loaded_instance end end end ================================================ FILE: spec/unit/config/component_dirs_spec.rb ================================================ # frozen_string_literal: true require "dry/system/config/component_dirs" require "dry/system/config/component_dir" RSpec.describe Dry::System::Config::ComponentDirs do subject(:component_dirs) { described_class.new } describe "#dir" do it "returns the added dir for the given path" do dir = component_dirs.add("test/path") expect(component_dirs.dir("test/path")).to be dir end it "yields the dir" do dir = component_dirs.add("test/path") expect { |b| component_dirs.dir("test/path", &b) }.to yield_with_args dir end it "applies global default values configured before retrieval" do component_dirs.add("test/path") component_dirs.namespaces.add "global_default" expect(component_dirs.dir("test/path").namespaces.paths).to eq ["global_default"] end it "returns nil when no dir was added for the given path" do expect(component_dirs.dir("test/path")).to be nil end end describe "#[]" do it "returns the added dir for the given path" do dir = component_dirs.add("test/path") expect(component_dirs["test/path"]).to be dir end it "yields the dir" do dir = component_dirs.add("test/path") expect { |b| component_dirs["test/path", &b] }.to yield_with_args dir end it "applies global default values configured before retrieval" do component_dirs.add("test/path") component_dirs.namespaces.add "global_default" expect(component_dirs["test/path"].namespaces.paths).to eq ["global_default"] end it "returns nil when no dir was added for the given path" do expect(component_dirs["test/path"]).to be nil end end describe "#add" do it "adds a component dir by path, with config set on a yielded dir" do expect { component_dirs.add "test/path" do |dir| dir.auto_register = false dir.add_to_load_path = false end } .to change { component_dirs.length } .from(0).to(1) dir = component_dirs["test/path"] expect(dir.path).to eq "test/path" expect(dir.auto_register).to eq false expect(dir.add_to_load_path).to eq false end it "adds a pre-built component dir" do dir = Dry::System::Config::ComponentDir.new("test/path").tap do |d| d.auto_register = false d.add_to_load_path = false end expect { component_dirs.add(dir) } .to change { component_dirs.length } .from(0).to(1) expect(component_dirs["test/path"]).to be dir end it "raises an error when a component dir has already been added for the given path" do component_dirs.add "test/path" expect { component_dirs.add "test/path" }.to raise_error(Dry::System::ComponentDirAlreadyAddedError, %r{test/path}) end it "raises an error when a component dir has already been added for the given dir's path" do component_dirs.add "test/path" expect { component_dirs.add Dry::System::Config::ComponentDir.new("test/path") } .to raise_error(Dry::System::ComponentDirAlreadyAddedError, %r{test/path}) end it "applies default values configured before adding" do component_dirs.namespaces.add "global_default" component_dirs.add "test/path" dir = component_dirs["test/path"] expect(dir.namespaces.to_a.map(&:path)).to eq ["global_default", nil] end it "does not apply default values over the component dir's own config" do component_dirs.namespaces.add "global_default" component_dirs.memoize = true component_dirs.add "test/path" do |dir| dir.namespaces.add_root # force the default value dir.memoize = false end dir = component_dirs["test/path"] expect(dir.namespaces.to_a.map(&:path)).to eq [nil] expect(dir.memoize).to be false end end describe "#delete" do it "deletes and returns the component dir for the given path" do added_dir = component_dirs.add("test/path") deleted_dir = nil expect { deleted_dir = component_dirs.delete("test/path") } .to change { component_dirs.length } .from(1).to(0) expect(deleted_dir).to be added_dir end it "returns nil when no component dir has been added for the given path" do expect(component_dirs.delete("test/path")).to be nil expect(component_dirs.length).to eq 0 end end describe "#length" do it "returns the count of component dirs" do component_dirs.add "test/path_1" component_dirs.add "test/path_2" expect(component_dirs.length).to eq 2 end it "returns 0 when there are no configured component dirs" do expect(component_dirs.length).to eq 0 end end end ================================================ FILE: spec/unit/config/namespaces_spec.rb ================================================ # frozen_string_literal: true require "dry/system/config/namespaces" require "dry/system/config/namespace" RSpec.describe Dry::System::Config::Namespaces do subject(:namespaces) { described_class.new } describe "#namespace" do it "returns the previously configured namespace for the given path" do added_namespace = namespaces.add "test/path", key: "key_ns", const: "const_ns" expect(namespaces.namespace("test/path")).to be added_namespace end it "returns nil when no namepace was previously configured for the given path" do expect(namespaces.namespace("test/path")).to be nil end end describe "#[]" do it "returns the previously configured namespace for the given path" do added_namespace = namespaces.add "test/path", key: "key_ns", const: "const_ns" expect(namespaces["test/path"]).to be added_namespace end it "returns nil when no namepace was previously configured for the given path" do expect(namespaces["test/path"]).to be nil end end describe "#root" do it "returns the previously configured root namespace" do added_root_namespace = namespaces.add_root expect(namespaces.root).to be added_root_namespace end it "returns nil when no root namespace was previously configured" do expect(namespaces.root).to be nil end end describe "#add" do it "adds the namespace with the given configuration" do expect { namespaces.add "test/path", key: "key_ns", const: "const_ns" } .to change { namespaces.length } .from(0).to(1) ns = namespaces.namespaces["test/path"] expect(ns.path).to eq "test/path" expect(ns.key).to eq "key_ns" expect(ns.const).to eq "const_ns" end it "raises an exception when a namespace is already added" do namespaces.add "test/path" expect { namespaces.add "test/path" }.to raise_error(Dry::System::NamespaceAlreadyAddedError, %r{test/path}) end end describe "#add_root" do it "adds a root namespace with the given configuration" do expect { namespaces.add_root key: "key_ns", const: "const_ns" } .to change { namespaces.length } .from(0).to(1) ns = namespaces.namespaces[nil] expect(ns).to be_root expect(ns.path).to be_nil expect(ns.key).to eq "key_ns" expect(ns.const).to eq "const_ns" end it "raises an exception when a root namespace is already added" do namespaces.add_root expect { namespaces.add_root }.to raise_error(Dry::System::NamespaceAlreadyAddedError, /root path/) end end describe "#delete" do it "deletes and returns the configured namespace for the given path" do added_namespace = namespaces.add "test/path" deleted_namespace = nil expect { deleted_namespace = namespaces.delete("test/path") } .to change { namespaces.length } .from(1).to(0) expect(deleted_namespace).to be added_namespace end it "returns nil when no namespace has been configured for the given path" do expect(namespaces.delete("test/path")).to be nil expect(namespaces).to be_empty end end describe "#delete_root" do it "deletes and returns the configured root namespace" do added_namespace = namespaces.add_root deleted_namespace = nil expect { deleted_namespace = namespaces.delete_root } .to change { namespaces.length } .from(1).to(0) expect(deleted_namespace).to be added_namespace end it "returns nil when no root namespace has been configured" do expect(namespaces.delete_root).to be nil expect(namespaces).to be_empty end end describe "#length" do it "returns the count of configured namespaces" do namespaces.add "test/path_1" namespaces.add "test/path_2" expect(namespaces.length).to eq 2 end it "returns 0 when there are no configured namespaces" do expect(namespaces.length).to eq 0 end end describe "#empty?" do it "returns true if there are no configured namespaces" do expect(namespaces).to be_empty end it "returns false if a namespace has been added" do expect { namespaces.add "test/path" } .to change { namespaces.empty? } .from(true).to(false) end end describe "#to_a" do it "returns an array of the configured namespaces, in order of definition" do namespaces.add "test/path", key: "test_key_ns" namespaces.add_root key: "root_key_ns" arr = namespaces.to_a expect(arr.length).to eq 2 expect(arr[0].path).to eq "test/path" expect(arr[0].key).to eq "test_key_ns" expect(arr[1].path).to eq nil expect(arr[1].key).to eq "root_key_ns" end it "appends a default root namespace if not explicitly configured" do namespaces.add "test/path", key: "test_key_ns" arr = namespaces.to_a expect(arr.length).to eq 2 expect(arr[0].path).to eq "test/path" expect(arr[0].key).to eq "test_key_ns" expect(arr[1].path).to be nil expect(arr[1].key).to be nil expect(arr[1].const).to be nil end it "includes a default root namespace if no namespaces configured" do arr = namespaces.to_a expect(arr.length).to eq 1 expect(arr[0].path).to be nil expect(arr[0].key).to be nil expect(arr[0].const).to be nil end end describe "#each" do it "yields each configured namespace" do namespaces.add "test/path", key: "test_key_ns" namespaces.add_root key: "root_key_ns" expect { |b| namespaces.each(&b) }.to yield_successive_args( an_object_satisfying { |ns| ns.path == "test/path" }, an_object_satisfying(&:root?) ) end end describe "#dup" do it "dups the registry of namespaces" do namespaces.add "test/path", key: "test_key_ns" new_namespaces = namespaces.dup expect(new_namespaces.to_a).to eq(namespaces.to_a) expect(new_namespaces.namespaces).not_to be(namespaces.namespaces) end end end ================================================ FILE: spec/unit/container/auto_register_spec.rb ================================================ # frozen_string_literal: true require "dry/system/container" RSpec.describe Dry::System::Container, ".auto_register!" do context "standard loader" do before do class Test::Container < Dry::System::Container configure do |config| config.root = SPEC_ROOT.join("fixtures").realpath config.component_dirs.add "components" do |dir| dir.namespaces.add "test", key: nil end end end end it { expect(Test::Container["foo"]).to be_an_instance_of(Test::Foo) } it { expect(Test::Container["bar"]).to be_an_instance_of(Test::Bar) } it { expect(Test::Container["bar.baz"]).to be_an_instance_of(Test::Bar::Baz) } it "doesn't register files with inline option 'auto_register: false'" do expect(Test::Container.registered?("no_register")).to eql false end end context "standard loader with a default namespace configured" do before do class Test::Container < Dry::System::Container configure do |config| config.root = SPEC_ROOT.join("fixtures").realpath config.component_dirs.add "namespaced_components" do |dir| dir.namespaces.add "namespaced", key: nil end end end end specify { expect(Test::Container["bar"]).to be_a(Namespaced::Bar) } specify { expect(Test::Container["bar"].foo).to be_a(Namespaced::Foo) } specify { expect(Test::Container["foo"]).to be_a(Namespaced::Foo) } end context "standard loader with default namespace but boot files without" do before do class Test::Container < Dry::System::Container configure do |config| config.root = SPEC_ROOT.join("fixtures").realpath config.component_dirs.add "components" do |dir| dir.namespaces.add "test", key: nil end end end end specify { expect(Test::Container["foo"]).to be_an_instance_of(Test::Foo) } specify { expect(Test::Container["bar"]).to be_an_instance_of(Test::Bar) } specify { expect(Test::Container["bar.baz"]).to be_an_instance_of(Test::Bar::Baz) } end context "standard loader with a default namespace with multiple level" do before do class Test::Container < Dry::System::Container configure do |config| config.root = SPEC_ROOT.join("fixtures").realpath config.component_dirs.add "multiple_namespaced_components" do |dir| dir.namespaces.add "multiple/level", key: nil end end end end specify { expect(Test::Container["baz"]).to be_a(Multiple::Level::Baz) } specify { expect(Test::Container["foz"]).to be_a(Multiple::Level::Foz) } end context "with a custom loader" do before do class Test::Loader < Dry::System::Loader def self.call(component, *args) require!(component) constant = self.constant(component) constant.respond_to?(:call) ? constant : constant.new(*args) end end class Test::Container < Dry::System::Container configure do |config| config.root = SPEC_ROOT.join("fixtures").realpath config.component_dirs.loader = Test::Loader config.component_dirs.add "components" do |dir| dir.namespaces.add "test", key: nil end end end end it { expect(Test::Container["foo"]).to be_an_instance_of(Test::Foo) } it { expect(Test::Container["bar"]).to eq(Test::Bar) } it { expect(Test::Container["bar"].call).to eq("Welcome to my Moe's Tavern!") } it { expect(Test::Container["bar.baz"]).to be_an_instance_of(Test::Bar::Baz) } end context "when component directory is missing" do before do class Test::Container < Dry::System::Container configure do |config| config.root = SPEC_ROOT.join("fixtures").realpath config.component_dirs.add "components" do |dir| dir.namespaces.add "test", key: nil end config.component_dirs.add "unknown_dir" end end end it "warns about it" do expect { Test::Container.finalize! }.to raise_error Dry::System::ComponentDirNotFoundError, %r{fixtures/unknown_dir} end end end ================================================ FILE: spec/unit/container/boot_spec.rb ================================================ # frozen_string_literal: true RSpec.describe Dry::System::Container, ".register_provider" do subject(:system) { Test::App } let(:db) { spy(:db) } let(:client) { spy(:client) } before do Test.const_set(:DB, db) Test.const_set(:Client, client) module Test class App < Dry::System::Container configure do |config| config.root = SPEC_ROOT.join("fixtures/test") end register_provider(:db) do prepare do register(:db, Test::DB) db.establish_connection end start do db.load end stop do db.close_connection end end register_provider(:client) do prepare do register(:client, Test::Client) client.establish_connection end start do client.load end stop do client.close_connection end end end end end describe "#init" do it "calls init function" do system.providers.prepare(:db) expect(db).to have_received(:establish_connection) end end describe "#start" do it "calls start function" do system.providers.start(:db) expect(db).to have_received(:load) end xit "store booted component" do system.providers.start(:db) expect(system.providers.booted.map(&:name)).to include(:db) end end describe "#stop" do context "provider has started" do it "calls stop function" do system.providers.start(:db) system.providers.stop(:db) expect(db).to have_received(:close_connection) end it "marks the provider as stopped" do expect { system.providers.start(:db) system.providers.stop(:db) } .to change { system.providers[:db].stopped? } .from(false). to true end end context "provider has not started" do it "does not call stop function" do system.providers.stop(:db) expect(db).not_to have_received(:close_connection) end it "does not mark the provider as stopped" do expect { system.providers.stop(:db) }.not_to change { system.providers[:db].stopped? } expect(system.providers[:db]).not_to be_stopped end end end describe "#shutdown" do it "calls stop function for every component" do system.providers.start(:db) system.providers.start(:client) system.providers.shutdown expect(db).to have_received(:close_connection) expect(client).to have_received(:close_connection) end end specify "boot triggers prepare" do system.providers.prepare(:db) expect(db).to have_received(:establish_connection) expect(db).to_not have_received(:load) end specify "start triggers init + start" do system.providers.start(:db) expect(db).to have_received(:establish_connection) expect(db).to have_received(:load) end specify "start raises error on undefined method or variable" do expect { system.register_provider(:broken_1) { oops("arg") } system.providers.start(:broken_1) }.to raise_error(NoMethodError, /oops/) expect { system.register_provider(:broken_2) { oops } system.providers.start(:broken_2) }.to raise_error(NameError, /oops/) end specify "lifecycle triggers are called only once" do system.providers.start(:db) system.providers.start(:db) system.providers.prepare(:db) system.providers.prepare(:db) expect(db).to have_received(:establish_connection).exactly(1) expect(db).to have_received(:load).exactly(1) expect(system.providers[:db].statuses).to eql(%i[prepare start]) end it "raises when a duplicated identifier was used" do system.register_provider(:logger) {} expect { system.register_provider(:logger) {} }.to raise_error(Dry::System::ProviderAlreadyRegisteredError, /logger/) end it "allow setting namespace to true" do system.register_provider(:api, namespace: true) do start do register(:client, "connected") end end expect(system["api.client"]).to eql("connected") end it "raises when namespace value is not valid" do expect { system.register_provider(:api, namespace: 312) {} } .to raise_error(ArgumentError, /\+namespace:\+ must be true, string or symbol/) end end ================================================ FILE: spec/unit/container/config/root_spec.rb ================================================ # frozen_string_literal: true require "dry/system/container" RSpec.describe Dry::System::Container, ".config" do subject(:config) { Test::Container.config } let(:configuration) { proc {} } before do class Test::Container < Dry::System::Container end Test::Container.configure(&configuration) end describe "#root" do subject(:root) { config.root } context "no value" do it "defaults to pwd" do expect(root).to eq Pathname.pwd end end context "string provided" do let(:configuration) { proc { |config| config.root = "/tmp" } } it "coerces string paths to pathname" do expect(root).to eq Pathname("/tmp") end end context "pathname provided" do let(:configuration) { proc { |config| config.root = Pathname("/tmp") } } it "accepts the pathname" do expect(root).to eq Pathname("/tmp") end end end end ================================================ FILE: spec/unit/container/configuration_spec.rb ================================================ # frozen_string_literal: true require "dry/system/container" RSpec.describe Dry::System::Container, "configuration phase" do subject(:container) { Class.new(described_class) } describe "#configure" do it "configures the container" do expect { container.configure do |config| config.root = "/root" end }.to change { container.config.root }.to Pathname("/root") end it "marks the container as configured" do expect { container.configure {} } .to change { container.configured? } .from(false).to true end it "runs after configure hooks" do container.instance_eval do def hooks_trace @hooks_trace ||= [] end after :configure do hooks_trace << :after_configure end end expect { container.configure {} } .to change { container.hooks_trace } .from([]) .to [:after_configure] end it "does not run after configure hooks when called a second time" do container.instance_eval do def hooks_trace @hooks_trace ||= [] end after :configure do hooks_trace << :after_configure end end expect { container.configure(finalize_config: false) {} container.configure(finalize_config: false) {} } .to change { container.hooks_trace } .from([]) .to [:after_configure] end it "finalizes (freezes) the config" do expect { container.configure {} } .to change { container.config.frozen? } .from(false).to true expect { container.configure { |c| c.root = "/root" } } .to raise_error Dry::Configurable::FrozenConfigError end it "does not finalize the config with `finalize_config: false`" do expect { container.configure(finalize_config: false) {} } .not_to change { container.config.frozen? } expect(container.config).not_to be_frozen expect { container.configure { |c| c.root = "/root" } } .not_to raise_error end end describe "#configured!" do it "marks the container as configured" do expect { container.configured! } .to change { container.configured? } .from(false).to true end it "runs after configure hooks" do container.instance_eval do def hooks_trace @hooks_trace ||= [] end after :configure do hooks_trace << :after_configure end end expect { container.configured! } .to change { container.hooks_trace } .from([]) .to [:after_configure] end it "does not run after configure hooks when run a second time" do container.instance_eval do def hooks_trace @hooks_trace ||= [] end after :configure do hooks_trace << :after_configure end end expect { container.configured!; container.configured! } .to change { container.hooks_trace } .from([]) .to [:after_configure] end it "finalizes (freezes) the config" do expect { container.configured! } .to change { container.config.frozen? } .from(false).to true expect { container.config.root = "/root" }.to raise_error Dry::Configurable::FrozenConfigError end it "does not finalize the config with `finalize_config: false`" do expect { container.configured!(finalize_config: false) } .not_to change { container.config.frozen? } expect(container.config).not_to be_frozen expect { container.config.root = "/root" }.not_to raise_error end end describe "#finalize!" do it "marks the container as configured if not configured prior" do expect { container.finalize! } .to change { container.configured? }.from(false).to true end it "runs after configure hooks if not run prior" do container.instance_eval do def hooks_trace @hooks_trace ||= [] end after :configure do hooks_trace << :after_configure end end expect { container.finalize! } .to change { container.hooks_trace } .from([]) .to [:after_configure] end it "does not run after configure hooks when run a second time" do container.instance_eval do def hooks_trace @hooks_trace ||= [] end after :configure do hooks_trace << :after_configure end end expect { container.finalize!; container.finalize! } .to change { container.hooks_trace } .from([]) .to [:after_configure] end end end ================================================ FILE: spec/unit/container/decorate_spec.rb ================================================ # frozen_string_literal: true require "delegate" RSpec.describe Dry::System::Container do subject(:system) do Class.new(Dry::System::Container) end describe ".decorate" do it "decorates registered singleton object with provided decorator API" do system.register(:foo, "foo") system.decorate(:foo, with: SimpleDelegator) expect(system[:foo]).to be_instance_of(SimpleDelegator) end it "decorates registered object with provided decorator API" do system.register(:foo) { "foo" } system.decorate(:foo, with: SimpleDelegator) expect(system[:foo]).to be_instance_of(SimpleDelegator) expect(system[:foo].__getobj__).to eql("foo") end end end ================================================ FILE: spec/unit/container/hooks/after_hooks_spec.rb ================================================ # frozen_string_literal: true RSpec.describe Dry::System::Container do subject(:system) do Class.new(described_class) end describe "after_register hook" do it "executes after a new key is registered" do expect { |hook| system.after(:register, &hook) system.register(:foo) { "bar" } }.to yield_with_args(:foo) end it "provides the fully-qualified key" do expect { |hook| system.after(:register, &hook) system.namespace :foo do register(:bar) { "baz" } end }.to yield_with_args("foo.bar") end end describe "after_finalize hook" do it "executes after finalization" do expect { |hook| system.after(:finalize, &hook) system.finalize! }.to yield_control end it "executes before the container is frozen" do is_frozen = nil system.after(:finalize) { is_frozen = frozen? } system.finalize! expect(is_frozen).to eq false expect(system).to be_frozen end end end ================================================ FILE: spec/unit/container/hooks/load_path_hook_spec.rb ================================================ # frozen_string_literal: true RSpec.describe Dry::System::Container, "Default hooks / Load path" do let(:container) { Class.new(Dry::System::Container) { config.root = SPEC_ROOT.join("fixtures/test") } } before do @load_path_before = $LOAD_PATH end after do $LOAD_PATH.replace(@load_path_before) end context "component_dirs configured with add_to_load_path = true" do before do container.config.component_dirs.add "lib" do |dir| dir.add_to_load_path = true end end it "adds the component dirs to the load path" do expect { container.configure do end }.to change { $LOAD_PATH.include?(SPEC_ROOT.join("fixtures/test/lib").to_s) } .from(false).to(true) end end context "component_dirs configured with add_to_load_path = false" do before do container.config.component_dirs.add "lib" do |dir| dir.add_to_load_path = false end end it "does not change the load path" do expect { container.configure do end }.not_to(change { $LOAD_PATH }) end end end ================================================ FILE: spec/unit/container/hooks_spec.rb ================================================ # frozen_string_literal: true RSpec.describe Dry::System::Container do subject(:system) do Class.new(Dry::System::Container) end describe ".after" do it "registers an after hook" do system.after(:configure) do register(:test, true) end system.configure {} expect(system[:test]).to be(true) end it "inherits hooks from superclass" do system.after(:configure) do register(:test_1, true) end descendant = Class.new(system) do after(:configure) do register(:test_2, true) end end descendant.configure {} expect(descendant[:test_1]).to be(true) expect(descendant[:test_2]).to be(true) end end end ================================================ FILE: spec/unit/container/import_spec.rb ================================================ # frozen_string_literal: true require "dry/system/container" RSpec.describe Dry::System::Container, ".import" do subject(:app) { Class.new(Dry::System::Container) } let(:db) do Class.new(Dry::System::Container) do register(:users, %w[jane joe]) end end it "imports one container into another" do app.import(from: db, as: :persistence) expect(app.registered?("persistence.users")).to be(false) app.finalize! expect(app["persistence.users"]).to eql(%w[jane joe]) end context "when container has been finalized" do it "raises an error" do app.finalize! expect do app.import(from: db, as: :persistence) end.to raise_error(Dry::System::ContainerAlreadyFinalizedError) end end describe "import module" do it "loads system when it was not loaded in the imported container yet" do class Test::Other < Dry::System::Container configure do |config| config.root = SPEC_ROOT.join("fixtures/import_test").realpath config.component_dirs.add "lib" end end class Test::Container < Dry::System::Container configure do |config| config.root = SPEC_ROOT.join("fixtures/test").realpath config.component_dirs.add "lib" end import from: Test::Other, as: :other end module Test Import = Container.injector end class Test::Foo include Test::Import["other.test.bar"] end expect(Test::Foo.new.bar).to be_instance_of(Test::Bar) end end end ================================================ FILE: spec/unit/container/injector_spec.rb ================================================ # frozen_string_literal: true require "dry/system/container" RSpec.describe Dry::System::Container, ".injector" do context "default injector" do it "works correct" do Test::Foo = Class.new Test::Container = Class.new(Dry::System::Container) do register "foo", Test::Foo.new end Test::Inject = Test::Container.injector injected_class = Class.new do include Test::Inject["foo"] end obj = injected_class.new expect(obj.foo).to be_a Test::Foo another = Object.new obj = injected_class.new(foo: another) expect(obj.foo).to eq another end end context "injector_options provided" do it "builds an injector with the provided options" do Test::Foo = Class.new Test::Container = Class.new(Dry::System::Container) do register "foo", Test::Foo.new end Test::Inject = Test::Container.injector(strategies: { default: Dry::AutoInject::Strategies::Args, australian: Dry::AutoInject::Strategies::Args }) injected_class = Class.new do include Test::Inject.australian["foo"] end obj = injected_class.new expect(obj.foo).to be_a Test::Foo another = Object.new obj = injected_class.new(another) expect(obj.foo).to eq another end end end ================================================ FILE: spec/unit/container/load_path_spec.rb ================================================ # frozen_string_literal: true require "dry/system/container" RSpec.describe Dry::System::Container, "Load path handling" do let(:container) { class Test::Container < Dry::System::Container config.root = SPEC_ROOT.join("fixtures/test") config.component_dirs.add "lib" end Test::Container } before do @load_path_before = $LOAD_PATH end after do $LOAD_PATH.replace(@load_path_before) end describe ".add_to_load_path!" do it "adds the given directories, relative to the container's root, to the beginning of the $LOAD_PATH" do expect { container.add_to_load_path!("lib", "system") } .to change { $LOAD_PATH.include?(SPEC_ROOT.join("fixtures/test/lib").to_s) } .from(false).to(true) .and change { $LOAD_PATH.include?(SPEC_ROOT.join("fixtures/test/system").to_s) } .from(false).to(true) expect($LOAD_PATH[0..1]).to eq [ SPEC_ROOT.join("fixtures/test/lib").to_s, SPEC_ROOT.join("fixtures/test/system").to_s ] end end end ================================================ FILE: spec/unit/container/monitor_spec.rb ================================================ # frozen_string_literal: true RSpec.describe Dry::System::Container do subject(:system) do Class.new(Dry::System::Container) do use :monitoring end end describe ".monitor" do let(:klass) do Class.new do def self.name "Test::Class_#{__id__}" end def say(word, lang: nil, &block) block&.call word end def other; end end end let(:object) do klass.new end before do system.configure {} system.register(:object, klass.new) end it "monitors object public method calls" do captured = [] system.monitor(:object) do |event| captured << [event.id, event[:target], event[:method], event[:args], event[:kwargs]] end object = system[:object] block_result = [] block = proc { block_result << true } result = object.say("hi", lang: "en", &block) expect(block_result).to eql([true]) expect(result).to eql("hi") expect(captured).to eql([[:monitoring, :object, :say, ["hi"], {lang: "en"}]]) end it "monitors specified object method calls" do captured = [] system.monitor(:object, methods: [:say]) do |event| captured << [event.id, event[:target], event[:method], event[:args], event[:kwargs]] end object = system[:object] object.say("hi") object.other expect(captured).to eql([[:monitoring, :object, :say, ["hi"], {}]]) end end end ================================================ FILE: spec/unit/container/notifications_spec.rb ================================================ # frozen_string_literal: true RSpec.describe Dry::System::Container do subject(:system) do Class.new(Dry::System::Container) do use :notifications end end describe ".notifications" do it "returns configured notifications" do system.configure {} expect(system[:notifications]).to be_instance_of(Dry::Monitor::Notifications) end end end ================================================ FILE: spec/unit/container_spec.rb ================================================ # frozen_string_literal: true require "dry/system/container" require "dry/system/stubs" RSpec.describe Dry::System::Container do subject(:container) { Test::Container } context "with default core dir" do before do class Test::Container < Dry::System::Container configure do |config| config.root = SPEC_ROOT.join("fixtures/test").realpath config.component_dirs.add "lib" end end module Test Import = Container.injector end end describe ".require_from_root" do it "requires a single file" do container.require_from_root(Pathname("lib/test/models")) expect(Test.const_defined?(:Models)).to be(true) end it "requires many files when glob pattern is passed" do container.require_from_root(Pathname("lib/test/models/*.rb")) expect(Test::Models.const_defined?(:User)).to be(true) expect(Test::Models.const_defined?(:Book)).to be(true) end end end describe ".init" do before do class Test::Container < Dry::System::Container configure do |config| config.root = SPEC_ROOT.join("fixtures/lazytest").realpath end add_to_load_path!("lib") end end it "lazy-boot a given system" do container.prepare(:bar) expect(Test.const_defined?(:Bar)).to be(true) expect(container.registered?("test.bar")).to be(false) end end describe ".start" do shared_examples_for "a booted system" do it "boots a given system and finalizes it" do container.start(:bar) expect(Test.const_defined?(:Bar)).to be(true) expect(container["test.bar"]).to eql("I was finalized") end it "expects name to point to an existing boot file" do expect { container.start(:foo) }.to raise_error(Dry::System::ProviderNotFoundError, "Provider :foo not found") end describe "mismatch between finalize name and registered component" do it "raises a meaningful error" do expect { container.start(:hell) }.to raise_error(Dry::System::ProviderNotFoundError, "Provider :hell not found") end end end context "with the default core dir" do it_behaves_like "a booted system" do before do class Test::Container < Dry::System::Container configure do |config| config.root = SPEC_ROOT.join("fixtures/test").realpath end add_to_load_path!("lib") end end end end context "with a custom provider dir specified" do it_behaves_like "a booted system" do before do class Test::Container < Dry::System::Container configure do |config| config.root = SPEC_ROOT.join("fixtures/other").realpath config.provider_dirs = ["config/providers"] end add_to_load_path!("lib") end end end end end describe ".stub" do let(:stubbed_car) do double(:car, wheels_count: 5) end before do class Test::Container < Dry::System::Container configure do |config| config.root = SPEC_ROOT.join("fixtures/stubbing").realpath config.component_dirs.add "lib" end end end describe "with stubs disabled" do it "raises error when trying to stub frozen container" do expect { container.stub("test.car", stubbed_car) }.to raise_error(NoMethodError, /stub/) end end describe "with stubs enabled" do before do container.enable_stubs! end it "lazy-loads a component" do # This test doens't really make sense # why do we test it again afterwards? It's also nothing to do with stubbing really... expect(container[:db]).to be_instance_of(Test::DB) # byebug container.finalize! expect(container[:db]).to be_instance_of(Test::DB) end it "allows to stub components" do container.finalize! expect(container["test.car"].wheels_count).to be(4) container.stub("test.car", stubbed_car) expect(container["test.car"].wheels_count).to be(5) end end end describe ".key?" do before do class Test::FalseyContainer < Dry::System::Container register(:else) { :else } register(:false) { false } register(:nil) { nil } end class Test::Container < Dry::System::Container configure do |config| config.root = SPEC_ROOT.join("fixtures/test").realpath config.component_dirs.add "lib" end import from: Test::FalseyContainer, as: :falses end end it "tries to load component" do expect(container.key?("test.dep")).to be(true) end it "returns false for non-existing component" do expect(container.key?("test.missing")).to be(false) end it "returns true if registered value is false or nil" do expect(container.key?("falses.false")).to be(true) expect(container.key?("falses.nil")).to be(true) end end describe ".resolve" do before do class Test::Container < Dry::System::Container configure do |config| config.root = SPEC_ROOT.join("fixtures/test").realpath config.component_dirs.add "lib" end end end it "runs a fallback block when a component cannot be resolved" do expect(container.resolve("missing") { :fallback }).to be(:fallback) end end describe ".registered?" do before do class Test::Container < Dry::System::Container configure do |config| config.root = SPEC_ROOT.join("fixtures/test").realpath config.component_dirs.add "lib" end end end it "checks if a component is registered" do expect(container.registered?("test.dep")).to be(false) container.resolve("test.dep") expect(container.registered?("test.dep")).to be(true) end end end ================================================ FILE: spec/unit/errors_spec.rb ================================================ # frozen_string_literal: true require "dry/system/errors" module Dry module System RSpec.describe "Errors" do describe ComponentNotLoadableError do let(:component) { instance_double(Dry::System::Component, key: key) } let(:error) { instance_double(NameError, name: "Foo", receiver: "Test") } subject { described_class.new(component, error, corrections: corrections) } describe "without corrections" do let(:corrections) { [] } let(:key) { "test.foo" } it do expect(subject.message).to eq( "Component 'test.foo' is not loadable.\n" \ "Looking for Test::Foo." ) end end describe "with corrections" do describe "acronym" do describe "single class name correction" do let(:corrections) { ["Test::FOO"] } let(:key) { "test.foo" } it do expect(subject.message).to eq( <<~ERROR_MESSAGE Component 'test.foo' is not loadable. Looking for Test::Foo. You likely need to add: acronym('FOO') to your container's inflector, since we found a Test::FOO class. ERROR_MESSAGE ) end end describe "module and class name correction" do let(:error) { instance_double(NameError, name: "Foo", receiver: "Test::Api") } let(:corrections) { ["Test::API::FOO"] } let(:key) { "test.api.foo" } it do expect(subject.message).to eq( <<~ERROR_MESSAGE Component 'test.api.foo' is not loadable. Looking for Test::Api::Foo. You likely need to add: acronym('API', 'FOO') to your container's inflector, since we found a Test::API::FOO class. ERROR_MESSAGE ) end end end describe "typo" do let(:corrections) { ["Test::Fon", "Test::Flo"] } let(:key) { "test.foo" } it do expect(subject.message).to eq( <<~ERROR_MESSAGE.chomp Component 'test.foo' is not loadable. Looking for Test::Foo. Did you mean? Test::Fon Test::Flo ERROR_MESSAGE ) end end end end end end end ================================================ FILE: spec/unit/identifier_spec.rb ================================================ # frozen_string_literal: true require "dry/system/identifier" RSpec.describe Dry::System::Identifier do subject(:identifier) { described_class.new(key) } let(:key) { "kittens.operations.belly_rub" } describe "#key" do it "returns the identifier's key" do expect(identifier.key).to eql "kittens.operations.belly_rub" end context "non-string key given" do let(:key) { :db } it "converts to a string" do expect(identifier.key).to eq "db" end end end describe "#to_s" do it "returns the key" do expect(identifier.to_s).to eq "kittens.operations.belly_rub" end end describe "#root_key" do it "returns the base segment of the key, as a symbol" do expect(identifier.root_key).to eq :kittens end end describe "#start_with?" do it "returns true when the given string matches the base segment of the key" do expect(identifier.start_with?("kittens")).to be true end it "returns true when the given string matches multiple base segments of the key" do expect(identifier.start_with?("kittens.operations")).to be true end it "returns false if the given string is only a partial base segment of the key" do expect(identifier.start_with?("kitten")).to be false end it "returns false if the given string is not a base segment of the key" do expect(identifier.start_with?("puppies")).to be false end it "returns true when the given string matches all segments of the key" do expect(identifier.start_with?("kittens.operations.belly_rub")).to be true end it "returns true when the given string is nil" do expect(identifier.start_with?(nil)).to be true end it "returns true if the given string is empty" do expect(identifier.start_with?("")).to be true end context "component is identified by a single segment" do let(:key) { "belly_rub" } it "returns true when the given string matches the key" do expect(identifier.start_with?("belly_rub")).to be true end it "returns false when the given string does not match the key" do expect(identifier.start_with?("head_scratch")).to be false end end end describe "#end_with?" do it "returns true when the given string matches the last segment of the key" do expect(identifier.end_with?("belly_rub")).to be true end it "returns true when the given string matches multiple trailing segments of the key" do expect(identifier.end_with?("operations.belly_rub")).to be true end it "returns false if the given string is an incomplete part of a trailing segment" do expect(identifier.end_with?("rub")).to be false expect(identifier.end_with?("ations.belly_rub")).to be false end it "return false if the given string is not part of any trailing segment" do expect(identifier.end_with?("head_scratch")).to be false end it "returns true if the given string matches all segments of the key" do expect(identifier.end_with?("kittens.operations.belly_rub")).to be true end it "returns true if the given string is nil" do expect(identifier.end_with?(nil)).to be true end it "returns true if the given string is empty" do expect(identifier.end_with?("")).to be true end context "component key with only a single segment" do let(:key) { "belly_rub" } it "returns true when the given string matches the key" do expect(identifier.end_with?("belly_rub")).to be true end it "returns false when the given string does not match the key" do expect(identifier.end_with?("head_scratch")).to be false end end end describe "#include?" do it "returns true when the given string matches one or more whole key segments" do expect(identifier.include?("kittens.operations")).to be true end it "returns false when the given string is an incomplete part of a key segment" do expect(identifier.include?("kitten")).to be false expect(identifier.include?("kittens.operation")).to be false end it "returns false when the given string is not any of the key segments" do expect(identifier.include?("puppies")).to be false end it "returns false if the given string is nil" do expect(identifier.include?(nil)).to be false end it "returns false if the given string is blank" do expect(identifier.include?("")).to be false end end describe "#key_with_separator" do it "returns the key split by the given separator" do expect(identifier.key_with_separator("/")).to eq "kittens/operations/belly_rub" end end describe "#namespaced" do let(:new_identifier) { identifier.namespaced(from: from, to: to, **opts) } let(:from) { "kittens" } let(:to) { "cats" } let(:opts) { {} } it "returns a new identifier" do expect(new_identifier).to be_an_instance_of(described_class) end it "replaces the leading namespace" do expect(new_identifier.key).to eq "cats.operations.belly_rub" end context "multiple leading namespaces" do let(:from) { "kittens.operations" } it "replaces the namespaces" do expect(new_identifier.key).to eq "cats.belly_rub" end end context "removing the leading namespace" do let(:to) { nil } it "removes the namespace" do expect(new_identifier.key).to eq "operations.belly_rub" end end context "adding a leading namespace" do let(:from) { nil } it "adds the namespace" do expect(new_identifier.key).to eq "cats.kittens.operations.belly_rub" end end it "returns itself if the key is unchanged" do expect(identifier.namespaced(from: nil, to: nil)).to be identifier end end end ================================================ FILE: spec/unit/indirect_component_spec.rb ================================================ # frozen_string_literal: true require "dry/system/identifier" require "dry/system/indirect_component" RSpec.describe Dry::System::IndirectComponent do subject(:component) { described_class.new(identifier) } let(:identifier) { Dry::System::Identifier.new("test.foo") } it "is not loadable" do expect(component).not_to be_loadable end describe "#identifier" do it "is the given identifier" do expect(component.identifier).to be identifier end end describe "#key" do it "returns the identifier's key" do expect(component.key).to eq "test.foo" end end describe "#root_key" do it "returns the identifier's root key" do expect(component.root_key).to eq :test end end end ================================================ FILE: spec/unit/loader/autoloading_spec.rb ================================================ # frozen_string_literal: true require "dry/system/loader/autoloading" require "dry/system/component" require "dry/system/config/namespace" require "dry/system/identifier" RSpec.describe Dry::System::Loader::Autoloading do describe "#require!" do subject(:loader) { described_class } let(:component) { Dry::System::Component.new( Dry::System::Identifier.new("test.not_loaded_const"), file_path: "/path/to/test/not_loaded_const.rb", namespace: Dry::System::Config::Namespace.default_root ) } before do allow(loader).to receive(:require) allow(Test).to receive(:const_missing) end it "loads the constant " do loader.require!(component) expect(loader).not_to have_received(:require) expect(Test).to have_received(:const_missing).with :NotLoadedConst end it "returns self" do expect(loader.require!(component)).to eql loader end end end ================================================ FILE: spec/unit/loader_spec.rb ================================================ # frozen_string_literal: true require "dry/system/loader" require "dry/inflector" require "dry/system/component" require "dry/system/config/namespace" require "dry/system/identifier" require "singleton" RSpec.describe Dry::System::Loader do subject(:loader) { described_class } describe "#require!" do let(:component) { Dry::System::Component.new( Dry::System::Identifier.new("test.bar"), file_path: "/path/to/test/bar.rb", namespace: Dry::System::Config::Namespace.default_root ) } before do expect(loader).to receive(:require).with("test/bar").at_least(1) end it "requires the components's path" do loader.require!(component) end it "returns self" do expect(loader.require!(component)).to eql loader end end describe "#call" do shared_examples_for "object loader" do let(:instance) { loader.call(component) } context "not singleton" do it "returns a new instance of the constant" do expect(instance).to be_instance_of(constant) expect(instance).not_to be(loader.call(component)) end end context "singleton" do before { constant.send(:include, Singleton) } it "returns singleton instance" do expect(instance).to be(constant.instance) end end end context "with a singular name" do let(:component) { Dry::System::Component.new( Dry::System::Identifier.new("test.bar"), file_path: "/path/to/test/bar.rb", namespace: Dry::System::Config::Namespace.default_root ) } let(:constant) { Test::Bar } before do expect(loader).to receive(:require).with("test/bar").at_least(1) module Test Bar = Class.new end end it_behaves_like "object loader" end context "with a constructor accepting args" do let(:component) { Dry::System::Component.new( Dry::System::Identifier.new("test.bar"), file_path: "/path/to/test/bar.rb", namespace: Dry::System::Config::Namespace.default_root ) } before do expect(loader).to receive(:require).with("test/bar").at_least(1) module Test Bar = Struct.new(:one, :two) end end it "passes args to the constructor" do instance = loader.call(component, 1, 2) expect(instance.one).to be(1) expect(instance.two).to be(2) end end context "with a custom inflector" do let(:component) { Dry::System::Component.new( Dry::System::Identifier.new("test.api_bar"), file_path: "/path/to/test/api_bar.rb", namespace: Dry::System::Config::Namespace.default_root, inflector: Dry::Inflector.new { |i| i.acronym("API") } ) } let(:constant) { Test::APIBar } before do expect(loader).to receive(:require).with("test/api_bar").at_least(1) Test::APIBar = Class.new end it_behaves_like "object loader" end end describe "#constant" do let(:component) { Dry::System::Component.new( Dry::System::Identifier.new("test.api_bar"), file_path: "/path/to/test/api_bar.rb", namespace: Dry::System::Config::Namespace.default_root, inflector: Dry::Inflector.new { |i| i.acronym("API") } ) } describe "successful constant loading" do before do Test::APIBar = Class.new end it "returns the constant" do expect(loader.constant(component)).to eq(Test::APIBar) end end describe "unsuccessful constant loading" do before do Test::APIBoo = Class.new end it "raises custom error" do expect { loader.constant(component) }.to raise_error( Dry::System::ComponentNotLoadableError ).with_message( <<~ERROR_MESSAGE.chomp Component 'test.api_bar' is not loadable. Looking for Test::APIBar. Did you mean? Test::APIBoo ERROR_MESSAGE ) end end end end ================================================ FILE: spec/unit/magic_comments_parser_spec.rb ================================================ # frozen_string_literal: true require "dry/system/magic_comments_parser" RSpec.describe Dry::System::MagicCommentsParser, ".call" do let(:file_name) { SPEC_ROOT.join("fixtures/magic_comments/comments.rb") } let(:comments) { described_class.(file_name) } it "makes comment names available as symbols" do expect(comments.key?(:valid_comment)).to eql true end it "finds magic comments after other commented lines or blank lines" do expect(comments[:valid_comment]).to eq "hello" end it "does not match comments with invalid names" do expect(comments.values).not_to include "value for comment using dashes" end it "supports comment names with alpha-numeric characters and underscores (numbers not allowed for first character)" do expect(comments[:comment_123]).to eq "alpha-numeric and underscores allowed" expect(comments.keys).not_to include(:"123_will_not_match") end it "only matches comments at the start of the line" do expect(comments.key?(:not_at_start_of_line)).to eql false end it "does not match any comments after any lines of code" do expect(comments.key?(:after_code)).to eql false end describe "coercions" do it 'coerces "true" to true' do expect(comments[:true_comment]).to eq true end it 'coerces "false" to false' do expect(comments[:false_comment]).to eq false end end end ================================================ FILE: spec/unit/provider/source_spec.rb ================================================ # frozen_string_literal: true RSpec.describe Dry::System::Provider::Source do let(:target_container) do Dry::Core::Container.new end let(:provider_container) do Dry::Core::Container.new end shared_examples_for "a provider class" do let(:provider) do provider_class.new( provider_container: provider_container, target_container: target_container ) end it "exposes start callback" do expect(provider.provider_container.key?("persistence")).to be(false) provider.start expect(provider.provider_container.key?("persistence")).to be(true) end end context "using a base class" do it_behaves_like "a provider class" do let(:provider_class) do described_class.for(name: "Persistence") do start do register(:persistence, {}) end end end end end context "using a sub-class" do it_behaves_like "a provider class" do let(:parent_class) do described_class.for(name: "Persistence") do start do register(:persistence, {}) end end end let(:provider_class) do Class.new(parent_class) end end end end ================================================ FILE: spec/unit/provider_sources/settings/loader_spec.rb ================================================ # frozen_string_literal: true require "dry/system/provider_sources/settings/loader" RSpec.describe Dry::System::ProviderSources::Settings::Loader do subject(:loader) { described_class.new(root: root, env: env) } let(:root) { "/system/root" } subject(:env) { :development } before do allow_any_instance_of(described_class).to receive(:require).with("dotenv") end describe "#initialize" do context "dotenv available" do let(:dotenv) { spy(:dotenv) } before do stub_const "Dotenv", dotenv end context "non-test environment" do let(:env) { :development } it "requires dotenv and loads a range of .env files" do loader expect(loader).to have_received(:require).with("dotenv").ordered expect(dotenv).to have_received(:load).ordered.with( "/system/root/.env.development.local", "/system/root/.env.local", "/system/root/.env.development", "/system/root/.env" ) end end context "test environment" do let(:env) { :test } it "loads a range of .env files, not including .env.local" do loader expect(dotenv).to have_received(:load).ordered.with( "/system/root/.env.test.local", "/system/root/.env.test", "/system/root/.env" ) end end end context "dotenv unavailable" do it "attempts to require dotenv" do loader expect(loader).to have_received(:require).with("dotenv") end it "does not raise any error" do expect { loader }.not_to raise_error end end end describe "#[]" do it "returns a values from ENV" do allow(ENV).to receive(:[]).and_call_original allow(ENV).to receive(:[]).with("SOME_KEY").and_return "some value" expect(loader["SOME_KEY"]).to eq "some value" end end end ================================================ FILE: zizmor.yml ================================================ rules: unpinned-uses: config: policies: hanakai-rb/*: ref-pin