How to create a component-based Rails application

Components can improve maintainability, reduce complexity, and accelerate testing in large Rails applications

How to create a component-based Rails application
Regenwolke0 (CC0)

The use of the components approach in Rails can improve maintainability, reduce complexity, and accelerate testing in large Rails applications.

In preparation for creating your first component-based Rails application (CBRA) app, you should ensure that your system is up to date with regard to Ruby and Rails. I suggest using the latest published versions of both Ruby and the Rails gems. The way I have my system set up, using the Ruby Version Manager (RVM), is something like the following:

Install rvm, bundler, and rails. 
Execute anywhere $ rvm get stable $ rvm install 2.4.2 $ gem install bundler -v '1.15.4' $ gem install rails -v '5.1.4'

The entire app inside a component

In this section, we will be creating a Rails application that contains no handwritten code directly in the app folder and instead has all code inside an engine mounted into the application.

The first step in creating a CBRA app is the same as creating any new Rails app: rails new!

Generate Sportsball app. Execute where you like to 
store your Rails projects $ rails new sportsball $ cd sportsball

I often refer to this Rails app as the main app or container app. However, we are not going to bother writing any code of the application into the newly created folder structure. Instead, we are going to write all of it inside a component. That component will be implemented through a Rails engine. In fact, let us force ourselves to stick to this and delete the app folder.

Delete the app folder. Execute in ./ $ rm -rf app

There is no obvious candidate as to where the components of our application should be added in the created folder structure. All folders have their specific purpose and our components don’t really fit in any of them. Consequently, I believe components should not go into the created folder structure and instead should find their place in a new directory under the root. I like to use components as the folder name.

Create the components folder. Execute in ./
$ mkdir components

To create our first component, we will use the rails plugin new command to create a Rails engine. For lack of a better name, we will call this first component app_component.

Initially, I will use app_component as a component name. This is despite what I said before about the naming of components. Read the following for a defense. But first, generate this component using the following command:

Generate the app_component gem. Execute in ./
$ rails plugin new components/app_component --full --mountable

The command line parameters --full and --mountable make the plugin generator create a gem that loads a Rails::Engine that isolates itself from the Rails application. It comes with a test harness application and blueprints for tests based on Test::Unit.

We can now cd into the component folder in ./components/app_ component and execute bundle. Depending on our version of bundler, there will be a warning (in older versions) or error about our gemspec not being valid:

app_component gem error during bundle. 
Execute in ./components/app_component $ bundle The latest bundler is1.16.0.pre.3, but you are currently
running 1.15.4. To update, run `gem install bundler --pre` You have one or more invalid gemspecs that need to be fixed. The gemspec at ./components/app_component/app_component.gemspec is not valid. Please fix this gemspec. The validation error was '"FIXME" or "TODO" is not
a description'

This warning is because there are a bunch of TODO entries in the gemspec that are picked up on by bundler and that we need to remove. If you fill out all the fields with TODO in the Gem::Specification block of the gemspec, this warning error will go away. However, email, homepage, description, and license are not required, and if you delete those you only need to fill in authors and summary to get rid of the warning.

Removing all TODOs from the generated gemspec leads to the following file, which will not throw an error:

./components/app_component/app_component.gemspec
 1 $:.push File.expand_path("../lib", FILE)
 2
 3 # Maintain your gems version:
 4 require "app_component/version"
 5
 6 # Describe your gem and declare its dependencies:
 7 Gem::Specification.new do |s|
 8  s.name      = "app_component"
 9  s.version   = AppComponent::VERSION
10  s.authors   = ["Stephan Hagemann"]
11  s.summary   = "Summary of AppComponent."
12  s.license   = "MIT"
13
14  s.files = Dir["{app,config,db,lib}/**/*",
                   "MIT-LICENSE", "Rakefile", "README.md"]
15
16  s.add_dependency"rails", "~> 5.1.0"
17
18  s.add_development_dependency"sqlite3"
19 end

If we now go back up into the root folder of Sportsball and call bundle there, you’ll notice that among the many gems that are being used is the new component app_component.

