Skip to content

Commit

Permalink
fix branded schema metadata
Browse files Browse the repository at this point in the history
  • Loading branch information
patroza committed Apr 3, 2024
1 parent 7957e05 commit 7b77e33
Show file tree
Hide file tree
Showing 8 changed files with 95 additions and 63 deletions.
6 changes: 6 additions & 0 deletions .changeset/plenty-apples-dream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"effect-app": minor
"@effect-app/schema": minor
---

fix branded schemas
27 changes: 9 additions & 18 deletions packages/prelude/src/schema.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,30 @@
import { Option, pipe, ReadonlyArray } from "@effect-app/core"
import { isValidEmail, isValidPhone } from "@effect-app/core/validation"
import { type A, type Email as EmailT, fromBrand, nominal, type PhoneNumber as PhoneNumberT } from "@effect-app/schema"
import { type A, type Email as EmailT, type PhoneNumber as PhoneNumberT } from "@effect-app/schema"
import * as S from "@effect-app/schema"
import { fakerArb } from "./faker.js"
import { extendM } from "./utils.js"

export * from "@effect-app/schema"

export const Email = S
.string
.Email
.pipe(
S.filter(isValidEmail, {
title: "Email",
description: "an email according to RFC 5322",
jsonSchema: { format: "email" },
S.annotations({
// eslint-disable-next-line @typescript-eslint/unbound-method
arbitrary: (): A.Arbitrary<string> => fakerArb((faker) => faker.internet.exampleEmail)
arbitrary: (): A.Arbitrary<Email> => (fc) => fakerArb((faker) => faker.internet.exampleEmail)(fc).map(Email)
}),
fromBrand(nominal<Email>(), { jsonSchema: {} }),
S.withDefaults
)

export type Email = EmailT

export const PhoneNumber = S
.string
.PhoneNumber
.pipe(
S.filter(isValidPhone, {
title: "PhoneNumber",
description: "a phone number with at least 7 digits",
jsonSchema: { format: "phone" },
S.annotations({
// eslint-disable-next-line @typescript-eslint/unbound-method
arbitrary: (): A.Arbitrary<string> => fakerArb((faker) => faker.phone.number)
arbitrary: (): A.Arbitrary<PhoneNumber> => (fc) => fakerArb((faker) => faker.phone.number)(fc).map(PhoneNumber)
}),
fromBrand(nominal<PhoneNumber>(), { jsonSchema: {} }),
S.withDefaults
)

Expand Down Expand Up @@ -92,5 +85,3 @@ export const taggedUnion = <Members extends readonly S.Schema<{ _tag: string },
pipe(S.union(...a), (_) => extendM(_, (_) => ({ is: makeIs(_), isAnyOf: makeIsAnyOf(_) })))

export type PhoneNumber = PhoneNumberT

export * from "@effect-app/schema"
13 changes: 11 additions & 2 deletions packages/prelude/test/schema.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import { StringId } from "@effect-app/schema"
import { generateFromArbitrary } from "@effect-app/infra/test.arbs"
import { JSONSchema } from "@effect/schema"
import { ReadonlyArray, S } from "effect-app"
import { test } from "vitest"

const A = S.struct({ a: S.NonEmptyString255, email: S.nullable(S.Email) })
test("works", () => {
console.log(StringId.make())
console.log(S.StringId.make())
console.log(generateFromArbitrary(S.A.make(A)).value)
console.log(S.AST.getTitleAnnotation(S.Email.ast))
console.log(S.AST.getDescriptionAnnotation(S.Email.ast))
console.log(S.AST.getJSONSchemaAnnotation(S.Email.ast))
console.log(JSONSchema.make(S.Email))
console.log(S.decodeEither(A, { errors: "all" })({ a: ReadonlyArray.range(1, 256).join(""), email: "hello" }))
})
10 changes: 6 additions & 4 deletions packages/schema/src/email.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { isValidEmail } from "@effect-app/core/validation"
import { isValidEmail as isValidEmailOrig } from "@effect-app/core/validation"
import * as S from "@effect/schema/Schema"
import type { Simplify } from "effect/Types"
import { fromBrand, nominal } from "./brand.js"
import { withDefaults } from "./ext.js"
import type { B } from "./schema.js"
import type { NonEmptyStringBrand } from "./strings.js"
Expand All @@ -10,16 +9,19 @@ export interface EmailBrand extends Simplify<NonEmptyStringBrand & B.Brand<"Emai

export type Email = string & EmailBrand

function isValidEmail(str: string): str is Email {
return isValidEmailOrig(str)
}

export const Email = S
.string
.pipe(
S.filter(isValidEmail, {
identifier: "Email",
title: "Email",
description: "an email according to RFC 5322",
jsonSchema: { format: "email" },
arbitrary: () => (fc) => fc.emailAddress()
}),
fromBrand(nominal<Email>(), { jsonSchema: {} }),
S.identifier("Email"),
withDefaults
)
34 changes: 20 additions & 14 deletions packages/schema/src/moreStrings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,8 @@ export type NonEmptyString50 = string & NonEmptyString50Brand
* A string that is at least 1 character long and a maximum of 50.
*/
export const NonEmptyString50 = NonEmptyString.pipe(
S.maxLength(50, { title: "NonEmptyString50" }),
fromBrand(nominal<NonEmptyString2k>(), { jsonSchema: {} }),
S.identifier("NonEmptyString50"),
S.maxLength(50),
fromBrand(nominal<NonEmptyString2k>(), { identifier: "NonEmptyString50", title: "NonEmptyString50", jsonSchema: {} }),
withDefaults
)

Expand All @@ -49,9 +48,8 @@ export type Min3String255 = string & Min3String255Brand
export const Min3String255 = pipe(
S.string,
S.minLength(3),
S.maxLength(255, { title: "Min3String255" }),
fromBrand(nominal<NonEmptyString2k>(), { jsonSchema: {} }),
S.identifier("Min3String255"),
S.maxLength(255),
fromBrand(nominal<NonEmptyString2k>(), { identifier: "Min3String255", title: "Min3String255", jsonSchema: {} }),
withDefaults
)

Expand Down Expand Up @@ -82,9 +80,13 @@ export const StringId = extendM(
pipe(
S.string,
S.minLength(minLength),
S.maxLength(maxLength, { title: "StringId", arbitrary: StringIdArb }),
S.identifier("StringId"),
fromBrand(nominal<StringIdBrand>(), { jsonSchema: {} })
S.maxLength(maxLength),
fromBrand(nominal<StringIdBrand>(), {
identifier: "StringId",
title: "StringId",
arbitrary: StringIdArb,
jsonSchema: {}
})
),
(s) => ({
make: makeStringId,
Expand Down Expand Up @@ -125,7 +127,11 @@ export function prefixedStringId<Brand extends StringId>() {
)
const s: S.Schema<string & Brand, string> = StringId
.pipe(
S.filter((x: StringId): x is Brand => x.startsWith(pref), { arbitrary: arb, title: name })
S.filter((x: StringId): x is string & Brand => x.startsWith(pref), {
arbitrary: arb,
identifier: name,
title: name
})
)
const schema = s.pipe(withDefaults)
const make = () => (pref + StringId.make().substring(0, 50 - pref.length)) as Brand
Expand Down Expand Up @@ -173,20 +179,20 @@ export interface PrefixedStringUtils<

export interface UrlBrand extends Simplify<B.Brand<"Url"> & NonEmptyStringBrand> {}

export type Url = NonEmptyString & UrlBrand
export type Url = string & UrlBrand

const isUrl: Refinement<string, Url> = (s: string): s is Url => {
return validator.default.isURL(s, { require_tld: false })
}

export const Url = NonEmptyString
export const Url = S
.string
.pipe(
S.filter(isUrl, {
arbitrary: (): Arbitrary<string> => (fc) => fc.webUrl(),
identifier: "Url",
title: "Url",
jsonSchema: { format: "uri" }
}),
fromBrand(nominal<UrlBrand>(), { jsonSchema: {} }),
S.identifier("Url"),
withDefaults
)
26 changes: 17 additions & 9 deletions packages/schema/src/numbers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ export interface PositiveIntBrand
export const PositiveInt = extendM(
S.Int.pipe(
S.positive(),
fromBrand(nominal<PositiveIntBrand>(), { jsonSchema: {} }),
S.identifier("PositiveInt"),
fromBrand(nominal<PositiveIntBrand>(), { identifier: "PositiveInt", title: "PositiveInt", jsonSchema: {} }),
withDefaults
),
(s) => ({ withDefault: S.propertySignature(s, { default: () => s(1) }) })
Expand All @@ -23,8 +22,11 @@ export interface NonNegativeIntBrand extends Simplify<B.Brand<"NonNegativeInt">
export const NonNegativeInt = extendM(
S.Int.pipe(
S.nonNegative(),
fromBrand(nominal<NonNegativeIntBrand>(), { jsonSchema: {} }),
S.identifier("NonNegativeInt"),
fromBrand(nominal<NonNegativeIntBrand>(), {
identifier: "NonNegativeInt",
title: "NonNegativeInt",
jsonSchema: {}
}),
withDefaults
),
(s) => ({ withDefault: S.propertySignature(s, { default: () => s(0) }) })
Expand All @@ -33,7 +35,7 @@ export type NonNegativeInt = S.Schema.Type<typeof NonNegativeInt>

export interface IntBrand extends Simplify<B.Brand<"Int">> {}
export const Int = extendM(
S.Int.pipe(fromBrand(nominal<IntBrand>(), { jsonSchema: {} }), S.identifier("Int"), withDefaults),
S.Int.pipe(fromBrand(nominal<IntBrand>(), { identifier: "Int", title: "Int", jsonSchema: {} }), withDefaults),
(s) => ({ withDefault: S.propertySignature(s, { default: () => s(0) }) })
)
export type Int = S.Schema.Type<typeof Int>
Expand All @@ -42,8 +44,11 @@ export interface PositiveNumberBrand extends Simplify<B.Brand<"PositiveNumber">
export const PositiveNumber = extendM(
S.number.pipe(
S.positive(),
fromBrand(nominal<PositiveNumberBrand>(), { jsonSchema: {} }),
S.identifier("PositiveNumber"),
fromBrand(nominal<PositiveNumberBrand>(), {
identifier: "PositiveNumber",
title: "PositiveNumber",
jsonSchema: {}
}),
withDefaults
),
(s) => ({ withDefault: S.propertySignature(s, { default: () => s(1) }) })
Expand All @@ -56,8 +61,11 @@ export const NonNegativeNumber = extendM(
.number
.pipe(
S.nonNegative(),
fromBrand(nominal<NonNegativeNumberBrand>(), { jsonSchema: {} }),
S.identifier("NonNegativeNumber"),
fromBrand(nominal<NonNegativeNumberBrand>(), {
identifier: "NonNegativeNumber",
title: "NonNegativeNumber",
jsonSchema: {}
}),
withDefaults
),
(s) => ({ withDefault: S.propertySignature(s, { default: () => s(0) }) })
Expand Down
10 changes: 6 additions & 4 deletions packages/schema/src/phoneNumber.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,27 @@
import { isValidPhone } from "@effect-app/core/validation"
import { isValidPhone as isValidPhoneOrig } from "@effect-app/core/validation"
import * as S from "@effect/schema/Schema"
import type { Simplify } from "effect/Types"
import { fromBrand, nominal } from "./brand.js"
import { withDefaults } from "./ext.js"
import { Numbers } from "./FastCheck.js"
import type { B } from "./schema.js"
import type { NonEmptyStringBrand } from "./strings.js"

function isValidPhone(str: string): str is PhoneNumber {
return isValidPhoneOrig(str)
}

export interface PhoneNumberBrand extends Simplify<B.Brand<"PhoneNumber"> & NonEmptyStringBrand> {}
export type PhoneNumber = string & PhoneNumberBrand

export const PhoneNumber = S
.string
.pipe(
S.filter(isValidPhone, {
identifier: "PhoneNumber",
title: "PhoneNumber",
description: "a phone number with at least 7 digits",
arbitrary: () => Numbers(7, 10),
jsonSchema: { format: "phone" }
}),
fromBrand(nominal<PhoneNumber>(), { jsonSchema: {} }),
S.identifier("PhoneNumber"),
withDefaults
)
32 changes: 20 additions & 12 deletions packages/schema/src/strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,43 +4,51 @@ import type { Simplify } from "effect/Types"
import { fromBrand, nominal } from "./brand.js"
import { withDefaults } from "./ext.js"

const nonEmptyString = S.string.pipe(S.nonEmpty({ title: "NonEmptyString" }))
const nonEmptyString = S.string.pipe(S.nonEmpty())

export type NonEmptyStringBrand = B.Brand<"NonEmptyString">
export type NonEmptyString = string & NonEmptyStringBrand
export const NonEmptyString = nonEmptyString
.pipe(
fromBrand(nominal<NonEmptyString>(), { jsonSchema: {} }),
S.identifier("NonEmptyString"),
fromBrand(nominal<NonEmptyString>(), { identifier: "NonEmptyString", title: "NonEmptyString", jsonSchema: {} }),
withDefaults
)

export interface NonEmptyString64kBrand extends Simplify<B.Brand<"NonEmptyString64k"> & NonEmptyStringBrand> {}
export type NonEmptyString64k = string & NonEmptyString64kBrand
export const NonEmptyString64k = nonEmptyString
.pipe(
S.maxLength(64 * 1024, { title: "NonEmptyString64k" }),
fromBrand(nominal<NonEmptyString64k>(), { jsonSchema: {} }),
S.identifier("NonEmptyString64k"),
S.maxLength(64 * 1024),
fromBrand(nominal<NonEmptyString64k>(), {
identifier: "NonEmptyString64k",
title: "NonEmptyString64k",
jsonSchema: {}
}),
withDefaults
)

export interface NonEmptyString2kBrand extends Simplify<B.Brand<"NonEmptyString2k"> & NonEmptyString64kBrand> {}
export type NonEmptyString2k = string & NonEmptyString2kBrand
export const NonEmptyString2k = nonEmptyString
.pipe(
S.maxLength(2 * 1024, { title: "NonEmptyString2k" }),
fromBrand(nominal<NonEmptyString2k>(), { jsonSchema: {} }),
S.identifier("NonEmptyString2k"),
S.maxLength(2 * 1024),
fromBrand(nominal<NonEmptyString2k>(), {
identifier: "NonEmptyString2k",
title: "NonEmptyString2k",
jsonSchema: {}
}),
withDefaults
)

export interface NonEmptyString255Brand extends Simplify<B.Brand<"NonEmptyString255"> & NonEmptyString2kBrand> {}
export type NonEmptyString255 = string & NonEmptyString255Brand
export const NonEmptyString255 = nonEmptyString
.pipe(
S.maxLength(255, { title: "NonEmptyString255" }),
fromBrand(nominal<NonEmptyString255>(), { jsonSchema: {} }),
S.identifier("NonEmptyString255"),
S.maxLength(255),
fromBrand(nominal<NonEmptyString255>(), {
identifier: "NonEmptyString255",
title: "NonEmptyString255",
jsonSchema: {}
}),
withDefaults
)

0 comments on commit 7b77e33

Please sign in to comment.