/** * Numeric brand schemas with `.withConstructorDefault` extensions. * * Each `.withConstructorDefault` here is **only** applied when the field is * omitted during construction (`.make(...)`). It is **not** applied during * decode and therefore cannot be used to JIT-migrate database fields. * * For persisted data, prefer an explicit, preferably versioned migration * over decode-time fallbacks. See `./ext.ts` for the full policy note. */ import { extendM } from "effect-app/utils" import * as Effect from "effect/Effect" import * as S from "effect/Schema" import type * as SchemaAST from "effect/SchemaAST" import type { Simplify } from "effect/Types" import { type BrandedSchema, fromBrand, nominal } from "./brand.js" import { withDefaultMake } from "./ext.js" import { type B } from "./schema.js" export interface PositiveIntBrand extends Simplify & NonNegativeIntBrand & PositiveNumberBrand> {} export type PositiveInt = number & PositiveIntBrand /** Positive integer. `.withConstructorDefault` => `1` (construction-only). */ export interface PositiveIntSchema extends BrandedSchema { (i: number, options?: SchemaAST.ParseOptions): PositiveInt readonly withConstructorDefault: S.withConstructorDefault> } export const PositiveInt: PositiveIntSchema = extendM( S.Int.pipe( S.check(S.isGreaterThan(0)), fromBrand(nominal(), { identifier: "PositiveInt", jsonSchema: {} }), withDefaultMake ), (s) => ({ /** * Construction-only default `1`. Applied only when the field is omitted * from `.make(...)` input. NOT applied during decode — cannot be used to * JIT-migrate database fields. See file-level note. */ withConstructorDefault: s.pipe(S.withConstructorDefault(Effect.sync(() => s(1)))) }) ) export interface NonNegativeIntBrand extends Simplify & IntBrand & NonNegativeNumberBrand> {} export type NonNegativeInt = number & NonNegativeIntBrand /** Non-negative integer. `.withConstructorDefault` => `0` (construction-only). */ export interface NonNegativeIntSchema extends BrandedSchema { (i: number, options?: SchemaAST.ParseOptions): NonNegativeInt readonly withConstructorDefault: S.withConstructorDefault> } export const NonNegativeInt: NonNegativeIntSchema = extendM( S.Int.pipe( S.check(S.isGreaterThanOrEqualTo(0)), fromBrand(nominal(), { identifier: "NonNegativeInt", jsonSchema: {} }), withDefaultMake ), (s) => ({ /** * Construction-only default `0`. Applied only when the field is omitted * from `.make(...)` input. NOT applied during decode — cannot be used to * JIT-migrate database fields. See file-level note. */ withConstructorDefault: s.pipe(S.withConstructorDefault(Effect.sync(() => s(0)))) }) ) export interface IntBrand extends Simplify> {} export type Int = number & IntBrand /** Integer. `.withConstructorDefault` => `0` (construction-only). */ export interface IntSchema extends BrandedSchema { (i: number, options?: SchemaAST.ParseOptions): Int readonly withConstructorDefault: S.withConstructorDefault> } export const Int: IntSchema = extendM( S.Int.pipe(fromBrand(nominal(), { identifier: "Int", jsonSchema: {} }), withDefaultMake), (s) => ({ /** * Construction-only default `0`. Applied only when the field is omitted * from `.make(...)` input. NOT applied during decode — cannot be used to * JIT-migrate database fields. See file-level note. */ withConstructorDefault: s.pipe(S.withConstructorDefault(Effect.sync(() => s(0)))) }) ) export interface PositiveNumberBrand extends Simplify & NonNegativeNumberBrand> {} export type PositiveNumber = number & PositiveNumberBrand /** Positive finite number. `.withConstructorDefault` => `1` (construction-only). */ export interface PositiveNumberSchema extends BrandedSchema { (i: number, options?: SchemaAST.ParseOptions): PositiveNumber readonly withConstructorDefault: S.withConstructorDefault> } export const PositiveNumber: PositiveNumberSchema = extendM( S.Finite.pipe( S.check(S.isGreaterThan(0)), fromBrand(nominal(), { identifier: "PositiveNumber", jsonSchema: {} }), withDefaultMake ), (s) => ({ /** * Construction-only default `1`. Applied only when the field is omitted * from `.make(...)` input. NOT applied during decode — cannot be used to * JIT-migrate database fields. See file-level note. */ withConstructorDefault: s.pipe(S.withConstructorDefault(Effect.sync(() => s(1)))) }) ) export interface NonNegativeNumberBrand extends Simplify> {} export type NonNegativeNumber = number & NonNegativeNumberBrand /** Non-negative finite number. `.withConstructorDefault` => `0` (construction-only). */ export interface NonNegativeNumberSchema extends BrandedSchema { (i: number, options?: SchemaAST.ParseOptions): NonNegativeNumber readonly withConstructorDefault: S.withConstructorDefault> } export const NonNegativeNumber: NonNegativeNumberSchema = extendM( S .Finite .pipe( S.check(S.isGreaterThanOrEqualTo(0)), fromBrand(nominal(), { identifier: "NonNegativeNumber", jsonSchema: {} }), withDefaultMake ), (s) => ({ /** * Construction-only default `0`. Applied only when the field is omitted * from `.make(...)` input. NOT applied during decode — cannot be used to * JIT-migrate database fields. See file-level note. */ withConstructorDefault: s.pipe(S.withConstructorDefault(Effect.sync(() => s(0)))) }) ) /** @deprecated Not an actual decimal */ export const NonNegativeDecimal = NonNegativeNumber /** @deprecated Not an actual decimal */ export type NonNegativeDecimal = NonNegativeNumber /** @deprecated Not an actual decimal */ export const PositiveDecimal = PositiveNumber /** @deprecated Not an actual decimal */ export type PositiveDecimal = PositiveNumber