{"id":17038661,"url":"https://github.com/wbotelhos/normalizy","last_synced_at":"2026-06-06T23:01:05.778Z","repository":{"id":20963393,"uuid":"91396256","full_name":"wbotelhos/normalizy","owner":"wbotelhos","description":":wrench: Attribute normalizer for Rails","archived":false,"fork":false,"pushed_at":"2022-05-18T03:55:15.000Z","size":217,"stargazers_count":19,"open_issues_count":3,"forks_count":4,"subscribers_count":1,"default_branch":"master","last_synced_at":"2026-05-08T06:11:33.237Z","etag":null,"topics":["activerecord","attributes","filter","hacktoberfest","hacktoberfest2022","normalize","normalizer","rails","typecast"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/wbotelhos.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null},"funding":{"patreon":"wbotelhos"}},"created_at":"2017-05-16T00:22:00.000Z","updated_at":"2023-09-01T12:41:08.000Z","dependencies_parsed_at":"2022-07-27T05:46:32.687Z","dependency_job_id":null,"html_url":"https://github.com/wbotelhos/normalizy","commit_stats":null,"previous_names":[],"tags_count":12,"template":false,"template_full_name":null,"purl":"pkg:github/wbotelhos/normalizy","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wbotelhos%2Fnormalizy","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wbotelhos%2Fnormalizy/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wbotelhos%2Fnormalizy/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wbotelhos%2Fnormalizy/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/wbotelhos","download_url":"https://codeload.github.com/wbotelhos/normalizy/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wbotelhos%2Fnormalizy/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34002561,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-06T02:00:07.033Z","response_time":107,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["activerecord","attributes","filter","hacktoberfest","hacktoberfest2022","normalize","normalizer","rails","typecast"],"created_at":"2024-10-14T08:57:26.795Z","updated_at":"2026-06-06T23:01:05.763Z","avatar_url":"https://github.com/wbotelhos.png","language":"Ruby","funding_links":["https://patreon.com/wbotelhos","https://www.patreon.com/wbotelhos"],"categories":[],"sub_categories":[],"readme":"# Normalizy\n\n[![CI](https://github.com/wbotelhos/normalizy/workflows/CI/badge.svg)](https://github.com/wbotelhos/normalizy/actions)\n[![Gem Version](https://badge.fury.io/rb/normalizy.svg)](https://badge.fury.io/rb/normalizy)\n[![Maintainability](https://api.codeclimate.com/v1/badges/3896d0a11bee012c818c/maintainability)](https://codeclimate.com/github/wbotelhos/normalizy/maintainability)\n[![codecov](https://codecov.io/gh/wbotelhos/normalizy/branch/master/graph/badge.svg?token=0XTRFDFHDq)](https://codecov.io/gh/wbotelhos/normalizy)\n[![Sponsor](https://img.shields.io/badge/sponsor-%3C3-green)](https://www.patreon.com/wbotelhos)\n\nAttribute normalizer for Rails.\n\n## Description\n\nIf 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@example.com  ` valid like `email@example.com` with no need to override acessors methods.\n\n## install\n\nAdd the following code on your `Gemfile` and run `bundle install`:\n\n```ruby\ngem 'normalizy'\n```\n\nSo generates an initializer for future custom configurations:\n\n```ruby\nrails g normalizy:install\n```\n\nIt will generates a file `config/initializers/normalizy.rb` where you can configure you own normalizer and choose some defaults one.\n\n## Usage\n\nOn your model, just add `normalizy` callback with the attribute you want to normalize and the filter to be used:\n\n```ruby\nclass User \u003c ApplicationRecord\n  normalizy :name, with: :downcase\nend\n```\n\nNow some email like `MyEmail@Example.com` will be saved as `myemail@example.com`.\n\n## Filters\n\nWe have a couple of built-in filters.\n\n### Date\n\nTransform a value to date format.\n\n```ruby\nnormalizy :birthday, with: :date\n\n'1984-10-23'\n# Tue, 23 Oct 1984 00:00:00 UTC +00:00\n```\n\nBy default, the date is treat as `%F` format and as `UTC` time.\n\n#### format\n\nYou can change the format using the `format` options:\n\n```ruby\nnormalizy :birthday, with: { date: { format: '%y/%m/%d' } }\n\n'84/10/23'\n# Tue, 23 Oct 1984 00:00:00 UTC +00:00\n```\n\n#### time zone\n\nTo convert the date on your time zone, just provide the `time_zone` option:\n\n```ruby\nnormalizy :birthday, with: { date: { time_zone: Time.zone } }\n\n'1984-10-23'\n# Tue, 23 Oct 1984 00:00:00 EDT -04:00\n```\n\n#### error message\n\nIf an invalid date is provided, Normalizy will add an error on attribute of the related object.\nYou can customize the error via I18n config:\n\n```yml\nen:\n  normalizy:\n    errors:\n      date:\n        user:\n          birthday: '%{value} is an invalid date.'\n```\n\nIf no configuration is provided, the default message will be `'%{value} is an invalid date.`.\n\n#### adjust\n\nIf your model receive a `Time` or `DateTime`, you can provide `adjust` options to change you time to begin o the day:\n\n```ruby\nnormalizy :birthday, with: { date: { adjust: :begin } }\n\nTue, 23 Oct 1984 11:30:00 EDT -04:00\n# Tue, 23 Oct 1984 00:00:00 EDT -04:00\n```\n\nOr to the end of the day:\n\n```ruby\nnormalizy :birthday, with: { date: { adjust: :end } }\n\nTue, 23 Oct 1984 00:00:00 EDT -04:00\n# Tue, 23 Oct 1984 11:59:59 EDT -04:00\n```\n\n### Money\n\nTransform a value to money format.\n\n```ruby\nnormalizy :amount, with: :money\n\n'$ 42.00'\n# '42.00'\n```\n\n#### separator\n\nThe `separator` will be keeped on value to be possible cast the right integer value.\nYou can change this separator:\n\n```ruby\nnormalizy :amount, with: { money: { separator: ',' } }\n\n'R$ 42,00'\n# '42,00'\n```\n\nIf you do not want pass it as options, Normalizy will fetch your I18n config:\n\n```yaml\nen:\n  number:\n    currency:\n      format:\n        separator: '.'\n```\n\nAnd if it does not exists, `.` will be used as default.\n\n#### type\n\nYou can retrieve the value in *cents* format, use the `type` options as `cents`:\n\n```ruby\nnormalizy :amount, with: { money: { type: :cents } }\n\n'$ 42.00'\n# '4200'\n```\n\n#### precision\n\nAs you could see on the last example, when using `type: :cents` is important the number of decimal digits.\nSo, you can configure it to avoid the following error:\n\n```ruby\nnormalizy :amount, with: { money: { type: :cents } }\n\n'$ 42.0'\n# 420\n```\n\nWhen 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`:\n\n```ruby\nnormalizy :amount, with: { money: { precision: 2 } }\n\n'$ 42.0'\n# 42.00\n```\n\n```ruby\nnormalizy :amount, with: { money: { precision: 2, type: :cents } }\n\n'$ 42.0'\n# 4200\n```\n\nIf you do not want pass it as options, Normalizy will fetch your I18n config:\n\n```yaml\nen:\n  number:\n    currency:\n      format:\n        precision: 2\n```\n\nAnd if it does not exists, `2` will be used as default.\n\n#### cast\n\nIf you need get a number over a normalized string in a number style, provide `cast` option with desired cast method:\n\n```ruby\nnormalizy :amount, with: { money: { cast: :to_i } }\n\n'$ 42.00'\n# 4200\n```\n\nJust pay attention to avoid to use `type: :cents` together `cast` with float parses.\nSince `type` runs first, you will add decimal in a number that already is represented with decimal, but as integer:\n\n```ruby\nnormalizy :amount, with: { money: { cast: :to_f, type: :cents } }\n\n'$ 42.00'\n# 4200.0\n```\n\n### Number\n\nTransform text to valid number.\n\n```ruby\nnormalizy :age, with: :number\n\n' 32x'\n# '32'\n```\n\nIf you want cast the value, provide `cast` option with desired cast method:\n\n```ruby\nnormalizy :age, with: { number: { cast: :to_i } }\n\n' 32x'\n# 32\n```\n\n### Percent\n\nTransform a value to a valid percent format.\n\n```ruby\nnormalizy :amount, with: :percent\n\n'42.00 %'\n# '42.00'\n```\n\n#### separator\n\nThe `separator` will be keeped on value to be possible cast the right integer value.\nYou can change this separator:\n\n```ruby\nnormalizy :amount, with: { percent: { separator: ',' } }\n\n'42,00 %'\n# '42,00'\n```\n\nIf you do not want pass it as options, Normalizy will fetch your I18n config:\n\n```yaml\nen:\n  number:\n    percentage:\n      format:\n        separator: '.'\n```\n\nAnd if it does not exists, `.` will be used as default.\n\n#### type\n\nYou can retrieve the value in *integer* format, use the `type` options as `integer`:\n\n```ruby\nnormalizy :amount, with: { percent: { type: :integer } }\n\n'42.00 %'\n# '4200'\n```\n\n#### precision\n\nAs you could see on the last example, when using `type: :integer` is important the number of decimal digits.\nSo, you can configure it to avoid the following error:\n\n```ruby\nnormalizy :amount, with: { percent: { type: :integer } }\n\n'42.0 %'\n# 420\n```\n\nWhen 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`:\n\n```ruby\nnormalizy :amount, with: { percent: { precision: 2 } }\n\n'42.0 %'\n# 42.00\n```\n\n```ruby\nnormalizy :amount, with: { percent: { precision: 2, type: :integer } }\n\n'42.0 %'\n# 4200\n```\n\nIf you do not want pass it as options, Normalizy will fetch your I18n config:\n\n```yaml\nen:\n  number:\n    percentage:\n      format:\n        separator: 2\n```\n\nAnd if it does not exists, `2` will be used as default.\n\n#### cast\n\nIf you need get a number over a normalized string in a number style, provide `cast` option with desired cast method:\n\n```ruby\nnormalizy :amount, with: { percent: { cast: :to_i } }\n\n'42.00 %'\n# 4200\n```\n\nJust pay attention to avoid to use `type: :integer` together `cast` with float parses.\nSince `type` runs first, you will add decimal in a number that already is represented with decimal, but as integer:\n\n```ruby\nnormalizy :amount, with: { percent: { cast: :to_f, type: :integer } }\n\n'42.00 %'\n# 4200.0\n```\n\n### Slug\n\nConvert texto to slug.\n\n```ruby\nnormalizy :slug, with: :slug\n'Washington é Botelho'\n# 'washington-e-botelho'\n```\n\n#### to\n\nYou can slug a field based on other just sending the result value.\n\n```ruby\nnormalizy :title, with: { slug: { to: :slug } }\n\nmodel.title = 'Washington é Botelho'\n\nmodel.slug\n# 'washington-e-botelho'\n```\n\n### Strip\n\nCleans edge spaces.\n\nOptions:\n\n- `side`: `:left`, `:right` or `:both`. Default: `:both`\n\n```ruby\nnormalizy :name, with: :strip\n'  Washington  Botelho  '\n# 'Washington  Botelho'\n```\n\n```ruby\nnormalizy :name, with: { strip: { side: :left } }\n'  Washington  Botelho  '\n# 'Washington  Botelho  '\n```\n\n```ruby\nnormalizy :name, with: { strip: { side: :right } }\n'  Washington  Botelho  '\n# '  Washington  Botelho'\n```\n\n```ruby\nnormalizy :name, with: { strip: { side: :both } }\n'  Washington  Botelho  '\n# 'Washington  Botelho'\n```\n\nAs you can see, the rules can be passed as Symbol/String or as Hash if it has options.\n\n### Truncate\n\nRemove excedent string part from a gived limit.\n\n```ruby\nnormalizy :description, with: { truncate: { limit: 10 } }\n\n'Once upon a time in a world far far away'\n# 'Once upon '\n```\n\n## Multiple Filters\n\nYou can normalize with a couple of filters at once:\n\n```ruby\nnormalizy :name, with: { %i[squish titleize] }\n'  washington  botelho  '\n# 'Washington Botelho'\n```\n\n## Multiple Attributes\n\nYou can normalize more than one attribute at once too, with one or multiple filters:\n\n```ruby\nnormalizy :email, :username, with: :downcase\n```\n\nOf course you can declare multiple attribute and multiple filters, either.\nIt is possible to make sequential normalizy calls, but *take care*!\nSince we use `prepend` module the last line will run first then others:\n\n```ruby\nnormalizy :username, with: :downcase\nnormalizy :username, with: :titleize\n\n'BoteLho'\n# 'bote lho'\n```\n\nAs you can see, `titleize` runs first then `downcase`.\nEach line will be evaluated from the *bottom* to the *top*.\nIf it is hard to you accept, use [Muiltiple Filters](#multiple-filters)\n\n## Default Filters\n\nYou can configure some default filters to be runned.\nEdit initializer at `config/initializers/normalizy.rb`:\n\n```ruby\nNormalizy.configure do |config|\n  config.default_filters = [:squish]\nend\n```\n\nNow, all normalization will include `squish`, even when no rule is declared.\n\n```ruby\nnormalizy :name\n\"  Washington  \\n  Botelho  \"\n# 'Washington Botelho'\n```\n\nIf you declare some filter, the default filter `squish` will be runned together:\n\n```ruby\nnormalizy :name, with: :downcase\n'  washington  botelho  '\n# 'Washington Botelho'\n```\n\n## Custom Filter\n\nYou can create a custom filter that implements `call` method with an `input` as argument and an optional `options`:\n\n```ruby\nmodule Normalizy\n  module Filters\n    module Blacklist\n      def self.call(input)\n        input.gsub 'Fuck', replacement: '***'\n      end\n    end\n  end\nend\n```\n\n```ruby\nNormalizy.configure do |config|\n  config.add :blacklist, Normalizy::Filters::Blacklist\nend\n```\n\nNow you can use your custom filter:\n\n```ruby\nnormalizy :name, with: :blacklist\n\n'Washington Fuck Botelho'\n# 'Washington *** Botelho'\n```\n\n#### options\n\nIf you want to pass options to your filter, just call it as a hash and the value will be send to the custom filter:\n\n```ruby\nmodule Normalizy\n  module Filters\n    module Blacklist\n      def self.call(input, options: {})\n        input.gsub 'Fuck', replacement: options[:replacement]\n      end\n    end\n  end\nend\n```\n\n```ruby\nnormalizy :name, with: { blacklist: { replacement: '---' } }\n\n'Washington Fuck Botelho'\n# 'Washington --- Botelho'\n```\n\n### options value\n\nBy default, Modules and instance methods of class will receveis the following attributes on `options` argument:\n\n- `object`: The object that Normalizy is acting;\n- `attribute`: The attribute of the object that Normalizy is acting.\n\nYou can pass a block and it will be received on filter:\n\n```ruby\nmodule Normalizy\n  module Filters\n    module Blacklist\n      def self.call(input, options: {})\n        value = input.gsub('Fuck', 'filtered')\n\n        value = yield(value) if block_given?\n\n        value\n      end\n    end\n  end\nend\n```\n\n```ruby\nnormalizy :name, with: { :blacklist, \u0026-\u003e(value) { value.sub('filtered', '(filtered 2x)') } }\n\n'Washington Fuck Botelho'\n# 'Washington (filtered 2x) Botelho'\n```\n\n## Method Filters\n\nIf a built-in filter is not found, Normalizy will try to find a method in the current class.\n\n```ruby\nnormalizy :birthday, with: :parse_date\n\ndef parse_date(input)\n  Time.zone.parse(input).strftime '%Y/%m/%d'\nend\n\n'1984-10-23'\n# '1984/10/23'\n```\n\nIf you gives an option, it will be passed to the function:\n\n```ruby\nnormalizy :birthday, with: { parse_date: { format: '%Y/%m/%d' }\n\ndef parse_date(input, options = {})\n  Time.zone.parse(input).strftime options[:format]\nend\n\n'1984-10-23'\n# '1984/10/23'\n```\n\nBlock methods works here either.\n\n## Native Filter\n\nAfter the missing built-in and class method, the fallback will be the value of native methods.\n\n```ruby\nnormalizy :name, with: :reverse\n\n'Washington Botelho'\n# \"ohletoB notgnihsaW\"\n```\n\n## Inline Filter\n\nMaybe you want to declare an inline filter, in this case, just use a Lambda or Proc:\n\n```ruby\nnormalizy :age, with: -\u003e(input) { input.to_i.abs }\n\n-32\n# 32\n```\n\nYou can use it on filters declaration too:\n\n```ruby\nNormalizy.configure do |config|\n  config.add :age, -\u003e(input) { input.to_i.abs }\nend\n```\n\n## Alias\n\nSometimes you want to give a better name to your filter, just to keep the things semantic.\nDuplicates the code, as you know, is not a good idea, so, create an alias:\n\n```ruby\nNormalizy.configure do |config|\n  config.alias :age, :number\nend\n```\n\nNow, `age` will delegate to `number` filter.\n\nAnd now, the aliased filter will work fine:\n\n```ruby\nnormalizy :age, with: :age\n\n'= 42'\n# 42\n```\n\nIf you need to alias multiple filters, just provide an array of them:\n\n```ruby\nNormalizy.configure do |config|\n  config.alias :username, %i[squish downcase]\nend\n```\n\nAlias accepts options parameters too:\n\n```ruby\nNormalizy.configure do |config|\n  config.alias :left_trim, trim: { side: :left }\nend\n```\n\n## RSpec\n\nIf you use [RSpec](http://rspec.info), we did built-in matchers for you.\nAdd the following code to your `rails_helper.rb`\n\n```ruby\nRSpec.configure do |config|\n config.include Normalizy::RSpec\nend\n```\n\nAnd now you can use some of the matchers:\n\n##### Result Matcher\n\n```ruby\nit { is_expected.to normalizy(:email).from(' Email@example.com  ').to 'email@example.com' }\n```\n\n##### Filter Matcher\n\nIt will match the given filter literally:\n\n```ruby\nit { is_expected.to normalizy(:email).with :downcase }\n```\n\n```ruby\nit { is_expected.to normalizy(:email).with %i[downcase squish] }\n```\n\n```ruby\nit { is_expected.to normalizy(:email).with(trim: { side: :left }) }\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwbotelhos%2Fnormalizy","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwbotelhos%2Fnormalizy","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwbotelhos%2Fnormalizy/lists"}