Skip to content
This repository has been archived by the owner on Nov 28, 2017. It is now read-only.

Commit

Permalink
Step 1 of refactoring API project into a Tenant API, Admin API and a …
Browse files Browse the repository at this point in the history
…API Utils project.

NOTHING WORKS YET!
  • Loading branch information
frankbille committed Jul 29, 2017
1 parent f3d29fc commit 589fae8
Show file tree
Hide file tree
Showing 9 changed files with 537 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/vendor/*
!/vendor/vendor.json
/cohousing-api-utils.iml
45 changes: 45 additions & 0 deletions api/linkfactory.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package utils

import (
"github.com/cohousing/cohousing-api-utils/domain"
"github.com/gin-gonic/gin"
"reflect"
)

var (
factory map[string]LinkFactory = make(map[string]LinkFactory)
halResourceInterface domain.HalResource
)

type LinkFactory func(c *gin.Context, halResource domain.HalResource, basePath string, detailed bool)

func AddLinkFactory(resource interface{}, linkFactory LinkFactory) {
resourceType := reflect.TypeOf(resource)
if resourceType.Implements(reflect.TypeOf(&halResourceInterface).Elem()) {
factory[resourceType.String()] = linkFactory
} else {
panic("Must add a resource that implements HalResource. Also remember to parse it as a pointer.")
}
}

// Add links to resource based on the type of it
func AddLinks(c *gin.Context, resource interface{}, basePath string, detailed bool) {
addLinks := func(halResource domain.HalResource) {
linkFactory := factory[reflect.TypeOf(halResource).String()]
linkFactory(c, halResource, basePath, detailed)
}

resourceType := reflect.TypeOf(resource)
if resourceType.Implements(reflect.TypeOf(&halResourceInterface).Elem()) {
addLinks(resource.(domain.HalResource))
} else if resourceType.Kind() == reflect.Ptr {
AddLinks(c, reflect.ValueOf(resource).Elem().Interface(), basePath, detailed)
} else if resourceType.Kind() == reflect.Slice {
valueList := reflect.ValueOf(resource)
listLength := valueList.Len()
for i := 0; i < listLength; i++ {
object := valueList.Index(i).Addr().Interface()
addLinks(object.(domain.HalResource))
}
}
}
75 changes: 75 additions & 0 deletions api/pagination.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package utils

import (
"fmt"
"github.com/cohousing/cohousing-api-utils/domain"
"github.com/gin-gonic/gin"
"strconv"
)

type PaginatedList struct {
CurrentPage int
Count int
Objects interface{} `json:"objects"`
domain.DefaultHalResource
}

func CreatePaginatedList(baseUrl string, objects interface{}, currentPage, count, recordsPerPage int) PaginatedList {
objectList := PaginatedList{
CurrentPage: currentPage,
Count: count,
Objects: objects,
}

AddPaginationLinks(&objectList, baseUrl, recordsPerPage)

return objectList
}

func GetCurrentPage(c *gin.Context) int {
var page int
if pageInt, err := strconv.ParseUint(c.DefaultQuery("page", "1"), 10, 32); err == nil {
page = int(pageInt)
}
if page < 1 {
page = 1
}
return page
}

func AddPaginationLinks(objectList *PaginatedList, baseUrl string, recordsPerPage int) {
var firstPage int = 1

var lastPage int = objectList.Count / recordsPerPage
if lastPage < 1 {
lastPage = 1
}

var prevPage int = objectList.CurrentPage - 1
var nextPage int = objectList.CurrentPage + 1

objectList.AddLink(domain.REL_SELF, generatePaginationUrl(baseUrl, objectList.CurrentPage))

if firstPage != lastPage {
objectList.AddLink(domain.REL_FIRST, generatePaginationUrl(baseUrl, firstPage))
objectList.AddLink(domain.REL_LAST, generatePaginationUrl(baseUrl, lastPage))
}
if prevPage >= firstPage && prevPage < lastPage {
objectList.AddLink(domain.REL_PREV, generatePaginationUrl(baseUrl, prevPage))
}
if nextPage <= lastPage {
objectList.AddLink(domain.REL_NEXT, generatePaginationUrl(baseUrl, nextPage))
}
}

func generatePaginationUrl(baseUrl string, page int) string {
if page > 1 {
return fmt.Sprintf("%s?page=%d", baseUrl, page)
} else {
return baseUrl
}
}

func GetStartRecord(currentPage, recordsPerPage int) int {
return (currentPage - 1) * recordsPerPage
}
32 changes: 32 additions & 0 deletions api/reflect_utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package utils

import (
"reflect"
)

func GetFieldByName(object interface{}, name string) *reflect.Value {
var foundField *reflect.Value
objectType := reflect.TypeOf(object)
objectValue := reflect.ValueOf(object)
if objectValue.Kind() == reflect.Ptr {
objectType = objectType.Elem()
objectValue = objectValue.Elem()
}

for i := 0; i < objectValue.NumField(); i++ {
fieldType := objectType.Field(i)
fieldValue := objectValue.Field(i)

if fieldType.Name == name {
foundField = &fieldValue
} else if fieldType.Anonymous {
foundField = GetFieldByName(fieldValue.Interface(), name)
}

if foundField != nil {
break
}
}

return foundField
}
189 changes: 189 additions & 0 deletions api/rest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
package utils

import (
"bytes"
"encoding/json"
"fmt"
"github.com/cohousing/cohousing-api-utils/db"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/jinzhu/gorm"
"net/http"
"os"
"reflect"
"strconv"
"strings"
)

var (
RecordsPerPage int = 50
)

type BasicEndpointConfig struct {
Path string
Domain interface{}
domainType reflect.Type
DBFactory db.DBFactory
RouterHandlers []gin.HandlerFunc
GetListHandlers []gin.HandlerFunc
GetHandlers []gin.HandlerFunc
CreateHandlers []gin.HandlerFunc
UpdateHandlers []gin.HandlerFunc
DeleteHandlers []gin.HandlerFunc
}

func ConfigureBasicEndpoint(router *gin.RouterGroup, config BasicEndpointConfig) *gin.RouterGroup {
config.domainType = reflect.TypeOf(config.Domain)

endpoint := router.Group(config.Path, config.RouterHandlers...)

repository := db.CreateRepository(config.domainType, config.DBFactory)

endpoint.GET("/", append(config.GetListHandlers, getResourceList(config, endpoint.BasePath(), repository))...)
endpoint.GET("/:id", append(config.GetHandlers, getResourceById(config, endpoint.BasePath(), repository))...)
endpoint.POST("/", append(config.CreateHandlers, createNewResource(config, endpoint.BasePath(), repository))...)
endpoint.PUT("/:id", append(config.UpdateHandlers, updateResource(config, endpoint.BasePath(), repository))...)
endpoint.DELETE("/:id", append(config.DeleteHandlers, deleteResource(repository))...)

return endpoint
}

func getResourceList(config BasicEndpointConfig, basePath string, repository *db.Repository) gin.HandlerFunc {
return func(c *gin.Context) {
lookupObject, page := parseQuery(c, config.domainType)
list, count := repository.GetList(c, lookupObject, GetStartRecord(page, RecordsPerPage), RecordsPerPage)
AddLinks(c, list, basePath, false)
domainList := CreatePaginatedList(basePath, list, page, count, RecordsPerPage)

c.JSON(http.StatusOK, domainList)
}
}

func getResourceById(config BasicEndpointConfig, basePath string, repository *db.Repository) gin.HandlerFunc {
return func(c *gin.Context) {
if id, err := strconv.ParseUint(c.Param("id"), 10, 64); err == nil {
if object, err := repository.GetById(c, id); err == nil {
AddLinks(c, object, basePath, true)
c.JSON(http.StatusOK, object)
} else if err == gorm.ErrRecordNotFound {
c.AbortWithStatus(http.StatusNotFound)
} else {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"error": err,
})
}
} else {
abortOnIdParsingError(c, id)
}
}
}

func createNewResource(config BasicEndpointConfig, basePath string, repository *db.Repository) gin.HandlerFunc {
return func(c *gin.Context) {
object := reflect.New(config.domainType).Interface()
if err := c.ShouldBindWith(&object, binding.JSON); err == nil {

createdObject, err := repository.Create(c, object)

if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"error": err,
})
} else {
AddLinks(c, createdObject, basePath, true)
c.JSON(http.StatusCreated, createdObject)
}
} else {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
"error": err,
})
}
}
}

func updateResource(config BasicEndpointConfig, basePath string, repository *db.Repository) gin.HandlerFunc {
return func(c *gin.Context) {
object := reflect.New(config.domainType).Interface()
if c.BindJSON(&object) == nil {
if id, err := strconv.ParseUint(c.Param("id"), 10, 64); err == nil {
if objectId := GetFieldByName(object, "ID").Uint(); objectId == id {
updatedObject, err := repository.Update(c, object)

if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"error": err,
})
} else {
AddLinks(c, updatedObject, basePath, true)
c.JSON(http.StatusOK, updatedObject)
}
} else {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
"error": fmt.Sprintf("Id on path is different from id in object: %v != %v", id, objectId),
})
}
} else {
abortOnIdParsingError(c, id)
}
}
}
}

func deleteResource(repository *db.Repository) gin.HandlerFunc {
return func(c *gin.Context) {
if id, err := strconv.ParseUint(c.Param("id"), 10, 64); err == nil {
if err = repository.Delete(c, id); err == nil {
c.Status(http.StatusNoContent)
} else {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"error": err,
})
}
} else {
abortOnIdParsingError(c, id)
}
}
}

func abortOnIdParsingError(c *gin.Context, id uint64) {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
"error": fmt.Sprintf("Id is not an unsigned integer: %v", id),
})
}

func parseQuery(c *gin.Context, domainType reflect.Type) (lookupObject interface{}, pageNumber int) {
page := GetCurrentPage(c)

var buffer bytes.Buffer
buffer.WriteString("{")
queryParams := c.Request.URL.Query()
for queryParam := range queryParams {
if queryParam != "page" {
if buffer.Len() > 1 {
buffer.WriteString(",")
}
buffer.WriteString("\"")
buffer.WriteString(strings.Replace(queryParam, "\"", "\\\"", -1))
buffer.WriteString("\":")

queryValue := queryParams.Get(queryParam)
_, err := strconv.ParseInt(queryValue, 10, 64)
if err == nil {
buffer.WriteString(queryValue)
} else {
buffer.WriteString("\"")
buffer.WriteString(strings.Replace(queryValue, "\"", "\\\"", -1))
buffer.WriteString("\"")
}
}
}
buffer.WriteString("}")

lookupObject = reflect.New(domainType).Interface()
err := json.Unmarshal(buffer.Bytes(), lookupObject)
if err != nil {
fmt.Fprintf(os.Stderr, "Error parsing JSON: %v\n", err)
}

return lookupObject, page
}
Loading

0 comments on commit 589fae8

Please sign in to comment.