Add some documentation for NULID as well as minor tweaks.

This commit is contained in:
Yu Vitaqua fer Chronos 2023-10-07 23:54:13 +01:00
parent 14433a793b
commit 61b4312bad
4 changed files with 93 additions and 17 deletions

0
config.nims Normal file
View file

View file

@ -1,6 +1,6 @@
# Package
version = "0.2.1"
version = "0.2.2"
author = "Yu Vitaqua fer Chronos"
description = "An implementation of ULID!"
license = "CC0"

View file

@ -1,6 +1,5 @@
import std/[
asyncdispatch,
sysrand,
times
]
@ -9,30 +8,72 @@ import pkg/[
nint128
]
const insecureRandom = defined(nulidInsecureRandom) or defined(js) # No sysrand on the JS backend
when insecureRandom:
import std/random
else:
import std/sysrand
# Relying on internal implementation details isn't great, but it's unlikely to change so...
import pkg/nint128/vendor/stew/endians2
const HighUint80 = u128("1208925819614629174706176")
type
NULID* = object
## An object representing a ULID.
timestamp*: int64 = 0
randomness*: UInt128 = 0.u128
NULIDGenerator* = ref object
lastTime: int64 = 0
random: UInt128 = u128(0)
## A NULID generator object, contains details needed to follow the spec.
## A generator was made to be compliant with the NULID spec and also to be
## threadsafe not use globals that could change.
lastTime: int64 ## Timestamp of last ULID
random: UInt128 ## A random number
when insecureRandom:
rand: Rand ## Random generator when using insecure random
proc initNulidGenerator*(): NULIDGenerator =
## Initialises a `NULIDGenerator` for use.
result = NULIDGenerator(lastTime: 0, random: 0.u128)
when insecureRandom:
result.rand = initRand()
let globalGen = initNulidGenerator()
func swapBytes(x: Int128): Int128 =
result.lo = swapBytes(cast[uint64](x.hi))
result.hi = cast[int64](swapBytes(x.lo))
proc toArray*[T](oa: openArray[T], size: static Slice[int]): array[size.len, T] =
func toArray[T](oa: openArray[T], size: static Slice[int]): array[size.len, T] =
result[0..<size.len] = oa[size]
proc randomBits(): UInt128 =
let rnd = urandom(10)
proc randomBits(n: NULIDGenerator): UInt128 =
var arr: array[16, byte]
result = UInt128.fromBytesBE(@[0.byte, 0, 0, 0, 0, 0] & rnd)
when insecureRandom:
var rnd: array[10, byte]
rnd[0..7] = cast[array[8, byte]](n.rand.next())
rnd[8..9] = cast[array[2, byte]](n.rand.rand(high(int16)).int16)
arr[6..15] = rnd
else:
var rnd: array[10, byte]
if not urandom(rnd):
raise newException(OSError, "Was unable to use a secure source of randomness! " &
"Please either compile with `--define:nulidInsecureRandom` or fix this!")
arr[6..15] = rnd
result = UInt128.fromBytesBE(arr)
template getTime: int64 = (epochTime() * 1000).int64
@ -44,6 +85,7 @@ proc wait(gen: NULIDGenerator): Future[int64] {.async.} =
result = getTime()
proc nulid*(gen: NULIDGenerator, timestamp: int64 = 0): Future[NULID] {.async.} =
## Asynchronously generate a `NULID`.
if timestamp == 0:
var now = getTime()
@ -57,7 +99,7 @@ proc nulid*(gen: NULIDGenerator, timestamp: int64 = 0): Future[NULID] {.async.}
now = await gen.wait()
else:
gen.random = randomBits()
gen.random = gen.randomBits()
result.timestamp = now
@ -66,29 +108,55 @@ proc nulid*(gen: NULIDGenerator, timestamp: int64 = 0): Future[NULID] {.async.}
result.randomness = gen.random
proc nulidSync*(gen: NULIDGenerator, timestamp: int64 = 0): NULID =
## Synchronously generate a `NULID`.
result = waitFor gen.nulid(timestamp)
proc toInt128*(ulid: NULID): Int128 =
proc nulid*(timestamp: int64 = 0): Future[NULID] =
## Asynchronously generate a `NULID` using the global generator.
result = nulid(globalGen, timestamp)
proc nulidSync*(timestamp: int64 = 0): NULID =
## Synchronously generate a `NULID` using the global generator.
runnableExamples:
echo nulidSync()
result = waitFor nulid(timestamp)
func toInt128*(ulid: NULID): Int128 =
## Allows for a ULID to be converted to an Int128.
runnableExamples:
echo nulidSync().toInt128()
result = i128(ulid.timestamp) shl 80
result.hi += cast[int64](ulid.randomness.hi)
result.lo += ulid.randomness.lo
proc fromInt128*(_: typedesc[NULID], val: Int128): NULID =
func fromInt128*(_: typedesc[NULID], val: Int128): NULID =
## Parses an Int128 to a NULID.
result.timestamp = (val shr 16).hi
result.randomness = UInt128(
hi: cast[uint64]((val.hi shl 48) shr 48),
lo: val.lo
)
proc toBytes*(ulid: NULID): array[16, byte] =
func toBytes*(ulid: NULID): array[16, byte] =
## Allows for a NULID to be converted to a byte array.
runnableExamples:
let
nulid = parseNulid("01H999MBGTEA8BDS0M5AWEBB1A")
nulidBytes = [1.byte, 138, 82, 154, 46, 26, 114, 144, 182, 228, 20, 42, 184, 229, 172, 42]
echo nulid == NULID.fromBytes(nulidBytes)
when cpuEndian == littleEndian:
return cast[array[16, byte]](ulid.toInt128().swapBytes())
else:
return cast[array[16, byte]](ulid.toInt128())
proc fromBytes*(_: typedesc[NULID], ulidBytes: openArray[byte]): NULID =
func fromBytes*(_: typedesc[NULID], ulidBytes: openArray[byte]): NULID =
## Parses a byte array to a NULID.
if ulidBytes.len != 16:
raise newException(RangeDefect, "Given byte array must be 16 bytes long!")
@ -98,15 +166,23 @@ proc fromBytes*(_: typedesc[NULID], ulidBytes: openArray[byte]): NULID =
else:
return NULID.fromInt128(cast[Int128](ulidBytes.toArray(0..15)))
proc parseNulid*(ulidStr: string): NULID =
func parseNulid*(ulidStr: string): NULID =
## Parses a string to a NULID.
runnableExamples:
echo parseNulid("01H999MBGTEA8BDS0M5AWEBB1A")
if ulidStr.len != 26:
raise newException(RangeDefect, "Invalid ULID! Must be 26 characters long!")
result.timestamp = int64.decode(ulidStr[0..9])
result.randomness = UInt128.decode(ulidStr[10..25])
proc `$`*(ulid: NULID): string =
func `$`*(ulid: NULID): string =
## Returns the string representation of a ULID.
runnableExamples:
echo nulidSync()
result = Int128.encode(ulid.toInt128(), 26)
if result.len < 26:
if result.len < 26: # Crappy fix
result.insert("0")

View file

@ -11,7 +11,7 @@ import pkg/nint128
import nulid
let gen = NULIDGenerator()
let gen = initNulidGenerator()
test "NULID Generation":
for _ in 0..5: