rails_ops 1.0.0.beta1

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 (57) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.rubocop.yml +84 -0
  4. data/.travis.yml +5 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +1216 -0
  8. data/RUBY_VERSION +1 -0
  9. data/Rakefile +39 -0
  10. data/VERSION +1 -0
  11. data/lib/rails_ops.rb +96 -0
  12. data/lib/rails_ops/authorization_backend/abstract.rb +7 -0
  13. data/lib/rails_ops/authorization_backend/can_can_can.rb +14 -0
  14. data/lib/rails_ops/configuration.rb +4 -0
  15. data/lib/rails_ops/context.rb +35 -0
  16. data/lib/rails_ops/controller_mixin.rb +105 -0
  17. data/lib/rails_ops/exceptions.rb +19 -0
  18. data/lib/rails_ops/hooked_job.rb +25 -0
  19. data/lib/rails_ops/hookup.rb +80 -0
  20. data/lib/rails_ops/hookup/dsl.rb +29 -0
  21. data/lib/rails_ops/hookup/dsl_validator.rb +45 -0
  22. data/lib/rails_ops/hookup/hook.rb +11 -0
  23. data/lib/rails_ops/log_subscriber.rb +24 -0
  24. data/lib/rails_ops/mixins.rb +2 -0
  25. data/lib/rails_ops/mixins/authorization.rb +83 -0
  26. data/lib/rails_ops/mixins/log_settings.rb +20 -0
  27. data/lib/rails_ops/mixins/model.rb +4 -0
  28. data/lib/rails_ops/mixins/model/authorization.rb +64 -0
  29. data/lib/rails_ops/mixins/model/nesting.rb +180 -0
  30. data/lib/rails_ops/mixins/policies.rb +42 -0
  31. data/lib/rails_ops/mixins/require_context.rb +33 -0
  32. data/lib/rails_ops/mixins/routes.rb +35 -0
  33. data/lib/rails_ops/mixins/schema_validation.rb +25 -0
  34. data/lib/rails_ops/mixins/sub_ops.rb +35 -0
  35. data/lib/rails_ops/model_casting.rb +17 -0
  36. data/lib/rails_ops/model_mixins.rb +12 -0
  37. data/lib/rails_ops/model_mixins/ar_extension.rb +20 -0
  38. data/lib/rails_ops/model_mixins/parent_op.rb +10 -0
  39. data/lib/rails_ops/model_mixins/protected_attributes.rb +78 -0
  40. data/lib/rails_ops/model_mixins/virtual_attributes.rb +24 -0
  41. data/lib/rails_ops/model_mixins/virtual_attributes/virtual_column_wrapper.rb +9 -0
  42. data/lib/rails_ops/model_mixins/virtual_has_one.rb +32 -0
  43. data/lib/rails_ops/operation.rb +215 -0
  44. data/lib/rails_ops/operation/model.rb +168 -0
  45. data/lib/rails_ops/operation/model/create.rb +35 -0
  46. data/lib/rails_ops/operation/model/destroy.rb +26 -0
  47. data/lib/rails_ops/operation/model/load.rb +72 -0
  48. data/lib/rails_ops/operation/model/update.rb +31 -0
  49. data/lib/rails_ops/patches/active_type_patch.rb +52 -0
  50. data/lib/rails_ops/profiler.rb +47 -0
  51. data/lib/rails_ops/profiler/node.rb +64 -0
  52. data/lib/rails_ops/railtie.rb +19 -0
  53. data/lib/rails_ops/scoped_env.rb +20 -0
  54. data/lib/rails_ops/virtual_model.rb +19 -0
  55. data/rails_ops.gemspec +58 -0
  56. data/test/test_helper.rb +3 -0
  57. metadata +252 -0
