api-blocks 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6840a132c6e9d821734ded5b586cbc46d2399c071cd15bed72081c8babc99eb2
4
+ data.tar.gz: 7ca022286f2e3f0f989b8e77564450eb4ee01adcce2d7cb7603fa7f82d5ad115
5
+ SHA512:
6
+ metadata.gz: 0d0e202aa2766ac5ffdfeb66be737cbb7976a866a9aec7a0f1c891ef61a10c87063f2e991ef66d6b3a09a70e3cb525c6d6c97396bfe77192c9036962cbf6eaae
7
+ data.tar.gz: 1ba5337c3cd30b24dfe60aee1963f8a3f972729e2fdcba477984c62e8a5d5ddab67ea8a0879e88943f6cef7c9f9fc6fbb7005af8b386f0c7b5e37c183c15f22e
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubygems'
4
+ require 'bundler/setup'
5
+
6
+ require 'api_blocks/version'
7
+ require 'active_support/concern'
8
+ require 'active_support/dependencies/autoload'
9
+
10
+
11
+ # ApiBlocks provides simple and consistent rails api extensions.
12
+ module ApiBlocks
13
+ extend ActiveSupport::Autoload
14
+
15
+ autoload :Controller
16
+ autoload :Responder
17
+ autoload :Interactor
18
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_litreal: true
2
+
3
+ require "pundit"
4
+
5
+ # ApiBlocks::Controller provides a set of default configurations for
6
+ # Ruby on Rails api controllers.
7
+ #
8
+ # It sets up `ApiBlocks::Responder` as a responder, `Pundit` and controller defaults.
9
+ #
10
+ # @example
11
+ #
12
+ # class Api::V1::ApplicationController < ActionController::API
13
+ # include ApiBlocks::Controller
14
+ #
15
+ # pundit_scope :api, :v1
16
+ # end
17
+ #
18
+ module ApiBlocks::Controller
19
+ extend ActiveSupport::Concern
20
+
21
+ included do
22
+ self.responder = ApiBlocks::Responder
23
+
24
+ before_action :verify_request_format!
25
+
26
+ include Pundit
27
+ rescue_from Pundit::NotAuthorizedError, with: :render_forbidden_error
28
+
29
+ # Enable pundit after_action hooks to ensure policies are consistently
30
+ # used.
31
+ after_action :verify_authorized
32
+ after_action :verify_policy_scoped, except: :create
33
+
34
+ # Override policy_scope to lookup pundit policies under the `scope`
35
+ # namespace
36
+ def policy_scope(scope)
37
+ super(self.class.pundit_api_scope + [scope])
38
+ end
39
+
40
+ # Override authorize to lookup pundit policies under the `scope`
41
+ # namespace
42
+ def authorize(record, query = nil)
43
+ super(self.class.pundit_api_scope + [record], query)
44
+ end
45
+ end
46
+
47
+ class_methods do
48
+ # Provide a default scope to pundit's `PolicyFinder`.
49
+ def pundit_scope(*scope)
50
+ @pundit_api_scope = scope
51
+ end
52
+
53
+ # Returns the scope for pundit's `PolicyFinder`.
54
+ def pundit_api_scope
55
+ @pundit_api_scope || []
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/transaction"
4
+ require "dry/validation"
5
+
6
+ # ApiBlocks::Interactor implements a base interactor class.
7
+ #
8
+ # It is based on `Dry::Transaction` and implements input schema parsing and
9
+ # validation as well as database transaction handling.
10
+ #
11
+ # @example
12
+ #
13
+ # class InviteUser < ApplicationInteractor
14
+ # input do
15
+ # schema do
16
+ # required(:email).filled
17
+ # required(:password).filled
18
+ #
19
+ # optional(:first_name)
20
+ # optional(:last_name)
21
+ # end
22
+ # end
23
+ #
24
+ # around :database_transaction!
25
+ #
26
+ # step :validate_input!
27
+ # try :create_user, catch: ActiveRecord::RecordInvalid
28
+ # tee :deliver_invitation
29
+ #
30
+ # def create_user(params)
31
+ # Success(User.create!(params))
32
+ # end
33
+ #
34
+ # def deliver_invitation(user, mailer)
35
+ # mailer.accept_invitation(user)
36
+ # end
37
+ # end
38
+ #
39
+ class ApiBlocks::Interactor
40
+ include Dry::Transaction
41
+
42
+ class << self
43
+ attr_accessor :input_schema
44
+ end
45
+
46
+ # Define a contract for the input of this interactor using
47
+ # `dry-validation`
48
+ #
49
+ # @example
50
+ #
51
+ # class FooInteractor < ApplicationInteractor
52
+ # input do
53
+ # schema do
54
+ # required(:bar).filled
55
+ # end
56
+ # end
57
+ #
58
+ # step :validate_input!
59
+ # end
60
+ #
61
+ def self.input(&block)
62
+ @input_schema = Class.new(Dry::Validation::Contract, &block).new
63
+ end
64
+
65
+ # Call the interactor with its arguments.
66
+ #
67
+ # @example
68
+ #
69
+ # InviteUser.call(
70
+ # email: "foo@example.com",
71
+ # first_name: "Foo",
72
+ # last_name: "Bar"
73
+ # )
74
+ #
75
+ def self.call(*args)
76
+ new.call(*args)
77
+ end
78
+
79
+ # Call the interactor with additional step arguments.
80
+ #
81
+ # @example
82
+ #
83
+ # InviteUser.with_step_args(deliver_invitation: [mailer: UserMailer])
84
+ #
85
+ def self.with_step_args(*args)
86
+ new.with_step_args(*args)
87
+ end
88
+
89
+ protected
90
+
91
+ # Validates input with the class attribute `schema` if it is
92
+ # defined.
93
+ #
94
+ # Add this step to your interactor if you want to validate its input.
95
+ #
96
+ def validate_input!(input)
97
+ return Success(input) unless self.class.input_schema
98
+
99
+ result = self.class.input_schema.call(input)
100
+
101
+ if result.success?
102
+ Success(result.values)
103
+ else
104
+ Failure(result)
105
+ end
106
+ end
107
+
108
+ # Wraps the steps inside an AR transaction.
109
+ #
110
+ # Add this step to your interactor if you want to wrap its operations inside a
111
+ # database transaction
112
+ #
113
+ def database_transaction!(input)
114
+ result = nil
115
+
116
+ ActiveRecord::Base.transaction do
117
+ result = yield(Success(input))
118
+ raise ActiveRecord::Rollback if result.failure?
119
+ end
120
+
121
+ result
122
+ end
123
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_controller/responder"
4
+ require "responders"
5
+ require "dry/monads"
6
+
7
+ # ApiBlocks::Responder provides a responder with better error handling and
8
+ # `ApiBlocks::Interactor` through `Dry::Monads::Result` support.
9
+ #
10
+ class ApiBlocks::Responder < ActionController::Responder
11
+ include Responders::HttpCacheResponder
12
+
13
+ # Override resource_errors to handle more error kinds
14
+ def resource_errors
15
+ case @resource
16
+ when ApplicationRecord
17
+ { errors: @resource.errors }
18
+ when ActiveRecord::RecordInvalid
19
+ { errors: @resource.record.errors }
20
+ else
21
+ # propagate the error so it can be handled through the standard rails
22
+ # error handlers.
23
+ raise @resource
24
+ end
25
+ end
26
+
27
+ def to_format
28
+ return super unless resource.is_a?(Dry::Monads::Result)
29
+
30
+ # unwrap the result monad so it can be processed by ActionController::Responder
31
+ resource.fmap { |result| @resource = result }.or do |failure|
32
+ @resource = failure
33
+ @failure = true
34
+ end
35
+
36
+ super
37
+ end
38
+
39
+ def has_errors? # rubocop:disable Naming/PredicateName
40
+ return true if @failure
41
+
42
+ super
43
+ end
44
+
45
+ # Override ActionController::Responder#api_behavior in order to
46
+ # provide one that matches our API documentation.
47
+ #
48
+ # The only difference so far is that on POST we do not render `status:
49
+ # :created` along with a `Location` header.
50
+ #
51
+ # Moreover, we display the resource on PUT.
52
+ #
53
+ def api_behavior
54
+ raise(MissingRenderer, format) unless has_renderer?
55
+
56
+ if get? || post? || put?
57
+ display resource
58
+ else
59
+ head :no_content
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ module ApiBlocks
4
+ # Current version of ApiBlocks
5
+ VERSION = '0.1.0'.freeze
6
+ end
metadata ADDED
@@ -0,0 +1,271 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: api-blocks
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Paul d'Hubert
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-10-29 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 3.0.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 3.0.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: rails
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 6.0.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 6.0.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: pundit
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '2.1'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '2.1'
55
+ - !ruby/object:Gem::Dependency
56
+ name: dry-monads
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.3'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.3'
69
+ - !ruby/object:Gem::Dependency
70
+ name: dry-transaction
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.13'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.13'
83
+ - !ruby/object:Gem::Dependency
84
+ name: dry-validation
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.3'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.3'
97
+ - !ruby/object:Gem::Dependency
98
+ name: responders
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: 3.0.0
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: 3.0.0
111
+ - !ruby/object:Gem::Dependency
112
+ name: rails
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '6.0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '6.0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: bundler
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: pry
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: rake
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: rspec
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: 3.0.0
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: 3.0.0
181
+ - !ruby/object:Gem::Dependency
182
+ name: rubocop
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - '='
186
+ - !ruby/object:Gem::Version
187
+ version: 0.76.0
188
+ type: :development
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - '='
193
+ - !ruby/object:Gem::Version
194
+ version: 0.76.0
195
+ - !ruby/object:Gem::Dependency
196
+ name: yard
197
+ requirement: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - ">="
200
+ - !ruby/object:Gem::Version
201
+ version: '0'
202
+ type: :development
203
+ prerelease: false
204
+ version_requirements: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - ">="
207
+ - !ruby/object:Gem::Version
208
+ version: '0'
209
+ - !ruby/object:Gem::Dependency
210
+ name: yard-activesupport-concern
211
+ requirement: !ruby/object:Gem::Requirement
212
+ requirements:
213
+ - - ">="
214
+ - !ruby/object:Gem::Version
215
+ version: '0'
216
+ type: :development
217
+ prerelease: false
218
+ version_requirements: !ruby/object:Gem::Requirement
219
+ requirements:
220
+ - - ">="
221
+ - !ruby/object:Gem::Version
222
+ version: '0'
223
+ - !ruby/object:Gem::Dependency
224
+ name: mocha
225
+ requirement: !ruby/object:Gem::Requirement
226
+ requirements:
227
+ - - ">="
228
+ - !ruby/object:Gem::Version
229
+ version: '0'
230
+ type: :development
231
+ prerelease: false
232
+ version_requirements: !ruby/object:Gem::Requirement
233
+ requirements:
234
+ - - ">="
235
+ - !ruby/object:Gem::Version
236
+ version: '0'
237
+ description: Simple and consistent rails api extensions
238
+ email: dev@tymate.com
239
+ executables: []
240
+ extensions: []
241
+ extra_rdoc_files: []
242
+ files:
243
+ - lib/api_blocks.rb
244
+ - lib/api_blocks/controller.rb
245
+ - lib/api_blocks/interactor.rb
246
+ - lib/api_blocks/responder.rb
247
+ - lib/api_blocks/version.rb
248
+ homepage: https://github.com/tymate/api-blocks
249
+ licenses:
250
+ - MIT
251
+ metadata: {}
252
+ post_install_message:
253
+ rdoc_options: []
254
+ require_paths:
255
+ - lib
256
+ required_ruby_version: !ruby/object:Gem::Requirement
257
+ requirements:
258
+ - - ">="
259
+ - !ruby/object:Gem::Version
260
+ version: '0'
261
+ required_rubygems_version: !ruby/object:Gem::Requirement
262
+ requirements:
263
+ - - ">="
264
+ - !ruby/object:Gem::Version
265
+ version: '0'
266
+ requirements: []
267
+ rubygems_version: 3.0.3
268
+ signing_key:
269
+ specification_version: 4
270
+ summary: Simple and consistent rails api extensions
271
+ test_files: []