lev 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,48 @@
1
+
2
+ module Lev
3
+
4
+ # A utility method for calling handlers from controllers. To use,
5
+ # include this in your relevant controllers (or in your ApplicationController),
6
+ # e.g.:
7
+ #
8
+ # class ApplicationController
9
+ # include Lev::HandleWith
10
+ # ...
11
+ # end
12
+ #
13
+ # Then, call it from your various controller actions, e.g.:
14
+ #
15
+ # handle_with(MyFormHandler,
16
+ # params: params,
17
+ # success: lambda { redirect_to 'show', notice: 'Success!'},
18
+ # failure: lambda { render 'new', alert: 'Error' })
19
+ #
20
+ # handle_with takes care of calling the handler and populates
21
+ # @errors and @results objects with the return values from the handler
22
+ #
23
+ # The 'success' and 'failure' lambdas are called if there aren't or are errors,
24
+ # respectively. Alternatively, if you supply a 'complete' lambda, that lambda
25
+ # will be called regardless of whether there are any errors.
26
+ #
27
+ # Specifying 'params' is optional. If you don't specify it, HandleWith will
28
+ # use the entire params hash from the request.
29
+ #
30
+ module HandleWith
31
+ def handle_with(handler, options)
32
+ options[:success] ||= lambda {}
33
+ options[:failure] ||= lambda {}
34
+ options[:params] ||= params
35
+
36
+ @results, @errors = handler.handle(current_user, options[:params])
37
+
38
+ if options[:complete].nil?
39
+ @errors.empty? ?
40
+ options[:success].call :
41
+ options[:failure].call
42
+ else
43
+ options[:complete].call
44
+ end
45
+ end
46
+ end
47
+
48
+ end
Binary file
@@ -0,0 +1,30 @@
1
+ module Lev::Handler
2
+
3
+ class Error
4
+ # need a type or source that can be :activerecord
5
+ # when activerecord, data should contain specific fields that
6
+ # can be used by generate_message in BetterErrors
7
+ attr_accessor :code
8
+ attr_accessor :data
9
+ attr_accessor :kind
10
+ attr_accessor :message
11
+ attr_accessor :offending_params
12
+
13
+ def initialize(args={})
14
+ raise IllegalArgument if args[:code].blank?
15
+
16
+ self.code = args[:code]
17
+ self.data = args[:data]
18
+ self.kind = args[:kind]
19
+ self.message = args[:message]
20
+
21
+ self.offending_params = args[:offending_params]
22
+ self.offending_params = [self.offending_params] if !(self.offending_params.is_a? Array)
23
+ end
24
+
25
+ def translate
26
+ ErrorTranslator.translate(self)
27
+ end
28
+ end
29
+
30
+ end
@@ -0,0 +1,28 @@
1
+ module Lev::Handler
2
+
3
+ class ErrorTransferer
4
+
5
+ def self.transfer(source, handler_target, param_group)
6
+ case source
7
+ when ActiveRecord::Base, Lev::Paramifier
8
+ source.errors.each_with_type_and_message do |attribute, type, message|
9
+ handler_target.errors.add(
10
+ code: type,
11
+ data: {
12
+ model: source,
13
+ attribute: attribute
14
+ },
15
+ kind: :activerecord,
16
+ message: message,
17
+ offending_params: [param_group].flatten << attribute
18
+ )
19
+ end
20
+ else
21
+ raise Exception
22
+ end
23
+
24
+ end
25
+
26
+ end
27
+
28
+ end
@@ -0,0 +1,20 @@
1
+ module Lev::Handler
2
+
3
+ class ErrorTranslator
4
+
5
+ def self.translate(error)
6
+ case error.kind
7
+ when :activerecord
8
+ model = error.data[:model]
9
+ attribute = error.data[:attribute]
10
+ # TODO error.message might always be populated now -- really need the other call after ||?
11
+ message = error.message || Lev::BetterActiveModelErrors.generate_message(model, attribute, error.code)
12
+ Lev::BetterActiveModelErrors.full_message(model, attribute, message)
13
+ else
14
+ error.code.to_s
15
+ end
16
+ end
17
+
18
+ end
19
+
20
+ end
@@ -0,0 +1,19 @@
1
+ module Lev::Handler
2
+
3
+ class Errors < Array
4
+ def add(args)
5
+ push(Error.new(args))
6
+ end
7
+
8
+ def [](key)
9
+ self[key]
10
+ end
11
+
12
+ # Checks to see if the provided param identifier is one of the offending
13
+ # params, e.g. has_offending_param?([:my_form, :my_text_field_name])
14
+ def has_offending_param?(param)
15
+ self.any?{|error| error.offending_params == param}
16
+ end
17
+ end
18
+
19
+ end
@@ -0,0 +1,194 @@
1
+
2
+ module Lev
3
+
4
+ class Paramifier
5
+ end
6
+
7
+ # Common methods for all handlers. Handlers are classes that are responsible
8
+ # for taking input data from a form or other widget and doing something
9
+ # with it.
10
+ #
11
+ # All handlers must:
12
+ # 2) include this module ("include Lev::Handler")
13
+ # 3) implement the 'exec' method which takes no arguments and does the
14
+ # work the handler is charged with
15
+ # 4) implement the 'authorized?' method which returns true iff the
16
+ # caller is authorized to do what the handler is charged with
17
+ #
18
+ # Handlers may:
19
+ # 1) implement the 'setup' method which runs before 'authorized?' and 'exec'.
20
+ # This method can do anything, and will likely include setting up some
21
+ # instance objects based on the params.
22
+ # 2) Call the class method "paramify" to declare, cast, and validate parts of
23
+ # the params hash. The first argument to paramify is the key in params
24
+ # which points to a hash of params to be paramified. The block passed to
25
+ # paramify looks just like the guts of an ActiveAttr model. Examples:
26
+ #
27
+ # when the incoming params includes :search => {:type, :terms, :num_results}
28
+ # the Handler class would look like:
29
+ #
30
+ # class MyHandler
31
+ # include Lev::Handler
32
+ #
33
+ # paramify :search do
34
+ # attribute :search_type, type: String
35
+ # validates :search_type, presence: true,
36
+ # inclusion: { in: %w(Name Username Any),
37
+ # message: "is not valid" }
38
+ #
39
+ # attribute :search_terms, type: String
40
+ # validates :search_terms, presence: true
41
+ #
42
+ # attribute :num_results, type: Integer
43
+ # validates :num_results, numericality: { only_integer: true,
44
+ # greater_than_or_equal_to: 0 }
45
+ # end
46
+ #
47
+ # def exec
48
+ # # By this time, if there were any errors the handler would have
49
+ # # already populated the errors object and returned.
50
+ # #
51
+ # # Paramify makes a 'search_params' attribute available through
52
+ # # which you can access the paramified params, e.g.
53
+ # x = search_params.num_results
54
+ # ...
55
+ # end
56
+ # end
57
+ #
58
+ # All handler instance methods have the following available to them:
59
+ # 1) 'params' -- the params from the input
60
+ # 2) 'caller' -- the user submitting the input
61
+ # 3) 'errors' -- an object in which to store errors
62
+ # 4) 'results' -- a hash in which to store results for return to calling code
63
+ #
64
+ # See the documentation for Lev::RoutineNesting about other requirements and
65
+ # capabilities of handler classes.
66
+ #
67
+ # The handle methods take the caller and the params objects, which should be
68
+ # self-explanatory.
69
+ #
70
+ # Example:
71
+ #
72
+ # class MyHandler
73
+ # include Lev::Handler
74
+ # protected
75
+ # def authorized?
76
+ # # return true iff exec is allowed to be called, e.g. might
77
+ # # check the caller against the params
78
+ # def exec
79
+ # # do the work, add errors to errors object and results to the results hash as needed
80
+ # end
81
+ # end
82
+ #
83
+ module Handler
84
+
85
+ def self.included(base)
86
+ base.extend(ClassMethods)
87
+ base.class_eval do
88
+ include Lev::RoutineNesting
89
+ end
90
+ end
91
+
92
+ def call(caller, params, options={})
93
+ in_transaction do
94
+ handle_guts(caller, params)
95
+ end
96
+ end
97
+
98
+ module ClassMethods
99
+ def handle(caller, params, options={})
100
+ new.call(caller, params, options)
101
+ end
102
+
103
+ def paramify(group, options={}, &block)
104
+ method_name = "#{group.to_s}_params"
105
+ variable_sym = "@#{method_name}".to_sym
106
+
107
+ # Generate the dynamic ActiveAttr class given
108
+ # the paramify block; I think the caching of the class
109
+ # in paramify_classes is only necessary to maintain
110
+ # the name of the class set in the const_set statement
111
+
112
+ if paramify_classes[group].nil?
113
+ paramify_classes[group] = Class.new(Lev::Paramifier) do
114
+ include ActiveAttr::Model
115
+ cattr_accessor :group
116
+ end
117
+ paramify_classes[group].class_eval(&block)
118
+ paramify_classes[group].group = group
119
+
120
+ # Attach a name to this dynamic class
121
+ const_set("#{group.to_s.capitalize}Paramifier",
122
+ paramify_classes[group])
123
+ end
124
+
125
+ # Define the "#{group}_params" method to get the paramifier
126
+ # instance wrapping the params
127
+ define_method method_name.to_sym do
128
+ if !instance_variable_get(variable_sym)
129
+ instance_variable_set(variable_sym,
130
+ self.class.paramify_classes[group].new(params[group]))
131
+ end
132
+ instance_variable_get(variable_sym)
133
+ end
134
+
135
+ # Keep track of the accessor for the params so we can check
136
+ # errors in it later
137
+ paramify_methods.push(method_name.to_sym)
138
+ end
139
+
140
+ def paramify_methods
141
+ @paramify_methods ||= []
142
+ end
143
+
144
+ def paramify_classes
145
+ @paramify_classes ||= {}
146
+ end
147
+ end
148
+
149
+ def transfer_errors_from(source, param_group)
150
+ ErrorTransferer.transfer(source, self, param_group)
151
+ end
152
+
153
+ attr_accessor :errors
154
+
155
+ protected
156
+
157
+ attr_accessor :params
158
+ attr_accessor :caller
159
+ attr_accessor :results
160
+
161
+ def handle_guts(caller, params)
162
+ self.params = params
163
+ self.caller = caller
164
+ self.errors = Errors.new
165
+ self.results = {}
166
+
167
+ setup
168
+ raise SecurityTransgression unless authorized?
169
+ validate_paramified_params
170
+ exec unless errors?
171
+
172
+ [self.results, self.errors]
173
+ end
174
+
175
+ def setup; end
176
+
177
+ def authorized?
178
+ false # default for safety, forces implementation in the handler
179
+ end
180
+
181
+ def validate_paramified_params
182
+ self.class.paramify_methods.each do |method|
183
+ params = send(method)
184
+ transfer_errors_from(params, params.group) if !params.valid?
185
+ end
186
+ end
187
+
188
+ def errors?
189
+ errors.any?
190
+ end
191
+
192
+ end
193
+
194
+ end
@@ -0,0 +1,3 @@
1
+ def handler_errors
2
+ @errors || Lev::Handler::Errors.new
3
+ end
@@ -0,0 +1,127 @@
1
+ module Lev
2
+
3
+ # Manages running of routines inside other routines. In the Lev context,
4
+ # Handlers and Algorithms are routines. A routine and any routines nested
5
+ # inside of it are executed within a single transaction, or depending on the
6
+ # requirements of all the routines, no transaction at all.
7
+ #
8
+ # Classes that include this module get:
9
+ #
10
+ # 1) a "run" method for running nested routines in a standardized way.
11
+ # Routines executed through the run method get hooked into the calling
12
+ # hierarchy.
13
+ #
14
+ # 2) a "runner" accessor which points to the routine which called it. If
15
+ # runner is nil that means that no other routine called it (some other
16
+ # code did)
17
+ #
18
+ # 3) a "topmost_runner" which points to the highest routine in the calling
19
+ # hierarchy (that routine whose 'runner' is nil)
20
+ #
21
+ # Classes that include this module must:
22
+ #
23
+ # 1) supply a "call" instance method (def call(*args, &block)) that passes
24
+ # its arguments and block to whatever code inside the class does the work
25
+ # of the class
26
+ #
27
+ # Classes that include this module may:
28
+ #
29
+ # 1) Call the class-level "uses_routine" method to indicate which other
30
+ # routines will be run. Helps set isolation levels, etc. When this
31
+ # method is used, the provided routine may
32
+ #
33
+ # 2) Set a default transaction isolation level by declaring a class method
34
+ # named "default_transaction_isolation" that returns an instance of
35
+ # Lev::TransactionIsolation
36
+ #
37
+ #
38
+ module RoutineNesting
39
+
40
+ def self.included(base)
41
+ base.extend(ClassMethods)
42
+ end
43
+
44
+ module ClassMethods
45
+
46
+ # Called at a routine's class level to foretell which other routines will
47
+ # be used when this routine executes. Helpful for figuring out ahead of
48
+ # time what kind of transaction isolation level should be used.
49
+ def uses_routine(routine_class, options={})
50
+ symbol = options[:as] || routine_class.name.underscore.gsub('/','_').to_sym
51
+
52
+ raise IllegalArgument, "Routine #{routine_class} has already been registered" \
53
+ if nested_routines[symbol]
54
+
55
+ nested_routines[symbol] = routine_class
56
+
57
+ transaction_isolation.replace_if_more_isolated(routine_class.transaction_isolation)
58
+ end
59
+
60
+ def transaction_isolation
61
+ @transaction_isolation ||= default_transaction_isolation
62
+ end
63
+
64
+ def default_transaction_isolation
65
+ TransactionIsolation.no_transaction
66
+ end
67
+
68
+ def nested_routines
69
+ @nested_routines ||= {}
70
+ end
71
+
72
+ end
73
+
74
+ def in_transaction(options={})
75
+ if self == topmost_runner || self.class.transaction_isolation == TransactionIsolation.no_transaction
76
+ yield
77
+ else
78
+ ActiveRecord::Base.isolation_level( self.class.transaction_isolation.symbol ) do
79
+ ActiveRecord::Base.transaction { yield }
80
+ end
81
+ end
82
+ end
83
+
84
+ def run(other_routine, *args, &block)
85
+ if other_routine.is_a? Symbol
86
+ nested_routine = self.class.nested_routines[other_routine]
87
+ if nested_routine.nil?
88
+ raise IllegalArgument,
89
+ "Routine symbol #{other_routine} does not point to a registered routine"
90
+ end
91
+ other_routine = nested_routine
92
+ end
93
+
94
+ other_routine = other_routine.new if other_routine.is_a? Class
95
+
96
+ included_modules = other_routine.eigenclass.included_modules
97
+
98
+ raise IllegalArgument, "Can only run another nested routine" \
99
+ if !(included_modules.include? Lev::RoutineNesting)
100
+
101
+ other_routine.runner = self
102
+ other_routine.call(*args, &block)
103
+ end
104
+
105
+ attr_reader :runner
106
+
107
+ protected
108
+
109
+ attr_writer :runner
110
+
111
+ def topmost_runner
112
+ runner.nil? ? self : runner.topmost_runner
113
+ end
114
+
115
+ def runner=(runner)
116
+ @runner = runner
117
+
118
+ if topmost_runner.class.transaction_isolation.weaker_than(self.class.default_transaction_isolation)
119
+ raise IsolationMismatch,
120
+ "The routine being run has a stronger isolation requirement than " +
121
+ "the isolation being used by the routine(s) running it; call the " +
122
+ "'uses' method in the running routine's initializer"
123
+ end
124
+ end
125
+
126
+ end
127
+ end
@@ -0,0 +1,59 @@
1
+ module Lev
2
+ class TransactionIsolation
3
+
4
+ def initialize(symbol)
5
+ raise IllegalArgument, "Invalid isolation symbol" if !@@symbols_to_isolation_levels.has_key?(symbol)
6
+ @symbol = symbol
7
+ end
8
+
9
+ def self.no_transaction; new(:no_transaction); end
10
+ def self.read_uncommitted; new(:read_uncommitted); end
11
+ def self.read_committed; new(:read_committed); end
12
+ def self.repeatable_read; new(:repeatable_read); end
13
+ def self.serializable; new(:serializable); end
14
+
15
+
16
+ def replace_if_more_isolated(other_transaction_isolation)
17
+ if other_transaction_isolation.isolation_level > self.isolation_level
18
+ self.symbol = other_transaction_isolation.symbol
19
+ end
20
+ self
21
+ end
22
+
23
+ def weaker_than(other)
24
+ self.isolation_level < other.isolation_level
25
+ end
26
+
27
+ def self.mysql_default
28
+ # MySQL default per https://blog.engineyard.com/2010/a-gentle-introduction-to-isolation-levels
29
+ repeatable_read
30
+ end
31
+
32
+ def ==(other)
33
+ self.symbol == other.symbol
34
+ end
35
+
36
+ def eql?(other)
37
+ self == other
38
+ end
39
+
40
+ attr_reader :symbol
41
+
42
+ protected
43
+
44
+ def isolation_level
45
+ @@symbols_to_isolation_levels[symbol]
46
+ end
47
+
48
+ @@symbols_to_isolation_levels = {
49
+ no_transaction: 0,
50
+ read_uncommitted: 1,
51
+ read_committed: 2,
52
+ repeatable_read: 3,
53
+ serializable: 4
54
+ }
55
+
56
+ attr_writer :symbol
57
+
58
+ end
59
+ end
@@ -0,0 +1,3 @@
1
+ module Lev
2
+ VERSION = "0.0.1"
3
+ end
data/lib/lev.rb ADDED
@@ -0,0 +1,57 @@
1
+ require "transaction_isolation"
2
+ require "transaction_retry"
3
+ require "active_attr"
4
+
5
+ require "lev/version"
6
+ require "lev/exceptions"
7
+ require "lev/routine_nesting"
8
+ require "lev/better_active_model_errors"
9
+ require "lev/handler"
10
+ require "lev/handle_with"
11
+ require "lev/handler_helper"
12
+ require "lev/handler/error"
13
+ require "lev/handler/errors"
14
+ require "lev/handler/error_transferer"
15
+ require "lev/handler/error_translator"
16
+ require "lev/form_builder"
17
+ require "lev/algorithm"
18
+ require "lev/delegate_to_algorithm"
19
+ require "lev/transaction_isolation"
20
+
21
+
22
+ module Lev
23
+ class << self
24
+
25
+ ###########################################################################
26
+ #
27
+ # Configuration machinery.
28
+ #
29
+ # To configure Lev, put the following code in your applications
30
+ # initialization logic (eg. in the config/initializers in a Rails app)
31
+ #
32
+ # Lev.configure do |config|
33
+ # config.form_error_class = 'fancy_error'
34
+ # ...
35
+ # end
36
+ #
37
+
38
+ def configure
39
+ yield configuration
40
+ end
41
+
42
+ def configuration
43
+ @configuration ||= Configuration.new
44
+ end
45
+
46
+ class Configuration
47
+ # This HTML class is added to form fields that caused errors
48
+ attr_accessor :form_error_class
49
+
50
+ def initialize
51
+ @form_error_class = 'error'
52
+ super
53
+ end
54
+ end
55
+
56
+ end
57
+ end