├── .github ├── FUNDING.yaml └── workflows │ └── test.yaml ├── LICENSE ├── README.md ├── go.mod ├── sandid.go └── sandid_test.go /.github/FUNDING.yaml: -------------------------------------------------------------------------------- 1 | github: aofei 2 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | branches: 5 | - "**" 6 | pull_request: 7 | branches: 8 | - "**" 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | go: 15 | - 1.13.x 16 | - 1.14.x 17 | - 1.15.x 18 | - 1.16.x 19 | - 1.17.x 20 | - 1.18.x 21 | - 1.19.x 22 | - 1.20.x 23 | - 1.21.x 24 | - 1.22.x 25 | - 1.23.x 26 | - 1.24.x 27 | steps: 28 | - name: Check out code 29 | uses: actions/checkout@v4 30 | - name: Set up Go 31 | uses: actions/setup-go@v5 32 | with: 33 | go-version: ${{matrix.go}} 34 | - name: Download Go modules 35 | run: go mod download 36 | - name: Test Go code 37 | run: go test -v -race -covermode atomic -coverprofile coverage.out ./... 38 | - name: Upload code coverage 39 | uses: codecov/codecov-action@v5 40 | with: 41 | token: ${{secrets.CODECOV_TOKEN}} 42 | disable_search: true 43 | files: coverage.out 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Aofei Sheng 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a 6 | copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included 14 | in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SandID 2 | 3 | [![Test](https://github.com/aofei/sandid/actions/workflows/test.yaml/badge.svg)](https://github.com/aofei/sandid/actions/workflows/test.yaml) 4 | [![codecov](https://codecov.io/gh/aofei/sandid/branch/master/graph/badge.svg)](https://codecov.io/gh/aofei/sandid) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/aofei/sandid)](https://goreportcard.com/report/github.com/aofei/sandid) 6 | [![Go Reference](https://pkg.go.dev/badge/github.com/aofei/sandid.svg)](https://pkg.go.dev/github.com/aofei/sandid) 7 | 8 | Every grain of sand on Earth has its own ID. 9 | 10 | The algorithm used to generate the [`sandid.SandID`](https://pkg.go.dev/github.com/aofei/sandid#SandID) mainly come 11 | from the [UUID](https://tools.ietf.org/html/rfc4122) version 1. Some 12 | [adjustments](https://www.percona.com/blog/2014/12/19/store-uuid-optimized-way/) were made to enhance the efficiency of 13 | database insertion. 14 | 15 | ## Features 16 | 17 | - Extremely easy to use 18 | - Fixed length 19 | - 16 bytes 20 | - 22 characters 21 | - 128-bit 22 | - Huge capacity 23 | - Up to 2e128 24 | - URL safe 25 | - `^[A-Za-z0-9-_]{22}$` 26 | - Encoding friendly 27 | - Implemented [`encoding.TextMarshaler`](https://pkg.go.dev/encoding#TextMarshaler) and [`encoding.TextUnmarshaler`](https://pkg.go.dev/encoding#TextUnmarshaler) 28 | - Implemented [`encoding.BinaryMarshaler`](https://pkg.go.dev/encoding#BinaryMarshaler) and [`encoding.BinaryUnmarshaler`](https://pkg.go.dev/encoding#BinaryUnmarshaler) 29 | - Implemented [`json.Marshaler`](https://pkg.go.dev/encoding/json#Marshaler) and [`json.Unmarshaler`](https://pkg.go.dev/encoding/json#Unmarshaler) 30 | - SQL friendly 31 | - [`sandid.NullSandID`](https://pkg.go.dev/github.com/aofei/sandid#NullSandID) support 32 | - Implemented [`sql.Scanner`](https://pkg.go.dev/database/sql#Scanner) and [`driver.Valuer`](https://pkg.go.dev/database/sql/driver#Valuer) 33 | - Zero third-party dependencies 34 | 35 | ## Installation 36 | 37 | To use this project programmatically, `go get` it: 38 | 39 | ```bash 40 | go get github.com/aofei/sandid 41 | ``` 42 | 43 | ## Community 44 | 45 | If you have any questions or ideas about this project, feel free to discuss them 46 | [here](https://github.com/aofei/sandid/discussions). 47 | 48 | ## Contributing 49 | 50 | If you would like to contribute to this project, please submit issues [here](https://github.com/aofei/sandid/issues) 51 | or pull requests [here](https://github.com/aofei/sandid/pulls). 52 | 53 | When submitting a pull request, please make sure its commit messages adhere to 54 | [Conventional Commits 1.0.0](https://www.conventionalcommits.org/en/v1.0.0/). 55 | 56 | ## License 57 | 58 | This project is licensed under the [MIT License](LICENSE). 59 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/aofei/sandid 2 | 3 | go 1.13 4 | -------------------------------------------------------------------------------- /sandid.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package sandid implements a unique ID generation algorithm to ensure that every 3 | grain of sand on Earth has its own ID. 4 | */ 5 | package sandid 6 | 7 | import ( 8 | "bytes" 9 | "crypto/rand" 10 | "database/sql/driver" 11 | "encoding/binary" 12 | "encoding/json" 13 | "errors" 14 | "net" 15 | "strconv" 16 | "sync" 17 | "time" 18 | ) 19 | 20 | // SandID is an ID of sand. 21 | type SandID [16]byte 22 | 23 | var ( 24 | zeroSandID SandID 25 | 26 | storageMutex sync.Mutex 27 | luckyNibble byte 28 | clockSequence uint16 29 | hardwareAddress [6]byte 30 | lastTime uint64 31 | 32 | encoding = [64]byte{ 33 | 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 34 | 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 35 | 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 36 | 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 37 | '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_', 38 | } 39 | decoding [256]byte 40 | ) 41 | 42 | func init() { 43 | b := make([]byte, 9) 44 | rand.Read(b) 45 | 46 | luckyNibble = b[0] 47 | clockSequence = binary.BigEndian.Uint16(b[1:3]) 48 | 49 | copy(hardwareAddress[:], b[3:]) 50 | hardwareAddress[0] |= 0x01 51 | netInterfaces, _ := net.Interfaces() 52 | for _, ni := range netInterfaces { 53 | if len(ni.HardwareAddr) >= 6 { 54 | copy(hardwareAddress[:], ni.HardwareAddr) 55 | break 56 | } 57 | } 58 | 59 | for i := 0; i < len(decoding); i++ { 60 | decoding[i] = 0xff 61 | } 62 | for i := 0; i < len(encoding); i++ { 63 | decoding[encoding[i]] = byte(i) 64 | } 65 | } 66 | 67 | // New creates a new instance of [SandID]. 68 | func New() SandID { 69 | storageMutex.Lock() 70 | defer storageMutex.Unlock() 71 | 72 | timeNow := 122192928000000000 + uint64(time.Now().UnixNano()/100) 73 | if timeNow <= lastTime { 74 | clockSequence++ 75 | } 76 | lastTime = timeNow 77 | 78 | var sID SandID 79 | binary.BigEndian.PutUint16(sID[0:], uint16(timeNow>>44)) 80 | binary.BigEndian.PutUint16(sID[2:], uint16(timeNow>>28)) 81 | binary.BigEndian.PutUint32(sID[4:], uint32(timeNow<<4)) 82 | binary.BigEndian.PutUint16(sID[8:], clockSequence) 83 | copy(sID[10:], hardwareAddress[:]) 84 | sID[7] = sID[7]&0xf0 | luckyNibble&0x0f 85 | return sID 86 | } 87 | 88 | // Parse parses the s into a new instance of [SandID]. 89 | func Parse(s string) (SandID, error) { 90 | var sID SandID 91 | return sID, sID.UnmarshalText([]byte(s)) 92 | } 93 | 94 | // MustParse is like [Parse], but panics if the s cannot be parsed. 95 | func MustParse(s string) SandID { 96 | sID, err := Parse(s) 97 | if err != nil { 98 | panic(err) 99 | } 100 | return sID 101 | } 102 | 103 | // IsZero reports whether the sID is zero. 104 | func (sID SandID) IsZero() bool { 105 | return Equal(sID, zeroSandID) 106 | } 107 | 108 | // String returns the serialization of the sID. 109 | func (sID SandID) String() string { 110 | b, _ := sID.MarshalText() 111 | return string(b) 112 | } 113 | 114 | // Scan implements [database/sql.Scanner]. 115 | // 116 | // The value must be a string or []byte. 117 | func (sID *SandID) Scan(value interface{}) error { 118 | switch value := value.(type) { 119 | case string: 120 | return sID.UnmarshalText([]byte(value)) 121 | case []byte: 122 | return sID.UnmarshalBinary(value) 123 | } 124 | return errors.New("sandid: invalid type value") 125 | } 126 | 127 | // Value implements [driver.Valuer]. 128 | func (sID SandID) Value() (driver.Value, error) { 129 | return sID.MarshalBinary() 130 | } 131 | 132 | // MarshalText implements [encoding.TextMarshaler]. 133 | func (sID SandID) MarshalText() ([]byte, error) { 134 | d := make([]byte, 22) 135 | 136 | var si, di int 137 | for ; si < 15; si, di = si+3, di+4 { // si < (len(sID) / 3) * 3 138 | v := uint(sID[si])<<16 | uint(sID[si+1])<<8 | uint(sID[si+2]) 139 | d[di] = encoding[v>>18&0x3f] 140 | d[di+1] = encoding[v>>12&0x3f] 141 | d[di+2] = encoding[v>>6&0x3f] 142 | d[di+3] = encoding[v&0x3f] 143 | } 144 | 145 | v := uint(sID[si]) << 16 146 | d[di] = encoding[v>>18&0x3f] 147 | d[di+1] = encoding[v>>12&0x3f] 148 | 149 | return d, nil 150 | } 151 | 152 | // UnmarshalText implements [encoding.TextUnmarshaler]. 153 | func (sID *SandID) UnmarshalText(text []byte) error { 154 | if len(text) != 22 { 155 | return errors.New("sandid: invalid length string") 156 | } 157 | 158 | var si, n int 159 | if strconv.IntSize >= 64 { 160 | for ; si <= 22-8 && n <= 16-8; si, n = si+8, n+6 { 161 | n1 := decoding[text[si]] 162 | n2 := decoding[text[si+1]] 163 | n3 := decoding[text[si+2]] 164 | n4 := decoding[text[si+3]] 165 | n5 := decoding[text[si+4]] 166 | n6 := decoding[text[si+5]] 167 | n7 := decoding[text[si+6]] 168 | n8 := decoding[text[si+7]] 169 | if n1|n2|n3|n4|n5|n6|n7|n8 == 0xff { 170 | return errors.New("sandid: invalid string") 171 | } 172 | binary.BigEndian.PutUint64( 173 | sID[n:], 174 | uint64(n1)<<58| 175 | uint64(n2)<<52| 176 | uint64(n3)<<46| 177 | uint64(n4)<<40| 178 | uint64(n5)<<34| 179 | uint64(n6)<<28| 180 | uint64(n7)<<22| 181 | uint64(n8)<<16, 182 | ) 183 | } 184 | } 185 | for ; si <= 22-4 && n <= 16-4; si, n = si+4, n+3 { 186 | n1 := decoding[text[si]] 187 | n2 := decoding[text[si+1]] 188 | n3 := decoding[text[si+2]] 189 | n4 := decoding[text[si+3]] 190 | if n1|n2|n3|n4 == 0xff { 191 | return errors.New("sandid: invalid string") 192 | } 193 | binary.BigEndian.PutUint32( 194 | sID[n:], 195 | uint32(n1)<<26| 196 | uint32(n2)<<20| 197 | uint32(n3)<<14| 198 | uint32(n4)<<8, 199 | ) 200 | } 201 | 202 | var b [4]byte 203 | for i := 0; i < 4 && si < 22; i, si = i+1, si+1 { 204 | if b[i] = decoding[text[si]]; b[i] == 0xff { 205 | return errors.New("sandid: invalid string") 206 | } 207 | } 208 | 209 | v := uint(b[0])<<18 | uint(b[1])<<12 | uint(b[2])<<6 | uint(b[3]) 210 | sID[n] = byte(v >> 16) 211 | 212 | return nil 213 | } 214 | 215 | // MarshalBinary implements [encoding.BinaryMarshaler]. 216 | func (sID SandID) MarshalBinary() ([]byte, error) { 217 | return sID[:], nil 218 | } 219 | 220 | // UnmarshalBinary implements [encoding.BinaryUnmarshaler]. 221 | func (sID *SandID) UnmarshalBinary(data []byte) error { 222 | if len(data) != 16 { 223 | return errors.New("sandid: invalid length bytes") 224 | } 225 | copy(sID[:], data) 226 | return nil 227 | } 228 | 229 | // MarshalJSON implements [json.Marshaler]. 230 | func (sID SandID) MarshalJSON() ([]byte, error) { 231 | return json.Marshal(sID.String()) 232 | } 233 | 234 | // UnmarshalJSON implements [json.Unmarshaler]. 235 | func (sID *SandID) UnmarshalJSON(data []byte) error { 236 | var s string 237 | if err := json.Unmarshal(data, &s); err != nil { 238 | return err 239 | } 240 | return sID.UnmarshalText([]byte(s)) 241 | } 242 | 243 | // Equal reports whether the a and b are equal. 244 | func Equal(a, b SandID) bool { 245 | return Compare(a, b) == 0 246 | } 247 | 248 | // Compare returns an integer comparing the a and b lexicographically. The 249 | // result will be 0 if a == b, -1 if a < b, and +1 if a > b. 250 | func Compare(a, b SandID) int { 251 | return bytes.Compare(a[:], b[:]) 252 | } 253 | 254 | // NullSandID represents an instance of [SandID] that may be null. It 255 | // implements [database/sql.Scanner] so it can be used as a scan destination. 256 | type NullSandID struct { 257 | SandID SandID 258 | Valid bool 259 | } 260 | 261 | // Scan implements [database/sql.Scanner]. 262 | func (nsID *NullSandID) Scan(value interface{}) error { 263 | if value == nil { 264 | nsID.SandID, nsID.Valid = SandID{}, false 265 | return nil 266 | } 267 | nsID.Valid = true 268 | return nsID.SandID.Scan(value) 269 | } 270 | 271 | // Value implements [driver.Valuer]. 272 | func (nsID NullSandID) Value() (driver.Value, error) { 273 | if !nsID.Valid { 274 | return nil, nil 275 | } 276 | return nsID.SandID.Value() 277 | } 278 | -------------------------------------------------------------------------------- /sandid_test.go: -------------------------------------------------------------------------------- 1 | package sandid 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "reflect" 7 | "strconv" 8 | "testing" 9 | ) 10 | 11 | func TestNew(t *testing.T) { 12 | for i := 0; i < 1_000_000; i++ { 13 | sID := New() 14 | if bytes.Equal(sID[:], zeroSandID[:]) { 15 | t.Error("want false") 16 | } 17 | } 18 | 19 | lastTime = 1<<64 - 1 20 | 21 | sID := New() 22 | if bytes.Equal(sID[:], zeroSandID[:]) { 23 | t.Error("want false") 24 | } 25 | } 26 | 27 | func TestParse(t *testing.T) { 28 | for _, tt := range []struct { 29 | name string 30 | s string 31 | want SandID 32 | wantErr bool 33 | }{ 34 | {"Zero", "AAAAAAAAAAAAAAAAAAAAAA", zeroSandID, false}, 35 | {"NonZero", "AAECAwQFBgcICQoLDA0ODw", SandID{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, false}, 36 | {"Invalid", "AAAAAAAAAAAAAAAAAAAAA=", zeroSandID, true}, 37 | } { 38 | t.Run(tt.name, func(t *testing.T) { 39 | sID, err := Parse(tt.s) 40 | if tt.wantErr { 41 | if err == nil { 42 | t.Fatal("expected error") 43 | } 44 | } else { 45 | if err != nil { 46 | t.Fatalf("unexpected error %v", err) 47 | } 48 | if !bytes.Equal(sID[:], tt.want[:]) { 49 | t.Errorf("got %v, want %v", sID, tt.want) 50 | } 51 | } 52 | }) 53 | } 54 | } 55 | 56 | func TestMustParse(t *testing.T) { 57 | for _, tt := range []struct { 58 | name string 59 | s string 60 | wantPanic bool 61 | }{ 62 | {"Valid", "AAECAwQFBgcICQoLDA0ODw", false}, 63 | {"Invalid", "AAAAAAAAAAAAAAAAAAAAA=", true}, 64 | } { 65 | t.Run(tt.name, func(t *testing.T) { 66 | defer func() { 67 | r := recover() 68 | if tt.wantPanic { 69 | if r == nil { 70 | t.Fatal("expected panic") 71 | } 72 | } else { 73 | if r != nil { 74 | t.Fatalf("unexpected panic %v", r) 75 | } 76 | } 77 | }() 78 | MustParse(tt.s) 79 | }) 80 | } 81 | } 82 | 83 | func TestSandIDIsZero(t *testing.T) { 84 | for _, tt := range []struct { 85 | name string 86 | sID SandID 87 | want bool 88 | }{ 89 | {"Zero", zeroSandID, true}, 90 | {"Empty", SandID{}, true}, 91 | {"NonZero", New(), false}, 92 | } { 93 | t.Run(tt.name, func(t *testing.T) { 94 | got := tt.sID.IsZero() 95 | if got != tt.want { 96 | t.Errorf("got %t, want %t", got, tt.want) 97 | } 98 | }) 99 | } 100 | } 101 | 102 | func TestSandIDString(t *testing.T) { 103 | got := zeroSandID.String() 104 | want := "AAAAAAAAAAAAAAAAAAAAAA" 105 | if got != want { 106 | t.Errorf("got %q, want %q", got, want) 107 | } 108 | } 109 | 110 | func TestSandIDScan(t *testing.T) { 111 | for _, tt := range []struct { 112 | name string 113 | value interface{} 114 | want string 115 | wantErr bool 116 | }{ 117 | {"ValidString", "AAECAwQFBgcICQoLDA0ODw", "AAECAwQFBgcICQoLDA0ODw", false}, 118 | {"EmptyString", "", "", true}, 119 | {"InvalidByteSliceLength", make([]byte, 17), "", true}, 120 | {"InvalidType", 0, "", true}, 121 | } { 122 | t.Run(tt.name, func(t *testing.T) { 123 | var sID SandID 124 | err := sID.Scan(tt.value) 125 | if tt.wantErr { 126 | if err == nil { 127 | t.Fatal("expected error") 128 | } 129 | } else { 130 | if err != nil { 131 | t.Fatalf("unexpected error %v", err) 132 | } 133 | got := sID.String() 134 | if got != tt.want { 135 | t.Errorf("got %q, want %q", got, tt.want) 136 | } 137 | } 138 | }) 139 | } 140 | } 141 | 142 | func TestSandIDValue(t *testing.T) { 143 | sID := New() 144 | 145 | v, err := sID.Value() 146 | if err != nil { 147 | t.Fatalf("unexpected error %v", err) 148 | } 149 | 150 | got, ok := v.([]byte) 151 | if !ok { 152 | t.Error("want true") 153 | } 154 | want := sID[:] 155 | if !bytes.Equal(got, want) { 156 | t.Error("want true") 157 | } 158 | } 159 | 160 | func TestSandIDMarshalText(t *testing.T) { 161 | sID := New() 162 | 163 | b, err := sID.MarshalText() 164 | if err != nil { 165 | t.Fatalf("unexpected error %v", err) 166 | } 167 | 168 | got := string(b) 169 | want := base64.URLEncoding.EncodeToString(sID[:])[:22] 170 | if got != want { 171 | t.Errorf("got %q, want %q", got, want) 172 | } 173 | } 174 | 175 | func TestSandIDUnmarshalText(t *testing.T) { 176 | for _, tt := range []struct { 177 | name string 178 | text []byte 179 | want []byte 180 | wantErr bool 181 | }{ 182 | { 183 | name: "Valid", 184 | text: []byte{ 185 | 65, 65, 69, 67, 186 | 65, 119, 81, 70, 187 | 66, 103, 99, 73, 188 | 67, 81, 111, 76, 189 | 68, 65, 48, 79, 190 | 68, 119, 191 | }, 192 | want: []byte{ 193 | 0, 1, 2, 3, 194 | 4, 5, 6, 7, 195 | 8, 9, 10, 11, 196 | 12, 13, 14, 15, 197 | }, 198 | wantErr: false, 199 | }, 200 | { 201 | name: "InvalidLength", 202 | text: []byte{ 203 | 65, 65, 69, 67, 204 | 65, 119, 81, 70, 205 | 66, 103, 99, 73, 206 | 67, 81, 111, 76, 207 | 68, 65, 48, 79, 208 | 68, 209 | }, 210 | wantErr: true, 211 | }, 212 | { 213 | name: "InvalidCharAtBeginning", 214 | text: []byte{ 215 | 0xff, 65, 69, 67, 216 | 65, 119, 81, 70, 217 | 66, 103, 99, 73, 218 | 67, 81, 111, 76, 219 | 68, 65, 48, 79, 220 | 68, 119, 221 | }, 222 | wantErr: true, 223 | }, 224 | { 225 | name: "InvalidCharInMiddle", 226 | text: []byte{ 227 | 65, 65, 69, 67, 228 | 65, 119, 81, 70, 229 | 66, 103, 99, 73, 230 | 67, 81, 111, 76, 231 | 68, 65, 48, 0xff, 232 | 68, 119, 233 | }, 234 | wantErr: true, 235 | }, 236 | } { 237 | t.Run(tt.name, func(t *testing.T) { 238 | var sID SandID 239 | err := sID.UnmarshalText(tt.text) 240 | if tt.wantErr { 241 | if err == nil { 242 | t.Fatal("expected error") 243 | } 244 | } else { 245 | if err != nil { 246 | t.Fatalf("unexpected error %v", err) 247 | } 248 | got := sID[:] 249 | if !bytes.Equal(got, tt.want) { 250 | t.Errorf("got %v, want %v", got, tt.want) 251 | } 252 | } 253 | }) 254 | } 255 | } 256 | 257 | func TestSandIDMarshalBinary(t *testing.T) { 258 | sID := New() 259 | got, err := sID.MarshalBinary() 260 | if err != nil { 261 | t.Fatalf("unexpected error %v", err) 262 | } 263 | want := sID[:] 264 | if !bytes.Equal(got, want) { 265 | t.Errorf("got %v, want %v", got, want) 266 | } 267 | } 268 | 269 | func TestSandIDUnmarshalBinary(t *testing.T) { 270 | var sID SandID 271 | if err := sID.UnmarshalBinary([]byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}); err != nil { 272 | t.Fatalf("unexpected error %v", err) 273 | } 274 | got := sID.String() 275 | want := "AAECAwQFBgcICQoLDA0ODw" 276 | if got != want { 277 | t.Errorf("got %q, want %q", got, want) 278 | } 279 | } 280 | 281 | func TestSandIDMarshalJSON(t *testing.T) { 282 | sID := New() 283 | b, err := sID.MarshalJSON() 284 | if err != nil { 285 | t.Fatalf("unexpected error %v", err) 286 | } 287 | got := string(b) 288 | want := strconv.Quote(sID.String()) 289 | if got != want { 290 | t.Errorf("got %q, want %q", got, want) 291 | } 292 | } 293 | 294 | func TestSandIDUnmarshalJSON(t *testing.T) { 295 | for _, tt := range []struct { 296 | name string 297 | json []byte 298 | want string 299 | wantErr bool 300 | }{ 301 | {"Valid", []byte(strconv.Quote("AAECAwQFBgcICQoLDA0ODw")), "AAECAwQFBgcICQoLDA0ODw", false}, 302 | {"Invalid", []byte("{"), "", true}, 303 | {"Nil", nil, "", true}, 304 | } { 305 | t.Run(tt.name, func(t *testing.T) { 306 | var sID SandID 307 | err := sID.UnmarshalJSON(tt.json) 308 | 309 | if tt.wantErr { 310 | if err == nil { 311 | t.Fatal("expected error") 312 | } 313 | } else { 314 | if err != nil { 315 | t.Fatalf("unexpected error %v", err) 316 | } 317 | got := sID.String() 318 | if got != tt.want { 319 | t.Errorf("got %q, want %q", got, tt.want) 320 | } 321 | } 322 | }) 323 | } 324 | } 325 | 326 | func TestEqual(t *testing.T) { 327 | for _, tt := range []struct { 328 | name string 329 | a, b SandID 330 | want bool 331 | }{ 332 | {"ZeroAndEmpty", zeroSandID, SandID{}, true}, 333 | {"SameAndCopy", MustParse("AAECAwQFBgcICQoLDA0ODw"), MustParse("AAECAwQFBgcICQoLDA0ODw"), true}, 334 | {"Different", New(), New(), false}, 335 | } { 336 | t.Run(tt.name, func(t *testing.T) { 337 | got := Equal(tt.a, tt.b) 338 | if got != tt.want { 339 | t.Errorf("got %t, want %t", got, tt.want) 340 | } 341 | }) 342 | } 343 | } 344 | 345 | func TestCompare(t *testing.T) { 346 | for _, tt := range []struct { 347 | name string 348 | a, b SandID 349 | want int 350 | }{ 351 | {"LessThan", MustParse("AAAAAAAAAAAAAAAAAAAAAQ"), MustParse("AAAAAAAAAAAAAAAAAAAAAg"), -1}, 352 | {"EqualTo", zeroSandID, SandID{}, 0}, 353 | {"GreaterThan", MustParse("AAAAAAAAAAAAAAAAAAAAAg"), MustParse("AAAAAAAAAAAAAAAAAAAAAQ"), 1}, 354 | } { 355 | t.Run(tt.name, func(t *testing.T) { 356 | got := Compare(tt.a, tt.b) 357 | if got != tt.want { 358 | t.Errorf("got %d, want %d", got, tt.want) 359 | } 360 | }) 361 | } 362 | } 363 | 364 | func TestNullSandIDScan(t *testing.T) { 365 | for _, tt := range []struct { 366 | name string 367 | input interface{} 368 | wantID SandID 369 | wantValid bool 370 | wantErr bool 371 | }{ 372 | {"Valid", []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, SandID{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, true, false}, 373 | {"Invalid", []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, zeroSandID, false, true}, 374 | {"Nil", nil, zeroSandID, false, false}, 375 | } { 376 | t.Run(tt.name, func(t *testing.T) { 377 | var nsID NullSandID 378 | err := nsID.Scan(tt.input) 379 | if tt.wantErr { 380 | if err == nil { 381 | t.Fatal("expected error") 382 | } 383 | } else { 384 | if err != nil { 385 | t.Fatalf("unexpected error %v", err) 386 | } 387 | if got, want := nsID.SandID, tt.wantID; !bytes.Equal(got[:], want[:]) { 388 | t.Errorf("got %v, want %v", got, want) 389 | } 390 | if got, want := nsID.Valid, tt.wantValid; got != want { 391 | t.Errorf("got %v, want %v", got, want) 392 | } 393 | } 394 | }) 395 | } 396 | } 397 | 398 | func TestNullSandIDValue(t *testing.T) { 399 | for _, tt := range []struct { 400 | name string 401 | nsID NullSandID 402 | want interface{} 403 | }{ 404 | {"Zero", NullSandID{}, nil}, 405 | {"NonZero", NullSandID{SandID: MustParse("AAECAwQFBgcICQoLDA0ODw"), Valid: true}, []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}}, 406 | } { 407 | t.Run(tt.name, func(t *testing.T) { 408 | got, err := tt.nsID.Value() 409 | if err != nil { 410 | t.Fatalf("unexpected error %v", err) 411 | } 412 | if !reflect.DeepEqual(got, tt.want) { 413 | t.Errorf("got %v, want %v", got, tt.want) 414 | } 415 | }) 416 | } 417 | } 418 | --------------------------------------------------------------------------------