pragma 1.2.6 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pragma
4
+ module Operation
5
+ module Macro
6
+ def self.Classes
7
+ step = ->(input, options) { Classes.for(input, options) }
8
+ [step, name: 'classes']
9
+ end
10
+
11
+ module Classes
12
+ class << self
13
+ def for(input, options)
14
+ {
15
+ 'model.class' => expected_model_class(input, options),
16
+ 'policy.default.class' => expected_policy_class(input, options),
17
+ 'policy.default.scope.class' => expected_policy_scope_class(input, options),
18
+ 'decorator.instance.class' => expected_instance_decorator_class(input, options),
19
+ 'decorator.collection.class' => expected_collection_decorator_class(input, options),
20
+ 'contract.default.class' => expected_contract_class(input, options)
21
+ }.each_pair do |key, value|
22
+ next if options[key]
23
+
24
+ # FIXME: This entire block is required to trigger Rails autoloading. Ugh.
25
+ begin
26
+ Object.const_get(value)
27
+ rescue NameError => e
28
+ # We check the error message to avoid silently ignoring other NameErrors
29
+ # thrown while initializing the constant.
30
+ if e.message.start_with?('uninitialized constant')
31
+ # Required instead of a simple equality check because loading
32
+ # API::V1::Post::Contract::Index might throw "uninitialized constant
33
+ # API::V1::Post::Contract" if the resource has no contracts at all.
34
+ error_constant = e.message.split.last
35
+ raise e unless value.sub(/\A::/, '').start_with?(error_constant)
36
+ end
37
+ end
38
+
39
+ options[key] = if Object.const_defined?(value)
40
+ Object.const_get(value)
41
+ end
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def resource_namespace(input, _options)
48
+ input.class.name.split('::')[0..-3]
49
+ end
50
+
51
+ def expected_model_class(input, options)
52
+ [
53
+ nil,
54
+ resource_namespace(input, options).last
55
+ ].join('::')
56
+ end
57
+
58
+ def expected_policy_class(input, options)
59
+ [
60
+ resource_namespace(input, options),
61
+ 'Policy'
62
+ ].join('::')
63
+ end
64
+
65
+ def expected_policy_scope_class(input, options)
66
+ "#{expected_policy_class(input, options)}::Scope"
67
+ end
68
+
69
+ def expected_instance_decorator_class(input, options)
70
+ [
71
+ resource_namespace(input, options),
72
+ 'Decorator',
73
+ 'Instance'
74
+ ].join('::')
75
+ end
76
+
77
+ def expected_collection_decorator_class(input, options)
78
+ [
79
+ resource_namespace(input, options),
80
+ 'Decorator',
81
+ 'Collection'
82
+ ].join('::')
83
+ end
84
+
85
+ def expected_contract_class(input, options)
86
+ [
87
+ resource_namespace(input, options),
88
+ 'Contract',
89
+ input.class.name.split('::').last
90
+ ].join('::')
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'trailblazer/operation/contract'
4
+
5
+ module Pragma
6
+ module Operation
7
+ module Macro
8
+ module Contract
9
+ def self.Build(name: 'default', constant: nil, builder: nil)
10
+ step = lambda do |input, options|
11
+ Trailblazer::Operation::Contract::Build.for(
12
+ input,
13
+ options,
14
+ name: name,
15
+ constant: constant,
16
+ builder: builder
17
+ ).tap do |contract|
18
+ contract.current_user = options['current_user']
19
+ end
20
+ end
21
+
22
+ [step, name: 'contract.build']
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'trailblazer/operation/persist'
4
+
5
+ module Pragma
6
+ module Operation
7
+ module Macro
8
+ module Contract
9
+ def self.Persist(**args)
10
+ step = lambda do |input, options|
11
+ Trailblazer::Operation::Pipetree::Step.new(
12
+ Trailblazer::Operation::Contract::Persist(**args).first
13
+ ).call(input, options).tap do |result|
14
+ unless result
15
+ options['result.response'] = Pragma::Operation::Response::UnprocessableEntity.new(
16
+ errors: options['model'].errors.messages
17
+ ).decorate_with(Pragma::Decorator::Error)
18
+ end
19
+ end
20
+ end
21
+
22
+ [step, name: 'persist.save']
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'trailblazer/operation/validate'
4
+
5
+ module Pragma
6
+ module Operation
7
+ module Macro
8
+ module Contract
9
+ def self.Validate(name: 'default', **args)
10
+ step = lambda do |input, options|
11
+ Trailblazer::Operation::Pipetree::Step.new(
12
+ Trailblazer::Operation::Contract::Validate(**args).first
13
+ ).call(input, options).tap do |result|
14
+ unless result
15
+ options['result.response'] = Pragma::Operation::Response::UnprocessableEntity.new(
16
+ errors: options['contract.default'].errors.messages
17
+ ).decorate_with(Pragma::Decorator::Error)
18
+ end
19
+ end
20
+ end
21
+
22
+ [step, name: "contract.#{name}.validate"]
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pragma
4
+ module Operation
5
+ module Macro
6
+ def self.Decorator(name: :instance)
7
+ step = ->(input, options) { Decorator.for(input, name, options) }
8
+ [step, name: "decorator.#{name}"]
9
+ end
10
+
11
+ module Decorator
12
+ class << self
13
+ def for(_input, name, options)
14
+ unless validate_params(options)
15
+ handle_invalid_contract(options)
16
+ return false
17
+ end
18
+
19
+ options["result.decorator.#{name}"] = options["decorator.#{name}.class"].new(
20
+ options['model']
21
+ )
22
+
23
+ validate_expansion(options, name)
24
+ end
25
+
26
+ private
27
+
28
+ def validate_params(options)
29
+ options['contract.expand'] = Dry::Validation.Schema do
30
+ optional(:expand).each(:str?)
31
+ end
32
+
33
+ options['result.contract.expand'] = options['contract.expand'].call(options['params'])
34
+
35
+ options['result.contract.expand'].errors.empty?
36
+ end
37
+
38
+ def handle_invalid_contract(options)
39
+ options['result.response'] = Response::UnprocessableEntity.new(
40
+ errors: options['result.contract.expand'].errors
41
+ ).decorate_with(Pragma::Decorator::Error)
42
+ end
43
+
44
+ def validate_expansion(options, name)
45
+ return true unless options["result.decorator.#{name}"].respond_to?(:validate_expansion)
46
+ options["result.decorator.#{name}"].validate_expansion(options['params'][:expand])
47
+ true
48
+ rescue Pragma::Decorator::Association::ExpansionError => e
49
+ options['result.response'] = Response::BadRequest.new(
50
+ entity: Pragma::Operation::Error.new(
51
+ error_type: :expansion_error,
52
+ error_message: e.message
53
+ )
54
+ ).decorate_with(Pragma::Decorator::Error)
55
+ false
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'trailblazer/operation/model'
4
+
5
+ module Pragma
6
+ module Operation
7
+ module Macro
8
+ def self.Model(action = nil)
9
+ step = lambda do |input, options|
10
+ Trailblazer::Operation::Pipetree::Step.new(
11
+ Trailblazer::Operation::Model.for(options['model.class'], action),
12
+ 'model.class' => options['model.class'],
13
+ 'model.action' => action
14
+ ).call(input, options).tap do |result|
15
+ unless result
16
+ options['result.response'] = Pragma::Operation::Response::NotFound.new.decorate_with(
17
+ Pragma::Decorator::Error
18
+ )
19
+ end
20
+ end
21
+ end
22
+
23
+ [step, name: 'model.build']
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pragma
4
+ module Operation
5
+ module Macro
6
+ def self.Pagination
7
+ step = ->(input, options) { Pagination.for(input, options) }
8
+ [step, name: 'pagination']
9
+ end
10
+
11
+ module Pagination
12
+ class << self
13
+ def for(_input, options)
14
+ set_defaults(options)
15
+ normalize_params(options)
16
+
17
+ unless validate_params(options)
18
+ handle_invalid_contract(options)
19
+ return false
20
+ end
21
+
22
+ options['model'] = options['model'].paginate(
23
+ page: page(options, **options),
24
+ per_page: per_page(options, **options)
25
+ )
26
+ end
27
+
28
+ private
29
+
30
+ def set_defaults(options)
31
+ {
32
+ 'pagination.page_param' => :page,
33
+ 'pagination.per_page_param' => :per_page,
34
+ 'pagination.default_per_page' => 30,
35
+ 'pagination.max_per_page' => 100
36
+ }.each_pair do |key, value|
37
+ options[key] ||= value
38
+ end
39
+ end
40
+
41
+ def normalize_params(options)
42
+ # This is required because Rails treats all incoming parameters as strings, since it
43
+ # can't distinguish. Maybe there's a better way to do it?
44
+ options['params'].tap do |p|
45
+ %w[pagination.page_param pagination.per_page_param].each do |key|
46
+ if p[options[key]] && p[options[key]].respond_to?(:to_i)
47
+ p[options[key]] = p[options[key]].to_i
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ def validate_params(options)
54
+ options['contract.pagination'] = Dry::Validation.Schema do
55
+ optional(options['pagination.page_param']).filled { int? & gteq?(1) }
56
+ optional(options['pagination.per_page_param']).filled { int? & (gteq?(1) & lteq?(options['pagination.max_per_page'])) }
57
+ end
58
+
59
+ options['result.contract.pagination'] = options['contract.pagination'].call(options['params'])
60
+
61
+ options['result.contract.pagination'].errors.empty?
62
+ end
63
+
64
+ def handle_invalid_contract(options)
65
+ options['result.response'] = Response::UnprocessableEntity.new(
66
+ errors: options['result.contract.pagination'].errors
67
+ ).decorate_with(Pragma::Decorator::Error)
68
+ end
69
+
70
+ def page(options, params:, **)
71
+ return 1 if
72
+ !params[options['pagination.page_param']] ||
73
+ params[options['pagination.page_param']].blank?
74
+
75
+ params[options['pagination.page_param']].to_i
76
+ end
77
+
78
+ def per_page(options, params:, **)
79
+ return options['pagination.default_per_page'] if
80
+ !params[options['pagination.per_page_param']] ||
81
+ params[options['pagination.per_page_param']].blank?
82
+
83
+ [
84
+ params[options['pagination.per_page_param']].to_i,
85
+ options['pagination.max_per_page']
86
+ ].min
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'trailblazer/operation/pundit'
4
+
5
+ module Pragma
6
+ module Operation
7
+ module Macro
8
+ def self.Policy(name: :default)
9
+ step = ->(input, options) { Policy.for(input, name, options) }
10
+ [step, name: "policy.#{name}"]
11
+ end
12
+
13
+ module Policy
14
+ class << self
15
+ def for(input, name, options)
16
+ policy = options["policy.#{name}.class"].new(options['current_user'], options['model'])
17
+
18
+ options["result.policy.#{name}"] = Trailblazer::Operation::Result.new(
19
+ policy.send("#{input.class.operation_name}?"),
20
+ 'policy' => policy
21
+ )
22
+
23
+ unless options["result.policy.#{name}"].success?
24
+ handle_unauthorized!(options)
25
+ return false
26
+ end
27
+
28
+ true
29
+ end
30
+
31
+ private
32
+
33
+ def handle_unauthorized!(options)
34
+ options['result.response'] = Pragma::Operation::Response::Forbidden.new.decorate_with(Pragma::Decorator::Error)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -1,26 +1,19 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Pragma
3
4
  module Operation
