text_transform
libraryIn this tutorial, we will walk through the steps of building a simple Rails app
called Flipper. It is essentially a simplified version of the Textify
demo that allows you to flip a piece of text upside down:
To make things interesting, we will be implementing the core functionality in Rust using Helix. At the end of the tutorial we will also cover deploying this app to Heroku.
Before we begin, we need to install Rust using the rustup installer:
$ curl https://sh.rustup.rs -sSf | sh
If you already have rustup installed, run this command to ensure you have the latest version of Rust:
$ rustup update
First, weâll need a new rails project. (If you are integrating Helix into an existing Rails project, you may skip this step.)
$ rails new --skip-active-record flipper
Since we are not going to need a database for this simple app, we can simplify
things by removing Active Record with the --skip-active-record
flag. To make
sure things are working properly, letâs make sure we can run the Rails server:
$ bin/rails server
If you visit http://localhost:3000 in your browser, you should be greeted by a page similar to this:
Once you have verified that everything is working, exit the Rails server by pressing Ctrl+C.
To start using Helix, add the helix-rails
gem to your Gemfile:
source 'https://rubygems.org'
# ...
gem 'helix-rails', '~> 0.5.0'
Be sure to run bundle install
afterwards.
Now that we have Helix installed, we can generate a Helix crate:
$ rails generate helix:crate text_transform
This will generate a Helix crate called text_transform, located in
crates/text_transform
. A Helix crate is simultaneously a Rust crate
and a Ruby gem. This encourages
you to structure your Rust code as a self-contained library separate from your
application code.
Looking at the boilerplate generated by Helix, we can see that it generated a Rust file for us:
#[macro_use]
extern crate helix;
ruby! {
class TextTransform {
def hello() {
println!("Hello from text_transform!");
}
}
}
This defines a simple Ruby class TextTransform
with a single class method. To
test this out, we can run rake irb
from crates/text_transform
, which
automatically compiles the Rust code and puts us into an irb session:
$ rake irb
>> TextTransform.hello
Hello from text_transform!
=> nil
As you can see, we were able to invoke the method (implemented in Rust) from Ruby. Pretty cool!
text_transform
libraryNow that we have the boilerplate down, letâs implement the text_transform
library.
Letâs begin by writing some tests using RSpec.
First we will add rspec
as development dependency:
Gem::Specification.new do |s|
s.name = 'text_transform'
# ...
s.add_development_dependency 'rspec', '~> 3.6'
end
Be sure to run bundle install
afterwards.
Then we will add our test:
require "text_transform"
describe "TextTransform" do
it "can flip text" do
expect(TextTransform.flip("Hello Aaron (@tenderlove)!")).to eq("¡(ÇÊolɹÇpuÇÊ@) uoɹÉâ ollÇH")
end
it "can flip the text back" do
expect(TextTransform.flip("¡(ÇÊolɹÇpuÇÊ@) uoɹÉâ ollÇH")).to eq("Hello Aaron (@tenderlove)!")
end
end
As expected, the tests will fail as we have not implemented the flip
method:
$ rspec
FF
Failures:
1) TextTransform can flip text
Failure/Error: expect(TextTransform.flip("Hello Aaron (@tenderlove)!")).to eq("¡(ÇÊolɹÇpuÇÊ@) uoɹÉâ ollÇH")
NoMethodError:
undefined method `flip' for TextTransform:Class
# ./spec/text_transform_spec.rb:5:in `block (2 levels) in <top (required)>'
2) TextTransform can flip the text back
Failure/Error: expect(TextTransform.flip("¡(ÇÊolɹÇpuÇÊ@) uoɹÉâ ollÇH")).to eq("Hello Aaron (@tenderlove)!")
NoMethodError:
undefined method `flip' for TextTransform:Class
# ./spec/text_transform_spec.rb:9:in `block (2 levels) in <top (required)>'
Finished in 0.00068 seconds (files took 0.13472 seconds to load)
2 examples, 2 failures
Now that we have some failing tests, letâs implement the missing method (in Rust!):
#[macro_use]
extern crate helix;
ruby! {
class TextTransform {
def flip(text: String) -> String {
text.chars().rev().map(|char| {
match char {
'!' => '¡', '"' => 'â', '&' => 'â
', '\'' => 'â', '(' => ')', ')' => '(', ',' => 'â', '.' => 'Ë',
'1' => 'Æ', '2' => 'á
', '3' => 'Æ', '4' => 'ã£', '5' => 'Ï', '6' => '9', '7' => 'ã¥',
'9' => '6', ';' => 'Ø', '<' => '>', '>' => '<', '?' => '¿',
'A' => 'â', 'B' => 'ð', 'C' => 'â', 'D' => 'â', 'E' => 'Æ', 'F' => 'â²', 'G' => 'â
',
'J' => 'Å¿', 'K' => 'Ê', 'L' => 'â
', 'M' => 'W',
'P' => 'Ô', 'Q' => 'Î', 'R' => 'á´', 'T' => 'â¥', 'U' => 'â©', 'V' => 'á´§', 'W' => 'M',
'Y' => 'â
', '[' => ']', ']' => '[', '^' => 'v', '_' => 'â¾',
'`' => ',', 'a' => 'É', 'b' => 'q', 'c' => 'É', 'd' => 'p', 'e' => 'Ç', 'f' => 'É', 'g' => 'Æ',
'h' => 'É¥', 'i' => 'á´', 'j' => 'ɾ', 'k' => 'Ê', 'm' => 'ɯ', 'n' => 'u',
'p' => 'd', 'q' => 'b', 'r' => 'ɹ', 't' => 'Ê', 'u' => 'n', 'v' => 'Ê', 'w' => 'Ê',
'y' => 'Ê', '{' => '}', '}' => '{',
// Flip back
'¡' => '!', 'â' => '"', 'â
' => '&', 'â' => '\'', 'â' => ',', 'Ë' => '.',
'Æ' => '1', 'á
' => '2', 'Æ' => '3', 'ã£' => '4', 'Ï' => '5', 'ã¥' => '7',
'Ø' => ';', '¿' => '?',
'â' => 'A', 'ð' => 'B', 'â' => 'C', 'â' => 'D', 'Æ' => 'E', 'â²' => 'F', 'â
' => 'G',
'Å¿' => 'J', 'â
' => 'L',
'Ô' => 'P', 'Î' => 'Q', 'á´' => 'R', 'â¥' => 'T', 'â©' => 'U', 'á´§' => 'V',
'â
' => 'Y', 'â¾' => '_',
'É' => 'a', 'É' => 'c', 'Ç' => 'e', 'É' => 'f', 'Æ' => 'g',
'É¥' => 'h', 'á´' => 'i', 'ɾ' => 'j', 'Ê' => 'k', 'ɯ' => 'm',
'ɹ' => 'r', 'Ê' => 't', 'Ê' => 'v', 'Ê' => 'w','Ê' => 'y',
_ => char,
}
}).collect()
}
}
}
The flip
method takes a string as input, splits it into characters, maps each
character into its âupside down lookalikeâ and joins them back up into a new
string.
If you look at the code, youâll notice that weâre using a lot of high-level features here such as iterators and blocks. Now this might sound suboptimal, but the Rust compiler will be able to see through all of that and generate highly-optimized machine code that could even outperform your carefully hand-written loop.
Now that we have implemented the method, letâs run the tests again:
$ rspec
FF
Failures:
1) TextTransform can flip text
Failure/Error: expect(TextTransform.flip("Hello Aaron (@tenderlove)!")).to eq("¡(ÇÊolɹÇpuÇÊ@) uoɹÉâ ollÇH")
NoMethodError:
undefined method `flip' for TextTransform:Class
# ./spec/text_transform_spec.rb:5:in `block (2 levels) in <top (required)>'
2) TextTransform can flip the text back
Failure/Error: expect(TextTransform.flip("¡(ÇÊolɹÇpuÇÊ@) uoɹÉâ ollÇH")).to eq("Hello Aaron (@tenderlove)!")
NoMethodError:
undefined method `flip' for TextTransform:Class
# ./spec/text_transform_spec.rb:9:in `block (2 levels) in <top (required)>'
Finished in 0.00068 seconds (files took 0.13472 seconds to load)
2 examples, 2 failures
Hmm, it is not seeing the flip
method we just implemented. This is because
since Rust is a compiled-language, we would have to re-compile our code after
making any changes:
$ rake build
cargo rustc --release -- -C link-args=-Wl,-undefined,dynamic_lookup
Compiling text_transform v0.1.0 (file:///private/tmp/flipper/crates/text_transform)
Finished release [optimized] target(s) in 0.95 secs
Now if we run the tests again, everything will work as expected:
$ rspec
..
Finished in 0.00348 seconds (files took 0.12317 seconds to load)
2 examples, 0 failures
To avoid needing to manually recompile, we can wrap this in a rake task and
make rake build
its dependency:
require 'bundler/setup'
require 'rspec/core/rake_task'
import 'lib/tasks/helix_runtime.rake'
RSpec::Core::RakeTask.new(:spec) do |t|
t.verbose = false
end
task :spec => :build
task :default => :spec
The trick is to make rake build
a dependency of your spec task. That way,
running rake spec
will always ensure the Rust code is built (and up-to-date)
before running your tests, just like the built-in rake irb
task.
To show you that workflow, letâs try to add a new feature.
require "text_transform"
describe "TextTransform" do
# it "can flip text" ...
# it "can flip the text back" ...
it "can flip table" do
expect(TextTransform.flip("â¬ââ⬠ã( ã-ãã)")).to eq("(â¯Â°â¡Â°ï¼â¯ï¸µ â»ââ»")
end
it "can flip the table back" do
expect(TextTransform.flip("(â¯Â°â¡Â°ï¼â¯ï¸µ â»ââ»")).to eq("â¬ââ⬠ã( ã-ãã)")
end
end
As you can see, this is a pretty simple feature: if you give a table, itâll flip it; if you give it a flipped table, itâll flip it back.
So now we can try running our test again with rake spec
, and theyâre failing
as expected.
$ rake spec
cargo rustc --release -- -C link-args=-Wl,-undefined,dynamic_lookup
Compiling text_transform v0.1.0 (file:///~/code/flipper/crates/text_transform)
Finished release [optimized] target(s) in 1.7 secs
..FF
Failures:
1) TextTransform can flip table
Failure/Error: expect(TextTransform.flip("â¬ââ⬠ã( ã-ãã)")).to eq("(â¯Â°â¡Â°ï¼â¯ï¸µ â»ââ»")
expected: "(â¯Â°â¡Â°ï¼â¯ï¸µ â»ââ»"
got: "(ãã-ã )ã â¬âââ¬"
(compared using ==)
# ./spec/text_transform_spec.rb:13:in `block (2 levels) in <top (required)>'
2) TextTransform can flip the table back
Failure/Error: expect(TextTransform.flip("(â¯Â°â¡Â°ï¼â¯ï¸µ â»ââ»")).to eq("â¬ââ⬠ã( ã-ãã)")
expected: "â¬ââ⬠ã( ã-ãã)"
got: "â»â⻠︵â¯ï¼Â°â¡Â°â¯)"
(compared using ==)
# ./spec/text_transform_spec.rb:17:in `block (2 levels) in <top (required)>'
Finished in 0.02586 seconds (files took 0.14297 seconds to load)
4 examples, 2 failures
Failed examples:
rspec ./spec/text_transform_spec.rb:12 # TextTransform can flip table
rspec ./spec/text_transform_spec.rb:16 # TextTransform can flip the table back
With the tests in place, we can go ahead and implement our feature. This is going to be pretty straightforward; weâre just going to have a conditional at the top to check for the special cases.
#[macro_use]
extern crate helix;
ruby! {
class TextTransform {
def flip(text: String) -> String {
if text == "â¬ââ⬠ã( ã-ãã)" {
return "(â¯Â°â¡Â°ï¼â¯ï¸µ â»ââ»".to_string();
} else if text == "(â¯Â°â¡Â°ï¼â¯ï¸µ â»ââ»" {
return "â¬ââ⬠ã( ã-ãã)".to_string();
}
// ...
}
}
}
Going back to the terminal, you can see that by running ârake specâ, it automatically rebuilds our native extension. Therefore, everything Just Workedâ¢.
$ rake spec
cargo rustc --release -- -C link-args=-Wl,-undefined,dynamic_lookup
Compiling text_transform v0.1.0 (file:///~/code/flipper/crates/text_transform)
Finished release [optimized] target(s) in 1.9 secs
....
Finished in 0.00363 seconds (files took 0.13734 seconds to load)
4 examples, 0 failures
Now that we have built a library to do the heavily-lifting for us, we wire everything up inside our Rails app.
First letâs create the route:
Rails.application.routes.draw do
resources :flips, path: '/', only: [:index, :create]
end
Then we will create the controller:
class FlipsController < ApplicationController
def index
@text = params[:text] || "Hello world!"
end
def create
@text = TextTransform.flip(params[:text])
render :index
end
end
And finally the template:
<h1>Flipper</h1>
<%= form_tag do %>
<%= text_field_tag :text, @text %>
<%= submit_tag "Flip!" %>
<% end %>
After starting the Rails server with the bin/rails server
command, you should
have a working Flipper app waiting for you at http://localhost:3000:
As you can see, with pretty minimal effort, we were able to create a Ruby native extension written in Rust using Helix, and integrate it into our Rails app.
Finally, we will deploy our Flipper app to Heroku.
First, you will need to create a Heroku account and install the Heroku CLI tools.
Then, we will need to create a Heroku app:
$ heroku create
Since Flipper is both a Ruby and a Rust app, we will need to set up the buildpacks manually:
$ heroku buildpacks:add https://github.com/hone/heroku-buildpack-rust
$ heroku buildpacks:add heroku/ruby
These commands add the Rust buildpack, which makes the Rust compiler available, as well as the regular Ruby buildpack that knows how to configure a Rails app.
Finally, we can deploy the app to Heroku:
$ git push heroku master
With that, you should have a working Flipper app â powered by Rust, running inside a Rails app â up and running on the Internet. Congratulations!