tram-policy 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.codeclimate.yml +15 -0
- data/.gitignore +11 -0
- data/.rspec +4 -0
- data/.rubocop.yml +22 -0
- data/.travis.yml +27 -0
- data/CHANGELOG.md +13 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +21 -0
- data/README.md +373 -0
- data/Rakefile +7 -0
- data/bin/tram-policy +4 -0
- data/lib/tram-policy.rb +1 -0
- data/lib/tram/policy.rb +113 -0
- data/lib/tram/policy/error.rb +88 -0
- data/lib/tram/policy/errors.rb +102 -0
- data/lib/tram/policy/generator.rb +111 -0
- data/lib/tram/policy/generator/policy.erb +20 -0
- data/lib/tram/policy/generator/policy_spec.erb +17 -0
- data/lib/tram/policy/inflector.rb +26 -0
- data/lib/tram/policy/matchers.rb +112 -0
- data/lib/tram/policy/validation_error.rb +18 -0
- data/spec/spec_helper.rb +27 -0
- data/spec/tram/policy/error_spec.rb +61 -0
- data/spec/tram/policy/errors_spec.rb +112 -0
- data/spec/tram/policy/inflector_spec.rb +14 -0
- data/spec/tram/policy/matchers_spec.rb +70 -0
- data/spec/tram/policy/validation_error_spec.rb +23 -0
- data/spec/tram/policy_spec.rb +173 -0
- data/tram-policy.gemspec +25 -0
- metadata +182 -0
@@ -0,0 +1,17 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
RSpec.describe <%= klass %>, "#valid?" do
|
4
|
+
subject(:policy) { described_class[<%= policy_signature.join(", ") %>] }
|
5
|
+
|
6
|
+
<% (parsed_params + parsed_options).each do |name| -%>
|
7
|
+
let(:<%= name %>) { FactoryGirl.build :<%= name %> }
|
8
|
+
<% end -%>
|
9
|
+
|
10
|
+
it { is_expected.to be_valid }
|
11
|
+
<% parsed_validators.each do |validator| %>
|
12
|
+
it "is invalid when not <%= validator %>" do
|
13
|
+
policy # modify it correspondingly
|
14
|
+
expect { policy }.to be_invalid_at # add tags to check
|
15
|
+
end
|
16
|
+
<% end -%>
|
17
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
class Tram::Policy
|
2
|
+
if Object.const_defined? "ActiveSupport::Inflector"
|
3
|
+
Inflector = ActiveSupport::Inflector
|
4
|
+
elsif Object.const_defined? "Inflecto"
|
5
|
+
Inflector = ::Inflecto
|
6
|
+
else
|
7
|
+
module Inflector
|
8
|
+
def self.underscore(name)
|
9
|
+
name.dup.tap do |n|
|
10
|
+
n.gsub!(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
11
|
+
n.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
|
12
|
+
n.gsub!("::", "/")
|
13
|
+
n.tr!("-", "_")
|
14
|
+
n.downcase!
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.camelize(name)
|
19
|
+
name.dup.tap do |n|
|
20
|
+
n.gsub!(/(?:\A|_+)(.)/) { $1.upcase }
|
21
|
+
n.gsub!(%r{(?:[/|-]+)(.)}) { "::#{$1.upcase}" }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
require "rspec"
|
2
|
+
|
3
|
+
# Checks that a block provides policy that has errors under given tags
|
4
|
+
# It also check that selected messages has translations to all available locales
|
5
|
+
#
|
6
|
+
# @example
|
7
|
+
# subject(:policy) { UserPolicy[name: nil] }
|
8
|
+
# expect { policy }.to be_invalid_at field: "name", level: "error"
|
9
|
+
#
|
10
|
+
# You have to wrap expectation to a block called for available locales.
|
11
|
+
#
|
12
|
+
RSpec::Matchers.define :be_invalid_at do |**tags|
|
13
|
+
supports_block_expectations
|
14
|
+
|
15
|
+
# ****************************************************************************
|
16
|
+
# Result collectors for all available locations
|
17
|
+
# ****************************************************************************
|
18
|
+
|
19
|
+
attr_accessor :policy
|
20
|
+
|
21
|
+
def errors
|
22
|
+
@errors ||= {}
|
23
|
+
end
|
24
|
+
|
25
|
+
def tags
|
26
|
+
@tags ||= {}
|
27
|
+
end
|
28
|
+
|
29
|
+
def messages
|
30
|
+
@messages ||= {}
|
31
|
+
end
|
32
|
+
|
33
|
+
# ****************************************************************************
|
34
|
+
# Helpers to provide results for all locales
|
35
|
+
# ****************************************************************************
|
36
|
+
|
37
|
+
# Runs block in every available locale
|
38
|
+
def in_available_locales
|
39
|
+
Array(I18n.available_locales).flat_map do |locale|
|
40
|
+
I18n.with_locale(locale) { yield }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Collects results for the current locale
|
45
|
+
def prepare_results(policy_block, tags, locale = I18n.locale)
|
46
|
+
localized_policy = policy_block.call
|
47
|
+
localized_errors = localized_policy&.errors || []
|
48
|
+
self.policy = localized_policy.inspect
|
49
|
+
errors[locale] = localized_errors.by_tags(tags)
|
50
|
+
messages[locale] = localized_errors.full_messages
|
51
|
+
end
|
52
|
+
|
53
|
+
# ****************************************************************************
|
54
|
+
# Checkers for collected results
|
55
|
+
# ****************************************************************************
|
56
|
+
|
57
|
+
# Checks if selected errors are present in all available locales
|
58
|
+
def errored?
|
59
|
+
errors.values.map(&:any?).reduce(true, &:&) == true
|
60
|
+
end
|
61
|
+
|
62
|
+
# Checks if selected errors are absent in all available locales
|
63
|
+
def not_errored?
|
64
|
+
errors.values.map(&:empty?).reduce(true, &:&) == true
|
65
|
+
end
|
66
|
+
|
67
|
+
# Checks if all collected errors are translated
|
68
|
+
def translated?
|
69
|
+
texts = errors.values.flatten.map(&:message)
|
70
|
+
texts.select { |text| text.start_with?("translation missing:") }.empty?
|
71
|
+
end
|
72
|
+
|
73
|
+
def report_errors
|
74
|
+
text = "Actual errors:\n"
|
75
|
+
messages.each do |locale, list|
|
76
|
+
text << " #{locale}:\n"
|
77
|
+
list.each { |item| text << " - #{item}\n" }
|
78
|
+
end
|
79
|
+
text
|
80
|
+
end
|
81
|
+
|
82
|
+
# ****************************************************************************
|
83
|
+
# Positive matcher
|
84
|
+
# ****************************************************************************
|
85
|
+
|
86
|
+
match do |policy_block|
|
87
|
+
in_available_locales { prepare_results(policy_block, tags) }
|
88
|
+
errored? && translated?
|
89
|
+
end
|
90
|
+
|
91
|
+
failure_message do |_|
|
92
|
+
text = "#{policy} should have had errors with tags: #{tags}, "
|
93
|
+
text << "whose messages are translated in all available locales.\n"
|
94
|
+
text << report_errors
|
95
|
+
text
|
96
|
+
end
|
97
|
+
|
98
|
+
# ****************************************************************************
|
99
|
+
# Negative matcher
|
100
|
+
# ****************************************************************************
|
101
|
+
|
102
|
+
match_when_negated do |policy_block|
|
103
|
+
in_available_locales { prepare_results(policy_block, tags) }
|
104
|
+
not_errored?
|
105
|
+
end
|
106
|
+
|
107
|
+
failure_message_when_negated do |_|
|
108
|
+
text = "#{policy} should not have had any error with tags: #{tags}.\n"
|
109
|
+
text << report_errors
|
110
|
+
text
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class Tram::Policy
|
2
|
+
# An exception to be risen by [Tram::Policy#validate!]
|
3
|
+
class ValidationError < RuntimeError
|
4
|
+
# Policy object whose validation has caused the exception
|
5
|
+
#
|
6
|
+
# @return [Tram::Policy]
|
7
|
+
#
|
8
|
+
attr_reader :policy
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def initialize(policy, filter)
|
13
|
+
@policy = policy
|
14
|
+
messages = policy.errors.reject(&filter).map(&:full_message)
|
15
|
+
super (["Validation failed with errors:"] + messages).join("\n- ")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
begin
|
2
|
+
require "pry"
|
3
|
+
rescue
|
4
|
+
nil
|
5
|
+
end
|
6
|
+
require "bundler/setup"
|
7
|
+
require "tram/policy"
|
8
|
+
require "tram/policy/matchers"
|
9
|
+
require "rspec/its"
|
10
|
+
|
11
|
+
RSpec.configure do |config|
|
12
|
+
config.example_status_persistence_file_path = ".rspec_status"
|
13
|
+
config.expect_with :rspec do |c|
|
14
|
+
c.syntax = :expect
|
15
|
+
end
|
16
|
+
|
17
|
+
config.order = :random
|
18
|
+
config.filter_run focus: true
|
19
|
+
config.run_all_when_everything_filtered = true
|
20
|
+
|
21
|
+
# Prepare the Test namespace for constants defined in specs
|
22
|
+
config.around(:each) do |example|
|
23
|
+
Test = Class.new(Module)
|
24
|
+
example.run
|
25
|
+
Object.send :remove_const, :Test
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
RSpec.describe Tram::Policy::Error do
|
2
|
+
subject(:error) { described_class.new "Something bad happened", tags }
|
3
|
+
|
4
|
+
let(:tags) { { level: "warning" } }
|
5
|
+
|
6
|
+
describe "#message" do
|
7
|
+
subject { error.message }
|
8
|
+
it { is_expected.to eq "Something bad happened" }
|
9
|
+
end
|
10
|
+
|
11
|
+
describe "#full_message" do
|
12
|
+
subject { error.full_message }
|
13
|
+
|
14
|
+
context "with tags:" do
|
15
|
+
it { is_expected.to eq "Something bad happened {:level=>\"warning\"}" }
|
16
|
+
end
|
17
|
+
|
18
|
+
context "without tags:" do
|
19
|
+
let(:tags) { {} }
|
20
|
+
it { is_expected.to eq "Something bad happened" }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe "#to_h" do
|
25
|
+
subject { error.to_h }
|
26
|
+
it { is_expected.to eq message: "Something bad happened", level: "warning" }
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "#==" do
|
30
|
+
subject { error == other }
|
31
|
+
|
32
|
+
context "when other object has the same #to_h:" do
|
33
|
+
let(:other) { double to_h: error.to_h }
|
34
|
+
it { is_expected.to eq true }
|
35
|
+
end
|
36
|
+
|
37
|
+
context "when other object has different #to_h:" do
|
38
|
+
let(:other) { double to_h: error.to_h.merge(foo: :bar) }
|
39
|
+
it { is_expected.to eq false }
|
40
|
+
end
|
41
|
+
|
42
|
+
context "when other object not respond to #to_h:" do
|
43
|
+
let(:other) { double }
|
44
|
+
it { is_expected.to eq false }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe "arbitrary tag" do
|
49
|
+
subject { error.send tag }
|
50
|
+
|
51
|
+
context "when tag is defined:" do
|
52
|
+
let(:tag) { "level" }
|
53
|
+
it { is_expected.to eq "warning" }
|
54
|
+
end
|
55
|
+
|
56
|
+
context "when tag not defined:" do
|
57
|
+
let(:tag) { :weight }
|
58
|
+
it { is_expected.to be_nil }
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
RSpec.describe Tram::Policy::Errors do
|
2
|
+
let(:policy) { double :policy, t: "OMG!" }
|
3
|
+
let(:errors) { described_class.new(policy) }
|
4
|
+
|
5
|
+
describe ".new" do
|
6
|
+
subject { errors }
|
7
|
+
|
8
|
+
it { is_expected.to be_kind_of Enumerable }
|
9
|
+
it { is_expected.to respond_to :empty? }
|
10
|
+
it { is_expected.to be_empty }
|
11
|
+
its(:policy) { is_expected.to eql policy }
|
12
|
+
end
|
13
|
+
|
14
|
+
describe "#add" do
|
15
|
+
subject { errors.add :omg, level: "info", field: "name" }
|
16
|
+
let(:error) { errors.to_a.last }
|
17
|
+
|
18
|
+
it "adds an error to the collection:" do
|
19
|
+
expect { 2.times { subject } }.to change { errors.count }.by 1
|
20
|
+
|
21
|
+
expect(error).to be_kind_of Tram::Policy::Error
|
22
|
+
expect(error).to eq message: "OMG!", level: "info", field: "name"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe "#merge" do
|
27
|
+
let(:other) { described_class.new(policy) }
|
28
|
+
|
29
|
+
before do
|
30
|
+
errors.add "D'OH!", level: "disaster"
|
31
|
+
other.add "OUCH!", level: "error"
|
32
|
+
end
|
33
|
+
|
34
|
+
context "without a block:" do
|
35
|
+
subject { errors.merge(other) }
|
36
|
+
|
37
|
+
it "merges other collection as is" do
|
38
|
+
expect(subject).to be_a Tram::Policy::Errors
|
39
|
+
expect(subject.map(&:to_h)).to match_array [
|
40
|
+
{ message: "OMG!", level: "disaster" },
|
41
|
+
{ message: "OMG!", level: "error" }
|
42
|
+
]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
context "with a block:" do
|
47
|
+
subject { errors.merge(other) { |err| err.merge(source: "Homer") } }
|
48
|
+
|
49
|
+
it "merges filtered collection as is" do
|
50
|
+
expect(subject).to be_a Tram::Policy::Errors
|
51
|
+
expect(subject.map(&:to_h)).to match_array [
|
52
|
+
{ message: "OMG!", level: "disaster" },
|
53
|
+
{ message: "OMG!", level: "error", source: "Homer" }
|
54
|
+
]
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
context "not errors:" do
|
59
|
+
subject { errors.merge 1 }
|
60
|
+
it { is_expected.to eql errors }
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
describe "#messages" do
|
65
|
+
subject { errors.messages }
|
66
|
+
|
67
|
+
it { is_expected.to eq [] }
|
68
|
+
|
69
|
+
context "with errors added:" do
|
70
|
+
before { errors.add "OMG!", level: "info", field: "name" }
|
71
|
+
it { is_expected.to eq %w[OMG!] }
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
describe "#full_messages" do
|
76
|
+
subject { errors.full_messages }
|
77
|
+
|
78
|
+
it { is_expected.to eq [] }
|
79
|
+
|
80
|
+
context "with errors added:" do
|
81
|
+
before { errors.add "OMG!", level: "info", field: "name" }
|
82
|
+
it { is_expected.to eq ["OMG! {:level=>\"info\", :field=>\"name\"}"] }
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
describe "#by_tags" do
|
87
|
+
before do
|
88
|
+
errors.add :foo, field: "name", level: "error"
|
89
|
+
errors.add :foo, field: "email", level: "info"
|
90
|
+
errors.add :foo, field: "email", level: "error"
|
91
|
+
end
|
92
|
+
|
93
|
+
context "with filter" do
|
94
|
+
subject { errors.by_tags level: "error" }
|
95
|
+
|
96
|
+
it "returns selected errors only" do
|
97
|
+
expect(subject.map(&:to_h)).to match_array [
|
98
|
+
{ message: "OMG!", field: "name", level: "error" },
|
99
|
+
{ message: "OMG!", field: "email", level: "error" }
|
100
|
+
]
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
context "without a filter" do
|
105
|
+
subject { errors.by_tags }
|
106
|
+
|
107
|
+
it "returns selected all errors" do
|
108
|
+
expect(subject.map(&:to_h)).to match_array errors.to_a
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
RSpec.describe Tram::Policy::Inflector do
|
2
|
+
let(:snake) { "test/admin2_user_new_policy" }
|
3
|
+
let(:camel) { "Test::Admin2UserNewPolicy" }
|
4
|
+
|
5
|
+
describe "#underscore" do
|
6
|
+
subject { described_class.underscore "Test::Admin2USERNew-Policy" }
|
7
|
+
it { is_expected.to eq snake }
|
8
|
+
end
|
9
|
+
|
10
|
+
describe "#camelize" do
|
11
|
+
subject { described_class.camelize snake }
|
12
|
+
it { is_expected.to eq camel }
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
RSpec.describe "RSpec matchers:" do
|
2
|
+
subject { Test::UserPolicy[name: nil] }
|
3
|
+
|
4
|
+
before do
|
5
|
+
I18n.available_locales = %i[en]
|
6
|
+
I18n.backend.store_translations \
|
7
|
+
:en, { "test/user_policy" => { "name_presence" => "Name is absent" } }
|
8
|
+
|
9
|
+
class Test::UserPolicy < Tram::Policy
|
10
|
+
option :name
|
11
|
+
|
12
|
+
validate :name_presence
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def name_presence
|
17
|
+
return if name
|
18
|
+
errors.add :name_presence, field: "name"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe "to be_invalid_at" do
|
24
|
+
it "passes when some translated error present w/o tags constraint" do
|
25
|
+
expect do
|
26
|
+
expect { subject }.to be_invalid_at
|
27
|
+
end.not_to raise_error
|
28
|
+
end
|
29
|
+
|
30
|
+
it "passes when some translated error present under given tags" do
|
31
|
+
expect do
|
32
|
+
expect { subject }.to be_invalid_at field: "name"
|
33
|
+
end.not_to raise_error
|
34
|
+
end
|
35
|
+
|
36
|
+
it "fails when no errors present under given tags" do
|
37
|
+
expect do
|
38
|
+
expect { subject }.to be_invalid_at field: "email"
|
39
|
+
end.to raise_error RSpec::Expectations::ExpectationNotMetError
|
40
|
+
end
|
41
|
+
|
42
|
+
it "fails when some translations are absent" do
|
43
|
+
I18n.available_locales = %i[ru en]
|
44
|
+
|
45
|
+
expect do
|
46
|
+
expect { subject }.to be_invalid_at field: "name"
|
47
|
+
end.to raise_error RSpec::Expectations::ExpectationNotMetError
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe "not_to be_invalid_at" do
|
52
|
+
it "passes when no errors present under given tags" do
|
53
|
+
expect do
|
54
|
+
expect { subject }.not_to be_invalid_at field: "email"
|
55
|
+
end.not_to raise_error
|
56
|
+
end
|
57
|
+
|
58
|
+
it "fails when some error present under given tags" do
|
59
|
+
expect do
|
60
|
+
expect { subject }.not_to be_invalid_at field: "name"
|
61
|
+
end.to raise_error RSpec::Expectations::ExpectationNotMetError
|
62
|
+
end
|
63
|
+
|
64
|
+
it "fails when some error present w/o tags constraint" do
|
65
|
+
expect do
|
66
|
+
expect { subject }.not_to be_invalid_at
|
67
|
+
end.to raise_error RSpec::Expectations::ExpectationNotMetError
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|