Shiftable
Do your Spaceships belong to Captains, but sometimes a Captain will retire, and you need to reassign the spaceship?
We've all been there. This gem provides structure around the process of "shifting" your records from one associated record to a new record.
Project | Shiftable |
---|---|
name, license, docs | |
version & downloads | |
dependencies & linting | |
unit tests | |
coverage & maintainability | |
resources | |
Spread ~♡ⓛⓞⓥⓔ♡~ | 🌏 👼 💻 🌹 |
Compatibility
Targeted ruby compatibility is non-EOL versions of Ruby, currently 2.6, 2.7, and 3.0, but may work on older Rubies back to 2.0, though it is limited to 2.5 in the gemspec. Feel free to fork if you need something older! Targeted ActiveRecord (Rails not required) compatibility follows the same scheme as Rails Security Issue maintenance policy, currently 6.1, 6.0, 5.2, but it is highly likely that this code will work in any version of ActiveRecord/Rails that runs on Ruby 2+.
Installation
Add this line to your application's Gemfile:
gem "shiftable"
And then execute:
$ bundle install
Or install it yourself as:
$ gem install shiftable
Usage
You are a spaceship captain (who isn't?!) so you have a spaceship (duh!)
class Captain < ActiveRecord::Base
has_one :spaceship
end
Spaceships belong to the Captain.
class Spaceship < ActiveRecord::Base
belongs_to :captain
end
But because you can't afford fuel, and this dystopian future continues to burn carbon, in episode 11, you need to sell the spaceship to your arch-nemesis Captain Sturgle.
class Captain < ActiveRecord::Base
has_one :spaceship
def sell_spaceship_to(nemesis_captain)
Spaceship.shift_single(shift_to: nemesis_captain, shift_from: self)
end
end
But how can you accomplish this? If you used the shiftable
gem, won't take but a line(s) of code...
class Spaceship < ActiveRecord::Base
belongs_to :captain
extend Shiftable::Single.new belongs_to: :captain, has_one: :spaceship, precheck: true,
before_shift: ->(shifting_rel) { shifting_rel.result.ownership_changes += 1 }
end
NOTE: It doesn't matter if the extend occurs before or after the association macro belongs_to
. In fact, it doesn't matter so much that you can even do this...
class Spaceship < ActiveRecord::Base
belongs_to :captain
class << self
include Shiftable::Single.new belongs_to: :captain, has_one: :spaceship, precheck: true,
before_shift: lambda { |shifting:, shift_to:, shift_from:|
shifting.ownership_changes += 1
}
end
end
Single Table Inheritance
This works as you would expect with STI (single table inheritance) classes, i.e. when defined on a subclass, only the records of that class get shifted.
Multiple associations on a single class
What if the captain and the spaceship have a boss... the space federation! And in a run-in with their arch-Nemesis the Plinth-inth, all federation spaceships are commandeered! You are ruined!
class Spaceship < ActiveRecord::Base
belongs_to :space_federation
extend Shiftable::Collection.new belongs_to: :space_federation, has_many: :spaceships,
before_shift: lambda { |shifting_rel|
shifting_rel.each { |spaceship| spaceship.federation_changes += 1 }
}
end
class SpaceFederation < ActiveRecord::Base
has_many :spaceships
def all_spaceships_commandeered_by(nemesis_federation)
Spaceship.shift_cx(shift_to: nemesis_federation, shift_from: self)
end
end
Polymorphism and has_many through
class SpaceTreaty < ActiveRecord::Base
has_many :space_treaty_signature
end
class SpaceTreatySignature < ActiveRecord::Base
belongs_to :space_treaty
belongs_to :signatory, polymorphic: true
# When two space federations assimilate (i.e. merge) to form a single larger federation,
# they become party to (i.e. signatories of) all the treaties that had been signed by either.
# In practical terms, this means:
#
# surviving_federation = SpaceFederation.find(1)
# assimilated_federation = SpaceFederation.find(2)
# SpaceTreatySignature.where(
# signatory_id: assimilated_federation_id,
# signatory_type: "SpaceFederation"
# ).update_all(
# signatory_id: surviving_federation.id
# )
extend Shiftable::Collection.new(
belongs_to: :signatory, has_many: :space_treaty_signature,
polymorphic: { type: "SpaceFederation", as: :signatory },
method_prefix: "space_federation_",
before_shift: lambda { |shifting_rel|
# Each item in shifting_rel is an instance of the class where Shiftable::Collection is defined,
# in this case: SpaceTreatySignature
# And each of them has a signatory which is of type "SpaceFederation",
# because a polymorphic collection only targets one type.
# shifting_rel.each { |signature| signature.signatory == "SpaceFederation" }
}
)
end
class SpaceFederation < ActiveRecord::Base
has_many :space_treaty_signature, as: :signatory
has_many :space_treaties, through: :space_treaty_signatures, as: :signatory
has_many :treaty_planets, class_name: "Planet", through: :space_treaty_signatures, as: :signatory
has_many :treaty_stations, class_name: "SpaceStation", through: :space_treaty_signatures, as: :signatory
def assimilate_from(other_federation)
SpaceTreatySignature.space_federation_shift_pcx(shift_to: self, shift_from: other_federation)
end
end
# Including Planet and SpaceStation, for completeness of the example as the other "types" of polymorphic signatories
class Planet < ActiveRecord::Base
has_many :space_treaty_signature, as: :signatory
has_many :space_treaties, through: :space_treaty_signatures
has_many :treaty_federations, class_name: "SpaceFederation", through: :space_treaty_signatures, as: :signatory
has_many :treaty_stations, class_name: "SpaceStation", through: :space_treaty_signatures, as: :signatory
end
class SpaceStation < ActiveRecord::Base
has_many :space_treaty_signature, as: :signatory
has_many :space_treaties, through: :space_treaty_signatures
has_many :treaty_federations, class_name: "SpaceFederation", through: :space_treaty_signatures, as: :signatory
has_many :treaty_planets, class_name: "Planet", through: :space_treaty_signatures, as: :signatory
end
Wrapping a shift
For example, in a transaction. Let's update the nemesis foundation example from above with a transaction shift_each_wrapper,
which we'll pull from the activerecord-transactionable
gem, which provides best practice framing around transactions.
class Spaceship < ActiveRecord::Base
belongs_to :space_federation
extend Shiftable::Collection.new(
belongs_to: :space_federation,
has_many: :spaceships,
before_shift: lambda { |shifting_rel|
shifting_rel.each { |spaceship| spaceship.federation_changes += 1 }
},
wrapper: {
each: lambda { |rel, record, &block|
tresult = record.transaction_wrapper(outside_rescued_errors: ActiveRecord::RecordNotUnique) do
puts "melon #{record.name} honey #{rel.count}"
block.call # does the actual saving!
end
# NOTE: The value returned by the wrapper will also be returned by the call to `shift_cx`.
# You could return the whole tresult object here, instead of just true/false!
tresult.success?
},
all: lambda { |rel, &block|
tresult = Spaceship.transaction_wrapper do
puts "can you eat #{rel.count} shoes"
block.call
end
tresult.success?
}
}
)
end
class SpaceFederation < ActiveRecord::Base
has_many :spaceships
def all_spaceships_commandeered_by(nemesis_federation)
Spaceship.shift_cx(shift_to: nemesis_federation, shift_from: self)
end
end
Complete example
Putting it all together...
class Captain < ActiveRecord::Base
has_one :spaceship
def sell_spaceship_to(nemesis_captain)
Spaceship.shift_single(shift_to: nemesis_captain, shift_from: self)
end
end
class Spaceship < ActiveRecord::Base
belongs_to :captain
extend Shiftable::Single.new belongs_to: :captain, has_one: :spaceship, precheck: true,
before_shift: ->(shifting_rel) { shifting_rel.result.ownership_changes += 1 }
belongs_to :space_federation
extend Shiftable::Collection.new(
belongs_to: :space_federation,
has_many: :spaceships,
before_shift: lambda { |shifting_rel|
shifting_rel.each { |spaceship| spaceship.federation_changes += 1 }
},
wrapper: {
each: lambda { |rel, record, &block|
tresult = record.transaction_wrapper(outside_rescued_errors: ActiveRecord::RecordNotUnique) do
puts "melon #{record.name} honey #{rel.count}"
block.call # does the actual saving!
end
tresult.success?
},
all: lambda { |rel, &block|
tresult = Spaceship.transaction_wrapper do
puts "can you eat #{rel.count} shoes"
block.call
end
tresult.success?
}
}
)
end
class SpaceFederation < ActiveRecord::Base
has_many :captains
has_many :spaceships
has_many :space_treaty_signature, as: :signatory
has_many :space_treaties, through: :space_treaty_signatures, as: :signatory
has_many :treaty_planets, class_name: "Planet", through: :space_treaty_signatures, as: :signatory
has_many :treaty_stations, class_name: "SpaceStation", through: :space_treaty_signatures, as: :signatory
def assimilate_from(other_federation)
SpaceTreatySignature.space_federation_shift_cx(shift_to: self, shift_from: other_federation)
end
def all_spaceships_commandeered_by(nemesis_federation)
Spaceship.shift_cx(shift_to: nemesis_federation, shift_from: self)
end
end
class SpaceTreaty < ActiveRecord::Base
has_many :space_treaty_signatures
end
class SpaceTreatySignature < ActiveRecord::Base
belongs_to :space_treaty
belongs_to :signatory, polymorphic: true
extend Shiftable::Collection.new(
belongs_to: :signatory, has_many: :space_treaty_signatures,
polymorphic: { type: "SpaceFederation", as: :signatory },
method_prefix: "space_federation_"
)
end
# Including Planet and SpaceStation, for completeness of the example as the other "types" of polymorphic signatories
class Planet < ActiveRecord::Base
has_many :space_treaty_signatures, as: :signatory
has_many :space_treaties, through: :space_treaty_signatures
has_many :treaty_federations, class_name: "SpaceFederation", through: :space_treaty_signatures, as: :signatory
has_many :treaty_stations, class_name: "SpaceStation", through: :space_treaty_signatures, as: :signatory
end
class SpaceStation < ActiveRecord::Base
has_many :space_treaty_signature, as: :signatory
has_many :space_treaties, through: :space_treaty_signatures
has_many :treaty_federations, class_name: "SpaceFederation", through: :space_treaty_signatures, as: :signatory
has_many :treaty_planets, class_name: "Planet", through: :space_treaty_signatures, as: :signatory
end
... stay tuned!
More Information
Code of Conduct
Everyone interacting in the Shiftable project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.
Development
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.
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 the created tag, and push the .gem
file to rubygems.org.
Contributing
See [CONTRIBUTING.md][contributing]
Contributors
Made with contributors-img.
Versioning
This library aims to adhere to Semantic Versioning 2.0.0. Violations of this scheme should be reported as bugs. Specifically, if a minor or patch version is released that breaks backward compatibility, a new version should be immediately released that restores compatibility. Breaking changes to the public API will only be introduced with new major versions.
As a result of this policy, you can (and should) specify a dependency on this gem using the Pessimistic Version Constraint with two digits of precision.
For example:
spec.add_dependency "shiftable", "~> 0.7"
Contact
Author and maintainer is Peter Boling (@pboling).
Comments are welcome in the GitHub Discussions board.
For security-related issues see SECURITY.
License
The gem is available as open source under the terms of the MIT License . See LICENSE for the official Copyright Notice.
- Copyright (c) 2021 Peter H. Boling of Rails Bling