/* eslint-disable @typescript-eslint/ban-types */ import { pipe } from "@effect-ts-app/core/Function" import type { ApiSelfType, DefaultSchema, NonEmptyString, Parser, SchemaDefaultSchema, SchemaUPI, Utils } from "@effect-ts-app/schema" import { annotate, brand, EParserFor, extendWithUtils, extendWithUtilsAnd, leafE, literal, makeAnnotation, named, nonEmptyStringFromString, prop, refine } from "@effect-ts-app/schema" import type { Refinement } from "@effect-ts/core/Function" import type * as FC from "fast-check" import { customRandom, nanoid, urlAlphabet } from "nanoid" import validator from "validator" import { curriedMagix } from "../Function.js" import type { ParsedShapeOfCustom, ReasonableStringBrand, UnionBrand } from "./_schema.js" import { Arbitrary, arbitrary, customE, Email as Email_, fakerArb, fromString, makeConstrainedFromString, PositiveInt, ReasonableString, string, stringNumber, withDefaults } from "./_schema.js" import * as Eq from "@effect-ts/core/Equal" export function tag(tag: K) { return prop(literal(tag)) } export const stringPositiveIntIdentifier = makeAnnotation<{}>() export const StringPositiveInt: DefaultSchema = pipe( stringNumber[">>>"](PositiveInt), withDefaults, annotate(stringPositiveIntIdentifier, {}) ) export type StringPositiveInt = PositiveInt /** * A string that is at least 3 character long and a maximum of 50. */ export interface ShortStringBrand extends ReasonableStringBrand { readonly ShortString: unique symbol } /** * A string that is at least 3 character long and a maximum of 50. */ export type ShortString = string & ShortStringBrand /** * A string that is at least 3 character long and a maximum of 50. */ export const shortStringFromString = pipe( makeConstrainedFromString(3, 50), arbitrary(FC => FC.lorem({ mode: "words", maxCount: 2 }) .filter(x => x.length < 50 && x.length >= 3) .map(x => x as ShortString) ), // arbitrary removes brand benefit brand() ) /** * A string that is at least 3 character long and a maximum of 50. */ export const ShortString = extendWithUtils( pipe(string[">>>"](shortStringFromString), brand()) ) /** * A string that is at least 3 character long and a maximum of 255. */ export interface ReasonableString3Brand extends ReasonableStringBrand { readonly ReasonableString3: unique symbol } /** * A string that is at least 3 character long and a maximum of 255. */ export type ReasonableString3 = string & ReasonableString3Brand /** * A string that is at least 3 character long and a maximum of 255. */ export const reasonableString3FromString = pipe( makeConstrainedFromString(3, 255), arbitrary(FC => FC.lorem({ mode: "words", maxCount: 2 }) .filter(x => x.length < 255 && x.length >= 3) .map(x => x as ReasonableString3) ), // arbitrary removes brand benefit brand() ) /** * A string that is at least 3 character long and a maximum of 255. */ export const ReasonableString3 = extendWithUtils( pipe(string[">>>"](reasonableString3FromString), brand()) ) /** * A string that is at least 6 characters long and a maximum of 50. */ export interface StringIdBrand extends ReasonableStringBrand { readonly StringId: unique symbol } /** * A string that is at least 6 characters long and a maximum of 50. */ export type StringId = string & StringIdBrand const MIN_LENGTH = 6 const MAX_LENGTH = 50 const size = 21 const length = 10 * size export const stringIdFromString = pipe( makeConstrainedFromString(MIN_LENGTH, MAX_LENGTH), arbitrary(FC => FC.uint8Array({ minLength: length, maxLength: length }) .map(_ => customRandom(urlAlphabet, size, size => _.subarray(0, size))() as StringId) ), // arbitrary removes the benefit of Brand, brand() ) /** * A string that is at least 6 characters long and a maximum of 50. */ export interface StringIdSchema extends SchemaDefaultSchema< unknown, StringId, string, string, ApiSelfType > {} const StringIdSchema: StringIdSchema = string[">>>"](stringIdFromString)["|>"]( brand() ) export const StringId = extendWithUtilsAnd(StringIdSchema, () => ({ make(this: void): StringId { return nanoid() as unknown as StringId } })) const stringIdArb = Arbitrary.for(StringId) export const prefixedStringIdUnsafe = (prefix: string) => StringId(prefix + StringId.make()) export const prefixedStringIdUnsafeThunk = (prefix: string) => () => prefixedStringIdUnsafe(prefix) export interface PrefixedStringIdSchema< Brand extends StringId, Prefix extends string, Separator extends string > extends SchemaWithUtils< SchemaDefaultSchema> >, PrefixedStringUtils {} export type SchemaWithUtils = Schema & Utils export function prefixedStringId() { return ( prefix: Prefix, name: string, separator?: Separator ): PrefixedStringIdSchema => { type FullPrefix = `${Prefix}${Separator}` // type PrefixedId = `${FullPrefix}${string}` const pref = `${prefix}${separator ?? "-"}` as FullPrefix const refinement = (x: StringId): x is Brand => x.startsWith(pref) const fromString = pipe( stringIdFromString, refine( refinement, n => leafE(customE(n, `a StringId prefixed with '${pref}'`)) ), arbitrary(FC => stringIdArb(FC).map( x => (pref + x.substring(0, MAX_LENGTH - pref.length)) as Brand ) ) ) const schema = string[">>>"](fromString)["|>"](named(name))["|>"](brand()) return extendWithUtilsAnd( schema, (ex): PrefixedStringUtils => ({ EParser: EParserFor(ex), create: () => (pref + StringId.make()) as Brand, /** * Automatically adds the prefix. */ unsafeFrom: (str: string) => ex(pref + str), /** * Must provide a literal string starting with prefix. */ prefixSafe: (str: `${Prefix}${Separator}${REST}`) => ex(str), is: refinement, prefix, eq: Eq.string as Equal }) ) } } export interface PrefixedStringUtils< Brand extends StringId, Prefix extends string, Separator extends string > { readonly EParser: Parser.Parser readonly create: () => Brand readonly unsafeFrom: (str: string) => Brand prefixSafe: (str: `${Prefix}${Separator}${REST}`) => Brand readonly is: (x: StringId) => x is Brand readonly prefix: Prefix eq: Equal } export interface UrlBrand { readonly Url: unique symbol } export type Url = NonEmptyString & UrlBrand // eslint-disable-next-line @typescript-eslint/ban-types export const UrlFromStringIdentifier = makeAnnotation<{}>() const isUrl: Refinement = (s: string): s is Url => { return validator.default.isURL(s, { require_tld: false }) } // eslint-disable-next-line @typescript-eslint/ban-types export const UrlFromString: DefaultSchema = pipe( fromString, arbitrary(FC => FC.webUrl()), refine(isUrl, n => leafE(customE(n, "a valid Web URL according to | RFC 3986 and | WHATWG URL Standard"))), brand(), annotate(UrlFromStringIdentifier, {}) ) // eslint-disable-next-line @typescript-eslint/ban-types export const UrlIdentifier = makeAnnotation<{}>() export const Url = extendWithUtils( pipe( string[">>>"](UrlFromString), // eslint-disable-next-line @typescript-eslint/unbound-method arbitrary(FC => fakerArb(faker => faker.internet.url)(FC) as FC.Arbitrary), brand(), annotate(UrlIdentifier, {}) ) ) export const avatarUrl = pipe(string[">>>"](nonEmptyStringFromString)) ["|>"]( arbitrary( // eslint-disable-next-line @typescript-eslint/unbound-method FC => fakerArb(faker => faker.internet.avatar)(FC) as FC.Arbitrary ) ) ["|>"](brand()) export type avatarUrl = NonEmptyString & UnionBrand export const customUrlFromString = (pool: readonly Url[]) => pipe( UrlFromString, arbitrary(FC => FC.oneof(...pool.map(FC.constant))), brand() ) export const customUrl = (pool: readonly Url[]) => pipe(string[">>>"](customUrlFromString(pool)), brand()) // for now be less restrictive about the PhoneNumber const PhoneNumber_ = StringId export const PhoneNumber = PhoneNumber_["|>"]( arbitrary(FC => // eslint-disable-next-line @typescript-eslint/unbound-method fakerArb(faker => faker.phone.phoneNumber)(FC).map(x => x as StringId) ) )["|>"](brand()) export type PhoneNumber = StringId & UnionBrand const endsWith = curriedMagix( (e: Email) => (s: string) => e.toLowerCase().endsWith(s.toLowerCase()) ) const Email__ = Object.assign( extendWithUtils( Email_["|>"]( arbitrary(FC => // eslint-disable-next-line @typescript-eslint/unbound-method fakerArb(faker => faker.internet.email)(FC).map(x => x as Email) ) )["|>"](brand()) ), { eq: { equals: (a: Email, b: Email) => a.toLowerCase() === b.toLowerCase() }, endsWith, toDomain: (email: Email) => ReasonableString(email.split("@")[1]), isDomain: curriedMagix( (e: Email) => (domain: string) => endsWith._("@" + domain, e) ), // eslint-disable-next-line @typescript-eslint/no-non-null-assertion toDisplayName: (e: Email) => e.split("@")[0] } ) type EmailSchema__ = typeof Email__ export interface EmailSchema extends EmailSchema__ {} export const Email: EmailSchema = Email__ export type Email = ParsedShapeOfCustom & { split: (separator: "@") => [ReasonableString, ReasonableString] }