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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 155c66fd00efabe09496be40e05ba3b2c93b5434
4
- data.tar.gz: a724471382751d9e1d588f2cbdd8677c7489ee17
3
+ metadata.gz: b48bd839081e93bf86ae68587a35c6f3ad07fc4d
4
+ data.tar.gz: a0ce83bc5177c74503d82b744126f0f02931093c
5
5
  SHA512:
6
- metadata.gz: 93a1c9a43f9e4750f0e9dbf53f69fdb1c3a480d0f09ba08730f6088cd6b3a31fcb16b82cb3c56071f47dfcfc23965c38826dd3bdc3f5cd229d6efc7ca71cc8eb
7
- data.tar.gz: 3d640627d7f36be9a19891816085584f3d8f176098d12484d82cd4f0eb760f1d955f8b19053f8caf959d07013d84876a0cc1f09425561e25dd82bd5dce236c44
6
+ metadata.gz: cde98287cc82bc74923286973dc18cf83c04847943bc5bbc86652ac59b7391272b2b41a336cfbc47f2da9f3cc2dbf3c0f381a0dde76040a5a00d36d87848ca5d
7
+ data.tar.gz: 9d4179add815dcb6c5732c8c8501ba407298d97a1ac78c749e1499e85dede8a6b2d146c6e48e6db890df171f426f3a78e85469f2eed9aafe22f52b085245b015
@@ -15,6 +15,7 @@ AllCops:
15
15
  - 'config/**/*'
16
16
  - '**/Rakefile'
17
17
  - '**/Gemfile'
18
+ - 'pragma-operation.gemspec'
18
19
 
19
20
  RSpec/DescribeClass:
20
21
  Exclude:
@@ -24,26 +25,26 @@ Style/BlockDelimiters:
24
25
  Exclude:
25
26
  - 'spec/**/*'
26
27
 
27
- Style/AlignParameters:
28
+ Layout/AlignParameters:
28
29
  EnforcedStyle: with_fixed_indentation
29
30
 
30
- Style/ClosingParenthesisIndentation:
31
+ Layout/ClosingParenthesisIndentation:
31
32
  Enabled: false
32
33
 
33
34
  Metrics/LineLength:
34
35
  Max: 100
35
36
  AllowURI: true
36
37
 
37
- Style/FirstParameterIndentation:
38
+ Layout/FirstParameterIndentation:
38
39
  Enabled: false
39
40
 
40
- Style/MultilineMethodCallIndentation:
41
+ Layout/MultilineMethodCallIndentation:
41
42
  EnforcedStyle: indented
42
43
 
43
- Style/IndentArray:
44
+ Layout/IndentArray:
44
45
  EnforcedStyle: consistent
45
46
 
46
- Style/IndentHash:
47
+ Layout/IndentHash:
47
48
  EnforcedStyle: consistent
48
49
 
49
50
  Style/SignalException:
@@ -68,7 +69,7 @@ RSpec/NamedSubject:
68
69
  RSpec/ExampleLength:
69
70
  Enabled: false
70
71
 
71
- Style/MultilineMethodCallBraceLayout:
72
+ Layout/MultilineMethodCallBraceLayout:
72
73
  Enabled: false
73
74
 
74
75
  Metrics/MethodLength:
@@ -86,7 +87,7 @@ Metrics/CyclomaticComplexity:
86
87
  Metrics/ClassLength:
87
88
  Enabled: false
88
89
 
89
- Style/CaseIndentation:
90
+ Layout/CaseIndentation:
90
91
  EnforcedStyle: end
91
92
  IndentOneStep: false
92
93
 
data/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  Operations encapsulate the business logic of your JSON API.
9
9
 
