Skip to content
Browse files

initial structure and README

  • Loading branch information...
1 parent 6458a37 commit 3cf7dcb2659ff720f1114a9582aadcfd092a1b3e @kristianmandrup committed Aug 2, 2012
View
2 .gitignore
@@ -28,7 +28,7 @@ pkg
#
# For MacOS:
#
-#.DS_Store
+.DS_Store
# For TextMate
#*.tmproj
View
24 Gemfile
@@ -1,14 +1,16 @@
-source "http://rubygems.org"
-# Add dependencies required to use your gem here.
-# Example:
-# gem "activesupport", ">= 2.3.5"
+source :rubygems
+
+gem 'hashie', '~> 1.2'
+
+group :test do
+ gem 'imperator', :git => 'git://github.com/kristianmandrup/imperator.git'
+ gem 'focused_controller'
+end
-# Add dependencies to develop your gem here.
-# Include everything needed to run rake, tests, features, etc.
group :development do
- gem "rspec", "~> 2.8.0"
- gem "rdoc", "~> 3.12"
- gem "bundler", "~> 1.0.0"
- gem "jeweler", "~> 1.8.4"
- gem "rcov", ">= 0"
+ gem "rspec", ">= 2.8.0"
+ gem "rdoc", ">= 3.12"
+ gem "bundler", ">= 1.0.0"
+ gem "jeweler", ">= 1.8.4"
+ gem "simplecov",">= 0.5"
end
View
291 README.md
@@ -0,0 +1,291 @@
+# controll
+
+Some nice and nifty utilities to help you manage complex controller logic.
+
+## Background
+
+This gem contains logic extracted from my `oauth_assist` gem/engine which again was a response to this article [oauth pure tutorial](http://www.communityguides.eu/articles/16).
+
+## Justification
+
+As you can see, the following `#create` REST action is a nightmare of complexity and flow control leading to various different flash messages and redirect/render depending on various outcomes... there MUST be a better way!
+
+```ruby
+def create
+ # get the service parameter from the Rails router
+ params[:service] ? service_route = params[:service] : service_route = 'No service recognized (invalid callback)'
+
+ # get the full hash from omniauth
+ omniauth = request.env['omniauth.auth']
+
+ # continue only if hash and parameter exist
+ if omniauth and params[:service]
+
+ # map the returned hashes to our variables first - the hashes differs for every service
+
+ # create a new hash
+ @authhash = Hash.new
+
+ extract_auth_data!
+
+ if unknown_auth?
+ # debug to output the hash that has been returned when adding new services
+ render :text => omniauth.to_yaml
+ return
+ end
+
+ if @authhash[:uid] != '' and @authhash[:provider] != ''
+
+ auth = Service.find_by_provider_and_uid(@authhash[:provider], @authhash[:uid])
+
+ # if the user is currently signed in, he/she might want to add another account to signin
+ if user_signed_in?
+ if auth
+ flash[:notice] = 'Your account at ' + @authhash[:provider].capitalize + ' is already connected with this site.'
+ redirect_to services_path
+ else
+ current_user.services.create!(:provider => @authhash[:provider], :uid => @authhash[:uid], :uname => @authhash[:name], :uemail => @authhash[:email])
+ flash[:notice] = 'Your ' + @authhash[:provider].capitalize + ' account has been added for signing in at this site.'
+ redirect_to services_path
+ end
+ else
+ if auth
+ # signin existing user
+ # in the session his user id and the service id used for signing in is stored
+ session[:user_id] = auth.user.id
+ session[:service_id] = auth.id
+
+ flash[:notice] = 'Signed in successfully via ' + @authhash[:provider].capitalize + '.'
+ redirect_to root_url
+ else
+ # this is a new user; show signup; @authhash is available to the view and stored in the sesssion for creation of a new user
+ session[:authhash] = @authhash
+ render signup_services_path
+ end
+ end
+ else
+ flash[:error] = 'Error while authenticating via ' + service_route + '/' + @authhash[:provider].capitalize + '. The service returned invalid data for the user id.'
+ redirect_to signin_path
+ end
+ else
+ flash[:error] = 'Error while authenticating via ' + service_route.capitalize + '. The service did not return valid data.'
+ redirect_to signin_path
+ end
+end
+```
+
+Using the tools contained in `controll` the above logic can be encapsulated like this:
+
+```ruby
+def create
+ FlowHandler::CreateService.new(self).execute
+end
+```
+
+A `FlowHandler` can use Executors to encapsulate execution logic, which again can execute Commands that encapsulate business logic related to the user Session or models (data).
+
+The FlowHandler can manage Redirect, Render and Notifications in a standardized, much more Object Oriented fashion, which adheres to the Single Responsibility pattern.
+
+Controll has built in notification management which work both for flash messages (or other types of notifications) and as return codes for use in flow-control logic.
+
+Using the `controll` helpers, you can avoid the typical Rails anti-pattern of Thick controllers, without bloating your Models with unrelated model logic or pulling in various Helper modules which pollute the space of the Controller anyhow!
+
+## Usage
+
+In your controller include the `Controll::Messaging` helper module for notification handling.
+
+```ruby
+class ServicesController < ApplicationController
+ include Controll::Helper
+
+ before_filter :authenticate_user!, :except => accessible_actions
+ protect_from_forgery :except => :create
+end
+```
+
+Better yet, to make it available for all controllers, include it in your ApplicationController or whatever base controller you use.
+
+```ruby
+class ApplicationController
+ include Controll::Helper
+end
+```
+
+In your Controller you should define a MessageHandler to be used.
+
+```ruby
+class ServicesController < ApplicationController
+ include Controll::Helper
+
+ ...
+
+ protected
+
+ def msg_handler
+ @msg_handler ||= MessageHandler::Services.new self
+ end
+end
+```
+
+We will implement this MessageHandler later when we know the notifications we want to issue.
+
+For Controller actions that require complex flow control, use a FlowHandler:
+
+```ruby
+module FlowHandler
+ class CreateService < Control
+ protected
+
+ # use for more advanced render/redirect logic (fx with arguments)
+ def use_alternatives
+ end
+
+ def use_fallback
+ event == :no_auth ? do_render(:text => omniauth.to_yaml) : fallback_action
+ end
+
+ def action_handlers
+ [Redirect, Render]
+ end
+
+ def event
+ @event ||= Authenticator.new(controller).execute
+ end
+
+ class Render < FlowHandler::Render
+ def self.default_path
+ :signup_services_path
+ end
+
+ def self.events
+ [:signed_in_new_user]
+ end
+ end
+
+ class Redirect < FlowHandler::Render
+ def self.redirect_map
+ {
+ signin_path: [:error, :invalid, :auth_error]
+ signup_services_path: :signed_in_new_user
+ services_path: [:signed_in_connect, :signed_in_new_connect]
+ root_url: [:signed_in_user, :other]
+ }
+ end
+ end
+ end
+end
+```
+
+In the `Redirect` class we are setting up a mapping for various path, specifying which notifications/event should cause a redirect to that path.
+
+If you are rendering or redirecting to paths that take arguments, you can either extend the `#action` class method of your Redirect or Render class implementation or you can define a `#use_alternatives` method in your `FlowHandler` that contains this particular flow logic. You can also use the `#use_fallback` method for this purpose.
+
+## The Authenticator Executor
+
+The `Authenticator` inherits from `Executor::Base` which uses method_missing in order to delegate any missing method back to the initiator of the Executor, in this case the FlowHandler.
+
+```ruby
+module Executor
+ class Authenticator < Base
+ def execute
+ notify(:error) and return unless valid_params?
+ notify(:auth_invalid) and return unless auth_valid?
+
+ sign_in_command.perform
+ result
+ end
+
+ protected
+
+ def result
+ notifications.last || :success # return last notification as result
+ end
+
+ def valid_params?
+ omniauth and service and auth_hash
+ end
+
+ def sign_in_command
+ SignInCommand.new auth: auth, auth_hash: auth_hash, user_id: user_id, service_id: service_id, service_hash: service_hash, executor: self
+ end
+ end
+end
+```
+
+As you can see, we use the `#notify` command to signal notifications, the last one on the stack which acts as return value for the `#execute` method.
+
+To encapsulate more complex busines logic affecting the user Session or Model data, we execute an Imperator command (see `imperator` gem).
+
+## Message Handler
+
+Now we are finally ready to define the message handler for each notification event we have defined (this should ideally be done as you define each event!).
+
+```ruby
+module MessageHandler
+ class Services < Typed
+ class ErrorMsg < MessageHandler::Notify
+ type :error
+
+ def msg_map
+ {
+ must_sign_in: 'You need to sign in before accessing this page!',
+ auth_service_error: 'There was an error at the remote authentication service. You have not been signed in.',
+ cant_delete_current_account: 'You are currently signed in with this account!',
+ user_save_error: 'This is embarrassing! There was an error while creating your account from which we were not able to recover.',
+ }
+ end
+
+ # TODO: Use I18n string replacement technique for params
+ def auth_error!
+ 'Error while authenticating via ' + service_name + '. The service did not return valid data.'
+ end
+
+ def auth_invalid!
+ 'Error while authenticating via ' + full_route + '. The service returned invalid data for the user id.'
+ end
+ end
+
+ # TODO: Support I18n via naming convention t('services.notice.already_connected') and so on...
+ class NoticeMsg < MessageHandler::Notify
+ type :notice
+
+ def msg_map
+ {
+ signed_in: 'Your account has been created and you have been signed in!',
+ signed_out: 'You have been signed out!'
+ }
+ end
+
+ def already_connected
+ 'Your account at ' + provider_name + ' is already connected with this site.'
+ end
+
+ def account_added
+ 'Your ' + provider_name + ' account has been added for signing in at this site.'
+ end
+
+ def sign_in_success
+ 'Signed in successfully via ' + provider_name + '.'
+ end
+ end
+ end
+end
+```
+
+Note: The `MessageHandler` currently still needs some love for the above to work and it should also be made to work nicely with I18n.
+
+## Contributing to controll
+
+* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
+* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
+* Fork the project.
+* Start a feature/bugfix branch.
+* Commit and push until you are happy with your contribution.
+* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
+* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
+
+== Copyright
+
+Copyright (c) 2012 Kristian Mandrup. See LICENSE.txt for
+further details.
+
View
19 README.rdoc
@@ -1,19 +0,0 @@
-= controll
-
-Description goes here.
-
-== Contributing to controll
-
-* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
-* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
-* Fork the project.
-* Start a feature/bugfix branch.
-* Commit and push until you are happy with your contribution.
-* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
-* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
-
-== Copyright
-
-Copyright (c) 2012 Kristian Mandrup. See LICENSE.txt for
-further details.
-
View
5 lib/controll.rb
@@ -0,0 +1,5 @@
+require 'controll/executor/base'
+require 'controll/message_handler'
+require 'controll/flow_handler'
+require 'controll/helper'
+
View
13 lib/controll/executor/base.rb
@@ -0,0 +1,13 @@
+module Executor
+ class Base
+ attr_accessor :initiator
+
+ def initialize initiator
+ @initiator = initiator
+ end
+
+ def method_missing(meth, *args, &block)
+ initiator.send(meth, *args, &block)
+ end
+ end
+end
View
3 lib/controll/flow_handler.rb
@@ -0,0 +1,3 @@
+require 'controll/flow_handler/control'
+require 'controll/flow_handler/render'
+require 'controll/flow_handler/redirect'
View
17 lib/controll/flow_handler/base.rb
@@ -0,0 +1,17 @@
+module FlowHandler
+ class Redirect < Base
+ attr_reader :path
+
+ def initialize path
+ @path = path
+ end
+
+ def perform controller
+ raise NotImplementedError, 'You must implement the #perform method'
+ end
+
+ def self.action event
+ raise NotImplementedError, 'You must implement the #action class method'
+ end
+ end
+ end
View
60 lib/controll/flow_handler/control.rb
@@ -0,0 +1,60 @@
+module FlowHandler
+ class Control
+ attr_reader :controller
+
+ def initialize controller
+ @controller = controller
+ end
+
+ def execute
+ use_action_handlers
+ use_alternatives
+ use_fallback if !executed?
+ end
+
+ protected
+
+ # can be used to set up control logic that fall outside what can be done
+ # with the basic action_handlers but can not be considered fall-back.
+ def use_alternatives
+ end
+
+ def use_fallback
+ raise NotImplementedError, 'You must define a #use_fallback method'
+ end
+
+ def event
+ raise NotImplementedError, 'You must define a #event method that at least returns an event (Symbol). You can use an Executor for this.'
+ end
+
+ def action_handlers
+ []
+ end
+
+ def fallback_action
+ do_redirect root_url
+ end
+
+ def use_action_handlers
+ action_handlers.each do |action_handler|
+ execute_with action_handler.action(event)
+ end
+ end
+
+ def action_handlers
+ [Redirect, Render]
+ end
+
+ def execute_with action
+ return if !action
+ action.perform(controller)
+ executed!
+ end
+
+ def executed!
+ @executed = true
+ end
+
+ def executed?
+ @executed
+ end
View
23 lib/controll/flow_handler/redirect.rb
@@ -0,0 +1,23 @@
+module FlowHandler
+ class Redirect < Base
+ def initialize path, redirect_map = nil
+ super path
+ @redirect_map = redirect_map if redirect_map
+ end
+
+ def perform controller
+ controller.do_redirect controller.send(path)
+ end
+
+ def self.action event
+ redirect_map.each do |path, events|
+ return self.new(path) if events.include? event
+ end
+ nil
+ end
+
+ def self.redirect_map
+ {}
+ end
+ end
+ end
View
20 lib/controll/flow_handler/render.rb
@@ -0,0 +1,20 @@
+module FlowHandler
+ class Render < Base
+ def initialize path, events = []
+ super path
+ @events = events
+ end
+
+ def perform controller
+ controller.render controller.send(path)
+ end
+
+ def self.action event
+ self.new(default_path) if events.include? event
+ end
+
+ def self.default_path
+ raise NotImplementedError, 'You must set a default_path or override the #action class method'
+ end
+ end
+end
View
15 lib/controll/helper.rb
@@ -0,0 +1,15 @@
+module Controll
+ module Helper
+ include Messaging
+
+ def do_redirect path
+ notify!
+ redirect_to path
+ end
+
+ def do_render path
+ notify!
+ render path
+ end
+ end
+end
View
3 lib/controll/message_handler.rb
@@ -0,0 +1,3 @@
+require 'controll/message_handler/flash'
+require 'controll/message_handler/notify'
+require 'controll/message_handler/typed'
View
45 lib/controll/message_handler/flash.rb
@@ -0,0 +1,45 @@
+module MessageHandler
+ class Flash
+ attr_reader :controller
+
+ def initialize controller
+ @controller = controller
+ set_options!
+ end
+
+ protected
+
+ delegate :flash, to: :controller
+
+ def options
+ controller.msg_options
+ end
+
+ def signal msg, type = nil
+ type ||= signal_type
+ flash[type] = msg
+ end
+
+ def signal_type
+ self.class.signal_type
+ end
+
+ def set_options!
+ # create instance method for each msg option
+ case options
+ when Hash
+ options.each do |key, value|
+ self.class.define_method key do
+ value
+ end
+ end
+ when Array
+ options.each do |meth|
+ self.class.define_method meth do
+ send(meth)
+ end
+ end
+ end
+ end
+ end
+end
View
18 lib/controll/message_handler/notify.rb
@@ -0,0 +1,18 @@
+require 'controll/message_handler/flash'
+
+module MessageHandler
+ class Notify < Flash
+ def notify name, options = {}
+ self.options.merge! options
+ send(name)
+ end
+
+ class << self
+ attr_reader :signal_type
+
+ def type name
+ @signal_type = name
+ end
+ end
+ end
+end
View
15 lib/controll/message_handler/typed.rb
@@ -0,0 +1,15 @@
+require 'controll/message_handler/flash'
+
+module MessageHandler
+ class Typed < Flash
+ def error
+ raise "ErrorMsg class missing for this message handler" unless ErrorMsg
+ @error ||= ErrorMsg.new flash, options
+ end
+
+ def notice
+ raise "NoticeMsg class missing for this message handler" unless NoticeMsg
+ @notice ||= NoticeMsg.new flash, options
+ end
+ end
+end
View
20 lib/controll/messaging.rb
@@ -0,0 +1,20 @@
+module Controll
+ module Messaging
+ # msg stack
+ def notifications
+ @notifications ||= []
+ end
+
+ def notify name, *args
+ options = args.extract_options!
+ type = args.first || :notify
+ notifications << Hashie::Mash.new {name: name, type: type, options: options}
+ end
+
+ def notify!
+ notifications.each do |message|
+ msg_handler.send(message.type).notify message.name, message.options
+ end
+ end
+ end
+end

0 comments on commit 3cf7dcb

Please sign in to comment.
Something went wrong with that request. Please try again.