Seeing AppComponent being used in the app (some results omitted). 
Execute in ./ $ bundle The latest bundler is 1.16.0.pre.3, but you are currently
running 1.15.4. To update, run `gem install bundler --pre` Resolving dependencies... Using rake 12.2.1> ... Using rails 5.1.4 Using sass-rails 5.0.6 Using app_component 0.1.0 from source at `components/app_component` Bundle complete! 17 Gemfile dependencies,73 gems now installed. Use `bundle info [gemname]` to see where a bundled gem is installed.

This is because rails plugin new not only creates a new gem, it also automatically adds a reference to this gem into the Gemfile of an app in the context of which it was created. That is, because we executed rails plugin new from the root of the Sportsball app, we got also got a new entry in the Gemfile.

./Gemfile showing reference to AppComponent gem (some lines omitted)
1 source 'https://rubygems.org'
2
3 ...
4
5 gem 'tzinfo-data', platforms:[:mingw, :mswin, :x64_mingw,:jruby]
6 gem 'app_component', path:'components/app_component'

In earlier versions of Rails, this does not happen automatically. If you are in this situation, add the last line from the previous snippet to your Gemfile to get the same result when bundling the app. Notice that the gem reference uses the path option to specify where this gem can be found (namely, your local filesystem). Commonly, path is used only for gems under development, but as you will see, it works just fine for use in CBRA applications.

Back to AppComponent. While it is now hooked up in the Sportsball application, it does not actually do anything yet. Let us fix that next. We will create a landing page and route the root of the engine to it.

Generate a controller for the component. Execute in ./
$ cd components/app_component
$ rails g controller welcome index

By moving into the folder of the gem, we are using the engine’s Rails setup. Because of that and because we made this a mountable engine, the welcome controller is created inside the AppComponent namespace.

We change the component’s routes as follows to hook the new controller up to the root of the engine’s routes:

./components/app_component/config/routes.rb
1 AppComponent::Engine.routes.draw do
2   root to:"welcome#index"
3 end

Last, we make the main app aware of the routes of the engine by mounting them into the routes of the application. Here, we are mounting the engine to the root of the app as there are no other engines and the main app will never have any routes of its own.

./config/routes.rb
1 Rails.application.routes.draw do
2   mount AppComponent::Engine, at: "/"
3 end

That’s all. Let’s start up the server!

Start up the Rails server. Execute in ./
$ rails s
=> Booting Puma
=> Rails 5.1.4 application starting in development
=> Run `rails server -h` for more startup options
Puma starting in single mode...
* Version 3.10.0 (ruby 2.4.2-p198), codename: Russell's Teapot
* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://0.0.0.0:3000
Use Ctrl-C to stop

Now, when you open http://localhost:3000 with a browser, you should see (as in Figure 1) your new welcome controller’s index page. That wasn’t too bad, was it?

cbra fig01 Pearson Addison-Wesley

Figure 1. Your first CBRA-run web page

Having the component separated from the container application also allows us to draw a component diagram (see Figure 2). This diagram shows our component in the middle using its gem name. The surrounding box with the application name indicates the Rails application of which our code is a part. The arrow indicates that the container app is dependent directly on app_component (as we just saw, AppComponent was added directly to the Gemfile of Sportsball).

cbra fig02 Pearson Addison-Wesley

Figure 2. Your first component diagram

Generating this graph yourself at this stage of the app is a bit tricky. It is the way we are referencing the component in Gemfile currently. If you still want to do it, there are a couple of steps involved.

First, you need to install the cobradeps gem, a gem I wrote to allow for the generation of Rails component diagrams. This gem depends on graphviz, an open-source graph visualization software. You install these two applications as follows (assuming that you are on MacOS and are homebrew, which I recommend).

Install graphviz and cobradeps. Execute anywhere
$ brew install graphviz
$ gem install cobradeps

To alleviate the gem reference problem we mentioned, change the AppComponent line in your Gemfile to this:

Reference to app_component in ./Gemfile allowing
for graph generation 1 gem 'app_component', 2   path: 'components/app_component', 3   group: [:default, :direct]

The group: [:default, :direct] behaves normally with bundler and is used by cobradeps to determine that the gem is indeed a direct dependency (you will see later why this is necessary). Now you can generate the component graph by executing the following statement. The result will be output into ./component_diagram.png.

Generate a component graph for Sportsball. Execute in ./
$ cobradeps -g component_diagram .

That’s it! Your first CBRA app is ready to get serious. You can now continue all feature development inside of components/app_component.

