mirror of
https://github.com/denoland/std.git
synced 2024-11-21 20:50:22 +00:00
feat(uuid/unstable): @std/uuid/v7
(#5887)
* feat(uuid): add uuid v7 generation and validation * remove as string * fmt * update mod-exports check * mark more items experimental * fix test name * call getRandomValues once * add checks for user provided timestamp * fmt * consolidate checks * fix missing options.timestamp * consolidate error check * use pre-shifted variant and version * add extractTimestamp function for UUIDv7 * remove random option from uuid v7 generate * fix import statements for extractTimestamp function in uuid/v7.ts * remove bad comment * tweaks * add uuid v7 module doc * fmt * align extractTimestamp invalid uuid error message with style guide * fmt * add experimental tags * use timestamp argument instead of options generate v7 uuid * fmt * tweak --------- Co-authored-by: Yoshiya Hinosawa <stibium121@gmail.com> Co-authored-by: Asher Gomez <ashersaupingomez@gmail.com>
This commit is contained in:
parent
6c684b8bc8
commit
b808ee6de6
@ -48,6 +48,7 @@ for await (
|
||||
/uuid(\/|\\)v3\.ts$/,
|
||||
/uuid(\/|\\)v4\.ts$/,
|
||||
/uuid(\/|\\)v5\.ts$/,
|
||||
/uuid(\/|\\)v7\.ts$/,
|
||||
/yaml(\/|\\)schema\.ts$/,
|
||||
/test\.ts$/,
|
||||
/\.d\.ts$/,
|
||||
|
@ -8,6 +8,7 @@
|
||||
"./v1": "./v1.ts",
|
||||
"./v3": "./v3.ts",
|
||||
"./v4": "./v4.ts",
|
||||
"./v5": "./v5.ts"
|
||||
"./v5": "./v5.ts",
|
||||
"./v7": "./v7.ts"
|
||||
}
|
||||
}
|
||||
|
26
uuid/mod.ts
26
uuid/mod.ts
@ -33,6 +33,11 @@ import { generate as generateV1, validate as validateV1 } from "./v1.ts";
|
||||
import { generate as generateV3, validate as validateV3 } from "./v3.ts";
|
||||
import { validate as validateV4 } from "./v4.ts";
|
||||
import { generate as generateV5, validate as validateV5 } from "./v5.ts";
|
||||
import {
|
||||
extractTimestamp as extractTimestampV7,
|
||||
generate as generateV7,
|
||||
validate as validateV7,
|
||||
} from "./v7.ts";
|
||||
|
||||
/**
|
||||
* Generator and validator for
|
||||
@ -106,3 +111,24 @@ export const v5 = {
|
||||
generate: generateV5,
|
||||
validate: validateV5,
|
||||
};
|
||||
|
||||
/**
|
||||
* Generator and validator for
|
||||
* {@link https://www.rfc-editor.org/rfc/rfc9562.html#section-5.7 | UUIDv7}.
|
||||
*
|
||||
* @experimental **UNSTABLE**: New API, yet to be vetted.
|
||||
*
|
||||
* @example Usage
|
||||
* ```ts
|
||||
* import { v7 } from "@std/uuid";
|
||||
* import { assert } from "@std/assert";
|
||||
*
|
||||
* const uuid = v7.generate();
|
||||
* assert(v7.validate(uuid));
|
||||
* ```
|
||||
*/
|
||||
export const v7 = {
|
||||
generate: generateV7,
|
||||
validate: validateV7,
|
||||
extractTimestamp: extractTimestampV7,
|
||||
};
|
||||
|
117
uuid/v7.ts
Normal file
117
uuid/v7.ts
Normal file
@ -0,0 +1,117 @@
|
||||
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
/**
|
||||
* Functions for working with UUID Version 7 strings.
|
||||
*
|
||||
* UUID Version 7 is defined in {@link https://www.rfc-editor.org/rfc/rfc9562.html#section-5.7 | RFC 9562}.
|
||||
*
|
||||
* @experimental **UNSTABLE**: New API, yet to be vetted.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { generate, validate, extractTimestamp } from "@std/uuid/v7";
|
||||
* import { assert, assertEquals } from "@std/assert";
|
||||
*
|
||||
* const uuid = generate();
|
||||
* assert(validate(uuid));
|
||||
* assertEquals(extractTimestamp("017f22e2-79b0-7cc3-98c4-dc0c0c07398f"), 1645557742000);
|
||||
* ```
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import { bytesToUuid } from "./_common.ts";
|
||||
|
||||
const UUID_RE =
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[7][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||
/**
|
||||
* Determines whether a string is a valid
|
||||
* {@link https://www.rfc-editor.org/rfc/rfc9562.html#section-5.7 | UUIDv7}.
|
||||
*
|
||||
* @experimental **UNSTABLE**: New API, yet to be vetted.
|
||||
*
|
||||
* @param id UUID value.
|
||||
*
|
||||
* @returns `true` if the string is a valid UUIDv7, otherwise `false`.
|
||||
*
|
||||
* @example Usage
|
||||
* ```ts
|
||||
* import { validate } from "@std/uuid/v7";
|
||||
* import { assert, assertFalse } from "@std/assert";
|
||||
*
|
||||
* assert(validate("017f22e2-79b0-7cc3-98c4-dc0c0c07398f"));
|
||||
* assertFalse(validate("fac8c1e0-ad1a-4204-a0d0-8126ae84495d"));
|
||||
* ```
|
||||
*/
|
||||
export function validate(id: string): boolean {
|
||||
return UUID_RE.test(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a {@link https://www.rfc-editor.org/rfc/rfc9562.html#section-5.7 | UUIDv7}.
|
||||
*
|
||||
* @experimental **UNSTABLE**: New API, yet to be vetted.
|
||||
*
|
||||
* @throws {RangeError} If the timestamp is not a non-negative integer.
|
||||
*
|
||||
* @param timestamp Unix Epoch timestamp in milliseconds.
|
||||
*
|
||||
* @returns Returns a UUIDv7 string
|
||||
*
|
||||
* @example Usage
|
||||
* ```ts
|
||||
* import { generate, validate } from "@std/uuid/v7";
|
||||
* import { assert } from "@std/assert";
|
||||
*
|
||||
* const uuid = generate();
|
||||
* assert(validate(uuid));
|
||||
* ```
|
||||
*/
|
||||
export function generate(timestamp: number = Date.now()): string {
|
||||
const bytes = new Uint8Array(16);
|
||||
const view = new DataView(bytes.buffer);
|
||||
// Unix timestamp in milliseconds (truncated to 48 bits)
|
||||
if (!Number.isInteger(timestamp) || timestamp < 0) {
|
||||
throw new RangeError(
|
||||
`Cannot generate UUID as timestamp must be a non-negative integer: timestamp ${timestamp}`,
|
||||
);
|
||||
}
|
||||
view.setBigUint64(0, BigInt(timestamp) << 16n);
|
||||
crypto.getRandomValues(bytes.subarray(6));
|
||||
// Version (4 bits) Occupies bits 48 through 51 of octet 6.
|
||||
view.setUint8(6, (view.getUint8(6) & 0b00001111) | 0b01110000);
|
||||
// Variant (2 bits) Occupies bits 64 through 65 of octet 8.
|
||||
view.setUint8(8, (view.getUint8(8) & 0b00111111) | 0b10000000);
|
||||
return bytesToUuid(bytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the timestamp from a UUIDv7.
|
||||
*
|
||||
* @experimental **UNSTABLE**: New API, yet to be vetted.
|
||||
*
|
||||
* @param uuid UUIDv7 string to extract the timestamp from.
|
||||
* @returns Returns the timestamp in milliseconds.
|
||||
*
|
||||
* @throws {TypeError} If the UUID is not a valid UUIDv7.
|
||||
*
|
||||
* @example Usage
|
||||
* ```ts
|
||||
* import { extractTimestamp } from "@std/uuid/v7";
|
||||
* import { assertEquals } from "@std/assert";
|
||||
*
|
||||
* const uuid = "017f22e2-79b0-7cc3-98c4-dc0c0c07398f";
|
||||
* const timestamp = extractTimestamp(uuid);
|
||||
* assertEquals(timestamp, 1645557742000);
|
||||
* ```
|
||||
*/
|
||||
export function extractTimestamp(uuid: string): number {
|
||||
if (!validate(uuid)) {
|
||||
throw new TypeError(
|
||||
`Cannot extract timestamp because the UUID is not a valid UUIDv7: uuid is "${uuid}"`,
|
||||
);
|
||||
}
|
||||
const timestampHex = uuid.slice(0, 8) + uuid.slice(9, 13);
|
||||
return parseInt(timestampHex, 16);
|
||||
}
|
94
uuid/v7_test.ts
Normal file
94
uuid/v7_test.ts
Normal file
@ -0,0 +1,94 @@
|
||||
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
|
||||
import { assert, assertEquals, assertThrows } from "@std/assert";
|
||||
import { extractTimestamp, generate, validate } from "./v7.ts";
|
||||
import { stub } from "../testing/mock.ts";
|
||||
|
||||
Deno.test("generate() generates a non-empty string", () => {
|
||||
const u1 = generate();
|
||||
|
||||
assertEquals(typeof u1, "string", "returns a string");
|
||||
assert(u1 !== "", "return string is not empty");
|
||||
});
|
||||
|
||||
Deno.test("generate() generates UUIDs in version 7 format", () => {
|
||||
for (let i = 0; i < 10000; i++) {
|
||||
const u = generate();
|
||||
assert(validate(u), `${u} is not a valid uuid v7`);
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("generate() generates a UUIDv7 matching the example test vector", () => {
|
||||
/**
|
||||
* Example test vector from the RFC:
|
||||
* {@see https://www.rfc-editor.org/rfc/rfc9562.html#appendix-A.6}
|
||||
*/
|
||||
const timestamp = 0x017F22E279B0;
|
||||
const random = new Uint8Array([
|
||||
// rand_a = 0xCC3
|
||||
0xC,
|
||||
0xC3,
|
||||
// rand_b = 0b01, 0x8C4DC0C0C07398F
|
||||
0x18,
|
||||
0xC4,
|
||||
0xDC,
|
||||
0x0C,
|
||||
0x0C,
|
||||
0x07,
|
||||
0x39,
|
||||
0x8F,
|
||||
]);
|
||||
using _getRandomValuesStub = stub(crypto, "getRandomValues", (array) => {
|
||||
for (let index = 0; index < (random.length); index++) {
|
||||
array[index] = random[index]!;
|
||||
}
|
||||
return random;
|
||||
});
|
||||
const u = generate(timestamp);
|
||||
assertEquals(u, "017f22e2-79b0-7cc3-98c4-dc0c0c07398f");
|
||||
});
|
||||
|
||||
Deno.test("generate() throws on invalid timestamp", () => {
|
||||
assertThrows(
|
||||
() => generate(-1),
|
||||
RangeError,
|
||||
"Cannot generate UUID as timestamp must be a non-negative integer: timestamp -1",
|
||||
);
|
||||
assertThrows(
|
||||
() => generate(NaN),
|
||||
RangeError,
|
||||
"Cannot generate UUID as timestamp must be a non-negative integer: timestamp NaN",
|
||||
);
|
||||
assertThrows(
|
||||
() => generate(2.3),
|
||||
RangeError,
|
||||
"Cannot generate UUID as timestamp must be a non-negative integer: timestamp 2.3",
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("validate() checks if a string is a valid v7 UUID", () => {
|
||||
const u = generate();
|
||||
const t = "017f22e2-79b0-7cc3-98c4-dc0c0c07398f";
|
||||
assert(validate(u), `generated ${u} should be valid`);
|
||||
assert(validate(t), `${t} should be valid`);
|
||||
});
|
||||
|
||||
Deno.test("extractTimestamp(uuid) extracts the timestamp from a UUIDv7", () => {
|
||||
const u = "017f22e2-79b0-7cc3-98c4-dc0c0c07398f";
|
||||
const now = Date.now();
|
||||
const u2 = generate(now);
|
||||
assertEquals(extractTimestamp(u), 1645557742000);
|
||||
assertEquals(extractTimestamp(u2), now);
|
||||
});
|
||||
|
||||
Deno.test("extractTimestamp(uuid) throws on invalid UUID", () => {
|
||||
assertThrows(
|
||||
() => extractTimestamp("invalid-uuid"),
|
||||
TypeError,
|
||||
`Cannot extract timestamp because the UUID is not a valid UUIDv7: uuid is "invalid-uuid"`,
|
||||
);
|
||||
assertThrows(
|
||||
() => extractTimestamp(crypto.randomUUID()),
|
||||
TypeError,
|
||||
`Cannot extract timestamp because the UUID is not a valid UUIDv7:`,
|
||||
);
|
||||
});
|
Loading…
Reference in New Issue
Block a user