carbonyte 0.1.0 → 0.2.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 +4 -4
- data/README.md +0 -3
- data/Rakefile +6 -0
- data/app/controllers/carbonyte/application_controller.rb +17 -0
- data/app/controllers/carbonyte/concerns/correlatable.rb +48 -0
- data/app/controllers/carbonyte/concerns/loggable.rb +16 -0
- data/app/controllers/carbonyte/concerns/policiable.rb +27 -0
- data/app/controllers/carbonyte/concerns/rescuable.rb +93 -47
- data/app/controllers/carbonyte/concerns/serializable.rb +48 -0
- data/app/interactors/carbonyte/application_interactor.rb +14 -0
- data/app/interactors/carbonyte/finders/application_finder.rb +4 -6
- data/app/policies/carbonyte/application_policy.rb +55 -0
- data/config/routes.rb +1 -0
- data/lib/carbonyte.rb +1 -0
- data/lib/carbonyte/engine.rb +14 -0
- data/lib/carbonyte/initializers.rb +9 -0
- data/lib/carbonyte/initializers/lograge.rb +74 -0
- data/lib/carbonyte/version.rb +1 -1
- data/spec/api/health_spec.rb +12 -0
- data/spec/controllers/concerns/rescuable_spec.rb +89 -0
- data/spec/controllers/concerns/serializable_spec.rb +69 -0
- data/spec/dummy/Rakefile +8 -0
- data/spec/dummy/app/controllers/application_controller.rb +7 -0
- data/spec/dummy/app/models/application_record.rb +5 -0
- data/spec/dummy/app/models/user.rb +5 -0
- data/spec/dummy/bin/rails +6 -0
- data/spec/dummy/bin/rake +6 -0
- data/spec/dummy/bin/setup +35 -0
- data/spec/dummy/config.ru +7 -0
- data/spec/dummy/config/application.rb +33 -0
- data/spec/dummy/config/boot.rb +7 -0
- data/spec/dummy/config/database.yml +25 -0
- data/spec/dummy/config/environment.rb +7 -0
- data/spec/dummy/config/environments/development.rb +64 -0
- data/spec/dummy/config/environments/production.rb +114 -0
- data/spec/dummy/config/environments/test.rb +51 -0
- data/spec/dummy/config/initializers/content_security_policy.rb +29 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +6 -0
- data/spec/dummy/config/initializers/inflections.rb +17 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +16 -0
- data/spec/dummy/config/locales/en.yml +33 -0
- data/spec/dummy/config/puma.rb +40 -0
- data/spec/dummy/config/routes.rb +5 -0
- data/spec/dummy/config/spring.rb +8 -0
- data/spec/dummy/config/storage.yml +34 -0
- data/spec/dummy/db/migrate/20201007132857_create_users.rb +11 -0
- data/spec/dummy/db/schema.rb +21 -0
- data/spec/interactors/application_interactor_spec.rb +23 -0
- data/spec/interactors/finders/application_finder_spec.rb +60 -0
- data/spec/policies/application_policy_spec.rb +20 -0
- data/spec/rails_helper.rb +65 -0
- data/spec/spec_helper.rb +101 -0
- data/spec/support/carbonyte/api_helpers.rb +13 -0
- data/spec/support/carbonyte/spec_helper.rb +7 -0
- metadata +81 -23
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e0182bc2a05957e2aa33ff9d29b1bef6e80e0bf46e043cab50ea83088a8b9979
|
4
|
+
data.tar.gz: 9975a93ce713a8a3dad2a0493905a75f257f9f2d36403ece1e145e0ed03896c3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4546a5fb7554b04c2cc1f38657d315c157611ee4cd16af526443c3d24d8216976392950016ce144ab7dc5eefb9949ea0070c4031c5dadea3a8dcd1fcfa10c61b
|
7
|
+
data.tar.gz: f9808c42d3dcea7d9d293ce3481dfd79b5215218587cedba4b62e114843cbc268bc793cc2b0a2f28e2d5a96e40e90ed2c8908601a277911c6e4d27cd433b08c6
|
data/README.md
CHANGED
data/Rakefile
CHANGED
@@ -3,5 +3,22 @@
|
|
3
3
|
module Carbonyte
|
4
4
|
# Carbonyte base class for all controllers
|
5
5
|
class ApplicationController < ActionController::API
|
6
|
+
include Concerns::Correlatable
|
7
|
+
include Concerns::Loggable
|
8
|
+
include Concerns::Rescuable
|
9
|
+
include Concerns::Policiable
|
10
|
+
include Concerns::Serializable
|
11
|
+
|
12
|
+
# GET /health
|
13
|
+
def health
|
14
|
+
payload = {
|
15
|
+
correlation_id: correlation_id,
|
16
|
+
db_status: ActiveRecord::Base.connected? ? 'OK' : 'Not Connected',
|
17
|
+
environment: Rails.env,
|
18
|
+
pid: ::Process.pid
|
19
|
+
}
|
20
|
+
|
21
|
+
render json: payload, status: :ok
|
22
|
+
end
|
6
23
|
end
|
7
24
|
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'request_store'
|
4
|
+
|
5
|
+
module Carbonyte
|
6
|
+
# Module enclosing all concerns
|
7
|
+
module Concerns
|
8
|
+
# The Correlatable concern automatically manages the correlation ID
|
9
|
+
module Correlatable
|
10
|
+
extend ActiveSupport::Concern
|
11
|
+
|
12
|
+
included do
|
13
|
+
before_action :find_or_create_correlation_id
|
14
|
+
after_action :send_correlation_id
|
15
|
+
end
|
16
|
+
|
17
|
+
# Retrieves the request correlation ID
|
18
|
+
def correlation_id
|
19
|
+
RequestStore.store[:correlation_id]
|
20
|
+
end
|
21
|
+
|
22
|
+
protected
|
23
|
+
|
24
|
+
# Finds the correlation ID in the request.
|
25
|
+
# Can be overridden to fit the application
|
26
|
+
def find_correlation_id
|
27
|
+
request.headers['X-Correlation-Id'] || request.headers['X-Request-Id']
|
28
|
+
end
|
29
|
+
|
30
|
+
# Generates a correlation ID (defaults to Rails auto-generated one).
|
31
|
+
# Can be overridden to fit the application
|
32
|
+
def create_correlation_id
|
33
|
+
request.request_id || SecureRandom.uuid
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def find_or_create_correlation_id
|
39
|
+
correlation_id = find_correlation_id || create_correlation_id
|
40
|
+
RequestStore.store[:correlation_id] = correlation_id
|
41
|
+
end
|
42
|
+
|
43
|
+
def send_correlation_id
|
44
|
+
response.set_header('X-Correlation-Id', correlation_id)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Carbonyte
|
4
|
+
module Concerns
|
5
|
+
# The Loggable concern integrates with lograge to log efficiently
|
6
|
+
module Loggable
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
# Used for logging IP.
|
10
|
+
# @see Carbonyte::Engine
|
11
|
+
def remote_ip
|
12
|
+
request.headers['HTTP_X_FORWARDED_FOR'] || request.headers['HTTP_X_REAL_IP'] || request.remote_ip
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Carbonyte
|
4
|
+
module Concerns
|
5
|
+
# The Policiable concern integrates with Pundit to manage policies
|
6
|
+
module Policiable
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
included do
|
10
|
+
include Pundit
|
11
|
+
rescue_from Pundit::NotAuthorizedError, with: :policy_not_authorized
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def policy_not_authorized(exc)
|
17
|
+
payload = {
|
18
|
+
code: exc.class.name,
|
19
|
+
source: { policy: exc.policy.class.name },
|
20
|
+
title: 'Not Authorized By Policy',
|
21
|
+
detail: exc.message
|
22
|
+
}
|
23
|
+
render json: serialized_errors(payload), status: :forbidden
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -1,61 +1,107 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Carbonyte
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
included do
|
11
|
-
rescue_from Pundit::NotAuthorizedError, with: :unauthorized
|
12
|
-
rescue_from Interactor::Failure, with: :interactor_failure
|
13
|
-
rescue_from ActiveRecord::RecordInvalid, with: :record_invalid
|
14
|
-
end
|
4
|
+
module Concerns
|
5
|
+
# The Rescuable concern rescues controllers from bubbling up the most common exceptions
|
6
|
+
# and provides a uniformed response body in case of such errors.
|
7
|
+
# @see https://jsonapi.org/format/#error-objects
|
8
|
+
module Rescuable
|
9
|
+
extend ActiveSupport::Concern
|
15
10
|
|
16
|
-
|
11
|
+
included do
|
12
|
+
# The first one MUST be StandardError as otherwise will catch all others
|
13
|
+
rescue_from StandardError, with: :internal_error
|
17
14
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
15
|
+
rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
|
16
|
+
rescue_from Interactor::Failure, with: :interactor_failure
|
17
|
+
rescue_from ActiveRecord::RecordInvalid,
|
18
|
+
ActiveRecord::RecordNotSaved,
|
19
|
+
with: :record_invalid
|
20
|
+
end
|
22
21
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
}
|
30
|
-
render json: serialized_errors(payload), status: :unauthorized
|
31
|
-
end
|
22
|
+
# Upon rescuing an exception, stores that exception in RequestStore for later logging
|
23
|
+
# @param exception [StandardError] the rescued exception
|
24
|
+
def rescue_with_handler(exception)
|
25
|
+
RequestStore.store[:rescued_exception] = exception
|
26
|
+
super
|
27
|
+
end
|
32
28
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
29
|
+
# This is a special case
|
30
|
+
def route_not_found
|
31
|
+
payload = {
|
32
|
+
code: 'RoutingError',
|
33
|
+
source: request.path,
|
34
|
+
title: 'Route not found',
|
35
|
+
detail: "No route matches #{request.path}"
|
36
|
+
}
|
37
|
+
render json: serialized_errors(payload), status: :not_found
|
38
|
+
end
|
42
39
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
40
|
+
private
|
41
|
+
|
42
|
+
def serialized_errors(payload)
|
43
|
+
payload = [payload] unless payload.is_a?(Array)
|
44
|
+
{ errors: payload }.to_json
|
45
|
+
end
|
47
46
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
47
|
+
def unauthenticated(exc)
|
48
|
+
payload = {
|
49
|
+
code: exc.class.name,
|
50
|
+
title: 'User Not Authenticated',
|
51
|
+
detail: exc.message
|
52
|
+
}
|
53
|
+
render json: serialized_errors(payload), status: :unauthorized
|
54
|
+
end
|
55
|
+
|
56
|
+
def record_not_found(exc)
|
57
|
+
payload = {
|
58
|
+
code: exc.class.name,
|
59
|
+
source: {
|
60
|
+
model: exc.model,
|
61
|
+
id: exc.id
|
62
|
+
},
|
63
|
+
title: 'Resource Not Found',
|
64
|
+
detail: exc.message
|
65
|
+
}
|
66
|
+
render json: serialized_errors(payload), status: :not_found
|
67
|
+
end
|
68
|
+
|
69
|
+
def interactor_failure(exc)
|
70
|
+
payload = {
|
71
|
+
code: exc.class.name,
|
72
|
+
source: { interactor: exc.context.current_interactor.class.name },
|
73
|
+
title: 'Interactor Failure',
|
74
|
+
detail: exc.context.error
|
75
|
+
}
|
76
|
+
render json: serialized_errors(payload), status: :unprocessable_entity
|
77
|
+
end
|
78
|
+
|
79
|
+
def record_invalid(exc)
|
80
|
+
payload = errors_for_record(exc.record, exc.class.name).flatten
|
81
|
+
render json: serialized_errors(payload), status: :unprocessable_entity
|
82
|
+
end
|
83
|
+
|
84
|
+
def errors_for_record(record, code)
|
85
|
+
record.errors.messages.map do |field, errors|
|
86
|
+
errors.map do |error_message|
|
87
|
+
{
|
88
|
+
code: code,
|
89
|
+
source: { pointer: "attributes/#{field}" },
|
90
|
+
title: 'Invalid Field',
|
91
|
+
detail: error_message
|
92
|
+
}
|
93
|
+
end
|
57
94
|
end
|
58
95
|
end
|
96
|
+
|
97
|
+
def internal_error(exc)
|
98
|
+
payload = {
|
99
|
+
code: exc.class.name,
|
100
|
+
title: 'Internal Server Error',
|
101
|
+
detail: exc.message
|
102
|
+
}
|
103
|
+
render json: serialized_errors(payload), status: :internal_server_error
|
104
|
+
end
|
59
105
|
end
|
60
106
|
end
|
61
107
|
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Carbonyte
|
4
|
+
module Concerns
|
5
|
+
# The Serializable concern helps with JSON:API serialization
|
6
|
+
module Serializable
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
# Helper method to render json responses
|
10
|
+
# @param object [Any] the object (or list) to be serialized
|
11
|
+
# @param serializer_class [JSONAPI::Serializer] serializer class to use. Inferred if not provided.
|
12
|
+
# @param status [Symbol, Number] the status code to return, defaults to 200 (OK)
|
13
|
+
# @example Serializes `user` using UserSerializer and returns status 200 (OK)
|
14
|
+
# user = User.find(123)
|
15
|
+
# serialize(user)
|
16
|
+
# @example Serializes `created_record` using the specified UserSerializer and returning status 201 (CREATED)
|
17
|
+
# serialize(created_record, serializer: UserSerializer, status: :created)
|
18
|
+
def serialize(object, serializer_class: nil, status: :ok)
|
19
|
+
serializer_class ||= serializer_for(object)
|
20
|
+
render json: serializer_class.new(object, serializer_options).serializable_hash.to_json, status: status
|
21
|
+
end
|
22
|
+
|
23
|
+
# Default options for serializers
|
24
|
+
def serializer_options
|
25
|
+
{
|
26
|
+
include: include_option,
|
27
|
+
params: {
|
28
|
+
current_user: current_user
|
29
|
+
}
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
# Default include options for serializers
|
34
|
+
def include_option
|
35
|
+
return [] unless params[:include]
|
36
|
+
|
37
|
+
params[:include].split(',').map(&:to_sym)
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def serializer_for(object)
|
43
|
+
object = object.first if object.is_a?(Array)
|
44
|
+
"#{object.class.name}Serializer".constantize
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -4,5 +4,19 @@ module Carbonyte
|
|
4
4
|
# Carbonyte base class for all interactors
|
5
5
|
class ApplicationInteractor
|
6
6
|
include Interactor
|
7
|
+
|
8
|
+
# Interactor hooks are not inherited by subclasses, so we need a hack.
|
9
|
+
# @see https://github.com/collectiveidea/interactor/issues/114
|
10
|
+
def self.inherited(klass)
|
11
|
+
klass.class_eval do
|
12
|
+
before do
|
13
|
+
context.current_interactor = self
|
14
|
+
end
|
15
|
+
|
16
|
+
after do
|
17
|
+
context.current_interactor = nil
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
7
21
|
end
|
8
22
|
end
|
@@ -25,8 +25,6 @@ module Carbonyte
|
|
25
25
|
paginate
|
26
26
|
end
|
27
27
|
|
28
|
-
protected
|
29
|
-
|
30
28
|
# Returns true if the provided relation can be included
|
31
29
|
# @param _rel [Symbol] the relation to include
|
32
30
|
def can_include?(_rel)
|
@@ -37,9 +35,9 @@ module Carbonyte
|
|
37
35
|
|
38
36
|
def include_relations
|
39
37
|
context.params[:include].split(',').each do |relation|
|
40
|
-
next unless can_include?(relation)
|
38
|
+
next unless can_include?(relation.to_sym)
|
41
39
|
|
42
|
-
context.scope = context.scope.includes(relation)
|
40
|
+
context.scope = context.scope.includes(relation.to_sym)
|
43
41
|
end
|
44
42
|
end
|
45
43
|
|
@@ -48,9 +46,9 @@ module Carbonyte
|
|
48
46
|
end
|
49
47
|
|
50
48
|
def sort_exp(prop)
|
51
|
-
return prop unless prop.start_with?('-')
|
49
|
+
return prop.to_sym unless prop.start_with?('-')
|
52
50
|
|
53
|
-
{ prop[1..] => :desc }
|
51
|
+
{ prop[1..].to_sym => :desc }
|
54
52
|
end
|
55
53
|
|
56
54
|
def sort
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Carbonyte
|
4
|
+
# Carbonyte base class for all policies
|
5
|
+
class ApplicationPolicy
|
6
|
+
attr_reader :user, :record
|
7
|
+
|
8
|
+
# Initializes a new policy with the current user and the record
|
9
|
+
def initialize(user, record)
|
10
|
+
@user = user
|
11
|
+
@record = record
|
12
|
+
end
|
13
|
+
|
14
|
+
# Can the user get a list of records?
|
15
|
+
def index?
|
16
|
+
true
|
17
|
+
end
|
18
|
+
|
19
|
+
# Can the user get this specific record?
|
20
|
+
def show?
|
21
|
+
true
|
22
|
+
end
|
23
|
+
|
24
|
+
# Can the user create a new record?
|
25
|
+
def create?
|
26
|
+
true
|
27
|
+
end
|
28
|
+
|
29
|
+
# Can the user update this record?
|
30
|
+
def update?
|
31
|
+
true
|
32
|
+
end
|
33
|
+
|
34
|
+
# Can the user destroy this record?
|
35
|
+
def destroy?
|
36
|
+
true
|
37
|
+
end
|
38
|
+
|
39
|
+
# Carbonyte base class for all policy scopes
|
40
|
+
class Scope
|
41
|
+
attr_reader :user, :scope
|
42
|
+
|
43
|
+
# Initializes a new scope from the user and a base scope
|
44
|
+
def initialize(user, scope)
|
45
|
+
@user = user
|
46
|
+
@scope = scope
|
47
|
+
end
|
48
|
+
|
49
|
+
# Resolves the scope effectively triggering the query
|
50
|
+
def resolve
|
51
|
+
scope.all
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|