Some aspects of what we glossed over in this section are covered in more depth in the appendixes. Refer to Appendix A for an in-depth look at the various kinds of engines that Rails features, and Appendix B for an introduction to engine routing and mounting.

ActiveRecord and handling migrations in components

Let’s add some actual functionality to the currently barebones application. The first feature we are going to focus on is for the app to be able to predict the outcome of future games based on past performances. To this end, we will add teams and games as models to AppComponent. We will create an admin interface for both teams and games, which will give us enough data to try our hand at predicting some games.

Remember to execute these commands in the context of AppComponent, that is, under ./components/app_component.

Scaffolding Team and Game. Execute in ./components/app_component
$ rails g scaffold team name:string
$ rails g scaffold game date:datetime \
                        location:string \
                        first_team_id:integer
                        second_team_id:integer \
                        winning_team:integer \
                        first_team_score:integer \
                        second_team_score:integer

The next step is to run rake db:migrate to create the appropriate tables in the database. Interestingly, this will work when called in ./components/app_component, but not in ./. It does not fail for the main app. It just doesn’t do anything.

Scaffolding Team and Game. Execute in ./components/app_component
$ rake db:migrate
== 20171029235211 CreateAppComponentTeams:migrating ================
-- create_table(:app_component_teams)
   -> 0.0005s
== 20171029235211 CreateAppComponentTeams: migrated (0.0006s) ======

== 20171029235221 CreateAppComponentGames:migrating ================
-- create_table(:app_component_games)
   -> 0.0007s
== 20171029235221 CreateAppComponentGames: migrated (0.0007s) ======
 
$ cd ../..
$ rake db:migrate
Running via Spring preloader in process 58196

Before this will work, we need to make the main app aware of the migrations provided by the engine.

Installing engine migrations with rake

The common solution to this is to install the engine’s migrations into the main app with rake app_component:install:migrations. This will copy all the migrations found in the engine’s db/migrate folder into that of the main app.

There are a few gems out there that use this functionality. For example, the gem Commontator does this. Most widely used gems, like ActiveAdmin and Devise, generate more complex migrations in the host app. They don’t actually come with migrations of their own, but instead use generators to create them based on user-specified configuration options.

If you were to run rake app_component:install:migrations in the Sportsball app, you would get the following contents in the engine and the main app:

Install engine migrations into the main app. Execute in ./
$ rake app_component:install:migrations
Running via Spring preloader in process 58464
Copied migration 20171030000159_create_app_component_teams.\
 app_component.rb from app_component
Copied migration 20171030000160_create_app_component_games.\
 app_component.rb from app_component

AppComponent engine migration
$ tree ./components/app_component/db/migrate
 components/app_component/db/migrate
 |—20171029235211_create_app_component_teams.rb
 |—20171029235221_create_app_component_games.rb

Sportsball application migrations $ tree ./db/migrate
./db/migrate
 |—20170507205125_create_app_component_teams.app_component.rb
 |—20170507205126_create_app_component_games.app_component.rb

While the original migrations in the engine are still present, the rake task has copied them into the main app. In doing so, it renamed them and changed their date to the current time. This ensures that the engine’s migrations are being run as the last ones in the application (“last” at the time they are installed into the app).

The redating of migrations to the current time is very important for engines that are intended to be distributed (like the ones mentioned previously), because it is unknown when the gem, and thus its migrations, are going to be added to an application. If the dates were not changed, there would be no control over where in the overall migration sequence they would fall. However, since the gem had to have been published before the development of the app (or the relevant portion of the app) was started, they are likely going to be run very early. That, in turn, would likely lead to an invalid overall migration state on any system that runs the app—even production: While newer migrations have run, older ones are missing.

Note that rake railties:install:migrations installs all new migrations from all engines in an application. If migrations are added after the installer has run, it will need to be run again to ensure all migrations are present.

Having to install migrations every time they are added to an engine is an extra step in comparison to what we are used to. And, as discussed previously, the reason it is needed in many situations (gems being developed independently from applications) does not hold true in our case. It turns out that we can change our engine to have the host Rails application automatically find and use its migrations.

Loading engine migrations in place

Instead of copying migrations from components into the main application, we can ensure that the main app can find them with a few lines of code added to the component’s engine.rb. This technique was first suggested by Ben Smith.

