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.
@@ -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
@@ -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