@mimlet/effect
v0.1.0-beta.8
Published
Native schema fixtures preserving input/output semantics and builder capabilities.
Downloads
2,216
Maintainers
Readme
Native Effect fixtures
This package supports Effect 4: its peer range is effect >=4.0.0 <5, and each
release from 4.0.0 through 4.0.1 is tested against the packed adapter. Effect 4
replaced the bundled fast-check with its own effect/Arbitrary engine, which Effect
marks as unstable, so a newer Effect 4 release can change generated values. The
adapter's metadata names the loaded Effect release, and mimlet doctor reports an
untested release as PEER_VERSION_UNTESTED. Newer releases usually work; pin a
tested release when you need a tested setup, and report problems in the
issue tracker. Toolkit 0.1.0-alpha.3
and earlier target [email protected]; Effect 3 projects should stay on that train.
Pin exact versions when you need to reproduce fixtures; see Getting started.
# Effect 4
npm install --save-dev @mimlet/core @mimlet/effect [email protected]
# Effect 3
npm install --save-dev @mimlet/[email protected] @mimlet/[email protected] [email protected]Loaded with Effect 3, this release throws a TypeError that names the required
version, and mimlet doctor reports PEER_VERSION_UNSUPPORTED. Effect 4's own type
declarations reference DOM types and Disposable. Without skipLibCheck, a project
needs the DOM lib plus either the ESNext.Disposable lib or @types/node; either of
those alone is not enough in a project without DOM.
import * as S from 'effect/Schema';
import { createSession } from '@mimlet/core';
import { fromEffect } from '@mimlet/effect';
const schema = S.Struct({ age: S.NumberFromString });
const session = createSession({ seed: 123, fingerprint: 'person/v1', provider: '[email protected]' });
const people = fromEffect(schema);
const input = people.with({ age: '42' }).build(session);
const output = people.with({ age: '42' }).buildValidated(session);A native arbitrary generates a decoded output and the original encoder turns
it into fixture input. This retains native declarations and transformation rules
instead of forcing them through JSON. fromEffectAsync uses the native asynchronous
encoder; use it for codecs with asynchronous encoding. Synchronous builds over such a
codec raise a TypeError that points to fromEffectAsync. fromEffectFactory accepts an explicit input factory for one-way codecs,
unsupported arbitrary derivations and application-specific fixture logic.
Generated values come from Effect's arbitrary engine, which favors edge cases:
strings such as __proto__, toString, empty strings, lone surrogates and control
characters, and dates at the limits of the Date range. For presentable or
persisted data, set fields with .with() or use fromEffectFactory. Annotate the
factory's session parameter; unannotated, TypeScript infers it as never.
import type { GenerationSession } from '@mimlet/core';
import { fromEffectFactory } from '@mimlet/effect';
const adults = fromEffectFactory(schema, (session: GenerationSession) => ({
age: String(session.integer(18, 99)),
}));
const adult = adults.buildValidated(session);A generic helper's return type can stay inferred. When a lint rule such as
@typescript-eslint/explicit-function-return-type requires one, name the builder
for a Schema.Codec<A, I>: fromEffect(schema) returns EffectBuilder<A, I> and
fromEffectFactory(schema, factory) returns EffectFactoryBuilder<A, I, F>, where
F is the factory's type, for example (session: GenerationSession) => I.
fromEffectAsync(schema) returns AsyncSchemaBuilder<I, A, [session: GenerationSession]>
from @mimlet/core. Each type is exactly what its function returns. While I is
unresolved, TypeScript cannot tell whether the factory returns a promise, so call
.with() and .withFactory() on a factory builder where the helper is called.
effectFields(schema) lists a struct's top-level keys as encoded, which is what
builders take: a key renamed with Schema.encodeKeys is listed by its encoded name.
Pass it to fluent() from @mimlet/core for a withX() setter per field. It reads
the schema's AST only, so it works with every entry point and in a generic helper:
import { fluent } from '@mimlet/core';
import type * as S from 'effect/Schema';
import { effectFields, fromEffect } from '@mimlet/effect';
function rows<A, I extends object>(schema: S.Codec<A, I>) {
return fluent(fromEffect(schema), effectFields(schema));
}
const person = rows(schema).withAge('42').buildValidated(session);Non-struct schemas such as unions throw a TypeError; symbol keys get no setter, and
structs with index signatures (records, StructWithRest) get no typed setters. See
named setters.
effectAdapter exposes the original source, native Standard Schema validation,
input/output checks, sync/async encode/decode, and native Effect 4 input/output
arbitraries. Input checking checks the encoded shape, not the success of a subsequent
decode. Input-arbitrary shrinking re-encodes each native output shrink. Property tests
run through Effect's own Arbitrary.checkEffect, which shrinks and returns a replay
token; these arbitraries are not fast-check arbitraries and are not passed to
@mimlet/fast-check.
import * as A from 'effect/Arbitrary';
import * as E from 'effect/Effect';
import * as S from 'effect/Schema';
import { effectAdapter } from '@mimlet/effect';
const counts = effectAdapter(S.NumberFromString.pipe(S.decodeTo(S.Int)));
const report = E.runSync(
A.checkEffect(counts.inputArbitrary(), (input) => Number(input) < 5, { seed: 1 })
);
// A failing report carries shrunkInput and a replay token for checkEffect({ replay }).inputArbitrary() encodes each sample synchronously, so it cannot be used with
codecs that encode asynchronously. For those, check outputArbitrary() with an
effectful property that calls adapter.encodeAsync, and run it with E.runPromise.
Transformations and arbitrary annotations must remain pure and terminating for property testing.
Encoding failures propagate, not trigger hidden retries. Validation invokes the
native Standard Schema entry once per validated build, after builder overrides.
The original schema owns parsing behavior; excess object properties default to
errors and can be configured through parseOptions. Each adapter converts its own
Standard Schema wrapper, so two adapters over one schema keep their own parse
options and the caller's schema object is not modified.
Schemas requiring Effect services are not accepted by these constructors; provide
services in your own factory/validation wrapper. This restriction is type-tested.
Async schema execution is supported through async build methods, not detected by
inspecting private AST nodes. A schema whose native generation is asynchronous raises
a TypeError from synchronous builds that points to fromEffectAsync. Native failures and issues remain available and may
contain application values; no reports are logged or transmitted by this package.
Sessions are explicit and caller-versioned: include the schema, annotations,
codec behavior and native dependency versions in your replay identity. Unlike the
JSON Schema-based adapters, there is no default session. fromEffect and
fromEffectAsync require one for every build and list call; omitting it is a type
error and, from JavaScript, raises a TypeError before any generation. To opt in,
pass your own defaultSession, such as () => createSession({ seed: 1, fingerprint:
'person/v1', provider: '[email protected]' }); the builders and fromEffectFactory type it,
and builds may then omit the session. Every
native sampling option (count, size, discards and seed) is passed explicitly, so
Arbitrary.configureGlobal cannot change generated fixtures, and the adapter does
not modify native configuration. Effect does not promise identical samples across
releases; include the Effect version (for example [email protected], also in
effectAdapter(schema).metadata.version) in your session provider. Native schemas/annotations are
trusted code; this is not an interruptible worker or a guarantee of arbitrary
termination for opaque user filters.
Native peer declarations use Web platform types. Their compatibility consumers
include DOM types (and Node types for ArkType); this does not add DOM dependencies
to the schema-free core. The pinned Zod Standard Schema implementation probes
async refinements synchronously before retrying them asynchronously. The generic
standards path calls its validator entry once but does not override that native
behavior. Use an explicit safeParseAsync validation wrapper for effectful Zod
refinements that must not be probed twice.
