thoughtbot/shoulda-matchers — pull request #178 by fernandors87 (2012-10-24 03:49:47+00:00)
Title: ValidateFormatOfMatcher improvements

Hello, these days I made some changes in the ValidateFormatOfMatcher for personal use, and I think it can be useful for someone else.
Here is the summary of my changes and a short list of things I'd like to have an opinion.
I apologize in advance for my poor English.

## 1. Renaming with and not_with

This is not really important, but let me know what you think.

### Purpose

IMO, seems more natural use:

```ruby
it { should validate_format_of(:attr).allowing("x") }
it { should validate_format_of(:attr).denying("y") }
```

Instead of:

```ruby
it { should validate_format_of(:attr).with("x") }
it { should validate_format_of(:attr).not_with("y") }
```

### To discuss

1. As this change doesn't increase any (real)functionality, do you think it's convenient to keep it up?
2. If yes, there's interest in keeping the old method names?
3. If yes again, should I treat the 'old' methods as an alias or the 'new' ones? Should I mark the old methods as subject to deprecation?

## 2. The #placing method

### Purpose

Enlarge the specs coverage writing less.
Instead of write one spec for each possible issue, the programmer can supply an example and let the computer iterate over it, changing some characters and checking if it matches.

### Example

```ruby
class User < ActiveRecord::Base
validates_format_of :name, :with => /^[a-zA-Z ]+$/
end

it "should valid lowercased letters" { should validate_format_of(:name).with("thomas paine") }
it "should valid uppercased letters" { should validate_format_of(:name).with("THOMAS PAINE") }
it "should not valid numbers" { should validate_format_of(:name).with("thomas pa1ne") }
it "should not valid dots" { should validate_format_of(:name).with("THOMAS PA.NE") }
```

As an "ideal" scenario, the tester must specify all possible success and failures, sometimes it can spend much time.
i.e., in the above example, the tester must be sure that all letters and spaces will pass, and all that is not letter or space will fail.
What led me to create the #placing method, to decrease the trade-off between the tests coverage and the amount written.

```ruby
it "should validate :name format by allowing 'thomas paine' with lowercase, uppercase or space characters" do
should validate_format_of(:name).allowing("thomas paine").placing(/[\p{Lu}\p{Ll} ]/u)
end
it "should validate :name format by denying 'thomas paine' with non lowercase, uppercase or space characters" do
should validate_format_of(:name).denying("thomas paine").placing(/[^\p{Lu}\p{Ll} ]/u)
end
```

### How it works

When #placing is called, a list of characters is made based on arg regexp.
To see all characters generated by `/[\p{Lu}\p{Ll} ]/u` run the following in your IRB:

```ruby
(0..591).map{ |c| c.chr("UTF-8") }.grep(/[^\p{Lu}\p{Ll} ]/u)
```

The #matches? will make a list of words replacing these characters into the 'thomas paine'.
The position of the replaced character is random:

```ruby
["t omas paine", "thomaA paine", "thomasBpaine", ..., "thomas pɏine"]
```

If a single word doesn't match, #matches? stops evaluation e return false.
i.e. All words must match and the spec will pass.

For `.placing(/[\p{Lu}\p{Ll} ]/u)`:

```ruby
"t omas paine" #=> value accepted, test pass, proceed
"thomaA paine" #=> value accepted, test pass, proceed
"thomasBpaine" #=> value accepted, test pass, proceed
# ...
"thÀmas paine" #=> value denied, test fail, stops evaluation and return false
```

For `.placing(/[^\p{Lu}\p{Ll} ]/u)`:

```ruby
"thom\u0000s paine" #=> value denied, test pass, proceed
"thomas pain\u0001" #=> value denied, test pass, proceed
# ...
"\ahomas paine" #=> value denied, test pass, proceed
# ...
"thomas \naine" #=> value accepted, test fail, stops evaluation and return false
```

### To discuss

1. For performance reasons, now the matcher cover only Latin character (0..591) by default. May be interesting open this to be changed by configuration. In this case where can I place a method to change this range? Inside the matcher?

## 3. The #at method

As #placing sets a random place for characters. The #at provide means to specify where the characters will be replaced.

```ruby
class Client < ActiveRecord::Base
validates_format_of :phone, :with => /\A\+\d{2}\p{Z}\(\d{2}\)\p{Z}\d{4}\p{P}\d{4}\z/u
end

it "should allow expected characters in it's respective positions" do
should validate_format_of(:phone).allowing("?00 (11) 2222-3333").placing("+").at(0)
should validate_format_of(:phone).allowing("+?? (??) ????-????").placing(/[0-9]/).at(1..2, 5..6, 9..12, 14..17)
should validate_format_of(:phone).allowing("+00?(11)?2222-3333").placing(/[\p{Z}]/u).at(3, 8)
should validate_format_of(:phone).allowing("+00 ?11) 2222-3333").placing("(").at(4)
should validate_format_of(:phone).allowing("+00 (11? 2222-3333").placing(")").at(7)
end

it "should deny non expected characters in it's respective positions" do
should validate_format_of(:phone).denying("?00 (11) 2222-3333").placing(/[^+]/).at(0)
should validate_format_of(:phone).denying("+?? (??) ????-????").placing(/[^0-9]/).at(1..2, 5..6, 9..12, 14..17)
should validate_format_of(:phone).denying("+00?(11)?2222-3333").placing(/[\p{Z}]/u).at(3, 8)
should validate_format_of(:phone).denying("+00 ?11) 2222-3333").placing(/[^\(]/).at(4)
should validate_format_of(:phone).denying("+00 (11? 2222-3333").placing(/[^\)]/).at(7)
end
```

## TODO

- Documentation (Will be made after all changes we discuss)

I hope you guys enjoy.

Thanks,
Fernando