Compare commits

...

13 commits
v1.1.0 ... main

Author SHA1 Message Date
Luyten Orion 59d884d18f
Merge pull request #4 from The-Ticking-Clockwork/devel
Merge fix for Nim 2.2.0
2025-02-19 18:29:22 +00:00
Luyten-Orion 62bb09c7f0 Update dependencies (and fix Nim 2.2.0) 2025-02-19 18:26:46 +00:00
Luyten-Orion 952412e5f5 Update dependencies (and fix Nim 2.2.0) 2025-02-19 18:05:16 +00:00
Yu-Vitaqua-fer-Chronos a5d234e0c2 Doc edits 2024-01-25 21:57:01 +00:00
Yu Vitaqua fer Chronos 8c3165a000
Merge pull request #2 from Yu-Vitaqua-fer-Chronos/devel
Add std/json support
2024-01-25 21:53:24 +00:00
Yu-Vitaqua-fer-Chronos 5d35f54bac Fix action 2024-01-25 21:49:52 +00:00
Yu-Vitaqua-fer-Chronos 5e3beea010 Add std/json support, remove redundant test on tag 2024-01-25 21:44:41 +00:00
Yu-Vitaqua-fer-Chronos d0470fca92 Fix docs 2023-12-15 19:37:57 +00:00
Yu-Vitaqua-fer-Chronos f998c0d751 Fix docs 2023-12-15 19:35:42 +00:00
Yu Vitaqua fer Chronos 8ce252be13
Merge pull request #1 from Yu-Vitaqua-fer-Chronos/devel
Merge Devel into Main
2023-11-01 21:53:03 +00:00
Yu-Vitaqua-fer-Chronos 677855bbd8 Add more tests to GitHub workflow 2023-11-01 21:48:50 +00:00
Yu-Vitaqua-fer-Chronos 138ea30fb7 Fix tests for JavaScript, raise errors and defects that inherit from one type 2023-11-01 21:43:23 +00:00
Mythical Forest Collective a889cc57f8 W.I.P BigInts support, not complete yet 2023-11-01 20:05:58 +00:00
8 changed files with 281 additions and 91 deletions

View file

@ -47,10 +47,10 @@ jobs:
uses: actions/configure-pages@v3 uses: actions/configure-pages@v3
- name: Upload artifact - name: Upload artifact
uses: actions/upload-pages-artifact@v1 uses: actions/upload-pages-artifact@v3
with: with:
path: ${{ env.deploy-dir }} path: ${{ env.deploy-dir }}
- name: Deploy to GitHub Pages - name: Deploy to GitHub Pages
id: deployment id: deployment
uses: actions/deploy-pages@v1 uses: actions/deploy-pages@v4

View file

@ -1,7 +1,10 @@
name: Run Tests name: Run Tests
on: on:
[push, pull_request] pull_request:
push:
branches:
- '**'
env: env:
nim-version: 'stable' nim-version: 'stable'
@ -20,4 +23,20 @@ jobs:
- run: nimble install -Y - run: nimble install -Y
- run: nimble test - name: Run Tests C (Normal Usage)
run: nimble test
- name: Run Tests C (Locks Disabled)
run: nimble test -d:nulidNoLocks
- name: Run Tests C (Insecure Random)
run: nimble test -d:nulidInsecureRandom
- name: Run Tests C (Insecure Random & Locks Disabled)
run: nimble test -d:nulidInsecureRandom -d:nulidNoLocks
- name: Run Tests JS (Normal Usage; Locks Disabled by Default)
run: nimble test -b:js
- name: Run Tests JS (Insecure Random)
run: nimble test -b:js -d:nulidInsecureRandom

1
.gitignore vendored
View file

@ -1,2 +1,3 @@
tests/test1.js
tests/test1 tests/test1
htmldocs/ htmldocs/

View file

