rails_ops 1.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
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