labnotes

Roda + Sequel

maybe its time to move past sinatra

tags
ruby
roda
sequel

Sinatra is getitng a bit old in the teeth, and there are a couple of other ruby api frameworks out there. Roda by Jeremy Evans seems like it is another interesting one – that and Hanami – and since he did sequel I thought I'd explore what it would take.

So lets get going!

1
2
  bundle init
  bundle add rerun roda puma
1
2
3
4
5
  # config.ru

  require_relative 'app'

  run App.app
1
2
3
4
5
6
7
8
9
  require 'roda'

  class App < Roda
    route do |r|
      r.root do
        "Hello there"
      end
    end
  end
1
  rerun rackup

Adding views

1
2
3
  mkdir -p public/css
  mkdir -p public/js
  mkdir -p views

Add views/layout.erb

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
  <!DOCTYPE html>
  <html lang="en">
    <head>
      <meta charset="utf-8">
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <title>Title</title>
      <link rel="stylesheet" href="/css/styles.css">
    </head>

    <body>
      <header>
      </header>
      <main>
        <%= yield %>
      </main>
      <footer>
      </footer>
    </body>
  </html>

Then public/css/styles.css

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
  ,*, *::before, *::after {
      box-sizing: border-box;
  }

  body {
      margin: 0;
      font-family: Avenir, Montserrat, Corbel, 'URW Gothic', source-sans-pro, sans-serif;
      font-weight: normal;
      }

  main {
      max-width: 1000px;
      margin: 0 auto;
  }

Then view/homepage.erb:

1
2
  <h1>This is a title</h1>
  <p>Welcome to my new website!</p>

Update app.rb

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
  require 'roda'

  class App < Roda
    plugin :static, ["/images", "/css", "/js"]
    plugin :render

    route do |r|
      r.root do
        view( :homepage )
      end
    end
  end

Adding sequel

This took me a bit to figure out. Sequel has a slightly different model than ActiveRecord does, and here's a nice introduction to it.

Both roda and sequel are very plugin heavy, which is sort of refreshing.

1
2
  bundle add sequel sqlite3 dotenv
  mkdir -p db/migrations

.env:

1
  DATABASE_URL=sqlite://./db/development.db
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
  # db/migrations/001_accounts.rb

  puts "Loading up migration"

  Sequel.migration do
    change do
      create_table(:accounts) do
        primary_key :id
        String :email, null: false
        Boolean :confirmed, default: false
        String :name
        String :login_hash
        DateTime :hash_valid_until, null: true
        DateTime :created_at, null: false
        DateTime :updated_at, null: false
      end
    end
  end

db.rb:

1
2
3
4
5
6
7
8
  require 'dotenv/load'
  require 'sequel'

  throw "DATABASE_URL is unset" if !ENV['DATABASE_URL'] || ENV['DATABASE_URL'] == ""

  DB = Sequel.connect( ENV['DATABASE_URL'] )

  Sequel::Model.plugin :timestamps, update_on_create: true

Rakefile:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
  require 'dotenv/load'

  namespace :db do
    desc "Migration the database"
    task :migrate do
      puts "Calling db:migrate"
      require_relative 'db.rb'
      version = nil
      
      Sequel.extension(:migration)
      
      # Perform migrations based on migration files in a specified directory.
      Sequel::Migrator.apply(DB, 'db/migrations')
      
      # Dump database schema after migration.
      #Rake::Task['db:dump'].invoke
    end
  end
1
  dotenvx run -- rake db:migrate
[dotenvx@1.6.4] injecting env (1) from .env
Calling db:migrate

Create the model

1
mkdir -p models

models/account.rb:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
  # frozen_string_literal: true
  require_relative '../db'
  require 'securerandom'

  class Account < Sequel::Model
    def generate_login_hash
      self.login_hash = SecureRandom.hex(16)
      self.hash_valid_until = Time.now + 3600 # 1 hour in seconds
      save
    end
  end

Building out account route

1
  mkdir -p routes

We are going to dynamically load all of the routes in the folder so we don't need to mess around with individually adding them. It's a nice pattern.

Update app.rb:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
  require 'roda'

  class App < Roda
    require_relative 'mailer.rb' # This comes later
    plugin :static, ["/images", "/css", "/js"]
    plugin :render
    plugin :hash_branches
    
    route do |r|
      r.root do
        view( :homepage )
      end
      
      Dir["routes/**/*.rb"].each do |route_file|
        require_relative route_file
      end
      
      r.hash_branches
    end
  end

routes/account.rb:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
  require_relative '../models/account.rb'

  class App
    hash_branch "account" do |r|
      r.is Integer do |id|
        #account = Account
        "account #{id}"
      end
      
      r.on 'hash' do |hash|
        r.get String do |hash|
          account = Account.where( login_hash: hash ).first
          if account
            "Hello #{account.email}"
          else
            "Not found"
          end
        end
      end
      
      r.is do
        p r.params
        
        r.get do
          "Post to create an account"
        end
                
        r.post do
          account = Account.new( name: r.params["name"], email: r.params["email"] )
          account.generate_login_hash
          
          if account.save
            "Hash is #{account.login_hash}"
          else
            puts "Missing something"
          end
        end
      end
    end
  end

Debug

1
  curl http://localhost:9292/account
Post to create an account

Creating a new account:

1
2
3
4
  curl -X POST \
       -F name=Will \
       -F email=wschenk@gmail.com \
       http://localhost:9292/account
Hash is 174b8bcc0d58ca52b0b2b2b36f326397

Calling it without a hash

1
  curl http://localhost:9292/account/hash

Calling it with the hash (the idea is that the account is verified)

1
  curl http://localhost:9292/account/hash/174b8bcc0d58ca52b0b2b2b36f326397
Hello wschenk@gmail.com

Email

1
2
  bundle add mail
  mkdir -p views/mail

mailer.rb:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  class App::Mailer < Roda
     plugin :render, views: 'views/mail', layout: nil
     plugin :mailer
     
     route do |r|
       r.on "account", Integer do |id|
         puts "Looking up #{id}"
         @account = Account[id]
         no_mail! unless @account
         
         puts @account
         
         from "tasks@example.com"
         to @account.email
         r.mail "welcome" do
           subject "Your login hash"
           render('welcome_html')
           # text_part render( 'welcome_text' )
           #  html_part render( 'welcome_html' )
         end
       end
     end     
   end

views/mail/welcome_text.erb:

1
  Go to http://localhost:9292/account/hash/<%= @account.login_hash %> to login

views/mail/welcome_html.erb:

1
2
3
4
  Go to
  <a href="http://localhost:9292/account/hash/<%= @account.login_hash %>"
  >http://localhost:9292/account/hash/<%= @account.login_hash %></a>
  to login

Preview

1
  bundle add roda-mailer_preview

routes/mail_view.rb:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
  class App
    plugin :mailer_preview
    
    hash_branch "mail_view" do |r|
      r.is "welcome" do
        mail = Mailer.mail("/account/1/welcome")
        require 'pp'
        pp mail
        preview(mail)
      end
      
      r.is true do
        mailers = ["/welcome"]
        preview_index(mailers)
      end
    end
  end

Previously

labnotes

Telegram with curl

so easy to send messages

tags
telegram
bot
curl

Next

howto

Building a blog using github issues

what else can we do with github actions

tags
github
static_sites