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