diff --git a/.vscode/common-imports.code-snippets b/.vscode/common-imports.code-snippets index fc5e4ef..12db0c0 100644 --- a/.vscode/common-imports.code-snippets +++ b/.vscode/common-imports.code-snippets @@ -37,17 +37,10 @@ "Model": { "prefix": "+MO", "body": [ - "import * as MO from \"@effect-ts-demo/core/ext/Model\"" + "import * as MO from \"@effect-ts-demo/core/ext/Schema\"" ], "description": "Schema as MO" }, - "Morphic": { - "prefix": "+MOR", - "body": [ - "import * as MO from \"@effect-ts-demo/core/ext/Morphic\"" - ], - "description": "Morphic as MO" - }, "Effect": { "prefix": "+T", "body": [ diff --git a/.vscode/morphic.code-snippets b/.vscode/morphic.code-snippets deleted file mode 100644 index 00c5242..0000000 --- a/.vscode/morphic.code-snippets +++ /dev/null @@ -1,85 +0,0 @@ -{ - "Morphic opaque": { - "prefix": "moo", - "body": [ - "export interface $1 extends AType {}", - "export interface $1E extends EType {}", - "export const $1 = opaque<$1E, $1>()($1_)", - "" - ], - "description": "Defines a Morphic opaque signature" - }, - "Morphic": { - "prefix": "mo", - "body": [ - "const $1_ = make(F => F.$0 )", - "" - ], - "description": "Defines a Morphic interface" - }, - "Import Morphic": { - "prefix": "+MO", - "body": [ - "import * as MO from \"@effect-ts/morphic\"" - ], - "description": "Imports Morphic as MO" - }, - "Morphic Newtype": { - "prefix": "mont", - "body": [ - "export interface $1 extends NT.Newtype<\"$1\", $2> {}", - "", - "export const $1Iso = Iso.newtype<$1>()", - "", - "export const $1 = make((F) => F.newtypeIso($1Iso, F.$3()))", - "" - ], - "description": "Defines a Morphic newtype via iso" - }, - "Morphic Newtype Prism": { - "prefix": "mont-pri", - "body": [ - "export interface $1 extends NT.Newtype<\"$1\", $2> {}", - "", - "export const $1Prism = Prism.newtype<$1>((_) => $3)", - "export const $1Iso = Iso.newtype<$1>()", - "", - "export const $1 = make((F) => F.newtypePrism($1Prism, F.$4()))", - "" - ], - "description": "Defines a Morphic newtype via prism" - }, - "Morphic interface": { - "prefix": "moi", - "body": [ - "const $1_ = make(F => F.interface({ $0 }, { name: \"$1\" }))", - "", - "export interface $1 extends AType {}", - "export interface $1E extends EType {}", - "export const $1 = opaque<$1E, $1>()($1_)", - "" - ], - "description": "Defines a Morphic interface" - }, - "Morphic intersection": { - "prefix": "mo&", - "body": [ - "const $1_ = make(F => F.intersection($0)({ name: \"$1\" }))", - "", - "export interface $1 extends AType {}", - "export interface $1E extends EType {}", - "export const $1 = opaque<$1E, $1>()($1_)", - "" - ], - "description": "Defines a Morphic intersection" - }, - "Morphic opaque for tagged unions": { - "prefix": "mo|", - "body": [ - "export const $1 = makeADT('$2')({ $3 })", - "export type $1 = AType", - "" - ], - "description": "Defines a Morphic tagged union" - } -} diff --git a/apps/api/Tasks/shared.ts b/apps/api/Tasks/shared.ts index 15cf71a..91a880e 100644 --- a/apps/api/Tasks/shared.ts +++ b/apps/api/Tasks/shared.ts @@ -4,7 +4,6 @@ import { NotLoggedInError } from "@effect-ts-demo/infra/errors" import { UserSVC } from "@effect-ts-demo/infra/services" import { TaskList, UserId } from "@effect-ts-demo/todo-types/" import * as T from "@effect-ts/core/Effect" -import { AType, M } from "@effect-ts/morphic" import { SchemaAny } from "@effect-ts/schema" import { Chunk } from "@effect-ts/system/Collections/Immutable/Chunk" @@ -27,14 +26,6 @@ export const getLoggedInUser = T.gen(function* ($) { ) }) -export function makeHandler< - TReq extends M<{}, any, any>, - TRes extends M<{}, any, any> ->(_: { Request: TReq; Response: TRes }) { - // TODO: Prevent over providing, although strict encoding removes it already. - return (h: (r: AType) => T.Effect>) => h -} - export function handle< TReq extends { Model: SchemaAny }, TRes extends { Model: SchemaAny } | SchemaAny = typeof S.Void diff --git a/apps/api/package.json b/apps/api/package.json index 9e82db5..509a6df 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -11,7 +11,6 @@ "@effect-ts/core": "^0.39.0", "@effect-ts/express": "^0.19.0", "@effect-ts/monocle": "^0.32.0", - "@effect-ts/morphic": "^0.35.0", "@effect-ts/node": "^0.24.0", "@effect-ts/schema": "^0.7.1", "body-parser": "^1.19.0", diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 598a202..8212e34 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -10,7 +10,6 @@ "@effect-ts-demo/todo-types": "*", "@effect-ts/core": "^0.39.0", "@effect-ts/monocle": "^0.32.0", - "@effect-ts/morphic": "^0.35.0", "@effect-ts/react": "^0.2.0", "@effect-ts/schema": "^0.7.1", "@emotion/react": "^11.4.0", diff --git a/packages/atlas-plutus/package.json b/packages/atlas-plutus/package.json index b61a640..f93e6a5 100644 --- a/packages/atlas-plutus/package.json +++ b/packages/atlas-plutus/package.json @@ -12,6 +12,7 @@ "lint": "eslint . --ext .ts,.tsx" }, "dependencies": { + "@effect-ts/morphic": "^0.35.0", "redoc-express": "^1.0.0" } } diff --git a/packages/client/fetch.ts b/packages/client/fetch.ts index d7d4256..dc11457 100644 --- a/packages/client/fetch.ts +++ b/packages/client/fetch.ts @@ -11,9 +11,6 @@ import * as S from "@effect-ts-demo/core/ext/Schema" import * as H from "@effect-ts-demo/core/http/http-client" import { pipe } from "@effect-ts/core" import { flow } from "@effect-ts/core/Function" -import { M } from "@effect-ts/morphic" -import { Decode, decode, Errors } from "@effect-ts/morphic/Decoder" -import * as MO from "@effect-ts/morphic/Encoder" import { Path } from "path-parser" import { getConfig } from "./config" @@ -28,7 +25,6 @@ export class ResponseError { constructor(public readonly error: unknown) {} } -export const mapResponseError = T.mapError((err: Errors) => new ResponseError(err)) export const mapResponseErrorS = T.mapError((err: unknown) => new ResponseError(err)) export function fetchApi(method: H.Method, path: string, body?: unknown) { @@ -44,21 +40,6 @@ export function fetchApi(method: H.Method, path: string, body?: unknown) { ) } -type Encode = MO.Encoder["encode"] - -export function fetchApi2( - encodeRequest: Encode, - decodeResponse: Decode -) { - const decodeRes = flow(decodeResponse, mapResponseError) - return (method: H.Method, path: string) => (req: RequestA) => - pipe( - encodeRequest(req), - T.chain((r) => fetchApi(method, path, r)), - T.chain(decodeRes) - ) -} - type ComputeUnlessClass = T extends { new (...args: any[]): any } ? T : Compute export function fetchApi2S( @@ -79,23 +60,6 @@ export function fetchApi2S( ) } -export function fetchApi3( - { - Request, - Response, - }: { - // eslint-disable-next-line @typescript-eslint/ban-types - Request: M<{}, RequestE, RequestA> - // eslint-disable-next-line @typescript-eslint/ban-types - Response: M<{}, ResponseE, ResponseA> - }, - method: H.Method = "POST" -) { - const encodeRequest = MO.encode(Request) - const decodeResponse = decode(Response) - return (path: string) => fetchApi2(encodeRequest, decodeResponse)(method, path) -} - // TODO: validate headers vs path vs body vs query? export function fetchApi3S({ Request, diff --git a/packages/client/package.json b/packages/client/package.json index 7794b89..c74b53b 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -8,7 +8,6 @@ "@effect-ts-demo/todo-types": "*", "@effect-ts/core": "^0.39.0", "@effect-ts/monocle": "^0.32.0", - "@effect-ts/morphic": "^0.35.0", "cross-fetch": "^3.1.4", "path-parser": "^6.1.0" }, diff --git a/packages/core/ext/Morphic/array.ts b/packages/core/ext/Morphic/array.ts deleted file mode 100644 index 7565544..0000000 --- a/packages/core/ext/Morphic/array.ts +++ /dev/null @@ -1,11 +0,0 @@ -import * as A from "@effect-ts/core/Collections/Immutable/Array" -import * as O from "@effect-ts/core/Option" - -import { flow } from "../Function" -import * as T from "../Sync" -import "@effect-ts/core/Operator" - -export function interpretArray(decode: (i: unknown) => T.IO) { - return (ar: A.Array) => - A.filterMap_(ar, flow(decode, T.runEither, O.fromEither)) -} diff --git a/packages/core/ext/Morphic/index.ts b/packages/core/ext/Morphic/index.ts deleted file mode 100644 index fff10d3..0000000 --- a/packages/core/ext/Morphic/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./model" -export * from "./model.types" -export * from "./array" -export * from "./set" diff --git a/packages/core/ext/Morphic/model.ts b/packages/core/ext/Morphic/model.ts deleted file mode 100644 index fa59841..0000000 --- a/packages/core/ext/Morphic/model.ts +++ /dev/null @@ -1,655 +0,0 @@ -/* eslint-disable @typescript-eslint/ban-types */ -/* eslint-disable @typescript-eslint/no-explicit-any */ -// TODO: Convert to @effect-ts/morphic - -import * as A from "@effect-ts/core/Collections/Immutable/Array" -import * as O from "@effect-ts/core/Option" -import { - AType, - EType, - DecoderURI, - M, - make, - opaque as opaqueOriginal, -} from "@effect-ts/morphic" -import { - Context, - ContextEntry, - decoder, - Decoder, - Errors, - Validate, - Validation, -} from "@effect-ts/morphic/Decoder" -import { encoder } from "@effect-ts/morphic/Encoder" -import * as EQ from "@effect-ts/morphic/Equal" -import * as FC from "@effect-ts/morphic/FastCheck" -import * as Guard from "@effect-ts/morphic/Guard" -import * as Show from "@effect-ts/morphic/Show" -import { strict } from "@effect-ts/morphic/Strict" -import { strictDecoder } from "@effect-ts/morphic/StrictDecoder" -import short from "short-uuid" -import { v4 } from "uuid" - -import { pipe, flow } from "../Function" -import * as Ord from "../Order" -import * as T from "../Sync" -import * as u from "../utils" - -import type { Branded } from "@effect-ts/core/Branded" -import type { UUID } from "@effect-ts/morphic/Algebra/Primitives" - -import "@effect-ts/core/Operator" - -// eslint-disable-next-line @typescript-eslint/ban-types -export const getFunctionName = (f: Function): string => - (f as any).displayName || (f as any).name || `` - -export const deriveEq = EQ.equal -export const deriveFC = FC.arbitrary -export const deriveShow = Show.show -export const deriveGuard = Guard.guard - -export declare type Exact = T & - { - [K in ({ - [K in keyof X]: K - } & - { - [K in keyof T]: never - } & { - [key: string]: never - })[keyof X]]?: never - } - -export const exact = - () => - >(a: AA) => { - return a as T - } - -export const withNiceMessage = - (message: (i: unknown) => string) => - (codec: Decoder) => - withNiceMessage_(codec, message) - -// Fixes that when a message is specified in the parent -// we still try to show the child errors... -export function withNiceMessage_( - codec: Decoder, - message: (i: unknown) => string -) { - const codecc = codec as any as { with(validate: Validate): Decoder } - return codecc.with(function (i, c) { - return pipe( - codec.validate(i, c), - T.mapError((errors) => { - //console.log(JSON.stringify(errors, undefined, 2), c.length, errors[0].value, i) - // When children have errors, report them - // otherwise if parent has errors, report that - // c.length === 1 && - if (errors[0].value != i) { - return errors - } - return [ - { - value: i, - context: c, - message: message(i), - }, - ] - }) - ) - }) -} - -export const withMessage = - (message: (i: unknown) => string) => - (codec: Decoder) => - withMessage_(codec, message) - -// Fixes that when a message is specified in the parent -// we still try to show the child errors... -export function withMessage_(codec: Decoder, message: (i: unknown) => string) { - const codecc = codec as any as { with(validate: Validate): Decoder } - return codecc.with(function (i, c) { - return pipe( - codec.validate(i, c), - T.mapError((errors) => { - //console.log(JSON.stringify(errors, undefined, 2), c.length, errors[0].value, i) - // When children have errors, report them - // otherwise if parent has errors, report that - // c.length === 1 && - if (errors[0].value != i) { - return errors - } - return [ - { - value: i, - context: c, - message: message(i), - }, - ] - }) - ) - }) -} - -export function printErrors(errors: Errors) { - return pipe(formatErrors(errors)).join("\n") -} - -export function formatErrors(errors: Errors) { - return pipe(decodeErrors(errors), A.map(formatError)) -} - -export function formatError({ - expectedType, - message, - path, - provided, - rootType, -}: ValidationErrorEntry) { - return `${describeValue(provided.value)} ${message} at: ${rootType}.${ - path ? `[${path}]: ` : "" - }${expectedType}` -} - -export function decodeErrors(x: Errors) { - return pipe( - x, - A.map(({ message, context: [root, ...rest], value }) => { - const processCtx = (current: ContextEntry, path?: string, rootType?: string) => ({ - message: message ? message : getErrorMessage(current), - expectedType: current.type.name, - rootType, - path, - provided: { - value, - type: typeof value, - constructor: - value && typeof value === "object" - ? ` ${value.constructor.name}` - : undefined, - }, - }) - return rest.length - ? processCtx( - rest[rest.length - 1], - rest - .map((x) => x.key) - // the root object inside an array, then has no key again. - .filter(u.isTruthy) - .join("."), - root.type.name - ) - : processCtx(root) - }) - ) -} - -function getErrorMessage(current: ContextEntry) { - switch (current.type.name) { - case "NonEmptyString": - return "Must not be empty" - } - if (current.type.name?.startsWith("NonEmptyArray<")) { - return "Must not be empty" - } - return `Invalid value specified` -} - -export function stringify(v: unknown) { - if (typeof v === "function") { - return getFunctionName(v) - } - if (typeof v === "number" && !isFinite(v)) { - if (isNaN(v)) { - return "NaN" - } - return v > 0 ? "Infinity" : "-Infinity" - } - return JSON.stringify(v) -} - -export function describeValue(v: unknown) { - return `${stringify(v)} (${typeof v}${ - v && typeof v === "object" ? ` ${v.constructor.name}` : "" - })` -} - -export const toValidationError = flow(decodeErrors, (errors) => - ValidationError.build({ - _tag: "ValidationError", - message: "One or more Validation errors ocurred", - errors, - }) -) - -export const makeStrict = - (dec: (i: unknown, mode?: Mode) => T.IO) => - (i: unknown) => - dec(i, "strict") - -export interface Interpreter { - meta: { - name: string - } - build: (a: A) => A - validate_: (i: unknown, context: Context, strict?: Mode) => T.IO - encode_: (a: A, strict?: Mode) => T.IO - decode_: (i: unknown, strict?: Mode) => T.IO - parse_: (i: E, strict?: Mode) => T.IO - - validate: (strict: Mode) => (i: unknown, context: Context) => T.IO - encode: (strict: Mode) => (a: A) => T.IO - decode: (strict: Mode) => (i: unknown) => T.IO - parse: (strict: Mode) => (i: E) => T.IO -} - -export type HasDecoder = { - decode_: (i: unknown, strict?: Mode) => T.IO - decode: (strict: Mode) => (i: unknown) => T.IO -} - -export type HasEncoder = { - encode_: (i: A, strict?: Mode) => T.UIO - encode: (strict: Mode) => (i: A) => T.UIO -} - -export const ValidationErrorEntry = make((F) => - F.both( - { - message: F.string(), - provided: F.both({ value: F.unknown() }, { constructor: F.string() }), - }, - { - expectedType: F.string(), - path: F.string(), - rootType: F.string(), - } - ) -) -export type ValidationErrorEntry = AType - -const ValidationError_ = make((F) => - F.interface( - { - _tag: F.stringLiteral("ValidationError"), - message: F.string(), - errors: F.array(ValidationErrorEntry(F)), - }, - { name: "ValidationError" } - ) -) -export interface ValidationError extends AType {} -export interface ValidationErrorE extends EType {} - -export const ValidationError = - opaque()(ValidationError_) - -// const asInvalidContract = (context: unknown) => (a: T.IO) => -// pipe( -// a, -// T.mapError((error) => -// as.InvalidState({ message: "Invalid data", details: { context, error } }) -// ) -// ) - -// export const decodeResponse = ( -// decoder: (i: unknown) => T.IO -// ) => (context?: unknown) => (d: unknown) => pipe(decoder(d), asInvalidContract(context)) - -// export const decodeResponseorDie = ( -// decoder: (i: unknown) => T.IO -// ) => { -// const decode = decodeResponse(decoder) -// return (context: unknown) => flow(decode(context), T.orDie) -// } - -type Mode = "classic" | "strict" - -export function ext(m: Interpreter): Extensions { - const exactA = exact() - const exactE = exact() - - // backwards compatibility - const validateM_ = m.validate_ - const parseM_ = m.parse_ - const encodeM_ = m.encode_ - const decodeM_ = m.decode_ - - const parseV_ = flow(m.parse_, T.mapError(toValidationError)) - const parseVM_ = parseV_ - - const decodeV_ = flow(m.decode_, T.mapError(toValidationError)) - const decodeVM_ = decodeV_ - - const validateM = (strict: Mode) => (i: unknown, context: Context) => - validateM_(i, context, strict) - - const parseM = m.parse - const encodeM = m.encode - const decodeM = m.decode - - const parseV = (mode: Mode = "classic") => - flow(m.parse(mode), T.mapError(toValidationError)) - const parseVM = parseV - - const decodeV = (mode: Mode = "classic") => - flow(m.decode(mode), T.mapError(toValidationError)) - const decodeVM = decodeV - - // const decodeRM_ = (d: unknown, mode?: Mode, context?: unknown) => - // decodeResponse(decodeVM(mode))(context)(d) - // const decodeRMorDie_ = (d: unknown, mode?: Mode, context?: unknown) => - // decodeResponseorDie(decodeVM(mode))(context)(d) - - // const decodeRM = (mode: Mode) => decodeResponse(decodeVM(mode)) - // const decodeRMorDie = (mode: Mode) => decodeResponseorDie(decodeVM(mode)) - - return { - /** - * Make sure there are no excess properties - */ - exactA, - - /** - * Make sure there are no excess properties - */ - exactE, - - validateM, - parseM, - encodeM, - decodeM, - /** - * Like decode, but maps error - */ - decodeV, - /** - * Like decodeV, but as Effect - */ - decodeVM, - - // /** - // * Like decodeVM, but maps to InvalidStateError - // */ - // decodeRM, - - // /** - // * Like decodeVM, but maps to InvalidStateError and Aborts - // */ - // decodeRMorDie, - - /** - * Like parse, but maps error - */ - parseV, - /** - * Like parseV, but as Effect - */ - parseVM, - - validateM_, - parseM_, - encodeM_, - decodeM_, - /** - * Like decode, but maps error - */ - decodeV_, - /** - * Like decodeV, but as Effect - */ - decodeVM_, - - // /** - // * Like decodeVM, but maps to InvalidStateError - // */ - // decodeRM_, - - // /** - // * Like decodeVM, but maps to InvalidStateError and Aborts - // */ - // decodeRMorDie_, - - /** - * Like parse, but maps error - */ - parseV_, - /** - * Like parseV, but as Effect - */ - parseVM_, - } -} -export interface Extensions { - /** - * Make sure there are no excess properties - */ - exactA: >(a: AA) => A - /** - * Make sure there are no excess properties - */ - exactE: >(a: EE) => E - - /** - * Like decode, but maps error - */ - decodeV_: (i: unknown, strict?: Mode | undefined) => T.IO - /** - * Like decodeV, but as Effect - */ - decodeVM_: (i: unknown, strict?: Mode | undefined) => T.IO - // /** - // * Like decodeVM, but maps to InvalidStateError - // */ - // decodeRM_: ( - // i: unknown, - // strict?: Mode | undefined, - // context?: unknown - // ) => T.IO - // /** - // * Like decodeVM, but maps to InvalidStateError and Aborts - // */ - // decodeRMorDie_: (i: unknown, strict?: Mode | undefined, context?: unknown) => T.UIO - /** - * Like parse, but maps error - */ - parseV_: (i: E, strict?: Mode | undefined) => T.IO - /** - * Like parseV, but as Effect - */ - parseVM_: (i: E, strict?: Mode | undefined) => T.IO - - validateM_: (i: unknown, context: Context, strict?: Mode) => T.IO - encodeM_: (a: A, strict?: Mode) => T.UIO - decodeM_: (i: unknown, strict?: Mode) => T.IO - parseM_: (i: E, strict?: Mode) => T.IO - - /** - * Like decode, but maps error - */ - decodeV: (strict: Mode) => (i: unknown) => T.IO - /** - * Like decodeV, but as Effect - */ - decodeVM: (strict: Mode) => (i: unknown) => T.IO - // /** - // * Like decodeVM, but maps to InvalidStateError - // */ - // decodeRM: ( - // strict: Mode - // ) => (context?: unknown) => (i: unknown) => T.IO - // /** - // * Like decodeVM, but maps to InvalidStateError and Aborts - // */ - // decodeRMorDie: (strict: Mode) => (context?: unknown) => (i: unknown) => T.UIO - /** - * Like parse, but maps error - */ - parseV: (strict: Mode) => (i: E) => T.IO - /** - * Like parseV, but as Effect - */ - parseVM: (strict: Mode) => (i: E) => T.IO - - validateM: (strict: Mode) => (a: A, context: Context) => T.IO - encodeM: (strict: Mode) => (a: A) => T.UIO - decodeM: (strict: Mode) => (i: unknown) => T.IO - parseM: (strict: Mode) => (i: E) => T.IO -} - -function extn(a: T, ext: X) { - Object.assign(a, ext) - return a as T & X -} - -type GetE = TM extends M<{}, infer E, any> ? E : never -type GetA = TM extends M<{}, any, infer A> ? A : never - -export function extend>(m: TM) { - type E = GetE - type A = GetA - const { decode: decodeClassic, name, validate: validateClassic } = decoder(m) - const { encode: encodeClassic } = encoder(m) - - const str = strict(m) - const { decode: decodeStrict, validate: validateStrict } = strictDecoder(m) - - const encodeStrict = (a: A) => pipe(str.shrink(a), T.chain(encodeClassic)) - - const decode_ = (e: unknown, mode: Mode = "classic") => - mode === "classic" ? decodeClassic(e) : decodeStrict(e) - - const build = (a: A) => a - - const parse_: (e: E, mode?: Mode) => Validation = decode_ - - const encode_ = (a: A, mode: Mode = "classic") => - mode === "classic" ? encodeClassic(a) : encodeStrict(a) - - const validate_ = (e: unknown, context: Context, mode: Mode = "classic") => - mode === "classic" ? validateClassic(e, context) : validateStrict(e, context) - - const validate = (mode: Mode) => (e: unknown, context: Context) => - validate_(e, context, mode) - - const decode = (mode: Mode) => (i: unknown) => decode_(i, mode) - const parse = (mode: Mode) => (e: E) => parse_(e, mode) - const encode = (mode: Mode) => (a: A) => encode_(a, mode) - - if (!name) { - throw new Error("you should really set a name!") - } - - const interpreter: Interpreter = { - meta: { name }, - build, - - validate_, - decode_, - parse_, - encode_, - - validate, - decode, - parse, - encode, - } - const mn = extn(m, interpreter) - return extn(mn, ext(mn)) -} - -export function opaque() { - return (m: M<{}, E, A>) => { - const op = opaqueOriginal()(m) - return extend(op) - } -} - -// eslint-disable-next-line @typescript-eslint/ban-types -export function opaqueA() { - return (m: M<{}, E, A>) => opaque()(m) -} - -export function castBrand() { - return (i: T) => i as Branded -} - -export function createUnorder(): Ord.Ord { - return { - compare: (_a: T, _b: T) => 0, - } -} - -export const StringSameORD = createUnorder() - -export const generateUuidV4: T.UIO = T.succeedWith(() => v4() as UUID) - -const translator = short() -type ShortId = string -export const generateShortId: T.UIO = T.succeedWith(translator.generate) - -// TODO: new service for sync+async -export const getCurrentDate = T.succeedWith(() => new Date()) - -export const create = T.struct({ createdAt: getCurrentDate, id: generateUuidV4 }) -export function createWith(computeState: (cd: Date, id: UUID) => T) { - return pipe( - create, - T.map(({ createdAt, id }) => computeState(createdAt, id)) - ) -} - -export const createShort = T.struct({ - createdAt: getCurrentDate, - id: generateShortId, -}) -export function createWithShort(computeState: (cd: Date, id: ShortId) => T) { - return pipe( - createShort, - T.map(({ createdAt, id }) => computeState(createdAt, id)) - ) -} - -type GetErrorTag = T extends { _errorTag: infer K } ? K : never -export const isOfErrorType = - (tag: GetErrorTag) => - (e: { _errorTag: string }): e is T => - e._errorTag === tag - -type GetTag = T extends { _tag: infer K } ? K : never -export const isOfType = - (tag: GetTag) => - (e: { _tag: string }): e is T => - e._tag === tag - -export function withEmptyStringAsNullable( - codec: Decoder> -) { - return codec.with((u, c) => codec.validate(u === "" ? null : u, c)) -} - -export function makeNullableStringWithFallback( - t: M<{}, E, A> -) { - return make((F) => - F.nullable(t(F), { - conf: { - [DecoderURI]: withEmptyStringAsNullable, - }, - }) - ) -} - -export function makeKeys(a: readonly T[]) { - return a.reduce((prev, cur) => { - prev[cur] = null - return prev - }, {} as { [P in typeof a[number]]: null }) -} - -export type { Errors } -export * from "@effect-ts/morphic" diff --git a/packages/core/ext/Morphic/model.types.ts b/packages/core/ext/Morphic/model.types.ts deleted file mode 100644 index b085308..0000000 --- a/packages/core/ext/Morphic/model.types.ts +++ /dev/null @@ -1,369 +0,0 @@ -import { constVoid } from "@effect-ts/core/Function" -import * as Sy from "@effect-ts/core/Sync" -import { IntBrand, PositiveBrand } from "@effect-ts/schema" - -import { flow, pipe } from "../Function" -import * as V from "../validation" - -import { - AType, - make, - FastCheckURI, - castBrand, - DecoderURI, - extend, - withMessage, - EncoderURI, - opaque, -} from "./model" - -import type { Arbitrary, FC } from "../FastCheck" -import type { Branded } from "@effect-ts/core/Branded" - -export const UUID = make((F) => F.uuid()) -export type UUID = AType - -export type { Branded } - -// TODO: Arbitraries should still be cut/filtered on max and min lengths - -const MIN = 1 -type NonEmptyStringBranded = Branded - -export const isNonEmptyString = (v: string): v is NonEmptyStringBranded => - V.all_(v.length, V.minN(MIN)) - -/** - * A string of Min 1 and Max 256KB characters - */ -export const NonEmptyString = extend( - make((F) => - F.refined(F.string(), isNonEmptyString, { - name: "NonEmptyString", - conf: { - [FastCheckURI]: (_c, fc) => - fc.module - // let's be reasonable - .string({ minLength: MIN }) // DONT DO THIS!!!! maxLength: 256 * 1024 - .map((x) => x as NonEmptyStringBranded), - [DecoderURI]: withMessage(() => "is not a NonEmpty String"), - }, - extensions: { - openapiMeta: { - minLength: MIN, - }, - }, - }) - ) -) -export interface NonEmptyStringBrand { - readonly NonEmptyString: unique symbol -} -export type NonEmptyString = AType - -const REASONABLE_STRING_MAX = 256 - 1 - -export const isReasonableString = ( - v: string -): v is Branded & NonEmptyStringBranded => - V.all_(v.length, V.minN(MIN), V.maxN(REASONABLE_STRING_MAX)) - -export function makeReasonableString( - arbF: ( - c: Arbitrary>, - fc_: FC - ) => Arbitrary -) { - return make((F) => - F.refined(F.string(), isReasonableString, { - name: "ReasonableString", - conf: { - [FastCheckURI]: (c, fc) => - pipe(arbF(c, fc.module), (a) => - a.map( - flow( - (x) => x.substring(0, REASONABLE_STRING_MAX), - (i) => - i as Branded & NonEmptyStringBranded - ) - ) - ), - [DecoderURI]: withMessage( - () => `is not a Reasonable String (${MIN}-${REASONABLE_STRING_MAX})` - ), - }, - extensions: { - openapiMeta: { - minLength: MIN, - maxLength: REASONABLE_STRING_MAX, - }, - }, - }) - ) -} -/** - * A string of Min 1 and Max 255 characters - */ -export const ReasonableString = make((F) => - F.refined(NonEmptyString(F), isReasonableString, { - name: "ReasonableString", - conf: { - [FastCheckURI]: (_c, fc) => - fc.module - .string({ minLength: MIN, maxLength: REASONABLE_STRING_MAX }) - .map( - (x) => x as Branded & NonEmptyStringBranded - ), - [DecoderURI]: withMessage( - () => `is not a Reasonable String (${MIN}-${REASONABLE_STRING_MAX})` - ), - }, - extensions: { - openapiMeta: { - minLength: MIN, - maxLength: REASONABLE_STRING_MAX, - }, - }, - }) -) -export interface ReasonableStringBrand extends NonEmptyStringBrand { - readonly ReasonableString: unique symbol -} -export type ReasonableString = AType - -const LONG_STRING_MAX = 2048 - 1 -export const isLongString = (v: string): v is Branded => - V.all_(v.length, V.minN(MIN), V.maxN(LONG_STRING_MAX)) - -export function makeLongString( - arbF: (c: Arbitrary>, fc_: FC) => Arbitrary -) { - return make((F) => - F.refined(F.string(), isLongString, { - name: "LongString", - conf: { - [FastCheckURI]: (c, fc) => - pipe(arbF(c, fc.module), (a) => - a.map( - flow( - (x) => x.substring(0, LONG_STRING_MAX), - castBrand() - ) - ) - ), - [DecoderURI]: withMessage( - () => - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - `is not a Long String (${MIN}-${LONG_STRING_MAX})` - ), - }, - extensions: { - openapiMeta: { - minLength: MIN, - maxLength: LONG_STRING_MAX, - }, - }, - }) - ) -} - -/** - * A string of Min 1 and Max 2047 characters - */ -export const LongString = makeLongString((_c, fc) => - fc.string({ minLength: MIN, maxLength: LONG_STRING_MAX }) -) -export interface LongStringBrand extends ReasonableStringBrand { - readonly LongString: unique symbol -} -export type LongString = AType - -const MAX_TEXT_STRING = 64 * 1024 -export const isTextString = (v: string): v is Branded => - V.all_(v.length, V.minN(MIN), V.maxN(MAX_TEXT_STRING)) - -export function makeTextString( - arbF: (c: Arbitrary>, fc_: FC) => Arbitrary -) { - return make((F) => - F.refined(F.string(), isTextString, { - name: "TextString", - conf: { - [FastCheckURI]: (c, fc) => - pipe(arbF(c, fc.module), (a) => - a.map( - flow( - (x) => x.substring(0, MAX_TEXT_STRING), - castBrand() - ) - ) - ), - [DecoderURI]: withMessage( - () => - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - `is not a Text String (${MIN}-${MAX_TEXT_STRING})` - ), - }, - extensions: { - openapiMeta: { - minLength: MIN, - maxLength: MAX_TEXT_STRING, - }, - }, - }) - ) -} - -/** - * A string of Min 1 and Max 64kb characters - */ -export const TextString = makeTextString((_c, fc) => - fc.string({ minLength: MIN, maxLength: MAX_TEXT_STRING }) -) -export interface TextStringBrand extends LongStringBrand { - readonly TextString: unique symbol -} -export type TextString = AType - -const MIN_STRING_ID = 6 -const MAX_STRING_ID = 50 -export const isStringId = ( - v: string -): v is Branded & NonEmptyStringBranded => - V.all_(v.length, V.minN(MIN_STRING_ID), V.maxN(MAX_STRING_ID)) - -/** - * A string of Min 6 and Max 50 characters - */ -export const StringId = make((F) => - F.refined(NonEmptyString(F), isStringId, { - name: "StringId", - conf: { - [FastCheckURI]: (_c, fc) => - fc.module - .string({ minLength: MIN_STRING_ID, maxLength: MAX_STRING_ID }) - .map((x) => x as Branded & NonEmptyStringBranded), - [DecoderURI]: withMessage( - () => - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - `is not a StringID (${MIN_STRING_ID}-${MAX_STRING_ID})` - ), - }, - extensions: { - openapiMeta: { - minLength: MIN_STRING_ID, - maxLength: MAX_STRING_ID, - }, - }, - }) -) -export type StringId = AType - -export interface StringIdBrand extends NonEmptyStringBrand { - readonly StringId: unique symbol -} - -type PositiveIntBranded = Branded & IntBrand & PositiveBrand -export const isPositiveInt = (v: number): v is PositiveIntBranded => V.minN(0)(v) - -export function makePositiveInt( - arbF: (c: Arbitrary, fc_: FC) => Arbitrary -) { - return extend( - make((F) => - F.refined(F.number(), isPositiveInt, { - name: "PositiveInt", - conf: { - [FastCheckURI]: (c, fc) => - pipe(arbF(c, fc.module), (a) => - a.map((a) => (isPositiveInt(a) ? a : (0 as PositiveIntBranded))) - ), - }, - extensions: { - openapiMeta: { - minimum: 0, - }, - }, - }) - ) - ) -} - -export const PositiveInt = makePositiveInt((_c, fc) => fc.nat()) -export type PositiveInt = AType -export interface PositiveIntBrand { - readonly PositiveInt: unique symbol -} - -const isPositiveNumber = (v: number): v is Branded => - V.minN(0)(v) - -export const PositiveNumber = make((F) => - F.refined(F.number(), isPositiveNumber, { - name: "PositiveNumber", - conf: { - [FastCheckURI]: (_c, fc) => - fc.module.float(0, 100).map(castBrand()), - }, - extensions: { - openapiMeta: { - minimum: 0, - }, - }, - }) -) -export type PositiveNumber = AType -export interface PositiveNumberBrand { - readonly PositiveNumber: unique symbol -} - -const isPercent = (v: number): v is Branded => - V.all(V.minN(0), V.maxN(100))(v) - -export const Percent = make((F) => - F.refined(F.number(), isPercent, { - name: "Percent", - conf: { - [FastCheckURI]: (_c, fc) => - fc.module.float(0, 100).map(castBrand()), - }, - extensions: { - openapiMeta: { - minimum: 0, - maximum: 100, - }, - }, - }) -) -export type Percent = AType -export interface PercentBrand { - readonly Percent: unique symbol -} - -// TODO -export const TODO = make((F) => F.stringLiteral("TODO")) -export type TODO = AType -export const constTODO = "TODO" as TODO - -export const NonEmptyTextString = makeTextString((_c, fc) => - fc.lorem({ maxCount: 15, mode: "sentences" }) -) - -export const NonEmptyShortString = makeLongString((_c, fc) => fc.lorem({ maxCount: 3 })) - -export const Word = makeReasonableString((_c, fc) => fc.lorem({ maxCount: 1 })) - -const defaultVoid = Sy.succeed(constVoid()) -const defaultVoidThunk = () => defaultVoid -const Void_ = make((F) => - F.unknown({ - conf: { - [DecoderURI]: (codec) => codec.with(defaultVoidThunk), - [EncoderURI]: () => ({ encode: defaultVoidThunk }), - }, - }) -) -export type Void = void - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const Void = opaque()(Void_ as any) diff --git a/packages/core/ext/Schema/_api/set.ts b/packages/core/ext/Schema/_api/set.ts deleted file mode 100644 index e69de29..0000000 diff --git a/packages/core/ext/Morphic/set.ts b/packages/core/ext/Schema/_api/set.ts.bak similarity index 95% rename from packages/core/ext/Morphic/set.ts rename to packages/core/ext/Schema/_api/set.ts.bak index ed14552..91d0711 100644 --- a/packages/core/ext/Morphic/set.ts +++ b/packages/core/ext/Schema/_api/set.ts.bak @@ -101,3 +101,9 @@ export function withSetPreInterpreter( ) ) } + + +export function interpretArray(decode: (i: unknown) => T.IO) { + return (ar: A.Array) => + A.filterMap_(ar, flow(decode, T.runEither, O.fromEither)) +} diff --git a/packages/infra/context/morphic.ts b/packages/infra/context/morphic.ts deleted file mode 100644 index 0a19e0d..0000000 --- a/packages/infra/context/morphic.ts +++ /dev/null @@ -1,35 +0,0 @@ -import * as T from "@effect-ts-demo/core/ext/Effect" -import { M } from "@effect-ts-demo/core/ext/Morphic" -import * as A from "@effect-ts/core/Collections/Immutable/Array" -import * as Map from "@effect-ts/core/Collections/Immutable/Map" -import { flow, pipe } from "@effect-ts/core/Function" -import * as Sy from "@effect-ts/core/Sync" -import { encode } from "@effect-ts/morphic/Encoder" -import { strict } from "@effect-ts/morphic/Strict" -import { strictDecoder } from "@effect-ts/morphic/StrictDecoder" - -//// Helpers -// eslint-disable-next-line @typescript-eslint/ban-types -export function makeCodec(t: M<{}, E, A>) { - const { decode } = strictDecoder(t) - const decodeOrDie = flow(decode, T.orDie) - const encode = strictEncode(t) - const encodeToMap = toMap(encode) - return [decodeOrDie, encode, encodeToMap] as const -} - -// eslint-disable-next-line @typescript-eslint/ban-types -function strictEncode(t: M<{}, E, A>) { - const { shrink } = strict(t) - const enc = encode(t) - return (u: A) => pipe(shrink(u), Sy.chain(enc)) -} - -function toMap(encode: (a: A) => Sy.UIO) { - return (a: A.Array) => - pipe( - A.map_(a, (task) => Sy.tuple(Sy.succeed(task.id as A["id"]), encode(task))), - Sy.collectAll, - Sy.map(Map.make) - ) -} diff --git a/packages/infra/express/makeJsonSchema.ts b/packages/infra/express/makeJsonSchema.ts index 0b0ce34..aebec09 100644 --- a/packages/infra/express/makeJsonSchema.ts +++ b/packages/infra/express/makeJsonSchema.ts @@ -5,7 +5,6 @@ import * as CNK from "@effect-ts/core/Collections/Immutable/Chunk" import * as T from "@effect-ts/core/Effect" import { _A } from "@effect-ts/core/Utils" -import * as RM from "./morphic/routing" import * as RS from "./schema/routing" type Methods = "GET" | "PUT" | "POST" | "PATCH" | "DELETE" @@ -15,17 +14,11 @@ type Methods = "GET" | "PUT" | "POST" | "PATCH" | "DELETE" */ export function makeJsonSchema( // eslint-disable-next-line @typescript-eslint/no-explicit-any - r: Iterable< - | RM.RouteDescriptor - | RS.RouteDescriptor - > + r: Iterable> ) { return pipe( CNK.from(r), - //CNK.filter((x) => x._tag === "Morphic"), - T.forEach((e) => - e._tag === "Morphic" ? RM.makeFromMorphic(e) : RS.makeFromSchema(e) - ), + T.forEach(RS.makeFromSchema), T.map((e) => { const map = ({ method, path, responses, ...rest }: _A) => ({ [method]: { diff --git a/packages/infra/express/morphic/requestHandler.ts b/packages/infra/express/morphic/requestHandler.ts deleted file mode 100644 index 8abbb7f..0000000 --- a/packages/infra/express/morphic/requestHandler.ts +++ /dev/null @@ -1,339 +0,0 @@ -/* eslint-disable @typescript-eslint/ban-types */ -import * as SO from "@effect-ts-demo/core/ext/SyncOption" -import { DSL, Has } from "@effect-ts/core" -import { makeAssociative } from "@effect-ts/core/Associative" -import * as A from "@effect-ts/core/Collections/Immutable/Array" -import * as T from "@effect-ts/core/Effect" -import * as L from "@effect-ts/core/Effect/Layer" -import { flow, pipe } from "@effect-ts/core/Function" -import * as O from "@effect-ts/core/Option" -import * as Sy from "@effect-ts/core/Sync" -import * as EU from "@effect-ts/core/Utils" -import { M } from "@effect-ts/morphic" -import { ContextEntry, Decode, Errors } from "@effect-ts/morphic/Decoder" -import { Encoder, encode } from "@effect-ts/morphic/Encoder" -import { strict } from "@effect-ts/morphic/Strict" -import { strictDecoder } from "@effect-ts/morphic/StrictDecoder" -import express from "express" - -import { NotFoundError, NotLoggedInError } from "../../errors" -import { UserSVC } from "../../services" - -export type Request< - PathA, - CookieA, - QueryA, - BodyA, - HeaderA, - ReqA extends PathA & QueryA & BodyA -> = M<{}, unknown, ReqA> & { - Cookie?: M<{}, Record, CookieA> - Path?: M<{}, Record, PathA> - Body?: M<{}, unknown, BodyA> - Query?: M<{}, Record, QueryA> - Headers?: M<{}, Record, HeaderA> -} -type Encode = Encoder["encode"] - -class ValidationError { - public readonly _tag = "ValidationError" - constructor(public readonly errors: A.Array) {} -} - -export type SupportedErrors = ValidationError | NotFoundError - -function getErrorMessage(current: ContextEntry) { - switch (current.type.name) { - case "NonEmptyString": - return "Must not be empty" - } - if (current.type.name?.startsWith("NonEmptyArray<")) { - return "Must not be empty" - } - return `Invalid value specified` -} -export function decodeErrors(x: Errors) { - return pipe( - x, - A.map(({ message, context: [root, ...rest], value }) => { - const processCtx = (current: ContextEntry, path?: string, rootType?: string) => ({ - message: message ? message : getErrorMessage(current), - expectedType: current.type.name, - rootType, - path, - provided: { - value, - type: typeof value, - constructor: - value && typeof value === "object" - ? ` ${value.constructor.name}` - : undefined, - }, - }) - return rest.length - ? processCtx( - rest[rest.length - 1], - rest - .map((x) => x.key) - // the root object inside an array, then has no key again. - .filter(Boolean) - .join("."), - root.type.name - ) - : processCtx(root) - }) - ) -} - -const ValidationApplicative = T.getValidationApplicative( - makeAssociative }>>( - (l, r) => l.concat(r) - ) -) - -const structValidation = DSL.structF(ValidationApplicative) - -function parseRequestParams( - parsers: RequestParsers -) { - return ({ - body, - cookies, - headers, - method, - originalUrl, - params, - query, - }: express.Request) => - pipe( - T.succeedWith(() => ({ path: params, query, body, headers, cookies })), - T.tap((pars) => - T.succeedWith(() => - console.log( - `${new Date().toISOString()} ${method} ${originalUrl} processing request`, - pars - ) - ) - ), - T.chain(() => { - const result = structValidation( - mapErrors_( - { - body: parsers.parseBody(body), - cookie: parsers.parseCookie(cookies), - headers: parsers.parseHeaders(headers), - query: parsers.parseQuery(query), - path: parsers.parsePath(params), - }, - makeError - ) - ) - return result - }), - T.mapError((err) => new ValidationError(err)) - ) -} - -function mapErrors_>>( - t: NER, // TODO: enforce non empty - mapErrors: (k: keyof NER) => (err: E) => NE -): { - [K in keyof NER]: T.Effect, NE, EU._A> -} { - return typedKeysOf(t).reduce( - (prev, cur) => { - prev[cur] = t[cur]["|>"](T.mapError(mapErrors(cur))) - return prev - }, - {} as { - [K in keyof NER]: T.Effect, NE, EU._A> - } - ) -} - -export const typedKeysOf = (obj: T) => Object.keys(obj) as (keyof T)[] - -function makeError(type: string) { - return (e: Errors) => [{ type, errors: decodeErrors(e) }] -} - -function respondSuccess(encodeResponse: Encode) { - return (res: express.Response) => - flow( - encodeResponse, - T.chain((r) => - T.succeedWith(() => { - r === undefined - ? res.status(204).send() - : res.status(200).send(r === null ? JSON.stringify(null) : r) - }) - ) - ) -} - -function handleRequest( - requestParsers: RequestParsers, - encodeResponse: Encode, - handle: ( - r: PathA & QueryA & BodyA & {} - ) => T.Effect, SupportedErrors, ResA> -) { - const parseRequest = parseRequestParams(requestParsers) - const respond = respondSuccess(encodeResponse) - return (req: express.Request, res: express.Response) => - pipe( - parseRequest(req), - T.chain(({ body, path, query }) => - handle({ - ...O.toUndefined(body), - ...O.toUndefined(query), - ...O.toUndefined(path), - } as PathA & QueryA & BodyA)["|>"]( - // TODO; able to configure only when needed. - T.provideSomeLayer( - UserSVC.LiveUserEnv(req.headers["authorization"] as unknown)["|>"]( - L.mapError(() => new NotLoggedInError()) - ) - ) - ) - ), - T.chain(respond(res)), - T.catch("_tag", "ValidationError", (err) => - T.succeedWith(() => { - res.status(400).send(err.errors) - }) - ), - T.catch("_tag", "NotFoundError", (err) => - T.succeedWith(() => { - res.status(404).send(err) - }) - ), - // final catch all - T.catchAll((err: any) => - T.succeedWith(() => - console.error( - "Program error, compiler probably silenced, got an unsupported Error in Error Channel of Effect", - err - ) - )["|>"](T.chain(T.die)) - ), - T.tapCause(() => T.succeedWith(() => res.status(500).send())) - ) -} - -export interface RequestHandler< - R, - PathA, - CookieA, - QueryA, - BodyA, - HeaderA, - ReqA extends PathA & QueryA & BodyA, - ResA -> { - Request: Request - Response: M<{}, unknown, ResA> - handle: (i: PathA & QueryA & BodyA & {}) => T.Effect -} - -export function makeRequestHandler< - R, - PathA, - CookieA, - QueryA, - BodyA, - HeaderA, - ReqA extends PathA & QueryA & BodyA, - ResA ->({ - Request, - Response, - handle, -}: RequestHandler< - R & Has.Has, - PathA, - CookieA, - QueryA, - BodyA, - HeaderA, - ReqA, - ResA ->) { - const encodeResponse = encode(Response) - const { shrink: shrinkResponse } = strict(Response) - - return handleRequest( - makeRequestParsers(Request), - flow(shrinkResponse, Sy.chain(encodeResponse)), - handle - ) -} - -function makeRequestParsers< - R, - PathA, - CookieA, - QueryA, - BodyA, - HeaderA, - ReqA extends PathA & QueryA & BodyA, - ResA ->( - Request: RequestHandler< - R, - PathA, - CookieA, - QueryA, - BodyA, - HeaderA, - ReqA, - ResA - >["Request"] -): RequestParsers { - const ph = O.fromNullable(Request.Headers) - ["|>"](O.map(strictDecoder)) - ["|>"](O.map((x) => x.decode)) - ["|>"](SO.fromOption) - const parseHeaders = (u: unknown) => - ph["|>"](SO.chain((d) => d(u)["|>"](SO.fromSync))) - - const pq = O.fromNullable(Request.Query) - ["|>"](O.map(strictDecoder)) - ["|>"](O.map((x) => x.decode)) - ["|>"](SO.fromOption) - const parseQuery = (u: unknown) => pq["|>"](SO.chain((d) => d(u)["|>"](SO.fromSync))) - - const pb = O.fromNullable(Request.Body) - ["|>"](O.map(strictDecoder)) - ["|>"](O.map((x) => x.decode)) - ["|>"](SO.fromOption) - const parseBody = (u: unknown) => pb["|>"](SO.chain((d) => d(u)["|>"](SO.fromSync))) - - const pp = O.fromNullable(Request.Path) - ["|>"](O.map(strictDecoder)) - ["|>"](O.map((x) => x.decode)) - ["|>"](SO.fromOption) - const parsePath = (u: unknown) => pp["|>"](SO.chain((d) => d(u)["|>"](SO.fromSync))) - - const pc = O.fromNullable(Request.Cookie) - ["|>"](O.map(strictDecoder)) - ["|>"](O.map((x) => x.decode)) - ["|>"](SO.fromOption) - const parseCookie = (u: unknown) => pc["|>"](SO.chain((d) => d(u)["|>"](SO.fromSync))) - - return { - parseBody, - parseCookie, - parseHeaders, - parsePath, - parseQuery, - } -} - -interface RequestParsers { - parseHeaders: Decode> - parseQuery: Decode> - parseBody: Decode> - parsePath: Decode> - parseCookie: Decode> -} diff --git a/packages/infra/express/morphic/routing.ts b/packages/infra/express/morphic/routing.ts deleted file mode 100644 index 9af15aa..0000000 --- a/packages/infra/express/morphic/routing.ts +++ /dev/null @@ -1,307 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { - isObjectSchema, - JSONSchema, - ParameterLocation, - SubSchema, -} from "@atlas-ts/plutus" -import { schema } from "@atlas-ts/plutus/Schema" -import * as EO from "@effect-ts-demo/core/ext/EffectOption" -import { Void } from "@effect-ts-demo/core/ext/Morphic" -import { Has, pipe } from "@effect-ts/core" -import * as A from "@effect-ts/core/Collections/Immutable/Array" -import * as T from "@effect-ts/core/Effect" -import * as O from "@effect-ts/core/Option" -import * as Ex from "@effect-ts/express" - -import { UserSVC } from "../../services" - -import { makeRequestHandler, RequestHandler } from "./requestHandler" - -type Methods = "GET" | "PUT" | "POST" | "PATCH" | "DELETE" - -export interface RouteDescriptor< - R, - PathA, - CookieA, - QueryA, - BodyA, - HeaderA, - ReqA extends PathA & QueryA & BodyA, - ResA, - METHOD extends Methods = Methods -> { - path: string - method: METHOD - handler: RequestHandler< - R & Has.Has, - PathA, - CookieA, - QueryA, - BodyA, - HeaderA, - ReqA, - ResA - > - _tag: "Morphic" -} - -export function makeRouteDescriptor< - R, - PathA, - CookieA, - QueryA, - BodyA, - HeaderA, - ReqA extends PathA & QueryA & BodyA, - ResA, - METHOD extends Methods = Methods ->( - path: string, - method: METHOD, - handler: RequestHandler< - R & Has.Has, - PathA, - CookieA, - QueryA, - BodyA, - HeaderA, - ReqA, - ResA - > -) { - return { path, method, handler, _tag: "Morphic" } as RouteDescriptor< - R, - PathA, - CookieA, - QueryA, - BodyA, - HeaderA, - ReqA, - ResA, - METHOD - > -} - -export function get< - R, - PathA, - CookieA, - QueryA, - BodyA, - HeaderA, - ReqA extends PathA & QueryA & BodyA, - ResA ->( - path: string, - r: RequestHandler< - R & Has.Has, - PathA, - CookieA, - QueryA, - BodyA, - HeaderA, - ReqA, - ResA - > -) { - return pipe( - Ex.get(path, makeRequestHandler(r)), - T.zipRight(T.succeedWith(() => makeRouteDescriptor(path, "GET", r))) - ) -} - -export function post< - R, - PathA, - CookieA, - QueryA, - BodyA, - HeaderA, - ReqA extends PathA & QueryA & BodyA, - ResA ->( - path: string, - r: RequestHandler< - R & Has.Has, - PathA, - CookieA, - QueryA, - BodyA, - HeaderA, - ReqA, - ResA - > -) { - return pipe( - Ex.post(path, makeRequestHandler(r)), - T.zipRight(T.succeedWith(() => makeRouteDescriptor(path, "POST", r))) - ) -} - -export function put< - R, - PathA, - CookieA, - QueryA, - BodyA, - HeaderA, - ReqA extends PathA & QueryA & BodyA, - ResA ->( - path: string, - r: RequestHandler< - R & Has.Has, - PathA, - CookieA, - QueryA, - BodyA, - HeaderA, - ReqA, - ResA - > -) { - return pipe( - Ex.put(path, makeRequestHandler(r)), - T.zipRight(T.succeedWith(() => makeRouteDescriptor(path, "PUT", r))) - ) -} - -export function patch< - R, - PathA, - CookieA, - QueryA, - BodyA, - HeaderA, - ReqA extends PathA & QueryA & BodyA, - ResA ->( - path: string, - r: RequestHandler< - R & Has.Has, - PathA, - CookieA, - QueryA, - BodyA, - HeaderA, - ReqA, - ResA - > -) { - return pipe( - Ex.patch(path, makeRequestHandler(r)), - T.zipRight(T.succeedWith(() => makeRouteDescriptor(path, "PATCH", r))) - ) -} - -function del< - R, - PathA, - CookieA, - QueryA, - BodyA, - HeaderA, - ReqA extends PathA & QueryA & BodyA, - ResA ->( - path: string, - r: RequestHandler< - R & Has.Has, - PathA, - CookieA, - QueryA, - BodyA, - HeaderA, - ReqA, - ResA - > -) { - return pipe( - Ex.delete(path, makeRequestHandler(r)), - T.zipRight(T.succeedWith(() => makeRouteDescriptor(path, "DELETE", r))) - ) -} -export { del as delete } - -export function makeFromMorphic( - e: RouteDescriptor -) { - const { Request: Req, Response: Res } = e.handler - // TODO: use the path vs body etc serialisation also in the Client. - const makeReqQuerySchema = EO.fromNullable(Req.Query)["|>"](EO.chainEffect(schema)) - const makeReqHeadersSchema = EO.fromNullable(Req.Headers)["|>"]( - EO.chainEffect(schema) - ) - const makeReqCookieSchema = EO.fromNullable(Req.Cookie)["|>"](EO.chainEffect(schema)) - const makeReqPathSchema = EO.fromNullable(Req.Path)["|>"](EO.chainEffect(schema)) - const makeReqBodySchema = EO.fromNullable(Req.Body)["|>"](EO.chainEffect(schema)) - //const makeReqSchema = schema(Req) - const makeResSchema = schema(Res) - - // TODO: custom void type - 204 response - // https://github.com/Effect-TS/morphic/commit/da3a02fb527089807bcd5253652ee5a5b1efa371 - - function makeParameters(inn: ParameterLocation) { - return (a: O.Option) => { - return a["|>"](O.chain((o) => (isObjectSchema(o) ? O.some(o) : O.none))) - ["|>"]( - O.map((x) => { - return Object.keys(x.properties!).map((p) => { - const schema = x.properties![p] - const required = Boolean(x.required?.includes(p)) - return { name: p, in: inn, required, schema } - }) - }) - ) - ["|>"](O.getOrElse(() => [])) - } - } - - return pipe( - T.struct({ - reqQuery: makeReqQuerySchema, - reqHeaders: makeReqHeadersSchema, - reqBody: makeReqBodySchema, - reqPath: makeReqPathSchema, - reqCookie: makeReqCookieSchema, - res: makeResSchema, - }), - T.map((_) => ({ - path: e.path, - method: e.method.toLowerCase(), - parameters: [ - ..._.reqPath["|>"](makeParameters("path")), - ..._.reqQuery["|>"](makeParameters("query")), - ..._.reqHeaders["|>"](makeParameters("header")), - ..._.reqCookie["|>"](makeParameters("cookie")), - ], - requestBody: O.toUndefined( - _.reqBody["|>"]( - O.map((schema) => ({ content: { "application/json": { schema } } })) - ) - ), - responses: A.concat_( - [ - e.handler.Response === Void - ? new Response(204, { description: "Empty" }) - : new Response(200, { - description: "OK", - content: { "application/json": { schema: _.res } }, - }), - new Response(400, { description: "ValidationError" }), - ], - e.path.includes(":") && e.handler.Response === Void - ? [new Response(404, { description: "NotFoundError" })] - : [] - ), - })) - ) -} - -class Response { - constructor( - public readonly statusCode: number, - public readonly type: any //string | JSONSchema | SubSchema - ) {} -} diff --git a/packages/infra/express/schema/routing.ts b/packages/infra/express/schema/routing.ts index e517fa3..61a4fca 100644 --- a/packages/infra/express/schema/routing.ts +++ b/packages/infra/express/schema/routing.ts @@ -49,7 +49,6 @@ export interface RouteDescriptor< info?: { tags: A.Array } - _tag: "Schema" } export type RouteDescriptorAny = RouteDescriptor @@ -311,9 +310,6 @@ export function makeFromSchema( const makeResSchema = jsonSchema_(Res) - // TODO: custom void type - 204 response - // https://github.com/Effect-TS/morphic/commit/da3a02fb527089807bcd5253652ee5a5b1efa371 - function makeParameters(inn: ParameterLocation) { return (a: O.Option) => { return a["|>"](O.chain((o) => (isObjectSchema(o) ? O.some(o) : O.none))) diff --git a/packages/infra/simpledb/shared.ts b/packages/infra/simpledb/shared.ts index 3ca51be..9614aac 100644 --- a/packages/infra/simpledb/shared.ts +++ b/packages/infra/simpledb/shared.ts @@ -1,12 +1,7 @@ -import assert from "assert" - -import { pipe } from "@effect-ts-demo/core/ext/Function" -import * as MO from "@effect-ts-demo/core/ext/Morphic" import * as S from "@effect-ts-demo/core/ext/Schema" import { SchemaAny } from "@effect-ts-demo/core/ext/Schema" import * as T from "@effect-ts/core/Effect" import * as O from "@effect-ts/core/Option" -import * as Sy from "@effect-ts/core/Sync" class BaseError { constructor(public message: string) {} @@ -89,38 +84,38 @@ export interface EffectMap { set: (k: TKey, v: T) => T.UIO } -export function encodeOnlyWhenStrictMatch( - encode: MO.HasEncoder["encode_"], - v: A -) { - const e1 = Sy.run(encode(v, "strict")) - const e2 = Sy.run(encode(v, "classic")) - try { - assert.deepStrictEqual(e1, e2) - } catch (err) { - throw new Error( - // eslint-disable-next-line @typescript-eslint/restrict-plus-operands - "The strict encoding of these objects does not match the classic encoding of these objects. This means that there is a chance of a data-loss, and is probably a programming error\n" + - err - ) - } - return e1 -} +// export function encodeOnlyWhenStrictMatch( +// encode: MO.HasEncoder["encode_"], +// v: A +// ) { +// const e1 = Sy.run(encode(v, "strict")) +// const e2 = Sy.run(encode(v, "classic")) +// try { +// assert.deepStrictEqual(e1, e2) +// } catch (err) { +// throw new Error( +// // eslint-disable-next-line @typescript-eslint/restrict-plus-operands +// "The strict encoding of these objects does not match the classic encoding of these objects. This means that there is a chance of a data-loss, and is probably a programming error\n" + +// err +// ) +// } +// return e1 +// } -export function decodeOnlyWhenStrictMatch( - decode: MO.HasDecoder["decode_"], - u: unknown -) { - return pipe( - decode(u, "strict"), - Sy.tap((v) => - pipe( - decode(u), - Sy.tap((v2) => { - assert.deepStrictEqual(v, v2) - return Sy.succeed(v2) - }) - ) - ) - ) -} +// export function decodeOnlyWhenStrictMatch( +// decode: MO.HasDecoder["decode_"], +// u: unknown +// ) { +// return pipe( +// decode(u, "strict"), +// Sy.tap((v) => +// pipe( +// decode(u), +// Sy.tap((v2) => { +// assert.deepStrictEqual(v, v2) +// return Sy.succeed(v2) +// }) +// ) +// ) +// ) +// }