explicit_activerecord 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +168 -0
- data/lib/explicit_activerecord.rb +8 -0
- data/lib/explicit_activerecord/no_db_access.rb +49 -0
- data/lib/explicit_activerecord/persistence.rb +133 -0
- metadata +159 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: f9d1ed0101b826fc591cb78e46eef4f297754b42dba7fa3e2ee3c8342082ddd3
|
4
|
+
data.tar.gz: c52614803edb50bd57da611fe5e19507f68199de979c314ad34c32f6495bf1e2
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 968de86a11cd97fcf632c2d9d4d043d44eda52bda87ccf1e55a944afdd1feeaa95e9caaca75027a8047c51be5c3bc01675502f128a29bdaf7fbdcc3fdeac56d5
|
7
|
+
data.tar.gz: d41aeb3ecd8d4b32492f2d71253ace7884883a436c23307cfe13040b4dd98fd59dee0595318cb7cb3c0a4c0fb7ffd1e5368d6dcd7ac214e8bd1e7e14dc79845d
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2021 Gusto
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, 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,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,168 @@
|
|
1
|
+
# ExplicitActiveRecord
|
2
|
+
|
3
|
+
[![Build status](https://badge.buildkite.com/3899d0bb95a2680f73cad1df4cedc875476548d85392d47740.svg?branch=main)](https://buildkite.com/gusto/explicitactiverecord)
|
4
|
+
|
5
|
+
|
6
|
+
If you're like a lot of Rails projects, you use `ActiveRecord`. And like a lot of other Rails projects, you probably call `save!`, `update!`, and `destroy!` on instances of your model throughout your codebase, and probably couldn't easily locate all of the places.
|
7
|
+
|
8
|
+
`ExplicitActiveRecord` exists for users of `ActiveRecord` who want to use `ActiveRecord` more explicitly.
|
9
|
+
|
10
|
+
Today, there are lots of very implicit ways to use `ActiveRecord`.
|
11
|
+
|
12
|
+
Examples:
|
13
|
+
```ruby
|
14
|
+
class MyModel < ActiveRecord::Base; end
|
15
|
+
class MyOtherModel < ActiveRecord::Base
|
16
|
+
has_one :my_model, autosave: true
|
17
|
+
end
|
18
|
+
instance = MyModel.new
|
19
|
+
|
20
|
+
# Lots of different instance methods
|
21
|
+
instance.save!
|
22
|
+
instance.update!
|
23
|
+
|
24
|
+
# Dynamic creation methods off of associations
|
25
|
+
MyOtherModel.new.create_my_model
|
26
|
+
|
27
|
+
# Autosaves "MyModel" association
|
28
|
+
MyOtherModel.new.save!
|
29
|
+
|
30
|
+
# Can reference the class implicitly
|
31
|
+
self.class.create!
|
32
|
+
|
33
|
+
# ... and more!!!
|
34
|
+
```
|
35
|
+
|
36
|
+
This gem gives several APIs to allow you to use `ActiveRecord` more explicitly.
|
37
|
+
|
38
|
+
Note that this gem primarily exists as a way to use `ActiveRecord` explicitly when your current usage is highly implicit. We recommend eventually migrating to Ruby and Rails features once your model is using ActiveRecord explicitly. See the last section for more information.
|
39
|
+
|
40
|
+
## ExplicitActiveRecord::Persistence
|
41
|
+
`ExplicitActiveRecord::Persistence` has a single public method to find all places where persistence events — `create`, `update`, or `destroy` — happen.
|
42
|
+
|
43
|
+
Once your model is configured correctly to use `ExplicitActiveRecord::Persistence`, you will be required to wrap all code that persists your model with code that explicitly declares both the class and instance that is being persisted. See usage for more information.
|
44
|
+
|
45
|
+
### Incremental
|
46
|
+
`ExplicitActiveRecord::Persistence` is *incremental*. This means that you can include `ExplicitActiveRecord::Persistence`, and specify a *non-raising* behavior when the model is persisted implicitly, and use your logs or bug tracking system to find places where the model is implicitly raising, without breaking production. [See the `README` for `deprecation_helper` for more information on this](https://github.com/Gusto/deprecation_helper/blob/main/README.md).
|
47
|
+
|
48
|
+
### Usage
|
49
|
+
The first step is to `include ExplicitActiveRecord::Persistence` in your models:
|
50
|
+
|
51
|
+
Secondly, you'll need to configure `dangerous_update_behaviors` (see the `deprecation_helper` gem for more info).
|
52
|
+
|
53
|
+
All together, this looks like this:
|
54
|
+
```ruby
|
55
|
+
# Example 1
|
56
|
+
class MyModel < ActiveRecord::Base
|
57
|
+
include ExplicitActiveRecord::Persistence
|
58
|
+
self.dangerous_update_behaviors = [DeprecationHelper::Strategies::LogError.new]
|
59
|
+
end
|
60
|
+
|
61
|
+
# Example 2
|
62
|
+
class MyOtherModel < ActiveRecord::Base
|
63
|
+
include ExplicitActiveRecord::Persistence
|
64
|
+
self.dangerous_update_behaviors = [DeprecationHelper::Strategies::RaiseError.new]
|
65
|
+
has_one :my_model, autosave: true
|
66
|
+
end
|
67
|
+
```
|
68
|
+
|
69
|
+
Now you can use the `with_explicit_persistence` method to declare explicitly when the model is being persisted. Here are some examples of supported usages:
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
# You can pass in a single instance of the `MyModel` class:
|
73
|
+
MyModel.with_explicit_persistence_for(instance) do
|
74
|
+
instance.save!
|
75
|
+
end
|
76
|
+
|
77
|
+
# You can pass in multiple instances of the `MyModel` class:
|
78
|
+
MyModel.with_explicit_persistence_for([instance1, instance2]) do
|
79
|
+
instance1.save!
|
80
|
+
instance2.save!
|
81
|
+
end
|
82
|
+
# or
|
83
|
+
instances = [instance1, instance2]
|
84
|
+
MyModel.with_explicit_persistence_for(instances) do
|
85
|
+
instances.destroy_all
|
86
|
+
end
|
87
|
+
```
|
88
|
+
|
89
|
+
Note that you cannot pass in a relation to `with_explicit_persistence_for` — only an instance of or array of instances of the class.
|
90
|
+
|
91
|
+
It is *not* recommended to use this dynamically, such as:
|
92
|
+
```ruby
|
93
|
+
# Don't do this!
|
94
|
+
self.class.with_explicit_persistence_forinstance) do
|
95
|
+
instance.save!
|
96
|
+
end
|
97
|
+
```
|
98
|
+
The reason we do not want to do this is because writing to the class is no longer explicit! Just looking at this code, you cannot tell what class `self.class` is. If `self.class` is `MyModel`, then searching your codebase for `MyModel.with_explicit_persistence_for` will no longer reveal all of the persistence locations.
|
99
|
+
|
100
|
+
### Specifying different dangerous update behavior
|
101
|
+
You can specify multiple behaviors to invoke when the model is updated implicitly/dangerously, as long as that strategy conforms to `DeprecationHelper::Strategies::StrategyInterface` (see `deprecation_helper` for more information).
|
102
|
+
|
103
|
+
Note that by default, `ExplicitActiveRecord` uses the global configuration for `DeprecationHelper`. If your project has already configured `DeprecationHelper`, using:
|
104
|
+
```ruby
|
105
|
+
DeprecationHelper.configure { |config| config.deprecation_strategies = [...] })
|
106
|
+
```
|
107
|
+
then `ExplicitActiveRecord` will use the global configuration.
|
108
|
+
|
109
|
+
### How it works
|
110
|
+
When a client calls `my_model.save!` or `my_model.update!` without using the explicit persistence wrapper, `ExplicitActiveRecord::Persistence` will invoke `DeprecationHelper` with whatever deprecation strategies your model is configured with.
|
111
|
+
|
112
|
+
|
113
|
+
## ExplicitActiveRecord::NoDbAccess
|
114
|
+
`ExplicitActiveRecord::NoDbAccess` has a single public method to restrict the use of the database.
|
115
|
+
|
116
|
+
Once your class or module is configured correctly to use `ExplicitActiveRecord::NoDbAccess`, you will not be able to use the database within the `no_db_access` block.
|
117
|
+
|
118
|
+
### Incremental
|
119
|
+
Note that unlike `ExplicitActiveRecord::Persistence`, `ExplicitActiveRecord::NoDbAccess` is NOT *incremental*. This means that using the DB within one of these blocks *will raise*. If you're interested in allowing `no_db_access` to behave like `Persistence`, please file an issue. It is recommended to use `NoDbAccess` in new code where you are very confident your code should not be using the DB.
|
120
|
+
|
121
|
+
### Usage
|
122
|
+
The first step is to `include ExplicitActiveRecord::NoDbAccess` in your module or class.
|
123
|
+
|
124
|
+
Then, you can use the `no_db_access` block.
|
125
|
+
|
126
|
+
All together, this looks like this:
|
127
|
+
```ruby
|
128
|
+
class MyClass # can also be a module
|
129
|
+
include ExplicitActiveRecord::NoDbAccess
|
130
|
+
|
131
|
+
# Class method
|
132
|
+
def self.my_method
|
133
|
+
no_db_access do
|
134
|
+
# anything that does not use the DB
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# Instance method
|
139
|
+
def my_method
|
140
|
+
self.class.no_db_access do
|
141
|
+
# anything that does not use the DB
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
```
|
146
|
+
|
147
|
+
### How it works
|
148
|
+
`NoDbAccess` uses `ActiveSupport::Notifications` to subscribe to the `sql.active_record` event. This ensures that any DB use via `ActiveRecord` is disallowed.
|
149
|
+
|
150
|
+
# Why you want to use this gem, and why you don't
|
151
|
+
|
152
|
+
There are several native Ruby and Rails features that can be used to match the intent of this gem. `ExplicitActiveRecord` is not a total substitute for these Ruby and Rails primitives, but rather they intend to create a safe way to migrate your codebase to use them.
|
153
|
+
|
154
|
+
This gem adds some verbosity to the use of `ActiveRecord`. More importantly, Ruby offers native primitives for this sort of work. When creating a new rails project or rails engine, it is not recommended to use this gem. Instead, it is recommended to make use of `private_constant`, like this:
|
155
|
+
```ruby
|
156
|
+
module MyModule
|
157
|
+
class MyModel < ActiveRecord::Base
|
158
|
+
end
|
159
|
+
|
160
|
+
private_constant :MyModel
|
161
|
+
end
|
162
|
+
```
|
163
|
+
|
164
|
+
Furthermore, I recommend not returning instances of `MyModel` when a call is made to the public API of `MyModule`. Instead, you can return a [value object](https://martinfowler.com/bliki/ValueObject.html). The important thing here is that by not returning instances of your model or letting unauthorized clients reference it, you can systematically and technically enforce the idea that your model is only persisted in one place.
|
165
|
+
|
166
|
+
Another way to implement this idea using native Rails is by using [Multiple Databases with Active Record](https://guides.rubyonrails.org/active_record_multiple_databases.html). For example, for NoDbAccess, you can simply set up a null database in your application, and switch to it within your block.
|
167
|
+
|
168
|
+
The reason this gem exists is because if your Rails project is like the ones I've seen, you are not doing this from the start. `ExplicitActiveRecord` is *not* a substitute for Ruby primitives like `private_constant` and conceptual primitives like value objects. Those primitives are the end goal, and this gem is just meant to provide a safe and incremental way to get you there.
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# typed: false
|
2
|
+
|
3
|
+
# this is meant to allow callers to write code like this:
|
4
|
+
# model = MyModel.find
|
5
|
+
# no_db_access do
|
6
|
+
# (do things with model...)
|
7
|
+
# end
|
8
|
+
# model.save!
|
9
|
+
# you are guaranteed that no database access will happen within the no_db_access block
|
10
|
+
module ExplicitActiveRecord
|
11
|
+
module NoDBAccess
|
12
|
+
extend T::Sig
|
13
|
+
|
14
|
+
sig { returns(T::Boolean) }
|
15
|
+
def self.in_a_no_db_access_block?
|
16
|
+
(@pure_function_block_counter || 0) > 0
|
17
|
+
end
|
18
|
+
|
19
|
+
sig { void }
|
20
|
+
def self.no_db_access
|
21
|
+
unless @subscribed
|
22
|
+
ActiveSupport::Notifications.subscribe('sql.active_record') do |_name, _start, _finish, _id, _payload|
|
23
|
+
if NoDBAccess.in_a_no_db_access_block?
|
24
|
+
@should_raise = true
|
25
|
+
end
|
26
|
+
end
|
27
|
+
@subscribed = true
|
28
|
+
end
|
29
|
+
|
30
|
+
@pure_function_block_counter = (@pure_function_block_counter || 0) + 1
|
31
|
+
yield
|
32
|
+
|
33
|
+
if @should_raise
|
34
|
+
@should_raise = false
|
35
|
+
raise DbAccessError.new('Cannot execute sql within a no_db_access block.')
|
36
|
+
end
|
37
|
+
ensure
|
38
|
+
@should_raise = false
|
39
|
+
@pure_function_block_counter -= 1
|
40
|
+
end
|
41
|
+
|
42
|
+
sig { params(block: T.proc.void).void }
|
43
|
+
def no_db_access(&block)
|
44
|
+
NoDBAccess.no_db_access(&block)
|
45
|
+
end
|
46
|
+
|
47
|
+
class DbAccessError < StandardError; end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
# typed: false
|
2
|
+
|
3
|
+
require 'sorbet-runtime'
|
4
|
+
require 'active_support/concern'
|
5
|
+
require 'deprecation_helper'
|
6
|
+
|
7
|
+
module ExplicitActiveRecord
|
8
|
+
module Persistence
|
9
|
+
extend T::Sig
|
10
|
+
|
11
|
+
extend ActiveSupport::Concern
|
12
|
+
|
13
|
+
class UnrecognizedDangerousUpdateResponseBehaviorError < StandardError
|
14
|
+
extend T::Sig
|
15
|
+
|
16
|
+
sig { void }
|
17
|
+
def initialize
|
18
|
+
super 'Please provide a behavior that implements DeprecationHelper::Strategies::StrategyBase'
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class InvalidInstanceOfClassError < ArgumentError
|
23
|
+
extend T::Sig
|
24
|
+
|
25
|
+
sig { params(klass: T.class_of(ActiveRecord::Base), instances: T::Array[ActiveRecord::Base]).void }
|
26
|
+
def initialize(klass, instances)
|
27
|
+
super "The provided instances of (#{instances.map(&:class).to_sentence}) are not an instance of the class (#{klass}) extending this concern."
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
included do |base|
|
32
|
+
extend T::Sig
|
33
|
+
|
34
|
+
before_save :ensure_explicitly_persisted!
|
35
|
+
before_destroy :ensure_explicitly_persisted!
|
36
|
+
|
37
|
+
base.class_eval do
|
38
|
+
sig { params(behaviors: T::Array[DeprecationHelper::Strategies::BaseStrategyInterface]).returns(T::Boolean) }
|
39
|
+
def self.dangerous_update_behaviors=(behaviors)
|
40
|
+
if behaviors.all? { |behavior| behavior.is_a?(DeprecationHelper::Strategies::BaseStrategyInterface) }
|
41
|
+
@dangerous_update_behaviors = behaviors
|
42
|
+
true
|
43
|
+
else
|
44
|
+
raise UnrecognizedDangerousUpdateResponseBehaviorError
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
sig { returns(T::Array[DeprecationHelper::Strategies::BaseStrategyInterface]) }
|
49
|
+
def self.dangerous_update_behaviors
|
50
|
+
# By default, `ExplicitActiveRecord` uses whatever the global configuration is for DeprecationHelper.
|
51
|
+
@dangerous_update_behaviors || DeprecationHelper.deprecation_strategies
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
sig { params(instance_or_instances: T.nilable(T.any(ActiveRecord::Base, T::Array[ActiveRecord::Base]))).returns(T.untyped) }
|
56
|
+
def self.with_explicit_persistence_for(instance_or_instances)
|
57
|
+
if instance_or_instances.present?
|
58
|
+
instances = Array.wrap(instance_or_instances)
|
59
|
+
|
60
|
+
invalid_instances = instances.select { |i| i.class != self }
|
61
|
+
if invalid_instances.any?
|
62
|
+
raise InvalidInstanceOfClassError.new(self, invalid_instances)
|
63
|
+
end
|
64
|
+
|
65
|
+
begin
|
66
|
+
instances.each { |model| PermissionStore.permit(model) }
|
67
|
+
ret = yield
|
68
|
+
ensure
|
69
|
+
instances.each { |model| PermissionStore.revoke(model) }
|
70
|
+
end
|
71
|
+
|
72
|
+
ret
|
73
|
+
else
|
74
|
+
yield
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
sig { returns(T.untyped) }
|
79
|
+
def can_be_dangerously_updated
|
80
|
+
@can_be_dangerously_updated
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
sig { returns(T.untyped) }
|
86
|
+
def ensure_explicitly_persisted!
|
87
|
+
return true if PermissionStore.permitted?(self)
|
88
|
+
|
89
|
+
DeprecationHelper.deprecate!(
|
90
|
+
"#{self.class.name} instances should only be persisted explicitly",
|
91
|
+
allow_list: ['bin/rails'],
|
92
|
+
deprecation_strategies: self.class.dangerous_update_behaviors,
|
93
|
+
)
|
94
|
+
|
95
|
+
true
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Note that this should probably be a `private_constant :PermissionStore`
|
100
|
+
# However, some clients are stubbing the methods here because the want to stub
|
101
|
+
# ExplicitActiveRecord in spec.
|
102
|
+
#
|
103
|
+
# TODO: Give a supported API for stubbing explicit persistence and then make this a private constant.
|
104
|
+
#
|
105
|
+
class PermissionStore
|
106
|
+
extend T::Sig
|
107
|
+
|
108
|
+
sig { params(model: ActiveRecord::Base).returns(Array) }
|
109
|
+
def self.permit(model)
|
110
|
+
self.storage.push(model)
|
111
|
+
end
|
112
|
+
|
113
|
+
sig { params(model: ActiveRecord::Base).returns(T.untyped) }
|
114
|
+
def self.revoke(model)
|
115
|
+
idx = self.storage.index(model)
|
116
|
+
self.storage.delete_at(idx) unless idx.nil?
|
117
|
+
end
|
118
|
+
|
119
|
+
sig { params(model: ActiveRecord::Base).returns(T::Boolean) }
|
120
|
+
def self.permitted?(model)
|
121
|
+
model.can_be_dangerously_updated || self.storage.include?(model)
|
122
|
+
end
|
123
|
+
|
124
|
+
sig { returns(Array) }
|
125
|
+
def self.storage
|
126
|
+
@storage ||= []
|
127
|
+
end
|
128
|
+
|
129
|
+
# don't need to construct this class, directly
|
130
|
+
private_class_method :new
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
metadata
ADDED
@@ -0,0 +1,159 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: explicit_activerecord
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Alex Evanczuk
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-02-23 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: sorbet-runtime
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.5.6293
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.5.6293
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: deprecation_helper
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: activerecord
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: activesupport
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: pry
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rspec
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '3.0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '3.0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: sorbet
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 0.5.6293
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 0.5.6293
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: sqlite3
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
description:
|
126
|
+
email:
|
127
|
+
- alex.evanczuk@gusto.com
|
128
|
+
executables: []
|
129
|
+
extensions: []
|
130
|
+
extra_rdoc_files: []
|
131
|
+
files:
|
132
|
+
- LICENSE
|
133
|
+
- README.md
|
134
|
+
- lib/explicit_activerecord.rb
|
135
|
+
- lib/explicit_activerecord/no_db_access.rb
|
136
|
+
- lib/explicit_activerecord/persistence.rb
|
137
|
+
homepage:
|
138
|
+
licenses: []
|
139
|
+
metadata: {}
|
140
|
+
post_install_message:
|
141
|
+
rdoc_options: []
|
142
|
+
require_paths:
|
143
|
+
- lib
|
144
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
145
|
+
requirements:
|
146
|
+
- - ">="
|
147
|
+
- !ruby/object:Gem::Version
|
148
|
+
version: '0'
|
149
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
150
|
+
requirements:
|
151
|
+
- - ">="
|
152
|
+
- !ruby/object:Gem::Version
|
153
|
+
version: '0'
|
154
|
+
requirements: []
|
155
|
+
rubygems_version: 3.0.3
|
156
|
+
signing_key:
|
157
|
+
specification_version: 4
|
158
|
+
summary: This is a gem for using ActiveRecord more explicitly.
|
159
|
+
test_files: []
|