In Golang ozzo-validation, how can I validate a field which is dependent on another field ?
For example, if I have the following:
return validation.ValidateStruct(&c,
validation.Field(&c.Name, validation.Required, validation.Length(5, 20)),
validation.Field(&c.Gender, validation.In("Female", "Male")),
validation.Field(&c.Email, is.Email),
validation.Field(&c.Address),
How can I add a validation that the Address is required only if email is not empty?
You can achieve it in two ways-
Adding your own custom rules
Conditionally add FieldRules based on precondition-value i.e check Email while creating field rules then supply it to validation.ValidateStruct
For e.g.:
type Sample struct {
Name string
Gender string
Email string
Address Address
}
type Address struct {
// ... fields
}
func (s Sample) Validate() error {
var fieldRules []*validation.FieldRules
fieldRules = append(fieldRules, validation.Field(&s.Name, validation.Required, validation.Length(5, 20)))
fieldRules = append(fieldRules, validation.Field(&s.Gender, validation.In("Female", "Male")))
fieldRules = append(fieldRules, validation.Field(&s.Email, is.Email))
if len(strings.TrimSpace(s.Email)) > 0 {
fieldRules = append(fieldRules, validation.Field(&s.Address, validation.Required))
fieldRules = append(fieldRules, validation.Field(&s.Address))
}
return validation.ValidateStruct(&s, fieldRules...)
}
The library now supports conditional validation by the validation.When function.
Here is a code snipped which fits the validation you described.
package main
import (
"fmt"
validation "github.com/go-ozzo/ozzo-validation" // or "github.com/go-ozzo/ozzo-validation/v4" if "When" not found
)
type Entry struct {
Name string
Gender string
Email string
Address string
}
func main() {
v := func(e Entry) {
fmt.Println(validation.ValidateStruct(&e,
validation.Field(&e.Name, validation.Required, validation.Length(5, 20)),
// Note that if gender is "" and not required, validation returns no error.
validation.Field(&e.Gender, validation.Required, validation.In("Female", "Male")),
validation.Field(&e.Address, validation.When(e.Email != "", validation.Required.Error("Address is required if Email is set"))),
))
}
// All is fine for no Email.
e := Entry{
Name: "My name is!",
Gender: "Male",
}
v(e)
// Validation fails for Email and no Address.
e = Entry{
Name: "My name is!",
Gender: "Male",
Email: "a#org.com",
}
v(e)
}
It outputs.
<nil>
Address: Address is required if Email is set.
The library documentation describes it as well: https://github.com/go-ozzo/ozzo-validation#conditional-validation
Related
I am working on unit test for a restAPI implementation in Golang.
I need to pass an array of object into url.
Here is an example of struct I have:
type version struct {
Name string `json:"name"`
Ver string `json:"ver"`
}
type event struct {
ID string `json:"id"`
Title string `json:"Title"`
Description string `json:"Description"`
Versions []version `json:"versions"`
}
The sample json input i tested in postman will be look like this one
{
"id": "101",
"title": "This is simple Golang title for testing!",
"Description":"Sample code for REST api implementation in Golang 2021!",
"versions": [
{
"name": "pingPong",
"ver": "10.2"
},
{
"name": "Ninja",
"ver": "10.24"
}
]
}
My question is that how can i pass an array of objects as URL parameters.
I expect to have something like below but not how to fill the ending part i highlighted by the ...
url?ID=20&Title=urlTitle&Description=UrlDescription&...
I don't know how you want the URL like, so I wrote it myself in a way that you can change it any way you want, And let me add that I don't know how many versions you have, so I wrote in such a way that no matter how many versions you have, it can handle it.
package main
import (
"fmt"
"strings"
"encoding/json"
)
var jsonData string =
`{
"id": "101",
"title": "This is simple Golang title for testing!",
"Description":"Sample code for REST api implementation in Golang 2021!",
"versions": [
{
"name": "pingPong",
"ver": "10.2"
},
{
"name": "Ninja",
"ver": "10.24"
}
]
}`
type (
Event struct {
Id string `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Versions []Version `json:"versions"`
}
Version struct {
Name string `json:"name"`
Ver string `json:"ver"`
}
)
func fillVersions(event *Event, baseUrl string) string {
var finalUrl string = baseUrl
for index, value := range event.Versions {
restUrl := fmt.Sprintf("Version%d=%s-%s", index + 1, value.Name, value.Ver)
finalUrl = fmt.Sprintf(
finalUrl + "%s" + "&",
restUrl,
)
}
return strings.TrimRight(finalUrl, "&")
}
func main() {
var event Event
json.Unmarshal([]byte(jsonData), &event)
baseUrl := fmt.Sprintf(
"https://test.com/test?Id=%s&Title=%s&Description=%s&",
event.Id,
event.Title,
event.Description,
)
finalUrl := fillVersions(&event, baseUrl)
fmt.Println(finalUrl)
}
The output of the program is as follows:
https://test.com/test?Id=101&Title=This is simple Golang title for testing!&Description=Sample code for REST api implementation in Golang 2021!&Version1=pingPong-10.2&Version2=Ninja-10.24
I would also like to say that the last & will be removed, If you don't want to do this, Remove the following line and write as follows (also remove the strings library from the import scope):
return strings.TrimRight(finalUrl, "&") // remove this
return finalUrl // add this
type listParams struct {
Status int `form:"status"`
Keyword string `form:"keyword"`
ShowType int `form:"show_type"`
}
func List(c *gin.Context) {
var ReqData listParams
_ = c.ShouldBind(&ReqData)
// I fetch this by PostForm() to check it empy if it equal to empty string
if stat := c.PostForm("status"); stat == "" {
ReqData.Status = -99
}
// .......
}
In this code, How can I know that was front-end post the status or not?
Because of the default value of go, if I check the reqData.Status == 0, it will always return true if the front-end didn't post it, but In my case, 0 is a meaningful value, So I can't check it by equal to 0.
So am I have the others way to check it?
PS: I tried and found out that gorm will not update the field in struct if I don't assign:
var d &User{} // User is a definition of user table
d.ID = 1
d.Name = "Joy"
// d.Status = 1 // It has this field, but I dont assign it
db.Model(&User{}).updates(&d)
Finally, status won't update to 0(In my understanding, d.Status should be 0)
Use a pointer type to circumvent the 0 default value:
type listParams struct {
Status &int `form:"status"`
Keyword string `form:"keyword"`
ShowType int `form:"show_type"`
}
The check if d.Status is nil, otherwise get the associated value
I think this constraint raises the need for having a sole public way to construct User.
type user struct { Status int Keyword string ShowTypeint}
func NewUser() (*user) { return &user{Status: -1} }
That way you ensure that user struct is only constructed through NewUser having Status default value Status always equal to -1.
type Old struct {
UserID int `json:"user_ID"`
Data struct {
Address string `json:"address"`
} `json:"old_data"`
}
type New struct {
UserID int `json:"userId"`
Data struct {
Address string `json:"address"`
} `json:"new_data"`
}
func (old Old) ToNew() New {
return New{
UserID: old.UserID,
Data: { // from here it says missing expression
Address: old.Data.Address,
},
}
}
What is "missing expression" error when using structs?
I am transforming old object to a new one. I minified them just to get straight to the point but the transformation is much more complex. The UserID field for example works great. But when I use struct (which intended to be a JSON object in the end) the Goland IDE screams "missing expression" and the compiler says "missing type in composite literal" on this line. What I am doing wrong? Maybe should I use something else instead of struct? Please help.
Data is an anonymous struct, so you need to write it like this:
type New struct {
UserID int `json:"userId"`
Data struct {
Address string `json:"address"`
} `json:"new_data"`
}
func (old Old) ToNew() New {
return New{
UserID: old.UserID,
Data: struct {
Address string `json:"address"`
}{
Address: old.Data.Address,
},
}
}
(playground link)
I think it'd be cleanest to create a named Address struct.
You're defining Data as an inline struct. When assigning values to it, you must first put the inline declaration:
func (old Old) ToNew() New {
return New{
UserID: old.UserID,
Data: struct {
Address string `json:"address"`
}{
Address: old.Data.Address,
},
}
}
Hence it is generally better to define a separate type for Data, just like User.
So I have an Struct that holds data that has a AddedByUser which links to my User Struct.
What I want to be able to do it remove the UserLevel from the AddedByUser
Now I want to be able to do it from this function only, so using the json:"-" is not an option. That would remove it from all json output. I only want to remove it form this one function.
I should also say that these are Gorm models and when I have been trying to remove the 10 option (UserLevels) it only removes the outer data set not the UserLevel from all of the data.
{
"ID": 1,
"CreatedAt": "2019-01-08T16:33:09.514711Z",
"UpdatedAt": "2019-01-08T16:33:09.514711Z",
"DeletedAt": null,
"UUID": "00000000-0000-0000-0000-000000000000",
"Title": "title000",
"Information": "info999",
"EventDate": "2006-01-02T15:04:05Z",
"AddedByUser": {
"ID": 2,
"CreatedAt": "2019-01-08T15:27:52.435397Z",
"UpdatedAt": "2019-01-08T15:27:52.435397Z",
"DeletedAt": null,
"UUID": "b019df80-a7e4-4397-814a-795e7e84b4ca",
"Firstname": "Me",
"Surname": "admin",
"Password": "....",
"Email": "admin#email.co.uk",
"UserLevel": {
"ID": 0,
"CreatedAt": "0001-01-01T00:00:00Z",
"UpdatedAt": "0001-01-01T00:00:00Z",
"DeletedAt": null,
"LevelTitle": "",
"UserLevel": null
},
So this is what I have tried,
data := []models.MyData{}
data = append(data[0:2])
I have about 14 results, with out the append it loads all the results but with this is only loads two results. The idea was to remove either UpdateAt or Title. As I am not sure if the gorm model information is all 0 or if the slice sees them as 0,1,2,3,4 etc.
I have also tried to range over the slice of models, while I can access each of the sections, I can not seem to find a simple method to remove data by name from a struct? Maps seem to have that but not structs which I am not sure why?
Thanks.
UPDATE
This is the model I am using:
//Model
type MyData struct {
gorm.Model
UUID uuid.UUID
Title string
Information string
EventDate time.Time
AddedByUser Users `gorm:"ForeignKey:added_by_user_fk"`
AddedByUserFK uint
}
//Users Model
type Users struct {
gorm.Model
UUID uuid.UUID
Firstname string
Surname string
Password string
Email string
UserLevel UserLevels `gorm:"ForeignKey:user_level_fk" json:",omitempty"`
UserLevelFK uint
}
As mentioned in the comments, you cannot remove fields from a struct value, because that would yield a value of a different type.
However, you can set fields to their zero value. Combined with the omitempty JSON tag, you can exclude fields from the JSON encoding. To make this work properly, you have to change the UserLevel field to a pointer type (otherwise you end up with empty objects in the JSON document).
Types shortened for brevity:
package main
import (
"encoding/json"
"fmt"
)
type MyData struct {
Title string
AddedByUser Users
}
type Users struct {
ID int
UserLevel *UserLevels `json:",omitempty"` // pointer type with omitempty
}
type UserLevels struct {
LevelTitle string
}
func main() {
var x MyData
x.Title = "foo"
x.AddedByUser.ID = 2
x.AddedByUser.UserLevel = &UserLevels{}
f(x)
b, _ := json.MarshalIndent(x, "", " ")
fmt.Println("main:\n" + string(b))
}
func f(x MyData) {
// "unset" UserLevel. Since we are receiving a copy of MyData, this is
// invisible to the caller.
x.AddedByUser.UserLevel = nil
b, _ := json.MarshalIndent(x, "", " ")
fmt.Println("f:\n" + string(b))
}
// Output:
// f:
// {
// "Title": "foo",
// "AddedByUser": {
// "ID": 2
// }
// }
// main:
// {
// "Title": "foo",
// "AddedByUser": {
// "ID": 2,
// "UserLevel": {
// "LevelTitle": ""
// }
// }
// }
Try it on the playground: https://play.golang.org/p/trUgnYamVOA
Alternatively, you can define new types that exclude the AddedByUser field. However, since this field isn't at the top level, this is a lot of work, and it's easy to forget to update those types when new fields are added to the original types.
If the field were at the top level, the compiler would do most of the work for you, because types that only differ in their field tags can be directly converted to one another:
type MyData struct {
ID int
Title string
}
func main() {
var x MyData
x.ID = 1
x.Title = "foo"
f(x)
}
func f(x MyData) {
type data struct { // same as MyData, except the field tags
ID int
Title string `json:"-"`
}
b, _ := json.MarshalIndent(data(x), "", " ")
fmt.Println("main:\n" + string(b))
}
I'm trying to do validation on my form struct in a method that returns a bool, but I keep getting false even when it should be returning true..
If you look towards the end of the Validate method, you'll see I write validated := len(this.Errors) == 0 which should be making "validated" either true or false based on whether the Errors map has items or not, and then I return validated.
When I fill out my form accurately, there should be no errors yet I still get false when I should be getting true.
Can someone explain? Is this not how Go works?
form.go:
package models
import (
"../config"
"../util"
)
type Form struct {
Name string
Email string
Phone string
Message string
Thanks string
ErrorHandler
}
func (this *Form) Validate() bool {
this.Errors = make(map[string]string)
matched := util.MatchRegexp(".+#.+\\..+", this.Email)
if !util.IsEmpty(this.Email) {
if matched == false {
this.Errors["Email"] = config.EMAIL_INVALID
}
} else {
this.Errors["Email"] = config.EMAIL_EMPTY
}
if util.IsEmpty(this.Name) {
this.Errors["Name"] = config.NAME_EMPTY
}
if util.IsEmpty(this.Phone) {
this.Errors["Phone"] = config.PHONE_EMPTY
}
if util.IsEmpty(this.Message) {
this.Errors["Message"] = config.MESSAGE_EMPTY
}
validated := len(this.Errors) == 0
if validated {
this.Thanks = config.THANK_YOU
}
return validated
}
errorhandler.go:
package models
type ErrorHandler struct {
Errors map[string]string
}
func (this *ErrorHandler) HandleErr(err string) {
this.Errors = make(map[string]string)
this.Errors["Error"] = err
}
And this is where I try to call the Validate method -- in a function in my controller:
form := &models.Form{
Name: r.FormValue("name"),
Email: r.FormValue("email"),
Phone: r.FormValue("phone"),
Message: r.FormValue("message")}
if form.Validate() {
// This never runs because 'form.Validate()' is always false
}
I don't think the util.IsEmpty() is the culprit here.. just checks if the string is empty:
func IsEmpty(str string) bool {
return strings.TrimSpace(str) == ""
}
Any help would be appreciated!
It's best to debug this kind of problem with a log statement like:
log.Printf("form: %v", form)
before calling validate, so it's clear what the input data looks like.
Greetings, Philip