satisfactory 0.0.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d75154f80250b3760e281317aeb9dbe5943df83925488c3e66164ace3736f422
4
- data.tar.gz: a29f35668835598adc52196519d9ca849354c9c0ed23a62d1bbb3997a2343db6
3
+ metadata.gz: 06dfe6edd70f452888cfeb3d5379a592a3d88983c5651024cdc634dad1af3ca5
4
+ data.tar.gz: f50122a32a4068fd77e3fb6d4639957117ceaf7592af1530bf35e0b4a2b9594f
5
5
  SHA512:
6
- metadata.gz: 3dfcf2f1eaf54ffe8345fac3efd11351c26e0b3840457727bf378cbbfadb47d93a01f39fb26cc6e2753ee92016a4b048d55f9d1e99d3c54d93778cb6b8b68e78
7
- data.tar.gz: 514ed076dbc00f6313be39609a1b7cb48dbf309247870e6bda9a6af7ff3a6c21d59dbd126c0ee33197e4f07226a9b9c6a80972fb8d92c2fa40cf47a96c31d48c
6
+ metadata.gz: 6fe4c14011c38c9b3116e20b501ce8d65dcc458cd63b9fe44dd1e8291db5ca535d61d63069bc95b118342d17550e5c424aea7217bb5802a51df0cc74e8a0cbf8
7
+ data.tar.gz: 522edd69c8e2e31138e7a8cd0fe5521135b5bf42b544c2f9511ef8aa5144635a8cc3a3b953c9578446d041a6144b1e367723c6d7d00f3c679c2bdb0ce0955d17
data/LICENCE ADDED
@@ -0,0 +1,2 @@
1
+ Satisfactory © 2022 by Smart/Casual Ltd is licensed under CC BY-NC-SA 4.0.
2
+ To view a copy of this license, visit http://creativecommons.org/licenses/by-nc-sa/4.0/
data/Rakefile CHANGED
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  require "bundler/gem_tasks"
4
2
  require "rubocop/rake_task"
5
3
 
