pragma 2.1.1 → 2.2.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.
Files changed (44) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +1 -0
  3. data/CHANGELOG.md +23 -1
  4. data/README.md +22 -15
  5. data/lib/pragma.rb +21 -15
  6. data/lib/pragma/filter/base.rb +17 -0
  7. data/lib/pragma/filter/equals.rb +18 -0
  8. data/lib/pragma/filter/ilike.rb +18 -0
  9. data/lib/pragma/filter/like.rb +18 -0
  10. data/lib/pragma/filter/scope.rb +18 -0
  11. data/lib/pragma/filter/where.rb +18 -0
  12. data/lib/pragma/macro/classes.rb +98 -0
  13. data/lib/pragma/macro/contract/build.rb +25 -0
  14. data/lib/pragma/macro/contract/persist.rb +25 -0
  15. data/lib/pragma/macro/contract/validate.rb +25 -0
  16. data/lib/pragma/macro/decorator.rb +80 -0
  17. data/lib/pragma/macro/filtering.rb +45 -0
  18. data/lib/pragma/macro/model.rb +28 -0
  19. data/lib/pragma/macro/ordering.rb +81 -0
  20. data/lib/pragma/macro/pagination.rb +94 -0
  21. data/lib/pragma/macro/policy.rb +41 -0
  22. data/lib/pragma/operation/create.rb +1 -1
  23. data/lib/pragma/operation/destroy.rb +2 -2
  24. data/lib/pragma/operation/filter.rb +7 -0
  25. data/lib/pragma/operation/index.rb +3 -3
  26. data/lib/pragma/operation/macro.rb +7 -0
  27. data/lib/pragma/operation/show.rb +1 -1
  28. data/lib/pragma/operation/update.rb +1 -1
  29. data/lib/pragma/version.rb +1 -1
  30. metadata +21 -17
  31. data/lib/pragma/operation/filter/base.rb +0 -20
  32. data/lib/pragma/operation/filter/equals.rb +0 -13
  33. data/lib/pragma/operation/filter/ilike.rb +0 -13
  34. data/lib/pragma/operation/filter/like.rb +0 -13
  35. data/lib/pragma/operation/macro/classes.rb +0 -102
  36. data/lib/pragma/operation/macro/contract/build.rb +0 -27
  37. data/lib/pragma/operation/macro/contract/persist.rb +0 -27
  38. data/lib/pragma/operation/macro/contract/validate.rb +0 -27
  39. data/lib/pragma/operation/macro/decorator.rb +0 -82
  40. data/lib/pragma/operation/macro/filtering.rb +0 -47
  41. data/lib/pragma/operation/macro/model.rb +0 -30
  42. data/lib/pragma/operation/macro/ordering.rb +0 -84
  43. data/lib/pragma/operation/macro/pagination.rb +0 -96
  44. data/lib/pragma/operation/macro/policy.rb +0 -40
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'trailblazer/operation/validate'
4
+
5
+ module Pragma
6
+ module Macro
7
+ module Contract
8
+ def self.Validate(name: 'default', **args)
9
+ step = lambda do |input, options|
10
+ Trailblazer::Operation::Pipetree::Step.new(
11
+ Trailblazer::Operation::Contract::Validate(**args).first
12
+ ).call(input, options).tap do |result|
13
+ unless result
14
+ options['result.response'] = Pragma::Operation::Response::UnprocessableEntity.new(
15
+ errors: options["contract.#{name}"].errors.messages
16
+ ).decorate_with(Pragma::Decorator::Error)
17
+ end
18
+ end
19
+ end
20
+
21
+ [step, name: "contract.#{name}.validate"]
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pragma
4
+ module Macro
5
+ def self.Decorator(name: :instance)
6
+ step = ->(input, options) { Decorator.for(input, name, options) }
7
+ [step, name: "decorator.#{name}"]
8
+ end
9
+
10
+ module Decorator
11
+ class << self
12
+ def for(_input, name, options)
13
+ set_defaults(options)
14
+
15
+ return false unless validate_params(options)
16
+
17
+ options["result.decorator.#{name}"] = options["decorator.#{name}.class"].new(
18
+ options['model']
19
+ )
20
+
21
+ validate_expansion(options, name)
22
+ end
23
+
24
+ private
25
+
26
+ def set_defaults(options)
27
+ hash_options = options.to_hash
28
+
29
+ {
30
+ 'expand.enabled' => true
31
+ }.each_pair do |key, value|
32
+ options[key] = value unless hash_options.key?(key.to_sym)
33
+ end
34
+ end
35
+
36
+ def validate_params(options)
37
+ options['contract.expand'] = Dry::Validation.Schema do
38
+ optional(:expand) do
39
+ if options['expand.enabled']
40
+ array? do
41
+ each(:str?) &
42
+ # This is the ugliest, only way I found to define a dynamic validation tree.
43
+ (options['expand.limit'] ? max_size?(options['expand.limit']) : array?)
44
+ end
45
+ else
46
+ none? | empty?
47
+ end
48
+ end
49
+ end
50
+
51
+ options['result.contract.expand'] = options['contract.expand'].call(options['params'])
52
+
53
+ if options['result.contract.expand'].errors.any?
54
+ options['result.response'] = Operation::Response::UnprocessableEntity.new(
55
+ errors: options['result.contract.expand'].errors
56
+ ).decorate_with(Pragma::Decorator::Error)
57
+
58
+ return false
59
+ end
60
+
61
+ true
62
+ end
63
+
64
+ def validate_expansion(options, name)
65
+ return true unless options["result.decorator.#{name}"].respond_to?(:validate_expansion)
66
+ options["result.decorator.#{name}"].validate_expansion(options['params'][:expand])
67
+ true
68
+ rescue Pragma::Decorator::Association::ExpansionError => e
69
+ options['result.response'] = Operation::Response::BadRequest.new(
70
+ entity: Pragma::Operation::Error.new(
71
+ error_type: :expansion_error,
72
+ error_message: e.message
73
+ )
74
+ ).decorate_with(Pragma::Decorator::Error)
75
+ false
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pragma
4
+ module Macro
5
+ def self.Filtering
6
+ step = ->(input, options) { Filtering.for(input, options) }
7
+ [step, name: 'filtering']
8
+ end
9
+
10
+ module Filtering
11
+ class << self
12
+ def for(_input, options)
13
+ set_defaults(options)
14
+
15
+ options['model'] = apply_filtering(options)
16
+
17
+ true
18
+ end
19
+
20
+ private
21
+
22
+ def set_defaults(options)
23
+ {
24
+ 'filtering.filters' => []
25
+ }.each_pair do |key, value|
26
+ options[key] = value unless options[key]
27
+ end
28
+ end
29
+
30
+ def apply_filtering(options)
31
+ relation = options['model']
32
+
33
+ options['filtering.filters'].each do |filter|
34
+ value = options['params'][filter.param]
35
+ next unless value.present?
36
+
37
+ relation = filter.apply(relation: options['model'], value: value)
38
+ end
39
+
40
+ relation
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'trailblazer/operation/model'
4
+
5
+ module Pragma
6
+ module Macro
7
+ def self.Model(action = nil)
8
+ step = lambda do |input, options|
9
+ Trailblazer::Operation::Pipetree::Step.new(
10
+ Trailblazer::Operation::Model.for(options['model.class'], action),
11
+ 'model.class' => options['model.class'],
12
+ 'model.action' => action
13
+ ).call(input, options).tap do |result|
14
+ unless result
15
+ options['result.response'] = Pragma::Operation::Response::NotFound.new.decorate_with(
16
+ Pragma::Decorator::Error
17
+ )
18
+ end
19
+ end
20
+ end
21
+
22
+ [step, name: "model.#{action || 'build'}"]
23
+ end
24
+
25
+ module Model
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pragma
4
+ module Macro
5
+ def self.Ordering
6
+ step = ->(input, options) { Ordering.for(input, options) }
7
+ [step, name: 'ordering']
8
+ end
9
+
10
+ module Ordering
11
+ class << self
12
+ def for(_input, options)
13
+ set_defaults(options)
14
+
15
+ unless validate_params(options)
16
+ handle_invalid_contract(options)
17
+ return false
18
+ end
19
+
20
+ order_column = order_column(options)
21
+
22
+ if order_column
23
+ direction = order_direction(options)
24
+ options['model'] = options['model'].order("#{order_column} #{direction}")
25
+ end
26
+
27
+ true
28
+ end
29
+
30
+ private
31
+
32
+ def set_defaults(options)
33
+ default_column = (:created_at if options['model.class']&.method_defined?(:created_at))
34
+
35
+ {
36
+ 'ordering.columns' => [default_column].compact,
37
+ 'ordering.default_column' => default_column,
38
+ 'ordering.default_direction' => :desc,
39
+ 'ordering.column_param' => :order_property,
40
+ 'ordering.direction_param' => :order_direction
41
+ }.each_pair do |key, value|
42
+ options[key] = value unless options[key]
43
+ end
44
+ end
45
+
46
+ def validate_params(options)
47
+ options['contract.ordering'] = Dry::Validation.Schema do
48
+ optional(options['ordering.column_param']).filled do
49
+ str? & included_in?(options['ordering.columns'].map(&:to_s))
50
+ end
51
+ optional(options['ordering.direction_param']).filled do
52
+ str? & included_in?(%w[asc desc ASC DESC])
53
+ end
54
+ end
55
+
56
+ options['result.contract.ordering'] = options['contract.ordering'].call(
57
+ options['params']
58
+ )
59
+
60
+ options['result.contract.ordering'].errors.empty?
61
+ end
62
+
63
+ def handle_invalid_contract(options)
64
+ options['result.response'] = Operation::Response::UnprocessableEntity.new(
65
+ errors: options['result.contract.ordering'].errors
66
+ ).decorate_with(Pragma::Decorator::Error)
67
+ end
68
+
69
+ def order_column(options)
70
+ params = options['params']
71
+ params[options['ordering.column_param']] || options['ordering.default_column']
72
+ end
73
+
74
+ def order_direction(options)
75
+ params = options['params']
76
+ params[options['ordering.direction_param']] || options['ordering.default_direction']
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pragma
4
+ module Macro
5
+ def self.Pagination
6
+ step = ->(input, options) { Pagination.for(input, options) }
7
+ [step, name: 'pagination']
8
+ end
9
+
10
+ module Pagination
11
+ class << self
12
+ def for(_input, options)
13
+ set_defaults(options)
14
+ normalize_params(options)
15
+
16
+ unless validate_params(options)
17
+ handle_invalid_contract(options)
18
+ return false
19
+ end
20
+
21
+ options['model'] = options['model'].paginate(
22
+ page: page(options, **options),
23
+ per_page: per_page(options, **options)
24
+ )
25
+ end
26
+
27
+ private
28
+
29
+ def set_defaults(options)
30
+ {
31
+ 'pagination.page_param' => :page,
32
+ 'pagination.per_page_param' => :per_page,
33
+ 'pagination.default_per_page' => 30,
34
+ 'pagination.max_per_page' => 100
35
+ }.each_pair do |key, value|
36
+ options[key] = value unless options[key]
37
+ end
38
+ end
39
+
40
+ def normalize_params(options)
41
+ # This is required because Rails treats all incoming parameters as strings, since it
42
+ # can't distinguish. Maybe there's a better way to do it?
43
+ options['params'].tap do |p|
44
+ %w[pagination.page_param pagination.per_page_param].each do |key|
45
+ if p[options[key]] && p[options[key]].respond_to?(:to_i)
46
+ p[options[key]] = p[options[key]].to_i
47
+ end
48
+ end
49
+ end
50
+ end
51
+
52
+ def validate_params(options)
53
+ options['contract.pagination'] = Dry::Validation.Schema do
54
+ optional(options['pagination.page_param']).filled { int? & gteq?(1) }
55
+ optional(options['pagination.per_page_param']).filled do
56
+ int? & (gteq?(1) & lteq?(options['pagination.max_per_page']))
57
+ end
58
+ end
59
+
60
+ options['result.contract.pagination'] = options['contract.pagination'].call(
61
+ options['params']
62
+ )
63
+
64
+ options['result.contract.pagination'].errors.empty?
65
+ end
66
+
67
+ def handle_invalid_contract(options)
68
+ options['result.response'] = Operation::Response::UnprocessableEntity.new(
69
+ errors: options['result.contract.pagination'].errors
70
+ ).decorate_with(Pragma::Decorator::Error)
71
+ end
72
+
73
+ def page(options, params:, **)
74
+ return 1 if
75
+ !params[options['pagination.page_param']] ||
76
+ params[options['pagination.page_param']].blank?
77
+
78
+ params[options['pagination.page_param']].to_i
79
+ end
80
+
81
+ def per_page(options, params:, **)
82
+ return options['pagination.default_per_page'] if
83
+ !params[options['pagination.per_page_param']] ||
84
+ params[options['pagination.per_page_param']].blank?
85
+
86
+ [
87
+ params[options['pagination.per_page_param']].to_i,
88
+ options['pagination.max_per_page']
89
+ ].min
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pragma
4
+ module Macro
5
+ def self.Policy(name: :default, action: nil)
6
+ step = ->(input, options) { Policy.for(input, name, options, action) }
7
+ [step, name: "policy.#{name}"]
8
+ end
9
+
10
+ module Policy
11
+ class << self
12
+ def for(input, name, options, action = nil)
13
+ policy = options["policy.#{name}.class"].new(options['current_user'], options['model'])
14
+
15
+ action_name = action.is_a?(Proc) ? action.call(options) : action
16
+ action_name ||= input.class.operation_name
17
+
18
+ options["result.policy.#{name}"] = Trailblazer::Operation::Result.new(
19
+ policy.send("#{action_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(
35
+ Pragma::Decorator::Error
36
+ )
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -13,7 +13,7 @@ module Pragma
13
13
  step Macro::Contract::Validate()
14
14
  step Macro::Contract::Persist()
15
15
  step Macro::Decorator()
16
- step :respond!
16
+ step :respond!, name: 'respond'
17
17
 
18
18
  def respond!(options)
19
19
  options['result.response'] = Response::Created.new(
@@ -9,8 +9,8 @@ module Pragma
9
9
  step Macro::Classes()
10
10
  step Macro::Model(:find_by)
11
11
  step Macro::Policy()
12
- step :destroy!
13
- step :respond!
12
+ step :destroy!, name: 'destroy'
13
+ step :respond!, name: 'respond'
14
14
 
15
15
  def destroy!(_options, model:, **)
16
16
  unless model.destroy
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pragma
4
+ module Operation
5
+ Filter = Pragma::Filter
6
+ end
7
+ end
@@ -8,13 +8,13 @@ module Pragma
8
8
  # @author Alessandro Desantis
9
9
  class Index < Pragma::Operation::Base
10
10
  step Macro::Classes()
11
- step :retrieve!
12
- step :scope!
11
+ step :retrieve!, name: 'retrieve'
12
+ step :scope!, name: 'scope'
13
13
  step Macro::Filtering()
14
14
  step Macro::Ordering()
15
15
  step Macro::Pagination()
16
16
  step Macro::Decorator(name: :collection)
17
- step :respond!
17
+ step :respond!, name: 'respond'
18
18
 
19
19
  def retrieve!(options)
20
20
  options['model'] = options['model.class'].all