Skip to content

Commit

Permalink
feat(nominal-typebox): add brandedRegExp
Browse files Browse the repository at this point in the history
Signed-off-by: Andres Correa Casablanca <andreu@kindspells.dev>
  • Loading branch information
castarco committed Aug 11, 2024
1 parent c5b8a9e commit a4827fa
Show file tree
Hide file tree
Showing 12 changed files with 159 additions and 53 deletions.
2 changes: 1 addition & 1 deletion @coderspirit/nominal-inputs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"devDependencies": {
"@arethetypeswrong/cli": "^0.15.4",
"@biomejs/biome": "1.8.3",
"@types/node": "^22.1.0",
"@types/node": "^22.2.0",
"get-tsconfig": "^4.7.6",
"publint": "^0.2.9",
"rollup": "^4.20.0",
Expand Down
2 changes: 1 addition & 1 deletion @coderspirit/nominal-symbols/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"devDependencies": {
"@arethetypeswrong/cli": "^0.15.4",
"@biomejs/biome": "1.8.3",
"@types/node": "^22.1.0",
"@types/node": "^22.2.0",
"get-tsconfig": "^4.7.6",
"publint": "^0.2.9",
"rollup": "^4.20.0",
Expand Down
1 change: 1 addition & 0 deletions @coderspirit/nominal-typebox/.node-version
51 changes: 45 additions & 6 deletions @coderspirit/nominal-typebox/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ yarn add @coderspirit/nominal-typebox

## Usage instructions

### Typebox' Type.String -> brandedString
### TypeBox' Type.String -> brandedString

```typescript
import type { FastBrand } from '@coderspirit/nominal'
Expand Down Expand Up @@ -61,7 +61,42 @@ const username: Username = requestObject.username // OK
const corruptedUserame: Username = 'untagged string' // type error
```

### Typebox' Type.Number -> brandedNumber
### TypeBox' Type.RegExp -> brandedRegExp

```typescript

import type { FastBrand } from '@coderspirit/nominal'
import { brandedRegExp } from '@coderspirit/nominal-typebox'

import { Object as TBObject } from '@sinclair/typebox'
import { TypeCompiler } from '@sinclair/typebox/compiler'

type UserId = FastBrand<string, 'UserId'>

// Use `brandedString` instead of Typebox' `Type.String`
const requestSchema = TBObject({
// We can pass the same options Type.String has
userId: brandedRegExp<'UserId'>(
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
)
})
const requestValidator = TypeCompiler.Compile(requestSchema)

const requestObject = getRequestFromSomewhere() // unknown
if (!requestValidator.Check(requestObject)) {
throw new Error('Invalid request!')
}

// At this point, the type checker knows that requestObject.username is
// "branded" as 'Username'

const userId: UserId = requestObject.userId // OK
const corruptedUserId: UserId = 'untagged (and probably wrong) id' // type error
```

---

### TypeBox' Type.Number -> brandedNumber


```typescript
Expand Down Expand Up @@ -93,12 +128,14 @@ const corruptedLat: Latitude = 10 // type error
const corruptedLon: Longitude = 10 // type error
```

### Typebox' Type.Integer -> brandedInteger
### TypeBox' Type.Integer -> brandedInteger

The same applies as for the two previous examples, you can use `brandedInteger`
instead of Typebox' `Type.Integer`.

### Typebox' Type.Array -> brandedArray
---

### TypeBox' Type.Array -> brandedArray

`brandedArray` has the same signature as Typebox' `Type.Array`, except that we
have to pass a "brand" string argument as its first parameter:
Expand All @@ -115,7 +152,7 @@ const arraySchema = brandedArray(
)
```

### Typebox' Type.Object -> brandedObject
### TypeBox' Type.Object -> brandedObject

`brandedObject` has the same signature as Typebox' `Type.Object`, except that we
have to pass a "brand" string argument as its first parameter:
Expand All @@ -134,7 +171,7 @@ const objectSchema = brandedObject(
)
```

### Typebox' Type.Union -> brandedUnion
### TypeBox' Type.Union -> brandedUnion

`brandedUnion` has the same signature as Typebox' `Type.Union`, except that we
have to pass a "brand" string argument as its first parameter:
Expand All @@ -149,6 +186,8 @@ const unionSchema = brandedUnion(
)
```

---

### Fallback alternative

In case this library does not provide a specific schema factory for your type,
Expand Down
6 changes: 3 additions & 3 deletions @coderspirit/nominal-typebox/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@coderspirit/nominal-typebox",
"version": "1.0.0",
"version": "1.1.0",
"description": "Integration of @coderspirit/nominal with @sinclair/typebox",
"main": "./dist/main.cjs",
"module": "./dist/main.mjs",
Expand Down Expand Up @@ -48,8 +48,8 @@
"@arethetypeswrong/cli": "^0.15.4",
"@biomejs/biome": "1.8.3",
"@coderspirit/nominal-inputs": "workspace:^",
"@sinclair/typebox": "^0.33.3",
"@types/node": "^22.1.0",
"@sinclair/typebox": "^0.33.4",
"@types/node": "^22.2.0",
"@vitest/coverage-v8": "^2.0.5",
"get-tsconfig": "^4.7.6",
"publint": "^0.2.9",
Expand Down
2 changes: 2 additions & 0 deletions @coderspirit/nominal-typebox/src/main.mts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export type { BrandedSchema } from './schema.mts'
// Basic types
export type { BrandedIntegerSchema, BrandedNumberSchema } from './number.mts'
export type { BrandedStringSchema } from './string.mts'
export type { BrandedRegExpSchema } from './regexp.mts'

// Complex types
export type { BrandedArraySchema } from './array.mts'
Expand All @@ -16,6 +17,7 @@ export { brandedSchema } from './schema.mts'
// Basic schemas
export { brandedInteger, brandedNumber } from './number.mts'
export { brandedString } from './string.mts'
export { brandedRegExp } from './regexp.mts'

// Complex schemas
export { brandedArray } from './array.mts'
Expand Down
31 changes: 31 additions & 0 deletions @coderspirit/nominal-typebox/src/regexp.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { FastBrand } from '@coderspirit/nominal'
import type { Kind, RegExpOptions, TSchema } from '@sinclair/typebox'
import { RegExp as TBRegExp } from '@sinclair/typebox'

export interface BrandedRegExpSchema<B extends string> extends TSchema {
// Copied from TRegExp
[Kind]: 'RegExp'
type: 'RegExp'
source: string
flags: string

// Our sauce
static: FastBrand<string, B>
}

function brandedRegExp<const B extends string>(
regex: RegExp,
options?: RegExpOptions,
): BrandedRegExpSchema<B>
function brandedRegExp<const B extends string>(
regex: string,
options?: RegExpOptions,
): BrandedRegExpSchema<B>
function brandedRegExp<const B extends string>(
pattern: string | RegExp,
options?: RegExpOptions,
): BrandedRegExpSchema<B> {
return TBRegExp(pattern as string, options) as BrandedRegExpSchema<B>
}

export { brandedRegExp }
32 changes: 32 additions & 0 deletions @coderspirit/nominal-typebox/src/tests/regexp.test.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import assert from 'node:assert/strict'

import type { FastBrand } from '@coderspirit/nominal'
import { TypeCompiler } from '@sinclair/typebox/compiler'
import { describe, expect, it } from 'vitest'

import { brandedRegExp } from '../main.mjs'

describe('brandedRegExp', () => {
it('lets typebox to annotate a regexp with a brand', () => {
const regexpSchema = brandedRegExp<'UUID'>(
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/,
)
const regexpValidator = TypeCompiler.Compile(regexpSchema)

const valueToCheck = '09e77e4a-175d-46a1-b6c6-ff960c30193b'
if (!regexpValidator.Check(valueToCheck)) {
throw new assert.AssertionError({ message: 'validation should pass' })
}

const regexpSink: FastBrand<string, 'UUID'> = valueToCheck
expect(regexpSink).toBe('09e77e4a-175d-46a1-b6c6-ff960c30193b')

// We perform the following useless assignments to show the contrast between
// tagged values and untagged values.

// @ts-expect-error
const corruptedRegexpSink: FastBrand<string, 'UUID'> =
'09e77e4a-175d-46a1-b6c6-ff960c30193b'
expect(corruptedRegexpSink).toBe('09e77e4a-175d-46a1-b6c6-ff960c30193b')
})
})
2 changes: 1 addition & 1 deletion @coderspirit/nominal/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"devDependencies": {
"@arethetypeswrong/cli": "^0.15.4",
"@biomejs/biome": "1.8.3",
"@types/node": "^22.1.0",
"@types/node": "^22.2.0",
"@vitest/coverage-v8": "^2.0.5",
"get-tsconfig": "^4.7.6",
"publint": "^0.2.9",
Expand Down
5 changes: 3 additions & 2 deletions @coderspirit/nominal/rollup.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const tsconfig = getTsconfig(projectDir)
const target = tsconfig?.config.compilerOptions?.target ?? 'es2020'

const input = 'src/main.mts'
const external = ['@coderspirit/nominal-symbols']

export default defineConfig([
{
Expand All @@ -26,7 +27,7 @@ export default defineConfig([
minify: true,
}),
],
external: ['@coderspirit/nominal-symbols'],
external,
},
{
input,
Expand All @@ -35,6 +36,6 @@ export default defineConfig([
{ format: 'esm', file: 'dist/main.d.mts' },
],
plugins: [dts()],
external: ['@coderspirit/nominal-symbols'],
external,
},
])
2 changes: 1 addition & 1 deletion @coderspirit/safe-env/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
"devDependencies": {
"@arethetypeswrong/cli": "^0.15.4",
"@biomejs/biome": "1.8.3",
"@types/node": "^22.1.0",
"@types/node": "^22.2.0",
"@vitest/coverage-v8": "^2.0.5",
"get-tsconfig": "^4.7.6",
"publint": "^0.2.9",
Expand Down
Loading

0 comments on commit a4827fa

Please sign in to comment.