lev 0.0.1

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