From 53bc3f57415c301eeb1ed0e5ab246646e8de35e4 Mon Sep 17 00:00:00 2001 From: Michael Bleigh Date: Mon, 26 Sep 2011 11:44:17 -0500 Subject: [PATCH] Small but powerful change: Strategy options now inherit from a default, are Mashes, and can be declaratively configured. --- lib/omniauth.rb | 6 +++ lib/omniauth/strategy.rb | 73 ++++++++++++++++++++++++++++++++-- spec/omniauth/strategy_spec.rb | 36 +++++++++++++++++ spec/omniauth_spec.rb | 6 +++ 4 files changed, 117 insertions(+), 4 deletions(-) diff --git a/lib/omniauth.rb b/lib/omniauth.rb index 3e8699a96..a7c78acd5 100644 --- a/lib/omniauth.rb +++ b/lib/omniauth.rb @@ -74,6 +74,12 @@ def add_mock(provider, mock={}) self.mock_auth[provider.to_sym] = mock end + # This is a convenience method to be used by strategy authors + # so that they can add special cases to the camelization utility + # method that allows OmniAuth::Builder to work. + # + # @param name [String] The underscored name, e.g. `oauth` + # @param camelized [String] The properly camelized name, e.g. 'OAuth' def add_camelization(name, camelized) self.camelizations[name.to_s] = camelized.to_s end diff --git a/lib/omniauth/strategy.rb b/lib/omniauth/strategy.rb index c32dcf893..53842b9bc 100644 --- a/lib/omniauth/strategy.rb +++ b/lib/omniauth/strategy.rb @@ -1,4 +1,5 @@ require 'omniauth' +require 'hashie/mash' module OmniAuth class NoSessionError < StandardError; end @@ -12,12 +13,58 @@ def self.included(base) base.class_eval do attr_reader :app, :name, :env, :options, :response end + base.extend ClassMethods end + module ClassMethods + # Returns an inherited set of default options set at the class-level + # for each strategy. + def default_options + return @default_options if @default_options + existing = superclass.respond_to?(:default_options) ? superclass.default_options : {} + @default_options = OmniAuth::Strategy::Options.new(existing) + end + + # This allows for more declarative subclassing of strategies by allowing + # default options to be set using a simple configure call. + # + # @param options [Hash] If supplied, these will be the default options (deep-merged into the superclass's default options). + # @yield [Options] The options Mash that allows you to set your defaults as you'd like. + # + # @example Using a yield to configure the default options. + # + # class MyStrategy + # include OmniAuth::Strategy + # + # configure do |c| + # c.foo = 'bar' + # end + # end + # + # @example Using a hash to configure the default options. + # + # class MyStrategy + # include OmniAuth::Strategy + # configure foo: 'bar' + # end + def configure(options = nil) + yield default_options and return unless options + default_options.deep_merge!(options) + end + end + + # Initializes the strategy by passing in the Rack endpoint, + # the unique URL segment name for this strategy, and any + # additional arguments. An `options` hash is automatically + # created from the last argument if it is a hash. + # + # @param app [Rack application] The application on which this middleware is applied. + # @param name [String] A unique URL segment to describe this particular strategy. For example, `'openid'`. + # @yield [Strategy] Yields itself for block-based configuration. def initialize(app, name, *args, &block) @app = app - @name = name.to_sym - @options = args.last.is_a?(Hash) ? args.pop : {} + @name = name.to_s + @options = self.class.default_options.deep_merge(args.last.is_a?(Hash) ? args.pop : {}) yield self if block_given? end @@ -26,10 +73,17 @@ def inspect "#<#{self.class.to_s}>" end + # Duplicates this instance and runs #call! on it. + # @param [Hash] The Rack environment. def call(env) dup.call!(env) end + # The logic for dispatching any additional actions that need + # to be taken. For instance, calling the request phase if + # the request path is recognized. + # + # @param env [Hash] The Rack environment. def call!(env) raise OmniAuth::NoSessionError.new("You must provide a session to use OmniAuth.") unless env['rack.session'] @@ -75,6 +129,8 @@ def callback_call callback_phase end + # Returns true if the environment recognizes either the + # request or callback path. def on_auth_path? on_request_path? || on_callback_path? end @@ -95,6 +151,9 @@ def options_request? request.request_method == 'OPTIONS' end + # This is called in lieu of the normal request process + # in the event that OmniAuth has been configured to be + # in test mode. def mock_call!(env) return mock_request_call if on_request_path? return mock_callback_call if on_callback_path? @@ -115,7 +174,7 @@ def mock_request_call def mock_callback_call setup_phase - mocked_auth = OmniAuth.mock_auth_for(name.to_sym) + mocked_auth = OmniAuth.mock_auth_for(name.to_s) if mocked_auth.is_a?(Symbol) fail!(mocked_auth) else @@ -126,6 +185,10 @@ def mock_callback_call end end + # The setup phase looks for the `:setup` option to exist and, + # if it is, will call either the Rack endpoint supplied to the + # `:setup` option or it will call out to the setup path of the + # underlying application. This will default to `/auth/:provider/setup`. def setup_phase if options[:setup].respond_to?(:call) options[:setup].call(env) @@ -183,7 +246,7 @@ def call_app!(env = @env) end def auth_hash - AuthHash.new(:provider => name.to_s) + AuthHash.new(:provider => name) end def full_host @@ -238,5 +301,7 @@ def fail!(message_key, exception = nil) OmniAuth.config.on_failure.call(self.env) end + + class Options < Hashie::Mash; end end end diff --git a/spec/omniauth/strategy_spec.rb b/spec/omniauth/strategy_spec.rb index 7137f6b7a..4895c23fb 100644 --- a/spec/omniauth/strategy_spec.rb +++ b/spec/omniauth/strategy_spec.rb @@ -29,6 +29,37 @@ def make_env(path = '/auth/test', props = {}) describe OmniAuth::Strategy do let(:app){ lambda{|env| [404, {}, ['Awesome']]}} + + describe '.default_options' do + it 'should be inherited from a parent class' do + superklass = Class.new + superklass.send :include, OmniAuth::Strategy + superklass.configure do |c| + c.foo = 'bar' + end + + klass = Class.new(superklass) + klass.default_options.foo.should == 'bar' + end + end + + describe '.configure' do + subject { klass = Class.new; klass.send :include, OmniAuth::Strategy; klass } + it 'should take a block and allow for default options setting' do + subject.configure do |c| + c.wakka = 'doo' + end + subject.default_options.should == {"wakka" => "doo"} + end + + it 'should take a hash and deep merge it' do + subject.configure :abc => {:def => 123} + subject.configure :abc => {:hgi => 456} + subject.default_options.should == {'abc' => {'def' => 123, 'hgi' => 456}} + end + end + + describe '#initialize' do context 'options extraction' do it 'should be the last argument if the last argument is a Hash' do @@ -38,6 +69,11 @@ def make_env(path = '/auth/test', props = {}) it 'should be a blank hash if none are provided' do ExampleStrategy.new(app, 'test').options.should == {} end + + it 'should be the default options if any are provided' do + ExampleStrategy.stub!(:default_options).and_return(OmniAuth::Strategy::Options.new(:abc => 123)) + ExampleStrategy.new(app, 'test').options.abc.should == 123 + end end end diff --git a/spec/omniauth_spec.rb b/spec/omniauth_spec.rb index 99e814bf9..7b9d65eff 100644 --- a/spec/omniauth_spec.rb +++ b/spec/omniauth_spec.rb @@ -11,6 +11,12 @@ end context 'configuration' do + describe '.defaults' do + it 'should be a hash of default configuration' do + OmniAuth::Configuration.defaults.should be_kind_of(Hash) + end + end + it 'should be callable from .configure' do OmniAuth.configure do |c| c.should be_kind_of(OmniAuth::Configuration)