tram-policy 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|