4
5
  # Finds the requested record, authorizes it and decorates it.
5
6
  #
6
7
  # @author Alessandro Desantis
7
8
  class Show < Pragma::Operation::Base
8
- include Pragma::Operation::Defaults
9
-
10
- def call
11
- context.record = find_record
12
- authorize! context.record
13
-
14
- respond_with resource: decorate(context.record), status: :ok
15
- end
16
-
17
- protected
9
+ step Macro::Classes()
10
+ step Macro::Model(:find_by), fail_fast: true
11
+ step Macro::Policy(), fail_fast: true
12
+ step Macro::Decorator()
13
+ step :respond!
18
14
 
19
- # Finds the requested record.
20
- #
21
- # @return [Object]
22
- def find_record
23
- self.class.model_klass.find(params[:id])
15
+ def respond!(options)
16
+ options['result.response'] = Response::Ok.new(entity: options['result.decorator.instance'])
24
17
  end
25
18
  end
26
19
  end
@@ -1,33 +1,22 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Pragma
3
4
  module Operation
4
- # Finds the requested record, authorizes it, updates it accordingly to the parameters and
5
- # responds with the decorated record.
5
+ # Finds an existing record, updates it and responds with the decorated record.
6
6
  #
7
7
  # @author Alessandro Desantis
8
8
  class Update < Pragma::Operation::Base
9
- include Pragma::Operation::Defaults
10
-
11
- def call
12
- context.record = find_record
13
- context.contract = build_contract(context.record)
14
-
15
- validate! context.contract
16
- authorize! context.contract
17
-
18
- context.contract.save
19
- context.record.save!
20
-
21
- respond_with resource: decorate(context.record), status: :ok
22
- end
23
-
24
- protected
9
+ step Macro::Classes()
10
+ step Macro::Model(:find_by), fail_fast: true
11
+ step Macro::Policy(), fail_fast: true
12
+ step Macro::Contract::Build()
13
+ step Macro::Contract::Validate(), fail_fast: true
14
+ step Macro::Contract::Persist(), fail_fast: true
15
+ step Macro::Decorator()
16
+ step :respond!
25
17
 
26
- # Finds the requested record.
27
- #
28
- # @return [Object]
29
- def find_record
30
- self.class.model_klass.find(params[:id])
18
+ def respond!(options)
19
+ options['result.response'] = Response::Ok.new(entity: options['result.decorator.instance'])
31
20
  end
32
21
  end
33
22
  end