Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/wbotelhos/normalizy
:wrench: Attribute normalizer for Rails
https://github.com/wbotelhos/normalizy
activerecord attributes filter hacktoberfest hacktoberfest2022 normalize normalizer rails typecast
Last synced: 2 months ago
JSON representation
:wrench: Attribute normalizer for Rails
- Host: GitHub
- URL: https://github.com/wbotelhos/normalizy
- Owner: wbotelhos
- License: mit
- Created: 2017-05-16T00:22:00.000Z (over 7 years ago)
- Default Branch: master
- Last Pushed: 2022-05-18T03:55:15.000Z (over 2 years ago)
- Last Synced: 2024-08-09T08:07:51.492Z (5 months ago)
- Topics: activerecord, attributes, filter, hacktoberfest, hacktoberfest2022, normalize, normalizer, rails, typecast
- Language: Ruby
- Homepage:
- Size: 212 KB
- Stars: 19
- Watchers: 3
- Forks: 4
- Open Issues: 3
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Funding: .github/FUNDING.yml
- License: LICENSE
Awesome Lists containing this project
README
# Normalizy
[![CI](https://github.com/wbotelhos/normalizy/workflows/CI/badge.svg)](https://github.com/wbotelhos/normalizy/actions)
[![Gem Version](https://badge.fury.io/rb/normalizy.svg)](https://badge.fury.io/rb/normalizy)
[![Maintainability](https://api.codeclimate.com/v1/badges/3896d0a11bee012c818c/maintainability)](https://codeclimate.com/github/wbotelhos/normalizy/maintainability)
[![codecov](https://codecov.io/gh/wbotelhos/normalizy/branch/master/graph/badge.svg?token=0XTRFDFHDq)](https://codecov.io/gh/wbotelhos/normalizy)
[![Sponsor](https://img.shields.io/badge/sponsor-%3C3-green)](https://www.patreon.com/wbotelhos)Attribute normalizer for Rails.
## Description
If you know the obvious format of an input, why not normalize it instead of raise an validation error to your use? Make the follow email ` [email protected] ` valid like `[email protected]` with no need to override acessors methods.
## install
Add the following code on your `Gemfile` and run `bundle install`:
```ruby
gem 'normalizy'
```So generates an initializer for future custom configurations:
```ruby
rails g normalizy:install
```It will generates a file `config/initializers/normalizy.rb` where you can configure you own normalizer and choose some defaults one.
## Usage
On your model, just add `normalizy` callback with the attribute you want to normalize and the filter to be used:
```ruby
class User < ApplicationRecord
normalizy :name, with: :downcase
end
```Now some email like `[email protected]` will be saved as `[email protected]`.
## Filters
We have a couple of built-in filters.
### Date
Transform a value to date format.
```ruby
normalizy :birthday, with: :date'1984-10-23'
# Tue, 23 Oct 1984 00:00:00 UTC +00:00
```By default, the date is treat as `%F` format and as `UTC` time.
#### format
You can change the format using the `format` options:
```ruby
normalizy :birthday, with: { date: { format: '%y/%m/%d' } }'84/10/23'
# Tue, 23 Oct 1984 00:00:00 UTC +00:00
```#### time zone
To convert the date on your time zone, just provide the `time_zone` option:
```ruby
normalizy :birthday, with: { date: { time_zone: Time.zone } }'1984-10-23'
# Tue, 23 Oct 1984 00:00:00 EDT -04:00
```#### error message
If an invalid date is provided, Normalizy will add an error on attribute of the related object.
You can customize the error via I18n config:```yml
en:
normalizy:
errors:
date:
user:
birthday: '%{value} is an invalid date.'
```If no configuration is provided, the default message will be `'%{value} is an invalid date.`.
#### adjust
If your model receive a `Time` or `DateTime`, you can provide `adjust` options to change you time to begin o the day:
```ruby
normalizy :birthday, with: { date: { adjust: :begin } }Tue, 23 Oct 1984 11:30:00 EDT -04:00
# Tue, 23 Oct 1984 00:00:00 EDT -04:00
```Or to the end of the day:
```ruby
normalizy :birthday, with: { date: { adjust: :end } }Tue, 23 Oct 1984 00:00:00 EDT -04:00
# Tue, 23 Oct 1984 11:59:59 EDT -04:00
```### Money
Transform a value to money format.
```ruby
normalizy :amount, with: :money'$ 42.00'
# '42.00'
```#### separator
The `separator` will be keeped on value to be possible cast the right integer value.
You can change this separator:```ruby
normalizy :amount, with: { money: { separator: ',' } }'R$ 42,00'
# '42,00'
```If you do not want pass it as options, Normalizy will fetch your I18n config:
```yaml
en:
number:
currency:
format:
separator: '.'
```And if it does not exists, `.` will be used as default.
#### type
You can retrieve the value in *cents* format, use the `type` options as `cents`:
```ruby
normalizy :amount, with: { money: { type: :cents } }'$ 42.00'
# '4200'
```#### precision
As you could see on the last example, when using `type: :cents` is important the number of decimal digits.
So, you can configure it to avoid the following error:```ruby
normalizy :amount, with: { money: { type: :cents } }'$ 42.0'
# 420
```When you parse it back, the value need to be divided by `100` to be presented, but it will result in a value you do not want: `4.2` instead of the original `42.0`. Just provide a `precision`:
```ruby
normalizy :amount, with: { money: { precision: 2 } }'$ 42.0'
# 42.00
``````ruby
normalizy :amount, with: { money: { precision: 2, type: :cents } }'$ 42.0'
# 4200
```If you do not want pass it as options, Normalizy will fetch your I18n config:
```yaml
en:
number:
currency:
format:
precision: 2
```And if it does not exists, `2` will be used as default.
#### cast
If you need get a number over a normalized string in a number style, provide `cast` option with desired cast method:
```ruby
normalizy :amount, with: { money: { cast: :to_i } }'$ 42.00'
# 4200
```Just pay attention to avoid to use `type: :cents` together `cast` with float parses.
Since `type` runs first, you will add decimal in a number that already is represented with decimal, but as integer:```ruby
normalizy :amount, with: { money: { cast: :to_f, type: :cents } }'$ 42.00'
# 4200.0
```### Number
Transform text to valid number.
```ruby
normalizy :age, with: :number' 32x'
# '32'
```If you want cast the value, provide `cast` option with desired cast method:
```ruby
normalizy :age, with: { number: { cast: :to_i } }' 32x'
# 32
```### Percent
Transform a value to a valid percent format.
```ruby
normalizy :amount, with: :percent'42.00 %'
# '42.00'
```#### separator
The `separator` will be keeped on value to be possible cast the right integer value.
You can change this separator:```ruby
normalizy :amount, with: { percent: { separator: ',' } }'42,00 %'
# '42,00'
```If you do not want pass it as options, Normalizy will fetch your I18n config:
```yaml
en:
number:
percentage:
format:
separator: '.'
```And if it does not exists, `.` will be used as default.
#### type
You can retrieve the value in *integer* format, use the `type` options as `integer`:
```ruby
normalizy :amount, with: { percent: { type: :integer } }'42.00 %'
# '4200'
```#### precision
As you could see on the last example, when using `type: :integer` is important the number of decimal digits.
So, you can configure it to avoid the following error:```ruby
normalizy :amount, with: { percent: { type: :integer } }'42.0 %'
# 420
```When you parse it back, the value need to be divided by `100` to be presented, but it will result in a value you do not want: `4.2` instead of the original `42.0`. Just provide a `precision`:
```ruby
normalizy :amount, with: { percent: { precision: 2 } }'42.0 %'
# 42.00
``````ruby
normalizy :amount, with: { percent: { precision: 2, type: :integer } }'42.0 %'
# 4200
```If you do not want pass it as options, Normalizy will fetch your I18n config:
```yaml
en:
number:
percentage:
format:
separator: 2
```And if it does not exists, `2` will be used as default.
#### cast
If you need get a number over a normalized string in a number style, provide `cast` option with desired cast method:
```ruby
normalizy :amount, with: { percent: { cast: :to_i } }'42.00 %'
# 4200
```Just pay attention to avoid to use `type: :integer` together `cast` with float parses.
Since `type` runs first, you will add decimal in a number that already is represented with decimal, but as integer:```ruby
normalizy :amount, with: { percent: { cast: :to_f, type: :integer } }'42.00 %'
# 4200.0
```### Slug
Convert texto to slug.
```ruby
normalizy :slug, with: :slug
'Washington é Botelho'
# 'washington-e-botelho'
```#### to
You can slug a field based on other just sending the result value.
```ruby
normalizy :title, with: { slug: { to: :slug } }model.title = 'Washington é Botelho'
model.slug
# 'washington-e-botelho'
```### Strip
Cleans edge spaces.
Options:
- `side`: `:left`, `:right` or `:both`. Default: `:both`
```ruby
normalizy :name, with: :strip
' Washington Botelho '
# 'Washington Botelho'
``````ruby
normalizy :name, with: { strip: { side: :left } }
' Washington Botelho '
# 'Washington Botelho '
``````ruby
normalizy :name, with: { strip: { side: :right } }
' Washington Botelho '
# ' Washington Botelho'
``````ruby
normalizy :name, with: { strip: { side: :both } }
' Washington Botelho '
# 'Washington Botelho'
```As you can see, the rules can be passed as Symbol/String or as Hash if it has options.
### Truncate
Remove excedent string part from a gived limit.
```ruby
normalizy :description, with: { truncate: { limit: 10 } }'Once upon a time in a world far far away'
# 'Once upon '
```## Multiple Filters
You can normalize with a couple of filters at once:
```ruby
normalizy :name, with: { %i[squish titleize] }
' washington botelho '
# 'Washington Botelho'
```## Multiple Attributes
You can normalize more than one attribute at once too, with one or multiple filters:
```ruby
normalizy :email, :username, with: :downcase
```Of course you can declare multiple attribute and multiple filters, either.
It is possible to make sequential normalizy calls, but *take care*!
Since we use `prepend` module the last line will run first then others:```ruby
normalizy :username, with: :downcase
normalizy :username, with: :titleize'BoteLho'
# 'bote lho'
```As you can see, `titleize` runs first then `downcase`.
Each line will be evaluated from the *bottom* to the *top*.
If it is hard to you accept, use [Muiltiple Filters](#multiple-filters)## Default Filters
You can configure some default filters to be runned.
Edit initializer at `config/initializers/normalizy.rb`:```ruby
Normalizy.configure do |config|
config.default_filters = [:squish]
end
```Now, all normalization will include `squish`, even when no rule is declared.
```ruby
normalizy :name
" Washington \n Botelho "
# 'Washington Botelho'
```If you declare some filter, the default filter `squish` will be runned together:
```ruby
normalizy :name, with: :downcase
' washington botelho '
# 'Washington Botelho'
```## Custom Filter
You can create a custom filter that implements `call` method with an `input` as argument and an optional `options`:
```ruby
module Normalizy
module Filters
module Blacklist
def self.call(input)
input.gsub 'Fuck', replacement: '***'
end
end
end
end
``````ruby
Normalizy.configure do |config|
config.add :blacklist, Normalizy::Filters::Blacklist
end
```Now you can use your custom filter:
```ruby
normalizy :name, with: :blacklist'Washington Fuck Botelho'
# 'Washington *** Botelho'
```#### options
If you want to pass options to your filter, just call it as a hash and the value will be send to the custom filter:
```ruby
module Normalizy
module Filters
module Blacklist
def self.call(input, options: {})
input.gsub 'Fuck', replacement: options[:replacement]
end
end
end
end
``````ruby
normalizy :name, with: { blacklist: { replacement: '---' } }'Washington Fuck Botelho'
# 'Washington --- Botelho'
```### options value
By default, Modules and instance methods of class will receveis the following attributes on `options` argument:
- `object`: The object that Normalizy is acting;
- `attribute`: The attribute of the object that Normalizy is acting.You can pass a block and it will be received on filter:
```ruby
module Normalizy
module Filters
module Blacklist
def self.call(input, options: {})
value = input.gsub('Fuck', 'filtered')value = yield(value) if block_given?
value
end
end
end
end
``````ruby
normalizy :name, with: { :blacklist, &->(value) { value.sub('filtered', '(filtered 2x)') } }'Washington Fuck Botelho'
# 'Washington (filtered 2x) Botelho'
```## Method Filters
If a built-in filter is not found, Normalizy will try to find a method in the current class.
```ruby
normalizy :birthday, with: :parse_datedef parse_date(input)
Time.zone.parse(input).strftime '%Y/%m/%d'
end'1984-10-23'
# '1984/10/23'
```If you gives an option, it will be passed to the function:
```ruby
normalizy :birthday, with: { parse_date: { format: '%Y/%m/%d' }def parse_date(input, options = {})
Time.zone.parse(input).strftime options[:format]
end'1984-10-23'
# '1984/10/23'
```Block methods works here either.
## Native Filter
After the missing built-in and class method, the fallback will be the value of native methods.
```ruby
normalizy :name, with: :reverse'Washington Botelho'
# "ohletoB notgnihsaW"
```## Inline Filter
Maybe you want to declare an inline filter, in this case, just use a Lambda or Proc:
```ruby
normalizy :age, with: ->(input) { input.to_i.abs }-32
# 32
```You can use it on filters declaration too:
```ruby
Normalizy.configure do |config|
config.add :age, ->(input) { input.to_i.abs }
end
```## Alias
Sometimes you want to give a better name to your filter, just to keep the things semantic.
Duplicates the code, as you know, is not a good idea, so, create an alias:```ruby
Normalizy.configure do |config|
config.alias :age, :number
end
```Now, `age` will delegate to `number` filter.
And now, the aliased filter will work fine:
```ruby
normalizy :age, with: :age'= 42'
# 42
```If you need to alias multiple filters, just provide an array of them:
```ruby
Normalizy.configure do |config|
config.alias :username, %i[squish downcase]
end
```Alias accepts options parameters too:
```ruby
Normalizy.configure do |config|
config.alias :left_trim, trim: { side: :left }
end
```## RSpec
If you use [RSpec](http://rspec.info), we did built-in matchers for you.
Add the following code to your `rails_helper.rb````ruby
RSpec.configure do |config|
config.include Normalizy::RSpec
end
```And now you can use some of the matchers:
##### Result Matcher
```ruby
it { is_expected.to normalizy(:email).from(' [email protected] ').to '[email protected]' }
```##### Filter Matcher
It will match the given filter literally:
```ruby
it { is_expected.to normalizy(:email).with :downcase }
``````ruby
it { is_expected.to normalizy(:email).with %i[downcase squish] }
``````ruby
it { is_expected.to normalizy(:email).with(trim: { side: :left }) }
```