├── .travis.yml ├── .rspec ├── install.rb ├── spec ├── models │ ├── user.rb │ ├── journal.rb │ ├── person.rb │ ├── book.rb │ ├── author.rb │ ├── publisher.rb │ ├── article.rb │ └── magazine.rb ├── user_spec.rb ├── journal_spec.rb ├── attribute_normalizer_spec.rb ├── publisher_spec.rb ├── magazine_spec.rb ├── connection_and_schema.rb ├── test_helper.rb ├── author_spec.rb ├── book_spec.rb └── article_spec.rb ├── .gitignore ├── lib ├── attribute_normalizer │ ├── normalizers │ │ ├── strip_normalizer.rb │ │ ├── squish_normalizer.rb │ │ ├── blank_normalizer.rb │ │ ├── control_chars_normalizer.rb │ │ ├── whitespace_normalizer.rb │ │ ├── phone_normalizer.rb │ │ └── boolean_normalizer.rb │ ├── version.rb │ ├── rspec_matcher.rb │ └── model_inclusions.rb └── attribute_normalizer.rb ├── Rakefile ├── Gemfile ├── install.txt ├── Guardfile ├── attribute_normalizer.gemspec ├── MIT-LICENSE ├── ROADMAP.textile └── README.textile /.travis.yml: -------------------------------------------------------------------------------- 1 | rvm: 2 | - 1.9.3 3 | - 2.0 4 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format progress 3 | --profile 4 | -------------------------------------------------------------------------------- /install.rb: -------------------------------------------------------------------------------- 1 | puts IO.read(File.join(File.dirname(__FILE__), 'install.txt')) 2 | -------------------------------------------------------------------------------- /spec/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < Person 2 | normalize_attribute :firstname 3 | end 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle 2 | .yardoc 3 | coverage 4 | doc 5 | Gemfile.lock 6 | pkg 7 | rdoc 8 | tmp 9 | -------------------------------------------------------------------------------- /spec/models/journal.rb: -------------------------------------------------------------------------------- 1 | class Journal < ActiveRecord::Base 2 | normalize_attribute :name 3 | end 4 | -------------------------------------------------------------------------------- /spec/models/person.rb: -------------------------------------------------------------------------------- 1 | class Person < ActiveRecord::Base 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /lib/attribute_normalizer/normalizers/strip_normalizer.rb: -------------------------------------------------------------------------------- 1 | module AttributeNormalizer 2 | module Normalizers 3 | module StripNormalizer 4 | def self.normalize(value, options = {}) 5 | value.is_a?(String) ? value.strip : value 6 | end 7 | end 8 | end 9 | end -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | # require 'rdoc/task' 3 | require 'rspec/core/rake_task' 4 | require 'bundler/gem_tasks' 5 | 6 | desc 'Default: spec tests.' 7 | task :default => :spec 8 | 9 | desc 'Test the attribute_normalizer plugin.' 10 | RSpec::Core::RakeTask.new do |t| 11 | end 12 | 13 | -------------------------------------------------------------------------------- /lib/attribute_normalizer/normalizers/squish_normalizer.rb: -------------------------------------------------------------------------------- 1 | module AttributeNormalizer 2 | module Normalizers 3 | module SquishNormalizer 4 | def self.normalize(value, options = {}) 5 | value.is_a?(String) ? value.strip.gsub(/\s+/, ' ') : value 6 | end 7 | end 8 | end 9 | end -------------------------------------------------------------------------------- /spec/user_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(File.expand_path(__FILE__)) + '/test_helper' 2 | 3 | describe User do 4 | 5 | context 'on default attribute with the default normalizer changed' do 6 | it { should normalize_attribute(:firstname).from(' here ').to('here') } 7 | end 8 | 9 | end 10 | -------------------------------------------------------------------------------- /lib/attribute_normalizer/normalizers/blank_normalizer.rb: -------------------------------------------------------------------------------- 1 | module AttributeNormalizer 2 | module Normalizers 3 | module BlankNormalizer 4 | def self.normalize(value, options = {}) 5 | value.nil? || (value.is_a?(String) && value !~ /\S/) ? nil : value 6 | end 7 | end 8 | end 9 | end -------------------------------------------------------------------------------- /lib/attribute_normalizer/version.rb: -------------------------------------------------------------------------------- 1 | module AttributeNormalizer 2 | 3 | module Version 4 | 5 | MAJOR = 1 6 | MINOR = 2 7 | PATCH = 0 8 | BUILD = nil 9 | 10 | def self.to_s 11 | [ MAJOR, MINOR, PATCH, BUILD ].compact.join('.') 12 | end 13 | 14 | end 15 | 16 | end 17 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | group :development do 6 | gem 'activerecord' 7 | gem 'bson_ext' 8 | gem 'guard' 9 | gem 'guard-rspec' 10 | gem 'guard-yard' 11 | gem 'mongoid', '> 4' 12 | gem 'pry' 13 | gem 'rake' 14 | gem 'RedCloth' 15 | gem 'rspec' 16 | gem 'sqlite3' 17 | end 18 | -------------------------------------------------------------------------------- /lib/attribute_normalizer/normalizers/control_chars_normalizer.rb: -------------------------------------------------------------------------------- 1 | module AttributeNormalizer 2 | module Normalizers 3 | module ControlCharsNormalizer 4 | def self.normalize(value, options = {}) 5 | value.is_a?(String) ? value.gsub(/[[:cntrl:]&&[^[:space:]]]/, '') : value 6 | end 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/attribute_normalizer/normalizers/whitespace_normalizer.rb: -------------------------------------------------------------------------------- 1 | module AttributeNormalizer 2 | module Normalizers 3 | module WhitespaceNormalizer 4 | def self.normalize(value, options = {}) 5 | value.is_a?(String) ? value.gsub(/[^\S\n]+/, ' ').gsub(/\s?\n\s?/, "\n").strip : value 6 | end 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/attribute_normalizer/normalizers/phone_normalizer.rb: -------------------------------------------------------------------------------- 1 | module AttributeNormalizer 2 | module Normalizers 3 | module PhoneNormalizer 4 | def self.normalize(value, options = {}) 5 | value = value.is_a?(String) ? value.gsub(/[^0-9]+/, '') : value 6 | value.is_a?(String) && value.empty? ? nil : value 7 | end 8 | end 9 | end 10 | end -------------------------------------------------------------------------------- /spec/models/book.rb: -------------------------------------------------------------------------------- 1 | class Book < ActiveRecord::Base 2 | 3 | normalize_attribute :author 4 | 5 | normalize_attribute :us_price, :cnd_price, :with => :currency 6 | 7 | normalize_attributes :summary, :with => [ :strip, { :truncate => { :length => 12 } }, :blank ] 8 | 9 | normalize_attributes :title do |value| 10 | value.is_a?(String) ? value.titleize.strip : value 11 | end 12 | 13 | end -------------------------------------------------------------------------------- /spec/journal_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(File.expand_path(__FILE__)) + '/test_helper' 2 | 3 | describe Journal do 4 | 5 | context 'Testing the built in normalizers' do 6 | # default normalization [ :strip, :blank ] 7 | it { should normalize_attribute(:name) } 8 | it { should normalize_attribute(:name).from(' Physical Review ').to('Physical Review') } 9 | it { should normalize_attribute(:name).from(' ').to(nil) } 10 | end 11 | 12 | end -------------------------------------------------------------------------------- /spec/models/author.rb: -------------------------------------------------------------------------------- 1 | class Author < ActiveRecord::Base 2 | 3 | normalize_attribute :name 4 | normalize_attribute :nickname, :with => :squish 5 | normalize_attribute :first_name, :with => :strip 6 | normalize_attribute :last_name, :with => :blank 7 | normalize_attribute :phone_number, :with => :phone 8 | normalize_attribute :biography, :with => :whitespace 9 | normalize_attribute :bibliography, :with => :control_chars 10 | 11 | end 12 | -------------------------------------------------------------------------------- /spec/models/publisher.rb: -------------------------------------------------------------------------------- 1 | class Publisher < ActiveRecord::Base 2 | 3 | attr_accessor :custom_writer 4 | 5 | def name=(_) 6 | self.custom_writer = true 7 | super 8 | end 9 | 10 | normalize_attribute :name, :with => :blank 11 | normalize_attribute :phone_number, :with => :phone 12 | 13 | normalize_attribute :international_phone_number do |number| 14 | self.country == 'fr' ? number.sub(/^0/,'+33') : number 15 | end 16 | 17 | end 18 | -------------------------------------------------------------------------------- /spec/models/article.rb: -------------------------------------------------------------------------------- 1 | class Article < ActiveRecord::Base 2 | 3 | normalize_attribute :limited_slug, :before => [ :strip, :blank ], :after => [ { :truncate => { :length => 11, :omission => '' } } ] do |value| 4 | value.present? && value.is_a?(String) ? value.downcase.gsub(/\s+/, '-') : value 5 | end 6 | 7 | normalize_attribute :title 8 | 9 | normalize_attribute :slug, :with => [ :strip, :blank ] do |value| 10 | value.present? && value.is_a?(String) ? value.downcase.gsub(/\s+/, '-') : value 11 | end 12 | 13 | end 14 | -------------------------------------------------------------------------------- /install.txt: -------------------------------------------------------------------------------- 1 | ----------------------------------------------------------------------- 2 | Attribute Normalizer News: 3 | 4 | New with the 0.3.X release is the ability to change the default 5 | normalization and also the ability to chain normalizers together. 6 | 7 | After the flow of patches slows down on this release I will likely 8 | freeze the feature set and API for a 1.0 release of the gem. 9 | 10 | Cheers, 11 | Michael Deering http://mdeering.com 12 | ----------------------------------------------------------------------- -------------------------------------------------------------------------------- /lib/attribute_normalizer/normalizers/boolean_normalizer.rb: -------------------------------------------------------------------------------- 1 | # Extracted from ActiveRecord::ConnectionAdapters::Column 2 | require 'set' 3 | 4 | module AttributeNormalizer 5 | module Normalizers 6 | module BooleanNormalizer 7 | TRUE_VALUES = [true, 1, '1', 't', 'T', 'true', 'TRUE', 'on', 'ON'].to_set 8 | 9 | def self.normalize(value, options = {}) 10 | if value.is_a?(String) && value.blank? 11 | nil 12 | else 13 | TRUE_VALUES.include?(value) 14 | end 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # A sample Guardfile 2 | # More info at https://github.com/guard/guard#readme 3 | 4 | guard 'rspec', all_on_start: true, cmd: 'bundle exec rspec' do 5 | watch(%r{^spec/.+_spec\.rb$}) 6 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" } 7 | watch('spec/spec_helper.rb') { "spec" } 8 | watch(%r{^spec/models/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } 9 | watch(%r{^spec/support/(.+)\.rb$}) { "spec" } 10 | end 11 | 12 | guard 'yard' do 13 | watch(%r{app/.+\.rb}) 14 | watch(%r{lib/.+\.rb}) 15 | watch(%r{ext/.+\.c}) 16 | end 17 | -------------------------------------------------------------------------------- /attribute_normalizer.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | 4 | require "attribute_normalizer/version" 5 | 6 | Gem::Specification.new do |s| 7 | 8 | s.name = 'attribute_normalizer' 9 | s.version = AttributeNormalizer::Version 10 | 11 | s.authors = [ 'Michael Deering' ] 12 | s.email = [ 'mdeering@mdeering.com' ] 13 | s.homepage = 'https://github.com/mdeering/attribute_normalizer' 14 | 15 | s.license = 'MIT' 16 | s.summary = 'Configurable data normalization' 17 | s.description = 'Configurable attribute data normalization' 18 | 19 | s.require_path = 'lib' 20 | 21 | s.files = Dir["{lib}/**/*"] + [ 'README.textile' ] 22 | 23 | end 24 | -------------------------------------------------------------------------------- /spec/models/magazine.rb: -------------------------------------------------------------------------------- 1 | class Magazine 2 | 3 | include AttributeNormalizer 4 | 5 | attr_accessor :name, 6 | :cnd_price, 7 | :us_price, 8 | :summary, 9 | :title, 10 | :sold 11 | 12 | normalize_attributes :name 13 | normalize_attribute :us_price, :cnd_price, :with => :currency 14 | 15 | normalize_attributes :summary, 16 | :with => [ 17 | :strip, 18 | { :truncate => { :length => 12 } }, 19 | :blank 20 | ] 21 | 22 | normalize_attributes :title do |value| 23 | value.is_a?(String) ? value.titleize.strip : value 24 | end 25 | 26 | normalize_attribute :sold, with: :boolean 27 | 28 | end 29 | -------------------------------------------------------------------------------- /spec/attribute_normalizer_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(File.expand_path(__FILE__)) + '/test_helper' 2 | 3 | describe AttributeNormalizer do 4 | 5 | it 'should add the class method Class#normalize_attributes and Class#normalize_attribute when included' do 6 | klass = Class.new do 7 | include AttributeNormalizer 8 | end 9 | 10 | expect(klass).to respond_to(:normalize_attributes) 11 | expect(klass).to respond_to(:normalize_attribute) 12 | end 13 | 14 | it 'should not fail due to database exceptions raised by table_exists?' do 15 | class PGError < RuntimeError; end 16 | 17 | Class.new(ActiveRecord::Base) do 18 | def self.table_exists? 19 | raise PGError, "FATAL: something bad happened trying to probe for table existence" 20 | end 21 | 22 | include AttributeNormalizer 23 | end 24 | 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/publisher_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(File.expand_path(__FILE__)) + '/test_helper' 2 | 3 | describe Publisher do 4 | 5 | context 'using the built in phone normalizer' do 6 | it { should normalize_attribute(:phone_number).from('no-numbers-here').to(nil) } 7 | it { should normalize_attribute(:phone_number).from('1.877.987.9875').to('18779879875') } 8 | it { should normalize_attribute(:phone_number).from('+ 1 (877) 987-9875').to('18779879875') } 9 | end 10 | 11 | context 'access to object in normalizer block' do 12 | subject { Publisher.new(:country => 'fr') } 13 | it { should normalize_attribute(:international_phone_number).from('0612345678').to('+33612345678') } 14 | end 15 | 16 | context 'on custom writer method' do 17 | subject { Publisher.new(:name => 'Mike') } 18 | it { should normalize_attribute(:name).from('').to(nil) } 19 | context 'custom_writer' do 20 | subject { Publisher.new(:name => 'Mike').custom_writer } 21 | it { should be(true) } 22 | end 23 | end 24 | 25 | end 26 | -------------------------------------------------------------------------------- /spec/magazine_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(File.expand_path(__FILE__)) + '/test_helper' 2 | 3 | describe Magazine do 4 | 5 | it do 6 | should normalize_attribute(:name). 7 | from(' Plain Old Ruby Objects ').to('Plain Old Ruby Objects') 8 | end 9 | 10 | it do 11 | should normalize_attribute(:us_price).from('$3.50').to('3.50') 12 | end 13 | 14 | it do 15 | should normalize_attribute(:cnd_price).from('$3,450.98').to('3450.98') 16 | end 17 | 18 | it do 19 | should normalize_attribute(:summary). 20 | from(' Here is my summary that is a little to long '). 21 | to('Here is m...') 22 | end 23 | 24 | it do 25 | should normalize_attribute(:title). 26 | from('some really interesting title'). 27 | to('Some Really Interesting Title') 28 | end 29 | 30 | it do 31 | should normalize_attribute(:sold). 32 | from('true').to(true) 33 | end 34 | 35 | it do 36 | should normalize_attribute(:sold). 37 | from('0').to(false) 38 | end 39 | 40 | it do 41 | should normalize_attribute(:sold). 42 | from('').to(nil) 43 | end 44 | 45 | end 46 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 Michael Deering 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /ROADMAP.textile: -------------------------------------------------------------------------------- 1 | h1. 0.3.0 2 | * -Default normalizers- 3 | * -Normalizer Chaining- 4 | * -Ability to change the default attribute normalization.- 5 | 6 | h1. 0.2.1 7 | * -ActiveModel ORM added- 8 | * -Fix for :with option getting dropped when normalizing several attributes- 9 | 10 | h1. 0.2.0 11 | 12 | * -Remove normalization on read in preference of presenters- 13 | * -RSpec matcher included- 14 | * -Preconfigured normalization blocks for reuse across classes/attributes- 15 | 16 | h1. 0.1.2 17 | 18 | * -Re-factored to use 'super' calls so that we don't break any other gems/plugins or the call chain in general- 19 | 20 | h1. 0.1.1 21 | 22 | * -Alias _normalize_attribute_ to _normalize_attributes_ for syntactical sugar based on "suggestion here":http://mdeering.com/posts/019-activerecord-attribute-normalization-rails-plugin#comments- 23 | * -Use calls to super for attribute setting to avoid bypassing other plugins and gems and issues with calling reload on the model.- "Myron Marston":http://github.com/myronmarston 24 | 25 | h1. 0.1.0 26 | 27 | * -Documentation fixes and updates.- 28 | * -Rspec fixes and updates.- 29 | * -Take the existing code that is a rails plugin only and turn it into a proper Ruby gem hosted over at "Gemcutter":http://gemcutter.org- 30 | -------------------------------------------------------------------------------- /spec/connection_and_schema.rb: -------------------------------------------------------------------------------- 1 | ActiveRecord::Base.establish_connection({ 2 | :database => ":memory:", 3 | :adapter => 'sqlite3', 4 | :timeout => 500 5 | }) 6 | 7 | ActiveRecord::Schema.define do 8 | create_table :publishers, :force => true do |t| 9 | t.string :name 10 | t.string :country 11 | t.string :phone_number 12 | t.string :international_phone_number 13 | end 14 | 15 | create_table :authors, :force => true do |t| 16 | t.string :name 17 | t.string :nickname 18 | t.string :first_name 19 | t.string :last_name 20 | t.string :phone_number 21 | t.text :biography 22 | t.text :bibliography 23 | end 24 | 25 | create_table :books, :force => true do |t| 26 | t.string :author 27 | t.string :isbn 28 | t.decimal :cnd_price 29 | t.decimal :us_price 30 | t.string :summary 31 | t.string :title 32 | end 33 | 34 | create_table :journals, :force => true do |t| 35 | t.string :name 36 | end 37 | 38 | create_table :articles, :force => true do |t| 39 | t.string :title 40 | t.string :slug 41 | t.string :limited_slug 42 | end 43 | 44 | create_table :users, :force => true do |t| 45 | t.string :firstname 46 | t.string :lastname 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/attribute_normalizer/rspec_matcher.rb: -------------------------------------------------------------------------------- 1 | module AttributeNormalizer 2 | 3 | module RSpecMatcher 4 | 5 | def normalize_attribute(attribute) 6 | NormalizeAttribute.new(attribute) 7 | end 8 | 9 | class NormalizeAttribute 10 | 11 | def initialize(attribute) 12 | @attribute = attribute 13 | @from = '' 14 | end 15 | 16 | def description 17 | "normalize #{@attribute} from #{@from.nil? ? 'nil' : "\"#{@from}\""} to #{@to.nil? ? 'nil' : "\"#{@to}\""}" 18 | end 19 | 20 | def failure_message 21 | "#{@attribute} did not normalize as expected! \"#{@subject.send(@attribute)}\" != #{@to.nil? ? 'nil' : "\"#{@to}\""}" 22 | end 23 | 24 | def failure_message_when_negated 25 | "expected #{@attribute} to not be normalized from #{@from.nil? ? 'nil' : "\"#{@from}\""} to #{@to.nil? ? 'nil' : "\"#{@to}\""}" 26 | end 27 | alias negative_failure_message failure_message_when_negated 28 | 29 | def from(value) 30 | @from = value 31 | self 32 | end 33 | 34 | def to(value) 35 | @to = value 36 | self 37 | end 38 | 39 | def matches?(subject) 40 | @subject = subject 41 | @subject.send("#{@attribute}=", @from) 42 | 43 | @subject.send(@attribute) == @to 44 | end 45 | 46 | end 47 | 48 | end 49 | 50 | end 51 | -------------------------------------------------------------------------------- /spec/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rspec' 3 | require 'active_record' 4 | require 'mongoid' 5 | 6 | $LOAD_PATH.unshift(File.dirname(__FILE__) + '/../lib') 7 | 8 | require 'attribute_normalizer' 9 | 10 | AttributeNormalizer.configure do |config| 11 | 12 | config.normalizers[:currency] = lambda do |value, options| 13 | value.is_a?(String) ? value.gsub(/[^0-9\.]+/, '') : value 14 | end 15 | 16 | config.normalizers[:truncate] = lambda do |text, options| 17 | if text.is_a?(String) 18 | options.reverse_merge!(:length => 30, :omission => "...") 19 | l = options[:length] - options[:omission].mb_chars.length 20 | chars = text.mb_chars 21 | (chars.length > options[:length] ? chars[0...l] + options[:omission] : text).to_s 22 | else 23 | text 24 | end 25 | end 26 | 27 | config.normalizers[:special_normalizer] = lambda do |value, options| 28 | (value.is_a?(String) && value.match(/testing the default normalizer/)) ? 'testing the default normalizer' : value 29 | end 30 | 31 | config.default_normalizers = :strip, :special_normalizer, :blank 32 | 33 | end 34 | 35 | 36 | require 'connection_and_schema' 37 | require 'models/book' 38 | require 'models/author' 39 | require 'models/journal' 40 | require 'models/article' 41 | require 'models/magazine' 42 | require 'models/publisher' 43 | require 'models/person' 44 | require 'models/user' 45 | 46 | RSpec.configure do |config| 47 | config.include AttributeNormalizer::RSpecMatcher 48 | end 49 | -------------------------------------------------------------------------------- /spec/author_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(File.expand_path(__FILE__)) + '/test_helper' 2 | 3 | describe Author do 4 | 5 | context 'Testing the built in normalizers' do 6 | # default normalization [ :strip, :blank ] 7 | it { should normalize_attribute(:name) } 8 | it { should normalize_attribute(:name).from(' this ').to('this') } 9 | it { should normalize_attribute(:name).from(' ').to(nil) } 10 | 11 | # :strip normalizer 12 | it { should normalize_attribute(:first_name).from(' this ').to('this') } 13 | it { should normalize_attribute(:first_name).from(' ').to('') } 14 | 15 | # :squish normalizer 16 | it { should normalize_attribute(:nickname).from(' this nickname ').to('this nickname') } 17 | 18 | # :blank normalizer 19 | it { should normalize_attribute(:last_name).from('').to(nil) } 20 | it { should normalize_attribute(:last_name).from(' ').to(nil) } 21 | it { should normalize_attribute(:last_name).from(' this ').to(' this ') } 22 | 23 | # :whitespace normalizer 24 | it { should normalize_attribute(:biography).from(" this line\nbreak ").to("this line\nbreak") } 25 | it { should normalize_attribute(:biography).from("\tthis\tline\nbreak ").to("this line\nbreak") } 26 | it { should normalize_attribute(:biography).from(" \tthis \tline \nbreak \t \nthis").to("this line\nbreak\nthis") } 27 | it { should normalize_attribute(:biography).from(' ').to('') } 28 | 29 | # :control_chars normalizer 30 | it { should normalize_attribute(:bibliography).from("No \bcontrol\u0003 chars").to("No control chars") } 31 | it { should normalize_attribute(:bibliography).from("Except for\tspaces.\r\nAll kinds").to("Except for\tspaces.\r\nAll kinds") } 32 | end 33 | 34 | context 'on default attribute with the default normalizer changed' do 35 | it { should normalize_attribute(:phone_number).from('no-numbers-here').to(nil) } 36 | it { should normalize_attribute(:phone_number).from('1.877.987.9875').to('18779879875') } 37 | it { should normalize_attribute(:phone_number).from('+ 1 (877) 987-9875').to('18779879875') } 38 | end 39 | 40 | end 41 | -------------------------------------------------------------------------------- /spec/book_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(File.expand_path(__FILE__)) + '/test_helper' 2 | 3 | describe Book do 4 | 5 | it { should normalize_attribute(:author).from(' Michael Deering ').to('Michael Deering') } 6 | 7 | it { should normalize_attribute(:us_price).from('$3.50').to(3.50) } 8 | it { should normalize_attribute(:cnd_price).from('$3,450.98').to(3450.98) } 9 | 10 | it { should normalize_attribute(:summary).from(' Here is my summary that is a little to long ').to('Here is m...') } 11 | 12 | it { should normalize_attribute(:title).from('pick up chicks with magic tricks').to('Pick Up Chicks With Magic Tricks') } 13 | 14 | context 'normalization should not interfere with other hooks and aliases on the attribute assignment' do 15 | before do 16 | @book = Book.create!(:title => 'Original Title') 17 | end 18 | 19 | it 'should still reflect that the attribute has been changed through the call to super' do 20 | expect { @book.title = 'New Title' }.to change(@book, :title_changed?).from(false).to(true) 21 | end 22 | end 23 | 24 | context 'when another instance of the same saved record has been changed' do 25 | before do 26 | @book = Book.create!(:title => 'Original Title') 27 | @book2 = Book.find(@book.id) 28 | @book2.update_attributes(:title => 'New Title') 29 | end 30 | 31 | it "should reflect the change when the record is reloaded" do 32 | expect { @book.reload }.to change(@book, :title).from('Original Title').to('New Title') 33 | end 34 | end 35 | 36 | context 'normalization should work with multiple attributes at the same time' do 37 | before do 38 | @book = Book.new(:title => ' Bad Title ', :author => ' Bad Author ') 39 | end 40 | 41 | it "should apply normalizations to both attributes" do 42 | expect(@book.title).to eq('Bad Title') 43 | expect(@book.author).to eq('Bad Author') 44 | end 45 | end 46 | 47 | context 'with the default normalizer changed' do 48 | it "only strips leading and trailing whitespace" do 49 | @book = Book.new :author => ' testing the default normalizer ' 50 | expect(@book.author).to eq('testing the default normalizer') 51 | end 52 | end 53 | 54 | end 55 | -------------------------------------------------------------------------------- /spec/article_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(File.expand_path(__FILE__)) + '/test_helper' 2 | 3 | describe Article do 4 | it { should normalize_attribute(:title).from(' Social Life at the Edge of Chaos ').to('Social Life at the Edge of Chaos') } 5 | it { should normalize_attribute(:slug) } 6 | it { should normalize_attribute(:slug).from(' Social Life at the Edge of Chaos ').to('social-life-at-the-edge-of-chaos') } 7 | it { should normalize_attribute(:limited_slug) } 8 | it { should normalize_attribute(:limited_slug).from(' Social Life at the Edge of Chaos ').to('social-life') } 9 | 10 | context 'normalization should not interfere with other hooks and aliases on the attribute assignment' do 11 | before do 12 | @article = Article.create!(:title => 'Original Title') 13 | end 14 | 15 | it 'should still reflect that the attribute has been changed through the call to super' do 16 | expect{ @article.title = 'New Title' }.to change(@article, :title).from('Original Title').to('New Title') 17 | end 18 | end 19 | 20 | context 'when another instance of the same saved record has been changed' do 21 | before do 22 | @article = Article.create!(:title => 'Original Title') 23 | @article2 = Article.find(@article.id) 24 | @article2.update_attributes(:title => 'New Title') 25 | end 26 | 27 | it "should reflect the change when the record is reloaded" do 28 | expect{ @article.reload }.to change(@article, :title).from('Original Title').to('New Title') 29 | end 30 | end 31 | 32 | context 'normalization should work with multiple attributes at the same time' do 33 | before do 34 | @article = Article.new(:slug => ' Bad Slug ', :limited_slug => ' Bad Limited Slug ') 35 | end 36 | 37 | it "should apply normalizations to both attributes" do 38 | expect(@article.slug).to eq 'bad-slug' 39 | expect(@article.limited_slug).to eq 'bad-limited' 40 | end 41 | end 42 | 43 | context 'with the default normalizer changed' do 44 | it "only strips leading and trailing whitespace" do 45 | @book = Book.new :author => ' testing the default normalizer ' 46 | expect(@book.author).to eq('testing the default normalizer') 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/attribute_normalizer.rb: -------------------------------------------------------------------------------- 1 | require 'attribute_normalizer/normalizers/blank_normalizer' 2 | require 'attribute_normalizer/normalizers/phone_normalizer' 3 | require 'attribute_normalizer/normalizers/strip_normalizer' 4 | require 'attribute_normalizer/normalizers/squish_normalizer' 5 | require 'attribute_normalizer/normalizers/whitespace_normalizer' 6 | require 'attribute_normalizer/normalizers/boolean_normalizer' 7 | require 'attribute_normalizer/normalizers/control_chars_normalizer' 8 | 9 | module AttributeNormalizer 10 | 11 | class MissingNormalizer < ArgumentError; end 12 | 13 | class << self 14 | attr_accessor :configuration 15 | end 16 | 17 | def self.configuration 18 | @configuration ||= Configuration.new 19 | end 20 | 21 | def self.configure 22 | yield(configuration) 23 | end 24 | 25 | 26 | class Configuration 27 | attr_accessor :default_normalizers, :normalizers 28 | 29 | def default_normalizers=(normalizers) 30 | @default_normalizers = normalizers.is_a?(Array) ? normalizers : [ normalizers ] 31 | end 32 | 33 | def initialize 34 | 35 | @normalizers = { 36 | :blank => AttributeNormalizer::Normalizers::BlankNormalizer, 37 | :phone => AttributeNormalizer::Normalizers::PhoneNormalizer, 38 | :squish => AttributeNormalizer::Normalizers::SquishNormalizer, 39 | :strip => AttributeNormalizer::Normalizers::StripNormalizer, 40 | :whitespace => AttributeNormalizer::Normalizers::WhitespaceNormalizer, 41 | :boolean => AttributeNormalizer::Normalizers::BooleanNormalizer, 42 | :control_chars => AttributeNormalizer::Normalizers::ControlCharsNormalizer 43 | } 44 | 45 | @default_normalizers = [ :strip, :blank ] 46 | 47 | end 48 | 49 | end 50 | 51 | end 52 | 53 | 54 | require 'attribute_normalizer/model_inclusions' 55 | require 'attribute_normalizer/rspec_matcher' 56 | 57 | def include_attribute_normalizer(class_or_module) 58 | return if class_or_module.include?(AttributeNormalizer) 59 | class_or_module.class_eval do 60 | extend AttributeNormalizer::ClassMethods 61 | end 62 | end 63 | 64 | 65 | include_attribute_normalizer(ActiveModel::Base) if defined?(ActiveModel::Base) 66 | include_attribute_normalizer(ActiveRecord::Base) if defined?(ActiveRecord::Base) 67 | include_attribute_normalizer(CassandraObject::Base) if defined?(CassandraObject::Base) 68 | -------------------------------------------------------------------------------- /lib/attribute_normalizer/model_inclusions.rb: -------------------------------------------------------------------------------- 1 | module AttributeNormalizer 2 | 3 | def self.included(base) 4 | base.extend ClassMethods 5 | end 6 | 7 | module ClassMethods 8 | 9 | def normalize_attributes(*attributes, &block) 10 | options = attributes.last.is_a?(::Hash) ? attributes.pop : {} 11 | 12 | normalizers = [ options[:with] ].flatten.compact 13 | normalizers = [ options[:before] ].flatten.compact if block_given? && normalizers.empty? 14 | post_normalizers = [ options[:after] ].flatten.compact if block_given? 15 | 16 | if normalizers.empty? && !block_given? 17 | normalizers = AttributeNormalizer.configuration.default_normalizers # the default normalizers 18 | end 19 | 20 | attributes.each do |attribute| 21 | define_method "normalize_#{attribute}" do |value| 22 | normalized = value 23 | 24 | normalizers.each do |normalizer_name| 25 | unless normalizer_name.kind_of?(Symbol) 26 | normalizer_name, options = normalizer_name.keys[0], normalizer_name[ normalizer_name.keys[0] ] 27 | end 28 | normalizer = AttributeNormalizer.configuration.normalizers[normalizer_name] 29 | raise AttributeNormalizer::MissingNormalizer.new("No normalizer was found for #{normalizer_name}") unless normalizer 30 | normalized = normalizer.respond_to?(:normalize) ? normalizer.normalize( normalized , options) : normalizer.call(normalized, options) 31 | end 32 | 33 | normalized = block_given? ? instance_exec(normalized, &block) : normalized 34 | 35 | if block_given? 36 | post_normalizers.each do |normalizer_name| 37 | unless normalizer_name.kind_of?(Symbol) 38 | normalizer_name, options = normalizer_name.keys[0], normalizer_name[ normalizer_name.keys[0] ] 39 | end 40 | normalizer = AttributeNormalizer.configuration.normalizers[normalizer_name] 41 | raise AttributeNormalizer::MissingNormalizer.new("No normalizer was found for #{normalizer_name}") unless normalizer 42 | normalized = normalizer.respond_to?(:normalize) ? normalizer.normalize( normalized , options) : normalizer.call(normalized, options) 43 | end 44 | end 45 | 46 | normalized 47 | end 48 | 49 | self.send :private, "normalize_#{attribute}" 50 | 51 | if method_defined?(:"#{attribute}=") 52 | alias_method "old_#{attribute}=", "#{attribute}=" 53 | 54 | define_method "#{attribute}=" do |value| 55 | normalized_value = self.send(:"normalize_#{attribute}", value) 56 | self.send("old_#{attribute}=", normalized_value) 57 | end 58 | else 59 | define_method "#{attribute}=" do |value| 60 | super(self.send(:"normalize_#{attribute}", value)) 61 | end 62 | end 63 | 64 | end 65 | end 66 | 67 | alias :normalize_attribute :normalize_attributes 68 | 69 | end 70 | 71 | end 72 | -------------------------------------------------------------------------------- /README.textile: -------------------------------------------------------------------------------- 1 | h1. Attribute Normalizer 2 | 3 | !https://secure.travis-ci.org/mdeering/attribute_normalizer.png?branch=master(Build Status)!:http://travis-ci.org/mdeering/attribute_normalizer 4 | 5 | p. A little normalization goes a long way in helping maintain data integrity. 6 | 7 | h2. Change History 8 | * 1.1.0 9 | ** Allow the use of default normalizers before and after the evaluation of a given block 10 | 11 | * 1.0.0 12 | ** -DSL Changes- 13 | ** Default attributes to normalize 14 | ** mongid support 15 | 16 | * 0.3.0 17 | ** Normalizer Chaining 18 | ** Built-in common normalizers 19 | ** Ability to change the default attribute normalization. 20 | 21 | * 0.2.1 22 | ** ActiveModel Support Built-in 23 | ** Fix for :with option getting dropped when normalizing several attributes 24 | 25 | * 0.2.0 26 | ** Removed the normalization on reads. 27 | ** Added out of the box support for CassandraObjects 28 | ** Included RSpec matcher _normalizer_attribute_ 29 | ** Added the ability to define global normalizers 30 | ** *Strings no longer get 'strip' called on them before getting passed on to your defined normalization blocks* 31 | 32 | h2. Supported ORMs 33 | 34 | * _Active Model_ 35 | * Active Record 36 | * CassandraObjects 37 | 38 | p. _I will gladly take pull requests to automatically load Attribute Normalizer into your ORM of choice if requested. To test it out on your ORM just include the AttributeNormalizer module after requiring it._ 39 | 40 |
# your_initializer.rb
 41 | require 'attribute_normalizer'
 42 | YourORM::Base.send :include, AttributeNormalizer
