diff --git a/README.md b/README.md index f6b39c6..7b88cec 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ It also provides convenient extensions to go-openapi users. - mac (e.g "01:02:03:04:05:06") - rgbcolor (e.g. "rgb(100,100,100)") - ssn - - uuid, uuid3, uuid4, uuid5 + - uuid, uuid3, uuid4, uuid5, uuid7 - cidr (e.g. "192.0.2.1/24", "2001:db8:a0b:12f0::1/32") - ulid (e.g. "00000PP9HGSBSSDZ1JTEXBJ0PW", [spec](https://github.com/ulid/spec)) @@ -81,7 +81,8 @@ List of defined types: - SSN - URI - UUID -- UUID3 -- UUID4 -- UUID5 +- [UUID3](https://www.rfc-editor.org/rfc/rfc9562.html#name-uuid-version-3) +- [UUID4](https://www.rfc-editor.org/rfc/rfc9562.html#name-uuid-version-4) +- [UUID5](https://www.rfc-editor.org/rfc/rfc9562.html#name-uuid-version-5) +- [UUID7](https://www.rfc-editor.org/rfc/rfc9562.html#name-uuid-version-7) - [ULID](https://github.com/ulid/spec) diff --git a/conv/default.go b/conv/default.go index 078a1e5..0312612 100644 --- a/conv/default.go +++ b/conv/default.go @@ -184,6 +184,21 @@ func UUID5Value(v *strfmt.UUID5) strfmt.UUID5 { return *v } +// UUID7 returns a pointer to of the UUID7 value passed in. +func UUID7(v strfmt.UUID7) *strfmt.UUID7 { + return &v +} + +// UUID7Value returns the value of the UUID7 pointer passed in or +// the default value if the pointer is nil. +func UUID7Value(v *strfmt.UUID7) strfmt.UUID7 { + if v == nil { + return strfmt.UUID7("") + } + + return *v +} + // ISBN returns a pointer to of the ISBN value passed in. func ISBN(v strfmt.ISBN) *strfmt.ISBN { return &v diff --git a/conv/default_test.go b/conv/default_test.go index 8b779c5..02fa1a0 100644 --- a/conv/default_test.go +++ b/conv/default_test.go @@ -80,6 +80,12 @@ func TestUUID5Value(t *testing.T) { assert.Equal(t, value, UUID5Value(&value)) } +func TestUUID7Value(t *testing.T) { + assert.Equal(t, strfmt.UUID7(""), UUID7Value(nil)) + value := strfmt.UUID7("foo") + assert.Equal(t, value, UUID7Value(&value)) +} + func TestISBNValue(t *testing.T) { assert.Equal(t, strfmt.ISBN(""), ISBNValue(nil)) value := strfmt.ISBN("foo") diff --git a/default.go b/default.go index 56f76ad..04dc364 100644 --- a/default.go +++ b/default.go @@ -346,6 +346,7 @@ const ( uuidV3 = 3 uuidV4 = 4 uuidV5 = 5 + uuidV7 = 7 ) // IsUUID3 returns true is the string matches a UUID v3, upper case is allowed @@ -366,6 +367,12 @@ func IsUUID5(str string) bool { return err == nil && id.Version() == uuid.Version(uuidV5) } +// IsUUID7 returns true is the string matches a UUID v7, upper case is allowed +func IsUUID7(str string) bool { + id, err := uuid.Parse(str) + return err == nil && id.Version() == uuid.Version(uuidV7) +} + // IsEmail validates an email address. func IsEmail(str string) bool { addr, e := mail.ParseAddress(str) @@ -394,6 +401,7 @@ func init() { // - uuid3 // - uuid4 // - uuid5 + // - uuid7 u := URI("") Default.Add("uri", &u, isRequestURI) @@ -427,6 +435,9 @@ func init() { uid5 := UUID5("") Default.Add("uuid5", &uid5, IsUUID5) + uid7 := UUID7("") + Default.Add("uuid7", &uid7, IsUUID7) + isbn := ISBN("") Default.Add("isbn", &isbn, func(str string) bool { return isISBN10(str) || isISBN13(str) }) @@ -1320,6 +1331,78 @@ func (u *UUID5) DeepCopy() *UUID5 { return out } +// UUID7 represents a uuid7 string format +// +// swagger:strfmt uuid7 +type UUID7 string + +// MarshalText turns this instance into text +func (u UUID7) MarshalText() ([]byte, error) { + return []byte(string(u)), nil +} + +// UnmarshalText hydrates this instance from text +func (u *UUID7) UnmarshalText(data []byte) error { // validation is performed later on + *u = UUID7(string(data)) + return nil +} + +// Scan read a value from a database driver +func (u *UUID7) Scan(raw any) error { + switch v := raw.(type) { + case []byte: + *u = UUID7(string(v)) + case string: + *u = UUID7(v) + default: + return fmt.Errorf("cannot sql.Scan() strfmt.UUID7 from: %#v: %w", v, ErrFormat) + } + + return nil +} + +// Value converts a value to a database driver value +func (u UUID7) Value() (driver.Value, error) { + return driver.Value(string(u)), nil +} + +func (u UUID7) String() string { + return string(u) +} + +// MarshalJSON returns the UUID as JSON +func (u UUID7) MarshalJSON() ([]byte, error) { + return json.Marshal(string(u)) +} + +// UnmarshalJSON sets the UUID from JSON +func (u *UUID7) UnmarshalJSON(data []byte) error { + if string(data) == jsonNull { + return nil + } + var ustr string + if err := json.Unmarshal(data, &ustr); err != nil { + return err + } + *u = UUID7(ustr) + return nil +} + +// DeepCopyInto copies the receiver and writes its value into out. +func (u *UUID7) DeepCopyInto(out *UUID7) { + *out = *u +} + +// DeepCopy copies the receiver into a new UUID7. +func (u *UUID7) DeepCopy() *UUID7 { + if u == nil { + return nil + } + out := new(UUID7) + u.DeepCopyInto(out) + return out +} + // ISBN represents an isbn string format // // swagger:strfmt isbn diff --git a/default_test.go b/default_test.go index 344a365..b39c39a 100644 --- a/default_test.go +++ b/default_test.go @@ -372,6 +372,48 @@ func TestFormatUUID5(t *testing.T) { assert.Equal(t, UUID5(""), uuidZero) } +func validUUID7s() []string { + other7 := uuid.Must(uuid.NewV7()) + + return []string{ + other7.String(), + strings.ReplaceAll(other7.String(), "-", ""), + } +} + +func invalidUUID7s() []string { + other3 := uuid.NewMD5(uuid.NameSpaceURL, []byte("somewhere.com")) + other4 := uuid.Must(uuid.NewRandom()) + other5 := uuid.NewSHA1(uuid.NameSpaceURL, []byte("somewhereelse.com")) + + return []string{ + "not-a-uuid", + other3.String(), + other4.String(), + strings.ReplaceAll(other3.String(), "-", ""), + strings.ReplaceAll(other4.String(), "-", ""), + strings.Replace(other3.String(), "-", "", 2), + strings.Replace(other4.String(), "-", "", 2), + strings.Replace(other5.String(), "-", "", 2), + } +} + +func TestFormatUUID7(t *testing.T) { + first7 := uuid.Must(uuid.NewV7()) + str := first7.String() + uuid7 := UUID7(str) + testStringFormat(t, &uuid7, "uuid7", str, + validUUID7s(), + invalidUUID7s(), + ) + + // special case for zero UUID + var uuidZero UUID7 + err := uuidZero.UnmarshalJSON([]byte(jsonNull)) + require.NoError(t, err) + assert.Equal(t, UUID7(""), uuidZero) +} + func validUUIDs() []string { other3 := uuid.NewSHA1(uuid.NameSpaceURL, []byte("somewhereelse.com")) other4 := uuid.Must(uuid.NewRandom()) @@ -820,6 +862,23 @@ func TestDeepCopyUUID5(t *testing.T) { assert.Nil(t, out3) } +func TestDeepCopyUUID7(t *testing.T) { + first7 := uuid.Must(uuid.NewV7()) + uuid7 := UUID7(first7.String()) + in := &uuid7 + + out := new(UUID7) + in.DeepCopyInto(out) + assert.Equal(t, in, out) + + out2 := in.DeepCopy() + assert.Equal(t, in, out2) + + var inNil *UUID7 + out3 := inNil.DeepCopy() + assert.Nil(t, out3) +} + func TestDeepCopyISBN(t *testing.T) { isbn := ISBN("0321751043") in := &isbn diff --git a/format.go b/format.go index c79063a..3c3f654 100644 --- a/format.go +++ b/format.go @@ -118,6 +118,8 @@ func (f *defaultFormats) MapStructureHookFunc() mapstructure.DecodeHookFunc { return UUID4(data), nil case "uuid5": return UUID5(data), nil + case "uuid7": + return UUID7(data), nil case "hostname": return Hostname(data), nil case "ipv4": diff --git a/format_test.go b/format_test.go index 6693cc2..7611e14 100644 --- a/format_test.go +++ b/format_test.go @@ -138,6 +138,7 @@ type testStruct struct { UUID3 UUID3 `json:"uuid3,omitempty"` UUID4 UUID4 `json:"uuid4,omitempty"` UUID5 UUID5 `json:"uuid5,omitempty"` + UUID7 UUID7 `json:"uuid7,omitempty"` Hn Hostname `json:"hn,omitempty"` Ipv4 IPv4 `json:"ipv4,omitempty"` Ipv6 IPv6 `json:"ipv6,omitempty"` @@ -167,6 +168,7 @@ func TestDecodeHook(t *testing.T) { "uuid3": "bcd02e22-68f0-3046-a512-327cca9def8f", "uuid4": "025b0d74-00a2-4048-bf57-227c5111bb34", "uuid5": "886313e1-3b8a-5372-9b90-0c9aee199e5d", + "uuid7": "019a15e6-cd5e-7204-b11b-12075f4c8a25", "hn": "somewhere.com", "ipv4": "192.168.254.1", "ipv6": "::1", @@ -199,6 +201,7 @@ func TestDecodeHook(t *testing.T) { UUID3: UUID3("bcd02e22-68f0-3046-a512-327cca9def8f"), UUID4: UUID4("025b0d74-00a2-4048-bf57-227c5111bb34"), UUID5: UUID5("886313e1-3b8a-5372-9b90-0c9aee199e5d"), + UUID7: UUID7("019a15e6-cd5e-7204-b11b-12075f4c8a25"), Hn: Hostname("somewhere.com"), Ipv4: IPv4("192.168.254.1"), Ipv6: IPv6("::1"), diff --git a/mongo.go b/mongo.go index b616680..0d94bf3 100644 --- a/mongo.go +++ b/mongo.go @@ -47,6 +47,8 @@ var ( _ bson.Unmarshaler = (*UUID4)(nil) _ bson.Marshaler = UUID5("") _ bson.Unmarshaler = (*UUID5)(nil) + _ bson.Marshaler = UUID7("") + _ bson.Unmarshaler = (*UUID7)(nil) _ bson.Marshaler = ISBN("") _ bson.Unmarshaler = (*ISBN)(nil) _ bson.Marshaler = ISBN10("") @@ -452,6 +454,25 @@ func (u *UUID5) UnmarshalBSON(data []byte) error { return fmt.Errorf("couldn't unmarshal bson bytes as UUID5: %w", ErrFormat) } +// MarshalBSON document from this value +func (u UUID7) MarshalBSON() ([]byte, error) { + return bson.Marshal(bson.M{"data": u.String()}) +} + +// UnmarshalBSON document into this value +func (u *UUID7) UnmarshalBSON(data []byte) error { + var m bson.M + if err := bson.Unmarshal(data, &m); err != nil { + return err + } + + if ud, ok := m["data"].(string); ok { + *u = UUID7(ud) + return nil + } + return fmt.Errorf("couldn't unmarshal bson bytes as UUID7: %w", ErrFormat) +} + // MarshalBSON document from this value func (u ISBN) MarshalBSON() ([]byte, error) { return bson.Marshal(bson.M{"data": u.String()}) diff --git a/mongo_test.go b/mongo_test.go index 7dc02ee..18970c7 100644 --- a/mongo_test.go +++ b/mongo_test.go @@ -203,6 +203,16 @@ func TestFormatBSON(t *testing.T) { ) }) + t.Run("with UUID7", func(t *testing.T) { + first7 := uuid.Must(uuid.NewV7()) + str := first7.String() + uuid7 := UUID7(str) + testBSONStringFormat(t, &uuid7, "uuid7", str, + validUUID7s(), + invalidUUID7s(), + ) + }) + t.Run("with UUID", func(t *testing.T) { first5 := uuid.NewSHA1(uuid.NameSpaceURL, []byte("somewhere.com")) other5 := uuid.NewSHA1(uuid.NameSpaceURL, []byte("somewhereelse.com"))