├── .gitignore ├── .travis.sh ├── .travis.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── bin └── gin ├── gin-0.2.0-1.rockspec ├── gin ├── cli │ ├── api_console.lua │ ├── application.lua │ ├── base_launcher.lua │ ├── console.lua │ ├── launcher.lua │ └── migrations.lua ├── core │ ├── controller.lua │ ├── detached.lua │ ├── error.lua │ ├── gin.lua │ ├── request.lua │ ├── response.lua │ ├── router.lua │ ├── routes.lua │ └── settings.lua ├── db │ ├── migrations.lua │ ├── sql.lua │ └── sql │ │ ├── common │ │ └── orm.lua │ │ ├── mysql │ │ ├── adapter.lua │ │ ├── adapter_detached.lua │ │ └── orm.lua │ │ ├── orm.lua │ │ └── postgresql │ │ ├── adapter.lua │ │ ├── adapter_detached.lua │ │ ├── helpers.lua │ │ └── orm.lua ├── helpers │ ├── command.lua │ └── common.lua └── spec │ ├── init.lua │ ├── runner.lua │ └── runners │ ├── integration.lua │ └── response.lua └── spec ├── cli └── launcher_spec.lua ├── core ├── controller_spec.lua ├── error_spec.lua ├── request_spec.lua ├── response_spec.lua ├── router_spec.lua ├── routes_spec.lua ├── settings_spec.lua └── zebra_spec.lua ├── db ├── migrations_spec.lua ├── sql │ ├── mysql │ │ └── orm_spec.lua │ ├── orm_spec.lua │ └── postgresql │ │ └── orm_spec.lua └── sql_spec.lua ├── spec └── runners │ ├── integration_spec.lua │ └── response_spec.lua └── spec_helper.lua /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | tmp 3 | 4 | # vim 5 | .*.sw[a-z] 6 | *.un~ 7 | Session.vim 8 | 9 | # OSX 10 | .DS_Store 11 | ._* 12 | .Spotlight-V100 13 | .Trashes 14 | *.swp 15 | -------------------------------------------------------------------------------- /.travis.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | set -e 4 | 5 | export PREFIX=$HOME/.bin && mkdir -p $PREFIX && export PATH=$PATH:$PREFIX/bin 6 | 7 | if [ "$(expr substr $LUA 1 6)" == "luajit" ]; then 8 | git clone http://luajit.org/git/luajit-2.0.git luajit && cd luajit 9 | [[ "$LUA" == "luajit2.1" ]] && git checkout v2.1 || git checkout v2.0.4 10 | CFLAGS="$CFLAGS -DLUAJIT_ENABLE_LUA52COMPAT" make 11 | make PREFIX=$PREFIX INSTALL_TSYMNAME=lua install 12 | [[ -f $PREFIX/bin/lua ]] || ln -sf $PREFIX/bin/luajit-2.1.0-* $PREFIX/bin/lua 13 | else 14 | [[ "$LUA" == "lua5.1" ]] && wget -O - http://www.lua.org/ftp/lua-5.1.5.tar.gz | tar xz 15 | [[ "$LUA" == "lua5.3" ]] && wget -O - http://www.lua.org/ftp/lua-5.3.0.tar.gz | tar xz 16 | [[ "$LUA" == "lua5.2" || "$LUA" == "lua" ]] && wget -O - http://www.lua.org/ftp/lua-5.2.4.tar.gz | tar xz 17 | cd lua-5.* 18 | sed -i -e 's/-DLUA_COMPAT_ALL//g' -e 's/-DLUA_COMPAT_5_2//g' src/Makefile 19 | make linux && make INSTALL_TOP=$PREFIX install 20 | fi 21 | 22 | cd .. && wget -O - http://luarocks.org/releases/luarocks-2.2.2.tar.gz | tar xz && cd luarocks-* 23 | 24 | [[ "$(expr substr $LUA 1 6)" == "luajit" ]] && ./configure --prefix=$PREFIX \ 25 | --with-lua-include=$PREFIX/include/luajit-2.$([ "$LUA" == "luajit2.1" ] && echo "1" || echo "0") || ./configure --prefix=$PREFIX 26 | 27 | make build && make install && cd .. 28 | rm -rf luajit; rm -rf lua-5.*; rm -rf luarocks-* 29 | 30 | lua -v && luarocks --version 31 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: c 2 | sudo: false 3 | 4 | env: 5 | matrix: 6 | - LUA=lua5.1 7 | - LUA=luajit 8 | 9 | branches: 10 | only: master 11 | 12 | before_install: 13 | - source .travis.sh 14 | 15 | install: luarocks install *.rockspec --only-deps 16 | 17 | script: busted 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ### 0.1.5: 4 | * Add --trace option 5 | * Freeze dependencies 6 | 7 | ### 0.1.4: 8 | * Bug fixes 9 | 10 | ### 0.1.3: 11 | * Add DBI dependency 12 | * Bug fixes 13 | 14 | ### 0.1.2: 15 | * Improve performance 16 | * Bug fixes 17 | 18 | ### 0.1.1: 19 | * Add PostgreSql adapter 20 | * Bug fixes 21 | 22 | ### 0.1: 23 | * initial release. 24 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Roberto Ostinelli 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/ostinelli/gin.svg?branch=master)](https://travis-ci.org/ostinelli/gin) 2 | 3 | # GIN JSON-API framework 4 | 5 | Gin is an JSON-API framework, currently in its early stage. 6 | 7 | It has been designed to allow for fast development, TDD and ease of maintenance. 8 | 9 | Gin is helpful when you need an extra-boost in performance and scalability, as it runs embedded in a packaged version of nginx 10 | called [OpenResty](http://openresty.org/) and it's entirely written in [Lua](http://www.lua.org/). 11 | For those not familiar with Lua, don't let that scare you away: Lua is really easy to use, very fast and simple to get started with. 12 | 13 | For instance, this is what a simple Gin controller looks like: 14 | 15 | ```lua 16 | local InfoController = {} 17 | 18 | function InfoController:whoami() 19 | return 200, { name = 'gin' } 20 | end 21 | 22 | return InfoController 23 | ``` 24 | 25 | When called, this returns an HTTP `200` response with body: 26 | 27 | ```json 28 | { 29 | "name": "gin" 30 | } 31 | ``` 32 | 33 | #### Features 34 | 35 | Gin already provides: 36 | 37 | * [API Versioning](http://gin.io/docs/api_versioning.html) embedded in the framework 38 | * [Routes](http://gin.io/docs/routes.html) with named and pattern routes support 39 | * [Controllers](http://gin.io/docs/controllers.html) 40 | * [Models](http://gin.io/docs/models.html) and a MySql ORM 41 | * [Migrations](http://gin.io/docs/migrations.html) for SQL engines 42 | * [Test helpers](http://gin.io/docs/testing.html) and wrappers 43 | * Simple [error](http://gin.io/docs/errors.html) raising and definition 44 | * Support for multiple databases in your application 45 | * An embedded [API Console](http://gin.io/docs/api_console.html) to play with your API 46 | * A client to create, start and stop your applications 47 | 48 | Get started now! Please refer to the official [gin.io](http://gin.io) website for documentation. 49 | 50 | 51 | #### Contributing 52 | So you want to contribute? That's great! 53 | Please follow the guidelines below. It will make it easier to get merged in. 54 | 55 | Before implementing a new feature, please submit a ticket to discuss what you intend to do. 56 | Your feature might already be in the works, or an alternative implementation might have already been discussed. 57 | 58 | Every pull request should have its own topic branch. 59 | In this way, every additional adjustments to the original pull request might be done easily, and 60 | squashed with `git rebase -i`. The updated branch will be visible in the same pull request, so 61 | there will be no need to open new pull requests when there are changes to be applied. 62 | 63 | Do not commit to master in your fork. 64 | Provide a clean branch without merge commits. 65 | 66 | Ensure to include proper testing. To test gin you simply have to be in the project's root directory 67 | and issue: 68 | 69 | ``` 70 | $ busted 71 | 72 | ●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●● ○ 73 | 195 successes / 0 failures / 0 pending : 0.156489 seconds. 74 | ``` 75 | 76 | There will be no merges without a clean build. 77 | -------------------------------------------------------------------------------- /bin/gin: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env lua 2 | 3 | local Gin = require 'gin.core.gin' 4 | 5 | local help_description = [[ 6 | 7 | /++++/: 8 | NNNNNmm 9 | mNNMNmy 10 | .dNMMNms` 11 | .:+sydMMMMMdyo/-` 12 | `sdmmmNNNMMMMMNmdhhddo 13 | `dMMNNMMMMMMMMMNdhdMMy 14 | `dMMMNMMMMdsymMNdhdNMy 15 | `dMMMNNho-....:shhdNMy 16 | `dMMdo-......`.``:sNMy 17 | `dd/........-..````-hy 18 | `:...--.`----..`````.- 19 | ``//+- G I N .+-/` 20 | `:..-..``-...-.`.`.``- 21 | `hs-```-./.:`-/`.```os 22 | `hNms-.``.-.-.````+mmy 23 | `hNNNNh+.-:---`-smNNmy 24 | `hNNNMMMNy+.:sdNNNNNmy` 25 | `dNNNNMMMMMNNNNNNNNNNy` 26 | `hNNNNNMMMMMMMMNNmNNNy` 27 | ./osyhhddddddhhyss+:` 28 | 29 | GIN v]] .. Gin.version .. [[, a JSON-API web framework. 30 | 31 | Usage: gin COMMAND [ARGS] [OPTIONS] 32 | 33 | The available gin commands are: 34 | new [name] Create a new Gin application 35 | start Starts the Gin server 36 | stop Stops the Gin server 37 | console Start a Gin console 38 | generate migration [name] Create a new migration (name is optional) 39 | migrate Run all migrations that have not been run 40 | migrate rollback Rollback one migration 41 | 42 | Options: 43 | --trace Shows additional logs 44 | ]] 45 | 46 | local launcher = require 'gin.cli.launcher' 47 | local application = require 'gin.cli.application' 48 | local migrations = require 'gin.cli.migrations' 49 | local console = require 'gin.cli.console' 50 | 51 | -- check trace 52 | GIN_TRACE = false 53 | if arg[#arg] == '--trace' then 54 | table.remove(arg, #arg) 55 | GIN_TRACE = true 56 | end 57 | 58 | -- check args 59 | if arg[1] == 'new' and arg[2] then application.new(arg[2]) 60 | elseif arg[1] == 'start' then launcher.start() 61 | elseif arg[1] == 'stop' then launcher.stop() 62 | elseif (arg[1] == 'generate' or arg[1] == 'g') and arg[2] == 'migration' then migrations.new(arg[3]) 63 | elseif arg[1] == 'migrate' and arg[2] == nil then migrations.up() 64 | elseif arg[1] == 'migrate' and arg[2] == "rollback" then migrations.down() 65 | elseif arg[1] == 'console' or arg[1] == 'c' then console.start() 66 | else print(help_description) 67 | end 68 | -------------------------------------------------------------------------------- /gin-0.2.0-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "gin" 2 | version = "0.2.0-1" 3 | 4 | source = { 5 | url = "git://github.com/ostinelli/gin.git" 6 | } 7 | 8 | description = { 9 | summary = "A fast, low-latency, low-memory footprint, web JSON-API framework with Test Driven Development helpers and patterns.", 10 | homepage = "http://gin.io", 11 | maintainer = "Roberto Ostinelli ", 12 | license = "MIT" 13 | } 14 | 15 | dependencies = { 16 | "lua = 5.1", 17 | "ansicolors = 1.0.2-3", 18 | "busted = 2.0.rc10-0", 19 | "lua-cjson = 2.1.0-1", 20 | "luasocket = 3.0rc1-2", 21 | "luafilesystem = 1.6.3-1", 22 | "luaposix = 33.3.1-1", 23 | "penlight = 1.3.2-2", 24 | "luadbi = 0.5-1" 25 | } 26 | 27 | build = { 28 | type = "builtin", 29 | modules = { 30 | ["gin.cli.api_console"] = "gin/cli/api_console.lua", 31 | ["gin.cli.application"] = "gin/cli/application.lua", 32 | ["gin.cli.base_launcher"] = "gin/cli/base_launcher.lua", 33 | ["gin.cli.console"] = "gin/cli/console.lua", 34 | ["gin.cli.launcher"] = "gin/cli/launcher.lua", 35 | ["gin.cli.migrations"] = "gin/cli/migrations.lua", 36 | ["gin.core.controller"] = "gin/core/controller.lua", 37 | ["gin.core.detached"] = "gin/core/detached.lua", 38 | ["gin.core.error"] = "gin/core/error.lua", 39 | ["gin.core.gin"] = "gin/core/gin.lua", 40 | ["gin.core.request"] = "gin/core/request.lua", 41 | ["gin.core.response"] = "gin/core/response.lua", 42 | ["gin.core.router"] = "gin/core/router.lua", 43 | ["gin.core.routes"] = "gin/core/routes.lua", 44 | ["gin.core.settings"] = "gin/core/settings.lua", 45 | ["gin.db.sql.common.orm"] = "gin/db/sql/common/orm.lua", 46 | ["gin.db.sql.mysql.adapter"] = "gin/db/sql/mysql/adapter.lua", 47 | ["gin.db.sql.mysql.adapter_detached"] = "gin/db/sql/mysql/adapter_detached.lua", 48 | ["gin.db.sql.mysql.orm"] = "gin/db/sql/mysql/orm.lua", 49 | ["gin.db.sql.postgresql.adapter"] = "gin/db/sql/postgresql/adapter.lua", 50 | ["gin.db.sql.postgresql.adapter_detached"] = "gin/db/sql/postgresql/adapter_detached.lua", 51 | ["gin.db.sql.postgresql.helpers"] = "gin/db/sql/postgresql/helpers.lua", 52 | ["gin.db.sql.postgresql.orm"] = "gin/db/sql/postgresql/orm.lua", 53 | ["gin.db.sql.orm"] = "gin/db/sql/orm.lua", 54 | ["gin.db.migrations"] = "gin/db/migrations.lua", 55 | ["gin.db.sql"] = "gin/db/sql.lua", 56 | ["gin.helpers.command"] = "gin/helpers/command.lua", 57 | ["gin.helpers.common"] = "gin/helpers/common.lua", 58 | ["gin.spec.runners.integration"] = "gin/spec/runners/integration.lua", 59 | ["gin.spec.runners.response"] = "gin/spec/runners/response.lua", 60 | ["gin.spec.init"] = "gin/spec/init.lua", 61 | ["gin.spec.runner"] = "gin/spec/runner.lua", 62 | }, 63 | install = { 64 | bin = { "bin/gin" } 65 | }, 66 | } 67 | -------------------------------------------------------------------------------- /gin/cli/application.lua: -------------------------------------------------------------------------------- 1 | -- dep 2 | local ansicolors = require 'ansicolors' 3 | 4 | -- gin 5 | local Gin = require 'gin.core.gin' 6 | local helpers = require 'gin.helpers.common' 7 | 8 | 9 | local gitignore = [[ 10 | # gin 11 | client_body_temp 12 | fastcgi_temp 13 | logs 14 | proxy_temp 15 | tmp 16 | uwsgi_temp 17 | 18 | # vim 19 | .*.sw[a-z] 20 | *.un~ 21 | Session.vim 22 | 23 | # textmate 24 | *.tmproj 25 | *.tmproject 26 | tmtags 27 | 28 | # OSX 29 | .DS_Store 30 | ._* 31 | .Spotlight-V100 32 | .Trashes 33 | *.swp 34 | ]] 35 | 36 | 37 | 38 | local pages_controller = [[ 39 | local PagesController = {} 40 | 41 | function PagesController:root() 42 | return 200, { message = "Hello world from Gin!" } 43 | end 44 | 45 | return PagesController 46 | ]] 47 | 48 | 49 | local errors = [[ 50 | ------------------------------------------------------------------------------------------------------------------- 51 | -- Define all of your application errors in here. They should have the format: 52 | -- 53 | -- local Errors = { 54 | -- [1000] = { status = 400, message = "My Application error.", headers = { ["X-Header"] = "header" } }, 55 | -- } 56 | -- 57 | -- where: 58 | -- '1000' is the error number that can be raised from controllers with `self:raise_error(1000) 59 | -- 'status' (required) is the http status code 60 | -- 'message' (required) is the error description 61 | -- 'headers' (optional) are the headers to be returned in the response 62 | ------------------------------------------------------------------------------------------------------------------- 63 | 64 | local Errors = {} 65 | 66 | return Errors 67 | ]] 68 | 69 | 70 | local application = [[ 71 | local Application = { 72 | name = "{{APP_NAME}}", 73 | version = '0.0.1' 74 | } 75 | 76 | return Application 77 | ]] 78 | 79 | 80 | mysql = [[ 81 | local SqlDatabase = require 'gin.db.sql' 82 | local Gin = require 'gin.core.gin' 83 | 84 | -- First, specify the environment settings for this database, for instance: 85 | -- local DbSettings = { 86 | -- development = { 87 | -- adapter = 'mysql', 88 | -- host = "127.0.0.1", 89 | -- port = 3306, 90 | -- database = "{{APP_NAME}}_development", 91 | -- user = "root", 92 | -- password = "", 93 | -- pool = 5 94 | -- }, 95 | 96 | -- test = { 97 | -- adapter = 'mysql', 98 | -- host = "127.0.0.1", 99 | -- port = 3306, 100 | -- database = "{{APP_NAME}}_test", 101 | -- user = "root", 102 | -- password = "", 103 | -- pool = 5 104 | -- }, 105 | 106 | -- production = { 107 | -- adapter = 'mysql', 108 | -- host = "127.0.0.1", 109 | -- port = 3306, 110 | -- database = "{{APP_NAME}}_production", 111 | -- user = "root", 112 | -- password = "", 113 | -- pool = 5 114 | -- } 115 | -- } 116 | 117 | -- Then initialize and return your database: 118 | -- local MySql = SqlDatabase.new(DbSettings[Gin.env]) 119 | -- 120 | -- return MySql 121 | ]] 122 | 123 | 124 | local nginx_config = [[ 125 | pid ]] .. Gin.app_dirs.tmp .. [[/{{GIN_ENV}}-nginx.pid; 126 | 127 | # This number should be at maxium the number of CPU on the server 128 | worker_processes 4; 129 | 130 | events { 131 | # Number of connections per worker 132 | worker_connections 4096; 133 | } 134 | 135 | http { 136 | # use sendfile 137 | sendfile on; 138 | 139 | # Gin initialization 140 | {{GIN_INIT}} 141 | 142 | server { 143 | # List port 144 | listen {{GIN_PORT}}; 145 | 146 | # Access log with buffer, or disable it completetely if unneeded 147 | access_log ]] .. Gin.app_dirs.logs .. [[/{{GIN_ENV}}-access.log combined buffer=16k; 148 | # access_log off; 149 | 150 | # Error log 151 | error_log ]] .. Gin.app_dirs.logs .. [[/{{GIN_ENV}}-error.log; 152 | 153 | # Gin runtime 154 | {{GIN_RUNTIME}} 155 | } 156 | 157 | # set temp paths 158 | proxy_temp_path tmp; 159 | client_body_temp_path tmp; 160 | fastcgi_temp_path tmp; 161 | scgi_temp_path tmp; 162 | uwsgi_temp_path tmp; 163 | } 164 | ]] 165 | 166 | 167 | local routes = [[ 168 | local routes = require 'gin.core.routes' 169 | 170 | -- define version 171 | local v1 = routes.version(1) 172 | 173 | -- define routes 174 | v1:GET("/", { controller = "pages", action = "root" }) 175 | 176 | return routes 177 | ]] 178 | 179 | 180 | local settings = [[ 181 | -------------------------------------------------------------------------------- 182 | -- Settings defined here are environment dependent. Inside of your application, 183 | -- `Gin.settings` will return the ones that correspond to the environment 184 | -- you are running the server in. 185 | -------------------------------------------------------------------------------- 186 | 187 | local Settings = {} 188 | 189 | Settings.development = { 190 | code_cache = false, 191 | port = 7200, 192 | expose_api_console = true 193 | } 194 | 195 | Settings.test = { 196 | code_cache = true, 197 | port = 7201, 198 | expose_api_console = false 199 | } 200 | 201 | Settings.production = { 202 | code_cache = true, 203 | port = 80, 204 | expose_api_console = false 205 | } 206 | 207 | return Settings 208 | ]] 209 | 210 | 211 | local pages_controller_spec = [[ 212 | require 'spec.spec_helper' 213 | 214 | describe("PagesController", function() 215 | 216 | describe("#root", function() 217 | it("responds with a welcome message", function() 218 | local response = hit({ 219 | method = 'GET', 220 | path = "/" 221 | }) 222 | 223 | assert.are.same(200, response.status) 224 | assert.are.same({ message = "Hello world from Gin!" }, response.body) 225 | end) 226 | end) 227 | end) 228 | ]] 229 | 230 | 231 | local spec_helper = [[ 232 | require 'gin.spec.runner' 233 | ]] 234 | 235 | 236 | local GinApplication = {} 237 | 238 | GinApplication.files = { 239 | ['.gitignore'] = gitignore, 240 | ['app/controllers/1/pages_controller.lua'] = pages_controller, 241 | ['app/models/.gitkeep'] = "", 242 | ['config/errors.lua'] = errors, 243 | ['config/application.lua'] = "", 244 | ['config/nginx.conf'] = nginx_config, 245 | ['config/routes.lua'] = routes, 246 | ['config/settings.lua'] = settings, 247 | ['db/migrations/.gitkeep'] = "", 248 | ['db/schemas/.gitkeep'] = "", 249 | ['db/mysql.lua'] = "", 250 | ['lib/.gitkeep'] = "", 251 | ['spec/controllers/1/pages_controller_spec.lua'] = pages_controller_spec, 252 | ['spec/models/.gitkeep'] = "", 253 | ['spec/spec_helper.lua'] = spec_helper 254 | } 255 | 256 | function GinApplication.new(name) 257 | print(ansicolors("Creating app %{cyan}" .. name .. "%{reset}...")) 258 | 259 | GinApplication.files['config/application.lua'] = string.gsub(application, "{{APP_NAME}}", name) 260 | GinApplication.files['db/mysql.lua'] = string.gsub(mysql, "{{APP_NAME}}", name) 261 | GinApplication.create_files(name) 262 | end 263 | 264 | function GinApplication.create_files(parent) 265 | for file_path, file_content in pairs(GinApplication.files) do 266 | -- ensure containing directory exists 267 | local full_file_path = parent .. "/" .. file_path 268 | helpers.mkdirs(full_file_path) 269 | 270 | -- create file 271 | local fw = io.open(full_file_path, "w") 272 | fw:write(file_content) 273 | fw:close() 274 | 275 | print(ansicolors(" %{green}created file%{reset} " .. full_file_path)) 276 | end 277 | end 278 | 279 | return GinApplication 280 | -------------------------------------------------------------------------------- /gin/cli/base_launcher.lua: -------------------------------------------------------------------------------- 1 | -- dep 2 | local lfs = require 'lfs' 3 | 4 | -- gin 5 | local Gin = require 'gin.core.gin' 6 | 7 | 8 | local function create_dirs(necessary_dirs) 9 | for _, dir in pairs(necessary_dirs) do 10 | lfs.mkdir(dir) 11 | end 12 | end 13 | 14 | local function create_nginx_conf(nginx_conf_file_path, nginx_conf_content) 15 | local fw = io.open(nginx_conf_file_path, "w") 16 | fw:write(nginx_conf_content) 17 | fw:close() 18 | end 19 | 20 | local function remove_nginx_conf(nginx_conf_file_path) 21 | os.remove(nginx_conf_file_path) 22 | end 23 | 24 | local function nginx_command(env, nginx_conf_file_path, nginx_signal) 25 | local devnull_logs = "" 26 | if GIN_TRACE == false then devnull_logs = " 2>/dev/null" end 27 | 28 | local env_cmd = "" 29 | if env ~= nil then env_cmd = "-g \"env GIN_ENV=" .. env .. ";\"" end 30 | local cmd = "nginx " .. nginx_signal .. " " .. env_cmd .. " -p `pwd`/ -c " .. nginx_conf_file_path .. devnull_logs 31 | 32 | if GIN_TRACE == true then 33 | print(cmd) 34 | end 35 | 36 | return os.execute(cmd) 37 | end 38 | 39 | local function start_nginx(env, nginx_conf_file_path) 40 | return nginx_command(env, nginx_conf_file_path, '') 41 | end 42 | 43 | local function stop_nginx(env, nginx_conf_file_path) 44 | return nginx_command(env, nginx_conf_file_path, '-s stop') 45 | end 46 | 47 | 48 | local BaseLauncher = {} 49 | BaseLauncher.__index = BaseLauncher 50 | 51 | function BaseLauncher.new(nginx_conf_content, nginx_conf_file_path) 52 | local necessary_dirs = Gin.app_dirs 53 | 54 | local instance = { 55 | nginx_conf_content = nginx_conf_content, 56 | nginx_conf_file_path = nginx_conf_file_path, 57 | necessary_dirs = necessary_dirs 58 | } 59 | setmetatable(instance, BaseLauncher) 60 | return instance 61 | end 62 | 63 | function BaseLauncher:start(env) 64 | create_dirs(self.necessary_dirs) 65 | create_nginx_conf(self.nginx_conf_file_path, self.nginx_conf_content) 66 | 67 | return start_nginx(env, self.nginx_conf_file_path) 68 | end 69 | 70 | function BaseLauncher:stop(env) 71 | result = stop_nginx(env, self.nginx_conf_file_path) 72 | remove_nginx_conf(self.nginx_conf_file_path) 73 | 74 | return result 75 | end 76 | 77 | 78 | return BaseLauncher 79 | -------------------------------------------------------------------------------- /gin/cli/console.lua: -------------------------------------------------------------------------------- 1 | -- dep 2 | local ansicolors = require 'ansicolors' 3 | local prettyprint = require 'pl.pretty' 4 | 5 | -- gin 6 | local Gin = require 'gin.core.gin' 7 | 8 | 9 | local GinConsole = {} 10 | 11 | function GinConsole.start() 12 | print(ansicolors("Loading %{cyan}" .. Gin.env .. "%{reset} environment (Gin v" .. Gin.version .. ")")) 13 | os.execute("lua -i -e \"require 'gin.core.detached' require 'gin.helpers.command'\"") 14 | end 15 | 16 | return GinConsole 17 | -------------------------------------------------------------------------------- /gin/cli/launcher.lua: -------------------------------------------------------------------------------- 1 | -- dep 2 | local ansicolors = require 'ansicolors' 3 | 4 | -- gin 5 | local Gin = require 'gin.core.gin' 6 | local BaseLauncher = require 'gin.cli.base_launcher' 7 | local helpers = require 'gin.helpers.common' 8 | 9 | -- settings 10 | local nginx_conf_source = 'config/nginx.conf' 11 | 12 | 13 | local GinLauncher = {} 14 | 15 | -- convert true|false to on|off 16 | local function convert_boolean_to_onoff(value) 17 | if value == true then value = 'on' else value = 'off' end 18 | return value 19 | end 20 | 21 | -- get application database modules 22 | local function database_modules() 23 | return helpers.module_names_in_path(Gin.app_dirs.db) 24 | end 25 | 26 | -- add upstream for databases 27 | local function gin_init_databases(gin_init) 28 | local modules = database_modules() 29 | 30 | for _, module_name in ipairs(modules) do 31 | local db = require(module_name) 32 | 33 | if type(db) == "table" and db.options.adapter == 'postgresql' then 34 | local name = db.adapter.location_for(db.options) 35 | gin_init = gin_init .. [[ 36 | upstream ]] .. name .. [[ { 37 | postgres_server ]] .. db.options.host .. [[:]] .. db.options.port .. [[ dbname=]] .. db.options.database .. [[ user=]] .. db.options.user .. [[ password=]] .. db.options.password .. [[; 38 | } 39 | ]] 40 | end 41 | end 42 | 43 | return gin_init 44 | end 45 | 46 | -- gin init 47 | local function gin_init(nginx_content) 48 | -- gin init 49 | local gin_init = [[ 50 | lua_code_cache ]] .. convert_boolean_to_onoff(Gin.settings.code_cache) .. [[; 51 | lua_package_path "./?.lua;$prefix/lib/?.lua;${LUA_PACKAGE_PATH};;"; 52 | ]] 53 | 54 | -- add db upstreams 55 | gin_init = gin_init_databases(gin_init) 56 | 57 | return string.gsub(nginx_content, "{{GIN_INIT}}", gin_init) 58 | end 59 | 60 | -- add locations for databases 61 | local function gin_runtime_databases(gin_runtime) 62 | local modules = database_modules() 63 | local postgresql_adapter = require 'gin.db.sql.postgresql.adapter' 64 | 65 | for _, module_name in ipairs(modules) do 66 | local db = require(module_name) 67 | 68 | if type(db) == "table" and db.options.adapter == 'postgresql' then 69 | local location = postgresql_adapter.location_for(db.options) 70 | local execute_location = postgresql_adapter.execute_location_for(db.options) 71 | 72 | gin_runtime = gin_runtime .. [[ 73 | location = /]] .. execute_location .. [[ { 74 | internal; 75 | postgres_pass ]] .. location .. [[; 76 | postgres_query $echo_request_body; 77 | } 78 | ]] 79 | end 80 | end 81 | 82 | return gin_runtime 83 | end 84 | 85 | -- gin runtime 86 | local function gin_runtime(nginx_content) 87 | local gin_runtime = [[ 88 | location / { 89 | content_by_lua 'require(\"gin.core.router\").handler(ngx)'; 90 | } 91 | ]] 92 | if Gin.settings.expose_api_console == true then 93 | gin_runtime = gin_runtime .. [[ 94 | location /ginconsole { 95 | content_by_lua 'require(\"gin.cli.api_console\").handler(ngx)'; 96 | } 97 | ]] 98 | end 99 | 100 | -- add db locations 101 | gin_runtime = gin_runtime_databases(gin_runtime) 102 | 103 | return string.gsub(nginx_content, "{{GIN_RUNTIME}}", gin_runtime) 104 | end 105 | 106 | 107 | function GinLauncher.nginx_conf_content() 108 | -- read nginx.conf file 109 | local nginx_conf_template = helpers.read_file(nginx_conf_source) 110 | 111 | -- append notice 112 | nginx_conf_template = [[ 113 | # ===================================================================== # 114 | # THIS FILE IS AUTO GENERATED. DO NOT MODIFY. # 115 | # IF YOU CAN SEE IT, THERE PROBABLY IS A RUNNING SERVER REFERENCING IT. # 116 | # ===================================================================== # 117 | 118 | ]] .. nginx_conf_template 119 | 120 | -- inject params in content 121 | local nginx_content = nginx_conf_template 122 | nginx_content = string.gsub(nginx_content, "{{GIN_PORT}}", Gin.settings.port) 123 | nginx_content = string.gsub(nginx_content, "{{GIN_ENV}}", Gin.env) 124 | 125 | -- gin imit & runtime 126 | nginx_content = gin_init(nginx_content) 127 | nginx_content = gin_runtime(nginx_content) 128 | 129 | -- return 130 | return nginx_content 131 | end 132 | 133 | function nginx_conf_file_path() 134 | return Gin.app_dirs.tmp .. "/" .. Gin.env .. "-nginx.conf" 135 | end 136 | 137 | function base_launcher() 138 | return BaseLauncher.new(GinLauncher.nginx_conf_content(), nginx_conf_file_path()) 139 | end 140 | 141 | 142 | function GinLauncher.start(env) 143 | -- init base_launcher 144 | local ok, base_launcher = pcall(function() return base_launcher() end) 145 | 146 | if ok == false then 147 | print(ansicolors("%{red}ERROR:%{reset} Cannot initialize launcher: " .. base_launcher)) 148 | return 149 | end 150 | 151 | result = base_launcher:start(env) 152 | 153 | if result == 0 then 154 | if Gin.env ~= 'test' then 155 | print(ansicolors("Gin app in %{cyan}" .. Gin.env .. "%{reset} was succesfully started on port " .. Gin.settings.port .. ".")) 156 | end 157 | else 158 | print(ansicolors("%{red}ERROR:%{reset} Could not start Gin app on port " .. Gin.settings.port .. " (is it running already?).")) 159 | end 160 | end 161 | 162 | function GinLauncher.stop(env) 163 | -- init base_launcher 164 | local base_launcher = base_launcher() 165 | 166 | result = base_launcher:stop(env) 167 | 168 | if Gin.env ~= 'test' then 169 | if result == 0 then 170 | print(ansicolors("Gin app in %{cyan}" .. Gin.env .. "%{reset} was succesfully stopped.")) 171 | else 172 | print(ansicolors("%{red}ERROR:%{reset} Could not stop Gin app (are you sure it is running?).")) 173 | end 174 | end 175 | end 176 | 177 | return GinLauncher 178 | -------------------------------------------------------------------------------- /gin/cli/migrations.lua: -------------------------------------------------------------------------------- 1 | -- dep 2 | local ansicolors = require 'ansicolors' 3 | 4 | -- gin 5 | local Gin = require 'gin.core.gin' 6 | local helpers = require 'gin.helpers.common' 7 | local Migrations = require 'gin.db.migrations' 8 | 9 | 10 | local migrations_new = [====[ 11 | local SqlMigration = {} 12 | 13 | -- specify the database used in this migration (needed by the Gin migration engine) 14 | -- SqlMigration.db = require 'db.mysql' 15 | 16 | function SqlMigration.up() 17 | -- Run your migration 18 | end 19 | 20 | function SqlMigration.down() 21 | -- Run your rollback 22 | end 23 | 24 | return SqlMigration 25 | ]====] 26 | 27 | 28 | local function display_result(direction, response) 29 | local error_head, error_message, success_message, symbol 30 | 31 | if direction == "up" then 32 | error_head = "An error occurred while running the migration:" 33 | error_message = "More recent migrations have been canceled. Please review the error:" 34 | success_message = "Successfully applied migration:" 35 | symbol = "==>" 36 | else 37 | error_head = "An error occurred while rolling back the migration:" 38 | error_message = "Please review the error:" 39 | success_message = "Successfully rolled back migration:" 40 | symbol = "<==" 41 | end 42 | 43 | if #response > 0 then 44 | for k, version_info in ipairs(response) do 45 | if version_info.error ~= nil then 46 | print(ansicolors("%{red}ERROR:%{reset} " .. error_head .. " %{cyan}" .. version_info.version .. "%{reset}")) 47 | print(error_message) 48 | print("-------------------------------------------------------------------") 49 | print(version_info.error) 50 | print("-------------------------------------------------------------------") 51 | else 52 | print(ansicolors(symbol .. " %{green}" .. success_message .. "%{reset} " .. version_info.version)) 53 | end 54 | end 55 | end 56 | end 57 | 58 | 59 | local SqlMigrations = {} 60 | 61 | function SqlMigrations.new(name) 62 | -- define file path 63 | local timestamp = os.date("%Y%m%d%H%M%S") 64 | local full_file_path = Gin.app_dirs.migrations .. '/' .. timestamp .. (name and '_' .. name or '') .. '.lua' 65 | 66 | -- create file 67 | local fw = io.open(full_file_path, "w") 68 | fw:write(migrations_new) 69 | fw:close() 70 | 71 | -- output message 72 | print(ansicolors("%{green}Created new migration file%{reset}")) 73 | print(" " .. full_file_path) 74 | end 75 | 76 | function SqlMigrations.up() 77 | print(ansicolors("Migrating up in %{cyan}" .. Gin.env .. "%{reset} environment")) 78 | 79 | local ok, response = Migrations.up() 80 | display_result("up", response) 81 | 82 | end 83 | 84 | function SqlMigrations.down() 85 | print(ansicolors("Rolling back one migration in %{cyan}" .. Gin.env .. "%{reset} environment")) 86 | 87 | local ok, response = Migrations.down() 88 | display_result("down", response) 89 | end 90 | 91 | return SqlMigrations 92 | -------------------------------------------------------------------------------- /gin/core/controller.lua: -------------------------------------------------------------------------------- 1 | -- perf 2 | local error = error 3 | local pairs = pairs 4 | local setmetatable = setmetatable 5 | 6 | 7 | local Controller = {} 8 | Controller.__index = Controller 9 | 10 | function Controller.new(request, params) 11 | params = params or {} 12 | 13 | local instance = { 14 | params = params, 15 | request = request 16 | } 17 | setmetatable(instance, Controller) 18 | return instance 19 | end 20 | 21 | function Controller:raise_error(code, custom_attrs) 22 | error({ code = code, custom_attrs = custom_attrs }) 23 | end 24 | 25 | function Controller:accepted_params(param_filters, params) 26 | local accepted_params = {} 27 | for _, param in pairs(param_filters) do 28 | accepted_params[param] = params[param] 29 | end 30 | return accepted_params 31 | end 32 | 33 | return Controller 34 | -------------------------------------------------------------------------------- /gin/core/detached.lua: -------------------------------------------------------------------------------- 1 | -- detached 2 | local adapter_mysql = require 'gin.db.sql.mysql.adapter_detached' 3 | package.loaded['gin.db.sql.mysql.adapter'] = adapter_mysql 4 | 5 | local adapter_postgresql = require 'gin.db.sql.postgresql.adapter_detached' 6 | package.loaded['gin.db.sql.postgresql.adapter'] = adapter_postgresql 7 | -------------------------------------------------------------------------------- /gin/core/error.lua: -------------------------------------------------------------------------------- 1 | -- gin 2 | local helpers = require 'gin.helpers.common' 3 | 4 | -- perf 5 | local error = error 6 | local pairs = pairs 7 | local setmetatable = setmetatable 8 | 9 | 10 | -- define error 11 | Error = {} 12 | Error.__index = Error 13 | 14 | local function init_errors() 15 | -- get app errors 16 | local errors = helpers.try_require('config.errors', {}) 17 | -- add system errors 18 | errors[100] = { status = 412, message = "Accept header not set." } 19 | errors[101] = { status = 412, message = "Invalid Accept header format." } 20 | errors[102] = { status = 412, message = "Unsupported version specified in the Accept header." } 21 | errors[103] = { status = 400, message = "Could not parse JSON in body." } 22 | errors[104] = { status = 400, message = "Body should be a JSON hash." } 23 | 24 | return errors 25 | end 26 | 27 | Error.list = init_errors() 28 | 29 | function Error.new(code, custom_attrs) 30 | local err = Error.list[code] 31 | if err == nil then error("invalid error code") end 32 | 33 | local body = { 34 | code = code, 35 | message = err.message 36 | } 37 | 38 | if custom_attrs ~= nil then 39 | for k,v in pairs(custom_attrs) do body[k] = v end 40 | end 41 | 42 | local instance = { 43 | status = err.status, 44 | headers = err.headers or {}, 45 | body = body, 46 | } 47 | setmetatable(instance, Error) 48 | return instance 49 | end 50 | 51 | return Error 52 | -------------------------------------------------------------------------------- /gin/core/gin.lua: -------------------------------------------------------------------------------- 1 | -- gin 2 | local settings = require 'gin.core.settings' 3 | 4 | -- perf 5 | local ogetenv = os.getenv 6 | 7 | 8 | local Gin = {} 9 | 10 | -- version 11 | Gin.version = '0.2.0' 12 | 13 | -- environment 14 | Gin.env = ogetenv("GIN_ENV") or 'development' 15 | 16 | -- directories 17 | Gin.app_dirs = { 18 | tmp = 'tmp', 19 | logs = 'logs', 20 | db = 'db', 21 | schemas = 'db/schemas', 22 | migrations = 'db/migrations' 23 | } 24 | 25 | Gin.settings = settings.for_environment(Gin.env) 26 | 27 | return Gin 28 | -------------------------------------------------------------------------------- /gin/core/request.lua: -------------------------------------------------------------------------------- 1 | -- dep 2 | local json = require 'cjson' 3 | 4 | -- perf 5 | local error = error 6 | local jdecode = json.decode 7 | local pcall = pcall 8 | local rawget = rawget 9 | local setmetatable = setmetatable 10 | 11 | 12 | local Request = {} 13 | Request.__index = Request 14 | 15 | function Request.new(ngx) 16 | -- read body 17 | ngx.req.read_body() 18 | local body_raw = ngx.req.get_body_data() 19 | 20 | -- parse body 21 | local body = nil 22 | if body_raw == nil then 23 | body = nil 24 | else 25 | ok, json_or_error = pcall(function() return jdecode(body_raw) end) 26 | if ok == false then error({ code = 103 }) end 27 | if json_or_error[1] ~= nil then error({ code = 104 }) end 28 | body = json_or_error 29 | end 30 | 31 | -- init instance 32 | local instance = { 33 | ngx = ngx, 34 | uri = ngx.var.uri, 35 | method = ngx.var.request_method, 36 | headers = ngx.req.get_headers(), 37 | body_raw = body_raw, 38 | body= body, 39 | api_version = nil, 40 | __cache = {} 41 | } 42 | setmetatable(instance, Request) 43 | return instance 44 | end 45 | 46 | function Request:__index(index) 47 | local out = rawget(rawget(self, '__cache'), index) 48 | if out then return out end 49 | 50 | if index == 'uri_params' then 51 | self.__cache[index] = self.ngx.req.get_uri_args() 52 | return self.__cache[index] 53 | 54 | else 55 | return rawget(self, index) 56 | end 57 | end 58 | 59 | return Request 60 | -------------------------------------------------------------------------------- /gin/core/response.lua: -------------------------------------------------------------------------------- 1 | -- perf 2 | local setmetatable = setmetatable 3 | 4 | 5 | local Response = {} 6 | Response.__index = Response 7 | 8 | function Response.new(options) 9 | options = options or {} 10 | 11 | local instance = { 12 | status = options.status or 200, 13 | headers = options.headers or {}, 14 | body = options.body or {}, 15 | } 16 | setmetatable(instance, Response) 17 | return instance 18 | end 19 | 20 | return Response 21 | -------------------------------------------------------------------------------- /gin/core/router.lua: -------------------------------------------------------------------------------- 1 | package.path = './app/controllers/?.lua;' .. package.path 2 | 3 | -- dep 4 | local json = require 'cjson' 5 | 6 | -- gin 7 | local Gin = require 'gin.core.gin' 8 | local Controller = require 'gin.core.controller' 9 | local Request = require 'gin.core.request' 10 | local Response = require 'gin.core.response' 11 | local Error = require 'gin.core.error' 12 | 13 | -- app 14 | local Routes = require 'config.routes' 15 | local Application = require 'config.application' 16 | 17 | -- perf 18 | local error = error 19 | local jencode = json.encode 20 | local pairs = pairs 21 | local pcall = pcall 22 | local require = require 23 | local setmetatable = setmetatable 24 | local smatch = string.match 25 | local function tappend(t, v) t[#t+1] = v end 26 | 27 | 28 | -- init Router and set routes 29 | local Router = {} 30 | 31 | -- response version header 32 | local response_version_header = 'gin/'.. Gin.version 33 | 34 | -- accept header for application 35 | local accept_header_matcher = "^application/vnd." .. Application.name .. ".v(%d+)(.*)+json$" 36 | 37 | 38 | local function create_request(ngx) 39 | local ok, request_or_error = pcall(function() return Request.new(ngx) end) 40 | if ok == false then 41 | -- parsing errors 42 | local err = Error.new(request_or_error.code, request_or_error.custom_attrs) 43 | response = Response.new({ status = err.status, body = err.body }) 44 | Router.respond(ngx, response) 45 | return false 46 | end 47 | return request_or_error 48 | end 49 | 50 | -- main handler function, called from nginx 51 | function Router.handler(ngx) 52 | -- add headers 53 | ngx.header.content_type = 'application/json' 54 | ngx.header["X-Framework"] = response_version_header; 55 | 56 | -- create request object 57 | local request = create_request(ngx) 58 | if request == false then return end 59 | 60 | -- get routes 61 | local ok, controller_name_or_error, action, params, request = pcall(function() return Router.match(request) end) 62 | 63 | local response 64 | 65 | if ok == false then 66 | -- match returned an error (for instance a 412 for no header match) 67 | local err = Error.new(controller_name_or_error.code, controller_name_or_error.custom_attrs) 68 | response = Response.new({ status = err.status, body = err.body }) 69 | Router.respond(ngx, response) 70 | 71 | elseif controller_name_or_error then 72 | -- matching routes found 73 | response = Router.call_controller(request, controller_name_or_error, action, params) 74 | Router.respond(ngx, response) 75 | 76 | else 77 | -- no matching routes found 78 | ngx.exit(ngx.HTTP_NOT_FOUND) 79 | end 80 | end 81 | 82 | -- match request to routes 83 | function Router.match(request) 84 | local uri = request.uri 85 | local method = request.method 86 | 87 | -- match version based on headers 88 | if request.headers['accept'] == nil then error({ code = 100 }) end 89 | 90 | local major_version, rest_version = smatch(request.headers['accept'], accept_header_matcher) 91 | if major_version == nil then error({ code = 101 }) end 92 | 93 | local routes_dispatchers = Routes.dispatchers[tonumber(major_version)] 94 | if routes_dispatchers == nil then error({ code = 102 }) end 95 | 96 | -- loop dispatchers to find route 97 | for i = 1, #routes_dispatchers do 98 | local dispatcher = routes_dispatchers[i] 99 | if dispatcher[method] then -- avoid matching if method is not defined in dispatcher 100 | local match = { smatch(uri, dispatcher.pattern) } 101 | 102 | if #match > 0 then 103 | local params = {} 104 | for j = 1, #match do 105 | if dispatcher[method].params[j] then 106 | params[dispatcher[method].params[j]] = match[j] 107 | else 108 | tappend(params, match[j]) 109 | end 110 | end 111 | 112 | -- set version on request 113 | request.api_version = major_version .. rest_version 114 | -- return 115 | return major_version .. '/' .. dispatcher[method].controller, dispatcher[method].action, params, request 116 | end 117 | end 118 | end 119 | end 120 | 121 | -- call the controller 122 | function Router.call_controller(request, controller_name, action, params) 123 | -- load matched controller and set metatable to new instance of controller 124 | local matched_controller = require(controller_name) 125 | setmetatable(matched_controller, Controller) 126 | local controller_instance = Controller.new(request, params) 127 | setmetatable(controller_instance, {__index = matched_controller}) 128 | 129 | -- call action 130 | local ok, status_or_error, body, headers = pcall(function() return matched_controller[action](controller_instance) end) 131 | 132 | local response 133 | 134 | if ok then 135 | -- successful 136 | response = Response.new({ status = status_or_error, headers = headers, body = body }) 137 | else 138 | -- controller raised an error 139 | local ok, err = pcall(function() return Error.new(status_or_error.code, status_or_error.custom_attrs) end) 140 | 141 | if ok then 142 | -- API error 143 | response = Response.new({ status = err.status, headers = err.headers, body = err.body }) 144 | else 145 | -- another error, throw 146 | error(status_or_error) 147 | end 148 | end 149 | 150 | return response 151 | end 152 | 153 | function Router.respond(ngx, response) 154 | -- set status 155 | ngx.status = response.status 156 | -- set headers 157 | for k, v in pairs(response.headers) do 158 | ngx.header[k] = v 159 | end 160 | -- encode body 161 | local json_body = jencode(response.body) 162 | -- ensure content-length is set 163 | ngx.header["Content-Length"] = ngx.header["Content-Length"] or ngx.header["content-length"] or json_body:len() 164 | -- print body 165 | ngx.print(json_body) 166 | end 167 | 168 | return Router 169 | -------------------------------------------------------------------------------- /gin/core/routes.lua: -------------------------------------------------------------------------------- 1 | -- perf 2 | local error = error 3 | local pairs = pairs 4 | local setmetatable = setmetatable 5 | local sgsub = string.gsub 6 | local smatch = string.match 7 | local tostring = tostring 8 | local type = type 9 | local function tappend(t, v) t[#t+1] = v end 10 | 11 | 12 | -- versions 13 | local Version = {} 14 | Version.__index = Version 15 | 16 | function Version.new(routes, number) 17 | if type(number) ~= 'number' then error("version is not an integer number (got string).") end 18 | if smatch(tostring(number), "%.") ~= nil then error("version is not an integer number (got float).") end 19 | 20 | local instance = { 21 | routes = routes, 22 | number = number 23 | } 24 | setmetatable(instance, Version) 25 | return instance 26 | end 27 | 28 | 29 | function Version:add(method, pattern, route_info) 30 | local pattern, params = self:build_named_parameters(pattern) 31 | 32 | pattern = "^" .. pattern .. "/???$" 33 | 34 | route_info.controller = route_info.controller .. "_controller" 35 | route_info.params = params 36 | 37 | tappend(self.routes.dispatchers[self.number], { pattern = pattern, [method] = route_info }) 38 | end 39 | 40 | function Version:build_named_parameters(pattern) 41 | local params = {} 42 | local new_pattern = sgsub(pattern, "/:([A-Za-z0-9_]+)", function(m) 43 | tappend(params, m) 44 | return "/([A-Za-z0-9_]+)" 45 | end) 46 | return new_pattern, params 47 | end 48 | 49 | local supported_http_methods = { 50 | GET = true, 51 | POST = true, 52 | HEAD = true, 53 | OPTIONS = true, 54 | PUT = true, 55 | PATCH = true, 56 | DELETE = true, 57 | TRACE = true, 58 | CONNECT = true 59 | } 60 | 61 | for http_method, _ in pairs(supported_http_methods) do 62 | Version[http_method] = function(self, pattern, route_info) 63 | self:add(http_method, pattern, route_info) 64 | end 65 | end 66 | 67 | 68 | -- routes 69 | local Routes = {} 70 | Routes.dispatchers = {} 71 | 72 | function Routes.version(number) 73 | local version = Version.new(Routes, number) 74 | 75 | if Routes.dispatchers[number] then error("version has already been defined (got " .. number .. ").") end 76 | Routes.dispatchers[number] = {} 77 | 78 | return version 79 | end 80 | 81 | return Routes 82 | -------------------------------------------------------------------------------- /gin/core/settings.lua: -------------------------------------------------------------------------------- 1 | -- gin 2 | local helpers = require 'gin.helpers.common' 3 | 4 | -- perf 5 | local pairs = pairs 6 | 7 | 8 | local GinSettings = {} 9 | 10 | GinSettings.defaults = { 11 | development = { 12 | code_cache = false, 13 | port = 7200, 14 | expose_api_console = true 15 | }, 16 | 17 | test = { 18 | code_cache = true, 19 | port = 7201, 20 | expose_api_console = false 21 | }, 22 | 23 | production = { 24 | code_cache = true, 25 | port = 80, 26 | expose_api_console = false 27 | }, 28 | 29 | other = { 30 | code_cache = true, 31 | port = 80, 32 | expose_api_console = false 33 | } 34 | } 35 | 36 | function GinSettings.for_environment(env) 37 | -- load defaults 38 | local settings = GinSettings.defaults[env] 39 | if settings == nil then settings = GinSettings.defaults.other end 40 | 41 | -- override defaults from app settings 42 | local app_settings = helpers.try_require('config.settings', {}) 43 | 44 | if app_settings ~= nil then 45 | local app_settings_env = app_settings[env] 46 | if app_settings_env ~= nil then 47 | for k, v in pairs(app_settings_env) do 48 | settings[k] = v 49 | end 50 | end 51 | end 52 | 53 | return settings 54 | end 55 | 56 | return GinSettings 57 | -------------------------------------------------------------------------------- /gin/db/migrations.lua: -------------------------------------------------------------------------------- 1 | -- detached 2 | require 'gin.core.detached' 3 | 4 | -- gin 5 | local Gin = require 'gin.core.gin' 6 | local helpers = require 'gin.helpers.common' 7 | 8 | -- settings 9 | local accepted_adapters = { "mysql", "postgresql" } 10 | 11 | 12 | local Migrations = {} 13 | Migrations.migrations_table_name = 'schema_migrations' 14 | 15 | 16 | local create_schema_migrations_sql = [[ 17 | CREATE TABLE ]] .. Migrations.migrations_table_name .. [[ ( 18 | version varchar(14) NOT NULL, 19 | PRIMARY KEY (version) 20 | ); 21 | ]] 22 | 23 | local function create_db(db) 24 | local db_name = db.options.database 25 | -- use default db 26 | db.options.database = db.adapter.default_database 27 | -- create 28 | db:execute("CREATE DATABASE " .. db_name .. ";") 29 | -- revert db name 30 | db.options.database = db_name 31 | end 32 | 33 | local function is_database_not_found_error(err) 34 | local db_not_found = string.match(err, "Unknown database '.+'") -- mysql match 35 | if db_not_found == nil then 36 | db_not_found = string.match(err, 'database ".+" does not exist') -- postgresql match 37 | end 38 | return db_not_found ~= nil 39 | end 40 | 41 | local function ensure_db_and_schema_migrations_exist(db) 42 | local ok, tables = pcall(function() return db:tables() end) 43 | if ok == false then 44 | if is_database_not_found_error(tables) == true then 45 | -- database does not exist, create 46 | create_db(db) 47 | tables = db:tables() 48 | else 49 | error(migration_module) 50 | end 51 | end 52 | 53 | -- chech if exists 54 | for _, table_name in pairs(tables) do 55 | if table_name == Migrations.migrations_table_name then 56 | -- table found, exit 57 | return 58 | end 59 | end 60 | -- table does not exist, create 61 | db:execute(create_schema_migrations_sql) 62 | end 63 | 64 | function Migrations.version_already_run(db, version) 65 | local res = db:execute("SELECT version FROM " .. Migrations.migrations_table_name .. " WHERE version = '" .. version .. "';") 66 | return #res > 0 67 | end 68 | 69 | local function add_version(db, version) 70 | db:execute("INSERT INTO " .. Migrations.migrations_table_name .. " (version) VALUES ('" .. version .. "');") 71 | end 72 | 73 | local function remove_version(db, version) 74 | db:execute("DELETE FROM " .. Migrations.migrations_table_name .. " WHERE version = '" .. version .. "';") 75 | end 76 | 77 | local function version_from(module_name) 78 | return string.match(module_name, ".*/([^_$]*)") 79 | end 80 | 81 | local function dump_schema_for(db) 82 | local schema_dump_file_path = Gin.app_dirs.schemas .. '/' .. db.options.adapter .. '-' .. db.options.database .. '.lua' 83 | local schema = db:schema() 84 | -- write to file 85 | helpers.pp_to_file(schema, schema_dump_file_path) 86 | end 87 | 88 | -- get migration modules 89 | function Migrations.migration_modules() 90 | return helpers.table_order(helpers.module_names_in_path(Gin.app_dirs.migrations)) 91 | end 92 | 93 | function Migrations.migration_modules_reverse() 94 | return helpers.table_order(helpers.module_names_in_path(Gin.app_dirs.migrations), false) 95 | end 96 | 97 | local function run_migration(direction, module_name) 98 | local version = version_from(module_name) 99 | local migration_module = require(module_name) 100 | local db = migration_module.db 101 | 102 | -- check adapter is supported 103 | if helpers.included_in_table(accepted_adapters, db.options.adapter) == false then 104 | err_message = "Cannot run migrations for the adapter '" .. db.options.adapter .. "'. Supported adapters are: '" .. table.concat(accepted_adapters, "', '") .. "'." 105 | return false, version, err_message 106 | end 107 | 108 | if direction == "up" then ensure_db_and_schema_migrations_exist(db) end 109 | 110 | -- exit if version already run 111 | local should_run = direction == "up" 112 | if Migrations.version_already_run(db, version) == should_run then return end 113 | 114 | -- run migration 115 | local ok, err = pcall(function() return migration_module[direction]() end) 116 | 117 | if ok == true then 118 | -- track version 119 | if direction == "up" then 120 | add_version(db, version) 121 | else 122 | remove_version(db, version) 123 | end 124 | 125 | -- dump schema 126 | dump_schema_for(db) 127 | else 128 | return false, version, err 129 | end 130 | 131 | -- return result 132 | return ok, version, err 133 | end 134 | 135 | local function migrate(direction) 136 | local response = {} 137 | 138 | -- get modules 139 | local modules 140 | 141 | if direction == "up" then 142 | modules = Migrations.migration_modules() 143 | else 144 | modules = Migrations.migration_modules_reverse() 145 | end 146 | 147 | -- loop migration modules & build response 148 | for _, module_name in ipairs(modules) do 149 | local ok, version, err = run_migration(direction, module_name) 150 | 151 | if version ~= nil then 152 | table.insert(response, { version = version, error = err }) 153 | end 154 | 155 | if ok == false then 156 | -- an error occurred 157 | return false, response 158 | end 159 | 160 | if direction == "down" and version ~= nil then break end 161 | end 162 | 163 | -- return response 164 | return true, response 165 | end 166 | 167 | function Migrations.up() 168 | return migrate("up") 169 | end 170 | 171 | function Migrations.down() 172 | return migrate("down") 173 | end 174 | 175 | return Migrations 176 | -------------------------------------------------------------------------------- /gin/db/sql.lua: -------------------------------------------------------------------------------- 1 | -- perf 2 | local error = error 3 | local pairs = pairs 4 | local require = require 5 | local setmetatable = setmetatable 6 | local tconcat = table.concat 7 | local function tappend(t, v) t[#t+1] = v end 8 | 9 | local SqlDatabase = {} 10 | SqlDatabase.__index = SqlDatabase 11 | 12 | 13 | function SqlDatabase.new(options) 14 | -- check for required params 15 | local required_options = { 16 | adapter = true, 17 | host = true, 18 | port = true, 19 | database = true, 20 | user = true, 21 | password = true, 22 | pool = true 23 | } 24 | for k, _ in pairs(options) do required_options[k] = nil end 25 | local missing_options = {} 26 | for k, _ in pairs(required_options) do tappend(missing_options, k) end 27 | 28 | if #missing_options > 0 then error("missing required database options: " .. tconcat(missing_options, ', ')) end 29 | 30 | -- init adapter 31 | local adapter = require('gin.db.sql.' .. options.adapter .. '.adapter') 32 | 33 | -- init instance 34 | local instance = { 35 | options = options, 36 | adapter = adapter 37 | } 38 | setmetatable(instance, SqlDatabase) 39 | 40 | return instance 41 | end 42 | 43 | 44 | function SqlDatabase:execute(sql) 45 | return self.adapter.execute(self.options, sql) 46 | end 47 | 48 | function SqlDatabase:execute_and_return_last_id(sql) 49 | return self.adapter.execute_and_return_last_id(self.options, sql) 50 | end 51 | 52 | 53 | function SqlDatabase:quote(str) 54 | return self.adapter.quote(self.options, str) 55 | end 56 | 57 | function SqlDatabase:tables() 58 | return self.adapter.tables(self.options) 59 | end 60 | 61 | function SqlDatabase:schema() 62 | return self.adapter.schema(self.options) 63 | end 64 | 65 | return SqlDatabase 66 | -------------------------------------------------------------------------------- /gin/db/sql/common/orm.lua: -------------------------------------------------------------------------------- 1 | -- perf 2 | local next = next 3 | local pairs = pairs 4 | local setmetatable = setmetatable 5 | local tconcat = table.concat 6 | local type = type 7 | local function tappend(t, v) t[#t+1] = v end 8 | 9 | -- field and values helper 10 | local function field_and_values(quote, attrs, concat) 11 | local fav = {} 12 | for field, value in pairs(attrs) do 13 | local key_pair = {} 14 | tappend(key_pair, field) 15 | if type(value) ~= 'number' then value = quote(value) end 16 | tappend(key_pair, "=") 17 | tappend(key_pair, value) 18 | 19 | tappend(fav, tconcat(key_pair)) 20 | end 21 | return tconcat(fav, concat) 22 | end 23 | 24 | -- where 25 | local function build_where(self, sql, attrs) 26 | if attrs ~= nil then 27 | if type(attrs) == 'table' then 28 | if next(attrs) ~= nil then 29 | tappend(sql, " WHERE (") 30 | tappend(sql, field_and_values(self.quote, attrs, ' AND ')) 31 | tappend(sql, ")") 32 | end 33 | else 34 | tappend(sql, " WHERE (") 35 | tappend(sql, attrs) 36 | tappend(sql, ")") 37 | end 38 | end 39 | end 40 | 41 | 42 | local SqlCommonOrm = {} 43 | SqlCommonOrm.__index = SqlCommonOrm 44 | 45 | function SqlCommonOrm.new(table_name, quote_fun) 46 | -- init instance 47 | local instance = { 48 | table_name = table_name, 49 | quote = quote_fun 50 | } 51 | setmetatable(instance, SqlCommonOrm) 52 | 53 | return instance 54 | end 55 | 56 | 57 | function SqlCommonOrm:create(attrs) 58 | -- health check 59 | if attrs == nil or next(attrs) == nil then 60 | error("no attributes were specified to create new model instance") 61 | end 62 | -- init sql 63 | local sql = {} 64 | -- build fields 65 | local fields = {} 66 | local values = {} 67 | for field, value in pairs(attrs) do 68 | tappend(fields, field) 69 | if type(value) ~= 'number' then value = self.quote(value) end 70 | tappend(values, value) 71 | end 72 | -- build sql 73 | tappend(sql, "INSERT INTO ") 74 | tappend(sql, self.table_name) 75 | tappend(sql, " (") 76 | tappend(sql, tconcat(fields, ',')) 77 | tappend(sql, ") VALUES (") 78 | tappend(sql, tconcat(values, ',')) 79 | tappend(sql, ");") 80 | -- hit server 81 | return tconcat(sql) 82 | end 83 | 84 | function SqlCommonOrm:where(attrs, options) 85 | -- init sql 86 | local sql = {} 87 | -- start 88 | tappend(sql, "SELECT * FROM ") 89 | tappend(sql, self.table_name) 90 | -- where 91 | build_where(self, sql, attrs) 92 | -- options 93 | if options then 94 | -- order 95 | if options.order ~= nil then 96 | tappend(sql, " ORDER BY ") 97 | tappend(sql, options.order) 98 | end 99 | -- limit 100 | if options.limit ~= nil then 101 | tappend(sql, " LIMIT ") 102 | tappend(sql, options.limit) 103 | end 104 | -- offset 105 | if options.offset ~= nil then 106 | tappend(sql, " OFFSET ") 107 | tappend(sql, options.offset) 108 | end 109 | end 110 | -- close 111 | tappend(sql, ";") 112 | -- execute 113 | return tconcat(sql) 114 | end 115 | 116 | function SqlCommonOrm:delete_where(attrs, options) 117 | -- init sql 118 | local sql = {} 119 | -- start 120 | tappend(sql, "DELETE FROM ") 121 | tappend(sql, self.table_name) 122 | -- where 123 | build_where(self, sql, attrs) 124 | -- options 125 | if options then 126 | -- limit 127 | if options.limit ~= nil then 128 | tappend(sql, " LIMIT ") 129 | tappend(sql, options.limit) 130 | end 131 | end 132 | -- close 133 | tappend(sql, ";") 134 | -- execute 135 | return tconcat(sql) 136 | end 137 | 138 | function SqlCommonOrm:update_where(attrs, where_attrs) 139 | -- health check 140 | if attrs == nil or next(attrs) == nil then 141 | error("no attributes were specified to create new model instance") 142 | end 143 | -- init sql 144 | local sql = {} 145 | -- start 146 | tappend(sql, "UPDATE ") 147 | tappend(sql, self.table_name) 148 | tappend(sql, " SET ") 149 | -- updates 150 | tappend(sql, field_and_values(self.quote, attrs, ',')) 151 | -- where 152 | build_where(self, sql, where_attrs) 153 | -- close 154 | tappend(sql, ";") 155 | -- execute 156 | return tconcat(sql) 157 | end 158 | 159 | return SqlCommonOrm 160 | -------------------------------------------------------------------------------- /gin/db/sql/mysql/adapter.lua: -------------------------------------------------------------------------------- 1 | -- perf 2 | local error = error 3 | local ipairs = ipairs 4 | local pairs = pairs 5 | local require = require 6 | local tonumber = tonumber 7 | local function tappend(t, v) t[#t+1] = v end 8 | 9 | -- settings 10 | local timeout_subsequent_ops = 1000 -- 1 sec 11 | local max_idle_timeout = 10000 -- 10 sec 12 | local max_packet_size = 1024 * 1024 -- 1MB 13 | 14 | 15 | local MySql = {} 16 | MySql.default_database = 'mysql' 17 | 18 | local function mysql_connect(options) 19 | -- ini mysql 20 | local mysql = require "resty.mysql" 21 | -- create sql object 22 | local db, err = mysql:new() 23 | if not db then error("failed to instantiate mysql: " .. err) end 24 | -- set 1 second timeout for suqsequent operations 25 | db:set_timeout(timeout_subsequent_ops) 26 | -- connect to db 27 | local db_options = { 28 | host = options.host, 29 | port = options.port, 30 | database = options.database, 31 | user = options.user, 32 | password = options.password, 33 | max_packet_size = max_packet_size 34 | } 35 | local ok, err, errno, sqlstate = db:connect(db_options) 36 | if not ok then error("failed to connect to mysql: " .. err .. ": " .. errno .. " " .. sqlstate) end 37 | -- return 38 | return db 39 | end 40 | 41 | local function mysql_keepalive(db, options) 42 | -- put it into the connection pool 43 | local ok, err = db:set_keepalive(max_idle_timeout, options.pool) 44 | if not ok then error("failed to set mysql keepalive: ", err) end 45 | end 46 | 47 | -- quote 48 | function MySql.quote(options, str) 49 | return ngx.quote_sql_str(str) 50 | end 51 | 52 | -- return list of tables 53 | function MySql.tables(options) 54 | local res = MySql.execute(options, "SHOW TABLES IN " .. options.database .. ";") 55 | local tables = {} 56 | 57 | for _, v in pairs(res) do 58 | for _, table_name in pairs(v) do 59 | tappend(tables, table_name) 60 | end 61 | end 62 | 63 | return tables 64 | end 65 | 66 | -- return schema as a table 67 | function MySql.schema(options) 68 | local Migration = require 'gin.db.sql.migrations' 69 | local schema = {} 70 | 71 | local tables = MySql.tables(options) 72 | for i, table_name in ipairs(tables) do 73 | if table_name ~= Migration.migrations_table_name then 74 | local columns_info = MySql.execute(options, "SHOW COLUMNS IN " .. table_name .. ";") 75 | tappend(schema, { [table_name] = columns_info }) 76 | end 77 | end 78 | 79 | return schema 80 | end 81 | 82 | -- execute query on db 83 | local function db_execute(options, db, sql) 84 | local res, err, errno, sqlstate = db:query(sql) 85 | if not res then error("bad mysql result: " .. err .. ": " .. errno .. " " .. sqlstate) end 86 | -- return 87 | return res 88 | end 89 | 90 | -- execute a query 91 | function MySql.execute(options, sql) 92 | -- get db object 93 | local db = mysql_connect(options) 94 | -- execute query 95 | local res = db_execute(options, db, sql) 96 | -- keepalive 97 | mysql_keepalive(db, options) 98 | -- return 99 | return res 100 | end 101 | 102 | --- Execute a query and return the last ID 103 | function MySql.execute_and_return_last_id(options, sql, id_col) 104 | -- get db object 105 | local db = mysql_connect(options) 106 | -- execute query 107 | db_execute(options, db, sql) 108 | -- get last id 109 | local id_col = id_col 110 | local res = db_execute(options, db, "SELECT LAST_INSERT_ID() AS " .. id_col .. ";") 111 | -- keepalive 112 | mysql_keepalive(db, options) 113 | return tonumber(res[1][id_col]) 114 | end 115 | 116 | return MySql 117 | -------------------------------------------------------------------------------- /gin/db/sql/mysql/adapter_detached.lua: -------------------------------------------------------------------------------- 1 | -- dep 2 | local dbi = require 'DBI' 3 | 4 | -- gin 5 | local Gin = require 'gin.core.gin' 6 | local helpers = require 'gin.helpers.common' 7 | 8 | -- perf 9 | local assert = assert 10 | local ipairs = ipairs 11 | local pairs = pairs 12 | local pcall = pcall 13 | local setmetatable = setmetatable 14 | local smatch = string.match 15 | local tonumber = tonumber 16 | local function tappend(t, v) t[#t+1] = v end 17 | 18 | 19 | local MySql = {} 20 | MySql.default_database = 'mysql' 21 | 22 | local function mysql_connect(options) 23 | local db = assert(dbi.Connect("MySQL", options.database, options.user, options.password, options.host, options.port)) 24 | db:autocommit(true) 25 | 26 | return db 27 | end 28 | 29 | local function mysql_close(db) 30 | db:close() 31 | end 32 | 33 | -- quote 34 | function MySql.quote(options, str) 35 | local db = mysql_connect(options) 36 | local quoted_str = "'" .. db:quote(str) .. "'" 37 | mysql_close(db) 38 | return quoted_str 39 | end 40 | 41 | -- return list of tables 42 | function MySql.tables(options) 43 | local res = MySql.execute(options, "SHOW TABLES IN " .. options.database .. ";") 44 | 45 | local tables = {} 46 | 47 | for _, v in pairs(res) do 48 | for _, table_name in pairs(v) do 49 | tappend(tables, table_name) 50 | end 51 | end 52 | 53 | return tables 54 | end 55 | 56 | -- return schema as a table 57 | function MySql.schema(options) 58 | local Migration = require 'gin.db.migrations' 59 | local schema = {} 60 | 61 | local tables = MySql.tables(options) 62 | for _, table_name in ipairs(tables) do 63 | if table_name ~= Migration.migrations_table_name then 64 | local table_info = MySql.execute(options, "SHOW COLUMNS IN " .. table_name .. ";") 65 | tappend(schema, { [table_name] = table_info }) 66 | end 67 | end 68 | 69 | return schema 70 | end 71 | 72 | -- execute query on db 73 | local function db_execute(db, sql) 74 | -- execute 75 | local sth = assert(db:prepare(sql)) 76 | local ok, err = sth:execute() 77 | if ok == false then error(err) end 78 | -- get first returned row (if any) 79 | local ok, row = pcall(function() return sth:fetch(true) end) 80 | if ok == false then row = nil end 81 | return sth, row 82 | end 83 | 84 | -- execute a query 85 | function MySql.execute(options, sql) 86 | -- connect 87 | local db = mysql_connect(options) 88 | -- execute 89 | local sth, row = db_execute(db, sql) 90 | if row == nil then return {} end 91 | -- build res 92 | local res = {} 93 | while row do 94 | local irow = helpers.shallowcopy(row) 95 | tappend(res, irow) 96 | row = sth:fetch(true) 97 | end 98 | -- close 99 | sth:close() 100 | mysql_close(db) 101 | -- return 102 | return res 103 | end 104 | 105 | -- execute a query and return the last ID 106 | function MySql.execute_and_return_last_id(options, sql, id_col) 107 | -- connect 108 | local db = mysql_connect(options) 109 | -- execute sql 110 | local sth, row = db_execute(db, sql) 111 | sth:close() 112 | -- get last id 113 | local sth, row = db_execute(db, "SELECT BINARY LAST_INSERT_ID() AS " .. id_col .. ";") 114 | local id = row[id_col] 115 | -- close 116 | sth:close() 117 | mysql_close(db) 118 | -- return 119 | return tonumber(id) 120 | end 121 | 122 | return MySql 123 | -------------------------------------------------------------------------------- /gin/db/sql/mysql/orm.lua: -------------------------------------------------------------------------------- 1 | local MySqlOrm = require 'gin.db.sql.common.orm' 2 | return MySqlOrm 3 | -------------------------------------------------------------------------------- /gin/db/sql/orm.lua: -------------------------------------------------------------------------------- 1 | -- perf 2 | local require = require 3 | local function tappend(t, v) t[#t+1] = v end 4 | 5 | 6 | local SqlOrm = {} 7 | 8 | --- Define a model. 9 | -- The default primary key is set to 'id' 10 | -- @param sql_database the sql database instance 11 | -- @param table_name the name of the table to create a lightweight orm mapping for 12 | -- @param id_col set to true to use table_name .. '_id' as primary key, 13 | -- set to arbitrary string to use any other column as primary key 14 | function SqlOrm.define_model(sql_database, table_name, id_col) 15 | local GinModel = {} 16 | GinModel.__index = GinModel 17 | if true == id_col then 18 | id_col = table_name .. '_id' 19 | elseif id_col then 20 | id_col = tostring(id_col) 21 | else 22 | id_col = 'id' -- backward compatible default 23 | end 24 | GinModel.__id_col = id_col 25 | 26 | -- init 27 | local function quote(str) 28 | return sql_database:quote(str) 29 | end 30 | local orm = require('gin.db.sql.' .. sql_database.options.adapter .. '.orm').new(table_name, quote) 31 | 32 | function GinModel.new(attrs) 33 | local instance = attrs or {} 34 | setmetatable(instance, GinModel) 35 | return instance 36 | end 37 | 38 | function GinModel.create(attrs) 39 | local sql = orm:create(attrs) 40 | local id_col = GinModel.__id_col 41 | local id = sql_database:execute_and_return_last_id(sql, id_col) 42 | 43 | local model = GinModel.new(attrs) 44 | model[id_col] = id 45 | 46 | return model 47 | end 48 | 49 | function GinModel.where(attrs, options) 50 | local sql = orm:where(attrs, options) 51 | local results = sql_database:execute(sql) 52 | 53 | local models = {} 54 | for i = 1, #results do 55 | tappend(models, GinModel.new(results[i])) 56 | end 57 | return models 58 | end 59 | 60 | function GinModel.all(options) 61 | return GinModel.where({}, options) 62 | end 63 | 64 | function GinModel.find_by(attrs, options) 65 | local merged_options = { limit = 1 } 66 | if options and options.order then 67 | merged_options.order = options.order 68 | end 69 | 70 | return GinModel.where(attrs, merged_options)[1] 71 | end 72 | 73 | function GinModel.delete_where(attrs, options) 74 | local sql = orm:delete_where(attrs, options) 75 | return sql_database:execute(sql) 76 | end 77 | 78 | function GinModel.delete_all(options) 79 | return GinModel.delete_where({}, options) 80 | end 81 | 82 | function GinModel.update_where(attrs, options) 83 | local sql = orm:update_where(attrs, options) 84 | return sql_database:execute(sql) 85 | end 86 | 87 | function GinModel:save() 88 | local id_col = GinModel.__id_col 89 | local id = self[id_col] 90 | if id ~= nil then 91 | self[id_col] = nil 92 | local result = GinModel.update_where(self, { [id_col] = id }) 93 | self[id_col] = id 94 | return result 95 | else 96 | return GinModel.create(self) 97 | end 98 | end 99 | 100 | function GinModel:delete() 101 | local id_col = GinModel.__id_col 102 | local id = self[id_col] 103 | if id ~= nil then 104 | return GinModel.delete_where({ [id_col] = id }) 105 | else 106 | error("cannot delete a model without an id") 107 | end 108 | end 109 | 110 | return GinModel 111 | end 112 | 113 | 114 | return SqlOrm 115 | -------------------------------------------------------------------------------- /gin/db/sql/postgresql/adapter.lua: -------------------------------------------------------------------------------- 1 | -- gin 2 | local postgresql_helpers = require 'gin.db.sql.postgresql.helpers' 3 | 4 | -- perf 5 | local error = error 6 | local require = require 7 | local smatch = string.match 8 | local tonumber = tonumber 9 | local function tappend(t, v) t[#t+1] = v end 10 | 11 | 12 | local PostgreSql = {} 13 | PostgreSql.default_database = 'postgres' 14 | 15 | 16 | -- locations 17 | function PostgreSql.location_for(options) 18 | return postgresql_helpers.location_for(options) 19 | end 20 | 21 | function PostgreSql.execute_location_for(options) 22 | return postgresql_helpers.execute_location_for(options) 23 | end 24 | 25 | -- quote 26 | function PostgreSql.quote(options, str) 27 | return ndk.set_var.set_quote_pgsql_str(str) 28 | end 29 | 30 | -- return list of tables 31 | function PostgreSql.tables(options) 32 | local sql = "SELECT table_name FROM information_schema.tables WHERE table_catalog='" .. options.database .. "' AND table_schema = 'public';" 33 | local res = PostgreSql.execute(options, sql) 34 | local tables = {} 35 | 36 | for _, v in pairs(res) do 37 | for _, table_name in pairs(v) do 38 | tappend(tables, table_name) 39 | end 40 | end 41 | 42 | return tables 43 | end 44 | 45 | -- return schema as a table 46 | function PostgreSql.schema(options) 47 | local Migration = require 'gin.db.sql.migrations' 48 | local schema = {} 49 | 50 | local tables = PostgreSql.tables(options) 51 | for i, table_name in ipairs(tables) do 52 | if table_name ~= Migration.migrations_table_name then 53 | local sql = "SELECT column_name, column_default, is_nullable, data_type, character_maximum_length, numeric_precision, datetime_precision FROM information_schema.columns WHERE table_name ='" .. table_name .. "';" 54 | local columns_info = PostgreSql.execute(options, "SHOW COLUMNS IN " .. sql .. ";") 55 | tappend(schema, { [table_name] = columns_info }) 56 | end 57 | end 58 | 59 | return schema 60 | end 61 | 62 | -- execute query on db 63 | local function db_execute(options, db, sql) 64 | local location = PostgreSql.execute_location_for(options) 65 | 66 | -- execute query 67 | local resp = ngx.location.capture("/" .. location, { 68 | method = ngx.HTTP_POST, body = sql 69 | }) 70 | if resp.status ~= ngx.HTTP_OK or not resp.body then error("failed to query postgresql") end 71 | 72 | -- parse response 73 | local parser = require "rds.parser" 74 | local parsed_res, err = parser.parse(resp.body) 75 | if parsed_res == nil then error("failed to parse RDS: " .. err) end 76 | 77 | local rows = parsed_res.resultset 78 | if not rows or #rows == 0 then 79 | -- empty resultset 80 | return {} 81 | else 82 | return rows 83 | end 84 | end 85 | 86 | -- execute a query 87 | function PostgreSql.execute(options, sql) 88 | return db_execute(options, db, sql) 89 | end 90 | 91 | local function append_to_sql(sql, append_sql) 92 | local sql_without_last_semicolon = smatch(sql, "(.*);") 93 | if sql_without_last_semicolon ~= nil then 94 | sql = sql_without_last_semicolon 95 | end 96 | return sql_without_last_semicolon .. append_sql 97 | end 98 | 99 | -- execute a query and return the last ID 100 | function PostgreSql.execute_and_return_last_id(options, sql, id_col) 101 | -- execute query and get last id 102 | sql = append_to_sql(sql, " RETURNING " .. id_col .. ";") 103 | local res = db_execute(options, db, sql) 104 | return tonumber(res[1][id_col]) 105 | end 106 | 107 | return PostgreSql 108 | -------------------------------------------------------------------------------- /gin/db/sql/postgresql/adapter_detached.lua: -------------------------------------------------------------------------------- 1 | -- dep 2 | local dbi = require 'DBI' 3 | 4 | -- gin 5 | local Gin = require 'gin.core.gin' 6 | local helpers = require 'gin.helpers.common' 7 | local postgresql_helpers = require 'gin.db.sql.postgresql.helpers' 8 | 9 | -- perf 10 | local assert = assert 11 | local ipairs = ipairs 12 | local pairs = pairs 13 | local pcall = pcall 14 | local setmetatable = setmetatable 15 | local smatch = string.match 16 | local tonumber = tonumber 17 | local function tappend(t, v) t[#t+1] = v end 18 | 19 | 20 | local PostgreSql = {} 21 | PostgreSql.default_database = 'postgres' 22 | 23 | 24 | -- locations 25 | function PostgreSql.location_for(options) 26 | return postgresql_helpers.location_for(options) 27 | end 28 | 29 | function PostgreSql.execute_location_for(options) 30 | return postgresql_helpers.execute_location_for(options) 31 | end 32 | 33 | local function postgresql_connect(options) 34 | local db = assert(dbi.Connect("PostgreSQL", options.database, options.user, options.password, options.host, options.port)) 35 | db:autocommit(true) 36 | 37 | return db 38 | end 39 | 40 | local function postgresql_close(db) 41 | db:close() 42 | end 43 | 44 | -- quote 45 | function PostgreSql.quote(options, str) 46 | local db = postgresql_connect(options) 47 | local quoted_str = "'" .. db:quote(str) .. "'" 48 | postgresql_close(db) 49 | return quoted_str 50 | end 51 | 52 | -- return list of tables 53 | function PostgreSql.tables(options) 54 | local sql = "SELECT table_name FROM information_schema.tables WHERE table_catalog='" .. options.database .. "' AND table_schema = 'public';" 55 | local res = PostgreSql.execute(options, sql) 56 | 57 | local tables = {} 58 | 59 | for _, v in pairs(res) do 60 | for _, table_name in pairs(v) do 61 | tappend(tables, table_name) 62 | end 63 | end 64 | 65 | return tables 66 | end 67 | 68 | -- return schema as a table 69 | function PostgreSql.schema(options) 70 | local Migration = require 'gin.db.migrations' 71 | local schema = {} 72 | 73 | local tables = PostgreSql.tables(options) 74 | for _, table_name in ipairs(tables) do 75 | if table_name ~= Migration.migrations_table_name then 76 | local sql = "SELECT column_name, column_default, is_nullable, data_type, character_maximum_length, numeric_precision, datetime_precision FROM information_schema.columns WHERE table_name ='" .. table_name .. "';" 77 | local table_info = PostgreSql.execute(options, sql) 78 | tappend(schema, { [table_name] = table_info }) 79 | end 80 | end 81 | 82 | return schema 83 | end 84 | 85 | -- execute query on db 86 | local function db_execute(db, sql) 87 | -- execute 88 | local sth = assert(db:prepare(sql)) 89 | local ok, err = sth:execute() 90 | if ok == false then error(err) end 91 | -- get first returned row (if any) 92 | local ok, row = pcall(function() return sth:fetch(true) end) 93 | if ok == false then row = nil end 94 | return sth, row 95 | end 96 | 97 | -- execute a query 98 | function PostgreSql.execute(options, sql) 99 | -- connect 100 | local db = postgresql_connect(options) 101 | -- execute 102 | local sth, row = db_execute(db, sql) 103 | if row == nil then return {} end 104 | -- build res 105 | local res = {} 106 | while row do 107 | local irow = helpers.shallowcopy(row) 108 | tappend(res, irow) 109 | row = sth:fetch(true) 110 | end 111 | -- close 112 | sth:close() 113 | postgresql_close(db) 114 | -- return 115 | return res 116 | end 117 | 118 | local function append_to_sql(sql, append_sql) 119 | local sql_without_last_semicolon = smatch(sql, "(.*);") 120 | if sql_without_last_semicolon ~= nil then 121 | sql = sql_without_last_semicolon 122 | end 123 | return sql_without_last_semicolon .. append_sql 124 | end 125 | 126 | -- execute a query and return the last ID 127 | function PostgreSql.execute_and_return_last_id(options, sql, id_col) 128 | -- connect 129 | local db = postgresql_connect(options) 130 | -- execute sql and get last id 131 | sql = append_to_sql(sql, " RETURNING " .. id_col .. ";") 132 | -- get last id 133 | local sth, row = db_execute(db, sql) 134 | local id = row[id_col] 135 | -- close 136 | sth:close() 137 | postgresql_close(db) 138 | -- return 139 | return tonumber(id) 140 | end 141 | 142 | return PostgreSql 143 | -------------------------------------------------------------------------------- /gin/db/sql/postgresql/helpers.lua: -------------------------------------------------------------------------------- 1 | -- perf 2 | local tconcat = table.concat 3 | 4 | 5 | local PostgreSqlHelpers = {} 6 | 7 | -- build location execute name 8 | function PostgreSqlHelpers.location_for(options) 9 | name = { 10 | 'gin', 11 | options.adapter, 12 | options.host, 13 | options.port, 14 | options.database, 15 | } 16 | return tconcat(name, '|') 17 | end 18 | 19 | function PostgreSqlHelpers.execute_location_for(options) 20 | name = { 21 | PostgreSqlHelpers.location_for(options), 22 | 'execute' 23 | } 24 | return tconcat(name, '|') 25 | end 26 | 27 | return PostgreSqlHelpers 28 | -------------------------------------------------------------------------------- /gin/db/sql/postgresql/orm.lua: -------------------------------------------------------------------------------- 1 | local PostgreSqlOrm = require 'gin.db.sql.common.orm' 2 | return PostgreSqlOrm 3 | -------------------------------------------------------------------------------- /gin/helpers/command.lua: -------------------------------------------------------------------------------- 1 | -- gin 2 | local Helpers = require 'gin.helpers.common' 3 | 4 | 5 | -- pretty print 6 | function pp(o) 7 | Helpers.pp(o) 8 | end 9 | -------------------------------------------------------------------------------- /gin/helpers/common.lua: -------------------------------------------------------------------------------- 1 | -- dep 2 | local lfs = require 'lfs' 3 | local prettyprint = require 'pl.pretty' 4 | 5 | -- perf 6 | local assert = assert 7 | local iopen = io.open 8 | local pairs = pairs 9 | local pcall = pcall 10 | local require = require 11 | local sfind = string.find 12 | local sgsub = string.gsub 13 | local smatch = string.match 14 | local tsort = table.sort 15 | local ssub = string.sub 16 | local type = type 17 | local function tappend(t, v) t[#t+1] = v end 18 | local function desc(a, b) return a > b end 19 | 20 | local CommonHelpers = {} 21 | 22 | -- try to require 23 | function CommonHelpers.try_require(module_name, default) 24 | local ok, module_or_err = pcall(function() return require(module_name) end) 25 | 26 | if ok == true then return module_or_err end 27 | 28 | if ok == false and smatch(module_or_err, "'" .. module_name .. "' not found") then 29 | return default 30 | else 31 | error(module_or_err) 32 | end 33 | end 34 | 35 | -- read file 36 | function CommonHelpers.read_file(file_path) 37 | local f = iopen(file_path, "rb") 38 | local content = f:read("*all") 39 | f:close() 40 | return content 41 | end 42 | 43 | -- check if folder exists 44 | function CommonHelpers.folder_exists(folder_path) 45 | return lfs.attributes(sgsub(folder_path, "\\$",""), "mode") == "directory" 46 | end 47 | 48 | -- split function 49 | function CommonHelpers.split(str, pat) 50 | local t = {} 51 | local fpat = "(.-)" .. pat 52 | local last_end = 1 53 | local s, e, cap = sfind(str, fpat, 1) 54 | 55 | while s do 56 | if s ~= 1 or cap ~= "" then 57 | tappend(t,cap) 58 | end 59 | last_end = e+1 60 | s, e, cap = sfind(str, fpat, last_end) 61 | end 62 | 63 | if last_end <= #str then 64 | cap = ssub(str, last_end) 65 | tappend(t, cap) 66 | end 67 | 68 | return t 69 | end 70 | 71 | -- split a path in individual parts 72 | function CommonHelpers.split_path(str) 73 | return CommonHelpers.split(str, '[\\/]+') 74 | end 75 | 76 | -- recursively make directories 77 | function CommonHelpers.mkdirs(file_path) 78 | -- get dir path and parts 79 | local dir_path = smatch(file_path, "(.*)/.*") 80 | local parts = CommonHelpers.split_path(dir_path) 81 | -- loop 82 | local current_dir = nil 83 | for i = 1, #parts do 84 | if current_dir == nil then 85 | current_dir = parts[i] 86 | else 87 | current_dir = current_dir .. '/' .. parts[i] 88 | end 89 | lfs.mkdir(current_dir) 90 | end 91 | end 92 | 93 | -- value in table? 94 | function CommonHelpers.included_in_table(t, value) 95 | for i = 1, #t do 96 | if t[i] == value then return true end 97 | end 98 | return false 99 | end 100 | 101 | function CommonHelpers.table_order(t, ...) 102 | if ... == false then 103 | tsort(t, desc) 104 | elseif ... and type(...) == 'function' then 105 | tsort(t, ...) 106 | else 107 | tsort(t) 108 | end 109 | return t 110 | end 111 | 112 | -- pretty print to file 113 | function CommonHelpers.pp_to_file(o, file_path) 114 | prettyprint.dump(o, file_path) 115 | end 116 | 117 | -- pretty print 118 | function CommonHelpers.pp(o) 119 | prettyprint.dump(o) 120 | end 121 | 122 | -- check if folder exists 123 | function folder_exists(folder_path) 124 | return lfs.attributes(sgsub(folder_path, "\\$",""), "mode") == "directory" 125 | end 126 | 127 | -- get the lua module name 128 | function CommonHelpers.get_lua_module_name(file_path) 129 | return smatch(file_path, "(.*)%.lua") 130 | end 131 | 132 | -- shallow copy of a table 133 | function CommonHelpers.shallowcopy(orig) 134 | local orig_type = type(orig) 135 | local copy 136 | if orig_type == 'table' then 137 | copy = {} 138 | for orig_key, orig_value in pairs(orig) do 139 | copy[orig_key] = orig_value 140 | end 141 | else -- number, string, boolean, etc 142 | copy = orig 143 | end 144 | return copy 145 | end 146 | 147 | function CommonHelpers.module_names_in_path(path) 148 | local modules = {} 149 | 150 | if CommonHelpers.folder_exists(path) then 151 | for file_name in lfs.dir(path) do 152 | if file_name ~= "." and file_name ~= ".." then 153 | local file_path = path .. '/' .. file_name 154 | local attr = lfs.attributes(file_path) 155 | assert(type(attr) == "table") 156 | if attr.mode ~= "directory" then 157 | local module_name = CommonHelpers.get_lua_module_name(file_path) 158 | if module_name ~= nil then 159 | -- add to modules' list 160 | tappend(modules, module_name) 161 | end 162 | end 163 | end 164 | end 165 | end 166 | 167 | return modules 168 | end 169 | 170 | return CommonHelpers 171 | -------------------------------------------------------------------------------- /gin/spec/init.lua: -------------------------------------------------------------------------------- 1 | package.path = './?.lua;' .. package.path 2 | 3 | -- gin 4 | local helpers = require 'gin.helpers.common' 5 | 6 | 7 | -- ensure test environment is specified 8 | local posix = require "posix" 9 | posix.setenv("GIN_ENV", 'test') 10 | 11 | -- detached 12 | require 'gin.core.detached' 13 | 14 | -- helpers 15 | function pp(o) 16 | return helpers.pp(o) 17 | end 18 | -------------------------------------------------------------------------------- /gin/spec/runner.lua: -------------------------------------------------------------------------------- 1 | -- gin 2 | require 'gin.spec.init' 3 | 4 | 5 | -- add integration runner 6 | local IntegrationRunner = require 'gin.spec.runners.integration' 7 | 8 | -- helpers 9 | function hit(request) 10 | return IntegrationRunner.hit(request) 11 | end 12 | -------------------------------------------------------------------------------- /gin/spec/runners/integration.lua: -------------------------------------------------------------------------------- 1 | -- dep 2 | local http = require 'socket.http' 3 | local url = require 'socket.url' 4 | local json = require 'cjson' 5 | local ltn12 = require 'ltn12' 6 | 7 | -- gin 8 | local Gin = require 'gin.core.gin' 9 | local Application = require 'config.application' 10 | 11 | 12 | local IntegrationRunner = {} 13 | 14 | -- Code portion taken from: 15 | -- 16 | function IntegrationRunner.encode_table(args) 17 | if args == nil or next(args) == nil then return "" end 18 | 19 | local strp = "" 20 | for key, vals in pairs(args) do 21 | if type(vals) ~= "table" then vals = {vals} end 22 | 23 | for i, val in ipairs(vals) do 24 | strp = strp .. "&" .. key .. "=" .. url.escape(val) 25 | end 26 | end 27 | 28 | return string.sub(strp, 2) 29 | end 30 | 31 | local function ensure_content_length(request) 32 | if request.headers == nil then request.headers = {} end 33 | if request.headers["content-length"] == nil and request.headers["Content-Length"] == nil then 34 | if request.body ~= nil then 35 | request.headers["Content-Length"] = request.body:len() 36 | else 37 | request.headers["Content-Length"] = 0 38 | end 39 | end 40 | return request 41 | end 42 | 43 | function IntegrationRunner.source_for_caller_at(i) 44 | return debug.getinfo(i).source 45 | end 46 | 47 | local function major_version_for_caller() 48 | local major_version 49 | -- limit to 10 stacktrace items 50 | for i = 1, 10 do 51 | -- local source = debug.getinfo(i).source 52 | local source = IntegrationRunner.source_for_caller_at(i) 53 | if source == nil then break end 54 | 55 | major_version = string.match(source, "controllers/(%d+)/(.*)_spec.lua") 56 | if major_version ~= nil then break end 57 | end 58 | if major_version == nil then error("Could not determine API major version from controller spec file. Ensure to follow naming conventions.") end 59 | 60 | return major_version 61 | end 62 | 63 | local function check_and_get_request_api_version(request, major_version) 64 | local api_version 65 | if request.api_version ~= nil and request.api_version ~= major_version then 66 | if string.match(request.api_version, major_version .. '%.') == nil then 67 | error("Specified API version " .. request.api_version .. " does not match controller spec namespace (" .. major_version .. ")") 68 | end 69 | api_version = request.api_version 70 | else 71 | api_version = major_version 72 | end 73 | 74 | return api_version 75 | end 76 | 77 | local function set_accept_header(request, api_version) 78 | request.headers["accept"] = nil 79 | request.headers["Accept"] = "application/vnd." .. Application.name .. ".v" .. api_version .. "+json" 80 | 81 | return request 82 | end 83 | 84 | local function hit_server(request) 85 | local full_url = url.build({ 86 | scheme = 'http', 87 | host = '127.0.0.1', 88 | port = Gin.settings.port, 89 | path = request.path, 90 | query = IntegrationRunner.encode_table(request.uri_params) 91 | }) 92 | 93 | local response_body = {} 94 | local ok, response_status, response_headers = http.request({ 95 | method = request.method, 96 | url = full_url, 97 | source = ltn12.source.string(request.body), 98 | headers = request.headers, 99 | sink = ltn12.sink.table(response_body), 100 | redirect = false 101 | }) 102 | 103 | response_body = table.concat(response_body, "") 104 | 105 | return ok, response_status, response_headers, response_body 106 | end 107 | 108 | function IntegrationRunner.hit(request) 109 | local launcher = require 'gin.cli.launcher' 110 | local ResponseSpec = require 'gin.spec.runners.response' 111 | 112 | -- convert body to JSON request 113 | if request.body ~= nil then 114 | request.body = json.encode(request.body) 115 | end 116 | 117 | -- ensure content-length is set 118 | request = ensure_content_length(request) 119 | 120 | -- get major version for caller 121 | local major_version = major_version_for_caller() 122 | 123 | -- check request.api_version 124 | local api_version = check_and_get_request_api_version(request, major_version) 125 | 126 | -- set Accept header 127 | request = set_accept_header(request, api_version) 128 | 129 | -- start nginx 130 | launcher.start(Gin.env) 131 | 132 | -- hit server 133 | local ok, response_status, response_headers, response_body = hit_server(request) 134 | 135 | -- stop nginx 136 | launcher.stop(Gin.env) 137 | 138 | if ok == nil then error("An error occurred while connecting to the test server.") end 139 | 140 | -- build response object and return 141 | local response = ResponseSpec.new({ 142 | status = response_status, 143 | headers = response_headers, 144 | body = response_body 145 | }) 146 | 147 | return response 148 | end 149 | 150 | return IntegrationRunner 151 | -------------------------------------------------------------------------------- /gin/spec/runners/response.lua: -------------------------------------------------------------------------------- 1 | -- dep 2 | local json = require 'cjson' 3 | 4 | 5 | local ResponseSpec = {} 6 | ResponseSpec.__index = ResponseSpec 7 | 8 | 9 | local function trim(str) 10 | return str:match'^%s*(.*%S)' or '' 11 | end 12 | 13 | function ResponseSpec.new(options) 14 | options = options or {} 15 | 16 | -- body 17 | local json_body = {} 18 | local ok 19 | if options.body ~= nil and trim(options.body) ~= "" then 20 | ok, json_body = pcall(function() return json.decode(options.body) end) 21 | if ok == false then json_body = nil end 22 | end 23 | 24 | -- init instance 25 | local instance = { 26 | status = options.status, 27 | headers = options.headers or {}, 28 | body = json_body, 29 | body_raw = options.body 30 | } 31 | setmetatable(instance, ResponseSpec) 32 | return instance 33 | end 34 | 35 | return ResponseSpec 36 | -------------------------------------------------------------------------------- /spec/cli/launcher_spec.lua: -------------------------------------------------------------------------------- 1 | require 'spec.spec_helper' 2 | 3 | 4 | describe("Launcher", function() 5 | before_each(function() 6 | package.loaded['gin.core.gin'] = { 7 | env = 'development', 8 | settings = { 9 | port = 12345 10 | }, 11 | app_dirs = { 12 | db = 'db' 13 | } 14 | } 15 | 16 | package.loaded['gin.helpers.common'] = { 17 | read_file = function() return "" end, 18 | module_names_in_path = function() return {} end 19 | } 20 | package.loaded['gin.db.sql.postgresql.adapter'] = nil 21 | end) 22 | 23 | after_each(function() 24 | package.loaded['gin.core.gin'] = nil 25 | package.loaded['gin.helpers.common'] = nil 26 | end) 27 | 28 | describe(".nginx_conf_content", function() 29 | after_each(function() 30 | package.loaded['gin.cli.launcher'] = nil 31 | end) 32 | 33 | it("converts GIN_PORT", function() 34 | package.loaded['gin.helpers.common'].read_file = function() 35 | return "{{GIN_PORT}} part1 {{GIN_PORT}} part3 {{GIN_PORT}}" 36 | end 37 | 38 | local content = require('gin.cli.launcher').nginx_conf_content() 39 | assert.are.equal(true, string.find(content, "12345 part1 12345 part3 12345") ~= nil) 40 | end) 41 | 42 | it("converts GIN_ENV", function() 43 | package.loaded['gin.helpers.common'].read_file = function() 44 | return "{{GIN_ENV}} part1 {{GIN_ENV}} part3 {{GIN_ENV}}" 45 | end 46 | 47 | local content = require('gin.cli.launcher').nginx_conf_content() 48 | 49 | assert.are.equal(true, string.find(content, "development part1 development part3 development") ~= nil) 50 | end) 51 | 52 | describe("{{GIN_INIT}}", function() 53 | before_each(function() 54 | package.loaded['gin.helpers.common'].read_file = function() 55 | return "{{GIN_INIT}}" 56 | end 57 | end) 58 | 59 | describe("code cache", function() 60 | describe("when it is true", function() 61 | before_each(function() 62 | package.loaded['gin.core.gin'].settings.code_cache = true 63 | end) 64 | 65 | it("adds the code cache ON directive", function() 66 | local content = require('gin.cli.launcher').nginx_conf_content() 67 | assert.are.equal(true, string.find(content, "lua_code_cache on") ~= nil) 68 | end) 69 | end) 70 | 71 | describe("when it is false", function() 72 | before_each(function() 73 | package.loaded['gin.core.gin'].settings.code_cache = false 74 | end) 75 | 76 | it("adds the code cache OFF directive", function() 77 | local content = require('gin.cli.launcher').nginx_conf_content() 78 | assert.are.equal(true, string.find(content, "lua_code_cache off") ~= nil) 79 | end) 80 | end) 81 | end) 82 | 83 | describe("PostgreSQL", function() 84 | before_each(function() 85 | package.loaded['gin.helpers.common'].module_names_in_path = function() 86 | return { "db/pgsql", "db/mysql" } 87 | end 88 | 89 | package.loaded['db/pgsql'] = { 90 | adapter = require('gin.db.sql.postgresql.adapter'), 91 | options = { 92 | adapter = "postgresql", 93 | host = "127.15.22.32-example.com", 94 | port = 12345, 95 | database = "demo_development", 96 | user = 'postgresuser', 97 | password = 'posgrespass' 98 | } 99 | } 100 | 101 | package.loaded['db/mysql'] = { 102 | options = { 103 | adapter = "mysql", 104 | } 105 | } 106 | end) 107 | 108 | after_each(function() 109 | package.loaded['db/pgsql'] = nil 110 | package.loaded['db/mysql'] = nil 111 | end) 112 | 113 | it("adds the upstream for the db", function() 114 | local content = require('gin.cli.launcher').nginx_conf_content() 115 | 116 | local name = "gin|postgresql|127%.15%.22%.32%-example.com|12345|demo_development" 117 | local upstream = "upstream " .. name .. " {" 118 | upstream = upstream .. "%s*postgres_server 127%.15%.22%.32%-example.com:12345 dbname=demo_development user=postgresuser password=posgrespass;" 119 | upstream = upstream .. "%s*}" 120 | 121 | assert.are.equal(true, string.find(content, upstream) ~= nil) 122 | end) 123 | end) 124 | end) 125 | 126 | describe("{{GIN_RUNTIME}}", function() 127 | before_each(function() 128 | package.loaded['gin.helpers.common'].read_file = function() 129 | return "{{GIN_RUNTIME}}" 130 | end 131 | end) 132 | 133 | describe("API console", function() 134 | describe("when it is true", function() 135 | before_each(function() 136 | package.loaded['gin.core.gin'].settings.expose_api_console = true 137 | end) 138 | 139 | it("adds the directive", function() 140 | local content = require('gin.cli.launcher').nginx_conf_content() 141 | assert.are.equal(true, string.find(content, "location /ginconsole") ~= nil) 142 | end) 143 | end) 144 | 145 | describe("when it is false", function() 146 | before_each(function() 147 | package.loaded['gin.core.gin'].settings.expose_api_console = false 148 | end) 149 | 150 | it("does not add the directive", function() 151 | local content = require('gin.cli.launcher').nginx_conf_content() 152 | assert.are.equal(true, string.find(content, "location /ginconsole") == nil) 153 | end) 154 | end) 155 | end) 156 | 157 | describe("PostgreSQL", function() 158 | before_each(function() 159 | package.loaded['gin.helpers.common'].module_names_in_path = function() 160 | return { "db/pgsql", "db/mysql" } 161 | end 162 | 163 | package.loaded['db/pgsql'] = { 164 | adapter = require('gin.db.sql.postgresql.adapter'), 165 | options = { 166 | adapter = "postgresql", 167 | host = "127.15.22.32-example.com", 168 | port = 12345, 169 | database = "demo_development", 170 | user = 'postgresuser', 171 | password = 'posgrespass' 172 | } 173 | } 174 | 175 | package.loaded['db/mysql'] = { 176 | options = { 177 | adapter = "mysql", 178 | } 179 | } 180 | end) 181 | 182 | after_each(function() 183 | package.loaded['db/pgsql'] = nil 184 | package.loaded['db/mysql'] = nil 185 | end) 186 | 187 | it("adds the execute location for the db", function() 188 | local content = require('gin.cli.launcher').nginx_conf_content() 189 | 190 | local name = "gin|postgresql|127%.15%.22%.32%-example.com|12345|demo_development" 191 | local location = "location = /" .. name .. "|execute {" 192 | location = location .. "%s*internal;" 193 | location = location .. "%s*postgres_pass%s*" .. name .. ";" 194 | location = location .. "%s*postgres_query%s*$echo_request_body;" 195 | location = location .. "%s*}" 196 | 197 | assert.are.equal(true, string.find(content, location) ~= nil) 198 | end) 199 | end) 200 | end) 201 | end) 202 | end) 203 | -------------------------------------------------------------------------------- /spec/core/controller_spec.lua: -------------------------------------------------------------------------------- 1 | require 'spec.spec_helper' 2 | 3 | 4 | describe("Controller", function() 5 | before_each(function() 6 | Controller = require 'gin.core.controller' 7 | Request = require 'gin.core.request' 8 | 9 | ngx = { 10 | req = { 11 | read_body = function() return end, 12 | get_body_data = function() return '{"param":"value"}' end, 13 | get_headers = function() return {} end, 14 | }, 15 | var = { 16 | uri = "/uri", 17 | request_method = 'POST' 18 | } 19 | } 20 | request = Request.new(ngx) 21 | params = {} 22 | controller = Controller.new(request, params) 23 | end) 24 | 25 | after_each(function() 26 | package.loaded['gin.core.controller'] = nil 27 | package.loaded['gin.core.request'] = nil 28 | ngx = nil 29 | request = nil 30 | params = nil 31 | controller = nil 32 | end) 33 | 34 | describe(".new", function() 35 | it("creates a new instance of a controller with request and params", function() 36 | assert.are.same(request, controller.request) 37 | assert.are.same(params, controller.params) 38 | end) 39 | 40 | it("creates and initializes the controller's request object", function() 41 | assert.are.same({ param = "value" }, controller.request.body) 42 | end) 43 | end) 44 | 45 | describe("#raise_error", function() 46 | it("raises an error with a code", function() 47 | local ok, err = pcall(function() controller:raise_error(1000) end) 48 | 49 | assert.are.equal(false, ok) 50 | assert.are.equal(1000, err.code) 51 | end) 52 | 53 | it("raises an error with a code and custom attributes", function() 54 | local custom_attrs = { custom_attr_1 = "1", custom_attr_2 = "2" } 55 | local ok, err = pcall(function() controller:raise_error(1000, custom_attrs) end) 56 | 57 | assert.are.equal(false, ok) 58 | assert.are.equal(1000, err.code) 59 | assert.are.same(custom_attrs, err.custom_attrs) 60 | end) 61 | end) 62 | 63 | describe("#accepted_params", function() 64 | it("keeps only the params specified in filters", function() 65 | local param_filters = { 'first_name', 'last_name', 'other_param' } 66 | params = { 67 | first_name = 'roberto', 68 | last_name = 'gin', 69 | injection_param = 4 70 | } 71 | 72 | local accepted_params = controller:accepted_params(param_filters, params) 73 | assert.are.same({ 74 | first_name = 'roberto', 75 | last_name = 'gin' 76 | }, accepted_params) 77 | end) 78 | end) 79 | end) 80 | -------------------------------------------------------------------------------- /spec/core/error_spec.lua: -------------------------------------------------------------------------------- 1 | require 'spec.spec_helper' 2 | 3 | 4 | describe("Error", function() 5 | before_each(function() 6 | Error = require 'gin.core.error' 7 | end) 8 | 9 | after_each(function() 10 | package.loaded['gin.core.error'] = nil 11 | Error = nil 12 | end) 13 | 14 | describe(".new", function() 15 | describe("when no matching error can be found", function() 16 | it("raises an error", function() 17 | local ok, err = pcall(function() 18 | Error.new(9999) 19 | end) 20 | 21 | assert.are.equal(false, ok) 22 | 23 | local contains_error = string.match(err, "invalid error code") ~= nil 24 | assert.are.equal(true, contains_error) 25 | end) 26 | end) 27 | 28 | describe("when a matching error can be found", function() 29 | describe("when no custom attributes are passed in", function() 30 | describe("when headers are defined in Errors", function() 31 | before_each(function() 32 | Error.list = { 33 | [1000] = { 34 | status = 500, 35 | headers = { ["X-Info"] = "additional-info"}, 36 | message = "Something bad happened here" 37 | } 38 | } 39 | end) 40 | 41 | it("sets the appropriate values", function() 42 | local err = Error.new(1000) 43 | 44 | local expected_body = { 45 | code = 1000, 46 | message = "Something bad happened here" 47 | } 48 | 49 | assert.are.equal(500, err.status) 50 | assert.are.same({ ["X-Info"] = "additional-info"}, err.headers) 51 | assert.are.same(expected_body, err.body) 52 | end) 53 | end) 54 | 55 | describe("when headers are not defined in Errors", function() 56 | before_each(function() 57 | Error.list = { 58 | [1000] = { 59 | status = 500, 60 | message = "Something bad happened here" 61 | } 62 | } 63 | end) 64 | 65 | it("sets the appropriate values", function() 66 | local err = Error.new(1000) 67 | 68 | local expected_body = { 69 | code = 1000, 70 | message = "Something bad happened here" 71 | } 72 | 73 | assert.are.equal(500, err.status) 74 | assert.are.same({}, err.headers) 75 | assert.are.same(expected_body, err.body) 76 | end) 77 | end) 78 | end) 79 | 80 | describe("when custom attributes are passed in", function() 81 | before_each(function() 82 | Error.list = { 83 | [1000] = { 84 | status = 500, 85 | message = "Something bad happened here" 86 | } 87 | } 88 | end) 89 | 90 | it("adds them to the error", function() 91 | local err = Error.new(1000, { custom_attr = "custom_value" }) 92 | 93 | local expected_body = { 94 | code = 1000, 95 | message = "Something bad happened here", 96 | custom_attr = "custom_value" 97 | } 98 | 99 | assert.are.equal(500, err.status) 100 | assert.are.same({}, err.headers) 101 | assert.are.same(expected_body, err.body) 102 | end) 103 | end) 104 | end) 105 | end) 106 | end) -------------------------------------------------------------------------------- /spec/core/request_spec.lua: -------------------------------------------------------------------------------- 1 | require 'spec.spec_helper' 2 | 3 | -- gin 4 | local Request = require 'gin.core.request' 5 | 6 | 7 | describe("Request", function() 8 | before_each(function() 9 | ngx = { 10 | var = { 11 | uri = "/uri", 12 | request_method = 'POST' 13 | }, 14 | 15 | req = { 16 | read_body = function() return end, 17 | get_body_data = function() return nil end, 18 | get_uri_args = function() return { uri_param = '2' } end, 19 | get_headers = function() return { ["Content-Type"] = "application/json" } end, 20 | get_post_args = function() return { body_param = '2' } end 21 | } 22 | } 23 | end) 24 | 25 | after_each(function() 26 | ngx = nil 27 | end) 28 | 29 | describe("body", function() 30 | it("reads the body on init", function() 31 | spy.on(ngx.req, "read_body") 32 | local request = Request.new(ngx) 33 | 34 | assert.spy(ngx.req.read_body).was.called(1) 35 | 36 | ngx.req.read_body:revert() 37 | end) 38 | 39 | it("sets raw body to the returned value", function() 40 | ngx.req.get_body_data = function() return '{"param":"value"}' end 41 | local request = Request.new(ngx) 42 | 43 | assert.are.equal('{"param":"value"}', request.body_raw) 44 | end) 45 | 46 | describe("when body is a valid JSON", function() 47 | it("sets request body to a table", function() 48 | ngx.req.get_body_data = function() return '{"param":"value"}' end 49 | 50 | local request = Request.new(ngx) 51 | 52 | assert.are.same({ param = "value" }, request.body) 53 | end) 54 | end) 55 | 56 | describe("when body is nil", function() 57 | it("sets request body to nil", function() 58 | ngx.req.get_body_data = function() return nil end 59 | 60 | local request = Request.new(ngx) 61 | 62 | assert.are.same(nil, request.body) 63 | end) 64 | end) 65 | 66 | describe("when body is an invalid JSON", function() 67 | it("raises an error", function() 68 | ngx.req.get_body_data = function() return "not-json" end 69 | 70 | local ok, err = pcall(function() return Request.new(ngx) end) 71 | 72 | assert.are.equal(false, ok) 73 | assert.are.equal(103, err.code) 74 | end) 75 | end) 76 | 77 | describe("when body is not a JSON hash", function() 78 | it("raises an error", function() 79 | ngx.req.get_body_data = function() return'["one", "two"]' end 80 | 81 | local ok, err = pcall(function() return Request.new(ngx) end) 82 | 83 | assert.are.equal(false, ok) 84 | assert.are.equal(104, err.code) 85 | end) 86 | end) 87 | end) 88 | 89 | describe("common attributes", function() 90 | before_each(function() 91 | request = Request.new(ngx) 92 | end) 93 | 94 | after_each(function() 95 | request = nil 96 | end) 97 | 98 | it("returns nil for unset attrs", function() 99 | assert.are.same(nil, request.unexisting_attr) 100 | end) 101 | 102 | it("returns uri", function() 103 | assert.are.same('/uri', request.uri) 104 | end) 105 | 106 | it("returns method", function() 107 | assert.are.same('POST', request.method) 108 | end) 109 | 110 | it("returns uri_params", function() 111 | assert.are.same({ uri_param = '2' }, request.uri_params) 112 | end) 113 | 114 | it("returns headers", function() 115 | assert.are.same({ ["Content-Type"] = "application/json" }, request.headers) 116 | end) 117 | 118 | it("sets and returns api_version", function() 119 | request.api_version = '1.2' 120 | assert.are.same('1.2', request.api_version) 121 | end) 122 | end) 123 | end) 124 | -------------------------------------------------------------------------------- /spec/core/response_spec.lua: -------------------------------------------------------------------------------- 1 | require 'spec.spec_helper' 2 | 3 | -- gin 4 | local Response = require 'gin.core.response' 5 | 6 | 7 | describe("Response", function() 8 | describe(".new", function() 9 | describe("when no options are passed in", function() 10 | it("initializes an instance with defaults", function() 11 | local response = Response.new() 12 | 13 | assert.are.same(200, response.status) 14 | assert.are.same({}, response.headers) 15 | assert.are.same({}, response.body) 16 | end) 17 | end) 18 | 19 | describe("when options are passed in", function() 20 | it("saves them to the instance", function() 21 | local response = Response.new({ 22 | status = 403, 23 | headers = { ["X-Custom"] = "custom" }, 24 | body = "The body." 25 | }) 26 | 27 | assert.are.same(403, response.status) 28 | assert.are.same({ ["X-Custom"] = "custom" }, response.headers) 29 | assert.are.same("The body.", response.body) 30 | end) 31 | end) 32 | end) 33 | end) -------------------------------------------------------------------------------- /spec/core/router_spec.lua: -------------------------------------------------------------------------------- 1 | require 'spec.spec_helper' 2 | 3 | 4 | describe("Router", function() 5 | before_each(function() 6 | -- stub application packages 7 | package.loaded['config.routes'] = {} 8 | Application = require 'config.application' 9 | 10 | Router = require 'gin.core.router' 11 | Routes = require 'gin.core.routes' 12 | Controller = require 'gin.core.controller' 13 | Request = require 'gin.core.request' 14 | 15 | ngx = { 16 | HTTP_NOT_FOUND = 404, 17 | exit = function(code) return end, 18 | print = function(print) return end, 19 | status = 200, 20 | header = {}, 21 | req = { 22 | read_body = function() return end, 23 | get_body_data = function() return end, 24 | get_headers = function() return end, 25 | }, 26 | var = { 27 | uri = "/users", 28 | request_method = 'GET' 29 | } 30 | } 31 | end) 32 | 33 | after_each(function() 34 | package.loaded['config.routes'] = nil 35 | Application = nil 36 | 37 | package.loaded['gin.core.router'] = nil 38 | package.loaded['gin.core.routes'] = nil 39 | package.loaded['gin.core.controller'] = nil 40 | package.loaded['gin.core.request'] = nil 41 | 42 | Router = nil 43 | Routes = nil 44 | Controller = nil 45 | Request = nil 46 | 47 | ngx = nil 48 | end) 49 | 50 | describe(".handler", function() 51 | 52 | describe("when a request error is raised", function() 53 | before_each(function() 54 | Request.new = function(_) error({ code = 104, custom_attrs = { custom_attr = "custom_attr_value" } }) end 55 | end) 56 | 57 | it("responds with an error response", function() 58 | local arg_ngx, arg_response 59 | Router.respond = function(...) arg_ngx, arg_response = ... end 60 | 61 | Router.handler(ngx) 62 | 63 | assert.are.same(arg_ngx, ngx) 64 | assert.are.same(104, arg_response.body.code) 65 | assert.are.same("custom_attr_value", arg_response.body.custom_attr) 66 | end) 67 | end) 68 | 69 | it("calls the router match", function() 70 | local arg_request 71 | Router.match = function(...) arg_request = ... end 72 | 73 | Router.handler(ngx) 74 | 75 | assert.are.same(arg_request, Request.new(ngx)) 76 | end) 77 | 78 | describe("when the router match returns an error", function() 79 | before_each(function() 80 | Router.match = function(req) error({ code = 100, custom_attrs = { custom_attr = "custom_attr_value" } }) end 81 | end) 82 | 83 | it("responds with an error response", function() 84 | local arg_ngx, arg_response 85 | Router.respond = function(...) arg_ngx, arg_response = ... end 86 | 87 | Router.handler(ngx) 88 | 89 | assert.are.same(arg_ngx, ngx) 90 | assert.are.same(100, arg_response.body.code) 91 | assert.are.same("custom_attr_value", arg_response.body.custom_attr) 92 | end) 93 | end) 94 | 95 | describe("when a router match does not find a matching API", function() 96 | before_each(function() 97 | Router.match = function(req) return end 98 | end) 99 | 100 | it("raises a 404 error if no match is found", function() 101 | stub(ngx, 'exit') 102 | 103 | Router.handler(ngx) 104 | 105 | assert.stub(ngx.exit).was.called_with(ngx.HTTP_NOT_FOUND) 106 | 107 | ngx.exit:revert() 108 | end) 109 | end) 110 | 111 | describe("when a router match is found", function() 112 | before_each(function() 113 | request = Request.new(ngx) 114 | Router.match = function(req) return "controller_name", "action", "params", request end 115 | end) 116 | 117 | after_each(function() 118 | request = nil 119 | end) 120 | 121 | it("calls controller", function() 122 | stub(Router, 'respond') -- stub to avoid calling the function 123 | 124 | local arg_request, arg_controller_name, arg_action, arg_params 125 | Router.call_controller = function(...) arg_request, arg_controller_name, arg_action, arg_params = ... end 126 | 127 | Router.handler(ngx) 128 | 129 | assert.are.same(ngx, arg_request.ngx) 130 | assert.are.same("/users", arg_request.uri) 131 | assert.are.same('GET', arg_request.method) 132 | 133 | assert.are.equal("controller_name", arg_controller_name) 134 | assert.are.equal("action", arg_action) 135 | assert.are.equal("params", arg_params) 136 | 137 | Router.respond:revert() 138 | end) 139 | 140 | it("responds with the response", function() 141 | Router.call_controller = function() return "response" end 142 | 143 | stub(Router, 'respond') 144 | 145 | Router.handler(ngx) 146 | 147 | assert.stub(Router.respond).was.called_with(ngx, "response") 148 | end) 149 | end) 150 | end) 151 | 152 | describe(".call_controller", function() 153 | before_each(function() 154 | Error.list = { 155 | [1000] = { 156 | status = 500, 157 | headers = { ["X-Info"] = "additional-info"}, 158 | message = "Something bad happened here" 159 | } 160 | } 161 | 162 | instance = {} -- we're going to set self to instance so we can assert on it 163 | TestController = {} 164 | function TestController:action() 165 | instance = self 166 | end 167 | package.loaded['controller_name'] = TestController 168 | end) 169 | 170 | after_each(function() 171 | instance = nil 172 | TestController = nil 173 | package.loaded['controller_name'] = nil 174 | end) 175 | 176 | it("calls the action of an instance of the matched controller name", function() 177 | spy.on(TestController, 'action') 178 | 179 | local request = Request.new(ngx) 180 | Router.call_controller(request, "controller_name", "action", "params") 181 | 182 | assert.spy(TestController.action).was.called() 183 | 184 | -- assert the instance was initialized with the correct arguments 185 | assert.are.same(request, instance.request) 186 | assert.are.same("params", instance.params) 187 | 188 | TestController.action:revert() 189 | end) 190 | 191 | describe("when the controller successfully returns", function() 192 | describe("when the controller only returns the status code", function() 193 | before_each(function() 194 | TestController = {} 195 | function TestController:action() 196 | return 403 197 | end 198 | package.loaded['controller_name'] = TestController 199 | end) 200 | 201 | it("returns a response with the status", function() 202 | local response = Router.call_controller(ngx, "controller_name", "action", "params") 203 | 204 | assert.are.equal(403, response.status) 205 | end) 206 | 207 | it("returns a response with the body to an empty json", function() 208 | local response = Router.call_controller(ngx, "controller_name", "action", "params") 209 | 210 | assert.are.same({}, response.body) 211 | end) 212 | end) 213 | 214 | describe("when the controller returns the status code and the body", function() 215 | before_each(function() 216 | TestController = {} 217 | function TestController:action() 218 | return 403, { name = 'gin' } 219 | end 220 | package.loaded['controller_name'] = TestController 221 | end) 222 | 223 | it("sets the response response status to the controller's response status", function() 224 | local response = Router.call_controller(ngx, "controller_name", "action", "params") 225 | 226 | assert.are.equal(403, response.status) 227 | end) 228 | 229 | it("calls nginx with the serialized json of the controller response body", function() 230 | local response = Router.call_controller(ngx, "controller_name", "action", "params") 231 | 232 | assert.are.same({ name = 'gin' }, response.body) 233 | end) 234 | end) 235 | 236 | describe("when the controller returns the status code, the body and headers", function() 237 | before_each(function() 238 | TestController = {} 239 | function TestController:action() 240 | local headers = { ["Cache-Control"] = "max-age=3600", ["Retry-After"] = "120" } 241 | return 403, { name = 'gin' }, headers 242 | end 243 | package.loaded['controller_name'] = TestController 244 | end) 245 | 246 | it("sets the response status to the controller's response status", function() 247 | local response = Router.call_controller(ngx, "controller_name", "action", "params") 248 | 249 | assert.are.equal(403, response.status) 250 | end) 251 | 252 | it("calls nginx with the serialized json of the controller response body", function() 253 | local response = Router.call_controller(ngx, "controller_name", "action", "params") 254 | 255 | assert.are.same({ name = 'gin' }, response.body) 256 | end) 257 | 258 | it("sets the nginx response headers", function() 259 | local response = Router.call_controller(ngx, "controller_name", "action", "params") 260 | 261 | assert.are.equal("max-age=3600", response.headers["Cache-Control"]) 262 | assert.are.equal("120", response.headers["Retry-After"]) 263 | end) 264 | end) 265 | end) 266 | 267 | describe("when the controller raises an API error", function() 268 | before_each(function() 269 | TestController = {} 270 | function TestController:action() 271 | self:raise_error(1000, { additional_info = "some-info" }) 272 | return { name = 'gin' } 273 | end 274 | package.loaded['controller_name'] = TestController 275 | end) 276 | 277 | it("sets the response status to the controller's error status", function() 278 | local response = Router.call_controller(ngx, "controller_name", "action", "params") 279 | 280 | assert.are.equal(500, response.status) 281 | end) 282 | 283 | it("sets the response headers", function() 284 | local response = Router.call_controller(ngx, "controller_name", "action", "params") 285 | 286 | assert.are.equal("additional-info", response.headers["X-Info"]) 287 | end) 288 | 289 | it("calls nginx with the serialized json of the controller response", function() 290 | local response = Router.call_controller(ngx, "controller_name", "action", "params") 291 | 292 | assert.are.same({ 293 | code = 1000, 294 | message = "Something bad happened here", 295 | additional_info = "some-info" 296 | }, response.body) 297 | end) 298 | end) 299 | 300 | describe("when the controller raises an API error", function() 301 | before_each(function() 302 | TestController = {} 303 | function TestController:action() 304 | error("blew up!") 305 | end 306 | package.loaded['controller_name'] = TestController 307 | end) 308 | 309 | it("doesn't eat up the error", function() 310 | local ok, err = pcall(function() 311 | Router.call_controller(ngx, "controller_name", "action", "params") 312 | end) 313 | 314 | assert.are.equal(false, ok) 315 | 316 | local contains_error = string.match(err, "blew up!") ~= nil 317 | assert.are.equal(true, contains_error) 318 | end) 319 | end) 320 | end) 321 | 322 | describe(".match", function() 323 | before_each(function() 324 | -- set routes 325 | local Routes = require 'gin.core.routes' 326 | 327 | local v1 = Routes.version(1) 328 | 329 | v1:POST("/users", { controller = "users", action = "create" }) 330 | v1:GET("/users", { controller = "users", action = "index" }) 331 | v1:GET("/users/:id", { controller = "users", action = "show" }) 332 | v1:PUT("/users/:id", { controller = "users", action = "edit" }) 333 | 334 | local v2 = Routes.version(2) 335 | v2:DELETE("/users/:user_id/messages/:id", { controller = "messages", action = "destroy" }) 336 | 337 | package.loaded['config.routes'] = Routes 338 | 339 | package.loaded['gin.core.router'] = nil 340 | Router = require 'gin.core.router' 341 | end) 342 | 343 | after_each(function() 344 | package.loaded['config.routes'] = {} 345 | end) 346 | 347 | it("returns the controller, action and params for a single param", function() 348 | ngx.var.uri = "/users/roberto" 349 | ngx.var.request_method = "GET" 350 | ngx.req.get_headers = function() return { ['accept'] = "application/vnd." .. Application.name .. ".v1+json" } end 351 | 352 | local request = Request.new(ngx) 353 | 354 | controller, action, params, request = Router.match(request) 355 | 356 | assert.are.same("1/users_controller", controller) 357 | assert.are.same("show", action) 358 | assert.are.same({ id = "roberto" }, params) 359 | assert.are.same('1', request.api_version) 360 | end) 361 | 362 | it("returns the controller, action and params for a multiple params", function() 363 | ngx.var.uri = "/users/roberto/messages/123" 364 | ngx.var.request_method = "DELETE" 365 | ngx.req.get_headers = function() return { ['accept'] = "application/vnd." .. Application.name .. ".v2.1-p3+json" } end 366 | 367 | local request = Request.new(ngx) 368 | 369 | controller, action, params, version = Router.match(request) 370 | 371 | assert.are.same("2/messages_controller", controller) 372 | assert.are.same("destroy", action) 373 | assert.are.same({ user_id = "roberto", id = "123" }, params) 374 | assert.are.same('2.1-p3', request.api_version) 375 | end) 376 | 377 | it("raises an error if an Accept header is not set", function() 378 | ngx.req.get_headers = function() return {} end 379 | 380 | local request = Request.new(ngx) 381 | 382 | local ok, err = pcall(function() return Router.match(request) end) 383 | 384 | assert.are.equal(false, ok) 385 | assert.are.equal(100, err.code) 386 | end) 387 | 388 | it("raises an error if an Accept header set does not match the appropriate vendor format", function() 389 | ngx.req.get_headers = function() return { ['accept'] = "application/vnd.other.v1+json" } end 390 | 391 | local request = Request.new(ngx) 392 | 393 | local ok, err = pcall(function() return Router.match(request) end) 394 | 395 | assert.are.equal(false, ok) 396 | assert.are.equal(101, err.code) 397 | end) 398 | 399 | it("raises an error if an Accept header corresponds to an unsupported version", function() 400 | ngx.req.get_headers = function() return { ['accept'] = "application/vnd." .. Application.name .. ".v3+json" } end 401 | 402 | local request = Request.new(ngx) 403 | 404 | local ok, err = pcall(function() return Router.match(request) end) 405 | 406 | assert.are.equal(false, ok) 407 | assert.are.equal(102, err.code) 408 | end) 409 | end) 410 | 411 | describe(".respond", function() 412 | before_each(function() 413 | local Response = require 'gin.core.response' 414 | response = Response.new({ 415 | status = 200, 416 | headers = { ['one'] = 'first', ['two'] = 'second' }, 417 | body = { name = 'gin'} 418 | }) 419 | end) 420 | 421 | it("sets the ngx status", function() 422 | Router.respond(ngx, response) 423 | 424 | assert.are.equal(200, ngx.status) 425 | end) 426 | 427 | it("sets the ngx headers", function() 428 | Router.respond(ngx, response) 429 | 430 | assert.are.equal('first', ngx.header['one']) 431 | assert.are.equal('second', ngx.header['two']) 432 | end) 433 | 434 | it("sets the content length header", function() 435 | Router.respond(ngx, response) 436 | 437 | assert.are.equal(14, ngx.header['Content-Length']) 438 | end) 439 | 440 | it("calls ngx print with the encoded body", function() 441 | stub(ngx, 'print') 442 | 443 | Router.respond(ngx, response) 444 | 445 | assert.stub(ngx.print).was_called_with('{"name":"gin"}') 446 | end) 447 | end) 448 | end) 449 | -------------------------------------------------------------------------------- /spec/core/routes_spec.lua: -------------------------------------------------------------------------------- 1 | require 'spec.spec_helper' 2 | 3 | 4 | describe("Routes", function() 5 | 6 | before_each(function() 7 | Routes = require('gin.core.routes') 8 | end) 9 | 10 | after_each(function() 11 | package.loaded['gin.core.routes'] = nil 12 | Routes = nil 13 | end) 14 | 15 | describe(".version", function() 16 | describe("when it's a string", function() 17 | it("raises an error", function() 18 | local ok, err = pcall(function() return Routes.version("1") end) 19 | 20 | assert.are.equal(false, ok) 21 | assert.are.same(true, string.find(err, "version is not an integer number %(got string%)") > 0) 22 | end) 23 | end) 24 | 25 | describe("when it's a float", function() 26 | it("raises an error", function() 27 | local ok, err = pcall(function() return Routes.version(1.2) end) 28 | 29 | assert.are.equal(false, ok) 30 | assert.are.same(true, string.find(err, "version is not an integer number %(got float%).") > 0) 31 | end) 32 | end) 33 | 34 | describe("when it's an integer", function() 35 | it("sets the dispatcher key and returns a version object", function() 36 | local version = Routes.version(1) 37 | 38 | assert.are.same({ [1] = {} }, Routes.dispatchers) 39 | assert.are.same(1, version.number) 40 | end) 41 | end) 42 | 43 | describe("when a version has already been created", function() 44 | it("returns an error", function() 45 | local version = Routes.version(1) 46 | 47 | 48 | local ok, err = pcall(function() return Routes.version(1) end) 49 | 50 | assert.are.equal(false, ok) 51 | assert.are.same(true, string.find(err, "version has already been defined %(got 1%).") > 0) 52 | end) 53 | end) 54 | 55 | describe("when another version gets created", function() 56 | it("sets the dispatcher keys", function() 57 | local version_1 = Routes.version(1) 58 | local version_2 = Routes.version(2) 59 | assert.are.same({ [1] = {}, [2] = {} }, Routes.dispatchers) 60 | end) 61 | end) 62 | end) 63 | 64 | describe("Adding routes", function() 65 | before_each(function() 66 | version = Routes.version(1) 67 | end) 68 | 69 | after_each(function() 70 | version = nil 71 | end) 72 | 73 | describe(".add", function() 74 | it("adds a simple route", function() 75 | version:add('GET', "/users", { controller = "users", action = "index" }) 76 | 77 | assert.are.same({ 78 | [1] = { 79 | [1] = { 80 | pattern = "^/users/???$", 81 | GET = { controller = "users_controller", action = "index", params = {} } 82 | } 83 | } 84 | }, Routes.dispatchers) 85 | end) 86 | 87 | it("adds a named parameter route", function() 88 | version:add('GET', "/users/:id", { controller = "users", action = "show" }) 89 | 90 | assert.are.same({ 91 | [1] = { 92 | [1] = { 93 | pattern = "^/users/([A-Za-z0-9_]+)/???$", 94 | GET = { controller = "users_controller", action = "show", params = { [1] = "id" } } 95 | } 96 | } 97 | }, Routes.dispatchers) 98 | end) 99 | 100 | it("adds routes with multiple named parameters", function() 101 | version:add('GET', "/users/:user_id/messages/:id", { controller = "messages", action = "show" }) 102 | 103 | assert.are.same({ 104 | [1] = { 105 | [1] = { 106 | pattern = "^/users/([A-Za-z0-9_]+)/messages/([A-Za-z0-9_]+)/???$", 107 | GET = { controller = "messages_controller", action = "show", params = { [1] = "user_id", [2] = "id" } } 108 | } 109 | } 110 | }, Routes.dispatchers) 111 | end) 112 | 113 | it("add multiple routes", function() 114 | version:add('GET', "/users", { controller = "users", action = "index" }) 115 | version:add('POST', "/users", { controller = "users", action = "create" }) 116 | 117 | assert.are.same({ 118 | [1] = { 119 | [1] = { 120 | pattern = "^/users/???$", 121 | GET = { controller = "users_controller", action = "index", params = {} } 122 | }, 123 | [2] = { 124 | pattern = "^/users/???$", 125 | POST = { controller = "users_controller", action = "create", params = {} } 126 | } 127 | } 128 | }, Routes.dispatchers) 129 | end) 130 | 131 | it("does not modify entered regexes", function() 132 | version:add('PUT', "/users/:(.*)", { controller = "messages", action = "show" }) 133 | 134 | assert.are.same({ 135 | [1] = { 136 | [1] = { 137 | pattern = "^/users/:(.*)/???$", 138 | PUT = { controller = "messages_controller", action = "show", params = {} } 139 | } 140 | } 141 | }, Routes.dispatchers) 142 | end) 143 | 144 | it("adds routes to the appropriate version", function() 145 | local version_2 = Routes.version(2) 146 | 147 | version:add('GET', "/users", { controller = "users", action = "index" }) 148 | version_2:add('GET', "/messages", { controller = "messages", action = "index" }) 149 | 150 | assert.are.same({ 151 | [1] = { 152 | [1] = { 153 | pattern = "^/users/???$", 154 | GET = { controller = "users_controller", action = "index", params = {} } 155 | } 156 | }, 157 | 158 | [2] = { 159 | [1] = { 160 | pattern = "^/messages/???$", 161 | GET = { controller = "messages_controller", action = "index", params = {} } 162 | } 163 | } 164 | }, Routes.dispatchers) 165 | end) 166 | end) 167 | end) 168 | 169 | describe("Version helpers", function() 170 | 171 | before_each(function() 172 | version = Routes.version(1) 173 | -- spy.on(version, 'add') 174 | t = { controller = "users", action = "index" } 175 | end) 176 | 177 | after_each(function() 178 | -- version.add:revert() 179 | version = nil 180 | end) 181 | 182 | local supported_http_methods = { 183 | GET = true, 184 | POST = true, 185 | HEAD = true, 186 | OPTIONS = true, 187 | PUT = true, 188 | PATCH = true, 189 | DELETE = true, 190 | TRACE = true, 191 | CONNECT = true 192 | } 193 | 194 | for http_method, _ in pairs(supported_http_methods) do 195 | describe("." .. http_method, function() 196 | it("calls the .add method with ".. http_method, function() 197 | local self, method, pattern, route_info 198 | 199 | version.add = function(...) self, method, pattern, route_info = ... end 200 | 201 | version[http_method](version, "/users", t) 202 | 203 | assert.are.same(version, self) 204 | assert.are.same(http_method, method) 205 | assert.are.same('/users', pattern) 206 | assert.are.same(t, route_info) 207 | end) 208 | end) 209 | end 210 | end) 211 | end) 212 | -------------------------------------------------------------------------------- /spec/core/settings_spec.lua: -------------------------------------------------------------------------------- 1 | require 'spec.spec_helper' 2 | 3 | 4 | describe("Settings", function() 5 | 6 | before_each(function() 7 | settings = require('gin.core.settings') 8 | end) 9 | 10 | after_each(function() 11 | package.loaded['config.settings'] = {} -- reset to mock 12 | package.loaded['gin.core.settings'] = nil 13 | settings = nil 14 | end) 15 | 16 | describe(".for_environment", function() 17 | describe("the defaults", function() 18 | describe("when in development environment", function() 19 | it("returns the defaults", function() 20 | local defaults = { 21 | code_cache = false, 22 | port = 7200, 23 | expose_api_console = true 24 | } 25 | 26 | assert.are.same(defaults, settings.for_environment('development')) 27 | end) 28 | end) 29 | 30 | describe("when in test environment", function() 31 | it("returns the defaults", function() 32 | local defaults = { 33 | code_cache = true, 34 | port = 7201, 35 | expose_api_console = false 36 | } 37 | 38 | package.loaded['config.settings'] = false 39 | package.loaded['config.settings'] = {} 40 | assert.are.same(defaults, settings.for_environment('test')) 41 | end) 42 | end) 43 | 44 | describe("when in production environment", function() 45 | it("returns the defaults", function() 46 | local defaults = { 47 | code_cache = true, 48 | port = 80, 49 | expose_api_console = false 50 | } 51 | 52 | assert.are.same(defaults, settings.for_environment('production')) 53 | end) 54 | end) 55 | 56 | describe("when in any other environments", function() 57 | it("returns the defaults", function() 58 | local defaults = { 59 | code_cache = true, 60 | port = 80, 61 | expose_api_console = false 62 | } 63 | 64 | assert.are.same(defaults, settings.for_environment('something-else')) 65 | end) 66 | end) 67 | end) 68 | 69 | describe("merging values from the application settings", function() 70 | before_each(function() 71 | app_settings = {} 72 | app_settings.development = { 73 | code_cache = true, 74 | port = 7202, 75 | custom_setting = 'my setting' 76 | } 77 | app_settings.production = { 78 | code_cache = false 79 | } 80 | package.loaded['config.settings'] = app_settings 81 | end) 82 | 83 | after_each(function() 84 | package.loaded['config.settings'] = nil 85 | app_settings = nil 86 | end) 87 | 88 | it("returns merged values", function() 89 | local s = settings.for_environment('development') 90 | 91 | assert.are.same(true, s.code_cache) 92 | assert.are.same(7202, s.port) 93 | assert.are.same('my setting', s.custom_setting) 94 | end) 95 | end) 96 | end) 97 | end) 98 | -------------------------------------------------------------------------------- /spec/core/zebra_spec.lua: -------------------------------------------------------------------------------- 1 | require 'spec.spec_helper' 2 | 3 | 4 | describe("Gin", function() 5 | before_each(function() 6 | package.loaded['gin.core.gin'] = nil 7 | end) 8 | 9 | after_each(function() 10 | package.loaded['gin.core.gin'] = nil 11 | end) 12 | 13 | describe(".env", function() 14 | describe("when the GIN_ENV value is set", function() 15 | it("sets it to the GIN_ENV value", function() 16 | local original_getenv = os.getenv 17 | os.getenv = function(arg) 18 | if arg == "GIN_ENV" then return 'myenv' end 19 | end 20 | 21 | local Gin = require 'gin.core.gin' 22 | 23 | assert.are.equal('myenv', Gin.env) 24 | 25 | os.getenv = original_getenv 26 | end) 27 | end) 28 | 29 | describe("when the GIN_ENV value is not set", function() 30 | it("sets it to development", function() 31 | local original_getenv = os.getenv 32 | os.getenv = function(arg) 33 | return nil 34 | end 35 | 36 | local Gin = require 'gin.core.gin' 37 | 38 | assert.are.equal('development', Gin.env) 39 | 40 | os.getenv = original_getenv 41 | end) 42 | end) 43 | end) 44 | 45 | describe(".settings", function() 46 | it("sets them to the current environment settings", function() 47 | package.loaded['gin.core.settings'] = { 48 | for_environment = function() return { mysetting = 'my-setting' } end 49 | } 50 | 51 | local Gin = require 'gin.core.gin' 52 | 53 | assert.are.same('my-setting', Gin.settings.mysetting) 54 | 55 | package.loaded['gin.core.settings'] = nil 56 | end) 57 | end) 58 | end) 59 | -------------------------------------------------------------------------------- /spec/db/migrations_spec.lua: -------------------------------------------------------------------------------- 1 | require 'spec.spec_helper' 2 | 3 | 4 | local create_schema_migrations_sql = [[ 5 | CREATE TABLE schema_migrations ( 6 | version varchar(14) NOT NULL, 7 | PRIMARY KEY (version) 8 | ); 9 | ]] 10 | 11 | 12 | describe("Migrations", function() 13 | before_each(function() 14 | helper_common = require 'gin.helpers.common' 15 | 16 | helper_common.module_names_in_path = function(path) 17 | return { 'migration/1', 'migration/2' } 18 | end 19 | 20 | migrations = require 'gin.db.migrations' 21 | 22 | stub(_G, "pp_to_file") 23 | 24 | queries_1 = {} 25 | queries_2 = {} 26 | 27 | db_1 = { 28 | options = { 29 | adapter = 'mysql', 30 | database = 'mydb' 31 | }, 32 | tables = function(...) return {} end, 33 | schema = function(...) return "" end, 34 | execute = function(self, q) 35 | table.insert(queries_1, q) 36 | return {} 37 | end 38 | } 39 | 40 | db_2 = { 41 | options = { 42 | adapter = 'mysql', 43 | database = 'mydb' 44 | }, 45 | tables = function(...) return { 'schema_migrations' } end, 46 | schema = function(...) return "" end, 47 | execute = function(self, q) 48 | table.insert(queries_2, q) 49 | return {} 50 | end 51 | } 52 | 53 | migration_1 = { 54 | db = db_1, 55 | up = function(...) 56 | db_1:execute("MIGRATION 1 SQL;") 57 | end 58 | } 59 | 60 | migration_2 = { 61 | db = db_2, 62 | up = function(...) 63 | db_2:execute("MIGRATION 2 SQL;") 64 | end, 65 | down = function(...) 66 | db_2:execute("ROLLBACK 2 SQL;") 67 | end 68 | } 69 | 70 | package.loaded['migration/1'] = migration_1 71 | package.loaded['migration/2'] = migration_2 72 | end) 73 | 74 | after_each(function() 75 | migrations = nil 76 | helper_common = nil 77 | package.loaded['gin.db.migrations'] = nil 78 | package.loaded['migration/1'] = nil 79 | package.loaded['migration/2'] = nil 80 | queries_1 = nil 81 | queries_2 = nil 82 | db_1 = nil 83 | db_2 = nil 84 | migration_1 = nil 85 | migration_2 = nil 86 | _G.pp_to_file:revert() 87 | end) 88 | 89 | describe("up", function() 90 | describe("when both migrations are run successfully", function() 91 | it("runs them, creating the database and the schema_migration if necessary", function() 92 | migrations.up() 93 | 94 | assert.are.same(create_schema_migrations_sql, queries_1[1]) 95 | assert.are.same("SELECT version FROM schema_migrations WHERE version = '1';", queries_1[2]) 96 | assert.are.same("MIGRATION 1 SQL;", queries_1[3]) 97 | assert.are.same("INSERT INTO schema_migrations (version) VALUES ('1');", queries_1[4]) 98 | 99 | assert.are.same("SELECT version FROM schema_migrations WHERE version = '2';", queries_2[1]) 100 | assert.are.same("MIGRATION 2 SQL;", queries_2[2]) 101 | assert.are.same("INSERT INTO schema_migrations (version) VALUES ('2');", queries_2[3]) 102 | end) 103 | 104 | it("returns the results", function() 105 | local ok, response = migrations.up() 106 | 107 | assert.are.equal(true, ok) 108 | assert.are.same({ 109 | [1] = { version = '1' }, 110 | [2] = { version = '2' }, 111 | }, response) 112 | end) 113 | end) 114 | 115 | describe("when one version has already been run", function() 116 | before_each(function() 117 | migrations.version_already_run = function(_, version) 118 | return version == '1' 119 | end 120 | end) 121 | 122 | it("skips it", function() 123 | migrations.up() 124 | 125 | assert.are.same({ create_schema_migrations_sql }, queries_1) 126 | 127 | assert.are.same("MIGRATION 2 SQL;", queries_2[1]) 128 | assert.are.same("INSERT INTO schema_migrations (version) VALUES ('2');", queries_2[2]) 129 | end) 130 | 131 | it("returns the results", function() 132 | local ok, response = migrations.up() 133 | 134 | assert.are.equal(true, ok) 135 | assert.are.same({ 136 | [1] = { version = '2' }, 137 | }, response) 138 | end) 139 | end) 140 | 141 | describe("when the database does not exist", function() 142 | local called_one 143 | before_each(function() 144 | called_one = false 145 | package.loaded['migration/1'].db.tables = function(...) 146 | if called_one == false then 147 | called_one = true 148 | error("Failed to connect to database: Unknown database 'nonexistent-database'") 149 | end 150 | return {} 151 | end 152 | 153 | package.loaded['migration/1'].db.adapter = { 154 | default_database = 'mysql', 155 | db = { 156 | close = function() end 157 | } 158 | } 159 | end) 160 | 161 | after_each(function() 162 | called_one = nil 163 | end) 164 | 165 | it("creates it", function() 166 | migrations.up() 167 | 168 | assert.are.same("CREATE DATABASE mydb;", queries_1[1]) 169 | end) 170 | end) 171 | 172 | describe("when running a migration with an unsupported adapter", function() 173 | before_each(function() 174 | migration_1.db.options.adapter = 'unsupported-adapter' 175 | end) 176 | 177 | it("stops from migrating subsequent migrations", function() 178 | migrations.up() 179 | 180 | assert.are.same({}, queries_1) 181 | assert.are.same({}, queries_2) 182 | end) 183 | 184 | it("returns an error", function() 185 | local ok, response = migrations.up() 186 | 187 | err_message = "Cannot run migrations for the adapter 'unsupported-adapter'. Supported adapters are: 'mysql', 'postgresql'." 188 | 189 | assert.are.equal(false, ok) 190 | assert.are.same({ 191 | [1] = { version = '1', error = err_message } 192 | }, response) 193 | end) 194 | end) 195 | 196 | describe("when an error occurs in the migration", function() 197 | before_each(function() 198 | migration_1.db.execute = function(self, sql) 199 | if sql == "MIGRATION 1 SQL;" then error("migration error") end 200 | return {} 201 | end 202 | end) 203 | 204 | it("returns an error", function() 205 | local ok, response = migrations.up() 206 | 207 | assert.are.equal(false, ok) 208 | 209 | assert.are.equal(1, #response) 210 | assert.are.equal('1', response[1].version) 211 | assert.are.equal(true, string.find(response[1].error, "migration error") > 0) 212 | end) 213 | end) 214 | end) 215 | 216 | describe("down", function() 217 | describe("when the most recent migration has not been rolled back", function() 218 | before_each(function() 219 | migrations.version_already_run = function(_, version) 220 | return true 221 | end 222 | end) 223 | 224 | describe("and the migration rolls back succesfully", function() 225 | it("rolls back only the most recent one", function() 226 | migrations.down() 227 | 228 | assert.are.same("ROLLBACK 2 SQL;", queries_2[1]) 229 | assert.are.same("DELETE FROM schema_migrations WHERE version = '2';", queries_2[2]) 230 | 231 | assert.are.same({}, queries_1) 232 | end) 233 | 234 | it("returns the results", function() 235 | local ok, response = migrations.down() 236 | 237 | assert.are.equal(true, ok) 238 | assert.are.same({ 239 | [1] = { version = '2' } 240 | }, response) 241 | end) 242 | end) 243 | 244 | describe("when the database does not exist", function() 245 | before_each(function() 246 | migrations.version_already_run = function(...) 247 | error("no database") 248 | end 249 | end) 250 | 251 | it("blows up", function() 252 | local ok, err = pcall(function() return migrations.down() end) 253 | 254 | assert.are.equal(false, ok) 255 | assert.are.equal(true, string.find(err, "no database") > 0) 256 | end) 257 | end) 258 | 259 | describe("when running a migration with an unsupported adapter", function() 260 | before_each(function() 261 | migration_2.db.options.adapter = 'unsupported-adapter' 262 | end) 263 | 264 | it("stops the migration", function() 265 | migrations.down() 266 | 267 | assert.are.same({}, queries_1) 268 | assert.are.same({}, queries_2) 269 | end) 270 | 271 | it("returns the results", function() 272 | local ok, response = migrations.down() 273 | 274 | err_message = "Cannot run migrations for the adapter 'unsupported-adapter'. Supported adapters are: 'mysql', 'postgresql'." 275 | 276 | assert.are.equal(false, ok) 277 | assert.are.same({ 278 | [1] = { version = '2', error = err_message } 279 | }, response) 280 | end) 281 | end) 282 | 283 | describe("when an error occurs in the migration", function() 284 | before_each(function() 285 | migration_2.db.execute = function() error("migration error") end 286 | end) 287 | 288 | it("returns an error", function() 289 | local ok, response = migrations.down() 290 | 291 | assert.are.equal(false, ok) 292 | 293 | assert.are.equal(1, #response) 294 | assert.are.equal('2', response[1].version) 295 | assert.are.equal(true, string.find(response[1].error, "migration error") > 0) 296 | end) 297 | end) 298 | end) 299 | end) 300 | end) 301 | -------------------------------------------------------------------------------- /spec/db/sql/mysql/orm_spec.lua: -------------------------------------------------------------------------------- 1 | require 'spec.spec_helper' 2 | 3 | 4 | describe("MySqlOrm", function() 5 | 6 | before_each(function() 7 | MySqlOrm = require 'gin.db.sql.mysql.orm' 8 | local quote_fun = function(str) return "'q-".. str .. "'" end 9 | orm = MySqlOrm.new('users', quote_fun) 10 | end) 11 | 12 | after_each(function() 13 | package.loaded['gin.db.sql.mysql.orm'] = nil 14 | MySqlOrm = nil 15 | orm = nil 16 | end) 17 | 18 | describe("#create", function() 19 | describe("when attrs are specified", function() 20 | it("creates a new entry", function() 21 | local sql = orm:create({ first_name = 'roberto', last_name = 'gin', age = 3, seen_at = '2013-10-12T16:31:21 UTC' }) 22 | assert.are.equal(112, #sql) 23 | assert.are.equal("INSERT INTO users (", sql:sub(1, 19)) 24 | assert.is.equal(52, (sql:find(") VALUES (", 20, true))) 25 | assert.are.equal(");", sql:sub(-2)) 26 | assert.is.number(sql:find("seen_at", 20, true)) 27 | assert.is.number(sql:find("last_name", 20, true)) 28 | assert.is.number(sql:find("first_name", 20, true)) 29 | assert.is.number(sql:find("age", 20, true)) 30 | assert.is.number(sql:find("'q-2013-10-12T16:31:21 UTC'", 62, true)) 31 | assert.is.number(sql:find("'q-gin'", 62, true)) 32 | assert.is.number(sql:find("'q-roberto'", 62, true)) 33 | assert.is.number(sql:find("3", 62, true)) 34 | end) 35 | end) 36 | 37 | describe("when no attrs are specified", function() 38 | it("raises an error", function() 39 | local ok, err = pcall(function() return orm:create() end) 40 | 41 | assert.are.equal(false, ok) 42 | assert.are.equal(true, string.find(err, "no attributes were specified to create new model instance") > 0) 43 | end) 44 | end) 45 | end) 46 | 47 | describe("#where", function() 48 | describe("when attrs are specified", function() 49 | describe("when attrs are a table", function() 50 | describe("when no options are specified", function() 51 | it("finds and returns models without options", function() 52 | local sql = orm:where({ first_name = 'roberto', last_name = 'gin', age = 3, seen_at = '2013-10-12T16:31:21 UTC' }) 53 | assert.are.equal(123, #sql) 54 | assert.are.equal("SELECT * FROM users WHERE (",sql:sub(1,27)) 55 | assert.are.equal(");",sql:sub(-2)) 56 | 57 | local andpos = sql:find(" AND ",28,true) 58 | assert.is.number(andpos) 59 | andpos = sql:find(" AND ",1+andpos,true) 60 | assert.is.number(andpos) 61 | andpos = sql:find(" AND ",1+andpos,true) 62 | assert.is.number(andpos) 63 | assert.is_nil(sql:find(" AND ",1+andpos,true)) 64 | 65 | assert.is.number(sql:find("seen_at='q-2013-10-12T16:31:21 UTC'",28,true)) 66 | assert.is.number(sql:find("last_name='q-gin'",28,true)) 67 | assert.is.number(sql:find("first_name='q-roberto'",28,true)) 68 | assert.is.number(sql:find("age=3",28,true)) 69 | end) 70 | end) 71 | 72 | describe("when the limit option is specified", function() 73 | it("finds models with limit", function() 74 | local sql = orm:where({ first_name = 'roberto'}, { limit = 12 }) 75 | assert.are.equal("SELECT * FROM users WHERE (first_name='q-roberto') LIMIT 12;", sql) 76 | end) 77 | end) 78 | 79 | describe("when the offset option is specified", function() 80 | it("finds models with offset", function() 81 | local sql = orm:where({ first_name = 'roberto'}, { offset = 10 }) 82 | assert.are.equal("SELECT * FROM users WHERE (first_name='q-roberto') OFFSET 10;", sql) 83 | end) 84 | end) 85 | 86 | describe("when the order option is specified", function() 87 | it("order model results", function() 88 | local sql = orm:where({ first_name = 'roberto'}, { order = "first_name DESC" }) 89 | assert.are.equal("SELECT * FROM users WHERE (first_name='q-roberto') ORDER BY first_name DESC;", sql) 90 | end) 91 | end) 92 | 93 | describe("when the order, limit and offset options are specified", function() 94 | it("finds models with offset", function() 95 | local sql = orm:where({ first_name = 'roberto'}, { order = "first_name DESC", limit = 12, offset = 10 }) 96 | assert.are.equal("SELECT * FROM users WHERE (first_name='q-roberto') ORDER BY first_name DESC LIMIT 12 OFFSET 10;", sql) 97 | end) 98 | end) 99 | end) 100 | 101 | describe("when attrs are a table", function() 102 | describe("when no options are specified", function() 103 | it("finds and returns models without options", function() 104 | local sql = orm:where("age > 3") 105 | assert.are.equal("SELECT * FROM users WHERE (age > 3);", sql) 106 | end) 107 | end) 108 | 109 | describe("when the limit option is specified", function() 110 | it("finds models with limit", function() 111 | local sql = orm:where("age > 3", { limit = 12 }) 112 | assert.are.equal("SELECT * FROM users WHERE (age > 3) LIMIT 12;", sql) 113 | end) 114 | end) 115 | 116 | describe("when the offset option is specified", function() 117 | it("finds models with offset", function() 118 | local sql = orm:where("age > 3", { offset = 10 }) 119 | assert.are.equal("SELECT * FROM users WHERE (age > 3) OFFSET 10;", sql) 120 | end) 121 | end) 122 | 123 | describe("when the order option is specified", function() 124 | it("order model results", function() 125 | local sql = orm:where("age > 3", { order = "first_name DESC" }) 126 | assert.are.equal("SELECT * FROM users WHERE (age > 3) ORDER BY first_name DESC;", sql) 127 | end) 128 | end) 129 | 130 | describe("when the order, limit and offset options are specified", function() 131 | it("finds models with offset", function() 132 | local sql = orm:where("age > 3", { order = "first_name DESC", limit = 12, offset = 10 }) 133 | assert.are.equal("SELECT * FROM users WHERE (age > 3) ORDER BY first_name DESC LIMIT 12 OFFSET 10;", sql) 134 | end) 135 | end) 136 | end) 137 | end) 138 | 139 | describe("when no attrs are specified", function() 140 | describe("when no options are specified", function() 141 | it("finds all models", function() 142 | local sql = orm:where() 143 | assert.are.equal("SELECT * FROM users;", sql) 144 | end) 145 | end) 146 | 147 | describe("when the limit option is specified", function() 148 | it("finds models with limit", function() 149 | local sql = orm:where({}, { limit = 12 }) 150 | assert.are.equal("SELECT * FROM users LIMIT 12;", sql) 151 | end) 152 | end) 153 | 154 | describe("when the offset option is specified", function() 155 | it("finds models with offset", function() 156 | local sql = orm:where({}, { offset = 10 }) 157 | assert.are.equal("SELECT * FROM users OFFSET 10;", sql) 158 | end) 159 | end) 160 | 161 | describe("when the order option is specified", function() 162 | it("order model results", function() 163 | local sql = orm:where({}, { order = "first_name DESC" }) 164 | assert.are.equal("SELECT * FROM users ORDER BY first_name DESC;", sql) 165 | end) 166 | end) 167 | 168 | describe("when the order, limit and offset options are specified", function() 169 | it("finds models with offset", function() 170 | local sql = orm:where({ }, { order = "first_name DESC", limit = 12, offset = 10 }) 171 | assert.are.equal("SELECT * FROM users ORDER BY first_name DESC LIMIT 12 OFFSET 10;", sql) 172 | end) 173 | end) 174 | end) 175 | end) 176 | 177 | describe("#delete_where", function() 178 | describe("when attrs are specified", function() 179 | describe("when attrs are a table", function() 180 | describe("when no options are specified", function() 181 | it("calls .delete_where", function() 182 | local sql = orm:delete_where({ first_name = 'roberto', last_name = 'gin', age = 3, seen_at = '2013-10-12T16:31:21 UTC' }) 183 | assert.are.equal(121, #sql) 184 | assert.are.equal("DELETE FROM users WHERE (",sql:sub(1,25)) 185 | assert.are.equal(");",sql:sub(-2)) 186 | 187 | local andpos = sql:find(" AND ",26,true) 188 | assert.is.number(andpos) 189 | andpos = sql:find(" AND ",1+andpos,true) 190 | assert.is.number(andpos) 191 | andpos = sql:find(" AND ",1+andpos,true) 192 | assert.is.number(andpos) 193 | assert.is_nil(sql:find(" AND ",1+andpos,true)) 194 | 195 | assert.is.number(sql:find("seen_at='q-2013-10-12T16:31:21 UTC'",26,true)) 196 | assert.is.number(sql:find("last_name='q-gin'",26,true)) 197 | assert.is.number(sql:find("first_name='q-roberto'",26,true)) 198 | assert.is.number(sql:find("age=3",26,true)) 199 | end) 200 | end) 201 | 202 | describe("when the limit option is specified", function() 203 | it("calls .delete_where with limit", function() 204 | local sql = orm:delete_where({ first_name = 'roberto'}, { limit = 12 }) 205 | assert.are.equal("DELETE FROM users WHERE (first_name='q-roberto') LIMIT 12;", sql) 206 | end) 207 | end) 208 | end) 209 | 210 | describe("when attrs are a string", function() 211 | describe("when no options are specified", function() 212 | it("calls .delete_where", function() 213 | local sql = orm:delete_where("age > 3") 214 | assert.are.equal("DELETE FROM users WHERE (age > 3);", sql) 215 | end) 216 | end) 217 | 218 | describe("when the limit option is specified", function() 219 | it("calls .delete_where with limit", function() 220 | local sql = orm:delete_where("age > 3", { limit = 12 }) 221 | assert.are.equal("DELETE FROM users WHERE (age > 3) LIMIT 12;", sql) 222 | end) 223 | end) 224 | end) 225 | end) 226 | 227 | describe("when attrs are not specified", function() 228 | describe("when no options are specified", function() 229 | it("calls .delete_where", function() 230 | local sql = orm:delete_where() 231 | assert.are.equal("DELETE FROM users;", sql) 232 | end) 233 | end) 234 | 235 | describe("when the limit option is specified", function() 236 | it("calls .delete_where with limit", function() 237 | local sql = orm:delete_where({}, { limit = 12 }) 238 | assert.are.equal("DELETE FROM users LIMIT 12;", sql) 239 | end) 240 | end) 241 | end) 242 | end) 243 | 244 | describe("#update_where", function() 245 | describe("when no attrs are specified", function() 246 | it("raises an error", function() 247 | local ok, err = pcall(function() return orm:update_where() end) 248 | 249 | assert.are.equal(false, ok) 250 | assert.are.equal(true, string.find(err, "no attributes were specified to create new model instance") > 0) 251 | end) 252 | end) 253 | 254 | describe("when attrs are specified", function() 255 | describe("when no where is specified", function() 256 | it("calls .update_where", function() 257 | local sql = orm:update_where({ first_name = 'roberto', last_name = 'gin', age = 3, seen_at = '2013-10-12T16:31:21 UTC' }) 258 | assert.are.equal(100, #sql) 259 | assert.are.equal("UPDATE users SET ",sql:sub(1,17)) 260 | assert.are.equal(";",sql:sub(-1)) 261 | assert.is.number(sql:find("seen_at='q-2013-10-12T16:31:21 UTC'",18,true)) 262 | assert.is.number(sql:find("last_name='q-gin'",18,true)) 263 | assert.is.number(sql:find("first_name='q-roberto'",18,true)) 264 | assert.is.number(sql:find("age=3",18,true)) 265 | end) 266 | end) 267 | 268 | describe("when where is specified", function() 269 | describe("and where is a table", function() 270 | it("calls .update_where", function() 271 | local sql = orm:update_where( 272 | { first_name = 'roberto', last_name = 'gin', age = 3, seen_at = '2013-10-12T16:31:21 UTC' }, 273 | { id = 4, first_name = 'robbb' } 274 | ) 275 | assert.are.equal(138, #sql) 276 | assert.are.equal("UPDATE users SET ",sql:sub(1,17)) 277 | assert.are.equal(" WHERE (",sql:sub(-39,-32)) 278 | assert.are.equal(");",sql:sub(-2)) 279 | assert.is.number(sql:find(" AND ",-39,true)) 280 | assert.is.number(sql:find("first_name='q-robbb'",-39,true)) 281 | assert.is.number(sql:find("id=4",-39,true)) 282 | assert.is.number(sql:find("seen_at='q-2013-10-12T16:31:21 UTC'",18,true)) 283 | assert.is.number(sql:find("last_name='q-gin'",18,true)) 284 | assert.is.number(sql:find("first_name='q-roberto'",18,true)) 285 | assert.is.number(sql:find("age=3",18,true)) 286 | end) 287 | end) 288 | 289 | describe("and where is a string", function() 290 | it("calls .update_where", function() 291 | local sql = orm:update_where( 292 | { first_name = 'roberto', last_name = 'gin', age = 3, seen_at = '2013-10-12T16:31:21 UTC' }, 293 | "age > 3" 294 | ) 295 | assert.are.equal(116, #sql) 296 | assert.are.equal("UPDATE users SET ",sql:sub(1,17)) 297 | assert.are.equal(" WHERE (age > 3);",sql:sub(-17)) 298 | assert.is.number(sql:find("seen_at='q-2013-10-12T16:31:21 UTC'",18,true)) 299 | assert.is.number(sql:find("last_name='q-gin'",18,true)) 300 | assert.is.number(sql:find("first_name='q-roberto'",18,true)) 301 | assert.is.number(sql:find("age=3",18,true)) 302 | end) 303 | end) 304 | end) 305 | end) 306 | end) 307 | end) -------------------------------------------------------------------------------- /spec/db/sql/orm_spec.lua: -------------------------------------------------------------------------------- 1 | require 'spec.spec_helper' 2 | 3 | -- gin 4 | local helpers = require 'gin.helpers.common' 5 | 6 | 7 | describe("SqlOrm", function() 8 | 9 | before_each(function() 10 | SqlOrm = require 'gin.db.sql.orm' 11 | MySql = { 12 | options = { 13 | adapter = 'mysql' 14 | }, 15 | quote = function(self, str) return "q-" .. str end 16 | } 17 | end) 18 | 19 | after_each(function() 20 | package.loaded['gin.db.sql.orm'] = nil 21 | SqlOrm = nil 22 | MySql = nil 23 | package.loaded['gin.db.sql.mysql.orm'] = nil 24 | end) 25 | 26 | describe("A model created with .define_model", function() 27 | 28 | after_each(function() 29 | Model = nil 30 | table_name_arg = nil 31 | quote_fun_arg = nil 32 | end) 33 | 34 | it("initializes the orm with the correct params", function() 35 | package.loaded['gin.db.sql.mysql.orm'] = { 36 | new = function(table_name, quote_fun) 37 | table_name_arg, quote_fun_arg = table_name, quote_fun 38 | 39 | return { 40 | table_name = table_name, 41 | quote = quote_fun 42 | } 43 | end 44 | } 45 | 46 | SqlOrm.define_model(MySql, 'users') 47 | 48 | assert.are.same('users', table_name_arg) 49 | assert.are.same('q-roberto', quote_fun_arg('roberto')) 50 | end) 51 | 52 | describe(".new", function() 53 | before_each(function() 54 | Model = SqlOrm.define_model(MySql, 'users') 55 | end) 56 | 57 | it("returns a new instance of Model", function() 58 | local model = Model.new({ first_name = 'roberto', last_name = 'gin' }) 59 | assert.are.same({ first_name = 'roberto', last_name = 'gin' }, model) 60 | end) 61 | end) 62 | 63 | describe(".create", function() 64 | before_each(function() 65 | MySql.execute_and_return_last_id = function(self, sql) 66 | sql_arg = sql 67 | return 10 68 | end 69 | 70 | package.loaded['gin.db.sql.mysql.orm'] = { 71 | new = function(table_name, quote_fun) 72 | return { 73 | table_name = table_name, 74 | quote = quote_fun, 75 | create = function(self, attrs) 76 | attrs_arg = helpers.shallowcopy(attrs) 77 | return "SQL CREATE" 78 | end 79 | } 80 | end 81 | } 82 | Model = SqlOrm.define_model(MySql, 'users') 83 | end) 84 | 85 | after_each(function() 86 | attrs_arg = nil 87 | sql_arg = nil 88 | end) 89 | 90 | it("calls the orm with the correct params", function() 91 | Model.create({ first_name = 'roberto', last_name = 'gin' }) 92 | assert.are.same({ first_name = 'roberto', last_name = 'gin' }, attrs_arg) 93 | assert.are.same("id", Model.__id_col) 94 | end) 95 | 96 | it("may use table_name .. '_id' as primary key", function() 97 | Model = SqlOrm.define_model(MySql, 'users', true) 98 | assert.are.same("users_id", Model.__id_col) 99 | end) 100 | 101 | it("may use an arbitrary column as primary key", function() 102 | Model = SqlOrm.define_model(MySql, 'users', 'my_primary_column') 103 | assert.are.same("my_primary_column", Model.__id_col) 104 | end) 105 | 106 | it("calls execute_and_return_last_id with the correct params", function() 107 | Model.create({ first_name = 'roberto', last_name = 'gin' }) 108 | assert.are.same("SQL CREATE", sql_arg) 109 | end) 110 | 111 | it("returns a new model", function() 112 | local model = Model.create({ first_name = 'roberto', last_name = 'gin' }) 113 | assert.are.same({ id = 10, first_name = 'roberto', last_name = 'gin' }, model) 114 | end) 115 | 116 | it("returns a new model with table name based id", function() 117 | Model = SqlOrm.define_model(MySql, 'users', true) 118 | local model = Model.create({ first_name = 'roberto', last_name = 'gin' }) 119 | assert.are.same({ users_id = 10, first_name = 'roberto', last_name = 'gin' }, model) 120 | end) 121 | 122 | it("returns a new model with arbitrary id", function() 123 | Model = SqlOrm.define_model(MySql, 'users', 'my_primary_column') 124 | local model = Model.create({ first_name = 'roberto', last_name = 'gin' }) 125 | assert.are.same({ my_primary_column = 10, first_name = 'roberto', last_name = 'gin' }, model) 126 | end) 127 | 128 | end) 129 | 130 | describe(".where", function() 131 | before_each(function() 132 | MySql.execute = function(self, sql) 133 | sql_arg = sql 134 | return { 135 | { first_name = 'roberto', last_name = 'gin' }, 136 | { first_name = 'hedy', last_name = 'tonic' } 137 | } 138 | end 139 | 140 | package.loaded['gin.db.sql.mysql.orm'] = { 141 | new = function(table_name, quote_fun) 142 | return { 143 | table_name = table_name, 144 | quote = quote_fun, 145 | where = function(self, ...) 146 | attrs_arg, options_arg = ... 147 | return "SQL WHERE" 148 | end 149 | } 150 | end 151 | } 152 | Model = SqlOrm.define_model(MySql, 'users') 153 | end) 154 | 155 | after_each(function() 156 | attrs_arg = nil 157 | options_arg = nil 158 | sql_arg = nil 159 | end) 160 | 161 | it("calls the orm with the correct params and options", function() 162 | Model.where({ first_name = 'roberto', last_name = 'gin' }, "options") 163 | assert.are.same({ first_name = 'roberto', last_name = 'gin' }, attrs_arg) 164 | assert.are.same("options", options_arg) 165 | end) 166 | 167 | it("calls execute with the correct params", function() 168 | Model.where({ first_name = 'roberto', last_name = 'gin' }) 169 | assert.are.same("SQL WHERE", sql_arg) 170 | end) 171 | 172 | it("returns the models", function() 173 | local models = Model.where() -- params are stubbed in the execute return 174 | 175 | assert.are.equal(2, #models) 176 | local roberto = models[1] 177 | assert.are.same({ first_name = 'roberto', last_name = 'gin' }, roberto) 178 | local hedy = models[2] 179 | assert.are.same({ first_name = 'hedy', last_name = 'tonic' }, hedy) 180 | end) 181 | end) 182 | 183 | describe(".all", function() 184 | before_each(function() 185 | Model = SqlOrm.define_model(MySql, 'users') 186 | Model.where = function(...) 187 | attrs_arg, options_arg = ... 188 | return 'all models' 189 | end 190 | end) 191 | 192 | after_each(function() 193 | attrs_arg = nil 194 | options_arg = nil 195 | end) 196 | 197 | it("calls where with the correct options", function() 198 | local models = Model.all("options") 199 | 200 | assert.are.same({}, attrs_arg) 201 | assert.are.same("options", options_arg) 202 | assert.are.same("all models", models) 203 | end) 204 | end) 205 | 206 | describe(".find_by", function() 207 | before_each(function() 208 | Model = SqlOrm.define_model(MySql, 'users') 209 | Model.where = function(...) 210 | attrs_arg, options_arg = ... 211 | return { 'first model' } 212 | end 213 | end) 214 | 215 | after_each(function() 216 | attrs_arg = nil 217 | options_arg = nil 218 | end) 219 | 220 | describe("when called without options", function() 221 | it("calls .where with limit 1", function() 222 | local model = Model.find_by({ first_name = 'roberto' }) 223 | 224 | assert.are.same({ first_name = 'roberto' }, attrs_arg) 225 | assert.are.same({ limit = 1 }, options_arg) 226 | assert.are.same("first model", model) 227 | end) 228 | end) 229 | 230 | describe("when called with options", function() 231 | it("calls .where with limit 1 keeping only the order option", function() 232 | local model = Model.find_by({ first_name = 'roberto' }, { limit = 10, offset = 5, order = "first_name DESC" }) 233 | 234 | assert.are.same({ first_name = 'roberto' }, attrs_arg) 235 | assert.are.same({ limit = 1, order = "first_name DESC" }, options_arg) 236 | assert.are.same("first model", model) 237 | end) 238 | end) 239 | end) 240 | 241 | describe(".delete_where", function() 242 | before_each(function() 243 | MySql.execute = function(self, sql) 244 | sql_arg = sql 245 | return 1 246 | end 247 | 248 | package.loaded['gin.db.sql.mysql.orm'] = { 249 | new = function(table_name, quote_fun) 250 | return { 251 | table_name = table_name, 252 | quote = quote_fun, 253 | delete_where = function(self, ...) 254 | attrs_arg, options_arg = ... 255 | return "SQL DELETE WHERE" 256 | end 257 | } 258 | end 259 | } 260 | Model = SqlOrm.define_model(MySql, 'users') 261 | end) 262 | 263 | after_each(function() 264 | attrs_arg = nil 265 | options_arg = nil 266 | sql_arg = nil 267 | end) 268 | 269 | it("calls the orm with the correct params and options", function() 270 | Model.delete_where({ first_name = 'roberto', last_name = 'gin' }, "options") 271 | assert.are.same({ first_name = 'roberto', last_name = 'gin' }, attrs_arg) 272 | assert.are.same("options", options_arg) 273 | end) 274 | 275 | it("calls execute with the correct params", function() 276 | Model.delete_where({ first_name = 'roberto', last_name = 'gin' }) 277 | assert.are.same("SQL DELETE WHERE", sql_arg) 278 | end) 279 | 280 | it("returns the result", function() 281 | local result = Model.delete_where() -- params are stubbed in the execute return 282 | assert.are.equal(1, result) 283 | end) 284 | end) 285 | 286 | describe(".delete_all", function() 287 | before_each(function() 288 | Model = SqlOrm.define_model(MySql, 'users') 289 | Model.delete_where = function(...) 290 | attrs_arg, options_arg = ... 291 | return 10 292 | end 293 | end) 294 | 295 | after_each(function() 296 | attrs_arg = nil 297 | options_arg = nil 298 | end) 299 | 300 | it("calls where with the correct options", function() 301 | local result = Model.delete_all("options") 302 | 303 | assert.are.same({}, attrs_arg) 304 | assert.are.same("options", options_arg) 305 | assert.are.same(10, result) 306 | end) 307 | end) 308 | 309 | describe(".update_where", function() 310 | before_each(function() 311 | MySql.execute = function(self, sql) 312 | sql_arg = sql 313 | return 1 314 | end 315 | 316 | package.loaded['gin.db.sql.mysql.orm'] = { 317 | new = function(table_name, quote_fun) 318 | return { 319 | table_name = table_name, 320 | quote = quote_fun, 321 | update_where = function(self, ...) 322 | attrs_arg, options_arg = ... 323 | return "SQL UPDATE WHERE" 324 | end 325 | } 326 | end 327 | } 328 | Model = SqlOrm.define_model(MySql, 'users') 329 | end) 330 | 331 | after_each(function() 332 | attrs_arg = nil 333 | options_arg = nil 334 | sql_arg = nil 335 | end) 336 | 337 | it("calls the orm with the correct params and options", function() 338 | Model.update_where({ first_name = 'roberto', last_name = 'gin' }, "options") 339 | assert.are.same({ first_name = 'roberto', last_name = 'gin' }, attrs_arg) 340 | assert.are.same("options", options_arg) 341 | end) 342 | 343 | it("calls execute with the correct params", function() 344 | Model.update_where({ first_name = 'roberto', last_name = 'gin' }) 345 | assert.are.same("SQL UPDATE WHERE", sql_arg) 346 | end) 347 | 348 | it("returns the result", function() 349 | local result = Model.update_where() -- params are stubbed in the execute return 350 | assert.are.equal(1, result) 351 | end) 352 | end) 353 | 354 | describe("#save", function() 355 | before_each(function() 356 | Model = SqlOrm.define_model(MySql, 'users') 357 | end) 358 | 359 | after_each(function() 360 | attrs_arg = nil 361 | options_arg = nil 362 | end) 363 | 364 | describe("when the instance is already saved", function() 365 | before_each(function() 366 | Model.update_where = function(attrs, options) 367 | attrs_arg = helpers.shallowcopy(attrs) 368 | options_arg = options 369 | return 1 370 | end 371 | model = Model.new({ id = 4, first_name = 'roberto', last_name = 'gin' }) 372 | end) 373 | 374 | after_each(function() 375 | model = nil 376 | end) 377 | 378 | it("calls update_where with the the correct parameters", function() 379 | local result = model:save() 380 | 381 | assert.are.same({ first_name = 'roberto', last_name = 'gin' }, attrs_arg) 382 | assert.are.same({ id = 4 }, options_arg) 383 | assert.are.same(1, result) 384 | end) 385 | end) 386 | 387 | describe("when the instance has not been saved yet", function() 388 | before_each(function() 389 | Model.create = function(attrs) 390 | attrs_arg = helpers.shallowcopy(attrs) 391 | attrs.id = 12 392 | return attrs 393 | end 394 | model = Model.new({ first_name = 'roberto', last_name = 'gin' }) 395 | end) 396 | 397 | after_each(function() 398 | model = nil 399 | end) 400 | 401 | it("calls create with the the correct parameters", function() 402 | model:save() 403 | 404 | assert.are.same({ first_name = 'roberto', last_name = 'gin' }, attrs_arg) 405 | assert.are.same(12, model.id) 406 | end) 407 | end) 408 | end) 409 | 410 | describe("#delete", function() 411 | before_each(function() 412 | Model = SqlOrm.define_model(MySql, 'users') 413 | Model.delete_where = function(attrs, options) 414 | attrs_arg = helpers.shallowcopy(attrs) 415 | options_arg = options 416 | return 1 417 | end 418 | end) 419 | 420 | after_each(function() 421 | attrs_arg = nil 422 | options_arg = nil 423 | end) 424 | 425 | describe("when the instance is persisted", function() 426 | before_each(function() 427 | model = Model.new({ id = 4, first_name = 'roberto', last_name = 'gin' }) 428 | end) 429 | 430 | after_each(function() 431 | model = nil 432 | end) 433 | 434 | it("calls delete_where with the the correct parameters", function() 435 | local result = model:delete() 436 | 437 | assert.are.same({ id = 4 }, attrs_arg) 438 | assert.are.same(nil, options_arg) 439 | assert.are.same(1, result) 440 | end) 441 | end) 442 | 443 | describe("when the instance is not persisted", function() 444 | before_each(function() 445 | model = Model.new({ first_name = 'roberto', last_name = 'gin' }) 446 | end) 447 | 448 | after_each(function() 449 | model = nil 450 | end) 451 | 452 | it("returns an error", function() 453 | local ok, err = pcall(function() return model:delete() end) 454 | 455 | assert.are.equal(false, ok) 456 | assert.are.equal(true, string.find(err, "cannot delete a model without an id") > 0) 457 | end) 458 | end) 459 | end) 460 | end) 461 | end) 462 | -------------------------------------------------------------------------------- /spec/db/sql/postgresql/orm_spec.lua: -------------------------------------------------------------------------------- 1 | require 'spec.spec_helper' 2 | 3 | 4 | describe("PostgreSqlOrm", function() 5 | 6 | before_each(function() 7 | PostgreSqlOrm = require 'gin.db.sql.postgresql.orm' 8 | local quote_fun = function(str) return "'q-".. str .. "'" end 9 | orm = PostgreSqlOrm.new('users', quote_fun) 10 | end) 11 | 12 | after_each(function() 13 | package.loaded['gin.db.sql.postgresql.orm'] = nil 14 | PostgreSqlOrm = nil 15 | orm = nil 16 | end) 17 | 18 | describe("#create", function() 19 | describe("when attrs are specified", function() 20 | it("creates a new entry", function() 21 | local sql = orm:create({ first_name = 'roberto', last_name = 'gin', age = 3, seen_at = '2013-10-12T16:31:21 UTC' }) 22 | assert.are.equal(112, #sql) 23 | assert.are.equal("INSERT INTO users (", sql:sub(1, 19)) 24 | assert.is.equal(52, (sql:find(") VALUES (", 20, true))) 25 | assert.are.equal(");", sql:sub(-2)) 26 | assert.is_number(sql:find("seen_at", 20, true)) 27 | assert.is_number(sql:find("last_name", 20, true)) 28 | assert.is_number(sql:find("first_name", 20, true)) 29 | assert.is_number(sql:find("age", 20, true)) 30 | assert.is_number(sql:find("'q-2013-10-12T16:31:21 UTC'", 62, true)) 31 | assert.is_number(sql:find("'q-gin'", 62, true)) 32 | assert.is_number(sql:find("'q-roberto'", 62, true)) 33 | assert.is_number(sql:find("3", 62, true)) 34 | end) 35 | end) 36 | 37 | describe("when no attrs are specified", function() 38 | it("raises an error", function() 39 | local ok, err = pcall(function() return orm:create() end) 40 | 41 | assert.are.equal(false, ok) 42 | assert.are.equal(true, string.find(err, "no attributes were specified to create new model instance") > 0) 43 | end) 44 | end) 45 | end) 46 | 47 | describe("#where", function() 48 | describe("when attrs are specified", function() 49 | describe("when attrs are a table", function() 50 | describe("when no options are specified", function() 51 | it("finds and returns models without options", function() 52 | local sql = orm:where({ first_name = 'roberto', last_name = 'gin', age = 3, seen_at = '2013-10-12T16:31:21 UTC' }) 53 | assert.are.equal(123, #sql) 54 | assert.are.equal("SELECT * FROM users WHERE (",sql:sub(1,27)) 55 | assert.are.equal(");",sql:sub(-2)) 56 | 57 | local andpos = sql:find(" AND ",28,true) 58 | assert.is_number(andpos) 59 | andpos = sql:find(" AND ",1+andpos,true) 60 | assert.is_number(andpos) 61 | andpos = sql:find(" AND ",1+andpos,true) 62 | assert.is_number(andpos) 63 | assert.is_nil(sql:find(" AND ",1+andpos,true)) 64 | 65 | assert.is_number(sql:find("seen_at='q-2013-10-12T16:31:21 UTC'",28,true)) 66 | assert.is_number(sql:find("last_name='q-gin'",28,true)) 67 | assert.is_number(sql:find("first_name='q-roberto'",28,true)) 68 | assert.is_number(sql:find("age=3",28,true)) 69 | end) 70 | end) 71 | 72 | describe("when the limit option is specified", function() 73 | it("finds models with limit", function() 74 | local sql = orm:where({ first_name = 'roberto'}, { limit = 12 }) 75 | assert.are.equal("SELECT * FROM users WHERE (first_name='q-roberto') LIMIT 12;", sql) 76 | end) 77 | end) 78 | 79 | describe("when the offset option is specified", function() 80 | it("finds models with offset", function() 81 | local sql = orm:where({ first_name = 'roberto'}, { offset = 10 }) 82 | assert.are.equal("SELECT * FROM users WHERE (first_name='q-roberto') OFFSET 10;", sql) 83 | end) 84 | end) 85 | 86 | describe("when the order option is specified", function() 87 | it("order model results", function() 88 | local sql = orm:where({ first_name = 'roberto'}, { order = "first_name DESC" }) 89 | assert.are.equal("SELECT * FROM users WHERE (first_name='q-roberto') ORDER BY first_name DESC;", sql) 90 | end) 91 | end) 92 | 93 | describe("when the order, limit and offset options are specified", function() 94 | it("finds models with offset", function() 95 | local sql = orm:where({ first_name = 'roberto'}, { order = "first_name DESC", limit = 12, offset = 10 }) 96 | assert.are.equal("SELECT * FROM users WHERE (first_name='q-roberto') ORDER BY first_name DESC LIMIT 12 OFFSET 10;", sql) 97 | end) 98 | end) 99 | end) 100 | 101 | describe("when attrs are a table", function() 102 | describe("when no options are specified", function() 103 | it("finds and returns models without options", function() 104 | local sql = orm:where("age > 3") 105 | assert.are.equal("SELECT * FROM users WHERE (age > 3);", sql) 106 | end) 107 | end) 108 | 109 | describe("when the limit option is specified", function() 110 | it("finds models with limit", function() 111 | local sql = orm:where("age > 3", { limit = 12 }) 112 | assert.are.equal("SELECT * FROM users WHERE (age > 3) LIMIT 12;", sql) 113 | end) 114 | end) 115 | 116 | describe("when the offset option is specified", function() 117 | it("finds models with offset", function() 118 | local sql = orm:where("age > 3", { offset = 10 }) 119 | assert.are.equal("SELECT * FROM users WHERE (age > 3) OFFSET 10;", sql) 120 | end) 121 | end) 122 | 123 | describe("when the order option is specified", function() 124 | it("order model results", function() 125 | local sql = orm:where("age > 3", { order = "first_name DESC" }) 126 | assert.are.equal("SELECT * FROM users WHERE (age > 3) ORDER BY first_name DESC;", sql) 127 | end) 128 | end) 129 | 130 | describe("when the order, limit and offset options are specified", function() 131 | it("finds models with offset", function() 132 | local sql = orm:where("age > 3", { order = "first_name DESC", limit = 12, offset = 10 }) 133 | assert.are.equal("SELECT * FROM users WHERE (age > 3) ORDER BY first_name DESC LIMIT 12 OFFSET 10;", sql) 134 | end) 135 | end) 136 | end) 137 | end) 138 | 139 | describe("when no attrs are specified", function() 140 | describe("when no options are specified", function() 141 | it("finds all models", function() 142 | local sql = orm:where() 143 | assert.are.equal("SELECT * FROM users;", sql) 144 | end) 145 | end) 146 | 147 | describe("when the limit option is specified", function() 148 | it("finds models with limit", function() 149 | local sql = orm:where({}, { limit = 12 }) 150 | assert.are.equal("SELECT * FROM users LIMIT 12;", sql) 151 | end) 152 | end) 153 | 154 | describe("when the offset option is specified", function() 155 | it("finds models with offset", function() 156 | local sql = orm:where({}, { offset = 10 }) 157 | assert.are.equal("SELECT * FROM users OFFSET 10;", sql) 158 | end) 159 | end) 160 | 161 | describe("when the order option is specified", function() 162 | it("order model results", function() 163 | local sql = orm:where({}, { order = "first_name DESC" }) 164 | assert.are.equal("SELECT * FROM users ORDER BY first_name DESC;", sql) 165 | end) 166 | end) 167 | 168 | describe("when the order, limit and offset options are specified", function() 169 | it("finds models with offset", function() 170 | local sql = orm:where({ }, { order = "first_name DESC", limit = 12, offset = 10 }) 171 | assert.are.equal("SELECT * FROM users ORDER BY first_name DESC LIMIT 12 OFFSET 10;", sql) 172 | end) 173 | end) 174 | end) 175 | end) 176 | 177 | describe("#delete_where", function() 178 | describe("when attrs are specified", function() 179 | describe("when attrs are a table", function() 180 | describe("when no options are specified", function() 181 | it("calls .delete_where", function() 182 | local sql = orm:delete_where({ first_name = 'roberto', last_name = 'gin', age = 3, seen_at = '2013-10-12T16:31:21 UTC' }) 183 | assert.are.equal(121, #sql) 184 | assert.are.equal("DELETE FROM users WHERE (",sql:sub(1,25)) 185 | assert.are.equal(");",sql:sub(-2)) 186 | 187 | local andpos = sql:find(" AND ",26,true) 188 | assert.is_number(andpos) 189 | andpos = sql:find(" AND ",1+andpos,true) 190 | assert.is_number(andpos) 191 | andpos = sql:find(" AND ",1+andpos,true) 192 | assert.is_number(andpos) 193 | assert.is_nil(sql:find(" AND ",1+andpos,true)) 194 | 195 | assert.is_number(sql:find("seen_at='q-2013-10-12T16:31:21 UTC'",26,true)) 196 | assert.is_number(sql:find("last_name='q-gin'",26,true)) 197 | assert.is_number(sql:find("first_name='q-roberto'",26,true)) 198 | assert.is_number(sql:find("age=3",26,true)) 199 | end) 200 | end) 201 | 202 | describe("when the limit option is specified", function() 203 | it("calls .delete_where with limit", function() 204 | local sql = orm:delete_where({ first_name = 'roberto'}, { limit = 12 }) 205 | assert.are.equal("DELETE FROM users WHERE (first_name='q-roberto') LIMIT 12;", sql) 206 | end) 207 | end) 208 | end) 209 | 210 | describe("when attrs are a string", function() 211 | describe("when no options are specified", function() 212 | it("calls .delete_where", function() 213 | local sql = orm:delete_where("age > 3") 214 | assert.are.equal("DELETE FROM users WHERE (age > 3);", sql) 215 | end) 216 | end) 217 | 218 | describe("when the limit option is specified", function() 219 | it("calls .delete_where with limit", function() 220 | local sql = orm:delete_where("age > 3", { limit = 12 }) 221 | assert.are.equal("DELETE FROM users WHERE (age > 3) LIMIT 12;", sql) 222 | end) 223 | end) 224 | end) 225 | end) 226 | 227 | describe("when attrs are not specified", function() 228 | describe("when no options are specified", function() 229 | it("calls .delete_where", function() 230 | local sql = orm:delete_where() 231 | assert.are.equal("DELETE FROM users;", sql) 232 | end) 233 | end) 234 | 235 | describe("when the limit option is specified", function() 236 | it("calls .delete_where with limit", function() 237 | local sql = orm:delete_where({}, { limit = 12 }) 238 | assert.are.equal("DELETE FROM users LIMIT 12;", sql) 239 | end) 240 | end) 241 | end) 242 | end) 243 | 244 | describe("#update_where", function() 245 | describe("when no attrs are specified", function() 246 | it("raises an error", function() 247 | local ok, err = pcall(function() return orm:update_where() end) 248 | 249 | assert.are.equal(false, ok) 250 | assert.are.equal(true, string.find(err, "no attributes were specified to create new model instance") > 0) 251 | end) 252 | end) 253 | 254 | describe("when attrs are specified", function() 255 | describe("when no where is specified", function() 256 | it("calls .update_where", function() 257 | local sql = orm:update_where({ first_name = 'roberto', last_name = 'gin', age = 3, seen_at = '2013-10-12T16:31:21 UTC' }) 258 | assert.are.equal(100, #sql) 259 | assert.are.equal("UPDATE users SET ",sql:sub(1,17)) 260 | assert.are.equal(";",sql:sub(-1)) 261 | assert.is_number(sql:find("seen_at='q-2013-10-12T16:31:21 UTC'",18,true)) 262 | assert.is_number(sql:find("last_name='q-gin'",18,true)) 263 | assert.is_number(sql:find("first_name='q-roberto'",18,true)) 264 | assert.is_number(sql:find("age=3",18,true)) 265 | end) 266 | end) 267 | 268 | describe("when where is specified", function() 269 | describe("and where is a table", function() 270 | it("calls .update_where", function() 271 | local sql = orm:update_where( 272 | { first_name = 'roberto', last_name = 'gin', age = 3, seen_at = '2013-10-12T16:31:21 UTC' }, 273 | { id = 4, first_name = 'robbb' } 274 | ) 275 | assert.are.equal(138, #sql) 276 | assert.are.equal("UPDATE users SET ",sql:sub(1,17)) 277 | assert.are.equal(" WHERE (",sql:sub(-39,-32)) 278 | assert.are.equal(");",sql:sub(-2)) 279 | assert.is_number(sql:find(" AND ",-39,true)) 280 | assert.is_number(sql:find("first_name='q-robbb'",-39,true)) 281 | assert.is_number(sql:find("id=4",-39,true)) 282 | assert.is_number(sql:find("seen_at='q-2013-10-12T16:31:21 UTC'",18,true)) 283 | assert.is_number(sql:find("last_name='q-gin'",18,true)) 284 | assert.is_number(sql:find("first_name='q-roberto'",18,true)) 285 | assert.is_number(sql:find("age=3",18,true)) 286 | end) 287 | end) 288 | 289 | describe("and where is a string", function() 290 | it("calls .update_where", function() 291 | local sql = orm:update_where( 292 | { first_name = 'roberto', last_name = 'gin', age = 3, seen_at = '2013-10-12T16:31:21 UTC' }, 293 | "age > 3" 294 | ) 295 | assert.are.equal(116, #sql) 296 | assert.are.equal("UPDATE users SET ",sql:sub(1,17)) 297 | assert.are.equal(" WHERE (age > 3);",sql:sub(-17)) 298 | assert.is_number(sql:find("seen_at='q-2013-10-12T16:31:21 UTC'",18,true)) 299 | assert.is_number(sql:find("last_name='q-gin'",18,true)) 300 | assert.is_number(sql:find("first_name='q-roberto'",18,true)) 301 | assert.is_number(sql:find("age=3",18,true)) 302 | end) 303 | end) 304 | end) 305 | end) 306 | end) 307 | end) -------------------------------------------------------------------------------- /spec/db/sql_spec.lua: -------------------------------------------------------------------------------- 1 | require 'spec.spec_helper' 2 | 3 | 4 | describe("Database SQL", function() 5 | before_each(function() 6 | SqlDatabase = require 'gin.db.sql' 7 | 8 | options = { 9 | adapter = 'mysql', 10 | host = "127.0.0.1", 11 | port = 3306, 12 | database = "gin_development", 13 | user = "root", 14 | password = "", 15 | pool = 5 16 | } 17 | 18 | package.loaded['gin.db.sql.mysql.adapter'] = { 19 | name = 'adapter' 20 | } 21 | end) 22 | 23 | 24 | after_each(function() 25 | package.loaded['gin.db.sql'] = nil 26 | SqlDatabase = nil 27 | options = nil 28 | package.loaded['gin.db.sql.mysql.adapter'] = nil 29 | end) 30 | 31 | describe(".new", function() 32 | describe("when all the required options are passed", function() 33 | it("initializes an instance", function() 34 | local DB = SqlDatabase.new(options) 35 | assert.are.equal(options, DB.options) 36 | assert.are.equal('adapter', DB.adapter.name) 37 | end) 38 | end) 39 | 40 | describe("when not all the required options are passed", function() 41 | it("raises an error", function() 42 | options = { 43 | adapter = 'mysql', 44 | host = "127.0.0.1", 45 | user = "root", 46 | password = "", 47 | pool = 5 48 | } 49 | 50 | local ok, err = pcall(function() return db.new(options) end) 51 | assert.are.equal(false, ok) 52 | assert.are.not_equals(true, string.match(err, "missing required database options: database, port")) 53 | end) 54 | end) 55 | end) 56 | 57 | describe("#execute", function() 58 | before_each(function() 59 | package.loaded['gin.db.sql.mysql.adapter'].execute = function(...) 60 | options_arg, sql_arg = ... 61 | end 62 | end) 63 | 64 | after_each(function() 65 | options_arg = nil 66 | sql_arg = nil 67 | end) 68 | 69 | it("calls execute on the adapter", function() 70 | local DB = SqlDatabase.new(options) 71 | 72 | DB:execute("SELECT 1;") 73 | 74 | assert.are.equal(options, options_arg) 75 | assert.are.equal("SELECT 1;", sql_arg) 76 | end) 77 | end) 78 | 79 | describe("#execute_and_return_last_id", function() 80 | before_each(function() 81 | package.loaded['gin.db.sql.mysql.adapter'].execute_and_return_last_id = function(...) 82 | options_arg = ... 83 | end 84 | end) 85 | 86 | after_each(function() 87 | options_arg = nil 88 | end) 89 | 90 | it("calls execute_and_return_last_id on the adapter", function() 91 | local DB = SqlDatabase.new(options) 92 | 93 | DB:execute_and_return_last_id() 94 | 95 | assert.are.equal(options, options_arg) 96 | end) 97 | end) 98 | 99 | describe("#quote", function() 100 | before_each(function() 101 | package.loaded['gin.db.sql.mysql.adapter'].quote = function(...) 102 | options_arg, str_arg = ... 103 | end 104 | end) 105 | 106 | after_each(function() 107 | options_arg = nil 108 | str_arg = nil 109 | end) 110 | 111 | it("calls quote on the adapter", function() 112 | local DB = SqlDatabase.new(options) 113 | 114 | DB:quote("string") 115 | 116 | assert.are.equal(options, options_arg) 117 | assert.are.equal("string", str_arg) 118 | end) 119 | end) 120 | 121 | describe("#tables", function() 122 | before_each(function() 123 | package.loaded['gin.db.sql.mysql.adapter'].tables = function(...) 124 | options_arg = ... 125 | end 126 | end) 127 | 128 | after_each(function() 129 | options_arg = nil 130 | end) 131 | 132 | it("calls tables on the adapter", function() 133 | local DB = SqlDatabase.new(options) 134 | 135 | DB:tables() 136 | 137 | assert.are.equal(options, options_arg) 138 | end) 139 | end) 140 | 141 | describe("#schema", function() 142 | before_each(function() 143 | package.loaded['gin.db.sql.mysql.adapter'].schema = function(...) 144 | options_arg = ... 145 | end 146 | end) 147 | 148 | after_each(function() 149 | options_arg = nil 150 | end) 151 | 152 | it("calls schema on the adapter", function() 153 | local DB = SqlDatabase.new(options) 154 | 155 | DB:schema() 156 | 157 | assert.are.equal(options, options_arg) 158 | end) 159 | end) 160 | end) -------------------------------------------------------------------------------- /spec/spec/runners/integration_spec.lua: -------------------------------------------------------------------------------- 1 | require 'spec.spec_helper' 2 | 3 | 4 | describe("Integration", function() 5 | before_each(function() 6 | IntegrationRunner = require 'gin.spec.runners.integration' 7 | end) 8 | 9 | after_each(function() 10 | package.loaded['gin.spec.runners.integration'] = nil 11 | IntegrationRunner = nil 12 | end) 13 | 14 | describe(".encode_table", function() 15 | it("encodes a table into querystring params", function() 16 | args = { 17 | arg1 = 1.0, 18 | arg2 = { "two/string", "another/string" }, 19 | ['arg~3'] = "a tag", 20 | [5] = "five" 21 | } 22 | 23 | local urlencoded = IntegrationRunner.encode_table(args) 24 | 25 | assert.are.equal(67, #urlencoded) 26 | 27 | local amppos = urlencoded:find("&", 6, true) 28 | assert.is.number(amppos) 29 | amppos = urlencoded:find("&", 1 + amppos, true) 30 | assert.is.number(amppos) 31 | amppos = urlencoded:find("&", 1 + amppos, true) 32 | assert.is.number(amppos) 33 | amppos = urlencoded:find("&", 1 + amppos, true) 34 | assert.is.number(amppos) 35 | assert.is_nil(urlencoded:find("&", 1 + amppos, true)) 36 | 37 | assert.is_number(urlencoded:find("arg~3=a%20tag", 1, true)) 38 | assert.is_number(urlencoded:find("arg2=two%2fstring", 1, true)) 39 | assert.is_number(urlencoded:find("arg2=another%2fstring", 1, true)) 40 | assert.is_number(urlencoded:find("5=five", 1, true)) 41 | assert.is_number(urlencoded:find("arg1=1", 1, true)) 42 | end) 43 | end) 44 | 45 | describe(".hit", function() 46 | before_each(function() 47 | require 'gin.cli.launcher' 48 | stub(package.loaded['gin.cli.launcher'], "start") 49 | stub(package.loaded['gin.cli.launcher'], "stop") 50 | 51 | require 'socket.http' 52 | request = nil 53 | package.loaded['socket.http'].request = function(...) 54 | request = ... 55 | return true, 201, { ['Some-Header'] = 'some-header-value' } 56 | end 57 | 58 | local info 59 | 60 | IntegrationRunner.source_for_caller_at = function(...) 61 | return "/controllers/1/controller_spec.lua" 62 | end 63 | end) 64 | 65 | after_each(function() 66 | package.loaded['gin.cli.launcher'] = nil 67 | package.loaded['socket.http'] = nil 68 | request = nil 69 | end) 70 | 71 | it("ensures content length is set", function() 72 | IntegrationRunner.hit({ 73 | method = 'GET', 74 | path = "/", 75 | body = { name = 'gin' } 76 | }) 77 | 78 | assert.are.same(14, request.headers["Content-Length"]) 79 | end) 80 | 81 | it("raises an error when the caller major version cannot be retrieved", function() 82 | IntegrationRunner.source_for_caller_at = function(...) 83 | return "controller_spec.lua" 84 | end 85 | 86 | local ok, err = pcall(function() 87 | return IntegrationRunner.hit({ 88 | method = 'GET', 89 | path = "/" 90 | }) 91 | end) 92 | 93 | assert.are.equal(false, ok) 94 | assert.are.equal(true, string.find(err, "Could not determine API major version from controller spec file. Ensure to follow naming conventions") > 0) 95 | end) 96 | 97 | it("raises an error when the caller major version does not match the specified api_version", function() 98 | local ok, err = pcall(function() 99 | return IntegrationRunner.hit({ 100 | api_version = '2', 101 | method = 'GET', 102 | path = "/" 103 | }) 104 | end) 105 | 106 | assert.are.equal(false, ok) 107 | assert.are.equal(true, string.find(err, "Specified API version 2 does not match controller spec namespace %(1%)") > 0) 108 | end) 109 | 110 | describe("Accept header", function() 111 | describe("when no api_version is specified", function() 112 | it("sets the accept header from the namespace", function() 113 | local response = IntegrationRunner.hit({ 114 | method = 'GET', 115 | path = "/" 116 | }) 117 | 118 | assert.are.same("application/vnd.ginapp.v1+json", request.headers["Accept"]) 119 | end) 120 | end) 121 | 122 | describe("when a specifid api_version is specified", function() 123 | it("sets the accept header from the namespace", function() 124 | local response = IntegrationRunner.hit({ 125 | api_version = '1.2.3-p247', 126 | method = 'GET', 127 | path = "/" 128 | }) 129 | 130 | assert.are.same("application/vnd.ginapp.v1.2.3-p247+json", request.headers["Accept"]) 131 | end) 132 | end) 133 | end) 134 | 135 | it("calls the server with the correct parameters", function() 136 | local request_body_arg 137 | ltn12.source.string = function(request_body) 138 | request_body_arg = request_body 139 | return request_body 140 | end 141 | 142 | local request_body_arg 143 | ltn12.sink.table = function(request_body) 144 | request_body_arg = request_body 145 | return request_body 146 | end 147 | 148 | IntegrationRunner.hit({ 149 | method = 'GET', 150 | path = "/", 151 | headers = { ['Test-Header'] = 'test-header-value' }, 152 | body = { name = 'gin' } 153 | }) 154 | 155 | assert.are.equal("http://127.0.0.1:7201/?", request.url) 156 | assert.are.equal('GET', request.method) 157 | assert.are.same('test-header-value', request.headers['Test-Header']) 158 | assert.are.same('{"name":"gin"}', request.source) 159 | assert.are.same(request_body_arg, request.sink) 160 | end) 161 | 162 | it("returns a ResponseSpec", function() 163 | local response = IntegrationRunner.hit({ 164 | method = 'GET', 165 | path = "/" 166 | }) 167 | 168 | assert.are.equal(201, response.status) 169 | assert.are.same({ ['Some-Header'] = 'some-header-value' }, response.headers) 170 | end) 171 | end) 172 | end) 173 | -------------------------------------------------------------------------------- /spec/spec/runners/response_spec.lua: -------------------------------------------------------------------------------- 1 | require 'spec.spec_helper' 2 | 3 | 4 | describe("ResponseSpec", function() 5 | before_each(function() 6 | ResponseSpec = require 'gin.spec.runners.response' 7 | end) 8 | 9 | after_each(function() 10 | package.loaded['gin.spec.runners.response'] = nil 11 | ResponseSpec = nil 12 | end) 13 | 14 | describe(".new", function() 15 | describe("when no options are passed in", function() 16 | it("initializes an instance with defaults", function() 17 | local response = ResponseSpec.new() 18 | 19 | assert.are.same(nil, response.status) 20 | assert.are.same({}, response.headers) 21 | assert.are.same({}, response.body) 22 | assert.are.same(nil, response.body_raw) 23 | end) 24 | end) 25 | 26 | describe("when a blank body string is passed in", function() 27 | it("initializes an instance with defaults", function() 28 | local response = ResponseSpec.new({ 29 | body = "" 30 | }) 31 | 32 | assert.are.same(nil, response.status) 33 | assert.are.same({}, response.headers) 34 | assert.are.same({}, response.body) 35 | assert.are.same("", response.body_raw) 36 | end) 37 | end) 38 | 39 | describe("when an html body string is passed in", function() 40 | it("initializes an instance with defaults", function() 41 | local response = ResponseSpec.new({ 42 | body = "

404 Not Found

" 43 | }) 44 | 45 | assert.are.same(nil, response.status) 46 | assert.are.same({}, response.headers) 47 | assert.are.same(nil, response.body) 48 | assert.are.same("

404 Not Found

", response.body_raw) 49 | end) 50 | end) 51 | 52 | describe("when options are passed in", function() 53 | it("saves them to the instance", function() 54 | local response = ResponseSpec.new({ 55 | status = 403, 56 | headers = { ["X-Custom"] = "custom" }, 57 | body = '{"name":"gin"}' 58 | }) 59 | 60 | assert.are.same(403, response.status) 61 | assert.are.same({ ["X-Custom"] = "custom" }, response.headers) 62 | assert.are.same({ name = "gin" }, response.body) 63 | assert.are.same('{"name":"gin"}', response.body_raw) 64 | end) 65 | end) 66 | end) 67 | end) -------------------------------------------------------------------------------- /spec/spec_helper.lua: -------------------------------------------------------------------------------- 1 | -- mock application modules here 2 | package.loaded['config.routes'] = { } 3 | package.loaded['config.application'] = { name = "ginapp" } 4 | 5 | -- init 6 | require 'gin.spec.init' 7 | --------------------------------------------------------------------------------