-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
24 changed files
with
467 additions
and
487 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import { test, expect } from 'vitest' | ||
import { min, max, between } from './string' | ||
|
||
test('min length string', () => { | ||
expect(min(3).decode('123')).toBeRight() | ||
expect(min(3).decode('1234')).toBeRight() | ||
expect(min(3).decode('12')).toBeLeft() | ||
}) | ||
test('max length string', () => { | ||
expect(max(5).decode('1234')).toBeRight() | ||
expect(max(5).decode('12345')).toBeRight() | ||
expect(max(5).decode('123456')).toBeLeft() | ||
}) | ||
test('between string length', () => { | ||
expect(between(3, 5).decode('123')).toBeRight() | ||
expect(between(3, 5).decode('1234')).toBeRight() | ||
expect(between(3, 5).decode('12345')).toBeRight() | ||
expect(between(3, 5).decode('123456')).toBeLeft() | ||
expect(between(3, 5).decode('12')).toBeLeft() | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import { pipe } from 'fp-ts/lib/function' | ||
import * as t from 'io-ts' | ||
import * as D from 'io-ts/Decoder' | ||
|
||
export type MinBrand<N extends number> = { | ||
readonly Min: unique symbol | ||
readonly min: N | ||
} | ||
|
||
export type Min<N extends number> = string & MinBrand<N> | ||
|
||
export const min = <N extends number>(min: N) => | ||
D.fromRefinement( | ||
(s: string): s is Min<N> => s.length >= min, | ||
`at lest ${min} characters long` | ||
) | ||
|
||
export type MaxBrand<N extends number> = { | ||
readonly Max: unique symbol | ||
readonly max: N | ||
} | ||
|
||
export type Max<N extends number> = string & MaxBrand<N> | ||
|
||
export const max = <N extends number>(max: N) => | ||
D.fromRefinement( | ||
(s: string): s is Max<N> => s.length <= max, | ||
`at most ${max} characters long` | ||
) | ||
|
||
export const between = <Low extends number, Hi extends number>( | ||
low: Low, | ||
hi: Hi | ||
) => pipe(min(low), D.intersect(max(hi))) |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { test, expect } from 'vitest' | ||
import { v4 } from 'uuid' | ||
import { UUID, generate } from './uuid' | ||
|
||
test('decoding valid UUID', () => { | ||
expect(UUID.decode(v4())).toBeRight() | ||
}) | ||
test('decoding invalid uuid', () => { | ||
expect(UUID.decode('abc')).toBeLeft() | ||
}) | ||
test('generating UUID', () => { | ||
expect(UUID.decode(generate())).toBeRight() | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,34 +1,17 @@ | ||
import { v4 as uuidv4 } from 'uuid' | ||
import { Opaque, UnwrapOpaque } from 'type-fest' | ||
import { z } from 'zod' | ||
import * as E from 'fp-ts/Either' | ||
import { v4 as UUIDv4 } from 'uuid' | ||
import { pipe } from 'fp-ts/lib/function' | ||
import * as D from 'io-ts/Decoder' | ||
import { validate as uuidValidate } from 'uuid' | ||
|
||
type URI = 'UUID' | ||
|
||
export class InvalidUUIDError extends Error { | ||
public _tag: 'InvalidUUIDError' | ||
private constructor(value: string) { | ||
super(`${value} is not a valid UUID`) | ||
this._tag = 'InvalidUUIDError' | ||
} | ||
|
||
public static of(value: string) { | ||
return new InvalidUUIDError(value) | ||
} | ||
export type UUIDBrand = { | ||
readonly UUID: unique symbol | ||
} | ||
|
||
export type UUID = Opaque<string, URI> | ||
|
||
export const parse = (uuid: string = uuidv4()) => { | ||
const result = z.string().uuid().safeParse(uuid) | ||
export type UUID = string & UUIDBrand | ||
|
||
return result.success | ||
? E.right(result.data as UUID) | ||
: E.left(InvalidUUIDError.of(uuid)) | ||
} | ||
|
||
export const create = () => { | ||
return uuidv4() as UUID | ||
} | ||
export const UUID: D.Decoder<unknown, UUID> = pipe( | ||
D.string, | ||
D.refine((s): s is UUID => uuidValidate(s), 'UUID invalid') | ||
) | ||
|
||
export const value = (uuid: UUID): UnwrapOpaque<UUID> => uuid | ||
export const generate = () => UUIDv4() as UUID |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import * as D from 'io-ts/Decoder' | ||
|
||
export class FactionDecodingError extends Error { | ||
public _tag: 'FactionDecodingError' | ||
public decodeError: D.DecodeError | ||
constructor(error: D.DecodeError) { | ||
super('error decoding faction') | ||
this._tag = 'FactionDecodingError' | ||
this.decodeError = error | ||
} | ||
|
||
public static of(error: D.DecodeError) { | ||
return new FactionDecodingError(error) | ||
} | ||
} | ||
|
||
export class FactionNameAlreadyExistsError extends Error { | ||
public _tag: 'FactionNameAlreadyExistsError' | ||
private constructor(name: string) { | ||
super(`Faction name: ${name} aready exists`) | ||
this._tag = 'FactionNameAlreadyExistsError' | ||
} | ||
|
||
public static of(name: string) { | ||
return new FactionNameAlreadyExistsError(name) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,17 +1,16 @@ | ||
import * as UUID from '../../common/uuid' | ||
import * as E from 'fp-ts/Either' | ||
import { Opaque, UnwrapOpaque } from 'type-fest' | ||
import { flow, pipe } from 'fp-ts/lib/function' | ||
import { UUID, generate as generateUUID } from '../../common/uuid' | ||
import * as D from 'io-ts/Decoder' | ||
import { pipe } from 'fp-ts/lib/function' | ||
|
||
export type FactionId = Opaque<UnwrapOpaque<UUID.UUID>, 'FactionId'> | ||
|
||
type Create = (factionId: string) => E.Either<UUID.InvalidUUIDError, FactionId> | ||
type FactionIdBrand = { | ||
readonly FactionId: unique symbol | ||
} | ||
|
||
const _tag = (factionId: UUID.UUID): FactionId => | ||
UUID.value(factionId) as FactionId | ||
export type FactionId = UUID & FactionIdBrand | ||
|
||
export const parse: Create = (factionId) => { | ||
return pipe(factionId, UUID.parse, E.map(_tag)) | ||
} | ||
export const FactionId: D.Decoder<unknown, FactionId> = pipe( | ||
UUID, | ||
D.refine((s): s is FactionId => true, 'FactionId') | ||
) | ||
|
||
export const create = flow(UUID.create, _tag) | ||
export const generate: () => FactionId = () => generateUUID() as FactionId |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,35 +1,30 @@ | ||
import { describe, it, expect } from 'vitest' | ||
import { toValidFactionNameTE, validateFactionTE } from './implementation' | ||
import { UnvalidatedFaction } from './types' | ||
import { describe, test, expect } from 'vitest' | ||
import * as TE from 'fp-ts/TaskEither' | ||
import { validateFaction } from './implementation' | ||
import { FactionDecodingError, FactionNameAlreadyExistsError } from './errors' | ||
|
||
describe('toValidFactionName', () => { | ||
it('should pass a name that does not exist already', async () => { | ||
const checkFactionExists = () => TE.right(false) | ||
const factionName = 'Goliath' | ||
const actual = await toValidFactionNameTE(checkFactionExists)(factionName)() | ||
expect(actual).toStrictEqualRight(factionName) | ||
describe('validateFaction', () => { | ||
test('valid faction', async () => { | ||
const validInput = { name: 'Goliath' } | ||
const mockCheckFactionNameExists = () => TE.right(false) | ||
expect( | ||
await validateFaction(mockCheckFactionNameExists)(validInput)() | ||
).toStrictEqualRight({ id: expect.any(String), name: validInput.name }) | ||
}) | ||
it('should reject pre-existing faction', async () => { | ||
const checkFactionExists = () => TE.right(true) | ||
const factionName = 'Escher' | ||
|
||
test('invalid faction format', async () => { | ||
const missingName = {} as any | ||
const mockCheckFactionNameExists = () => TE.right(false) | ||
expect( | ||
await toValidFactionNameTE(checkFactionExists)(factionName)() | ||
).toBeLeft() | ||
await validateFaction(mockCheckFactionNameExists)(missingName)() | ||
).toStrictEqualLeft(expect.any(FactionDecodingError)) | ||
}) | ||
}) | ||
|
||
describe('validateFaction', () => { | ||
it('should pass a valid faction', async () => { | ||
const unvalidatedFaction: UnvalidatedFaction = { | ||
name: 'Orlock', | ||
} | ||
const checkFactionExists = () => TE.right(false) | ||
test('faction already exists', async () => { | ||
const faction = { name: 'Goliath' } | ||
const mockCheckFactionNameExists = () => TE.right(true) | ||
expect( | ||
await validateFactionTE(checkFactionExists)(unvalidatedFaction)() | ||
).toStrictEqualRight({ | ||
id: expect.any(String), | ||
name: unvalidatedFaction.name, | ||
}) | ||
await validateFaction(mockCheckFactionNameExists)(faction)() | ||
).toStrictEqualLeft(expect.any(FactionNameAlreadyExistsError)) | ||
}) | ||
}) |
Oops, something went wrong.