The time has come for my Module 2 Sinatra Project. Dun dun dun!
While my previous case study on Lakers < ActiveRecord::Base solely focused on the Model from MVC (Model-View-Controller), this Sinatra capstone project will encapsulate the whole MVC paradigm. One of the major concepts in MVC is to assign separation of concerns in Models, Views and Controllers. Models encapsulate the logic behind our application. Views are the user's interface, and implementation may utilize various platform from HTML, CSS, forms to ERB (Embedded Ruby). Controllers are the mediator in between our Models and Views. Each entity has its own single responsibility.
What is Sinatra?
Sinatra is a Domain Specific Language, or DSL.
Sinatra is a light-weight alternative to Ruby on Rails application framework, and of course, named after the infamous musician Frank Sinatra. It allows the creation of a simple app development in Ruby with minimal effort. However, understanding Sinatra is a great catalyst to Rails' massive framework application.
Ideation
Flatiron School curriculum requires our cohort to choose a domain we care about and are familiar with. I was a gamer back in the days. I remember religiously playing PS1 on mostly RPGs (FF series, Chrono Cross, Parasite Eve, Xenogears, Suikoden.. I can go on.. and on.. and on...) as well as fighting games. I was glad we were then moving on to PS2 and XBox One. Eventually, technology allows us to exercise social distancing from our monitor screen and fully decommissioned this old-school wired controller. Ahhh.. so nostalgic!
After a few days contemplating a domain that I would love to explore more in depth, I have decided to adopt Street Fighter as my domain modeling.
Domain and MVP
In my Street Fighter domain modeling universe, the Minimum Viable Product (MVP) I was aiming for this capstone project would be to allow a user to select a character and perform character's fighting moves on multiple stage platforms.
The extended function I am aiming for would be to save a user's session when signing up or logging in, and user's ability to record multiple selections of characters with various fighting moves and stage platforms. This is my preliminary Entity Relationship Diagram (ERD), and overall model relationships.
user
has_many :characters
character
belongs_to :user
character
has_many :moves
character
has_many :stages
, through: :moves
stage
has_and_belongs_to_many :moves
stage
has_many :characters
, through: :moves
move
belongs_to :character
move
has_and_belongs_to_many :stages
Back End::ORMs and ActiveRecord Migration Unit
Major thanks to Ruby gem Corneal in scaffolding basic initial setup. It automatically populates my app folder with models, controllers and views. Upon instantiation with corneal new APP-NAME
in the terminal and bundle install
, I have my basic Sinatra file structure setup.
The Object Relational Mapping (ORM) concept in vanilla ActiveRecord model classes and associations are defined as follows:
class User < ActiveRecord::Base
has_secure_password
validates_presence_of :username, :email, :password
validates_uniqueness_of :username, :email
has_many :characters
end
class Character < ActiveRecord::Base
belongs_to :user
has_many :moves
has_many :stages, through: :moves
def slug
self.name.downcase.split.join("-")
end
def self.find_by_slug(slug)
self.all.find {|character| character.slug == slug}
end
end
class Stage < ActiveRecord::Base
validates_uniqueness_of :name
has_and_belongs_to_many :moves
has_many :characters, through: :moves
end
class Move < ActiveRecord::Base
validates_uniqueness_of :name
belongs_to :character
has_and_belongs_to_many :stages
end
The ActiveRecord
gem allows us to create a mapping between our model and database. Each migration is recorded by ActiveRecord in the database, eliminating manual execution and any duplication errors. Classes inheriting from ActiveRecord::Base
carry meta-programming methods to associate the models and their respective database tables. The AR macros of has_many
, belongs_to
and has_and_belongs_to_many
further define model associations.
When architecting a new application, it is a good practice to start bottom up; starting from setting up the database. Once rake db:migrate
triggered, schema.rb
file will be auto-generated and it displays the current state of database migration.
ActiveRecord::Schema.define(version: 2020_10_31_224114) do
create_table "users", force: :cascade do |t|
t.string "username", default: "username"
t.string "email", default: "email"
t.string "password_digest", default: "password"
end
create_table "characters", force: :cascade do |t|
t.string "name"
t.string "quote"
t.text "bio"
t.string "image"
t.string "video"
t.integer "user_id"
end
create_table "moves", force: :cascade do |t|
t.string "name"
t.integer "character_id"
end
create_table "moves_stages", force: :cascade do |t|
t.integer "stage_id"
t.integer "move_id"
end
create_table "stages", force: :cascade do |t|
t.string "name"
t.string "image"
end
end
The user_id
and character_id
are defined as foreign keys in relational database structure. It uses that primary key of another table in referring to a row of that table. user_id
in "characters"
table is equivalent to user.id
in "users"
table. character_id
in "moves"
table is equivalent to character.id
in "characters"
table.
Since a stage has_and_belongs_to_many
fighting moves, and a move has_and_belongs_to_many
stage platforms. The join table moves_stages
fulfills the many-to-many relationships. It has two columns, stage_id
and move_id
. One for each of the table primary key reference.
Database Searching Saga
At first, I thought it would not be difficult in finding an API or HTML to parse out and sanitize the data in order to feed my rudimentary database seed.rb
. Most of the gaming webpages are buggy and not consistent. I required a solid database for my CRUD study. After searching of little or no avail, I decided to hard-code my seed data from multiple sources. :silent-crying-mode
Front End::Controllers
Routes are part of the code application connecting HTTP to specific methods in my Controller Action. These specific URLs via HTTP mappings are defined as Routes. When the user makes a request, matching the route to the controller action, it triggers the code inside of the controller action block. As programmers, we should be able to define the specificity of our controllers, and navigate the corresponding users to our web application.
Thanks to Roy Fielding for Representational State Transfer (REST) routes. This pattern-oriented URLs allow conventions for developers as they work unanimously in complex settings. CRUD (Create, Read, Update and Delete) actions are semantically followed by corresponding HTTP verbs. My project RESTful routes are defined as follows:
Application Controller
Request | Route | CRUD action |
---|---|---|
GET | '/' | read |
The ApplicationController
is the mother of other ancillary controllers in handling all of the incoming requests. It has the application setups, routes and controller actions.
My web application requires UsersController
at its inception in managing users database. The challenge became more apparent when developing associative controllers, and creating dynamic routes :
. CharactersController
, MovesController
and StagesController
utilize more of these dynamic routes in passing data from users' inputs to database, and vice versa.
Users Controller
Request | Route | CRUD action |
---|---|---|
GET | '/signup' | create |
POST | '/signup' | create |
GET | '/login' | read |
POST | '/login' | read |
GET | '/index' | read |
GET | '/edit' | update |
PATCH | '/edit' | update |
GET | '/delete' | read |
DELETE | '/delete' | delete |
GET | '/logout' | read |
POST | '/logout' | delete |
Characters Controller
Request | Route | CRUD action |
---|---|---|
GET | '/characters' | create and read |
POST | '/characters' | create |
GET | '/characters/:slug' | read and update |
DELETE | '/characters/:slug' | delete |
GET | '/characters/:slug/:id' | create |
GET | '/characters/:slug/:id/battle' | read |
Stages Controller
Request | Route | CRUD action |
---|---|---|
GET | '/:slug/stages/new' | create |
POST | '/:slug/stages/new' | create |
GET | '/:slug/stages/edit' | update |
POST | '/:slug/stages/edit' | update |
Moves Controller
Request | Route | CRUD action |
---|---|---|
GET | '/:slug/moves/new' | create |
POST | '/:slug/moves/new' | create |
GET | '/:slug/moves/edit' | update and delete |
POST | '/:slug/moves/edit' | update and delete |
During the development of my controllers and views, Ruby gem shotgun and binding.pry
became my best friends. rackup
only allows one-time read of application code, while shotgun
allows application code reload upon every request. It is prudent as programmers to check consistencies when building controllers and sending appropriate data to views simultaneously. I also learned the hard way not to forget use Rack::MethodOverride
as delete
and patch
requires Sinatra Middleware to find requests with name="_method"
.
Manipulating params
in the controllers from users' inputs, and transversing instance variables
from controllers to views are ways in which both controllers and views communicate, passing data back and forth.
As my controllers overloaded with more HTTP routes, code repetitions became un-avoidable. Helper methods have been very useful. Aside from logged_in?
and current_user
, I added finding_character_slug
in finding instance variable @character
.
def finding_character_slug
@character = current_user.characters.find_by_slug(params[:slug])
end
Front End::Views and Forms
ERB (or, Embedded Ruby) provides application for both HTML and Ruby code syntax. It is a great alternative to .html
format where its lexical environment is restricted to HTML tags. ERB has two types: <%= %>
substitution tag and <% %>
scripting tag. <%= %>
displays the results while <% %>
only evaluates.
Defining form methods, actions and retrieving inputs is essential for users in passing data. These form fields have various data type
inputs. Any user input data passed from the URL forms to the controllers is in Ruby hash data types, or params
. As developers we need to provide directions on where and how to send the data from the user. After all, creating dynamic web application allows user interaction, it gives more value than simply having static pages.
When creating these views, my biggest challenge was to create a flow for the user. As the user navigates through the site and populates user-driven attributes, I had to break a couple of RESTful conventions.
Authentication, Authorization and Validations
Open-source gem bcrypt
allows encrypted password in hashing algorithm. This is to avoid saving a password in its raw form - developer's nightmare! The User model needs to include has_secure_password
in order to allow AR macro method authenticate
when a user logs in.
HTTP (Hyper-Text Transfer Protocol) is a stateless protocol, meaning user's requests are treated as independent transactions. I need my web app to remember the user's identity from page to page. Welcome to Session Cookies! Session is an object (a hash datatype) storing user's data on the server when interacting with the web application, and passes the data to the client as a cookie. It persists user's information while the user navigates through pages. It typically expires when the user logs out or closes the browser. By checking concurrence with the user's session
hash, we authorize the corresponding user to access the app.
I have only used two validations for this project. validates_presence_of
is to confirm user's input (no empty data, or != ""
) on username, email, and password. Otherwise the system will not allow the user object to be saved in the database. validates_uniqueness_of
is to confirm no duplication on username or email. They are both useful in managing users. Along with these validations, I included gem rack-flash3
flash messages to display validation failures.
Execution
When either shotgun
or rackup
is triggered, config.ru
will be executed. My config.ru
requires my config/environment
where I have established sqlite3
database connection and controllers. Short video of back-end demo below.
Difficult Lessons Learned
At the inception of this project, I had both User and Character model relationships as has_and_belongs_to_many
. The idea was to have every single user the ability to access all characters, and each character belongs to many users. As I progressed further, I was faced with a challenge of my character attributes. Let's assume User A selects attributes of fighting moves and stages for a specific character. When User B selects the same character, it inherits pre-determined attributes. A brand new user would want a fresh set of characters with no pre-existing attributes.
My amazing cohort leader, Ally Kadel, helped me brainstorm a possible solution. Eventually, I revised the User and Character model relationships to has_many
and belongs_to
. User has_many
characters, and each character belongs_to
a user. In the CharactersController
, I duplicated the user's selected character using .dup
method, leaving the original character selections un-touched. User A will have duplicated sets of the original characters, and this allows User B to have another set of duplicate characters. The solution was rather simplistic, but I was stuck for the longest time - multiple brain farts do exist!
CSS the Fun Way
I got a chance to contribute a tiny bit of my design skills. I am an architect after all. Though, the project does not require any CSS requirements, but I figured why not. It's fun, and was my first CSS exposure. I decided to keep things simple and outsource from Bootswatch. I have learned div
, class
, style
, text-align
, display
along with few other css
attributes. I found using css selectors
on web browser's inspect
proved to be very useful in experimenting display results. I did not get a chance to do a deep dive into CSS documentation, but surely I will be more exposed in future modules. It gets complicated to apply consistent style and layout across all pages. Short video of front-end demo below.
Conclusion
Overall, I am satisfied with my rudimentary application of Content Management System (CMS) with Sinatra CRUD and MVC. I would love to eventually learn how to build integration testings RSpec
(another Domain Specific Language) with Capybara Ruby Library when time permits. Sinatra is another milestone in my developer journey. Moving on to Rails!
"Don't fight for victory—fight to improve yourself. Victory will come."
-Ryu, Super Street Fighter II Turbo HD Remix
fentybit / SinatraStreetFighter
Welcome to my simplistic version of Street Fighter simulation. You can select a character, add fighting moves and a final stage where your epic battle commences!
Post Scriptum:
This is my Module 2 capstone project with Flatiron School. I believe one of the catalyst to becoming a good programmer is to welcome constructive criticism. Feel free to drop a message. 🙂
Keep Calm, and Code On.
External Sources:
Street Fighter V Wiki
Ruby Gem Corneal
Free themes for Bootstrap
Active Record Associations
Top comments (1)
just wow, thanks for sharing