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
@@ -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