@@ -0,0 +1,61 @@
1
+ require_relative "upstream_record_finder"
2
+
3
+ module Satisfactory
4
+ # Represents a collection of homogenous records.
5
+ class Collection < Array
6
+ # @api private
7
+ def initialize(*args, upstream:, **kwargs, &block)
8
+ super(*args, **kwargs, &block)
9
+ @upstream = upstream
10
+ end
11
+
12
+ # @api private
13
+ attr_reader :upstream
14
+
15
+ # @!method and
16
+ # Delegates to the upstream record.
17
+ # @return (see Satisfactory::Record#and)
18
+ # @see Satisfactory::Record#and
19
+ # @!method create
20
+ # Delegates to the upstream record.
21
+ # @return (see Satisfactory::Record#create)
22
+ # @see Satisfactory::Record#create
23
+ # @!method to_plan
24
+ # Delegates to the upstream record.
25
+ # @return (see Satisfactory::Record#to_plan)
26
+ # @see Satisfactory::Record#to_plan
27
+ delegate :and, :create, :to_plan, to: :upstream
28
+
29
+ # Calls {#with} on each entry in the collection.
30
+ #
31
+ # @return [Satisfactory::Collection]
32
+ def with(...)
33
+ self.class.new(map { |entry| entry.with(...) }, upstream:)
34
+ end
35
+ alias each_with with
36
+
37
+ # Calls {#which_is} on each entry in the collection.
38
+ #
39
+ # @param (see Satisfactory::Record#which_is)
40
+ # @return [Satisfactory::Collection]
41
+ def which_are(...)
42
+ self.class.new(map { |entry| entry.which_is(...) }, upstream:)
43
+ end
44
+ alias which_is which_are
45
+
46
+ # (see Satisfactory::Record#and_same)
47
+ def and_same(upstream_type)
48
+ Satisfactory::UpstreamRecordFinder.new(upstream:).find(upstream_type)
49
+ end
50
+
51
+ # @api private
52
+ def build
53
+ flat_map(&:build)
54
+ end
55
+
56
+ # @api private
57
+ def build_plan
58
+ flat_map(&:build_plan)
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,43 @@
1
+ require "factory_bot_rails"
2
+
3
+ module Satisfactory
4
+ # Loads factory configurations from FactoryBot.
5
+ #
6
+ # @api private
7
+ class Loader
8
+ class << self
9
+ # Skips factories that don't have a model that inherits from ApplicationRecord.
10
+ #
11
+ # @return [{Symbol => Hash}] a hash of factory configurations by factory name
12
+ def factory_configurations # rubocop:disable Metrics/AbcSize
13
+ FactoryBot.factories.each.with_object({}) do |(factory, model), configurations|
14
+ next unless (model = factory.build_class)
15
+ next unless model < ApplicationRecord
16
+
17
+ associations = associations_for(model)
18
+ parent_factory = factory.send(:parent)
19
+
20
+ configurations[factory.name] = {
21
+ associations: associations.transform_values { |a| a.map(&:name) },
22
+ model:,
23
+ name: factory.name,
24
+ parent: (parent_factory.name unless parent_factory.is_a?(FactoryBot::NullFactory)),
25
+ traits: factory.defined_traits.map(&:name),
26
+ }
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def associations_for(model)
33
+ all = model.reflect_on_all_associations.reject(&:polymorphic?)
34
+ plural = model.reflect_on_all_associations(:has_many).reject(&:polymorphic?)
35
+
36
+ {
37
+ plural:,
38
+ singular: all - plural,
39
+ }
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,208 @@
1
+ require_relative "collection"
2
+ require_relative "upstream_record_finder"
3
+
4
+ module Satisfactory
5
+ # Represents a usage of a type.
6
+ #
7
+ # @todo This whole class needs a tidy up
8
+ class Record # rubocop:disable Metrics/ClassLength
9
+ # @api private
10
+ # @param type [Symbol] The type of record to create. Must be a known factory.
11
+ # @param factory_name [Symbol] The name of the factory to use (if different).
12
+ # @param upstream [Satisfactory::Record, Satisfactory::Collection, Satisfactory::Root] The upstream record-ish.
13
+ # @param attributes [Hash] The attributes to use when creating the record.
14
+ def initialize(type:, factory_name: nil, upstream: nil, attributes: {})
15
+ @factory_name = factory_name || type
16
+
17
+ config = Satisfactory.factory_configurations[type]
18
+ raise ArgumentError, "Unknown factory #{type}" unless config
19
+
20
+ @type = config[:parent] || type
21
+ @type_config = Satisfactory.factory_configurations[@type]
22
+ @traits = []
23
+ @upstream = upstream
24
+
25
+ @attributes = attributes
26
+
27
+ @associations = type_config.dig(:associations, :plural).each.with_object({}) do |name, hash|
28
+ hash[name] = Satisfactory::Collection.new(upstream: self)
29
+ end
30
+ end
31
+
32
+ # @api private
33
+ attr_accessor :type, :type_config, :traits, :upstream, :factory_name, :attributes
34
+
35
+ # Add an associated record to this record's build plan.
36
+ #
37
+ # @param count [Integer] The number of records to create.
38
+ # @param downstream_type [Symbol] The type of record to create.
39
+ # @param force [Boolean] Whether to force the creation of the record.
40
+ # For internal use only. Use {#and} instead.
41
+ # @param attributes [Hash] The attributes to use when creating the record.
42
+ # @return [Satisfactory::Record, Satisfactory::Collection]
43
+ def with(count = nil, downstream_type, force: false, **attributes) # rubocop:disable Style/OptionalArguments, Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
44
+ if singular?(downstream_type)
45
+ if count && count > 1 # rubocop:disable Style/IfUnlessModifier
46
+ raise ArgumentError, "Cannot create multiple of singular associations (e.g. belongs_to)"
47
+ end
48
+
49
+ add_singular_association(downstream_type, factory_name: downstream_type, force:, attributes:)
50
+ elsif plural?(downstream_type) && (singular = singular_from_plural(downstream_type))
51
+ add_plural_association(downstream_type, factory_name: singular, count:, force:, attributes:)
52
+ elsif (config = Satisfactory.factory_configurations[downstream_type])
53
+ singular = config[:parent] || downstream_type
54
+ plural = plural_from_singular(singular)
55
+ add_singular_for_plural_association(plural, singular:, factory_name: downstream_type, force:, attributes:)
56
+ elsif (config = Satisfactory.factory_configurations[downstream_type.to_s.singularize])
57
+ unless (parent = config[:parent])
58
+ raise ArgumentError, "Cannot create multiple of singular associations (e.g. belongs_to)"
59
+ end
60
+
61
+ plural = plural_from_singular(parent)
62
+ add_plural_association(plural, factory_name: downstream_type.to_s.singularize, count:, force:, attributes:)
63
+ else
64
+ raise ArgumentError, "Unknown association #{type}->#{downstream_type}"
65
+ end
66
+ end
67
+
68
+ # Add a sibling record to the parent record's build plan.
69
+ # e.g. adding a second user to a project.
70
+ #
71
+ # @param count [Integer] The number of records to create.
72
+ # @param downstream_type [Symbol] The type of record to create.
73
+ # @param attributes [Hash] The attributes to use when creating the record.
74
+ # @return (see #with)
75
+ def and(count = nil, downstream_type, **attributes) # rubocop:disable Style/OptionalArguments
76
+ upstream.with(count, downstream_type, force: true, **attributes)
77
+ end
78
+
79
+ # Apply one or more traits to this record's build plan.
80
+ #
81
+ # @param *traits [Symbol, ...] The traits to apply.
82
+ def which_is(*traits)
83
+ traits.each { |trait| self.traits << trait }
84
+ self
85
+ end
86
+
87
+ # Locate the nearest ancestor of the given type.
88
+ #
89
+ # @param upstream_type [Symbol] The type of ancestor to find.
90
+ # @return [Satisfactory::Record, Satisfactory::Collection, Satisfactory::Root]
91
+ def and_same(upstream_type)
92
+ Satisfactory::UpstreamRecordFinder.new(upstream:).find(upstream_type)
93
+ end
94
+
95
+ # @api private
96
+ def modify
97
+ yield(self).upstream
98
+ end
99
+
100
+ # Trigger the creation of this tree's build plan.
101
+ #
102
+ # @return (see Satisfactory::Root#create)
103
+ # @todo Check if we still need the upstream check.
104
+ def create
105
+ if upstream
106
+ upstream.create
107
+ else
108
+ create_self
109
+ end
110
+ end
111
+
112
+ # Construct this tree's build plan.
113
+ #
114
+ # @return [Hash]
115
+ def to_plan
116
+ if upstream
117
+ upstream.to_plan
118
+ else
119
+ build_plan
120
+ end
121
+ end
122
+
123
+ # @api private
124
+ def build_plan
125
+ {
126
+ traits:,
127
+ }.merge(associations_plan).compact_blank
128
+ end
129
+
130
+ # @api private
131
+ # @return (see #reify)
132
+ def build
133
+ reify(:build)
134
+ end
135
+
136
+ # @api private
137
+ # @return (see #reify)
138
+ def create_self
139
+ reify(:create)
140
+ end
141
+
142
+ private
143
+
144
+ attr_reader :associations
145
+
146
+ # @return [ApplicationRecord]
147
+ def reify(method)
148
+ FactoryBot.public_send(method, factory_name, *traits, attributes.merge(associations.transform_values(&:build)))
149
+ end
150
+
151
+ def associations_plan
152
+ associations.transform_values(&:build_plan).compact_blank
153
+ end
154
+
155
+ def plural?(association_name)
156
+ type_config.dig(:associations, :plural).include?(association_name)
157
+ end
158
+
159
+ def singular?(association_name)
160
+ type_config.dig(:associations, :singular).include?(association_name)
161
+ end
162
+
163
+ def plural_from_singular(singular_association_name)
164
+ type_config.dig(:associations, :plural).find do |name|
165
+ singular_association_name.to_s == name.to_s.singularize
166
+ end
167
+ end
168
+
169
+ def singular_from_plural(plural_association_name)
170
+ Satisfactory.factory_configurations.keys.find do |name|
171
+ plural_association_name.to_s == name.to_s.pluralize
172
+ end
173
+ end
174
+
175
+ def add_singular_association(name, factory_name:, force: false, attributes: {})
176
+ if force || associations[name].blank?
177
+ associations[name] = self.class.new(type: name, factory_name:, upstream: self, attributes:)
178
+ else
179
+ associations[name]
180
+ end
181
+ end
182
+
183
+ def add_plural_association(name, factory_name:, count: nil, force: false, attributes: {})
184
+ count ||= 1
185
+ singular_name = name.to_s.singularize.to_sym
186
+
187
+ Satisfactory::Collection.new(upstream: self).tap do |new_associations|
188
+ count.times do
189
+ new_associations << self.class.new(type: singular_name, factory_name:, upstream: self, attributes:)
190
+ end
191
+
192
+ if force
193
+ associations[name] << new_associations
194
+ else
195
+ associations[name] = new_associations
196
+ end
197
+ end
198
+ end
199
+
200
+ def add_singular_for_plural_association(name, singular:, factory_name:, force: false, attributes: {})
201
+ if force || associations[name].empty?
202
+ associations[name] << self.class.new(type: singular, factory_name:, upstream: self, attributes:)
203
+ end
204
+
205
+ associations[name].last
206
+ end
207
+ end
208
+ end
@@ -0,0 +1,47 @@
1
+ require_relative "record"
2
+
3
+ module Satisfactory
4
+ # The root of the factory graph.
5
+ # This is where you begin creating records.
6
+ class Root
7
+ # @api private
8
+ def initialize
9
+ @root_records = Hash.new { |h, k| h[k] = [] }
10
+ end
11
+
12
+ # Add a top-level record to the root.
13
+ # This is your entry point into the factory graph, and
14
+ # the way to begin creating records.
15
+ def add(factory_name, **attributes)
16
+ raise FactoryNotDefinedError, factory_name unless Satisfactory.factory_configurations.key?(factory_name)
17
+
18
+ Satisfactory::Record.new(
19
+ type: factory_name,
20
+ upstream: self,
21
+ attributes:,
22
+ ).tap { |r| @root_records[factory_name] << r }
23
+ end
24
+
25
+ # @api private
26
+ # @return [Hash<Symbol, Array<ApplicationRecord>>]
27
+ def create
28
+ @root_records.transform_values do |records|
29
+ records.map(&:create_self)
30
+ end
31
+ end
32
+
33
+ # @api private
34
+ # @return [Hash<Symbol, Array<Hash>>]
35
+ def to_plan
36
+ @root_records.transform_values do |records|
37
+ records.map(&:build_plan)
38
+ end
39
+ end
40
+
41
+ # @api private
42
+ # @return [nil]
43
+ def upstream
44
+ nil
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,30 @@
1
+ module Satisfactory
2
+ # Finds the upstream record of a given type.
3
+ #
4
+ # @api private
5
+ class UpstreamRecordFinder
6
+ def initialize(upstream:)
7
+ @upstream = upstream
8
+ end
9
+
10
+ attr_accessor :upstream
11
+
12
+ def find(type)
13
+ raise MissingUpstreamRecordError, type if upstream.nil?
14
+
15
+ if type == upstream.type
16
+ self
17
+ else
18
+ self.upstream = upstream.upstream
19
+ find(type)
20
+ end
21
+ end
22
+
23
+ def with(*args, **kwargs)
24
+ upstream.with(*args, force: true, **kwargs)
25
+ end
26
+
27
+ # @api private
28
+ class MissingUpstreamRecordError < StandardError; end
29
+ end
30
+ end
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  module Satisfactory
4
- VERSION = "0.0.0"
2
+ VERSION = "0.2.1".freeze
5
3
  end
