rom-factory 0.10.2 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.devtools/templates/changelog.erb +3 -0
  3. data/.devtools/templates/release.erb +36 -0
  4. data/.github/FUNDING.yml +1 -0
  5. data/.github/ISSUE_TEMPLATE/{---bug-report.md → bug-report.md} +6 -10
  6. data/.github/ISSUE_TEMPLATE/config.yml +5 -0
  7. data/.github/SUPPORT.md +3 -0
  8. data/.github/workflows/ci.yml +17 -25
  9. data/.github/workflows/docsite.yml +6 -6
  10. data/.github/workflows/rubocop.yml +46 -0
  11. data/.github/workflows/sync_configs.yml +7 -13
  12. data/.repobot.yml +24 -0
  13. data/.rubocop.yml +135 -16
  14. data/CHANGELOG.md +64 -0
  15. data/CODEOWNERS +1 -0
  16. data/CONTRIBUTING.md +3 -3
  17. data/Gemfile +20 -19
  18. data/Gemfile.devtools +6 -5
  19. data/LICENSE +1 -1
  20. data/README.md +3 -3
  21. data/Rakefile +1 -1
  22. data/benchmarks/basic.rb +10 -10
  23. data/changelog.yml +35 -0
  24. data/docsite/source/index.html.md +162 -1
  25. data/lib/rom/factory/attribute_registry.rb +6 -2
  26. data/lib/rom/factory/attributes/association.rb +127 -29
  27. data/lib/rom/factory/attributes/callable.rb +1 -1
  28. data/lib/rom/factory/attributes/value.rb +1 -1
  29. data/lib/rom/factory/attributes.rb +4 -6
  30. data/lib/rom/factory/builder/persistable.rb +21 -6
  31. data/lib/rom/factory/builder.rb +17 -19
  32. data/lib/rom/factory/constants.rb +1 -1
  33. data/lib/rom/factory/dsl.rb +43 -25
  34. data/lib/rom/factory/factories.rb +13 -15
  35. data/lib/rom/factory/registry.rb +2 -2
  36. data/lib/rom/factory/sequences.rb +4 -3
  37. data/lib/rom/factory/tuple_evaluator.rb +92 -43
  38. data/lib/rom/factory/version.rb +1 -1
  39. data/lib/rom/factory.rb +8 -5
  40. data/lib/rom-factory.rb +1 -1
  41. data/project.yml +1 -0
  42. data/rom-factory.gemspec +9 -10
  43. metadata +40 -26
  44. data/.github/ISSUE_TEMPLATE/----please-don-t-ask-for-support-via-issues.md +0 -10
  45. data/.github/ISSUE_TEMPLATE/----please-don-t-report-feature-requests-via-issues.md +0 -10
  46. data/.github/ISSUE_TEMPLATE/---a-detailed-bug-report.md +0 -30
  47. data/.github/ISSUE_TEMPLATE/---feature-request.md +0 -18
  48. data/.github/workflows/custom/ci.yml +0 -26
  49. data/Appraisals +0 -9
  50. data/LICENSE.txt +0 -21
data/Gemfile CHANGED
@@ -1,38 +1,39 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- source 'https://rubygems.org'
3
+ source "https://rubygems.org"
4
4
 
5
5
  gemspec
6
6
 
7
- eval_gemfile 'Gemfile.devtools'
7
+ eval_gemfile "Gemfile.devtools"
8
8
 
9
- gem 'faker', "~> #{ENV['FAKER'].eql?('faker-1') ? '1.7' : '2.8'}"
9
+ gem "faker", "~> 3.0"
10
10
 
11
- gem 'rake', '~> 12.0'
12
- gem 'rspec', '~> 3.0'
11
+ gem "rspec", "~> 3.0"
13
12
 
14
- gem 'rom', github: 'rom-rb/rom', branch: 'master' do
15
- gem 'rom-core'
13
+ git "https://github.com/rom-rb/rom.git", branch: "release-5.3" do
14
+ gem "rom-core"
15
+ gem "rom-changeset"
16
+ gem "rom-repository"
17
+ gem "rom"
16
18
  end
17
19
 
18
20
  group :test do
