pragma 2.1.1 → 2.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.gitignore +1 -0
- data/CHANGELOG.md +23 -1
- data/README.md +22 -15
- data/lib/pragma.rb +21 -15
- data/lib/pragma/filter/base.rb +17 -0
- data/lib/pragma/filter/equals.rb +18 -0
- data/lib/pragma/filter/ilike.rb +18 -0
- data/lib/pragma/filter/like.rb +18 -0
- data/lib/pragma/filter/scope.rb +18 -0
- data/lib/pragma/filter/where.rb +18 -0
- data/lib/pragma/macro/classes.rb +98 -0
- data/lib/pragma/macro/contract/build.rb +25 -0
- data/lib/pragma/macro/contract/persist.rb +25 -0
- data/lib/pragma/macro/contract/validate.rb +25 -0
- data/lib/pragma/macro/decorator.rb +80 -0
- data/lib/pragma/macro/filtering.rb +45 -0
- data/lib/pragma/macro/model.rb +28 -0
- data/lib/pragma/macro/ordering.rb +81 -0
- data/lib/pragma/macro/pagination.rb +94 -0
- data/lib/pragma/macro/policy.rb +41 -0
- data/lib/pragma/operation/create.rb +1 -1
- data/lib/pragma/operation/destroy.rb +2 -2
- data/lib/pragma/operation/filter.rb +7 -0
- data/lib/pragma/operation/index.rb +3 -3
- data/lib/pragma/operation/macro.rb +7 -0
- data/lib/pragma/operation/show.rb +1 -1
- data/lib/pragma/operation/update.rb +1 -1
- data/lib/pragma/version.rb +1 -1
- metadata +21 -17
- data/lib/pragma/operation/filter/base.rb +0 -20
- data/lib/pragma/operation/filter/equals.rb +0 -13
- data/lib/pragma/operation/filter/ilike.rb +0 -13
- data/lib/pragma/operation/filter/like.rb +0 -13
- data/lib/pragma/operation/macro/classes.rb +0 -102
- data/lib/pragma/operation/macro/contract/build.rb +0 -27
- data/lib/pragma/operation/macro/contract/persist.rb +0 -27
- data/lib/pragma/operation/macro/contract/validate.rb +0 -27
- data/lib/pragma/operation/macro/decorator.rb +0 -82
- data/lib/pragma/operation/macro/filtering.rb +0 -47
- data/lib/pragma/operation/macro/model.rb +0 -30
- data/lib/pragma/operation/macro/ordering.rb +0 -84
- data/lib/pragma/operation/macro/pagination.rb +0 -96
- 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
|
@@ -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
|
@@ -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
|