./components/app_component/lib/app_component/engine.rb –
Engine migrations configuration  1 moduleAppComponent  2  class Engine< ::Rails::Engine  3    isolate_namespace AppComponent  4>  5    initializer:append_migrations do |app|  6       unlessapp.root.to_s.match root.to_s+File::SEPARATOR  7         app.config.paths["db/migrate"].concat(                 config.paths["db/migrate"].expanded)  8       end  9     end 10   end 11 end

Now rake db:migrate will find and correctly run the engine’s migrations.

It is important not to use migration installation rake tasks in combination with this technique and to remove what may have been added while following the previous section. That would result in problems with migrations being loaded twice, which is similar to something we will see next.

Known issues

There are a few snags you can run into with this approach that should be avoided.

Chaining db:migrate in rake tasks fails to add

For a still unknown reason, rake db:drop db:create db:migrate works fine for a normal Rails app, but fails to add the migrations when engine migrations are loaded in place. The simplest way around this is to split this command in two by running rake db:drop db:create && rake db:migrate instead. This has performance implications, as the rake now has to load twice.

Ben Smith proposes a few different fixes for this issue, the most concise being to require the Rails environment to be loaded before the db:load_config by adding the following file db.rake:

./components/app_component/lib/tasks/db.rake
1 Rake::Task["db:load_config"].enhance [:environment]

This has the side effect of the environment (i.e., the Rails app’s contents) always being loaded before any database tasks are run. Not only does this have a performance implication, it also affects every environment. And while the pattern rake db:drop db:create db:migrate is common in development, it will never be called in the production environment. In my opinion, it is a bad trade-off to affect production in unknown ways to achieve small benefits in development.

In conclusion, I recommend working around the problem instead of using a solution that is not fully understood. This issue should be fixed by understanding and fixing the true cause in whatever it is that Rails does to run migrations. That would also make a nice pull request to Rails for the one who finds it!

Other rake tasks reported not working

It has been reported that other database-related tasks, like rake db:setup, rake db:rollback and rake db:migration:redo, stop working with this approach. I have never been able to confirm this and it certainly works in the Sportsball app as we have created it so far.

Problems with naming engines

In the code snippet that adds the engine’s migrations, you may have noticed the peculiar line unless app.root.to_s.match root.to_ s+File::SEPARATOR. It prevents a problem with running rake app_ component:db:migrate inside the engine itself, which throws the following error otherwise:

Engine migrations loaded twice through dummy app. Executed in ./components/app_ component (if the check is removed)
$ rake app_component:db:migrate
rake aborted!
ActiveRecord::DuplicateMigrationNameError:

Multiple migrations have the name CreateAppComponentTeams

The match is a heuristic to determine whether the engine is currently being loaded within its own dummy app. If that is the case, the initializer should not add the routes, as they are added automatically. The heuristic fails if the dummy app of the engine is outside of its own directory structure, which should never be the case.

A less powerful version of this heuristic evaluates app.root.to_s. match root.to_s, which fails if used with two engines where the name of the including engine starts with the name of the included engine and they are in the same directory. For example, if /components/users_ server depends on /components/users, the former matches the latter and will not load its migrations. The more robust version of the heuristic should be used to prevent this problem.

Conclusion

We have added models for Team and Game and we have ensured that migrations can be run in the main application. Do so, if you haven’t yet, and start the server.

Run migrations and start the server. Executed in ./
$ rake db:migrate
Running via Spring preloader in process 58740
== 20171029235211 CreateAppComponentTeams:migrating =================
-- create_table(:app_component_teams)
   -> 0.0010s
== 20171029235211 CreateAppComponentTeams: migrated (0.0010s) =======
 
== 20171029235221 CreateAppComponentGames:migrating =================
-- create_table(:app_component_games)
   -> 0.0007s
== 20171029235221 CreateAppComponentGames: migrated (0.0007s) =======
 
$ rails s
=> Booting Puma
=> Rails 5.1.4 application starting in development
=> Run `rails server -h` for more startup options
Puma starting in single mode...
* Version 3.10.0 (ruby 2.4.2-p198), codename: Russell's Teapot
* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://0.0.0.0:3000
Use Ctrl-C to stop

With this, you can reach the UI for teams and games by navigating to http://localhost:3000/teams and http://localhost:3000/games, respectively. There, you are greeted with the standard look of scaffolded admin pages, as shown for teams in Figures 3 and 4.