19
- gem 'rom-sql', github: 'rom-rb/rom-sql', branch: 'master'
20
- gem 'inflecto'
21
- gem 'pry-byebug', '~> 3.8', platforms: :ruby
22
- gem 'pry', '~> 0.12.0', '<= 0.13'
21
+ gem "pry"
22
+ gem "pry-byebug", "~> 3.8", platforms: :ruby
23
+ gem "rom-sql", github: "rom-rb/rom-sql", branch: "release-3.6"
23
24
 
24
- gem 'pg', '~> 0.21', platforms: :ruby
25
- gem 'jdbc-postgres', platforms: :jruby
25
+ gem "jdbc-postgres", platforms: :jruby
26
+ gem "pg", "~> 1.5", platforms: :ruby
26
27
  end
27
28
 
28
29
  group :tools do
29
- gem 'byebug', platform: :mri
30
- gem 'redcarpet' # for yard
30
+ gem "byebug", platform: :mri
31
+ gem "redcarpet" # for yard
31
32
  end
32
33
 
33
34
  group :benchmarks do
34
- gem 'activerecord'
35
- gem 'benchmark-ips'
36
- gem 'factory_bot'
37
- gem 'fabrication'
35
+ gem "activerecord"
36
+ gem "benchmark-ips"
37
+ gem "fabrication"
38
+ gem "factory_bot"
38
39
  end
data/Gemfile.devtools CHANGED
@@ -2,18 +2,19 @@
2
2
 
3
3
  # this file is managed by rom-rb/devtools project
4
4
 
5
+ gem "rake", ">= 12.3.3"
6
+
5
7
  git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
6
8
 
7
9
  group :test do
8
- # 0.18.x breaks codacy result parser
9
- gem "simplecov", "0.17.1", require: false, platforms: :ruby
10
-
11
- gem "codacy-coverage", require: false, platforms: :ruby
10
+ gem "simplecov", require: false, platforms: :ruby
11
+ gem "simplecov-cobertura", require: false, platforms: :ruby
12
+ gem "rexml", require: false
12
13
 
13
14
  gem "warning" if RUBY_VERSION >= "2.4.0"
14
15
  end
15
16
 
16
17
  group :tools do
17
18
  # this is the same version that we use on codacy
18
- gem "rubocop", "0.71.0"
19
+ gem "rubocop", "1.26.1"
19
20
  end
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2015-2020 rom-rb team
3
+ Copyright (c) 2015-2021 rom-rb team
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy of
6
6
  this software and associated documentation files (the "Software"), to deal in
data/README.md CHANGED
@@ -14,14 +14,14 @@
14
14
 
15
15
  ## Links
16
16
 
