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.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rubocop.yml +84 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +1216 -0
- data/RUBY_VERSION +1 -0
- data/Rakefile +39 -0
- data/VERSION +1 -0
- data/lib/rails_ops.rb +96 -0
- data/lib/rails_ops/authorization_backend/abstract.rb +7 -0
- data/lib/rails_ops/authorization_backend/can_can_can.rb +14 -0
- data/lib/rails_ops/configuration.rb +4 -0
- data/lib/rails_ops/context.rb +35 -0
- data/lib/rails_ops/controller_mixin.rb +105 -0
- data/lib/rails_ops/exceptions.rb +19 -0
- data/lib/rails_ops/hooked_job.rb +25 -0
- data/lib/rails_ops/hookup.rb +80 -0
- data/lib/rails_ops/hookup/dsl.rb +29 -0
- data/lib/rails_ops/hookup/dsl_validator.rb +45 -0
- data/lib/rails_ops/hookup/hook.rb +11 -0
- data/lib/rails_ops/log_subscriber.rb +24 -0
- data/lib/rails_ops/mixins.rb +2 -0
- data/lib/rails_ops/mixins/authorization.rb +83 -0
- data/lib/rails_ops/mixins/log_settings.rb +20 -0
- data/lib/rails_ops/mixins/model.rb +4 -0
- data/lib/rails_ops/mixins/model/authorization.rb +64 -0
- data/lib/rails_ops/mixins/model/nesting.rb +180 -0
- data/lib/rails_ops/mixins/policies.rb +42 -0
- data/lib/rails_ops/mixins/require_context.rb +33 -0
- data/lib/rails_ops/mixins/routes.rb +35 -0
- data/lib/rails_ops/mixins/schema_validation.rb +25 -0
- data/lib/rails_ops/mixins/sub_ops.rb +35 -0
- data/lib/rails_ops/model_casting.rb +17 -0
- data/lib/rails_ops/model_mixins.rb +12 -0
- data/lib/rails_ops/model_mixins/ar_extension.rb +20 -0
- data/lib/rails_ops/model_mixins/parent_op.rb +10 -0
- data/lib/rails_ops/model_mixins/protected_attributes.rb +78 -0
- data/lib/rails_ops/model_mixins/virtual_attributes.rb +24 -0
- data/lib/rails_ops/model_mixins/virtual_attributes/virtual_column_wrapper.rb +9 -0
- data/lib/rails_ops/model_mixins/virtual_has_one.rb +32 -0
- data/lib/rails_ops/operation.rb +215 -0
- data/lib/rails_ops/operation/model.rb +168 -0
- data/lib/rails_ops/operation/model/create.rb +35 -0
- data/lib/rails_ops/operation/model/destroy.rb +26 -0
- data/lib/rails_ops/operation/model/load.rb +72 -0
- data/lib/rails_ops/operation/model/update.rb +31 -0
- data/lib/rails_ops/patches/active_type_patch.rb +52 -0
- data/lib/rails_ops/profiler.rb +47 -0
- data/lib/rails_ops/profiler/node.rb +64 -0
- data/lib/rails_ops/railtie.rb +19 -0
- data/lib/rails_ops/scoped_env.rb +20 -0
- data/lib/rails_ops/virtual_model.rb +19 -0
- data/rails_ops.gemspec +58 -0
- data/test/test_helper.rb +3 -0
- 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,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,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
|