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
data/config/routes.rb
CHANGED
data/lib/carbonyte.rb
CHANGED
data/lib/carbonyte/engine.rb
CHANGED
@@ -1,8 +1,22 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'carbonyte/initializers'
|
4
|
+
require 'interactor/rails'
|
5
|
+
require 'jsonapi/serializer'
|
6
|
+
require 'pundit'
|
7
|
+
require 'rails/engine'
|
8
|
+
|
3
9
|
module Carbonyte
|
4
10
|
# Carbonyte Engine
|
5
11
|
class Engine < ::Rails::Engine
|
6
12
|
isolate_namespace Carbonyte
|
13
|
+
|
14
|
+
include Initializers::Lograge
|
15
|
+
|
16
|
+
initializer 'carbonyte.catch_404' do
|
17
|
+
config.after_initialize do |app|
|
18
|
+
app.routes.append { match '*path', to: 'carbonyte/application#route_not_found', via: :all }
|
19
|
+
end
|
20
|
+
end
|
7
21
|
end
|
8
22
|
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'lograge'
|
4
|
+
|
5
|
+
module Carbonyte
|
6
|
+
module Initializers
|
7
|
+
# This initializer setups lograge and automatically logs exceptions
|
8
|
+
module Lograge
|
9
|
+
extend ActiveSupport::Concern
|
10
|
+
|
11
|
+
# Items to be removed from `params` hash
|
12
|
+
PARAMS_EXCEPTIONS = %w[controller action].freeze
|
13
|
+
# Log type, this allows to distinguish these logs from others
|
14
|
+
LOG_TYPE = 'SERVER_REQUEST'
|
15
|
+
|
16
|
+
included do
|
17
|
+
initializer 'carbonyte.lograge' do
|
18
|
+
Rails.application.config.tap do |config|
|
19
|
+
config.lograge.enabled = true
|
20
|
+
config.lograge.base_controller_class = 'ActionController::API'
|
21
|
+
config.lograge.formatter = ::Lograge::Formatters::Logstash.new
|
22
|
+
|
23
|
+
config.lograge.custom_options = lambda do |event|
|
24
|
+
custom_options(event)
|
25
|
+
end
|
26
|
+
|
27
|
+
config.lograge.custom_payload do |controller|
|
28
|
+
custom_payload(controller)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Adds custom options to the Lograge event
|
35
|
+
def custom_options(event)
|
36
|
+
opts = {
|
37
|
+
type: LOG_TYPE,
|
38
|
+
params: event.payload[:params].except(*PARAMS_EXCEPTIONS),
|
39
|
+
correlation_id: RequestStore.store[:correlation_id],
|
40
|
+
environment: Rails.env,
|
41
|
+
pid: ::Process.pid
|
42
|
+
}
|
43
|
+
add_rescued_exception(opts, RequestStore.store[:rescued_exception])
|
44
|
+
opts
|
45
|
+
end
|
46
|
+
|
47
|
+
# Adds the rescued exception (if any) to the Lograge event
|
48
|
+
def add_rescued_exception(opts, exc)
|
49
|
+
return unless exc
|
50
|
+
|
51
|
+
opts[:rescued_exception] = {
|
52
|
+
name: exc.class.name,
|
53
|
+
message: exc.message,
|
54
|
+
backtrace: %('#{Array(exc.backtrace.first(10)).join("\n\t")}')
|
55
|
+
}
|
56
|
+
end
|
57
|
+
|
58
|
+
# Adds custom payload to the Lograge event
|
59
|
+
def custom_payload(controller)
|
60
|
+
payload = {
|
61
|
+
headers: parse_headers(controller.request.headers),
|
62
|
+
remote_ip: controller.remote_ip
|
63
|
+
}
|
64
|
+
payload[:user_id] = controller.current_user&.id if controller.respond_to?(:current_user)
|
65
|
+
payload
|
66
|
+
end
|
67
|
+
|
68
|
+
# Parses headers returning only those starting with "HTTP", but excluding cookies
|
69
|
+
def parse_headers(headers)
|
70
|
+
headers.to_h.select { |k, _v| k.start_with?('HTTP') and k != 'HTTP_COOKIE' }
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
data/lib/carbonyte/version.rb
CHANGED
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe '/health', type: :request do
|
4
|
+
it 'returns the health of the application' do
|
5
|
+
get '/health'
|
6
|
+
|
7
|
+
expect(parsed_response).to have_key('correlation_id')
|
8
|
+
expect(parsed_response).to have_key('db_status')
|
9
|
+
expect(parsed_response).to have_key('environment')
|
10
|
+
expect(parsed_response).to have_key('pid')
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe Carbonyte::Concerns::Rescuable, type: :controller do
|
4
|
+
class FakePolicy < Carbonyte::ApplicationPolicy
|
5
|
+
def index?
|
6
|
+
false
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class FakeInteractor < Carbonyte::ApplicationInteractor
|
11
|
+
def call
|
12
|
+
context.fail!
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
controller(ApplicationController) do
|
17
|
+
def index
|
18
|
+
raise 'Something unpredictable happened.'
|
19
|
+
end
|
20
|
+
|
21
|
+
def record_not_found_error
|
22
|
+
User.find(-1)
|
23
|
+
end
|
24
|
+
|
25
|
+
def policy_not_authorized_error
|
26
|
+
authorize(User.new, 'index?', policy_class: FakePolicy)
|
27
|
+
end
|
28
|
+
|
29
|
+
def interactor_error
|
30
|
+
FakeInteractor.call!(error: 'Something went wrong.')
|
31
|
+
end
|
32
|
+
|
33
|
+
def record_invalid_error
|
34
|
+
User.create!
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
let(:errors) { parsed_response['errors'] }
|
39
|
+
let(:error) { errors.first }
|
40
|
+
|
41
|
+
it 'rescues all exceptions inheriting from StandardError' do
|
42
|
+
get :index
|
43
|
+
expect(errors.size).to eq(1)
|
44
|
+
expect(error['code']).to eq('RuntimeError')
|
45
|
+
expect(error['detail']).to eq('Something unpredictable happened.')
|
46
|
+
expect(error['title']).to eq('Internal Server Error')
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'rescues ActiveRecord::RecordNotFound' do
|
50
|
+
routes.draw { get :record_not_found_error, to: 'anonymous#record_not_found_error' }
|
51
|
+
get :record_not_found_error
|
52
|
+
expect(errors.size).to eq(1)
|
53
|
+
expect(error['code']).to eq('ActiveRecord::RecordNotFound')
|
54
|
+
expect(error['detail']).to eq("Couldn't find User with 'id'=-1")
|
55
|
+
expect(error['source']['id']).to eq(-1)
|
56
|
+
expect(error['source']['model']).to eq('User')
|
57
|
+
expect(error['title']).to eq('Resource Not Found')
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'rescues Pundit::NotAuthorizedError' do
|
61
|
+
routes.draw { get :policy_not_authorized_error, to: 'anonymous#policy_not_authorized_error' }
|
62
|
+
get :policy_not_authorized_error
|
63
|
+
expect(errors.size).to eq(1)
|
64
|
+
expect(error['code']).to eq('Pundit::NotAuthorizedError')
|
65
|
+
expect(error).to have_key('detail')
|
66
|
+
expect(error['source']['policy']).to eq('FakePolicy')
|
67
|
+
expect(error['title']).to eq('Not Authorized By Policy')
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'rescues Interactor::Failure' do
|
71
|
+
routes.draw { get :interactor_error, to: 'anonymous#interactor_error' }
|
72
|
+
get :interactor_error
|
73
|
+
expect(errors.size).to eq(1)
|
74
|
+
expect(error['code']).to eq('Interactor::Failure')
|
75
|
+
expect(error['detail']).to eq('Something went wrong.')
|
76
|
+
expect(error['source']['interactor']).to eq('FakeInteractor')
|
77
|
+
expect(error['title']).to eq('Interactor Failure')
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'rescues ActiveRecord::RecordInvalid' do
|
81
|
+
routes.draw { get :record_invalid_error, to: 'anonymous#record_invalid_error' }
|
82
|
+
get :record_invalid_error
|
83
|
+
expect(errors.size).to eq(1)
|
84
|
+
expect(error['code']).to eq('ActiveRecord::RecordInvalid')
|
85
|
+
expect(error['detail']).to eq('can\'t be blank')
|
86
|
+
expect(error['source']['pointer']).to eq('attributes/email')
|
87
|
+
expect(error['title']).to eq('Invalid Field')
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe Carbonyte::Concerns::Serializable, type: :controller do
|
4
|
+
class FakeSerializer < Carbonyte::ApplicationSerializer
|
5
|
+
attributes :name, :age
|
6
|
+
end
|
7
|
+
|
8
|
+
class AlternativeFakeSerializer < Carbonyte::ApplicationSerializer
|
9
|
+
set_type :fake
|
10
|
+
attributes :name
|
11
|
+
end
|
12
|
+
|
13
|
+
class Fake
|
14
|
+
attr_accessor :id, :name, :age
|
15
|
+
end
|
16
|
+
|
17
|
+
controller(ApplicationController) do
|
18
|
+
def fake
|
19
|
+
Fake.new.tap do |f|
|
20
|
+
f.id = 1
|
21
|
+
f.name = 'test'
|
22
|
+
f.age = 20
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def inferred
|
27
|
+
serialize(fake)
|
28
|
+
end
|
29
|
+
|
30
|
+
def explicit
|
31
|
+
serialize(fake, serializer_class: AlternativeFakeSerializer, status: :created)
|
32
|
+
end
|
33
|
+
|
34
|
+
def list
|
35
|
+
serialize([fake, fake])
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'infers the serializer type automatically when not specified' do
|
40
|
+
expect(FakeSerializer).to receive(:new).and_call_original
|
41
|
+
routes.draw { get :inferred, to: 'anonymous#inferred' }
|
42
|
+
get :inferred
|
43
|
+
expect(response.status).to eq(200)
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'allows to customize the serializer type and status' do
|
47
|
+
expect(AlternativeFakeSerializer).to receive(:new).and_call_original
|
48
|
+
routes.draw { get :explicit, to: 'anonymous#explicit' }
|
49
|
+
get :explicit
|
50
|
+
expect(response.status).to eq(201)
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'automatically passes include options to serializer' do
|
54
|
+
object_matcher = instance_of(Fake)
|
55
|
+
options_matcher = hash_including(include: %i[field1 field2])
|
56
|
+
expect(FakeSerializer).to receive(:new)
|
57
|
+
.with(object_matcher, options_matcher)
|
58
|
+
.and_call_original
|
59
|
+
routes.draw { get :inferred, to: 'anonymous#inferred' }
|
60
|
+
get :inferred, params: { include: 'field1,field2' }
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'works with lists as well' do
|
64
|
+
expect(FakeSerializer).to receive(:new).and_call_original
|
65
|
+
routes.draw { get :list, to: 'anonymous#list' }
|
66
|
+
get :list
|
67
|
+
expect(response.status).to eq(200)
|
68
|
+
end
|
69
|
+
end
|
data/spec/dummy/Rakefile
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Add your own tasks in files placed in lib/tasks ending in .rake,
|
4
|
+
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
|
5
|
+
|
6
|
+
require_relative 'config/application'
|
7
|
+
|
8
|
+
Rails.application.load_tasks
|
data/spec/dummy/bin/rake
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'fileutils'
|
5
|
+
|
6
|
+
# path to your application root.
|
7
|
+
APP_ROOT = File.expand_path('..', __dir__)
|
8
|
+
|
9
|
+
def system!(*args)
|
10
|
+
system(*args) || abort("\n== Command #{args} failed ==")
|
11
|
+
end
|
12
|
+
|
13
|
+
FileUtils.chdir APP_ROOT do
|
14
|
+
# This script is a way to setup or update your development environment automatically.
|
15
|
+
# This script is idempotent, so that you can run it at anytime and get an expectable outcome.
|
16
|
+
# Add necessary setup steps to this file.
|
17
|
+
|
18
|
+
puts '== Installing dependencies =='
|
19
|
+
system! 'gem install bundler --conservative'
|
20
|
+
system('bundle check') || system!('bundle install')
|
21
|
+
|
22
|
+
# puts "\n== Copying sample files =="
|
23
|
+
# unless File.exist?('config/database.yml')
|
24
|
+
# FileUtils.cp 'config/database.yml.sample', 'config/database.yml'
|
25
|
+
# end
|
26
|
+
|
27
|
+
puts "\n== Preparing database =="
|
28
|
+
system! 'bin/rails db:prepare'
|
29
|
+
|
30
|
+
puts "\n== Removing old logs and tempfiles =="
|
31
|
+
system! 'bin/rails log:clear tmp:clear'
|
32
|
+
|
33
|
+
puts "\n== Restarting application server =="
|
34
|
+
system! 'bin/rails restart'
|
35
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'boot'
|
4
|
+
|
5
|
+
require 'rails'
|
6
|
+
# Pick the frameworks you want:
|
7
|
+
require 'active_model/railtie'
|
8
|
+
# require 'active_job/railtie'
|
9
|
+
require 'active_record/railtie'
|
10
|
+
require 'active_storage/engine'
|
11
|
+
require 'action_controller/railtie'
|
12
|
+
require 'action_mailer/railtie'
|
13
|
+
# require 'action_view/railtie'
|
14
|
+
# require 'action_cable/engine'
|
15
|
+
# require 'sprockets/railtie'
|
16
|
+
# require "rails/test_unit/railtie"
|
17
|
+
|
18
|
+
Bundler.require(*Rails.groups)
|
19
|
+
require 'carbonyte'
|
20
|
+
|
21
|
+
module Dummy
|
22
|
+
class Application < Rails::Application
|
23
|
+
# Initialize configuration defaults for originally generated Rails version.
|
24
|
+
config.load_defaults 6.0
|
25
|
+
|
26
|
+
# Settings in config/environments/* take precedence over those specified here.
|
27
|
+
# Application configuration can go into files in config/initializers
|
28
|
+
# -- all .rb files in that directory are automatically loaded after loading
|
29
|
+
# the framework and any gems in your application.
|
30
|
+
|
31
|
+
config.api_only = true
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Set up gems listed in the Gemfile.
|
4
|
+
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../Gemfile', __dir__)
|
5
|
+
|
6
|
+
require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
|
7
|
+
$LOAD_PATH.unshift File.expand_path('../../../lib', __dir__)
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# SQLite. Versions 3.8.0 and up are supported.
|
2
|
+
# gem install sqlite3
|
3
|
+
#
|
4
|
+
# Ensure the SQLite 3 gem is defined in your Gemfile
|
5
|
+
# gem 'sqlite3'
|
6
|
+
#
|
7
|
+
default: &default
|
8
|
+
adapter: sqlite3
|
9
|
+
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
|
10
|
+
timeout: 5000
|
11
|
+
|
12
|
+
development:
|
13
|
+
<<: *default
|
14
|
+
database: db/development.sqlite3
|
15
|
+
|
16
|
+
# Warning: The database defined as "test" will be erased and
|
17
|
+
# re-generated from your development database when you run "rake".
|
18
|
+
# Do not set this db to the same as development or production.
|
19
|
+
test:
|
20
|
+
<<: *default
|
21
|
+
database: db/test.sqlite3
|
22
|
+
|
23
|
+
production:
|
24
|
+
<<: *default
|
25
|
+
database: db/production.sqlite3
|