43 | 44 | h2. Install 45 | 46 |
sudo gem install attribute_normalizer
47 | 48 | p. Then just required it. Rails usages is as follows. 49 | 50 | h3. Rails 2 51 | 52 |
# config/environment.rb
 53 | config.gem 'attribute_normalizer'
54 | 55 | h3. Rails 3 56 | 57 |
# Gemfile
 58 | gem 'attribute_normalizer'
59 | 60 | p. It also still works as a traditional Rails plugin. 61 | 62 |
./script/plugin install git://github.com/mdeering/attribute_normalizer.git
63 | 64 | h2. Usage 65 | 66 | p. Lets create a quick test/spec for what we want to accomplish as far as normalization using the built in RSpec matcher. 67 | 68 |
# spec/models/book_spec.rb
 69 | describe Book do
 70 |   it { should normalize_attribute(:author) }
 71 |   it { should normalize_attribute(:price).from('$3,450.98').to(3450.98) }
 72 |   it { should normalize_attribute(:summary).from('   Here is my summary that is a little to long   ').to('Here is m...') }
 73 |   it { should normalize_attribute(:title).from(' pick up chicks with magic tricks  ').to('Pick Up Chicks With Magic Tricks') }
 74 |   it { should normalize_attribute(:slug).from(' Social Life at the Edge of Chaos    ').to('social-life-at-the-edge-of-chaos') }
 75 |   it { should normalize_attribute(:limited_slug).from(' Social Life at the Edge of Chaos    ').to('social-life') }
 76 | end
