toil 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 8872978ce2bae67af8b5c87d3735d964e998336d
4
+ data.tar.gz: ba1f898bba5f18ab7be297ef17ed34f3394dac05
5
+ SHA512:
6
+ metadata.gz: 11b1211e720a569863e24098483d94162a2bc112720300ca78df027d0574b79d8abcbf73d0bd28f20f4516454cb702493e5752865407d533bae65a095f7ab54a
7
+ data.tar.gz: dd08d2a0344f8dbacfacffcf6b933c683120a703149c9017674d18384b2b54d71413df90855f309c3e1efbf7301b6d64a7192961d7880cbf0f2d039746e0c6e3
data/LICENSE.txt ADDED
@@ -0,0 +1,18 @@
1
+ Copyright (c) 2019 Joshua Hansen
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to
5
+ deal in the Software without restriction, including without limitation the
6
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7
+ sell copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
16
+ THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,171 @@
1
+ # Toil
2
+
3
+ ## Introduction
4
+
5
+ Apparently, the Ruby world needed yet another factory gem for testing. So, here it is.
6
+
7
+ ### Why not just use [insert significantly more popular factory gem here]?
8
+
9
+ This gem was built to scratch a particular itch. Some of the larger projects I test have moved away from instantiating models directly and generally use service-like objects or function modules to create new models or other resources. While the resulting object is quite often some kind of ORM model, the creation often takes a lot of virtual attributes or even takes argument signatures that are something other than a hash. I've used [Fabrication](https://www.fabricationgem.org) for years, but adapting it to my current needs didn't work out as smoothly as I would have liked.
10
+
11
+ So, here we are. I rolled my own.
12
+
13
+ ### What's with the name?
14
+
15
+ Look, all the other names were gone. [Fabrication](https://www.fabricationgem.org) is already in use with a very nice website. [Machinist](https://github.com/notahat/machinist) is also taken. [factory_girl](https://github.com/thoughtbot/factory_bot) (ahem, sorry, _factory_bot_ since it got all [woke](https://thoughtbot.com/blog/factory_bot)) has a monopoly on factory_*. Even [Mike Perham](https://github.com/mperham) of [Sidekiq](https://sidekiq.org) fame is working on [Faktory](https://github.com/contribsys/faktory)—and while it has nothing to do with tests, I don't even have the obvious rename-with-a-wrong-spelling option out there without confusing people. I guess I could have gone with like Factori, Factoree, or like... never mind. Those clearly suck. Even Sweatshop, the original name, was taken.
16
+
17
+ Tests are a toil. It's four letters. It was available.
18
+
19
+ ### Design Goals
20
+
21
+ I use [Fabrication](https://www.fabricationgem.org) without any nesting to generate attributes. That's about it. Those attributes are then passed to service objects to make stuff. This means generating attributes doesn't have to know anything about dependencies. This works okay until:
22
+
23
+ 1. You're working with something other than hashes.
24
+ 2. You go to modify/extend your existing factory gem and realize it's not exactly the right tool for the job.
25
+
26
+ I wanted a very small, simple codebase. This is really just a container for potentially dynamic attribute generation and methods for spitting out dependencies for tests. I also wanted a codebase that doesn't really care about what it's building. Whether you're using [Sequel](https://github.com/jeremyevans/sequel), [ActiveRecord](https://github.com/rails/rails/tree/master/activerecord), [ROM](https://rom-rb.org), or something that doesn't touch a database, it doesn't matter.
27
+
28
+ This generates arguments, passes them to some sort of constructor or creator object (it's just got to respond to `call`), processes a few optional callbacks, and spits out your object. Simple, repetitive, and mind-numbing—sound familiar?
29
+
30
+ ## Installation
31
+
32
+ Pretty standard gem stuff.
33
+
34
+ ```
35
+ $ gem install toil
36
+ ```
37
+
38
+ If you're using [Bundler](https://bundler.io) (and who isn't?) it's likely you'll add this to the `:test` group of your `Gemfile` like so:
39
+
40
+ ```
41
+ group :test do
42
+ gem 'toil'
43
+ end
44
+ ```
45
+
46
+ Maybe include it in `:development` too. Whatever. You be you.
47
+
48
+ ## Usage
49
+
50
+ While one of the primary goals is dealing with an array of arguments, a hash of attributes still works just as naturally and a lot of the DSL is very much biased in that direction.
51
+
52
+ These examples will use [Faker](https://github.com/stympy/faker).
53
+
54
+ ### Registration
55
+
56
+ Register a new prototype like so:
57
+
58
+ ```ruby
59
+ Toil.register(:person, ->(*args) { Person.create(*args) }) do
60
+ name { Faker::Name.name }
61
+ end
62
+ ```
63
+
64
+ The second argument must respond to `call`, so a `proc` or `lambda` will work fine in instances where you have no constructor object.
65
+
66
+ You can duplicate and extend existing prototypes passing a `Symbol`:
67
+
68
+ ```ruby
69
+ Toil.register(:star_wars_character, :person) do
70
+ name { Faker::StarWars.character }
71
+ end
72
+ ```
73
+ ### Arguments vs. Attributes
74
+
75
+ The DSL is quite opinionated toward a single argument attribute hash. All the DSL methods effect the first hash in the arguments list. This means if you have two hashes, you'll have to make changes to the second in a more "manual" fashion.
76
+
77
+ The `arg` and `arg_at` methods can be used to add or insert arguments when creating or duplicating a prototype. Overrides also account arrays to override arguments.
78
+
79
+ It can get really complicated when duplicating factories. There's little reason to abuse this. An extremely common pattern is:
80
+
81
+ `CreatorClass.call(object1, object2, attributes)`
82
+
83
+ Basically, you have some related dependencies that don't get added as attributes (or maybe the attributes are optional). Whatever the case, these options exist to satisfy constructors that don't just take a hash of options.
84
+
85
+ ### Callbacks
86
+
87
+ There are only two callbacks, `before_create` and `after_create`. Each time the method is invoked, a new callback is added to each stack. So, if you're duplicating an existing prototype, keep in mind you'll be adding more callbacks, not replacing existing ones.
88
+
89
+ #### `before_create`
90
+
91
+ This is meant to transform arguments being passed to the constructor in some way that requires context from existing arguments. A simple example would be that you wanted to create an email address from a randomly generated name.
92
+
93
+ ```ruby
94
+ Toil.register(:person_with_email, :person) do
95
+ before_create do |attributes, *|
96
+ attributes[:email] = Faker::Internet.email(attributes[:name])
97
+ end
98
+ end
99
+ ```
100
+
101
+ Note: Arguments are passed as a single array, since you may want to mutate any possible arguments. If you plan on having a single attributes hash, remember to append your method with a splat like the example above.
102
+
103
+ #### `after_create`
104
+
105
+ The object created once attributes are passed to the constructor will always be yielded to `after_create`. Unlike `before_create` you don't have to pay any attention to what is returned. The same object will be yielded to every `after_create` callback. This is generally for adding relationships or processing state transitions on an object. For example:
106
+
107
+ ```ruby
108
+ Toil.register(:pending_order, OrderCreator) do
109
+ # ...
110
+ end
111
+
112
+ Toil.register(:paid_order, :pending_order) do
113
+ after_create { |order| OrderPayer.call(order, amount: order.full_amount) }
114
+ end
115
+ ```
116
+
117
+ ### What about relationships?
118
+
119
+ You don't use nested attributes or arguments to build relationships. Dependencies and related resources should be created with other prototypes either as arguments or in `after_create` hooks. For example:
120
+
121
+ ```ruby
122
+ Toil.register(:album, AlbumCreator)
123
+
124
+ Toil.register(:rio_album, :album) do
125
+ artist { Toil.create(:duran_duran) }
126
+ tracks 9
127
+ release_date Date.new(1982, 5, 10)
128
+ end
129
+
130
+ Toil.register(:rio_album_multiplatinum, :rio_album) do
131
+ after_create do |obj|
132
+ 2_000_000.times { Toil.create(:album_sale, album: obj) }
133
+ end
134
+ end
135
+
136
+ ```
137
+
138
+ ### Creating Objects
139
+
140
+ Use `create` to try and create an object of some sort. You pass in overrides, either as a hash or an array (it gets splatted). You can use this to add more arguments, or override defaults:
141
+
142
+ ```ruby
143
+ Toil.create(:star_wars_character, name: 'James T. Kirk')
144
+ ```
145
+
146
+ Overrides are resolved first, so if your prototypes create dependencies, they **will not** be created in addition to whatever override is passed in.
147
+
148
+ ## Contributing
149
+
150
+ ### Issue Guidelines
151
+
152
+ GitHub issues are for bugs, not support. As of right now, there is no official support for this gem. You can try reaching out to the author, [Joshua Hansen](mailto:joshua@epicbanality.com?subject=Toil+sucks) if you're really stuck, but there's a pretty high chance that won't go anywhere at the moment or you'll get a response like this:
153
+
154
+ > Hi. I'm super busy. It's nothing personal. Check the README first if you haven't already. If you don't find your answer there, it's time to start reading the source. Have fun! Let me know if I screwed something up.
155
+
156
+ ### Pull Request Guidelines
157
+
158
+ * Include tests with your PRs.
159
+ * Run `bundle exec rubocop` to ensure your style fits with the rest of the project.
160
+
161
+ ### Code of Conduct
162
+
163
+ Sorry, I'm not woke.
164
+
165
+ ## License
166
+
167
+ See [`LICENSE.txt`](LICENSE.txt).
168
+
169
+ ## What if I stop maintaining this?
170
+
171
+ The codebase is pretty small. That was one of the main design goals. If you can figure out how to use it, I'm sure you can maintain it.
data/lib/toil.rb ADDED
@@ -0,0 +1,33 @@
1
+ # frozen-string-literal: true
2
+
3
+ require 'toil/prototype'
4
+
5
+ module Toil
6
+ class << self
7
+ def call(prototype_name, *overrides)
8
+ self[prototype_name].call(*overrides)
9
+ end
10
+ alias create call
11
+
12
+ def [](prototype_name)
13
+ Prototype[prototype_name]
14
+ end
15
+
16
+ def register(prototype_name, obj = nil, &blk)
17
+ Prototype.register(prototype_name, obj, &blk)
18
+ end
19
+
20
+ def to_a(prototype_name, *overrides)
21
+ self[prototype_name].to_a(*overrides)
22
+ end
23
+ alias args to_a
24
+ alias arguments to_a
25
+
26
+ def to_h(prototype_name, overrides = {})
27
+ self[prototype_name].to_h(overrides)
28
+ end
29
+ alias atts to_h
30
+ alias attributes to_h
31
+ alias params to_h
32
+ end
33
+ end
@@ -0,0 +1,93 @@
1
+ # frozen-string-literal: true
2
+
3
+ require 'toil/attributes'
4
+
5
+ module Toil
6
+ class Arguments
7
+ def initialize(args, &blk)
8
+ @args = __array__(args).map { |v| __arg__(v) }
9
+ __find_attributes__
10
+ instance_eval(&blk) if block_given?
11
+ end
12
+
13
+ def method_missing(m, *args, &blk)
14
+ attribute(m, args.first, &blk)
15
+ end
16
+
17
+ def arg(value = nil, &blk)
18
+ arg_at(@args.size, value, &blk)
19
+ end
20
+
21
+ def arg_at(index, value = nil, &blk)
22
+ @args[index] = __arg__(value, &blk)
23
+ __find_attributes__
24
+ @args[index]
25
+ end
26
+
27
+ def attribute(key, value = nil, &blk)
28
+ @attributes || arg({})
29
+ @attributes.__set__(key, value, &blk)
30
+ end
31
+
32
+ def attributes_at
33
+ @args.each_with_index { |v, i| return i if v == @attributes }
34
+ nil
35
+ end
36
+
37
+ def dup(&blk)
38
+ self.class.new(@args, &blk)
39
+ end
40
+
41
+ def [](key)
42
+ @attributes[key]
43
+ end
44
+
45
+ def to_a(*overrides)
46
+ @args.each_with_index.map do |a, i|
47
+ if overrides.size > i
48
+ __merge_or_override__(overrides[i], a)
49
+ elsif a.is_a?(Attributes)
50
+ a.to_h
51
+ else
52
+ a.call
53
+ end
54
+ end + Array(overrides[(@args.size)..-1])
55
+ end
56
+ alias to_ary to_a
57
+
58
+ def to_h(overrides = {})
59
+ @attributes.to_h(overrides)
60
+ end
61
+ alias to_hash to_h
62
+
63
+ private
64
+
65
+ def __arg__(arg, &blk)
66
+ return DynamicValue.new(&blk) if block_given?
67
+
68
+ case arg
69
+ when Attributes
70
+ arg.dup
71
+ when DynamicValue
72
+ arg
73
+ when Hash
74
+ Attributes.new(arg)
75
+ else
76
+ DynamicValue.new(arg)
77
+ end
78
+ end
79
+
80
+ def __array__(args)
81
+ args.is_a?(Array) ? args : [args]
82
+ end
83
+
84
+ def __find_attributes__
85
+ @attributes ||= @args.reverse.find { |v| v.is_a?(Attributes) }
86
+ end
87
+
88
+ def __merge_or_override__(override, arg)
89
+ return arg.to_h(override) if override.is_a?(Hash) && arg.is_a?(Attributes)
90
+ override
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,37 @@
1
+ # frozen-string-literal: true
2
+
3
+ require 'toil/dynamic_value'
4
+
5
+ module Toil
6
+ class Attributes
7
+ def initialize(hash = {}, &blk)
8
+ @attributes = hash.each_with_object({}) do |(k, v), h|
9
+ h[k] = v.is_a?(DynamicValue) ? v : DynamicValue.new(v)
10
+ end
11
+ instance_eval(&blk) if block_given?
12
+ end
13
+
14
+ def method_missing(m, *args, &blk)
15
+ __set__(m, args.first, &blk)
16
+ end
17
+
18
+ def dup(&blk)
19
+ self.class.new(@attributes, &blk)
20
+ end
21
+
22
+ def to_h(overrides = {})
23
+ @attributes.each_with_object({}) do |(k, v), h|
24
+ h[k] = v.call unless overrides.key?(k)
25
+ end.merge(overrides)
26
+ end
27
+ alias to_hash to_h
28
+
29
+ def [](key)
30
+ (v = @attributes[key]) && v.call
31
+ end
32
+
33
+ def __set__(key, value = nil, &blk)
34
+ @attributes[key] = DynamicValue.new(value, &blk)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,26 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Toil
4
+ class DynamicValue
5
+ def initialize(value = nil, &blk)
6
+ @static = false
7
+ @proc =
8
+ if block_given?
9
+ blk
10
+ elsif value.is_a?(Proc)
11
+ value
12
+ else
13
+ @static = true
14
+ proc { value }
15
+ end
16
+ end
17
+
18
+ def call
19
+ @proc.call
20
+ end
21
+
22
+ def static?
23
+ @static
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,87 @@
1
+ # frozen-string-literal: true
2
+
3
+ require 'toil/arguments'
4
+
5
+ module Toil
6
+ class Prototype
7
+ CALLBACKS = %i[after_create before_create].freeze
8
+ NO_ATTS_MSG = 'There are no attribute arguments for this prototype.'.freeze
9
+ AlreadyRegistered = Class.new(ArgumentError)
10
+ NoAttributesDefined = Class.new(RuntimeError)
11
+ NotRegistered = Class.new(ArgumentError)
12
+
13
+ @@registry = {}
14
+
15
+ class << self
16
+ def [](key)
17
+ raise NotRegistered, "`:#{key}` is not a registered prototype" unless
18
+ @@registry.key?(key)
19
+ @@registry[key]
20
+ end
21
+
22
+ def register(key, obj = nil, &blk)
23
+ key = key.to_sym
24
+ raise AlreadyRegistered, "`:#{key}` has already been registered" if
25
+ @@registry.key?(key)
26
+
27
+ @@registry[key] = obj.is_a?(Symbol) ? self[obj].dup(&blk) : new(obj, &blk)
28
+ end
29
+ end
30
+
31
+ def initialize(constructor, arguments = [], callbacks = {}, &blk)
32
+ @constructor = __constructor__(constructor)
33
+ @arguments = __arguments__(arguments)
34
+ @callbacks = __callbacks__(callbacks)
35
+ instance_eval(&blk) if block_given?
36
+ end
37
+
38
+ def call(*overrides)
39
+ __exec_callbacks__(:after_create, @constructor.(*to_a(*overrides)))
40
+ end
41
+
42
+ def method_missing(m, *args, &blk)
43
+ @arguments.public_send(m, *args, &blk)
44
+ end
45
+
46
+ def dup(&blk)
47
+ self.class.new(@constructor, @arguments, @callbacks, &blk)
48
+ end
49
+
50
+ def to_a(*overrides)
51
+ __exec_callbacks__(:before_create, @arguments.to_a(*overrides))
52
+ end
53
+ alias to_ary to_a
54
+
55
+ def to_h(overrides = {})
56
+ raise NoAttributesDefined, NO_ATTS_MSG unless (at = @arguments.attributes_at)
57
+ args = @arguments.to_a
58
+ args[at].merge!(overrides)
59
+ __exec_callbacks__(:before_create, args)[at]
60
+ end
61
+ alias to_hash to_h
62
+
63
+ CALLBACKS.each do |key|
64
+ define_method(key) { |&blk| @callbacks[key] += [blk] }
65
+ end
66
+
67
+ private
68
+
69
+ def __arguments__(args)
70
+ args.is_a?(Arguments) ? args.dup : Arguments.new(args)
71
+ end
72
+
73
+ def __callbacks__(callbacks)
74
+ CALLBACKS.each_with_object({}) { |k, h| h[k] = [] }.merge(callbacks)
75
+ end
76
+
77
+ def __constructor__(obj)
78
+ raise ArgumentError, 'Object does not respond to `call`' unless obj.respond_to?(:call)
79
+ obj
80
+ end
81
+
82
+ def __exec_callbacks__(key, args)
83
+ @callbacks[key].each { |clbk| clbk.(args) }
84
+ args
85
+ end
86
+ end
87
+ end
@@ -0,0 +1 @@
1
+ # frozen-string-literal: true
@@ -0,0 +1,12 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Toil
4
+ MAJOR = 0
5
+ MINOR = 1
6
+ TINY = 0
7
+ VERSION = [MAJOR, MINOR, TINY].join('.').freeze
8
+
9
+ def self.version
10
+ VERSION
11
+ end
12
+ end
metadata ADDED
@@ -0,0 +1,110 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: toil
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Joshua
8
+ - Hansen
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2019-03-18 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: bundler
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '1.16'
21
+ type: :development
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: '1.16'
28
+ - !ruby/object:Gem::Dependency
29
+ name: minitest
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '5.0'
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '5.0'
42
+ - !ruby/object:Gem::Dependency
43
+ name: rake
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '10.0'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '10.0'
56
+ - !ruby/object:Gem::Dependency
57
+ name: rubocop
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - "~>"
61
+ - !ruby/object:Gem::Version
62
+ version: '0.56'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: '0.56'
70
+ description: Yet another factory library.
71
+ email:
72
+ - joshua@epicbanality.com
73
+ executables: []
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - LICENSE.txt
78
+ - README.md
79
+ - lib/toil.rb
80
+ - lib/toil/arguments.rb
81
+ - lib/toil/attributes.rb
82
+ - lib/toil/dynamic_value.rb
83
+ - lib/toil/prototype.rb
84
+ - lib/toil/sequence.rb
85
+ - lib/toil/version.rb
86
+ homepage: https://github.com/binarypaladin/toil
87
+ licenses:
88
+ - MIT
89
+ metadata: {}
90
+ post_install_message:
91
+ rdoc_options: []
92
+ require_paths:
93
+ - lib
94
+ required_ruby_version: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ version: 2.2.0
99
+ required_rubygems_version: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ requirements: []
105
+ rubyforge_project:
106
+ rubygems_version: 2.4.5.4
107
+ signing_key:
108
+ specification_version: 4
109
+ summary: Yet another factory library.
110
+ test_files: []