lev 0.0.3 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -5,4 +5,4 @@ spec/dummy/db/*.sqlite3
5
5
  spec/dummy/log/*.log
6
6
  spec/dummy/tmp/
7
7
  spec/dummy/.sass-cache
8
- .DS_Store
8
+ *.DS_Store
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
@@ -1,14 +1,23 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- lev (0.0.3)
4
+ lev (1.0.0)
5
+ actionpack (>= 3.0)
5
6
  active_attr
7
+ activemodel (>= 3.0)
8
+ activerecord (>= 3.0)
6
9
  transaction_isolation
7
10
  transaction_retry
8
11
 
9
12
  GEM
10
13
  remote: https://rubygems.org/
11
14
  specs:
15
+ actionpack (4.0.0)
16
+ activesupport (= 4.0.0)
17
+ builder (~> 3.1.0)
18
+ erubis (~> 2.7.0)
19
+ rack (~> 1.5.2)
20
+ rack-test (~> 0.6.2)
12
21
  active_attr (0.8.2)
13
22
  activemodel (>= 3.0.2, < 4.1)
14
23
  activesupport (>= 3.0.2, < 4.1)
@@ -30,10 +39,30 @@ GEM
30
39
  arel (4.0.0)
31
40
  atomic (1.1.14)
32
41
  builder (3.1.4)
42
+ columnize (0.3.6)
43
+ debugger (1.6.2)
44
+ columnize (>= 0.3.1)
45
+ debugger-linecache (~> 1.2.0)
46
+ debugger-ruby_core_source (~> 1.2.3)
47
+ debugger-linecache (1.2.0)
48
+ debugger-ruby_core_source (1.2.3)
49
+ diff-lcs (1.2.4)
50
+ erubis (2.7.0)
33
51
  i18n (0.6.5)
34
52
  minitest (4.7.5)
35
53
  multi_json (1.8.0)
54
+ rack (1.5.2)
55
+ rack-test (0.6.2)
56
+ rack (>= 1.0)
36
57
  rake (10.1.0)
58
+ rspec (2.14.1)
59
+ rspec-core (~> 2.14.0)
60
+ rspec-expectations (~> 2.14.0)
61
+ rspec-mocks (~> 2.14.0)
62
+ rspec-core (2.14.5)
63
+ rspec-expectations (2.14.3)
64
+ diff-lcs (>= 1.1.3, < 2.0)
65
+ rspec-mocks (2.14.3)
37
66
  thread_safe (0.1.3)
38
67
  atomic
39
68
  transaction_isolation (1.0.3)
@@ -48,5 +77,7 @@ PLATFORMS
48
77
 
49
78
  DEPENDENCIES
50
79
  bundler (~> 1.3)
80
+ debugger
51
81
  lev!
52
82
  rake
83
+ rspec
data/README.md CHANGED
@@ -1,6 +1,50 @@
1
1
  # Lev
2
2
 
3
- TODO: Write a gem description
3
+ Rails is fantastic and obviously super successful. Lev is an attempt to improve Rails by:
4
+
5
+ 1. Providing a better, more structured, and more organized way to implement code features
6
+ 2. De-emphasizing the "model is king" mindset when appropriate
7
+
8
+ Rails' MVC-view of the world is very compelling and provides a sturdy scaffold with which to create applications quickly. However, sometimes it can lead to business logic code getting a little spread out. When trying to figure out where to best put business logic, you often hear folks recommending "fat models" and "skinny controllers". They are saying that the business logic of your app should live in the model classes and not in the controllers. I agree that the logic shouldn't live in the controllers, but I also argue that it shouldn't always live in the models either, especially when that logic touches multiple models.
9
+
10
+ When all of the business logic lives in the models, some bad things can happen:
11
+
12
+ 1. your models can become bloated with code that only applies to certain features
13
+ 2. your models end up knowing way too much about other models, sometimes multiple hops away
14
+ 3. your business logic gets spread all over the place. The execution of one "feature" can jump between bits of code in multiple models, their various ActiveRecord life cycle callbacks (before_create, etc), and their associated observers.
15
+
16
+ Lev introduces "routines" which you can think of as pieces of code that have all the responsibility for making one thing (use case) happen, e.g. "add an email to a user", "register a student to a class", etc).
17
+
18
+ Routines...
19
+
20
+ 1. Can call other routines
21
+ 2. Have a common error reporting framework
22
+ 3. Run within a single transaction with a controllable isolation level
23
+
24
+ Handlers are specialized routines that take user input (e.g. form data) and then take an action based on that input.
25
+
26
+ Handlers...
27
+
28
+ 1. Help you verify that the calling user is authorized to run the handler
29
+ 2. Provide ways to validate incoming parameters in a very ActiveModel-like way (even when the parameters are not associated with a model)
30
+ 3. Integrate will with basic routines
31
+
32
+ In a Lev-oriented Rails app, controllers are just responsible for connecting routes to Handlers. In fact, controller methods just end up being calls to ```handle_with(MyHandler)```, ```handle_with``` being a helper method provided by Lev.
33
+
34
+ Lev also provides a ```lev_form_for``` form builder to replace ```form_for```. This builder integrates well with the error reporting infrastructure in routines and handlers, and in general is a nice way to get away from forms that are very single-model-centric.
35
+
36
+ When using Lev, model classes have the following responsibilities:
37
+
38
+
39
+ 1. Hook into the ORM (i.e., inherit from ActiveRecord::Base)
40
+ 2. Establish relationships to other models (e.g. belongs_to, has_many, including dependent: :destroy)
41
+ 3. Validate internal state
42
+ 1. Do not validate state in related models
43
+ 2. Can validate the presence of relationship (can check that a foreign key is present)
44
+ 4. Can perform queries when those queries only use internal model state and are aware of internal model state (e.g. arguments to queries should be in the language of the model state)
45
+ 5. Can create records when those creations only need values internal to this model and take arguments in the language of the internal model state.
46
+
47
+ The result of the principles above and below, model classes end up being very small. This is good because a lot of code depends on the models and having the be small normally means they are also stable.
4
48
 
5
49
  ## Installation
6
50
 
@@ -18,7 +62,14 @@ Or install it yourself as:
18
62
 
19
63
  ## Usage
20
64
 
21
- TODO: Write usage instructions here
65
+ For the moment, details on how to use lev live in big sections of comments at the top of:
66
+
67
+ * https://github.com/lml/lev/blob/master/lib/lev/routine.rb
68
+ * https://github.com/lml/lev/blob/master/lib/lev/handler.rb
69
+ * https://github.com/lml/lev/blob/master/lib/lev/handle_with.rb
70
+ * https://github.com/lml/lev/blob/master/lib/lev/form_builder.rb
71
+
72
+ TBD: talk about ```delegate_to_routine```.
22
73
 
23
74
  ## Contributing
24
75
 
@@ -18,10 +18,16 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
20
 
21
- spec.add_dependency "transaction_isolation"
22
- spec.add_dependency "transaction_retry"
23
- spec.add_dependency "active_attr"
21
+ spec.add_runtime_dependency(%q<activemodel>, [">= 3.0"])
22
+ spec.add_runtime_dependency(%q<activerecord>, [">= 3.0"])
23
+ spec.add_runtime_dependency(%q<actionpack>, [">= 3.0"])
24
+ spec.add_runtime_dependency "transaction_isolation"
25
+ spec.add_runtime_dependency "transaction_retry"
26
+ spec.add_runtime_dependency "active_attr"
24
27
 
25
28
  spec.add_development_dependency "bundler", "~> 1.3"
26
29
  spec.add_development_dependency "rake"
30
+ spec.add_development_dependency "rspec"
31
+ spec.add_development_dependency "debugger"
32
+
27
33
  end
data/lib/lev.rb CHANGED
@@ -1,21 +1,24 @@
1
+ require "action_view"
1
2
  require "transaction_isolation"
2
3
  require "transaction_retry"
3
4
  require "active_attr"
4
5
 
5
6
  require "lev/version"
7
+ require "lev/utilities"
6
8
  require "lev/exceptions"
7
- require "lev/routine_nesting"
8
9
  require "lev/better_active_model_errors"
10
+ require "lev/term_mapper"
11
+ require "lev/routine"
9
12
  require "lev/handler"
10
13
  require "lev/handle_with"
11
14
  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"
15
+ require "lev/error"
16
+ require "lev/errors"
17
+ require "lev/error_transferer"
18
+ require "lev/error_translator"
19
+
16
20
  require "lev/form_builder"
17
- require "lev/algorithm"
18
- require "lev/delegate_to_algorithm"
21
+ require "lev/delegate_to_routine"
19
22
  require "lev/transaction_isolation"
20
23
 
21
24
 
@@ -0,0 +1,25 @@
1
+ # ActiveRecord::Base.delegate_to_routine
2
+ #
3
+ # Let active records delegate certain (likely non-trivial) actions to routines
4
+ #
5
+ # Arguments:
6
+ # method: a symbol for the instance method to delegate, e.g. :destroy
7
+ # options: a hash of options including...
8
+ # :routine_class => The class of the routine to delegate to; if not
9
+ # given,
10
+ ActiveRecord::Base.define_singleton_method(:delegate_to_routine) do |method, options={}|
11
+ routine_class = options[:routine_class]
12
+
13
+ if routine_class.nil?
14
+ routine_class_name = "#{method.to_s.capitalize}#{self.name}"
15
+ routine_class = Kernel.const_get(routine_class_name)
16
+ end
17
+
18
+ self.instance_eval do
19
+ alias_method "#{method}_original".to_sym, method
20
+ define_method method do
21
+ routine_class.call(self)
22
+ end
23
+ end
24
+
25
+ end
@@ -0,0 +1,28 @@
1
+ module Lev
2
+
3
+ class Error
4
+
5
+ attr_accessor :code
6
+ attr_accessor :data
7
+ attr_accessor :kind
8
+ attr_accessor :message
9
+
10
+ # The inputs related to this error
11
+ attr_accessor :offending_inputs
12
+
13
+ def initialize(args={})
14
+ raise IllegalArgument, "must supply a :code" 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
+ self.offending_inputs = args[:offending_inputs]
21
+ end
22
+
23
+ def translate
24
+ ErrorTranslator.translate(self)
25
+ end
26
+ end
27
+
28
+ end
@@ -0,0 +1,41 @@
1
+ module Lev
2
+
3
+ class ErrorTransferer
4
+
5
+ def self.transfer(source, target_routine, input_mapper, fail_if_errors=false)
6
+ case source
7
+ when ActiveRecord::Base, Lev::Paramifier
8
+ source.errors.each_with_type_and_message do |attribute, type, message|
9
+ target_routine.nonfatal_error(
10
+ code: type,
11
+ data: {
12
+ model: source,
13
+ attribute: attribute
14
+ },
15
+ kind: :activerecord,
16
+ message: message,
17
+ offending_inputs: input_mapper.map(attribute)
18
+ )
19
+ end
20
+ when Lev::Errors
21
+ source.each do |error|
22
+ target_routine.nonfatal_error(
23
+ code: error.code,
24
+ data: error.data,
25
+ kind: error.kind,
26
+ message: error.message,
27
+ offending_inputs: input_mapper.map(error.offending_inputs)
28
+ )
29
+ end
30
+ else
31
+ raise Exception
32
+ end
33
+
34
+ # We add nonfatal errors above and then have this call here so that all
35
+ # errors can be transferred before we freak out.
36
+ throw :fatal_errors_encountered if target_routine.errors? && fail_if_errors
37
+ end
38
+
39
+ end
40
+
41
+ end
@@ -1,4 +1,4 @@
1
- module Lev::Handler
1
+ module Lev
2
2
 
3
3
  class ErrorTranslator
4
4
 
@@ -0,0 +1,41 @@
1
+ module Lev
2
+
3
+ # A collection of Error objects.
4
+ #
5
+ class Errors < Array
6
+
7
+ def add(fail, args={})
8
+ args[:kind] ||= :lev
9
+ error = Error.new(args)
10
+ return if ignored_error_procs.any?{|proc| proc.call(error)}
11
+ self.push(error)
12
+ throw :fatal_errors_encountered if fail
13
+ end
14
+
15
+ def ignore(arg)
16
+ proc = arg.is_a?(Symbol) ?
17
+ Proc.new{|error| error.code == arg} :
18
+ arg
19
+
20
+ raise IllegalArgument if !proc.respond_to?(:call)
21
+
22
+ ignored_error_procs.push(proc)
23
+ end
24
+
25
+ def [](key)
26
+ self[key]
27
+ end
28
+
29
+ # Checks to see if the provided input is associated with one of the errors.
30
+ def has_offending_input?(input)
31
+ self.any? {|error| [error.offending_inputs].flatten.include? input}
32
+ end
33
+
34
+ protected
35
+
36
+ def ignored_error_procs
37
+ @ignored_error_procs ||= []
38
+ end
39
+
40
+ end
41
+ end
@@ -1,2 +1,4 @@
1
1
  class Lev::IsolationMismatch < StandardError; end
2
- class Lev::SecurityTransgression < StandardError; end
2
+ class Lev::SecurityTransgression < StandardError; end
3
+ class Lev::AlgorithmError < StandardError; end
4
+ class Lev::AbstractMethodCalled < StandardError; end
@@ -26,7 +26,7 @@ module Lev
26
26
  def fields_for(record_name, record_object = nil, fields_options = {}, &block)
27
27
  raise "Didn't put fields_for into LevitateFormBuilder yet"
28
28
  end
29
-
29
+
30
30
  protected
31
31
 
32
32
  def get_form_params_entry(name)
@@ -38,7 +38,7 @@ module Lev
38
38
  end
39
39
 
40
40
  def has_error?(name)
41
- @options[:errors].present? ? @options[:errors].has_offending_param?([@object_name, name]) : false
41
+ @options[:errors].present? ? @options[:errors].has_offending_input?([@object_name, name]) : false
42
42
  end
43
43
 
44
44
  def set_value_if_available(method, options)
@@ -58,6 +58,6 @@ end
58
58
  def lev_form_for(record_or_name_or_array, *args, &proc)
59
59
  options = args.extract_options!
60
60
  options[:params] = params
61
- options[:errors] = @errors || []
61
+ options[:errors] = handler_errors # @errors || (@handler_outcome ? @handler_outcome.errors : [])
62
62
  form_for(record_or_name_or_array, *(args << options.merge(:builder => Lev::FormBuilder)), &proc)
63
63
  end
@@ -18,14 +18,13 @@ module Lev
18
18
  # failure: lambda { render 'new', alert: 'Error' })
19
19
  #
20
20
  # handle_with takes care of calling the handler and populates
21
- # @errors and @results objects with the return values from the handler
22
- #
21
+ # a @handler_result object with the return value from the handler
23
22
  #
24
23
  # The 'success' and 'failure' lambdas are called if there aren't or are errors,
25
24
  # respectively. Alternatively, if you supply a 'complete' lambda, that lambda
26
25
  # will be called regardless of whether there are any errors. Inside these lambdas
27
- # (and inside the views they connect to), there will be @errors and @results
28
- # variables containing the errors and results from the handler.
26
+ # (and inside the views they connect to), there will be the @handler_outcome
27
+ # variable containing the errors and results from the handler.
29
28
  #
30
29
  # Specifying 'params' is optional. If you don't specify it, HandleWith will
31
30
  # use the entire params hash from the request.
@@ -42,10 +41,10 @@ module Lev
42
41
  options[:request] ||= request
43
42
  options[:caller] ||= current_user
44
43
 
45
- @results, @errors = handler.handle(options)
44
+ @handler_result = handler.handle(options)
46
45
 
47
46
  if complete_action.nil?
48
- @errors.empty? ?
47
+ @handler_result.errors.empty? ?
49
48
  success_action.call :
50
49
  failure_action.call
51
50
  else
@@ -2,29 +2,33 @@
2
2
  module Lev
3
3
 
4
4
  class Paramifier
5
+ def as_hash(keys)
6
+ keys = [keys].flatten.compact
7
+ Hash[keys.collect { |key| [key, self.send(key)] }]
8
+ end
5
9
  end
6
10
 
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.
11
+ # Common methods for all handlers. Handlers are extensions of Routines
12
+ # and are responsible for taking input data from a form or other widget and
13
+ # doing something with it. See Lev::Routine for more information.
10
14
  #
11
15
  # All handlers must:
12
16
  # 2) include this module ("include Lev::Handler")
13
- # 3) implement the 'exec' method which takes no arguments and does the
17
+ # 3) implement the 'handle' method which takes no arguments and does the
14
18
  # work the handler is charged with
15
19
  # 4) implement the 'authorized?' method which returns true iff the
16
20
  # caller is authorized to do what the handler is charged with
17
21
  #
18
22
  # Handlers may:
19
- # 1) implement the 'setup' method which runs before 'authorized?' and 'exec'.
23
+ # 1) implement the 'setup' method which runs before 'authorized?' and 'handle'.
20
24
  # This method can do anything, and will likely include setting up some
21
25
  # instance objects based on the params.
22
26
  # 2) Call the class method "paramify" to declare, cast, and validate parts of
23
27
  # the params hash. The first argument to paramify is the key in params
24
28
  # 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:
29
+ # paramify looks just like the guts of an ActiveAttr model.
26
30
  #
27
- # when the incoming params includes :search => {:type, :terms, :num_results}
31
+ # When the incoming params includes :search => {:type, :terms, :num_results}
28
32
  # the Handler class would look like:
29
33
  #
30
34
  # class MyHandler
@@ -44,7 +48,7 @@ module Lev
44
48
  # greater_than_or_equal_to: 0 }
45
49
  # end
46
50
  #
47
- # def exec
51
+ # def handle
48
52
  # # By this time, if there were any errors the handler would have
49
53
  # # already populated the errors object and returned.
50
54
  # #
@@ -60,26 +64,27 @@ module Lev
60
64
  # 2) 'caller' -- the user submitting the input
61
65
  # 3) 'errors' -- an object in which to store errors
62
66
  # 4) 'results' -- a hash in which to store results for return to calling code
63
- # 5) 'options' -- a hash containing the options passed in, useful for other
67
+ # 5) 'request' -- the HTTP request
68
+ # 6) 'options' -- a hash containing the options passed in, useful for other
64
69
  # nonstandard data.
65
- #
66
- # Handler 'exec' methods don't return anything; they just set values in
70
+ #
71
+ # These methods are available iff these data were supplied in the call
72
+ # to the handler (not all handlers need all of this). However, note that
73
+ # the Lev::HandleWith module supplies an easy way to call Handlers from
74
+ # controllers -- when this way is used, all of the methods above are available.
75
+ #
76
+ # Handler 'handle' methods don't return anything; they just set values in
67
77
  # the errors and results objects. The documentation for each handler
68
78
  # should explain what the results will be and any nonstandard data required
69
79
  # to be passed in in the options.
70
80
  #
71
- # See the documentation for Lev::RoutineNesting about other requirements and
72
- # capabilities of handler classes.
73
- #
74
- # The handle methods take a hash of arguments.
75
- # caller: the calling user
76
- # params: the params object
77
- # request: the http request object
81
+ # In addition to the class- and instance-level "call" methods provided by
82
+ # Lev::Routine, Handlers have a class-level "handle" method (an alias of
83
+ # the class-level "call" method). The convention for handlers is that the
84
+ # call methods take a hash of options/inputs. The instance-level handle
85
+ # method doesn't take any arguments since the arguments have been stored
86
+ # as instance variables by the time the instance-level handle method is called.
78
87
  #
79
- # These arguments are optional or required depending on the implementation of
80
- # the specific handler, i.e. if a handler wants to use the 'caller' method, it
81
- # must have been supplied to the handle method.
82
- #
83
88
  # Example:
84
89
  #
85
90
  # class MyHandler
@@ -88,7 +93,7 @@ module Lev
88
93
  # def authorized?
89
94
  # # return true iff exec is allowed to be called, e.g. might
90
95
  # # check the caller against the params
91
- # def exec
96
+ # def handle
92
97
  # # do the work, add errors to errors object and results to the results hash as needed
93
98
  # end
94
99
  # end
@@ -98,21 +103,14 @@ module Lev
98
103
  def self.included(base)
99
104
  base.extend(ClassMethods)
100
105
  base.class_eval do
101
- include Lev::RoutineNesting
106
+ include Lev::Routine
102
107
  end
103
108
  end
104
109
 
105
- def handle(options={})
106
- in_transaction do
107
- handle_guts(options)
108
- end
109
- end
110
-
111
- alias_method :call, :handle
112
-
113
110
  module ClassMethods
111
+
114
112
  def handle(options={})
115
- new.handle(options)
113
+ call(options)
116
114
  end
117
115
 
118
116
  def paramify(group, options={}, &block)
@@ -161,53 +159,57 @@ module Lev
161
159
  end
162
160
  end
163
161
 
164
- def transfer_errors_from(source, param_group)
165
- ErrorTransferer.transfer(source, self, param_group)
166
- end
167
-
168
- attr_accessor :errors
169
-
170
162
  protected
171
163
 
172
164
  attr_accessor :params
173
165
  attr_accessor :request
174
166
  attr_accessor :options
175
167
  attr_accessor :caller
176
- attr_accessor :results
177
168
 
178
- def handle_guts(options)
169
+ # Provided for development / debugging purposes -- gives a way to pass
170
+ # information in a raised security transgression when authorized? is false
171
+ attr_accessor :auth_error_details
172
+
173
+ # This is a method required by Lev::Routine. It enforces the steps common
174
+ # to all handlers.
175
+ def exec(options)
179
176
  self.params = options.delete(:params)
180
177
  self.request = options.delete(:request)
181
178
  self.caller = options.delete(:caller)
182
179
  self.options = options
183
- self.errors = Errors.new
184
- self.results = {}
185
180
 
186
181
  setup
187
- raise Lev.configuration.security_transgression_error unless authorized?
182
+ raise Lev.configuration.security_transgression_error, auth_error_details unless authorized?
188
183
  validate_paramified_params
189
- exec unless errors?
190
-
191
- [self.results, self.errors]
184
+ handle unless errors?
192
185
  end
193
186
 
187
+ # Default setup implementation -- a no-op
194
188
  def setup; end
195
189
 
190
+ # Default authorized? implementation. It returns true so that every
191
+ # handler realization has to make a conscious decision about who is authorized
192
+ # to call the handler. To help the common error of forgetting to override this
193
+ # method in a handler instance, we provide an error message when this default
194
+ # implementation is called.
196
195
  def authorized?
197
- false # default for safety, forces implementation in the handler
196
+ self.auth_error_details =
197
+ "Access to handlers is prevented by default. You need to override the " +
198
+ "'authorized?' in this handler to explicitly grant access."
199
+ false
198
200
  end
199
201
 
202
+
203
+
204
+ # Helper method to validate paramified params and to transfer any errors
205
+ # into the handler.
200
206
  def validate_paramified_params
201
207
  self.class.paramify_methods.each do |method|
202
208
  params = send(method)
203
- transfer_errors_from(params, params.group) if !params.valid?
209
+ transfer_errors_from(params, TermsMapper.scope(params.group)) if !params.valid?
204
210
  end
205
211
  end
206
212
 
207
- def errors?
208
- errors.any?
209
- end
210
-
211
213
  end
212
214
 
213
215
  end