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 [][rubygem] [][actions]
[][forum]
[][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