diff --git a/repos/effect/.changeset/cold-loops-arrive.md b/repos/effect/.changeset/cold-loops-arrive.md new file mode 100644 index 0000000..b21395c --- /dev/null +++ b/repos/effect/.changeset/cold-loops-arrive.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix $match generic type parameter inference inside arms (#6249) diff --git a/repos/effect/.changeset/fix-bedrock-thinking-generate-object.md b/repos/effect/.changeset/fix-bedrock-thinking-generate-object.md deleted file mode 100644 index 04143c2..0000000 --- a/repos/effect/.changeset/fix-bedrock-thinking-generate-object.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@effect/ai-amazon-bedrock": patch ---- - -`generateObject` no longer fails when extended thinking is configured via `withConfigOverride`. Anthropic's API rejects requests that set `thinking` in `additionalModelRequestFields` alongside a forced `toolChoice` — which `generateObject` always does. The fix strips `thinking` from `additionalModelRequestFields` in the `json` response format path before the request is sent. diff --git a/repos/effect/.changeset/fix-cli-completions-hyphen.md b/repos/effect/.changeset/fix-cli-completions-hyphen.md deleted file mode 100644 index 3b681f5..0000000 --- a/repos/effect/.changeset/fix-cli-completions-hyphen.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@effect/cli": patch ---- - -fix(cli): replace all hyphens in shell completion command names diff --git a/repos/effect/.changeset/fix-cli-weak-span-dark-terminal.md b/repos/effect/.changeset/fix-cli-weak-span-dark-terminal.md deleted file mode 100644 index d3cbe86..0000000 --- a/repos/effect/.changeset/fix-cli-weak-span-dark-terminal.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@effect/cli": patch ---- - -Fix `@effect/cli` help output to use `Ansi.blackBright` instead of `Ansi.black` for `Weak` spans. The previous black foreground was invisible on dark terminal backgrounds. diff --git a/repos/effect/.changeset/forward-parent-pointer-discard.md b/repos/effect/.changeset/forward-parent-pointer-discard.md deleted file mode 100644 index b55505e..0000000 --- a/repos/effect/.changeset/forward-parent-pointer-discard.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@effect/workflow": patch ---- - -Forward the parent pointer when spawning a child workflow with `discard: true` diff --git a/repos/effect/.changeset/json-schema-never-additional-properties.md b/repos/effect/.changeset/json-schema-never-additional-properties.md new file mode 100644 index 0000000..e95f825 --- /dev/null +++ b/repos/effect/.changeset/json-schema-never-additional-properties.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Emit `additionalProperties: false` for records with string keys and `Schema.Never` values. diff --git a/repos/effect/packages/ai/amazon-bedrock/CHANGELOG.md b/repos/effect/packages/ai/amazon-bedrock/CHANGELOG.md index 06ed052..2634105 100644 --- a/repos/effect/packages/ai/amazon-bedrock/CHANGELOG.md +++ b/repos/effect/packages/ai/amazon-bedrock/CHANGELOG.md @@ -1,5 +1,15 @@ # @effect/ai-amazon-bedrock +## 0.15.1 + +### Patch Changes + +- [#6206](https://github.com/Effect-TS/effect/pull/6206) [`13c8207`](https://github.com/Effect-TS/effect/commit/13c82074fd183a118407d1c7574a9b060f71b4d5) Thanks @Zelys-DFKH! - `generateObject` no longer fails when extended thinking is configured via `withConfigOverride`. Anthropic's API rejects requests that set `thinking` in `additionalModelRequestFields` alongside a forced `toolChoice` — which `generateObject` always does. The fix strips `thinking` from `additionalModelRequestFields` in the `json` response format path before the request is sent. + +- Updated dependencies []: + - @effect/ai-anthropic@0.25.0 + - @effect/experimental@0.60.0 + ## 0.15.0 ### Patch Changes diff --git a/repos/effect/packages/ai/amazon-bedrock/package.json b/repos/effect/packages/ai/amazon-bedrock/package.json index efbae14..b2e301f 100644 --- a/repos/effect/packages/ai/amazon-bedrock/package.json +++ b/repos/effect/packages/ai/amazon-bedrock/package.json @@ -1,7 +1,7 @@ { "name": "@effect/ai-amazon-bedrock", "type": "module", - "version": "0.15.0", + "version": "0.15.1", "license": "MIT", "description": "Effect modules for working with Amazon Bedrock AI apis", "homepage": "https://effect.website", diff --git a/repos/effect/packages/cli/CHANGELOG.md b/repos/effect/packages/cli/CHANGELOG.md index 497d206..90bde29 100644 --- a/repos/effect/packages/cli/CHANGELOG.md +++ b/repos/effect/packages/cli/CHANGELOG.md @@ -1,5 +1,13 @@ # @effect/cli +## 0.75.2 + +### Patch Changes + +- [#6213](https://github.com/Effect-TS/effect/pull/6213) [`1a63ec8`](https://github.com/Effect-TS/effect/commit/1a63ec87cd295972b05b51c9b4ad2db9567dc994) Thanks @lihan3238! - fix(cli): replace all hyphens in shell completion command names + +- [#6208](https://github.com/Effect-TS/effect/pull/6208) [`e71ba68`](https://github.com/Effect-TS/effect/commit/e71ba68273026a1a2c1ace7218bdb206b0d3386d) Thanks @Zelys-DFKH! - Fix `@effect/cli` help output to use `Ansi.blackBright` instead of `Ansi.black` for `Weak` spans. The previous black foreground was invisible on dark terminal backgrounds. + ## 0.75.1 ### Patch Changes diff --git a/repos/effect/packages/cli/package.json b/repos/effect/packages/cli/package.json index 939a961..b7b5a2b 100644 --- a/repos/effect/packages/cli/package.json +++ b/repos/effect/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@effect/cli", - "version": "0.75.1", + "version": "0.75.2", "type": "module", "license": "MIT", "description": "A library for building command-line interfaces with Effect", diff --git a/repos/effect/packages/cluster/CHANGELOG.md b/repos/effect/packages/cluster/CHANGELOG.md index 8310d71..4bcae4a 100644 --- a/repos/effect/packages/cluster/CHANGELOG.md +++ b/repos/effect/packages/cluster/CHANGELOG.md @@ -1,5 +1,16 @@ # @effect/cluster +## 0.59.0 + +### Minor Changes + +- [#6255](https://github.com/Effect-TS/effect/pull/6255) [`26e1922`](https://github.com/Effect-TS/effect/commit/26e19228e1422decbe11ef58e29757f013d96fc8) Thanks @tim-smart! - Backport cluster shard group fixes: use available shard groups for SQL advisory lock numbering and route workflow durable clock/deferred messages through the owning workflow shard group. + +### Patch Changes + +- Updated dependencies [[`e5998a4`](https://github.com/Effect-TS/effect/commit/e5998a45f69960b38eb2b8cb67cbb07b9e6962c7)]: + - @effect/workflow@0.18.2 + ## 0.58.3 ### Patch Changes diff --git a/repos/effect/packages/cluster/package.json b/repos/effect/packages/cluster/package.json index dc8e7c8..9d01b86 100644 --- a/repos/effect/packages/cluster/package.json +++ b/repos/effect/packages/cluster/package.json @@ -1,7 +1,7 @@ { "name": "@effect/cluster", "type": "module", - "version": "0.58.3", + "version": "0.59.0", "description": "Unified interfaces for common cluster-specific services", "publishConfig": { "access": "public", diff --git a/repos/effect/packages/cluster/src/ClusterWorkflowEngine.ts b/repos/effect/packages/cluster/src/ClusterWorkflowEngine.ts index afdb1f5..015d181 100644 --- a/repos/effect/packages/cluster/src/ClusterWorkflowEngine.ts +++ b/repos/effect/packages/cluster/src/ClusterWorkflowEngine.ts @@ -1,13 +1,18 @@ /** * @since 1.0.0 */ +import * as Headers from "@effect/platform/Headers" import * as Rpc from "@effect/rpc/Rpc" import * as RpcServer from "@effect/rpc/RpcServer" import { DurableDeferred } from "@effect/workflow" import * as Activity from "@effect/workflow/Activity" import * as DurableClock from "@effect/workflow/DurableClock" import * as Workflow from "@effect/workflow/Workflow" -import { makeUnsafe, WorkflowEngine, WorkflowInstance } from "@effect/workflow/WorkflowEngine" +import { + makeUnsafe, + WorkflowEngine, + WorkflowInstance +} from "@effect/workflow/WorkflowEngine" import * as Arr from "effect/Array" import * as Cause from "effect/Cause" import * as Context from "effect/Context" @@ -33,6 +38,8 @@ import * as Entity from "./Entity.js" import { EntityAddress } from "./EntityAddress.js" import { EntityId } from "./EntityId.js" import { EntityType } from "./EntityType.js" +import * as Envelope from "./Envelope.js" +import * as Message from "./Message.js" import { MessageStorage } from "./MessageStorage.js" import type { WithExitEncoded } from "./Reply.js" import * as Reply from "./Reply.js" @@ -43,7 +50,7 @@ import * as Snowflake from "./Snowflake.js" * @since 1.0.0 * @category Constructors */ -export const make = Effect.gen(function*() { +export const make = Effect.gen(function* () { const sharding = yield* Sharding.Sharding const storage = yield* MessageStorage @@ -53,26 +60,36 @@ export const make = Effect.gen(function*() { Entity.Entity< string, | Rpc.Rpc< - "run", - Schema.Struct< - Record< - typeof payloadParentKey, - Schema.optional< - Schema.Struct<{ - workflowName: typeof Schema.String - executionId: typeof Schema.String - }> + "run", + Schema.Struct< + Record< + typeof payloadParentKey, + Schema.optional< + Schema.Struct<{ + workflowName: typeof Schema.String + executionId: typeof Schema.String + }> + > > - > - >, - Schema.Schema> - > - | Rpc.Rpc<"deferred", Schema.Struct<{ name: typeof Schema.String; exit: typeof ExitUnknown }>, typeof ExitUnknown> + >, + Schema.Schema> + > + | Rpc.Rpc< + "deferred", + Schema.Struct<{ + name: typeof Schema.String + exit: typeof ExitUnknown + }>, + typeof ExitUnknown + > | Rpc.Rpc< - "activity", - Schema.Struct<{ name: typeof Schema.String; attempt: typeof Schema.Number }>, - Schema.Schema> - > + "activity", + Schema.Struct<{ + name: typeof Schema.String + attempt: typeof Schema.Number + }>, + Schema.Schema> + > | Rpc.Rpc<"resume", Schema.Struct<{}>> > >() @@ -80,12 +97,22 @@ export const make = Effect.gen(function*() { string, Entity.Entity< string, - | Rpc.Rpc<"deferred", Schema.Struct<{ name: typeof Schema.String; exit: typeof ExitUnknown }>, typeof ExitUnknown> | Rpc.Rpc< - "activity", - Schema.Struct<{ name: typeof Schema.String; attempt: typeof Schema.Number }>, - Schema.Schema> - > + "deferred", + Schema.Struct<{ + name: typeof Schema.String + exit: typeof ExitUnknown + }>, + typeof ExitUnknown + > + | Rpc.Rpc< + "activity", + Schema.Struct<{ + name: typeof Schema.String + attempt: typeof Schema.Number + }>, + Schema.Schema> + > | Rpc.Rpc<"resume"> > >() @@ -107,61 +134,114 @@ export const make = Effect.gen(function*() { return entity! } - const activities = new Map - }>() + const activities = new Map< + string, + { + readonly activity: Activity.Any + readonly runtime: Runtime.Runtime + } + >() const interruptedActivities = new Set() const activityLatches = new Map() const clients = yield* RcMap.make({ - lookup: Effect.fnUntraced(function*(workflowName: string) { + lookup: Effect.fnUntraced(function* (workflowName: string) { const entity = entities.get(workflowName) if (!entity) { - return yield* Effect.dieMessage(`Workflow ${workflowName} not registered`) + return yield* Effect.dieMessage( + `Workflow ${workflowName} not registered` + ) } return yield* entity.client }), idleTimeToLive: "5 minutes" }) const clientsPartial = yield* RcMap.make({ - lookup: Effect.fnUntraced(function*(workflowName: string) { - const entity = entities.get(workflowName) ?? ensurePartialEntity(workflowName) + lookup: Effect.fnUntraced(function* (workflowName: string) { + const entity = + entities.get(workflowName) ?? ensurePartialEntity(workflowName) return yield* entity.client }), idleTimeToLive: "5 minutes" }) - const clockClient = yield* ClockEntity.client - - const requestIdFor = Effect.fnUntraced(function*(options: { + const entityAddressFor = (options: { readonly workflow: Workflow.Any readonly entityType: string readonly executionId: string - readonly tag: string - readonly id: string - }) { - const shardGroup = Context.get(options.workflow.annotations, ClusterSchema.ShardGroup)( - options.executionId as EntityId - ) + }) => { + const shardGroup = Context.get( + options.workflow.annotations, + ClusterSchema.ShardGroup + )(options.executionId as EntityId) const entityId = EntityId.make(options.executionId) - const address = new EntityAddress({ + return new EntityAddress({ entityType: EntityType.make(options.entityType), entityId, shardId: sharding.getShardId(entityId, shardGroup) }) - return yield* storage.requestIdForPrimaryKey({ address, tag: options.tag, id: options.id }) + } + + const sendDiscard = Effect.fnUntraced(function* (options: { + readonly rpc: Rpc.AnyWithProps + readonly address: EntityAddress + readonly payload: unknown + }) { + const payload = options.rpc.payloadSchema.make + ? options.rpc.payloadSchema.make(options.payload) + : options.payload + const envelope = Envelope.makeRequest({ + requestId: yield* sharding.getSnowflake, + address: options.address, + tag: options.rpc._tag as any, + payload, + headers: Headers.empty + }) + yield* sharding.sendOutgoing( + new Message.OutgoingRequest({ + envelope, + context: Context.empty() as Context.Context, + lastReceivedReply: Option.none(), + rpc: options.rpc, + respond: () => Effect.void + }), + true + ) }) - const replyForRequestId = Effect.fnUntraced(function*(requestId: Snowflake.Snowflake) { + const requestIdFor = Effect.fnUntraced(function* (options: { + readonly workflow: Workflow.Any + readonly entityType: string + readonly executionId: string + readonly tag: string + readonly id: string + }) { + const address = entityAddressFor(options) + return yield* storage.requestIdForPrimaryKey({ + address, + tag: options.tag, + id: options.id + }) + }) + + const replyForRequestId = Effect.fnUntraced(function* ( + requestId: Snowflake.Snowflake + ) { const replies = yield* storage.repliesForUnfiltered([requestId]) return Arr.last(replies).pipe( Option.filter((reply) => reply._tag === "WithExit"), - Option.map((reply) => - reply as WithExitEncoded, Schema.Schema>>> + Option.map( + (reply) => + reply as WithExitEncoded< + Rpc.Rpc< + string, + Schema.Struct<{}>, + Schema.Schema> + > + > ) ) }) - const requestReply = Effect.fnUntraced(function*(options: { + const requestReply = Effect.fnUntraced(function* (options: { readonly workflow: Workflow.Any readonly entityType: string readonly executionId: string @@ -176,7 +256,7 @@ export const make = Effect.gen(function*() { }) const resetActivityAttempt = Effect.fnUntraced( - function*(options: { + function* (options: { readonly workflow: Workflow.Any readonly executionId: string readonly activity: Activity.Any @@ -199,13 +279,14 @@ export const make = Effect.gen(function*() { Effect.orDie ) - const clearClock = Effect.fnUntraced(function*(options: { + const clearClock = Effect.fnUntraced(function* (options: { readonly workflow: Workflow.Any readonly executionId: string }) { - const shardGroup = Context.get(options.workflow.annotations, ClusterSchema.ShardGroup)( - options.executionId as EntityId - ) + const shardGroup = Context.get( + options.workflow.annotations, + ClusterSchema.ShardGroup + )(options.executionId as EntityId) const entityId = EntityId.make(options.executionId) const shardId = sharding.getShardId(entityId, shardGroup) const clockAddress = new EntityAddress({ @@ -216,7 +297,10 @@ export const make = Effect.gen(function*() { yield* storage.clearAddress(clockAddress) }) - const resume = Effect.fnUntraced(function*(workflow: Workflow.Any, executionId: string) { + const resume = Effect.fnUntraced(function* ( + workflow: Workflow.Any, + executionId: string + ) { const maybeReply = yield* requestReply({ workflow, entityType: `Workflow/${workflow.name}`, @@ -226,14 +310,15 @@ export const make = Effect.gen(function*() { }) const maybeSuspended = Option.filter( maybeReply, - (reply) => reply.exit._tag === "Success" && reply.exit.value._tag === "Suspended" + (reply) => + reply.exit._tag === "Success" && reply.exit.value._tag === "Suspended" ) if (Option.isNone(maybeSuspended)) return yield* sharding.reset(Snowflake.Snowflake(maybeSuspended.value.requestId)) yield* sharding.pollStorage }) - const sendResumeParent = Effect.fnUntraced(function*(options: { + const sendResumeParent = Effect.fnUntraced(function* (options: { readonly workflowName: string readonly executionId: string }) { @@ -245,7 +330,9 @@ export const make = Effect.gen(function*() { id: "" }) if (Option.isNone(requestId)) { - const client = (yield* RcMap.get(clientsPartial, options.workflowName))(options.executionId) + const client = (yield* RcMap.get(clientsPartial, options.workflowName))( + options.executionId + ) return yield* client.resume({} as any, { discard: true }) } const reply = yield* replyForRequestId(requestId.value) @@ -255,104 +342,142 @@ export const make = Effect.gen(function*() { const engine = makeUnsafe({ register: (workflow, execute) => - Effect.suspend(() => - sharding.registerEntity( - ensureEntity(workflow), - Effect.gen(function*() { - const address = yield* Entity.CurrentAddress - const executionId = address.entityId - return { - run: (request: Entity.Request) => { - const instance = WorkflowInstance.initial(workflow, executionId) - const payload = request.payload - let parent: { workflowName: string; executionId: string } | undefined - if (payload[payloadParentKey]) { - parent = payload[payloadParentKey] - } - return execute(workflow.payloadSchema.make(payload), executionId).pipe( - Effect.onExit((exit) => { - const suspendOnFailure = Context.get(workflow.annotations, Workflow.SuspendOnFailure) - if (!instance.suspended && !(suspendOnFailure && exit._tag === "Failure")) { - return parent ? ensureSuccess(sendResumeParent(parent)) : Effect.void - } - return engine.deferredResult(InterruptSignal).pipe( - Effect.flatMap((maybeExit) => { - if (maybeExit === undefined) { - return Effect.void - } - instance.suspended = false - instance.interrupted = true - return Effect.zipRight( - Effect.ignore(clearClock({ workflow, executionId })), - Effect.withFiberRuntime((fiber) => Effect.interruptible(Fiber.interrupt(fiber))) - ) - }), - Effect.orDie - ) - }), - Workflow.intoResult, - Effect.provideService(WorkflowInstance, instance) - ) as any - }, - - activity(request: Entity.Request) { - const activityId = `${executionId}/${request.payload.name}` - const instance = WorkflowInstance.initial(workflow, executionId) - interruptedActivities.delete(activityId) - return Effect.gen(function*() { - let entry = activities.get(activityId) - while (!entry) { - const latch = Effect.unsafeMakeLatch() - activityLatches.set(activityId, latch) - yield* latch.await - entry = activities.get(activityId) + Effect.suspend( + () => + sharding.registerEntity( + ensureEntity(workflow), + Effect.gen(function* () { + const address = yield* Entity.CurrentAddress + const executionId = address.entityId + return { + run: (request: Entity.Request) => { + const instance = WorkflowInstance.initial( + workflow, + executionId + ) + const payload = request.payload + let parent: + | { workflowName: string; executionId: string } + | undefined + if (payload[payloadParentKey]) { + parent = payload[payloadParentKey] } - const contextMap = new Map(entry.runtime.context.unsafeMap) - contextMap.set(Activity.CurrentAttempt.key, request.payload.attempt) - contextMap.set(WorkflowInstance.key, instance) - const runtime = Runtime.make({ - context: Context.unsafeMake(contextMap), - fiberRefs: entry.runtime.fiberRefs, - runtimeFlags: Runtime.defaultRuntimeFlags - }) - return yield* entry.activity.executeEncoded.pipe( - Effect.provide(runtime) + return execute( + workflow.payloadSchema.make(payload), + executionId + ).pipe( + Effect.onExit((exit) => { + const suspendOnFailure = Context.get( + workflow.annotations, + Workflow.SuspendOnFailure + ) + if ( + !instance.suspended && + !(suspendOnFailure && exit._tag === "Failure") + ) { + return parent + ? ensureSuccess(sendResumeParent(parent)) + : Effect.void + } + return engine.deferredResult(InterruptSignal).pipe( + Effect.flatMap((maybeExit) => { + if (maybeExit === undefined) { + return Effect.void + } + instance.suspended = false + instance.interrupted = true + return Effect.zipRight( + Effect.ignore( + clearClock({ workflow, executionId }) + ), + Effect.withFiberRuntime((fiber) => + Effect.interruptible(Fiber.interrupt(fiber)) + ) + ) + }), + Effect.orDie + ) + }), + Workflow.intoResult, + Effect.provideService(WorkflowInstance, instance) + ) as any + }, + + activity(request: Entity.Request) { + const activityId = `${executionId}/${request.payload.name}` + const instance = WorkflowInstance.initial( + workflow, + executionId ) - }).pipe( - Workflow.intoResult, - Effect.catchAllCause((cause) => { - const interruptors = Cause.interruptors(cause) - // we only want to store interrupts as suspends when the - // client requested it - const ids = Array.from(interruptors, (id) => Array.from(FiberId.ids(id))).flat() - const suspend = ids.includes(RpcServer.fiberIdClientInterrupt.id) - if (suspend) { - interruptedActivities.add(activityId) - return Effect.succeed(new Workflow.Suspended()) + interruptedActivities.delete(activityId) + return Effect.gen(function* () { + let entry = activities.get(activityId) + while (!entry) { + const latch = Effect.unsafeMakeLatch() + activityLatches.set(activityId, latch) + yield* latch.await + entry = activities.get(activityId) } - return Effect.failCause(cause) - }), - Effect.provideService(WorkflowInstance, instance), - Effect.provideService(Activity.CurrentAttempt, request.payload.attempt), - Effect.ensuring(Effect.sync(() => { - activities.delete(activityId) - })), - Rpc.wrap({ - fork: true, - uninterruptible: true - }) - ) - }, - - deferred: Effect.fnUntraced(function*(request: Entity.Request) { - yield* ensureSuccess(resume(workflow, executionId)) - return request.payload.exit - }), - - resume: () => ensureSuccess(resume(workflow, executionId)) - } - }) - ) as Effect.Effect + const contextMap = new Map(entry.runtime.context.unsafeMap) + contextMap.set( + Activity.CurrentAttempt.key, + request.payload.attempt + ) + contextMap.set(WorkflowInstance.key, instance) + const runtime = Runtime.make({ + context: Context.unsafeMake(contextMap), + fiberRefs: entry.runtime.fiberRefs, + runtimeFlags: Runtime.defaultRuntimeFlags + }) + return yield* entry.activity.executeEncoded.pipe( + Effect.provide(runtime) + ) + }).pipe( + Workflow.intoResult, + Effect.catchAllCause((cause) => { + const interruptors = Cause.interruptors(cause) + // we only want to store interrupts as suspends when the + // client requested it + const ids = Array.from(interruptors, (id) => + Array.from(FiberId.ids(id)) + ).flat() + const suspend = ids.includes( + RpcServer.fiberIdClientInterrupt.id + ) + if (suspend) { + interruptedActivities.add(activityId) + return Effect.succeed(new Workflow.Suspended()) + } + return Effect.failCause(cause) + }), + Effect.provideService(WorkflowInstance, instance), + Effect.provideService( + Activity.CurrentAttempt, + request.payload.attempt + ), + Effect.ensuring( + Effect.sync(() => { + activities.delete(activityId) + }) + ), + Rpc.wrap({ + fork: true, + uninterruptible: true + }) + ) + }, + + deferred: Effect.fnUntraced(function* ( + request: Entity.Request + ) { + yield* ensureSuccess(resume(workflow, executionId)) + return request.payload.exit + }), + + resume: () => ensureSuccess(resume(workflow, executionId)) + } + }) + ) as Effect.Effect ), execute: (workflow, { discard, executionId, parent, payload }) => { @@ -360,12 +485,15 @@ export const make = Effect.gen(function*() { return RcMap.get(clients, workflow.name).pipe( Effect.flatMap((make) => make(executionId).run( - parent ? - { - ...payload, - [payloadParentKey]: { workflowName: parent.workflow.name, executionId: parent.executionId } - } : - payload, + parent + ? { + ...payload, + [payloadParentKey]: { + workflowName: parent.workflow.name, + executionId: parent.executionId + } + } + : payload, { discard } ) ), @@ -374,7 +502,7 @@ export const make = Effect.gen(function*() { ) }, - poll: Effect.fnUntraced(function*(workflow, executionId) { + poll: Effect.fnUntraced(function* (workflow, executionId) { const entity = ensureEntity(workflow) const exitSchema = Rpc.exitSchema(entity.protocol.requests.get("run")!) const oreply = yield* requestReply({ @@ -385,15 +513,14 @@ export const make = Effect.gen(function*() { id: "" }) if (Option.isNone(oreply)) return undefined - const exit = yield* (Schema.decode(exitSchema)(oreply.value.exit) as Effect.Effect< - Exit.Exit, - ParseResult.ParseError - >) + const exit = yield* Schema.decode(exitSchema)( + oreply.value.exit + ) as Effect.Effect, ParseResult.ParseError> return yield* exit }, Effect.orDie), interrupt: Effect.fnUntraced( - function*(workflow, executionId) { + function* (workflow, executionId) { ensureEntity(workflow) const oreply = yield* requestReply({ workflow, @@ -403,7 +530,11 @@ export const make = Effect.gen(function*() { id: "" }) const nonSuspendedReply = oreply.pipe( - Option.filter((reply) => reply.exit._tag !== "Success" || reply.exit.value._tag !== "Suspended") + Option.filter( + (reply) => + reply.exit._tag !== "Success" || + reply.exit.value._tag !== "Suspended" + ) ) if (Option.isSome(nonSuspendedReply)) { return @@ -424,43 +555,48 @@ export const make = Effect.gen(function*() { Effect.orDie ), - resume: (workflow, executionId) => ensureSuccess(resume(workflow, executionId)), - - activityExecute: Effect.fnUntraced( - function*(activity, attempt) { - const runtime = yield* Effect.runtime() - const context = runtime.context - const instance = Context.get(context, WorkflowInstance) - yield* Effect.annotateCurrentSpan("executionId", instance.executionId) - const activityId = `${instance.executionId}/${activity.name}` - const client = (yield* RcMap.get(clientsPartial, instance.workflow.name))(instance.executionId) - while (true) { - if (!activities.has(activityId)) { - activities.set(activityId, { activity, runtime }) - const latch = activityLatches.get(activityId) - if (latch) { - yield* latch.release - activityLatches.delete(activityId) - } - } - const result = yield* Effect.orDie(client.activity({ name: activity.name, attempt })) - // If the activity has suspended and did not execute, we need to resume - // it by resetting the attempt and re-executing. - if (result._tag === "Suspended" && (activities.has(activityId) || interruptedActivities.has(activityId))) { - yield* resetActivityAttempt({ - workflow: instance.workflow, - executionId: instance.executionId, - activity, - attempt - }) - continue + resume: (workflow, executionId) => + ensureSuccess(resume(workflow, executionId)), + + activityExecute: Effect.fnUntraced(function* (activity, attempt) { + const runtime = yield* Effect.runtime() + const context = runtime.context + const instance = Context.get(context, WorkflowInstance) + yield* Effect.annotateCurrentSpan("executionId", instance.executionId) + const activityId = `${instance.executionId}/${activity.name}` + const client = (yield* RcMap.get(clientsPartial, instance.workflow.name))( + instance.executionId + ) + while (true) { + if (!activities.has(activityId)) { + activities.set(activityId, { activity, runtime }) + const latch = activityLatches.get(activityId) + if (latch) { + yield* latch.release + activityLatches.delete(activityId) } - activities.delete(activityId) - return result } - }, - Effect.scoped - ), + const result = yield* Effect.orDie( + client.activity({ name: activity.name, attempt }) + ) + // If the activity has suspended and did not execute, we need to resume + // it by resetting the attempt and re-executing. + if ( + result._tag === "Suspended" && + (activities.has(activityId) || interruptedActivities.has(activityId)) + ) { + yield* resetActivityAttempt({ + workflow: instance.workflow, + executionId: instance.executionId, + activity, + attempt + }) + continue + } + activities.delete(activityId) + return result + } + }, Effect.scoped), deferredResult: (deferred) => WorkflowInstance.pipe( @@ -491,28 +627,57 @@ export const make = Effect.gen(function*() { Effect.orDie ), - deferredDone: Effect.fnUntraced( - function*({ deferredName, executionId, exit, workflowName }) { - const client = yield* RcMap.get(clientsPartial, workflowName) + deferredDone: Effect.fnUntraced(function* ({ + deferredName, + executionId, + exit, + workflowName + }) { + const workflow = workflows.get(workflowName) + if (workflow) { return yield* Effect.orDie( - client(executionId).deferred({ + sendDiscard({ + rpc: DeferredRpc, + address: entityAddressFor({ + workflow, + entityType: `Workflow/${workflowName}`, + executionId + }), + payload: { + name: deferredName, + exit + } + }) + ) + } + const client = yield* RcMap.get(clientsPartial, workflowName) + return yield* Effect.orDie( + client(executionId).deferred( + { name: deferredName, exit - }, { discard: true }) + }, + { discard: true } ) - }, - Effect.scoped - ), + ) + }, Effect.scoped), scheduleClock(workflow, options) { - const client = clockClient(options.executionId) return DateTime.now.pipe( Effect.flatMap((now) => - client.run({ - name: options.clock.name, - workflowName: workflow.name, - wakeUp: DateTime.addDuration(now, options.clock.duration) - }, { discard: true }) + sendDiscard({ + rpc: ClockRpc, + address: entityAddressFor({ + workflow, + entityType: ClockEntity.type, + executionId: options.executionId + }), + payload: { + name: options.clock.name, + workflowName: workflow.name, + wakeUp: DateTime.addDuration(now, options.clock.duration) + } + }) ), Effect.orDie ) @@ -527,11 +692,7 @@ const retryPolicy = Schedule.exponential(200, 1.5).pipe( ) const ensureSuccess = (effect: Effect.Effect) => - effect.pipe( - Effect.sandbox, - Effect.retry(retryPolicy), - Effect.orDie - ) + effect.pipe(Effect.sandbox, Effect.retry(retryPolicy), Effect.orDie) const ActivityRpc = Rpc.make("activity", { payload: { @@ -554,10 +715,12 @@ const makeWorkflowEntity = (workflow: Workflow.Any) => Rpc.make("run", { payload: { ...workflow.payloadSchema.fields, - [payloadParentKey]: Schema.optional(Schema.Struct({ - workflowName: Schema.String, - executionId: Schema.String - })) + [payloadParentKey]: Schema.optional( + Schema.Struct({ + workflowName: Schema.String, + executionId: Schema.String + }) + ) }, primaryKey: () => "", success: Workflow.Result({ @@ -590,7 +753,9 @@ const DeferredRpc = Rpc.make("deferred", { .annotate(ClusterSchema.Persisted, true) .annotate(ClusterSchema.Uninterruptible, true) -const decodeDeferredWithExit = Schema.decodeSync(Reply.WithExit.schema(DeferredRpc)) +const decodeDeferredWithExit = Schema.decodeSync( + Reply.WithExit.schema(DeferredRpc) +) const ResumeRpc = Rpc.make("resume", { payload: {}, @@ -600,15 +765,14 @@ const ResumeRpc = Rpc.make("resume", { .annotate(ClusterSchema.Uninterruptible, true) const makePartialWorkflowEntity = (workflowName: string) => - Entity.make(`Workflow/${workflowName}`, [ - DeferredRpc, - ResumeRpc, - ActivityRpc - ]) + Entity.make(`Workflow/${workflowName}`, [DeferredRpc, ResumeRpc, ActivityRpc]) -const activityPrimaryKey = (activity: string, attempt: number) => `${activity}/${attempt}` +const activityPrimaryKey = (activity: string, attempt: number) => + `${activity}/${attempt}` -class ClockPayload extends Schema.Class(`Workflow/DurableClock/Run`)({ +class ClockPayload extends Schema.Class( + `Workflow/DurableClock/Run` +)({ name: Schema.String, workflowName: Schema.String, wakeUp: Schema.DateTimeUtcFromNumber @@ -621,28 +785,35 @@ class ClockPayload extends Schema.Class(`Workflow/DurableClock/Run } } -const ClockEntity = Entity.make("Workflow/-/DurableClock", [ - Rpc.make("run", { payload: ClockPayload }) - .annotate(ClusterSchema.Persisted, true) - .annotate(ClusterSchema.Uninterruptible, true) -]) - -const ClockEntityLayer = ClockEntity.toLayer(Effect.gen(function*() { - const engine = yield* WorkflowEngine - const address = yield* Entity.CurrentAddress - const executionId = address.entityId - return { - run(request) { - const deferred = DurableClock.make({ name: request.payload.name, duration: Duration.zero }).deferred - return ensureSuccess(engine.deferredDone(deferred, { - workflowName: request.payload.workflowName, - executionId, - deferredName: deferred.name, - exit: Exit.void - })) +const ClockRpc = Rpc.make("run", { payload: ClockPayload }) + .annotate(ClusterSchema.Persisted, true) + .annotate(ClusterSchema.Uninterruptible, true) + +const ClockEntity = Entity.make("Workflow/-/DurableClock", [ClockRpc]) + +const ClockEntityLayer = ClockEntity.toLayer( + Effect.gen(function* () { + const engine = yield* WorkflowEngine + const address = yield* Entity.CurrentAddress + const executionId = address.entityId + return { + run(request) { + const deferred = DurableClock.make({ + name: request.payload.name, + duration: Duration.zero + }).deferred + return ensureSuccess( + engine.deferredDone(deferred, { + workflowName: request.payload.workflowName, + executionId, + deferredName: deferred.name, + exit: Exit.void + }) + ) + } } - } -})) + }) +) const InterruptSignal = DurableDeferred.make("Workflow/InterruptSignal") diff --git a/repos/effect/packages/cluster/src/Sharding.ts b/repos/effect/packages/cluster/src/Sharding.ts index 129637f..b85c856 100644 --- a/repos/effect/packages/cluster/src/Sharding.ts +++ b/repos/effect/packages/cluster/src/Sharding.ts @@ -30,11 +30,19 @@ import * as Schedule from "effect/Schedule" import * as Scope from "effect/Scope" import * as Stream from "effect/Stream" import type { MailboxFull, PersistenceError } from "./ClusterError.js" -import { AlreadyProcessingMessage, EntityNotAssignedToRunner } from "./ClusterError.js" +import { + AlreadyProcessingMessage, + EntityNotAssignedToRunner +} from "./ClusterError.js" import * as ClusterMetrics from "./ClusterMetrics.js" import { Persisted, Uninterruptible } from "./ClusterSchema.js" import * as ClusterSchema from "./ClusterSchema.js" -import type { CurrentAddress, CurrentRunnerAddress, Entity, HandlersFrom } from "./Entity.js" +import type { + CurrentAddress, + CurrentRunnerAddress, + Entity, + HandlersFrom +} from "./Entity.js" import type { EntityAddress } from "./EntityAddress.js" import { make as makeEntityAddress } from "./EntityAddress.js" import type { EntityId } from "./EntityId.js" @@ -56,8 +64,12 @@ import { Runners } from "./Runners.js" import { RunnerStorage } from "./RunnerStorage.js" import type { ShardId } from "./ShardId.js" import { make as makeShardId } from "./ShardId.js" -import { ShardingConfig } from "./ShardingConfig.js" -import { EntityRegistered, type ShardingRegistrationEvent, SingletonRegistered } from "./ShardingRegistrationEvent.js" +import { shardGroupConfig, ShardingConfig } from "./ShardingConfig.js" +import { + EntityRegistered, + type ShardingRegistrationEvent, + SingletonRegistered +} from "./ShardingRegistrationEvent.js" import { SingletonAddress } from "./SingletonAddress.js" import * as Snowflake from "./Snowflake.js" @@ -65,127 +77,140 @@ import * as Snowflake from "./Snowflake.js" * @since 1.0.0 * @category models */ -export class Sharding extends Context.Tag("@effect/cluster/Sharding") - - /** - * Returns the `ShardId` of the shard to which the entity at the specified - * `address` is assigned. - */ - readonly getShardId: (entityId: EntityId, group: string) => ShardId - - /** - * Returns `true` if the specified `shardId` is assigned to this runner. - */ - readonly hasShardId: (shardId: ShardId) => boolean - - /** - * Generate a Snowflake ID that is unique to this runner. - */ - readonly getSnowflake: Effect.Effect - - /** - * Returns `true` if sharding is shutting down, `false` otherwise. - */ - readonly isShutdown: Effect.Effect - - /** - * Constructs a `RpcClient` which can be used to send messages to the - * specified `Entity`. - */ - readonly makeClient: ( - entity: Entity - ) => Effect.Effect< - ( - entityId: string - ) => RpcClient.RpcClient.From< - Rpcs, - MailboxFull | AlreadyProcessingMessage | PersistenceError +export class Sharding extends Context.Tag("@effect/cluster/Sharding")< + Sharding, + { + /** + * Returns a stream of events that occur when the runner registers entities or + * singletons. + */ + readonly getRegistrationEvents: Stream.Stream + + /** + * Returns the `ShardId` of the shard to which the entity at the specified + * `address` is assigned. + */ + readonly getShardId: (entityId: EntityId, group: string) => ShardId + + /** + * Returns `true` if the specified `shardId` is assigned to this runner. + */ + readonly hasShardId: (shardId: ShardId) => boolean + + /** + * Generate a Snowflake ID that is unique to this runner. + */ + readonly getSnowflake: Effect.Effect + + /** + * Returns `true` if sharding is shutting down, `false` otherwise. + */ + readonly isShutdown: Effect.Effect + + /** + * Constructs a `RpcClient` which can be used to send messages to the + * specified `Entity`. + */ + readonly makeClient: ( + entity: Entity + ) => Effect.Effect< + ( + entityId: string + ) => RpcClient.RpcClient.From< + Rpcs, + MailboxFull | AlreadyProcessingMessage | PersistenceError + > > - > - - /** - * Registers a new entity with the runner. - */ - readonly registerEntity: , RX>( - entity: Entity, - handlers: Effect.Effect, - options?: { - readonly maxIdleTime?: DurationInput | undefined - readonly concurrency?: number | "unbounded" | undefined - readonly mailboxCapacity?: number | "unbounded" | undefined - readonly disableFatalDefects?: boolean | undefined - readonly defectRetryPolicy?: Schedule.Schedule | undefined - readonly spanAttributes?: Record | undefined - } - ) => Effect.Effect< - void, - never, - | Scope.Scope - | Rpc.Context - | Rpc.Middleware - | Exclude - > - - /** - * Registers a new singleton with the runner. - */ - readonly registerSingleton: ( - name: string, - run: Effect.Effect, - options?: { - readonly shardGroup?: string | undefined - } - ) => Effect.Effect - /** - * Sends a message to the specified entity. - */ - readonly send: (message: Message.Incoming) => Effect.Effect< - void, - EntityNotAssignedToRunner | MailboxFull | AlreadyProcessingMessage - > + /** + * Registers a new entity with the runner. + */ + readonly registerEntity: < + Type extends string, + Rpcs extends Rpc.Any, + Handlers extends HandlersFrom, + RX + >( + entity: Entity, + handlers: Effect.Effect, + options?: { + readonly maxIdleTime?: DurationInput | undefined + readonly concurrency?: number | "unbounded" | undefined + readonly mailboxCapacity?: number | "unbounded" | undefined + readonly disableFatalDefects?: boolean | undefined + readonly defectRetryPolicy?: Schedule.Schedule | undefined + readonly spanAttributes?: Record | undefined + } + ) => Effect.Effect< + void, + never, + | Scope.Scope + | Rpc.Context + | Rpc.Middleware + | Exclude + > - /** - * Sends an outgoing message - */ - readonly sendOutgoing: ( - message: Message.Outgoing, - discard: boolean - ) => Effect.Effect< - void, - MailboxFull | AlreadyProcessingMessage | PersistenceError - > - - /** - * Notify sharding that a message has been persisted to storage. - */ - readonly notify: (message: Message.Incoming, options?: { - readonly waitUntilRead?: boolean | undefined - }) => Effect.Effect< - void, - EntityNotAssignedToRunner | AlreadyProcessingMessage - > + /** + * Registers a new singleton with the runner. + */ + readonly registerSingleton: ( + name: string, + run: Effect.Effect, + options?: { + readonly shardGroup?: string | undefined + } + ) => Effect.Effect + + /** + * Sends a message to the specified entity. + */ + readonly send: ( + message: Message.Incoming + ) => Effect.Effect< + void, + EntityNotAssignedToRunner | MailboxFull | AlreadyProcessingMessage + > + + /** + * Sends an outgoing message + */ + readonly sendOutgoing: ( + message: Message.Outgoing, + discard: boolean + ) => Effect.Effect< + void, + MailboxFull | AlreadyProcessingMessage | PersistenceError + > + + /** + * Notify sharding that a message has been persisted to storage. + */ + readonly notify: ( + message: Message.Incoming, + options?: { + readonly waitUntilRead?: boolean | undefined + } + ) => Effect.Effect< + void, + EntityNotAssignedToRunner | AlreadyProcessingMessage + > - /** - * Reset the state of a message - */ - readonly reset: (requestId: Snowflake.Snowflake) => Effect.Effect + /** + * Reset the state of a message + */ + readonly reset: (requestId: Snowflake.Snowflake) => Effect.Effect - /** - * Trigger a storage read, which will read all unprocessed messages. - */ - readonly pollStorage: Effect.Effect + /** + * Trigger a storage read, which will read all unprocessed messages. + */ + readonly pollStorage: Effect.Effect - /** - * Retrieves the active entity count for the current runner. - */ - readonly activeEntityCount: Effect.Effect -}>() {} + /** + * Retrieves the active entity count for the current runner. + */ + readonly activeEntityCount: Effect.Effect + } +>() {} // ----------------------------------------------------------------------------- // Implementation @@ -197,8 +222,9 @@ interface EntityManagerState { status: "alive" | "closing" | "closed" } -const make = Effect.gen(function*() { +const make = Effect.gen(function* () { const config = yield* ShardingConfig + const shardGroups = shardGroupConfig(config) const clock = yield* Effect.clock const runnersService = yield* Runners @@ -208,7 +234,9 @@ const make = Effect.gen(function*() { const isShutdown = MutableRef.make(false) const fiberSet = yield* FiberSet.make() const runFork = yield* FiberSet.runtime(fiberSet)().pipe( - Effect.mapInputContext((context: Context.Context) => Context.omit(Scope.Scope)(context)) + Effect.mapInputContext((context: Context.Context) => + Context.omit(Scope.Scope)(context) + ) ) const storage = yield* MessageStorage.MessageStorage @@ -225,10 +253,12 @@ const make = Effect.gen(function*() { const activeShardsLatch = yield* Effect.makeLatch(false) const events = yield* PubSub.unbounded() - const getRegistrationEvents: Stream.Stream = Stream.fromPubSub(events) + const getRegistrationEvents: Stream.Stream = + Stream.fromPubSub(events) const isLocalRunner = (address: RunnerAddress) => - Option.isSome(config.runnerAddress) && Equal.equals(address, config.runnerAddress.value) + Option.isSome(config.runnerAddress) && + Equal.equals(address, config.runnerAddress.value) function getShardId(entityId: EntityId, group: string): ShardId { const id = Math.abs(hashString(entityId) % config.shardsPerGroup) + 1 @@ -241,10 +271,12 @@ const make = Effect.gen(function*() { yield* Scope.addFinalizer( shardingScope, - Effect.logDebug("Shutdown complete").pipe(Effect.annotateLogs({ - package: "@effect/cluster", - module: "Sharding" - })) + Effect.logDebug("Shutdown complete").pipe( + Effect.annotateLogs({ + package: "@effect/cluster", + module: "Sharding" + }) + ) ) // --- Shard acquisition --- @@ -264,7 +296,7 @@ const make = Effect.gen(function*() { const releaseShardsMap = yield* FiberMap.make() const releaseShard = Effect.fnUntraced( - function*(shardId: ShardId) { + function* (shardId: ShardId) { const fibers = Arr.empty>() for (const state of entityManagers.values()) { if (state.status === "closed") continue @@ -293,14 +325,14 @@ const make = Effect.gen(function*() { FiberMap.run(releaseShardsMap, shardId, { onlyIfMissing: true }) ) ) - const releaseShards = Effect.gen(function*() { + const releaseShards = Effect.gen(function* () { for (const shardId of releasingShards) { if (FiberMap.unsafeHas(releaseShardsMap, shardId)) continue yield* releaseShard(shardId) } }) - yield* Effect.gen(function*() { + yield* Effect.gen(function* () { activeShardsLatch.unsafeOpen() while (true) { @@ -322,7 +354,11 @@ const make = Effect.gen(function*() { // if a shard has been assigned to this runner, we acquire it const unacquiredShards = MutableHashSet.empty() for (const shardId of selfShards) { - if (MutableHashSet.has(acquiredShards, shardId) || MutableHashSet.has(releasingShards, shardId)) continue + if ( + MutableHashSet.has(acquiredShards, shardId) || + MutableHashSet.has(releasingShards, shardId) + ) + continue MutableHashSet.add(unacquiredShards, shardId) } @@ -330,21 +366,26 @@ const make = Effect.gen(function*() { continue } - const oacquired = yield* runnerStorage.acquire(selfAddress, unacquiredShards).pipe( - Effect.timeoutOption(config.shardLockRefreshInterval) - ) + const oacquired = yield* runnerStorage + .acquire(selfAddress, unacquiredShards) + .pipe(Effect.timeoutOption(config.shardLockRefreshInterval)) if (Option.isNone(oacquired)) { activeShardsLatch.unsafeOpen() continue } const acquired = oacquired.value - yield* storage.resetShards(acquired).pipe( - Effect.ignore, - Effect.timeoutOption(config.shardLockRefreshInterval) - ) + yield* storage + .resetShards(acquired) + .pipe( + Effect.ignore, + Effect.timeoutOption(config.shardLockRefreshInterval) + ) for (const shardId of acquired) { - if (MutableHashSet.has(releasingShards, shardId) || !MutableHashSet.has(selfShards, shardId)) { + if ( + MutableHashSet.has(releasingShards, shardId) || + !MutableHashSet.has(selfShards, shardId) + ) { continue } MutableHashSet.add(acquiredShards, shardId) @@ -354,13 +395,18 @@ const make = Effect.gen(function*() { yield* Effect.forkIn(syncSingletons, shardingScope) // update metrics - ClusterMetrics.shards.unsafeUpdate(BigInt(MutableHashSet.size(acquiredShards)), []) + ClusterMetrics.shards.unsafeUpdate( + BigInt(MutableHashSet.size(acquiredShards)), + [] + ) } yield* Effect.sleep(1000) activeShardsLatch.unsafeOpen() } }).pipe( - Effect.catchAllCause((cause) => Effect.logWarning("Could not acquire/release shards", cause)), + Effect.catchAllCause((cause) => + Effect.logWarning("Could not acquire/release shards", cause) + ), Effect.repeat(Schedule.spaced(config.entityMessagePollInterval)), Effect.annotateLogs({ package: "@effect/cluster", @@ -445,10 +491,12 @@ const make = Effect.gen(function*() { if (storageEnabled && Option.isSome(config.runnerAddress)) { const selfAddress = config.runnerAddress.value - const entityRegistrationTimeoutMillis = Duration.toMillis(config.entityRegistrationTimeout) + const entityRegistrationTimeoutMillis = Duration.toMillis( + config.entityRegistrationTimeout + ) const storageStartMillis = clock.unsafeCurrentTimeMillis() - yield* Effect.gen(function*() { + yield* Effect.gen(function* () { yield* Effect.logDebug("Starting") yield* Effect.addFinalizer(() => Effect.logDebug("Shutting down")) @@ -472,14 +520,17 @@ const make = Effect.gen(function*() { } const state = entityManagers.get(address.entityType) if (!state) { - const sinceStart = clock.unsafeCurrentTimeMillis() - storageStartMillis + const sinceStart = + clock.unsafeCurrentTimeMillis() - storageStartMillis if (sinceStart < entityRegistrationTimeoutMillis) { // reset address in the case that the entity is slow to register MutableHashSet.add(resetAddresses, address) return Effect.void } // if the entity did not register in time, we save a defect reply - return Effect.die(new Error(`Entity type '${address.entityType}' not registered`)) + return Effect.die( + new Error(`Entity type '${address.entityType}' not registered`) + ) } else if (state.status === "closed") { return Effect.void } @@ -494,7 +545,10 @@ const make = Effect.gen(function*() { // If the request is already processing, we skip it. // Or if the entity is closing, we skip all incoming messages. return Effect.void - } else if (message._tag === "IncomingRequest" && pendingNotifications.has(message.envelope.requestId)) { + } else if ( + message._tag === "IncomingRequest" && + pendingNotifications.has(message.envelope.requestId) + ) { const entry = pendingNotifications.get(message.envelope.requestId)! pendingNotifications.delete(message.envelope.requestId) removableNotifications.delete(entry) @@ -503,11 +557,17 @@ const make = Effect.gen(function*() { // If the entity was resuming in another fiber, we add the message // id to the unprocessed set. - const resumptionState = MutableHashMap.get(entityResumptionState, address) + const resumptionState = MutableHashMap.get( + entityResumptionState, + address + ) if (Option.isSome(resumptionState)) { resumptionState.value.unprocessed.add(message.envelope.requestId) if (message.envelope._tag === "Interrupt") { - resumptionState.value.interrupts.set(message.envelope.requestId, message as Message.IncomingEnvelope) + resumptionState.value.interrupts.set( + message.envelope.requestId, + message as Message.IncomingEnvelope + ) } return Effect.void } @@ -521,15 +581,21 @@ const make = Effect.gen(function*() { if (Cause.isInterrupted(cause)) { return Effect.void } - return Effect.ignore(storage.saveReply(Reply.ReplyWithContext.fromDefect({ - id: snowflakeGen.unsafeNext(), - requestId: message.envelope.requestId, - defect: Cause.squash(cause) - }))) + return Effect.ignore( + storage.saveReply( + Reply.ReplyWithContext.fromDefect({ + id: snowflakeGen.unsafeNext(), + requestId: message.envelope.requestId, + defect: Cause.squash(cause) + }) + ) + ) } if (error.left._tag === "MailboxFull") { // MailboxFull can only happen for requests, so this cast is safe - return resumeEntityFromStorage(message as Message.IncomingRequest) + return resumeEntityFromStorage( + message as Message.IncomingRequest + ) } return Effect.void } @@ -550,7 +616,9 @@ const make = Effect.gen(function*() { entityManagers.forEach((state) => state.manager.clearProcessed()) if (pendingNotifications.size > 0) { - pendingNotifications.forEach((entry) => removableNotifications.add(entry)) + pendingNotifications.forEach((entry) => + removableNotifications.add(entry) + ) } messages = yield* storage.unprocessedMessages(acquiredShards) @@ -560,15 +628,21 @@ const make = Effect.gen(function*() { if (removableNotifications.size > 0) { removableNotifications.forEach(({ message, resume }) => { pendingNotifications.delete(message.envelope.requestId) - resume(Effect.fail(new EntityNotAssignedToRunner({ address: message.envelope.address }))) + resume( + Effect.fail( + new EntityNotAssignedToRunner({ + address: message.envelope.address + }) + ) + ) }) removableNotifications.clear() } if (MutableHashSet.size(resetAddresses) > 0) { for (const address of resetAddresses) { - yield* Effect.logWarning("Could not find entity manager for address, retrying").pipe( - Effect.annotateLogs({ address }) - ) + yield* Effect.logWarning( + "Could not find entity manager for address, retrying" + ).pipe(Effect.annotateLogs({ address })) yield* Effect.forkIn(storage.resetAddress(address), shardingScope) } MutableHashSet.clear(resetAddresses) @@ -580,7 +654,9 @@ const make = Effect.gen(function*() { }).pipe( Effect.scoped, Effect.ensuring(storageReadLock.releaseAll), - Effect.catchAllCause((cause) => Effect.logWarning("Could not read messages from storage", cause)), + Effect.catchAllCause((cause) => + Effect.logWarning("Could not read messages from storage", cause) + ), Effect.forever, Effect.annotateLogs({ package: "@effect/cluster", @@ -602,15 +678,22 @@ const make = Effect.gen(function*() { ) // Resume unprocessed messages for entities that reached a full mailbox. - const entityResumptionState = MutableHashMap.empty - interrupts: Map - }>() - const resumeEntityFromStorage = (lastReceivedMessage: Message.IncomingRequest) => { + const entityResumptionState = MutableHashMap.empty< + EntityAddress, + { + unprocessed: Set + interrupts: Map + } + >() + const resumeEntityFromStorage = ( + lastReceivedMessage: Message.IncomingRequest + ) => { const address = lastReceivedMessage.envelope.address const resumptionState = MutableHashMap.get(entityResumptionState, address) if (Option.isSome(resumptionState)) { - resumptionState.value.unprocessed.add(lastReceivedMessage.envelope.requestId) + resumptionState.value.unprocessed.add( + lastReceivedMessage.envelope.requestId + ) return Effect.void } MutableHashMap.set(entityResumptionState, address, { @@ -620,14 +703,16 @@ const make = Effect.gen(function*() { return resumeEntityFromStorageImpl(address) } const resumeEntityFromStorageImpl = Effect.fnUntraced( - function*(address: EntityAddress) { + function* (address: EntityAddress) { const state = entityManagers.get(address.entityType) if (!state) { MutableHashMap.remove(entityResumptionState, address) return } - const resumptionState = Option.getOrThrow(MutableHashMap.get(entityResumptionState, address)) + const resumptionState = Option.getOrThrow( + MutableHashMap.get(entityResumptionState, address) + ) let done = false while (!done) { @@ -653,31 +738,33 @@ const make = Effect.gen(function*() { let index = 0 - const sendWithRetry: Effect.Effect< - void, - EntityNotAssignedToRunner - > = Effect.catchTags( - Effect.suspend(() => { - if (!MutableHashSet.has(acquiredShards, address.shardId)) { - return Effect.fail(new EntityNotAssignedToRunner({ address })) + const sendWithRetry: Effect.Effect = + Effect.catchTags( + Effect.suspend(() => { + if (!MutableHashSet.has(acquiredShards, address.shardId)) { + return Effect.fail(new EntityNotAssignedToRunner({ address })) + } + + const message = messages[index] + // check if this is a request that was interrupted + const interrupt = + message._tag === "IncomingRequest" && + resumptionState.interrupts.get(message.envelope.requestId) + return interrupt + ? Effect.flatMap(state.manager.send(message), () => { + resumptionState.interrupts.delete( + message.envelope.requestId + ) + return state.manager.send(interrupt) + }) + : state.manager.send(message) + }), + { + MailboxFull: () => + Effect.delay(sendWithRetry, config.sendRetryInterval), + AlreadyProcessingMessage: () => Effect.void } - - const message = messages[index] - // check if this is a request that was interrupted - const interrupt = message._tag === "IncomingRequest" && - resumptionState.interrupts.get(message.envelope.requestId) - return interrupt ? - Effect.flatMap(state.manager.send(message), () => { - resumptionState.interrupts.delete(message.envelope.requestId) - return state.manager.send(interrupt) - }) : - state.manager.send(message) - }), - { - MailboxFull: () => Effect.delay(sendWithRetry, config.sendRetryInterval), - AlreadyProcessingMessage: () => Effect.void - } - ) + ) yield* Effect.whileLoop({ while: () => index < messages.length, @@ -691,19 +778,23 @@ const make = Effect.gen(function*() { if (resumptionState.unprocessed.size > 0) continue // if we have caught up to the main storage loop, we let it take over - yield* withStorageReadLock(Effect.sync(() => { - if (resumptionState.unprocessed.size === 0) { - MutableHashMap.remove(entityResumptionState, address) - done = true - } - })) + yield* withStorageReadLock( + Effect.sync(() => { + if (resumptionState.unprocessed.size === 0) { + MutableHashMap.remove(entityResumptionState, address) + done = true + } + }) + ) } }, Effect.retry({ while: (e) => e._tag === "PersistenceError", schedule: Schedule.spaced(config.entityMessagePollInterval) }), - Effect.catchAllCause((cause) => Effect.logDebug("Could not resume unprocessed messages", cause)), + Effect.catchAllCause((cause) => + Effect.logDebug("Could not resume unprocessed messages", cause) + ), (effect, address) => Effect.annotateLogs(effect, { package: "@effect/cluster", @@ -715,7 +806,9 @@ const make = Effect.gen(function*() { (effect, address) => Effect.ensuring( effect, - Effect.sync(() => MutableHashMap.remove(entityResumptionState, address)) + Effect.sync(() => + MutableHashMap.remove(entityResumptionState, address) + ) ), Effect.withUnhandledErrorLogLevel(Option.none()), Effect.forkIn(shardingScope), @@ -725,7 +818,9 @@ const make = Effect.gen(function*() { // --- Sending messages --- - const sendLocal = | Message.Incoming>(message: M) => + const sendLocal = | Message.Incoming>( + message: M + ) => Effect.suspend(function loop(): Effect.Effect< void, | EntityNotAssignedToRunner @@ -740,25 +835,32 @@ const make = Effect.gen(function*() { const state = entityManagers.get(address.entityType) if (!state) { return Effect.flatMap(waitForEntityManager(address.entityType), loop) - } else if (state.status === "closed" || (state.status === "closing" && message._tag === "IncomingRequest")) { + } else if ( + state.status === "closed" || + (state.status === "closing" && message._tag === "IncomingRequest") + ) { // if we are shutting down, we don't accept new requests return Effect.fail(new EntityNotAssignedToRunner({ address })) } - return message._tag === "IncomingRequest" || message._tag === "IncomingEnvelope" ? - state.manager.send(message) : - runnersService.sendLocal({ - message, - send: state.manager.sendLocal, - simulateRemoteSerialization: config.simulateRemoteSerialization - }) as any + return message._tag === "IncomingRequest" || + message._tag === "IncomingEnvelope" + ? state.manager.send(message) + : (runnersService.sendLocal({ + message, + send: state.manager.sendLocal, + simulateRemoteSerialization: config.simulateRemoteSerialization + }) as any) }) type PendingNotification = { resume: (_: Effect.Effect) => void readonly message: Message.IncomingRequest } - const pendingNotifications = new Map() + const pendingNotifications = new Map< + Snowflake.Snowflake, + PendingNotification + >() const notifyLocal = | Message.Incoming>( message: M, discard: boolean, @@ -785,14 +887,26 @@ const make = Effect.gen(function*() { ? openStorageReadLatch : () => Effect.die("Sharding.notifyLocal: storage is disabled") - if (message._tag === "IncomingRequest" || message._tag === "IncomingEnvelope") { + if ( + message._tag === "IncomingRequest" || + message._tag === "IncomingEnvelope" + ) { if (!isLocal) { return Effect.fail(new EntityNotAssignedToRunner({ address })) } else if ( - message._tag === "IncomingRequest" && state.manager.isProcessingFor(message, { excludeReplies: true }) + message._tag === "IncomingRequest" && + state.manager.isProcessingFor(message, { excludeReplies: true }) + ) { + return Effect.fail( + new AlreadyProcessingMessage({ + address, + envelopeId: message.envelope.requestId + }) + ) + } else if ( + message._tag === "IncomingRequest" && + options?.waitUntilRead ) { - return Effect.fail(new AlreadyProcessingMessage({ address, envelopeId: message.envelope.requestId })) - } else if (message._tag === "IncomingRequest" && options?.waitUntilRead) { if (!storageEnabled) return notify() return Effect.async((resume) => { let entry = pendingNotifications.get(message.envelope.requestId) @@ -812,7 +926,12 @@ const make = Effect.gen(function*() { return notify() } - return runnersService.notifyLocal({ message, notify, discard, storageOnly: !isLocal }) as any + return runnersService.notifyLocal({ + message, + notify, + discard, + storageOnly: !isLocal + }) as any }) function sendOutgoing( @@ -828,10 +947,16 @@ const make = Effect.gen(function*() { const address = message.envelope.address const isPersisted = Context.get(message.rpc.annotations, Persisted) if (isPersisted && !storageEnabled) { - return Effect.die("Sharding.sendOutgoing: Persisted messages require MessageStorage") + return Effect.die( + "Sharding.sendOutgoing: Persisted messages require MessageStorage" + ) } - const maybeRunner = MutableHashMap.get(shardAssignments, address.shardId) - const runnerIsLocal = Option.isSome(maybeRunner) && isLocalRunner(maybeRunner.value) + const maybeRunner = MutableHashMap.get( + shardAssignments, + address.shardId + ) + const runnerIsLocal = + Option.isSome(maybeRunner) && isLocalRunner(maybeRunner.value) if (isPersisted) { return runnerIsLocal ? notifyLocal(message, discard) @@ -843,12 +968,17 @@ const make = Effect.gen(function*() { ? sendLocal(message) : runnersService.send({ address: maybeRunner.value, message }) }), - (error) => error._tag === "EntityNotAssignedToRunner" || error._tag === "RunnerUnavailable", + (error) => + error._tag === "EntityNotAssignedToRunner" || + error._tag === "RunnerUnavailable", (error) => { if (retries === 0) { return Effect.die(error) } - return Effect.delay(sendOutgoing(message, discard, retries && retries - 1), config.sendRetryInterval) + return Effect.delay( + sendOutgoing(message, discard, retries && retries - 1), + config.sendRetryInterval + ) } ) } @@ -868,13 +998,13 @@ const make = Effect.gen(function*() { // shard assignments for outgoing messages (they could still be in use by // entities that are shutting down). - const selfRunner = Option.isSome(config.runnerAddress) ? - new Runner({ - address: config.runnerAddress.value, - groups: config.shardGroups, - weight: config.runnerShardWeight - }) : - undefined + const selfRunner = Option.isSome(config.runnerAddress) + ? new Runner({ + address: config.runnerAddress.value, + groups: Array.from(shardGroups.assigned), + weight: config.runnerShardWeight + }) + : undefined let allRunners = MutableHashMap.empty() let healthyRunnerCount = 0 @@ -885,7 +1015,7 @@ const make = Effect.gen(function*() { ClusterMetrics.runnersHealthy.unsafeUpdate(BigInt(1), []) } - yield* Effect.gen(function*() { + yield* Effect.gen(function* () { const hashRings = new Map>() let nextRunners = MutableHashMap.empty() const healthyRunners = MutableHashSet.empty() @@ -893,9 +1023,15 @@ const make = Effect.gen(function*() { while (true) { // Ensure the current runner is registered - if (selfRunner && !isShutdown.current && !MutableHashMap.has(allRunners, selfRunner)) { + if ( + selfRunner && + !isShutdown.current && + !MutableHashMap.has(allRunners, selfRunner) + ) { yield* Effect.logDebug("Registering runner", selfRunner) - const machineId = yield* withTimeout(runnerStorage.register(selfRunner, true)) + const machineId = yield* withTimeout( + runnerStorage.register(selfRunner, true) + ) yield* snowflakeGen.setMachineId(machineId) } @@ -944,7 +1080,11 @@ const make = Effect.gen(function*() { healthyRunnerCount = MutableHashSet.size(healthyRunners) // Ensure the current runner is registered - if (selfRunner && !isShutdown.current && !MutableHashMap.has(allRunners, selfRunner)) { + if ( + selfRunner && + !isShutdown.current && + !MutableHashMap.has(allRunners, selfRunner) + ) { continue } @@ -982,7 +1122,9 @@ const make = Effect.gen(function*() { yield* Effect.logWarning("No healthy runners available") // to prevent a deadlock, we will mark the current node as healthy to // start the health check singleton again - yield* withTimeout(runnerStorage.setRunnerHealth(selfRunner.address, true)) + yield* withTimeout( + runnerStorage.setRunnerHealth(selfRunner.address, true) + ) } yield* Effect.sleep(config.refreshAssignmentsInterval) @@ -1011,151 +1153,176 @@ const make = Effect.gen(function*() { const clients: ResourceMap< Entity, - (entityId: string) => RpcClient.RpcClient< - any, - MailboxFull | AlreadyProcessingMessage - >, + ( + entityId: string + ) => RpcClient.RpcClient, never - > = yield* ResourceMap.make(Effect.fnUntraced(function*(entity: Entity) { - const client = yield* RpcClient.makeNoSerialization(entity.protocol, { - spanPrefix: `${entity.type}.client`, - disableTracing: !Context.get(entity.protocol.annotations, ClusterSchema.ClientTracingEnabled), - supportsAck: true, - generateRequestId: () => RequestId(snowflakeGen.unsafeNext()), - flatten: true, - onFromClient(options): Effect.Effect< - void, - MailboxFull | AlreadyProcessingMessage | PersistenceError - > { - const address = Context.unsafeGet(options.context, ClientAddressTag) - switch (options.message._tag) { - case "Request": { - const fiber = Option.getOrThrow(Fiber.getCurrentFiber()) - const id = Snowflake.Snowflake(options.message.id) - const rpc = entity.protocol.requests.get(options.message.tag)! - let respond: (reply: Reply.Reply) => Effect.Effect - if (!options.discard) { - const entry: ClientRequestEntry = { - rpc: rpc as any, - services: fiber.currentContext + > = yield* ResourceMap.make( + Effect.fnUntraced(function* (entity: Entity) { + const client = yield* RpcClient.makeNoSerialization(entity.protocol, { + spanPrefix: `${entity.type}.client`, + disableTracing: !Context.get( + entity.protocol.annotations, + ClusterSchema.ClientTracingEnabled + ), + supportsAck: true, + generateRequestId: () => RequestId(snowflakeGen.unsafeNext()), + flatten: true, + onFromClient( + options + ): Effect.Effect< + void, + MailboxFull | AlreadyProcessingMessage | PersistenceError + > { + const address = Context.unsafeGet(options.context, ClientAddressTag) + switch (options.message._tag) { + case "Request": { + const fiber = Option.getOrThrow(Fiber.getCurrentFiber()) + const id = Snowflake.Snowflake(options.message.id) + const rpc = entity.protocol.requests.get(options.message.tag)! + let respond: (reply: Reply.Reply) => Effect.Effect + if (!options.discard) { + const entry: ClientRequestEntry = { + rpc: rpc as any, + services: fiber.currentContext + } + clientRequests.set(id, entry) + respond = makeClientRespond(entry, client.write) + } else { + respond = clientRespondDiscard } - clientRequests.set(id, entry) - respond = makeClientRespond(entry, client.write) - } else { - respond = clientRespondDiscard - } - return sendOutgoing( - new Message.OutgoingRequest({ - envelope: Envelope.makeRequest({ - requestId: id, - address, - tag: options.message.tag, - payload: options.message.payload, - headers: options.message.headers, - traceId: options.message.traceId, - spanId: options.message.spanId, - sampled: options.message.sampled + return sendOutgoing( + new Message.OutgoingRequest({ + envelope: Envelope.makeRequest({ + requestId: id, + address, + tag: options.message.tag, + payload: options.message.payload, + headers: options.message.headers, + traceId: options.message.traceId, + spanId: options.message.spanId, + sampled: options.message.sampled + }), + lastReceivedReply: Option.none(), + rpc, + context: fiber.currentContext as Context.Context, + respond }), - lastReceivedReply: Option.none(), - rpc, - context: fiber.currentContext as Context.Context, - respond - }), - options.discard - ) - } - case "Ack": { - const requestId = Snowflake.Snowflake(options.message.requestId) - const entry = clientRequests.get(requestId) - if (!entry) return Effect.void - return sendOutgoing( - new Message.OutgoingEnvelope({ - envelope: new Envelope.AckChunk({ - id: snowflakeGen.unsafeNext(), - address, - requestId, - replyId: entry.lastChunkId! + options.discard + ) + } + case "Ack": { + const requestId = Snowflake.Snowflake(options.message.requestId) + const entry = clientRequests.get(requestId) + if (!entry) return Effect.void + return sendOutgoing( + new Message.OutgoingEnvelope({ + envelope: new Envelope.AckChunk({ + id: snowflakeGen.unsafeNext(), + address, + requestId, + replyId: entry.lastChunkId! + }), + rpc: entry.rpc }), - rpc: entry.rpc - }), - false - ) - } - case "Interrupt": { - const requestId = Snowflake.Snowflake(options.message.requestId) - const entry = clientRequests.get(requestId)! - if (!entry) return Effect.void - clientRequests.delete(requestId) - if (Uninterruptible.forClient(entry.rpc.annotations)) { - return Effect.void + false + ) } - // for durable messages, we ignore interrupts on shutdown or as a - // result of a shard being resassigned - const isTransientInterrupt = MutableRef.get(isShutdown) || - options.message.interruptors.some((id) => internalInterruptors.has(id)) - if (isTransientInterrupt && Context.get(entry.rpc.annotations, Persisted)) { - return Effect.void + case "Interrupt": { + const requestId = Snowflake.Snowflake(options.message.requestId) + const entry = clientRequests.get(requestId)! + if (!entry) return Effect.void + clientRequests.delete(requestId) + if (Uninterruptible.forClient(entry.rpc.annotations)) { + return Effect.void + } + // for durable messages, we ignore interrupts on shutdown or as a + // result of a shard being resassigned + const isTransientInterrupt = + MutableRef.get(isShutdown) || + options.message.interruptors.some((id) => + internalInterruptors.has(id) + ) + if ( + isTransientInterrupt && + Context.get(entry.rpc.annotations, Persisted) + ) { + return Effect.void + } + return Effect.ignore( + sendOutgoing( + new Message.OutgoingEnvelope({ + envelope: new Envelope.Interrupt({ + id: snowflakeGen.unsafeNext(), + address, + requestId + }), + rpc: entry.rpc + }), + false, + 3 + ) + ) } - return Effect.ignore(sendOutgoing( - new Message.OutgoingEnvelope({ - envelope: new Envelope.Interrupt({ - id: snowflakeGen.unsafeNext(), - address, - requestId - }), - rpc: entry.rpc - }), - false, - 3 - )) } + return Effect.void } - return Effect.void - } - }) - - yield* Scope.addFinalizer( - yield* Effect.scope, - Effect.fiberIdWith((fiberId) => { - internalInterruptors.add(fiberId) - return Effect.void }) - ) - return (entityId: string) => { - const id = makeEntityId(entityId) - const address = ClientAddressTag.context(makeEntityAddress({ - shardId: getShardId(id, entity.getShardGroup(entityId as EntityId)), - entityId: id, - entityType: entity.type - })) - const clientFn = function(tag: string, payload: any, options?: { - readonly context?: Context.Context - }) { - const context = options?.context ? Context.merge(options.context, address) : address - return client.client(tag, payload, { - ...options, - context + yield* Scope.addFinalizer( + yield* Effect.scope, + Effect.fiberIdWith((fiberId) => { + internalInterruptors.add(fiberId) + return Effect.void }) - } - const proxyClient: any = {} - return new Proxy(proxyClient, { - has(_, p) { - return entity.protocol.requests.has(p as string) - }, - get(target, p) { - if (p in target) { - return target[p] - } else if (!entity.protocol.requests.has(p as string)) { - return undefined + ) + + return (entityId: string) => { + const id = makeEntityId(entityId) + const address = ClientAddressTag.context( + makeEntityAddress({ + shardId: getShardId(id, entity.getShardGroup(entityId as EntityId)), + entityId: id, + entityType: entity.type + }) + ) + const clientFn = function ( + tag: string, + payload: any, + options?: { + readonly context?: Context.Context } - return target[p] = (payload: any, options?: {}) => clientFn(p as string, payload, options) + ) { + const context = options?.context + ? Context.merge(options.context, address) + : address + return client.client(tag, payload, { + ...options, + context + }) } - }) - } - })) + const proxyClient: any = {} + return new Proxy(proxyClient, { + has(_, p) { + return entity.protocol.requests.has(p as string) + }, + get(target, p) { + if (p in target) { + return target[p] + } else if (!entity.protocol.requests.has(p as string)) { + return undefined + } + return (target[p] = (payload: any, options?: {}) => + clientFn(p as string, payload, options)) + } + }) + } + }) + ) - const makeClient = (entity: Entity): Effect.Effect< + const makeClient = ( + entity: Entity + ): Effect.Effect< ( entityId: string ) => RpcClient.RpcClient.From @@ -1163,41 +1330,45 @@ const make = Effect.gen(function*() { const clientRespondDiscard = (_reply: Reply.Reply) => Effect.void - const makeClientRespond = ( - entry: ClientRequestEntry, - write: (reply: FromServer) => Effect.Effect - ) => - (reply: Reply.Reply) => { - switch (reply._tag) { - case "Chunk": { - entry.lastChunkId = reply.id - return write({ - _tag: "Chunk", - clientId: 0, - requestId: RequestId(reply.requestId), - values: reply.values - }) - } - case "WithExit": { - clientRequests.delete(reply.requestId) - return write({ - _tag: "Exit", - clientId: 0, - requestId: RequestId(reply.requestId), - exit: reply.exit - }) + const makeClientRespond = + ( + entry: ClientRequestEntry, + write: (reply: FromServer) => Effect.Effect + ) => + (reply: Reply.Reply) => { + switch (reply._tag) { + case "Chunk": { + entry.lastChunkId = reply.id + return write({ + _tag: "Chunk", + clientId: 0, + requestId: RequestId(reply.requestId), + values: reply.values + }) + } + case "WithExit": { + clientRequests.delete(reply.requestId) + return write({ + _tag: "Exit", + clientId: 0, + requestId: RequestId(reply.requestId), + exit: reply.exit + }) + } } } - } // --- Singletons --- - const singletons = new Map>>() + const singletons = new Map< + ShardId, + MutableHashMap.MutableHashMap> + >() const singletonFibers = yield* FiberMap.make() const withSingletonLock = Effect.unsafeMakeSemaphore(1).withPermits(1) - const registerSingleton: Sharding["Type"]["registerSingleton"] = Effect.fnUntraced( - function*(name, run, options) { + const registerSingleton: Sharding["Type"]["registerSingleton"] = + Effect.fnUntraced(function* (name, run, options) { const shardGroup = options?.shardGroup ?? "default" const address = new SingletonAddress({ shardId: getShardId(makeEntityId(name), shardGroup), @@ -1237,30 +1408,32 @@ const make = Effect.gen(function*() { MutableHashMap.remove(map, address) return FiberMap.remove(singletonFibers, address) }) - }, - withSingletonLock - ) - - const syncSingletons = withSingletonLock(Effect.gen(function*() { - for (const [shardId, map] of singletons) { - for (const [address, run] of map) { - const running = FiberMap.unsafeHas(singletonFibers, address) - const shouldBeRunning = MutableHashSet.has(acquiredShards, shardId) - if (running && !shouldBeRunning) { - yield* Effect.logDebug("Stopping singleton", address) - internalInterruptors.add(Option.getOrThrow(Fiber.getCurrentFiber()).id()) - yield* FiberMap.remove(singletonFibers, address) - } else if (!running && shouldBeRunning) { - yield* Effect.logDebug("Starting singleton", address) - yield* FiberMap.run(singletonFibers, address, run) + }, withSingletonLock) + + const syncSingletons = withSingletonLock( + Effect.gen(function* () { + for (const [shardId, map] of singletons) { + for (const [address, run] of map) { + const running = FiberMap.unsafeHas(singletonFibers, address) + const shouldBeRunning = MutableHashSet.has(acquiredShards, shardId) + if (running && !shouldBeRunning) { + yield* Effect.logDebug("Stopping singleton", address) + internalInterruptors.add( + Option.getOrThrow(Fiber.getCurrentFiber()).id() + ) + yield* FiberMap.remove(singletonFibers, address) + } else if (!running && shouldBeRunning) { + yield* Effect.logDebug("Starting singleton", address) + yield* FiberMap.run(singletonFibers, address, run) + } } } - } - ClusterMetrics.singletons.unsafeUpdate( - BigInt(yield* FiberMap.size(singletonFibers)), - [] - ) - })) + ClusterMetrics.singletons.unsafeUpdate( + BigInt(yield* FiberMap.size(singletonFibers)), + [] + ) + }) + ) // --- Entities --- @@ -1269,8 +1442,12 @@ const make = Effect.gen(function*() { const entityManagerLatches = new Map() const registerEntity: Sharding["Type"]["registerEntity"] = Effect.fnUntraced( - function*(entity, build, options) { - if (Option.isNone(config.runnerAddress) || entityManagers.has(entity.type)) return + function* (entity, build, options) { + if ( + Option.isNone(config.runnerAddress) || + entityManagers.has(entity.type) + ) + return const scope = yield* Effect.scope yield* Scope.addFinalizer( scope, @@ -1284,11 +1461,13 @@ const make = Effect.gen(function*() { runnerAddress: config.runnerAddress.value, sharding }).pipe( - Effect.provide(context.pipe( - Context.add(EntityReaper, reaper), - Context.add(Scope.Scope, scope), - Context.add(Snowflake.Generator, snowflakeGen) - )) + Effect.provide( + context.pipe( + Context.add(EntityReaper, reaper), + Context.add(Scope.Scope, scope), + Context.add(Snowflake.Generator, snowflakeGen) + ) + ) ) as Effect.Effect const state: EntityManagerState = { entity, @@ -1308,13 +1487,15 @@ const make = Effect.gen(function*() { // register entities while storage is idle // this ensures message order is preserved - yield* withStorageReadLock(Effect.sync(() => { - entityManagers.set(entity.type, state) - if (entityManagerLatches.has(entity.type)) { - entityManagerLatches.get(entity.type)!.unsafeOpen() - entityManagerLatches.delete(entity.type) - } - })) + yield* withStorageReadLock( + Effect.sync(() => { + entityManagers.set(entity.type, state) + if (entityManagerLatches.has(entity.type)) { + entityManagerLatches.get(entity.type)!.unsafeOpen() + entityManagerLatches.delete(entity.type) + } + }) + ) yield* PubSub.publish(events, EntityRegistered({ entity })) } @@ -1338,7 +1519,9 @@ const make = Effect.gen(function*() { if (isAlive) { healthyRunnerCount++ return Effect.logDebug(`Runner is healthy`, runner).pipe( - Effect.andThen(runnerStorage.setRunnerHealth(runner.address, isAlive)) + Effect.andThen( + runnerStorage.setRunnerHealth(runner.address, isAlive) + ) ) } if (healthyRunnerCount <= 1) { @@ -1353,16 +1536,21 @@ const make = Effect.gen(function*() { yield* registerSingleton( "effect/cluster/Sharding/RunnerHealth", - Effect.gen(function*() { + Effect.gen(function* () { while (true) { // Skip health checks if we are the only runner if (MutableHashMap.size(allRunners) > 1) { - yield* Effect.forEach(allRunners, checkRunner, { discard: true, concurrency: 10 }) + yield* Effect.forEach(allRunners, checkRunner, { + discard: true, + concurrency: 10 + }) } yield* Effect.sleep(config.runnerHealthCheckInterval) } }).pipe( - Effect.catchAllCause((cause) => Effect.logDebug("Runner health check failed", cause)), + Effect.catchAllCause((cause) => + Effect.logDebug("Runner health check failed", cause) + ), Effect.forever, Effect.annotateLogs({ package: "@effect/cluster", @@ -1375,9 +1563,14 @@ const make = Effect.gen(function*() { // --- Finalization --- - const shutdown = Effect.fnUntraced(function*(exit?: Exit.Exit) { + const shutdown = Effect.fnUntraced(function* ( + exit?: Exit.Exit + ) { if (exit) { - yield* Effect.logDebug("Shutting down", exit._tag === "Failure" ? exit.cause : {}).pipe( + yield* Effect.logDebug( + "Shutting down", + exit._tag === "Failure" ? exit.cause : {} + ).pipe( Effect.annotateLogs({ package: "@effect/cluster", module: "Sharding" @@ -1396,7 +1589,7 @@ const make = Effect.gen(function*() { yield* Scope.addFinalizerExit(shardingScope, shutdown) - const activeEntityCount = Effect.gen(function*() { + const activeEntityCount = Effect.gen(function* () { let count = 0 for (const state of entityManagers.values()) { count += yield* state.manager.activeEntityCount @@ -1434,11 +1627,17 @@ const make = Effect.gen(function*() { export const layer: Layer.Layer< Sharding, never, - ShardingConfig | Runners | MessageStorage.MessageStorage | RunnerStorage | RunnerHealth.RunnerHealth + | ShardingConfig + | Runners + | MessageStorage.MessageStorage + | RunnerStorage + | RunnerHealth.RunnerHealth > = Layer.scoped(Sharding)(make).pipe( Layer.provide([Snowflake.layerGenerator, EntityReaper.Default]) ) // Utilities -const ClientAddressTag = Context.GenericTag("@effect/cluster/Sharding/ClientAddress") +const ClientAddressTag = Context.GenericTag( + "@effect/cluster/Sharding/ClientAddress" +) diff --git a/repos/effect/packages/cluster/src/ShardingConfig.ts b/repos/effect/packages/cluster/src/ShardingConfig.ts index 20f80b3..9a4e1ae 100644 --- a/repos/effect/packages/cluster/src/ShardingConfig.ts +++ b/repos/effect/packages/cluster/src/ShardingConfig.ts @@ -18,112 +18,126 @@ import { RunnerAddress } from "./RunnerAddress.js" * @since 1.0.0 * @category models */ -export class ShardingConfig extends Context.Tag("@effect/cluster/ShardingConfig") - /** - * The listen address for the current runner. - * - * Defaults to the `runnerAddress`. - */ - readonly runnerListenAddress: Option.Option - /** - * A number that determines how many shards this runner will be assigned - * relative to other runners. - * - * Defaults to `1`. - * - * A value of `2` means that this runner should be assigned twice as many - * shards as a runner with a weight of `1`. - */ - readonly runnerShardWeight: number - /** - * The shard groups that are assigned to this runner. - * - * Defaults to `["default"]`. - */ - readonly shardGroups: ReadonlyArray - /** - * The number of shards to allocate per shard group. - * - * **Note**: this value should be consistent across all runners. - */ - readonly shardsPerGroup: number - /** - * Shard lock refresh interval. - */ - readonly shardLockRefreshInterval: DurationInput - /** - * Shard lock expiration duration. - */ - readonly shardLockExpiration: DurationInput - /** - * Disable the use of advisory locks for shard locking. - */ - readonly shardLockDisableAdvisory: boolean - /** - * Start shutting down as soon as an Entity has started shutting down. - * - * Defaults to `true`. - */ - readonly preemptiveShutdown: boolean - /** - * The default capacity of the mailbox for entities. - */ - readonly entityMailboxCapacity: number | "unbounded" - /** - * The maximum duration of inactivity (i.e. without receiving a message) - * after which an entity will be interrupted. - */ - readonly entityMaxIdleTime: DurationInput - /** - * If an entity does not register itself within this time after a message is - * sent to it, the message will be marked as failed. - * - * Defaults to 1 minute. - */ - readonly entityRegistrationTimeout: DurationInput - /** - * The maximum duration of time to wait for an entity to terminate. - * - * By default this is set to 15 seconds to stay within kubernetes defaults. - */ - readonly entityTerminationTimeout: DurationInput - /** - * The interval at which to poll for unprocessed messages from storage. - */ - readonly entityMessagePollInterval: DurationInput - /** - * The interval at which to poll for client replies from storage. - */ - readonly entityReplyPollInterval: DurationInput - /** - * The interval at which to poll for new runners and refresh shard - * assignments. - */ - readonly refreshAssignmentsInterval: DurationInput - /** - * The interval to retry a send if EntityNotAssignedToRunner is returned. - */ - readonly sendRetryInterval: DurationInput - /** - * The interval at which to check for unhealthy runners and report them - */ - readonly runnerHealthCheckInterval: DurationInput - /** - * Simulate serialization and deserialization to remote runners for local - * entities. - */ - readonly simulateRemoteSerialization: boolean -}>() {} +export class ShardingConfig extends Context.Tag( + "@effect/cluster/ShardingConfig" +)< + ShardingConfig, + { + /** + * The address for the current runner that other runners can use to + * communicate with it. + * + * If `None`, the runner is not part of the cluster and will be in a client-only + * mode. + */ + readonly runnerAddress: Option.Option + /** + * The listen address for the current runner. + * + * Defaults to the `runnerAddress`. + */ + readonly runnerListenAddress: Option.Option + /** + * A number that determines how many shards this runner will be assigned + * relative to other runners. + * + * Defaults to `1`. + * + * A value of `2` means that this runner should be assigned twice as many + * shards as a runner with a weight of `1`. + */ + readonly runnerShardWeight: number + /** + * The shard groups available across all runners. + * + * Defaults to `["default"]`. + */ + readonly availableShardGroups: ReadonlyArray + /** + * The shard groups that are assigned to this runner. + * + * Defaults to `["default"]`. + */ + readonly assignedShardGroups: ReadonlyArray + /** + * The number of shards to allocate per shard group. + * + * **Note**: this value should be consistent across all runners. + */ + readonly shardsPerGroup: number + /** + * Shard lock refresh interval. + */ + readonly shardLockRefreshInterval: DurationInput + /** + * Shard lock expiration duration. + */ + readonly shardLockExpiration: DurationInput + /** + * Disable the use of advisory locks for shard locking. + */ + readonly shardLockDisableAdvisory: boolean + /** + * Start shutting down as soon as an Entity has started shutting down. + * + * Defaults to `true`. + */ + readonly preemptiveShutdown: boolean + /** + * The default capacity of the mailbox for entities. + */ + readonly entityMailboxCapacity: number | "unbounded" + /** + * The maximum duration of inactivity (i.e. without receiving a message) + * after which an entity will be interrupted. + */ + readonly entityMaxIdleTime: DurationInput + /** + * If an entity does not register itself within this time after a message is + * sent to it, the message will be marked as failed. + * + * Defaults to 1 minute. + */ + readonly entityRegistrationTimeout: DurationInput + /** + * The maximum duration of time to wait for an entity to terminate. + * + * By default this is set to 15 seconds to stay within kubernetes defaults. + */ + readonly entityTerminationTimeout: DurationInput + /** + * The interval at which to poll for unprocessed messages from storage. + */ + readonly entityMessagePollInterval: DurationInput + /** + * The interval at which to poll for client replies from storage. + */ + readonly entityReplyPollInterval: DurationInput + /** + * The interval at which to poll for new runners and refresh shard + * assignments. + */ + readonly refreshAssignmentsInterval: DurationInput + /** + * The interval to retry a send if EntityNotAssignedToRunner is returned. + */ + readonly sendRetryInterval: DurationInput + /** + * The interval at which to check for unhealthy runners and report them + */ + readonly runnerHealthCheckInterval: DurationInput + /** + * Simulate serialization and deserialization to remote runners for local + * entities. + */ + readonly simulateRemoteSerialization: boolean + } +>() {} -const defaultRunnerAddress = RunnerAddress.make({ host: "localhost", port: 34431 }) +const defaultRunnerAddress = RunnerAddress.make({ + host: "localhost", + port: 34431 +}) /** * @since 1.0.0 @@ -134,7 +148,8 @@ export const defaults: ShardingConfig["Type"] = { runnerListenAddress: Option.none(), runnerShardWeight: 1, shardsPerGroup: 300, - shardGroups: ["default"], + availableShardGroups: ["default"], + assignedShardGroups: ["default"], preemptiveShutdown: true, shardLockRefreshInterval: Duration.seconds(10), shardLockExpiration: Duration.seconds(35), @@ -155,7 +170,9 @@ export const defaults: ShardingConfig["Type"] = { * @since 1.0.0 * @category Layers */ -export const layer = (options?: Partial): Layer.Layer => +export const layer = ( + options?: Partial +): Layer.Layer => Layer.succeed(ShardingConfig, { ...defaults, ...options }) /** @@ -178,7 +195,10 @@ export const config: Config.Config = Config.all({ Config.withDefault(defaultRunnerAddress.port), Config.withDescription("The port used for inter-runner communication.") ) - }).pipe(Config.map((options) => RunnerAddress.make(options)), Config.option), + }).pipe( + Config.map((options) => RunnerAddress.make(options)), + Config.option + ), runnerListenAddress: Config.all({ host: Config.string("listenHost").pipe( Config.withDescription("The host to listen on.") @@ -187,11 +207,20 @@ export const config: Config.Config = Config.all({ Config.withDefault(defaultRunnerAddress.port), Config.withDescription("The port to listen on.") ) - }).pipe(Config.map((options) => RunnerAddress.make(options)), Config.option), + }).pipe( + Config.map((options) => RunnerAddress.make(options)), + Config.option + ), runnerShardWeight: Config.integer("runnerShardWeight").pipe( Config.withDefault(defaults.runnerShardWeight) ), - shardGroups: Config.array(Config.string("shardGroups")).pipe( + availableShardGroups: Config.array( + Config.string("availableShardGroups") + ).pipe( + Config.withDefault(["default"]), + Config.withDescription("The shard groups available across all runners.") + ), + assignedShardGroups: Config.array(Config.string("shardGroups")).pipe( Config.withDefault(["default"]), Config.withDescription("The shard groups that are assigned to this runner.") ), @@ -201,7 +230,9 @@ export const config: Config.Config = Config.all({ ), preemptiveShutdown: Config.boolean("preemptiveShutdown").pipe( Config.withDefault(defaults.preemptiveShutdown), - Config.withDescription("Start shutting down as soon as an Entity has started shutting down.") + Config.withDescription( + "Start shutting down as soon as an Entity has started shutting down." + ) ), shardLockRefreshInterval: Config.duration("shardLockRefreshInterval").pipe( Config.withDefault(defaults.shardLockRefreshInterval), @@ -213,7 +244,9 @@ export const config: Config.Config = Config.all({ ), shardLockDisableAdvisory: Config.boolean("shardLockDisableAdvisory").pipe( Config.withDefault(defaults.shardLockDisableAdvisory), - Config.withDescription("Disable the use of advisory locks for shard locking.") + Config.withDescription( + "Disable the use of advisory locks for shard locking." + ) ), entityMailboxCapacity: Config.integer("entityMailboxCapacity").pipe( Config.withDefault(defaults.entityMailboxCapacity), @@ -233,31 +266,49 @@ export const config: Config.Config = Config.all({ ), entityTerminationTimeout: Config.duration("entityTerminationTimeout").pipe( Config.withDefault(defaults.entityTerminationTimeout), - Config.withDescription("The maximum duration of time to wait for an entity to terminate.") + Config.withDescription( + "The maximum duration of time to wait for an entity to terminate." + ) ), entityMessagePollInterval: Config.duration("entityMessagePollInterval").pipe( Config.withDefault(defaults.entityMessagePollInterval), - Config.withDescription("The interval at which to poll for unprocessed messages from storage.") + Config.withDescription( + "The interval at which to poll for unprocessed messages from storage." + ) ), entityReplyPollInterval: Config.duration("entityReplyPollInterval").pipe( Config.withDefault(defaults.entityReplyPollInterval), - Config.withDescription("The interval at which to poll for client replies from storage.") + Config.withDescription( + "The interval at which to poll for client replies from storage." + ) ), sendRetryInterval: Config.duration("sendRetryInterval").pipe( Config.withDefault(defaults.sendRetryInterval), - Config.withDescription("The interval to retry a send if EntityNotAssignedToRunner is returned.") + Config.withDescription( + "The interval to retry a send if EntityNotAssignedToRunner is returned." + ) ), - refreshAssignmentsInterval: Config.duration("refreshAssignmentsInterval").pipe( + refreshAssignmentsInterval: Config.duration( + "refreshAssignmentsInterval" + ).pipe( Config.withDefault(defaults.refreshAssignmentsInterval), - Config.withDescription("The interval at which to refresh shard assignments.") + Config.withDescription( + "The interval at which to refresh shard assignments." + ) ), runnerHealthCheckInterval: Config.duration("runnerHealthCheckInterval").pipe( Config.withDefault(defaults.runnerHealthCheckInterval), - Config.withDescription("The interval at which to check for unhealthy runners and report them.") + Config.withDescription( + "The interval at which to check for unhealthy runners and report them." + ) ), - simulateRemoteSerialization: Config.boolean("simulateRemoteSerialization").pipe( + simulateRemoteSerialization: Config.boolean( + "simulateRemoteSerialization" + ).pipe( Config.withDefault(defaults.simulateRemoteSerialization), - Config.withDescription("Simulate serialization and deserialization to remote runners for local entities.") + Config.withDescription( + "Simulate serialization and deserialization to remote runners for local entities." + ) ) }) @@ -267,9 +318,7 @@ export const config: Config.Config = Config.all({ */ export const configFromEnv = config.pipe( Effect.withConfigProvider( - ConfigProvider.fromEnv().pipe( - ConfigProvider.constantCase - ) + ConfigProvider.fromEnv().pipe(ConfigProvider.constantCase) ) ) @@ -277,11 +326,35 @@ export const configFromEnv = config.pipe( * @since 1.0.0 * @category Layers */ -export const layerFromEnv = (options?: Partial | undefined): Layer.Layer< - ShardingConfig, - ConfigError -> => +export const layerFromEnv = ( + options?: Partial | undefined +): Layer.Layer => Layer.effect( ShardingConfig, - options ? Effect.map(configFromEnv, (config) => ({ ...config, ...options })) : configFromEnv + options + ? Effect.map(configFromEnv, (config) => ({ ...config, ...options })) + : configFromEnv ) + +/** + * Normalizes the provided `ShardingConfig` to calculate the available and + * assigned shard groups. + * + * @since 1.0.0 + * @category Shard groups + */ +export const shardGroupConfig = ( + config: ShardingConfig["Type"] +): { + readonly available: ReadonlySet + readonly assigned: ReadonlySet +} => { + const available = new Set(config.availableShardGroups.slice().sort()) + const assigned = new Set() + available.forEach((group) => { + if (config.assignedShardGroups.includes(group)) { + assigned.add(group) + } + }) + return { available, assigned } +} diff --git a/repos/effect/packages/cluster/src/SqlRunnerStorage.ts b/repos/effect/packages/cluster/src/SqlRunnerStorage.ts index 99f2033..a96e15d 100644 --- a/repos/effect/packages/cluster/src/SqlRunnerStorage.ts +++ b/repos/effect/packages/cluster/src/SqlRunnerStorage.ts @@ -21,10 +21,12 @@ const withTracerDisabled = Effect.withTracerEnabled(false) * @since 1.0.0 * @category Constructors */ -export const make = Effect.fnUntraced(function*(options: { +export const make = Effect.fnUntraced(function* (options: { readonly prefix?: string | undefined }) { const config = yield* ShardingConfig.ShardingConfig + const shardGroups = ShardingConfig.shardGroupConfig(config) + const availableShardGroups = Array.from(shardGroups.available) const disableAdvisoryLocks = config.shardLockDisableAdvisory const sql = (yield* SqlClient.SqlClient).withoutTransforms() const prefix = options?.prefix ?? "cluster" @@ -32,19 +34,20 @@ export const make = Effect.fnUntraced(function*(options: { const acquireLockConn = sql.onDialectOrElse({ pg: () => - Effect.fnUntraced(function*(scope: Scope.Scope) { - const conn = yield* Effect.orDie(sql.reserve).pipe( - Scope.extend(scope) + Effect.fnUntraced(function* (scope: Scope.Scope) { + const conn = yield* Effect.orDie(sql.reserve).pipe(Scope.extend(scope)) + const pid = (yield* conn.executeValues( + "SELECT pg_backend_pid()", + [] + ))[0][0] as number + yield* Scope.addFinalizerExit(scope, () => + Effect.orDie(conn.executeRaw("SELECT pg_advisory_unlock_all()", [])) ) - const pid = (yield* conn.executeValues("SELECT pg_backend_pid()", []))[0][0] as number - yield* Scope.addFinalizerExit(scope, () => Effect.orDie(conn.executeRaw("SELECT pg_advisory_unlock_all()", []))) return [conn, pid] as const }, Effect.orDie), mysql: () => - Effect.fnUntraced(function*(scope: Scope.Scope) { - const conn = yield* Effect.orDie(sql.reserve).pipe( - Scope.extend(scope) - ) + Effect.fnUntraced(function* (scope: Scope.Scope) { + const conn = yield* Effect.orDie(sql.reserve).pipe(Scope.extend(scope)) // we need to get the connection id using IS_USED_LOCK to properly // support vitess let pid: number | undefined = undefined @@ -57,21 +60,26 @@ export const make = Effect.fnUntraced(function*(options: { if (taken[0] === null) continue pid = taken[1] } - yield* Scope.addFinalizerExit(scope, () => Effect.orDie(conn.executeRaw("SELECT RELEASE_ALL_LOCKS()", []))) + yield* Scope.addFinalizerExit(scope, () => + Effect.orDie(conn.executeRaw("SELECT RELEASE_ALL_LOCKS()", [])) + ) return [conn, pid] as const }, Effect.orDie), orElse: () => undefined }) - const lockConn = acquireLockConn && (yield* ResourceRef.from(yield* Effect.scope, acquireLockConn)) + const lockConn = + acquireLockConn && + (yield* ResourceRef.from(yield* Effect.scope, acquireLockConn)) const runnersTable = table("runners") const runnersTableSql = sql(runnersTable) // Migrate old tables if they exist // TODO: Remove in next major version - const hasOldTables = yield* sql`SELECT shard_id FROM ${sql(table("shards"))} LIMIT 1`.pipe( - Effect.isSuccess - ) + const hasOldTables = + yield* sql`SELECT shard_id FROM ${sql(table("shards"))} LIMIT 1`.pipe( + Effect.isSuccess + ) if (hasOldTables) { yield* sql`DROP TABLE ${sql(table("shards"))}`.pipe(Effect.ignore) yield* sql`DROP TABLE ${runnersTableSql}`.pipe(Effect.ignore) @@ -174,7 +182,9 @@ export const make = Effect.fnUntraced(function*(options: { }) const sqlNow = sql.literal(sqlNowString) - const expiresSeconds = sql.literal(Math.ceil(Duration.toSeconds(config.shardLockExpiration)).toString()) + const expiresSeconds = sql.literal( + Math.ceil(Duration.toSeconds(config.shardLockExpiration)).toString() + ) const lockExpiresAt = sql.onDialectOrElse({ pg: () => sql`${sqlNow} - INTERVAL '${expiresSeconds} seconds'`, mysql: () => sql`DATE_SUB(${sqlNow}, INTERVAL ${expiresSeconds} SECOND)`, @@ -193,9 +203,9 @@ export const make = Effect.fnUntraced(function*(options: { mssql: () => (address: string, runner: string, healthy: boolean) => sql` MERGE ${runnersTableSql} AS target - USING (SELECT ${address} AS address, ${runner} AS runner, ${sqlNow} AS last_heartbeat, ${ - encodeBoolean(healthy) - } AS healthy) AS source + USING (SELECT ${address} AS address, ${runner} AS runner, ${sqlNow} AS last_heartbeat, ${encodeBoolean( + healthy + )} AS healthy) AS source ON target.address = source.address WHEN MATCHED THEN UPDATE SET runner = source.runner, last_heartbeat = source.last_heartbeat, healthy = source.healthy @@ -239,7 +249,9 @@ export const make = Effect.fnUntraced(function*(options: { `.values }) - const execWithLockConn = (effect: Statement.Statement): Effect.Effect => { + const execWithLockConn = ( + effect: Statement.Statement + ): Effect.Effect => { if (!lockConn) return effect const [query, params] = effect.compile() return lockConn.await.pipe( @@ -253,7 +265,9 @@ export const make = Effect.fnUntraced(function*(options: { if (!lockConn) return effect.values const [query, params] = effect.compile() return lockConn.await.pipe( - Effect.flatMap(([conn]) => conn.executeUnprepared(query, params, undefined)), + Effect.flatMap(([conn]) => + conn.executeUnprepared(query, params, undefined) + ), Effect.onError(() => lockConn.unsafeRebuild()) ) } @@ -272,8 +286,9 @@ export const make = Effect.fnUntraced(function*(options: { pg: () => { if (disableAdvisoryLocks) { return (address: string, shardIds: ReadonlyArray) => { - const values = shardIds.map((shardId) => - sql`(${stringLiteral(shardId)}, ${stringLiteral(address)}, ${sqlNow})` + const values = shardIds.map( + (shardId) => + sql`(${stringLiteral(shardId)}, ${stringLiteral(address)}, ${sqlNow})` ) return sql` INSERT INTO ${locksTableSql} (shard_id, address, acquired_at) VALUES ${sql.csv(values)} @@ -281,83 +296,101 @@ export const make = Effect.fnUntraced(function*(options: { SET address = ${address}, acquired_at = ${sqlNow} WHERE ${locksTableSql}.address = ${address} OR ${locksTableSql}.acquired_at < ${lockExpiresAt} -`.pipe( - Effect.andThen(acquiredLocks(address, shardIds)) - ) +`.pipe(Effect.andThen(acquiredLocks(address, shardIds))) } } - return Effect.fnUntraced(function*(_address: string, shardIds: ReadonlyArray) { - const [conn, pid] = yield* lockConn!.await - const acquiredShardIds: Array = [] - const toAcquire = new Map(shardIds.map((shardId) => [lockNumbers.get(shardId)!, shardId])) - const takenLocks = yield* conn.executeValues( - `SELECT objid FROM pg_locks WHERE locktype = 'advisory' AND granted = true AND pid = ${pid} ORDER BY objid`, - [] - ) - for (let i = 0; i < takenLocks.length; i++) { - const lockNum = takenLocks[i][0] as number - acquiredShardIds.push(lockNumbersReverse.get(lockNum)!) - toAcquire.delete(lockNum) - } - if (toAcquire.size === 0) { - return acquiredShardIds - } - const rows = yield* conn.executeUnprepared(`SELECT ${pgLocks(toAcquire)}`, [], undefined) - const results = rows[0] as Record - for (const shardId in results) { - if (results[shardId]) { - acquiredShardIds.push(shardId) + return Effect.fnUntraced( + function* (_address: string, shardIds: ReadonlyArray) { + const [conn, pid] = yield* lockConn!.await + const acquiredShardIds: Array = [] + const toAcquire = new Map( + shardIds.map((shardId) => [lockNumbers.get(shardId)!, shardId]) + ) + const takenLocks = yield* conn.executeValues( + `SELECT objid FROM pg_locks WHERE locktype = 'advisory' AND granted = true AND pid = ${pid} ORDER BY objid`, + [] + ) + for (let i = 0; i < takenLocks.length; i++) { + const lockNum = takenLocks[i][0] as number + acquiredShardIds.push(lockNumbersReverse.get(lockNum)!) + toAcquire.delete(lockNum) } - } - return acquiredShardIds - }, Effect.onError(() => lockConn!.unsafeRebuild())) + if (toAcquire.size === 0) { + return acquiredShardIds + } + const rows = yield* conn.executeUnprepared( + `SELECT ${pgLocks(toAcquire)}`, + [], + undefined + ) + const results = rows[0] as Record + for (const shardId in results) { + if (results[shardId]) { + acquiredShardIds.push(shardId) + } + } + return acquiredShardIds + }, + Effect.onError(() => lockConn!.unsafeRebuild()) + ) }, mysql: () => { if (disableAdvisoryLocks) { return (address: string, shardIds: ReadonlyArray) => { - const values = shardIds.map((shardId) => - sql`(${stringLiteral(shardId)}, ${stringLiteral(address)}, ${sqlNow})` + const values = shardIds.map( + (shardId) => + sql`(${stringLiteral(shardId)}, ${stringLiteral(address)}, ${sqlNow})` ) return sql` INSERT INTO ${locksTableSql} (shard_id, address, acquired_at) VALUES ${sql.csv(values)} ON DUPLICATE KEY UPDATE address = IF(address = VALUES(address) OR acquired_at < ${lockExpiresAt}, VALUES(address), address), acquired_at = IF(address = VALUES(address) OR acquired_at < ${lockExpiresAt}, VALUES(acquired_at), acquired_at) -`.unprepared.pipe( - Effect.andThen(acquiredLocks(address, shardIds)) - ) +`.unprepared.pipe(Effect.andThen(acquiredLocks(address, shardIds))) } } - return Effect.fnUntraced(function*(_address: string, shardIds: ReadonlyArray) { - const [conn, pid] = yield* lockConn!.await - const takenLocks = (yield* conn.executeValues(`SELECT ${allMySqlTakenLocks}`, []))[0] as Array - const acquiredShardIds: Array = [] - const toAcquire: Array = [] - for (let i = 0; i < shardIds.length; i++) { - const shardId = shardIds[i] - const lockTakenBy = takenLocks[shardIdsIndex.get(shardId)!] - if (lockTakenBy === pid) { - acquiredShardIds.push(shardId) - } else if (shardIds.includes(shardId)) { - toAcquire.push(shardId) + return Effect.fnUntraced( + function* (_address: string, shardIds: ReadonlyArray) { + const [conn, pid] = yield* lockConn!.await + const takenLocks = (yield* conn.executeValues( + `SELECT ${allMySqlTakenLocks}`, + [] + ))[0] as Array + const acquiredShardIds: Array = [] + const toAcquire: Array = [] + for (let i = 0; i < shardIds.length; i++) { + const shardId = shardIds[i] + const lockTakenBy = takenLocks[shardIdsIndex.get(shardId)!] + if (lockTakenBy === pid) { + acquiredShardIds.push(shardId) + } else if (shardIds.includes(shardId)) { + toAcquire.push(shardId) + } } - } - if (toAcquire.length === 0) { - return acquiredShardIds - } - const results = (yield* conn.executeValues(`SELECT ${mysqlLocks(toAcquire)}`, []))[0] as Array - for (let i = 0; i < results.length; i++) { - if (results[i] === 1) { - acquiredShardIds.push(toAcquire[i]) + if (toAcquire.length === 0) { + return acquiredShardIds } - } - return acquiredShardIds - }, Effect.onError(() => lockConn!.unsafeRebuild())) + const results = (yield* conn.executeValues( + `SELECT ${mysqlLocks(toAcquire)}`, + [] + ))[0] as Array + for (let i = 0; i < results.length; i++) { + if (results[i] === 1) { + acquiredShardIds.push(toAcquire[i]) + } + } + return acquiredShardIds + }, + Effect.onError(() => lockConn!.unsafeRebuild()) + ) }, mssql: () => (address: string, shardIds: ReadonlyArray) => { - const values = shardIds.map((shardId) => sql`(${stringLiteral(shardId)}, ${stringLiteral(address)}, ${sqlNow})`) + const values = shardIds.map( + (shardId) => + sql`(${stringLiteral(shardId)}, ${stringLiteral(address)}, ${sqlNow})` + ) return sql` MERGE ${locksTableSql} WITH (HOLDLOCK) AS target USING (SELECT * FROM (VALUES ${sql.csv(values)})) AS source (shard_id, address, acquired_at) @@ -374,7 +407,10 @@ export const make = Effect.fnUntraced(function*(options: { }, orElse: () => (address: string, shardIds: ReadonlyArray) => { - const values = shardIds.map((shardId) => sql`(${stringLiteral(shardId)}, ${stringLiteral(address)}, ${sqlNow})`) + const values = shardIds.map( + (shardId) => + sql`(${stringLiteral(shardId)}, ${stringLiteral(address)}, ${sqlNow})` + ) return sql` WITH source(shard_id, address, acquired_at) AS (VALUES ${sql.csv(values)}) INSERT INTO ${locksTableSql} (shard_id, address, acquired_at) @@ -397,8 +433,8 @@ export const make = Effect.fnUntraced(function*(options: { const lockNumbers = new Map() const lockNumbersReverse = new Map() - for (let i = 0; i < config.shardGroups.length; i++) { - const group = config.shardGroups[i] + for (let i = 0; i < availableShardGroups.length; i++) { + const group = availableShardGroups[i] const base = (i + 1) * 1000000 for (let shard = 1; shard <= config.shardsPerGroup; shard++) { const shardId = ShardId.make(group, shard).toString() @@ -413,8 +449,8 @@ export const make = Effect.fnUntraced(function*(options: { const lockNamesReverse = new Map() { let index = 0 - for (let i = 0; i < config.shardGroups.length; i++) { - const group = config.shardGroups[i] + for (let i = 0; i < availableShardGroups.length; i++) { + const group = availableShardGroups[i] for (let shard = 1; shard <= config.shardsPerGroup; shard++) { const shardId = ShardId.make(group, shard).toString() const lockName = `${prefix}.${shardId}` @@ -432,7 +468,11 @@ export const make = Effect.fnUntraced(function*(options: { ).join(", ") const mysqlLocks = (shardIds: ReadonlyArray) => - shardIds.map((shardId) => `GET_LOCK('${lockNames.get(shardId)!}', 0) AS "${shardId}"`).join(", ") + shardIds + .map( + (shardId) => `GET_LOCK('${lockNames.get(shardId)!}', 0) AS "${shardId}"` + ) + .join(", ") const allMySqlTakenLocks = Array.from( lockNames.entries(), @@ -445,16 +485,15 @@ export const make = Effect.fnUntraced(function*(options: { WHERE address = ${address} AND acquired_at >= ${lockExpiresAt} AND shard_id IN ${stringLiteralArr(shardIds)} - `.values.pipe( - Effect.map((rows) => rows.map((row) => row[0] as string)) - ) + `.values.pipe(Effect.map((rows) => rows.map((row) => row[0] as string))) const wrapString = sql.onDialectOrElse({ mssql: () => (s: string) => `N'${s}'`, orElse: () => (s: string) => `'${s}'` }) const stringLiteral = (s: string) => sql.literal(wrapString(s)) - const stringLiteralArr = (arr: ReadonlyArray) => sql.literal(`(${arr.map(wrapString).join(",")})`) + const stringLiteralArr = (arr: ReadonlyArray) => + sql.literal(`(${arr.map(wrapString).join(",")})`) const refreshShards = sql.onDialectOrElse({ pg: () => { @@ -491,22 +530,33 @@ export const make = Effect.fnUntraced(function*(options: { SET acquired_at = ${sqlNow} OUTPUT inserted.shard_id WHERE address = ${address} AND shard_id IN ${stringLiteralArr(shardIds)} - `.pipe(execWithLockConnValues, Effect.map((rows) => rows.map((row) => row[0] as string))), + `.pipe( + execWithLockConnValues, + Effect.map((rows) => rows.map((row) => row[0] as string)) + ), orElse: () => (address: string, shardIds: ReadonlyArray) => sql` UPDATE ${locksTableSql} SET acquired_at = ${sqlNow} WHERE address = ${address} AND shard_id IN ${stringLiteralArr(shardIds)} RETURNING shard_id - `.pipe(execWithLockConnValues, Effect.map((rows) => rows.map((row) => row[0] as string))) + `.pipe( + execWithLockConnValues, + Effect.map((rows) => rows.map((row) => row[0] as string)) + ) }) return RunnerStorage.makeEncoded({ - getRunners: sql`SELECT runner, healthy FROM ${runnersTableSql} WHERE last_heartbeat > ${lockExpiresAt}`.values.pipe( - PersistenceError.refail, - Effect.map(Arr.map(([runner, healthy]) => [String(runner), Boolean(healthy)] as const)), - withTracerDisabled - ), + getRunners: + sql`SELECT runner, healthy FROM ${runnersTableSql} WHERE last_heartbeat > ${lockExpiresAt}`.values.pipe( + PersistenceError.refail, + Effect.map( + Arr.map( + ([runner, healthy]) => [String(runner), Boolean(healthy)] as const + ) + ), + withTracerDisabled + ), register: (address, runner, healthy) => insertRunner(address, runner, healthy).pipe( @@ -523,12 +573,11 @@ export const make = Effect.fnUntraced(function*(options: { ), setRunnerHealth: (address, healthy) => - sql`UPDATE ${runnersTableSql} SET healthy = ${encodeBoolean(healthy)} WHERE address = ${address}` - .pipe( - Effect.asVoid, - PersistenceError.refail, - withTracerDisabled - ), + sql`UPDATE ${runnersTableSql} SET healthy = ${encodeBoolean(healthy)} WHERE address = ${address}`.pipe( + Effect.asVoid, + PersistenceError.refail, + withTracerDisabled + ), acquire: (address, shardIds) => acquireLock(address, shardIds).pipe( @@ -539,9 +588,9 @@ export const make = Effect.fnUntraced(function*(options: { refresh: (address, shardIds) => sql`UPDATE ${runnersTableSql} SET last_heartbeat = ${sqlNow} WHERE address = ${address}`.pipe( execWithLockConn, - shardIds.length > 0 ? - Effect.andThen(refreshShards(address, shardIds)) : - Effect.as([]), + shardIds.length > 0 + ? Effect.andThen(refreshShards(address, shardIds)) + : Effect.as([]), PersistenceError.refail ), @@ -554,11 +603,14 @@ export const make = Effect.fnUntraced(function*(options: { ) } return Effect.fnUntraced( - function*(_address, shardId) { + function* (_address, shardId) { const lockNum = lockNumbers.get(shardId)! for (let i = 0; i < 5; i++) { const [conn] = yield* lockConn!.await - yield* conn.executeRaw(`SELECT pg_advisory_unlock(${lockNum})`, []) + yield* conn.executeRaw( + `SELECT pg_advisory_unlock(${lockNum})`, + [] + ) const takenLocks = yield* conn.executeValues( `SELECT 1 FROM pg_locks WHERE locktype = 'advisory' AND granted = true AND pid = pg_backend_pid() AND objid = ${lockNum}`, [] @@ -581,7 +633,7 @@ export const make = Effect.fnUntraced(function*(options: { ) } return Effect.fnUntraced( - function*(_address, shardId) { + function* (_address, shardId) { const lockName = lockNames.get(shardId)! while (true) { const [conn, pid] = yield* lockConn!.await @@ -658,5 +710,8 @@ export const layer: Layer.Layer< */ export const layerWith = (options: { readonly prefix?: string | undefined -}): Layer.Layer => - Layer.scoped(RunnerStorage.RunnerStorage)(make(options)) +}): Layer.Layer< + RunnerStorage.RunnerStorage, + SqlError, + SqlClient.SqlClient | ShardingConfig.ShardingConfig +> => Layer.scoped(RunnerStorage.RunnerStorage)(make(options)) diff --git a/repos/effect/packages/cluster/test/ClusterWorkflowEngine.test.ts b/repos/effect/packages/cluster/test/ClusterWorkflowEngine.test.ts index 293e401..863f760 100644 --- a/repos/effect/packages/cluster/test/ClusterWorkflowEngine.test.ts +++ b/repos/effect/packages/cluster/test/ClusterWorkflowEngine.test.ts @@ -1,4 +1,5 @@ import { + ClusterSchema, ClusterWorkflowEngine, MessageStorage, Runners, @@ -12,7 +13,10 @@ import { DurableDeferred, Workflow } from "@effect/workflow" -import { WorkflowInstance } from "@effect/workflow/WorkflowEngine" +import { + WorkflowEngine, + WorkflowInstance +} from "@effect/workflow/WorkflowEngine" import { DateTime, Effect, Exit, Fiber, Layer, Schema, TestClock } from "effect" import * as Cause from "effect/Cause" import * as Duration from "effect/Duration" @@ -231,6 +235,83 @@ describe.concurrent("ClusterWorkflowEngine", () => { }).pipe(Effect.provide(TestWorkflowLayer)) ) + it.effect("routes durable clock wakeups to the workflow shard group", () => + Effect.gen(function* () { + const driver = yield* MessageStorage.MemoryDriver + const sharding = yield* Sharding.Sharding + + const fiber = yield* ShardedClockWorkflow.execute({ + id: "sharded-clock" + }).pipe(Effect.fork) + + yield* TestClock.adjust(1) + + const envelope = driver.journal.find( + (envelope) => + envelope._tag === "Request" && + envelope.address.entityType === "Workflow/-/DurableClock" + ) + assert.exists(envelope) + assert.strictEqual(envelope.address.shardId.group, "workflow") + + yield* TestClock.adjust("10 seconds") + yield* sharding.pollStorage + yield* TestClock.adjust(5000) + yield* Fiber.join(fiber) + }).pipe(Effect.provide(TestWorkflowLayer)) + ) + + it.effect( + "routes durable deferred completions to the workflow shard group after a partial client is cached", + () => + Effect.gen(function* () { + const driver = yield* MessageStorage.MemoryDriver + const engine = yield* WorkflowEngine + const executionIdBeforeRegister = + yield* ShardedDeferredWorkflow.executionId({ id: "before-register" }) + const tokenBeforeRegister = DurableDeferred.tokenFromExecutionId( + ShardedDeferred, + { + workflow: ShardedDeferredWorkflow, + executionId: executionIdBeforeRegister + } + ) + + const pendingDone = yield* DurableDeferred.done(ShardedDeferred, { + token: tokenBeforeRegister, + exit: Exit.void + }).pipe(Effect.fork) + + yield* engine.register(ShardedDeferredWorkflow, () => Effect.void) + yield* Fiber.join(pendingDone) + + const executionIdAfterRegister = + yield* ShardedDeferredWorkflow.executionId({ id: "after-register" }) + const tokenAfterRegister = DurableDeferred.tokenFromExecutionId( + ShardedDeferred, + { + workflow: ShardedDeferredWorkflow, + executionId: executionIdAfterRegister + } + ) + const journalLength = driver.journal.length + yield* DurableDeferred.done(ShardedDeferred, { + token: tokenAfterRegister, + exit: Exit.void + }) + + const envelope = driver.journal + .slice(journalLength) + .find( + (envelope) => + envelope._tag === "Request" && + envelope.address.entityType === "Workflow/ShardedDeferredWorkflow" + ) + assert.exists(envelope) + assert.strictEqual(envelope.address.shardId.group, "workflow") + }).pipe(Effect.scoped, Effect.provide(TestWorkflowEngine)) + ) + it.effect("SuspendOnFailure", () => Effect.gen(function* () { const flags = yield* Flags @@ -306,6 +387,8 @@ describe.concurrent("ClusterWorkflowEngine", () => { const TestShardingConfig = ShardingConfig.layer({ shardsPerGroup: 300, + availableShardGroups: ["default", "workflow"], + assignedShardGroups: ["default", "workflow"], entityMailboxCapacity: 10, entityTerminationTimeout: 0, entityMessagePollInterval: 5000, @@ -572,6 +655,38 @@ const ChildWorkflowLayer = ChildWorkflow.toLayer( }) ) +const ShardedClockWorkflow = Workflow.make({ + name: "ShardedClockWorkflow", + payload: { + id: Schema.String + }, + idempotencyKey(payload) { + return payload.id + } +}).annotate(ClusterSchema.ShardGroup, () => "workflow") + +const ShardedClockWorkflowLayer = ShardedClockWorkflow.toLayer( + Effect.fnUntraced(function* () { + yield* DurableClock.sleep({ + name: "ShardedClock", + duration: "10 seconds", + inMemoryThreshold: Duration.zero + }) + }) +) + +const ShardedDeferred = DurableDeferred.make("ShardedDeferred") + +const ShardedDeferredWorkflow = Workflow.make({ + name: "ShardedDeferredWorkflow", + payload: { + id: Schema.String + }, + idempotencyKey(payload) { + return payload.id + } +}).annotate(ClusterSchema.ShardGroup, () => "workflow") + const DiscardParentWorkflow = Workflow.make({ name: "DiscardParentWorkflow", payload: { id: Schema.String }, @@ -665,6 +780,7 @@ const TestWorkflowLayer = EmailWorkflowLayer.pipe( Layer.merge(DurableRaceWorkflowLayer), Layer.merge(ParentWorkflowLayer), Layer.merge(ChildWorkflowLayer), + Layer.merge(ShardedClockWorkflowLayer), Layer.merge(DiscardParentWorkflowLayer), Layer.merge(DiscardChildWorkflowLayer), Layer.merge(SuspendOnFailureWorkflowLayer), diff --git a/repos/effect/packages/effect/dtslint/Data.tst.ts b/repos/effect/packages/effect/dtslint/Data.tst.ts index f3db883..319e38d 100644 --- a/repos/effect/packages/effect/dtslint/Data.tst.ts +++ b/repos/effect/packages/effect/dtslint/Data.tst.ts @@ -70,7 +70,12 @@ describe("Data", () => { } const taggedPerson = Data.tagged("Person") - expect(taggedPerson).type.toBe<(args: { readonly name: string; readonly optional?: string }) => TaggedPerson>() + expect(taggedPerson).type.toBe< + (args: { + readonly name: string + readonly optional?: string + }) => TaggedPerson + >() }) it("Class", () => { @@ -85,10 +90,17 @@ describe("Data", () => { }) it("TaggedClass", () => { - class Person extends Data.TaggedClass("Person")<{ name: string; age?: number }> {} + class Person extends Data.TaggedClass("Person")<{ + name: string + age?: number + }> {} const person = new Person({ name: "Mike" }) // fields should be readonly - expect(person).type.toBe<{ readonly name: string; readonly age?: number; readonly _tag: "Person" }>() + expect(person).type.toBe<{ + readonly name: string + readonly age?: number + readonly _tag: "Person" + }>() class Void extends Data.TaggedClass("Void") {} // void constructor @@ -96,16 +108,22 @@ describe("Data", () => { }) it("Error", () => { - class Err extends Data.Error<{ message: string; a: number; optional?: string }> {} + class Err extends Data.Error<{ + message: string + a: number + optional?: string + }> {} const err = new Err({ message: "Oh no!", a: 1 }) // assignable to Error expect().type.toBeAssignableTo() // non-Error fields should be readonly - expect(pick(err, "message", "a", "optional")).type.toBe< - { message: string; readonly a: number; readonly optional?: string } - >() + expect(pick(err, "message", "a", "optional")).type.toBe<{ + message: string + readonly a: number + readonly optional?: string + }>() class Void extends Data.Error {} // void constructor @@ -113,7 +131,10 @@ describe("Data", () => { }) it("TaggedError", () => { - class Err extends Data.TaggedError("Foo")<{ message?: string; a: number }> {} + class Err extends Data.TaggedError("Foo")<{ + message?: string + a: number + }> {} // Test optional props are allowed new Err({ a: 1 }) @@ -123,7 +144,10 @@ describe("Data", () => { const err = new Err({ message: "Oh no!", a: 1 }) // non-Error fields should be readonly - expect(pick(err, "message", "a")).type.toBe<{ message: string; readonly a: number }>() + expect(pick(err, "message", "a")).type.toBe<{ + message: string + readonly a: number + }>() class Void extends Data.TaggedError("Foo") {} // void constructor @@ -136,12 +160,14 @@ describe("Data", () => { A: { readonly required: string } B: { readonly optional?: number } }> - expect>().type.toBe< - { readonly _tag: "A"; readonly required: string } - >() - expect>().type.toBe< - { readonly _tag: "B"; readonly optional?: number } - >() + expect>().type.toBe<{ + readonly _tag: "A" + readonly required: string + }>() + expect>().type.toBe<{ + readonly _tag: "B" + readonly optional?: number + }>() }) it("should raise an error if one of the variants has a _tag property", () => { @@ -161,10 +187,20 @@ describe("Data", () => { }> const { $is, A, B } = Data.taggedEnum() - expect>().type.toBe<[{ readonly required: string }]>() - expect>().type.toBe<{ readonly _tag: "A"; readonly required: string }>() - expect>().type.toBe<[{ readonly optional?: number }]>() - expect>().type.toBe<{ readonly _tag: "B"; readonly optional?: number }>() + expect>().type.toBe< + [{ readonly required: string }] + >() + expect>().type.toBe<{ + readonly _tag: "A" + readonly required: string + }>() + expect>().type.toBe< + [{ readonly optional?: number }] + >() + expect>().type.toBe<{ + readonly _tag: "B" + readonly optional?: number + }>() const isA = $is("A") expect(isA).type.toBe< (u: unknown) => u is { readonly _tag: "A"; readonly required: string } @@ -186,8 +222,37 @@ describe("Data", () => { } const { A, B } = Data.taggedEnum() - expect().type.toBe<((args: { readonly a: A }) => { readonly _tag: "A"; readonly a: A })>() - expect().type.toBe<((args: { readonly b?: B }) => { readonly _tag: "B"; readonly b?: B })>() + expect().type.toBe< + (args: { readonly a: A }) => { readonly _tag: "A"; readonly a: A } + >() + expect().type.toBe< + (args: { readonly b?: B }) => { readonly _tag: "B"; readonly b?: B } + >() + }) + + it("should preserve the generic type parameter inside $match arms (#6249)", () => { + type TE = Data.TaggedEnum<{ + Leaf: { value: T } + Branch: { children: ReadonlyArray> } + }> + + interface TEDefinition extends Data.TaggedEnum.WithGenerics<1> { + readonly taggedEnum: TE + } + + const TE = Data.taggedEnum() + + function collectValues(node: TE): ReadonlyArray { + return TE.$match(node, { + Leaf: (leaf) => { + expect(leaf.value).type.toBe() + return [leaf.value] + }, + Branch: (branch) => branch.children.flatMap(collectValues) + }) + } + + expect(collectValues).type.toBe<(node: TE) => ReadonlyArray>() }) }) }) diff --git a/repos/effect/packages/effect/src/Data.ts b/repos/effect/packages/effect/src/Data.ts index f4910d4..94fff20 100644 --- a/repos/effect/packages/effect/src/Data.ts +++ b/repos/effect/packages/effect/src/Data.ts @@ -19,7 +19,9 @@ export declare namespace Case { */ export interface Constructor { ( - args: Types.VoidIfEmpty<{ readonly [P in keyof A as P extends Tag ? never : P]: A[P] }> + args: Types.VoidIfEmpty<{ + readonly [P in keyof A as P extends Tag ? never : P]: A[P] + }> ): A } } @@ -44,13 +46,17 @@ export declare namespace Case { * @category constructors * @since 2.0.0 */ -export const struct: >(a: A) => { readonly [P in keyof A]: A[P] } = internal.struct +export const struct: >( + a: A +) => { readonly [P in keyof A]: A[P] } = internal.struct /** * @category constructors * @since 2.0.0 */ -export const unsafeStruct = >(as: A): { readonly [P in keyof A]: A[P] } => +export const unsafeStruct = >( + as: A +): { readonly [P in keyof A]: A[P] } => Object.setPrototypeOf(as, StructuralPrototype) /** @@ -73,7 +79,8 @@ export const unsafeStruct = >(as: A): { readonly [ * @category constructors * @since 2.0.0 */ -export const tuple = >(...as: As): Readonly => unsafeArray(as) +export const tuple = >(...as: As): Readonly => + unsafeArray(as) /** * @example @@ -101,17 +108,23 @@ export const tuple = >(...as: As): Readonly => * @category constructors * @since 2.0.0 */ -export const array = >(as: As): Readonly => unsafeArray(as.slice(0) as unknown as As) +export const array = >(as: As): Readonly => + unsafeArray(as.slice(0) as unknown as As) /** * @category constructors * @since 2.0.0 */ -export const unsafeArray = >(as: As): Readonly => - Object.setPrototypeOf(as, internal.ArrayProto) +export const unsafeArray = >( + as: As +): Readonly => Object.setPrototypeOf(as, internal.ArrayProto) -const _case = (): Case.Constructor => (args) => - (args === undefined ? Object.create(StructuralPrototype) : struct(args)) as any +const _case = + (): Case.Constructor => + (args) => + (args === undefined + ? Object.create(StructuralPrototype) + : struct(args)) as any export { /** @@ -168,14 +181,16 @@ export { * @since 2.0.0 * @category constructors */ -export const tagged = ( - tag: A["_tag"] -): Case.Constructor => -(args) => { - const value = args === undefined ? Object.create(StructuralPrototype) : struct(args) - value._tag = tag - return value -} +export const tagged = + ( + tag: A["_tag"] + ): Case.Constructor => + (args) => { + const value = + args === undefined ? Object.create(StructuralPrototype) : struct(args) + value._tag = tag + return value + } /** * Provides a constructor for a Case Class. @@ -200,7 +215,7 @@ export const tagged = ( * @since 2.0.0 * @category constructors */ -export const Class: new = {}>( +export const Class: new = {}>( args: Types.VoidIfEmpty<{ readonly [P in keyof A]: A[P] }> ) => Readonly = internal.Structural as any @@ -231,8 +246,10 @@ export const Class: new = {}>( */ export const TaggedClass = ( tag: Tag -): new = {}>( - args: Types.VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] }> +): new = {}>( + args: Types.VoidIfEmpty<{ + readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] + }> ) => Readonly & { readonly _tag: Tag } => { class Base extends Class { readonly _tag = tag @@ -244,7 +261,7 @@ export const TaggedClass = ( * @since 2.0.0 * @category constructors */ -export const Structural: new( +export const Structural: new ( args: Types.VoidIfEmpty<{ readonly [P in keyof A]: A[P] }> ) => {} = internal.Structural as any @@ -279,19 +296,26 @@ export const Structural: new( */ export type TaggedEnum< A extends Record> & UntaggedChildren -> = keyof A extends infer Tag ? - Tag extends keyof A ? Types.Simplify<{ readonly _tag: Tag } & { readonly [K in keyof A[Tag]]: A[Tag][K] }> - : never +> = keyof A extends infer Tag + ? Tag extends keyof A + ? Types.Simplify< + { readonly _tag: Tag } & { readonly [K in keyof A[Tag]]: A[Tag][K] } + > + : never : never -type ChildrenAreTagged = keyof A extends infer K ? K extends keyof A ? "_tag" extends keyof A[K] ? true - : false - : never +type ChildrenAreTagged = keyof A extends infer K + ? K extends keyof A + ? "_tag" extends keyof A[K] + ? true + : false + : never : never -type UntaggedChildren = true extends ChildrenAreTagged - ? "It looks like you're trying to create a tagged enum, but one or more of its members already has a `_tag` property." - : unknown +type UntaggedChildren = + true extends ChildrenAreTagged + ? "It looks like you're trying to create a tagged enum, but one or more of its members already has a `_tag` property." + : unknown /** * @since 2.0.0 @@ -335,7 +359,10 @@ export declare namespace TaggedEnum { A extends { readonly _tag: string }, K extends A["_tag"], E = Extract - > = { readonly [K in keyof E as K extends "_tag" ? never : K]: E[K] } extends infer T ? Types.VoidIfEmpty + > = { + readonly [K in keyof E as K extends "_tag" ? never : K]: E[K] + } extends infer T + ? Types.VoidIfEmpty : never /** @@ -350,22 +377,30 @@ export declare namespace TaggedEnum { * @since 3.1.0 */ export type Constructor = Types.Simplify< - & { - readonly [Tag in A["_tag"]]: Case.Constructor, "_tag"> - } - & { - readonly $is: (tag: Tag) => (u: unknown) => u is Extract + { + readonly [Tag in A["_tag"]]: Case.Constructor< + Extract, + "_tag" + > + } & { + readonly $is: ( + tag: Tag + ) => (u: unknown) => u is Extract readonly $match: { < const Cases extends { - readonly [Tag in A["_tag"]]: (args: Extract) => any + readonly [Tag in A["_tag"]]: ( + args: Extract + ) => any } >( cases: Cases & { [K in Exclude]: never } ): (value: A) => Unify> < const Cases extends { - readonly [Tag in A["_tag"]]: (args: Extract) => any + readonly [Tag in A["_tag"]]: ( + args: Extract + ) => any } >( value: A, @@ -389,18 +424,16 @@ export declare namespace TaggedEnum { } readonly $match: { < - A, - B, - C, - D, - Cases extends { - readonly [Tag in Z["taggedEnum"]["_tag"]]: ( - args: Extract, { readonly _tag: Tag }> + const Self extends TaggedEnum.Kind, + const Cases extends { + readonly [Tag in Self["_tag"]]: ( + args: Extract ) => any } >( - cases: Cases & { [K in Exclude]: never } - ): (self: TaggedEnum.Kind) => Unify> + self: Self, + cases: Cases & { [K in Exclude]: never } + ): Unify> < A, B, @@ -408,13 +441,19 @@ export declare namespace TaggedEnum { D, Cases extends { readonly [Tag in Z["taggedEnum"]["_tag"]]: ( - args: Extract, { readonly _tag: Tag }> + args: Extract< + TaggedEnum.Kind, + { readonly _tag: Tag } + > ) => any } >( - self: TaggedEnum.Kind, - cases: Cases & { [K in Exclude]: never } - ): Unify> + cases: Cases & { + [K in Exclude]: never + } + ): ( + self: TaggedEnum.Kind + ) => Unify> } } } @@ -505,16 +544,19 @@ export const taggedEnum: { (): TaggedEnum.Constructor } = () => - new Proxy({}, { - get(_target, tag, _receiver) { - if (tag === "$is") { - return Predicate.isTagged - } else if (tag === "$match") { - return taggedMatch + new Proxy( + {}, + { + get(_target, tag, _receiver) { + if (tag === "$is") { + return Predicate.isTagged + } else if (tag === "$match") { + return taggedMatch + } + return tagged(tag as string) } - return tagged(tag as string) } - }) as any + ) as any function taggedMatch< A extends { readonly _tag: string }, @@ -536,7 +578,7 @@ function taggedMatch< >(): any { if (arguments.length === 1) { const cases = arguments[0] as Cases - return function(value: A): ReturnType { + return function (value: A): ReturnType { return cases[value._tag as A["_tag"]](value as any) } } @@ -551,9 +593,9 @@ function taggedMatch< * @since 2.0.0 * @category constructors */ -export const Error: new = {}>( +export const Error: new = {}>( args: Types.VoidIfEmpty<{ readonly [P in keyof A]: A[P] }> -) => Cause.YieldableError & Readonly = (function() { +) => Cause.YieldableError & Readonly = (function () { const plainArgsSymbol = Symbol.for("effect/Data/Error/plainArgs") const O = { BaseEffectError: class extends core.YieldableError { @@ -562,7 +604,10 @@ export const Error: new = {}>( if (args) { Object.assign(this, args) // @effect-diagnostics-next-line floatingEffect:off - Object.defineProperty(this, plainArgsSymbol, { value: args, enumerable: false }) + Object.defineProperty(this, plainArgsSymbol, { + value: args, + enumerable: false + }) } } toJSON() { @@ -577,8 +622,12 @@ export const Error: new = {}>( * @since 2.0.0 * @category constructors */ -export const TaggedError = (tag: Tag): new = {}>( - args: Types.VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] }> +export const TaggedError = ( + tag: Tag +): new = {}>( + args: Types.VoidIfEmpty<{ + readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] + }> ) => Cause.YieldableError & { readonly _tag: Tag } & Readonly => { const O = { BaseEffectError: class extends Error<{}> { diff --git a/repos/effect/packages/effect/src/JSONSchema.ts b/repos/effect/packages/effect/src/JSONSchema.ts index 4b5220e..5484597 100644 --- a/repos/effect/packages/effect/src/JSONSchema.ts +++ b/repos/effect/packages/effect/src/JSONSchema.ts @@ -12,7 +12,13 @@ import * as Record from "./Record.js" import type * as Schema from "./Schema.js" import * as AST from "./SchemaAST.js" -type JsonValue = string | number | boolean | null | Array | { [key: string]: JsonValue } +type JsonValue = + | string + | number + | boolean + | null + | Array + | { [key: string]: JsonValue } /** * @category model @@ -64,10 +70,7 @@ export interface JsonSchema7Void extends JsonSchemaAnnotations { */ export interface JsonSchema7object extends JsonSchemaAnnotations { $id: "/schemas/object" - anyOf: [ - { type: "object" }, - { type: "array" } - ] + anyOf: [{ type: "object" }, { type: "array" }] } /** @@ -76,10 +79,7 @@ export interface JsonSchema7object extends JsonSchemaAnnotations { */ export interface JsonSchema7empty extends JsonSchemaAnnotations { $id: "/schemas/%7B%7D" - anyOf: [ - { type: "object" }, - { type: "array" } - ] + anyOf: [{ type: "object" }, { type: "array" }] } /** @@ -261,15 +261,20 @@ export type JsonSchema7Root = JsonSchema7 & { * @category encoding * @since 3.10.0 */ -export const make = (schema: Schema.Schema, options?: { - readonly target?: Target | undefined -}): JsonSchema7Root => { +export const make = ( + schema: Schema.Schema, + options?: { + readonly target?: Target | undefined + } +): JsonSchema7Root => { const definitions: Record = {} const target = options?.target ?? "jsonSchema7" - const ast = AST.isTransformation(schema.ast) && isParseJsonTransformation(schema.ast.from) - // Special case top level `parseJson` transformations - ? schema.ast.to - : schema.ast + const ast = + AST.isTransformation(schema.ast) && + isParseJsonTransformation(schema.ast.from) + ? // Special case top level `parseJson` transformations + schema.ast.to + : schema.ast const jsonSchema = fromAST(ast, { definitions, target @@ -287,7 +292,11 @@ export const make = (schema: Schema.Schema, options?: { return out } -type Target = "jsonSchema7" | "jsonSchema2019-09" | "openApi3.1" | "jsonSchema2020-12" +type Target = + | "jsonSchema7" + | "jsonSchema2019-09" + | "openApi3.1" + | "jsonSchema2020-12" type TopLevelReferenceStrategy = "skip" | "keep" @@ -334,18 +343,24 @@ export function getMetaSchemaUri(target: Target) { * @since 3.11.5 * @experimental */ -export const fromAST = (ast: AST.AST, options: { - readonly definitions: Record - readonly definitionPath?: string | undefined - readonly target?: Target | undefined - readonly topLevelReferenceStrategy?: TopLevelReferenceStrategy | undefined - readonly additionalPropertiesStrategy?: AdditionalPropertiesStrategy | undefined -}): JsonSchema7 => { +export const fromAST = ( + ast: AST.AST, + options: { + readonly definitions: Record + readonly definitionPath?: string | undefined + readonly target?: Target | undefined + readonly topLevelReferenceStrategy?: TopLevelReferenceStrategy | undefined + readonly additionalPropertiesStrategy?: + | AdditionalPropertiesStrategy + | undefined + } +): JsonSchema7 => { const definitionPath = options.definitionPath ?? "#/$defs/" const getRef = (id: string) => definitionPath + id const target = options.target ?? "jsonSchema7" const topLevelReferenceStrategy = options.topLevelReferenceStrategy ?? "keep" - const additionalPropertiesStrategy = options.additionalPropertiesStrategy ?? "strict" + const additionalPropertiesStrategy = + options.additionalPropertiesStrategy ?? "strict" return go( ast, options.definitions, @@ -381,29 +396,29 @@ const constVoid: JsonSchema7Void = { const constObject: JsonSchema7object = { $id: "/schemas/object", - "anyOf": [ - { "type": "object" }, - { "type": "array" } - ] + anyOf: [{ type: "object" }, { type: "array" }] } const constEmptyStruct: JsonSchema7empty = { $id: "/schemas/%7B%7D", - "anyOf": [ - { "type": "object" }, - { "type": "array" } - ] + anyOf: [{ type: "object" }, { type: "array" }] } -function getRawDescription(annotated: AST.Annotated | undefined): string | undefined { - if (annotated !== undefined) return Option.getOrUndefined(AST.getDescriptionAnnotation(annotated)) +function getRawDescription( + annotated: AST.Annotated | undefined +): string | undefined { + if (annotated !== undefined) + return Option.getOrUndefined(AST.getDescriptionAnnotation(annotated)) } function getRawTitle(annotated: AST.Annotated | undefined): string | undefined { - if (annotated !== undefined) return Option.getOrUndefined(AST.getTitleAnnotation(annotated)) + if (annotated !== undefined) + return Option.getOrUndefined(AST.getTitleAnnotation(annotated)) } -function getRawDefault(annotated: AST.Annotated | undefined): Option.Option { +function getRawDefault( + annotated: AST.Annotated | undefined +): Option.Option { if (annotated !== undefined) return AST.getDefaultAnnotation(annotated) return Option.none() } @@ -413,32 +428,58 @@ function encodeDefault(ast: AST.AST, def: unknown): Option.Option { return getOption(def) } -function getRawExamples(annotated: AST.Annotated | undefined): ReadonlyArray | undefined { - if (annotated !== undefined) return Option.getOrUndefined(AST.getExamplesAnnotation(annotated)) +function getRawExamples( + annotated: AST.Annotated | undefined +): ReadonlyArray | undefined { + if (annotated !== undefined) + return Option.getOrUndefined(AST.getExamplesAnnotation(annotated)) } -function encodeExamples(ast: AST.AST, examples: ReadonlyArray): Array | undefined { +function encodeExamples( + ast: AST.AST, + examples: ReadonlyArray +): Array | undefined { const getOption = ParseResult.getOption(ast, false) - const out = Arr.filterMap(examples, (e) => getOption(e).pipe(Option.filter(isJsonValue))) + const out = Arr.filterMap(examples, (e) => + getOption(e).pipe(Option.filter(isJsonValue)) + ) return out.length > 0 ? out : undefined } -function filterBuiltIn(ast: AST.AST, annotation: string | undefined, key: symbol): string | undefined { +function filterBuiltIn( + ast: AST.AST, + annotation: string | undefined, + key: symbol +): string | undefined { if (annotation !== undefined) { switch (ast._tag) { case "StringKeyword": - return annotation !== AST.stringKeyword.annotations[key] ? annotation : undefined + return annotation !== AST.stringKeyword.annotations[key] + ? annotation + : undefined case "NumberKeyword": - return annotation !== AST.numberKeyword.annotations[key] ? annotation : undefined + return annotation !== AST.numberKeyword.annotations[key] + ? annotation + : undefined case "BooleanKeyword": - return annotation !== AST.booleanKeyword.annotations[key] ? annotation : undefined + return annotation !== AST.booleanKeyword.annotations[key] + ? annotation + : undefined } } return annotation } -function isJsonValue(value: unknown, visited: Set = new Set()): value is JsonValue { - if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") { +function isJsonValue( + value: unknown, + visited: Set = new Set() +): value is JsonValue { + if ( + value === null || + typeof value === "string" || + typeof value === "number" || + typeof value === "boolean" + ) { return true } if (Array.isArray(value) || typeof value === "object") { @@ -497,7 +538,10 @@ function pruneJsonSchemaAnnotations( return out } -function getContextJsonSchemaAnnotations(ast: AST.AST, annotated: AST.Annotated): JsonSchemaAnnotations | undefined { +function getContextJsonSchemaAnnotations( + ast: AST.AST, + annotated: AST.Annotated +): JsonSchemaAnnotations | undefined { return pruneJsonSchemaAnnotations( ast, getRawDescription(annotated), @@ -507,7 +551,9 @@ function getContextJsonSchemaAnnotations(ast: AST.AST, annotated: AST.Annotated) ) } -function getJsonSchemaAnnotations(ast: AST.AST): JsonSchemaAnnotations | undefined { +function getJsonSchemaAnnotations( + ast: AST.AST +): JsonSchemaAnnotations | undefined { return pruneJsonSchemaAnnotations( ast, filterBuiltIn(ast, getRawDescription(ast), AST.DescriptionAnnotationId), @@ -532,21 +578,31 @@ function mergeJsonSchemaAnnotations( const pruneUndefined = (ast: AST.AST): AST.AST | undefined => { if (Option.isNone(AST.getJSONSchemaAnnotation(ast))) { - return AST.pruneUndefined(ast, pruneUndefined, (ast) => pruneUndefined(ast.from)) + return AST.pruneUndefined(ast, pruneUndefined, (ast) => + pruneUndefined(ast.from) + ) } } const isParseJsonTransformation = (ast: AST.AST): boolean => ast.annotations[AST.SchemaIdAnnotationId] === AST.ParseJsonSchemaId -const isOverrideAnnotation = (ast: AST.AST, jsonSchema: JsonSchema7): boolean => { +const isOverrideAnnotation = ( + ast: AST.AST, + jsonSchema: JsonSchema7 +): boolean => { if (AST.isRefinement(ast)) { const schemaId = ast.annotations[AST.SchemaIdAnnotationId] if (schemaId === schemaId_.IntSchemaId) { return "type" in jsonSchema && jsonSchema.type !== "integer" } } - return ("type" in jsonSchema) || ("oneOf" in jsonSchema) || ("anyOf" in jsonSchema) || ("$ref" in jsonSchema) + return ( + "type" in jsonSchema || + "oneOf" in jsonSchema || + "anyOf" in jsonSchema || + "$ref" in jsonSchema + ) } const mergeRefinements = (from: any, jsonSchema: any, ast: AST.AST): any => { @@ -567,8 +623,14 @@ const mergeRefinements = (from: any, jsonSchema: any, ast: AST.AST): any => { handle("maxItems", (i) => i.maxItems < jsonSchema.maxItems) handle("minimum", (i) => i.minimum > jsonSchema.minimum) handle("maximum", (i) => i.maximum < jsonSchema.maximum) - handle("exclusiveMinimum", (i) => i.exclusiveMinimum > jsonSchema.exclusiveMinimum) - handle("exclusiveMaximum", (i) => i.exclusiveMaximum < jsonSchema.exclusiveMaximum) + handle( + "exclusiveMinimum", + (i) => i.exclusiveMinimum > jsonSchema.exclusiveMinimum + ) + handle( + "exclusiveMaximum", + (i) => i.exclusiveMaximum < jsonSchema.exclusiveMaximum + ) handle("multipleOf", (i) => i.multipleOf !== jsonSchema.multipleOf) if (out.allOf.length === 0) { @@ -608,7 +670,10 @@ function addASTAnnotations(jsonSchema: JsonSchema7, ast: AST.AST): JsonSchema7 { return addAnnotations(jsonSchema, getJsonSchemaAnnotations(ast)) } -function addAnnotations(jsonSchema: JsonSchema7, annotations: JsonSchemaAnnotations | undefined): JsonSchema7 { +function addAnnotations( + jsonSchema: JsonSchema7, + annotations: JsonSchemaAnnotations | undefined +): JsonSchema7 { if (annotations === undefined || Object.keys(annotations).length === 0) { return jsonSchema } @@ -624,7 +689,11 @@ function getIdentifierAnnotation(ast: AST.AST): string | undefined { if (AST.isSuspend(ast)) { return getIdentifierAnnotation(ast.f()) } - if (AST.isTransformation(ast) && AST.isTypeLiteral(ast.from) && AST.isDeclaration(ast.to)) { + if ( + AST.isTransformation(ast) && + AST.isTypeLiteral(ast.from) && + AST.isDeclaration(ast.to) + ) { const to = ast.to const surrogate = AST.getSurrogateAnnotation(to) if (Option.isSome(surrogate)) { @@ -650,11 +719,19 @@ function go( ) { const id = getIdentifierAnnotation(ast) if (id !== undefined) { - const escapedId = id.replace(/~/ig, "~0").replace(/\//ig, "~1") + const escapedId = id.replace(/~/gi, "~0").replace(/\//gi, "~1") const out = { $ref: options.getRef(escapedId) } if (!Record.has($defs, id)) { $defs[id] = out - $defs[id] = go(ast, $defs, "ignore-identifier", path, options, "handle-annotation", errors) + $defs[id] = go( + ast, + $defs, + "ignore-identifier", + path, + options, + "handle-annotation", + errors + ) } return out } @@ -676,17 +753,41 @@ function go( const t = AST.getTransformationFrom(ast) if (t === undefined) { return mergeRefinements( - go(ast.from, $defs, identifier, path, options, "handle-annotation", errors), + go( + ast.from, + $defs, + identifier, + path, + options, + "handle-annotation", + errors + ), handler, ast ) } else { - return go(t, $defs, identifier, path, options, "handle-annotation", errors) + return go( + t, + $defs, + identifier, + path, + options, + "handle-annotation", + errors + ) } } default: return { - ...go(ast, $defs, identifier, path, options, "ignore-annotation", errors), + ...go( + ast, + $defs, + identifier, + path, + options, + "ignore-annotation", + errors + ), ...handler } as any } @@ -695,7 +796,15 @@ function go( } const surrogate = AST.getSurrogateAnnotation(ast) if (Option.isSome(surrogate)) { - return go(surrogate.value, $defs, identifier, path, options, "handle-annotation", errors) + return go( + surrogate.value, + $defs, + identifier, + path, + options, + "handle-annotation", + errors + ) } switch (ast._tag) { // Unsupported @@ -705,14 +814,29 @@ function go( case "UniqueSymbol": case "SymbolKeyword": { if (errors === "ignore-errors") return addASTAnnotations(constAny, ast) - throw new Error(errors_.getJSONSchemaMissingAnnotationErrorMessage(path, ast)) + throw new Error( + errors_.getJSONSchemaMissingAnnotationErrorMessage(path, ast) + ) } case "Suspend": { if (identifier === "handle-identifier") { if (errors === "ignore-errors") return addASTAnnotations(constAny, ast) - throw new Error(errors_.getJSONSchemaMissingIdentifierAnnotationErrorMessage(path, ast)) + throw new Error( + errors_.getJSONSchemaMissingIdentifierAnnotationErrorMessage( + path, + ast + ) + ) } - return go(ast.f(), $defs, "ignore-identifier", path, options, "handle-annotation", errors) + return go( + ast.f(), + $defs, + "ignore-identifier", + path, + options, + "handle-annotation", + errors + ) } // Primitives case "NeverKeyword": @@ -743,30 +867,53 @@ function go( return addASTAnnotations({ type: "boolean", enum: [literal] }, ast) } if (errors === "ignore-errors") return addASTAnnotations(constAny, ast) - throw new Error(errors_.getJSONSchemaMissingAnnotationErrorMessage(path, ast)) + throw new Error( + errors_.getJSONSchemaMissingAnnotationErrorMessage(path, ast) + ) } case "Enums": { const anyOf = ast.enums.map((e) => { - const type: "string" | "number" = Predicate.isNumber(e[1]) ? "number" : "string" + const type: "string" | "number" = Predicate.isNumber(e[1]) + ? "number" + : "string" return { type, title: e[0], enum: [e[1]] } }) - return anyOf.length >= 1 ? - addASTAnnotations({ - $comment: "/schemas/enums", - anyOf - }, ast) : - addASTAnnotations(constNever, ast) + return anyOf.length >= 1 + ? addASTAnnotations( + { + $comment: "/schemas/enums", + anyOf + }, + ast + ) + : addASTAnnotations(constNever, ast) } case "TupleType": { const elements = ast.elements.map((e, i) => mergeJsonSchemaAnnotations( - go(e.type, $defs, "handle-identifier", path.concat(i), options, "handle-annotation", errors), + go( + e.type, + $defs, + "handle-identifier", + path.concat(i), + options, + "handle-annotation", + errors + ), getContextJsonSchemaAnnotations(e.type, e) ) ) const rest = ast.rest.map((type) => mergeJsonSchemaAnnotations( - go(type.type, $defs, "handle-identifier", path, options, "handle-annotation", errors), + go( + type.type, + $defs, + "handle-identifier", + path, + options, + "handle-annotation", + errors + ), getContextJsonSchemaAnnotations(type.type, type) ) ) @@ -776,7 +923,8 @@ function go( // --------------------------------------------- const len = ast.elements.length if (len > 0) { - output.minItems = len - ast.elements.filter((element) => element.isOptional).length + output.minItems = + len - ast.elements.filter((element) => element.isOptional).length if (options.target === "jsonSchema7") { output.items = elements } else { @@ -789,7 +937,9 @@ function go( const restLength = rest.length if (restLength > 0) { const head = rest[0] - const isHomogeneous = restLength === 1 && ast.elements.every((e) => e.type === ast.rest[0].type) + const isHomogeneous = + restLength === 1 && + ast.elements.every((e) => e.type === ast.rest[0].type) if (isHomogeneous) { if (options.target === "jsonSchema7") { output.items = head @@ -809,8 +959,11 @@ function go( // handle post rest elements // --------------------------------------------- if (restLength > 1) { - if (errors === "ignore-errors") return addASTAnnotations(constAny, ast) - throw new Error(errors_.getJSONSchemaUnsupportedPostRestElementsErrorMessage(path)) + if (errors === "ignore-errors") + return addASTAnnotations(constAny, ast) + throw new Error( + errors_.getJSONSchemaUnsupportedPostRestElementsErrorMessage(path) + ) } } else { if (len > 0) { @@ -827,7 +980,10 @@ function go( return addASTAnnotations(output, ast) } case "TypeLiteral": { - if (ast.propertySignatures.length === 0 && ast.indexSignatures.length === 0) { + if ( + ast.propertySignatures.length === 0 && + ast.indexSignatures.length === 0 + ) { return addASTAnnotations(constEmptyStruct, ast) } const output: JsonSchema7Object = { @@ -843,7 +999,7 @@ function go( const parameter = is.parameter switch (parameter._tag) { case "StringKeyword": { - output.additionalProperties = go( + const additionalProperties = go( pruned, $defs, "handle-identifier", @@ -852,10 +1008,23 @@ function go( "handle-annotation", errors ) + output.additionalProperties = isNeverWithoutCustomAnnotations( + additionalProperties + ) + ? false + : additionalProperties break } case "TemplateLiteral": { - patternProperties = go(pruned, $defs, "handle-identifier", path, options, "handle-annotation", errors) + patternProperties = go( + pruned, + $defs, + "handle-identifier", + path, + options, + "handle-annotation", + errors + ) propertyNames = { type: "string", pattern: AST.getTemplateLiteralRegExp(parameter).source @@ -863,8 +1032,24 @@ function go( break } case "Refinement": { - patternProperties = go(pruned, $defs, "handle-identifier", path, options, "handle-annotation", errors) - propertyNames = go(parameter, $defs, "handle-identifier", path, options, "handle-annotation", errors) + patternProperties = go( + pruned, + $defs, + "handle-identifier", + path, + options, + "handle-annotation", + errors + ) + propertyNames = go( + parameter, + $defs, + "handle-identifier", + path, + options, + "handle-annotation", + errors + ) break } case "SymbolKeyword": { @@ -901,7 +1086,15 @@ function go( const pruned = pruneUndefined(ps.type) const type = pruned ?? ps.type output.properties[name] = mergeJsonSchemaAnnotations( - go(type, $defs, "handle-identifier", path.concat(ps.name), options, "handle-annotation", errors), + go( + type, + $defs, + "handle-identifier", + path.concat(ps.name), + options, + "handle-annotation", + errors + ), getContextJsonSchemaAnnotations(type, ps) ) // --------------------------------------------- @@ -911,8 +1104,11 @@ function go( output.required.push(name) } } else { - if (errors === "ignore-errors") return addASTAnnotations(constAny, ast) - throw new Error(errors_.getJSONSchemaUnsupportedKeyErrorMessage(name, path)) + if (errors === "ignore-errors") + return addASTAnnotations(constAny, ast) + throw new Error( + errors_.getJSONSchemaUnsupportedKeyErrorMessage(name, path) + ) } } // --------------------------------------------- @@ -930,7 +1126,15 @@ function go( } case "Union": { const members: Array = ast.types.map((t) => - go(t, $defs, "handle-identifier", path, options, "handle-annotation", errors) + go( + t, + $defs, + "handle-identifier", + path, + options, + "handle-annotation", + errors + ) ) const anyOf = compactUnion(members) switch (anyOf.length) { @@ -943,33 +1147,68 @@ function go( } } case "Refinement": - return go(ast.from, $defs, identifier, path, options, "handle-annotation", errors) + return go( + ast.from, + $defs, + identifier, + path, + options, + "handle-annotation", + errors + ) case "TemplateLiteral": { const regex = AST.getTemplateLiteralRegExp(ast) - return addASTAnnotations({ - type: "string", - title: String(ast), - description: "a template literal", - pattern: regex.source - }, ast) + return addASTAnnotations( + { + type: "string", + title: String(ast), + description: "a template literal", + pattern: regex.source + }, + ast + ) } case "Transformation": { if (isParseJsonTransformation(ast.from)) { const out: JsonSchema7String & { contentSchema?: JsonSchema7 } = { - "type": "string", - "contentMediaType": "application/json" + type: "string", + contentMediaType: "application/json" } if (isContentSchemaSupported(options)) { - out["contentSchema"] = go(ast.to, $defs, identifier, path, options, "handle-annotation", errors) + out["contentSchema"] = go( + ast.to, + $defs, + identifier, + path, + options, + "handle-annotation", + errors + ) } return out } - const from = go(ast.from, $defs, identifier, path, options, "handle-annotation", errors) + const from = go( + ast.from, + $defs, + identifier, + path, + options, + "handle-annotation", + errors + ) if ( ast.transformation._tag === "TypeLiteralTransformation" && isJsonSchema7Object(from) ) { - const to = go(ast.to, {}, "ignore-identifier", path, options, "handle-annotation", "ignore-errors") + const to = go( + ast.to, + {}, + "ignore-identifier", + path, + options, + "handle-annotation", + "ignore-errors" + ) if (isJsonSchema7Object(to)) { for (const t of ast.transformation.propertySignatureTransformations) { const toKey = t.to @@ -980,13 +1219,22 @@ function go( const fromProperty = from.properties[fromKey] if (Predicate.isRecord(fromProperty)) { const annotations: JsonSchemaAnnotations = {} - if (Predicate.isString(toProperty.title)) annotations.title = toProperty.title - if (Predicate.isString(toProperty.description)) annotations.description = toProperty.description - if (Array.isArray(toProperty.examples)) annotations.examples = toProperty.examples - if (Object.hasOwn(toProperty, "default") && toProperty.default !== undefined) { + if (Predicate.isString(toProperty.title)) + annotations.title = toProperty.title + if (Predicate.isString(toProperty.description)) + annotations.description = toProperty.description + if (Array.isArray(toProperty.examples)) + annotations.examples = toProperty.examples + if ( + Object.hasOwn(toProperty, "default") && + toProperty.default !== undefined + ) { annotations.default = toProperty.default } - from.properties[fromKey] = addAnnotations(fromProperty, annotations) + from.properties[fromKey] = addAnnotations( + fromProperty, + annotations + ) } } } @@ -998,13 +1246,24 @@ function go( } } -function isJsonSchema7Object(jsonSchema: unknown): jsonSchema is JsonSchema7Object { - return Predicate.isRecord(jsonSchema) && jsonSchema.type === "object" && Predicate.isRecord(jsonSchema.properties) +function isJsonSchema7Object( + jsonSchema: unknown +): jsonSchema is JsonSchema7Object { + return ( + Predicate.isRecord(jsonSchema) && + jsonSchema.type === "object" && + Predicate.isRecord(jsonSchema.properties) + ) } function isNeverWithoutCustomAnnotations(jsonSchema: JsonSchema7): boolean { - return jsonSchema === constNever || (Predicate.hasProperty(jsonSchema, "$id") && jsonSchema.$id === constNever.$id && - Object.keys(jsonSchema).length === 3 && jsonSchema.title === AST.neverKeyword.annotations[AST.TitleAnnotationId]) + return ( + jsonSchema === constNever || + (Predicate.hasProperty(jsonSchema, "$id") && + jsonSchema.$id === constNever.$id && + Object.keys(jsonSchema).length === 3 && + jsonSchema.title === AST.neverKeyword.annotations[AST.TitleAnnotationId]) + ) } function isAny(jsonSchema: JsonSchema7): jsonSchema is JsonSchema7Any { @@ -1019,8 +1278,14 @@ function isVoid(jsonSchema: JsonSchema7): jsonSchema is JsonSchema7Void { return "$id" in jsonSchema && jsonSchema.$id === constVoid.$id } -function isCompactableLiteral(jsonSchema: JsonSchema7 | undefined): jsonSchema is JsonSchema7Enum { - return Predicate.hasProperty(jsonSchema, "enum") && "type" in jsonSchema && Object.keys(jsonSchema).length === 2 +function isCompactableLiteral( + jsonSchema: JsonSchema7 | undefined +): jsonSchema is JsonSchema7Enum { + return ( + Predicate.hasProperty(jsonSchema, "enum") && + "type" in jsonSchema && + Object.keys(jsonSchema).length === 2 + ) } function compactUnion(members: Array): Array { diff --git a/repos/effect/packages/effect/test/Schema/JSONSchema.test.ts b/repos/effect/packages/effect/test/Schema/JSONSchema.test.ts index e3eaec5..ae379a5 100644 --- a/repos/effect/packages/effect/test/Schema/JSONSchema.test.ts +++ b/repos/effect/packages/effect/test/Schema/JSONSchema.test.ts @@ -1,5 +1,10 @@ import { describe, it } from "@effect/vitest" -import { assertFalse, assertTrue, deepStrictEqual, throws } from "@effect/vitest/utils" +import { + assertFalse, + assertTrue, + deepStrictEqual, + throws +} from "@effect/vitest/utils" import Ajv from "ajv" import * as A from "effect/Arbitrary" import * as fc from "effect/FastCheck" @@ -28,7 +33,10 @@ const expectProperty = ( const arb = A.make(encodedBoundSchema) const is = Schema.is(encodedBoundSchema) const validate = getAjvValidate(jsonSchema) - fc.assert(fc.property(arb, (i) => is(i) && validate(i)), params) + fc.assert( + fc.property(arb, (i) => is(i) && validate(i)), + params + ) } } @@ -38,7 +46,7 @@ const expectJSONSchema = ( ) => { const jsonSchema = JSONSchema.make(schema) deepStrictEqual(jsonSchema, { - "$schema": "http://json-schema.org/draft-07/schema#", + $schema: "http://json-schema.org/draft-07/schema#", ...expectedJsonSchema } as any) return jsonSchema @@ -93,7 +101,11 @@ const expectJSONSchemaAnnotations = ( description: "269d3e58-8fb2-43cb-a389-8146c353fdd5", title: "5401c637-61f2-49b8-b74d-17f058c2670f" } - expectJSONSchemaProperty(schema.annotations(jsonSchemaAnnotations), { ...expected, ...jsonSchemaAnnotations }, params) + expectJSONSchemaProperty( + schema.annotations(jsonSchemaAnnotations), + { ...expected, ...jsonSchemaAnnotations }, + params + ) } const expectError = (schema: Schema.Schema, message: string) => { @@ -101,22 +113,26 @@ const expectError = (schema: Schema.Schema, message: string) => { } // Using this instead of Schema.JsonNumber to avoid cluttering the output with unnecessary description and title -const JsonNumber = Schema.Number.pipe(Schema.filter((n) => Number.isFinite(n), { jsonSchema: {} })) +const JsonNumber = Schema.Number.pipe( + Schema.filter((n) => Number.isFinite(n), { jsonSchema: {} }) +) describe("fromAST", () => { it("definitionsPath", () => { - const schema = Schema.String.annotations({ identifier: "08368672-2c02-4d6d-92b0-dd0019b33a7b" }) + const schema = Schema.String.annotations({ + identifier: "08368672-2c02-4d6d-92b0-dd0019b33a7b" + }) const definitions = {} const jsonSchema = JSONSchema.fromAST(schema.ast, { definitions, definitionPath: "#/components/schemas/" }) deepStrictEqual(jsonSchema, { - "$ref": "#/components/schemas/08368672-2c02-4d6d-92b0-dd0019b33a7b" + $ref: "#/components/schemas/08368672-2c02-4d6d-92b0-dd0019b33a7b" }) deepStrictEqual(definitions, { "08368672-2c02-4d6d-92b0-dd0019b33a7b": { - "type": "string" + type: "string" } }) }) @@ -126,87 +142,84 @@ describe("fromAST", () => { describe("nullable handling", () => { it("Null", () => { const schema = Schema.Null - expectJSONSchemaAnnotations(schema, { "type": "null" }) + expectJSONSchemaAnnotations(schema, { type: "null" }) }) it("NullOr(String)", () => { const schema = Schema.NullOr(Schema.String) expectJSONSchemaAnnotations(schema, { - "anyOf": [ - { "type": "string" }, - { "type": "null" } - ] + anyOf: [{ type: "string" }, { type: "null" }] }) }) it("NullOr(Any)", () => { const schema = Schema.NullOr(Schema.Any) expectJSONSchemaAnnotations(schema, { - "$id": "/schemas/any", - "title": "any" + $id: "/schemas/any", + title: "any" }) }) it("NullOr(Unknown)", () => { const schema = Schema.NullOr(Schema.Unknown) expectJSONSchemaAnnotations(schema, { - "$id": "/schemas/unknown", - "title": "unknown" + $id: "/schemas/unknown", + title: "unknown" }) }) it("NullOr(Void)", () => { const schema = Schema.NullOr(Schema.Void) expectJSONSchemaAnnotations(schema, { - "$id": "/schemas/void", - "title": "void" + $id: "/schemas/void", + title: "void" }) }) it("Literal | null", () => { const schema = Schema.Literal("a", null) expectJSONSchemaAnnotations(schema, { - "anyOf": [ + anyOf: [ { - "type": "string", - "enum": ["a"] + type: "string", + enum: ["a"] }, - { "type": "null" } + { type: "null" } ] }) }) it("Literal | null(with description)", () => { - const schema = Schema.Union(Schema.Literal("a"), Schema.Null.annotations({ description: "mydescription" })) + const schema = Schema.Union( + Schema.Literal("a"), + Schema.Null.annotations({ description: "mydescription" }) + ) expectJSONSchemaAnnotations(schema, { - "anyOf": [ + anyOf: [ { - "type": "string", - "enum": ["a"] + type: "string", + enum: ["a"] }, { - "type": "null", - "description": "mydescription" + type: "null", + description: "mydescription" } ] }) }) it("Nested nullable unions", () => { - const schema = Schema.Union(Schema.NullOr(Schema.String), Schema.Literal("a", null)) + const schema = Schema.Union( + Schema.NullOr(Schema.String), + Schema.Literal("a", null) + ) expectJSONSchemaAnnotations(schema, { - "anyOf": [ + anyOf: [ { - "anyOf": [ - { "type": "string" }, - { "type": "null" } - ] + anyOf: [{ type: "string" }, { type: "null" }] }, { - "anyOf": [ - { "type": "string", "enum": ["a"] }, - { "type": "null" } - ] + anyOf: [{ type: "string", enum: ["a"] }, { type: "null" }] } ] }) @@ -214,16 +227,18 @@ describe("fromAST", () => { }) it("parseJson handling", () => { - const schema = Schema.parseJson(Schema.Struct({ - a: Schema.parseJson(Schema.NumberFromString) - })) + const schema = Schema.parseJson( + Schema.Struct({ + a: Schema.parseJson(Schema.NumberFromString) + }) + ) const definitions = {} const jsonSchema = JSONSchema.fromAST(schema.ast, { definitions }) deepStrictEqual(jsonSchema, { - "type": "string", - "contentMediaType": "application/json" + type: "string", + contentMediaType: "application/json" }) deepStrictEqual(definitions, {}) }) @@ -233,120 +248,151 @@ describe("fromAST", () => { describe("nullable handling", () => { it("Null", () => { const schema = Schema.Null - expectJSONSchema2019(schema, { "type": "null" }, {}) + expectJSONSchema2019(schema, { type: "null" }, {}) }) it("NullOr(String)", () => { const schema = Schema.NullOr(Schema.String) - expectJSONSchema2019(schema, { - "anyOf": [ - { "type": "string" }, - { "type": "null" } - ] - }, {}) + expectJSONSchema2019( + schema, + { + anyOf: [{ type: "string" }, { type: "null" }] + }, + {} + ) }) it("NullOr(Any)", () => { const schema = Schema.NullOr(Schema.Any) - expectJSONSchema2019(schema, { - "$id": "/schemas/any", - "title": "any" - }, {}) + expectJSONSchema2019( + schema, + { + $id: "/schemas/any", + title: "any" + }, + {} + ) }) it("NullOr(Unknown)", () => { const schema = Schema.NullOr(Schema.Unknown) - expectJSONSchema2019(schema, { - "$id": "/schemas/unknown", - "title": "unknown" - }, {}) + expectJSONSchema2019( + schema, + { + $id: "/schemas/unknown", + title: "unknown" + }, + {} + ) }) it("NullOr(Void)", () => { const schema = Schema.NullOr(Schema.Void) - expectJSONSchema2019(schema, { - "$id": "/schemas/void", - "title": "void" - }, {}) + expectJSONSchema2019( + schema, + { + $id: "/schemas/void", + title: "void" + }, + {} + ) }) it("Literal | null", () => { const schema = Schema.Literal("a", null) - expectJSONSchema2019(schema, { - "anyOf": [ - { - "type": "string", - "enum": ["a"] - }, - { "type": "null" } - ] - }, {}) + expectJSONSchema2019( + schema, + { + anyOf: [ + { + type: "string", + enum: ["a"] + }, + { type: "null" } + ] + }, + {} + ) }) it("Literal | null(with description)", () => { - const schema = Schema.Union(Schema.Literal("a"), Schema.Null.annotations({ description: "mydescription" })) - expectJSONSchema2019(schema, { - "anyOf": [ - { - "type": "string", - "enum": ["a"] - }, - { - "type": "null", - "description": "mydescription" - } - ] - }, {}) + const schema = Schema.Union( + Schema.Literal("a"), + Schema.Null.annotations({ description: "mydescription" }) + ) + expectJSONSchema2019( + schema, + { + anyOf: [ + { + type: "string", + enum: ["a"] + }, + { + type: "null", + description: "mydescription" + } + ] + }, + {} + ) }) it("Nested nullable unions", () => { - const schema = Schema.Union(Schema.NullOr(Schema.String), Schema.Literal("a", null)) - expectJSONSchema2019(schema, { - "anyOf": [ - { - "anyOf": [ - { "type": "string" }, - { "type": "null" } - ] - }, - { - "anyOf": [ - { "type": "string", "enum": ["a"] }, - { "type": "null" } - ] - } - ] - }, {}) + const schema = Schema.Union( + Schema.NullOr(Schema.String), + Schema.Literal("a", null) + ) + expectJSONSchema2019( + schema, + { + anyOf: [ + { + anyOf: [{ type: "string" }, { type: "null" }] + }, + { + anyOf: [{ type: "string", enum: ["a"] }, { type: "null" }] + } + ] + }, + {} + ) }) }) it("parseJson handling", () => { - const schema = Schema.parseJson(Schema.Struct({ - a: Schema.parseJson(Schema.NumberFromString) - })) - expectJSONSchema2019(schema, { - "type": "string", - "contentMediaType": "application/json", - "contentSchema": { - "type": "object", - "required": ["a"], - "properties": { - "a": { - "type": "string", - "contentMediaType": "application/json", - "contentSchema": { - "$ref": "#/$defs/NumberFromString" + const schema = Schema.parseJson( + Schema.Struct({ + a: Schema.parseJson(Schema.NumberFromString) + }) + ) + expectJSONSchema2019( + schema, + { + type: "string", + contentMediaType: "application/json", + contentSchema: { + type: "object", + required: ["a"], + properties: { + a: { + type: "string", + contentMediaType: "application/json", + contentSchema: { + $ref: "#/$defs/NumberFromString" + } } - } - }, - "additionalProperties": false - } - }, { - "NumberFromString": { - "description": "a string to be decoded into a number", - "type": "string" + }, + additionalProperties: false + } + }, + { + NumberFromString: { + description: "a string to be decoded into a number", + type: "string" + } } - }) + ) }) }) @@ -354,144 +400,176 @@ describe("fromAST", () => { describe("nullable handling", () => { it("Null", () => { const schema = Schema.Null - expectJSONSchemaOpenApi31(schema, { "type": "null" }, {}) + expectJSONSchemaOpenApi31(schema, { type: "null" }, {}) }) it("NullOr(String)", () => { const schema = Schema.NullOr(Schema.String) - expectJSONSchemaOpenApi31(schema, { - "anyOf": [ - { "type": "string" }, - { "type": "null" } - ] - }, {}) + expectJSONSchemaOpenApi31( + schema, + { + anyOf: [{ type: "string" }, { type: "null" }] + }, + {} + ) }) it("NullOr(Any)", () => { const schema = Schema.NullOr(Schema.Any) - expectJSONSchemaOpenApi31(schema, { - "$id": "/schemas/any", - "title": "any" - }, {}) + expectJSONSchemaOpenApi31( + schema, + { + $id: "/schemas/any", + title: "any" + }, + {} + ) }) it("NullOr(Unknown)", () => { const schema = Schema.NullOr(Schema.Unknown) - expectJSONSchemaOpenApi31(schema, { - "$id": "/schemas/unknown", - "title": "unknown" - }, {}) + expectJSONSchemaOpenApi31( + schema, + { + $id: "/schemas/unknown", + title: "unknown" + }, + {} + ) }) it("NullOr(Void)", () => { const schema = Schema.NullOr(Schema.Void) - expectJSONSchemaOpenApi31(schema, { - "$id": "/schemas/void", - "title": "void" - }, {}) + expectJSONSchemaOpenApi31( + schema, + { + $id: "/schemas/void", + title: "void" + }, + {} + ) }) it("NullOr(Object)", () => { const schema = Schema.NullOr(Schema.Object) - expectJSONSchemaOpenApi31(schema, { - "anyOf": [ - { - "$id": "/schemas/object", - "anyOf": [ - { "type": "object" }, - { "type": "array" } - ], - "description": "an object in the TypeScript meaning, i.e. the `object` type", - "title": "object" - }, - { "type": "null" } - ] - }, {}) + expectJSONSchemaOpenApi31( + schema, + { + anyOf: [ + { + $id: "/schemas/object", + anyOf: [{ type: "object" }, { type: "array" }], + description: + "an object in the TypeScript meaning, i.e. the `object` type", + title: "object" + }, + { type: "null" } + ] + }, + {} + ) }) it("NullOr(Struct({}))", () => { const schema = Schema.NullOr(Schema.Struct({})) - expectJSONSchemaOpenApi31(schema, { - "anyOf": [ - { - "$id": "/schemas/%7B%7D", - "anyOf": [ - { "type": "object" }, - { "type": "array" } - ] - }, - { "type": "null" } - ] - }, {}) + expectJSONSchemaOpenApi31( + schema, + { + anyOf: [ + { + $id: "/schemas/%7B%7D", + anyOf: [{ type: "object" }, { type: "array" }] + }, + { type: "null" } + ] + }, + {} + ) }) it("NullOr(Ref)", () => { const schema = Schema.NullOr( - Schema.String.annotations({ identifier: "b812aaa1-cfe1-4dda-8c9c-360bfa6cb855" }) + Schema.String.annotations({ + identifier: "b812aaa1-cfe1-4dda-8c9c-360bfa6cb855" + }) ) - expectJSONSchemaOpenApi31(schema, { - "anyOf": [ - { - "$ref": "#/$defs/b812aaa1-cfe1-4dda-8c9c-360bfa6cb855" - }, - { "type": "null" } - ] - }, { - "b812aaa1-cfe1-4dda-8c9c-360bfa6cb855": { - "type": "string" + expectJSONSchemaOpenApi31( + schema, + { + anyOf: [ + { + $ref: "#/$defs/b812aaa1-cfe1-4dda-8c9c-360bfa6cb855" + }, + { type: "null" } + ] + }, + { + "b812aaa1-cfe1-4dda-8c9c-360bfa6cb855": { + type: "string" + } } - }) + ) }) it("NullOr(Number)", () => { const schema = Schema.NullOr(Schema.Number) - expectJSONSchemaOpenApi31(schema, { - "anyOf": [ - { "type": "number" }, - { "type": "null" } - ] - }, {}) + expectJSONSchemaOpenApi31( + schema, + { + anyOf: [{ type: "number" }, { type: "null" }] + }, + {} + ) }) it("NullOr(Int)", () => { const schema = Schema.NullOr(Schema.Int) - expectJSONSchemaOpenApi31(schema, { - "anyOf": [ - { - "$ref": "#/$defs/Int" - }, - { "type": "null" } - ] - }, { - "Int": { - "title": "int", - "description": "an integer", - "type": "integer" + expectJSONSchemaOpenApi31( + schema, + { + anyOf: [ + { + $ref: "#/$defs/Int" + }, + { type: "null" } + ] + }, + { + Int: { + title: "int", + description: "an integer", + type: "integer" + } } - }) + ) }) it("NullOr(Boolean)", () => { const schema = Schema.NullOr(Schema.Boolean) - expectJSONSchemaOpenApi31(schema, { - "anyOf": [ - { "type": "boolean" }, - { "type": "null" } - ] - }, {}) + expectJSONSchemaOpenApi31( + schema, + { + anyOf: [{ type: "boolean" }, { type: "null" }] + }, + {} + ) }) it("NullOr(Array)", () => { const schema = Schema.NullOr(Schema.Array(Schema.String)) - expectJSONSchemaOpenApi31(schema, { - "anyOf": [ - { - "items": { "type": "string" }, - "type": "array" - }, - { "type": "null" } - ] - }, {}) + expectJSONSchemaOpenApi31( + schema, + { + anyOf: [ + { + items: { type: "string" }, + type: "array" + }, + { type: "null" } + ] + }, + {} + ) }) it("NullOr(Enum)", () => { @@ -500,147 +578,179 @@ describe("fromAST", () => { Banana } const schema = Schema.NullOr(Schema.Enums(Fruits)) - expectJSONSchemaOpenApi31(schema, { - "anyOf": [ - { - "$comment": "/schemas/enums", - "anyOf": [ - { - "type": "number", - "title": "Apple", - "enum": [0] - }, - { - "type": "number", - "title": "Banana", - "enum": [1] - } - ] - }, - { "type": "null" } - ] - }, {}) + expectJSONSchemaOpenApi31( + schema, + { + anyOf: [ + { + $comment: "/schemas/enums", + anyOf: [ + { + type: "number", + title: "Apple", + enum: [0] + }, + { + type: "number", + title: "Banana", + enum: [1] + } + ] + }, + { type: "null" } + ] + }, + {} + ) }) it("NullOr(Literal)", () => { const schema = Schema.NullOr(Schema.Literal("a")) - expectJSONSchemaOpenApi31(schema, { - "anyOf": [ - { - "type": "string", - "enum": ["a"] - }, - { "type": "null" } - ] - }, {}) + expectJSONSchemaOpenApi31( + schema, + { + anyOf: [ + { + type: "string", + enum: ["a"] + }, + { type: "null" } + ] + }, + {} + ) }) it("Literal | null", () => { const schema = Schema.Literal("a", null) - expectJSONSchemaOpenApi31(schema, { - "anyOf": [ - { - "type": "string", - "enum": ["a"] - }, - { "type": "null" } - ] - }, {}) + expectJSONSchemaOpenApi31( + schema, + { + anyOf: [ + { + type: "string", + enum: ["a"] + }, + { type: "null" } + ] + }, + {} + ) }) it("Literal | null(with description)", () => { - const schema = Schema.Union(Schema.Literal("a"), Schema.Null.annotations({ description: "mydescription" })) - expectJSONSchemaOpenApi31(schema, { - "anyOf": [ - { - "type": "string", - "enum": ["a"] - }, - { - "description": "mydescription", - "type": "null" - } - ] - }, {}) + const schema = Schema.Union( + Schema.Literal("a"), + Schema.Null.annotations({ description: "mydescription" }) + ) + expectJSONSchemaOpenApi31( + schema, + { + anyOf: [ + { + type: "string", + enum: ["a"] + }, + { + description: "mydescription", + type: "null" + } + ] + }, + {} + ) }) it("Nested nullable unions", () => { - const schema = Schema.Union(Schema.NullOr(Schema.String), Schema.Literal("a", null)) - expectJSONSchemaOpenApi31(schema, { - "anyOf": [ - { - "anyOf": [ - { "type": "string" }, - { "type": "null" } - ] - }, - { - "anyOf": [ - { "type": "string", "enum": ["a"] }, - { "type": "null" } - ] - } - ] - }, {}) + const schema = Schema.Union( + Schema.NullOr(Schema.String), + Schema.Literal("a", null) + ) + expectJSONSchemaOpenApi31( + schema, + { + anyOf: [ + { + anyOf: [{ type: "string" }, { type: "null" }] + }, + { + anyOf: [{ type: "string", enum: ["a"] }, { type: "null" }] + } + ] + }, + {} + ) }) it("NullOr(Struct({ a: String }))", () => { const schema = Schema.NullOr(Schema.Struct({ a: Schema.String })) - expectJSONSchemaOpenApi31(schema, { - "anyOf": [ - { - "additionalProperties": false, - "properties": { "a": { "type": "string" } }, - "required": ["a"], - "type": "object" - }, - { "type": "null" } - ] - }, {}) + expectJSONSchemaOpenApi31( + schema, + { + anyOf: [ + { + additionalProperties: false, + properties: { a: { type: "string" } }, + required: ["a"], + type: "object" + }, + { type: "null" } + ] + }, + {} + ) }) }) it("parseJson handling", () => { - const schema = Schema.parseJson(Schema.Struct({ - a: Schema.parseJson(Schema.NumberFromString) - })) - expectJSONSchemaOpenApi31(schema, { - "type": "string", - "contentMediaType": "application/json", - "contentSchema": { - "type": "object", - "required": ["a"], - "properties": { - "a": { - "type": "string", - "contentMediaType": "application/json", - "contentSchema": { - "$ref": "#/$defs/NumberFromString" + const schema = Schema.parseJson( + Schema.Struct({ + a: Schema.parseJson(Schema.NumberFromString) + }) + ) + expectJSONSchemaOpenApi31( + schema, + { + type: "string", + contentMediaType: "application/json", + contentSchema: { + type: "object", + required: ["a"], + properties: { + a: { + type: "string", + contentMediaType: "application/json", + contentSchema: { + $ref: "#/$defs/NumberFromString" + } } - } - }, - "additionalProperties": false - } - }, { - "NumberFromString": { - "description": "a string to be decoded into a number", - "type": "string" + }, + additionalProperties: false + } + }, + { + NumberFromString: { + description: "a string to be decoded into a number", + type: "string" + } } - }) + ) }) }) }) describe("topLevelReferenceStrategy", () => { it(`"skip"`, () => { - const schema = Schema.String.annotations({ identifier: "1b205579-f159-48d4-a218-f09426bca040" }) + const schema = Schema.String.annotations({ + identifier: "1b205579-f159-48d4-a218-f09426bca040" + }) const definitions = {} const jsonSchema = JSONSchema.fromAST(schema.ast, { definitions, topLevelReferenceStrategy: "skip" }) deepStrictEqual(jsonSchema, { - "type": "string" + type: "string" }) deepStrictEqual(definitions, {}) }) @@ -655,14 +765,14 @@ describe("fromAST", () => { additionalPropertiesStrategy: "allow" }) deepStrictEqual(jsonSchema, { - "type": "object", - "properties": { - "a": { - "type": "string" + type: "object", + properties: { + a: { + type: "string" } }, - "required": ["a"], - "additionalProperties": true + required: ["a"], + additionalProperties: true }) deepStrictEqual(definitions, {}) }) @@ -673,31 +783,33 @@ describe("make", () => { it("should filter out non-JSON values and cyclic references from default and examples", () => { const cyclic: any = { value: "test" } cyclic.self = cyclic - const schema = Schema.String.annotations({ default: 1n as any, examples: ["a", 1n as any, cyclic, "b"] }) + const schema = Schema.String.annotations({ + default: 1n as any, + examples: ["a", 1n as any, cyclic, "b"] + }) expectJSONSchemaAnnotations(schema, { - "type": "string", - "examples": ["a", "b"] + type: "string", + examples: ["a", "b"] }) }) - it("handling of a top level `parseJson` should targeting the \"to\" side", () => { - const schema = Schema.parseJson(Schema.Struct({ - a: Schema.parseJson(Schema.NumberFromString) - })) - expectJSONSchema( - schema, - { - "type": "object", - "required": ["a"], - "properties": { - "a": { - "type": "string", - "contentMediaType": "application/json" - } - }, - "additionalProperties": false - } + it('handling of a top level `parseJson` should targeting the "to" side', () => { + const schema = Schema.parseJson( + Schema.Struct({ + a: Schema.parseJson(Schema.NumberFromString) + }) ) + expectJSONSchema(schema, { + type: "object", + required: ["a"], + properties: { + a: { + type: "string", + contentMediaType: "application/json" + } + }, + additionalProperties: false + }) }) describe("Unsupported schemas", () => { @@ -832,9 +944,9 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` it("Never", () => { const jsonSchema: Root = { - "$id": "/schemas/never", - "not": {}, - "title": "never" + $id: "/schemas/never", + not: {}, + title: "never" } expectJSONSchema(Schema.Never, jsonSchema) const validate = getAjvValidate(jsonSchema) @@ -843,27 +955,25 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` it("Any", () => { expectJSONSchemaAnnotations(Schema.Any, { - "$id": "/schemas/any", - "title": "any" + $id: "/schemas/any", + title: "any" }) }) it("Unknown", () => { expectJSONSchemaAnnotations(Schema.Unknown, { - "$id": "/schemas/unknown", - "title": "unknown" + $id: "/schemas/unknown", + title: "unknown" }) }) it("Object", () => { const jsonSchema: Root = { - "$id": "/schemas/object", - "anyOf": [ - { "type": "object" }, - { "type": "array" } - ], - "description": "an object in the TypeScript meaning, i.e. the `object` type", - "title": "object" + $id: "/schemas/object", + anyOf: [{ type: "object" }, { type: "array" }], + description: + "an object in the TypeScript meaning, i.e. the `object` type", + title: "object" } expectJSONSchemaAnnotations(Schema.Object, jsonSchema) @@ -879,12 +989,15 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` it("empty struct: Schema.Struct({})", () => { const schema = Schema.Struct({}) const jsonSchema: Root = { - "$id": "/schemas/%7B%7D", - "anyOf": [{ - "type": "object" - }, { - "type": "array" - }] + $id: "/schemas/%7B%7D", + anyOf: [ + { + type: "object" + }, + { + type: "array" + } + ] } expectJSONSchemaAnnotations(schema, jsonSchema) const validate = getAjvValidate(jsonSchema) @@ -898,49 +1011,49 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` it("Void", () => { expectJSONSchemaAnnotations(Schema.Void, { - "$id": "/schemas/void", - "title": "void" + $id: "/schemas/void", + title: "void" }) }) it("String", () => { expectJSONSchemaAnnotations(Schema.String, { - "type": "string" + type: "string" }) }) it("Number", () => { expectJSONSchema(Schema.Number, { - "type": "number" + type: "number" }) }) it("JsonNumber", () => { expectJSONSchemaProperty(Schema.JsonNumber, { - "$defs": { - "JsonNumber": { - "type": "number", - "title": "finite", - "description": "a finite number" + $defs: { + JsonNumber: { + type: "number", + title: "finite", + description: "a finite number" } }, - "$ref": "#/$defs/JsonNumber" + $ref: "#/$defs/JsonNumber" }) }) it("Boolean", () => { expectJSONSchemaAnnotations(Schema.Boolean, { - "type": "boolean" + type: "boolean" }) }) it("TemplateLiteral", () => { const schema = Schema.TemplateLiteral(Schema.Literal("a"), Schema.Number) const jsonSchema: Root = { - "type": "string", - "pattern": "^a[+-]?\\d*\\.?\\d+(?:[Ee][+-]?\\d+)?$", - "title": "`a${number}`", - "description": "a template literal" + type: "string", + pattern: "^a[+-]?\\d*\\.?\\d+(?:[Ee][+-]?\\d+)?$", + title: "`a${number}`", + description: "a template literal" } expectJSONSchemaAnnotations(schema, jsonSchema) const validate = getAjvValidate(jsonSchema) @@ -953,60 +1066,65 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` describe("Literal", () => { it("null literal", () => { expectJSONSchemaAnnotations(Schema.Null, { - "type": "null" - }) - expectJSONSchemaProperty(Schema.Null.annotations({ identifier: "9b7d3b2b-3b3a-4741-8c4c-9cae776c47f6" }), { - "$defs": { - "9b7d3b2b-3b3a-4741-8c4c-9cae776c47f6": { - "type": "null" - } - }, - "$ref": "#/$defs/9b7d3b2b-3b3a-4741-8c4c-9cae776c47f6" + type: "null" }) + expectJSONSchemaProperty( + Schema.Null.annotations({ + identifier: "9b7d3b2b-3b3a-4741-8c4c-9cae776c47f6" + }), + { + $defs: { + "9b7d3b2b-3b3a-4741-8c4c-9cae776c47f6": { + type: "null" + } + }, + $ref: "#/$defs/9b7d3b2b-3b3a-4741-8c4c-9cae776c47f6" + } + ) }) it("string literals", () => { expectJSONSchemaAnnotations(Schema.Literal("a"), { - "type": "string", - "enum": ["a"] + type: "string", + enum: ["a"] }) expectJSONSchemaAnnotations(Schema.Literal("a", "b"), { - "type": "string", - "enum": ["a", "b"] + type: "string", + enum: ["a", "b"] }) }) it("number literals", () => { expectJSONSchemaAnnotations(Schema.Literal(1), { - "type": "number", - "enum": [1] + type: "number", + enum: [1] }) expectJSONSchemaAnnotations(Schema.Literal(1, 2), { - "type": "number", - "enum": [1, 2] + type: "number", + enum: [1, 2] }) }) it("boolean literals", () => { expectJSONSchemaAnnotations(Schema.Literal(true), { - "type": "boolean", - "enum": [true] + type: "boolean", + enum: [true] }) expectJSONSchemaAnnotations(Schema.Literal(false), { - "type": "boolean", - "enum": [false] + type: "boolean", + enum: [false] }) expectJSONSchemaAnnotations(Schema.Literal(true, false), { - "type": "boolean", - "enum": [true, false] + type: "boolean", + enum: [true, false] }) }) it("union of literals", () => { expectJSONSchemaAnnotations(Schema.Literal(1, true), { - "anyOf": [ - { "type": "number", "enum": [1] }, - { "type": "boolean", "enum": [true] } + anyOf: [ + { type: "number", enum: [1] }, + { type: "boolean", enum: [true] } ] }) }) @@ -1016,8 +1134,8 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` it("empty enum", () => { enum Empty {} const jsonSchema = expectJSONSchema(Schema.Enums(Empty), { - "$id": "/schemas/never", - "not": {} + $id: "/schemas/never", + not: {} }) const validate = getAjvValidate(jsonSchema) assertFalse(validate(1)) @@ -1028,12 +1146,12 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` Apple } expectJSONSchemaAnnotations(Schema.Enums(Fruits), { - "$comment": "/schemas/enums", - "anyOf": [ + $comment: "/schemas/enums", + anyOf: [ { - "type": "number", - "title": "Apple", - "enum": [0] + type: "number", + title: "Apple", + enum: [0] } ] }) @@ -1045,17 +1163,17 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` Banana } expectJSONSchemaAnnotations(Schema.Enums(Fruits), { - "$comment": "/schemas/enums", - "anyOf": [ + $comment: "/schemas/enums", + anyOf: [ { - "type": "number", - "title": "Apple", - "enum": [0] + type: "number", + title: "Apple", + enum: [0] }, { - "type": "number", - "title": "Banana", - "enum": [1] + type: "number", + title: "Banana", + enum: [1] } ] }) @@ -1067,17 +1185,17 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` Banana = "banana" } expectJSONSchemaAnnotations(Schema.Enums(Fruits), { - "$comment": "/schemas/enums", - "anyOf": [ + $comment: "/schemas/enums", + anyOf: [ { - "type": "string", - "title": "Apple", - "enum": ["apple"] + type: "string", + title: "Apple", + enum: ["apple"] }, { - "type": "string", - "title": "Banana", - "enum": ["banana"] + type: "string", + title: "Banana", + enum: ["banana"] } ] }) @@ -1090,22 +1208,22 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` Cantaloupe = 0 } expectJSONSchemaAnnotations(Schema.Enums(Fruits), { - "$comment": "/schemas/enums", - "anyOf": [ + $comment: "/schemas/enums", + anyOf: [ { - "type": "string", - "title": "Apple", - "enum": ["apple"] + type: "string", + title: "Apple", + enum: ["apple"] }, { - "type": "string", - "title": "Banana", - "enum": ["banana"] + type: "string", + title: "Banana", + enum: ["banana"] }, { - "type": "number", - "title": "Cantaloupe", - "enum": [0] + type: "number", + title: "Cantaloupe", + enum: [0] } ] }) @@ -1118,22 +1236,22 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` Cantaloupe: 3 } as const expectJSONSchemaAnnotations(Schema.Enums(Fruits), { - "$comment": "/schemas/enums", - "anyOf": [ + $comment: "/schemas/enums", + anyOf: [ { - "type": "string", - "title": "Apple", - "enum": ["apple"] + type: "string", + title: "Apple", + enum: ["apple"] }, { - "type": "string", - "title": "Banana", - "enum": ["banana"] + type: "string", + title: "Banana", + enum: ["banana"] }, { - "type": "number", - "title": "Cantaloupe", - "enum": [3] + type: "number", + title: "Cantaloupe", + enum: [3] } ] }) @@ -1142,476 +1260,590 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` describe("Refinement", () => { it("itemsCount (Array)", () => { - expectJSONSchemaAnnotations(Schema.Array(Schema.String).pipe(Schema.itemsCount(2)), { - "type": "array", - "items": { - "type": "string" - }, - "description": "an array of exactly 2 item(s)", - "title": "itemsCount(2)", - "minItems": 2, - "maxItems": 2 - }) + expectJSONSchemaAnnotations( + Schema.Array(Schema.String).pipe(Schema.itemsCount(2)), + { + type: "array", + items: { + type: "string" + }, + description: "an array of exactly 2 item(s)", + title: "itemsCount(2)", + minItems: 2, + maxItems: 2 + } + ) }) it("itemsCount (NonEmptyArray)", () => { - expectJSONSchemaAnnotations(Schema.NonEmptyArray(Schema.String).pipe(Schema.itemsCount(2)), { - "type": "array", - "items": { - "type": "string" - }, - "description": "an array of exactly 2 item(s)", - "title": "itemsCount(2)", - "minItems": 2, - "maxItems": 2 - }) + expectJSONSchemaAnnotations( + Schema.NonEmptyArray(Schema.String).pipe(Schema.itemsCount(2)), + { + type: "array", + items: { + type: "string" + }, + description: "an array of exactly 2 item(s)", + title: "itemsCount(2)", + minItems: 2, + maxItems: 2 + } + ) }) it("minItems (Array)", () => { - expectJSONSchemaAnnotations(Schema.Array(Schema.String).pipe(Schema.minItems(2)), { - "type": "array", - "items": { - "type": "string" - }, - "description": "an array of at least 2 item(s)", - "title": "minItems(2)", - "minItems": 2 - }) + expectJSONSchemaAnnotations( + Schema.Array(Schema.String).pipe(Schema.minItems(2)), + { + type: "array", + items: { + type: "string" + }, + description: "an array of at least 2 item(s)", + title: "minItems(2)", + minItems: 2 + } + ) }) it("minItems (NonEmptyArray)", () => { - expectJSONSchemaAnnotations(Schema.NonEmptyArray(Schema.String).pipe(Schema.minItems(2)), { - "type": "array", - "items": { - "type": "string" - }, - "description": "an array of at least 2 item(s)", - "title": "minItems(2)", - "minItems": 2 - }) + expectJSONSchemaAnnotations( + Schema.NonEmptyArray(Schema.String).pipe(Schema.minItems(2)), + { + type: "array", + items: { + type: "string" + }, + description: "an array of at least 2 item(s)", + title: "minItems(2)", + minItems: 2 + } + ) }) it("maxItems (Array)", () => { - expectJSONSchemaAnnotations(Schema.Array(Schema.String).pipe(Schema.maxItems(2)), { - "type": "array", - "items": { - "type": "string" - }, - "description": "an array of at most 2 item(s)", - "title": "maxItems(2)", - "maxItems": 2 - }) + expectJSONSchemaAnnotations( + Schema.Array(Schema.String).pipe(Schema.maxItems(2)), + { + type: "array", + items: { + type: "string" + }, + description: "an array of at most 2 item(s)", + title: "maxItems(2)", + maxItems: 2 + } + ) }) it("maxItems (NonEmptyArray)", () => { - expectJSONSchemaAnnotations(Schema.NonEmptyArray(Schema.String).pipe(Schema.maxItems(2)), { - "type": "array", - "items": { - "type": "string" - }, - "description": "an array of at most 2 item(s)", - "title": "maxItems(2)", - "minItems": 1, - "maxItems": 2 - }) + expectJSONSchemaAnnotations( + Schema.NonEmptyArray(Schema.String).pipe(Schema.maxItems(2)), + { + type: "array", + items: { + type: "string" + }, + description: "an array of at most 2 item(s)", + title: "maxItems(2)", + minItems: 1, + maxItems: 2 + } + ) }) it("minLength", () => { expectJSONSchemaAnnotations(Schema.String.pipe(Schema.minLength(1)), { - "type": "string", - "title": "minLength(1)", - "description": "a string at least 1 character(s) long", - "minLength": 1 + type: "string", + title: "minLength(1)", + description: "a string at least 1 character(s) long", + minLength: 1 }) }) it("maxLength", () => { expectJSONSchemaAnnotations(Schema.String.pipe(Schema.maxLength(1)), { - "type": "string", - "title": "maxLength(1)", - "description": "a string at most 1 character(s) long", - "maxLength": 1 + type: "string", + title: "maxLength(1)", + description: "a string at most 1 character(s) long", + maxLength: 1 }) }) it("length: number", () => { expectJSONSchemaAnnotations(Schema.String.pipe(Schema.length(1)), { - "type": "string", - "title": "length(1)", - "description": "a single character", - "maxLength": 1, - "minLength": 1 + type: "string", + title: "length(1)", + description: "a single character", + maxLength: 1, + minLength: 1 }) }) it("length: { min, max }", () => { - expectJSONSchemaAnnotations(Schema.String.pipe(Schema.length({ min: 2, max: 4 })), { - "type": "string", - "title": "length({ min: 2, max: 4)", - "description": "a string at least 2 character(s) and at most 4 character(s) long", - "maxLength": 4, - "minLength": 2 - }) + expectJSONSchemaAnnotations( + Schema.String.pipe(Schema.length({ min: 2, max: 4 })), + { + type: "string", + title: "length({ min: 2, max: 4)", + description: + "a string at least 2 character(s) and at most 4 character(s) long", + maxLength: 4, + minLength: 2 + } + ) }) it("greaterThan", () => { expectJSONSchemaAnnotations(JsonNumber.pipe(Schema.greaterThan(1)), { - "type": "number", - "title": "greaterThan(1)", - "description": "a number greater than 1", - "exclusiveMinimum": 1 + type: "number", + title: "greaterThan(1)", + description: "a number greater than 1", + exclusiveMinimum: 1 }) }) it("greaterThanOrEqualTo", () => { - expectJSONSchemaAnnotations(JsonNumber.pipe(Schema.greaterThanOrEqualTo(1)), { - "type": "number", - "title": "greaterThanOrEqualTo(1)", - "description": "a number greater than or equal to 1", - "minimum": 1 - }) + expectJSONSchemaAnnotations( + JsonNumber.pipe(Schema.greaterThanOrEqualTo(1)), + { + type: "number", + title: "greaterThanOrEqualTo(1)", + description: "a number greater than or equal to 1", + minimum: 1 + } + ) }) it("lessThan", () => { expectJSONSchemaAnnotations(JsonNumber.pipe(Schema.lessThan(1)), { - "type": "number", - "title": "lessThan(1)", - "description": "a number less than 1", - "exclusiveMaximum": 1 + type: "number", + title: "lessThan(1)", + description: "a number less than 1", + exclusiveMaximum: 1 }) }) it("lessThanOrEqualTo", () => { - expectJSONSchemaAnnotations(JsonNumber.pipe(Schema.lessThanOrEqualTo(1)), { - "type": "number", - "title": "lessThanOrEqualTo(1)", - "description": "a number less than or equal to 1", - "maximum": 1 - }) + expectJSONSchemaAnnotations( + JsonNumber.pipe(Schema.lessThanOrEqualTo(1)), + { + type: "number", + title: "lessThanOrEqualTo(1)", + description: "a number less than or equal to 1", + maximum: 1 + } + ) }) it("pattern", () => { - expectJSONSchemaAnnotations(Schema.String.pipe(Schema.pattern(/^abb+$/)), { - "type": "string", - "description": "a string matching the pattern ^abb+$", - "pattern": "^abb+$" - }) + expectJSONSchemaAnnotations( + Schema.String.pipe(Schema.pattern(/^abb+$/)), + { + type: "string", + description: "a string matching the pattern ^abb+$", + pattern: "^abb+$" + } + ) }) it("int", () => { expectJSONSchemaAnnotations(JsonNumber.pipe(Schema.int()), { - "type": "integer", - "title": "int", - "description": "an integer" + type: "integer", + title: "int", + description: "an integer" }) }) it("Trimmed", () => { const schema = Schema.Trimmed expectJSONSchemaProperty(schema, { - "$defs": { - "Trimmed": { - "title": "trimmed", - "description": "a string with no leading or trailing whitespace", - "pattern": "^\\S[\\s\\S]*\\S$|^\\S$|^$", - "type": "string" + $defs: { + Trimmed: { + title: "trimmed", + description: "a string with no leading or trailing whitespace", + pattern: "^\\S[\\s\\S]*\\S$|^\\S$|^$", + type: "string" } }, - "$ref": "#/$defs/Trimmed" + $ref: "#/$defs/Trimmed" }) }) it("Lowercased", () => { const schema = Schema.Lowercased expectJSONSchemaProperty(schema, { - "$defs": { - "Lowercased": { - "title": "lowercased", - "description": "a lowercase string", - "pattern": "^[^A-Z]*$", - "type": "string" + $defs: { + Lowercased: { + title: "lowercased", + description: "a lowercase string", + pattern: "^[^A-Z]*$", + type: "string" } }, - "$ref": "#/$defs/Lowercased" + $ref: "#/$defs/Lowercased" }) }) it("Uppercased", () => { const schema = Schema.Uppercased expectJSONSchemaProperty(schema, { - "$defs": { - "Uppercased": { - "title": "uppercased", - "description": "an uppercase string", - "pattern": "^[^a-z]*$", - "type": "string" + $defs: { + Uppercased: { + title: "uppercased", + description: "an uppercase string", + pattern: "^[^a-z]*$", + type: "string" } }, - "$ref": "#/$defs/Uppercased" + $ref: "#/$defs/Uppercased" }) }) it("Capitalized", () => { const schema = Schema.Capitalized expectJSONSchemaProperty(schema, { - "$defs": { - "Capitalized": { - "title": "capitalized", - "description": "a capitalized string", - "pattern": "^[^a-z]?.*$", - "type": "string" + $defs: { + Capitalized: { + title: "capitalized", + description: "a capitalized string", + pattern: "^[^a-z]?.*$", + type: "string" } }, - "$ref": "#/$defs/Capitalized" + $ref: "#/$defs/Capitalized" }) }) it("Uncapitalized", () => { const schema = Schema.Uncapitalized expectJSONSchemaProperty(schema, { - "$defs": { - "Uncapitalized": { - "title": "uncapitalized", - "description": "a uncapitalized string", - "pattern": "^[^A-Z]?.*$", - "type": "string" + $defs: { + Uncapitalized: { + title: "uncapitalized", + description: "a uncapitalized string", + pattern: "^[^A-Z]?.*$", + type: "string" } }, - "$ref": "#/$defs/Uncapitalized" + $ref: "#/$defs/Uncapitalized" }) }) describe("should handle merge conflicts", () => { it("minLength + minLength", () => { - expectJSONSchemaProperty(Schema.String.pipe(Schema.minLength(1), Schema.minLength(2)), { - "type": "string", - "title": "minLength(2)", - "description": "a string at least 2 character(s) long", - "minLength": 2 - }) - expectJSONSchemaProperty(Schema.String.pipe(Schema.minLength(2), Schema.minLength(1)), { - "type": "string", - "title": "minLength(1)", - "description": "a string at least 1 character(s) long", - "minLength": 1, - "allOf": [ - { "minLength": 2 } - ] - }) - expectJSONSchemaProperty(Schema.String.pipe(Schema.minLength(2), Schema.minLength(1), Schema.minLength(2)), { - "type": "string", - "title": "minLength(2)", - "description": "a string at least 2 character(s) long", - "minLength": 2 - }) + expectJSONSchemaProperty( + Schema.String.pipe(Schema.minLength(1), Schema.minLength(2)), + { + type: "string", + title: "minLength(2)", + description: "a string at least 2 character(s) long", + minLength: 2 + } + ) + expectJSONSchemaProperty( + Schema.String.pipe(Schema.minLength(2), Schema.minLength(1)), + { + type: "string", + title: "minLength(1)", + description: "a string at least 1 character(s) long", + minLength: 1, + allOf: [{ minLength: 2 }] + } + ) + expectJSONSchemaProperty( + Schema.String.pipe( + Schema.minLength(2), + Schema.minLength(1), + Schema.minLength(2) + ), + { + type: "string", + title: "minLength(2)", + description: "a string at least 2 character(s) long", + minLength: 2 + } + ) }) it("maxLength + maxLength", () => { - expectJSONSchemaProperty(Schema.String.pipe(Schema.maxLength(1), Schema.maxLength(2)), { - "type": "string", - "title": "maxLength(2)", - "description": "a string at most 2 character(s) long", - "maxLength": 2, - "allOf": [ - { "maxLength": 1 } - ] - }) - expectJSONSchemaProperty(Schema.String.pipe(Schema.maxLength(2), Schema.maxLength(1)), { - "type": "string", - "title": "maxLength(1)", - "description": "a string at most 1 character(s) long", - "maxLength": 1 - }) - expectJSONSchemaProperty(Schema.String.pipe(Schema.maxLength(1), Schema.maxLength(2), Schema.maxLength(1)), { - "type": "string", - "title": "maxLength(1)", - "description": "a string at most 1 character(s) long", - "maxLength": 1 - }) + expectJSONSchemaProperty( + Schema.String.pipe(Schema.maxLength(1), Schema.maxLength(2)), + { + type: "string", + title: "maxLength(2)", + description: "a string at most 2 character(s) long", + maxLength: 2, + allOf: [{ maxLength: 1 }] + } + ) + expectJSONSchemaProperty( + Schema.String.pipe(Schema.maxLength(2), Schema.maxLength(1)), + { + type: "string", + title: "maxLength(1)", + description: "a string at most 1 character(s) long", + maxLength: 1 + } + ) + expectJSONSchemaProperty( + Schema.String.pipe( + Schema.maxLength(1), + Schema.maxLength(2), + Schema.maxLength(1) + ), + { + type: "string", + title: "maxLength(1)", + description: "a string at most 1 character(s) long", + maxLength: 1 + } + ) }) it("pattern + pattern", () => { - expectJSONSchemaProperty(Schema.String.pipe(Schema.startsWith("a"), Schema.endsWith("c")), { - "type": "string", - "title": "endsWith(\"c\")", - "description": "a string ending with \"c\"", - "pattern": "^.*c$", - "allOf": [ - { "pattern": "^a" } - ] - }) expectJSONSchemaProperty( - Schema.String.pipe(Schema.startsWith("a"), Schema.endsWith("c"), Schema.startsWith("a")), + Schema.String.pipe(Schema.startsWith("a"), Schema.endsWith("c")), { - "type": "string", - "title": "startsWith(\"a\")", - "description": "a string starting with \"a\"", - "pattern": "^a", - "allOf": [ - { "pattern": "^.*c$" } - ] + type: "string", + title: 'endsWith("c")', + description: 'a string ending with "c"', + pattern: "^.*c$", + allOf: [{ pattern: "^a" }] } ) expectJSONSchemaProperty( - Schema.String.pipe(Schema.endsWith("c"), Schema.startsWith("a"), Schema.endsWith("c")), + Schema.String.pipe( + Schema.startsWith("a"), + Schema.endsWith("c"), + Schema.startsWith("a") + ), { - "type": "string", - "title": "endsWith(\"c\")", - "description": "a string ending with \"c\"", - "pattern": "^.*c$", - "allOf": [ - { "pattern": "^a" } - ] + type: "string", + title: 'startsWith("a")', + description: 'a string starting with "a"', + pattern: "^a", + allOf: [{ pattern: "^.*c$" }] + } + ) + expectJSONSchemaProperty( + Schema.String.pipe( + Schema.endsWith("c"), + Schema.startsWith("a"), + Schema.endsWith("c") + ), + { + type: "string", + title: 'endsWith("c")', + description: 'a string ending with "c"', + pattern: "^.*c$", + allOf: [{ pattern: "^a" }] + } + ) + }) + + it("minItems + minItems", () => { + expectJSONSchemaProperty( + Schema.Array(Schema.String).pipe( + Schema.minItems(1), + Schema.minItems(2) + ), + { + type: "array", + items: { + type: "string" + }, + description: "an array of at least 2 item(s)", + title: "minItems(2)", + minItems: 2 + } + ) + expectJSONSchemaProperty( + Schema.Array(Schema.String).pipe( + Schema.minItems(2), + Schema.minItems(1) + ), + { + type: "array", + items: { + type: "string" + }, + title: "minItems(1)", + description: "an array of at least 1 item(s)", + minItems: 1, + allOf: [{ minItems: 2 }] + } + ) + expectJSONSchemaProperty( + Schema.Array(Schema.String).pipe( + Schema.minItems(2), + Schema.minItems(1), + Schema.minItems(2) + ), + { + type: "array", + items: { + type: "string" + }, + description: "an array of at least 2 item(s)", + title: "minItems(2)", + minItems: 2 + } + ) + }) + + it("maxItems + maxItems", () => { + expectJSONSchemaProperty( + Schema.Array(Schema.String).pipe( + Schema.maxItems(1), + Schema.maxItems(2) + ), + { + type: "array", + items: { + type: "string" + }, + title: "maxItems(2)", + description: "an array of at most 2 item(s)", + maxItems: 2, + allOf: [{ maxItems: 1 }] + } + ) + expectJSONSchemaProperty( + Schema.Array(Schema.String).pipe( + Schema.maxItems(2), + Schema.maxItems(1) + ), + { + type: "array", + items: { + type: "string" + }, + title: "maxItems(1)", + description: "an array of at most 1 item(s)", + maxItems: 1 + } + ) + expectJSONSchemaProperty( + Schema.Array(Schema.String).pipe( + Schema.maxItems(1), + Schema.maxItems(2), + Schema.maxItems(1) + ), + { + type: "array", + items: { + type: "string" + }, + title: "maxItems(1)", + description: "an array of at most 1 item(s)", + maxItems: 1 + } + ) + }) + + it("minimum + minimum", () => { + expectJSONSchemaProperty( + JsonNumber.pipe( + Schema.greaterThanOrEqualTo(1), + Schema.greaterThanOrEqualTo(2) + ), + { + type: "number", + title: "greaterThanOrEqualTo(2)", + description: "a number greater than or equal to 2", + minimum: 2 + } + ) + expectJSONSchemaProperty( + JsonNumber.pipe( + Schema.greaterThanOrEqualTo(2), + Schema.greaterThanOrEqualTo(1) + ), + { + type: "number", + minimum: 1, + title: "greaterThanOrEqualTo(1)", + description: "a number greater than or equal to 1", + allOf: [{ minimum: 2 }] + } + ) + expectJSONSchemaProperty( + JsonNumber.pipe( + Schema.greaterThanOrEqualTo(2), + Schema.greaterThanOrEqualTo(1), + Schema.greaterThanOrEqualTo(2) + ), + { + type: "number", + title: "greaterThanOrEqualTo(2)", + description: "a number greater than or equal to 2", + minimum: 2 } ) }) - it("minItems + minItems", () => { - expectJSONSchemaProperty(Schema.Array(Schema.String).pipe(Schema.minItems(1), Schema.minItems(2)), { - "type": "array", - "items": { - "type": "string" - }, - "description": "an array of at least 2 item(s)", - "title": "minItems(2)", - "minItems": 2 - }) - expectJSONSchemaProperty(Schema.Array(Schema.String).pipe(Schema.minItems(2), Schema.minItems(1)), { - "type": "array", - "items": { - "type": "string" - }, - "title": "minItems(1)", - "description": "an array of at least 1 item(s)", - "minItems": 1, - "allOf": [ - { "minItems": 2 } - ] - }) + it("maximum + maximum", () => { expectJSONSchemaProperty( - Schema.Array(Schema.String).pipe(Schema.minItems(2), Schema.minItems(1), Schema.minItems(2)), + JsonNumber.pipe( + Schema.lessThanOrEqualTo(1), + Schema.lessThanOrEqualTo(2) + ), { - "type": "array", - "items": { - "type": "string" - }, - "description": "an array of at least 2 item(s)", - "title": "minItems(2)", - "minItems": 2 + type: "number", + title: "lessThanOrEqualTo(2)", + description: "a number less than or equal to 2", + maximum: 2, + allOf: [{ maximum: 1 }] } ) - }) - - it("maxItems + maxItems", () => { - expectJSONSchemaProperty(Schema.Array(Schema.String).pipe(Schema.maxItems(1), Schema.maxItems(2)), { - "type": "array", - "items": { - "type": "string" - }, - "title": "maxItems(2)", - "description": "an array of at most 2 item(s)", - "maxItems": 2, - "allOf": [ - { "maxItems": 1 } - ] - }) - expectJSONSchemaProperty(Schema.Array(Schema.String).pipe(Schema.maxItems(2), Schema.maxItems(1)), { - "type": "array", - "items": { - "type": "string" - }, - "title": "maxItems(1)", - "description": "an array of at most 1 item(s)", - "maxItems": 1 - }) expectJSONSchemaProperty( - Schema.Array(Schema.String).pipe(Schema.maxItems(1), Schema.maxItems(2), Schema.maxItems(1)), + JsonNumber.pipe( + Schema.lessThanOrEqualTo(2), + Schema.lessThanOrEqualTo(1) + ), { - "type": "array", - "items": { - "type": "string" - }, - "title": "maxItems(1)", - "description": "an array of at most 1 item(s)", - "maxItems": 1 + type: "number", + title: "lessThanOrEqualTo(1)", + description: "a number less than or equal to 1", + maximum: 1 } ) - }) - - it("minimum + minimum", () => { - expectJSONSchemaProperty(JsonNumber.pipe(Schema.greaterThanOrEqualTo(1), Schema.greaterThanOrEqualTo(2)), { - "type": "number", - "title": "greaterThanOrEqualTo(2)", - "description": "a number greater than or equal to 2", - "minimum": 2 - }) - expectJSONSchemaProperty(JsonNumber.pipe(Schema.greaterThanOrEqualTo(2), Schema.greaterThanOrEqualTo(1)), { - "type": "number", - "minimum": 1, - "title": "greaterThanOrEqualTo(1)", - "description": "a number greater than or equal to 1", - "allOf": [ - { "minimum": 2 } - ] - }) expectJSONSchemaProperty( JsonNumber.pipe( - Schema.greaterThanOrEqualTo(2), - Schema.greaterThanOrEqualTo(1), - Schema.greaterThanOrEqualTo(2) + Schema.lessThanOrEqualTo(1), + Schema.lessThanOrEqualTo(2), + Schema.lessThanOrEqualTo(1) ), { - "type": "number", - "title": "greaterThanOrEqualTo(2)", - "description": "a number greater than or equal to 2", - "minimum": 2 + type: "number", + title: "lessThanOrEqualTo(1)", + description: "a number less than or equal to 1", + maximum: 1 } ) }) - it("maximum + maximum", () => { - expectJSONSchemaProperty(JsonNumber.pipe(Schema.lessThanOrEqualTo(1), Schema.lessThanOrEqualTo(2)), { - "type": "number", - "title": "lessThanOrEqualTo(2)", - "description": "a number less than or equal to 2", - "maximum": 2, - "allOf": [ - { "maximum": 1 } - ] - }) - expectJSONSchemaProperty(JsonNumber.pipe(Schema.lessThanOrEqualTo(2), Schema.lessThanOrEqualTo(1)), { - "type": "number", - "title": "lessThanOrEqualTo(1)", - "description": "a number less than or equal to 1", - "maximum": 1 - }) + it("exclusiveMinimum + exclusiveMinimum", () => { expectJSONSchemaProperty( - JsonNumber.pipe(Schema.lessThanOrEqualTo(1), Schema.lessThanOrEqualTo(2), Schema.lessThanOrEqualTo(1)), + JsonNumber.pipe(Schema.greaterThan(1), Schema.greaterThan(2)), { - "type": "number", - "title": "lessThanOrEqualTo(1)", - "description": "a number less than or equal to 1", - "maximum": 1 + type: "number", + title: "greaterThan(2)", + description: "a number greater than 2", + exclusiveMinimum: 2 + } + ) + expectJSONSchemaProperty( + JsonNumber.pipe(Schema.greaterThan(2), Schema.greaterThan(1)), + { + type: "number", + exclusiveMinimum: 1, + title: "greaterThan(1)", + description: "a number greater than 1", + allOf: [{ exclusiveMinimum: 2 }] } ) - }) - - it("exclusiveMinimum + exclusiveMinimum", () => { - expectJSONSchemaProperty(JsonNumber.pipe(Schema.greaterThan(1), Schema.greaterThan(2)), { - "type": "number", - "title": "greaterThan(2)", - "description": "a number greater than 2", - "exclusiveMinimum": 2 - }) - expectJSONSchemaProperty(JsonNumber.pipe(Schema.greaterThan(2), Schema.greaterThan(1)), { - "type": "number", - "exclusiveMinimum": 1, - "title": "greaterThan(1)", - "description": "a number greater than 1", - "allOf": [ - { "exclusiveMinimum": 2 } - ] - }) expectJSONSchemaProperty( JsonNumber.pipe( Schema.greaterThan(2), @@ -1619,73 +1851,86 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` Schema.greaterThan(2) ), { - "type": "number", - "title": "greaterThan(2)", - "description": "a number greater than 2", - "exclusiveMinimum": 2 + type: "number", + title: "greaterThan(2)", + description: "a number greater than 2", + exclusiveMinimum: 2 } ) }) it("exclusiveMaximum + exclusiveMaximum", () => { - expectJSONSchemaProperty(JsonNumber.pipe(Schema.lessThan(1), Schema.lessThan(2)), { - "type": "number", - "title": "lessThan(2)", - "description": "a number less than 2", - "exclusiveMaximum": 2, - "allOf": [ - { "exclusiveMaximum": 1 } - ] - }) - expectJSONSchemaProperty(JsonNumber.pipe(Schema.lessThan(2), Schema.lessThan(1)), { - "type": "number", - "title": "lessThan(1)", - "description": "a number less than 1", - "exclusiveMaximum": 1 - }) expectJSONSchemaProperty( - JsonNumber.pipe(Schema.lessThan(1), Schema.lessThan(2), Schema.lessThan(1)), + JsonNumber.pipe(Schema.lessThan(1), Schema.lessThan(2)), { - "type": "number", - "title": "lessThan(1)", - "description": "a number less than 1", - "exclusiveMaximum": 1 + type: "number", + title: "lessThan(2)", + description: "a number less than 2", + exclusiveMaximum: 2, + allOf: [{ exclusiveMaximum: 1 }] + } + ) + expectJSONSchemaProperty( + JsonNumber.pipe(Schema.lessThan(2), Schema.lessThan(1)), + { + type: "number", + title: "lessThan(1)", + description: "a number less than 1", + exclusiveMaximum: 1 + } + ) + expectJSONSchemaProperty( + JsonNumber.pipe( + Schema.lessThan(1), + Schema.lessThan(2), + Schema.lessThan(1) + ), + { + type: "number", + title: "lessThan(1)", + description: "a number less than 1", + exclusiveMaximum: 1 } ) }) it("multipleOf + multipleOf", () => { - expectJSONSchema(JsonNumber.pipe(Schema.multipleOf(2), Schema.multipleOf(3)), { - "type": "number", - "title": "multipleOf(3)", - "description": "a number divisible by 3", - "multipleOf": 3, - "allOf": [ - { "multipleOf": 2 } - ] - }) expectJSONSchema( - JsonNumber.pipe(Schema.multipleOf(2), Schema.multipleOf(3), Schema.multipleOf(3)), + JsonNumber.pipe(Schema.multipleOf(2), Schema.multipleOf(3)), { - "type": "number", - "title": "multipleOf(3)", - "description": "a number divisible by 3", - "multipleOf": 3, - "allOf": [ - { "multipleOf": 2 } - ] + type: "number", + title: "multipleOf(3)", + description: "a number divisible by 3", + multipleOf: 3, + allOf: [{ multipleOf: 2 }] } ) expectJSONSchema( - JsonNumber.pipe(Schema.multipleOf(3), Schema.multipleOf(2), Schema.multipleOf(3)), + JsonNumber.pipe( + Schema.multipleOf(2), + Schema.multipleOf(3), + Schema.multipleOf(3) + ), { - "type": "number", - "title": "multipleOf(3)", - "description": "a number divisible by 3", - "multipleOf": 3, - "allOf": [ - { "multipleOf": 2 } - ] + type: "number", + title: "multipleOf(3)", + description: "a number divisible by 3", + multipleOf: 3, + allOf: [{ multipleOf: 2 }] + } + ) + expectJSONSchema( + JsonNumber.pipe( + Schema.multipleOf(3), + Schema.multipleOf(2), + Schema.multipleOf(3) + ), + { + type: "number", + title: "multipleOf(3)", + description: "a number divisible by 3", + multipleOf: 3, + allOf: [{ multipleOf: 2 }] } ) }) @@ -1696,8 +1941,8 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` it("empty tuple", () => { const schema = Schema.Tuple() const jsonSchema: Root = { - "type": "array", - "maxItems": 0 + type: "array", + maxItems: 0 } expectJSONSchemaAnnotations(schema, jsonSchema) const validate = getAjvValidate(jsonSchema) @@ -1708,12 +1953,14 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` it("element", () => { const schema = Schema.Tuple(JsonNumber) const jsonSchema: Root = { - "type": "array", - "items": [{ - "type": "number" - }], - "minItems": 1, - "additionalItems": false + type: "array", + items: [ + { + type: "number" + } + ], + minItems: 1, + additionalItems: false } expectJSONSchemaAnnotations(schema, jsonSchema) const validate = getAjvValidate(jsonSchema) @@ -1727,13 +1974,15 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` expectJSONSchemaAnnotations( Schema.Tuple(JsonNumber.annotations({ description: "inner" })), { - "type": "array", - "items": [{ - "type": "number", - "description": "inner" - }], - "minItems": 1, - "additionalItems": false + type: "array", + items: [ + { + type: "number", + description: "inner" + } + ], + minItems: 1, + additionalItems: false } ) }) @@ -1741,16 +1990,20 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` it("element + outer annotations should override inner annotations", () => { expectJSONSchemaAnnotations( Schema.Tuple( - Schema.element(JsonNumber.annotations({ description: "inner" })).annotations({ description: "outer" }) + Schema.element( + JsonNumber.annotations({ description: "inner" }) + ).annotations({ description: "outer" }) ), { - "type": "array", - "items": [{ - "type": "number", - "description": "outer" - }], - "minItems": 1, - "additionalItems": false + type: "array", + items: [ + { + type: "number", + description: "outer" + } + ], + minItems: 1, + additionalItems: false } ) }) @@ -1758,14 +2011,14 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` it("optionalElement", () => { const schema = Schema.Tuple(Schema.optionalElement(JsonNumber)) const jsonSchema: Root = { - "type": "array", - "minItems": 0, - "items": [ + type: "array", + minItems: 0, + items: [ { - "type": "number" + type: "number" } ], - "additionalItems": false + additionalItems: false } expectJSONSchemaAnnotations(schema, jsonSchema) const validate = getAjvValidate(jsonSchema) @@ -1777,17 +2030,21 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` it("optionalElement + inner annotations", () => { expectJSONSchemaAnnotations( - Schema.Tuple(Schema.optionalElement(JsonNumber).annotations({ description: "inner" })), + Schema.Tuple( + Schema.optionalElement(JsonNumber).annotations({ + description: "inner" + }) + ), { - "type": "array", - "minItems": 0, - "items": [ + type: "array", + minItems: 0, + items: [ { - "type": "number", - "description": "inner" + type: "number", + description: "inner" } ], - "additionalItems": false + additionalItems: false } ) }) @@ -1795,43 +2052,49 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` it("optionalElement + outer annotations should override inner annotations", () => { expectJSONSchemaAnnotations( Schema.Tuple( - Schema.optionalElement(JsonNumber).annotations({ description: "inner" }).annotations({ description: "outer" }) + Schema.optionalElement(JsonNumber) + .annotations({ description: "inner" }) + .annotations({ description: "outer" }) ), { - "type": "array", - "minItems": 0, - "items": [ + type: "array", + minItems: 0, + items: [ { - "type": "number", - "description": "outer" + type: "number", + description: "outer" } ], - "additionalItems": false + additionalItems: false } ) }) it("element + optionalElement", () => { const schema = Schema.Tuple( - Schema.element(Schema.String.annotations({ description: "inner" })).annotations({ description: "outer" }), - Schema.optionalElement(JsonNumber.annotations({ description: "inner?" })).annotations({ + Schema.element( + Schema.String.annotations({ description: "inner" }) + ).annotations({ description: "outer" }), + Schema.optionalElement( + JsonNumber.annotations({ description: "inner?" }) + ).annotations({ description: "outer?" }) ) const jsonSchema: Root = { - "type": "array", - "minItems": 1, - "items": [ + type: "array", + minItems: 1, + items: [ { - "type": "string", - "description": "outer" + type: "string", + description: "outer" }, { - "type": "number", - "description": "outer?" + type: "number", + description: "outer?" } ], - "additionalItems": false + additionalItems: false } expectJSONSchemaAnnotations(schema, jsonSchema) const validate = getAjvValidate(jsonSchema) @@ -1845,9 +2108,9 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` it("rest", () => { const schema = Schema.Array(JsonNumber) const jsonSchema: Root = { - "type": "array", - "items": { - "type": "number" + type: "array", + items: { + type: "number" } } expectJSONSchemaAnnotations(schema, jsonSchema) @@ -1861,13 +2124,16 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` }) it("rest + inner annotations", () => { - expectJSONSchemaAnnotations(Schema.Array(JsonNumber.annotations({ description: "inner" })), { - "type": "array", - "items": { - "type": "number", - "description": "inner" + expectJSONSchemaAnnotations( + Schema.Array(JsonNumber.annotations({ description: "inner" })), + { + type: "array", + items: { + type: "number", + description: "inner" + } } - }) + ) }) it("optionalElement + rest + inner annotations", () => { @@ -1876,16 +2142,16 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` Schema.element(JsonNumber.annotations({ description: "inner" })) ) const jsonSchema: Root = { - "type": "array", - "minItems": 0, - "items": [ + type: "array", + minItems: 0, + items: [ { - "type": "string" + type: "string" } ], - "additionalItems": { - "type": "number", - "description": "inner" + additionalItems: { + type: "number", + description: "inner" } } expectJSONSchemaAnnotations(schema, jsonSchema) @@ -1902,19 +2168,21 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` expectJSONSchemaAnnotations( Schema.Tuple( [Schema.optionalElement(Schema.String)], - Schema.element(JsonNumber.annotations({ description: "inner" })).annotations({ description: "outer" }) + Schema.element( + JsonNumber.annotations({ description: "inner" }) + ).annotations({ description: "outer" }) ), { - "type": "array", - "minItems": 0, - "items": [ + type: "array", + minItems: 0, + items: [ { - "type": "string" + type: "string" } ], - "additionalItems": { - "type": "number", - "description": "outer" + additionalItems: { + type: "number", + description: "outer" } } ) @@ -1923,13 +2191,15 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` it("element + rest", () => { const schema = Schema.Tuple([Schema.String], JsonNumber) const jsonSchema: Root = { - "type": "array", - "items": [{ - "type": "string" - }], - "minItems": 1, - "additionalItems": { - "type": "number" + type: "array", + items: [ + { + type: "string" + } + ], + minItems: 1, + additionalItems: { + type: "number" } } expectJSONSchemaAnnotations(schema, jsonSchema) @@ -1944,14 +2214,11 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` }) it("NonEmptyArray", () => { - expectJSONSchemaProperty( - Schema.NonEmptyArray(Schema.String), - { - type: "array", - minItems: 1, - items: { type: "string" } - } - ) + expectJSONSchemaProperty(Schema.NonEmptyArray(Schema.String), { + type: "array", + minItems: 1, + items: { type: "string" } + }) }) }) @@ -1962,13 +2229,13 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` b: JsonNumber }) const jsonSchema: Root = { - "type": "object", - "properties": { - "a": { "type": "string" }, - "b": { "type": "number" } + type: "object", + properties: { + a: { type: "string" }, + b: { type: "number" } }, - "required": ["a", "b"], - "additionalProperties": false + required: ["a", "b"], + additionalProperties: false } expectJSONSchemaAnnotations(schema, jsonSchema) const validate = getAjvValidate(jsonSchema) @@ -1985,15 +2252,15 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` a: Schema.String.annotations({ description: "inner" }) }), { - "type": "object", - "properties": { - "a": { - "type": "string", - "description": "inner" + type: "object", + properties: { + a: { + type: "string", + description: "inner" } }, - "required": ["a"], - "additionalProperties": false + required: ["a"], + additionalProperties: false } ) }) @@ -2001,40 +2268,43 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` it("field + outer annotation should override inner annotation", () => { expectJSONSchemaAnnotations( Schema.Struct({ - a: Schema.propertySignature(Schema.String.annotations({ description: "inner" })).annotations({ + a: Schema.propertySignature( + Schema.String.annotations({ description: "inner" }) + ).annotations({ description: "outer" }) }), { - "type": "object", - "properties": { - "a": { - "type": "string", - "description": "outer" + type: "object", + properties: { + a: { + type: "string", + description: "outer" } }, - "required": ["a"], - "additionalProperties": false + required: ["a"], + additionalProperties: false } ) }) it("Struct + Record", () => { - const schema = Schema.Struct({ - a: Schema.String - }, Schema.Record({ key: Schema.String, value: Schema.String })) + const schema = Schema.Struct( + { + a: Schema.String + }, + Schema.Record({ key: Schema.String, value: Schema.String }) + ) const jsonSchema: Root = { - "type": "object", - "required": [ - "a" - ], - "properties": { - "a": { - "type": "string" + type: "object", + required: ["a"], + properties: { + a: { + type: "string" } }, - "additionalProperties": { - "type": "string" + additionalProperties: { + type: "string" } } expectJSONSchemaAnnotations(schema, jsonSchema) @@ -2053,17 +2323,17 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` b: Schema.optionalWith(JsonNumber, { exact: true }) }) const jsonSchema: Root = { - "type": "object", - "properties": { - "a": { - "type": "string" + type: "object", + properties: { + a: { + type: "string" }, - "b": { - "type": "number" + b: { + type: "number" } }, - "required": ["a"], - "additionalProperties": false + required: ["a"], + additionalProperties: false } expectJSONSchemaAnnotations(schema, jsonSchema) const validate = getAjvValidate(jsonSchema) @@ -2077,18 +2347,21 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` it("exact optional field + inner annotation", () => { expectJSONSchemaAnnotations( Schema.Struct({ - a: Schema.optionalWith(Schema.String.annotations({ description: "inner" }), { exact: true }) + a: Schema.optionalWith( + Schema.String.annotations({ description: "inner" }), + { exact: true } + ) }), { - "type": "object", - "properties": { - "a": { - "type": "string", - "description": "inner" + type: "object", + properties: { + a: { + type: "string", + description: "inner" } }, - "required": [], - "additionalProperties": false + required: [], + additionalProperties: false } ) }) @@ -2096,20 +2369,23 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` it("exact optional field + outer annotation should override inner annotations", () => { expectJSONSchemaAnnotations( Schema.Struct({ - a: Schema.optionalWith(Schema.String.annotations({ description: "inner" }), { exact: true }).annotations({ + a: Schema.optionalWith( + Schema.String.annotations({ description: "inner" }), + { exact: true } + ).annotations({ description: "outer" }) }), { - "type": "object", - "properties": { - "a": { - "type": "string", - "description": "outer" + type: "object", + properties: { + a: { + type: "string", + description: "outer" } }, - "required": [], - "additionalProperties": false + required: [], + additionalProperties: false } ) }) @@ -2118,72 +2394,99 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` describe("Record", () => { it("Record(refinement, number)", () => { expectJSONSchemaAnnotations( - Schema.Record({ key: Schema.String.pipe(Schema.minLength(1)), value: JsonNumber }), + Schema.Record({ + key: Schema.String.pipe(Schema.minLength(1)), + value: JsonNumber + }), { - "type": "object", - "required": [], - "properties": {}, - "patternProperties": { + type: "object", + required: [], + properties: {}, + patternProperties: { "": { - "type": "number" + type: "number" } }, - "propertyNames": { - "type": "string", - "title": "minLength(1)", - "description": "a string at least 1 character(s) long", - "minLength": 1 + propertyNames: { + type: "string", + title: "minLength(1)", + description: "a string at least 1 character(s) long", + minLength: 1 } } ) }) it("Record(string, number)", () => { - expectJSONSchemaAnnotations(Schema.Record({ key: Schema.String, value: JsonNumber }), { - "type": "object", - "properties": {}, - "required": [], - "additionalProperties": { - "type": "number" + expectJSONSchemaAnnotations( + Schema.Record({ key: Schema.String, value: JsonNumber }), + { + type: "object", + properties: {}, + required: [], + additionalProperties: { + type: "number" + } } - }) + ) + }) + + it("Record(string, never)", () => { + const jsonSchema = expectJSONSchema( + Schema.Record({ key: Schema.String, value: Schema.Never }), + { + type: "object", + properties: {}, + required: [], + additionalProperties: false + } + ) + const validate = getAjvValidate(jsonSchema) + assertTrue(validate({})) + assertFalse(validate({ a: null })) }) it("Record('a' | 'b', number)", () => { expectJSONSchemaAnnotations( - Schema.Record( - { key: Schema.Union(Schema.Literal("a"), Schema.Literal("b")), value: JsonNumber } - ), + Schema.Record({ + key: Schema.Union(Schema.Literal("a"), Schema.Literal("b")), + value: JsonNumber + }), { - "type": "object", - "properties": { - "a": { - "type": "number" + type: "object", + properties: { + a: { + type: "number" }, - "b": { - "type": "number" + b: { + type: "number" } }, - "required": ["a", "b"], - "additionalProperties": false + required: ["a", "b"], + additionalProperties: false } ) }) it("Record(${string}-${string}, number)", () => { - const schema = Schema.Record( - { key: Schema.TemplateLiteral(Schema.String, Schema.Literal("-"), Schema.String), value: JsonNumber } - ) + const schema = Schema.Record({ + key: Schema.TemplateLiteral( + Schema.String, + Schema.Literal("-"), + Schema.String + ), + value: JsonNumber + }) const jsonSchema: Root = { - "type": "object", - "required": [], - "properties": {}, - "patternProperties": { - "": { "type": "number" } + type: "object", + required: [], + properties: {}, + patternProperties: { + "": { type: "number" } }, - "propertyNames": { - "pattern": "^[\\s\\S]*?-[\\s\\S]*?$", - "type": "string" + propertyNames: { + pattern: "^[\\s\\S]*?-[\\s\\S]*?$", + type: "string" } } expectJSONSchemaAnnotations(schema, jsonSchema) @@ -2198,22 +2501,23 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` }) it("Record(pattern, number)", () => { - const schema = Schema.Record( - { key: Schema.String.pipe(Schema.pattern(new RegExp("^.*-.*$"))), value: JsonNumber } - ) + const schema = Schema.Record({ + key: Schema.String.pipe(Schema.pattern(new RegExp("^.*-.*$"))), + value: JsonNumber + }) const jsonSchema: Root = { - "type": "object", - "required": [], - "properties": {}, - "patternProperties": { + type: "object", + required: [], + properties: {}, + patternProperties: { "": { - "type": "number" + type: "number" } }, - "propertyNames": { - "description": "a string matching the pattern ^.*-.*$", - "pattern": "^.*-.*$", - "type": "string" + propertyNames: { + description: "a string matching the pattern ^.*-.*$", + pattern: "^.*-.*$", + type: "string" } } expectJSONSchemaAnnotations(schema, jsonSchema) @@ -2230,31 +2534,39 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` it("Record(SymbolFromSelf & annotation, number)", () => { expectJSONSchemaAnnotations( Schema.Record({ - key: Schema.SymbolFromSelf.annotations({ jsonSchema: { "type": "string" } }), + key: Schema.SymbolFromSelf.annotations({ + jsonSchema: { type: "string" } + }), value: JsonNumber }), { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "required": [], - "properties": {}, - "additionalProperties": { - "type": "number" + $schema: "http://json-schema.org/draft-07/schema#", + type: "object", + required: [], + properties: {}, + additionalProperties: { + type: "number" }, - "propertyNames": { - "type": "string" + propertyNames: { + type: "string" } } ) }) it("Record(string, UndefinedOr(number))", () => { - expectJSONSchemaAnnotations(Schema.Record({ key: Schema.String, value: Schema.UndefinedOr(JsonNumber) }), { - "type": "object", - "properties": {}, - "required": [], - "additionalProperties": { "type": "number" } - }) + expectJSONSchemaAnnotations( + Schema.Record({ + key: Schema.String, + value: Schema.UndefinedOr(JsonNumber) + }), + { + type: "object", + properties: {}, + required: [], + additionalProperties: { type: "number" } + } + ) }) it("partial(Struct + Record(string, number))", () => { @@ -2269,16 +2581,16 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` ) expectJSONSchemaAnnotations(schema, { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "required": [], - "properties": { - "foo": { - "type": "number" + $schema: "http://json-schema.org/draft-07/schema#", + type: "object", + required: [], + properties: { + foo: { + type: "number" } }, - "additionalProperties": { - "type": "number" + additionalProperties: { + type: "number" } }) }) @@ -2287,46 +2599,51 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` describe("Union", () => { it("should ignore never members", () => { expectJSONSchema(Schema.Union(Schema.String, Schema.Never), { - "type": "string" - }) - expectJSONSchema(Schema.Union(Schema.String, Schema.Union(Schema.Never, Schema.Never)), { - "type": "string" + type: "string" }) + expectJSONSchema( + Schema.Union(Schema.String, Schema.Union(Schema.Never, Schema.Never)), + { + type: "string" + } + ) }) it("string | JsonNumber", () => { expectJSONSchemaAnnotations(Schema.Union(Schema.String, JsonNumber), { - "anyOf": [ - { "type": "string" }, - { "type": "number" } - ] + anyOf: [{ type: "string" }, { type: "number" }] }) }) describe("Union including literals", () => { it(`1 | 2`, () => { - expectJSONSchemaAnnotations(Schema.Union(Schema.Literal(1), Schema.Literal(2)), { - "type": "number", - "enum": [1, 2] - }) + expectJSONSchemaAnnotations( + Schema.Union(Schema.Literal(1), Schema.Literal(2)), + { + type: "number", + enum: [1, 2] + } + ) }) it(`1(with description) | 2`, () => { expectJSONSchemaAnnotations( Schema.Union( - Schema.Literal(1).annotations({ description: "43d87cd1-df64-457f-8119-0401ecd1399e" }), + Schema.Literal(1).annotations({ + description: "43d87cd1-df64-457f-8119-0401ecd1399e" + }), Schema.Literal(2) ), { - "anyOf": [ + anyOf: [ { - "type": "number", - "enum": [1], - "description": "43d87cd1-df64-457f-8119-0401ecd1399e" + type: "number", + enum: [1], + description: "43d87cd1-df64-457f-8119-0401ecd1399e" }, { - "type": "number", - "enum": [2] + type: "number", + enum: [2] } ] } @@ -2337,18 +2654,20 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` expectJSONSchemaAnnotations( Schema.Union( Schema.Literal(1), - Schema.Literal(2).annotations({ description: "28e1ba58-7c13-4667-88cb-2baa1ac31a0f" }) + Schema.Literal(2).annotations({ + description: "28e1ba58-7c13-4667-88cb-2baa1ac31a0f" + }) ), { - "anyOf": [ + anyOf: [ { - "type": "number", - "enum": [1] + type: "number", + enum: [1] }, { - "type": "number", - "enum": [2], - "description": "28e1ba58-7c13-4667-88cb-2baa1ac31a0f" + type: "number", + enum: [2], + description: "28e1ba58-7c13-4667-88cb-2baa1ac31a0f" } ] } @@ -2356,43 +2675,51 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` }) it(`1 | 2 | string`, () => { - expectJSONSchemaAnnotations(Schema.Union(Schema.Literal(1), Schema.Literal(2), Schema.String), { - "anyOf": [ - { - "type": "number", - "enum": [1, 2] - }, - { "type": "string" } - ] - }) + expectJSONSchemaAnnotations( + Schema.Union(Schema.Literal(1), Schema.Literal(2), Schema.String), + { + anyOf: [ + { + type: "number", + enum: [1, 2] + }, + { type: "string" } + ] + } + ) }) it(`(1 | 2) | string`, () => { - expectJSONSchemaAnnotations(Schema.Union(Schema.Literal(1, 2), Schema.String), { - "anyOf": [ - { - "type": "number", - "enum": [1, 2] - }, - { "type": "string" } - ] - }) + expectJSONSchemaAnnotations( + Schema.Union(Schema.Literal(1, 2), Schema.String), + { + anyOf: [ + { + type: "number", + enum: [1, 2] + }, + { type: "string" } + ] + } + ) }) it(`(1 | 2)(with description) | string`, () => { expectJSONSchemaAnnotations( Schema.Union( - Schema.Literal(1, 2).annotations({ description: "d0121d0e-8b56-4a2e-9963-47a0965d6a3c" }), + Schema.Literal(1, 2).annotations({ + description: "d0121d0e-8b56-4a2e-9963-47a0965d6a3c" + }), Schema.String ), { - "anyOf": [ + anyOf: [ { - "type": "number", - "description": "d0121d0e-8b56-4a2e-9963-47a0965d6a3c", - "enum": [1, 2] + type: "number", + description: "d0121d0e-8b56-4a2e-9963-47a0965d6a3c", + enum: [1, 2] }, - { "type": "string" } + { type: "string" } ] } ) @@ -2401,22 +2728,24 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` it(`(1 | 2)(with description) | 3 | string`, () => { expectJSONSchemaAnnotations( Schema.Union( - Schema.Literal(1, 2).annotations({ description: "eca4431f-c97c-454f-8167-6c2e81430c6b" }), + Schema.Literal(1, 2).annotations({ + description: "eca4431f-c97c-454f-8167-6c2e81430c6b" + }), Schema.Literal(3), Schema.String ), { - "anyOf": [ + anyOf: [ { - "type": "number", - "description": "eca4431f-c97c-454f-8167-6c2e81430c6b", - "enum": [1, 2] + type: "number", + description: "eca4431f-c97c-454f-8167-6c2e81430c6b", + enum: [1, 2] }, { - "type": "number", - "enum": [3] + type: "number", + enum: [3] }, - { "type": "string" } + { type: "string" } ] } ) @@ -2425,22 +2754,24 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` it(`1(with description) | 2 | string`, () => { expectJSONSchemaAnnotations( Schema.Union( - Schema.Literal(1).annotations({ description: "867c07f5-5710-477c-8296-239694e86562" }), + Schema.Literal(1).annotations({ + description: "867c07f5-5710-477c-8296-239694e86562" + }), Schema.Literal(2), Schema.String ), { - "anyOf": [ + anyOf: [ { - "type": "number", - "description": "867c07f5-5710-477c-8296-239694e86562", - "enum": [1] + type: "number", + description: "867c07f5-5710-477c-8296-239694e86562", + enum: [1] }, { - "type": "number", - "enum": [2] + type: "number", + enum: [2] }, - { "type": "string" } + { type: "string" } ] } ) @@ -2450,68 +2781,78 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` expectJSONSchemaAnnotations( Schema.Union( Schema.Literal(1), - Schema.Literal(2).annotations({ description: "4e49a840-5fb8-43f6-916f-565cbf532db4" }), + Schema.Literal(2).annotations({ + description: "4e49a840-5fb8-43f6-916f-565cbf532db4" + }), Schema.String ), { - "anyOf": [ + anyOf: [ { - "type": "number", - "enum": [1] + type: "number", + enum: [1] }, { - "type": "number", - "description": "4e49a840-5fb8-43f6-916f-565cbf532db4", - "enum": [2] + type: "number", + description: "4e49a840-5fb8-43f6-916f-565cbf532db4", + enum: [2] }, - { "type": "string" } + { type: "string" } ] } ) }) it(`string | 1 | 2 `, () => { - expectJSONSchemaAnnotations(Schema.Union(Schema.String, Schema.Literal(1), Schema.Literal(2)), { - "anyOf": [ - { "type": "string" }, - { - "type": "number", - "enum": [1, 2] - } - ] - }) + expectJSONSchemaAnnotations( + Schema.Union(Schema.String, Schema.Literal(1), Schema.Literal(2)), + { + anyOf: [ + { type: "string" }, + { + type: "number", + enum: [1, 2] + } + ] + } + ) }) it(`string | (1 | 2) `, () => { - expectJSONSchemaAnnotations(Schema.Union(Schema.String, Schema.Literal(1, 2)), { - "anyOf": [ - { "type": "string" }, - { - "type": "number", - "enum": [1, 2] - } - ] - }) + expectJSONSchemaAnnotations( + Schema.Union(Schema.String, Schema.Literal(1, 2)), + { + anyOf: [ + { type: "string" }, + { + type: "number", + enum: [1, 2] + } + ] + } + ) }) it(`string | 1(with description) | 2`, () => { expectJSONSchemaAnnotations( Schema.Union( Schema.String, - Schema.Literal(1).annotations({ description: "26521e57-cfb6-4563-abe2-2fe920398e16" }), + Schema.Literal(1).annotations({ + description: "26521e57-cfb6-4563-abe2-2fe920398e16" + }), Schema.Literal(2) ), { - "anyOf": [ - { "type": "string" }, + anyOf: [ + { type: "string" }, { - "type": "number", - "description": "26521e57-cfb6-4563-abe2-2fe920398e16", - "enum": [1] + type: "number", + description: "26521e57-cfb6-4563-abe2-2fe920398e16", + enum: [1] }, { - "type": "number", - "enum": [2] + type: "number", + enum: [2] } ] } @@ -2523,19 +2864,21 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` Schema.Union( Schema.String, Schema.Literal(1), - Schema.Literal(2).annotations({ description: "c4fb2a01-68ff-43d2-81d0-de799c06e9c0" }) + Schema.Literal(2).annotations({ + description: "c4fb2a01-68ff-43d2-81d0-de799c06e9c0" + }) ), { - "anyOf": [ - { "type": "string" }, + anyOf: [ + { type: "string" }, { - "type": "number", - "enum": [1] + type: "number", + enum: [1] }, { - "type": "number", - "description": "c4fb2a01-68ff-43d2-81d0-de799c06e9c0", - "enum": [2] + type: "number", + description: "c4fb2a01-68ff-43d2-81d0-de799c06e9c0", + enum: [2] } ] } @@ -2547,44 +2890,38 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` describe("Transformation", () => { it("NumberFromString", () => { expectJSONSchemaProperty(Schema.NumberFromString, { - "$defs": { - "NumberFromString": { - "type": "string", - "description": "a string to be decoded into a number" + $defs: { + NumberFromString: { + type: "string", + description: "a string to be decoded into a number" } }, - "$ref": "#/$defs/NumberFromString" + $ref: "#/$defs/NumberFromString" }) }) it("DateFromString", () => { - expectJSONSchemaProperty( - Schema.DateFromString, - { - "$defs": { - "DateFromString": { - "type": "string", - "description": "a string to be decoded into a Date" - } - }, - "$ref": "#/$defs/DateFromString" - } - ) + expectJSONSchemaProperty(Schema.DateFromString, { + $defs: { + DateFromString: { + type: "string", + description: "a string to be decoded into a Date" + } + }, + $ref: "#/$defs/DateFromString" + }) }) it("Date", () => { - expectJSONSchemaProperty( - Schema.Date, - { - "$defs": { - "Date": { - "type": "string", - "description": "a string to be decoded into a Date" - } - }, - "$ref": "#/$defs/Date" - } - ) + expectJSONSchemaProperty(Schema.Date, { + $defs: { + Date: { + type: "string", + description: "a string to be decoded into a Date" + } + }, + $ref: "#/$defs/Date" + }) }) it("OptionFromNullOr", () => { @@ -2593,27 +2930,22 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` a: Schema.OptionFromNullOr(Schema.NonEmptyString) }), { - "$defs": { - "NonEmptyString": { - "type": "string", - "title": "nonEmptyString", - "description": "a non empty string", - "minLength": 1 + $defs: { + NonEmptyString: { + type: "string", + title: "nonEmptyString", + description: "a non empty string", + minLength: 1 } }, - "type": "object", - "required": [ - "a" - ], - "properties": { - "a": { - "anyOf": [ - { "$ref": "#/$defs/NonEmptyString" }, - { "type": "null" } - ] + type: "object", + required: ["a"], + properties: { + a: { + anyOf: [{ $ref: "#/$defs/NonEmptyString" }, { type: "null" }] } }, - "additionalProperties": false + additionalProperties: false } ) }) @@ -2625,26 +2957,26 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` value: Schema.NumberFromString }), { - "$defs": { - "NumberFromString": { - "type": "string", - "description": "a string to be decoded into a number" + $defs: { + NumberFromString: { + type: "string", + description: "a string to be decoded into a number" } }, - "type": "object", - "description": "a record to be decoded into a ReadonlyMap", - "required": [], - "properties": {}, - "patternProperties": { + type: "object", + description: "a record to be decoded into a ReadonlyMap", + required: [], + properties: {}, + patternProperties: { "": { - "$ref": "#/$defs/NumberFromString" + $ref: "#/$defs/NumberFromString" } }, - "propertyNames": { - "title": "minLength(2)", - "description": "a string at least 2 character(s) long", - "minLength": 2, - "type": "string" + propertyNames: { + title: "minLength(2)", + description: "a string at least 2 character(s) long", + minLength: 2, + type: "string" } } ) @@ -2657,26 +2989,26 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` value: Schema.NumberFromString }), { - "$defs": { - "NumberFromString": { - "type": "string", - "description": "a string to be decoded into a number" + $defs: { + NumberFromString: { + type: "string", + description: "a string to be decoded into a number" } }, - "type": "object", - "description": "a record to be decoded into a Map", - "required": [], - "properties": {}, - "patternProperties": { + type: "object", + description: "a record to be decoded into a Map", + required: [], + properties: {}, + patternProperties: { "": { - "$ref": "#/$defs/NumberFromString" + $ref: "#/$defs/NumberFromString" } }, - "propertyNames": { - "title": "minLength(2)", - "description": "a string at least 2 character(s) long", - "minLength": 2, - "type": "string" + propertyNames: { + title: "minLength(2)", + description: "a string at least 2 character(s) long", + minLength: 2, + type: "string" } } ) @@ -2687,24 +3019,32 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` it.skip("a title annotation on the transformation should not overwrite an annotation set on the from part", () => { const schema = Schema.make( new AST.Transformation( - new AST.TypeLiteral([new AST.PropertySignature("a", Schema.String.ast, false, true)], [], { - [AST.TitleAnnotationId]: "from-title" - }), - new AST.TypeLiteral([new AST.PropertySignature("a", Schema.String.ast, false, true)], [], { - [AST.TitleAnnotationId]: "to-title" - }), + new AST.TypeLiteral( + [new AST.PropertySignature("a", Schema.String.ast, false, true)], + [], + { + [AST.TitleAnnotationId]: "from-title" + } + ), + new AST.TypeLiteral( + [new AST.PropertySignature("a", Schema.String.ast, false, true)], + [], + { + [AST.TitleAnnotationId]: "to-title" + } + ), new AST.TypeLiteralTransformation([]), { [AST.TitleAnnotationId]: "transformation-title" } ) ) expectJSONSchemaProperty(schema, { - "type": "object", - "required": ["a"], - "properties": { - "a": { "type": "string" } + type: "object", + required: ["a"], + properties: { + a: { type: "string" } }, - "additionalProperties": false, - "title": "from-title" + additionalProperties: false, + title: "from-title" }) }) @@ -2712,24 +3052,32 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` it.skip("a description annotation on the transformation should not overwrite an annotation set on the from part", () => { const schema = Schema.make( new AST.Transformation( - new AST.TypeLiteral([new AST.PropertySignature("a", Schema.String.ast, false, true)], [], { - [AST.DescriptionAnnotationId]: "from-description" - }), - new AST.TypeLiteral([new AST.PropertySignature("a", Schema.String.ast, false, true)], [], { - [AST.DescriptionAnnotationId]: "to-description" - }), + new AST.TypeLiteral( + [new AST.PropertySignature("a", Schema.String.ast, false, true)], + [], + { + [AST.DescriptionAnnotationId]: "from-description" + } + ), + new AST.TypeLiteral( + [new AST.PropertySignature("a", Schema.String.ast, false, true)], + [], + { + [AST.DescriptionAnnotationId]: "to-description" + } + ), new AST.TypeLiteralTransformation([]), { [AST.DescriptionAnnotationId]: "transformation-description" } ) ) expectJSONSchemaProperty(schema, { - "type": "object", - "required": ["a"], - "properties": { - "a": { "type": "string" } + type: "object", + required: ["a"], + properties: { + a: { type: "string" } }, - "additionalProperties": false, - "description": "from-description" + additionalProperties: false, + description: "from-description" }) }) @@ -2753,19 +3101,19 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` title: "outer-title" }), { - "type": "object", - "description": "outer-description", - "title": "outer-title", - "required": [], - "properties": { - "a": { - "description": "middle-description", - "minLength": 1, - "title": "middle-title", - "type": "string" + type: "object", + description: "outer-description", + title: "outer-title", + required: [], + properties: { + a: { + description: "middle-description", + minLength: 1, + title: "middle-title", + type: "string" } }, - "additionalProperties": false + additionalProperties: false } ) }) @@ -2778,22 +3126,22 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` a: Schema.optionalWith(Schema.NonEmptyString, { as: "Option" }) }), { - "$defs": { - "NonEmptyString": { - "type": "string", - "title": "nonEmptyString", - "description": "a non empty string", - "minLength": 1 + $defs: { + NonEmptyString: { + type: "string", + title: "nonEmptyString", + description: "a non empty string", + minLength: 1 } }, - "type": "object", - "required": [], - "properties": { - "a": { - "$ref": "#/$defs/NonEmptyString" + type: "object", + required: [], + properties: { + a: { + $ref: "#/$defs/NonEmptyString" } }, - "additionalProperties": false + additionalProperties: false } ) }) @@ -2808,25 +3156,25 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` title: "aa67b73c-3161-4640-b1e1-5b5830cfb173" }), { - "$ref": "#/$defs/aa6f48cd-03e4-470a-beb7-5f7cc532c676", - "$defs": { - "NonEmptyString": { - "type": "string", - "title": "nonEmptyString", - "description": "a non empty string", - "minLength": 1 + $ref: "#/$defs/aa6f48cd-03e4-470a-beb7-5f7cc532c676", + $defs: { + NonEmptyString: { + type: "string", + title: "nonEmptyString", + description: "a non empty string", + minLength: 1 }, "aa6f48cd-03e4-470a-beb7-5f7cc532c676": { - "type": "object", - "required": [], - "properties": { - "a": { - "$ref": "#/$defs/NonEmptyString" + type: "object", + required: [], + properties: { + a: { + $ref: "#/$defs/NonEmptyString" } }, - "additionalProperties": false, - "description": "b964b873-0266-446b-acf4-97dc125e7553", - "title": "aa67b73c-3161-4640-b1e1-5b5830cfb173" + additionalProperties: false, + description: "b964b873-0266-446b-acf4-97dc125e7553", + title: "aa67b73c-3161-4640-b1e1-5b5830cfb173" } } } @@ -2839,27 +3187,28 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` it("a <- b", () => { expectJSONSchemaProperty( Schema.Struct({ - a: Schema.NonEmptyString.pipe(Schema.propertySignature, Schema.fromKey("b")) + a: Schema.NonEmptyString.pipe( + Schema.propertySignature, + Schema.fromKey("b") + ) }), { - "$defs": { - "NonEmptyString": { - "type": "string", - "title": "nonEmptyString", - "description": "a non empty string", - "minLength": 1 + $defs: { + NonEmptyString: { + type: "string", + title: "nonEmptyString", + description: "a non empty string", + minLength: 1 } }, - "type": "object", - "required": [ - "b" - ], - "properties": { - "b": { - "$ref": "#/$defs/NonEmptyString" + type: "object", + required: ["b"], + properties: { + b: { + $ref: "#/$defs/NonEmptyString" } }, - "additionalProperties": false + additionalProperties: false } ) }) @@ -2867,27 +3216,28 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` it("a <- b & annotations", () => { expectJSONSchemaProperty( Schema.Struct({ - a: Schema.NonEmptyString.pipe(Schema.propertySignature, Schema.fromKey("b")).annotations({}) + a: Schema.NonEmptyString.pipe( + Schema.propertySignature, + Schema.fromKey("b") + ).annotations({}) }), { - "$defs": { - "NonEmptyString": { - "type": "string", - "title": "nonEmptyString", - "description": "a non empty string", - "minLength": 1 + $defs: { + NonEmptyString: { + type: "string", + title: "nonEmptyString", + description: "a non empty string", + minLength: 1 } }, - "type": "object", - "required": [ - "b" - ], - "properties": { - "b": { - "$ref": "#/$defs/NonEmptyString" + type: "object", + required: ["b"], + properties: { + b: { + $ref: "#/$defs/NonEmptyString" } }, - "additionalProperties": false + additionalProperties: false } ) }) @@ -2895,34 +3245,35 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` it("with transformation identifier annotation", () => { expectJSONSchemaProperty( Schema.Struct({ - a: Schema.NonEmptyString.pipe(Schema.propertySignature, Schema.fromKey("b")) + a: Schema.NonEmptyString.pipe( + Schema.propertySignature, + Schema.fromKey("b") + ) }).annotations({ identifier: "d5ff7bc8-1bd5-42a7-8186-e29fd4c217ea", description: "5f7bc5b8-dd68-4ec5-b9e9-64df74bd3c45", title: "119da226-70aa-4ae6-ab63-7db10c7e9dde" }), { - "$ref": "#/$defs/d5ff7bc8-1bd5-42a7-8186-e29fd4c217ea", - "$defs": { - "NonEmptyString": { - "type": "string", - "title": "nonEmptyString", - "description": "a non empty string", - "minLength": 1 + $ref: "#/$defs/d5ff7bc8-1bd5-42a7-8186-e29fd4c217ea", + $defs: { + NonEmptyString: { + type: "string", + title: "nonEmptyString", + description: "a non empty string", + minLength: 1 }, "d5ff7bc8-1bd5-42a7-8186-e29fd4c217ea": { - "type": "object", - "required": [ - "b" - ], - "properties": { - "b": { - "$ref": "#/$defs/NonEmptyString" + type: "object", + required: ["b"], + properties: { + b: { + $ref: "#/$defs/NonEmptyString" } }, - "additionalProperties": false, - "description": "5f7bc5b8-dd68-4ec5-b9e9-64df74bd3c45", - "title": "119da226-70aa-4ae6-ab63-7db10c7e9dde" + additionalProperties: false, + description: "5f7bc5b8-dd68-4ec5-b9e9-64df74bd3c45", + title: "119da226-70aa-4ae6-ab63-7db10c7e9dde" } } } @@ -2946,26 +3297,23 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` }) ).annotations({ identifier: "cdb51157-6f4a-42c1-9075-5b9af3a1448c" }) const jsonSchema: Root = { - "$ref": "#/$defs/cdb51157-6f4a-42c1-9075-5b9af3a1448c", - "$defs": { + $ref: "#/$defs/cdb51157-6f4a-42c1-9075-5b9af3a1448c", + $defs: { "cdb51157-6f4a-42c1-9075-5b9af3a1448c": { - "type": "object", - "required": [ - "a", - "as" - ], - "properties": { - "a": { - "type": "string" + type: "object", + required: ["a", "as"], + properties: { + a: { + type: "string" }, - "as": { - "type": "array", - "items": { - "$ref": "#/$defs/cdb51157-6f4a-42c1-9075-5b9af3a1448c" + as: { + type: "array", + items: { + $ref: "#/$defs/cdb51157-6f4a-42c1-9075-5b9af3a1448c" } } }, - "additionalProperties": false + additionalProperties: false } } } @@ -2973,12 +3321,32 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` const validate = getAjvValidate(jsonSchema) assertTrue(validate({ a: "a1", as: [] })) assertTrue(validate({ a: "a1", as: [{ a: "a2", as: [] }] })) - assertTrue(validate({ a: "a1", as: [{ a: "a2", as: [] }, { a: "a3", as: [] }] })) assertTrue( - validate({ a: "a1", as: [{ a: "a2", as: [] }, { a: "a3", as: [{ a: "a4", as: [] }] }] }) + validate({ + a: "a1", + as: [ + { a: "a2", as: [] }, + { a: "a3", as: [] } + ] + }) + ) + assertTrue( + validate({ + a: "a1", + as: [ + { a: "a2", as: [] }, + { a: "a3", as: [{ a: "a4", as: [] }] } + ] + }) ) assertFalse( - validate({ a: "a1", as: [{ a: "a2", as: [] }, { a: "a3", as: [{ a: "a4", as: [1] }] }] }) + validate({ + a: "a1", + as: [ + { a: "a2", as: [] }, + { a: "a3", as: [{ a: "a4", as: [1] }] } + ] + }) ) }) @@ -2996,42 +3364,36 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` ) }) const jsonSchema: Root = { - "type": "object", - "required": [ - "a", - "as" - ], - "properties": { - "a": { - "type": "string" + type: "object", + required: ["a", "as"], + properties: { + a: { + type: "string" }, - "as": { - "type": "array", - "items": { - "$ref": "#/$defs/c4588a13-c003-4b8d-930f-d3469925ec1b" + as: { + type: "array", + items: { + $ref: "#/$defs/c4588a13-c003-4b8d-930f-d3469925ec1b" } } }, - "additionalProperties": false, - "$defs": { + additionalProperties: false, + $defs: { "c4588a13-c003-4b8d-930f-d3469925ec1b": { - "type": "object", - "required": [ - "a", - "as" - ], - "properties": { - "a": { - "type": "string" + type: "object", + required: ["a", "as"], + properties: { + a: { + type: "string" }, - "as": { - "type": "array", - "items": { - "$ref": "#/$defs/c4588a13-c003-4b8d-930f-d3469925ec1b" + as: { + type: "array", + items: { + $ref: "#/$defs/c4588a13-c003-4b8d-930f-d3469925ec1b" } } }, - "additionalProperties": false + additionalProperties: false } } } @@ -3039,12 +3401,32 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` const validate = getAjvValidate(jsonSchema) assertTrue(validate({ a: "a1", as: [] })) assertTrue(validate({ a: "a1", as: [{ a: "a2", as: [] }] })) - assertTrue(validate({ a: "a1", as: [{ a: "a2", as: [] }, { a: "a3", as: [] }] })) assertTrue( - validate({ a: "a1", as: [{ a: "a2", as: [] }, { a: "a3", as: [{ a: "a4", as: [] }] }] }) + validate({ + a: "a1", + as: [ + { a: "a2", as: [] }, + { a: "a3", as: [] } + ] + }) + ) + assertTrue( + validate({ + a: "a1", + as: [ + { a: "a2", as: [] }, + { a: "a3", as: [{ a: "a4", as: [] }] } + ] + }) ) assertFalse( - validate({ a: "a1", as: [{ a: "a2", as: [] }, { a: "a3", as: [{ a: "a4", as: [1] }] }] }) + validate({ + a: "a1", + as: [ + { a: "a2", as: [] }, + { a: "a3", as: [{ a: "a4", as: [1] }] } + ] + }) ) }) @@ -3055,48 +3437,63 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` } const schema = Schema.Struct({ name: Schema.String, - categories: Schema.Array(Schema.suspend((): Schema.Schema => schema)) + categories: Schema.Array( + Schema.suspend((): Schema.Schema => schema) + ) }).annotations({ identifier: "5c2a4755-f8f2-4290-a40f-ed247803a1a0" }) const jsonSchema: Root = { - "$ref": "#/$defs/5c2a4755-f8f2-4290-a40f-ed247803a1a0", - "$defs": { + $ref: "#/$defs/5c2a4755-f8f2-4290-a40f-ed247803a1a0", + $defs: { "5c2a4755-f8f2-4290-a40f-ed247803a1a0": { - "type": "object", - "required": [ - "name", - "categories" - ], - "properties": { - "name": { - "type": "string" + type: "object", + required: ["name", "categories"], + properties: { + name: { + type: "string" }, - "categories": { - "type": "array", - "items": { - "$ref": "#/$defs/5c2a4755-f8f2-4290-a40f-ed247803a1a0" + categories: { + type: "array", + items: { + $ref: "#/$defs/5c2a4755-f8f2-4290-a40f-ed247803a1a0" } } }, - "additionalProperties": false + additionalProperties: false } } } expectJSONSchemaProperty(schema, jsonSchema) const validate = getAjvValidate(jsonSchema) assertTrue(validate({ name: "a1", categories: [] })) - assertTrue(validate({ name: "a1", categories: [{ name: "a2", categories: [] }] })) - assertTrue(validate({ name: "a1", categories: [{ name: "a2", categories: [] }, { name: "a3", categories: [] }] })) + assertTrue( + validate({ name: "a1", categories: [{ name: "a2", categories: [] }] }) + ) + assertTrue( + validate({ + name: "a1", + categories: [ + { name: "a2", categories: [] }, + { name: "a3", categories: [] } + ] + }) + ) assertTrue( validate({ name: "a1", - categories: [{ name: "a2", categories: [] }, { name: "a3", categories: [{ name: "a4", categories: [] }] }] + categories: [ + { name: "a2", categories: [] }, + { name: "a3", categories: [{ name: "a4", categories: [] }] } + ] }) ) assertFalse( validate({ name: "a1", - categories: [{ name: "a2", categories: [] }, { name: "a3", categories: [{ name: "a4", categories: [1] }] }] + categories: [ + { name: "a2", categories: [] }, + { name: "a3", categories: [{ name: "a4", categories: [1] }] } + ] }) ) }) @@ -3133,156 +3530,151 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` ).annotations({ identifier: "e0f2ce47-eac7-4991-8730-90ebe4e0ffda" }) const jsonSchema: Root = { - "$ref": "#/$defs/e0f2ce47-eac7-4991-8730-90ebe4e0ffda", - "$defs": { + $ref: "#/$defs/e0f2ce47-eac7-4991-8730-90ebe4e0ffda", + $defs: { "e0f2ce47-eac7-4991-8730-90ebe4e0ffda": { - "type": "object", - "required": [ - "type", - "operator", - "left", - "right" - ], - "properties": { - "type": { - "type": "string", - "enum": ["operation"] + type: "object", + required: ["type", "operator", "left", "right"], + properties: { + type: { + type: "string", + enum: ["operation"] }, - "operator": { - "type": "string", - "enum": ["+", "-"] + operator: { + type: "string", + enum: ["+", "-"] }, - "left": { - "$ref": "#/$defs/2ad5683a-878f-4e4d-909c-496e59ce62e0" + left: { + $ref: "#/$defs/2ad5683a-878f-4e4d-909c-496e59ce62e0" }, - "right": { - "$ref": "#/$defs/2ad5683a-878f-4e4d-909c-496e59ce62e0" + right: { + $ref: "#/$defs/2ad5683a-878f-4e4d-909c-496e59ce62e0" } }, - "additionalProperties": false + additionalProperties: false }, "2ad5683a-878f-4e4d-909c-496e59ce62e0": { - "type": "object", - "required": [ - "type", - "value" - ], - "properties": { - "type": { - "type": "string", - "enum": ["expression"] + type: "object", + required: ["type", "value"], + properties: { + type: { + type: "string", + enum: ["expression"] }, - "value": { - "anyOf": [ + value: { + anyOf: [ { - "type": "number" + type: "number" }, { - "$ref": "#/$defs/e0f2ce47-eac7-4991-8730-90ebe4e0ffda" + $ref: "#/$defs/e0f2ce47-eac7-4991-8730-90ebe4e0ffda" } ] } }, - "additionalProperties": false + additionalProperties: false } } } expectJSONSchemaProperty(Operation, jsonSchema, { numRuns: 5 }) const validate = getAjvValidate(jsonSchema) - assertTrue(validate({ - type: "operation", - operator: "+", - left: { - type: "expression", - value: 1 - }, - right: { - type: "expression", - value: { - type: "operation", - operator: "-", - left: { - type: "expression", - value: 3 - }, - right: { - type: "expression", - value: 2 + assertTrue( + validate({ + type: "operation", + operator: "+", + left: { + type: "expression", + value: 1 + }, + right: { + type: "expression", + value: { + type: "operation", + operator: "-", + left: { + type: "expression", + value: 3 + }, + right: { + type: "expression", + value: 2 + } } } - } - })) + }) + ) }) }) it("examples JSON Schema annotation support", () => { - expectJSONSchemaAnnotations(Schema.String.annotations({ examples: ["a", "b"] }), { - "type": "string", - "examples": ["a", "b"] - }) - expectJSONSchemaProperty(Schema.BigInt.annotations({ examples: [1n, 2n] }), { - "description": "a string to be decoded into a bigint", - "examples": [ - "1", - "2" - ], - "type": "string" - }) + expectJSONSchemaAnnotations( + Schema.String.annotations({ examples: ["a", "b"] }), + { + type: "string", + examples: ["a", "b"] + } + ) + expectJSONSchemaProperty( + Schema.BigInt.annotations({ examples: [1n, 2n] }), + { + description: "a string to be decoded into a bigint", + examples: ["1", "2"], + type: "string" + } + ) expectJSONSchemaProperty( Schema.Struct({ - a: Schema.propertySignature(Schema.BigInt).annotations({ examples: [1n, 2n] }) + a: Schema.propertySignature(Schema.BigInt).annotations({ + examples: [1n, 2n] + }) }), { - "$defs": { - "BigInt": { - "type": "string", - "description": "a string to be decoded into a bigint" + $defs: { + BigInt: { + type: "string", + description: "a string to be decoded into a bigint" } }, - "type": "object", - "required": [ - "a" - ], - "properties": { - "a": { - "allOf": [ - { "$ref": "#/$defs/BigInt" } - ], - "examples": ["1", "2"] + type: "object", + required: ["a"], + properties: { + a: { + allOf: [{ $ref: "#/$defs/BigInt" }], + examples: ["1", "2"] } }, - "additionalProperties": false + additionalProperties: false } ) }) it("default JSON Schema annotation support", () => { expectJSONSchemaAnnotations(Schema.String.annotations({ default: "" }), { - "type": "string", - "default": "" + type: "string", + default: "" }) }) describe("Class", () => { it("should use the identifier as JSON Schema identifier", () => { const input = Schema.Struct({ a: Schema.String }) - class A extends Schema.Class("7a8b06e3-ebc1-4bdd-ab0d-3ec493d96d95")(input) {} + class A extends Schema.Class("7a8b06e3-ebc1-4bdd-ab0d-3ec493d96d95")( + input + ) {} expectJSONSchemaProperty(A, { - "$defs": { + $defs: { "7a8b06e3-ebc1-4bdd-ab0d-3ec493d96d95": { - "type": "object", - "required": [ - "a" - ], - "properties": { - "a": { - "type": "string" + type: "object", + required: ["a"], + properties: { + a: { + type: "string" } }, - "additionalProperties": false + additionalProperties: false } }, - "$ref": "#/$defs/7a8b06e3-ebc1-4bdd-ab0d-3ec493d96d95" + $ref: "#/$defs/7a8b06e3-ebc1-4bdd-ab0d-3ec493d96d95" }) }) @@ -3290,21 +3682,19 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` const input = Schema.Struct({ a: Schema.String }) class A extends Schema.Class("~package/name")(input) {} expectJSONSchemaProperty(A, { - "$defs": { + $defs: { "~package/name": { - "type": "object", - "required": [ - "a" - ], - "properties": { - "a": { - "type": "string" + type: "object", + required: ["a"], + properties: { + a: { + type: "string" } }, - "additionalProperties": false + additionalProperties: false } }, - "$ref": "#/$defs/~0package~1name" + $ref: "#/$defs/~0package~1name" }) }) }) @@ -3315,24 +3705,22 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` a: Schema.NonEmptyString.pipe(Schema.compose(Schema.NumberFromString)) }), { - "$defs": { - "NonEmptyString": { - "type": "string", - "title": "nonEmptyString", - "description": "a non empty string", - "minLength": 1 + $defs: { + NonEmptyString: { + type: "string", + title: "nonEmptyString", + description: "a non empty string", + minLength: 1 } }, - "type": "object", - "required": [ - "a" - ], - "properties": { - "a": { - "$ref": "#/$defs/NonEmptyString" + type: "object", + required: ["a"], + properties: { + a: { + $ref: "#/$defs/NonEmptyString" } }, - "additionalProperties": false + additionalProperties: false } ) }) @@ -3342,29 +3730,35 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` expectJSONSchemaAnnotations( Schema.Struct({ a: Schema.String - }).pipe( - Schema.filter(() => true, { - jsonSchema: { "examples": ["c5052c04-d6c9-44f3-9c8f-ede707d6ce38"] } - }) - ).pipe(Schema.extend( - Schema.Struct({ - b: JsonNumber - }).pipe( + }) + .pipe( Schema.filter(() => true, { - jsonSchema: { "$comment": "940b4ea4-6313-4b59-9e64-ff7a41b8eb15" } + jsonSchema: { examples: ["c5052c04-d6c9-44f3-9c8f-ede707d6ce38"] } }) ) - )), + .pipe( + Schema.extend( + Schema.Struct({ + b: JsonNumber + }).pipe( + Schema.filter(() => true, { + jsonSchema: { + $comment: "940b4ea4-6313-4b59-9e64-ff7a41b8eb15" + } + }) + ) + ) + ), { - "type": "object", - "required": ["a", "b"], - "properties": { - "a": { "type": "string" }, - "b": { "type": "number" } + type: "object", + required: ["a", "b"], + properties: { + a: { type: "string" }, + b: { type: "number" } }, - "examples": ["c5052c04-d6c9-44f3-9c8f-ede707d6ce38"], - "$comment": "940b4ea4-6313-4b59-9e64-ff7a41b8eb15", - "additionalProperties": false + examples: ["c5052c04-d6c9-44f3-9c8f-ede707d6ce38"], + $comment: "940b4ea4-6313-4b59-9e64-ff7a41b8eb15", + additionalProperties: false } ) }) @@ -3372,29 +3766,36 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` describe("identifier annotation support", () => { it("String", () => { - expectJSONSchemaProperty(Schema.String.annotations({ identifier: "6f274f5e-be19-48e6-8f33-16e9789b2731" }), { - "$defs": { - "6f274f5e-be19-48e6-8f33-16e9789b2731": { - "type": "string" - } - }, - "$ref": "#/$defs/6f274f5e-be19-48e6-8f33-16e9789b2731" - }) + expectJSONSchemaProperty( + Schema.String.annotations({ + identifier: "6f274f5e-be19-48e6-8f33-16e9789b2731" + }), + { + $defs: { + "6f274f5e-be19-48e6-8f33-16e9789b2731": { + type: "string" + } + }, + $ref: "#/$defs/6f274f5e-be19-48e6-8f33-16e9789b2731" + } + ) }) it("Refinement", () => { expectJSONSchemaProperty( - Schema.String.pipe(Schema.minLength(2)).annotations({ identifier: "cd6647a4-dc64-40a7-a031-61d35ed904ca" }), + Schema.String.pipe(Schema.minLength(2)).annotations({ + identifier: "cd6647a4-dc64-40a7-a031-61d35ed904ca" + }), { - "$defs": { + $defs: { "cd6647a4-dc64-40a7-a031-61d35ed904ca": { - "type": "string", - "title": "minLength(2)", - "description": "a string at least 2 character(s) long", - "minLength": 2 + type: "string", + title: "minLength(2)", + description: "a string at least 2 character(s) long", + minLength: 2 } }, - "$ref": "#/$defs/cd6647a4-dc64-40a7-a031-61d35ed904ca" + $ref: "#/$defs/cd6647a4-dc64-40a7-a031-61d35ed904ca" } ) }) @@ -3404,19 +3805,21 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` expectJSONSchemaProperty( Schema.Struct({ a: Schema.String - }).annotations({ identifier: "0df962f3-f649-4ffc-a3ec-a8b5344dd7de" }), + }).annotations({ + identifier: "0df962f3-f649-4ffc-a3ec-a8b5344dd7de" + }), { - "$defs": { + $defs: { "0df962f3-f649-4ffc-a3ec-a8b5344dd7de": { - "type": "object", - "required": ["a"], - "properties": { - "a": { "type": "string" } + type: "object", + required: ["a"], + properties: { + a: { type: "string" } }, - "additionalProperties": false + additionalProperties: false } }, - "$ref": "#/$defs/0df962f3-f649-4ffc-a3ec-a8b5344dd7de" + $ref: "#/$defs/0df962f3-f649-4ffc-a3ec-a8b5344dd7de" } ) }) @@ -3431,21 +3834,21 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` a: Name }) expectJSONSchemaProperty(schema, { - "$defs": { + $defs: { "44873d66-d138-4e2a-9782-5982a29f6ea8": { - "type": "string", - "description": "e5d30f53-b2df-4fa3-b151-9fc3a47d258e", - "title": "0115ccbf-5d27-41ed-a658-83c5f4a8805f" + type: "string", + description: "e5d30f53-b2df-4fa3-b151-9fc3a47d258e", + title: "0115ccbf-5d27-41ed-a658-83c5f4a8805f" } }, - "type": "object", - "required": ["a"], - "properties": { - "a": { - "$ref": "#/$defs/44873d66-d138-4e2a-9782-5982a29f6ea8" + type: "object", + required: ["a"], + properties: { + a: { + $ref: "#/$defs/44873d66-d138-4e2a-9782-5982a29f6ea8" } }, - "additionalProperties": false + additionalProperties: false }) }) @@ -3458,24 +3861,26 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` expectJSONSchemaProperty( Schema.Struct({ a: Name - }).annotations({ identifier: "7e559891-9143-4138-ae3e-81a5f0907380" }), + }).annotations({ + identifier: "7e559891-9143-4138-ae3e-81a5f0907380" + }), { - "$defs": { + $defs: { "7e559891-9143-4138-ae3e-81a5f0907380": { - "type": "object", - "required": ["a"], - "properties": { - "a": { "$ref": "#/$defs/b49f125d-1646-4eb5-8120-9524ab6039de" } + type: "object", + required: ["a"], + properties: { + a: { $ref: "#/$defs/b49f125d-1646-4eb5-8120-9524ab6039de" } }, - "additionalProperties": false + additionalProperties: false }, "b49f125d-1646-4eb5-8120-9524ab6039de": { - "type": "string", - "description": "703b7ff0-cb8d-49de-aeeb-05d92faa4599", - "title": "4b6d9ea6-7c4d-4073-a427-8d1b82fd1677" + type: "string", + description: "703b7ff0-cb8d-49de-aeeb-05d92faa4599", + title: "4b6d9ea6-7c4d-4073-a427-8d1b82fd1677" } }, - "$ref": "#/$defs/7e559891-9143-4138-ae3e-81a5f0907380" + $ref: "#/$defs/7e559891-9143-4138-ae3e-81a5f0907380" } ) }) @@ -3488,29 +3893,29 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` }) const schema = Schema.Struct({ a: Name, b: Schema.Struct({ c: Name }) }) expectJSONSchemaProperty(schema, { - "$defs": { + $defs: { "434a08dd-3f8f-4de4-b91d-8846aab1fb05": { - "type": "string", - "description": "eb183f5c-404c-4686-b78b-1bd00d18f8fd", - "title": "c0cbd438-1fb5-47fe-bf81-1ff5527e779a" + type: "string", + description: "eb183f5c-404c-4686-b78b-1bd00d18f8fd", + title: "c0cbd438-1fb5-47fe-bf81-1ff5527e779a" } }, - "type": "object", - "required": ["a", "b"], - "properties": { - "a": { - "$ref": "#/$defs/434a08dd-3f8f-4de4-b91d-8846aab1fb05" + type: "object", + required: ["a", "b"], + properties: { + a: { + $ref: "#/$defs/434a08dd-3f8f-4de4-b91d-8846aab1fb05" }, - "b": { - "type": "object", - "required": ["c"], - "properties": { - "c": { "$ref": "#/$defs/434a08dd-3f8f-4de4-b91d-8846aab1fb05" } + b: { + type: "object", + required: ["c"], + properties: { + c: { $ref: "#/$defs/434a08dd-3f8f-4de4-b91d-8846aab1fb05" } }, - "additionalProperties": false + additionalProperties: false } }, - "additionalProperties": false + additionalProperties: false }) }) }) @@ -3529,21 +3934,21 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` }) ), { - "$defs": { + $defs: { "170d659f-112e-4e3b-85db-464b668f2aed": { - "type": "string", - "enum": ["a"], - "description": "ef296f1c-01fe-4a20-bd35-ed449c964c49" + type: "string", + enum: ["a"], + description: "ef296f1c-01fe-4a20-bd35-ed449c964c49" }, "2a4e4f67-3732-4f7b-a505-856e51dd1578": { - "type": "string", - "enum": ["b"], - "description": "effbf54b-a62d-455b-86fa-97a5af46c6f3" + type: "string", + enum: ["b"], + description: "effbf54b-a62d-455b-86fa-97a5af46c6f3" } }, - "anyOf": [ - { "$ref": "#/$defs/170d659f-112e-4e3b-85db-464b668f2aed" }, - { "$ref": "#/$defs/2a4e4f67-3732-4f7b-a505-856e51dd1578" } + anyOf: [ + { $ref: "#/$defs/170d659f-112e-4e3b-85db-464b668f2aed" }, + { $ref: "#/$defs/2a4e4f67-3732-4f7b-a505-856e51dd1578" } ] } ) @@ -3555,28 +3960,45 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` it("an identifier annotation on the transformation should overwrite an annotation set on the from part", () => { const schema = Schema.make( new AST.Transformation( - new AST.TypeLiteral([new AST.PropertySignature("a", Schema.String.ast, false, true)], [], { - [AST.IdentifierAnnotationId]: "0f70b90b-b268-46c8-a5a3-035139ad9126" - }), - new AST.TypeLiteral([new AST.PropertySignature("a", Schema.String.ast, false, true)], [], { - [AST.IdentifierAnnotationId]: "77bb2410-9cf3-47cf-af76-fa3be1a3c626" - }), + new AST.TypeLiteral( + [ + new AST.PropertySignature("a", Schema.String.ast, false, true) + ], + [], + { + [AST.IdentifierAnnotationId]: + "0f70b90b-b268-46c8-a5a3-035139ad9126" + } + ), + new AST.TypeLiteral( + [ + new AST.PropertySignature("a", Schema.String.ast, false, true) + ], + [], + { + [AST.IdentifierAnnotationId]: + "77bb2410-9cf3-47cf-af76-fa3be1a3c626" + } + ), new AST.TypeLiteralTransformation([]), - { [AST.IdentifierAnnotationId]: "18e1de28-a15e-4373-bd2f-d53903942656" } + { + [AST.IdentifierAnnotationId]: + "18e1de28-a15e-4373-bd2f-d53903942656" + } ) ) expectJSONSchemaProperty(schema, { - "$defs": { + $defs: { "18e1de28-a15e-4373-bd2f-d53903942656": { - "type": "object", - "required": ["a"], - "properties": { - "a": { "type": "string" } + type: "object", + required: ["a"], + properties: { + a: { type: "string" } }, - "additionalProperties": false + additionalProperties: false } }, - "$ref": "#/$defs/18e1de28-a15e-4373-bd2f-d53903942656" + $ref: "#/$defs/18e1de28-a15e-4373-bd2f-d53903942656" }) }) @@ -3599,24 +4021,24 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` identifier: "75d9b539-eb6b-48d3-81dd-61176a9bce78" }), { - "$defs": { + $defs: { "75d9b539-eb6b-48d3-81dd-61176a9bce78": { - "type": "object", - "description": "outer-description", - "title": "outer-title", - "required": [], - "properties": { - "a": { - "type": "string", - "description": "middle-description", - "title": "middle-title", - "minLength": 1 + type: "object", + description: "outer-description", + title: "outer-title", + required: [], + properties: { + a: { + type: "string", + description: "middle-description", + title: "middle-title", + minLength: 1 } }, - "additionalProperties": false + additionalProperties: false } }, - "$ref": "#/$defs/75d9b539-eb6b-48d3-81dd-61176a9bce78" + $ref: "#/$defs/75d9b539-eb6b-48d3-81dd-61176a9bce78" } ) }) @@ -3629,19 +4051,19 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` it("should support typeSchema(Class)", () => { class A extends Schema.Class("A")({ a: Schema.String }) {} expectJSONSchemaProperty(Schema.typeSchema(A), { - "$defs": { - "A": { - "type": "object", - "required": ["a"], - "properties": { - "a": { - "type": "string" + $defs: { + A: { + type: "object", + required: ["a"], + properties: { + a: { + type: "string" } }, - "additionalProperties": false + additionalProperties: false } }, - "$ref": "#/$defs/A" + $ref: "#/$defs/A" }) expectJSONSchemaProperty( Schema.typeSchema(A).annotations({ @@ -3649,42 +4071,45 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` title: "title" }), { - "type": "object", - "required": ["a"], - "properties": { - "a": { - "type": "string" + type: "object", + required: ["a"], + properties: { + a: { + type: "string" } }, - "additionalProperties": false, - "description": "description", - "title": "title" + additionalProperties: false, + description: "description", + title: "title" } ) }) it("with identifier annotation", () => { - class A extends Schema.Class("A")({ a: Schema.String }, { - identifier: "ID", - description: "description", - title: "title" - }) {} + class A extends Schema.Class("A")( + { a: Schema.String }, + { + identifier: "ID", + description: "description", + title: "title" + } + ) {} expectJSONSchemaProperty(Schema.typeSchema(A), { - "$defs": { - "ID": { - "type": "object", - "required": ["a"], - "properties": { - "a": { - "type": "string" + $defs: { + ID: { + type: "object", + required: ["a"], + properties: { + a: { + type: "string" } }, - "additionalProperties": false, - "description": "description", - "title": "title" + additionalProperties: false, + description: "description", + title: "title" } }, - "$ref": "#/$defs/ID" + $ref: "#/$defs/ID" }) expectJSONSchemaProperty( Schema.typeSchema(A).annotations({ @@ -3692,16 +4117,16 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` title: "title" }), { - "type": "object", - "required": ["a"], - "properties": { - "a": { - "type": "string" + type: "object", + required: ["a"], + properties: { + a: { + type: "string" } }, - "additionalProperties": false, - "description": "description", - "title": "title" + additionalProperties: false, + description: "description", + title: "title" } ) }) @@ -3712,7 +4137,7 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` it("refinements without a jsonSchema annotation should be ignored rather than raising an error", () => { const schema = Schema.String.pipe(Schema.filter(() => true)) expectJSONSchema(schema, { - "type": "string" + type: "string" }) }) @@ -3720,26 +4145,29 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` expectJSONSchema( Schema.String.annotations({ [AST.SurrogateAnnotationId]: Schema.Number.ast, - jsonSchema: { "type": "custom" } + jsonSchema: { type: "custom" } }), { - "type": "custom" + type: "custom" } ) }) describe("Class", () => { it("should support typeSchema(Class) with custom annotation", () => { - class A extends Schema.Class("3c9977ee-0e9b-4471-99af-c6c73340f9ed")({ a: Schema.String }, { - jsonSchema: { "type": "custom" } - }) {} + class A extends Schema.Class("3c9977ee-0e9b-4471-99af-c6c73340f9ed")( + { a: Schema.String }, + { + jsonSchema: { type: "custom" } + } + ) {} expectJSONSchema(Schema.typeSchema(A), { - "$defs": { + $defs: { "3c9977ee-0e9b-4471-99af-c6c73340f9ed": { - "type": "custom" + type: "custom" } }, - "$ref": "#/$defs/3c9977ee-0e9b-4471-99af-c6c73340f9ed" + $ref: "#/$defs/3c9977ee-0e9b-4471-99af-c6c73340f9ed" }) }) }) @@ -3757,126 +4185,165 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` description: "My Description" }) expectJSONSchema(schema, { - "type": "my-type", - "title": "My Title", - "description": "My Description" + type: "my-type", + title: "My Title", + description: "My Description" }) }) it("Void", () => { - expectJSONSchema(Schema.Void.annotations({ jsonSchema: { "type": "custom" } }), { - "type": "custom" - }) + expectJSONSchema( + Schema.Void.annotations({ jsonSchema: { type: "custom" } }), + { + type: "custom" + } + ) }) it("Never", () => { - expectJSONSchema(Schema.Never.annotations({ jsonSchema: { "type": "custom" } }), { - "type": "custom" - }) + expectJSONSchema( + Schema.Never.annotations({ jsonSchema: { type: "custom" } }), + { + type: "custom" + } + ) }) it("Literal", () => { - expectJSONSchema(Schema.Literal("a").annotations({ jsonSchema: { "type": "custom" } }), { - "type": "custom" - }) + expectJSONSchema( + Schema.Literal("a").annotations({ jsonSchema: { type: "custom" } }), + { + type: "custom" + } + ) }) it("SymbolFromSelf", () => { - expectJSONSchema(Schema.SymbolFromSelf.annotations({ jsonSchema: { "type": "custom" } }), { - "type": "custom" - }) + expectJSONSchema( + Schema.SymbolFromSelf.annotations({ jsonSchema: { type: "custom" } }), + { + type: "custom" + } + ) }) it("UniqueSymbolFromSelf", () => { expectJSONSchema( - Schema.UniqueSymbolFromSelf(Symbol.for("effect/schema/test/a")).annotations({ - jsonSchema: { "type": "custom" } + Schema.UniqueSymbolFromSelf( + Symbol.for("effect/schema/test/a") + ).annotations({ + jsonSchema: { type: "custom" } }), - { "type": "custom" } + { type: "custom" } ) }) it("TemplateLiteral", () => { expectJSONSchema( - Schema.TemplateLiteral(Schema.Literal("a"), Schema.String, Schema.Literal("b")).annotations({ - jsonSchema: { "type": "custom" } + Schema.TemplateLiteral( + Schema.Literal("a"), + Schema.String, + Schema.Literal("b") + ).annotations({ + jsonSchema: { type: "custom" } }), - { "type": "custom" } + { type: "custom" } ) }) it("Undefined", () => { - expectJSONSchema(Schema.Undefined.annotations({ jsonSchema: { "type": "custom" } }), { - "type": "custom" - }) + expectJSONSchema( + Schema.Undefined.annotations({ jsonSchema: { type: "custom" } }), + { + type: "custom" + } + ) }) it("Unknown", () => { - expectJSONSchema(Schema.Unknown.annotations({ jsonSchema: { "type": "custom" } }), { - "type": "custom" - }) + expectJSONSchema( + Schema.Unknown.annotations({ jsonSchema: { type: "custom" } }), + { + type: "custom" + } + ) }) it("Any", () => { - expectJSONSchema(Schema.Any.annotations({ jsonSchema: { "type": "custom" } }), { - "type": "custom" - }) + expectJSONSchema( + Schema.Any.annotations({ jsonSchema: { type: "custom" } }), + { + type: "custom" + } + ) }) it("Object", () => { - expectJSONSchema(Schema.Object.annotations({ jsonSchema: { "type": "custom" } }), { - "type": "custom" - }) + expectJSONSchema( + Schema.Object.annotations({ jsonSchema: { type: "custom" } }), + { + type: "custom" + } + ) }) it("String", () => { expectJSONSchema( Schema.String.annotations({ jsonSchema: { - "type": "custom", - "description": "description", - "format": "uuid" + type: "custom", + description: "description", + format: "uuid" } }), { - "type": "custom", - "description": "description", - "format": "uuid" + type: "custom", + description: "description", + format: "uuid" } ) expectJSONSchema( Schema.String.annotations({ identifier: "630d10c4-7030-45e7-894d-2c0bf5acadcf", - jsonSchema: { "type": "custom", "description": "description" } + jsonSchema: { type: "custom", description: "description" } }), { - "$defs": { + $defs: { "630d10c4-7030-45e7-894d-2c0bf5acadcf": { - "type": "custom", - "description": "description" + type: "custom", + description: "description" } }, - "$ref": "#/$defs/630d10c4-7030-45e7-894d-2c0bf5acadcf" + $ref: "#/$defs/630d10c4-7030-45e7-894d-2c0bf5acadcf" } ) }) it("Number", () => { - expectJSONSchema(Schema.Number.annotations({ jsonSchema: { "type": "custom" } }), { - "type": "custom" - }) + expectJSONSchema( + Schema.Number.annotations({ jsonSchema: { type: "custom" } }), + { + type: "custom" + } + ) }) it("BigintFromSelf", () => { - expectJSONSchema(Schema.BigIntFromSelf.annotations({ jsonSchema: { "type": "custom" } }), { - "type": "custom" - }) + expectJSONSchema( + Schema.BigIntFromSelf.annotations({ jsonSchema: { type: "custom" } }), + { + type: "custom" + } + ) }) it("Boolean", () => { - expectJSONSchema(Schema.Boolean.annotations({ jsonSchema: { "type": "custom" } }), { - "type": "custom" - }) + expectJSONSchema( + Schema.Boolean.annotations({ jsonSchema: { type: "custom" } }), + { + type: "custom" + } + ) }) it("Enums", () => { @@ -3884,49 +4351,54 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` Apple, Banana } - expectJSONSchema(Schema.Enums(Fruits).annotations({ jsonSchema: { "type": "custom" } }), { - "type": "custom" - }) + expectJSONSchema( + Schema.Enums(Fruits).annotations({ jsonSchema: { type: "custom" } }), + { + type: "custom" + } + ) }) it("Tuple", () => { expectJSONSchema( - Schema.Tuple(Schema.String, JsonNumber).annotations({ jsonSchema: { "type": "custom" } }), - { "type": "custom" } + Schema.Tuple(Schema.String, JsonNumber).annotations({ + jsonSchema: { type: "custom" } + }), + { type: "custom" } ) }) it("Struct", () => { expectJSONSchema( Schema.Struct({ a: Schema.String, b: JsonNumber }).annotations({ - jsonSchema: { "type": "custom" } + jsonSchema: { type: "custom" } }), - { "type": "custom" } + { type: "custom" } ) }) it("Union", () => { expectJSONSchema( - Schema.Union(Schema.String, JsonNumber).annotations({ jsonSchema: { "type": "custom" } }), - { "type": "custom" } + Schema.Union(Schema.String, JsonNumber).annotations({ + jsonSchema: { type: "custom" } + }), + { type: "custom" } ) }) it("UUID", () => { - expectJSONSchema( - Schema.UUID, - { - "$defs": { - "UUID": { - "description": "a Universally Unique Identifier", - "pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", - "type": "string", - "format": "uuid" - } - }, - "$ref": "#/$defs/UUID" - } - ) + expectJSONSchema(Schema.UUID, { + $defs: { + UUID: { + description: "a Universally Unique Identifier", + pattern: + "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", + type: "string", + format: "uuid" + } + }, + $ref: "#/$defs/UUID" + }) }) it("Suspend", () => { @@ -3937,48 +4409,52 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` const schema = Schema.Struct({ a: Schema.String, as: Schema.Array( - Schema.suspend((): Schema.Schema => schema).annotations({ jsonSchema: { "type": "custom" } }) + Schema.suspend((): Schema.Schema => schema).annotations({ + jsonSchema: { type: "custom" } + }) ) }) expectJSONSchema(schema, { - "type": "object", - "required": [ - "a", - "as" - ], - "properties": { - "a": { - "type": "string" + type: "object", + required: ["a", "as"], + properties: { + a: { + type: "string" }, - "as": { - "type": "array", - "items": { - "type": "custom" + as: { + type: "array", + items: { + type: "custom" } } }, - "additionalProperties": false + additionalProperties: false }) }) describe("Refinement", () => { it("Int", () => { - expectJSONSchema(Schema.Int.annotations({ jsonSchema: { "type": "custom" } }), { - "type": "custom" - }) + expectJSONSchema( + Schema.Int.annotations({ jsonSchema: { type: "custom" } }), + { + type: "custom" + } + ) }) it("custom", () => { expectJSONSchemaProperty( - Schema.String.pipe(Schema.filter(() => true, { jsonSchema: {} })).annotations({ + Schema.String.pipe( + Schema.filter(() => true, { jsonSchema: {} }) + ).annotations({ identifier: "230acf3d-b3b0-4c3e-8ccc-5ca089c80014" }), { - "$ref": "#/$defs/230acf3d-b3b0-4c3e-8ccc-5ca089c80014", - "$defs": { + $ref: "#/$defs/230acf3d-b3b0-4c3e-8ccc-5ca089c80014", + $defs: { "230acf3d-b3b0-4c3e-8ccc-5ca089c80014": { - "type": "string" + type: "string" } } } @@ -3987,22 +4463,30 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` }) it("Transformation", () => { - expectJSONSchema(Schema.NumberFromString.annotations({ jsonSchema: { "type": "custom" } }), { - "type": "custom" - }) + expectJSONSchema( + Schema.NumberFromString.annotations({ jsonSchema: { type: "custom" } }), + { + type: "custom" + } + ) }) it("refinement of a transformation with an override annotation", () => { - expectJSONSchema(Schema.Date.annotations({ jsonSchema: { type: "string", format: "date-time" } }), { - "format": "date-time", - "type": "string" - }) + expectJSONSchema( + Schema.Date.annotations({ + jsonSchema: { type: "string", format: "date-time" } + }), + { + format: "date-time", + type: "string" + } + ) expectJSONSchema( Schema.Date.annotations({ jsonSchema: { anyOf: [{ type: "object" }, { type: "array" }] } }), { - "anyOf": [{ "type": "object" }, { "type": "array" }] + anyOf: [{ type: "object" }, { type: "array" }] } ) expectJSONSchema( @@ -4010,32 +4494,42 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` jsonSchema: { anyOf: [{ type: "object" }, { type: "array" }] } }), { - "anyOf": [{ "type": "object" }, { "type": "array" }] + anyOf: [{ type: "object" }, { type: "array" }] } ) - expectJSONSchema(Schema.Date.annotations({ jsonSchema: { "$ref": "x" } }), { - "$ref": "x" - }) - expectJSONSchema(Schema.Date.annotations({ jsonSchema: { "type": "number", "const": 1 } }), { - "type": "number", - "const": 1 - }) - expectJSONSchema(Schema.Date.annotations({ jsonSchema: { "type": "number", "enum": [1] } }), { - "type": "number", - "enum": [1] + expectJSONSchema(Schema.Date.annotations({ jsonSchema: { $ref: "x" } }), { + $ref: "x" }) + expectJSONSchema( + Schema.Date.annotations({ jsonSchema: { type: "number", const: 1 } }), + { + type: "number", + const: 1 + } + ) + expectJSONSchema( + Schema.Date.annotations({ jsonSchema: { type: "number", enum: [1] } }), + { + type: "number", + enum: [1] + } + ) }) it("refinement of a transformation without an override annotation", () => { expectJSONSchema(Schema.Trim.pipe(Schema.nonEmptyString()), { - "type": "string", - "description": "a string that will be trimmed" + type: "string", + description: "a string that will be trimmed" }) expectJSONSchema( - Schema.Trim.pipe(Schema.nonEmptyString({ jsonSchema: { title: "a0ba6c10-091e-4ceb-9773-25fb1466fb1b" } })), + Schema.Trim.pipe( + Schema.nonEmptyString({ + jsonSchema: { title: "a0ba6c10-091e-4ceb-9773-25fb1466fb1b" } + }) + ), { - "type": "string", - "description": "a string that will be trimmed" + type: "string", + description: "a string that will be trimmed" } ) expectJSONSchema( @@ -4043,8 +4537,8 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` jsonSchema: { title: "75f7eb4f-626d-4dc6-af48-c17094418d85" } }), { - "type": "string", - "description": "a string that will be trimmed" + type: "string", + description: "a string that will be trimmed" } ) }) @@ -4057,16 +4551,16 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` a: Schema.Undefined }), { - "type": "object", - "required": [], - "properties": { - "a": { - "$id": "/schemas/never", - "not": {}, - "title": "never" + type: "object", + required: [], + properties: { + a: { + $id: "/schemas/never", + not: {}, + title: "never" } }, - "additionalProperties": false + additionalProperties: false } ) }) @@ -4077,16 +4571,16 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` a: Schema.UndefinedOr(Schema.Undefined) }), { - "type": "object", - "required": [], - "properties": { - "a": { - "$id": "/schemas/never", - "not": {}, - "title": "never" + type: "object", + required: [], + properties: { + a: { + $id: "/schemas/never", + not: {}, + title: "never" } }, - "additionalProperties": false + additionalProperties: false } ) }) @@ -4097,16 +4591,16 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` a: Schema.UndefinedOr(Schema.UndefinedOr(Schema.Undefined)) }), { - "type": "object", - "required": [], - "properties": { - "a": { - "$id": "/schemas/never", - "not": {}, - "title": "never" + type: "object", + required: [], + properties: { + a: { + $id: "/schemas/never", + not: {}, + title: "never" } }, - "additionalProperties": false + additionalProperties: false } ) }) @@ -4117,12 +4611,12 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` a: Schema.optional(Schema.String) }), { - "type": "object", - "required": [], - "properties": { - "a": { "type": "string" } + type: "object", + required: [], + properties: { + a: { type: "string" } }, - "additionalProperties": false + additionalProperties: false } ) }) @@ -4130,18 +4624,20 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` it("Schema.optional + inner annotation", () => { expectJSONSchemaAnnotations( Schema.Struct({ - a: Schema.optional(Schema.String.annotations({ description: "inner" })) + a: Schema.optional( + Schema.String.annotations({ description: "inner" }) + ) }), { - "type": "object", - "required": [], - "properties": { - "a": { - "type": "string", - "description": "inner" + type: "object", + required: [], + properties: { + a: { + type: "string", + description: "inner" } }, - "additionalProperties": false + additionalProperties: false } ) }) @@ -4149,20 +4645,22 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` it("Schema.optional + outer annotation should override inner annotation", () => { expectJSONSchemaAnnotations( Schema.Struct({ - a: Schema.optional(Schema.String.annotations({ description: "inner" })).annotations({ + a: Schema.optional( + Schema.String.annotations({ description: "inner" }) + ).annotations({ description: "outer" }) }), { - "type": "object", - "required": [], - "properties": { - "a": { - "type": "string", - "description": "outer" + type: "object", + required: [], + properties: { + a: { + type: "string", + description: "outer" } }, - "additionalProperties": false + additionalProperties: false } ) }) @@ -4173,14 +4671,14 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` a: Schema.UndefinedOr(Schema.String) }), { - "type": "object", - "required": [], - "properties": { - "a": { - "type": "string" + type: "object", + required: [], + properties: { + a: { + type: "string" } }, - "additionalProperties": false + additionalProperties: false } ) }) @@ -4188,18 +4686,20 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` it("UndefinedOr + inner annotation", () => { expectJSONSchemaAnnotations( Schema.Struct({ - a: Schema.UndefinedOr(Schema.String.annotations({ description: "inner" })) + a: Schema.UndefinedOr( + Schema.String.annotations({ description: "inner" }) + ) }), { - "type": "object", - "required": [], - "properties": { - "a": { - "type": "string", - "description": "inner" + type: "object", + required: [], + properties: { + a: { + type: "string", + description: "inner" } }, - "additionalProperties": false + additionalProperties: false } ) }) @@ -4207,20 +4707,22 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` it("UndefinedOr + annotation should not override inner annotations", () => { expectJSONSchemaAnnotations( Schema.Struct({ - a: Schema.UndefinedOr(Schema.String.annotations({ description: "inner" })).annotations({ + a: Schema.UndefinedOr( + Schema.String.annotations({ description: "inner" }) + ).annotations({ description: "middle" }) }), { - "type": "object", - "required": [], - "properties": { - "a": { - "type": "string", - "description": "inner" + type: "object", + required: [], + properties: { + a: { + type: "string", + description: "inner" } }, - "additionalProperties": false + additionalProperties: false } ) }) @@ -4229,21 +4731,23 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` expectJSONSchemaAnnotations( Schema.Struct({ a: Schema.propertySignature( - Schema.UndefinedOr(Schema.String.annotations({ description: "inner" })).annotations({ + Schema.UndefinedOr( + Schema.String.annotations({ description: "inner" }) + ).annotations({ description: "middle" }) ).annotations({ description: "outer" }) }), { - "type": "object", - "required": [], - "properties": { - "a": { - "type": "string", - "description": "outer" + type: "object", + required: [], + properties: { + a: { + type: "string", + description: "outer" } }, - "additionalProperties": false + additionalProperties: false } ) }) @@ -4251,15 +4755,17 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` it("UndefinedOr + jsonSchema annotation should keep the property required", () => { expectJSONSchema( Schema.Struct({ - a: Schema.UndefinedOr(Schema.String).annotations({ jsonSchema: { "type": "string" } }) + a: Schema.UndefinedOr(Schema.String).annotations({ + jsonSchema: { type: "string" } + }) }), { - "type": "object", - "required": ["a"], - "properties": { - "a": { "type": "string" } + type: "object", + required: ["a"], + properties: { + a: { type: "string" } }, - "additionalProperties": false + additionalProperties: false } ) }) @@ -4270,14 +4776,14 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` a: Schema.OptionFromUndefinedOr(Schema.String) }), { - "type": "object", - "required": [], - "properties": { - "a": { - "type": "string" + type: "object", + required: [], + properties: { + a: { + type: "string" } }, - "additionalProperties": false + additionalProperties: false } ) }) @@ -4288,14 +4794,14 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` a: Schema.suspend(() => Schema.UndefinedOr(Schema.String)) }), { - "type": "object", - "required": [], - "properties": { - "a": { - "type": "string" + type: "object", + required: [], + properties: { + a: { + type: "string" } }, - "additionalProperties": false + additionalProperties: false } ) }) @@ -4305,12 +4811,14 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` describe("borrowing the identifier", () => { describe("Declaration", () => { it("without inner transformation", () => { - const schema = Schema.Chunk(Schema.String).annotations({ identifier: "ID" }) + const schema = Schema.Chunk(Schema.String).annotations({ + identifier: "ID" + }) const expected = { - "items": { - "type": "string" + items: { + type: "string" }, - "type": "array" + type: "array" } expectJSONSchemaProperty(Schema.encodedBoundSchema(schema), expected) expectJSONSchemaProperty(Schema.encodedSchema(schema), expected) @@ -4321,11 +4829,11 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` identifier: "ID" }) const expected = { - "items": { - "description": "a string to be decoded into a number", - "type": "string" + items: { + description: "a string to be decoded into a number", + type: "string" }, - "type": "array" + type: "array" } expectJSONSchemaProperty(Schema.encodedBoundSchema(schema), expected) expectJSONSchemaProperty(Schema.encodedSchema(schema), expected) @@ -4336,65 +4844,71 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` it("without from transformation", () => { const schema = Schema.Trimmed expectJSONSchemaProperty(Schema.encodedBoundSchema(schema), { - "$defs": { - "Trimmed": { - "title": "trimmed", - "description": "a string with no leading or trailing whitespace", - "pattern": "^\\S[\\s\\S]*\\S$|^\\S$|^$", - "type": "string" + $defs: { + Trimmed: { + title: "trimmed", + description: "a string with no leading or trailing whitespace", + pattern: "^\\S[\\s\\S]*\\S$|^\\S$|^$", + type: "string" } }, - "$ref": "#/$defs/Trimmed" + $ref: "#/$defs/Trimmed" }) expectJSONSchemaProperty(Schema.encodedSchema(schema), { - "type": "string" + type: "string" }) }) it("with from transformation", () => { - const schema = Schema.compose(Schema.String, Schema.Trimmed).annotations({ + const schema = Schema.compose( + Schema.String, + Schema.Trimmed + ).annotations({ identifier: "ID" }) const expected = { - "type": "string" + type: "string" } expectJSONSchemaProperty(Schema.encodedBoundSchema(schema), expected) expectJSONSchemaProperty(Schema.encodedSchema(schema), expected) }) it("a stable filter without inner transformations", () => { - const schema = Schema.Array(Schema.NumberFromString).pipe(Schema.minItems(2)).annotations( - { identifier: "ID" } - ) + const schema = Schema.Array(Schema.NumberFromString) + .pipe(Schema.minItems(2)) + .annotations({ identifier: "ID" }) expectJSONSchemaProperty(Schema.encodedBoundSchema(schema), { - "$defs": { - "ID": { - "description": "an array of at least 2 item(s)", - "title": "minItems(2)", - "items": { - "description": "a string to be decoded into a number", - "type": "string" + $defs: { + ID: { + description: "an array of at least 2 item(s)", + title: "minItems(2)", + items: { + description: "a string to be decoded into a number", + type: "string" }, - "minItems": 2, - "type": "array" + minItems: 2, + type: "array" } }, - "$ref": "#/$defs/ID" + $ref: "#/$defs/ID" }) expectJSONSchemaProperty(Schema.encodedSchema(schema), { - "items": { - "description": "a string to be decoded into a number", - "type": "string" + items: { + description: "a string to be decoded into a number", + type: "string" }, - "type": "array" + type: "array" }) }) it("a stable filter with inner transformations SHOULD NOT borrow the annotations, identifier included", () => { - const schema = Schema.compose(Schema.Unknown, Schema.Array(Schema.String)).pipe(Schema.minItems(1)) + const schema = Schema.compose( + Schema.Unknown, + Schema.Array(Schema.String) + ).pipe(Schema.minItems(1)) const expected = { - "$id": "/schemas/unknown", - "title": "unknown" + $id: "/schemas/unknown", + title: "unknown" } expectJSONSchemaProperty(Schema.encodedBoundSchema(schema), expected) expectJSONSchemaProperty(Schema.encodedSchema(schema), expected) @@ -4407,15 +4921,15 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` identifier: "4d8bbca3-9462-4679-8ee6-e4e718711552" }) const expected = { - "$defs": { + $defs: { "4d8bbca3-9462-4679-8ee6-e4e718711552": { - "additionalItems": false, - "items": [{ "type": "string" }], - "minItems": 1, - "type": "array" + additionalItems: false, + items: [{ type: "string" }], + minItems: 1, + type: "array" } }, - "$ref": "#/$defs/4d8bbca3-9462-4679-8ee6-e4e718711552" + $ref: "#/$defs/4d8bbca3-9462-4679-8ee6-e4e718711552" } expectJSONSchemaProperty(Schema.encodedBoundSchema(schema), expected) expectJSONSchemaProperty(Schema.encodedSchema(schema), expected) @@ -4426,13 +4940,15 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` identifier: "ID" }) const expected = { - "additionalItems": false, - "items": [{ - "description": "a string to be decoded into a number", - "type": "string" - }], - "minItems": 1, - "type": "array" + additionalItems: false, + items: [ + { + description: "a string to be decoded into a number", + type: "string" + } + ], + minItems: 1, + type: "array" } expectJSONSchemaProperty(Schema.encodedBoundSchema(schema), expected) expectJSONSchemaProperty(Schema.encodedSchema(schema), expected) @@ -4445,36 +4961,38 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` identifier: "c8d0663b-c41b-4b6f-8b6e-bff59afc87c3" }) const expected = { - "$defs": { + $defs: { "c8d0663b-c41b-4b6f-8b6e-bff59afc87c3": { - "additionalProperties": false, - "properties": { - "a": { "type": "string" } + additionalProperties: false, + properties: { + a: { type: "string" } }, - "required": ["a"], - "type": "object" + required: ["a"], + type: "object" } }, - "$ref": "#/$defs/c8d0663b-c41b-4b6f-8b6e-bff59afc87c3" + $ref: "#/$defs/c8d0663b-c41b-4b6f-8b6e-bff59afc87c3" } expectJSONSchemaProperty(Schema.encodedBoundSchema(schema), expected) expectJSONSchemaProperty(Schema.encodedSchema(schema), expected) }) it("with inner transformations", () => { - const schema = Schema.Struct({ a: Schema.NumberFromString }).annotations({ + const schema = Schema.Struct({ + a: Schema.NumberFromString + }).annotations({ identifier: "ID" }) const expected = { - "additionalProperties": false, - "properties": { - "a": { - "description": "a string to be decoded into a number", - "type": "string" + additionalProperties: false, + properties: { + a: { + description: "a string to be decoded into a number", + type: "string" } }, - "required": ["a"], - "type": "object" + required: ["a"], + type: "object" } expectJSONSchemaProperty(Schema.encodedBoundSchema(schema), expected) expectJSONSchemaProperty(Schema.encodedSchema(schema), expected) @@ -4483,41 +5001,44 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` describe("Union", () => { it("without inner transformations", () => { - const schema = Schema.Union(Schema.String, Schema.JsonNumber).annotations({ + const schema = Schema.Union( + Schema.String, + Schema.JsonNumber + ).annotations({ identifier: "ID" }) expectJSONSchemaProperty(Schema.encodedBoundSchema(schema), { - "$defs": { - "JsonNumber": { - "description": "a finite number", - "title": "finite", - "type": "number" + $defs: { + JsonNumber: { + description: "a finite number", + title: "finite", + type: "number" }, - "ID": { - "anyOf": [ - { "type": "string" }, - { "$ref": "#/$defs/JsonNumber" } - ] + ID: { + anyOf: [{ type: "string" }, { $ref: "#/$defs/JsonNumber" }] } }, - "$ref": "#/$defs/ID" + $ref: "#/$defs/ID" }) expectJSONSchema(Schema.encodedSchema(schema), { - "anyOf": [ - { "type": "string" }, - { "type": "number" } - ] + anyOf: [{ type: "string" }, { type: "number" }] }) }) it("with inner transformations", () => { - const schema = Schema.Union(Schema.String, Schema.NumberFromString).annotations({ + const schema = Schema.Union( + Schema.String, + Schema.NumberFromString + ).annotations({ identifier: "ID" }) const expected = { - "anyOf": [ - { "type": "string" }, - { "description": "a string to be decoded into a number", "type": "string" } + anyOf: [ + { type: "string" }, + { + description: "a string to be decoded into a number", + type: "string" + } ] } expectJSONSchemaProperty(Schema.encodedBoundSchema(schema), expected) @@ -4540,82 +5061,70 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` }) expectJSONSchemaProperty(Schema.encodedBoundSchema(schema), { - "type": "object", - "required": [ - "name", - "categories" - ], - "properties": { - "name": { - "type": "string" + type: "object", + required: ["name", "categories"], + properties: { + name: { + type: "string" }, - "categories": { - "type": "array", - "items": { - "$ref": "#/$defs/IDEncodedBound" + categories: { + type: "array", + items: { + $ref: "#/$defs/IDEncodedBound" } } }, - "additionalProperties": false, - "$defs": { - "IDEncodedBound": { - "type": "object", - "required": [ - "name", - "categories" - ], - "properties": { - "name": { - "type": "string" + additionalProperties: false, + $defs: { + IDEncodedBound: { + type: "object", + required: ["name", "categories"], + properties: { + name: { + type: "string" }, - "categories": { - "type": "array", - "items": { - "$ref": "#/$defs/IDEncodedBound" + categories: { + type: "array", + items: { + $ref: "#/$defs/IDEncodedBound" } } }, - "additionalProperties": false + additionalProperties: false } } }) expectJSONSchemaProperty(Schema.encodedSchema(schema), { - "type": "object", - "required": [ - "name", - "categories" - ], - "properties": { - "name": { - "type": "string" + type: "object", + required: ["name", "categories"], + properties: { + name: { + type: "string" }, - "categories": { - "type": "array", - "items": { - "$ref": "#/$defs/IDEncoded" + categories: { + type: "array", + items: { + $ref: "#/$defs/IDEncoded" } } }, - "additionalProperties": false, - "$defs": { - "IDEncoded": { - "type": "object", - "required": [ - "name", - "categories" - ], - "properties": { - "name": { - "type": "string" + additionalProperties: false, + $defs: { + IDEncoded: { + type: "object", + required: ["name", "categories"], + properties: { + name: { + type: "string" }, - "categories": { - "type": "array", - "items": { - "$ref": "#/$defs/IDEncoded" + categories: { + type: "array", + items: { + $ref: "#/$defs/IDEncoded" } } }, - "additionalProperties": false + additionalProperties: false } } }) @@ -4631,94 +5140,83 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` readonly categories: ReadonlyArray } - const schema: Schema.Schema = Schema.Struct({ - name: Schema.NumberFromString, - categories: Schema.Array( - Schema.suspend(() => schema).annotations({ identifier: "ID" }) - ) - }) + const schema: Schema.Schema = + Schema.Struct({ + name: Schema.NumberFromString, + categories: Schema.Array( + Schema.suspend(() => schema).annotations({ identifier: "ID" }) + ) + }) expectJSONSchemaProperty(Schema.encodedBoundSchema(schema), { - "type": "object", - "required": [ - "name", - "categories" - ], - "properties": { - "name": { - "description": "a string to be decoded into a number", - "type": "string" + type: "object", + required: ["name", "categories"], + properties: { + name: { + description: "a string to be decoded into a number", + type: "string" }, - "categories": { - "type": "array", - "items": { - "$ref": "#/$defs/IDEncodedBound" + categories: { + type: "array", + items: { + $ref: "#/$defs/IDEncodedBound" } } }, - "additionalProperties": false, - "$defs": { - "IDEncodedBound": { - "type": "object", - "required": [ - "name", - "categories" - ], - "properties": { - "name": { - "description": "a string to be decoded into a number", - "type": "string" + additionalProperties: false, + $defs: { + IDEncodedBound: { + type: "object", + required: ["name", "categories"], + properties: { + name: { + description: "a string to be decoded into a number", + type: "string" }, - "categories": { - "type": "array", - "items": { - "$ref": "#/$defs/IDEncodedBound" + categories: { + type: "array", + items: { + $ref: "#/$defs/IDEncodedBound" } } }, - "additionalProperties": false + additionalProperties: false } } }) expectJSONSchemaProperty(Schema.encodedSchema(schema), { - "type": "object", - "required": [ - "name", - "categories" - ], - "properties": { - "name": { - "description": "a string to be decoded into a number", - "type": "string" + type: "object", + required: ["name", "categories"], + properties: { + name: { + description: "a string to be decoded into a number", + type: "string" }, - "categories": { - "type": "array", - "items": { - "$ref": "#/$defs/IDEncoded" + categories: { + type: "array", + items: { + $ref: "#/$defs/IDEncoded" } } }, - "additionalProperties": false, - "$defs": { - "IDEncoded": { - "type": "object", - "required": [ - "name", - "categories" - ], - "properties": { - "name": { - "description": "a string to be decoded into a number", - "type": "string" + additionalProperties: false, + $defs: { + IDEncoded: { + type: "object", + required: ["name", "categories"], + properties: { + name: { + description: "a string to be decoded into a number", + type: "string" }, - "categories": { - "type": "array", - "items": { - "$ref": "#/$defs/IDEncoded" + categories: { + type: "array", + items: { + $ref: "#/$defs/IDEncoded" } } }, - "additionalProperties": false + additionalProperties: false } } }) @@ -4727,11 +5225,17 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` it("Transformation", () => { const expected = { - "type": "string", - "description": "a string to be decoded into a number" + type: "string", + description: "a string to be decoded into a number" } - expectJSONSchemaProperty(Schema.encodedBoundSchema(Schema.NumberFromString), expected) - expectJSONSchemaProperty(Schema.encodedSchema(Schema.NumberFromString), expected) + expectJSONSchemaProperty( + Schema.encodedBoundSchema(Schema.NumberFromString), + expected + ) + expectJSONSchemaProperty( + Schema.encodedSchema(Schema.NumberFromString), + expected + ) }) }) }) @@ -4743,256 +5247,199 @@ details: Cannot encode Symbol(effect/Schema/test/a) key to JSON Schema` defect: Schema.Defect }) expectJSONSchemaProperty(schema, { - "$schema": "http://json-schema.org/draft-07/schema#", - "$defs": { - "CauseEncoded0": { - "anyOf": [ + $schema: "http://json-schema.org/draft-07/schema#", + $defs: { + CauseEncoded0: { + anyOf: [ { - "type": "object", - "required": [ - "_tag" - ], - "properties": { - "_tag": { - "type": "string", - "enum": [ - "Empty" - ] + type: "object", + required: ["_tag"], + properties: { + _tag: { + type: "string", + enum: ["Empty"] } }, - "additionalProperties": false + additionalProperties: false }, { - "type": "object", - "required": [ - "_tag", - "error" - ], - "properties": { - "_tag": { - "type": "string", - "enum": [ - "Fail" - ] + type: "object", + required: ["_tag", "error"], + properties: { + _tag: { + type: "string", + enum: ["Fail"] }, - "error": { - "type": "string" + error: { + type: "string" } }, - "additionalProperties": false + additionalProperties: false }, { - "type": "object", - "required": [ - "_tag", - "defect" - ], - "properties": { - "_tag": { - "type": "string", - "enum": [ - "Die" - ] + type: "object", + required: ["_tag", "defect"], + properties: { + _tag: { + type: "string", + enum: ["Die"] }, - "defect": { - "$ref": "#/$defs/Defect" + defect: { + $ref: "#/$defs/Defect" } }, - "additionalProperties": false + additionalProperties: false }, { - "type": "object", - "required": [ - "_tag", - "fiberId" - ], - "properties": { - "_tag": { - "type": "string", - "enum": [ - "Interrupt" - ] + type: "object", + required: ["_tag", "fiberId"], + properties: { + _tag: { + type: "string", + enum: ["Interrupt"] }, - "fiberId": { - "$ref": "#/$defs/FiberIdEncoded" + fiberId: { + $ref: "#/$defs/FiberIdEncoded" } }, - "additionalProperties": false + additionalProperties: false }, { - "type": "object", - "required": [ - "_tag", - "left", - "right" - ], - "properties": { - "_tag": { - "type": "string", - "enum": [ - "Sequential" - ] + type: "object", + required: ["_tag", "left", "right"], + properties: { + _tag: { + type: "string", + enum: ["Sequential"] }, - "left": { - "$ref": "#/$defs/CauseEncoded0" + left: { + $ref: "#/$defs/CauseEncoded0" }, - "right": { - "$ref": "#/$defs/CauseEncoded0" + right: { + $ref: "#/$defs/CauseEncoded0" } }, - "additionalProperties": false + additionalProperties: false }, { - "type": "object", - "required": [ - "_tag", - "left", - "right" - ], - "properties": { - "_tag": { - "type": "string", - "enum": [ - "Parallel" - ] + type: "object", + required: ["_tag", "left", "right"], + properties: { + _tag: { + type: "string", + enum: ["Parallel"] }, - "left": { - "$ref": "#/$defs/CauseEncoded0" + left: { + $ref: "#/$defs/CauseEncoded0" }, - "right": { - "$ref": "#/$defs/CauseEncoded0" + right: { + $ref: "#/$defs/CauseEncoded0" } }, - "additionalProperties": false + additionalProperties: false } ], - "title": "CauseEncoded" + title: "CauseEncoded" }, - "Defect": { - "$id": "/schemas/unknown", - "title": "unknown" + Defect: { + $id: "/schemas/unknown", + title: "unknown" }, - "FiberIdEncoded": { - "anyOf": [ + FiberIdEncoded: { + anyOf: [ { - "$ref": "#/$defs/FiberIdNoneEncoded" + $ref: "#/$defs/FiberIdNoneEncoded" }, { - "$ref": "#/$defs/FiberIdRuntimeEncoded" + $ref: "#/$defs/FiberIdRuntimeEncoded" }, { - "$ref": "#/$defs/FiberIdCompositeEncoded" + $ref: "#/$defs/FiberIdCompositeEncoded" } ] }, - "FiberIdNoneEncoded": { - "type": "object", - "required": [ - "_tag" - ], - "properties": { - "_tag": { - "type": "string", - "enum": [ - "None" - ] + FiberIdNoneEncoded: { + type: "object", + required: ["_tag"], + properties: { + _tag: { + type: "string", + enum: ["None"] } }, - "additionalProperties": false + additionalProperties: false }, - "FiberIdRuntimeEncoded": { - "type": "object", - "required": [ - "_tag", - "id", - "startTimeMillis" - ], - "properties": { - "_tag": { - "type": "string", - "enum": [ - "Runtime" - ] + FiberIdRuntimeEncoded: { + type: "object", + required: ["_tag", "id", "startTimeMillis"], + properties: { + _tag: { + type: "string", + enum: ["Runtime"] }, - "id": { - "$ref": "#/$defs/Int" + id: { + $ref: "#/$defs/Int" }, - "startTimeMillis": { - "$ref": "#/$defs/Int" + startTimeMillis: { + $ref: "#/$defs/Int" } }, - "additionalProperties": false + additionalProperties: false }, - "Int": { - "type": "integer", - "description": "an integer", - "title": "int" + Int: { + type: "integer", + description: "an integer", + title: "int" }, - "FiberIdCompositeEncoded": { - "type": "object", - "required": [ - "_tag", - "left", - "right" - ], - "properties": { - "_tag": { - "type": "string", - "enum": [ - "Composite" - ] + FiberIdCompositeEncoded: { + type: "object", + required: ["_tag", "left", "right"], + properties: { + _tag: { + type: "string", + enum: ["Composite"] }, - "left": { - "$ref": "#/$defs/FiberIdEncoded" + left: { + $ref: "#/$defs/FiberIdEncoded" }, - "right": { - "$ref": "#/$defs/FiberIdEncoded" + right: { + $ref: "#/$defs/FiberIdEncoded" } }, - "additionalProperties": false + additionalProperties: false } }, - "anyOf": [ + anyOf: [ { - "type": "object", - "required": [ - "_tag", - "cause" - ], - "properties": { - "_tag": { - "type": "string", - "enum": [ - "Failure" - ] + type: "object", + required: ["_tag", "cause"], + properties: { + _tag: { + type: "string", + enum: ["Failure"] }, - "cause": { - "$ref": "#/$defs/CauseEncoded0" + cause: { + $ref: "#/$defs/CauseEncoded0" } }, - "additionalProperties": false + additionalProperties: false }, { - "type": "object", - "required": [ - "_tag", - "value" - ], - "properties": { - "_tag": { - "type": "string", - "enum": [ - "Success" - ] + type: "object", + required: ["_tag", "value"], + properties: { + _tag: { + type: "string", + enum: ["Success"] }, - "value": { - "type": "number" + value: { + type: "number" } }, - "additionalProperties": false + additionalProperties: false } ], - "title": "ExitEncoded" + title: "ExitEncoded" }) }) }) diff --git a/repos/effect/packages/platform-bun/CHANGELOG.md b/repos/effect/packages/platform-bun/CHANGELOG.md index 21362c5..801e6f8 100644 --- a/repos/effect/packages/platform-bun/CHANGELOG.md +++ b/repos/effect/packages/platform-bun/CHANGELOG.md @@ -1,5 +1,13 @@ # @effect/platform-bun +## 0.90.0 + +### Patch Changes + +- Updated dependencies [[`26e1922`](https://github.com/Effect-TS/effect/commit/26e19228e1422decbe11ef58e29757f013d96fc8)]: + - @effect/cluster@0.59.0 + - @effect/platform-node-shared@0.60.0 + ## 0.89.0 ### Patch Changes diff --git a/repos/effect/packages/platform-bun/package.json b/repos/effect/packages/platform-bun/package.json index ea6d0ef..401b4a3 100644 --- a/repos/effect/packages/platform-bun/package.json +++ b/repos/effect/packages/platform-bun/package.json @@ -1,7 +1,7 @@ { "name": "@effect/platform-bun", "type": "module", - "version": "0.89.0", + "version": "0.90.0", "license": "MIT", "description": "Platform specific implementations for the Bun runtime", "homepage": "https://effect.website", diff --git a/repos/effect/packages/platform-node-shared/CHANGELOG.md b/repos/effect/packages/platform-node-shared/CHANGELOG.md index fb1ef20..3cac247 100644 --- a/repos/effect/packages/platform-node-shared/CHANGELOG.md +++ b/repos/effect/packages/platform-node-shared/CHANGELOG.md @@ -1,5 +1,12 @@ # @effect/platform-node-shared +## 0.60.0 + +### Patch Changes + +- Updated dependencies [[`26e1922`](https://github.com/Effect-TS/effect/commit/26e19228e1422decbe11ef58e29757f013d96fc8)]: + - @effect/cluster@0.59.0 + ## 0.59.0 ### Patch Changes diff --git a/repos/effect/packages/platform-node-shared/package.json b/repos/effect/packages/platform-node-shared/package.json index 5425b54..129b400 100644 --- a/repos/effect/packages/platform-node-shared/package.json +++ b/repos/effect/packages/platform-node-shared/package.json @@ -1,7 +1,7 @@ { "name": "@effect/platform-node-shared", "type": "module", - "version": "0.59.0", + "version": "0.60.0", "license": "MIT", "description": "Unified interfaces for common platform-specific services", "homepage": "https://effect.website", diff --git a/repos/effect/packages/platform-node/CHANGELOG.md b/repos/effect/packages/platform-node/CHANGELOG.md index f89937e..28033d0 100644 --- a/repos/effect/packages/platform-node/CHANGELOG.md +++ b/repos/effect/packages/platform-node/CHANGELOG.md @@ -1,5 +1,13 @@ # @effect/platform-node +## 0.107.0 + +### Patch Changes + +- Updated dependencies [[`26e1922`](https://github.com/Effect-TS/effect/commit/26e19228e1422decbe11ef58e29757f013d96fc8)]: + - @effect/cluster@0.59.0 + - @effect/platform-node-shared@0.60.0 + ## 0.106.0 ### Patch Changes diff --git a/repos/effect/packages/platform-node/package.json b/repos/effect/packages/platform-node/package.json index 18d83a3..07deeed 100644 --- a/repos/effect/packages/platform-node/package.json +++ b/repos/effect/packages/platform-node/package.json @@ -1,7 +1,7 @@ { "name": "@effect/platform-node", "type": "module", - "version": "0.106.0", + "version": "0.107.0", "license": "MIT", "description": "Platform specific implementations for the Node.js runtime", "homepage": "https://effect.website", diff --git a/repos/effect/packages/sql-clickhouse/CHANGELOG.md b/repos/effect/packages/sql-clickhouse/CHANGELOG.md index dc4e477..e7ef6ca 100644 --- a/repos/effect/packages/sql-clickhouse/CHANGELOG.md +++ b/repos/effect/packages/sql-clickhouse/CHANGELOG.md @@ -1,5 +1,13 @@ # @effect/sql-clickhouse +## 0.49.0 + +### Patch Changes + +- Updated dependencies []: + - @effect/platform-node@0.107.0 + - @effect/experimental@0.60.0 + ## 0.48.0 ### Patch Changes diff --git a/repos/effect/packages/sql-clickhouse/package.json b/repos/effect/packages/sql-clickhouse/package.json index 2dc2c17..bf6de7c 100644 --- a/repos/effect/packages/sql-clickhouse/package.json +++ b/repos/effect/packages/sql-clickhouse/package.json @@ -1,6 +1,6 @@ { "name": "@effect/sql-clickhouse", - "version": "0.48.0", + "version": "0.49.0", "type": "module", "license": "MIT", "description": "A Clickhouse toolkit for Effect", diff --git a/repos/effect/packages/workflow/CHANGELOG.md b/repos/effect/packages/workflow/CHANGELOG.md index b64aaba..2ac72de 100644 --- a/repos/effect/packages/workflow/CHANGELOG.md +++ b/repos/effect/packages/workflow/CHANGELOG.md @@ -1,5 +1,14 @@ # @effect/workflow +## 0.18.2 + +### Patch Changes + +- [#6241](https://github.com/Effect-TS/effect/pull/6241) [`e5998a4`](https://github.com/Effect-TS/effect/commit/e5998a45f69960b38eb2b8cb67cbb07b9e6962c7) Thanks @jcalem-rogo! - Forward the parent pointer when spawning a child workflow with `discard: true` + +- Updated dependencies []: + - @effect/experimental@0.60.0 + ## 0.18.1 ### Patch Changes diff --git a/repos/effect/packages/workflow/package.json b/repos/effect/packages/workflow/package.json index ea55389..6d1ec69 100644 --- a/repos/effect/packages/workflow/package.json +++ b/repos/effect/packages/workflow/package.json @@ -1,7 +1,7 @@ { "name": "@effect/workflow", "type": "module", - "version": "0.18.1", + "version": "0.18.2", "description": "Durable workflows for Effect", "publishConfig": { "access": "public",