valhammer 0.1.3 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.simplecov +2 -6
- data/README.md +55 -5
- data/lib/valhammer/validations.rb +16 -5
- data/lib/valhammer/version.rb +1 -1
- data/spec/db/schema.rb +3 -0
- data/spec/dummy/config.ru +1 -1
- data/spec/lib/valhammer/validations_spec.rb +46 -10
- data/valhammer.gemspec +2 -1
- metadata +17 -4
- data/.travis.yml +0 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a17bafd0cee09ccf70430c604c381aa2cf9bcd1d
|
4
|
+
data.tar.gz: 1104fae993e967539bf5f96eb2f2b917cfe9fc9f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e48594d0e505482ab9a8915c7336890351e245dd97ba4849ddcf604ae836ef3421b07985a93338d984ba21475064332b3c0008a739b3b515ed360fdf8344fb55
|
7
|
+
data.tar.gz: 75fc238355ba8e2338af112a085f340f427c32c8a0e0b9bfea5fcdc0d89369694d426d15b45055620e2600a7efc9ca5a38e08a683a74b0b94cc09ba948002910
|
data/.simplecov
CHANGED
@@ -1,9 +1,5 @@
|
|
1
|
-
require '
|
2
|
-
|
3
|
-
SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
|
4
|
-
SimpleCov::Formatter::HTMLFormatter,
|
5
|
-
Coveralls::SimpleCov::Formatter
|
6
|
-
]
|
1
|
+
require 'codeclimate-test-reporter'
|
2
|
+
CodeClimate::TestReporter.start
|
7
3
|
|
8
4
|
SimpleCov.start do
|
9
5
|
add_filter('spec')
|
data/README.md
CHANGED
@@ -4,19 +4,18 @@
|
|
4
4
|
[![Build Status][BS img]][Build Status]
|
5
5
|
[![Dependency Status][DS img]][Dependency Status]
|
6
6
|
[![Code Climate][CC img]][Code Climate]
|
7
|
-
[![Coverage Status][CS img]][
|
7
|
+
[![Coverage Status][CS img]][Code Climate]
|
8
8
|
|
9
9
|
[Gem Version]: https://rubygems.org/gems/valhammer
|
10
|
-
[Build Status]: https://
|
10
|
+
[Build Status]: https://codeship.com/projects/91215
|
11
11
|
[Dependency Status]: https://gemnasium.com/ausaccessfed/valhammer
|
12
12
|
[Code Climate]: https://codeclimate.com/github/ausaccessfed/valhammer
|
13
|
-
[Coverage Status]: https://coveralls.io/r/ausaccessfed/valhammer
|
14
13
|
|
15
14
|
[GV img]: https://img.shields.io/gem/v/valhammer.svg
|
16
|
-
[BS img]: https://img.shields.io/
|
15
|
+
[BS img]: https://img.shields.io/codeship/eb0d3cd0-0cd1-0133-3c85-7aae0ba3591b/develop.svg
|
17
16
|
[DS img]: https://img.shields.io/gemnasium/ausaccessfed/valhammer.svg
|
18
17
|
[CC img]: https://img.shields.io/codeclimate/github/ausaccessfed/valhammer.svg
|
19
|
-
[CS img]: https://img.shields.io/
|
18
|
+
[CS img]: https://img.shields.io/codeclimate/coverage/github/ausaccessfed/valhammer.svg
|
20
19
|
|
21
20
|
Automatically validate ActiveRecord models based on the database schema.
|
22
21
|
|
@@ -119,6 +118,39 @@ end
|
|
119
118
|
That is, the last column in the key is the field which gets validated, and the
|
120
119
|
other columns form the `scope` argument.
|
121
120
|
|
121
|
+
If any of the scope columns are nullable, the validation will be conditional on
|
122
|
+
the presence of the scope values. This avoids the situation where your
|
123
|
+
underlying database would accept a row (because it considers `NULL` values not
|
124
|
+
equal to each other, which is true of [SQLite][sqlite-null-index],
|
125
|
+
[PostgreSQL][postgres-null-index] and [MySQL/MariaDB][mysql-null-index] at the
|
126
|
+
least.)
|
127
|
+
|
128
|
+
If the above example table had nullable columns, for example:
|
129
|
+
|
130
|
+
```ruby
|
131
|
+
create_table(:widgets) do |t|
|
132
|
+
t.string :supplier_code, null: true, default: nil
|
133
|
+
t.string :item_code, null: true, default: nil
|
134
|
+
|
135
|
+
t.index [:supplier_code, :item_code], unique: true
|
136
|
+
end
|
137
|
+
```
|
138
|
+
|
139
|
+
This amended table structure causes Valhammer to create validations as though
|
140
|
+
you had written:
|
141
|
+
|
142
|
+
```ruby
|
143
|
+
class Widget < ActiveRecord::Base
|
144
|
+
validates :item_code, uniqueness: { scope: :supplier_code,
|
145
|
+
if: -> { supplier_code },
|
146
|
+
allow_nil: true }
|
147
|
+
end
|
148
|
+
```
|
149
|
+
|
150
|
+
[sqlite-null-index]: https://www.sqlite.org/lang_createindex.html
|
151
|
+
[postgres-null-index]: http://www.postgresql.org/docs/9.0/static/indexes-unique.html
|
152
|
+
[mysql-null-index]: https://dev.mysql.com/doc/refman/5.0/en/create-index.html
|
153
|
+
|
122
154
|
## Duplicate Unique Keys
|
123
155
|
|
124
156
|
Valhammer is able to handle the simple case when multiple unique keys reference
|
@@ -184,6 +216,24 @@ anyway, if it means your association queries benefit from the index).
|
|
184
216
|
|
185
217
|
Alternatively, apply the validation yourself using ActiveRecord.
|
186
218
|
|
219
|
+
## Partial Unique Keys
|
220
|
+
|
221
|
+
When a unique key is partially applied to a relation, that key will not be given
|
222
|
+
a uniqueness validation.
|
223
|
+
|
224
|
+
```ruby
|
225
|
+
create_table(:widgets) do |t|
|
226
|
+
t.string :supplier_code, null: true, default: nil
|
227
|
+
t.string :item_code, null: true, default: nil
|
228
|
+
|
229
|
+
t.index [:supplier_code, :item_code], unique: true,
|
230
|
+
where: 'item_code LIKE "a%"'
|
231
|
+
end
|
232
|
+
```
|
233
|
+
|
234
|
+
In this case, it is not possible for valhammer to determine the behaviour of the
|
235
|
+
`where` clause, so the validation must be manually created.
|
236
|
+
|
187
237
|
## Contributing
|
188
238
|
|
189
239
|
Refer to [GitHub Flow](https://guides.github.com/introduction/flow/) for
|
@@ -62,16 +62,21 @@ module Valhammer
|
|
62
62
|
def valhammer_unique(validations, column, opts)
|
63
63
|
return unless opts[:uniqueness]
|
64
64
|
|
65
|
-
unique_keys =
|
66
|
-
i.unique && i.columns.last == column.name
|
67
|
-
end
|
68
|
-
|
65
|
+
unique_keys = valhammer_unique_keys(column)
|
69
66
|
return unless unique_keys.one?
|
70
67
|
|
71
68
|
scope = unique_keys.first.columns[0..-2]
|
72
69
|
|
73
|
-
|
70
|
+
validations[:uniqueness] = valhammer_unique_opts(scope)
|
71
|
+
end
|
72
|
+
|
73
|
+
def valhammer_unique_opts(scope)
|
74
|
+
nullable = scope.select { |c| columns_hash[c].null }
|
75
|
+
|
76
|
+
opts = { allow_nil: true }
|
74
77
|
opts[:scope] = scope if scope.any?
|
78
|
+
opts[:if] = -> { nullable.all? { |c| send(c) } } if nullable.any?
|
79
|
+
opts
|
75
80
|
end
|
76
81
|
|
77
82
|
def valhammer_numeric(validations, column, opts)
|
@@ -108,6 +113,12 @@ module Valhammer
|
|
108
113
|
field == primary_key || VALHAMMER_EXCLUDED_FIELDS.include?(field)
|
109
114
|
end
|
110
115
|
|
116
|
+
def valhammer_unique_keys(column)
|
117
|
+
@valhammer_indexes.select do |i|
|
118
|
+
i.unique && !i.where && i.columns.last == column.name
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
111
122
|
def valhammer_assoc_name(field)
|
112
123
|
reflect_on_all_associations(:belongs_to)
|
113
124
|
.find { |a| a.foreign_key == field }.try(:name)
|
data/lib/valhammer/version.rb
CHANGED
data/spec/db/schema.rb
CHANGED
@@ -15,17 +15,20 @@ ActiveRecord::Schema.define(version: 0) do
|
|
15
15
|
t.timestamps null: false
|
16
16
|
|
17
17
|
t.index :identifier, unique: true
|
18
|
+
t.index [:organisation_id, :name], unique: true
|
18
19
|
end
|
19
20
|
|
20
21
|
create_table :capabilities, force: true do |t|
|
21
22
|
t.belongs_to :organisation, null: true, default: nil
|
22
23
|
|
23
24
|
t.string :name, null: false, default: nil
|
25
|
+
t.string :identifier, null: false, default: nil
|
24
26
|
t.boolean :core, null: true, default: nil
|
25
27
|
|
26
28
|
t.timestamps null: false
|
27
29
|
|
28
30
|
t.index [:organisation_id, :name], unique: true
|
31
|
+
t.index :identifier, unique: true, where: 'organisation_id IS NULL'
|
29
32
|
end
|
30
33
|
|
31
34
|
create_table :organisations, force: true do |t|
|
data/spec/dummy/config.ru
CHANGED
@@ -1,2 +1,2 @@
|
|
1
|
-
require ::File.expand_path('../config/environment',
|
1
|
+
require ::File.expand_path('../config/environment', __FILE__)
|
2
2
|
run Rails.application
|
@@ -1,4 +1,11 @@
|
|
1
1
|
RSpec.describe Valhammer::Validations do
|
2
|
+
around do |example|
|
3
|
+
ActiveRecord::Base.transaction do
|
4
|
+
example.run
|
5
|
+
fail(ActiveRecord::Rollback)
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
2
9
|
def validation_impl(kind)
|
3
10
|
name = "#{kind.to_s.camelize}Validator"
|
4
11
|
|
@@ -10,8 +17,11 @@ RSpec.describe Valhammer::Validations do
|
|
10
17
|
end
|
11
18
|
|
12
19
|
RSpec::Matchers.define :a_validator_for do |field, kind, opts = nil|
|
20
|
+
include RSpec::Matchers::Composable
|
21
|
+
|
13
22
|
match do |v|
|
14
|
-
v.is_a?(validation_impl(kind)) &&
|
23
|
+
v.is_a?(validation_impl(kind)) &&
|
24
|
+
(opts.nil? || values_match?(opts, v.options)) &&
|
15
25
|
v.attributes.map(&:to_s) == [field.to_s]
|
16
26
|
end
|
17
27
|
|
@@ -85,13 +95,46 @@ RSpec.describe Valhammer::Validations do
|
|
85
95
|
it { is_expected.not_to include(a_validator_for(:mail, :uniqueness)) }
|
86
96
|
end
|
87
97
|
|
88
|
-
context 'with a
|
98
|
+
context 'with a partial unique index' do
|
89
99
|
subject { Capability.validators }
|
90
100
|
|
101
|
+
it { is_expected.not_to include(a_validator_for(:identifier, :uniqueness)) }
|
102
|
+
end
|
103
|
+
|
104
|
+
context 'with a composite unique index' do
|
91
105
|
let(:opts) do
|
92
106
|
{ scope: ['organisation_id'], case_sensitive: true, allow_nil: true }
|
93
107
|
end
|
108
|
+
|
109
|
+
it { is_expected.to include(a_validator_for(:name, :uniqueness, opts)) }
|
110
|
+
end
|
111
|
+
|
112
|
+
context 'with a composite unique index with nullable scope column' do
|
113
|
+
subject { Capability.validators }
|
114
|
+
|
115
|
+
let(:opts) do
|
116
|
+
{ scope: ['organisation_id'], case_sensitive: true, allow_nil: true,
|
117
|
+
if: an_instance_of(Proc) }
|
118
|
+
end
|
119
|
+
|
94
120
|
it { is_expected.to include(a_validator_for(:name, :uniqueness, opts)) }
|
121
|
+
|
122
|
+
it 'skips validation when the nullable scope column is null' do
|
123
|
+
o = Organisation.create!(country: 'Australia', city: 'Brisbane',
|
124
|
+
name: 'Test Organisation')
|
125
|
+
|
126
|
+
attrs = { organisation_id: o.id, name: 'Software Development',
|
127
|
+
identifier: SecureRandom.urlsafe_base64 }
|
128
|
+
|
129
|
+
Capability.create!(attrs)
|
130
|
+
|
131
|
+
attrs[:identifier] = SecureRandom.urlsafe_base64
|
132
|
+
Capability.create!(attrs.merge(organisation_id: nil))
|
133
|
+
|
134
|
+
attrs[:identifier] = SecureRandom.urlsafe_base64
|
135
|
+
expect(Capability.new(attrs.merge(organisation_id: nil))).to be_valid
|
136
|
+
expect(Capability.new(attrs)).not_to be_valid
|
137
|
+
end
|
95
138
|
end
|
96
139
|
|
97
140
|
context 'with duplicate unique indexes' do
|
@@ -205,13 +248,6 @@ RSpec.describe Valhammer::Validations do
|
|
205
248
|
end
|
206
249
|
|
207
250
|
context 'sanity check' do
|
208
|
-
around do |example|
|
209
|
-
ActiveRecord::Base.transaction do
|
210
|
-
example.run
|
211
|
-
fail(ActiveRecord::Rollback)
|
212
|
-
end
|
213
|
-
end
|
214
|
-
|
215
251
|
let(:organisation) do
|
216
252
|
Organisation.create!(name: 'Enhanced Collaborative Methodologies Pty Ltd',
|
217
253
|
country: 'Australia', city: 'Brisbane')
|
@@ -231,7 +267,7 @@ RSpec.describe Valhammer::Validations do
|
|
231
267
|
context Capability do
|
232
268
|
subject do
|
233
269
|
Capability.create!(organisation: organisation, core: true,
|
234
|
-
name: 'Project Management')
|
270
|
+
name: 'Project Management', identifier: 'pm')
|
235
271
|
end
|
236
272
|
|
237
273
|
it { is_expected.to be_valid }
|
data/valhammer.gemspec
CHANGED
@@ -23,11 +23,12 @@ Gem::Specification.new do |spec|
|
|
23
23
|
spec.add_development_dependency 'rails', '~> 4.1'
|
24
24
|
spec.add_development_dependency 'bundler', '~> 1.7'
|
25
25
|
spec.add_development_dependency 'rake', '~> 10.0'
|
26
|
+
spec.add_development_dependency 'rubocop', '~> 0.34'
|
26
27
|
spec.add_development_dependency 'guard'
|
27
28
|
spec.add_development_dependency 'guard-rubocop'
|
28
29
|
spec.add_development_dependency 'guard-rspec'
|
29
30
|
spec.add_development_dependency 'guard-bundler'
|
30
31
|
spec.add_development_dependency 'simplecov'
|
31
|
-
spec.add_development_dependency '
|
32
|
+
spec.add_development_dependency 'codeclimate-test-reporter'
|
32
33
|
spec.add_development_dependency 'sqlite3'
|
33
34
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: valhammer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Shaun Mangelsdorf
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-
|
11
|
+
date: 2015-10-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -66,6 +66,20 @@ dependencies:
|
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '10.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rubocop
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0.34'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0.34'
|
69
83
|
- !ruby/object:Gem::Dependency
|
70
84
|
name: guard
|
71
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -137,7 +151,7 @@ dependencies:
|
|
137
151
|
- !ruby/object:Gem::Version
|
138
152
|
version: '0'
|
139
153
|
- !ruby/object:Gem::Dependency
|
140
|
-
name:
|
154
|
+
name: codeclimate-test-reporter
|
141
155
|
requirement: !ruby/object:Gem::Requirement
|
142
156
|
requirements:
|
143
157
|
- - ">="
|
@@ -175,7 +189,6 @@ files:
|
|
175
189
|
- ".rspec"
|
176
190
|
- ".rubocop.yml"
|
177
191
|
- ".simplecov"
|
178
|
-
- ".travis.yml"
|
179
192
|
- Gemfile
|
180
193
|
- Guardfile
|
181
194
|
- LICENSE
|