77 | 78 | p. The following normalizers are already included with the +0.3 version of the gem. 79 | 80 | * _:blank_ Will return _nil_ on empty strings 81 | * _:phone_ Will strip out all non-digit characters and return nil on empty strings 82 | * _:strip_ Will strip leading and trailing whitespace. 83 | * _:squish_ Will strip leading and trailing whitespace and convert any consecutive spaces to one space each 84 | 85 | p. And lets predefine some normalizers that we may use in other classes/models or that we don't want to clutter up our class/model's readability with. 86 | 87 |
# config/initializers/attribute_normalizer.rb
 88 | AttributeNormalizer.configure do |config|
 89 | 
 90 |   config.normalizers[:currency] = lambda do |value, options|
 91 |     value.is_a?(String) ? value.gsub(/[^0-9\.]+/, '') : value
 92 |   end
 93 | 
 94 |   config.normalizers[:truncate] = lambda do |text, options|
 95 |     if text.is_a?(String)
 96 |       options.reverse_merge!(:length => 30, :omission => "...")
 97 |       l = options[:length] - options[:omission].mb_chars.length
 98 |       chars = text.mb_chars
 99 |       (chars.length > options[:length] ? chars[0...l] + options[:omission] : text).to_s
100 |     else
101 |       text
102 |     end
103 |   end
104 | 
105 |   # The default normalizers if no :with option or block is given is to apply the :strip and :blank normalizers (in that order).
106 |   # You can change this if you would like as follows:
107 |   # config.default_normalizers = :strip, :blank
108 | 
109 | end
110 | 
111 | 112 | The _normalize_attributes_ method is eager loaded into your ORM. _normalize_attribute_ is aliased to _normalize_attributes_ and both can take in a single attribute or an array of attributes. 113 | 114 |
class Book < ActiveRecord::Base
115 | 
116 |   # By default it will strip leading and trailing whitespace
117 |   # and set to nil if blank.
118 |   normalize_attributes :author, :publisher
119 | 
120 |   # Using one of our predefined normalizers.
121 |   normalize_attribute  :price, :with => :currency
122 | 
123 |   # Using more then one of our predefined normalizers including one with options
124 |   normalize_attribute :summary, :with => [ :strip, { :truncate => { :length => 12 } } ]
125 | 
126 |   # You can also define your normalization block inline.
127 |   normalize_attribute :title do |value|
128 |     value.is_a?(String) ? value.titleize.strip : value
129 |   end
130 | 
131 |   # Or use a combination of normalizers plus an inline block.
132 |   # the normalizers in the :with option will each be evalulated
133 |   # in order and the result will be given to the block.
134 |   # You could also use option :before in place of :with
135 |   normalize_attribute :slug, :with => [ :strip, :blank ] do |value|
136 |     value.present? && value.is_a?(String) ? value.downcase.gsub(/\s+/, '-') : value
137 |   end
138 | 
139 |   # Use builtin normalizers before and after the evaluation of your inline
140 |   # block
141 |   normalize_attribute :limited_slug, :before => [ :strip, :blank ], :after => [ { :truncate => { :length => 11, :omission => '' } } ] do |value|
142 |     value.present? && value.is_a?(String) ? value.downcase.gsub(/\s+/, '-') : value
143 |   end
144 | 
145 | end
146 | 147 | p. All the specs will pass now. Here is quick look at the behaviour from a console. 148 | 149 |
summary = 'Here is my summary that is a little to long'
150 | title   = 'pick up chicks with magic tricks'
151 | book    = Book.create!(:author => '', :price => '$3,450.89', :summary => summary, :title => title, :slug => title, :limited_slug => title)
152 | book.author       # => nil
153 | book.price        # => 3450.89
154 | book.summary      # => 'Here is m...'
155 | book.title        # => 'Pick Up Chicks With Magic Tricks'
156 | book.slug         # => 'pick-up-chicks-with-magic-tricks'
157 | book.limited_slug # => 'pick-up-chi'
158 | 
159 | 160 | h2. Test Helpers 161 | 162 | p. If you are running RSpec there is a matcher available for use. Usage can been seen above. Include it as follows. 163 | 164 | h3. Rails 2 165 | 166 |
# spec/spec_helper.rb
167 | RSpec.configure do |config|
168 |   config.include AttributeNormalizer::RSpecMatcher, :type => :models
169 | end
170 | 171 | h3. Rails 3 172 | 173 |
# spec/spec_helper.rb
174 | RSpec.configure do |config|
175 |   config.include AttributeNormalizer::RSpecMatcher, :type => :model
176 | end
177 | 178 | p. _I will gladly take a patch to add a macro to Test::Unit if someone submits it._ 179 | 180 | h2. Credits 181 | 182 | Original module code and concept was taken from "Dan Kubb":http://github.com/dkubb during a project we worked on together. I found that I was consistently using this across all my projects so I wanted to plugin-er-size and gem this up for easy reuse. 183 | 184 | h2. Copyright 185 | 186 | Copyright (c) 2009-2010 "Michael Deering(Edmonton Ruby on Rails)":http://mdeering.com See MIT-LICENSE for details. 187 | --------------------------------------------------------------------------------