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
@@ -0,0 +1,168 @@
1
+ class RailsOps::Operation::Model < RailsOps::Operation
2
+ include RailsOps::Mixins::Model::Authorization
3
+ include RailsOps::Mixins::Model::Nesting
4
+
5
+ class_attribute :_model_class
6
+
7
+ # Allows to set or get a model class associated with this operation.
8
+ #
9
+ # To get the model class, call this method without any parameters. Note that
10
+ # this will result in an exception if the model class has not been previously
11
+ # set.
12
+ #
13
+ # To set the model class, there are 3 ways:
14
+ #
15
+ # - You only specify an existing class as `model_class`.
16
+ # - You only specify a block of code. This will result in a dynamically
17
+ # created class using the provided code block. The base class that is
18
+ # extended by this block is determined by the method
19
+ # {default_model_class}. Check this method to determine the used base class
20
+ # or overwrite it. This only makes sense for inheritance as there is the
21
+ # parameter `model_class` (see next point).
22
+ # - You specify a `model_class` as well as a block of code. In this case, the
23
+ # supplied class will be dynamically extended with the code block provided.
24
+ #
25
+ # This is a DSL method. Do not call it at runtime (i.e. after bootup) as it
26
+ # is not thread-safe and may generate anonymous classes.
27
+ #
28
+ # @param model_class [Class] The model class to take or extend
29
+ # @param name [String,Symbol] A custom name that will be used for generating
30
+ # the anonymous class
31
+ # @yield [] This optional block is executed in context of the newly generated,
32
+ # anonymous class and allows you to extend it
33
+ def self.model(model_class = nil, name = nil, &block)
34
+ if model_class || block_given?
35
+ fail 'Model class can only be set once.' if _model_class
36
+
37
+ model_class ||= default_model_class
38
+
39
+ # ---------------------------------------------------------------
40
+ # If we're given a block or we're configured to always extend the
41
+ # given model class, create a new model class inheriting from the
42
+ # given one and mix in required modules.
43
+ # ---------------------------------------------------------------
44
+ if block_given? || always_extend_model_class?
45
+ self._model_class = Class.new(model_class)
46
+
47
+ # Include operation mixins that provide various functionality needed for
48
+ # operation-specific models. We can safely include this even if it has
49
+ # already been included, as module includes only happen once.
50
+ _model_class.send(:include, RailsOps::ModelMixins)
51
+
52
+ # Apply the given block to the newly created class.
53
+ _model_class.class_eval(&block) if block_given?
54
+
55
+ # Set virtual model name if given.
56
+ if name && _model_class.respond_to?(:virtual_model_name=)
57
+ _model_class.virtual_model_name = ActiveModel::Name.new(_model_class, nil, name.to_s)
58
+ end
59
+
60
+ # ---------------------------------------------------------------
61
+ # We just use the given model class without any adaptions
62
+ # ---------------------------------------------------------------
63
+ else
64
+ self._model_class = model_class
65
+ end
66
+ elsif _model_class.nil?
67
+ fail 'No model class has been set.'
68
+ end
69
+
70
+ return _model_class
71
+ end
72
+
73
+ # This method determines whether the given model class (specified using the
74
+ # static `model` method) is always extended with an anonymous class. This can
75
+ # be required if the operation modifies the model class.
76
+ def self.always_extend_model_class?
77
+ false
78
+ end
79
+
80
+ # Returns the class used for extension when defining a model using the {model}
81
+ # method.
82
+ def self.default_model_class
83
+ ActiveType::Object
84
+ end
85
+
86
+ # Returns an instance of the operation model class. The instance is obtained
87
+ # using {build_model} and cached for the lifespan of the operation instance.
88
+ def model
89
+ unless @model
90
+ build_model
91
+ end
92
+
93
+ return @model
94
+ end
95
+
96
+ # Assigns attributes to a model. If no arguments are given, it extracts the
97
+ # attributes from the {params} hash and assigns them to the {model}.
98
+ #
99
+ # @param attributes [Hash] The attributes hash to assign. If not given,
100
+ # attributes will be obtained from the {params} hash using
101
+ # {extract_attributes_from_params}.
102
+ # @param without_protection [Boolean] If `true`, attributes wil be assigned without
103
+ # mass assignment protection.
104
+ # @param model [ActiveRecord::Base] Allows to manually specify a model the attributes
105
+ # will be assigned to. If not given, the model will be obtained from the
106
+ # {model} method.
107
+ # @param without_nested_models [Boolean] Per default, attribute params that
108
+ # are meant for nested models (registered via `nested_model_op`) will not
109
+ # be assigned. You can turn this filtering off by passing `false`.
110
+ def assign_attributes(attributes = nil, model: nil, without_protection: false, without_nested_models: true)
111
+ model ||= self.model
112
+
113
+ unless attributes
114
+ # Extract attributes from params hash
115
+ attributes = extract_attributes_from_params(model)
116
+ end
117
+
118
+ if without_nested_models
119
+ # Remove parameters that will be passed to nested model operations
120
+ # such as `post_attributes: {}`.
121
+ attributes = attributes.except(*self.class.nested_model_param_keys)
122
+ end
123
+
124
+ # Do nothing if there are no attributes to assign
125
+ return if attributes.nil? || attributes.empty?
126
+
127
+ # Assign attributes, either with or without protection
128
+ if !without_protection && model.respond_to?(:assign_attributes_with_protection)
129
+ model.assign_attributes_with_protection(attributes)
130
+ else
131
+ model.assign_attributes(attributes)
132
+ end
133
+ end
134
+
135
+ # Extracts model attributes from the {params} hash using
136
+ # `model.model_name.param_key`. If no model is given, the model will be
137
+ # obtained from the {model} method.
138
+ # @param model [ActiveRecord::Base] An optional model for determining the
139
+ # param key name
140
+ def extract_attributes_from_params(model = nil)
141
+ model ||= self.model
142
+ return params[model.model_name.param_key] || {}
143
+ end
144
+
145
+ protected
146
+
147
+ # Performs nested model operations and then saves the model. If you have
148
+ # nested model operations, you should call this method instead of calling
149
+ # `model.save!` directly.
150
+ def save!
151
+ perform_nested_model_ops!
152
+ model.save!
153
+ end
154
+
155
+ # Override of the base classes method to provide the model under key `model`
156
+ # to operations called via the `after_run` hook. You can still override this
157
+ # to suit your own operation's needs.
158
+ def after_run_trigger_params
159
+ { model: model }
160
+ end
161
+
162
+ # This method must be implemented on superclasses in order to return an
163
+ # instance of the operation model class. This can be a new or existing record,
164
+ # depending on the respective operation's needs.
165
+ def build_model
166
+ fail NotImplementedError, 'Method `build_model` must be implemented.'
167
+ end
168
+ end
@@ -0,0 +1,35 @@
1
+ class RailsOps::Operation::Model::Create < RailsOps::Operation::Model
2
+ model_authorization_action :create
3
+
4
+ policy :on_init do
5
+ model_authorization
6
+ end
7
+
8
+ # As this operation might extend the model class, we need to make sure that
9
+ # the operation works using an extended 'copy' of the given model class.
10
+ def self.always_extend_model_class?
11
+ true
12
+ end
13
+
14
+ def model_authorization
15
+ return unless authorization_enabled?
16
+
17
+ unless model_authorization_action.nil?
18
+ authorize_model! model_authorization_action, model
19
+ end
20
+ end
21
+
22
+ def build_model
23
+ fail 'Model can only be built once.' if @model
24
+ @model = self.class.model.new
25
+ if @model.respond_to?(:parent_op=)
26
+ @model.parent_op = self
27
+ end
28
+ build_nested_model_ops :create
29
+ assign_attributes
30
+ end
31
+
32
+ def perform
33
+ save!
34
+ end
35
+ end
@@ -0,0 +1,26 @@
1
+ class RailsOps::Operation::Model::Destroy < RailsOps::Operation::Model::Load
2
+ model_authorization_action :destroy
3
+
4
+ def model_authorization
5
+ return unless authorization_enabled?
6
+
7
+ unless load_model_authorization_action.nil?
8
+ authorize_model_with_authorize_only! load_model_authorization_action, model
9
+ end
10
+
11
+ unless model_authorization_action.nil?
12
+ authorize_model! model_authorization_action, model
13
+ end
14
+ end
15
+
16
+ policy do
17
+ if model.respond_to?(:deleteable?) && !model.deleteable?
18
+ fail RailsOps::Exceptions::ModelNotDeleteable
19
+ end
20
+ end
21
+
22
+ def perform
23
+ trigger :before_destroy, model: model
24
+ model.destroy!
25
+ end
26
+ end
@@ -0,0 +1,72 @@
1
+ class RailsOps::Operation::Model::Load < RailsOps::Operation::Model
2
+ class_attribute :_lock_model_at_build
3
+ class_attribute :_load_model_authorization_action
4
+
5
+ policy :on_init do
6
+ model_authorization
7
+ end
8
+
9
+ # Gets or sets the action verb used for authorizing models on load.
10
+ def self.load_model_authorization_action(*action)
11
+ if action.size == 1
12
+ self._load_model_authorization_action = action.first
13
+ elsif action.size > 1
14
+ fail ArgumentError, 'Too many arguments'
15
+ end
16
+
17
+ return _load_model_authorization_action
18
+ end
19
+
20
+ def model_authorization
21
+ return unless authorization_enabled?
22
+
23
+ unless load_model_authorization_action.nil?
24
+ authorize_model! load_model_authorization_action, model
25
+ end
26
+ end
27
+
28
+ def load_model_authorization_action
29
+ self.class.load_model_authorization_action
30
+ end
31
+
32
+ load_model_authorization_action :read
33
+
34
+ def self.lock_model_at_build(enabled = true)
35
+ self._lock_model_at_build = enabled
36
+ end
37
+
38
+ def self.lock_model_at_build?
39
+ _lock_model_at_build.nil? ? RailsOps.config.lock_models_at_build? : _lock_model_at_build
40
+ end
41
+
42
+ def model_id_field
43
+ :id
44
+ end
45
+
46
+ def find_model
47
+ unless params[model_id_field]
48
+ fail "Param #{model_id_field.inspect} must be given."
49
+ end
50
+
51
+ # Get model class
52
+ relation = self.class.model
53
+
54
+ # Express intention to lock if required
55
+ relation = relation.lock if self.class.lock_model_at_build?
56
+
57
+ # Fetch (and possibly lock) model
58
+ return relation.find_by!(model_id_field => params[model_id_field])
59
+ end
60
+
61
+ def build_model
62
+ @model = find_model
63
+
64
+ if @model.respond_to?(:parent_op=)
65
+ @model.parent_op = self
66
+ end
67
+ end
68
+
69
+ def extract_id_from_params
70
+ params[model_id_field]
71
+ end
72
+ end
@@ -0,0 +1,31 @@
1
+ class RailsOps::Operation::Model::Update < RailsOps::Operation::Model::Load
2
+ model_authorization_action :update
3
+
4
+ # As this operation might extend the model class, we need to make sure that
5
+ # the operation works using an extended 'copy' of the given model class.
6
+ def self.always_extend_model_class?
7
+ true
8
+ end
9
+
10
+ def model_authorization
11
+ return unless authorization_enabled?
12
+
13
+ unless load_model_authorization_action.nil?
14
+ authorize_model_with_authorize_only! load_model_authorization_action, model
15
+ end
16
+
17
+ unless model_authorization_action.nil?
18
+ authorize_model! model_authorization_action, model
19
+ end
20
+ end
21
+
22
+ def build_model
23
+ super
24
+ build_nested_model_ops :update
25
+ assign_attributes
26
+ end
27
+
28
+ def perform
29
+ save!
30
+ end
31
+ end
@@ -0,0 +1,52 @@
1
+ # Internal reference: #25564
2
+ module ActiveType
3
+ class TypeCaster
4
+ def type_cast_from_user(value)
5
+ # For some reason, Rails defines additional type casting logic
6
+ # outside the classes that have that responsibility.
7
+ case @type
8
+ when :integer
9
+ if value == ''
10
+ nil
11
+ else
12
+ native_type_cast_from_user(value)
13
+ end
14
+ when :timestamp, :datetime
15
+ time = native_type_cast_from_user(value)
16
+ if time && ActiveRecord::Base.time_zone_aware_attributes
17
+ time = ActiveSupport::TimeWithZone.new(nil, Time.zone, time)
18
+ end
19
+ time
20
+ when Integer.class
21
+ if value == ''
22
+ nil
23
+ else
24
+ value.to_i
25
+ end
26
+ else
27
+ native_type_cast_from_user(value)
28
+ end
29
+ end
30
+
31
+ module NativeCasters
32
+ class DelegateToType
33
+ def initialize(type, connection)
34
+ # The specified type (e.g. "string") may not necessary match the
35
+ # native type ("varchar") expected by the connection adapter.
36
+ # PostgreSQL is one of these. Perform a translation if the adapter
37
+ # supports it (but don't turn a mysql boolean into a tinyint).
38
+ if !type.nil? && !(type == :boolean) && type.respond_to?(:to_sym) && connection.respond_to?(:native_database_types)
39
+ native_type = connection.native_database_types[type.try(:to_sym)]
40
+ if native_type && native_type[:name]
41
+ type = native_type[:name]
42
+ else
43
+ # unknown type, we just dont cast
44
+ type = nil
45
+ end
46
+ end
47
+ @active_record_type = connection.lookup_cast_type(type)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,47 @@
1
+ module RailsOps
2
+ class Profiler
3
+ def self.profile(object_id, description = nil, &_block)
4
+ node = tstore_nodes[object_id] = ::RailsOps::Profiler::Node.new(object_id, description, tstore_current_parent)
5
+ self.tstore_current_parent = node
6
+ res = yield
7
+ self.tstore_current_parent = node.parent
8
+ node.finish_measure
9
+ res
10
+ end
11
+
12
+ def self.time(descr = nil, &_block)
13
+ descr += ' - ' if descr
14
+ start = Time.now
15
+ res = yield
16
+ puts "#{descr}#{((Time.now - start).to_f * 1000).round(1)}ms elapsed.".magenta
17
+ res
18
+ end
19
+
20
+ def self.forget_all
21
+ Thread.current[:rails_ops_profiler][:nodes] = {}
22
+ end
23
+
24
+ def self.forget(object_id)
25
+ tstore_nodes.delete(object_id)
26
+ end
27
+
28
+ def self.node(object_id)
29
+ tstore_nodes[object_id] || fail("Unkown object_id #{object_id}.")
30
+ end
31
+
32
+ def self.tstore_current_parent
33
+ Thread.current[:rails_ops_profiler] ||= {}
34
+ Thread.current[:rails_ops_profiler][:current_parent]
35
+ end
36
+
37
+ def self.tstore_current_parent=(parent)
38
+ Thread.current[:rails_ops_profiler] ||= {}
39
+ Thread.current[:rails_ops_profiler][:current_parent] = parent
40
+ end
41
+
42
+ def self.tstore_nodes
43
+ Thread.current[:rails_ops_profiler] ||= {}
44
+ Thread.current[:rails_ops_profiler][:nodes] ||= {}
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,64 @@
1
+ module RailsOps
2
+ class Profiler::Node
3
+ def initialize(object_id, description = nil, parent = nil)
4
+ @object_id = object_id
5
+ @description = description
6
+ @parent = parent
7
+ parent&.add_child(self)
8
+ @children = []
9
+ @t_start = Time.now
10
+ end
11
+
12
+ attr_reader :parent
13
+ attr_reader :description
14
+
15
+ def finish_measure
16
+ @t_stop = Time.now
17
+ end
18
+
19
+ def t_self
20
+ t_total - t_kids
21
+ end
22
+
23
+ def t_kids
24
+ @children.map(&:t_total).inject(:+) || 0
25
+ end
26
+
27
+ def t_total
28
+ fail "Measure for object_id #{@object_id} (#{@description}) is not finished." unless @t_stop
29
+ (@t_stop - @t_start).to_f
30
+ end
31
+
32
+ def t_self_s
33
+ t_self
34
+ end
35
+
36
+ def t_kids_s
37
+ t_kids
38
+ end
39
+
40
+ def t_total_s
41
+ t_total
42
+ end
43
+
44
+ def t_self_ms
45
+ t_self * 1000
46
+ end
47
+
48
+ def t_kids_ms
49
+ t_kids * 1000
50
+ end
51
+
52
+ def t_total_ms
53
+ t_total * 1000
54
+ end
55
+
56
+ def free
57
+ ::RailsOps::Profiler.forget(@object_id) unless parent
58
+ end
59
+
60
+ def add_child(child)
61
+ @children << child
62
+ end
63
+ end
64
+ end