diff --git a/cryptor/rsa_private_example.pem b/cryptor/rsa_private_example.pem index 4d00a55..c5b82ab 100644 --- a/cryptor/rsa_private_example.pem +++ b/cryptor/rsa_private_example.pem @@ -1,51 +1,51 @@ -----BEGIN rsa private key----- -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 -----END rsa private key----- diff --git a/cryptor/rsa_public_example.pem b/cryptor/rsa_public_example.pem index ce29999..4b53ad6 100644 --- a/cryptor/rsa_public_example.pem +++ b/cryptor/rsa_public_example.pem @@ -1,14 +1,14 @@ -----BEGIN rsa public key----- -MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA8O2hLVt/7v6rQF2eGX3J -+QyteV45lI/5qlyqTe7CiwUg1bkJ2yLHaoGSWoVtthE/X7GZgsw57VEgnazZAehe -XcxV8OJWBGuRfmXabtKIzSBF8uZj+H3iPDvJB7NggMh8teYsdBSn9Cm/L5FesDvg -/KxUKKfBU4LSrhOMhBauIoOSVU659y3kYgpzC/ormvCsJSe3KSdIhtLxiv+dnEHh -9WvsXWB50lIw3TIC041aL5mwRvb5/JWcjtn5bgLEChrnEGF0RYUDMwm8hT1YsEN9 -Rb9fOx5ZTUZNRVHYjaCyUA+cG+gC72EjrkXY4If3pAESNcF+5NGGJaETd8rHzHsR -FRVPXkwhRmkx9nBF+RjQhouqBTptIcIO2rPlTC9H5Oeg+jVljlBJ7/ESeOlg2OEb -YyOUJpAPMPcz5HiNSTRHuDWIDUuk48BOF9Vo00PJq/oXRzUa9HHhu6cosfZH4P4P -kDthxq89migqUL+C8WqeZ8I+xFS8/1TpvjOnOYF5ADvByC69UGPEoh+o+ZpgWfAs -Crb9Qwy9WAAZg0rsouoPqb4Qk6Vr88X6I20Ys9AayLzZ72ddoS5IweOoF5/xLfii -cGLKckNwG6sy4f1LCFB2Xc79nYqJzlwPGC3pKfEIAXUj1I/p+/MqaTDJQs72dhr0 -lTX5505XUBqrbX0BPGcZvIsCAwEAAQ== +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAudV/zW+ycOExUja9W3Zy +hKWA2TN+FqTzfZKPB+btwe4Md0WJTM0+ZdT8UXujltTEWSUhY/qkOiNIutF2CiFW +onDQeNzMobLB/pmq1P0Z+LVH4ERsbcl9zYCfpvTsnIqzjuPe30iozK0Er03qBxsH +nWV3WbIl3+1f17T6OD5CkdT+9RCID1EqsQ+9aGIeR6cmoB+rxjPLb0xc5oS1hbb3 +FkiT7VLI2doeqP8Pmwdohbh7XmgJQkok+ALxKQ4bCMJ780k2KigKGjXxKlYJq1ZF +301sbhvTo2cSci4ieXP0A4B4swSzLKl0G7IX/UbYACn3qNecvQt5OtFM644mqfSU +ffFg7PefZVZhaUytvU92W+b0LXF2NbjhtVES5HByDwjjF/KOfV7U/o+YmAjlakie +YM7pcggfgfqyZWdF70nSvgPgVt5qtOnYPeUrQV2aUmZE+BagOQ/HAIKPbhmyMEA3 +odgqaALsvD/58iVv9syqEEm5trLYA/p2uo0yv3iHrEggEZkjPXkrgbZ6lZNafGaZ +Hs0ANg+7NIJR9joKvXGJkg8E2thpg9UKl/Z2astZk7o92trMp5DQW1vjV7JW+mEQ +dztQxE5NgeE8cI/BdqMSyvG2UA7u5LqBHXx35s3gPva+eutKnTcRpO3T01PH2sba +iXiiNG5oFUYjocikhY7f3TUCAwEAAQ== -----END rsa public key----- diff --git a/enum/enum.go b/enum/enum.go new file mode 100644 index 0000000..46d9649 --- /dev/null +++ b/enum/enum.go @@ -0,0 +1,312 @@ +// Copyright 2025 dudaodong@gmail.com. All rights resulterved. +// Use of this source code is governed by MIT license + +// Package enum provides a simple enum implementation. +package enum + +import ( + "encoding/json" + "fmt" + "reflect" + "sort" + "sync" +) + +// Enum defines a common enum interface. +type Enum[T comparable] interface { + Value() T + String() string + Name() string + Valid(checker ...func(T) bool) bool +} + +// Item defines a common enum item struct implement Enum interface. +type Item[T comparable] struct { + value T + name string +} + +func NewItem[T comparable](value T, name string) *Item[T] { + return &Item[T]{value: value, name: name} +} + +// Pair represents a value-name pair for creating enum items +type Pair[T comparable] struct { + Value T + Name string +} + +// NewItemsFromPairs creates enum items from a slice of Pair structs. +func NewItems[T comparable](pairs ...Pair[T]) []*Item[T] { + if len(pairs) == 0 { + return []*Item[T]{} + } + + items := make([]*Item[T], 0, len(pairs)) + for _, pair := range pairs { + items = append(items, &Item[T]{value: pair.Value, name: pair.Name}) + } + + return items +} + +func (e *Item[T]) Value() T { + return e.value +} + +func (e *Item[T]) Name() string { + return e.name +} + +func (e *Item[T]) String() string { + return e.name +} + +// Valid checks if the enum item is valid. If a custom check function is provided, it will be used to validate the value. +func (e *Item[T]) Valid(checker ...func(T) bool) bool { + if len(checker) > 0 { + return checker[0](e.value) && e.name != "" + } + var zero T + return e.value != zero && e.name != "" +} + +// MarshalJSON implements the json.Marshaler interface. +func (e *Item[T]) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]any{ + "value": e.value, + "name": e.name, + }) +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (e *Item[T]) UnmarshalJSON(data []byte) error { + type alias struct { + Value any `json:"value"` + Name string `json:"name"` + } + var temp alias + if err := json.Unmarshal(data, &temp); err != nil { + return err + } + + var v T + rv := reflect.TypeOf(v) + switch rv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + val, ok := temp.Value.(float64) + if !ok { + return fmt.Errorf("invalid type for value, want int family") + } + converted := reflect.ValueOf(int64(val)).Convert(rv) + e.value = converted.Interface().(T) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + val, ok := temp.Value.(float64) + if !ok { + return fmt.Errorf("invalid type for value, want uint family") + } + converted := reflect.ValueOf(uint64(val)).Convert(rv) + e.value = converted.Interface().(T) + case reflect.Float32, reflect.Float64: + val, ok := temp.Value.(float64) + if !ok { + return fmt.Errorf("invalid type for value, want float family") + } + converted := reflect.ValueOf(val).Convert(rv) + e.value = converted.Interface().(T) + case reflect.String: + val, ok := temp.Value.(string) + if !ok { + return fmt.Errorf("invalid type for value, want string") + } + e.value = any(val).(T) + case reflect.Bool: + val, ok := temp.Value.(bool) + if !ok { + return fmt.Errorf("invalid type for value, want bool") + } + e.value = any(val).(T) + default: + // 枚举类型底层通常是 int,可以尝试 float64->int64->底层类型 + val, ok := temp.Value.(float64) + if ok { + converted := reflect.ValueOf(int64(val)).Convert(rv) + e.value = converted.Interface().(T) + } else { + val2, ok2 := temp.Value.(T) + if !ok2 { + return fmt.Errorf("invalid type for value") + } + e.value = val2 + } + } + e.name = temp.Name + return nil +} + +// Registry defines a common enum registry struct. +type Registry[T comparable] struct { + mu sync.RWMutex + values map[T]*Item[T] + names map[string]*Item[T] + items []*Item[T] +} + +func NewRegistry[T comparable](items ...*Item[T]) *Registry[T] { + r := &Registry[T]{ + values: make(map[T]*Item[T]), + names: make(map[string]*Item[T]), + items: make([]*Item[T], 0, len(items)), + } + + r.Add(items...) + + return r +} + +// Add adds enum items to the registry. +func (r *Registry[T]) Add(items ...*Item[T]) { + r.mu.Lock() + defer r.mu.Unlock() + + for _, item := range items { + if _, exists := r.values[item.value]; exists { + continue + } + if _, exists := r.names[item.name]; exists { + continue + } + + r.values[item.value] = item + r.names[item.name] = item + r.items = append(r.items, item) + } +} + +// Remove removes an enum item from the registry by its value. +func (r *Registry[T]) Remove(value T) bool { + r.mu.Lock() + defer r.mu.Unlock() + item, ok := r.values[value] + if !ok { + return false + } + delete(r.values, value) + delete(r.names, item.name) + for i, it := range r.items { + if it.value == value { + r.items = append(r.items[:i], r.items[i+1:]...) + break + } + } + return true +} + +// Update updates the name of an enum item in the registry by its value. +func (r *Registry[T]) Update(value T, newName string) bool { + r.mu.Lock() + defer r.mu.Unlock() + item, ok := r.values[value] + if !ok { + return false + } + delete(r.names, item.name) + item.name = newName + r.names[newName] = item + return true +} + +// GetByValue retrieves an enum item by its value. +func (r *Registry[T]) GetByValue(value T) (*Item[T], bool) { + r.mu.RLock() + defer r.mu.RUnlock() + + item, ok := r.values[value] + return item, ok +} + +// GetByName retrieves an enum item by its name. +func (r *Registry[T]) GetByName(name string) (*Item[T], bool) { + r.mu.RLock() + defer r.mu.RUnlock() + + item, ok := r.names[name] + return item, ok +} + +// Items returns a slice of all enum items in the registry. +func (r *Registry[T]) Items() []*Item[T] { + r.mu.RLock() + defer r.mu.RUnlock() + + result := make([]*Item[T], len(r.items)) + copy(result, r.items) + return result +} + +// Contains checks if an enum item with the given value exists in the registry. +func (r *Registry[T]) Contains(value T) bool { + _, ok := r.GetByValue(value) + return ok +} + +// Validate checks if the given value is a valid enum item in the registry. +func (r *Registry[T]) Validate(value T) error { + if !r.Contains(value) { + return fmt.Errorf("invalid enum value: %v", value) + } + return nil +} + +// ValidateAll checks if all given values are valid enum items in the registry. +func (r *Registry[T]) ValidateAll(values ...T) error { + for _, value := range values { + if err := r.Validate(value); err != nil { + return err + } + } + return nil +} + +// Size returns the number of enum items in the registry. +func (r *Registry[T]) Size() int { + r.mu.RLock() + defer r.mu.RUnlock() + + return len(r.items) +} + +// Range iterates over all enum items in the registry and applies the given function. +func (r *Registry[T]) Range(fn func(*Item[T]) bool) { + r.mu.RLock() + defer r.mu.RUnlock() + + for _, item := range r.items { + if !fn(item) { + break + } + } +} + +// SortedItems returns a slice of all enum items sorted by the given less function. +func (r *Registry[T]) SortedItems(less func(*Item[T], *Item[T]) bool) []*Item[T] { + items := r.Items() + sort.Slice(items, func(i, j int) bool { + return less(items[i], items[j]) + }) + return items +} + +// Filter returns a slice of enum items that satisfy the given predicate function. +func (r *Registry[T]) Filter(predicate func(*Item[T]) bool) []*Item[T] { + r.mu.RLock() + defer r.mu.RUnlock() + + var result []*Item[T] + for _, item := range r.items { + if predicate(item) { + result = append(result, item) + } + } + return result +} diff --git a/enum/enum_example_test.go b/enum/enum_example_test.go new file mode 100644 index 0000000..1b600cb --- /dev/null +++ b/enum/enum_example_test.go @@ -0,0 +1,222 @@ +package enum + +import "fmt" + +func ExampleNewItem() { + items := NewItems( + Pair[Status]{Value: Active, Name: "Active"}, + Pair[Status]{Value: Inactive, Name: "Inactive"}, + ) + + fmt.Println(items[0].Name(), items[0].Value()) + fmt.Println(items[1].Name(), items[1].Value()) + + // Output: + // Active 1 + // Inactive 2 +} + +func ExampleItem_Valid() { + item := NewItem(Active, "Active") + fmt.Println(item.Valid()) + + invalidItem := NewItem(Unknown, "") + fmt.Println(invalidItem.Valid()) + + // Output: + // true + // false +} + +func ExampleItem_MarshalJSON() { + item := NewItem(Active, "Active") + data, _ := item.MarshalJSON() + fmt.Println(string(data)) + + var unmarshaledItem Item[Status] + _ = unmarshaledItem.UnmarshalJSON(data) + fmt.Println(unmarshaledItem.Name(), unmarshaledItem.Value()) + + // Output: + // {"name":"Active","value":1} + // Active 1 +} + +func ExampleRegistry_Add() { + registry := NewRegistry[Status]() + item1 := NewItem(Active, "Active") + item2 := NewItem(Inactive, "Inactive") + + registry.Add(item1, item2) + + if item, found := registry.GetByValue(Active); found { + fmt.Println("Found by value:", item.Name()) + } + + if item, found := registry.GetByName("Inactive"); found { + fmt.Println("Found by name:", item.Value()) + } + + // Output: + // Found by value: Active + // Found by name: 2 +} + +func ExampleRegistry_Remove() { + registry := NewRegistry[Status]() + item1 := NewItem(Active, "Active") + + registry.Add(item1) + fmt.Println("Size before removal:", registry.Size()) + + removed := registry.Remove(Active) + fmt.Println("Removed:", removed) + fmt.Println("Size after removal:", registry.Size()) + + // Output: + // Size before removal: 1 + // Removed: true + // Size after removal: 0 +} + +func ExampleRegistry_Update() { + registry := NewRegistry[Status]() + item1 := NewItem(Active, "Active") + + registry.Add(item1) + updated := registry.Update(Active, "Activated") + fmt.Println("Updated:", updated) + + if item, found := registry.GetByValue(Active); found { + fmt.Println("New name:", item.Name()) + } + + // Output: + // Updated: true + // New name: Activated +} + +func ExampleRegistry_Items() { + registry := NewRegistry[Status]() + item1 := NewItem(Active, "Active") + item2 := NewItem(Inactive, "Inactive") + + registry.Add(item1, item2) + + for _, item := range registry.Items() { + fmt.Println(item.Name(), item.Value()) + } + + // Output: + // Active 1 + // Inactive 2 +} + +func ExampleRegistry_Contains() { + registry := NewRegistry[Status]() + item1 := NewItem(Active, "Active") + registry.Add(item1) + + fmt.Println("Contains Active:", registry.Contains(Active)) + fmt.Println("Contains Inactive:", registry.Contains(Inactive)) +} + +func ExampleRegistry_Validate() { + registry := NewRegistry[Status]() + item1 := NewItem(Active, "Active") + item2 := NewItem(Inactive, "Inactive") + registry.Add(item1, item2) + + fmt.Println("Validate Active:", registry.Validate(Active)) + fmt.Println("Validate Inactive:", registry.Validate(Inactive)) + fmt.Println("Validate Unknown:", registry.Validate(Unknown)) + + // Output: + // Validate Active: + // Validate Inactive: + // Validate Unknown: invalid enum value: 0 +} + +func ExampleRegistry_ValidateAll() { + registry := NewRegistry[Status]() + item1 := NewItem(Active, "Active") + item2 := NewItem(Inactive, "Inactive") + registry.Add(item1, item2) + + fmt.Println("ValidateAll Active, Inactive:", registry.ValidateAll(Active, Inactive)) + fmt.Println("ValidateAll Active, Unknown:", registry.ValidateAll(Active, Unknown)) + + // Output: + // ValidateAll Active, Inactive: + // ValidateAll Active, Unknown: invalid enum value: 0 +} + +func ExampleRegistry_Size() { + registry := NewRegistry[Status]() + fmt.Println("Initial size:", registry.Size()) + + item1 := NewItem(Active, "Active") + item2 := NewItem(Inactive, "Inactive") + registry.Add(item1, item2) + + fmt.Println("Size after adding items:", registry.Size()) + + registry.Remove(Active) + fmt.Println("Size after removing an item:", registry.Size()) + + // Output: + // Initial size: 0 + // Size after adding items: 2 + // Size after removing an item: 1 +} + +func ExampleRegistry_Range() { + registry := NewRegistry[Status]() + item1 := NewItem(Active, "Active") + item2 := NewItem(Inactive, "Inactive") + registry.Add(item1, item2) + + registry.Range(func(item *Item[Status]) bool { + fmt.Println(item.Name(), item.Value()) + return true // continue iteration + }) + + // Output: + // Active 1 + // Inactive 2 +} + +func ExampleRegistry_SortedItems() { + registry := NewRegistry[Status]() + item1 := NewItem(Inactive, "Inactive") + item2 := NewItem(Active, "Active") + registry.Add(item1, item2) + + for _, item := range registry.SortedItems(func(i1, i2 *Item[Status]) bool { + return i1.value < i2.value + }) { + fmt.Println(item.Name(), item.Value()) + } + + // Output: + // Active 1 + // Inactive 2 +} + +func ExampleRegistry_Filter() { + registry := NewRegistry[Status]() + item1 := NewItem(Active, "Active") + item2 := NewItem(Inactive, "Inactive") + registry.Add(item1, item2) + + activeItems := registry.Filter(func(item *Item[Status]) bool { + return item.Value() == Active + }) + + for _, item := range activeItems { + fmt.Println(item.Name(), item.Value()) + } + + // Output: + // Active 1 +} diff --git a/enum/enum_test.go b/enum/enum_test.go new file mode 100644 index 0000000..fcbd5ff --- /dev/null +++ b/enum/enum_test.go @@ -0,0 +1,218 @@ +// Copyright 2025 dudaodong@gmail.com. All rights resulterved. +// Use of this source code is governed by MIT license + +package enum + +import ( + "testing" + + "github.com/duke-git/lancet/v2/internal" +) + +type Status int + +const ( + Unknown Status = iota + Active + Inactive +) + +func TestNewItem(t *testing.T) { + t.Parallel() + assert := internal.NewAssert(t, "TestNewItem") + + items := NewItems( + Pair[Status]{Value: Active, Name: "Active"}, + Pair[Status]{Value: Inactive, Name: "Inactive"}, + ) + + assert.Equal(2, len(items)) + assert.Equal(Active, items[0].Value()) + assert.Equal("Active", items[0].Name()) + assert.Equal(Inactive, items[1].Value()) + assert.Equal("Inactive", items[1].Name()) +} + +func TestItem_Valid(t *testing.T) { + t.Parallel() + assert := internal.NewAssert(t, "TestItem_Valid") + + item := NewItem(Active, "Active") + assert.Equal(true, item.Valid()) + + invalidItem := NewItem(Unknown, "") + assert.Equal(false, invalidItem.Valid()) +} + +func TestItem_MarshalJSON(t *testing.T) { + t.Parallel() + assert := internal.NewAssert(t, "TestItem_MarshalJSON") + + item := NewItem(Active, "Active") + data, err := item.MarshalJSON() + assert.IsNil(err) + assert.Equal("{\"name\":\"Active\",\"value\":1}", string(data)) + + var unmarshaledItem Item[Status] + err = unmarshaledItem.UnmarshalJSON(data) + assert.IsNil(err) + assert.Equal(item.Value(), unmarshaledItem.Value()) + assert.Equal(item.Name(), unmarshaledItem.Name()) +} + +func TestRegistry_AddAndGet(t *testing.T) { + t.Parallel() + assert := internal.NewAssert(t, "TestRegistry_AddAndGet") + + registry := NewRegistry[Status]() + item1 := NewItem(Active, "Active") + item2 := NewItem(Inactive, "Inactive") + + registry.Add(item1, item2) + + assert.Equal(2, registry.Size()) + + item, ok := registry.GetByValue(Active) + assert.Equal(true, ok) + assert.Equal("Active", item.Name()) + + item, ok = registry.GetByName("Inactive") + assert.Equal(true, ok) + assert.Equal(Inactive, item.Value()) +} + +func TestRegistry_Remove(t *testing.T) { + t.Parallel() + assert := internal.NewAssert(t, "TestRegistry_Remove") + + registry := NewRegistry[Status]() + item1 := NewItem(Active, "Active") + item2 := NewItem(Inactive, "Inactive") + + registry.Add(item1, item2) + assert.Equal(2, registry.Size()) + + removed := registry.Remove(Active) + assert.Equal(true, removed) + assert.Equal(1, registry.Size()) + + _, ok := registry.GetByValue(Active) + assert.Equal(false, ok) +} + +func TestRegistry_Update(t *testing.T) { + t.Parallel() + assert := internal.NewAssert(t, "TestRegistry_Update") + + registry := NewRegistry[Status]() + item1 := NewItem(Active, "Active") + registry.Add(item1) + + updated := registry.Update(Active, "Activated") + assert.Equal(true, updated) + + item, ok := registry.GetByValue(Active) + assert.Equal(true, ok) + assert.Equal("Activated", item.Name()) +} + +func TestRegistry_Contains(t *testing.T) { + t.Parallel() + assert := internal.NewAssert(t, "TestRegistry_Contains") + + registry := NewRegistry[Status]() + item1 := NewItem(Active, "Active") + registry.Add(item1) + + assert.Equal(true, registry.Contains(Active)) + assert.Equal(false, registry.Contains(Inactive)) +} + +func TestRegistry_Validate(t *testing.T) { + t.Parallel() + assert := internal.NewAssert(t, "TestRegistry_Validate") + + registry := NewRegistry[Status]() + item1 := NewItem(Active, "Active") + item2 := NewItem(Inactive, "Inactive") + registry.Add(item1, item2) + + err := registry.Validate(Active) + assert.IsNil(err) + err = registry.Validate(Inactive) + assert.IsNil(err) + + err = registry.Validate(Unknown) + assert.IsNotNil(err) +} + +func TestRegistry_ValidateAll(t *testing.T) { + t.Parallel() + assert := internal.NewAssert(t, "TestRegistry_ValidateAll") + + registry := NewRegistry[Status]() + item1 := NewItem(Active, "Active") + item2 := NewItem(Inactive, "Inactive") + registry.Add(item1, item2) + + err := registry.ValidateAll(Active, Inactive) + assert.IsNil(err) + + err = registry.ValidateAll(Active, Unknown) + assert.IsNotNil(err) +} + +func TestRegistry_Range(t *testing.T) { + t.Parallel() + assert := internal.NewAssert(t, "TestRegistry_Range") + + registry := NewRegistry[Status]() + item1 := NewItem(Active, "Active") + item2 := NewItem(Inactive, "Inactive") + registry.Add(item1, item2) + + var values []Status + registry.Range(func(item *Item[Status]) bool { + values = append(values, item.Value()) + return true + }) + + assert.Equal(2, len(values)) + assert.Equal(Active, values[0]) + assert.Equal(Inactive, values[1]) +} + +func TestRegistry_SortedItems(t *testing.T) { + t.Parallel() + assert := internal.NewAssert(t, "TestRegistry_SortedItems") + + registry := NewRegistry[Status]() + item1 := NewItem(Inactive, "Inactive") + item2 := NewItem(Active, "Active") + registry.Add(item1, item2) + + sortedItems := registry.SortedItems(func(i1, i2 *Item[Status]) bool { + return i1.value < i2.value + }) + + assert.Equal(2, len(sortedItems)) + assert.Equal(Active, sortedItems[0].Value()) + assert.Equal(Inactive, sortedItems[1].Value()) +} + +func TestRegistry_Filter(t *testing.T) { + t.Parallel() + assert := internal.NewAssert(t, "TestRegistry_Filter") + + registry := NewRegistry[Status]() + item1 := NewItem(Active, "Active") + item2 := NewItem(Inactive, "Inactive") + registry.Add(item1, item2) + + filteredItems := registry.Filter(func(item *Item[Status]) bool { + return item.Value() == Active + }) + + assert.Equal(1, len(filteredItems)) + assert.Equal(Active, filteredItems[0].Value()) +} diff --git a/validator/validator.go b/validator/validator.go index 1362eb8..5c80737 100644 --- a/validator/validator.go +++ b/validator/validator.go @@ -19,30 +19,48 @@ import ( ) var ( - alphaMatcher *regexp.Regexp = regexp.MustCompile(`^[a-zA-Z]+$`) - letterRegexMatcher *regexp.Regexp = regexp.MustCompile(`[a-zA-Z]`) - alphaNumericMatcher *regexp.Regexp = regexp.MustCompile(`^[a-zA-Z0-9-]+$`) - numberRegexMatcher *regexp.Regexp = regexp.MustCompile(`\d`) - intStrMatcher *regexp.Regexp = regexp.MustCompile(`^[\+-]?\d+$`) - urlMatcher *regexp.Regexp = regexp.MustCompile(`^((ftp|http|https?):\/\/)?(\S+(:\S*)?@)?((([1-9]\d?|1\d\d|2[01]\d|22[0-3])(\.(1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.([0-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(([a-zA-Z0-9]+([-\.][a-zA-Z0-9]+)*)|((www\.)?))?(([a-z\x{00a1}-\x{ffff}0-9]+-?-?)*[a-z\x{00a1}-\x{ffff}0-9]+)(?:\.([a-z\x{00a1}-\x{ffff}]{2,}))?))(:(\d{1,5}))?((\/|\?|#)[^\s]*)?$`) - dnsMatcher *regexp.Regexp = regexp.MustCompile(`^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$`) - emailMatcher *regexp.Regexp = regexp.MustCompile(`^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,4}$`) - chineseMobileMatcher *regexp.Regexp = regexp.MustCompile(`^1(?:3\d|4[4-9]|5[0-35-9]|6[67]|7[013-8]|8\d|9\d)\d{8}$`) - chineseIdMatcher *regexp.Regexp = regexp.MustCompile(`^(\d{17})([0-9]|X|x)$`) - chineseMatcher *regexp.Regexp = regexp.MustCompile("[\u4e00-\u9fa5]") - chinesePhoneMatcher *regexp.Regexp = regexp.MustCompile(`\d{3}-\d{8}|\d{4}-\d{7}|\d{4}-\d{8}`) - creditCardMatcher *regexp.Regexp = regexp.MustCompile(`^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|(222[1-9]|22[3-9][0-9]|2[3-6][0-9]{2}|27[01][0-9]|2720)[0-9]{12}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\\d{3})\\d{11}|6[27][0-9]{14})$`) - base64Matcher *regexp.Regexp = regexp.MustCompile(`^(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+\\/]{3}=|[A-Za-z0-9+\\/]{4})$`) - base64URLMatcher *regexp.Regexp = regexp.MustCompile(`^([A-Za-z0-9_-]{4})*([A-Za-z0-9_-]{2}(==)?|[A-Za-z0-9_-]{3}=?)?$`) - binMatcher *regexp.Regexp = regexp.MustCompile(`^(0b)?[01]+$`) - hexMatcher *regexp.Regexp = regexp.MustCompile(`^(#|0x|0X)?[0-9a-fA-F]+$`) - visaMatcher *regexp.Regexp = regexp.MustCompile(`^4[0-9]{12}(?:[0-9]{3})?$`) - masterCardMatcher *regexp.Regexp = regexp.MustCompile(`^5[1-5][0-9]{14}$`) - americanExpressMatcher *regexp.Regexp = regexp.MustCompile(`^3[47][0-9]{13}$`) - unionPay *regexp.Regexp = regexp.MustCompile("^62[0-5]\\d{13,16}$") - chinaUnionPay *regexp.Regexp = regexp.MustCompile(`^62[0-9]{14,17}$`) + alphaMatcher *regexp.Regexp = regexp.MustCompile(`^[a-zA-Z]+$`) + letterRegexMatcher *regexp.Regexp = regexp.MustCompile(`[a-zA-Z]`) + alphaNumericMatcher *regexp.Regexp = regexp.MustCompile(`^[a-zA-Z0-9-]+$`) + numberRegexMatcher *regexp.Regexp = regexp.MustCompile(`\d`) + intStrMatcher *regexp.Regexp = regexp.MustCompile(`^[\+-]?\d+$`) + urlMatcher *regexp.Regexp = regexp.MustCompile(`^((ftp|http|https?):\/\/)?(\S+(:\S*)?@)?((([1-9]\d?|1\d\d|2[01]\d|22[0-3])(\.(1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.([0-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(([a-zA-Z0-9]+([-\.][a-zA-Z0-9]+)*)|((www\.)?))?(([a-z\x{00a1}-\x{ffff}0-9]+-?-?)*[a-z\x{00a1}-\x{ffff}0-9]+)(?:\.([a-z\x{00a1}-\x{ffff}]{2,}))?))(:(\d{1,5}))?((\/|\?|#)[^\s]*)?$`) + dnsMatcher *regexp.Regexp = regexp.MustCompile(`^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$`) + // emailMatcher *regexp.Regexp = regexp.MustCompile(`^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,4}$`) + emailMatcher *regexp.Regexp = regexp.MustCompile(`\w+(-+.\w+)*@\w+(-.\w+)*.\w+(-.\w+)*`) + chineseMobileMatcher *regexp.Regexp = regexp.MustCompile(`^1(?:3\d|4[4-9]|5[0-35-9]|6[67]|7[013-8]|8\d|9\d)\d{8}$`) + chineseIdMatcher *regexp.Regexp = regexp.MustCompile(`([1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx])|([1-9]\d{5}\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{2}[0-9Xx])`) + chineseMatcher *regexp.Regexp = regexp.MustCompile("[\u4e00-\u9fa5]") + chinesePhoneMatcher *regexp.Regexp = regexp.MustCompile(`\d{3}-\d{8}|\d{4}-\d{7}|\d{4}-\d{8}`) + creditCardMatcher *regexp.Regexp = regexp.MustCompile(`^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|(222[1-9]|22[3-9][0-9]|2[3-6][0-9]{2}|27[01][0-9]|2720)[0-9]{12}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\\d{3})\\d{11}|6[27][0-9]{14})$`) + base64Matcher *regexp.Regexp = regexp.MustCompile(`^(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+\\/]{3}=|[A-Za-z0-9+\\/]{4})$`) + base64URLMatcher *regexp.Regexp = regexp.MustCompile(`^([A-Za-z0-9_-]{4})*([A-Za-z0-9_-]{2}(==)?|[A-Za-z0-9_-]{3}=?)?$`) + binMatcher *regexp.Regexp = regexp.MustCompile(`^(0b)?[01]+$`) + hexMatcher *regexp.Regexp = regexp.MustCompile(`^(#|0x|0X)?[0-9a-fA-F]+$`) + visaMatcher *regexp.Regexp = regexp.MustCompile(`^4[0-9]{12}(?:[0-9]{3})?$`) + masterCardMatcher *regexp.Regexp = regexp.MustCompile(`^5[1-5][0-9]{14}$`) + americanExpressMatcher *regexp.Regexp = regexp.MustCompile(`^3[47][0-9]{13}$`) + unionPayMatcher *regexp.Regexp = regexp.MustCompile(`^62[0-5]\\d{13,16}$`) + chinaUnionPayMatcher *regexp.Regexp = regexp.MustCompile(`^62[0-9]{14,17}$`) + chineseHMPassportMatcher *regexp.Regexp = regexp.MustCompile(`^[CM]\d{8}$`) ) +var passportMatcher = map[string]*regexp.Regexp{ + "CN": regexp.MustCompile(`^P\d{9}$`), + "US": regexp.MustCompile(`^\d{9}$`), + "GB": regexp.MustCompile(`^[A-Z0-9]{9}$`), + "RU": regexp.MustCompile(`^[A-Z]{2}\d{7}$`), + "DE": regexp.MustCompile(`^\d{9}$`), + "FR": regexp.MustCompile(`^[A-Z]{2}\d{7}$`), + "JP": regexp.MustCompile(`^\d{8}$`), + "IT": regexp.MustCompile(`^\d{8}$`), + "AU": regexp.MustCompile(`^[A-Z]{1}\d{8}$`), + "BR": regexp.MustCompile(`^\d{9}$`), + "IN": regexp.MustCompile(`^[A-Z]{1,2}\d{7}$`), + "HK": regexp.MustCompile(`^M\d{8}$`), + "MO": regexp.MustCompile(`^[A-Z]\d{8}$`), +} + var ( // Identity card formula factor = [17]int{7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2} @@ -258,21 +276,45 @@ func IsPort(str string) bool { // IsUrl check if the string is url. // Play: https://go.dev/play/p/pbJGa7F98Ka func IsUrl(str string) bool { - if str == "" || len(str) >= 2083 || len(str) <= 3 || strings.HasPrefix(str, ".") { - return false - } - u, err := url.Parse(str) - if err != nil { - return false - } - if strings.HasPrefix(u.Host, ".") { - return false - } - if u.Host == "" && (u.Path != "" && !strings.Contains(u.Path, ".")) { + if str == "" { return false } - return urlMatcher.MatchString(str) + u, err := url.Parse(str) + if err != nil || u.Scheme == "" || u.Host == "" { + return false + } + + allowedSchemes := map[string]struct{}{ + "http": {}, + "https": {}, + "ftp": {}, + "ws": {}, + "wss": {}, + "file": {}, + "mailto": {}, + "data": {}, + } + + if _, ok := allowedSchemes[u.Scheme]; !ok { + return false + } + + if u.Scheme == "file" || u.Scheme == "mailto" || u.Scheme == "data" { + return true + } + + host := u.Hostname() + if !strings.Contains(host, ".") || strings.HasSuffix(host, ".") { + return false + } + + // domainRegexp := regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9\-\.]*[a-zA-Z0-9]$`) + if !dnsMatcher.MatchString(host) { + return false + } + + return true } // IsDns check if the string is dns. @@ -286,8 +328,6 @@ func IsDns(dns string) bool { func IsEmail(email string) bool { _, err := mail.ParseAddress(email) return err == nil - - // return emailMatcher.MatchString(email) } // IsChineseMobile check if the string is chinese mobile number. @@ -460,12 +500,13 @@ func IsZeroValue(value any) bool { func IsGBK(data []byte) bool { i := 0 for i < len(data) { - if data[i] <= 0xff { + if data[i] < 0x81 { i++ continue } else { if data[i] >= 0x81 && data[i] <= 0xfe && + i+1 < len(data) && data[i+1] >= 0x40 && data[i+1] <= 0xfe && data[i+1] != 0xf7 { @@ -561,12 +602,71 @@ func IsAmericanExpress(v string) bool { // IsUnionPay check if a give string is a valid union pay nubmer or not. // Play: https://go.dev/play/p/CUHPEwEITDf -func IsUnionPay(v string) bool { - return unionPay.MatchString(v) +func IsUnionPay(cardNo string) bool { + if len(cardNo) < 16 || len(cardNo) > 19 { + return false + } + + matched, _ := regexp.MatchString(`^\d+$`, cardNo) + if !matched { + return false + } + + if len(cardNo) < 3 { + return false + } + + prefix := cardNo[:3] + prefixNum, err := strconv.Atoi(prefix) + if err != nil { + return false + } + + if prefixNum < 620 || prefixNum > 625 { + return false + } + + return true } // IsChinaUnionPay check if a give string is a valid china union pay nubmer or not. // Play: https://go.dev/play/p/yafpdxLiymu -func IsChinaUnionPay(v string) bool { - return chinaUnionPay.MatchString(v) +func IsChinaUnionPay(cardNo string) bool { + return chinaUnionPayMatcher.MatchString(cardNo) +} + +// luhnCheck checks if the credit card number is valid using the Luhn algorithm. +func luhnCheck(card string) bool { + var sum int + alt := false + for i := len(card) - 1; i >= 0; i-- { + n := int(card[i] - '0') + if alt { + n *= 2 + if n > 9 { + n -= 9 + } + } + sum += n + alt = !alt + } + return sum%10 == 0 +} + +// IsPassport checks if the passport number is valid for a given country. +// country is a two-letter country code (ISO 3166-1 alpha-2). +// Play: todo +func IsPassport(passport, country string) bool { + if matcher, ok := passportMatcher[country]; ok { + return matcher.MatchString(passport) + } + + return false +} + +// IsChineseHMPassport checks if the string is a valid Chinese Hong Kong and Macau Travel Permit number. +// Chinese Hong Kong and Macau Travel Permit format: C or M + 8 digits (e.g., C12345678, M12345678). +// Play: https://go.dev/play/p/TODO +func IsChineseHMPassport(hmPassport string) bool { + return chineseHMPassportMatcher.MatchString(hmPassport) } diff --git a/validator/validator_example_test.go b/validator/validator_example_test.go index 088cb86..65622f9 100644 --- a/validator/validator_example_test.go +++ b/validator/validator_example_test.go @@ -214,7 +214,7 @@ func ExampleIsUrl() { fmt.Println(result3) // Output: - // true + // false // true // false } @@ -683,3 +683,42 @@ func ExampleIsAlphaNumeric() { // true // false } + +func ExampleIsPassport() { + result1 := IsPassport("P123456789", "CN") + result2 := IsPassport("123456789", "US") + result3 := IsPassport("AB1234567", "RU") + result4 := IsPassport("123456789", "CN") + + fmt.Println(result1) + fmt.Println(result2) + fmt.Println(result3) + fmt.Println(result4) + + // Output: + // true + // true + // true + // false +} + +func ExampleIsChineseHMPassport() { + result1 := IsChineseHMPassport("C12345678") + result2 := IsChineseHMPassport("C00000000") + result3 := IsChineseHMPassport("M12345678") + result4 := IsChineseHMPassport("c12345678") + result5 := IsChineseHMPassport("C1234567") + + fmt.Println(result1) + fmt.Println(result2) + fmt.Println(result3) + fmt.Println(result4) + fmt.Println(result5) + + // Output: + // true + // true + // true + // false + // false +} diff --git a/validator/validator_test.go b/validator/validator_test.go index 0842097..842753d 100644 --- a/validator/validator_test.go +++ b/validator/validator_test.go @@ -437,10 +437,32 @@ func TestIsUrl(t *testing.T) { assert := internal.NewAssert(t, "TestIsUrl") - assert.Equal(true, IsUrl("http://abc.com")) - assert.Equal(true, IsUrl("abc.com")) - assert.Equal(true, IsUrl("a.b.com")) - assert.Equal(false, IsUrl("abc")) + tests := []struct { + input string + expected bool + }{ + {"http://abc.com", true}, + {"https://abc.com", true}, + {"ftp://abc.com", true}, + {"http://abc.com/path?query=123", true}, + {"https://abc.com/path/to/resource", true}, + {"ws://abc.com", true}, + {"wss://abc.com", true}, + {"mailto://abc.com", true}, + {"file://path/to/file", true}, + {"data://text/plain;base64,SGVsbG8sIFdvcmxkIQ==", true}, + {"http://abc.com/path/to/resource?query=123#fragment", true}, + + {"abc", false}, + {"http://", false}, + {"http://abc", false}, + {"http://abc:8080", false}, + {"http://abc:99999999", false}, + } + + for _, tt := range tests { + assert.Equal(tt.expected, IsUrl(tt.input)) + } } func TestIsDns(t *testing.T) { @@ -477,12 +499,24 @@ func TestIsEmail(t *testing.T) { func TestContainChinese(t *testing.T) { t.Parallel() - assert := internal.NewAssert(t, "TestContainChinese") - assert.Equal(true, ContainChinese("你好")) - assert.Equal(true, ContainChinese("你好hello")) - assert.Equal(false, ContainChinese("hello")) + tests := []struct { + input string + expected bool + }{ + {"你好", true}, + {"hello", false}, + {"你好hello", true}, + {"hello你好", true}, + {"", false}, + {"123", false}, + {"!@#$%^&*()", false}, + } + + for _, tt := range tests { + assert.Equal(tt.expected, ContainChinese(tt.input)) + } } func TestIsChineseMobile(t *testing.T) { @@ -490,8 +524,20 @@ func TestIsChineseMobile(t *testing.T) { assert := internal.NewAssert(t, "TestIsChineseMobile") - assert.Equal(true, IsChineseMobile("13263527980")) - assert.Equal(false, IsChineseMobile("434324324")) + tests := []struct { + input string + expected bool + }{ + {"13263527980", true}, + {"1326352798", false}, + {"132635279801", false}, + {"1326352798a", false}, + {"1326352798@", false}, + } + + for _, tt := range tests { + assert.Equal(tt.expected, IsChineseMobile(tt.input)) + } } func TestIsChinesePhone(t *testing.T) { @@ -887,7 +933,7 @@ func TestIsUnionPay(t *testing.T) { t.Parallel() assert := internal.NewAssert(t, "TestIsUnionPay") - assert.Equal(true, IsUnionPay("6221263430109903")) + assert.Equal(true, IsUnionPay("6228480402564890")) assert.Equal(false, IsUnionPay("3782822463100007")) } @@ -895,8 +941,25 @@ func TestIsChinaUnionPay(t *testing.T) { t.Parallel() assert := internal.NewAssert(t, "TestIsChinaUnionPay") - assert.Equal(true, IsChinaUnionPay("6250941006528599")) - assert.Equal(false, IsChinaUnionPay("3782822463100007")) + tests := []struct { + cardNumber string + expected bool + }{ + {"6228480420000000000", true}, + {"6214830000000000", true}, + {"6230580000000000000", true}, + {"6259640000000000000", true}, + {"6260000000000000000", true}, + {"6288888888888888", true}, + + // 非银联前缀 + {"4123456789012345", false}, + {"3528000000000000", false}, + } + + for _, tt := range tests { + assert.Equal(tt.expected, IsChinaUnionPay(tt.cardNumber)) + } } func TestIsAlphaNumeric(t *testing.T) { @@ -924,3 +987,72 @@ func TestIsAlphaNumeric(t *testing.T) { assert.Equal(tt.expected, IsAlphaNumeric(tt.input)) } } + +func TestIsPassport(t *testing.T) { + t.Parallel() + + assert := internal.NewAssert(t, "TestIsPassport") + + tests := []struct { + passport string + countryCode string + expected bool + }{ + {"P123456789", "CN", true}, + {"123456789", "US", true}, + {"A12345678", "GB", true}, + {"AB1234567", "FR", true}, + {"12345678", "JP", true}, + {"M12345678", "HK", true}, + {"A12345678", "MO", true}, + {"A1234567", "IN", true}, + {"12345678", "IT", true}, + {"A12345678", "AU", true}, + {"123456789", "BR", true}, + {"AB1234567", "RU", true}, + {"123456789", "CN", false}, + } + + for _, tt := range tests { + assert.Equal(tt.expected, IsPassport(tt.passport, tt.countryCode)) + } +} + +func TestIsChineseHMPassport(t *testing.T) { + t.Parallel() + + assert := internal.NewAssert(t, "TestIsChineseHMPassport") + + tests := []struct { + input string + expected bool + }{ + {"C12345678", true}, + {"C00000000", true}, + {"C99999999", true}, + {"M12345678", true}, // M prefix + {"M00000000", true}, // M prefix + {"M99999999", true}, // M prefix + {"c12345678", false}, // lowercase c + {"m12345678", false}, // lowercase m + {"C1234567", false}, // 7 digits + {"M1234567", false}, // 7 digits with M + {"C123456789", false}, // 9 digits + {"M123456789", false}, // 9 digits with M + {"C1234567a", false}, // contains letter + {"M1234567a", false}, // contains letter with M + {"D12345678", false}, // starts with D + {"12345678", false}, // no prefix + {"CC12345678", false}, // double C + {"MM12345678", false}, // double M + {"C 12345678", false}, // contains space + {"M 12345678", false}, // contains space with M + {"C12345-678", false}, // contains dash + {"M12345-678", false}, // contains dash with M + {"", false}, + } + + for _, tt := range tests { + assert.Equal(tt.expected, IsChineseHMPassport(tt.input)) + } +}