cbra fig03 Pearson Addison-Wesley

Figure 3. List of teams with two teams already added

cbra fig04 Pearson Addison-Wesley

Figure 4. Adding a new team

Handling dependencies in components

With Sportsball now having the ability to store teams and games, we can turn to the question of how to predict the outcome of games based on past performance. To this end, we would like to add a page that will allow us to pick two teams. Click a button labeled something like “Predict the winner!” and see the application’s prediction of who is more likely to win as a result.

Using path blocks for specifying CBRA dependencies

When we added our component, we used the following format for stating this dependency in the app’s Gemfile as follows:

Sample Gemfile reference using path option
1 gem 'app_component', path:'components/app_component'

There is another way of stating this dependency using a block syntax, like so:

Same Gemfile reference using path block
1 path "components" do 
2  gem "app_component" 
3 end

The first visible difference is that there will be less code to write when the list of dependencies grows. That is, of course, only if we put future components into the same components folder. Additional components are simply added to the block:

Gemfile reference with multiple gems in path block
1 path "components" do
2   gem "app_component"
3   gem "component_a"
4   gem "component_b"
5 end

There is another difference between the path option and the block syntax. As Enrico Teotti reports, the block syntax uses a feature in bundler that ensures that transitive dependencies of AppComponent are looked up in the stated path folder. That means that it is not necessary to state every transitive CBRA dependency explicitly in the Gemfile. Instead, only the direct dependencies need to be listed.

For example, imagine that in the previous Gemfile, component_a depends on component_c. Without path block syntax, we would need to add gem "component_c", path: "components/component_c" to our Gemfile. With path block syntax, we don’t have to. We get this for free since we already stated that the direct dependency component_ais listed.

Because of this, when using cobradeps to generate component diagrams, it is no longer necessary to specify a special group for direct dependencies; cobradeps simply assumes that all stated dependencies are direct dependencies.

Adding a regular gem: slim—different templating

Before we get to the part where we calculate a likely outcome, we need to add the new page. I find ERB unnecessarily verbose and avoid it when possible. Luckily, there are plenty of alternatives out there, and we can add the first dependency to our component that is not Rails.

I like slim because it greatly reduces the amount of code I have to write in comparison to ERB. Particularly, the number of chevrons (the < and > symbols so common in HTML) is greatly reduced, which I like a lot. Instead of adding the slim gem, we will add slim-rails, which in turn will require slim, but in addition adds Rails generators that can create views in slim syntax.

./components/app_component/app_component.gemspec Add slim dependency
1 s.add_dependency "slim-rails"

This line should be added to AppComponent’s gemspec file to require slim-rails. Running bundle in the main app’s root folder, we should see slim-rails and slim being installed. Take a note of the exact version of slim-rails that is installed. At the time of writing, it is 3.1.3.

To use our new gem, let us use the current welcome page as an example and translate it into slim. In fact, the current welcome page still contains that default auto-generated text. We will use the opportunity to give the page a bit more meaningful content. So, let’s delete ./components/app_component/app/views/app_component/welcome/index.html.erb and create an index.html.slim in the same folder instead. The new page links to the admin pages of Team and Game.

./components/app_component/app/views/app_component/welcome/index.html.slim
1 h1 Welcome to Sportsball!
2 p Predicting the outcome of matches since 2015.
3
4 = link_to "Manage Teams", teams_path
5 | &nbsp;|&nbsp;
6 = link_to "Manage Games", games_path

When we fire up the server, however, and try to load the new homepage of our app, instead of a page, we get the “Template is missing” error depicted in Figure 5.

cbra fig05 Pearson Addison-Wesley

Figure 5. The “Template missing” error after switching to slim

The reason for this is that, unlike Rails applications, which automatically require all the gems they are directly dependent upon, Rails engines do not. Check out Jonathan Rochkind’s blog post on the issue. We never require slim in our engine and it shows, because Rails reports only the template handlers that come standard: :handlers=>[:erb, :builder, :raw, :ruby], but not :slim as you would expect.

To fix the issue, we must explicitly require slim-rails in the AppComponent component, as follows. Note that I moved require "app_component/engine" into the scope of the AppComponent module. There is no programmatic need for that, but I like for gems to indicate this way which requires are local (i.e., within the gem) versus external (i.e., external gem dependencies).

