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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +0 -3
  3. data/Rakefile +6 -0
  4. data/app/controllers/carbonyte/application_controller.rb +17 -0
  5. data/app/controllers/carbonyte/concerns/correlatable.rb +48 -0
  6. data/app/controllers/carbonyte/concerns/loggable.rb +16 -0
  7. data/app/controllers/carbonyte/concerns/policiable.rb +27 -0
  8. data/app/controllers/carbonyte/concerns/rescuable.rb +93 -47
  9. data/app/controllers/carbonyte/concerns/serializable.rb +48 -0
  10. data/app/interactors/carbonyte/application_interactor.rb +14 -0
  11. data/app/interactors/carbonyte/finders/application_finder.rb +4 -6
  12. data/app/policies/carbonyte/application_policy.rb +55 -0
  13. data/config/routes.rb +1 -0
  14. data/lib/carbonyte.rb +1 -0
  15. data/lib/carbonyte/engine.rb +14 -0
  16. data/lib/carbonyte/initializers.rb +9 -0
  17. data/lib/carbonyte/initializers/lograge.rb +74 -0
  18. data/lib/carbonyte/version.rb +1 -1
  19. data/spec/api/health_spec.rb +12 -0
  20. data/spec/controllers/concerns/rescuable_spec.rb +89 -0
  21. data/spec/controllers/concerns/serializable_spec.rb +69 -0
  22. data/spec/dummy/Rakefile +8 -0
  23. data/spec/dummy/app/controllers/application_controller.rb +7 -0
  24. data/spec/dummy/app/models/application_record.rb +5 -0
  25. data/spec/dummy/app/models/user.rb +5 -0
  26. data/spec/dummy/bin/rails +6 -0
  27. data/spec/dummy/bin/rake +6 -0
  28. data/spec/dummy/bin/setup +35 -0
  29. data/spec/dummy/config.ru +7 -0
  30. data/spec/dummy/config/application.rb +33 -0
  31. data/spec/dummy/config/boot.rb +7 -0
  32. data/spec/dummy/config/database.yml +25 -0
  33. data/spec/dummy/config/environment.rb +7 -0
  34. data/spec/dummy/config/environments/development.rb +64 -0
  35. data/spec/dummy/config/environments/production.rb +114 -0
  36. data/spec/dummy/config/environments/test.rb +51 -0
  37. data/spec/dummy/config/initializers/content_security_policy.rb +29 -0
  38. data/spec/dummy/config/initializers/filter_parameter_logging.rb +6 -0
  39. data/spec/dummy/config/initializers/inflections.rb +17 -0
  40. data/spec/dummy/config/initializers/wrap_parameters.rb +16 -0
  41. data/spec/dummy/config/locales/en.yml +33 -0
  42. data/spec/dummy/config/puma.rb +40 -0
  43. data/spec/dummy/config/routes.rb +5 -0
  44. data/spec/dummy/config/spring.rb +8 -0
  45. data/spec/dummy/config/storage.yml +34 -0
  46. data/spec/dummy/db/migrate/20201007132857_create_users.rb +11 -0
  47. data/spec/dummy/db/schema.rb +21 -0
  48. data/spec/interactors/application_interactor_spec.rb +23 -0
  49. data/spec/interactors/finders/application_finder_spec.rb +60 -0
  50. data/spec/policies/application_policy_spec.rb +20 -0
  51. data/spec/rails_helper.rb +65 -0
  52. data/spec/spec_helper.rb +101 -0
  53. data/spec/support/carbonyte/api_helpers.rb +13 -0
  54. data/spec/support/carbonyte/spec_helper.rb +7 -0
  55. metadata +81 -23
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  Carbonyte::Engine.routes.draw do
4
+ get :health, controller: :application, via: :all
4
5
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'carbonyte/engine'
4
+ require 'carbonyte/version'
4
5
 
5
6
  # Main Carbonyte module
6
7
  module Carbonyte
@@ -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,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'carbonyte/initializers/lograge'
4
+
5
+ module Carbonyte
6
+ # Module enclosing all initializers
7
+ module Initializers
8
+ end
9
+ 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
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Carbonyte
4
4
  # Carbonyte version
5
- VERSION = '0.1.0'
5
+ VERSION = '0.2.0'
6
6
  end
@@ -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
@@ -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
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ApplicationController < Carbonyte::ApplicationController
4
+ def current_user
5
+ User.first
6
+ end
7
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ApplicationRecord < Carbonyte::ApplicationRecord
4
+ self.abstract_class = true
5
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class User < ApplicationRecord
4
+ validates :email, presence: true
5
+ end
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ APP_PATH = File.expand_path('../config/application', __dir__)
5
+ require_relative '../config/boot'
6
+ require 'rails/commands'
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../config/boot'
5
+ require 'rake'
6
+ Rake.application.run
@@ -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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This file is used by Rack-based servers to start the application.
4
+
5
+ require_relative 'config/environment'
6
+
7
+ run Rails.application
@@ -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