Validate JSON config (#240)
* exposed ValidateNodeConfig() * extended NodeConfig interface with Validate() method * added validate tag to some NodeConfig fields
This commit is contained in:
parent
3b316c0700
commit
7433828a26
|
@ -6,6 +6,8 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
|
||||
"gopkg.in/go-playground/validator.v9"
|
||||
|
||||
"github.com/status-im/status-go/geth/common"
|
||||
"github.com/status-im/status-go/geth/params"
|
||||
)
|
||||
|
@ -42,6 +44,48 @@ func StopNode() *C.char {
|
|||
return makeJSONResponse(err)
|
||||
}
|
||||
|
||||
//export ValidateNodeConfig
|
||||
func ValidateNodeConfig(configJSON *C.char) *C.char {
|
||||
var resp common.APIDetailedResponse
|
||||
|
||||
_, err := params.LoadNodeConfig(C.GoString(configJSON))
|
||||
|
||||
// Convert errors to common.APIDetailedResponse
|
||||
switch err := err.(type) {
|
||||
case validator.ValidationErrors:
|
||||
resp = common.APIDetailedResponse{
|
||||
Message: "validation: validation failed",
|
||||
FieldErrors: make([]common.APIFieldError, len(err)),
|
||||
}
|
||||
|
||||
for i, ve := range err {
|
||||
resp.FieldErrors[i] = common.APIFieldError{
|
||||
Parameter: ve.Namespace(),
|
||||
Errors: []common.APIError{
|
||||
{
|
||||
Message: fmt.Sprintf("field validation failed on the '%s' tag", ve.Tag()),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
case error:
|
||||
resp = common.APIDetailedResponse{
|
||||
Message: fmt.Sprintf("validation: %s", err.Error()),
|
||||
}
|
||||
case nil:
|
||||
resp = common.APIDetailedResponse{
|
||||
Status: true,
|
||||
}
|
||||
}
|
||||
|
||||
respJSON, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
return makeJSONResponse(err)
|
||||
}
|
||||
|
||||
return C.CString(string(respJSON))
|
||||
}
|
||||
|
||||
//export ResetChainData
|
||||
func ResetChainData() *C.char {
|
||||
_, err := statusAPI.ResetChainDataAsync()
|
||||
|
|
|
@ -2,6 +2,9 @@ package main
|
|||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/status-im/status-go/geth/common"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// the actual test functions are in non-_test.go files (so that they can use cgo i.e. import "C")
|
||||
|
@ -12,3 +15,58 @@ func TestExportedAPI(t *testing.T) {
|
|||
|
||||
<-allTestsDone
|
||||
}
|
||||
|
||||
func TestValidateNodeConfig(t *testing.T) {
|
||||
noErrorsCallback := func(resp common.APIDetailedResponse) {
|
||||
require.True(t, resp.Status, "expected status equal true")
|
||||
require.Empty(t, resp.FieldErrors)
|
||||
require.Empty(t, resp.Message)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
Name string
|
||||
Config string
|
||||
Callback func(common.APIDetailedResponse)
|
||||
}{
|
||||
{
|
||||
Name: "response for valid config",
|
||||
Config: `{
|
||||
"NetworkId": 1,
|
||||
"DataDir": "/tmp"
|
||||
}`,
|
||||
Callback: noErrorsCallback,
|
||||
},
|
||||
{
|
||||
Name: "response for invalid JSON string",
|
||||
Config: `{"Network": }`,
|
||||
Callback: func(resp common.APIDetailedResponse) {
|
||||
require.False(t, resp.Status)
|
||||
require.Contains(t, resp.Message, "validation: invalid character '}'")
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "response for config with multiple errors",
|
||||
Config: `{}`,
|
||||
Callback: func(resp common.APIDetailedResponse) {
|
||||
required := map[string]string{
|
||||
"NodeConfig.NetworkID": "required",
|
||||
"NodeConfig.DataDir": "required",
|
||||
}
|
||||
|
||||
require.False(t, resp.Status)
|
||||
require.Contains(t, resp.Message, "validation: validation failed")
|
||||
require.Equal(t, 2, len(resp.FieldErrors))
|
||||
|
||||
for _, err := range resp.FieldErrors {
|
||||
require.Contains(t, required, err.Parameter)
|
||||
require.Contains(t, err.Error(), required[err.Parameter])
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Logf("TestValidateNodeConfig: %s", tc.Name)
|
||||
testValidateNodeConfig(t, tc.Config, tc.Callback)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ import (
|
|||
"github.com/status-im/status-go/geth/params"
|
||||
. "github.com/status-im/status-go/geth/testing"
|
||||
"github.com/status-im/status-go/static"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const zeroHash = "0x0000000000000000000000000000000000000000000000000000000000000000"
|
||||
|
@ -1476,3 +1477,14 @@ func startTestNode(t *testing.T) <-chan struct{} {
|
|||
|
||||
return waitForNodeStart
|
||||
}
|
||||
|
||||
func testValidateNodeConfig(t *testing.T, config string, fn func(common.APIDetailedResponse)) {
|
||||
result := ValidateNodeConfig(C.CString(config))
|
||||
|
||||
var resp common.APIDetailedResponse
|
||||
|
||||
err := json.Unmarshal([]byte(C.GoString(result)), &resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
fn(resp)
|
||||
}
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts"
|
||||
|
@ -204,6 +207,56 @@ type APIResponse struct {
|
|||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
// APIDetailedResponse represents a generic response
|
||||
// with possible errors.
|
||||
type APIDetailedResponse struct {
|
||||
Status bool `json:"status"`
|
||||
Message string `json:"message,omitempty"`
|
||||
FieldErrors []APIFieldError `json:"field_errors,omitempty"`
|
||||
}
|
||||
|
||||
func (r APIDetailedResponse) Error() string {
|
||||
buf := bytes.NewBufferString("")
|
||||
|
||||
for _, err := range r.FieldErrors {
|
||||
buf.WriteString(err.Error())
|
||||
buf.WriteString("\n")
|
||||
}
|
||||
|
||||
return strings.TrimSpace(buf.String())
|
||||
}
|
||||
|
||||
// APIFieldError represents a set of errors
|
||||
// related to a parameter.
|
||||
type APIFieldError struct {
|
||||
Parameter string `json:"parameter,omitempty"`
|
||||
Errors []APIError `json:"errors"`
|
||||
}
|
||||
|
||||
func (e APIFieldError) Error() string {
|
||||
if len(e.Errors) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
buf := bytes.NewBufferString(fmt.Sprintf("Parameter: %s\n", e.Parameter))
|
||||
|
||||
for _, err := range e.Errors {
|
||||
buf.WriteString(err.Error())
|
||||
buf.WriteString("\n")
|
||||
}
|
||||
|
||||
return strings.TrimSpace(buf.String())
|
||||
}
|
||||
|
||||
// APIError represents a single error.
|
||||
type APIError struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func (e APIError) Error() string {
|
||||
return fmt.Sprintf("message=%s", e.Message)
|
||||
}
|
||||
|
||||
// AccountInfo represents account's info
|
||||
type AccountInfo struct {
|
||||
Address string `json:"address"`
|
||||
|
|
|
@ -215,10 +215,10 @@ type NodeConfig struct {
|
|||
DevMode bool
|
||||
|
||||
// NetworkID sets network to use for selecting peers to connect to
|
||||
NetworkID uint64 `json:"NetworkId,"`
|
||||
NetworkID uint64 `json:"NetworkId" validate:"required"`
|
||||
|
||||
// DataDir is the file system folder the node should use for any data storage needs.
|
||||
DataDir string
|
||||
DataDir string `validate:"required"`
|
||||
|
||||
// KeyStoreDir is the file system folder that contains private keys.
|
||||
// If KeyStoreDir is empty, the default location is the "keystore" subdirectory of DataDir.
|
||||
|
@ -230,7 +230,7 @@ type NodeConfig struct {
|
|||
NodeKeyFile string
|
||||
|
||||
// Name sets the instance name of the node. It must not contain the / character.
|
||||
Name string
|
||||
Name string `validate:"excludes=/"`
|
||||
|
||||
// Version exposes program's version. It is used in the devp2p node identifier.
|
||||
Version string
|
||||
|
@ -282,22 +282,22 @@ type NodeConfig struct {
|
|||
LogFile string
|
||||
|
||||
// LogLevel defines minimum log level. Valid names are "ERROR", "WARNING", "INFO", "DEBUG", and "TRACE".
|
||||
LogLevel string
|
||||
LogLevel string `validate:"eq=ERROR|eq=WARNING|eq=INFO|eq=DEBUG|eq=TRACE"`
|
||||
|
||||
// LogToStderr defines whether logged info should also be output to os.Stderr
|
||||
LogToStderr bool
|
||||
|
||||
// BootClusterConfig extra configuration for supporting cluster
|
||||
BootClusterConfig *BootClusterConfig `json:"BootClusterConfig,"`
|
||||
BootClusterConfig *BootClusterConfig `json:"BootClusterConfig," validate:"structonly"`
|
||||
|
||||
// LightEthConfig extra configuration for LES
|
||||
LightEthConfig *LightEthConfig `json:"LightEthConfig,"`
|
||||
LightEthConfig *LightEthConfig `json:"LightEthConfig," validate:"structonly"`
|
||||
|
||||
// WhisperConfig extra configuration for SHH
|
||||
WhisperConfig *WhisperConfig `json:"WhisperConfig,"`
|
||||
WhisperConfig *WhisperConfig `json:"WhisperConfig," validate:"structonly"`
|
||||
|
||||
// SwarmConfig extra configuration for Swarm and ENS
|
||||
SwarmConfig *SwarmConfig `json:"SwarmConfig,"`
|
||||
SwarmConfig *SwarmConfig `json:"SwarmConfig," validate:"structonly"`
|
||||
}
|
||||
|
||||
// NewNodeConfig creates new node configuration object
|
||||
|
@ -350,13 +350,25 @@ func NewNodeConfig(dataDir string, networkID uint64, devMode bool) (*NodeConfig,
|
|||
|
||||
// LoadNodeConfig parses incoming JSON and returned it as Config
|
||||
func LoadNodeConfig(configJSON string) (*NodeConfig, error) {
|
||||
nodeConfig, err := loadNodeConfig(configJSON)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := nodeConfig.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nodeConfig, nil
|
||||
}
|
||||
|
||||
func loadNodeConfig(configJSON string) (*NodeConfig, error) {
|
||||
nodeConfig, err := NewNodeConfig("", 0, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(strings.NewReader(configJSON))
|
||||
//decoder.UseNumber()
|
||||
|
||||
// override default configuration with values by JSON input
|
||||
if err := decoder.Decode(&nodeConfig); err != nil {
|
||||
|
@ -368,17 +380,58 @@ func LoadNodeConfig(configJSON string) (*NodeConfig, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if len(nodeConfig.DataDir) == 0 {
|
||||
return nil, ErrMissingDataDir
|
||||
}
|
||||
|
||||
if nodeConfig.NetworkID <= 0 {
|
||||
return nil, ErrMissingNetworkID
|
||||
}
|
||||
|
||||
return nodeConfig, nil
|
||||
}
|
||||
|
||||
// Validate checks if NodeConfig fields have valid values.
|
||||
//
|
||||
// It returns nil if there are no errors, otherwise one or more errors
|
||||
// can be returned. Multiple errors are joined with a new line.
|
||||
//
|
||||
// A single error for a struct:
|
||||
//
|
||||
// type TestStruct struct {
|
||||
// TestField string `validate:"required"`
|
||||
// }
|
||||
//
|
||||
// has the following format:
|
||||
//
|
||||
// Key: 'TestStruct.TestField' Error:Field validation for 'TestField' failed on the 'required' tag
|
||||
//
|
||||
func (c *NodeConfig) Validate() error {
|
||||
validate := NewValidator()
|
||||
|
||||
if err := validate.Struct(c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.BootClusterConfig.Enabled {
|
||||
if err := validate.Struct(c.BootClusterConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if c.LightEthConfig.Enabled {
|
||||
if err := validate.Struct(c.LightEthConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if c.WhisperConfig.Enabled {
|
||||
if err := validate.Struct(c.WhisperConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if c.SwarmConfig.Enabled {
|
||||
if err := validate.Struct(c.SwarmConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Save dumps configuration to the disk
|
||||
func (c *NodeConfig) Save() error {
|
||||
data, err := json.MarshalIndent(c, "", " ")
|
||||
|
|
|
@ -7,7 +7,8 @@ import (
|
|||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"fmt"
|
||||
|
||||
"gopkg.in/go-playground/validator.v9"
|
||||
|
||||
"github.com/ethereum/go-ethereum/core"
|
||||
gethparams "github.com/ethereum/go-ethereum/params"
|
||||
|
@ -22,40 +23,13 @@ var loadConfigTestCases = []struct {
|
|||
validator func(t *testing.T, dataDir string, nodeConfig *params.NodeConfig, err error)
|
||||
}{
|
||||
{
|
||||
`invalid input configuration`,
|
||||
`invalid input JSON (missing comma at the end of key:value pair)`,
|
||||
`{
|
||||
"NetworkId": 3
|
||||
"DataDir": "$TMPDIR",
|
||||
"Name": "TestStatusNode",
|
||||
"WSPort": 8546,
|
||||
"IPCEnabled": true,
|
||||
"WSEnabled": false,
|
||||
"RPCEnabled": false,
|
||||
"LightEthConfig": {
|
||||
"DatabaseCache": 64
|
||||
}
|
||||
}`,
|
||||
func(t *testing.T, dataDir string, nodeConfig *params.NodeConfig, err error) {
|
||||
require.Error(t, err, "error is expected, not thrown")
|
||||
},
|
||||
},
|
||||
{
|
||||
`missing required field (DataDir)`,
|
||||
`{
|
||||
"NetworkId": 3,
|
||||
"Name": "TestStatusNode"
|
||||
}`,
|
||||
func(t *testing.T, dataDir string, nodeConfig *params.NodeConfig, err error) {
|
||||
require.Equal(t, params.ErrMissingDataDir, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
`missing required field (NetworkId)`,
|
||||
`{
|
||||
"DataDir": "$TMPDIR"
|
||||
}`,
|
||||
func(t *testing.T, dataDir string, nodeConfig *params.NodeConfig, err error) {
|
||||
require.Equal(t, params.ErrMissingNetworkID, err)
|
||||
require.Error(t, err, "error is expected, not thrown")
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -406,6 +380,7 @@ var loadConfigTestCases = []struct {
|
|||
},
|
||||
}
|
||||
|
||||
// TestLoadNodeConfig tests loading JSON configuration and setting default values.
|
||||
func TestLoadNodeConfig(t *testing.T) {
|
||||
tmpDir, err := ioutil.TempDir(os.TempDir(), "geth-config-tests")
|
||||
if err != nil {
|
||||
|
@ -456,3 +431,75 @@ func TestConfigWriteRead(t *testing.T) {
|
|||
configReadWrite(params.RopstenNetworkID, "testdata/config.ropsten.json")
|
||||
configReadWrite(params.MainNetworkID, "testdata/config.mainnet.json")
|
||||
}
|
||||
|
||||
// TestNodeConfigValidate checks validation of individual fields.
|
||||
func TestNodeConfigValidate(t *testing.T) {
|
||||
testCases := []struct {
|
||||
Name string
|
||||
Config string
|
||||
Error string
|
||||
FieldErrors map[string]string // map[Field]Tag
|
||||
}{
|
||||
{
|
||||
Name: "Valid JSON config",
|
||||
Config: `{
|
||||
"NetworkId": 1,
|
||||
"DataDir": "/tmp/data"
|
||||
}`,
|
||||
Error: "",
|
||||
FieldErrors: nil,
|
||||
},
|
||||
{
|
||||
Name: "Invalid JSON config",
|
||||
Config: `{"NetworkId": }`,
|
||||
Error: "invalid character '}'",
|
||||
FieldErrors: nil,
|
||||
},
|
||||
{
|
||||
Name: "Invalid field type",
|
||||
Config: `{"NetworkId": "abc"}`,
|
||||
Error: "json: cannot unmarshal string into Go struct field",
|
||||
FieldErrors: nil,
|
||||
},
|
||||
{
|
||||
Name: "Validate all required fields",
|
||||
Config: `{}`,
|
||||
Error: "",
|
||||
FieldErrors: map[string]string{
|
||||
"NetworkID": "required",
|
||||
"DataDir": "required",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Validate Name does not contain slash",
|
||||
Config: `{
|
||||
"NetworkId": 1,
|
||||
"DataDir": "/some/dir",
|
||||
"Name": "invalid/name"
|
||||
}`,
|
||||
Error: "",
|
||||
FieldErrors: map[string]string{
|
||||
"Name": "excludes",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Logf("Test Case %s", tc.Name)
|
||||
|
||||
_, err := params.LoadNodeConfig(tc.Config)
|
||||
|
||||
switch err := err.(type) {
|
||||
case validator.ValidationErrors:
|
||||
for _, ve := range err {
|
||||
require.Contains(t, tc.FieldErrors, ve.Field())
|
||||
require.Equal(t, tc.FieldErrors[ve.Field()], ve.Tag())
|
||||
}
|
||||
case error:
|
||||
require.Contains(t, err.Error(), tc.Error)
|
||||
case nil:
|
||||
require.Empty(t, tc.Error)
|
||||
require.Nil(t, tc.FieldErrors)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
package params
|
||||
|
||||
import (
|
||||
"gopkg.in/go-playground/validator.v9"
|
||||
)
|
||||
|
||||
// NewValidator returns a new validator.Validate.
|
||||
func NewValidator() *validator.Validate {
|
||||
return validator.New()
|
||||
}
|
|
@ -0,0 +1,306 @@
|
|||
package currency
|
||||
|
||||
// Type is the currency type associated with the locales currency enum
|
||||
type Type int
|
||||
|
||||
// locale currencies
|
||||
const (
|
||||
ADP Type = iota
|
||||
AED
|
||||
AFA
|
||||
AFN
|
||||
ALK
|
||||
ALL
|
||||
AMD
|
||||
ANG
|
||||
AOA
|
||||
AOK
|
||||
AON
|
||||
AOR
|
||||
ARA
|
||||
ARL
|
||||
ARM
|
||||
ARP
|
||||
ARS
|
||||
ATS
|
||||
AUD
|
||||
AWG
|
||||
AZM
|
||||
AZN
|
||||
BAD
|
||||
BAM
|
||||
BAN
|
||||
BBD
|
||||
BDT
|
||||
BEC
|
||||
BEF
|
||||
BEL
|
||||
BGL
|
||||
BGM
|
||||
BGN
|
||||
BGO
|
||||
BHD
|
||||
BIF
|
||||
BMD
|
||||
BND
|
||||
BOB
|
||||
BOL
|
||||
BOP
|
||||
BOV
|
||||
BRB
|
||||
BRC
|
||||
BRE
|
||||
BRL
|
||||
BRN
|
||||
BRR
|
||||
BRZ
|
||||
BSD
|
||||
BTN
|
||||
BUK
|
||||
BWP
|
||||
BYB
|
||||
BYN
|
||||
BYR
|
||||
BZD
|
||||
CAD
|
||||
CDF
|
||||
CHE
|
||||
CHF
|
||||
CHW
|
||||
CLE
|
||||
CLF
|
||||
CLP
|
||||
CNX
|
||||
CNY
|
||||
COP
|
||||
COU
|
||||
CRC
|
||||
CSD
|
||||
CSK
|
||||
CUC
|
||||
CUP
|
||||
CVE
|
||||
CYP
|
||||
CZK
|
||||
DDM
|
||||
DEM
|
||||
DJF
|
||||
DKK
|
||||
DOP
|
||||
DZD
|
||||
ECS
|
||||
ECV
|
||||
EEK
|
||||
EGP
|
||||
ERN
|
||||
ESA
|
||||
ESB
|
||||
ESP
|
||||
ETB
|
||||
EUR
|
||||
FIM
|
||||
FJD
|
||||
FKP
|
||||
FRF
|
||||
GBP
|
||||
GEK
|
||||
GEL
|
||||
GHC
|
||||
GHS
|
||||
GIP
|
||||
GMD
|
||||
GNF
|
||||
GNS
|
||||
GQE
|
||||
GRD
|
||||
GTQ
|
||||
GWE
|
||||
GWP
|
||||
GYD
|
||||
HKD
|
||||
HNL
|
||||
HRD
|
||||
HRK
|
||||
HTG
|
||||
HUF
|
||||
IDR
|
||||
IEP
|
||||
ILP
|
||||
ILR
|
||||
ILS
|
||||
INR
|
||||
IQD
|
||||
IRR
|
||||
ISJ
|
||||
ISK
|
||||
ITL
|
||||
JMD
|
||||
JOD
|
||||
JPY
|
||||
KES
|
||||
KGS
|
||||
KHR
|
||||
KMF
|
||||
KPW
|
||||
KRH
|
||||
KRO
|
||||
KRW
|
||||
KWD
|
||||
KYD
|
||||
KZT
|
||||
LAK
|
||||
LBP
|
||||
LKR
|
||||
LRD
|
||||
LSL
|
||||
LTL
|
||||
LTT
|
||||
LUC
|
||||
LUF
|
||||
LUL
|
||||
LVL
|
||||
LVR
|
||||
LYD
|
||||
MAD
|
||||
MAF
|
||||
MCF
|
||||
MDC
|
||||
MDL
|
||||
MGA
|
||||
MGF
|
||||
MKD
|
||||
MKN
|
||||
MLF
|
||||
MMK
|
||||
MNT
|
||||
MOP
|
||||
MRO
|
||||
MTL
|
||||
MTP
|
||||
MUR
|
||||
MVP
|
||||
MVR
|
||||
MWK
|
||||
MXN
|
||||
MXP
|
||||
MXV
|
||||
MYR
|
||||
MZE
|
||||
MZM
|
||||
MZN
|
||||
NAD
|
||||
NGN
|
||||
NIC
|
||||
NIO
|
||||
NLG
|
||||
NOK
|
||||
NPR
|
||||
NZD
|
||||
OMR
|
||||
PAB
|
||||
PEI
|
||||
PEN
|
||||
PES
|
||||
PGK
|
||||
PHP
|
||||
PKR
|
||||
PLN
|
||||
PLZ
|
||||
PTE
|
||||
PYG
|
||||
QAR
|
||||
RHD
|
||||
ROL
|
||||
RON
|
||||
RSD
|
||||
RUB
|
||||
RUR
|
||||
RWF
|
||||
SAR
|
||||
SBD
|
||||
SCR
|
||||
SDD
|
||||
SDG
|
||||
SDP
|
||||
SEK
|
||||
SGD
|
||||
SHP
|
||||
SIT
|
||||
SKK
|
||||
SLL
|
||||
SOS
|
||||
SRD
|
||||
SRG
|
||||
SSP
|
||||
STD
|
||||
SUR
|
||||
SVC
|
||||
SYP
|
||||
SZL
|
||||
THB
|
||||
TJR
|
||||
TJS
|
||||
TMM
|
||||
TMT
|
||||
TND
|
||||
TOP
|
||||
TPE
|
||||
TRL
|
||||
TRY
|
||||
TTD
|
||||
TWD
|
||||
TZS
|
||||
UAH
|
||||
UAK
|
||||
UGS
|
||||
UGX
|
||||
USD
|
||||
USN
|
||||
USS
|
||||
UYI
|
||||
UYP
|
||||
UYU
|
||||
UZS
|
||||
VEB
|
||||
VEF
|
||||
VND
|
||||
VNN
|
||||
VUV
|
||||
WST
|
||||
XAF
|
||||
XAG
|
||||
XAU
|
||||
XBA
|
||||
XBB
|
||||
XBC
|
||||
XBD
|
||||
XCD
|
||||
XDR
|
||||
XEU
|
||||
XFO
|
||||
XFU
|
||||
XOF
|
||||
XPD
|
||||
XPF
|
||||
XPT
|
||||
XRE
|
||||
XSU
|
||||
XTS
|
||||
XUA
|
||||
XXX
|
||||
YDD
|
||||
YER
|
||||
YUD
|
||||
YUM
|
||||
YUN
|
||||
YUR
|
||||
ZAL
|
||||
ZAR
|
||||
ZMK
|
||||
ZMW
|
||||
ZRN
|
||||
ZRZ
|
||||
ZWD
|
||||
ZWL
|
||||
ZWR
|
||||
)
|
|
@ -0,0 +1,293 @@
|
|||
package locales
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/go-playground/locales/currency"
|
||||
)
|
||||
|
||||
// // ErrBadNumberValue is returned when the number passed for
|
||||
// // plural rule determination cannot be parsed
|
||||
// type ErrBadNumberValue struct {
|
||||
// NumberValue string
|
||||
// InnerError error
|
||||
// }
|
||||
|
||||
// // Error returns ErrBadNumberValue error string
|
||||
// func (e *ErrBadNumberValue) Error() string {
|
||||
// return fmt.Sprintf("Invalid Number Value '%s' %s", e.NumberValue, e.InnerError)
|
||||
// }
|
||||
|
||||
// var _ error = new(ErrBadNumberValue)
|
||||
|
||||
// PluralRule denotes the type of plural rules
|
||||
type PluralRule int
|
||||
|
||||
// PluralRule's
|
||||
const (
|
||||
PluralRuleUnknown PluralRule = iota
|
||||
PluralRuleZero // zero
|
||||
PluralRuleOne // one - singular
|
||||
PluralRuleTwo // two - dual
|
||||
PluralRuleFew // few - paucal
|
||||
PluralRuleMany // many - also used for fractions if they have a separate class
|
||||
PluralRuleOther // other - required—general plural form—also used if the language only has a single form
|
||||
)
|
||||
|
||||
const (
|
||||
pluralsString = "UnknownZeroOneTwoFewManyOther"
|
||||
)
|
||||
|
||||
// Translator encapsulates an instance of a locale
|
||||
// NOTE: some values are returned as a []byte just in case the caller
|
||||
// wishes to add more and can help avoid allocations; otherwise just cast as string
|
||||
type Translator interface {
|
||||
|
||||
// The following Functions are for overriding, debugging or developing
|
||||
// with a Translator Locale
|
||||
|
||||
// Locale returns the string value of the translator
|
||||
Locale() string
|
||||
|
||||
// returns an array of cardinal plural rules associated
|
||||
// with this translator
|
||||
PluralsCardinal() []PluralRule
|
||||
|
||||
// returns an array of ordinal plural rules associated
|
||||
// with this translator
|
||||
PluralsOrdinal() []PluralRule
|
||||
|
||||
// returns an array of range plural rules associated
|
||||
// with this translator
|
||||
PluralsRange() []PluralRule
|
||||
|
||||
// returns the cardinal PluralRule given 'num' and digits/precision of 'v' for locale
|
||||
CardinalPluralRule(num float64, v uint64) PluralRule
|
||||
|
||||
// returns the ordinal PluralRule given 'num' and digits/precision of 'v' for locale
|
||||
OrdinalPluralRule(num float64, v uint64) PluralRule
|
||||
|
||||
// returns the ordinal PluralRule given 'num1', 'num2' and digits/precision of 'v1' and 'v2' for locale
|
||||
RangePluralRule(num1 float64, v1 uint64, num2 float64, v2 uint64) PluralRule
|
||||
|
||||
// returns the locales abbreviated month given the 'month' provided
|
||||
MonthAbbreviated(month time.Month) string
|
||||
|
||||
// returns the locales abbreviated months
|
||||
MonthsAbbreviated() []string
|
||||
|
||||
// returns the locales narrow month given the 'month' provided
|
||||
MonthNarrow(month time.Month) string
|
||||
|
||||
// returns the locales narrow months
|
||||
MonthsNarrow() []string
|
||||
|
||||
// returns the locales wide month given the 'month' provided
|
||||
MonthWide(month time.Month) string
|
||||
|
||||
// returns the locales wide months
|
||||
MonthsWide() []string
|
||||
|
||||
// returns the locales abbreviated weekday given the 'weekday' provided
|
||||
WeekdayAbbreviated(weekday time.Weekday) string
|
||||
|
||||
// returns the locales abbreviated weekdays
|
||||
WeekdaysAbbreviated() []string
|
||||
|
||||
// returns the locales narrow weekday given the 'weekday' provided
|
||||
WeekdayNarrow(weekday time.Weekday) string
|
||||
|
||||
// WeekdaysNarrowreturns the locales narrow weekdays
|
||||
WeekdaysNarrow() []string
|
||||
|
||||
// returns the locales short weekday given the 'weekday' provided
|
||||
WeekdayShort(weekday time.Weekday) string
|
||||
|
||||
// returns the locales short weekdays
|
||||
WeekdaysShort() []string
|
||||
|
||||
// returns the locales wide weekday given the 'weekday' provided
|
||||
WeekdayWide(weekday time.Weekday) string
|
||||
|
||||
// returns the locales wide weekdays
|
||||
WeekdaysWide() []string
|
||||
|
||||
// The following Functions are common Formatting functionsfor the Translator's Locale
|
||||
|
||||
// returns 'num' with digits/precision of 'v' for locale and handles both Whole and Real numbers based on 'v'
|
||||
FmtNumber(num float64, v uint64) string
|
||||
|
||||
// returns 'num' with digits/precision of 'v' for locale and handles both Whole and Real numbers based on 'v'
|
||||
// NOTE: 'num' passed into FmtPercent is assumed to be in percent already
|
||||
FmtPercent(num float64, v uint64) string
|
||||
|
||||
// returns the currency representation of 'num' with digits/precision of 'v' for locale
|
||||
FmtCurrency(num float64, v uint64, currency currency.Type) string
|
||||
|
||||
// returns the currency representation of 'num' with digits/precision of 'v' for locale
|
||||
// in accounting notation.
|
||||
FmtAccounting(num float64, v uint64, currency currency.Type) string
|
||||
|
||||
// returns the short date representation of 't' for locale
|
||||
FmtDateShort(t time.Time) string
|
||||
|
||||
// returns the medium date representation of 't' for locale
|
||||
FmtDateMedium(t time.Time) string
|
||||
|
||||
// returns the long date representation of 't' for locale
|
||||
FmtDateLong(t time.Time) string
|
||||
|
||||
// returns the full date representation of 't' for locale
|
||||
FmtDateFull(t time.Time) string
|
||||
|
||||
// returns the short time representation of 't' for locale
|
||||
FmtTimeShort(t time.Time) string
|
||||
|
||||
// returns the medium time representation of 't' for locale
|
||||
FmtTimeMedium(t time.Time) string
|
||||
|
||||
// returns the long time representation of 't' for locale
|
||||
FmtTimeLong(t time.Time) string
|
||||
|
||||
// returns the full time representation of 't' for locale
|
||||
FmtTimeFull(t time.Time) string
|
||||
}
|
||||
|
||||
// String returns the string value of PluralRule
|
||||
func (p PluralRule) String() string {
|
||||
|
||||
switch p {
|
||||
case PluralRuleZero:
|
||||
return pluralsString[7:11]
|
||||
case PluralRuleOne:
|
||||
return pluralsString[11:14]
|
||||
case PluralRuleTwo:
|
||||
return pluralsString[14:17]
|
||||
case PluralRuleFew:
|
||||
return pluralsString[17:20]
|
||||
case PluralRuleMany:
|
||||
return pluralsString[20:24]
|
||||
case PluralRuleOther:
|
||||
return pluralsString[24:]
|
||||
default:
|
||||
return pluralsString[:7]
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Precision Notes:
|
||||
//
|
||||
// must specify a precision >= 0, and here is why https://play.golang.org/p/LyL90U0Vyh
|
||||
//
|
||||
// v := float64(3.141)
|
||||
// i := float64(int64(v))
|
||||
//
|
||||
// fmt.Println(v - i)
|
||||
//
|
||||
// or
|
||||
//
|
||||
// s := strconv.FormatFloat(v-i, 'f', -1, 64)
|
||||
// fmt.Println(s)
|
||||
//
|
||||
// these will not print what you'd expect: 0.14100000000000001
|
||||
// and so this library requires a precision to be specified, or
|
||||
// inaccurate plural rules could be applied.
|
||||
//
|
||||
//
|
||||
//
|
||||
// n - absolute value of the source number (integer and decimals).
|
||||
// i - integer digits of n.
|
||||
// v - number of visible fraction digits in n, with trailing zeros.
|
||||
// w - number of visible fraction digits in n, without trailing zeros.
|
||||
// f - visible fractional digits in n, with trailing zeros.
|
||||
// t - visible fractional digits in n, without trailing zeros.
|
||||
//
|
||||
//
|
||||
// Func(num float64, v uint64) // v = digits/precision and prevents -1 as a special case as this can lead to very unexpected behaviour, see precision note's above.
|
||||
//
|
||||
// n := math.Abs(num)
|
||||
// i := int64(n)
|
||||
// v := v
|
||||
//
|
||||
//
|
||||
// w := strconv.FormatFloat(num-float64(i), 'f', int(v), 64) // then parse backwards on string until no more zero's....
|
||||
// f := strconv.FormatFloat(n, 'f', int(v), 64) // then turn everything after decimal into an int64
|
||||
// t := strconv.FormatFloat(n, 'f', int(v), 64) // then parse backwards on string until no more zero's....
|
||||
//
|
||||
//
|
||||
//
|
||||
// General Inclusion Rules
|
||||
// - v will always be available inherently
|
||||
// - all require n
|
||||
// - w requires i
|
||||
//
|
||||
|
||||
// W returns the number of visible fraction digits in N, without trailing zeros.
|
||||
func W(n float64, v uint64) (w int64) {
|
||||
|
||||
s := strconv.FormatFloat(n-float64(int64(n)), 'f', int(v), 64)
|
||||
|
||||
// with either be '0' or '0.xxxx', so if 1 then w will be zero
|
||||
// otherwise need to parse
|
||||
if len(s) != 1 {
|
||||
|
||||
s = s[2:]
|
||||
end := len(s) + 1
|
||||
|
||||
for i := end; i >= 0; i-- {
|
||||
if s[i] != '0' {
|
||||
end = i + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
w = int64(len(s[:end]))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// F returns the visible fractional digits in N, with trailing zeros.
|
||||
func F(n float64, v uint64) (f int64) {
|
||||
|
||||
s := strconv.FormatFloat(n-float64(int64(n)), 'f', int(v), 64)
|
||||
|
||||
// with either be '0' or '0.xxxx', so if 1 then f will be zero
|
||||
// otherwise need to parse
|
||||
if len(s) != 1 {
|
||||
|
||||
// ignoring error, because it can't fail as we generated
|
||||
// the string internally from a real number
|
||||
f, _ = strconv.ParseInt(s[2:], 10, 64)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// T returns the visible fractional digits in N, without trailing zeros.
|
||||
func T(n float64, v uint64) (t int64) {
|
||||
|
||||
s := strconv.FormatFloat(n-float64(int64(n)), 'f', int(v), 64)
|
||||
|
||||
// with either be '0' or '0.xxxx', so if 1 then t will be zero
|
||||
// otherwise need to parse
|
||||
if len(s) != 1 {
|
||||
|
||||
s = s[2:]
|
||||
end := len(s) + 1
|
||||
|
||||
for i := end; i >= 0; i-- {
|
||||
if s[i] != '0' {
|
||||
end = i + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// ignoring error, because it can't fail as we generated
|
||||
// the string internally from a real number
|
||||
t, _ = strconv.ParseInt(s[:end], 10, 64)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
*.test
|
||||
*.prof
|
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Go Playground
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,89 @@
|
|||
## universal-translator
|
||||
<img align="right" src="https://raw.githubusercontent.com/go-playground/universal-translator/master/logo.png">![Project status](https://img.shields.io/badge/version-0.16.0-green.svg)
|
||||
[![Build Status](https://semaphoreci.com/api/v1/joeybloggs/universal-translator/branches/master/badge.svg)](https://semaphoreci.com/joeybloggs/universal-translator)
|
||||
[![Coverage Status](https://coveralls.io/repos/github/go-playground/universal-translator/badge.svg)](https://coveralls.io/github/go-playground/universal-translator)
|
||||
[![Go Report Card](https://goreportcard.com/badge/github.com/go-playground/universal-translator)](https://goreportcard.com/report/github.com/go-playground/universal-translator)
|
||||
[![GoDoc](https://godoc.org/github.com/go-playground/universal-translator?status.svg)](https://godoc.org/github.com/go-playground/universal-translator)
|
||||
![License](https://img.shields.io/dub/l/vibe-d.svg)
|
||||
[![Gitter](https://badges.gitter.im/go-playground/universal-translator.svg)](https://gitter.im/go-playground/universal-translator?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||
|
||||
Universal Translator is an i18n Translator for Go/Golang using CLDR data + pluralization rules
|
||||
|
||||
Why another i18n library?
|
||||
--------------------------
|
||||
Because none of the plural rules seem to be correct out there, including the previous implementation of this package,
|
||||
so I took it upon myself to create [locales](https://github.com/go-playground/locales) for everyone to use; this package
|
||||
is a thin wrapper around [locales](https://github.com/go-playground/locales) in order to store and translate text for
|
||||
use in your applications.
|
||||
|
||||
Features
|
||||
--------
|
||||
- [x] Rules generated from the [CLDR](http://cldr.unicode.org/index/downloads) data, v30.0.3
|
||||
- [x] Contains Cardinal, Ordinal and Range Plural Rules
|
||||
- [x] Contains Month, Weekday and Timezone translations built in
|
||||
- [x] Contains Date & Time formatting functions
|
||||
- [x] Contains Number, Currency, Accounting and Percent formatting functions
|
||||
- [x] Supports the "Gregorian" calendar only ( my time isn't unlimited, had to draw the line somewhere )
|
||||
- [x] Support loading translations from files
|
||||
- [x] Exporting translations to file(s), mainly for getting them professionally translated
|
||||
- [ ] Code Generation for translation files -> Go code.. i.e. after it has been professionally translated
|
||||
- [ ] Tests for all languages, I need help with this, please see [here](https://github.com/go-playground/locales/issues/1)
|
||||
|
||||
Installation
|
||||
-----------
|
||||
|
||||
Use go get
|
||||
|
||||
```shell
|
||||
go get github.com/go-playground/universal-translator
|
||||
```
|
||||
|
||||
Usage & Documentation
|
||||
-------
|
||||
|
||||
Please see https://godoc.org/github.com/go-playground/universal-translator for usage docs
|
||||
|
||||
##### Examples:
|
||||
|
||||
- [Basic](https://github.com/go-playground/universal-translator/tree/master/examples/basic)
|
||||
- [Full - no files](https://github.com/go-playground/universal-translator/tree/master/examples/full-no-files)
|
||||
- [Full - with files](https://github.com/go-playground/universal-translator/tree/master/examples/full-with-files)
|
||||
|
||||
File formatting
|
||||
--------------
|
||||
All types, Plain substitution, Cardinal, Ordinal and Range translations can all be contained withing the same file(s);
|
||||
they are only separated for easy viewing.
|
||||
|
||||
##### Examples:
|
||||
|
||||
- [Formats](https://github.com/go-playground/universal-translator/tree/master/examples/file-formats)
|
||||
|
||||
##### Basic Makeup
|
||||
NOTE: not all fields are needed for all translation types, see [examples](https://github.com/go-playground/universal-translator/tree/master/examples/file-formats)
|
||||
```json
|
||||
{
|
||||
"locale": "en",
|
||||
"key": "days-left",
|
||||
"trans": "You have {0} day left.",
|
||||
"type": "Cardinal",
|
||||
"rule": "One",
|
||||
"override": false
|
||||
}
|
||||
```
|
||||
|Field|Description|
|
||||
|---|---|
|
||||
|locale|The locale for which the translation is for.|
|
||||
|key|The translation key that will be used to store and lookup each translation; normally it is a string or integer.|
|
||||
|trans|The actual translation text.|
|
||||
|type|The type of translation Cardinal, Ordinal, Range or "" for a plain substitution(not required to be defined if plain used)|
|
||||
|rule|The plural rule for which the translation is for eg. One, Two, Few, Many or Other.(not required to be defined if plain used)|
|
||||
|override|If you wish to override an existing translation that has already been registered, set this to 'true'. 99% of the time there is no need to define it.|
|
||||
|
||||
Help With Tests
|
||||
---------------
|
||||
To anyone interesting in helping or contributing, I sure could use some help creating tests for each language.
|
||||
Please see issue [here](https://github.com/go-playground/locales/issues/1) for details.
|
||||
|
||||
License
|
||||
------
|
||||
Distributed under MIT License, please see license file in code for more details.
|
|
@ -0,0 +1,148 @@
|
|||
package ut
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/go-playground/locales"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrUnknowTranslation indicates the translation could not be found
|
||||
ErrUnknowTranslation = errors.New("Unknown Translation")
|
||||
)
|
||||
|
||||
var _ error = new(ErrConflictingTranslation)
|
||||
var _ error = new(ErrRangeTranslation)
|
||||
var _ error = new(ErrOrdinalTranslation)
|
||||
var _ error = new(ErrCardinalTranslation)
|
||||
var _ error = new(ErrMissingPluralTranslation)
|
||||
var _ error = new(ErrExistingTranslator)
|
||||
|
||||
// ErrExistingTranslator is the error representing a conflicting translator
|
||||
type ErrExistingTranslator struct {
|
||||
locale string
|
||||
}
|
||||
|
||||
// Error returns ErrExistingTranslator's internal error text
|
||||
func (e *ErrExistingTranslator) Error() string {
|
||||
return fmt.Sprintf("error: conflicting translator for locale '%s'", e.locale)
|
||||
}
|
||||
|
||||
// ErrConflictingTranslation is the error representing a conflicting translation
|
||||
type ErrConflictingTranslation struct {
|
||||
locale string
|
||||
key interface{}
|
||||
rule locales.PluralRule
|
||||
text string
|
||||
}
|
||||
|
||||
// Error returns ErrConflictingTranslation's internal error text
|
||||
func (e *ErrConflictingTranslation) Error() string {
|
||||
|
||||
if _, ok := e.key.(string); !ok {
|
||||
return fmt.Sprintf("error: conflicting key '%#v' rule '%s' with text '%s' for locale '%s', value being ignored", e.key, e.rule, e.text, e.locale)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("error: conflicting key '%s' rule '%s' with text '%s' for locale '%s', value being ignored", e.key, e.rule, e.text, e.locale)
|
||||
}
|
||||
|
||||
// ErrRangeTranslation is the error representing a range translation error
|
||||
type ErrRangeTranslation struct {
|
||||
text string
|
||||
}
|
||||
|
||||
// Error returns ErrRangeTranslation's internal error text
|
||||
func (e *ErrRangeTranslation) Error() string {
|
||||
return e.text
|
||||
}
|
||||
|
||||
// ErrOrdinalTranslation is the error representing an ordinal translation error
|
||||
type ErrOrdinalTranslation struct {
|
||||
text string
|
||||
}
|
||||
|
||||
// Error returns ErrOrdinalTranslation's internal error text
|
||||
func (e *ErrOrdinalTranslation) Error() string {
|
||||
return e.text
|
||||
}
|
||||
|
||||
// ErrCardinalTranslation is the error representing a cardinal translation error
|
||||
type ErrCardinalTranslation struct {
|
||||
text string
|
||||
}
|
||||
|
||||
// Error returns ErrCardinalTranslation's internal error text
|
||||
func (e *ErrCardinalTranslation) Error() string {
|
||||
return e.text
|
||||
}
|
||||
|
||||
// ErrMissingPluralTranslation is the error signifying a missing translation given
|
||||
// the locales plural rules.
|
||||
type ErrMissingPluralTranslation struct {
|
||||
locale string
|
||||
key interface{}
|
||||
rule locales.PluralRule
|
||||
translationType string
|
||||
}
|
||||
|
||||
// Error returns ErrMissingPluralTranslation's internal error text
|
||||
func (e *ErrMissingPluralTranslation) Error() string {
|
||||
|
||||
if _, ok := e.key.(string); !ok {
|
||||
return fmt.Sprintf("error: missing '%s' plural rule '%s' for translation with key '%#v' and locale '%s'", e.translationType, e.rule, e.key, e.locale)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("error: missing '%s' plural rule '%s' for translation with key '%s' and locale '%s'", e.translationType, e.rule, e.key, e.locale)
|
||||
}
|
||||
|
||||
// ErrMissingBracket is the error representing a missing bracket in a translation
|
||||
// eg. This is a {0 <-- missing ending '}'
|
||||
type ErrMissingBracket struct {
|
||||
locale string
|
||||
key interface{}
|
||||
text string
|
||||
}
|
||||
|
||||
// Error returns ErrMissingBracket error message
|
||||
func (e *ErrMissingBracket) Error() string {
|
||||
return fmt.Sprintf("error: missing bracket '{}', in translation. locale: '%s' key: '%v' text: '%s'", e.locale, e.key, e.text)
|
||||
}
|
||||
|
||||
// ErrBadParamSyntax is the error representing a bad parameter definition in a translation
|
||||
// eg. This is a {must-be-int}
|
||||
type ErrBadParamSyntax struct {
|
||||
locale string
|
||||
param string
|
||||
key interface{}
|
||||
text string
|
||||
}
|
||||
|
||||
// Error returns ErrBadParamSyntax error message
|
||||
func (e *ErrBadParamSyntax) Error() string {
|
||||
return fmt.Sprintf("error: bad parameter syntax, missing parameter '%s' in translation. locale: '%s' key: '%v' text: '%s'", e.param, e.locale, e.key, e.text)
|
||||
}
|
||||
|
||||
// import/export errors
|
||||
|
||||
// ErrMissingLocale is the error representing an expected locale that could
|
||||
// not be found aka locale not registered with the UniversalTranslator Instance
|
||||
type ErrMissingLocale struct {
|
||||
locale string
|
||||
}
|
||||
|
||||
// Error returns ErrMissingLocale's internal error text
|
||||
func (e *ErrMissingLocale) Error() string {
|
||||
return fmt.Sprintf("error: locale '%s' not registered.", e.locale)
|
||||
}
|
||||
|
||||
// ErrBadPluralDefinition is the error representing an incorrect plural definition
|
||||
// usually found within translations defined within files during the import process.
|
||||
type ErrBadPluralDefinition struct {
|
||||
tl translation
|
||||
}
|
||||
|
||||
// Error returns ErrBadPluralDefinition's internal error text
|
||||
func (e *ErrBadPluralDefinition) Error() string {
|
||||
return fmt.Sprintf("error: bad plural definition '%#v'", e.tl)
|
||||
}
|
70
vendor/github.com/go-playground/universal-translator/examples/basic/main.go
generated
vendored
Normal file
70
vendor/github.com/go-playground/universal-translator/examples/basic/main.go
generated
vendored
Normal file
|
@ -0,0 +1,70 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-playground/locales"
|
||||
"github.com/go-playground/locales/en"
|
||||
"github.com/go-playground/locales/en_CA"
|
||||
"github.com/go-playground/locales/fr"
|
||||
"github.com/go-playground/locales/nl"
|
||||
"github.com/go-playground/universal-translator"
|
||||
)
|
||||
|
||||
// only one instance as translators within are shared + goroutine safe
|
||||
var universalTraslator *ut.UniversalTranslator
|
||||
|
||||
func main() {
|
||||
|
||||
// NOTE: this example is omitting a lot of error checking for brevity
|
||||
e := en.New()
|
||||
universalTraslator = ut.New(e, e, en_CA.New(), nl.New(), fr.New())
|
||||
|
||||
en, _ := universalTraslator.GetTranslator("en")
|
||||
|
||||
// generally used after parsing an http 'Accept-Language' header
|
||||
// and this will try to find a matching locale you support or
|
||||
// fallback locale.
|
||||
// en, _ := ut.FindTranslator([]string{"en", "en_CA", "nl"})
|
||||
|
||||
// this will help
|
||||
fmt.Println("Cardinal Plural Rules:", en.PluralsCardinal())
|
||||
fmt.Println("Ordinal Plural Rules:", en.PluralsOrdinal())
|
||||
fmt.Println("Range Plural Rules:", en.PluralsRange())
|
||||
|
||||
// add basic language only translations
|
||||
// last param indicates if it's ok to override the translation if one already exists
|
||||
en.Add("welcome", "Welcome {0} to our test", false)
|
||||
|
||||
// add language translations dependant on cardinal plural rules
|
||||
en.AddCardinal("days", "You have {0} day left to register", locales.PluralRuleOne, false)
|
||||
en.AddCardinal("days", "You have {0} days left to register", locales.PluralRuleOther, false)
|
||||
|
||||
// add language translations dependant on ordinal plural rules
|
||||
en.AddOrdinal("day-of-month", "{0}st", locales.PluralRuleOne, false)
|
||||
en.AddOrdinal("day-of-month", "{0}nd", locales.PluralRuleTwo, false)
|
||||
en.AddOrdinal("day-of-month", "{0}rd", locales.PluralRuleFew, false)
|
||||
en.AddOrdinal("day-of-month", "{0}th", locales.PluralRuleOther, false)
|
||||
|
||||
// add language translations dependant on range plural rules
|
||||
// NOTE: only one plural rule for range in 'en' locale
|
||||
en.AddRange("between", "It's {0}-{1} days away", locales.PluralRuleOther, false)
|
||||
|
||||
// now lets use the translations we just added, in the same order we added them
|
||||
|
||||
fmt.Println(en.T("welcome", "Joeybloggs"))
|
||||
|
||||
fmt.Println(en.C("days", 1, 0, en.FmtNumber(1, 0))) // you'd normally have variables defined for 1 and 0
|
||||
fmt.Println(en.C("days", 2, 0, en.FmtNumber(2, 0)))
|
||||
fmt.Println(en.C("days", 10456.25, 2, en.FmtNumber(10456.25, 2)))
|
||||
|
||||
fmt.Println(en.O("day-of-month", 1, 0, en.FmtNumber(1, 0)))
|
||||
fmt.Println(en.O("day-of-month", 2, 0, en.FmtNumber(2, 0)))
|
||||
fmt.Println(en.O("day-of-month", 3, 0, en.FmtNumber(3, 0)))
|
||||
fmt.Println(en.O("day-of-month", 4, 0, en.FmtNumber(4, 0)))
|
||||
fmt.Println(en.O("day-of-month", 10456.25, 0, en.FmtNumber(10456.25, 0)))
|
||||
|
||||
fmt.Println(en.R("between", 0, 0, 1, 0, en.FmtNumber(0, 0), en.FmtNumber(1, 0)))
|
||||
fmt.Println(en.R("between", 1, 0, 2, 0, en.FmtNumber(1, 0), en.FmtNumber(2, 0)))
|
||||
fmt.Println(en.R("between", 1, 0, 100, 0, en.FmtNumber(1, 0), en.FmtNumber(100, 0)))
|
||||
}
|
16
vendor/github.com/go-playground/universal-translator/examples/file-formats/cardinal.json
generated
vendored
Normal file
16
vendor/github.com/go-playground/universal-translator/examples/file-formats/cardinal.json
generated
vendored
Normal file
|
@ -0,0 +1,16 @@
|
|||
[
|
||||
{
|
||||
"locale": "en",
|
||||
"key": "cardinal_test",
|
||||
"trans": "You have {0} day left.",
|
||||
"type": "Cardinal",
|
||||
"rule": "One"
|
||||
},
|
||||
{
|
||||
"locale": "en",
|
||||
"key": "cardinal_test",
|
||||
"trans": "You have {0} days left.",
|
||||
"type": "Cardinal",
|
||||
"rule": "Other"
|
||||
}
|
||||
]
|
30
vendor/github.com/go-playground/universal-translator/examples/file-formats/ordinal.json
generated
vendored
Normal file
30
vendor/github.com/go-playground/universal-translator/examples/file-formats/ordinal.json
generated
vendored
Normal file
|
@ -0,0 +1,30 @@
|
|||
[
|
||||
{
|
||||
"locale": "en",
|
||||
"key": "day",
|
||||
"trans": "{0}st",
|
||||
"type": "Ordinal",
|
||||
"rule": "One"
|
||||
},
|
||||
{
|
||||
"locale": "en",
|
||||
"key": "day",
|
||||
"trans": "{0}nd",
|
||||
"type": "Ordinal",
|
||||
"rule": "Two"
|
||||
},
|
||||
{
|
||||
"locale": "en",
|
||||
"key": "day",
|
||||
"trans": "{0}rd",
|
||||
"type": "Ordinal",
|
||||
"rule": "Few"
|
||||
},
|
||||
{
|
||||
"locale": "en",
|
||||
"key": "day",
|
||||
"trans": "{0}th",
|
||||
"type": "Ordinal",
|
||||
"rule": "Other"
|
||||
}
|
||||
]
|
27
vendor/github.com/go-playground/universal-translator/examples/file-formats/plain-substitution.json
generated
vendored
Normal file
27
vendor/github.com/go-playground/universal-translator/examples/file-formats/plain-substitution.json
generated
vendored
Normal file
|
@ -0,0 +1,27 @@
|
|||
[
|
||||
{
|
||||
"locale": "en",
|
||||
"key": "test_trans4",
|
||||
"trans": "{0}{1}"
|
||||
},
|
||||
{
|
||||
"locale": "en",
|
||||
"key": "test_trans",
|
||||
"trans": "Welcome {0} to the {1}."
|
||||
},
|
||||
{
|
||||
"locale": "en",
|
||||
"key": -1,
|
||||
"trans": "Welcome {0}"
|
||||
},
|
||||
{
|
||||
"locale": "en",
|
||||
"key": "test_trans2",
|
||||
"trans": "{0} to the {1}."
|
||||
},
|
||||
{
|
||||
"locale": "en",
|
||||
"key": "test_trans3",
|
||||
"trans": "Welcome {0} to the {1}"
|
||||
}
|
||||
]
|
16
vendor/github.com/go-playground/universal-translator/examples/file-formats/range.json
generated
vendored
Normal file
16
vendor/github.com/go-playground/universal-translator/examples/file-formats/range.json
generated
vendored
Normal file
|
@ -0,0 +1,16 @@
|
|||
[
|
||||
{
|
||||
"locale": "nl",
|
||||
"key": "day",
|
||||
"trans": "er {0}-{1} dag vertrokken",
|
||||
"type": "Range",
|
||||
"rule": "One"
|
||||
},
|
||||
{
|
||||
"locale": "nl",
|
||||
"key": "day",
|
||||
"trans": "er zijn {0}-{1} dagen over",
|
||||
"type": "Range",
|
||||
"rule": "Other"
|
||||
}
|
||||
]
|
35
vendor/github.com/go-playground/universal-translator/examples/full-no-files/home.tmpl
generated
vendored
Normal file
35
vendor/github.com/go-playground/universal-translator/examples/full-no-files/home.tmpl
generated
vendored
Normal file
|
@ -0,0 +1,35 @@
|
|||
{{ define "home" }}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Home</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Locale: {{ .Trans.Locale }}</p>
|
||||
<p>Trans1: {{ .Trans.C "days-left" 1 0 "1" }}</p>
|
||||
<p>Trans2: {{ .Trans.C "days-left" 2 0 "2" }}</p>
|
||||
<p>FmtNumber Positive: {{ .Trans.FmtNumber .PositiveNum 2 }}</p>
|
||||
<p>FmtNumber Negative: {{ .Trans.FmtNumber .NegativeNum 2 }}</p>
|
||||
<p>FmtPercent Negative: {{ .Trans.FmtPercent .Percent 2 }}</p>
|
||||
<p>FmtCurrency Negative: {{ .Trans.FmtCurrency .PositiveNum 2 .Trans.Currency }}</p>
|
||||
<p>FmtCurrency Negative: {{ .Trans.FmtCurrency .NegativeNum 2 .Trans.Currency }}</p>
|
||||
<p>FmtAccounting Negative: {{ .Trans.FmtAccounting .PositiveNum 2 .Trans.Currency }}</p>
|
||||
<p>FmtAccounting Negative: {{ .Trans.FmtAccounting .NegativeNum 2 .Trans.Currency }}</p>
|
||||
<p>FmtDateShort: {{ .Trans.FmtDateShort .Now }}</p>
|
||||
<p>FmtDateMedium: {{ .Trans.FmtDateMedium .Now }}</p>
|
||||
<p>FmtDateLong: {{ .Trans.FmtDateLong .Now }}</p>
|
||||
<p>FmtDateFull: {{ .Trans.FmtDateFull .Now }}</p>
|
||||
<p>FmtTimeShort: {{ .Trans.FmtTimeShort .Now }}</p>
|
||||
<p>FmtTimeMedium: {{ .Trans.FmtTimeMedium .Now }}</p>
|
||||
<p>FmtTimeLong: {{ .Trans.FmtTimeLong .Now }}</p>
|
||||
<p>FmtTimeFull: {{ .Trans.FmtTimeFull .Now }}</p>
|
||||
<p>MonthsAbbreviated: {{ .Trans.MonthsAbbreviated }}</p>
|
||||
<p>MonthsNarrow: {{ .Trans.MonthsNarrow }}</p>
|
||||
<p>MonthsWide: {{ .Trans.MonthsWide }}</p>
|
||||
<p>WeekdaysAbbreviated: {{ .Trans.WeekdaysAbbreviated }}</p>
|
||||
<p>WeekdaysNarrow: {{ .Trans.WeekdaysNarrow }}</p>
|
||||
<p>WeekdaysShort: {{ .Trans.WeekdaysShort }}</p>
|
||||
<p>WeekdaysWide: {{ .Trans.WeekdaysWide }}</p>
|
||||
</body>
|
||||
</html>
|
||||
{{ end }}
|
210
vendor/github.com/go-playground/universal-translator/examples/full-no-files/main.go
generated
vendored
Normal file
210
vendor/github.com/go-playground/universal-translator/examples/full-no-files/main.go
generated
vendored
Normal file
|
@ -0,0 +1,210 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-playground/locales"
|
||||
"github.com/go-playground/locales/currency"
|
||||
"github.com/go-playground/locales/en"
|
||||
"github.com/go-playground/locales/fr"
|
||||
"github.com/go-playground/pure"
|
||||
"github.com/go-playground/pure/examples/middleware/logging-recovery"
|
||||
"github.com/go-playground/universal-translator"
|
||||
)
|
||||
|
||||
var (
|
||||
tmpls *template.Template
|
||||
utrans *ut.UniversalTranslator
|
||||
transKey = struct {
|
||||
name string
|
||||
}{
|
||||
name: "transKey",
|
||||
}
|
||||
)
|
||||
|
||||
// Translator wraps ut.Translator in order to handle errors transparently
|
||||
// it is totally optional but recommended as it can now be used directly in
|
||||
// templates and nobody can add translations where they're not supposed to.
|
||||
type Translator interface {
|
||||
locales.Translator
|
||||
|
||||
// creates the translation for the locale given the 'key' and params passed in.
|
||||
// wraps ut.Translator.T to handle errors
|
||||
T(key interface{}, params ...string) string
|
||||
|
||||
// creates the cardinal translation for the locale given the 'key', 'num' and 'digit' arguments
|
||||
// and param passed in.
|
||||
// wraps ut.Translator.C to handle errors
|
||||
C(key interface{}, num float64, digits uint64, param string) string
|
||||
|
||||
// creates the ordinal translation for the locale given the 'key', 'num' and 'digit' arguments
|
||||
// and param passed in.
|
||||
// wraps ut.Translator.O to handle errors
|
||||
O(key interface{}, num float64, digits uint64, param string) string
|
||||
|
||||
// creates the range translation for the locale given the 'key', 'num1', 'digit1', 'num2' and
|
||||
// 'digit2' arguments and 'param1' and 'param2' passed in
|
||||
// wraps ut.Translator.R to handle errors
|
||||
R(key interface{}, num1 float64, digits1 uint64, num2 float64, digits2 uint64, param1, param2 string) string
|
||||
|
||||
// Currency returns the type used by the given locale.
|
||||
Currency() currency.Type
|
||||
}
|
||||
|
||||
// implements Translator interface definition above.
|
||||
type translator struct {
|
||||
locales.Translator
|
||||
trans ut.Translator
|
||||
}
|
||||
|
||||
var _ Translator = (*translator)(nil)
|
||||
|
||||
func (t *translator) T(key interface{}, params ...string) string {
|
||||
|
||||
s, err := t.trans.T(key, params...)
|
||||
if err != nil {
|
||||
log.Printf("issue translating key: '%v' error: '%s'", key, err)
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (t *translator) C(key interface{}, num float64, digits uint64, param string) string {
|
||||
|
||||
s, err := t.trans.C(key, num, digits, param)
|
||||
if err != nil {
|
||||
log.Printf("issue translating cardinal key: '%v' error: '%s'", key, err)
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (t *translator) O(key interface{}, num float64, digits uint64, param string) string {
|
||||
|
||||
s, err := t.trans.C(key, num, digits, param)
|
||||
if err != nil {
|
||||
log.Printf("issue translating ordinal key: '%v' error: '%s'", key, err)
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (t *translator) R(key interface{}, num1 float64, digits1 uint64, num2 float64, digits2 uint64, param1, param2 string) string {
|
||||
|
||||
s, err := t.trans.R(key, num1, digits1, num2, digits2, param1, param2)
|
||||
if err != nil {
|
||||
log.Printf("issue translating range key: '%v' error: '%s'", key, err)
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (t *translator) Currency() currency.Type {
|
||||
|
||||
// choose your own locale. The reason it isn't mapped for you is because many
|
||||
// countries have multiple currencies; it's up to you and you're application how
|
||||
// and which currencies to use. I recommend adding a function it to to your custon translator
|
||||
// interface like defined above.
|
||||
switch t.Locale() {
|
||||
case "en":
|
||||
return currency.USD
|
||||
case "fr":
|
||||
return currency.EUR
|
||||
default:
|
||||
return currency.USD
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
en := en.New()
|
||||
utrans = ut.New(en, en, fr.New())
|
||||
setup()
|
||||
|
||||
tmpls, _ = template.ParseFiles("home.tmpl")
|
||||
|
||||
r := pure.New()
|
||||
r.Use(middleware.LoggingAndRecovery(true), translatorMiddleware)
|
||||
r.Get("/", home)
|
||||
|
||||
log.Println("Running on Port :8080")
|
||||
log.Println("Try me with URL http://localhost:8080/?locale=en")
|
||||
log.Println("and then http://localhost:8080/?locale=fr")
|
||||
http.ListenAndServe(":8080", r.Serve())
|
||||
}
|
||||
|
||||
func home(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// get locale translator ( could be wrapped into a helper function )
|
||||
t := r.Context().Value(transKey).(Translator)
|
||||
|
||||
s := struct {
|
||||
Trans Translator
|
||||
Now time.Time
|
||||
PositiveNum float64
|
||||
NegativeNum float64
|
||||
Percent float64
|
||||
}{
|
||||
Trans: t,
|
||||
Now: time.Now(),
|
||||
PositiveNum: 1234576.45,
|
||||
NegativeNum: -35900394.34,
|
||||
Percent: 96.76,
|
||||
}
|
||||
|
||||
if err := tmpls.ExecuteTemplate(w, "home", s); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func translatorMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// there are many ways to check, this is just checking for query param &
|
||||
// Accept-Language header but can be expanded to Cookie's etc....
|
||||
|
||||
params := r.URL.Query()
|
||||
|
||||
locale := params.Get("locale")
|
||||
var t ut.Translator
|
||||
|
||||
if len(locale) > 0 {
|
||||
|
||||
var found bool
|
||||
|
||||
if t, found = utrans.GetTranslator(locale); found {
|
||||
goto END
|
||||
}
|
||||
}
|
||||
|
||||
// get and parse the "Accept-Language" http header and return an array
|
||||
t, _ = utrans.FindTranslator(pure.AcceptedLanguages(r)...)
|
||||
END:
|
||||
// I would normally wrap ut.Translator with one with my own functions in order
|
||||
// to handle errors and be able to use all functions from translator within the templates.
|
||||
r = r.WithContext(context.WithValue(r.Context(), transKey, &translator{trans: t, Translator: t.(locales.Translator)}))
|
||||
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func setup() {
|
||||
|
||||
en, _ := utrans.FindTranslator("en")
|
||||
en.AddCardinal("days-left", "There is {0} day left", locales.PluralRuleOne, false)
|
||||
en.AddCardinal("days-left", "There are {0} days left", locales.PluralRuleOther, false)
|
||||
|
||||
fr, _ := utrans.FindTranslator("fr")
|
||||
fr.AddCardinal("days-left", "Il reste {0} jour", locales.PluralRuleOne, false)
|
||||
fr.AddCardinal("days-left", "Il reste {0} jours", locales.PluralRuleOther, false)
|
||||
|
||||
err := utrans.VerifyTranslations()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
35
vendor/github.com/go-playground/universal-translator/examples/full-with-files/home.tmpl
generated
vendored
Normal file
35
vendor/github.com/go-playground/universal-translator/examples/full-with-files/home.tmpl
generated
vendored
Normal file
|
@ -0,0 +1,35 @@
|
|||
{{ define "home" }}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Home</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Locale: {{ .Trans.Locale }}</p>
|
||||
<p>Trans1: {{ .Trans.C "days-left" 1 0 "1" }}</p>
|
||||
<p>Trans2: {{ .Trans.C "days-left" 2 0 "2" }}</p>
|
||||
<p>FmtNumber Positive: {{ .Trans.FmtNumber .PositiveNum 2 }}</p>
|
||||
<p>FmtNumber Negative: {{ .Trans.FmtNumber .NegativeNum 2 }}</p>
|
||||
<p>FmtPercent Negative: {{ .Trans.FmtPercent .Percent 2 }}</p>
|
||||
<p>FmtCurrency Negative: {{ .Trans.FmtCurrency .PositiveNum 2 .Trans.Currency }}</p>
|
||||
<p>FmtCurrency Negative: {{ .Trans.FmtCurrency .NegativeNum 2 .Trans.Currency }}</p>
|
||||
<p>FmtAccounting Negative: {{ .Trans.FmtAccounting .PositiveNum 2 .Trans.Currency }}</p>
|
||||
<p>FmtAccounting Negative: {{ .Trans.FmtAccounting .NegativeNum 2 .Trans.Currency }}</p>
|
||||
<p>FmtDateShort: {{ .Trans.FmtDateShort .Now }}</p>
|
||||
<p>FmtDateMedium: {{ .Trans.FmtDateMedium .Now }}</p>
|
||||
<p>FmtDateLong: {{ .Trans.FmtDateLong .Now }}</p>
|
||||
<p>FmtDateFull: {{ .Trans.FmtDateFull .Now }}</p>
|
||||
<p>FmtTimeShort: {{ .Trans.FmtTimeShort .Now }}</p>
|
||||
<p>FmtTimeMedium: {{ .Trans.FmtTimeMedium .Now }}</p>
|
||||
<p>FmtTimeLong: {{ .Trans.FmtTimeLong .Now }}</p>
|
||||
<p>FmtTimeFull: {{ .Trans.FmtTimeFull .Now }}</p>
|
||||
<p>MonthsAbbreviated: {{ .Trans.MonthsAbbreviated }}</p>
|
||||
<p>MonthsNarrow: {{ .Trans.MonthsNarrow }}</p>
|
||||
<p>MonthsWide: {{ .Trans.MonthsWide }}</p>
|
||||
<p>WeekdaysAbbreviated: {{ .Trans.WeekdaysAbbreviated }}</p>
|
||||
<p>WeekdaysNarrow: {{ .Trans.WeekdaysNarrow }}</p>
|
||||
<p>WeekdaysShort: {{ .Trans.WeekdaysShort }}</p>
|
||||
<p>WeekdaysWide: {{ .Trans.WeekdaysWide }}</p>
|
||||
</body>
|
||||
</html>
|
||||
{{ end }}
|
207
vendor/github.com/go-playground/universal-translator/examples/full-with-files/main.go
generated
vendored
Normal file
207
vendor/github.com/go-playground/universal-translator/examples/full-with-files/main.go
generated
vendored
Normal file
|
@ -0,0 +1,207 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-playground/locales"
|
||||
"github.com/go-playground/locales/currency"
|
||||
"github.com/go-playground/locales/en"
|
||||
"github.com/go-playground/locales/fr"
|
||||
"github.com/go-playground/pure"
|
||||
"github.com/go-playground/pure/examples/middleware/logging-recovery"
|
||||
"github.com/go-playground/universal-translator"
|
||||
)
|
||||
|
||||
var (
|
||||
tmpls *template.Template
|
||||
utrans *ut.UniversalTranslator
|
||||
transKey = struct {
|
||||
name string
|
||||
}{
|
||||
name: "transKey",
|
||||
}
|
||||
)
|
||||
|
||||
// Translator wraps ut.Translator in order to handle errors transparently
|
||||
// it is totally optional but recommended as it can now be used directly in
|
||||
// templates and nobody can add translations where they're not supposed to.
|
||||
type Translator interface {
|
||||
locales.Translator
|
||||
|
||||
// creates the translation for the locale given the 'key' and params passed in.
|
||||
// wraps ut.Translator.T to handle errors
|
||||
T(key interface{}, params ...string) string
|
||||
|
||||
// creates the cardinal translation for the locale given the 'key', 'num' and 'digit' arguments
|
||||
// and param passed in.
|
||||
// wraps ut.Translator.C to handle errors
|
||||
C(key interface{}, num float64, digits uint64, param string) string
|
||||
|
||||
// creates the ordinal translation for the locale given the 'key', 'num' and 'digit' arguments
|
||||
// and param passed in.
|
||||
// wraps ut.Translator.O to handle errors
|
||||
O(key interface{}, num float64, digits uint64, param string) string
|
||||
|
||||
// creates the range translation for the locale given the 'key', 'num1', 'digit1', 'num2' and
|
||||
// 'digit2' arguments and 'param1' and 'param2' passed in
|
||||
// wraps ut.Translator.R to handle errors
|
||||
R(key interface{}, num1 float64, digits1 uint64, num2 float64, digits2 uint64, param1, param2 string) string
|
||||
|
||||
// Currency returns the type used by the given locale.
|
||||
Currency() currency.Type
|
||||
}
|
||||
|
||||
// implements Translator interface definition above.
|
||||
type translator struct {
|
||||
locales.Translator
|
||||
trans ut.Translator
|
||||
}
|
||||
|
||||
var _ Translator = (*translator)(nil)
|
||||
|
||||
func (t *translator) T(key interface{}, params ...string) string {
|
||||
|
||||
s, err := t.trans.T(key, params...)
|
||||
if err != nil {
|
||||
log.Printf("issue translating key: '%v' error: '%s'", key, err)
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (t *translator) C(key interface{}, num float64, digits uint64, param string) string {
|
||||
|
||||
s, err := t.trans.C(key, num, digits, param)
|
||||
if err != nil {
|
||||
log.Printf("issue translating cardinal key: '%v' error: '%s'", key, err)
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (t *translator) O(key interface{}, num float64, digits uint64, param string) string {
|
||||
|
||||
s, err := t.trans.C(key, num, digits, param)
|
||||
if err != nil {
|
||||
log.Printf("issue translating ordinal key: '%v' error: '%s'", key, err)
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (t *translator) R(key interface{}, num1 float64, digits1 uint64, num2 float64, digits2 uint64, param1, param2 string) string {
|
||||
|
||||
s, err := t.trans.R(key, num1, digits1, num2, digits2, param1, param2)
|
||||
if err != nil {
|
||||
log.Printf("issue translating range key: '%v' error: '%s'", key, err)
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (t *translator) Currency() currency.Type {
|
||||
|
||||
// choose your own locale. The reason it isn't mapped for you is because many
|
||||
// countries have multiple currencies; it's up to you and you're application how
|
||||
// and which currencies to use. I recommend adding a function it to to your custon translator
|
||||
// interface like defined above.
|
||||
switch t.Locale() {
|
||||
case "en":
|
||||
return currency.USD
|
||||
case "fr":
|
||||
return currency.EUR
|
||||
default:
|
||||
return currency.USD
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
en := en.New()
|
||||
utrans = ut.New(en, en, fr.New())
|
||||
setup()
|
||||
|
||||
tmpls, _ = template.ParseFiles("home.tmpl")
|
||||
|
||||
r := pure.New()
|
||||
r.Use(middleware.LoggingAndRecovery(true), translatorMiddleware)
|
||||
r.Get("/", home)
|
||||
|
||||
log.Println("Running on Port :8080")
|
||||
log.Println("Try me with URL http://localhost:8080/?locale=en")
|
||||
log.Println("and then http://localhost:8080/?locale=fr")
|
||||
http.ListenAndServe(":8080", r.Serve())
|
||||
}
|
||||
|
||||
func home(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// get locale translator ( could be wrapped into a helper function )
|
||||
t := r.Context().Value(transKey).(Translator)
|
||||
|
||||
s := struct {
|
||||
Trans Translator
|
||||
Now time.Time
|
||||
PositiveNum float64
|
||||
NegativeNum float64
|
||||
Percent float64
|
||||
}{
|
||||
Trans: t,
|
||||
Now: time.Now(),
|
||||
PositiveNum: 1234576.45,
|
||||
NegativeNum: -35900394.34,
|
||||
Percent: 96.76,
|
||||
}
|
||||
|
||||
if err := tmpls.ExecuteTemplate(w, "home", s); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func translatorMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// there are many ways to check, this is just checking for query param &
|
||||
// Accept-Language header but can be expanded to Cookie's etc....
|
||||
|
||||
params := r.URL.Query()
|
||||
|
||||
locale := params.Get("locale")
|
||||
var t ut.Translator
|
||||
|
||||
if len(locale) > 0 {
|
||||
|
||||
var found bool
|
||||
|
||||
if t, found = utrans.GetTranslator(locale); found {
|
||||
goto END
|
||||
}
|
||||
}
|
||||
|
||||
// get and parse the "Accept-Language" http header and return an array
|
||||
t, _ = utrans.FindTranslator(pure.AcceptedLanguages(r)...)
|
||||
END:
|
||||
// I would normally wrap ut.Translator with one with my own functions in order
|
||||
// to handle errors and be able to use all functions from translator within the templates.
|
||||
r = r.WithContext(context.WithValue(r.Context(), transKey, &translator{trans: t, Translator: t.(locales.Translator)}))
|
||||
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func setup() {
|
||||
|
||||
err := utrans.Import(ut.FormatJSON, "translations")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = utrans.VerifyTranslations()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
16
vendor/github.com/go-playground/universal-translator/examples/full-with-files/translations/en/home.json
generated
vendored
Normal file
16
vendor/github.com/go-playground/universal-translator/examples/full-with-files/translations/en/home.json
generated
vendored
Normal file
|
@ -0,0 +1,16 @@
|
|||
[
|
||||
{
|
||||
"locale": "en",
|
||||
"key": "days-left",
|
||||
"trans": "There is {0} day left",
|
||||
"type": "Cardinal",
|
||||
"rule": "One"
|
||||
},
|
||||
{
|
||||
"locale": "en",
|
||||
"key": "days-left",
|
||||
"trans": "There are {0} days left",
|
||||
"type": "Cardinal",
|
||||
"rule": "Other"
|
||||
}
|
||||
]
|
16
vendor/github.com/go-playground/universal-translator/examples/full-with-files/translations/fr/home.json
generated
vendored
Normal file
16
vendor/github.com/go-playground/universal-translator/examples/full-with-files/translations/fr/home.json
generated
vendored
Normal file
|
@ -0,0 +1,16 @@
|
|||
[
|
||||
{
|
||||
"locale": "fr",
|
||||
"key": "days-left",
|
||||
"trans": "Il reste {0} jour",
|
||||
"type": "Cardinal",
|
||||
"rule": "One"
|
||||
},
|
||||
{
|
||||
"locale": "fr",
|
||||
"key": "days-left",
|
||||
"trans": "Il reste {0} jours",
|
||||
"type": "Cardinal",
|
||||
"rule": "Other"
|
||||
}
|
||||
]
|
274
vendor/github.com/go-playground/universal-translator/import_export.go
generated
vendored
Normal file
274
vendor/github.com/go-playground/universal-translator/import_export.go
generated
vendored
Normal file
|
@ -0,0 +1,274 @@
|
|||
package ut
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"io"
|
||||
|
||||
"github.com/go-playground/locales"
|
||||
)
|
||||
|
||||
type translation struct {
|
||||
Locale string `json:"locale"`
|
||||
Key interface{} `json:"key"` // either string or integer
|
||||
Translation string `json:"trans"`
|
||||
PluralType string `json:"type,omitempty"`
|
||||
PluralRule string `json:"rule,omitempty"`
|
||||
OverrideExisting bool `json:"override,omitempty"`
|
||||
}
|
||||
|
||||
const (
|
||||
cardinalType = "Cardinal"
|
||||
ordinalType = "Ordinal"
|
||||
rangeType = "Range"
|
||||
)
|
||||
|
||||
// ImportExportFormat is the format of the file import or export
|
||||
type ImportExportFormat uint8
|
||||
|
||||
// supported Export Formats
|
||||
const (
|
||||
FormatJSON ImportExportFormat = iota
|
||||
)
|
||||
|
||||
// Export writes the translations out to a file on disk.
|
||||
//
|
||||
// NOTE: this currently only works with string or int translations keys.
|
||||
func (t *UniversalTranslator) Export(format ImportExportFormat, dirname string) error {
|
||||
|
||||
_, err := os.Stat(dirname)
|
||||
fmt.Println(dirname, err, os.IsNotExist(err))
|
||||
if err != nil {
|
||||
|
||||
if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = os.MkdirAll(dirname, 0744); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// build up translations
|
||||
var trans []translation
|
||||
var b []byte
|
||||
var ext string
|
||||
|
||||
for _, locale := range t.translators {
|
||||
|
||||
for k, v := range locale.(*translator).translations {
|
||||
trans = append(trans, translation{
|
||||
Locale: locale.Locale(),
|
||||
Key: k,
|
||||
Translation: v.text,
|
||||
})
|
||||
}
|
||||
|
||||
for k, pluralTrans := range locale.(*translator).cardinalTanslations {
|
||||
|
||||
for i, plural := range pluralTrans {
|
||||
|
||||
// leave enough for all plural rules
|
||||
// but not all are set for all languages.
|
||||
if plural == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
trans = append(trans, translation{
|
||||
Locale: locale.Locale(),
|
||||
Key: k.(string),
|
||||
Translation: plural.text,
|
||||
PluralType: cardinalType,
|
||||
PluralRule: locales.PluralRule(i).String(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for k, pluralTrans := range locale.(*translator).ordinalTanslations {
|
||||
|
||||
for i, plural := range pluralTrans {
|
||||
|
||||
// leave enough for all plural rules
|
||||
// but not all are set for all languages.
|
||||
if plural == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
trans = append(trans, translation{
|
||||
Locale: locale.Locale(),
|
||||
Key: k.(string),
|
||||
Translation: plural.text,
|
||||
PluralType: ordinalType,
|
||||
PluralRule: locales.PluralRule(i).String(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for k, pluralTrans := range locale.(*translator).rangeTanslations {
|
||||
|
||||
for i, plural := range pluralTrans {
|
||||
|
||||
// leave enough for all plural rules
|
||||
// but not all are set for all languages.
|
||||
if plural == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
trans = append(trans, translation{
|
||||
Locale: locale.Locale(),
|
||||
Key: k.(string),
|
||||
Translation: plural.text,
|
||||
PluralType: rangeType,
|
||||
PluralRule: locales.PluralRule(i).String(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
switch format {
|
||||
case FormatJSON:
|
||||
b, err = json.MarshalIndent(trans, "", " ")
|
||||
ext = ".json"
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(filepath.Join(dirname, fmt.Sprintf("%s%s", locale.Locale(), ext)), b, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
trans = trans[0:0]
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Import reads the translations out of a file or directory on disk.
|
||||
//
|
||||
// NOTE: this currently only works with string or int translations keys.
|
||||
func (t *UniversalTranslator) Import(format ImportExportFormat, dirnameOrFilename string) error {
|
||||
|
||||
fi, err := os.Stat(dirnameOrFilename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
processFn := func(filename string) error {
|
||||
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
return t.ImportByReader(format, f)
|
||||
}
|
||||
|
||||
if !fi.IsDir() {
|
||||
return processFn(dirnameOrFilename)
|
||||
}
|
||||
|
||||
// recursively go through directory
|
||||
walker := func(path string, info os.FileInfo, err error) error {
|
||||
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch format {
|
||||
case FormatJSON:
|
||||
// skip non JSON files
|
||||
if filepath.Ext(info.Name()) != ".json" {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return processFn(path)
|
||||
}
|
||||
|
||||
return filepath.Walk(dirnameOrFilename, walker)
|
||||
}
|
||||
|
||||
// ImportByReader imports the the translations found within the contents read from the supplied reader.
|
||||
//
|
||||
// NOTE: generally used when assets have been embedded into the binary and are already in memory.
|
||||
func (t *UniversalTranslator) ImportByReader(format ImportExportFormat, reader io.Reader) error {
|
||||
|
||||
b, err := ioutil.ReadAll(reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var trans []translation
|
||||
|
||||
switch format {
|
||||
case FormatJSON:
|
||||
err = json.Unmarshal(b, &trans)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, tl := range trans {
|
||||
|
||||
locale, found := t.FindTranslator(tl.Locale)
|
||||
if !found {
|
||||
return &ErrMissingLocale{locale: tl.Locale}
|
||||
}
|
||||
|
||||
pr := stringToPR(tl.PluralRule)
|
||||
|
||||
if pr == locales.PluralRuleUnknown {
|
||||
|
||||
err = locale.Add(tl.Key, tl.Translation, tl.OverrideExisting)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
switch tl.PluralType {
|
||||
case cardinalType:
|
||||
err = locale.AddCardinal(tl.Key, tl.Translation, pr, tl.OverrideExisting)
|
||||
case ordinalType:
|
||||
err = locale.AddOrdinal(tl.Key, tl.Translation, pr, tl.OverrideExisting)
|
||||
case rangeType:
|
||||
err = locale.AddRange(tl.Key, tl.Translation, pr, tl.OverrideExisting)
|
||||
default:
|
||||
return &ErrBadPluralDefinition{tl: tl}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func stringToPR(s string) locales.PluralRule {
|
||||
|
||||
switch s {
|
||||
case "One":
|
||||
return locales.PluralRuleOne
|
||||
case "Two":
|
||||
return locales.PluralRuleTwo
|
||||
case "Few":
|
||||
return locales.PluralRuleFew
|
||||
case "Many":
|
||||
return locales.PluralRuleMany
|
||||
case "Other":
|
||||
return locales.PluralRuleOther
|
||||
default:
|
||||
return locales.PluralRuleUnknown
|
||||
}
|
||||
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
11
vendor/github.com/go-playground/universal-translator/testdata/.gitignore
generated
vendored
Normal file
11
vendor/github.com/go-playground/universal-translator/testdata/.gitignore
generated
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
*
|
||||
|
||||
!.gitignore
|
||||
!*/
|
||||
!bad-translation1.json
|
||||
!bad-translation2.json
|
||||
!bad-translation3.json
|
||||
!bad-translation4.json
|
||||
!bad-translation5.json
|
||||
!bad-translation6.json
|
||||
!/nested1/**
|
7
vendor/github.com/go-playground/universal-translator/testdata/bad-translation1.json
generated
vendored
Normal file
7
vendor/github.com/go-playground/universal-translator/testdata/bad-translation1.json
generated
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
[
|
||||
{
|
||||
"locale": "en",
|
||||
"key": "test_trans3",
|
||||
"trans": "Welcome {lettersnotpermitted} to the {1}"
|
||||
}
|
||||
]
|
7
vendor/github.com/go-playground/universal-translator/testdata/bad-translation2.json
generated
vendored
Normal file
7
vendor/github.com/go-playground/universal-translator/testdata/bad-translation2.json
generated
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
[
|
||||
{
|
||||
"locale": "en",
|
||||
"key": "test_trans3",
|
||||
"trans": "Welcome {0 to the {1}"
|
||||
}
|
||||
]
|
7
vendor/github.com/go-playground/universal-translator/testdata/bad-translation3.json
generated
vendored
Normal file
7
vendor/github.com/go-playground/universal-translator/testdata/bad-translation3.json
generated
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
[
|
||||
{
|
||||
"locale": "nl",
|
||||
"key": "test_trans3",
|
||||
"trans": "Welcome {0 to the {1}"
|
||||
}
|
||||
]
|
9
vendor/github.com/go-playground/universal-translator/testdata/bad-translation4.json
generated
vendored
Normal file
9
vendor/github.com/go-playground/universal-translator/testdata/bad-translation4.json
generated
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
[
|
||||
{
|
||||
"locale": "en",
|
||||
"key": "cardinal_test",
|
||||
"trans": "You have {0} day left.",
|
||||
"type": "NotAPluralType",
|
||||
"rule": "One"
|
||||
}
|
||||
]
|
9
vendor/github.com/go-playground/universal-translator/testdata/bad-translation5.json
generated
vendored
Normal file
9
vendor/github.com/go-playground/universal-translator/testdata/bad-translation5.json
generated
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
[
|
||||
{
|
||||
"locale": "en",
|
||||
"key": "cardinal_test",
|
||||
"trans": "You have {0} day left.",
|
||||
"type": "Cardinal",
|
||||
"rule": "Many"
|
||||
}
|
||||
]
|
9
vendor/github.com/go-playground/universal-translator/testdata/bad-translation6.json
generated
vendored
Normal file
9
vendor/github.com/go-playground/universal-translator/testdata/bad-translation6.json
generated
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
[
|
||||
{
|
||||
"locale": "en",
|
||||
"key": "cardinal_test",
|
||||
"trans": "You have {0} day left.",
|
||||
"type": "Cardinal",
|
||||
"rule": "Many"
|
||||
|
||||
]
|
12
vendor/github.com/go-playground/universal-translator/testdata/nested1/nested1.json
generated
vendored
Normal file
12
vendor/github.com/go-playground/universal-translator/testdata/nested1/nested1.json
generated
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
[
|
||||
{
|
||||
"locale": "en",
|
||||
"key": -1,
|
||||
"trans": "Welcome {0}"
|
||||
},
|
||||
{
|
||||
"locale": "en",
|
||||
"key": "test_trans2",
|
||||
"trans": "{0} to the {1}."
|
||||
}
|
||||
]
|
17
vendor/github.com/go-playground/universal-translator/testdata/nested1/nested2/nested2.json
generated
vendored
Normal file
17
vendor/github.com/go-playground/universal-translator/testdata/nested1/nested2/nested2.json
generated
vendored
Normal file
|
@ -0,0 +1,17 @@
|
|||
[
|
||||
{
|
||||
"locale": "en",
|
||||
"key": "test_trans3",
|
||||
"trans": "Welcome {0} to the {1}"
|
||||
},
|
||||
{
|
||||
"locale": "en",
|
||||
"key": "test_trans4",
|
||||
"trans": "{0}{1}"
|
||||
},
|
||||
{
|
||||
"locale": "en",
|
||||
"key": "test_trans",
|
||||
"trans": "Welcome {0} to the {1}."
|
||||
}
|
||||
]
|
0
vendor/github.com/go-playground/universal-translator/testdata/nested1/nested2/unrelated-file.txt
generated
vendored
Normal file
0
vendor/github.com/go-playground/universal-translator/testdata/nested1/nested2/unrelated-file.txt
generated
vendored
Normal file
420
vendor/github.com/go-playground/universal-translator/translator.go
generated
vendored
Normal file
420
vendor/github.com/go-playground/universal-translator/translator.go
generated
vendored
Normal file
|
@ -0,0 +1,420 @@
|
|||
package ut
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/go-playground/locales"
|
||||
)
|
||||
|
||||
const (
|
||||
paramZero = "{0}"
|
||||
paramOne = "{1}"
|
||||
unknownTranslation = ""
|
||||
)
|
||||
|
||||
// Translator is universal translators
|
||||
// translator instance which is a thin wrapper
|
||||
// around locales.Translator instance providing
|
||||
// some extra functionality
|
||||
type Translator interface {
|
||||
locales.Translator
|
||||
|
||||
// adds a normal translation for a particular language/locale
|
||||
// {#} is the only replacement type accepted and are ad infinitum
|
||||
// eg. one: '{0} day left' other: '{0} days left'
|
||||
Add(key interface{}, text string, override bool) error
|
||||
|
||||
// adds a cardinal plural translation for a particular language/locale
|
||||
// {0} is the only replacement type accepted and only one variable is accepted as
|
||||
// multiple cannot be used for a plural rule determination, unless it is a range;
|
||||
// see AddRange below.
|
||||
// eg. in locale 'en' one: '{0} day left' other: '{0} days left'
|
||||
AddCardinal(key interface{}, text string, rule locales.PluralRule, override bool) error
|
||||
|
||||
// adds an ordinal plural translation for a particular language/locale
|
||||
// {0} is the only replacement type accepted and only one variable is accepted as
|
||||
// multiple cannot be used for a plural rule determination, unless it is a range;
|
||||
// see AddRange below.
|
||||
// eg. in locale 'en' one: '{0}st day of spring' other: '{0}nd day of spring'
|
||||
// - 1st, 2nd, 3rd...
|
||||
AddOrdinal(key interface{}, text string, rule locales.PluralRule, override bool) error
|
||||
|
||||
// adds a range plural translation for a particular language/locale
|
||||
// {0} and {1} are the only replacement types accepted and only these are accepted.
|
||||
// eg. in locale 'nl' one: '{0}-{1} day left' other: '{0}-{1} days left'
|
||||
AddRange(key interface{}, text string, rule locales.PluralRule, override bool) error
|
||||
|
||||
// creates the translation for the locale given the 'key' and params passed in
|
||||
T(key interface{}, params ...string) (string, error)
|
||||
|
||||
// creates the cardinal translation for the locale given the 'key', 'num' and 'digit' arguments
|
||||
// and param passed in
|
||||
C(key interface{}, num float64, digits uint64, param string) (string, error)
|
||||
|
||||
// creates the ordinal translation for the locale given the 'key', 'num' and 'digit' arguments
|
||||
// and param passed in
|
||||
O(key interface{}, num float64, digits uint64, param string) (string, error)
|
||||
|
||||
// creates the range translation for the locale given the 'key', 'num1', 'digit1', 'num2' and
|
||||
// 'digit2' arguments and 'param1' and 'param2' passed in
|
||||
R(key interface{}, num1 float64, digits1 uint64, num2 float64, digits2 uint64, param1, param2 string) (string, error)
|
||||
|
||||
// VerifyTranslations checks to ensures that no plural rules have been
|
||||
// missed within the translations.
|
||||
VerifyTranslations() error
|
||||
}
|
||||
|
||||
var _ Translator = new(translator)
|
||||
var _ locales.Translator = new(translator)
|
||||
|
||||
type translator struct {
|
||||
locales.Translator
|
||||
translations map[interface{}]*transText
|
||||
cardinalTanslations map[interface{}][]*transText // array index is mapped to locales.PluralRule index + the locales.PluralRuleUnknown
|
||||
ordinalTanslations map[interface{}][]*transText
|
||||
rangeTanslations map[interface{}][]*transText
|
||||
}
|
||||
|
||||
type transText struct {
|
||||
text string
|
||||
indexes []int
|
||||
}
|
||||
|
||||
func newTranslator(trans locales.Translator) Translator {
|
||||
return &translator{
|
||||
Translator: trans,
|
||||
translations: make(map[interface{}]*transText), // translation text broken up by byte index
|
||||
cardinalTanslations: make(map[interface{}][]*transText),
|
||||
ordinalTanslations: make(map[interface{}][]*transText),
|
||||
rangeTanslations: make(map[interface{}][]*transText),
|
||||
}
|
||||
}
|
||||
|
||||
// Add adds a normal translation for a particular language/locale
|
||||
// {#} is the only replacement type accepted and are ad infinitum
|
||||
// eg. one: '{0} day left' other: '{0} days left'
|
||||
func (t *translator) Add(key interface{}, text string, override bool) error {
|
||||
|
||||
if _, ok := t.translations[key]; ok && !override {
|
||||
return &ErrConflictingTranslation{locale: t.Locale(), key: key, text: text}
|
||||
}
|
||||
|
||||
lb := strings.Count(text, "{")
|
||||
rb := strings.Count(text, "}")
|
||||
|
||||
if lb != rb {
|
||||
return &ErrMissingBracket{locale: t.Locale(), key: key, text: text}
|
||||
}
|
||||
|
||||
trans := &transText{
|
||||
text: text,
|
||||
}
|
||||
|
||||
var idx int
|
||||
|
||||
for i := 0; i < lb; i++ {
|
||||
s := "{" + strconv.Itoa(i) + "}"
|
||||
idx = strings.Index(text, s)
|
||||
if idx == -1 {
|
||||
return &ErrBadParamSyntax{locale: t.Locale(), param: s, key: key, text: text}
|
||||
}
|
||||
|
||||
trans.indexes = append(trans.indexes, idx)
|
||||
trans.indexes = append(trans.indexes, idx+len(s))
|
||||
}
|
||||
|
||||
t.translations[key] = trans
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddCardinal adds a cardinal plural translation for a particular language/locale
|
||||
// {0} is the only replacement type accepted and only one variable is accepted as
|
||||
// multiple cannot be used for a plural rule determination, unless it is a range;
|
||||
// see AddRange below.
|
||||
// eg. in locale 'en' one: '{0} day left' other: '{0} days left'
|
||||
func (t *translator) AddCardinal(key interface{}, text string, rule locales.PluralRule, override bool) error {
|
||||
|
||||
var verified bool
|
||||
|
||||
// verify plural rule exists for locale
|
||||
for _, pr := range t.PluralsCardinal() {
|
||||
if pr == rule {
|
||||
verified = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !verified {
|
||||
return &ErrCardinalTranslation{text: fmt.Sprintf("error: cardinal plural rule '%s' does not exist for locale '%s' key: '%v' text: '%s'", rule, t.Locale(), key, text)}
|
||||
}
|
||||
|
||||
tarr, ok := t.cardinalTanslations[key]
|
||||
if ok {
|
||||
// verify not adding a conflicting record
|
||||
if len(tarr) > 0 && tarr[rule] != nil && !override {
|
||||
return &ErrConflictingTranslation{locale: t.Locale(), key: key, rule: rule, text: text}
|
||||
}
|
||||
|
||||
} else {
|
||||
tarr = make([]*transText, 7, 7)
|
||||
t.cardinalTanslations[key] = tarr
|
||||
}
|
||||
|
||||
trans := &transText{
|
||||
text: text,
|
||||
indexes: make([]int, 2, 2),
|
||||
}
|
||||
|
||||
tarr[rule] = trans
|
||||
|
||||
idx := strings.Index(text, paramZero)
|
||||
if idx == -1 {
|
||||
tarr[rule] = nil
|
||||
return &ErrCardinalTranslation{text: fmt.Sprintf("error: parameter '%s' not found, may want to use 'Add' instead of 'AddCardinal'. locale: '%s' key: '%v' text: '%s'", paramZero, t.Locale(), key, text)}
|
||||
}
|
||||
|
||||
trans.indexes[0] = idx
|
||||
trans.indexes[1] = idx + len(paramZero)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddOrdinal adds an ordinal plural translation for a particular language/locale
|
||||
// {0} is the only replacement type accepted and only one variable is accepted as
|
||||
// multiple cannot be used for a plural rule determination, unless it is a range;
|
||||
// see AddRange below.
|
||||
// eg. in locale 'en' one: '{0}st day of spring' other: '{0}nd day of spring' - 1st, 2nd, 3rd...
|
||||
func (t *translator) AddOrdinal(key interface{}, text string, rule locales.PluralRule, override bool) error {
|
||||
|
||||
var verified bool
|
||||
|
||||
// verify plural rule exists for locale
|
||||
for _, pr := range t.PluralsOrdinal() {
|
||||
if pr == rule {
|
||||
verified = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !verified {
|
||||
return &ErrOrdinalTranslation{text: fmt.Sprintf("error: ordinal plural rule '%s' does not exist for locale '%s' key: '%v' text: '%s'", rule, t.Locale(), key, text)}
|
||||
}
|
||||
|
||||
tarr, ok := t.ordinalTanslations[key]
|
||||
if ok {
|
||||
// verify not adding a conflicting record
|
||||
if len(tarr) > 0 && tarr[rule] != nil && !override {
|
||||
return &ErrConflictingTranslation{locale: t.Locale(), key: key, rule: rule, text: text}
|
||||
}
|
||||
|
||||
} else {
|
||||
tarr = make([]*transText, 7, 7)
|
||||
t.ordinalTanslations[key] = tarr
|
||||
}
|
||||
|
||||
trans := &transText{
|
||||
text: text,
|
||||
indexes: make([]int, 2, 2),
|
||||
}
|
||||
|
||||
tarr[rule] = trans
|
||||
|
||||
idx := strings.Index(text, paramZero)
|
||||
if idx == -1 {
|
||||
tarr[rule] = nil
|
||||
return &ErrOrdinalTranslation{text: fmt.Sprintf("error: parameter '%s' not found, may want to use 'Add' instead of 'AddOrdinal'. locale: '%s' key: '%v' text: '%s'", paramZero, t.Locale(), key, text)}
|
||||
}
|
||||
|
||||
trans.indexes[0] = idx
|
||||
trans.indexes[1] = idx + len(paramZero)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddRange adds a range plural translation for a particular language/locale
|
||||
// {0} and {1} are the only replacement types accepted and only these are accepted.
|
||||
// eg. in locale 'nl' one: '{0}-{1} day left' other: '{0}-{1} days left'
|
||||
func (t *translator) AddRange(key interface{}, text string, rule locales.PluralRule, override bool) error {
|
||||
|
||||
var verified bool
|
||||
|
||||
// verify plural rule exists for locale
|
||||
for _, pr := range t.PluralsRange() {
|
||||
if pr == rule {
|
||||
verified = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !verified {
|
||||
return &ErrRangeTranslation{text: fmt.Sprintf("error: range plural rule '%s' does not exist for locale '%s' key: '%v' text: '%s'", rule, t.Locale(), key, text)}
|
||||
}
|
||||
|
||||
tarr, ok := t.rangeTanslations[key]
|
||||
if ok {
|
||||
// verify not adding a conflicting record
|
||||
if len(tarr) > 0 && tarr[rule] != nil && !override {
|
||||
return &ErrConflictingTranslation{locale: t.Locale(), key: key, rule: rule, text: text}
|
||||
}
|
||||
|
||||
} else {
|
||||
tarr = make([]*transText, 7, 7)
|
||||
t.rangeTanslations[key] = tarr
|
||||
}
|
||||
|
||||
trans := &transText{
|
||||
text: text,
|
||||
indexes: make([]int, 4, 4),
|
||||
}
|
||||
|
||||
tarr[rule] = trans
|
||||
|
||||
idx := strings.Index(text, paramZero)
|
||||
if idx == -1 {
|
||||
tarr[rule] = nil
|
||||
return &ErrRangeTranslation{text: fmt.Sprintf("error: parameter '%s' not found, are you sure you're adding a Range Translation? locale: '%s' key: '%v' text: '%s'", paramZero, t.Locale(), key, text)}
|
||||
}
|
||||
|
||||
trans.indexes[0] = idx
|
||||
trans.indexes[1] = idx + len(paramZero)
|
||||
|
||||
idx = strings.Index(text, paramOne)
|
||||
if idx == -1 {
|
||||
tarr[rule] = nil
|
||||
return &ErrRangeTranslation{text: fmt.Sprintf("error: parameter '%s' not found, a Range Translation requires two parameters. locale: '%s' key: '%v' text: '%s'", paramOne, t.Locale(), key, text)}
|
||||
}
|
||||
|
||||
trans.indexes[2] = idx
|
||||
trans.indexes[3] = idx + len(paramOne)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// T creates the translation for the locale given the 'key' and params passed in
|
||||
func (t *translator) T(key interface{}, params ...string) (string, error) {
|
||||
|
||||
trans, ok := t.translations[key]
|
||||
if !ok {
|
||||
return unknownTranslation, ErrUnknowTranslation
|
||||
}
|
||||
|
||||
b := make([]byte, 0, 64)
|
||||
|
||||
var start, end, count int
|
||||
|
||||
for i := 0; i < len(trans.indexes); i++ {
|
||||
end = trans.indexes[i]
|
||||
b = append(b, trans.text[start:end]...)
|
||||
b = append(b, params[count]...)
|
||||
i++
|
||||
start = trans.indexes[i]
|
||||
count++
|
||||
}
|
||||
|
||||
b = append(b, trans.text[start:]...)
|
||||
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
// C creates the cardinal translation for the locale given the 'key', 'num' and 'digit' arguments and param passed in
|
||||
func (t *translator) C(key interface{}, num float64, digits uint64, param string) (string, error) {
|
||||
|
||||
tarr, ok := t.cardinalTanslations[key]
|
||||
if !ok {
|
||||
return unknownTranslation, ErrUnknowTranslation
|
||||
}
|
||||
|
||||
rule := t.CardinalPluralRule(num, digits)
|
||||
|
||||
trans := tarr[rule]
|
||||
|
||||
b := make([]byte, 0, 64)
|
||||
b = append(b, trans.text[:trans.indexes[0]]...)
|
||||
b = append(b, param...)
|
||||
b = append(b, trans.text[trans.indexes[1]:]...)
|
||||
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
// O creates the ordinal translation for the locale given the 'key', 'num' and 'digit' arguments and param passed in
|
||||
func (t *translator) O(key interface{}, num float64, digits uint64, param string) (string, error) {
|
||||
|
||||
tarr, ok := t.ordinalTanslations[key]
|
||||
if !ok {
|
||||
return unknownTranslation, ErrUnknowTranslation
|
||||
}
|
||||
|
||||
rule := t.OrdinalPluralRule(num, digits)
|
||||
|
||||
trans := tarr[rule]
|
||||
|
||||
b := make([]byte, 0, 64)
|
||||
b = append(b, trans.text[:trans.indexes[0]]...)
|
||||
b = append(b, param...)
|
||||
b = append(b, trans.text[trans.indexes[1]:]...)
|
||||
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
// R creates the range translation for the locale given the 'key', 'num1', 'digit1', 'num2' and 'digit2' arguments
|
||||
// and 'param1' and 'param2' passed in
|
||||
func (t *translator) R(key interface{}, num1 float64, digits1 uint64, num2 float64, digits2 uint64, param1, param2 string) (string, error) {
|
||||
|
||||
tarr, ok := t.rangeTanslations[key]
|
||||
if !ok {
|
||||
return unknownTranslation, ErrUnknowTranslation
|
||||
}
|
||||
|
||||
rule := t.RangePluralRule(num1, digits1, num2, digits2)
|
||||
|
||||
trans := tarr[rule]
|
||||
|
||||
b := make([]byte, 0, 64)
|
||||
b = append(b, trans.text[:trans.indexes[0]]...)
|
||||
b = append(b, param1...)
|
||||
b = append(b, trans.text[trans.indexes[1]:trans.indexes[2]]...)
|
||||
b = append(b, param2...)
|
||||
b = append(b, trans.text[trans.indexes[3]:]...)
|
||||
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
// VerifyTranslations checks to ensures that no plural rules have been
|
||||
// missed within the translations.
|
||||
func (t *translator) VerifyTranslations() error {
|
||||
|
||||
for k, v := range t.cardinalTanslations {
|
||||
|
||||
for _, rule := range t.PluralsCardinal() {
|
||||
|
||||
if v[rule] == nil {
|
||||
return &ErrMissingPluralTranslation{locale: t.Locale(), translationType: "plural", rule: rule, key: k}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for k, v := range t.ordinalTanslations {
|
||||
|
||||
for _, rule := range t.PluralsOrdinal() {
|
||||
|
||||
if v[rule] == nil {
|
||||
return &ErrMissingPluralTranslation{locale: t.Locale(), translationType: "ordinal", rule: rule, key: k}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for k, v := range t.rangeTanslations {
|
||||
|
||||
for _, rule := range t.PluralsRange() {
|
||||
|
||||
if v[rule] == nil {
|
||||
return &ErrMissingPluralTranslation{locale: t.Locale(), translationType: "range", rule: rule, key: k}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
113
vendor/github.com/go-playground/universal-translator/universal_translator.go
generated
vendored
Normal file
113
vendor/github.com/go-playground/universal-translator/universal_translator.go
generated
vendored
Normal file
|
@ -0,0 +1,113 @@
|
|||
package ut
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/go-playground/locales"
|
||||
)
|
||||
|
||||
// UniversalTranslator holds all locale & translation data
|
||||
type UniversalTranslator struct {
|
||||
translators map[string]Translator
|
||||
fallback Translator
|
||||
}
|
||||
|
||||
// New returns a new UniversalTranslator instance set with
|
||||
// the fallback locale and locales it should support
|
||||
func New(fallback locales.Translator, supportedLocales ...locales.Translator) *UniversalTranslator {
|
||||
|
||||
t := &UniversalTranslator{
|
||||
translators: make(map[string]Translator),
|
||||
}
|
||||
|
||||
for _, v := range supportedLocales {
|
||||
|
||||
trans := newTranslator(v)
|
||||
t.translators[strings.ToLower(trans.Locale())] = trans
|
||||
|
||||
if fallback.Locale() == v.Locale() {
|
||||
t.fallback = trans
|
||||
}
|
||||
}
|
||||
|
||||
if t.fallback == nil && fallback != nil {
|
||||
t.fallback = newTranslator(fallback)
|
||||
}
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
// FindTranslator trys to find a Translator based on an array of locales
|
||||
// and returns the first one it can find, otherwise returns the
|
||||
// fallback translator.
|
||||
func (t *UniversalTranslator) FindTranslator(locales ...string) (trans Translator, found bool) {
|
||||
|
||||
for _, locale := range locales {
|
||||
|
||||
if trans, found = t.translators[strings.ToLower(locale)]; found {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return t.fallback, false
|
||||
}
|
||||
|
||||
// GetTranslator returns the specified translator for the given locale,
|
||||
// or fallback if not found
|
||||
func (t *UniversalTranslator) GetTranslator(locale string) (trans Translator, found bool) {
|
||||
|
||||
if trans, found = t.translators[strings.ToLower(locale)]; found {
|
||||
return
|
||||
}
|
||||
|
||||
return t.fallback, false
|
||||
}
|
||||
|
||||
// GetFallback returns the fallback locale
|
||||
func (t *UniversalTranslator) GetFallback() Translator {
|
||||
return t.fallback
|
||||
}
|
||||
|
||||
// AddTranslator adds the supplied translator, if it already exists the override param
|
||||
// will be checked and if false an error will be returned, otherwise the translator will be
|
||||
// overridden; if the fallback matches the supplied translator it will be overridden as well
|
||||
// NOTE: this is normally only used when translator is embedded within a library
|
||||
func (t *UniversalTranslator) AddTranslator(translator locales.Translator, override bool) error {
|
||||
|
||||
lc := strings.ToLower(translator.Locale())
|
||||
_, ok := t.translators[lc]
|
||||
if ok && !override {
|
||||
return &ErrExistingTranslator{locale: translator.Locale()}
|
||||
}
|
||||
|
||||
trans := newTranslator(translator)
|
||||
|
||||
if t.fallback.Locale() == translator.Locale() {
|
||||
|
||||
// because it's optional to have a fallback, I don't impose that limitation
|
||||
// don't know why you wouldn't but...
|
||||
if !override {
|
||||
return &ErrExistingTranslator{locale: translator.Locale()}
|
||||
}
|
||||
|
||||
t.fallback = trans
|
||||
}
|
||||
|
||||
t.translators[lc] = trans
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifyTranslations runs through all locales and identifies any issues
|
||||
// eg. missing plural rules for a locale
|
||||
func (t *UniversalTranslator) VerifyTranslations() (err error) {
|
||||
|
||||
for _, trans := range t.translators {
|
||||
err = trans.VerifyTranslations()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
# Contribution Guidelines
|
||||
|
||||
## Quality Standard
|
||||
|
||||
To ensure the continued stability of this package tests are required to be written or already exist in order for a pull request to be merged.
|
||||
|
||||
## Reporting issues
|
||||
|
||||
Please open an issue or join the gitter chat [![Join the chat at https://gitter.im/go-playground/validator](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/go-playground/validator?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) for any issues, questions or possible enhancements to the package.
|
13
vendor/gopkg.in/go-playground/validator.v9/.github/ISSUE_TEMPLATE.md
generated
vendored
Normal file
13
vendor/gopkg.in/go-playground/validator.v9/.github/ISSUE_TEMPLATE.md
generated
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
### Package version eg. v8, v9:
|
||||
|
||||
|
||||
|
||||
### Issue, Question or Enhancement:
|
||||
|
||||
|
||||
|
||||
### Code sample, to showcase or reproduce:
|
||||
|
||||
```go
|
||||
|
||||
```
|
13
vendor/gopkg.in/go-playground/validator.v9/.github/PULL_REQUEST_TEMPLATE.md
generated
vendored
Normal file
13
vendor/gopkg.in/go-playground/validator.v9/.github/PULL_REQUEST_TEMPLATE.md
generated
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
Fixes Or Enhances # .
|
||||
|
||||
**Make sure that you've checked the boxes below before you submit PR:**
|
||||
- [ ] Tests exist or have been written that cover this particular change.
|
||||
|
||||
Change Details:
|
||||
|
||||
-
|
||||
-
|
||||
-
|
||||
|
||||
|
||||
@go-playground/admins
|
|
@ -0,0 +1,29 @@
|
|||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
*.test
|
||||
*.prof
|
||||
*.test
|
||||
*.out
|
||||
*.txt
|
||||
cover.html
|
||||
README.html
|
|
@ -0,0 +1,22 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Dean Karn
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
Package validator
|
||||
================
|
||||
<img align="right" src="https://raw.githubusercontent.com/go-playground/validator/v9/logo.png">[![Join the chat at https://gitter.im/go-playground/validator](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/go-playground/validator?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
![Project status](https://img.shields.io/badge/version-9.4.0-green.svg)
|
||||
[![Build Status](https://semaphoreci.com/api/v1/joeybloggs/validator/branches/v9/badge.svg)](https://semaphoreci.com/joeybloggs/validator)
|
||||
[![Coverage Status](https://coveralls.io/repos/go-playground/validator/badge.svg?branch=v9&service=github)](https://coveralls.io/github/go-playground/validator?branch=v9)
|
||||
[![Go Report Card](https://goreportcard.com/badge/github.com/go-playground/validator)](https://goreportcard.com/report/github.com/go-playground/validator)
|
||||
[![GoDoc](https://godoc.org/gopkg.in/go-playground/validator.v9?status.svg)](https://godoc.org/gopkg.in/go-playground/validator.v9)
|
||||
![License](https://img.shields.io/dub/l/vibe-d.svg)
|
||||
|
||||
Package validator implements value validations for structs and individual fields based on tags.
|
||||
|
||||
It has the following **unique** features:
|
||||
|
||||
- Cross Field and Cross Struct validations by using validation tags or custom validators.
|
||||
- Slice, Array and Map diving, which allows any or all levels of a multidimensional field to be validated.
|
||||
- Handles type interface by determining it's underlying type prior to validation.
|
||||
- Handles custom field types such as sql driver Valuer see [Valuer](https://golang.org/src/database/sql/driver/types.go?s=1210:1293#L29)
|
||||
- Alias validation tags, which allows for mapping of several validations to a single tag for easier defining of validations on structs
|
||||
- Extraction of custom defined Field Name e.g. can specify to extract the JSON name while validating and have it available in the resulting FieldError
|
||||
- Customizable i18n aware error messages.
|
||||
- Default validator for the [gin](https://github.com/gin-gonic/gin) web framework; upgrading from v8 to v9 in gin see [here](https://github.com/go-playground/validator/tree/v9/_examples/gin-upgrading-overriding)
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
Use go get.
|
||||
|
||||
go get gopkg.in/go-playground/validator.v9
|
||||
|
||||
Then import the validator package into your own code.
|
||||
|
||||
import "gopkg.in/go-playground/validator.v9"
|
||||
|
||||
Error Return Value
|
||||
-------
|
||||
|
||||
Validation functions return type error
|
||||
|
||||
They return type error to avoid the issue discussed in the following, where err is always != nil:
|
||||
|
||||
* http://stackoverflow.com/a/29138676/3158232
|
||||
* https://github.com/go-playground/validator/issues/134
|
||||
|
||||
Validator only InvalidValidationError for bad validation input, nil or ValidationErrors as type error; so, in your code all you need to do is check if the error returned is not nil, and if it's not check if error is InvalidValidationError ( if necessary, most of the time it isn't ) type cast it to type ValidationErrors like so:
|
||||
|
||||
```go
|
||||
err := validate.Struct(mystruct)
|
||||
validationErrors := err.(validator.ValidationErrors)
|
||||
```
|
||||
|
||||
Usage and documentation
|
||||
------
|
||||
|
||||
Please see http://godoc.org/gopkg.in/go-playground/validator.v9 for detailed usage docs.
|
||||
|
||||
##### Examples:
|
||||
|
||||
- [Simple](https://github.com/go-playground/validator/blob/v9/_examples/simple/main.go)
|
||||
- [Custom Field Types](https://github.com/go-playground/validator/blob/v9/_examples/custom/main.go)
|
||||
- [Struct Level](https://github.com/go-playground/validator/blob/v9/_examples/struct-level/main.go)
|
||||
- [Translations & Custom Errors](https://github.com/go-playground/validator/blob/v9/_examples/translations/main.go)
|
||||
- [Gin upgrade and/or override validator](https://github.com/go-playground/validator/tree/v9/_examples/gin-upgrading-overriding)
|
||||
- [wash - an example application putting it all together](https://github.com/bluesuncorp/wash)
|
||||
|
||||
Benchmarks
|
||||
------
|
||||
###### Run on Dell XPS 15 i7-7700HQ 32GB Go version go1.8.3 linux/amd64
|
||||
```go
|
||||
go test -run=XXX -bench=. -benchmem=true
|
||||
BenchmarkFieldSuccess-8 20000000 88.3 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkFieldSuccessParallel-8 50000000 30.4 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkFieldFailure-8 3000000 428 ns/op 208 B/op 4 allocs/op
|
||||
BenchmarkFieldFailureParallel-8 20000000 96.0 ns/op 208 B/op 4 allocs/op
|
||||
BenchmarkFieldDiveSuccess-8 2000000 695 ns/op 201 B/op 11 allocs/op
|
||||
BenchmarkFieldDiveSuccessParallel-8 10000000 205 ns/op 201 B/op 11 allocs/op
|
||||
BenchmarkFieldDiveFailure-8 1000000 1083 ns/op 412 B/op 16 allocs/op
|
||||
BenchmarkFieldDiveFailureParallel-8 5000000 278 ns/op 413 B/op 16 allocs/op
|
||||
BenchmarkFieldCustomTypeSuccess-8 10000000 229 ns/op 32 B/op 2 allocs/op
|
||||
BenchmarkFieldCustomTypeSuccessParallel-8 20000000 72.4 ns/op 32 B/op 2 allocs/op
|
||||
BenchmarkFieldCustomTypeFailure-8 5000000 377 ns/op 208 B/op 4 allocs/op
|
||||
BenchmarkFieldCustomTypeFailureParallel-8 20000000 93.0 ns/op 208 B/op 4 allocs/op
|
||||
BenchmarkFieldOrTagSuccess-8 2000000 767 ns/op 16 B/op 1 allocs/op
|
||||
BenchmarkFieldOrTagSuccessParallel-8 3000000 425 ns/op 16 B/op 1 allocs/op
|
||||
BenchmarkFieldOrTagFailure-8 2000000 548 ns/op 224 B/op 5 allocs/op
|
||||
BenchmarkFieldOrTagFailureParallel-8 3000000 411 ns/op 224 B/op 5 allocs/op
|
||||
BenchmarkStructLevelValidationSuccess-8 10000000 219 ns/op 32 B/op 2 allocs/op
|
||||
BenchmarkStructLevelValidationSuccessParallel-8 20000000 69.2 ns/op 32 B/op 2 allocs/op
|
||||
BenchmarkStructLevelValidationFailure-8 2000000 628 ns/op 304 B/op 8 allocs/op
|
||||
BenchmarkStructLevelValidationFailureParallel-8 10000000 165 ns/op 304 B/op 8 allocs/op
|
||||
BenchmarkStructSimpleCustomTypeSuccess-8 3000000 411 ns/op 32 B/op 2 allocs/op
|
||||
BenchmarkStructSimpleCustomTypeSuccessParallel-8 10000000 122 ns/op 32 B/op 2 allocs/op
|
||||
BenchmarkStructSimpleCustomTypeFailure-8 1000000 1022 ns/op 424 B/op 9 allocs/op
|
||||
BenchmarkStructSimpleCustomTypeFailureParallel-8 10000000 228 ns/op 440 B/op 10 allocs/op
|
||||
BenchmarkStructFilteredSuccess-8 2000000 737 ns/op 288 B/op 9 allocs/op
|
||||
BenchmarkStructFilteredSuccessParallel-8 10000000 192 ns/op 288 B/op 9 allocs/op
|
||||
BenchmarkStructFilteredFailure-8 3000000 583 ns/op 256 B/op 7 allocs/op
|
||||
BenchmarkStructFilteredFailureParallel-8 10000000 152 ns/op 256 B/op 7 allocs/op
|
||||
BenchmarkStructPartialSuccess-8 2000000 731 ns/op 256 B/op 6 allocs/op
|
||||
BenchmarkStructPartialSuccessParallel-8 10000000 173 ns/op 256 B/op 6 allocs/op
|
||||
BenchmarkStructPartialFailure-8 1000000 1164 ns/op 480 B/op 11 allocs/op
|
||||
BenchmarkStructPartialFailureParallel-8 5000000 253 ns/op 480 B/op 11 allocs/op
|
||||
BenchmarkStructExceptSuccess-8 1000000 1337 ns/op 496 B/op 12 allocs/op
|
||||
BenchmarkStructExceptSuccessParallel-8 10000000 153 ns/op 240 B/op 5 allocs/op
|
||||
BenchmarkStructExceptFailure-8 2000000 954 ns/op 464 B/op 10 allocs/op
|
||||
BenchmarkStructExceptFailureParallel-8 5000000 234 ns/op 464 B/op 10 allocs/op
|
||||
BenchmarkStructSimpleCrossFieldSuccess-8 3000000 420 ns/op 72 B/op 3 allocs/op
|
||||
BenchmarkStructSimpleCrossFieldSuccessParallel-8 10000000 125 ns/op 72 B/op 3 allocs/op
|
||||
BenchmarkStructSimpleCrossFieldFailure-8 2000000 790 ns/op 304 B/op 8 allocs/op
|
||||
BenchmarkStructSimpleCrossFieldFailureParallel-8 10000000 205 ns/op 304 B/op 8 allocs/op
|
||||
BenchmarkStructSimpleCrossStructCrossFieldSuccess-8 2000000 611 ns/op 80 B/op 4 allocs/op
|
||||
BenchmarkStructSimpleCrossStructCrossFieldSuccessParallel-8 10000000 172 ns/op 80 B/op 4 allocs/op
|
||||
BenchmarkStructSimpleCrossStructCrossFieldFailure-8 1000000 1112 ns/op 320 B/op 9 allocs/op
|
||||
BenchmarkStructSimpleCrossStructCrossFieldFailureParallel-8 5000000 258 ns/op 320 B/op 9 allocs/op
|
||||
BenchmarkStructSimpleSuccess-8 5000000 263 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkStructSimpleSuccessParallel-8 20000000 83.1 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkStructSimpleFailure-8 2000000 964 ns/op 424 B/op 9 allocs/op
|
||||
BenchmarkStructSimpleFailureParallel-8 10000000 212 ns/op 424 B/op 9 allocs/op
|
||||
BenchmarkStructComplexSuccess-8 1000000 1504 ns/op 128 B/op 8 allocs/op
|
||||
BenchmarkStructComplexSuccessParallel-8 3000000 427 ns/op 128 B/op 8 allocs/op
|
||||
BenchmarkStructComplexFailure-8 300000 7585 ns/op 3041 B/op 53 allocs/op
|
||||
BenchmarkStructComplexFailureParallel-8 1000000 1387 ns/op 3041 B/op 53 allocs/op
|
||||
```
|
||||
|
||||
Complementary Software
|
||||
----------------------
|
||||
|
||||
Here is a list of software that complements using this library either pre or post validation.
|
||||
|
||||
* [form](https://github.com/go-playground/form) - Decodes url.Values into Go value(s) and Encodes Go value(s) into url.Values. Dual Array and Full map support.
|
||||
* [Conform](https://github.com/leebenson/conform) - Trims, sanitizes & scrubs data based on struct tags.
|
||||
|
||||
How to Contribute
|
||||
------
|
||||
|
||||
Make a pull request...
|
||||
|
||||
License
|
||||
------
|
||||
Distributed under MIT License, please see license file within the code for more details.
|
51
vendor/gopkg.in/go-playground/validator.v9/_examples/custom/main.go
generated
vendored
Normal file
51
vendor/gopkg.in/go-playground/validator.v9/_examples/custom/main.go
generated
vendored
Normal file
|
@ -0,0 +1,51 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"gopkg.in/go-playground/validator.v9"
|
||||
)
|
||||
|
||||
// DbBackedUser User struct
|
||||
type DbBackedUser struct {
|
||||
Name sql.NullString `validate:"required"`
|
||||
Age sql.NullInt64 `validate:"required"`
|
||||
}
|
||||
|
||||
// use a single instance of Validate, it caches struct info
|
||||
var validate *validator.Validate
|
||||
|
||||
func main() {
|
||||
|
||||
validate = validator.New()
|
||||
|
||||
// register all sql.Null* types to use the ValidateValuer CustomTypeFunc
|
||||
validate.RegisterCustomTypeFunc(ValidateValuer, sql.NullString{}, sql.NullInt64{}, sql.NullBool{}, sql.NullFloat64{})
|
||||
|
||||
// build object for validation
|
||||
x := DbBackedUser{Name: sql.NullString{String: "", Valid: true}, Age: sql.NullInt64{Int64: 0, Valid: false}}
|
||||
|
||||
err := validate.Struct(x)
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Err(s):\n%+v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateValuer implements validator.CustomTypeFunc
|
||||
func ValidateValuer(field reflect.Value) interface{} {
|
||||
|
||||
if valuer, ok := field.Interface().(driver.Valuer); ok {
|
||||
|
||||
val, err := valuer.Value()
|
||||
if err == nil {
|
||||
return val
|
||||
}
|
||||
// handle the error how you want
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
10
vendor/gopkg.in/go-playground/validator.v9/_examples/gin-upgrading-overriding/main.go
generated
vendored
Normal file
10
vendor/gopkg.in/go-playground/validator.v9/_examples/gin-upgrading-overriding/main.go
generated
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
package main
|
||||
|
||||
import "github.com/gin-gonic/gin/binding"
|
||||
|
||||
func main() {
|
||||
|
||||
binding.Validator = new(defaultValidator)
|
||||
|
||||
// regular gin logic
|
||||
}
|
50
vendor/gopkg.in/go-playground/validator.v9/_examples/gin-upgrading-overriding/v8_to_v9.go
generated
vendored
Normal file
50
vendor/gopkg.in/go-playground/validator.v9/_examples/gin-upgrading-overriding/v8_to_v9.go
generated
vendored
Normal file
|
@ -0,0 +1,50 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"sync"
|
||||
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
"gopkg.in/go-playground/validator.v9"
|
||||
)
|
||||
|
||||
type defaultValidator struct {
|
||||
once sync.Once
|
||||
validate *validator.Validate
|
||||
}
|
||||
|
||||
var _ binding.StructValidator = &defaultValidator{}
|
||||
|
||||
func (v *defaultValidator) ValidateStruct(obj interface{}) error {
|
||||
|
||||
if kindOfData(obj) == reflect.Struct {
|
||||
|
||||
v.lazyinit()
|
||||
|
||||
if err := v.validate.Struct(obj); err != nil {
|
||||
return error(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *defaultValidator) lazyinit() {
|
||||
v.once.Do(func() {
|
||||
v.validate = validator.New()
|
||||
v.validate.SetTagName("binding")
|
||||
|
||||
// add any custom validations etc. here
|
||||
})
|
||||
}
|
||||
|
||||
func kindOfData(data interface{}) reflect.Kind {
|
||||
|
||||
value := reflect.ValueOf(data)
|
||||
valueType := value.Kind()
|
||||
|
||||
if valueType == reflect.Ptr {
|
||||
valueType = value.Elem().Kind()
|
||||
}
|
||||
return valueType
|
||||
}
|
101
vendor/gopkg.in/go-playground/validator.v9/_examples/simple/main.go
generated
vendored
Normal file
101
vendor/gopkg.in/go-playground/validator.v9/_examples/simple/main.go
generated
vendored
Normal file
|
@ -0,0 +1,101 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gopkg.in/go-playground/validator.v9"
|
||||
)
|
||||
|
||||
// User contains user information
|
||||
type User struct {
|
||||
FirstName string `validate:"required"`
|
||||
LastName string `validate:"required"`
|
||||
Age uint8 `validate:"gte=0,lte=130"`
|
||||
Email string `validate:"required,email"`
|
||||
FavouriteColor string `validate:"iscolor"` // alias for 'hexcolor|rgb|rgba|hsl|hsla'
|
||||
Addresses []*Address `validate:"required,dive,required"` // a person can have a home and cottage...
|
||||
}
|
||||
|
||||
// Address houses a users address information
|
||||
type Address struct {
|
||||
Street string `validate:"required"`
|
||||
City string `validate:"required"`
|
||||
Planet string `validate:"required"`
|
||||
Phone string `validate:"required"`
|
||||
}
|
||||
|
||||
// use a single instance of Validate, it caches struct info
|
||||
var validate *validator.Validate
|
||||
|
||||
func main() {
|
||||
|
||||
validate = validator.New()
|
||||
|
||||
validateStruct()
|
||||
validateVariable()
|
||||
}
|
||||
|
||||
func validateStruct() {
|
||||
|
||||
address := &Address{
|
||||
Street: "Eavesdown Docks",
|
||||
Planet: "Persphone",
|
||||
Phone: "none",
|
||||
}
|
||||
|
||||
user := &User{
|
||||
FirstName: "Badger",
|
||||
LastName: "Smith",
|
||||
Age: 135,
|
||||
Email: "Badger.Smith@gmail.com",
|
||||
FavouriteColor: "#000-",
|
||||
Addresses: []*Address{address},
|
||||
}
|
||||
|
||||
// returns nil or ValidationErrors ( map[string]*FieldError )
|
||||
err := validate.Struct(user)
|
||||
if err != nil {
|
||||
|
||||
// this check is only needed when your code could produce
|
||||
// an invalid value for validation such as interface with nil
|
||||
// value most including myself do not usually have code like this.
|
||||
if _, ok := err.(*validator.InvalidValidationError); ok {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, err := range err.(validator.ValidationErrors) {
|
||||
|
||||
fmt.Println(err.Namespace())
|
||||
fmt.Println(err.Field())
|
||||
fmt.Println(err.StructNamespace()) // can differ when a custom TagNameFunc is registered or
|
||||
fmt.Println(err.StructField()) // by passing alt name to ReportError like below
|
||||
fmt.Println(err.Tag())
|
||||
fmt.Println(err.ActualTag())
|
||||
fmt.Println(err.Kind())
|
||||
fmt.Println(err.Type())
|
||||
fmt.Println(err.Value())
|
||||
fmt.Println(err.Param())
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
// from here you can create your own error messages in whatever language you wish
|
||||
return
|
||||
}
|
||||
|
||||
// save user to database
|
||||
}
|
||||
|
||||
func validateVariable() {
|
||||
|
||||
myEmail := "joeybloggs.gmail.com"
|
||||
|
||||
errs := validate.Var(myEmail, "required,email")
|
||||
|
||||
if errs != nil {
|
||||
fmt.Println(errs) // output: Key: "" Error:Field validation for "" failed on the "email" tag
|
||||
return
|
||||
}
|
||||
|
||||
// email ok, move on
|
||||
}
|
109
vendor/gopkg.in/go-playground/validator.v9/_examples/struct-level/main.go
generated
vendored
Normal file
109
vendor/gopkg.in/go-playground/validator.v9/_examples/struct-level/main.go
generated
vendored
Normal file
|
@ -0,0 +1,109 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gopkg.in/go-playground/validator.v9"
|
||||
)
|
||||
|
||||
// User contains user information
|
||||
type User struct {
|
||||
FirstName string `json:"fname"`
|
||||
LastName string `json:"lname"`
|
||||
Age uint8 `validate:"gte=0,lte=130"`
|
||||
Email string `validate:"required,email"`
|
||||
FavouriteColor string `validate:"hexcolor|rgb|rgba"`
|
||||
Addresses []*Address `validate:"required,dive,required"` // a person can have a home and cottage...
|
||||
}
|
||||
|
||||
// Address houses a users address information
|
||||
type Address struct {
|
||||
Street string `validate:"required"`
|
||||
City string `validate:"required"`
|
||||
Planet string `validate:"required"`
|
||||
Phone string `validate:"required"`
|
||||
}
|
||||
|
||||
// use a single instance of Validate, it caches struct info
|
||||
var validate *validator.Validate
|
||||
|
||||
func main() {
|
||||
|
||||
validate = validator.New()
|
||||
|
||||
// register validation for 'User'
|
||||
// NOTE: only have to register a non-pointer type for 'User', validator
|
||||
// interanlly dereferences during it's type checks.
|
||||
validate.RegisterStructValidation(UserStructLevelValidation, User{})
|
||||
|
||||
// build 'User' info, normally posted data etc...
|
||||
address := &Address{
|
||||
Street: "Eavesdown Docks",
|
||||
Planet: "Persphone",
|
||||
Phone: "none",
|
||||
City: "Unknown",
|
||||
}
|
||||
|
||||
user := &User{
|
||||
FirstName: "",
|
||||
LastName: "",
|
||||
Age: 45,
|
||||
Email: "Badger.Smith@gmail.com",
|
||||
FavouriteColor: "#000",
|
||||
Addresses: []*Address{address},
|
||||
}
|
||||
|
||||
// returns InvalidValidationError for bad validation input, nil or ValidationErrors ( []FieldError )
|
||||
err := validate.Struct(user)
|
||||
if err != nil {
|
||||
|
||||
// this check is only needed when your code could produce
|
||||
// an invalid value for validation such as interface with nil
|
||||
// value most including myself do not usually have code like this.
|
||||
if _, ok := err.(*validator.InvalidValidationError); ok {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, err := range err.(validator.ValidationErrors) {
|
||||
|
||||
fmt.Println(err.Namespace())
|
||||
fmt.Println(err.Field())
|
||||
fmt.Println(err.StructNamespace()) // can differ when a custom TagNameFunc is registered or
|
||||
fmt.Println(err.StructField()) // by passing alt name to ReportError like below
|
||||
fmt.Println(err.Tag())
|
||||
fmt.Println(err.ActualTag())
|
||||
fmt.Println(err.Kind())
|
||||
fmt.Println(err.Type())
|
||||
fmt.Println(err.Value())
|
||||
fmt.Println(err.Param())
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
// from here you can create your own error messages in whatever language you wish
|
||||
return
|
||||
}
|
||||
|
||||
// save user to database
|
||||
}
|
||||
|
||||
// UserStructLevelValidation contains custom struct level validations that don't always
|
||||
// make sense at the field validation level. For Example this function validates that either
|
||||
// FirstName or LastName exist; could have done that with a custom field validation but then
|
||||
// would have had to add it to both fields duplicating the logic + overhead, this way it's
|
||||
// only validated once.
|
||||
//
|
||||
// NOTE: you may ask why wouldn't I just do this outside of validator, because doing this way
|
||||
// hooks right into validator and you can combine with validation tags and still have a
|
||||
// common error output format.
|
||||
func UserStructLevelValidation(sl validator.StructLevel) {
|
||||
|
||||
user := sl.Current().Interface().(User)
|
||||
|
||||
if len(user.FirstName) == 0 && len(user.LastName) == 0 {
|
||||
sl.ReportError(user.FirstName, "FirstName", "fname", "fnameorlname", "")
|
||||
sl.ReportError(user.LastName, "LastName", "lname", "fnameorlname", "")
|
||||
}
|
||||
|
||||
// plus can to more, even with different tag than "fnameorlname"
|
||||
}
|
129
vendor/gopkg.in/go-playground/validator.v9/_examples/translations/main.go
generated
vendored
Normal file
129
vendor/gopkg.in/go-playground/validator.v9/_examples/translations/main.go
generated
vendored
Normal file
|
@ -0,0 +1,129 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-playground/locales/en"
|
||||
ut "github.com/go-playground/universal-translator"
|
||||
"gopkg.in/go-playground/validator.v9"
|
||||
en_translations "gopkg.in/go-playground/validator.v9/translations/en"
|
||||
)
|
||||
|
||||
// User contains user information
|
||||
type User struct {
|
||||
FirstName string `validate:"required"`
|
||||
LastName string `validate:"required"`
|
||||
Age uint8 `validate:"gte=0,lte=130"`
|
||||
Email string `validate:"required,email"`
|
||||
FavouriteColor string `validate:"iscolor"` // alias for 'hexcolor|rgb|rgba|hsl|hsla'
|
||||
Addresses []*Address `validate:"required,dive,required"` // a person can have a home and cottage...
|
||||
}
|
||||
|
||||
// Address houses a users address information
|
||||
type Address struct {
|
||||
Street string `validate:"required"`
|
||||
City string `validate:"required"`
|
||||
Planet string `validate:"required"`
|
||||
Phone string `validate:"required"`
|
||||
}
|
||||
|
||||
// use a single instance , it caches struct info
|
||||
var (
|
||||
uni *ut.UniversalTranslator
|
||||
validate *validator.Validate
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
// NOTE: ommitting allot of error checking for brevity
|
||||
|
||||
en := en.New()
|
||||
uni = ut.New(en, en)
|
||||
|
||||
// this is usually know or extracted from http 'Accept-Language' header
|
||||
// also see uni.FindTranslator(...)
|
||||
trans, _ := uni.GetTranslator("en")
|
||||
|
||||
validate = validator.New()
|
||||
en_translations.RegisterDefaultTranslations(validate, trans)
|
||||
|
||||
translateAll(trans)
|
||||
translateIndividual(trans)
|
||||
translateOverride(trans) // yep you can specify your own in whatever locale you want!
|
||||
}
|
||||
|
||||
func translateAll(trans ut.Translator) {
|
||||
|
||||
type User struct {
|
||||
Username string `validate:"required"`
|
||||
Tagline string `validate:"required,lt=10"`
|
||||
Tagline2 string `validate:"required,gt=1"`
|
||||
}
|
||||
|
||||
user := User{
|
||||
Username: "Joeybloggs",
|
||||
Tagline: "This tagline is way too long.",
|
||||
Tagline2: "1",
|
||||
}
|
||||
|
||||
err := validate.Struct(user)
|
||||
if err != nil {
|
||||
|
||||
// translate all error at once
|
||||
errs := err.(validator.ValidationErrors)
|
||||
|
||||
// returns a map with key = namespace & value = translated error
|
||||
// NOTICE: 2 errors are returned and you'll see something surprising
|
||||
// translations are i18n aware!!!!
|
||||
// eg. '10 characters' vs '1 character'
|
||||
fmt.Println(errs.Translate(trans))
|
||||
}
|
||||
}
|
||||
|
||||
func translateIndividual(trans ut.Translator) {
|
||||
|
||||
type User struct {
|
||||
Username string `validate:"required"`
|
||||
}
|
||||
|
||||
var user User
|
||||
|
||||
err := validate.Struct(user)
|
||||
if err != nil {
|
||||
|
||||
errs := err.(validator.ValidationErrors)
|
||||
|
||||
for _, e := range errs {
|
||||
// can translate each error one at a time.
|
||||
fmt.Println(e.Translate(trans))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func translateOverride(trans ut.Translator) {
|
||||
|
||||
validate.RegisterTranslation("required", trans, func(ut ut.Translator) error {
|
||||
return ut.Add("required", "{0} must have a value!", true) // see universal-translator for details
|
||||
}, func(ut ut.Translator, fe validator.FieldError) string {
|
||||
t, _ := ut.T("required", fe.Field())
|
||||
|
||||
return t
|
||||
})
|
||||
|
||||
type User struct {
|
||||
Username string `validate:"required"`
|
||||
}
|
||||
|
||||
var user User
|
||||
|
||||
err := validate.Struct(user)
|
||||
if err != nil {
|
||||
|
||||
errs := err.(validator.ValidationErrors)
|
||||
|
||||
for _, e := range errs {
|
||||
// can translate each error one at a time.
|
||||
fmt.Println(e.Translate(trans))
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,267 @@
|
|||
package validator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
type tagType uint8
|
||||
|
||||
const (
|
||||
typeDefault tagType = iota
|
||||
typeOmitEmpty
|
||||
typeNoStructLevel
|
||||
typeStructOnly
|
||||
typeDive
|
||||
typeOr
|
||||
)
|
||||
|
||||
const (
|
||||
invalidValidation = "Invalid validation tag on field '%s'"
|
||||
undefinedValidation = "Undefined validation function '%s' on field '%s'"
|
||||
)
|
||||
|
||||
type structCache struct {
|
||||
lock sync.Mutex
|
||||
m atomic.Value // map[reflect.Type]*cStruct
|
||||
}
|
||||
|
||||
func (sc *structCache) Get(key reflect.Type) (c *cStruct, found bool) {
|
||||
c, found = sc.m.Load().(map[reflect.Type]*cStruct)[key]
|
||||
return
|
||||
}
|
||||
|
||||
func (sc *structCache) Set(key reflect.Type, value *cStruct) {
|
||||
|
||||
m := sc.m.Load().(map[reflect.Type]*cStruct)
|
||||
|
||||
nm := make(map[reflect.Type]*cStruct, len(m)+1)
|
||||
for k, v := range m {
|
||||
nm[k] = v
|
||||
}
|
||||
nm[key] = value
|
||||
sc.m.Store(nm)
|
||||
}
|
||||
|
||||
type tagCache struct {
|
||||
lock sync.Mutex
|
||||
m atomic.Value // map[string]*cTag
|
||||
}
|
||||
|
||||
func (tc *tagCache) Get(key string) (c *cTag, found bool) {
|
||||
c, found = tc.m.Load().(map[string]*cTag)[key]
|
||||
return
|
||||
}
|
||||
|
||||
func (tc *tagCache) Set(key string, value *cTag) {
|
||||
|
||||
m := tc.m.Load().(map[string]*cTag)
|
||||
|
||||
nm := make(map[string]*cTag, len(m)+1)
|
||||
for k, v := range m {
|
||||
nm[k] = v
|
||||
}
|
||||
nm[key] = value
|
||||
tc.m.Store(nm)
|
||||
}
|
||||
|
||||
type cStruct struct {
|
||||
name string
|
||||
fields []*cField
|
||||
fn StructLevelFuncCtx
|
||||
}
|
||||
|
||||
type cField struct {
|
||||
idx int
|
||||
name string
|
||||
altName string
|
||||
namesEqual bool
|
||||
cTags *cTag
|
||||
}
|
||||
|
||||
type cTag struct {
|
||||
tag string
|
||||
aliasTag string
|
||||
actualAliasTag string
|
||||
param string
|
||||
hasAlias bool
|
||||
typeof tagType
|
||||
hasTag bool
|
||||
fn FuncCtx
|
||||
next *cTag
|
||||
}
|
||||
|
||||
func (v *Validate) extractStructCache(current reflect.Value, sName string) *cStruct {
|
||||
|
||||
v.structCache.lock.Lock()
|
||||
defer v.structCache.lock.Unlock() // leave as defer! because if inner panics, it will never get unlocked otherwise!
|
||||
|
||||
typ := current.Type()
|
||||
|
||||
// could have been multiple trying to access, but once first is done this ensures struct
|
||||
// isn't parsed again.
|
||||
cs, ok := v.structCache.Get(typ)
|
||||
if ok {
|
||||
return cs
|
||||
}
|
||||
|
||||
cs = &cStruct{name: sName, fields: make([]*cField, 0), fn: v.structLevelFuncs[typ]}
|
||||
|
||||
numFields := current.NumField()
|
||||
|
||||
var ctag *cTag
|
||||
var fld reflect.StructField
|
||||
var tag string
|
||||
var customName string
|
||||
|
||||
for i := 0; i < numFields; i++ {
|
||||
|
||||
fld = typ.Field(i)
|
||||
|
||||
if !fld.Anonymous && len(fld.PkgPath) > 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
tag = fld.Tag.Get(v.tagName)
|
||||
|
||||
if tag == skipValidationTag {
|
||||
continue
|
||||
}
|
||||
|
||||
customName = fld.Name
|
||||
|
||||
if v.hasTagNameFunc {
|
||||
|
||||
name := v.tagNameFunc(fld)
|
||||
|
||||
if len(name) > 0 {
|
||||
customName = name
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: cannot use shared tag cache, because tags may be equal, but things like alias may be different
|
||||
// and so only struct level caching can be used instead of combined with Field tag caching
|
||||
|
||||
if len(tag) > 0 {
|
||||
ctag, _ = v.parseFieldTagsRecursive(tag, fld.Name, "", false)
|
||||
} else {
|
||||
// even if field doesn't have validations need cTag for traversing to potential inner/nested
|
||||
// elements of the field.
|
||||
ctag = new(cTag)
|
||||
}
|
||||
|
||||
cs.fields = append(cs.fields, &cField{
|
||||
idx: i,
|
||||
name: fld.Name,
|
||||
altName: customName,
|
||||
cTags: ctag,
|
||||
namesEqual: fld.Name == customName,
|
||||
})
|
||||
}
|
||||
|
||||
v.structCache.Set(typ, cs)
|
||||
|
||||
return cs
|
||||
}
|
||||
|
||||
func (v *Validate) parseFieldTagsRecursive(tag string, fieldName string, alias string, hasAlias bool) (firstCtag *cTag, current *cTag) {
|
||||
|
||||
var t string
|
||||
var ok bool
|
||||
noAlias := len(alias) == 0
|
||||
tags := strings.Split(tag, tagSeparator)
|
||||
|
||||
for i := 0; i < len(tags); i++ {
|
||||
|
||||
t = tags[i]
|
||||
|
||||
if noAlias {
|
||||
alias = t
|
||||
}
|
||||
|
||||
// check map for alias and process new tags, otherwise process as usual
|
||||
if tagsVal, found := v.aliases[t]; found {
|
||||
|
||||
if i == 0 {
|
||||
firstCtag, current = v.parseFieldTagsRecursive(tagsVal, fieldName, t, true)
|
||||
} else {
|
||||
next, curr := v.parseFieldTagsRecursive(tagsVal, fieldName, t, true)
|
||||
current.next, current = next, curr
|
||||
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if i == 0 {
|
||||
current = &cTag{aliasTag: alias, hasAlias: hasAlias, hasTag: true}
|
||||
firstCtag = current
|
||||
} else {
|
||||
current.next = &cTag{aliasTag: alias, hasAlias: hasAlias, hasTag: true}
|
||||
current = current.next
|
||||
}
|
||||
|
||||
switch t {
|
||||
|
||||
case diveTag:
|
||||
current.typeof = typeDive
|
||||
continue
|
||||
|
||||
case omitempty:
|
||||
current.typeof = typeOmitEmpty
|
||||
continue
|
||||
|
||||
case structOnlyTag:
|
||||
current.typeof = typeStructOnly
|
||||
continue
|
||||
|
||||
case noStructLevelTag:
|
||||
current.typeof = typeNoStructLevel
|
||||
continue
|
||||
|
||||
default:
|
||||
|
||||
// if a pipe character is needed within the param you must use the utf8Pipe representation "0x7C"
|
||||
orVals := strings.Split(t, orSeparator)
|
||||
|
||||
for j := 0; j < len(orVals); j++ {
|
||||
|
||||
vals := strings.SplitN(orVals[j], tagKeySeparator, 2)
|
||||
|
||||
if noAlias {
|
||||
alias = vals[0]
|
||||
current.aliasTag = alias
|
||||
} else {
|
||||
current.actualAliasTag = t
|
||||
}
|
||||
|
||||
if j > 0 {
|
||||
current.next = &cTag{aliasTag: alias, actualAliasTag: current.actualAliasTag, hasAlias: hasAlias, hasTag: true}
|
||||
current = current.next
|
||||
}
|
||||
|
||||
current.tag = vals[0]
|
||||
if len(current.tag) == 0 {
|
||||
panic(strings.TrimSpace(fmt.Sprintf(invalidValidation, fieldName)))
|
||||
}
|
||||
|
||||
if current.fn, ok = v.validations[current.tag]; !ok {
|
||||
panic(strings.TrimSpace(fmt.Sprintf(undefinedValidation, current.tag, fieldName)))
|
||||
}
|
||||
|
||||
if len(orVals) > 1 {
|
||||
current.typeof = typeOr
|
||||
}
|
||||
|
||||
if len(vals) > 1 {
|
||||
current.param = strings.Replace(strings.Replace(vals[1], utf8HexComma, ",", -1), utf8Pipe, "|", -1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
|
@ -0,0 +1,841 @@
|
|||
/*
|
||||
Package validator implements value validations for structs and individual fields
|
||||
based on tags.
|
||||
|
||||
It can also handle Cross-Field and Cross-Struct validation for nested structs
|
||||
and has the ability to dive into arrays and maps of any type.
|
||||
|
||||
see more examples https://github.com/go-playground/validator/tree/v9/_examples
|
||||
|
||||
Validation Functions Return Type error
|
||||
|
||||
Doing things this way is actually the way the standard library does, see the
|
||||
file.Open method here:
|
||||
|
||||
https://golang.org/pkg/os/#Open.
|
||||
|
||||
The authors return type "error" to avoid the issue discussed in the following,
|
||||
where err is always != nil:
|
||||
|
||||
http://stackoverflow.com/a/29138676/3158232
|
||||
https://github.com/go-playground/validator/issues/134
|
||||
|
||||
Validator only InvalidValidationError for bad validation input, nil or
|
||||
ValidationErrors as type error; so, in your code all you need to do is check
|
||||
if the error returned is not nil, and if it's not check if error is
|
||||
InvalidValidationError ( if necessary, most of the time it isn't ) type cast
|
||||
it to type ValidationErrors like so err.(validator.ValidationErrors).
|
||||
|
||||
Custom Validation Functions
|
||||
|
||||
Custom Validation functions can be added. Example:
|
||||
|
||||
// Structure
|
||||
func customFunc(fl FielddLevel) bool {
|
||||
|
||||
if fl.Field().String() == "invalid" {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
validate.RegisterValidation("custom tag name", customFunc)
|
||||
// NOTES: using the same tag name as an existing function
|
||||
// will overwrite the existing one
|
||||
|
||||
Cross-Field Validation
|
||||
|
||||
Cross-Field Validation can be done via the following tags:
|
||||
- eqfield
|
||||
- nefield
|
||||
- gtfield
|
||||
- gtefield
|
||||
- ltfield
|
||||
- ltefield
|
||||
- eqcsfield
|
||||
- necsfield
|
||||
- gtcsfield
|
||||
- ftecsfield
|
||||
- ltcsfield
|
||||
- ltecsfield
|
||||
|
||||
If, however, some custom cross-field validation is required, it can be done
|
||||
using a custom validation.
|
||||
|
||||
Why not just have cross-fields validation tags (i.e. only eqcsfield and not
|
||||
eqfield)?
|
||||
|
||||
The reason is efficiency. If you want to check a field within the same struct
|
||||
"eqfield" only has to find the field on the same struct (1 level). But, if we
|
||||
used "eqcsfield" it could be multiple levels down. Example:
|
||||
|
||||
type Inner struct {
|
||||
StartDate time.Time
|
||||
}
|
||||
|
||||
type Outer struct {
|
||||
InnerStructField *Inner
|
||||
CreatedAt time.Time `validate:"ltecsfield=InnerStructField.StartDate"`
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
inner := &Inner{
|
||||
StartDate: now,
|
||||
}
|
||||
|
||||
outer := &Outer{
|
||||
InnerStructField: inner,
|
||||
CreatedAt: now,
|
||||
}
|
||||
|
||||
errs := validate.Struct(outer)
|
||||
|
||||
// NOTE: when calling validate.Struct(val) topStruct will be the top level struct passed
|
||||
// into the function
|
||||
// when calling validate.FieldWithValue(val, field, tag) val will be
|
||||
// whatever you pass, struct, field...
|
||||
// when calling validate.Field(field, tag) val will be nil
|
||||
|
||||
Multiple Validators
|
||||
|
||||
Multiple validators on a field will process in the order defined. Example:
|
||||
|
||||
type Test struct {
|
||||
Field `validate:"max=10,min=1"`
|
||||
}
|
||||
|
||||
// max will be checked then min
|
||||
|
||||
Bad Validator definitions are not handled by the library. Example:
|
||||
|
||||
type Test struct {
|
||||
Field `validate:"min=10,max=0"`
|
||||
}
|
||||
|
||||
// this definition of min max will never succeed
|
||||
|
||||
Using Validator Tags
|
||||
|
||||
Baked In Cross-Field validation only compares fields on the same struct.
|
||||
If Cross-Field + Cross-Struct validation is needed you should implement your
|
||||
own custom validator.
|
||||
|
||||
Comma (",") is the default separator of validation tags. If you wish to
|
||||
have a comma included within the parameter (i.e. excludesall=,) you will need to
|
||||
use the UTF-8 hex representation 0x2C, which is replaced in the code as a comma,
|
||||
so the above will become excludesall=0x2C.
|
||||
|
||||
type Test struct {
|
||||
Field `validate:"excludesall=,"` // BAD! Do not include a comma.
|
||||
Field `validate:"excludesall=0x2C"` // GOOD! Use the UTF-8 hex representation.
|
||||
}
|
||||
|
||||
Pipe ("|") is the default separator of validation tags. If you wish to
|
||||
have a pipe included within the parameter i.e. excludesall=| you will need to
|
||||
use the UTF-8 hex representation 0x7C, which is replaced in the code as a pipe,
|
||||
so the above will become excludesall=0x7C
|
||||
|
||||
type Test struct {
|
||||
Field `validate:"excludesall=|"` // BAD! Do not include a a pipe!
|
||||
Field `validate:"excludesall=0x7C"` // GOOD! Use the UTF-8 hex representation.
|
||||
}
|
||||
|
||||
|
||||
Baked In Validators and Tags
|
||||
|
||||
Here is a list of the current built in validators:
|
||||
|
||||
|
||||
Skip Field
|
||||
|
||||
Tells the validation to skip this struct field; this is particularly
|
||||
handy in ignoring embedded structs from being validated. (Usage: -)
|
||||
Usage: -
|
||||
|
||||
|
||||
Or Operator
|
||||
|
||||
This is the 'or' operator allowing multiple validators to be used and
|
||||
accepted. (Usage: rbg|rgba) <-- this would allow either rgb or rgba
|
||||
colors to be accepted. This can also be combined with 'and' for example
|
||||
( Usage: omitempty,rgb|rgba)
|
||||
|
||||
Usage: |
|
||||
|
||||
StructOnly
|
||||
|
||||
When a field that is a nested struct is encountered, and contains this flag
|
||||
any validation on the nested struct will be run, but none of the nested
|
||||
struct fields will be validated. This is usefull if inside of you program
|
||||
you know the struct will be valid, but need to verify it has been assigned.
|
||||
NOTE: only "required" and "omitempty" can be used on a struct itself.
|
||||
|
||||
Usage: structonly
|
||||
|
||||
NoStructLevel
|
||||
|
||||
Same as structonly tag except that any struct level validations will not run.
|
||||
|
||||
Usage: nostructlevel
|
||||
|
||||
Omit Empty
|
||||
|
||||
Allows conditional validation, for example if a field is not set with
|
||||
a value (Determined by the "required" validator) then other validation
|
||||
such as min or max won't run, but if a value is set validation will run.
|
||||
|
||||
Usage: omitempty
|
||||
|
||||
Dive
|
||||
|
||||
This tells the validator to dive into a slice, array or map and validate that
|
||||
level of the slice, array or map with the validation tags that follow.
|
||||
Multidimensional nesting is also supported, each level you wish to dive will
|
||||
require another dive tag.
|
||||
|
||||
Usage: dive
|
||||
|
||||
Example #1
|
||||
|
||||
[][]string with validation tag "gt=0,dive,len=1,dive,required"
|
||||
// gt=0 will be applied to []
|
||||
// len=1 will be applied to []string
|
||||
// required will be applied to string
|
||||
|
||||
Example #2
|
||||
|
||||
[][]string with validation tag "gt=0,dive,dive,required"
|
||||
// gt=0 will be applied to []
|
||||
// []string will be spared validation
|
||||
// required will be applied to string
|
||||
|
||||
Required
|
||||
|
||||
This validates that the value is not the data types default zero value.
|
||||
For numbers ensures value is not zero. For strings ensures value is
|
||||
not "". For slices, maps, pointers, interfaces, channels and functions
|
||||
ensures the value is not nil.
|
||||
|
||||
Usage: required
|
||||
|
||||
Length
|
||||
|
||||
For numbers, length will ensure that the value is
|
||||
equal to the parameter given. For strings, it checks that
|
||||
the string length is exactly that number of characters. For slices,
|
||||
arrays, and maps, validates the number of items.
|
||||
|
||||
Usage: len=10
|
||||
|
||||
Maximum
|
||||
|
||||
For numbers, max will ensure that the value is
|
||||
less than or equal to the parameter given. For strings, it checks
|
||||
that the string length is at most that number of characters. For
|
||||
slices, arrays, and maps, validates the number of items.
|
||||
|
||||
Usage: max=10
|
||||
|
||||
Minimum
|
||||
|
||||
For numbers, min will ensure that the value is
|
||||
greater or equal to the parameter given. For strings, it checks that
|
||||
the string length is at least that number of characters. For slices,
|
||||
arrays, and maps, validates the number of items.
|
||||
|
||||
Usage: min=10
|
||||
|
||||
Equals
|
||||
|
||||
For strings & numbers, eq will ensure that the value is
|
||||
equal to the parameter given. For slices, arrays, and maps,
|
||||
validates the number of items.
|
||||
|
||||
Usage: eq=10
|
||||
|
||||
Not Equal
|
||||
|
||||
For strings & numbers, ne will ensure that the value is not
|
||||
equal to the parameter given. For slices, arrays, and maps,
|
||||
validates the number of items.
|
||||
|
||||
Usage: ne=10
|
||||
|
||||
Greater Than
|
||||
|
||||
For numbers, this will ensure that the value is greater than the
|
||||
parameter given. For strings, it checks that the string length
|
||||
is greater than that number of characters. For slices, arrays
|
||||
and maps it validates the number of items.
|
||||
|
||||
Example #1
|
||||
|
||||
Usage: gt=10
|
||||
|
||||
Example #2 (time.Time)
|
||||
|
||||
For time.Time ensures the time value is greater than time.Now.UTC().
|
||||
|
||||
Usage: gt
|
||||
|
||||
Greater Than or Equal
|
||||
|
||||
Same as 'min' above. Kept both to make terminology with 'len' easier.
|
||||
|
||||
|
||||
Example #1
|
||||
|
||||
Usage: gte=10
|
||||
|
||||
Example #2 (time.Time)
|
||||
|
||||
For time.Time ensures the time value is greater than or equal to time.Now.UTC().
|
||||
|
||||
Usage: gte
|
||||
|
||||
Less Than
|
||||
|
||||
For numbers, this will ensure that the value is less than the parameter given.
|
||||
For strings, it checks that the string length is less than that number of
|
||||
characters. For slices, arrays, and maps it validates the number of items.
|
||||
|
||||
Example #1
|
||||
|
||||
Usage: lt=10
|
||||
|
||||
Example #2 (time.Time)
|
||||
For time.Time ensures the time value is less than time.Now.UTC().
|
||||
|
||||
Usage: lt
|
||||
|
||||
Less Than or Equal
|
||||
|
||||
Same as 'max' above. Kept both to make terminology with 'len' easier.
|
||||
|
||||
Example #1
|
||||
|
||||
Usage: lte=10
|
||||
|
||||
Example #2 (time.Time)
|
||||
|
||||
For time.Time ensures the time value is less than or equal to time.Now.UTC().
|
||||
|
||||
Usage: lte
|
||||
|
||||
Field Equals Another Field
|
||||
|
||||
This will validate the field value against another fields value either within
|
||||
a struct or passed in field.
|
||||
|
||||
Example #1:
|
||||
|
||||
// Validation on Password field using:
|
||||
Usage: eqfield=ConfirmPassword
|
||||
|
||||
Example #2:
|
||||
|
||||
// Validating by field:
|
||||
validate.FieldWithValue(password, confirmpassword, "eqfield")
|
||||
|
||||
Field Equals Another Field (relative)
|
||||
|
||||
This does the same as eqfield except that it validates the field provided relative
|
||||
to the top level struct.
|
||||
|
||||
Usage: eqcsfield=InnerStructField.Field)
|
||||
|
||||
Field Does Not Equal Another Field
|
||||
|
||||
This will validate the field value against another fields value either within
|
||||
a struct or passed in field.
|
||||
|
||||
Examples:
|
||||
|
||||
// Confirm two colors are not the same:
|
||||
//
|
||||
// Validation on Color field:
|
||||
Usage: nefield=Color2
|
||||
|
||||
// Validating by field:
|
||||
validate.FieldWithValue(color1, color2, "nefield")
|
||||
|
||||
Field Does Not Equal Another Field (relative)
|
||||
|
||||
This does the same as nefield except that it validates the field provided
|
||||
relative to the top level struct.
|
||||
|
||||
Usage: necsfield=InnerStructField.Field
|
||||
|
||||
Field Greater Than Another Field
|
||||
|
||||
Only valid for Numbers and time.Time types, this will validate the field value
|
||||
against another fields value either within a struct or passed in field.
|
||||
usage examples are for validation of a Start and End date:
|
||||
|
||||
Example #1:
|
||||
|
||||
// Validation on End field using:
|
||||
validate.Struct Usage(gtfield=Start)
|
||||
|
||||
Example #2:
|
||||
|
||||
// Validating by field:
|
||||
validate.FieldWithValue(start, end, "gtfield")
|
||||
|
||||
|
||||
Field Greater Than Another Relative Field
|
||||
|
||||
This does the same as gtfield except that it validates the field provided
|
||||
relative to the top level struct.
|
||||
|
||||
Usage: gtcsfield=InnerStructField.Field
|
||||
|
||||
Field Greater Than or Equal To Another Field
|
||||
|
||||
Only valid for Numbers and time.Time types, this will validate the field value
|
||||
against another fields value either within a struct or passed in field.
|
||||
usage examples are for validation of a Start and End date:
|
||||
|
||||
Example #1:
|
||||
|
||||
// Validation on End field using:
|
||||
validate.Struct Usage(gtefield=Start)
|
||||
|
||||
Example #2:
|
||||
|
||||
// Validating by field:
|
||||
validate.FieldWithValue(start, end, "gtefield")
|
||||
|
||||
Field Greater Than or Equal To Another Relative Field
|
||||
|
||||
This does the same as gtefield except that it validates the field provided relative
|
||||
to the top level struct.
|
||||
|
||||
Usage: gtecsfield=InnerStructField.Field
|
||||
|
||||
Less Than Another Field
|
||||
|
||||
Only valid for Numbers and time.Time types, this will validate the field value
|
||||
against another fields value either within a struct or passed in field.
|
||||
usage examples are for validation of a Start and End date:
|
||||
|
||||
Example #1:
|
||||
|
||||
// Validation on End field using:
|
||||
validate.Struct Usage(ltfield=Start)
|
||||
|
||||
Example #2:
|
||||
|
||||
// Validating by field:
|
||||
validate.FieldWithValue(start, end, "ltfield")
|
||||
|
||||
Less Than Another Relative Field
|
||||
|
||||
This does the same as ltfield except that it validates the field provided relative
|
||||
to the top level struct.
|
||||
|
||||
Usage: ltcsfield=InnerStructField.Field
|
||||
|
||||
Less Than or Equal To Another Field
|
||||
|
||||
Only valid for Numbers and time.Time types, this will validate the field value
|
||||
against another fields value either within a struct or passed in field.
|
||||
usage examples are for validation of a Start and End date:
|
||||
|
||||
Example #1:
|
||||
|
||||
// Validation on End field using:
|
||||
validate.Struct Usage(ltefield=Start)
|
||||
|
||||
Example #2:
|
||||
|
||||
// Validating by field:
|
||||
validate.FieldWithValue(start, end, "ltefield")
|
||||
|
||||
Less Than or Equal To Another Relative Field
|
||||
|
||||
This does the same as ltefield except that it validates the field provided relative
|
||||
to the top level struct.
|
||||
|
||||
Usage: ltecsfield=InnerStructField.Field
|
||||
|
||||
Alpha Only
|
||||
|
||||
This validates that a string value contains ASCII alpha characters only
|
||||
|
||||
Usage: alpha
|
||||
|
||||
Alphanumeric
|
||||
|
||||
This validates that a string value contains ASCII alphanumeric characters only
|
||||
|
||||
Usage: alphanum
|
||||
|
||||
Alpha Unicode
|
||||
|
||||
This validates that a string value contains unicode alpha characters only
|
||||
|
||||
Usage: alphaunicode
|
||||
|
||||
Alphanumeric Unicode
|
||||
|
||||
This validates that a string value contains unicode alphanumeric characters only
|
||||
|
||||
Usage: alphanumunicode
|
||||
|
||||
Numeric
|
||||
|
||||
This validates that a string value contains a basic numeric value.
|
||||
basic excludes exponents etc...
|
||||
|
||||
Usage: numeric
|
||||
|
||||
Hexadecimal String
|
||||
|
||||
This validates that a string value contains a valid hexadecimal.
|
||||
|
||||
Usage: hexadecimal
|
||||
|
||||
Hexcolor String
|
||||
|
||||
This validates that a string value contains a valid hex color including
|
||||
hashtag (#)
|
||||
|
||||
Usage: hexcolor
|
||||
|
||||
RGB String
|
||||
|
||||
This validates that a string value contains a valid rgb color
|
||||
|
||||
Usage: rgb
|
||||
|
||||
RGBA String
|
||||
|
||||
This validates that a string value contains a valid rgba color
|
||||
|
||||
Usage: rgba
|
||||
|
||||
HSL String
|
||||
|
||||
This validates that a string value contains a valid hsl color
|
||||
|
||||
Usage: hsl
|
||||
|
||||
HSLA String
|
||||
|
||||
This validates that a string value contains a valid hsla color
|
||||
|
||||
Usage: hsla
|
||||
|
||||
E-mail String
|
||||
|
||||
This validates that a string value contains a valid email
|
||||
This may not conform to all possibilities of any rfc standard, but neither
|
||||
does any email provider accept all posibilities.
|
||||
|
||||
Usage: email
|
||||
|
||||
URL String
|
||||
|
||||
This validates that a string value contains a valid url
|
||||
This will accept any url the golang request uri accepts but must contain
|
||||
a schema for example http:// or rtmp://
|
||||
|
||||
Usage: url
|
||||
|
||||
URI String
|
||||
|
||||
This validates that a string value contains a valid uri
|
||||
This will accept any uri the golang request uri accepts
|
||||
|
||||
Usage: uri
|
||||
|
||||
Base64 String
|
||||
|
||||
This validates that a string value contains a valid base64 value.
|
||||
Although an empty string is valid base64 this will report an empty string
|
||||
as an error, if you wish to accept an empty string as valid you can use
|
||||
this with the omitempty tag.
|
||||
|
||||
Usage: base64
|
||||
|
||||
Contains
|
||||
|
||||
This validates that a string value contains the substring value.
|
||||
|
||||
Usage: contains=@
|
||||
|
||||
Contains Any
|
||||
|
||||
This validates that a string value contains any Unicode code points
|
||||
in the substring value.
|
||||
|
||||
Usage: containsany=!@#?
|
||||
|
||||
Contains Rune
|
||||
|
||||
This validates that a string value contains the supplied rune value.
|
||||
|
||||
Usage: containsrune=@
|
||||
|
||||
Excludes
|
||||
|
||||
This validates that a string value does not contain the substring value.
|
||||
|
||||
Usage: excludes=@
|
||||
|
||||
Excludes All
|
||||
|
||||
This validates that a string value does not contain any Unicode code
|
||||
points in the substring value.
|
||||
|
||||
Usage: excludesall=!@#?
|
||||
|
||||
Excludes Rune
|
||||
|
||||
This validates that a string value does not contain the supplied rune value.
|
||||
|
||||
Usage: excludesrune=@
|
||||
|
||||
International Standard Book Number
|
||||
|
||||
This validates that a string value contains a valid isbn10 or isbn13 value.
|
||||
|
||||
Usage: isbn
|
||||
|
||||
International Standard Book Number 10
|
||||
|
||||
This validates that a string value contains a valid isbn10 value.
|
||||
|
||||
Usage: isbn10
|
||||
|
||||
International Standard Book Number 13
|
||||
|
||||
This validates that a string value contains a valid isbn13 value.
|
||||
|
||||
Usage: isbn13
|
||||
|
||||
|
||||
Universally Unique Identifier UUID
|
||||
|
||||
This validates that a string value contains a valid UUID.
|
||||
|
||||
Usage: uuid
|
||||
|
||||
Universally Unique Identifier UUID v3
|
||||
|
||||
This validates that a string value contains a valid version 3 UUID.
|
||||
|
||||
Usage: uuid3
|
||||
|
||||
Universally Unique Identifier UUID v4
|
||||
|
||||
This validates that a string value contains a valid version 4 UUID.
|
||||
|
||||
Usage: uuid4
|
||||
|
||||
Universally Unique Identifier UUID v5
|
||||
|
||||
This validates that a string value contains a valid version 5 UUID.
|
||||
|
||||
Usage: uuid5
|
||||
|
||||
ASCII
|
||||
|
||||
This validates that a string value contains only ASCII characters.
|
||||
NOTE: if the string is blank, this validates as true.
|
||||
|
||||
Usage: ascii
|
||||
|
||||
Printable ASCII
|
||||
|
||||
This validates that a string value contains only printable ASCII characters.
|
||||
NOTE: if the string is blank, this validates as true.
|
||||
|
||||
Usage: printascii
|
||||
|
||||
Multi-Byte Characters
|
||||
|
||||
This validates that a string value contains one or more multibyte characters.
|
||||
NOTE: if the string is blank, this validates as true.
|
||||
|
||||
Usage: multibyte
|
||||
|
||||
Data URL
|
||||
|
||||
This validates that a string value contains a valid DataURI.
|
||||
NOTE: this will also validate that the data portion is valid base64
|
||||
|
||||
Usage: datauri
|
||||
|
||||
Latitude
|
||||
|
||||
This validates that a string value contains a valid latitude.
|
||||
|
||||
Usage: latitude
|
||||
|
||||
Longitude
|
||||
|
||||
This validates that a string value contains a valid longitude.
|
||||
|
||||
Usage: longitude
|
||||
|
||||
Social Security Number SSN
|
||||
|
||||
This validates that a string value contains a valid U.S. Social Security Number.
|
||||
|
||||
Usage: ssn
|
||||
|
||||
Internet Protocol Address IP
|
||||
|
||||
This validates that a string value contains a valid IP Adress.
|
||||
|
||||
Usage: ip
|
||||
|
||||
Internet Protocol Address IPv4
|
||||
|
||||
This validates that a string value contains a valid v4 IP Adress.
|
||||
|
||||
Usage: ipv4
|
||||
|
||||
Internet Protocol Address IPv6
|
||||
|
||||
This validates that a string value contains a valid v6 IP Adress.
|
||||
|
||||
Usage: ipv6
|
||||
|
||||
Classless Inter-Domain Routing CIDR
|
||||
|
||||
This validates that a string value contains a valid CIDR Adress.
|
||||
|
||||
Usage: cidr
|
||||
|
||||
Classless Inter-Domain Routing CIDRv4
|
||||
|
||||
This validates that a string value contains a valid v4 CIDR Adress.
|
||||
|
||||
Usage: cidrv4
|
||||
|
||||
Classless Inter-Domain Routing CIDRv6
|
||||
|
||||
This validates that a string value contains a valid v6 CIDR Adress.
|
||||
|
||||
Usage: cidrv6
|
||||
|
||||
Transmission Control Protocol Address TCP
|
||||
|
||||
This validates that a string value contains a valid resolvable TCP Adress.
|
||||
|
||||
Usage: tcp_addr
|
||||
|
||||
Transmission Control Protocol Address TCPv4
|
||||
|
||||
This validates that a string value contains a valid resolvable v4 TCP Adress.
|
||||
|
||||
Usage: tcp4_addr
|
||||
|
||||
Transmission Control Protocol Address TCPv6
|
||||
|
||||
This validates that a string value contains a valid resolvable v6 TCP Adress.
|
||||
|
||||
Usage: tcp6_addr
|
||||
|
||||
User Datagram Protocol Address UDP
|
||||
|
||||
This validates that a string value contains a valid resolvable UDP Adress.
|
||||
|
||||
Usage: udp_addr
|
||||
|
||||
User Datagram Protocol Address UDPv4
|
||||
|
||||
This validates that a string value contains a valid resolvable v4 UDP Adress.
|
||||
|
||||
Usage: udp4_addr
|
||||
|
||||
User Datagram Protocol Address UDPv6
|
||||
|
||||
This validates that a string value contains a valid resolvable v6 UDP Adress.
|
||||
|
||||
Usage: udp6_addr
|
||||
|
||||
Internet Protocol Address IP
|
||||
|
||||
This validates that a string value contains a valid resolvable IP Adress.
|
||||
|
||||
Usage: ip_addr
|
||||
|
||||
Internet Protocol Address IPv4
|
||||
|
||||
This validates that a string value contains a valid resolvable v4 IP Adress.
|
||||
|
||||
Usage: ip4_addr
|
||||
|
||||
Internet Protocol Address IPv6
|
||||
|
||||
This validates that a string value contains a valid resolvable v6 IP Adress.
|
||||
|
||||
Usage: ip6_addr
|
||||
|
||||
Unix domain socket end point Address
|
||||
|
||||
This validates that a string value contains a valid Unix Adress.
|
||||
|
||||
Usage: unix_addr
|
||||
|
||||
Media Access Control Address MAC
|
||||
|
||||
This validates that a string value contains a valid MAC Adress.
|
||||
|
||||
Usage: mac
|
||||
|
||||
Note: See Go's ParseMAC for accepted formats and types:
|
||||
|
||||
http://golang.org/src/net/mac.go?s=866:918#L29
|
||||
|
||||
Alias Validators and Tags
|
||||
|
||||
NOTE: When returning an error, the tag returned in "FieldError" will be
|
||||
the alias tag unless the dive tag is part of the alias. Everything after the
|
||||
dive tag is not reported as the alias tag. Also, the "ActualTag" in the before
|
||||
case will be the actual tag within the alias that failed.
|
||||
|
||||
Here is a list of the current built in alias tags:
|
||||
|
||||
"iscolor"
|
||||
alias is "hexcolor|rgb|rgba|hsl|hsla" (Usage: iscolor)
|
||||
|
||||
Validator notes:
|
||||
|
||||
regex
|
||||
a regex validator won't be added because commas and = signs can be part
|
||||
of a regex which conflict with the validation definitions. Although
|
||||
workarounds can be made, they take away from using pure regex's.
|
||||
Furthermore it's quick and dirty but the regex's become harder to
|
||||
maintain and are not reusable, so it's as much a programming philosiphy
|
||||
as anything.
|
||||
|
||||
In place of this new validator functions should be created; a regex can
|
||||
be used within the validator function and even be precompiled for better
|
||||
efficiency within regexes.go.
|
||||
|
||||
And the best reason, you can submit a pull request and we can keep on
|
||||
adding to the validation library of this package!
|
||||
|
||||
Panics
|
||||
|
||||
This package panics when bad input is provided, this is by design, bad code like
|
||||
that should not make it to production.
|
||||
|
||||
type Test struct {
|
||||
TestField string `validate:"nonexistantfunction=1"`
|
||||
}
|
||||
|
||||
t := &Test{
|
||||
TestField: "Test"
|
||||
}
|
||||
|
||||
validate.Struct(t) // this will panic
|
||||
*/
|
||||
package validator
|
|
@ -0,0 +1,270 @@
|
|||
package validator
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
ut "github.com/go-playground/universal-translator"
|
||||
)
|
||||
|
||||
const (
|
||||
fieldErrMsg = "Key: '%s' Error:Field validation for '%s' failed on the '%s' tag"
|
||||
)
|
||||
|
||||
// ValidationErrorsTranslations is the translation return type
|
||||
type ValidationErrorsTranslations map[string]string
|
||||
|
||||
// InvalidValidationError describes an invalid argument passed to
|
||||
// `Struct`, `StructExcept`, StructPartial` or `Field`
|
||||
type InvalidValidationError struct {
|
||||
Type reflect.Type
|
||||
}
|
||||
|
||||
// Error returns InvalidValidationError message
|
||||
func (e *InvalidValidationError) Error() string {
|
||||
|
||||
if e.Type == nil {
|
||||
return "validator: (nil)"
|
||||
}
|
||||
|
||||
return "validator: (nil " + e.Type.String() + ")"
|
||||
}
|
||||
|
||||
// ValidationErrors is an array of FieldError's
|
||||
// for use in custom error messages post validation.
|
||||
type ValidationErrors []FieldError
|
||||
|
||||
// Error is intended for use in development + debugging and not intended to be a production error message.
|
||||
// It allows ValidationErrors to subscribe to the Error interface.
|
||||
// All information to create an error message specific to your application is contained within
|
||||
// the FieldError found within the ValidationErrors array
|
||||
func (ve ValidationErrors) Error() string {
|
||||
|
||||
buff := bytes.NewBufferString("")
|
||||
|
||||
var fe *fieldError
|
||||
|
||||
for i := 0; i < len(ve); i++ {
|
||||
|
||||
fe = ve[i].(*fieldError)
|
||||
buff.WriteString(fe.Error())
|
||||
buff.WriteString("\n")
|
||||
}
|
||||
|
||||
return strings.TrimSpace(buff.String())
|
||||
}
|
||||
|
||||
// Translate translates all of the ValidationErrors
|
||||
func (ve ValidationErrors) Translate(ut ut.Translator) ValidationErrorsTranslations {
|
||||
|
||||
trans := make(ValidationErrorsTranslations)
|
||||
|
||||
var fe *fieldError
|
||||
|
||||
for i := 0; i < len(ve); i++ {
|
||||
fe = ve[i].(*fieldError)
|
||||
|
||||
// // in case an Anonymous struct was used, ensure that the key
|
||||
// // would be 'Username' instead of ".Username"
|
||||
// if len(fe.ns) > 0 && fe.ns[:1] == "." {
|
||||
// trans[fe.ns[1:]] = fe.Translate(ut)
|
||||
// continue
|
||||
// }
|
||||
|
||||
trans[fe.ns] = fe.Translate(ut)
|
||||
}
|
||||
|
||||
return trans
|
||||
}
|
||||
|
||||
// FieldError contains all functions to get error details
|
||||
type FieldError interface {
|
||||
|
||||
// returns the validation tag that failed. if the
|
||||
// validation was an alias, this will return the
|
||||
// alias name and not the underlying tag that failed.
|
||||
//
|
||||
// eg. alias "iscolor": "hexcolor|rgb|rgba|hsl|hsla"
|
||||
// will return "iscolor"
|
||||
Tag() string
|
||||
|
||||
// returns the validation tag that failed, even if an
|
||||
// alias the actual tag within the alias will be returned.
|
||||
// If an 'or' validation fails the entire or will be returned.
|
||||
//
|
||||
// eg. alias "iscolor": "hexcolor|rgb|rgba|hsl|hsla"
|
||||
// will return "hexcolor|rgb|rgba|hsl|hsla"
|
||||
ActualTag() string
|
||||
|
||||
// returns the namespace for the field error, with the tag
|
||||
// name taking precedence over the fields actual name.
|
||||
//
|
||||
// eq. JSON name "User.fname" see ActualNamespace for comparison
|
||||
//
|
||||
// NOTE: this field can be blank when validating a single primitive field
|
||||
// using validate.Field(...) as there is no way to extract it's name
|
||||
Namespace() string
|
||||
|
||||
// returns the namespace for the field error, with the fields
|
||||
// actual name.
|
||||
//
|
||||
// eq. "User.FirstName" see Namespace for comparison
|
||||
//
|
||||
// NOTE: this field can be blank when validating a single primitive field
|
||||
// using validate.Field(...) as there is no way to extract it's name
|
||||
StructNamespace() string
|
||||
|
||||
// returns the fields name with the tag name taking precedence over the
|
||||
// fields actual name.
|
||||
//
|
||||
// eq. JSON name "fname"
|
||||
// see ActualField for comparison
|
||||
Field() string
|
||||
|
||||
// returns the fields actual name from the struct, when able to determine.
|
||||
//
|
||||
// eq. "FirstName"
|
||||
// see Field for comparison
|
||||
StructField() string
|
||||
|
||||
// returns the actual fields value in case needed for creating the error
|
||||
// message
|
||||
Value() interface{}
|
||||
|
||||
// returns the param value, in string form for comparison; this will also
|
||||
// help with generating an error message
|
||||
Param() string
|
||||
|
||||
// Kind returns the Field's reflect Kind
|
||||
//
|
||||
// eg. time.Time's kind is a struct
|
||||
Kind() reflect.Kind
|
||||
|
||||
// Type returns the Field's reflect Type
|
||||
//
|
||||
// // eg. time.Time's type is time.Time
|
||||
Type() reflect.Type
|
||||
|
||||
// returns the FieldError's translated error
|
||||
// from the provided 'ut.Translator' and registered 'TranslationFunc'
|
||||
//
|
||||
// NOTE: is not registered translation can be found it returns the same
|
||||
// as calling fe.Error()
|
||||
Translate(ut ut.Translator) string
|
||||
}
|
||||
|
||||
// compile time interface checks
|
||||
var _ FieldError = new(fieldError)
|
||||
var _ error = new(fieldError)
|
||||
|
||||
// fieldError contains a single field's validation error along
|
||||
// with other properties that may be needed for error message creation
|
||||
// it complies with the FieldError interface
|
||||
type fieldError struct {
|
||||
v *Validate
|
||||
tag string
|
||||
actualTag string
|
||||
ns string
|
||||
structNs string
|
||||
fieldLen uint8
|
||||
structfieldLen uint8
|
||||
value interface{}
|
||||
param string
|
||||
kind reflect.Kind
|
||||
typ reflect.Type
|
||||
}
|
||||
|
||||
// Tag returns the validation tag that failed.
|
||||
func (fe *fieldError) Tag() string {
|
||||
return fe.tag
|
||||
}
|
||||
|
||||
// ActualTag returns the validation tag that failed, even if an
|
||||
// alias the actual tag within the alias will be returned.
|
||||
func (fe *fieldError) ActualTag() string {
|
||||
return fe.actualTag
|
||||
}
|
||||
|
||||
// Namespace returns the namespace for the field error, with the tag
|
||||
// name taking precedence over the fields actual name.
|
||||
func (fe *fieldError) Namespace() string {
|
||||
return fe.ns
|
||||
}
|
||||
|
||||
// StructNamespace returns the namespace for the field error, with the fields
|
||||
// actual name.
|
||||
func (fe *fieldError) StructNamespace() string {
|
||||
return fe.structNs
|
||||
}
|
||||
|
||||
// Field returns the fields name with the tag name taking precedence over the
|
||||
// fields actual name.
|
||||
func (fe *fieldError) Field() string {
|
||||
|
||||
return fe.ns[len(fe.ns)-int(fe.fieldLen):]
|
||||
// // return fe.field
|
||||
// fld := fe.ns[len(fe.ns)-int(fe.fieldLen):]
|
||||
|
||||
// log.Println("FLD:", fld)
|
||||
|
||||
// if len(fld) > 0 && fld[:1] == "." {
|
||||
// return fld[1:]
|
||||
// }
|
||||
|
||||
// return fld
|
||||
}
|
||||
|
||||
// returns the fields actual name from the struct, when able to determine.
|
||||
func (fe *fieldError) StructField() string {
|
||||
// return fe.structField
|
||||
return fe.structNs[len(fe.structNs)-int(fe.structfieldLen):]
|
||||
}
|
||||
|
||||
// Value returns the actual fields value in case needed for creating the error
|
||||
// message
|
||||
func (fe *fieldError) Value() interface{} {
|
||||
return fe.value
|
||||
}
|
||||
|
||||
// Param returns the param value, in string form for comparison; this will
|
||||
// also help with generating an error message
|
||||
func (fe *fieldError) Param() string {
|
||||
return fe.param
|
||||
}
|
||||
|
||||
// Kind returns the Field's reflect Kind
|
||||
func (fe *fieldError) Kind() reflect.Kind {
|
||||
return fe.kind
|
||||
}
|
||||
|
||||
// Type returns the Field's reflect Type
|
||||
func (fe *fieldError) Type() reflect.Type {
|
||||
return fe.typ
|
||||
}
|
||||
|
||||
// Error returns the fieldError's error message
|
||||
func (fe *fieldError) Error() string {
|
||||
return fmt.Sprintf(fieldErrMsg, fe.ns, fe.Field(), fe.tag)
|
||||
}
|
||||
|
||||
// Translate returns the FieldError's translated error
|
||||
// from the provided 'ut.Translator' and registered 'TranslationFunc'
|
||||
//
|
||||
// NOTE: is not registered translation can be found it returns the same
|
||||
// as calling fe.Error()
|
||||
func (fe *fieldError) Translate(ut ut.Translator) string {
|
||||
|
||||
m, ok := fe.v.transTagFunc[ut]
|
||||
if !ok {
|
||||
return fe.Error()
|
||||
}
|
||||
|
||||
fn, ok := m[fe.tag]
|
||||
if !ok {
|
||||
return fe.Error()
|
||||
}
|
||||
|
||||
return fn(ut, fe)
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
package validator
|
||||
|
||||
import "reflect"
|
||||
|
||||
// FieldLevel contains all the information and helper functions
|
||||
// to validate a field
|
||||
type FieldLevel interface {
|
||||
|
||||
// returns the top level struct, if any
|
||||
Top() reflect.Value
|
||||
|
||||
// returns the current fields parent struct, if any or
|
||||
// the comparison value if called 'VarWithValue'
|
||||
Parent() reflect.Value
|
||||
|
||||
// returns current field for validation
|
||||
Field() reflect.Value
|
||||
|
||||
// returns the field's name with the tag
|
||||
// name takeing precedence over the fields actual name.
|
||||
FieldName() string
|
||||
|
||||
// returns the struct field's name
|
||||
StructFieldName() string
|
||||
|
||||
// returns param for validation against current field
|
||||
Param() string
|
||||
|
||||
// ExtractType gets the actual underlying type of field value.
|
||||
// It will dive into pointers, customTypes and return you the
|
||||
// underlying value and it's kind.
|
||||
ExtractType(field reflect.Value) (value reflect.Value, kind reflect.Kind, nullable bool)
|
||||
|
||||
// traverses the parent struct to retrieve a specific field denoted by the provided namespace
|
||||
// in the param and returns the field, field kind and whether is was successful in retrieving
|
||||
// the field at all.
|
||||
//
|
||||
// NOTE: when not successful ok will be false, this can happen when a nested struct is nil and so the field
|
||||
// could not be retrieved because it didn't exist.
|
||||
GetStructFieldOK() (reflect.Value, reflect.Kind, bool)
|
||||
}
|
||||
|
||||
var _ FieldLevel = new(validate)
|
||||
|
||||
// Field returns current field for validation
|
||||
func (v *validate) Field() reflect.Value {
|
||||
return v.flField
|
||||
}
|
||||
|
||||
// FieldName returns the field's name with the tag
|
||||
// name takeing precedence over the fields actual name.
|
||||
func (v *validate) FieldName() string {
|
||||
return v.cf.altName
|
||||
}
|
||||
|
||||
// StructFieldName returns the struct field's name
|
||||
func (v *validate) StructFieldName() string {
|
||||
return v.cf.name
|
||||
}
|
||||
|
||||
// Param returns param for validation against current field
|
||||
func (v *validate) Param() string {
|
||||
return v.ct.param
|
||||
}
|
||||
|
||||
// GetStructFieldOK returns Param returns param for validation against current field
|
||||
func (v *validate) GetStructFieldOK() (reflect.Value, reflect.Kind, bool) {
|
||||
return v.getStructFieldOKInternal(v.slflParent, v.ct.param)
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
|
@ -0,0 +1,63 @@
|
|||
package validator
|
||||
|
||||
import "regexp"
|
||||
|
||||
const (
|
||||
alphaRegexString = "^[a-zA-Z]+$"
|
||||
alphaNumericRegexString = "^[a-zA-Z0-9]+$"
|
||||
alphaUnicodeRegexString = "^[\\p{L}]+$"
|
||||
alphaUnicodeNumericRegexString = "^[\\p{L}\\p{N}]+$"
|
||||
numericRegexString = "^[-+]?[0-9]+(?:\\.[0-9]+)?$"
|
||||
numberRegexString = "^[0-9]+$"
|
||||
hexadecimalRegexString = "^[0-9a-fA-F]+$"
|
||||
hexcolorRegexString = "^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$"
|
||||
rgbRegexString = "^rgb\\(\\s*(?:(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])|(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%)\\s*\\)$"
|
||||
rgbaRegexString = "^rgba\\(\\s*(?:(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])|(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%)\\s*,\\s*(?:(?:0.[1-9]*)|[01])\\s*\\)$"
|
||||
hslRegexString = "^hsl\\(\\s*(?:0|[1-9]\\d?|[12]\\d\\d|3[0-5]\\d|360)\\s*,\\s*(?:(?:0|[1-9]\\d?|100)%)\\s*,\\s*(?:(?:0|[1-9]\\d?|100)%)\\s*\\)$"
|
||||
hslaRegexString = "^hsla\\(\\s*(?:0|[1-9]\\d?|[12]\\d\\d|3[0-5]\\d|360)\\s*,\\s*(?:(?:0|[1-9]\\d?|100)%)\\s*,\\s*(?:(?:0|[1-9]\\d?|100)%)\\s*,\\s*(?:(?:0.[1-9]*)|[01])\\s*\\)$"
|
||||
emailRegexString = "^(?:(?:(?:(?:[a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(?:\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|(?:(?:\\x22)(?:(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(?:\\x20|\\x09)+)?(?:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:\\(?:[\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(\\x20|\\x09)+)?(?:\\x22)))@(?:(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|_|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|_|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$"
|
||||
base64RegexString = "^(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+\\/]{3}=|[A-Za-z0-9+\\/]{4})$"
|
||||
iSBN10RegexString = "^(?:[0-9]{9}X|[0-9]{10})$"
|
||||
iSBN13RegexString = "^(?:(?:97(?:8|9))[0-9]{10})$"
|
||||
uUID3RegexString = "^[0-9a-f]{8}-[0-9a-f]{4}-3[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$"
|
||||
uUID4RegexString = "^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
|
||||
uUID5RegexString = "^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
|
||||
uUIDRegexString = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
|
||||
aSCIIRegexString = "^[\x00-\x7F]*$"
|
||||
printableASCIIRegexString = "^[\x20-\x7E]*$"
|
||||
multibyteRegexString = "[^\x00-\x7F]"
|
||||
dataURIRegexString = "^data:.+\\/(.+);base64$"
|
||||
latitudeRegexString = "^[-+]?([1-8]?\\d(\\.\\d+)?|90(\\.0+)?)$"
|
||||
longitudeRegexString = "^[-+]?(180(\\.0+)?|((1[0-7]\\d)|([1-9]?\\d))(\\.\\d+)?)$"
|
||||
sSNRegexString = `^\d{3}[- ]?\d{2}[- ]?\d{4}$`
|
||||
)
|
||||
|
||||
var (
|
||||
alphaRegex = regexp.MustCompile(alphaRegexString)
|
||||
alphaNumericRegex = regexp.MustCompile(alphaNumericRegexString)
|
||||
alphaUnicodeRegex = regexp.MustCompile(alphaUnicodeRegexString)
|
||||
alphaUnicodeNumericRegex = regexp.MustCompile(alphaUnicodeNumericRegexString)
|
||||
numericRegex = regexp.MustCompile(numericRegexString)
|
||||
numberRegex = regexp.MustCompile(numberRegexString)
|
||||
hexadecimalRegex = regexp.MustCompile(hexadecimalRegexString)
|
||||
hexcolorRegex = regexp.MustCompile(hexcolorRegexString)
|
||||
rgbRegex = regexp.MustCompile(rgbRegexString)
|
||||
rgbaRegex = regexp.MustCompile(rgbaRegexString)
|
||||
hslRegex = regexp.MustCompile(hslRegexString)
|
||||
hslaRegex = regexp.MustCompile(hslaRegexString)
|
||||
emailRegex = regexp.MustCompile(emailRegexString)
|
||||
base64Regex = regexp.MustCompile(base64RegexString)
|
||||
iSBN10Regex = regexp.MustCompile(iSBN10RegexString)
|
||||
iSBN13Regex = regexp.MustCompile(iSBN13RegexString)
|
||||
uUID3Regex = regexp.MustCompile(uUID3RegexString)
|
||||
uUID4Regex = regexp.MustCompile(uUID4RegexString)
|
||||
uUID5Regex = regexp.MustCompile(uUID5RegexString)
|
||||
uUIDRegex = regexp.MustCompile(uUIDRegexString)
|
||||
aSCIIRegex = regexp.MustCompile(aSCIIRegexString)
|
||||
printableASCIIRegex = regexp.MustCompile(printableASCIIRegexString)
|
||||
multibyteRegex = regexp.MustCompile(multibyteRegexString)
|
||||
dataURIRegex = regexp.MustCompile(dataURIRegexString)
|
||||
latitudeRegex = regexp.MustCompile(latitudeRegexString)
|
||||
longitudeRegex = regexp.MustCompile(longitudeRegexString)
|
||||
sSNRegex = regexp.MustCompile(sSNRegexString)
|
||||
)
|
|
@ -0,0 +1,178 @@
|
|||
package validator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// StructLevelFunc accepts all values needed for struct level validation
|
||||
type StructLevelFunc func(sl StructLevel)
|
||||
|
||||
// StructLevelFuncCtx accepts all values needed for struct level validation
|
||||
// but also allows passing of contextual validation information vi context.Context.
|
||||
type StructLevelFuncCtx func(ctx context.Context, sl StructLevel)
|
||||
|
||||
// wrapStructLevelFunc wraps noramal StructLevelFunc makes it compatible with StructLevelFuncCtx
|
||||
func wrapStructLevelFunc(fn StructLevelFunc) StructLevelFuncCtx {
|
||||
return func(ctx context.Context, sl StructLevel) {
|
||||
fn(sl)
|
||||
}
|
||||
}
|
||||
|
||||
// StructLevel contains all the information and helper functions
|
||||
// to validate a struct
|
||||
type StructLevel interface {
|
||||
|
||||
// returns the main validation object, in case one want to call validations internally.
|
||||
// this is so you don;t have to use anonymous functoins to get access to the validate
|
||||
// instance.
|
||||
Validator() *Validate
|
||||
|
||||
// returns the top level struct, if any
|
||||
Top() reflect.Value
|
||||
|
||||
// returns the current fields parent struct, if any
|
||||
Parent() reflect.Value
|
||||
|
||||
// returns the current struct.
|
||||
Current() reflect.Value
|
||||
|
||||
// ExtractType gets the actual underlying type of field value.
|
||||
// It will dive into pointers, customTypes and return you the
|
||||
// underlying value and it's kind.
|
||||
ExtractType(field reflect.Value) (value reflect.Value, kind reflect.Kind, nullable bool)
|
||||
|
||||
// reports an error just by passing the field and tag information
|
||||
//
|
||||
// NOTES:
|
||||
//
|
||||
// fieldName and altName get appended to the existing namespace that
|
||||
// validator is on. eg. pass 'FirstName' or 'Names[0]' depending
|
||||
// on the nesting
|
||||
//
|
||||
// tag can be an existing validation tag or just something you make up
|
||||
// and process on the flip side it's up to you.
|
||||
ReportError(field interface{}, fieldName, altName, tag, param string)
|
||||
|
||||
// reports an error just by passing ValidationErrors
|
||||
//
|
||||
// NOTES:
|
||||
//
|
||||
// relativeNamespace and relativeActualNamespace get appended to the
|
||||
// existing namespace that validator is on.
|
||||
// eg. pass 'User.FirstName' or 'Users[0].FirstName' depending
|
||||
// on the nesting. most of the time they will be blank, unless you validate
|
||||
// at a level lower the the current field depth
|
||||
//
|
||||
// tag can be an existing validation tag or just something you make up
|
||||
// and process on the flip side it's up to you.
|
||||
ReportValidationErrors(relativeNamespace, relativeActualNamespace string, errs ValidationErrors)
|
||||
}
|
||||
|
||||
var _ StructLevel = new(validate)
|
||||
|
||||
// Top returns the top level struct
|
||||
//
|
||||
// NOTE: this can be the same as the current struct being validated
|
||||
// if not is a nested struct.
|
||||
//
|
||||
// this is only called when within Struct and Field Level validation and
|
||||
// should not be relied upon for an acurate value otherwise.
|
||||
func (v *validate) Top() reflect.Value {
|
||||
return v.top
|
||||
}
|
||||
|
||||
// Parent returns the current structs parent
|
||||
//
|
||||
// NOTE: this can be the same as the current struct being validated
|
||||
// if not is a nested struct.
|
||||
//
|
||||
// this is only called when within Struct and Field Level validation and
|
||||
// should not be relied upon for an acurate value otherwise.
|
||||
func (v *validate) Parent() reflect.Value {
|
||||
return v.slflParent
|
||||
}
|
||||
|
||||
// Current returns the current struct.
|
||||
func (v *validate) Current() reflect.Value {
|
||||
return v.slCurrent
|
||||
}
|
||||
|
||||
// Validator returns the main validation object, in case one want to call validations internally.
|
||||
func (v *validate) Validator() *Validate {
|
||||
return v.v
|
||||
}
|
||||
|
||||
// ExtractType gets the actual underlying type of field value.
|
||||
func (v *validate) ExtractType(field reflect.Value) (reflect.Value, reflect.Kind, bool) {
|
||||
return v.extractTypeInternal(field, false)
|
||||
}
|
||||
|
||||
// ReportError reports an error just by passing the field and tag information
|
||||
func (v *validate) ReportError(field interface{}, fieldName, structFieldName, tag, param string) {
|
||||
|
||||
fv, kind, _ := v.extractTypeInternal(reflect.ValueOf(field), false)
|
||||
|
||||
if len(structFieldName) == 0 {
|
||||
structFieldName = fieldName
|
||||
}
|
||||
|
||||
v.str1 = string(append(v.ns, fieldName...))
|
||||
|
||||
if v.v.hasTagNameFunc || fieldName != structFieldName {
|
||||
v.str2 = string(append(v.actualNs, structFieldName...))
|
||||
} else {
|
||||
v.str2 = v.str1
|
||||
}
|
||||
|
||||
if kind == reflect.Invalid {
|
||||
|
||||
v.errs = append(v.errs,
|
||||
&fieldError{
|
||||
v: v.v,
|
||||
tag: tag,
|
||||
actualTag: tag,
|
||||
ns: v.str1,
|
||||
structNs: v.str2,
|
||||
fieldLen: uint8(len(fieldName)),
|
||||
structfieldLen: uint8(len(structFieldName)),
|
||||
param: param,
|
||||
kind: kind,
|
||||
},
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
v.errs = append(v.errs,
|
||||
&fieldError{
|
||||
v: v.v,
|
||||
tag: tag,
|
||||
actualTag: tag,
|
||||
ns: v.str1,
|
||||
structNs: v.str2,
|
||||
fieldLen: uint8(len(fieldName)),
|
||||
structfieldLen: uint8(len(structFieldName)),
|
||||
value: fv.Interface(),
|
||||
param: param,
|
||||
kind: kind,
|
||||
typ: fv.Type(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// ReportValidationErrors reports ValidationErrors obtained from running validations within the Struct Level validation.
|
||||
//
|
||||
// NOTE: this function prepends the current namespace to the relative ones.
|
||||
func (v *validate) ReportValidationErrors(relativeNamespace, relativeStructNamespace string, errs ValidationErrors) {
|
||||
|
||||
var err *fieldError
|
||||
|
||||
for i := 0; i < len(errs); i++ {
|
||||
|
||||
err = errs[i].(*fieldError)
|
||||
err.ns = string(append(append(v.ns, relativeNamespace...), err.ns...))
|
||||
err.structNs = string(append(append(v.actualNs, relativeStructNamespace...), err.structNs...))
|
||||
|
||||
v.errs = append(v.errs, err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package validator
|
||||
|
||||
import ut "github.com/go-playground/universal-translator"
|
||||
|
||||
// TranslationFunc is the function type used to register or override
|
||||
// custom translations
|
||||
type TranslationFunc func(ut ut.Translator, fe FieldError) string
|
||||
|
||||
// RegisterTranslationsFunc allows for registering of translations
|
||||
// for a 'ut.Translator' for use withing the 'TranslationFunc'
|
||||
type RegisterTranslationsFunc func(ut ut.Translator) error
|
1306
vendor/gopkg.in/go-playground/validator.v9/translations/en/en.go
generated
vendored
Normal file
1306
vendor/gopkg.in/go-playground/validator.v9/translations/en/en.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,257 @@
|
|||
package validator
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// extractTypeInternal gets the actual underlying type of field value.
|
||||
// It will dive into pointers, customTypes and return you the
|
||||
// underlying value and it's kind.
|
||||
func (v *validate) extractTypeInternal(current reflect.Value, nullable bool) (reflect.Value, reflect.Kind, bool) {
|
||||
|
||||
BEGIN:
|
||||
switch current.Kind() {
|
||||
case reflect.Ptr:
|
||||
|
||||
nullable = true
|
||||
|
||||
if current.IsNil() {
|
||||
return current, reflect.Ptr, nullable
|
||||
}
|
||||
|
||||
current = current.Elem()
|
||||
goto BEGIN
|
||||
|
||||
case reflect.Interface:
|
||||
|
||||
nullable = true
|
||||
|
||||
if current.IsNil() {
|
||||
return current, reflect.Interface, nullable
|
||||
}
|
||||
|
||||
current = current.Elem()
|
||||
goto BEGIN
|
||||
|
||||
case reflect.Invalid:
|
||||
return current, reflect.Invalid, nullable
|
||||
|
||||
default:
|
||||
|
||||
if v.v.hasCustomFuncs {
|
||||
|
||||
if fn, ok := v.v.customFuncs[current.Type()]; ok {
|
||||
current = reflect.ValueOf(fn(current))
|
||||
goto BEGIN
|
||||
}
|
||||
}
|
||||
|
||||
return current, current.Kind(), nullable
|
||||
}
|
||||
}
|
||||
|
||||
// getStructFieldOKInternal traverses a struct to retrieve a specific field denoted by the provided namespace and
|
||||
// returns the field, field kind and whether is was successful in retrieving the field at all.
|
||||
//
|
||||
// NOTE: when not successful ok will be false, this can happen when a nested struct is nil and so the field
|
||||
// could not be retrieved because it didn't exist.
|
||||
func (v *validate) getStructFieldOKInternal(val reflect.Value, namespace string) (current reflect.Value, kind reflect.Kind, found bool) {
|
||||
|
||||
BEGIN:
|
||||
current, kind, _ = v.ExtractType(val)
|
||||
|
||||
if kind == reflect.Invalid {
|
||||
return
|
||||
}
|
||||
|
||||
if namespace == "" {
|
||||
found = true
|
||||
return
|
||||
}
|
||||
|
||||
switch kind {
|
||||
|
||||
case reflect.Ptr, reflect.Interface:
|
||||
return
|
||||
|
||||
case reflect.Struct:
|
||||
|
||||
typ := current.Type()
|
||||
fld := namespace
|
||||
ns := namespace
|
||||
|
||||
if typ != timeType {
|
||||
|
||||
idx := strings.Index(namespace, namespaceSeparator)
|
||||
|
||||
if idx != -1 {
|
||||
fld = namespace[:idx]
|
||||
ns = namespace[idx+1:]
|
||||
} else {
|
||||
ns = ""
|
||||
}
|
||||
|
||||
bracketIdx := strings.Index(fld, leftBracket)
|
||||
if bracketIdx != -1 {
|
||||
fld = fld[:bracketIdx]
|
||||
|
||||
ns = namespace[bracketIdx:]
|
||||
}
|
||||
|
||||
val = current.FieldByName(fld)
|
||||
namespace = ns
|
||||
goto BEGIN
|
||||
}
|
||||
|
||||
case reflect.Array, reflect.Slice:
|
||||
idx := strings.Index(namespace, leftBracket)
|
||||
idx2 := strings.Index(namespace, rightBracket)
|
||||
|
||||
arrIdx, _ := strconv.Atoi(namespace[idx+1 : idx2])
|
||||
|
||||
if arrIdx >= current.Len() {
|
||||
return current, kind, false
|
||||
}
|
||||
|
||||
startIdx := idx2 + 1
|
||||
|
||||
if startIdx < len(namespace) {
|
||||
if namespace[startIdx:startIdx+1] == namespaceSeparator {
|
||||
startIdx++
|
||||
}
|
||||
}
|
||||
|
||||
val = current.Index(arrIdx)
|
||||
namespace = namespace[startIdx:]
|
||||
goto BEGIN
|
||||
|
||||
case reflect.Map:
|
||||
idx := strings.Index(namespace, leftBracket) + 1
|
||||
idx2 := strings.Index(namespace, rightBracket)
|
||||
|
||||
endIdx := idx2
|
||||
|
||||
if endIdx+1 < len(namespace) {
|
||||
if namespace[endIdx+1:endIdx+2] == namespaceSeparator {
|
||||
endIdx++
|
||||
}
|
||||
}
|
||||
|
||||
key := namespace[idx:idx2]
|
||||
|
||||
switch current.Type().Key().Kind() {
|
||||
case reflect.Int:
|
||||
i, _ := strconv.Atoi(key)
|
||||
val = current.MapIndex(reflect.ValueOf(i))
|
||||
namespace = namespace[endIdx+1:]
|
||||
|
||||
case reflect.Int8:
|
||||
i, _ := strconv.ParseInt(key, 10, 8)
|
||||
val = current.MapIndex(reflect.ValueOf(int8(i)))
|
||||
namespace = namespace[endIdx+1:]
|
||||
|
||||
case reflect.Int16:
|
||||
i, _ := strconv.ParseInt(key, 10, 16)
|
||||
val = current.MapIndex(reflect.ValueOf(int16(i)))
|
||||
namespace = namespace[endIdx+1:]
|
||||
|
||||
case reflect.Int32:
|
||||
i, _ := strconv.ParseInt(key, 10, 32)
|
||||
val = current.MapIndex(reflect.ValueOf(int32(i)))
|
||||
namespace = namespace[endIdx+1:]
|
||||
|
||||
case reflect.Int64:
|
||||
i, _ := strconv.ParseInt(key, 10, 64)
|
||||
val = current.MapIndex(reflect.ValueOf(i))
|
||||
namespace = namespace[endIdx+1:]
|
||||
|
||||
case reflect.Uint:
|
||||
i, _ := strconv.ParseUint(key, 10, 0)
|
||||
val = current.MapIndex(reflect.ValueOf(uint(i)))
|
||||
namespace = namespace[endIdx+1:]
|
||||
|
||||
case reflect.Uint8:
|
||||
i, _ := strconv.ParseUint(key, 10, 8)
|
||||
val = current.MapIndex(reflect.ValueOf(uint8(i)))
|
||||
namespace = namespace[endIdx+1:]
|
||||
|
||||
case reflect.Uint16:
|
||||
i, _ := strconv.ParseUint(key, 10, 16)
|
||||
val = current.MapIndex(reflect.ValueOf(uint16(i)))
|
||||
namespace = namespace[endIdx+1:]
|
||||
|
||||
case reflect.Uint32:
|
||||
i, _ := strconv.ParseUint(key, 10, 32)
|
||||
val = current.MapIndex(reflect.ValueOf(uint32(i)))
|
||||
namespace = namespace[endIdx+1:]
|
||||
|
||||
case reflect.Uint64:
|
||||
i, _ := strconv.ParseUint(key, 10, 64)
|
||||
val = current.MapIndex(reflect.ValueOf(i))
|
||||
namespace = namespace[endIdx+1:]
|
||||
|
||||
case reflect.Float32:
|
||||
f, _ := strconv.ParseFloat(key, 32)
|
||||
val = current.MapIndex(reflect.ValueOf(float32(f)))
|
||||
namespace = namespace[endIdx+1:]
|
||||
|
||||
case reflect.Float64:
|
||||
f, _ := strconv.ParseFloat(key, 64)
|
||||
val = current.MapIndex(reflect.ValueOf(f))
|
||||
namespace = namespace[endIdx+1:]
|
||||
|
||||
case reflect.Bool:
|
||||
b, _ := strconv.ParseBool(key)
|
||||
val = current.MapIndex(reflect.ValueOf(b))
|
||||
namespace = namespace[endIdx+1:]
|
||||
|
||||
// reflect.Type = string
|
||||
default:
|
||||
val = current.MapIndex(reflect.ValueOf(key))
|
||||
namespace = namespace[endIdx+1:]
|
||||
}
|
||||
|
||||
goto BEGIN
|
||||
}
|
||||
|
||||
// if got here there was more namespace, cannot go any deeper
|
||||
panic("Invalid field namespace")
|
||||
}
|
||||
|
||||
// asInt returns the parameter as a int64
|
||||
// or panics if it can't convert
|
||||
func asInt(param string) int64 {
|
||||
|
||||
i, err := strconv.ParseInt(param, 0, 64)
|
||||
panicIf(err)
|
||||
|
||||
return i
|
||||
}
|
||||
|
||||
// asUint returns the parameter as a uint64
|
||||
// or panics if it can't convert
|
||||
func asUint(param string) uint64 {
|
||||
|
||||
i, err := strconv.ParseUint(param, 0, 64)
|
||||
panicIf(err)
|
||||
|
||||
return i
|
||||
}
|
||||
|
||||
// asFloat returns the parameter as a float64
|
||||
// or panics if it can't convert
|
||||
func asFloat(param string) float64 {
|
||||
|
||||
i, err := strconv.ParseFloat(param, 64)
|
||||
panicIf(err)
|
||||
|
||||
return i
|
||||
}
|
||||
|
||||
func panicIf(err error) {
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,445 @@
|
|||
package validator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// per validate contruct
|
||||
type validate struct {
|
||||
v *Validate
|
||||
top reflect.Value
|
||||
ns []byte
|
||||
actualNs []byte
|
||||
errs ValidationErrors
|
||||
isPartial bool
|
||||
hasExcludes bool
|
||||
includeExclude map[string]struct{} // reset only if StructPartial or StructExcept are called, no need otherwise
|
||||
|
||||
ffn FilterFunc
|
||||
|
||||
// StructLevel & FieldLevel fields
|
||||
slflParent reflect.Value
|
||||
slCurrent reflect.Value
|
||||
flField reflect.Value
|
||||
fldIsPointer bool
|
||||
cf *cField
|
||||
ct *cTag
|
||||
|
||||
// misc reusable values
|
||||
misc []byte
|
||||
str1 string
|
||||
str2 string
|
||||
}
|
||||
|
||||
// parent and current will be the same the first run of validateStruct
|
||||
func (v *validate) validateStruct(ctx context.Context, parent reflect.Value, current reflect.Value, typ reflect.Type, ns []byte, structNs []byte, ct *cTag) {
|
||||
|
||||
cs, ok := v.v.structCache.Get(typ)
|
||||
if !ok {
|
||||
cs = v.v.extractStructCache(current, typ.Name())
|
||||
}
|
||||
|
||||
if len(ns) == 0 && len(cs.name) != 0 {
|
||||
|
||||
ns = append(ns, cs.name...)
|
||||
ns = append(ns, '.')
|
||||
|
||||
structNs = append(structNs, cs.name...)
|
||||
structNs = append(structNs, '.')
|
||||
}
|
||||
|
||||
// ct is nil on top level struct, and structs as fields that have no tag info
|
||||
// so if nil or if not nil and the structonly tag isn't present
|
||||
if ct == nil || ct.typeof != typeStructOnly {
|
||||
|
||||
var f *cField
|
||||
|
||||
for i := 0; i < len(cs.fields); i++ {
|
||||
|
||||
f = cs.fields[i]
|
||||
|
||||
if v.isPartial {
|
||||
|
||||
if v.ffn != nil {
|
||||
// used with StructFiltered
|
||||
if v.ffn(append(structNs, f.name...)) {
|
||||
continue
|
||||
}
|
||||
|
||||
} else {
|
||||
// used with StructPartial & StructExcept
|
||||
_, ok = v.includeExclude[string(append(structNs, f.name...))]
|
||||
|
||||
if (ok && v.hasExcludes) || (!ok && !v.hasExcludes) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
v.traverseField(ctx, parent, current.Field(f.idx), ns, structNs, f, f.cTags)
|
||||
}
|
||||
}
|
||||
|
||||
// check if any struct level validations, after all field validations already checked.
|
||||
// first iteration will have no info about nostructlevel tag, and is checked prior to
|
||||
// calling the next iteration of validateStruct called from traverseField.
|
||||
if cs.fn != nil {
|
||||
|
||||
v.slflParent = parent
|
||||
v.slCurrent = current
|
||||
v.ns = ns
|
||||
v.actualNs = structNs
|
||||
|
||||
cs.fn(ctx, v)
|
||||
}
|
||||
}
|
||||
|
||||
// traverseField validates any field, be it a struct or single field, ensures it's validity and passes it along to be validated via it's tag options
|
||||
func (v *validate) traverseField(ctx context.Context, parent reflect.Value, current reflect.Value, ns []byte, structNs []byte, cf *cField, ct *cTag) {
|
||||
|
||||
var typ reflect.Type
|
||||
var kind reflect.Kind
|
||||
|
||||
current, kind, v.fldIsPointer = v.extractTypeInternal(current, false)
|
||||
|
||||
switch kind {
|
||||
case reflect.Ptr, reflect.Interface, reflect.Invalid:
|
||||
|
||||
if ct == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if ct.typeof == typeOmitEmpty {
|
||||
return
|
||||
}
|
||||
|
||||
if ct.hasTag {
|
||||
|
||||
v.str1 = string(append(ns, cf.altName...))
|
||||
|
||||
if v.v.hasTagNameFunc {
|
||||
v.str2 = string(append(structNs, cf.name...))
|
||||
} else {
|
||||
v.str2 = v.str1
|
||||
}
|
||||
|
||||
if kind == reflect.Invalid {
|
||||
|
||||
v.errs = append(v.errs,
|
||||
&fieldError{
|
||||
v: v.v,
|
||||
tag: ct.aliasTag,
|
||||
actualTag: ct.tag,
|
||||
ns: v.str1,
|
||||
structNs: v.str2,
|
||||
fieldLen: uint8(len(cf.altName)),
|
||||
structfieldLen: uint8(len(cf.name)),
|
||||
param: ct.param,
|
||||
kind: kind,
|
||||
},
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
v.errs = append(v.errs,
|
||||
&fieldError{
|
||||
v: v.v,
|
||||
tag: ct.aliasTag,
|
||||
actualTag: ct.tag,
|
||||
ns: v.str1,
|
||||
structNs: v.str2,
|
||||
fieldLen: uint8(len(cf.altName)),
|
||||
structfieldLen: uint8(len(cf.name)),
|
||||
value: current.Interface(),
|
||||
param: ct.param,
|
||||
kind: kind,
|
||||
typ: current.Type(),
|
||||
},
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
case reflect.Struct:
|
||||
|
||||
typ = current.Type()
|
||||
|
||||
if typ != timeType {
|
||||
|
||||
if ct != nil {
|
||||
|
||||
if ct.typeof == typeStructOnly {
|
||||
goto CONTINUE
|
||||
}
|
||||
|
||||
ct = ct.next
|
||||
}
|
||||
|
||||
if ct != nil && ct.typeof == typeNoStructLevel {
|
||||
return
|
||||
}
|
||||
|
||||
CONTINUE:
|
||||
// if len == 0 then validating using 'Var' or 'VarWithValue'
|
||||
// Var - doesn't make much sense to do it that way, should call 'Struct', but no harm...
|
||||
// VarWithField - this allows for validating against each field withing the struct against a specific value
|
||||
// pretty handly in certain situations
|
||||
if len(cf.name) > 0 {
|
||||
ns = append(append(ns, cf.altName...), '.')
|
||||
structNs = append(append(structNs, cf.name...), '.')
|
||||
}
|
||||
|
||||
v.validateStruct(ctx, current, current, typ, ns, structNs, ct)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !ct.hasTag {
|
||||
return
|
||||
}
|
||||
|
||||
typ = current.Type()
|
||||
|
||||
OUTER:
|
||||
for {
|
||||
if ct == nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch ct.typeof {
|
||||
|
||||
case typeOmitEmpty:
|
||||
|
||||
// set Field Level fields
|
||||
v.slflParent = parent
|
||||
v.flField = current
|
||||
v.cf = cf
|
||||
v.ct = ct
|
||||
|
||||
if !v.fldIsPointer && !hasValue(v) {
|
||||
return
|
||||
}
|
||||
|
||||
ct = ct.next
|
||||
continue
|
||||
|
||||
case typeDive:
|
||||
|
||||
ct = ct.next
|
||||
|
||||
// traverse slice or map here
|
||||
// or panic ;)
|
||||
switch kind {
|
||||
case reflect.Slice, reflect.Array:
|
||||
|
||||
var i64 int64
|
||||
reusableCF := &cField{}
|
||||
|
||||
for i := 0; i < current.Len(); i++ {
|
||||
|
||||
i64 = int64(i)
|
||||
|
||||
v.misc = append(v.misc[0:0], cf.name...)
|
||||
v.misc = append(v.misc, '[')
|
||||
v.misc = strconv.AppendInt(v.misc, i64, 10)
|
||||
v.misc = append(v.misc, ']')
|
||||
|
||||
reusableCF.name = string(v.misc)
|
||||
|
||||
if cf.namesEqual {
|
||||
reusableCF.altName = reusableCF.name
|
||||
} else {
|
||||
|
||||
v.misc = append(v.misc[0:0], cf.altName...)
|
||||
v.misc = append(v.misc, '[')
|
||||
v.misc = strconv.AppendInt(v.misc, i64, 10)
|
||||
v.misc = append(v.misc, ']')
|
||||
|
||||
reusableCF.altName = string(v.misc)
|
||||
}
|
||||
|
||||
v.traverseField(ctx, parent, current.Index(i), ns, structNs, reusableCF, ct)
|
||||
}
|
||||
|
||||
case reflect.Map:
|
||||
|
||||
var pv string
|
||||
reusableCF := &cField{}
|
||||
|
||||
for _, key := range current.MapKeys() {
|
||||
|
||||
pv = fmt.Sprintf("%v", key.Interface())
|
||||
|
||||
v.misc = append(v.misc[0:0], cf.name...)
|
||||
v.misc = append(v.misc, '[')
|
||||
v.misc = append(v.misc, pv...)
|
||||
v.misc = append(v.misc, ']')
|
||||
|
||||
reusableCF.name = string(v.misc)
|
||||
|
||||
if cf.namesEqual {
|
||||
reusableCF.altName = reusableCF.name
|
||||
} else {
|
||||
v.misc = append(v.misc[0:0], cf.altName...)
|
||||
v.misc = append(v.misc, '[')
|
||||
v.misc = append(v.misc, pv...)
|
||||
v.misc = append(v.misc, ']')
|
||||
|
||||
reusableCF.altName = string(v.misc)
|
||||
}
|
||||
|
||||
v.traverseField(ctx, parent, current.MapIndex(key), ns, structNs, reusableCF, ct)
|
||||
}
|
||||
|
||||
default:
|
||||
// throw error, if not a slice or map then should not have gotten here
|
||||
// bad dive tag
|
||||
panic("dive error! can't dive on a non slice or map")
|
||||
}
|
||||
|
||||
return
|
||||
|
||||
case typeOr:
|
||||
|
||||
v.misc = v.misc[0:0]
|
||||
|
||||
for {
|
||||
|
||||
// set Field Level fields
|
||||
v.slflParent = parent
|
||||
v.flField = current
|
||||
v.cf = cf
|
||||
v.ct = ct
|
||||
|
||||
if ct.fn(ctx, v) {
|
||||
|
||||
// drain rest of the 'or' values, then continue or leave
|
||||
for {
|
||||
|
||||
ct = ct.next
|
||||
|
||||
if ct == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if ct.typeof != typeOr {
|
||||
continue OUTER
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
v.misc = append(v.misc, '|')
|
||||
v.misc = append(v.misc, ct.tag...)
|
||||
|
||||
if len(ct.param) > 0 {
|
||||
v.misc = append(v.misc, '=')
|
||||
v.misc = append(v.misc, ct.param...)
|
||||
}
|
||||
|
||||
if ct.next == nil || ct.next.typeof != typeOr { // ct.typeof != typeOr
|
||||
// if we get here, no valid 'or' value and no more tags
|
||||
|
||||
v.str1 = string(append(ns, cf.altName...))
|
||||
|
||||
if v.v.hasTagNameFunc {
|
||||
v.str2 = string(append(structNs, cf.name...))
|
||||
} else {
|
||||
v.str2 = v.str1
|
||||
}
|
||||
|
||||
if ct.hasAlias {
|
||||
|
||||
v.errs = append(v.errs,
|
||||
&fieldError{
|
||||
v: v.v,
|
||||
tag: ct.aliasTag,
|
||||
actualTag: ct.actualAliasTag,
|
||||
ns: v.str1,
|
||||
structNs: v.str2,
|
||||
fieldLen: uint8(len(cf.altName)),
|
||||
structfieldLen: uint8(len(cf.name)),
|
||||
value: current.Interface(),
|
||||
param: ct.param,
|
||||
kind: kind,
|
||||
typ: typ,
|
||||
},
|
||||
)
|
||||
|
||||
} else {
|
||||
|
||||
tVal := string(v.misc)[1:]
|
||||
|
||||
v.errs = append(v.errs,
|
||||
&fieldError{
|
||||
v: v.v,
|
||||
tag: tVal,
|
||||
actualTag: tVal,
|
||||
ns: v.str1,
|
||||
structNs: v.str2,
|
||||
fieldLen: uint8(len(cf.altName)),
|
||||
structfieldLen: uint8(len(cf.name)),
|
||||
value: current.Interface(),
|
||||
param: ct.param,
|
||||
kind: kind,
|
||||
typ: typ,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ct = ct.next
|
||||
}
|
||||
|
||||
default:
|
||||
|
||||
// set Field Level fields
|
||||
v.slflParent = parent
|
||||
v.flField = current
|
||||
v.cf = cf
|
||||
v.ct = ct
|
||||
|
||||
// // report error interface functions need these
|
||||
// v.ns = ns
|
||||
// v.actualNs = structNs
|
||||
|
||||
if !ct.fn(ctx, v) {
|
||||
|
||||
v.str1 = string(append(ns, cf.altName...))
|
||||
|
||||
if v.v.hasTagNameFunc {
|
||||
v.str2 = string(append(structNs, cf.name...))
|
||||
} else {
|
||||
v.str2 = v.str1
|
||||
}
|
||||
|
||||
v.errs = append(v.errs,
|
||||
&fieldError{
|
||||
v: v.v,
|
||||
tag: ct.aliasTag,
|
||||
actualTag: ct.tag,
|
||||
ns: v.str1,
|
||||
structNs: v.str2,
|
||||
fieldLen: uint8(len(cf.altName)),
|
||||
structfieldLen: uint8(len(cf.name)),
|
||||
value: current.Interface(),
|
||||
param: ct.param,
|
||||
kind: kind,
|
||||
typ: typ,
|
||||
},
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
ct = ct.next
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,626 @@
|
|||
package validator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
ut "github.com/go-playground/universal-translator"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultTagName = "validate"
|
||||
utf8HexComma = "0x2C"
|
||||
utf8Pipe = "0x7C"
|
||||
tagSeparator = ","
|
||||
orSeparator = "|"
|
||||
tagKeySeparator = "="
|
||||
structOnlyTag = "structonly"
|
||||
noStructLevelTag = "nostructlevel"
|
||||
omitempty = "omitempty"
|
||||
skipValidationTag = "-"
|
||||
diveTag = "dive"
|
||||
requiredTag = "required"
|
||||
namespaceSeparator = "."
|
||||
leftBracket = "["
|
||||
rightBracket = "]"
|
||||
restrictedTagChars = ".[],|=+()`~!@#$%^&*\\\"/?<>{}"
|
||||
restrictedAliasErr = "Alias '%s' either contains restricted characters or is the same as a restricted tag needed for normal operation"
|
||||
restrictedTagErr = "Tag '%s' either contains restricted characters or is the same as a restricted tag needed for normal operation"
|
||||
)
|
||||
|
||||
var (
|
||||
timeType = reflect.TypeOf(time.Time{})
|
||||
defaultCField = &cField{namesEqual: true}
|
||||
)
|
||||
|
||||
// FilterFunc is the type used to filter fields using
|
||||
// StructFiltered(...) function.
|
||||
// returning true results in the field being filtered/skiped from
|
||||
// validation
|
||||
type FilterFunc func(ns []byte) bool
|
||||
|
||||
// CustomTypeFunc allows for overriding or adding custom field type handler functions
|
||||
// field = field value of the type to return a value to be validated
|
||||
// example Valuer from sql drive see https://golang.org/src/database/sql/driver/types.go?s=1210:1293#L29
|
||||
type CustomTypeFunc func(field reflect.Value) interface{}
|
||||
|
||||
// TagNameFunc allows for adding of a custom tag name parser
|
||||
type TagNameFunc func(field reflect.StructField) string
|
||||
|
||||
// Validate contains the validator settings and cache
|
||||
type Validate struct {
|
||||
tagName string
|
||||
pool *sync.Pool
|
||||
hasCustomFuncs bool
|
||||
hasTagNameFunc bool
|
||||
tagNameFunc TagNameFunc
|
||||
structLevelFuncs map[reflect.Type]StructLevelFuncCtx
|
||||
customFuncs map[reflect.Type]CustomTypeFunc
|
||||
aliases map[string]string
|
||||
validations map[string]FuncCtx
|
||||
transTagFunc map[ut.Translator]map[string]TranslationFunc // map[<locale>]map[<tag>]TranslationFunc
|
||||
tagCache *tagCache
|
||||
structCache *structCache
|
||||
}
|
||||
|
||||
// New returns a new instacne of 'validate' with sane defaults.
|
||||
func New() *Validate {
|
||||
|
||||
tc := new(tagCache)
|
||||
tc.m.Store(make(map[string]*cTag))
|
||||
|
||||
sc := new(structCache)
|
||||
sc.m.Store(make(map[reflect.Type]*cStruct))
|
||||
|
||||
v := &Validate{
|
||||
tagName: defaultTagName,
|
||||
aliases: make(map[string]string, len(bakedInAliases)),
|
||||
validations: make(map[string]FuncCtx, len(bakedInValidators)),
|
||||
tagCache: tc,
|
||||
structCache: sc,
|
||||
}
|
||||
|
||||
// must copy alias validators for separate validations to be used in each validator instance
|
||||
for k, val := range bakedInAliases {
|
||||
v.RegisterAlias(k, val)
|
||||
}
|
||||
|
||||
// must copy validators for separate validations to be used in each instance
|
||||
for k, val := range bakedInValidators {
|
||||
|
||||
// no need to error check here, baked in will always be valid
|
||||
v.registerValidation(k, wrapFunc(val), true)
|
||||
}
|
||||
|
||||
v.pool = &sync.Pool{
|
||||
New: func() interface{} {
|
||||
return &validate{
|
||||
v: v,
|
||||
ns: make([]byte, 0, 64),
|
||||
actualNs: make([]byte, 0, 64),
|
||||
misc: make([]byte, 32),
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
// SetTagName allows for changing of the default tag name of 'validate'
|
||||
func (v *Validate) SetTagName(name string) {
|
||||
v.tagName = name
|
||||
}
|
||||
|
||||
// RegisterTagNameFunc registers a function to get another name from the
|
||||
// StructField eg. the JSON name
|
||||
func (v *Validate) RegisterTagNameFunc(fn TagNameFunc) {
|
||||
v.tagNameFunc = fn
|
||||
v.hasTagNameFunc = true
|
||||
}
|
||||
|
||||
// RegisterValidation adds a validation with the given tag
|
||||
//
|
||||
// NOTES:
|
||||
// - if the key already exists, the previous validation function will be replaced.
|
||||
// - this method is not thread-safe it is intended that these all be registered prior to any validation
|
||||
func (v *Validate) RegisterValidation(tag string, fn Func) error {
|
||||
return v.RegisterValidationCtx(tag, wrapFunc(fn))
|
||||
}
|
||||
|
||||
// RegisterValidationCtx does the same as RegisterValidation on accepts a FuncCtx validation
|
||||
// allowing context.Context validation support.
|
||||
func (v *Validate) RegisterValidationCtx(tag string, fn FuncCtx) error {
|
||||
return v.registerValidation(tag, fn, false)
|
||||
}
|
||||
|
||||
func (v *Validate) registerValidation(tag string, fn FuncCtx, bakedIn bool) error {
|
||||
|
||||
if len(tag) == 0 {
|
||||
return errors.New("Function Key cannot be empty")
|
||||
}
|
||||
|
||||
if fn == nil {
|
||||
return errors.New("Function cannot be empty")
|
||||
}
|
||||
|
||||
_, ok := restrictedTags[tag]
|
||||
|
||||
if !bakedIn && (ok || strings.ContainsAny(tag, restrictedTagChars)) {
|
||||
panic(fmt.Sprintf(restrictedTagErr, tag))
|
||||
}
|
||||
|
||||
v.validations[tag] = fn
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegisterAlias registers a mapping of a single validation tag that
|
||||
// defines a common or complex set of validation(s) to simplify adding validation
|
||||
// to structs.
|
||||
//
|
||||
// NOTE: this function is not thread-safe it is intended that these all be registered prior to any validation
|
||||
func (v *Validate) RegisterAlias(alias, tags string) {
|
||||
|
||||
_, ok := restrictedTags[alias]
|
||||
|
||||
if ok || strings.ContainsAny(alias, restrictedTagChars) {
|
||||
panic(fmt.Sprintf(restrictedAliasErr, alias))
|
||||
}
|
||||
|
||||
v.aliases[alias] = tags
|
||||
}
|
||||
|
||||
// RegisterStructValidation registers a StructLevelFunc against a number of types.
|
||||
// This is akin to implementing the 'Validatable' interface, but for structs for which
|
||||
// you may not have access or rights to change.
|
||||
//
|
||||
// NOTES:
|
||||
// - if this and the 'Validatable' interface are implemented the Struct Level takes precedence as to enable
|
||||
// a struct out of your control's validation to be overridden
|
||||
// - this method is not thread-safe it is intended that these all be registered prior to any validation
|
||||
func (v *Validate) RegisterStructValidation(fn StructLevelFunc, types ...interface{}) {
|
||||
v.RegisterStructValidationCtx(wrapStructLevelFunc(fn), types...)
|
||||
}
|
||||
|
||||
// RegisterStructValidationCtx registers a StructLevelFuncCtx against a number of types and allows passing
|
||||
// of contextual validation information via context.Context.
|
||||
// This is akin to implementing a 'Validatable' interface, but for structs for which
|
||||
// you may not have access or rights to change.
|
||||
//
|
||||
// NOTES:
|
||||
// - if this and the 'Validatable' interface are implemented the Struct Level takes precedence as to enable
|
||||
// a struct out of your control's validation to be overridden
|
||||
// - this method is not thread-safe it is intended that these all be registered prior to any validation
|
||||
func (v *Validate) RegisterStructValidationCtx(fn StructLevelFuncCtx, types ...interface{}) {
|
||||
|
||||
if v.structLevelFuncs == nil {
|
||||
v.structLevelFuncs = make(map[reflect.Type]StructLevelFuncCtx)
|
||||
}
|
||||
|
||||
for _, t := range types {
|
||||
v.structLevelFuncs[reflect.TypeOf(t)] = fn
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterCustomTypeFunc registers a CustomTypeFunc against a number of types
|
||||
//
|
||||
// NOTE: this method is not thread-safe it is intended that these all be registered prior to any validation
|
||||
func (v *Validate) RegisterCustomTypeFunc(fn CustomTypeFunc, types ...interface{}) {
|
||||
|
||||
if v.customFuncs == nil {
|
||||
v.customFuncs = make(map[reflect.Type]CustomTypeFunc)
|
||||
}
|
||||
|
||||
for _, t := range types {
|
||||
v.customFuncs[reflect.TypeOf(t)] = fn
|
||||
}
|
||||
|
||||
v.hasCustomFuncs = true
|
||||
}
|
||||
|
||||
// RegisterTranslation registers translations against the provided tag.
|
||||
func (v *Validate) RegisterTranslation(tag string, trans ut.Translator, registerFn RegisterTranslationsFunc, translationFn TranslationFunc) (err error) {
|
||||
|
||||
if v.transTagFunc == nil {
|
||||
v.transTagFunc = make(map[ut.Translator]map[string]TranslationFunc)
|
||||
}
|
||||
|
||||
if err = registerFn(trans); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
m, ok := v.transTagFunc[trans]
|
||||
if !ok {
|
||||
m = make(map[string]TranslationFunc)
|
||||
v.transTagFunc[trans] = m
|
||||
}
|
||||
|
||||
m[tag] = translationFn
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Struct validates a structs exposed fields, and automatically validates nested structs, unless otherwise specified.
|
||||
//
|
||||
// It returns InvalidValidationError for bad values passed in and nil or ValidationErrors as error otherwise.
|
||||
// You will need to assert the error if it's not nil eg. err.(validator.ValidationErrors) to access the array of errors.
|
||||
func (v *Validate) Struct(s interface{}) error {
|
||||
return v.StructCtx(context.Background(), s)
|
||||
}
|
||||
|
||||
// StructCtx validates a structs exposed fields, and automatically validates nested structs, unless otherwise specified
|
||||
// and also allows passing of context.Context for contextual validation information.
|
||||
//
|
||||
// It returns InvalidValidationError for bad values passed in and nil or ValidationErrors as error otherwise.
|
||||
// You will need to assert the error if it's not nil eg. err.(validator.ValidationErrors) to access the array of errors.
|
||||
func (v *Validate) StructCtx(ctx context.Context, s interface{}) (err error) {
|
||||
|
||||
val := reflect.ValueOf(s)
|
||||
top := val
|
||||
|
||||
if val.Kind() == reflect.Ptr && !val.IsNil() {
|
||||
val = val.Elem()
|
||||
}
|
||||
|
||||
if val.Kind() != reflect.Struct || val.Type() == timeType {
|
||||
return &InvalidValidationError{Type: reflect.TypeOf(s)}
|
||||
}
|
||||
|
||||
// good to validate
|
||||
vd := v.pool.Get().(*validate)
|
||||
vd.top = top
|
||||
vd.isPartial = false
|
||||
// vd.hasExcludes = false // only need to reset in StructPartial and StructExcept
|
||||
|
||||
vd.validateStruct(ctx, top, val, val.Type(), vd.ns[0:0], vd.actualNs[0:0], nil)
|
||||
|
||||
if len(vd.errs) > 0 {
|
||||
err = vd.errs
|
||||
vd.errs = nil
|
||||
}
|
||||
|
||||
v.pool.Put(vd)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// StructFiltered validates a structs exposed fields, that pass the FilterFunc check and automatically validates
|
||||
// nested structs, unless otherwise specified.
|
||||
//
|
||||
// It returns InvalidValidationError for bad values passed in and nil or ValidationErrors as error otherwise.
|
||||
// You will need to assert the error if it's not nil eg. err.(validator.ValidationErrors) to access the array of errors.
|
||||
func (v *Validate) StructFiltered(s interface{}, fn FilterFunc) error {
|
||||
return v.StructFilteredCtx(context.Background(), s, fn)
|
||||
}
|
||||
|
||||
// StructFilteredCtx validates a structs exposed fields, that pass the FilterFunc check and automatically validates
|
||||
// nested structs, unless otherwise specified and also allows passing of contextual validation information via
|
||||
// context.Context
|
||||
//
|
||||
// It returns InvalidValidationError for bad values passed in and nil or ValidationErrors as error otherwise.
|
||||
// You will need to assert the error if it's not nil eg. err.(validator.ValidationErrors) to access the array of errors.
|
||||
func (v *Validate) StructFilteredCtx(ctx context.Context, s interface{}, fn FilterFunc) (err error) {
|
||||
val := reflect.ValueOf(s)
|
||||
top := val
|
||||
|
||||
if val.Kind() == reflect.Ptr && !val.IsNil() {
|
||||
val = val.Elem()
|
||||
}
|
||||
|
||||
if val.Kind() != reflect.Struct || val.Type() == timeType {
|
||||
return &InvalidValidationError{Type: reflect.TypeOf(s)}
|
||||
}
|
||||
|
||||
// good to validate
|
||||
vd := v.pool.Get().(*validate)
|
||||
vd.top = top
|
||||
vd.isPartial = true
|
||||
vd.ffn = fn
|
||||
// vd.hasExcludes = false // only need to reset in StructPartial and StructExcept
|
||||
|
||||
vd.validateStruct(context.Background(), top, val, val.Type(), vd.ns[0:0], vd.actualNs[0:0], nil)
|
||||
|
||||
if len(vd.errs) > 0 {
|
||||
err = vd.errs
|
||||
vd.errs = nil
|
||||
}
|
||||
|
||||
v.pool.Put(vd)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// StructPartial validates the fields passed in only, ignoring all others.
|
||||
// Fields may be provided in a namespaced fashion relative to the struct provided
|
||||
// eg. NestedStruct.Field or NestedArrayField[0].Struct.Name
|
||||
//
|
||||
// It returns InvalidValidationError for bad values passed in and nil or ValidationErrors as error otherwise.
|
||||
// You will need to assert the error if it's not nil eg. err.(validator.ValidationErrors) to access the array of errors.
|
||||
func (v *Validate) StructPartial(s interface{}, fields ...string) error {
|
||||
return v.StructPartialCtx(context.Background(), s, fields...)
|
||||
}
|
||||
|
||||
// StructPartialCtx validates the fields passed in only, ignoring all others and allows passing of contextual
|
||||
// validation validation information via context.Context
|
||||
// Fields may be provided in a namespaced fashion relative to the struct provided
|
||||
// eg. NestedStruct.Field or NestedArrayField[0].Struct.Name
|
||||
//
|
||||
// It returns InvalidValidationError for bad values passed in and nil or ValidationErrors as error otherwise.
|
||||
// You will need to assert the error if it's not nil eg. err.(validator.ValidationErrors) to access the array of errors.
|
||||
func (v *Validate) StructPartialCtx(ctx context.Context, s interface{}, fields ...string) (err error) {
|
||||
val := reflect.ValueOf(s)
|
||||
top := val
|
||||
|
||||
if val.Kind() == reflect.Ptr && !val.IsNil() {
|
||||
val = val.Elem()
|
||||
}
|
||||
|
||||
if val.Kind() != reflect.Struct || val.Type() == timeType {
|
||||
return &InvalidValidationError{Type: reflect.TypeOf(s)}
|
||||
}
|
||||
|
||||
// good to validate
|
||||
vd := v.pool.Get().(*validate)
|
||||
vd.top = top
|
||||
vd.isPartial = true
|
||||
vd.ffn = nil
|
||||
vd.hasExcludes = false
|
||||
vd.includeExclude = make(map[string]struct{})
|
||||
|
||||
typ := val.Type()
|
||||
name := typ.Name()
|
||||
|
||||
if fields != nil {
|
||||
for _, k := range fields {
|
||||
|
||||
flds := strings.Split(k, namespaceSeparator)
|
||||
if len(flds) > 0 {
|
||||
|
||||
vd.misc = append(vd.misc[0:0], name...)
|
||||
vd.misc = append(vd.misc, '.')
|
||||
|
||||
for _, s := range flds {
|
||||
|
||||
idx := strings.Index(s, leftBracket)
|
||||
|
||||
if idx != -1 {
|
||||
for idx != -1 {
|
||||
vd.misc = append(vd.misc, s[:idx]...)
|
||||
vd.includeExclude[string(vd.misc)] = struct{}{}
|
||||
|
||||
idx2 := strings.Index(s, rightBracket)
|
||||
idx2++
|
||||
vd.misc = append(vd.misc, s[idx:idx2]...)
|
||||
vd.includeExclude[string(vd.misc)] = struct{}{}
|
||||
s = s[idx2:]
|
||||
idx = strings.Index(s, leftBracket)
|
||||
}
|
||||
} else {
|
||||
|
||||
vd.misc = append(vd.misc, s...)
|
||||
vd.includeExclude[string(vd.misc)] = struct{}{}
|
||||
}
|
||||
|
||||
vd.misc = append(vd.misc, '.')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vd.validateStruct(ctx, top, val, typ, vd.ns[0:0], vd.actualNs[0:0], nil)
|
||||
|
||||
if len(vd.errs) > 0 {
|
||||
err = vd.errs
|
||||
vd.errs = nil
|
||||
}
|
||||
|
||||
v.pool.Put(vd)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// StructExcept validates all fields except the ones passed in.
|
||||
// Fields may be provided in a namespaced fashion relative to the struct provided
|
||||
// i.e. NestedStruct.Field or NestedArrayField[0].Struct.Name
|
||||
//
|
||||
// It returns InvalidValidationError for bad values passed in and nil or ValidationErrors as error otherwise.
|
||||
// You will need to assert the error if it's not nil eg. err.(validator.ValidationErrors) to access the array of errors.
|
||||
func (v *Validate) StructExcept(s interface{}, fields ...string) error {
|
||||
return v.StructExceptCtx(context.Background(), s, fields...)
|
||||
}
|
||||
|
||||
// StructExceptCtx validates all fields except the ones passed in and allows passing of contextual
|
||||
// validation validation information via context.Context
|
||||
// Fields may be provided in a namespaced fashion relative to the struct provided
|
||||
// i.e. NestedStruct.Field or NestedArrayField[0].Struct.Name
|
||||
//
|
||||
// It returns InvalidValidationError for bad values passed in and nil or ValidationErrors as error otherwise.
|
||||
// You will need to assert the error if it's not nil eg. err.(validator.ValidationErrors) to access the array of errors.
|
||||
func (v *Validate) StructExceptCtx(ctx context.Context, s interface{}, fields ...string) (err error) {
|
||||
val := reflect.ValueOf(s)
|
||||
top := val
|
||||
|
||||
if val.Kind() == reflect.Ptr && !val.IsNil() {
|
||||
val = val.Elem()
|
||||
}
|
||||
|
||||
if val.Kind() != reflect.Struct || val.Type() == timeType {
|
||||
return &InvalidValidationError{Type: reflect.TypeOf(s)}
|
||||
}
|
||||
|
||||
// good to validate
|
||||
vd := v.pool.Get().(*validate)
|
||||
vd.top = top
|
||||
vd.isPartial = true
|
||||
vd.ffn = nil
|
||||
vd.hasExcludes = true
|
||||
vd.includeExclude = make(map[string]struct{})
|
||||
|
||||
typ := val.Type()
|
||||
name := typ.Name()
|
||||
|
||||
for _, key := range fields {
|
||||
|
||||
vd.misc = vd.misc[0:0]
|
||||
|
||||
if len(name) > 0 {
|
||||
vd.misc = append(vd.misc, name...)
|
||||
vd.misc = append(vd.misc, '.')
|
||||
}
|
||||
|
||||
vd.misc = append(vd.misc, key...)
|
||||
vd.includeExclude[string(vd.misc)] = struct{}{}
|
||||
}
|
||||
|
||||
vd.validateStruct(ctx, top, val, typ, vd.ns[0:0], vd.actualNs[0:0], nil)
|
||||
|
||||
if len(vd.errs) > 0 {
|
||||
err = vd.errs
|
||||
vd.errs = nil
|
||||
}
|
||||
|
||||
v.pool.Put(vd)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Var validates a single variable using tag style validation.
|
||||
// eg.
|
||||
// var i int
|
||||
// validate.Var(i, "gt=1,lt=10")
|
||||
//
|
||||
// WARNING: a struct can be passed for validation eg. time.Time is a struct or if you have a custom type and have registered
|
||||
// a custom type handler, so must allow it; however unforseen validations will occur if trying to validate a struct
|
||||
// that is meant to be passed to 'validate.Struct'
|
||||
//
|
||||
// It returns InvalidValidationError for bad values passed in and nil or ValidationErrors as error otherwise.
|
||||
// You will need to assert the error if it's not nil eg. err.(validator.ValidationErrors) to access the array of errors.
|
||||
// validate Array, Slice and maps fields which may contain more than one error
|
||||
func (v *Validate) Var(field interface{}, tag string) error {
|
||||
return v.VarCtx(context.Background(), field, tag)
|
||||
}
|
||||
|
||||
// VarCtx validates a single variable using tag style validation and allows passing of contextual
|
||||
// validation validation information via context.Context.
|
||||
// eg.
|
||||
// var i int
|
||||
// validate.Var(i, "gt=1,lt=10")
|
||||
//
|
||||
// WARNING: a struct can be passed for validation eg. time.Time is a struct or if you have a custom type and have registered
|
||||
// a custom type handler, so must allow it; however unforseen validations will occur if trying to validate a struct
|
||||
// that is meant to be passed to 'validate.Struct'
|
||||
//
|
||||
// It returns InvalidValidationError for bad values passed in and nil or ValidationErrors as error otherwise.
|
||||
// You will need to assert the error if it's not nil eg. err.(validator.ValidationErrors) to access the array of errors.
|
||||
// validate Array, Slice and maps fields which may contain more than one error
|
||||
func (v *Validate) VarCtx(ctx context.Context, field interface{}, tag string) (err error) {
|
||||
if len(tag) == 0 || tag == skipValidationTag {
|
||||
return nil
|
||||
}
|
||||
|
||||
// find cached tag
|
||||
ctag, ok := v.tagCache.Get(tag)
|
||||
if !ok {
|
||||
v.tagCache.lock.Lock()
|
||||
defer v.tagCache.lock.Unlock()
|
||||
|
||||
// could have been multiple trying to access, but once first is done this ensures tag
|
||||
// isn't parsed again.
|
||||
ctag, ok = v.tagCache.Get(tag)
|
||||
if !ok {
|
||||
ctag, _ = v.parseFieldTagsRecursive(tag, "", "", false)
|
||||
v.tagCache.Set(tag, ctag)
|
||||
}
|
||||
}
|
||||
|
||||
val := reflect.ValueOf(field)
|
||||
|
||||
vd := v.pool.Get().(*validate)
|
||||
vd.top = val
|
||||
vd.isPartial = false
|
||||
|
||||
vd.traverseField(ctx, val, val, vd.ns[0:0], vd.actualNs[0:0], defaultCField, ctag)
|
||||
|
||||
if len(vd.errs) > 0 {
|
||||
err = vd.errs
|
||||
vd.errs = nil
|
||||
}
|
||||
|
||||
v.pool.Put(vd)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// VarWithValue validates a single variable, against another variable/field's value using tag style validation
|
||||
// eg.
|
||||
// s1 := "abcd"
|
||||
// s2 := "abcd"
|
||||
// validate.VarWithValue(s1, s2, "eqcsfield") // returns true
|
||||
//
|
||||
// WARNING: a struct can be passed for validation eg. time.Time is a struct or if you have a custom type and have registered
|
||||
// a custom type handler, so must allow it; however unforseen validations will occur if trying to validate a struct
|
||||
// that is meant to be passed to 'validate.Struct'
|
||||
//
|
||||
// It returns InvalidValidationError for bad values passed in and nil or ValidationErrors as error otherwise.
|
||||
// You will need to assert the error if it's not nil eg. err.(validator.ValidationErrors) to access the array of errors.
|
||||
// validate Array, Slice and maps fields which may contain more than one error
|
||||
func (v *Validate) VarWithValue(field interface{}, other interface{}, tag string) error {
|
||||
return v.VarWithValueCtx(context.Background(), field, other, tag)
|
||||
}
|
||||
|
||||
// VarWithValueCtx validates a single variable, against another variable/field's value using tag style validation and
|
||||
// allows passing of contextual validation validation information via context.Context.
|
||||
// eg.
|
||||
// s1 := "abcd"
|
||||
// s2 := "abcd"
|
||||
// validate.VarWithValue(s1, s2, "eqcsfield") // returns true
|
||||
//
|
||||
// WARNING: a struct can be passed for validation eg. time.Time is a struct or if you have a custom type and have registered
|
||||
// a custom type handler, so must allow it; however unforseen validations will occur if trying to validate a struct
|
||||
// that is meant to be passed to 'validate.Struct'
|
||||
//
|
||||
// It returns InvalidValidationError for bad values passed in and nil or ValidationErrors as error otherwise.
|
||||
// You will need to assert the error if it's not nil eg. err.(validator.ValidationErrors) to access the array of errors.
|
||||
// validate Array, Slice and maps fields which may contain more than one error
|
||||
func (v *Validate) VarWithValueCtx(ctx context.Context, field interface{}, other interface{}, tag string) (err error) {
|
||||
if len(tag) == 0 || tag == skipValidationTag {
|
||||
return nil
|
||||
}
|
||||
|
||||
// find cached tag
|
||||
ctag, ok := v.tagCache.Get(tag)
|
||||
if !ok {
|
||||
v.tagCache.lock.Lock()
|
||||
defer v.tagCache.lock.Unlock()
|
||||
|
||||
// could have been multiple trying to access, but once first is done this ensures tag
|
||||
// isn't parsed again.
|
||||
ctag, ok = v.tagCache.Get(tag)
|
||||
if !ok {
|
||||
ctag, _ = v.parseFieldTagsRecursive(tag, "", "", false)
|
||||
v.tagCache.Set(tag, ctag)
|
||||
}
|
||||
}
|
||||
|
||||
otherVal := reflect.ValueOf(other)
|
||||
|
||||
vd := v.pool.Get().(*validate)
|
||||
vd.top = otherVal
|
||||
vd.isPartial = false
|
||||
|
||||
vd.traverseField(ctx, otherVal, reflect.ValueOf(field), vd.ns[0:0], vd.actualNs[0:0], defaultCField, ctag)
|
||||
|
||||
if len(vd.errs) > 0 {
|
||||
err = vd.errs
|
||||
vd.errs = nil
|
||||
}
|
||||
|
||||
v.pool.Put(vd)
|
||||
|
||||
return
|
||||
}
|
Loading…
Reference in New Issue