This is a collection of mixins, helpers and classes that cover several aspects of a ruby on rails application, such as models, controllers and job processing. At Getsafe, we run multiple Ruby on Rails apps as part of our insurance infrastructure and we found that we wrote quite some shared helpers that are duplicated among applications and serve a generic purpose that we could share in this gem. Look at it as our version of ActiveSupport (which is amazing! ❤️), dropping in the pieces we sometimes miss in the beautiful puzzle of Rails.
Add this line to your application's Gemfile:
gem 'nxt_support'
And then execute:
$ bundle
Or install it yourself as:
$ gem install nxt_support
Here's an overview all the supporting features.
A Rack middleware that adds a Sentry-Error-ID
header to 5xx responses.
The header is only added if an error was reported during the request.
The error ID is gotten from sentry.error_event_id
in the Rack env).
You can then visit https://<org-slug>.sentry.io/issues/?query=<error-event-id>
to go directly to the error (it may not show up immediately).
Note that this middleware must be inserted before Sentry's own middleware.
You can run rails middleware
to verify the order of your registered middleware.
config.middleware.insert_before 0, NxtSupport::Middleware::SentryErrorID
Enjoy support for your models.
This class collects useful tools around working with email addresses. Use NxtSupport::Email::REGEXP
to match email address strings. See the sources for a list of criteria it validates.
This mixin provides the indifferently_accessible_json_attrs
class method which serializes and deserializes JSON database columns with ActiveSupport::HashWithIndifferentAccess
instead of Hash
.
class MyModel < ApplicationRecord
include IndifferentlyAccessibleJsonAttrs
indifferently_accessible_json_attrs :data
end
The NxtSupport::Models::SafelyFindOrCreateable
concern is aimed at ActiveRecord models with a uniqueness database constraint. If you use find_or_create_by
from ActiveRecord, it can happen that the find_by
call returns nil
(because no record for the given conditions exists), but in the small timeframe between the find_by
and the create
call, another thread inserts a record, so that the create
call raises an error.
The safely_find_or_create_by
method provided by this concern catches such an error and performs find_by
another time. It also offers a bang variant.
class Book < ApplicationRecord
include NxtSupport::SafelyFindOrCreateable
end
Book.safely_find_or_create_by!(market: 'de', title: 'Moche!')
The NxtSupport::AssignableValues
allows you to restrict the possible values for your ActiveRecord model attributes. These values will only be checked on new records or if the attribute in question has been changed, so existing records will not be affected even if they contain invalid values.
class Book < ApplicationRecord
include NxtSupport::AssignableValues
# You can use a block
assignable_values_for :genre do
%w[fantasy adventure crime historical]
end
# Or you can also use an array argument
assignable_values_for :genre, %w[fantasy adventure crime historical]
end
book = Book.new(genre: 'fantasy', title: 'Moche!')
book.valid? #=> true
book.genre = 'comedy'
book.valid? #=> false
book.valid_genres #=> ["fantasy", "adventure", "crime", "historical"]
A default can be included
assignable_values_for :genre, default: 'crime' do
%w[fantasy adventure crime historical]
end
book = Book.new(title: 'Moche!')
book.genre #=> crime
If the default value is not in the list of assignable values, then validation will fail.
This mixin provides the preprocess_attributes
class method which can preprocess columns before saving by either stripping whitespace, downcasing, or both.
class Book < ApplicationRecord
include NxtSupport::PreprocessAttributes
preprocess_attributes :title, :author, preprocessors: %i[strip downcase]
end
book = Book.new(title: ' Moche!')
book.save
book.title #=> "moche!"
Register a custom preprocesser:
NxtSupport::Preprocessor.register(:compress, CompressPreprocessor)
Also works with non-string columns
NxtSupport::Preprocessor.register(:add_one, AddOnePreprocessor, :integer)
class Book < ApplicationRecord
include NxtSupport::PreprocessAttributes
preprocess_attributes :views, preprocessors: %i[add_one], column_type: :integer
end
book = Book.new(views: 1000)
book.save
book.views #=> "1001"
This mixin provides the accessors for ActiveSupport::Duration
attributes.
Imagine you have a database table that needs to store some kind of duration data:
ActiveRecord::Schema.define do
create_table :courses, force: true do |t|
t.string :class_duration
t.string :topic_duration
t.string :total_duration
end
end
And an appropriate model:
class Course < ActiveRecord::Base
include NxtSupport::DurationAttributeAccessor
duration_attribute_accessor :class_duration, :topic_duration, :total_duration
end
You can then pass the ActiveSupport::Duration
objects as arguments and get them back:
course = Course.new(
class_duration: 1.hour,
topic_duration: 1.month,
total_duration: 1.year
)
# Fields persist in the database as ISO8601-compliant strings
course # => #<#<Course:0x00007f8bcf2bcfc0>:0x00007f8bcf2c7920 id: nil, class_duration: "PT1H", topic_duration: "P1M", total_duration: "P1Y">
course.class_duration # => 1 hour
course.topic_duration # => 1 month
course.total_duration # => 1 year
Enjoy mixins for your serializers.
This mixin provides your serializer classes with a attribute_as_iso8601
and a attributes_as_iso8601
method. They behave almost the same as the attribute
method of active_model_serializers (in fact they call it behind the scenes), but they convert the values of the given attributes to an ISO8601 string. This is useful for Date
, Time
or ActiveSupport::Duration
values.
class MySerializer < ActiveModel::Serializer
include NxtSupport::HasTimeAttributes
attributes_as_iso8601 :created_at, :updated_at
end
Enjoy some useful utilities
NxtSupport::Enum
provides a simple interface to organize enums.
Values will be normalized to be underscore and downcase and access with [] is raising a KeyError
in case there is
no value for the key. There are also normalized readers for all your values.
class Book
STATES = NxtSupport::Enum['draft', 'revised', 'Published', 'in weird State']
end
Book::STATES[:draft] # 'draft'
Book::STATES.draft # 'draft'
Book::STATES['revised'] # 'revised'
Book::STATES.revised # 'revised'
Book::STATES['published'] # 'Published'
Book::STATES.published # 'Published'
Book::STATES['Published'] # KeyError
Book::STATES['in_weird_state'] # 'in weird State'
Book::STATES.in_weird_state # 'in weird State'
NxtSupport::HashTranslator
is a module that allows you to manipulate keys and values of original hash through tuple hash.
Tuple hash is a hash where key
- represent's the key of original hash, and value
- represents the key of result hash.
Use #translate_hash
method to get the result hash.
class TestClass
include NxtSupport::HashTranslator
end
TestClass.translate_hash(firstname: 'John', firstname: :first_name)
=> { 'first_name' => 'John' }
The value
also could be a Hash
where key represents the new key in result hash and value must be a lambda or Proc
that would be used to process value from origin hash. If the tuple hash contains more than 1 key-value pairs or value in key value pair
is not a callable block InvalidTranslationArgument
error would be raised.
class TestClass
include NxtSupport::HashTranslator
end
hash = { firstname: 'John', phonenumber: '11-22-33-445' }
tuple = {
firstname: :first_name,
phonenumber: {
phone_number: ->(number) { number.to_s.prepend('+49-') }
}
}
TestClass.translate_hash(hash, tuple)
=> { 'first_name' => 'John', 'phone_number' => '+49-11-22-33-445' }
hash = { firstname: 'John', phonenumber: '11-22-33-445' }
tuple = {
firstname: :first_name,
phonenumber: {
phone_number: :some_symbol
}
}
TestClass.translate_hash(hash, tuple)
=> InvalidTranslationArgument (some_symbol must be a callable block!)
hash = { firstname: 'John', phonenumber: '11-22-33-445' }
tuple = {
firstname: :first_name,
phonenumber: {
phone_number: ->(number) { number.to_s.prepend('+49-') },
unused_key: :some_symbol
}
}
TestClass.translate_hash(hash, tuple)
=> InvalidTranslationArgument ({:phone_number=>#<Proc:0x00007ff503175b88 (lambda), :unused_key=>:some_symbol} must contain only 1 key-value pair!)
Or an Array
.
class TestClass
include NxtSupport::HashTranslator
end
hash = { firstname: 'John', lastname: 'Doe' }
tuple = { firstname: :first_name, lastname: [:last_name, :maiden_name] }
TestClass.translate_hash(hash, tuple)
=> { 'first_name' => 'John', 'last_name' => 'Doe', 'maiden_name' => 'Doe' }
NxtSupport::Crystalizer
crystallizes a shared value from an array of elements and screams in case the value is not
the same across the collection. This is useful in a scenario where you want to guarantee that certain objects share the same
attribute. Let's say you want to ensure that all users in your collection reference the same department, then the idea
is that you can crystallize the department from your collection.
NxtSupport::Crystalizer.new(collection: ['andy', 'andy']).call # => 'andy'
NxtSupport::Crystalizer.new(collection: []).call # NxtSupport::Crystalizer::Error
NxtSupport::Crystalizer.new(collection: ['andy', 'scotty']).call # NxtSupport::Crystalizer::Error
or using the refinement:
using NxtSupport::Refinements::Crystalizer
['andy', 'andy'].crystalize # => 'andy'
Note that for Ruby versions < 3.0 it only refines the class Array
instead of the module Enumerable
.
You can also specify the method to be checked and returned:
# Return `.effective_at` or error if `.effective_at`s are different
NxtSupport::Crystalizer.new(collection: insurances, with: :effective_at).call
NxtSupport::Crystalizer.new(collection: insurances, with: ->(i){ i.effective_at }).call
insurances.crystalize(with: :effective_at)
The crystallizer raises a NxtSupport::Crystalizer::Error
if the values are not the same, but you can override this:
insurances.crystalize(with: :effective_at) do |effective_ats|
raise SomethingsWrong, "Insurances don't start on the same date"
end
NxtSupport::BirthDate
takes a date and provides some convenience methods related to age.
NxtSupport::BirthDate.new(date: '1990-08-08').to_age # => 30
NxtSupport::BirthDate.new(date: '1990-08-08').to_age_in_months # => 361
A simple utility that uses Ruby's OptionParser to make it easier to pass CLI options to Rake tasks.
Call NxtSupport::Console.rake_cli_options
with a block. Use the option
method to define options.
It takes one required argument (the flag syntax), and optionally the data type and a default value.
Options which are not booleans must end with an =
.
task my_task: :environment do
opts = NxtSupport::Console.rake_cli_options do
option '--simulate', default: false
option '--contract_numbers=', Array, default: []
option '--limit=', Integer
option '--effective_at='
end
do_stuff_with opts
end
A --
must be passed after the Rake task name and any Rake-specific options,
before our custom task options. (With heroku run
, you'll also need a second --
).
rake my_task -- --simulate --contract_numbers=123,456 --effective_at=2022-01-02 --limit=20
# {:contract_numbers=>["123", "456"], :simulate=>true, :effective_at=>"2022-01-02", :limit=>20}
On Heroku (note the second --
):
heroku run rake my_task -- -- --simulate --contract_numbers=123,456 --effective_at=2022-01-02 --limit=20
If any options are not passed, they'll be replaced with their defaults. If no default was specified, they will not be present in the returned hash.
rake my_task -- --effective_at=2022-10-13
# {:simulate=>false, :effective_at=>"2022-10-13", :contract_numbers=>[]}
If you specify an option which was not defined, the command will exit with an error:
rake my_task -- --effective_at=2022-10-13 --testing
# invalid option: --testing
- Short arguments aren't supported (
-l
as a shortcut for--limit
). - On the command line, options with values must be passed with an
=
at the end.--effective_at 2022-10-13
will not work.
Enjoy your service objects.
NxtSupport::Services::Base
gives you some convenient sugar for service objects.
Instead of:
WeatherFetcher.new(location: 'Heidelberg').call
You can use:
WeatherFetcher.call(location: 'Heidelberg')
You have to include NxtSupport::Services::Base
in your service:
class WeatherFetcher
include NxtSupport::Services::Base
def call
'Getting the weather..'
end
end
Use it:
WeatherFetcher.call # => 'Getting the weather..'
If you implement a method other than #call
, you can use #class_interface :your_method
class HomeBuilder
include NxtSupport::Services::Base
include NxtInit
attr_init :width, :length, :height, :roof_type
class_interface :build
def build
"Building #{width}x#{length}x#{height} house with a #{roof_type} roof"
end
end
Use it:
HomeBuilder.build(width: 20, length: 40, height: 15, roof_type: :pitched)
# => Building 20x40x15 house with a pitched roof
After checking out the repo, run bin/setup
to install dependencies. Then, run rake test
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
.
First, if you don't want to always log in with your RubyGems password, you can create an API key from the web, and then:
bundle config set gem.push_key rubygems
Add to ~/.gem/credentials
(create if it doesn't exist):
:rubygems: <your Rubygems API key>
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.
Bug reports and pull requests are welcome on GitHub at https://github.com/nxt-insurance/nxt_support.
Use this command to push the tag to the GitHub Releases page as well as to RubyGems:
bin/release
For pushing to rubygems:
- Make sure you have ownership rights on RubyGems
- Release the new gem version
bundle exec rake release
The gem is available as open source under the terms of the MIT License.