├── .github └── workflows │ ├── build.yml │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── _example ├── .gitignore ├── README.md ├── controllers │ ├── .gitkeep │ ├── company.go │ ├── email.go │ ├── job.go │ ├── profile.go │ ├── root.go │ └── user.go ├── db │ ├── database.db │ ├── db.go │ ├── filter.go │ ├── filter_test.go │ ├── pagination.go │ ├── parameter.go │ ├── sort.go │ └── sort_test.go ├── docs │ ├── .gitkeep │ ├── company.apib │ ├── email.apib │ ├── index.apib │ ├── job.apib │ ├── profile.apib │ └── user.apib ├── helper │ ├── field.go │ └── field_test.go ├── main.go ├── middleware │ └── set_db.go ├── models │ ├── .gitkeep │ ├── company.go │ ├── email.go │ ├── job.go │ ├── profile.go │ └── user.go ├── router │ └── router.go ├── server │ └── server.go └── version │ ├── version.go │ └── version_test.go ├── _templates ├── README.md.tmpl ├── controller.go.tmpl ├── db.go.tmpl ├── index.apib.tmpl ├── model.apib.tmpl ├── root_controller.go.tmpl ├── router.go.tmpl └── skeleton │ ├── .gitignore.tmpl │ ├── README.md.tmpl │ ├── controllers │ └── .gitkeep.tmpl │ ├── db │ ├── db.go.tmpl │ ├── filter.go.tmpl │ ├── filter_test.go.tmpl │ ├── pagination.go.tmpl │ ├── parameter.go.tmpl │ ├── sort.go.tmpl │ └── sort_test.go.tmpl │ ├── docs │ └── .gitkeep.tmpl │ ├── helper │ ├── field.go.tmpl │ └── field_test.go.tmpl │ ├── main.go.tmpl │ ├── middleware │ └── set_db.go.tmpl │ ├── models │ └── .gitkeep.tmpl │ ├── router │ └── router.go.tmpl │ ├── server │ └── server.go.tmpl │ └── version │ ├── version.go.tmpl │ └── version_test.go.tmpl ├── apig ├── associate.go ├── associate_test.go ├── detail.go ├── generate.go ├── generate_test.go ├── import.go ├── import_test.go ├── model.go ├── parse.go ├── parse_test.go ├── skeleton.go ├── skeleton_test.go ├── testdata │ ├── README.md │ ├── controllers │ │ ├── root.go │ │ └── user.go │ ├── db │ │ ├── db_mysql.go │ │ ├── db_postgres.go │ │ └── db_sqlite.go │ ├── docs │ │ ├── index.apib │ │ └── user.apib │ ├── models.go │ ├── parse │ │ ├── models.go │ │ └── router.go │ └── router │ │ └── router.go ├── validate.go └── validate_test.go ├── cli.go ├── command ├── gen.go ├── gen_test.go ├── meta.go ├── new.go ├── new_test.go └── version.go ├── commands.go ├── go.mod ├── go.sum ├── main.go ├── msg └── msg.go ├── script └── generation_test.sh ├── util └── util.go └── version.go /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: push 4 | 5 | jobs: 6 | 7 | build: 8 | strategy: 9 | matrix: 10 | go-version: [1.16.x, 1.17.x] 11 | os: [ubuntu-latest] 12 | runs-on: ${{ matrix.os }} 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: ${{ matrix.go-version }} 20 | 21 | - name: Deps 22 | run: | 23 | make deps 24 | - name: Build Test 25 | run: | 26 | make install 27 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: push 4 | 5 | jobs: 6 | 7 | build: 8 | strategy: 9 | matrix: 10 | go-version: [1.16.x, 1.17.x] 11 | os: [ubuntu-latest] 12 | runs-on: ${{ matrix.os }} 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: ${{ matrix.go-version }} 20 | 21 | - name: Deps 22 | run: | 23 | make deps 24 | - name: Test 25 | run: | 26 | make test 27 | - name: Generation Test 28 | run: | 29 | make generation-test 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/go 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=go 3 | 4 | ### Go ### 5 | # Binaries for programs and plugins 6 | *.exe 7 | *.exe~ 8 | *.dll 9 | *.so 10 | *.dylib 11 | 12 | # Test binary, built with `go test -c` 13 | *.test 14 | 15 | # Output of the go coverage tool, specifically when used with LiteIDE 16 | *.out 17 | 18 | # Dependency directories (remove the comment below to include it) 19 | # vendor/ 20 | 21 | ### Go Patch ### 22 | /vendor/ 23 | /Godeps/ 24 | 25 | # End of https://www.toptal.com/developers/gitignore/api/go 26 | # Architecture specific extensions/prefixes 27 | *.[568vq] 28 | [568vq].out 29 | 30 | *.cgo1.go 31 | *.cgo2.c 32 | _cgo_defun.c 33 | _cgo_gotypes.go 34 | _cgo_export.* 35 | 36 | _testmain.go 37 | 38 | *.exe 39 | *.test 40 | *.prof 41 | 42 | /bin/ 43 | 44 | bindata.go 45 | 46 | /_example/_example 47 | /_example/go.mod 48 | /_example/go.sum 49 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.1.0 (2021-11-14) 2 | 3 | Initial release 4 | 5 | ### Added 6 | 7 | - Add Fundamental features 8 | 9 | ### Deprecated 10 | 11 | - Nothing 12 | 13 | ### Removed 14 | 15 | - Nothing 16 | 17 | ### Fixed 18 | 19 | - Nothing 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Wantedly, Inc. 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BINARY := apig 2 | SOURCES := $(shell find . -name '*.go' -type f | grep -v _examples) 3 | 4 | LDFLAGS := -ldflags="-s -w" 5 | 6 | .DEFAULT_GOAL := bin/$(BINARY) 7 | 8 | bin/$(BINARY): deps $(SOURCES) 9 | go generate 10 | go build $(LDFLAGS) -o bin/$(BINARY) 11 | 12 | .PHONY: clean 13 | clean: 14 | rm -fr bin/* 15 | rm -fr vendor/* 16 | 17 | .PHONY: deps 18 | deps: 19 | go get github.com/jteeuwen/go-bindata/... 20 | 21 | .PHONY: install 22 | install: 23 | go generate 24 | go install $(LDFLAGS) 25 | 26 | .PHONY: test 27 | test: 28 | go generate 29 | go test -cover -v ./apig ./command 30 | 31 | .PHONY: generation-test 32 | generation-test: bin/$(BINARY) 33 | script/generation_test.sh 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # apig: Golang RESTful API Server Generator 2 | [![Build](https://github.com/shimastripe/apig/actions/workflows/build.yml/badge.svg)](https://github.com/shimastripe/apig/actions/workflows/build.yml) 3 | [![Test](https://github.com/shimastripe/apig/actions/workflows/test.yml/badge.svg)](https://github.com/shimastripe/apig/actions/workflows/test.yml) 4 | 5 | apig is an RESTful API server generator. 6 | 7 | * Input: Model definitions based on [gorm](https://github.com/jinzhu/gorm) annotated struct 8 | * Output: RESTful JSON API server using [gin](https://github.com/gin-gonic/gin) including tests and documents 9 | 10 | ## Contents 11 | 12 | * [Contents](#contents) 13 | * [How to build and install](#how-to-build-and-install) 14 | * [How to use](#how-to-use) 15 | + [1. Generate boilerplate](#1-generate-boilerplate) 16 | + [2. Write model code](#2-write-model-code) 17 | + [3. Generate controllers, tests, documents etc. based on models.](#3-generate-controllers-tests-documents-etc-based-on-models) 18 | + [4. Build and run server](#4-build-and-run-server) 19 | * [Usage](#usage) 20 | + [`new` command](#new-command) 21 | + [`gen` command](#gen-command) 22 | + [API Document](#api-document) 23 | * [API server specification](#api-server-specification) 24 | + [Endpoints](#endpoints) 25 | + [Available URL parameters](#available-url-parameters) 26 | + [Data Type](#data-type) 27 | + [Pagination](#pagination) 28 | + [Versioning](#versioning) 29 | * [License](#license) 30 | 31 | ## How to build and install 32 | 33 | Go 1.16 or higher is required. 34 | 35 | After installing required version of Go, you can build and install `apig` by 36 | 37 | ```bash 38 | $ go get -d -u github.com/shimastripe/apig 39 | $ cd $GOPATH/src/github.com/shimastripe/apig 40 | $ make 41 | $ make install 42 | ``` 43 | 44 | `make` generates binary into `bin/apig`. 45 | `make install` put it to `$GOPATH/bin`. 46 | 47 | ## How to use 48 | 49 | ### 1. Generate boilerplate 50 | 51 | First, creating by `apig new` command. 52 | 53 | ```bash 54 | $ apig new -u shimastripe apig-sample 55 | ``` 56 | 57 | generates Golang API server boilerplate under `$GOPATH/src/github.com/shimastripe/apig-sample`. 58 | apig supports two database engines; SQLite (`sqlite`) and PostgreSQL (`postgres`) and Mysql (`mysql`). You can specify this by `-d, -database` option. 59 | 60 | Available command line options of `apig new` command are: 61 | 62 | |Option|Description|Required|Default| 63 | |------|-----------|--------|-------| 64 | |`-d, -database`|Database engine||`sqlite`| 65 | |`-n, -namespace`|Namespace of API||(empty)| 66 | |`-u, -user`|Username||github username| 67 | |`--vcs`|VCS||`github.com`| 68 | 69 | ### 2. Write model code 70 | 71 | Second, write model definitions under models/. For example, user and email model is like below: 72 | 73 | ```go 74 | // models/user.go 75 | package models 76 | 77 | import "time" 78 | 79 | type User struct { 80 | ID uint `gorm:"primary_key;AUTO_INCREMENT" json:"id" form:"id"` 81 | Name string `json:"name" form:"name"` 82 | Emails []Email `json:"emails" form:"emails"` 83 | CreatedAt *time.Time `json:"created_at" form:"created_at"` 84 | UpdatedAt *time.Time `json:"updated_at" form:"updated_at"` 85 | } 86 | ``` 87 | 88 | ```go 89 | // models/email.go 90 | package models 91 | 92 | type Email struct { 93 | ID uint `gorm:"primary_key;AUTO_INCREMENT" json:"id" form:"id"` 94 | UserID uint `json:"user_id" form:"user_id"` 95 | Address string `json:"address" form:"address"` 96 | User *User `json:"user form:"user` 97 | } 98 | ``` 99 | 100 | This models are based on [gorm](https://github.com/jinzhu/gorm) structure. 101 | Please refer [gorm document](http://jinzhu.me/gorm/) to write detailed models. 102 | 103 | ### 3. Generate controllers, tests, documents etc. based on models. 104 | 105 | Third, run the command: 106 | 107 | ```bash 108 | apig gen 109 | ``` 110 | 111 | It creates all necessary codes to provide RESTful endpoints of models. 112 | 113 | ### 4. Build and run server 114 | 115 | Finally, just build as normal go code. 116 | 117 | ```bash 118 | $ go mod init 119 | $ go mod tidy 120 | $ go build -o bin/server 121 | ``` 122 | 123 | After that just execute the server binary. 124 | For the first time, you may want to use `AUTOMIGRATE=1` when running the server. 125 | 126 | ```bash 127 | $ AUTOMIGRATE=1 bin/server 128 | ``` 129 | 130 | When `AUTOMIGRATE=1`, the db tables are generated automatically. 131 | After that, you can run the server just executing the command: 132 | 133 | ```bash 134 | $ bin/server 135 | ``` 136 | 137 | The server runs at http://localhost:8080. 138 | 139 | By default, use the port 8080. 140 | If you change the port, set environment variables. 141 | 142 | ```bash 143 | $ PORT=3000 bin/server 144 | ``` 145 | 146 | The server runs at http://localhost:3000. 147 | 148 | ## Usage 149 | 150 | ### `new` command 151 | `new` command tells apig to generate API server skeleton. 152 | 153 | ```bash 154 | $ apig new NAME 155 | ``` 156 | 157 | ### `gen` command 158 | `gen` command tells apig to generate files (routes, controllers, documents...) from [gorm](https://github.com/jinzhu/gorm) model files you wrote. 159 | 160 | You MUST run this command at the directory which was generated by `new` command. 161 | 162 | ```bash 163 | $ apig gen 164 | ``` 165 | 166 | ### API Document 167 | 168 | API Documents are generated automatically in `docs/` directory in the form of [API Blueprint](https://apiblueprint.org/). 169 | 170 | ``` 171 | docs 172 | ├── email.apib 173 | ├── index.apib 174 | └── user.apib 175 | ``` 176 | 177 | [Aglio](https://github.com/danielgtaylor/aglio) is an API Blueprint renderer. 178 | Aglio can be installed by 179 | 180 | ```bash 181 | $ npm install -g aglio 182 | ``` 183 | 184 | You can generate HTML files and run live preview server. 185 | 186 | ```bash 187 | // html file 188 | $ aglio -i index.apib -o index.html 189 | 190 | // running server on localhost:3000 191 | $ aglio -i index.apib --server 192 | ``` 193 | 194 | `index.apib` includes other files in your blueprint. 195 | 196 | ## API server specification 197 | 198 | ### Endpoints 199 | 200 | Each resource has 5 RESTful API endpoints. 201 | Resource name is written in the plural form. 202 | 203 | |Endpoint|Description|Example (User resource)| 204 | |--------|-----------|-------| 205 | |`GET /`|List items|`GET /users` List users| 206 | |`POST /`|Create new item|`POST /users` Create new user| 207 | |`GET //{id}`|Retrieve the item|`GET /users/1` Get the user which ID is 1| 208 | |`PUT //{id}`|Update the item|`PUT /users/1` Update the user which ID is 1| 209 | |`DELETE //{id}`|Delete the item|`DELETE /users/1` Delete the user which ID is 1| 210 | 211 | ### Available URL parameters 212 | 213 | #### `GET /` and `GET //{id}` 214 | 215 | |Parameter|Description|Default|Example| 216 | |---------|-----------|-------|-------| 217 | |`fields=`|Fields to receive|All fields|`name,emails.address`| 218 | |`preloads=`|Nested resources to preload|(empty)|`emails,profile`| 219 | |`pretty=`|Prettify JSON response|`false`|`true`| 220 | 221 | #### `GET /` only 222 | 223 | |Parameter|Description|Default|Example| 224 | |---------|-----------|-------|-------| 225 | |`stream=`|Return JSON in streaming format|`false`|`true`| 226 | |`q[field_name]=`|A unique query parameter for each field for filtering|(empty)|`q[id]=1,2,5`, `q[admin]=true&q[registered]=true`| 227 | |`sort=`|Retrieves a list in order of priority. `+` or (none) : ascending. `-` : descending|(empty)|`id`, `-age`, `id,-created_at`| 228 | |`limit=`|Maximum number of items|`25`|`50`| 229 | |`page=`|Page to receive|`1`|`3`| 230 | |`last_id=`|Beginning ID of items|(empty)|`1`| 231 | |`order=`|Order of items|`desc`|`asc`| 232 | |`v=`|API version|(empty)|`1.2.0`| 233 | 234 | ### Data Type 235 | 236 | #### Request 237 | 238 | API server accepts the form of `JSON` or `Form`. 239 | 240 | `application/json` 241 | 242 | ```bash 243 | $ curl -X POST http://localhost:8080/resources \ 244 | -H "Content-type: application/json" \ 245 | -d '{"field":"value"}' 246 | ``` 247 | 248 | `application/x-www-form-urlencoded` 249 | 250 | ```bash 251 | $ curl -X POST http://localhost:8080/users \ 252 | -d 'field=value' 253 | ``` 254 | 255 | `multipart/form-data` 256 | 257 | ```bash 258 | $ curl -X POST http://localhost:8080/users \ 259 | -F 'field=value' 260 | ``` 261 | 262 | #### Response 263 | 264 | Response data type is always `application/json`. 265 | 266 | ### Pagination 267 | 268 | API server supports 2 pagination types. 269 | 270 | #### Offset-based pagination 271 | 272 | Retrieve items by specifying page number and the number of items per page. 273 | 274 | For example: 275 | 276 | ``` 277 | http://example.com/api/users?limit=5&page=2 278 | ``` 279 | 280 | ``` 281 | +---------+---------+---------+---------+---------+---------+---------+ 282 | | ID: 5 | ID: 6 | ID: 7 | ID: 8 | ID: 9 | ID: 10 | ID: 11 | 283 | +---------+---------+---------+---------+---------+---------+---------+ 284 | | | 285 | Page 1 ->|<-------------------- Page 2 ------------------->|<- Page 3 286 | ``` 287 | 288 | Response header includes `Link` header. 289 | 290 | ``` 291 | Link: ; rel="next", 292 | ; rel="prev" 293 | ``` 294 | 295 | #### ID/Time-based pagination 296 | 297 | Retrieve items by specifying range from a certain point. 298 | 299 | For example: 300 | 301 | ``` 302 | http://example.com/api/users?limit=5&last_id=100&order=desc 303 | ``` 304 | 305 | ``` 306 | +---------+---------+---------+---------+---------+---------+---------+ 307 | | ID: 94 | ID: 95 | ID: 96 | ID: 97 | ID: 98 | ID: 99 | ID: 100 | 308 | +---------+---------+---------+---------+---------+---------+---------+ 309 | | 5 items (ID < 100) | 310 | |<------------------------------------------------| 311 | ``` 312 | 313 | Response header includes `Link` header. 314 | 315 | ``` 316 | Link: ; rel="next" 317 | ``` 318 | 319 | ### Versioning 320 | 321 | API server uses [Semantic Versioning](http://semver.org) for API versioning. 322 | 323 | There are 2 methods to specify API version. 324 | 325 | #### Request header 326 | 327 | Generally we recommend to include API version in `Accept` header. 328 | 329 | ``` 330 | Accept: application/json; version=1.0.0 331 | ``` 332 | 333 | #### URL parameter 334 | 335 | You can also include API version in URL parameter. 336 | This is userful for debug on browser or temporary use, 337 | 338 | ``` 339 | http://example.com/api/users?v=1.0.0 340 | ``` 341 | 342 | This method is prior to request header method. 343 | 344 | ## License 345 | [![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE) 346 | -------------------------------------------------------------------------------- /_example/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /_example/README.md: -------------------------------------------------------------------------------- 1 | # API Server 2 | 3 | Simple Rest API using gin(framework) & gorm(orm) 4 | 5 | ## Endpoint list 6 | 7 | ### Companies Resource 8 | 9 | ``` 10 | GET /api/companies 11 | GET /api/companies/:id 12 | POST /api/companies 13 | PUT /api/companies/:id 14 | DELETE /api/companies/:id 15 | ``` 16 | 17 | ### Emails Resource 18 | 19 | ``` 20 | GET /api/emails 21 | GET /api/emails/:id 22 | POST /api/emails 23 | PUT /api/emails/:id 24 | DELETE /api/emails/:id 25 | ``` 26 | 27 | ### Jobs Resource 28 | 29 | ``` 30 | GET /api/jobs 31 | GET /api/jobs/:id 32 | POST /api/jobs 33 | PUT /api/jobs/:id 34 | DELETE /api/jobs/:id 35 | ``` 36 | 37 | ### Profiles Resource 38 | 39 | ``` 40 | GET /api/profiles 41 | GET /api/profiles/:id 42 | POST /api/profiles 43 | PUT /api/profiles/:id 44 | DELETE /api/profiles/:id 45 | ``` 46 | 47 | ### Users Resource 48 | 49 | ``` 50 | GET /api/users 51 | GET /api/users/:id 52 | POST /api/users 53 | PUT /api/users/:id 54 | DELETE /api/users/:id 55 | ``` 56 | 57 | server runs at http://localhost:8080 58 | -------------------------------------------------------------------------------- /_example/controllers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shimastripe/apig/6535437d01d50156cd00d29f8303437a13eafad5/_example/controllers/.gitkeep -------------------------------------------------------------------------------- /_example/controllers/company.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | dbpkg "github.com/shimastripe/apig/_example/db" 8 | "github.com/shimastripe/apig/_example/helper" 9 | "github.com/shimastripe/apig/_example/models" 10 | "github.com/shimastripe/apig/_example/version" 11 | 12 | "github.com/gin-gonic/gin" 13 | ) 14 | 15 | func GetCompanies(c *gin.Context) { 16 | ver, err := version.New(c) 17 | if err != nil { 18 | c.JSON(400, gin.H{"error": err.Error()}) 19 | return 20 | } 21 | 22 | db := dbpkg.DBInstance(c) 23 | parameter, err := dbpkg.NewParameter(c, models.Company{}) 24 | if err != nil { 25 | c.JSON(400, gin.H{"error": err.Error()}) 26 | return 27 | } 28 | 29 | db, err = parameter.Paginate(db) 30 | if err != nil { 31 | c.JSON(400, gin.H{"error": err.Error()}) 32 | return 33 | } 34 | 35 | db = parameter.SetPreloads(db) 36 | db = parameter.SortRecords(db) 37 | db = parameter.FilterFields(db) 38 | companies := []models.Company{} 39 | fields := helper.ParseFields(c.DefaultQuery("fields", "*")) 40 | queryFields := helper.QueryFields(models.Company{}, fields) 41 | 42 | if err := db.Select(queryFields).Find(&companies).Error; err != nil { 43 | c.JSON(400, gin.H{"error": err.Error()}) 44 | return 45 | } 46 | 47 | index := 0 48 | 49 | if len(companies) > 0 { 50 | index = int(companies[len(companies)-1].ID) 51 | } 52 | 53 | if err := parameter.SetHeaderLink(c, index); err != nil { 54 | c.JSON(400, gin.H{"error": err.Error()}) 55 | return 56 | } 57 | 58 | if version.Range("1.0.0", "<=", ver) && version.Range(ver, "<", "2.0.0") { 59 | // conditional branch by version. 60 | // 1.0.0 <= this version < 2.0.0 !! 61 | } 62 | 63 | if _, ok := c.GetQuery("stream"); ok { 64 | enc := json.NewEncoder(c.Writer) 65 | c.Status(200) 66 | 67 | for _, company := range companies { 68 | fieldMap, err := helper.FieldToMap(company, fields) 69 | if err != nil { 70 | c.JSON(400, gin.H{"error": err.Error()}) 71 | return 72 | } 73 | 74 | if err := enc.Encode(fieldMap); err != nil { 75 | c.JSON(400, gin.H{"error": err.Error()}) 76 | return 77 | } 78 | } 79 | } else { 80 | fieldMaps := []map[string]interface{}{} 81 | 82 | for _, company := range companies { 83 | fieldMap, err := helper.FieldToMap(company, fields) 84 | if err != nil { 85 | c.JSON(400, gin.H{"error": err.Error()}) 86 | return 87 | } 88 | 89 | fieldMaps = append(fieldMaps, fieldMap) 90 | } 91 | 92 | if _, ok := c.GetQuery("pretty"); ok { 93 | c.IndentedJSON(200, fieldMaps) 94 | } else { 95 | c.JSON(200, fieldMaps) 96 | } 97 | } 98 | } 99 | 100 | func GetCompany(c *gin.Context) { 101 | ver, err := version.New(c) 102 | if err != nil { 103 | c.JSON(400, gin.H{"error": err.Error()}) 104 | return 105 | } 106 | 107 | db := dbpkg.DBInstance(c) 108 | parameter, err := dbpkg.NewParameter(c, models.Company{}) 109 | if err != nil { 110 | c.JSON(400, gin.H{"error": err.Error()}) 111 | return 112 | } 113 | 114 | db = parameter.SetPreloads(db) 115 | company := models.Company{} 116 | id := c.Params.ByName("id") 117 | fields := helper.ParseFields(c.DefaultQuery("fields", "*")) 118 | queryFields := helper.QueryFields(models.Company{}, fields) 119 | 120 | if err := db.Select(queryFields).First(&company, id).Error; err != nil { 121 | content := gin.H{"error": "company with id#" + id + " not found"} 122 | c.JSON(404, content) 123 | return 124 | } 125 | 126 | fieldMap, err := helper.FieldToMap(company, fields) 127 | if err != nil { 128 | c.JSON(400, gin.H{"error": err.Error()}) 129 | return 130 | } 131 | 132 | if version.Range("1.0.0", "<=", ver) && version.Range(ver, "<", "2.0.0") { 133 | // conditional branch by version. 134 | // 1.0.0 <= this version < 2.0.0 !! 135 | } 136 | 137 | if _, ok := c.GetQuery("pretty"); ok { 138 | c.IndentedJSON(200, fieldMap) 139 | } else { 140 | c.JSON(200, fieldMap) 141 | } 142 | } 143 | 144 | func CreateCompany(c *gin.Context) { 145 | ver, err := version.New(c) 146 | if err != nil { 147 | c.JSON(400, gin.H{"error": err.Error()}) 148 | return 149 | } 150 | 151 | db := dbpkg.DBInstance(c) 152 | company := models.Company{} 153 | 154 | if err := c.Bind(&company); err != nil { 155 | c.JSON(400, gin.H{"error": err.Error()}) 156 | return 157 | } 158 | 159 | if err := db.Create(&company).Error; err != nil { 160 | c.JSON(400, gin.H{"error": err.Error()}) 161 | return 162 | } 163 | 164 | if version.Range("1.0.0", "<=", ver) && version.Range(ver, "<", "2.0.0") { 165 | // conditional branch by version. 166 | // 1.0.0 <= this version < 2.0.0 !! 167 | } 168 | 169 | c.JSON(201, company) 170 | } 171 | 172 | func UpdateCompany(c *gin.Context) { 173 | ver, err := version.New(c) 174 | if err != nil { 175 | c.JSON(400, gin.H{"error": err.Error()}) 176 | return 177 | } 178 | 179 | db := dbpkg.DBInstance(c) 180 | id := c.Params.ByName("id") 181 | company := models.Company{} 182 | 183 | if db.First(&company, id).Error != nil { 184 | content := gin.H{"error": "company with id#" + id + " not found"} 185 | c.JSON(404, content) 186 | return 187 | } 188 | 189 | if err := c.Bind(&company); err != nil { 190 | c.JSON(400, gin.H{"error": err.Error()}) 191 | return 192 | } 193 | 194 | if err := db.Save(&company).Error; err != nil { 195 | c.JSON(400, gin.H{"error": err.Error()}) 196 | return 197 | } 198 | 199 | if version.Range("1.0.0", "<=", ver) && version.Range(ver, "<", "2.0.0") { 200 | // conditional branch by version. 201 | // 1.0.0 <= this version < 2.0.0 !! 202 | } 203 | 204 | c.JSON(200, company) 205 | } 206 | 207 | func DeleteCompany(c *gin.Context) { 208 | ver, err := version.New(c) 209 | if err != nil { 210 | c.JSON(400, gin.H{"error": err.Error()}) 211 | return 212 | } 213 | 214 | db := dbpkg.DBInstance(c) 215 | id := c.Params.ByName("id") 216 | company := models.Company{} 217 | 218 | if db.First(&company, id).Error != nil { 219 | content := gin.H{"error": "company with id#" + id + " not found"} 220 | c.JSON(404, content) 221 | return 222 | } 223 | 224 | if err := db.Delete(&company).Error; err != nil { 225 | c.JSON(400, gin.H{"error": err.Error()}) 226 | return 227 | } 228 | 229 | if version.Range("1.0.0", "<=", ver) && version.Range(ver, "<", "2.0.0") { 230 | // conditional branch by version. 231 | // 1.0.0 <= this version < 2.0.0 !! 232 | } 233 | 234 | c.Writer.WriteHeader(http.StatusNoContent) 235 | } 236 | -------------------------------------------------------------------------------- /_example/controllers/email.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | dbpkg "github.com/shimastripe/apig/_example/db" 8 | "github.com/shimastripe/apig/_example/helper" 9 | "github.com/shimastripe/apig/_example/models" 10 | "github.com/shimastripe/apig/_example/version" 11 | 12 | "github.com/gin-gonic/gin" 13 | ) 14 | 15 | func GetEmails(c *gin.Context) { 16 | ver, err := version.New(c) 17 | if err != nil { 18 | c.JSON(400, gin.H{"error": err.Error()}) 19 | return 20 | } 21 | 22 | db := dbpkg.DBInstance(c) 23 | parameter, err := dbpkg.NewParameter(c, models.Email{}) 24 | if err != nil { 25 | c.JSON(400, gin.H{"error": err.Error()}) 26 | return 27 | } 28 | 29 | db, err = parameter.Paginate(db) 30 | if err != nil { 31 | c.JSON(400, gin.H{"error": err.Error()}) 32 | return 33 | } 34 | 35 | db = parameter.SetPreloads(db) 36 | db = parameter.SortRecords(db) 37 | db = parameter.FilterFields(db) 38 | emails := []models.Email{} 39 | fields := helper.ParseFields(c.DefaultQuery("fields", "*")) 40 | queryFields := helper.QueryFields(models.Email{}, fields) 41 | 42 | if err := db.Select(queryFields).Find(&emails).Error; err != nil { 43 | c.JSON(400, gin.H{"error": err.Error()}) 44 | return 45 | } 46 | 47 | index := 0 48 | 49 | if len(emails) > 0 { 50 | index = int(emails[len(emails)-1].ID) 51 | } 52 | 53 | if err := parameter.SetHeaderLink(c, index); err != nil { 54 | c.JSON(400, gin.H{"error": err.Error()}) 55 | return 56 | } 57 | 58 | if version.Range("1.0.0", "<=", ver) && version.Range(ver, "<", "2.0.0") { 59 | // conditional branch by version. 60 | // 1.0.0 <= this version < 2.0.0 !! 61 | } 62 | 63 | if _, ok := c.GetQuery("stream"); ok { 64 | enc := json.NewEncoder(c.Writer) 65 | c.Status(200) 66 | 67 | for _, email := range emails { 68 | fieldMap, err := helper.FieldToMap(email, fields) 69 | if err != nil { 70 | c.JSON(400, gin.H{"error": err.Error()}) 71 | return 72 | } 73 | 74 | if err := enc.Encode(fieldMap); err != nil { 75 | c.JSON(400, gin.H{"error": err.Error()}) 76 | return 77 | } 78 | } 79 | } else { 80 | fieldMaps := []map[string]interface{}{} 81 | 82 | for _, email := range emails { 83 | fieldMap, err := helper.FieldToMap(email, fields) 84 | if err != nil { 85 | c.JSON(400, gin.H{"error": err.Error()}) 86 | return 87 | } 88 | 89 | fieldMaps = append(fieldMaps, fieldMap) 90 | } 91 | 92 | if _, ok := c.GetQuery("pretty"); ok { 93 | c.IndentedJSON(200, fieldMaps) 94 | } else { 95 | c.JSON(200, fieldMaps) 96 | } 97 | } 98 | } 99 | 100 | func GetEmail(c *gin.Context) { 101 | ver, err := version.New(c) 102 | if err != nil { 103 | c.JSON(400, gin.H{"error": err.Error()}) 104 | return 105 | } 106 | 107 | db := dbpkg.DBInstance(c) 108 | parameter, err := dbpkg.NewParameter(c, models.Email{}) 109 | if err != nil { 110 | c.JSON(400, gin.H{"error": err.Error()}) 111 | return 112 | } 113 | 114 | db = parameter.SetPreloads(db) 115 | email := models.Email{} 116 | id := c.Params.ByName("id") 117 | fields := helper.ParseFields(c.DefaultQuery("fields", "*")) 118 | queryFields := helper.QueryFields(models.Email{}, fields) 119 | 120 | if err := db.Select(queryFields).First(&email, id).Error; err != nil { 121 | content := gin.H{"error": "email with id#" + id + " not found"} 122 | c.JSON(404, content) 123 | return 124 | } 125 | 126 | fieldMap, err := helper.FieldToMap(email, fields) 127 | if err != nil { 128 | c.JSON(400, gin.H{"error": err.Error()}) 129 | return 130 | } 131 | 132 | if version.Range("1.0.0", "<=", ver) && version.Range(ver, "<", "2.0.0") { 133 | // conditional branch by version. 134 | // 1.0.0 <= this version < 2.0.0 !! 135 | } 136 | 137 | if _, ok := c.GetQuery("pretty"); ok { 138 | c.IndentedJSON(200, fieldMap) 139 | } else { 140 | c.JSON(200, fieldMap) 141 | } 142 | } 143 | 144 | func CreateEmail(c *gin.Context) { 145 | ver, err := version.New(c) 146 | if err != nil { 147 | c.JSON(400, gin.H{"error": err.Error()}) 148 | return 149 | } 150 | 151 | db := dbpkg.DBInstance(c) 152 | email := models.Email{} 153 | 154 | if err := c.Bind(&email); err != nil { 155 | c.JSON(400, gin.H{"error": err.Error()}) 156 | return 157 | } 158 | 159 | if err := db.Create(&email).Error; err != nil { 160 | c.JSON(400, gin.H{"error": err.Error()}) 161 | return 162 | } 163 | 164 | if version.Range("1.0.0", "<=", ver) && version.Range(ver, "<", "2.0.0") { 165 | // conditional branch by version. 166 | // 1.0.0 <= this version < 2.0.0 !! 167 | } 168 | 169 | c.JSON(201, email) 170 | } 171 | 172 | func UpdateEmail(c *gin.Context) { 173 | ver, err := version.New(c) 174 | if err != nil { 175 | c.JSON(400, gin.H{"error": err.Error()}) 176 | return 177 | } 178 | 179 | db := dbpkg.DBInstance(c) 180 | id := c.Params.ByName("id") 181 | email := models.Email{} 182 | 183 | if db.First(&email, id).Error != nil { 184 | content := gin.H{"error": "email with id#" + id + " not found"} 185 | c.JSON(404, content) 186 | return 187 | } 188 | 189 | if err := c.Bind(&email); err != nil { 190 | c.JSON(400, gin.H{"error": err.Error()}) 191 | return 192 | } 193 | 194 | if err := db.Save(&email).Error; err != nil { 195 | c.JSON(400, gin.H{"error": err.Error()}) 196 | return 197 | } 198 | 199 | if version.Range("1.0.0", "<=", ver) && version.Range(ver, "<", "2.0.0") { 200 | // conditional branch by version. 201 | // 1.0.0 <= this version < 2.0.0 !! 202 | } 203 | 204 | c.JSON(200, email) 205 | } 206 | 207 | func DeleteEmail(c *gin.Context) { 208 | ver, err := version.New(c) 209 | if err != nil { 210 | c.JSON(400, gin.H{"error": err.Error()}) 211 | return 212 | } 213 | 214 | db := dbpkg.DBInstance(c) 215 | id := c.Params.ByName("id") 216 | email := models.Email{} 217 | 218 | if db.First(&email, id).Error != nil { 219 | content := gin.H{"error": "email with id#" + id + " not found"} 220 | c.JSON(404, content) 221 | return 222 | } 223 | 224 | if err := db.Delete(&email).Error; err != nil { 225 | c.JSON(400, gin.H{"error": err.Error()}) 226 | return 227 | } 228 | 229 | if version.Range("1.0.0", "<=", ver) && version.Range(ver, "<", "2.0.0") { 230 | // conditional branch by version. 231 | // 1.0.0 <= this version < 2.0.0 !! 232 | } 233 | 234 | c.Writer.WriteHeader(http.StatusNoContent) 235 | } 236 | -------------------------------------------------------------------------------- /_example/controllers/job.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | dbpkg "github.com/shimastripe/apig/_example/db" 8 | "github.com/shimastripe/apig/_example/helper" 9 | "github.com/shimastripe/apig/_example/models" 10 | "github.com/shimastripe/apig/_example/version" 11 | 12 | "github.com/gin-gonic/gin" 13 | ) 14 | 15 | func GetJobs(c *gin.Context) { 16 | ver, err := version.New(c) 17 | if err != nil { 18 | c.JSON(400, gin.H{"error": err.Error()}) 19 | return 20 | } 21 | 22 | db := dbpkg.DBInstance(c) 23 | parameter, err := dbpkg.NewParameter(c, models.Job{}) 24 | if err != nil { 25 | c.JSON(400, gin.H{"error": err.Error()}) 26 | return 27 | } 28 | 29 | db, err = parameter.Paginate(db) 30 | if err != nil { 31 | c.JSON(400, gin.H{"error": err.Error()}) 32 | return 33 | } 34 | 35 | db = parameter.SetPreloads(db) 36 | db = parameter.SortRecords(db) 37 | db = parameter.FilterFields(db) 38 | jobs := []models.Job{} 39 | fields := helper.ParseFields(c.DefaultQuery("fields", "*")) 40 | queryFields := helper.QueryFields(models.Job{}, fields) 41 | 42 | if err := db.Select(queryFields).Find(&jobs).Error; err != nil { 43 | c.JSON(400, gin.H{"error": err.Error()}) 44 | return 45 | } 46 | 47 | index := 0 48 | 49 | if len(jobs) > 0 { 50 | index = int(jobs[len(jobs)-1].ID) 51 | } 52 | 53 | if err := parameter.SetHeaderLink(c, index); err != nil { 54 | c.JSON(400, gin.H{"error": err.Error()}) 55 | return 56 | } 57 | 58 | if version.Range("1.0.0", "<=", ver) && version.Range(ver, "<", "2.0.0") { 59 | // conditional branch by version. 60 | // 1.0.0 <= this version < 2.0.0 !! 61 | } 62 | 63 | if _, ok := c.GetQuery("stream"); ok { 64 | enc := json.NewEncoder(c.Writer) 65 | c.Status(200) 66 | 67 | for _, job := range jobs { 68 | fieldMap, err := helper.FieldToMap(job, fields) 69 | if err != nil { 70 | c.JSON(400, gin.H{"error": err.Error()}) 71 | return 72 | } 73 | 74 | if err := enc.Encode(fieldMap); err != nil { 75 | c.JSON(400, gin.H{"error": err.Error()}) 76 | return 77 | } 78 | } 79 | } else { 80 | fieldMaps := []map[string]interface{}{} 81 | 82 | for _, job := range jobs { 83 | fieldMap, err := helper.FieldToMap(job, fields) 84 | if err != nil { 85 | c.JSON(400, gin.H{"error": err.Error()}) 86 | return 87 | } 88 | 89 | fieldMaps = append(fieldMaps, fieldMap) 90 | } 91 | 92 | if _, ok := c.GetQuery("pretty"); ok { 93 | c.IndentedJSON(200, fieldMaps) 94 | } else { 95 | c.JSON(200, fieldMaps) 96 | } 97 | } 98 | } 99 | 100 | func GetJob(c *gin.Context) { 101 | ver, err := version.New(c) 102 | if err != nil { 103 | c.JSON(400, gin.H{"error": err.Error()}) 104 | return 105 | } 106 | 107 | db := dbpkg.DBInstance(c) 108 | parameter, err := dbpkg.NewParameter(c, models.Job{}) 109 | if err != nil { 110 | c.JSON(400, gin.H{"error": err.Error()}) 111 | return 112 | } 113 | 114 | db = parameter.SetPreloads(db) 115 | job := models.Job{} 116 | id := c.Params.ByName("id") 117 | fields := helper.ParseFields(c.DefaultQuery("fields", "*")) 118 | queryFields := helper.QueryFields(models.Job{}, fields) 119 | 120 | if err := db.Select(queryFields).First(&job, id).Error; err != nil { 121 | content := gin.H{"error": "job with id#" + id + " not found"} 122 | c.JSON(404, content) 123 | return 124 | } 125 | 126 | fieldMap, err := helper.FieldToMap(job, fields) 127 | if err != nil { 128 | c.JSON(400, gin.H{"error": err.Error()}) 129 | return 130 | } 131 | 132 | if version.Range("1.0.0", "<=", ver) && version.Range(ver, "<", "2.0.0") { 133 | // conditional branch by version. 134 | // 1.0.0 <= this version < 2.0.0 !! 135 | } 136 | 137 | if _, ok := c.GetQuery("pretty"); ok { 138 | c.IndentedJSON(200, fieldMap) 139 | } else { 140 | c.JSON(200, fieldMap) 141 | } 142 | } 143 | 144 | func CreateJob(c *gin.Context) { 145 | ver, err := version.New(c) 146 | if err != nil { 147 | c.JSON(400, gin.H{"error": err.Error()}) 148 | return 149 | } 150 | 151 | db := dbpkg.DBInstance(c) 152 | job := models.Job{} 153 | 154 | if err := c.Bind(&job); err != nil { 155 | c.JSON(400, gin.H{"error": err.Error()}) 156 | return 157 | } 158 | 159 | if err := db.Create(&job).Error; err != nil { 160 | c.JSON(400, gin.H{"error": err.Error()}) 161 | return 162 | } 163 | 164 | if version.Range("1.0.0", "<=", ver) && version.Range(ver, "<", "2.0.0") { 165 | // conditional branch by version. 166 | // 1.0.0 <= this version < 2.0.0 !! 167 | } 168 | 169 | c.JSON(201, job) 170 | } 171 | 172 | func UpdateJob(c *gin.Context) { 173 | ver, err := version.New(c) 174 | if err != nil { 175 | c.JSON(400, gin.H{"error": err.Error()}) 176 | return 177 | } 178 | 179 | db := dbpkg.DBInstance(c) 180 | id := c.Params.ByName("id") 181 | job := models.Job{} 182 | 183 | if db.First(&job, id).Error != nil { 184 | content := gin.H{"error": "job with id#" + id + " not found"} 185 | c.JSON(404, content) 186 | return 187 | } 188 | 189 | if err := c.Bind(&job); err != nil { 190 | c.JSON(400, gin.H{"error": err.Error()}) 191 | return 192 | } 193 | 194 | if err := db.Save(&job).Error; err != nil { 195 | c.JSON(400, gin.H{"error": err.Error()}) 196 | return 197 | } 198 | 199 | if version.Range("1.0.0", "<=", ver) && version.Range(ver, "<", "2.0.0") { 200 | // conditional branch by version. 201 | // 1.0.0 <= this version < 2.0.0 !! 202 | } 203 | 204 | c.JSON(200, job) 205 | } 206 | 207 | func DeleteJob(c *gin.Context) { 208 | ver, err := version.New(c) 209 | if err != nil { 210 | c.JSON(400, gin.H{"error": err.Error()}) 211 | return 212 | } 213 | 214 | db := dbpkg.DBInstance(c) 215 | id := c.Params.ByName("id") 216 | job := models.Job{} 217 | 218 | if db.First(&job, id).Error != nil { 219 | content := gin.H{"error": "job with id#" + id + " not found"} 220 | c.JSON(404, content) 221 | return 222 | } 223 | 224 | if err := db.Delete(&job).Error; err != nil { 225 | c.JSON(400, gin.H{"error": err.Error()}) 226 | return 227 | } 228 | 229 | if version.Range("1.0.0", "<=", ver) && version.Range(ver, "<", "2.0.0") { 230 | // conditional branch by version. 231 | // 1.0.0 <= this version < 2.0.0 !! 232 | } 233 | 234 | c.Writer.WriteHeader(http.StatusNoContent) 235 | } 236 | -------------------------------------------------------------------------------- /_example/controllers/profile.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | dbpkg "github.com/shimastripe/apig/_example/db" 8 | "github.com/shimastripe/apig/_example/helper" 9 | "github.com/shimastripe/apig/_example/models" 10 | "github.com/shimastripe/apig/_example/version" 11 | 12 | "github.com/gin-gonic/gin" 13 | ) 14 | 15 | func GetProfiles(c *gin.Context) { 16 | ver, err := version.New(c) 17 | if err != nil { 18 | c.JSON(400, gin.H{"error": err.Error()}) 19 | return 20 | } 21 | 22 | db := dbpkg.DBInstance(c) 23 | parameter, err := dbpkg.NewParameter(c, models.Profile{}) 24 | if err != nil { 25 | c.JSON(400, gin.H{"error": err.Error()}) 26 | return 27 | } 28 | 29 | db, err = parameter.Paginate(db) 30 | if err != nil { 31 | c.JSON(400, gin.H{"error": err.Error()}) 32 | return 33 | } 34 | 35 | db = parameter.SetPreloads(db) 36 | db = parameter.SortRecords(db) 37 | db = parameter.FilterFields(db) 38 | profiles := []models.Profile{} 39 | fields := helper.ParseFields(c.DefaultQuery("fields", "*")) 40 | queryFields := helper.QueryFields(models.Profile{}, fields) 41 | 42 | if err := db.Select(queryFields).Find(&profiles).Error; err != nil { 43 | c.JSON(400, gin.H{"error": err.Error()}) 44 | return 45 | } 46 | 47 | index := 0 48 | 49 | if len(profiles) > 0 { 50 | index = int(profiles[len(profiles)-1].ID) 51 | } 52 | 53 | if err := parameter.SetHeaderLink(c, index); err != nil { 54 | c.JSON(400, gin.H{"error": err.Error()}) 55 | return 56 | } 57 | 58 | if version.Range("1.0.0", "<=", ver) && version.Range(ver, "<", "2.0.0") { 59 | // conditional branch by version. 60 | // 1.0.0 <= this version < 2.0.0 !! 61 | } 62 | 63 | if _, ok := c.GetQuery("stream"); ok { 64 | enc := json.NewEncoder(c.Writer) 65 | c.Status(200) 66 | 67 | for _, profile := range profiles { 68 | fieldMap, err := helper.FieldToMap(profile, fields) 69 | if err != nil { 70 | c.JSON(400, gin.H{"error": err.Error()}) 71 | return 72 | } 73 | 74 | if err := enc.Encode(fieldMap); err != nil { 75 | c.JSON(400, gin.H{"error": err.Error()}) 76 | return 77 | } 78 | } 79 | } else { 80 | fieldMaps := []map[string]interface{}{} 81 | 82 | for _, profile := range profiles { 83 | fieldMap, err := helper.FieldToMap(profile, fields) 84 | if err != nil { 85 | c.JSON(400, gin.H{"error": err.Error()}) 86 | return 87 | } 88 | 89 | fieldMaps = append(fieldMaps, fieldMap) 90 | } 91 | 92 | if _, ok := c.GetQuery("pretty"); ok { 93 | c.IndentedJSON(200, fieldMaps) 94 | } else { 95 | c.JSON(200, fieldMaps) 96 | } 97 | } 98 | } 99 | 100 | func GetProfile(c *gin.Context) { 101 | ver, err := version.New(c) 102 | if err != nil { 103 | c.JSON(400, gin.H{"error": err.Error()}) 104 | return 105 | } 106 | 107 | db := dbpkg.DBInstance(c) 108 | parameter, err := dbpkg.NewParameter(c, models.Profile{}) 109 | if err != nil { 110 | c.JSON(400, gin.H{"error": err.Error()}) 111 | return 112 | } 113 | 114 | db = parameter.SetPreloads(db) 115 | profile := models.Profile{} 116 | id := c.Params.ByName("id") 117 | fields := helper.ParseFields(c.DefaultQuery("fields", "*")) 118 | queryFields := helper.QueryFields(models.Profile{}, fields) 119 | 120 | if err := db.Select(queryFields).First(&profile, id).Error; err != nil { 121 | content := gin.H{"error": "profile with id#" + id + " not found"} 122 | c.JSON(404, content) 123 | return 124 | } 125 | 126 | fieldMap, err := helper.FieldToMap(profile, fields) 127 | if err != nil { 128 | c.JSON(400, gin.H{"error": err.Error()}) 129 | return 130 | } 131 | 132 | if version.Range("1.0.0", "<=", ver) && version.Range(ver, "<", "2.0.0") { 133 | // conditional branch by version. 134 | // 1.0.0 <= this version < 2.0.0 !! 135 | } 136 | 137 | if _, ok := c.GetQuery("pretty"); ok { 138 | c.IndentedJSON(200, fieldMap) 139 | } else { 140 | c.JSON(200, fieldMap) 141 | } 142 | } 143 | 144 | func CreateProfile(c *gin.Context) { 145 | ver, err := version.New(c) 146 | if err != nil { 147 | c.JSON(400, gin.H{"error": err.Error()}) 148 | return 149 | } 150 | 151 | db := dbpkg.DBInstance(c) 152 | profile := models.Profile{} 153 | 154 | if err := c.Bind(&profile); err != nil { 155 | c.JSON(400, gin.H{"error": err.Error()}) 156 | return 157 | } 158 | 159 | if err := db.Create(&profile).Error; err != nil { 160 | c.JSON(400, gin.H{"error": err.Error()}) 161 | return 162 | } 163 | 164 | if version.Range("1.0.0", "<=", ver) && version.Range(ver, "<", "2.0.0") { 165 | // conditional branch by version. 166 | // 1.0.0 <= this version < 2.0.0 !! 167 | } 168 | 169 | c.JSON(201, profile) 170 | } 171 | 172 | func UpdateProfile(c *gin.Context) { 173 | ver, err := version.New(c) 174 | if err != nil { 175 | c.JSON(400, gin.H{"error": err.Error()}) 176 | return 177 | } 178 | 179 | db := dbpkg.DBInstance(c) 180 | id := c.Params.ByName("id") 181 | profile := models.Profile{} 182 | 183 | if db.First(&profile, id).Error != nil { 184 | content := gin.H{"error": "profile with id#" + id + " not found"} 185 | c.JSON(404, content) 186 | return 187 | } 188 | 189 | if err := c.Bind(&profile); err != nil { 190 | c.JSON(400, gin.H{"error": err.Error()}) 191 | return 192 | } 193 | 194 | if err := db.Save(&profile).Error; err != nil { 195 | c.JSON(400, gin.H{"error": err.Error()}) 196 | return 197 | } 198 | 199 | if version.Range("1.0.0", "<=", ver) && version.Range(ver, "<", "2.0.0") { 200 | // conditional branch by version. 201 | // 1.0.0 <= this version < 2.0.0 !! 202 | } 203 | 204 | c.JSON(200, profile) 205 | } 206 | 207 | func DeleteProfile(c *gin.Context) { 208 | ver, err := version.New(c) 209 | if err != nil { 210 | c.JSON(400, gin.H{"error": err.Error()}) 211 | return 212 | } 213 | 214 | db := dbpkg.DBInstance(c) 215 | id := c.Params.ByName("id") 216 | profile := models.Profile{} 217 | 218 | if db.First(&profile, id).Error != nil { 219 | content := gin.H{"error": "profile with id#" + id + " not found"} 220 | c.JSON(404, content) 221 | return 222 | } 223 | 224 | if err := db.Delete(&profile).Error; err != nil { 225 | c.JSON(400, gin.H{"error": err.Error()}) 226 | return 227 | } 228 | 229 | if version.Range("1.0.0", "<=", ver) && version.Range(ver, "<", "2.0.0") { 230 | // conditional branch by version. 231 | // 1.0.0 <= this version < 2.0.0 !! 232 | } 233 | 234 | c.Writer.WriteHeader(http.StatusNoContent) 235 | } 236 | -------------------------------------------------------------------------------- /_example/controllers/root.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | func APIEndpoints(c *gin.Context) { 11 | reqScheme := "http" 12 | 13 | if c.Request.TLS != nil { 14 | reqScheme = "https" 15 | } 16 | 17 | reqHost := c.Request.Host 18 | baseURL := fmt.Sprintf("%s://%s", reqScheme, reqHost) 19 | 20 | resources := map[string]string{ 21 | "companies_url": baseURL + "/api/companies", 22 | "company_url": baseURL + "/api/companies/{id}", 23 | "emails_url": baseURL + "/api/emails", 24 | "email_url": baseURL + "/api/emails/{id}", 25 | "jobs_url": baseURL + "/api/jobs", 26 | "job_url": baseURL + "/api/jobs/{id}", 27 | "profiles_url": baseURL + "/api/profiles", 28 | "profile_url": baseURL + "/api/profiles/{id}", 29 | "users_url": baseURL + "/api/users", 30 | "user_url": baseURL + "/api/users/{id}", 31 | } 32 | 33 | c.IndentedJSON(http.StatusOK, resources) 34 | } 35 | -------------------------------------------------------------------------------- /_example/controllers/user.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | dbpkg "github.com/shimastripe/apig/_example/db" 8 | "github.com/shimastripe/apig/_example/helper" 9 | "github.com/shimastripe/apig/_example/models" 10 | "github.com/shimastripe/apig/_example/version" 11 | 12 | "github.com/gin-gonic/gin" 13 | ) 14 | 15 | func GetUsers(c *gin.Context) { 16 | ver, err := version.New(c) 17 | if err != nil { 18 | c.JSON(400, gin.H{"error": err.Error()}) 19 | return 20 | } 21 | 22 | db := dbpkg.DBInstance(c) 23 | parameter, err := dbpkg.NewParameter(c, models.User{}) 24 | if err != nil { 25 | c.JSON(400, gin.H{"error": err.Error()}) 26 | return 27 | } 28 | 29 | db, err = parameter.Paginate(db) 30 | if err != nil { 31 | c.JSON(400, gin.H{"error": err.Error()}) 32 | return 33 | } 34 | 35 | db = parameter.SetPreloads(db) 36 | db = parameter.SortRecords(db) 37 | db = parameter.FilterFields(db) 38 | users := []models.User{} 39 | fields := helper.ParseFields(c.DefaultQuery("fields", "*")) 40 | queryFields := helper.QueryFields(models.User{}, fields) 41 | 42 | if err := db.Select(queryFields).Find(&users).Error; err != nil { 43 | c.JSON(400, gin.H{"error": err.Error()}) 44 | return 45 | } 46 | 47 | index := 0 48 | 49 | if len(users) > 0 { 50 | index = int(users[len(users)-1].ID) 51 | } 52 | 53 | if err := parameter.SetHeaderLink(c, index); err != nil { 54 | c.JSON(400, gin.H{"error": err.Error()}) 55 | return 56 | } 57 | 58 | if version.Range("1.0.0", "<=", ver) && version.Range(ver, "<", "2.0.0") { 59 | // conditional branch by version. 60 | // 1.0.0 <= this version < 2.0.0 !! 61 | } 62 | 63 | if _, ok := c.GetQuery("stream"); ok { 64 | enc := json.NewEncoder(c.Writer) 65 | c.Status(200) 66 | 67 | for _, user := range users { 68 | fieldMap, err := helper.FieldToMap(user, fields) 69 | if err != nil { 70 | c.JSON(400, gin.H{"error": err.Error()}) 71 | return 72 | } 73 | 74 | if err := enc.Encode(fieldMap); err != nil { 75 | c.JSON(400, gin.H{"error": err.Error()}) 76 | return 77 | } 78 | } 79 | } else { 80 | fieldMaps := []map[string]interface{}{} 81 | 82 | for _, user := range users { 83 | fieldMap, err := helper.FieldToMap(user, fields) 84 | if err != nil { 85 | c.JSON(400, gin.H{"error": err.Error()}) 86 | return 87 | } 88 | 89 | fieldMaps = append(fieldMaps, fieldMap) 90 | } 91 | 92 | if _, ok := c.GetQuery("pretty"); ok { 93 | c.IndentedJSON(200, fieldMaps) 94 | } else { 95 | c.JSON(200, fieldMaps) 96 | } 97 | } 98 | } 99 | 100 | func GetUser(c *gin.Context) { 101 | ver, err := version.New(c) 102 | if err != nil { 103 | c.JSON(400, gin.H{"error": err.Error()}) 104 | return 105 | } 106 | 107 | db := dbpkg.DBInstance(c) 108 | parameter, err := dbpkg.NewParameter(c, models.User{}) 109 | if err != nil { 110 | c.JSON(400, gin.H{"error": err.Error()}) 111 | return 112 | } 113 | 114 | db = parameter.SetPreloads(db) 115 | user := models.User{} 116 | id := c.Params.ByName("id") 117 | fields := helper.ParseFields(c.DefaultQuery("fields", "*")) 118 | queryFields := helper.QueryFields(models.User{}, fields) 119 | 120 | if err := db.Select(queryFields).First(&user, id).Error; err != nil { 121 | content := gin.H{"error": "user with id#" + id + " not found"} 122 | c.JSON(404, content) 123 | return 124 | } 125 | 126 | fieldMap, err := helper.FieldToMap(user, fields) 127 | if err != nil { 128 | c.JSON(400, gin.H{"error": err.Error()}) 129 | return 130 | } 131 | 132 | if version.Range("1.0.0", "<=", ver) && version.Range(ver, "<", "2.0.0") { 133 | // conditional branch by version. 134 | // 1.0.0 <= this version < 2.0.0 !! 135 | } 136 | 137 | if _, ok := c.GetQuery("pretty"); ok { 138 | c.IndentedJSON(200, fieldMap) 139 | } else { 140 | c.JSON(200, fieldMap) 141 | } 142 | } 143 | 144 | func CreateUser(c *gin.Context) { 145 | ver, err := version.New(c) 146 | if err != nil { 147 | c.JSON(400, gin.H{"error": err.Error()}) 148 | return 149 | } 150 | 151 | db := dbpkg.DBInstance(c) 152 | user := models.User{} 153 | 154 | if err := c.Bind(&user); err != nil { 155 | c.JSON(400, gin.H{"error": err.Error()}) 156 | return 157 | } 158 | 159 | if err := db.Create(&user).Error; err != nil { 160 | c.JSON(400, gin.H{"error": err.Error()}) 161 | return 162 | } 163 | 164 | if version.Range("1.0.0", "<=", ver) && version.Range(ver, "<", "2.0.0") { 165 | // conditional branch by version. 166 | // 1.0.0 <= this version < 2.0.0 !! 167 | } 168 | 169 | c.JSON(201, user) 170 | } 171 | 172 | func UpdateUser(c *gin.Context) { 173 | ver, err := version.New(c) 174 | if err != nil { 175 | c.JSON(400, gin.H{"error": err.Error()}) 176 | return 177 | } 178 | 179 | db := dbpkg.DBInstance(c) 180 | id := c.Params.ByName("id") 181 | user := models.User{} 182 | 183 | if db.First(&user, id).Error != nil { 184 | content := gin.H{"error": "user with id#" + id + " not found"} 185 | c.JSON(404, content) 186 | return 187 | } 188 | 189 | if err := c.Bind(&user); err != nil { 190 | c.JSON(400, gin.H{"error": err.Error()}) 191 | return 192 | } 193 | 194 | if err := db.Save(&user).Error; err != nil { 195 | c.JSON(400, gin.H{"error": err.Error()}) 196 | return 197 | } 198 | 199 | if version.Range("1.0.0", "<=", ver) && version.Range(ver, "<", "2.0.0") { 200 | // conditional branch by version. 201 | // 1.0.0 <= this version < 2.0.0 !! 202 | } 203 | 204 | c.JSON(200, user) 205 | } 206 | 207 | func DeleteUser(c *gin.Context) { 208 | ver, err := version.New(c) 209 | if err != nil { 210 | c.JSON(400, gin.H{"error": err.Error()}) 211 | return 212 | } 213 | 214 | db := dbpkg.DBInstance(c) 215 | id := c.Params.ByName("id") 216 | user := models.User{} 217 | 218 | if db.First(&user, id).Error != nil { 219 | content := gin.H{"error": "user with id#" + id + " not found"} 220 | c.JSON(404, content) 221 | return 222 | } 223 | 224 | if err := db.Delete(&user).Error; err != nil { 225 | c.JSON(400, gin.H{"error": err.Error()}) 226 | return 227 | } 228 | 229 | if version.Range("1.0.0", "<=", ver) && version.Range(ver, "<", "2.0.0") { 230 | // conditional branch by version. 231 | // 1.0.0 <= this version < 2.0.0 !! 232 | } 233 | 234 | c.Writer.WriteHeader(http.StatusNoContent) 235 | } 236 | -------------------------------------------------------------------------------- /_example/db/database.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shimastripe/apig/6535437d01d50156cd00d29f8303437a13eafad5/_example/db/database.db -------------------------------------------------------------------------------- /_example/db/db.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/shimastripe/apig/_example/models" 10 | 11 | "github.com/gin-gonic/gin" 12 | "github.com/jinzhu/gorm" 13 | _ "github.com/jinzhu/gorm/dialects/sqlite" 14 | "github.com/serenize/snaker" 15 | ) 16 | 17 | func Connect() *gorm.DB { 18 | dir := filepath.Dir("db/database.db") 19 | db, err := gorm.Open("sqlite3", dir+"/database.db") 20 | if err != nil { 21 | log.Fatalf("Got error when connect database, the error is '%v'", err) 22 | } 23 | 24 | db.LogMode(false) 25 | 26 | if gin.IsDebugging() { 27 | db.LogMode(true) 28 | } 29 | 30 | if os.Getenv("AUTOMIGRATE") == "1" { 31 | db.AutoMigrate( 32 | &models.Company{}, 33 | &models.Email{}, 34 | &models.Job{}, 35 | &models.Profile{}, 36 | &models.User{}, 37 | ) 38 | } 39 | 40 | return db 41 | } 42 | 43 | func DBInstance(c *gin.Context) *gorm.DB { 44 | return c.MustGet("DB").(*gorm.DB) 45 | } 46 | 47 | func (self *Parameter) SetPreloads(db *gorm.DB) *gorm.DB { 48 | if self.Preloads == "" { 49 | return db 50 | } 51 | 52 | for _, preload := range strings.Split(self.Preloads, ",") { 53 | var a []string 54 | 55 | for _, s := range strings.Split(preload, ".") { 56 | a = append(a, snaker.SnakeToCamel(s)) 57 | } 58 | 59 | db = db.Preload(strings.Join(a, ".")) 60 | } 61 | 62 | return db 63 | } 64 | -------------------------------------------------------------------------------- /_example/db/filter.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strings" 7 | 8 | "github.com/gin-gonic/gin" 9 | "github.com/jinzhu/gorm" 10 | ) 11 | 12 | func filterToMap(c *gin.Context, model interface{}) map[string]string { 13 | var jsonTag, jsonKey string 14 | filters := make(map[string]string) 15 | ts := reflect.TypeOf(model) 16 | 17 | for i := 0; i < ts.NumField(); i++ { 18 | f := ts.Field(i) 19 | jsonKey = f.Name 20 | 21 | if jsonTag = f.Tag.Get("json"); jsonTag != "" { 22 | jsonKey = strings.Split(jsonTag, ",")[0] 23 | } 24 | 25 | filters[jsonKey] = c.Query("q[" + jsonKey + "]") 26 | } 27 | 28 | return filters 29 | } 30 | 31 | func (self *Parameter) FilterFields(db *gorm.DB) *gorm.DB { 32 | for k, v := range self.Filters { 33 | if v != "" { 34 | db = db.Where(fmt.Sprintf("%s IN (?)", k), strings.Split(v, ",")) 35 | } 36 | } 37 | 38 | return db 39 | } 40 | 41 | func (self *Parameter) GetRawFilterQuery() string { 42 | var s string 43 | 44 | for k, v := range self.Filters { 45 | if v != "" { 46 | s += "&q[" + k + "]=" + v 47 | } 48 | } 49 | 50 | return s 51 | } 52 | -------------------------------------------------------------------------------- /_example/db/filter_test.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | type User struct { 11 | ID uint `json:"id,omitempty" form:"id"` 12 | Name string `json:"name,omitempty" form:"name"` 13 | Engaged bool `json:"engaged,omitempty" form:"engaged"` 14 | } 15 | 16 | func contains(ss map[string]string, s string) bool { 17 | _, ok := ss[s] 18 | 19 | return ok 20 | } 21 | 22 | func TestFilterToMap(t *testing.T) { 23 | req, _ := http.NewRequest("GET", "/?q[id]=1,5,100&q[name]=hoge,fuga&q[unexisted_field]=null", nil) 24 | c := &gin.Context{ 25 | Request: req, 26 | } 27 | value := filterToMap(c, User{}) 28 | 29 | if !contains(value, "id") { 30 | t.Fatalf("Filter should have `id` key.") 31 | } 32 | 33 | if !contains(value, "name") { 34 | t.Fatalf("Filter should have `name` key.") 35 | } 36 | 37 | if contains(value, "unexisted_field") { 38 | t.Fatalf("Filter should not have `unexisted_field` key.") 39 | } 40 | 41 | if value["id"] != "1,5,100" { 42 | t.Fatalf("filters[\"id\"] expected: `1,5,100`, actual: %s", value["id"]) 43 | } 44 | 45 | if value["name"] != "hoge,fuga" { 46 | t.Fatalf("filters[\"name\"] expected: `hoge,fuga`, actual: %s", value["id"]) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /_example/db/pagination.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/jinzhu/gorm" 9 | ) 10 | 11 | func (self *Parameter) Paginate(db *gorm.DB) (*gorm.DB, error) { 12 | if self == nil { 13 | return nil, errors.New("Parameter struct got nil.") 14 | } 15 | 16 | if self.IsLastID { 17 | if self.Order == "asc" { 18 | return db.Where("id > ?", self.LastID).Limit(self.Limit).Order("id asc"), nil 19 | } 20 | 21 | return db.Where("id < ?", self.LastID).Limit(self.Limit).Order("id desc"), nil 22 | } 23 | 24 | return db.Offset(self.Limit * (self.Page - 1)).Limit(self.Limit), nil 25 | } 26 | 27 | func (self *Parameter) SetHeaderLink(c *gin.Context, index int) error { 28 | if self == nil { 29 | return errors.New("Parameter struct got nil.") 30 | } 31 | 32 | var pretty, filters, preloads string 33 | reqScheme := "http" 34 | 35 | if c.Request.TLS != nil { 36 | reqScheme = "https" 37 | } 38 | 39 | if _, ok := c.GetQuery("pretty"); ok { 40 | pretty = "&pretty" 41 | } 42 | 43 | if len(self.Filters) != 0 { 44 | filters = self.GetRawFilterQuery() 45 | } 46 | 47 | if self.Preloads != "" { 48 | preloads = fmt.Sprintf("&preloads=%v", self.Preloads) 49 | } 50 | 51 | if self.IsLastID { 52 | c.Header("Link", fmt.Sprintf("<%s://%v%v?limit=%v%s%s&last_id=%v&order=%v%s>; rel=\"next\"", reqScheme, c.Request.Host, c.Request.URL.Path, self.Limit, filters, preloads, index, self.Order, pretty)) 53 | return nil 54 | } 55 | 56 | if self.Page == 1 { 57 | c.Header("Link", fmt.Sprintf("<%s://%v%v?limit=%v%s%s&page=%v%s>; rel=\"next\"", reqScheme, c.Request.Host, c.Request.URL.Path, self.Limit, filters, preloads, self.Page+1, pretty)) 58 | return nil 59 | } 60 | 61 | c.Header("Link", fmt.Sprintf( 62 | "<%s://%v%v?limit=%v%s%s&page=%v%s>; rel=\"next\",<%s://%v%v?limit=%v%s%s&page=%v%s>; rel=\"prev\"", reqScheme, 63 | c.Request.Host, c.Request.URL.Path, self.Limit, filters, preloads, self.Page+1, pretty, reqScheme, c.Request.Host, c.Request.URL.Path, self.Limit, filters, preloads, self.Page-1, pretty)) 64 | return nil 65 | } 66 | -------------------------------------------------------------------------------- /_example/db/parameter.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "math" 5 | "strconv" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | const ( 11 | defaultLimit = "25" 12 | defaultPage = "1" 13 | defaultOrder = "desc" 14 | ) 15 | 16 | type Parameter struct { 17 | Filters map[string]string 18 | Preloads string 19 | Sort string 20 | Limit int 21 | Page int 22 | LastID int 23 | Order string 24 | IsLastID bool 25 | } 26 | 27 | func NewParameter(c *gin.Context, model interface{}) (*Parameter, error) { 28 | parameter := &Parameter{} 29 | 30 | if err := parameter.initialize(c, model); err != nil { 31 | return nil, err 32 | } 33 | 34 | return parameter, nil 35 | } 36 | 37 | func (self *Parameter) initialize(c *gin.Context, model interface{}) error { 38 | self.Filters = filterToMap(c, model) 39 | self.Preloads = c.Query("preloads") 40 | self.Sort = c.Query("sort") 41 | 42 | limit, err := validate(c.DefaultQuery("limit", defaultLimit)) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | self.Limit = int(math.Max(1, math.Min(10000, float64(limit)))) 48 | page, err := validate(c.DefaultQuery("page", defaultPage)) 49 | if err != nil { 50 | return err 51 | } 52 | 53 | self.Page = int(math.Max(1, float64(page))) 54 | lastID, err := validate(c.Query("last_id")) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | if lastID != -1 { 60 | self.IsLastID = true 61 | self.LastID = int(math.Max(0, float64(lastID))) 62 | } 63 | 64 | self.Order = c.DefaultQuery("order", defaultOrder) 65 | return nil 66 | } 67 | 68 | func validate(s string) (int, error) { 69 | if s == "" { 70 | return -1, nil 71 | } 72 | 73 | num, err := strconv.Atoi(s) 74 | if err != nil { 75 | return 0, err 76 | } 77 | 78 | return num, nil 79 | } 80 | -------------------------------------------------------------------------------- /_example/db/sort.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/jinzhu/gorm" 7 | ) 8 | 9 | func convertPrefixToQuery(sort string) string { 10 | if strings.HasPrefix(sort, "-") { 11 | return strings.TrimLeft(sort, "-") + " desc" 12 | } else { 13 | return strings.TrimLeft(sort, " ") + " asc" 14 | } 15 | } 16 | 17 | func (self *Parameter) SortRecords(db *gorm.DB) *gorm.DB { 18 | if self.Sort == "" { 19 | return db 20 | } 21 | 22 | for _, sort := range strings.Split(self.Sort, ",") { 23 | db = db.Order(convertPrefixToQuery(sort)) 24 | } 25 | 26 | return db 27 | } 28 | -------------------------------------------------------------------------------- /_example/db/sort_test.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import "testing" 4 | 5 | func TestConvertPrefixToQueryPlus(t *testing.T) { 6 | value := convertPrefixToQuery("id") 7 | 8 | if value != "id asc" { 9 | t.Fatalf("Expected: `id asc`, actual: %s", value) 10 | } 11 | 12 | value = convertPrefixToQuery(" id") 13 | 14 | if value != "id asc" { 15 | t.Fatalf("Expected: `id asc`, actual: %s", value) 16 | } 17 | } 18 | 19 | func TestConvertPrefixToQueryMinus(t *testing.T) { 20 | value := convertPrefixToQuery("-id") 21 | 22 | if value != "id desc" { 23 | t.Fatalf("Expected: `id desc`, actual: %s", value) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /_example/docs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shimastripe/apig/6535437d01d50156cd00d29f8303437a13eafad5/_example/docs/.gitkeep -------------------------------------------------------------------------------- /_example/docs/company.apib: -------------------------------------------------------------------------------- 1 | # Group Companies 2 | Welcome to the companies API. This API provides access to the companies service. 3 | 4 | ## companies [/companies] 5 | 6 | ### Create company [POST] 7 | 8 | Create a new company 9 | 10 | + Request company (application/json; charset=utf-8) 11 | + Headers 12 | 13 | Accept: application/vnd.shimastripe+json 14 | + Attributes 15 | 16 | + name: NAME (string) 17 | + url: URL (string, nullable) 18 | + jobs (array[job]) 19 | 20 | + Response 201 (application/json; charset=utf-8) 21 | + Attributes (company, fixed) 22 | 23 | ### Get companies [GET] 24 | 25 | Returns a company list. 26 | 27 | + Request (application/json; charset=utf-8) 28 | + Headers 29 | 30 | Accept: application/vnd.shimastripe+json 31 | 32 | + Response 200 (application/json; charset=utf-8) 33 | + Attributes (array, fixed) 34 | + (company) 35 | 36 | ## company details [/companies/{id}] 37 | 38 | + Parameters 39 | + id: `1` (enum[string]) - The ID of the desired company. 40 | + Members 41 | + `1` 42 | + `2` 43 | + `3` 44 | 45 | ### Get company [GET] 46 | 47 | Returns a company. 48 | 49 | + Request (application/json; charset=utf-8) 50 | + Headers 51 | 52 | Accept: application/vnd.shimastripe+json 53 | 54 | + Response 200 (application/json; charset=utf-8) 55 | + Attributes (company, fixed) 56 | 57 | ### Update company [PUT] 58 | 59 | Update a company. 60 | 61 | + Request company (application/json; charset=utf-8) 62 | + Headers 63 | 64 | Accept: application/vnd.shimastripe+json 65 | + Attributes 66 | 67 | + name: NAME (string) 68 | + url: URL (string, nullable) 69 | + jobs (array[job]) 70 | 71 | + Response 200 (application/json; charset=utf-8) 72 | + Attributes (company, fixed) 73 | 74 | ### Delete company [DELETE] 75 | 76 | Delete a company. 77 | 78 | + Request (application/json; charset=utf-8) 79 | + Headers 80 | 81 | Accept: application/vnd.shimastripe+json 82 | 83 | + Response 204 84 | 85 | # Data Structures 86 | ## company (object) 87 | 88 | + id: *1* (number) 89 | + name: *NAME* (string) 90 | + url: *URL* (string, nullable) 91 | + jobs (array[job]) 92 | -------------------------------------------------------------------------------- /_example/docs/email.apib: -------------------------------------------------------------------------------- 1 | # Group Emails 2 | Welcome to the emails API. This API provides access to the emails service. 3 | 4 | ## emails [/emails] 5 | 6 | ### Create email [POST] 7 | 8 | Create a new email 9 | 10 | + Request email (application/json; charset=utf-8) 11 | + Headers 12 | 13 | Accept: application/vnd.shimastripe+json 14 | + Attributes 15 | 16 | + address: ADDRESS (string) 17 | + user_id: 1 (number) 18 | + user (user) 19 | 20 | + Response 201 (application/json; charset=utf-8) 21 | + Attributes (email, fixed) 22 | 23 | ### Get emails [GET] 24 | 25 | Returns an email list. 26 | 27 | + Request (application/json; charset=utf-8) 28 | + Headers 29 | 30 | Accept: application/vnd.shimastripe+json 31 | 32 | + Response 200 (application/json; charset=utf-8) 33 | + Attributes (array, fixed) 34 | + (email) 35 | 36 | ## email details [/emails/{id}] 37 | 38 | + Parameters 39 | + id: `1` (enum[string]) - The ID of the desired email. 40 | + Members 41 | + `1` 42 | + `2` 43 | + `3` 44 | 45 | ### Get email [GET] 46 | 47 | Returns an email. 48 | 49 | + Request (application/json; charset=utf-8) 50 | + Headers 51 | 52 | Accept: application/vnd.shimastripe+json 53 | 54 | + Response 200 (application/json; charset=utf-8) 55 | + Attributes (email, fixed) 56 | 57 | ### Update email [PUT] 58 | 59 | Update an email. 60 | 61 | + Request email (application/json; charset=utf-8) 62 | + Headers 63 | 64 | Accept: application/vnd.shimastripe+json 65 | + Attributes 66 | 67 | + address: ADDRESS (string) 68 | + user_id: 1 (number) 69 | + user (user) 70 | 71 | + Response 200 (application/json; charset=utf-8) 72 | + Attributes (email, fixed) 73 | 74 | ### Delete email [DELETE] 75 | 76 | Delete an email. 77 | 78 | + Request (application/json; charset=utf-8) 79 | + Headers 80 | 81 | Accept: application/vnd.shimastripe+json 82 | 83 | + Response 204 84 | 85 | # Data Structures 86 | ## email (object) 87 | 88 | + id: *1* (number) 89 | + address: *ADDRESS* (string) 90 | + user_id: *1* (number) 91 | + user (user) 92 | -------------------------------------------------------------------------------- /_example/docs/index.apib: -------------------------------------------------------------------------------- 1 | FORMAT: 1A 2 | HOST: http://localhost:8080 3 | 4 | # Apig/_example API 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /_example/docs/job.apib: -------------------------------------------------------------------------------- 1 | # Group Jobs 2 | Welcome to the jobs API. This API provides access to the jobs service. 3 | 4 | ## jobs [/jobs] 5 | 6 | ### Create job [POST] 7 | 8 | Create a new job 9 | 10 | + Request job (application/json; charset=utf-8) 11 | + Headers 12 | 13 | Accept: application/vnd.shimastripe+json 14 | + Attributes 15 | 16 | + user_id: 1 (number) 17 | + user (user) 18 | + company_id: 1 (number) 19 | + role_cd: 1 (number) 20 | 21 | + Response 201 (application/json; charset=utf-8) 22 | + Attributes (job, fixed) 23 | 24 | ### Get jobs [GET] 25 | 26 | Returns a job list. 27 | 28 | + Request (application/json; charset=utf-8) 29 | + Headers 30 | 31 | Accept: application/vnd.shimastripe+json 32 | 33 | + Response 200 (application/json; charset=utf-8) 34 | + Attributes (array, fixed) 35 | + (job) 36 | 37 | ## job details [/jobs/{id}] 38 | 39 | + Parameters 40 | + id: `1` (enum[string]) - The ID of the desired job. 41 | + Members 42 | + `1` 43 | + `2` 44 | + `3` 45 | 46 | ### Get job [GET] 47 | 48 | Returns a job. 49 | 50 | + Request (application/json; charset=utf-8) 51 | + Headers 52 | 53 | Accept: application/vnd.shimastripe+json 54 | 55 | + Response 200 (application/json; charset=utf-8) 56 | + Attributes (job, fixed) 57 | 58 | ### Update job [PUT] 59 | 60 | Update a job. 61 | 62 | + Request job (application/json; charset=utf-8) 63 | + Headers 64 | 65 | Accept: application/vnd.shimastripe+json 66 | + Attributes 67 | 68 | + user_id: 1 (number) 69 | + user (user) 70 | + company_id: 1 (number) 71 | + role_cd: 1 (number) 72 | 73 | + Response 200 (application/json; charset=utf-8) 74 | + Attributes (job, fixed) 75 | 76 | ### Delete job [DELETE] 77 | 78 | Delete a job. 79 | 80 | + Request (application/json; charset=utf-8) 81 | + Headers 82 | 83 | Accept: application/vnd.shimastripe+json 84 | 85 | + Response 204 86 | 87 | # Data Structures 88 | ## job (object) 89 | 90 | + id: *1* (number) 91 | + user_id: *1* (number) 92 | + user (user) 93 | + company_id: *1* (number) 94 | + role_cd: *1* (number) 95 | -------------------------------------------------------------------------------- /_example/docs/profile.apib: -------------------------------------------------------------------------------- 1 | # Group Profiles 2 | Welcome to the profiles API. This API provides access to the profiles service. 3 | 4 | ## profiles [/profiles] 5 | 6 | ### Create profile [POST] 7 | 8 | Create a new profile 9 | 10 | + Request profile (application/json; charset=utf-8) 11 | + Headers 12 | 13 | Accept: application/vnd.shimastripe+json 14 | + Attributes 15 | 16 | + user_id: 1 (number) 17 | + user (user) 18 | + birthday: `2000-01-01 00:00:00` (string) 19 | + engaged: false (boolean) 20 | 21 | + Response 201 (application/json; charset=utf-8) 22 | + Attributes (profile, fixed) 23 | 24 | ### Get profiles [GET] 25 | 26 | Returns a profile list. 27 | 28 | + Request (application/json; charset=utf-8) 29 | + Headers 30 | 31 | Accept: application/vnd.shimastripe+json 32 | 33 | + Response 200 (application/json; charset=utf-8) 34 | + Attributes (array, fixed) 35 | + (profile) 36 | 37 | ## profile details [/profiles/{id}] 38 | 39 | + Parameters 40 | + id: `1` (enum[string]) - The ID of the desired profile. 41 | + Members 42 | + `1` 43 | + `2` 44 | + `3` 45 | 46 | ### Get profile [GET] 47 | 48 | Returns a profile. 49 | 50 | + Request (application/json; charset=utf-8) 51 | + Headers 52 | 53 | Accept: application/vnd.shimastripe+json 54 | 55 | + Response 200 (application/json; charset=utf-8) 56 | + Attributes (profile, fixed) 57 | 58 | ### Update profile [PUT] 59 | 60 | Update a profile. 61 | 62 | + Request profile (application/json; charset=utf-8) 63 | + Headers 64 | 65 | Accept: application/vnd.shimastripe+json 66 | + Attributes 67 | 68 | + user_id: 1 (number) 69 | + user (user) 70 | + birthday: `2000-01-01 00:00:00` (string) 71 | + engaged: false (boolean) 72 | 73 | + Response 200 (application/json; charset=utf-8) 74 | + Attributes (profile, fixed) 75 | 76 | ### Delete profile [DELETE] 77 | 78 | Delete a profile. 79 | 80 | + Request (application/json; charset=utf-8) 81 | + Headers 82 | 83 | Accept: application/vnd.shimastripe+json 84 | 85 | + Response 204 86 | 87 | # Data Structures 88 | ## profile (object) 89 | 90 | + id: *1* (number) 91 | + user_id: *1* (number) 92 | + user (user) 93 | + birthday: `*2000-01-01 00:00:00*` (string) 94 | + engaged: *false* (boolean) 95 | -------------------------------------------------------------------------------- /_example/docs/user.apib: -------------------------------------------------------------------------------- 1 | # Group Users 2 | Welcome to the users API. This API provides access to the users service. 3 | 4 | ## users [/users] 5 | 6 | ### Create user [POST] 7 | 8 | Create a new user 9 | 10 | + Request user (application/json; charset=utf-8) 11 | + Headers 12 | 13 | Accept: application/vnd.shimastripe+json 14 | + Attributes 15 | 16 | + name: NAME (string) 17 | + profile (profile) 18 | + jobs (array[job]) 19 | + emails (array[email]) 20 | 21 | + Response 201 (application/json; charset=utf-8) 22 | + Attributes (user, fixed) 23 | 24 | ### Get users [GET] 25 | 26 | Returns an user list. 27 | 28 | + Request (application/json; charset=utf-8) 29 | + Headers 30 | 31 | Accept: application/vnd.shimastripe+json 32 | 33 | + Response 200 (application/json; charset=utf-8) 34 | + Attributes (array, fixed) 35 | + (user) 36 | 37 | ## user details [/users/{id}] 38 | 39 | + Parameters 40 | + id: `1` (enum[string]) - The ID of the desired user. 41 | + Members 42 | + `1` 43 | + `2` 44 | + `3` 45 | 46 | ### Get user [GET] 47 | 48 | Returns an user. 49 | 50 | + Request (application/json; charset=utf-8) 51 | + Headers 52 | 53 | Accept: application/vnd.shimastripe+json 54 | 55 | + Response 200 (application/json; charset=utf-8) 56 | + Attributes (user, fixed) 57 | 58 | ### Update user [PUT] 59 | 60 | Update an user. 61 | 62 | + Request user (application/json; charset=utf-8) 63 | + Headers 64 | 65 | Accept: application/vnd.shimastripe+json 66 | + Attributes 67 | 68 | + name: NAME (string) 69 | + profile (profile) 70 | + jobs (array[job]) 71 | + emails (array[email]) 72 | 73 | + Response 200 (application/json; charset=utf-8) 74 | + Attributes (user, fixed) 75 | 76 | ### Delete user [DELETE] 77 | 78 | Delete an user. 79 | 80 | + Request (application/json; charset=utf-8) 81 | + Headers 82 | 83 | Accept: application/vnd.shimastripe+json 84 | 85 | + Response 204 86 | 87 | # Data Structures 88 | ## user (object) 89 | 90 | + id: *1* (number) 91 | + name: *NAME* (string) 92 | + profile (profile) 93 | + jobs (array[job]) 94 | + emails (array[email]) 95 | -------------------------------------------------------------------------------- /_example/helper/field.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "strings" 7 | 8 | "github.com/serenize/snaker" 9 | ) 10 | 11 | type AssociationType int 12 | 13 | const ( 14 | none AssociationType = iota 15 | belongsTo 16 | hasMany 17 | hasOne 18 | ) 19 | 20 | func contains(ss map[string]interface{}, s string) bool { 21 | _, ok := ss[s] 22 | 23 | return ok 24 | } 25 | 26 | func merge(m1, m2 map[string]interface{}) map[string]interface{} { 27 | result := make(map[string]interface{}) 28 | 29 | for k, v := range m1 { 30 | result[k] = v 31 | } 32 | 33 | for k, v := range m2 { 34 | result[k] = v 35 | } 36 | 37 | return result 38 | } 39 | 40 | func QueryFields(model interface{}, fields map[string]interface{}) string { 41 | var jsonTag, jsonKey string 42 | 43 | ts, vs := reflect.TypeOf(model), reflect.ValueOf(model) 44 | 45 | assocs := make(map[string]AssociationType) 46 | 47 | for i := 0; i < ts.NumField(); i++ { 48 | f := ts.Field(i) 49 | jsonTag = f.Tag.Get("json") 50 | 51 | if jsonTag == "" { 52 | jsonKey = f.Name 53 | } else { 54 | jsonKey = strings.Split(jsonTag, ",")[0] 55 | } 56 | 57 | switch vs.Field(i).Kind() { 58 | case reflect.Ptr: 59 | if _, ok := ts.FieldByName(f.Name + "ID"); ok { 60 | assocs[jsonKey] = belongsTo 61 | } else { 62 | assocs[jsonKey] = hasOne 63 | } 64 | case reflect.Slice: 65 | assocs[jsonKey] = hasMany 66 | default: 67 | assocs[jsonKey] = none 68 | } 69 | } 70 | 71 | result := []string{} 72 | 73 | for k := range fields { 74 | if k == "*" { 75 | return "*" 76 | } 77 | 78 | if _, ok := assocs[k]; !ok { 79 | continue 80 | } 81 | 82 | switch assocs[k] { 83 | case none: 84 | result = append(result, k) 85 | case belongsTo: 86 | result = append(result, k+"_id") 87 | default: 88 | result = append(result, "id") 89 | } 90 | } 91 | 92 | return strings.Join(result, ",") 93 | } 94 | 95 | func ParseFields(fields string) map[string]interface{} { 96 | result := make(map[string]interface{}) 97 | 98 | if fields == "*" { 99 | result["*"] = nil 100 | return result 101 | } 102 | 103 | for _, field := range strings.Split(fields, ",") { 104 | parts := strings.SplitN(field, ".", 2) 105 | 106 | if len(parts) == 2 { 107 | if result[parts[0]] == nil { 108 | result[parts[0]] = ParseFields(parts[1]) 109 | } else { 110 | result[parts[0]] = merge(result[parts[0]].(map[string]interface{}), ParseFields(parts[1])) 111 | } 112 | } else { 113 | result[parts[0]] = nil 114 | } 115 | } 116 | 117 | return result 118 | } 119 | 120 | func isEmptyValue(v reflect.Value) bool { 121 | switch v.Kind() { 122 | case reflect.Array, reflect.Map, reflect.Slice, reflect.String: 123 | return v.Len() == 0 124 | case reflect.Bool: 125 | return !v.Bool() 126 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 127 | return v.Int() == 0 128 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: 129 | return v.Uint() == 0 130 | case reflect.Float32, reflect.Float64: 131 | return v.Float() == 0 132 | case reflect.Interface, reflect.Ptr: 133 | return v.IsNil() 134 | } 135 | return false 136 | } 137 | 138 | func FieldToMap(model interface{}, fields map[string]interface{}) (map[string]interface{}, error) { 139 | u := make(map[string]interface{}) 140 | ts, vs := reflect.TypeOf(model), reflect.ValueOf(model) 141 | 142 | if vs.Kind() != reflect.Struct { 143 | return nil, errors.New("Invalid Parameter. The specified parameter does not have a structure.") 144 | } 145 | 146 | if !contains(fields, "*") { 147 | for field, _ := range fields { 148 | if !vs.FieldByName(snaker.SnakeToCamel(field)).IsValid() { 149 | return nil, errors.New("Invalid Parameter. The specified field does not exist.") 150 | } 151 | } 152 | } 153 | 154 | var jsonKey string 155 | var omitEmpty bool 156 | 157 | for i := 0; i < ts.NumField(); i++ { 158 | field := ts.Field(i) 159 | jsonTag := field.Tag.Get("json") 160 | omitEmpty = false 161 | 162 | if jsonTag == "" { 163 | jsonKey = field.Name 164 | } else { 165 | ss := strings.Split(jsonTag, ",") 166 | jsonKey = ss[0] 167 | 168 | if len(ss) > 1 && ss[1] == "omitempty" { 169 | omitEmpty = true 170 | } 171 | } 172 | 173 | if contains(fields, "*") { 174 | if !omitEmpty || !isEmptyValue(vs.Field(i)) { 175 | u[jsonKey] = vs.Field(i).Interface() 176 | } 177 | 178 | continue 179 | } 180 | 181 | if contains(fields, jsonKey) { 182 | v := fields[jsonKey] 183 | 184 | if vs.Field(i).Kind() == reflect.Ptr { 185 | if !vs.Field(i).IsNil() { 186 | if v == nil { 187 | u[jsonKey] = vs.Field(i).Elem().Interface() 188 | } else { 189 | k, err := FieldToMap(vs.Field(i).Elem().Interface(), v.(map[string]interface{})) 190 | 191 | if err != nil { 192 | return nil, err 193 | } 194 | 195 | u[jsonKey] = k 196 | } 197 | } else { 198 | if v == nil { 199 | u[jsonKey] = nil 200 | } else { 201 | return nil, errors.New("Invalid Parameter. The structure is null.") 202 | } 203 | } 204 | } else if vs.Field(i).Kind() == reflect.Slice { 205 | var fieldMap []interface{} 206 | s := reflect.ValueOf(vs.Field(i).Interface()) 207 | 208 | for i := 0; i < s.Len(); i++ { 209 | if v == nil { 210 | fieldMap = append(fieldMap, s.Index(i).Interface()) 211 | } else { 212 | 213 | if s.Index(i).Kind() == reflect.Ptr { 214 | k, err := FieldToMap(s.Index(i).Elem().Interface(), v.(map[string]interface{})) 215 | 216 | if err != nil { 217 | return nil, err 218 | } 219 | 220 | fieldMap = append(fieldMap, k) 221 | } else { 222 | k, err := FieldToMap(s.Index(i).Interface(), v.(map[string]interface{})) 223 | 224 | if err != nil { 225 | return nil, err 226 | } 227 | 228 | fieldMap = append(fieldMap, k) 229 | } 230 | } 231 | } 232 | 233 | u[jsonKey] = fieldMap 234 | } else { 235 | if v == nil { 236 | u[jsonKey] = vs.Field(i).Interface() 237 | } else { 238 | k, err := FieldToMap(vs.Field(i).Interface(), v.(map[string]interface{})) 239 | 240 | if err != nil { 241 | return nil, err 242 | } 243 | 244 | u[jsonKey] = k 245 | } 246 | } 247 | } 248 | } 249 | 250 | return u, nil 251 | } 252 | -------------------------------------------------------------------------------- /_example/helper/field_test.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | type User struct { 8 | ID uint `json:"id" form:"id"` 9 | Jobs []*Job `json:"jobs,omitempty" form:"jobs"` 10 | Name string `json:"name" form:"name"` 11 | Profile *Profile `json:"profile,omitempty" form:"profile"` 12 | } 13 | 14 | type Profile struct { 15 | ID uint `json:"id" form:"id"` 16 | UserID uint `json:"user_id" form:"user_id"` 17 | User *User `json:"user" form:"user"` 18 | Engaged bool `json:"engaged" form:"engaged"` 19 | } 20 | 21 | type Job struct { 22 | ID uint `json:"id" form:"id"` 23 | UserID uint `json:"user_id" form:"user_id"` 24 | User *User `json:"user" form:"user"` 25 | RoleCd uint `json:"role_cd" form:"role_cd"` 26 | } 27 | 28 | type Company struct { 29 | ID uint `json:"id,omitempty" form:"id"` 30 | Name string `json:"name,omitempty" form:"name"` 31 | List bool `json:"list,omitempty" form:"list"` 32 | Subsidiary []*Company `json:"company,omitempty" form:"company"` 33 | Organization map[string]string `json:"organization,omitempty" form:"organization"` 34 | User *User `json:"user,omitempty" form:"user"` 35 | } 36 | 37 | func TestQueryFields_Wildcard(t *testing.T) { 38 | fields := map[string]interface{}{"*": nil} 39 | result := QueryFields(User{}, fields) 40 | expected := "*" 41 | 42 | if result != expected { 43 | t.Fatalf("result should be %s. actual: %s", expected, result) 44 | } 45 | } 46 | 47 | func TestQueryFields_Primitive(t *testing.T) { 48 | fields := map[string]interface{}{"name": nil} 49 | result := QueryFields(User{}, fields) 50 | expected := "name" 51 | 52 | if result != expected { 53 | t.Fatalf("result should be %s. actual: %s", expected, result) 54 | } 55 | } 56 | 57 | func TestQueryFields_Multiple(t *testing.T) { 58 | fields := map[string]interface{}{"id": nil, "name": nil} 59 | result := QueryFields(User{}, fields) 60 | expected := "id,name" 61 | 62 | if result != expected { 63 | t.Fatalf("result should be %s. actual: %s", expected, result) 64 | } 65 | } 66 | 67 | func TestQueryFields_BelongsTo(t *testing.T) { 68 | fields := map[string]interface{}{"user": nil} 69 | result := QueryFields(Profile{}, fields) 70 | expected := "user_id" 71 | 72 | if result != expected { 73 | t.Fatalf("result should be %s. actual: %s", expected, result) 74 | } 75 | } 76 | 77 | func TestQueryFields_HasOne(t *testing.T) { 78 | fields := map[string]interface{}{"profile": nil} 79 | result := QueryFields(User{}, fields) 80 | expected := "id" 81 | 82 | if result != expected { 83 | t.Fatalf("result should be %s. actual: %s", expected, result) 84 | } 85 | } 86 | 87 | func TestQueryFields_HasMany(t *testing.T) { 88 | fields := map[string]interface{}{"jobs": nil} 89 | result := QueryFields(User{}, fields) 90 | expected := "id" 91 | 92 | if result != expected { 93 | t.Fatalf("result should be %s. actual: %s", expected, result) 94 | } 95 | } 96 | 97 | func TestParseFields_Wildcard(t *testing.T) { 98 | fields := "*" 99 | result := ParseFields(fields) 100 | 101 | if _, ok := result["*"]; !ok { 102 | t.Fatalf("result[*] should exist: %#v", result) 103 | } 104 | 105 | if result["*"] != nil { 106 | t.Fatalf("result[*] should be nil: %#v", result) 107 | } 108 | } 109 | 110 | func TestParseFields_Flat(t *testing.T) { 111 | fields := "profile" 112 | result := ParseFields(fields) 113 | 114 | if _, ok := result["profile"]; !ok { 115 | t.Fatalf("result[profile] should exist: %#v", result) 116 | } 117 | 118 | if result["profile"] != nil { 119 | t.Fatalf("result[profile] should be nil: %#v", result) 120 | } 121 | } 122 | 123 | func TestParseFields_Nested(t *testing.T) { 124 | fields := "profile.nation" 125 | result := ParseFields(fields) 126 | 127 | if _, ok := result["profile"]; !ok { 128 | t.Fatalf("result[profile] should exist: %#v", result) 129 | } 130 | 131 | if _, ok := result["profile"].(map[string]interface{}); !ok { 132 | t.Fatalf("result[profile] should be map: %#v", result) 133 | } 134 | 135 | if _, ok := result["profile"].(map[string]interface{})["nation"]; !ok { 136 | t.Fatalf("result[profile][nation] should exist: %#v", result) 137 | } 138 | 139 | if result["profile"].(map[string]interface{})["nation"] != nil { 140 | t.Fatalf("result[profile][nation] should be nil: %#v", result) 141 | } 142 | } 143 | 144 | func TestParseFields_NestedDeeply(t *testing.T) { 145 | fields := "profile.nation.name" 146 | result := ParseFields(fields) 147 | 148 | if _, ok := result["profile"]; !ok { 149 | t.Fatalf("result[profile] should exist: %#v", result) 150 | } 151 | 152 | if _, ok := result["profile"].(map[string]interface{}); !ok { 153 | t.Fatalf("result[profile] should be map: %#v", result) 154 | } 155 | 156 | if _, ok := result["profile"].(map[string]interface{})["nation"]; !ok { 157 | t.Fatalf("result[profile][nation] should exist: %#v", result) 158 | } 159 | 160 | if _, ok := result["profile"].(map[string]interface{})["nation"].(map[string]interface{}); !ok { 161 | t.Fatalf("result[profile][nation] should be map: %#v", result) 162 | } 163 | 164 | if _, ok := result["profile"].(map[string]interface{})["nation"].(map[string]interface{})["name"]; !ok { 165 | t.Fatalf("result[profile][nation][name] should exist: %#v", result) 166 | } 167 | 168 | if result["profile"].(map[string]interface{})["nation"].(map[string]interface{})["name"] != nil { 169 | t.Fatalf("result[profile][nation][name] should be nil: %#v", result) 170 | } 171 | } 172 | 173 | func TestParseFields_MultipleFields(t *testing.T) { 174 | fields := "profile.nation.name,emails" 175 | result := ParseFields(fields) 176 | 177 | if _, ok := result["profile"]; !ok { 178 | t.Fatalf("result[profile] should exist: %#v", result) 179 | } 180 | 181 | if _, ok := result["profile"].(map[string]interface{}); !ok { 182 | t.Fatalf("result[profile] should be map: %#v", result) 183 | } 184 | 185 | if _, ok := result["profile"].(map[string]interface{})["nation"]; !ok { 186 | t.Fatalf("result[profile][nation] should exist: %#v", result) 187 | } 188 | 189 | if _, ok := result["profile"].(map[string]interface{})["nation"].(map[string]interface{}); !ok { 190 | t.Fatalf("result[profile][nation] should be map: %#v", result) 191 | } 192 | 193 | if _, ok := result["profile"].(map[string]interface{})["nation"].(map[string]interface{})["name"]; !ok { 194 | t.Fatalf("result[profile][nation][name] should exist: %#v", result) 195 | } 196 | 197 | if result["profile"].(map[string]interface{})["nation"].(map[string]interface{})["name"] != nil { 198 | t.Fatalf("result[profile][nation][name] should be nil: %#v", result) 199 | } 200 | 201 | if _, ok := result["emails"]; !ok { 202 | t.Fatalf("result[emails] should exist: %#v", result) 203 | } 204 | 205 | if result["emails"] != nil { 206 | t.Fatalf("result[emails] should be map: %#v", result) 207 | } 208 | } 209 | 210 | func TestParseFields_Included(t *testing.T) { 211 | fields := "profile.nation.name,profile" 212 | result := ParseFields(fields) 213 | 214 | if _, ok := result["profile"]; !ok { 215 | t.Fatalf("result[profile] should exist: %#v", result) 216 | } 217 | 218 | if result["profile"] != nil { 219 | t.Fatalf("result[profile] should be nil: %#v", result) 220 | } 221 | } 222 | 223 | var profile = Profile{ 224 | ID: 1, 225 | UserID: 1, 226 | User: nil, 227 | Engaged: true, 228 | } 229 | 230 | var job = Job{ 231 | ID: 1, 232 | UserID: 1, 233 | User: nil, 234 | RoleCd: 1, 235 | } 236 | 237 | func TestFieldToMap_Wildcard(t *testing.T) { 238 | user := User{ 239 | ID: 1, 240 | Jobs: []*Job{&job}, 241 | Name: "Taro Yamada", 242 | Profile: &profile, 243 | } 244 | 245 | fields := map[string]interface{}{ 246 | "*": nil, 247 | } 248 | result, err := FieldToMap(user, fields) 249 | 250 | if err != nil { 251 | t.Fatalf("FieldToMap return an error. detail: %#v", err.Error()) 252 | } 253 | 254 | for _, key := range []string{"id", "jobs", "name", "profile"} { 255 | if _, ok := result[key]; !ok { 256 | t.Fatalf("%s should exist. actual: %#v", key, result) 257 | } 258 | } 259 | 260 | if result["jobs"].([]*Job) == nil { 261 | t.Fatalf("jobs should not be nil. actual: %#v", result["jobs"]) 262 | } 263 | 264 | if result["profile"].(*Profile) == nil { 265 | t.Fatalf("profile should not be nil. actual: %#v", result["profile"]) 266 | } 267 | } 268 | 269 | func TestFieldToMap_OmitEmpty(t *testing.T) { 270 | user := User{ 271 | ID: 1, 272 | Jobs: nil, 273 | Name: "Taro Yamada", 274 | Profile: nil, 275 | } 276 | 277 | fields := map[string]interface{}{ 278 | "*": nil, 279 | } 280 | result, err := FieldToMap(user, fields) 281 | 282 | if err != nil { 283 | t.Fatalf("FieldToMap return an error. detail: %#v", err.Error()) 284 | } 285 | 286 | for _, key := range []string{"id", "name"} { 287 | if _, ok := result[key]; !ok { 288 | t.Fatalf("%s should exist. actual: %#v", key, result) 289 | } 290 | } 291 | 292 | for _, key := range []string{"jobs", "profile"} { 293 | if _, ok := result[key]; ok { 294 | t.Fatalf("%s should not exist. actual: %#v", key, result) 295 | } 296 | } 297 | } 298 | 299 | func TestFieldToMap_OmitEmptyWithField(t *testing.T) { 300 | user := User{ 301 | ID: 1, 302 | Jobs: nil, 303 | Name: "Taro Yamada", 304 | Profile: nil, 305 | } 306 | 307 | fields := map[string]interface{}{ 308 | "id": nil, 309 | "name": nil, 310 | "jobs": nil, 311 | } 312 | result, err := FieldToMap(user, fields) 313 | 314 | if err != nil { 315 | t.Fatalf("FieldToMap return an error. detail: %#v", err.Error()) 316 | } 317 | 318 | for _, key := range []string{"id", "name", "jobs"} { 319 | if _, ok := result[key]; !ok { 320 | t.Fatalf("%s should exist. actual: %#v", key, result) 321 | } 322 | } 323 | 324 | for _, key := range []string{"profile"} { 325 | if _, ok := result[key]; ok { 326 | t.Fatalf("%s should not exist. actual: %#v", key, result) 327 | } 328 | } 329 | } 330 | 331 | func TestFieldToMap_OmitEmptyAllTypes(t *testing.T) { 332 | company := Company{ 333 | ID: 0, 334 | Name: "", 335 | List: false, 336 | Subsidiary: []*Company{}, 337 | Organization: make(map[string]string), 338 | User: nil, 339 | } 340 | 341 | fields := map[string]interface{}{ 342 | "*": nil, 343 | } 344 | result, err := FieldToMap(company, fields) 345 | 346 | if err != nil { 347 | t.Fatalf("FieldToMap return an error. detail: %#v", err.Error()) 348 | } 349 | 350 | for _, key := range []string{"id", "name", "list", "subsidiary", "organization", "user"} { 351 | if _, ok := result[key]; ok { 352 | t.Fatalf("%s should not exist. actual: %#v", key, result) 353 | } 354 | } 355 | } 356 | 357 | func TestFieldToMap_SpecifyField(t *testing.T) { 358 | user := User{ 359 | ID: 1, 360 | Jobs: nil, 361 | Name: "Taro Yamada", 362 | Profile: nil, 363 | } 364 | 365 | fields := map[string]interface{}{ 366 | "id": nil, 367 | "name": nil, 368 | } 369 | result, err := FieldToMap(user, fields) 370 | 371 | if err != nil { 372 | t.Fatalf("FieldToMap return an error. detail: %#v", err.Error()) 373 | } 374 | 375 | for _, key := range []string{"id", "name"} { 376 | if _, ok := result[key]; !ok { 377 | t.Fatalf("%s should exist. actual: %#v", key, result) 378 | } 379 | } 380 | 381 | for _, key := range []string{"jobs", "profile"} { 382 | if _, ok := result[key]; ok { 383 | t.Fatalf("%s should not exist. actual: %#v", key, result) 384 | } 385 | } 386 | } 387 | 388 | func TestFieldToMap_NestedField(t *testing.T) { 389 | user := User{ 390 | ID: 1, 391 | Jobs: []*Job{&job}, 392 | Name: "Taro Yamada", 393 | Profile: &profile, 394 | } 395 | 396 | fields := map[string]interface{}{ 397 | "profile": map[string]interface{}{ 398 | "id": nil, 399 | }, 400 | "name": nil, 401 | } 402 | result, err := FieldToMap(user, fields) 403 | 404 | if err != nil { 405 | t.Fatalf("FieldToMap return an error. detail: %#v", err.Error()) 406 | } 407 | 408 | for _, key := range []string{"name", "profile"} { 409 | if _, ok := result[key]; !ok { 410 | t.Fatalf("%s should exist. actual: %#v", key, result) 411 | } 412 | } 413 | 414 | for _, key := range []string{"id", "jobs"} { 415 | if _, ok := result[key]; ok { 416 | t.Fatalf("%s should not exist. actual: %#v", key, result) 417 | } 418 | } 419 | 420 | if result["profile"].(map[string]interface{}) == nil { 421 | t.Fatalf("profile should not be nil. actual: %#v", result) 422 | } 423 | 424 | if _, ok := result["profile"].(map[string]interface{})["id"]; !ok { 425 | t.Fatalf("profile.id should exist. actual: %#v", result) 426 | } 427 | 428 | for _, key := range []string{"id"} { 429 | if _, ok := result["profile"].(map[string]interface{})[key]; !ok { 430 | t.Fatalf("profile.%s should exist. actual: %#v", key, result) 431 | } 432 | } 433 | 434 | for _, key := range []string{"user_id", "user", "engaged"} { 435 | if _, ok := result["profile"].(map[string]interface{})[key]; ok { 436 | t.Fatalf("profile.%s should not exist. actual: %#v", key, result) 437 | } 438 | } 439 | } 440 | -------------------------------------------------------------------------------- /_example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "strconv" 6 | 7 | "github.com/shimastripe/apig/_example/db" 8 | "github.com/shimastripe/apig/_example/server" 9 | ) 10 | 11 | // main ... 12 | func main() { 13 | database := db.Connect() 14 | s := server.Setup(database) 15 | port := "8080" 16 | 17 | if p := os.Getenv("PORT"); p != "" { 18 | if _, err := strconv.Atoi(p); err == nil { 19 | port = p 20 | } 21 | } 22 | 23 | s.Run(":" + port) 24 | } 25 | -------------------------------------------------------------------------------- /_example/middleware/set_db.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/jinzhu/gorm" 6 | ) 7 | 8 | func SetDBtoContext(db *gorm.DB) gin.HandlerFunc { 9 | return func(c *gin.Context) { 10 | c.Set("DB", db) 11 | c.Next() 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /_example/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shimastripe/apig/6535437d01d50156cd00d29f8303437a13eafad5/_example/models/.gitkeep -------------------------------------------------------------------------------- /_example/models/company.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "database/sql" 4 | 5 | type Company struct { 6 | ID uint `gorm:"primary_key;AUTO_INCREMENT" json:"id" form:"id"` 7 | Name string `json:"name" form:"name"` 8 | URL sql.NullString `json:"url" form:"url"` 9 | Jobs []*Job `json:"jobs" form:"jobs"` 10 | } 11 | -------------------------------------------------------------------------------- /_example/models/email.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Email struct { 4 | ID uint `gorm:"primary_key;AUTO_INCREMENT" json:"id" form:"id"` 5 | Address string `json:"address" form:"address"` 6 | UserID uint `json:"user_id" form:"user_id"` 7 | User *User `json:"user" form:"user"` 8 | } 9 | -------------------------------------------------------------------------------- /_example/models/job.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Job struct { 4 | ID uint `gorm:"primary_key;AUTO_INCREMENT" json:"id" form:"id"` 5 | UserID uint `json:"user_id" form:"user_id"` 6 | User *User `json:"user" form:"user"` 7 | CompanyID uint `json:"company_id" form:"company_id"` 8 | RoleCD uint `json:"role_cd" form:"role_cd"` 9 | } 10 | -------------------------------------------------------------------------------- /_example/models/profile.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "time" 4 | 5 | type Profile struct { 6 | ID uint `gorm:"primary_key;AUTO_INCREMENT" json:"id" form:"id"` 7 | UserID uint `json:"user_id" form:"user_id"` 8 | User *User `json:"user" form:"user"` 9 | Birthday time.Time `json:"birthday" form:"birthday"` 10 | Engaged bool `json:"engaged" form:"engaged"` 11 | } 12 | -------------------------------------------------------------------------------- /_example/models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type User struct { 4 | ID uint `gorm:"primary_key;AUTO_INCREMENT" json:"id" form:"id"` 5 | Name string `json:"name" form:"name"` 6 | Profile *Profile `json:"profile" form:"profile"` 7 | Jobs []*Job `json:"jobs" form:"jobs"` 8 | Emails []*Email `json:"emails" form:"emails"` 9 | } 10 | -------------------------------------------------------------------------------- /_example/router/router.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "github.com/shimastripe/apig/_example/controllers" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | func Initialize(r *gin.Engine) { 10 | r.GET("/", controllers.APIEndpoints) 11 | 12 | api := r.Group("api") 13 | { 14 | 15 | api.GET("/companies", controllers.GetCompanies) 16 | api.GET("/companies/:id", controllers.GetCompany) 17 | api.POST("/companies", controllers.CreateCompany) 18 | api.PUT("/companies/:id", controllers.UpdateCompany) 19 | api.DELETE("/companies/:id", controllers.DeleteCompany) 20 | 21 | api.GET("/emails", controllers.GetEmails) 22 | api.GET("/emails/:id", controllers.GetEmail) 23 | api.POST("/emails", controllers.CreateEmail) 24 | api.PUT("/emails/:id", controllers.UpdateEmail) 25 | api.DELETE("/emails/:id", controllers.DeleteEmail) 26 | 27 | api.GET("/jobs", controllers.GetJobs) 28 | api.GET("/jobs/:id", controllers.GetJob) 29 | api.POST("/jobs", controllers.CreateJob) 30 | api.PUT("/jobs/:id", controllers.UpdateJob) 31 | api.DELETE("/jobs/:id", controllers.DeleteJob) 32 | 33 | api.GET("/profiles", controllers.GetProfiles) 34 | api.GET("/profiles/:id", controllers.GetProfile) 35 | api.POST("/profiles", controllers.CreateProfile) 36 | api.PUT("/profiles/:id", controllers.UpdateProfile) 37 | api.DELETE("/profiles/:id", controllers.DeleteProfile) 38 | 39 | api.GET("/users", controllers.GetUsers) 40 | api.GET("/users/:id", controllers.GetUser) 41 | api.POST("/users", controllers.CreateUser) 42 | api.PUT("/users/:id", controllers.UpdateUser) 43 | api.DELETE("/users/:id", controllers.DeleteUser) 44 | 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /_example/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/shimastripe/apig/_example/middleware" 5 | "github.com/shimastripe/apig/_example/router" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/jinzhu/gorm" 9 | ) 10 | 11 | func Setup(db *gorm.DB) *gin.Engine { 12 | r := gin.Default() 13 | r.Use(middleware.SetDBtoContext(db)) 14 | router.Initialize(r) 15 | return r 16 | } 17 | -------------------------------------------------------------------------------- /_example/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "math" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | func New(c *gin.Context) (string, error) { 12 | ver := "" 13 | header := c.Request.Header.Get("Accept") 14 | header = strings.Join(strings.Fields(header), "") 15 | 16 | if strings.Contains(header, "version=") { 17 | ver = strings.Split(strings.SplitAfter(header, "version=")[1], ";")[0] 18 | } 19 | 20 | if v := c.Query("v"); v != "" { 21 | ver = v 22 | } 23 | 24 | if ver == "" { 25 | return "-1", nil 26 | } 27 | 28 | _, err := strconv.Atoi(strings.Join(strings.Split(ver, "."), "")) 29 | if err != nil { 30 | return "", err 31 | } 32 | 33 | return ver, nil 34 | } 35 | 36 | func Range(left string, op string, right string) bool { 37 | switch op { 38 | case "<": 39 | return (compare(left, right) == -1) 40 | case "<=": 41 | return (compare(left, right) <= 0) 42 | case ">": 43 | return (compare(left, right) == 1) 44 | case ">=": 45 | return (compare(left, right) >= 0) 46 | case "==": 47 | return (compare(left, right) == 0) 48 | } 49 | 50 | return false 51 | } 52 | 53 | func compare(left string, right string) int { 54 | // l > r : 1 55 | // l == r : 0 56 | // l < r : -1 57 | 58 | if left == "-1" { 59 | return 1 60 | } else if right == "-1" { 61 | return -1 62 | } 63 | 64 | lArr := strings.Split(left, ".") 65 | rArr := strings.Split(right, ".") 66 | lItems := len(lArr) 67 | rItems := len(rArr) 68 | min := int(math.Min(float64(lItems), float64(rItems))) 69 | 70 | for i := 0; i < min; i++ { 71 | l, _ := strconv.Atoi(lArr[i]) 72 | r, _ := strconv.Atoi(rArr[i]) 73 | 74 | if l != r { 75 | if l > r { 76 | return 1 77 | } 78 | 79 | return -1 80 | } 81 | } 82 | 83 | if lItems == rItems { 84 | return 0 85 | } 86 | 87 | if lItems < rItems { 88 | return 1 89 | } 90 | 91 | return -1 92 | } 93 | -------------------------------------------------------------------------------- /_example/version/version_test.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | func TestAcceptHeader(t *testing.T) { 11 | req, _ := http.NewRequest("GET", "/", nil) 12 | req.Header.Add("Accept", "application/json;version= 1.0.0 ; more information; more information") 13 | c := &gin.Context{ 14 | Request: req, 15 | } 16 | ver, _ := New(c) 17 | if ver != "1.0.0" { 18 | t.Errorf("Accept header should be `1.0.0`. actual: %#v", ver) 19 | } 20 | } 21 | 22 | func TestEmptyAcceptHeader(t *testing.T) { 23 | req, _ := http.NewRequest("GET", "/", nil) 24 | req.Header.Add("Accept", "application/json; more information; more information") 25 | c := &gin.Context{ 26 | Request: req, 27 | } 28 | ver, _ := New(c) 29 | if ver != "-1" { 30 | t.Errorf("Accept header should be the latest version `-1`. actual: %#v", ver) 31 | } 32 | } 33 | 34 | func TestUndefinedAcceptHeader(t *testing.T) { 35 | req, _ := http.NewRequest("GET", "/", nil) 36 | c := &gin.Context{ 37 | Request: req, 38 | } 39 | ver, _ := New(c) 40 | if ver != "-1" { 41 | t.Errorf("No accept header should be the latest version `-1`. actual: %#v", ver) 42 | } 43 | } 44 | 45 | func TestQuery(t *testing.T) { 46 | req, _ := http.NewRequest("GET", "/?v=1.0.1", nil) 47 | req.Header.Add("Accept", "application/json;version= 1.0.0 ; more information; more information") 48 | c := &gin.Context{ 49 | Request: req, 50 | } 51 | ver, _ := New(c) 52 | if ver != "1.0.1" { 53 | t.Errorf("URL Query should be `1.0.1`. actual: %#v", ver) 54 | } 55 | } 56 | 57 | func TestRange(t *testing.T) { 58 | 59 | if Range("1.2.3", "<", "0.9") { 60 | t.Errorf("defect in <") 61 | } 62 | 63 | if Range("1.2.3", "<", "0.9.1") { 64 | t.Errorf("defect in <") 65 | } 66 | 67 | if Range("1.2.3", "<", "1.2.2") { 68 | t.Errorf("defect in <") 69 | } 70 | 71 | if Range("1.2.3", "<", "1.2.3") { 72 | t.Errorf("defect in <") 73 | } 74 | 75 | if !Range("1.2.3", "<", "1.2.4") { 76 | t.Errorf("defect in <") 77 | } 78 | 79 | if !Range("1.2.3", "<", "1.2") { 80 | t.Errorf("defect in <") 81 | } 82 | 83 | if !Range("1.2.3", "<", "1.5") { 84 | t.Errorf("defect in <") 85 | } 86 | 87 | if !Range("1.2.3", "<", "-1") { 88 | t.Errorf("defect in <") 89 | } 90 | 91 | if Range("1.2.3", "<=", "0.9") { 92 | t.Errorf("defect in <=") 93 | } 94 | 95 | if Range("1.2.3", "<=", "0.9.1") { 96 | t.Errorf("defect in <=") 97 | } 98 | 99 | if Range("1.2.3", "<=", "1.2.2") { 100 | t.Errorf("defect in <=") 101 | } 102 | 103 | if !Range("1.2.3", "<=", "1.2.3") { 104 | t.Errorf("defect in <=") 105 | } 106 | 107 | if !Range("1.2.3", "<=", "1.2.4") { 108 | t.Errorf("defect in <=") 109 | } 110 | 111 | if !Range("1.2.3", "<=", "1.2") { 112 | t.Errorf("defect in <=") 113 | } 114 | 115 | if !Range("1.2.3", "<=", "1.5") { 116 | t.Errorf("defect in <=") 117 | } 118 | 119 | if !Range("1.2.3", "<=", "-1") { 120 | t.Errorf("defect in <=") 121 | } 122 | 123 | if !Range("1.2.3", ">", "0.9") { 124 | t.Errorf("defect in >") 125 | } 126 | 127 | if !Range("1.2.3", ">", "0.9.1") { 128 | t.Errorf("defect in >") 129 | } 130 | 131 | if !Range("1.2.3", ">", "1.2.2") { 132 | t.Errorf("defect in >") 133 | } 134 | 135 | if Range("1.2.3", ">", "1.2.3") { 136 | t.Errorf("defect in >") 137 | } 138 | 139 | if Range("1.2.3", ">", "1.2.4") { 140 | t.Errorf("defect in >") 141 | } 142 | 143 | if Range("1.2.3", ">", "1.2") { 144 | t.Errorf("defect in >") 145 | } 146 | 147 | if Range("1.2.3", ">", "1.5") { 148 | t.Errorf("defect in >") 149 | } 150 | 151 | if Range("1.2.3", ">", "-1") { 152 | t.Errorf("defect in >") 153 | } 154 | 155 | if !Range("1.2.3", ">=", "0.9") { 156 | t.Errorf("defect in >=") 157 | } 158 | 159 | if !Range("1.2.3", ">=", "0.9.1") { 160 | t.Errorf("defect in >=") 161 | } 162 | 163 | if !Range("1.2.3", ">=", "1.2.2") { 164 | t.Errorf("defect in >=") 165 | } 166 | 167 | if !Range("1.2.3", ">=", "1.2.3") { 168 | t.Errorf("defect in >=") 169 | } 170 | 171 | if Range("1.2.3", ">=", "1.2.4") { 172 | t.Errorf("defect in >=") 173 | } 174 | 175 | if Range("1.2.3", ">=", "1.2") { 176 | t.Errorf("defect in >=") 177 | } 178 | 179 | if Range("1.2.3", ">=", "1.5") { 180 | t.Errorf("defect in >=") 181 | } 182 | 183 | if Range("1.2.3", ">=", "-1") { 184 | t.Errorf("defect in >=") 185 | } 186 | 187 | if Range("1.2.3", "==", "0.9") { 188 | t.Errorf("defect in ==") 189 | } 190 | 191 | if Range("1.2.3", "==", "0.9.1") { 192 | t.Errorf("defect in ==") 193 | } 194 | 195 | if Range("1.2.3", "==", "1.2.2") { 196 | t.Errorf("defect in ==") 197 | } 198 | 199 | if !Range("1.2.3", "==", "1.2.3") { 200 | t.Errorf("defect in ==") 201 | } 202 | 203 | if Range("1.2.3", "==", "1.2.4") { 204 | t.Errorf("defect in ==") 205 | } 206 | 207 | if Range("1.2.3", "==", "1.2") { 208 | t.Errorf("defect in ==") 209 | } 210 | 211 | if Range("1.2.3", "==", "1.5") { 212 | t.Errorf("defect in ==") 213 | } 214 | 215 | if Range("1.2.3", "==", "-1") { 216 | t.Errorf("defect in ==") 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /_templates/README.md.tmpl: -------------------------------------------------------------------------------- 1 | # API Server 2 | 3 | Simple Rest API using gin(framework) & gorm(orm) 4 | 5 | ## Endpoint list 6 | {{ range .Models }} 7 | ### {{ pluralize .Name }} Resource 8 | 9 | ``` 10 | GET {{ if ne ($.Namespace) "" }}/{{ $.Namespace }}{{ end }}/{{ pluralize (toLower .Name) }} 11 | GET {{ if ne ($.Namespace) "" }}/{{ $.Namespace }}{{ end }}/{{ pluralize (toLower .Name) }}/:id 12 | POST {{ if ne ($.Namespace) "" }}/{{ $.Namespace }}{{ end }}/{{ pluralize (toLower .Name) }} 13 | PUT {{ if ne ($.Namespace) "" }}/{{ $.Namespace }}{{ end }}/{{ pluralize (toLower .Name) }}/:id 14 | DELETE {{ if ne ($.Namespace) "" }}/{{ $.Namespace }}{{ end }}/{{ pluralize (toLower .Name) }}/:id 15 | ``` 16 | {{ end }} 17 | server runs at http://localhost:8080 18 | -------------------------------------------------------------------------------- /_templates/controller.go.tmpl: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | dbpkg "{{ .ImportDir }}/db" 8 | "{{ .ImportDir }}/helper" 9 | "{{ .ImportDir }}/models" 10 | "{{ .ImportDir }}/version" 11 | 12 | "github.com/gin-gonic/gin" 13 | ) 14 | 15 | func Get{{ pluralize .Model.Name }}(c *gin.Context) { 16 | ver, err := version.New(c) 17 | if err != nil { 18 | c.JSON(400, gin.H{"error": err.Error()}) 19 | return 20 | } 21 | 22 | db := dbpkg.DBInstance(c) 23 | parameter, err := dbpkg.NewParameter(c, models.{{ .Model.Name }}{}) 24 | if err != nil { 25 | c.JSON(400, gin.H{"error": err.Error()}) 26 | return 27 | } 28 | 29 | db, err = parameter.Paginate(db) 30 | if err != nil { 31 | c.JSON(400, gin.H{"error": err.Error()}) 32 | return 33 | } 34 | 35 | db = parameter.SetPreloads(db) 36 | db = parameter.SortRecords(db) 37 | db = parameter.FilterFields(db) 38 | {{ pluralize (toLowerCamelCase .Model.Name) }} := []models.{{ .Model.Name }}{} 39 | fields := helper.ParseFields(c.DefaultQuery("fields", "*")) 40 | queryFields := helper.QueryFields(models.{{ .Model.Name }}{}, fields) 41 | 42 | if err := db.Select(queryFields).Find(&{{ pluralize (toLowerCamelCase .Model.Name) }}).Error; err != nil { 43 | c.JSON(400, gin.H{"error": err.Error()}) 44 | return 45 | } 46 | 47 | index := 0 48 | 49 | if len({{ pluralize (toLowerCamelCase .Model.Name) }}) > 0 { 50 | index = int({{ pluralize (toLowerCamelCase .Model.Name) }}[len({{ pluralize (toLowerCamelCase .Model.Name) }})-1].ID) 51 | } 52 | 53 | if err := parameter.SetHeaderLink(c, index); err != nil { 54 | c.JSON(400, gin.H{"error": err.Error()}) 55 | return 56 | } 57 | 58 | if version.Range("1.0.0", "<=", ver) && version.Range(ver, "<", "2.0.0") { 59 | // conditional branch by version. 60 | // 1.0.0 <= this version < 2.0.0 !! 61 | } 62 | 63 | if _, ok := c.GetQuery("stream"); ok { 64 | enc := json.NewEncoder(c.Writer) 65 | c.Status(200) 66 | 67 | for _, {{ toLowerCamelCase .Model.Name }} := range {{ pluralize (toLowerCamelCase .Model.Name) }} { 68 | fieldMap, err := helper.FieldToMap({{ toLowerCamelCase .Model.Name }}, fields) 69 | if err != nil { 70 | c.JSON(400, gin.H{"error": err.Error()}) 71 | return 72 | } 73 | 74 | if err := enc.Encode(fieldMap); err != nil { 75 | c.JSON(400, gin.H{"error": err.Error()}) 76 | return 77 | } 78 | } 79 | } else { 80 | fieldMaps := []map[string]interface{}{} 81 | 82 | for _, {{ toLowerCamelCase .Model.Name }} := range {{ pluralize (toLowerCamelCase .Model.Name) }} { 83 | fieldMap, err := helper.FieldToMap({{ toLowerCamelCase .Model.Name }}, fields) 84 | if err != nil { 85 | c.JSON(400, gin.H{"error": err.Error()}) 86 | return 87 | } 88 | 89 | fieldMaps = append(fieldMaps, fieldMap) 90 | } 91 | 92 | if _, ok := c.GetQuery("pretty"); ok { 93 | c.IndentedJSON(200, fieldMaps) 94 | } else { 95 | c.JSON(200, fieldMaps) 96 | } 97 | } 98 | } 99 | 100 | func Get{{ .Model.Name }}(c *gin.Context) { 101 | ver, err := version.New(c) 102 | if err != nil { 103 | c.JSON(400, gin.H{"error": err.Error()}) 104 | return 105 | } 106 | 107 | db := dbpkg.DBInstance(c) 108 | parameter, err := dbpkg.NewParameter(c, models.{{ .Model.Name }}{}) 109 | if err != nil { 110 | c.JSON(400, gin.H{"error": err.Error()}) 111 | return 112 | } 113 | 114 | db = parameter.SetPreloads(db) 115 | {{ toLowerCamelCase .Model.Name }} := models.{{ .Model.Name }}{} 116 | id := c.Params.ByName("id") 117 | fields := helper.ParseFields(c.DefaultQuery("fields", "*")) 118 | queryFields := helper.QueryFields(models.{{ .Model.Name }}{}, fields) 119 | 120 | if err := db.Select(queryFields).First(&{{ toLowerCamelCase .Model.Name }}, id).Error; err != nil { 121 | content := gin.H{"error": "{{ toSnakeCase .Model.Name }} with id#" + id + " not found"} 122 | c.JSON(404, content) 123 | return 124 | } 125 | 126 | fieldMap, err := helper.FieldToMap({{ toLowerCamelCase .Model.Name }}, fields) 127 | if err != nil { 128 | c.JSON(400, gin.H{"error": err.Error()}) 129 | return 130 | } 131 | 132 | if version.Range("1.0.0", "<=", ver) && version.Range(ver, "<", "2.0.0") { 133 | // conditional branch by version. 134 | // 1.0.0 <= this version < 2.0.0 !! 135 | } 136 | 137 | if _, ok := c.GetQuery("pretty"); ok { 138 | c.IndentedJSON(200, fieldMap) 139 | } else { 140 | c.JSON(200, fieldMap) 141 | } 142 | } 143 | 144 | func Create{{ .Model.Name }}(c *gin.Context) { 145 | ver, err := version.New(c) 146 | if err != nil { 147 | c.JSON(400, gin.H{"error": err.Error()}) 148 | return 149 | } 150 | 151 | db := dbpkg.DBInstance(c) 152 | {{ toLowerCamelCase .Model.Name }} := models.{{ .Model.Name }}{} 153 | 154 | if err := c.Bind(&{{ toLowerCamelCase .Model.Name }}); err != nil { 155 | c.JSON(400, gin.H{"error": err.Error()}) 156 | return 157 | } 158 | 159 | if err := db.Create(&{{ toLowerCamelCase .Model.Name }}).Error; err != nil { 160 | c.JSON(400, gin.H{"error": err.Error()}) 161 | return 162 | } 163 | 164 | if version.Range("1.0.0", "<=", ver) && version.Range(ver, "<", "2.0.0") { 165 | // conditional branch by version. 166 | // 1.0.0 <= this version < 2.0.0 !! 167 | } 168 | 169 | c.JSON(201, {{ toLowerCamelCase .Model.Name }}) 170 | } 171 | 172 | func Update{{ .Model.Name }}(c *gin.Context) { 173 | ver, err := version.New(c) 174 | if err != nil { 175 | c.JSON(400, gin.H{"error": err.Error()}) 176 | return 177 | } 178 | 179 | db := dbpkg.DBInstance(c) 180 | id := c.Params.ByName("id") 181 | {{ toLowerCamelCase .Model.Name }} := models.{{ .Model.Name }}{} 182 | 183 | if db.First(&{{ toLowerCamelCase .Model.Name }}, id).Error != nil { 184 | content := gin.H{"error": "{{ toSnakeCase .Model.Name }} with id#" + id + " not found"} 185 | c.JSON(404, content) 186 | return 187 | } 188 | 189 | if err := c.Bind(&{{ toLowerCamelCase .Model.Name }}); err != nil { 190 | c.JSON(400, gin.H{"error": err.Error()}) 191 | return 192 | } 193 | 194 | if err := db.Save(&{{ toLowerCamelCase .Model.Name }}).Error; err != nil { 195 | c.JSON(400, gin.H{"error": err.Error()}) 196 | return 197 | } 198 | 199 | if version.Range("1.0.0", "<=", ver) && version.Range(ver, "<", "2.0.0") { 200 | // conditional branch by version. 201 | // 1.0.0 <= this version < 2.0.0 !! 202 | } 203 | 204 | c.JSON(200, {{ toLowerCamelCase .Model.Name }}) 205 | } 206 | 207 | func Delete{{ .Model.Name }}(c *gin.Context) { 208 | ver, err := version.New(c) 209 | if err != nil { 210 | c.JSON(400, gin.H{"error": err.Error()}) 211 | return 212 | } 213 | 214 | db := dbpkg.DBInstance(c) 215 | id := c.Params.ByName("id") 216 | {{ toLowerCamelCase .Model.Name }} := models.{{ .Model.Name }}{} 217 | 218 | if db.First(&{{ toLowerCamelCase .Model.Name }}, id).Error != nil { 219 | content := gin.H{"error": "{{ toSnakeCase .Model.Name }} with id#" + id + " not found"} 220 | c.JSON(404, content) 221 | return 222 | } 223 | 224 | if err := db.Delete(&{{ toLowerCamelCase .Model.Name }}).Error; err != nil { 225 | c.JSON(400, gin.H{"error": err.Error()}) 226 | return 227 | } 228 | 229 | if version.Range("1.0.0", "<=", ver) && version.Range(ver, "<", "2.0.0") { 230 | // conditional branch by version. 231 | // 1.0.0 <= this version < 2.0.0 !! 232 | } 233 | 234 | c.Writer.WriteHeader(http.StatusNoContent) 235 | } 236 | -------------------------------------------------------------------------------- /_templates/db.go.tmpl: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "log" 5 | "os" 6 | {{ if (eq .Database "sqlite") }} "path/filepath" 7 | {{ end -}} 8 | "strings" 9 | 10 | "{{ .ImportDir }}/models" 11 | 12 | "github.com/gin-gonic/gin" 13 | "github.com/jinzhu/gorm" 14 | _ "github.com/jinzhu/gorm/dialects/{{ .Database }}" 15 | "github.com/serenize/snaker" 16 | ) 17 | 18 | func Connect() *gorm.DB { 19 | {{ if (eq .Database "sqlite") -}} 20 | dir := filepath.Dir("db/database.db") 21 | db, err := gorm.Open("sqlite3", dir+"/database.db") 22 | {{ else if (eq .Database "postgres") -}} 23 | dbURL := os.Getenv("DATABASE_URL") 24 | if dbURL == "" { 25 | return nil 26 | } 27 | 28 | db, err := gorm.Open("postgres", dbURL) 29 | {{ else if (eq .Database "mysql") -}} 30 | dbURL := os.Getenv("DATABASE_URL") 31 | if dbURL == "" { 32 | return nil 33 | } 34 | 35 | db, err := gorm.Open("mysql", dbURL) 36 | {{ end -}} 37 | if err != nil { 38 | log.Fatalf("Got error when connect database, the error is '%v'", err) 39 | } 40 | 41 | db.LogMode(false) 42 | 43 | if gin.IsDebugging() { 44 | db.LogMode(true) 45 | } 46 | 47 | if os.Getenv("AUTOMIGRATE") == "1" { 48 | db.AutoMigrate({{ range .Models }} 49 | &models.{{ .Name }}{},{{ end }} 50 | ) 51 | } 52 | 53 | return db 54 | } 55 | 56 | func DBInstance(c *gin.Context) *gorm.DB { 57 | return c.MustGet("DB").(*gorm.DB) 58 | } 59 | 60 | func (self *Parameter) SetPreloads(db *gorm.DB) *gorm.DB { 61 | if self.Preloads == "" { 62 | return db 63 | } 64 | 65 | for _, preload := range strings.Split(self.Preloads, ",") { 66 | var a []string 67 | 68 | for _, s := range strings.Split(preload, ".") { 69 | a = append(a, snaker.SnakeToCamel(s)) 70 | } 71 | 72 | db = db.Preload(strings.Join(a, ".")) 73 | } 74 | 75 | return db 76 | } 77 | -------------------------------------------------------------------------------- /_templates/index.apib.tmpl: -------------------------------------------------------------------------------- 1 | FORMAT: 1A 2 | HOST: http://localhost:8080 3 | 4 | # {{ title .Project }} API 5 | {{ range .Models }} 6 | {{ end }} 7 | -------------------------------------------------------------------------------- /_templates/model.apib.tmpl: -------------------------------------------------------------------------------- 1 | # Group {{ pluralize .Model.Name }} 2 | Welcome to the {{ pluralize (toOriginalCase .Model.Name) }} API. This API provides access to the {{ pluralize (toOriginalCase .Model.Name) }} service. 3 | 4 | ## {{ pluralize (toOriginalCase .Model.Name) }} [/{{ pluralize (toSnakeCase .Model.Name) }}] 5 | 6 | ### Create {{ toOriginalCase .Model.Name }} [POST] 7 | 8 | Create a new {{ toOriginalCase .Model.Name }} 9 | 10 | + Request {{ toOriginalCase .Model.Name }} (application/json; charset=utf-8) 11 | + Headers 12 | 13 | Accept: application/vnd.{{ .User }}+json 14 | + Attributes 15 | {{ range (requestParams .Model.Fields) }} 16 | + {{ .JSONName }}{{ if ne (apibDefaultValue .) "" }}: {{ apibDefaultValue . }}{{ end }} ({{ apibType . }}){{ end }} 17 | 18 | + Response 201 (application/json; charset=utf-8) 19 | + Attributes ({{ toSnakeCase .Model.Name }}, fixed) 20 | 21 | ### Get {{ pluralize (toOriginalCase .Model.Name) }} [GET] 22 | 23 | Returns {{ article (toOriginalCase .Model.Name) }} list. 24 | 25 | + Request (application/json; charset=utf-8) 26 | + Headers 27 | 28 | Accept: application/vnd.{{ .User }}+json 29 | 30 | + Response 200 (application/json; charset=utf-8) 31 | + Attributes (array, fixed) 32 | + ({{ toSnakeCase .Model.Name }}) 33 | 34 | ## {{ toOriginalCase .Model.Name }} details [/{{ pluralize (toSnakeCase .Model.Name) }}/{id}] 35 | 36 | + Parameters 37 | + id: `1` (enum[string]) - The ID of the desired {{ toOriginalCase .Model.Name }}. 38 | + Members 39 | + `1` 40 | + `2` 41 | + `3` 42 | 43 | ### Get {{ toOriginalCase .Model.Name }} [GET] 44 | 45 | Returns {{ article (toOriginalCase .Model.Name) }}. 46 | 47 | + Request (application/json; charset=utf-8) 48 | + Headers 49 | 50 | Accept: application/vnd.{{ .User }}+json 51 | 52 | + Response 200 (application/json; charset=utf-8) 53 | + Attributes ({{ toSnakeCase .Model.Name }}, fixed) 54 | 55 | ### Update {{ toOriginalCase .Model.Name }} [PUT] 56 | 57 | Update {{ article (toOriginalCase .Model.Name) }}. 58 | 59 | + Request {{ toSnakeCase .Model.Name }} (application/json; charset=utf-8) 60 | + Headers 61 | 62 | Accept: application/vnd.{{ .User }}+json 63 | + Attributes 64 | {{ range (requestParams .Model.Fields) }} 65 | + {{ .JSONName }}{{ if ne (apibDefaultValue .) "" }}: {{ apibDefaultValue . }}{{ end }} ({{ apibType . }}){{ end }} 66 | 67 | + Response 200 (application/json; charset=utf-8) 68 | + Attributes ({{ toSnakeCase .Model.Name }}, fixed) 69 | 70 | ### Delete {{ toOriginalCase .Model.Name }} [DELETE] 71 | 72 | Delete {{ article (toOriginalCase .Model.Name) }}. 73 | 74 | + Request (application/json; charset=utf-8) 75 | + Headers 76 | 77 | Accept: application/vnd.{{ .User }}+json 78 | 79 | + Response 204 80 | 81 | # Data Structures 82 | ## {{ toSnakeCase .Model.Name }} (object) 83 | {{ range $key, $value := .Model.Fields }} 84 | + {{ .JSONName }}{{ if ne (apibDefaultValue .) "" }}: {{ apibExampleValue (apibDefaultValue .) }}{{ end }} ({{ apibType . }}){{ end }} 85 | -------------------------------------------------------------------------------- /_templates/root_controller.go.tmpl: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | func APIEndpoints(c *gin.Context) { 11 | reqScheme := "http" 12 | 13 | if c.Request.TLS != nil { 14 | reqScheme = "https" 15 | } 16 | 17 | reqHost := c.Request.Host 18 | baseURL := fmt.Sprintf("%s://%s", reqScheme, reqHost) 19 | 20 | resources := map[string]string{ 21 | {{ range .Models }} "{{ pluralize (toSnakeCase .Name) }}_url": baseURL + "{{ if ne ($.Namespace) "" }}/{{ $.Namespace }}{{ end }}/{{ pluralize (toSnakeCase .Name) }}", 22 | "{{ toSnakeCase .Name }}_url": baseURL + "{{ if ne ($.Namespace) "" }}/{{ $.Namespace }}{{ end }}/{{ pluralize (toSnakeCase .Name) }}/{id}", 23 | {{ end }} } 24 | 25 | c.IndentedJSON(http.StatusOK, resources) 26 | } 27 | -------------------------------------------------------------------------------- /_templates/router.go.tmpl: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "{{ .ImportDir }}/controllers" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | func Initialize(r *gin.Engine) { 10 | r.GET("/", controllers.APIEndpoints) 11 | 12 | api := r.Group("{{ .Namespace }}") 13 | { 14 | {{ range .Models }} 15 | api.GET("/{{ pluralize (toSnakeCase .Name) }}", controllers.Get{{ pluralize .Name }}) 16 | api.GET("/{{ pluralize (toSnakeCase .Name) }}/:id", controllers.Get{{ .Name }}) 17 | api.POST("/{{ pluralize (toSnakeCase .Name) }}", controllers.Create{{ .Name }}) 18 | api.PUT("/{{ pluralize (toSnakeCase .Name) }}/:id", controllers.Update{{ .Name }}) 19 | api.DELETE("/{{ pluralize (toSnakeCase .Name) }}/:id", controllers.Delete{{ .Name }}) 20 | {{ end }} 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /_templates/skeleton/.gitignore.tmpl: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /_templates/skeleton/README.md.tmpl: -------------------------------------------------------------------------------- 1 | # {{ .Project }} 2 | -------------------------------------------------------------------------------- /_templates/skeleton/controllers/.gitkeep.tmpl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shimastripe/apig/6535437d01d50156cd00d29f8303437a13eafad5/_templates/skeleton/controllers/.gitkeep.tmpl -------------------------------------------------------------------------------- /_templates/skeleton/db/db.go.tmpl: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "log" 5 | "os" 6 | {{ if (eq .Database "sqlite") }} "path/filepath" 7 | {{ end -}} 8 | "strings" 9 | 10 | "github.com/gin-gonic/gin" 11 | "github.com/jinzhu/gorm" 12 | _ "github.com/jinzhu/gorm/dialects/{{ .Database }}" 13 | "github.com/serenize/snaker" 14 | ) 15 | 16 | func Connect() *gorm.DB { 17 | {{ if (eq .Database "sqlite") -}} 18 | dir := filepath.Dir("db/database.db") 19 | db, err := gorm.Open("sqlite3", dir+"/database.db") 20 | {{ else if (eq .Database "postgres") -}} 21 | dbURL := os.Getenv("DATABASE_URL") 22 | if dbURL == "" { 23 | return nil 24 | } 25 | 26 | db, err := gorm.Open("postgres", dbURL) 27 | {{ else if (eq .Database "mysql") -}} 28 | dbURL := os.Getenv("DATABASE_URL") 29 | if dbURL == "" { 30 | return nil 31 | } 32 | 33 | db, err := gorm.Open("mysql", dbURL) 34 | {{ end -}} 35 | if err != nil { 36 | log.Fatalf("Got error when connect database, the error is '%v'", err) 37 | } 38 | 39 | db.LogMode(false) 40 | 41 | if gin.IsDebugging() { 42 | db.LogMode(true) 43 | } 44 | 45 | return db 46 | } 47 | 48 | func DBInstance(c *gin.Context) *gorm.DB { 49 | return c.MustGet("DB").(*gorm.DB) 50 | } 51 | 52 | func (self *Parameter) SetPreloads(db *gorm.DB) *gorm.DB { 53 | if self.Preloads == "" { 54 | return db 55 | } 56 | 57 | for _, preload := range strings.Split(self.Preloads, ",") { 58 | var a []string 59 | 60 | for _, s := range strings.Split(preload, ".") { 61 | a = append(a, snaker.SnakeToCamel(s)) 62 | } 63 | 64 | db = db.Preload(strings.Join(a, ".")) 65 | } 66 | 67 | return db 68 | } 69 | -------------------------------------------------------------------------------- /_templates/skeleton/db/filter.go.tmpl: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strings" 7 | 8 | "github.com/gin-gonic/gin" 9 | "github.com/jinzhu/gorm" 10 | ) 11 | 12 | func filterToMap(c *gin.Context, model interface{}) map[string]string { 13 | var jsonTag, jsonKey string 14 | filters := make(map[string]string) 15 | ts := reflect.TypeOf(model) 16 | 17 | for i := 0; i < ts.NumField(); i++ { 18 | f := ts.Field(i) 19 | jsonKey = f.Name 20 | 21 | if jsonTag = f.Tag.Get("json"); jsonTag != "" { 22 | jsonKey = strings.Split(jsonTag, ",")[0] 23 | } 24 | 25 | filters[jsonKey] = c.Query("q[" + jsonKey + "]") 26 | } 27 | 28 | return filters 29 | } 30 | 31 | func (self *Parameter) FilterFields(db *gorm.DB) *gorm.DB { 32 | for k, v := range self.Filters { 33 | if v != "" { 34 | db = db.Where(fmt.Sprintf("%s IN (?)", k), strings.Split(v, ",")) 35 | } 36 | } 37 | 38 | return db 39 | } 40 | 41 | func (self *Parameter) GetRawFilterQuery() string { 42 | var s string 43 | 44 | for k, v := range self.Filters { 45 | if v != "" { 46 | s += "&q[" + k + "]=" + v 47 | } 48 | } 49 | 50 | return s 51 | } 52 | -------------------------------------------------------------------------------- /_templates/skeleton/db/filter_test.go.tmpl: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | type User struct { 11 | ID uint `json:"id,omitempty" form:"id"` 12 | Name string `json:"name,omitempty" form:"name"` 13 | Engaged bool `json:"engaged,omitempty" form:"engaged"` 14 | } 15 | 16 | func contains(ss map[string]string, s string) bool { 17 | _, ok := ss[s] 18 | 19 | return ok 20 | } 21 | 22 | func TestFilterToMap(t *testing.T) { 23 | req, _ := http.NewRequest("GET", "/?q[id]=1,5,100&q[name]=hoge,fuga&q[unexisted_field]=null", nil) 24 | c := &gin.Context{ 25 | Request: req, 26 | } 27 | value := filterToMap(c, User{}) 28 | 29 | if !contains(value, "id") { 30 | t.Fatalf("Filter should have `id` key.") 31 | } 32 | 33 | if !contains(value, "name") { 34 | t.Fatalf("Filter should have `name` key.") 35 | } 36 | 37 | if contains(value, "unexisted_field") { 38 | t.Fatalf("Filter should not have `unexisted_field` key.") 39 | } 40 | 41 | if value["id"] != "1,5,100" { 42 | t.Fatalf("filters[\"id\"] expected: `1,5,100`, actual: %s", value["id"]) 43 | } 44 | 45 | if value["name"] != "hoge,fuga" { 46 | t.Fatalf("filters[\"name\"] expected: `hoge,fuga`, actual: %s", value["id"]) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /_templates/skeleton/db/pagination.go.tmpl: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/jinzhu/gorm" 9 | ) 10 | 11 | func (self *Parameter) Paginate(db *gorm.DB) (*gorm.DB, error) { 12 | if self == nil { 13 | return nil, errors.New("Parameter struct got nil.") 14 | } 15 | 16 | if self.IsLastID { 17 | if self.Order == "asc" { 18 | return db.Where("id > ?", self.LastID).Limit(self.Limit).Order("id asc"), nil 19 | } 20 | 21 | return db.Where("id < ?", self.LastID).Limit(self.Limit).Order("id desc"), nil 22 | } 23 | 24 | return db.Offset(self.Limit * (self.Page - 1)).Limit(self.Limit), nil 25 | } 26 | 27 | func (self *Parameter) SetHeaderLink(c *gin.Context, index int) error { 28 | if self == nil { 29 | return errors.New("Parameter struct got nil.") 30 | } 31 | 32 | var pretty, filters, preloads string 33 | reqScheme := "http" 34 | 35 | if c.Request.TLS != nil { 36 | reqScheme = "https" 37 | } 38 | 39 | if _, ok := c.GetQuery("pretty"); ok { 40 | pretty = "&pretty" 41 | } 42 | 43 | if len(self.Filters) != 0 { 44 | filters = self.GetRawFilterQuery() 45 | } 46 | 47 | if self.Preloads != "" { 48 | preloads = fmt.Sprintf("&preloads=%v", self.Preloads) 49 | } 50 | 51 | if self.IsLastID { 52 | c.Header("Link", fmt.Sprintf("<%s://%v%v?limit=%v%s%s&last_id=%v&order=%v%s>; rel=\"next\"", reqScheme, c.Request.Host, c.Request.URL.Path, self.Limit, filters, preloads, index, self.Order, pretty)) 53 | return nil 54 | } 55 | 56 | if self.Page == 1 { 57 | c.Header("Link", fmt.Sprintf("<%s://%v%v?limit=%v%s%s&page=%v%s>; rel=\"next\"", reqScheme, c.Request.Host, c.Request.URL.Path, self.Limit, filters, preloads, self.Page+1, pretty)) 58 | return nil 59 | } 60 | 61 | c.Header("Link", fmt.Sprintf( 62 | "<%s://%v%v?limit=%v%s%s&page=%v%s>; rel=\"next\",<%s://%v%v?limit=%v%s%s&page=%v%s>; rel=\"prev\"", reqScheme, 63 | c.Request.Host, c.Request.URL.Path, self.Limit, filters, preloads, self.Page+1, pretty, reqScheme, c.Request.Host, c.Request.URL.Path, self.Limit, filters, preloads, self.Page-1, pretty)) 64 | return nil 65 | } 66 | -------------------------------------------------------------------------------- /_templates/skeleton/db/parameter.go.tmpl: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "math" 5 | "strconv" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | const ( 11 | defaultLimit = "25" 12 | defaultPage = "1" 13 | defaultOrder = "desc" 14 | ) 15 | 16 | type Parameter struct { 17 | Filters map[string]string 18 | Preloads string 19 | Sort string 20 | Limit int 21 | Page int 22 | LastID int 23 | Order string 24 | IsLastID bool 25 | } 26 | 27 | func NewParameter(c *gin.Context, model interface{}) (*Parameter, error) { 28 | parameter := &Parameter{} 29 | 30 | if err := parameter.initialize(c, model); err != nil { 31 | return nil, err 32 | } 33 | 34 | return parameter, nil 35 | } 36 | 37 | func (self *Parameter) initialize(c *gin.Context, model interface{}) error { 38 | self.Filters = filterToMap(c, model) 39 | self.Preloads = c.Query("preloads") 40 | self.Sort = c.Query("sort") 41 | 42 | limit, err := validate(c.DefaultQuery("limit", defaultLimit)) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | self.Limit = int(math.Max(1, math.Min(10000, float64(limit)))) 48 | page, err := validate(c.DefaultQuery("page", defaultPage)) 49 | if err != nil { 50 | return err 51 | } 52 | 53 | self.Page = int(math.Max(1, float64(page))) 54 | lastID, err := validate(c.Query("last_id")) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | if lastID != -1 { 60 | self.IsLastID = true 61 | self.LastID = int(math.Max(0, float64(lastID))) 62 | } 63 | 64 | self.Order = c.DefaultQuery("order", defaultOrder) 65 | return nil 66 | } 67 | 68 | func validate(s string) (int, error) { 69 | if s == "" { 70 | return -1, nil 71 | } 72 | 73 | num, err := strconv.Atoi(s) 74 | if err != nil { 75 | return 0, err 76 | } 77 | 78 | return num, nil 79 | } 80 | -------------------------------------------------------------------------------- /_templates/skeleton/db/sort.go.tmpl: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/jinzhu/gorm" 7 | ) 8 | 9 | func convertPrefixToQuery(sort string) string { 10 | if strings.HasPrefix(sort, "-") { 11 | return strings.TrimLeft(sort, "-") + " desc" 12 | } else { 13 | return strings.TrimLeft(sort, " ") + " asc" 14 | } 15 | } 16 | 17 | func (self *Parameter) SortRecords(db *gorm.DB) *gorm.DB { 18 | if self.Sort == "" { 19 | return db 20 | } 21 | 22 | for _, sort := range strings.Split(self.Sort, ",") { 23 | db = db.Order(convertPrefixToQuery(sort)) 24 | } 25 | 26 | return db 27 | } 28 | -------------------------------------------------------------------------------- /_templates/skeleton/db/sort_test.go.tmpl: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import "testing" 4 | 5 | func TestConvertPrefixToQueryPlus(t *testing.T) { 6 | value := convertPrefixToQuery("id") 7 | 8 | if value != "id asc" { 9 | t.Fatalf("Expected: `id asc`, actual: %s", value) 10 | } 11 | 12 | value = convertPrefixToQuery(" id") 13 | 14 | if value != "id asc" { 15 | t.Fatalf("Expected: `id asc`, actual: %s", value) 16 | } 17 | } 18 | 19 | func TestConvertPrefixToQueryMinus(t *testing.T) { 20 | value := convertPrefixToQuery("-id") 21 | 22 | if value != "id desc" { 23 | t.Fatalf("Expected: `id desc`, actual: %s", value) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /_templates/skeleton/docs/.gitkeep.tmpl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shimastripe/apig/6535437d01d50156cd00d29f8303437a13eafad5/_templates/skeleton/docs/.gitkeep.tmpl -------------------------------------------------------------------------------- /_templates/skeleton/helper/field.go.tmpl: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "strings" 7 | 8 | "github.com/serenize/snaker" 9 | ) 10 | 11 | type AssociationType int 12 | 13 | const ( 14 | none AssociationType = iota 15 | belongsTo 16 | hasMany 17 | hasOne 18 | ) 19 | 20 | func contains(ss map[string]interface{}, s string) bool { 21 | _, ok := ss[s] 22 | 23 | return ok 24 | } 25 | 26 | func merge(m1, m2 map[string]interface{}) map[string]interface{} { 27 | result := make(map[string]interface{}) 28 | 29 | for k, v := range m1 { 30 | result[k] = v 31 | } 32 | 33 | for k, v := range m2 { 34 | result[k] = v 35 | } 36 | 37 | return result 38 | } 39 | 40 | func QueryFields(model interface{}, fields map[string]interface{}) string { 41 | var jsonTag, jsonKey string 42 | 43 | ts, vs := reflect.TypeOf(model), reflect.ValueOf(model) 44 | 45 | assocs := make(map[string]AssociationType) 46 | 47 | for i := 0; i < ts.NumField(); i++ { 48 | f := ts.Field(i) 49 | jsonTag = f.Tag.Get("json") 50 | 51 | if jsonTag == "" { 52 | jsonKey = f.Name 53 | } else { 54 | jsonKey = strings.Split(jsonTag, ",")[0] 55 | } 56 | 57 | switch vs.Field(i).Kind() { 58 | case reflect.Ptr: 59 | if _, ok := ts.FieldByName(f.Name + "ID"); ok { 60 | assocs[jsonKey] = belongsTo 61 | } else { 62 | assocs[jsonKey] = hasOne 63 | } 64 | case reflect.Slice: 65 | assocs[jsonKey] = hasMany 66 | default: 67 | assocs[jsonKey] = none 68 | } 69 | } 70 | 71 | result := []string{} 72 | 73 | for k := range fields { 74 | if k == "*" { 75 | return "*" 76 | } 77 | 78 | if _, ok := assocs[k]; !ok { 79 | continue 80 | } 81 | 82 | switch assocs[k] { 83 | case none: 84 | result = append(result, k) 85 | case belongsTo: 86 | result = append(result, k+"_id") 87 | default: 88 | result = append(result, "id") 89 | } 90 | } 91 | 92 | return strings.Join(result, ",") 93 | } 94 | 95 | func ParseFields(fields string) map[string]interface{} { 96 | result := make(map[string]interface{}) 97 | 98 | if fields == "*" { 99 | result["*"] = nil 100 | return result 101 | } 102 | 103 | for _, field := range strings.Split(fields, ",") { 104 | parts := strings.SplitN(field, ".", 2) 105 | 106 | if len(parts) == 2 { 107 | if result[parts[0]] == nil { 108 | result[parts[0]] = ParseFields(parts[1]) 109 | } else { 110 | result[parts[0]] = merge(result[parts[0]].(map[string]interface{}), ParseFields(parts[1])) 111 | } 112 | } else { 113 | result[parts[0]] = nil 114 | } 115 | } 116 | 117 | return result 118 | } 119 | 120 | func isEmptyValue(v reflect.Value) bool { 121 | switch v.Kind() { 122 | case reflect.Array, reflect.Map, reflect.Slice, reflect.String: 123 | return v.Len() == 0 124 | case reflect.Bool: 125 | return !v.Bool() 126 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 127 | return v.Int() == 0 128 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: 129 | return v.Uint() == 0 130 | case reflect.Float32, reflect.Float64: 131 | return v.Float() == 0 132 | case reflect.Interface, reflect.Ptr: 133 | return v.IsNil() 134 | } 135 | return false 136 | } 137 | 138 | func FieldToMap(model interface{}, fields map[string]interface{}) (map[string]interface{}, error) { 139 | u := make(map[string]interface{}) 140 | ts, vs := reflect.TypeOf(model), reflect.ValueOf(model) 141 | 142 | if vs.Kind() != reflect.Struct { 143 | return nil, errors.New("Invalid Parameter. The specified parameter does not have a structure.") 144 | } 145 | 146 | if !contains(fields, "*") { 147 | for field, _ := range fields { 148 | if !vs.FieldByName(snaker.SnakeToCamel(field)).IsValid() { 149 | return nil, errors.New("Invalid Parameter. The specified field does not exist.") 150 | } 151 | } 152 | } 153 | 154 | var jsonKey string 155 | var omitEmpty bool 156 | 157 | for i := 0; i < ts.NumField(); i++ { 158 | field := ts.Field(i) 159 | jsonTag := field.Tag.Get("json") 160 | omitEmpty = false 161 | 162 | if jsonTag == "" { 163 | jsonKey = field.Name 164 | } else { 165 | ss := strings.Split(jsonTag, ",") 166 | jsonKey = ss[0] 167 | 168 | if len(ss) > 1 && ss[1] == "omitempty" { 169 | omitEmpty = true 170 | } 171 | } 172 | 173 | if contains(fields, "*") { 174 | if !omitEmpty || !isEmptyValue(vs.Field(i)) { 175 | u[jsonKey] = vs.Field(i).Interface() 176 | } 177 | 178 | continue 179 | } 180 | 181 | if contains(fields, jsonKey) { 182 | v := fields[jsonKey] 183 | 184 | if vs.Field(i).Kind() == reflect.Ptr { 185 | if !vs.Field(i).IsNil() { 186 | if v == nil { 187 | u[jsonKey] = vs.Field(i).Elem().Interface() 188 | } else { 189 | k, err := FieldToMap(vs.Field(i).Elem().Interface(), v.(map[string]interface{})) 190 | 191 | if err != nil { 192 | return nil, err 193 | } 194 | 195 | u[jsonKey] = k 196 | } 197 | } else { 198 | if v == nil { 199 | u[jsonKey] = nil 200 | } else { 201 | return nil, errors.New("Invalid Parameter. The structure is null.") 202 | } 203 | } 204 | } else if vs.Field(i).Kind() == reflect.Slice { 205 | var fieldMap []interface{} 206 | s := reflect.ValueOf(vs.Field(i).Interface()) 207 | 208 | for i := 0; i < s.Len(); i++ { 209 | if v == nil { 210 | fieldMap = append(fieldMap, s.Index(i).Interface()) 211 | } else { 212 | 213 | if s.Index(i).Kind() == reflect.Ptr { 214 | k, err := FieldToMap(s.Index(i).Elem().Interface(), v.(map[string]interface{})) 215 | 216 | if err != nil { 217 | return nil, err 218 | } 219 | 220 | fieldMap = append(fieldMap, k) 221 | } else { 222 | k, err := FieldToMap(s.Index(i).Interface(), v.(map[string]interface{})) 223 | 224 | if err != nil { 225 | return nil, err 226 | } 227 | 228 | fieldMap = append(fieldMap, k) 229 | } 230 | } 231 | } 232 | 233 | u[jsonKey] = fieldMap 234 | } else { 235 | if v == nil { 236 | u[jsonKey] = vs.Field(i).Interface() 237 | } else { 238 | k, err := FieldToMap(vs.Field(i).Interface(), v.(map[string]interface{})) 239 | 240 | if err != nil { 241 | return nil, err 242 | } 243 | 244 | u[jsonKey] = k 245 | } 246 | } 247 | } 248 | } 249 | 250 | return u, nil 251 | } 252 | -------------------------------------------------------------------------------- /_templates/skeleton/helper/field_test.go.tmpl: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | type User struct { 8 | ID uint `json:"id" form:"id"` 9 | Jobs []*Job `json:"jobs,omitempty" form:"jobs"` 10 | Name string `json:"name" form:"name"` 11 | Profile *Profile `json:"profile,omitempty" form:"profile"` 12 | } 13 | 14 | type Profile struct { 15 | ID uint `json:"id" form:"id"` 16 | UserID uint `json:"user_id" form:"user_id"` 17 | User *User `json:"user" form:"user"` 18 | Engaged bool `json:"engaged" form:"engaged"` 19 | } 20 | 21 | type Job struct { 22 | ID uint `json:"id" form:"id"` 23 | UserID uint `json:"user_id" form:"user_id"` 24 | User *User `json:"user" form:"user"` 25 | RoleCd uint `json:"role_cd" form:"role_cd"` 26 | } 27 | 28 | type Company struct { 29 | ID uint `json:"id,omitempty" form:"id"` 30 | Name string `json:"name,omitempty" form:"name"` 31 | List bool `json:"list,omitempty" form:"list"` 32 | Subsidiary []*Company `json:"company,omitempty" form:"company"` 33 | Organization map[string]string `json:"organization,omitempty" form:"organization"` 34 | User *User `json:"user,omitempty" form:"user"` 35 | } 36 | 37 | func TestQueryFields_Wildcard(t *testing.T) { 38 | fields := map[string]interface{}{"*": nil} 39 | result := QueryFields(User{}, fields) 40 | expected := "*" 41 | 42 | if result != expected { 43 | t.Fatalf("result should be %s. actual: %s", expected, result) 44 | } 45 | } 46 | 47 | func TestQueryFields_Primitive(t *testing.T) { 48 | fields := map[string]interface{}{"name": nil} 49 | result := QueryFields(User{}, fields) 50 | expected := "name" 51 | 52 | if result != expected { 53 | t.Fatalf("result should be %s. actual: %s", expected, result) 54 | } 55 | } 56 | 57 | func TestQueryFields_Multiple(t *testing.T) { 58 | fields := map[string]interface{}{"id": nil, "name": nil} 59 | result := QueryFields(User{}, fields) 60 | expected := "id,name" 61 | 62 | if result != expected { 63 | t.Fatalf("result should be %s. actual: %s", expected, result) 64 | } 65 | } 66 | 67 | func TestQueryFields_BelongsTo(t *testing.T) { 68 | fields := map[string]interface{}{"user": nil} 69 | result := QueryFields(Profile{}, fields) 70 | expected := "user_id" 71 | 72 | if result != expected { 73 | t.Fatalf("result should be %s. actual: %s", expected, result) 74 | } 75 | } 76 | 77 | func TestQueryFields_HasOne(t *testing.T) { 78 | fields := map[string]interface{}{"profile": nil} 79 | result := QueryFields(User{}, fields) 80 | expected := "id" 81 | 82 | if result != expected { 83 | t.Fatalf("result should be %s. actual: %s", expected, result) 84 | } 85 | } 86 | 87 | func TestQueryFields_HasMany(t *testing.T) { 88 | fields := map[string]interface{}{"jobs": nil} 89 | result := QueryFields(User{}, fields) 90 | expected := "id" 91 | 92 | if result != expected { 93 | t.Fatalf("result should be %s. actual: %s", expected, result) 94 | } 95 | } 96 | 97 | func TestParseFields_Wildcard(t *testing.T) { 98 | fields := "*" 99 | result := ParseFields(fields) 100 | 101 | if _, ok := result["*"]; !ok { 102 | t.Fatalf("result[*] should exist: %#v", result) 103 | } 104 | 105 | if result["*"] != nil { 106 | t.Fatalf("result[*] should be nil: %#v", result) 107 | } 108 | } 109 | 110 | func TestParseFields_Flat(t *testing.T) { 111 | fields := "profile" 112 | result := ParseFields(fields) 113 | 114 | if _, ok := result["profile"]; !ok { 115 | t.Fatalf("result[profile] should exist: %#v", result) 116 | } 117 | 118 | if result["profile"] != nil { 119 | t.Fatalf("result[profile] should be nil: %#v", result) 120 | } 121 | } 122 | 123 | func TestParseFields_Nested(t *testing.T) { 124 | fields := "profile.nation" 125 | result := ParseFields(fields) 126 | 127 | if _, ok := result["profile"]; !ok { 128 | t.Fatalf("result[profile] should exist: %#v", result) 129 | } 130 | 131 | if _, ok := result["profile"].(map[string]interface{}); !ok { 132 | t.Fatalf("result[profile] should be map: %#v", result) 133 | } 134 | 135 | if _, ok := result["profile"].(map[string]interface{})["nation"]; !ok { 136 | t.Fatalf("result[profile][nation] should exist: %#v", result) 137 | } 138 | 139 | if result["profile"].(map[string]interface{})["nation"] != nil { 140 | t.Fatalf("result[profile][nation] should be nil: %#v", result) 141 | } 142 | } 143 | 144 | func TestParseFields_NestedDeeply(t *testing.T) { 145 | fields := "profile.nation.name" 146 | result := ParseFields(fields) 147 | 148 | if _, ok := result["profile"]; !ok { 149 | t.Fatalf("result[profile] should exist: %#v", result) 150 | } 151 | 152 | if _, ok := result["profile"].(map[string]interface{}); !ok { 153 | t.Fatalf("result[profile] should be map: %#v", result) 154 | } 155 | 156 | if _, ok := result["profile"].(map[string]interface{})["nation"]; !ok { 157 | t.Fatalf("result[profile][nation] should exist: %#v", result) 158 | } 159 | 160 | if _, ok := result["profile"].(map[string]interface{})["nation"].(map[string]interface{}); !ok { 161 | t.Fatalf("result[profile][nation] should be map: %#v", result) 162 | } 163 | 164 | if _, ok := result["profile"].(map[string]interface{})["nation"].(map[string]interface{})["name"]; !ok { 165 | t.Fatalf("result[profile][nation][name] should exist: %#v", result) 166 | } 167 | 168 | if result["profile"].(map[string]interface{})["nation"].(map[string]interface{})["name"] != nil { 169 | t.Fatalf("result[profile][nation][name] should be nil: %#v", result) 170 | } 171 | } 172 | 173 | func TestParseFields_MultipleFields(t *testing.T) { 174 | fields := "profile.nation.name,emails" 175 | result := ParseFields(fields) 176 | 177 | if _, ok := result["profile"]; !ok { 178 | t.Fatalf("result[profile] should exist: %#v", result) 179 | } 180 | 181 | if _, ok := result["profile"].(map[string]interface{}); !ok { 182 | t.Fatalf("result[profile] should be map: %#v", result) 183 | } 184 | 185 | if _, ok := result["profile"].(map[string]interface{})["nation"]; !ok { 186 | t.Fatalf("result[profile][nation] should exist: %#v", result) 187 | } 188 | 189 | if _, ok := result["profile"].(map[string]interface{})["nation"].(map[string]interface{}); !ok { 190 | t.Fatalf("result[profile][nation] should be map: %#v", result) 191 | } 192 | 193 | if _, ok := result["profile"].(map[string]interface{})["nation"].(map[string]interface{})["name"]; !ok { 194 | t.Fatalf("result[profile][nation][name] should exist: %#v", result) 195 | } 196 | 197 | if result["profile"].(map[string]interface{})["nation"].(map[string]interface{})["name"] != nil { 198 | t.Fatalf("result[profile][nation][name] should be nil: %#v", result) 199 | } 200 | 201 | if _, ok := result["emails"]; !ok { 202 | t.Fatalf("result[emails] should exist: %#v", result) 203 | } 204 | 205 | if result["emails"] != nil { 206 | t.Fatalf("result[emails] should be map: %#v", result) 207 | } 208 | } 209 | 210 | func TestParseFields_Included(t *testing.T) { 211 | fields := "profile.nation.name,profile" 212 | result := ParseFields(fields) 213 | 214 | if _, ok := result["profile"]; !ok { 215 | t.Fatalf("result[profile] should exist: %#v", result) 216 | } 217 | 218 | if result["profile"] != nil { 219 | t.Fatalf("result[profile] should be nil: %#v", result) 220 | } 221 | } 222 | 223 | var profile = Profile{ 224 | ID: 1, 225 | UserID: 1, 226 | User: nil, 227 | Engaged: true, 228 | } 229 | 230 | var job = Job{ 231 | ID: 1, 232 | UserID: 1, 233 | User: nil, 234 | RoleCd: 1, 235 | } 236 | 237 | func TestFieldToMap_Wildcard(t *testing.T) { 238 | user := User{ 239 | ID: 1, 240 | Jobs: []*Job{&job}, 241 | Name: "Taro Yamada", 242 | Profile: &profile, 243 | } 244 | 245 | fields := map[string]interface{}{ 246 | "*": nil, 247 | } 248 | result, err := FieldToMap(user, fields) 249 | 250 | if err != nil { 251 | t.Fatalf("FieldToMap return an error. detail: %#v", err.Error()) 252 | } 253 | 254 | for _, key := range []string{"id", "jobs", "name", "profile"} { 255 | if _, ok := result[key]; !ok { 256 | t.Fatalf("%s should exist. actual: %#v", key, result) 257 | } 258 | } 259 | 260 | if result["jobs"].([]*Job) == nil { 261 | t.Fatalf("jobs should not be nil. actual: %#v", result["jobs"]) 262 | } 263 | 264 | if result["profile"].(*Profile) == nil { 265 | t.Fatalf("profile should not be nil. actual: %#v", result["profile"]) 266 | } 267 | } 268 | 269 | func TestFieldToMap_OmitEmpty(t *testing.T) { 270 | user := User{ 271 | ID: 1, 272 | Jobs: nil, 273 | Name: "Taro Yamada", 274 | Profile: nil, 275 | } 276 | 277 | fields := map[string]interface{}{ 278 | "*": nil, 279 | } 280 | result, err := FieldToMap(user, fields) 281 | 282 | if err != nil { 283 | t.Fatalf("FieldToMap return an error. detail: %#v", err.Error()) 284 | } 285 | 286 | for _, key := range []string{"id", "name"} { 287 | if _, ok := result[key]; !ok { 288 | t.Fatalf("%s should exist. actual: %#v", key, result) 289 | } 290 | } 291 | 292 | for _, key := range []string{"jobs", "profile"} { 293 | if _, ok := result[key]; ok { 294 | t.Fatalf("%s should not exist. actual: %#v", key, result) 295 | } 296 | } 297 | } 298 | 299 | func TestFieldToMap_OmitEmptyWithField(t *testing.T) { 300 | user := User{ 301 | ID: 1, 302 | Jobs: nil, 303 | Name: "Taro Yamada", 304 | Profile: nil, 305 | } 306 | 307 | fields := map[string]interface{}{ 308 | "id": nil, 309 | "name": nil, 310 | "jobs": nil, 311 | } 312 | result, err := FieldToMap(user, fields) 313 | 314 | if err != nil { 315 | t.Fatalf("FieldToMap return an error. detail: %#v", err.Error()) 316 | } 317 | 318 | for _, key := range []string{"id", "name", "jobs"} { 319 | if _, ok := result[key]; !ok { 320 | t.Fatalf("%s should exist. actual: %#v", key, result) 321 | } 322 | } 323 | 324 | for _, key := range []string{"profile"} { 325 | if _, ok := result[key]; ok { 326 | t.Fatalf("%s should not exist. actual: %#v", key, result) 327 | } 328 | } 329 | } 330 | 331 | func TestFieldToMap_OmitEmptyAllTypes(t *testing.T) { 332 | company := Company{ 333 | ID: 0, 334 | Name: "", 335 | List: false, 336 | Subsidiary: []*Company{}, 337 | Organization: make(map[string]string), 338 | User: nil, 339 | } 340 | 341 | fields := map[string]interface{}{ 342 | "*": nil, 343 | } 344 | result, err := FieldToMap(company, fields) 345 | 346 | if err != nil { 347 | t.Fatalf("FieldToMap return an error. detail: %#v", err.Error()) 348 | } 349 | 350 | for _, key := range []string{"id", "name", "list", "subsidiary", "organization", "user"} { 351 | if _, ok := result[key]; ok { 352 | t.Fatalf("%s should not exist. actual: %#v", key, result) 353 | } 354 | } 355 | } 356 | 357 | func TestFieldToMap_SpecifyField(t *testing.T) { 358 | user := User{ 359 | ID: 1, 360 | Jobs: nil, 361 | Name: "Taro Yamada", 362 | Profile: nil, 363 | } 364 | 365 | fields := map[string]interface{}{ 366 | "id": nil, 367 | "name": nil, 368 | } 369 | result, err := FieldToMap(user, fields) 370 | 371 | if err != nil { 372 | t.Fatalf("FieldToMap return an error. detail: %#v", err.Error()) 373 | } 374 | 375 | for _, key := range []string{"id", "name"} { 376 | if _, ok := result[key]; !ok { 377 | t.Fatalf("%s should exist. actual: %#v", key, result) 378 | } 379 | } 380 | 381 | for _, key := range []string{"jobs", "profile"} { 382 | if _, ok := result[key]; ok { 383 | t.Fatalf("%s should not exist. actual: %#v", key, result) 384 | } 385 | } 386 | } 387 | 388 | func TestFieldToMap_NestedField(t *testing.T) { 389 | user := User{ 390 | ID: 1, 391 | Jobs: []*Job{&job}, 392 | Name: "Taro Yamada", 393 | Profile: &profile, 394 | } 395 | 396 | fields := map[string]interface{}{ 397 | "profile": map[string]interface{}{ 398 | "id": nil, 399 | }, 400 | "name": nil, 401 | } 402 | result, err := FieldToMap(user, fields) 403 | 404 | if err != nil { 405 | t.Fatalf("FieldToMap return an error. detail: %#v", err.Error()) 406 | } 407 | 408 | for _, key := range []string{"name", "profile"} { 409 | if _, ok := result[key]; !ok { 410 | t.Fatalf("%s should exist. actual: %#v", key, result) 411 | } 412 | } 413 | 414 | for _, key := range []string{"id", "jobs"} { 415 | if _, ok := result[key]; ok { 416 | t.Fatalf("%s should not exist. actual: %#v", key, result) 417 | } 418 | } 419 | 420 | if result["profile"].(map[string]interface{}) == nil { 421 | t.Fatalf("profile should not be nil. actual: %#v", result) 422 | } 423 | 424 | if _, ok := result["profile"].(map[string]interface{})["id"]; !ok { 425 | t.Fatalf("profile.id should exist. actual: %#v", result) 426 | } 427 | 428 | for _, key := range []string{"id"} { 429 | if _, ok := result["profile"].(map[string]interface{})[key]; !ok { 430 | t.Fatalf("profile.%s should exist. actual: %#v", key, result) 431 | } 432 | } 433 | 434 | for _, key := range []string{"user_id", "user", "engaged"} { 435 | if _, ok := result["profile"].(map[string]interface{})[key]; ok { 436 | t.Fatalf("profile.%s should not exist. actual: %#v", key, result) 437 | } 438 | } 439 | } 440 | -------------------------------------------------------------------------------- /_templates/skeleton/main.go.tmpl: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "strconv" 6 | 7 | "{{ .VCS }}/{{ .User }}/{{ .Project }}/db" 8 | "{{ .VCS }}/{{ .User }}/{{ .Project }}/server" 9 | ) 10 | 11 | // main ... 12 | func main() { 13 | database := db.Connect() 14 | s := server.Setup(database) 15 | port := "8080" 16 | 17 | if p := os.Getenv("PORT"); p != "" { 18 | if _, err := strconv.Atoi(p); err == nil { 19 | port = p 20 | } 21 | } 22 | 23 | s.Run(":" + port) 24 | } 25 | -------------------------------------------------------------------------------- /_templates/skeleton/middleware/set_db.go.tmpl: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/jinzhu/gorm" 6 | ) 7 | 8 | func SetDBtoContext(db *gorm.DB) gin.HandlerFunc { 9 | return func(c *gin.Context) { 10 | c.Set("DB", db) 11 | c.Next() 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /_templates/skeleton/models/.gitkeep.tmpl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shimastripe/apig/6535437d01d50156cd00d29f8303437a13eafad5/_templates/skeleton/models/.gitkeep.tmpl -------------------------------------------------------------------------------- /_templates/skeleton/router/router.go.tmpl: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "{{ .VCS }}/{{ .User }}/{{ .Project }}/controllers" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | func Initialize(r *gin.Engine) { 10 | api := r.Group("{{ .Namespace }}") 11 | { 12 | //Auto Generate 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /_templates/skeleton/server/server.go.tmpl: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "{{ .VCS }}/{{ .User }}/{{ .Project }}/middleware" 5 | "{{ .VCS }}/{{ .User }}/{{ .Project }}/router" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/jinzhu/gorm" 9 | ) 10 | 11 | func Setup(db *gorm.DB) *gin.Engine { 12 | r := gin.Default() 13 | r.Use(middleware.SetDBtoContext(db)) 14 | router.Initialize(r) 15 | return r 16 | } 17 | -------------------------------------------------------------------------------- /_templates/skeleton/version/version.go.tmpl: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "math" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | func New(c *gin.Context) (string, error) { 12 | ver := "" 13 | header := c.Request.Header.Get("Accept") 14 | header = strings.Join(strings.Fields(header), "") 15 | 16 | if strings.Contains(header, "version=") { 17 | ver = strings.Split(strings.SplitAfter(header, "version=")[1], ";")[0] 18 | } 19 | 20 | if v := c.Query("v"); v != "" { 21 | ver = v 22 | } 23 | 24 | if ver == "" { 25 | return "-1", nil 26 | } 27 | 28 | _, err := strconv.Atoi(strings.Join(strings.Split(ver, "."), "")) 29 | if err != nil { 30 | return "", err 31 | } 32 | 33 | return ver, nil 34 | } 35 | 36 | func Range(left string, op string, right string) bool { 37 | switch op { 38 | case "<": 39 | return (compare(left, right) == -1) 40 | case "<=": 41 | return (compare(left, right) <= 0) 42 | case ">": 43 | return (compare(left, right) == 1) 44 | case ">=": 45 | return (compare(left, right) >= 0) 46 | case "==": 47 | return (compare(left, right) == 0) 48 | } 49 | 50 | return false 51 | } 52 | 53 | func compare(left string, right string) int { 54 | // l > r : 1 55 | // l == r : 0 56 | // l < r : -1 57 | 58 | if left == "-1" { 59 | return 1 60 | } else if right == "-1" { 61 | return -1 62 | } 63 | 64 | lArr := strings.Split(left, ".") 65 | rArr := strings.Split(right, ".") 66 | lItems := len(lArr) 67 | rItems := len(rArr) 68 | min := int(math.Min(float64(lItems), float64(rItems))) 69 | 70 | for i := 0; i < min; i++ { 71 | l, _ := strconv.Atoi(lArr[i]) 72 | r, _ := strconv.Atoi(rArr[i]) 73 | 74 | if l != r { 75 | if l > r { 76 | return 1 77 | } 78 | 79 | return -1 80 | } 81 | } 82 | 83 | if lItems == rItems { 84 | return 0 85 | } 86 | 87 | if lItems < rItems { 88 | return 1 89 | } 90 | 91 | return -1 92 | } 93 | -------------------------------------------------------------------------------- /_templates/skeleton/version/version_test.go.tmpl: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | func TestAcceptHeader(t *testing.T) { 11 | req, _ := http.NewRequest("GET", "/", nil) 12 | req.Header.Add("Accept", "application/json;version= 1.0.0 ; more information; more information") 13 | c := &gin.Context{ 14 | Request: req, 15 | } 16 | ver, _ := New(c) 17 | if ver != "1.0.0" { 18 | t.Errorf("Accept header should be `1.0.0`. actual: %#v", ver) 19 | } 20 | } 21 | 22 | func TestEmptyAcceptHeader(t *testing.T) { 23 | req, _ := http.NewRequest("GET", "/", nil) 24 | req.Header.Add("Accept", "application/json; more information; more information") 25 | c := &gin.Context{ 26 | Request: req, 27 | } 28 | ver, _ := New(c) 29 | if ver != "-1" { 30 | t.Errorf("Accept header should be the latest version `-1`. actual: %#v", ver) 31 | } 32 | } 33 | 34 | func TestUndefinedAcceptHeader(t *testing.T) { 35 | req, _ := http.NewRequest("GET", "/", nil) 36 | c := &gin.Context{ 37 | Request: req, 38 | } 39 | ver, _ := New(c) 40 | if ver != "-1" { 41 | t.Errorf("No accept header should be the latest version `-1`. actual: %#v", ver) 42 | } 43 | } 44 | 45 | func TestQuery(t *testing.T) { 46 | req, _ := http.NewRequest("GET", "/?v=1.0.1", nil) 47 | req.Header.Add("Accept", "application/json;version= 1.0.0 ; more information; more information") 48 | c := &gin.Context{ 49 | Request: req, 50 | } 51 | ver, _ := New(c) 52 | if ver != "1.0.1" { 53 | t.Errorf("URL Query should be `1.0.1`. actual: %#v", ver) 54 | } 55 | } 56 | 57 | func TestRange(t *testing.T) { 58 | 59 | if Range("1.2.3", "<", "0.9") { 60 | t.Errorf("defect in <") 61 | } 62 | 63 | if Range("1.2.3", "<", "0.9.1") { 64 | t.Errorf("defect in <") 65 | } 66 | 67 | if Range("1.2.3", "<", "1.2.2") { 68 | t.Errorf("defect in <") 69 | } 70 | 71 | if Range("1.2.3", "<", "1.2.3") { 72 | t.Errorf("defect in <") 73 | } 74 | 75 | if !Range("1.2.3", "<", "1.2.4") { 76 | t.Errorf("defect in <") 77 | } 78 | 79 | if !Range("1.2.3", "<", "1.2") { 80 | t.Errorf("defect in <") 81 | } 82 | 83 | if !Range("1.2.3", "<", "1.5") { 84 | t.Errorf("defect in <") 85 | } 86 | 87 | if !Range("1.2.3", "<", "-1") { 88 | t.Errorf("defect in <") 89 | } 90 | 91 | if Range("1.2.3", "<=", "0.9") { 92 | t.Errorf("defect in <=") 93 | } 94 | 95 | if Range("1.2.3", "<=", "0.9.1") { 96 | t.Errorf("defect in <=") 97 | } 98 | 99 | if Range("1.2.3", "<=", "1.2.2") { 100 | t.Errorf("defect in <=") 101 | } 102 | 103 | if !Range("1.2.3", "<=", "1.2.3") { 104 | t.Errorf("defect in <=") 105 | } 106 | 107 | if !Range("1.2.3", "<=", "1.2.4") { 108 | t.Errorf("defect in <=") 109 | } 110 | 111 | if !Range("1.2.3", "<=", "1.2") { 112 | t.Errorf("defect in <=") 113 | } 114 | 115 | if !Range("1.2.3", "<=", "1.5") { 116 | t.Errorf("defect in <=") 117 | } 118 | 119 | if !Range("1.2.3", "<=", "-1") { 120 | t.Errorf("defect in <=") 121 | } 122 | 123 | if !Range("1.2.3", ">", "0.9") { 124 | t.Errorf("defect in >") 125 | } 126 | 127 | if !Range("1.2.3", ">", "0.9.1") { 128 | t.Errorf("defect in >") 129 | } 130 | 131 | if !Range("1.2.3", ">", "1.2.2") { 132 | t.Errorf("defect in >") 133 | } 134 | 135 | if Range("1.2.3", ">", "1.2.3") { 136 | t.Errorf("defect in >") 137 | } 138 | 139 | if Range("1.2.3", ">", "1.2.4") { 140 | t.Errorf("defect in >") 141 | } 142 | 143 | if Range("1.2.3", ">", "1.2") { 144 | t.Errorf("defect in >") 145 | } 146 | 147 | if Range("1.2.3", ">", "1.5") { 148 | t.Errorf("defect in >") 149 | } 150 | 151 | if Range("1.2.3", ">", "-1") { 152 | t.Errorf("defect in >") 153 | } 154 | 155 | if !Range("1.2.3", ">=", "0.9") { 156 | t.Errorf("defect in >=") 157 | } 158 | 159 | if !Range("1.2.3", ">=", "0.9.1") { 160 | t.Errorf("defect in >=") 161 | } 162 | 163 | if !Range("1.2.3", ">=", "1.2.2") { 164 | t.Errorf("defect in >=") 165 | } 166 | 167 | if !Range("1.2.3", ">=", "1.2.3") { 168 | t.Errorf("defect in >=") 169 | } 170 | 171 | if Range("1.2.3", ">=", "1.2.4") { 172 | t.Errorf("defect in >=") 173 | } 174 | 175 | if Range("1.2.3", ">=", "1.2") { 176 | t.Errorf("defect in >=") 177 | } 178 | 179 | if Range("1.2.3", ">=", "1.5") { 180 | t.Errorf("defect in >=") 181 | } 182 | 183 | if Range("1.2.3", ">=", "-1") { 184 | t.Errorf("defect in >=") 185 | } 186 | 187 | if Range("1.2.3", "==", "0.9") { 188 | t.Errorf("defect in ==") 189 | } 190 | 191 | if Range("1.2.3", "==", "0.9.1") { 192 | t.Errorf("defect in ==") 193 | } 194 | 195 | if Range("1.2.3", "==", "1.2.2") { 196 | t.Errorf("defect in ==") 197 | } 198 | 199 | if !Range("1.2.3", "==", "1.2.3") { 200 | t.Errorf("defect in ==") 201 | } 202 | 203 | if Range("1.2.3", "==", "1.2.4") { 204 | t.Errorf("defect in ==") 205 | } 206 | 207 | if Range("1.2.3", "==", "1.2") { 208 | t.Errorf("defect in ==") 209 | } 210 | 211 | if Range("1.2.3", "==", "1.5") { 212 | t.Errorf("defect in ==") 213 | } 214 | 215 | if Range("1.2.3", "==", "-1") { 216 | t.Errorf("defect in ==") 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /apig/associate.go: -------------------------------------------------------------------------------- 1 | package apig 2 | 3 | import "strings" 4 | 5 | func resolveAssociate(model *Model, modelMap map[string]*Model, parents map[string]bool) { 6 | parents[model.Name] = true 7 | 8 | for i, field := range model.Fields { 9 | if field.Association != nil && field.Association.Type != AssociationNone { 10 | continue 11 | } 12 | 13 | str := strings.Trim(field.Type, "[]*") 14 | if modelMap[str] != nil && !parents[str] { 15 | resolveAssociate(modelMap[str], modelMap, parents) 16 | 17 | var assoc int 18 | switch string([]rune(field.Type)[0]) { 19 | case "[": 20 | if validateForeignKey(modelMap[str].Fields, model.Name) { 21 | assoc = AssociationHasMany 22 | break 23 | } 24 | assoc = AssociationBelongsTo 25 | default: 26 | if validateForeignKey(modelMap[str].Fields, model.Name) { 27 | assoc = AssociationHasOne 28 | break 29 | } 30 | assoc = AssociationBelongsTo 31 | } 32 | model.Fields[i].Association = &Association{Type: assoc, Model: modelMap[str]} 33 | } else { 34 | model.Fields[i].Association = &Association{Type: AssociationNone} 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /apig/associate_test.go: -------------------------------------------------------------------------------- 1 | package apig 2 | 3 | import "testing" 4 | 5 | func TestResolveAssociate(t *testing.T) { 6 | user := &Model{ 7 | Name: "User", 8 | Fields: []*Field{ 9 | &Field{ 10 | Name: "ID", 11 | Type: "uint", 12 | }, 13 | &Field{ 14 | Name: "Profile", 15 | Type: "*Profile", 16 | }, 17 | &Field{ 18 | Name: "ProfileID", 19 | Type: "uint", 20 | }, 21 | }, 22 | } 23 | 24 | profile := &Model{ 25 | Name: "Profile", 26 | Fields: []*Field{ 27 | &Field{ 28 | Name: "ID", 29 | Type: "uint", 30 | }, 31 | &Field{ 32 | Name: "Name", 33 | Type: "string", 34 | }, 35 | &Field{ 36 | Name: "User", 37 | Type: "*User", 38 | }, 39 | }, 40 | } 41 | 42 | modelMap := modelToMap(user, profile) 43 | 44 | if len(modelMap) != 2 { 45 | t.Fatalf("Number of models map is incorrect. expected: 2, actual: %d", len(modelMap)) 46 | } 47 | 48 | resolveAssociate(user, modelMap, make(map[string]bool)) 49 | 50 | // Profile 51 | result := user.Fields[1].Association.Type 52 | expect := AssociationBelongsTo 53 | if result != expect { 54 | t.Fatalf("Incorrect result. expected: %v, actual: %v", expect, result) 55 | } 56 | } 57 | 58 | func modelToMap(models ...*Model) map[string]*Model { 59 | modelMap := make(map[string]*Model) 60 | for _, model := range models { 61 | modelMap[model.Name] = model 62 | } 63 | return modelMap 64 | } 65 | -------------------------------------------------------------------------------- /apig/detail.go: -------------------------------------------------------------------------------- 1 | package apig 2 | 3 | type Detail struct { 4 | VCS string 5 | User string 6 | Project string 7 | Namespace string 8 | Models Models 9 | Model *Model 10 | ImportDir string 11 | Database string 12 | } 13 | -------------------------------------------------------------------------------- /apig/generate_test.go: -------------------------------------------------------------------------------- 1 | package apig 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | "testing" 9 | 10 | "github.com/shimastripe/apig/msg" 11 | ) 12 | 13 | var userModel = &Model{ 14 | Name: "User", 15 | Fields: []*Field{ 16 | &Field{ 17 | Name: "ID", 18 | JSONName: "id", 19 | Type: "uint", 20 | Tag: "", 21 | Association: nil, 22 | }, 23 | &Field{ 24 | Name: "Name", 25 | JSONName: "name", 26 | Type: "string", 27 | Tag: "", 28 | Association: nil, 29 | }, 30 | &Field{ 31 | Name: "CreatedAt", 32 | JSONName: "created_at", 33 | Type: "*time.Time", 34 | Tag: "", 35 | Association: nil, 36 | }, 37 | &Field{ 38 | Name: "UpdatedAt", 39 | JSONName: "updated_at", 40 | Type: "*time.Time", 41 | Tag: "", 42 | Association: nil, 43 | }, 44 | }, 45 | } 46 | 47 | var detail = &Detail{ 48 | VCS: "github.com", 49 | User: "shimastripe", 50 | Project: "api-server", 51 | Model: userModel, 52 | Models: []*Model{userModel}, 53 | ImportDir: "github.com/shimastripe/api-server", 54 | Namespace: "", 55 | } 56 | 57 | func compareFiles(f1, f2 string) bool { 58 | c1, _ := ioutil.ReadFile(f1) 59 | c2, _ := ioutil.ReadFile(f2) 60 | 61 | return bytes.Compare(c1, c2) == 0 62 | } 63 | 64 | func setup() { 65 | msg.Mute = true 66 | } 67 | 68 | func teardown() { 69 | msg.Mute = false 70 | } 71 | 72 | func TestMain(m *testing.M) { 73 | setup() 74 | code := m.Run() 75 | teardown() 76 | os.Exit(code) 77 | } 78 | 79 | func TestGenerateApibIndex(t *testing.T) { 80 | outDir, err := ioutil.TempDir("", "generateApibIndex") 81 | if err != nil { 82 | t.Fatal("Failed to create tempdir") 83 | } 84 | defer os.RemoveAll(outDir) 85 | 86 | if err := generateApibIndex(detail, outDir); err != nil { 87 | t.Fatalf("Error should not be raised: %s", err) 88 | } 89 | 90 | path := filepath.Join(outDir, "docs", "index.apib") 91 | _, err = os.Stat(path) 92 | if err != nil { 93 | t.Fatalf("API Blueprint index is not generated: %s", path) 94 | } 95 | 96 | fixture := filepath.Join("testdata", "docs", "index.apib") 97 | 98 | if !compareFiles(path, fixture) { 99 | c1, _ := ioutil.ReadFile(fixture) 100 | c2, _ := ioutil.ReadFile(path) 101 | t.Fatalf("Failed to generate API Blueprint index correctly.\nexpected:\n%s\nactual:\n%s", string(c1), string(c2)) 102 | } 103 | } 104 | 105 | func TestGenerateApibModel(t *testing.T) { 106 | outDir, err := ioutil.TempDir("", "generateApibModel") 107 | if err != nil { 108 | t.Fatal("Failed to create tempdir") 109 | } 110 | defer os.RemoveAll(outDir) 111 | 112 | if err := generateApibModel(detail, outDir); err != nil { 113 | t.Fatalf("Error should not be raised: %s", err) 114 | } 115 | 116 | path := filepath.Join(outDir, "docs", "user.apib") 117 | _, err = os.Stat(path) 118 | if err != nil { 119 | t.Fatalf("API Blueprint model is not generated: %s", path) 120 | } 121 | 122 | fixture := filepath.Join("testdata", "docs", "user.apib") 123 | 124 | if !compareFiles(path, fixture) { 125 | c1, _ := ioutil.ReadFile(fixture) 126 | c2, _ := ioutil.ReadFile(path) 127 | t.Fatalf("Failed to generate API Blueprint model correctly.\nexpected:\n%s\nactual:\n%s", string(c1), string(c2)) 128 | } 129 | } 130 | 131 | func TestGenerateController(t *testing.T) { 132 | outDir, err := ioutil.TempDir("", "generateController") 133 | if err != nil { 134 | t.Fatal("Failed to create tempdir") 135 | } 136 | defer os.RemoveAll(outDir) 137 | 138 | if err := generateController(detail, outDir); err != nil { 139 | t.Fatalf("Error should not be raised: %s", err) 140 | } 141 | 142 | path := filepath.Join(outDir, "controllers", "user.go") 143 | _, err = os.Stat(path) 144 | if err != nil { 145 | t.Fatalf("Controller file is not generated: %s", path) 146 | } 147 | 148 | fixture := filepath.Join("testdata", "controllers", "user.go") 149 | 150 | if !compareFiles(path, fixture) { 151 | c1, _ := ioutil.ReadFile(fixture) 152 | c2, _ := ioutil.ReadFile(path) 153 | t.Fatalf("Failed to generate controller correctly.\nexpected:\n%s\nactual:\n%s", string(c1), string(c2)) 154 | } 155 | } 156 | 157 | func TestGenerateRootController(t *testing.T) { 158 | outDir, err := ioutil.TempDir("", "generateRootController") 159 | if err != nil { 160 | t.Fatal("Failed to create tempdir") 161 | } 162 | defer os.RemoveAll(outDir) 163 | 164 | if err := generateRootController(detail, outDir); err != nil { 165 | t.Fatalf("Error should not be raised: %s", err) 166 | } 167 | 168 | path := filepath.Join(outDir, "controllers", "root.go") 169 | _, err = os.Stat(path) 170 | if err != nil { 171 | t.Fatalf("Controller file is not generated: %s", path) 172 | } 173 | 174 | fixture := filepath.Join("testdata", "controllers", "root.go") 175 | 176 | if !compareFiles(path, fixture) { 177 | c1, _ := ioutil.ReadFile(fixture) 178 | c2, _ := ioutil.ReadFile(path) 179 | t.Fatalf("Failed to generate controller correctly.\nexpected:\n%s\nactual:\n%s", string(c1), string(c2)) 180 | } 181 | } 182 | 183 | func TestGenerateREADME(t *testing.T) { 184 | outDir, err := ioutil.TempDir("", "generateREADME") 185 | if err != nil { 186 | t.Fatal("Failed to create tempdir") 187 | } 188 | defer os.RemoveAll(outDir) 189 | 190 | if err := generateREADME(detail, outDir); err != nil { 191 | t.Fatalf("Error should not be raised: %s", err) 192 | } 193 | 194 | path := filepath.Join(outDir, "README.md") 195 | _, err = os.Stat(path) 196 | if err != nil { 197 | t.Fatalf("README is not generated: %s", path) 198 | } 199 | 200 | fixture := filepath.Join("testdata", "README.md") 201 | 202 | if !compareFiles(path, fixture) { 203 | c1, _ := ioutil.ReadFile(fixture) 204 | c2, _ := ioutil.ReadFile(path) 205 | t.Fatalf("Failed to generate README correctly.\nexpected:\n%s\nactual:\n%s", string(c1), string(c2)) 206 | } 207 | } 208 | 209 | func TestGenerateRouter(t *testing.T) { 210 | outDir, err := ioutil.TempDir("", "generateRouter") 211 | if err != nil { 212 | t.Fatal("Failed to create tempdir") 213 | } 214 | defer os.RemoveAll(outDir) 215 | 216 | if err := generateRouter(detail, outDir); err != nil { 217 | t.Fatalf("Error should not be raised: %s", err) 218 | } 219 | 220 | path := filepath.Join(outDir, "router", "router.go") 221 | _, err = os.Stat(path) 222 | if err != nil { 223 | t.Fatalf("Router file is not generated: %s", path) 224 | } 225 | 226 | fixture := filepath.Join("testdata", "router", "router.go") 227 | 228 | if !compareFiles(path, fixture) { 229 | c1, _ := ioutil.ReadFile(fixture) 230 | c2, _ := ioutil.ReadFile(path) 231 | t.Fatalf("Failed to generate router correctly.\nexpected:\n%s\nactual:\n%s", string(c1), string(c2)) 232 | } 233 | } 234 | 235 | func TestGenerateDBSQLite(t *testing.T) { 236 | detail.Database = "sqlite" 237 | 238 | outDir, err := ioutil.TempDir("", "generateDBSQLite") 239 | if err != nil { 240 | t.Fatal("Failed to create tempdir") 241 | } 242 | defer os.RemoveAll(outDir) 243 | 244 | if err := generateDB(detail, outDir); err != nil { 245 | t.Fatalf("Error should not be raised: %s", err) 246 | } 247 | 248 | path := filepath.Join(outDir, "db", "db.go") 249 | _, err = os.Stat(path) 250 | if err != nil { 251 | t.Fatalf("db.go is not generated: %s", path) 252 | } 253 | 254 | fixture := filepath.Join("testdata", "db", "db_sqlite.go") 255 | 256 | if !compareFiles(path, fixture) { 257 | c1, _ := ioutil.ReadFile(fixture) 258 | c2, _ := ioutil.ReadFile(path) 259 | t.Fatalf("Failed to generate db.go correctly.\nexpected:\n%s\nactual:\n%s", string(c1), string(c2)) 260 | } 261 | } 262 | 263 | func TestGenerateDBPostgres(t *testing.T) { 264 | detail.Database = "postgres" 265 | 266 | outDir, err := ioutil.TempDir("", "generateDBPostgres") 267 | if err != nil { 268 | t.Fatal("Failed to create tempdir") 269 | } 270 | defer os.RemoveAll(outDir) 271 | 272 | if err := generateDB(detail, outDir); err != nil { 273 | t.Fatalf("Error should not be raised: %s", err) 274 | } 275 | 276 | path := filepath.Join(outDir, "db", "db.go") 277 | _, err = os.Stat(path) 278 | if err != nil { 279 | t.Fatalf("db.go is not generated: %s", path) 280 | } 281 | 282 | fixture := filepath.Join("testdata", "db", "db_postgres.go") 283 | 284 | if !compareFiles(path, fixture) { 285 | c1, _ := ioutil.ReadFile(fixture) 286 | c2, _ := ioutil.ReadFile(path) 287 | t.Fatalf("Failed to generate db.go correctly.\nexpected:\n%s\nactual:\n%s", string(c1), string(c2)) 288 | } 289 | } 290 | 291 | func TestGenerateDBMysql(t *testing.T) { 292 | detail.Database = "mysql" 293 | 294 | outDir, err := ioutil.TempDir("", "generateDBMysql") 295 | if err != nil { 296 | t.Fatal("Failed to create tempdir") 297 | } 298 | defer os.RemoveAll(outDir) 299 | 300 | if err := generateDB(detail, outDir); err != nil { 301 | t.Fatalf("Error should not be raised: %s", err) 302 | } 303 | 304 | path := filepath.Join(outDir, "db", "db.go") 305 | _, err = os.Stat(path) 306 | if err != nil { 307 | t.Fatalf("db.go is not generated: %s", path) 308 | } 309 | 310 | fixture := filepath.Join("testdata", "db", "db_mysql.go") 311 | 312 | if !compareFiles(path, fixture) { 313 | c1, _ := ioutil.ReadFile(fixture) 314 | c2, _ := ioutil.ReadFile(path) 315 | t.Fatalf("Failed to generate db.go correctly.\nexpected:\n%s\nactual:\n%s", string(c1), string(c2)) 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /apig/import.go: -------------------------------------------------------------------------------- 1 | package apig 2 | 3 | import "path/filepath" 4 | 5 | func formatImportDir(paths []string) []string { 6 | results := make([]string, 0, len(paths)) 7 | flag := map[string]bool{} 8 | for i := 0; i < len(paths); i++ { 9 | dir := filepath.Dir(paths[i]) 10 | if !flag[dir] && dir != "." { 11 | flag[dir] = true 12 | results = append(results, dir) 13 | } 14 | } 15 | return results 16 | } 17 | -------------------------------------------------------------------------------- /apig/import_test.go: -------------------------------------------------------------------------------- 1 | package apig 2 | 3 | import "testing" 4 | 5 | func TestFormatImportDir(t *testing.T) { 6 | importPaths := generateImportSlice( 7 | "github.com/shimastripe/api-server/db", 8 | "github.com/shimastripe/api-server/models", 9 | "github.com/shimastripe/api-server/server", 10 | "fmt", 11 | ) 12 | 13 | result := formatImportDir(importPaths) 14 | if len(result) != 1 { 15 | t.Fatalf("Number of import dir is incorrect. expected: 1, actual: %d", len(result)) 16 | } 17 | 18 | expect := "github.com/shimastripe/api-server" 19 | if result[0] != expect { 20 | t.Fatalf("Incorrect import dir. expected: %s, actual: %s", expect, result[0]) 21 | } 22 | } 23 | 24 | func generateImportSlice(paths ...string) []string { 25 | var importPaths []string 26 | for _, path := range paths { 27 | importPaths = append(importPaths, path) 28 | } 29 | return importPaths 30 | } 31 | -------------------------------------------------------------------------------- /apig/model.go: -------------------------------------------------------------------------------- 1 | package apig 2 | 3 | const ( 4 | AssociationNone = 0 5 | AssociationBelongsTo = 1 6 | AssociationHasMany = 2 7 | AssociationHasOne = 3 8 | ) 9 | 10 | type Model struct { 11 | Name string 12 | Fields []*Field 13 | } 14 | 15 | func (m *Model) AllPreloadAssocs() []string { 16 | result := []string{} 17 | 18 | for _, field := range m.Fields { 19 | result = append(result, field.PreloadAssocs()...) 20 | } 21 | 22 | return result 23 | } 24 | 25 | type Models []*Model // implements Sort interface 26 | 27 | func (m Models) Len() int { 28 | return len(m) 29 | } 30 | 31 | func (m Models) Less(i, j int) bool { 32 | return m[i].Name < m[j].Name 33 | } 34 | 35 | func (m Models) Swap(i, j int) { 36 | m[i], m[j] = m[j], m[i] 37 | } 38 | 39 | type Field struct { 40 | Name string 41 | JSONName string 42 | Type string 43 | Tag string 44 | Association *Association 45 | } 46 | 47 | func (f *Field) PreloadAssocs() []string { 48 | if f.Association == nil || f.Association.Type == AssociationNone { 49 | return []string{} 50 | } 51 | 52 | result := []string{ 53 | f.Name, 54 | } 55 | 56 | for _, field := range f.Association.Model.Fields { 57 | if field.Association == nil || field.Association.Type == AssociationNone { 58 | continue 59 | } 60 | 61 | result = append(result, f.Name+"."+field.Name) 62 | } 63 | 64 | return result 65 | } 66 | 67 | func (f *Field) IsAssociation() bool { 68 | return f.Association != nil && f.Association.Type != AssociationNone 69 | } 70 | 71 | func (f *Field) IsBelongsTo() bool { 72 | return f.Association != nil && f.Association.Type == AssociationBelongsTo 73 | } 74 | 75 | type Association struct { 76 | Type int 77 | Model *Model 78 | } 79 | -------------------------------------------------------------------------------- /apig/parse.go: -------------------------------------------------------------------------------- 1 | package apig 2 | 3 | import ( 4 | "errors" 5 | "go/ast" 6 | "go/parser" 7 | "go/token" 8 | "reflect" 9 | "strconv" 10 | "strings" 11 | ) 12 | 13 | func parseField(field *ast.Field) (*Field, error) { 14 | if len(field.Names) != 1 { 15 | return nil, errors.New("Failed to read model files. Please fix struct.") 16 | } 17 | 18 | fieldName := field.Names[0].Name 19 | 20 | var fieldType string 21 | 22 | switch x := field.Type.(type) { 23 | case *ast.Ident: // e.g. string 24 | fieldType = x.Name 25 | 26 | case *ast.SelectorExpr: // e.g. time.Time, sql.NullString 27 | switch x2 := x.X.(type) { 28 | case *ast.Ident: 29 | fieldType = x2.Name + "." + x.Sel.Name 30 | } 31 | 32 | case *ast.ArrayType: // e.g. []Email 33 | switch x2 := x.Elt.(type) { 34 | case *ast.Ident: 35 | fieldType = "[]" + x2.Name 36 | 37 | case *ast.StarExpr: // e.g. []*Email 38 | switch x3 := x2.X.(type) { 39 | case *ast.Ident: 40 | fieldType = "[]*" + x3.Name 41 | } 42 | } 43 | 44 | case *ast.StarExpr: 45 | switch x2 := x.X.(type) { 46 | case *ast.Ident: // e.g. *Profile 47 | fieldType = "*" + x2.Name 48 | 49 | case *ast.SelectorExpr: // e.g. *time.Time 50 | switch x3 := x2.X.(type) { 51 | case *ast.Ident: 52 | fieldType = "*" + x3.Name + "." + x2.Sel.Name 53 | } 54 | } 55 | } 56 | 57 | var jsonName string 58 | var fieldTag string 59 | 60 | if field.Tag == nil { 61 | jsonName = fieldName 62 | fieldTag = "" 63 | } else { 64 | s, err := strconv.Unquote(field.Tag.Value) 65 | 66 | if err != nil { 67 | s = field.Tag.Value 68 | } 69 | 70 | jsonName = strings.Split((reflect.StructTag)(s).Get("json"), ",")[0] 71 | fieldTag = field.Tag.Value 72 | } 73 | 74 | fs := Field{ 75 | Name: fieldName, 76 | JSONName: jsonName, 77 | Type: fieldType, 78 | Tag: fieldTag, 79 | } 80 | 81 | return &fs, nil 82 | } 83 | 84 | func parseModel(path string) ([]*Model, error) { 85 | fset := token.NewFileSet() 86 | f, err := parser.ParseFile(fset, path, nil, 0) 87 | 88 | if err != nil { 89 | return nil, err 90 | } 91 | 92 | models := []*Model{} 93 | 94 | ast.Inspect(f, func(node ast.Node) bool { 95 | switch x := node.(type) { 96 | case *ast.GenDecl: 97 | if x.Tok != token.TYPE { 98 | break 99 | } 100 | 101 | for _, spec := range x.Specs { 102 | var fields []*Field 103 | 104 | var modelName string 105 | 106 | switch x2 := spec.(type) { 107 | case *ast.TypeSpec: 108 | modelName = x2.Name.Name 109 | 110 | switch x3 := x2.Type.(type) { 111 | case *ast.StructType: 112 | for _, field := range x3.Fields.List { 113 | fs, err := parseField(field) 114 | 115 | if err != nil { 116 | return false 117 | } 118 | 119 | fields = append(fields, fs) 120 | } 121 | } 122 | 123 | models = append(models, &Model{ 124 | Name: modelName, 125 | Fields: fields, 126 | }) 127 | } 128 | } 129 | } 130 | 131 | return true 132 | }) 133 | 134 | return models, nil 135 | } 136 | 137 | func parseImport(path string) ([]string, error) { 138 | fset := token.NewFileSet() 139 | f, err := parser.ParseFile(fset, path, nil, 0) 140 | 141 | if err != nil { 142 | return nil, err 143 | } 144 | 145 | var importPaths []string 146 | 147 | ast.Inspect(f, func(node ast.Node) bool { 148 | switch x := node.(type) { 149 | case *ast.GenDecl: 150 | if x.Tok != token.IMPORT { 151 | break 152 | } 153 | 154 | for _, spec := range x.Specs { 155 | switch x2 := spec.(type) { 156 | case *ast.ImportSpec: 157 | importPaths = append(importPaths, strings.Trim(x2.Path.Value, "\"")) 158 | } 159 | } 160 | } 161 | return true 162 | }) 163 | return importPaths, nil 164 | } 165 | 166 | func parseNamespace(path string) (string, error) { 167 | fset := token.NewFileSet() 168 | f, err := parser.ParseFile(fset, path, nil, 0) 169 | 170 | if err != nil { 171 | return "", err 172 | } 173 | 174 | var namespace string 175 | 176 | for _, decl := range f.Decls { 177 | ast.Inspect(decl, func(node ast.Node) bool { 178 | fn, ok := node.(*ast.FuncDecl) 179 | if !ok { 180 | return false 181 | } 182 | 183 | if fn.Name.Name != "Initialize" { 184 | return false 185 | } 186 | 187 | for _, stmt := range fn.Body.List { 188 | assign, ok := stmt.(*ast.AssignStmt) 189 | if !ok { 190 | continue 191 | } 192 | 193 | for _, expr := range assign.Rhs { 194 | call, ok := expr.(*ast.CallExpr) 195 | if !ok { 196 | continue 197 | } 198 | 199 | for _, arg := range call.Args { 200 | lit, ok := arg.(*ast.BasicLit) 201 | if !ok { 202 | continue 203 | } 204 | 205 | namespace, err = strconv.Unquote(lit.Value) 206 | if err != nil { 207 | continue 208 | } 209 | } 210 | } 211 | } 212 | 213 | return true 214 | }) 215 | } 216 | 217 | return namespace, nil 218 | } 219 | -------------------------------------------------------------------------------- /apig/parse_test.go: -------------------------------------------------------------------------------- 1 | package apig 2 | 3 | import ( 4 | "path/filepath" 5 | "testing" 6 | ) 7 | 8 | func fieldEquals(f1, f2 *Field) bool { 9 | if f1.Name != f2.Name { 10 | return false 11 | } 12 | 13 | if f1.JSONName != f2.JSONName { 14 | return false 15 | } 16 | 17 | if f1.Type != f2.Type { 18 | return false 19 | } 20 | 21 | return true 22 | } 23 | 24 | func TestParseModel(t *testing.T) { 25 | path := filepath.Join("testdata", "parse", "models.go") 26 | 27 | models, err := parseModel(path) 28 | 29 | if err != nil { 30 | t.Fatalf("Failed to parse model file. error: %s", err) 31 | } 32 | 33 | if len(models) != 2 { 34 | t.Fatalf("Number of parsed models is incorrect. expected: 2, actual: %d", len(models)) 35 | } 36 | 37 | user := models[0] 38 | 39 | if user.Name != "User" { 40 | t.Fatalf("Incorrect model name. expected: User, actual: %s", user.Name) 41 | } 42 | 43 | expectedFields := []*Field{ 44 | &Field{ 45 | Name: "ID", 46 | JSONName: "id", 47 | Type: "uint", 48 | }, 49 | &Field{ 50 | Name: "Name", 51 | JSONName: "name", 52 | Type: "string", 53 | }, 54 | &Field{ 55 | Name: "CreatedAt", 56 | JSONName: "created_at", 57 | Type: "*time.Time", 58 | }, 59 | &Field{ 60 | Name: "UpdatedAt", 61 | JSONName: "UpdatedAt", 62 | Type: "*time.Time", 63 | }, 64 | } 65 | 66 | for i, actual := range user.Fields { 67 | if !fieldEquals(expectedFields[i], actual) { 68 | t.Fatalf("Incorrect field. expected: %#v, actual: %#v", expectedFields[i], actual) 69 | } 70 | } 71 | } 72 | 73 | func TestParseImport(t *testing.T) { 74 | path := filepath.Join("testdata", "parse", "router.go") 75 | 76 | importPaths, err := parseImport(path) 77 | 78 | if err != nil { 79 | t.Fatalf("Failed to parse file. error: %s", err) 80 | } 81 | 82 | if len(importPaths) != 2 { 83 | t.Fatalf("Number of parsed import paths is incorrect. expected: 2, actual: %d", len(importPaths)) 84 | } 85 | 86 | importPath := importPaths[0] 87 | expect := "github.com/shimastripe/api-server/controllers" 88 | if importPath != expect { 89 | t.Fatalf("Incorrect import path. expected: %s, actual: %s", expect, importPath) 90 | } 91 | } 92 | 93 | func TestParseNamespace(t *testing.T) { 94 | path := filepath.Join("testdata", "parse", "router.go") 95 | 96 | namespace, err := parseNamespace(path) 97 | if err != nil { 98 | t.Fatalf("Failed to parse router. error: %s", err) 99 | } 100 | 101 | expected := "api" 102 | 103 | if namespace != expected { 104 | t.Fatalf("Incorrect namespace. expected: %s, actual: %s", expected, namespace) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /apig/skeleton.go: -------------------------------------------------------------------------------- 1 | package apig 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "go/format" 7 | "io/ioutil" 8 | "os" 9 | "path/filepath" 10 | "regexp" 11 | "strings" 12 | "sync" 13 | "text/template" 14 | 15 | "github.com/shimastripe/apig/msg" 16 | "github.com/shimastripe/apig/util" 17 | ) 18 | 19 | var r = regexp.MustCompile(`_templates/skeleton/.*\.tmpl$`) 20 | 21 | func generateSkeleton(detail *Detail, outDir string) error { 22 | var wg sync.WaitGroup 23 | errCh := make(chan error, 1) 24 | done := make(chan bool, 1) 25 | 26 | for _, skeleton := range AssetNames() { 27 | wg.Add(1) 28 | go func(s string) { 29 | defer wg.Done() 30 | 31 | if !r.MatchString(s) { 32 | return 33 | } 34 | 35 | trim := strings.Replace(s, "_templates/skeleton/", "", 1) 36 | path := strings.Replace(trim, ".tmpl", "", 1) 37 | dstPath := filepath.Join(outDir, path) 38 | 39 | body, err := Asset(s) 40 | if err != nil { 41 | errCh <- err 42 | } 43 | 44 | tmpl, err := template.New("complex").Parse(string(body)) 45 | if err != nil { 46 | errCh <- err 47 | } 48 | 49 | var buf bytes.Buffer 50 | var src []byte 51 | 52 | if err := tmpl.Execute(&buf, detail); err != nil { 53 | errCh <- err 54 | } 55 | 56 | if strings.HasSuffix(path, ".go") { 57 | src, err = format.Source(buf.Bytes()) 58 | if err != nil { 59 | errCh <- err 60 | } 61 | } else { 62 | src = buf.Bytes() 63 | } 64 | 65 | if !util.FileExists(filepath.Dir(dstPath)) { 66 | if err := util.Mkdir(filepath.Dir(dstPath)); err != nil { 67 | errCh <- err 68 | } 69 | } 70 | 71 | if err := ioutil.WriteFile(dstPath, src, 0644); err != nil { 72 | errCh <- err 73 | } 74 | 75 | msg.Printf("\t\x1b[32m%s\x1b[0m %s\n", "create", dstPath) 76 | }(skeleton) 77 | } 78 | 79 | wg.Wait() 80 | close(done) 81 | 82 | select { 83 | case <-done: 84 | case err := <-errCh: 85 | if err != nil { 86 | return err 87 | } 88 | } 89 | 90 | return nil 91 | } 92 | 93 | func Skeleton(gopath, vcs, username, project, namespace, database string) int { 94 | detail := &Detail{ 95 | VCS: vcs, 96 | User: username, 97 | Project: project, 98 | Namespace: namespace, 99 | Database: database, 100 | } 101 | outDir := filepath.Join(gopath, "src", detail.VCS, detail.User, detail.Project) 102 | if util.FileExists(outDir) { 103 | fmt.Fprintf(os.Stderr, "%s is already exists", outDir) 104 | return 1 105 | } 106 | if err := generateSkeleton(detail, outDir); err != nil { 107 | fmt.Fprintln(os.Stderr, err) 108 | return 1 109 | } 110 | 111 | msg.Printf("===> Created %s", outDir) 112 | return 0 113 | } 114 | -------------------------------------------------------------------------------- /apig/skeleton_test.go: -------------------------------------------------------------------------------- 1 | package apig 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | ) 9 | 10 | func TestGenerateSkeleton(t *testing.T) { 11 | tempDir, err := ioutil.TempDir("", "copyStaticFiles") 12 | if err != nil { 13 | t.Fatal("Failed to create tempdir") 14 | } 15 | defer os.RemoveAll(tempDir) 16 | 17 | outDir := filepath.Join(tempDir, "api-server") 18 | 19 | if err := generateSkeleton(detail, outDir); err != nil { 20 | t.Fatalf("Error should not be raised: %s", err) 21 | } 22 | 23 | files := []string{ 24 | "README.md", 25 | ".gitignore", 26 | "main.go", 27 | filepath.Join("db", "db.go"), 28 | filepath.Join("db", "pagination.go"), 29 | filepath.Join("router", "router.go"), 30 | filepath.Join("middleware", "set_db.go"), 31 | filepath.Join("server", "server.go"), 32 | filepath.Join("helper", "field.go"), 33 | filepath.Join("helper", "field_test.go"), 34 | filepath.Join("version", "version.go"), 35 | filepath.Join("version", "version_test.go"), 36 | filepath.Join("controllers", ".gitkeep"), 37 | filepath.Join("models", ".gitkeep"), 38 | } 39 | 40 | for _, file := range files { 41 | _, err := os.Stat(filepath.Join(outDir, file)) 42 | if err != nil { 43 | t.Fatalf("Static file is not copied: %s", file) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /apig/testdata/README.md: -------------------------------------------------------------------------------- 1 | # API Server 2 | 3 | Simple Rest API using gin(framework) & gorm(orm) 4 | 5 | ## Endpoint list 6 | 7 | ### Users Resource 8 | 9 | ``` 10 | GET /users 11 | GET /users/:id 12 | POST /users 13 | PUT /users/:id 14 | DELETE /users/:id 15 | ``` 16 | 17 | server runs at http://localhost:8080 18 | -------------------------------------------------------------------------------- /apig/testdata/controllers/root.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | func APIEndpoints(c *gin.Context) { 11 | reqScheme := "http" 12 | 13 | if c.Request.TLS != nil { 14 | reqScheme = "https" 15 | } 16 | 17 | reqHost := c.Request.Host 18 | baseURL := fmt.Sprintf("%s://%s", reqScheme, reqHost) 19 | 20 | resources := map[string]string{ 21 | "users_url": baseURL + "/users", 22 | "user_url": baseURL + "/users/{id}", 23 | } 24 | 25 | c.IndentedJSON(http.StatusOK, resources) 26 | } 27 | -------------------------------------------------------------------------------- /apig/testdata/controllers/user.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | dbpkg "github.com/shimastripe/api-server/db" 8 | "github.com/shimastripe/api-server/helper" 9 | "github.com/shimastripe/api-server/models" 10 | "github.com/shimastripe/api-server/version" 11 | 12 | "github.com/gin-gonic/gin" 13 | ) 14 | 15 | func GetUsers(c *gin.Context) { 16 | ver, err := version.New(c) 17 | if err != nil { 18 | c.JSON(400, gin.H{"error": err.Error()}) 19 | return 20 | } 21 | 22 | db := dbpkg.DBInstance(c) 23 | parameter, err := dbpkg.NewParameter(c, models.User{}) 24 | if err != nil { 25 | c.JSON(400, gin.H{"error": err.Error()}) 26 | return 27 | } 28 | 29 | db, err = parameter.Paginate(db) 30 | if err != nil { 31 | c.JSON(400, gin.H{"error": err.Error()}) 32 | return 33 | } 34 | 35 | db = parameter.SetPreloads(db) 36 | db = parameter.SortRecords(db) 37 | db = parameter.FilterFields(db) 38 | users := []models.User{} 39 | fields := helper.ParseFields(c.DefaultQuery("fields", "*")) 40 | queryFields := helper.QueryFields(models.User{}, fields) 41 | 42 | if err := db.Select(queryFields).Find(&users).Error; err != nil { 43 | c.JSON(400, gin.H{"error": err.Error()}) 44 | return 45 | } 46 | 47 | index := 0 48 | 49 | if len(users) > 0 { 50 | index = int(users[len(users)-1].ID) 51 | } 52 | 53 | if err := parameter.SetHeaderLink(c, index); err != nil { 54 | c.JSON(400, gin.H{"error": err.Error()}) 55 | return 56 | } 57 | 58 | if version.Range("1.0.0", "<=", ver) && version.Range(ver, "<", "2.0.0") { 59 | // conditional branch by version. 60 | // 1.0.0 <= this version < 2.0.0 !! 61 | } 62 | 63 | if _, ok := c.GetQuery("stream"); ok { 64 | enc := json.NewEncoder(c.Writer) 65 | c.Status(200) 66 | 67 | for _, user := range users { 68 | fieldMap, err := helper.FieldToMap(user, fields) 69 | if err != nil { 70 | c.JSON(400, gin.H{"error": err.Error()}) 71 | return 72 | } 73 | 74 | if err := enc.Encode(fieldMap); err != nil { 75 | c.JSON(400, gin.H{"error": err.Error()}) 76 | return 77 | } 78 | } 79 | } else { 80 | fieldMaps := []map[string]interface{}{} 81 | 82 | for _, user := range users { 83 | fieldMap, err := helper.FieldToMap(user, fields) 84 | if err != nil { 85 | c.JSON(400, gin.H{"error": err.Error()}) 86 | return 87 | } 88 | 89 | fieldMaps = append(fieldMaps, fieldMap) 90 | } 91 | 92 | if _, ok := c.GetQuery("pretty"); ok { 93 | c.IndentedJSON(200, fieldMaps) 94 | } else { 95 | c.JSON(200, fieldMaps) 96 | } 97 | } 98 | } 99 | 100 | func GetUser(c *gin.Context) { 101 | ver, err := version.New(c) 102 | if err != nil { 103 | c.JSON(400, gin.H{"error": err.Error()}) 104 | return 105 | } 106 | 107 | db := dbpkg.DBInstance(c) 108 | parameter, err := dbpkg.NewParameter(c, models.User{}) 109 | if err != nil { 110 | c.JSON(400, gin.H{"error": err.Error()}) 111 | return 112 | } 113 | 114 | db = parameter.SetPreloads(db) 115 | user := models.User{} 116 | id := c.Params.ByName("id") 117 | fields := helper.ParseFields(c.DefaultQuery("fields", "*")) 118 | queryFields := helper.QueryFields(models.User{}, fields) 119 | 120 | if err := db.Select(queryFields).First(&user, id).Error; err != nil { 121 | content := gin.H{"error": "user with id#" + id + " not found"} 122 | c.JSON(404, content) 123 | return 124 | } 125 | 126 | fieldMap, err := helper.FieldToMap(user, fields) 127 | if err != nil { 128 | c.JSON(400, gin.H{"error": err.Error()}) 129 | return 130 | } 131 | 132 | if version.Range("1.0.0", "<=", ver) && version.Range(ver, "<", "2.0.0") { 133 | // conditional branch by version. 134 | // 1.0.0 <= this version < 2.0.0 !! 135 | } 136 | 137 | if _, ok := c.GetQuery("pretty"); ok { 138 | c.IndentedJSON(200, fieldMap) 139 | } else { 140 | c.JSON(200, fieldMap) 141 | } 142 | } 143 | 144 | func CreateUser(c *gin.Context) { 145 | ver, err := version.New(c) 146 | if err != nil { 147 | c.JSON(400, gin.H{"error": err.Error()}) 148 | return 149 | } 150 | 151 | db := dbpkg.DBInstance(c) 152 | user := models.User{} 153 | 154 | if err := c.Bind(&user); err != nil { 155 | c.JSON(400, gin.H{"error": err.Error()}) 156 | return 157 | } 158 | 159 | if err := db.Create(&user).Error; err != nil { 160 | c.JSON(400, gin.H{"error": err.Error()}) 161 | return 162 | } 163 | 164 | if version.Range("1.0.0", "<=", ver) && version.Range(ver, "<", "2.0.0") { 165 | // conditional branch by version. 166 | // 1.0.0 <= this version < 2.0.0 !! 167 | } 168 | 169 | c.JSON(201, user) 170 | } 171 | 172 | func UpdateUser(c *gin.Context) { 173 | ver, err := version.New(c) 174 | if err != nil { 175 | c.JSON(400, gin.H{"error": err.Error()}) 176 | return 177 | } 178 | 179 | db := dbpkg.DBInstance(c) 180 | id := c.Params.ByName("id") 181 | user := models.User{} 182 | 183 | if db.First(&user, id).Error != nil { 184 | content := gin.H{"error": "user with id#" + id + " not found"} 185 | c.JSON(404, content) 186 | return 187 | } 188 | 189 | if err := c.Bind(&user); err != nil { 190 | c.JSON(400, gin.H{"error": err.Error()}) 191 | return 192 | } 193 | 194 | if err := db.Save(&user).Error; err != nil { 195 | c.JSON(400, gin.H{"error": err.Error()}) 196 | return 197 | } 198 | 199 | if version.Range("1.0.0", "<=", ver) && version.Range(ver, "<", "2.0.0") { 200 | // conditional branch by version. 201 | // 1.0.0 <= this version < 2.0.0 !! 202 | } 203 | 204 | c.JSON(200, user) 205 | } 206 | 207 | func DeleteUser(c *gin.Context) { 208 | ver, err := version.New(c) 209 | if err != nil { 210 | c.JSON(400, gin.H{"error": err.Error()}) 211 | return 212 | } 213 | 214 | db := dbpkg.DBInstance(c) 215 | id := c.Params.ByName("id") 216 | user := models.User{} 217 | 218 | if db.First(&user, id).Error != nil { 219 | content := gin.H{"error": "user with id#" + id + " not found"} 220 | c.JSON(404, content) 221 | return 222 | } 223 | 224 | if err := db.Delete(&user).Error; err != nil { 225 | c.JSON(400, gin.H{"error": err.Error()}) 226 | return 227 | } 228 | 229 | if version.Range("1.0.0", "<=", ver) && version.Range(ver, "<", "2.0.0") { 230 | // conditional branch by version. 231 | // 1.0.0 <= this version < 2.0.0 !! 232 | } 233 | 234 | c.Writer.WriteHeader(http.StatusNoContent) 235 | } 236 | -------------------------------------------------------------------------------- /apig/testdata/db/db_mysql.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "strings" 7 | 8 | "github.com/shimastripe/api-server/models" 9 | 10 | "github.com/gin-gonic/gin" 11 | "github.com/jinzhu/gorm" 12 | _ "github.com/jinzhu/gorm/dialects/mysql" 13 | "github.com/serenize/snaker" 14 | ) 15 | 16 | func Connect() *gorm.DB { 17 | dbURL := os.Getenv("DATABASE_URL") 18 | if dbURL == "" { 19 | return nil 20 | } 21 | 22 | db, err := gorm.Open("mysql", dbURL) 23 | if err != nil { 24 | log.Fatalf("Got error when connect database, the error is '%v'", err) 25 | } 26 | 27 | db.LogMode(false) 28 | 29 | if gin.IsDebugging() { 30 | db.LogMode(true) 31 | } 32 | 33 | if os.Getenv("AUTOMIGRATE") == "1" { 34 | db.AutoMigrate( 35 | &models.User{}, 36 | ) 37 | } 38 | 39 | return db 40 | } 41 | 42 | func DBInstance(c *gin.Context) *gorm.DB { 43 | return c.MustGet("DB").(*gorm.DB) 44 | } 45 | 46 | func (self *Parameter) SetPreloads(db *gorm.DB) *gorm.DB { 47 | if self.Preloads == "" { 48 | return db 49 | } 50 | 51 | for _, preload := range strings.Split(self.Preloads, ",") { 52 | var a []string 53 | 54 | for _, s := range strings.Split(preload, ".") { 55 | a = append(a, snaker.SnakeToCamel(s)) 56 | } 57 | 58 | db = db.Preload(strings.Join(a, ".")) 59 | } 60 | 61 | return db 62 | } 63 | -------------------------------------------------------------------------------- /apig/testdata/db/db_postgres.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "strings" 7 | 8 | "github.com/shimastripe/api-server/models" 9 | 10 | "github.com/gin-gonic/gin" 11 | "github.com/jinzhu/gorm" 12 | _ "github.com/jinzhu/gorm/dialects/postgres" 13 | "github.com/serenize/snaker" 14 | ) 15 | 16 | func Connect() *gorm.DB { 17 | dbURL := os.Getenv("DATABASE_URL") 18 | if dbURL == "" { 19 | return nil 20 | } 21 | 22 | db, err := gorm.Open("postgres", dbURL) 23 | if err != nil { 24 | log.Fatalf("Got error when connect database, the error is '%v'", err) 25 | } 26 | 27 | db.LogMode(false) 28 | 29 | if gin.IsDebugging() { 30 | db.LogMode(true) 31 | } 32 | 33 | if os.Getenv("AUTOMIGRATE") == "1" { 34 | db.AutoMigrate( 35 | &models.User{}, 36 | ) 37 | } 38 | 39 | return db 40 | } 41 | 42 | func DBInstance(c *gin.Context) *gorm.DB { 43 | return c.MustGet("DB").(*gorm.DB) 44 | } 45 | 46 | func (self *Parameter) SetPreloads(db *gorm.DB) *gorm.DB { 47 | if self.Preloads == "" { 48 | return db 49 | } 50 | 51 | for _, preload := range strings.Split(self.Preloads, ",") { 52 | var a []string 53 | 54 | for _, s := range strings.Split(preload, ".") { 55 | a = append(a, snaker.SnakeToCamel(s)) 56 | } 57 | 58 | db = db.Preload(strings.Join(a, ".")) 59 | } 60 | 61 | return db 62 | } 63 | -------------------------------------------------------------------------------- /apig/testdata/db/db_sqlite.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/shimastripe/api-server/models" 10 | 11 | "github.com/gin-gonic/gin" 12 | "github.com/jinzhu/gorm" 13 | _ "github.com/jinzhu/gorm/dialects/sqlite" 14 | "github.com/serenize/snaker" 15 | ) 16 | 17 | func Connect() *gorm.DB { 18 | dir := filepath.Dir("db/database.db") 19 | db, err := gorm.Open("sqlite3", dir+"/database.db") 20 | if err != nil { 21 | log.Fatalf("Got error when connect database, the error is '%v'", err) 22 | } 23 | 24 | db.LogMode(false) 25 | 26 | if gin.IsDebugging() { 27 | db.LogMode(true) 28 | } 29 | 30 | if os.Getenv("AUTOMIGRATE") == "1" { 31 | db.AutoMigrate( 32 | &models.User{}, 33 | ) 34 | } 35 | 36 | return db 37 | } 38 | 39 | func DBInstance(c *gin.Context) *gorm.DB { 40 | return c.MustGet("DB").(*gorm.DB) 41 | } 42 | 43 | func (self *Parameter) SetPreloads(db *gorm.DB) *gorm.DB { 44 | if self.Preloads == "" { 45 | return db 46 | } 47 | 48 | for _, preload := range strings.Split(self.Preloads, ",") { 49 | var a []string 50 | 51 | for _, s := range strings.Split(preload, ".") { 52 | a = append(a, snaker.SnakeToCamel(s)) 53 | } 54 | 55 | db = db.Preload(strings.Join(a, ".")) 56 | } 57 | 58 | return db 59 | } 60 | -------------------------------------------------------------------------------- /apig/testdata/docs/index.apib: -------------------------------------------------------------------------------- 1 | FORMAT: 1A 2 | HOST: http://localhost:8080 3 | 4 | # Api-Server API 5 | 6 | 7 | -------------------------------------------------------------------------------- /apig/testdata/docs/user.apib: -------------------------------------------------------------------------------- 1 | # Group Users 2 | Welcome to the users API. This API provides access to the users service. 3 | 4 | ## users [/users] 5 | 6 | ### Create user [POST] 7 | 8 | Create a new user 9 | 10 | + Request user (application/json; charset=utf-8) 11 | + Headers 12 | 13 | Accept: application/vnd.shimastripe+json 14 | + Attributes 15 | 16 | + name: NAME (string) 17 | 18 | + Response 201 (application/json; charset=utf-8) 19 | + Attributes (user, fixed) 20 | 21 | ### Get users [GET] 22 | 23 | Returns an user list. 24 | 25 | + Request (application/json; charset=utf-8) 26 | + Headers 27 | 28 | Accept: application/vnd.shimastripe+json 29 | 30 | + Response 200 (application/json; charset=utf-8) 31 | + Attributes (array, fixed) 32 | + (user) 33 | 34 | ## user details [/users/{id}] 35 | 36 | + Parameters 37 | + id: `1` (enum[string]) - The ID of the desired user. 38 | + Members 39 | + `1` 40 | + `2` 41 | + `3` 42 | 43 | ### Get user [GET] 44 | 45 | Returns an user. 46 | 47 | + Request (application/json; charset=utf-8) 48 | + Headers 49 | 50 | Accept: application/vnd.shimastripe+json 51 | 52 | + Response 200 (application/json; charset=utf-8) 53 | + Attributes (user, fixed) 54 | 55 | ### Update user [PUT] 56 | 57 | Update an user. 58 | 59 | + Request user (application/json; charset=utf-8) 60 | + Headers 61 | 62 | Accept: application/vnd.shimastripe+json 63 | + Attributes 64 | 65 | + name: NAME (string) 66 | 67 | + Response 200 (application/json; charset=utf-8) 68 | + Attributes (user, fixed) 69 | 70 | ### Delete user [DELETE] 71 | 72 | Delete an user. 73 | 74 | + Request (application/json; charset=utf-8) 75 | + Headers 76 | 77 | Accept: application/vnd.shimastripe+json 78 | 79 | + Response 204 80 | 81 | # Data Structures 82 | ## user (object) 83 | 84 | + id: *1* (number) 85 | + name: *NAME* (string) 86 | + created_at: `*2000-01-01 00:00:00*` (string) 87 | + updated_at: `*2000-01-01 00:00:00*` (string) 88 | -------------------------------------------------------------------------------- /apig/testdata/models.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type User struct { 8 | ID uint `gorm:"primary_key;AUTO_INCREMENT" json:"id,omitempty" form:"id"` 9 | Name string `json:"name,omitempty" form:"name"` 10 | CreatedAt *time.Time `json:"created_at,omitempty" form:"created_at"` 11 | UpdatedAt *time.Time `json:"updated_at,omitempty" form:"updated_at"` 12 | } 13 | 14 | type Job struct { 15 | ID uint `gorm:"primary_key;AUTO_INCREMENT" json:"id,omitempty" form:"id"` 16 | Name string `json:"name,omitempty" form:"name"` 17 | Description string `json:"description,omitempty" form:"description"` 18 | CreatedAt *time.Time `json:"created_at,omitempty" form:"created_at"` 19 | UpdatedAt *time.Time `json:"updated_at,omitempty" form:"updated_at"` 20 | } 21 | -------------------------------------------------------------------------------- /apig/testdata/parse/models.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type User struct { 8 | ID uint `gorm:"primary_key;AUTO_INCREMENT" json:"id,omitempty" form:"id"` 9 | Name string `json:"name,omitempty" form:"name"` 10 | CreatedAt *time.Time `json:"created_at,omitempty" form:"created_at"` 11 | UpdatedAt *time.Time 12 | } 13 | 14 | type Job struct { 15 | ID uint `gorm:"primary_key;AUTO_INCREMENT" json:"id,omitempty" form:"id"` 16 | Name string `json:"name,omitempty" form:"name"` 17 | Description string `json:"description,omitempty" form:"description"` 18 | CreatedAt *time.Time `json:"created_at,omitempty" form:"created_at"` 19 | UpdatedAt *time.Time `json:"updated_at,omitempty" form:"updated_at"` 20 | } 21 | -------------------------------------------------------------------------------- /apig/testdata/parse/router.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "github.com/shimastripe/api-server/controllers" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | func Initialize(r *gin.Engine) { 10 | r.GET("/", controllers.APIEndpoints) 11 | 12 | api := r.Group("api") 13 | { 14 | 15 | api.GET("/users", controllers.GetUsers) 16 | api.GET("/users/:id", controllers.GetUser) 17 | api.POST("/users", controllers.CreateUser) 18 | api.PUT("/users/:id", controllers.UpdateUser) 19 | api.DELETE("/users/:id", controllers.DeleteUser) 20 | 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /apig/testdata/router/router.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "github.com/shimastripe/api-server/controllers" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | func Initialize(r *gin.Engine) { 10 | r.GET("/", controllers.APIEndpoints) 11 | 12 | api := r.Group("") 13 | { 14 | 15 | api.GET("/users", controllers.GetUsers) 16 | api.GET("/users/:id", controllers.GetUser) 17 | api.POST("/users", controllers.CreateUser) 18 | api.PUT("/users/:id", controllers.UpdateUser) 19 | api.DELETE("/users/:id", controllers.DeleteUser) 20 | 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /apig/validate.go: -------------------------------------------------------------------------------- 1 | package apig 2 | 3 | func validateForeignKey(fields []*Field, name string) bool { 4 | for _, field := range fields { 5 | val := name + "ID" 6 | if field.Name == val { 7 | return true 8 | } 9 | } 10 | return false 11 | } 12 | -------------------------------------------------------------------------------- /apig/validate_test.go: -------------------------------------------------------------------------------- 1 | package apig 2 | 3 | import "testing" 4 | 5 | func TestValidateForeignKey(t *testing.T) { 6 | model := &Model{ 7 | Name: "User", 8 | Fields: []*Field{ 9 | &Field{ 10 | Name: "ID", 11 | Type: "uint", 12 | }, 13 | &Field{ 14 | Name: "Profile", 15 | Type: "*Profile", 16 | }, 17 | &Field{ 18 | Name: "ProfileID", 19 | Type: "uint", 20 | }, 21 | &Field{ 22 | Name: "CreatedAt", 23 | Type: "*time.Time", 24 | }, 25 | &Field{ 26 | Name: "UpdatedAt", 27 | Type: "*time.Time", 28 | }, 29 | }, 30 | } 31 | 32 | result := validateForeignKey(model.Fields, "Profile") 33 | if result != true { 34 | t.Fatalf("Incorrect result. expected: true, actual: %v", result) 35 | } 36 | 37 | result = validateForeignKey(model.Fields, "Nation") 38 | if result != false { 39 | t.Fatalf("Incorrect result. expected: false, actual: %v", result) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /cli.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/mitchellh/cli" 8 | "github.com/shimastripe/apig/command" 9 | ) 10 | 11 | func Run(args []string) int { 12 | 13 | // Meta-option for executables. 14 | // It defines output color and its stdout/stderr stream. 15 | meta := &command.Meta{ 16 | Ui: &cli.ColoredUi{ 17 | InfoColor: cli.UiColorBlue, 18 | ErrorColor: cli.UiColorRed, 19 | Ui: &cli.BasicUi{ 20 | Writer: os.Stdout, 21 | ErrorWriter: os.Stderr, 22 | Reader: os.Stdin, 23 | }, 24 | }} 25 | 26 | return RunCustom(args, Commands(meta)) 27 | } 28 | 29 | func RunCustom(args []string, commands map[string]cli.CommandFactory) int { 30 | 31 | // Get the command line args. We shortcut "--version" and "-v" to 32 | // just show the version. 33 | for _, arg := range args { 34 | if arg == "-v" || arg == "-version" || arg == "--version" { 35 | newArgs := make([]string, len(args)+1) 36 | newArgs[0] = "version" 37 | copy(newArgs[1:], args) 38 | args = newArgs 39 | break 40 | } 41 | } 42 | 43 | cli := &cli.CLI{ 44 | Args: args, 45 | Commands: commands, 46 | Version: Version, 47 | HelpFunc: cli.BasicHelpFunc(Name), 48 | HelpWriter: os.Stdout, 49 | } 50 | 51 | exitCode, err := cli.Run() 52 | if err != nil { 53 | fmt.Fprintf(os.Stderr, "Failed to execute: %s\n", err.Error()) 54 | } 55 | 56 | return exitCode 57 | } 58 | -------------------------------------------------------------------------------- /command/gen.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/shimastripe/apig/apig" 11 | "github.com/shimastripe/apig/util" 12 | ) 13 | 14 | const ( 15 | modelDir = "models" 16 | targetFile = "main.go" 17 | ) 18 | 19 | type GenCommand struct { 20 | Meta 21 | 22 | all bool 23 | } 24 | 25 | func (c *GenCommand) Run(args []string) int { 26 | wd, err := os.Getwd() 27 | if err != nil { 28 | fmt.Fprintln(os.Stderr, err) 29 | return 1 30 | } 31 | if !util.FileExists(filepath.Join(wd, targetFile)) || !util.FileExists(filepath.Join(wd, modelDir)) { 32 | fmt.Fprintf(os.Stderr, `%s is not project root. Please move. 33 | `, wd) 34 | return 1 35 | } 36 | 37 | if err := c.parseArgs(args); err != nil { 38 | fmt.Fprintln(os.Stderr, err) 39 | return 1 40 | } 41 | return apig.Generate(wd, modelDir, targetFile, c.all) 42 | } 43 | 44 | func (c *GenCommand) parseArgs(args []string) error { 45 | flag := flag.NewFlagSet("apig", flag.ContinueOnError) 46 | 47 | flag.BoolVar(&c.all, "a", false, "Generate all skelton") 48 | flag.BoolVar(&c.all, "all", false, "Generate all skelton") 49 | 50 | if err := flag.Parse(args); err != nil { 51 | return err 52 | } 53 | 54 | return nil 55 | } 56 | 57 | func (c *GenCommand) Synopsis() string { 58 | return "Generate controllers based on models" 59 | } 60 | 61 | func (c *GenCommand) Help() string { 62 | helpText := ` 63 | Usage: apig [options] gen 64 | 65 | Generates controllers and more based on models 66 | 67 | Options: 68 | -all, -a Generate all boilerplate including new command generated code 69 | ` 70 | return strings.TrimSpace(helpText) 71 | } 72 | -------------------------------------------------------------------------------- /command/gen_test.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/mitchellh/cli" 7 | ) 8 | 9 | func TestGenCommand_implement(t *testing.T) { 10 | var _ cli.Command = &GenCommand{} 11 | } 12 | -------------------------------------------------------------------------------- /command/meta.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import "github.com/mitchellh/cli" 4 | 5 | // Meta contain the meta-option that nearly all subcommand inherited. 6 | type Meta struct { 7 | Ui cli.Ui 8 | } 9 | -------------------------------------------------------------------------------- /command/new.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "fmt" 7 | "os" 8 | "strings" 9 | 10 | "github.com/shimastripe/apig/apig" 11 | "github.com/tcnksm/go-gitconfig" 12 | ) 13 | 14 | const ( 15 | defaultDatabase = "sqlite" 16 | defaultVCS = "github.com" 17 | ) 18 | 19 | type NewCommand struct { 20 | Meta 21 | 22 | vcs string 23 | username string 24 | project string 25 | namespace string 26 | database string 27 | } 28 | 29 | func (c *NewCommand) Run(args []string) int { 30 | if err := c.parseArgs(args); err != nil { 31 | fmt.Fprintln(os.Stderr, err) 32 | return 1 33 | } 34 | 35 | gopath := os.Getenv("GOPATH") 36 | if gopath == "" { 37 | fmt.Fprintln(os.Stderr, "Error: $GOPATH is not found") 38 | return 1 39 | } 40 | 41 | return apig.Skeleton(gopath, c.vcs, c.username, c.project, c.namespace, c.database) 42 | } 43 | 44 | func (c *NewCommand) parseArgs(args []string) error { 45 | flag := flag.NewFlagSet("apig", flag.ContinueOnError) 46 | 47 | flag.StringVar(&c.vcs, "vcs", defaultVCS, "VCS") 48 | flag.StringVar(&c.username, "u", "", "Username") 49 | flag.StringVar(&c.username, "user", "", "Username") 50 | flag.StringVar(&c.namespace, "n", "", "Namespace of API") 51 | flag.StringVar(&c.namespace, "namespace", "", "Namespace of API") 52 | flag.StringVar(&c.database, "d", defaultDatabase, "Database engine [sqlite,postgres,mysql]") 53 | flag.StringVar(&c.database, "database", defaultDatabase, "Database engine [sqlite,postgres,mysql]") 54 | 55 | if err := flag.Parse(args); err != nil { 56 | return err 57 | } 58 | if 0 < flag.NArg() { 59 | c.project = flag.Arg(0) 60 | } 61 | 62 | if c.project == "" { 63 | return errors.New("Please specify project name.") 64 | } 65 | 66 | if c.username == "" { 67 | var err error 68 | c.username, err = gitconfig.GithubUser() 69 | if err != nil { 70 | c.username, err = gitconfig.Username() 71 | if err != nil || strings.Contains(c.username, " ") { 72 | return errors.New("Cannot find github username in `~/.gitconfig` file.\n" + 73 | "Please use -u option") 74 | } 75 | } 76 | } 77 | return nil 78 | } 79 | 80 | func (c *NewCommand) Synopsis() string { 81 | return "Generate boilerplate" 82 | } 83 | 84 | func (c *NewCommand) Help() string { 85 | helpText := ` 86 | Usage: apig new [options] PROJECTNAME 87 | 88 | Generate go project and its boilerplate 89 | 90 | Options: 91 | -database=database, -d Database engine [sqlite,postgres,mysql] (default: sqlite) 92 | -namespace=namepace, -n Namespace of API (default: "" (blank string)) 93 | -user=name, -u Username of VCS (default: username of github in .gitconfig) 94 | -vcs=name Version controll system to use (default: github.com) 95 | ` 96 | return strings.TrimSpace(helpText) 97 | } 98 | -------------------------------------------------------------------------------- /command/new_test.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/mitchellh/cli" 7 | ) 8 | 9 | func TestNewCommand_implement(t *testing.T) { 10 | var _ cli.Command = &NewCommand{} 11 | } 12 | -------------------------------------------------------------------------------- /command/version.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | ) 7 | 8 | type VersionCommand struct { 9 | Meta 10 | 11 | Name string 12 | Version string 13 | Revision string 14 | } 15 | 16 | func (c *VersionCommand) Run(args []string) int { 17 | var versionString bytes.Buffer 18 | 19 | fmt.Fprintf(&versionString, "%s version %s", c.Name, c.Version) 20 | if c.Revision != "" { 21 | fmt.Fprintf(&versionString, " (%s)", c.Revision) 22 | } 23 | 24 | c.Ui.Output(versionString.String()) 25 | return 0 26 | } 27 | 28 | func (c *VersionCommand) Synopsis() string { 29 | return fmt.Sprintf("Print %s version and quit", c.Name) 30 | } 31 | 32 | func (c *VersionCommand) Help() string { 33 | return ` 34 | Usage: apig version 35 | 36 | Returns version of apig 37 | ` 38 | } 39 | -------------------------------------------------------------------------------- /commands.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/mitchellh/cli" 5 | "github.com/shimastripe/apig/command" 6 | ) 7 | 8 | func Commands(meta *command.Meta) map[string]cli.CommandFactory { 9 | return map[string]cli.CommandFactory{ 10 | "gen": func() (cli.Command, error) { 11 | return &command.GenCommand{ 12 | Meta: *meta, 13 | }, nil 14 | }, 15 | "new": func() (cli.Command, error) { 16 | return &command.NewCommand{ 17 | Meta: *meta, 18 | }, nil 19 | }, 20 | 21 | "version": func() (cli.Command, error) { 22 | return &command.VersionCommand{ 23 | Meta: *meta, 24 | Version: Version, 25 | Revision: GitCommit, 26 | Name: Name, 27 | }, nil 28 | }, 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/shimastripe/apig 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/gedex/inflector v0.0.0-20160409081948-91797f1712fd 7 | github.com/mitchellh/cli v1.1.2 8 | github.com/serenize/snaker v0.0.0-20160310080004-8824b61eca66 9 | github.com/tcnksm/go-gitconfig v0.1.3-0.20150505151006-6411ba19847f 10 | ) 11 | 12 | require ( 13 | github.com/onsi/ginkgo v1.16.5 // indirect 14 | github.com/onsi/gomega v1.17.0 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "os" 4 | 5 | //go:generate go-bindata -o ./apig/bindata.go -pkg apig _templates/... 6 | 7 | func main() { 8 | os.Exit(Run(os.Args[1:])) 9 | } 10 | -------------------------------------------------------------------------------- /msg/msg.go: -------------------------------------------------------------------------------- 1 | package msg 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | ) 7 | 8 | var ( 9 | Mute = false 10 | m sync.Mutex 11 | ) 12 | 13 | func Printf(format string, a ...interface{}) { 14 | if !Mute { 15 | m.Lock() 16 | fmt.Printf(format, a...) 17 | m.Unlock() 18 | } 19 | } 20 | 21 | func Println(a ...interface{}) { 22 | if !Mute { 23 | m.Lock() 24 | fmt.Println(a...) 25 | m.Unlock() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /script/generation_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eu 4 | 5 | echo "===> Generating API server..." 6 | cd _example 7 | ../bin/apig gen --all 8 | 9 | if [[ ! $(git status . | grep 'nothing to commit') ]]; then 10 | echo " x Generator artifact and example application are different." 11 | git --no-pager diff . 12 | exit 1 13 | fi 14 | 15 | echo "===> Building API server..." 16 | go mod init github.com/shimastripe/apig/_example 17 | go mod tidy 18 | go build 19 | 20 | if [[ $? -gt 0 ]]; then 21 | echo " x Failed to build generated API server." 22 | exit 1 23 | fi 24 | 25 | echo " o Generation test PASSED!" 26 | -------------------------------------------------------------------------------- /util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | func FileExists(dir string) bool { 8 | _, err := os.Stat(dir) 9 | return err == nil 10 | } 11 | 12 | func Mkdir(dir string) error { 13 | return os.MkdirAll(dir, os.ModePerm) 14 | } 15 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | const Name string = "apig" 4 | const Version string = "0.1.0" 5 | 6 | // GitCommit describes latest commit hash. 7 | // This value is extracted by git command when building. 8 | // To set this from outside, use go build -ldflags "-X main.GitCommit \"$(COMMIT)\"" 9 | var GitCommit string 10 | --------------------------------------------------------------------------------