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