17
- * [User documentation](http://rom-rb.org/learn/rom-factory)
18
- * [API documentation](http://rubydoc.info/gems/rom-factory)
17
+ * [User documentation](https://rom-rb.org/learn/factory)
18
+ * [API documentation](https://rubydoc.info/gems/rom-factory)
19
19
 
20
20
  ## Supported Ruby versions
21
21
 
22
22
  This library officially supports the following Ruby versions:
23
23
 
24
- * MRI >= `2.4`
24
+ * MRI >= `2.5`
25
25
  * jruby >= `9.2`
26
26
 
27
27
  ## License
data/Rakefile CHANGED
@@ -5,4 +5,4 @@ require "rspec/core/rake_task"
5
5
 
6
6
  RSpec::Core::RakeTask.new(:spec)
7
7
 
8
- task :default => :spec
8
+ task default: :spec
data/benchmarks/basic.rb CHANGED
@@ -1,13 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rom-factory'
4
- require 'rom-core'
5
- require 'active_record'
6
- require 'factory_bot'
7
- require 'fabrication'
8
- require 'benchmark/ips'
3
+ require "rom-factory"
4
+ require "rom-core"
5
+ require "active_record"
6
+ require "factory_bot"
7
+ require "fabrication"
8
+ require "benchmark/ips"
9
9
 
10
- DATABASE_URL = 'postgres://localhost/rom_factory_bench'.freeze
10
+ DATABASE_URL = "postgres://localhost/rom_factory_bench"
11
11
 
12
12
  rom = ROM.container(:sql, DATABASE_URL) do |conf|
13
13
  conf.default.connection.create_table?(:users) do
@@ -50,15 +50,15 @@ Fabricator(:user) do
50
50
  end
51
51
 
52
52
  Benchmark.ips do |x|
53
- x.report('rom-factory persisted struct') do
53
+ x.report("rom-factory persisted struct") do
54
54
  factory[:user]
55
55
  end
56
56
 
57
- x.report('factory_bot') do
57
+ x.report("factory_bot") do
58
58
  FactoryBot.create(:user)
59
59
  end
60
60
 
61
- x.report('fabrication') do
61
+ x.report("fabrication") do
62
62
  Fabricate(:user)
63
63
  end
64
64
 
data/changelog.yml CHANGED
@@ -1,4 +1,39 @@
1
1
  ---
2
+ - version: 0.12.0
3
+ date: 2024-01-19
4
+ added:
5
+ - Support for many-to-many and one-to-one-through associations (via #86) (@solnic)
6
+ - Support for UUID as PKs in associations (via #87) (@solnic)
7
+ fixed:
8
+ - Relations without PKs should work too (via #87) (@solnic)
9
+ - Relations with PK values generated on the Ruby side should work in SQlite too (via #87) (@solnic)
10
+ - version: 0.11.0
11
+ date: 2022-11-11
12
+ added:
13
+ - Support for one-to-one associations (@ianks)
14
+ - "[internal] cache for Faker constants (@flash-gordon)"
15
+ changed:
16
+ - |
17
+ [BREAKING] attributes are always passed as keywords (@alassek)
18
+ This may affect your code in places where attributes are passed as hashes.
19
+ Places like
20
+
21
+ ```ruby
22
+ user_attributes = { name: 'Jane' }
23
+ Factory[:user, user_attributes]
24
+
25
+ ```
26
+
27
+ must be updated to
28
+
29
+ ```ruby
30
+ user_attributes = { name: 'Jane' }
31
+ Factory[:user, **user_attributes]
32
+ ```
33
+ - "Upgraded to the latest versions of dry-rb dependencies, compatible with rom 5.3 (@flash-gordon)"
34
+ - Support for Faker 1.x was dropped (@alassek)
35
+ fixed:
36
+ - Support for plural Faker generators (@wuarmin)
2
37
  - version: 0.10.2
3
38
  date: "2020-04-05"
4
39
  fixed:
@@ -5,4 +5,165 @@ chapter: Factory
5
5
 
6
6
  # rom-factory
7
7
 
8
- TODO: write docs :)
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
+ #### Dependent attributes
145
+
146
+ Attributes can be based on the values of other attributes:
147
+
148
+ ```ruby
149
+ Factory.define(:user) do |f|
150
+ f.full_name { fake(:name) }
151
+ # Dependent attributes are inferred from the block parameter names:
152
+ f.login { |full_name| full_name.downcase.gsub(/\s+/, '_') }
153
+ # Works with sequences too:
154
+ f.sequence(:email) { |n, login| "#{login}-#{n}@example.com" }
155
+ end
156
+ ```
157
+
158
+ ### Build and persist objects
159
+
160
+ ```ruby
161
+ # Create in-memory object
162
+ Factory.structs[:user]
163
+
164
+ # Persist struct in database
165
+ Factory[:user]
166
+
167
+ # Override attributes
168
+ Factory[:user, age: 24]
169
+ ```
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'tsort'
3
+ require "tsort"
4
4
 
5
5
  module ROM
6
6
  module Factory
@@ -47,7 +47,11 @@ module ROM
47
47
 
48
48
  # @api private
49
49
  def associations
50
- self.class.new(elements.select { |e| e.kind_of?(Attributes::Association::Core) })
50
+ self.class.new(elements.select { |e| e.is_a?(Attributes::Association::Core) })
51
+ end
52
+
53
+ def reject(&block)
54
+ self.class.new(elements.reject(&block))
51
55
  end
52
56
 
53
57
  private
@@ -3,12 +3,12 @@
3
3
  module ROM::Factory
4
4
  module Attributes
5
5
  # @api private
6
+ # rubocop:disable Style/OptionalArguments
6
7
  module Association
7
8
  class << self
8
- def new(assoc, builder, *args)
9
- const_get(assoc.definition.type).new(assoc, builder, *args)
9
+ def new(assoc, builder, *traits, **options)
10
+ const_get(assoc.definition.type).new(assoc, builder, *traits, **options)
10
11
  end
11
- ruby2_keywords(:new) if respond_to?(:ruby2_keywords, true)
12
12
  end
13
13
 
14
14
  # @api private
@@ -23,6 +23,11 @@ module ROM::Factory
23
23
  @options = options
24
24
  end
25
25
 
26
+ # @api private
27
+ def through?
28
+ false
29
+ end
30
+
26
31
  # @api private
27
32
  def builder
28
33
  @__builder__ ||= @builder_proc.call
@@ -42,28 +47,62 @@ module ROM::Factory
42
47
  def value?
43
48
  false
44
49
  end
50
+
51
+ # @api private
52
+ def factories
53
+ builder.factories
54
+ end
55
+
56
+ # @api private
57
+ def foreign_key
58
+ assoc.foreign_key
59
+ end
60
+
61
+ # @api private
62
+ def count
63
+ options.fetch(:count, 1)
64
+ end
45
65
  end
46
66
 
47
67
  # @api private
48
68
  class ManyToOne < Core
49
69
  # @api private
70
+ # rubocop:disable Metrics/AbcSize
50
71
  def call(attrs, persist: true)
51
- if attrs.key?(name) && !attrs[foreign_key]
72
+ return if attrs.key?(name) && attrs[name].nil?
73
+
74
+ assoc_data = attrs.fetch(name, EMPTY_HASH)
75
+
76
+ if assoc_data.is_a?(Hash) && assoc_data[assoc.target.primary_key] && !attrs[foreign_key]
52
77
  assoc.associate(attrs, attrs[name])
53
- elsif !attrs[foreign_key]
54
- struct = if persist
55
- builder.persistable.create(*traits)
78
+ elsif assoc_data.is_a?(ROM::Struct)
79
+ assoc.associate(attrs, assoc_data)
80
+ else
81
+ parent = if persist && !attrs[foreign_key]
82
+ builder.persistable.create(*parent_traits, **assoc_data)
56
83
  else
57
- builder.struct(*traits)
84
+ builder.struct(
85
+ *parent_traits,
86
+ **assoc_data.merge(assoc.target.primary_key => attrs[foreign_key])
87
+ )
58
88
  end
59
- tuple = { name => struct }
60
- assoc.associate(tuple, struct)
89
+
90
+ tuple = {name => parent}
91
+
92
+ assoc.associate(tuple, parent)
61
93
  end
62
94
  end
95
+ # rubocop:enable Metrics/AbcSize
63
96
 
64
- # @api private
65
- def foreign_key
66
- assoc.foreign_key
97
+ private
98
+
99
+ def parent_traits
100
+ @parent_traits ||=
101
+ if assoc.target.associations.key?(assoc.source.name)
102
+ traits + [assoc.target.associations[assoc.source.name].key => false]
103
+ else
104
+ traits
105
+ end
67
106
  end
68
107
  end
69
108
 
@@ -78,24 +117,19 @@ module ROM::Factory
78
117
  association_hash = assoc.associate(attrs, parent)
79
118
 
80
119
  if persist
81
- builder.persistable.create(*traits, association_hash)
120
+ builder.persistable.create(*traits, **association_hash)
82
121
  else
83
- builder.struct(*traits, attrs.merge(association_hash))
122
+ builder.struct(*traits, **attrs, **association_hash)
84
123
  end
85
124
  end
86
125
 
87
- { name => structs }
126
+ {name => structs}
88
127
  end
89
128
 
90
129
  # @api private
91
130
  def dependency?(rel)
92
131
  assoc.source == rel
93
132
  end
94
-
95
- # @api private
96
- def count
97
- options.fetch(:count)
98
- end
99
133
  end
100
134
 
101
135
  # @api private
@@ -103,29 +137,93 @@ module ROM::Factory
103
137
  # @api private
104
138
  def call(attrs = EMPTY_HASH, parent, persist: true)
105
139
  # do not associate if count is 0
106
- return { name => nil } if count.zero?
140
+ return {name => nil} if count.zero?
107
141
 
108
142
  return if attrs.key?(name)
109
143
 
110
144
  association_hash = assoc.associate(attrs, parent)
111
145
 
112
146
  struct = if persist
113
- builder.persistable.create(*traits, association_hash)
147
+ builder.persistable.create(*traits, **association_hash)
114
148
  else
115
149
  belongs_to_name = Dry::Core::Inflector.singularize(assoc.source_alias)
116
- belongs_to_associations = { belongs_to_name.to_sym => parent }
150
+ belongs_to_associations = {belongs_to_name.to_sym => parent}
117
151
  final_attrs = attrs.merge(association_hash).merge(belongs_to_associations)
118
- builder.struct(*traits, final_attrs)
152
+ builder.struct(*traits, **final_attrs)
119
153
  end
120
154
 
121
- { name => struct }
155
+ {name => struct}
156
+ end
157
+ end
158
+
159
+ class ManyToMany < Core
160
+ def call(attrs = EMPTY_HASH, parent, persist: true)
161
+ return if attrs.key?(name)
162
+
163
+ structs = count.times.map do
164
+ if persist && attrs[tpk]
165
+ attrs
166
+ elsif persist
167
+ builder.persistable.create(*traits, **attrs)
168
+ else
169
+ builder.struct(*traits, **attrs)
170
+ end
171
+ end
172
+
173
+ # Delegate to through factory if it exists
174
+ if persist
175
+ if through_factory?
176
+ structs.each do |child|
177
+ through_attrs = {
178
+ Dry::Core::Inflector.singularize(assoc.source.name.key).to_sym => parent,
179
+ assoc.through.assoc_name => child
180
+ }
181
+
182
+ factories[through_factory_name, **through_attrs]
183
+ end
184
+ else
185
+ assoc.persist([parent], structs)
186
+ end
187
+
188
+ {name => result(structs)}
189
+ else
190
+ result(structs)
191
+ end
122
192
  end
123
193
 
124
- # @api private
125
- def count
126
- options.fetch(:count, 1)
194
+ def result(structs)
195
+ {name => structs}
196
+ end
197
+
198
+ def dependency?(rel)
199
+ assoc.source == rel
200
+ end
201
+
202
+ def through?
203
+ true
204
+ end
205
+
206
+ def through_factory?
207
+ factories.registry.key?(through_factory_name)
208
+ end
209
+
210
+ def through_factory_name
211
+ ROM::Inflector.singularize(assoc.definition.through.source).to_sym
212
+ end
213
+
214
+ private
215
+
216
+ def tpk
217
+ assoc.target.primary_key
218
+ end
219
+ end
220
+
221
+ class OneToOneThrough < ManyToMany
222
+ def result(structs)
223
+ {name => structs[0]}
127
224
  end
128
225
  end
129
226
  end
130
227
  end
228
+ # rubocop:enable Style/OptionalArguments
131
229
  end
@@ -16,7 +16,7 @@ module ROM::Factory
16
16
  # @api private
17
17
  def call(attrs, *args)
18
18
  result = attrs[name] || dsl.instance_exec(*args, &block)
19
- { name => result }
19
+ {name => result}
20
20
  end
21
21
 
22
22
  # @api private
@@ -16,7 +16,7 @@ module ROM::Factory
16
16
  def call(attrs = EMPTY_HASH)
17
17
  return if attrs.key?(name)
18
18
 
19
- { name => value }
19
+ {name => value}
20
20
  end
21
21
 
22
22
  # @api private
@@ -1,14 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/core/constants'
4
-
5
3
  module ROM
6
4
  module Factory
7
5
  include Dry::Core::Constants
8
6
  end
9
7
  end
10
8
 
11
- require 'rom/factory/attributes/value'
12
- require 'rom/factory/attributes/callable'
13
- require 'rom/factory/attributes/sequence'
14
- require 'rom/factory/attributes/association'
9
+ require "rom/factory/attributes/value"
10
+ require "rom/factory/attributes/callable"
11
+ require "rom/factory/attributes/sequence"
12
+ require "rom/factory/attributes/association"