rom-factory 0.10.2 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
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"