This repository has been archived by the owner on Nov 28, 2017. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Step 1 of refactoring API project into a Tenant API, Admin API and a …
…API Utils project. NOTHING WORKS YET!
- Loading branch information
1 parent
f3d29fc
commit 589fae8
Showing
9 changed files
with
537 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
/vendor/* | ||
!/vendor/vendor.json | ||
/cohousing-api-utils.iml |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.