10
- They are built on top of the awesome [Interactor](https://github.com/collectiveidea/interactor) gem.
10
+ They are built on top of the [Trailblazer::Operation](https://github.com/trailblazer/trailblazer-operation) gem.
11
11
 
12
12
  ## Installation
13
13
 
@@ -31,8 +31,169 @@ $ gem install pragma-operation
31
31
 
32
32
  ## Usage
33
33
 
34
- All documentation is in the [doc](https://github.com/pragmarb/pragma-operation/tree/master/doc)
35
- folder.
34
+ Let's build your first operation!
35
+
36
+ ```ruby
37
+ module API
38
+ module V1
39
+ module Article
40
+ class Show < Pragma::Operation::Base
41
+ step :find!
42
+ failure :handle_not_found!, fail_fast: true
43
+ step :authorize!
44
+ failure :handle_unauthorized!
45
+ step :respond!
46
+
47
+ def find!(params:, **options)
48
+ options['model'] = ::Article.find_by(id: params[:id])
49
+ end
50
+
51
+ def handle_not_found!(options)
52
+ options['result.response'] = Pragma::Operation::Response::NotFound.new
53
+ false
54
+ end
55
+
56
+ def authorize!(options)
57
+ options['result.authorization'] = options['model'].published? ||
58
+ options['model'].author == options['current_user']
59
+ end
60
+
61
+ def handle_unauthorized!(options)
62
+ options['result.response'] = Pragma::Operation::Response::Forbidden.new(
63
+ entity: Error.new(
64
+ error_type: :forbidden,
65
+ error_message: 'You can only access an article if published or authored by you.'
66
+ )
67
+ )
68
+ end
69
+
70
+ def respond!(options)
71
+ options['result.response'] = Pragma::Operation::Response::Ok.new(
72
+ entity: options['model'].as_json
73
+ )
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ ```
80
+
81
+ Yes, I know. This does not make any sense yet. Before continuing, I encourage you to read (and
82
+ understand!) the documentation of [Trailblazer::Operation](http://trailblazer.to/gems/operation/2.0/index.html).
83
+ Pragma::Operation is simply an extension of its TRB counterpart. For the rest of this guide, we will
84
+ assume you have a good understanding of TRB concepts like flow control and macros.
85
+
86
+ ### Response basics
87
+
88
+ The only requirement for a Pragma operation is that it sets a `result.response` key in the options
89
+ hash by the end of its execution. This is a `Pragma::Operation::Response` object that will be used
90
+ by [pragma-rails](https://github.com/pragmarb/pragma-rails) or another integration to respond with
91
+ the proper HTTP information.
92
+
93
+ Responses have, just as you'd expect, a status, headers and body. You can manipulate them by using
94
+ the `status`, `headers` and `entity` parameters of the initializer:
95
+
96
+ ```ruby
97
+ response = Pragma::Operation::Response.new(
98
+ status: 201,
99
+ headers: {
100
+ 'X-Api-Custom' => 'Value'
101
+ },
102
+ entity: my_model
103
+ )
104
+ ```
105
+
106
+ You can also set these properties through their accessors after instantiating the response:
107
+
108
+ ```ruby
109
+ # You can set the status as a symbol:
110
+ response.status = :created
111
+
112
+ # You can set it as an HTTP status code:
113
+ response.status = 201
114
+
115
+ # You can manipulate headers:
116
+ response.headers['X-Api-Custom'] = 'Value'
117
+
118
+ # You can manipulate the entity:
119
+ response.entity = my_model
120
+
121
+ # The entity can be any object responding to #to_json:
122
+ response.entity = {
123
+ foo: :bar
124
+ }
125
+ ```
126
+
127
+ ### Decorating entities
128
+
129
+ The response class also has support for Pragma [decorators](https://github.com/pragmarb/pragma-decorator).
130
+
131
+ If you use decorators, you can set a decorator as the entity or you can use the `#decorate_with`
132
+ convenience method to decorate the existing entity:
133
+
134
+ ```ruby
135
+ response.entity = ArticleDecorator.new(article)
136
+
137
+ # This is equivalent to the above:
138
+ response.entity = article
139
+ response.decorate_with(ArticleDecorator) # returns the response itself for chaining
140
+ ```
141
+
142
+ ### Errors
143
+
144
+ Pragma::Operation ships with an `Error` data structure that's simply the recommended way to present
145
+ your errors. You can build your custom error by creating a new instance of it and specify a
146
+ machine-readable error type and a human-readable error message:
147
+
148
+ ```ruby
149
+ error = Pragma::Operation::Error.new(
150
+ error_type: :invalid_date,
151
+ error_message: 'You have specified an invalid date in your request.'
152
+ )
153
+
154
+ error.as_json # => {:error_type=>:invalid_date, :error_message=>"You have specified an invalid date in your request.", :meta=>{}}
155
+ error.to_json # => {"error_type":"invalid_date","error_message":"You have specified an invalid date in your request.","meta":{}}
156
+ ```
157
+
158
+ Do you see that `meta` property in the JSON representation of the error? You can use it to include
159
+ additional metadata about the error. This is especially useful, for instance, with validation errors
160
+ as you can include the exact fields and validation messages (which is exactly what Pragma does by
161
+ default, by the way):
162
+
163
+ ```ruby
164
+ error = Pragma::Operation::Error.new(
165
+ error_type: :invalid_date,
166
+ error_message: 'You have specified an invalid date in your request.',
167
+ meta: {
168
+ expected_format: 'YYYY-MM-DD'
169
+ }
170
+ )
171
+
172
+ error.as_json # => {:error_type=>:invalid_date, :error_message=>"You have specified an invalid date in your request.", :meta=>{:expected_format=>"YYYY-MM-DD"}}
173
+ error.to_json # => {"error_type":"invalid_date","error_message":"You have specified an invalid date in your request.","meta":{"expected_format":"YYYY-MM-DD"}}
174
+ ```
175
+
176
+ If you don't want to go with this format, you are free to implement your own error class, but it is
177
+ not recommended, as the [built-in macros](https://github.com/pragmarb/pragma/tree/master/lib/pragma/operation/macro)
178
+ will use `Pragma::Operation::Error`.
179
+
180
+ ### Built-in responses
181
+
182
+ Last but not least, as you have seen in the example operation, Pragma provides some
183
+ [built-in responses](https://github.com/pragmarb/pragma-operation/tree/master/lib/pragma/operation/response)
184
+ for common status codes and bodies. Some of these only have a status code while others (the error
185
+ responses) also have a default entity attached to them. For instance, you can use `Pragma::Operation::Response::Forbidden`
186
+ without specifying your own error type and message:
187
+
188
+ ```ruby
189
+ response = Pragma::Operation::Response::Forbidden.new
190
+
191
+ response.status # => 403
192
+ response.entity.to_json # => {"error_type":"forbidden","error_message":"You are not authorized to access the requested resource.","meta":{}}
193
+ ```
194
+
195
+ The built-in responses are not meant to be comprehensive and you will most likely have to implement
196
+ your own. If you write some that you think could be useful, feel free to open a PR!
36
197
 
37
198
  ## Contributing
38
199
 
@@ -1,11 +1,21 @@
1
1
  # frozen_string_literal: true
2
- require 'interactor'
2
+
3
+ require 'json'
4
+
5
+ require 'trailblazer/operation'
3
6
 
4
7
  require 'pragma/operation/version'
5
- require 'pragma/operation/authorization'
6
- require 'pragma/operation/validation'
7
- require 'pragma/operation/decoration'
8
8
  require 'pragma/operation/base'
9
+ require 'pragma/operation/error'
10
+
11
+ require 'pragma/operation/response'
12
+ require 'pragma/operation/response/bad_request'
13
+ require 'pragma/operation/response/not_found'
14
+ require 'pragma/operation/response/forbidden'
15
+ require 'pragma/operation/response/unprocessable_entity'
16
+ require 'pragma/operation/response/created'
17
+ require 'pragma/operation/response/ok'
18
+ require 'pragma/operation/response/no_content'
9
19
 
10
20
  module Pragma
11
21
  # Operations provide business logic encapsulation for your JSON API.
@@ -1,86 +1,12 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Pragma
3
4
  module Operation
4
5
  # This is the base class all your operations should extend.
5
6
  #
6
7
  # @author Alessandro Desantis
7
- #
8
- # @abstract Subclass and override {#call} to implement an operation.
9
- class Base
10
- include Interactor
11
-
12
- include Authorization
13
- include Validation
14
- include Decoration
15
-
16
- STATUSES = {
17
- 200 => :ok,
18
- 201 => :created,
19
- 202 => :accepted,
20
- 203 => :non_authoritative_information,
21
- 204 => :no_content,
22
- 205 => :reset_content,
23
- 206 => :partial_content,
24
- 207 => :multi_status,
25
- 208 => :already_reported,
26
- 300 => :multiple_choices,
27
- 301 => :moved_permanently,
28
- 302 => :found,
29
- 303 => :see_other,
30
- 304 => :not_modified,
31
- 305 => :use_proxy,
32
- 307 => :temporary_redirect,
33
- 400 => :bad_request,
34
- 401 => :unauthorized,
35
- 402 => :payment_required,
36
- 403 => :forbidden,
37
- 404 => :not_found,
38
- 405 => :method_not_allowed,
39
- 406 => :not_acceptable,
40
- 407 => :proxy_authentication_required,
41
- 408 => :request_timeout,
42
- 409 => :conflict,
43
- 410 => :gone,
44
- 411 => :length_required,
45
- 412 => :precondition_failed,
46
- 413 => :request_entity_too_large,
47
- 414 => :request_uri_too_large,
48
- 415 => :unsupported_media_type,
49
- 416 => :request_range_not_satisfiable,
50
- 417 => :expectation_failed,
51
- 418 => :im_a_teapot,
52
- 422 => :unprocessable_entity,
53
- 423 => :locked,
54
- 424 => :failed_dependency,
55
- 425 => :unordered_collection,
56
- 426 => :upgrade_required,
57
- 428 => :precondition_required,
58
- 429 => :too_many_requests,
59
- 431 => :request_header_fields_too_large,
60
- 449 => :retry_with,
61
- 500 => :internal_server_error,
62
- 501 => :not_implemented,
63
- 502 => :bad_gateway,
64
- 503 => :service_unavailable,
65
- 504 => :gateway_timeout,
66
- 505 => :http_version_not_supported,
67
- 506 => :variant_also_negotiates,
68
- 507 => :insufficient_storage,
69
- 509 => :bandwidth_limit_exceeded,
70
- 510 => :not_extended,
71
- 511 => :network_authentication_required
72
- }.freeze
73
-
8
+ class Base < Trailblazer::Operation
74
9
  class << self
75
- def inherited(child)
76
- child.class_eval do
77
- before :setup_context
78
- around :handle_halt
79
- after :mark_result, :consolidate_status, :validate_status, :set_default_status
80
- after :build_links
81
- end
82
- end
83
-
84
10
  # Returns the name of this operation.
85
11
  #
86
12
  # For instance, if the operation is called +API::V1::Post::Operation::Create+, returns
@@ -96,177 +22,6 @@ module Pragma
96
22
  .to_sym
97
23
  end
98
24
  end
99
-
100
- # Runs the operation.
101
- def call
102
- fail NotImplementedError
103
- end
104
-
105
- protected
106
-
107
- # Returns the params this operation is being run with.
108
- #
109
- # This is just a shortcut for +context.params+.
110
- #
111
- # @return [Hash]
112
- def params
113
- context.params
114
- end
115
-
116
- # Sets the status and resource to respond with.
117
- #
118
- # You can achieve the same result by setting +context.status+, +context.headers+ and
119
- # +context.resource+ wherever you want in {#call}.
120
- #
121
- # Note that calling this method doesn't halt the execution of the operation and that this
122
- # method can be called multiple times, overriding the previous context.
123
- #
124
- # @param status [Integer|Symbol] an HTTP status code
125
- # @param resource [Object] an object responding to +#to_json+
126
- # @param headers [Hash] HTTP headers
127
- # @param links [Hash] links to use for building the +Link+ header
128
- def respond_with(status: nil, resource: nil, headers: nil, links: nil)
129
- context.status = status if status
130
- context.resource = resource if resource
131
- context.headers = headers if headers
132
- context.links = links if links
133
- end
134
-
135
- # Same as {#respond_with}, but also halts the execution of the operation.
136
- #
137
- # @see #respond_with
138
- def respond_with!(*args)
139
- respond_with(*args)
140
- fail Halt
141
- end
142
-
143
- # Sets the status to respond with.
144
- #
145
- # You can achieve the same result by setting +context.status+ wherever you want in {#call}.
146
- #
147
- # Note that calling this method doesn't halt the execution of the operation and that this
148
- # method can be called multiple times, overriding the previous context.
149
- #
150
- # @param status [Integer|Symbol] an HTTP status code
151
- def head(status)
152
- context.status = status
153
- end
154
-
155
- # Same as {#head}, but also halts the execution of the operation.
156
- #
157
- # @param status [Integer|Symbol] an HTTP status code
158
- #
159
- # @see #head
160
- def head!(status)
161
- head status
162
- fail Halt
163
- end
164
-
165
- # Returns the current user.
166
- #
167
- # This is just a shortcut for +context.current_user+.
168
- #
169
- # @return [Object]
170
- def current_user
171
- context.current_user
172
- end
173
-
174
- # Returns the headers we are responding with.
175
- #
176
- # This is just a shortcut for +context.headers+.
177
- #
178
- # @return [Hash]
179
- def headers
180
- context.headers
181
- end
182
-
183
- # Returns the links we are responding with.
184
- #
185
- # This is just a shotcut for +context.links+.
186
- #
187
- # @return [Hash]
188
- def links
189
- context.links
190
- end
191
-
192
- private
193
-
194
- def with_hooks
195
- # This overrides the default behavior, which is not to run after hooks if an exception is
196
- # raised either in +#call+ or one of the before hooks. See:
197
- # https://github.com/collectiveidea/interactor/blob/master/lib/interactor/hooks.rb#L210)
198
- run_around_hooks do
199
- begin
200
- run_before_hooks
201
- yield
202
- ensure
203
- run_after_hooks
204
- end
205
- end
206
- end
207
-
208
- def setup_context
209
- context.params ||= {}
210
- context.headers = {}
211
- context.links = {}
212
- end
213
-
214
- def handle_halt(interactor)
215
- interactor.call
216
- rescue Halt # rubocop:disable Lint/HandleExceptions
217
- end
218
-
219
- def set_default_status
220
- return if context.status
221
- context.status = context.resource ? :ok : :no_content
222
- end
223
-
224
- def validate_status
225
- if context.status.is_a?(Integer)
226
- fail InvalidStatusError, context.status unless STATUSES.key?(context.status)
227
- else
228
- fail InvalidStatusError, context.status unless STATUSES.invert.key?(context.status.to_sym)
229
- end
230
- end
231
-
232
- def consolidate_status
233
- context.status = if context.status.is_a?(Integer)
234
- STATUSES[context.status]
235
- else
236
- context.status.to_sym
237
- end
238
- end
239
-
240
- def mark_result
241
- return if /\A(2|3)\d{2}\z/ =~ STATUSES.invert[context.status].to_s
242
- context.fail! pragma_operation_failure: true
243
- end
244
-
245
- def build_links
246
- headers.delete('Link')
247
-
248
- link_header = context.links.each_pair.select do |_relation, url|
249
- url && !url.empty?
250
- end.map do |relation, url|
251
- %(<#{url}>; rel="#{relation}")
252
- end.join(",\n ")
253
-
254
- headers['Link'] = link_header unless link_header.empty?
255
- end
256
- end
257
-
258
- Halt = Class.new(StandardError)
259
-
260
- # This error is raised when an invalid status is set for an operation.
261
- #
262
- # @author Alessandro Desantis
263
- class InvalidStatusError < StandardError
264
- # Initializes the error.
265
- #
266
- # @param [Integer|Symbol] an invalid HTTP status code
267
- def initialize(status)
268
- super "'#{status}' is not a valid HTTP status code."
269
- end
270
25
  end
271
26
  end
272
27
  end