diff --git a/README.rdoc b/README.rdoc index 133c389..028ca22 100644 --- a/README.rdoc +++ b/README.rdoc @@ -16,7 +16,6 @@ FYI, {Mandrill}[http://mandrill.com/] is the transactional email service by the * Requires Rails >= 3.0.3 (including 3.1, 3.2 and 4). Food for thought (upcoming features maybe).. -* some generators may be handy to avoid the manual coding to wire up web hooks * I thought about implementing this as an engine, but the overhead did not seem appropriate. Maybe that view will change.. == The Mandrill::Rails Cookbook @@ -48,7 +47,29 @@ See the section below on 'Contributing to Mandrill::Rails' for more information. Say we have configured Mandrill to send requests to /inbox at our site (see the next recipes for how you do that). -Once we have Mandrill::Rails in our project, we just need to do two things. There's no generator to help you do this at the moment, but it is pretty simple: +Once we have Mandrill::Rails in our project, we just need to do two things. You can run a generator to do it for you, or you can configure things manually: + +==== Using the generator + +Run the generator and specify the name for your route and controller: + + rails generate mandrill inbox + +This will create a resource route and corresponding controller at /inbox. + +If you need a namespaced controller, specify it in the name: + + rails generate mandrill hooks/inbox + +This creates an `inbox` route that points to the `Hooks:InboxController` class. + +If you prefer pluralized names, that can be specified with a flag: + + rails generate mandrill inbox --pluralize_names + +This will create an `InboxesController` class and a resource route called `inboxes`. + +==== Manual configuration First, configure a resource route: diff --git a/lib/generators/templates/controller.rb b/lib/generators/templates/controller.rb new file mode 100644 index 0000000..01a025e --- /dev/null +++ b/lib/generators/templates/controller.rb @@ -0,0 +1,18 @@ +class <%= @controller_name %>Controller < ApplicationController + include Mandrill::Rails::WebHookProcessor + + # To completely ignore unhandled events (not even logging), uncomment this line + # ignore_unhandled_events! + + # If you want unhandled events to raise a hard exception, uncomment this line + # unhandled_events_raise_exceptions! + + # To enable authentication, uncomment this line and set your API key. + # It is recommended you pull your API keys from environment settings, + # or use some other means to avoid committing the API keys in your source code. + # authenticate_with_mandrill_keys! 'YOUR_MANDRILL_WEBHOOK_KEY' + + def handle_inbound(event_payload) + head(:ok) + end +end diff --git a/lib/generators/web_hook_generator.rb b/lib/generators/web_hook_generator.rb new file mode 100644 index 0000000..5b7ee2f --- /dev/null +++ b/lib/generators/web_hook_generator.rb @@ -0,0 +1,83 @@ +require 'rails/generators/named_base' + +module Mandrill + module Rails + module Generators + class WebHookGenerator < ::Rails::Generators::Base + namespace 'mandrill' + desc 'Generates a controller and routes for Mandrill web hooks.' + argument :name, type: :string + class_option :pluralize_names, aliases: '-p', type: :boolean, default: false, + desc: 'Pluralize names in route and controller' + class_option :routes, type: :boolean, default: true, + desc: 'Creates routes for web hooks' + class_option :controller, type: :boolean, default: true, + desc: 'Creates a controller for web hooks' + + source_root File.expand_path("../templates", __FILE__) + + def initialize(args, *options) + args[0] = args[0].dup if args[0].is_a?(String) && args[0].frozen? + super + assign_names!(self.name) + end + + def add_routes + return unless options.routes? + hook_route = "resource :#{resource_name}" + + controller = controller_path + + hook_route << %Q(, :controller => '#{controller}') + hook_route << %Q(, :only => [:show,:create]) + route hook_route + end + + def add_controller + return unless options.controller? + @controller_name = class_name + template 'controller.rb', controller_destination + end + + private + + attr_reader :file_name + + def assign_names!(name) + @class_path = name.include?('/') ? name.split('/') : name.split('::') + @class_path.map!(&:underscore) + @file_name = @class_path.pop + end + + def class_name + @class_name ||= (@class_path + [resource_name]).map!(&:camelize).join('::') + end + + def controller_destination + "app/controllers/#{controller_path}_controller.rb" + end + + def controller_path + @controller_path ||= if class_name.include?('::') + @class_path.collect {|dname| dname }.join + "/" + resource_name + else + resource_name + end + end + + def plural_name + @plural_name ||= singular_name.pluralize + end + + def resource_name + return singular_name unless options.pluralize_names? + plural_name + end + + def singular_name + file_name.downcase + end + end + end + end +end diff --git a/mandrill-rails.gemspec b/mandrill-rails.gemspec index 0c37b23..04b1f10 100644 --- a/mandrill-rails.gemspec +++ b/mandrill-rails.gemspec @@ -21,6 +21,7 @@ Gem::Specification.new do |spec| spec.add_development_dependency "bundler", "~> 1.7" spec.add_development_dependency "rake", "~> 10.0" spec.add_development_dependency "rspec", "~> 3.0" + spec.add_development_dependency "generator_spec", "~> 0.9" spec.add_development_dependency "guard-rspec", "~> 4.5" spec.add_development_dependency "rdoc" diff --git a/spec/generators/web_hook_generator_spec.rb b/spec/generators/web_hook_generator_spec.rb new file mode 100644 index 0000000..a9da919 --- /dev/null +++ b/spec/generators/web_hook_generator_spec.rb @@ -0,0 +1,246 @@ +require 'spec_helper' +require 'generator_spec' +require 'generators/web_hook_generator' + +describe Mandrill::Rails::Generators::WebHookGenerator, type: :generator do + destination File.expand_path("../../tmp", __FILE__) + + before do + prepare_destination + copy_routes + end + + describe 'route generation' do + context 'with no pluralized option' do + context 'with simple names' do + before { run_generator %w(inbox) } + + it 'creates a proper route' do + match = "resource :inbox, :controller => 'inbox', :only => [:show,:create]" + expect(destination_root).to have_structure { + directory 'config' do + file 'routes.rb' do + contains match + end + end + } + end + end + + context 'with namespaced names' do + before { run_generator %w(hooks/inbox) } + + it 'creates a proper route' do + match = "resource :inbox, :controller => 'hooks/inbox', :only => [:show,:create]" + expect(destination_root).to have_structure { + directory 'config' do + file 'routes.rb' do + contains match + end + end + } + end + end + + context 'with capitalized names' do + before { run_generator %w(Inbox) } + + it 'creates a proper route' do + match = "resource :inbox, :controller => 'inbox', :only => [:show,:create]" + expect(destination_root).to have_structure { + directory 'config' do + file 'routes.rb' do + contains match + end + end + } + end + end + end + + context 'with an explicit pluralized option' do + context 'with simple names' do + before { run_generator %w(inbox --pluralize_names) } + + it 'creates a proper route' do + match = "resource :inboxes, :controller => 'inboxes', :only => [:show,:create]" + expect(destination_root).to have_structure { + directory 'config' do + file 'routes.rb' do + contains match + end + end + } + end + end + + context 'with namespaced names' do + before { run_generator %w(hooks/inbox --pluralize_names) } + + it 'creates a proper route' do + match = "resource :inboxes, :controller => 'hooks/inboxes', :only => [:show,:create]" + expect(destination_root).to have_structure { + directory 'config' do + file 'routes.rb' do + contains match + end + end + } + end + end + + context 'with capitalized names' do + before { run_generator %w(hooks/Inbox --pluralize_names) } + + it 'creates a proper route' do + match = "resource :inboxes, :controller => 'hooks/inboxes', :only => [:show,:create]" + expect(destination_root).to have_structure { + directory 'config' do + file 'routes.rb' do + contains match + end + end + } + end + end + end + end + + describe 'controller generation' do + context 'with controller explicitly skipped' do + before { run_generator %w(inbox --skip-controller) } + + it 'does not create a controller file' do + expect(destination_root).to have_structure { + directory 'app' do + directory 'controllers' do + no_file 'inbox_controller.rb' + end + end + } + end + end + + context 'with no pluralized option' do + context 'with simple names' do + before { run_generator %w(inbox) } + + it 'creates a proper controller file' do + match = 'class InboxController < ApplicationController' + expect(destination_root).to have_structure { + directory 'app' do + directory 'controllers' do + file 'inbox_controller.rb' do + contains match + end + end + end + } + end + end + + context 'with namespaced names' do + before { run_generator %w(hooks/inbox) } + + it 'creates a proper controller file' do + match = 'class Hooks::InboxController < ApplicationController' + expect(destination_root).to have_structure { + directory 'app' do + directory 'controllers' do + directory 'hooks' do + file 'inbox_controller.rb' do + contains match + end + end + end + end + } + end + end + + context 'with capitalized names' do + before { run_generator %w(hooks/Inbox) } + + it 'creates a proper controller file' do + match = 'class Hooks::InboxController < ApplicationController' + expect(destination_root).to have_structure { + directory 'app' do + directory 'controllers' do + directory 'hooks' do + file 'inbox_controller.rb' do + contains match + end + end + end + end + } + end + end + end + + context 'with an explicit pluralized option' do + context 'with simple names' do + before { run_generator %w(inbox --pluralize_names) } + + it 'creates a proper controller file' do + match = 'class InboxesController < ApplicationController' + expect(destination_root).to have_structure { + directory 'app' do + directory 'controllers' do + file 'inboxes_controller.rb' do + contains match + end + end + end + } + end + end + + context 'with namespaced names' do + before { run_generator %w(hooks/inbox --pluralize_names) } + + it 'creates a proper controller file' do + match = 'class Hooks::InboxesController < ApplicationController' + expect(destination_root).to have_structure { + directory 'app' do + directory 'controllers' do + directory 'hooks' do + file 'inboxes_controller.rb' do + contains match + end + end + end + end + } + end + end + + context 'with capitalized names' do + before { run_generator %w(hooks/Inboxes --pluralized_names) } + + it 'creates a proper controller file' do + match = 'class Hooks::InboxesController < ApplicationController' + expect(destination_root).to have_structure { + directory 'app' do + directory 'controllers' do + directory 'hooks' do + file 'inboxes_controller.rb' do + contains match + end + end + end + end + } + end + end + end + end +end + +def copy_routes + routes = File.expand_path('../../rails_app/config/routes.rb', __FILE__) + destination = File.join(destination_root, 'config') + + FileUtils.mkdir_p(destination) + FileUtils.cp routes, destination +end diff --git a/spec/mandrill/web_hook/processor_spec.rb b/spec/mandrill/web_hook/processor_spec.rb index 1c8eb7f..85f3991 100644 --- a/spec/mandrill/web_hook/processor_spec.rb +++ b/spec/mandrill/web_hook/processor_spec.rb @@ -90,13 +90,6 @@ def handle_inbound; end end end context "and default missing handler behaviour" do - before do - class ::Rails - end - end - after do - Object.send(:remove_const, :Rails) - end it "logs an error" do processor.on_unhandled_mandrill_events = :log logger = double() diff --git a/spec/rails_app/config/routes.rb b/spec/rails_app/config/routes.rb new file mode 100644 index 0000000..681f51f --- /dev/null +++ b/spec/rails_app/config/routes.rb @@ -0,0 +1,4 @@ +Rails.application.routes.draw do + # Resources for testing + root to: "home#index", via: [:get, :post] +end