@planet-matrix/mobius-model 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/README.md +26 -0
- package/dist/index.js +3 -3
- package/dist/index.js.map +8 -5
- package/package.json +1 -1
- package/src/basic/README.md +17 -16
- package/src/basic/enhance.ts +10 -0
- package/src/basic/index.ts +2 -0
- package/src/basic/object.ts +82 -0
- package/src/encoding/README.md +105 -0
- package/src/encoding/base64.ts +98 -0
- package/src/encoding/index.ts +1 -0
- package/src/index.ts +2 -0
- package/src/random/README.md +109 -0
- package/src/random/index.ts +1 -0
- package/src/random/uuid.ts +103 -0
- package/tests/unit/basic/object.spec.ts +32 -1
- package/tests/unit/encoding/base64.spec.ts +40 -0
- package/tests/unit/random/uuid.spec.ts +37 -0
package/package.json
CHANGED
package/src/basic/README.md
CHANGED
|
@@ -9,22 +9,23 @@ This module provides runtime utilities organized by domain so you can quickly lo
|
|
|
9
9
|
### 1. Domain Areas
|
|
10
10
|
|
|
11
11
|
1. Helper: Shared runtime helpers used across other domains.
|
|
12
|
-
2. Is:
|
|
13
|
-
3. String: String
|
|
14
|
-
4. Number: Numeric
|
|
15
|
-
5. Boolean: Boolean
|
|
16
|
-
6. BigInt: BigInt
|
|
17
|
-
7. Symbol: Symbol
|
|
18
|
-
8. Array: Array
|
|
19
|
-
9. Object: Object
|
|
20
|
-
10. Function: Function
|
|
21
|
-
11. Temporal:
|
|
22
|
-
12. Error: Error
|
|
23
|
-
13. RegExp: RegExp
|
|
24
|
-
14. Promise: Promise
|
|
25
|
-
15. Stream:
|
|
26
|
-
|
|
27
|
-
|
|
12
|
+
2. Is: Runtime predicates and type guards (primitives, objects, iterables, DOM/browser targets, and more).
|
|
13
|
+
3. String: String generation, casing conversion, slicing, splitting, and truncation helpers.
|
|
14
|
+
4. Number: Numeric normalization, range constraint, parity checks, and randomization helpers.
|
|
15
|
+
5. Boolean: Boolean conversion and logical operation helpers.
|
|
16
|
+
6. BigInt: BigInt-oriented helpers.
|
|
17
|
+
7. Symbol: Symbol creation, inspection, and conversion helpers.
|
|
18
|
+
8. Array: Array-oriented helpers and transformations.
|
|
19
|
+
9. Object: Object field selection/exclusion, Date-field timestamp conversion, and object utility helpers.
|
|
20
|
+
10. Function: Function composition and execution-control helpers (once, debounce, throttle, memoize, etc.).
|
|
21
|
+
11. Temporal: Date/time formatting, relative-time, and humanization helpers.
|
|
22
|
+
12. Error: Error detection and exception-stringify helpers.
|
|
23
|
+
13. RegExp: RegExp-based validation helpers.
|
|
24
|
+
14. Promise: Promise control-flow, queueing, retry, interval, and forever-loop helpers.
|
|
25
|
+
15. Stream: ReadableStream construction, consumption, and transform helpers.
|
|
26
|
+
16. Enhance: Runtime enhancement helpers (for example, BigInt JSON serialization support).
|
|
27
|
+
|
|
28
|
+
For a full list of exports, see the domain files (Helper, Is, String, Number, Boolean, BigInt, Symbol, Array, Object, Function, Temporal, Error, RegExp, Promise, Stream, Enhance).
|
|
28
29
|
|
|
29
30
|
## For Contributors
|
|
30
31
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @see {@link https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/BigInt#%E5%9C%A8_json_%E4%B8%AD%E4%BD%BF%E7%94%A8}
|
|
3
|
+
*/
|
|
4
|
+
export const enhanceBigInt = (): void => {
|
|
5
|
+
// @ts-expect-error - extend BigInt toJSON
|
|
6
|
+
// oxlint-disable-next-line no-extend-native
|
|
7
|
+
BigInt.prototype.toJSON = function toJSON(): number {
|
|
8
|
+
return Number(this.toString())
|
|
9
|
+
}
|
|
10
|
+
}
|
package/src/basic/index.ts
CHANGED
package/src/basic/object.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AnyRecord } from "../type/index.ts"
|
|
2
|
+
import { isDate } from "./is.ts"
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Return a new object that includes only the specified keys from the source object.
|
|
@@ -56,3 +57,84 @@ export const excludeFields = <T extends AnyRecord, K extends keyof T>(
|
|
|
56
57
|
// oxlint-disable-next-line no-unsafe-type-assertion
|
|
57
58
|
return newObject as Omit<T, K> // 使用Omit类型确保返回的类型反映了被排除的键
|
|
58
59
|
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Convert all Date fields in an object to number.
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```before
|
|
66
|
+
* type Obj = {
|
|
67
|
+
* createdAt: Date,
|
|
68
|
+
* }
|
|
69
|
+
* ```
|
|
70
|
+
* ```after
|
|
71
|
+
* type Obj = {
|
|
72
|
+
* createdAt: number,
|
|
73
|
+
* }
|
|
74
|
+
* ```
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```before
|
|
78
|
+
* type Obj = {
|
|
79
|
+
* createdAt: Date | undefined,
|
|
80
|
+
* }
|
|
81
|
+
* ```
|
|
82
|
+
* ```after
|
|
83
|
+
* type Obj = {
|
|
84
|
+
* createdAt: number | undefined,
|
|
85
|
+
* }
|
|
86
|
+
* ```
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```before
|
|
90
|
+
* type Obj = {
|
|
91
|
+
* createdAt: Date | null,
|
|
92
|
+
* }
|
|
93
|
+
* ```
|
|
94
|
+
* ```after
|
|
95
|
+
* type Obj = {
|
|
96
|
+
* createdAt: number | null,
|
|
97
|
+
* }
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
export type ObjectDateFieldsToNumber<O extends object> = {
|
|
101
|
+
[K in keyof O]: O[K] extends Date
|
|
102
|
+
? number
|
|
103
|
+
: (
|
|
104
|
+
O[K] extends Date | undefined
|
|
105
|
+
? number | undefined
|
|
106
|
+
:
|
|
107
|
+
(O[K] extends Date | null
|
|
108
|
+
? number | null
|
|
109
|
+
: (
|
|
110
|
+
O[K] extends object
|
|
111
|
+
? ObjectDateFieldsToNumber<O[K]>
|
|
112
|
+
: O[K]
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Convert all Date fields in an object to number.
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* ```
|
|
122
|
+
* const obj = { createdAt: new Date() };
|
|
123
|
+
* const result = objectDateFieldsToNumber(obj);
|
|
124
|
+
* // Expect: { createdAt: 1712345678901 }
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
127
|
+
export const objectDateFieldsToNumber = <O extends Record<string | number | symbol, unknown>>(
|
|
128
|
+
obj: O
|
|
129
|
+
): ObjectDateFieldsToNumber<O> => {
|
|
130
|
+
// oxlint-disable-next-line guard-for-in
|
|
131
|
+
for (const key in obj) {
|
|
132
|
+
const value = obj[key];
|
|
133
|
+
if (isDate(value)) {
|
|
134
|
+
// oxlint-disable-next-line no-unsafe-type-assertion
|
|
135
|
+
obj[key] = value.getTime() as O[Extract<keyof O, string>]
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// oxlint-disable-next-line no-unsafe-type-assertion
|
|
139
|
+
return obj as ObjectDateFieldsToNumber<O>
|
|
140
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Encoding
|
|
2
|
+
|
|
3
|
+
Runtime utilities for encoding and decoding text. This module currently focuses on Base64 conversions with predictable UTF-8 behavior.
|
|
4
|
+
|
|
5
|
+
## For Users
|
|
6
|
+
|
|
7
|
+
This module provides lightweight encoding helpers for common text <-> Base64 scenarios.
|
|
8
|
+
|
|
9
|
+
### 1. Domain Areas
|
|
10
|
+
|
|
11
|
+
1. Base64: Convert UTF-8 strings to Base64, decode Base64 back to UTF-8 strings, and validate Base64 input.
|
|
12
|
+
|
|
13
|
+
Current public exports:
|
|
14
|
+
|
|
15
|
+
- `isBase64(input: string): boolean`
|
|
16
|
+
- `assertBase64(input: string): void`
|
|
17
|
+
- `stringToBase64(input: string): string`
|
|
18
|
+
- `base64ToString(input: string): string`
|
|
19
|
+
|
|
20
|
+
## For Contributors
|
|
21
|
+
|
|
22
|
+
This guide documents conventions and best practices for implementing encoding utilities in this module.
|
|
23
|
+
|
|
24
|
+
### 1. Documentation and Comments
|
|
25
|
+
|
|
26
|
+
#### 1.1 JSDoc Comment Format
|
|
27
|
+
|
|
28
|
+
Every exported function should include JSDoc in this form:
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
/**
|
|
32
|
+
* Brief one-line description of what the function does.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```
|
|
36
|
+
* // Expect: "aGVsbG8="
|
|
37
|
+
* const example1 = stringToBase64("hello")
|
|
38
|
+
* // Expect: "hello"
|
|
39
|
+
* const example2 = base64ToString("aGVsbG8=")
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export const stringToBase64 = (input: string): string => {
|
|
43
|
+
...
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Documentation Rules:**
|
|
48
|
+
- First line: Clear, concise description starting with a verb (Check, Get, Convert, etc.)
|
|
49
|
+
- Add a blank line after the description
|
|
50
|
+
- Use `@example` tag followed by triple backticks
|
|
51
|
+
- Include multiple cases showing different scenarios
|
|
52
|
+
- Use comment format: `// Expect: <result>`
|
|
53
|
+
- Assign example results to variables like `example1`, `example2` to keep examples readable
|
|
54
|
+
- Place `@see`(if has) after the `@example` block, separated by a blank line
|
|
55
|
+
- Prefer deterministic examples; avoid randomness or time-dependent output in docs
|
|
56
|
+
- If a function returns a non-scalar, show the expected shape or key properties
|
|
57
|
+
|
|
58
|
+
### 2. Runtime Implementation Patterns
|
|
59
|
+
|
|
60
|
+
#### 2.1 Input and Output Rules
|
|
61
|
+
|
|
62
|
+
- Text conversion utilities should accept explicit `string` input.
|
|
63
|
+
- Behavior should be deterministic and side-effect free.
|
|
64
|
+
- Keep UTF-8 semantics explicit in implementation.
|
|
65
|
+
|
|
66
|
+
#### 2.2 Helper Placement
|
|
67
|
+
|
|
68
|
+
- Place local helper constants/functions immediately before the utility they support.
|
|
69
|
+
- Prefix non-exported helpers with `internal`.
|
|
70
|
+
- Never export internal helpers.
|
|
71
|
+
|
|
72
|
+
#### 2.3 Spacing
|
|
73
|
+
|
|
74
|
+
- Separate different utility functions with a single blank line.
|
|
75
|
+
|
|
76
|
+
### 3. Naming Conventions
|
|
77
|
+
|
|
78
|
+
#### 3.1 Function Name Format
|
|
79
|
+
|
|
80
|
+
Use clear operation-oriented names in the `encoding` domain:
|
|
81
|
+
|
|
82
|
+
- `isBase64` for Base64 validation checks returning boolean
|
|
83
|
+
- `assertBase64` for Base64 validation that throws on malformed input
|
|
84
|
+
- `stringToBase64` for UTF-8 string to Base64 conversion
|
|
85
|
+
- `base64ToString` for Base64 to UTF-8 string conversion
|
|
86
|
+
|
|
87
|
+
Prefer names that encode both source and target representations.
|
|
88
|
+
|
|
89
|
+
### 4. Export Strategy
|
|
90
|
+
|
|
91
|
+
- Export all public utilities from domain files.
|
|
92
|
+
- Re-export them through `index.ts`.
|
|
93
|
+
- Do not export internal helpers.
|
|
94
|
+
|
|
95
|
+
### 5. Common Pitfalls to Avoid
|
|
96
|
+
|
|
97
|
+
1. Do not assume all Base64 input is valid without documenting behavior.
|
|
98
|
+
2. Do not mix encodings implicitly (keep UTF-8 explicit).
|
|
99
|
+
3. Do not add environment-specific APIs unless compatibility is documented.
|
|
100
|
+
4. Do not mutate shared state.
|
|
101
|
+
|
|
102
|
+
### 6. Testing Requirements
|
|
103
|
+
|
|
104
|
+
- Write one test per function.
|
|
105
|
+
- If multiple cases are needed, include them within the same test.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
const internalBase64Pattern = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/
|
|
2
|
+
const internalNormalizeBase64 = (input: string): string => {
|
|
3
|
+
return input.replaceAll(/\s+/g, "")
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Check whether input is a valid Base64 string.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```
|
|
10
|
+
* // Expect: true
|
|
11
|
+
* const example1 = isBase64("aGVsbG8=")
|
|
12
|
+
* // Expect: false
|
|
13
|
+
* const example2 = isBase64("abc")
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export const isBase64 = (input: string): boolean => {
|
|
17
|
+
const normalizedInput = internalNormalizeBase64(input)
|
|
18
|
+
return internalBase64Pattern.test(normalizedInput)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const internalAssertValidBase64 = (input: string): void => {
|
|
22
|
+
if (internalBase64Pattern.test(input) === false) {
|
|
23
|
+
throw new TypeError("Invalid Base64 input")
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Assert input is a valid Base64 string.
|
|
28
|
+
*
|
|
29
|
+
* @throws {TypeError} when input is not valid Base64
|
|
30
|
+
*/
|
|
31
|
+
export const assertBase64 = (input: string): void => {
|
|
32
|
+
const normalizedInput = internalNormalizeBase64(input)
|
|
33
|
+
internalAssertValidBase64(normalizedInput)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const internalStringToBase64ByBrowserApi = (input: string): string => {
|
|
37
|
+
const bytes = new TextEncoder().encode(input)
|
|
38
|
+
let binaryString = ""
|
|
39
|
+
|
|
40
|
+
bytes.forEach((byte) => {
|
|
41
|
+
binaryString = binaryString + String.fromCodePoint(byte)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
return btoa(binaryString)
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Convert a UTF-8 string into a Base64 string.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```
|
|
51
|
+
* // Expect: "aGVsbG8="
|
|
52
|
+
* const example1 = stringToBase64("hello")
|
|
53
|
+
* // Expect: "5L2g5aW9"
|
|
54
|
+
* const example2 = stringToBase64("你好")
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export const stringToBase64 = (input: string): string => {
|
|
58
|
+
if (typeof Buffer !== "undefined") {
|
|
59
|
+
return Buffer.from(input, "utf8").toString("base64")
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (typeof btoa !== "undefined") {
|
|
63
|
+
return internalStringToBase64ByBrowserApi(input)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
throw new Error("No Base64 runtime support found")
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const internalBase64ToStringByBrowserApi = (input: string): string => {
|
|
70
|
+
const binaryString = atob(input)
|
|
71
|
+
const bytes = Uint8Array.from(binaryString, char => char.codePointAt(0) ?? 0)
|
|
72
|
+
return new TextDecoder().decode(bytes)
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Convert a valid Base64 string into a UTF-8 string.
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```
|
|
79
|
+
* // Expect: "hello"
|
|
80
|
+
* const example1 = base64ToString("aGVsbG8=")
|
|
81
|
+
* // Expect: "你好"
|
|
82
|
+
* const example2 = base64ToString("5L2g5aW9")
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
export const base64ToString = (input: string): string => {
|
|
86
|
+
const normalizedInput = internalNormalizeBase64(input)
|
|
87
|
+
internalAssertValidBase64(normalizedInput)
|
|
88
|
+
|
|
89
|
+
if (typeof Buffer !== "undefined") {
|
|
90
|
+
return Buffer.from(normalizedInput, "base64").toString("utf8")
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (typeof atob !== "undefined") {
|
|
94
|
+
return internalBase64ToStringByBrowserApi(normalizedInput)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
throw new Error("No Base64 runtime support found")
|
|
98
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./base64.ts"
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Random
|
|
2
|
+
|
|
3
|
+
Runtime utilities for generating random-oriented values with cross-runtime compatibility. This module currently focuses on UUID generation.
|
|
4
|
+
|
|
5
|
+
## For Users
|
|
6
|
+
|
|
7
|
+
This module provides lightweight helpers for random value generation scenarios.
|
|
8
|
+
|
|
9
|
+
### 1. Domain Areas
|
|
10
|
+
|
|
11
|
+
1. UUID: Generate RFC 4122 version-4 UUID strings with runtime-aware fallback behavior.
|
|
12
|
+
|
|
13
|
+
Current public exports:
|
|
14
|
+
|
|
15
|
+
- `isUuid(input: string): boolean`
|
|
16
|
+
- `assertUuid(input: string): void`
|
|
17
|
+
- `getUuidVersion(input: string): number`
|
|
18
|
+
- `generateUuid(): string`
|
|
19
|
+
|
|
20
|
+
## For Contributors
|
|
21
|
+
|
|
22
|
+
This guide documents conventions and best practices for implementing random utilities in this module.
|
|
23
|
+
|
|
24
|
+
### 1. Documentation and Comments
|
|
25
|
+
|
|
26
|
+
#### 1.1 JSDoc Comment Format
|
|
27
|
+
|
|
28
|
+
Every exported function should include JSDoc in this form:
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
/**
|
|
32
|
+
* Brief one-line description of what the function does.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```
|
|
36
|
+
* const example1 = generateUuid()
|
|
37
|
+
* // Expect: true
|
|
38
|
+
* const example2 = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(example1)
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export const generateUuid = (): string => {
|
|
42
|
+
...
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**Documentation Rules:**
|
|
47
|
+
- First line: Clear, concise description starting with a verb (Generate, Check, Convert, etc.)
|
|
48
|
+
- Add a blank line after the description
|
|
49
|
+
- Use `@example` tag followed by triple backticks
|
|
50
|
+
- Include multiple cases showing different scenarios
|
|
51
|
+
- Use comment format: `// Expect: <result>`
|
|
52
|
+
- Assign example results to variables like `example1`, `example2` to keep examples readable
|
|
53
|
+
- Place `@see`(if has) after the `@example` block, separated by a blank line
|
|
54
|
+
- Prefer deterministic examples; avoid asserting exact random outputs
|
|
55
|
+
- If a function returns a structured string, show expected format characteristics
|
|
56
|
+
|
|
57
|
+
### 2. Runtime Implementation Patterns
|
|
58
|
+
|
|
59
|
+
#### 2.1 Compatibility First
|
|
60
|
+
|
|
61
|
+
- Prefer standard runtime APIs when available (for example, `crypto.randomUUID`).
|
|
62
|
+
- Keep fallback logic for environments where modern APIs are unavailable.
|
|
63
|
+
|
|
64
|
+
#### 2.2 Deterministic Interface
|
|
65
|
+
|
|
66
|
+
- Public API shape should remain deterministic even if output values are random.
|
|
67
|
+
- Return values should always conform to the documented output format.
|
|
68
|
+
|
|
69
|
+
#### 2.3 Helper Placement
|
|
70
|
+
|
|
71
|
+
- Place local helper constants/functions immediately before the utility they support.
|
|
72
|
+
- Prefix non-exported helpers with `internal`.
|
|
73
|
+
- Never export internal helpers.
|
|
74
|
+
|
|
75
|
+
#### 2.4 Spacing
|
|
76
|
+
|
|
77
|
+
- Separate different utility functions with a single blank line.
|
|
78
|
+
|
|
79
|
+
### 3. Naming Conventions
|
|
80
|
+
|
|
81
|
+
#### 3.1 Function Name Format
|
|
82
|
+
|
|
83
|
+
Use clear operation-oriented names in the `random` domain:
|
|
84
|
+
|
|
85
|
+
- `isUuid` for UUID format validation checks
|
|
86
|
+
- `assertUuid` for UUID validation that throws on malformed input
|
|
87
|
+
- `getUuidVersion` for extracting UUID version numbers
|
|
88
|
+
- `generateUuid` for UUID creation helpers
|
|
89
|
+
|
|
90
|
+
Prefer names that clearly indicate output format and intent.
|
|
91
|
+
|
|
92
|
+
### 4. Export Strategy
|
|
93
|
+
|
|
94
|
+
- Export all public utilities from domain files.
|
|
95
|
+
- Re-export them through `index.ts`.
|
|
96
|
+
- Do not export internal helpers.
|
|
97
|
+
|
|
98
|
+
### 5. Common Pitfalls to Avoid
|
|
99
|
+
|
|
100
|
+
1. Do not assume browser-only APIs are always available.
|
|
101
|
+
2. Do not couple random utilities to a specific runtime unless documented.
|
|
102
|
+
3. Do not assert exact random results in tests.
|
|
103
|
+
4. Do not mutate shared/global state permanently.
|
|
104
|
+
|
|
105
|
+
### 6. Testing Requirements
|
|
106
|
+
|
|
107
|
+
- Write one test per function.
|
|
108
|
+
- If multiple cases are needed, include them within the same test.
|
|
109
|
+
- For random outputs, validate shape/constraints instead of exact value.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./uuid.ts"
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
|
|
2
|
+
const internalUUID_REGEXP = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
|
|
3
|
+
/**
|
|
4
|
+
* Check whether input is a valid UUID string.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```
|
|
8
|
+
* // Expect: true
|
|
9
|
+
* const example1 = isUuid("550e8400-e29b-41d4-a716-446655440000")
|
|
10
|
+
* // Expect: false
|
|
11
|
+
* const example2 = isUuid("not-a-uuid")
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export const isUuid = (input: string): boolean => {
|
|
15
|
+
return internalUUID_REGEXP.test(input)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Assert input is a valid UUID string.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```
|
|
23
|
+
* // Expect: no throw
|
|
24
|
+
* const example1 = assertUuid("550e8400-e29b-41d4-a716-446655440000")
|
|
25
|
+
* // Expect: throws TypeError
|
|
26
|
+
* const example2 = () => assertUuid("not-a-uuid")
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* @throws {TypeError} when input is not a valid UUID
|
|
30
|
+
*/
|
|
31
|
+
export const assertUuid = (input: string): void => {
|
|
32
|
+
if (isUuid(input) === false) {
|
|
33
|
+
throw new TypeError(`Expected a valid UUID string, got: ${input}`)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Get the version number from a valid UUID string.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```
|
|
42
|
+
* // Expect: 4
|
|
43
|
+
* const example1 = getUuidVersion("550e8400-e29b-41d4-a716-446655440000")
|
|
44
|
+
* // Expect: 1
|
|
45
|
+
* const example2 = getUuidVersion("123e4567-e89b-12d3-a456-426614174000")
|
|
46
|
+
* ```
|
|
47
|
+
*
|
|
48
|
+
* @throws {TypeError} when input is not a valid UUID
|
|
49
|
+
*/
|
|
50
|
+
export const getUuidVersion = (input: string): number => {
|
|
51
|
+
// 1) Ensure the input is a syntactically valid UUID string.
|
|
52
|
+
// If invalid, assertUuid throws TypeError and prevents unsafe parsing.
|
|
53
|
+
assertUuid(input)
|
|
54
|
+
|
|
55
|
+
// 2) Per UUID canonical format xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx,
|
|
56
|
+
// the version nibble is the first hex digit of the 3rd group.
|
|
57
|
+
// In a 36-char UUID string, that position is index 14.
|
|
58
|
+
|
|
59
|
+
// 3) Convert the single hex character (for example "4") to a base-10 number.
|
|
60
|
+
// parseInt("4", 16) -> 4, parseInt("a", 16) -> 10.
|
|
61
|
+
return Number.parseInt(input[14]!, 16)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Generate a RFC 4122 version-4 UUID string.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```
|
|
69
|
+
* const example1 = generateUuid()
|
|
70
|
+
* // Expect: true
|
|
71
|
+
* const example2 = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(example1)
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export const generateUuid = (): string => {
|
|
75
|
+
if (typeof crypto === "object") {
|
|
76
|
+
if (typeof crypto.randomUUID === "function") {
|
|
77
|
+
return crypto.randomUUID()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (typeof crypto.getRandomValues === "function" && typeof Uint8Array === "function") {
|
|
81
|
+
const buffer = new Uint8Array(16)
|
|
82
|
+
crypto.getRandomValues(buffer)
|
|
83
|
+
|
|
84
|
+
// Per RFC 4122, set bits for version and `clock_seq_hi_and_reserved`
|
|
85
|
+
buffer[6] = (buffer[6]! & 0x0F) | 0x40 // version 4
|
|
86
|
+
buffer[8] = (buffer[8]! & 0x3F) | 0x80 // variant 1
|
|
87
|
+
|
|
88
|
+
// Convert buffer to UUID string format
|
|
89
|
+
const hex = [...buffer].map(b => b.toString(16).padStart(2, "0")).join("")
|
|
90
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Fallback for environments without crypto support
|
|
95
|
+
const fallbackUUID = (): string => {
|
|
96
|
+
const random = (a: number): string => {
|
|
97
|
+
return ((a ^ ((Math.random() * 16) >> (a / 4))) & 15).toString(16)
|
|
98
|
+
}
|
|
99
|
+
return "10000000-1000-4000-8000-100000000000".replaceAll(/[018]/g, (char: string) => random(Number(char)))
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return fallbackUUID()
|
|
103
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { expect, test } from "vitest"
|
|
2
2
|
|
|
3
|
-
import { excludeFields, includeFields } from "#Source/basic/object.ts"
|
|
3
|
+
import { excludeFields, includeFields, objectDateFieldsToNumber } from "#Source/basic/object.ts"
|
|
4
4
|
|
|
5
5
|
test("includeFields picks specified keys", () => {
|
|
6
6
|
expect(includeFields({ a: 1, b: 2, c: 3 }, ["a", "c"])).toEqual({ a: 1, c: 3 })
|
|
@@ -13,3 +13,34 @@ test("excludeFields omits specified keys", () => {
|
|
|
13
13
|
// @ts-expect-error - Testing behavior with undefined input
|
|
14
14
|
expect(excludeFields(undefined, ["a"])).toEqual({})
|
|
15
15
|
})
|
|
16
|
+
|
|
17
|
+
test("objectDateFieldsToNumber converts top-level Date fields", () => {
|
|
18
|
+
const createdAt = new Date("2024-01-02T03:04:05.678Z")
|
|
19
|
+
const updatedAt = new Date("2024-02-03T04:05:06.789Z")
|
|
20
|
+
const nestedDate = new Date("2024-03-04T05:06:07.890Z")
|
|
21
|
+
|
|
22
|
+
const source = {
|
|
23
|
+
id: "x-1",
|
|
24
|
+
createdAt,
|
|
25
|
+
updatedAt,
|
|
26
|
+
nested: {
|
|
27
|
+
occurredAt: nestedDate,
|
|
28
|
+
},
|
|
29
|
+
optional: undefined as Date | undefined,
|
|
30
|
+
nullable: null as Date | null,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const result = objectDateFieldsToNumber(source)
|
|
34
|
+
|
|
35
|
+
expect(result).toEqual({
|
|
36
|
+
id: "x-1",
|
|
37
|
+
createdAt: createdAt.getTime(),
|
|
38
|
+
updatedAt: updatedAt.getTime(),
|
|
39
|
+
nested: {
|
|
40
|
+
occurredAt: nestedDate,
|
|
41
|
+
},
|
|
42
|
+
optional: undefined,
|
|
43
|
+
nullable: null,
|
|
44
|
+
})
|
|
45
|
+
expect(result).toBe(source)
|
|
46
|
+
})
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { expect, test } from "vitest"
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
assertBase64,
|
|
5
|
+
base64ToString,
|
|
6
|
+
isBase64,
|
|
7
|
+
stringToBase64,
|
|
8
|
+
} from "#Source/encoding/index.ts"
|
|
9
|
+
|
|
10
|
+
test("isBase64 validates Base64 input", () => {
|
|
11
|
+
expect(isBase64("aGVsbG8=")).toBe(true)
|
|
12
|
+
expect(isBase64("5L2g5aW9")).toBe(true)
|
|
13
|
+
expect(isBase64("8J+Ri/CfjI0=")).toBe(true)
|
|
14
|
+
expect(isBase64("aGVs\n bG8=")).toBe(true)
|
|
15
|
+
|
|
16
|
+
expect(isBase64("%%%")).toBe(false)
|
|
17
|
+
expect(isBase64("abc")).toBe(false)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test("assertBase64 throws on malformed input", () => {
|
|
21
|
+
expect(() => assertBase64("aGVsbG8=")).not.toThrow()
|
|
22
|
+
expect(() => assertBase64("aGVs\n bG8=")).not.toThrow()
|
|
23
|
+
expect(() => assertBase64("%%%")).toThrow(TypeError)
|
|
24
|
+
expect(() => assertBase64("abc")).toThrow(TypeError)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test("stringToBase64 converts UTF-8 text to Base64", () => {
|
|
28
|
+
expect(stringToBase64("hello")).toBe("aGVsbG8=")
|
|
29
|
+
expect(stringToBase64("你好")).toBe("5L2g5aW9")
|
|
30
|
+
expect(stringToBase64("👋🌍")).toBe("8J+Ri/CfjI0=")
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test("base64ToString converts Base64 to UTF-8 text and validates malformed input", () => {
|
|
34
|
+
expect(base64ToString("aGVsbG8=")).toBe("hello")
|
|
35
|
+
expect(base64ToString("5L2g5aW9")).toBe("你好")
|
|
36
|
+
expect(base64ToString("8J+Ri/CfjI0=")).toBe("👋🌍")
|
|
37
|
+
|
|
38
|
+
expect(() => base64ToString("%%%")).toThrow(TypeError)
|
|
39
|
+
expect(() => base64ToString("abc")).toThrow(TypeError)
|
|
40
|
+
})
|