1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149
|
// Copyright 2017 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package uid supports generating unique IDs. Its chief purpose is to prevent
// multiple test executions from interfering with each other, and to facilitate
// cleanup of old entities that may remain if tests exit early.
package uid
import (
"fmt"
"regexp"
"strconv"
"sync/atomic"
"time"
)
// A Space manages a set of unique IDs distinguished by a prefix.
type Space struct {
Prefix string // Prefix of UIDs. Read-only.
Sep rune // Separates UID parts. Read-only.
Time time.Time // Timestamp for UIDs. Read-only.
re *regexp.Regexp
count int32 // atomic
short bool
}
// Options are optional values for a Space.
type Options struct {
Sep rune // Separates parts of the UID. Defaults to '-'.
Time time.Time // Timestamp for all UIDs made with this space. Defaults to current time.
// Short, if true, makes the result of space.New shorter by 6 characters.
// This can be useful for character restricted IDs. It will use a shorter
// but less readable time representation, and will only use two characters
// for the count suffix instead of four.
//
// e.x. normal: gotest-20181030-59751273685000-0001
// e.x. short: gotest-1540917351273685000-01
Short bool
}
// NewSpace creates a new UID space. A UID Space is used to generate unique IDs.
func NewSpace(prefix string, opts *Options) *Space {
var short bool
sep := '-'
tm := time.Now().UTC()
if opts != nil {
short = opts.Short
if opts.Sep != 0 {
sep = opts.Sep
}
if !opts.Time.IsZero() {
tm = opts.Time
}
}
var re string
if short {
re = fmt.Sprintf(`^%s%[2]c(\d+)%[2]c\d+$`, regexp.QuoteMeta(prefix), sep)
} else {
re = fmt.Sprintf(`^%s%[2]c(\d{4})(\d{2})(\d{2})%[2]c(\d+)%[2]c\d+$`,
regexp.QuoteMeta(prefix), sep)
}
return &Space{
Prefix: prefix,
Sep: sep,
Time: tm,
re: regexp.MustCompile(re),
short: short,
}
}
// New generates a new unique ID. The ID consists of the Space's prefix, a
// timestamp, and a counter value. All unique IDs generated in the same test
// execution will have the same timestamp.
//
// Aside from the characters in the prefix, IDs contain only letters, numbers
// and sep.
func (s *Space) New() string {
c := atomic.AddInt32(&s.count, 1)
if s.short && c > 99 {
// Short spaces only have space for 99 IDs. (two characters)
panic("Short space called New more than 99 times. Ran out of IDs.")
} else if c > 9999 {
// Spaces only have space for 9999 IDs. (four characters)
panic("New called more than 9999 times. Ran out of IDs.")
}
if s.short {
return fmt.Sprintf("%s%c%d%c%02d", s.Prefix, s.Sep, s.Time.UnixNano(), s.Sep, c)
}
// Write the time as a date followed by nanoseconds from midnight of that date.
// That makes it easier to see the approximate time of the ID when it is displayed.
y, m, d := s.Time.Date()
ns := s.Time.Sub(time.Date(y, m, d, 0, 0, 0, 0, time.UTC))
// Zero-pad the counter for lexical sort order for IDs with the same timestamp.
return fmt.Sprintf("%s%c%04d%02d%02d%c%d%c%04d",
s.Prefix, s.Sep, y, m, d, s.Sep, ns, s.Sep, c)
}
// Timestamp extracts the timestamp of uid, which must have been generated by
// s. The second return value is true on success, false if there was a problem.
func (s *Space) Timestamp(uid string) (time.Time, bool) {
subs := s.re.FindStringSubmatch(uid)
if subs == nil {
return time.Time{}, false
}
if s.short {
ns, err := strconv.ParseInt(subs[1], 10, 64)
if err != nil {
return time.Time{}, false
}
return time.Unix(ns/1e9, ns%1e9), true
}
y, err1 := strconv.Atoi(subs[1])
m, err2 := strconv.Atoi(subs[2])
d, err3 := strconv.Atoi(subs[3])
ns, err4 := strconv.ParseInt(subs[4], 10, 64)
if err1 != nil || err2 != nil || err3 != nil || err4 != nil {
return time.Time{}, false
}
return time.Date(y, time.Month(m), d, 0, 0, 0, 0, time.UTC).Add(time.Duration(ns)), true
}
// Older reports whether uid was created by m and has a timestamp older than
// the current time by at least d.
func (s *Space) Older(uid string, d time.Duration) bool {
ts, ok := s.Timestamp(uid)
if !ok {
return false
}
return time.Since(ts) > d
}
|