├── .devtools └── templates │ ├── changelog.erb │ └── release.erb ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug-report.md │ └── config.yml ├── SUPPORT.md └── workflows │ ├── ci.yml │ ├── docsite.yml │ ├── rubocop.yml │ └── sync_configs.yml ├── .gitignore ├── .inch.yml ├── .repobot.yml ├── .rspec ├── .rubocop.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Gemfile ├── Gemfile.devtools ├── LICENSE ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── .gitkeep ├── console └── setup ├── changelog.yml ├── docsite └── source │ ├── cache.html.md │ ├── classes.html.md │ ├── classes │ ├── class-attributes.html.md │ └── class-builder.html.md │ ├── constants.html.md │ ├── deprecations.html.md │ ├── equalizer.html.md │ ├── extensions.html.md │ └── index.html.md ├── dry-core.gemspec ├── lib ├── dry-core.rb └── dry │ ├── core.rb │ └── core │ ├── basic_object.rb │ ├── cache.rb │ ├── class_attributes.rb │ ├── class_builder.rb │ ├── constants.rb │ ├── container.rb │ ├── container │ ├── config.rb │ ├── configuration.rb │ ├── item.rb │ ├── item │ │ ├── callable.rb │ │ ├── factory.rb │ │ └── memoizable.rb │ ├── mixin.rb │ ├── namespace.rb │ ├── namespace_dsl.rb │ ├── registry.rb │ ├── resolver.rb │ └── stub.rb │ ├── deprecations.rb │ ├── descendants_tracker.rb │ ├── equalizer.rb │ ├── errors.rb │ ├── extensions.rb │ ├── inflector.rb │ ├── memoizable.rb │ └── version.rb ├── project.yml └── spec ├── dry ├── container_spec.rb ├── core │ ├── basic_object_spec.rb │ ├── cache_spec.rb │ ├── class_attributes_spec.rb │ ├── class_builder_spec.rb │ ├── constants_spec.rb │ ├── container │ │ └── mixin_spec.rb │ ├── deprecations_spec.rb │ ├── descendants_tracker_spec.rb │ ├── equalizer │ │ ├── included_spec.rb │ │ ├── legacy_name_spec.rb │ │ ├── methods │ │ │ ├── eql_predicate_spec.rb │ │ │ └── equality_operator_spec.rb │ │ └── universal_spec.rb │ ├── extensions_spec.rb │ ├── inflector_spec.rb │ └── memoizable_spec.rb └── core_spec.rb ├── fixtures └── project.rb ├── spec_helper.rb └── support ├── coverage.rb ├── memoized.rb ├── rspec_options.rb ├── shared_examples ├── container.rb └── memoizable.rb └── warnings.rb /.devtools/templates/changelog.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <% releases.each_with_index do |r, idx| %> 5 | ## <%= r.version %> <%= r.date %> 6 | 7 | <% if r.summary %> 8 | <%= r.summary %> 9 | 10 | <% end %> 11 | 12 | <% if r.added? %> 13 | ### Added 14 | 15 | <% r.added.each do |log| %> 16 | - <%= log %> 17 | <% end %> 18 | 19 | <% end %> 20 | <% if r.fixed? %> 21 | ### Fixed 22 | 23 | <% r.fixed.each do |log| %> 24 | - <%= log %> 25 | <% end %> 26 | 27 | <% end %> 28 | <% if r.changed? %> 29 | ### Changed 30 | 31 | <% r.changed.each do |log| %> 32 | - <%= log %> 33 | <% end %> 34 | <% end %> 35 | <% curr_ver = r.date ? "v#{r.version}" : 'master' %> 36 | <% prev_rel = releases[idx + 1] %> 37 | <% if prev_rel %> 38 | <% ver_range = "v#{prev_rel.version}...#{curr_ver}" %> 39 | 40 | [Compare <%=ver_range%>](https://github.com/dry-rb/<%= project.name %>/compare/<%=ver_range%>) 41 | <% end %> 42 | 43 | <% end %> 44 | -------------------------------------------------------------------------------- /.devtools/templates/release.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <% if latest_release.summary %> 5 | <%= latest_release.summary %> 6 | 7 | <% end %> 8 | 9 | <% if latest_release.added? %> 10 | ### Added 11 | 12 | <% latest_release.added.each do |log| %> 13 | - <%= log %> 14 | <% end %> 15 | 16 | <% end %> 17 | <% if latest_release.fixed? %> 18 | ### Fixed 19 | 20 | <% latest_release.fixed.each do |log| %> 21 | - <%= log %> 22 | <% end %> 23 | 24 | <% end %> 25 | <% if latest_release.changed? %> 26 | ### Changed 27 | 28 | <% latest_release.changed.each do |log| %> 29 | - <%= log %> 30 | <% end %> 31 | <% end %> 32 | <% if previous_release %> 33 | <% ver_range = "v#{previous_release.version}...v#{latest_release.version}" %> 34 | 35 | [Compare <%=ver_range%>](https://github.com/dry-rb/<%= project.name %>/compare/<%=ver_range%>) 36 | <% end %> 37 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: hanami 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41B Bug report" 3 | about: See CONTRIBUTING.md for more information 4 | title: '' 5 | labels: bug, help wanted 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Describe the bug 11 | 12 | A clear and concise description of what the bug is. 13 | 14 | ## To Reproduce 15 | 16 | Provide detailed steps to reproduce, **an executable script would be best**. 17 | 18 | ## Expected behavior 19 | 20 | A clear and concise description of what you expected to happen. 21 | 22 | ## My environment 23 | 24 | - Affects my production application: **YES/NO** 25 | - Ruby version: ... 26 | - OS: ... 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Community Support 4 | url: https://discourse.dry-rb.org 5 | about: Please ask and answer questions here. 6 | -------------------------------------------------------------------------------- /.github/SUPPORT.md: -------------------------------------------------------------------------------- 1 | ## Support 2 | 3 | If you need help with any of the dry-rb libraries, feel free to ask questions on our [discussion forum](https://discourse.dry-rb.org/). This is the best place to seek help. Make sure to search for a potential solution in past threads before posting your question. Thanks! :heart: 4 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This file is synced from dry-rb/template-gem repo 2 | name: CI 3 | 4 | on: 5 | push: 6 | paths: 7 | - ".github/workflows/ci.yml" 8 | - "lib/**" 9 | - "*.gemspec" 10 | - "spec/**" 11 | - "Rakefile" 12 | - "Gemfile" 13 | - "Gemfile.devtools" 14 | - ".rubocop.yml" 15 | - "project.yml" 16 | pull_request: 17 | branches: 18 | - main 19 | create: 20 | 21 | jobs: 22 | tests: 23 | runs-on: ubuntu-latest 24 | name: Tests 25 | strategy: 26 | fail-fast: false 27 | matrix: 28 | ruby: 29 | - "3.4" 30 | - "3.3" 31 | - "3.2" 32 | - "3.1" 33 | include: 34 | - ruby: "3.4" 35 | coverage: "true" 36 | env: 37 | COVERAGE: ${{matrix.coverage}} 38 | COVERAGE_TOKEN: ${{secrets.CODACY_PROJECT_TOKEN}} 39 | steps: 40 | - name: Checkout 41 | uses: actions/checkout@v3 42 | - name: Install package dependencies 43 | run: "[ -e $APT_DEPS ] || sudo apt-get install -y --no-install-recommends $APT_DEPS" 44 | - name: Set up Ruby 45 | uses: ruby/setup-ruby@v1 46 | with: 47 | ruby-version: ${{matrix.ruby}} 48 | bundler-cache: true 49 | - name: Run all tests 50 | run: bundle exec rake 51 | release: 52 | runs-on: ubuntu-latest 53 | if: contains(github.ref, 'tags') && github.event_name == 'create' 54 | needs: tests 55 | env: 56 | GITHUB_LOGIN: dry-bot 57 | GITHUB_TOKEN: ${{secrets.GH_PAT}} 58 | steps: 59 | - uses: actions/checkout@v3 60 | - name: Install package dependencies 61 | run: "[ -e $APT_DEPS ] || sudo apt-get install -y --no-install-recommends $APT_DEPS" 62 | - name: Set up Ruby 63 | uses: ruby/setup-ruby@v1 64 | with: 65 | ruby-version: 3.1 66 | - name: Install dependencies 67 | run: gem install ossy --no-document 68 | - name: Trigger release workflow 69 | run: | 70 | tag=$(echo $GITHUB_REF | cut -d / -f 3) 71 | ossy gh w dry-rb/devtools release --payload "{\"tag\":\"$tag\",\"sha\":\"${{github.sha}}\",\"tag_creator\":\"$GITHUB_ACTOR\",\"repo\":\"$GITHUB_REPOSITORY\",\"repo_name\":\"${{github.event.repository.name}}\"}" 72 | -------------------------------------------------------------------------------- /.github/workflows/docsite.yml: -------------------------------------------------------------------------------- 1 | # This file is synced from dry-rb/template-gem repo 2 | 3 | name: docsite 4 | 5 | on: 6 | push: 7 | paths: 8 | - docsite/** 9 | - .github/workflows/docsite.yml 10 | branches: 11 | - main 12 | - release-** 13 | tags: 14 | 15 | jobs: 16 | update-docs: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v3 20 | with: 21 | fetch-depth: 0 22 | - run: | 23 | git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/* 24 | - name: Set up Ruby 25 | uses: ruby/setup-ruby@v1 26 | with: 27 | ruby-version: "3.0.5" 28 | - name: Set up git user 29 | run: | 30 | git config --local user.email "dry-bot@dry-rb.org" 31 | git config --local user.name "dry-bot" 32 | - name: Install dependencies 33 | run: gem install ossy --no-document 34 | - name: Update release branches 35 | run: | 36 | branches=`git log --format=%B -n 1 $GITHUB_SHA | grep "docsite:release-" || echo "nothing"` 37 | 38 | if [[ ! $branches -eq "nothing" ]]; then 39 | for b in $branches 40 | do 41 | name=`echo $b | ruby -e 'puts gets[/:(.+)/, 1].gsub(/\s+/, "")'` 42 | 43 | echo "merging $GITHUB_SHA to $name" 44 | 45 | git checkout -b $name --track origin/$name 46 | 47 | echo `git log -n 1` 48 | 49 | git cherry-pick $GITHUB_SHA -m 1 50 | done 51 | 52 | git push --all "https://dry-bot:${{secrets.GH_PAT}}@github.com/$GITHUB_REPOSITORY.git" 53 | 54 | git checkout main 55 | else 56 | echo "no need to update branches" 57 | fi 58 | - name: Trigger dry-rb.org deploy 59 | env: 60 | GITHUB_LOGIN: dry-bot 61 | GITHUB_TOKEN: ${{secrets.GH_PAT}} 62 | run: ossy github workflow dry-rb/dry-rb.org ci 63 | -------------------------------------------------------------------------------- /.github/workflows/rubocop.yml: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file is synced from dry-rb/template-gem repo 4 | 5 | name: RuboCop 6 | 7 | on: [push, pull_request] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | env: 16 | BUNDLE_ONLY: tools 17 | 18 | steps: 19 | - uses: actions/checkout@v3 20 | 21 | - name: Set up Ruby 3.2 22 | uses: ruby/setup-ruby@v1 23 | with: 24 | ruby-version: 3.2 25 | bundler-cache: true 26 | 27 | - name: Run RuboCop 28 | run: bundle exec rubocop --parallel 29 | -------------------------------------------------------------------------------- /.github/workflows/sync_configs.yml: -------------------------------------------------------------------------------- 1 | # This file is synced from dry-rb/template-gem repo 2 | 3 | name: sync 4 | 5 | on: 6 | repository_dispatch: 7 | push: 8 | branches: 9 | - "main" 10 | 11 | jobs: 12 | main: 13 | runs-on: ubuntu-latest 14 | if: (github.event_name == 'repository_dispatch' && github.event.action == 'sync_configs') || github.event_name != 'repository_dispatch' 15 | env: 16 | GITHUB_LOGIN: dry-bot 17 | GITHUB_TOKEN: ${{ secrets.GH_PAT }} 18 | steps: 19 | - name: Checkout ${{github.repository}} 20 | uses: actions/checkout@v3 21 | - name: Checkout devtools 22 | uses: actions/checkout@v3 23 | with: 24 | repository: dry-rb/devtools 25 | path: tmp/devtools 26 | - name: Setup git user 27 | run: | 28 | git config --local user.email "dry-bot@dry-rb.org" 29 | git config --local user.name "dry-bot" 30 | - name: Set up Ruby 31 | uses: ruby/setup-ruby@v1 32 | with: 33 | ruby-version: "3.1" 34 | - name: Install dependencies 35 | run: gem install ossy --no-document 36 | - name: Update changelog.yml from commit 37 | run: tmp/devtools/bin/update-changelog-from-commit $GITHUB_SHA 38 | - name: Compile CHANGELOG.md 39 | run: tmp/devtools/bin/compile-changelog 40 | - name: Commit 41 | run: | 42 | git add -A 43 | git commit -m "[devtools] sync" || echo "nothing to commit" 44 | - name: Push changes 45 | run: | 46 | git pull --rebase origin main 47 | git push https://dry-bot:${{secrets.GH_PAT}}@github.com/${{github.repository}}.git HEAD:main 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | spec/examples.txt 11 | 12 | -------------------------------------------------------------------------------- /.inch.yml: -------------------------------------------------------------------------------- 1 | files: 2 | excluded: 3 | - lib/dry/core.rb 4 | - lib/dry/core/version.rb 5 | -------------------------------------------------------------------------------- /.repobot.yml: -------------------------------------------------------------------------------- 1 | ########################################################### 2 | # DO NOT EDIT THIS FILE 3 | # 4 | # This is a config synced from dry-rb/template-gem repo 5 | ########################################################### 6 | 7 | sources: 8 | - repo: dry-rb/template-gem 9 | sync: 10 | - ".repobot.yml.erb" 11 | - ".devtools/templates/*.sync:${{dir}}/${{name}}" 12 | - ".github/**/*.*" 13 | - ".rspec" 14 | - ".rubocop.yml" 15 | - "gemspec.erb:dry-core.gemspec" 16 | - "spec/support/*" 17 | - "CODE_OF_CONDUCT.md" 18 | - "CONTRIBUTING.md" 19 | - "LICENSE.erb" 20 | - "README.md.erb" 21 | - "Gemfile.devtools" 22 | - repo: repobot-app/workflows 23 | sync: 24 | - ".github/workflows/*.yml" 25 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | --order random 4 | --warnings 5 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | # This is a config synced from dry-rb/template-gem repo 2 | 3 | AllCops: 4 | TargetRubyVersion: 3.1 5 | NewCops: enable 6 | SuggestExtensions: false 7 | Exclude: 8 | - "**/vendor/**/*" # For GitHub Actions, see rubocop/rubocop#9832 9 | - benchmarks/*.rb 10 | - spec/support/coverage.rb 11 | - spec/support/warnings.rb 12 | - spec/support/rspec_options.rb 13 | - Gemfile.devtools 14 | - "*.gemspec" 15 | 16 | Layout/SpaceAroundMethodCallOperator: 17 | Enabled: false 18 | 19 | Layout/SpaceInLambdaLiteral: 20 | Enabled: false 21 | 22 | Layout/MultilineMethodCallIndentation: 23 | Enabled: true 24 | EnforcedStyle: indented 25 | 26 | Layout/FirstArrayElementIndentation: 27 | EnforcedStyle: consistent 28 | 29 | Layout/SpaceInsideHashLiteralBraces: 30 | Enabled: true 31 | EnforcedStyle: no_space 32 | EnforcedStyleForEmptyBraces: no_space 33 | 34 | Layout/LineLength: 35 | Max: 100 36 | Exclude: 37 | - "spec/**/*.rb" 38 | 39 | Lint/AmbiguousBlockAssociation: 40 | Enabled: true 41 | # because 'expect { foo }.to change { bar }' is fine 42 | Exclude: 43 | - "spec/**/*.rb" 44 | 45 | Lint/BooleanSymbol: 46 | Enabled: false 47 | 48 | Lint/ConstantDefinitionInBlock: 49 | Exclude: 50 | - "spec/**/*.rb" 51 | 52 | Lint/RaiseException: 53 | Enabled: false 54 | 55 | Lint/StructNewOverride: 56 | Enabled: false 57 | 58 | Lint/SuppressedException: 59 | Exclude: 60 | - "spec/spec_helper.rb" 61 | 62 | Lint/LiteralAsCondition: 63 | Exclude: 64 | - "spec/**/*.rb" 65 | 66 | Naming/PredicateName: 67 | Enabled: false 68 | 69 | Naming/FileName: 70 | Exclude: 71 | - "lib/*-*.rb" 72 | 73 | Naming/MethodName: 74 | Enabled: false 75 | 76 | Naming/MethodParameterName: 77 | Enabled: false 78 | 79 | Naming/MemoizedInstanceVariableName: 80 | Enabled: false 81 | 82 | Metrics/MethodLength: 83 | Enabled: false 84 | 85 | Metrics/ClassLength: 86 | Enabled: false 87 | 88 | Metrics/BlockLength: 89 | Enabled: false 90 | 91 | Metrics/AbcSize: 92 | Max: 25 93 | 94 | Metrics/CyclomaticComplexity: 95 | Enabled: true 96 | Max: 12 97 | 98 | Style/ExponentialNotation: 99 | Enabled: false 100 | 101 | Style/HashEachMethods: 102 | Enabled: false 103 | 104 | Style/HashTransformKeys: 105 | Enabled: false 106 | 107 | Style/HashTransformValues: 108 | Enabled: false 109 | 110 | Style/AccessModifierDeclarations: 111 | Enabled: false 112 | 113 | Style/Alias: 114 | Enabled: true 115 | EnforcedStyle: prefer_alias_method 116 | 117 | Style/AsciiComments: 118 | Enabled: false 119 | 120 | Style/BlockDelimiters: 121 | Enabled: false 122 | 123 | Style/ClassAndModuleChildren: 124 | Exclude: 125 | - "spec/**/*.rb" 126 | 127 | Style/ConditionalAssignment: 128 | Enabled: false 129 | 130 | Style/DateTime: 131 | Enabled: false 132 | 133 | Style/Documentation: 134 | Enabled: false 135 | 136 | Style/EachWithObject: 137 | Enabled: false 138 | 139 | Style/FormatString: 140 | Enabled: false 141 | 142 | Style/FormatStringToken: 143 | Enabled: false 144 | 145 | Style/GuardClause: 146 | Enabled: false 147 | 148 | Style/IfUnlessModifier: 149 | Enabled: false 150 | 151 | Style/Lambda: 152 | Enabled: false 153 | 154 | Style/LambdaCall: 155 | Enabled: false 156 | 157 | Style/ParallelAssignment: 158 | Enabled: false 159 | 160 | Style/RaiseArgs: 161 | Enabled: false 162 | 163 | Style/StabbyLambdaParentheses: 164 | Enabled: false 165 | 166 | Style/StringLiterals: 167 | Enabled: true 168 | EnforcedStyle: double_quotes 169 | ConsistentQuotesInMultiline: false 170 | 171 | Style/StringLiteralsInInterpolation: 172 | Enabled: true 173 | EnforcedStyle: double_quotes 174 | 175 | Style/SymbolArray: 176 | Exclude: 177 | - "spec/**/*.rb" 178 | 179 | Style/TrailingUnderscoreVariable: 180 | Enabled: false 181 | 182 | Style/MultipleComparison: 183 | Enabled: false 184 | 185 | Style/Next: 186 | Enabled: false 187 | 188 | Style/AccessorGrouping: 189 | Enabled: false 190 | 191 | Style/EmptyLiteral: 192 | Enabled: false 193 | 194 | Style/Semicolon: 195 | Exclude: 196 | - "spec/**/*.rb" 197 | 198 | Style/HashAsLastArrayItem: 199 | Exclude: 200 | - "spec/**/*.rb" 201 | 202 | Style/CaseEquality: 203 | Exclude: 204 | - "lib/dry/monads/**/*.rb" 205 | - "lib/dry/struct/**/*.rb" 206 | - "lib/dry/types/**/*.rb" 207 | - "spec/**/*.rb" 208 | 209 | Style/ExplicitBlockArgument: 210 | Exclude: 211 | - "lib/dry/types/**/*.rb" 212 | 213 | Style/CombinableLoops: 214 | Enabled: false 215 | 216 | Style/EmptyElse: 217 | Enabled: false 218 | 219 | Style/DoubleNegation: 220 | Enabled: false 221 | 222 | Style/MultilineBlockChain: 223 | Enabled: false 224 | 225 | Style/NumberedParametersLimit: 226 | Max: 2 227 | 228 | Lint/UnusedBlockArgument: 229 | Exclude: 230 | - "spec/**/*.rb" 231 | 232 | Lint/Debugger: 233 | Exclude: 234 | - "bin/console" 235 | 236 | Lint/BinaryOperatorWithIdenticalOperands: 237 | Exclude: 238 | - "spec/**/*.rb" 239 | 240 | Metrics/ParameterLists: 241 | Exclude: 242 | - "spec/**/*.rb" 243 | 244 | Lint/EmptyBlock: 245 | Exclude: 246 | - "spec/**/*.rb" 247 | 248 | Lint/EmptyFile: 249 | Exclude: 250 | - "spec/**/*.rb" 251 | 252 | Lint/UselessMethodDefinition: 253 | Exclude: 254 | - "spec/**/*.rb" 255 | 256 | Lint/SelfAssignment: 257 | Enabled: false 258 | 259 | Lint/EmptyClass: 260 | Enabled: false 261 | 262 | Naming/ConstantName: 263 | Exclude: 264 | - "spec/**/*.rb" 265 | 266 | Naming/VariableNumber: 267 | Exclude: 268 | - "spec/**/*.rb" 269 | 270 | Naming/BinaryOperatorParameterName: 271 | Enabled: false 272 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## 1.1.0 2025-01-04 4 | 5 | 6 | ### Changed 7 | 8 | - Minimal Ruby version is 3.1 (@flash-gordon) 9 | - Fixed clash with `dry-logger` (see #80) (@flash-gordon) 10 | 11 | [Compare v1.0.1...v1.1.0](https://github.com/dry-rb/dry-core/compare/v1.0.1...v1.1.0) 12 | 13 | ## 1.0.1 2023-08-06 14 | 15 | 16 | ### Fixed 17 | 18 | - [equalizer] Add `Dry::Core.Equalizer` method to make `include Dry::Core.Equalizer(...)` work as documented (via #79) (@timriley) 19 | 20 | Users of Equalizer should now only need to `require "dry/core"` first. 21 | 22 | 23 | ### Changed 24 | 25 | - Minimal Ruby version is 3.0 (@flash-gordon) 26 | 27 | [Compare v1.0.0...v1.0.1](https://github.com/dry-rb/dry-core/compare/v1.0.0...v1.0.1) 28 | 29 | ## 1.0.0 2022-11-04 30 | 31 | 32 | ### Added 33 | 34 | - Import dry-container as `Dry::Core::Container` (via #77) (@solnic) 35 | 36 | 37 | [Compare v0.9.1...v1.0.0](https://github.com/dry-rb/dry-core/compare/v0.9.1...v1.0.0) 38 | 39 | ## 0.9.1 2022-10-18 40 | 41 | 42 | ### Changed 43 | 44 | - Correct missing constant for IDENTITY (issue #75 fixed via #76) (@poloka) 45 | 46 | [Compare v0.9.0...v0.9.1](https://github.com/dry-rb/dry-core/compare/v0.9.0...v0.9.1) 47 | 48 | ## 0.9.0 2022-10-15 49 | 50 | 51 | ### Changed 52 | 53 | - dry-core now uses zeitwerk for autoloading (@solnic) 54 | 55 | [Compare v0.8.1...v0.9.0](https://github.com/dry-rb/dry-core/compare/v0.8.1...v0.9.0) 56 | 57 | ## 0.8.1 2022-07-27 58 | 59 | 60 | ### Fixed 61 | 62 | - [memoizable] plays better with inheritance. 63 | There were cases when cached values from base claesses were used, see #70 (@flash-gordon) 64 | 65 | 66 | 67 | [Compare v0.8.0...v0.8.1](https://github.com/dry-rb/dry-core/compare/v0.8.0...v0.8.1) 68 | 69 | ## 0.8.0 2022-07-15 70 | 71 | 72 | ### Added 73 | 74 | - `Dry::Core::BasicObject` ported from hanami-utils (@jodosha) 75 | 76 | ### Changed 77 | 78 | - [BREAKING] [descendants tracker] switch to using `Class#subclasses` on Ruby 3.1+. 79 | This changes the order of returned subclasses (immediate subclasses now go first) (@flash-gordon) 80 | 81 | 82 | [Compare v0.7.1...v0.8.0](https://github.com/dry-rb/dry-core/compare/v0.7.1...v0.8.0) 83 | 84 | ## 0.7.1 2021-07-10 85 | 86 | 87 | ### Fixed 88 | 89 | - [memoizable] memoizable correctly handles cases where a method 90 | has unnamed params (e.g. happens when the new `...` syntax is used) (@flash-gordon) 91 | 92 | 93 | 94 | [Compare v0.7.0...v0.7.1](https://github.com/dry-rb/dry-core/compare/v0.7.0...v0.7.1) 95 | 96 | ## 0.7.0 2021-07-08 97 | 98 | 99 | ### Fixed 100 | 101 | - [memoizable] warnings when using keyword arguments (@flash-gordon) 102 | - [deprecations] warnings show more relevant information about caller by default (@timriley) 103 | 104 | ### Changed 105 | 106 | - Minimal Ruby version is 2.6 107 | - [memoizable] memoization of block-accepting methods is deprecated (@flash-gordon) 108 | 109 | [Compare v0.6.0...v0.7.0](https://github.com/dry-rb/dry-core/compare/v0.6.0...v0.7.0) 110 | 111 | ## 0.6.0 2021-06-03 112 | 113 | 114 | ### Added 115 | 116 | - [memoizable] support for `BasicObject` (@oleander) 117 | - [memoizable] support for methods that accept blocks (@oleander) 118 | - [deprecations] allow printing frame info on warn when setting up Deprecation module (via #52) (@waiting-for-dev) 119 | 120 | ### Fixed 121 | 122 | - [memoizable] works with MRI 2.7+ keyword arguments now (@oleander) 123 | 124 | 125 | [Compare v0.5.0...v0.6.0](https://github.com/dry-rb/dry-core/compare/v0.5.0...v0.6.0) 126 | 127 | ## 0.5.0 2020-12-12 128 | 129 | 130 | ### Added 131 | 132 | - dry-equalizer has been imported into dry-core as `Dry::Core::Equalizer` but the interface remains the same, which is `include Dry.Equalizer(...)` - we'll be porting all other gems that depend on dry-equalizer to the latest dry-core with equalizer included *gradually*. Eventually dry-equalizer usage will be gone completely in rom-rb/dry-rb/hanami projects (@solnic) 133 | 134 | 135 | [Compare v0.4.10...v0.5.0](https://github.com/dry-rb/dry-core/compare/v0.4.10...v0.5.0) 136 | 137 | ## 0.4.10 2020-11-19 138 | 139 | 140 | ### Added 141 | 142 | - `ClassAttributes.defines` gets a new option for coercing values (tallica) 143 | ```ruby 144 | class Builder 145 | extend Dry::Core::ClassAttributes 146 | 147 | defines :nodes, coerce: -> value { Integer(value) } 148 | end 149 | ``` 150 | `:coerce` works with any callable as well as types from dry-types 151 | ```ruby 152 | defines :nodes, coerce: Dry::Types['coercible.integer'] 153 | ``` 154 | - `Constants::IDENTITY` which is the identity function (flash-gordon) 155 | 156 | 157 | [Compare v0.4.9...v0.4.10](https://github.com/dry-rb/dry-core/compare/v0.4.9...v0.4.10) 158 | 159 | ## 0.4.9 2019-08-09 160 | 161 | 162 | ### Added 163 | 164 | - `Undefined.coalesce` takes a variable number of arguments and returns the first non-`Undefined` value (flash-gordon) 165 | 166 | ```ruby 167 | Undefined.coalesce(Undefined, Undefined, :foo) # => :foo 168 | ``` 169 | 170 | ### Fixed 171 | 172 | - `Undefined.{dup,clone}` returns `Undefined` back, `Undefined` is a singleton (flash-gordon) 173 | 174 | 175 | [Compare v0.4.8...v0.4.9](https://github.com/dry-rb/dry-core/compare/v0.4.8...v0.4.9) 176 | 177 | ## 0.4.8 2019-06-23 178 | 179 | 180 | ### Added 181 | 182 | - `Undefined.map` for mapping non-undefined values (flash-gordon) 183 | 184 | ```ruby 185 | something = 1 186 | Undefined.map(something) { |v| v + 1 } # => 2 187 | 188 | something = Undefined 189 | Undefined.map(something) { |v| v + 1 } # => Undefined 190 | ``` 191 | 192 | 193 | [Compare v0.4.7...v0.4.8](https://github.com/dry-rb/dry-core/compare/v0.4.7...v0.4.8) 194 | 195 | ## 0.4.7 2018-06-25 196 | 197 | 198 | ### Fixed 199 | 200 | - Fix default logger for deprecations, it now uses `$stderr` by default, as it should (flash-gordon) 201 | 202 | 203 | [Compare v0.4.6...v0.4.7](https://github.com/dry-rb/dry-core/compare/v0.4.6...v0.4.7) 204 | 205 | ## 0.4.6 2018-05-15 206 | 207 | 208 | ### Changed 209 | 210 | - Trigger constant autoloading in the class builder (radar) 211 | 212 | [Compare v0.4.5...v0.4.6](https://github.com/dry-rb/dry-core/compare/v0.4.5...v0.4.6) 213 | 214 | ## 0.4.5 2018-03-14 215 | 216 | 217 | ### Added 218 | 219 | - `Dry::Core::Memoizable`, which provides a `memoize` macro for memoizing results of instance methods (timriley) 220 | 221 | 222 | [Compare v0.4.4...v0.4.5](https://github.com/dry-rb/dry-core/compare/v0.4.4...v0.4.5) 223 | 224 | ## 0.4.4 2018-02-10 225 | 226 | 227 | ### Added 228 | 229 | - `deprecate_constant` overrides `Module#deprecate_constant` and issues a labeled message on accessing a deprecated constant (flash-gordon) 230 | - `Undefined.default` which accepts two arguments and returns the first if it's not `Undefined`; otherwise, returns the second one or yields a block (flash-gordon) 231 | 232 | 233 | [Compare v0.4.3...v0.4.4](https://github.com/dry-rb/dry-core/compare/v0.4.3...v0.4.4) 234 | 235 | ## 0.4.3 2018-02-03 236 | 237 | 238 | ### Added 239 | 240 | - `Dry::Core::DescendantsTracker` which is a maintained version of the [`descendants_tracker`](https://github.com/dkubb/descendants_tracker) gem (flash-gordon) 241 | 242 | 243 | [Compare v0.4.2...v0.4.3](https://github.com/dry-rb/dry-core/compare/v0.4.2...v0.4.3) 244 | 245 | ## 0.4.2 2017-12-16 246 | 247 | 248 | ### Fixed 249 | 250 | - Class attributes now support private setters/getters (flash-gordon) 251 | 252 | 253 | [Compare v0.4.1...v0.4.2](https://github.com/dry-rb/dry-core/compare/v0.4.1...v0.4.2) 254 | 255 | ## 0.4.1 2017-11-04 256 | 257 | 258 | ### Changed 259 | 260 | - Improved error message on invalid attribute value (GustavoCaso) 261 | 262 | [Compare v0.4.0...v0.4.1](https://github.com/dry-rb/dry-core/compare/v0.4.0...v0.4.1) 263 | 264 | ## 0.4.0 2017-11-02 265 | 266 | 267 | ### Added 268 | 269 | - Added the `:type` option to class attributes, you can now restrict attribute values with a type. You can either use plain ruby types (`Integer`, `String`, etc) or `dry-types` (GustavoCaso) 270 | 271 | ```ruby 272 | class Foo 273 | extend Dry::Core::ClassAttributes 274 | 275 | defines :ruby_attr, type: Integer 276 | defines :dry_attr, type: Dry::Types['strict.int'] 277 | end 278 | ``` 279 | 280 | 281 | [Compare v0.3.4...v0.4.0](https://github.com/dry-rb/dry-core/compare/v0.3.4...v0.4.0) 282 | 283 | ## 0.3.4 2017-09-29 284 | 285 | 286 | ### Fixed 287 | 288 | - `Deprecations` output is set to `$stderr` by default now (solnic) 289 | 290 | 291 | [Compare v0.3.3...v0.3.4](https://github.com/dry-rb/dry-core/compare/v0.3.3...v0.3.4) 292 | 293 | ## 0.3.3 2017-08-31 294 | 295 | 296 | ### Fixed 297 | 298 | - The Deprecations module now shows the right caller line (flash-gordon) 299 | 300 | 301 | [Compare v0.3.2...v0.3.3](https://github.com/dry-rb/dry-core/compare/v0.3.2...v0.3.3) 302 | 303 | ## 0.3.2 2017-08-31 304 | 305 | 306 | ### Added 307 | 308 | - Accept an existing logger object in `Dry::Core::Deprecations.set_logger!` (flash-gordon) 309 | 310 | 311 | [Compare v0.3.1...v0.3.2](https://github.com/dry-rb/dry-core/compare/v0.3.1...v0.3.2) 312 | 313 | ## 0.3.1 2017-05-27 314 | 315 | 316 | ### Added 317 | 318 | - Support for building classes within an existing namespace (flash-gordon) 319 | 320 | 321 | [Compare v0.3.0...v0.3.1](https://github.com/dry-rb/dry-core/compare/v0.3.0...v0.3.1) 322 | 323 | ## 0.3.0 2017-05-05 324 | 325 | 326 | ### Changed 327 | 328 | - Class attributes are initialized _before_ running the `inherited` hook. It's slightly more convenient behavior and it's very unlikely anyone will be affected by this, but technically this is a breaking change (flash-gordon) 329 | 330 | [Compare v0.2.4...v0.3.0](https://github.com/dry-rb/dry-core/compare/v0.2.4...v0.3.0) 331 | 332 | ## 0.2.4 2017-01-26 333 | 334 | 335 | ### Fixed 336 | 337 | - Do not require deprecated method to be defined (flash-gordon) 338 | 339 | 340 | [Compare v0.2.3...v0.2.4](https://github.com/dry-rb/dry-core/compare/v0.2.3...v0.2.4) 341 | 342 | ## 0.2.3 2016-12-30 343 | 344 | 345 | ### Fixed 346 | 347 | - Fix warnings on using uninitialized class attributes (flash-gordon) 348 | 349 | 350 | [Compare v0.2.2...v0.2.3](https://github.com/dry-rb/dry-core/compare/v0.2.2...v0.2.3) 351 | 352 | ## 0.2.2 2016-12-30 353 | 354 | 355 | ### Added 356 | 357 | - `ClassAttributes` which provides `defines` method for defining get-or-set methods (flash-gordon) 358 | 359 | 360 | [Compare v0.2.1...v0.2.2](https://github.com/dry-rb/dry-core/compare/v0.2.1...v0.2.2) 361 | 362 | ## 0.2.1 2016-11-18 363 | 364 | 365 | ### Added 366 | 367 | - `Constants` are now available in nested scopes (flash-gordon) 368 | 369 | 370 | [Compare v0.2.0...v0.2.1](https://github.com/dry-rb/dry-core/compare/v0.2.0...v0.2.1) 371 | 372 | ## 0.2.0 2016-11-01 373 | 374 | 375 | 376 | [Compare v0.1.0...v0.2.0](https://github.com/dry-rb/dry-core/compare/v0.1.0...v0.2.0) 377 | 378 | ## 0.1.0 2016-09-17 379 | 380 | Initial release 381 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, caste, color, religion, or sexual 10 | identity and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or advances of 31 | any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email address, 35 | without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | team at dry-rb.org. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series of 86 | actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or permanent 93 | ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within the 113 | community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.1, available at 119 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 120 | 121 | Community Impact Guidelines were inspired by 122 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 123 | 124 | For answers to common questions about this code of conduct, see the FAQ at 125 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 126 | [https://www.contributor-covenant.org/translations][translations]. 127 | 128 | [homepage]: https://www.contributor-covenant.org 129 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 130 | [Mozilla CoC]: https://github.com/mozilla/diversity 131 | [FAQ]: https://www.contributor-covenant.org/faq 132 | [translations]: https://www.contributor-covenant.org/translations 133 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Issue Guidelines 2 | 3 | ## Reporting bugs 4 | 5 | If you found a bug, report an issue and describe what's the expected behavior versus what actually happens. If the bug causes a crash, attach a full backtrace. If possible, a reproduction script showing the problem is highly appreciated. 6 | 7 | ## Reporting feature requests 8 | 9 | Report a feature request **only after discussing it first on [discourse.dry-rb.org](https://discourse.dry-rb.org)** where it was accepted. Please provide a concise description of the feature. 10 | 11 | ## Reporting questions, support requests, ideas, concerns etc. 12 | 13 | **PLEASE DON'T** - use [discourse.dry-rb.org](https://discourse.dry-rb.org) instead. 14 | 15 | # Pull Request Guidelines 16 | 17 | A Pull Request will only be accepted if it addresses a specific issue that was reported previously, or fixes typos, mistakes in documentation etc. 18 | 19 | Other requirements: 20 | 21 | 1) Do not open a pull request if you can't provide tests along with it. If you have problems writing tests, ask for help in the related issue. 22 | 2) Follow the style conventions of the surrounding code. In most cases, this is standard ruby style. 23 | 3) Add API documentation if it's a new feature 24 | 4) Update API documentation if it changes an existing feature 25 | 5) Bonus points for sending a PR which updates user documentation in the `docsite` directory 26 | 27 | # Asking for help 28 | 29 | If these guidelines aren't helpful, and you're stuck, please post a message on [discourse.dry-rb.org](https://discourse.dry-rb.org). 30 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | eval_gemfile "Gemfile.devtools" 6 | 7 | gemspec 8 | 9 | group :test do 10 | gem "activesupport" 11 | gem "dry-inflector", github: "dry-rb/dry-inflector", branch: "main" 12 | gem "dry-logic", github: "dry-rb/dry-logic", branch: "main" 13 | gem "dry-types", github: "dry-rb/dry-types", branch: "main" 14 | gem "inflecto", "~> 0.0", ">= 0.0.2" 15 | end 16 | 17 | group :tools do 18 | gem "byebug", platform: :mri 19 | end 20 | -------------------------------------------------------------------------------- /Gemfile.devtools: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file is synced from dry-rb/template-gem repo 4 | 5 | gem "rake", ">= 12.3.3" 6 | 7 | group :test do 8 | gem "simplecov", require: false, platforms: :ruby 9 | gem "simplecov-cobertura", require: false, platforms: :ruby 10 | gem "rexml", require: false 11 | 12 | gem "warning" 13 | gem "rspec" 14 | end 15 | 16 | group :tools do 17 | gem "rubocop", "~> 1.69.2" 18 | gem "byebug" 19 | end 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2023 dry-rb team 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Nikita Shilnikov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [gem]: https://rubygems.org/gems/dry-core 3 | [actions]: https://github.com/dry-rb/dry-core/actions 4 | 5 | # dry-core [![Gem Version](https://badge.fury.io/rb/dry-core.svg)][gem] [![CI Status](https://github.com/dry-rb/dry-core/workflows/CI/badge.svg)][actions] 6 | 7 | ## Links 8 | 9 | * [User documentation](https://dry-rb.org/gems/dry-core) 10 | * [API documentation](http://rubydoc.info/gems/dry-core) 11 | * [Forum](https://discourse.dry-rb.org) 12 | 13 | ## Supported Ruby versions 14 | 15 | This library officially supports the following Ruby versions: 16 | 17 | * MRI `>= 3.1` 18 | * jruby `>= 9.4` (not tested on CI) 19 | 20 | ## License 21 | 22 | See `LICENSE` file. 23 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | require "rspec/core/rake_task" 5 | 6 | RSpec::Core::RakeTask.new(:spec) 7 | 8 | task default: :spec 9 | -------------------------------------------------------------------------------- /bin/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dry-rb/dry-core/df792ef0c6c75baf9fdd171c6524b656e8512336/bin/.gitkeep -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | require "dry/core" 6 | 7 | # You can add fixtures and/or initialization code here to make experimenting 8 | # with your gem easier. You can also use a different console, if you like. 9 | 10 | binding.irb 11 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /changelog.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - version: 1.1.0 3 | summary: 4 | date: 2025-01-04 5 | changed: 6 | - Minimal Ruby version is 3.1 (@flash-gordon) 7 | - 'Fixed clash with `dry-logger` (see #80) (@flash-gordon)' 8 | - version: 1.0.1 9 | summary: 10 | date: 2023-08-06 11 | changed: 12 | - Minimal Ruby version is 3.0 (@flash-gordon) 13 | fixed: 14 | - | 15 | [equalizer] Add `Dry::Core.Equalizer` method to make `include Dry::Core.Equalizer(...)` work as documented (via #79) (@timriley) 16 | 17 | Users of Equalizer should now only need to `require "dry/core"` first. 18 | - version: 1.0.0 19 | summary: 20 | date: 2022-11-04 21 | fixed: 22 | added: 23 | - 'Import dry-container as `Dry::Core::Container` (via #77) (@solnic)' 24 | changed: 25 | - version: 0.9.1 26 | date: 2022-10-18 27 | changed: 28 | - 'Correct missing constant for IDENTITY (issue #75 fixed via #76) (@poloka)' 29 | - version: 0.9.0 30 | date: 2022-10-15 31 | changed: 32 | - dry-core now uses zeitwerk for autoloading (@solnic) 33 | - version: 0.8.1 34 | date: 2022-07-27 35 | fixed: 36 | - | 37 | [memoizable] plays better with inheritance. 38 | There were cases when cached values from base claesses were used, see #70 (@flash-gordon) 39 | - version: 0.8.0 40 | date: 2022-07-15 41 | added: 42 | - "`Dry::Core::BasicObject` ported from hanami-utils (@jodosha)" 43 | changed: 44 | - | 45 | [BREAKING] [descendants tracker] switch to using `Class#subclasses` on Ruby 3.1+. 46 | This changes the order of returned subclasses (immediate subclasses now go first) (@flash-gordon) 47 | - version: 0.7.1 48 | date: 2021-07-10 49 | fixed: 50 | - | 51 | [memoizable] memoizable correctly handles cases where a method 52 | has unnamed params (e.g. happens when the new `...` syntax is used) (@flash-gordon) 53 | - version: 0.7.0 54 | date: 2021-07-08 55 | fixed: 56 | - "[memoizable] warnings when using keyword arguments (@flash-gordon)" 57 | - "[deprecations] warnings show more relevant information about caller by default 58 | (@timriley)" 59 | changed: 60 | - Minimal Ruby version is 2.6 61 | - "[memoizable] memoization of block-accepting methods is deprecated (@flash-gordon)" 62 | - version: 0.6.0 63 | summary: 64 | date: 2021-06-03 65 | fixed: 66 | - "[memoizable] works with MRI 2.7+ keyword arguments now (@oleander)" 67 | added: 68 | - "[memoizable] support for `BasicObject` (@oleander)" 69 | - "[memoizable] support for methods that accept blocks (@oleander)" 70 | - "[deprecations] allow printing frame info on warn when setting up Deprecation 71 | module (via #52) (@waiting-for-dev)" 72 | changed: 73 | - version: 0.5.0 74 | summary: 75 | date: '2020-12-12' 76 | fixed: 77 | added: 78 | - dry-equalizer has been imported into dry-core as `Dry::Core::Equalizer` but the 79 | interface remains the same, which is `include Dry.Equalizer(...)` - we'll be porting 80 | all other gems that depend on dry-equalizer to the latest dry-core with equalizer 81 | included *gradually*. Eventually dry-equalizer usage will be gone completely in 82 | rom-rb/dry-rb/hanami projects (@solnic) 83 | changed: 84 | - version: 0.4.10 85 | date: '2020-11-19' 86 | added: 87 | - |- 88 | `ClassAttributes.defines` gets a new option for coercing values (tallica) 89 | ```ruby 90 | class Builder 91 | extend Dry::Core::ClassAttributes 92 | 93 | defines :nodes, coerce: -> value { Integer(value) } 94 | end 95 | ``` 96 | `:coerce` works with any callable as well as types from dry-types 97 | ```ruby 98 | defines :nodes, coerce: Dry::Types['coercible.integer'] 99 | ``` 100 | - "`Constants::IDENTITY` which is the identity function (flash-gordon)" 101 | - version: 0.4.9 102 | date: '2019-08-09' 103 | added: 104 | - |- 105 | `Undefined.coalesce` takes a variable number of arguments and returns the first non-`Undefined` value (flash-gordon) 106 | 107 | ```ruby 108 | Undefined.coalesce(Undefined, Undefined, :foo) # => :foo 109 | ``` 110 | fixed: 111 | - "`Undefined.{dup,clone}` returns `Undefined` back, `Undefined` is a singleton 112 | (flash-gordon)" 113 | - version: 0.4.8 114 | date: '2019-06-23' 115 | added: 116 | - |- 117 | `Undefined.map` for mapping non-undefined values (flash-gordon) 118 | 119 | ```ruby 120 | something = 1 121 | Undefined.map(something) { |v| v + 1 } # => 2 122 | 123 | something = Undefined 124 | Undefined.map(something) { |v| v + 1 } # => Undefined 125 | ``` 126 | - version: 0.4.7 127 | date: '2018-06-25' 128 | fixed: 129 | - Fix default logger for deprecations, it now uses `$stderr` by default, as it should 130 | (flash-gordon) 131 | - version: 0.4.6 132 | date: '2018-05-15' 133 | changed: 134 | - Trigger constant autoloading in the class builder (radar) 135 | - version: 0.4.5 136 | date: '2018-03-14' 137 | added: 138 | - "`Dry::Core::Memoizable`, which provides a `memoize` macro for memoizing results 139 | of instance methods (timriley)" 140 | - version: 0.4.4 141 | date: '2018-02-10' 142 | added: 143 | - "`deprecate_constant` overrides `Module#deprecate_constant` and issues a labeled 144 | message on accessing a deprecated constant (flash-gordon)" 145 | - "`Undefined.default` which accepts two arguments and returns the first if it's 146 | not `Undefined`; otherwise, returns the second one or yields a block (flash-gordon)" 147 | - version: 0.4.3 148 | date: '2018-02-03' 149 | added: 150 | - "`Dry::Core::DescendantsTracker` which is a maintained version of the [`descendants_tracker`](https://github.com/dkubb/descendants_tracker) 151 | gem (flash-gordon)" 152 | - version: 0.4.2 153 | date: '2017-12-16' 154 | fixed: 155 | - Class attributes now support private setters/getters (flash-gordon) 156 | - version: 0.4.1 157 | date: '2017-11-04' 158 | changed: 159 | - Improved error message on invalid attribute value (GustavoCaso) 160 | - version: 0.4.0 161 | date: '2017-11-02' 162 | added: 163 | - |- 164 | Added the `:type` option to class attributes, you can now restrict attribute values with a type. You can either use plain ruby types (`Integer`, `String`, etc) or `dry-types` (GustavoCaso) 165 | 166 | ```ruby 167 | class Foo 168 | extend Dry::Core::ClassAttributes 169 | 170 | defines :ruby_attr, type: Integer 171 | defines :dry_attr, type: Dry::Types['strict.int'] 172 | end 173 | ``` 174 | - version: 0.3.4 175 | date: '2017-09-29' 176 | fixed: 177 | - "`Deprecations` output is set to `$stderr` by default now (solnic)" 178 | - version: 0.3.3 179 | date: '2017-08-31' 180 | fixed: 181 | - The Deprecations module now shows the right caller line (flash-gordon) 182 | - version: 0.3.2 183 | date: '2017-08-31' 184 | added: 185 | - Accept an existing logger object in `Dry::Core::Deprecations.set_logger!` (flash-gordon) 186 | - version: 0.3.1 187 | date: '2017-05-27' 188 | added: 189 | - Support for building classes within an existing namespace (flash-gordon) 190 | - version: 0.3.0 191 | date: '2017-05-05' 192 | changed: 193 | - Class attributes are initialized _before_ running the `inherited` hook. It's slightly 194 | more convenient behavior and it's very unlikely anyone will be affected by this, 195 | but technically this is a breaking change (flash-gordon) 196 | - version: 0.2.4 197 | date: '2017-01-26' 198 | fixed: 199 | - Do not require deprecated method to be defined (flash-gordon) 200 | - version: 0.2.3 201 | date: '2016-12-30' 202 | fixed: 203 | - Fix warnings on using uninitialized class attributes (flash-gordon) 204 | - version: 0.2.2 205 | date: '2016-12-30' 206 | added: 207 | - "`ClassAttributes` which provides `defines` method for defining get-or-set methods 208 | (flash-gordon)" 209 | - version: 0.2.1 210 | date: '2016-11-18' 211 | added: 212 | - "`Constants` are now available in nested scopes (flash-gordon)" 213 | - version: 0.2.0 214 | date: '2016-11-01' 215 | - version: 0.1.0 216 | date: '2016-09-17' 217 | summary: Initial release 218 | -------------------------------------------------------------------------------- /docsite/source/cache.html.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Cache 3 | layout: gem-single 4 | name: dry-core 5 | --- 6 | 7 | Allows you to cache call results that are solely determined by arguments. 8 | 9 | ```ruby 10 | require "dry/core" 11 | 12 | class Foo 13 | extend Dry::Core::Cache 14 | 15 | attr_reader :source 16 | 17 | def initialize(source) 18 | @source = source 19 | end 20 | 21 | def heavy_computation(arg1, arg2) 22 | fetch_or_store(source, arg1, arg2) { source ^ arg1 ^ arg2 } 23 | end 24 | end 25 | ``` 26 | 27 | ### Note 28 | 29 | Beware Proc instance hashes are not equal, i.e. `-> { 1 }.hash != -> { 1 }.hash`. 30 | This means you shouldn't pass Procs in args unless you're sure they are always the same instances, otherwise you introduce a memory leak 31 | -------------------------------------------------------------------------------- /docsite/source/classes.html.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Working with Classes 3 | layout: gem-single 4 | name: dry-core 5 | sections: 6 | - class-attributes 7 | - class-builder 8 | --- 9 | 10 | You can enhance your classes using the [Class Attributes](docs::classes/class-attributes) or eliminate extra boilerplate using the [Class Builder](docs::classes/class-builder) 11 | -------------------------------------------------------------------------------- /docsite/source/classes/class-attributes.html.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Class Attributes 3 | layout: gem-single 4 | name: dry-core 5 | --- 6 | 7 | ```ruby 8 | require 'dry/core/class_attributes' 9 | 10 | class ExtraClass 11 | extend Dry::Core::ClassAttributes 12 | 13 | defines :hello 14 | 15 | hello 'world' 16 | end 17 | 18 | # example with inheritance and type checking 19 | # setting up an invalid value will raise Dry::Core::InvalidClassAttributeValueError 20 | 21 | class MyClass 22 | extend Dry::Core::ClassAttributes 23 | 24 | defines :one, :two, type: Integer 25 | 26 | one 1 27 | two 2 28 | end 29 | 30 | class OtherClass < MyClass 31 | two 3 32 | end 33 | 34 | MyClass.one # => 1 35 | MyClass.two # => 2 36 | 37 | OtherClass.one # => 1 38 | OtherClass.two # => 3 39 | 40 | # example type checking with dry-types 41 | 42 | class Foo 43 | extend Dry::Core::ClassAttributes 44 | 45 | defines :one, :two, type: Dry::Types['strict.integer'] 46 | end 47 | ``` 48 | -------------------------------------------------------------------------------- /docsite/source/classes/class-builder.html.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Class Builder 3 | layout: gem-single 4 | name: dry-core 5 | --- 6 | 7 | ```ruby 8 | require 'dry/core/class_builder' 9 | 10 | builder = Dry::Core::ClassBuilder.new(name: 'MyClass') 11 | 12 | klass = builder.call 13 | klass.name # => "MyClass" 14 | ``` 15 | -------------------------------------------------------------------------------- /docsite/source/constants.html.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Constants 3 | layout: gem-single 4 | name: dry-core 5 | --- 6 | 7 | A list of constants you can use to avoid memory allocations or identity checks. 8 | 9 | * `EMPTY_ARRAY` 10 | * `EMPTY_HASH` 11 | * `EMPTY_OPTS` 12 | * `EMPTY_SET` 13 | * `EMPTY_STRING` 14 | * `Undefined` - A special value you can use as a default to know if no arguments were passed to you method 15 | -------------------------------------------------------------------------------- /docsite/source/deprecations.html.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Deprecations 3 | layout: gem-single 4 | name: dry-core 5 | --- 6 | 7 | To deprecate ruby methods you need to extend the `Dry::Core::Deprecations` module with a tag that will be displayed in the output. For example: 8 | 9 | ```ruby 10 | require "dry/core" 11 | 12 | class Foo 13 | extend Dry::Core::Deprecations[:tag] 14 | 15 | def self.old_class_api; end 16 | def self.new_class_api; end 17 | 18 | deprecate_class_method :old_class_api, :new_class_api 19 | 20 | def old_api; end 21 | def new_api; end 22 | 23 | deprecate :old_api, :new_api 24 | end 25 | 26 | Foo.old_class_api 27 | # => [tag] Foo.old_class_api is deprecated and will be removed in the next major version 28 | # => Please use Foo.new_class_api instead. 29 | # => file.rb:9:in `' 30 | 31 | Foo.new.old_api 32 | # => [tag] Foo#old_api is deprecated and will be removed in the next major version 33 | # => Please use Foo#new_api instead. 34 | # => file.rb:14:in `' 35 | ``` 36 | -------------------------------------------------------------------------------- /docsite/source/equalizer.html.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Equalizer 3 | layout: gem-single 4 | name: dry-core 5 | --- 6 | 7 | A simple mixin that can be used to add instance variable based equality, equivalence and inspection methods to your objects. 8 | 9 | ### Usage 10 | 11 | ```ruby 12 | require "dry/core" 13 | 14 | class GeoLocation 15 | include Dry::Core::Equalizer(:latitude, :longitude) 16 | 17 | attr_reader :latitude, :longitude 18 | 19 | def initialize(latitude, longitude) 20 | @latitude, @longitude = latitude, longitude 21 | end 22 | end 23 | 24 | point_a = GeoLocation.new(1, 2) 25 | point_b = GeoLocation.new(1, 2) 26 | point_c = GeoLocation.new(2, 2) 27 | 28 | point_a.inspect # => "#" 29 | 30 | point_a == point_b # => true 31 | point_a.hash == point_b.hash # => true 32 | point_a.eql?(point_b) # => true 33 | point_a.equal?(point_b) # => false 34 | 35 | point_a == point_c # => false 36 | point_a.hash == point_c.hash # => false 37 | point_a.eql?(point_c) # => false 38 | point_a.equal?(point_c) # => false 39 | ``` 40 | 41 | ### Configuration options 42 | 43 | #### `inspect` 44 | 45 | Use `inspect` option to skip `#inspect` method overloading: 46 | 47 | ```ruby 48 | class Foo 49 | include Dry::Core::Equalizer(:a, inspect: false) 50 | 51 | attr_reader :a, :b 52 | 53 | def initialize(a, b) 54 | @a, @b = a, b 55 | end 56 | end 57 | 58 | Foo.new(1, 2).inspect 59 | # => "#" 60 | ``` 61 | 62 | #### `immutable` 63 | 64 | For objects that are immutable it doesn't make sense to calculate `#hash` every time it's called. To memoize hash use `immutable` option: 65 | 66 | ```ruby 67 | class ImmutableHash 68 | include Dry::Core::Equalizer(:foo, :bar, immutable: true) 69 | 70 | attr_accessor :foo, :bar 71 | 72 | def initialize(foo, bar) 73 | @foo, @bar = foo, bar 74 | end 75 | end 76 | 77 | obj = ImmutableHash.new('foo', 'bar') 78 | old_hash = obj.hash 79 | obj.foo = 'changed' 80 | old_hash == obj.hash 81 | # => true 82 | ``` 83 | 84 | -------------------------------------------------------------------------------- /docsite/source/extensions.html.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Extensions 3 | layout: gem-single 4 | name: dry-core 5 | --- 6 | 7 | Define extensions that can be later enabled by the user. 8 | 9 | ```ruby 10 | require "dry/core" 11 | 12 | class Foo 13 | extend Dry::Core::Extensions 14 | 15 | register_extension(:bar) do 16 | def bar; :bar end 17 | end 18 | end 19 | 20 | Foo.new.bar # => NoMethodError 21 | Foo.load_extensions(:bar) 22 | Foo.new.bar # => :bar 23 | ``` 24 | -------------------------------------------------------------------------------- /docsite/source/index.html.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Introduction 3 | description: A toolset of small support modules used throughout the @dry-rb & @rom-rb ecosystems 4 | layout: gem-single 5 | order: 5 6 | type: gem 7 | name: dry-core 8 | sections: 9 | - cache 10 | - constants 11 | - classes 12 | - deprecations 13 | - equalizer 14 | - extensions 15 | --- 16 | 17 | `dry-core` is a simple toolset that can be used in many places. 18 | 19 | ## Features 20 | 21 | - [Cache](docs::cache) - allows you to cache call results that are solely determined by arguments. 22 | - [Class Attributes](docs::classes/class-attributes) 23 | - [Class Builder](docs::classes/class-builder) 24 | - [Constants](docs::constants) - a list of constants you can use to avoid memory allocations or identity checks. 25 | - [Deprecations](docs::deprecations) 26 | - [Equalizer](docs::equalizer) - simple mixin providing equality, equivalence and inspection methods. 27 | - [Extensions](docs::extensions) 28 | -------------------------------------------------------------------------------- /dry-core.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # this file is synced from dry-rb/template-gem project 4 | 5 | lib = File.expand_path("lib", __dir__) 6 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 7 | require "dry/core/version" 8 | 9 | Gem::Specification.new do |spec| 10 | spec.name = "dry-core" 11 | spec.authors = ["Nikita Shilnikov"] 12 | spec.email = ["fg@flashgordon.ru"] 13 | spec.license = "MIT" 14 | spec.version = Dry::Core::VERSION.dup 15 | 16 | spec.summary = "A toolset of small support modules used throughout the dry-rb ecosystem" 17 | spec.description = spec.summary 18 | spec.homepage = "https://dry-rb.org/gems/dry-core" 19 | spec.files = Dir["CHANGELOG.md", "LICENSE", "README.md", "dry-core.gemspec", "lib/**/*"] 20 | spec.bindir = "bin" 21 | spec.executables = [] 22 | spec.require_paths = ["lib"] 23 | 24 | spec.metadata["allowed_push_host"] = "https://rubygems.org" 25 | spec.metadata["changelog_uri"] = "https://github.com/dry-rb/dry-core/blob/main/CHANGELOG.md" 26 | spec.metadata["source_code_uri"] = "https://github.com/dry-rb/dry-core" 27 | spec.metadata["bug_tracker_uri"] = "https://github.com/dry-rb/dry-core/issues" 28 | spec.metadata["rubygems_mfa_required"] = "true" 29 | 30 | spec.required_ruby_version = ">= 3.1.0" 31 | 32 | # to update dependencies edit project.yml 33 | spec.add_dependency "concurrent-ruby", "~> 1.0" 34 | spec.add_dependency "logger" 35 | spec.add_dependency "zeitwerk", "~> 2.6" 36 | end 37 | -------------------------------------------------------------------------------- /lib/dry-core.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/core" 4 | -------------------------------------------------------------------------------- /lib/dry/core.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "zeitwerk" 4 | 5 | require "dry/core/constants" 6 | require "dry/core/errors" 7 | require "dry/core/version" 8 | 9 | # :nodoc: 10 | module Dry 11 | # :nodoc: 12 | module Core 13 | include Constants 14 | 15 | def self.loader 16 | @loader ||= ::Zeitwerk::Loader.new.tap do |loader| 17 | root = ::File.expand_path("..", __dir__) 18 | loader.tag = "dry-core" 19 | loader.inflector = ::Zeitwerk::GemInflector.new("#{root}/dry-core.rb") 20 | loader.push_dir(root) 21 | loader.ignore( 22 | "#{root}/dry-core.rb", 23 | "#{root}/dry/core/{constants,errors,version}.rb" 24 | ) 25 | loader.inflector.inflect("namespace_dsl" => "NamespaceDSL") 26 | end 27 | end 28 | 29 | loader.setup 30 | 31 | # Build an equalizer module for the inclusion in other class 32 | # 33 | # ## Credits 34 | # 35 | # Equalizer has been originally imported from the equalizer gem created by Dan Kubb 36 | # 37 | # @api public 38 | def self.Equalizer(*keys, **options) 39 | Equalizer.new(*keys, **options) 40 | end 41 | end 42 | 43 | # See dry/core/equalizer.rb 44 | unless singleton_class.method_defined?(:Equalizer) 45 | # Build an equalizer module for the inclusion in other class 46 | # 47 | # ## Credits 48 | # 49 | # Equalizer has been originally imported from the equalizer gem created by Dan Kubb 50 | # 51 | # @api public 52 | def self.Equalizer(*keys, **options) 53 | ::Dry::Core::Equalizer.new(*keys, **options) 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/dry/core/basic_object.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This implementation was imported from `hanami-utils` gem. 4 | module Dry 5 | module Core 6 | # BasicObject 7 | # 8 | # @since 0.8.0 9 | class BasicObject < ::BasicObject 10 | # Lookups constants at the top-level namespace, if they are missing in the 11 | # current context. 12 | # 13 | # @param name [Symbol] the constant name 14 | # 15 | # @return [Object, Module] the constant 16 | # 17 | # @raise [NameError] if the constant cannot be found 18 | # 19 | # @since 0.8.0 20 | # @api private 21 | # 22 | # @see https://ruby-doc.org/core/Module.html#method-i-const_missing 23 | def self.const_missing(name) 24 | ::Object.const_get(name) 25 | end 26 | 27 | # Returns the class for debugging purposes. 28 | # 29 | # @since 0.8.0 30 | # 31 | # @see http://ruby-doc.org/core/Object.html#method-i-class 32 | def class 33 | (class << self; self; end).superclass 34 | end 35 | 36 | # Bare minimum inspect for debugging purposes. 37 | # 38 | # @return [String] the inspect string 39 | # 40 | # @since 0.8.0 41 | # 42 | # @see http://ruby-doc.org/core/Object.html#method-i-inspect 43 | inspect_method = ::Kernel.instance_method(:inspect) 44 | define_method(:inspect) do 45 | original = inspect_method.bind_call(self) 46 | "#{original[0...-1]}#{__inspect}>" 47 | end 48 | 49 | # @!macro [attach] instance_of?(class) 50 | # 51 | # Determines if self is an instance of given class or module 52 | # 53 | # @param class [Class,Module] the class of module to verify 54 | # 55 | # @return [TrueClass,FalseClass] the result of the check 56 | # 57 | # @raise [TypeError] if the given argument is not of the expected types 58 | # 59 | # @since 0.8.0 60 | # 61 | # @see http://ruby-doc.org/core/Object.html#method-i-instance_of-3F 62 | define_method :instance_of?, ::Object.instance_method(:instance_of?) 63 | 64 | # @!macro [attach] is_a?(class) 65 | # 66 | # Determines if self is of the type of the object class or module 67 | # 68 | # @param class [Class,Module] the class of module to verify 69 | # 70 | # @return [TrueClass,FalseClass] the result of the check 71 | # 72 | # @raise [TypeError] if the given argument is not of the expected types 73 | # 74 | # @since 0.8.0 75 | # 76 | # @see http://ruby-doc.org/core/Object.html#method-i-is_a-3F 77 | define_method :is_a?, ::Object.instance_method(:is_a?) 78 | 79 | # @!macro [attach] kind_of?(class) 80 | # 81 | # Determines if self is of the kind of the object class or module 82 | # 83 | # @param class [Class,Module] the class of module to verify 84 | # 85 | # @return [TrueClass,FalseClass] the result of the check 86 | # 87 | # @raise [TypeError] if the given argument is not of the expected types 88 | # 89 | # @since 0.8.0 90 | # 91 | # @see http://ruby-doc.org/core/Object.html#method-i-kind_of-3F 92 | define_method :kind_of?, ::Object.instance_method(:kind_of?) 93 | 94 | # Alias for __id__ 95 | # 96 | # @return [Fixnum] the object id 97 | # 98 | # @since 0.8.0 99 | # 100 | # @see http://ruby-doc.org/core/Object.html#method-i-object_id 101 | def object_id 102 | __id__ 103 | end 104 | 105 | # Interface for pp 106 | # 107 | # @param printer [PP] the Pretty Printable printer 108 | # @return [String] the pretty-printable inspection of the object 109 | # 110 | # @since 0.8.0 111 | # 112 | # @see https://ruby-doc.org/stdlib/libdoc/pp/rdoc/PP.html 113 | def pretty_print(printer) 114 | printer.text(inspect) 115 | end 116 | 117 | # Returns true if responds to the given method. 118 | # 119 | # @return [TrueClass,FalseClass] the result of the check 120 | # 121 | # @since 0.8.0 122 | # 123 | # @see http://ruby-doc.org/core/Object.html#method-i-respond_to-3F 124 | def respond_to?(method_name, include_all = false) # rubocop:disable Style/OptionalBooleanParameter 125 | respond_to_missing?(method_name, include_all) 126 | end 127 | 128 | private 129 | 130 | # Must be overridden by descendants 131 | # 132 | # @since 0.8.0 133 | # @api private 134 | def respond_to_missing?(_method_name, _include_all) 135 | ::Kernel.raise ::NotImplementedError 136 | end 137 | 138 | # @since 0.8.0 139 | # @api private 140 | def __inspect; end 141 | end 142 | end 143 | end 144 | -------------------------------------------------------------------------------- /lib/dry/core/cache.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "concurrent/map" 4 | 5 | module Dry 6 | module Core 7 | # Allows you to cache call results that are solely determined by arguments. 8 | # 9 | # @example 10 | # require 'dry/core/cache' 11 | # 12 | # class Foo 13 | # extend Dry::Core::Cache 14 | # 15 | # def heavy_computation(arg1, arg2) 16 | # fetch_or_store(arg1, arg2) { arg1 ^ arg2 } 17 | # end 18 | # end 19 | # 20 | # @api public 21 | module Cache 22 | # @api private 23 | def self.extended(klass) 24 | super 25 | klass.include(Methods) 26 | klass.instance_variable_set(:@__cache__, ::Concurrent::Map.new) 27 | end 28 | 29 | # @api private 30 | def inherited(klass) 31 | super 32 | klass.instance_variable_set(:@__cache__, cache) 33 | end 34 | 35 | # @api private 36 | def cache 37 | @__cache__ 38 | end 39 | 40 | # Caches a result of the block evaluation 41 | # 42 | # @param [Array] args List of hashable objects 43 | # @yield An arbitrary block 44 | # 45 | # @note beware Proc instance hashes are not equal, i.e. -> { 1 }.hash != -> { 1 }.hash, 46 | # this means you shouldn't pass Procs in args unless you're sure 47 | # they are always the same instances, otherwise you introduce a memory leak 48 | # 49 | # @return [Object] block's return value (cached for subsequent calls with 50 | # the same argument values) 51 | def fetch_or_store(*args, &) 52 | cache.fetch_or_store(args.hash, &) 53 | end 54 | 55 | # Instance methods 56 | module Methods 57 | # Delegates call to the class-level method 58 | # 59 | # @param [Array] args List of hashable objects 60 | # @yield An arbitrary block 61 | # 62 | # @return [Object] block's return value 63 | def fetch_or_store(...) 64 | self.class.fetch_or_store(...) 65 | end 66 | end 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/dry/core/class_attributes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/core/constants" 4 | 5 | module Dry 6 | module Core 7 | # Internal support module for class-level settings 8 | # 9 | # @api public 10 | module ClassAttributes 11 | include Constants 12 | # Specify what attributes a class will use 13 | # 14 | # @example 15 | # class ExtraClass 16 | # extend Dry::Core::ClassAttributes 17 | # 18 | # defines :hello 19 | # 20 | # hello 'world' 21 | # end 22 | # 23 | # @example with inheritance and type checking 24 | # 25 | # class MyClass 26 | # extend Dry::Core::ClassAttributes 27 | # 28 | # defines :one, :two, type: Integer 29 | # 30 | # one 1 31 | # two 2 32 | # end 33 | # 34 | # class OtherClass < MyClass 35 | # two 3 36 | # end 37 | # 38 | # MyClass.one # => 1 39 | # MyClass.two # => 2 40 | # 41 | # OtherClass.one # => 1 42 | # OtherClass.two # => 3 43 | # 44 | # @example with dry-types 45 | # 46 | # class Foo 47 | # extend Dry::Core::ClassAttributes 48 | # 49 | # defines :one, :two, type: Dry::Types['strict.int'] 50 | # end 51 | # 52 | # @example with coercion using Proc 53 | # 54 | # class Bar 55 | # extend Dry::Core::ClassAttributes 56 | # 57 | # defines :one, coerce: proc { |value| value.to_s } 58 | # end 59 | # 60 | # @example with coercion using dry-types 61 | # 62 | # class Bar 63 | # extend Dry::Core::ClassAttributes 64 | # 65 | # defines :one, coerce: Dry::Types['coercible.string'] 66 | # end 67 | # 68 | def defines(*args, type: ::Object, coerce: IDENTITY) # rubocop:disable Metrics/PerceivedComplexity 69 | unless coerce.respond_to?(:call) 70 | raise ::ArgumentError, "Non-callable coerce option: #{coerce.inspect}" 71 | end 72 | 73 | mod = ::Module.new do 74 | args.each do |name| 75 | ivar = :"@#{name}" 76 | 77 | define_method(name) do |value = Undefined| 78 | if Undefined.equal?(value) 79 | if instance_variable_defined?(ivar) 80 | instance_variable_get(ivar) 81 | else 82 | nil 83 | end 84 | elsif type === value # rubocop:disable Style/CaseEquality 85 | instance_variable_set(ivar, coerce.call(value)) 86 | else 87 | raise InvalidClassAttributeValueError.new(name, value) 88 | end 89 | end 90 | end 91 | 92 | define_method(:inherited) do |klass| 93 | args.each { |name| klass.send(name, send(name)) } 94 | 95 | super(klass) 96 | end 97 | end 98 | 99 | extend(mod) 100 | end 101 | end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /lib/dry/core/class_builder.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dry 4 | module Core 5 | # Class for generating more classes 6 | class ClassBuilder 7 | ParentClassMismatch = ::Class.new(::TypeError) 8 | 9 | attr_reader :name, :parent, :namespace 10 | 11 | def initialize(name:, parent: nil, namespace: nil) 12 | @name = name 13 | @namespace = namespace 14 | @parent = parent || ::Object 15 | end 16 | 17 | # Generate a class based on options 18 | # 19 | # @example Create anonymous class 20 | # builder = Dry::Core::ClassBuilder.new(name: 'MyClass') 21 | # 22 | # klass = builder.call 23 | # klass.name # => "MyClass" 24 | # 25 | # @example Create named class 26 | # builder = Dry::Core::ClassBuilder.new(name: 'User', namespace: Entities) 27 | # 28 | # klass = builder.call 29 | # klass.name # => "Entities::User" 30 | # klass.superclass.name # => "Entities::User" 31 | # Entities::User # => "Entities::User" 32 | # klass.superclass == Entities::User # => true 33 | # 34 | # @return [Class] 35 | def call 36 | klass = if namespace 37 | create_named 38 | else 39 | create_anonymous 40 | end 41 | 42 | yield(klass) if block_given? 43 | 44 | klass 45 | end 46 | 47 | private 48 | 49 | # @api private 50 | def create_anonymous 51 | klass = ::Class.new(parent) 52 | name = self.name 53 | 54 | klass.singleton_class.class_eval do 55 | define_method(:name) { name } 56 | alias_method :inspect, :name 57 | alias_method :to_s, :name 58 | end 59 | 60 | klass 61 | end 62 | 63 | # @api private 64 | def create_named 65 | name = self.name 66 | base = create_base(namespace, name, parent) 67 | klass = ::Class.new(base) 68 | 69 | namespace.module_eval do 70 | remove_const(name) 71 | const_set(name, klass) 72 | 73 | remove_const(name) 74 | const_set(name, base) 75 | end 76 | 77 | klass 78 | end 79 | 80 | # @api private 81 | def create_base(namespace, name, parent) 82 | begin 83 | namespace.const_get(name) 84 | rescue NameError # rubocop:disable Lint/SuppressedException 85 | end 86 | 87 | if namespace.const_defined?(name, false) 88 | existing = namespace.const_get(name) 89 | 90 | unless existing <= parent 91 | raise ParentClassMismatch, "#{existing.name} must be a subclass of #{parent.name}" 92 | end 93 | 94 | existing 95 | else 96 | klass = ::Class.new(parent || ::Object) 97 | namespace.const_set(name, klass) 98 | klass 99 | end 100 | end 101 | end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /lib/dry/core/constants.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "set" 4 | 5 | module Dry 6 | module Core 7 | # A list of constants you can use to avoid memory allocations or identity checks. 8 | # 9 | # @example Just include this module to your class or module 10 | # class Foo 11 | # include Dry::Core::Constants 12 | # def call(value = EMPTY_ARRAY) 13 | # value.map(&:to_s) 14 | # end 15 | # end 16 | # 17 | # @api public 18 | module Constants 19 | # An empty array 20 | EMPTY_ARRAY = [].freeze 21 | # An empty hash 22 | EMPTY_HASH = {}.freeze 23 | # An empty list of options 24 | EMPTY_OPTS = {}.freeze 25 | # An empty set 26 | EMPTY_SET = ::Set.new.freeze 27 | # An empty string 28 | EMPTY_STRING = "" 29 | # Identity function 30 | IDENTITY = ->(x) { x }.freeze 31 | 32 | # A special value you can use as a default to know if no arguments 33 | # were passed to the method 34 | # 35 | # @example 36 | # def method(value = Undefined) 37 | # if Undefined.equal?(value) 38 | # puts 'no args' 39 | # else 40 | # puts value 41 | # end 42 | # end 43 | Undefined = ::Object.new.tap do |undefined| 44 | # @api private 45 | Self = -> { Undefined } # rubocop:disable Lint/ConstantDefinitionInBlock 46 | 47 | # @api public 48 | def undefined.to_s 49 | "Undefined" 50 | end 51 | 52 | # @api public 53 | def undefined.inspect 54 | "Undefined" 55 | end 56 | 57 | # Pick a value, if the first argument is not Undefined, return it back, 58 | # otherwise return the second arg or yield the block. 59 | # 60 | # @example 61 | # def method(val = Undefined) 62 | # 1 + Undefined.default(val, 2) 63 | # end 64 | # 65 | def undefined.default(x, y = self) 66 | if equal?(x) 67 | if equal?(y) 68 | yield 69 | else 70 | y 71 | end 72 | else 73 | x 74 | end 75 | end 76 | 77 | # Map a non-undefined value 78 | # 79 | # @example 80 | # def add_five(val = Undefined) 81 | # Undefined.map(val) { |x| x + 5 } 82 | # end 83 | # 84 | def undefined.map(value) 85 | if equal?(value) 86 | self 87 | else 88 | yield(value) 89 | end 90 | end 91 | 92 | # @api public 93 | def undefined.dup 94 | self 95 | end 96 | 97 | # @api public 98 | def undefined.clone 99 | self 100 | end 101 | 102 | # @api public 103 | def undefined.coalesce(*args) 104 | args.find(Self) { |x| !equal?(x) } 105 | end 106 | end.freeze 107 | 108 | def self.included(base) 109 | super 110 | 111 | constants.each do |const_name| 112 | base.const_set(const_name, const_get(const_name)) 113 | end 114 | end 115 | end 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /lib/dry/core/container.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dry 4 | module Core 5 | # Thread-safe object registry 6 | # 7 | # @example 8 | # 9 | # container = Dry::Core::Container.new 10 | # container.register(:item, 'item') 11 | # container.resolve(:item) 12 | # => 'item' 13 | # 14 | # container.register(:item1, -> { 'item' }) 15 | # container.resolve(:item1) 16 | # => 'item' 17 | # 18 | # container.register(:item2, -> { 'item' }, call: false) 19 | # container.resolve(:item2) 20 | # => # 21 | # 22 | # @api public 23 | class Container 24 | include Container::Mixin 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/dry/core/container/config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dry 4 | module Core 5 | class Container 6 | # @api public 7 | class Config 8 | DEFAULT_NAMESPACE_SEPARATOR = "." 9 | DEFAULT_RESOLVER = Resolver.new 10 | DEFAULT_REGISTRY = Registry.new 11 | 12 | # @api public 13 | attr_accessor :namespace_separator 14 | 15 | # @api public 16 | attr_accessor :resolver 17 | 18 | # @api public 19 | attr_accessor :registry 20 | 21 | # @api private 22 | def initialize( 23 | namespace_separator: DEFAULT_NAMESPACE_SEPARATOR, 24 | resolver: DEFAULT_RESOLVER, 25 | registry: DEFAULT_REGISTRY 26 | ) 27 | @namespace_separator = namespace_separator 28 | @resolver = resolver 29 | @registry = registry 30 | end 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/dry/core/container/configuration.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dry 4 | module Core 5 | class Container 6 | # @api public 7 | module Configuration 8 | # Use dry/configurable if it's available 9 | begin 10 | require "dry/configurable" 11 | 12 | # @api private 13 | def self.extended(klass) 14 | super 15 | klass.class_eval do 16 | extend Dry::Configurable 17 | 18 | setting :namespace_separator, default: Config::DEFAULT_NAMESPACE_SEPARATOR 19 | setting :resolver, default: Config::DEFAULT_RESOLVER 20 | setting :registry, default: Config::DEFAULT_REGISTRY 21 | end 22 | end 23 | rescue LoadError 24 | # @api private 25 | def config 26 | @config ||= Container::Config.new 27 | end 28 | end 29 | 30 | # @api private 31 | def configure 32 | yield config 33 | end 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/dry/core/container/item.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dry 4 | module Core 5 | class Container 6 | # Base class to abstract Memoizable and Callable implementations 7 | # 8 | # @api abstract 9 | # 10 | class Item 11 | NO_OPTIONS = {}.freeze 12 | 13 | # @return [Mixed] the item to be solved later 14 | attr_reader :item 15 | 16 | # @return [Hash] the options to memoize, call or no. 17 | attr_reader :options 18 | 19 | # @api abstract 20 | def initialize(item, options = NO_OPTIONS) 21 | @item = item 22 | @options = { 23 | call: item.is_a?(::Proc) && item.parameters.empty?, 24 | **options 25 | } 26 | end 27 | 28 | # @api abstract 29 | def call 30 | raise ::NotImplementedError 31 | end 32 | 33 | # @private 34 | def value? 35 | !callable? 36 | end 37 | 38 | # @private 39 | def callable? 40 | options[:call] 41 | end 42 | 43 | # Build a new item with transformation applied 44 | # 45 | # @private 46 | def map(func) 47 | if callable? 48 | self.class.new(-> { func.(item.call) }, options) 49 | else 50 | self.class.new(func.(item), options) 51 | end 52 | end 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/dry/core/container/item/callable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dry 4 | module Core 5 | class Container 6 | class Item 7 | # Callable class to returns a item call 8 | # 9 | # @api public 10 | # 11 | class Callable < Item 12 | # Returns the result of item call or item 13 | # 14 | # @return [Mixed] 15 | def call 16 | callable? ? item.call : item 17 | end 18 | end 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/dry/core/container/item/factory.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dry 4 | module Core 5 | class Container 6 | class Item 7 | # Factory for create an Item to register inside of container 8 | # 9 | # @api public 10 | class Factory 11 | # Creates an Item Memoizable or Callable 12 | # @param [Mixed] item 13 | # @param [Hash] options 14 | # 15 | # @raise [Dry::Core::Container::Error] 16 | # 17 | # @return [Dry::Core::Container::Item::Base] 18 | def call(item, options = {}) 19 | if options[:memoize] 20 | Item::Memoizable.new(item, options) 21 | else 22 | Item::Callable.new(item, options) 23 | end 24 | end 25 | end 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/dry/core/container/item/memoizable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dry 4 | module Core 5 | class Container 6 | class Item 7 | # Memoizable class to store and execute item calls 8 | # 9 | # @api public 10 | # 11 | class Memoizable < Item 12 | # @return [Mutex] the stored mutex 13 | attr_reader :memoize_mutex 14 | 15 | # Returns a new Memoizable instance 16 | # 17 | # @param [Mixed] item 18 | # @param [Hash] options 19 | # 20 | # @raise [Dry::Core::Container::Error] 21 | # 22 | # @return [Dry::Core::Container::Item::Base] 23 | def initialize(item, options = {}) 24 | super 25 | raise_not_supported_error unless callable? 26 | 27 | @memoize_mutex = ::Mutex.new 28 | end 29 | 30 | # Returns the result of item call using a syncronized mutex 31 | # 32 | # @return [Dry::Core::Container::Item::Base] 33 | def call 34 | memoize_mutex.synchronize do 35 | @memoized_item ||= item.call 36 | end 37 | end 38 | 39 | private 40 | 41 | # @private 42 | def raise_not_supported_error 43 | raise ::Dry::Core::Container::Error, "Memoize only supported for a block or a proc" 44 | end 45 | end 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/dry/core/container/mixin.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "concurrent/hash" 4 | require "dry/core/constants" 5 | 6 | module Dry 7 | module Core 8 | class Container 9 | include ::Dry::Core::Constants 10 | 11 | # @api public 12 | Error = ::Class.new(::StandardError) 13 | 14 | # Error raised when key is not defined in the registry 15 | # 16 | # @api public 17 | KeyError = ::Class.new(::KeyError) 18 | 19 | if defined?(::DidYouMean::KeyErrorChecker) 20 | ::DidYouMean.correct_error(KeyError, ::DidYouMean::KeyErrorChecker) 21 | end 22 | 23 | # Mixin to expose Inversion of Control (IoC) container behaviour 24 | # 25 | # @example 26 | # 27 | # class MyClass 28 | # extend Dry::Core::Container::Mixin 29 | # end 30 | # 31 | # MyClass.register(:item, 'item') 32 | # MyClass.resolve(:item) 33 | # => 'item' 34 | # 35 | # class MyObject 36 | # include Dry::Core::Container::Mixin 37 | # end 38 | # 39 | # container = MyObject.new 40 | # container.register(:item, 'item') 41 | # container.resolve(:item) 42 | # => 'item' 43 | # 44 | # @api public 45 | # 46 | # rubocop:disable Metrics/ModuleLength 47 | module Mixin 48 | PREFIX_NAMESPACE = lambda do |namespace, key, config| 49 | [namespace, key].join(config.namespace_separator) 50 | end 51 | 52 | # @private 53 | def self.extended(base) 54 | hooks_mod = ::Module.new do 55 | def inherited(subclass) 56 | subclass.instance_variable_set(:@_container, @_container.dup) 57 | super 58 | end 59 | end 60 | 61 | base.class_eval do 62 | extend Configuration 63 | extend hooks_mod 64 | 65 | @_container = ::Concurrent::Hash.new 66 | end 67 | end 68 | 69 | # @private 70 | module Initializer 71 | def initialize(...) 72 | @_container = ::Concurrent::Hash.new 73 | super 74 | end 75 | end 76 | 77 | # @private 78 | def self.included(base) 79 | base.class_eval do 80 | extend Configuration 81 | prepend Initializer 82 | 83 | def config 84 | self.class.config 85 | end 86 | end 87 | end 88 | 89 | # Register an item with the container to be resolved later 90 | # 91 | # @param [Mixed] key 92 | # The key to register the container item with (used to resolve) 93 | # @param [Mixed] contents 94 | # The item to register with the container (if no block given) 95 | # @param [Hash] options 96 | # Options to pass to the registry when registering the item 97 | # @yield 98 | # If a block is given, contents will be ignored and the block 99 | # will be registered instead 100 | # 101 | # @return [Dry::Core::Container::Mixin] self 102 | # 103 | # @api public 104 | def register(key, contents = nil, options = EMPTY_HASH, &block) 105 | if block_given? 106 | item = block 107 | options = contents if contents.is_a?(::Hash) 108 | else 109 | item = contents 110 | end 111 | 112 | config.registry.call(_container, key, item, options) 113 | 114 | self 115 | rescue ::FrozenError 116 | raise ::FrozenError, 117 | "can't modify frozen #{self.class} (when attempting to register '#{key}')" 118 | end 119 | 120 | # Resolve an item from the container 121 | # 122 | # @param [Mixed] key 123 | # The key for the item you wish to resolve 124 | # @yield 125 | # Fallback block to call when a key is missing. Its result will be returned 126 | # @yieldparam [Mixed] key Missing key 127 | # 128 | # @return [Mixed] 129 | # 130 | # @api public 131 | def resolve(key, &) 132 | config.resolver.call(_container, key, &) 133 | end 134 | 135 | # Resolve an item from the container 136 | # 137 | # @param [Mixed] key 138 | # The key for the item you wish to resolve 139 | # 140 | # @return [Mixed] 141 | # 142 | # @api public 143 | # @see Dry::Core::Container::Mixin#resolve 144 | def [](key) 145 | resolve(key) 146 | end 147 | 148 | # Merge in the items of the other container 149 | # 150 | # @param [Dry::Core::Container] other 151 | # The other container to merge in 152 | # @param [Symbol, nil] namespace 153 | # Namespace to prefix other container items with, defaults to nil 154 | # 155 | # @return [Dry::Core::Container::Mixin] self 156 | # 157 | # @api public 158 | def merge(other, namespace: nil, &block) 159 | if namespace 160 | _container.merge!( 161 | other._container.each_with_object(::Concurrent::Hash.new) { |(key, item), hsh| 162 | hsh[PREFIX_NAMESPACE.call(namespace, key, config)] = item 163 | }, 164 | &block 165 | ) 166 | else 167 | _container.merge!(other._container, &block) 168 | end 169 | 170 | self 171 | end 172 | 173 | # Check whether an item is registered under the given key 174 | # 175 | # @param [Mixed] key 176 | # The key you wish to check for registration with 177 | # 178 | # @return [Bool] 179 | # 180 | # @api public 181 | def key?(key) 182 | config.resolver.key?(_container, key) 183 | end 184 | 185 | # An array of registered names for the container 186 | # 187 | # @return [Array] 188 | # 189 | # @api public 190 | def keys 191 | config.resolver.keys(_container) 192 | end 193 | 194 | # Calls block once for each key in container, passing the key as a parameter. 195 | # 196 | # If no block is given, an enumerator is returned instead. 197 | # 198 | # @return [Dry::Core::Container::Mixin] self 199 | # 200 | # @api public 201 | def each_key(&) 202 | config.resolver.each_key(_container, &) 203 | self 204 | end 205 | 206 | # Calls block once for each key/value pair in the container, passing the key and 207 | # the registered item parameters. 208 | # 209 | # If no block is given, an enumerator is returned instead. 210 | # 211 | # @return [Enumerator] 212 | # 213 | # @api public 214 | # 215 | # @note In discussions with other developers, it was felt that being able to iterate 216 | # over not just the registered keys, but to see what was registered would be 217 | # very helpful. This is a step toward doing that. 218 | def each(&) 219 | config.resolver.each(_container, &) 220 | end 221 | 222 | # Decorates an item from the container with specified decorator 223 | # 224 | # @return [Dry::Core::Container::Mixin] self 225 | # 226 | # @api public 227 | def decorate(key, with: nil, &block) 228 | key = key.to_s 229 | original = _container.delete(key) do 230 | raise KeyError, "Nothing registered with the key #{key.inspect}" 231 | end 232 | 233 | if with.is_a?(Class) 234 | decorator = with.method(:new) 235 | elsif block.nil? && !with.respond_to?(:call) 236 | raise Error, "Decorator needs to be a Class, block, or respond to the `call` method" 237 | else 238 | decorator = with || block 239 | end 240 | 241 | _container[key] = original.map(decorator) 242 | self 243 | end 244 | 245 | # Evaluate block and register items in namespace 246 | # 247 | # @param [Mixed] namespace 248 | # The namespace to register items in 249 | # 250 | # @return [Dry::Core::Container::Mixin] self 251 | # 252 | # @api public 253 | def namespace(namespace, &) 254 | ::Dry::Core::Container::NamespaceDSL.new( 255 | self, 256 | namespace, 257 | config.namespace_separator, 258 | & 259 | ) 260 | 261 | self 262 | end 263 | 264 | # Import a namespace 265 | # 266 | # @param [Dry::Core::Container::Namespace] namespace 267 | # The namespace to import 268 | # 269 | # @return [Dry::Core::Container::Mixin] self 270 | # 271 | # @api public 272 | def import(namespace) 273 | namespace(namespace.name, &namespace.block) 274 | 275 | self 276 | end 277 | 278 | # Freeze the container. Nothing can be registered after freezing 279 | # 280 | # @api public 281 | def freeze 282 | super 283 | _container.freeze 284 | self 285 | end 286 | 287 | # @private no, really 288 | def _container 289 | @_container 290 | end 291 | 292 | # @api public 293 | def dup 294 | copy = super 295 | copy.instance_variable_set(:@_container, _container.dup) 296 | copy 297 | end 298 | 299 | # @api public 300 | def clone 301 | copy = super 302 | unless copy.frozen? 303 | copy.instance_variable_set(:@_container, _container.dup) 304 | end 305 | copy 306 | end 307 | end 308 | # rubocop:enable Metrics/ModuleLength 309 | end 310 | end 311 | end 312 | -------------------------------------------------------------------------------- /lib/dry/core/container/namespace.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dry 4 | module Core 5 | class Container 6 | # Create a namespace to be imported 7 | # 8 | # @example 9 | # 10 | # ns = Dry::Core::Container::Namespace.new('name') do 11 | # register('item', 'item') 12 | # end 13 | # 14 | # container = Dry::Core::Container.new 15 | # 16 | # container.import(ns) 17 | # 18 | # container.resolve('name.item') 19 | # => 'item' 20 | # 21 | # 22 | # @api public 23 | class Namespace 24 | # @return [Mixed] The namespace (name) 25 | attr_reader :name 26 | 27 | # @return [Proc] The block to be executed when the namespace is imported 28 | attr_reader :block 29 | 30 | # Create a new namespace 31 | # 32 | # @param [Mixed] name 33 | # The name of the namespace 34 | # @yield 35 | # The block to evaluate when the namespace is imported 36 | # 37 | # @return [Dry::Core::Container::Namespace] 38 | # 39 | # @api public 40 | def initialize(name, &block) 41 | @name = name 42 | @block = block 43 | end 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/dry/core/container/namespace_dsl.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "delegate" 4 | 5 | module Dry 6 | module Core 7 | class Container 8 | # @api private 9 | class NamespaceDSL < ::SimpleDelegator 10 | # DSL for defining namespaces 11 | # 12 | # @param [Dry::Core::Container::Mixin] container 13 | # The container 14 | # @param [String] namespace 15 | # The namespace (name) 16 | # @param [String] namespace_separator 17 | # The namespace separator 18 | # @yield 19 | # The block to evaluate to define the namespace 20 | # 21 | # @return [Mixed] 22 | # 23 | # @api private 24 | def initialize(container, namespace, namespace_separator, &block) 25 | @namespace = namespace 26 | @namespace_separator = namespace_separator 27 | 28 | super(container) 29 | 30 | if block.arity.zero? 31 | instance_eval(&block) 32 | else 33 | yield self 34 | end 35 | end 36 | 37 | def register(key, ...) 38 | super(namespaced(key), ...) 39 | end 40 | 41 | def namespace(namespace, &) 42 | super(namespaced(namespace), &) 43 | end 44 | 45 | def import(namespace) 46 | namespace(namespace.name, &namespace.block) 47 | 48 | self 49 | end 50 | 51 | def resolve(key) 52 | super(namespaced(key)) 53 | end 54 | 55 | private 56 | 57 | def namespaced(key) 58 | [@namespace, key].join(@namespace_separator) 59 | end 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/dry/core/container/registry.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dry 4 | module Core 5 | class Container 6 | # Default registry for registering items with the container 7 | # 8 | # @api public 9 | class Registry 10 | # @private 11 | def initialize 12 | @_mutex = ::Mutex.new 13 | end 14 | 15 | # Register an item with the container to be resolved later 16 | # 17 | # @param [Concurrent::Hash] container 18 | # The container 19 | # @param [Mixed] key 20 | # The key to register the container item with (used to resolve) 21 | # @param [Mixed] item 22 | # The item to register with the container 23 | # @param [Hash] options 24 | # @option options [Symbol] :call 25 | # Whether the item should be called when resolved 26 | # 27 | # @raise [Dry::Core::Container::KeyError] 28 | # If an item is already registered with the given key 29 | # 30 | # @return [Mixed] 31 | # 32 | # @api public 33 | def call(container, key, item, options) 34 | key = key.to_s.dup.freeze 35 | 36 | @_mutex.synchronize do 37 | if container.key?(key) 38 | raise KeyError, "There is already an item registered with the key #{key.inspect}" 39 | end 40 | 41 | container[key] = factory.call(item, options) 42 | end 43 | end 44 | 45 | # @api private 46 | def factory 47 | @factory ||= Container::Item::Factory.new 48 | end 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/dry/core/container/resolver.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dry 4 | module Core 5 | class Container 6 | # Default resolver for resolving items from container 7 | # 8 | # @api public 9 | class Resolver 10 | # Resolve an item from the container 11 | # 12 | # @param [Concurrent::Hash] container 13 | # The container 14 | # @param [Mixed] key 15 | # The key for the item you wish to resolve 16 | # @yield 17 | # Fallback block to call when a key is missing. Its result will be returned 18 | # @yieldparam [Mixed] key Missing key 19 | # 20 | # @raise [KeyError] 21 | # If the given key is not registered with the container (and no block provided) 22 | # 23 | # 24 | # @return [Mixed] 25 | # 26 | # @api public 27 | def call(container, key) 28 | item = container.fetch(key.to_s) do 29 | if block_given? 30 | return yield(key) 31 | else 32 | raise KeyError.new(%(key not found: "#{key}"), key: key.to_s, receiver: container) 33 | end 34 | end 35 | 36 | item.call 37 | end 38 | 39 | # Check whether an items is registered under the given key 40 | # 41 | # @param [Concurrent::Hash] container 42 | # The container 43 | # @param [Mixed] key 44 | # The key you wish to check for registration with 45 | # 46 | # @return [Bool] 47 | # 48 | # @api public 49 | def key?(container, key) 50 | container.key?(key.to_s) 51 | end 52 | 53 | # An array of registered names for the container 54 | # 55 | # @return [Array] 56 | # 57 | # @api public 58 | def keys(container) 59 | container.keys 60 | end 61 | 62 | # Calls block once for each key in container, passing the key as a parameter. 63 | # 64 | # If no block is given, an enumerator is returned instead. 65 | # 66 | # @return Hash 67 | # 68 | # @api public 69 | def each_key(container, &) 70 | container.each_key(&) 71 | end 72 | 73 | # Calls block once for each key in container, passing the key and 74 | # the registered item parameters. 75 | # 76 | # If no block is given, an enumerator is returned instead. 77 | # 78 | # @return Key, Value 79 | # 80 | # @api public 81 | # @note In discussions with other developers, it was felt that being able 82 | # to iterate over not just the registered keys, but to see what was 83 | # registered would be very helpful. This is a step toward doing that. 84 | def each(container, &) 85 | container.map { |key, value| [key, value.call] }.each(&) 86 | end 87 | end 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /lib/dry/core/container/stub.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dry 4 | module Core 5 | class Container 6 | module Stub 7 | # Overrides resolve to look into stubbed keys first 8 | # 9 | # @api public 10 | def resolve(key) 11 | _stubs.fetch(key.to_s) { super } 12 | end 13 | 14 | # Add a stub to the container 15 | def stub(key, value, &block) 16 | unless key?(key) 17 | raise ::ArgumentError, "cannot stub #{key.to_s.inspect} - no such key in container" 18 | end 19 | 20 | _stubs[key.to_s] = value 21 | 22 | if block 23 | yield 24 | unstub(key) 25 | end 26 | 27 | self 28 | end 29 | 30 | # Remove stubbed keys from the container 31 | def unstub(*keys) 32 | keys = _stubs.keys if keys.empty? 33 | keys.each { |key| _stubs.delete(key.to_s) } 34 | end 35 | 36 | # Stubs have already been enabled turning this into a noop 37 | def enable_stubs! 38 | # DO NOTHING 39 | end 40 | 41 | private 42 | 43 | # Stubs container 44 | def _stubs 45 | @_stubs ||= {} 46 | end 47 | end 48 | 49 | module Mixin 50 | # Enable stubbing functionality into the current container 51 | def enable_stubs! 52 | extend ::Dry::Core::Container::Stub 53 | end 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/dry/core/deprecations.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "logger" 4 | 5 | module Dry 6 | module Core 7 | # An extension for issuing warnings on using deprecated methods. 8 | # 9 | # @example 10 | # 11 | # class Foo 12 | # def self.old_class_api; end 13 | # def self.new_class_api; end 14 | # 15 | # deprecate_class_method :old_class_api, :new_class_api 16 | # 17 | # def old_api; end 18 | # def new_api; end 19 | # 20 | # deprecate :old_api, :new_api, message: "old_api is no-no" 21 | # end 22 | # 23 | # @example You also can use this module for your custom messages 24 | # 25 | # Dry::Core::Deprecations.announce("Foo", "use bar instead") 26 | # Dry::Core::Deprecations.warn("Baz is going to be removed soon") 27 | # 28 | # @api public 29 | module Deprecations 30 | STACK = -> { caller.find { |l| l !~ %r{(lib/dry/core)|(gems)} } } 31 | 32 | class << self 33 | # Prints a warning 34 | # 35 | # @param [String] msg Warning string 36 | # @param [String] tag Tag to help identify the source of the warning. 37 | # Defaults to "deprecated" 38 | # @param [Integer] Caller frame to add to the message 39 | def warn(msg, tag: nil, uplevel: nil) 40 | caller_info = uplevel.nil? ? nil : "#{caller_locations(uplevel + 2, 1)[0]} " 41 | tag = "[#{tag || "deprecated"}] " 42 | hint = msg.gsub(/^\s+/, "") 43 | 44 | logger.warn("#{caller_info}#{tag}#{hint}") 45 | end 46 | 47 | # Wraps arguments with a standard message format and prints a warning 48 | # 49 | # @param [Object] name what is deprecated 50 | # @param [String] msg additional message usually containing upgrade instructions 51 | def announce(name, msg, tag: nil, uplevel: nil) 52 | # Bump the uplevel (if provided) by one to account for the uplevel calculation 53 | # taking place one frame deeper in `.warn` 54 | uplevel += 1 if uplevel 55 | 56 | warn(deprecation_message(name, msg), tag: tag, uplevel: uplevel) 57 | end 58 | 59 | # @api private 60 | def deprecation_message(name, msg) 61 | <<-MSG 62 | #{name} is deprecated and will be removed in the next major version 63 | #{msg} 64 | MSG 65 | end 66 | 67 | # @api private 68 | def deprecated_name_message(old, new = nil, msg = nil) 69 | if new 70 | deprecation_message(old, <<-MSG) 71 | Please use #{new} instead. 72 | #{msg} 73 | MSG 74 | else 75 | deprecation_message(old, msg) 76 | end 77 | end 78 | 79 | # Returns the logger used for printing warnings. 80 | # You can provide your own with .set_logger! 81 | # 82 | # @param [IO] output output stream 83 | # 84 | # @return [Logger] 85 | def logger(output = $stderr) 86 | if defined?(@logger) 87 | @logger 88 | else 89 | set_logger!(output) 90 | end 91 | end 92 | 93 | # Sets a custom logger. This is a global setting. 94 | # 95 | # @overload set_logger!(output) 96 | # @param [IO] output Stream for messages 97 | # 98 | # @overload set_logger! 99 | # Stream messages to stdout 100 | # 101 | # @overload set_logger!(logger) 102 | # @param [#warn] logger 103 | # 104 | # @api public 105 | def set_logger!(output = $stderr) 106 | if output.respond_to?(:warn) 107 | @logger = output 108 | else 109 | @logger = ::Logger.new(output).tap do |logger| 110 | logger.formatter = proc { |_, _, _, msg| "#{msg}\n" } 111 | end 112 | end 113 | end 114 | 115 | def [](tag) 116 | Tagged.new(tag) 117 | end 118 | end 119 | 120 | # @api private 121 | class Tagged < ::Module 122 | def initialize(tag) 123 | super() 124 | @tag = tag 125 | end 126 | 127 | def extended(base) 128 | base.extend Interface 129 | base.deprecation_tag @tag 130 | end 131 | end 132 | 133 | module Interface 134 | # Sets/gets deprecation tag 135 | # 136 | # @option [String,Symbol] tag tag 137 | def deprecation_tag(tag = nil) 138 | if defined?(@deprecation_tag) 139 | @deprecation_tag 140 | else 141 | @deprecation_tag = tag 142 | end 143 | end 144 | 145 | # Issue a tagged warning message 146 | # 147 | # @param [String] msg warning message 148 | def warn(msg) 149 | Deprecations.warn(msg, tag: deprecation_tag) 150 | end 151 | 152 | # Mark instance method as deprecated 153 | # 154 | # @param [Symbol] old_name deprecated method 155 | # @param [Symbol] new_name replacement (not required) 156 | # @option [String] message optional deprecation message 157 | def deprecate(old_name, new_name = nil, message: nil) 158 | full_msg = Deprecations.deprecated_name_message( 159 | "#{name}##{old_name}", 160 | new_name ? "#{name}##{new_name}" : nil, 161 | message 162 | ) 163 | mod = self 164 | 165 | if new_name 166 | undef_method old_name if method_defined?(old_name) 167 | 168 | define_method(old_name) do |*args, &block| 169 | mod.warn("#{full_msg}\n#{STACK.()}") 170 | __send__(new_name, *args, &block) 171 | end 172 | else 173 | aliased_name = :"#{old_name}_without_deprecation" 174 | alias_method aliased_name, old_name 175 | private aliased_name 176 | undef_method old_name 177 | 178 | define_method(old_name) do |*args, &block| 179 | mod.warn("#{full_msg}\n#{STACK.()}") 180 | __send__(aliased_name, *args, &block) 181 | end 182 | end 183 | end 184 | 185 | # Mark class-level method as deprecated 186 | # 187 | # @param [Symbol] old_name deprecated method 188 | # @param [Symbol] new_name replacement (not required) 189 | # @option [String] message optional deprecation message 190 | def deprecate_class_method(old_name, new_name = nil, message: nil) 191 | full_msg = Deprecations.deprecated_name_message( 192 | "#{name}.#{old_name}", 193 | new_name ? "#{name}.#{new_name}" : nil, 194 | message 195 | ) 196 | 197 | meth = method(new_name || old_name) 198 | 199 | singleton_class.instance_exec do 200 | undef_method old_name if method_defined?(old_name) 201 | 202 | define_method(old_name) do |*args, &block| 203 | warn("#{full_msg}\n#{STACK.()}") 204 | meth.call(*args, &block) 205 | end 206 | end 207 | end 208 | 209 | # Mark a constant as deprecated 210 | # @param [Symbol] constant_name constant name to be deprecated 211 | # @option [String] message optional deprecation message 212 | def deprecate_constant(constant_name, message: nil) 213 | value = const_get(constant_name) 214 | remove_const(constant_name) 215 | 216 | full_msg = Deprecations.deprecated_name_message( 217 | "#{name}::#{constant_name}", 218 | message 219 | ) 220 | 221 | mod = ::Module.new do 222 | define_method(:const_missing) do |missing| 223 | if missing == constant_name 224 | warn("#{full_msg}\n#{STACK.()}") 225 | value 226 | else 227 | super(missing) 228 | end 229 | end 230 | end 231 | 232 | extend(mod) 233 | end 234 | end 235 | end 236 | end 237 | end 238 | -------------------------------------------------------------------------------- /lib/dry/core/descendants_tracker.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "concurrent/array" 4 | 5 | module Dry 6 | module Core 7 | # An implementation of descendants tracker, heavily inspired 8 | # by the descendants_tracker gem. 9 | # 10 | # @example 11 | # 12 | # class Base 13 | # extend Dry::Core::DescendantsTracker 14 | # end 15 | # 16 | # class A < Base 17 | # end 18 | # 19 | # class B < Base 20 | # end 21 | # 22 | # class C < A 23 | # end 24 | # 25 | # Base.descendants # => [C, B, A] 26 | # A.descendants # => [C] 27 | # B.descendants # => [] 28 | # 29 | module DescendantsTracker 30 | class << self 31 | # @api private 32 | def setup(target) 33 | target.instance_variable_set(:@descendants, ::Concurrent::Array.new) 34 | end 35 | 36 | private 37 | 38 | # @api private 39 | def extended(base) 40 | super 41 | 42 | DescendantsTracker.setup(base) 43 | end 44 | end 45 | 46 | # Return the descendants of this class 47 | # 48 | # @example 49 | # descendants = Parent.descendants 50 | # 51 | # @return [Array] 52 | # 53 | # @api public 54 | attr_reader :descendants 55 | 56 | protected 57 | 58 | # @api private 59 | def add_descendant(descendant) 60 | ancestor = superclass 61 | if ancestor.respond_to?(:add_descendant, true) 62 | ancestor.add_descendant(descendant) 63 | end 64 | descendants.unshift(descendant) 65 | end 66 | 67 | private 68 | 69 | # @api private 70 | def inherited(descendant) 71 | super 72 | 73 | DescendantsTracker.setup(descendant) 74 | add_descendant(descendant) 75 | end 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/dry/core/equalizer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dry 4 | module Core 5 | # Define equality, equivalence and inspection methods 6 | class Equalizer < ::Module 7 | # Initialize an Equalizer with the given keys 8 | # 9 | # Will use the keys with which it is initialized to define #cmp?, 10 | # #hash, and #inspect 11 | # 12 | # @param [Array] keys 13 | # @param [Hash] options 14 | # @option options [Boolean] :inspect whether to define #inspect method 15 | # @option options [Boolean] :immutable whether to memoize #hash method 16 | # 17 | # @return [undefined] 18 | # 19 | # @api private 20 | def initialize(*keys, **options) 21 | super() 22 | @keys = keys.uniq 23 | define_methods(**options) 24 | freeze 25 | end 26 | 27 | private 28 | 29 | # Hook called when module is included 30 | # 31 | # @param [Module] descendant 32 | # the module or class including Equalizer 33 | # 34 | # @return [self] 35 | # 36 | # @api private 37 | def included(descendant) 38 | super 39 | descendant.include Methods 40 | end 41 | 42 | # Define the equalizer methods based on #keys 43 | # 44 | # @param [Boolean] inspect whether to define #inspect method 45 | # @param [Boolean] immutable whether to memoize #hash method 46 | # 47 | # @return [undefined] 48 | # 49 | # @api private 50 | def define_methods(inspect: true, immutable: false) 51 | define_cmp_method 52 | define_hash_method(immutable: immutable) 53 | define_inspect_method if inspect 54 | end 55 | 56 | # Define an #cmp? method based on the instance's values identified by #keys 57 | # 58 | # @return [undefined] 59 | # 60 | # @api private 61 | def define_cmp_method 62 | keys = @keys 63 | define_method(:cmp?) do |comparator, other| 64 | keys.all? do |key| 65 | __send__(key).public_send(comparator, other.__send__(key)) 66 | end 67 | end 68 | private :cmp? 69 | end 70 | 71 | # Define a #hash method based on the instance's values identified by #keys 72 | # 73 | # @return [undefined] 74 | # 75 | # @api private 76 | def define_hash_method(immutable:) 77 | calculate_hash = ->(obj) { @keys.map { |key| obj.__send__(key) }.push(obj.class).hash } 78 | if immutable 79 | define_method(:hash) do 80 | @__hash__ ||= calculate_hash.call(self) 81 | end 82 | define_method(:freeze) do 83 | hash 84 | super() 85 | end 86 | else 87 | define_method(:hash) do 88 | calculate_hash.call(self) 89 | end 90 | end 91 | end 92 | 93 | # Define an inspect method that reports the values of the instance's keys 94 | # 95 | # @return [undefined] 96 | # 97 | # @api private 98 | def define_inspect_method 99 | keys = @keys 100 | define_method(:inspect) do 101 | klass = self.class 102 | name = klass.name || klass.inspect 103 | "#<#{name}#{keys.map { |key| " #{key}=#{__send__(key).inspect}" }.join}>" 104 | end 105 | end 106 | 107 | # The comparison methods 108 | module Methods 109 | # Compare the object with other object for equality 110 | # 111 | # @example 112 | # object.eql?(other) # => true or false 113 | # 114 | # @param [Object] other 115 | # the other object to compare with 116 | # 117 | # @return [Boolean] 118 | # 119 | # @api public 120 | def eql?(other) 121 | instance_of?(other.class) && cmp?(__method__, other) 122 | end 123 | 124 | # Compare the object with other object for equivalency 125 | # 126 | # @example 127 | # object == other # => true or false 128 | # 129 | # @param [Object] other 130 | # the other object to compare with 131 | # 132 | # @return [Boolean] 133 | # 134 | # @api public 135 | def ==(other) 136 | other.is_a?(self.class) && cmp?(__method__, other) 137 | end 138 | end 139 | end 140 | end 141 | 142 | # Old modules that depend on dry/core/equalizer may miss 143 | # this method if dry/core is not required explicitly 144 | unless singleton_class.method_defined?(:Equalizer) 145 | # Build an equalizer module for the inclusion in other class 146 | # 147 | # ## Credits 148 | # 149 | # Equalizer has been originally imported from the equalizer gem created by Dan Kubb 150 | # 151 | # @api public 152 | def self.Equalizer(*keys, **options) 153 | ::Dry::Core::Equalizer.new(*keys, **options) 154 | end 155 | end 156 | end 157 | -------------------------------------------------------------------------------- /lib/dry/core/errors.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dry 4 | module Core 5 | class InvalidClassAttributeValueError < ::StandardError 6 | def initialize(name, value) 7 | super( 8 | "Value #{value.inspect} is invalid for class attribute #{name.inspect}" 9 | ) 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/dry/core/extensions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "set" 4 | 5 | module Dry 6 | module Core 7 | # Define extensions that can be later enabled by the user. 8 | # 9 | # @example 10 | # 11 | # class Foo 12 | # extend Dry::Core::Extensions 13 | # 14 | # register_extension(:bar) do 15 | # def bar; :bar end 16 | # end 17 | # end 18 | # 19 | # Foo.new.bar # => NoMethodError 20 | # Foo.load_extensions(:bar) 21 | # Foo.new.bar # => :bar 22 | # 23 | module Extensions 24 | # @api private 25 | def self.extended(obj) 26 | super 27 | obj.instance_variable_set(:@__available_extensions__, {}) 28 | obj.instance_variable_set(:@__loaded_extensions__, ::Set.new) 29 | end 30 | 31 | # Register an extension 32 | # 33 | # @param [Symbol] name extension name 34 | # @yield extension block. This block guaranteed not to be called more than once 35 | def register_extension(name, &block) 36 | @__available_extensions__[name] = block 37 | end 38 | 39 | # Whether an extension is available 40 | # 41 | # @param [Symbol] name extension name 42 | # @return [Boolean] Extension availability 43 | def available_extension?(name) 44 | @__available_extensions__.key?(name) 45 | end 46 | 47 | # Enables specified extensions. Already enabled extensions remain untouched 48 | # 49 | # @param [Array] extensions list of extension names 50 | def load_extensions(*extensions) 51 | extensions.each do |ext| 52 | block = @__available_extensions__.fetch(ext) do 53 | raise ::ArgumentError, "Unknown extension: #{ext.inspect}" 54 | end 55 | unless @__loaded_extensions__.include?(ext) 56 | block.call 57 | @__loaded_extensions__ << ext 58 | end 59 | end 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/dry/core/inflector.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dry 4 | module Core 5 | # Helper module providing thin interface around an inflection backend. 6 | module Inflector 7 | # List of supported backends 8 | BACKENDS = { 9 | activesupport: [ 10 | "active_support/inflector", 11 | proc { ::ActiveSupport::Inflector } 12 | ], 13 | dry_inflector: [ 14 | "dry/inflector", 15 | proc { ::Dry::Inflector.new } 16 | ], 17 | inflecto: [ 18 | "inflecto", 19 | proc { ::Inflecto } 20 | ] 21 | }.freeze 22 | 23 | # Try to activate a backend 24 | # 25 | # @api private 26 | def self.realize_backend(path, backend_factory) 27 | require path 28 | rescue ::LoadError 29 | nil 30 | else 31 | backend_factory.call 32 | end 33 | 34 | # Set up first available backend 35 | # 36 | # @api private 37 | def self.detect_backend 38 | BACKENDS.inject(nil) do |backend, (_, (path, factory))| 39 | backend || realize_backend(path, factory) 40 | end || raise( 41 | LoadError, 42 | "No inflector library could be found: " \ 43 | "please install either the `inflecto` or `activesupport` gem." 44 | ) 45 | end 46 | 47 | # Set preferred backend 48 | # 49 | # @param [Symbol] name backend name (:activesupport or :inflecto) 50 | def self.select_backend(name = nil) 51 | if name && !BACKENDS.key?(name) 52 | raise ::NameError, "Invalid inflector library selection: '#{name}'" 53 | end 54 | 55 | @inflector = name ? realize_backend(*BACKENDS[name]) : detect_backend 56 | end 57 | 58 | # Inflector accessor. Lazily initializes a backend 59 | # 60 | # @api private 61 | def self.inflector 62 | defined?(@inflector) ? @inflector : select_backend 63 | end 64 | 65 | # Transform string to camel case 66 | # 67 | # @example 68 | # Dry::Core::Inflector.camelize('foo_bar') # => 'FooBar' 69 | # 70 | # @param [String] input input string 71 | # @return Transformed string 72 | def self.camelize(input) 73 | inflector.camelize(input) 74 | end 75 | 76 | # Transform string to snake case 77 | # 78 | # @example 79 | # Dry::Core::Inflector.underscore('FooBar') # => 'foo_bar' 80 | # 81 | # @param [String] input input string 82 | # @return Transformed string 83 | def self.underscore(input) 84 | inflector.underscore(input) 85 | end 86 | 87 | # Get a singlular form of a word 88 | # 89 | # @example 90 | # Dry::Core::Inflector.singularize('chars') # => 'char' 91 | # 92 | # @param [String] input input string 93 | # @return Transformed string 94 | def self.singularize(input) 95 | inflector.singularize(input) 96 | end 97 | 98 | # Get a plural form of a word 99 | # 100 | # @example 101 | # Dry::Core::Inflector.pluralize('string') # => 'strings' 102 | # 103 | # @param [String] input input string 104 | # @return Transformed string 105 | def self.pluralize(input) 106 | inflector.pluralize(input) 107 | end 108 | 109 | # Remove namespaces from a constant name 110 | # 111 | # @example 112 | # Dry::Core::Inflector.demodulize('Deeply::Nested::Name') # => 'Name' 113 | # 114 | # @param [String] input input string 115 | # @return Unnested constant name 116 | def self.demodulize(input) 117 | inflector.demodulize(input) 118 | end 119 | 120 | # Get a constant value by its name 121 | # 122 | # @example 123 | # Dry::Core::Inflector.constantize('Foo::Bar') # => Foo::Bar 124 | # 125 | # @param [String] input input constant name 126 | # @return Constant value 127 | def self.constantize(input) 128 | inflector.constantize(input) 129 | end 130 | 131 | # Transform a file path to a constant name 132 | # 133 | # @example 134 | # Dry::Core::Inflector.classify('foo/bar') # => 'Foo::Bar' 135 | # 136 | # @param [String] input input string 137 | # @return Constant name 138 | def self.classify(input) 139 | inflector.classify(input) 140 | end 141 | end 142 | end 143 | end 144 | -------------------------------------------------------------------------------- /lib/dry/core/memoizable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dry 4 | module Core 5 | module Memoizable 6 | MEMOIZED_HASH = {}.freeze 7 | PARAM_PLACEHOLDERS = %i[* ** &].freeze 8 | 9 | module ClassInterface 10 | module Base 11 | def memoize(*names) 12 | prepend(Memoizer.new(self, names)) 13 | end 14 | 15 | def inherited(base) 16 | super 17 | 18 | memoizer = base.ancestors.find { _1.is_a?(Memoizer) } 19 | base.prepend(memoizer.dup) if memoizer 20 | end 21 | end 22 | 23 | module BasicObject 24 | include Base 25 | 26 | def new(*, **) 27 | obj = super 28 | obj.instance_eval { @__memoized__ = MEMOIZED_HASH.dup } 29 | obj 30 | end 31 | end 32 | 33 | module Object 34 | include Base 35 | 36 | def new(*, **) 37 | obj = super 38 | obj.instance_variable_set(:@__memoized__, MEMOIZED_HASH.dup) 39 | obj 40 | end 41 | end 42 | end 43 | 44 | def self.included(klass) 45 | super 46 | 47 | if klass <= Object 48 | klass.extend(ClassInterface::Object) 49 | else 50 | klass.extend(ClassInterface::BasicObject) 51 | end 52 | end 53 | 54 | # @api private 55 | class Memoizer < ::Module 56 | KERNEL = { 57 | singleton: ::Kernel.instance_method(:singleton_class), 58 | ivar_set: ::Kernel.instance_method(:instance_variable_set), 59 | frozen: ::Kernel.instance_method(:frozen?) 60 | }.freeze 61 | 62 | # @api private 63 | def initialize(klass, names) 64 | super() 65 | names.each do |name| 66 | define_memoizable( 67 | method: klass.instance_method(name) 68 | ) 69 | end 70 | end 71 | 72 | private 73 | 74 | # @api private 75 | # rubocop:disable Metrics/AbcSize 76 | def define_memoizable(method:) 77 | parameters = method.parameters 78 | mod = self 79 | kernel = KERNEL 80 | 81 | if parameters.empty? 82 | key = "#{__id__}:#{method.name}".hash.abs 83 | 84 | define_method(method.name) do 85 | value = super() 86 | 87 | if kernel[:frozen].bind_call(self) 88 | # It's not possible to modify singleton classes 89 | # of frozen objects 90 | mod.remove_method(method.name) 91 | mod.module_eval(<<~RUBY, __FILE__, __LINE__ + 1) 92 | def #{method.name} # def slow_calc 93 | cached = @__memoized__[#{key}] # cached = @__memoized__[12345678] 94 | # 95 | if cached || @__memoized__.key?(#{key}) # if cached || @__memoized__.key?(12345678) 96 | cached # cached 97 | else # else 98 | @__memoized__[#{key}] = super # @__memoized__[12345678] = super 99 | end # end 100 | end # end 101 | RUBY 102 | else 103 | # We make an attr_reader for computed value. 104 | # Readers are "special-cased" in ruby so such 105 | # access will be the fastest way, faster than you'd 106 | # expect :) 107 | attr_name = :"__memozed_#{key}__" 108 | ivar_name = :"@#{attr_name}" 109 | kernel[:ivar_set].bind_call(self, ivar_name, value) 110 | eigenclass = kernel[:singleton].bind_call(self) 111 | eigenclass.attr_reader(attr_name) 112 | eigenclass.alias_method(method.name, attr_name) 113 | eigenclass.remove_method(attr_name) 114 | end 115 | 116 | value 117 | end 118 | else 119 | mapping = parameters.to_h { |k, v = nil| [k, v] } 120 | params, binds = declaration(parameters, mapping) 121 | last_param = parameters.last 122 | 123 | if last_param[0].eql?(:block) && !last_param[1].eql?(:&) 124 | Deprecations.warn(<<~WARN) 125 | Memoization for block-accepting methods isn't safe. 126 | Every call creates a new block instance bloating cached results. 127 | In the future, blocks will still be allowed but won't participate in 128 | cache key calculation. 129 | WARN 130 | end 131 | 132 | module_eval(<<~RUBY, __FILE__, __LINE__ + 1) 133 | def #{method.name}(#{params.join(", ")}) # def slow_calc(arg1, arg2, arg3) 134 | key = [:"#{method.name}", #{binds.join(", ")}].hash # key = [:slow_calc, arg1, arg2, arg3].hash 135 | # 136 | if @__memoized__.key?(key) # if @__memoized__.key?(key) 137 | @__memoized__[key] # @__memoized__[key] 138 | else # else 139 | @__memoized__[key] = super # @__memoized__[key] = super 140 | end # end 141 | end # end 142 | RUBY 143 | 144 | end 145 | end 146 | 147 | # rubocop:enable Metrics/AbcSize 148 | # @api private 149 | def declaration(definition, lookup) 150 | params = [] 151 | binds = [] 152 | defined = {} 153 | 154 | definition.each do |type, name| 155 | mapped_type = map_bind_type(type, name, lookup, defined) do 156 | raise ::NotImplementedError, "type: #{type}, name: #{name}" 157 | end 158 | 159 | if mapped_type 160 | defined[mapped_type] = true 161 | bind = name_from_param(name) || make_bind_name(binds.size) 162 | 163 | binds << bind 164 | params << param(bind, mapped_type) 165 | end 166 | end 167 | 168 | [params, binds] 169 | end 170 | 171 | # @api private 172 | def name_from_param(name) 173 | if PARAM_PLACEHOLDERS.include?(name) 174 | nil 175 | else 176 | name 177 | end 178 | end 179 | 180 | # @api private 181 | def make_bind_name(idx) 182 | :"__lv_#{idx}__" 183 | end 184 | 185 | # @api private 186 | def map_bind_type(type, name, original_params, defined_types) # rubocop:disable Metrics/PerceivedComplexity 187 | case type 188 | when :req 189 | :reqular 190 | when :rest, :keyreq, :keyrest 191 | type 192 | when :block 193 | if name.eql?(:&) 194 | # most likely this is a case of delegation 195 | # rather than actual block 196 | nil 197 | else 198 | type 199 | end 200 | when :opt 201 | if original_params.key?(:rest) || defined_types[:rest] 202 | nil 203 | else 204 | :rest 205 | end 206 | when :key 207 | if original_params.key?(:keyrest) || defined_types[:keyrest] 208 | nil 209 | else 210 | :keyrest 211 | end 212 | else 213 | yield 214 | end 215 | end 216 | 217 | # @api private 218 | def param(name, type) 219 | case type 220 | when :reqular 221 | name 222 | when :rest 223 | "*#{name}" 224 | when :keyreq 225 | "#{name}:" 226 | when :keyrest 227 | "**#{name}" 228 | when :block 229 | "&#{name}" 230 | end 231 | end 232 | end 233 | end 234 | end 235 | end 236 | -------------------------------------------------------------------------------- /lib/dry/core/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dry 4 | module Core 5 | VERSION = "1.1.0" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /project.yml: -------------------------------------------------------------------------------- 1 | name: dry-core 2 | codacy_id: 40946292b9094624beec604a149a6023 3 | gemspec: 4 | authors: ["Nikita Shilnikov"] 5 | email: ["fg@flashgordon.ru"] 6 | summary: "A toolset of small support modules used throughout the dry-rb ecosystem" 7 | runtime_dependencies: 8 | - [concurrent-ruby, "~> 1.0"] 9 | - [logger] 10 | - [zeitwerk, "~> 2.6"] 11 | -------------------------------------------------------------------------------- /spec/dry/container_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Dry::Core::Container do 4 | let(:klass) { Dry::Core::Container } 5 | let(:container) { klass.new } 6 | 7 | it_behaves_like "a container" 8 | 9 | describe "inheritance" do 10 | it "sets up a container for a child class" do 11 | parent = Class.new { extend Dry::Core::Container::Mixin } 12 | child = Class.new(parent) 13 | 14 | parent.register(:foo, Object.new) 15 | child.register(:foo, Object.new) 16 | 17 | expect(parent[:foo]).to_not be(child[:foo]) 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/dry/core/basic_object_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ExternalTestClass 4 | end 5 | 6 | class TestClass < Dry::Core::BasicObject 7 | class InternalTestClass 8 | end 9 | 10 | def internal 11 | InternalTestClass 12 | end 13 | 14 | def external 15 | ExternalTestClass 16 | end 17 | end 18 | 19 | class InspectTestClass < TestClass 20 | def __inspect 21 | " hello" 22 | end 23 | end 24 | 25 | RSpec.describe Dry::Core::BasicObject do 26 | describe ".const_missing" do 27 | subject { TestClass.new } 28 | 29 | it "lookups constants at the top-level namespace" do 30 | expect(subject.internal).to eq(TestClass::InternalTestClass) 31 | expect(subject.external).to eq(ExternalTestClass) 32 | end 33 | end 34 | 35 | describe "#respond_to_missing?" do 36 | it "raises an exception if respond_to? method is not implemented" do 37 | expect { TestClass.new.respond_to?(:no_existing_method) } 38 | .to raise_error(NotImplementedError) 39 | end 40 | 41 | it "returns true given respond_to? method was implemented" do 42 | TestCase = Class.new(TestClass) do 43 | def respond_to?(_method_name, _include_all = false) # rubocop:disable Style/OptionalBooleanParameter 44 | true 45 | end 46 | end 47 | 48 | expect(TestCase.new).to respond_to(:no_existing_method) 49 | end 50 | end 51 | 52 | describe "#class" do 53 | it "returns TestClass" do 54 | expect(TestClass.new.class).to eq TestClass 55 | end 56 | end 57 | 58 | describe "#inspect" do 59 | it "returns object inspection" do 60 | actual = TestClass.new.inspect 61 | expect(actual).to match(/\A#\z/) 62 | end 63 | 64 | it "returns custom object inspection" do 65 | actual = InspectTestClass.new.inspect 66 | expect(actual).to match(/\A#\z/) 67 | end 68 | end 69 | 70 | describe "#pretty_print" do 71 | # See https://github.com/hanami/hanami/issues/629 72 | it "is pretty printable" do 73 | expect { pp TestClass.new }.to output(/TestClass/).to_stdout 74 | end 75 | 76 | # See https://github.com/hanami/utils/issues/234 77 | it "outputs the inspection to the given printer" do 78 | require "pp" # rubucop:disable Lint/RedundantRequireStatement 79 | printer = PP.new 80 | subject = TestClass.new 81 | subject.pretty_print(printer) 82 | 83 | expect(printer.output).to match(/\A#\z/) 84 | end 85 | end 86 | 87 | describe "#instance_of?" do 88 | subject { TestClass.new } 89 | 90 | context "when object is instance of the given class" do 91 | it "returns true" do 92 | expect(subject.instance_of?(TestClass)).to be(true) 93 | end 94 | end 95 | 96 | context "when object is not instance of the given class" do 97 | it "returns false" do 98 | expect(subject.instance_of?(String)).to be(false) 99 | end 100 | end 101 | 102 | context "when given argument is not a class or module" do 103 | it "raises error" do 104 | expect { subject.instance_of?("foo") }.to raise_error(TypeError) 105 | end 106 | end 107 | end 108 | 109 | describe "#is_a?" do 110 | subject { TestClass.new } 111 | let(:test_class) { TestClass } 112 | 113 | context "when object is instance of the given class" do 114 | it "returns true" do 115 | expect(subject.is_a?(TestClass)).to be(true) 116 | end 117 | end 118 | 119 | context "when object is not instance of the given class" do 120 | it "returns false" do 121 | expect(subject.is_a?(String)).to be(false) 122 | end 123 | end 124 | 125 | context "when given argument is not a class or module" do 126 | it "raises error" do 127 | expect { subject.is_a?("foo") }.to raise_error(TypeError) 128 | end 129 | end 130 | 131 | context "when object is instance of the subclass" do 132 | subject { Class.new(TestClass).new } 133 | 134 | it "returns true" do 135 | expect(subject.is_a?(TestClass)).to be(true) 136 | end 137 | end 138 | 139 | context "when object has given module included" do 140 | subject do 141 | m = mod 142 | Class.new { include m }.new 143 | end 144 | 145 | let(:mod) { Module.new } 146 | 147 | it "returns true" do 148 | expect(subject.is_a?(mod)).to be(true) 149 | end 150 | end 151 | end 152 | 153 | # rubocop:disable Style/ClassCheck 154 | describe "#kind_of?" do 155 | context "when object is instance of the given class" do 156 | subject { TestClass.new } 157 | 158 | it "returns true" do 159 | expect(subject.kind_of?(TestClass)).to be(true) 160 | end 161 | end 162 | 163 | context "when object is instance of the subclass" do 164 | subject { Class.new(TestClass).new } 165 | 166 | it "returns true" do 167 | expect(subject.kind_of?(TestClass)).to be(true) 168 | end 169 | end 170 | 171 | context "when object is not instance of the given class" do 172 | it "returns false" do 173 | expect(subject.kind_of?(String)).to be(false) 174 | end 175 | end 176 | 177 | context "when given argument is not a class or module" do 178 | it "raises error" do 179 | expect { subject.kind_of?("foo") }.to raise_error(TypeError) 180 | end 181 | end 182 | 183 | context "when object has given module included" do 184 | subject do 185 | m = mod 186 | Class.new { include m }.new 187 | end 188 | 189 | let(:mod) { Module.new } 190 | 191 | it "returns true" do 192 | expect(subject.kind_of?(mod)).to be(true) 193 | end 194 | end 195 | end 196 | # rubocop:enable Style/ClassCheck 197 | end 198 | -------------------------------------------------------------------------------- /spec/dry/core/cache_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Dry::Core::Cache do 4 | shared_examples_for "class with cache" do 5 | describe "#fetch_or_store" do 6 | it "stores and fetches a value" do 7 | args = [1, 2, 3] 8 | value = "foo" 9 | 10 | expect(klass.fetch_or_store(*args) { value }).to be(value) 11 | expect(klass.fetch_or_store(*args)).to be(value) 12 | 13 | object = klass.new 14 | 15 | expect(object.fetch_or_store(*args) { value }).to be(value) 16 | expect(object.fetch_or_store(*args)).to be(value) 17 | end 18 | end 19 | end 20 | 21 | let(:base_class) do 22 | Class.new do 23 | extend Dry::Core::Cache 24 | end 25 | end 26 | 27 | let(:child_class) do 28 | Class.new(base_class) 29 | end 30 | 31 | it_behaves_like "class with cache" do 32 | let(:klass) { base_class } 33 | end 34 | 35 | context "inheritance" do 36 | it_behaves_like "class with cache" do 37 | let(:klass) { child_class } 38 | end 39 | 40 | it "uses the same values in child and parent" do 41 | value = Object.new 42 | expect(base_class.fetch_or_store(1, 2) { value }).to be(value) 43 | expect(base_class.fetch_or_store(1, 2) { raise }).to be(value) 44 | 45 | expect(child_class.fetch_or_store(1, 2) { raise }).to be(value) 46 | expect(child_class.new.fetch_or_store(1, 2) { raise }).to be(value) 47 | end 48 | 49 | it "does not depend on fetch order" do 50 | value = Object.new 51 | expect(child_class.fetch_or_store(1, 2) { value }).to be(value) 52 | expect(child_class.fetch_or_store(1, 2) { raise }).to be(value) 53 | 54 | expect(base_class.fetch_or_store(1, 2) { raise }).to be(value) 55 | expect(base_class.new.fetch_or_store(1, 2) { raise }).to be(value) 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /spec/dry/core/class_attributes_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry-types" 4 | 5 | RSpec.describe "Class Macros" do 6 | before do 7 | module Test 8 | class MyClass 9 | extend Dry::Core::ClassAttributes 10 | 11 | defines :one, :two, :three 12 | 13 | one 1 14 | two 2 15 | three 3 16 | end 17 | 18 | class OtherClass < Test::MyClass 19 | two "two" 20 | three nil 21 | end 22 | end 23 | end 24 | 25 | it "defines accessor like methods on the class and subclasses" do 26 | %i[one two three].each do |method_name| 27 | expect(Test::MyClass).to respond_to(method_name) 28 | expect(Test::OtherClass).to respond_to(method_name) 29 | end 30 | end 31 | 32 | it "allows storage of values on the class" do 33 | expect(Test::MyClass.one).to eq(1) 34 | expect(Test::MyClass.two).to eq(2) 35 | expect(Test::MyClass.three).to eq(3) 36 | end 37 | 38 | it "allows overwriting of inherited values with nil" do 39 | expect(Test::OtherClass.three).to eq(nil) 40 | end 41 | 42 | context "type option" do 43 | let(:klass) do 44 | module Test 45 | class NewClass 46 | extend Dry::Core::ClassAttributes 47 | end 48 | end 49 | 50 | Test::NewClass 51 | end 52 | 53 | context "using classes" do 54 | before do 55 | klass.defines :one, type: String 56 | end 57 | 58 | it "allows to pass type option" do 59 | klass.one "1" 60 | expect(Test::NewClass.one).to eq "1" 61 | end 62 | 63 | it "raises InvalidClassAttributeValueError when invalid value is pass" do 64 | expect { 65 | klass.one 1 66 | }.to raise_error( 67 | Dry::Core::InvalidClassAttributeValueError, 68 | "Value 1 is invalid for class attribute :one" 69 | ) 70 | end 71 | end 72 | 73 | context "using dry-types" do 74 | before do 75 | module Test 76 | class Types 77 | include Dry::Types() 78 | end 79 | end 80 | 81 | klass.defines :one, type: Test::Types::String 82 | end 83 | 84 | it "allows to pass type option" do 85 | klass.one "1" 86 | expect(Test::NewClass.one).to eq "1" 87 | end 88 | 89 | it "raises InvalidClassAttributeValueError when invalid value is pass" do 90 | expect { 91 | klass.one 1 92 | }.to raise_error(Dry::Core::InvalidClassAttributeValueError) 93 | end 94 | end 95 | end 96 | 97 | context "coerce option" do 98 | let(:klass) do 99 | module Test 100 | class NewClass 101 | extend Dry::Core::ClassAttributes 102 | end 103 | end 104 | 105 | Test::NewClass 106 | end 107 | 108 | context "using procs" do 109 | before do 110 | klass.defines :one, coerce: proc(&:to_s) 111 | end 112 | 113 | it "converts value" do 114 | klass.one 1 115 | expect(Test::NewClass.one).to eq "1" 116 | end 117 | end 118 | 119 | context "using dry-types" do 120 | before do 121 | module Test 122 | class Types 123 | include Dry::Types() 124 | end 125 | end 126 | 127 | klass.defines :one, coerce: Test::Types::Coercible::String 128 | end 129 | 130 | it "converts value" do 131 | klass.one 1 132 | expect(Test::NewClass.one).to eq "1" 133 | end 134 | end 135 | 136 | context "using non-callable coerce option" do 137 | it "raises InvalidCoerceOption" do 138 | expect { 139 | klass.defines :one, coerce: String 140 | }.to raise_error( 141 | ArgumentError, 142 | "Non-callable coerce option: String" 143 | ) 144 | end 145 | end 146 | end 147 | 148 | it "allows inheritance of values" do 149 | expect(Test::OtherClass.one).to eq(1) 150 | end 151 | 152 | it "allows overwriting of inherited values" do 153 | expect(Test::OtherClass.two).to eq("two") 154 | end 155 | 156 | it "copies values from the parent before running hooks" do 157 | subclass_value = nil 158 | 159 | module_with_hook = Module.new do 160 | define_method(:inherited) do |klass| 161 | super(klass) 162 | 163 | subclass_value = klass.one 164 | end 165 | end 166 | 167 | base_class = Class.new do 168 | extend Dry::Core::ClassAttributes 169 | extend module_with_hook 170 | 171 | defines :one 172 | one 1 173 | end 174 | 175 | Class.new(base_class) 176 | 177 | expect(subclass_value).to be 1 178 | end 179 | 180 | it "works with private setters/getters and inheritance" do 181 | base_class = Class.new do 182 | extend Dry::Core::ClassAttributes 183 | 184 | defines :one 185 | class << self; private :one; end 186 | one 1 187 | end 188 | 189 | spec = self 190 | 191 | child = Class.new(base_class) do |chld| 192 | spec.instance_exec { expect(chld.send(:one)).to spec.eql(1) } 193 | 194 | one "one" 195 | end 196 | 197 | expect(child.send(:one)).to eql("one") 198 | end 199 | end 200 | -------------------------------------------------------------------------------- /spec/dry/core/class_builder_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Dry::Core::ClassBuilder do 4 | subject(:builder) { described_class.new(**options) } 5 | 6 | let(:klass) { builder.call } 7 | 8 | describe "#call" do 9 | context "anonymous" do 10 | let(:options) do 11 | {name: "Test", parent: parent} 12 | end 13 | 14 | let(:parent) { Class.new } 15 | 16 | it "returns a class constant" do 17 | expect(klass).to be_instance_of(Class) 18 | end 19 | 20 | it "sets class name based on provided :name option" do 21 | expect(klass.name).to eql(options[:name]) 22 | end 23 | 24 | it "uses a parent class provided by :parent option" do 25 | expect(klass).to be < parent 26 | end 27 | 28 | it "defines to_s and inspect" do 29 | expect(klass.to_s).to eql(options[:name]) 30 | expect(klass.inspect).to eql(options[:name]) 31 | end 32 | 33 | it "yields created class" do 34 | klass = builder.call do |yielded_class| 35 | yielded_class.class_eval do 36 | def self.testing; end 37 | end 38 | end 39 | 40 | expect(klass).to respond_to(:testing) 41 | end 42 | end 43 | 44 | context "namespaced" do 45 | context "without parent" do 46 | let(:options) do 47 | {name: "User", namespace: Test} 48 | end 49 | 50 | it "creates a class within the given namespace" do 51 | expect(klass).to be_instance_of(Class) 52 | expect(klass.name).to eql("Test::User") 53 | expect(klass.superclass).to be(Test::User) 54 | end 55 | end 56 | 57 | context "with parent" do 58 | let(:parent) do 59 | Test::Parent = Class.new 60 | end 61 | 62 | let(:options) do 63 | {name: "User", parent: parent, namespace: Test} 64 | end 65 | 66 | it "creates a class with the given parent" do 67 | expect(klass).to be_instance_of(Class) 68 | expect(klass.name).to eql("Test::User") 69 | expect(klass.superclass).to be(Test::User) 70 | expect(klass.superclass.superclass).to be(Test::Parent) 71 | end 72 | end 73 | 74 | context "with mismatched parent class" do 75 | before do 76 | Test::InvalidParent = Class.new 77 | Test::User = Class.new(Test::InvalidParent) 78 | end 79 | 80 | let(:parent) do 81 | Test::Parent = Class.new 82 | end 83 | 84 | let(:options) do 85 | {name: "User", namespace: Test, parent: parent} 86 | end 87 | 88 | it "raises meaningful error on mismatched parent class" do 89 | expect { klass }.to raise_error( 90 | Dry::Core::ClassBuilder::ParentClassMismatch, 91 | "Test::User must be a subclass of Test::Parent" 92 | ) 93 | end 94 | end 95 | 96 | context "with parent inherited from object" do 97 | let(:parent) do 98 | Test::Parent = Class.new 99 | end 100 | 101 | let(:options) do 102 | {name: "File", namespace: Test, parent: parent} 103 | end 104 | 105 | it "does not search for parent class through inheritance" do 106 | expect(klass.name).to eql("Test::File") 107 | expect(klass.superclass).to be(Test::File) 108 | expect(Test::File).not_to be(File) 109 | end 110 | end 111 | 112 | context "autoloaded constant" do 113 | before do 114 | Test.module_eval do 115 | autoload :Project, File.join(__dir__, "../../fixtures/project") 116 | end 117 | end 118 | 119 | after do 120 | Test.module_eval do 121 | remove_const(:Project) 122 | end 123 | end 124 | 125 | let(:parent) do 126 | Test::Parent = Class.new 127 | end 128 | 129 | let(:options) do 130 | {name: "Project", namespace: Test, parent: parent} 131 | end 132 | 133 | it "autoloads the specified class" do 134 | expect(klass.name).to eq("Test::Project") 135 | expect(klass.superclass).to be(Test::Project) 136 | expect(klass.instance_methods).to include(:to_model) 137 | end 138 | end 139 | end 140 | end 141 | end 142 | -------------------------------------------------------------------------------- /spec/dry/core/constants_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Dry::Core::Constants do 4 | before do 5 | class ClassWithConstants 6 | include Dry::Core::Constants 7 | 8 | def empty_array 9 | EMPTY_ARRAY 10 | end 11 | 12 | def empty_hash 13 | EMPTY_HASH 14 | end 15 | 16 | def empty_set 17 | EMPTY_SET 18 | end 19 | 20 | def empty_string 21 | EMPTY_STRING 22 | end 23 | 24 | def empty_opts 25 | EMPTY_OPTS 26 | end 27 | 28 | def undefined 29 | Undefined 30 | end 31 | end 32 | end 33 | 34 | after do 35 | Object.send(:remove_const, :ClassWithConstants) 36 | end 37 | 38 | subject { ClassWithConstants.new } 39 | 40 | it "makes constants available in your class" do 41 | expect(subject.empty_array).to be Dry::Core::Constants::EMPTY_ARRAY 42 | expect(subject.empty_array).to eql([]) 43 | 44 | expect(subject.empty_hash).to be Dry::Core::Constants::EMPTY_HASH 45 | expect(subject.empty_hash).to eql({}) 46 | 47 | expect(subject.empty_set).to be Dry::Core::Constants::EMPTY_SET 48 | expect(subject.empty_set).to eql(Set.new) 49 | 50 | expect(subject.empty_string).to be Dry::Core::Constants::EMPTY_STRING 51 | expect(subject.empty_string).to eql("") 52 | 53 | expect(subject.empty_opts).to be Dry::Core::Constants::EMPTY_OPTS 54 | expect(subject.empty_opts).to eql({}) 55 | 56 | expect(subject.undefined).to be Dry::Core::Constants::Undefined 57 | end 58 | 59 | describe "nested" do 60 | before do 61 | class ClassWithConstants 62 | class Nested 63 | def empty_array 64 | EMPTY_ARRAY 65 | end 66 | end 67 | end 68 | end 69 | 70 | subject { ClassWithConstants::Nested.new } 71 | 72 | example "constants available in lexical scope" do 73 | expect(subject.empty_array).to be Dry::Core::Constants::EMPTY_ARRAY 74 | end 75 | end 76 | 77 | describe "Undefined" do 78 | subject(:undefined) { Dry::Core::Constants::Undefined } 79 | 80 | describe ".inspect" do 81 | it 'returns "Undefined"' do 82 | expect(subject.inspect).to eql("Undefined") 83 | end 84 | end 85 | 86 | describe ".to_s" do 87 | it 'returns "Undefined"' do 88 | expect(subject.to_s).to eql("Undefined") 89 | end 90 | end 91 | 92 | describe ".default" do 93 | it "returns the first arg if it's not Undefined" do 94 | expect(subject.default(:first, :second)).to eql(:first) 95 | end 96 | 97 | it "returns the second arg if the first one is Undefined" do 98 | expect(subject.default(subject, :second)).to eql(:second) 99 | end 100 | 101 | it "yields a block" do 102 | expect(subject.default(subject) { :second }).to eql(:second) 103 | end 104 | end 105 | 106 | describe ".map" do 107 | it "maps non-undefined value" do 108 | expect(subject.map("foo", &:to_sym)).to be(:foo) 109 | expect(subject.map(subject, &:to_sym)).to be(subject) 110 | end 111 | end 112 | 113 | describe ".dup" do 114 | subject { undefined.dup } 115 | 116 | it { is_expected.to be(undefined) } 117 | end 118 | 119 | describe ".clone" do 120 | subject { undefined.clone } 121 | 122 | it { is_expected.to be(undefined) } 123 | end 124 | 125 | describe ".coalesce" do 126 | it "returns first non-undefined value in a list" do 127 | expect(undefined.coalesce(1, 2)).to be(1) 128 | expect(undefined.coalesce(undefined, 1, 2)).to be(1) 129 | expect(undefined.coalesce(undefined, undefined, 1, 2)).to be(1) 130 | expect(undefined.coalesce(undefined, undefined)).to be(undefined) 131 | expect(undefined.coalesce(nil)).to be(nil) 132 | expect(undefined.coalesce(undefined, nil)).to be(nil) 133 | expect(undefined.coalesce(undefined, nil, false)).to be(nil) 134 | expect(undefined.coalesce(undefined, false, nil)).to be(false) 135 | end 136 | end 137 | end 138 | end 139 | -------------------------------------------------------------------------------- /spec/dry/core/container/mixin_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Dry::Core::Container::Mixin do 4 | describe "extended" do 5 | let(:klass) do 6 | Class.new { extend Dry::Core::Container::Mixin } 7 | end 8 | 9 | let(:container) do 10 | klass 11 | end 12 | 13 | it_behaves_like "a container" 14 | end 15 | 16 | describe "included" do 17 | let(:klass) do 18 | Class.new { include Dry::Core::Container::Mixin } 19 | end 20 | 21 | let(:container) do 22 | klass.new 23 | end 24 | 25 | it_behaves_like "a container" 26 | 27 | context "into a class with a custom .initialize method" do 28 | let(:klass) do 29 | Class.new do 30 | attr_reader :test 31 | 32 | include Dry::Core::Container::Mixin 33 | 34 | def initialize 35 | @test = true 36 | end 37 | end 38 | end 39 | 40 | it "does not fail on missing member variable" do 41 | expect { container.register :key, -> {} }.to_not raise_error 42 | end 43 | 44 | it "doesn't override the original initialize method" do 45 | expect(container.test).to be(true) 46 | end 47 | end 48 | end 49 | 50 | if defined?(Dry::Configurable) 51 | context "using custom settings via Dry::Configurable with a class" do 52 | let(:klass) do 53 | Class.new do 54 | extend Dry::Core::Container::Mixin 55 | 56 | setting :root, default: "/tmp" 57 | end 58 | end 59 | 60 | let(:container) do 61 | klass 62 | end 63 | 64 | it "exposes custom config" do 65 | expect(container.config.root).to eql("/tmp") 66 | end 67 | end 68 | 69 | context "using custom settings via Dry::Configurable with an object" do 70 | let(:klass) do 71 | Class.new do 72 | include Dry::Core::Container::Mixin 73 | 74 | setting :root, default: "/tmp" 75 | end 76 | end 77 | 78 | let(:container) do 79 | klass.new 80 | end 81 | 82 | it "exposes custom config" do 83 | expect(container.config.root).to eql("/tmp") 84 | end 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /spec/dry/core/deprecations_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "tempfile" 4 | 5 | RSpec.describe Dry::Core::Deprecations do 6 | let(:log_file) do 7 | Tempfile.new("dry_deprecations") 8 | end 9 | 10 | before do 11 | Dry::Core::Deprecations.set_logger!(log_file) 12 | end 13 | 14 | let(:log_output) do 15 | log_file.close 16 | log_file.open.read 17 | end 18 | 19 | describe ".warn" do 20 | it "logs a warning message" do 21 | Dry::Core::Deprecations.warn("hello world") 22 | expect(log_output).to include("[deprecated] hello world") 23 | end 24 | 25 | it "logs a tagged message" do 26 | Dry::Core::Deprecations.warn("hello world", tag: :spec) 27 | expect(log_output).to include("[spec] hello world") 28 | end 29 | 30 | it "prints information about the caller frame if uplevel is given" do 31 | Dry::Core::Deprecations.warn("hello world", uplevel: 0) 32 | expect(log_output).to include("/rspec/") 33 | end 34 | end 35 | 36 | describe ".announce" do 37 | it "warns about a deprecated method" do 38 | Dry::Core::Deprecations.announce(:foo, "hello world", tag: :spec, uplevel: 0) 39 | expect(log_output).to include("[spec] foo is deprecated and will be removed") 40 | expect(log_output).to include("hello world") 41 | expect(log_output).to include("/rspec/") 42 | end 43 | end 44 | 45 | shared_examples_for "an entity with deprecated methods" do 46 | it "deprecates method that is to be removed" do 47 | res = subject.hello("world") 48 | 49 | expect(res).to eql("hello world") 50 | expect(log_output).to match(/\[spec\] Test(\.|#)hello is deprecated and will be removed/) 51 | expect(log_output).to include("is no more") 52 | end 53 | 54 | it "deprecates a method in favor of another" do 55 | res = subject.logging("foo") 56 | 57 | expect(res).to eql("log: foo") 58 | expect(log_output).to match(/\[spec\] Test(\.|#)logging is deprecated and will be removed/) 59 | end 60 | 61 | it "does not require deprecated method to be defined" do 62 | res = subject.missing("bar") 63 | 64 | expect(res).to eql("log: bar") 65 | expect(log_output).to match(/\[spec\] Test(\.|#)missing is deprecated and will be removed/) 66 | end 67 | end 68 | 69 | describe ".deprecate_class_method" do 70 | subject(:klass) do 71 | Class.new do 72 | extend Dry::Core::Deprecations[:spec] 73 | 74 | def self.name 75 | "Test" 76 | end 77 | 78 | def self.log(msg) 79 | "log: #{msg}" 80 | end 81 | 82 | def self.hello(word) 83 | "hello #{word}" 84 | end 85 | deprecate_class_method :hello, message: "is no more" 86 | 87 | def self.logging(msg) 88 | "logging: #{msg}" 89 | end 90 | deprecate_class_method :logging, :log 91 | 92 | deprecate_class_method :missing, :log 93 | end 94 | end 95 | 96 | it_behaves_like "an entity with deprecated methods" do 97 | subject { klass } 98 | end 99 | end 100 | 101 | describe ".deprecate" do 102 | subject(:klass) do 103 | Class.new do 104 | extend Dry::Core::Deprecations[:spec] 105 | 106 | def self.name 107 | "Test" 108 | end 109 | 110 | def log(msg) 111 | "log: #{msg}" 112 | end 113 | 114 | def hello(word) 115 | "hello #{word}" 116 | end 117 | deprecate :hello, message: "is no more" 118 | 119 | def logging(msg) 120 | "logging: #{msg}" 121 | end 122 | deprecate :logging, :log 123 | 124 | deprecate :missing, :log 125 | end 126 | end 127 | 128 | it_behaves_like "an entity with deprecated methods" do 129 | subject { klass.new } 130 | end 131 | end 132 | 133 | describe ".deprecate_constant" do 134 | before do 135 | module Test 136 | extend Dry::Core::Deprecations[:spec] 137 | 138 | Obsolete = :no_more 139 | 140 | deprecate_constant(:Obsolete) 141 | 142 | Deprecated = :fix 143 | 144 | deprecate_constant(:Deprecated, message: "Shiny New") 145 | end 146 | end 147 | 148 | it "deprecates a constant in favor of another" do 149 | expect(Test::Obsolete).to be(:no_more) 150 | expect(log_output).to match(/\[spec\] Test::Obsolete is deprecated and will be removed/) 151 | end 152 | 153 | it "can have an optional messaage" do 154 | expect(Test::Deprecated).to be(:fix) 155 | expect(log_output).to match(/Shiny New/) 156 | end 157 | end 158 | 159 | describe ".[]" do 160 | subject(:klass) do 161 | Class.new do 162 | extend Dry::Core::Deprecations[:spec] 163 | end 164 | end 165 | 166 | describe ".warn" do 167 | it "logs a tagged message" do 168 | klass.warn("hello") 169 | expect(log_output).to include("[spec] hello") 170 | end 171 | end 172 | end 173 | 174 | describe ".set_logger!" do 175 | let(:logger) do 176 | Class.new { 177 | attr_reader :messages 178 | 179 | def initialize 180 | @messages = [] 181 | end 182 | 183 | def warn(message) 184 | messages << message 185 | end 186 | }.new 187 | end 188 | 189 | it "accepts preconfigured logger" do 190 | Dry::Core::Deprecations.set_logger!(logger) 191 | Dry::Core::Deprecations.warn("Don't!") 192 | 193 | expect(logger.messages).to eql(["[deprecated] Don't!"]) 194 | end 195 | end 196 | 197 | describe ".logger" do 198 | let(:stderr) do 199 | Class.new { 200 | attr_reader :messages 201 | 202 | def initialize 203 | @messages = [] 204 | end 205 | 206 | def write(message) 207 | messages << message 208 | end 209 | 210 | def close 211 | messages.freeze 212 | end 213 | }.new 214 | end 215 | 216 | around do |ex| 217 | $stderr = stderr 218 | ex.run 219 | $stderr = STDERR 220 | end 221 | 222 | before do 223 | module Dry::Core::Deprecations 224 | remove_instance_variable :@logger 225 | end 226 | end 227 | 228 | let(:default_logger) { Dry::Core::Deprecations.logger } 229 | 230 | it "sets $stderr as a default stream" do 231 | default_logger.warn("Test") 232 | expect(stderr.messages).not_to be_empty 233 | end 234 | end 235 | end 236 | -------------------------------------------------------------------------------- /spec/dry/core/descendants_tracker_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Dry::Core::DescendantsTracker do 4 | before do 5 | module Test 6 | class Parent 7 | extend Dry::Core::DescendantsTracker 8 | end 9 | 10 | class Child < Parent 11 | end 12 | 13 | class Grandchild < Child 14 | end 15 | end 16 | end 17 | 18 | it "tracks descendants" do 19 | expect(Test::Parent.descendants).to eql([Test::Grandchild, Test::Child]) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/dry/core/equalizer/included_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Dry::Core::Equalizer, "#included" do 6 | subject { descendant.instance_exec(object) { |mod| include mod } } 7 | 8 | let(:object) { described_class.new } 9 | let(:descendant) { Class.new } 10 | let(:superclass) { described_class.superclass } 11 | 12 | before do 13 | # Prevent Module.included from being called through inheritance 14 | allow(described_class::Methods).to receive(:included) 15 | end 16 | 17 | around do |example| 18 | # Restore included method after each example 19 | superclass.class_eval do 20 | alias_method :original_included, :included 21 | example.call 22 | undef_method :included 23 | alias_method :included, :original_included 24 | end 25 | end 26 | 27 | it "delegates to the superclass #included method" do 28 | # This is the most succinct approach I could think of to test whether the 29 | # superclass#included method is called. All of the built-in rspec helpers 30 | # did not seem to work for this. 31 | included = false 32 | 33 | superclass.class_eval do 34 | define_method(:included) do |_| 35 | # Only set the flag when an Dry::Core::Equalizer instance is included. 36 | # Otherwise, other module includes (which get triggered internally 37 | # in RSpec when `change` is used for the first time, since it uses 38 | # autoloading for its matchers) will wrongly set this flag. 39 | included = true if is_a?(Dry::Core::Equalizer) 40 | end 41 | end 42 | 43 | expect { subject }.to change { included }.from(false).to(true) 44 | end 45 | 46 | it "includes methods into the descendant" do 47 | subject 48 | expect(descendant.included_modules).to include(described_class::Methods) 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /spec/dry/core/equalizer/legacy_name_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe "legacy Dry::Equalizer name" do 4 | it "behaves the same as when using Dry::Core::Equalizer" do 5 | klass = Class.new { 6 | attr_reader :name 7 | 8 | def initialize(name) 9 | @name = name 10 | end 11 | } 12 | allow(klass).to receive_messages(name: nil, inspect: "User") # specify the class #inspect method 13 | klass.include Dry::Equalizer(:name) 14 | 15 | instance = klass.new("Jane") 16 | other = instance.dup 17 | 18 | expect(instance).to eql(other) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/dry/core/equalizer/methods/eql_predicate_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Dry::Core::Equalizer::Methods, "#eql?" do 6 | subject { object.eql?(other) } 7 | 8 | let(:object) { described_class.new(true) } 9 | 10 | let(:described_class) do 11 | Class.new do 12 | include Dry::Core::Equalizer::Methods 13 | 14 | attr_reader :boolean 15 | 16 | def initialize(boolean) 17 | @boolean = boolean 18 | end 19 | 20 | def cmp?(comparator, other) 21 | boolean.send(comparator, other.boolean) 22 | end 23 | end 24 | end 25 | 26 | context "with the same object" do 27 | let(:other) { object } 28 | 29 | it { should be(true) } 30 | 31 | it "is symmetric" do 32 | should eql(other.eql?(object)) 33 | end 34 | end 35 | 36 | context "with an equivalent object" do 37 | let(:other) { object.dup } 38 | 39 | it { should be(true) } 40 | 41 | it "is symmetric" do 42 | should eql(other.eql?(object)) 43 | end 44 | end 45 | 46 | context "with an equivalent object of a subclass" do 47 | let(:other) { Class.new(described_class).new(true) } 48 | 49 | it { should be(false) } 50 | 51 | it "is symmetric" do 52 | should eql(other.eql?(object)) 53 | end 54 | end 55 | 56 | context "with a different object" do 57 | let(:other) { described_class.new(false) } 58 | 59 | it { should be(false) } 60 | 61 | it "is symmetric" do 62 | should eql(other.eql?(object)) 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /spec/dry/core/equalizer/methods/equality_operator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Dry::Core::Equalizer::Methods, "#==" do 6 | subject { object == other } 7 | 8 | let(:object) { described_class.new(true) } 9 | let(:described_class) { Class.new(super_class) } 10 | 11 | let(:super_class) do 12 | Class.new do 13 | include Dry::Core::Equalizer::Methods 14 | 15 | attr_reader :boolean 16 | 17 | def initialize(boolean) 18 | @boolean = boolean 19 | end 20 | 21 | def cmp?(comparator, other) 22 | boolean.send(comparator, other.boolean) 23 | end 24 | end 25 | end 26 | 27 | context "with the same object" do 28 | let(:other) { object } 29 | 30 | it { should be(true) } 31 | 32 | it "is symmetric" do 33 | should eql(other == object) 34 | end 35 | end 36 | 37 | context "with an equivalent object" do 38 | let(:other) { object.dup } 39 | 40 | it { should be(true) } 41 | 42 | it "is symmetric" do 43 | should eql(other == object) 44 | end 45 | end 46 | 47 | context "with a subclass instance having equivalent obervable state" do 48 | let(:other) { Class.new(described_class).new(true) } 49 | 50 | it { should be(true) } 51 | 52 | it "is not symmetric" do 53 | # the subclass instance should maintain substitutability with the object 54 | # (in the LSP sense) the reverse is not true. 55 | should_not eql(other == object) 56 | end 57 | end 58 | 59 | context "with a superclass instance having equivalent observable state" do 60 | let(:other) { super_class.new(true) } 61 | 62 | it { should be(false) } 63 | 64 | it "is not symmetric" do 65 | should_not eql(other == object) 66 | end 67 | end 68 | 69 | context "with an object of another class" do 70 | let(:other) { Class.new.new } 71 | 72 | it { should be(false) } 73 | 74 | it "is symmetric" do 75 | should eql(other == object) 76 | end 77 | end 78 | 79 | context "with a different object" do 80 | let(:other) { described_class.new(false) } 81 | 82 | it { should be(false) } 83 | 84 | it "is symmetric" do 85 | should eql(other == object) 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /spec/dry/core/equalizer/universal_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Dry::Core::Equalizer do 6 | let(:name) { "User" } 7 | let(:klass) { Class.new } 8 | 9 | context "with no keys" do 10 | subject { Dry::Core::Equalizer() } 11 | 12 | before do 13 | # specify the class #name method 14 | allow(klass).to receive(:name).and_return(name) 15 | klass.include(subject) 16 | end 17 | 18 | let(:instance) { klass.new } 19 | 20 | it { should be_instance_of(described_class) } 21 | 22 | it { should be_frozen } 23 | 24 | it "defines #hash and #inspect methods dynamically" do 25 | expect(subject.public_instance_methods(false).map(&:to_s).sort) 26 | .to eql(%w[hash inspect]) 27 | end 28 | 29 | describe "#eql?" do 30 | context "when the objects are similar" do 31 | let(:other) { instance.dup } 32 | 33 | it { expect(instance.eql?(other)).to be(true) } 34 | end 35 | 36 | context "when the objects are different" do 37 | let(:other) { double("other") } 38 | 39 | it { expect(instance.eql?(other)).to be(false) } 40 | end 41 | end 42 | 43 | describe "#==" do 44 | context "when the objects are similar" do 45 | let(:other) { instance.dup } 46 | 47 | it { expect(instance == other).to be(true) } 48 | end 49 | 50 | context "when the objects are different" do 51 | let(:other) { double("other") } 52 | 53 | it { expect(instance == other).to be(false) } 54 | end 55 | end 56 | 57 | describe "#hash" do 58 | it "has the expected arity" do 59 | expect(klass.instance_method(:hash).arity).to be(0) 60 | end 61 | 62 | it { expect(instance.hash).to eql([klass].hash) } # rubocop:disable Security/CompoundHash 63 | end 64 | 65 | describe "#inspect" do 66 | it "has the expected arity" do 67 | expect(klass.instance_method(:inspect).arity).to be(0) 68 | end 69 | 70 | it { expect(instance.inspect).to eql("#") } 71 | end 72 | end 73 | 74 | context "with keys" do 75 | subject { Dry::Core::Equalizer(*keys) } 76 | 77 | let(:keys) { %i[firstname lastname].freeze } 78 | let(:firstname) { "John" } 79 | let(:lastname) { "Doe" } 80 | let(:instance) { klass.new(firstname, lastname) } 81 | 82 | let(:klass) do 83 | Class.new do 84 | attr_reader :firstname, :lastname 85 | attr_writer :firstname 86 | private :firstname, :lastname 87 | 88 | def initialize(firstname, lastname) 89 | @firstname = firstname 90 | @lastname = lastname 91 | end 92 | end 93 | end 94 | 95 | before do 96 | # specify the class #inspect method 97 | allow(klass).to receive_messages(name: nil, inspect: name) 98 | klass.include(subject) 99 | end 100 | 101 | it { should be_instance_of(described_class) } 102 | 103 | it { should be_frozen } 104 | 105 | it "defines #hash and #inspect methods dynamically" do 106 | expect(subject.public_instance_methods(false).map(&:to_s).sort) 107 | .to eql(%w[hash inspect]) 108 | end 109 | 110 | describe "#eql?" do 111 | context "when the objects are similar" do 112 | let(:other) { instance.dup } 113 | 114 | it { expect(instance.eql?(other)).to be(true) } 115 | end 116 | 117 | context "when the objects are different" do 118 | let(:other) { double("other") } 119 | 120 | it { expect(instance.eql?(other)).to be(false) } 121 | end 122 | end 123 | 124 | describe "#==" do 125 | context "when the objects are similar" do 126 | let(:other) { instance.dup } 127 | 128 | it { expect(instance == other).to be(true) } 129 | end 130 | 131 | context "when the objects are different type" do 132 | let(:other) { klass.new("Foo", "Bar") } 133 | 134 | it { expect(instance == other).to be(false) } 135 | end 136 | 137 | context "when the objects are from different type" do 138 | let(:other) { double("other") } 139 | 140 | it { expect(instance == other).to be(false) } 141 | end 142 | end 143 | 144 | describe "#hash" do 145 | it "returns the expected hash" do 146 | expect(instance.hash) 147 | .to eql([firstname, lastname, klass].hash) 148 | end 149 | end 150 | 151 | describe "#inspect" do 152 | it "returns the expected string" do 153 | expect(instance.inspect) 154 | .to eql('#') 155 | end 156 | end 157 | 158 | context "when immutable" do 159 | describe "#hash" do 160 | subject { Dry::Core::Equalizer(*keys, immutable: true) } 161 | 162 | it "returns memoized hash" do 163 | expect { instance.firstname = "Changed" }.not_to(change { instance.hash }) 164 | end 165 | 166 | context "when frozen" do 167 | it "returns memoized hash" do 168 | instance.freeze 169 | 170 | expect(instance.hash) 171 | .to eql([firstname, lastname, klass].hash) 172 | end 173 | end 174 | end 175 | end 176 | end 177 | 178 | context "with duplicate keys" do 179 | subject { Dry::Core::Equalizer(*keys) } 180 | 181 | let(:keys) { %i[firstname firstname lastname].freeze } 182 | let(:firstname) { "John" } 183 | let(:lastname) { "Doe" } 184 | let(:instance) { klass.new(firstname, lastname) } 185 | 186 | let(:klass) do 187 | Class.new do 188 | attr_reader :firstname, :lastname 189 | private :firstname, :lastname 190 | 191 | def initialize(firstname, lastname) 192 | @firstname = firstname 193 | @lastname = lastname 194 | end 195 | end 196 | end 197 | 198 | before do 199 | # specify the class #inspect method 200 | allow(klass).to receive_messages(name: nil, inspect: name) 201 | klass.include(subject) 202 | end 203 | 204 | it { should be_instance_of(described_class) } 205 | 206 | it { should be_frozen } 207 | 208 | describe "#inspect" do 209 | it "returns the expected string" do 210 | expect(instance.inspect) 211 | .to eql('#') 212 | end 213 | end 214 | end 215 | 216 | context "with options" do 217 | context "w/o inspect" do 218 | subject { Dry::Core::Equalizer(*keys, inspect: false) } 219 | 220 | let(:keys) { %i[firstname lastname].freeze } 221 | let(:firstname) { "John" } 222 | let(:lastname) { "Doe" } 223 | let(:instance) { klass.new(firstname, lastname) } 224 | 225 | let(:klass) do 226 | Struct.new(:firstname, :lastname) 227 | end 228 | 229 | before { klass.include(subject) } 230 | 231 | describe "#inspect" do 232 | it "returns the default string" do 233 | expect(instance.inspect).to eql('#') 234 | expect(instance.to_s).to eql('#') 235 | end 236 | end 237 | end 238 | end 239 | end 240 | -------------------------------------------------------------------------------- /spec/dry/core/extensions_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Dry::Core::Extensions do 4 | subject do 5 | Class.new do 6 | extend Dry::Core::Extensions 7 | end 8 | end 9 | 10 | it "allows to register and load extensions" do 11 | foo = false 12 | bar = false 13 | 14 | subject.register_extension(:foo) { foo = true } 15 | subject.register_extension(:bar) { bar = true } 16 | 17 | subject.load_extensions(:foo, :bar) 18 | 19 | expect(foo).to be true 20 | expect(bar).to be true 21 | end 22 | 23 | it "swallows double loading" do 24 | cnt = 0 25 | 26 | subject.register_extension(:foo) { cnt += 1 } 27 | subject.load_extensions(:foo) 28 | subject.load_extensions(:foo) 29 | 30 | expect(cnt).to be 1 31 | end 32 | 33 | it "raise ArgumentError on loading unknown extension" do 34 | subject.register_extension(:foo) { raise } 35 | expect { 36 | subject.load_extensions(:bar) 37 | }.to raise_error ArgumentError, "Unknown extension: :bar" 38 | end 39 | 40 | it "allows to query if an extension is available" do 41 | subject.register_extension(:foo) {} 42 | expect(subject.available_extension?(:foo)).to be true 43 | expect(subject.available_extension?(:bar)).to be false 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /spec/dry/core/inflector_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Dry::Core::Inflector do 4 | shared_examples "an inflector" do 5 | it "singularises" do 6 | expect(api.singularize("tasks")).to eql("task") 7 | end 8 | 9 | it "pluralizes" do 10 | expect(api.pluralize("task")).to eql("tasks") 11 | end 12 | 13 | it "camelizes" do 14 | expect(api.camelize("task_user")).to eql("TaskUser") 15 | end 16 | 17 | it "underscores" do 18 | expect(api.underscore("TaskUser")).to eql("task_user") 19 | end 20 | 21 | it "demodulizes" do 22 | expect(api.demodulize("Task::User")).to eql("User") 23 | end 24 | 25 | it "classifies" do 26 | expect(api.classify("task_user/name")).to eql("TaskUser::Name") 27 | end 28 | end 29 | 30 | shared_examples "an inflector with constantize" do 31 | it "constantizes" do 32 | expect(api.constantize("String")).to be String 33 | end 34 | end 35 | 36 | subject(:api) { Dry::Core::Inflector } 37 | 38 | context "with detected inflector" do 39 | before do 40 | if api.instance_variables.include?(:@inflector) 41 | api.__send__(:remove_instance_variable, :@inflector) 42 | end 43 | end 44 | 45 | it "prefers ActiveSupport::Inflector" do 46 | expect(api.inflector).to be ActiveSupport::Inflector 47 | end 48 | end 49 | 50 | context "with automatic detection" do 51 | before do 52 | if api.instance_variables.include?(:@inflector) 53 | api.__send__(:remove_instance_variable, :@inflector) 54 | end 55 | end 56 | 57 | it "automatically selects an inflector backend" do 58 | expect(api.inflector).not_to be nil 59 | end 60 | end 61 | 62 | context "with ActiveSupport::Inflector" do 63 | before do 64 | api.select_backend(:activesupport) 65 | end 66 | 67 | it "is ActiveSupport::Inflector" do 68 | expect(api.inflector).to be(ActiveSupport::Inflector) 69 | end 70 | 71 | it_behaves_like "an inflector" 72 | it_behaves_like "an inflector with constantize" 73 | end 74 | 75 | context "with Inflecto" do 76 | before do 77 | api.select_backend(:inflecto) 78 | end 79 | 80 | it "is Inflecto" do 81 | expect(api.inflector).to be(Inflecto) 82 | end 83 | 84 | it_behaves_like "an inflector" 85 | it_behaves_like "an inflector with constantize" 86 | end 87 | 88 | context "with Dry::Inflector" do 89 | before do 90 | api.select_backend(:dry_inflector) 91 | end 92 | 93 | it "is Dry::Inflector" do 94 | expect(api.inflector).to be_a(Dry::Inflector) 95 | end 96 | 97 | it_behaves_like "an inflector" 98 | end 99 | 100 | context "an unrecognized inflector library is selected" do 101 | it "raises a NameError" do 102 | expect { api.select_backend(:foo) }.to raise_error(NameError) 103 | end 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /spec/dry/core/memoizable_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "concurrent/atomic/atomic_fixnum" 4 | require "tempfile" 5 | require_relative "../../support/memoized" 6 | 7 | RSpec.describe Dry::Core::Memoizable do 8 | before do 9 | Dry::Core::Deprecations.set_logger!(Tempfile.new("dry_deprecations")) 10 | end 11 | 12 | before { Memoized.memoize_methods } 13 | 14 | describe ".memoize" do 15 | describe Object do 16 | it_behaves_like "a memoizable class" do 17 | context "frozen object" do 18 | before { object.freeze } 19 | 20 | it "works" do 21 | expect(object.foo).to be(object.foo) 22 | end 23 | end 24 | end 25 | end 26 | 27 | describe BasicObject do 28 | it_behaves_like "a memoizable class" 29 | end 30 | 31 | describe Class.new(Object) do 32 | it_behaves_like "a memoizable class" 33 | end 34 | 35 | describe Class.new(BasicObject) do 36 | it_behaves_like "a memoizable class" 37 | end 38 | end 39 | 40 | describe Memoized.new do 41 | let(:block) { -> {} } 42 | 43 | describe "test1" do 44 | it_behaves_like "a memoized method" do 45 | let(:new_meth) { described_class.method(:test1) } 46 | 47 | it "does not raise an error" do 48 | 2.times do 49 | new_meth.("a", kwarg1: "world", other: "test", &block) 50 | end 51 | end 52 | end 53 | end 54 | 55 | describe "test2" do 56 | it_behaves_like "a memoized method" do 57 | let(:new_meth) { described_class.method(:test2) } 58 | 59 | it "does not raise an error" do 60 | 2.times { new_meth.("a", &block) } 61 | end 62 | end 63 | end 64 | 65 | describe "test3" do 66 | it_behaves_like "a memoized method" do 67 | let(:new_meth) { described_class.method(:test3) } 68 | 69 | it "does not raise an error" do 70 | 2.times { new_meth.(&block) } 71 | end 72 | end 73 | end 74 | 75 | describe "test4" do 76 | it_behaves_like "a memoized method" do 77 | before { described_class.test4 } 78 | 79 | let(:new_meth) { described_class.method(:test4) } 80 | 81 | it "does not raise an error" do 82 | 2.times { new_meth.call } 83 | end 84 | end 85 | end 86 | end 87 | 88 | describe ".new" do 89 | let(:args) { [double("arg")] } 90 | let(:kwargs) { {key: double("value")} } 91 | let(:block) { -> { double("block") } } 92 | 93 | let(:object) do 94 | Class.new do 95 | include Dry::Core::Memoizable 96 | attr_reader :args, :kwargs, :block 97 | 98 | def initialize(*args, **kwargs, &block) 99 | @args = args 100 | @kwargs = kwargs 101 | @block = block 102 | end 103 | end.new(*args, **kwargs, &block) 104 | end 105 | 106 | describe "#args" do 107 | subject { object.args } 108 | 109 | it { is_expected.to eq(args) } 110 | end 111 | 112 | describe "#kwargs" do 113 | subject { object.kwargs } 114 | 115 | it { is_expected.to eq(kwargs) } 116 | end 117 | 118 | describe "#block" do 119 | subject { object.block } 120 | 121 | it { is_expected.to eq(block) } 122 | end 123 | end 124 | 125 | context "test calls" do 126 | let(:klass) { Class.new.include(Dry::Core::Memoizable) } 127 | 128 | let(:instance) { klass.new } 129 | 130 | let(:counter) { Concurrent::AtomicFixnum.new } 131 | 132 | context "no args" do 133 | before do 134 | counter = self.counter 135 | klass.define_method(:meth) { counter.increment } 136 | klass.memoize(:meth) 137 | end 138 | 139 | it "gets called only once" do 140 | instance.meth 141 | instance.meth 142 | instance.meth 143 | 144 | expect(counter.value).to eql(1) 145 | end 146 | end 147 | 148 | context "pos arg" do 149 | before do 150 | counter = self.counter 151 | klass.define_method(:meth) { |req| counter.increment } 152 | klass.memoize(:meth) 153 | end 154 | 155 | it "memoizes results" do 156 | instance.meth(1) 157 | instance.meth(1) 158 | instance.meth(2) 159 | instance.meth(2) 160 | 161 | expect(counter.value).to eql(2) 162 | end 163 | end 164 | 165 | context "splat" do 166 | before do 167 | counter = self.counter 168 | klass.define_method(:meth) { |v, *args| counter.increment } 169 | klass.memoize(:meth) 170 | end 171 | 172 | it "memoizes results" do 173 | instance.meth(1) 174 | instance.meth(1) 175 | expect(counter.value).to eql(1) 176 | 177 | instance.meth(1, 2) 178 | instance.meth(1, 2) 179 | expect(counter.value).to eql(2) 180 | 181 | instance.meth(1, 2, 3) 182 | instance.meth(1, 2, 3) 183 | expect(counter.value).to eql(3) 184 | end 185 | end 186 | 187 | context "**kwargs" do 188 | before do 189 | counter = self.counter 190 | klass.define_method(:meth) { |foo:, **kwargs| counter.increment } 191 | klass.memoize(:meth) 192 | end 193 | 194 | it "memoizes results" do 195 | instance.meth(foo: 1) 196 | instance.meth(foo: 1) 197 | expect(counter.value).to eql(1) 198 | 199 | instance.meth(foo: 1, bar: 2) 200 | instance.meth(foo: 1, bar: 2) 201 | expect(counter.value).to eql(2) 202 | 203 | instance.meth(foo: 1, baz: 2) 204 | instance.meth(foo: 1, baz: 2) 205 | expect(counter.value).to eql(3) 206 | end 207 | end 208 | 209 | context "inheritance" do 210 | let(:subclass) { Class.new(klass) } 211 | 212 | context "with memoisation" do 213 | before do 214 | klass.define_method(:nodes) { [:root] } 215 | klass.define_method(:path) { [*nodes, :leaf] } 216 | klass.memoize(:nodes, :path) 217 | 218 | subclass.define_method(:nodes) { [*super(), :node] } 219 | end 220 | 221 | let(:subclass_instance) do 222 | subclass.new 223 | end 224 | 225 | it "memoizes results separately" do 226 | expect(instance.nodes).to eql([:root]) 227 | expect(instance.path).to eql([:root, :leaf]) 228 | 229 | expect(subclass_instance.nodes).to eql([:root, :node]) 230 | expect(subclass_instance.path).to eql([:root, :node, :leaf]) 231 | end 232 | end 233 | 234 | context "inheritance w/o module" do 235 | before do 236 | klass.define_method(:nodes) { [:root] } 237 | end 238 | 239 | let(:subclass_instance) do 240 | subclass.new 241 | end 242 | 243 | it "doesn't throw an error" do 244 | expect(subclass_instance.nodes).to eql([:root]) 245 | end 246 | end 247 | end 248 | end 249 | end 250 | -------------------------------------------------------------------------------- /spec/dry/core_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Dry::Core do 4 | it "has a version number" do 5 | expect(Dry::Core::VERSION).not_to be nil 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/fixtures/project.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Test::Project = Class.new(Test::Parent) do 4 | def to_model 5 | "Project" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "support/coverage" 4 | require_relative "support/shared_examples/memoizable" 5 | require_relative "support/shared_examples/container" 6 | 7 | $LOAD_PATH.unshift File.expand_path("../lib", __dir__) 8 | require "rspec/version" 9 | 10 | begin 11 | require "pry" 12 | require "pry-byebug" 13 | rescue LoadError 14 | end 15 | 16 | require "dry/core" 17 | 18 | module Test 19 | def self.remove_constants 20 | constants.each(&method(:remove_const)) 21 | end 22 | end 23 | 24 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 25 | RSpec.configure do |config| 26 | if RSpec::Version::STRING >= "4.0.0" 27 | raise "This condition block can be safely removed" 28 | else 29 | config.expect_with :rspec do |expectations| 30 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 31 | end 32 | 33 | config.mock_with :rspec do |mocks| 34 | mocks.verify_partial_doubles = true 35 | end 36 | 37 | config.shared_context_metadata_behavior = :apply_to_host_groups 38 | end 39 | 40 | config.after do 41 | Test.remove_constants 42 | end 43 | 44 | # This allows you to limit a spec run to individual examples or groups 45 | # you care about by tagging them with `:focus` metadata. When nothing 46 | # is tagged with `:focus`, all examples get run. RSpec also provides 47 | # aliases for `it`, `describe`, and `context` that include `:focus` 48 | # metadata: `fit`, `fdescribe` and `fcontext`, respectively. 49 | config.filter_run_when_matching :focus 50 | 51 | # Allows RSpec to persist some state between runs in order to support 52 | # the `--only-failures` and `--next-failure` CLI options. 53 | config.example_status_persistence_file_path = "spec/examples.txt" 54 | 55 | config.disable_monkey_patching! 56 | 57 | config.warnings = true 58 | 59 | # Use the documentation formatter for detailed output 60 | config.default_formatter = "doc" if config.files_to_run.one? 61 | 62 | config.order = :random 63 | 64 | Kernel.srand config.seed 65 | end 66 | -------------------------------------------------------------------------------- /spec/support/coverage.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # this file is managed by dry-rb/devtools 4 | 5 | if ENV["COVERAGE"] == "true" 6 | require "simplecov" 7 | require "simplecov-cobertura" 8 | 9 | SimpleCov.formatter = SimpleCov::Formatter::CoberturaFormatter 10 | 11 | SimpleCov.start do 12 | add_filter "/spec/" 13 | enable_coverage :branch 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/support/memoized.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Memoized 4 | include Module.new { 5 | def test9 6 | # NOP 7 | end 8 | } 9 | include Dry::Core::Memoizable 10 | 11 | def test1(arg1, *args, kwarg1:, kwarg2: "default", **kwargs, &) 12 | # NOP 13 | end 14 | 15 | def test2(arg1, arg2 = "default", *args, &) 16 | # NOP 17 | end 18 | 19 | def test3(&) 20 | # NOP 21 | end 22 | 23 | def test4 24 | # NOP 25 | end 26 | 27 | def test5(args, *) 28 | # NOP 29 | end 30 | 31 | def test6(kwargs, **) 32 | # NOP 33 | end 34 | 35 | def test7(args, kwargs, *, **) 36 | # NOP 37 | end 38 | 39 | def test8(args = 1, kwargs = 2, *, **) 40 | # NOP 41 | end 42 | 43 | def test9(...) 44 | super 45 | end 46 | 47 | def self.memoize_methods 48 | @memoized ||= begin 49 | memoize :test1, :test2, :test3, :test4, :test5, :test6, :test7, :test8, :test9 50 | true 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /spec/support/rspec_options.rb: -------------------------------------------------------------------------------- 1 | RSpec.configure do |config| 2 | # When no filter given, search and run focused tests 3 | config.filter_run_when_matching :focus 4 | 5 | # Disables rspec monkey patches (no reason for their existence tbh) 6 | config.disable_monkey_patching! 7 | 8 | # Run ruby in verbose mode 9 | config.warnings = true 10 | 11 | # Collect all failing expectations automatically, 12 | # without calling aggregate_failures everywhere 13 | config.define_derived_metadata do |meta| 14 | meta[:aggregate_failures] = true 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/support/shared_examples/container.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.shared_examples "a container" do 4 | describe "configuration" do 5 | describe "registry" do 6 | describe "default" do 7 | it { expect(klass.config.registry).to be_a(Dry::Core::Container::Registry) } 8 | end 9 | 10 | describe "custom" do 11 | let(:custom_registry) { double("Registry") } 12 | let(:key) { :key } 13 | let(:item) { :item } 14 | let(:options) { {} } 15 | 16 | before do 17 | klass.configure do |config| 18 | config.registry = custom_registry 19 | end 20 | 21 | allow(custom_registry).to receive(:call) 22 | end 23 | 24 | after do 25 | # HACK: Have to reset the configuration so that it doesn't 26 | # interfere with other specs 27 | klass.configure do |config| 28 | config.registry = Dry::Core::Container::Registry.new 29 | end 30 | end 31 | 32 | subject! { container.register(key, item, options) } 33 | 34 | it do 35 | expect(custom_registry).to have_received(:call).with( 36 | container._container, 37 | key, 38 | item, 39 | options 40 | ) 41 | end 42 | end 43 | end 44 | 45 | describe "resolver" do 46 | describe "default" do 47 | it { expect(klass.config.resolver).to be_a(Dry::Core::Container::Resolver) } 48 | end 49 | 50 | describe "custom" do 51 | let(:custom_resolver) { double("Resolver") } 52 | let(:item) { double("Item") } 53 | let(:key) { :key } 54 | 55 | before do 56 | klass.configure do |config| 57 | config.resolver = custom_resolver 58 | end 59 | 60 | allow(custom_resolver).to receive(:call).and_return(item) 61 | end 62 | 63 | after do 64 | # HACK: Have to reset the configuration so that it doesn't 65 | # interfere with other specs 66 | klass.configure do |config| 67 | config.resolver = Dry::Core::Container::Resolver.new 68 | end 69 | end 70 | 71 | subject! { container.resolve(key) } 72 | 73 | it { expect(custom_resolver).to have_received(:call).with(container._container, key) } 74 | it { is_expected.to eq(item) } 75 | end 76 | end 77 | 78 | describe "namespace_separator" do 79 | describe "default" do 80 | it { expect(klass.config.namespace_separator).to eq(".") } 81 | end 82 | 83 | describe "custom" do 84 | let(:custom_registry) { double("Registry") } 85 | let(:key) { "key" } 86 | let(:namespace_separator) { "-" } 87 | let(:namespace) { "one" } 88 | 89 | before do 90 | klass.configure do |config| 91 | config.namespace_separator = namespace_separator 92 | end 93 | 94 | container.namespace(namespace) do 95 | register("key", "item") 96 | end 97 | end 98 | 99 | after do 100 | # HACK: Have to reset the configuration so that it doesn't 101 | # interfere with other specs 102 | klass.configure do |config| 103 | config.namespace_separator = "." 104 | end 105 | end 106 | 107 | subject! { container.resolve([namespace, key].join(namespace_separator)) } 108 | 109 | it { is_expected.to eq("item") } 110 | end 111 | end 112 | end 113 | 114 | context "with default configuration" do 115 | describe "registering a block" do 116 | context "without options" do 117 | context "without arguments" do 118 | it "registers and resolves an object" do 119 | container.register(:item) { "item" } 120 | 121 | expect(container.keys).to eq(["item"]) 122 | expect(container.key?(:item)).to be true 123 | expect(container.resolve(:item)).to eq("item") 124 | end 125 | end 126 | 127 | context "with arguments" do 128 | it "registers and resolves a proc" do 129 | container.register(:item) { |item| item } 130 | 131 | expect(container.resolve(:item).call("item")).to eq("item") 132 | end 133 | 134 | it "does not call a proc on resolving if one accepts an arbitrary number of keyword arguments" do 135 | container.register(:item) { |*| "item" } 136 | 137 | expect(container.resolve(:item)).to be_a_kind_of Proc 138 | expect(container.resolve(:item).call).to eq("item") 139 | end 140 | end 141 | end 142 | 143 | context "with option call: false" do 144 | it "registers and resolves a proc" do 145 | container.register(:item, call: false) { "item" } 146 | 147 | expect(container.keys).to eq(["item"]) 148 | expect(container.key?(:item)).to be true 149 | expect(container.resolve(:item).call).to eq("item") 150 | expect(container[:item].call).to eq("item") 151 | end 152 | end 153 | end 154 | 155 | describe "registering a proc" do 156 | context "without options" do 157 | context "without arguments" do 158 | it "registers and resolves an object" do 159 | container.register(:item, proc { "item" }) 160 | 161 | expect(container.keys).to eq(["item"]) 162 | expect(container.key?(:item)).to be true 163 | expect(container.resolve(:item)).to eq("item") 164 | expect(container[:item]).to eq("item") 165 | end 166 | end 167 | 168 | context "with arguments" do 169 | it "registers and resolves a proc" do 170 | container.register(:item, proc { |item| item }) 171 | 172 | expect(container.keys).to eq(["item"]) 173 | expect(container.key?(:item)).to be true 174 | expect(container.resolve(:item).call("item")).to eq("item") 175 | expect(container[:item].call("item")).to eq("item") 176 | end 177 | end 178 | end 179 | 180 | context "with option call: false" do 181 | it "registers and resolves a proc" do 182 | container.register(:item, proc { "item" }, call: false) 183 | 184 | expect(container.keys).to eq(["item"]) 185 | expect(container.key?(:item)).to be true 186 | expect(container.resolve(:item).call).to eq("item") 187 | expect(container[:item].call).to eq("item") 188 | end 189 | end 190 | 191 | context "with option memoize: true" do 192 | it "registers and resolves a proc" do 193 | container.register(:item, proc { "item" }, memoize: true) 194 | 195 | expect(container[:item]).to be container[:item] 196 | expect(container.keys).to eq(["item"]) 197 | expect(container.key?(:item)).to be true 198 | expect(container.resolve(:item)).to eq("item") 199 | expect(container[:item]).to eq("item") 200 | end 201 | 202 | it "only resolves the proc once" do 203 | resolved_times = 0 204 | 205 | container.register(:item, proc { resolved_times += 1 }, memoize: true) 206 | 207 | expect(container.resolve(:item)).to be 1 208 | expect(container.resolve(:item)).to be 1 209 | end 210 | 211 | context "when receiving something other than a proc" do 212 | it do 213 | expect { container.register(:item, "Hello!", memoize: true) }.to raise_error(Dry::Core::Container::Error) 214 | end 215 | end 216 | end 217 | end 218 | 219 | describe "registering an object" do 220 | context "without options" do 221 | it "registers and resolves the object" do 222 | item = "item" 223 | container.register(:item, item) 224 | 225 | expect(container.keys).to eq(["item"]) 226 | expect(container.key?(:item)).to be true 227 | expect(container.resolve(:item)).to be(item) 228 | expect(container[:item]).to be(item) 229 | end 230 | end 231 | 232 | context "with option call: false" do 233 | it "registers and resolves an object" do 234 | item = -> { "test" } 235 | container.register(:item, item, call: false) 236 | 237 | expect(container.keys).to eq(["item"]) 238 | expect(container.key?(:item)).to be true 239 | expect(container.resolve(:item)).to eq(item) 240 | expect(container[:item]).to eq(item) 241 | end 242 | end 243 | end 244 | 245 | describe "registering with the same key multiple times" do 246 | it do 247 | container.register(:item, proc { "item" }) 248 | 249 | expect { container.register(:item, proc { "item" }) }.to raise_error(Dry::Core::Container::KeyError) 250 | end 251 | end 252 | 253 | describe "resolving with a key that has not been registered" do 254 | it do 255 | expect(container.key?(:item)).to be false 256 | expect { container.resolve(:item) }.to raise_error(KeyError) do |error| 257 | # This is the API needed for DidYouMean::KeyErrorChecker to provide corrections 258 | expect(error.key).to eq("item") 259 | expect(error.receiver).to eq(container._container) 260 | expect(error.spell_checker).to be_instance_of(DidYouMean::KeyErrorChecker) 261 | end 262 | end 263 | end 264 | 265 | describe "mixing Strings and Symbols" do 266 | it do 267 | container.register(:item, "item") 268 | expect(container.resolve("item")).to eql("item") 269 | end 270 | end 271 | 272 | describe "#merge" do 273 | let(:key) { :key } 274 | let(:other) { Dry::Core::Container.new } 275 | 276 | before do 277 | other.register(key) { :item } 278 | end 279 | 280 | context "without namespace argument" do 281 | subject! { container.merge(other) } 282 | 283 | it { expect(container.resolve(key)).to be(:item) } 284 | it { expect(container[key]).to be(:item) } 285 | end 286 | 287 | context "with namespace argument" do 288 | subject! { container.merge(other, namespace: namespace) } 289 | 290 | context "when namespace is nil" do 291 | let(:namespace) { nil } 292 | 293 | it { expect(container.resolve(key)).to be(:item) } 294 | it { expect(container[key]).to be(:item) } 295 | end 296 | 297 | context "when namespace is not nil" do 298 | let(:namespace) { "namespace" } 299 | 300 | it { expect(container.resolve("#{namespace}.#{key}")).to be(:item) } 301 | it { expect(container["#{namespace}.#{key}"]).to be(:item) } 302 | end 303 | end 304 | 305 | context "with a block resolving conflicts" do 306 | before do 307 | container.register(:conflicting_key, "original") 308 | other.register(:conflicting_key, "from other") 309 | end 310 | 311 | it "resolves conflict using provided block" do 312 | container.merge(other) { |_, left, right| left } 313 | 314 | expect(container[:conflicting_key]).to eql("original") 315 | end 316 | end 317 | 318 | context "with a block resolving conflicts with a namespace" do 319 | before do 320 | container.register("items.conflicting_key", "original") 321 | other.register("conflicting_key", "from other") 322 | end 323 | 324 | it "resolves conflict using provided block" do 325 | container.merge(other, namespace: "items") { |_, left, right| left } 326 | 327 | expect(container["items.conflicting_key"]).to eql("original") 328 | end 329 | end 330 | end 331 | 332 | describe "#key?" do 333 | let(:key) { :key } 334 | 335 | before do 336 | container.register(key) { :item } 337 | end 338 | 339 | subject! { container.key?(resolve_key) } 340 | 341 | context "when key exists in container" do 342 | let(:resolve_key) { key } 343 | 344 | it { is_expected.to be true } 345 | end 346 | 347 | context "when key does not exist in container" do 348 | let(:resolve_key) { :random } 349 | 350 | it { is_expected.to be false } 351 | end 352 | end 353 | 354 | describe "#keys" do 355 | let(:keys) { [:key_1, :key_2] } 356 | let(:expected_keys) { %w[key_1 key_2] } 357 | 358 | before do 359 | keys.each do |key| 360 | container.register(key) { :item } 361 | end 362 | end 363 | 364 | subject! { container.keys } 365 | 366 | it "returns stringified versions of all registered keys" do 367 | is_expected.to match_array(expected_keys) 368 | end 369 | end 370 | 371 | describe "#each_key" do 372 | let(:keys) { [:key_1, :key_2] } 373 | let(:expected_keys) { %w[key_1 key_2] } 374 | let!(:yielded_keys) { [] } 375 | 376 | before do 377 | keys.each do |key| 378 | container.register(key) { :item } 379 | end 380 | end 381 | 382 | subject! do 383 | container.each_key { |key| yielded_keys << key } 384 | end 385 | 386 | it "yields stringified versions of all registered keys to the block" do 387 | expect(yielded_keys).to match_array(expected_keys) 388 | end 389 | 390 | it "returns the container" do 391 | is_expected.to eq(container) 392 | end 393 | end 394 | 395 | describe "#each" do 396 | let(:keys) { [:key_1, :key_2] } 397 | let(:expected_key_value_pairs) { [%w[key_1 value_for_key_1], %w[key_2 value_for_key_2]] } 398 | let!(:yielded_key_value_pairs) { [] } 399 | 400 | before do 401 | keys.each do |key| 402 | container.register(key) { "value_for_#{key}" } 403 | end 404 | end 405 | 406 | subject! do 407 | container.each { |key, value| yielded_key_value_pairs << [key, value] } 408 | end 409 | 410 | it "yields stringified versions of all registered keys to the block" do 411 | expect(yielded_key_value_pairs).to match_array(expected_key_value_pairs) 412 | end 413 | 414 | it "returns the container" do 415 | is_expected.to eq(expected_key_value_pairs) 416 | end 417 | end 418 | 419 | describe "#decorate" do 420 | require "delegate" 421 | 422 | let(:key) { :key } 423 | let(:decorated_class_spy) { spy(:decorated_class_spy) } 424 | let(:decorated_class) { Class.new } 425 | 426 | context "for callable item" do 427 | before do 428 | allow(decorated_class_spy).to receive(:new) { decorated_class.new } 429 | container.register(key, memoize: memoize) { decorated_class_spy.new } 430 | container.decorate(key, with: SimpleDelegator) 431 | end 432 | 433 | context "memoize false" do 434 | let(:memoize) { false } 435 | 436 | it "does not call the block until the key is resolved" do 437 | expect(decorated_class_spy).not_to have_received(:new) 438 | container.resolve(key) 439 | expect(decorated_class_spy).to have_received(:new) 440 | end 441 | 442 | specify do 443 | expect(container[key]).to be_instance_of(SimpleDelegator) 444 | expect(container[key].__getobj__).to be_instance_of(decorated_class) 445 | expect(container[key]).not_to be(container[key]) 446 | expect(container[key].__getobj__).not_to be(container[key].__getobj__) 447 | end 448 | end 449 | 450 | context "memoize true" do 451 | let(:memoize) { true } 452 | 453 | specify do 454 | expect(container[key]).to be_instance_of(SimpleDelegator) 455 | expect(container[key].__getobj__).to be_instance_of(decorated_class) 456 | expect(container[key]).to be(container[key]) 457 | end 458 | end 459 | end 460 | 461 | context "for not callable item" do 462 | describe "wrapping" do 463 | before do 464 | container.register(key, call: false) { "value" } 465 | container.decorate(key, with: SimpleDelegator) 466 | end 467 | 468 | it "expected to be an instance of SimpleDelegator" do 469 | expect(container.resolve(key)).to be_instance_of(SimpleDelegator) 470 | expect(container.resolve(key).__getobj__.call).to eql("value") 471 | end 472 | end 473 | 474 | describe "memoization" do 475 | before do 476 | @called = 0 477 | container.register(key, "value") 478 | 479 | container.decorate(key) do |value| 480 | @called += 1 481 | "<#{value}>" 482 | end 483 | end 484 | 485 | it "decorates static value only once" do 486 | expect(container.resolve(key)).to eql("") 487 | expect(container.resolve(key)).to eql("") 488 | expect(@called).to be(1) 489 | end 490 | end 491 | end 492 | 493 | context "with an instance as a decorator" do 494 | let(:decorator) do 495 | double.tap do |decorator| 496 | allow(decorator).to receive(:call) { |input| "decorated #{input}" } 497 | end 498 | end 499 | 500 | before do 501 | container.register(key) { "value" } 502 | container.decorate(key, with: decorator) 503 | end 504 | 505 | it "expected to pass original value to decorator#call method" do 506 | expect(container.resolve(key)).to eq("decorated value") 507 | end 508 | end 509 | end 510 | 511 | describe "namespace" do 512 | context "when block does not take arguments" do 513 | before do 514 | container.namespace("one") do 515 | register("two", 2) 516 | end 517 | end 518 | 519 | subject! { container.resolve("one.two") } 520 | 521 | it "registers items under the given namespace" do 522 | is_expected.to eq(2) 523 | end 524 | end 525 | 526 | context "when block takes arguments" do 527 | before do 528 | container.namespace("one") do |c| 529 | c.register("two", 2) 530 | end 531 | end 532 | 533 | subject! { container.resolve("one.two") } 534 | 535 | it "registers items under the given namespace" do 536 | is_expected.to eq(2) 537 | end 538 | end 539 | 540 | context "with nesting" do 541 | before do 542 | container.namespace("one") do 543 | namespace("two") do 544 | register("three", 3) 545 | end 546 | end 547 | end 548 | 549 | subject! { container.resolve("one.two.three") } 550 | 551 | it "registers items under the given namespaces" do 552 | is_expected.to eq(3) 553 | end 554 | end 555 | 556 | context "with nesting and when block takes arguments" do 557 | before do 558 | container.namespace("one") do |c| 559 | c.register("two", 2) 560 | c.register("three", c.resolve("two")) 561 | end 562 | end 563 | 564 | subject! { container.resolve("one.three") } 565 | 566 | it "resolves items relative to the namespace" do 567 | is_expected.to eq(2) 568 | end 569 | end 570 | end 571 | 572 | describe "import" do 573 | it "allows importing of namespaces" do 574 | ns = Dry::Core::Container::Namespace.new("one") do 575 | register("two", 2) 576 | end 577 | 578 | container.import(ns) 579 | 580 | expect(container.resolve("one.two")).to eq(2) 581 | end 582 | 583 | it "allows importing of nested namespaces" do 584 | ns = Dry::Core::Container::Namespace.new("two") do 585 | register("three", 3) 586 | end 587 | 588 | container.namespace("one") do 589 | import(ns) 590 | end 591 | 592 | expect(container.resolve("one.two.three")).to eq(3) 593 | end 594 | end 595 | end 596 | 597 | describe "stubbing" do 598 | before :all do 599 | require "dry/core/container/stub" 600 | end 601 | 602 | before do 603 | container.enable_stubs! 604 | 605 | container.register(:item, "item") 606 | container.register(:foo, "bar") 607 | end 608 | 609 | after do 610 | container.unstub 611 | end 612 | 613 | it "keys can be stubbed" do 614 | container.stub(:item, "stub") 615 | expect(container.resolve(:item)).to eql("stub") 616 | expect(container[:item]).to eql("stub") 617 | end 618 | 619 | it "only other keys remain accesible" do 620 | container.stub(:item, "stub") 621 | expect(container.resolve(:foo)).to eql("bar") 622 | expect(container[:foo]).to eql("bar") 623 | end 624 | 625 | it "keys can be reverted back to their original value" do 626 | container.stub(:item, "stub") 627 | container.unstub(:item) 628 | 629 | expect(container.resolve(:item)).to eql("item") 630 | expect(container[:item]).to eql("item") 631 | end 632 | 633 | describe "with block argument" do 634 | it "executes the block with the given stubs" do 635 | expect { |b| container.stub(:item, "stub", &b) }.to yield_control 636 | end 637 | 638 | it "keys are stubbed only while inside the block" do 639 | container.stub(:item, "stub") do 640 | expect(container.resolve(:item)).to eql("stub") 641 | end 642 | 643 | expect(container.resolve(:item)).to eql("item") 644 | end 645 | end 646 | 647 | describe "mixing Strings and Symbols" do 648 | it do 649 | container.stub(:item, "stub") 650 | expect(container.resolve("item")).to eql("stub") 651 | end 652 | end 653 | 654 | it "raises an error when key is missing" do 655 | expect { container.stub(:non_existing, "something") } 656 | .to raise_error(ArgumentError, 'cannot stub "non_existing" - no such key in container') 657 | end 658 | end 659 | 660 | describe ".freeze" do 661 | before do 662 | container.register(:foo, "bar") 663 | end 664 | 665 | it "allows to freeze a container so that nothing can be registered later" do 666 | container.freeze 667 | expect { container.register(:baz, "quux") }.to raise_error(FrozenError) 668 | expect(container).to be_frozen 669 | end 670 | 671 | it "wraps FrozenError to provide which key was attempted to be registered" do 672 | container.freeze 673 | expect { container.register(:baz, "quux") } 674 | .to raise_error( 675 | FrozenError, 676 | /can't modify frozen \S+ \(when attempting to register 'baz'\)/ 677 | ) 678 | end 679 | 680 | it "returns self back" do 681 | expect(container.freeze).to be(container) 682 | end 683 | end 684 | 685 | describe ".dup" do 686 | it "returns a copy that doesn't share registered keys with the parent" do 687 | container.dup.register(:foo, "bar") 688 | expect(container.key?(:foo)).to be false 689 | end 690 | end 691 | 692 | describe ".clone" do 693 | it "returns a copy that doesn't share registered keys with the parent" do 694 | container.clone.register(:foo, "bar") 695 | expect(container.key?(:foo)).to be false 696 | end 697 | 698 | it "re-uses frozen container" do 699 | expect(container.freeze.clone).to be_frozen 700 | expect(container.clone._container).to be(container._container) 701 | end 702 | end 703 | 704 | describe ".resolve" do 705 | it "accepts a fallback block" do 706 | expect(container.resolve("missing") { :fallback }).to be(:fallback) 707 | end 708 | end 709 | end 710 | -------------------------------------------------------------------------------- /spec/support/shared_examples/memoizable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.shared_examples "a memoizable class" do 4 | subject(:object) do 5 | Class.new(described_class) do 6 | include Dry::Core::Memoizable 7 | 8 | attr_reader :falsey_call_count 9 | 10 | def initialize 11 | super 12 | @falsey_call_count = 0 13 | end 14 | 15 | def foo 16 | %w[a ab abc].max 17 | end 18 | memoize :foo 19 | 20 | def bar(_arg) 21 | {a: "1", b: "2"} 22 | end 23 | memoize :bar 24 | 25 | def bar_with_block(&block) 26 | block.call 27 | end 28 | memoize :bar_with_block 29 | 30 | def bar_with_kwargs(*args, **kwargs) 31 | {args: args, kwargs: kwargs} 32 | end 33 | memoize :bar_with_kwargs 34 | 35 | def falsey 36 | @falsey_call_count += 1 37 | false 38 | end 39 | memoize :falsey 40 | end.new 41 | end 42 | 43 | it "memoizes method return value" do 44 | expect(object.foo).to be(object.foo) 45 | end 46 | 47 | it "memoizes method return value with an arg" do 48 | expect(object.bar(:a)).to be(object.bar(:a)) 49 | expect(object.bar(:b)).to be(object.bar(:b)) 50 | end 51 | 52 | it "memoizes falsey values" do 53 | expect(object.falsey).to be(object.falsey) 54 | expect(object.falsey_call_count).to eq 1 55 | end 56 | 57 | describe "keyword arguments" do 58 | let(:kwargs) { {key: "value"} } 59 | let(:args) { [1] } 60 | 61 | it "memoizes keyword arguments" do 62 | expect(object.bar_with_kwargs(*args, **kwargs)).to eq({args: args, kwargs: kwargs}) 63 | end 64 | end 65 | 66 | describe "with block" do 67 | let(:spy1) { double(:spy1) } 68 | let(:spy2) { double(:spy2) } 69 | let(:block1) { -> { spy1.call } } 70 | let(:block2) { -> { spy2.call } } 71 | let(:returns1) { :return_value1 } 72 | let(:returns2) { :return_value2 } 73 | 74 | before do 75 | expect(spy1).to receive(:call).and_return(returns1).once 76 | expect(spy2).to receive(:call).and_return(returns2).once 77 | end 78 | 79 | let(:results1) do 80 | 2.times.map do 81 | object.bar_with_block(&block1) 82 | end 83 | end 84 | 85 | let(:results2) do 86 | 2.times.map do 87 | object.bar_with_block(&block2) 88 | end 89 | end 90 | 91 | let(:returns) do 92 | [returns1, returns2] * 2 93 | end 94 | 95 | subject do 96 | results1 + results2 97 | end 98 | 99 | it { is_expected.to match_array(returns) } 100 | end 101 | end 102 | 103 | RSpec.shared_examples "a memoized method" do 104 | let(:old_meth) { described_class.class.instance_method(new_meth.name) } 105 | 106 | describe "new != old" do 107 | subject { new_meth } 108 | it { is_expected.not_to eq(old_meth) } 109 | end 110 | 111 | describe "new.arity == old.arity" do 112 | subject { new_meth.arity } 113 | it { is_expected.to eq(old_meth.arity) } 114 | end 115 | 116 | describe "new.name == old.name" do 117 | subject { new_meth.name } 118 | it { is_expected.to eq(old_meth.name) } 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /spec/support/warnings.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # this file is managed by dry-rb/devtools project 4 | 5 | require "warning" 6 | 7 | Warning.ignore(%r{rspec/core}) 8 | Warning.ignore(%r{rspec/mocks}) 9 | Warning.ignore(/codacy/) 10 | Warning[:experimental] = false if Warning.respond_to?(:[]) 11 | --------------------------------------------------------------------------------