./components/app_component/lib/app_component.rb Require slim
1 require "slim-rails"
2
3 module AppComponent
4   require "app_component/engine"
5 end

We restart Rails to make it pick up the newly required gem and when we reload the homepage, we get the desired outcome, shown in Figure 6.

cbra fig06 Pearson Addison-Wesley

Figure 6. New welcome page written in slim

Locking down gem versions

Let us take another closer look at the runtime dependencies now present in our AppComponent gemspec.

./components/app_component/app_component.gemspec Production dependencies
1s.add_dependency "rails","~> 5.1.4"
2s.add_dependency "slim-rails"

The Rails dependency was generated as ~> 5.1.4, allowing all versions of Rails 5.1.* (where * is 4 or greater).(For a full explanation of the possibilities of version restriction specifications, check out the Bundler page on gemfiles and the RubyGems Guide on patterns.) We added slim-rails without any version restrictions.

Commonly, when developing gems, authors strive to keep the range of acceptable versions of needed gems as broad as possible. This is to exclude the fewest number of developers who might be in different situations and on different update paths from using a gem. Only for incompatible differences, which would prevent the gem from working properly, would a restriction typically be added.

Contrary to this, in Rails applications, Gemfile.lock is added to source control to lock down the versions of all dependencies. This ensures that when code is run in different environments or by different people, it will behave the same.

So. We are building an app, but are using gems. Which strategy should we take? Should we have a loose or a tight version policy? Well, I lock down all runtime dependencies in components to exact versions, like the following:

./components/app_component/app_component.gemspec Production dependencies locked down
1 s.add_dependency "rails", "5.1.4"
2 s.add_dependency "slim-rails", "3.1.3"

The reason for the version lockdown has to do with the testing of the component and is based on a couple of assumptions. I assume that you write:

  • Automated tests for your code
  • Different kinds of tests, like unit, functional, integration, and feature
  • Tests at the lowest possible level

If these assumptions are true for you, you will try to verify all of the internals of the component in the component itself. That also means you will not be testing the internals outside of the component; that is, in the context of the completed Rails application. What would happen if, in this situation, the versions of dependencies of the component somehow drifted from the ones used in the Rails app? That would be an untested dependency in production: The functioning of the component would be verified against a version of its dependencies that are not used in production. The version lockdown enforces that all components bound together by the Rails app run with and are tested against the same version of a dependency. Testing components is the topic of the next section.

For this section, suffice it to say that there is merit to keeping the versions of dependencies in sync among all parts of the app. In the running app, only one version of every dependency is going to be loaded; we might as well try not to be surprised by how it works.

Adding the development version of a gem: Trueskill—a rating calculation library

We can now turn to the prediction of the outcome of future games. If we do not want to be in the business of figuring out how to do that, we better find a gem that will do such a calculation for us. Luckily, there is a lot of theory we could potentially draw from, such as ranking algorithms, rating algorithms, or Bayesian networks. I started my search for a fitting gem with the FIFA World Rankings page on Wikipedia, which, while not explaining how the official rankings are calculated, mentions an alternative, the Elo rating system. Elo was created for use in chess but is now used in many competitor-versus-competitor games. An improvement to Elo is the Glicko rating system, which in turn was extended by Microsoft to Trueskill, a system that works for multiplayer games. For all of these—Elo, Glicko, and Trueskill—we can find corresponding gems on Rubygems. For the following, we are going to work with the trueskill gem. Not only does the idea of assessing a team’s strength while taking into account the players’ strengths sound appealing, but the gem also poses a nice little problem: It is totally outdated. At the time of writing, the last version of the gem was published in 2011. However, code has been contributed to forks of the original project until late 2014.

The version of the code we would like to use for trueskill is commit e404f45af5 on the benjaminleesmith fork. (I know this fork works as intended because I previously used it for the app that ranked foosball players in the Boulder Pivotal Labs office: true-foos-skills.)

The problem is that we can only specify gems to depend on published versions of other gems. There is no way for us to set a restriction based on a commit SHA. For gems that are intended to be published, this makes sense: They should not depend on code that was not also published and distributed as a gem.

To work around this problem, we have to use the gem’s gemspec and its Gemfile at the same time.

