pragma 2.1.1 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
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