data/lib/satisfactory.rb CHANGED
@@ -1,8 +1,24 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "satisfactory/version"
1
+ require_relative "satisfactory/loader"
2
+ require_relative "satisfactory/root"
4
3
 
4
+ # Satisfactory is a factory library for Ruby,
5
+ # helping you to navigate your factory associations.
6
+ #
7
+ # Currently implemented atop FactoryBot, but
8
+ # could be extended to support other factory libraries.
9
+ #
10
+ # @since 0.2.0
5
11
  module Satisfactory
6
- class Error < StandardError; end
7
- # Your code goes here...
12
+ # @!attribute [r] factory_configurations
13
+ class << self
14
+ def root
15
+ Root.new
16
+ end
17
+
18
+ # @api private
19
+ # @return (see Loader.factory_configurations)
20
+ def factory_configurations
21
+ @factory_configurations ||= Loader.factory_configurations
22
+ end
23
+ end
8
24
  end
@@ -0,0 +1,37 @@
1
+ require_relative "lib/satisfactory/version"
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "satisfactory"
5
+ spec.version = Satisfactory::VERSION
6
+ spec.authors = ["Elliot Crosby-McCullough"]
7
+ spec.email = ["elliot.cm@gmail.com"]
8
+
9
+ spec.summary = "A DSL for navigating your factories and building test data"
10
+ spec.homepage = "https://github.com/SmartCasual/satisfactory"
11
+ spec.license = "CC-BY-NC-SA-4.0"
12
+
13
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/v#{spec.version}/CHANGELOG.md"
14
+ spec.metadata["homepage_uri"] = spec.homepage
15
+ spec.metadata["source_code_uri"] = spec.homepage
16
+
17
+ spec.metadata["rubygems_mfa_required"] = "true"
18
+ spec.required_ruby_version = ">= 3.1.0"
19
+
20
+ spec.files = Dir.chdir(File.expand_path(__dir__)) {
21
+ Dir[
22
+ "lib/**/*",
23
+ "CHANGELOG.md",
24
+ "LICENCE",
25
+ "Rakefile",
26
+ "README.md",
27
+ "satisfactory.gemspec",
28
+ ]
29
+ }
30
+
31
+ spec.require_paths = ["lib"]
32
+
33
+ spec.add_dependency "factory_bot_rails", "~> 6.2"
34
+
35
+ spec.add_development_dependency "rubocop", "~> 1.40"
36
+ spec.add_development_dependency "yard", "~> 0.9"
37
+ end
metadata CHANGED
@@ -1,15 +1,57 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: satisfactory
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Elliot Crosby-McCullough
8
8
  autorequire:
