├── .circleci └── config.yml ├── .gitignore ├── .rbenv-gemsets ├── Appraisals ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── app ├── assets │ ├── config │ │ └── manifest.js │ └── javascripts │ │ └── judge.js └── controllers │ └── judge │ └── validations_controller.rb ├── config └── routes.rb ├── gemfiles ├── rails_5_0.gemfile ├── rails_5_1.gemfile └── rails_5_2.gemfile ├── judge.gemspec ├── lib ├── generators │ └── judge │ │ └── install │ │ └── install_generator.rb ├── judge.rb ├── judge │ ├── config.rb │ ├── confirmation_validator.rb │ ├── controller.rb │ ├── each_validator.rb │ ├── engine.rb │ ├── form_builder.rb │ ├── html.rb │ ├── message_collection.rb │ ├── message_config.rb │ ├── validation.rb │ ├── validator.rb │ ├── validator_collection.rb │ └── version.rb └── tasks │ ├── js.rake │ └── judge_tasks.rake ├── package.json ├── script └── rails ├── spec ├── config_spec.rb ├── confirmation_validator_spec.rb ├── controllers │ └── judge │ │ └── validations_controller_spec.rb ├── dummy │ ├── README.rdoc │ ├── Rakefile │ ├── app │ │ ├── assets │ │ │ ├── config │ │ │ │ └── manifest.js │ │ │ ├── javascripts │ │ │ │ └── application.js │ │ │ └── stylesheets │ │ │ │ └── application.css │ │ ├── controllers │ │ │ └── application_controller.rb │ │ ├── helpers │ │ │ └── application_helper.rb │ │ ├── mailers │ │ │ └── .gitkeep │ │ ├── models │ │ │ ├── .gitkeep │ │ │ ├── category.rb │ │ │ ├── discipline.rb │ │ │ ├── sport.rb │ │ │ ├── team.rb │ │ │ └── user.rb │ │ ├── validators │ │ │ └── city_validator.rb │ │ └── views │ │ │ └── layouts │ │ │ └── application.html.erb │ ├── config.ru │ ├── config │ │ ├── application.rb │ │ ├── boot.rb │ │ ├── database.yml │ │ ├── environment.rb │ │ ├── environments │ │ │ ├── development.rb │ │ │ ├── production.rb │ │ │ └── test.rb │ │ ├── initializers │ │ │ ├── backtrace_silencers.rb │ │ │ ├── inflections.rb │ │ │ ├── mime_types.rb │ │ │ ├── secret_token.rb │ │ │ ├── session_store.rb │ │ │ └── wrap_parameters.rb │ │ ├── locales │ │ │ └── en.yml │ │ └── routes.rb │ ├── db │ │ ├── migrate │ │ │ ├── 20120426221242_create_users.rb │ │ │ ├── 20120426221441_create_teams.rb │ │ │ ├── 20120426221448_create_categories.rb │ │ │ ├── 20120426221455_create_sports.rb │ │ │ └── 20120426221506_create_disciplines.rb │ │ └── schema.rb │ ├── lib │ │ └── assets │ │ │ └── .gitkeep │ ├── log │ │ └── .gitkeep │ ├── public │ │ ├── 404.html │ │ ├── 422.html │ │ ├── 500.html │ │ └── favicon.ico │ └── script │ │ └── rails ├── each_validator_spec.rb ├── factories │ └── factories.rb ├── form_builder_spec.rb ├── javascripts │ ├── helpers │ │ ├── customMatchers.js │ │ ├── jasmine-jquery.js │ │ ├── jasmine.js │ │ ├── jquery.js │ │ ├── sinon-ie.js │ │ ├── sinon.js │ │ └── tap-reporter.js │ ├── index.html │ ├── javascript_spec_server.rb │ ├── judge-spec.js │ └── run.js ├── message_collection_spec.rb ├── models │ └── validation_spec.rb ├── routing │ └── engine_routing_spec.rb ├── spec_helper.rb ├── validator_collection_spec.rb └── validator_spec.rb └── vendor └── assets └── javascripts ├── json2.js └── underscore.js /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2.1 3 | jobs: 4 | build: 5 | working_directory: ~/judge 6 | docker: 7 | - image: cimg/ruby:2.7 8 | environment: 9 | RAILS_ENV: test 10 | - image: keinos/sqlite3:3.40.1 11 | 12 | steps: 13 | - checkout 14 | 15 | - run: bundle install 16 | - run: bundle exec appraisal install 17 | 18 | - run: bundle exec appraisal rake db:create 19 | - run: bundle exec appraisal rake db:migrate 20 | 21 | - run: bundle exec appraisal rspec 22 | 23 | 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | Gemfile.lock 4 | pkg/* 5 | docs 6 | doc 7 | rdoc 8 | .rvmrc 9 | .ruby-version 10 | .ruby-gemset 11 | .rspec 12 | spec/dummy/tmp/**/* 13 | spec/dummy/log/**/* 14 | *.sqlite3 15 | .rspec 16 | gemfiles/*.lock 17 | .rbenv-gemsets -------------------------------------------------------------------------------- /.rbenv-gemsets: -------------------------------------------------------------------------------- 1 | judge 2 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | appraise 'rails_5_0' do 2 | gem 'rails', '~> 5.0' 3 | end 4 | 5 | appraise 'rails_5_1' do 6 | gem 'rails', '~> 5.1' 7 | end 8 | 9 | appraise 'rails_5_2' do 10 | gem 'rails', '~> 5.2' 11 | end 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | * **3.1.0** 4 | - [#96](https://github.com/judgegem/judge/pull/96): Allows more flexible validation definitions for foreign keys 5 | - [#98](https://github.com/judgegem/judge/pull/98): Updates to CircleCI 6 | * **3.0.0** 7 | - Updates Rails support to 5.0+ and Ruby support to 2.2+ 8 | - [#71](https://github.com/joecorcoran/judge/pull/71): Add Code of Conduct from contributor-covenant.org 9 | - [#72](https://github.com/joecorcoran/judge/pull/72): Add `telephone_field` support 10 | - [#75](https://github.com/joecorcoran/judge/pull/75): Additional documentation for error messages 11 | - [#78](https://github.com/joecorcoran/judge/pull/78): Fix uniqueness validation after source code changes 12 | - [#83](https://github.com/joecorcoran/judge/pull/83): Remove `ActiveModel::Errors#get` deprecation for Rails 5.1 13 | * **2.1.1** 14 | - [#63](https://github.com/joecorcoran/judge/pull/63): add array length validation 15 | - [#68](https://github.com/joecorcoran/judge/pull/68): count newlines as two characters when validating length 16 | * **2.1.0** 17 | - [#55](https://github.com/joecorcoran/judge/pull/55): move confirmation validator to comfirmation field 18 | - [#56](https://github.com/joecorcoran/judge/pull/56): add `email_field` support 19 | - [#58](https://github.com/joecorcoran/judge/pull/58): add presence validation support to radio buttons 20 | - [#60](https://github.com/joecorcoran/judge/pull/60): support Ruby-style regex in JavaScript 21 | * **2.0.6** 22 | - [#46](https://github.com/joecorcoran/judge/pull/46): remove Responders 23 | - [#49](https://github.com/joecorcoran/judge/pull/49): remove status check from tryClose 24 | - [#48](https://github.com/joecorcoran/judge/pull/48): added original value to uniqueness validations 25 | - [#51](https://github.com/joecorcoran/judge/pull/51): Seperate required parameters from conditional parameters 26 | - [#52](https://github.com/joecorcoran/judge/pull/52): Fix nested model uniqueness validation 27 | * **2.0.5** 28 | * **2.0.4** Better empty param handling and safer loading of engine files. 29 | * **2.0.3** Fixed Internet Explorer XHR bug. 30 | * **2.0.2** Fixed controller inheritance bug. 31 | * **2.0.1** Fixed URI encoding bug. 32 | * **2.0.0** Breaking changes. Event/queueing in the front end; uniqueness and other XHR validations. 33 | * **1.5.0** Added interface for declaring localised messages within EachValidators, which means we can reliably pass custom messages to the client side. Some internal implementation details have been altered and underscore.js was updated. 34 | * **1.4.0** judge.store.validate now accepts callback function too. 35 | * **1.3.0** Validate methods now accept callbacks; judge.utils removed in favour of functions within a closure; some specs were tidied/deleted as appropriate. 36 | * **1.2.0** Changes to the format of validator functions; improved method of including custom validators; updated dependencies; fixed RegExp flag bug. 37 | * **1.1.0** Fixed incorrect Enumerable implementation in ValidatorCollection. Lots more internal tidying, including extraction of HTML attribute building. 38 | * **1.0.0** Validator code extracted into classes; Form builder methods no longer require “validated_” prefix; Added judge.store.validate() shortcut method; Some minor implementation updates to judge.js; No more dummy app in tests, no more crappy Gemfile, no more Jeweler. 39 | * **0.5.0** Error messages looked up through Rails i**18**n. 40 | * **0.4.3** IE bug fixes: No longer checking for element type in the Watcher constructor, to avoid IE object string "quirk"; now using typeof to check for undefined properties of window. 41 | * **0.4.2** Fixed bug introduced in the last bug fix – now globally replacing double slashes :) 42 | * **0.4.1** Fixed slash escaping bug in judge.js RegExp converter; removed uniqueness data from data attributes to prevent judge.js from expecting a validation method. 43 | * **0.4.0** Added new form builders. 44 | * **0.3.1** Removed unused keys from validator options in data attributes. Include Judge::FormHelper in ApplicationHelper to avoid clash with haml aliases. 45 | * **0.3.0** Added confirmation and acceptance validation, more form builders. 46 | * **0.2.0** Remove jQuery dependency; refactored to include new namespace, watchers and store; added headless testing. 47 | * **0.1.1** Removed duplicate dependencies in gemspec. 48 | * **0.1.0** First release. -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, and in the interest of 4 | fostering an open and welcoming community, we pledge to respect all people who 5 | contribute through reporting issues, posting feature requests, updating 6 | documentation, submitting pull requests or patches, and other activities. 7 | 8 | We are committed to making participation in this project a harassment-free 9 | experience for everyone, regardless of level of experience, gender, gender 10 | identity and expression, sexual orientation, disability, personal appearance, 11 | body size, race, ethnicity, age, religion, or nationality. 12 | 13 | Examples of unacceptable behavior by participants include: 14 | 15 | * The use of sexualized language or imagery 16 | * Personal attacks 17 | * Trolling or insulting/derogatory comments 18 | * Public or private harassment 19 | * Publishing other's private information, such as physical or electronic 20 | addresses, without explicit permission 21 | * Other unethical or unprofessional conduct 22 | 23 | Project maintainers have the right and responsibility to remove, edit, or 24 | reject comments, commits, code, wiki edits, issues, and other contributions 25 | that are not aligned to this Code of Conduct, or to ban temporarily or 26 | permanently any contributor for other behaviors that they deem inappropriate, 27 | threatening, offensive, or harmful. 28 | 29 | By adopting this Code of Conduct, project maintainers commit themselves to 30 | fairly and consistently applying these principles to every aspect of managing 31 | this project. Project maintainers who do not follow or enforce the Code of 32 | Conduct may be permanently removed from the project team. 33 | 34 | This Code of Conduct applies both within project spaces and in public spaces 35 | when an individual is representing the project or its community. 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 38 | reported by contacting a project maintainer at joe@corcoran.io. All 39 | complaints will be reviewed and investigated and will result in a response that 40 | is deemed necessary and appropriate to the circumstances. Maintainers are 41 | obligated to maintain confidentiality with regard to the reporter of an 42 | incident. 43 | 44 | 45 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 46 | version 1.3.0, available at 47 | [http://contributor-covenant.org/version/1/3/0/][version] 48 | 49 | [homepage]: http://contributor-covenant.org 50 | [version]: http://contributor-covenant.org/version/1/3/0/ 51 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Joe Corcoran 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Judge](http://rubygems.org/gems/judge) 2 | 3 | [![Build status](https://secure.travis-ci.org/joecorcoran/judge.png?branch=master)](http://travis-ci.org/joecorcoran/judge) 4 | 5 | Judge allows easy client side form validation for Rails by porting many `ActiveModel::Validation` features to JavaScript. The most common validations work through JSON strings stored within HTML5 data attributes and are executed purely on the client side. Wherever you need to, Judge provides a simple interface for AJAX validations too. 6 | 7 | ## Rationale 8 | 9 | Whenever we need to give the user instant feedback on their form data, it's common to write some JavaScript to test form element values. Since whatever code we write to manage our data integrity in Ruby has to be copied as closely as possible in JavaScript, we end up with some very unsatisfying duplication of application logic. 10 | 11 | In many cases it would be simpler to safely expose the validation information from our models to the client – this is where Judge steps in. 12 | 13 | ## Installation 14 | 15 | Judge supports Rails 5.0+. 16 | If you require Rails 4 support, please use version 2.1.x. 17 | 18 | Judge relies on [Underscore.js](underscore) in general and [JSON2.js](json2) for browsers that lack proper JSON support. If your application already makes use of these files, you can safely ignore the versions provided with Judge. 19 | 20 | ### With asset pipeline enabled 21 | 22 | Add `judge` to your Gemfile and run `bundle install`. 23 | 24 | Mount the engine in your routes file, as follows: 25 | 26 | ```ruby 27 | # config/routes.rb 28 | mount Judge::Engine => '/judge' 29 | ``` 30 | 31 | Judge makes three JavaScript files available. You'll always need *judge.js* and *underscore.js*, whereas *json2.js* is only needed in older browsers. Add the following lines to *application.js*: 32 | 33 | ``` 34 | //= require underscore 35 | //= require json2 36 | //= require judge 37 | ``` 38 | 39 | ### Without asset pipeline 40 | 41 | Add `judge` to your Gemfile and run `bundle install`. Then run 42 | 43 | ```bash 44 | $ rails generate judge:install path/to/your/js/dir 45 | ``` 46 | 47 | to copy *judge.js* to your application. There are **--json2** and **--underscore** options to copy the dependencies too. 48 | 49 | Mount the engine in your routes file, as follows: 50 | 51 | ```ruby 52 | # config/routes.rb 53 | mount Judge::Engine => '/judge' 54 | ``` 55 | 56 | ## Getting started 57 | 58 | Add a simple validation to your model. 59 | 60 | ```ruby 61 | class Post < ActiveRecord::Base 62 | validates :title, :presence => true 63 | end 64 | ``` 65 | 66 | Make sure your form uses the Judge::FormBuilder and add the :validate option to the field. 67 | 68 | ```ruby 69 | form_for(@post, :builder => Judge::FormBuilder) do |f| 70 | f.text_field :title, :validate => true 71 | end 72 | ``` 73 | 74 | On the client side, you can now validate the title input. 75 | 76 | ```javascript 77 | judge.validate(document.getElementById('post_title'), { 78 | valid: function(element) { 79 | element.style.border = '1px solid green'; 80 | }, 81 | invalid: function(element, messages) { 82 | element.style.border = '1px solid red'; 83 | alert(messages.join(',')); 84 | } 85 | }); 86 | ``` 87 | 88 | ## Judge::FormBuilder 89 | 90 | You can use any of the methods from the standard ActionView::Helpers::FormBuilder – just add `:validate => true` to the options hash. 91 | 92 | ```ruby 93 | f.date_select :birthday, :validate => true 94 | ``` 95 | 96 | If you need to use Judge in conjunction with your own custom `FormBuilder` methods, make sure your `FormBuilder` inherits from `Judge::FormBuilder` and use the `#add_validate_attr!` helper. 97 | 98 | ```ruby 99 | class MyFormBuilder < Judge::FormBuilder 100 | def fancy_text_field(method, options = {}) 101 | add_validate_attr!(self.object, method, options) 102 | # do your stuff here 103 | end 104 | end 105 | ``` 106 | 107 | ## Available validators 108 | 109 | * presence; 110 | * length (options: *minimum*, *maximum*, *is*); 111 | * exclusion (options: *in*); 112 | * inclusion (options: *in*); 113 | * format (options: *with*, *without*); and 114 | * numericality (options: *greater_than*, *greater_than_or_equal_to*, *less_than*, *less_than_or_equal_to*, *equal_to*, *odd*, *even*, *only_integer*); 115 | * acceptance; 116 | * confirmation (input and confirmation input must have matching ids); 117 | * uniqueness; 118 | * any `EachValidator` that you have written, provided you add a JavaScript version too and add it to `judge.eachValidators`. 119 | 120 | The *allow_blank* option is available everywhere it should be. 121 | 122 | ### Error messages 123 | Error messages are looked up according to the [Rails i18n API](http://guides.rubyonrails.org/i18n.html#error-message-interpolation). 124 | 125 | To override error messages, add entries to `config/locales/en.yml` for the messages you'd like to customize. Here's an example with Rails default values: 126 | ``` 127 | en: 128 | errors: 129 | format: "%{attribute} %{message}" 130 | messages: 131 | accepted: must be accepted 132 | blank: can't be blank 133 | present: must be blank 134 | confirmation: doesn't match %{attribute} 135 | empty: can't be empty 136 | equal_to: must be equal to %{count} 137 | even: must be even 138 | exclusion: is reserved 139 | greater_than: must be greater than %{count} 140 | greater_than_or_equal_to: must be greater than or equal to %{count} 141 | inclusion: is not included in the list 142 | invalid: is invalid 143 | less_than: must be less than %{count} 144 | less_than_or_equal_to: must be less than or equal to %{count} 145 | model_invalid: "Validation failed: %{errors}" 146 | not_a_number: is not a number 147 | not_an_integer: must be an integer 148 | odd: must be odd 149 | required: must exist 150 | taken: has already been taken 151 | too_long: 152 | one: is too long (maximum is 1 character) 153 | other: is too long (maximum is %{count} characters) 154 | too_short: 155 | one: is too short (minimum is 1 character) 156 | other: is too short (minimum is %{count} characters) 157 | wrong_length: 158 | one: is the wrong length (should be 1 character) 159 | other: is the wrong length (should be %{count} characters) 160 | other_than: must be other than %{count} 161 | ``` 162 | 163 | ## Validating uniqueness 164 | 165 | In order to validate uniqueness Judge sends requests to the mounted `Judge::Engine` path, which responds with a JSON representation of an error message array. The array is empty if the value is valid. 166 | 167 | Since this effectively means adding an open, queryable endpoint to your application, Judge is cautious and requires you to be explicit about which attributes from which models you would like to expose for validation via XHR. Allowed attributes are configurable as in the following example. Note that you are only required to do this for `uniqueness` and any other validators you write that make requests to the server. 168 | 169 | ```ruby 170 | # config/initializers/judge.rb 171 | Judge.configure do 172 | expose Post, :title, :body 173 | end 174 | ``` 175 | 176 | ## Mounting the engine at a different location 177 | 178 | You can choose a path other than `'/judge'` if you need to; just make sure to set this on the client side too: 179 | 180 | ```ruby 181 | # config/routes.rb 182 | mount Judge::Engine => '/whatever' 183 | ``` 184 | 185 | ```javascript 186 | judge.enginePath = '/whatever'; 187 | ``` 188 | 189 | ## Unsupported validator options 190 | 191 | The `:tokenizer` option is not currently supported. 192 | 193 | Options like `:if`, `:unless` and `:on` are not relevant to Judge. They are reliant on areas of your application that Judge does not expose on the client side. 194 | 195 | By default, Judge drops these options on the client side. This seems to work well for the common case, but if you want to ignore validators with unsupported options at global level, do the following in your config. 196 | 197 | ```ruby 198 | Judge.configure do 199 | ignore_unsupported_validators true 200 | end 201 | ``` 202 | 203 | You can set this behaviour at the validator level too. In your model, use the `:judge` option. 204 | 205 | ```ruby 206 | validates :foo, :presence => { :judge => :ignore } 207 | ``` 208 | 209 | If you've set unsupported validators to be ignored globally, you can still turn them back on at the validator level. 210 | 211 | ```ruby 212 | validates :foo, :presence => { :judge => :force } 213 | ``` 214 | 215 | ## Writing your own `EachValidator` 216 | 217 | If you write your own `ActiveModel::EachValidator`, Judge provides a way to ensure that your I18n error messages are available on the client side. Simply pass to `uses_messages` any number of message keys and Judge will look up the translated messages. Let's run through an example. 218 | 219 | ```ruby 220 | class FooValidator < ActiveModel::EachValidator 221 | uses_messages :not_foo 222 | 223 | def validate_each(record, attribute, value) 224 | unless value == 'foo' 225 | record.errors.add(:title, :not_foo) 226 | end 227 | end 228 | end 229 | ``` 230 | 231 | We'll use the validator in the example above to validate the title attribute of a Post object: 232 | 233 | ```ruby 234 | class Post < ActiveRecord::Base 235 | validates :title, :foo => true 236 | end 237 | ``` 238 | 239 | ```ruby 240 | form_for(@post, :builder => Judge::FormBuilder) do |f| 241 | f.text_field :title, :validate => true 242 | end 243 | ``` 244 | 245 | Judge will look for the `not_foo` message at 246 | `activerecord.errors.models.post.attributes.title.not_foo` 247 | first and then onwards down the [Rails I18n lookup chain](http://guides.rubyonrails.org/i18n.html#translations-for-active-record-models). 248 | 249 | We then need to add our own validator method to the `judge.eachValidators` object on the client side: 250 | 251 | ```javascript 252 | judge.eachValidators.foo = function(options, messages) { 253 | var errorMessages = []; 254 | // 'this' refers to the form element 255 | if (this.value !== 'foo') { 256 | errorMessages.push(messages.not_foo); 257 | } 258 | return new judge.Validation(errorMessages); 259 | }; 260 | ``` 261 | 262 | ## `judge.Validation` 263 | 264 | All client side validators must return a `Validation` – an object that can exist in three different states: *valid*, *invalid* or *pending*. If your validator function is synchronous, you can return a closed `Validation` simply by passing an array of error messages to the constructor. 265 | 266 | ```javascript 267 | new judge.Validation([]); 268 | // => empty array, this Validation is 'valid' 269 | new judge.Validation(['must not be blank']); 270 | // => array has messages, this Validation is 'invalid' 271 | ``` 272 | 273 | The *pending* state is provided for asynchronous validation; a `Validation` object we will close some time in the future. Let's look at an example, using jQuery's popular `ajax` function: 274 | 275 | ```javascript 276 | judge.eachValidators.bar = function() { 277 | // create a 'pending' validation 278 | var validation = new judge.Validation(); 279 | $.ajax('/bar-checking-service').done(function(messages) { 280 | // You can close a Validation with either an array 281 | // or a string that represents a JSON array 282 | validation.close(messages); 283 | }); 284 | return validation; 285 | }; 286 | ``` 287 | 288 | There are helper functions, `judge.pending()` and `judge.closed()` for creating a new `Validation` too. 289 | 290 | ```javascript 291 | judge.eachValidators.bar = function() { 292 | return judge.closed(['not valid']); 293 | }; 294 | 295 | judge.eachValidators.bar = function() { 296 | var validation = new judge.pending(); 297 | doAsyncStuff(function(messages) { 298 | validation.close(messages); 299 | }); 300 | return validation; 301 | }; 302 | ``` 303 | 304 | In the unlikely event that you don't already use a library with AJAX capability, a basic function is provided for making GET requests as follows: 305 | 306 | ```javascript 307 | judge.get('/something', { 308 | success: function(status, headers, text) { 309 | // status code 20x 310 | }, 311 | error: function(status, headers, text) { 312 | // any other status code 313 | } 314 | }); 315 | ``` 316 | 317 | ## Judge extensions 318 | 319 | If you use [Formtastic](https://github.com/justinfrench/formtastic) or [SimpleForm](https://github.com/plataformatec/simple_form), there are extension gems to help you use Judge within your forms without any extra setup. They are essentially basic patches that add the `:validate => true` option to the `input` method. 320 | 321 | ### Formtastic 322 | 323 | [https://github.com/joecorcoran/judge-formtastic](https://github.com/joecorcoran/judge-formtastic) 324 | 325 | ```ruby 326 | gem 'judge-formtastic' 327 | ``` 328 | 329 | ```ruby 330 | semantic_form_for(@user) do |f| 331 | f.input :name, :validate => true 332 | end 333 | ``` 334 | 335 | ### SimpleForm 336 | 337 | [https://github.com/joecorcoran/judge-simple_form](https://github.com/joecorcoran/judge-simple_form) 338 | 339 | ```ruby 340 | gem 'judge-simple_form' 341 | ``` 342 | 343 | ```ruby 344 | simple_form_for(@user) do |f| 345 | f.input :name, :validate => true 346 | end 347 | ``` 348 | 349 | ## Contributing 350 | 351 | Fork this repo and submit a pull request with an explanation of the changes you've made. If you're thinking of making a relatively big change, open an issue and let's discuss it first! :) 352 | 353 | Run tests (the JavaScript tests require [PhantomJS](http://phantomjs.org/)): 354 | ```bash 355 | $ bundle exec rake 356 | ``` 357 | 358 | To test against all supported minor versions of Rails: 359 | ```bash 360 | $ bundle exec rake appraisal:install 361 | $ bundle exec rake appraisal 362 | ``` 363 | 364 | ## Credit 365 | 366 | Created by [Joe Corcoran](https://corcoran.io). Thank you to every user, email corresponder and pull request submitter. 367 | 368 | [Released under an MIT license](https://github.com/joecorcoran/judge/blob/master/LICENSE.txt). 369 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | begin 3 | require 'bundler/setup' 4 | rescue LoadError 5 | puts 'You must `gem install bundler` and `bundle install` to run rake tasks' 6 | end 7 | 8 | require_relative 'spec/javascripts/javascript_spec_server' 9 | require 'appraisal' 10 | require 'rspec/core/rake_task' 11 | RSpec::Core::RakeTask.new(:spec) 12 | 13 | APP_RAKEFILE = File.expand_path('../spec/dummy/Rakefile', __FILE__) 14 | load 'rails/tasks/engine.rake' 15 | 16 | desc 'Require phantomjs' 17 | task :phantomjs do 18 | sh 'which phantomjs' do |ok, ps| 19 | fail 'phantomjs not found' unless ok 20 | end 21 | end 22 | 23 | desc 'Run JavaScript tests' 24 | task :js => [:phantomjs] do 25 | port = 8080 26 | server = JavascriptSpecServer.new(port, './') 27 | server.boot 28 | sh "phantomjs spec/javascripts/run.js #{port}" do |ok, ps| 29 | exit(ps.exitstatus) 30 | end 31 | end 32 | 33 | desc 'Run all tests' 34 | task :default => [:spec, :js] 35 | 36 | Bundler::GemHelper.install_tasks 37 | -------------------------------------------------------------------------------- /app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link javascripts/judge.js -------------------------------------------------------------------------------- /app/assets/javascripts/judge.js: -------------------------------------------------------------------------------- 1 | // Judge 2.1.1 2 | // (c) 2011-2013 Joe Corcoran 3 | // http://raw.github.com/joecorcoran/judge/master/LICENSE.txt 4 | 5 | // This is judge.js: the JavaScript part of Judge. Judge is a client-side 6 | // validation gem for Rails 3. You can find the Judge gem API documentation at 7 | // . 8 | 9 | (function() { 10 | 11 | var root = this; 12 | 13 | // The judge namespace. 14 | var judge = root.judge = {}, 15 | _ = root._; 16 | 17 | judge.VERSION = '2.1.1'; 18 | 19 | // Trying to be a bit more descriptive than the basic error types allow. 20 | var DependencyError = function(message) { 21 | this.name = 'DependencyError'; 22 | this.message = message; 23 | }; 24 | DependencyError.prototype = new Error(); 25 | DependencyError.prototype.constructor = DependencyError; 26 | 27 | // Throw dependency errors if necessary. 28 | if (typeof _ === 'undefined') { 29 | throw new DependencyError('Ensure underscore.js is loaded'); 30 | } 31 | if (_.isUndefined(root.JSON)) { 32 | throw new DependencyError( 33 | 'Judge depends on the global JSON object (load json2.js in old browsers)' 34 | ); 35 | } 36 | 37 | // Returns the object type as represented in `Object.prototype.toString`. 38 | var objectString = function(object) { 39 | var string = Object.prototype.toString.call(object); 40 | return string.replace(/\[|\]/g, '').split(' ')[1]; 41 | }; 42 | 43 | // A way of checking isArray, but including weird object types that are 44 | // returned from collection queries. 45 | var isCollection = function(object) { 46 | var type = objectString(object), 47 | types = [ 48 | 'Array', 49 | 'NodeList', 50 | 'StaticNodeList', 51 | 'HTMLCollection', 52 | 'HTMLFormElement', 53 | 'HTMLAllCollection' 54 | ]; 55 | return _(types).include(type); 56 | }; 57 | 58 | // eval is used here for stuff like `(3, '<', 4) => '3 < 4' => true`. 59 | var operate = function(input, operator, validInput) { 60 | return eval(input+' '+operator+' '+validInput); 61 | }; 62 | 63 | // Some nifty numerical helpers. 64 | var 65 | isInt = function(value) { return Math.round(value) == value; }, 66 | isEven = function(value) { return (value % 2 === 0) ? true : false; }, 67 | isOdd = function(value) { return !isEven(value); }; 68 | 69 | // Converts a Ruby regular expression, given as a string, into JavaScript. 70 | // This is rudimentary at best, as there are many, many differences between 71 | // Ruby and JavaScript when it comes to regexp-fu. The plan is to replace this 72 | // with an XRegExp plugin which will port some Ruby regexp features to 73 | // JavaScript. 74 | var convertFlags = function(string) { 75 | var on = string.split('-')[0]; 76 | return (/m/.test(on)) ? 'm' : ''; 77 | }; 78 | var convertRegExp = function(string) { 79 | var parts = string.slice(1, -1).split(':'), 80 | flags = parts.shift().replace('?', ''), 81 | source = parts.join(':').replace(/\\\\/g, '\\').replace('\\A', '^').replace('\\z', '$'); 82 | return new RegExp(source, convertFlags(flags)); 83 | }; 84 | 85 | // Returns a browser-specific XHR object, or null if one cannot be constructed. 86 | var reqObj = function() { 87 | return ( 88 | (root.ActiveXObject && new root.ActiveXObject('Microsoft.XMLHTTP')) || 89 | (root.XMLHttpRequest && new root.XMLHttpRequest()) || 90 | null 91 | ); 92 | }; 93 | 94 | // Performs a GET request using the browser's XHR object. This provides very 95 | // basic ajax capability and was written specifically for use in the provided 96 | // uniqueness validator without requiring jQuery. 97 | var get = judge.get = function(url, callbacks) { 98 | var req = reqObj(); 99 | if (!!req) { 100 | req.onreadystatechange = function() { 101 | if (req.readyState === 4) { 102 | req.onreadystatechange = function() {}; 103 | var callback = /^20\d$/.test(req.status) ? callbacks.success : callbacks['error']; 104 | callback(req.status, req.responseHeaders, req.responseText); 105 | } 106 | }; 107 | req.open('GET', url, true); 108 | req.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); 109 | req.setRequestHeader('Accept', 'application/json'); 110 | req.send(); 111 | } 112 | return req; 113 | }; 114 | 115 | // Some helper methods for working with Rails-style input attributes. 116 | var 117 | attrFromName = function(name) { 118 | var matches, attr = ''; 119 | if (matches = name.match(/\[(\w+)\]$/)) { 120 | attr = matches[1]; 121 | } 122 | return attr; 123 | }; 124 | originalValue = function(el) { 125 | var validations = JSON.parse(el.getAttribute('data-validate')); 126 | var validation = _.filter(validations, function (validation) { return validation.kind === "uniqueness"})[0]; 127 | return validation.original_value; 128 | }; 129 | 130 | // Build the URL necessary to send a GET request to the mounted validations 131 | // controller to check the validity of the given form element. 132 | var urlFor = judge.urlFor = function(el, kind) { 133 | var path = judge.enginePath, 134 | params = { 135 | 'klass' : el.getAttribute('data-klass'), 136 | 'attribute': attrFromName(el.name), 137 | 'value' : el.value, 138 | 'kind' : kind 139 | }; 140 | if (kind === 'uniqueness') { 141 | params['original_value'] = originalValue(el); 142 | } 143 | return path + queryString(params); 144 | }; 145 | 146 | // Convert an object literal into an encoded query string. 147 | var queryString = function(obj) { 148 | var e = encodeURIComponent, 149 | qs = _.reduce(obj, function(memo, value, key) { 150 | return memo + e(key) + '=' + e(value) + '&'; 151 | }, '?'); 152 | return qs.replace(/&$/, '').replace(/%20/g, '+'); 153 | }; 154 | 155 | // Default path to mounted engine. Override this if you decide to mount 156 | // Judge::Engine at a different location. 157 | judge.enginePath = '/judge'; 158 | 159 | // Provides event dispatch behaviour when mixed into an object. Concept 160 | // taken from Backbone.js, stripped down and altered. 161 | // Backbone.js (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc. 162 | // http://backbonejs.org 163 | var Dispatcher = judge.Dispatcher = { 164 | on: function(eventName, callback, scope) { 165 | if (!_.isFunction(callback)) return this; 166 | this._events || (this._events = {}); 167 | var events = this._events[eventName] || (this._events[eventName] = []); 168 | events.push({ callback: callback, scope: scope || this }); 169 | if (eventName !== 'bind') { 170 | this.trigger('bind', eventName); 171 | } 172 | return this; 173 | }, 174 | trigger: function(eventName) { 175 | if (!this._events) return this; 176 | var args = _.rest(arguments), 177 | events = this._events[eventName] || (this._events[eventName] = []); 178 | _.each(events, function(eventObj) { 179 | eventObj.callback.apply(eventObj.scope, args); 180 | }); 181 | return this; 182 | } 183 | }; 184 | 185 | // A queue of closed or pending Validation objects. 186 | var ValidationQueue = judge.ValidationQueue = function(element) { 187 | this.element = element; 188 | this.validations = []; 189 | this.attrValidators = root.JSON.parse(this.element.getAttribute('data-validate')); 190 | 191 | _.each(this.attrValidators, function(av) { 192 | if (this.element.value.length || av.options.allow_blank !== true) { 193 | var method = _.bind(judge.eachValidators[av.kind], this.element), 194 | validation = method(av.options, av.messages); 195 | validation.on('close', this.tryClose, this); 196 | this.validations.push(validation); 197 | } 198 | }, this); 199 | 200 | this.on('bind', this.tryClose, this); 201 | }; 202 | 203 | _.extend(ValidationQueue.prototype, Dispatcher, { 204 | tryClose: function(eventName) { 205 | var report = _.reduce(this.validations, function(obj, validation) { 206 | obj.statuses = _.union(obj.statuses, [validation.status()]); 207 | obj.messages = _.union(obj.messages, _.compact(validation.messages)); 208 | return obj; 209 | }, { statuses: [], messages: [] }, this); 210 | 211 | if (!_.contains(report.statuses, 'pending')) { 212 | var status = _.contains(report.statuses, 'invalid') ? 'invalid' : 'valid'; 213 | 214 | // handle single callback 215 | this.trigger('close', this.element, status, report.messages); 216 | 217 | // handle named callbacks 218 | this.trigger(status, this.element, report.messages); 219 | } 220 | } 221 | }); 222 | 223 | // Event-capable object returned by validator methods. 224 | var Validation = judge.Validation = function(messages) { 225 | this.messages = null; 226 | if (_.isArray(messages)) this.close(messages); 227 | return this; 228 | }; 229 | _.extend(Validation.prototype, Dispatcher, { 230 | close: function(messages) { 231 | if (this.closed()) return null; 232 | this.messages = _.isString(messages) ? root.JSON.parse(messages) : messages; 233 | this.trigger('close', this.status(), this.messages); 234 | return this; 235 | }, 236 | closed: function() { 237 | return _.isArray(this.messages); 238 | }, 239 | status: function() { 240 | if (!this.closed()) return 'pending'; 241 | return this.messages.length > 0 ? 'invalid' : 'valid'; 242 | } 243 | }); 244 | 245 | // Convenience methods for creating Validation objects in different states. 246 | var pending = judge.pending = function() { 247 | return new Validation(); 248 | }; 249 | var closed = judge.closed = function(messages) { 250 | return new Validation(messages); 251 | }; 252 | 253 | // Ported ActiveModel validators. 254 | // See for 255 | // the originals. 256 | judge.eachValidators = { 257 | // ActiveModel::Validations::PresenceValidator 258 | presence: function(options, messages) { 259 | if (this.type === 'radio') { 260 | var is_selected = root.document.querySelectorAll('[name="'+this.name+'"]:checked').length; 261 | return closed(is_selected ? [] : [messages.blank]); 262 | } else { 263 | return closed(this.value.length ? [] : [messages.blank]); 264 | } 265 | }, 266 | 267 | // ActiveModel::Validations::LengthValidator 268 | length: function(options, messages) { 269 | var msgs = [], 270 | types = { 271 | minimum: { operator: '<', message: 'too_short' }, 272 | maximum: { operator: '>', message: 'too_long' }, 273 | is: { operator: '!=', message: 'wrong_length' } 274 | }; 275 | _(types).each(function(properties, type) { 276 | var length = this.length || this.value.length; 277 | // Rails validations count new lines as two characters, we account for them here 278 | length += (this.value.match(/\n/g) || []).length; 279 | var invalid = operate(length, properties.operator, options[type]); 280 | if (_(options).has(type) && invalid) { 281 | msgs.push(messages[properties.message]); 282 | } 283 | }, this); 284 | return closed(msgs); 285 | }, 286 | 287 | // ActiveModel::Validations::ExclusionValidator 288 | exclusion: function(options, messages) { 289 | var stringIn = _(options['in']).map(function(o) { 290 | return o.toString(); 291 | }); 292 | return closed( 293 | _.include(stringIn, this.value) ? [messages.exclusion] : [] 294 | ); 295 | }, 296 | 297 | // ActiveModel::Validations::InclusionValidator 298 | inclusion: function(options, messages) { 299 | var stringIn = _(options['in']).map(function(o) { 300 | return o.toString(); 301 | }); 302 | return closed( 303 | !_.include(stringIn, this.value) ? [messages.inclusion] : [] 304 | ); 305 | }, 306 | 307 | // ActiveModel::Validations::NumericalityValidator 308 | numericality: function(options, messages) { 309 | var operators = { 310 | greater_than: '>', 311 | greater_than_or_equal_to: '>=', 312 | equal_to: '==', 313 | less_than: '<', 314 | less_than_or_equal_to: '<=' 315 | }, 316 | msgs = [], 317 | parsedValue = parseFloat(this.value, 10); 318 | 319 | if (isNaN(Number(this.value))) { 320 | msgs.push(messages.not_a_number); 321 | } else { 322 | if (options.odd && isEven(parsedValue)) msgs.push(messages.odd); 323 | if (options.even && isOdd(parsedValue)) msgs.push(messages.even); 324 | if (options.only_integer && !isInt(parsedValue)) msgs.push(messages.not_an_integer); 325 | _(operators).each(function(operator, key) { 326 | var valid = operate(parsedValue, operators[key], parseFloat(options[key], 10)); 327 | if (_(options).has(key) && !valid) { 328 | msgs.push(messages[key]); 329 | } 330 | }); 331 | } 332 | return closed(msgs); 333 | }, 334 | 335 | // ActiveModel::Validations::FormatValidator 336 | format: function(options, messages) { 337 | var msgs = []; 338 | if (_(options).has('with')) { 339 | var withReg = convertRegExp(options['with']); 340 | if (!withReg.test(this.value)) { 341 | msgs.push(messages.invalid); 342 | } 343 | } 344 | if (_(options).has('without')) { 345 | var withoutReg = convertRegExp(options.without); 346 | if (withoutReg.test(this.value)) { 347 | msgs.push(messages.invalid); 348 | } 349 | } 350 | return closed(msgs); 351 | }, 352 | 353 | // ActiveModel::Validations::AcceptanceValidator 354 | acceptance: function(options, messages) { 355 | return closed(this.checked === true ? [] : [messages.accepted]); 356 | }, 357 | 358 | // ActiveModel::Validations::ConfirmationValidator 359 | confirmation: function(options, messages) { 360 | var id = this.getAttribute('id'), 361 | confId = id.replace('_confirmation', ''), 362 | confElem = root.document.getElementById(confId); 363 | return closed( 364 | this.value === confElem.value ? [] : [messages.confirmation] 365 | ); 366 | }, 367 | 368 | // ActiveModel::Validations::UniquenessValidator 369 | uniqueness: function(options, messages) { 370 | var validation = pending(); 371 | get(urlFor(this, 'uniqueness'), { 372 | success: function(status, headers, text) { 373 | validation.close(text); 374 | }, 375 | error: function(status, headers, text) { 376 | validation.close(['Request error: ' + status]); 377 | } 378 | }); 379 | return validation; 380 | } 381 | }; 382 | 383 | var isCallbacksObj = function(obj) { 384 | return _.isObject(obj) && _.has(obj, 'valid') && _.has(obj, 'invalid'); 385 | }; 386 | 387 | // Method for validating a form element. Pass either a single 388 | // callback or one for valid and one for invalid. 389 | judge.validate = function(element, callbacks) { 390 | var queue = new ValidationQueue(element); 391 | if (_.isFunction(callbacks)) { 392 | queue.on('close', callbacks); 393 | } else if (isCallbacksObj(callbacks)) { 394 | queue.on('valid', _.once(callbacks.valid)); 395 | queue.on('invalid', _.once(callbacks.invalid)); 396 | } 397 | return queue; 398 | }; 399 | 400 | }).call(this); 401 | -------------------------------------------------------------------------------- /app/controllers/judge/validations_controller.rb: -------------------------------------------------------------------------------- 1 | module Judge 2 | class ValidationsController < ActionController::Base 3 | include Judge::Controller 4 | 5 | def build 6 | render json: validation(params) 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Judge::Engine.routes.draw do 2 | root :to => "validations#build", :as => :build_validation, :via => :get 3 | end 4 | -------------------------------------------------------------------------------- /gemfiles/rails_5_0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "http://rubygems.org" 4 | 5 | gem "rails", "~> 5.0" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /gemfiles/rails_5_1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "http://rubygems.org" 4 | 5 | gem "rails", "~> 5.1" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /gemfiles/rails_5_2.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "http://rubygems.org" 4 | 5 | gem "rails", "~> 5.2" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /judge.gemspec: -------------------------------------------------------------------------------- 1 | $:.push File.expand_path('../lib', __FILE__) 2 | 3 | require 'judge/version' 4 | 5 | Gem::Specification.new do |s| 6 | s.name = 'judge' 7 | s.version = Judge::VERSION 8 | s.homepage = 'http://github.com/joecorcoran/judge' 9 | s.summary = 'Client side validation for Rails' 10 | s.description = 'Validate Rails forms on the client side, simply' 11 | s.email = 'joecorcoran@gmail.com' 12 | s.authors = ['Joe Corcoran'] 13 | s.files = Dir['{app,config,lib,vendor}/**/*'] + ['LICENSE.txt', 'README.md'] 14 | s.license = 'MIT' 15 | 16 | s.add_runtime_dependency 'rails', '>= 5.0' 17 | 18 | s.add_development_dependency 'rspec-rails', '~> 4.0.1' 19 | s.add_development_dependency 'rspec-extra-formatters', '~> 1.0' 20 | s.add_development_dependency 'jquery-rails' 21 | s.add_development_dependency 'sqlite3', '~> 1.3' 22 | s.add_development_dependency 'factory_girl', '~> 4.5' 23 | s.add_development_dependency 'appraisal', '~> 1.0' 24 | end 25 | -------------------------------------------------------------------------------- /lib/generators/judge/install/install_generator.rb: -------------------------------------------------------------------------------- 1 | module Judge 2 | module Generators 3 | class InstallGenerator < ::Rails::Generators::Base 4 | 5 | desc %Q{For use where asset pipeline is disabled. 6 | Installs judge.js and optionally installs underscore.js and json2.js. 7 | 8 | Copy judge.js to public/javascripts: 9 | $ rails generate judge:install 10 | 11 | Copy judge.js to different path: 12 | $ rails generate judge:install path 13 | 14 | Copy judge.js and dependencies: 15 | $ rails generate judge:install path --underscore --json2 16 | } 17 | 18 | argument :path, :type => :string, :default => "public/javascripts" 19 | class_option :underscore, :type => :boolean, :default => false, :desc => "Install underscore.js" 20 | class_option :json2, :type => :boolean, :default => false, :desc => "Install json2.js" 21 | source_root File.expand_path("../../../../..", __FILE__) 22 | 23 | def exec 24 | unless !::Rails.application.config.assets.enabled 25 | say_status("deprecated", "You don't need to use this generator as your app is running on Rails >= 3.1 with the asset pipeline enabled") 26 | return 27 | end 28 | say_status("copying", "judge.js", :green) 29 | copy_file("app/assets/javascripts/judge.js", "#{path}/judge.js") 30 | if options.underscore? 31 | say_status("copying", "underscore.js", :green) 32 | copy_file("vendor/assets/javascripts/underscore.js", "#{path}/underscore.js") 33 | end 34 | if options.json2? 35 | say_status("copying", "json2.js", :green) 36 | copy_file("vendor/assets/javascripts/json2.js", "#{path}/json2.js") 37 | end 38 | end 39 | 40 | end 41 | end 42 | end -------------------------------------------------------------------------------- /lib/judge.rb: -------------------------------------------------------------------------------- 1 | files = [ 2 | 'version', 3 | 'config', 4 | 'engine', 5 | 'validator', 6 | 'validator_collection', 7 | 'message_collection', 8 | 'html', 9 | 'form_builder', 10 | 'each_validator', 11 | 'validation', 12 | 'controller', 13 | 'confirmation_validator' 14 | ] 15 | files.each { |filename| require "judge/#{filename}" } -------------------------------------------------------------------------------- /lib/judge/config.rb: -------------------------------------------------------------------------------- 1 | require 'singleton' 2 | 3 | module Judge 4 | class Config 5 | include Singleton 6 | 7 | @@exposed = {} 8 | @@ignore_unsupported_validators = false 9 | @@use_association_name_for_validations = false 10 | 11 | def expose(klass, *attributes) 12 | attrs = (@@exposed[klass.name] ||= []) 13 | attrs.concat(attributes).uniq! 14 | end 15 | 16 | def exposed 17 | @@exposed 18 | end 19 | 20 | def exposed?(klass, attribute) 21 | @@exposed.has_key?(klass.name) && @@exposed[klass.name].include?(attribute) 22 | end 23 | 24 | def unexpose(klass, *attributes) 25 | attributes.each do |a| 26 | @@exposed[klass.name].delete(a) 27 | end 28 | if attributes.empty? || @@exposed[klass.name].empty? 29 | @@exposed.delete(klass.name) 30 | end 31 | end 32 | 33 | def ignore_unsupported_validators(status) 34 | @@ignore_unsupported_validators = status 35 | end 36 | 37 | def ignore_unsupported_validators? 38 | @@ignore_unsupported_validators 39 | end 40 | 41 | def use_association_name_for_validations(status) 42 | @@use_association_name_for_validations = status 43 | end 44 | 45 | def use_association_name_for_validations? 46 | @@use_association_name_for_validations 47 | end 48 | end 49 | 50 | def self.config 51 | Config.instance 52 | end 53 | 54 | def self.configure(&block) 55 | Config.instance.instance_eval(&block) 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/judge/confirmation_validator.rb: -------------------------------------------------------------------------------- 1 | module Judge 2 | class ConfirmationValidator 3 | 4 | include Judge::EachValidator 5 | 6 | attr_reader :object, :method, :amv 7 | 8 | def initialize(object, method) 9 | @object = object 10 | @method = method 11 | @amv = amv_from_original 12 | end 13 | 14 | def kind 15 | @amv.kind if @amv.present? 16 | end 17 | 18 | def options 19 | @amv.options if @amv.present? 20 | end 21 | 22 | private 23 | 24 | def amv_from_original 25 | original_amv = nil 26 | original_method = method.to_s.gsub('_confirmation', '').to_sym 27 | object.class.validators_on(original_method).each do |v| 28 | original_amv = v if v.class.name['ConfirmationValidator'] 29 | end 30 | 31 | original_amv 32 | end 33 | 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/judge/controller.rb: -------------------------------------------------------------------------------- 1 | module Judge 2 | module Controller 3 | 4 | def validation(params) 5 | params = normalized_params(params) 6 | if params_present?(params) && params_exposed?(params) 7 | Validation.new(params) 8 | else 9 | NullValidation.new(params) 10 | end 11 | end 12 | 13 | private 14 | 15 | REQUIRED_PARAMS = %w{klass attribute value kind} 16 | CONDITIONAL_PARAMS = {kind: ['uniqueness', :original_value]} 17 | 18 | def params_exposed?(params) 19 | Judge.config.exposed?(params[:klass], params[:attribute]) 20 | end 21 | 22 | def params_present?(params) 23 | required_params_present?(params) && conditional_params_present?(params) 24 | end 25 | 26 | def required_params_present?(params) 27 | REQUIRED_PARAMS.all? {|s| params.key? s} && params.values_at(*REQUIRED_PARAMS).all? 28 | end 29 | 30 | def conditional_params_present?(params) 31 | CONDITIONAL_PARAMS.each do |required_param, constraint| 32 | if params[required_param] == constraint.first 33 | return false unless params[constraint.last] 34 | end 35 | end 36 | end 37 | 38 | def normalized_params(params) 39 | params = params.dup.keep_if {|k| REQUIRED_PARAMS.include?(k) || (k == 'original_value' && params[:kind] == 'uniqueness')} 40 | params[:klass] = find_klass(params[:klass]) if params[:klass] 41 | params[:attribute] = params[:attribute].to_sym if params[:attribute] 42 | params[:value] = CGI::unescape(params[:value]) if params[:value] 43 | params[:kind] = params[:kind].to_sym if params[:kind] 44 | params[:original_value] = CGI::unescape(params[:original_value]) if params[:original_value] 45 | params 46 | end 47 | 48 | def find_klass(name) 49 | Module.const_get(name.classify) 50 | rescue NameError 51 | nil 52 | end 53 | 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/judge/each_validator.rb: -------------------------------------------------------------------------------- 1 | module Judge 2 | module EachValidator 3 | 4 | require 'set' 5 | 6 | def self.included(base) 7 | base.send(:cattr_accessor, :messages_to_lookup) { Set.new } 8 | base.send(:extend, ClassMethods) 9 | end 10 | 11 | module ClassMethods 12 | 13 | def uses_messages(*keys) 14 | self.messages_to_lookup.merge(keys) 15 | end 16 | 17 | end 18 | 19 | end 20 | end 21 | 22 | ::ActiveModel::EachValidator.send(:include, Judge::EachValidator) if defined?(::ActiveModel::EachValidator) -------------------------------------------------------------------------------- /lib/judge/engine.rb: -------------------------------------------------------------------------------- 1 | module Judge 2 | class Engine < ::Rails::Engine 3 | isolate_namespace Judge 4 | end 5 | end -------------------------------------------------------------------------------- /lib/judge/form_builder.rb: -------------------------------------------------------------------------------- 1 | module Judge 2 | 3 | class FormBuilder < ActionView::Helpers::FormBuilder 4 | 5 | include Judge::Html 6 | 7 | %w{text_field text_area password_field email_field telephone_field}.each do |type| 8 | helper = <<-END 9 | def #{type}(method, options = {}) 10 | add_validate_attr!(self.object, method, options) 11 | super 12 | end 13 | END 14 | class_eval helper, __FILE__, __LINE__ 15 | end 16 | 17 | def radio_button(method, tag_value, options = {}) 18 | add_validate_attr!(self.object, method, options) 19 | super 20 | end 21 | 22 | def check_box(method, options = {}, checked_value = "1", unchecked_value = "0") 23 | add_validate_attr!(self.object, method, options) 24 | super 25 | end 26 | 27 | def select(method, choices, options = {}, html_options = {}) 28 | add_validate_attr!(self.object, method, options, html_options) 29 | super 30 | end 31 | 32 | def collection_select(method, collection, value_method, text_method, options = {}, html_options = {}) 33 | add_validate_attr!(self.object, method, options, html_options) 34 | super 35 | end 36 | 37 | def grouped_collection_select(method, collection, group_method, group_label_method, option_key_method, option_value_method, options = {}, html_options = {}) 38 | add_validate_attr!(self.object, method, options, html_options) 39 | super 40 | end 41 | 42 | %w{date_select datetime_select time_select}.each do |type| 43 | helper = <<-END 44 | def #{type}(method, options = {}, html_options = {}) 45 | add_validate_attr!(self.object, method, options, html_options) 46 | super 47 | end 48 | END 49 | class_eval helper, __FILE__, __LINE__ 50 | end 51 | 52 | def time_zone_select(method, priority_zones = nil, options = {}, html_options = {}) 53 | add_validate_attr!(self.object, method, options, html_options) 54 | super 55 | end 56 | 57 | private 58 | 59 | def add_validate_attr!(object, method, options, html_options = nil) 60 | options_to_merge = html_options || options 61 | if options.delete(:validate) 62 | options_to_merge.merge! attrs_for(object, method) 63 | end 64 | end 65 | 66 | end 67 | 68 | end 69 | -------------------------------------------------------------------------------- /lib/judge/html.rb: -------------------------------------------------------------------------------- 1 | module Judge 2 | module Html 3 | extend self 4 | 5 | def attrs_for(object, method) 6 | { 7 | 'data-validate' => ValidatorCollection.new(object, method).to_json, 8 | 'data-klass' => object.class.name 9 | } 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/judge/message_collection.rb: -------------------------------------------------------------------------------- 1 | require "judge/message_config" 2 | 3 | module Judge 4 | 5 | class MessageCollection 6 | 7 | include MessageConfig 8 | 9 | attr_reader :object, :method, :amv, :kind, :options 10 | 11 | def initialize(object, method, amv) 12 | @object = object 13 | @method = method 14 | @amv = amv 15 | @kind = amv.kind 16 | @options = amv.options.dup 17 | @messages = {} 18 | generate_messages! 19 | end 20 | 21 | def generate_messages! 22 | return if @kind == :uniqueness 23 | %w{base options integer custom blank}.each do |type| 24 | @messages = @messages.merge(self.send(:"#{type}_messages")) 25 | end 26 | end 27 | 28 | def to_hash 29 | @messages 30 | end 31 | 32 | def custom_messages? 33 | amv.respond_to?(:messages_to_lookup) && amv.messages_to_lookup.present? 34 | end 35 | 36 | private 37 | 38 | def base_messages 39 | msgs = {} 40 | if MESSAGE_MAP.has_key?(kind) && MESSAGE_MAP[kind][:base].present? 41 | base_message = MESSAGE_MAP[kind][:base] 42 | msgs[base_message] = object.errors.generate_message(method, base_message, options) 43 | end 44 | msgs 45 | end 46 | 47 | def options_messages 48 | msgs = {} 49 | if MESSAGE_MAP.has_key?(kind) && MESSAGE_MAP[kind][:options].present? 50 | opt_messages = MESSAGE_MAP[kind][:options] 51 | opt_messages.each do |opt, opt_message| 52 | if options.has_key?(opt) 53 | options_for_interpolation = { :count => options[opt] }.merge(options) 54 | msgs[opt_message] = object.errors.generate_message(method, opt_message, options_for_interpolation) 55 | end 56 | end 57 | end 58 | msgs 59 | end 60 | 61 | def blank_messages 62 | msgs = {} 63 | if ALLOW_BLANK.include?(kind) && options[:allow_blank].blank? && @messages[:blank].blank? 64 | msgs[:blank] = object.errors.generate_message(method, :blank) 65 | end 66 | msgs 67 | end 68 | 69 | def integer_messages 70 | msgs = {} 71 | if kind == :numericality && options[:only_integer].present? 72 | msgs[:not_an_integer] = object.errors.generate_message(method, :not_an_integer) 73 | end 74 | msgs 75 | end 76 | 77 | def custom_messages 78 | msgs = {} 79 | if custom_messages? 80 | amv.messages_to_lookup.each do |key| 81 | msgs[key.to_sym] = object.errors.generate_message(method, key) 82 | end 83 | end 84 | msgs 85 | end 86 | 87 | end 88 | 89 | end -------------------------------------------------------------------------------- /lib/judge/message_config.rb: -------------------------------------------------------------------------------- 1 | module Judge 2 | 3 | module MessageConfig 4 | 5 | ALLOW_BLANK = [ 6 | :format, 7 | :exclusion, 8 | :inclusion, 9 | :length 10 | ] 11 | 12 | MESSAGE_MAP = { 13 | :confirmation => { :base => :confirmation }, 14 | :acceptance => { :base => :accepted }, 15 | :presence => { :base => :blank }, 16 | :length => { :base => nil, 17 | :options => { 18 | :minimum => :too_short, 19 | :maximum => :too_long, 20 | :is => :wrong_length 21 | } 22 | }, 23 | :format => { :base => :invalid }, 24 | :inclusion => { :base => :inclusion }, 25 | :exclusion => { :base => :exclusion }, 26 | :numericality => { :base => :not_a_number, 27 | :options => { 28 | :greater_than => :greater_than, 29 | :greater_than_or_equal_to => :greater_than_or_equal_to, 30 | :equal_to => :equal_to, 31 | :less_than => :less_than, 32 | :less_than_or_equal_to => :less_than_or_equal_to, 33 | :odd => :odd, 34 | :even => :even 35 | } 36 | }, 37 | :uniqueness => { :base => :taken } 38 | } 39 | 40 | end 41 | 42 | end -------------------------------------------------------------------------------- /lib/judge/validation.rb: -------------------------------------------------------------------------------- 1 | module Judge 2 | 3 | class Validation 4 | def initialize(params) 5 | @klass = params[:klass] 6 | @attribute = params[:attribute] 7 | @value = params[:value] 8 | @kind = params[:kind] 9 | @original_value = params[:original_value] 10 | validate! 11 | end 12 | 13 | def amv 14 | @amv ||= begin 15 | validators = @klass.validators_on(@attribute) 16 | validators.keep_if { |amv| amv.kind == @kind } 17 | validators.first 18 | end 19 | end 20 | 21 | def record 22 | @record ||= begin 23 | rec = @klass.new 24 | rec[@attribute] = @value 25 | rec 26 | end 27 | end 28 | 29 | def validate! 30 | record.errors.delete(@attribute) 31 | amv.validate_each(record, @attribute, @value) unless amv.kind == :uniqueness && @value == @original_value && @original_value != "" 32 | self 33 | end 34 | 35 | def as_json(options = {}) 36 | record.errors[@attribute] || [] 37 | end 38 | end 39 | 40 | class NullValidation 41 | def initialize(params) 42 | @params = params 43 | end 44 | 45 | def as_json(options = {}) 46 | ["Judge validation for #{@params[:klass]}##{@params[:attribute]} not allowed"] 47 | end 48 | 49 | def method_missing(*args) 50 | self 51 | end 52 | end 53 | 54 | end 55 | -------------------------------------------------------------------------------- /lib/judge/validator.rb: -------------------------------------------------------------------------------- 1 | module Judge 2 | 3 | class Validator 4 | 5 | attr_reader :active_model_validator, :kind, :options, :method, :messages, :original_value 6 | 7 | REJECTED_OPTIONS = [:if, :on, :unless, :tokenizer, :scope, :case_sensitive, :judge] 8 | 9 | def initialize(object, method, amv) 10 | @kind = amv.kind 11 | @options = amv.options.reject { |key| REJECTED_OPTIONS.include?(key) } 12 | @method = method 13 | @messages = Judge::MessageCollection.new(object, method, amv) 14 | @original_value = object.send(method) 15 | end 16 | 17 | def to_hash 18 | params = { 19 | :kind => kind, 20 | :options => options, 21 | :messages => messages.to_hash 22 | } 23 | params[:original_value] = original_value if kind == :uniqueness 24 | params 25 | end 26 | 27 | end 28 | 29 | end -------------------------------------------------------------------------------- /lib/judge/validator_collection.rb: -------------------------------------------------------------------------------- 1 | module Judge 2 | 3 | class ValidatorCollection 4 | 5 | include Enumerable 6 | 7 | attr_reader :validators, :object, :method 8 | 9 | def initialize(object, method) 10 | @object = object 11 | @method = method 12 | @validators = amvs.map { |amv| Judge::Validator.new(object, method, amv) } 13 | end 14 | 15 | def each(&block) 16 | validators.each do |v| 17 | block.call(v) 18 | end 19 | end 20 | 21 | def to_json 22 | validators.map { |v| v.to_hash }.to_json 23 | end 24 | 25 | protected 26 | 27 | UNSUPPORTED_OPTIONS = [:if, :on, :unless, :tokenizer, :scope, :case_sensitive] 28 | 29 | # returns an array of ActiveModel::Validations 30 | # starts with all Validations attached to method and removes one that are: 31 | # ignored based on a config 32 | # ConfirmationValidators, which are moved directly to the confirmation method 33 | # unsupported by Judge 34 | # if it's a confirmation field, an AM::V like class is added to handle the confirmation validations 35 | def amvs 36 | method_to_search = method 37 | 38 | if Judge.config.use_association_name_for_validations? 39 | # since the method that gets passed in here for associations comes in the form of the generated form attribute 40 | # i.e. :wine_id or :acclaim_ids 41 | # object.class.validators_on(:wine_id) will fail if the active model validation is on the association directly 42 | # this ensures that validations defined as 'validates :wine, presence: true' still get applied 43 | # and client side error messages get generated 44 | regex_for_assocations = /_id|_ids/ 45 | if method.to_s =~ regex_for_assocations 46 | parsed_method = method.to_s.gsub(regex_for_assocations, ''); 47 | reflection = find_association_reflection(parsed_method) 48 | method_to_search = reflection.name if reflection 49 | end 50 | end 51 | 52 | amvs = object.class.validators_on(method_to_search) 53 | amvs = amvs.reject { |amv| reject?(amv) || amv.class.name['ConfirmationValidator'] } 54 | amvs = amvs.reject { |amv| unsupported_options?(amv) && reject?(amv) != false } if Judge.config.ignore_unsupported_validators? 55 | amvs << Judge::ConfirmationValidator.new(object, method) if is_confirmation? 56 | 57 | amvs 58 | end 59 | 60 | def find_association_reflection(association) 61 | if object.class.respond_to?(:reflect_on_association) 62 | object.class.reflect_on_association(association) || object.class.reflect_on_association(association.pluralize) 63 | end 64 | end 65 | 66 | def unsupported_options?(amv) 67 | unsupported = !(amv.options.keys & UNSUPPORTED_OPTIONS).empty? 68 | return false unless unsupported 69 | # Apparently, uniqueness validations always have the case_sensitive option, even 70 | # when it is not explicitly used (in which case it has value true). Hence, we only 71 | # report the validation as unsupported when case_sensitive is set to false. 72 | unsupported = amv.options.keys & UNSUPPORTED_OPTIONS 73 | unsupported.length > 1 || unsupported != [:case_sensitive] || amv.options[:case_sensitive] == false 74 | end 75 | 76 | # decides whether to reject a validation based on the presence of the judge option. 77 | # return values: 78 | # true when :judge => :ignore is present in the options 79 | # false when :judge => :force is present 80 | # nil otherwise (e.g. when no :judge option or an unknown option is present) 81 | def reject?(amv) 82 | return unless [:force, :ignore].include?( amv.options[:judge] ) 83 | amv.options[:judge] == :ignore 84 | end 85 | 86 | def is_confirmation? 87 | method.to_s['_confirmation'] 88 | end 89 | 90 | end 91 | 92 | end -------------------------------------------------------------------------------- /lib/judge/version.rb: -------------------------------------------------------------------------------- 1 | module Judge 2 | VERSION = '3.1.0' 3 | end 4 | -------------------------------------------------------------------------------- /lib/tasks/js.rake: -------------------------------------------------------------------------------- 1 | namespace :jasmine do 2 | task :require_phantom do 3 | sh "which phantomjs" do |ok, res| 4 | fail 'Cannot find phantomjs on $PATH' unless ok 5 | end 6 | end 7 | 8 | task :require_casper do 9 | sh "which casperjs" do |ok, res| 10 | fail 'Cannot find casperjs on $PATH' unless ok 11 | end 12 | end 13 | 14 | desc "Run continuous integration tests headlessly with phantom.js" 15 | task :headless => ['jasmine:require', 'jasmine:require_phantom', 'jasmine:require_casper'] do 16 | support_dir = File.expand_path('../../spec/javascripts/support', File.dirname(__FILE__)) 17 | config_overrides = File.join(support_dir, 'jasmine_config.rb') 18 | require config_overrides if File.exists?(config_overrides) 19 | 20 | test_runner = File.join(support_dir, 'runner.js') 21 | config = Jasmine::Config.new 22 | config.start_jasmine_server 23 | 24 | jasmine_url = "#{config.jasmine_host}:#{config.jasmine_server_port}" 25 | puts "Running tests against #{jasmine_url}" 26 | sh "casperjs #{test_runner} #{jasmine_url}" do |ok, res| 27 | fail "jasmine suite failed" unless ok 28 | end 29 | end 30 | end 31 | 32 | -------------------------------------------------------------------------------- /lib/tasks/judge_tasks.rake: -------------------------------------------------------------------------------- 1 | # desc "Explaining what the task does" 2 | # task :judge do 3 | # # Task goes here 4 | # end 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "judge", 3 | "version": "2.1.1", 4 | "testling": { 5 | "scripts": [ 6 | "vendor/assets/javascripts/*.js", 7 | "app/assets/javascripts/judge.js", 8 | "spec/javascripts/helpers/sinon.js", 9 | "spec/javascripts/helpers/sinon-ie.js", 10 | "spec/javascripts/helpers/customMatchers.js", 11 | "spec/javascripts/support/jasmine.js", 12 | "spec/javascripts/support/tap-reporter.js", 13 | "spec/javascripts/judge-spec.js", 14 | "spec/javascripts/support/tap-runner.js" 15 | ], 16 | "browsers": { 17 | "ie": [6, 7, 8, 9], 18 | "firefox": [3.6, 19], 19 | "chrome": [25], 20 | "safari": [6], 21 | "opera": [12], 22 | "iphone": [6], 23 | "ipad": [6] 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /script/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. 3 | 4 | ENGINE_ROOT = File.expand_path('../..', __FILE__) 5 | ENGINE_PATH = File.expand_path('../../lib/judge/engine', __FILE__) 6 | 7 | require 'rails/all' 8 | require 'rails/engine/commands' 9 | -------------------------------------------------------------------------------- /spec/config_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Judge::Config do 4 | 5 | before(:each) do 6 | class User; end 7 | class Role; end 8 | end 9 | 10 | after(:each) do 11 | Judge.configure do 12 | unexpose User 13 | end 14 | end 15 | 16 | describe ".configure" do 17 | describe "expose" do 18 | it "adds attributes to allowed hash" do 19 | Judge.configure do 20 | expose User, :username, :email 21 | end 22 | Judge.config.exposed['User'].should eql [:username, :email] 23 | end 24 | 25 | it "accepts multiple declarations for the same class" do 26 | Judge.configure do 27 | expose User, :username 28 | expose User, :username, :email 29 | end 30 | Judge.config.exposed['User'].length.should eq 2 31 | end 32 | end 33 | describe "unexpose" do 34 | before(:each) do 35 | Judge.configure do 36 | expose User, :username, :email 37 | end 38 | end 39 | it "removes exposed attributes" do 40 | Judge.configure do 41 | unexpose User, :email 42 | end 43 | Judge.config.exposed['User'].length.should eq 1 44 | end 45 | it "removes whole class when no attributes given" do 46 | Judge.configure do 47 | unexpose User 48 | end 49 | Judge.config.exposed.should_not include User 50 | end 51 | it "removes class when final attribute removed" do 52 | Judge.configure do 53 | unexpose User, :username, :email 54 | end 55 | Judge.config.exposed.should_not include User 56 | end 57 | end 58 | end 59 | 60 | describe ".config" do 61 | describe ".exposed?" do 62 | before(:each) do 63 | Judge.configure do 64 | expose User, :username 65 | end 66 | end 67 | it "returns true if constant and attribute are present in allowed hash" do 68 | Judge.config.exposed?(User, :username).should be_truthy 69 | end 70 | it "returns false otherwise" do 71 | Judge.config.exposed?(User, :foo).should be_falsy 72 | Judge.config.exposed?(Role, :foo).should be_falsy 73 | end 74 | end 75 | end 76 | 77 | end 78 | -------------------------------------------------------------------------------- /spec/confirmation_validator_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Judge::ConfirmationValidator do 4 | 5 | let :password_confirmation do 6 | user = FactoryGirl.build(:user) 7 | Judge::ConfirmationValidator.new(user, :password_confirmation) 8 | end 9 | 10 | describe '#amv' do 11 | it "should return a ConfirmationValidator" do 12 | expect(password_confirmation.amv).to be_a(ActiveModel::Validations::ConfirmationValidator) 13 | end 14 | 15 | it "should be the ConfirmationValidator from :password" do 16 | expect(password_confirmation.amv.attributes).to include(:password) 17 | end 18 | end 19 | 20 | describe '#kind' do 21 | it "should return the the original amv's kind (:confiramtion)" do 22 | expect(password_confirmation.kind).to eq(:confirmation) 23 | expect(password_confirmation.kind).to eq(password_confirmation.amv.kind) 24 | end 25 | end 26 | 27 | describe '#options' do 28 | it "should return the original amv's options (an empty hash)" do 29 | expect(password_confirmation.options).to eq(password_confirmation.amv.options) 30 | end 31 | end 32 | 33 | end 34 | -------------------------------------------------------------------------------- /spec/controllers/judge/validations_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Judge::ValidationsController, type: :controller do 4 | 5 | let(:headers) do 6 | { :accept => "application/json" } 7 | end 8 | 9 | let(:valid_params) do 10 | { 11 | :use_route => :judge, 12 | :format => :json, 13 | :klass => "User", 14 | :attribute => "username", 15 | :value => "tinbucktwo", 16 | :kind => "uniqueness", 17 | :original_value => "tinbucktwo" 18 | } 19 | end 20 | 21 | let(:invalid_params) do 22 | { 23 | :use_route => :judge, 24 | :format => :json, 25 | :klass => "User", 26 | :attribute => "city", 27 | :value => "", 28 | :kind => "city", 29 | :original_value => "nil" 30 | } 31 | end 32 | 33 | describe "GET 'build'" do 34 | describe "when allowed" do 35 | before(:each) { 36 | Judge.config.stub(:exposed?).and_return(true) 37 | @request.headers['accept'] = 'application/json' 38 | } 39 | it "responds with empty JSON array if valid" do 40 | get :build, params: valid_params 41 | response.should be_success 42 | response.body.should eql "[]" 43 | end 44 | it "responds with empty JSON array if original_value equals the value" do 45 | FactoryGirl.create(:user, username: 'tinbucktwo') 46 | get :build, params: valid_params 47 | response.should be_success 48 | response.body.should eql "[]" 49 | end 50 | it "responds with JSON array of error messages if invalid" do 51 | get :build, params: invalid_params 52 | response.should be_success 53 | response.body.should eql "[\"City must be an approved city\"]" 54 | end 55 | end 56 | describe "when not allowed" do 57 | it "responds with JSON array of error messages if class and attribute are not allowed in Judge config" do 58 | get :build, params: valid_params 59 | response.should be_success 60 | response.body.should eql "[\"Judge validation for User#username not allowed\"]" 61 | end 62 | end 63 | end 64 | 65 | describe "#validation" do 66 | let(:controller) { Judge::ValidationsController.new } 67 | let(:params) { valid_params.with_indifferent_access } 68 | describe "when params allowed" do 69 | before(:each) { Judge.config.stub(:exposed?).and_return(true) } 70 | it "returns a Validation object" do 71 | controller.validation(params).should be_a Judge::Validation 72 | end 73 | end 74 | 75 | describe "when params not allowed" do 76 | it "returns a NullValidation object" do 77 | controller.validation(params).should be_a Judge::NullValidation 78 | end 79 | end 80 | end 81 | 82 | end 83 | -------------------------------------------------------------------------------- /spec/dummy/README.rdoc: -------------------------------------------------------------------------------- 1 | == Welcome to Rails 2 | 3 | Rails is a web-application framework that includes everything needed to create 4 | database-backed web applications according to the Model-View-Control pattern. 5 | 6 | This pattern splits the view (also called the presentation) into "dumb" 7 | templates that are primarily responsible for inserting pre-built data in between 8 | HTML tags. The model contains the "smart" domain objects (such as Account, 9 | Product, Person, Post) that holds all the business logic and knows how to 10 | persist themselves to a database. The controller handles the incoming requests 11 | (such as Save New Account, Update Product, Show Post) by manipulating the model 12 | and directing data to the view. 13 | 14 | In Rails, the model is handled by what's called an object-relational mapping 15 | layer entitled Active Record. This layer allows you to present the data from 16 | database rows as objects and embellish these data objects with business logic 17 | methods. You can read more about Active Record in 18 | link:files/vendor/rails/activerecord/README.html. 19 | 20 | The controller and view are handled by the Action Pack, which handles both 21 | layers by its two parts: Action View and Action Controller. These two layers 22 | are bundled in a single package due to their heavy interdependence. This is 23 | unlike the relationship between the Active Record and Action Pack that is much 24 | more separate. Each of these packages can be used independently outside of 25 | Rails. You can read more about Action Pack in 26 | link:files/vendor/rails/actionpack/README.html. 27 | 28 | 29 | == Getting Started 30 | 31 | 1. At the command prompt, create a new Rails application: 32 | rails new myapp (where myapp is the application name) 33 | 34 | 2. Change directory to myapp and start the web server: 35 | cd myapp; rails server (run with --help for options) 36 | 37 | 3. Go to http://localhost:3000/ and you'll see: 38 | "Welcome aboard: You're riding Ruby on Rails!" 39 | 40 | 4. Follow the guidelines to start developing your application. You can find 41 | the following resources handy: 42 | 43 | * The Getting Started Guide: http://guides.rubyonrails.org/getting_started.html 44 | * Ruby on Rails Tutorial Book: http://www.railstutorial.org/ 45 | 46 | 47 | == Debugging Rails 48 | 49 | Sometimes your application goes wrong. Fortunately there are a lot of tools that 50 | will help you debug it and get it back on the rails. 51 | 52 | First area to check is the application log files. Have "tail -f" commands 53 | running on the server.log and development.log. Rails will automatically display 54 | debugging and runtime information to these files. Debugging info will also be 55 | shown in the browser on requests from 127.0.0.1. 56 | 57 | You can also log your own messages directly into the log file from your code 58 | using the Ruby logger class from inside your controllers. Example: 59 | 60 | class WeblogController < ActionController::Base 61 | def destroy 62 | @weblog = Weblog.find(params[:id]) 63 | @weblog.destroy 64 | logger.info("#{Time.now} Destroyed Weblog ID ##{@weblog.id}!") 65 | end 66 | end 67 | 68 | The result will be a message in your log file along the lines of: 69 | 70 | Mon Oct 08 14:22:29 +1000 2007 Destroyed Weblog ID #1! 71 | 72 | More information on how to use the logger is at http://www.ruby-doc.org/core/ 73 | 74 | Also, Ruby documentation can be found at http://www.ruby-lang.org/. There are 75 | several books available online as well: 76 | 77 | * Programming Ruby: http://www.ruby-doc.org/docs/ProgrammingRuby/ (Pickaxe) 78 | * Learn to Program: http://pine.fm/LearnToProgram/ (a beginners guide) 79 | 80 | These two books will bring you up to speed on the Ruby language and also on 81 | programming in general. 82 | 83 | 84 | == Debugger 85 | 86 | Debugger support is available through the debugger command when you start your 87 | Mongrel or WEBrick server with --debugger. This means that you can break out of 88 | execution at any point in the code, investigate and change the model, and then, 89 | resume execution! You need to install ruby-debug to run the server in debugging 90 | mode. With gems, use sudo gem install ruby-debug. Example: 91 | 92 | class WeblogController < ActionController::Base 93 | def index 94 | @posts = Post.all 95 | debugger 96 | end 97 | end 98 | 99 | So the controller will accept the action, run the first line, then present you 100 | with a IRB prompt in the server window. Here you can do things like: 101 | 102 | >> @posts.inspect 103 | => "[#nil, "body"=>nil, "id"=>"1"}>, 105 | #"Rails", "body"=>"Only ten..", "id"=>"2"}>]" 107 | >> @posts.first.title = "hello from a debugger" 108 | => "hello from a debugger" 109 | 110 | ...and even better, you can examine how your runtime objects actually work: 111 | 112 | >> f = @posts.first 113 | => #nil, "body"=>nil, "id"=>"1"}> 114 | >> f. 115 | Display all 152 possibilities? (y or n) 116 | 117 | Finally, when you're ready to resume execution, you can enter "cont". 118 | 119 | 120 | == Console 121 | 122 | The console is a Ruby shell, which allows you to interact with your 123 | application's domain model. Here you'll have all parts of the application 124 | configured, just like it is when the application is running. You can inspect 125 | domain models, change values, and save to the database. Starting the script 126 | without arguments will launch it in the development environment. 127 | 128 | To start the console, run rails console from the application 129 | directory. 130 | 131 | Options: 132 | 133 | * Passing the -s, --sandbox argument will rollback any modifications 134 | made to the database. 135 | * Passing an environment name as an argument will load the corresponding 136 | environment. Example: rails console production. 137 | 138 | To reload your controllers and models after launching the console run 139 | reload! 140 | 141 | More information about irb can be found at: 142 | link:http://www.rubycentral.org/pickaxe/irb.html 143 | 144 | 145 | == dbconsole 146 | 147 | You can go to the command line of your database directly through rails 148 | dbconsole. You would be connected to the database with the credentials 149 | defined in database.yml. Starting the script without arguments will connect you 150 | to the development database. Passing an argument will connect you to a different 151 | database, like rails dbconsole production. Currently works for MySQL, 152 | PostgreSQL and SQLite 3. 153 | 154 | == Description of Contents 155 | 156 | The default directory structure of a generated Ruby on Rails application: 157 | 158 | |-- app 159 | | |-- assets 160 | | |-- images 161 | | |-- javascripts 162 | | `-- stylesheets 163 | | |-- controllers 164 | | |-- helpers 165 | | |-- mailers 166 | | |-- models 167 | | `-- views 168 | | `-- layouts 169 | |-- config 170 | | |-- environments 171 | | |-- initializers 172 | | `-- locales 173 | |-- db 174 | |-- doc 175 | |-- lib 176 | | `-- tasks 177 | |-- log 178 | |-- public 179 | |-- script 180 | |-- test 181 | | |-- fixtures 182 | | |-- functional 183 | | |-- integration 184 | | |-- performance 185 | | `-- unit 186 | |-- tmp 187 | | |-- cache 188 | | |-- pids 189 | | |-- sessions 190 | | `-- sockets 191 | `-- vendor 192 | |-- assets 193 | `-- stylesheets 194 | `-- plugins 195 | 196 | app 197 | Holds all the code that's specific to this particular application. 198 | 199 | app/assets 200 | Contains subdirectories for images, stylesheets, and JavaScript files. 201 | 202 | app/controllers 203 | Holds controllers that should be named like weblogs_controller.rb for 204 | automated URL mapping. All controllers should descend from 205 | ApplicationController which itself descends from ActionController::Base. 206 | 207 | app/models 208 | Holds models that should be named like post.rb. Models descend from 209 | ActiveRecord::Base by default. 210 | 211 | app/views 212 | Holds the template files for the view that should be named like 213 | weblogs/index.html.erb for the WeblogsController#index action. All views use 214 | eRuby syntax by default. 215 | 216 | app/views/layouts 217 | Holds the template files for layouts to be used with views. This models the 218 | common header/footer method of wrapping views. In your views, define a layout 219 | using the layout :default and create a file named default.html.erb. 220 | Inside default.html.erb, call <% yield %> to render the view using this 221 | layout. 222 | 223 | app/helpers 224 | Holds view helpers that should be named like weblogs_helper.rb. These are 225 | generated for you automatically when using generators for controllers. 226 | Helpers can be used to wrap functionality for your views into methods. 227 | 228 | config 229 | Configuration files for the Rails environment, the routing map, the database, 230 | and other dependencies. 231 | 232 | db 233 | Contains the database schema in schema.rb. db/migrate contains all the 234 | sequence of Migrations for your schema. 235 | 236 | doc 237 | This directory is where your application documentation will be stored when 238 | generated using rake doc:app 239 | 240 | lib 241 | Application specific libraries. Basically, any kind of custom code that 242 | doesn't belong under controllers, models, or helpers. This directory is in 243 | the load path. 244 | 245 | public 246 | The directory available for the web server. Also contains the dispatchers and the 247 | default HTML files. This should be set as the DOCUMENT_ROOT of your web 248 | server. 249 | 250 | script 251 | Helper scripts for automation and generation. 252 | 253 | test 254 | Unit and functional tests along with fixtures. When using the rails generate 255 | command, template test files will be generated for you and placed in this 256 | directory. 257 | 258 | vendor 259 | External libraries that the application depends on. Also includes the plugins 260 | subdirectory. If the app has frozen rails, those gems also go here, under 261 | vendor/rails/. This directory is in the load path. 262 | -------------------------------------------------------------------------------- /spec/dummy/Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | # Add your own tasks in files placed in lib/tasks ending in .rake, 3 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 4 | 5 | require File.expand_path('../config/application', __FILE__) 6 | 7 | Dummy::Application.load_tasks 8 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link_directory ../javascripts .js 3 | //= link_directory ../stylesheets .css -------------------------------------------------------------------------------- /spec/dummy/app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // the compiled file. 9 | // 10 | // WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD 11 | // GO AFTER THE REQUIRES BELOW. 12 | // 13 | //= require_tree . 14 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the top of the 9 | * compiled file, but it's generally better to create a new file per style scope. 10 | * 11 | *= require_self 12 | *= require_tree . 13 | */ 14 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | protect_from_forgery 3 | end 4 | -------------------------------------------------------------------------------- /spec/dummy/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /spec/dummy/app/mailers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/judgegem/judge/99505d172e6c036d900c5a30db49864aa9d68c91/spec/dummy/app/mailers/.gitkeep -------------------------------------------------------------------------------- /spec/dummy/app/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/judgegem/judge/99505d172e6c036d900c5a30db49864aa9d68c91/spec/dummy/app/models/.gitkeep -------------------------------------------------------------------------------- /spec/dummy/app/models/category.rb: -------------------------------------------------------------------------------- 1 | class Category < ActiveRecord::Base 2 | has_many :sports 3 | end -------------------------------------------------------------------------------- /spec/dummy/app/models/discipline.rb: -------------------------------------------------------------------------------- 1 | class Discipline < ActiveRecord::Base 2 | belongs_to :sport 3 | belongs_to :user 4 | 5 | validates :sport, presence: true 6 | end -------------------------------------------------------------------------------- /spec/dummy/app/models/sport.rb: -------------------------------------------------------------------------------- 1 | class Sport < ActiveRecord::Base 2 | belongs_to :category 3 | has_many :disciplines 4 | end -------------------------------------------------------------------------------- /spec/dummy/app/models/team.rb: -------------------------------------------------------------------------------- 1 | class Team < ActiveRecord::Base; end -------------------------------------------------------------------------------- /spec/dummy/app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ActiveRecord::Base 2 | belongs_to :team 3 | belongs_to :discipline 4 | 5 | validates :name, :presence => true 6 | validates :username, :length => { :maximum => 10 }, :uniqueness => true 7 | validates :country, :format => { :with => /[A-Za-z]/, :allow_blank => true } 8 | validates :age, :numericality => { :only_integer => true, :greater_than => 13 } 9 | validates :bio, :presence => true 10 | validates :password, :format => { :with => /.+/ }, :confirmation => true 11 | validates :accepted, :acceptance => true 12 | validates :gender, :inclusion => { :in => ["male", "female", "other", "withheld"] } 13 | validates :dob, :presence => true 14 | validates :team_id, :presence => true 15 | validates :time_zone, :presence => true 16 | validates :discipline_id, :presence => true 17 | validates :city, :city => true 18 | validates :name, :length => { :maximum => 10 }, :if => Proc.new { false } 19 | validates :bio, :uniqueness => true 20 | validates :country, :uniqueness => { :case_sensitive => false } 21 | validates :dob, :uniqueness => true, :unless => Proc.new { true } 22 | validates :team_id, :numericality => { :only_integer => true, :judge => :ignore } 23 | validates :discipline_id, :uniqueness => { :judge => :force }, :if => Proc.new { false } 24 | validates :time_zone, :presence => { :judge => :ignore } 25 | validates :gender, :presence => { :judge => :unknown_option } 26 | validates :telephone, :numericality => { :only_integer => true } 27 | end 28 | -------------------------------------------------------------------------------- /spec/dummy/app/validators/city_validator.rb: -------------------------------------------------------------------------------- 1 | class CityValidator < ActiveModel::EachValidator 2 | uses_messages :not_valid_city 3 | uses_messages :no_towns 4 | 5 | def validate_each(record, attribute, value) 6 | unless ["London", "New York City"].include? value 7 | record.errors.add attribute, :not_valid_city 8 | end 9 | record.errors.add(attribute, :no_towns) if value == "Ipswich" 10 | end 11 | end -------------------------------------------------------------------------------- /spec/dummy/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dummy 5 | <%= stylesheet_link_tag "application", :media => "all" %> 6 | <%= javascript_include_tag "application" %> 7 | <%= csrf_meta_tags %> 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /spec/dummy/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Dummy::Application 5 | -------------------------------------------------------------------------------- /spec/dummy/config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | # Pick the frameworks you want: 4 | require "active_record/railtie" 5 | require "action_controller/railtie" 6 | require "action_mailer/railtie" 7 | require "sprockets/railtie" 8 | 9 | Bundler.require 10 | require "judge" 11 | 12 | module Dummy 13 | class Application < Rails::Application 14 | # Settings in config/environments/* take precedence over those specified here. 15 | # Application configuration should go into files in config/initializers 16 | # -- all .rb files in that directory are automatically loaded. 17 | 18 | # Custom directories with classes and modules you want to be autoloadable. 19 | # config.autoload_paths += %W(#{config.root}/extras) 20 | 21 | # Only load the plugins named here, in the order given (default is alphabetical). 22 | # :all can be used as a placeholder for all plugins not explicitly named. 23 | # config.plugins = [ :exception_notification, :ssl_requirement, :all ] 24 | 25 | # Activate observers that should always be running. 26 | # config.active_record.observers = :cacher, :garbage_collector, :forum_observer 27 | 28 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 29 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 30 | # config.time_zone = 'Central Time (US & Canada)' 31 | 32 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 33 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 34 | # config.i18n.default_locale = :de 35 | 36 | # Configure the default encoding used in templates for Ruby 1.9. 37 | config.encoding = "utf-8" 38 | 39 | # Configure sensitive parameters which will be filtered from the log file. 40 | config.filter_parameters += [:password] 41 | 42 | # Use SQL instead of Active Record's schema dumper when creating the database. 43 | # This is necessary if your schema can't be completely dumped by the schema dumper, 44 | # like if you have constraints or database-specific column types 45 | # config.active_record.schema_format = :sql 46 | 47 | # Enforce whitelist mode for mass assignment. 48 | # This will create an empty whitelist of attributes available for mass-assignment for all models 49 | # in your app. As such, your models will need to explicitly whitelist or blacklist accessible 50 | # parameters by using an attr_accessible or attr_protected declaration. 51 | # config.active_record.whitelist_attributes = true 52 | 53 | # Enable the asset pipeline 54 | config.assets.enabled = true 55 | 56 | # Version of your assets, change this if you want to expire all your assets 57 | config.assets.version = '1.0' 58 | 59 | config.i18n.enforce_available_locales = false if Rails::VERSION::MAJOR >= 4 60 | end 61 | end 62 | 63 | -------------------------------------------------------------------------------- /spec/dummy/config/boot.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | gemfile = File.expand_path('../../../../Gemfile', __FILE__) 3 | 4 | if File.exist?(gemfile) 5 | ENV['BUNDLE_GEMFILE'] = gemfile 6 | require 'bundler' 7 | Bundler.setup 8 | end 9 | 10 | $:.unshift File.expand_path('../../../../lib', __FILE__) -------------------------------------------------------------------------------- /spec/dummy/config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite version 3.x 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem 'sqlite3' 6 | development: 7 | adapter: sqlite3 8 | database: db/development.sqlite3 9 | pool: 5 10 | timeout: 5000 11 | 12 | # Warning: The database defined as "test" will be erased and 13 | # re-generated from your development database when you run "rake". 14 | # Do not set this db to the same as development or production. 15 | test: 16 | adapter: sqlite3 17 | database: db/test.sqlite3 18 | pool: 5 19 | timeout: 5000 20 | 21 | production: 22 | adapter: sqlite3 23 | database: db/production.sqlite3 24 | pool: 5 25 | timeout: 5000 26 | -------------------------------------------------------------------------------- /spec/dummy/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the rails application 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the rails application 5 | Dummy::Application.initialize! 6 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Log error messages when you accidentally call methods on nil. 10 | config.whiny_nils = true 11 | 12 | # Show full error reports and disable caching 13 | config.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Don't care if the mailer can't send 17 | config.action_mailer.raise_delivery_errors = false 18 | 19 | # Print deprecation notices to the Rails logger 20 | config.active_support.deprecation = :log 21 | 22 | # Only use best-standards-support built into browsers 23 | config.action_dispatch.best_standards_support = :builtin 24 | 25 | # Raise exception on mass assignment protection for Active Record models 26 | config.active_record.mass_assignment_sanitizer = :strict 27 | 28 | # Log the query plan for queries taking more than this (works 29 | # with SQLite, MySQL, and PostgreSQL) 30 | config.active_record.auto_explain_threshold_in_seconds = 0.5 31 | 32 | # Do not compress assets 33 | config.assets.compress = false 34 | 35 | # Expands the lines which load the assets 36 | config.assets.debug = true 37 | end 38 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # Code is not reloaded between requests 5 | config.cache_classes = true 6 | 7 | # Full error reports are disabled and caching is turned on 8 | config.consider_all_requests_local = false 9 | config.action_controller.perform_caching = true 10 | 11 | # Disable Rails's static asset server (Apache or nginx will already do this) 12 | config.serve_static_assets = false 13 | 14 | # Compress JavaScripts and CSS 15 | config.assets.compress = true 16 | 17 | # Don't fallback to assets pipeline if a precompiled asset is missed 18 | config.assets.compile = false 19 | 20 | # Generate digests for assets URLs 21 | config.assets.digest = true 22 | 23 | # Defaults to Rails.root.join("public/assets") 24 | # config.assets.manifest = YOUR_PATH 25 | 26 | # Specifies the header that your server uses for sending files 27 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache 28 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx 29 | 30 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 31 | # config.force_ssl = true 32 | 33 | # See everything in the log (default is :info) 34 | # config.log_level = :debug 35 | 36 | # Prepend all log lines with the following tags 37 | # config.log_tags = [ :subdomain, :uuid ] 38 | 39 | # Use a different logger for distributed setups 40 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) 41 | 42 | # Use a different cache store in production 43 | # config.cache_store = :mem_cache_store 44 | 45 | # Enable serving of images, stylesheets, and JavaScripts from an asset server 46 | # config.action_controller.asset_host = "http://assets.example.com" 47 | 48 | # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added) 49 | # config.assets.precompile += %w( search.js ) 50 | 51 | # Disable delivery errors, bad email addresses will be ignored 52 | # config.action_mailer.raise_delivery_errors = false 53 | 54 | # Enable threaded mode 55 | # config.threadsafe! 56 | 57 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 58 | # the I18n.default_locale when a translation can not be found) 59 | config.i18n.fallbacks = true 60 | 61 | # Send deprecation notices to registered listeners 62 | config.active_support.deprecation = :notify 63 | 64 | # Log the query plan for queries taking more than this (works 65 | # with SQLite, MySQL, and PostgreSQL) 66 | # config.active_record.auto_explain_threshold_in_seconds = 0.5 67 | end 68 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Configure static asset server for tests with Cache-Control for performance 11 | config.serve_static_files = true 12 | # config.static_cache_control = "public, max-age=3600" 13 | 14 | # Log error messages when you accidentally call methods on nil 15 | # config.whiny_nils = true 16 | 17 | # Show full error reports and disable caching 18 | config.consider_all_requests_local = true 19 | config.action_controller.perform_caching = false 20 | 21 | # Raise exceptions instead of rendering exception templates 22 | config.action_dispatch.show_exceptions = false 23 | 24 | # Disable request forgery protection in test environment 25 | config.action_controller.allow_forgery_protection = false 26 | 27 | # Tell Action Mailer not to deliver emails to the real world. 28 | # The :test delivery method accumulates sent emails in the 29 | # ActionMailer::Base.deliveries array. 30 | config.action_mailer.delivery_method = :test 31 | 32 | # Print deprecation notices to the stderr 33 | config.active_support.deprecation = :stderr 34 | 35 | config.eager_load = true 36 | end 37 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format 4 | # (all these examples are active by default): 5 | # ActiveSupport::Inflector.inflections do |inflect| 6 | # inflect.plural /^(ox)$/i, '\1en' 7 | # inflect.singular /^(ox)en/i, '\1' 8 | # inflect.irregular 'person', 'people' 9 | # inflect.uncountable %w( fish sheep ) 10 | # end 11 | # 12 | # These inflection rules are supported but not enabled by default: 13 | # ActiveSupport::Inflector.inflections do |inflect| 14 | # inflect.acronym 'RESTful' 15 | # end 16 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | # Mime::Type.register_alias "text/html", :iphone 6 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/secret_token.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | # Make sure the secret is at least 30 characters and all random, 6 | # no regular words or you'll be exposed to dictionary attacks. 7 | Dummy::Application.config.secret_token = 'f325ffc39dad5f30fbfff123bc30834a6b1bb36034d95160dd630383c4b63f096a75b7a1fd9ba4aeaca20646ea2b9b0ab9cc1861e2daad3f9f7d5fa578d8e193' 8 | Dummy::Application.config.secret_key_base = 'abc123' 9 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Dummy::Application.config.session_store :cookie_store, key: '_dummy_session' 4 | 5 | # Use the database for sessions instead of the cookie-based default, 6 | # which shouldn't be used to store highly confidential information 7 | # (create the session table with "rails generate session_migration") 8 | # Dummy::Application.config.session_store :active_record_store 9 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | # 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # Disable root element in JSON by default. 12 | ActiveSupport.on_load(:active_record) do 13 | self.include_root_in_json = false 14 | end 15 | -------------------------------------------------------------------------------- /spec/dummy/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | activerecord: 3 | errors: 4 | models: 5 | user: 6 | attributes: 7 | username: 8 | taken: "%{attribute} \"%{value}\" has already been taken" 9 | city: 10 | not_valid_city: "%{attribute} must be an approved city" 11 | no_towns: "%{attribute} can't be a town" 12 | errors: 13 | attributes: 14 | city: 15 | not_valid_city: "This is never reached" 16 | 17 | messages: 18 | not_an_integer: "%{attribute} must be an integer" 19 | blank: "%{attribute} must not be blank" 20 | greater_than: "%{attribute} must be greater than %{count}" 21 | too_long: "%{attribute} is too long (must be less than %{count} characters)" -------------------------------------------------------------------------------- /spec/dummy/config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | mount Judge::Engine => "/judge" 3 | end 4 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20120426221242_create_users.rb: -------------------------------------------------------------------------------- 1 | class CreateUsers < ActiveRecord::Migration[5.2] 2 | def change 3 | create_table :users do |t| 4 | t.string :name 5 | t.string :username 6 | t.string :country 7 | t.integer :age 8 | t.text :bio 9 | t.string :password 10 | t.boolean :accepted 11 | t.text :gender 12 | t.date :dob 13 | t.integer :team_id 14 | t.string :time_zone 15 | t.integer :discipline_id 16 | t.string :city 17 | t.string :telephone 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20120426221441_create_teams.rb: -------------------------------------------------------------------------------- 1 | class CreateTeams < ActiveRecord::Migration[5.2] 2 | def change 3 | create_table :teams do |t| 4 | t.string :name 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20120426221448_create_categories.rb: -------------------------------------------------------------------------------- 1 | class CreateCategories < ActiveRecord::Migration[5.2] 2 | def change 3 | create_table :categories do |t| 4 | t.string :name 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20120426221455_create_sports.rb: -------------------------------------------------------------------------------- 1 | class CreateSports < ActiveRecord::Migration[5.2] 2 | def change 3 | create_table :sports do |t| 4 | t.string :name 5 | t.integer :category_id 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20120426221506_create_disciplines.rb: -------------------------------------------------------------------------------- 1 | class CreateDisciplines < ActiveRecord::Migration[5.2] 2 | def change 3 | create_table :disciplines do |t| 4 | t.string :name 5 | t.integer :sport_id 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/dummy/db/schema.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # This file is auto-generated from the current state of the database. Instead 3 | # of editing this file, please use the migrations feature of Active Record to 4 | # incrementally modify your database, and then regenerate this schema definition. 5 | # 6 | # Note that this schema.rb definition is the authoritative source for your 7 | # database schema. If you need to create the application database on another 8 | # system, you should be using db:schema:load, not running all the migrations 9 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 10 | # you'll amass, the slower it'll run and the greater likelihood for issues). 11 | # 12 | # It's strongly recommended to check this file into your version control system. 13 | 14 | ActiveRecord::Schema.define(:version => 20120426221506) do 15 | 16 | create_table "categories", :force => true do |t| 17 | t.string "name" 18 | end 19 | 20 | create_table "disciplines", :force => true do |t| 21 | t.string "name" 22 | t.integer "sport_id" 23 | end 24 | 25 | create_table "sports", :force => true do |t| 26 | t.string "name" 27 | t.integer "category_id" 28 | end 29 | 30 | create_table "teams", :force => true do |t| 31 | t.string "name" 32 | end 33 | 34 | create_table "users", :force => true do |t| 35 | t.string "name" 36 | t.string "username" 37 | t.string "country" 38 | t.integer "age" 39 | t.text "bio" 40 | t.string "password" 41 | t.boolean "accepted" 42 | t.text "gender" 43 | t.date "dob" 44 | t.integer "team_id" 45 | t.string "time_zone" 46 | t.integer "discipline_id" 47 | t.string "city" 48 | t.string "telephone" 49 | end 50 | 51 | end 52 | -------------------------------------------------------------------------------- /spec/dummy/lib/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/judgegem/judge/99505d172e6c036d900c5a30db49864aa9d68c91/spec/dummy/lib/assets/.gitkeep -------------------------------------------------------------------------------- /spec/dummy/log/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/judgegem/judge/99505d172e6c036d900c5a30db49864aa9d68c91/spec/dummy/log/.gitkeep -------------------------------------------------------------------------------- /spec/dummy/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

