activerecord-transactionable 0.1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 134ddc5a2d594c0ddf785ff45ff5ec2661f0660e
4
+ data.tar.gz: 3fb2afbaa552fbec81c70bb7824874646efacfde
5
+ SHA512:
6
+ metadata.gz: 78d23d187b037feb977342e6120d001c92699b7db37dfd4cb953bd4add4dcc91762f822c359ce841dd3725d6c6232fcfe375d4b2f99f61415a9fc9aabff9e5a7
7
+ data.tar.gz: 71d8ae5296a058dca0ff90c746b15320824556c6fde806abcdf3e01e3556e7b8a4b1e09e438bec82473222c3b60631525e9b67632f275317122055b872b49590
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1.2
4
+ before_install: gem install bundler -v 1.11.2
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # So the gem can run the simple test suite against the raw bundled gems without the complex BUNDLE_GEMFILE setup
4
+ gem "sqlite3", :platforms => [:ruby]
5
+
6
+ # Specify your gem's dependencies in activerecord-transactionable.gemspec
7
+ gemspec
data/README.md ADDED
@@ -0,0 +1,48 @@
1
+ # Activerecord::Transactionable
2
+
3
+ Provides a method, `transaction_wrapper` at the class and instance levels that can be used instead of `ActiveRecord#transaction`.
4
+
5
+ Useful as an example of correct behavior for wrapping transactions.
6
+
7
+ NOTE: Rails' transactions are per-database connection, not per-model, nor per-instance,
8
+ see: http://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html
9
+
10
+ ## Installation
11
+
12
+ Add this line to your application's Gemfile:
13
+
14
+ ```ruby
15
+ gem 'activerecord-transactionable'
16
+ ```
17
+
18
+ And then execute:
19
+
20
+ $ bundle
21
+
22
+ Or install it yourself as:
23
+
24
+ $ gem install activerecord-transactionable
25
+
26
+ ## Usage
27
+
28
+ When creating, saving, deleting within the transaction make sure to use the bang methods (`!`) in order to ensure a rollback on failure.
29
+
30
+ ```
31
+ car = Car.new(name: "Fiesta")
32
+ car.transaction_wrapper do
33
+ car.save!
34
+ end
35
+ ```
36
+
37
+ Also see the specs.
38
+
39
+ ## Development
40
+
41
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
42
+
43
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
44
+
45
+ ## Contributing
46
+
47
+ Bug reports and pull requests are welcome on GitHub at https://github.com/pboling/activerecord-transactionable.
48
+
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'activerecord/transactionable/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "activerecord-transactionable"
8
+ spec.version = Activerecord::Transactionable::VERSION
9
+ spec.authors = ["Peter Boling"]
10
+ spec.email = ["peter.boling@gmail.com"]
11
+ spec.licenses = ["MIT"]
12
+
13
+ spec.summary = %q{Do ActiveRecord transactions the right way.}
14
+ spec.description = %q{Getting transactions right is hard, and this gem makes it easier.}
15
+ spec.homepage = "http://www.railsbling.com/tags/activerecord-transactionable"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = "exe"
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_dependency "activemodel"
23
+ spec.add_dependency "activerecord"
24
+
25
+ spec.add_development_dependency "bundler", "~> 1.11"
26
+ spec.add_development_dependency "rake", "~> 10.0"
27
+ spec.add_development_dependency "rspec", "~> 3.4"
28
+ end
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "activerecord/transactionable"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,83 @@
1
+ require "activerecord/transactionable/version"
2
+ require "active_model"
3
+ require "active_record"
4
+
5
+ module Activerecord
6
+ # SRP: Provides an example of correct behavior for wrapping transactions.
7
+ # NOTE: Rails' transactions are per-database connection, not per-model, nor per-instance,
8
+ # see: http://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html
9
+ module Transactionable
10
+ extend ActiveSupport::Concern
11
+
12
+ DEFAULT_ERRORS_TO_HANDLE = [ActiveRecord::RecordInvalid]
13
+ DEFAULT_ERRORS_WHICH_PREPARE_ERRORS_ON_SELF = [ActiveRecord::RecordInvalid]
14
+
15
+ def transaction_wrapper(**args)
16
+ self.class.transaction_wrapper(object: self, **args) do
17
+ yield
18
+ end
19
+ end
20
+
21
+ module ClassMethods
22
+ def transaction_wrapper(object: nil, **args)
23
+ rescued_errors = Array(args[:rescued_errors])
24
+ prepared_errors = Array(args[:prepared_errors])
25
+ retriable_errors = Array(args[:retriable_errors])
26
+ reraisable_errors = Array(args[:reraisable_errors])
27
+ rescued_errors.concat(DEFAULT_ERRORS_TO_HANDLE)
28
+ prepared_errors.concat(DEFAULT_ERRORS_WHICH_PREPARE_ERRORS_ON_SELF)
29
+ already_been_added_to_self, needing_added_to_self = rescued_errors.partition {|error_class| prepared_errors.include?(error_class)}
30
+ re_try = false
31
+ begin
32
+ ActiveRecord::Base.transaction do
33
+ yield
34
+ end
35
+ true # <= make the return value meaningful. Meaning is either: transaction succeeded, OR raised ActiveRecord::Rollback
36
+ rescue *reraisable_errors => error
37
+ # This has highest precedence because raising is the most critical functionality of a raised error to keep
38
+ # if that is in the intended behavior, and this way a specific child of StandardError can be reraised while
39
+ # the parent can still be caught and added to self.errors
40
+ # Also adds the error to the object if there is an object.
41
+ transaction_error_logger(object: object, error: error, add_to: nil, additional_message: " [re-raising!]")
42
+ raise error
43
+ rescue *retriable_errors => error
44
+ # This will re-run the begin block above
45
+ # WARNING: If the same error keeps getting thrown this would infinitely recurse!
46
+ # To avoid the infinite recursion, we track the retry state
47
+ if re_try
48
+ transaction_error_logger(object: object, error: error, additional_message: " [2nd attempt]")
49
+ false # <= make the return value meaningful. Meaning is: transaction failed after two attempts
50
+ else
51
+ re_try = true
52
+ # Not adding error to base when retrying, because otherwise the added error may
53
+ # prevent the subsequent save from working, in a catch-22
54
+ transaction_error_logger(object: object, error: error, add_to: nil, additional_message: " [1st attempt]")
55
+ retry
56
+ end
57
+ rescue *already_been_added_to_self => error
58
+ # ActiveRecord::RecordInvalid, when done correctly, will have already added the error to object.
59
+ transaction_error_logger(object: nil, error: error, additional_message: nil)
60
+ false # <= make the return value meaningful. Meaning is: transaction failed
61
+ rescue *needing_added_to_self => error
62
+ transaction_error_logger(object: object, error: error, additional_message: nil)
63
+ false # <= make the return value meaningful. Meaning is: transaction failed
64
+ end
65
+ end
66
+
67
+ def transaction_error_logger(object:, error:, add_to: :base, additional_message: nil)
68
+ # Ruby arguments, like object, are passed by reference,
69
+ # so this update to errors will be available to the caller
70
+ if object.nil?
71
+ # when a transaction wraps a bunch of CRUD actions,
72
+ # the specific record that caused the ActiveRecord::RecordInvalid error may be out of scope
73
+ # Ideally you would rewrite the caller to call transaction_wrapper on a single record (even if updates happen on other records)
74
+ logger.error("[#{self}.transaction_wrapper] #{error.class}: #{error.message}#{additional_message}")
75
+ else
76
+ logger.error("[#{self}.transaction_wrapper] On #{object.class} #{error.class}: #{error.message}#{additional_message}")
77
+ object.errors.add(add_to, error.message) unless add_to.nil?
78
+ end
79
+ end
80
+ end
81
+
82
+ end
83
+ end
@@ -0,0 +1,5 @@
1
+ module Activerecord
2
+ module Transactionable
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
metadata ADDED
@@ -0,0 +1,125 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: activerecord-transactionable
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Peter Boling
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-03-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activemodel
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activerecord
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: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.11'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.11'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '10.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '10.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.4'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.4'
83
+ description: Getting transactions right is hard, and this gem makes it easier.
84
+ email:
85
+ - peter.boling@gmail.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - ".gitignore"
91
+ - ".rspec"
92
+ - ".travis.yml"
93
+ - Gemfile
94
+ - README.md
95
+ - Rakefile
96
+ - activerecord-transactionable.gemspec
97
+ - bin/console
98
+ - bin/setup
99
+ - lib/activerecord/transactionable.rb
100
+ - lib/activerecord/transactionable/version.rb
101
+ homepage: http://www.railsbling.com/tags/activerecord-transactionable
102
+ licenses:
103
+ - MIT
104
+ metadata: {}
105
+ post_install_message:
106
+ rdoc_options: []
107
+ require_paths:
108
+ - lib
109
+ required_ruby_version: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - ">="
112
+ - !ruby/object:Gem::Version
113
+ version: '0'
114
+ required_rubygems_version: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - ">="
117
+ - !ruby/object:Gem::Version
118
+ version: '0'
119
+ requirements: []
120
+ rubyforge_project:
121
+ rubygems_version: 2.4.8
122
+ signing_key:
123
+ specification_version: 4
124
+ summary: Do ActiveRecord transactions the right way.
125
+ test_files: []