9
- bindir: exe
9
+ bindir: bin
10
10
  cert_chain: []
11
- date: 2022-12-09 00:00:00.000000000 Z
12
- dependencies: []
11
+ date: 2022-12-19 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: factory_bot_rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '6.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '6.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rubocop
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.40'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.40'
41
+ - !ruby/object:Gem::Dependency
42
+ name: yard
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.9'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.9'
13
55
  description:
14
56
  email:
15
57
  - elliot.cm@gmail.com
@@ -17,19 +59,26 @@ executables: []
17
59
  extensions: []
18
60
  extra_rdoc_files: []
19
61
  files:
20
- - ".rubocop.yml"
21
62
  - CHANGELOG.md
22
- - Gemfile
63
+ - LICENCE
23
64
  - README.md
24
65
  - Rakefile
25
66
  - lib/satisfactory.rb
67
+ - lib/satisfactory/collection.rb
68
+ - lib/satisfactory/loader.rb
69
+ - lib/satisfactory/record.rb
70
+ - lib/satisfactory/root.rb
71
+ - lib/satisfactory/upstream_record_finder.rb
26
72
  - lib/satisfactory/version.rb
27
- - sig/satisfactory.rbs
73
+ - satisfactory.gemspec
28
74
  homepage: https://github.com/SmartCasual/satisfactory
