├── .devtools └── templates │ ├── changelog.erb │ └── release.erb ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug-report.md │ └── config.yml ├── SUPPORT.md └── workflows │ ├── ci.yml │ ├── docsite.yml │ ├── rubocop.yml │ └── sync_configs.yml ├── .gitignore ├── .postgres.env ├── .repobot.yml ├── .rspec ├── .rubocop.yml ├── .yardopts ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Gemfile ├── Gemfile.devtools ├── LICENSE ├── README.md ├── Rakefile ├── benchmarks └── basic.rb ├── changelog.yml ├── compose.yml ├── docsite └── source │ └── index.html.md ├── lib ├── rom-factory.rb └── rom │ ├── factory.rb │ └── factory │ ├── attribute_registry.rb │ ├── attributes.rb │ ├── attributes │ ├── association.rb │ ├── callable.rb │ ├── sequence.rb │ └── value.rb │ ├── builder.rb │ ├── builder │ └── persistable.rb │ ├── constants.rb │ ├── dsl.rb │ ├── factories.rb │ ├── registry.rb │ ├── sequences.rb │ ├── tuple_evaluator.rb │ └── version.rb ├── project.yml ├── rom-factory.gemspec └── spec ├── integration └── rom │ └── factory_spec.rb ├── shared ├── database.rb └── relations.rb ├── spec_helper.rb ├── support ├── coverage.rb ├── rspec_options.rb └── warnings.rb └── unit └── rom └── factory ├── attribute_registry_spec.rb └── builder_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/rom-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/rom-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.rom-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 rom-rb libraries, feel free to ask questions on our [discussion forum](https://discourse.rom-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 | --- 2 | name: ci 3 | on: 4 | push: 5 | pull_request: 6 | create: 7 | schedule: 8 | - cron: "30 4 * * *" 9 | jobs: 10 | tests: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | ruby: 16 | - '3.4' 17 | - '3.3' 18 | - '3.2' 19 | - '3.1' 20 | env: 21 | COVERAGE: "${{matrix.coverage}}" 22 | COVERAGE_TOKEN: "${{secrets.CODACY_PROJECT_TOKEN}}" 23 | APT_DEPS: libpq-dev libmysqlclient-dev libsqlite3-dev 24 | DATABASE_URL: "postgres://rom-factory:rom-factory@localhost:5432/rom_factory" 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v1 28 | - name: Install package dependencies 29 | run: "[ -e $APT_DEPS ] || sudo apt-get install -y --no-install-recommends $APT_DEPS" 30 | - name: Set up Ruby 31 | uses: ruby/setup-ruby@v1 32 | with: 33 | ruby-version: "${{matrix.ruby}}" 34 | - name: Install latest bundler 35 | run: | 36 | gem install bundler --no-document 37 | bundle config set without 'tools benchmarks docs' 38 | - name: Bundle install 39 | run: bundle install --jobs 4 --retry 3 40 | - name: Run all tests 41 | run: bundle exec rake 42 | - name: Run codacy-coverage-reporter 43 | uses: codacy/codacy-coverage-reporter-action@master 44 | if: env.COVERAGE == 'true' && env.COVERAGE_TOKEN != '' 45 | with: 46 | project-token: "${{secrets.CODACY_PROJECT_TOKEN}}" 47 | coverage-reports: coverage/coverage.xml 48 | services: 49 | db: 50 | image: postgres:16.1 51 | env: 52 | POSTGRES_USER: rom-factory 53 | POSTGRES_PASSWORD: rom-factory 54 | POSTGRES_DB: rom_factory 55 | ports: 56 | - 5432:5432 57 | options: "--health-cmd pg_isready --health-interval 10s --health-timeout 5s 58 | --health-retries 5" 59 | release: 60 | runs-on: ubuntu-latest 61 | if: contains(github.ref, 'tags') && github.event_name == 'create' 62 | needs: tests 63 | env: 64 | GITHUB_LOGIN: rom-bot 65 | GITHUB_TOKEN: "${{secrets.GH_PAT}}" 66 | steps: 67 | - uses: actions/checkout@v1 68 | - name: Install package dependencies 69 | run: "[ -e $APT_DEPS ] || sudo apt-get install -y --no-install-recommends $APT_DEPS" 70 | - name: Set up Ruby 71 | uses: ruby/setup-ruby@v1 72 | with: 73 | ruby-version: "3.1" 74 | - name: Install dependencies 75 | run: gem install ossy --no-document 76 | - name: Trigger release workflow 77 | run: | 78 | tag=$(echo $GITHUB_REF | cut -d / -f 3) 79 | ossy gh w rom-rb/devtools release --payload "{\"tag\":\"$tag\",\"sha\":\"${{github.sha}}\",\"tag_creator\":\"$GITHUB_ACTOR\",\"repo\":\"$GITHUB_REPOSITORY\",\"repo_name\":\"${{github.event.repository.name}}\"}" 80 | 81 | workflow-keepalive: 82 | if: github.event_name == 'schedule' 83 | runs-on: ubuntu-latest 84 | permissions: 85 | actions: write 86 | steps: 87 | - uses: liskin/gh-workflow-keepalive@v1 88 | -------------------------------------------------------------------------------- /.github/workflows/docsite.yml: -------------------------------------------------------------------------------- 1 | # This file is synced from rom-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@v2 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.1" 28 | - name: Set up git user 29 | run: | 30 | git config --local user.email "rom-bot@rom-rb.org" 31 | git config --local user.name "rom-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://rom-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 rom-rb.org deploy 59 | env: 60 | GITHUB_LOGIN: rom-bot 61 | GITHUB_TOKEN: ${{secrets.GH_PAT}} 62 | run: ossy github workflow rom-rb/rom-rb.org ci 63 | -------------------------------------------------------------------------------- /.github/workflows/rubocop.yml: -------------------------------------------------------------------------------- 1 | name: "RuboCop" 2 | on: 3 | push: 4 | branches: 5 | - "main" 6 | - "master" 7 | paths: 8 | - "**/*.rb" 9 | - "**/*.rake" 10 | - "Rakefile" 11 | - "Gemfile*" 12 | - ".rubocop.yml" 13 | pull_request: 14 | branches: 15 | - "main" 16 | - "master" 17 | types: 18 | - "opened" 19 | - "synchronize" 20 | workflow_dispatch: 21 | jobs: 22 | run: 23 | runs-on: ubuntu-latest 24 | name: ${{ matrix.type }} 25 | strategy: 26 | fail-fast: false 27 | matrix: 28 | type: ["Style", "Layout", "Naming", "Lint", "Metrics", "Security"] 29 | steps: 30 | - name: Clone 31 | uses: actions/checkout@v2 32 | - name: Get git diff 33 | id: get_diff 34 | uses: technote-space/get-diff-action@v4 35 | with: 36 | PATTERNS: | 37 | **/*.rb 38 | **/*.rake 39 | Gemfile 40 | Rakefile 41 | - name: Check ${{ matrix.type }} 42 | uses: action-hero/actions/rubocop@main 43 | if: ${{ env.GIT_DIFF != '' }} 44 | with: 45 | diff: ${{ env.GIT_DIFF }} 46 | type: ${{ matrix.type }} 47 | -------------------------------------------------------------------------------- /.github/workflows/sync_configs.yml: -------------------------------------------------------------------------------- 1 | # This file is synced from rom-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: rom-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: rom-rb/devtools 25 | path: tmp/devtools 26 | - name: Setup git user 27 | run: | 28 | git config --local user.email "rom-bot@rom-rb.org" 29 | git config --local user.name "rom-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://rom-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 | .vscode 11 | -------------------------------------------------------------------------------- /.postgres.env: -------------------------------------------------------------------------------- 1 | POSTGRES_USER="rom" 2 | POSTGRES_PASSWORD="password" 3 | POSTGRES_DATABASE="rom_factory" 4 | POSTGRES_HOST_AUTH_METHOD="trust" 5 | -------------------------------------------------------------------------------- /.repobot.yml: -------------------------------------------------------------------------------- 1 | ########################################################### 2 | # DO NOT EDIT THIS FILE 3 | # 4 | # This is a config synced from rom-rb/template-gem repo 5 | ########################################################### 6 | 7 | sources: 8 | - repo: rom-rb/template-gem 9 | sync: 10 | - ".repobot.yml.erb" 11 | - ".devtools/templates/*.sync:${{dir}}/${{name}}" 12 | - ".github/**/*.*" 13 | - ".rspec" 14 | - ".rubocop.yml" 15 | - "spec/support/*" 16 | - "CODE_OF_CONDUCT.md" 17 | - "CONTRIBUTING.md" 18 | - "CODEOWNERS" 19 | - "LICENSE.erb" 20 | - "README.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 rom-rb/template-gem repo 2 | AllCops: 3 | TargetRubyVersion: 3.1 4 | NewCops: enable 5 | SuggestExtensions: false 6 | Exclude: 7 | - spec/support/coverage.rb 8 | - spec/support/warnings.rb 9 | - spec/support/rspec_options.rb 10 | - Gemfile.devtools 11 | - "*.gemspec" 12 | 13 | Layout/ArgumentAlignment: 14 | Enabled: false 15 | EnforcedStyle: with_fixed_indentation 16 | 17 | Layout/SpaceAroundMethodCallOperator: 18 | Enabled: false 19 | 20 | Layout/SpaceInLambdaLiteral: 21 | Enabled: false 22 | 23 | Layout/MultilineMethodCallIndentation: 24 | Enabled: true 25 | EnforcedStyle: indented 26 | 27 | Layout/FirstArrayElementIndentation: 28 | EnforcedStyle: consistent 29 | 30 | Layout/SpaceInsideHashLiteralBraces: 31 | Enabled: true 32 | EnforcedStyle: no_space 33 | EnforcedStyleForEmptyBraces: no_space 34 | 35 | Layout/LineLength: 36 | Max: 120 37 | Exclude: 38 | - "spec/**/*_spec.rb" 39 | 40 | Lint/AmbiguousBlockAssociation: 41 | Enabled: true 42 | # because 'expect { foo }.to change { bar }' is fine 43 | Exclude: 44 | - "spec/**/*.rb" 45 | 46 | Lint/BooleanSymbol: 47 | Enabled: false 48 | 49 | Lint/MissingSuper: 50 | Enabled: false 51 | 52 | Lint/ConstantDefinitionInBlock: 53 | Enabled: false 54 | 55 | Lint/EmptyBlock: 56 | Exclude: 57 | - "spec/**/*.rb" 58 | 59 | Lint/EmptyClass: 60 | Exclude: 61 | - "spec/**/*.rb" 62 | 63 | Lint/RaiseException: 64 | Enabled: false 65 | 66 | Lint/StructNewOverride: 67 | Enabled: false 68 | 69 | Lint/SuppressedException: 70 | Exclude: 71 | - "spec/**/*.rb" 72 | 73 | Lint/UselessMethodDefinition: 74 | Exclude: 75 | - "lib/rom/struct.rb" 76 | 77 | Lint/UnderscorePrefixedVariableName: 78 | Enabled: false 79 | 80 | Lint/ToEnumArguments: 81 | Exclude: 82 | - "lib/rom/command.rb" 83 | 84 | Naming/MethodParameterName: 85 | Enabled: false 86 | 87 | Naming/AccessorMethodName: 88 | Enabled: false 89 | 90 | Naming/VariableNumber: 91 | Enabled: false 92 | 93 | Naming/PredicateName: 94 | Enabled: false 95 | 96 | Naming/FileName: 97 | Exclude: 98 | - "lib/*-*.rb" 99 | 100 | Naming/MethodName: 101 | Enabled: false 102 | 103 | Naming/MemoizedInstanceVariableName: 104 | Enabled: false 105 | 106 | Metrics/MethodLength: 107 | Enabled: false 108 | 109 | Metrics/ModuleLength: 110 | Enabled: false 111 | 112 | Metrics/ClassLength: 113 | Enabled: false 114 | 115 | Metrics/BlockLength: 116 | Enabled: false 117 | 118 | Metrics/AbcSize: 119 | Max: 36 120 | 121 | Metrics/CyclomaticComplexity: 122 | Enabled: true 123 | Max: 12 124 | 125 | Metrics/PerceivedComplexity: 126 | Max: 14 127 | 128 | Metrics/ParameterLists: 129 | Enabled: false 130 | 131 | Style/AccessorGrouping: 132 | Enabled: false 133 | 134 | Style/ExponentialNotation: 135 | Enabled: false 136 | 137 | Style/HashEachMethods: 138 | Enabled: false 139 | 140 | Style/HashTransformKeys: 141 | Enabled: false 142 | 143 | Style/HashTransformValues: 144 | Enabled: false 145 | 146 | Style/AccessModifierDeclarations: 147 | Enabled: false 148 | 149 | Style/Alias: 150 | Enabled: true 151 | EnforcedStyle: prefer_alias_method 152 | 153 | Style/AsciiComments: 154 | Enabled: false 155 | 156 | Style/BlockDelimiters: 157 | Enabled: false 158 | 159 | Style/ClassAndModuleChildren: 160 | Enabled: false 161 | 162 | Style/ConditionalAssignment: 163 | Enabled: false 164 | 165 | Style/DateTime: 166 | Enabled: false 167 | 168 | Style/Documentation: 169 | Enabled: false 170 | 171 | Style/EachWithObject: 172 | Enabled: false 173 | 174 | Style/FormatString: 175 | Enabled: false 176 | 177 | Style/GuardClause: 178 | Enabled: false 179 | 180 | Style/IfUnlessModifier: 181 | Enabled: false 182 | 183 | Style/Lambda: 184 | Enabled: false 185 | 186 | Style/LambdaCall: 187 | Enabled: false 188 | 189 | Style/ParallelAssignment: 190 | Enabled: false 191 | 192 | Style/StabbyLambdaParentheses: 193 | Enabled: false 194 | 195 | Style/StringLiterals: 196 | Enabled: true 197 | EnforcedStyle: double_quotes 198 | ConsistentQuotesInMultiline: false 199 | 200 | Style/StringLiteralsInInterpolation: 201 | Enabled: true 202 | EnforcedStyle: double_quotes 203 | 204 | Style/SymbolArray: 205 | Enabled: false 206 | 207 | Style/OptionalBooleanParameter: 208 | Enabled: false 209 | 210 | Style/MultilineBlockChain: 211 | Enabled: false 212 | 213 | Style/DocumentDynamicEvalDefinition: 214 | Enabled: false 215 | 216 | Style/TrailingUnderscoreVariable: 217 | Enabled: false 218 | 219 | Style/MultipleComparison: 220 | Enabled: false 221 | 222 | Style/StringConcatenation: 223 | Enabled: false 224 | 225 | Style/OpenStructUse: 226 | Enabled: false 227 | 228 | Style/MapToHash: 229 | Enabled: false 230 | 231 | Style/FormatStringToken: 232 | Enabled: false 233 | 234 | Style/StructInheritance: 235 | Enabled: false 236 | 237 | Style/PreferredHashMethods: 238 | Enabled: false 239 | 240 | Style/DoubleNegation: 241 | Enabled: false 242 | 243 | Style/MissingRespondToMissing: 244 | Enabled: false 245 | 246 | Style/CombinableLoops: 247 | Enabled: false 248 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --plugin junk 2 | --query '@api.text != "private"' 3 | --embed-mixins 4 | -r README.md 5 | --markup-provider=redcarpet 6 | --markup=markdown 7 | --files CHANGELOG.md 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## 0.13.0 2025-01-21 4 | 5 | 6 | ### Added 7 | 8 | - Support for unique options in `fake` DSL (via #94) (@sean-dickinson) 9 | 10 | ```ruby 11 | Factory.define(:user) do |f| 12 | f.name { fake(:name, unique: true) } 13 | end 14 | ``` 15 | 16 | Be advised there's `Faker::UniqueGenerator.clear` to clear the cache of unique values. 17 | 18 | - Support for setting traits with a keyword argument for associations (via #84) (@parndt) 19 | 20 | ```ruby 21 | Factory.define :category do |f| 22 | f.association :image, traits: [:fancy] 23 | end 24 | ``` 25 | 26 | - Aliases for factory methods (via #92) (@cflipse) 27 | 28 | `Factory.create` is aliased to `Factory.[]` 29 | `Factory.build` is aliased to `Factory.structs[]` 30 | 31 | 32 | ### Changed 33 | 34 | - Minimum Ruby version is now 3.1 (@flash-gordon) 35 | 36 | [Compare v0.12.0...v0.13.0](https://github.com/rom-rb/rom-factory/compare/v0.12.0...v0.13.0) 37 | 38 | ## 0.12.0 2024-01-19 39 | 40 | 41 | ### Added 42 | 43 | - Support for many-to-many and one-to-one-through associations (via #86) (@solnic) 44 | - Support for UUID as PKs in associations (via #87) (@solnic) 45 | 46 | ### Fixed 47 | 48 | - Relations without PKs should work too (via #87) (@solnic) 49 | - Relations with PK values generated on the Ruby side should work in SQlite too (via #87) (@solnic) 50 | 51 | 52 | [Compare v0.11.0...v0.12.0](https://github.com/rom-rb/rom-factory/compare/v0.11.0...v0.12.0) 53 | 54 | ## 0.11.0 2022-11-11 55 | 56 | 57 | ### Added 58 | 59 | - Support for one-to-one associations (@ianks) 60 | - [internal] cache for Faker constants (@flash-gordon) 61 | 62 | ### Fixed 63 | 64 | - Support for plural Faker generators (@wuarmin) 65 | 66 | ### Changed 67 | 68 | - [BREAKING] attributes are always passed as keywords (@alassek) 69 | This may affect your code in places where attributes are passed as hashes. 70 | Places like 71 | 72 | ```ruby 73 | user_attributes = { name: 'Jane' } 74 | Factory[:user, user_attributes] 75 | 76 | ``` 77 | 78 | must be updated to 79 | 80 | ```ruby 81 | user_attributes = { name: 'Jane' } 82 | Factory[:user, **user_attributes] 83 | ``` 84 | 85 | - Upgraded to the latest versions of dry-rb dependencies, compatible with rom 5.3 (@flash-gordon) 86 | - Support for Faker 1.x was dropped (@alassek) 87 | 88 | [Compare v0.10.2...v0.11.0](https://github.com/rom-rb/rom-factory/compare/v0.10.2...v0.11.0) 89 | 90 | ## 0.10.2 2020-04-05 91 | 92 | 93 | ### Fixed 94 | 95 | - Fix more keyword warnings (@flash-gordon) 96 | 97 | 98 | [Compare v0.10.1...v0.10.2](https://github.com/rom-rb/rom-factory/compare/v0.10.1...v0.10.2) 99 | 100 | ## 0.10.1 2019-12-28 101 | 102 | 103 | ### Added 104 | 105 | - Support for faker 2 (@ianks) 106 | 107 | ### Fixed 108 | 109 | - Keyword warnings reported by Ruby 2.7 (@flash-gordon) 110 | 111 | 112 | [Compare v0.10.0...v0.10.1](https://github.com/rom-rb/rom-factory/compare/v0.10.0...v0.10.1) 113 | 114 | ## 0.10.0 2019-12-11 115 | 116 | 117 | ### Added 118 | 119 | - `struct_namespace` option is supported by factory builders (@graceful-potato) 120 | 121 | ``` ruby 122 | factories.define(:user, struct_namespace: MyApp::Entities) do |f| 123 | # ... 124 | end 125 | ``` 126 | 127 | ### Fixed 128 | 129 | - Support building structs when child assoc does not define parent (@psparrow) 130 | - Fixed `TupleEvaluator#struct_attrs` for non-standard output schema (@AMHOL) 131 | 132 | 133 | [Compare v0.9.1...v0.10.0](https://github.com/rom-rb/rom-factory/compare/v0.9.1...v0.10.0) 134 | 135 | ## 0.9.1 2019-10-23 136 | 137 | 138 | ### Fixed 139 | 140 | - Attributes of a struct are no longer accidentally passed to their associations (@psparrow) 141 | 142 | 143 | [Compare v0.9.0...v0.9.1](https://github.com/rom-rb/rom-factory/compare/v0.9.0...v0.9.1) 144 | 145 | ## 0.9.0 2019-08-12 146 | 147 | 148 | ### Added 149 | 150 | - When attributes hash includes unknown attributes, a `ROM::Factory::UnknownAttributeError` will be raised (@rawburt) 151 | 152 | 153 | [Compare v0.8.0...v0.9.0](https://github.com/rom-rb/rom-factory/compare/v0.8.0...v0.9.0) 154 | 155 | ## 0.8.0 2019-04-24 156 | 157 | 158 | ### Fixed 159 | 160 | - Loaded association structs are no longer rejected by output schemas (issue #34) (flash-gordon + solnic) 161 | 162 | 163 | [Compare v0.7.0...v0.8.0](https://github.com/rom-rb/rom-factory/compare/v0.7.0...v0.8.0) 164 | 165 | ## 0.7.0 2018-11-17 166 | 167 | 168 | ### Added 169 | 170 | - Support for traits (v-kolesnikov) 171 | - Support building structs with associations (@ianks) 172 | 173 | ### Fixed 174 | 175 | - Overwritten attributes with dependencies (JanaVPetrova) 176 | 177 | 178 | [Compare v0.6.0...v0.7.0](https://github.com/rom-rb/rom-factory/compare/v0.6.0...v0.7.0) 179 | 180 | ## 0.6.0 2018-01-31 181 | 182 | 183 | ### Added 184 | 185 | - Support for factories with custom struct namespaces (solnic) 186 | 187 | ### Fixed 188 | 189 | - Using dependent attributes with sequences works correctly, ie `f.sequence(:login) { |i, name| "name-#{i}"}` (solnic) 190 | 191 | ### Changed 192 | 193 | - Accessing a factory which is not defined will result in `FactoryNotDefinedError` exception (GustavoCaso + solnic) 194 | 195 | [Compare v0.5.0...v0.6.0](https://github.com/rom-rb/rom-factory/compare/v0.5.0...v0.6.0) 196 | 197 | ## 0.5.0 2017-10-24 198 | 199 | 200 | ### Added 201 | 202 | - Updated to rom 4.0 (solnic) 203 | - Support for `has_many` and `has_one` associations (solnic) 204 | - Support for attributes depending on values from other attributes (solnic) 205 | - Support for `rand` inside the generator block (flash-gordon) 206 | 207 | ### Changed 208 | 209 | - Depends on `rom-core` now (solnic) 210 | 211 | [Compare v0.4.0...v0.5.0](https://github.com/rom-rb/rom-factory/compare/v0.4.0...v0.5.0) 212 | 213 | ## 0.4.0 2017-03-03 214 | 215 | improves internals. 216 | 217 | ### Added 218 | 219 | - Support for defining multiple factories via `MyFactory = ROM::Factory.configure { |c| ... }` (solnic) 220 | - Support for builder inheritence via `define(admin: :user) { |f| ... }` (solnic) 221 | - Support for generating in-memory structs via `MyFactory.structs[:user]` that are not persisted (solnic) 222 | - Support for `belongs_to` associations via `f.association(:user)` (solnic) 223 | - New DSL for defining builders `MyFactory.define(:user) { |f| ... }` which infers default relation name (solnic) 224 | - New factory method `MyFactory#[]` ie `MyFactory[:user, name: "Jane"]` (solnic) 225 | - New `fake` helper which uses faker gem under the hood ie `f.email { fake(:internet, :email) }` (solnic) 226 | 227 | ### Changed 228 | 229 | - `Rom::Factory::Config.configure` was replaced with `ROM::Factory.configure` (solnic) 230 | - Global factory config and builders are gone (solnic) 231 | - Structs are now based on dry-struct (solnic) 232 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 4 | 5 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, age, or religion. 6 | 7 | Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. 8 | 9 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. 10 | 11 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 12 | 13 | This Code of Conduct is adapted from the [Contributor Covenant](http:contributor-covenant.org), version 1.4.0, available at [https://www.contributor-covenant.org/version/1/4/code-of-conduct](https://www.contributor-covenant.org/version/1/4/code-of-conduct) 14 | -------------------------------------------------------------------------------- /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.rom-rb.org](https://discourse.rom-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.rom-rb.org](https://discourse.rom-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.rom-rb.org](https://discourse.rom-rb.org). 30 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gemspec 6 | 7 | eval_gemfile "Gemfile.devtools" 8 | 9 | gem "faker", "~> 3.0" 10 | 11 | gem "rspec", "~> 3.0" 12 | 13 | gem "dotenv" 14 | 15 | git "https://github.com/rom-rb/rom.git", branch: "release-5.4" do 16 | gem "rom" 17 | gem "rom-changeset" 18 | gem "rom-core" 19 | gem "rom-repository" 20 | end 21 | 22 | group :test do 23 | if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.4") 24 | gem "debug" 25 | else 26 | gem "pry" 27 | gem "pry-byebug", "~> 3.8", platforms: :ruby 28 | end 29 | gem "rom-sql", github: "rom-rb/rom-sql", branch: "release-3.7" 30 | 31 | gem "jdbc-postgres", platforms: :jruby 32 | gem "pg", "~> 1.5", platforms: :ruby 33 | end 34 | 35 | group :tools do 36 | if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.4") 37 | gem "byebug", platform: :mri 38 | else 39 | gem "pry-byebug", "~> 3.8", platforms: :ruby 40 | end 41 | gem "redcarpet" # for yard 42 | end 43 | 44 | group :benchmarks do 45 | gem "activerecord" 46 | gem "benchmark-ips" 47 | gem "fabrication" 48 | gem "factory_bot" 49 | end 50 | -------------------------------------------------------------------------------- /Gemfile.devtools: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # this file is managed by rom-rb/devtools project 4 | 5 | gem "rake", ">= 12.3.3" 6 | 7 | git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } 8 | 9 | group :test do 10 | gem "simplecov", require: false, platforms: :ruby 11 | gem "simplecov-cobertura", require: false, platforms: :ruby 12 | gem "rexml", require: false 13 | 14 | gem "warning" if RUBY_VERSION >= "2.4.0" 15 | end 16 | 17 | group :tools do 18 | # this is the same version that we use on codacy 19 | gem "rubocop", "1.69.2" 20 | end 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2021 rom-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 | [gem]: https://rubygems.org/gems/rom-factory 2 | [actions]: https://github.com/rom-rb/rom-factory/actions 3 | [codacy]: https://www.codacy.com/gh/rom-rb/rom-factory 4 | [chat]: https://rom-rb.zulipchat.com 5 | [inchpages]: http://inch-ci.org/github/rom-rb/rom-factory 6 | 7 | # rom-factory [![Join the chat at https://rom-rb.zulipchat.com](https://img.shields.io/badge/rom--rb-join%20chat-%23346b7a.svg)][chat] 8 | 9 | [![Gem Version](https://badge.fury.io/rb/rom-factory.svg)][gem] 10 | [![CI Status](https://github.com/rom-rb/rom-factory/workflows/ci/badge.svg)][actions] 11 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/5fd26fae687549218458879b1a607e18)][codacy] 12 | [![Codacy Badge](https://api.codacy.com/project/badge/Coverage/5fd26fae687549218458879b1a607e18)][codacy] 13 | [![Inline docs](http://inch-ci.org/github/rom-rb/rom-factory.svg?branch=master)][inchpages] 14 | 15 | ## Links 16 | 17 | * [User documentation](https://rom-rb.org/learn/factory) 18 | * [API documentation](https://rubydoc.info/gems/rom-factory) 19 | 20 | ## Supported Ruby versions 21 | 22 | This library officially supports the following Ruby versions: 23 | 24 | * MRI >= `3.1` 25 | * jruby >= `9.4` (not tested in CI) 26 | 27 | ## License 28 | 29 | See `LICENSE` file. 30 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | require "rspec/core/rake_task" 5 | 6 | RSpec::Core::RakeTask.new(:spec) 7 | 8 | task default: :spec 9 | -------------------------------------------------------------------------------- /benchmarks/basic.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rom-factory" 4 | require "rom-core" 5 | require "active_record" 6 | require "factory_bot" 7 | require "fabrication" 8 | require "benchmark/ips" 9 | 10 | DATABASE_URL = "postgres://localhost/rom_factory_bench" 11 | 12 | rom = ROM.container(:sql, DATABASE_URL) do |conf| 13 | conf.default.connection.create_table?(:users) do 14 | primary_key :id 15 | column :last_name, String, null: false 16 | column :first_name, String, null: false 17 | column :admin, TrueClass 18 | end 19 | 20 | conf.relation(:users) do 21 | schema(infer: true) 22 | end 23 | end 24 | 25 | factory = ROM::Factory.configure { |c| c.rom = rom } 26 | 27 | factory.define(:user) do |f| 28 | f.first_name { "John" } 29 | f.last_name { "Doe" } 30 | f.admin { false } 31 | end 32 | 33 | class User < ActiveRecord::Base 34 | end 35 | 36 | ActiveRecord::Base.establish_connection(DATABASE_URL) 37 | 38 | FactoryBot.define do 39 | factory(:user) do 40 | first_name { "John" } 41 | last_name { "Doe" } 42 | admin { false } 43 | end 44 | end 45 | 46 | Fabricator(:user) do 47 | first_name { "John" } 48 | last_name { "Doe" } 49 | admin { false } 50 | end 51 | 52 | Benchmark.ips do |x| 53 | x.report("rom-factory persisted struct") do 54 | factory[:user] 55 | end 56 | 57 | x.report("factory_bot") do 58 | FactoryBot.create(:user) 59 | end 60 | 61 | x.report("fabrication") do 62 | Fabricate(:user) 63 | end 64 | 65 | x.compare! 66 | end 67 | -------------------------------------------------------------------------------- /changelog.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - version: 0.13.0 3 | date: 2025-01-21 4 | added: 5 | - | 6 | Support for unique options in `fake` DSL (via #94) (@sean-dickinson) 7 | 8 | ```ruby 9 | Factory.define(:user) do |f| 10 | f.name { fake(:name, unique: true) } 11 | end 12 | ``` 13 | 14 | Be advised there's `Faker::UniqueGenerator.clear` to clear the cache of unique values. 15 | - | 16 | Support for setting traits with a keyword argument for associations (via #84) (@parndt) 17 | 18 | ```ruby 19 | Factory.define :category do |f| 20 | f.association :image, traits: [:fancy] 21 | end 22 | ``` 23 | - | 24 | Aliases for factory methods (via #92) (@cflipse) 25 | 26 | `Factory.create` is aliased to `Factory.[]` 27 | `Factory.build` is aliased to `Factory.structs[]` 28 | 29 | changed: 30 | - Minimum Ruby version is now 3.1 (@flash-gordon) 31 | - version: 0.12.0 32 | date: 2024-01-19 33 | added: 34 | - "Support for many-to-many and one-to-one-through associations (via #86) (@solnic)" 35 | - "Support for UUID as PKs in associations (via #87) (@solnic)" 36 | fixed: 37 | - "Relations without PKs should work too (via #87) (@solnic)" 38 | - "Relations with PK values generated on the Ruby side should work in SQlite too (via #87) (@solnic)" 39 | - version: 0.11.0 40 | date: 2022-11-11 41 | added: 42 | - "Support for one-to-one associations (@ianks)" 43 | - "[internal] cache for Faker constants (@flash-gordon)" 44 | changed: 45 | - | 46 | [BREAKING] attributes are always passed as keywords (@alassek) 47 | This may affect your code in places where attributes are passed as hashes. 48 | Places like 49 | 50 | ```ruby 51 | user_attributes = { name: 'Jane' } 52 | Factory[:user, user_attributes] 53 | 54 | ``` 55 | 56 | must be updated to 57 | 58 | ```ruby 59 | user_attributes = { name: 'Jane' } 60 | Factory[:user, **user_attributes] 61 | ``` 62 | - "Upgraded to the latest versions of dry-rb dependencies, compatible with rom 5.3 (@flash-gordon)" 63 | - Support for Faker 1.x was dropped (@alassek) 64 | fixed: 65 | - Support for plural Faker generators (@wuarmin) 66 | - version: 0.10.2 67 | date: "2020-04-05" 68 | fixed: 69 | - Fix more keyword warnings (@flash-gordon) 70 | - version: 0.10.1 71 | date: '2019-12-28' 72 | added: 73 | - 'Support for faker 2 (@ianks)' 74 | fixed: 75 | - Keyword warnings reported by Ruby 2.7 (@flash-gordon) 76 | - version: 0.10.0 77 | date: '2019-12-11' 78 | added: 79 | - |- 80 | `struct_namespace` option is supported by factory builders (@graceful-potato) 81 | 82 | ``` ruby 83 | factories.define(:user, struct_namespace: MyApp::Entities) do |f| 84 | # ... 85 | end 86 | ``` 87 | fixed: 88 | - Support building structs when child assoc does not define parent (@psparrow) 89 | - 'Fixed `TupleEvaluator#struct_attrs` for non-standard output schema (@AMHOL)' 90 | - version: 0.9.1 91 | date: '2019-10-23' 92 | fixed: 93 | - Attributes of a struct are no longer accidentally passed to their associations (@psparrow) 94 | - version: 0.9.0 95 | date: '2019-08-12' 96 | added: 97 | - 'When attributes hash includes unknown attributes, a `ROM::Factory::UnknownAttributeError` will be raised (@rawburt)' 98 | - version: 0.8.0 99 | date: '2019-04-24' 100 | fixed: 101 | - 'Loaded association structs are no longer rejected by output schemas (issue #34) (flash-gordon + solnic)' 102 | - version: 0.7.0 103 | date: '2018-11-17' 104 | added: 105 | - Support for traits (v-kolesnikov) 106 | - Support building structs with associations (@ianks) 107 | fixed: 108 | - Overwritten attributes with dependencies (JanaVPetrova) 109 | - version: 0.6.0 110 | date: '2018-01-31' 111 | added: 112 | - Support for factories with custom struct namespaces (solnic) 113 | changed: 114 | - 'Accessing a factory which is not defined will result in `FactoryNotDefinedError` exception (GustavoCaso + solnic)' 115 | fixed: 116 | - 'Using dependent attributes with sequences works correctly, ie `f.sequence(:login) { |i, name| "name-#{i}"}` (solnic)' 117 | - version: 0.5.0 118 | date: '2017-10-24' 119 | added: 120 | - Updated to rom 4.0 (solnic) 121 | - 'Support for `has_many` and `has_one` associations (solnic)' 122 | - Support for attributes depending on values from other attributes (solnic) 123 | - 'Support for `rand` inside the generator block (flash-gordon)' 124 | changed: 125 | - 'Depends on `rom-core` now (solnic)' 126 | - version: 0.4.0 127 | date: '2017-03-03' 128 | summary: improves internals. 129 | added: 130 | - 'Support for defining multiple factories via `MyFactory = ROM::Factory.configure { |c| ... }` (solnic)' 131 | - 'Support for builder inheritence via `define(admin: :user) { |f| ... }` (solnic)' 132 | - 'Support for generating in-memory structs via `MyFactory.structs[:user]` that are not persisted (solnic)' 133 | - 'Support for `belongs_to` associations via `f.association(:user)` (solnic)' 134 | - 'New DSL for defining builders `MyFactory.define(:user) { |f| ... }` which infers default relation name (solnic)' 135 | - 'New factory method `MyFactory#[]` ie `MyFactory[:user, name: "Jane"]` (solnic)' 136 | - 'New `fake` helper which uses faker gem under the hood ie `f.email { fake(:internet, :email) }` (solnic)' 137 | changed: 138 | - "`Rom::Factory::Config.configure` was replaced with `ROM::Factory.configure` (solnic)" 139 | - Global factory config and builders are gone (solnic) 140 | - Structs are now based on dry-struct (solnic) 141 | -------------------------------------------------------------------------------- /compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | services: 3 | db: 4 | image: postgres 5 | ports: 6 | - 5432 7 | env_file: .postgres.env 8 | -------------------------------------------------------------------------------- /docsite/source/index.html.md: -------------------------------------------------------------------------------- 1 | --- 2 | position: 7 3 | chapter: Factory 4 | --- 5 | 6 | # rom-factory 7 | 8 | `rom-factory` provides a simple API for creating and persisting `ROM::Struct`'s. If you already know FactoryBot you'll definitely understand the concept. 9 | 10 | ## Installation 11 | 12 | First of all you need to define a ROM Container for Factory. For example if you are using `rspec`, you can add these lines to `spec_helper.rb`. Also you need to require here all files with Factories. 13 | 14 | ```ruby 15 | Factory = ROM::Factory.configure do |config| 16 | config.rom = my_rom_container 17 | end 18 | 19 | Dir[File.dirname(__FILE__) + '/support/factories/*.rb'].each { |file| require file } 20 | ``` 21 | 22 | ## Usage 23 | 24 | ### Define factory 25 | 26 | ```ruby 27 | # 'spec/support/factories/users.rb' 28 | 29 | Factory.define(:user) do |f| 30 | f.name 'John' 31 | f.age 42 32 | end 33 | ``` 34 | #### Specify relations 35 | 36 | You can specify ROM relation if you want. It'll be pluralized factory name by default. 37 | 38 | ```ruby 39 | Factory.define(:user, relation: :people) do |f| 40 | f.name 'John' 41 | f.age 42 42 | end 43 | ``` 44 | 45 | #### Specify namespace for your structs 46 | 47 | Struct `User` will be found in `MyApp::Entities` namespace 48 | 49 | ```ruby 50 | Factory.define(:user, struct_namespace: MyApp::Entities) do |f| 51 | # ... 52 | end 53 | ``` 54 | 55 | #### Sequences 56 | 57 | You can use sequences for uniq fields 58 | 59 | ```ruby 60 | Factory.define(:user) do |f| 61 | f.name 'John' 62 | f.sequence(:email) { |n| "john#{n}@example.com" } 63 | end 64 | ``` 65 | 66 | #### Timestamps 67 | 68 | ```ruby 69 | Factory.define(:user) do |f| 70 | f.name 'John' 71 | f.timestamps 72 | # same as 73 | # f.created_at { Time.now } 74 | # f.updated_at { Time.now } 75 | end 76 | ``` 77 | 78 | #### Associations 79 | 80 | * belongs_to 81 | 82 | ```ruby 83 | Factory.define(:group) do |f| 84 | f.name 'Admins' 85 | end 86 | 87 | Factory.define(:user) do |f| 88 | f.name 'John' 89 | f.association(:group) 90 | end 91 | ``` 92 | 93 | * has_many 94 | 95 | ```ruby 96 | Factory.define(:group) do |f| 97 | f.name 'Admins' 98 | f.association(:user, count: 2) 99 | end 100 | 101 | Factory.define(:user) do |f| 102 | f.name 'John' 103 | end 104 | ``` 105 | 106 | #### Extend already existing factory 107 | 108 | ```ruby 109 | Factory.define(:user) do |f| 110 | f.name 'John' 111 | f.admin false 112 | end 113 | 114 | Factory.define(admin: :user) do |f| 115 | f.admin true 116 | end 117 | 118 | # Factory.structs[:admin] 119 | ``` 120 | 121 | #### Traits 122 | 123 | ```ruby 124 | Factory.define(:user) do |f| 125 | f.name 'John' 126 | f.admin false 127 | 128 | f.trait :with_age do |t| 129 | t.age 42 130 | end 131 | end 132 | 133 | # Factory.structs[:user, :with_age] 134 | ``` 135 | 136 | #### Build-in [Faker](https://github.com/faker-ruby/faker) objects 137 | 138 | ```ruby 139 | Factory.define(:user) do |f| 140 | f.email { fake(:internet, :email) } 141 | end 142 | ``` 143 | 144 | ##### Unique values with fake 145 | Passing the unique: true option will use Faker's [unique](https://github.com/faker-ruby/faker#ensuring-unique-values) feature 146 | ```ruby 147 | Factory.define(:user) do |f| 148 | f.email { fake(:internet, :email, unique: true) } 149 | end 150 | ``` 151 | 152 | #### Dependent attributes 153 | 154 | Attributes can be based on the values of other attributes: 155 | 156 | ```ruby 157 | Factory.define(:user) do |f| 158 | f.full_name { fake(:name) } 159 | # Dependent attributes are inferred from the block parameter names: 160 | f.login { |full_name| full_name.downcase.gsub(/\s+/, '_') } 161 | # Works with sequences too: 162 | f.sequence(:email) { |n, login| "#{login}-#{n}@example.com" } 163 | end 164 | ``` 165 | 166 | ### Build and persist objects 167 | 168 | ```ruby 169 | # Create in-memory object 170 | Factory.build(:user) 171 | 172 | # Persist struct in database 173 | Factory.create(:user) 174 | 175 | # Override attributes 176 | Factory.create(:user, age: 24) 177 | 178 | # Build and Create via #[] accessors 179 | Factory.structs[:user] 180 | Factory[:user] 181 | ``` 182 | -------------------------------------------------------------------------------- /lib/rom-factory.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rom/factory" 4 | -------------------------------------------------------------------------------- /lib/rom/factory.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/core" 4 | require "dry/configurable" 5 | require "dry/struct" 6 | 7 | require "rom/factory/factories" 8 | 9 | module ROM 10 | # Main ROM::Factory API 11 | # 12 | # @api public 13 | module Factory 14 | DEFAULT_NAME = "Factories" 15 | 16 | # Configure a new factory 17 | # 18 | # @example 19 | # MyFactory = ROM::Factory.configure do |config| 20 | # config.rom = my_rom_container 21 | # end 22 | # 23 | # @param [Symbol] name An optional factory class name 24 | # 25 | # @return [Class] 26 | # 27 | # @api public 28 | def self.configure(name = DEFAULT_NAME, &block) 29 | klass = Dry::Core::ClassBuilder.new(name: name, parent: Factories).call do |c| 30 | c.configure(&block) 31 | end 32 | 33 | klass.new(klass.config.rom) 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/rom/factory/attribute_registry.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "tsort" 4 | 5 | module ROM 6 | module Factory 7 | # @api private 8 | class AttributeRegistry 9 | include Enumerable 10 | include TSort 11 | 12 | # @api private 13 | attr_reader :elements 14 | 15 | # @api private 16 | def initialize(elements = []) 17 | @elements = elements 18 | end 19 | 20 | # @api private 21 | def each(&) = elements.each(&) 22 | 23 | # @api private 24 | def [](name) = detect { |e| e.name.equal?(name) } 25 | 26 | # @api private 27 | def <<(element) 28 | existing = self[element.name] 29 | elements.delete(existing) if existing 30 | elements << element 31 | self 32 | end 33 | 34 | # @api private 35 | def dup = self.class.new(elements.dup) 36 | 37 | # @api private 38 | def values = self.class.new(elements.select(&:value?)) 39 | 40 | # @api private 41 | def associations 42 | self.class.new(elements.select { |e| e.is_a?(Attributes::Association::Core) }) 43 | end 44 | 45 | def reject(&) = self.class.new(elements.reject(&)) 46 | 47 | # @api private 48 | def inspect 49 | "#<#{self.class} #{elements.inspect}>" 50 | end 51 | alias_method :to_s, :inspect 52 | 53 | private 54 | 55 | # @api private 56 | def tsort_each_node(&) = each(&) 57 | 58 | # @api private 59 | def tsort_each_child(attr, &) 60 | attr.dependency_names.map { |name| self[name] }.compact.each(&) 61 | end 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/rom/factory/attributes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ROM 4 | module Factory 5 | include Dry::Core::Constants 6 | end 7 | end 8 | 9 | require "rom/factory/attributes/value" 10 | require "rom/factory/attributes/callable" 11 | require "rom/factory/attributes/sequence" 12 | require "rom/factory/attributes/association" 13 | -------------------------------------------------------------------------------- /lib/rom/factory/attributes/association.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ROM::Factory 4 | module Attributes 5 | # @api private 6 | # rubocop:disable Style/OptionalArguments 7 | module Association 8 | class << self 9 | def new(assoc, ...) 10 | const_get(assoc.definition.type).new(assoc, ...) 11 | end 12 | end 13 | 14 | # @api private 15 | class Core 16 | attr_reader :assoc, :options, :traits 17 | 18 | # @api private 19 | def initialize(assoc, builder, *traits, **options) 20 | @assoc = assoc 21 | @builder_proc = builder 22 | @traits = traits 23 | @options = options 24 | end 25 | 26 | # @api private 27 | def through? = false 28 | 29 | # @api private 30 | def builder 31 | @__builder__ ||= @builder_proc.call 32 | end 33 | 34 | # @api private 35 | def name = assoc.key 36 | 37 | # @api private 38 | def dependency?(*) = false 39 | 40 | # @api private 41 | def value? = false 42 | 43 | # @api private 44 | def factories = builder.factories 45 | 46 | # @api private 47 | def foreign_key = assoc.foreign_key 48 | 49 | # @api private 50 | def count = options.fetch(:count, 1) 51 | end 52 | 53 | # @api private 54 | class ManyToOne < Core 55 | # @api private 56 | # rubocop:disable Metrics/AbcSize 57 | def call(attrs, persist: true) 58 | return if attrs.key?(name) && attrs[name].nil? 59 | 60 | assoc_data = attrs.fetch(name, EMPTY_HASH) 61 | 62 | if assoc_data.is_a?(::Hash) && assoc_data[assoc.target.primary_key] && !attrs[foreign_key] 63 | assoc.associate(attrs, attrs[name]) 64 | elsif assoc_data.is_a?(::ROM::Struct) 65 | assoc.associate(attrs, assoc_data) 66 | else 67 | parent = 68 | if persist && !attrs[foreign_key] 69 | builder.persistable.create(*parent_traits, **assoc_data) 70 | else 71 | builder.struct( 72 | *parent_traits, 73 | **assoc_data, assoc.target.primary_key => attrs[foreign_key] 74 | ) 75 | end 76 | 77 | tuple = {name => parent} 78 | 79 | assoc.associate(tuple, parent) 80 | end 81 | end 82 | # rubocop:enable Metrics/AbcSize 83 | 84 | private 85 | 86 | def parent_traits 87 | @parent_traits ||= 88 | if assoc.target.associations.key?(assoc.source.name) 89 | traits + [assoc.target.associations[assoc.source.name].key => false] 90 | else 91 | traits 92 | end 93 | end 94 | end 95 | 96 | # @api private 97 | class OneToMany < Core 98 | # @api private 99 | def call(attrs = EMPTY_HASH, parent, persist: true) 100 | return if attrs.key?(name) 101 | 102 | structs = ::Array.new(count).map do 103 | # hash which contains the foreign key info, i.e: { user_id: 1 } 104 | association_hash = assoc.associate(attrs, parent) 105 | 106 | if persist 107 | builder.persistable.create(*traits, **association_hash) 108 | else 109 | builder.struct(*traits, **attrs, **association_hash) 110 | end 111 | end 112 | 113 | {name => structs} 114 | end 115 | 116 | # @api private 117 | def dependency?(rel) = assoc.source == rel 118 | end 119 | 120 | # @api private 121 | class OneToOne < OneToMany 122 | # @api private 123 | def call(attrs = EMPTY_HASH, parent, persist: true) 124 | # do not associate if count is 0 125 | return {name => nil} if count.zero? 126 | 127 | return if attrs.key?(name) 128 | 129 | association_hash = assoc.associate(attrs, parent) 130 | 131 | struct = 132 | if persist 133 | builder.persistable.create(*traits, **association_hash) 134 | else 135 | belongs_to_name = ::ROM::Inflector.singularize(assoc.source_alias) 136 | belongs_to_associations = {belongs_to_name.to_sym => parent} 137 | final_attrs = attrs.merge(association_hash).merge(belongs_to_associations) 138 | builder.struct(*traits, **final_attrs) 139 | end 140 | 141 | {name => struct} 142 | end 143 | end 144 | 145 | class ManyToMany < Core 146 | def call(attrs = EMPTY_HASH, parent, persist: true) 147 | return if attrs.key?(name) 148 | 149 | structs = count.times.map do 150 | if persist && attrs[tpk] 151 | attrs 152 | elsif persist 153 | builder.persistable.create(*traits, **attrs) 154 | else 155 | builder.struct(*traits, **attrs) 156 | end 157 | end 158 | 159 | # Delegate to through factory if it exists 160 | if persist 161 | if through_factory? 162 | structs.each do |child| 163 | through_attrs = { 164 | ::ROM::Inflector.singularize(assoc.source.name.key).to_sym => parent, 165 | assoc.through.assoc_name => child 166 | } 167 | 168 | factories[through_factory_name, **through_attrs] 169 | end 170 | else 171 | assoc.persist([parent], structs) 172 | end 173 | 174 | {name => result(structs)} 175 | else 176 | result(structs) 177 | end 178 | end 179 | 180 | def result(structs) = {name => structs} 181 | 182 | def dependency?(rel) = assoc.source == rel 183 | 184 | def through? = true 185 | 186 | def through_factory? 187 | factories.registry.key?(through_factory_name) 188 | end 189 | 190 | def through_factory_name 191 | ::ROM::Inflector.singularize(assoc.definition.through.source).to_sym 192 | end 193 | 194 | private 195 | 196 | def tpk = assoc.target.primary_key 197 | end 198 | 199 | class OneToOneThrough < ManyToMany 200 | def result(structs) = {name => structs[0]} 201 | end 202 | end 203 | end 204 | # rubocop:enable Style/OptionalArguments 205 | end 206 | -------------------------------------------------------------------------------- /lib/rom/factory/attributes/callable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ROM::Factory 4 | module Attributes 5 | # @api private 6 | class Callable 7 | attr_reader :name, :dsl, :block 8 | 9 | # @api private 10 | def initialize(name, dsl, block) 11 | @name = name 12 | @dsl = dsl 13 | @block = block 14 | end 15 | 16 | # @api private 17 | def call(attrs, *args) 18 | result = attrs[name] || dsl.instance_exec(*args, &block) 19 | {name => result} 20 | end 21 | 22 | # @api private 23 | def value? 24 | true 25 | end 26 | 27 | # @api private 28 | def dependency_names 29 | block.parameters.map(&:last) 30 | end 31 | 32 | # @api private 33 | def inspect 34 | "#<#{self.class.name} #{name} at #{block.source_location.join(":")}>" 35 | end 36 | alias_method :to_s, :inspect 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/rom/factory/attributes/sequence.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ROM::Factory 4 | module Attributes 5 | class Sequence 6 | attr_reader :name, :count, :block 7 | 8 | def initialize(name, &block) 9 | @name = name 10 | @count = 0 11 | @block = block 12 | end 13 | 14 | def call(*args) 15 | block.call(increment, *args) 16 | end 17 | 18 | def to_proc 19 | method(:call).to_proc 20 | end 21 | 22 | def increment 23 | @count += 1 24 | end 25 | 26 | def dependency_names 27 | EMPTY_ARRAY 28 | end 29 | 30 | def parameters 31 | block.parameters 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/rom/factory/attributes/value.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ROM::Factory 4 | module Attributes 5 | # @api private 6 | class Value 7 | attr_reader :name, :value 8 | 9 | # @api private 10 | def initialize(name, value) 11 | @name = name 12 | @value = value 13 | end 14 | 15 | # @api private 16 | def call(attrs = EMPTY_HASH) 17 | return if attrs.key?(name) 18 | 19 | {name => value} 20 | end 21 | 22 | # @api private 23 | def value? 24 | true 25 | end 26 | 27 | # @api private 28 | def dependency_names 29 | EMPTY_ARRAY 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/rom/factory/builder.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rom/struct" 4 | require "rom/initializer" 5 | require "rom/factory/tuple_evaluator" 6 | require "rom/factory/builder/persistable" 7 | 8 | module ROM::Factory 9 | # @api private 10 | class Builder 11 | extend ROM::Initializer 12 | 13 | include Dry::Core::Constants 14 | 15 | # @!attribute [r] attributes 16 | # @return [ROM::Factory::Attributes] 17 | param :attributes 18 | 19 | # @!attribute [r] traits 20 | # @return [Hash] 21 | param :traits, default: -> { EMPTY_HASH } 22 | 23 | # @!attribute [r] relation 24 | # @return [ROM::Relation] 25 | option :relation, reader: false 26 | 27 | # @!attribute [r] struct_namespace 28 | # @return [Module] Custom struct namespace 29 | option :struct_namespace, reader: false 30 | 31 | # @!attribute [r] factories 32 | # @return [Module] Factories with other builders 33 | option :factories, reader: true, optional: true 34 | 35 | # @api private 36 | def tuple(*traits, **attrs) 37 | tuple_evaluator.defaults(traits, attrs) 38 | end 39 | 40 | # @api private 41 | def struct(*traits, **attrs) 42 | validate_keys(traits, attrs, allow_associations: true) 43 | 44 | tuple_evaluator.struct(*traits, **attrs) 45 | end 46 | alias_method :create, :struct 47 | 48 | # @api private 49 | def struct_namespace(namespace) 50 | if options[:struct_namespace][:overridable] 51 | with(struct_namespace: options[:struct_namespace].merge(namespace: namespace)) 52 | else 53 | self 54 | end 55 | end 56 | 57 | # @api private 58 | def persistable 59 | Persistable.new(self, relation) 60 | end 61 | 62 | # @api private 63 | def tuple_evaluator 64 | @__tuple_evaluator__ ||= TupleEvaluator.new( 65 | attributes, 66 | tuple_evaluator_relation, 67 | traits 68 | ) 69 | end 70 | 71 | # @api private 72 | def tuple_evaluator_relation 73 | options[:relation].struct_namespace(options[:struct_namespace][:namespace]) 74 | end 75 | 76 | # @api private 77 | def relation 78 | tuple_evaluator.relation 79 | end 80 | 81 | # @api private 82 | def validate_keys(traits, tuple, allow_associations: false) 83 | schema_keys = relation.schema.attributes.map(&:name) 84 | assoc_keys = tuple_evaluator.assoc_names(traits) 85 | unknown_keys = tuple.keys - schema_keys - assoc_keys 86 | 87 | unknown_keys -= relation.schema.associations.to_h.keys if allow_associations 88 | 89 | raise UnknownFactoryAttributes, unknown_keys unless unknown_keys.empty? 90 | end 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /lib/rom/factory/builder/persistable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "delegate" 4 | 5 | module ROM 6 | module Factory 7 | class Builder 8 | # @api private 9 | class Persistable < SimpleDelegator 10 | # @api private 11 | attr_reader :builder 12 | 13 | # @api private 14 | attr_reader :relation 15 | 16 | # @api private 17 | def initialize(builder, relation = builder.relation) 18 | super(builder) 19 | @builder = builder 20 | @relation = relation 21 | end 22 | 23 | # @api private 24 | def create(*traits, **attrs) 25 | validate_keys(traits, attrs) 26 | 27 | tuple = tuple(*traits, **attrs) 28 | persisted = persist(tuple) 29 | 30 | if tuple_evaluator.has_associations?(traits) 31 | tuple_evaluator.persist_associations(tuple, persisted, traits) 32 | 33 | pk = primary_key_names.map { |key| persisted[key] } 34 | 35 | relation.by_pk(*pk).combine(*tuple_evaluator.assoc_names(traits)).first 36 | else 37 | persisted 38 | end 39 | end 40 | 41 | private 42 | 43 | # @api private 44 | def persist(attrs) 45 | result = relation 46 | .with(auto_struct: !tuple_evaluator.has_associations?) 47 | .command(:create) 48 | .call(attrs) 49 | 50 | # Handle PK values generated by the factory 51 | if pk? && (pks = attrs.values_at(*primary_key_names)).compact.size == primary_key_names.size 52 | relation.by_pk(*pks).one! 53 | elsif result 54 | result 55 | else 56 | relation.where(attrs).one! 57 | end 58 | end 59 | 60 | # @api private 61 | def primary_key_names 62 | relation.schema.primary_key.map(&:name) 63 | end 64 | 65 | def pk? 66 | primary_key_names.any? 67 | end 68 | end 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/rom/factory/constants.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ROM 4 | module Factory 5 | class FactoryNotDefinedError < StandardError 6 | def initialize(name) 7 | super("Factory +#{name}+ not defined") 8 | end 9 | end 10 | 11 | class UnknownFactoryAttributes < StandardError 12 | def initialize(attrs) 13 | super("Unknown attributes: #{attrs.join(", ")}") 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/rom/factory/dsl.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "faker" 4 | 5 | require "rom/factory/builder" 6 | require "rom/factory/attribute_registry" 7 | require "rom/factory/attributes" 8 | 9 | module ROM 10 | module Factory 11 | extend ::Dry::Core::Cache 12 | 13 | class << self 14 | # @api private 15 | def fake(*args, unique: false, **options) 16 | factory, produce = fetch_or_store(:faker, unique, *args) do 17 | *ns, method_name = args 18 | 19 | const = ns.reduce(::Faker) do |obj, name| 20 | obj.const_get(::ROM::Inflector.camelize(name)) 21 | end 22 | 23 | if unique 24 | [const.unique, method_name] 25 | else 26 | [const, method_name] 27 | end 28 | end 29 | 30 | factory.public_send(produce, **options) 31 | end 32 | end 33 | 34 | # Factory builder DSL 35 | # 36 | # @api public 37 | class DSL < ::BasicObject 38 | # @api private 39 | module Kernel 40 | %i[binding class instance_of? is_a? rand respond_to_missing? singleton_class].each do |meth| 41 | define_method(meth, ::Kernel.instance_method(meth)) 42 | end 43 | 44 | private :respond_to_missing?, :rand, :binding 45 | end 46 | 47 | include Kernel 48 | 49 | attr_reader :_name, :_relation, :_attributes, :_factories, :_struct_namespace, :_valid_names 50 | attr_reader :_traits 51 | 52 | # @api private 53 | def initialize(name, relation:, factories:, struct_namespace:, attributes: AttributeRegistry.new) 54 | @_name = name 55 | @_relation = relation 56 | @_factories = factories 57 | @_struct_namespace = struct_namespace 58 | @_attributes = attributes.dup 59 | @_traits = {} 60 | @_valid_names = _relation.schema.attributes.map(&:name) 61 | yield(self) 62 | end 63 | 64 | # @api private 65 | def call 66 | ::ROM::Factory::Builder.new( 67 | _attributes, 68 | _traits, 69 | relation: _relation, 70 | struct_namespace: _struct_namespace, 71 | factories: _factories 72 | ) 73 | end 74 | 75 | # Delegate to a builder and persist a struct 76 | # 77 | # @param [Symbol] The name of the registered builder 78 | # 79 | # @api public 80 | def create(name, *args) = _factories[name, *args] 81 | 82 | # Create a sequence attribute 83 | # 84 | # @param [Symbol] name The attribute name 85 | # 86 | # @api private 87 | def sequence(meth, &) 88 | define_sequence(meth, &) if _valid_names.include?(meth) 89 | end 90 | 91 | # Set timestamp attributes 92 | # 93 | # @api public 94 | def timestamps 95 | created_at { ::Time.now } 96 | updated_at { ::Time.now } 97 | end 98 | 99 | # Create a fake value using Faker gem 100 | # 101 | # @overload fake(type) 102 | # @example 103 | # f.email { fake(:name) } 104 | # 105 | # @param [Symbol] type The value type to generate 106 | # 107 | # @overload fake(genre, type) 108 | # @example 109 | # f.email { fake(:internet, :email) } 110 | # 111 | # @param [Symbol] genre The faker API identifier ie. :internet, :product etc. 112 | # @param [Symbol] type The value type to generate 113 | # 114 | # @overload fake(genre, type, **options) 115 | # @example 116 | # f.email { fake(:number, :between, from: 10, to: 100) } 117 | # 118 | # @example 119 | # f.email { fake(:internet, :email, unique: true) } 120 | # 121 | # @param [Symbol] genre The faker API identifier ie. :internet, :product etc. 122 | # @param [Symbol] type The value type to generate 123 | # @param [Hash] options Additional arguments, including unique: true will generate unique values 124 | # 125 | # 126 | # @overload fake(genre, subgenre, type, **options) 127 | # @example 128 | # f.quote { fake(:books, :dune, :quote, character: 'stilgar') } 129 | # 130 | # @param [Symbol] genre The Faker genre of API i.e. :books, :creature, :games etc 131 | # @param [Symbol] subgenre The subgenre of API i.e. :dune, :bird, :myst etc 132 | # @param [Symbol] type the value type to generate 133 | # @param [Hash] options Additional arguments 134 | # 135 | # @see https://github.com/faker-ruby/faker/tree/master/doc 136 | # 137 | # @api public 138 | def fake(...) = ::ROM::Factory.fake(...) 139 | 140 | def trait(name, parents = [], &) 141 | _traits[name] = DSL.new( 142 | "#{_name}_#{name}", 143 | attributes: _traits.values_at(*parents).flat_map(&:elements).inject( 144 | AttributeRegistry.new, :<< 145 | ), 146 | relation: _relation, 147 | factories: _factories, 148 | struct_namespace: _struct_namespace, 149 | & 150 | )._attributes 151 | end 152 | 153 | # Create an association attribute 154 | # 155 | # @example belongs-to 156 | # f.association(:group) 157 | # 158 | # @example has-many 159 | # f.association(:posts, count: 2) 160 | # 161 | # @example adding traits 162 | # f.association(:posts, traits: [:published]) 163 | # 164 | # @param [Symbol] name The name of the configured association 165 | # @param [Hash] options Additional options 166 | # @option options [Integer] count Number of objects to generate 167 | # @option options [Array] traits Traits to apply to the association 168 | # 169 | # @api public 170 | def association(name, *seq_traits, traits: EMPTY_ARRAY, **options) 171 | assoc = _relation.associations[name] 172 | 173 | if assoc.is_a?(::ROM::SQL::Associations::OneToOne) && options.fetch(:count, 1) > 1 174 | ::Kernel.raise ::ArgumentError, "count cannot be greater than 1 on a OneToOne" 175 | end 176 | 177 | builder = -> { _factories.for_relation(assoc.target) } 178 | 179 | _attributes << attributes::Association.new( 180 | assoc, 181 | builder, 182 | *seq_traits, 183 | *traits, 184 | **options 185 | ) 186 | end 187 | 188 | # @api private 189 | def inspect = "#<#{self.class} name=#{_name}>" 190 | alias_method :to_s, :inspect 191 | 192 | private 193 | 194 | # @api private 195 | def method_missing(meth, ...) 196 | if _valid_names.include?(meth) 197 | define_attr(meth, ...) 198 | else 199 | super 200 | end 201 | end 202 | 203 | # @api private 204 | def respond_to_missing?(method_name, include_private = false) 205 | _valid_names.include?(method_name) || super 206 | end 207 | 208 | # @api private 209 | def define_sequence(name, &) 210 | _attributes << attributes::Callable.new(name, self, attributes::Sequence.new(name, &)) 211 | end 212 | 213 | # @api private 214 | def define_attr(name, *args, &block) 215 | _attributes << 216 | if block 217 | attributes::Callable.new(name, self, block) 218 | else 219 | attributes::Value.new(name, *args) 220 | end 221 | end 222 | 223 | # @api private 224 | def attributes = ::ROM::Factory::Attributes 225 | end 226 | end 227 | end 228 | -------------------------------------------------------------------------------- /lib/rom/factory/factories.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rom/initializer" 4 | require "rom/struct" 5 | require "rom/factory/dsl" 6 | require "rom/factory/registry" 7 | 8 | module ROM::Factory 9 | # In-memory builder API 10 | # 11 | # @api public 12 | class Structs 13 | # @!attribute [r] registry 14 | # @return [HashBuilder>] 15 | attr_reader :registry 16 | 17 | # @!attribute [r] struct_namespace 18 | # @return [Module] 19 | attr_reader :struct_namespace 20 | 21 | # @api private 22 | def initialize(registry, struct_namespace) 23 | @registry = registry 24 | @struct_namespace = struct_namespace 25 | end 26 | 27 | # Build an in-memory struct 28 | # 29 | # @example create a struct with default attributes 30 | # MyFactory[:user] 31 | # 32 | # @example create a struct with some attributes overridden 33 | # MyFactory.structs[:user, name: "Jane"] 34 | # 35 | # @param [Symbol] name The name of the registered factory 36 | # @param [Hash] attrs An optional hash with attributes 37 | # 38 | # @return [ROM::Struct] 39 | # 40 | # @api public 41 | def [](name, *traits, **attrs) 42 | registry[name].struct_namespace(struct_namespace).create(*traits, **attrs) 43 | end 44 | end 45 | 46 | # A registry with all configured factories 47 | # 48 | # @api public 49 | class Factories 50 | extend Dry::Configurable 51 | extend ROM::Initializer 52 | 53 | setting :rom 54 | 55 | # @!attribute [r] rom 56 | # @return [ROM::Container] configured rom container 57 | param :rom 58 | 59 | # @!attribute [r] struct_namespace 60 | # @return [Structs] in-memory struct builder instance 61 | option :struct_namespace, optional: true, default: proc { ROM::Struct } 62 | 63 | # @!attribute [r] registry 64 | # @return [HashBuilder>] a map with defined db-backed builders 65 | option :registry, default: proc { Registry.new } 66 | 67 | # Define a new builder 68 | # 69 | # @example a simple builder 70 | # MyFactory.define(:user) do |f| 71 | # f.name "Jane" 72 | # f.email "jane@doe.org" 73 | # end 74 | # 75 | # @example a builder using auto-generated fake values 76 | # MyFactory.define(:user) do |f| 77 | # f.name { fake(:name) } 78 | # f.email { fake(:internet, :email) } 79 | # end 80 | # 81 | # @example a builder using sequenced values 82 | # MyFactory.define(:user) do |f| 83 | # f.sequence(:name) { |n| "user-#{n}" } 84 | # end 85 | # 86 | # @example a builder using values from other attribute(s) 87 | # MyFactory.define(:user) do |f| 88 | # f.name "Jane" 89 | # f.email { |name| "#{name.downcase}@rom-rb.org" } 90 | # end 91 | # 92 | # @example a builder with "belongs-to" association 93 | # MyFactory.define(:group) do |f| 94 | # f.name "Admins" 95 | # end 96 | # 97 | # MyFactory.define(:user) do |f| 98 | # f.name "Jane" 99 | # f.association(:group) 100 | # end 101 | # 102 | # @example a builder with "has-many" association 103 | # MyFactory.define(:group) do |f| 104 | # f.name "Admins" 105 | # f.association(:users, count: 2) 106 | # end 107 | # 108 | # MyFactory.define(:user) do |f| 109 | # f.sequence(:name) { |n| "user-#{n}" } 110 | # end 111 | # 112 | # @example a builder which extends another builder 113 | # MyFactory.define(:user) do |f| 114 | # f.name "Jane" 115 | # f.admin false 116 | # end 117 | # 118 | # MyFactory.define(admin: :user) do |f| 119 | # f.admin true 120 | # end 121 | # 122 | # @param [Symbol, HashSymbol>] spec Builder identifier, can point to a parent builder too 123 | # @param [Hash] opts Additional options 124 | # @option opts [Symbol] relation An optional relation name (defaults to pluralized builder name) 125 | # 126 | # @return [ROM::Factory::Builder] 127 | # 128 | # @api public 129 | def define(spec, opts = EMPTY_HASH, &) 130 | name, parent = spec.is_a?(Hash) ? spec.flatten(1) : spec 131 | namespace = opts[:struct_namespace] 132 | relation_name = opts.fetch(:relation) { infer_relation(name) } 133 | 134 | if registry.key?(name) 135 | raise ArgumentError, "#{name.inspect} factory has been already defined" 136 | end 137 | 138 | builder = 139 | if parent 140 | extend_builder(name, registry[parent], relation_name, namespace, &) 141 | else 142 | relation = rom.relations[relation_name] 143 | DSL.new( 144 | name, 145 | relation: relation, 146 | factories: self, 147 | struct_namespace: builder_struct_namespace(namespace), 148 | & 149 | ).call 150 | end 151 | 152 | registry[name] = builder 153 | end 154 | 155 | # Create and persist a new struct 156 | # 157 | # @example create a struct with default attributes 158 | # MyFactory[:user] 159 | # 160 | # @example create a struct with some attributes overridden 161 | # MyFactory[:user, name: "Jane"] 162 | # 163 | # @param [Symbol] name The name of the registered factory 164 | # @param [Array] traits List of traits to apply 165 | # @param [Hash] attrs optional attributes to override the defaults 166 | # 167 | # @return [ROM::Struct] 168 | # 169 | # @api public 170 | def [](name, *traits, **attrs) 171 | registry[name].struct_namespace(struct_namespace).persistable.create(*traits, **attrs) 172 | end 173 | alias_method :create, :[] 174 | 175 | # Return in-memory struct builder 176 | # 177 | # @return [Structs] 178 | # 179 | # @api public 180 | def structs 181 | @__structs__ ||= Structs.new(registry, struct_namespace) 182 | end 183 | 184 | # Return a new, non-persisted struct 185 | # 186 | # @example create a struct with default attributes 187 | # MyFactory.build(:user) 188 | # 189 | # @example create a struct with some attributes overridden 190 | # MyFactory.build(:uesr, name: "Jane") 191 | # 192 | # @param [Symbol] name The name of the registered factory 193 | # @param [Array] traits List of traits to apply 194 | # @param [Hash] attrs optional attributes to override the defaults 195 | # 196 | # @return [ROM::Struct] 197 | # 198 | # @api public 199 | def build(name, *traits, **attrs) 200 | structs[name, *traits, **attrs] 201 | end 202 | 203 | # Get factories with a custom struct namespace 204 | # 205 | # @example 206 | # EntityFactory = MyFactory.struct_namespace(MyApp::Entities) 207 | # 208 | # EntityFactory[:user] 209 | # # => # 210 | # 211 | # @param [Module] namespace 212 | # 213 | # @return [Factories] 214 | # 215 | # @api public 216 | def struct_namespace(namespace = Undefined) 217 | if namespace.equal?(Undefined) 218 | options[:struct_namespace] 219 | else 220 | with(struct_namespace: namespace) 221 | end 222 | end 223 | 224 | # @api private 225 | def builder_struct_namespace(ns) 226 | ns ? {namespace: ns, overridable: false} : {namespace: struct_namespace, overridable: true} 227 | end 228 | 229 | # @api private 230 | def for_relation(relation) 231 | registry[infer_factory_name(relation.name.to_sym)] 232 | end 233 | 234 | # @api private 235 | def infer_factory_name(name) 236 | ::ROM::Inflector.singularize(name).to_sym 237 | end 238 | 239 | # @api private 240 | def infer_relation(name) 241 | ::ROM::Inflector.pluralize(name).to_sym 242 | end 243 | 244 | # @api private 245 | def extend_builder(name, parent, relation_name, ns, &) 246 | namespace = parent.options[:struct_namespace] 247 | namespace = builder_struct_namespace(ns) if ns 248 | relation = rom.relations.fetch(relation_name) { parent.relation } 249 | DSL.new( 250 | name, 251 | attributes: parent.attributes, 252 | relation: relation, 253 | factories: self, 254 | struct_namespace: namespace, 255 | & 256 | ).call 257 | end 258 | end 259 | end 260 | -------------------------------------------------------------------------------- /lib/rom/factory/registry.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rom/factory/constants" 4 | 5 | module ROM 6 | module Factory 7 | # @api private 8 | class Registry 9 | # @!attribute [r] elements 10 | # @return [Hash] a hash with factory builders 11 | attr_reader :elements 12 | 13 | # @api private 14 | def initialize 15 | @elements = {} 16 | end 17 | 18 | # @api private 19 | def key?(name) 20 | elements.key?(name) 21 | end 22 | 23 | # @api private 24 | def []=(name, builder) 25 | elements[name] = builder 26 | end 27 | 28 | # @api private 29 | def [](name) 30 | elements.fetch(name) do 31 | raise FactoryNotDefinedError, name 32 | end 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/rom/factory/sequences.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "concurrent/map" 4 | require "singleton" 5 | 6 | module ROM 7 | module Factory 8 | # @api private 9 | class Sequences 10 | include Singleton 11 | 12 | # @api private 13 | attr_reader :registry 14 | 15 | # @api private 16 | def self.[](relation) 17 | key = :"#{relation.gateway}-#{relation.name.dataset}" 18 | -> { instance.next(key) } 19 | end 20 | 21 | # @api private 22 | def initialize 23 | reset 24 | end 25 | 26 | # @api private 27 | def next(key) 28 | registry.compute(key) { |v| (v || 0).succ } 29 | end 30 | 31 | # @api private 32 | def reset 33 | @registry = Concurrent::Map.new 34 | self 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/rom/factory/tuple_evaluator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rom/factory/sequences" 4 | 5 | module ROM 6 | module Factory 7 | # @api private 8 | class TupleEvaluator 9 | class TupleEvaluatorError < StandardError 10 | attr_reader :original_exception 11 | 12 | def initialize(relation, original_exception, attrs, traits, assoc_attrs) 13 | super(<<~STR) 14 | Failed to build attributes for #{relation.name} 15 | 16 | Attributes: 17 | #{attrs.inspect} 18 | 19 | Associations: 20 | #{assoc_attrs} 21 | 22 | Traits: 23 | #{traits.inspect} 24 | 25 | Original exception: #{original_exception.message} 26 | STR 27 | 28 | set_backtrace(original_exception.backtrace) 29 | end 30 | end 31 | 32 | # @api private 33 | attr_reader :attributes 34 | 35 | # @api private 36 | attr_reader :relation 37 | 38 | # @api private 39 | attr_reader :traits 40 | 41 | # @api private 42 | attr_reader :sequence 43 | 44 | # @api private 45 | def initialize(attributes, relation, traits = {}) 46 | @attributes = attributes 47 | @relation = relation.with(auto_struct: true) 48 | @traits = traits 49 | @sequence = Sequences[relation] 50 | end 51 | 52 | def model(traits, combine: assoc_names(traits)) 53 | @relation.combine(*combine).mapper.model 54 | end 55 | 56 | # @api private 57 | def defaults(traits, attrs, **opts) 58 | mergeable_attrs = select_mergeable_attrs(traits, attrs) 59 | evaluate(traits, attrs, opts).merge(mergeable_attrs) 60 | end 61 | 62 | # @api private 63 | def struct(*traits, **attrs) 64 | merged_attrs = struct_attrs.merge(defaults(traits, attrs, persist: false)) 65 | is_callable = proc { |_name, value| value.respond_to?(:call) } 66 | 67 | callables = merged_attrs.select(&is_callable) 68 | attributes = merged_attrs.reject(&is_callable) 69 | 70 | materialized_callables = {} 71 | callables.each_value do |callable| 72 | materialized_callables.merge!(callable.call(attributes, persist: false)) 73 | end 74 | 75 | attributes.merge!(materialized_callables) 76 | 77 | assoc_attrs = attributes.slice(*assoc_names(traits)).merge( 78 | assoc_names(traits) 79 | .select { |key| 80 | build_assoc?(key, attributes) 81 | } 82 | .map { |key| 83 | [key, build_assoc_attrs(key, attributes[relation.primary_key], attributes[key])] 84 | } 85 | .to_h 86 | ) 87 | 88 | model_attrs = relation.output_schema[attributes] 89 | model_attrs.update(assoc_attrs) 90 | 91 | model(traits).new(**model_attrs) 92 | rescue StandardError => e 93 | raise TupleEvaluatorError.new(relation, e, attrs, traits, assoc_attrs) 94 | end 95 | 96 | def build_assoc?(name, attributes) 97 | attributes.key?(name) && attributes[name] != [] && !attributes[name].nil? 98 | end 99 | 100 | def build_assoc_attrs(key, fk, value) 101 | if value.is_a?(Array) 102 | value.map { |el| build_assoc_attrs(key, fk, el) } 103 | else 104 | {attributes[key].foreign_key => fk}.merge(value.to_h) 105 | end 106 | end 107 | 108 | # @api private 109 | def persist_associations(tuple, parent, traits = []) 110 | assoc_names(traits).each do |name| 111 | assoc = tuple[name] 112 | assoc.call(parent, persist: true) if assoc.is_a?(Proc) 113 | end 114 | end 115 | 116 | # @api private 117 | def assoc_names(traits = []) 118 | assocs(traits).map(&:name) 119 | end 120 | 121 | def assocs(traits_names = []) 122 | found_assocs = traits 123 | .values_at(*traits_names) 124 | .compact 125 | .map(&:associations).flat_map(&:elements) 126 | .inject(AttributeRegistry.new(attributes.associations.elements), :<<) 127 | 128 | exclude = traits_names.select { |t| t.is_a?(Hash) }.reduce(:merge) || EMPTY_HASH 129 | 130 | found_assocs.reject { |a| exclude[a.name] == false } 131 | end 132 | 133 | # @api private 134 | def has_associations?(traits = []) 135 | !assoc_names(traits).empty? 136 | end 137 | 138 | # @api private 139 | def primary_key 140 | relation.primary_key 141 | end 142 | 143 | private 144 | 145 | # @api private 146 | def evaluate(traits, attrs, opts) 147 | evaluate_values(attrs) 148 | .merge(evaluate_associations(traits, attrs, opts)) 149 | .merge(evaluate_traits(traits, attrs, opts)) 150 | end 151 | 152 | # @api private 153 | def evaluate_values(attrs) 154 | attributes.values.tsort.each_with_object(attrs.dup) do |attr, h| 155 | deps = attr.dependency_names.filter_map { |k| h[k] } 156 | result = attr.(h, *deps) 157 | 158 | if result 159 | h.update(result) 160 | end 161 | end 162 | end 163 | 164 | def evaluate_traits(trait_list, attrs, opts) 165 | return EMPTY_HASH if trait_list.empty? 166 | 167 | traits = trait_list.map { |v| v.is_a?(Hash) ? v : {v => true} }.reduce(:merge) 168 | 169 | traits_attrs = self.traits.select { |key, _value| traits[key] }.values.flat_map(&:elements) 170 | registry = AttributeRegistry.new(traits_attrs) 171 | 172 | self.class.new(registry, relation).defaults([], attrs, **opts) 173 | end 174 | 175 | # @api private 176 | def evaluate_associations(traits, attrs, opts) 177 | assocs(traits).associations.each_with_object({}) do |assoc, memo| 178 | if attrs.key?(assoc.name) && attrs[assoc.name].nil? 179 | memo 180 | elsif assoc.dependency?(relation) 181 | memo[assoc.name] = ->(parent, call_opts) do 182 | assoc.call(parent, **opts, **call_opts) 183 | end 184 | else 185 | result = assoc.(attrs, **opts) 186 | memo.update(result) if result 187 | end 188 | end 189 | end 190 | 191 | # @api private 192 | def struct_attrs 193 | struct_attrs = relation.schema 194 | .reject(&:primary_key?) 195 | .map { |attr| [attr.name, nil] }.to_h 196 | 197 | if primary_key 198 | struct_attrs.merge(primary_key => next_id) 199 | else 200 | struct_attrs 201 | end 202 | end 203 | 204 | # @api private 205 | def next_id 206 | sequence.() 207 | end 208 | 209 | def select_mergeable_attrs(traits, attrs) 210 | unmergeable = assocs(traits).select(&:through?).map do |a| 211 | ::ROM::Inflector.singularize(a.assoc.target.name.to_sym).to_sym 212 | end 213 | attrs.dup.delete_if { |key, _| unmergeable.include?(key) } 214 | end 215 | end 216 | end 217 | end 218 | -------------------------------------------------------------------------------- /lib/rom/factory/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ROM 4 | module Factory 5 | VERSION = "0.13.0" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /project.yml: -------------------------------------------------------------------------------- 1 | name: rom-factory 2 | custom_ci: true 3 | codacy_id: 5fd26fae687549218458879b1a607e18 4 | -------------------------------------------------------------------------------- /rom-factory.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path("lib", __dir__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require "rom/factory/version" 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = "rom-factory" 9 | spec.version = ROM::Factory::VERSION 10 | spec.authors = ["Janis Miezitis", "Piotr Solnica"] 11 | spec.email = ["janjiss@gmail.com", "piotr.solnica@gmail.com"] 12 | 13 | spec.summary = "ROM based builder library to make your specs awesome. DSL partially inspired by FactoryBot." 14 | spec.description = "" 15 | spec.homepage = "https://github.com/rom-rb/rom-factory" 16 | spec.license = "MIT" 17 | 18 | # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host' 19 | # to allow pushing to a single host or delete this section to allow pushing to any host. 20 | spec.metadata["allowed_push_host"] = "https://rubygems.org" 21 | spec.metadata["rubygems_mfa_required"] = "true" 22 | 23 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 24 | spec.bindir = "exe" 25 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 26 | spec.require_paths = ["lib"] 27 | 28 | spec.required_ruby_version = ">= 3.1.0" 29 | 30 | spec.add_dependency "dry-configurable", "~> 1.3" 31 | spec.add_dependency "dry-core", "~> 1.1" 32 | spec.add_dependency "dry-struct", "~> 1.7" 33 | spec.add_dependency "faker", ">= 2.0", "< 4" 34 | spec.add_dependency "rom-core", "~> 5.4" 35 | end 36 | -------------------------------------------------------------------------------- /spec/integration/rom/factory_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe ROM::Factory do 4 | include_context "relations" 5 | 6 | subject(:factories) do 7 | ROM::Factory.configure do |config| 8 | config.rom = rom 9 | end 10 | end 11 | 12 | describe "factory is not defined" do 13 | it "raises error for persistable" do 14 | expect { factories[:not_defined] } 15 | .to raise_error(ROM::Factory::FactoryNotDefinedError) 16 | end 17 | 18 | it "raises error for structs" do 19 | expect { factories.structs[:not_defined] } 20 | .to raise_error(ROM::Factory::FactoryNotDefinedError) 21 | end 22 | end 23 | 24 | shared_examples_for "it builds a plain struct" do 25 | it "returns a plain struct builder" do 26 | factories.define(:user) do |f| 27 | f.first_name "Jane" 28 | f.last_name "Doe" 29 | f.email "jane@doe.org" 30 | f.timestamps 31 | end 32 | 33 | user1 = build(:user) 34 | user2 = build(:user) 35 | 36 | expect(user1.id).to_not be(nil) 37 | expect(user1.first_name).to eql("Jane") 38 | expect(user1.last_name).to_not be(nil) 39 | expect(user1.email).to_not be(nil) 40 | expect(user1.created_at).to_not be(nil) 41 | expect(user1.updated_at).to_not be(nil) 42 | 43 | expect(user1.id).to_not eql(user2.id) 44 | 45 | expect(rom.relations[:users].count).to be_zero 46 | 47 | expect(user1.class).to be(user2.class) 48 | end 49 | 50 | context "one-to-many" do 51 | before do 52 | factories.define(:task) do |f| 53 | f.sequence(:title) { |n| "Task #{n}" } 54 | end 55 | 56 | factories.define(:user) do |f| 57 | f.first_name "Jane" 58 | f.last_name "Doe" 59 | f.email "jane@doe.org" 60 | f.timestamps 61 | f.association(:tasks, count: 2) 62 | end 63 | end 64 | 65 | it "works when building parent" do 66 | user_with_tasks = build(:user) 67 | 68 | expect(user_with_tasks.tasks.length).to eql(2) 69 | expect(relations[:tasks].count).to be_zero 70 | expect(relations[:users].count).to be_zero 71 | expect(user_with_tasks.tasks).to all(respond_to(:title, :user_id)) 72 | expect(user_with_tasks.tasks).to all(have_attributes(user_id: user_with_tasks.id)) 73 | end 74 | 75 | it "sets a value explicitly" do 76 | user = build(:user, tasks: []) 77 | 78 | expect(user.tasks).to be_empty 79 | end 80 | 81 | it "sets a value explicitly when persisting" do 82 | user = build(:user, tasks: []) 83 | 84 | expect(user.tasks).to be_empty 85 | end 86 | 87 | it "does not create records when building child" do 88 | build(:task) 89 | 90 | expect(relations[:tasks].count).to be_zero 91 | expect(relations[:users].count).to be_zero 92 | end 93 | 94 | it "does not pass provided attributes into associations" do 95 | expect { 96 | build(:user, email: "jane@doe.com") 97 | }.not_to raise_error 98 | end 99 | end 100 | 101 | context "many-to-one" do 102 | context "with an association that is not aliased" do 103 | before do 104 | factories.define(:task) do |f| 105 | f.title { "Foo" } 106 | f.association(:user) 107 | end 108 | 109 | factories.define(:user) do |f| 110 | f.first_name "Jane" 111 | f.last_name "Doe" 112 | f.email "janjiss@gmail.com" 113 | f.timestamps 114 | 115 | f.association(:tasks) 116 | end 117 | end 118 | 119 | it "creates a struct with associated parent" do 120 | task = build(:task, title: "Bar") 121 | 122 | expect(task.title).to eql("Bar") 123 | expect(task.user.first_name).to eql("Jane") 124 | end 125 | 126 | it "does not build associated struct if it's set to nil explicitly" do 127 | task = build(:task, user: nil) 128 | 129 | expect(task.user).to be(nil) 130 | end 131 | 132 | it "does not persist associated struct if it's set to nil explicitly" do 133 | task = factories[:task, user: nil] 134 | 135 | expect(task.user).to be(nil) 136 | end 137 | 138 | it "creates the associated record with provided attributes" do 139 | task = factories[:task, user: {first_name: "John"}] 140 | 141 | expect(task.user.first_name).to eql("John") 142 | end 143 | end 144 | 145 | context "with an aliased association" do 146 | before do 147 | factories.define(:task) do |f| 148 | f.title { "Foo" } 149 | f.association(:author) 150 | end 151 | 152 | factories.define(:user) do |f| 153 | f.first_name "Jane" 154 | f.last_name "Doe" 155 | f.email "janjiss@gmail.com" 156 | f.timestamps 157 | 158 | f.association(:tasks) 159 | end 160 | end 161 | 162 | it "creates a struct with associated parent" do 163 | task = build(:task, title: "Bar") 164 | 165 | expect(task.title).to eql("Bar") 166 | expect(task.author.first_name).to eql("Jane") 167 | end 168 | 169 | it "does not build associated struct if it's set to nil explicitly" do 170 | task = build(:task, author: nil) 171 | 172 | expect(task.author).to be_nil 173 | end 174 | 175 | it "does not persist associated struct if it's set to nil explicitly" do 176 | task = factories[:task, author: nil] 177 | 178 | expect(task.author).to be_nil 179 | end 180 | 181 | it "creates the associated record with provided attributes" do 182 | task = factories[:task, author: {first_name: "John"}] 183 | 184 | expect(task.author.first_name).to eql("John") 185 | end 186 | end 187 | 188 | context "with a self-ref association" do 189 | before do 190 | factories.define(:task) do |f| 191 | f.title { "A Task" } 192 | f.association(:parent) 193 | end 194 | end 195 | 196 | it "creates the associated record with provided attributes" do 197 | task = factories[:task, title: "Foo", parent: {title: "Bar"}] 198 | 199 | expect(task.title).to eql("Foo") 200 | expect(task.parent.title).to eql("Bar") 201 | end 202 | end 203 | end 204 | 205 | context "one-to-one-through" do 206 | before do 207 | factories.define(:user) do |f| 208 | f.first_name "Janis" 209 | f.last_name "Miezitis" 210 | f.email "janjiss@gmail.com" 211 | f.timestamps 212 | 213 | f.association :address 214 | end 215 | 216 | factories.define(:user_address) do |f| 217 | f.association(:user) 218 | f.association(:address) 219 | f.timestamps 220 | end 221 | 222 | factories.define(:address) do |f| 223 | f.full_address "123 Elm St." 224 | end 225 | end 226 | 227 | context "when persisting" do 228 | it "creates the correct records when there's no pre-existing entity" do 229 | user = factories[:user] 230 | 231 | expect(user.address).to have_attributes(full_address: "123 Elm St.") 232 | end 233 | 234 | it "creates the join table record when there is a pre-existing entity" do 235 | address = factories[:address] 236 | user = factories[:user, address: address] 237 | 238 | expect(user.address).to have_attributes(full_address: "123 Elm St.") 239 | end 240 | end 241 | 242 | context "when building a struct" do 243 | it "persists the relation properly with pre-existing assoc record" do 244 | address = build(:address) 245 | user = build(:user, address: address) 246 | 247 | expect(user.address).to have_attributes(full_address: "123 Elm St.") 248 | end 249 | 250 | it "persists the relation properly without pre-existing assoc record" do 251 | user = build(:user) 252 | 253 | expect(user.address).to have_attributes(full_address: "123 Elm St.") 254 | end 255 | end 256 | end 257 | 258 | context "one-to-one" do 259 | let(:rom) do 260 | ROM.container(:sql, conn) do |conf| 261 | conf.default.create_table(:basic_users) do 262 | primary_key :id 263 | end 264 | 265 | conf.default.create_table(:basic_accounts) do 266 | primary_key :id 267 | foreign_key :basic_user_id, :basic_users 268 | column :created_at, Time, null: false 269 | end 270 | 271 | conf.relation(:basic_users) do 272 | schema(infer: true) do 273 | associations do 274 | has_one :basic_account 275 | end 276 | end 277 | end 278 | 279 | conf.relation(:basic_accounts) do 280 | schema(infer: true) do 281 | associations do 282 | belongs_to :basic_user 283 | end 284 | end 285 | end 286 | end 287 | end 288 | 289 | context "when both factories define the associations" do 290 | before do 291 | conn.drop_table?(:basic_accounts) 292 | conn.drop_table?(:basic_users) 293 | 294 | factories.define(:basic_user) do |f| 295 | f.association(:basic_account) 296 | end 297 | 298 | factories.define(:basic_account) do |f| 299 | f.association(:basic_user) 300 | end 301 | end 302 | 303 | it "works with one to one relationships with parent" do 304 | user = build(:basic_user) 305 | 306 | expect(relations[:basic_accounts].count).to be_zero 307 | expect(relations[:basic_users].count).to be_zero 308 | expect(user.basic_account).to have_attributes(basic_user_id: user.id) 309 | end 310 | 311 | it "does not persist when building a child struct" do 312 | build(:basic_account) 313 | 314 | expect(relations[:basic_accounts].count).to be_zero 315 | expect(relations[:basic_users].count).to be_zero 316 | end 317 | 318 | it "does not pass provided attributes into associations" do 319 | expect { 320 | build(:basic_account, created_at: Time.now) 321 | }.not_to raise_error 322 | end 323 | end 324 | 325 | context "when the child factory does not define the parent association" do 326 | before do 327 | conn.drop_table?(:basic_accounts) 328 | conn.drop_table?(:basic_users) 329 | 330 | factories.define(:basic_user) do |f| 331 | f.association(:basic_account) 332 | end 333 | 334 | factories.define(:basic_account) do |f| 335 | end 336 | end 337 | 338 | it "still allows building the parent struct" do 339 | basic_user = build(:basic_user) 340 | 341 | expect(basic_user.basic_account).to respond_to(:id) 342 | end 343 | end 344 | 345 | context "when the count is specified as 0" do 346 | before do 347 | conn.drop_table?(:basic_accounts) 348 | conn.drop_table?(:basic_users) 349 | 350 | factories.define(:basic_user) do |f| 351 | f.association(:basic_account, count: 0) 352 | end 353 | 354 | factories.define(:basic_account) do |f| 355 | f.association(:basic_user) 356 | end 357 | end 358 | 359 | it "does not create the related record" do 360 | user = factories[:basic_user] 361 | 362 | expect(user.basic_account).to be_nil 363 | end 364 | 365 | it "does not build the related record" do 366 | user = build(:basic_user) 367 | 368 | expect(user.basic_account).to be_nil 369 | end 370 | end 371 | 372 | context "when the count is greater than 0" do 373 | before do 374 | conn.drop_table?(:basic_accounts) 375 | conn.drop_table?(:basic_users) 376 | end 377 | 378 | it "raises an ArgumentError" do 379 | defining_with_count_greater_than_zero = proc do 380 | factories.define(:basic_user) do |f| 381 | f.association(:basic_account, count: 2) 382 | end 383 | end 384 | 385 | expect(&defining_with_count_greater_than_zero).to raise_error(ArgumentError) 386 | end 387 | end 388 | end 389 | end 390 | 391 | describe ".structs" do 392 | it_behaves_like "it builds a plain struct" 393 | 394 | def build(factory, *traits, **attrs) 395 | factories.structs[factory, *traits, **attrs] 396 | end 397 | end 398 | 399 | describe ".build" do 400 | it_behaves_like "it builds a plain struct" 401 | 402 | def build(factory, *traits, **attrs) 403 | factories.build(factory, *traits, **attrs) 404 | end 405 | end 406 | 407 | describe "factories builder DSL" do 408 | it "infers relation from the name" do 409 | factories.define(:user) do |f| 410 | f.first_name "Janis" 411 | f.last_name "Miezitis" 412 | f.email "janjiss@gmail.com" 413 | f.timestamps 414 | end 415 | 416 | user = factories[:user] 417 | 418 | expect(user.id).to_not be(nil) 419 | expect(user.first_name).to eql("Janis") 420 | end 421 | 422 | it "raises an error if arguments are not part of schema" do 423 | expect { 424 | factories.define(:user, relation: :users) do |f| 425 | f.boobly "Janis" 426 | end 427 | }.to raise_error(NoMethodError) 428 | end 429 | end 430 | 431 | shared_examples_for "it creates records" do 432 | it "creates a record based on defined factories" do 433 | factories.define(:user, relation: :users) do |f| 434 | f.first_name "Janis" 435 | f.last_name "Miezitis" 436 | f.email "janjiss@gmail.com" 437 | f.created_at Time.now 438 | f.updated_at Time.now 439 | end 440 | 441 | expect(user.email).not_to be_empty 442 | expect(user.first_name).not_to be_empty 443 | expect(user.last_name).not_to be_empty 444 | end 445 | 446 | it "supports callable values" do 447 | factories.define(:user, relation: :users) do |f| 448 | f.first_name "Janis" 449 | f.last_name "Miezitis" 450 | f.email "janjiss@gmail.com" 451 | f.created_at { Time.now } 452 | f.updated_at { Time.now } 453 | end 454 | 455 | expect(user.email).not_to be_empty 456 | expect(user.first_name).not_to be_empty 457 | expect(user.last_name).not_to be_empty 458 | expect(user.created_at).not_to be_nil 459 | expect(user.updated_at).not_to be_nil 460 | end 461 | 462 | it "supports rand inside the DSL" do 463 | factories.define(:user) do |f| 464 | f.first_name "Janis" 465 | f.last_name "Miezitis" 466 | f.email { "janjiss+#{rand(300)}@gmail.com" } 467 | f.created_at { Time.now } 468 | f.updated_at { Time.now } 469 | end 470 | 471 | expect(user.email).to match(/\d{1,3}/) 472 | end 473 | end 474 | 475 | context "creation of records via .[]" do 476 | let(:user) { factories[:user] } 477 | 478 | it_should_behave_like "it creates records" 479 | end 480 | 481 | context "creation of records via .create" do 482 | let(:user) { factories.create(:user) } 483 | 484 | it_should_behave_like "it creates records" 485 | end 486 | 487 | context "changing values" do 488 | it "supports overwriting of values" do 489 | factories.define(:user, relation: :users) do |f| 490 | f.first_name "Janis" 491 | f.last_name "Miezitis" 492 | f.email "janjiss@gmail.com" 493 | f.created_at Time.now 494 | f.updated_at Time.now 495 | end 496 | 497 | user = factories[:user, email: "holla@gmail.com"] 498 | 499 | expect(user.email).to eq("holla@gmail.com") 500 | end 501 | 502 | it "supports overwriting create values" do 503 | factories.define(:user, relation: :users) do |f| 504 | f.first_name "Janis" 505 | f.last_name "Miezitis" 506 | f.email "janjiss@gmail.com" 507 | f.created_at Time.now 508 | f.updated_at Time.now 509 | end 510 | 511 | user = factories.create(:user, email: "holla@gmail.com") 512 | 513 | expect(user.email).to eq("holla@gmail.com") 514 | end 515 | end 516 | 517 | context "dependant attributes" do 518 | it "passes generated value to the block of another attribute" do 519 | factories.define(:user, relation: :users) do |f| 520 | f.first_name { fake(:name) } 521 | f.last_name { fake(:name) } 522 | f.sequence(:email) { |i, first_name, last_name| "#{first_name}.#{last_name}@test-#{i}.org" } 523 | f.timestamps 524 | end 525 | 526 | user = factories[:user] 527 | 528 | expect(user.email).to eql("#{user.first_name}.#{user.last_name}@test-1.org") 529 | end 530 | 531 | it "can use passed values in the block" do 532 | factories.define(:user, relation: :users) do |f| 533 | f.last_name { fake(:name) } 534 | f.email { |first_name, last_name| "#{first_name}.#{last_name}@example.com" } 535 | f.timestamps 536 | end 537 | 538 | user = factories[:user, first_name: "Jane"] 539 | 540 | expect(user.email).to eql("Jane.#{user.last_name}@example.com") 541 | end 542 | 543 | it "can use passed values in the block" do 544 | factories.define(:user, relation: :users) do |f| 545 | f.first_name nil 546 | f.last_name { fake(:name) } 547 | f.email { |first_name| "#{first_name}@example.com" } 548 | f.timestamps 549 | end 550 | 551 | user = factories[:user, first_name: "Jane"] 552 | 553 | expect(user.email).to eql("Jane@example.com") 554 | end 555 | 556 | it "can use passed values in the block" do 557 | factories.define(:user, relation: :users) do |f| 558 | f.last_name { fake(:name) } 559 | f.email { |first_name| "#{first_name}@example.com" } 560 | f.timestamps 561 | end 562 | 563 | user = factories[:user, first_name: "Jane"] 564 | 565 | expect(user.email).to eql("Jane@example.com") 566 | end 567 | end 568 | 569 | context "changing values of dependant attributes" do 570 | it "sets correct values to attributes with overwritten dependant attributes" do 571 | factories.define(:user) do |f| 572 | f.first_name { fake(:name) } 573 | f.last_name { fake(:name) } 574 | f.email { |last_name| "#{last_name}@gmail.com" } 575 | f.timestamps 576 | end 577 | 578 | overwritten_last_name = "ivanov" 579 | user = factories[:user, last_name: overwritten_last_name] 580 | 581 | expect(user.last_name).to eql(overwritten_last_name) 582 | expect(user.email).to eq("#{overwritten_last_name}@gmail.com") 583 | end 584 | end 585 | 586 | context "incomplete schema" do 587 | it "fills in missing attributes" do 588 | factories.define(:user, relation: :users) do |f| 589 | f.first_name "Janis" 590 | f.last_name "Miezitis" 591 | f.email "janjiss@gmail.com" 592 | f.timestamps 593 | end 594 | 595 | user = factories[:user] 596 | 597 | expect(user.id).to_not be(nil) 598 | expect(user.age).to be(nil) 599 | end 600 | end 601 | 602 | context "errors" do 603 | it "raises error if factories with the same name is registered" do 604 | define = -> { 605 | factories.define(:user, relation: :users) {} 606 | } 607 | 608 | define.() 609 | 610 | expect { define.() }.to raise_error(ArgumentError) 611 | end 612 | 613 | it "raises error when trying to set missing attribute" do 614 | factories.define(:user, relation: :users) do |f| 615 | f.first_name "Janis" 616 | f.last_name "Miezitis" 617 | f.email "janjiss@gmail.com" 618 | f.timestamps 619 | end 620 | 621 | expect { 622 | factories[:user, not_real_attribute: "invalid attribute value"] 623 | }.to raise_error(ROM::Factory::UnknownFactoryAttributes) 624 | end 625 | end 626 | 627 | context "sequence" do 628 | it "supports sequencing of values" do 629 | factories.define(:user, relation: :users) do |f| 630 | f.sequence(:email) { |n| "janjiss#{n}@gmail.com" } 631 | f.first_name "Janis" 632 | f.last_name "Miezitis" 633 | f.created_at Time.now 634 | f.updated_at Time.now 635 | end 636 | 637 | user1 = factories[:user] 638 | user2 = factories[:user] 639 | 640 | expect(user1.email).to eq("janjiss1@gmail.com") 641 | expect(user2.email).to eq("janjiss2@gmail.com") 642 | end 643 | end 644 | 645 | context "timestamps" do 646 | it "creates timestamps, created_at and updated_at, based on callable property" do 647 | factories.define(:user, relation: :users) do |f| 648 | f.first_name "Janis" 649 | f.last_name "Miezitis" 650 | f.email "janjiss@gmail.com" 651 | f.timestamps 652 | end 653 | 654 | user1 = factories[:user] 655 | sleep 1 656 | user2 = factories[:user] 657 | 658 | expect(user1.created_at.class).to eq(Time) 659 | expect(user1.updated_at.class).to eq(Time) 660 | 661 | expect(user2.created_at).not_to eq(user1.created_at) 662 | expect(user2.updated_at).not_to eq(user1.updated_at) 663 | end 664 | end 665 | 666 | context "inheritance" do 667 | context "without struct_namespace option" do 668 | before do 669 | factories.define(:user, &:timestamps) 670 | 671 | factories.define(jane: :user) do |f| 672 | f.first_name "Jane" 673 | f.last_name "Doe" 674 | f.email "jane@doe.org" 675 | end 676 | 677 | factories.define(john: :jane) do |f| 678 | f.first_name "John" 679 | f.email "john@doe.org" 680 | end 681 | end 682 | context "using in-memory structs" do 683 | let(:jane) { factories.structs[:jane] } 684 | let(:john) { factories.structs[:john] } 685 | 686 | it "sets up a new builder based on another" do 687 | expect(jane.first_name).to eql("Jane") 688 | expect(jane.email).to eql("jane@doe.org") 689 | 690 | expect(john.first_name).to eql("John") 691 | expect(john.last_name).to eql("Doe") 692 | expect(john.email).to eql("john@doe.org") 693 | end 694 | end 695 | 696 | context "using persistable structs" do 697 | let(:jane) { factories[:jane] } 698 | let(:john) { factories[:john] } 699 | 700 | it "sets up a new builder based on another" do 701 | expect(jane.first_name).to eql("Jane") 702 | expect(jane.email).to eql("jane@doe.org") 703 | 704 | expect(john.first_name).to eql("John") 705 | expect(john.last_name).to eql("Doe") 706 | expect(john.email).to eql("john@doe.org") 707 | end 708 | end 709 | end 710 | 711 | context "with struct_namespace option" do 712 | before do 713 | module Test 714 | module Entities 715 | class User < ROM::Struct 716 | end 717 | end 718 | 719 | module AnotherEntities 720 | class Admin < ROM::Struct 721 | end 722 | end 723 | end 724 | 725 | factories.define(:user, struct_namespace: Test::Entities, &:timestamps) 726 | 727 | factories.define(jane: :user) do |f| 728 | f.first_name "Jane" 729 | f.last_name "Doe" 730 | f.email "jane@doe.org" 731 | end 732 | 733 | factories.define({admin: :jane}, struct_namespace: Test::AnotherEntities) do |f| 734 | f.type "Admin" 735 | end 736 | 737 | factories.define({john: :jane}, struct_namespace: Test::AnotherEntities) do |f| 738 | f.first_name "John" 739 | f.email "john@doe.org" 740 | end 741 | end 742 | 743 | context "using in-memory structs" do 744 | let(:jane) { factories.structs[:jane] } 745 | let(:john) { factories.structs[:john] } 746 | let(:admin) { factories.structs[:admin] } 747 | 748 | it "sets up a new builder based on another with correct struct_namespace" do 749 | expect(jane.first_name).to eql("Jane") 750 | expect(jane.email).to eql("jane@doe.org") 751 | expect(jane).to be_kind_of(Test::Entities::User) 752 | 753 | expect(jane.first_name).to eql("Jane") 754 | expect(jane.email).to eql("jane@doe.org") 755 | expect(admin.type).to eql("Admin") 756 | expect(admin).to be_kind_of(Test::AnotherEntities::Admin) 757 | 758 | expect(john.first_name).to eql("John") 759 | expect(john.last_name).to eql("Doe") 760 | expect(john.email).to eql("john@doe.org") 761 | expect(john).to be_kind_of(Test::AnotherEntities::User) 762 | end 763 | end 764 | 765 | context "using persistable structs" do 766 | let(:jane) { factories[:jane] } 767 | let(:john) { factories[:john] } 768 | let(:admin) { factories[:admin] } 769 | 770 | it "sets up a new builder based on another with correct struct_namespace" do 771 | expect(jane.first_name).to eql("Jane") 772 | expect(jane.email).to eql("jane@doe.org") 773 | expect(jane).to be_kind_of(Test::Entities::User) 774 | 775 | expect(jane.first_name).to eql("Jane") 776 | expect(jane.email).to eql("jane@doe.org") 777 | expect(admin.type).to eql("Admin") 778 | expect(admin).to be_kind_of(Test::AnotherEntities::Admin) 779 | 780 | expect(john.first_name).to eql("John") 781 | expect(john.last_name).to eql("Doe") 782 | expect(john.email).to eql("john@doe.org") 783 | expect(john).to be_kind_of(Test::AnotherEntities::User) 784 | end 785 | end 786 | end 787 | end 788 | 789 | context "with traits" do 790 | it "allows to define traits" do 791 | factories.define(:user) do |f| 792 | f.timestamps 793 | 794 | f.trait :jane do |t| 795 | t.first_name "Jane" 796 | t.email "jane@doe.org" 797 | end 798 | 799 | f.trait :doe do |t| 800 | t.last_name "Doe" 801 | end 802 | end 803 | 804 | jane = factories.structs[:user, :jane] 805 | 806 | expect(jane.first_name).to eql("Jane") 807 | expect(jane.last_name).to eql nil 808 | expect(jane.email).to eql("jane@doe.org") 809 | 810 | jane_doe = factories.structs[:user, :jane, :doe] 811 | 812 | expect(jane_doe.first_name).to eql("Jane") 813 | expect(jane_doe.last_name).to eql("Doe") 814 | expect(jane_doe.email).to eql("jane@doe.org") 815 | end 816 | 817 | it "allows to define nested traits" do 818 | factories.define(:user) do |f| 819 | f.timestamps 820 | 821 | f.trait :jane do |t| 822 | t.first_name "Jane" 823 | t.email "jane@doe.org" 824 | end 825 | 826 | f.trait :jane_doe, %i[jane] do |t| 827 | t.last_name "Doe" 828 | end 829 | end 830 | 831 | jane = factories.structs[:user, :jane_doe] 832 | 833 | expect(jane.first_name).to eql("Jane") 834 | expect(jane.last_name).to eql("Doe") 835 | expect(jane.email).to eql("jane@doe.org") 836 | end 837 | 838 | it "allows to define traits for persisted" do 839 | factories.define(:user) do |f| 840 | f.timestamps 841 | 842 | f.trait :jane do |t| 843 | t.first_name "Jane" 844 | t.email "jane@doe.org" 845 | end 846 | 847 | f.trait :doe do |t| 848 | t.last_name "Doe" 849 | end 850 | end 851 | 852 | jane = factories[:user, :jane, :doe] 853 | 854 | expect(jane.first_name).to eql("Jane") 855 | expect(jane.last_name).to eql("Doe") 856 | expect(jane.email).to eql("jane@doe.org") 857 | end 858 | 859 | it "allows to define traits with associations" do 860 | factories.define(:task) do |f| 861 | f.sequence(:title) { |n| "Task #{n}" } 862 | end 863 | 864 | factories.define(:user) do |f| 865 | f.timestamps 866 | 867 | f.trait :jane do |t| 868 | t.first_name "Jane" 869 | t.email "jane@doe.org" 870 | end 871 | 872 | f.trait :doe do |t| 873 | t.last_name "Doe" 874 | end 875 | 876 | f.trait :with_tasks do |t| 877 | t.association(:tasks, count: 2) 878 | end 879 | end 880 | 881 | user = factories[:user, :jane, :doe] 882 | expect(user).not_to be_respond_to(:tasks) 883 | 884 | user_with_tasks = factories[:user, :jane, :doe, :with_tasks] 885 | 886 | expect(user_with_tasks.first_name).to eql("Jane") 887 | expect(user_with_tasks.last_name).to eql("Doe") 888 | expect(user_with_tasks.email).to eql("jane@doe.org") 889 | 890 | expect(user_with_tasks.tasks.count).to be(2) 891 | 892 | t1, t2 = user_with_tasks.tasks 893 | 894 | expect(t1.user_id).to be(user_with_tasks.id) 895 | expect(t1.title).to eql("Task 1") 896 | 897 | expect(t2.user_id).to be(user_with_tasks.id) 898 | expect(t2.title).to eql("Task 2") 899 | end 900 | end 901 | 902 | context "faker" do 903 | it "exposes faker API in the DSL" do 904 | factories.define(:user) do |f| 905 | f.first_name { fake(:name) } 906 | f.last_name { fake(:name, :last_name) } 907 | f.alias { fake(:greek_philosophers, :name) } 908 | f.email { fake(:internet, :email) } 909 | f.timestamps 910 | end 911 | 912 | user = factories[:user] 913 | 914 | expect(user.id).to_not be(nil) 915 | expect(user.first_name).to_not be(nil) 916 | expect(user.last_name).to_not be(nil) 917 | expect(user.alias).to_not be(nil) 918 | expect(user.email).to_not be(nil) 919 | expect(user.created_at).to_not be(nil) 920 | expect(user.created_at).to_not be(nil) 921 | end 922 | 923 | it "uses unique when the unique option is true" do 924 | factories.define(:user) do |f| 925 | f.first_name { fake(:name) } 926 | f.last_name { fake(:name, :last_name) } 927 | f.email { fake(:internet, :email, unique: true) } 928 | f.timestamps 929 | end 930 | 931 | emails = 10.times.map { factories.structs[:user].email } 932 | expect(emails.uniq.length).to eq(emails.length) 933 | end 934 | end 935 | 936 | context "custom non integer sequence primary_key" do 937 | let(:rom) do 938 | ROM.container(:sql, conn) do |conf| 939 | conf.default.create_table(:custom_primary_keys) do 940 | column :custom_id, String 941 | column :name, String 942 | end 943 | 944 | conf.relation(:custom_primary_keys) do 945 | schema(infer: true) do 946 | attribute :custom_id, ROM::SQL::Types::String.meta(primary_key: true) 947 | end 948 | end 949 | end 950 | end 951 | 952 | before do 953 | conn.drop_table?(:custom_primary_keys) 954 | end 955 | 956 | it "doesn't assume primary_key is an integer sequence" do 957 | factories.define(:custom_primary_key) do |f| 958 | f.custom_id { fake(:color, :color_name) } 959 | f.name { fake(:name, :name) } 960 | end 961 | 962 | result = factories[:custom_primary_key] 963 | 964 | expect(result.custom_id).not_to be(nil) 965 | expect(result.custom_id).not_to be_a(Integer) 966 | expect(result.custom_id).to be_a(String) 967 | end 968 | 969 | it "doesn't assume primary_key is an integer sequence for a struct" do 970 | factories.define(:custom_primary_key) do |f| 971 | f.custom_id { fake(:color, :color_name) } 972 | f.name { fake(:name, :name) } 973 | end 974 | 975 | result = factories.structs[:custom_primary_key] 976 | 977 | expect(result.custom_id).not_to be(nil) 978 | expect(result.custom_id).not_to be_a(Integer) 979 | expect(result.custom_id).to be_a(String) 980 | end 981 | end 982 | 983 | context "with a custom output schema" do 984 | it "doesn't assume primary_key exists" do 985 | factories.define(:key_values) do |f| 986 | f.key "a_key" 987 | f.value "a_value" 988 | end 989 | 990 | result = factories[:key_values] 991 | 992 | expect(result.key).to eql("a_key") 993 | expect(result.value).to eql("a_value") 994 | end 995 | 996 | it "doesn't assume primary_key exists for a struct" do 997 | factories.define(:key_values) do |f| 998 | f.key "a_key" 999 | f.value "a_value" 1000 | end 1001 | 1002 | result = factories.structs[:key_values] 1003 | 1004 | expect(result.key).to eql("a_key") 1005 | expect(result.value).to eql("a_value") 1006 | end 1007 | end 1008 | 1009 | context "using builders within callable blocks" do 1010 | it "exposes create method in callable attribute blocks" do 1011 | factories.define(:user) do |f| 1012 | f.first_name "Jane" 1013 | f.last_name "Doe" 1014 | f.email "jane@doe.org" 1015 | f.timestamps 1016 | end 1017 | 1018 | factories.define(:task) do |f| 1019 | f.title "A task" 1020 | f.user_id { create(:user).id } 1021 | end 1022 | 1023 | task = factories[:task] 1024 | 1025 | expect(task.title).to eql("A task") 1026 | expect(task.user_id).to_not be(nil) 1027 | end 1028 | end 1029 | 1030 | context "using associations" do 1031 | context "with traits" do 1032 | context "sequential" do 1033 | before do 1034 | factories.define(:user) do |f| 1035 | f.first_name "Jane" 1036 | f.last_name "Doe" 1037 | f.email "jane@doe.org" 1038 | f.timestamps 1039 | f.association(:tasks, :important, count: 2) 1040 | end 1041 | 1042 | factories.define(:task) do |f| 1043 | f.sequence(:title) { |n| "Task #{n}" } 1044 | f.trait :important do |t| 1045 | t.sequence(:title) { |n| "Important Task #{n}" } 1046 | end 1047 | end 1048 | end 1049 | 1050 | it "creates associated records with the given trait" do 1051 | user = factories[:user] 1052 | 1053 | expect(user.tasks.count).to be(2) 1054 | 1055 | t1, t2 = user.tasks 1056 | 1057 | expect(t1.user_id).to be(user.id) 1058 | expect(t1.title).to eql("Important Task 1") 1059 | 1060 | expect(t2.user_id).to be(user.id) 1061 | expect(t2.title).to eql("Important Task 2") 1062 | end 1063 | end 1064 | 1065 | context "keyword" do 1066 | before do 1067 | factories.define(:user) do |f| 1068 | f.first_name "Jane" 1069 | f.last_name "Doe" 1070 | f.email "jane@doe.org" 1071 | f.timestamps 1072 | f.association(:tasks, count: 2, traits: [:important]) 1073 | end 1074 | 1075 | factories.define(:task) do |f| 1076 | f.sequence(:title) { |n| "Task #{n}" } 1077 | f.trait :important do |t| 1078 | t.sequence(:title) { |n| "Important Task #{n}" } 1079 | end 1080 | end 1081 | end 1082 | 1083 | it "creates associated records with the given trait" do 1084 | user = factories[:user] 1085 | expect(user.tasks.count).to be(2) 1086 | end 1087 | end 1088 | end 1089 | 1090 | context "has_many" do 1091 | context "when count is > 0" do 1092 | before do 1093 | factories.define(:user) do |f| 1094 | f.first_name "Jane" 1095 | f.last_name "Doe" 1096 | f.email "jane@doe.org" 1097 | f.timestamps 1098 | f.association(:tasks, count: 2) 1099 | end 1100 | 1101 | factories.define(:task) do |f| 1102 | f.sequence(:title) { |n| "Task #{n}" } 1103 | end 1104 | end 1105 | 1106 | it "creates associated records" do 1107 | user = factories[:user] 1108 | 1109 | expect(user.tasks.count).to be(2) 1110 | 1111 | t1, t2 = user.tasks 1112 | 1113 | expect(t1.user_id).to be(user.id) 1114 | expect(t1.title).to eql("Task 1") 1115 | 1116 | expect(t2.user_id).to be(user.id) 1117 | expect(t2.title).to eql("Task 2") 1118 | end 1119 | end 1120 | 1121 | context "when count is 0" do 1122 | before do 1123 | factories.define(:user) do |f| 1124 | f.first_name "Jane" 1125 | f.last_name "Doe" 1126 | f.email "jane@doe.org" 1127 | f.timestamps 1128 | f.association(:tasks, count: 0) 1129 | end 1130 | 1131 | factories.define(:task) do |f| 1132 | f.sequence(:title) { |n| "Task #{n}" } 1133 | end 1134 | end 1135 | 1136 | it "doesn't build associated records" do 1137 | user = factories.structs[:user] 1138 | 1139 | expect(user.tasks).to be_empty 1140 | end 1141 | 1142 | it "doesn't create associated records" do 1143 | user = factories[:user] 1144 | 1145 | expect(user.tasks).to be_empty 1146 | end 1147 | end 1148 | end 1149 | 1150 | context "has_many-through" do 1151 | context "when count is > 0" do 1152 | before do 1153 | factories.define(:user) do |f| 1154 | f.first_name "Jane" 1155 | f.last_name "Doe" 1156 | f.email "jane@doe.org" 1157 | f.timestamps 1158 | f.association(:addresses, count: 2) 1159 | end 1160 | 1161 | factories.define(:user_address) do |f| 1162 | f.association(:user) 1163 | f.association(:address) 1164 | f.timestamps 1165 | end 1166 | 1167 | factories.define(:address) do |f| 1168 | f.sequence(:full_address) { |n| "Address #{n}" } 1169 | end 1170 | end 1171 | 1172 | it "creates associated records" do 1173 | user = factories[:user] 1174 | 1175 | expect(user.addresses.count).to be(2) 1176 | 1177 | a1, a2 = user.addresses 1178 | 1179 | expect(a1.user_id).to be(user.id) 1180 | expect(a1.full_address).to eql("Address 1") 1181 | 1182 | expect(a2.user_id).to be(user.id) 1183 | expect(a2.full_address).to eql("Address 2") 1184 | end 1185 | end 1186 | 1187 | context "when count is 0" do 1188 | before do 1189 | factories.define(:user) do |f| 1190 | f.first_name "Jane" 1191 | f.last_name "Doe" 1192 | f.email "jane@doe.org" 1193 | f.timestamps 1194 | f.association(:addresses, count: 0) 1195 | end 1196 | 1197 | factories.define(:address) do |f| 1198 | f.sequence(:full_address) { |n| "Address #{n}" } 1199 | end 1200 | end 1201 | 1202 | it "doesn't build associated records" do 1203 | user = factories.structs[:user] 1204 | 1205 | expect(user.addresses).to be_empty 1206 | end 1207 | 1208 | it "doesn't create associated records" do 1209 | user = factories[:user] 1210 | 1211 | expect(user.addresses).to be_empty 1212 | end 1213 | end 1214 | end 1215 | 1216 | context "belongs_to" do 1217 | before do 1218 | factories.define(:user) do |f| 1219 | f.first_name "Jane" 1220 | f.last_name "Doe" 1221 | f.email "jane@doe.org" 1222 | f.timestamps 1223 | end 1224 | 1225 | factories.define(:task) do |f| 1226 | f.title "A task" 1227 | f.association(:user) 1228 | end 1229 | end 1230 | 1231 | it "exposes create method in callable attribute blocks" do 1232 | task = factories[:task] 1233 | 1234 | expect(task.title).to eql("A task") 1235 | expect(task.user_id).to_not be(nil) 1236 | end 1237 | 1238 | it "allows overrides" do 1239 | user = factories[:user, first_name: "Joe"] 1240 | task = factories[:task, user: user] 1241 | 1242 | expect(task.title).to eql("A task") 1243 | expect(task.user_id).to be(user.id) 1244 | 1245 | expect(rom.relations[:users].count).to be(1) 1246 | expect(rom.relations[:tasks].count).to be(1) 1247 | end 1248 | 1249 | it "works with structs" do 1250 | user = factories.structs[:user, first_name: "Joe"] 1251 | task = factories.structs[:task, user: user] 1252 | 1253 | expect(user.first_name).to eql("Joe") 1254 | expect(task.title).to eql("A task") 1255 | expect(task.user_id).to be(user.id) 1256 | 1257 | expect(rom.relations[:users].count).to be(0) 1258 | expect(rom.relations[:tasks].count).to be(0) 1259 | end 1260 | 1261 | it "respects FK" do 1262 | task = factories.structs[:task, user_id: 312] 1263 | 1264 | expect(task.user_id).to be(312) 1265 | expect(task.user.id).to be(312) 1266 | end 1267 | 1268 | it "raises UnknownFactoryAttributes when unknown attributes are used" do 1269 | expect { factories.structs[:user, name: "Joe"] } 1270 | .to raise_error(ROM::Factory::UnknownFactoryAttributes, /name/) 1271 | end 1272 | end 1273 | end 1274 | 1275 | context "without PK" do 1276 | let(:rom) do 1277 | ROM.container(:sql, conn) do |conf| 1278 | conf.default.create_table(:dummies) do 1279 | column :id, Integer, default: 1 1280 | column :name, String, null: false 1281 | end 1282 | 1283 | conf.relation(:dummies) do 1284 | schema(infer: true) do 1285 | attribute :id, ROM::SQL::Types::Serial 1286 | end 1287 | end 1288 | end 1289 | end 1290 | 1291 | before do 1292 | conn.drop_table?(:dummies) 1293 | end 1294 | 1295 | it "works even if the table does not have a PK" do 1296 | factories.define(:dummy) do |f| 1297 | f.name "Jane" 1298 | end 1299 | 1300 | result = factories[:dummy] 1301 | 1302 | expect(result.id).to be(1) 1303 | expect(result.name).to eql("Jane") 1304 | end 1305 | end 1306 | 1307 | context "factory without custom struct namespace" do 1308 | context "with builder without custom struct namespace" do 1309 | before do 1310 | factories.define(:user) do |f| 1311 | f.first_name "Jane" 1312 | f.last_name "Doe" 1313 | f.email "jane@doe.org" 1314 | f.timestamps 1315 | end 1316 | end 1317 | 1318 | context "using in-memory structs" do 1319 | it "returns an instance of a default struct" do 1320 | result = factories.structs[:user] 1321 | 1322 | expect(result).to be_kind_of(ROM::Struct::User) 1323 | 1324 | expect(result.id).to be(1) 1325 | expect(result.first_name).to eql("Jane") 1326 | expect(result.last_name).to eql("Doe") 1327 | expect(result.email).to eql("jane@doe.org") 1328 | expect(result.created_at).to_not be(nil) 1329 | expect(result.updated_at).to_not be(nil) 1330 | end 1331 | end 1332 | 1333 | context "using persistable structs" do 1334 | it "returns an instance of a default struct" do 1335 | result = factories[:user] 1336 | 1337 | expect(result).to be_kind_of(ROM::Struct::User) 1338 | 1339 | expect(result.id).to be(1) 1340 | expect(result.first_name).to eql("Jane") 1341 | expect(result.last_name).to eql("Doe") 1342 | expect(result.email).to eql("jane@doe.org") 1343 | expect(result.created_at).to_not be(nil) 1344 | expect(result.updated_at).to_not be(nil) 1345 | end 1346 | end 1347 | end 1348 | 1349 | context "with builder with custom struct namespace" do 1350 | before do 1351 | module Test 1352 | module Entities 1353 | class User < ROM::Struct 1354 | end 1355 | end 1356 | end 1357 | 1358 | factories.define(:user, struct_namespace: Test::Entities) do |f| 1359 | f.first_name "Jane" 1360 | f.last_name "Doe" 1361 | f.email "jane@doe.org" 1362 | f.timestamps 1363 | end 1364 | end 1365 | 1366 | context "using in-memory structs" do 1367 | it "returns an instance of a custom struct" do 1368 | result = factories.structs[:user] 1369 | 1370 | expect(result).to be_kind_of(Test::Entities::User) 1371 | 1372 | expect(result.id).to be(1) 1373 | expect(result.first_name).to eql("Jane") 1374 | expect(result.last_name).to eql("Doe") 1375 | expect(result.email).to eql("jane@doe.org") 1376 | expect(result.created_at).to_not be(nil) 1377 | expect(result.updated_at).to_not be(nil) 1378 | end 1379 | end 1380 | 1381 | context "using persistable structs" do 1382 | it "returns an instance of a custom struct" do 1383 | result = factories[:user] 1384 | 1385 | expect(result).to be_kind_of(Test::Entities::User) 1386 | 1387 | expect(result.id).to be(1) 1388 | expect(result.first_name).to eql("Jane") 1389 | expect(result.last_name).to eql("Doe") 1390 | expect(result.email).to eql("jane@doe.org") 1391 | expect(result.created_at).to_not be(nil) 1392 | expect(result.updated_at).to_not be(nil) 1393 | end 1394 | end 1395 | end 1396 | end 1397 | 1398 | context "factory with custom struct namespace" do 1399 | context "with builder without custom struct namespace" do 1400 | let(:entities) { factories.struct_namespace(Test::Entities) } 1401 | 1402 | before do 1403 | module Test 1404 | module Entities 1405 | class User < ROM::Struct 1406 | end 1407 | end 1408 | end 1409 | 1410 | factories.define(:user) do |f| 1411 | f.first_name "Jane" 1412 | f.last_name "Doe" 1413 | f.email "jane@doe.org" 1414 | f.timestamps 1415 | end 1416 | end 1417 | 1418 | context "using in-memory structs" do 1419 | it "returns an instance of a custom struct" do 1420 | result = entities.structs[:user] 1421 | 1422 | expect(result).to be_kind_of(Test::Entities::User) 1423 | 1424 | expect(result.id).to be(1) 1425 | expect(result.first_name).to eql("Jane") 1426 | expect(result.last_name).to eql("Doe") 1427 | expect(result.email).to eql("jane@doe.org") 1428 | expect(result.created_at).to_not be(nil) 1429 | expect(result.updated_at).to_not be(nil) 1430 | end 1431 | end 1432 | 1433 | context "using persistable structs" do 1434 | it "returns an instance of a custom struct" do 1435 | result = entities[:user] 1436 | 1437 | expect(result).to be_kind_of(Test::Entities::User) 1438 | 1439 | expect(result.id).to be(1) 1440 | expect(result.first_name).to eql("Jane") 1441 | expect(result.last_name).to eql("Doe") 1442 | expect(result.email).to eql("jane@doe.org") 1443 | expect(result.created_at).to_not be(nil) 1444 | expect(result.updated_at).to_not be(nil) 1445 | end 1446 | end 1447 | end 1448 | 1449 | context "with builder with custom struct namespace" do 1450 | let(:entities) { factories.struct_namespace(Test::Entities) } 1451 | 1452 | before do 1453 | module Test 1454 | module Entities 1455 | class User < ROM::Struct 1456 | end 1457 | end 1458 | 1459 | module AnotherEntities 1460 | class User < ROM::Struct 1461 | end 1462 | end 1463 | end 1464 | 1465 | factories.define(:user, struct_namespace: Test::AnotherEntities) do |f| 1466 | f.first_name "Jane" 1467 | f.last_name "Doe" 1468 | f.email "jane@doe.org" 1469 | f.timestamps 1470 | end 1471 | end 1472 | 1473 | context "using in-memory structs" do 1474 | it "returns an instance of a custom struct" do 1475 | result = entities.structs[:user] 1476 | 1477 | expect(result).to be_kind_of(Test::AnotherEntities::User) 1478 | 1479 | expect(result.id).to be(1) 1480 | expect(result.first_name).to eql("Jane") 1481 | expect(result.last_name).to eql("Doe") 1482 | expect(result.email).to eql("jane@doe.org") 1483 | expect(result.created_at).to_not be(nil) 1484 | expect(result.updated_at).to_not be(nil) 1485 | end 1486 | end 1487 | 1488 | context "using persistable structs" do 1489 | it "returns an instance of a custom struct" do 1490 | result = entities[:user] 1491 | 1492 | expect(result).to be_kind_of(Test::AnotherEntities::User) 1493 | 1494 | expect(result.id).to be(1) 1495 | expect(result.first_name).to eql("Jane") 1496 | expect(result.last_name).to eql("Doe") 1497 | expect(result.email).to eql("jane@doe.org") 1498 | expect(result.created_at).to_not be(nil) 1499 | expect(result.updated_at).to_not be(nil) 1500 | end 1501 | end 1502 | end 1503 | end 1504 | 1505 | describe "using read types with one-to-many" do 1506 | before do 1507 | conf.relation(:capitalized_tasks) do 1508 | schema(:tasks, infer: true) do 1509 | attribute :title, ROM::SQL::Types::String.meta( 1510 | read: ROM::SQL::Types::String.constructor(&:upcase) 1511 | ) 1512 | 1513 | associations do 1514 | belongs_to :user 1515 | end 1516 | end 1517 | end 1518 | end 1519 | 1520 | specify do 1521 | factories.define(:capitalized_task) do |f| 1522 | f.title "A task" 1523 | f.association(:user) 1524 | end 1525 | 1526 | factories.define(:user) do |f| 1527 | f.first_name "Janis" 1528 | f.last_name "Miezitis" 1529 | f.email "janjiss@gmail.com" 1530 | f.timestamps 1531 | end 1532 | 1533 | task = factories.structs[:capitalized_task] 1534 | 1535 | expect(task.title).to eql("A TASK") 1536 | expect(task.user.first_name).to eql("Janis") 1537 | end 1538 | end 1539 | 1540 | describe "fake options" do 1541 | specify do 1542 | factories.define(:user) do |f| 1543 | f.first_name "Jane" 1544 | f.age { fake(:number, :within, range: 0..150) } 1545 | end 1546 | 1547 | user = factories.structs[:user] 1548 | expect(user.age).to be_between(0, 150) 1549 | end 1550 | end 1551 | 1552 | describe "using UUID as PKs" do 1553 | context "many-to-one" do 1554 | let(:rom) do 1555 | %i[jobs workers].each { |table| conn.drop_table?(table) } 1556 | 1557 | ROM.container(:sql, conn) do |conf| 1558 | conf.default.create_table(:workers) do 1559 | column :id, :uuid, primary_key: true 1560 | column :name, String 1561 | end 1562 | 1563 | conf.default.create_table(:jobs) do 1564 | column :id, :uuid, primary_key: true 1565 | foreign_key :worker_id, :workers, type: :uuid 1566 | column :name, String 1567 | end 1568 | 1569 | conf.relation(:workers) do 1570 | schema(infer: true) do 1571 | associations do 1572 | has_many :jobs 1573 | end 1574 | end 1575 | end 1576 | 1577 | conf.relation(:jobs) do 1578 | schema(infer: true) do 1579 | attribute :worker_id, ROM::SQL::Types.ForeignKey(:workers, ROM::SQL::Types::String) 1580 | 1581 | associations do 1582 | belongs_to :worker 1583 | end 1584 | end 1585 | end 1586 | end 1587 | end 1588 | 1589 | before do 1590 | factories.define(:worker) do |f| 1591 | f.id { fake(:internet, :uuid) } 1592 | f.name "Test Worker" 1593 | 1594 | f.association(:jobs, count: 0) 1595 | 1596 | f.trait(:with_jobs) do |t| 1597 | t.association(:jobs, count: 2) 1598 | end 1599 | end 1600 | 1601 | factories.define(:job) do |f| 1602 | f.id { fake(:internet, :uuid) } 1603 | f.name "Test Job" 1604 | 1605 | f.association :worker 1606 | end 1607 | end 1608 | 1609 | it "persists a parent struct" do 1610 | worker = factories[:worker] 1611 | 1612 | expect(worker.id).to_not be(nil) 1613 | expect(worker.name).to eql("Test Worker") 1614 | end 1615 | 1616 | it "persists a parent struct with its children" do 1617 | worker = factories[:worker, :with_jobs] 1618 | 1619 | expect(worker.id).to_not be(nil) 1620 | expect(worker.name).to eql("Test Worker") 1621 | expect(worker.jobs.size).to be(2) 1622 | end 1623 | end 1624 | end 1625 | end 1626 | -------------------------------------------------------------------------------- /spec/shared/database.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rom-core" 4 | 5 | RSpec.shared_context "database" do 6 | let(:conf) do 7 | ROM::Configuration.new(:sql, DB_URI) 8 | end 9 | 10 | let(:rom) do 11 | ROM.container(conf) 12 | end 13 | 14 | let(:conn) do 15 | conf.gateways[:default].connection 16 | end 17 | 18 | after do 19 | conn.disconnect 20 | end 21 | 22 | let(:relations) do 23 | rom.relations 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/shared/relations.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.shared_context "relations" do 4 | include_context "database" 5 | 6 | before do 7 | conn.create_table?(:users) do 8 | primary_key :id 9 | column :last_name, String, null: false 10 | column :first_name, String, null: false 11 | column :alias, String, null: true 12 | column :email, String, null: false 13 | column :created_at, Time, null: false 14 | column :updated_at, Time, null: false 15 | column :age, Integer 16 | column :type, String 17 | end 18 | 19 | conn.create_table?(:tasks) do 20 | primary_key :id 21 | foreign_key :user_id, :users 22 | foreign_key :task_id, :tasks, null: true 23 | column :title, String, null: false 24 | end 25 | 26 | conn.create_table?(:addresses) do 27 | primary_key :id 28 | column :full_address, String, null: false 29 | end 30 | 31 | conn.create_table?(:user_addresses) do 32 | primary_key :id 33 | foreign_key :user_id, :users, on_delete: :cascade 34 | foreign_key :address_id, :addresses, on_delete: :cascade 35 | column :created_at, Time, null: false 36 | column :updated_at, Time, null: false 37 | end 38 | 39 | conn.create_table?(:key_values) do 40 | column :key, String 41 | column :value, String 42 | end 43 | 44 | conf.relation(:tasks) do 45 | schema(infer: true) do 46 | associations do 47 | belongs_to :user 48 | belongs_to :user, as: :author 49 | belongs_to :task, as: :parent 50 | end 51 | end 52 | end 53 | 54 | conf.relation(:users) do 55 | schema(infer: true) do 56 | associations do 57 | has_many :tasks 58 | has_one :user_addresses 59 | has_one :address, through: :user_addresses 60 | has_many :addresses, through: :user_addresses 61 | end 62 | end 63 | end 64 | 65 | conf.relation(:addresses) do 66 | schema(infer: true) do 67 | associations do 68 | has_one :user_addresses 69 | has_one :user, through: :user_addresses 70 | has_one :users, through: :user_addresses 71 | end 72 | end 73 | end 74 | 75 | conf.relation(:user_addresses) do 76 | schema(infer: true) do 77 | associations do 78 | belongs_to :user 79 | belongs_to :address 80 | end 81 | end 82 | end 83 | 84 | conf.relation(:admins) do 85 | dataset { where(type: "Admin") } 86 | 87 | schema(:users, as: :admins, infer: true) do 88 | associations do 89 | has_many :tasks 90 | end 91 | end 92 | end 93 | 94 | conf.relation(:key_values) do 95 | option :output_schema, default: -> do 96 | ROM::Types::Hash.schema( 97 | schema.map { |attr| [attr.key, attr.to_read_type] }.to_h 98 | ).with_key_transform(&:to_sym) 99 | end 100 | 101 | schema(infer: true) {} 102 | end 103 | end 104 | 105 | after do 106 | conn.drop_table(:user_addresses) 107 | conn.drop_table(:addresses) 108 | conn.drop_table(:tasks) 109 | conn.drop_table(:users) 110 | conn.drop_table(:key_values) 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "support/coverage" 4 | require "dotenv" 5 | Dotenv.load(".postgres.env", ".env") 6 | 7 | require "pathname" 8 | SPEC_ROOT = root = Pathname(__FILE__).dirname 9 | 10 | require "rom-factory" 11 | 12 | %w[debug byebug pry].each do |debugger| 13 | require debugger 14 | rescue LoadError 15 | # ignore 16 | else 17 | break 18 | end 19 | 20 | require "rspec" 21 | 22 | Dir[root.join("support/*.rb").to_s].each do |f| 23 | require f 24 | end 25 | 26 | Dir[root.join("shared/*.rb").to_s].each do |f| 27 | require f 28 | end 29 | 30 | DB_URI = ENV.fetch("DATABASE_URL") do 31 | auth = ENV.values_at("POSTGRES_USER", "POSTGRES_PASSWORD").join(":") 32 | address = `docker compose port db 5432 2> /dev/null`.strip 33 | address = [auth, address].join("@") if address 34 | 35 | address ||= "localhost" 36 | 37 | if defined? JRUBY_VERSION 38 | "jdbc:postgresql://#{address}" 39 | else 40 | "postgres://#{address}" 41 | end 42 | end 43 | 44 | module SileneceWarnings 45 | def warn(str) 46 | if str["/sequel/"] || str["/rspec-core"] 47 | nil 48 | else 49 | super 50 | end 51 | end 52 | end 53 | 54 | module Helpers 55 | def attribute(type, *args, **kwargs) 56 | ROM::Factory::Attributes.const_get(type).new(*args, **kwargs) 57 | end 58 | 59 | def value(name, *args) 60 | attribute(:Value, name, *args) 61 | end 62 | 63 | def sequence(name, &) 64 | attribute(:Sequence, name, &) 65 | end 66 | 67 | def callable(name, *args, &block) 68 | attribute(:Callable, name, *args, nil, block) 69 | end 70 | end 71 | 72 | Warning.extend(SileneceWarnings) 73 | 74 | RSpec.configure do |config| 75 | config.disable_monkey_patching! 76 | config.warnings = true 77 | config.include(Helpers) 78 | config.before { ROM::Factory::Sequences.instance.reset } 79 | config.filter_run_when_matching :focus 80 | end 81 | -------------------------------------------------------------------------------- /spec/support/coverage.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # this file is managed by rom-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 rom-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/unit/rom/factory/attribute_registry_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rom/factory/attribute_registry" 4 | 5 | RSpec.describe ROM::Factory::AttributeRegistry do 6 | subject(:registry) { ROM::Factory::AttributeRegistry.new(elements) } 7 | 8 | let(:elements) do 9 | [email_attr, id_attr, name_attr] 10 | end 11 | 12 | let(:name_attr) do 13 | value(:name, "Jane") 14 | end 15 | 16 | let(:email_attr) do 17 | callable(:email) { |name| "#{name}@rom-rb.org" } 18 | end 19 | 20 | let(:id_attr) do 21 | sequence(:id) { |n| n } 22 | end 23 | 24 | describe "#tsort" do 25 | it "sorts attributes by their dependencies" do 26 | expect(registry.tsort).to eql([name_attr, email_attr, id_attr]) 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/unit/rom/factory/builder_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rom/factory/builder" 4 | 5 | RSpec.describe ROM::Factory::Builder do 6 | subject(:builder) do 7 | ROM::Factory::Builder.new(ROM::Factory::AttributeRegistry.new(attributes), 8 | relation: relation, 9 | struct_namespace: {namespace: ROM::Struct, overridable: true}).persistable 10 | end 11 | 12 | include_context "database" 13 | 14 | let(:factories) do 15 | ROM::Factory.configure do |config| 16 | config.rom = rom 17 | end 18 | end 19 | 20 | describe "dependant attributes" do 21 | let(:attributes) do 22 | [callable(:email) { |name| "#{name.downcase}@rom-rb.org" }, 23 | value(:name, "Jane")] 24 | end 25 | 26 | let(:relation) { relations[:users] } 27 | 28 | before do 29 | conn.create_table(:users) do 30 | primary_key :id 31 | column :name, String 32 | column :email, String 33 | end 34 | 35 | conf.relation(:users) do 36 | schema(infer: true) 37 | end 38 | end 39 | 40 | after do 41 | conn.drop_table(:users) 42 | end 43 | 44 | it "evaluates attributes in correct order" do 45 | user = builder.create 46 | 47 | expect(user.name).to eql("Jane") 48 | expect(user.email).to eql("jane@rom-rb.org") 49 | end 50 | end 51 | 52 | describe "belongs_to association" do 53 | let(:attributes) do 54 | [attribute(:Value, :title, "To-do"), 55 | attribute(:Association, tasks.associations[:user], -> { factories.registry[:user] })] 56 | end 57 | 58 | let(:tasks) { relations[:tasks] } 59 | let(:users) { relations[:users] } 60 | let(:relation) { tasks } 61 | 62 | before do 63 | conn.create_table(:users) do 64 | primary_key :id 65 | column :name, String 66 | end 67 | 68 | conn.create_table(:tasks) do 69 | primary_key :id 70 | foreign_key :user_id, :users 71 | column :title, String, null: false 72 | end 73 | 74 | conf.relation(:tasks) do 75 | schema(infer: true) do 76 | associations do 77 | belongs_to :user 78 | end 79 | end 80 | end 81 | 82 | conf.relation(:users) do 83 | schema(infer: true) 84 | end 85 | 86 | factories.define(:user) do |f| 87 | f.name "Jane" 88 | end 89 | end 90 | 91 | after do 92 | conn.drop_table(:tasks) 93 | conn.drop_table(:users) 94 | end 95 | 96 | describe "#create" do 97 | it "builds associated struct" do 98 | task = builder.create 99 | 100 | expect(task.title).to eql("To-do") 101 | expect(task.user_id).to be(task.user.id) 102 | expect(task.user.name).to eql("Jane") 103 | end 104 | 105 | it "sets existing parent and fills in FK" do 106 | user = users.command(:create).call(id: 312, name: "Jade") 107 | task = builder.create(user: user) 108 | 109 | expect(task.title).to eql("To-do") 110 | expect(task.user_id).to eql(user[:id]) 111 | expect(task.user.name).to eql(user[:name]) 112 | 113 | expect(users.count).to be(1) 114 | end 115 | 116 | it "respects existing data" do 117 | user = users.command(:create).call(id: 312, name: "Jade") 118 | task = builder.create(user: user, user_id: 312) 119 | 120 | expect(task.title).to eql("To-do") 121 | expect(task.user_id).to eql(user[:id]) 122 | expect(task.user.name).to eql(user[:name]) 123 | 124 | expect(users.count).to be(1) 125 | end 126 | end 127 | end 128 | 129 | describe "belongs_to association with composite pk" do 130 | let(:attributes) do 131 | [attribute(:Association, users_tasks.associations[:user], -> { factories.registry[:user] }), 132 | attribute(:Association, users_tasks.associations[:task], -> { factories.registry[:task] })] 133 | end 134 | 135 | let(:tasks) { relations[:tasks] } 136 | let(:users) { relations[:users] } 137 | let(:users_tasks) { relations[:users_tasks] } 138 | let(:relation) { users_tasks } 139 | 140 | before do 141 | conn.create_table(:users) do 142 | primary_key :id 143 | column :name, String 144 | end 145 | 146 | conn.create_table(:tasks) do 147 | primary_key :id 148 | column :title, String, null: false 149 | end 150 | 151 | conn.create_table(:users_tasks) do 152 | primary_key [:user_id, :task_id] 153 | foreign_key :user_id, :users, null: false 154 | foreign_key :task_id, :tasks, null: false 155 | end 156 | 157 | conf.relation(:users) do 158 | schema(infer: true) do 159 | associations do 160 | has_many :users, through: :users_tasks 161 | end 162 | end 163 | end 164 | 165 | conf.relation(:tasks) do 166 | schema(infer: true) do 167 | associations do 168 | has_many :users, through: :users_tasks 169 | end 170 | end 171 | end 172 | 173 | conf.relation(:users_tasks) do 174 | schema(infer: true) do 175 | associations do 176 | belongs_to :user 177 | belongs_to :task 178 | end 179 | end 180 | end 181 | 182 | factories.define(:user) do |f| 183 | f.name "Jane" 184 | end 185 | 186 | factories.define(:task) do |f| 187 | f.title "To-do" 188 | end 189 | end 190 | 191 | after do 192 | conn.drop_table(:users_tasks) 193 | conn.drop_table(:tasks) 194 | conn.drop_table(:users) 195 | end 196 | 197 | describe "#create" do 198 | it "builds associated structs" do 199 | user_task = builder.create 200 | 201 | expect(user_task.user.name).to eql("Jane") 202 | expect(user_task.task.title).to eql("To-do") 203 | end 204 | end 205 | end 206 | 207 | describe "has_many association" do 208 | let(:attributes) do 209 | [attribute(:Value, :name, "Jane"), 210 | attribute(:Association, users.associations[:tasks], -> { factories.registry[:task] }, count: 2)] 211 | end 212 | 213 | let(:tasks) { relations[:tasks] } 214 | let(:users) { relations[:users] } 215 | let(:relation) { users } 216 | 217 | before do 218 | conn.create_table(:users) do 219 | primary_key :id 220 | column :name, String 221 | end 222 | 223 | conn.create_table(:tasks) do 224 | primary_key :id 225 | foreign_key :user_id, :users, null: false 226 | column :title, String, null: false 227 | end 228 | 229 | conf.relation(:tasks) do 230 | schema(infer: true) do 231 | associations do 232 | belongs_to :user 233 | end 234 | end 235 | end 236 | 237 | conf.relation(:users) do 238 | schema(infer: true) do 239 | associations do 240 | has_many :tasks 241 | end 242 | end 243 | end 244 | 245 | factories.define(:task) do |f| 246 | f.sequence(:title) { |n| "Task #{n}" } 247 | end 248 | end 249 | 250 | after do 251 | conn.drop_table(:tasks) 252 | conn.drop_table(:users) 253 | end 254 | 255 | describe "#create" do 256 | it "builds associated structs" do 257 | user = builder.create 258 | 259 | expect(user.name).to eql("Jane") 260 | expect(user.tasks.size).to be(2) 261 | 262 | t1, t2 = user.tasks 263 | 264 | expect(t1.title).to eql("Task 1") 265 | expect(t1.user_id).to be(user.id) 266 | 267 | expect(t2.title).to eql("Task 2") 268 | expect(t2.user_id).to be(user.id) 269 | end 270 | end 271 | end 272 | 273 | describe "has_one association" do 274 | let(:attributes) do 275 | [attribute(:Value, :name, "Jane"), 276 | attribute(:Association, users.associations[:tasks], -> { factories.registry[:task] })] 277 | end 278 | 279 | let(:tasks) { relations[:tasks] } 280 | let(:users) { relations[:users] } 281 | let(:relation) { users } 282 | 283 | before do 284 | conn.create_table(:users) do 285 | primary_key :id 286 | column :name, String 287 | end 288 | 289 | conn.create_table(:tasks) do 290 | primary_key :id 291 | foreign_key :user_id, :users, null: false 292 | column :title, String, null: false 293 | end 294 | 295 | conf.relation(:tasks) do 296 | schema(infer: true) do 297 | associations do 298 | belongs_to :user 299 | end 300 | end 301 | end 302 | 303 | conf.relation(:users) do 304 | schema(infer: true) do 305 | associations do 306 | has_one :task 307 | end 308 | end 309 | end 310 | 311 | factories.define(:task) do |f| 312 | f.title "To-do" 313 | end 314 | end 315 | 316 | after do 317 | conn.drop_table(:tasks) 318 | conn.drop_table(:users) 319 | end 320 | 321 | describe "#create" do 322 | it "builds associated structs" do 323 | user = builder.create 324 | 325 | expect(user.name).to eql("Jane") 326 | expect(user.task.title).to eql("To-do") 327 | expect(user.task.user_id).to be(user.id) 328 | end 329 | end 330 | end 331 | end 332 | --------------------------------------------------------------------------------