The page you were looking for doesn't exist.

23 |

You may have mistyped the address or the page may have moved.

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /spec/dummy/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

The change you wanted was rejected.

23 |

Maybe you tried to change something you didn't have access to.

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /spec/dummy/public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

We're sorry, but something went wrong.

23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /spec/dummy/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/judgegem/judge/99505d172e6c036d900c5a30db49864aa9d68c91/spec/dummy/public/favicon.ico -------------------------------------------------------------------------------- /spec/dummy/script/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. 3 | 4 | APP_PATH = File.expand_path('../../config/application', __FILE__) 5 | require File.expand_path('../../config/boot', __FILE__) 6 | require 'rails/commands' 7 | -------------------------------------------------------------------------------- /spec/each_validator_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Judge::EachValidator do 4 | 5 | let(:amv) { User.validators_on(:city).first } 6 | 7 | specify { CityValidator.include?(Judge::EachValidator).should be_truthy } 8 | specify { amv.should respond_to :messages_to_lookup } 9 | specify { amv.messages_to_lookup.should be_a Set } 10 | specify { amv.messages_to_lookup.should include :not_valid_city } 11 | specify { amv.messages_to_lookup.should include :no_towns } 12 | 13 | end 14 | -------------------------------------------------------------------------------- /spec/factories/factories.rb: -------------------------------------------------------------------------------- 1 | FactoryGirl.define do 2 | factory :user do 3 | dob { Time.new(2011,11,5, 17,00,00) } 4 | sequence(:name) { |n| "User #{n}" } 5 | age 40 6 | bio "I'm a user" 7 | sequence(:password) { |n| "password_#{n}" } 8 | gender { ["male", "female", "other", "withheld"].sample } 9 | city "London" 10 | time_zone "London" 11 | discipline 12 | team 13 | telephone{ rand(10**9..10**10).to_s } 14 | end 15 | 16 | factory :team do 17 | sequence(:name) {|n| "Team #{n}" } 18 | end 19 | 20 | factory :category do 21 | sequence(:name) {|n| "Category #{n}" } 22 | end 23 | 24 | factory :sport do 25 | sequence(:name) {|n| "Sport #{n}" } 26 | category 27 | end 28 | 29 | factory :discipline do 30 | sequence(:name) {|n| "Discipline #{n}" } 31 | sport 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/form_builder_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Judge::FormBuilder do 4 | 5 | let(:builder) do 6 | args = [:user, FactoryGirl.build(:user), ActionView::Base.new, {}] 7 | args << nil if Rails::VERSION::MAJOR == 3 8 | Judge::FormBuilder.new(*args) 9 | end 10 | let(:categories) do 11 | category = FactoryGirl.build(:category) 12 | sport = FactoryGirl.build(:sport) 13 | sport.disciplines << FactoryGirl.build_list(:discipline, 3) 14 | category.sports << sport 15 | [category] 16 | end 17 | let(:expected) do 18 | /data\-validate=\"\[.+\]\"/ 19 | end 20 | 21 | specify "#text_field" do 22 | builder.text_field(:name, :validate => true).should match expected 23 | end 24 | 25 | specify "#text_area" do 26 | builder.text_area(:bio, :validate => true).should match expected 27 | end 28 | 29 | specify "#password_field" do 30 | builder.password_field(:password, :validate => true).should match expected 31 | end 32 | 33 | specify "#check_box" do 34 | builder.check_box(:accepted, :validate => true).should match expected 35 | end 36 | 37 | specify "#radio_button" do 38 | builder.radio_button(:gender, "female", :validate => true).should match expected 39 | end 40 | 41 | specify "#select" do 42 | builder.select(:country, [["US", "US"], ["GB", "GB"]], :validate => true).should match expected 43 | end 44 | 45 | specify "#collection_select" do 46 | cs = builder.collection_select(:team_id, FactoryGirl.create_list(:team, 5), :id, :name, :validate => true) 47 | cs.should match expected 48 | end 49 | 50 | specify "#grouped_collection_select" do 51 | gcs = builder.grouped_collection_select(:discipline_id, categories, :sports, :name, :id, :name, :validate => true) 52 | gcs.should match expected 53 | end 54 | 55 | specify "#date_select" do 56 | builder.date_select(:dob, :validate => true, :minute_step => 30).should match expected 57 | end 58 | 59 | specify "#datetime_select" do 60 | builder.datetime_select(:dob, :validate => true, :minute_step => 30).should match expected 61 | end 62 | 63 | specify "#time_select" do 64 | builder.time_select(:dob, :validate => true, :minute_step => 30).should match expected 65 | end 66 | 67 | specify "#time_zone_select" do 68 | tzs = builder.time_zone_select(:time_zone, ActiveSupport::TimeZone.us_zones, :include_blank => true, :validate => true) 69 | tzs.should match expected 70 | end 71 | 72 | specify "#email_field" do 73 | builder.email_field(:username, :validate => true).should match expected 74 | end 75 | 76 | specify "#telephone_field" do 77 | builder.telephone_field(:telephone, :validate => true).should match expected 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /spec/javascripts/helpers/customMatchers.js: -------------------------------------------------------------------------------- 1 | var customMatchers = (function() { 2 | var hasStatus = function(validation, status) { 3 | return (validation instanceof judge.Validation) && 4 | (validation.status() === status) 5 | }, 6 | matchers = { 7 | toBeInstanceOf: function(instanceType) { 8 | return this.actual instanceof instanceType; 9 | }, 10 | toBePending: function() { 11 | return hasStatus(this.actual, 'pending'); 12 | }, 13 | toBeValid: function() { 14 | return hasStatus(this.actual, 'valid'); 15 | }, 16 | toBeInvalid: function() { 17 | return hasStatus(this.actual, 'invalid'); 18 | }, 19 | toBeInvalidWith: function(messages) { 20 | return hasStatus(this.actual, 'invalid') && 21 | _.isEqual(this.actual.messages, messages); 22 | } 23 | }; 24 | 25 | return matchers; 26 | })(); 27 | 28 | -------------------------------------------------------------------------------- /spec/javascripts/helpers/jasmine-jquery.js: -------------------------------------------------------------------------------- 1 | var readFixtures = function() { 2 | return jasmine.getFixtures().proxyCallTo_('read', arguments) 3 | } 4 | 5 | var preloadFixtures = function() { 6 | jasmine.getFixtures().proxyCallTo_('preload', arguments) 7 | } 8 | 9 | var loadFixtures = function() { 10 | jasmine.getFixtures().proxyCallTo_('load', arguments) 11 | } 12 | 13 | var appendLoadFixtures = function() { 14 | jasmine.getFixtures().proxyCallTo_('appendLoad', arguments) 15 | } 16 | 17 | var setFixtures = function(html) { 18 | jasmine.getFixtures().proxyCallTo_('set', arguments) 19 | } 20 | 21 | var appendSetFixtures = function() { 22 | jasmine.getFixtures().proxyCallTo_('appendSet', arguments) 23 | } 24 | 25 | var sandbox = function(attributes) { 26 | return jasmine.getFixtures().sandbox(attributes) 27 | } 28 | 29 | var spyOnEvent = function(selector, eventName) { 30 | return jasmine.JQuery.events.spyOn(selector, eventName) 31 | } 32 | 33 | var preloadStyleFixtures = function() { 34 | jasmine.getStyleFixtures().proxyCallTo_('preload', arguments) 35 | } 36 | 37 | var loadStyleFixtures = function() { 38 | jasmine.getStyleFixtures().proxyCallTo_('load', arguments) 39 | } 40 | 41 | var appendLoadStyleFixtures = function() { 42 | jasmine.getStyleFixtures().proxyCallTo_('appendLoad', arguments) 43 | } 44 | 45 | var setStyleFixtures = function(html) { 46 | jasmine.getStyleFixtures().proxyCallTo_('set', arguments) 47 | } 48 | 49 | var appendSetStyleFixtures = function(html) { 50 | jasmine.getStyleFixtures().proxyCallTo_('appendSet', arguments) 51 | } 52 | 53 | var loadJSONFixtures = function() { 54 | return jasmine.getJSONFixtures().proxyCallTo_('load', arguments) 55 | } 56 | 57 | var getJSONFixture = function(url) { 58 | return jasmine.getJSONFixtures().proxyCallTo_('read', arguments)[url] 59 | } 60 | 61 | jasmine.spiedEventsKey = function (selector, eventName) { 62 | return [$(selector).selector, eventName].toString() 63 | } 64 | 65 | jasmine.getFixtures = function() { 66 | return jasmine.currentFixtures_ = jasmine.currentFixtures_ || new jasmine.Fixtures() 67 | } 68 | 69 | jasmine.getStyleFixtures = function() { 70 | return jasmine.currentStyleFixtures_ = jasmine.currentStyleFixtures_ || new jasmine.StyleFixtures() 71 | } 72 | 73 | jasmine.Fixtures = function() { 74 | this.containerId = 'jasmine-fixtures' 75 | this.fixturesCache_ = {} 76 | this.fixturesPath = 'spec/javascripts/fixtures' 77 | } 78 | 79 | jasmine.Fixtures.prototype.set = function(html) { 80 | this.cleanUp() 81 | this.createContainer_(html) 82 | } 83 | 84 | jasmine.Fixtures.prototype.appendSet= function(html) { 85 | this.addToContainer_(html) 86 | } 87 | 88 | jasmine.Fixtures.prototype.preload = function() { 89 | this.read.apply(this, arguments) 90 | } 91 | 92 | jasmine.Fixtures.prototype.load = function() { 93 | this.cleanUp() 94 | this.createContainer_(this.read.apply(this, arguments)) 95 | } 96 | 97 | jasmine.Fixtures.prototype.appendLoad = function() { 98 | this.addToContainer_(this.read.apply(this, arguments)) 99 | } 100 | 101 | jasmine.Fixtures.prototype.read = function() { 102 | var htmlChunks = [] 103 | 104 | var fixtureUrls = arguments 105 | for(var urlCount = fixtureUrls.length, urlIndex = 0; urlIndex < urlCount; urlIndex++) { 106 | htmlChunks.push(this.getFixtureHtml_(fixtureUrls[urlIndex])) 107 | } 108 | 109 | return htmlChunks.join('') 110 | } 111 | 112 | jasmine.Fixtures.prototype.clearCache = function() { 113 | this.fixturesCache_ = {} 114 | } 115 | 116 | jasmine.Fixtures.prototype.cleanUp = function() { 117 | $('#' + this.containerId).remove() 118 | } 119 | 120 | jasmine.Fixtures.prototype.sandbox = function(attributes) { 121 | var attributesToSet = attributes || {} 122 | return $('
').attr(attributesToSet) 123 | } 124 | 125 | jasmine.Fixtures.prototype.createContainer_ = function(html) { 126 | var container 127 | if(html instanceof $) { 128 | container = $('
') 129 | container.html(html) 130 | } else { 131 | container = '
' + html + '
' 132 | } 133 | $('body').append(container) 134 | } 135 | 136 | jasmine.Fixtures.prototype.addToContainer_ = function(html){ 137 | var container = $('body').find('#'+this.containerId).append(html) 138 | if(!container.length){ 139 | this.createContainer_(html) 140 | } 141 | } 142 | 143 | jasmine.Fixtures.prototype.getFixtureHtml_ = function(url) { 144 | if (typeof this.fixturesCache_[url] === 'undefined') { 145 | this.loadFixtureIntoCache_(url) 146 | } 147 | return this.fixturesCache_[url] 148 | } 149 | 150 | jasmine.Fixtures.prototype.loadFixtureIntoCache_ = function(relativeUrl) { 151 | var url = this.makeFixtureUrl_(relativeUrl) 152 | var request = $.ajax({ 153 | type: "GET", 154 | url: url + "?" + new Date().getTime(), 155 | async: false 156 | }) 157 | this.fixturesCache_[relativeUrl] = request.responseText 158 | } 159 | 160 | jasmine.Fixtures.prototype.makeFixtureUrl_ = function(relativeUrl){ 161 | return this.fixturesPath.match('/$') ? this.fixturesPath + relativeUrl : this.fixturesPath + '/' + relativeUrl 162 | } 163 | 164 | jasmine.Fixtures.prototype.proxyCallTo_ = function(methodName, passedArguments) { 165 | return this[methodName].apply(this, passedArguments) 166 | } 167 | 168 | 169 | jasmine.StyleFixtures = function() { 170 | this.fixturesCache_ = {} 171 | this.fixturesNodes_ = [] 172 | this.fixturesPath = 'spec/javascripts/fixtures' 173 | } 174 | 175 | jasmine.StyleFixtures.prototype.set = function(css) { 176 | this.cleanUp() 177 | this.createStyle_(css) 178 | } 179 | 180 | jasmine.StyleFixtures.prototype.appendSet = function(css) { 181 | this.createStyle_(css) 182 | } 183 | 184 | jasmine.StyleFixtures.prototype.preload = function() { 185 | this.read_.apply(this, arguments) 186 | } 187 | 188 | jasmine.StyleFixtures.prototype.load = function() { 189 | this.cleanUp() 190 | this.createStyle_(this.read_.apply(this, arguments)) 191 | } 192 | 193 | jasmine.StyleFixtures.prototype.appendLoad = function() { 194 | this.createStyle_(this.read_.apply(this, arguments)) 195 | } 196 | 197 | jasmine.StyleFixtures.prototype.cleanUp = function() { 198 | while(this.fixturesNodes_.length) { 199 | this.fixturesNodes_.pop().remove() 200 | } 201 | } 202 | 203 | jasmine.StyleFixtures.prototype.createStyle_ = function(html) { 204 | var styleText = $('
').html(html).text(), 205 | style = $('') 206 | 207 | this.fixturesNodes_.push(style) 208 | 209 | $('head').append(style) 210 | } 211 | 212 | jasmine.StyleFixtures.prototype.clearCache = jasmine.Fixtures.prototype.clearCache 213 | 214 | jasmine.StyleFixtures.prototype.read_ = jasmine.Fixtures.prototype.read 215 | 216 | jasmine.StyleFixtures.prototype.getFixtureHtml_ = jasmine.Fixtures.prototype.getFixtureHtml_ 217 | 218 | jasmine.StyleFixtures.prototype.loadFixtureIntoCache_ = jasmine.Fixtures.prototype.loadFixtureIntoCache_ 219 | 220 | jasmine.StyleFixtures.prototype.makeFixtureUrl_ = jasmine.Fixtures.prototype.makeFixtureUrl_ 221 | 222 | jasmine.StyleFixtures.prototype.proxyCallTo_ = jasmine.Fixtures.prototype.proxyCallTo_ 223 | 224 | jasmine.getJSONFixtures = function() { 225 | return jasmine.currentJSONFixtures_ = jasmine.currentJSONFixtures_ || new jasmine.JSONFixtures() 226 | } 227 | 228 | jasmine.JSONFixtures = function() { 229 | this.fixturesCache_ = {} 230 | this.fixturesPath = 'spec/javascripts/fixtures/json' 231 | } 232 | 233 | jasmine.JSONFixtures.prototype.load = function() { 234 | this.read.apply(this, arguments) 235 | return this.fixturesCache_ 236 | } 237 | 238 | jasmine.JSONFixtures.prototype.read = function() { 239 | var fixtureUrls = arguments 240 | for(var urlCount = fixtureUrls.length, urlIndex = 0; urlIndex < urlCount; urlIndex++) { 241 | this.getFixtureData_(fixtureUrls[urlIndex]) 242 | } 243 | return this.fixturesCache_ 244 | } 245 | 246 | jasmine.JSONFixtures.prototype.clearCache = function() { 247 | this.fixturesCache_ = {} 248 | } 249 | 250 | jasmine.JSONFixtures.prototype.getFixtureData_ = function(url) { 251 | this.loadFixtureIntoCache_(url) 252 | return this.fixturesCache_[url] 253 | } 254 | 255 | jasmine.JSONFixtures.prototype.loadFixtureIntoCache_ = function(relativeUrl) { 256 | var self = this 257 | var url = this.fixturesPath.match('/$') ? this.fixturesPath + relativeUrl : this.fixturesPath + '/' + relativeUrl 258 | $.ajax({ 259 | async: false, // must be synchronous to guarantee that no tests are run before fixture is loaded 260 | cache: false, 261 | dataType: 'json', 262 | url: url, 263 | success: function(data) { 264 | self.fixturesCache_[relativeUrl] = data 265 | }, 266 | error: function(jqXHR, status, errorThrown) { 267 | throw Error('JSONFixture could not be loaded: ' + url + ' (status: ' + status + ', message: ' + errorThrown.message + ')') 268 | } 269 | }) 270 | } 271 | 272 | jasmine.JSONFixtures.prototype.proxyCallTo_ = function(methodName, passedArguments) { 273 | return this[methodName].apply(this, passedArguments) 274 | } 275 | 276 | jasmine.JQuery = function() {} 277 | 278 | jasmine.JQuery.browserTagCaseIndependentHtml = function(html) { 279 | return $('
').append(html).html() 280 | } 281 | 282 | jasmine.JQuery.elementToString = function(element) { 283 | var domEl = $(element).get(0) 284 | if (domEl == undefined || domEl.cloneNode) 285 | return $('
').append($(element).clone()).html() 286 | else 287 | return element.toString() 288 | } 289 | 290 | jasmine.JQuery.matchersClass = {} 291 | 292 | !function(namespace) { 293 | var data = { 294 | spiedEvents: {}, 295 | handlers: [] 296 | } 297 | 298 | namespace.events = { 299 | spyOn: function(selector, eventName) { 300 | var handler = function(e) { 301 | data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)] = e 302 | } 303 | $(selector).bind(eventName, handler) 304 | data.handlers.push(handler) 305 | return { 306 | selector: selector, 307 | eventName: eventName, 308 | handler: handler, 309 | reset: function(){ 310 | delete data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)] 311 | } 312 | } 313 | }, 314 | 315 | wasTriggered: function(selector, eventName) { 316 | return !!(data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)]) 317 | }, 318 | 319 | wasPrevented: function(selector, eventName) { 320 | return data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)].isDefaultPrevented() 321 | }, 322 | 323 | cleanUp: function() { 324 | data.spiedEvents = {} 325 | data.handlers = [] 326 | } 327 | } 328 | }(jasmine.JQuery) 329 | 330 | !function(){ 331 | var jQueryMatchers = { 332 | toHaveClass: function(className) { 333 | return this.actual.hasClass(className) 334 | }, 335 | 336 | toHaveCss: function(css){ 337 | for (var prop in css){ 338 | if (this.actual.css(prop) !== css[prop]) return false 339 | } 340 | return true 341 | }, 342 | 343 | toBeVisible: function() { 344 | return this.actual.is(':visible') 345 | }, 346 | 347 | toBeHidden: function() { 348 | return this.actual.is(':hidden') 349 | }, 350 | 351 | toBeSelected: function() { 352 | return this.actual.is(':selected') 353 | }, 354 | 355 | toBeChecked: function() { 356 | return this.actual.is(':checked') 357 | }, 358 | 359 | toBeEmpty: function() { 360 | return this.actual.is(':empty') 361 | }, 362 | 363 | toExist: function() { 364 | return $(document).find(this.actual).length 365 | }, 366 | 367 | toHaveAttr: function(attributeName, expectedAttributeValue) { 368 | return hasProperty(this.actual.attr(attributeName), expectedAttributeValue) 369 | }, 370 | 371 | toHaveProp: function(propertyName, expectedPropertyValue) { 372 | return hasProperty(this.actual.prop(propertyName), expectedPropertyValue) 373 | }, 374 | 375 | toHaveId: function(id) { 376 | return this.actual.attr('id') == id 377 | }, 378 | 379 | toHaveHtml: function(html) { 380 | return this.actual.html() == jasmine.JQuery.browserTagCaseIndependentHtml(html) 381 | }, 382 | 383 | toContainHtml: function(html){ 384 | var actualHtml = this.actual.html() 385 | var expectedHtml = jasmine.JQuery.browserTagCaseIndependentHtml(html) 386 | return (actualHtml.indexOf(expectedHtml) >= 0) 387 | }, 388 | 389 | toHaveText: function(text) { 390 | var trimmedText = $.trim(this.actual.text()) 391 | if (text && $.isFunction(text.test)) { 392 | return text.test(trimmedText) 393 | } else { 394 | return trimmedText == text 395 | } 396 | }, 397 | 398 | toHaveValue: function(value) { 399 | return this.actual.val() == value 400 | }, 401 | 402 | toHaveData: function(key, expectedValue) { 403 | return hasProperty(this.actual.data(key), expectedValue) 404 | }, 405 | 406 | toBe: function(selector) { 407 | return this.actual.is(selector) 408 | }, 409 | 410 | toContain: function(selector) { 411 | return this.actual.find(selector).length 412 | }, 413 | 414 | toBeDisabled: function(selector){ 415 | return this.actual.is(':disabled') 416 | }, 417 | 418 | toBeFocused: function(selector) { 419 | return this.actual.is(':focus') 420 | }, 421 | 422 | toHandle: function(event) { 423 | 424 | var events = $._data(this.actual.get(0), "events") 425 | 426 | if(!events || !event || typeof event !== "string") { 427 | return false 428 | } 429 | 430 | var namespaces = event.split(".") 431 | var eventType = namespaces.shift() 432 | var sortedNamespaces = namespaces.slice(0).sort() 433 | var namespaceRegExp = new RegExp("(^|\\.)" + sortedNamespaces.join("\\.(?:.*\\.)?") + "(\\.|$)") 434 | 435 | if(events[eventType] && namespaces.length) { 436 | for(var i = 0; i < events[eventType].length; i++) { 437 | var namespace = events[eventType][i].namespace 438 | if(namespaceRegExp.test(namespace)) { 439 | return true 440 | } 441 | } 442 | } else { 443 | return events[eventType] && events[eventType].length > 0 444 | } 445 | }, 446 | 447 | // tests the existence of a specific event binding + handler 448 | toHandleWith: function(eventName, eventHandler) { 449 | var stack = $._data(this.actual.get(0), "events")[eventName] 450 | for (var i = 0; i < stack.length; i++) { 451 | if (stack[i].handler == eventHandler) return true 452 | } 453 | return false 454 | } 455 | } 456 | 457 | var hasProperty = function(actualValue, expectedValue) { 458 | if (expectedValue === undefined) return actualValue !== undefined 459 | return actualValue == expectedValue 460 | } 461 | 462 | var bindMatcher = function(methodName) { 463 | var builtInMatcher = jasmine.Matchers.prototype[methodName] 464 | 465 | jasmine.JQuery.matchersClass[methodName] = function() { 466 | if (this.actual 467 | && (this.actual instanceof $ 468 | || jasmine.isDomNode(this.actual))) { 469 | this.actual = $(this.actual) 470 | var result = jQueryMatchers[methodName].apply(this, arguments) 471 | var element 472 | if (this.actual.get && (element = this.actual.get()[0]) && !$.isWindow(element) && element.tagName !== "HTML") 473 | this.actual = jasmine.JQuery.elementToString(this.actual) 474 | return result 475 | } 476 | 477 | if (builtInMatcher) { 478 | return builtInMatcher.apply(this, arguments) 479 | } 480 | 481 | return false 482 | } 483 | } 484 | 485 | for(var methodName in jQueryMatchers) { 486 | bindMatcher(methodName) 487 | } 488 | }() 489 | 490 | beforeEach(function() { 491 | this.addMatchers(jasmine.JQuery.matchersClass) 492 | this.addMatchers({ 493 | toHaveBeenTriggeredOn: function(selector) { 494 | this.message = function() { 495 | return [ 496 | "Expected event " + this.actual + " to have been triggered on " + selector, 497 | "Expected event " + this.actual + " not to have been triggered on " + selector 498 | ] 499 | } 500 | return jasmine.JQuery.events.wasTriggered(selector, this.actual) 501 | } 502 | }) 503 | this.addMatchers({ 504 | toHaveBeenTriggered: function(){ 505 | var eventName = this.actual.eventName, 506 | selector = this.actual.selector 507 | this.message = function() { 508 | return [ 509 | "Expected event " + eventName + " to have been triggered on " + selector, 510 | "Expected event " + eventName + " not to have been triggered on " + selector 511 | ] 512 | } 513 | return jasmine.JQuery.events.wasTriggered(selector, eventName) 514 | } 515 | }) 516 | this.addMatchers({ 517 | toHaveBeenPreventedOn: function(selector) { 518 | this.message = function() { 519 | return [ 520 | "Expected event " + this.actual + " to have been prevented on " + selector, 521 | "Expected event " + this.actual + " not to have been prevented on " + selector 522 | ] 523 | } 524 | return jasmine.JQuery.events.wasPrevented(selector, this.actual) 525 | } 526 | }) 527 | this.addMatchers({ 528 | toHaveBeenPrevented: function() { 529 | var eventName = this.actual.eventName, 530 | selector = this.actual.selector 531 | this.message = function() { 532 | return [ 533 | "Expected event " + eventName + " to have been prevented on " + selector, 534 | "Expected event " + eventName + " not to have been prevented on " + selector 535 | ] 536 | } 537 | return jasmine.JQuery.events.wasPrevented(selector, eventName) 538 | } 539 | }) 540 | }) 541 | 542 | afterEach(function() { 543 | jasmine.getFixtures().cleanUp() 544 | jasmine.getStyleFixtures().cleanUp() 545 | jasmine.JQuery.events.cleanUp() 546 | }) -------------------------------------------------------------------------------- /spec/javascripts/helpers/sinon-ie.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Sinon.JS 1.5.2, 2012/11/27 3 | * 4 | * @author Christian Johansen (christian@cjohansen.no) 5 | * @author Contributors: https://github.com/cjohansen/Sinon.JS/blob/master/AUTHORS 6 | * 7 | * (The BSD License) 8 | * 9 | * Copyright (c) 2010-2012, Christian Johansen, christian@cjohansen.no 10 | * All rights reserved. 11 | * 12 | * Redistribution and use in source and binary forms, with or without modification, 13 | * are permitted provided that the following conditions are met: 14 | * 15 | * * Redistributions of source code must retain the above copyright notice, 16 | * this list of conditions and the following disclaimer. 17 | * * Redistributions in binary form must reproduce the above copyright notice, 18 | * this list of conditions and the following disclaimer in the documentation 19 | * and/or other materials provided with the distribution. 20 | * * Neither the name of Christian Johansen nor the names of his contributors 21 | * may be used to endorse or promote products derived from this software 22 | * without specific prior written permission. 23 | * 24 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 25 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 26 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 27 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 28 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 29 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 30 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 31 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 32 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 33 | * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 34 | */ 35 | 36 | /*global sinon, setTimeout, setInterval, clearTimeout, clearInterval, Date*/ 37 | /** 38 | * Helps IE run the fake timers. By defining global functions, IE allows 39 | * them to be overwritten at a later point. If these are not defined like 40 | * this, overwriting them will result in anything from an exception to browser 41 | * crash. 42 | * 43 | * If you don't require fake timers to work in IE, don't include this file. 44 | * 45 | * @author Christian Johansen (christian@cjohansen.no) 46 | * @license BSD 47 | * 48 | * Copyright (c) 2010-2011 Christian Johansen 49 | */ 50 | function setTimeout() {} 51 | function clearTimeout() {} 52 | function setInterval() {} 53 | function clearInterval() {} 54 | function Date() {} 55 | 56 | // Reassign the original functions. Now their writable attribute 57 | // should be true. Hackish, I know, but it works. 58 | setTimeout = sinon.timers.setTimeout; 59 | clearTimeout = sinon.timers.clearTimeout; 60 | setInterval = sinon.timers.setInterval; 61 | clearInterval = sinon.timers.clearInterval; 62 | Date = sinon.timers.Date; 63 | 64 | /*global sinon*/ 65 | /** 66 | * Helps IE run the fake XMLHttpRequest. By defining global functions, IE allows 67 | * them to be overwritten at a later point. If these are not defined like 68 | * this, overwriting them will result in anything from an exception to browser 69 | * crash. 70 | * 71 | * If you don't require fake XHR to work in IE, don't include this file. 72 | * 73 | * @author Christian Johansen (christian@cjohansen.no) 74 | * @license BSD 75 | * 76 | * Copyright (c) 2010-2011 Christian Johansen 77 | */ 78 | function XMLHttpRequest() {} 79 | 80 | // Reassign the original function. Now its writable attribute 81 | // should be true. Hackish, I know, but it works. 82 | XMLHttpRequest = sinon.xhr.XMLHttpRequest || undefined; -------------------------------------------------------------------------------- /spec/javascripts/helpers/tap-reporter.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | if (! jasmine) { 3 | throw new Exception("jasmine library does not exist in global namespace!"); 4 | } 5 | 6 | /** 7 | * TAP (http://en.wikipedia.org/wiki/Test_Anything_Protocol) reporter. 8 | * outputs spec results to the console. 9 | * 10 | * Heavily inspired by ConsoleReporter found at: 11 | * https://github.com/larrymyers/jasmine-reporters/ 12 | * 13 | * Usage: 14 | * 15 | * jasmine.getEnv().addReporter(new jasmine.TapReporter()); 16 | * jasmine.getEnv().execute(); 17 | */ 18 | var TapReporter = function() { 19 | this.started = false; 20 | this.finished = false; 21 | }; 22 | 23 | TapReporter.prototype = { 24 | 25 | reportRunnerStarting: function(runner) { 26 | this.started = true; 27 | this.start_time = (new Date()).getTime(); 28 | this.executed_specs = 0; 29 | this.passed_specs = 0; 30 | this.executed_asserts = 0; 31 | this.passed_asserts = 0; 32 | // should have at least 1 spec, otherwise it's considered a failure 33 | this.log('1..'+ Math.max(runner.specs().length, 1)); 34 | }, 35 | 36 | reportSpecStarting: function(spec) { 37 | this.executed_specs++; 38 | }, 39 | 40 | reportSpecResults: function(spec) { 41 | var resultText = "not ok"; 42 | var errorMessage = ''; 43 | 44 | var results = spec.results(); 45 | var passed = results.passed(); 46 | 47 | this.passed_asserts += results.passedCount; 48 | this.executed_asserts += results.totalCount; 49 | 50 | if (passed) { 51 | this.passed_specs++; 52 | resultText = "ok"; 53 | } else { 54 | var items = results.getItems(); 55 | var i = 0; 56 | var expectationResult, stackMessage; 57 | while (expectationResult = items[i++]) { 58 | if (expectationResult.trace) { 59 | stackMessage = expectationResult.trace.stack? expectationResult.trace.stack : expectationResult.message; 60 | errorMessage += '\n '+ stackMessage; 61 | } 62 | } 63 | } 64 | 65 | this.log(resultText +" "+ (spec.id + 1) +" - "+ spec.suite.description +" : "+ spec.description + errorMessage); 66 | }, 67 | 68 | reportRunnerResults: function(runner) { 69 | var dur = (new Date()).getTime() - this.start_time; 70 | var failed = this.executed_specs - this.passed_specs; 71 | var spec_str = this.executed_specs + (this.executed_specs === 1 ? " spec, " : " specs, "); 72 | var fail_str = failed + (failed === 1 ? " failure in " : " failures in "); 73 | var assert_str = this.executed_asserts + (this.executed_asserts === 1 ? " assertion, " : " assertions, "); 74 | 75 | if (this.executed_asserts) { 76 | this.log("# "+ spec_str + assert_str + fail_str + (dur/1000) + "s."); 77 | } else { 78 | this.log('not ok 1 - no asserts run.'); 79 | } 80 | this.finished = true; 81 | }, 82 | 83 | log: function(str) { 84 | var console = jasmine.getGlobal().console; 85 | if (console && console.log) { 86 | console.log(str); 87 | } 88 | } 89 | }; 90 | 91 | // export public 92 | jasmine.TapReporter = TapReporter; 93 | })(); -------------------------------------------------------------------------------- /spec/javascripts/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /spec/javascripts/javascript_spec_server.rb: -------------------------------------------------------------------------------- 1 | require 'rack' 2 | require 'net/http' 3 | 4 | class JavascriptSpecServer < Struct.new(:port, :root) 5 | 6 | def boot 7 | thread = Thread.new do 8 | app = Rack::File.new(root) 9 | Rack::Server.start(:app => app, :Port => port, :AccessLog => []) 10 | end 11 | thread.join(0.1) until ready? 12 | end 13 | 14 | def ready? 15 | uri = URI("http://localhost:#{port}/spec/javascripts/index.html") 16 | response = Net::HTTP.get_response(uri) 17 | response.is_a? Net::HTTPSuccess 18 | rescue SystemCallError 19 | return false 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/javascripts/judge-spec.js: -------------------------------------------------------------------------------- 1 | describe('judge', function() { 2 | var server, 3 | uniquenessAttr = '[{"kind":"uniqueness","options":{},"messages":{},"original_value":"leader"}]', 4 | presenceAttr = '[{"kind":"presence","options":{},"messages":{"blank":"must not be blank"}}]', 5 | uniqPresenceAttr = '[{"kind":"uniqueness","options":{},"messages":{},"original_value":"leader"},{"kind":"presence","options":{},"messages":{"blank":"must not be blank"}}]', 6 | mixedAttr = '[{"kind":"presence","options":{},"messages":{"blank":"must not be blank"}},{"kind":"inclusion","options":{"in":["a","b"]},"messages":{"inclusion":"must be a or b"}}]', 7 | uniqAndIncAttr = '[{"kind":"uniqueness","options":{},"messages":{}},{"kind":"inclusion","options":{"in":["a","b"]},"messages":{"inclusion":"must be a or b"}}]'; 8 | 9 | beforeEach(function() { 10 | this.addMatchers(customMatchers); 11 | server = sinon.fakeServer.create(); 12 | }); 13 | afterEach(function() { 14 | server.restore(); 15 | }); 16 | 17 | describe('judge.validate', function() { 18 | var el; 19 | beforeEach(function() { 20 | el = document.createElement('input'); 21 | el.setAttribute('data-validate', mixedAttr); 22 | }); 23 | it('returns a ValidationQueue', function() { 24 | expect(judge.validate(el)).toEqual(jasmine.any(judge.ValidationQueue)); 25 | }); 26 | describe('when given one callback', function() { 27 | it('calls callback with status and messages', function() { 28 | var callback = jasmine.createSpy(); 29 | el.value = ''; 30 | judge.validate(el, callback); 31 | expect(callback).toHaveBeenCalledWith(el, 'invalid', ['must not be blank', 'must be a or b']); 32 | expect(callback.callCount).toEqual(1); 33 | }); 34 | }); 35 | describe('when given named callbacks', function() { 36 | it('calls first callback when queue is closed as valid', function() { 37 | var first = jasmine.createSpy(), second = jasmine.createSpy(); 38 | el.value = 'a'; 39 | judge.validate(el, { 40 | valid: first, 41 | invalid: second 42 | }); 43 | expect(first).toHaveBeenCalled(); 44 | expect(first.callCount).toEqual(1); 45 | }); 46 | it('calls second callback wth messages when queue is closed as invalid', function() { 47 | var first = jasmine.createSpy('first'), second = jasmine.createSpy('second'); 48 | el.value = ''; 49 | judge.validate(el, { 50 | valid: first, 51 | invalid: second 52 | }); 53 | expect(second).toHaveBeenCalledWith(el, ['must not be blank', 'must be a or b']); 54 | expect(second.callCount).toEqual(1); 55 | }); 56 | }); 57 | 58 | describe('when checking local and remote validation messages', function() { 59 | it('calls valid callback when remote(unique) validation is eventually closed', function() { 60 | var valid = jasmine.createSpy(), invalid = jasmine.createSpy(); 61 | el.setAttribute('data-validate', uniqAndIncAttr); 62 | el.value = 'a'; 63 | 64 | var queue = judge.validate(el, { 65 | valid: valid, 66 | invalid: invalid 67 | }); 68 | expect(valid).not.toHaveBeenCalled(); 69 | expect(invalid).not.toHaveBeenCalled(); 70 | 71 | queue.validations[0].close([]); 72 | expect(valid).toHaveBeenCalledWith(el, []); 73 | expect(valid.callCount).toEqual(1); 74 | }); 75 | it('calls invalid callback when remote(unique) validation is eventually closed', function() { 76 | var valid = jasmine.createSpy(), invalid = jasmine.createSpy(); 77 | el.setAttribute('data-validate', uniqAndIncAttr); 78 | el.value = 'c'; // Not included, will fail. 79 | 80 | var queue = judge.validate(el, { 81 | valid: valid, 82 | invalid: invalid 83 | }); 84 | expect(valid).not.toHaveBeenCalled(); 85 | expect(invalid).not.toHaveBeenCalled(); 86 | 87 | queue.validations[0].close([]); 88 | expect(invalid).toHaveBeenCalledWith(el, ["must be a or b"]); 89 | expect(invalid.callCount).toEqual(1); 90 | }); 91 | }); 92 | }); 93 | 94 | describe('judge.Dispatcher', function() { 95 | var object; 96 | beforeEach(function() { 97 | object = {}; 98 | _.extend(object, judge.Dispatcher); 99 | }); 100 | describe('on/trigger', function() { 101 | var callback; 102 | beforeEach(function() { 103 | callback = jasmine.createSpy(); 104 | }); 105 | it('stores callback', function() { 106 | object.on('eventName', callback); 107 | expect(object._events.eventName[0].callback).toBe(callback); 108 | }); 109 | it('triggers callback', function() { 110 | object.on('eventName', callback); 111 | object.trigger('eventName', 10, 20); 112 | expect(callback).toHaveBeenCalledWith(10, 20); 113 | }); 114 | }); 115 | it('works with multiple callbacks', function() { 116 | var first = jasmine.createSpy(), second = jasmine.createSpy(); 117 | object.on('eventName', first); 118 | object.on('eventName', second); 119 | object.trigger('eventName'); 120 | expect(first).toHaveBeenCalled(); 121 | expect(second).toHaveBeenCalled(); 122 | }); 123 | it('triggers bind event when event is bound', function() { 124 | spyOn(object, 'trigger'); 125 | var callback = jasmine.createSpy(); 126 | object.on('eventName', callback); 127 | expect(object.trigger).toHaveBeenCalledWith('bind', 'eventName'); 128 | }); 129 | }); 130 | 131 | describe('judge.ValidationQueue', function() { 132 | var el, queue; 133 | describe('constructor', function() { 134 | beforeEach(function() { 135 | el = document.createElement('input'); 136 | el.setAttribute('data-validate', uniqPresenceAttr); 137 | queue = new judge.ValidationQueue(el); 138 | }); 139 | it('creates Validation objects from data attr', function() { 140 | expect(queue.validations[0]).toEqual(jasmine.any(judge.Validation)); 141 | }); 142 | it('creates one Validation for each object in data attr JSON array', function() { 143 | expect(queue.validations.length).toBe(2); 144 | }); 145 | it('binds closed event to stored Validations', function() { 146 | expect(_.keys(queue.validations[0]._events)).toContain('close'); 147 | }); 148 | }); 149 | describe('closing the queue', function() { 150 | var callback; 151 | it('triggers close event when queued Validations are eventually closed', function() { 152 | el = document.createElement('input'); 153 | el.setAttribute('data-validate', uniquenessAttr); 154 | queue = new judge.ValidationQueue(el); 155 | callback = jasmine.createSpy(); 156 | 157 | queue.on('close', callback); 158 | expect(callback).not.toHaveBeenCalled(); 159 | 160 | queue.validations[0].close([]); 161 | expect(callback).toHaveBeenCalledWith(el, 'valid', []); 162 | expect(callback.callCount).toEqual(1); 163 | }); 164 | it('triggers close event immediately if queued Validations are closed when event is bound', function() { 165 | el = document.createElement('input'); 166 | el.setAttribute('data-validate', presenceAttr); 167 | queue = new judge.ValidationQueue(el); 168 | callback = jasmine.createSpy(); 169 | queue.on('close', callback); 170 | expect(callback).toHaveBeenCalledWith(el, 'invalid', ['must not be blank']); 171 | expect(callback.callCount).toEqual(1); 172 | }); 173 | it('does not trigger close event if queued Validations are never closed', function() { 174 | el = document.createElement('input'); 175 | el.setAttribute('data-validate', uniquenessAttr); 176 | queue = new judge.ValidationQueue(el); 177 | callback = jasmine.createSpy(); 178 | queue.on('close', callback); 179 | expect(callback).not.toHaveBeenCalled(); 180 | }); 181 | it('triggers valid event if all queued Validations are valid when closed', function() { 182 | el = document.createElement('input'); 183 | el.setAttribute('data-validate', presenceAttr); 184 | el.value = 'foo'; 185 | queue = new judge.ValidationQueue(el); 186 | callback = jasmine.createSpy(); 187 | queue.on('valid', callback); 188 | expect(callback).toHaveBeenCalledWith(el, []); 189 | expect(callback.callCount).toEqual(1); 190 | }); 191 | it('triggers invalid event if any queued Validations are invalid when closed', function() { 192 | el = document.createElement('input'); 193 | el.setAttribute('data-validate', presenceAttr); 194 | queue = new judge.ValidationQueue(el); 195 | callback = jasmine.createSpy(); 196 | queue.on('invalid', callback); 197 | expect(callback).toHaveBeenCalledWith(el, ['must not be blank']); 198 | expect(callback.callCount).toEqual(1); 199 | }); 200 | }); 201 | }); 202 | 203 | describe('judge.Validation', function() { 204 | var validation; 205 | describe('constructor', function() { 206 | it('is pending when no messages given to constructor', function() { 207 | validation = new judge.Validation(); 208 | expect(validation.status()).toBe('pending'); 209 | }); 210 | it('is closed as invalid when present messages array given to constructor', function() { 211 | validation = new judge.Validation(['foo']); 212 | expect(validation.status()).toBe('invalid'); 213 | }); 214 | it('is closed as valid when empty array given to constructor', function() { 215 | validation = new judge.Validation([]); 216 | expect(validation.status()).toBe('valid') 217 | }); 218 | }); 219 | describe('close method', function() { 220 | it('is closed as valid when empty array is given', function() { 221 | validation = new judge.Validation(); 222 | validation.close([]); 223 | expect(validation.status()).toBe('valid'); 224 | }); 225 | it('is closed as invalid when present array is given', function() { 226 | validation = new judge.Validation(); 227 | validation.close(['foo']); 228 | expect(validation.status()).toBe('invalid'); 229 | }); 230 | it('does not overwrite messages once set', function() { 231 | validation = new judge.Validation(); 232 | validation.close(['foo']); 233 | validation.close([]); 234 | expect(validation.messages).toEqual(['foo']); 235 | }); 236 | it('triggers closed event with correct args', function() { 237 | validation = new judge.Validation(); 238 | callback = jasmine.createSpy(); 239 | validation.on('close', callback); 240 | validation.close(['foo']); 241 | expect(callback).toHaveBeenCalledWith('invalid', ['foo']); 242 | }); 243 | }); 244 | }); 245 | 246 | describe('eachValidators', function() { 247 | var el, validator, textarea; 248 | beforeEach(function() { 249 | el = document.createElement('input'); 250 | }); 251 | 252 | describe('presence', function() { 253 | beforeEach(function() { 254 | validator = _.bind(judge.eachValidators.presence, el); 255 | }); 256 | it('returns invalid Validation if element has no value', function() { 257 | expect(validator({}, { blank: 'Must not be blank' })).toBeInvalid(); 258 | }); 259 | it('returns valid Validation if element has value', function() { 260 | el.value = 'foo'; 261 | expect(validator({}, { blank: 'Must not be blank' })).toBeValid(); 262 | }); 263 | it('returns invalid Validation if radio has no selection', function() { 264 | el.type = 'radio'; 265 | el.name = 'radio_group'; 266 | el.value = 'option1'; 267 | // eachValidators.presence for radio btns rely on querySelectorAll 268 | // so we have to add the el to the body 269 | document.body.appendChild(el); 270 | expect(validator({}, { blank: 'Must not be blank' })).toBeInvalid(); 271 | }); 272 | it('returns valid Validation if radio has selection', function() { 273 | el.type = 'radio'; 274 | el.name = 'radio_group'; 275 | el.value = 'option1'; 276 | el.checked = true; 277 | // eachValidators.presence for radio btns rely on querySelectorAll 278 | // so we have to add the el to the body 279 | document.body.appendChild(el); 280 | expect(validator({}, { blank: 'Must not be blank' })).toBeValid(); 281 | }); 282 | }); 283 | 284 | describe('length', function() { 285 | describe('length : single input', function() { 286 | beforeEach(function() { 287 | validator = _.bind(judge.eachValidators.length, el); 288 | }); 289 | it('returns invalid Validation if value is too short', function() { 290 | el.value = 'abc'; 291 | expect(validator({ minimum: 5 }, { too_short: '2 shrt' })).toBeInvalidWith(['2 shrt']); 292 | }); 293 | it('returns invalid Validation if value is too long', function() { 294 | el.value = 'abcdef'; 295 | expect(validator({ maximum: 5 }, { too_long: '2 lng' })).toBeInvalidWith(['2 lng']); 296 | }); 297 | it('returns valid Validation for valid value', function() { 298 | el.value = 'abc'; 299 | expect(validator({ minimum: 2, maximum: 5 }, {})).toBeValid(); 300 | }); 301 | }); 302 | 303 | describe('length', function() { 304 | describe('length : textarea', function() { 305 | beforeEach(function() { 306 | textarea = document.createElement('textarea'); 307 | validator = _.bind(judge.eachValidators.length, textarea); 308 | }); 309 | it('counts new lines as two characters', function() { 310 | textarea.value = 'abc\nd'; 311 | expect(validator({ maximum: 5 }, { too_long: '2 lng' })).toBeInvalidWith(['2 lng']); 312 | }); 313 | it('counts each variation of new line as two character', function() { 314 | textarea.value = 'abc\nd\refg\r\nhi'; 315 | expect(validator({ is: 15 })).toBeValid(); 316 | }); 317 | }); 318 | }); 319 | 320 | describe('length : multiple input', function() { 321 | var options; 322 | beforeEach(function() { 323 | el = document.createElement('select'); 324 | options = { 325 | option1 : 'text a', 326 | option2 : 'text b', 327 | option3 : 'text c' 328 | }; 329 | for(var i in options) { 330 | el.options[el.options.length] = new Option(options[i], i); 331 | } 332 | validator = _.bind(judge.eachValidators.length, el); 333 | }); 334 | 335 | it('returns invalid Validation if array is too small', function() { 336 | expect(validator({ minimum: 4 }, { too_short: '2 shrt' })).toBeInvalidWith(['2 shrt']); 337 | }); 338 | 339 | it('returns invalid Validation if array is too big', function() { 340 | expect(validator({ maximum: 2 }, { too_long: '2 lng' })).toBeInvalidWith(['2 lng']); 341 | }); 342 | 343 | it('returns valid Validation for valid size of array', function() { 344 | expect(validator({ minimum: 2, maximum: 5 }, {})).toBeValid(); 345 | }); 346 | }); 347 | }); 348 | 349 | describe('exclusion', function() { 350 | beforeEach(function() { 351 | validator = _.bind(judge.eachValidators.exclusion, el); 352 | }); 353 | it('returns valid Validation when value is not in array', function() { 354 | el.value = 'baz'; 355 | expect(validator({ 'in': ['foo', 'bar'] }, {})).toBeValid(); 356 | }); 357 | it('returns invalid Validation when value is in array', function() { 358 | el.value = 'foo'; 359 | var validation = validator( 360 | { 'in': ['foo', 'bar'] }, 361 | { exclusion: 'foo and bar are not allowed' } 362 | ); 363 | expect(validation).toBeInvalidWith(['foo and bar are not allowed']); 364 | }); 365 | }); 366 | 367 | describe('inclusion', function() { 368 | beforeEach(function() { 369 | validator = _.bind(judge.eachValidators.inclusion, el); 370 | }); 371 | it('returns valid Validation when value is in array', function() { 372 | el.value = 'foo'; 373 | expect(validator({ 'in': ['foo', 'bar'] }, {})).toBeValid(); 374 | }); 375 | it('returns invalid Validation when value is not in array', function() { 376 | el.value = 'baz'; 377 | expect(validator({ 'in': ['foo', 'bar'] }, { inclusion: 'must be foo or bar' })).toBeInvalidWith(['must be foo or bar']); 378 | }); 379 | }); 380 | 381 | describe('numericality', function() { 382 | beforeEach(function() { 383 | validator = _.bind(judge.eachValidators.numericality, el); 384 | }); 385 | 386 | it('returns invalid Validation when value is not a number', function() { 387 | el.value = 'abc'; 388 | expect(validator({}, { not_a_number: 'not a number' })).toBeInvalidWith(['not a number']); 389 | }); 390 | 391 | it('returns invalid Validation when value must be odd but is even', function() { 392 | el.value = '2'; 393 | expect(validator({ odd: true }, { odd: 'must be odd' })).toBeInvalidWith(['must be odd']) 394 | }); 395 | 396 | it('returns valid Validation when value must be odd and is odd', function() { 397 | el.value = '1'; 398 | expect(validator({ odd: true }, {})).toBeValid(); 399 | }); 400 | 401 | it('returns invalid Validation when value must be even but is odd', function() { 402 | el.value = '1'; 403 | expect(validator({ even: true }, { even: 'must be even' })).toBeInvalidWith(['must be even']) 404 | }); 405 | 406 | it('returns valid Validation when value must be even and is even', function() { 407 | el.value = '2'; 408 | expect(validator({ even: true }, {})).toBeValid(); 409 | }); 410 | 411 | it('returns valid Validation when value must be an integer and value is an integer', function() { 412 | el.value = '1'; 413 | expect(validator({ only_integer: true }, {})).toBeValid(); 414 | }); 415 | 416 | it('returns invalid Validation when value must be an integer and value is a float', function() { 417 | el.value = '1.1'; 418 | expect(validator({ only_integer: true }, { not_an_integer: 'must be integer' })).toBeInvalidWith(['must be integer']); 419 | }); 420 | 421 | it('returns invalid Validation when value is too low', function() { 422 | el.value = '1'; 423 | expect(validator({ greater_than: 2 }, { greater_than: 'too low' })).toBeInvalidWith(['too low']); 424 | }); 425 | 426 | it('returns valid Validation when value is high enough', function() { 427 | el.value = '3'; 428 | expect(validator({ greater_than: 2 }, {})).toBeValid(); 429 | }); 430 | 431 | it('returns invalid Validation when value is too high', function() { 432 | el.value = '2'; 433 | expect(validator({ less_than: 2 }, { less_than: 'too high' })).toBeInvalidWith(['too high']); 434 | }); 435 | 436 | it('returns valid Validation when value is low enough', function() { 437 | el.value = '1'; 438 | expect(validator({ less_than: 2 }, {})).toBeValid(); 439 | }); 440 | }); 441 | 442 | describe('format', function() { 443 | beforeEach(function() { 444 | validator = _.bind(judge.eachValidators.format, el); 445 | }); 446 | 447 | describe('with', function() { 448 | it('returns invalid Validation when value does not match with', function() { 449 | el.value = '123'; 450 | expect(validator({ 'with': '(?-mix:[A-Za-z]+)' }, { invalid: 'is invalid' })).toBeInvalidWith(['is invalid']); 451 | }); 452 | 453 | it('returns valid Validation when value matches with', function() { 454 | el.value = 'AbC'; 455 | expect(validator({ 'with': '(?-mix:[A-Za-z]+)' }, {})).toBeValid(); 456 | }); 457 | 458 | it('converts devise\'s Ruby email regex and returns invalid with invalid email', function() { 459 | el.value = 'not an email'; 460 | expect(validator({ 'with': '(?-mix:\\A[^@\\s]+@([^@\\s]+\\.)+[^@\\s]+\\z)' }, { invalid: 'is invalid' })).toBeInvalidWith(['is invalid']); 461 | }); 462 | 463 | it('converts devise\'s Ruby email regex and returns valid with valid email', function() { 464 | el.value = 'john.doe@somesite.com'; 465 | expect(validator({ 'with': '(?-mix:\\A[^@\\s]+@([^@\\s]+\\.)+[^@\\s]+\\z)' }, {})).toBeValid(); 466 | }); 467 | 468 | it('converts a Ruby slug regex and returns invalid with invalid slug', function() { 469 | el.value = 'not an slug.'; 470 | expect(validator({ 'with': '(?-mix:\\A[-a-zA-Z0-9]+\\z)' }, { invalid: 'is invalid' })).toBeInvalidWith(['is invalid']); 471 | }); 472 | 473 | it('converts a Ruby slug regex and returns valid with valid slug', function() { 474 | el.value = 'this-is-a-slug'; 475 | expect(validator({ 'with': '(?-mix:\\A[-a-zA-Z0-9]+\\z)' }, {})).toBeValid(); 476 | }); 477 | }); 478 | 479 | describe('without', function() { 480 | it('returns invalid Validation when value matches without', function() { 481 | el.value = 'AbC'; 482 | expect(validator({ without: '(?-mix:[A-Za-z]+)' }, { invalid: 'is invalid' })).toBeInvalidWith(['is invalid']); 483 | }); 484 | 485 | it('returns valid Validation when value does not match without', function() { 486 | el.value = '123'; 487 | expect(validator({ without: '(?-mix:[A-Za-z]+)' }, {})).toBeValid(); 488 | }); 489 | }); 490 | }); 491 | 492 | describe('acceptance', function() { 493 | beforeEach(function() { 494 | validator = _.bind(judge.eachValidators.acceptance, el); 495 | }); 496 | it('returns valid Validation when el is checked', function() { 497 | el.type = 'checkbox'; 498 | el.checked = true; 499 | expect(validator({ accept: 1 }, {})).toBeValid(); 500 | }); 501 | it('returns invalid Validation when el is not checked', function() { 502 | el.type = 'checkbox'; 503 | expect(validator({ accept: 1 }, { accepted: 'must be accepted' })).toBeInvalidWith(['must be accepted']); 504 | }); 505 | }); 506 | 507 | describe('confirmation', function() { 508 | var confEl; 509 | beforeEach(function() { 510 | el.id = 'pw_confirmation'; 511 | confEl = document.createElement('input'); 512 | confEl.id = 'pw'; 513 | document.body.appendChild(confEl); 514 | validator = _.bind(judge.eachValidators.confirmation, el); 515 | }); 516 | afterEach(function() { 517 | document.body.removeChild(confEl); 518 | }); 519 | it('returns a valid Validation when values match', function() { 520 | el.value = 'foo', confEl.value = 'foo'; 521 | expect(validator({}, {})).toBeValid(); 522 | }); 523 | it('returns a valid Validation when values match', function() { 524 | el.value = 'foo', confEl.value = 'bar'; 525 | expect(validator({}, { confirmation: 'must be confirmed' })).toBeInvalidWith(['must be confirmed']); 526 | }); 527 | }); 528 | 529 | describe('uniqueness', function() { 530 | var validation; 531 | beforeEach(function() { 532 | validator = _.bind(judge.eachValidators.uniqueness, el); 533 | el.value = 'leader@team.com'; 534 | el.name = 'team[leader][email]'; 535 | el.setAttribute('data-validate', uniquenessAttr); 536 | el.setAttribute('data-klass', 'Leader'); 537 | }); 538 | it('returns a pending Validation', function() { 539 | validation = validator({}, {}); 540 | expect(validation).toBePending(); 541 | }); 542 | it('makes request to correct path', function() { 543 | runs(function() { 544 | server.respondWith([200, {}, '[]']); 545 | validation = validator({}, {}); 546 | }); 547 | runs(function() { 548 | server.respond(); 549 | }); 550 | runs(function() { 551 | expect(server.requests[0].url).toBe('/judge?klass=Leader&attribute=email&value=leader%40team.com&kind=uniqueness&original_value=leader'); 552 | }); 553 | }); 554 | it('closes Validation as valid if the server responds with an empty JSON array', function() { 555 | runs(function() { 556 | server.respondWith([200, {}, '[]']); 557 | validation = validator({}, {}); 558 | }); 559 | runs(function() { 560 | server.respond(); 561 | }); 562 | runs(function() { 563 | expect(validation).toBeValid(); 564 | }); 565 | }); 566 | it('closes Validation as invalid if the server responds with a JSON array of error messages', function() { 567 | runs(function() { 568 | server.respondWith([200, {}, '["already taken"]']); 569 | validation = validator({}, {}); 570 | }); 571 | runs(function() { 572 | server.respond(); 573 | }); 574 | runs(function() { 575 | expect(validation).toBeInvalidWith(['already taken']); 576 | }); 577 | }); 578 | it('throws error if server responds with a non-20x status', function() { 579 | runs(function() { 580 | server.respondWith([500, {}, 'Server error']); 581 | validation = validator({}, {}); 582 | }); 583 | runs(function() { 584 | server.respond(); 585 | }); 586 | runs(function() { 587 | expect(validation).toBeInvalidWith(['Request error: 500']); 588 | }); 589 | }); 590 | }); 591 | 592 | describe('user added validator', function() { 593 | beforeEach(function() { 594 | judge.eachValidators.foo = function(options, messages) { 595 | return new judge.Validation(['nope']); 596 | }; 597 | validator = _.bind(judge.eachValidators.foo, el); 598 | }); 599 | it('is callable in the same way as standard validators', function() { 600 | var validation = validator({}, {}); 601 | expect(validation).toBeInvalid(); 602 | }); 603 | }); 604 | 605 | }); 606 | 607 | }); 608 | -------------------------------------------------------------------------------- /spec/javascripts/run.js: -------------------------------------------------------------------------------- 1 | var system = require('system'), 2 | page = require('webpage').create(); 3 | 4 | page.onConsoleMessage = function(msg) { 5 | console.log(msg); 6 | }; 7 | 8 | page.open('http://localhost:' + system.args[1] + '/spec/javascripts/index.html', function(status) { 9 | if (status !== 'success') { 10 | console.log('Page status:', status); 11 | phantom.exit(1); 12 | } 13 | 14 | page.evaluate(function() { 15 | var env = jasmine.getEnv(); 16 | window.reporter = new jasmine.TapReporter(); 17 | env.addReporter(reporter); 18 | env.execute(); 19 | }); 20 | 21 | var wait = setInterval(function() { 22 | var report = page.evaluate(function() { 23 | return [ 24 | window.reporter.finished, 25 | window.reporter.passed_specs, 26 | window.reporter.executed_specs 27 | ]; 28 | }); 29 | if (report[0]) { 30 | clearInterval(wait); 31 | var didPass = report[1] === report[2], exitCode = didPass ? 0 : 1; 32 | console.log('Exiting with status', exitCode); 33 | phantom.exit(exitCode); 34 | } 35 | }, 250); 36 | }); 37 | -------------------------------------------------------------------------------- /spec/message_collection_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Judge::MessageCollection do 4 | 5 | let(:user) { FactoryGirl.build(:user) } 6 | 7 | it "has to_hash method which returns messages hash" do 8 | amv = User.validators_on(:name).first 9 | message_collection = Judge::MessageCollection.new(user, :name, amv) 10 | message_collection.should respond_to :to_hash 11 | message_collection.to_hash.should be_a Hash 12 | end 13 | 14 | describe "base messages" do 15 | it "adds correct base message to messages hash" do 16 | amv = User.validators_on(:name).first 17 | messages = Judge::MessageCollection.new(user, :name, amv).to_hash 18 | messages[:blank].should eql "Name must not be blank" 19 | end 20 | end 21 | 22 | describe "options messages" do 23 | it "adds correct optional messages to messages hash when present (length)" do 24 | amv = User.validators_on(:username).first 25 | messages = Judge::MessageCollection.new(user, :username, amv).to_hash 26 | messages[:too_long].should eql "Username is too long (must be less than 10 characters)" 27 | end 28 | 29 | it "adds correct optional messages to messages hash when present (numericality)" do 30 | amv = User.validators_on(:age).first 31 | messages = Judge::MessageCollection.new(user, :age, amv).to_hash 32 | messages[:greater_than].should eql "Age must be greater than 13" 33 | end 34 | 35 | it "adds nothing to messages hash when optional messages not present" do 36 | amv = User.validators_on(:name).first 37 | messages = Judge::MessageCollection.new(user, :name, amv).to_hash 38 | messages[:too_long].should be_nil 39 | messages[:greater_than].should be_nil 40 | end 41 | end 42 | 43 | describe "blank messages" do 44 | it "adds blank message to messages hash if applicable" do 45 | amv = User.validators_on(:username).first 46 | messages = Judge::MessageCollection.new(user, :username, amv).to_hash 47 | messages[:blank].should eql "Username must not be blank" 48 | end 49 | 50 | it "does not add blank message to messages hash if allow_blank is true" do 51 | amv = User.validators_on(:country).first 52 | messages = Judge::MessageCollection.new(user, :country, amv).to_hash 53 | messages[:blank].should be_nil 54 | end 55 | end 56 | 57 | describe "integer messages" do 58 | it "adds not_an_integer message to messages hash if only_integer is true" do 59 | amv = User.validators_on(:age).first 60 | messages = Judge::MessageCollection.new(user, :age, amv).to_hash 61 | messages[:not_an_integer].should eql "Age must be an integer" 62 | end 63 | 64 | it "adds not_an_integer message to messages hash if only_integer is true" do 65 | amv = User.validators_on(:telephone).first 66 | messages = Judge::MessageCollection.new(user, :telephone, amv).to_hash 67 | messages[:not_an_integer].should eql "Telephone must be an integer" 68 | end 69 | end 70 | 71 | describe "custom messages" do 72 | it "adds custom messages to messages hash if declared inside EachValidator" do 73 | amv = User.validators_on(:city).first 74 | messages = Judge::MessageCollection.new(user, :city, amv).to_hash 75 | messages[:not_valid_city].should eql "City must be an approved city" 76 | end 77 | end 78 | 79 | end 80 | -------------------------------------------------------------------------------- /spec/models/validation_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "validations" do 4 | 5 | before(:all) do 6 | FactoryGirl.create(:user, :username => "existing") 7 | end 8 | let(:valid_params) do 9 | { 10 | :klass => User, 11 | :attribute => :username, 12 | :value => "new", 13 | :kind => :uniqueness 14 | }.with_indifferent_access 15 | end 16 | let(:invalid_params) do 17 | { 18 | :klass => User, 19 | :attribute => :username, 20 | :value => "existing", 21 | :kind => :uniqueness 22 | }.with_indifferent_access 23 | end 24 | after(:all) { User.destroy_all } 25 | 26 | describe Judge::Validation do 27 | describe "with valid value" do 28 | subject(:validation) { Judge::Validation.new(valid_params) } 29 | specify { validation.amv.should be_a ActiveRecord::Validations::UniquenessValidator } 30 | specify do 31 | validation.record.should be_a User 32 | validation.record.username.should eql "new" 33 | end 34 | specify do 35 | validation.as_json.should be_an Array 36 | validation.as_json.should be_empty 37 | end 38 | end 39 | 40 | describe "with invalid value" do 41 | subject(:validation) { Judge::Validation.new(invalid_params) } 42 | it { should be_a Judge::Validation } 43 | specify { validation.as_json.should eql ["Username \"existing\" has already been taken"] } 44 | end 45 | end 46 | 47 | describe Judge::NullValidation do 48 | subject(:validation) { Judge::NullValidation.new(valid_params) } 49 | it "does not build object" do 50 | validation.record.should eql validation 51 | end 52 | it "does not look up active model validator" do 53 | validation.amv.should eql validation 54 | end 55 | specify { validation.as_json.should eql ["Judge validation for User#username not allowed"] } 56 | end 57 | 58 | end -------------------------------------------------------------------------------- /spec/routing/engine_routing_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe "routes" do 4 | #specify do 5 | # @routes = Dummy::Application.routes 6 | # puts @routes.routes.inspect 7 | # #{ :get => "/validations/uniqueness" }.should route_to("judge/validations#uniqueness") 8 | #end 9 | end -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | ENV['RAILS_ENV'] ||= 'test' 2 | require File.expand_path('../../spec/dummy/config/environment', __FILE__) 3 | 4 | require 'rspec/rails' 5 | require 'rspec/autorun' 6 | require 'factory_girl' 7 | require 'factories/factories' 8 | 9 | RSpec.configure do |config| 10 | config.use_transactional_fixtures = true 11 | config.color = false 12 | config.formatter = 'TapFormatter' 13 | end 14 | -------------------------------------------------------------------------------- /spec/validator_collection_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Judge::ValidatorCollection do 4 | 5 | let(:vc) { Judge::ValidatorCollection.new(FactoryGirl.build(:user), :name) } 6 | 7 | it "contains validators" do 8 | vc.validators.should be_an Array 9 | vc.validators.first.should be_a Judge::Validator 10 | end 11 | 12 | it "converts to json correctly" do 13 | vc.to_json.should be_a String 14 | end 15 | 16 | it "is enumerable" do 17 | vc.should be_an Enumerable 18 | vc.should respond_to :each 19 | end 20 | 21 | it "respects the global ignore_unsupported_validators configuration option" do 22 | vc.validators.length.should eq 2 23 | Judge::ValidatorCollection.new(FactoryGirl.build(:user), :country).validators.length.should eq 2 24 | Judge::ValidatorCollection.new(FactoryGirl.build(:user), :bio).validators.length.should eq 2 25 | Judge::ValidatorCollection.new(FactoryGirl.build(:user), :dob).validators.length.should eq 2 26 | Judge.config.ignore_unsupported_validators true 27 | Judge::ValidatorCollection.new(FactoryGirl.build(:user), :name).validators.length.should eq 1 28 | Judge::ValidatorCollection.new(FactoryGirl.build(:user), :country).validators.length.should eq 1 29 | Judge::ValidatorCollection.new(FactoryGirl.build(:user), :bio).validators.length.should eq 2 30 | Judge::ValidatorCollection.new(FactoryGirl.build(:user), :dob).validators.length.should eq 1 31 | end 32 | 33 | it "respects the per-validator judge configuration option" do 34 | Judge::ValidatorCollection.new(FactoryGirl.build(:user), :team_id).validators.length.should eq 1 35 | Judge::ValidatorCollection.new(FactoryGirl.build(:user), :discipline_id).validators.length.should eq 2 36 | Judge::ValidatorCollection.new(FactoryGirl.build(:user), :time_zone).validators.length.should eq 1 37 | end 38 | 39 | it "respects the use association name judge configuration option" do 40 | Judge.config.use_association_name_for_validations true 41 | # validation is defined as (validates :sport, presence: true) instead of (validates :sport_id, presence: true) 42 | Judge::ValidatorCollection.new(FactoryGirl.build(:discipline), :sport_id).validators.length.should eq 1 43 | end 44 | 45 | it "ignores unknown per-validator judge configuration options" do 46 | Judge::ValidatorCollection.new(FactoryGirl.build(:user), :gender).validators.length.should eq 2 47 | end 48 | 49 | it "should remove confirmation validation from password" do 50 | Judge::ValidatorCollection.new(FactoryGirl.build(:user), :password).validators.each do |validator| 51 | validator.kind.should_not eq :confirmation 52 | end 53 | end 54 | 55 | it "should add confirmation validation to password_confirmation" do 56 | Judge::ValidatorCollection.new(FactoryGirl.create(:user), :password_confirmation).validators.length.should eq 1 57 | end 58 | 59 | end -------------------------------------------------------------------------------- /spec/validator_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Judge::Validator do 4 | 5 | before(:each) do 6 | user = FactoryGirl.build(:user) 7 | amv = User.validators_on(:name).first 8 | @validator = Judge::Validator.new(user, :name, amv) 9 | end 10 | 11 | it "has correct kind attr" do 12 | @validator.kind.should eql :presence 13 | end 14 | 15 | it "has hash in options attr" do 16 | @validator.options.should be_a Hash 17 | end 18 | 19 | it "has Judge::MessageCollection in messages attr" do 20 | @validator.messages.should be_a Judge::MessageCollection 21 | end 22 | 23 | describe "#to_hash" do 24 | it "converts to hash with correct properties" do 25 | hash = @validator.to_hash 26 | hash.should be_a Hash 27 | hash[:kind].should be_a Symbol 28 | hash[:options].should be_a Hash 29 | hash[:messages].should be_a Hash 30 | end 31 | end 32 | 33 | end -------------------------------------------------------------------------------- /vendor/assets/javascripts/json2.js: -------------------------------------------------------------------------------- 1 | /* 2 | json2.js 3 | 2012-10-08 4 | 5 | Public Domain. 6 | 7 | NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. 8 | 9 | See http://www.JSON.org/js.html 10 | 11 | 12 | This code should be minified before deployment. 13 | See http://javascript.crockford.com/jsmin.html 14 | 15 | USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO 16 | NOT CONTROL. 17 | 18 | 19 | This file creates a global JSON object containing two methods: stringify 20 | and parse. 21 | 22 | JSON.stringify(value, replacer, space) 23 | value any JavaScript value, usually an object or array. 24 | 25 | replacer an optional parameter that determines how object 26 | values are stringified for objects. It can be a 27 | function or an array of strings. 28 | 29 | space an optional parameter that specifies the indentation 30 | of nested structures. If it is omitted, the text will 31 | be packed without extra whitespace. If it is a number, 32 | it will specify the number of spaces to indent at each 33 | level. If it is a string (such as '\t' or ' '), 34 | it contains the characters used to indent at each level. 35 | 36 | This method produces a JSON text from a JavaScript value. 37 | 38 | When an object value is found, if the object contains a toJSON 39 | method, its toJSON method will be called and the result will be 40 | stringified. A toJSON method does not serialize: it returns the 41 | value represented by the name/value pair that should be serialized, 42 | or undefined if nothing should be serialized. The toJSON method 43 | will be passed the key associated with the value, and this will be 44 | bound to the value 45 | 46 | For example, this would serialize Dates as ISO strings. 47 | 48 | Date.prototype.toJSON = function (key) { 49 | function f(n) { 50 | // Format integers to have at least two digits. 51 | return n < 10 ? '0' + n : n; 52 | } 53 | 54 | return this.getUTCFullYear() + '-' + 55 | f(this.getUTCMonth() + 1) + '-' + 56 | f(this.getUTCDate()) + 'T' + 57 | f(this.getUTCHours()) + ':' + 58 | f(this.getUTCMinutes()) + ':' + 59 | f(this.getUTCSeconds()) + 'Z'; 60 | }; 61 | 62 | You can provide an optional replacer method. It will be passed the 63 | key and value of each member, with this bound to the containing 64 | object. The value that is returned from your method will be 65 | serialized. If your method returns undefined, then the member will 66 | be excluded from the serialization. 67 | 68 | If the replacer parameter is an array of strings, then it will be 69 | used to select the members to be serialized. It filters the results 70 | such that only members with keys listed in the replacer array are 71 | stringified. 72 | 73 | Values that do not have JSON representations, such as undefined or 74 | functions, will not be serialized. Such values in objects will be 75 | dropped; in arrays they will be replaced with null. You can use 76 | a replacer function to replace those with JSON values. 77 | JSON.stringify(undefined) returns undefined. 78 | 79 | The optional space parameter produces a stringification of the 80 | value that is filled with line breaks and indentation to make it 81 | easier to read. 82 | 83 | If the space parameter is a non-empty string, then that string will 84 | be used for indentation. If the space parameter is a number, then 85 | the indentation will be that many spaces. 86 | 87 | Example: 88 | 89 | text = JSON.stringify(['e', {pluribus: 'unum'}]); 90 | // text is '["e",{"pluribus":"unum"}]' 91 | 92 | 93 | text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t'); 94 | // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]' 95 | 96 | text = JSON.stringify([new Date()], function (key, value) { 97 | return this[key] instanceof Date ? 98 | 'Date(' + this[key] + ')' : value; 99 | }); 100 | // text is '["Date(---current time---)"]' 101 | 102 | 103 | JSON.parse(text, reviver) 104 | This method parses a JSON text to produce an object or array. 105 | It can throw a SyntaxError exception. 106 | 107 | The optional reviver parameter is a function that can filter and 108 | transform the results. It receives each of the keys and values, 109 | and its return value is used instead of the original value. 110 | If it returns what it received, then the structure is not modified. 111 | If it returns undefined then the member is deleted. 112 | 113 | Example: 114 | 115 | // Parse the text. Values that look like ISO date strings will 116 | // be converted to Date objects. 117 | 118 | myData = JSON.parse(text, function (key, value) { 119 | var a; 120 | if (typeof value === 'string') { 121 | a = 122 | /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value); 123 | if (a) { 124 | return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], 125 | +a[5], +a[6])); 126 | } 127 | } 128 | return value; 129 | }); 130 | 131 | myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) { 132 | var d; 133 | if (typeof value === 'string' && 134 | value.slice(0, 5) === 'Date(' && 135 | value.slice(-1) === ')') { 136 | d = new Date(value.slice(5, -1)); 137 | if (d) { 138 | return d; 139 | } 140 | } 141 | return value; 142 | }); 143 | 144 | 145 | This is a reference implementation. You are free to copy, modify, or 146 | redistribute. 147 | */ 148 | 149 | /*jslint evil: true, regexp: true */ 150 | 151 | /*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply, 152 | call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours, 153 | getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, 154 | lastIndex, length, parse, prototype, push, replace, slice, stringify, 155 | test, toJSON, toString, valueOf 156 | */ 157 | 158 | 159 | // Create a JSON object only if one does not already exist. We create the 160 | // methods in a closure to avoid creating global variables. 161 | 162 | if (typeof JSON !== 'object') { 163 | JSON = {}; 164 | } 165 | 166 | (function () { 167 | 'use strict'; 168 | 169 | function f(n) { 170 | // Format integers to have at least two digits. 171 | return n < 10 ? '0' + n : n; 172 | } 173 | 174 | if (typeof Date.prototype.toJSON !== 'function') { 175 | 176 | Date.prototype.toJSON = function (key) { 177 | 178 | return isFinite(this.valueOf()) 179 | ? this.getUTCFullYear() + '-' + 180 | f(this.getUTCMonth() + 1) + '-' + 181 | f(this.getUTCDate()) + 'T' + 182 | f(this.getUTCHours()) + ':' + 183 | f(this.getUTCMinutes()) + ':' + 184 | f(this.getUTCSeconds()) + 'Z' 185 | : null; 186 | }; 187 | 188 | String.prototype.toJSON = 189 | Number.prototype.toJSON = 190 | Boolean.prototype.toJSON = function (key) { 191 | return this.valueOf(); 192 | }; 193 | } 194 | 195 | var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, 196 | escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, 197 | gap, 198 | indent, 199 | meta = { // table of character substitutions 200 | '\b': '\\b', 201 | '\t': '\\t', 202 | '\n': '\\n', 203 | '\f': '\\f', 204 | '\r': '\\r', 205 | '"' : '\\"', 206 | '\\': '\\\\' 207 | }, 208 | rep; 209 | 210 | 211 | function quote(string) { 212 | 213 | // If the string contains no control characters, no quote characters, and no 214 | // backslash characters, then we can safely slap some quotes around it. 215 | // Otherwise we must also replace the offending characters with safe escape 216 | // sequences. 217 | 218 | escapable.lastIndex = 0; 219 | return escapable.test(string) ? '"' + string.replace(escapable, function (a) { 220 | var c = meta[a]; 221 | return typeof c === 'string' 222 | ? c 223 | : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); 224 | }) + '"' : '"' + string + '"'; 225 | } 226 | 227 | 228 | function str(key, holder) { 229 | 230 | // Produce a string from holder[key]. 231 | 232 | var i, // The loop counter. 233 | k, // The member key. 234 | v, // The member value. 235 | length, 236 | mind = gap, 237 | partial, 238 | value = holder[key]; 239 | 240 | // If the value has a toJSON method, call it to obtain a replacement value. 241 | 242 | if (value && typeof value === 'object' && 243 | typeof value.toJSON === 'function') { 244 | value = value.toJSON(key); 245 | } 246 | 247 | // If we were called with a replacer function, then call the replacer to 248 | // obtain a replacement value. 249 | 250 | if (typeof rep === 'function') { 251 | value = rep.call(holder, key, value); 252 | } 253 | 254 | // What happens next depends on the value's type. 255 | 256 | switch (typeof value) { 257 | case 'string': 258 | return quote(value); 259 | 260 | case 'number': 261 | 262 | // JSON numbers must be finite. Encode non-finite numbers as null. 263 | 264 | return isFinite(value) ? String(value) : 'null'; 265 | 266 | case 'boolean': 267 | case 'null': 268 | 269 | // If the value is a boolean or null, convert it to a string. Note: 270 | // typeof null does not produce 'null'. The case is included here in 271 | // the remote chance that this gets fixed someday. 272 | 273 | return String(value); 274 | 275 | // If the type is 'object', we might be dealing with an object or an array or 276 | // null. 277 | 278 | case 'object': 279 | 280 | // Due to a specification blunder in ECMAScript, typeof null is 'object', 281 | // so watch out for that case. 282 | 283 | if (!value) { 284 | return 'null'; 285 | } 286 | 287 | // Make an array to hold the partial results of stringifying this object value. 288 | 289 | gap += indent; 290 | partial = []; 291 | 292 | // Is the value an array? 293 | 294 | if (Object.prototype.toString.apply(value) === '[object Array]') { 295 | 296 | // The value is an array. Stringify every element. Use null as a placeholder 297 | // for non-JSON values. 298 | 299 | length = value.length; 300 | for (i = 0; i < length; i += 1) { 301 | partial[i] = str(i, value) || 'null'; 302 | } 303 | 304 | // Join all of the elements together, separated with commas, and wrap them in 305 | // brackets. 306 | 307 | v = partial.length === 0 308 | ? '[]' 309 | : gap 310 | ? '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' 311 | : '[' + partial.join(',') + ']'; 312 | gap = mind; 313 | return v; 314 | } 315 | 316 | // If the replacer is an array, use it to select the members to be stringified. 317 | 318 | if (rep && typeof rep === 'object') { 319 | length = rep.length; 320 | for (i = 0; i < length; i += 1) { 321 | if (typeof rep[i] === 'string') { 322 | k = rep[i]; 323 | v = str(k, value); 324 | if (v) { 325 | partial.push(quote(k) + (gap ? ': ' : ':') + v); 326 | } 327 | } 328 | } 329 | } else { 330 | 331 | // Otherwise, iterate through all of the keys in the object. 332 | 333 | for (k in value) { 334 | if (Object.prototype.hasOwnProperty.call(value, k)) { 335 | v = str(k, value); 336 | if (v) { 337 | partial.push(quote(k) + (gap ? ': ' : ':') + v); 338 | } 339 | } 340 | } 341 | } 342 | 343 | // Join all of the member texts together, separated with commas, 344 | // and wrap them in braces. 345 | 346 | v = partial.length === 0 347 | ? '{}' 348 | : gap 349 | ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' 350 | : '{' + partial.join(',') + '}'; 351 | gap = mind; 352 | return v; 353 | } 354 | } 355 | 356 | // If the JSON object does not yet have a stringify method, give it one. 357 | 358 | if (typeof JSON.stringify !== 'function') { 359 | JSON.stringify = function (value, replacer, space) { 360 | 361 | // The stringify method takes a value and an optional replacer, and an optional 362 | // space parameter, and returns a JSON text. The replacer can be a function 363 | // that can replace values, or an array of strings that will select the keys. 364 | // A default replacer method can be provided. Use of the space parameter can 365 | // produce text that is more easily readable. 366 | 367 | var i; 368 | gap = ''; 369 | indent = ''; 370 | 371 | // If the space parameter is a number, make an indent string containing that 372 | // many spaces. 373 | 374 | if (typeof space === 'number') { 375 | for (i = 0; i < space; i += 1) { 376 | indent += ' '; 377 | } 378 | 379 | // If the space parameter is a string, it will be used as the indent string. 380 | 381 | } else if (typeof space === 'string') { 382 | indent = space; 383 | } 384 | 385 | // If there is a replacer, it must be a function or an array. 386 | // Otherwise, throw an error. 387 | 388 | rep = replacer; 389 | if (replacer && typeof replacer !== 'function' && 390 | (typeof replacer !== 'object' || 391 | typeof replacer.length !== 'number')) { 392 | throw new Error('JSON.stringify'); 393 | } 394 | 395 | // Make a fake root object containing our value under the key of ''. 396 | // Return the result of stringifying the value. 397 | 398 | return str('', {'': value}); 399 | }; 400 | } 401 | 402 | 403 | // If the JSON object does not yet have a parse method, give it one. 404 | 405 | if (typeof JSON.parse !== 'function') { 406 | JSON.parse = function (text, reviver) { 407 | 408 | // The parse method takes a text and an optional reviver function, and returns 409 | // a JavaScript value if the text is a valid JSON text. 410 | 411 | var j; 412 | 413 | function walk(holder, key) { 414 | 415 | // The walk method is used to recursively walk the resulting structure so 416 | // that modifications can be made. 417 | 418 | var k, v, value = holder[key]; 419 | if (value && typeof value === 'object') { 420 | for (k in value) { 421 | if (Object.prototype.hasOwnProperty.call(value, k)) { 422 | v = walk(value, k); 423 | if (v !== undefined) { 424 | value[k] = v; 425 | } else { 426 | delete value[k]; 427 | } 428 | } 429 | } 430 | } 431 | return reviver.call(holder, key, value); 432 | } 433 | 434 | 435 | // Parsing happens in four stages. In the first stage, we replace certain 436 | // Unicode characters with escape sequences. JavaScript handles many characters 437 | // incorrectly, either silently deleting them, or treating them as line endings. 438 | 439 | text = String(text); 440 | cx.lastIndex = 0; 441 | if (cx.test(text)) { 442 | text = text.replace(cx, function (a) { 443 | return '\\u' + 444 | ('0000' + a.charCodeAt(0).toString(16)).slice(-4); 445 | }); 446 | } 447 | 448 | // In the second stage, we run the text against regular expressions that look 449 | // for non-JSON patterns. We are especially concerned with '()' and 'new' 450 | // because they can cause invocation, and '=' because it can cause mutation. 451 | // But just to be safe, we want to reject all unexpected forms. 452 | 453 | // We split the second stage into 4 regexp operations in order to work around 454 | // crippling inefficiencies in IE's and Safari's regexp engines. First we 455 | // replace the JSON backslash pairs with '@' (a non-JSON character). Second, we 456 | // replace all simple value tokens with ']' characters. Third, we delete all 457 | // open brackets that follow a colon or comma or that begin the text. Finally, 458 | // we look to see that the remaining characters are only whitespace or ']' or 459 | // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. 460 | 461 | if (/^[\],:{}\s]*$/ 462 | .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@') 463 | .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']') 464 | .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { 465 | 466 | // In the third stage we use the eval function to compile the text into a 467 | // JavaScript structure. The '{' operator is subject to a syntactic ambiguity 468 | // in JavaScript: it can begin a block or an object literal. We wrap the text 469 | // in parens to eliminate the ambiguity. 470 | 471 | j = eval('(' + text + ')'); 472 | 473 | // In the optional fourth stage, we recursively walk the new structure, passing 474 | // each name/value pair to a reviver function for possible transformation. 475 | 476 | return typeof reviver === 'function' 477 | ? walk({'': j}, '') 478 | : j; 479 | } 480 | 481 | // If the text is not JSON parseable, then a SyntaxError is thrown. 482 | 483 | throw new SyntaxError('JSON.parse'); 484 | }; 485 | } 486 | }()); --------------------------------------------------------------------------------