Giter Site home page Giter Site logo

normalizy's Introduction

Normalizy

CI Gem Version Maintainability codecov Sponsor

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:

gem 'normalizy'

So generates an initializer for future custom configurations:

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:

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.

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:

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:

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:

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:

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:

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.

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:

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:

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:

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:

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:

normalizy :amount, with: { money: { precision: 2 } }

'$ 42.0'
# 42.00
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:

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:

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:

normalizy :amount, with: { money: { cast: :to_f, type: :cents } }

'$ 42.00'
# 4200.0

Number

Transform text to valid number.

normalizy :age, with: :number

' 32x'
# '32'

If you want cast the value, provide cast option with desired cast method:

normalizy :age, with: { number: { cast: :to_i } }

' 32x'
# 32

Percent

Transform a value to a valid percent format.

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:

normalizy :amount, with: { percent: { separator: ',' } }

'42,00 %'
# '42,00'

If you do not want pass it as options, Normalizy will fetch your I18n config:

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:

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:

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:

normalizy :amount, with: { percent: { precision: 2 } }

'42.0 %'
# 42.00
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:

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:

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:

normalizy :amount, with: { percent: { cast: :to_f, type: :integer } }

'42.00 %'
# 4200.0

Slug

Convert texto to slug.

normalizy :slug, with: :slug
'Washington é Botelho'
# 'washington-e-botelho'

to

You can slug a field based on other just sending the result value.

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
normalizy :name, with: :strip
'  Washington  Botelho  '
# 'Washington  Botelho'
normalizy :name, with: { strip: { side: :left } }
'  Washington  Botelho  '
# 'Washington  Botelho  '
normalizy :name, with: { strip: { side: :right } }
'  Washington  Botelho  '
# '  Washington  Botelho'
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.

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:

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:

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:

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

Default Filters

You can configure some default filters to be runned. Edit initializer at config/initializers/normalizy.rb:

Normalizy.configure do |config|
  config.default_filters = [:squish]
end

Now, all normalization will include squish, even when no rule is declared.

normalizy :name
"  Washington  \n  Botelho  "
# 'Washington Botelho'

If you declare some filter, the default filter squish will be runned together:

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:

module Normalizy
  module Filters
    module Blacklist
      def self.call(input)
        input.gsub 'Fuck', replacement: '***'
      end
    end
  end
end
Normalizy.configure do |config|
  config.add :blacklist, Normalizy::Filters::Blacklist
end

Now you can use your custom filter:

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:

module Normalizy
  module Filters
    module Blacklist
      def self.call(input, options: {})
        input.gsub 'Fuck', replacement: options[:replacement]
      end
    end
  end
end
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:

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
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.

normalizy :birthday, with: :parse_date

def 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:

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.

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:

normalizy :age, with: ->(input) { input.to_i.abs }

-32
# 32

You can use it on filters declaration too:

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:

Normalizy.configure do |config|
  config.alias :age, :number
end

Now, age will delegate to number filter.

And now, the aliased filter will work fine:

normalizy :age, with: :age

'= 42'
# 42

If you need to alias multiple filters, just provide an array of them:

Normalizy.configure do |config|
  config.alias :username, %i[squish downcase]
end

Alias accepts options parameters too:

Normalizy.configure do |config|
  config.alias :left_trim, trim: { side: :left }
end

RSpec

If you use RSpec, we did built-in matchers for you. Add the following code to your rails_helper.rb

RSpec.configure do |config|
 config.include Normalizy::RSpec
end

And now you can use some of the matchers:

Result Matcher
it { is_expected.to normalizy(:email).from(' [email protected]  ').to '[email protected]' }
Filter Matcher

It will match the given filter literally:

it { is_expected.to normalizy(:email).with :downcase }
it { is_expected.to normalizy(:email).with %i[downcase squish] }
it { is_expected.to normalizy(:email).with(trim: { side: :left }) }

normalizy's People

Contributors

rafaeldev avatar wbotelhos avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

normalizy's Issues

Money format from locale file

The example showed on documentation is little bit different from Money gem:

# config/locale/en.yml
en:
  number:
    currency:
      format:
        delimiter: ","
        separator: "."
  # falling back to
  number:
    format:
      delimiter: ","
      separator: "."

On money gem the currency config are scoped on number attribute.
The normalizy gem can use money configure? It's make sense?

Thanks!

Percent to float

I have a doubt:
It doesn't work like expected when using money or percent to filter to parse a string with Brazilian currency to float.

Field is configured with the following:
normalizy :discount, with: { money: { type: :cents, separator: ",", precision: 2, cast: :to_f } }

Using a value R$ 12.000,56 as example is expected it returns 12000.56 but it returns 1200056.0

That's right?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.