Tutorial
This is a simple tutorial showing how to build a Gin application from the grounds up.
This tutorial showcases the use of Test-Driven Development techniques to build a simple application that allows to see a list of users, and perform basic operations on them.
For the purpose of this tutorial we have omitted API authentication. We also assume that you have Gin and MySql properly installed. If not, follow the install instructions before proceeding.
The code of this tutorial can be found in the Github repo gin-demo.
Create application
Let's create an application called demo
:
$ gin new demo
Creating app demo...
created file demo/spec/models/.gitkeep
created file demo/app/models/.gitkeep
created file demo/spec/spec_helper.lua
created file demo/config/settings.lua
created file demo/spec/controllers/1/pages_controller_spec.lua
created file demo/db/schemas/.gitkeep
created file demo/db/mysql.lua
created file demo/config/nginx.conf
created file demo/config/errors.lua
created file demo/lib/.gitkeep
created file demo/config/application.lua
created file demo/app/controllers/1/pages_controller.lua
created file demo/db/migrations/.gitkeep
created file demo/.gitignore
created file demo/config/routes.lua
Let's now CD in the newly created directory:
$ cd demo
Run the default tests
The autogenerated application includes a controller and its tests. Ensure that everything works properly by running busted
:
$ busted
●
1 success / 0 failures / 0 pending : 0.052732 seconds.
Remove the existing controller and controller spec
We will not need the autogenerated application's controller and its test file. Go ahead and delete them:
$ rm app/controllers/1/pages_controller.lua
$ rm spec/controllers/1/pages_controller_spec.lua
Users index
Let's start by implementing a Users' index
API call.
Create the Users' controller test file
Create the file ./spec/controllers/1/users_controller_spec.lua
, and let's write the first test:
require 'spec.spec_helper'
describe("UsersController", function()
describe("#index", function()
it("shows the list of users", function()
local response = hit({
method = 'GET',
path = "/users"
})
assert.are.equal(200, response.status)
assert.are.same({}, response.body)
end)
end)
end)
As a start, we just want our Users' controller to return a successful response with an empty JSON body. Let's run the tests:
$ busted spec/controllers/1/users_controller_spec.lua
●
0 successes / 1 failure / 0 pending : 0.024976 seconds.
Failure → ./spec/controllers/1/users_controllers_spec.lua @ 6
UsersController / #index / shows the list of users
./spec/controllers/1/users_controllers_spec.lua:12: Expected objects to be the same. Passed in:
(number) 404
Expected:
(number) 200
That is to be expected, as we obviously still do not have created the users' controller nor have we specified the routes to handle the request on /users
.
Add the routes
Edit the file ./config/routes.lua
. Let's create a version 1
namespace, and the route to /users
.
local routes = require 'gin.core.routes'
-- define version
local v1 = routes.version(1)
-- define routes
v1:GET("/users", { controller = "users", action = "index" })
return routes
Create Users' controller
Create the file ./app/controllers/1/users_controller.lua
, and add the action for the route that we have just defined:
local UsersController = {}
function UsersController:index()
return 200, {}
end
return UsersController
Our tests will now pass:
$ busted spec/controllers/1/users_controller_spec.lua
●
1 success / 0 failures / 0 pending : 0.027029 seconds.
Congratulations! Now let's modify our controller to return the users in the database.
Setup Database
We will be using a MySql database, so let's edit the ./db/mysql.lua
file with our settings.
local SqlDatabase = require 'gin.db.sql'
local Gin = require 'gin.core.gin'
-- First, specify the environment settings for this database, for instance:
local DbSettings = {
development = {
adapter = 'mysql',
host = "127.0.0.1",
port = 3306,
database = "demo_development",
user = "root",
password = "",
pool = 5
},
test = {
adapter = 'mysql',
host = "127.0.0.1",
port = 3306,
database = "demo_test",
user = "root",
password = "",
pool = 5
},
production = {
adapter = 'mysql',
host = "127.0.0.1",
port = 3306,
database = "demo_production",
user = "root",
password = "",
pool = 5
}
}
-- Then initialize and return your database:
local MySql = SqlDatabase.new(DbSettings[Gin.env])
return MySql
We have now defined a connection to our MySql database.
Create a Users' table
Let's go ahead and generate a new migration for the newly created MySql
connection:
$ gin generate migration
Created new migration file
db/migrations/20131116131423.lua
Edit the file ./db/migrations/20131116131423.lua
to create the users
table in the up()
method (called when the migration is run), and destroy it in the down()
method (when the migration is rolled back), like this:
local SqlMigration = {}
-- specify the database used in this migration (needed by the Gin migration engine)
SqlMigration.db = require 'db.mysql'
function SqlMigration.up()
-- Run your migration
SqlMigration.db:execute([[
CREATE TABLE users (
id int NOT NULL AUTO_INCREMENT,
first_name varchar(255) NOT NULL,
last_name varchar(255),
PRIMARY KEY (id),
UNIQUE (first_name)
);
]])
end
function SqlMigration.down()
-- Run your rollback
SqlMigration.db:execute([[
DROP TABLE users;
]])
end
return SqlMigration
Let's now apply the migration in the test
environment:
$ GIN_ENV=test gin migrate
Migrating up in test environment
==> Successfully applied migration: 20131116131423
The migration engine has created the database
demo_test
for us.
Create Users model
In order to return users, we first need to create a Users
model. Go ahead and create the file ./app/models/users.lua
, with this content:
-- gin
local MySql = require 'db.mysql'
local SqlOrm = require 'gin.db.sql.orm'
-- define
return SqlOrm.define_model(MySql, 'users')
This defines a model Users
that corresponds to the database table users
, for the database connection MySql
.
By convention, models in Gin have plural names.
Modify Users' controller test
Let's now modify the Users' controller test to return users. Edit the file ./spec/controllers/1/users_controller_spec.lua
to add some fixture users in our database:
require 'spec.spec_helper'
local MySql = require 'db.mysql'
local Users = require 'app.models.users'
local function clean_db()
MySql:execute("TRUNCATE TABLE users;")
end
describe("UsersController", function()
before_each(function()
clean_db()
end)
after_each(function()
clean_db()
end)
describe("#index", function()
before_each(function()
roberto = Users.create({first_name = 'roberto', last_name = 'gin'})
hedy = Users.create({first_name = 'hedy', last_name = 'tonic'})
end)
after_each(function()
roberto = nil
hedy = nil
end)
it("shows the list of users ordered by first name", function()
local response = hit({
method = 'GET',
path = "/users"
})
assert.are.equal(200, response.status)
assert.are.same({
[1] = hedy,
[2] = roberto
}, response.body)
end)
end)
end)
Please note that in Lua, arrays start with index 1
, hence the expectations above.
Remember to clean up after your tests, this is what the
MySql:execute("TRUNCATE TABLE users;")
call inclean_db()
is for. We are callingclean_db()
before and after all tests, to ensure that the database has a clean start even in the case that previous tests were run improperly or aborted for any reason.Also, don't forget to reset any global variables, like
roberto
andhedy
in the example here above.
Let's run the controller tests:
$ busted spec/controllers/1/users_controllers_spec.lua
●
0 successes / 1 failure / 0 pending : 0.072839 seconds.
Failure → ./spec/controllers/1/users_controllers_spec.lua @ 31
UsersController / #index / shows the list of users ordered by first name
./spec/controllers/1/users_controllers_spec.lua:39: Expected objects to be the same. Passed in:
(table): { }
Expected:
(table): {
[2] = {
[last_name] = 'gin'
[first_name] = 'roberto'
[id] = 1 }
[1] = {
[last_name] = 'tonic'
[first_name] = 'hedy'
[id] = 2 } }
Let's go ahead and edit the controller:
local UsersController = {}
function UsersController:index()
local Users = require 'app.models.users'
local users = Users.all({ order = "first_name" })
return 200, users
end
return UsersController
For optimization reasons, ensure that you require the
Users
model in the controller action itself, not at module level.
If we run our tests now:
$ busted spec/controllers/1/users_controllers_spec.lua
●
1 success / 0 failures / 0 pending : 0.073925 seconds.
Great! Now what about smoke testing our application in a browser?
Prepare the development environment
Ensure migrations are run in the development
environment:
$ gin migrate
Migrating up in development environment
==> Successfully applied migration: 20131116131423
Now let's create some fake users in the development
database, from the Gin console:
$ gin console
Loading development environment (Gin v0.1)
Lua 5.1.5 Copyright (C) 1994-2012 Lua.org, PUC-Rio
> Users = require 'app.models.users'
> Users.create({first_name = 'roberto', last_name = 'gin'})
> Users.create({first_name = 'hedy', last_name = 'tonic'})
> pp(Users.all())
{
{
first_name = "roberto",
last_name = "gin",
id = 1
},
{
first_name = "hedy",
last_name = "tonic",
id = 2
}
}
As we can see from the pp
command (aka 'pretty print') issued in the console, our users have successfully been created in the development
database. Quit the console with CTRL-C
.
Let's start a server in the development
environment:
$ gin start
Gin app in development was succesfully started on port 7200.
Open up a browser and point it to the API Console at the address http://localhost:7200/ginconsole.
Input http://localhost:7200/users
in the API Console URL bar, and click on the HIT
button. You should see the response body from your server:
[
{
"last_name": "tonic",
"first_name": "hedy",
"id": 2
},
{
"last_name": "gin",
"first_name": "roberto",
"id": 1
}
]
Users create
Let's now implement the Users' create
API call.
For readability concerns, code related to the
index
action described in the previous section is partially omitted.
Add controller's test
Edit your controller test to add the tests for the create
action.
require 'spec.spec_helper'
local MySql = require 'db.mysql'
local Users = require 'app.models.users'
local function clean_db()
MySql:execute("TRUNCATE TABLE users;")
end
describe("UsersController", function()
before_each(function()
clean_db()
end)
after_each(function()
clean_db()
end)
[...]
describe("#create", function()
it("adds a new user", function()
local response = hit({
method = 'POST',
path = "/users",
body = { first_name = 'new-user', last_name = 'gin' }
})
local new_user = Users.find_by({ first_name = 'new-user' })
assert.are_not.equals(nil, new_user)
assert.are.equal(201, response.status)
assert.are.same(new_user, response.body)
end)
end)
end)
Running the tests returns an error:
$ busted spec/controllers/1/users_controllers_spec.lua
●●
1 success / 1 failure / 0 pending : 0.133645 seconds.
Failure → ./spec/controllers/1/users_controllers_spec.lua @ 47
UsersController / #create / adds a new user
./spec/controllers/1/users_controllers_spec.lua:57: Expected objects to be the same. Passed in:
(number) 404
Expected:
(number) 201
The passing test is obviously the one related to the index
action.
Create the route for the create
action, by editing the ./config/routes.lua
file:
local routes = require 'gin.core.routes'
-- define version
local v1 = routes.version(1)
-- define routes
v1:GET("/users", { controller = "users", action = "index" })
v1:POST("/users", { controller = "users", action = "create" })
return routes
Edit the Users' controller in ./app/controllers/1/users_controller.lua
to add the create action:
local UsersController = {}
[...]
function UsersController:create()
local Users = require 'app.models.users'
local new_user = Users.create(self.request.body)
return 201, new_user
end
return UsersController
Let's run the tests again:
$ busted spec/controllers/1/users_controllers_spec.lua
●●
2 successes / 0 failures / 0 pending : 0.166405 seconds.
We are now able to allow for user creation.
Accepted params
We're currently not filtering out the params that we are allowing external callers to set in our models. In this example, for instance, we do not want an external caller to be able to set the id
they want on new users, nor to make our server raise an error due to non-existent params being passed in.
Add the test:
require 'spec.spec_helper'
local MySql = require 'db.mysql'
local Users = require 'app.models.users'
local function clean_db()
MySql:execute("TRUNCATE TABLE users;")
end
describe("UsersController", function()
before_each(function()
clean_db()
end)
after_each(function()
clean_db()
end)
[...]
describe("#create", function()
it("adds a new user filtering out unaccepted params", function()
local request_new_user = {
first_name = 'new-user',
last_name = 'gin',
id = 400,
nonexisent_param = 'non-existent'
}
local response = hit({
method = 'POST',
path = "/users",
body = request_new_user
})
local new_user = Users.find_by({ first_name = 'new-user' })
assert.are_not.equals(nil, new_user)
assert.are.equal(201, response.status)
assert.are.same('new-user', new_user.first_name)
assert.are.same('gin', new_user.last_name)
assert.are.not_equals(400, new_user.id)
assert.are.not_equals('non-existent', new_user.nonexisent_param)
end)
end)
end)
Run the tests:
$ busted spec/controllers/1/users_controllers_spec.lua
●●
1 success / 1 failure / 0 pending : 0.138575 seconds.
Failure → ./spec/controllers/1/users_controllers_spec.lua @ 47
UsersController / #create / adds a new user filtering out unaccepted params
./spec/controllers/1/users_controllers_spec.lua:62: Expected objects to not be equal. Passed in:
(nil)
Did not expect:
(nil)
We can see that the user did not get created. If we open up the test logs (file logs/test-error.log
), we see:
2013/11/16 14:09:28 [error] 11545#0: *1 lua entry thread aborted: runtime error: /usr/local/share/lua/5.1/gin/core/router.lua:145: /usr/local/share/lua/5.1/gin/db/sql/mysql/adapter.lua:94: bad mysql result: Unknown column 'nonexisent_param' in 'field list': 1054 42S22
stack traceback:
coroutine 0:
[C]: in function 'error'
/usr/local/share/lua/5.1/gin/core/router.lua:145: in function 'call_controller'
/usr/local/share/lua/5.1/gin/core/router.lua:74: in function 'handler'
[string "content_by_lua"]:1: in function <[string "content_by_lua"]:1>, client: 127.0.0.1, server: , request: "POST /users? HTTP/1.1", host: "127.0.0.1"
This explains why our user did not get created, since there's an Unknown column 'nonexisent_param' in 'field list'
.
Let's implement the filter with the controller's accepted_params
method:
local UsersController = {}
[...]
function UsersController:create()
local Users = require 'app.models.users'
local params = self:accepted_params({ 'first_name', 'last_name' }, self.request.body)
local new_user = Users.create(params)
return 201, new_user
end
return UsersController
Our tests are now successful:
$ busted spec/controllers/1/users_controllers_spec.lua
●●
2 successes / 0 failures / 0 pending : 0.138814 seconds.
User Show
Let's finishe this tutorial by using a named route which implements the Users' show
API call.
For readability concerns, code related to the
index
andcreate
actions described in the previous sections is partially omitted.
Add the test for the show
method:
require 'spec.spec_helper'
local MySql = require 'db.mysql'
local Users = require 'app.models.users'
local function clean_db()
MySql:execute("TRUNCATE TABLE users;")
end
describe("UsersController", function()
before_each(function()
clean_db()
end)
after_each(function()
clean_db()
end)
[...]
describe("#show", function()
before_each(function()
roberto = Users.create({first_name = 'roberto', last_name = 'gin'})
end)
after_each(function()
roberto = nil
end)
it("shows a user", function()
local response = hit({
method = 'GET',
path = "/users/roberto"
})
assert.are.equal(200, response.status)
assert.are.same(roberto, response.body)
end)
end)
end)
We consider
first_name
as being the friendly identifier for a user. This is why we have a uniqueness constraint at database level on the columnfirst_name
, as defined in our migration here above.
Test fails with a 404
:
$ busted spec/controllers/1/users_controllers_spec.lua
●●●
2 successes / 1 failure / 0 pending : 0.191136 seconds.
Failure → ./spec/controllers/1/users_controllers_spec.lua @ 82
UsersController / #show / shows a user
./spec/controllers/1/users_controllers_spec.lua:88: Expected objects to be the same. Passed in:
(number) 404
Expected:
(number) 200
Add the named route :first_name
in ./config/routes.lua
:
local routes = require 'gin.core.routes'
-- define version
local v1 = routes.version(1)
-- define routes
v1:GET("/users", { controller = "users", action = "index" })
v1:POST("/users", { controller = "users", action = "create" })
v1:GET("/users/:first_name", { controller = "users", action = "show" })
return routes
Implement the show
code the controller:
local UsersController = {}
[...]
function UsersController:show()
local Users = require 'app.models.users'
local user = Users.find_by({ first_name = self.params.first_name })
return 200, user
end
return UsersController
Test now succeed:
$ busted spec/controllers/1/users_controllers_spec.lua
●●●
3 successes / 0 failures / 0 pending : 0.199211 seconds.
One last thing: we need to return a 404 if the user cannot be found. The test for the show
action becomes:
require 'spec.spec_helper'
local MySql = require 'db.mysql'
local Users = require 'app.models.users'
local function clean_db()
MySql:execute("TRUNCATE TABLE users;")
end
describe("UsersController", function()
before_each(function()
clean_db()
end)
after_each(function()
clean_db()
end)
[...]
describe("#show", function()
describe("when the user can be found", function()
before_each(function()
roberto = Users.create({first_name = 'roberto', last_name = 'gin'})
end)
after_each(function()
roberto = nil
end)
it("shows a user", function()
local response = hit({
method = 'GET',
path = "/users/roberto"
})
assert.are.equal(200, response.status)
assert.are.same(roberto, response.body)
end)
end)
describe("when the user cannot be found", function()
it("returns a 404", function()
local response = hit({
method = 'GET',
path = "/users/roberto"
})
assert.are.equal(404, response.status)
assert.are.same({}, response.body)
end)
end)
end)
end)
And the controller implementation:
local UsersController = {}
[...]
function UsersController:show()
local Users = require 'app.models.users'
local user = Users.find_by({ first_name = self.params.first_name })
if user then
return 200, user
else
return 404
end
end
return UsersController
This concludes the tutorial. To see the complete code for this application, please checkout the Github repo gin-demo.