api-blocks 0.1.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 +7 -0
- data/lib/api_blocks.rb +18 -0
- data/lib/api_blocks/controller.rb +58 -0
- data/lib/api_blocks/interactor.rb +123 -0
- data/lib/api_blocks/responder.rb +62 -0
- data/lib/api_blocks/version.rb +6 -0
- metadata +271 -0
checksums.yaml
ADDED
@@ -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
|
data/lib/api_blocks.rb
ADDED
@@ -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
|
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: []
|