diff --git a/import/error_checker.go b/import/error_checker.go new file mode 100644 index 0000000..d8f3d59 --- /dev/null +++ b/import/error_checker.go @@ -0,0 +1,26 @@ +package impt + +import ( + "context" + "fmt" +) + +type ErrorChecker struct { + validate func(ctx context.Context, model interface{}) ([]ErrorMessage, error) +} + +func NewErrorChecker(validate func(context.Context, interface{}) ([]ErrorMessage, error)) *ErrorChecker { + return &ErrorChecker{validate: validate} +} + +func (v *ErrorChecker) Check(ctx context.Context, model interface{}) error { + errors, err := v.validate(ctx, model) + if err != nil { + return err + } + if errors != nil && len(errors) > 0 { + m := fmt.Sprintf("%s", errors) + return fmt.Errorf(m) + } + return nil +} diff --git a/import/file_reader.go b/import/file_reader.go index c635055..d290cc1 100644 --- a/import/file_reader.go +++ b/import/file_reader.go @@ -19,7 +19,7 @@ type FileReader struct { func NewFileReader(buildFileName func() string) (*FileReader, error) { return NewDelimiterFileReader(buildFileName, ',') } -func NewDelimiterFileReader(buildFileName func() string, delimiter rune, opts... *encoding.Decoder) (*FileReader, error) { +func NewDelimiterFileReader(buildFileName func() string, delimiter rune, opts ...*encoding.Decoder) (*FileReader, error) { var decoder *encoding.Decoder if len(opts) > 0 && opts[0] != nil { decoder = opts[0] @@ -35,33 +35,35 @@ func NewDelimiterFileReader(buildFileName func() string, delimiter rune, opts... return &fr, nil } -func (fr *FileReader) Read(next func(lines []string, err error) error) error { +func (fr *FileReader) Read(next func(lines []string, err error, numLine int) error) error { file, err := os.Open(fr.FileName) if err != nil { err = errors.New("cannot open file") - next(make([]string, 0), err) + next(make([]string, 0), err, 0) return err } defer file.Close() scanner := bufio.NewScanner(file) + i := 1 for scanner.Scan() { if scanner.Text() != "" { - err := next([]string{scanner.Text()}, nil) + err := next([]string{scanner.Text()}, nil, i) if err != nil { return err } } + i++ } - next([]string{}, io.EOF) + next([]string{}, io.EOF, i) return nil } -func (fr *FileReader) ReadDelimiterFile(next func(lines []string, err error) error) error { +func (fr *FileReader) ReadDelimiterFile(next func(lines []string, err error, numLine int) error) error { file, err := os.Open(fr.FileName) if err != nil { - next(make([]string, 0), err) + next(make([]string, 0), err, 0) } var r *csv.Reader if fr.Decoder != nil { @@ -75,15 +77,17 @@ func (fr *FileReader) ReadDelimiterFile(next func(lines []string, err error) err } defer file.Close() + i := 1 for { record, err := r.Read() - err2 := next(record, err) + err2 := next(record, err, i) if err2 != nil { return err2 } if err == io.EOF { break } + i++ } return err } diff --git a/import/importer.go b/import/importer.go index d60fa62..6bcb85c 100644 --- a/import/importer.go +++ b/import/importer.go @@ -2,66 +2,191 @@ package impt import ( "context" - "database/sql" + "fmt" "io" "reflect" ) -func NewImportRepository(db *sql.DB, modelType reflect.Type, +type ErrorMessage struct { + Field string `yaml:"field" mapstructure:"field" json:"field,omitempty" gorm:"column:field" bson:"field,omitempty" dynamodbav:"field,omitempty" firestore:"field,omitempty"` + Code string `yaml:"code" mapstructure:"code" json:"code,omitempty" gorm:"column:code" bson:"code,omitempty" dynamodbav:"code,omitempty" firestore:"code,omitempty"` + Param string `yaml:"param" mapstructure:"param" json:"param,omitempty" gorm:"column:param" bson:"param,omitempty" dynamodbav:"param,omitempty" firestore:"param,omitempty"` + Message string `yaml:"message" mapstructure:"message" json:"message,omitempty" gorm:"column:message" bson:"message,omitempty" dynamodbav:"message,omitempty" firestore:"message,omitempty"` +} + +type ErrorHandler struct { + HandleError func(ctx context.Context, format string, fields map[string]interface{}) + FileName string + LineNumber string + Map *map[string]interface{} +} + +func NewErrorHandler(logger func(ctx context.Context, format string, fields map[string]interface{}), fileName string, lineNumber string, mp *map[string]interface{}) *ErrorHandler { + if len(fileName) <= 0 { + fileName = "filename" + } + if len(lineNumber) <= 0 { + lineNumber = "lineNumber" + } + return &ErrorHandler{ + HandleError: logger, + FileName: fileName, + LineNumber: lineNumber, + Map: mp, + } +} + +func (e *ErrorHandler) HandlerError(ctx context.Context, rs interface{}, err []ErrorMessage, i int, fileName string) { + var ext = make(map[string]interface{}) + if e.Map != nil { + ext = *e.Map + } + if len(e.FileName) > 0 && len(e.LineNumber) > 0 { + if len(fileName) > 0 { + ext[e.FileName] = fileName + } + if i > 0 { + ext[e.LineNumber] = i + } + e.HandleError(ctx, fmt.Sprintf("Message is invalid: %+v . Error: %+v", rs, err), ext) + } else if len(e.FileName) > 0 { + if len(fileName) > 0 { + ext[e.FileName] = fileName + } + e.HandleError(ctx, fmt.Sprintf("Message is invalid: %+v . Error: %+v line: %d", rs, err, i), ext) + } else if len(e.LineNumber) > 0 { + if i > 0 { + ext[e.LineNumber] = i + } + e.HandleError(ctx, fmt.Sprintf("Message is invalid: %+v . Error: %+v filename:%s", rs, err, fileName), ext) + } else { + e.HandleError(ctx, fmt.Sprintf("Message is invalid: %+v . Error: %+v filename:%s line: %d", rs, err, fileName, i), ext) + } +} + +func (e *ErrorHandler) HandlerException(ctx context.Context, rs interface{}, err error, i int, fileName string) { + var ext = make(map[string]interface{}) + if e.Map != nil { + ext = *e.Map + } + if len(e.FileName) > 0 && len(e.LineNumber) > 0 { + if len(fileName) > 0 { + ext[e.FileName] = fileName + } + if i > 0 { + ext[e.LineNumber] = i + } + e.HandleError(ctx, fmt.Sprintf("Error to write: %+v . Error: %+v", rs, err), ext) + } else if len(e.FileName) > 0 { + if len(fileName) > 0 { + ext[e.FileName] = fileName + } + e.HandleError(ctx, fmt.Sprintf("Error to write: %+v . Error: %+v line: %d", rs, err, i), ext) + } else if len(e.LineNumber) > 0 { + if i > 0 { + ext[e.LineNumber] = i + } + e.HandleError(ctx, fmt.Sprintf("Error to write: %+v . Error: %+v filename:%s", rs, err, fileName), ext) + } else { + e.HandleError(ctx, fmt.Sprintf("Error to write: %+v . Error: %v filename: %s line: %d", rs, err, fileName, i), ext) + } +} + +func NewImportRepository(modelType reflect.Type, transform func(ctx context.Context, lines []string) (interface{}, error), write func(ctx context.Context, data interface{}, endLineFlag bool) error, - read func(next func(lines []string, err error) error) error, + read func(next func(lines []string, err error, numLine int) error) error, + handleException func(ctx context.Context, rs interface{}, err error, i int, fileName string), + validate func(ctx context.Context, model interface{}) ([]ErrorMessage, error), + logError func(ctx context.Context, rs interface{}, err []ErrorMessage, i int, fileName string), + opt ...string, ) *Importer { - return NewImporter(db, modelType, transform, write, read) + return NewImporter(modelType, transform, write, read, handleException, validate, logError, opt...) } -func NewImportAdapter(db *sql.DB, modelType reflect.Type, +func NewImportAdapter(modelType reflect.Type, transform func(ctx context.Context, lines []string) (interface{}, error), write func(ctx context.Context, data interface{}, endLineFlag bool) error, - read func(next func(lines []string, err error) error) error, + read func(next func(lines []string, err error, numLine int) error) error, + handleException func(ctx context.Context, rs interface{}, err error, i int, fileName string), + validate func(ctx context.Context, model interface{}) ([]ErrorMessage, error), + logError func(ctx context.Context, rs interface{}, err []ErrorMessage, i int, fileName string), + opt ...string, ) *Importer { - return NewImporter(db, modelType, transform, write, read) + return NewImporter(modelType, transform, write, read, handleException, validate, logError, opt...) } -func NewImportService(db *sql.DB, modelType reflect.Type, +func NewImportService(modelType reflect.Type, transform func(ctx context.Context, lines []string) (interface{}, error), write func(ctx context.Context, data interface{}, endLineFlag bool) error, - read func(next func(lines []string, err error) error) error, + read func(next func(lines []string, err error, numLine int) error) error, + handleException func(ctx context.Context, rs interface{}, err error, i int, fileName string), + validate func(ctx context.Context, model interface{}) ([]ErrorMessage, error), + logError func(ctx context.Context, rs interface{}, err []ErrorMessage, i int, fileName string), + opt ...string, ) *Importer { - return NewImporter(db, modelType, transform, write, read) + return NewImporter(modelType, transform, write, read, handleException, validate, logError, opt...) } -func NewImporter(db *sql.DB, modelType reflect.Type, +func NewImporter(modelType reflect.Type, transform func(ctx context.Context, lines []string) (interface{}, error), write func(ctx context.Context, data interface{}, endLineFlag bool) error, - read func(next func(lines []string, err error) error) error, + read func(next func(lines []string, err error, numLine int) error) error, + handleException func(ctx context.Context, rs interface{}, err error, i int, fileName string), + validate func(ctx context.Context, model interface{}) ([]ErrorMessage, error), + handleError func(ctx context.Context, rs interface{}, err []ErrorMessage, i int, fileName string), + opt ...string, ) *Importer { - return &Importer{DB: db, modelType: modelType, Transform: transform, Write: write, Read: read} + filename := "" + if len(opt) > 0 { + filename = opt[0] + } + return &Importer{modelType: modelType, Transform: transform, Write: write, Read: read, Validate: validate, HandleError: handleError, HandleException: handleException, Filename: filename} } type Importer struct { - DB *sql.DB - modelType reflect.Type - Transform func(ctx context.Context, lines []string) (interface{}, error) - Read func(next func(lines []string, err error) error) error - Write func(ctx context.Context, data interface{}, endLineFlag bool) error + modelType reflect.Type + Transform func(ctx context.Context, lines []string) (interface{}, error) + Read func(next func(lines []string, err error, numLine int) error) error + Write func(ctx context.Context, data interface{}, endLineFlag bool) error + Validate func(ctx context.Context, model interface{}) ([]ErrorMessage, error) + HandleError func(ctx context.Context, rs interface{}, err []ErrorMessage, i int, fileName string) + HandleException func(ctx context.Context, rs interface{}, err error, i int, fileName string) + Filename string } -func (s *Importer) Import(ctx context.Context) (err error) { - err = s.Read(func(lines []string, err error) error { +func (s *Importer) Import(ctx context.Context) (total int, success int, err error) { + err = s.Read(func(lines []string, err error, numLine int) error { if err == io.EOF { err = s.Write(ctx, nil, true) return nil } + total++ itemStruct, err := s.Transform(ctx, lines) if err != nil { return err } + if s.Validate != nil { + errs, err := s.Validate(ctx, itemStruct) + if err != nil { + return err + } + if len(errs) > 0 { + s.HandleError(ctx, itemStruct, errs, numLine, s.Filename) + return nil + } + } err = s.Write(ctx, itemStruct, false) if err != nil { - return err + if s.HandleException != nil { + s.HandleException(ctx, itemStruct, err, numLine, s.Filename) + return nil + } else { + return err + } } + success++ return nil }) if err != nil && err != io.EOF { - return err + return total, success, err } - return nil + return total, success, nil } diff --git a/validator/country.go b/validator/country.go new file mode 100644 index 0000000..7928439 --- /dev/null +++ b/validator/country.go @@ -0,0 +1,451 @@ +package validator + +func IsCountryCode(v string) bool { + _, ok := CountryCodes[v] + if ok { + return ok + } + _, ok2 := Alpha3CountryCodes[v] + return ok2 +} +func IsAlpha2CountryCode(v string) bool { + _, ok := CountryCodes[v] + return ok +} +func IsAlpha3CountryCode(v string) bool { + _, ok := Alpha3CountryCodes[v] + return ok +} + +var CountryCodes = map[string]string{ + "CA": "1", + "US": "1", + "EG": "20", + "SS": "211", + "MA": "212", + "EH": "212", + "DZ": "213", + "TN": "216", + "LY": "218", + "GM": "220", + "SN": "221", + "MR": "222", + "ML": "223", + "GN": "224", + "CI": "225", + "BF": "226", + "NE": "227", + "TG": "228", + "BJ": "229", + "MU": "230", + "LR": "231", + "SL": "232", + "GH": "233", + "NG": "234", + "TD": "235", + "CF": "236", + "CM": "237", + "CV": "238", + "ST": "239", + "GQ": "240", + "GA": "241", + "CG": "242", + "CD": "243", + "AO": "244", + "GW": "245", + "IO": "246", + "SC": "248", + "SD": "249", + "RW": "250", + "ET": "251", + "SO": "252", + "DJ": "253", + "KE": "254", + "TZ": "255", + "UG": "256", + "BI": "257", + "MZ": "258", + "ZM": "260", + "MG": "261", + "RE": "262", + "YT": "262", + "ZW": "263", + "NA": "264", + "MW": "265", + "LS": "266", + "BW": "267", + "SZ": "268", + "KM": "269", + "ZA": "27", + "SH": "290", + "ER": "291", + "AW": "297", + "FO": "298", + "GL": "299", + "GR": "30", + "NL": "31", + "BE": "32", + "FR": "33", + "ES": "34", + "GI": "350", + "PT": "351", + "LU": "352", + "IE": "353", + "IS": "354", + "AL": "355", + "MT": "356", + "CY": "357", + "FI": "358", + "BG": "359", + "HU": "36", + "LT": "370", + "LV": "371", + "EE": "372", + "MD": "373", + "AM": "374", + "BY": "375", + "AD": "376", + "MC": "377", + "SM": "378", + "VA": "379", + "UA": "380", + "RS": "381", + "ME": "382", + "XK": "383", + "HR": "385", + "SI": "386", + "BA": "387", + "MK": "389", + "IT": "39", + "RO": "40", + "CH": "41", + "CZ": "420", + "SK": "421", + "LI": "423", + "AT": "43", + "GB": "44", + "DK": "45", + "SE": "46", + "NO": "47", + "SJ": "47", + "PL": "48", + "DE": "49", + "FK": "500", + "BZ": "501", + "GT": "502", + "SV": "503", + "HN": "504", + "NI": "505", + "CR": "506", + "PA": "507", + "PM": "508", + "HT": "509", + "PE": "51", + "MX": "52", + "CU": "53", + "AR": "54", + "BR": "55", + "CL": "56", + "CO": "57", + "VE": "58", + "BL": "590", + "MF": "590", + "BO": "591", + "GY": "592", + "EC": "593", + "PY": "595", + "SR": "597", + "UY": "598", + "AN": "599", + "CW": "599", + "MY": "60", + "AU": "61", + "CC": "61", + "CX": "61", + "ID": "62", + "PH": "63", + "NZ": "64", + "PN": "64", + "SG": "65", + "TH": "66", + "TL": "670", + "AQ": "672", + "BN": "673", + "NR": "674", + "PG": "675", + "TO": "676", + "SB": "677", + "VU": "678", + "FJ": "679", + "PW": "680", + "WF": "681", + "CK": "682", + "NU": "683", + "WS": "685", + "KI": "686", + "NC": "687", + "TV": "688", + "PF": "689", + "TK": "690", + "FM": "691", + "MH": "692", + "KZ": "7", + "RU": "7", + "JP": "81", + "KR": "82", + "VN": "84", + "CN": "86", + "KP": "850", + "HK": "852", + "MO": "853", + "KH": "855", + "LA": "856", + "BD": "880", + "TW": "886", + "TR": "90", + "IN": "91", + "PK": "92", + "AF": "93", + "LK": "94", + "MM": "95", + "IR": "98", + "MV": "960", + "LB": "961", + "JO": "962", + "SY": "963", + "IQ": "964", + "KW": "965", + "SA": "966", + "YE": "967", + "OM": "968", + "PS": "970", + "AE": "971", + "IL": "972", + "BH": "973", + "QA": "974", + "BT": "975", + "MN": "976", + "NP": "977", + "TJ": "992", + "TM": "993", + "AZ": "994", + "GE": "995", + "KG": "996", + "UZ": "998", +} +var Alpha3CountryCodes = map[string]string{ + "CAN": "1", + "USA": "1", + "EGY": "20", + "SSD": "211", + "MAR": "212", + "ESH": "212", + "DZA": "213", + "TUN": "216", + "LBY": "218", + "GMB": "220", + "SEN": "221", + "MRT": "222", + "MLI": "223", + "GIN": "224", + "CIV": "225", + "BFA": "226", + "NER": "227", + "TGO": "228", + "BEN": "229", + "MUS": "230", + "LBR": "231", + "SLE": "232", + "GHA": "233", + "NGA": "234", + "TCD": "235", + "CAF": "236", + "CMR": "237", + "CPV": "238", + "STP": "239", + "GNQ": "240", + "GAB": "241", + "COG": "242", + "COD": "243", + "AGO": "244", + "GNB": "245", + "IOT": "246", + "SYC": "248", + "SDN": "249", + "RWA": "250", + "ETH": "251", + "SOM": "252", + "DJI": "253", + "KEN": "254", + "TZA": "255", + "UGA": "256", + "BDI": "257", + "MOZ": "258", + "ZMB": "260", + "MDG": "261", + "REU": "262", + "MYT": "262", + "ZWE": "263", + "NAM": "264", + "MWI": "265", + "LSO": "266", + "BWA": "267", + "SWZ": "268", + "COM": "269", + "ZAF": "27", + "SHN": "290", + "ERI": "291", + "ABW": "297", + "FRO": "298", + "GRL": "299", + "GRC": "30", + "NLD": "31", + "BEL": "32", + "FRA": "33", + "ESP": "34", + "GIB": "350", + "PRT": "351", + "LUX": "352", + "IRL": "353", + "ISL": "354", + "ALB": "355", + "MLT": "356", + "CYP": "357", + "FIN": "358", + "BGR": "359", + "HUN": "36", + "LTU": "370", + "LVA": "371", + "EST": "372", + "MDA": "373", + "ARM": "374", + "BLR": "375", + "AND": "376", + "MCO": "377", + "SMR": "378", + "VAT": "379", + "UKR": "380", + "SRB": "381", + "MNE": "382", + "XKX": "383", + "HRV": "385", + "SVN": "386", + "BIH": "387", + "MKD": "389", + "ITA": "39", + "ROU": "40", + "CHE": "41", + "CZE": "420", + "SVK": "421", + "LIE": "423", + "AUT": "43", + "GBR": "44", + "DNK": "45", + "SWE": "46", + "NOR": "47", + "SJM": "47", + "POL": "48", + "DEU": "49", + "FLK": "500", + "BLZ": "501", + "GTM": "502", + "SLV": "503", + "HND": "504", + "NIC": "505", + "CRI": "506", + "PAN": "507", + "SPM": "508", + "HTI": "509", + "PER": "51", + "MEX": "52", + "CUB": "53", + "ARG": "54", + "BRA": "55", + "CHL": "56", + "COL": "57", + "VEN": "58", + "BLM": "590", + "MAF": "590", + "BOL": "591", + "GUY": "592", + "ECU": "593", + "PRY": "595", + "SUR": "597", + "URY": "598", + "ANT": "599", + "CUW": "599", + "MYS": "60", + "AUS": "61", + "CCK": "61", + "CXR": "61", + "IDN": "62", + "PHL": "63", + "NZL": "64", + "PCN": "64", + "SGP": "65", + "THA": "66", + "TLS": "670", + "ATA": "672", + "BRN": "673", + "NRU": "674", + "PNG": "675", + "TON": "676", + "SLB": "677", + "VUT": "678", + "FJI": "679", + "PLW": "680", + "WLF": "681", + "COK": "682", + "NIU": "683", + "WSM": "685", + "KIR": "686", + "NCL": "687", + "TUV": "688", + "PYF": "689", + "TKL": "690", + "FSM": "691", + "MHL": "692", + "KAZ": "7", + "RUS": "7", + "JPN": "81", + "KOR": "82", + "VNM": "84", + "CHN": "86", + "PRK": "850", + "HKG": "852", + "MAC": "853", + "KHM": "855", + "LAO": "856", + "BGD": "880", + "TWN": "886", + "TUR": "90", + "IND": "91", + "PAK": "92", + "AFG": "93", + "LKA": "94", + "MMR": "95", + "IRN": "98", + "MDV": "960", + "LBN": "961", + "JOR": "962", + "SYR": "963", + "IRQ": "964", + "KWT": "965", + "SAU": "966", + "YEM": "967", + "OMN": "968", + "PSE": "970", + "ARE": "971", + "ISR": "972", + "BHR": "973", + "QAT": "974", + "BTN": "975", + "MNG": "976", + "NPL": "977", + "TJK": "992", + "TKM": "993", + "AZE": "994", + "GEO": "995", + "KGZ": "996", + "UZB": "998", +} diff --git a/validator/custom.go b/validator/custom.go new file mode 100644 index 0000000..79ec1f4 --- /dev/null +++ b/validator/custom.go @@ -0,0 +1,108 @@ +package validator + +import ( + s "github.com/core-go/io/import" + "github.com/go-playground/validator/v10" + "strings" +) + +type CustomValidate struct { + Fn validator.Func + Tag string +} + +var PatternMap = map[string]string{ + "digit": "^\\d+$", + "dash_digit": "^[0-9-]*$", + "code": "^\\w*\\d*$", +} + +func GetCustomValidateList() (list []CustomValidate) { + list = append(list, CustomValidate{Fn: CheckEmail, Tag: "email"}) + list = append(list, CustomValidate{Fn: CheckUrl, Tag: "url"}) + list = append(list, CustomValidate{Fn: CheckUri, Tag: "uri"}) + list = append(list, CustomValidate{Fn: CheckFax, Tag: "fax"}) + list = append(list, CustomValidate{Fn: CheckPhone, Tag: "phone"}) + list = append(list, CustomValidate{Fn: CheckIp, Tag: "ip"}) + list = append(list, CustomValidate{Fn: CheckIpV4, Tag: "ipv4"}) + list = append(list, CustomValidate{Fn: CheckIpV6, Tag: "ipv6"}) + list = append(list, CustomValidate{Fn: CheckDigit, Tag: "digit"}) + list = append(list, CustomValidate{Fn: CheckAbc, Tag: "abc"}) + list = append(list, CustomValidate{Fn: CheckId, Tag: "id"}) + list = append(list, CustomValidate{Fn: CheckCode, Tag: "code"}) + list = append(list, CustomValidate{Fn: CheckCountryCode, Tag: "country_code"}) + list = append(list, CustomValidate{Fn: CheckUsername, Tag: "username"}) + list = append(list, CustomValidate{Fn: CheckPattern, Tag: "regex"}) + return +} +func CheckString(fl validator.FieldLevel, fn func(string) bool) bool { + s := fl.Field().String() + if len(s) == 0 { + return true + } + return fn(s) +} +func CheckEmail(fl validator.FieldLevel) bool { + return CheckString(fl, IsEmail) +} +func CheckUrl(fl validator.FieldLevel) bool { + return CheckString(fl, IsUrl) +} +func CheckUri(fl validator.FieldLevel) bool { + return CheckString(fl, IsUri) +} +func CheckFax(fl validator.FieldLevel) bool { + return CheckString(fl, IsFax) +} +func CheckPhone(fl validator.FieldLevel) bool { + return CheckString(fl, IsPhone) +} +func CheckIp(fl validator.FieldLevel) bool { + return CheckString(fl, IsIpAddress) +} +func CheckIpV4(fl validator.FieldLevel) bool { + return CheckString(fl, IsIpAddressV4) +} +func CheckIpV6(fl validator.FieldLevel) bool { + return CheckString(fl, IsIpAddressV6) +} +func CheckDigit(fl validator.FieldLevel) bool { + return CheckString(fl, IsDigit) +} +func CheckAbc(fl validator.FieldLevel) bool { + return CheckString(fl, IsAbc) +} +func CheckId(fl validator.FieldLevel) bool { + return CheckString(fl, IsCode) +} +func CheckCode(fl validator.FieldLevel) bool { + return CheckString(fl, IsDashCode) +} +func CheckCountryCode(fl validator.FieldLevel) bool { + return CheckString(fl, IsCountryCode) +} +func CheckUsername(fl validator.FieldLevel) bool { + return CheckString(fl, IsUserName) +} +func CheckPattern(fl validator.FieldLevel) bool { + param := fl.Param() + if pattern, ok := PatternMap[param]; ok { + return IsValidPattern(pattern, fl.Field().String()) + } else { + panic("invalid pattern") + } +} +func RemoveRequiredError(errors []s.ErrorMessage) []s.ErrorMessage { + if errors == nil || len(errors) == 0 { + return errors + } + errs := make([]s.ErrorMessage, 0) + for _, s := range errors { + if s.Code != "required" && !strings.HasPrefix(s.Code, "minlength") { + errs = append(errs, s) + } else if strings.Index(s.Field, ".") >= 0 { + errs = append(errs, s) + } + } + return errs +} diff --git a/validator/default_validator.go b/validator/default_validator.go new file mode 100644 index 0000000..edc32b1 --- /dev/null +++ b/validator/default_validator.go @@ -0,0 +1,116 @@ +package validator + +import ( + "context" + "fmt" + "reflect" + "strings" + "unicode" + + s "github.com/core-go/io/import" + "github.com/go-playground/validator/v10" +) + +const ( + method = "method" + patch = "patch" +) + +type DefaultValidator struct { + validate *validator.Validate + CustomValidateList []CustomValidate +} + +func NewValidator() *DefaultValidator { + list := GetCustomValidateList() + return &DefaultValidator{CustomValidateList: list} +} + +func (p *DefaultValidator) Validate(ctx context.Context, model interface{}) ([]s.ErrorMessage, error) { + errors := make([]s.ErrorMessage, 0) + if p.validate == nil { + validate := validator.New() + validate = p.RegisterCustomValidate(validate) + p.validate = validate + } + err := p.validate.Struct(model) + + if err != nil { + errors, err = MapErrors(err) + } + v := ctx.Value(method) + if v != nil { + v2, ok := v.(string) + if ok { + if v2 == patch { + errs := RemoveRequiredError(errors) + return errs, nil + } + } + } + return errors, err +} + +var alias = map[string]string{ + "max": "maxlength", + "min": "minlength", + "gtefield": "minfield", + "ltefield": "maxfield", +} + +func MapErrors(err error) (list []s.ErrorMessage, err1 error) { + if _, ok := err.(*validator.InvalidValidationError); ok { + err1 = fmt.Errorf("InvalidValidationError") + return + } + for _, err := range err.(validator.ValidationErrors) { + code := formatCodeMsg(err) + list = append(list, s.ErrorMessage{Field: FormatErrorField(err.Namespace()), Code: code}) + } + return +} + +func formatCodeMsg(err validator.FieldError) string { + var code string + if aliasTag, ok := alias[err.Tag()]; ok { + if (err.Tag() == "max" || err.Tag() == "min") && err.Kind() != reflect.String { + code = err.Tag() + } else { + code = aliasTag + } + } else { + code = err.Tag() + } + if err.Param() != "" { + code += ":" + lcFirstChar(err.Param()) + } + return code +} +func (p *DefaultValidator) RegisterCustomValidate(validate *validator.Validate) *validator.Validate { + for _, v := range p.CustomValidateList { + validate.RegisterValidation(v.Tag, v.Fn) + } + return validate +} +func FormatErrorField(s string) string { + splitField := strings.Split(s, ".") + length := len(splitField) + if length == 1 { + return lcFirstChar(splitField[0]) + } else if length > 1 { + var tmp []string + for _, v := range splitField[1:] { + tmp = append(tmp, lcFirstChar(v)) + } + return strings.Join(tmp, ".") + } + return s +} +func lcFirstChar(s string) string { + if len(s) > 0 { + runes := []rune(s) + runes[0] = unicode.ToLower(runes[0]) + return string(runes) + } + return s +} diff --git a/validator/error_checker.go b/validator/error_checker.go new file mode 100644 index 0000000..f3d2b78 --- /dev/null +++ b/validator/error_checker.go @@ -0,0 +1,8 @@ +package validator + +import sv "github.com/core-go/io/import" + +func NewErrorChecker() *sv.ErrorChecker { + v := NewValidator() + return sv.NewErrorChecker(v.Validate) +} diff --git a/validator/validation.go b/validator/validation.go new file mode 100644 index 0000000..e2ddd6e --- /dev/null +++ b/validator/validation.go @@ -0,0 +1,378 @@ +package validator + +import ( + "net" + "net/url" + "regexp" + "strings" +) + +var ( + emailPattern = "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$" + phonePattern = `^(?:(?:\(?(?:00|\+)([1-4]\d\d|[1-9]\d?)\)?)?[\-\.\ \\\/]?)?((?:\(?\d{1,}\)?[\-\.\ \\\/]?){0,})(?:[\-\.\ \\\/]?(?:#|ext\.?|extension|x)[\-\.\ \\\/]?(\d+))?$` + faxPattern = `^(\+?\d{1,}(\s?|\-?)\d*(\s?|\-?)\(?\d{2,}\)?(\s?|\-?)\d{3,}\s?\d{3,})$` + + regEmail = regexp.MustCompile(emailPattern) + regPhone = regexp.MustCompile(phonePattern) + regFax = regexp.MustCompile(faxPattern) +) + +func IsEmpty(v string) bool { + return len(strings.TrimSpace(v)) == 0 +} + +func IsDigit(v string) bool { + // result, _ := regexp.MatchString(`\D+`, v) + // return !result + var len = len(v) - 1 + for i := 0; i <= len; i++ { + var chr = string(v[i]) + if !(chr >= "0" && chr <= "9") { + return false + } + } + return true +} + +func IsDashDigit(v string) bool { + if IsEmpty(v) == true { + return false + } + var len = len(v) - 1 + for i := 0; i <= len; i++ { + var chr = string(v[i]) + if !((chr >= "0" && chr <= "9") || chr == "-") { + return false + } + } + return true +} + +func IsAbc(v string) bool { + if IsEmpty(v) == true { + return false + } + var len = len(v) - 1 + for i := 0; i <= len; i++ { + var chr = string(v[i]) + if !((chr >= "A" && chr <= "Z") || (chr >= "a" && chr <= "z")) { + return false + } + } + return true +} + +func IsCode(v string) bool { + if IsEmpty(v) == true { + return false + } + var len = len(v) - 1 + for i := 0; i <= len; i++ { + var chr = string(v[i]) + if !((chr >= "0" && chr <= "9") || (chr >= "A" && chr <= "Z") || (chr >= "a" && chr <= "z")) { + return false + } + } + return true +} +func IsUserName(v string) bool { + if len(v) < 6 { + return false + } + if IsEmail(v) { + return true + } + if IsPhone(v) { + return true + } + if len(v) > 30 { + return false + } + + v2 := v + "@gmail.com" + return IsEmail(v2) +} +func IsDashCode(v string) bool { + if IsEmpty(v) == true { + return false + } + var len = len(v) - 1 + for i := 0; i <= len; i++ { + var chr = string(v[i]) + if !((chr >= "0" && chr <= "9") || (chr >= "A" && chr <= "Z") || (chr >= "a" && chr <= "z") || chr == "-" || chr == "_") { + return false + } + } + return true +} + +func IsEmail(v string) bool { + return regEmail.MatchString(v) +} + +func IsUrl(v string) bool { + u, err := url.Parse(v) + return err == nil && u.Scheme != "" && u.Host != "" +} +func IsUri(v string) bool { + _, err := url.ParseRequestURI(v) + return err == nil +} +func IsIpAddress(s string) bool { + ip := net.ParseIP(s) + return ip != nil +} +func IsIpAddressV4(s string) bool { + ip := net.ParseIP(s) + return ip != nil && strings.Contains(s, ".") +} +func IsIpAddressV6(s string) bool { + ip := net.ParseIP(s) + return ip != nil && strings.Contains(s, ":") +} + +func IsValidPattern(pattern string, v string) bool { + reg := regexp.MustCompile(pattern) + return reg.MatchString(v) +} + +func IsPhone(v string) bool { + if strings.HasPrefix(v, "+") { + chars := []rune(v) + if IsDigit(string(chars[1:])) { + for i := 2; i <= 4; i++ { + countryCode := string(chars[1:i]) + if _, ok := PhoneDic[countryCode]; ok { + return true + } + } + return false + } else { + return false + } + } else { + return regPhone.MatchString(v) + } +} + +func IsFax(v string) bool { + if strings.HasPrefix(v, "+") { + chars := []rune(v) + for i := 2; i <= 4; i++ { + countryCode := string(chars[1:i]) + if _, ok := PhoneDic[countryCode]; ok { + return regFax.MatchString(v) + } + } + return false + } else { + return regFax.MatchString(v) + } +} + +var PhoneDic = map[string]string{ + "1": "CA:CAN,US:USA", + "20": "EG:EGY", + "211": "SS:SSD", + "212": "MA:MAR,EH:ESH", + "213": "DZ:DZA", + "216": "TN:TUN", + "218": "LY:LBY", + "220": "GM:GMB", + "221": "SN:SEN", + "222": "MR:MRT", + "223": "ML:MLI", + "224": "GN:GIN", + "225": "CI:CIV", + "226": "BF:BFA", + "227": "NE:NER", + "228": "TG:TGO", + "229": "BJ:BEN", + "230": "MU:MUS", + "231": "LR:LBR", + "232": "SL:SLE", + "233": "GH:GHA", + "234": "NG:NGA", + "235": "TD:TCD", + "236": "CF:CAF", + "237": "CM:CMR", + "238": "CV:CPV", + "239": "ST:STP", + "240": "GQ:GNQ", + "241": "GA:GAB", + "242": "CG:COG", + "243": "CD:COD", + "244": "AO:AGO", + "245": "GW:GNB", + "246": "IO:IOT", + "248": "SC:SYC", + "249": "SD:SDN", + "250": "RW:RWA", + "251": "ET:ETH", + "252": "SO:SOM", + "253": "DJ:DJI", + "254": "KE:KEN", + "255": "TZ:TZA", + "256": "UG:UGA", + "257": "BI:BDI", + "258": "MZ:MOZ", + "260": "ZM:ZMB", + "261": "MG:MDG", + "262": "RE:REU,YT:MYT", + "263": "ZW:ZWE", + "264": "NA:NAM", + "265": "MW:MWI", + "266": "LS:LSO", + "267": "BW:BWA", + "268": "SZ:SWZ", + "269": "KM:COM", + "27": "ZA:ZAF", + "290": "SH:SHN", + "291": "ER:ERI", + "297": "AW:ABW", + "298": "FO:FRO", + "299": "GL:GRL", + "30": "GR:GRC", + "31": "NL:NLD", + "32": "BE:BEL", + "33": "FR:FRA", + "34": "ES:ESP", + "350": "GI:GIB", + "351": "PT:PRT", + "352": "LU:LUX", + "353": "IE:IRL", + "354": "IS:ISL", + "355": "AL:ALB", + "356": "MT:MLT", + "357": "CY:CYP", + "358": "FI:FIN", + "359": "BG:BGR", + "36": "HU:HUN", + "370": "LT:LTU", + "371": "LV:LVA", + "372": "EE:EST", + "373": "MD:MDA", + "374": "AM:ARM", + "375": "BY:BLR", + "376": "AD:AND", + "377": "MC:MCO", + "378": "SM:SMR", + "379": "VA:VAT", + "380": "UA:UKR", + "381": "RS:SRB", + "382": "ME:MNE", + "383": "XK:XKX", + "385": "HR:HRV", + "386": "SI:SVN", + "387": "BA:BIH", + "389": "MK:MKD", + "39": "IT:ITA", + "40": "RO:ROU", + "41": "CH:CHE", + "420": "CZ:CZE", + "421": "SK:SVK", + "423": "LI:LIE", + "43": "AT:AUT", + "44": "GB:GBR", + "45": "DK:DNK", + "46": "SE:SWE", + "47": "NO:NOR,SJ:SJM", + "48": "PL:POL", + "49": "DE:DEU", + "500": "FK:FLK", + "501": "BZ:BLZ", + "502": "GT:GTM", + "503": "SV:SLV", + "504": "HN:HND", + "505": "NI:NIC", + "506": "CR:CRI", + "507": "PA:PAN", + "508": "PM:SPM", + "509": "HT:HTI", + "51": "PE:PER", + "52": "MX:MEX", + "53": "CU:CUB", + "54": "AR:ARG", + "55": "BR:BRA", + "56": "CL:CHL", + "57": "CO:COL", + "58": "VE:VEN", + "590": "BL:BLM,MF:MAF", + "591": "BO:BOL", + "592": "GY:GUY", + "593": "EC:ECU", + "595": "PY:PRY", + "597": "SR:SUR", + "598": "UY:URY", + "599": "AN:ANT,CW:CUW", + "60": "MY:MYS", + "61": "AU:AUS,CC:CCK,CX:CXR", + "62": "ID:IDN", + "63": "PH:PHL", + "64": "NZ:NZL,PN:PCN", + "65": "SG:SGP", + "66": "TH:THA", + "670": "TL:TLS", + "672": "AQ:ATA", + "673": "BN:BRN", + "674": "NR:NRU", + "675": "PG:PNG", + "676": "TO:TON", + "677": "SB:SLB", + "678": "VU:VUT", + "679": "FJ:FJI", + "680": "PW:PLW", + "681": "WF:WLF", + "682": "CK:COK", + "683": "NU:NIU", + "685": "WS:WSM", + "686": "KI:KIR", + "687": "NC:NCL", + "688": "TV:TUV", + "689": "PF:PYF", + "690": "TK:TKL", + "691": "FM:FSM", + "692": "MH:MHL", + "7": "KZ:KAZ,RU:RUS", + "81": "JP:JPN", + "82": "KR:KOR", + "84": "VN:VNM", + "86": "CN:CHN", + "850": "KP:PRK", + "852": "HK:HKG", + "853": "MO:MAC", + "855": "KH:KHM", + "856": "LA:LAO", + "880": "BD:BGD", + "886": "TW:TWN", + "90": "TR:TUR", + "91": "IN:IND", + "92": "PK:PAK", + "93": "AF:AFG", + "94": "LK:LKA", + "95": "MM:MMR", + "98": "IR:IRN", + "960": "MV:MDV", + "961": "LB:LBN", + "962": "JO:JOR", + "963": "SY:SYR", + "964": "IQ:IRQ", + "965": "KW:KWT", + "966": "SA:SAU", + "967": "YE:YEM", + "968": "OM:OMN", + "970": "PS:PSE", + "971": "AE:ARE", + "972": "IL:ISR", + "973": "BH:BHR", + "974": "QA:QAT", + "975": "BT:BTN", + "976": "MN:MNG", + "977": "NP:NPL", + "992": "TJ:TJK", + "993": "TM:TKM", + "994": "AZ:AZE", + "995": "GE:GEO", + "996": "KG:KGZ", + "998": "UZ:UZB", +}