data/RUBY_VERSION ADDED
@@ -0,0 +1 @@
1
+ ruby-2.3.1-p112
data/Rakefile ADDED
@@ -0,0 +1,39 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
3
+
4
+ task default: :test
5
+
6
+ task :gemspec do
7
+ gemspec = Gem::Specification.new do |spec|
8
+ spec.name = 'rails_ops'
9
+ spec.version = IO.read('VERSION').chomp
10
+ spec.authors = ['Sitrox']
11
+ spec.summary = 'A skeleton that allows extracting queries into atomic, reusable classes.'
12
+ spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
13
+ spec.executables = []
14
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
15
+ spec.require_paths = ['lib']
16
+
17
+ spec.add_development_dependency 'bundler', '~> 1.3'
18
+ spec.add_development_dependency 'rake'
19
+ spec.add_development_dependency 'sqlite3'
20
+ spec.add_development_dependency 'yard'
21
+ spec.add_development_dependency 'rubocop', '0.47.1'
22
+ spec.add_development_dependency 'redcarpet'
23
+ spec.add_dependency 'active_type', '~> 0.7.1'
24
+ spec.add_dependency 'minitest'
25
+ spec.add_dependency 'activesupport'
26
+ spec.add_dependency 'activerecord'
27
+ spec.add_dependency 'schemacop', '~> 2.0'
28
+ end
29
+
30
+ File.open('rails_ops.gemspec', 'w') { |f| f.write(gemspec.to_ruby.strip) }
31
+ end
32
+
33
+ require 'rake/testtask'
34
+
35
+ Rake::TestTask.new do |t|
36
+ t.pattern = 'test/rails_ops/**/*_test.rb'
37
+ t.verbose = false
38
+ t.libs << 'test'
39
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.0.beta1
data/lib/rails_ops.rb ADDED
@@ -0,0 +1,96 @@
1
+ module RailsOps
2
+ AUTH_THREAD_STORAGE_KEY = :rails_ops_authorization_enabled
3
+
4
+ def self.config
5
+ @config ||= Configuration.new
6
+ end
7
+
8
+ def self.configure(&_block)
9
+ yield(config)
10
+ end
11
+
12
+ def self.authorization_backend
13
+ return nil unless config.authorization_backend
14
+ return @authorization_backend ||= config.authorization_backend.constantize.new
15
+ end
16
+
17
+ # Returns an instance of the {RailsOps::Hookup} class. This instance is
18
+ # cached depending on the application environment.
19
+ def self.hookup
20
+ Hookup.instance
21
+ end
22
+
23
+ def self.authorization_enabled?
24
+ fail 'No authorization backend is configured.' unless authorization_backend
25
+
26
+ return false unless authorization_backend
27
+
28
+ if Thread.current[AUTH_THREAD_STORAGE_KEY].nil?
29
+ return true
30
+ else
31
+ return Thread.current[AUTH_THREAD_STORAGE_KEY]
32
+ end
33
+ end
34
+
35
+ # Operations within the given block will have disabled authorization.
36
+ # This only applies to the current thread.
37
+ def self.without_authorization(&_block)
38
+ previous_value = Thread.current[AUTH_THREAD_STORAGE_KEY]
39
+ Thread.current[AUTH_THREAD_STORAGE_KEY] = false
40
+ yield
41
+ Thread.current[AUTH_THREAD_STORAGE_KEY] = previous_value
42
+ end
43
+ end
44
+
45
+ # ---------------------------------------------------------------
46
+ # Require Gem active_type and monkey patch
47
+ # ---------------------------------------------------------------
48
+ require 'active_type'
49
+ require 'active_type/type_caster'
50
+ require 'rails_ops/patches/active_type_patch'
51
+
52
+ # ---------------------------------------------------------------
53
+ # Require RailsOps
54
+ # ---------------------------------------------------------------
55
+ require 'rails_ops/authorization_backend/abstract.rb'
56
+ require 'rails_ops/authorization_backend/can_can_can.rb'
57
+ require 'rails_ops/configuration.rb'
58
+ require 'rails_ops/context.rb'
59
+ require 'rails_ops/controller_mixin.rb'
60
+ require 'rails_ops/exceptions.rb'
61
+ require 'rails_ops/hooked_job.rb'
62
+ require 'rails_ops/hookup.rb'
63
+ require 'rails_ops/hookup/dsl.rb'
64
+ require 'rails_ops/hookup/dsl_validator.rb'
65
+ require 'rails_ops/hookup/hook.rb'
66
+ require 'rails_ops/log_subscriber.rb'
67
+ require 'rails_ops/mixins.rb'
68
+ require 'rails_ops/mixins/authorization.rb'
69
+ require 'rails_ops/mixins/log_settings.rb'
70
+ require 'rails_ops/mixins/model.rb'
71
+ require 'rails_ops/mixins/model/authorization.rb'
72
+ require 'rails_ops/mixins/model/nesting.rb'
73
+ require 'rails_ops/mixins/policies.rb'
74
+ require 'rails_ops/mixins/require_context.rb'
75
+ require 'rails_ops/mixins/routes.rb'
76
+ require 'rails_ops/mixins/schema_validation.rb'
77
+ require 'rails_ops/mixins/sub_ops.rb'
78
+ require 'rails_ops/model_casting.rb'
79
+ require 'rails_ops/model_mixins.rb'
80
+ require 'rails_ops/model_mixins/ar_extension.rb'
81
+ require 'rails_ops/model_mixins/parent_op.rb'
82
+ require 'rails_ops/model_mixins/protected_attributes.rb'
83
+ require 'rails_ops/model_mixins/virtual_attributes.rb'
84
+ require 'rails_ops/model_mixins/virtual_attributes/virtual_column_wrapper.rb'
85
+ require 'rails_ops/model_mixins/virtual_has_one.rb'
86
+ require 'rails_ops/operation.rb'
87
+ require 'rails_ops/operation/model.rb'
88
+ require 'rails_ops/operation/model/load.rb'
89
+ require 'rails_ops/operation/model/create.rb'
90
+ require 'rails_ops/operation/model/destroy.rb'
91
+ require 'rails_ops/operation/model/update.rb'
92
+ require 'rails_ops/profiler.rb'
93
+ require 'rails_ops/profiler/node.rb'
94
+ require 'rails_ops/railtie.rb'
95
+ require 'rails_ops/scoped_env.rb'
96
+ require 'rails_ops/virtual_model.rb'
@@ -0,0 +1,7 @@
1
+ module RailsOps::AuthorizationBackend
2
+ class Abstract
3
+ def authorize!(_operation, *_args)
4
+ fail NotImplementedError
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,14 @@
1
+ module RailsOps::AuthorizationBackend
2
+ class CanCanCan < Abstract
3
+ def initialize
4
+ unless defined?(CanCanCan)
5
+ fail "RailsOps is configured to use CanCanCan authorization backend, but the Gem 'cancancan' does not appear to be installed."
6
+ end
7
+ end
8
+
9
+ def authorize!(operation, *args)
10
+ ability = operation.context.try(:ability) || fail(RailsOps::Exceptions::AuthorizationNotPerformable)
11
+ ability.authorize!(*args)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,4 @@
1
+ class RailsOps::Configuration < ActiveType::Object
2
+ attribute :lock_models_at_build, :boolean, default: true
3
+ attribute :authorization_backend, :string, default: nil
4
+ end
@@ -0,0 +1,35 @@
1
+ module RailsOps
2
+ class Context < ActiveType::Object
3
+ attribute :user
4
+ attribute :ability
5
+ attribute :op_chain, default: []
6
+ attribute :session
7
+ attribute :called_via_hook
8
+ attribute :url_options
9
+
10
+ # Returns a copy of the context with the given operation added to the
11
+ # contexts operation chain.
12
+ def spawn(op)
13
+ return Context.new(
14
+ user: user,
15
+ ability: ability,
16
+ session: session,
17
+ op_chain: op_chain + [op],
18
+ called_via_hook: false,
19
+ url_options: url_options
20
+ )
21
+ end
22
+
23
+ # Runs the given operation in this particular context with the given args
24
+ # using the non-bang `run` method.
25
+ def run(op, *args)
26
+ op.run(self, *args)
27
+ end
28
+
29
+ # Runs the given operation in this particular context with the given args
30
+ # using the bang `run!` method.
31
+ def run!(op, *args)
32
+ op.run!(self, *args)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,105 @@
1
+ module RailsOps
2
+ module ControllerMixin
3
+ extend ActiveSupport::Concern
4
+
5
+ # TODO: A similar thing is already done in the operation
6
+ # initializer. Are both necessary? This one here contains
7
+ # more exceptions though.
8
+ EXCEPT_PARAMS = [
9
+ :controller,
10
+ :action,
11
+ :utf8,
12
+ :authenticity_token,
13
+ :_referer_depth,
14
+ :_referer,
15
+ :_method
16
+ ].freeze
17
+
18
+ included do
19
+ if defined?(helper_method)
20
+ helper_method :model
21
+ helper_method :op
22
+ helper_method :op?
23
+
24
+ after_action :ensure_operation_authorize_called!
25
+ end
26
+ end
27
+
28
+ # Instantiates and returns a new operation with the given class. If no class
29
+ # is given, it just returns the previously assigned operation or raises if
30
+ # none has been given.
31
+ def op(op_class = nil, custom_params = nil)
32
+ set_op_class(op_class, custom_params) if op_class
33
+ fail 'Operation is not set.' unless @op
34
+ return @op
35
+ end
36
+
37
+ def op?
38
+ !!@op
39
+ end
40
+
41
+ # If there is a current operation set, it is made sure that authorization
42
+ # has been performed within the operation. This only applies if
43
+ # authorization is not disabled.
44
+ def ensure_operation_authorize_called!
45
+ return unless op?
46
+ op.ensure_authorize_called!
47
+ end
48
+
49
+ # Runs an operation and fails on validation errors using an exception. If
50
+ # no operation class is given, it takes the operation previosly set by {op}
51
+ # or fails if no operation has been set. If an op_class is given, it will be
52
+ # set using the {op} method.
53
+ def run!(op_class = nil, custom_params = nil)
54
+ op(op_class, custom_params) if op_class
55
+ op.run!
56
+ end
57
+
58
+ # Runs an operation and returns `true` for success and `false` for any
59
+ # validation errors. The supplied block is yielded only on success.
60
+ # See {run!} for more information.
61
+ def run(op_class = nil, custom_params = nil, &_block)
62
+ op(op_class, custom_params) if op_class
63
+ success = op.run
64
+ yield if success && block_given?
65
+ return success
66
+ end
67
+
68
+ def model
69
+ return @model if @model
70
+ fail 'Current operation does not support `model` method.' unless op.respond_to?(:model)
71
+ return op.model
72
+ end
73
+
74
+ def filter_op_params(params)
75
+ (params || {}).except(*EXCEPT_PARAMS)
76
+ end
77
+
78
+ def op_params
79
+ filter_op_params(params.permit!).to_h
80
+ end
81
+
82
+ def op_context
83
+ @op_context ||= begin
84
+ context = RailsOps::Context.new
85
+ context.user = current_user if defined?(:current_user)
86
+ context.ability = current_ability if defined?(:current_ability)
87
+ context.session = session
88
+ context.url_options = url_options
89
+ context
90
+ end
91
+ end
92
+
93
+ protected
94
+
95
+ def set_op_class(op_class, custom_params = nil)
96
+ fail 'Operation class is already set.' if @op_class
97
+ @op_class = op_class
98
+ @op = instantiate_op(custom_params)
99
+ end
100
+
101
+ def instantiate_op(custom_params = nil)
102
+ return @op_class.new(op_context, custom_params || op_params)
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,19 @@
1
+ module RailsOps::Exceptions
2
+ class Base < StandardError; end
3
+ class ValidationFailed < Base; end
4
+ class ModelNotDeleteable < Base; end
5
+ class AuthorizationNotPerformable < Base; end
6
+ class NoAuthorizationPerformed < Base; end
7
+ class MissingContextAttribute < Base; end
8
+ class RoutingNotAvailable < Base; end
9
+ class RollbackRequired < Base; end
10
+
11
+ class SubOpValidationFailed < Base
12
+ attr_reader :original_exception
13
+
14
+ def initialize(original_exception)
15
+ @original_exception = original_exception
16
+ super original_exception.message
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,25 @@
1
+ module RailsOps
2
+ # This class extends ActiveJob::Job and, when subclassed, allows to link
3
+ # common job classes to operations. When defining a Job, just extend this
4
+ # class and use the static `op` method in order to hook it to a specific
5
+ # operation. The `perform` method will then automatically run the operation
6
+ # via its `run!` method and with the given params.
7
+ class HookedJob < ActiveJob::Base
8
+ class_attribute :operation_class
9
+
10
+ # Set an operation class this job shall be hooked with. This is mandatory
11
+ # unless you override the `perform` method (which would be an abuse of this
12
+ # class anyways).
13
+ def self.op(klass)
14
+ self.operation_class = klass
15
+ end
16
+
17
+ # This method is called by the ActiveJob framework and solely executes the
18
+ # hooked operation's `run!` method. If no operation has been hooked (use the
19
+ # static method `op` for that), it will raise an exception.
20
+ def perform(params = {})
21
+ fail 'This job is not hooked to any operation.' unless operation_class
22
+ operation_class.run!(params)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,80 @@
1
+ class RailsOps::Hookup
2
+ REQUEST_STORE_KEY = 'RailsOps::Hookup'.freeze
3
+ CONFIG_PATH = 'config/hookup.rb'.freeze
4
+
5
+ attr_reader :hooks
6
+
7
+ def self.instance
8
+ if defined?(Rails) && Rails.env.development?
9
+ return RequestStore.store[REQUEST_STORE_KEY] ||= new
10
+ else
11
+ @instance ||= new
12
+ return @instance
13
+ end
14
+ end
15
+
16
+ def initialize
17
+ @hooks = nil
18
+ @drawn = false
19
+ @config_loaded = false
20
+ end
21
+
22
+ def load_config
23
+ unless @config_loaded
24
+ @config_loaded = true
25
+ load Rails.root.join(CONFIG_PATH)
26
+ end
27
+
28
+ unless @drawn
29
+ fail 'Hooks are not drawn.'
30
+ end
31
+ end
32
+
33
+ def draw(&block)
34
+ if @drawn
35
+ fail "Hooks can't be drawn twice."
36
+ end
37
+
38
+ dsl = DSL.new(&block)
39
+ dsl.validate!
40
+
41
+ @hooks = dsl.hooks
42
+ @drawn = true
43
+ end
44
+
45
+ def hooks_for(operation, event)
46
+ load_config
47
+
48
+ hooks = []
49
+
50
+ @hooks.slice('*', operation.class.name).values.each do |hooks_by_event|
51
+ hooks += hooks_by_event.slice('*', event).values.flatten || []
52
+ end
53
+
54
+ return hooks
55
+ end
56
+
57
+ def trigger_params
58
+ {}
59
+ end
60
+
61
+ def trigger(operation, event, params)
62
+ context = operation.context.spawn(operation)
63
+ context.called_via_hook = true
64
+
65
+ hooks_for(operation, event).each do |hook|
66
+ if context.op_chain.collect(&:class).collect(&:to_s).include?(hook.target_operation)
67
+ next
68
+ end
69
+
70
+ begin
71
+ op_class = hook.target_operation.constantize
72
+ rescue NameError
73
+ fail "Could not find hook target operation #{hook.target_operation}."
74
+ end
75
+
76
+ op = op_class.new(context, params)
77
+ op.run!
78
+ end
79
+ end
80
+ end