29
- licenses: []
75
+ licenses:
76
+ - CC-BY-NC-SA-4.0
30
77
  metadata:
78
+ changelog_uri: https://github.com/SmartCasual/satisfactory/blob/v0.2.1/CHANGELOG.md
31
79
  homepage_uri: https://github.com/SmartCasual/satisfactory
32
80
  source_code_uri: https://github.com/SmartCasual/satisfactory
81
+ rubygems_mfa_required: 'true'
33
82
  post_install_message:
34
83
  rdoc_options: []
35
84
  require_paths:
@@ -38,7 +87,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
38
87
  requirements:
39
88
  - - ">="
40
89
  - !ruby/object:Gem::Version
41
- version: 2.6.0
90
+ version: 3.1.0
42
91
  required_rubygems_version: !ruby/object:Gem::Requirement
43
92
  requirements:
44
93
  - - ">="
@@ -48,5 +97,5 @@ requirements: []
48
97
  rubygems_version: 3.3.7
49
98
  signing_key:
50
99
  specification_version: 4
51
- summary: Write a short summary, because RubyGems requires one.
100
+ summary: A DSL for navigating your factories and building test data
52
101
  test_files: []
data/.rubocop.yml DELETED
@@ -1,13 +0,0 @@
1
- AllCops:
2
- TargetRubyVersion: 2.6
3
-
4
- Style/StringLiterals:
5
- Enabled: true
6
- EnforcedStyle: double_quotes
7
-
8
- Style/StringLiteralsInInterpolation:
9
- Enabled: true
10
- EnforcedStyle: double_quotes
11
-
12
- Layout/LineLength:
13
- Max: 120
data/Gemfile DELETED
@@ -1,10 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- source "https://rubygems.org"
4
-
5
- # Specify your gem's dependencies in satisfactory.gemspec
6
- gemspec
7
-
8
- gem "rake", "~> 13.0"
9
-
10
- gem "rubocop", "~> 1.21"
data/sig/satisfactory.rbs DELETED
@@ -1,4 +0,0 @@
1
- module Satisfactory
2
- VERSION: String
3
- # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
- end