pragma-operation 1.6.3 → 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.
- checksums.yaml +4 -4
- data/.rubocop.yml +9 -8
- data/README.md +164 -3
- data/lib/pragma/operation.rb +14 -4
- data/lib/pragma/operation/base.rb +2 -247
- data/lib/pragma/operation/error.rb +27 -0
- data/lib/pragma/operation/response.rb +102 -0
- data/lib/pragma/operation/response/bad_request.rb +16 -0
- data/lib/pragma/operation/response/created.rb +13 -0
- data/lib/pragma/operation/response/forbidden.rb +19 -0
- data/lib/pragma/operation/response/no_content.rb +13 -0
- data/lib/pragma/operation/response/not_found.rb +19 -0
- data/lib/pragma/operation/response/ok.rb +13 -0
- data/lib/pragma/operation/response/unprocessable_entity.rb +23 -0
- data/lib/pragma/operation/version.rb +2 -1
- data/pragma-operation.gemspec +16 -19
- metadata +15 -55
- data/doc/01-basic-usage.md +0 -264
- data/doc/02-contracts.md +0 -154
- data/doc/03-policies.md +0 -179
- data/doc/04-decorators.md +0 -50
- data/lib/pragma/operation/authorization.rb +0 -130
- data/lib/pragma/operation/decoration.rb +0 -76
- data/lib/pragma/operation/validation.rb +0 -146
data/doc/02-contracts.md
DELETED
@@ -1,154 +0,0 @@
|
|
1
|
-
# Contracts
|
2
|
-
|
3
|
-
Operations integrate with [Pragma::Contract](https://github.com/pragmarb/pragma-contract). You can
|
4
|
-
specify the contract to use with `#contract` and get access to `#validate` and `#validate!` in your
|
5
|
-
operations:
|
6
|
-
|
7
|
-
```ruby
|
8
|
-
module API
|
9
|
-
module V1
|
10
|
-
module Post
|
11
|
-
module Operation
|
12
|
-
class Create < Pragma::Operation::Base
|
13
|
-
contract API::V1::Post::Contract::Create
|
14
|
-
|
15
|
-
def call
|
16
|
-
post = Post.new
|
17
|
-
|
18
|
-
validate! contract
|
19
|
-
contract.save
|
20
|
-
|
21
|
-
respond_with status: :created, resource: post
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|
27
|
-
end
|
28
|
-
```
|
29
|
-
|
30
|
-
You can also pass a block to compute the contract class dynamically. If the block returns `nil`,
|
31
|
-
validation will be skipped:
|
32
|
-
|
33
|
-
```ruby
|
34
|
-
module API
|
35
|
-
module V1
|
36
|
-
module Post
|
37
|
-
module Operation
|
38
|
-
class Create < Pragma::Operation::Base
|
39
|
-
contract do |context|
|
40
|
-
# ...
|
41
|
-
end
|
42
|
-
|
43
|
-
def call
|
44
|
-
# ...
|
45
|
-
end
|
46
|
-
end
|
47
|
-
end
|
48
|
-
end
|
49
|
-
end
|
50
|
-
end
|
51
|
-
```
|
52
|
-
|
53
|
-
If the contract passes validation, then all is good. If not, an error is raised:
|
54
|
-
|
55
|
-
```ruby
|
56
|
-
result1 = API::V1::Post::Operation::Create.call(params: {
|
57
|
-
title: 'My First Post',
|
58
|
-
body: 'Hello everyone, this is my first post!'
|
59
|
-
})
|
60
|
-
|
61
|
-
result1.status # => :created
|
62
|
-
result1.resource
|
63
|
-
# => {
|
64
|
-
# 'title' => 'My First Post'
|
65
|
-
# 'body' => 'Hello everyone, this is my first post!'
|
66
|
-
# }
|
67
|
-
|
68
|
-
result2 = API::V1::Post::Operation::Create.call(params: {
|
69
|
-
title: 'My First Post'
|
70
|
-
})
|
71
|
-
|
72
|
-
result2.status # => :forbidden
|
73
|
-
result2.resource
|
74
|
-
# => {
|
75
|
-
# 'error_type' => 'unprocessable_entity',
|
76
|
-
# 'error_message' => 'The contract for this operation was not respected.',
|
77
|
-
# 'meta' => {
|
78
|
-
# 'errors' => {
|
79
|
-
# 'body' => ["can't be blank"]
|
80
|
-
# }
|
81
|
-
# }
|
82
|
-
# }
|
83
|
-
```
|
84
|
-
|
85
|
-
You can also use the non-bang method `#validate` to implement your own logic:
|
86
|
-
|
87
|
-
```ruby
|
88
|
-
module API
|
89
|
-
module V1
|
90
|
-
module Post
|
91
|
-
module Operation
|
92
|
-
class Create < Pragma::Operation::Base
|
93
|
-
contract API::V1::Post::Contract::Create
|
94
|
-
|
95
|
-
def call
|
96
|
-
post = Post.new
|
97
|
-
contract = build_contract(post)
|
98
|
-
|
99
|
-
unless validate(contract)
|
100
|
-
respond_with!(
|
101
|
-
status: :unprocessable_entity,
|
102
|
-
resource: nil
|
103
|
-
)
|
104
|
-
end
|
105
|
-
|
106
|
-
contract.save
|
107
|
-
|
108
|
-
respond_with status: :created, resource: post
|
109
|
-
end
|
110
|
-
end
|
111
|
-
end
|
112
|
-
end
|
113
|
-
end
|
114
|
-
end
|
115
|
-
```
|
116
|
-
|
117
|
-
If you want to run some logic after validation, you can override the `#after_validation` method
|
118
|
-
in your operation. It takes the result of the validation as its first argument:
|
119
|
-
|
120
|
-
```ruby
|
121
|
-
module API
|
122
|
-
module V1
|
123
|
-
module Post
|
124
|
-
module Operation
|
125
|
-
class Create < Pragma::Operation::Base
|
126
|
-
contract API::V1::Post::Contract::Create
|
127
|
-
|
128
|
-
def call
|
129
|
-
post = Post.new
|
130
|
-
contract = build_contract(post)
|
131
|
-
|
132
|
-
unless validate(contract)
|
133
|
-
respond_with!(
|
134
|
-
status: :unprocessable_entity,
|
135
|
-
resource: nil
|
136
|
-
)
|
137
|
-
end
|
138
|
-
|
139
|
-
contract.save
|
140
|
-
|
141
|
-
respond_with status: :created, resource: post
|
142
|
-
end
|
143
|
-
|
144
|
-
protected
|
145
|
-
|
146
|
-
def after_validation(result)
|
147
|
-
# ...
|
148
|
-
end
|
149
|
-
end
|
150
|
-
end
|
151
|
-
end
|
152
|
-
end
|
153
|
-
end
|
154
|
-
```
|
data/doc/03-policies.md
DELETED
@@ -1,179 +0,0 @@
|
|
1
|
-
# Policies
|
2
|
-
|
3
|
-
Operations integrate with [Pragma::Policy](https://github.com/pragmarb/pragma-policy). All you have
|
4
|
-
to do is specify the policy class with `#policy`:
|
5
|
-
|
6
|
-
```ruby
|
7
|
-
module API
|
8
|
-
module V1
|
9
|
-
module Post
|
10
|
-
module Operation
|
11
|
-
class Create < Pragma::Operation::Base
|
12
|
-
policy API::V1::Post::Policy
|
13
|
-
|
14
|
-
def call
|
15
|
-
post = Post.new(params)
|
16
|
-
authorize! post
|
17
|
-
|
18
|
-
post.save!
|
19
|
-
|
20
|
-
respond_with status: :created, resource: post
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|
27
|
-
```
|
28
|
-
|
29
|
-
You can also pass a block to compute the policy class dynamically. If the block returns `nil`,
|
30
|
-
authorization will be skipped:
|
31
|
-
|
32
|
-
```ruby
|
33
|
-
module API
|
34
|
-
module V1
|
35
|
-
module Post
|
36
|
-
module Operation
|
37
|
-
class Create < Pragma::Operation::Base
|
38
|
-
policy do |context|
|
39
|
-
# ...
|
40
|
-
end
|
41
|
-
|
42
|
-
def call
|
43
|
-
# ...
|
44
|
-
end
|
45
|
-
end
|
46
|
-
end
|
47
|
-
end
|
48
|
-
end
|
49
|
-
end
|
50
|
-
```
|
51
|
-
|
52
|
-
Of course, you will now have to pass the user performing the operation in addition to the usual
|
53
|
-
parameters:
|
54
|
-
|
55
|
-
```ruby
|
56
|
-
result1 = API::V1::Post::Operation::Create.call(
|
57
|
-
params: {
|
58
|
-
title: 'My First Post',
|
59
|
-
body: 'Hello everyone, this is my first post!'
|
60
|
-
},
|
61
|
-
current_user: authorized_user
|
62
|
-
)
|
63
|
-
|
64
|
-
result1.status # => :created
|
65
|
-
result1.resource
|
66
|
-
# => {
|
67
|
-
# 'title' => 'My First Post'
|
68
|
-
# 'body' => 'Hello everyone, this is my first post!'
|
69
|
-
# }
|
70
|
-
|
71
|
-
result2 = API::V1::Post::Operation::Create.call(
|
72
|
-
params: {
|
73
|
-
title: 'My First Post',
|
74
|
-
body: 'Hello everyone, this is my first post!'
|
75
|
-
},
|
76
|
-
current_user: unauthorized_user
|
77
|
-
)
|
78
|
-
|
79
|
-
result2.status # => :forbidden
|
80
|
-
result2.resource
|
81
|
-
# => {
|
82
|
-
# 'error_type' => 'forbidden',
|
83
|
-
# 'error_message' => 'You are not authorized to perform this operation.'
|
84
|
-
# }
|
85
|
-
```
|
86
|
-
|
87
|
-
If you want to customize how you handle authorization, you can use the non-bang method `#authorize`:
|
88
|
-
|
89
|
-
```ruby
|
90
|
-
module API
|
91
|
-
module V1
|
92
|
-
module Post
|
93
|
-
module Operation
|
94
|
-
class Create < Pragma::Operation::Base
|
95
|
-
policy API::V1::Post::Policy
|
96
|
-
|
97
|
-
def call
|
98
|
-
post = Post.new(params)
|
99
|
-
|
100
|
-
unless authorize(post)
|
101
|
-
respond_with!(
|
102
|
-
status: :forbidden,
|
103
|
-
resource: nil # if you don't need error info
|
104
|
-
)
|
105
|
-
end
|
106
|
-
|
107
|
-
post.save!
|
108
|
-
|
109
|
-
respond_with status: :created, resource: post
|
110
|
-
end
|
111
|
-
end
|
112
|
-
end
|
113
|
-
end
|
114
|
-
end
|
115
|
-
end
|
116
|
-
```
|
117
|
-
|
118
|
-
If you want to run some logic after authorization, you can override the `#after_authorization` method
|
119
|
-
in your operation. It takes the result of the authorization as its first argument:
|
120
|
-
|
121
|
-
```ruby
|
122
|
-
module API
|
123
|
-
module V1
|
124
|
-
module Post
|
125
|
-
module Operation
|
126
|
-
class Create < Pragma::Operation::Base
|
127
|
-
policy API::V1::Post::Policy
|
128
|
-
|
129
|
-
def call
|
130
|
-
post = Post.new(params)
|
131
|
-
|
132
|
-
unless authorize(post)
|
133
|
-
respond_with!(
|
134
|
-
status: :forbidden,
|
135
|
-
resource: nil # if you don't need error info
|
136
|
-
)
|
137
|
-
end
|
138
|
-
|
139
|
-
post.save!
|
140
|
-
|
141
|
-
respond_with status: :created, resource: post
|
142
|
-
end
|
143
|
-
|
144
|
-
protected
|
145
|
-
|
146
|
-
def after_authorization(result)
|
147
|
-
# ...
|
148
|
-
end
|
149
|
-
end
|
150
|
-
end
|
151
|
-
end
|
152
|
-
end
|
153
|
-
end
|
154
|
-
```
|
155
|
-
|
156
|
-
## Authorizing collections
|
157
|
-
|
158
|
-
To authorize a collection, use `#authorize_collection`. This will call `.accessible_by` on the
|
159
|
-
policy class with the current user and the provided collection and return an authorized collection
|
160
|
-
containing only records accessible by the user.
|
161
|
-
|
162
|
-
```ruby
|
163
|
-
module API
|
164
|
-
module V1
|
165
|
-
module Post
|
166
|
-
module Operation
|
167
|
-
class Index < Pragma::Operation::Base
|
168
|
-
policy API::V1::Post::Policy
|
169
|
-
|
170
|
-
def call
|
171
|
-
posts = authorize_collection Post.all
|
172
|
-
respond_with status: :ok, resource: posts
|
173
|
-
end
|
174
|
-
end
|
175
|
-
end
|
176
|
-
end
|
177
|
-
end
|
178
|
-
end
|
179
|
-
```
|
data/doc/04-decorators.md
DELETED
@@ -1,50 +0,0 @@
|
|
1
|
-
# Decorators
|
2
|
-
|
3
|
-
Operations integrate with [Pragma::Decorator](https://github.com/pragmarb/pragma-decorator). All you
|
4
|
-
have to do is specify the decorator class with `#decorator`. This will give you access to
|
5
|
-
`#decorate`:
|
6
|
-
|
7
|
-
```ruby
|
8
|
-
module API
|
9
|
-
module V1
|
10
|
-
module Post
|
11
|
-
module Operation
|
12
|
-
class Show < Pragma::Operation::Base
|
13
|
-
decorator API::V1::Post::Decorator
|
14
|
-
|
15
|
-
def call
|
16
|
-
post = Post.find(params[:id])
|
17
|
-
respond_with status: :ok, resource: decorate(post)
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
24
|
-
```
|
25
|
-
|
26
|
-
You can also pass a block to compute the decorator class dynamically. If the block returns `nil`,
|
27
|
-
decoration will be skipped:
|
28
|
-
|
29
|
-
```ruby
|
30
|
-
module API
|
31
|
-
module V1
|
32
|
-
module Post
|
33
|
-
module Operation
|
34
|
-
class Show < Pragma::Operation::Base
|
35
|
-
decorator do |context|
|
36
|
-
# ...
|
37
|
-
end
|
38
|
-
|
39
|
-
def call
|
40
|
-
# ...
|
41
|
-
end
|
42
|
-
end
|
43
|
-
end
|
44
|
-
end
|
45
|
-
end
|
46
|
-
end
|
47
|
-
```
|
48
|
-
|
49
|
-
Note that `#decorate` works with both singular resources and collections, as it uses the decorator's
|
50
|
-
[`.represent`](http://trailblazer.to/gems/representable/3.0/api.html) method.
|
@@ -1,130 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
module Pragma
|
3
|
-
module Operation
|
4
|
-
# Provides integration with {https://github.com/pragmarb/pragma-policy Pragma::Policy}.
|
5
|
-
#
|
6
|
-
# @author Alessandro Desantis
|
7
|
-
module Authorization
|
8
|
-
def self.included(base)
|
9
|
-
base.extend ClassMethods
|
10
|
-
base.include InstanceMethods
|
11
|
-
end
|
12
|
-
|
13
|
-
module ClassMethods # :nodoc:
|
14
|
-
# Sets the policy to use for authorizing this operation.
|
15
|
-
#
|
16
|
-
# @param klass [Class] a subclass of +Pragma::Policy::Base+
|
17
|
-
#
|
18
|
-
# @yield A block which will be called with the operation's context which should return
|
19
|
-
# the policy class. The block can also return +nil+ if authorization should be skipped.
|
20
|
-
def policy(klass = nil, &block)
|
21
|
-
if !klass && !block_given?
|
22
|
-
fail ArgumentError, 'You must pass either a policy class or a block'
|
23
|
-
end
|
24
|
-
|
25
|
-
@policy = klass || block
|
26
|
-
end
|
27
|
-
|
28
|
-
# Returns the policy class.
|
29
|
-
#
|
30
|
-
# @return [Class]
|
31
|
-
def policy_klass
|
32
|
-
@policy
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
|
-
module InstanceMethods # :nodoc:
|
37
|
-
# Builds the policy for the current user and the given resource, using the previously
|
38
|
-
# defined policy class.
|
39
|
-
#
|
40
|
-
# @param resource [Object]
|
41
|
-
#
|
42
|
-
# @return [Pragma::Policy::Base]
|
43
|
-
#
|
44
|
-
# @see .policy
|
45
|
-
# @see .build_policy
|
46
|
-
def build_policy(resource)
|
47
|
-
policy_klass = compute_policy_klass
|
48
|
-
return resource unless policy_klass
|
49
|
-
|
50
|
-
policy_klass.new(user: current_user, resource: resource)
|
51
|
-
end
|
52
|
-
|
53
|
-
# Authorizes this operation on the provided resource or policy.
|
54
|
-
#
|
55
|
-
# If no policy was defined, simply returns true.
|
56
|
-
#
|
57
|
-
# @param authorizable [Pragma::Policy::Base|Object] resource or policy
|
58
|
-
#
|
59
|
-
# @return [Boolean] whether the operation is authorized
|
60
|
-
def authorize(authorizable)
|
61
|
-
return true unless compute_policy_klass
|
62
|
-
|
63
|
-
# rubocop:disable Metrics/LineLength
|
64
|
-
policy = if Object.const_defined?('Pragma::Policy::Base') && authorizable.is_a?(Pragma::Policy::Base)
|
65
|
-
authorizable
|
66
|
-
else
|
67
|
-
build_policy(authorizable)
|
68
|
-
end
|
69
|
-
# rubocop:enable Metrics/LineLength
|
70
|
-
|
71
|
-
if Object.const_defined?('Pragma::Contract::Base') && authorizable.is_a?(Pragma::Contract::Base)
|
72
|
-
authorizable.deserialize(params)
|
73
|
-
end
|
74
|
-
|
75
|
-
policy.send("#{self.class.operation_name}?").tap do |result|
|
76
|
-
after_authorization result
|
77
|
-
end
|
78
|
-
end
|
79
|
-
|
80
|
-
# Authorizes this operation on the provided resource or policy. If the user is not
|
81
|
-
# authorized to perform the operation, responds with 403 Forbidden and an error body and
|
82
|
-
# halts the execution.
|
83
|
-
#
|
84
|
-
# @param authorizable [Pragma::Policy::Base|Object] resource or policy
|
85
|
-
def authorize!(authorizable)
|
86
|
-
return if authorize(authorizable)
|
87
|
-
|
88
|
-
respond_with!(
|
89
|
-
status: :forbidden,
|
90
|
-
resource: {
|
91
|
-
error_type: :forbidden,
|
92
|
-
error_message: 'You are not authorized to perform this operation.'
|
93
|
-
}
|
94
|
-
)
|
95
|
-
end
|
96
|
-
|
97
|
-
# Runs after authorization is done.
|
98
|
-
#
|
99
|
-
# @param result [Boolean] the result of the authorization
|
100
|
-
def after_authorization(result)
|
101
|
-
end
|
102
|
-
|
103
|
-
# Scopes the provided collection.
|
104
|
-
#
|
105
|
-
# If no policy class is defined, simply returns the collection.
|
106
|
-
#
|
107
|
-
# @param collection [Enumerable]
|
108
|
-
#
|
109
|
-
# @return [Pragma::Decorator::Base|Enumerable]
|
110
|
-
def authorize_collection(collection)
|
111
|
-
policy_klass = compute_policy_klass
|
112
|
-
return collection unless policy_klass
|
113
|
-
|
114
|
-
policy_klass.accessible_by(
|
115
|
-
user: current_user,
|
116
|
-
scope: collection
|
117
|
-
)
|
118
|
-
end
|
119
|
-
|
120
|
-
def compute_policy_klass
|
121
|
-
if self.class.policy_klass.is_a?(Proc)
|
122
|
-
self.class.policy_klass.call(context)
|
123
|
-
else
|
124
|
-
self.class.policy_klass
|
125
|
-
end
|
126
|
-
end
|
127
|
-
end
|
128
|
-
end
|
129
|
-
end
|
130
|
-
end
|