├── .gitignore ├── .rspec ├── .rubocop.yml ├── .rubocop_todo.yml ├── .travis.yml ├── CHANGELOG.md ├── Gemfile ├── Guardfile ├── LICENSE ├── README.md ├── Rakefile ├── lib ├── data │ └── country_codes.yaml ├── phony_rails.rb ├── phony_rails │ ├── locales │ │ ├── de.yml │ │ ├── en.yml │ │ ├── es.yml │ │ ├── fr.yml │ │ ├── he.yml │ │ ├── it.yml │ │ ├── ja.yml │ │ ├── km.yml │ │ ├── ko.yml │ │ ├── nb.yml │ │ ├── nl.yml │ │ ├── pt.yml │ │ ├── ru.yml │ │ ├── tr.yml │ │ └── uk.yml │ ├── string_extensions.rb │ └── version.rb └── validators │ └── phony_validator.rb ├── phony_rails.gemspec └── spec ├── lib ├── phony_rails_spec.rb └── validators │ └── phony_validator_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.rvmrc 3 | /.idea/ 4 | .ruby-gemset 5 | .ruby-version 6 | coverage/* 7 | /.bundle 8 | /vendor 9 | Gemfile.lock 10 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --colour 2 | --drb 3 | --format documentation 4 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: .rubocop_todo.yml 2 | require: rubocop-performance 3 | AllCops: 4 | TargetRubyVersion: 2.4 5 | Metrics/BlockLength: 6 | ExcludedMethods: ['describe', 'context', 'define', 'shared_examples_for'] 7 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config` 3 | # on 2016-03-12 20:44:43 +0100 using RuboCop version 0.38.0. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | # Offense count: 2 10 | # Configuration parameters: AllowSafeAssignment. 11 | Lint/AssignmentInCondition: 12 | Exclude: 13 | - 'lib/phony_rails.rb' 14 | 15 | # Offense count: 2 16 | # Cop supports --auto-correct. 17 | Lint/SendWithMixinArgument: 18 | Exclude: 19 | - 'lib/phony_rails.rb' 20 | 21 | # Offense count: 2 22 | # Configuration parameters: AllowKeywordBlockArguments. 23 | Lint/UnderscorePrefixedVariableName: 24 | Exclude: 25 | - 'lib/phony_rails.rb' 26 | 27 | # Offense count: 4 28 | Metrics/AbcSize: 29 | Max: 41 30 | 31 | # Offense count: 3 32 | Metrics/CyclomaticComplexity: 33 | Max: 21 34 | 35 | # Offense count: 162 36 | # Configuration parameters: AllowHeredoc, AllowURI, URISchemes. 37 | # URISchemes: http, https 38 | Metrics/LineLength: 39 | Max: 177 40 | 41 | # Offense count: 4 42 | # Configuration parameters: CountComments. 43 | Metrics/MethodLength: 44 | Max: 25 45 | 46 | # Offense count: 2 47 | Metrics/PerceivedComplexity: 48 | Max: 23 49 | 50 | # Offense count: 1 51 | # Cop supports --auto-correct. 52 | Performance/StartWith: 53 | Exclude: 54 | - 'lib/phony_rails.rb' 55 | 56 | # Offense count: 1 57 | # Cop supports --auto-correct. 58 | Performance/RegexpMatch: 59 | Exclude: 60 | - 'lib/phony_rails.rb' 61 | 62 | # Offense count: 1 63 | # Configuration parameters: EnforcedStyle, SupportedStyles. 64 | # SupportedStyles: nested, compact 65 | Style/ClassAndModuleChildren: 66 | Exclude: 67 | - 'lib/phony_rails.rb' 68 | 69 | # Offense count: 7 70 | Style/Documentation: 71 | Exclude: 72 | - 'spec/**/*' 73 | - 'test/**/*' 74 | - 'lib/phony_rails.rb' 75 | - 'lib/phony_rails/string_extensions.rb' 76 | - 'lib/validators/phony_validator.rb' 77 | 78 | Style/RescueModifier: 79 | Enabled: false 80 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.4.4 4 | - 2.5.5 5 | - 2.6.2 6 | - 3.0.0 7 | script: 8 | - bundle exec rspec spec 9 | - bundle exec rubocop 10 | deploy: 11 | skip_cleanup: true 12 | provider: rubygems 13 | api_key: 14 | secure: KP9HSdb/cWP66VD57MgLG3c3H6nlzWH0h6jF/j4Hi5ylJH1BcAU7FYB8ej2yj/WvCCk8Nwc6Pvr3xipAkDzRPCeDiZk0MPu0U+nnE+vlytZ3wmcoo0puDSRxn2KMjCv78ZdgbQkE3ik1a7TsmaxMMQGNrkT5ADDi2XHBneyCPYs= 15 | on: 16 | tags: true 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [v0.15.0](https://github.com/joost/phony_rails/tree/v0.15.0) (2021-04-15) 4 | 5 | [Full Changelog](https://github.com/joost/phony_rails/compare/v0.14.13...v0.15.0) 6 | 7 | **Closed issues:** 8 | 9 | - normalized\_when\_valid: true seems broken if there are two phone attributes [\#195](https://github.com/joost/phony_rails/issues/195) 10 | 11 | **Merged pull requests:** 12 | 13 | - Test fixes - No mongoid [\#207](https://github.com/joost/phony_rails/pull/207) ([joost](https://github.com/joost)) 14 | - Updated requirements \(had to remove mongodb compatibility\) [\#206](https://github.com/joost/phony_rails/pull/206) ([joost](https://github.com/joost)) 15 | - Updated phony gem requirements to be Ruby 3 compatible [\#205](https://github.com/joost/phony_rails/pull/205) ([NikoRoberts](https://github.com/NikoRoberts)) 16 | - Make CI Green Again [\#201](https://github.com/joost/phony_rails/pull/201) ([amatsuda](https://github.com/amatsuda)) 17 | 18 | ## [v0.14.13](https://github.com/joost/phony_rails/tree/v0.14.13) (2019-07-03) 19 | 20 | [Full Changelog](https://github.com/joost/phony_rails/compare/v0.14.12...v0.14.13) 21 | 22 | **Closed issues:** 23 | 24 | - 0.4.11 was the latest release. However the new normalize\_when\_valid has been sitting here for a while... Release? [\#194](https://github.com/joost/phony_rails/issues/194) 25 | 26 | **Merged pull requests:** 27 | 28 | - Fixing \#195 - Original value should be cached in correct instance var [\#196](https://github.com/joost/phony_rails/pull/196) ([dlikhten](https://github.com/dlikhten)) 29 | 30 | ## [v0.14.12](https://github.com/joost/phony_rails/tree/v0.14.12) (2019-06-21) 31 | 32 | [Full Changelog](https://github.com/joost/phony_rails/compare/v0.14.11...v0.14.12) 33 | 34 | **Closed issues:** 35 | 36 | - Some German numbers not passing plausible\_numbers? without country\_code [\#193](https://github.com/joost/phony_rails/issues/193) 37 | 38 | **Merged pull requests:** 39 | 40 | - Upgrade Ruby versions and Relax dependancies [\#192](https://github.com/joost/phony_rails/pull/192) ([berkos](https://github.com/berkos)) 41 | - Add possibility to return original phone number when is not valid [\#190](https://github.com/joost/phony_rails/pull/190) ([synion](https://github.com/synion)) 42 | - Add UK country\_code. [\#189](https://github.com/joost/phony_rails/pull/189) ([pudiva](https://github.com/pudiva)) 43 | 44 | ## [v0.14.11](https://github.com/joost/phony_rails/tree/v0.14.11) (2018-10-11) 45 | 46 | [Full Changelog](https://github.com/joost/phony_rails/compare/v0.14.10...v0.14.11) 47 | 48 | **Closed issues:** 49 | 50 | - Problem with normalizing Estonian number [\#187](https://github.com/joost/phony_rails/issues/187) 51 | 52 | **Merged pull requests:** 53 | 54 | - Add Korean translation\(including spec\) [\#186](https://github.com/joost/phony_rails/pull/186) ([freelyageha](https://github.com/freelyageha)) 55 | 56 | ## [v0.14.10](https://github.com/joost/phony_rails/tree/v0.14.10) (2018-10-11) 57 | 58 | [Full Changelog](https://github.com/joost/phony_rails/compare/v0.14.9...v0.14.10) 59 | 60 | **Closed issues:** 61 | 62 | - PhonyRails.default\_country\_code too intrusive in plausible\_number? method [\#179](https://github.com/joost/phony_rails/issues/179) 63 | - default\_country\_code overrides 00-prefix country code [\#175](https://github.com/joost/phony_rails/issues/175) 64 | - Valid 260 area code number is reported as invalid [\#168](https://github.com/joost/phony_rails/issues/168) 65 | - Croatian number not validating as plausible. [\#165](https://github.com/joost/phony_rails/issues/165) 66 | - phony\_formatted not returning original String for non-digit only strings [\#163](https://github.com/joost/phony_rails/issues/163) 67 | - should all normalized numbers be valid? [\#162](https://github.com/joost/phony_rails/issues/162) 68 | - Extensions not working out of the box for validator [\#160](https://github.com/joost/phony_rails/issues/160) 69 | 70 | **Merged pull requests:** 71 | 72 | - Remove errant `puts` [\#188](https://github.com/joost/phony_rails/pull/188) ([stevenharman](https://github.com/stevenharman)) 73 | - Close issues [\#185](https://github.com/joost/phony_rails/pull/185) ([joost](https://github.com/joost)) 74 | - Better PhonyRails.plausible\_number? method. Closes \#179. [\#184](https://github.com/joost/phony_rails/pull/184) ([joost](https://github.com/joost)) 75 | - Allow numbers starting with 00 country codes. Closes \#175. [\#183](https://github.com/joost/phony_rails/pull/183) ([joost](https://github.com/joost)) 76 | 77 | ## [v0.14.9](https://github.com/joost/phony_rails/tree/v0.14.9) (2018-09-05) 78 | 79 | [Full Changelog](https://github.com/joost/phony_rails/compare/v0.14.7...v0.14.9) 80 | 81 | **Closed issues:** 82 | 83 | - Cut new release 0.14.7 to include new extension removal logic [\#177](https://github.com/joost/phony_rails/issues/177) 84 | - default\_country\_code based on relation [\#174](https://github.com/joost/phony_rails/issues/174) 85 | 86 | **Merged pull requests:** 87 | 88 | - Allow brackets. Closes \#170. [\#182](https://github.com/joost/phony_rails/pull/182) ([joost](https://github.com/joost)) 89 | - add lambda support for default country code [\#181](https://github.com/joost/phony_rails/pull/181) ([kimyu92](https://github.com/kimyu92)) 90 | - Add Spanish translation for validation error [\#178](https://github.com/joost/phony_rails/pull/178) ([r-sierra](https://github.com/r-sierra)) 91 | 92 | ## [v0.14.7](https://github.com/joost/phony_rails/tree/v0.14.7) (2018-05-25) 93 | 94 | [Full Changelog](https://github.com/joost/phony_rails/compare/v0.14.6...v0.14.7) 95 | 96 | **Closed issues:** 97 | 98 | - Country code wrapped in brackets is not recognized [\#170](https://github.com/joost/phony_rails/issues/170) 99 | - Invalid number in countryCode VN [\#159](https://github.com/joost/phony_rails/issues/159) 100 | 101 | **Merged pull requests:** 102 | 103 | - Extension option added to normalize\_number [\#176](https://github.com/joost/phony_rails/pull/176) ([ramaboo](https://github.com/ramaboo)) 104 | - return country from phone number [\#169](https://github.com/joost/phony_rails/pull/169) ([aovertus](https://github.com/aovertus)) 105 | - Fix code example in README to match description [\#167](https://github.com/joost/phony_rails/pull/167) ([mattruzicka](https://github.com/mattruzicka)) 106 | 107 | ## [v0.14.6](https://github.com/joost/phony_rails/tree/v0.14.6) (2017-06-20) 108 | 109 | [Full Changelog](https://github.com/joost/phony_rails/compare/v0.14.5...v0.14.6) 110 | 111 | **Closed issues:** 112 | 113 | - Fail validation on raw input [\#161](https://github.com/joost/phony_rails/issues/161) 114 | - Extension example in README does not work [\#155](https://github.com/joost/phony_rails/issues/155) 115 | - Switching the dependancy from `ActiveRecord::Base` to `ActiveModel::Model` breaks support for Rails 3 apps [\#147](https://github.com/joost/phony_rails/issues/147) 116 | 117 | **Merged pull requests:** 118 | 119 | - Conditional Normalization [\#166](https://github.com/joost/phony_rails/pull/166) ([Ross-Hunter](https://github.com/Ross-Hunter)) 120 | - Fixed belongs\_to\_required\_by\_default in Rails 5 [\#158](https://github.com/joost/phony_rails/pull/158) ([olivierpichon](https://github.com/olivierpichon)) 121 | - `subbed` always return number [\#157](https://github.com/joost/phony_rails/pull/157) ([mrclmrvn](https://github.com/mrclmrvn)) 122 | 123 | ## [v0.14.5](https://github.com/joost/phony_rails/tree/v0.14.5) (2017-02-08) 124 | 125 | [Full Changelog](https://github.com/joost/phony_rails/compare/v0.14.4...v0.14.5) 126 | 127 | **Closed issues:** 128 | 129 | - phone number not being validated [\#154](https://github.com/joost/phony_rails/issues/154) 130 | - Make phony\_normalize optional, on condition [\#149](https://github.com/joost/phony_rails/issues/149) 131 | 132 | **Merged pull requests:** 133 | 134 | - Fix Rails 3 compatibility in issue \#147 [\#156](https://github.com/joost/phony_rails/pull/156) ([wvanheerde](https://github.com/wvanheerde)) 135 | - Support of phone numbers with extension in validator [\#153](https://github.com/joost/phony_rails/pull/153) ([Kukunin](https://github.com/Kukunin)) 136 | 137 | ## [v0.14.4](https://github.com/joost/phony_rails/tree/v0.14.4) (2016-10-10) 138 | 139 | [Full Changelog](https://github.com/joost/phony_rails/compare/v0.14.2...v0.14.4) 140 | 141 | **Closed issues:** 142 | 143 | - Add support for internal Russian phone style \(8 926 ... instead of +7 926 ...\) [\#148](https://github.com/joost/phony_rails/issues/148) 144 | - is it necessary to extend ActiveRecord::Base instead of ActiveModel::Model ? [\#143](https://github.com/joost/phony_rails/issues/143) 145 | 146 | **Merged pull requests:** 147 | 148 | - Bundle updates and fixes Travis [\#151](https://github.com/joost/phony_rails/pull/151) ([joost](https://github.com/joost)) 149 | 150 | ## [v0.14.2](https://github.com/joost/phony_rails/tree/v0.14.2) (2016-06-16) 151 | 152 | [Full Changelog](https://github.com/joost/phony_rails/compare/v0.14.1...v0.14.2) 153 | 154 | **Merged pull requests:** 155 | 156 | - Do not use insecure multiline regex in examples [\#146](https://github.com/joost/phony_rails/pull/146) ([bdewater](https://github.com/bdewater)) 157 | - support for ActiveModel::Model alternative to database-backed models only [\#144](https://github.com/joost/phony_rails/pull/144) ([brandondees](https://github.com/brandondees)) 158 | 159 | ## [v0.14.1](https://github.com/joost/phony_rails/tree/v0.14.1) (2016-05-08) 160 | 161 | [Full Changelog](https://github.com/joost/phony_rails/compare/v0.14.0...v0.14.1) 162 | 163 | **Closed issues:** 164 | 165 | - Pull request \#139 \(released in 0.14.0\) breaks message: :improbable\_phone option [\#140](https://github.com/joost/phony_rails/issues/140) 166 | 167 | **Merged pull requests:** 168 | 169 | - Fixed a bug that prevents a normalized attribute from being set to nil [\#142](https://github.com/joost/phony_rails/pull/142) ([kylerippey](https://github.com/kylerippey)) 170 | - Read message value directly from options [\#141](https://github.com/joost/phony_rails/pull/141) ([monfresh](https://github.com/monfresh)) 171 | 172 | ## [v0.14.0](https://github.com/joost/phony_rails/tree/v0.14.0) (2016-04-21) 173 | 174 | [Full Changelog](https://github.com/joost/phony_rails/compare/v0.13.0...v0.14.0) 175 | 176 | **Closed issues:** 177 | 178 | - In normalize\_number, .clone is being used, which preserves "frozenness", causing method to fail sometimes [\#136](https://github.com/joost/phony_rails/issues/136) 179 | - question Is thr any way to find country code from mobile no? [\#135](https://github.com/joost/phony_rails/issues/135) 180 | - invalid number assumed to be valid [\#130](https://github.com/joost/phony_rails/issues/130) 181 | - Split fails when a + is present [\#123](https://github.com/joost/phony_rails/issues/123) 182 | 183 | **Merged pull requests:** 184 | 185 | - Adds ability to pass symbols as option values to phony model helpers [\#139](https://github.com/joost/phony_rails/pull/139) ([jonathan-wheeler](https://github.com/jonathan-wheeler)) 186 | - Add support for phone numbers with extensions [\#138](https://github.com/joost/phony_rails/pull/138) ([jerryclinesmith](https://github.com/jerryclinesmith)) 187 | - Add support for a default country code [\#137](https://github.com/joost/phony_rails/pull/137) ([jerryclinesmith](https://github.com/jerryclinesmith)) 188 | - Added first RuboCop stuff. [\#134](https://github.com/joost/phony_rails/pull/134) ([joost](https://github.com/joost)) 189 | 190 | ## [v0.13.0](https://github.com/joost/phony_rails/tree/v0.13.0) (2016-03-12) 191 | 192 | [Full Changelog](https://github.com/joost/phony_rails/compare/v0.12.11...v0.13.0) 193 | 194 | **Closed issues:** 195 | 196 | - Adding country code twice for Luxemburg numbers [\#128](https://github.com/joost/phony_rails/issues/128) 197 | - Unexpected result when calling normalize\_number multiple times with country\_code option [\#126](https://github.com/joost/phony_rails/issues/126) 198 | - No method find\_by\_normalized\_phone\_number [\#125](https://github.com/joost/phony_rails/issues/125) 199 | - Invalid number is valid? [\#124](https://github.com/joost/phony_rails/issues/124) 200 | - Can it validate mobile phone? [\#122](https://github.com/joost/phony_rails/issues/122) 201 | 202 | **Merged pull requests:** 203 | 204 | - Do not raise NoMethodError when an illegal country code is set [\#133](https://github.com/joost/phony_rails/pull/133) ([klaustopher](https://github.com/klaustopher)) 205 | - only assigned normalize values if there is one [\#132](https://github.com/joost/phony_rails/pull/132) ([ekkyou](https://github.com/ekkyou)) 206 | - Adding Kosovo phone code [\#131](https://github.com/joost/phony_rails/pull/131) ([Xanders](https://github.com/Xanders)) 207 | - Add Dutch translation for invalid number [\#129](https://github.com/joost/phony_rails/pull/129) ([bdewater](https://github.com/bdewater)) 208 | 209 | ## [v0.12.11](https://github.com/joost/phony_rails/tree/v0.12.11) (2015-11-12) 210 | 211 | [Full Changelog](https://github.com/joost/phony_rails/compare/v0.12.9...v0.12.11) 212 | 213 | **Closed issues:** 214 | 215 | - French formatting [\#121](https://github.com/joost/phony_rails/issues/121) 216 | - French phony\_normalize [\#120](https://github.com/joost/phony_rails/issues/120) 217 | - Correct phone number failed the validatiton [\#115](https://github.com/joost/phony_rails/issues/115) 218 | - 'no implicit conversion of nil into String' from phony\_formatted!\(spaces: '-', strict: true\) with invalid numbers [\#113](https://github.com/joost/phony_rails/issues/113) 219 | - Can i skip a validation with another number? [\#110](https://github.com/joost/phony_rails/issues/110) 220 | - Consider dropping the dependency on the countries gem and using a YAML file instead [\#108](https://github.com/joost/phony_rails/issues/108) 221 | - Some Finnish mobile numbers are formatted wrong [\#107](https://github.com/joost/phony_rails/issues/107) 222 | - undefined method `\[\]' for Data:Class [\#106](https://github.com/joost/phony_rails/issues/106) 223 | - Phony is out of date [\#102](https://github.com/joost/phony_rails/issues/102) 224 | 225 | **Merged pull requests:** 226 | 227 | - Update readme [\#117](https://github.com/joost/phony_rails/pull/117) ([toydestroyer](https://github.com/toydestroyer)) 228 | - Add uk, ru error message translations [\#114](https://github.com/joost/phony_rails/pull/114) ([shhavel](https://github.com/shhavel)) 229 | - Update phony\_rails.gemspec [\#112](https://github.com/joost/phony_rails/pull/112) ([Agsiegert](https://github.com/Agsiegert)) 230 | - Don't re-parse country codes YAML file every time it's needed. [\#111](https://github.com/joost/phony_rails/pull/111) ([jcoleman](https://github.com/jcoleman)) 231 | - Replace countries dependency with YAML file [\#109](https://github.com/joost/phony_rails/pull/109) ([monfresh](https://github.com/monfresh)) 232 | 233 | ## [v0.12.9](https://github.com/joost/phony_rails/tree/v0.12.9) (2015-07-13) 234 | 235 | [Full Changelog](https://github.com/joost/phony_rails/compare/v0.12.8...v0.12.9) 236 | 237 | **Closed issues:** 238 | 239 | - Countries 0.11.5 introduces a breaking change [\#103](https://github.com/joost/phony_rails/issues/103) 240 | 241 | **Merged pull requests:** 242 | 243 | - Get country data in a more straight forward way [\#105](https://github.com/joost/phony_rails/pull/105) ([humancopy](https://github.com/humancopy)) 244 | - Replace Data with Setup.data [\#104](https://github.com/joost/phony_rails/pull/104) ([monfresh](https://github.com/monfresh)) 245 | 246 | ## [v0.12.8](https://github.com/joost/phony_rails/tree/v0.12.8) (2015-06-22) 247 | 248 | [Full Changelog](https://github.com/joost/phony_rails/compare/v0.12.7...v0.12.8) 249 | 250 | **Closed issues:** 251 | 252 | - activerecord dependency [\#99](https://github.com/joost/phony_rails/issues/99) 253 | - Using a number different from the country [\#97](https://github.com/joost/phony_rails/issues/97) 254 | - UK 0203 numbers not handled correctly [\#95](https://github.com/joost/phony_rails/issues/95) 255 | - Consider keeping a Changelog for changes in each version. [\#91](https://github.com/joost/phony_rails/issues/91) 256 | - Phone numbers with extensions [\#78](https://github.com/joost/phony_rails/issues/78) 257 | 258 | **Merged pull requests:** 259 | 260 | - remove active\_record dependency [\#100](https://github.com/joost/phony_rails/pull/100) ([sbounmy](https://github.com/sbounmy)) 261 | - Add enforce\_record\_country option to phony\_normalize method [\#98](https://github.com/joost/phony_rails/pull/98) ([phillipp](https://github.com/phillipp)) 262 | 263 | ## [v0.12.7](https://github.com/joost/phony_rails/tree/v0.12.7) (2015-06-18) 264 | 265 | [Full Changelog](https://github.com/joost/phony_rails/compare/v0.12.6...v0.12.7) 266 | 267 | **Closed issues:** 268 | 269 | - inconsistent normalization [\#93](https://github.com/joost/phony_rails/issues/93) 270 | 271 | **Merged pull requests:** 272 | 273 | - Adding default error translation for the Hebrew language [\#96](https://github.com/joost/phony_rails/pull/96) ([pazaricha](https://github.com/pazaricha)) 274 | 275 | ## [v0.12.6](https://github.com/joost/phony_rails/tree/v0.12.6) (2015-05-11) 276 | 277 | [Full Changelog](https://github.com/joost/phony_rails/compare/v0.12.5...v0.12.6) 278 | 279 | **Closed issues:** 280 | 281 | - Nil return values for normalize cause validations to pass [\#92](https://github.com/joost/phony_rails/issues/92) 282 | 283 | **Merged pull requests:** 284 | 285 | - pass all options from String\#phony\_normalized to PhonyRails.normalize\_number [\#94](https://github.com/joost/phony_rails/pull/94) ([krukgit](https://github.com/krukgit)) 286 | 287 | ## [v0.12.5](https://github.com/joost/phony_rails/tree/v0.12.5) (2015-04-30) 288 | 289 | [Full Changelog](https://github.com/joost/phony_rails/compare/v0.12.4...v0.12.5) 290 | 291 | **Closed issues:** 292 | 293 | - phony\_normalize strips parentheses from NDC part [\#89](https://github.com/joost/phony_rails/issues/89) 294 | - Does not normalize when validations are skipped [\#88](https://github.com/joost/phony_rails/issues/88) 295 | 296 | ## [v0.12.4](https://github.com/joost/phony_rails/tree/v0.12.4) (2015-04-05) 297 | 298 | [Full Changelog](https://github.com/joost/phony_rails/compare/v0.12.2...v0.12.4) 299 | 300 | ## [v0.12.2](https://github.com/joost/phony_rails/tree/v0.12.2) (2015-04-05) 301 | 302 | [Full Changelog](https://github.com/joost/phony_rails/compare/v0.12.1...v0.12.2) 303 | 304 | **Closed issues:** 305 | 306 | - Some numbers not normalizing properly as of 0.12.1 [\#87](https://github.com/joost/phony_rails/issues/87) 307 | - Something wrong with normalization of NO phones [\#85](https://github.com/joost/phony_rails/issues/85) 308 | 309 | ## [v0.12.1](https://github.com/joost/phony_rails/tree/v0.12.1) (2015-04-01) 310 | 311 | [Full Changelog](https://github.com/joost/phony_rails/compare/v0.12.0...v0.12.1) 312 | 313 | **Closed issues:** 314 | 315 | - Validate a phone number format, but don't require the presence [\#84](https://github.com/joost/phony_rails/issues/84) 316 | - Simple question about creating a record [\#83](https://github.com/joost/phony_rails/issues/83) 317 | 318 | ## [v0.12.0](https://github.com/joost/phony_rails/tree/v0.12.0) (2015-03-26) 319 | 320 | [Full Changelog](https://github.com/joost/phony_rails/compare/v0.11.0...v0.12.0) 321 | 322 | **Closed issues:** 323 | 324 | - Allow validating against multiple countries [\#81](https://github.com/joost/phony_rails/issues/81) 325 | 326 | **Merged pull requests:** 327 | 328 | - allow all valid options [\#82](https://github.com/joost/phony_rails/pull/82) ([zzma](https://github.com/zzma)) 329 | 330 | ## [v0.11.0](https://github.com/joost/phony_rails/tree/v0.11.0) (2015-03-04) 331 | 332 | [Full Changelog](https://github.com/joost/phony_rails/compare/v0.10.1...v0.11.0) 333 | 334 | **Closed issues:** 335 | 336 | - Method phony_formatted return "undefined method `split' for 1:Fixnum" [\#79](https://github.com/joost/phony_rails/issues/79) 337 | 338 | **Merged pull requests:** 339 | 340 | - Fix incorrect Japanese translation [\#80](https://github.com/joost/phony_rails/pull/80) ([ykzts](https://github.com/ykzts)) 341 | 342 | ## [v0.10.1](https://github.com/joost/phony_rails/tree/v0.10.1) (2015-01-21) 343 | 344 | [Full Changelog](https://github.com/joost/phony_rails/compare/v0.10.0...v0.10.1) 345 | 346 | **Closed issues:** 347 | 348 | - PhonyRails.normalize\_number is removing unexpected zero [\#77](https://github.com/joost/phony_rails/issues/77) 349 | - support for arrays in postgres [\#59](https://github.com/joost/phony_rails/issues/59) 350 | - Phone extension support [\#57](https://github.com/joost/phony_rails/issues/57) 351 | 352 | **Merged pull requests:** 353 | 354 | - Fixes \#55 - Validation fails if record country code does not match code ... [\#56](https://github.com/joost/phony_rails/pull/56) ([juanpaco](https://github.com/juanpaco)) 355 | 356 | ## [v0.10.0](https://github.com/joost/phony_rails/tree/v0.10.0) (2015-01-21) 357 | 358 | [Full Changelog](https://github.com/joost/phony_rails/compare/v0.9.0...v0.10.0) 359 | 360 | **Closed issues:** 361 | 362 | - Already normalized numbers have default country code prepended [\#76](https://github.com/joost/phony_rails/issues/76) 363 | 364 | ## [v0.9.0](https://github.com/joost/phony_rails/tree/v0.9.0) (2015-01-13) 365 | 366 | [Full Changelog](https://github.com/joost/phony_rails/compare/v0.8.2...v0.9.0) 367 | 368 | **Merged pull requests:** 369 | 370 | - change kh to km [\#75](https://github.com/joost/phony_rails/pull/75) ([Samda](https://github.com/Samda)) 371 | - update phony [\#74](https://github.com/joost/phony_rails/pull/74) ([Samda](https://github.com/Samda)) 372 | - add Khmer language translation [\#73](https://github.com/joost/phony_rails/pull/73) ([Samda](https://github.com/Samda)) 373 | - Add PhonyRails.plausible\_number? [\#72](https://github.com/joost/phony_rails/pull/72) ([marcantonio](https://github.com/marcantonio)) 374 | 375 | ## [v0.8.2](https://github.com/joost/phony_rails/tree/v0.8.2) (2014-12-18) 376 | 377 | [Full Changelog](https://github.com/joost/phony_rails/compare/v0.8.0...v0.8.2) 378 | 379 | **Closed issues:** 380 | 381 | - uninitialized constant Listen::Turnstile [\#69](https://github.com/joost/phony_rails/issues/69) 382 | - Issue with brazilian numbers [\#68](https://github.com/joost/phony_rails/issues/68) 383 | - Phony is now at 2.8.x [\#67](https://github.com/joost/phony_rails/issues/67) 384 | - Update to latest phony version? [\#65](https://github.com/joost/phony_rails/issues/65) 385 | 386 | **Merged pull requests:** 387 | 388 | - Remove depreciation warnings while running tests. [\#71](https://github.com/joost/phony_rails/pull/71) ([jmera](https://github.com/jmera)) 389 | - Update guard to handle change in listen dependency [\#70](https://github.com/joost/phony_rails/pull/70) ([JonMidhir](https://github.com/JonMidhir)) 390 | 391 | ## [v0.8.0](https://github.com/joost/phony_rails/tree/v0.8.0) (2014-11-07) 392 | 393 | [Full Changelog](https://github.com/joost/phony_rails/compare/v0.7.3...v0.8.0) 394 | 395 | **Closed issues:** 396 | 397 | - Update README [\#66](https://github.com/joost/phony_rails/issues/66) 398 | 399 | ## [v0.7.3](https://github.com/joost/phony_rails/tree/v0.7.3) (2014-10-23) 400 | 401 | [Full Changelog](https://github.com/joost/phony_rails/compare/v0.7.2...v0.7.3) 402 | 403 | **Merged pull requests:** 404 | 405 | - Ability to validate against the normalized input [\#64](https://github.com/joost/phony_rails/pull/64) ([dimroc](https://github.com/dimroc)) 406 | 407 | ## [v0.7.2](https://github.com/joost/phony_rails/tree/v0.7.2) (2014-10-15) 408 | 409 | [Full Changelog](https://github.com/joost/phony_rails/compare/v0.7.1...v0.7.2) 410 | 411 | **Merged pull requests:** 412 | 413 | - Add italian translations [\#63](https://github.com/joost/phony_rails/pull/63) ([philipgiuliani](https://github.com/philipgiuliani)) 414 | 415 | ## [v0.7.1](https://github.com/joost/phony_rails/tree/v0.7.1) (2014-10-01) 416 | 417 | [Full Changelog](https://github.com/joost/phony_rails/compare/v0.7.0...v0.7.1) 418 | 419 | ## [v0.7.0](https://github.com/joost/phony_rails/tree/v0.7.0) (2014-09-30) 420 | 421 | [Full Changelog](https://github.com/joost/phony_rails/compare/v0.6.0...v0.7.0) 422 | 423 | **Closed issues:** 424 | 425 | - TAG on release [\#62](https://github.com/joost/phony_rails/issues/62) 426 | - Unable to run migrations if "as" attribute added [\#60](https://github.com/joost/phony_rails/issues/60) 427 | - Rails not recognizing phony\_rails method [\#58](https://github.com/joost/phony_rails/issues/58) 428 | - Validation fails if record country code does not match code in phone number [\#55](https://github.com/joost/phony_rails/issues/55) 429 | - Phony 2.2.3 breaks test [\#51](https://github.com/joost/phony_rails/issues/51) 430 | - Country code not set when first two digits eq country code [\#50](https://github.com/joost/phony_rails/issues/50) 431 | - Phony 2.1 incompatibility related to country codes/numbers [\#48](https://github.com/joost/phony_rails/issues/48) 432 | - Clarify Indended Functionality and Require a Default Country Code [\#43](https://github.com/joost/phony_rails/issues/43) 433 | - Use Phony 2.0 [\#28](https://github.com/joost/phony_rails/issues/28) 434 | 435 | **Merged pull requests:** 436 | 437 | - Raise runtime errors not argument errors when :as attr undefined [\#61](https://github.com/joost/phony_rails/pull/61) ([chelsea](https://github.com/chelsea)) 438 | - Add turkish locale file. [\#54](https://github.com/joost/phony_rails/pull/54) ([onurozgurozkan](https://github.com/onurozgurozkan)) 439 | - Translate german [\#53](https://github.com/joost/phony_rails/pull/53) ([toxix](https://github.com/toxix)) 440 | - Fix country code being incorrectly passed to phony [\#49](https://github.com/joost/phony_rails/pull/49) ([pjg](https://github.com/pjg)) 441 | 442 | ## [v0.6.0](https://github.com/joost/phony_rails/tree/v0.6.0) (2014-01-28) 443 | 444 | [Full Changelog](https://github.com/joost/phony_rails/compare/2065287d48f58f3940e8a618b7dd9473b52486f0...v0.6.0) 445 | 446 | **Closed issues:** 447 | 448 | - French normalized number isn't good [\#42](https://github.com/joost/phony_rails/issues/42) 449 | - Invalid numbers should not be formatted [\#41](https://github.com/joost/phony_rails/issues/41) 450 | - Error when formatting invalid numbers [\#40](https://github.com/joost/phony_rails/issues/40) 451 | - License missing from gemspec [\#38](https://github.com/joost/phony_rails/issues/38) 452 | - Expose Country objects, and allow national-to-international conversion [\#34](https://github.com/joost/phony_rails/issues/34) 453 | - default\_country\_code forces country code [\#33](https://github.com/joost/phony_rails/issues/33) 454 | - "translation missing" when using validator on non-activerecord backed models [\#30](https://github.com/joost/phony_rails/issues/30) 455 | - Error when normalizing long telephone numbers with default country code [\#29](https://github.com/joost/phony_rails/issues/29) 456 | - Fix default\_country\_number appending repeatedly [\#25](https://github.com/joost/phony_rails/issues/25) 457 | - Detect if phone number has country code specified and use that [\#22](https://github.com/joost/phony_rails/issues/22) 458 | - problem with v 0.2.1 [\#21](https://github.com/joost/phony_rails/issues/21) 459 | - Error with phony\_normalize on migration [\#19](https://github.com/joost/phony_rails/issues/19) 460 | - Mongoid Error Message [\#18](https://github.com/joost/phony_rails/issues/18) 461 | - Make dependency on newer version of phony [\#11](https://github.com/joost/phony_rails/issues/11) 462 | - add a wiki [\#7](https://github.com/joost/phony_rails/issues/7) 463 | - validator not included [\#4](https://github.com/joost/phony_rails/issues/4) 464 | - Country Number out of Country gem [\#3](https://github.com/joost/phony_rails/issues/3) 465 | 466 | **Merged pull requests:** 467 | 468 | - Add support for phony version ~\> 2.1 [\#45](https://github.com/joost/phony_rails/pull/45) ([pjg](https://github.com/pjg)) 469 | - In the validator: add country code & number handling [\#44](https://github.com/joost/phony_rails/pull/44) ([robink](https://github.com/robink)) 470 | - PhonyRails.country\_number\_for should accept case agnostic country code [\#39](https://github.com/joost/phony_rails/pull/39) ([ahegyi](https://github.com/ahegyi)) 471 | - option for country code validation in helper [\#37](https://github.com/joost/phony_rails/pull/37) ([JeffLtz](https://github.com/JeffLtz)) 472 | - Fix phone number formatting method call in README [\#36](https://github.com/joost/phony_rails/pull/36) ([pjg](https://github.com/pjg)) 473 | - Better attribute accessor pattern + Japanese translation [\#35](https://github.com/joost/phony_rails/pull/35) ([johnnyshields](https://github.com/johnnyshields)) 474 | - Cleanup for better Mongoid support [\#32](https://github.com/joost/phony_rails/pull/32) ([johnnyshields](https://github.com/johnnyshields)) 475 | - add activemodel validation translation [\#31](https://github.com/joost/phony_rails/pull/31) ([ghiculescu](https://github.com/ghiculescu)) 476 | - use default\_country\_code when normalizing [\#27](https://github.com/joost/phony_rails/pull/27) ([espen](https://github.com/espen)) 477 | - update Gemfile.lock with lastest version [\#26](https://github.com/joost/phony_rails/pull/26) ([espen](https://github.com/espen)) 478 | - Raise only an exception at validation for non-existing attributes \(\#19\) [\#20](https://github.com/joost/phony_rails/pull/20) ([k4nar](https://github.com/k4nar)) 479 | - Do not normalize an implausible phone [\#16](https://github.com/joost/phony_rails/pull/16) ([Jell](https://github.com/Jell)) 480 | - Override the default loading of the "countries" gem so that the Country class isn't unqualified. [\#15](https://github.com/joost/phony_rails/pull/15) ([jcoleman](https://github.com/jcoleman)) 481 | - Mongoid support. [\#14](https://github.com/joost/phony_rails/pull/14) ([siong1987](https://github.com/siong1987)) 482 | - Do not pollute the global namespace with a Country class [\#13](https://github.com/joost/phony_rails/pull/13) ([Jell](https://github.com/Jell)) 483 | - Address issue \#11 - need to use a newer version of phony for additional countries [\#12](https://github.com/joost/phony_rails/pull/12) ([rjhaveri](https://github.com/rjhaveri)) 484 | - Compatibility with Ruby 1.8.7 [\#10](https://github.com/joost/phony_rails/pull/10) ([triskweline](https://github.com/triskweline)) 485 | - remove cause for warning: already initialized constant VERSION [\#9](https://github.com/joost/phony_rails/pull/9) ([triskweline](https://github.com/triskweline)) 486 | - validator translation [\#8](https://github.com/joost/phony_rails/pull/8) ([ddidier](https://github.com/ddidier)) 487 | - refactored tests and added options to validates\_plausible\_phone [\#6](https://github.com/joost/phony_rails/pull/6) ([ddidier](https://github.com/ddidier)) 488 | - some tests and a helper method [\#5](https://github.com/joost/phony_rails/pull/5) ([ddidier](https://github.com/ddidier)) 489 | - Bumped Phony dependency to the latest [\#2](https://github.com/joost/phony_rails/pull/2) ([Rodeoclash](https://github.com/Rodeoclash)) 490 | - Changed the remaining references to phony\_number to phony\_rails. [\#1](https://github.com/joost/phony_rails/pull/1) ([floere](https://github.com/floere)) 491 | 492 | 493 | 494 | \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* 495 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | gemspec # Specify your gem's dependencies in phony_number.gemspec 6 | 7 | gem 'coveralls', require: false 8 | gem 'guard' # , '~> 2.10.1' 9 | gem 'guard-bundler' # , '~> 2.0.0' 10 | gem 'guard-rspec' # , '~> 4.2.0' 11 | gem 'rake' 12 | gem 'rspec' # , '~> 2.14.0' 13 | gem 'rubocop' 14 | gem 'rubocop-performance' 15 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # More info at https://github.com/guard/guard#readme 4 | 5 | guard 'bundler' do 6 | watch('Gemfile') 7 | watch(/^.+\.gemspec/) 8 | end 9 | 10 | guard 'rspec', cmd: 'bundle exec rspec' do 11 | watch(%r{^spec/.+_spec\.rb$}) 12 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" } 13 | watch('spec/spec_helper.rb') { 'spec' } 14 | end 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Joost Hietbrink 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PhonyRails [![Build Status](https://travis-ci.org/joost/phony_rails.svg?branch=master)](https://travis-ci.org/joost/phony_rails) [![Coverage Status](https://coveralls.io/repos/joost/phony_rails/badge.svg)](https://coveralls.io/r/joost/phony_rails) ![Dependencies Status](https://img.shields.io/gem/v/phony_rails.svg) 2 | 3 | This small Gem adds useful methods to your Rails app to validate, display and save phone numbers. 4 | It uses the super awesome Phony gem (https://github.com/floere/phony). 5 | 6 | Find version information in the [CHANGELOG](CHANGELOG.md). 7 | 8 | ## Installation 9 | 10 | Add this line to your application's Gemfile (requires Ruby > 2.3): 11 | 12 | ```ruby 13 | gem 'phony_rails' 14 | ``` 15 | 16 | And then execute: 17 | 18 | ``` 19 | $ bundle 20 | ``` 21 | 22 | Or install it yourself as: 23 | 24 | ``` 25 | $ gem install phony_rails 26 | ``` 27 | 28 | ## Usage 29 | 30 | ### Normalization / Model Usage 31 | 32 | #### ActiveRecord 33 | 34 | For **ActiveRecord**, in your model add: 35 | 36 | ```ruby 37 | class SomeModel < ActiveRecord::Base 38 | # Normalizes the attribute itself before validation 39 | phony_normalize :phone_number, default_country_code: 'US' 40 | 41 | # Normalizes attribute before validation and saves into other attribute 42 | phony_normalize :phone_number, as: :phone_number_normalized_version, default_country_code: 'US' 43 | 44 | # Creates method normalized_fax_number that returns the normalized version of fax_number 45 | phony_normalized_method :fax_number 46 | 47 | # Conditionally normalizes the attribute 48 | phony_normalize :recipient, default_country_code: 'US', if: -> { contact_method == 'phone_number' } 49 | end 50 | ``` 51 | 52 | #### ActiveModel (models without database) 53 | 54 | For Rails-like models without a database, add: 55 | 56 | ```ruby 57 | class SomeModel 58 | include ActiveModel::Model # we get AR-like attributes and validations 59 | include ActiveModel::Validations::Callbacks # a dependency for normalization 60 | 61 | # your attributes must be defined, they are not inherited from a DB table 62 | attr_accessor :phone_number, :phone_number_as_normalized 63 | 64 | # Once the model is set up, we have the same things as with ActiveRecord 65 | phony_normalize :phone_number, default_country_code: 'US' 66 | end 67 | ``` 68 | 69 | #### Mongoid (DEPRECATED) 70 | 71 | WARNING: From v0.15.0 Mongoid support has been removed! 72 | 73 | #### General info 74 | 75 | The `:default_country_code` options is used to specify a country_code when normalizing. 76 | 77 | PhonyRails will also check your model for a country_code method to use when normalizing the number. So `'070-12341234'` with `country_code` 'NL' will get normalized to `'+317012341234'`. 78 | 79 | You can also do-it-yourself and call: 80 | 81 | ```ruby 82 | # Options: 83 | # :country_code => The country code we should use (forced). 84 | # :default_country_code => Some fallback code (eg. 'NL') that can be used as default (comes from phony_normalize_numbers method). 85 | 86 | PhonyRails.normalize_number('some number', country_code: 'NL') 87 | 88 | PhonyRails.normalize_number('+4790909090', country_code: 'SE') # => '+464790909090' (forced to +46) 89 | PhonyRails.normalize_number('+4790909090', default_country_code: 'SE') # => '+4790909090' (still +47 so not changed) 90 | ``` 91 | 92 | The country_code should always be a ISO 3166-1 alpha-2 (http://en.wikipedia.org/wiki/ISO_3166-1_alpha-2). 93 | 94 | #### Default for all models 95 | 96 | You can set the default_country_code for all models using: 97 | 98 | ```ruby 99 | PhonyRails.default_country_code = "US" 100 | ``` 101 | 102 | ### Validation 103 | 104 | In your model use the Phony.plausible method to validate an attribute: 105 | 106 | ```ruby 107 | validates :phone_number, phony_plausible: true 108 | ``` 109 | 110 | or the helper method: 111 | 112 | ```ruby 113 | validates_plausible_phone :phone_number 114 | ``` 115 | 116 | this method use other validators under the hood to provide: 117 | 118 | - presence validation using `ActiveModel::Validations::PresenceValidator` 119 | - format validation using `ActiveModel::Validations::FormatValidator` 120 | 121 | so we can use: 122 | 123 | ```ruby 124 | validates_plausible_phone :phone_number, presence: true 125 | validates_plausible_phone :phone_number, with: /\A\+\d+/ 126 | validates_plausible_phone :phone_number, without: /\A\+\d+/ 127 | validates_plausible_phone :phone_number, presence: true, with: /\A\+\d+/ 128 | ``` 129 | 130 | the i18n key is `:improbable_phone`. Languages supported by default: de, en, es, fr, it, ja, kh, ko, nl, pt, tr, ua and ru. 131 | 132 | You can also validate if a number has the correct country number: 133 | 134 | ```ruby 135 | validates_plausible_phone :phone_number, country_number: '61' 136 | ``` 137 | 138 | or correct country code: 139 | 140 | ```ruby 141 | validates_plausible_phone :phone_number, country_code: 'AU' 142 | ``` 143 | 144 | You can validate against the normalized input as opposed to the raw input: 145 | 146 | ```ruby 147 | phony_normalize :phone_number, as: :phone_number_normalized, default_country_code: 'US' 148 | validates_plausible_phone :phone_number_normalized, presence: true, if: :phone_number? 149 | ``` 150 | 151 | Validation supports phone numbers with extension, such as `+18181231234 x1234` or `'+1 (818)151-5483 #4312'` out-of-the-box. 152 | 153 | Return original value after validation: 154 | 155 | The flag normalize_when_valid (disabled by default), allows to return the original phone_number when is the object is not valid. When phone validation fails, normalization is not triggered at all. It could prevent a situation where user fills in the phone number and after validation, he gets back different, already normalized phone number value, even if phone number was wrong. 156 | 157 | Example usage: 158 | 159 | ```ruby 160 | validates_plausible_phone :phone_number 161 | phony_normalize :phone_number, country_code: :country_code, normalize_when_valid: true 162 | ``` 163 | 164 | Filling in the number will result with following: 165 | 166 | When the number is incorrect (e.g. phone_number: `+44 888 888 888` for country_code 'PL'), the original validation behavior is preserved, but if the number is still invalid, the original value is returned. 167 | When number is valid, it will save the normalized number (e.g. `+48 888 888 888` will be saved as `+48888888888`). 168 | 169 | #### Allowing records country codes to not match phone number country codes 170 | 171 | You may have a record specifying one country (via a `country_code` attribute) but using a phone number from another country. For example, your record may be from Japan but have a phone number from the Philippines. By default, `phony_rails` will consider your record's `country_code` as part of the validation. If that country doesn't match the country code in the phone number, validation will fail. 172 | 173 | Additionally, `phony_normalize` will always add the records country code as the country number (eg. the user enters '+81xxx' for Japan and the records `country_code` is 'DE' then `phony_normalize` will change the number to '+4981'). You can turn this off by adding `enforce_record_country: false` to the validation options. The country_code will then only be added if no country code is specified. 174 | 175 | If you want to allow records from one country to have phone numbers from a different one, there are a couple of options you can use: `ignore_record_country_number` and `ignore_record_country_code`. Use them like so: 176 | 177 | ```ruby 178 | validates :phone_number, phony_plausible: { ignore_record_country_code: true, ignore_record_country_number: true } 179 | ``` 180 | 181 | Obviously, you don't have to use both, and you may not need or want to set either. 182 | 183 | ### Display / Views 184 | 185 | In your views use: 186 | 187 | ```erb 188 | <%= "311012341234".phony_formatted(format: :international, spaces: '-') %> 189 | <%= "+31-10-12341234".phony_formatted(format: :international, spaces: '-') %> 190 | <%= "+31(0)1012341234".phony_formatted(format: :international, spaces: '-') %> 191 | ``` 192 | 193 | To first normalize the String to a certain country use: 194 | 195 | ```erb 196 | <%= "010-12341234".phony_formatted(normalize: :NL, format: :international, spaces: '-') %> 197 | ``` 198 | 199 | To return nil when a number is not valid: 200 | 201 | ```ruby 202 | "123".phony_formatted(strict: true) # => nil 203 | ``` 204 | 205 | You can also use the bang method (phony_formatted!): 206 | 207 | ```ruby 208 | number = "010-12341234" 209 | number.phony_formatted!(normalize: :NL, format: :international) 210 | number # => "+31 10 123 41234" 211 | ``` 212 | 213 | You can also easily normalize a phone number String: 214 | 215 | ```ruby 216 | "+31 (0)30 1234 123".phony_normalized # => '31301234123' 217 | "(0)30 1234 123".phony_normalized # => '301234123' 218 | "(0)30 1234 123".phony_normalized(country_code: 'NL') # => '301234123' 219 | ``` 220 | 221 | Extensions are supported (identified by "ext", "ex", "x", "xt", "#", or ":") and will show at the end of the number: 222 | 223 | ```ruby 224 | "+31 (0)30 1234 123 x999".phony_normalized # => '31301234123 x999' 225 | "+31 (0)30 1234 123 ext999".phony_normalized # => '31301234123 x999' 226 | "+31 (0)30 1234 123 #999".phony_normalized # => '31301234123 x999' 227 | ``` 228 | 229 | ### Find by normalized number 230 | 231 | Say you want to find a record by a phone number. Best is to normalize user input and compare to an attribute stored in the db. 232 | 233 | ```ruby 234 | Home.find_by_normalized_phone_number(PhonyRails.normalize_number(params[:phone_number])) 235 | ``` 236 | 237 | ## Contributing 238 | 239 | 1. Fork it 240 | 2. Create your feature branch (`git checkout -b my-new-feature`) 241 | 3. Commit your changes (`git commit -am 'Added some feature'`) 242 | 4. Push to the branch (`git push origin my-new-feature`) 243 | 5. Create new Pull Request 244 | 245 | Don't forget to add tests and run rspec before creating a pull request :) 246 | 247 | See all contributors on https://github.com/joost/phony_rails/graphs/contributors. 248 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | # frozen_string_literal: true 3 | 4 | require 'bundler/gem_tasks' 5 | require 'rspec/core/rake_task' 6 | 7 | RSpec::Core::RakeTask.new(:spec) 8 | task default: :spec 9 | 10 | task :console do 11 | require 'irb' 12 | require 'irb/completion' 13 | require 'phony_rails' 14 | ARGV.clear 15 | IRB.start 16 | end 17 | -------------------------------------------------------------------------------- /lib/data/country_codes.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | AD: 3 | country_code: '376' 4 | AE: 5 | country_code: '971' 6 | AF: 7 | country_code: '93' 8 | AG: 9 | country_code: '1' 10 | AI: 11 | country_code: '1' 12 | AL: 13 | country_code: '355' 14 | AM: 15 | country_code: '374' 16 | AN: 17 | country_code: '599' 18 | AO: 19 | country_code: '244' 20 | AQ: 21 | country_code: '672' 22 | AR: 23 | country_code: '54' 24 | AS: 25 | country_code: '1' 26 | AT: 27 | country_code: '43' 28 | AU: 29 | country_code: '61' 30 | AW: 31 | country_code: '297' 32 | AX: 33 | country_code: '358' 34 | AZ: 35 | country_code: '994' 36 | BA: 37 | country_code: '387' 38 | BB: 39 | country_code: '1' 40 | BD: 41 | country_code: '880' 42 | BE: 43 | country_code: '32' 44 | BF: 45 | country_code: '226' 46 | BG: 47 | country_code: '359' 48 | BH: 49 | country_code: '973' 50 | BI: 51 | country_code: '257' 52 | BJ: 53 | country_code: '229' 54 | BL: 55 | country_code: '590' 56 | BM: 57 | country_code: '1' 58 | BN: 59 | country_code: '673' 60 | BO: 61 | country_code: '591' 62 | BQ: 63 | country_code: '599' 64 | BR: 65 | country_code: '55' 66 | BS: 67 | country_code: '1' 68 | BT: 69 | country_code: '975' 70 | BV: 71 | country_code: '' 72 | BW: 73 | country_code: '267' 74 | BY: 75 | country_code: '375' 76 | BZ: 77 | country_code: '501' 78 | CA: 79 | country_code: '1' 80 | CC: 81 | country_code: '61' 82 | CD: 83 | country_code: '243' 84 | CF: 85 | country_code: '236' 86 | CG: 87 | country_code: '242' 88 | CH: 89 | country_code: '41' 90 | CI: 91 | country_code: '225' 92 | CK: 93 | country_code: '682' 94 | CL: 95 | country_code: '56' 96 | CM: 97 | country_code: '237' 98 | CN: 99 | country_code: '86' 100 | CO: 101 | country_code: '57' 102 | CR: 103 | country_code: '506' 104 | CU: 105 | country_code: '53' 106 | CV: 107 | country_code: '238' 108 | CW: 109 | country_code: '599' 110 | CX: 111 | country_code: '61' 112 | CY: 113 | country_code: '357' 114 | CZ: 115 | country_code: '420' 116 | DE: 117 | country_code: '49' 118 | DJ: 119 | country_code: '253' 120 | DK: 121 | country_code: '45' 122 | DM: 123 | country_code: '1' 124 | DO: 125 | country_code: '1' 126 | DZ: 127 | country_code: '213' 128 | EC: 129 | country_code: '593' 130 | EE: 131 | country_code: '372' 132 | EG: 133 | country_code: '20' 134 | EH: 135 | country_code: '212' 136 | ER: 137 | country_code: '291' 138 | ES: 139 | country_code: '34' 140 | ET: 141 | country_code: '251' 142 | FI: 143 | country_code: '358' 144 | FJ: 145 | country_code: '679' 146 | FK: 147 | country_code: '500' 148 | FM: 149 | country_code: '691' 150 | FO: 151 | country_code: '298' 152 | FR: 153 | country_code: '33' 154 | GA: 155 | country_code: '241' 156 | GB: 157 | country_code: '44' 158 | GD: 159 | country_code: '1' 160 | GE: 161 | country_code: '995' 162 | GF: 163 | country_code: '594' 164 | GG: 165 | country_code: '44' 166 | GH: 167 | country_code: '233' 168 | GI: 169 | country_code: '350' 170 | GL: 171 | country_code: '299' 172 | GM: 173 | country_code: '220' 174 | GN: 175 | country_code: '224' 176 | GP: 177 | country_code: '590' 178 | GQ: 179 | country_code: '240' 180 | GR: 181 | country_code: '30' 182 | GS: 183 | country_code: '500' 184 | GT: 185 | country_code: '502' 186 | GU: 187 | country_code: '1' 188 | GW: 189 | country_code: '245' 190 | GY: 191 | country_code: '592' 192 | HK: 193 | country_code: '852' 194 | HM: 195 | country_code: '' 196 | HN: 197 | country_code: '504' 198 | HR: 199 | country_code: '385' 200 | HT: 201 | country_code: '509' 202 | HU: 203 | country_code: '36' 204 | ID: 205 | country_code: '62' 206 | IE: 207 | country_code: '353' 208 | IL: 209 | country_code: '972' 210 | IM: 211 | country_code: '44' 212 | IN: 213 | country_code: '91' 214 | IO: 215 | country_code: '246' 216 | IQ: 217 | country_code: '964' 218 | IR: 219 | country_code: '98' 220 | IS: 221 | country_code: '354' 222 | IT: 223 | country_code: '39' 224 | JE: 225 | country_code: '44' 226 | JM: 227 | country_code: '1' 228 | JO: 229 | country_code: '962' 230 | JP: 231 | country_code: '81' 232 | KE: 233 | country_code: '254' 234 | KG: 235 | country_code: '996' 236 | KH: 237 | country_code: '855' 238 | KI: 239 | country_code: '686' 240 | KM: 241 | country_code: '269' 242 | KN: 243 | country_code: '1' 244 | KP: 245 | country_code: '850' 246 | KR: 247 | country_code: '82' 248 | KW: 249 | country_code: '965' 250 | KY: 251 | country_code: '1' 252 | KZ: 253 | country_code: '7' 254 | LA: 255 | country_code: '856' 256 | LB: 257 | country_code: '961' 258 | LC: 259 | country_code: '1' 260 | LI: 261 | country_code: '423' 262 | LK: 263 | country_code: '94' 264 | LR: 265 | country_code: '231' 266 | LS: 267 | country_code: '266' 268 | LT: 269 | country_code: '370' 270 | LU: 271 | country_code: '352' 272 | LV: 273 | country_code: '371' 274 | LY: 275 | country_code: '218' 276 | MA: 277 | country_code: '212' 278 | MC: 279 | country_code: '377' 280 | MD: 281 | country_code: '373' 282 | ME: 283 | country_code: '382' 284 | MF: 285 | country_code: '590' 286 | MG: 287 | country_code: '261' 288 | MH: 289 | country_code: '692' 290 | MK: 291 | country_code: '389' 292 | ML: 293 | country_code: '223' 294 | MM: 295 | country_code: '95' 296 | MN: 297 | country_code: '976' 298 | MO: 299 | country_code: '853' 300 | MP: 301 | country_code: '1' 302 | MQ: 303 | country_code: '596' 304 | MR: 305 | country_code: '222' 306 | MS: 307 | country_code: '1' 308 | MT: 309 | country_code: '356' 310 | MU: 311 | country_code: '230' 312 | MV: 313 | country_code: '960' 314 | MW: 315 | country_code: '265' 316 | MX: 317 | country_code: '52' 318 | MY: 319 | country_code: '60' 320 | MZ: 321 | country_code: '258' 322 | NA: 323 | country_code: '264' 324 | NC: 325 | country_code: '687' 326 | NE: 327 | country_code: '227' 328 | NF: 329 | country_code: '672' 330 | NG: 331 | country_code: '234' 332 | NI: 333 | country_code: '505' 334 | NL: 335 | country_code: '31' 336 | 'NO': 337 | country_code: '47' 338 | NP: 339 | country_code: '977' 340 | NR: 341 | country_code: '674' 342 | NU: 343 | country_code: '683' 344 | NZ: 345 | country_code: '64' 346 | OM: 347 | country_code: '968' 348 | PA: 349 | country_code: '507' 350 | PE: 351 | country_code: '51' 352 | PF: 353 | country_code: '689' 354 | PG: 355 | country_code: '675' 356 | PH: 357 | country_code: '63' 358 | PK: 359 | country_code: '92' 360 | PL: 361 | country_code: '48' 362 | PM: 363 | country_code: '508' 364 | PN: 365 | country_code: '' 366 | PR: 367 | country_code: '1' 368 | PS: 369 | country_code: '970' 370 | PT: 371 | country_code: '351' 372 | PW: 373 | country_code: '680' 374 | PY: 375 | country_code: '595' 376 | QA: 377 | country_code: '974' 378 | RE: 379 | country_code: '262' 380 | RO: 381 | country_code: '40' 382 | RS: 383 | country_code: '381' 384 | RU: 385 | country_code: '7' 386 | RW: 387 | country_code: '250' 388 | SA: 389 | country_code: '966' 390 | SB: 391 | country_code: '677' 392 | SC: 393 | country_code: '248' 394 | SD: 395 | country_code: '249' 396 | SE: 397 | country_code: '46' 398 | SG: 399 | country_code: '65' 400 | SH: 401 | country_code: '290' 402 | SI: 403 | country_code: '386' 404 | SJ: 405 | country_code: '47' 406 | SK: 407 | country_code: '421' 408 | SL: 409 | country_code: '232' 410 | SM: 411 | country_code: '378' 412 | SN: 413 | country_code: '221' 414 | SO: 415 | country_code: '252' 416 | SR: 417 | country_code: '597' 418 | SS: 419 | country_code: '211' 420 | ST: 421 | country_code: '239' 422 | SV: 423 | country_code: '503' 424 | SX: 425 | country_code: '1' 426 | SY: 427 | country_code: '963' 428 | SZ: 429 | country_code: '268' 430 | TC: 431 | country_code: '1' 432 | TD: 433 | country_code: '235' 434 | TF: 435 | country_code: '' 436 | TG: 437 | country_code: '228' 438 | TH: 439 | country_code: '66' 440 | TJ: 441 | country_code: '992' 442 | TK: 443 | country_code: '690' 444 | TL: 445 | country_code: '670' 446 | TM: 447 | country_code: '993' 448 | TN: 449 | country_code: '216' 450 | TO: 451 | country_code: '676' 452 | TR: 453 | country_code: '90' 454 | TT: 455 | country_code: '1' 456 | TV: 457 | country_code: '688' 458 | TW: 459 | country_code: '886' 460 | TZ: 461 | country_code: '255' 462 | UA: 463 | country_code: '380' 464 | UG: 465 | country_code: '256' 466 | UK: 467 | country_code: '44' 468 | UM: 469 | country_code: '' 470 | US: 471 | country_code: '1' 472 | UY: 473 | country_code: '598' 474 | UZ: 475 | country_code: '998' 476 | VA: 477 | country_code: '39' 478 | VC: 479 | country_code: '1' 480 | VE: 481 | country_code: '58' 482 | VG: 483 | country_code: '1' 484 | VI: 485 | country_code: '1' 486 | VN: 487 | country_code: '84' 488 | VU: 489 | country_code: '678' 490 | WF: 491 | country_code: '681' 492 | WS: 493 | country_code: '685' 494 | XK: 495 | country_code: '383' 496 | YE: 497 | country_code: '967' 498 | YT: 499 | country_code: '262' 500 | ZA: 501 | country_code: '27' 502 | ZM: 503 | country_code: '260' 504 | ZW: 505 | country_code: '263' 506 | -------------------------------------------------------------------------------- /lib/phony_rails.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'phony' 4 | require 'phony_rails/string_extensions' 5 | require 'validators/phony_validator' 6 | require 'phony_rails/version' 7 | require 'yaml' 8 | 9 | module PhonyRails 10 | def self.default_country_code 11 | @default_country_code ||= nil 12 | end 13 | 14 | def self.default_country_code=(new_code) 15 | @default_country_code = new_code 16 | @default_country_number = nil # Reset default country number, will lookup next time its asked for 17 | end 18 | 19 | def self.default_country_number 20 | @default_country_number ||= default_country_code.present? ? country_number_for(default_country_code) : nil 21 | end 22 | 23 | def self.default_country_number=(new_number) 24 | @default_country_number = new_number 25 | end 26 | 27 | def self.country_number_for(country_code) 28 | return if country_code.nil? 29 | 30 | country_codes_hash.fetch(country_code.to_s.upcase, {})['country_code'] 31 | end 32 | 33 | def self.country_codes_hash 34 | @country_codes_hash ||= YAML.load_file(File.join(File.dirname(File.expand_path(__FILE__)), 'data/country_codes.yaml')) 35 | end 36 | 37 | # This method requires a country_code attribute (eg. NL) and phone_number to be set. 38 | # Options: 39 | # :country_number => The country dial code (eg. 31 for NL). 40 | # :default_country_number => Fallback country code. 41 | # :country_code => The country code we should use. 42 | # :default_country_code => Some fallback code (eg. 'NL') that can be used as default (comes from phony_normalize_numbers method). 43 | # :add_plus => Add a '+' in front so we know the country code is added. (default: true) 44 | # :extension => Include the extension. (default: true) 45 | # This idea came from: 46 | # http://www.redguava.com.au/2011/06/rails-convert-phone-numbers-to-international-format-for-sms/ 47 | def self.normalize_number(number, options = {}, current_instance = nil) 48 | return if number.nil? 49 | 50 | original_number = number 51 | number = number.dup # Just to be sure, we don't want to change the original. 52 | number, ext = extract_extension(number) 53 | number.gsub!(/[^()\d+]/, '') # Strips weird stuff from the number 54 | return if number.blank? 55 | 56 | if _country_number = options[:country_number] || country_number_for(options[:country_code]) 57 | options[:add_plus] = true if options[:add_plus].nil? 58 | # (Force) add country_number if missing 59 | # NOTE: do we need to force adding country code? Otherwise we can share logic with next block 60 | number = "#{_country_number}#{number}" if !Phony.plausible?(number) || _country_number != country_code_from_number(number) 61 | elsif _default_country_number = extract_default_country_number(options, current_instance) 62 | options[:add_plus] = true if options[:add_plus].nil? 63 | number = normalize_number_default_country(number, _default_country_number) 64 | end 65 | normalized_number = Phony.normalize(number) 66 | options[:add_plus] = true if options[:add_plus].nil? && Phony.plausible?(normalized_number) 67 | normalized_number = options[:add_plus] ? "+#{normalized_number}" : normalized_number 68 | 69 | options[:extension] = true if options[:extension].nil? 70 | options[:extension] ? format_extension(normalized_number, ext) : normalized_number 71 | rescue StandardError 72 | original_number # If all goes wrong .. we still return the original input. 73 | end 74 | 75 | def self.normalize_number_default_country(number, default_country_number) 76 | # We try to add the default country number and see if it is a 77 | # correct phone number. See https://github.com/joost/phony_rails/issues/87#issuecomment-89324426 78 | unless number =~ /\A\(?(\+|00)/ # if we don't have a + or 00 79 | return "#{default_country_number}#{number}" if Phony.plausible?("#{default_country_number}#{number}") || !Phony.plausible?(number) || country_code_from_number(number).nil? 80 | # If the number starts with ONE zero (two might indicate a country code) 81 | # and this is a plausible number for the default_country 82 | # we prefer that one. 83 | return "#{default_country_number}#{number.gsub(/^0/, '')}" if (number =~ /^0[^0]/) && Phony.plausible?("#{default_country_number}#{number.gsub(/^0/, '')}") 84 | end 85 | # number = "#{default_country_number}#{number}" unless Phony.plausible?(number) 86 | # Just return the number unchanged 87 | number 88 | end 89 | 90 | def self.extract_default_country_number(options = {}, current_instance = nil) 91 | country_code = if current_instance.present? && options[:default_country_code].respond_to?(:call) 92 | options[:default_country_code].call(current_instance) rescue nil 93 | else 94 | options[:default_country_code] 95 | end 96 | options[:default_country_number] || country_number_for(country_code) || default_country_number 97 | end 98 | 99 | # Returns the country dail code (eg. '31') for a number (eg. +31612341234). 100 | # Should probably be named 'country_number_from_number'. 101 | def self.country_code_from_number(number) 102 | return nil unless Phony.plausible?(number) 103 | 104 | Phony.split(Phony.normalize(number)).first 105 | end 106 | 107 | # Returns the country (eg. 'NL') for a number (eg. +31612341234). 108 | def self.country_from_number(number) 109 | return nil unless Phony.plausible?(number) 110 | 111 | country_codes_hash.select { |_country, hash| hash['country_code'] == country_code_from_number(number) }.keys[0] 112 | end 113 | 114 | # Wrapper for Phony.plausible?. Takes the same options as #normalize_number. 115 | # NB: This method calls #normalize_number and passes _options_ directly to that method. 116 | # It uses the 'cc' option for Phony. This was a required param before? 117 | def self.plausible_number?(number, options = {}) 118 | return false if number.blank? 119 | 120 | number = extract_extension(number).first 121 | number = normalize_number(number, options) 122 | country_number = options[:country_number] || country_number_for(options[:country_code]) || 123 | country_code_from_number(number) || 124 | options[:default_country_number] || country_number_for(options[:default_country_code]) || 125 | default_country_number 126 | Phony.plausible? number, cc: country_number 127 | rescue StandardError 128 | false 129 | end 130 | 131 | COMMON_EXTENSIONS = / *(ext|ex|x|xt|#|:)+[^0-9]*\(?([-0-9]{1,})\)?#?$/i.freeze 132 | 133 | def self.extract_extension(number_and_ext) 134 | return [nil, nil] if number_and_ext.nil? 135 | 136 | subbed = number_and_ext.sub(COMMON_EXTENSIONS, '') 137 | [subbed, Regexp.last_match(2)] 138 | end 139 | 140 | def self.format_extension(number, ext) 141 | ext.present? ? "#{number} x#{ext}" : number 142 | end 143 | 144 | module Extension 145 | extend ActiveSupport::Concern 146 | 147 | included do 148 | private 149 | 150 | # This methods sets the attribute to the normalized version. 151 | # It also adds the country_code (number), eg. 31 for NL numbers. 152 | def set_phony_normalized_numbers(current_instance, attributes, options = {}) 153 | options = options.dup 154 | assign_values_for_phony_symbol_options(options) 155 | if respond_to?(:country_code) 156 | set_country_as = options[:enforce_record_country] ? :country_code : :default_country_code 157 | options[set_country_as] ||= country_code 158 | end 159 | attributes.each do |attribute| 160 | attribute_name = options[:as] || attribute 161 | raise("No attribute #{attribute_name} found on #{self.class.name} (PhonyRails)") unless self.class.attribute_method?(attribute_name) 162 | 163 | cache_original_attribute(current_instance, attribute) if options[:normalize_when_valid] 164 | new_value = PhonyRails.normalize_number(send(attribute), options, current_instance) 165 | current_instance.public_send("#{attribute_name}=", new_value) if new_value || attribute_name != attribute 166 | end 167 | end 168 | 169 | def assign_values_for_phony_symbol_options(options) 170 | symbol_options = %i[country_number default_country_number country_code default_country_code] 171 | symbol_options.each do |option| 172 | options[option] = send(options[option]) if options[option].is_a?(Symbol) 173 | end 174 | end 175 | end 176 | 177 | def cache_original_attribute(current_instance, attribute) 178 | attribute_name = "#{attribute}_original" 179 | current_instance.define_singleton_method("#{attribute_name}=") { |value| instance_variable_set("@#{attribute_name}", value) } 180 | current_instance.define_singleton_method(attribute_name) { instance_variable_get("@#{attribute_name}") } 181 | current_instance.public_send("#{attribute}_original=", current_instance.public_send(attribute.to_s)) 182 | end 183 | 184 | module ClassMethods 185 | PHONY_RAILS_COLLECTION_VALID_KEYS = %i[ 186 | add_plus 187 | as 188 | country_code 189 | country_number 190 | default_country_code 191 | default_country_number 192 | enforce_record_country 193 | if 194 | normalize_when_valid 195 | unless 196 | ].freeze 197 | # Use this method on the class level like: 198 | # phony_normalize :phone_number, :fax_number, :default_country_code => 'NL' 199 | # 200 | # It checks your model object for a a country_code attribute (eg. 'NL') to do the normalizing so make sure 201 | # you've geocoded before calling this method! 202 | def phony_normalize(*attributes) 203 | options = attributes.last.is_a?(Hash) ? attributes.pop : {} 204 | options.assert_valid_keys(*PHONY_RAILS_COLLECTION_VALID_KEYS) 205 | raise ArgumentError, ':as option can not be used on phony_normalize with multiple attribute names! (PhonyRails)' if options[:as].present? && (attributes.size > 1) 206 | 207 | options[:enforce_record_country] = true if options[:enforce_record_country].nil? 208 | 209 | conditional = create_before_validation_conditional_hash(options) 210 | 211 | # Add before validation that saves a normalized version of the phone number 212 | before_validation conditional do 213 | set_phony_normalized_numbers(self, attributes, options) 214 | end 215 | end 216 | 217 | # Usage: 218 | # phony_normalized_method :fax_number, :default_country_code => 'US' 219 | # Creates a normalized_fax_number method. 220 | def phony_normalized_method(*attributes) 221 | main_options = attributes.last.is_a?(Hash) ? attributes.pop : {} 222 | main_options.assert_valid_keys :country_code, :default_country_code 223 | attributes.each do |attribute| 224 | raise(StandardError, "Instance method normalized_#{attribute} already exists on #{name} (PhonyRails)") if method_defined?(:"normalized_#{attribute}") 225 | 226 | define_method :"normalized_#{attribute}" do |*args| 227 | options = main_options.merge(args.first || {}) 228 | assign_values_for_phony_symbol_options(options) 229 | raise(ArgumentError, "No attribute/method #{attribute} found on #{self.class.name} (PhonyRails)") unless respond_to?(attribute) 230 | 231 | options[:country_code] ||= country_code if respond_to?(:country_code) 232 | PhonyRails.normalize_number(send(attribute), options) 233 | end 234 | end 235 | end 236 | 237 | private 238 | 239 | # Creates a hash representing a conditional for before_validation 240 | # This allows conditional normalization 241 | # Returns something like `{ unless: -> { attribute == 'something' } }` 242 | # If no if/unless options passed in, returns `{ if: -> { true } }` 243 | def create_before_validation_conditional_hash(options) 244 | if options[:if].present? 245 | type = :if 246 | source = options[:if] 247 | elsif options[:unless].present? 248 | type = :unless 249 | source = options[:unless] 250 | else 251 | type = :if 252 | source = true 253 | end 254 | 255 | conditional = {} 256 | conditional[type] = if source.respond_to?(:call) 257 | source 258 | elsif source.respond_to?(:to_sym) 259 | -> { send(source.to_sym) } 260 | else 261 | -> { source } 262 | end 263 | conditional 264 | end 265 | end 266 | end 267 | end 268 | 269 | # check whether it is ActiveRecord or Mongoid being used 270 | if defined?(ActiveRecord) 271 | ActiveSupport.on_load(:active_record) do 272 | ActiveRecord::Base.send :include, PhonyRails::Extension 273 | end 274 | end 275 | 276 | ActiveModel::Model.send :include, PhonyRails::Extension if defined?(ActiveModel::Model) 277 | 278 | # if defined?(Mongoid) 279 | # module Mongoid::Phony 280 | # extend ActiveSupport::Concern 281 | # include PhonyRails::Extension 282 | # end 283 | # end 284 | 285 | Dir["#{File.dirname(__FILE__)}/phony_rails/locales/*.yml"].each do |file| 286 | I18n.load_path << file 287 | end 288 | -------------------------------------------------------------------------------- /lib/phony_rails/locales/de.yml: -------------------------------------------------------------------------------- 1 | de: 2 | errors: 3 | messages: 4 | improbable_phone: "ist keine gültige Nummer" 5 | -------------------------------------------------------------------------------- /lib/phony_rails/locales/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | errors: 3 | messages: 4 | improbable_phone: "is an invalid number" 5 | -------------------------------------------------------------------------------- /lib/phony_rails/locales/es.yml: -------------------------------------------------------------------------------- 1 | es: 2 | errors: 3 | messages: 4 | improbable_phone: "es un número inválido" 5 | -------------------------------------------------------------------------------- /lib/phony_rails/locales/fr.yml: -------------------------------------------------------------------------------- 1 | fr: 2 | errors: 3 | messages: 4 | improbable_phone: "est un numéro invalide" 5 | -------------------------------------------------------------------------------- /lib/phony_rails/locales/he.yml: -------------------------------------------------------------------------------- 1 | he: 2 | errors: 3 | messages: 4 | improbable_phone: "אינו תקין" 5 | -------------------------------------------------------------------------------- /lib/phony_rails/locales/it.yml: -------------------------------------------------------------------------------- 1 | it: 2 | errors: 3 | messages: 4 | improbable_phone: "non è un numero valido" 5 | -------------------------------------------------------------------------------- /lib/phony_rails/locales/ja.yml: -------------------------------------------------------------------------------- 1 | ja: 2 | errors: 3 | messages: 4 | improbable_phone: "は正しい電話番号ではありません" 5 | -------------------------------------------------------------------------------- /lib/phony_rails/locales/km.yml: -------------------------------------------------------------------------------- 1 | km: 2 | errors: 3 | messages: 4 | improbable_phone: "គឺជាលេខមិនត្រឹមត្រូវ" 5 | -------------------------------------------------------------------------------- /lib/phony_rails/locales/ko.yml: -------------------------------------------------------------------------------- 1 | ko: 2 | errors: 3 | messages: 4 | improbable_phone: "는 올바른 전화번호가 아닙니다" 5 | -------------------------------------------------------------------------------- /lib/phony_rails/locales/nb.yml: -------------------------------------------------------------------------------- 1 | nb: 2 | errors: 3 | messages: 4 | improbable_phone: "er et ugyldig nummer" 5 | -------------------------------------------------------------------------------- /lib/phony_rails/locales/nl.yml: -------------------------------------------------------------------------------- 1 | nl: 2 | errors: 3 | messages: 4 | improbable_phone: "is geen geldig nummer" 5 | -------------------------------------------------------------------------------- /lib/phony_rails/locales/pt.yml: -------------------------------------------------------------------------------- 1 | es: 2 | errors: 3 | messages: 4 | improbable_phone: "é um número inválido" 5 | -------------------------------------------------------------------------------- /lib/phony_rails/locales/ru.yml: -------------------------------------------------------------------------------- 1 | ru: 2 | errors: 3 | messages: 4 | improbable_phone: "является недействительным номером" 5 | -------------------------------------------------------------------------------- /lib/phony_rails/locales/tr.yml: -------------------------------------------------------------------------------- 1 | tr: 2 | errors: 3 | messages: 4 | improbable_phone: "geçersiz numara" 5 | -------------------------------------------------------------------------------- /lib/phony_rails/locales/uk.yml: -------------------------------------------------------------------------------- 1 | uk: 2 | errors: 3 | messages: 4 | improbable_phone: "є недійсним номером" 5 | -------------------------------------------------------------------------------- /lib/phony_rails/string_extensions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class String 4 | # Usage: 5 | # "+31 (0)30 1234 123".phony_normalized # => '+31301234123' 6 | # "(0)30 1234 123".phony_normalized # => '301234123' 7 | # "(0)30 1234 123".phony_normalized(country_code: 'NL') # => '301234123' 8 | def phony_normalized(options = {}) 9 | raise ArgumentError, "Expected options to be a Hash, got #{options.inspect}" unless options.is_a?(Hash) 10 | 11 | options = options.dup 12 | PhonyRails.normalize_number(self, options) 13 | end 14 | 15 | # Add a method to the String class so we can easily format phone numbers. 16 | # This enables: 17 | # "31612341234".phony_formatted # => '06 12341234' 18 | # "31612341234".phony_formatted(:spaces => '-') # => '06-12341234' 19 | # To first normalize a String use: 20 | # "010-12341234".phony_formatted(:normalize => :NL) 21 | # To return nil when a number is not correct (checked using Phony.plausible?) use 22 | # "010-12341234".phony_formatted(strict: true) 23 | # When an error occurs during conversion it will return the original String. 24 | # To raise an error use: 25 | # "somestring".phone_formatted(raise: true) 26 | def phony_formatted(options = {}) 27 | raise ArgumentError, "Expected options to be a Hash, got #{options.inspect}" unless options.is_a?(Hash) 28 | 29 | options = options.dup 30 | normalize_country_code = options.delete(:normalize) 31 | s, ext = PhonyRails.extract_extension(self) 32 | s = (normalize_country_code ? PhonyRails.normalize_number(s, default_country_code: normalize_country_code.to_s, add_plus: false) : s.gsub(/\D/, '')) 33 | return if s.blank? 34 | return if options[:strict] && !Phony.plausible?(s) 35 | 36 | PhonyRails.format_extension(Phony.format(s, options.reverse_merge(format: :national)), ext) 37 | rescue StandardError 38 | raise if options[:raise] 39 | 40 | s 41 | end 42 | 43 | # The bang method 44 | def phony_formatted!(options = {}) 45 | raise ArgumentError, 'The :strict options is only supported in the phony_formatted (non bang) method.' if options[:strict] 46 | 47 | replace(phony_formatted(options)) 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/phony_rails/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module PhonyRails 4 | VERSION = '0.15.0' 5 | end 6 | -------------------------------------------------------------------------------- /lib/validators/phony_validator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Uses the Phony.plausible method to validate an attribute. 4 | # Usage: 5 | # validate :phone_number, :phony_plausible => true 6 | require 'active_model' 7 | 8 | class PhonyPlausibleValidator < ActiveModel::EachValidator 9 | # Validates a String using Phony.plausible? method. 10 | def validate_each(record, attribute, value) 11 | return if value.blank? 12 | 13 | @record = record 14 | value = PhonyRails.normalize_number(value.dup, default_country_code: normalized_country_code) if normalized_country_code 15 | value = PhonyRails.extract_extension(value).first 16 | @record.errors.add(attribute, error_message) unless Phony.plausible?(value, cc: country_number) 17 | @record.public_send("#{attribute}=", @record.public_send("#{attribute}_original")) if @record.respond_to?("#{attribute}_original") && 18 | !Phony.plausible?(value, cc: country_number) 19 | end 20 | 21 | private 22 | 23 | def error_message 24 | options[:message] || :improbable_phone 25 | end 26 | 27 | def country_number 28 | options_value(:country_number) || record_country_number || country_number_from_country_code 29 | end 30 | 31 | def record_country_number 32 | @record.country_number if @record.respond_to?(:country_number) && !options_value(:ignore_record_country_number) 33 | end 34 | 35 | def country_number_from_country_code 36 | PhonyRails.country_number_for(country_code) 37 | end 38 | 39 | def country_code 40 | options_value(:country_code) || record_country_code 41 | end 42 | 43 | def record_country_code 44 | @record.country_code if @record.respond_to?(:country_code) && !options_value(:ignore_record_country_code) 45 | end 46 | 47 | def normalized_country_code 48 | options_value(:normalized_country_code) 49 | end 50 | 51 | def options_value(option) 52 | option_value = options[option] 53 | 54 | return option_value unless option_value.is_a?(Symbol) 55 | 56 | @record.send(option_value) 57 | end 58 | end 59 | 60 | module ActiveModel 61 | module Validations 62 | module HelperMethods 63 | def validates_plausible_phone(*attr_names) 64 | # merged attributes are modified somewhere, so we are cloning them for each validator 65 | merged_attributes = _merge_attributes(attr_names) 66 | 67 | validates_with PresenceValidator, merged_attributes.dup if merged_attributes[:presence] 68 | validates_with FormatValidator, merged_attributes.dup if merged_attributes[:with] || merged_attributes[:without] 69 | validates_with PhonyPlausibleValidator, merged_attributes.dup 70 | end 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /phony_rails.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path('lib/phony_rails/version', __dir__) 4 | 5 | Gem::Specification.new do |gem| 6 | gem.authors = ['Joost Hietbrink'] 7 | gem.email = ['joost@joopp.com'] 8 | gem.description = 'This Gem adds useful methods to your Rails app to validate, display and save phone numbers.' 9 | gem.summary = 'This Gem adds useful methods to your Rails app to validate, display and save phone numbers.' 10 | gem.homepage = 'https://github.com/joost/phony_rails' 11 | gem.license = 'MIT' 12 | 13 | gem.files = `git ls-files`.split($OUTPUT_RECORD_SEPARATOR) 14 | gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) } 15 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 16 | gem.name = 'phony_rails' 17 | gem.require_paths = ['lib'] 18 | gem.version = PhonyRails::VERSION 19 | gem.required_ruby_version = '>= 2.4' 20 | 21 | gem.add_runtime_dependency 'activesupport', '>= 3.0' 22 | gem.add_runtime_dependency 'phony', '>= 2.18.12' 23 | gem.add_development_dependency 'activerecord', '>= 3.0' 24 | 25 | # For testing 26 | gem.add_development_dependency 'sqlite3', '>= 1.4.0' 27 | end 28 | -------------------------------------------------------------------------------- /spec/lib/phony_rails_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | EXT_PREFIXES = %w[ext ex x xt # :].freeze 6 | describe PhonyRails do 7 | it 'should not pollute the global namespace with a Country class' do 8 | should_not be_const_defined 'Country' 9 | end 10 | 11 | describe 'phony_format String extension' do 12 | describe 'the phony_formatted method' do 13 | it 'does not modify the original options Hash' do 14 | options = { normalize: :NL, format: :international } 15 | '0101234123'.phony_formatted(options) 16 | expect(options).to eql(normalize: :NL, format: :international) 17 | end 18 | 19 | describe 'with the bang!' do 20 | it 'changes the String using the bang method' do 21 | # Mutable String 22 | s = +'0101234123' rescue '0101234123' 23 | expect(s.phony_formatted!(normalize: :NL, format: :international)).to eql('+31 10 123 4123') 24 | expect(s).to eql('+31 10 123 4123') 25 | end 26 | end 27 | 28 | describe 'with strict option' do 29 | it 'returns nil with non plausible number' do 30 | number = '+319090' # not valid 31 | expect(Phony.plausible?(number)).to be false 32 | expect(number.phony_formatted(strict: true)).to eql(nil) 33 | end 34 | 35 | it 'should not return nil with plausible number' do 36 | number = '+31101234123' # valid 37 | expect(Phony.plausible?(number)).to be true 38 | expect(number.phony_formatted(strict: true)).to_not eql(nil) 39 | end 40 | end 41 | 42 | describe 'with normalize option' do 43 | it 'should phony_format' do 44 | expect('101234123'.phony_formatted(normalize: :NL)).to eql('010 123 4123') 45 | expect('101234123'.phony_formatted(normalize: :NL, format: :international)).to eql('+31 10 123 4123') 46 | end 47 | 48 | it 'should not change original String' do 49 | s = '0101234123' 50 | expect(s.phony_formatted(normalize: :NL)).to eql('010 123 4123') 51 | expect(s).to eql('0101234123') 52 | end 53 | 54 | it 'should phony_format String with country code' do 55 | expect('31101234123'.phony_formatted(normalize: :NL)).to eql('010 123 4123') 56 | end 57 | 58 | it 'should phony_format String with country code' do 59 | expect('31101234123'.phony_formatted(normalize: :NL)).to eql('010 123 4123') 60 | end 61 | 62 | it 'should accept strings with non-digits in it' do 63 | expect('+31-10-1234123'.phony_formatted(normalize: :NL, format: :international, spaces: '-')).to eql('+31-10-123-4123') 64 | end 65 | 66 | it 'should phony_format String with country code different than normalized value' do 67 | expect('+4790909090'.phony_formatted(normalize: :SE, format: :international)).to eql('+47 909 09 090') 68 | end 69 | end 70 | 71 | describe 'with raise option' do 72 | # https://github.com/joost/phony_rails/issues/79 73 | context 'when raise is true' do 74 | it 'should raise the error' do 75 | expect(lambda do 76 | '8887716095'.phony_formatted(format: :international, raise: true) 77 | end).to raise_error(Phony::FormattingError) 78 | end 79 | end 80 | 81 | context 'when raise is false (default)' do 82 | it 'returns original String on exception' do 83 | expect('8887716095'.phony_formatted(format: :international)).to eq('8887716095') 84 | end 85 | end 86 | end 87 | 88 | describe 'with extensions' do 89 | EXT_PREFIXES.each do |prefix| 90 | it "should format number with #{prefix} extension" do 91 | expect("+319090#{prefix}123".phony_formatted(strict: true)).to eql(nil) 92 | expect("101234123#{prefix}123".phony_formatted(normalize: :NL)).to eql('010 123 4123 x123') 93 | expect("101234123#{prefix}123".phony_formatted(normalize: :NL, format: :international)).to eql('+31 10 123 4123 x123') 94 | expect("31101234123#{prefix}123".phony_formatted(normalize: :NL)).to eql('010 123 4123 x123') 95 | expect("8887716095#{prefix}123".phony_formatted(format: :international, normalize: 'US', raise: true)).to eq('+1 (888) 771-6095 x123') 96 | expect("+12145551212#{prefix}123".phony_formatted).to eq('(214) 555-1212 x123') 97 | end 98 | end 99 | end 100 | 101 | describe 'specific tests from issues' do 102 | # https://github.com/joost/phony_rails/issues/79 103 | it 'should pass Github issue #42' do 104 | expect('8887716095'.phony_formatted(format: :international, normalize: 'US', raise: true)).to eq('+1 (888) 771-6095') 105 | end 106 | 107 | # https://github.com/joost/phony_rails/issues/42 108 | it 'should pass Github issue #42' do 109 | expect(PhonyRails.normalize_number('0606060606', default_country_code: 'FR')).to eq('+33606060606') 110 | end 111 | 112 | it 'should pass Github issue #85' do 113 | expect(PhonyRails.normalize_number('47386160', default_country_code: 'NO')).to eq('+4747386160') 114 | expect(PhonyRails.normalize_number('47386160', country_number: '47')).to eq('+4747386160') 115 | end 116 | 117 | it 'should pass Github issue #87' do 118 | expect(PhonyRails.normalize_number('2318725305', country_code: 'US')).to eq('+12318725305') 119 | expect(PhonyRails.normalize_number('2318725305', default_country_code: 'US')).to eq('+12318725305') 120 | expect(PhonyRails.normalize_number('+2318725305', default_country_code: 'US')).to eq('+2318725305') 121 | # expect(Phony.plausible?("#{PhonyRails.country_number_for('US')}02318725305")).to be_truthy 122 | expect(PhonyRails.normalize_number('02318725305', default_country_code: 'US')).to eq('+12318725305') 123 | end 124 | 125 | it 'should pass Github issue #89' do 126 | number = '+33 (0)6 87 36 18 75' 127 | expect(Phony.plausible?(number)).to be true 128 | expect(PhonyRails.normalize_number(number, country_code: 'FR')).to eq('+33687361875') 129 | end 130 | 131 | it 'should pass Github issue #90' do 132 | number = '(0)30 1234 123' 133 | expect(number.phony_normalized(country_code: 'NL')).to eq('+31301234123') 134 | end 135 | 136 | it 'should pass Github issue #107' do 137 | number = '04575700834' 138 | expect(number.phony_normalized(country_code: 'FI')).to eq('+3584575700834') 139 | # Seems this number can be interpreted as from multiple countries, following fails: 140 | # expect(number.phony_normalized(default_country_code: 'FI')).to eq('+3584575700834') 141 | # expect("04575700834".phony_formatted(normalize: 'FI', format: :international)).to eql('+358 45 757 00 834') 142 | end 143 | 144 | it 'should pass Github issue #113' do 145 | number = '(951) 703-593' 146 | expect(lambda do 147 | number.phony_formatted!(normalize: 'US', spaces: '-', strict: true) 148 | end).to raise_error(ArgumentError) 149 | end 150 | 151 | it 'should pass Github issue #95' do 152 | number = '02031234567' 153 | expect(number.phony_normalized(default_country_code: 'GB')).to eq('+442031234567') 154 | end 155 | 156 | it 'should pass Github issue #121' do 157 | number = '06-87-73-83-58' 158 | expect(number.phony_normalized(default_country_code: 'FR')).to eq('+33687738358') 159 | end 160 | 161 | it 'returns the original input if all goes wrong' do 162 | expect(Phony).to receive(:plausible?).and_raise('unexpected error') 163 | number = '(0)30 1234 123' 164 | expect(number.phony_normalized(country_code: 'NL')).to eq number 165 | end 166 | 167 | it 'should pass Github issue #126 (country_code)' do 168 | phone = '0143590213' # A plausible FR number 169 | phone = PhonyRails.normalize_number(phone, country_code: 'FR') 170 | expect(phone).to eq('+33143590213') 171 | expect(Phony.plausible?(phone)).to be_truthy 172 | phone = PhonyRails.normalize_number(phone, country_code: 'FR') 173 | expect(phone).to eq('+33143590213') 174 | end 175 | 176 | # Adding a country code is expected behavior when a 177 | # number is nog plausible. 178 | it 'should pass Github issue #126 (country_code) (intended)' do 179 | phone = '06123456789' # A non-plausible FR number 180 | phone = PhonyRails.normalize_number(phone, country_code: 'FR') 181 | expect(phone).to eq('+336123456789') 182 | expect(Phony.plausible?(phone)).to be_falsy 183 | phone = PhonyRails.normalize_number(phone, country_code: 'FR') 184 | expect(phone).to eq('+33336123456789') 185 | end 186 | 187 | it 'should pass Github issue #126 (default_country_code)' do 188 | phone = '06123456789' # French phone numbers have to be 10 chars long 189 | phone = PhonyRails.normalize_number(phone, default_country_code: 'FR') 190 | expect(phone).to eq('+336123456789') 191 | phone = PhonyRails.normalize_number(phone, default_country_code: 'FR') 192 | expect(phone).to eq('+336123456789') 193 | end 194 | 195 | it 'should pass Github issue #92 (invalid number with normalization)' do 196 | ActiveRecord::Schema.define do 197 | create_table :normal_homes do |table| 198 | table.column :phone_number, :string 199 | end 200 | end 201 | 202 | # rubocop:disable Lint/ConstantDefinitionInBlock 203 | class NormalHome < ActiveRecord::Base 204 | attr_accessor :phone_number 205 | 206 | phony_normalize :phone_number, default_country_code: 'US' 207 | validates :phone_number, phony_plausible: true 208 | end 209 | # rubocop:enable Lint/ConstantDefinitionInBlock 210 | 211 | normal = NormalHome.new 212 | normal.phone_number = 'HAHA' 213 | expect(normal).to_not be_valid 214 | expect(normal.phone_number).to eq('HAHA') 215 | expect(normal.errors.messages.to_hash).to include(phone_number: ['is an invalid number']) 216 | end 217 | 218 | it 'should pass Github issue #170' do 219 | phone = '(+49) 175 123 4567' 220 | phone = PhonyRails.normalize_number(phone) 221 | expect(phone).to eq('+491751234567') 222 | end 223 | 224 | it 'should pass Github issue #170 (part 2)' do 225 | phone = '(+49) 175 123 4567' 226 | phone = PhonyRails.normalize_number(phone, default_country_code: 'DE') 227 | expect(phone).to eq('+491751234567') 228 | end 229 | 230 | it 'should pass Github issue #175' do 231 | phone = '0041 23456789' 232 | phone = PhonyRails.normalize_number(phone, default_country_code: 'DE') 233 | expect(phone).to eq('+4123456789') 234 | end 235 | 236 | it 'should pass Github issue #175' do 237 | phone = '+1 260-437-9123' 238 | phone = PhonyRails.normalize_number(phone, default_country_code: 'DE') 239 | expect(phone).to eq('+12604379123') 240 | end 241 | 242 | it 'should pass Github issue #187' do 243 | phone1 = '0037253400030' 244 | phone1 = PhonyRails.normalize_number(phone1, default_country_code: 'EE') 245 | expect(phone1).to eq('+37253400030') 246 | 247 | phone2 = '0037275016183' 248 | phone2 = PhonyRails.normalize_number(phone2, default_country_code: 'EE') 249 | expect(phone2).to eq('+37275016183') 250 | end 251 | 252 | it 'should pass Github issue #180' do 253 | phone = '5555555555' 254 | phone = PhonyRails.normalize_number(phone, default_country_code: 'AU') 255 | expect(phone).to eq('+615555555555') 256 | phone = PhonyRails.normalize_number(phone, default_country_code: 'AU') 257 | expect(phone).to eq('+615555555555') 258 | end 259 | end 260 | 261 | it 'should not change original String' do 262 | s = '0101234123' 263 | expect(s.phony_formatted(normalize: :NL)).to eql('010 123 4123') 264 | expect(s).to eql('0101234123') 265 | end 266 | 267 | it 'should phony_format a digits string with spaces String' do 268 | expect('31 10 1234123'.phony_formatted(format: :international, spaces: '-')).to eql('+31-10-123-4123') 269 | end 270 | 271 | it 'should phony_format a digits String' do 272 | expect('31101234123'.phony_formatted(format: :international, spaces: '-')).to eql('+31-10-123-4123') 273 | end 274 | 275 | it 'returns nil if implausible phone' do 276 | expect('this is not a phone'.phony_formatted).to be_nil 277 | end 278 | 279 | it 'returns nil on blank string' do 280 | expect(''.phony_formatted).to be_nil 281 | end 282 | end 283 | 284 | describe 'the phony_normalized method' do 285 | it 'returns blank on blank string' do 286 | expect(''.phony_normalized).to be_nil 287 | end 288 | 289 | it 'should not modify the original options Hash' do 290 | options = { normalize: :NL, format: :international } 291 | '0101234123'.phony_normalized(options) 292 | expect(options).to eql(normalize: :NL, format: :international) 293 | end 294 | 295 | context 'when String misses a country_code' do 296 | it 'should normalize with :country_code option' do 297 | expect('010 1231234'.phony_normalized(country_code: :NL)).to eql('+31101231234') 298 | end 299 | 300 | it 'should normalize without :country_code option' do 301 | expect('010 1231234'.phony_normalized).to eql('101231234') 302 | end 303 | 304 | it 'should normalize with :add_plus option' do 305 | expect('010 1231234'.phony_normalized(country_code: :NL, add_plus: false)).to eql('31101231234') 306 | end 307 | end 308 | 309 | it 'should normalize with :add_plus option' do 310 | expect('+31 (0)10 1231234'.phony_normalized(add_plus: false)).to eql('31101231234') 311 | end 312 | 313 | it 'should normalize a String' do 314 | expect('+31 (0)10 1231234'.phony_normalized).to eql('+31101231234') 315 | end 316 | end 317 | end 318 | 319 | describe 'PhonyRails#normalize_number' do 320 | context 'number with a country code' do 321 | it 'should not add default_country_code' do 322 | expect(PhonyRails.normalize_number('+4790909090', default_country_code: 'SE')).to eql('+4790909090') # SE = +46 323 | expect(PhonyRails.normalize_number('004790909090', default_country_code: 'SE')).to eql('+4790909090') 324 | expect(PhonyRails.normalize_number('4790909090', default_country_code: 'NO')).to eql('+4790909090') # NO = +47 325 | end 326 | 327 | it 'should force add country_code' do 328 | expect(PhonyRails.normalize_number('+4790909090', country_code: 'SE')).to eql('+464790909090') 329 | expect(PhonyRails.normalize_number('004790909090', country_code: 'SE')).to eql('+4604790909090') # FIXME: differs due to Phony.normalize in v2.7.1?! 330 | expect(PhonyRails.normalize_number('4790909090', country_code: 'SE')).to eql('+464790909090') 331 | end 332 | 333 | it 'should recognize lowercase country codes' do 334 | expect(PhonyRails.normalize_number('4790909090', country_code: 'se')).to eql('+464790909090') 335 | end 336 | end 337 | 338 | context 'number without a country code' do 339 | it 'should normalize with a default_country_code' do 340 | expect(PhonyRails.normalize_number('010-1234123', default_country_code: 'NL')).to eql('+31101234123') 341 | end 342 | 343 | it 'should normalize with a country_code' do 344 | expect(PhonyRails.normalize_number('010-1234123', country_code: 'NL', default_country_code: 'DE')).to eql('+31101234123') 345 | expect(PhonyRails.normalize_number('010-1234123', country_code: 'NL')).to eql('+31101234123') 346 | end 347 | 348 | it 'should handle different countries' do 349 | expect(PhonyRails.normalize_number('(030) 8 61 29 06', country_code: 'DE')).to eql('+49308612906') 350 | expect(PhonyRails.normalize_number('0203 330 8897', country_code: 'GB')).to eql('+442033308897') 351 | end 352 | 353 | it 'should prefer country_code over default_country_code' do 354 | expect(PhonyRails.normalize_number('(030) 8 61 29 06', country_code: 'DE', default_country_code: 'NL')).to eql('+49308612906') 355 | end 356 | 357 | it 'should recognize lowercase country codes' do 358 | expect(PhonyRails.normalize_number('010-1234123', country_code: 'nl')).to eql('+31101234123') 359 | end 360 | end 361 | 362 | context 'number with an extension' do 363 | EXT_PREFIXES.each do |prefix| 364 | it "should handle some edge cases (with country_code) and #{prefix} extension" do 365 | expect(PhonyRails.normalize_number("some nasty stuff in this +31 number 10-1234123 string #{prefix}123", country_code: 'NL')).to eql('+31101234123 x123') 366 | expect(PhonyRails.normalize_number("070-4157134#{prefix}123", country_code: 'NL')).to eql('+31704157134 x123') 367 | expect(PhonyRails.normalize_number("0031-70-4157134#{prefix}123", country_code: 'NL')).to eql('+31704157134 x123') 368 | expect(PhonyRails.normalize_number("+31-70-4157134#{prefix}123", country_code: 'NL')).to eql('+31704157134 x123') 369 | expect(PhonyRails.normalize_number("0322-69497#{prefix}123", country_code: 'BE')).to eql('+3232269497 x123') 370 | expect(PhonyRails.normalize_number("+32 3 226 94 97#{prefix}123", country_code: 'BE')).to eql('+3232269497 x123') 371 | expect(PhonyRails.normalize_number("0450 764 000#{prefix}123", country_code: 'AU')).to eql('+61450764000 x123') 372 | end 373 | 374 | it "should handle some edge cases (with default_country_code) and #{prefix}" do 375 | expect(PhonyRails.normalize_number("some nasty stuff in this +31 number 10-1234123 string #{prefix}123", country_code: 'NL')).to eql('+31101234123 x123') 376 | expect(PhonyRails.normalize_number("070-4157134#{prefix}123", default_country_code: 'NL')).to eql('+31704157134 x123') 377 | expect(PhonyRails.normalize_number("0031-70-4157134#{prefix}123", default_country_code: 'NL')).to eql('+31704157134 x123') 378 | expect(PhonyRails.normalize_number("+31-70-4157134#{prefix}123", default_country_code: 'NL')).to eql('+31704157134 x123') 379 | expect(PhonyRails.normalize_number("0322-69497#{prefix}123", default_country_code: 'BE')).to eql('+3232269497 x123') 380 | expect(PhonyRails.normalize_number("+32 3 226 94 97#{prefix}123", default_country_code: 'BE')).to eql('+3232269497 x123') 381 | expect(PhonyRails.normalize_number("0450 764 000#{prefix}123", default_country_code: 'AU')).to eql('+61450764000 x123') 382 | end 383 | 384 | it "should remove #{prefix} extension (with extension: false)" do 385 | expect(PhonyRails.normalize_number("0031-70-4157134#{prefix}123", extension: false, country_code: 'NL')).to eql('+31704157134') 386 | expect(PhonyRails.normalize_number("+31-70-4157134#{prefix}123", extension: false, country_code: 'NL')).to eql('+31704157134') 387 | expect(PhonyRails.normalize_number("0322-69497#{prefix}123", extension: false, country_code: 'BE')).to eql('+3232269497') 388 | end 389 | end 390 | end 391 | 392 | it 'should handle some edge cases (with country_code)' do 393 | expect(PhonyRails.normalize_number('some nasty stuff in this +31 number 10-1234123 string', country_code: 'NL')).to eql('+31101234123') 394 | expect(PhonyRails.normalize_number('070-4157134', country_code: 'NL')).to eql('+31704157134') 395 | expect(PhonyRails.normalize_number('0031-70-4157134', country_code: 'NL')).to eql('+31704157134') 396 | expect(PhonyRails.normalize_number('+31-70-4157134', country_code: 'NL')).to eql('+31704157134') 397 | expect(PhonyRails.normalize_number('0322-69497', country_code: 'BE')).to eql('+3232269497') 398 | expect(PhonyRails.normalize_number('+32 3 226 94 97', country_code: 'BE')).to eql('+3232269497') 399 | expect(PhonyRails.normalize_number('0450 764 000', country_code: 'AU')).to eql('+61450764000') 400 | end 401 | 402 | it 'should handle some edge cases (with default_country_code)' do 403 | expect(PhonyRails.normalize_number('some nasty stuff in this +31 number 10-1234123 string', country_code: 'NL')).to eql('+31101234123') 404 | expect(PhonyRails.normalize_number('070-4157134', default_country_code: 'NL')).to eql('+31704157134') 405 | expect(PhonyRails.normalize_number('0031-70-4157134', default_country_code: 'NL')).to eql('+31704157134') 406 | expect(PhonyRails.normalize_number('+31-70-4157134', default_country_code: 'NL')).to eql('+31704157134') 407 | expect(PhonyRails.normalize_number('0322-69497', default_country_code: 'BE')).to eql('+3232269497') 408 | expect(PhonyRails.normalize_number('+32 3 226 94 97', default_country_code: 'BE')).to eql('+3232269497') 409 | expect(PhonyRails.normalize_number('0450 764 000', default_country_code: 'AU')).to eql('+61450764000') 410 | end 411 | 412 | it 'should normalize even an implausible number' do 413 | expect(PhonyRails.normalize_number('01')).to eql('1') 414 | end 415 | 416 | context 'with default_country_code set' do 417 | before { PhonyRails.default_country_code = 'NL' } 418 | after { PhonyRails.default_country_code = nil } 419 | 420 | it 'normalize using the default' do 421 | expect(PhonyRails.normalize_number('010-1234123')).to eql('+31101234123') 422 | expect(PhonyRails.normalize_number('010-1234123')).to eql('+31101234123') 423 | expect(PhonyRails.normalize_number('070-4157134')).to eql('+31704157134') 424 | expect(PhonyRails.normalize_number('0031-70-4157134')).to eql('+31704157134') 425 | expect(PhonyRails.normalize_number('+31-70-4157134')).to eql('+31704157134') 426 | end 427 | 428 | it 'allows default_country_code to be overridden' do 429 | expect(PhonyRails.normalize_number('0322-69497', country_code: 'BE')).to eql('+3232269497') 430 | expect(PhonyRails.normalize_number('+32 3 226 94 97', country_code: 'BE')).to eql('+3232269497') 431 | expect(PhonyRails.normalize_number('0450 764 000', country_code: 'AU')).to eql('+61450764000') 432 | 433 | expect(PhonyRails.normalize_number('0322-69497', default_country_code: 'BE')).to eql('+3232269497') 434 | expect(PhonyRails.normalize_number('+32 3 226 94 97', default_country_code: 'BE')).to eql('+3232269497') 435 | expect(PhonyRails.normalize_number('0450 764 000', default_country_code: 'AU')).to eql('+61450764000') 436 | end 437 | end 438 | end 439 | 440 | describe 'PhonyRails.plausible_number?' do 441 | subject { described_class } 442 | let(:valid_number) { '1 555 555 5555' } 443 | let(:invalid_number) { '123456789 123456789 123456789 123456789' } 444 | let(:another_invalid_number) { '441212' } 445 | let(:normalizable_number) { '555 555 5555' } 446 | let(:formatted_french_number_with_country_code) { '+33 627899541' } 447 | let(:empty_number) { '' } 448 | let(:nil_number) { nil } 449 | 450 | it 'returns true for a valid number' do 451 | is_expected.to be_plausible_number valid_number, country_code: 'US' 452 | end 453 | 454 | it 'returns false for an invalid numbers' do 455 | is_expected.not_to be_plausible_number invalid_number 456 | is_expected.not_to be_plausible_number invalid_number, country_code: 'US' 457 | is_expected.not_to be_plausible_number another_invalid_number 458 | is_expected.not_to be_plausible_number another_invalid_number, country_code: 'US' 459 | end 460 | 461 | it 'returns true for a normalizable number' do 462 | is_expected.to be_plausible_number normalizable_number, country_code: 'US' 463 | end 464 | 465 | it 'returns false for a valid number with the wrong country code' do 466 | is_expected.not_to be_plausible_number normalizable_number, country_code: 'FR' 467 | end 468 | 469 | it 'returns true for a well formatted valid number' do 470 | is_expected.to be_plausible_number formatted_french_number_with_country_code, country_code: 'FR' 471 | end 472 | 473 | it 'returns false for an empty number' do 474 | is_expected.not_to be_plausible_number empty_number, country_code: 'US' 475 | end 476 | 477 | it 'returns false for a nil number' do 478 | is_expected.not_to be_plausible_number nil_number, country_code: 'US' 479 | end 480 | 481 | it 'returns false when no country code is supplied' do 482 | is_expected.not_to be_plausible_number normalizable_number 483 | end 484 | 485 | it 'returns false if something goes wrong' do 486 | expect(Phony).to receive(:plausible?).twice.and_raise('unexpected error') 487 | is_expected.not_to be_plausible_number normalizable_number, country_code: 'US' 488 | end 489 | 490 | it 'should pass Github issue #95' do 491 | is_expected.to be_plausible_number '+358414955444', default_country_code: :de 492 | end 493 | 494 | context 'with default_country_code set' do 495 | before { PhonyRails.default_country_code = 'FR' } 496 | after { PhonyRails.default_country_code = nil } 497 | 498 | it 'uses the default' do 499 | is_expected.not_to be_plausible_number normalizable_number 500 | is_expected.to be_plausible_number formatted_french_number_with_country_code 501 | end 502 | 503 | it 'allows default_country_code to be overridden' do 504 | is_expected.not_to be_plausible_number empty_number, country_code: 'US' 505 | is_expected.not_to be_plausible_number nil_number, country_code: 'US' 506 | end 507 | end 508 | end 509 | 510 | describe 'PhonyRails.default_country' do 511 | before { PhonyRails.default_country_code = 'US' } 512 | after { PhonyRails.default_country_code = nil } 513 | 514 | it 'can set a global default country code' do 515 | expect(PhonyRails.default_country_code).to eq 'US' 516 | end 517 | 518 | it 'can set a global default country code' do 519 | PhonyRails.default_country_number = '1' 520 | expect(PhonyRails.default_country_number).to eq '1' 521 | end 522 | 523 | it 'default country code affects default country number' do 524 | expect(PhonyRails.default_country_number).to eq '1' 525 | end 526 | end 527 | 528 | describe 'PhonyRails#extract_extension' do 529 | it 'returns [nil, nil] on nil input' do 530 | expect(PhonyRails.extract_extension(nil)).to eq [nil, nil] 531 | end 532 | 533 | it 'returns [number, nil] when number does not have an extension' do 534 | expect(PhonyRails.extract_extension('123456789')).to eq ['123456789', nil] 535 | end 536 | 537 | EXT_PREFIXES.each do |prefix| 538 | it "returns [number, ext] when number has a #{prefix} extension" do 539 | expect(PhonyRails.extract_extension("123456789#{prefix}123")).to eq %w[123456789 123] 540 | end 541 | end 542 | end 543 | 544 | describe 'PhonyRails.country_code_from_number' do 545 | it 'returns the code of the plausible phone number' do 546 | expect(PhonyRails.country_code_from_number('+32475000000')).to eq '32' 547 | end 548 | end 549 | 550 | describe 'PhonyRails.country_from_number' do 551 | it 'returns the country of the plausible phone number' do 552 | expect(PhonyRails.country_from_number('+32475000000')).to eq 'BE' 553 | end 554 | end 555 | 556 | describe 'PhonyRails#format_extension' do 557 | it 'returns just number if no extension' do 558 | expect(PhonyRails.format_extension('+123456789', nil)).to eq '+123456789' 559 | end 560 | 561 | it 'returns number with extension if extension exists' do 562 | expect(PhonyRails.format_extension('+123456789', '123')).to eq '+123456789 x123' 563 | end 564 | end 565 | 566 | shared_examples_for 'model with PhonyRails' do 567 | describe 'defining model#phony_normalized_method' do 568 | it 'should add a normalized_phone_attribute method' do 569 | expect(model_klass.new).to respond_to(:normalized_phone_attribute) 570 | end 571 | 572 | it 'should add a normalized_phone_method method' do 573 | expect(model_klass.new).to respond_to(:normalized_phone_method) 574 | end 575 | 576 | it 'should raise error on existing methods' do 577 | expect(lambda do 578 | model_klass.phony_normalized_method(:phone_method) 579 | end).to raise_error(StandardError) 580 | end 581 | 582 | it 'should raise error on not existing attribute' do 583 | model_klass.phony_normalized_method(:phone_non_existing_method) 584 | expect(lambda do 585 | model_klass.new.normalized_phone_non_existing_method 586 | end).to raise_error(ArgumentError) 587 | end 588 | end 589 | 590 | describe 'defining model#phony_normalize' do 591 | it 'should not accept :as option with multiple attribute names' do 592 | expect(lambda do 593 | model_klass.phony_normalize(:phone_number, :phone1_method, as: 'non_existing_attribute') 594 | end).to raise_error(ArgumentError) 595 | end 596 | 597 | it 'should accept :as option with non existing attribute name' do 598 | expect(lambda do 599 | dummy_klass.phony_normalize(:non_existing_attribute, as: 'non_existing_attribute') 600 | end).to_not raise_error 601 | end 602 | 603 | it 'should accept :as option with single non existing attribute name' do 604 | expect(lambda do 605 | dummy_klass.phony_normalize(:phone_number, as: 'something_else') 606 | end).to_not raise_error 607 | end 608 | 609 | it 'should accept :as option with single existing attribute name' do 610 | expect(lambda do 611 | model_klass.phony_normalize(:phone_number, as: 'phone_number_as_normalized') 612 | end).to_not raise_error 613 | end 614 | 615 | it 'should accept a non existing attribute name' do 616 | expect(lambda do 617 | dummy_klass.phony_normalize(:non_existing_attribute) 618 | end).to_not raise_error 619 | end 620 | 621 | it 'should accept supported options' do 622 | options = %i[country_number default_country_number country_code default_country_code add_plus as enforce_record_country] 623 | options.each do |option_sym| 624 | expect(lambda do 625 | dummy_klass.phony_normalize(:phone_number, option_sym => false) 626 | end).to_not raise_error 627 | end 628 | end 629 | 630 | it 'should not accept unsupported options' do 631 | expect(lambda do 632 | dummy_klass.phony_normalize(:phone_number, unsupported_option: false) 633 | end).to raise_error(ArgumentError) 634 | end 635 | end 636 | 637 | describe 'using model#phony_normalized_method' do 638 | # Following examples have complete number (with country code!) 639 | it 'returns a normalized version of an attribute' do 640 | model = model_klass.new(phone_attribute: '+31-(0)10-1234123') 641 | expect(model.normalized_phone_attribute).to eql('+31101234123') 642 | end 643 | 644 | it 'returnsa normalized version of a method' do 645 | model = model_klass.new(phone_method: '+31-(0)10-1234123') 646 | expect(model.normalized_phone_method).to eql('+31101234123') 647 | end 648 | 649 | # Following examples have incomplete number 650 | it 'should normalize even a unplausible number (no country code)' do 651 | model = model_klass.new(phone_attribute: '(0)10-1234123') 652 | expect(model.normalized_phone_attribute).to eql('101234123') 653 | end 654 | 655 | it 'should use country_code option' do 656 | model = model_klass.new(phone_attribute: '(0)10-1234123') 657 | expect(model.normalized_phone_attribute(country_code: 'NL')).to eql('+31101234123') 658 | end 659 | 660 | it 'should use country_code object method' do 661 | model = model_klass.new(phone_attribute: '(0)10-1234123', country_code: 'NL') 662 | expect(model.normalized_phone_attribute).to eql('+31101234123') 663 | end 664 | 665 | it 'should fallback to default_country_code option' do 666 | model = model_klass.new(phone1_method: '(030) 8 61 29 06') 667 | expect(model.normalized_phone1_method).to eql('+49308612906') 668 | end 669 | 670 | it 'should overwrite default_country_code option with object method' do 671 | model = model_klass.new(phone1_method: '(030) 8 61 29 06', country_code: 'NL') 672 | expect(model.normalized_phone1_method).to eql('+31308612906') 673 | end 674 | 675 | it 'should overwrite default_country_code option with option' do 676 | model = model_klass.new(phone1_method: '(030) 8 61 29 06') 677 | expect(model.normalized_phone1_method(country_code: 'NL')).to eql('+31308612906') 678 | end 679 | 680 | it 'should use last passed options' do 681 | model = model_klass.new(phone1_method: '(030) 8 61 29 06') 682 | expect(model.normalized_phone1_method(country_code: 'NL')).to eql('+31308612906') 683 | expect(model.normalized_phone1_method(country_code: 'DE')).to eql('+49308612906') 684 | expect(model.normalized_phone1_method(country_code: nil)).to eql('+49308612906') 685 | end 686 | 687 | it 'should use last object method' do 688 | model = model_klass.new(phone1_method: '(030) 8 61 29 06') 689 | model.country_code = 'NL' 690 | expect(model.normalized_phone1_method).to eql('+31308612906') 691 | model.country_code = 'DE' 692 | expect(model.normalized_phone1_method).to eql('+49308612906') 693 | model.country_code = nil 694 | expect(model.normalized_phone1_method(country_code: nil)).to eql('+49308612906') 695 | end 696 | 697 | it 'should accept a symbol when setting country_code options' do 698 | model = model_klass.new(symboled_phone_method: '02031234567', country_code_attribute: 'GB') 699 | expect(model.normalized_symboled_phone_method).to eql('+442031234567') 700 | end 701 | end 702 | 703 | describe 'using model#phony_normalize' do 704 | it 'should not change normalized numbers (see #76)' do 705 | model = model_klass.new(phone_number: '+31-(0)10-1234123') 706 | expect(model).to be_valid 707 | expect(model.phone_number).to eql('+31101234123') 708 | end 709 | 710 | it 'should nilify attribute when it is set to nil' do 711 | model = model_klass.new(phone_number: '+31-(0)10-1234123') 712 | model.phone_number = nil 713 | expect(model).to be_valid 714 | expect(model.phone_number).to eql(nil) 715 | end 716 | 717 | it 'should nilify attribute when it is set to nil' do 718 | model = ActiveRecordModel.create!(phone_number: '+31-(0)10-1234123') 719 | model.phone_number = nil 720 | expect(model).to be_valid 721 | expect(model.save).to be(true) 722 | expect(model.reload.phone_number).to eql(nil) 723 | end 724 | 725 | it 'should empty attribute when it is set to ""' do # Github issue #149 726 | model = ActiveRecordModel.create!(phone_number: '+31-(0)10-1234123') 727 | model.phone_number = '' 728 | expect(model).to be_valid 729 | expect(model.save).to be(true) 730 | expect(model.reload.phone_number).to eql('') 731 | end 732 | 733 | it 'should set a normalized version of an attribute using :as option' do 734 | model_klass.phony_normalize :phone_number, as: :phone_number_as_normalized 735 | model = model_klass.new(phone_number: '+31-(0)10-1234123') 736 | expect(model).to be_valid 737 | expect(model.phone_number_as_normalized).to eql('+31101234123') 738 | end 739 | 740 | it 'should nilify normalized version of an attribute when it is set to nil using :as option ' do 741 | model_klass.phony_normalize :phone_number, as: :phone_number_as_normalized 742 | model = model_klass.new(phone_number: '+31-(0)10-1234123', phone_number_as_normalized: '+31101234123') 743 | model.phone_number = nil 744 | expect(model).to be_valid 745 | expect(model.phone_number_as_normalized).to eq(nil) 746 | end 747 | 748 | it 'should not add a + using :add_plus option' do 749 | model_klass.phony_normalize :phone_number, add_plus: false 750 | model = model_klass.new(phone_number: '+31-(0)10-1234123') 751 | expect(model).to be_valid 752 | expect(model.phone_number).to eql('31101234123') 753 | end 754 | 755 | it 'should raise a RuntimeError at validation if the attribute doesn\'t exist' do 756 | dummy_klass.phony_normalize :non_existing_attribute 757 | dummy = dummy_klass.new 758 | expect(lambda do 759 | dummy.valid? 760 | end).to raise_error(RuntimeError) 761 | end 762 | 763 | it 'should raise a RuntimeError at validation if the :as option attribute doesn\'t exist' do 764 | dummy_klass.phony_normalize :phone_number, as: :non_existing_attribute 765 | dummy = dummy_klass.new 766 | expect(lambda do 767 | dummy.valid? 768 | end).to raise_error(RuntimeError) 769 | end 770 | 771 | it 'should accept a symbol when setting country_code options' do 772 | model = model_klass.new(symboled_phone: '0606060606', country_code_attribute: 'FR') 773 | expect(model).to be_valid 774 | expect(model.symboled_phone).to eql('+33606060606') 775 | end 776 | 777 | context 'relational normalization' do 778 | it 'should normalize based on custom attribute of the current model' do 779 | model_klass.phony_normalize :phone_number, default_country_code: ->(instance) { instance.custom_country_code } 780 | model = model_klass.new phone_number: '012 416 0001', custom_country_code: 'MY' 781 | expect(model).to be_valid 782 | expect(model.phone_number).to eq('+60124160001') 783 | end 784 | 785 | it 'should normalize based on specific attribute of the associative model' do 786 | model_klass.phony_normalize :phone_number, default_country_code: ->(instance) { instance.home_country.country_code } 787 | home_country = double('home_country', country_code: 'MY') 788 | model = model_klass.new phone_number: '012 416 0001', home_country: home_country 789 | expect(model).to be_valid 790 | expect(model.phone_number).to eq('+60124160001') 791 | end 792 | 793 | it 'should normalize based on default value if missing associative model' do 794 | model_klass.phony_normalize :phone_number, default_country_code: ->(instance) { instance.home_country&.country_code || 'MY' } 795 | model = model_klass.new phone_number: '012 416 0001', home_country: nil 796 | expect(model).to be_valid 797 | expect(model.phone_number).to eq('+60124160001') 798 | end 799 | end 800 | 801 | context 'conditional normalization' do 802 | context 'standalone methods' do 803 | it 'should only normalize if the :if conditional is true' do 804 | model_klass.phony_normalize :recipient, default_country_code: 'US', if: :use_phone? 805 | 806 | sms_alarm = model_klass.new recipient: '222 333 4444', delivery_method: 'sms' 807 | email_alarm = model_klass.new recipient: 'foo123@example.com', delivery_method: 'email' 808 | expect(sms_alarm).to be_valid 809 | expect(email_alarm).to be_valid 810 | expect(sms_alarm.recipient).to eq('+12223334444') 811 | expect(email_alarm.recipient).to eq('foo123@example.com') 812 | end 813 | 814 | it 'should only normalize if the :unless conditional is false' do 815 | model_klass.phony_normalize :recipient, default_country_code: 'US', unless: :use_email? 816 | 817 | sms_alarm = model_klass.new recipient: '222 333 4444', delivery_method: 'sms' 818 | email_alarm = model_klass.new recipient: 'foo123@example.com', delivery_method: 'email' 819 | expect(sms_alarm).to be_valid 820 | expect(email_alarm).to be_valid 821 | expect(sms_alarm.recipient).to eq('+12223334444') 822 | expect(email_alarm.recipient).to eq('foo123@example.com') 823 | end 824 | end 825 | 826 | context 'using lambdas' do 827 | it 'should only normalize if the :if conditional is true' do 828 | model_klass.phony_normalize :recipient, default_country_code: 'US', if: -> { delivery_method == 'sms' } 829 | 830 | sms_alarm = model_klass.new recipient: '222 333 4444', delivery_method: 'sms' 831 | email_alarm = model_klass.new recipient: 'foo123@example.com', delivery_method: 'email' 832 | expect(sms_alarm).to be_valid 833 | expect(email_alarm).to be_valid 834 | expect(sms_alarm.recipient).to eq('+12223334444') 835 | expect(email_alarm.recipient).to eq('foo123@example.com') 836 | end 837 | 838 | it 'should only normalize if the :unless conditional is false' do 839 | model_klass.phony_normalize :recipient, default_country_code: 'US', unless: -> { delivery_method == 'email' } 840 | 841 | sms_alarm = model_klass.new recipient: '222 333 4444', delivery_method: 'sms' 842 | email_alarm = model_klass.new recipient: 'foo123@example.com', delivery_method: 'email' 843 | expect(sms_alarm).to be_valid 844 | expect(email_alarm).to be_valid 845 | expect(sms_alarm.recipient).to eq('+12223334444') 846 | expect(email_alarm.recipient).to eq('foo123@example.com') 847 | end 848 | end 849 | end 850 | end 851 | end 852 | 853 | describe 'ActiveModel + ActiveModel::Validations::Callbacks' do 854 | let(:model_klass) { ActiveModelModel } 855 | let(:dummy_klass) { ActiveModelDummy } 856 | it_behaves_like 'model with PhonyRails' 857 | end 858 | 859 | describe 'ActiveRecord' do 860 | let(:model_klass) { ActiveRecordModel } 861 | let(:dummy_klass) { ActiveRecordDummy } 862 | it_behaves_like 'model with PhonyRails' 863 | 864 | it 'should correctly keep a hard set country_code' do 865 | model = model_klass.new(fax_number: '+1 978 555 0000') 866 | expect(model.valid?).to be true 867 | expect(model.fax_number).to eql('+19785550000') 868 | expect(model.save).to be true 869 | expect(model.save).to be true # revalidate 870 | model.reload 871 | expect(model.fax_number).to eql('+19785550000') 872 | model.fax_number = '(030) 8 61 29 06' 873 | expect(model.save).to be true # revalidate 874 | model.reload 875 | expect(model.fax_number).to eql('+61308612906') 876 | end 877 | 878 | context 'when enforce_record_country is turned off' do 879 | let(:model_klass) { RelaxedActiveRecordModel } 880 | let(:record) { model_klass.new } 881 | 882 | before do 883 | record.phone_number = phone_number 884 | record.country_code = 'DE' 885 | record.valid? # run the empty validation chain to execute the before hook (normalized the number) 886 | end 887 | 888 | context 'when the country_code attribute does not match the country number' do 889 | context 'when the number is prefixed with a country number and a plus' do 890 | let(:phone_number) { '+436601234567' } 891 | 892 | it 'should not add the records country number' do 893 | expect(record.phone_number).to eql('+436601234567') 894 | end 895 | end 896 | 897 | # In this case it's not clear if there is a country number, so it should be added 898 | context 'when the number is prefixed with a country number' do 899 | let(:phone_number) { '436601234567' } 900 | 901 | it 'should add the records country number' do 902 | expect(record.phone_number).to eql('+49436601234567') 903 | end 904 | end 905 | end 906 | 907 | # This should be the case anyways 908 | context 'when the country_code attribute matches the country number' do 909 | context 'when the number includes a country number and a plus' do 910 | let(:phone_number) { '+491721234567' } 911 | 912 | it 'should not add the records country number' do 913 | expect(record.phone_number).to eql('+491721234567') 914 | end 915 | end 916 | 917 | context 'when the number has neither country number nor plus' do 918 | let(:phone_number) { '01721234567' } 919 | 920 | it 'should not add the records country number' do 921 | expect(record.phone_number).to eql('+491721234567') 922 | end 923 | end 924 | end 925 | end 926 | end 927 | 928 | # describe 'Mongoid' do 929 | # let(:model_klass) { MongoidModel } 930 | # let(:dummy_klass) { MongoidDummy } 931 | # it_behaves_like 'model with PhonyRails' 932 | # end 933 | end 934 | -------------------------------------------------------------------------------- /spec/lib/validators/phony_validator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | #----------------------------------------------------------------------------------------------------------------------- 6 | # Model 7 | #----------------------------------------------------------------------------------------------------------------------- 8 | 9 | #-------------------- 10 | ActiveRecord::Schema.define do 11 | create_table :simple_homes do |table| 12 | table.column :phone_number, :string 13 | end 14 | 15 | create_table :helpful_homes do |table| 16 | table.column :phone_number, :string 17 | end 18 | 19 | create_table :required_helpful_homes do |table| 20 | table.column :phone_number, :string 21 | end 22 | 23 | create_table :optional_helpful_homes do |table| 24 | table.column :phone_number, :string 25 | end 26 | 27 | create_table :formatted_helpful_homes do |table| 28 | table.column :phone_number, :string 29 | end 30 | 31 | create_table :not_formatted_helpful_homes do |table| 32 | table.column :phone_number, :string 33 | end 34 | 35 | create_table :normalizable_helpful_homes do |table| 36 | table.column :phone_number, :string 37 | end 38 | 39 | create_table :big_helpful_homes do |table| 40 | table.column :phone_number, :string 41 | end 42 | 43 | create_table :australian_helpful_homes do |table| 44 | table.column :phone_number, :string 45 | end 46 | 47 | create_table :polish_helpful_homes do |table| 48 | table.column :phone_number, :string 49 | end 50 | 51 | create_table :mismatched_helpful_homes do |table| 52 | table.column :phone_number, :string 53 | end 54 | 55 | create_table :invalid_country_code_helpful_homes do |table| 56 | table.column :phone_number, :string 57 | end 58 | 59 | create_table :symbolizable_helpful_homes do |table| 60 | table.column :phone_number, :string 61 | table.column :phone_number_country_code, :string 62 | end 63 | 64 | create_table :normalizabled_phone_homes do |table| 65 | table.column :phone_number, :string 66 | end 67 | end 68 | 69 | #-------------------- 70 | class SimpleHome < ActiveRecord::Base 71 | attr_accessor :phone_number 72 | 73 | validates :phone_number, phony_plausible: true 74 | end 75 | 76 | #-------------------- 77 | class HelpfulHome < ActiveRecord::Base 78 | attr_accessor :phone_number 79 | 80 | validates_plausible_phone :phone_number 81 | end 82 | 83 | #-------------------- 84 | class RequiredHelpfulHome < ActiveRecord::Base 85 | attr_accessor :phone_number 86 | 87 | validates_plausible_phone :phone_number, presence: true 88 | end 89 | 90 | #-------------------- 91 | class OptionalHelpfulHome < ActiveRecord::Base 92 | attr_accessor :phone_number 93 | 94 | validates_plausible_phone :phone_number, presence: false 95 | end 96 | 97 | #-------------------- 98 | class FormattedHelpfulHome < ActiveRecord::Base 99 | attr_accessor :phone_number 100 | 101 | validates_plausible_phone :phone_number, with: /\A\+\d+/ 102 | end 103 | 104 | #-------------------- 105 | class NotFormattedHelpfulHome < ActiveRecord::Base 106 | attr_accessor :phone_number 107 | 108 | validates_plausible_phone :phone_number, without: /\A\+\d+/ 109 | end 110 | 111 | #-------------------- 112 | class NormalizableHelpfulHome < ActiveRecord::Base 113 | attr_accessor :phone_number 114 | 115 | validates_plausible_phone :phone_number, normalized_country_code: 'US' 116 | end 117 | 118 | #-------------------- 119 | class AustralianHelpfulHome < ActiveRecord::Base 120 | attr_accessor :phone_number 121 | 122 | validates_plausible_phone :phone_number, country_number: '61' 123 | end 124 | 125 | #-------------------- 126 | class PolishHelpfulHome < ActiveRecord::Base 127 | attr_accessor :phone_number 128 | 129 | validates_plausible_phone :phone_number, country_code: 'PL' 130 | end 131 | 132 | #-------------------- 133 | class BigHelpfulHome < ActiveRecord::Base 134 | attr_accessor :phone_number 135 | 136 | validates_plausible_phone :phone_number, presence: true, with: /\A\+\d+/, country_number: '33' 137 | end 138 | 139 | #-------------------- 140 | class MismatchedHelpfulHome < ActiveRecord::Base 141 | attr_accessor :phone_number, :country_code 142 | 143 | validates :phone_number, phony_plausible: { ignore_record_country_code: true } 144 | end 145 | 146 | #-------------------- 147 | 148 | class InvalidCountryCodeHelpfulHome < ActiveRecord::Base 149 | attr_accessor :phone_number 150 | 151 | validates_plausible_phone :phone_number 152 | 153 | def country_code 154 | '--' 155 | end 156 | end 157 | 158 | #-------------------- 159 | class SymbolizableHelpfulHome < ActiveRecord::Base 160 | attr_accessor :phone_number, :phone_number_country_code 161 | 162 | validates_plausible_phone :phone_number, country_code: :phone_number_country_code 163 | end 164 | 165 | #-------------------- 166 | class NoModelMethod < HelpfulHome 167 | attr_accessor :phone_number 168 | 169 | validates_plausible_phone :phone_number, country_code: :nonexistent_method 170 | end 171 | 172 | #-------------------- 173 | class MessageOptionUndefinedInModel < HelpfulHome 174 | attr_accessor :phone_number 175 | 176 | validates_plausible_phone :phone_number, message: :email 177 | end 178 | 179 | #-------------------- 180 | class MessageOptionSameAsModelMethod < HelpfulHome 181 | attr_accessor :phone_number 182 | 183 | validates_plausible_phone :phone_number, message: :email 184 | 185 | def email 186 | 'user@example.com' 187 | end 188 | end 189 | 190 | #-------------------- 191 | class NormalizabledPhoneHome < ActiveRecord::Base 192 | attr_accessor :phone_number, :phone_number2, :country_code 193 | 194 | validates_plausible_phone :phone_number 195 | validates_plausible_phone :phone_number2 196 | phony_normalize :phone_number, country_code: 'PL', normalize_when_valid: true 197 | phony_normalize :phone_number2, country_code: 'PL', normalize_when_valid: true 198 | end 199 | 200 | #----------------------------------------------------------------------------------------------------------------------- 201 | # Tests 202 | #----------------------------------------------------------------------------------------------------------------------- 203 | 204 | I18n.locale = :en 205 | VALID_NUMBER = '1 555 555 5555' 206 | VALID_NUMBER_WITH_EXTENSION = '1 555 555 5555 x123' 207 | VALID_NUMBER_WITH_INVALID_EXTENSION = '1 555 555 5555 x1a' 208 | NORMALIZABLE_NUMBER = '555 555 5555' 209 | AUSTRALIAN_NUMBER_WITH_COUNTRY_CODE = '61390133997' 210 | POLISH_NUMBER_WITH_COUNTRY_CODE = '48600600600' 211 | FORMATTED_AUSTRALIAN_NUMBER_WITH_COUNTRY_CODE = '+61 390133997' 212 | FRENCH_NUMBER_WITH_COUNTRY_CODE = '33627899541' 213 | FORMATTED_FRENCH_NUMBER_WITH_COUNTRY_CODE = '+33 627899541' 214 | CROATIA_NUMBER_WITH_COUNTRY_CODE = '385 98 352 085' 215 | INVALID_NUMBER = '123456789 123456789 123456789 123456789' 216 | NOT_A_NUMBER = 'HAHA' 217 | JAPAN_COUNTRY = 'jp' 218 | 219 | #----------------------------------------------------------------------------------------------------------------------- 220 | describe PhonyPlausibleValidator do 221 | #-------------------- 222 | describe '#validates' do 223 | before(:each) do 224 | @home = SimpleHome.new 225 | end 226 | 227 | it 'should validate an empty number' do 228 | expect(@home).to be_valid 229 | end 230 | 231 | it 'should validate a valid number' do 232 | @home.phone_number = VALID_NUMBER 233 | expect(@home).to be_valid 234 | end 235 | 236 | it 'should validate a valid number with extension' do 237 | @home.phone_number = VALID_NUMBER_WITH_EXTENSION 238 | expect(@home).to be_valid 239 | end 240 | 241 | it 'should invalidate an invalid number' do 242 | @home.phone_number = INVALID_NUMBER 243 | expect(@home).to_not be_valid 244 | expect(@home.errors.messages.to_hash).to include(phone_number: ['is an invalid number']) 245 | end 246 | 247 | it 'should invalidate an valid number with invalid extension' do 248 | @home.phone_number = VALID_NUMBER_WITH_INVALID_EXTENSION 249 | expect(@home).to_not be_valid 250 | expect(@home.errors.messages.to_hash).to include(phone_number: ['is an invalid number']) 251 | end 252 | 253 | it 'should invalidate not a number' do 254 | @home.phone_number = NOT_A_NUMBER 255 | expect(@home).to_not be_valid 256 | expect(@home.errors.messages.to_hash).to include(phone_number: ['is an invalid number']) 257 | end 258 | 259 | it 'should translate the error message in Dutch' do 260 | I18n.with_locale(:nl) do 261 | @home.phone_number = INVALID_NUMBER 262 | @home.valid? 263 | expect(@home.errors.messages.to_hash).to include(phone_number: ['is geen geldig nummer']) 264 | end 265 | end 266 | 267 | it 'should translate the error message in English' do 268 | I18n.with_locale(:en) do 269 | @home.phone_number = INVALID_NUMBER 270 | @home.valid? 271 | expect(@home.errors.messages.to_hash).to include(phone_number: ['is an invalid number']) 272 | end 273 | end 274 | 275 | it 'should translate the error message in Spanish' do 276 | I18n.with_locale(:es) do 277 | @home.phone_number = INVALID_NUMBER 278 | @home.valid? 279 | expect(@home.errors.messages.to_hash).to include(phone_number: ['es un número inválido']) 280 | end 281 | end 282 | 283 | it 'should translate the error message in French' do 284 | I18n.with_locale(:fr) do 285 | @home.phone_number = INVALID_NUMBER 286 | @home.valid? 287 | expect(@home.errors.messages.to_hash).to include(phone_number: ['est un numéro invalide']) 288 | end 289 | end 290 | 291 | it 'should translate the error message in Japanese' do 292 | I18n.with_locale(:ja) do 293 | @home.phone_number = INVALID_NUMBER 294 | @home.valid? 295 | expect(@home.errors.messages.to_hash).to include(phone_number: ['は正しい電話番号ではありません']) 296 | end 297 | end 298 | 299 | it 'should translate the error message in Khmer' do 300 | I18n.with_locale(:km) do 301 | @home.phone_number = INVALID_NUMBER 302 | @home.valid? 303 | expect(@home.errors.messages.to_hash).to include(phone_number: ['គឺជាលេខមិនត្រឹមត្រូវ']) 304 | end 305 | end 306 | 307 | it 'should translate the error message in Korean' do 308 | I18n.with_locale(:ko) do 309 | @home.phone_number = INVALID_NUMBER 310 | @home.valid? 311 | expect(@home.errors.messages.to_hash).to include(phone_number: ['는 올바른 전화번호가 아닙니다']) 312 | end 313 | end 314 | 315 | it 'should translate the error message in Ukrainian' do 316 | I18n.with_locale(:uk) do 317 | @home.phone_number = INVALID_NUMBER 318 | @home.valid? 319 | expect(@home.errors.messages.to_hash).to include(phone_number: ['є недійсним номером']) 320 | end 321 | end 322 | 323 | it 'should translate the error message in Russian' do 324 | I18n.with_locale(:ru) do 325 | @home.phone_number = INVALID_NUMBER 326 | @home.valid? 327 | expect(@home.errors.messages.to_hash).to include(phone_number: ['является недействительным номером']) 328 | end 329 | end 330 | 331 | it 'should translate the error message in Portuguese' do 332 | I18n.with_locale(:pt) do 333 | @home.phone_number = INVALID_NUMBER 334 | @home.valid? 335 | expect(@home.errors.messages.to_hash).to include(phone_number: ['é um número inválido']) 336 | end 337 | end 338 | end 339 | end 340 | 341 | #----------------------------------------------------------------------------------------------------------------------- 342 | describe ActiveModel::Validations::HelperMethods do 343 | #-------------------- 344 | describe '#validates_plausible_phone' do 345 | #-------------------- 346 | context 'when a number is optional' do 347 | before(:each) do 348 | @home = HelpfulHome.new 349 | end 350 | 351 | it 'should validate an empty number' do 352 | expect(@home).to be_valid 353 | end 354 | 355 | it 'should validate a valid number' do 356 | @home.phone_number = VALID_NUMBER 357 | expect(@home).to be_valid 358 | end 359 | 360 | it 'should invalidate an invalid number' do 361 | @home.phone_number = INVALID_NUMBER 362 | expect(@home).to_not be_valid 363 | expect(@home.errors.messages.to_hash).to include(phone_number: ['is an invalid number']) 364 | end 365 | end 366 | 367 | #-------------------- 368 | context 'when a number is required (:presence = true)' do 369 | before(:each) do 370 | @home = RequiredHelpfulHome.new 371 | end 372 | 373 | it 'should invalidate an empty number' do 374 | expect(@home).to_not be_valid 375 | expect(@home.errors.messages.to_hash).to include(phone_number: ["can't be blank"]) 376 | end 377 | 378 | it 'should validate a valid number' do 379 | @home.phone_number = VALID_NUMBER 380 | expect(@home).to be_valid 381 | end 382 | 383 | it 'should invalidate an invalid number' do 384 | @home.phone_number = INVALID_NUMBER 385 | expect(@home).to_not be_valid 386 | expect(@home.errors.messages.to_hash).to include(phone_number: ['is an invalid number']) 387 | end 388 | end 389 | 390 | #-------------------- 391 | context 'when a number is not required (!presence = false)' do 392 | before(:each) do 393 | @home = OptionalHelpfulHome.new 394 | end 395 | 396 | it 'should validate an nil number' do 397 | @home.phone_number = nil 398 | expect(@home).to be_valid 399 | end 400 | 401 | it 'should validate an empty number' do 402 | @home.phone_number = '' 403 | expect(@home).to be_valid 404 | end 405 | 406 | it 'should validate a valid number' do 407 | @home.phone_number = VALID_NUMBER 408 | expect(@home).to be_valid 409 | end 410 | 411 | it 'should invalidate an invalid number' do 412 | @home.phone_number = INVALID_NUMBER 413 | expect(@home).to_not be_valid 414 | expect(@home.errors.messages.to_hash).to include(phone_number: ['is an invalid number']) 415 | end 416 | end 417 | 418 | #-------------------- 419 | context 'when a number must be formatted (:with)' do 420 | before(:each) do 421 | @home = FormattedHelpfulHome.new 422 | end 423 | 424 | it 'should invalidate an empty number' do 425 | expect(@home).to_not be_valid 426 | expect(@home.errors.messages.to_hash).to include(phone_number: ['is invalid']) 427 | end 428 | 429 | it 'should validate a well formatted valid number' do 430 | @home.phone_number = "+#{VALID_NUMBER}" 431 | expect(@home).to be_valid 432 | end 433 | 434 | it 'should invalidate a bad formatted valid number' do 435 | @home.phone_number = VALID_NUMBER 436 | expect(@home).to_not be_valid 437 | expect(@home.errors.messages.to_hash).to include(phone_number: ['is invalid']) 438 | end 439 | end 440 | 441 | #-------------------- 442 | context 'when a number must not be formatted (:without)' do 443 | before(:each) do 444 | @home = NotFormattedHelpfulHome.new 445 | end 446 | 447 | it 'should validate an empty number' do 448 | expect(@home).to be_valid 449 | end 450 | 451 | it 'should validate a well formatted valid number' do 452 | @home.phone_number = VALID_NUMBER 453 | expect(@home).to be_valid 454 | end 455 | 456 | it 'should invalidate a bad formatted valid number' do 457 | @home.phone_number = "+#{VALID_NUMBER}" 458 | expect(@home).to_not be_valid 459 | expect(@home.errors.messages.to_hash).to include(phone_number: ['is invalid']) 460 | end 461 | end 462 | 463 | #-------------------- 464 | context 'when a number must include a specific country number' do 465 | before(:each) do 466 | @home = AustralianHelpfulHome.new 467 | end 468 | 469 | it 'should validate an empty number' do 470 | expect(@home).to be_valid 471 | end 472 | 473 | it 'should validate a valid number with the right country code' do 474 | @home.phone_number = AUSTRALIAN_NUMBER_WITH_COUNTRY_CODE 475 | expect(@home).to be_valid 476 | end 477 | 478 | it 'should invalidate a valid number with the wrong country code' do 479 | @home.phone_number = FRENCH_NUMBER_WITH_COUNTRY_CODE 480 | expect(@home).to_not be_valid 481 | expect(@home.errors.messages.to_hash).to include(phone_number: ['is an invalid number']) 482 | end 483 | 484 | it 'should invalidate a valid number without a country code' do 485 | @home.phone_number = VALID_NUMBER 486 | expect(@home).to_not be_valid 487 | expect(@home.errors.messages.to_hash).to include(phone_number: ['is an invalid number']) 488 | end 489 | end 490 | 491 | #-------------------- 492 | context 'when a number must be validated after normalization' do 493 | before(:each) do 494 | @home = NormalizableHelpfulHome.new 495 | end 496 | 497 | it 'should validate an empty number' do 498 | expect(@home).to be_valid 499 | end 500 | 501 | it 'should validate a valid number' do 502 | @home.phone_number = VALID_NUMBER 503 | expect(@home).to be_valid 504 | end 505 | 506 | it 'should validate a normalizable number' do 507 | @home.phone_number = NORMALIZABLE_NUMBER 508 | expect(@home).to be_valid 509 | end 510 | 511 | it 'should invalidate an invalid number' do 512 | @home.phone_number = INVALID_NUMBER 513 | expect(@home).to_not be_valid 514 | expect(@home.errors.messages.to_hash).to include(phone_number: ['is an invalid number']) 515 | end 516 | end 517 | 518 | #-------------------- 519 | context 'when a number must include a specific country code' do 520 | before(:each) do 521 | @home = PolishHelpfulHome.new 522 | end 523 | 524 | it 'should validate an empty number' do 525 | expect(@home).to be_valid 526 | end 527 | 528 | it 'should validate a valid number with the right country code' do 529 | @home.phone_number = POLISH_NUMBER_WITH_COUNTRY_CODE 530 | expect(@home).to be_valid 531 | end 532 | 533 | it 'should invalidate a valid number with the wrong country code' do 534 | @home.phone_number = FRENCH_NUMBER_WITH_COUNTRY_CODE 535 | expect(@home).to_not be_valid 536 | expect(@home.errors.messages.to_hash).to include(phone_number: ['is an invalid number']) 537 | end 538 | 539 | it 'should invalidate a valid number without a country code' do 540 | @home.phone_number = VALID_NUMBER 541 | expect(@home).to_not be_valid 542 | expect(@home.errors.messages.to_hash).to include(phone_number: ['is an invalid number']) 543 | end 544 | end 545 | 546 | context 'when lots of things are being validated simultaneously' do 547 | before(:each) do 548 | @home = BigHelpfulHome.new 549 | end 550 | 551 | it 'should invalidate an empty number' do 552 | expect(@home).to_not be_valid 553 | end 554 | 555 | it 'should invalidate an invalid number' do 556 | @home.phone_number = INVALID_NUMBER 557 | expect(@home).to_not be_valid 558 | expect(@home.errors.messages[:phone_number]).to include 'is an invalid number' 559 | end 560 | 561 | it 'should invalidate a badly formatted number with the right country code' do 562 | @home.phone_number = FRENCH_NUMBER_WITH_COUNTRY_CODE 563 | expect(@home).to_not be_valid 564 | expect(@home.errors.messages[:phone_number]).to include 'is invalid' 565 | end 566 | 567 | it 'should invalidate a properly formatted number with the wrong country code' do 568 | @home.phone_number = FORMATTED_AUSTRALIAN_NUMBER_WITH_COUNTRY_CODE 569 | expect(@home).to_not be_valid 570 | expect(@home.errors.messages[:phone_number]).to include 'is an invalid number' 571 | end 572 | 573 | it 'should validate a properly formatted number with the right country code' do 574 | @home.phone_number = FORMATTED_FRENCH_NUMBER_WITH_COUNTRY_CODE 575 | expect(@home).to be_valid 576 | end 577 | end 578 | 579 | #-------------------- 580 | context 'when a phone number does not match the records country' do 581 | before(:each) do 582 | @home = MismatchedHelpfulHome.new 583 | @home.country_code = JAPAN_COUNTRY 584 | @home.phone_number = FRENCH_NUMBER_WITH_COUNTRY_CODE 585 | end 586 | 587 | it 'should allow this number' do 588 | expect(@home).to be_valid 589 | end 590 | end 591 | 592 | #-------------------- 593 | context 'when a country code is set to something invalid' do 594 | before(:each) do 595 | @home = InvalidCountryCodeHelpfulHome.new 596 | end 597 | 598 | it 'should allow any valid number' do 599 | @home.phone_number = FRENCH_NUMBER_WITH_COUNTRY_CODE 600 | expect(@home).to be_valid 601 | end 602 | 603 | it 'should not allow any invalid number' do 604 | @home.phone_number = INVALID_NUMBER 605 | expect(@home).to_not be_valid 606 | end 607 | 608 | it 'should not raise a NoMethodError when looking up a country fails (Regression)' do 609 | expect do 610 | @home.valid? 611 | end.to_not raise_error 612 | end 613 | end 614 | 615 | #-------------------- 616 | context 'when a country code is passed as a symbol' do 617 | before(:each) do 618 | @home = SymbolizableHelpfulHome.new 619 | end 620 | 621 | it 'should validate an empty number' do 622 | expect(@home).to be_valid 623 | end 624 | 625 | it 'should validate a valid number with the right country code' do 626 | @home.phone_number = POLISH_NUMBER_WITH_COUNTRY_CODE 627 | @home.phone_number_country_code = 'PL' 628 | expect(@home).to be_valid 629 | end 630 | 631 | it 'should invalidate a valid number with the wrong country code' do 632 | @home.phone_number = FRENCH_NUMBER_WITH_COUNTRY_CODE 633 | @home.phone_number_country_code = 'PL' 634 | expect(@home).to_not be_valid 635 | expect(@home.errors.messages.to_hash).to include(phone_number: ['is an invalid number']) 636 | end 637 | 638 | it 'should invalidate a valid number without a country code' do 639 | @home.phone_number = VALID_NUMBER 640 | @home.phone_number_country_code = 'PL' 641 | expect(@home).to_not be_valid 642 | expect(@home.errors.messages.to_hash).to include(phone_number: ['is an invalid number']) 643 | end 644 | 645 | it 'should pass Gitlab issue #165' do 646 | @home.phone_number = CROATIA_NUMBER_WITH_COUNTRY_CODE 647 | @home.phone_number_country_code = 'HR' 648 | expect(@home).to be_valid 649 | end 650 | end 651 | 652 | #-------------------- 653 | context 'when a nonexistent method is passed as a symbol to an option other than message' do 654 | it 'raises NoMethodError' do 655 | @home = NoModelMethod.new 656 | @home.phone_number = FRENCH_NUMBER_WITH_COUNTRY_CODE 657 | 658 | expect { @home.save }.to raise_error(NoMethodError) 659 | end 660 | end 661 | 662 | #-------------------- 663 | context 'when a nonexistent method is passed as a symbol to the message option' do 664 | it 'does not raise an error' do 665 | @home = MessageOptionUndefinedInModel.new 666 | @home.phone_number = INVALID_NUMBER 667 | 668 | expect { @home.save }.to_not raise_error 669 | end 670 | end 671 | 672 | #-------------------- 673 | context 'when an existing Model method is passed as a symbol to the message option' do 674 | it 'does not use the Model method' do 675 | @home = MessageOptionSameAsModelMethod.new 676 | @home.phone_number = INVALID_NUMBER 677 | 678 | expect(@home).to_not receive(:email) 679 | 680 | @home.save 681 | end 682 | end 683 | 684 | context 'when a number has already code_number' do 685 | it 'does not normalize code after validation' do 686 | @home = NormalizabledPhoneHome.new 687 | @home.phone_number = '+44 799 449 595' 688 | @home.country_code = 'PL' 689 | 690 | expect(@home).to_not be_valid 691 | expect(@home.phone_number).to eql('+44 799 449 595') 692 | 693 | @home.phone_number = '+48 799 449 595' 694 | 695 | expect(@home).to be_valid 696 | expect(@home.phone_number).to eql('+48799449595') 697 | end 698 | 699 | it 'does not normalize code after validation with multiple attributes' do 700 | @home = NormalizabledPhoneHome.new 701 | @home.phone_number = '+44 799 449 595' 702 | @home.phone_number2 = '+44 222 111 333' 703 | @home.country_code = 'PL' 704 | 705 | expect(@home).to_not be_valid 706 | expect(@home.phone_number).to eql('+44 799 449 595') 707 | expect(@home.phone_number2).to eql('+44 222 111 333') 708 | 709 | @home.phone_number = '+48 799 449 595' 710 | @home.phone_number2 = '+48 222 111 333' 711 | expect(@home).to be_valid 712 | expect(@home.phone_number).to eql('+48799449595') 713 | expect(@home.phone_number2).to eql('+48222111333') 714 | end 715 | end 716 | end 717 | end 718 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'coveralls' 4 | Coveralls.wear! 5 | 6 | # Own code here. 7 | 8 | require 'rubygems' 9 | require 'bundler/setup' 10 | 11 | require 'active_record' 12 | # require 'mongoid' 13 | require 'phony_rails' 14 | 15 | ActiveRecord::Base.establish_connection( 16 | adapter: 'sqlite3', 17 | database: ':memory:' 18 | ) 19 | 20 | ActiveRecord::Schema.define do 21 | create_table :active_record_models do |table| 22 | table.column :phone_attribute, :string 23 | table.column :phone_number, :string 24 | table.column :phone_number_as_normalized, :string 25 | table.column :fax_number, :string 26 | table.column :country_code_attribute, :string 27 | table.column :symboled_phone, :string 28 | end 29 | end 30 | 31 | module SharedModelMethods 32 | extend ActiveSupport::Concern 33 | included do 34 | attr_accessor( 35 | :country_code, :country_code_attribute, :custom_country_code, :delivery_method, 36 | :home_country, :phone_method, :phone1_method, :recipient, :symboled_phone_method 37 | ) 38 | 39 | phony_normalized_method :phone_attribute # adds normalized_phone_attribute method 40 | phony_normalized_method :phone_method # adds normalized_phone_method method 41 | phony_normalized_method :phone1_method, default_country_code: 'DE' # adds normalized_phone_method method 42 | phony_normalized_method :symboled_phone_method, country_code: :country_code_attribute # adds phone_with_symboled_options method 43 | phony_normalize :phone_number # normalized on validation 44 | phony_normalize :fax_number, default_country_code: 'AU' 45 | phony_normalize :symboled_phone, default_country_code: :country_code_attribute 46 | 47 | def use_phone? 48 | delivery_method == 'sms' 49 | end 50 | 51 | def use_email? 52 | delivery_method == 'email' 53 | end 54 | end 55 | end 56 | 57 | class ActiveRecordModel < ActiveRecord::Base 58 | include SharedModelMethods 59 | end 60 | 61 | class RelaxedActiveRecordModel < ActiveRecord::Base 62 | self.table_name = 'active_record_models' 63 | attr_accessor :phone_number, :country_code 64 | 65 | phony_normalize :phone_number, enforce_record_country: false 66 | end 67 | 68 | class ActiveRecordDummy < ActiveRecordModel 69 | end 70 | 71 | # In case you don't want a database for your model 72 | class ActiveModelModel 73 | include ActiveModel::Model # this provides most of the interface of AR 74 | include ActiveModel::Validations::Callbacks # we use callbacks for normalization 75 | include SharedModelMethods 76 | 77 | # database columns don't give us free attributes, we have to define them 78 | attr_accessor :phone_number, :phone_attribute, :phone_number_as_normalized, :country_code_attribute, :fax_number, :symboled_phone 79 | end 80 | 81 | class ActiveModelDummy < ActiveModelModel 82 | end 83 | 84 | # class MongoidModel 85 | # include Mongoid::Document 86 | # include Mongoid::Phony 87 | # field :phone_attribute, type: String 88 | # field :phone_number, type: String 89 | # field :phone_number_as_normalized, type: String 90 | # field :fax_number 91 | # field :country_code_attribute, type: String 92 | # field :symboled_phone, type: String 93 | # include SharedModelMethods 94 | # end 95 | 96 | # class MongoidDummy < MongoidModel 97 | # end 98 | 99 | I18n.config.enforce_available_locales = true 100 | 101 | # RSpec.configure do |config| 102 | # # some (optional) config here 103 | # end 104 | --------------------------------------------------------------------------------