./components/app_component/Gemfile
1 source "https://rubygems.org"
2
3 gemspec
4
5 gem "trueskill",
6      git: "https://github.com/benjaminleesmith/trueskill",
7      ref: "e404f45af5b3fb86982881ce064a9c764cc6a901"
 
./components/app_component/app_component.gemspec – Dependencies
1 s.add_dependency "rails","5.1.4"
2 s.add_dependency "slim-rails","3.1.3"
3 s.add_dependency "trueskill"

The Gemfile in a gem’s directory is used during development of the gem, just like the Gemfile in a Rails app. When bundle is called in this directory, it will install all the gem dependencies listed there. The special line gemspec tells bundler to look for a gemspec file in the current directory and add all dependencies specified there to the current bundle. In our case, the gemspec states that AppComponent has a runtime dependency on trueskill and the Gemfile restricts this to be from the specified git URL at the given SHA.

Bundle AppComponent with trueskill (some results omitted). 
Execute in ./components/app_component $ bundle The latest bundler is 1.16.0.pre.3, but you are currently running 1.15.4. To update, run `gem install bundler --pre` Fetching https://github.com/benjaminleesmith/trueskill Fetching gem metadata from https://rubygems.org/.......... Fetching version metadata from https://rubygems.org/.. Fetching dependency metadata from https://rubygems.org/. Resolving dependencies... ... Using trueskill 1.0.0 from https://github.com/benjaminleesmith/\    trueskill (at e404f45@e404f45) ... Bundle complete! 3 Gemfile dependencies,46 gems now installed. Use `bundle info [gemname]` to see where a bundled gem is installed.

When bundling the component, we see Git checking out the repository specified and using the correct SHA. However, bundling the main app will reveal that it does not take into account the restriction posed by the Gemfile. That is because the Gemfile of any gem is ignored by other gems or apps depending on it (again, due to fact that the common expectation is for a gem to be published).

To work around this, there is no other way than to ensure that the version of the dependency is enforced by the app itself. That leads to an exact duplicate of the trueskill line from AppComponent’s Gemfile in the main app’s Gemfile.

New lines in ./Gemfile
1 gem "trueskill",
2      git: "https://github.com/benjaminleesmith/trueskill",
3      ref: "e404f45af5b3fb86982881ce064a9c764cc6a901"

And just like with slim-rails, we need to explicitly require the trueskill gem in AppComponent to make sure it is loaded.

./components/app_component/lib/app_component.rb Requiring the trueskill
dependency
1 require"slim-rails"
2 require"saulabs/trueskill"
3
4 module AppComponent
5   require"app_component/engine"
6 end

Adding predictions to the app

With models, scaffolds for administration, and the rating calculation library in place, we can turn to implementing the first iteration of game prediction.

Let’s create a cursory sketch of how our models might interact to generate a prediction. A predictor object might get a collection of all the games it should consider. As we are using an external library, we don’t reallyknow what is going on. The best way we can describe it is that the predictor learns (about the teams or the games). Because of this, we will make learn the first method of the public interface of the class.

After the predictor has learned the strengths of teams it can, given two teams, predict the outcome of their next match. predict becomes the second method of the public interface.

./components/app_component/app/models/app_component/predictor.rb
 1 moduleAppComponent
 2   class Predictor
 3     def initialize(teams)
 4       @teams_lookup = teams.inject({}) do |memo, team|
 5        memo[team.id] = {
 6          team: team,
 7          rating:[Saulabs::TrueSkill::Rating.new(
 8              1500.0, 1000.0,1.0)]
 9        }
10        memo
11      end
12    end
13
14   def learn(games)
15     games.each do |game|
16       first_team_rating =
17         @teams_lookup[game.first_team_id][:rating]
18       second_team_rating =
19         @teams_lookup[game.second_team_id][:rating]
20     game_result = game.winning_team == 1 ?
21       [first_team_rating, second_team_rating] :
22       [second_team_rating, first_team_rating]
23     Saulabs::TrueSkill::FactorGraph.new(
24       game_result, [1, 2]).update_skills
25   end
26 end
27
28 def predict(first_team, second_team)
29   team1 = @teams_lookup[first_team.id][:team]
30   team2 = @teams_lookup[second_team.id][:team]
31   winner = higher_mean_team(first_team, second_team) ?
32     team1 : team2
33   AppComponent::Prediction.new(team1, team2, winner)
34 end
35
36 def higher_mean_team(first_team, second_team)
37   @teams_lookup[first_team.id][:rating].first.mean >
38     @teams_lookup[second_team.id][:rating].first.mean
39   end
40  end
41 end

