import {Integer, isInteger} from './types' import {ErrorWithContext} from '../error/ErrorWithContext' import {JSONState} from './Rehydratable' import {isJSON} from './data' export class Safe { protected thrower: (message: string, value: unknown) => never constructor( protected readonly value: unknown, ) { this.thrower = (message) => { throw new ErrorWithContext('Invalid value', { message, value, }) } } onError(thrower: (message: string, value: unknown) => never): this { this.thrower = thrower return this } present(): this { if ( !this.value && this.value !== 0 && this.value !== false ) { this.thrower('Missing value', this.value) } return this } integer(): Integer { const value = parseInt(String(this.value), 10) if ( !isInteger(value) ) { this.thrower('Invalid integer', this.value) } return value } number(): number { const value = parseFloat(String(this.value)) if ( isNaN(value) ) { this.thrower('Invalid number', this.value) } return value } string(): string { this.present() return String(this.value) } json(): JSONState { const str = this.string() if ( !isJSON(str) ) { this.thrower('Invalid JSON', str) } return JSON.parse(str) } or(other: unknown): Safe { if ( !this.value && this.value !== 0 && this.value !== false ) { return new Safe(other) } return this } in(allowed: T[]): T { if ( allowed.includes(this.value as any) ) { return this.value as T } this.thrower('Invalid value', this.value) } }