pragma 1.2.6 → 2.0.0

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,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