diff --git a/.reek.yml b/.reek.yml index 0be343ab..e765f3c7 100644 --- a/.reek.yml +++ b/.reek.yml @@ -22,6 +22,7 @@ detectors: TooManyStatements: exclude: + - Macro#self.Policy - DefaultEndpoint#default_cases - DefaultEndpoint#default_handler - Macro#self.Decorate @@ -61,6 +62,7 @@ detectors: - Macro#self.Model - Macro#self.Assign - Macro#self.Decorate + - Macro#self.Policy BooleanParameter: exclude: diff --git a/.rubocop.yml b/.rubocop.yml index 4e691426..32bae0db 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -64,6 +64,9 @@ Rails/IndexWith: - lib/macro/schema.rb # Metrics --------------------------------------------------------------------- +Metrics/ParameterLists: + Exclude: + - lib/macro/policy.rb Metrics/BlockLength: ExcludedMethods: @@ -79,8 +82,7 @@ Metrics/BlockLength: Metrics/AbcSize: Exclude: - app/controllers/concerns/default_endpoint.rb - - lib/macro/model.rb - - lib/macro/links_builder.rb + - lib/macro/*.rb Metrics/MethodLength: Max: 15 diff --git a/README.md b/README.md index d7b449e8..1db5de91 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ Project wiki: https://github.com/rubygarage/boilerplate/wiki | ```Macro::ModelRemove``` | Provides to destroy and delete model. | | ```Macro::Renderer``` | Provides to render operation result with specified serializer with strict following Jsonapi specification | | ```Macro::Semantic``` | Provides to set value of semantic marker (```semantic_success``` or ```semantic_failure```) into context | +| ```Macro::Policy``` | Set semantic marker ```semantic_failure``` to equel ```:forbidden``` and add errors if no policy| ### Services diff --git a/lib/macro/policy.rb b/lib/macro/policy.rb new file mode 100644 index 00000000..9882084b --- /dev/null +++ b/lib/macro/policy.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Macro + def self.Policy(policy_class, rule, policy_params: nil, name: :default, model: :model, **) + task = ->((ctx, flow_options), **) { + policy_namespace = :"macro.policy.#{name}" + ctx[policy_namespace] = policy_class.new(ctx[:current_user], ctx[model]) + result = if policy_params + ctx[policy_namespace].public_send(rule, ctx[:policy_params]) + else + ctx[policy_namespace].public_send(rule) + end + unless result + message = "#{policy_class.name.demodulize.underscore}.#{rule}" || 'default' + current_errors = ctx[:errors] || {} + ctx[:errors] = current_errors.deep_merge( + { + policy: [I18n.t("policy.errors.#{message}")] + } + ) + + ctx[:semantic_failure] = :forbidden + end + + signal = result ? Trailblazer::Activity::Right : Trailblazer::Activity::Left + [signal, [ctx, flow_options]] + } + + { task: task, id: "#{name}/#{__method__}_id_#{task.object_id}".underscore } + end +end diff --git a/spec/lib/macro/policy_spec.rb b/spec/lib/macro/policy_spec.rb new file mode 100644 index 00000000..87f6bdcc --- /dev/null +++ b/spec/lib/macro/policy_spec.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +RSpec.describe Macro do + describe '.Policy' do + subject(:result) { described_class::Policy(policy_class, rule)[:task].call(ctx, **flow_options) } + + let(:flow_options) { {} } + let(:rule) { :update_role? } + let(:ctx) { {} } + let(:policy_class) { TestPolicy } + + before { stub_const('TestPolicy', Struct.new(:TestPolicy)) } + + context 'when set policy_params to true' do + subject(:result) do + described_class::Policy(policy_class, + rule, + policy_params: true)[:task].call(ctx, **flow_options) + end + + let(:ctx) { { policy_params: true } } + + it 'is allowed' do + expect(policy_class).to receive_message_chain(:new, rule).with(ctx[:policy_params]).and_return(true) + expect(result[0]).to eq(Trailblazer::Activity::Right) + end + end + + context 'when current_user in have access to model' do + before do + allow(policy_class).to receive_message_chain(:new, rule).and_return(true) + end + + it 'is allowed' do + expect(result[0]).to eq(Trailblazer::Activity::Right) + expect(ctx[:semantic_failure]).to be_nil + end + + it 'is put policy in right namespace' do + expect(result.second.first[:'macro.policy.default']).to be_instance_of(RSpec::Mocks::Double) + expect(result[1][0][:'macro.policy.default']).to be_respond_to(rule) + end + end + + context 'when current_user in have not access to model' do + before do + allow(policy_class).to receive_message_chain(:new, rule).and_return(false) + end + + it 'is not allowed' do + expect(result[0]).to eq(Trailblazer::Activity::Left) + expect(ctx[:semantic_failure]).to eq(:forbidden) + end + end + + context 'when other than default namespace' do + subject(:result) { described_class::Policy(policy_class, rule, name: 'custom')[:task].call(ctx, **flow_options) } + + before do + allow(policy_class).to receive_message_chain(:new, rule).and_return(true) + end + + it 'is put policy in right namespace' do + expect(result[0]).to eq(Trailblazer::Activity::Right) + expect(result[1][0][:'macro.policy.custom']).to be_instance_of(RSpec::Mocks::Double) + expect(result[1][0][:'macro.policy.custom']).to be_respond_to(rule) + expect(result[1][0][:'macro.policy.default']).not_to be_present + end + end + end +end