Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Taking the Red Pill : The Ruby toolbox to start you down the rabbit hole

Taking the Red Pill : The Ruby toolbox to start you down the rabbit hole

Building an application in a new language can be a daunting task but it is the best way to learn a new language. This session is a review of what tools, libraries, and methods that were used to build Morale, a software as a service application developed in Ruby on Rails. We will learn about why these tools were chosen and what alternatives exist. We will review what was used for the development, the testing, and the deployment of a production application with a web component, an api component, and a commerce component. This will serve as a shortcut to anyone looking to build their first Ruby on Rails application.

Jamie Wright

January 29, 2012
Tweet

More Decks by Jamie Wright

Other Decks in Programming

Transcript

  1. Taking the Red Pill the Ruby toolbox to start you

    down the rabbit hole Sunday, January 29, 12
  2. Soon lat you’re going to realize, just as I did,

    that th e’s a diff ence between knowing the path and walking the path “ Sunday, January 29, 12
  3. Web App API Why Ruby? Installation Gems Command Line Int

    face Testing Sunday, January 29, 12
  4. Web App API Why Ruby? Installation Gems Command Line Int

    face Testing Gem Auth ing Sunday, January 29, 12
  5. Web App API Why Ruby? Installation Gems Command Line Int

    face Testing Gem Auth ing Accepting Payments Sunday, January 29, 12
  6. Web App API Why Ruby? Installation Gems Command Line Int

    face Testing Gem Auth ing Accepting Payments Deployment Sunday, January 29, 12
  7. Object Oriented class Alcoholic < Addict def substance puts “vodka”

    end end class SpeedFreak < Addict def substance puts “crystal meth” end end Sunday, January 29, 12
  8. 5.years.ago 10.times { puts “Sexy” } users.each { |u| puts

    “Hello #{u.Name}” } Sunday, January 29, 12
  9. 5.years.ago 10.times { puts “Sexy” } users.each { |u| puts

    “Hello #{u.Name}” } var1 || “not null” Sunday, January 29, 12
  10. Open Objects class Numeric def plus(x) self.+(x) end end y

    = 5.plus 6 #y is now 11 Sunday, January 29, 12
  11. Open Objects class Numeric def +(x) self - x end

    end y = 5 + 6 #y is now -1 sucker!!!! Sunday, January 29, 12
  12. M ale Gems #Gemfile source ‘http://rubygems.org’ gem ‘rails’ gem ‘haml’

    gem ‘formtastic’ gem ‘rails_warden’ gem ‘paperclip’ gem ‘chronic’ gem ‘grape’ gem ‘aws-s3’ gem ‘cancan’ gem ‘chargify_api_ares’ gem ‘delayed_job’ gem ‘thor’ #Gemfile, con’t group :development, :test do gem ‘rspec-rails’ gem ‘factory_girl’ gem ‘capybara-webkit’ gem ‘database_cleaner’ gem ‘cucumber’ gem ‘pickle’ end Sunday, January 29, 12
  13. Bundl #update your Gemfile > bundle install #run a generator

    > bundle exec cucumber Sunday, January 29, 12
  14. ActiveRec d class Account < ActiveRecord::Base belongs_to :plan belongs_to :owner,

    :class_name => ‘User’, :foreign_key => :owner_id has_many :assignments end Sunday, January 29, 12
  15. ActiveRec d class Account < ActiveRecord::Base belongs_to :plan belongs_to :owner,

    :class_name => ‘User’, :foreign_key => :owner_id has_many :assignments end account = Account.find_by_site_address ‘bf’ account.assignments.where(:user_id => current_user.id) Sunday, January 29, 12
  16. Updating a Model class AddAllowApiAccessToAccount < ActiveRecord::Migration def self.up add_column

    :accounts, :allow_api_access, :boolean end def self.down remove_column :accounts, :allow_api_access end end Sunday, January 29, 12
  17. Updating a Model class AddAllowApiAccessToAccount < ActiveRecord::Migration def self.up add_column

    :accounts, :allow_api_access, :boolean end def self.down remove_column :accounts, :allow_api_access end end account = Account.find_by_site_address ‘bf’ account.allow_api_access = true Sunday, January 29, 12
  18. Qu y Int face account = Account.first account = Account.find

    10 accounts = Account.all Sunday, January 29, 12
  19. Qu y Int face account = Account.first account = Account.find

    10 accounts = Account.all accounts = Account.where(‘status = ?’, ‘past_due’) Sunday, January 29, 12
  20. Qu y Int face account = Account.first account = Account.find

    10 accounts = Account.all accounts = Account.where(‘status = ?’, ‘past_due’) accounts = Account.where(‘created_at >= :start_date AND created_at < :end_date’, {:start_date => 2.days.ago, :end_date => 1.day.ago}) Sunday, January 29, 12
  21. Haml .main-container-title %h1 My Account .main-container-inner = render :partial =>

    "shared/success" = render :partial => "shared/errors", :object => @account, :as => :model .sub-title Change settings for my account .content = semantic_form_for @account do |form| .form-area.span-9 = form.input :group_name, :label => "Account name", :input_html => { :class => "focusable" } .form-area.span-4 = form.input :site_address, :label => "Subdomain", :input_html => { :style => "width: 100px;", :onkeyup => "Accounts.copySiteAddress();" } .command-area = link_to "Cancel my account", cancel_account_path = form.commit_button Sunday, January 29, 12
  22. Haml .main-container-title %h1 My Account .main-container-inner = render :partial =>

    "shared/success" = render :partial => "shared/errors", :object => @account, :as => :model .sub-title Change settings for my account .content = semantic_form_for @account do |form| .form-area.span-9 = form.input :group_name, :label => "Account name", :input_html => { :class => "focusable" } .form-area.span-4 = form.input :site_address, :label => "Subdomain", :input_html => { :style => "width: 100px;", :onkeyup => "Accounts.copySiteAddress();" } .command-area = link_to "Cancel my account", cancel_account_path = form.commit_button <div class=”main-container-title”> <h1>My Account</h1> </div> Sunday, January 29, 12
  23. Haml .main-container-title %h1 My Account .main-container-inner = render :partial =>

    "shared/success" = render :partial => "shared/errors", :object => @account, :as => :model .sub-title Change settings for my account .content = semantic_form_for @account do |form| .form-area.span-9 = form.input :group_name, :label => "Account name", :input_html => { :class => "focusable" } .form-area.span-4 = form.input :site_address, :label => "Subdomain", :input_html => { :style => "width: 100px;", :onkeyup => "Accounts.copySiteAddress();" } .command-area = link_to "Cancel my account", cancel_account_path = form.commit_button <label for=”account_group_name”>Account name:</label> <input id=”account_group_name” name=”account.group_name” type=”type” class=”focusable” /> Sunday, January 29, 12
  24. <div class=’main-container-title’> <h1>My Account</h1> </div> <div class=’ main-container-inner’> <%= render

    :partial => "shared/success" %> <% = render :partial => "shared/errors", :object => @account, :as => :model %> <div class=’ sub-title’>Change settings for my account</div> <div class=’ content’> <% = semantic_form_for @account do |form| %> <div class=’form-area span-9’> <% = form.input :group_name, :label => "Account name", :input_html => { :class => "focusable" } %> </div> <div class=’form-area span-4’> <% = form.input :site_address, :label => "Subdomain", :input_html => { :style => "width: 100px;", :onkeyup => "Accounts.copySiteAddress();" } %> </div> </div> <div class=’command-area’> <% = link_to "Cancel my account", cancel_account_path %> <% = form.commit_button %> </div> <%= end %> </div> ERB Sunday, January 29, 12
  25. RSpec describe "String" do describe "#random" do it "should not

    include excluded characters" do excluded = ["0", "O", "o", "I", "i", "1"] 1.upto 1000 do |i| String.random(10, excluded).split(//).should_not include excluded end end end end Sunday, January 29, 12
  26. RSpec describe "String" do describe "#random" do it "should not

    include excluded characters" do excluded = ["0", "O", "o", "I", "i", "1"] 1.upto 1000 do |i| String.random(10, excluded).split(//).should_not include excluded end end end end String#random should not include excluded characters Finished in 0.007546 seconds 1 example, 0 failures Sunday, January 29, 12
  27. class RandomStringTest < Test::Unit::TestCase def should_not_include_excluded_characters excluded = ["0", "O",

    "o", "I", "i", "1"] 1.upto 1000 do |i| assert !String.random(10, excluded).split(//).include? excluded end end end Test Unit Sunday, January 29, 12
  28. class RandomStringTest < Test::Unit::TestCase def should_not_include_excluded_characters excluded = ["0", "O",

    "o", "I", "i", "1"] 1.upto 1000 do |i| assert !String.random(10, excluded).split(//).include? excluded end end end Test Unit Started . Finished in 0.007546 seconds 1 tests, 1 assertion, 0 failures, 0 errors Sunday, January 29, 12
  29. Cucumb Feature: Updating an account In order to update billing

    information or add/remove projects As an account owner I need to be able to update the account settings Scenario: Removing a project from an account that is not the last one Given I am authenticated with project: "Testing Project", role: "Account Owner" And a project exists with name: "Testing Project #2" And I am on the account edit page When I follow "Delete Testing Project #2" Then I should not see "Testing Project #2" within ".project-detail" And I should see "You removed the project Testing Project #2 from the Testing Account account." Sunday, January 29, 12
  30. Using the default profile... Feature: Updating an account In order

    to update billing information or add/remove projects As an account owner I need to be able to update the account settings Scenario: Removing a project from an account that is not the last one # features/account_page.feature:26 Given I am authenticated with project: "Testing Project", role: "Account Owner" # features/step_definitions/ account_steps.rb:21 And a project exists with name: "Testing Project #2" # features/step_definitions/pickle_steps.rb:4 And I am on the account edit page # features/step_definitions/web_steps.rb:19 When I follow "Delete Testing Project #2" # features/step_definitions/web_steps.rb:33 Then I should not see "Testing Project #2" within ".project-detail" # features/step_definitions/ web_steps.rb:128 And I should see "You removed the project Testing Project #2 from the Testing Account account." # features/ step_definitions/web_steps.rb:107 1 scenario (1 passed) 7 steps (7 passed) 0m3.881s Cucumb Sunday, January 29, 12
  31. Write Scenarios Steps are pending Write Step Definitions Go down

    to RSpec Write failing test Sunday, January 29, 12
  32. Write Scenarios Steps are pending Write Step Definitions Go down

    to RSpec Write failing test Make example pass Sunday, January 29, 12
  33. Write Scenarios Steps are pending Write Step Definitions Go down

    to RSpec Write failing test Make example pass Go back up to Cucumber Sunday, January 29, 12
  34. Write Scenarios Steps are pending Write Step Definitions Go down

    to RSpec Write failing test Make example pass Go back up to Cucumber Make steps pass Sunday, January 29, 12
  35. Write Scenarios Steps are pending Refactor! Write Step Definitions Go

    down to RSpec Write failing test Make example pass Go back up to Cucumber Make steps pass Sunday, January 29, 12
  36. Chargify require 'chargify_api_ares' class Subscription < PaymentProcessor include ActiveModel::Dirty include

    ActiveModel::Validations define_attribute_methods [:first_name, :last_name, :email, :card_number, :expiration_month, :expiration_year, :plan] validates_presence_of :first_name, :last_name, :email, :plan validates_format_of :email, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i validates_inclusion_of :expiration_month, :in => 1..12, :allow_nil => true, :message => "is not a valid month." validates_numericality_of :expiration_year, :only_integer => true, :allow_nil => true, :message => "is not a valid year." def save_customer! begin customer = Chargify::Customer.find_by_reference @reference rescue ActiveResource::ResourceNotFound customer = Chargify::Customer.new( :reference => @reference ) end customer.first_name = @first_name customer.last_name = @last_name customer.email = @email customer.save! end end Sunday, January 29, 12
  37. Grape require 'grape' module Morale class API < Grape::API error_format

    :json prefix 'api' version 'v1' resources :projects do http_basic do |u,p| !User.authenticate_by_api_key(u, p).nil? end helpers do def project @project ||= Project.find_by_id(params[:id]) end end # GET /api/v1/projects get do account.projects end .... Sunday, January 29, 12
  38. require 'grape' module Morale class API < Grape::API error_format :json

    prefix 'api' version 'v1' resources :projects do http_basic do |u,p| !User.authenticate_by_api_key(u, p).nil? end helpers do def project @project ||= Project.find_by_id(params[:id]) end end # GET /api/v1/projects get do account.projects end .... # GET /api/v1/projects/1 get '/:id' do error!("Project does not exist", 404) if project.nil? project end # PUT /api/v1/projects/1 put '/:id' do error!("Project does not exist", 404) if project.nil? project.update_attributes!(params[:project]) project end # POST /api/v1/projects post do account = validate_account project = Project.new(params[:project]) end # DELETE /api/v1/projects/1 delete '/:id' do error!("Project does not exist", 404) if project.nil? project.delete project.save! project end Sunday, January 29, 12
  39. API curl --user api-test:XfADs6yw0qshq2Svaf https://api- test.teammorale.com/api/v1/projects curl --user api-test:XfADs6yw0qshq2Svaf --data

    "{'project': {'name':'Test Project #2'}}" -X POST https://api-test.teammorale.com/ api/v1/projects Sunday, January 29, 12
  40. API curl --user api-test:XfADs6yw0qshq2Svaf https://api- test.teammorale.com/api/v1/projects curl --user api-test:XfADs6yw0qshq2Svaf --data

    "{'project': {'name':'Test Project #2'}}" -X POST https://api-test.teammorale.com/ api/v1/projects curl --user api-test:XfADs6yw0qshq2Svaf -X DELETE https://api- test.teammorale.com/api/v1/projects/1 Sunday, January 29, 12
  41. require 'thor' module Morale class Command < Thor desc "login",

    "Signs a user in using their email address and password. Stores the users API key locally to use for access later." def login Morale::Commands::Authorization.login end desc "accounts [EMAIL]", "Gets all the subdomains available for a given email address if provided, else it uses the current api key." method_options :change => false def accounts(email="") Morale::Commands::Account.list email, options.change end desc "accounts ID", "Changes the current account to the numeric identifier of the account specified." def account(id) Morale::Commands::Account.select id end desc "projects", "Lists the projects available to the user and the current account." method_options :change => false def projects Morale::Commands::Project.list options.change end Th Sunday, January 29, 12
  42. Feature: Running the projects command In order to view and

    select a project to work with As a command line user of Morale I should be able to run projects to view available projects and store my selected project information locally @interactive Scenario: Running projects should not require authorization if the account and api key are stored Given I am authenticated When I run `morale projects` interactively Then the output should contain: """ 1. Skunk Works 2. Spin Free Project """ @interactive Scenario: Running projects with the --change option should ask me to change the project Given I am authenticated When I run `morale projects --change` interactively Then the output should contain: """ Choose a project: """ uba Sunday, January 29, 12
  43. Gem::Specification.new do |s| s.specification_version = 2 if s.respond_to? :specification_version= s.required_rubygems_version

    = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.rubygems_version = '1.3.5' s.name = 'morale' s.version = '1.1.1' s.date = '2011-12-12' s.rubyforge_project = 'morale' s.summary = "Command line interface to create & manage tickets on Morale." s.description = "Client library and command-line tool to manage tickets and control your account on Morale." s.authors = ["Brilliant Fantastic"] s.email = '[email protected]' s.homepage = 'http://teammorale.com' s.require_paths = %w[lib] s.executables = ["morale"] s.rdoc_options = ["--charset=UTF-8"] s.extra_rdoc_files = %w[README.md] s.add_dependency('morale-client', "~> 0.0.1") s.add_dependency('hirb', "~> 0.5.0") s.add_dependency('json', "~> 1.4.6") s.add_dependency('thor', "~> 0.14.6") M ale GemSpec Sunday, January 29, 12
  44. desc "Create tag v#{version} and build and push #{gem_file} to

    Rubygems" task :release => :build do unless `git branch` =~ /^\* master$/ puts "You must be on the master branch to release!" exit! end sh "git commit --allow-empty -a -m 'Release #{version}'" sh "git tag v#{version}" sh "git push origin master" sh "git push origin v#{version}" sh "gem push pkg/#{name}-#{version}.gem" end desc "Build #{gem_file} into the pkg directory" task :build => :gemspec do sh "mkdir -p pkg" sh "gem build #{gemspec_file}" sh "mv #{gem_file} pkg" Rake GemSpec Sunday, January 29, 12
  45. I imagine that right now, you’re feeling a bit like

    Alice. Hmm? Tumbling down the rabbit hole? “ Sunday, January 29, 12
  46. Don’t y and bend the spoon. That’s impossible. Instead y

    and realize the uth… Th e is no spoon. Then you see that it is n the spoon that bends, it’s only yourself. “ “ Sunday, January 29, 12