├── .devtools └── templates │ ├── changelog.erb │ └── release.erb ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug-report.md │ └── config.yml ├── SUPPORT.md └── workflows │ ├── ci.yml │ ├── custom │ └── ci.yml │ ├── docsite.yml │ ├── rubocop.yml │ └── sync_configs.yml ├── .gitignore ├── .repobot.yml ├── .rspec ├── .rubocop.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Gemfile ├── Gemfile.devtools ├── Guardfile ├── LICENSE ├── README.md ├── Rakefile ├── benchmarks ├── compare_several_defaults.rb ├── plain_options.rb ├── plain_params.rb ├── with_coercion.rb ├── with_defaults.rb └── with_defaults_and_coercion.rb ├── bin ├── .gitkeep └── console ├── changelog.yml ├── docsite └── source │ ├── attributes.html.md │ ├── container-version.html.md │ ├── index.html.md │ ├── inheritance.html.md │ ├── optionals-and-defaults.html.md │ ├── params-and-options.html.md │ ├── rails-support.html.md │ ├── readers.html.md │ ├── skip-undefined.html.md │ ├── tolerance-to-unknown-arguments.html.md │ └── type-constraints.html.md ├── dry-initializer.gemspec ├── lib ├── dry-initializer.rb ├── dry │ ├── initializer.rb │ └── initializer │ │ ├── builders.rb │ │ ├── builders │ │ ├── attribute.rb │ │ ├── initializer.rb │ │ ├── reader.rb │ │ └── signature.rb │ │ ├── config.rb │ │ ├── definition.rb │ │ ├── dispatchers.rb │ │ ├── dispatchers │ │ ├── build_nested_type.rb │ │ ├── check_type.rb │ │ ├── prepare_default.rb │ │ ├── prepare_ivar.rb │ │ ├── prepare_optional.rb │ │ ├── prepare_reader.rb │ │ ├── prepare_source.rb │ │ ├── prepare_target.rb │ │ ├── unwrap_type.rb │ │ └── wrap_type.rb │ │ ├── dsl.rb │ │ ├── mixin.rb │ │ ├── mixin │ │ ├── local.rb │ │ └── root.rb │ │ ├── struct.rb │ │ ├── undefined.rb │ │ └── version.rb └── tasks │ ├── benchmark.rake │ └── profile.rake ├── project.yml └── spec ├── attributes_spec.rb ├── coercion_of_nil_spec.rb ├── custom_dispatchers_spec.rb ├── custom_initializer_spec.rb ├── default_values_spec.rb ├── definition_spec.rb ├── invalid_default_spec.rb ├── list_type_spec.rb ├── missed_default_spec.rb ├── nested_type_spec.rb ├── optional_spec.rb ├── options_tolerance_spec.rb ├── public_attributes_utility_spec.rb ├── reader_spec.rb ├── repetitive_definitions_spec.rb ├── several_assignments_spec.rb ├── spec_helper.rb ├── subclassing_spec.rb ├── support ├── coverage.rb ├── rspec_options.rb └── warnings.rb ├── type_argument_spec.rb ├── type_constraint_spec.rb └── value_coercion_via_dry_types_spec.rb /.devtools/templates/changelog.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <% releases.each_with_index do |r, idx| %> 5 | ## <%= r.version %> <%= r.date %> 6 | 7 | <% if r.summary %> 8 | <%= r.summary %> 9 | 10 | <% end %> 11 | 12 | <% if r.added? %> 13 | ### Added 14 | 15 | <% r.added.each do |log| %> 16 | - <%= log %> 17 | <% end %> 18 | 19 | <% end %> 20 | <% if r.fixed? %> 21 | ### Fixed 22 | 23 | <% r.fixed.each do |log| %> 24 | - <%= log %> 25 | <% end %> 26 | 27 | <% end %> 28 | <% if r.changed? %> 29 | ### Changed 30 | 31 | <% r.changed.each do |log| %> 32 | - <%= log %> 33 | <% end %> 34 | <% end %> 35 | <% curr_ver = r.date ? "v#{r.version}" : 'master' %> 36 | <% prev_rel = releases[idx + 1] %> 37 | <% if prev_rel %> 38 | <% ver_range = "v#{prev_rel.version}...#{curr_ver}" %> 39 | 40 | [Compare <%=ver_range%>](https://github.com/dry-rb/<%= project.name %>/compare/<%=ver_range%>) 41 | <% end %> 42 | 43 | <% end %> 44 | -------------------------------------------------------------------------------- /.devtools/templates/release.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <% if latest_release.summary %> 5 | <%= latest_release.summary %> 6 | 7 | <% end %> 8 | 9 | <% if latest_release.added? %> 10 | ### Added 11 | 12 | <% latest_release.added.each do |log| %> 13 | - <%= log %> 14 | <% end %> 15 | 16 | <% end %> 17 | <% if latest_release.fixed? %> 18 | ### Fixed 19 | 20 | <% latest_release.fixed.each do |log| %> 21 | - <%= log %> 22 | <% end %> 23 | 24 | <% end %> 25 | <% if latest_release.changed? %> 26 | ### Changed 27 | 28 | <% latest_release.changed.each do |log| %> 29 | - <%= log %> 30 | <% end %> 31 | <% end %> 32 | <% if previous_release %> 33 | <% ver_range = "v#{previous_release.version}...v#{latest_release.version}" %> 34 | 35 | [Compare <%=ver_range%>](https://github.com/dry-rb/<%= project.name %>/compare/<%=ver_range%>) 36 | <% end %> 37 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: hanami 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41B Bug report" 3 | about: See CONTRIBUTING.md for more information 4 | title: '' 5 | labels: bug, help wanted 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Describe the bug 11 | 12 | A clear and concise description of what the bug is. 13 | 14 | ## To Reproduce 15 | 16 | Provide detailed steps to reproduce, **an executable script would be best**. 17 | 18 | ## Expected behavior 19 | 20 | A clear and concise description of what you expected to happen. 21 | 22 | ## My environment 23 | 24 | - Affects my production application: **YES/NO** 25 | - Ruby version: ... 26 | - OS: ... 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Community Support 4 | url: https://discourse.dry-rb.org 5 | about: Please ask and answer questions here. 6 | -------------------------------------------------------------------------------- /.github/SUPPORT.md: -------------------------------------------------------------------------------- 1 | ## Support 2 | 3 | If you need help with any of the dry-rb libraries, feel free to ask questions on our [discussion forum](https://discourse.dry-rb.org/). This is the best place to seek help. Make sure to search for a potential solution in past threads before posting your question. Thanks! :heart: 4 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This file is synced from dry-rb/template-gem repo 2 | name: CI 3 | 4 | on: 5 | push: 6 | paths: 7 | - ".github/workflows/ci.yml" 8 | - "lib/**" 9 | - "*.gemspec" 10 | - "spec/**" 11 | - "Rakefile" 12 | - "Gemfile" 13 | - "Gemfile.devtools" 14 | - ".rubocop.yml" 15 | - "project.yml" 16 | pull_request: 17 | branches: 18 | - main 19 | create: 20 | 21 | jobs: 22 | tests: 23 | runs-on: ubuntu-latest 24 | name: Tests 25 | strategy: 26 | fail-fast: false 27 | matrix: 28 | ruby: 29 | - "3.3" 30 | - "3.2" 31 | - "3.1" 32 | include: 33 | - ruby: "3.4" 34 | coverage: "true" 35 | env: 36 | COVERAGE: ${{matrix.coverage}} 37 | COVERAGE_TOKEN: ${{secrets.CODACY_PROJECT_TOKEN}} 38 | steps: 39 | - name: Checkout 40 | uses: actions/checkout@v3 41 | - name: Install package dependencies 42 | run: "[ -e $APT_DEPS ] || sudo apt-get install -y --no-install-recommends $APT_DEPS" 43 | - name: Set up Ruby 44 | uses: ruby/setup-ruby@v1 45 | with: 46 | ruby-version: ${{matrix.ruby}} 47 | bundler-cache: true 48 | - name: Run all tests 49 | run: bundle exec rake 50 | release: 51 | runs-on: ubuntu-latest 52 | if: contains(github.ref, 'tags') && github.event_name == 'create' 53 | needs: tests 54 | env: 55 | GITHUB_LOGIN: dry-bot 56 | GITHUB_TOKEN: ${{secrets.GH_PAT}} 57 | steps: 58 | - uses: actions/checkout@v3 59 | - name: Install package dependencies 60 | run: "[ -e $APT_DEPS ] || sudo apt-get install -y --no-install-recommends $APT_DEPS" 61 | - name: Set up Ruby 62 | uses: ruby/setup-ruby@v1 63 | with: 64 | ruby-version: 3.1 65 | - name: Install dependencies 66 | run: gem install ossy --no-document 67 | - name: Trigger release workflow 68 | run: | 69 | tag=$(echo $GITHUB_REF | cut -d / -f 3) 70 | 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}}\"}" 71 | -------------------------------------------------------------------------------- /.github/workflows/custom/ci.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | tests: 3 | strategy: 4 | matrix: 5 | runtime_deps: 6 | - "dry-types-latest" 7 | - "dry-types-master" 8 | include: 9 | - runtime_deps: "dry-types-master" 10 | dry_types_from_master: "true" 11 | env: 12 | DRY_TYPES_FROM_MASTER: ${{matrix.dry_types_from_master}} 13 | -------------------------------------------------------------------------------- /.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 | /*.gem 11 | 12 | .vscode 13 | -------------------------------------------------------------------------------- /.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-initializer.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: disable 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 | ## 3.2.0 2025-01-01 4 | 5 | 6 | ### Changed 7 | 8 | - Set minimal supported Ruby version to 3.1 (@flash-gordon) 9 | - Exclude block forwarding from `Root#initialize`. This helps 10 | with tracking down calls that shouldn't pass a block in Ruby 3.4 (see #109) (@flash-gordon) 11 | 12 | 13 | [Compare v3.1.1...v3.2.0](https://github.com/dry-rb/dry-initializer/compare/v3.1.1...v3.2.0) 14 | 15 | ## 3.1.1 2022-01-19 16 | 17 | 18 | ### Changed 19 | 20 | - Improved error messages were rolled back, they created an implicit dependency on dry-types (@flash-gordon) 21 | 22 | [Compare v3.1.0...v3.1.1](https://github.com/dry-rb/dry-initializer/compare/v3.1.0...v3.1.1) 23 | 24 | ## 3.1.0 2022-01-16 25 | 26 | 27 | ### Changed 28 | 29 | - Improved error messages on type mismatch (@swerling) 30 | - [BREAKING] Minimal supported Ruby version is 2.7 (@flash-gordon) 31 | 32 | [Compare v3.0.4...v3.1.0](https://github.com/dry-rb/dry-initializer/compare/v3.0.4...v3.1.0) 33 | 34 | ## 3.0.4 2020-09-29 35 | 36 | 37 | ### Fixed 38 | 39 | - Arity check for lambdas used for coercion (@flash-gordon) 40 | 41 | 42 | [Compare v3.0.3...v3.0.4](https://github.com/dry-rb/dry-initializer/compare/v3.0.3...v3.0.4) 43 | 44 | ## 3.0.3 2020-01-08 45 | 46 | 47 | ### Fixed 48 | 49 | - Constrained member arrays work correctly now (see #33) (@bjeanes + @solnic) 50 | 51 | 52 | [Compare v3.0.2...v3.0.3](https://github.com/dry-rb/dry-initializer/compare/v3.0.2...v3.0.3) 53 | 54 | ## 3.0.2 2019-11-07 55 | 56 | 57 | ### Fixed 58 | 59 | - Warnings about keyword arguments (flash-gordon) 60 | 61 | 62 | [Compare v3.0.1...v3.0.2](https://github.com/dry-rb/dry-initializer/compare/v3.0.1...v3.0.2) 63 | 64 | ## 3.0.1 2019-04-15 65 | 66 | 67 | ### Fixed 68 | 69 | - Usage of underscored names of `option`-s and `param`-s (nepalez) 70 | 71 | You can use any sequence of underscores except for in nested types. 72 | In nested types single underscores can be used to split alphanumeric 73 | parts only. 74 | 75 | ```ruby 76 | class Test 77 | extend Dry::Initializer 78 | 79 | # Proper usage 80 | option :foo_bar do 81 | option :__foo__, proc(&:to_s) 82 | end 83 | end 84 | 85 | # Improper usage 86 | option :__foo__ do 87 | # ... 88 | end 89 | 90 | option :foo__bar do 91 | # ... 92 | end 93 | end 94 | ``` 95 | 96 | This restriction is necessary because we constantize option/param names 97 | when defining nested structs. 98 | 99 | 100 | [Compare v3.0.0...v3.0.1](https://github.com/dry-rb/dry-initializer/compare/v3.0.0...v3.0.1) 101 | 102 | ## 3.0.0 2019-04-14 103 | 104 | 105 | ### Added 106 | 107 | - Support of wrapped types/coercers (nepalez) 108 | 109 | ```ruby 110 | class Test 111 | # Wrap type to the array 112 | param :foo, [proc(&:to_s)] 113 | end 114 | 115 | # And the value will be wrapped as well 116 | test = Test.new(42) 117 | test.foo # => ["42"] 118 | ``` 119 | - It works with several layers of nesting (nepalez) 120 | 121 | ```ruby 122 | class Test 123 | # Wrap type to the array 124 | param :foo, [[proc(&:to_s)]] 125 | end 126 | 127 | # And the value will be wrapped as well 128 | test = Test.new(42) 129 | test.foo # => [["42"]] 130 | ``` 131 | - Support of nested types/coercers (nepalez) 132 | 133 | ```ruby 134 | class Test 135 | param :foo do 136 | option :bar do 137 | option :baz, proc(&:to_s) 138 | end 139 | end 140 | end 141 | 142 | test = Test.new(bar: { "baz" => 42 }) 143 | test.foo.bar.baz # => "42" 144 | ``` 145 | - Wrapped/nested combinations are supported as well (nepalez) 146 | 147 | ```ruby 148 | class Test 149 | param :foo, [] do 150 | option :bar, proc(&:to_s) 151 | end 152 | end 153 | 154 | test = Test.new(bar: 42) 155 | test.foo.first.bar # => "42" 156 | ``` 157 | - ## [2.7.0] Unreleazed 158 | 159 | ### Fixed 160 | 161 | - Roll back master to the state of [2.5.0]. 162 | 163 | Somehow distinction between `@default_null` and `@null` variables 164 | in the `Dry::Initializer::Builders` broken the `rom` library. 165 | 166 | The version [2.6.0] has been yanked on rubygems, so the master 167 | was rolled back to the previous state until the reason for 168 | the incompatibility become clear (bjeanes, nepalez) 169 | - ## [2.6.0] [2018-09-09] (YANKED) 170 | 171 | 172 | [Compare v2.5.0...v3.0.0](https://github.com/dry-rb/dry-initializer/compare/v2.5.0...v3.0.0) 173 | 174 | ## 2.5.0 2018-08-17 175 | 176 | 177 | ### Fixed 178 | 179 | - `nil` coercion (belousovAV) 180 | 181 | When default value is `nil` instead of `Dry::Initializer::UNDEFINED`, 182 | the coercion should be applied to any value, including `nil`, because 183 | we cannot distinct "undefined" `nil` from the "assigned" `nil` value. 184 | 185 | 186 | [Compare v2.4.0...v2.5.0](https://github.com/dry-rb/dry-initializer/compare/v2.4.0...v2.5.0) 187 | 188 | ## 2.4.0 2018-02-01 189 | 190 | 191 | ### Added 192 | 193 | - Dispatchers for adding syntax sugar to `param` and `options` (nepalez) 194 | 195 | ```ruby 196 | # Converts `integer: true` to `type: proc(&:to_i)` 197 | dispatcher = ->(op) { op[:integer] ? op.merge(type: proc(&:to_i)) : op } 198 | # Register a dispatcher 199 | Dry::Initializer::Dispatchers << dispatcher 200 | # Use syntax sugar 201 | class User 202 | param :id, integer: true # same as param :id, proc(&:to_i) 203 | end 204 | ``` 205 | 206 | 207 | [Compare v2.3.0...v2.4.0](https://github.com/dry-rb/dry-initializer/compare/v2.3.0...v2.4.0) 208 | 209 | ## 2.3.0 2017-09-19 210 | 211 | 212 | ### Added 213 | 214 | - Type coercer can take second argument for the initialized instance (nepalez) 215 | This allows to wrap assigned value to the object that refers back 216 | to the initializer instance. More verbose example: 217 | 218 | ```ruby 219 | class Location < String 220 | attr_reader :parameter # refers back to its parameter 221 | 222 | def initialize(name, parameter) 223 | super(name) 224 | @parameter = parameter 225 | end 226 | end 227 | 228 | class Parameter 229 | extend Dry::Initializer 230 | param :name 231 | param :location, ->(value, param) { Location.new(value, param) } 232 | end 233 | 234 | offset = Parameter.new "offset", location: "query" 235 | offset.name # => "offset" 236 | offset.location # => "query" 237 | offset.location.parameter == offset # true 238 | ``` 239 | 240 | 241 | [Compare v2.2.0...v2.3.0](https://github.com/dry-rb/dry-initializer/compare/v2.2.0...v2.3.0) 242 | 243 | ## 2.2.0 2017-09-13 244 | 245 | 246 | ### Added 247 | 248 | - Option `:desc` for option/param to add a description (nepalez) 249 | - Methods `Definition#inch` and `Config#inch` to inspect definitions (nepalez) 250 | 251 | ```ruby 252 | class User 253 | extend Dry::Initializer 254 | option :name, proc(&:to_s), optional: true, desc: "User name" 255 | option :email, optional: true, desc: "user email" 256 | end 257 | 258 | User.dry_initializer.inch 259 | # @!method initialize(*, **options) 260 | # Initializes an instance of User 261 | # @option [Object] :name (optional) User name 262 | # @option [Object] :email (optional) User email 263 | # @return [User] 264 | ``` 265 | 266 | 267 | [Compare v2.1.0...v2.2.0](https://github.com/dry-rb/dry-initializer/compare/v2.1.0...v2.2.0) 268 | 269 | ## 2.1.0 2017-09-11 270 | 271 | 272 | ### Added 273 | 274 | - Method `#options` to param/option definition (nepalez) 275 | 276 | ```ruby 277 | class User 278 | extend Dry::Initializer 279 | option :name, proc(&:to_s), optional: true 280 | option :email, optional: true 281 | end 282 | 283 | User.dry_initializer.options.map do |option| 284 | [option.source, option.options] 285 | end 286 | # => [ 287 | # [:name, { type: proc(&:to_s), as: :name, optional: true }], 288 | # [:email, { as: :email, optional: true }] 289 | # ] 290 | ``` 291 | 292 | This method can be helpful for replicating params/options 293 | in another class without inheritance. 294 | 295 | 296 | [Compare v2.0.0...v2.1.0](https://github.com/dry-rb/dry-initializer/compare/v2.0.0...v2.1.0) 297 | 298 | ## 2.0.0 2017-08-28 299 | 300 | and to @gzigzigzeo for persuading me to do this refactoring. 301 | 302 | ### Added 303 | 304 | - Class method `.dry_initializer` -- a container for `.params` and `.options` 305 | `.definitions` along with the `.null` setting (either `nil` or `UNDEFINED`) 306 | used for unassigned values (nepalez) 307 | - `.dry_initializer.attributes` method takes an instance of the same class 308 | and returns the hash of assigned options. This provide the same 309 | functionality as previously used instance variable `@__options__` (nepalez) 310 | 311 | ```ruby 312 | object.class.dry_initializer.attributes(object) 313 | ``` 314 | 315 | When you use "Dry::Initializer.define -> { ... }" syntax, 316 | the class method `.dry_initializer` is not defined. To access attributes 317 | you should use private instance method `#__dry_initializer_config__` instead: 318 | 319 | ```ruby 320 | object.send(:__dry_initializer_config__).attributes(object) 321 | ``` 322 | 323 | Both methods `.dry_initializer` and `#__dry_initializer_config__` refer 324 | to the same object. 325 | - `.dry_initializer.public_attributes`. This method works differently: 326 | it looks through (possibly reloaded) readers instead of variables 327 | (gzigzigzeo, nepalez) 328 | 329 | ```ruby 330 | object.class.dry_initializer.public_attributes(object) 331 | ``` 332 | 333 | You can use the same trick as above mutatis mutandis. 334 | 335 | ### Fixed 336 | 337 | - Definition order dependency bug (nepalez) 338 | 339 | I've found out that if you provided a subclass and then changed params 340 | or options of its superclass, these changes woudn't be reflected in 341 | subclasses until you change any of it params/options as well. 342 | 343 | Now this bug is fixed: every time you call `param` or `option` at 344 | any class, the gem scans through all its descendants to the very bottom 345 | of the tree, and reloads their defintitions. 346 | 347 | Being done in load time, the rebuilt makes no effect on runtime performance. 348 | - Possible misbehavior when you define param and option with the same name (nepalez) 349 | 350 | Doing this will provide `option :name` only, not both: 351 | 352 | ```ruby 353 | param :name 354 | option :name 355 | ``` 356 | - Attempt to redefine param/option of superclass with option/param in 357 | its subclass will cause an exception because it would break 358 | Liskov substitute principle with unexpected behaviour (nepalez) 359 | 360 | No, you can do neither these definitions, nor vice versa: 361 | 362 | ```ruby 363 | class Foo 364 | extend Dry::Intitializer 365 | param :name 366 | end 367 | 368 | class Bar < Foo 369 | option :name 370 | end 371 | ``` 372 | - When you reloading previously defined param of superclass, the gem 373 | will check all its descendands for whether all required positional params 374 | goes before optional ones (nepalez) 375 | 376 | ```ruby 377 | class Foo 378 | param :name 379 | # Foo: def initializer(name) 380 | end 381 | 382 | class Bar 383 | param :email 384 | # Bar: def initializer(name, email) 385 | end 386 | 387 | class Foo 388 | # This raises SyntaxError because in Bar this would cause wrong definition 389 | # Foo: def initializer(name = nil) 390 | # Bar: def initializer(name = nil, email) 391 | param :name, optional: true 392 | end 393 | ``` 394 | 395 | ### Changed 396 | 397 | - Under the hood I've separated param/option settings declaration (a container 398 | with param/option settings) from code builders for initializer and readers 399 | (nepalez) 400 | 401 | You can check both the code for the `__initializer__`: 402 | 403 | ```ruby 404 | class Foo 405 | extend Dry::Initializer 406 | # ... 407 | end 408 | 409 | Foo.dry_initializer.code 410 | ``` 411 | 412 | and readers: 413 | 414 | ```ruby 415 | Foo.dry_initializer.params.map(&:code) 416 | Foo.dry_initializer.options.map(&:code) 417 | 418 | # or 419 | 420 | Foo.dry_initializer.definitions.values.map(&:code) 421 | ``` 422 | 423 | You can also check settings for every param and option using methods 424 | `dry_initializer.params`, `dry_initializer.options` (lists), or 425 | `dry_initializer.definitions` (hash). 426 | 427 | You can check null value via `.dry_initializer.null` which is different 428 | for `Dry::Initializer` and `Dry::Initializer[undefined: false]` modules. 429 | - Optimized the code for `__initializer__`-s (the method where all magics occurs) 430 | (nepalez) 431 | 432 | Benchmarks remained about the same: 433 | 434 | ```shell 435 | rake benchmark 436 | ``` 437 | 438 | ``` 439 | Benchmark for instantiation with plain params 440 | value_struct: 4317196.9 i/s 441 | plain Ruby: 4129803.9 i/s - 1.05x slower 442 | dry-initializer: 1710702.1 i/s - 2.52x slower 443 | concord: 1372630.4 i/s - 3.15x slower 444 | values: 601651.8 i/s - 7.18x slower 445 | attr_extras: 535599.5 i/s - 8.06x slower 446 | ``` 447 | 448 | ``` 449 | Benchmark for instantiation with plain options 450 | plain Ruby: 1769174.1 i/s 451 | dry-initializer: 636634.1 i/s - 2.78x slower 452 | kwattr: 423296.5 i/s - 4.18x slower 453 | anima: 399415.0 i/s - 4.43x slower 454 | ``` 455 | 456 | ``` 457 | Benchmark for instantiation with coercion 458 | plain Ruby: 1565501.0 i/s 459 | fast_attributes: 569952.9 i/s - 2.75x slower 460 | dry-initializer: 461122.1 i/s - 3.39x slower 461 | virtus: 138074.8 i/s - 11.34x slower 462 | ``` 463 | 464 | ``` 465 | Benchmark for instantiation with default values 466 | plain Ruby: 3402455.4 i/s 467 | kwattr: 586206.5 i/s - 5.80x slower 468 | dry-initializer: 528482.2 i/s - 6.44x slower 469 | active_attr: 298697.7 i/s - 11.39x slower 470 | ``` 471 | 472 | ``` 473 | Benchmark for instantiation with type constraints and default values 474 | plain Ruby: 2881696.1 i/s 475 | dry-initializer: 470815.1 i/s - 6.12x slower 476 | virtus: 180272.6 i/s - 15.99x slower 477 | ``` 478 | 479 | [Compare v1.4.1...v2.0.0](https://github.com/dry-rb/dry-initializer/compare/v1.4.1...v2.0.0) 480 | 481 | ## 1.4.1 2017-04-05 482 | 483 | 484 | ### Fixed 485 | 486 | - Warning about redefined `#initialize` in case the method reloaded in a klass 487 | that extends the module (nepalez, sergey-chechaev) 488 | 489 | ### Changed 490 | 491 | - Rename `Dry::Initializer::DSL` -> `Dry::Initializer::ClassDSL` (nepalez) 492 | - Add `Dry::Initializer::InstanceDSL` (nepalez) 493 | 494 | [Compare v1.4.0...v1.4.1](https://github.com/dry-rb/dry-initializer/compare/v1.4.0...v1.4.1) 495 | 496 | ## 1.4.0 2017-03-08 497 | 498 | 499 | ### Changed 500 | 501 | - The `@__options__` hash now collects all assigned attributes, 502 | collected via `#option` (as before), and `#param` (nepalez) 503 | 504 | [Compare v1.3.0...v1.4.0](https://github.com/dry-rb/dry-initializer/compare/v1.3.0...v1.4.0) 505 | 506 | ## 1.3.0 2017-03-05 507 | 508 | 509 | ### Added 510 | 511 | - No-undefined configuration of the initializer (nepalez, flash-gordon) 512 | 513 | You can either extend or include module `Dry::Initializer` with additional option 514 | `[undefined: false]`. This time `nil` will be assigned instead of 515 | `Dry::Initializer::UNDEFINED`. Readers becomes faster because there is no need 516 | to chech whether a variable was defined or not. At the same time the initializer 517 | doesn't distinct cases when a variable was set to `nil` explicitly, and when it wasn's set at all: 518 | 519 | class Foo # old behavior 520 | extend Dry::Initializer 521 | param :qux, optional: true 522 | end 523 | 524 | class Bar # new behavior 525 | extend Dry::Initializer[undefined: false] 526 | param :qux, optional: true 527 | end 528 | 529 | Foo.new.instance_variable_get(:@qux) # => Dry::Initializer::UNDEFINED 530 | Bar.new.instance_variable_get(:@qux) # => nil 531 | 532 | ### Changed 533 | 534 | - Fixed method definitions for performance at the load time (nepalez, flash-gordon) 535 | 536 | [Compare v1.2.0...v1.3.0](https://github.com/dry-rb/dry-initializer/compare/v1.2.0...v1.3.0) 537 | 538 | ## 1.2.0 2017-03-05 539 | 540 | 541 | ### Fixed 542 | 543 | - The `@__options__` variable collects renamed options after default values and coercions were applied (nepalez) 544 | 545 | 546 | [Compare v1.1.3...v1.2.0](https://github.com/dry-rb/dry-initializer/compare/v1.1.3...v1.2.0) 547 | 548 | ## 1.1.3 2017-03-01 549 | 550 | 551 | ### Added 552 | 553 | - Support for lambdas as default values (nepalez, gzigzigzeo) 554 | 555 | 556 | [Compare v1.1.2...v1.1.3](https://github.com/dry-rb/dry-initializer/compare/v1.1.2...v1.1.3) 557 | 558 | ## 1.1.2 2017-02-06 559 | 560 | 561 | ### Changed 562 | 563 | - Remove previously defined methods before redefining them (flash-gordon) 564 | 565 | [Compare v1.1.1...v1.1.2](https://github.com/dry-rb/dry-initializer/compare/v1.1.1...v1.1.2) 566 | 567 | ## 1.1.1 2017-02-04 568 | 569 | 570 | ### Fixed 571 | 572 | - `@__options__` collects defined options only (nepalez) 573 | 574 | 575 | [Compare v1.1.0...v1.1.1](https://github.com/dry-rb/dry-initializer/compare/v1.1.0...v1.1.1) 576 | 577 | ## 1.1.0 2017-01-28 578 | 579 | 580 | ### Added 581 | 582 | - enhancement via `Dry::Initializer::Attribute.dispatchers` registry (nepalez) 583 | 584 | ```ruby 585 | # Register dispatcher for `:string` option 586 | Dry::Initializer::Attribute.dispatchers << ->(string: nil, **op) do 587 | string ? op.merge(type: proc(&:to_s)) : op 588 | end 589 | 590 | # Now you can use the `:string` key for `param` and `option` 591 | class User 592 | extend Dry::Initializer 593 | param :name, string: true 594 | end 595 | 596 | User.new(:Andy).name # => "Andy" 597 | ``` 598 | 599 | ### Changed 600 | 601 | - optimize assignments for performance (nepalez) 602 | 603 | [Compare v1.0.0...v1.1.0](https://github.com/dry-rb/dry-initializer/compare/v1.0.0...v1.1.0) 604 | 605 | ## 1.0.0 2017-01-22 606 | 607 | In this version the code has been rewritten for simplicity 608 | 609 | ### Added 610 | 611 | - support for reloading `param` and `option` definitions (nepalez) 612 | 613 | class User 614 | extend Dry::Initializer 615 | param :name 616 | param :phone, optional: true 617 | end 618 | 619 | User.new # => Boom! 620 | 621 | class Admin < User 622 | param :name, default: proc { 'Merlin' } 623 | end 624 | 625 | # order of the param not changed 626 | Admin.new.name # => "Merlin" 627 | - support for assignment of attributes via several options (nepalez) 628 | 629 | class User 630 | extend Dry::Initializer 631 | option :phone 632 | option :number, as: :phone 633 | end 634 | 635 | # Both ways provide the same result 636 | User.new(phone: '1234567890').phone # => '1234567890' 637 | User.new(number: '1234567890').phone # => '1234567890' 638 | 639 | ### Changed 640 | 641 | - [BREAKING] when `param` or `option` was not defined, the corresponding **variable** is set to `Dry::Initializer::UNDEFINED`, but the **reader** (when defined) will return `nil` (nepalez) 642 | - `Dry::Initializer` and `Dry::Initializer::Mixin` became aliases (nepalez) 643 | 644 | [Compare v0.11.0...v1.0.0](https://github.com/dry-rb/dry-initializer/compare/v0.11.0...v1.0.0) 645 | 646 | ## 0.11.0 2017-01-02 647 | 648 | 649 | ### Added 650 | 651 | - Support of reloading `#initializer` with `super` (nepalez) 652 | 653 | 654 | [Compare v0.10.2...v0.11.0](https://github.com/dry-rb/dry-initializer/compare/v0.10.2...v0.11.0) 655 | 656 | ## 0.10.2 2016-12-31 657 | 658 | 659 | ### Added 660 | 661 | - Support of Ruby 2.4 (flas-gordon) 662 | 663 | 664 | [Compare v0.10.1...v0.10.2](https://github.com/dry-rb/dry-initializer/compare/v0.10.1...v0.10.2) 665 | 666 | ## 0.10.1 2016-12-27 667 | 668 | 669 | ### Fixed 670 | 671 | - Wrong arity when there were no options and the last param had a default (nolith) 672 | 673 | 674 | [Compare v0.10.0...v0.10.1](https://github.com/dry-rb/dry-initializer/compare/v0.10.0...v0.10.1) 675 | 676 | ## 0.10.0 2016-11-20 677 | 678 | 679 | 680 | [Compare v0.9.3...v0.10.0](https://github.com/dry-rb/dry-initializer/compare/v0.9.3...v0.10.0) 681 | 682 | ## 0.9.3 2016-11-20 683 | 684 | 685 | ### Fixed 686 | 687 | - Support of weird option names (nepalez) 688 | 689 | ```ruby 690 | option :"First name", as: :first_name 691 | ``` 692 | 693 | 694 | [Compare v0.9.2...v0.9.3](https://github.com/dry-rb/dry-initializer/compare/v0.9.2...v0.9.3) 695 | 696 | ## 0.9.2 2016-11-10 697 | 698 | 699 | ### Fixed 700 | 701 | - Validation of attributes (params and options) (nepalez) 702 | 703 | 704 | [Compare v0.9.1...v0.9.2](https://github.com/dry-rb/dry-initializer/compare/v0.9.1...v0.9.2) 705 | 706 | ## 0.9.1 2016-11-06 707 | 708 | 709 | ### Added 710 | 711 | - Support for renaming an option during initialization (nepalez) 712 | 713 | option :name, as: :username # to take :name option and create :username attribute 714 | 715 | 716 | [Compare v0.9.0...v0.9.1](https://github.com/dry-rb/dry-initializer/compare/v0.9.0...v0.9.1) 717 | 718 | ## 0.9.0 2016-11-06 719 | 720 | 721 | ### Added 722 | 723 | - The method `#initialize` is defined when a class extended the module (nepalez) 724 | 725 | In previous versions the method was defined only by `param` and `option` calls. 726 | 727 | 728 | [Compare v0.8.1...v0.9.0](https://github.com/dry-rb/dry-initializer/compare/v0.8.1...v0.9.0) 729 | 730 | ## 0.8.1 2016-11-05 731 | 732 | 733 | ### Added 734 | 735 | - Support for `dry-struct`ish syntax for constraints (type as a second parameter) (nepalez) 736 | 737 | option :name, Dry::Types['strict.string'] 738 | 739 | 740 | [Compare v0.8.0...v0.8.1](https://github.com/dry-rb/dry-initializer/compare/v0.8.0...v0.8.1) 741 | 742 | ## 0.8.0 2016-11-05 743 | 744 | are deprecated and will be removed in the next version of the gem. 745 | 746 | ### Added 747 | 748 | - support for special options like `option :end`, `option :begin` etc. (nepalez) 749 | 750 | ### Changed 751 | 752 | - switched from key arguments to serialized hash argument in the initializer (nepalez) 753 | 754 | [Compare v0.7.0...v0.8.0](https://github.com/dry-rb/dry-initializer/compare/v0.7.0...v0.8.0) 755 | 756 | ## 0.7.0 2016-10-11 757 | 758 | 759 | ### Added 760 | 761 | - Shared settings with `#using` method (nepalez) 762 | 763 | 764 | [Compare v0.6.0...v0.7.0](https://github.com/dry-rb/dry-initializer/compare/v0.6.0...v0.7.0) 765 | 766 | ## 0.6.0 2016-10-09 767 | 768 | 769 | ### Added 770 | 771 | - Support for private and protected readers in the `reader:` option (jmgarnier) 772 | 773 | 774 | [Compare v0.5.0...v0.6.0](https://github.com/dry-rb/dry-initializer/compare/v0.5.0...v0.6.0) 775 | 776 | ## 0.5.0 2016-08-21 777 | 778 | 779 | ### Added 780 | 781 | - Allow `optional` attribute to be left undefined (nepalez) 782 | 783 | 784 | [Compare v0.4.0...v0.5.0](https://github.com/dry-rb/dry-initializer/compare/v0.4.0...v0.5.0) 785 | 786 | ## 0.4.0 2016-05-28 787 | 788 | 789 | 790 | [Compare v0.3.3...v0.4.0](https://github.com/dry-rb/dry-initializer/compare/v0.3.3...v0.4.0) 791 | 792 | ## 0.3.3 2016-05-28 793 | 794 | 795 | 796 | [Compare v0.3.2...v0.3.3](https://github.com/dry-rb/dry-initializer/compare/v0.3.2...v0.3.3) 797 | 798 | ## 0.3.2 2016-05-25 799 | 800 | 801 | ### Fixed 802 | 803 | - Add explicit requirement for ruby 'set' (rickenharp) 804 | 805 | 806 | [Compare v0.3.1...v0.3.2](https://github.com/dry-rb/dry-initializer/compare/v0.3.1...v0.3.2) 807 | 808 | ## 0.3.1 2016-05-22 809 | 810 | 811 | ### Added 812 | 813 | - Support for tolerance to unknown options (nepalez) 814 | 815 | 816 | [Compare v0.3.0...v0.3.1](https://github.com/dry-rb/dry-initializer/compare/v0.3.0...v0.3.1) 817 | 818 | ## 0.3.0 2016-05-19 819 | 820 | its method #register doesn't mutate the builder instance. 821 | 822 | ### Fixed 823 | 824 | - Prevent plugin's registry from polluting superclass (nepalez) 825 | 826 | ### Changed 827 | 828 | - Made Mixin##initializer_builder method private (nepalez) 829 | - Add Mixin#register_initializer_plugin(plugin) method (nepalez) 830 | - Make all instances (Builder and Signature) immutable (nepalez) 831 | - Decouple mixin from a builder to prevent pollution (nepalez) 832 | - Ensure default value block can use private variables (jeremyf) 833 | 834 | [Compare v0.2.1...v0.3.0](https://github.com/dry-rb/dry-initializer/compare/v0.2.1...v0.3.0) 835 | 836 | ## 0.2.1 2016-05-19 837 | 838 | 839 | ### Fixed 840 | 841 | - Fix polluting superclass with declarations from subclass (nepalez) 842 | 843 | ### Changed 844 | 845 | - Make all instances (Builder and Signature) immutable (nepalez) 846 | - Decouple mixin from a builder to prevent pollution (nepalez) 847 | - Ensure default value block can use private variables (jeremyf) 848 | 849 | [Compare v0.2.0...v0.2.1](https://github.com/dry-rb/dry-initializer/compare/v0.2.0...v0.2.1) 850 | 851 | ## 0.2.0 2016-05-16 852 | 853 | Default assignments became slower (while plain type constraint are not)! 854 | 855 | ### Added 856 | 857 | - Support type constraint via every object's case equality (nepalez) 858 | 859 | ```ruby 860 | option :name, type: /foo/ 861 | option :name, type: (1...14) 862 | ``` 863 | - Support defaults and type constraints for the "container" syntax (nepalez) 864 | - Support adding extensions via plugin system (nepalez) 865 | 866 | ### Changed 867 | 868 | - Make dry-types constraint to coerce variables (nepalez) 869 | 870 | ```ruby 871 | # This will coerce `name: :foo` to `"foo"` 872 | option :name, type: Dry::Types::Coercible::String 873 | ``` 874 | - Stop supporing proc type constraint (nepalez) 875 | 876 | ```ruby 877 | option :name, type: ->(v) { String === v } # this does NOT work any more 878 | ``` 879 | 880 | later it will be implemented via coercion plugin (not added by default): 881 | 882 | ```ruby 883 | require 'dry/initializer/coercion' 884 | 885 | class MyClass 886 | extend Dry::Initializer::Mixin 887 | extend Dry::Initializer::Coercion 888 | 889 | option :name, coercer: ->(v) { (String === v) ? v.to_sym : fail } 890 | end 891 | ``` 892 | 893 | [Compare v0.1.1...v0.2.0](https://github.com/dry-rb/dry-initializer/compare/v0.1.1...v0.2.0) 894 | 895 | ## 0.1.1 2016-04-28 896 | 897 | 898 | ### Added 899 | 900 | - `include Dry::Initializer.define -> do ... end` syntax (flash-gordon) 901 | 902 | 903 | [Compare v0.1.0...v0.1.1](https://github.com/dry-rb/dry-initializer/compare/v0.1.0...v0.1.1) 904 | 905 | ## 0.1.0 2016-04-26 906 | 907 | Backward compatibility is broken. 908 | 909 | ### Added 910 | 911 | - Use `include Dry::Initializer.define(&block)` as an alternative to extending the class (nepalez) 912 | 913 | ### Changed 914 | 915 | - Use `extend Dry::Initializer::Mixin` instead of `extend Dry::Initializer` (nepalez) 916 | 917 | [Compare v0.0.1...v0.1.0](https://github.com/dry-rb/dry-initializer/compare/v0.0.1...v0.1.0) 918 | 919 | ## 0.0.1 2016-04-09 920 | 921 | First public release 922 | -------------------------------------------------------------------------------- /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 | if ENV["DRY_TYPES_FROM_MASTER"].eql?("true") 10 | gem "dry-types", github: "dry-rb/dry-types", branch: "master" 11 | else 12 | gem "dry-types" 13 | end 14 | 15 | group :benchmarks do 16 | gem "active_attr" 17 | gem "activesupport" 18 | gem "anima" 19 | gem "attr_extras" 20 | gem "benchmark-ips", "~> 2.5" 21 | gem "concord" 22 | gem "fast_attributes" 23 | gem "kwattr" 24 | gem "ruby-prof", platform: :mri 25 | gem "values" 26 | gem "value_struct" 27 | gem "virtus" 28 | end 29 | -------------------------------------------------------------------------------- /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 | end 14 | 15 | group :tools do 16 | gem "rubocop", "~> 1.55.0" 17 | gem "byebug" 18 | end 19 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | guard :rspec, cmd: "bundle exec rspec" do 4 | watch(%r{^spec/.+_spec\.rb$}) 5 | watch(%r{^spec/(spec_helper|support)}) { "spec" } 6 | watch(%r{^lib/.+}) { "spec" } 7 | end 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2025 dry-rb team 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [gem]: https://rubygems.org/gems/dry-initializer 3 | [actions]: https://github.com/dry-rb/dry-initializer/actions 4 | 5 | # dry-initializer [![Gem Version](https://badge.fury.io/rb/dry-initializer.svg)][gem] [![CI Status](https://github.com/dry-rb/dry-initializer/workflows/ci/badge.svg)][actions] 6 | 7 | ## Links 8 | 9 | * [User documentation](https://dry-rb.org/gems/dry-initializer) 10 | * [API documentation](http://rubydoc.info/gems/dry-initializer) 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.0` 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/setup" 4 | Bundler::GemHelper.install_tasks 5 | 6 | require "rspec/core/rake_task" 7 | RSpec::Core::RakeTask.new :default 8 | 9 | load "lib/tasks/benchmark.rake" 10 | load "lib/tasks/profile.rake" 11 | -------------------------------------------------------------------------------- /benchmarks/compare_several_defaults.rb: -------------------------------------------------------------------------------- 1 | Bundler.require(:benchmarks) 2 | 3 | require "dry-initializer" 4 | class WithoutDefaults 5 | extend Dry::Initializer 6 | 7 | param :foo 8 | param :bar 9 | param :baz 10 | end 11 | 12 | class WithOneDefault 13 | extend Dry::Initializer 14 | 15 | param :foo 16 | param :bar 17 | param :baz, default: proc { "BAZ" } 18 | end 19 | 20 | class WithTwoDefaults 21 | extend Dry::Initializer 22 | 23 | param :foo 24 | param :bar, default: proc { "BAR" } 25 | param :baz, default: proc { "BAZ" } 26 | end 27 | 28 | class WithThreeDefaults 29 | extend Dry::Initializer 30 | 31 | param :foo, default: proc { "FOO" } 32 | param :bar, default: proc { "BAR" } 33 | param :baz, default: proc { "BAZ" } 34 | end 35 | 36 | puts "Benchmark for various options" 37 | 38 | Benchmark.ips do |x| 39 | x.config time: 15, warmup: 10 40 | 41 | x.report("without defaults") do 42 | WithoutDefaults.new "FOO", "BAR", "BAZ" 43 | end 44 | 45 | x.report("with 0 of 1 default used") do 46 | WithOneDefault.new "FOO", "BAR", "BAZ" 47 | end 48 | 49 | x.report("with 1 of 1 default used") do 50 | WithOneDefault.new "FOO", "BAR" 51 | end 52 | 53 | x.report("with 0 of 2 defaults used") do 54 | WithTwoDefaults.new "FOO", "BAR", "BAZ" 55 | end 56 | 57 | x.report("with 1 of 2 defaults used") do 58 | WithTwoDefaults.new "FOO", "BAR" 59 | end 60 | 61 | x.report("with 2 of 2 defaults used") do 62 | WithTwoDefaults.new "FOO" 63 | end 64 | 65 | x.report("with 0 of 3 defaults used") do 66 | WithThreeDefaults.new "FOO", "BAR", "BAZ" 67 | end 68 | 69 | x.report("with 1 of 3 defaults used") do 70 | WithThreeDefaults.new "FOO", "BAR" 71 | end 72 | 73 | x.report("with 2 of 3 defaults used") do 74 | WithThreeDefaults.new "FOO" 75 | end 76 | 77 | x.report("with 3 of 3 defaults used") do 78 | WithThreeDefaults.new 79 | end 80 | 81 | x.compare! 82 | end 83 | -------------------------------------------------------------------------------- /benchmarks/plain_options.rb: -------------------------------------------------------------------------------- 1 | Bundler.require(:benchmarks) 2 | 3 | require "dry-initializer" 4 | class DryTest 5 | extend Dry::Initializer[undefined: false] 6 | 7 | option :foo 8 | option :bar 9 | end 10 | 11 | class DryTestUndefined 12 | extend Dry::Initializer 13 | 14 | option :foo 15 | option :bar 16 | end 17 | 18 | class PlainRubyTest 19 | attr_reader :foo, :bar 20 | 21 | def initialize(options = {}) 22 | @foo = options[:foo] 23 | @bar = options[:bar] 24 | end 25 | end 26 | 27 | require "anima" 28 | class AnimaTest 29 | include Anima.new(:foo, :bar) 30 | end 31 | 32 | require "kwattr" 33 | class KwattrTest 34 | kwattr :foo, :bar 35 | end 36 | 37 | puts "Benchmark for instantiation with plain options" 38 | 39 | Benchmark.ips do |x| 40 | x.config time: 15, warmup: 10 41 | 42 | x.report("plain Ruby") do 43 | PlainRubyTest.new foo: "FOO", bar: "BAR" 44 | end 45 | 46 | x.report("dry-initializer") do 47 | DryTest.new foo: "FOO", bar: "BAR" 48 | end 49 | 50 | x.report("dry-initializer (with UNDEFINED)") do 51 | DryTestUndefined.new foo: "FOO", bar: "BAR" 52 | end 53 | 54 | x.report("anima") do 55 | AnimaTest.new foo: "FOO", bar: "BAR" 56 | end 57 | 58 | x.report("kwattr") do 59 | KwattrTest.new foo: "FOO", bar: "BAR" 60 | end 61 | 62 | x.compare! 63 | end 64 | -------------------------------------------------------------------------------- /benchmarks/plain_params.rb: -------------------------------------------------------------------------------- 1 | Bundler.require(:benchmarks) 2 | 3 | require "dry-initializer" 4 | class DryTest 5 | extend Dry::Initializer[undefined: false] 6 | 7 | param :foo 8 | param :bar 9 | end 10 | 11 | class DryTestUndefined 12 | extend Dry::Initializer 13 | 14 | param :foo 15 | param :bar 16 | end 17 | 18 | class PlainRubyTest 19 | attr_reader :foo, :bar 20 | 21 | def initialize(foo, bar) 22 | @foo = foo 23 | @bar = bar 24 | end 25 | end 26 | 27 | StructTest = Struct.new(:foo, :bar) 28 | 29 | require "concord" 30 | class ConcordTest 31 | include Concord.new(:foo, :bar) 32 | end 33 | 34 | require "values" 35 | ValueTest = Value.new(:foo, :bar) 36 | 37 | require "value_struct" 38 | ValueStructTest = ValueStruct.new(:foo, :bar) 39 | 40 | require "attr_extras" 41 | class AttrExtrasText 42 | attr_initialize :foo, :bar 43 | attr_reader :foo, :bar 44 | end 45 | 46 | puts "Benchmark for instantiation with plain params" 47 | 48 | Benchmark.ips do |x| 49 | x.config time: 15, warmup: 10 50 | 51 | x.report("plain Ruby") do 52 | PlainRubyTest.new "FOO", "BAR" 53 | end 54 | 55 | x.report("Core Struct") do 56 | StructTest.new "FOO", "BAR" 57 | end 58 | 59 | x.report("values") do 60 | ValueTest.new "FOO", "BAR" 61 | end 62 | 63 | x.report("value_struct") do 64 | ValueStructTest.new "FOO", "BAR" 65 | end 66 | 67 | x.report("dry-initializer") do 68 | DryTest.new "FOO", "BAR" 69 | end 70 | 71 | x.report("dry-initializer (with UNDEFINED)") do 72 | DryTestUndefined.new "FOO", "BAR" 73 | end 74 | 75 | x.report("concord") do 76 | ConcordTest.new "FOO", "BAR" 77 | end 78 | 79 | x.report("attr_extras") do 80 | AttrExtrasText.new "FOO", "BAR" 81 | end 82 | 83 | x.compare! 84 | end 85 | -------------------------------------------------------------------------------- /benchmarks/with_coercion.rb: -------------------------------------------------------------------------------- 1 | Bundler.require(:benchmarks) 2 | 3 | require "dry-initializer" 4 | class DryTest 5 | extend Dry::Initializer[undefined: false] 6 | 7 | option :foo, proc(&:to_s) 8 | option :bar, proc(&:to_s) 9 | end 10 | 11 | class DryTestUndefined 12 | extend Dry::Initializer 13 | 14 | option :foo, proc(&:to_s) 15 | option :bar, proc(&:to_s) 16 | end 17 | 18 | class PlainRubyTest 19 | attr_reader :foo, :bar 20 | 21 | def initialize(options) 22 | @foo = options[:foo].to_s 23 | @bar = options[:bar].to_s 24 | end 25 | end 26 | 27 | require "virtus" 28 | class VirtusTest 29 | include Virtus.model 30 | 31 | attribute :foo, String 32 | attribute :bar, String 33 | end 34 | 35 | require "fast_attributes" 36 | class FastAttributesTest 37 | extend FastAttributes 38 | 39 | define_attributes initialize: true do 40 | attribute :foo, String 41 | attribute :bar, String 42 | end 43 | end 44 | 45 | puts "Benchmark for instantiation with coercion" 46 | 47 | Benchmark.ips do |x| 48 | x.config time: 15, warmup: 10 49 | 50 | x.report("plain Ruby") do 51 | PlainRubyTest.new foo: "FOO", bar: "BAR" 52 | end 53 | 54 | x.report("dry-initializer") do 55 | DryTest.new foo: "FOO", bar: "BAR" 56 | end 57 | 58 | x.report("dry-initializer (with UNDEFINED)") do 59 | DryTestUndefined.new foo: "FOO", bar: "BAR" 60 | end 61 | 62 | x.report("virtus") do 63 | VirtusTest.new foo: "FOO", bar: "BAR" 64 | end 65 | 66 | x.report("fast_attributes") do 67 | FastAttributesTest.new foo: "FOO", bar: "BAR" 68 | end 69 | 70 | x.compare! 71 | end 72 | -------------------------------------------------------------------------------- /benchmarks/with_defaults.rb: -------------------------------------------------------------------------------- 1 | Bundler.require(:benchmarks) 2 | 3 | require "dry-initializer" 4 | class DryTest 5 | extend Dry::Initializer[undefined: false] 6 | 7 | option :foo, default: -> { "FOO" } 8 | option :bar, default: -> { "BAR" } 9 | end 10 | 11 | class DryTestUndefined 12 | extend Dry::Initializer 13 | 14 | option :foo, default: -> { "FOO" } 15 | option :bar, default: -> { "BAR" } 16 | end 17 | 18 | class PlainRubyTest 19 | attr_reader :foo, :bar 20 | 21 | def initialize(foo: "FOO", bar: "BAR") 22 | @foo = foo 23 | @bar = bar 24 | end 25 | end 26 | 27 | require "kwattr" 28 | class KwattrTest 29 | kwattr foo: "FOO", bar: "BAR" 30 | end 31 | 32 | require "active_attr" 33 | class ActiveAttrTest 34 | include ActiveAttr::AttributeDefaults 35 | 36 | attribute :foo, default: "FOO" 37 | attribute :bar, default: "BAR" 38 | end 39 | 40 | puts "Benchmark for instantiation with default values" 41 | 42 | Benchmark.ips do |x| 43 | x.config time: 15, warmup: 10 44 | 45 | x.report("plain Ruby") do 46 | PlainRubyTest.new 47 | end 48 | 49 | x.report("dry-initializer") do 50 | DryTest.new 51 | end 52 | 53 | x.report("dry-initializer (with UNDEFINED)") do 54 | DryTestUndefined.new 55 | end 56 | 57 | x.report("kwattr") do 58 | KwattrTest.new 59 | end 60 | 61 | x.report("active_attr") do 62 | ActiveAttrTest.new 63 | end 64 | 65 | x.compare! 66 | end 67 | -------------------------------------------------------------------------------- /benchmarks/with_defaults_and_coercion.rb: -------------------------------------------------------------------------------- 1 | Bundler.require(:benchmarks) 2 | 3 | require "dry-initializer" 4 | class DryTest 5 | extend Dry::Initializer[undefined: false] 6 | 7 | option :foo, proc(&:to_s), default: -> { "FOO" } 8 | option :bar, proc(&:to_s), default: -> { "BAR" } 9 | end 10 | 11 | class DryTestUndefined 12 | extend Dry::Initializer 13 | 14 | option :foo, proc(&:to_s), default: -> { "FOO" } 15 | option :bar, proc(&:to_s), default: -> { "BAR" } 16 | end 17 | 18 | class PlainRubyTest 19 | attr_reader :foo, :bar 20 | 21 | def initialize(foo: "FOO", bar: "BAR") 22 | @foo = foo 23 | @bar = bar 24 | raise TypeError unless String === @foo 25 | raise TypeError unless String === @bar 26 | end 27 | end 28 | 29 | require "virtus" 30 | class VirtusTest 31 | include Virtus.model 32 | 33 | attribute :foo, String, default: "FOO" 34 | attribute :bar, String, default: "BAR" 35 | end 36 | 37 | puts "Benchmark for instantiation with type constraints and default values" 38 | 39 | Benchmark.ips do |x| 40 | x.config time: 15, warmup: 10 41 | 42 | x.report("plain Ruby") do 43 | PlainRubyTest.new 44 | end 45 | 46 | x.report("dry-initializer") do 47 | DryTest.new 48 | end 49 | 50 | x.report("dry-initializer (with UNDEFINED)") do 51 | DryTest.new 52 | end 53 | 54 | x.report("virtus") do 55 | VirtusTest.new 56 | end 57 | 58 | x.compare! 59 | end 60 | -------------------------------------------------------------------------------- /bin/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dry-rb/dry-initializer/e4f3e50d6cd07afaacc6cf7e472be2b1e67ab624/bin/.gitkeep -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | require "dry/initializer" 6 | 7 | binding.irb 8 | 9 | puts 10 | -------------------------------------------------------------------------------- /changelog.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - version: 3.2.0 3 | date: '2025-01-01' 4 | changed: 5 | - Set minimal supported Ruby version to 3.1 (@flash-gordon) 6 | - | 7 | Exclude block forwarding from `Root#initialize`. This helps 8 | with tracking down calls that shouldn't pass a block in Ruby 3.4 (see #109) (@flash-gordon) 9 | - version: 3.1.1 10 | date: '2022-01-19' 11 | changed: 12 | - Improved error messages were rolled back, they created 13 | an implicit dependency on dry-types (@flash-gordon) 14 | - version: 3.1.0 15 | date: '2022-01-16' 16 | changed: 17 | - Improved error messages on type mismatch (@swerling) 18 | - '[BREAKING] Minimal supported Ruby version is 2.7 (@flash-gordon)' 19 | - version: 3.0.4 20 | date: '2020-09-29' 21 | fixed: 22 | - 'Arity check for lambdas used for coercion (@flash-gordon)' 23 | - version: 3.0.3 24 | date: '2020-01-08' 25 | fixed: 26 | - 'Constrained member arrays work correctly now (see #33) (@bjeanes + @solnic)' 27 | - version: 3.0.2 28 | date: '2019-11-07' 29 | fixed: 30 | - Warnings about keyword arguments (flash-gordon) 31 | - version: 3.0.1 32 | date: '2019-04-15' 33 | fixed: 34 | - |- 35 | Usage of underscored names of `option`-s and `param`-s (nepalez) 36 | 37 | You can use any sequence of underscores except for in nested types. 38 | In nested types single underscores can be used to split alphanumeric 39 | parts only. 40 | 41 | ```ruby 42 | class Test 43 | extend Dry::Initializer 44 | 45 | # Proper usage 46 | option :foo_bar do 47 | option :__foo__, proc(&:to_s) 48 | end 49 | end 50 | 51 | # Improper usage 52 | option :__foo__ do 53 | # ... 54 | end 55 | 56 | option :foo__bar do 57 | # ... 58 | end 59 | end 60 | ``` 61 | 62 | This restriction is necessary because we constantize option/param names 63 | when defining nested structs. 64 | - version: 3.0.0 65 | date: '2019-04-14' 66 | added: 67 | - |- 68 | Support of wrapped types/coercers (nepalez) 69 | 70 | ```ruby 71 | class Test 72 | # Wrap type to the array 73 | param :foo, [proc(&:to_s)] 74 | end 75 | 76 | # And the value will be wrapped as well 77 | test = Test.new(42) 78 | test.foo # => ["42"] 79 | ``` 80 | - |- 81 | It works with several layers of nesting (nepalez) 82 | 83 | ```ruby 84 | class Test 85 | # Wrap type to the array 86 | param :foo, [[proc(&:to_s)]] 87 | end 88 | 89 | # And the value will be wrapped as well 90 | test = Test.new(42) 91 | test.foo # => [["42"]] 92 | ``` 93 | - |- 94 | Support of nested types/coercers (nepalez) 95 | 96 | ```ruby 97 | class Test 98 | param :foo do 99 | option :bar do 100 | option :baz, proc(&:to_s) 101 | end 102 | end 103 | end 104 | 105 | test = Test.new(bar: { "baz" => 42 }) 106 | test.foo.bar.baz # => "42" 107 | ``` 108 | - |- 109 | Wrapped/nested combinations are supported as well (nepalez) 110 | 111 | ```ruby 112 | class Test 113 | param :foo, [] do 114 | option :bar, proc(&:to_s) 115 | end 116 | end 117 | 118 | test = Test.new(bar: 42) 119 | test.foo.first.bar # => "42" 120 | ``` 121 | - "## [2.7.0] Unreleazed" 122 | fixed: 123 | - |- 124 | Roll back master to the state of [2.5.0]. 125 | 126 | Somehow distinction between `@default_null` and `@null` variables 127 | in the `Dry::Initializer::Builders` broken the `rom` library. 128 | 129 | The version [2.6.0] has been yanked on rubygems, so the master 130 | was rolled back to the previous state until the reason for 131 | the incompatibility become clear (bjeanes, nepalez) 132 | - "## [2.6.0] [2018-09-09] (YANKED)" 133 | - version: 2.5.0 134 | date: '2018-08-17' 135 | fixed: 136 | - |- 137 | `nil` coercion (belousovAV) 138 | 139 | When default value is `nil` instead of `Dry::Initializer::UNDEFINED`, 140 | the coercion should be applied to any value, including `nil`, because 141 | we cannot distinct "undefined" `nil` from the "assigned" `nil` value. 142 | - version: 2.4.0 143 | date: '2018-02-01' 144 | added: 145 | - |- 146 | Dispatchers for adding syntax sugar to `param` and `options` (nepalez) 147 | 148 | ```ruby 149 | # Converts `integer: true` to `type: proc(&:to_i)` 150 | dispatcher = ->(op) { op[:integer] ? op.merge(type: proc(&:to_i)) : op } 151 | # Register a dispatcher 152 | Dry::Initializer::Dispatchers << dispatcher 153 | # Use syntax sugar 154 | class User 155 | param :id, integer: true # same as param :id, proc(&:to_i) 156 | end 157 | ``` 158 | - version: 2.3.0 159 | date: '2017-09-19' 160 | added: 161 | - |- 162 | Type coercer can take second argument for the initialized instance (nepalez) 163 | This allows to wrap assigned value to the object that refers back 164 | to the initializer instance. More verbose example: 165 | 166 | ```ruby 167 | class Location < String 168 | attr_reader :parameter # refers back to its parameter 169 | 170 | def initialize(name, parameter) 171 | super(name) 172 | @parameter = parameter 173 | end 174 | end 175 | 176 | class Parameter 177 | extend Dry::Initializer 178 | param :name 179 | param :location, ->(value, param) { Location.new(value, param) } 180 | end 181 | 182 | offset = Parameter.new "offset", location: "query" 183 | offset.name # => "offset" 184 | offset.location # => "query" 185 | offset.location.parameter == offset # true 186 | ``` 187 | - version: 2.2.0 188 | date: '2017-09-13' 189 | added: 190 | - Option `:desc` for option/param to add a description (nepalez) 191 | - |- 192 | Methods `Definition#inch` and `Config#inch` to inspect definitions (nepalez) 193 | 194 | ```ruby 195 | class User 196 | extend Dry::Initializer 197 | option :name, proc(&:to_s), optional: true, desc: "User name" 198 | option :email, optional: true, desc: "user email" 199 | end 200 | 201 | User.dry_initializer.inch 202 | # @!method initialize(*, **options) 203 | # Initializes an instance of User 204 | # @option [Object] :name (optional) User name 205 | # @option [Object] :email (optional) User email 206 | # @return [User] 207 | ``` 208 | - version: 2.1.0 209 | date: '2017-09-11' 210 | added: 211 | - |- 212 | Method `#options` to param/option definition (nepalez) 213 | 214 | ```ruby 215 | class User 216 | extend Dry::Initializer 217 | option :name, proc(&:to_s), optional: true 218 | option :email, optional: true 219 | end 220 | 221 | User.dry_initializer.options.map do |option| 222 | [option.source, option.options] 223 | end 224 | # => [ 225 | # [:name, { type: proc(&:to_s), as: :name, optional: true }], 226 | # [:email, { as: :email, optional: true }] 227 | # ] 228 | ``` 229 | 230 | This method can be helpful for replicating params/options 231 | in another class without inheritance. 232 | - version: 2.0.0 233 | date: '2017-08-28' 234 | summary: and to @gzigzigzeo for persuading me to do this refactoring. 235 | added: 236 | - |- 237 | Class method `.dry_initializer` -- a container for `.params` and `.options` 238 | `.definitions` along with the `.null` setting (either `nil` or `UNDEFINED`) 239 | used for unassigned values (nepalez) 240 | - |- 241 | `.dry_initializer.attributes` method takes an instance of the same class 242 | and returns the hash of assigned options. This provide the same 243 | functionality as previously used instance variable `@__options__` (nepalez) 244 | 245 | ```ruby 246 | object.class.dry_initializer.attributes(object) 247 | ``` 248 | 249 | When you use "Dry::Initializer.define -> { ... }" syntax, 250 | the class method `.dry_initializer` is not defined. To access attributes 251 | you should use private instance method `#__dry_initializer_config__` instead: 252 | 253 | ```ruby 254 | object.send(:__dry_initializer_config__).attributes(object) 255 | ``` 256 | 257 | Both methods `.dry_initializer` and `#__dry_initializer_config__` refer 258 | to the same object. 259 | - |- 260 | `.dry_initializer.public_attributes`. This method works differently: 261 | it looks through (possibly reloaded) readers instead of variables 262 | (gzigzigzeo, nepalez) 263 | 264 | ```ruby 265 | object.class.dry_initializer.public_attributes(object) 266 | ``` 267 | 268 | You can use the same trick as above mutatis mutandis. 269 | fixed: 270 | - |- 271 | Definition order dependency bug (nepalez) 272 | 273 | I've found out that if you provided a subclass and then changed params 274 | or options of its superclass, these changes woudn't be reflected in 275 | subclasses until you change any of it params/options as well. 276 | 277 | Now this bug is fixed: every time you call `param` or `option` at 278 | any class, the gem scans through all its descendants to the very bottom 279 | of the tree, and reloads their defintitions. 280 | 281 | Being done in load time, the rebuilt makes no effect on runtime performance. 282 | - |- 283 | Possible misbehavior when you define param and option with the same name (nepalez) 284 | 285 | Doing this will provide `option :name` only, not both: 286 | 287 | ```ruby 288 | param :name 289 | option :name 290 | ``` 291 | - |- 292 | Attempt to redefine param/option of superclass with option/param in 293 | its subclass will cause an exception because it would break 294 | Liskov substitute principle with unexpected behaviour (nepalez) 295 | 296 | No, you can do neither these definitions, nor vice versa: 297 | 298 | ```ruby 299 | class Foo 300 | extend Dry::Intitializer 301 | param :name 302 | end 303 | 304 | class Bar < Foo 305 | option :name 306 | end 307 | ``` 308 | - |- 309 | When you reloading previously defined param of superclass, the gem 310 | will check all its descendands for whether all required positional params 311 | goes before optional ones (nepalez) 312 | 313 | ```ruby 314 | class Foo 315 | param :name 316 | # Foo: def initializer(name) 317 | end 318 | 319 | class Bar 320 | param :email 321 | # Bar: def initializer(name, email) 322 | end 323 | 324 | class Foo 325 | # This raises SyntaxError because in Bar this would cause wrong definition 326 | # Foo: def initializer(name = nil) 327 | # Bar: def initializer(name = nil, email) 328 | param :name, optional: true 329 | end 330 | ``` 331 | changed: 332 | - |- 333 | Under the hood I've separated param/option settings declaration (a container 334 | with param/option settings) from code builders for initializer and readers 335 | (nepalez) 336 | 337 | You can check both the code for the `__initializer__`: 338 | 339 | ```ruby 340 | class Foo 341 | extend Dry::Initializer 342 | # ... 343 | end 344 | 345 | Foo.dry_initializer.code 346 | ``` 347 | 348 | and readers: 349 | 350 | ```ruby 351 | Foo.dry_initializer.params.map(&:code) 352 | Foo.dry_initializer.options.map(&:code) 353 | 354 | # or 355 | 356 | Foo.dry_initializer.definitions.values.map(&:code) 357 | ``` 358 | 359 | You can also check settings for every param and option using methods 360 | `dry_initializer.params`, `dry_initializer.options` (lists), or 361 | `dry_initializer.definitions` (hash). 362 | 363 | You can check null value via `.dry_initializer.null` which is different 364 | for `Dry::Initializer` and `Dry::Initializer[undefined: false]` modules. 365 | - |- 366 | Optimized the code for `__initializer__`-s (the method where all magics occurs) 367 | (nepalez) 368 | 369 | Benchmarks remained about the same: 370 | 371 | ```shell 372 | rake benchmark 373 | ``` 374 | 375 | ``` 376 | Benchmark for instantiation with plain params 377 | value_struct: 4317196.9 i/s 378 | plain Ruby: 4129803.9 i/s - 1.05x slower 379 | dry-initializer: 1710702.1 i/s - 2.52x slower 380 | concord: 1372630.4 i/s - 3.15x slower 381 | values: 601651.8 i/s - 7.18x slower 382 | attr_extras: 535599.5 i/s - 8.06x slower 383 | ``` 384 | 385 | ``` 386 | Benchmark for instantiation with plain options 387 | plain Ruby: 1769174.1 i/s 388 | dry-initializer: 636634.1 i/s - 2.78x slower 389 | kwattr: 423296.5 i/s - 4.18x slower 390 | anima: 399415.0 i/s - 4.43x slower 391 | ``` 392 | 393 | ``` 394 | Benchmark for instantiation with coercion 395 | plain Ruby: 1565501.0 i/s 396 | fast_attributes: 569952.9 i/s - 2.75x slower 397 | dry-initializer: 461122.1 i/s - 3.39x slower 398 | virtus: 138074.8 i/s - 11.34x slower 399 | ``` 400 | 401 | ``` 402 | Benchmark for instantiation with default values 403 | plain Ruby: 3402455.4 i/s 404 | kwattr: 586206.5 i/s - 5.80x slower 405 | dry-initializer: 528482.2 i/s - 6.44x slower 406 | active_attr: 298697.7 i/s - 11.39x slower 407 | ``` 408 | 409 | ``` 410 | Benchmark for instantiation with type constraints and default values 411 | plain Ruby: 2881696.1 i/s 412 | dry-initializer: 470815.1 i/s - 6.12x slower 413 | virtus: 180272.6 i/s - 15.99x slower 414 | ``` 415 | - version: 1.4.1 416 | date: '2017-04-05' 417 | fixed: 418 | - |- 419 | Warning about redefined `#initialize` in case the method reloaded in a klass 420 | that extends the module (nepalez, sergey-chechaev) 421 | changed: 422 | - Rename `Dry::Initializer::DSL` -> `Dry::Initializer::ClassDSL` (nepalez) 423 | - Add `Dry::Initializer::InstanceDSL` (nepalez) 424 | - version: 1.4.0 425 | date: '2017-03-08' 426 | changed: 427 | - |- 428 | The `@__options__` hash now collects all assigned attributes, 429 | collected via `#option` (as before), and `#param` (nepalez) 430 | - version: 1.3.0 431 | date: '2017-03-05' 432 | added: 433 | - |- 434 | No-undefined configuration of the initializer (nepalez, flash-gordon) 435 | 436 | You can either extend or include module `Dry::Initializer` with additional option 437 | `[undefined: false]`. This time `nil` will be assigned instead of 438 | `Dry::Initializer::UNDEFINED`. Readers becomes faster because there is no need 439 | to chech whether a variable was defined or not. At the same time the initializer 440 | doesn't distinct cases when a variable was set to `nil` explicitly, and when it wasn's set at all: 441 | 442 | class Foo # old behavior 443 | extend Dry::Initializer 444 | param :qux, optional: true 445 | end 446 | 447 | class Bar # new behavior 448 | extend Dry::Initializer[undefined: false] 449 | param :qux, optional: true 450 | end 451 | 452 | Foo.new.instance_variable_get(:@qux) # => Dry::Initializer::UNDEFINED 453 | Bar.new.instance_variable_get(:@qux) # => nil 454 | changed: 455 | - Fixed method definitions for performance at the load time (nepalez, flash-gordon) 456 | - version: 1.2.0 457 | date: '2017-03-05' 458 | fixed: 459 | - The `@__options__` variable collects renamed options after default values and 460 | coercions were applied (nepalez) 461 | - version: 1.1.3 462 | date: '2017-03-01' 463 | added: 464 | - Support for lambdas as default values (nepalez, gzigzigzeo) 465 | - version: 1.1.2 466 | date: '2017-02-06' 467 | changed: 468 | - Remove previously defined methods before redefining them (flash-gordon) 469 | - version: 1.1.1 470 | date: '2017-02-04' 471 | fixed: 472 | - "`@__options__` collects defined options only (nepalez)" 473 | - version: 1.1.0 474 | date: '2017-01-28' 475 | added: 476 | - |- 477 | enhancement via `Dry::Initializer::Attribute.dispatchers` registry (nepalez) 478 | 479 | ```ruby 480 | # Register dispatcher for `:string` option 481 | Dry::Initializer::Attribute.dispatchers << ->(string: nil, **op) do 482 | string ? op.merge(type: proc(&:to_s)) : op 483 | end 484 | 485 | # Now you can use the `:string` key for `param` and `option` 486 | class User 487 | extend Dry::Initializer 488 | param :name, string: true 489 | end 490 | 491 | User.new(:Andy).name # => "Andy" 492 | ``` 493 | changed: 494 | - optimize assignments for performance (nepalez) 495 | - version: 1.0.0 496 | date: '2017-01-22' 497 | summary: In this version the code has been rewritten for simplicity 498 | changed: 499 | - "[BREAKING] when `param` or `option` was not defined, the corresponding **variable** 500 | is set to `Dry::Initializer::UNDEFINED`, but the **reader** (when defined) will 501 | return `nil` (nepalez)" 502 | - "`Dry::Initializer` and `Dry::Initializer::Mixin` became aliases (nepalez)" 503 | added: 504 | - |- 505 | support for reloading `param` and `option` definitions (nepalez) 506 | 507 | class User 508 | extend Dry::Initializer 509 | param :name 510 | param :phone, optional: true 511 | end 512 | 513 | User.new # => Boom! 514 | 515 | class Admin < User 516 | param :name, default: proc { 'Merlin' } 517 | end 518 | 519 | # order of the param not changed 520 | Admin.new.name # => "Merlin" 521 | - |- 522 | support for assignment of attributes via several options (nepalez) 523 | 524 | class User 525 | extend Dry::Initializer 526 | option :phone 527 | option :number, as: :phone 528 | end 529 | 530 | # Both ways provide the same result 531 | User.new(phone: '1234567890').phone # => '1234567890' 532 | User.new(number: '1234567890').phone # => '1234567890' 533 | - version: 0.11.0 534 | date: '2017-01-02' 535 | added: 536 | - Support of reloading `#initializer` with `super` (nepalez) 537 | - version: 0.10.2 538 | date: '2016-12-31' 539 | added: 540 | - Support of Ruby 2.4 (flas-gordon) 541 | - version: 0.10.1 542 | date: '2016-12-27' 543 | fixed: 544 | - Wrong arity when there were no options and the last param had a default (nolith) 545 | - version: 0.10.0 546 | date: '2016-11-20' 547 | - version: 0.9.3 548 | date: '2016-11-20' 549 | fixed: 550 | - |- 551 | Support of weird option names (nepalez) 552 | 553 | ```ruby 554 | option :"First name", as: :first_name 555 | ``` 556 | - version: 0.9.2 557 | date: '2016-11-10' 558 | fixed: 559 | - Validation of attributes (params and options) (nepalez) 560 | - version: 0.9.1 561 | date: '2016-11-06' 562 | added: 563 | - |- 564 | Support for renaming an option during initialization (nepalez) 565 | 566 | option :name, as: :username # to take :name option and create :username attribute 567 | - version: 0.9.0 568 | date: '2016-11-06' 569 | added: 570 | - |- 571 | The method `#initialize` is defined when a class extended the module (nepalez) 572 | 573 | In previous versions the method was defined only by `param` and `option` calls. 574 | - version: 0.8.1 575 | date: '2016-11-05' 576 | added: 577 | - |- 578 | Support for `dry-struct`ish syntax for constraints (type as a second parameter) (nepalez) 579 | 580 | option :name, Dry::Types['strict.string'] 581 | - version: 0.8.0 582 | date: '2016-11-05' 583 | summary: are deprecated and will be removed in the next version of the gem. 584 | added: 585 | - support for special options like `option :end`, `option :begin` etc. (nepalez) 586 | changed: 587 | - switched from key arguments to serialized hash argument in the initializer (nepalez) 588 | - version: 0.7.0 589 | date: '2016-10-11' 590 | added: 591 | - Shared settings with `#using` method (nepalez) 592 | - version: 0.6.0 593 | date: '2016-10-09' 594 | added: 595 | - Support for private and protected readers in the `reader:` option (jmgarnier) 596 | - version: 0.5.0 597 | date: '2016-08-21' 598 | added: 599 | - Allow `optional` attribute to be left undefined (nepalez) 600 | - version: 0.4.0 601 | date: '2016-05-28' 602 | - version: 0.3.3 603 | date: '2016-05-28' 604 | - version: 0.3.2 605 | date: '2016-05-25' 606 | fixed: 607 | - Add explicit requirement for ruby 'set' (rickenharp) 608 | - version: 0.3.1 609 | date: '2016-05-22' 610 | added: 611 | - Support for tolerance to unknown options (nepalez) 612 | - version: 0.3.0 613 | date: '2016-05-19' 614 | summary: 'its method #register doesn''t mutate the builder instance.' 615 | changed: 616 | - Made Mixin##initializer_builder method private (nepalez) 617 | - Add Mixin#register_initializer_plugin(plugin) method (nepalez) 618 | - Make all instances (Builder and Signature) immutable (nepalez) 619 | - Decouple mixin from a builder to prevent pollution (nepalez) 620 | - Ensure default value block can use private variables (jeremyf) 621 | fixed: 622 | - Prevent plugin's registry from polluting superclass (nepalez) 623 | - version: 0.2.1 624 | date: '2016-05-19' 625 | fixed: 626 | - Fix polluting superclass with declarations from subclass (nepalez) 627 | changed: 628 | - Make all instances (Builder and Signature) immutable (nepalez) 629 | - Decouple mixin from a builder to prevent pollution (nepalez) 630 | - Ensure default value block can use private variables (jeremyf) 631 | - version: 0.2.0 632 | date: '2016-05-16' 633 | summary: Default assignments became slower (while plain type constraint are not)! 634 | changed: 635 | - |- 636 | Make dry-types constraint to coerce variables (nepalez) 637 | 638 | ```ruby 639 | # This will coerce `name: :foo` to `"foo"` 640 | option :name, type: Dry::Types::Coercible::String 641 | ``` 642 | - |- 643 | Stop supporing proc type constraint (nepalez) 644 | 645 | ```ruby 646 | option :name, type: ->(v) { String === v } # this does NOT work any more 647 | ``` 648 | 649 | later it will be implemented via coercion plugin (not added by default): 650 | 651 | ```ruby 652 | require 'dry/initializer/coercion' 653 | 654 | class MyClass 655 | extend Dry::Initializer::Mixin 656 | extend Dry::Initializer::Coercion 657 | 658 | option :name, coercer: ->(v) { (String === v) ? v.to_sym : fail } 659 | end 660 | ``` 661 | added: 662 | - |- 663 | Support type constraint via every object's case equality (nepalez) 664 | 665 | ```ruby 666 | option :name, type: /foo/ 667 | option :name, type: (1...14) 668 | ``` 669 | - Support defaults and type constraints for the "container" syntax (nepalez) 670 | - Support adding extensions via plugin system (nepalez) 671 | - version: 0.1.1 672 | date: '2016-04-28' 673 | added: 674 | - "`include Dry::Initializer.define -> do ... end` syntax (flash-gordon)" 675 | - version: 0.1.0 676 | date: '2016-04-26' 677 | summary: Backward compatibility is broken. 678 | changed: 679 | - Use `extend Dry::Initializer::Mixin` instead of `extend Dry::Initializer` (nepalez) 680 | added: 681 | - Use `include Dry::Initializer.define(&block)` as an alternative to extending the 682 | class (nepalez) 683 | - version: 0.0.1 684 | date: '2016-04-09' 685 | summary: First public release 686 | -------------------------------------------------------------------------------- /docsite/source/attributes.html.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Attributes 3 | layout: gem-single 4 | name: dry-initializer 5 | --- 6 | 7 | Sometimes you need to access all attributes assigned via params and options of the object constructor. 8 | 9 | We support 2 methods: `attributes` and `public_attributes` for this goal. Both methods are wrapped into container accessible via `.dry_initializer` container: 10 | 11 | ```ruby 12 | require 'dry-initializer' 13 | 14 | class User 15 | extend Dry::Initializer 16 | 17 | param :name 18 | option :email, optional: true 19 | option :telefon, optional: true, as: :phone 20 | end 21 | 22 | user = User.new "Andy", telefon: "71002003040" 23 | 24 | User.dry_initializer.attributes(user) 25 | # => { name: "Andy", phone: "71002003040" } 26 | ``` 27 | 28 | What the method does is extracts *variables assigned* to the object (and skips unassigned ones like the `email` above). It doesn't matter whether you send it via `params` or `option`; we look at the result of the instantiation, not at the interface. 29 | 30 | Method `public_attributes` works different. Let's look at the following example to see the difference: 31 | 32 | ```ruby 33 | require 'dry-initializer' 34 | 35 | class User 36 | extend Dry::Initializer 37 | 38 | param :name 39 | option :telefon, optional: true, as: :phone 40 | option :email, optional: true 41 | option :token, optional: true, reader: :private 42 | option :password, optional: true, reader: false 43 | end 44 | 45 | user = User.new "Andy", telefon: "71002003040", token: "foo", password: "bar" 46 | 47 | User.dry_initializer.attributes(user) 48 | # => { name: "Andy", phone: "71002003040", token: "foo", password: "bar" } 49 | 50 | User.dry_initializer.public_attributes(user) 51 | # => { name: "Andy", phone: "71002003040", email: nil } 52 | ``` 53 | 54 | Notice that `public_attribute` reads *public reader methods*, not variables. That's why it skips both the private `token`, and the `password` whose reader hasn't been defined. 55 | 56 | Another difference concerns unassigned values. Because the reader `user.email` returns `nil` (its `@email` variable contains `Dry::Initializer::UNDEFINED` constant), the `public_attributes` adds this value to the hash using the method. 57 | 58 | The third thing to mention is that you can override the reader, and it is the overriden method which will be used by `public_attributes`: 59 | 60 | ```ruby 61 | require 'dry-initializer' 62 | 63 | class User 64 | extend Dry::Initializer 65 | 66 | param :name 67 | option :password, optional: true 68 | 69 | def password 70 | super.hash.to_s 71 | end 72 | end 73 | 74 | user = User.new "Joe", password: "foo" 75 | 76 | User.dry_initializer.attributes(user) 77 | # => { user: "Joe", password: "foo" } 78 | 79 | User.dry_initializer.public_attributes(user) 80 | # => { user: "Joe", password: "-1844874613000160009" } 81 | ``` 82 | 83 | This feature works for the "extend Dry::Initializer" syntax. But what about "include Dry::Initializer.define ..."? Now we don't pollute class namespace with new methods, that's why `.dry_initializer` is absent. 84 | 85 | To access config you can use a hack. Under the hood we define private instance method `#__dry_initializer_config__` which refers to the same container. So you can write: 86 | 87 | ```ruby 88 | require 'dry-initializer' 89 | 90 | class User 91 | extend Dry::Initializer 92 | param :name 93 | end 94 | 95 | user = User.new "Joe" 96 | 97 | user.send(:__dry_initializer_config__).attributes(user) 98 | # => { user: "Joe" } 99 | 100 | user.send(:__dry_initializer_config__).public_attributes(user) 101 | # => { user: "Joe" } 102 | ``` 103 | 104 | This is a hack because the `__dry_initializer_config__` is not a part of the gem's public interface; there's a possibility it can be changed or removed in the later releases. 105 | 106 | We'll try to be careful with it, and mark it as deprecated method in case of such a removal. 107 | -------------------------------------------------------------------------------- /docsite/source/container-version.html.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Container Version 3 | layout: gem-single 4 | name: dry-initializer 5 | --- 6 | 7 | Instead of extending a class with the `Dry::Initializer`, you can include a container with the `initializer` method only. This method should be preferred when you don't need subclassing. 8 | 9 | ```ruby 10 | require 'dry-initializer' 11 | 12 | class User 13 | # notice `-> do .. end` syntax 14 | include Dry::Initializer.define -> do 15 | param :name, proc(&:to_s) 16 | param :role, default: proc { 'customer' } 17 | option :admin, default: proc { false } 18 | end 19 | end 20 | ``` 21 | 22 | If you still need the DSL (`param` and `option`) to be inherited, use the direct extension: 23 | 24 | ```ruby 25 | require 'dry-initializer' 26 | 27 | class BaseService 28 | extend Dry::Initializer 29 | 30 | class << self 31 | alias_method :dependency, :param 32 | end 33 | end 34 | 35 | class ShowUser < BaseService 36 | dependency :user 37 | 38 | def call 39 | puts user&.name 40 | end 41 | end 42 | ``` 43 | -------------------------------------------------------------------------------- /docsite/source/index.html.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Introduction & Usage 3 | description: DSL for defining initializer params and options 4 | layout: gem-single 5 | order: 8 6 | type: gem 7 | name: dry-initializer 8 | sections: 9 | - container-version 10 | - params-and-options 11 | - tolerance-to-unknown-arguments 12 | - optionals-and-defaults 13 | - type-constraints 14 | - readers 15 | - inheritance 16 | - skip-undefined 17 | - attributes 18 | - rails-support 19 | --- 20 | 21 | `dry-initializer` is a simple mixin of class methods `params` and `options` for instances. 22 | 23 | ## Synopsis 24 | 25 | ```ruby 26 | require 'dry-initializer' 27 | 28 | class User 29 | extend Dry::Initializer 30 | 31 | param :name, proc(&:to_s) 32 | param :role, default: proc { 'customer' } 33 | option :admin, default: proc { false } 34 | option :phone, optional: true 35 | end 36 | 37 | user = User.new 'Vladimir', 'admin', admin: true 38 | 39 | user.name # => 'Vladimir' 40 | user.role # => 'admin' 41 | user.admin # => true 42 | user.phone # => nil 43 | ``` 44 | -------------------------------------------------------------------------------- /docsite/source/inheritance.html.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Inheritance 3 | layout: gem-single 4 | name: dry-initializer 5 | --- 6 | 7 | Subclassing preserves all definitions being made inside a superclass. 8 | 9 | ```ruby 10 | require 'dry-initializer' 11 | 12 | class User 13 | extend Dry::Initializer 14 | 15 | param :name 16 | end 17 | 18 | class Employee < User 19 | param :position 20 | end 21 | 22 | employee = Employee.new('John', 'supercargo') 23 | employee.name # => 'John' 24 | employee.position # => 'supercargo' 25 | 26 | employee = Employee.new # => fails because type 27 | ``` 28 | 29 | You can override params and options. 30 | Such overriding leaves initial order of params (positional arguments) unchanged: 31 | 32 | ```ruby 33 | class Employee < User 34 | param :position, optional: true 35 | param :name, default: proc { 'Unknown' } 36 | end 37 | 38 | user = User.new # => Boom! because User#name is required 39 | employee = Employee.new # passes because who cares on employee's name 40 | 41 | employee.name 42 | # => 'Unknown' because it is the name that positioned first like in User 43 | ``` 44 | -------------------------------------------------------------------------------- /docsite/source/optionals-and-defaults.html.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Optional Attributes and Default Values 3 | layout: gem-single 4 | name: dry-initializer 5 | --- 6 | 7 | By default both params and options are mandatory. Use `:default` key to make them optional: 8 | 9 | ```ruby 10 | require 'dry-initializer' 11 | 12 | class User 13 | extend Dry::Initializer 14 | 15 | param :name, default: proc { 'Unknown user' } 16 | option :email, default: proc { 'unknown@example.com' } 17 | option :phone, optional: true 18 | end 19 | 20 | user = User.new 21 | user.name # => 'Unknown user' 22 | user.email # => 'unknown@example.com' 23 | user.phone # => Dry::Initializer::UNDEFINED 24 | 25 | user = User.new 'Vladimir', email: 'vladimir@example.com', phone: '71234567788' 26 | user.name # => 'Vladimir' 27 | user.email # => 'vladimir@example.com' 28 | user.phone # => '71234567788' 29 | ``` 30 | 31 | You cannot define required **parameter** after optional one. The following example raises `SyntaxError` exception: 32 | 33 | ```ruby 34 | require 'dry-initializer' 35 | 36 | class User 37 | extend Dry::Initializer 38 | 39 | param :name, default: proc { 'Unknown name' } 40 | param :email # => # 41 | end 42 | ``` 43 | 44 | You should assign `nil` value explicitly. Otherwise an instance variable it will be left undefined. In both cases attribute reader method will return `nil`. 45 | 46 | ```ruby 47 | require 'dry-initializer' 48 | 49 | class User 50 | extend Dry::Initializer 51 | 52 | param :name 53 | option :email, optional: true 54 | end 55 | 56 | user = User.new 'Andrew' 57 | user.email # => nil 58 | user.instance_variable_get :@email 59 | # => Dry::Initializer::UNDEFINED 60 | 61 | user = User.new 'Andrew', email: nil 62 | user.email # => nil 63 | user.instance_variable_get :@email 64 | # => nil 65 | ``` 66 | 67 | You can also set `nil` as a default value: 68 | 69 | ```ruby 70 | require 'dry-initializer' 71 | 72 | class User 73 | extend Dry::Initializer 74 | 75 | param :name 76 | option :email, default: proc { nil } 77 | end 78 | 79 | user = User.new 'Andrew' 80 | user.email # => nil 81 | user.instance_variable_get :@email 82 | # => nil 83 | ``` 84 | 85 | You **must** wrap default values into procs. 86 | 87 | If you need to **assign** proc as a default value, wrap it to another one: 88 | 89 | ```ruby 90 | require 'dry-initializer' 91 | 92 | class User 93 | extend Dry::Initializer 94 | 95 | param :name_proc, default: proc { proc { 'Unknown user' } } 96 | end 97 | 98 | user = User.new 99 | user.name_proc.call # => 'Unknown user' 100 | ``` 101 | 102 | Proc will be executed in a scope of new instance. You can refer to other arguments: 103 | 104 | ```ruby 105 | require 'dry-initializer' 106 | 107 | class User 108 | extend Dry::Initializer 109 | 110 | param :name 111 | param :email, default: proc { "#{name.downcase}@example.com" } 112 | end 113 | 114 | user = User.new 'Andrew' 115 | user.email # => 'andrew@example.com' 116 | ``` 117 | -------------------------------------------------------------------------------- /docsite/source/params-and-options.html.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Params and Options 3 | layout: gem-single 4 | name: dry-initializer 5 | --- 6 | 7 | Use `param` to define plain argument: 8 | 9 | ```ruby 10 | require 'dry-initializer' 11 | 12 | class User 13 | extend Dry::Initializer 14 | 15 | param :name 16 | param :email 17 | end 18 | 19 | user = User.new 'Andrew', 'andrew@email.com' 20 | user.name # => 'Andrew' 21 | user.email # => 'andrew@email.com' 22 | ``` 23 | 24 | Use `option` to define named (hash) argument: 25 | 26 | ```ruby 27 | require 'dry-initializer' 28 | 29 | class User 30 | extend Dry::Initializer 31 | 32 | option :name 33 | option :email 34 | end 35 | 36 | user = User.new email: 'andrew@email.com', name: 'Andrew' 37 | user.name # => 'Andrew' 38 | user.email # => 'andrew@email.com' 39 | ``` 40 | 41 | Options can be renamed using `as:` key: 42 | 43 | ```ruby 44 | require 'dry-initializer' 45 | 46 | class User 47 | extend Dry::Initializer 48 | 49 | option :name, as: :username 50 | end 51 | 52 | user = User.new name: "Joe" 53 | user.username # => "Joe" 54 | user.instance_variable_get :@username # => "Joe" 55 | user.instance_variable_get :@name # => nil 56 | user.respond_to? :name # => false 57 | ``` 58 | 59 | You can also define several ways of initializing the same argument via different options: 60 | 61 | ```ruby 62 | require 'dry-initializer' 63 | 64 | class User 65 | extend Dry::Initializer 66 | 67 | option :phone 68 | option :telephone, as: :phone 69 | option :name, optional: true 70 | end 71 | 72 | User.new(phone: '1234567890').phone # => '1234567890' 73 | User.new(telephone: '1234567890').phone # => '1234567890' 74 | ``` 75 | -------------------------------------------------------------------------------- /docsite/source/rails-support.html.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Rails Support 3 | layout: gem-single 4 | name: dry-initializer 5 | --- 6 | 7 | Rails plugin is implemented in a separate [dry-initializer-rails](https://github.com/nepalez/dry-initializer-rails) gem. 8 | 9 | It provides coercion of assigned values to corresponding ActiveRecord instances. 10 | 11 | ### Base Example 12 | 13 | Add the `:model` setting to `param` or `option`: 14 | 15 | ```ruby 16 | require 'dry-initializer-rails' 17 | 18 | class CreateOrder 19 | extend Dry::Initializer 20 | 21 | # Params and options 22 | param :customer, model: 'Customer' # use either a name 23 | option :product, model: Product # or a class 24 | 25 | def call 26 | Order.create customer: customer, product: product 27 | end 28 | end 29 | ``` 30 | 31 | Now you can assign values as pre-initialized model instances: 32 | 33 | ```ruby 34 | customer = Customer.find(1) 35 | product = Product.find(2) 36 | 37 | order = CreateOrder.new(customer, product: product).call 38 | order.customer # => 39 | order.product # => 40 | ``` 41 | 42 | ...or their ids: 43 | 44 | ```ruby 45 | order = CreateOrder.new(1, product: 2).call 46 | order.customer # => 47 | order.product # => 48 | ``` 49 | 50 | The instance is envoked using method `find_by(id: ...)`. 51 | With wrong ids `nil` values are assigned to corresponding params and options: 52 | 53 | ```ruby 54 | order = CreateOrder.new(0, product: 0).call 55 | order.customer # => nil 56 | order.product # => nil 57 | ``` 58 | 59 | ### Custom Keys 60 | 61 | You can specify custom `key` for searching model instance: 62 | 63 | ```ruby 64 | require 'dry-initializer-rails' 65 | 66 | class CreateOrder 67 | extend Dry::Initializer 68 | 69 | param :customer, model: 'User', find_by: 'name' 70 | option :product, model: Item, find_by: :name 71 | end 72 | ``` 73 | 74 | This time you can send names (not ids) to the initializer: 75 | 76 | ```ruby 77 | order = CreateOrder.new('Andrew', product: 'the_thing_no_123').call 78 | 79 | order.customer # => 80 | order.product # => 81 | ``` 82 | 83 | ### Container Syntax 84 | 85 | If you prefer [container syntax](docs::container-version), extend plugin inside the block: 86 | 87 | ```ruby 88 | require 'dry-initializer-rails' 89 | 90 | class CreateOrder 91 | include Dry::Initializer.define -> do 92 | # ... params/options declarations 93 | end 94 | end 95 | ``` 96 | 97 | ### Types vs Models 98 | 99 | [Type constraints](docs::type-constraints) are checked before the coercion. 100 | 101 | When mixing `:type` and `:model` settings for the same param/option, you should use [sum types](/gems/dry-types/1.2/sum) that accept both model instances and their attributes. 102 | -------------------------------------------------------------------------------- /docsite/source/readers.html.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Readers 3 | layout: gem-single 4 | name: dry-initializer 5 | --- 6 | 7 | By default public attribute reader is defined for every param and option. 8 | 9 | You can define private or protected reader instead: 10 | 11 | ```ruby 12 | require 'dry-initializer' 13 | 14 | class User 15 | extend Dry::Initializer 16 | 17 | param :name, reader: :private # the same as adding `private :name` 18 | param :email, reader: :protected # the same as adding `protected :email` 19 | end 20 | ``` 21 | 22 | To skip any reader, use `reader: false`: 23 | 24 | ```ruby 25 | require 'dry-initializer' 26 | 27 | class User 28 | extend Dry::Initializer 29 | 30 | param :name 31 | param :email, reader: false 32 | end 33 | 34 | user = User.new 'Luke', 'luke@example.com' 35 | user.name # => 'Luke' 36 | 37 | user.email # => # 38 | user.instance_variable_get :@email # => 'luke@example.com' 39 | ``` 40 | 41 | Notice that any other value except for `false`, `:protected` and `:private` provides a public reader. 42 | 43 | No writers are defined. Define them using pure ruby `attr_writer` when necessary. 44 | -------------------------------------------------------------------------------- /docsite/source/skip-undefined.html.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Skip Undefined 3 | layout: gem-single 4 | name: dry-initializer 5 | --- 6 | 7 | The initializer uses special constant `Dry::Initializer::UNDEFINED` to distinguish variables that are set to `nil` from those that are not set at all. 8 | 9 | When no value was provided, the constant is assigned to a variable, but hidden in a reader. 10 | 11 | ```ruby 12 | require 'dry-initializer' 13 | 14 | class User 15 | extend Dry::Initializer 16 | option :email, optional: true 17 | end 18 | 19 | user = User.new 20 | 21 | user.email 22 | # => nil 23 | 24 | user.instance_variable_get :@email 25 | # => Dry::Initializer::UNDEFINED 26 | ``` 27 | 28 | This gives you full control of the real state of the attributes. However, all that checks cost about >30% of instantiation time, and make attribute readers 2 times slower. 29 | 30 | To avoid the overhead in cases you don't care about the differences between `nil` and undefined, you can use a light version of the module. Add `[undefined: false]` config to either `extend` or `include` line of code: 31 | 32 | ```ruby 33 | extend Dry::Initializer[undefined: false] 34 | ``` 35 | 36 | ```ruby 37 | include Dry::Initializer[undefined: false].define -> do 38 | # ... 39 | end 40 | ``` 41 | 42 | This time you should expect `nil` every time no value was given to an optional attribute: 43 | 44 | ```ruby 45 | require 'dry-initializer' 46 | 47 | class User 48 | extend Dry::Initializer[undefined: false] 49 | option :email, optional: true 50 | end 51 | 52 | user = User.new 53 | 54 | user.email 55 | # => nil 56 | 57 | user.instance_variable_get :@email 58 | # => nil 59 | ``` 60 | -------------------------------------------------------------------------------- /docsite/source/tolerance-to-unknown-arguments.html.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Tolerance to Unknown Arguments 3 | layout: gem-single 4 | name: dry-initializer 5 | --- 6 | 7 | By default the initializer is tolerant for both params (positional arguments) and options. 8 | All unknown arguments of the initializer are ignored silently. 9 | 10 | ```ruby 11 | require 'dry-initializer' 12 | 13 | class User 14 | extend Dry::Initializer 15 | end 16 | 17 | user = User.new 'Joe', role: 'admin' 18 | user.respond_to? :role # => false 19 | 20 | User.dry_initializer.attributes(user) 21 | # => {} 22 | ``` 23 | -------------------------------------------------------------------------------- /docsite/source/type-constraints.html.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Type Constraints 3 | layout: gem-single 4 | name: dry-initializer 5 | --- 6 | 7 | ## Base Syntax 8 | 9 | Use `:type` key in a `param` or `option` declarations to add type coercer. 10 | 11 | ```ruby 12 | require 'dry-initializer' 13 | 14 | class User 15 | extend Dry::Initializer 16 | param :name, type: proc(&:to_s) 17 | end 18 | 19 | user = User.new :Andrew 20 | user.name # => "Andrew" 21 | ``` 22 | 23 | Any object that responds to `#call` with 1 argument can be used as a type. Common examples are `proc(&:to_s)` for strings, `method(:Array)` (for arrays) or `Array.method(:wrap)` in Rails, `->(v) { !!v }` (for booleans), etc. 24 | 25 | ## Dry Types as coercers 26 | 27 | Another important example is the usage of `dry-types` as type constraints: 28 | 29 | ```ruby 30 | require 'dry-initializer' 31 | require 'dry-types' 32 | 33 | class User 34 | extend Dry::Initializer 35 | param :name, type: Dry::Types['strict.string'] 36 | end 37 | 38 | user = User.new :Andrew # => # 39 | ``` 40 | 41 | ## Positional Argument 42 | 43 | Instead of `:type` option you can send a constraint/coercer as the second argument: 44 | 45 | ```ruby 46 | require 'dry-initializer' 47 | require 'dry-types' 48 | 49 | class User 50 | extend Dry::Initializer 51 | param :name, Dry::Types['coercible.string'] 52 | param :email, proc(&:to_s) 53 | end 54 | ``` 55 | 56 | ## Array Types 57 | 58 | As mentioned above, the `:type` option takes a callable object... with one important exception. 59 | 60 | You can use arrays for values that should be wrapped to array: 61 | 62 | ```ruby 63 | class User 64 | extend Dry::Initializer 65 | 66 | option :name, proc(&:to_s) 67 | option :emails, [proc(&:to_s)] 68 | end 69 | 70 | user = User.new name: "joe", emails: :"joe@example.com" 71 | user.emails # => ["joe@example.com"] 72 | 73 | user = User.new name: "jane", emails: [:"jane@example.com", :"jane@example.org"] 74 | user.emails # => ["jane@example.com", "jane@example.org"] 75 | ``` 76 | 77 | You can wrap the coercer into several arrays as well: 78 | 79 | ```ruby 80 | class User 81 | extend Dry::Initializer 82 | 83 | option :emails, [[proc(&:to_s)]] 84 | end 85 | 86 | user = User.new name: "joe", emails: "joe@example.com" 87 | user.emails # => [["joe@example.com"]] 88 | ``` 89 | 90 | Eventually, you can use an empty array as a coercer. In that case we just wrap the source value(s) into array, not modifying the items: 91 | 92 | ```ruby 93 | class Article 94 | extend Dry::Initializer 95 | 96 | option :tags, [] 97 | end 98 | 99 | article = Article.new(tags: 1) 100 | article.tags # => [1] 101 | ``` 102 | 103 | ## Nested Options 104 | 105 | Sometimes you need to describe a structure with nested options. In this case you can use a block with `options` inside. 106 | 107 | ```ruby 108 | class User 109 | extend Dry::Initializer 110 | 111 | option :name, proc(&:to_s) 112 | 113 | option :emails, [] do 114 | option :address, proc(&:to_s) 115 | option :description, proc(&:to_s) 116 | end 117 | end 118 | 119 | user = User.new name: "joe", 120 | emails: { address: "joe@example.com", description: "Job email" } 121 | 122 | user.emails.class # => Array 123 | user.emails.first.class # => User::Emails 124 | user.emails.first.address # => "joe@example.com" 125 | 126 | user.emails.map(&:to_h) # => [{ address: "joe@example.com", description: "Job email" }] 127 | ``` 128 | 129 | Notice how we mixed array wrapper with a nested type. 130 | 131 | The only syntax restriction here is that you cannot use a positional `param` _inside_ the block. 132 | 133 | ## Back References 134 | 135 | Sometimes you need to refer back to the initialized instance. In this case use a second argument to explicitly give the instance to a coercer: 136 | 137 | ```ruby 138 | class Location < String 139 | attr_reader :parameter # refers back to its parameter 140 | 141 | def initialize(name, parameter) 142 | super(name) 143 | @parameter = parameter 144 | end 145 | end 146 | 147 | class Parameter 148 | extend Dry::Initializer 149 | param :name 150 | param :location, ->(value, param) { Location.new(value, param) } 151 | end 152 | 153 | offset = Parameter.new "offset", "query" 154 | offset.name # => "offset" 155 | offset.location # => "query" 156 | offset.location.parameter == offset # true 157 | ``` 158 | 159 | [dry-types]: https://github.com/dry-rb/dry-types 160 | [dry-types-docs]: http://dry-rb.org/gems/dry-types/ 161 | -------------------------------------------------------------------------------- /dry-initializer.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/initializer/version" 8 | 9 | Gem::Specification.new do |spec| 10 | spec.name = "dry-initializer" 11 | spec.authors = ["Vladimir Kochnev (marshall-lee)", "Andrew Kozin (nepalez)"] 12 | spec.email = ["andrew.kozin@gmail.com"] 13 | spec.license = "MIT" 14 | spec.version = Dry::Initializer::VERSION.dup 15 | 16 | spec.summary = "DSL for declaring params and options of the initializer" 17 | spec.description = spec.summary 18 | spec.homepage = "https://dry-rb.org/gems/dry-initializer" 19 | spec.files = Dir["CHANGELOG.md", "LICENSE", "README.md", "dry-initializer.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-initializer/blob/main/CHANGELOG.md" 26 | spec.metadata["source_code_uri"] = "https://github.com/dry-rb/dry-initializer" 27 | spec.metadata["bug_tracker_uri"] = "https://github.com/dry-rb/dry-initializer/issues" 28 | 29 | spec.required_ruby_version = ">= 3.1.0" 30 | 31 | # to update dependencies edit project.yml 32 | 33 | spec.add_development_dependency "rake" 34 | spec.add_development_dependency "rspec" 35 | end 36 | -------------------------------------------------------------------------------- /lib/dry-initializer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "dry/initializer" 4 | -------------------------------------------------------------------------------- /lib/dry/initializer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "set" 4 | 5 | # Namespace for gems in a dry-rb community 6 | module Dry 7 | # 8 | # DSL for declaring params and options of class initializers 9 | # 10 | module Initializer 11 | require_relative "initializer/undefined" 12 | require_relative "initializer/dsl" 13 | require_relative "initializer/definition" 14 | require_relative "initializer/builders" 15 | require_relative "initializer/config" 16 | require_relative "initializer/mixin" 17 | require_relative "initializer/dispatchers" 18 | 19 | # Adds methods [.[]] and [.define] 20 | extend DSL 21 | 22 | # Gem-related configuration 23 | # @return [Dry::Initializer::Config] 24 | def dry_initializer 25 | @dry_initializer ||= Config.new(self) 26 | end 27 | 28 | # Adds or redefines a parameter of [#dry_initializer] 29 | # @param [Symbol] name 30 | # @param [#call, nil] type (nil) 31 | # @option opts [Proc] :default 32 | # @option opts [Boolean] :optional 33 | # @option opts [Symbol] :as 34 | # @option opts [true, false, :protected, :public, :private] :reader 35 | # @yield block with nested definition 36 | # @return [self] itself 37 | def param(name, type = nil, **opts, &block) 38 | dry_initializer.param(name, type, **opts, &block) 39 | self 40 | end 41 | 42 | # Adds or redefines an option of [#dry_initializer] 43 | # @param (see #param) 44 | # @option (see #param) 45 | # @yield (see #param) 46 | # @return (see #param) 47 | def option(name, type = nil, **opts, &block) 48 | dry_initializer.option(name, type, **opts, &block) 49 | self 50 | end 51 | 52 | private 53 | 54 | def inherited(klass) 55 | super 56 | config = Config.new(klass, null: dry_initializer.null) 57 | klass.send(:instance_variable_set, :@dry_initializer, config) 58 | dry_initializer.children << config 59 | end 60 | 61 | require_relative "initializer/struct" 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/dry/initializer/builders.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dry 4 | module Initializer 5 | # @private 6 | module Builders 7 | require_relative "builders/reader" 8 | require_relative "builders/initializer" 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/dry/initializer/builders/attribute.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dry 4 | module Initializer 5 | module Builders 6 | # @private 7 | class Attribute 8 | def self.[](definition) 9 | new(definition).call 10 | end 11 | 12 | def call 13 | lines.compact 14 | end 15 | 16 | private 17 | 18 | def initialize(definition) 19 | @definition = definition 20 | @option = definition.option 21 | @type = definition.type 22 | @optional = definition.optional 23 | @default = definition.default 24 | @source = definition.source 25 | @ivar = definition.ivar 26 | @null = definition.null ? "Dry::Initializer::UNDEFINED" : "nil" 27 | @opts = "__dry_initializer_options__" 28 | @congif = "__dry_initializer_config__" 29 | @item = "__dry_initializer_definition__" 30 | @val = @option ? "__dry_initializer_value__" : @source 31 | end 32 | 33 | def lines 34 | [ 35 | "", 36 | definition_line, 37 | reader_line, 38 | default_line, 39 | coercion_line, 40 | assignment_line 41 | ] 42 | end 43 | 44 | def reader_line 45 | return unless @option 46 | 47 | @optional ? optional_reader : required_reader 48 | end 49 | 50 | def optional_reader 51 | "#{@val} = #{@opts}.fetch(:'#{@source}', #{@null})" 52 | end 53 | 54 | def required_reader 55 | "#{@val} = #{@opts}.fetch(:'#{@source}')" \ 56 | " { raise KeyError, \"\#{self.class}: #{@definition} is required\" }" 57 | end 58 | 59 | def definition_line 60 | return unless @type || @default 61 | 62 | "#{@item} = __dry_initializer_config__.definitions[:'#{@source}']" 63 | end 64 | 65 | def default_line 66 | return unless @default 67 | 68 | "#{@val} = instance_exec(&#{@item}.default) if #{@null} == #{@val}" 69 | end 70 | 71 | def coercion_line 72 | return unless @type 73 | 74 | arity = @type.is_a?(Proc) ? @type.arity : @type.method(:call).arity 75 | 76 | if arity.equal?(1) || arity.negative? 77 | "#{@val} = #{@item}.type.call(#{@val}) unless #{@null} == #{@val}" 78 | else 79 | "#{@val} = #{@item}.type.call(#{@val}, self) unless #{@null} == #{@val}" 80 | end 81 | end 82 | 83 | def assignment_line 84 | "#{@ivar} = #{@val}" \ 85 | " unless #{@null} == #{@val} && instance_variable_defined?(:#{@ivar})" 86 | end 87 | end 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /lib/dry/initializer/builders/initializer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dry 4 | module Initializer 5 | module Builders 6 | # @private 7 | class Initializer 8 | require_relative "signature" 9 | require_relative "attribute" 10 | 11 | def self.[](config) 12 | new(config).call 13 | end 14 | 15 | def call 16 | lines.flatten.compact.join("\n") 17 | end 18 | 19 | private 20 | 21 | def initialize(config) 22 | @config = config 23 | @definitions = config.definitions.values 24 | end 25 | 26 | def lines 27 | [ 28 | undef_line, 29 | define_line, 30 | params_lines, 31 | options_lines, 32 | end_line 33 | ] 34 | end 35 | 36 | def undef_line 37 | "undef :__dry_initializer_initialize__" \ 38 | " if private_method_defined? :__dry_initializer_initialize__" 39 | end 40 | 41 | def define_line 42 | "private def __dry_initializer_initialize__(#{Signature[@config]})" 43 | end 44 | 45 | def params_lines 46 | @definitions.reject(&:option).flat_map { Attribute[_1] }.map { " #{_1}" } 47 | end 48 | 49 | def options_lines 50 | @definitions.select(&:option).flat_map { Attribute[_1] }.map { " #{_1}" } 51 | end 52 | 53 | def end_line 54 | "end" 55 | end 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/dry/initializer/builders/reader.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dry 4 | module Initializer 5 | module Builders 6 | # @private 7 | class Reader 8 | def self.[](definition) 9 | new(definition).call 10 | end 11 | 12 | def call 13 | lines.flatten.compact.join("\n") 14 | end 15 | 16 | private 17 | 18 | def initialize(definition) 19 | @target = definition.target 20 | @ivar = definition.ivar 21 | @null = definition.null 22 | @reader = definition.reader 23 | end 24 | 25 | def lines 26 | [undef_line, attribute_line, method_lines, type_line] 27 | end 28 | 29 | def undef_line 30 | "undef :#{@target} if method_defined?(:#{@target})" \ 31 | " || private_method_defined?(:#{@target})" \ 32 | " || protected_method_defined?(:#{@target})" 33 | end 34 | 35 | def attribute_line 36 | return unless @reader 37 | 38 | "attr_reader :#{@target}" unless @null 39 | end 40 | 41 | def method_lines 42 | return unless @reader 43 | return unless @null 44 | 45 | [ 46 | "def #{@target}", 47 | " #{@ivar} unless Dry::Initializer::UNDEFINED == #{@ivar}", 48 | "end" 49 | ] 50 | end 51 | 52 | def type_line 53 | "#{@reader} :#{@target}" if %i[private protected].include? @reader 54 | end 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/dry/initializer/builders/signature.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dry 4 | module Initializer 5 | module Builders 6 | # @private 7 | class Signature 8 | def self.[](config) 9 | new(config).call 10 | end 11 | 12 | def call 13 | [*required_params, *optional_params, "*", options].compact.join(", ") 14 | end 15 | 16 | private 17 | 18 | def initialize(config) 19 | @config = config 20 | @options = config.options.any? 21 | @null = config.null ? "Dry::Initializer::UNDEFINED" : "nil" 22 | end 23 | 24 | def required_params 25 | @config.params.reject(&:optional).map(&:source) 26 | end 27 | 28 | def optional_params 29 | @config.params.select(&:optional).map { |rec| "#{rec.source} = #{@null}" } 30 | end 31 | 32 | def options 33 | "**__dry_initializer_options__" if @options 34 | end 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/dry/initializer/config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dry 4 | module Initializer 5 | # 6 | # Gem-related configuration of some class 7 | # 8 | class Config 9 | # @!attribute [r] null 10 | # @return [Dry::Initializer::UNDEFINED, nil] value of unassigned variable 11 | 12 | # @!attribute [r] extended_class 13 | # @return [Class] the class whose config collected by current object 14 | 15 | # @!attribute [r] parent 16 | # @return [Dry::Initializer::Config] parent configuration 17 | 18 | # @!attribute [r] definitions 19 | # @return [Hash] 20 | # hash of attribute definitions with their source names 21 | 22 | attr_reader :null, :extended_class, :parent, :definitions 23 | 24 | # @!attribute [r] mixin 25 | # @return [Module] reference to the module to be included into class 26 | def mixin 27 | @mixin ||= Module.new.tap do |mod| 28 | initializer = self 29 | mod.extend(Mixin::Local) 30 | mod.define_method(:__dry_initializer_config__) do 31 | initializer 32 | end 33 | mod.send :private, :__dry_initializer_config__ 34 | end 35 | end 36 | 37 | # List of configs of all subclasses of the [#extended_class] 38 | # @return [Array] 39 | def children 40 | @children ||= Set.new 41 | end 42 | 43 | # List of definitions for initializer params 44 | # @return [Array] 45 | def params 46 | definitions.values.reject(&:option) 47 | end 48 | 49 | # List of definitions for initializer options 50 | # @return [Array] 51 | def options 52 | definitions.values.select(&:option) 53 | end 54 | 55 | # Adds or redefines a parameter 56 | # @param [Symbol] name 57 | # @param [#call, nil] type (nil) 58 | # @option opts [Proc] :default 59 | # @option opts [Boolean] :optional 60 | # @option opts [Symbol] :as 61 | # @option opts [true, false, :protected, :public, :private] :reader 62 | # @return [self] itself 63 | def param(name, type = nil, **opts, &block) 64 | add_definition(false, name, type, block, **opts) 65 | end 66 | 67 | # Adds or redefines an option of [#dry_initializer] 68 | # 69 | # @param (see #param) 70 | # @option (see #param) 71 | # @return (see #param) 72 | # 73 | def option(name, type = nil, **opts, &block) 74 | add_definition(true, name, type, block, **opts) 75 | end 76 | 77 | # The hash of public attributes for an instance of the [#extended_class] 78 | # @param [Dry::Initializer::Instance] instance 79 | # @return [Hash] 80 | def public_attributes(instance) 81 | definitions.values.each_with_object({}) do |item, obj| 82 | key = item.target 83 | next unless instance.respond_to? key 84 | 85 | val = instance.send(key) 86 | obj[key] = val unless null == val 87 | end 88 | end 89 | 90 | # The hash of assigned attributes for an instance of the [#extended_class] 91 | # @param [Dry::Initializer::Instance] instance 92 | # @return [Hash] 93 | def attributes(instance) 94 | definitions.values.each_with_object({}) do |item, obj| 95 | key = item.target 96 | val = instance.send(:instance_variable_get, item.ivar) 97 | obj[key] = val unless null == val 98 | end 99 | end 100 | 101 | # Code of the `#__initialize__` method 102 | # @return [String] 103 | def code 104 | Builders::Initializer[self] 105 | end 106 | 107 | # Finalizes config 108 | # @return [self] 109 | def finalize 110 | @definitions = final_definitions 111 | check_order_of_params 112 | mixin.class_eval(code, "#{__FILE__}:#{__LINE__} class_eval") 113 | children.each(&:finalize) 114 | self 115 | end 116 | 117 | # Human-readable representation of configured params and options 118 | # @return [String] 119 | def inch 120 | line = Builders::Signature[self] 121 | line = line.gsub("__dry_initializer_options__", "options") 122 | lines = ["@!method initialize(#{line})"] 123 | lines += ["Initializes an instance of #{extended_class}"] 124 | lines += definitions.values.map(&:inch) 125 | lines += ["@return [#{extended_class}]"] 126 | lines.join("\n") 127 | end 128 | 129 | private 130 | 131 | def initialize(extended_class = nil, null: UNDEFINED) 132 | @extended_class = extended_class.tap { |klass| klass&.include mixin } 133 | sklass = extended_class&.superclass 134 | @parent = sklass.dry_initializer if sklass.is_a? Dry::Initializer 135 | @null = null || parent&.null 136 | @definitions = {} 137 | finalize 138 | end 139 | 140 | def add_definition(option, name, type, block, **opts) 141 | opts = { 142 | parent: extended_class, 143 | option:, 144 | null:, 145 | source: name, 146 | type:, 147 | block:, 148 | **opts 149 | } 150 | 151 | options = Dispatchers.call(**opts) 152 | definition = Definition.new(**options) 153 | definitions[definition.source] = definition 154 | finalize 155 | mixin.class_eval definition.code 156 | end 157 | 158 | def final_definitions 159 | parent_definitions = Hash(parent&.definitions&.dup) 160 | definitions.each_with_object(parent_definitions) do |(key, val), obj| 161 | obj[key] = check_type(obj[key], val) 162 | end 163 | end 164 | 165 | def check_type(previous, current) 166 | return current unless previous 167 | return current if previous.option == current.option 168 | 169 | raise SyntaxError, 170 | "cannot reload #{previous} of #{extended_class.superclass}" \ 171 | " by #{current} of its subclass #{extended_class}" 172 | end 173 | 174 | def check_order_of_params 175 | params.inject(nil) do |optional, current| 176 | if current.optional 177 | current 178 | elsif optional 179 | raise SyntaxError, "#{extended_class}: required #{current}" \ 180 | " goes after optional #{optional}" 181 | else 182 | optional 183 | end 184 | end 185 | end 186 | end 187 | end 188 | end 189 | -------------------------------------------------------------------------------- /lib/dry/initializer/definition.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dry 4 | module Initializer 5 | # 6 | # @private 7 | # @abstract 8 | # 9 | # Base class for parameter or option definitions 10 | # Defines methods to add corresponding reader to the class, 11 | # and build value of instance attribute. 12 | # 13 | class Definition 14 | attr_reader :option, :null, :source, :target, :ivar, 15 | :type, :optional, :default, :reader, 16 | :desc 17 | 18 | def options 19 | { 20 | as: target, 21 | type:, 22 | optional:, 23 | default:, 24 | reader:, 25 | desc: 26 | }.compact 27 | end 28 | 29 | def name 30 | @name ||= "#{option ? "option" : "parameter"} '#{source}'" 31 | end 32 | alias_method :to_s, :name 33 | alias_method :to_str, :name 34 | alias_method :inspect, :name 35 | 36 | def ==(other) 37 | other.instance_of?(self.class) && (other.source == source) 38 | end 39 | 40 | def code 41 | Builders::Reader[self] 42 | end 43 | 44 | def inch 45 | @inch ||= (option ? "@option" : "@param ").tap do |text| 46 | text << " [Object]" 47 | text << (option ? " :#{source}" : " #{source}") 48 | text << (optional ? " (optional)" : " (required)") 49 | text << " #{desc}" if desc 50 | end 51 | end 52 | 53 | private 54 | 55 | def initialize(**options) 56 | @option = options[:option] 57 | @null = options[:null] 58 | @source = options[:source] 59 | @target = options[:target] 60 | @ivar = "@#{@target}" 61 | @type = options[:type] 62 | @reader = options[:reader] 63 | @default = options[:default] 64 | @optional = options[:optional] 65 | @desc = options[:desc] 66 | end 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/dry/initializer/dispatchers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # The module is responsible for __normalizing__ arguments 4 | # of `.param` and `.option`. 5 | # 6 | # What the module does is convert the source list of arguments 7 | # into the standard set of options: 8 | # - `:option` -- whether an argument is an option (or param) 9 | # - `:source` -- the name of source option 10 | # - `:target` -- the target name of the reader 11 | # - `:reader` -- if the reader's privacy (:public, :protected, :private, nil) 12 | # - `:ivar` -- the target name of the variable 13 | # - `:type` -- the callable coercer of the source value 14 | # - `:optional` -- if the argument is optional 15 | # - `:default` -- the proc returning the default value of the source value 16 | # - `:null` -- the value to be set to unassigned optional argument 17 | # 18 | # It is this set is used to build [Dry::Initializer::Definition]. 19 | # 20 | # @example 21 | # # from `option :foo, [], as: :bar, optional: :true 22 | # input = { name: :foo, as: :bar, type: [], optional: true } 23 | # 24 | # Dry::Initializer::Dispatcher.call(input) 25 | # # => { 26 | # # source: "foo", 27 | # # target: "bar", 28 | # # reader: :public, 29 | # # ivar: "@bar", 30 | # # type: ->(v) { Array(v) } }, # simplified for brevity 31 | # # optional: true, 32 | # # default: -> { Dry::Initializer::UNDEFINED }, 33 | # # } 34 | # 35 | # # Settings 36 | # 37 | # The module uses global setting `null` to define what value 38 | # should be set to variables that kept unassigned. By default it 39 | # uses `Dry::Initializer::UNDEFINED` 40 | # 41 | # # Syntax Extensions 42 | # 43 | # The module supports syntax extensions. You can add any number 44 | # of custom dispatchers __on top__ of the stack of default dispatchers. 45 | # Every dispatcher should be a callable object that takes 46 | # the source set of options and converts it to another set of options. 47 | # 48 | # @example Add special dispatcher 49 | # 50 | # # Define a dispatcher for key :integer 51 | # dispatcher = proc do |integer: false, **opts| 52 | # opts.merge(type: proc(&:to_i)) if integer 53 | # end 54 | # 55 | # # Register a dispatcher 56 | # Dry::Initializer::Dispatchers << dispatcher 57 | # 58 | # # Now you can use option `integer: true` instead of `type: proc(&:to_i)` 59 | # class Foo 60 | # extend Dry::Initializer 61 | # param :id, integer: true 62 | # end 63 | # 64 | module Dry 65 | module Initializer 66 | module Dispatchers 67 | extend self 68 | 69 | # @!attribute [rw] null Defines a value to be set to unassigned attributes 70 | # @return [Object] 71 | attr_accessor :null 72 | 73 | # 74 | # Registers a new dispatcher 75 | # 76 | # @param [#call] dispatcher 77 | # @return [self] itself 78 | # 79 | def <<(dispatcher) 80 | @pipeline = [dispatcher] + pipeline 81 | self 82 | end 83 | 84 | # 85 | # Normalizes the source set of options 86 | # 87 | # @param [Hash] options 88 | # @return [Hash] normalized set of options 89 | # 90 | def call(**options) 91 | options = {null:, **options} 92 | pipeline.reduce(options) { |opts, dispatcher| dispatcher.call(**opts) } 93 | end 94 | 95 | private 96 | 97 | require_relative "dispatchers/build_nested_type" 98 | require_relative "dispatchers/check_type" 99 | require_relative "dispatchers/prepare_default" 100 | require_relative "dispatchers/prepare_ivar" 101 | require_relative "dispatchers/prepare_optional" 102 | require_relative "dispatchers/prepare_reader" 103 | require_relative "dispatchers/prepare_source" 104 | require_relative "dispatchers/prepare_target" 105 | require_relative "dispatchers/unwrap_type" 106 | require_relative "dispatchers/wrap_type" 107 | 108 | def pipeline 109 | @pipeline ||= [ 110 | PrepareSource, PrepareTarget, PrepareIvar, PrepareReader, 111 | PrepareDefault, PrepareOptional, 112 | UnwrapType, CheckType, BuildNestedType, WrapType 113 | ] 114 | end 115 | end 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /lib/dry/initializer/dispatchers/build_nested_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Prepare nested data type from a block 4 | # 5 | # @example 6 | # option :foo do 7 | # option :bar 8 | # option :qux 9 | # end 10 | # 11 | module Dry 12 | module Initializer 13 | module Dispatchers 14 | module BuildNestedType 15 | extend self 16 | 17 | # rubocop: disable Metrics/ParameterLists 18 | def call(parent:, source:, target:, type: nil, block: nil, **options) 19 | check_certainty!(source, type, block) 20 | check_name!(target, block) 21 | type ||= build_nested_type(parent, target, block) 22 | {parent:, source:, target:, type:, **options} 23 | end 24 | # rubocop: enable Metrics/ParameterLists 25 | 26 | private 27 | 28 | def check_certainty!(source, type, block) 29 | return unless block 30 | return unless type 31 | 32 | raise ArgumentError, <<~MESSAGE 33 | You should define coercer of values of argument '#{source}' 34 | either though the parameter/option, or via nested block, but not the both. 35 | MESSAGE 36 | end 37 | 38 | def check_name!(name, block) 39 | return unless block 40 | return unless name[/^_|__|_$/] 41 | 42 | raise ArgumentError, <<~MESSAGE 43 | The name of the argument '#{name}' cannot be used for nested struct. 44 | A proper name can use underscores _ to divide alphanumeric parts only. 45 | MESSAGE 46 | end 47 | 48 | def build_nested_type(parent, name, block) 49 | return unless block 50 | 51 | klass_name = full_name(parent, name) 52 | build_struct(klass_name, block) 53 | end 54 | 55 | def full_name(parent, name) 56 | "::#{parent.name}::#{name.to_s.split("_").compact.map(&:capitalize).join}" 57 | end 58 | 59 | def build_struct(klass_name, block) 60 | # rubocop: disable Security/Eval 61 | eval <<~RUBY, TOPLEVEL_BINDING, __FILE__, __LINE__ + 1 62 | class #{klass_name} < Dry::Initializer::Struct 63 | end 64 | RUBY 65 | # rubocop: enable Style/DocumentDynamicEvalDefinition 66 | # rubocop: enable Security/Eval 67 | const_get(klass_name).tap { _1.class_eval(&block) } 68 | end 69 | end 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/dry/initializer/dispatchers/check_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Checks whether an unwrapped type is valid 4 | # 5 | module Dry 6 | module Initializer 7 | module Dispatchers 8 | module CheckType 9 | extend self 10 | 11 | def call(source:, type: nil, wrap: 0, **options) 12 | check_if_callable! source, type 13 | check_arity! source, type, wrap 14 | 15 | {source:, type:, wrap:, **options} 16 | end 17 | 18 | private 19 | 20 | def check_if_callable!(source, type) 21 | return if type.nil? 22 | return if type.respond_to?(:call) 23 | 24 | raise ArgumentError, 25 | "The type of the argument '#{source}' should be callable" 26 | end 27 | 28 | def check_arity!(_source, type, wrap) 29 | return if type.nil? 30 | return if wrap.zero? 31 | return if type.method(:call).arity.abs == 1 32 | 33 | raise ArgumentError, <<~MESSAGE 34 | The dry_intitializer supports wrapped types with one argument only. 35 | You cannot use array types with element coercers having several arguments. 36 | 37 | For example, this definitions are correct: 38 | option :foo, [proc(&:to_s)] 39 | option :bar, type: [[]] 40 | option :baz, ->(a, b) { [a, b] } 41 | 42 | While this is not: 43 | option :foo, [->(a, b) { [a, b] }] 44 | MESSAGE 45 | end 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/dry/initializer/dispatchers/prepare_default.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Prepares the `:default` option 4 | # 5 | # It must respond to `.call` without arguments 6 | # 7 | module Dry 8 | module Initializer 9 | module Dispatchers 10 | module PrepareDefault 11 | extend self 12 | 13 | def call(default: nil, optional: nil, **options) 14 | default = callable! default 15 | check_arity! default 16 | 17 | {default:, optional: (optional | default), **options} 18 | end 19 | 20 | private 21 | 22 | def callable!(default) 23 | return unless default 24 | return default if default.respond_to?(:call) 25 | return callable(default.to_proc) if default.respond_to?(:to_proc) 26 | 27 | invalid!(default) 28 | end 29 | 30 | def check_arity!(default) 31 | return unless default 32 | 33 | arity = default.method(:call).arity.to_i 34 | return unless arity.positive? 35 | 36 | invalid!(default) 37 | end 38 | 39 | def invalid!(default) 40 | raise TypeError, "The #{default.inspect} should be" \ 41 | " either convertable to proc with no arguments," \ 42 | " or respond to #call without arguments." 43 | end 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/dry/initializer/dispatchers/prepare_ivar.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Prepares the variable name of a parameter or an option. 4 | # 5 | module Dry 6 | module Initializer 7 | module Dispatchers 8 | module PrepareIvar 9 | module_function 10 | 11 | def call(target:, **options) 12 | ivar = "@#{target}".delete("?").to_sym 13 | 14 | {target:, ivar:, **options} 15 | end 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/dry/initializer/dispatchers/prepare_optional.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Defines whether an argument is optional 4 | # 5 | module Dry 6 | module Initializer 7 | module Dispatchers 8 | module PrepareOptional 9 | module_function 10 | 11 | def call(optional: nil, default: nil, required: nil, **options) 12 | optional ||= default 13 | optional &&= !required 14 | 15 | {optional: !!optional, default:, **options} 16 | end 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/dry/initializer/dispatchers/prepare_reader.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Checks the reader privacy 4 | # 5 | module Dry 6 | module Initializer 7 | module Dispatchers 8 | module PrepareReader 9 | extend self 10 | 11 | def call(target: nil, reader: :public, **options) 12 | reader = case reader.to_s 13 | when "false", "" then nil 14 | when "true" then :public 15 | when "public", "private", "protected" then reader.to_sym 16 | else invalid_reader!(target, reader) 17 | end 18 | 19 | {target:, reader:, **options} 20 | end 21 | 22 | private 23 | 24 | def invalid_reader!(target, _reader) 25 | raise ArgumentError, <<~MESSAGE 26 | Invalid setting for the ##{target} reader's privacy. 27 | Use the one of the following values for the `:reader` option: 28 | - 'public' (true) for the public reader (default) 29 | - 'private' for the private reader 30 | - 'protected' for the protected reader 31 | - nil (false) if no reader should be defined 32 | MESSAGE 33 | end 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/dry/initializer/dispatchers/prepare_source.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # The dispatcher verifies a correctness of the source name 4 | # of param or option, taken as a `:source` option. 5 | # 6 | # We allow any stringified name for the source. 7 | # For example, this syntax is correct because we accept any key 8 | # in the original hash of arguments, but give them proper names: 9 | # 10 | # ```ruby 11 | # class Foo 12 | # extend Dry::Initializer 13 | # 14 | # option "", as: :first 15 | # option 1, as: :second 16 | # end 17 | # 18 | # foo = Foo.new("": 42, 1: 666) 19 | # foo.first # => 42 20 | # foo.second # => 666 21 | # ``` 22 | # 23 | module Dry 24 | module Initializer 25 | module Dispatchers 26 | module PrepareSource 27 | module_function 28 | 29 | def call(source:, **options) 30 | {source: source.to_s.to_sym, **options} 31 | end 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/dry/initializer/dispatchers/prepare_target.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Prepares the target name of a parameter or an option. 4 | # 5 | # Unlike source, the target must satisfy requirements for Ruby variable names. 6 | # It also shouldn't be in conflict with names used by the gem. 7 | # 8 | module Dry 9 | module Initializer 10 | module Dispatchers 11 | module PrepareTarget 12 | extend self 13 | 14 | # List of variable names reserved by the gem 15 | RESERVED = %i[ 16 | __dry_initializer_options__ 17 | __dry_initializer_config__ 18 | __dry_initializer_value__ 19 | __dry_initializer_definition__ 20 | __dry_initializer_initializer__ 21 | ].freeze 22 | 23 | def call(source:, target: nil, as: nil, **options) 24 | target ||= as || source 25 | target = target.to_s.to_sym.downcase 26 | 27 | check_ruby_name!(target) 28 | check_reserved_names!(target) 29 | 30 | {source:, target:, **options} 31 | end 32 | 33 | private 34 | 35 | def check_ruby_name!(target) 36 | return if target[/\A[[:alpha:]_][[:alnum:]_]*\??\z/u] 37 | 38 | raise ArgumentError, 39 | "The name `#{target}` is not allowed for Ruby methods" 40 | end 41 | 42 | def check_reserved_names!(target) 43 | return unless RESERVED.include?(target) 44 | 45 | raise ArgumentError, 46 | "The method name `#{target}` is reserved by the dry-initializer gem" 47 | end 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/dry/initializer/dispatchers/unwrap_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Looks at the `:type` option and counts how many nested arrays 4 | # it contains around either nil or a callable value. 5 | # 6 | # The counted number is preserved in the `:wrap` virtual option 7 | # used by the [WrapType] dispatcher. 8 | # 9 | module Dry 10 | module Initializer 11 | module Dispatchers 12 | module UnwrapType 13 | extend self 14 | 15 | def call(type: nil, wrap: 0, **options) 16 | type, count = unwrap(type, wrap) 17 | 18 | {type:, wrap: count, **options} 19 | end 20 | 21 | private 22 | 23 | def unwrap(type, count) 24 | if type.is_a?(::Array) 25 | unwrap(type.first, count + 1) 26 | else 27 | [type, count] 28 | end 29 | end 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/dry/initializer/dispatchers/wrap_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Takes `:type` and `:wrap` to construct the final value coercer 4 | # 5 | module Dry 6 | module Initializer 7 | module Dispatchers 8 | module WrapType 9 | extend self 10 | 11 | def call(type: nil, wrap: 0, **options) 12 | {type: wrapped_type(type, wrap), **options} 13 | end 14 | 15 | private 16 | 17 | def wrapped_type(type, count) 18 | return type if count.zero? 19 | 20 | ->(value) { wrap_value(value, count, type) } 21 | end 22 | 23 | def wrap_value(value, count, type) 24 | if count.zero? 25 | type ? type.call(value) : value 26 | else 27 | return [wrap_value(value, count - 1, type)] unless value.is_a?(Array) 28 | 29 | value.map { |item| wrap_value(item, count - 1, type) } 30 | end 31 | end 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/dry/initializer/dsl.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dry 4 | module Initializer 5 | # Module-level DSL 6 | module DSL 7 | # Setting for null (undefined value) 8 | # @return [nil, Dry::Initializer::UNDEFINED] 9 | attr_reader :null 10 | 11 | # Returns a version of the module with custom settings 12 | # @option settings [Boolean] :undefined 13 | # If unassigned params and options should be treated different from nil 14 | # @return [Dry::Initializer] 15 | def [](undefined: true, **) 16 | null = undefined == false ? nil : UNDEFINED 17 | Module.new.tap do |mod| 18 | mod.extend DSL 19 | mod.include self 20 | mod.send(:instance_variable_set, :@null, null) 21 | end 22 | end 23 | 24 | # Returns mixin module to be included to target class by hand 25 | # @return [Module] 26 | # @yield proc defining params and options 27 | def define(procedure = nil, &block) 28 | config = Config.new(null:) 29 | config.instance_exec(&procedure || block) 30 | config.mixin.include Mixin::Root 31 | config.mixin 32 | end 33 | 34 | private 35 | 36 | def extended(klass) 37 | config = Config.new(klass, null:) 38 | klass.send :instance_variable_set, :@dry_initializer, config 39 | klass.include Mixin::Root 40 | end 41 | 42 | class << self 43 | private 44 | 45 | def extended(mod) 46 | mod.instance_variable_set :@null, UNDEFINED 47 | end 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/dry/initializer/mixin.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dry 4 | module Initializer 5 | # @private 6 | module Mixin 7 | extend DSL # @deprecated 8 | include Dry::Initializer # @deprecated 9 | # @deprecated 10 | def self.extended(klass) 11 | warn "[DEPRECATED] Use Dry::Initializer instead of its alias" \ 12 | " Dry::Initializer::Mixin. The later will be removed in v2.1.0" 13 | super 14 | end 15 | 16 | require_relative "mixin/root" 17 | require_relative "mixin/local" 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/dry/initializer/mixin/local.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dry 4 | module Initializer 5 | module Mixin 6 | # @private 7 | module Local 8 | attr_reader :klass 9 | 10 | def inspect 11 | "Dry::Initializer::Mixin::Local[#{klass}]" 12 | end 13 | alias_method :to_s, :inspect 14 | alias_method :to_str, :inspect 15 | 16 | private 17 | 18 | def included(klass) 19 | @klass = klass 20 | super 21 | end 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/dry/initializer/mixin/root.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dry 4 | module Initializer 5 | module Mixin 6 | # @private 7 | module Root 8 | private 9 | 10 | def initialize(*args, **kwargs) 11 | __dry_initializer_initialize__(*args, **kwargs) 12 | end 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/dry/initializer/struct.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # The nested structure that takes nested hashes with indifferent access 4 | # 5 | module Dry 6 | module Initializer 7 | class Struct 8 | extend ::Dry::Initializer 9 | 10 | class << self 11 | undef_method :param 12 | 13 | def new(options) 14 | super(**Hash(options).each_with_object({}) { |(k, v), h| h[k.to_sym] = v }) 15 | end 16 | alias_method :call, :new 17 | end 18 | 19 | # 20 | # Represents event data as a nested hash with deeply stringified keys 21 | # @return [Hash] 22 | # 23 | def to_h 24 | self 25 | .class 26 | .dry_initializer 27 | .attributes(self) 28 | .each_with_object({}) { |(k, v), h| h[k.to_s] = __hashify(v) } 29 | end 30 | 31 | private 32 | 33 | def __hashify(value) 34 | case value 35 | when Hash 36 | value.each_with_object({}) { |(k, v), obj| obj[k.to_s] = __hashify(v) } 37 | when Array then value.map { |v| __hashify(v) } 38 | when Dry::Initializer::Struct then value.to_h 39 | else value 40 | end 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/dry/initializer/undefined.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dry 4 | module Initializer 5 | module UNDEFINED 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/dry/initializer/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dry 4 | module Initializer 5 | VERSION = "3.2.0" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/tasks/benchmark.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | namespace :benchmark do 4 | desc "Runs benchmarks for plain params" 5 | task :plain_params do 6 | system "ruby benchmarks/plain_params.rb" 7 | end 8 | 9 | desc "Runs benchmarks for plain options" 10 | task :plain_options do 11 | system "ruby benchmarks/plain_options.rb" 12 | end 13 | 14 | desc "Runs benchmarks for value coercion" 15 | task :with_coercion do 16 | system "ruby benchmarks/with_coercion.rb" 17 | end 18 | 19 | desc "Runs benchmarks with defaults" 20 | task :with_defaults do 21 | system "ruby benchmarks/with_defaults.rb" 22 | end 23 | 24 | desc "Runs benchmarks with defaults and coercion" 25 | task :with_defaults_and_coercion do 26 | system "ruby benchmarks/with_defaults_and_coercion.rb" 27 | end 28 | 29 | desc "Runs benchmarks for several defaults" 30 | task :compare_several_defaults do 31 | system "ruby benchmarks/with_several_defaults.rb" 32 | end 33 | end 34 | 35 | desc "Runs all benchmarks" 36 | task benchmark: %i[ 37 | benchmark:plain_params 38 | benchmark:plain_options 39 | benchmark:with_coercion 40 | benchmark:with_defaults 41 | benchmark:with_defaults_and_coercion 42 | benchmark:compare_several_defaults 43 | ] 44 | -------------------------------------------------------------------------------- /lib/tasks/profile.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # rubocop: disable Lint/ConstantDefinitionInBlock 4 | namespace :profile do 5 | def profile(name, execution, &definition) 6 | require "dry-initializer" 7 | require "ruby-prof" 8 | require "fileutils" 9 | 10 | definition.call 11 | result = RubyProf.profile do 12 | 1_000.times { execution.call } 13 | end 14 | 15 | FileUtils.mkdir_p "./tmp" 16 | 17 | FileUtils.touch "./tmp/#{name}.dot" 18 | File.open("./tmp/#{name}.dot", "w+") do |output| 19 | RubyProf::DotPrinter.new(result).print(output, min_percent: 0) 20 | end 21 | 22 | FileUtils.touch "./tmp/#{name}.html" 23 | File.open("./tmp/#{name}.html", "w+") do |output| 24 | RubyProf::CallStackPrinter.new(result).print(output, min_percent: 0) 25 | end 26 | 27 | system "dot -Tpng ./tmp/#{name}.dot > ./tmp/#{name}.png" 28 | end 29 | 30 | desc "Profiles initialization with required param and option" 31 | task :required do 32 | profile("required", -> { User.new :Andy, email: "andy@example.com" }) do 33 | class User 34 | extend Dry::Initializer 35 | param :name 36 | option :email 37 | end 38 | end 39 | end 40 | 41 | desc "Profiles initialization with default param and option" 42 | task :defaults do 43 | profile("defaults", -> { User.new }) do 44 | class User 45 | extend Dry::Initializer 46 | param :name, default: -> { :Andy } 47 | option :email, default: -> { "andy@example.com" } 48 | end 49 | end 50 | end 51 | 52 | desc "Profiles initialization with coerced param and option" 53 | task :coercion do 54 | profile("coercion", -> { User.new :Andy, email: :"andy@example.com" }) do 55 | class User 56 | extend Dry::Initializer 57 | param :name, proc(&:to_s) 58 | option :email, proc(&:to_s) 59 | end 60 | end 61 | end 62 | 63 | desc "Profiles initialization with coerced defaults of param and option" 64 | task :default_coercion do 65 | profile("default_coercion", -> { User.new }) do 66 | class User 67 | extend Dry::Initializer 68 | param :name, proc(&:to_s), default: -> { :Andy } 69 | option :email, proc(&:to_s), default: -> { :"andy@example.com" } 70 | end 71 | end 72 | end 73 | end 74 | 75 | desc "Makes all profiling at once" 76 | task profile: %i[ 77 | profile:required 78 | profile:defaults 79 | profile:coercion 80 | profile:default_coercion 81 | ] 82 | # rubocop: enable Lint/ConstantDefinitionInBlock 83 | -------------------------------------------------------------------------------- /project.yml: -------------------------------------------------------------------------------- 1 | name: dry-initializer 2 | custom_ci: true 3 | codacy_id: 03c8923afd734e9fb0f4bfe9cc893edb 4 | gemspec: 5 | authors: ["Vladimir Kochnev (marshall-lee)", "Andrew Kozin (nepalez)"] 6 | email: ["andrew.kozin@gmail.com"] 7 | summary: "DSL for declaring params and options of the initializer" 8 | development_dependencies: 9 | - rake 10 | - rspec 11 | -------------------------------------------------------------------------------- /spec/attributes_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Dry::Initializer, "dry_initializer.attributes" do 4 | subject { instance.class.dry_initializer.attributes(instance) } 5 | 6 | context "when class has params" do 7 | before do 8 | class Test::Foo 9 | extend Dry::Initializer 10 | param :foo, proc(&:to_s) 11 | param :bar, default: proc { 1 } 12 | param :baz, optional: true 13 | end 14 | end 15 | 16 | let(:instance) { Test::Foo.new(:FOO) } 17 | 18 | it "collects coerced params with default values" do 19 | expect(subject).to eq({foo: "FOO", bar: 1}) 20 | end 21 | end 22 | 23 | context "when class has options" do 24 | before do 25 | class Test::Foo 26 | extend Dry::Initializer 27 | option :foo 28 | option :bar, default: proc { 1 } 29 | option :baz, optional: true 30 | option :qux, proc(&:to_s), as: :quxx 31 | end 32 | end 33 | 34 | let(:instance) { Test::Foo.new(foo: :FOO, qux: :QUX) } 35 | 36 | it "collects coerced and renamed options with default values" do 37 | expect(subject).to eq({foo: :FOO, bar: 1, quxx: "QUX"}) 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/coercion_of_nil_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe "coercion of nil" do 4 | before do 5 | class Test::Foo 6 | extend Dry::Initializer 7 | param :bar, proc(&:to_i) 8 | end 9 | 10 | class Test::Baz 11 | include Dry::Initializer.define -> do 12 | param :qux, proc(&:to_i) 13 | end 14 | end 15 | end 16 | 17 | let(:foo) { Test::Foo.new(nil) } 18 | let(:baz) { Test::Baz.new(nil) } 19 | 20 | it "works with extend syntax" do 21 | expect(foo.bar).to eq 0 22 | end 23 | 24 | it "works with include syntax" do 25 | expect(baz.qux).to eq 0 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/custom_dispatchers_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe "custom dispatchers" do 4 | subject { Test::Foo.new "123" } 5 | 6 | before do 7 | dispatcher = ->(op) { op[:integer] ? op.merge(type: proc(&:to_i)) : op } 8 | Dry::Initializer::Dispatchers << dispatcher 9 | end 10 | 11 | context "with extend syntax" do 12 | before do 13 | class Test::Foo 14 | extend Dry::Initializer 15 | param :id, integer: true 16 | end 17 | end 18 | 19 | it "adds syntax sugar" do 20 | expect(subject.id).to eq 123 21 | end 22 | end 23 | 24 | context "with include syntax" do 25 | before do 26 | class Test::Foo 27 | include Dry::Initializer.define -> do 28 | param :id, integer: true 29 | end 30 | end 31 | end 32 | 33 | it "adds syntax sugar" do 34 | expect(subject.id).to eq 123 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/custom_initializer_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe "custom initializer" do 4 | before do 5 | class Test::Foo 6 | extend Dry::Initializer 7 | 8 | param :bar 9 | 10 | def initialize(...) 11 | super 12 | @bar *= 3 13 | end 14 | end 15 | 16 | class Test::Baz < Test::Foo 17 | param :qux 18 | 19 | def initialize(...) 20 | super 21 | @qux += 1 22 | end 23 | end 24 | end 25 | 26 | it "reloads the initializer" do 27 | baz = Test::Baz.new(5, 5) 28 | 29 | expect(baz.bar).to eq 15 # 5 * 3 30 | expect(baz.qux).to eq 6 # 5 + 1 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/default_values_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe "default values" do 4 | before do 5 | class Test::Foo 6 | extend Dry::Initializer 7 | 8 | param :foo, default: proc { :FOO } 9 | param :bar, default: proc { :BAR } 10 | option :baz, default: -> { :BAZ } 11 | option :qux, default: proc { foo } 12 | option :mox, default: -> { default_mox } 13 | 14 | private 15 | 16 | def default_mox 17 | :MOX 18 | end 19 | end 20 | end 21 | 22 | it "instantiate arguments" do 23 | subject = Test::Foo.new(1, 2, baz: 3, qux: 4) 24 | 25 | expect(subject.foo).to eql 1 26 | expect(subject.bar).to eql 2 27 | expect(subject.baz).to eql 3 28 | expect(subject.qux).to eql 4 29 | end 30 | 31 | it "applies default values" do 32 | subject = Test::Foo.new 33 | 34 | expect(subject.foo).to eql :FOO 35 | expect(subject.bar).to eql :BAR 36 | expect(subject.baz).to eql :BAZ 37 | expect(subject.qux).to eql :FOO 38 | end 39 | 40 | it "applies default values partially" do 41 | subject = Test::Foo.new 1, baz: 3 42 | 43 | expect(subject.foo).to eql 1 44 | expect(subject.bar).to eql :BAR 45 | expect(subject.baz).to eql 3 46 | expect(subject.qux).to eql 1 47 | end 48 | 49 | it "applies default values from private methods" do 50 | subject = Test::Foo.new 51 | expect(subject.mox).to eql :MOX 52 | end 53 | 54 | describe "when the last param has a default and there are no options" do 55 | before do 56 | class Test::Bar 57 | extend Dry::Initializer 58 | 59 | param :foo 60 | param :bar, default: proc { {} } 61 | end 62 | end 63 | 64 | it "instantiates arguments" do 65 | subject = Test::Bar.new(1, 2) 66 | 67 | expect(subject.foo).to eql 1 68 | expect(subject.bar).to eql 2 69 | end 70 | 71 | it "applies default values" do 72 | subject = Test::Bar.new(1) 73 | 74 | expect(subject.foo).to eql 1 75 | expect(subject.bar).to eql({}) 76 | end 77 | 78 | it "instantiates arguments also if the last is an hash" do 79 | subject = Test::Bar.new(1, {baz: 2, qux: 3}) 80 | 81 | expect(subject.foo).to eql 1 82 | expect(subject.bar).to eql({baz: 2, qux: 3}) 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /spec/definition_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe "definition" do 4 | shared_examples :initializer do |in_context| 5 | subject { Test::Foo.new(1, bar: 2) } 6 | 7 | it "sets variables when defined via `#{in_context}`" do 8 | expect(subject.instance_variable_get(:@foo)).to eql 1 9 | expect(subject.instance_variable_get(:@bar)).to eql 2 10 | end 11 | end 12 | 13 | it_behaves_like :initializer, "extend Dry::Initializer" do 14 | before do 15 | class Test::Foo 16 | extend Dry::Initializer 17 | param :foo 18 | option :bar 19 | end 20 | end 21 | 22 | it "preservers definition params" do 23 | params = Test::Foo.dry_initializer.params.map do |definition| 24 | [definition.source, definition.options] 25 | end 26 | 27 | expect(params).to eq [ 28 | [:foo, {as: :foo, reader: :public, optional: false}] 29 | ] 30 | end 31 | 32 | it "preservers definition options" do 33 | options = Test::Foo.dry_initializer.options.map do |definition| 34 | [definition.source, definition.options] 35 | end 36 | 37 | expect(options).to eq [ 38 | [:bar, {as: :bar, reader: :public, optional: false}] 39 | ] 40 | end 41 | end 42 | 43 | it_behaves_like :initializer, "extend Dry::Initializer" do 44 | before do 45 | class Test::Foo 46 | extend Dry::Initializer 47 | param :foo 48 | option :bar 49 | end 50 | end 51 | end 52 | 53 | it_behaves_like :initializer, "extend Dry::Initializer[undefined: false]" do 54 | before do 55 | class Test::Foo 56 | extend Dry::Initializer[undefined: false] 57 | param :foo 58 | option :bar 59 | end 60 | end 61 | end 62 | 63 | it_behaves_like :initializer, "include Dry::Initializer with block" do 64 | before do 65 | class Test::Foo 66 | include( 67 | Dry::Initializer.define do 68 | param :foo 69 | option :bar 70 | end 71 | ) 72 | end 73 | end 74 | end 75 | 76 | it_behaves_like :initializer, "include Dry::Initializer with lambda" do 77 | before do 78 | class Test::Foo 79 | include Dry::Initializer.define -> do 80 | param :foo 81 | option :bar 82 | end 83 | end 84 | end 85 | end 86 | 87 | it_behaves_like :initializer, "include Dry::Initializer[undefined: false]" do 88 | before do 89 | class Test::Foo 90 | include( 91 | Dry::Initializer[undefined: false].define do 92 | param :foo 93 | option :bar 94 | end 95 | ) 96 | end 97 | end 98 | end 99 | 100 | # @deprecated 101 | it_behaves_like :initializer, "include Dry::Initializer::Mixin" do 102 | before do 103 | class Test::Foo 104 | include( 105 | Dry::Initializer::Mixin.define do 106 | param :foo 107 | option :bar 108 | end 109 | ) 110 | end 111 | end 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /spec/invalid_default_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe "invalid default value assignment" do 4 | subject do 5 | class Test::Foo 6 | extend Dry::Initializer 7 | 8 | param :foo, default: 1 9 | end 10 | end 11 | 12 | it "raises TypeError" do 13 | expect { subject }.to raise_error TypeError 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/list_type_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry-types" 4 | 5 | RSpec.describe "list type argument" do 6 | before do 7 | class Test::Foo 8 | extend Dry::Initializer 9 | param :foo, [proc(&:to_s)] 10 | option :bar, [Dry::Types["strict.string"]] 11 | option :baz, [] 12 | end 13 | end 14 | 15 | context "with single items" do 16 | subject { Test::Foo.new(1, bar: "2", baz: {qux: :QUX}) } 17 | 18 | it "coerces and wraps them to arrays" do 19 | expect(subject.foo).to eq %w[1] 20 | expect(subject.bar).to eq %w[2] 21 | expect(subject.baz).to eq [{qux: :QUX}] 22 | end 23 | end 24 | 25 | context "with arrays" do 26 | subject { Test::Foo.new([1], bar: %w[2], baz: [{qux: :QUX}]) } 27 | 28 | it "coerces elements" do 29 | expect(subject.foo).to eq %w[1] 30 | expect(subject.bar).to eq %w[2] 31 | expect(subject.baz).to eq [{qux: :QUX}] 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/missed_default_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe "missed default values" do 4 | subject do 5 | class Test::Foo 6 | extend Dry::Initializer 7 | 8 | param :foo, default: proc { :FOO } 9 | param :bar, required: true 10 | end 11 | end 12 | 13 | it "raises SyntaxError" do 14 | expect { subject }.to raise_error SyntaxError, /bar/ 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/nested_type_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe "nested type argument" do 4 | subject { Test::Xyz.new("bar" => {"baz" => 42}) } 5 | 6 | context "with nested definition only" do 7 | before do 8 | class Test::Xyz 9 | extend Dry::Initializer 10 | 11 | param :foo, as: :x do 12 | option :bar, as: :y do 13 | option :baz, proc(&:to_s), as: :z 14 | option :qux, as: :w, optional: true 15 | end 16 | end 17 | end 18 | end 19 | 20 | it "builds the type" do 21 | expect(subject.x.y.z).to eq "42" 22 | end 23 | 24 | it "converts the nested type to hash" do 25 | expect(subject.x.to_h).to eq("y" => {"z" => "42"}) 26 | end 27 | end 28 | 29 | context "with nested and wrapped definitions" do 30 | before do 31 | class Test::Xyz 32 | extend Dry::Initializer 33 | 34 | param :foo, [], as: :x do 35 | option :bar, as: :y do 36 | option :baz, proc(&:to_s), as: :z 37 | option :qux, as: :w, optional: true 38 | end 39 | end 40 | end 41 | end 42 | 43 | it "builds the type" do 44 | x = subject.x 45 | expect(x).to be_instance_of Array 46 | 47 | expect(x.first.y.z).to eq "42" 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /spec/optional_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe "optional value" do 4 | context "when has no default value" do 5 | before do 6 | class Test::Foo 7 | extend Dry::Initializer 8 | 9 | param :foo 10 | param :bar, optional: true 11 | end 12 | end 13 | 14 | it "quacks like nil" do 15 | subject = Test::Foo.new(1) 16 | 17 | expect(subject.bar).to eq nil 18 | end 19 | 20 | it "keeps info about been UNDEFINED" do 21 | subject = Test::Foo.new(1) 22 | 23 | expect(subject.instance_variable_get(:@bar)) 24 | .to eq Dry::Initializer::UNDEFINED 25 | end 26 | 27 | it "can be set explicitly" do 28 | subject = Test::Foo.new(1, "qux") 29 | 30 | expect(subject.bar).to eq "qux" 31 | end 32 | end 33 | 34 | context "with undefined: false" do 35 | before do 36 | class Test::Foo 37 | extend Dry::Initializer[undefined: false] 38 | 39 | param :foo 40 | param :bar, optional: true 41 | end 42 | end 43 | 44 | it "sets undefined values to nil" do 45 | subject = Test::Foo.new(1) 46 | 47 | expect(subject.instance_variable_get(:@bar)).to be_nil 48 | end 49 | end 50 | 51 | context "when has a default value" do 52 | before do 53 | class Test::Foo 54 | extend Dry::Initializer 55 | 56 | param :foo 57 | param :bar, optional: true, default: proc { "baz" } 58 | end 59 | end 60 | 61 | it "is takes default value" do 62 | subject = Test::Foo.new(1) 63 | 64 | expect(subject.bar).to eq "baz" 65 | end 66 | 67 | it "can be set explicitly" do 68 | subject = Test::Foo.new(1, "qux") 69 | 70 | expect(subject.bar).to eq "qux" 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /spec/options_tolerance_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe "options tolerance" do 4 | before do 5 | class Test::Foo 6 | extend Dry::Initializer 7 | end 8 | end 9 | 10 | it "allows options before any definition" do 11 | expect { Test::Foo.new bar: :baz }.not_to raise_error 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/public_attributes_utility_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Dry::Initializer, ".dry_initializer.public_attributes" do 4 | subject { instance.class.dry_initializer.public_attributes(instance) } 5 | 6 | context "when class has params" do 7 | before do 8 | class Test::Foo 9 | extend Dry::Initializer 10 | param :foo, proc(&:to_s), desc: "a weird parameter" 11 | option :moo, optional: true 12 | option :bar, default: proc { 1 }, reader: false 13 | option :baz, optional: true, reader: :protected 14 | option :qux, proc(&:to_s), as: :quxx, reader: :private 15 | end 16 | end 17 | 18 | let(:instance) { Test::Foo.new(:FOO, bar: :BAR, baz: :BAZ, qux: :QUX) } 19 | 20 | it "collects public options only" do 21 | expect(subject).to eq({foo: "FOO", moo: nil}) 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/reader_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe "reader" do 4 | shared_examples "it has no public attr_reader" do 5 | it "does not define a public attr_reader" do 6 | expect(subject).not_to respond_to :foo 7 | expect(subject).not_to respond_to :bar 8 | end 9 | end 10 | 11 | context "with reader: :public or no reader: option" do 12 | subject do 13 | class Test::Foo 14 | extend Dry::Initializer 15 | 16 | param :foo 17 | param :foo2, reader: :public 18 | option :bar 19 | option :bar2, reader: :public 20 | end 21 | 22 | Test::Foo.new 1, 2, bar: 3, bar2: 4 23 | end 24 | 25 | it "defines a public attr_reader by default" do 26 | expect(subject).to respond_to(:foo, :foo2) 27 | expect(subject).to respond_to :bar 28 | expect(subject).to respond_to :bar2 29 | end 30 | end 31 | 32 | context "with reader: false" do 33 | before do 34 | class Test::Foo 35 | extend Dry::Initializer 36 | 37 | param :foo, reader: false 38 | option :bar, reader: false 39 | end 40 | end 41 | 42 | subject { Test::Foo.new 1, bar: 2 } 43 | 44 | it_behaves_like "it has no public attr_reader" 45 | 46 | it "keeps assigning variables" do 47 | expect(subject.instance_variable_get(:@foo)).to eql 1 48 | expect(subject.instance_variable_get(:@bar)).to eql 2 49 | end 50 | end 51 | 52 | context "with reader: :private" do 53 | before do 54 | class Test::Foo 55 | extend Dry::Initializer 56 | 57 | param :foo, reader: :private 58 | option :bar, reader: :private 59 | end 60 | end 61 | 62 | subject { Test::Foo.new 1, bar: 2 } 63 | 64 | it_behaves_like "it has no public attr_reader" 65 | 66 | it "adds a private attr_reader" do 67 | expect(subject.send(:foo)).to eql 1 68 | expect(subject.send(:bar)).to eql 2 69 | end 70 | end 71 | 72 | context "with reader: :protected" do 73 | subject do 74 | class Test::Foo 75 | extend Dry::Initializer 76 | 77 | param :foo, reader: :protected 78 | option :bar, reader: :protected 79 | end 80 | 81 | Test::Foo.new 1, bar: 2 82 | end 83 | 84 | it "adds a protected attr_reader" do 85 | protected_instance_methods = subject.class.protected_instance_methods 86 | expect(protected_instance_methods).to match_array(%i[foo bar]) 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /spec/repetitive_definitions_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe "repetitive definitions" do 4 | subject { Test::Foo.new } 5 | 6 | context "of params" do 7 | before do 8 | class Test::Foo 9 | extend Dry::Initializer 10 | 11 | param :foo, default: proc { 0 } 12 | param :bar, default: proc { 1 } 13 | param :foo, default: proc { 2 } 14 | end 15 | end 16 | 17 | it "reloads the attribute" do 18 | expect(subject.foo).to eq 2 19 | end 20 | end 21 | 22 | context "of options" do 23 | before do 24 | class Test::Foo 25 | extend Dry::Initializer 26 | 27 | option :foo, default: proc { 0 } 28 | option :bar, default: proc { 1 } 29 | option :foo, default: proc { 2 } 30 | end 31 | end 32 | 33 | it "reloads the attribute" do 34 | expect(subject.foo).to eq 2 35 | end 36 | end 37 | 38 | context "of param and option" do 39 | before do 40 | class Test::Foo 41 | extend Dry::Initializer 42 | 43 | param :foo, default: proc { 0 } 44 | option :bar, default: proc { 1 } 45 | option :foo, default: proc { 2 } 46 | end 47 | end 48 | 49 | it "reloads the attribute" do 50 | expect(subject.foo).to eq 2 51 | end 52 | end 53 | 54 | context "of optional param and option" do 55 | before do 56 | class Test::Foo 57 | extend Dry::Initializer 58 | 59 | param :baz, optional: true, as: :foo 60 | option :bar, optional: true 61 | option :foo, optional: true 62 | end 63 | end 64 | 65 | it "allows various assignments" do 66 | expect(Test::Foo.new(1).foo).to eq 1 67 | expect(Test::Foo.new(foo: 2).foo).to eq 2 68 | expect(Test::Foo.new(1, foo: 2).foo).to eq 2 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /spec/several_assignments_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe "attribute with several assignments" do 4 | before do 5 | class Test::Foo 6 | extend Dry::Initializer 7 | 8 | option :bar, proc(&:to_s), optional: true 9 | option :"some foo", as: :bar, optional: true 10 | end 11 | end 12 | 13 | context "when not defined" do 14 | subject { Test::Foo.new } 15 | 16 | it "is left undefined" do 17 | expect(subject.bar).to be_nil 18 | expect(subject.instance_variable_get(:@bar)) 19 | .to eq Dry::Initializer::UNDEFINED 20 | end 21 | end 22 | 23 | context "when set directly" do 24 | subject { Test::Foo.new bar: :BAZ } 25 | 26 | it "sets the attribute" do 27 | expect(subject.bar).to eq "BAZ" 28 | end 29 | end 30 | 31 | context "when renamed" do 32 | subject { Test::Foo.new "some foo": :BAZ } 33 | 34 | it "renames the attribute" do 35 | expect(subject.bar).to eq :BAZ 36 | expect(subject).not_to respond_to :foo 37 | end 38 | 39 | it "renames the variable" do 40 | expect(subject.instance_variable_get(:@bar)).to eq :BAZ 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "support/coverage" 4 | 5 | require "dry/initializer" 6 | 7 | begin 8 | require "pry" 9 | rescue LoadError 10 | nil 11 | end 12 | 13 | RSpec.configure do |config| 14 | config.disable_monkey_patching! 15 | config.order = :random 16 | config.filter_run focus: true 17 | config.run_all_when_everything_filtered = true 18 | 19 | # Prepare the Test namespace for constants defined in specs 20 | config.around(:each) do |example| 21 | Test = Class.new(Module) 22 | example.run 23 | Object.send :remove_const, :Test 24 | end 25 | 26 | config.warnings = true 27 | end 28 | -------------------------------------------------------------------------------- /spec/subclassing_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe "subclassing" do 4 | before do 5 | class Test::Foo 6 | extend Dry::Initializer[undefined: false] 7 | param :foo 8 | option :bar 9 | end 10 | 11 | class Test::Bar < Test::Foo 12 | param :baz 13 | option :qux 14 | end 15 | end 16 | 17 | let(:instance_of_superclass) do 18 | Test::Foo.new 1, bar: 3 19 | end 20 | 21 | let(:instance_of_subclass) do 22 | Test::Bar.new 1, 2, bar: 3, qux: 4 23 | end 24 | 25 | it "preserves null definition" do 26 | expect(Test::Foo.dry_initializer.null).to be_nil 27 | expect(Test::Bar.dry_initializer.null).to be_nil 28 | end 29 | 30 | it "preserves definitions made in the superclass" do 31 | expect(instance_of_subclass.foo).to eql 1 32 | expect(instance_of_subclass.baz).to eql 2 33 | expect(instance_of_subclass.bar).to eql 3 34 | expect(instance_of_subclass.qux).to eql 4 35 | end 36 | 37 | it "does not pollute superclass with definitions from subclass" do 38 | expect(instance_of_superclass).not_to respond_to :baz 39 | expect(instance_of_superclass).not_to respond_to :qux 40 | end 41 | 42 | it "calls .inherited hook added by other mixin" do 43 | called = false 44 | mixin = Module.new { define_method(:inherited) { |_| called = true } } 45 | 46 | base = Class.new { extend mixin; extend Dry::Initializer } 47 | Class.new(base) 48 | 49 | expect(called).to be true 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /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/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/warnings.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # this file is managed by dry-rb/devtools project 4 | 5 | require "warning" 6 | 7 | Warning.ignore(%r{rspec/core}) 8 | Warning.ignore(%r{rspec/mocks}) 9 | Warning.ignore(/codacy/) 10 | Warning[:experimental] = false if Warning.respond_to?(:[]) 11 | -------------------------------------------------------------------------------- /spec/type_argument_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry-types" 4 | 5 | RSpec.describe "type argument" do 6 | before do 7 | class Test::Foo 8 | extend Dry::Initializer 9 | param :foo, Dry::Types["strict.string"] 10 | option :bar, Dry::Types["strict.string"] 11 | end 12 | end 13 | 14 | context "in case of param mismatch" do 15 | subject { Test::Foo.new 1, bar: "2" } 16 | 17 | it "raises TypeError" do 18 | expect { subject }.to raise_error Dry::Types::ConstraintError, /1/ 19 | end 20 | end 21 | 22 | context "in case of option mismatch" do 23 | subject { Test::Foo.new "1", bar: 2 } 24 | 25 | it "raises TypeError" do 26 | expect { subject }.to raise_error Dry::Types::ConstraintError, /2/ 27 | end 28 | end 29 | 30 | context "in case of match" do 31 | subject { Test::Foo.new "1", bar: "2" } 32 | 33 | it "completes the initialization" do 34 | expect { subject }.not_to raise_error 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/type_constraint_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry-types" 4 | 5 | RSpec.describe "type constraint" do 6 | context "by a proc with 1 argument" do 7 | before do 8 | class Test::Foo 9 | extend Dry::Initializer 10 | param :__foo__, proc(&:to_s), optional: true 11 | end 12 | end 13 | 14 | subject { Test::Foo.new :foo } 15 | 16 | it "coerces a value" do 17 | expect(subject.__foo__).to eq "foo" 18 | end 19 | end 20 | 21 | context "by a proc with 2 arguments" do 22 | before do 23 | class Test::Foo 24 | extend Dry::Initializer 25 | param :foo, proc { |val, obj| "#{obj.hash}:#{val}" }, optional: true 26 | end 27 | end 28 | 29 | subject { Test::Foo.new :foo } 30 | 31 | it "coerces a value with self as a second argument" do 32 | expect(subject.foo).to eq "#{subject.hash}:foo" 33 | end 34 | end 35 | 36 | context "by dry-type" do 37 | before do 38 | constraint = self.constraint 39 | 40 | Test::Foo = Class.new do 41 | extend Dry::Initializer 42 | param :foo, constraint, optional: true 43 | end 44 | end 45 | 46 | context "with a strict string" do 47 | let(:constraint) { Dry::Types["strict.string"] } 48 | 49 | context "in case of mismatch" do 50 | subject { Test::Foo.new 1 } 51 | 52 | it "raises ArgumentError" do 53 | expect { subject }.to raise_error Dry::Types::ConstraintError, /1/ 54 | end 55 | end 56 | 57 | context "in case of match" do 58 | subject { Test::Foo.new "foo" } 59 | 60 | it "completes the initialization" do 61 | expect { subject }.not_to raise_error 62 | end 63 | end 64 | 65 | context "if optional value not set" do 66 | subject { Test::Foo.new } 67 | 68 | it "not applicable to Dry::Initializer::UNDEFINED" do 69 | expect(subject.instance_variable_get(:@foo)) 70 | .to eq Dry::Initializer::UNDEFINED 71 | end 72 | end 73 | end 74 | 75 | context "with a integer member array" do 76 | let(:constraint) { Dry::Types["array"].of(Dry::Types["coercible.integer"]) } 77 | 78 | context "with arity other than 1" do 79 | subject { Test::Foo.new ["1"] } 80 | 81 | it "completes the initialization" do 82 | expect(subject.foo).to eql([1]) 83 | end 84 | end 85 | 86 | context "when value is not valid" do 87 | subject { Test::Foo.new "foo" } 88 | 89 | it "raises constraint error" do 90 | expect { subject }.to raise_error(Dry::Types::ConstraintError, /foo/) 91 | end 92 | end 93 | 94 | context "when member value is not valid" do 95 | subject { Test::Foo.new ["foo"] } 96 | 97 | it "raises constraint error" do 98 | expect { subject }.to raise_error(Dry::Types::CoercionError, /foo/) 99 | end 100 | end 101 | end 102 | end 103 | 104 | context "by invalid constraint" do 105 | it "raises ArgumentError" do 106 | expect do 107 | class Test::Foo 108 | extend Dry::Initializer 109 | param :foo, type: String 110 | end 111 | end.to raise_error(ArgumentError) 112 | end 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /spec/value_coercion_via_dry_types_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry-types" 4 | 5 | RSpec.describe "value coercion via dry-types" do 6 | before do 7 | module Test::Types 8 | include Dry.Types 9 | end 10 | 11 | class Test::Foo 12 | extend Dry::Initializer 13 | 14 | param :foo, type: Test::Types::Coercible::String 15 | option :bar, proc(&:to_i), default: proc { "16" } 16 | end 17 | end 18 | 19 | it "coerces assigned values" do 20 | subject = Test::Foo.new :foo, bar: "13" 21 | 22 | expect(subject.foo).to eql "foo" 23 | expect(subject.bar).to eql 13 24 | end 25 | 26 | it "coerces defaults as well" do 27 | subject = Test::Foo.new :foo 28 | 29 | expect(subject.bar).to eql 16 30 | end 31 | end 32 | --------------------------------------------------------------------------------