To start, initialize creates a lookup hash from all the teams it is handed that allows the Predictor class to efficiently access teams and their ratings by a team’s ID.

Inside of learn, the predictor loops over all the games that were given. It looks up the ratings of the two teams playing each game. The teams’ ratings are passed into an object from trueskillcalledFactorGraph in the order “winner first, loser second” so that the update_skills method can update the ratings of both teams.

predict simply compares the mean rating values of the two teams and “predicts” that the stronger team will win. It returns a Prediction object, which we will look at next.

There is not much going on in the Prediction class. It is simply a data object that holds on to the teams participating in the prediction, as well as the winning team.

./components/app_component/app/models/app_component/prediction.rb
 1 moduleAppComponent
 2   class Prediction
 3     attr_reader :first_team, :second_team, :winner
 4
 5     def initialize(first_team, second_team, winner)
 6       @first_team = first_team
 7       @second_team= second_team
 8       @winner= winner
 9     end
10   end
11 end

The PredictionsController has two actions: new and create. The first, new, loads all teams so they are available for the selection of the game to be predicted. create creates a new Predictor and then calls learn and predict in sequence to generate a prediction.

./components/app_component/app/controllers/app_component/
predictions_controller.rb  1 require_dependency "app_component/application_controller"  2 module AppComponent  3   class PredictionsController< ApplicationController  4     def new  5       @teams= AppComponent::Team.all  6     end  7  8     def create  9       predictor= Predictor.new(AppComponent::Team.all) 10       predictor.learn(AppComponent::Game.all) 11       @prediction = predictor.predict( 12         AppComponent::Team.find(params["first_team"]["id"]), 13         AppComponent::Team.find(params["second_team"]["id"])) 14      end 15    end 16 end

For completeness, we list the two views of the prediction interface as well as a helper that is used to generate the prediction result that will be displayed as a result.

./components/app_component/app/views/app_component/predictions/
new.html.slim  1  2  3 = form_tag prediction_path, method: "post" do |f|  4   .field  5     = label_tag :first_team_id  6     = collection_select(:first_team,:id, @teams,:id, :name)  7  8   .field  9     = label_tag :second_team_id 10     = collection_select(:second_team, :id,@teams, :id,:name) 11   .actions = submit_tag"What is it going to be?", class: "button"   ./components/app_component/app/views/app_component/predictions/
create.html.slim  1 h1 Prediction  2  3 =prediction_text @prediction.first_team,
@prediction.second_team,
@prediction.winner  4  5 .actions  6   = link_to "Try again!", new_prediction_path, class: "button"   ./components/app_component/app/helpers/app_component/
predictions_ helper.rb  1 moduleAppComponent  2   modulePredictionsHelper  3     def prediction_text(team1, team2, winner)  4       "In the game between #{team1.name} and #{team2.name} " +  5        "the winner will be #{winner.name}"  6     end  7   end  8 end

Finally, we can add a link to the prediction to the homepage to complete this feature.

./components/app_component/app/views/app_component/welcome/index.html.slim
1 h1 Welcome to Sportsball!
2 p Predicting the outcome of matches since 2015.
3
4 = link_to "Manage Teams", teams_path
5 | &nbsp;|&nbsp;
6 = link_to "Manage Games", games_path
7 | &nbsp;|&nbsp;
8 = link_to "Predict an outcome!", new_prediction_path

With the changes from this section in place, we can navigate to http:// localhost:3000/ to see a new homepage (see Figure 7) from which you can navigate to our new prediction section. Figure 8 shows how you can request a new prediction. Finally, in Figure 9, you see the result of a successful prediction.

cbra fig07 Pearson Addison-Wesley

The Figure 7. Sportsball homepage with link to predictions

cbra fig08 Pearson Addison-Wesley

Figure 8. Requesting the prediction of a game

cbra fig09 Pearson Addison-Wesley

Figure 9. Showing the prediction result

Sportsball is now fully functional! (At least for the purposes of this chapter.)

Copyright © 2018 IDG Communications, Inc.