@ -1,6 +1,6 @@
# NULID # NULID
This is an implementation of the [ULID](https://github.com/ulid/spec) This is an implementation of the [ULID](https://github.com/ulid/spec)
spec in Nim! spec in Nim! This also supports the JS backend for ULID generation!
This supports [`jsony`](https://github.com/treeform/jsony) and This supports [`jsony`](https://github.com/treeform/jsony) and
[`debby`](https://github.com/treeform/debby) out of the box too! [`debby`](https://github.com/treeform/debby) out of the box too!
@ -13,6 +13,9 @@ via Termux!
`-d:nulidNoLocks`: Disables any usage of locks within the program. `-d:nulidNoLocks`: Disables any usage of locks within the program.
The JS backend automatically defines `-d:nulidNoLocks`.
## Usage ## Usage
```nim ```nim
let gen = initUlidGenerator() let gen = initUlidGenerator()

View file

@ -1,6 +1,6 @@
# Package # Package
version = "1.1.0" version = "1.3.1"
author = "Yu Vitaqua fer Chronos" author = "Yu Vitaqua fer Chronos"
description = "An implementation of ULID!" description = "An implementation of ULID!"
license = "CC0" license = "CC0"
@ -11,4 +11,4 @@ srcDir = "src"
requires "nim >= 2.0.0" requires "nim >= 2.0.0"
requires "nint128 >= 0.3.2" requires "nint128 >= 0.3.2"
requires "crockfordb32 >= 1.0.0" requires "crockfordb32 >= 2.0.0"

View file

@ -1,13 +1,22 @@
{.define: crockfordb32NintSupport.}
import std/[ import std/[
times, times,
os json
] ]
import crockfordb32 import crockfordb32
import nint128
import ./nulid/private/constants import ./nulid/private/constants
import ./nulid/private/stew/endians2
when not defined(js):
import std/os
import nint128
import ./nulid/private/stew/endians2
else:
import std/jsbigints
when InsecureRandom: when InsecureRandom:
import std/random import std/random
@ -22,25 +31,42 @@ when not NoLocks:
Note: There are 2 defines that can be passed to the compiler to trigger different Note: There are 2 defines that can be passed to the compiler to trigger different
functionality in this library at runtime, they are listed here: functionality in this library at runtime, they are listed here:
- `--define:nulidInsecureRandom`: Uses `std/random` instead of `std/sysrand`. - `--define:nulidInsecureRandom`: Uses `std/random` instead of `std/sysrand`.
- `--define:nulidNoLocks` - `--define:nulidNoLocks`: Disables the use of locks.
The JS backend and Nimscript use both of these flags by default (whether either work The JS backend used `-d:nulidNoLocks` by default.
with NULID is untested).
]## ]##
when not defined(js):
type UInt128DropIn = UInt128
else:
type UInt128DropIn = JsBigInt
type type
ULIDError* = object of CatchableError
ULIDDefect* = object of Defect
ULIDGenerationError* = object of ULIDError
ULIDGenerationDefect* = object of ULIDDefect
ULID* = object ULID* = object
## An object representing a ULID. ## An object representing a ULID.
timestamp*: int64 timestamp*: int64
randomness*: UInt128 when not defined(js):
randomness*: UInt128
else:
randomness*: JsBigInt
ULIDGenerator* = ref object ULIDGenerator* = ref object
## A `ULID` generator object, contains details needed to follow the spec. ## A `ULID` generator object, contains details needed to follow the spec.
## A generator was made to be compliant with the ULID spec and also to be ## A generator was made to be compliant with the ULID spec and also to be
## threadsafe not use globals that could change. ## threadsafe.
when NoLocks: when NoLocks:
lastTime: int64 # Timestamp of last ULID, 48 bits lastTime: int64 # Timestamp of last ULID, 48 bits
random: UInt128 # A random number, 80 bits when not defined(js):
random: UInt128 # A random number, 80 bits
else:
random: JsBigInt
when InsecureRandom: when InsecureRandom:
rand: Rand # Random generator when using insecure random rand: Rand # Random generator when using insecure random
@ -53,44 +79,62 @@ type
when InsecureRandom: when InsecureRandom:
rand {.guard: lock.}: Rand # Random generator when using insecure random rand {.guard: lock.}: Rand # Random generator when using insecure random
template withLock(gen: ULIDGenerator, body: typed) = template withLock(gen: ULIDGenerator, body: typed) =
when NoLocks: when NoLocks:
body body
else: else:
{.cast(gcsafe).}: {.cast(gcsafe).}:
gen.lock.withRLock: acquire(gen.lock)
{.locks: [gen.lock].}:
try:
body body
finally:
{.cast(gcsafe).}:
release(gen.lock)
proc initUlidGenerator*(): ULIDGenerator = proc initUlidGenerator*(): ULIDGenerator =
## Initialises a `ULIDGenerator` for use. ## Initialises a `ULIDGenerator` for use.
when NoLocks: when NoLocks:
result = ULIDGenerator(lastTime: 0, random: 0.u128) result = ULIDGenerator(lastTime: LowInt48, random: LowUint80)
else: else:
result = ULIDGenerator(lock: RLock(), lastTime: 0, random: 0.u128) result = ULIDGenerator(lock: RLock(), lastTime: LowInt48, random: LowUint80)
initRLock(result.lock) initRLock(result.lock)
when InsecureRandom: when InsecureRandom:
result.withLock: result.withLock:
result.rand = initRand() result.rand = initRand()
let globalGen = initUlidGenerator() let globalGen = initUlidGenerator()
func swapBytes(x: Int128): Int128 =
result.lo = swapBytes(cast[uint64](x.hi)) when not defined(js):
result.hi = cast[int64](swapBytes(x.lo)) func swapBytes(x: Int128): Int128 =
result.lo = swapBytes(cast[uint64](x.hi))
result.hi = cast[int64](swapBytes(x.lo))
func 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] result[0..<size.len] = oa[size]
proc randomBits(n: ULIDGenerator): UInt128 {.gcsafe.} =
proc randomBits(n: ULIDGenerator): UInt128DropIn {.gcsafe.} =
var arr: array[16, byte] var arr: array[16, byte]
when InsecureRandom: when InsecureRandom:
var rnd: array[10, byte] var rnd: array[10, byte]
n.withLock: n.withLock:
rnd[0..7] = cast[array[8, byte]](n.rand.next()) when not defined(js):
rnd[8..9] = cast[array[2, byte]](n.rand.rand(high(int16)).int16) rnd[0..7] = cast[array[8, byte]](n.rand.next())
rnd[8..9] = cast[array[2, byte]](n.rand.rand(high(uint16)).uint16)
else:
for i in 0..9:
rnd[i] = n.rand.rand(high(byte)).byte
arr[6..15] = rnd arr[6..15] = rnd
@ -98,29 +142,45 @@ proc randomBits(n: ULIDGenerator): UInt128 {.gcsafe.} =
var rnd: array[10, byte] var rnd: array[10, byte]
if not urandom(rnd): if not urandom(rnd):
raise newException(OSError, "Was unable to use a secure source of randomness! " & raise newException(ULIDGenerationDefect, "Was unable to use a secure source of randomness! " &
"Please either compile with `-d:nulidInsecureRandom` or fix this!") "Please either compile with `-d:nulidInsecureRandom` or fix this somehow!")
arr[6..15] = rnd arr[6..15] = rnd
result = UInt128.fromBytesBE(arr) when not defined(js):
result = UInt128.fromBytesBE(arr)
else:
for i in arr:
result = result shl 8'big
result += big(i)
template getTime: int64 = (epochTime() * 1000).int64 template getTime: int64 = (epochTime() * 1000).int64
proc wait(gen: ULIDGenerator): int64 {.gcsafe.} = proc wait(gen: ULIDGenerator): int64 {.gcsafe.} =
result = getTime() when not defined(js):
result = getTime()
gen.withLock: gen.withLock:
while result <= gen.lastTime: while result <= gen.lastTime:
sleep(1) sleep(1)
result = getTime() result = getTime()
if result < gen.lastTime: if result < gen.lastTime:
raise newException(OSError, "Time went backwards!") raise newException(ULIDGenerationError, "Time went backwards!")
proc ulid*(gen: ULIDGenerator, timestamp = 0'i64, randomness = u128(0)): ULID = else:
raise newException(ULIDGenerationError, "Couldn't generate ULID! Try again in a millisecond.")
proc ulid*(gen: ULIDGenerator, timestamp = LowInt48, randomness = LowUint80): ULID {.gcsafe.} =
## Generate a `ULID`, if timestamp is equal to `0`, the `randomness` parameter ## Generate a `ULID`, if timestamp is equal to `0`, the `randomness` parameter
## will be ignored. ## will be ignored.
##
## See also:
## * `ulid(int64, UInt128) <#ulid_2>`_
runnableExamples: runnableExamples:
let gen = initUlidGenerator() let gen = initUlidGenerator()
@ -145,62 +205,101 @@ proc ulid*(gen: ULIDGenerator, timestamp = 0'i64, randomness = u128(0)): ULID =
result.timestamp = now result.timestamp = now
else: else:
result.timestamp = clamp(timestamp, 0, HighInt48) result.timestamp = clamp(timestamp, LowInt48, HighInt48)
result.randomness = clamp(randomness, low(UInt128), HighUint80) result.randomness = clamp(randomness, LowUint80, HighUint80)
proc ulid*(timestamp = 0'i64, randomness = u128(0)): ULID =
proc ulid*(timestamp = LowInt48, randomness = LowUint80): ULID =
## Generate a `ULID` using the global generator. ## Generate a `ULID` using the global generator.
## ##
## See also: ## See also:
## * `ulid(ULIDGenerator, int64, UInt128) <#ulid,ULIDGenerator,int64>`_ ## * `ulid(ULIDGenerator, int64, UInt128) <#ulid,ULIDGenerator>`_
runnableExamples: runnableExamples:
echo ulid() echo ulid()
result = ulid(globalGen, timestamp) result = ulid(globalGen, timestamp)
func toInt128*(ulid: ULID): Int128 =
## Allows for a `ULID` to be converted to an Int128.
runnableExamples:
echo ulid().toInt128()
result = i128(ulid.timestamp) shl 80 when not defined(js):
func toInt128*(ulid: ULID): Int128 =
## Allows for a `ULID` to be converted to an `Int128`.
##
## **Note:** On the JS backend this returns a `JsBigInt` from `std/jsbigints`
runnableExamples:
echo ulid().toInt128()
result.hi += cast[int64](ulid.randomness.hi) result = i128(ulid.timestamp) shl 80
result.lo += ulid.randomness.lo
func fromInt128*(_: typedesc[ULID], val: Int128): ULID = result.hi += cast[int64](ulid.randomness.hi)
## Parses an Int128 to a ULID. result.lo += ulid.randomness.lo
result.timestamp = (val shr 16).hi
result.randomness = UInt128(
hi: cast[uint64]((val.hi shl 48) shr 48),
lo: val.lo
)
func toBytes*(ulid: ULID): array[16, byte] =
## Allows for a `ULID` to be converted to a byte array for the binary format.
runnableExamples:
let
ulid = ULID.parse("01H999MBGTEA8BDS0M5AWEBB1A")
ulidBytes = [1.byte, 138, 82, 154, 46, 26, 114, 144, 182, 228, 20, 42, 184, 229, 172, 42]
assert ulid == ULID.fromBytes(ulidBytes) func fromInt128*(_: typedesc[ULID], val: Int128): ULID =
## Parses an `Int128` to a `ULID`.
##
## **Note:** On the JS backend this accepts a `JsBigInt` from `std/jsbigints`
result.timestamp = (val shr 16).hi
result.randomness = UInt128(
hi: cast[uint64]((val.hi shl 48) shr 48),
lo: val.lo
)
when cpuEndian == littleEndian:
return cast[array[16, byte]](ulid.toInt128().swapBytes())
else: else:
return cast[array[16, byte]](ulid.toInt128()) func toInt128*(ulid: ULID): JsBigInt =
## Allows for a `ULID` to be converted to a `JsBigInt`.
##
## **Note:** On the native backends this returns an `Int128` from `nint128`.
runnableExamples:
echo ulid().toInt128()
func fromBytes*(_: typedesc[ULID], ulidBytes: openArray[byte]): ULID = result = big(ulid.timestamp) shl 80'big
## Parses a byte array to a `ULID.`. result += ulid.randomness
if ulidBytes.len != 16:
raise newException(RangeDefect, "Given byte array must be 16 bytes long!")
when cpuEndian == littleEndian:
return ULID.fromInt128(cast[Int128](ulidBytes.toArray(0..15)).swapBytes())
else: proc fromInt128*(_: typedesc[ULID], val: JsBigInt): ULID =
return ULID.fromInt128(cast[Int128](ulidBytes.toArray(0..15))) ## Parses an `JsBigInt` to a `ULID`.
##
## **Note:** On the native backends this accepts an `Int128` from `nint128`
assert val <= HighInt128
result.timestamp = ((val and TimestampBitmask) shr 80'big).toNumber().int64
result.randomness = val and RandomnessBitmask
when not defined(js):
func toBytes*(ulid: ULID): array[16, byte] =
## Allows for a `ULID` to be converted to a byte array for the binary format.
##
## **Note:** This isn't available for the JS backend.
runnableExamples:
let
ulid = ULID.parse("01H999MBGTEA8BDS0M5AWEBB1A")
ulidBytes = [1.byte, 138, 82, 154, 46, 26, 114, 144, 182, 228, 20, 42, 184, 229, 172, 42]
assert ulid == ULID.fromBytes(ulidBytes)
when cpuEndian == littleEndian:
return cast[array[16, byte]](ulid.toInt128().swapBytes())
else:
return cast[array[16, byte]](ulid.toInt128())
func fromBytes*(_: typedesc[ULID], ulidBytes: openArray[byte]): ULID =
## Parses a byte array to a `ULID.`.
##
## **Note:** This isn't available for the JS backend.
if ulidBytes.len != 16:
raise newException(RangeDefect, "Given byte array must be 16 bytes long!")
when cpuEndian == littleEndian:
return ULID.fromInt128(cast[Int128](ulidBytes.toArray(0..15)).swapBytes())
else:
return ULID.fromInt128(cast[Int128](ulidBytes.toArray(0..15)))
func parse*(_: typedesc[ULID], ulidStr: string): ULID = func parse*(_: typedesc[ULID], ulidStr: string): ULID =
## Parses a `ULID` from a string. ## Parses a `ULID` from a string.
@ -211,16 +310,36 @@ func parse*(_: typedesc[ULID], ulidStr: string): ULID =
raise newException(RangeDefect, "Invalid ULID! Must be 26 characters long!") raise newException(RangeDefect, "Invalid ULID! Must be 26 characters long!")
result.timestamp = int64.decode(ulidStr[0..9]) result.timestamp = int64.decode(ulidStr[0..9])
result.randomness = UInt128.decode(ulidStr[10..25]) when not defined(js):
result.randomness = UInt128.decode(ulidStr[10..25])
else:
result.randomness = JsBigInt.decode(ulidStr[10..25])
proc `==`*(a, b: ULID): bool = a.toInt128() == b.toInt128() proc `==`*(a, b: ULID): bool = a.toInt128() == b.toInt128()
func `$`*(ulid: ULID): string = func `$`*(ulid: ULID): string =
## Returns the string representation of a ULID. ## Returns the string representation of a ULID.
runnableExamples: runnableExamples:
echo $ulid() echo $ulid()
result = Int128.encode(ulid.toInt128(), 26) when not defined(js):
result = Int128.encode(ulid.toInt128(), 26)
else:
result = JsBigInt.encode(ulid.toInt128(), 26)
# std/json support
proc `%`*(u: ULID): JsonNode =
## Serializes a `ULID` to JSON.
newJString($u)
proc to*(j: JsonNode, _: typedesc[ULID]): ULID =
## Deserializes a `ULID` from JSON.
if j.kind != JString:
raise newException(JsonKindError, "Expected a string!")
result = ULID.parse(j.getStr())
when HasJsony: when HasJsony:
import jsony import jsony
@ -239,4 +358,4 @@ when HasDebby:
func sqlParseHook*(data: string, v: var ULID) = func sqlParseHook*(data: string, v: var ULID) =
var res: Bytes var res: Bytes
sqlParseHook(data, res) sqlParseHook(data, res)
v = ULID.fromBytes(cast[seq[byte]](res)) v = ULID.fromBytes(cast[seq[byte]](res))

View file

@ -1,16 +1,44 @@
import nint128 const
LowInt48* = 0'i64
HighInt48* = 281474976710655'i64
const const
HighInt48* = 281474976710655'i64 HighUint80Str = "1208925819614629174706176"
HighUint80* = u128("1208925819614629174706176") TimestampBitmaskStr = "340282366920937254537554992802593505280"
RandomnessBitmaskStr = "1208925819614629174706175"
when not defined(js):
import nint128
const
LowUint80* = u128(0)
HighUint80* = u128(HighUint80Str)
#TimestampBitmask = big(TimestampBitmaskStr)
#RandomnessBitmask = big(RandomnessBitmaskStr)
else:
import std/[
jsbigints
]
let
LowUint80* = big(0)
HighUint80* = big(HighUint80Str)
HighInt128* = big("340282366920938463463374607431768211455")
TimestampBitmask* = big(TimestampBitmaskStr)
RandomnessBitmask* = big(RandomnessBitmaskStr)
# No sysrand on the JS backend nor VM. # No sysrand on the JS backend nor VM.
const InsecureRandom* = defined(nulidInsecureRandom) or defined(js) or defined(nimvm) const InsecureRandom* = defined(nulidInsecureRandom) or defined(nimscript)
const NoLocks* = defined(nulidNoLocks) or defined(js) or defined(nimvm) const NoLocks* = defined(nulidNoLocks) or defined(js) or defined(nimscript)
# Support for other libraries # Support for other libraries
{.warning[UnusedImport]:off.} {.warning[UnusedImport]:off.}
const const
HasJsony* = compiles do: import jsony HasJsony* = compiles do: import jsony
HasDebby* = compiles do: import debby/common HasDebby* = compiles do: import debby/common

View file

@ -4,10 +4,21 @@
# the letter 't'). # the letter 't').
# #
# To run these tests, simply execute `nimble test`. # To run these tests, simply execute `nimble test`.
import std/[
unittest,
json
]
import unittest const UlidRandStr = "541019288874337045949482"
import pkg/nint128 when not defined(js):
import nint128
const UlidRand = u128(UlidRandStr)
else:
import std/jsbigints
let UlidRand = big(UlidRandStr)
import nulid import nulid
@ -19,7 +30,7 @@ test "ULID Generation":
test "ULID Parsing": test "ULID Parsing":
let ulidStr = "01H999MBGTEA8BDS0M5AWEBB1A" let ulidStr = "01H999MBGTEA8BDS0M5AWEBB1A"
let ulid = ULID(timestamp: 1693602950682, let ulid = ULID(timestamp: 1693602950682,
randomness: u128("541019288874337045949482")) randomness: UlidRand)
check ULID.parse(ulidStr) == ulid check ULID.parse(ulidStr) == ulid
@ -28,10 +39,19 @@ test "ULID Int128 Conversion":
check ULID.fromInt128(ulid.toInt128()) == ulid check ULID.fromInt128(ulid.toInt128()) == ulid
test "ULID Binary Format": when not defined(js):
let # Not planned to be implemented yet for the JS backend
ulid = ULID.parse("01H999MBGTEA8BDS0M5AWEBB1A")
ulidBytes = [1.byte, 138, 82, 154, 46, 26, 114, 144, 182, 228, 20, 42, 184, 229, 172, 42]
check ulid == ULID.fromBytes(ulidBytes) test "ULID Binary Format":
check ulid.toBytes == ulidBytes let
ulid = ULID.parse("01H999MBGTEA8BDS0M5AWEBB1A")
ulidBytes = [1.byte, 138, 82, 154, 46, 26, 114, 144, 182, 228, 20, 42, 184, 229, 172, 42]
check ulid == ULID.fromBytes(ulidBytes)
check ulid.toBytes == ulidBytes
test "ULID std/json support":
let ulid = ULID.parse("01H999MBGTEA8BDS0M5AWEBB1A")
check (%ulid).getStr() == "01H999MBGTEA8BDS0M5AWEBB1A"
check (%ulid).to(ULID) == ulid