More elegant way of validate body in go-gin - go

Is there a more elegant way to validate json body and route id using go-gin?
package controllers
import (
"giin/inputs"
"net/http"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
func GetAccount(context *gin.Context) {
// validate if `accountId` is valid `uuid``
_, err := uuid.Parse(context.Param("accountId"))
if err != nil {
context.JSON(http.StatusBadRequest, gin.H{"message": err.Error()})
return
}
// some logic here...
context.JSON(http.StatusOK, gin.H{"message": "account received"})
}
func AddAccount(context *gin.Context) {
// validate if `body` is valid `inputs.Account`
var input inputs.Account
if error := context.ShouldBindJSON(&input); error != nil {
context.JSON(http.StatusBadRequest, error.Error())
return
}
// some logic here...
context.JSON(http.StatusOK, gin.H{"message": "account added"})
}
I created middleware which is able to detect if accountId was passed and if yes validate it and return bad request if accountId was not in uuid format but I couldn't do the same with the body because AccountBodyMiddleware tries to validate every request, could someone help me with this?
And also it would be nice if I could validate any type of body instead creating new middleware for each json body
package main
import (
"giin/controllers"
"giin/inputs"
"net/http"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
func AccountIdMiddleware(c *gin.Context) {
id := c.Param("accountId")
if id == "" {
c.Next()
return
}
if _, err := uuid.Parse(id); err != nil {
c.JSON(http.StatusBadRequest, "uuid not valid")
c.Abort()
return
}
}
func AccountBodyMiddleware(c *gin.Context) {
var input inputs.Account
if error := c.ShouldBindJSON(&input); error != nil {
c.JSON(http.StatusBadRequest, "body is not valid")
c.Abort()
return
}
c.Next()
}
func main() {
r := gin.Default()
r.Use(AccountIdMiddleware)
r.Use(AccountBodyMiddleware)
r.GET("/account/:accountId", controllers.GetAccount)
r.POST("/account", controllers.AddAccount)
r.Run(":5000")
}

Using middlewares is certainly not the way to go here, your hunch is correct! Using FastAPI as inspiration, I usually create models for every request/response that I have. You can then bind these models as query, path, or body models. An example of query model binding (just to show you that you can use this to more than just json post requests):
type User struct {
UserId string `form:"user_id"`
Name string `form:"name"`
}
func (user *User) Validate() errors.RestError {
if _, err := uuid.Parse(id); err != nil {
return errors.BadRequestError("user_id not a valid uuid")
}
return nil
}
Where errors is just a package you can define locally, so that can return validation errors directly in the following way:
func GetUser(c *gin.Context) {
// Bind query model
var q User
if err := c.ShouldBindQuery(&q); err != nil {
restError := errors.BadRequestError(err.Error())
c.JSON(restError.Status, restError)
return
}
// Validate request
if err := q.Validate(); err != nil {
c.JSON(err.Status, err)
return
}
// Business logic goes here
}
Bonus: In this way, you can also compose structs and call internal validation functions from a high level. I think this is what you were trying to accomplish by using middlewares (composing validation):
type UserId struct {
Id string
}
func (userid *UserId) Validate() errors.RestError {
if _, err := uuid.Parse(id); err != nil {
return errors.BadRequestError("user_id not a valid uuid")
}
return nil
}
type User struct {
UserId
Name string
}
func (user *User) Validate() errors.RestError {
if err := user.UserId.Validate(); err != nil {
return err
}
// Do some other validation
return nil
}
Extra bonus: read more about backend route design and model-based validation here if you're interested Softgrade - In Depth Guide to Backend Route Design
For reference, here is an example errors struct:
type RestError struct {
Message string `json:"message"`
Status int `json:"status"`
Error string `json:"error"`
}
func BadRequestError(message string) *RestError {
return &RestError{
Message: message,
Status: http.StatusBadRequest,
Error: "Invalid Request",
}
}

Related

how to use struct pointers in golang

I am trying to do a simple golang with gin post and get request, every other thing works just fine, apart from the part that the values that are supposed to be in the struct variables are empty, the example is bellow if i didnt explain well
my code(main)
package main
import (
//"fmt"
"github.com/cosimmichael/assessment/app/db_client"
"github.com/cosimmichael/assessment/app/controllers"
"github.com/gin-gonic/gin"
// you need to import go mod init for this parkage to work
// "github.com/cosimmichael/assessment/app/strutil"
// "github.com/cosimmichael/assessment/app/routers"
// "net/http"
)
func main(){
db_client.InitialiseDBConnection()
r := gin.Default()
r.POST("api/v1/products/create", controller.CreateProducts)
r.GET("api/v1/products/{product_id}/show", controller.GetPosts)
if err := r.Run(":3000"); err != nil {
panic(err.Error())
}
// router.HandleRoutes()
// fmt.Println("Server Starting.. # port :3000")
// http.ListenAndServe(":3000", nil)
}
my code (controller)
package controller
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/cosimmichael/assessment/app/db_client"
// "fmt"
)
type Post struct {
id int64 `json: "id"`
title *string `json: "title"`
description *string `json: "description"`
}
func CreateProducts(c *gin.Context) {
var reqBody Post
if err := c.ShouldBindJSON(&reqBody); err != nil {
c.JSON(http.StatusUnprocessableEntity, gin.H{
"error": true,
"message": "Invalid request body",
})
return
}
res, err := db_client.DBClient.Exec("INSERT INTO products (title, description) VALUES (?, ?);",
reqBody.title,//"testing",
reqBody.description,//"Just testing something",
)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": true,
"message": "Invalid request body2",
})
return
}
id, err := res.LastInsertId()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": true,
"message": "Invalid request body3",
})
return
}
c.JSON(http.StatusCreated, gin.H{
"error": false,
"id": id,
})
}
func GetPosts(c *gin.Context){
var posts []Post
rows, err := db_client.DBClient.Query("SELECT id, title, description FROM products;")
if err != nil {
c.JSON(http.StatusUnprocessableEntity, gin.H{
"error": true,
"message": "Invalid request body",
})
return
}
for rows.Next(){
var singlePost Post
if err := rows.Scan(&singlePost.id, &singlePost.title, &singlePost.description); err != nil {
c.JSON(http.StatusUnprocessableEntity, gin.H{
"error": true,
"message": "Invalid request body",
})
return
}
posts = append(posts, singlePost)
}
c.JSON(http.StatusOK, rows)
}
my code db_client
package db_client
import (
"database/sql"
//"time"
_ "github.com/go-sql-driver/mysql"
)
var DBClient *sql.DB
func InitialiseDBConnection(){
//[username[:password]#][protocol[(address)]]/dbname[?param1=value1&...&paramN=valueN]
db, err := sql.Open("mysql","root:2580#tcp(localhost:3306)/grabit?parseTime=true")
if err != nil {
panic(err.Error())
}
err = db.Ping()
if err != nil {
panic(err.Error())
}
DBClient = db
}
now when I use postman insert new row, it inserts an empty row with only id, no title nor description, when i try fetching, i get an empty array, please what is the problem, i am new to golang
you need to capitalise the first character of values inside struct field.
For Example:
type Book struct {
ID uint `json:"id" gorm:"primary_key"`
Title string `json:"title"`
Author string `json:"author"`
}
Need to use a capitalise letter because if you don't use it you can only see in the same package.
Capitalise letter = see in all package
Normal letter = see only in same package (for example: controller only here)
Using Structs
If a field or method name starts with a capital letter, the member is exported and is accessible outside of the package.
If a field or method starts with a lowercase letter, the member is unexported and does not have accessibility outside of the package.
Note: The Inorder to do the operations like Marshalling Un-marshalling etc in golang json package you need to have field names should start with uppercase letters. Because it uses reflection inside to process.

