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