How to validate API key in go-gin framework?

So I currently have a function that will take in a string APIKey to check it against my MongoDB collection. If nothing is found (not authenticated), it returns false - if a user is found, it returns true. My problem, however, is I'm unsure how to integrate this with a Gin POST route. Here is my code:
import (
"context"
"fmt"
"log"
"os"
"github.com/gin-gonic/gin"
_ "github.com/joho/godotenv/autoload"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
type User struct {
Name string
APIKey string
}
func validateAPIKey(users *mongo.Collection, APIKey string) bool {
var user User
filter := bson.D{primitive.E{Key: "APIKey", Value: APIKey}}
if err := users.FindOne(context.TODO(), filter).Decode(&user); err != nil {
fmt.Printf("Found 0 results for API Key: %s\n", APIKey)
return false
}
fmt.Printf("Found: %s\n", user.Name)
return true
}
func handleUpload(c *gin.Context) {
}
func main() {
r := gin.Default()
api := r.Group("/api")
v1 := api.Group("/v1")
v1.POST("/upload", handleUpload)
mongoURI := os.Getenv("MONGO_URI")
mongoOptions := options.Client().ApplyURI(mongoURI)
client, err := mongo.Connect(context.TODO(), mongoOptions)
if err != nil {
log.Fatal(err, "Unable to access MongoDB server, exiting...")
}
defer client.Disconnect(context.TODO())
// users := client.Database("sharex_api").Collection("authorized_users") // commented out when testing to ignore unused warnings
r.Run(":8085")
}
The validateAPIKey function works exactly as intended if tested alone, I am just unsure how I would run this function for a specific endpoint (in this case, /api/v1/upload) and pass in the users collection.
After a bit of searching, I found a resolution. I changed my validateAPIKey function to return git.HandlerFunc. Here's the code:
func validateAPIKey(users *mongo.Collection) gin.HandlerFunc {
return func(c *gin.Context) {
var user authorizedUser
APIKey := c.Request.Header.Get("X-API-Key")
filter := bson.D{primitive.E{Key: "APIKey", Value: APIKey}}
if err := users.FindOne(context.TODO(), filter).Decode(&user); err != nil {
fmt.Printf("Found 0 results for API Key: %s\n", APIKey)
c.JSON(http.StatusUnauthorized, gin.H{"status": 401, "message": "Authentication failed"})
return
}
return
}
}
For the route, I have the following:
v1.POST("/upload", validateAPIKey(users), handleUpload)

How To Use Embedding To Get Data From A Table

I am a PHP programmer and I have just learned Golang for weeks. I am writing REST API to get post information from my 'posts' table. I have the FindPostByID method in posts_controller. This controller uses Post struct which is model
func (p *Post) FindPostByID(db *gorm.DB, pid uint64) (*Post, error) {
var err error
err = db.Debug().Model(&Post{}).Where("id = ?", pid).Take(&p).Error
if err != nil {
return &Post{}, err
}
if p.ID != 0 {
err = db.Debug().Model(&User{}).Where("id = ?", p.AuthorID).Take(&p.Author).Error
if err != nil {
return &Post{}, err
}
}
return p, nil
}
But now I want to add method FindByID into parent struct, such as ParentModel because when I have articles_controller I can use FindByID method in parent struct to get an article info. So I have the following code in parent struct
type ParentModel struct {
}
func (m *ParentModel) FindByID(db *gorm.DB, uid uint64) (*ParentModel, error) {
var err error
err = db.Debug().Model(ParentModel{}).Where("id = ?", uid).Take(&m).Error
if err != nil {
return &ParentModel{}, err
}
if gorm.IsRecordNotFoundError(err) {
return &ParentModel{}, errors.New("User Not Found")
}
return m, err
}
And I change in the Post struct like following:
type Post struct {
ParentModel
ID uint64 `gorm:"primary_key;auto_increment" json:"id"`
Title string `gorm:"size:255;not null;unique" json:"title"`
Content string `gorm:"size:255;not null;" json:"content"`
Author User `json:"author"`
AuthorID uint32 `sql:"type:int REFERENCES users(id)" json:"author_id"`
CreatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"created_at"`
UpdatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"updated_at"`
}
I change FindPostByID method in posts_controller as well:
func (server *Server) GetPost(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
pid, err := strconv.ParseUint(vars["id"], 10, 64)
if err != nil {
responses.ERROR(w, http.StatusBadRequest, err)
return
}
post := models.Post{}
postReceived, err := post.FindByID(server.DB, pid)
if err != nil {
responses.ERROR(w, http.StatusInternalServerError, err)
return
}
responses.JSON(w, http.StatusOK, postReceived)
}
When I run my program, have the err: Table 'golang_rest_api.parent_models' doesn't exist.
How I can do to use inheritance method like PHP language
The error Table 'golang_rest_api.parent_models' doesn't exist means that no table named parent_models exists in your database.
This is because gorm by default uses the pluralised camelcase name of your struct, which you supplied in gorm's Model() method, as its table's name.
Gorm provides in-built support for many kinds of associations which you can find here. I would also like to ask you to complete your ParentModel struct's definition. It's empty right now.

"unexpected end of JSON input" when an array of objects is passed in POST request

import "github.com/gin-gonic/gin"
func Receive(c *gin.Context) {
// Gets JSON ecnoded data
rawData, err := c.GetRawData()
if err != nil {
return nil, err
}
logger.Info("Raw data received - ", rawData)
}
This code snippet works when I pass a Json object {"key":"value"} but gives an error:
"unexpected end of JSON input"
when I pass an array like [{"key":"val"},{"key": "val"}] as the input.
All GetRawData() does is return stream data, so that shouldn't cause your error:
// GetRawData return stream data.
func (c *Context) GetRawData() ([]byte, error) {
return ioutil.ReadAll(c.Request.Body)
}
However, try using BindJSON and deserialise into a struct. See for example this question.
type List struct {
Messages []string `key:"required"`
}
func Receive(c *gin.Context) {
data := new(List)
err := c.BindJSON(data)
if err != nil {
return nil, err
}
}

Index out of Range with array of structs in Go

I am new to Go so hopefully I'm making myself clear with this issue I'm having. My problem is that I am trying to iterate over an array of structs but I keep running into an index out of range issue. For the purposes of this problem, I have already verified that my array is not empty but that it in fact does contain at least one Services struct and file_content is the string that contains my valid JSON
Here is the snippet of code that represents the problem I'm having:
type service_config struct {
Services []struct {
Name string
Command string
Request map[string]interface{}
}
}
var ServiceConf = service_config{}
err_json := json.Unmarshal(file_content, &ServiceConf)
for _, s := range ServiceConf.Services {
log.Println(s)
}
So every time I run my code I get:
2014/03/14 18:19:53 http: panic serving [::1]:65448: runtime error: index out of range
{
"services" : [
{
"name": "translation",
"command": "to german",
"request": {
"key": "XXX",
"url": "https://www.googleapis.com/language/translate/v2?"
}
}
]
}
If you're interested in the complete source file:
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
)
type SlackResponse struct {
token string
team_id string
channel_id string
channel_name string
timestamp string
user_id string
user_name string
text string
}
type service_config struct {
Services []struct {
Name string
Command string
Request map[string]interface{}
}
}
var ServiceConf = service_config{}
func main() {
content, err_read := ioutil.ReadFile("config.ini")
if err_read != nil {
log.Println("Could not read config")
return
}
log.Println(string(content))
err_json := json.Unmarshal(content, &ServiceConf)
if err_json != nil {
log.Println(err_json)
}
http.HandleFunc("/", handler)
http.ListenAndServe(":"+os.Getenv("PORT"), nil)
}
func handler(w http.ResponseWriter, r *http.Request) {
slack_response := SlackResponse{
r.FormValue("token"),
r.FormValue("team_id"),
r.FormValue("channel_id"),
r.FormValue("channel_name"),
r.FormValue("timestamp"),
r.FormValue("user_id"),
r.FormValue("user_name"),
r.FormValue("text"),
}
// log.Println(ServiceConf.Services[0].Request["key"])
// loop through services to find command phrases
for _, s := range ServiceConf.Services {
log.Println(s)
}
if slack_response.user_name == "slackbot" {
return
}
// fmt.Fprintf(w, "{ \"text\": \"Master %s! You said: '%s'\" }", slack_response.user_name, slack_response.text)
content, err := getContent("https://www.googleapis.com/language/translate/v2?key=&source=en&target=de&q=" + url.QueryEscape(slack_response.text))
if err != nil {
fmt.Fprintf(w, "{ \"text\": \"Huh?!\" }")
} else {
type trans struct {
Data struct {
Translations []struct {
TranslatedText string `json:"translatedText"`
} `json:"translations"`
} `json:"data"`
}
f := trans{}
err := json.Unmarshal(content, &f)
if err != nil {
log.Println(err)
}
fmt.Fprintf(w, "{ \"text\": \"Translated to German you said: '%s'\" }", f.Data.Translations[0].TranslatedText)
}
}
// array of bytes if retrieved successfully.
func getContent(url string) ([]byte, error) {
// Build the request
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
// Send the request via a client
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
// Defer the closing of the body
defer resp.Body.Close()
// Read the content into a byte array
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
// At this point we're done - simply return the bytes
return body, nil
}
Here is the stack trace:
2014/03/21 23:21:29 http: panic serving [::1]:59508: runtime error: index out of range
goroutine 3 [running]:
net/http.funcĀ·009()
/usr/local/Cellar/go/1.2/libexec/src/pkg/net/http/server.go:1093 +0xae
runtime.panic(0x215f80, 0x4b6537)
/usr/local/Cellar/go/1.2/libexec/src/pkg/runtime/panic.c:248 +0x106
main.handler(0x5a85e8, 0xc21000f6e0, 0xc210037dd0)
/Users/et/src/go/src/github.com/etdebruin/gojacques/main.go:100 +0x81b
net/http.HandlerFunc.ServeHTTP(0x2cbc60, 0x5a85e8, 0xc21000f6e0, 0xc210037dd0)
/usr/local/Cellar/go/1.2/libexec/src/pkg/net/http/server.go:1220 +0x40
net/http.(*ServeMux).ServeHTTP(0xc21001e5d0, 0x5a85e8, 0xc21000f6e0, 0xc210037dd0)
/usr/local/Cellar/go/1.2/libexec/src/pkg/net/http/server.go:1496 +0x163
net/http.serverHandler.ServeHTTP(0xc21001f500, 0x5a85e8, 0xc21000f6e0, 0xc210037dd0)
/usr/local/Cellar/go/1.2/libexec/src/pkg/net/http/server.go:1597 +0x16e
net/http.(*conn).serve(0xc210058300)
/usr/local/Cellar/go/1.2/libexec/src/pkg/net/http/server.go:1167 +0x7b7
created by net/http.(*Server).Serve
/usr/local/Cellar/go/1.2/libexec/src/pkg/net/http/server.go:1644 +0x28b
The error comes from this line
fmt.Fprintf(w, "{ \"text\": \"Translated to German you said: '%s'\" }",
f.Data.Translations[0].TranslatedText)
So you didn't get any Translations back - that array is empty.
You might want to check resp.Status to see if an error was returned. This isn't returned as an error - you need to check it yourself.

Resources