From f86e5851cdbb643cc747112e9f347b8fc8b581ba Mon Sep 17 00:00:00 2001 From: Peter Williams Date: Tue, 13 Feb 2024 13:00:54 -0500 Subject: [PATCH] Port from fp/io-ts to effect-ts As hoped, this seems to fix our build issues. And the port is pretty straightforward; nearly doable with search-and-replace. Not yet extensively tested but it seems to be working ... --- package.json | 4 +- tsconfig.json | 4 +- utils/apis.ts | 445 ++++++++++++++++++++++++------------------------- utils/types.ts | 178 +++++++++----------- yarn.lock | 37 ++-- 5 files changed, 322 insertions(+), 346 deletions(-) diff --git a/package.json b/package.json index 5c8e050..ab35d4e 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,6 @@ { "dependencies": { + "@effect/schema": "^0.62", "@pinia/nuxt": "^0.5.1", "@vueuse/core": "^10.5.0", "@vueuse/nuxt": "^10.1.2", @@ -8,11 +9,10 @@ "@wwtelescope/engine-helpers": "^0.16", "@wwtelescope/engine-pinia": "^0.9", "@wwtelescope/engine-types": "^0.6.4", + "effect": "^2.3", "ellipsize": "^0.1", "escape-html": "^1.0.3", "femtotween": "^2.0.3", - "fp-ts": "^2.13", - "io-ts": "^2.2", "keycloak-js": "~21.0.0", "less": "^4.1.3", "pinia": "^2.0.32", diff --git a/tsconfig.json b/tsconfig.json index c84b5be..f4b49d4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,8 @@ // https://nuxt.com/docs/guide/concepts/typescript "extends": "./.nuxt/tsconfig.json", "compilerOptions": { - "allowSyntheticDefaultImports": true + "allowSyntheticDefaultImports": true, + "exactOptionalPropertyTypes": true, + "strict": true } } \ No newline at end of file diff --git a/utils/apis.ts b/utils/apis.ts index ef071e6..e912b62 100644 --- a/utils/apis.ts +++ b/utils/apis.ts @@ -2,20 +2,20 @@ // Interfaces associated with backend APIs -import { isLeft } from "fp-ts/lib/Either.js"; -import * as t from "io-ts"; -import { PathReporter } from "io-ts/lib/PathReporter.js"; -import { $Fetch } from "ofetch"; +import * as S from "@effect/schema/Schema"; +import { formatError } from "@effect/schema/TreeFormatter"; +import * as Either from "effect/Either"; +import { type $Fetch } from "ofetch"; import { ImageStorage, ImageWwt, PlaceDetails, SceneContentHydrated, - SceneCreationInfoT, + type SceneCreationInfoT, ScenePreviews, TessellationCell, - TessellationCellT, + type TessellationCellT, } from "./types"; function checkForError(item: any) { @@ -35,20 +35,20 @@ function checkForError(item: any) { //export interface AmISuperuserRequest { } -export const AmISuperuserResponse = t.type({ - result: t.boolean, +export const AmISuperuserResponse = S.struct({ + result: S.boolean, }); -export type AmISuperuserResponseT = t.TypeOf; +export type AmISuperuserResponseT = S.Schema.To; export async function amISuperuser(fetcher: $Fetch): Promise { return fetcher("/misc/amisuperuser").then((data) => { checkForError(data); - const maybe = AmISuperuserResponse.decode(data); + const maybe = S.decodeUnknownEither(AmISuperuserResponse)(data); - if (isLeft(maybe)) { - throw new Error(`GET /misc/amisuperuser: API response did not match schema: ${PathReporter.report(maybe).join("\n")}`); + if (Either.isLeft(maybe)) { + throw new Error(`GET /misc/amisuperuser: API response did not match schema: ${formatError(maybe.left)}`); } return maybe.right; @@ -60,18 +60,18 @@ export async function amISuperuser(fetcher: $Fetch): Promise; +export type MiscConfigDatabaseResponseT = S.Schema.To; export async function miscConfigDatabase(fetcher: $Fetch): Promise { return fetcher("/misc/config-database", { method: 'POST' }).then((data) => { checkForError(data); - const maybe = MiscConfigDatabaseResponse.decode(data); + const maybe = S.decodeUnknownEither(MiscConfigDatabaseResponse)(data); - if (isLeft(maybe)) { - throw new Error(`GET /misc/config-database: API response did not match schema: ${PathReporter.report(maybe).join("\n")}`); + if (Either.isLeft(maybe)) { + throw new Error(`GET /misc/config-database: API response did not match schema: ${formatError(maybe.left)}`); } return maybe.right; @@ -83,18 +83,18 @@ export async function miscConfigDatabase(fetcher: $Fetch): Promise; +export type MiscUpdateTimelineResponseT = S.Schema.To; export async function miscUpdateTimeline(fetcher: $Fetch, initialId: string): Promise { return fetcher("/misc/update-timeline", { query: { "initial_id": initialId }, method: 'POST' }).then((data) => { checkForError(data); - const maybe = MiscUpdateTimelineResponse.decode(data); + const maybe = S.decodeUnknownEither(MiscUpdateTimelineResponse)(data); - if (isLeft(maybe)) { - throw new Error(`GET /misc/update-timeline: API response did not match schema: ${PathReporter.report(maybe).join("\n")}`); + if (Either.isLeft(maybe)) { + throw new Error(`GET /misc/update-timeline: API response did not match schema: ${formatError(maybe.left)}`); } return maybe.right; @@ -105,18 +105,18 @@ export async function miscUpdateTimeline(fetcher: $Fetch, initialId: string): Pr // Endpoint: POST /misc/update-global-tessellation -export const MiscUpdateGlobalTessellationResponse = t.type({}); +export const MiscUpdateGlobalTessellationResponse = S.struct({}); -export type MiscUpdateGlobalTessellationResponseT = t.TypeOf; +export type MiscUpdateGlobalTessellationResponseT = S.Schema.To; export async function miscUpdateGlobalTessellation(fetcher: $Fetch): Promise { return fetcher("/misc/update-global-tessellation", { method: "POST" }).then((data) => { checkForError(data); - const maybe = MiscUpdateGlobalTessellationResponse.decode(data); + const maybe = S.decodeUnknownEither(MiscUpdateGlobalTessellationResponse)(data); - if (isLeft(maybe)) { - throw new Error(`GET /misc/update-global-tessellation: API response did not match schema: ${PathReporter.report(maybe).join("\n")}`); + if (Either.isLeft(maybe)) { + throw new Error(`GET /misc/update-global-tessellation: API response did not match schema: ${formatError(maybe.left)}`); } return maybe.right; @@ -126,12 +126,12 @@ export async function miscUpdateGlobalTessellation(fetcher: $Fetch): Promise; +export type GetHandleResponseT = S.Schema.To; // Returns null if a 404 is returned, i.e. the handle is not found. export async function getHandle(fetcher: $Fetch, handle: string): Promise { @@ -139,10 +139,10 @@ export async function getHandle(fetcher: $Fetch, handle: string): Promise; +export type HandlePermissionsResponseT = S.Schema.To; export async function handlePermissions(fetcher: $Fetch, handle: string): Promise { try { const data = await fetcher(`/handle/${encodeURIComponent(handle)}/permissions`); checkForError(data); - const maybe = HandlePermissionsResponse.decode(data); + const maybe = S.decodeUnknownEither(HandlePermissionsResponse)(data); - if (isLeft(maybe)) { - throw new Error(`GET /handle/:handle/permissions: API response did not match schema: ${PathReporter.report(maybe).join("\n")}`); + if (Either.isLeft(maybe)) { + throw new Error(`GET /handle/:handle/permissions: API response did not match schema: ${formatError(maybe.left)}`); } return maybe.right; @@ -190,27 +190,23 @@ export async function handlePermissions(fetcher: $Fetch, handle: string): Promis // Endpoint: GET /handle/:handle/imageinfo?page=$int&pagesize=$int -export const ImageSummary = t.intersection([ - t.type({ - _id: t.string, - handle_id: t.string, - creation_date: t.string, - note: t.string, - storage: ImageStorage, - }), - t.partial({ - alt_text: t.string, - }) -]); - -export type ImageSummaryT = t.TypeOf; - -export const HandleImageInfoResponse = t.type({ - total_count: t.number, - results: t.array(ImageSummary), +export const ImageSummary = S.struct({ + _id: S.string, + handle_id: S.string, + creation_date: S.string, + note: S.string, + storage: ImageStorage, + alt_text: S.optional(S.string, { exact: true }), }); -export type HandleImageInfoResponseT = t.TypeOf; +export type ImageSummaryT = S.Schema.To; + +export const HandleImageInfoResponse = S.struct({ + total_count: S.number, + results: S.array(ImageSummary), +}); + +export type HandleImageInfoResponseT = S.Schema.To; export async function handleImageInfo( fetcher: $Fetch, @@ -223,10 +219,10 @@ export async function handleImageInfo( { query: { page: page_num, pagesize: page_size } } ); checkForError(data); - const maybe = HandleImageInfoResponse.decode(data); + const maybe = S.decodeUnknownEither(HandleImageInfoResponse)(data); - if (isLeft(maybe)) { - throw new Error(`GET /handle/:handle/imageinfo: API response did not match schema: ${PathReporter.report(maybe).join("\n")}`); + if (Either.isLeft(maybe)) { + throw new Error(`GET /handle/:handle/imageinfo: API response did not match schema: ${formatError(maybe.left)}`); } return maybe.right; @@ -235,28 +231,25 @@ export async function handleImageInfo( // Endpoint: GET /handle/:handle/sceneinfo?page=$int&pagesize=$int -export const HandleSceneInfo = t.intersection([ - t.type({ - _id: t.string, - creation_date: t.string, - impressions: t.number, - likes: t.number, - text: t.string, - published: t.boolean, - }), t.partial({ - clicks: t.number, - shares: t.number, - }) -]); - -export type HandleSceneInfoT = t.TypeOf; - -export const HandleSceneInfoResponse = t.type({ - total_count: t.number, - results: t.array(HandleSceneInfo), +export const HandleSceneInfo = S.struct({ + _id: S.string, + creation_date: S.string, + impressions: S.number, + likes: S.number, + text: S.string, + published: S.boolean, + clicks: S.optional(S.number, { exact: true }), + shares: S.optional(S.number, { exact: true }), +}); + +export type HandleSceneInfoT = S.Schema.To; + +export const HandleSceneInfoResponse = S.struct({ + total_count: S.number, + results: S.array(HandleSceneInfo), }); -export type HandleSceneInfoResponseT = t.TypeOf; +export type HandleSceneInfoResponseT = S.Schema.To; export async function handleSceneInfo( fetcher: $Fetch, @@ -269,10 +262,10 @@ export async function handleSceneInfo( { query: { page: page_num, pagesize: page_size } } ); checkForError(data); - const maybe = HandleSceneInfoResponse.decode(data); + const maybe = S.decodeUnknownEither(HandleSceneInfoResponse)(data); - if (isLeft(maybe)) { - throw new Error(`GET /handle/:handle/sceneinfo: API response did not match schema: ${PathReporter.report(maybe).join("\n")}`); + if (Either.isLeft(maybe)) { + throw new Error(`GET /handle/:handle/sceneinfo: API response did not match schema: ${formatError(maybe.left)}`); } return maybe.right; @@ -281,33 +274,33 @@ export async function handleSceneInfo( // Endpoint: GET /handle/:handle/stats -export const HandleImageStats = t.type({ - count: t.number, +export const HandleImageStats = S.struct({ + count: S.number, }); -export const HandleSceneStats = t.type({ - count: t.number, - impressions: t.number, - likes: t.number, - clicks: t.number, - shares: t.number, +export const HandleSceneStats = S.struct({ + count: S.number, + impressions: S.number, + likes: S.number, + clicks: S.number, + shares: S.number, }); -export const HandleStatsResponse = t.type({ - handle: t.string, +export const HandleStatsResponse = S.struct({ + handle: S.string, images: HandleImageStats, scenes: HandleSceneStats, }); -export type HandleStatsResponseT = t.TypeOf; +export type HandleStatsResponseT = S.Schema.To; export async function handleStats(fetcher: $Fetch, handle: string): Promise { const data = await fetcher(`/handle/${encodeURIComponent(handle)}/stats`); checkForError(data); - const maybe = HandleStatsResponse.decode(data); + const maybe = S.decodeUnknownEither(HandleStatsResponse)(data); - if (isLeft(maybe)) { - throw new Error(`GET /handle/:handle/stats: API response did not match schema: ${PathReporter.report(maybe).join("\n")}`); + if (Either.isLeft(maybe)) { + throw new Error(`GET /handle/:handle/stats: API response did not match schema: ${formatError(maybe.left)}`); } return maybe.right; @@ -318,17 +311,17 @@ export async function handleStats(fetcher: $Fetch, handle: string): Promise; +export type HandleCreateRequestT = S.Schema.To; -export const HandleCreateResponse = t.type({ - id: t.string, +export const HandleCreateResponse = S.struct({ + id: S.string, }); -export type HandleCreateResponseT = t.TypeOf; +export type HandleCreateResponseT = S.Schema.To; export async function createHandle(fetcher: $Fetch, handle: string, req: HandleCreateRequestT): Promise { const path = `/handle/${encodeURIComponent(handle)}`; @@ -336,10 +329,10 @@ export async function createHandle(fetcher: $Fetch, handle: string, req: HandleC return fetcher(path, { method: 'POST', body: req }).then((data) => { checkForError(data); - const maybe = HandleCreateResponse.decode(data); + const maybe = S.decodeUnknownEither(HandleCreateResponse)(data); - if (isLeft(maybe)) { - throw new Error(`POST /handle/:handle: API response did not match schema: ${PathReporter.report(maybe).join("\n")}`); + if (Either.isLeft(maybe)) { + throw new Error(`POST /handle/:handle: API response did not match schema: ${formatError(maybe.left)}`); } return maybe.right; @@ -349,11 +342,11 @@ export async function createHandle(fetcher: $Fetch, handle: string, req: HandleC // Endpoint: PATCH /handle/:handle -export const HandleUpdateRequest = t.partial({ - display_name: t.string, +export const HandleUpdateRequest = S.struct({ + display_name: S.optional(S.string, { exact: true }), }); -export type HandleUpdateRequestT = t.TypeOf; +export type HandleUpdateRequestT = S.Schema.To; export async function updateHandle(fetcher: $Fetch, handle: string, req: HandleUpdateRequestT): Promise { const path = `/handle/${encodeURIComponent(handle)}`; @@ -366,15 +359,15 @@ export async function updateHandle(fetcher: $Fetch, handle: string, req: HandleU // Endpoint: POST /handle/:handle/add-owner -export const HandleAddOwnerRequest = t.type({ - account_id: t.string, +export const HandleAddOwnerRequest = S.struct({ + account_id: S.string, }); -export type HandleAddOwnerRequestT = t.TypeOf; +export type HandleAddOwnerRequestT = S.Schema.To; -export const HandleAddOwnerResponse = t.type({}); +export const HandleAddOwnerResponse = S.struct({}); -export type HandleAddOwnerResponseT = t.TypeOf; +export type HandleAddOwnerResponseT = S.Schema.To; export async function addHandleOwner( fetcher: $Fetch, @@ -386,10 +379,10 @@ export async function addHandleOwner( return fetcher(path, { method: 'POST', body: req }).then((data) => { checkForError(data); - const maybe = HandleAddOwnerResponse.decode(data); + const maybe = S.decodeUnknownEither(HandleAddOwnerResponse)(data); - if (isLeft(maybe)) { - throw new Error(`POST /handle/:handle/add-owner: API response did not match schema: ${PathReporter.report(maybe).join("\n")}`); + if (Either.isLeft(maybe)) { + throw new Error(`POST /handle/:handle/add-owner: API response did not match schema: ${formatError(maybe.left)}`); } return maybe.right; @@ -399,23 +392,19 @@ export async function addHandleOwner( // Endpoint: GET /image/:id -export const GetImageResponse = t.intersection([ - t.type({ - id: t.string, - handle_id: t.string, - handle: GetHandleResponse, - creation_date: t.string, - wwt: ImageWwt, - permissions: ImagePermissions, - storage: ImageStorage, - note: t.string, - }), - t.partial({ - alt_text: t.string, - }) -]); - -export type GetImageResponseT = t.TypeOf; +export const GetImageResponse = S.struct({ + id: S.string, + handle_id: S.string, + handle: GetHandleResponse, + creation_date: S.string, + wwt: ImageWwt, + permissions: ImagePermissions, + storage: ImageStorage, + note: S.string, + alt_text: S.optional(S.string, { exact: true }), +}); + +export type GetImageResponseT = S.Schema.To; // Returns null if a 404 is returned, i.e. the image is not found. export async function getImage(fetcher: $Fetch, id: string): Promise { @@ -423,10 +412,10 @@ export async function getImage(fetcher: $Fetch, id: string): Promise; +export type ImagePermissionsResponseT = S.Schema.To; export async function imagePermissions(fetcher: $Fetch, id: string): Promise { try { const data = await fetcher(`/image/${encodeURIComponent(id)}/permissions`); checkForError(data); - const maybe = ImagePermissionsResponse.decode(data); + const maybe = S.decodeUnknownEither(ImagePermissionsResponse)(data); - if (isLeft(maybe)) { - throw new Error(`GET /image/:id/permissions: API response did not match schema: ${PathReporter.report(maybe).join("\n")}`); + if (Either.isLeft(maybe)) { + throw new Error(`GET /image/:id/permissions: API response did not match schema: ${formatError(maybe.left)}`); } return maybe.right; @@ -474,13 +463,13 @@ export async function imagePermissions(fetcher: $Fetch, id: string): Promise; +export type ImageUpdateRequestT = S.Schema.To; export async function updateImage(fetcher: $Fetch, id: string, req: ImageUpdateRequestT): Promise { const path = `/image/${encodeURIComponent(id)}`; @@ -493,19 +482,19 @@ export async function updateImage(fetcher: $Fetch, id: string, req: ImageUpdateR // Endpoint: GET /images/builtin-backgrounds -const BuiltinBackgroundsResponse = t.type({ - results: t.array(ImageSummary), +const BuiltinBackgroundsResponse = S.struct({ + results: S.array(ImageSummary), }); export async function builtinBackgrounds( fetcher: $Fetch, -): Promise { +): Promise { const data = await fetcher("/images/builtin-backgrounds"); checkForError(data); - const maybe = BuiltinBackgroundsResponse.decode(data); + const maybe = S.decodeUnknownEither(BuiltinBackgroundsResponse)(data); - if (isLeft(maybe)) { - throw new Error(`GET /images/builtin-backgrounds: API response did not match schema: ${PathReporter.report(maybe).join("\n")}`); + if (Either.isLeft(maybe)) { + throw new Error(`GET /images/builtin-backgrounds: API response did not match schema: ${formatError(maybe.left)}`); } return maybe.right.results; @@ -514,35 +503,35 @@ export async function builtinBackgrounds( // Endpoint: GET /scene/:id -export const GetSceneResponse = t.type({ - id: t.string, - handle_id: t.string, +export const GetSceneResponse = S.struct({ + id: S.string, + handle_id: S.string, handle: GetHandleResponse, - creation_date: t.string, - likes: t.number, - impressions: t.number, - clicks: t.number, - shares: t.number, + creation_date: S.string, + likes: S.number, + impressions: S.number, + clicks: S.number, + shares: S.number, place: PlaceDetails, content: SceneContentHydrated, - text: t.string, - liked: t.boolean, - outgoing_url: t.union([t.string, t.undefined]), + text: S.string, + liked: S.boolean, + outgoing_url: S.union(S.string, S.undefined), previews: ScenePreviews, - published: t.boolean, + published: S.boolean, }); -export type GetSceneResponseT = t.TypeOf; +export type GetSceneResponseT = S.Schema.To; // Returns null if a 404 is returned, i.e. the scene is not found. export async function getScene(fetcher: $Fetch, scene_id: string): Promise { try { const data = await fetcher(`/scene/${encodeURIComponent(scene_id)}`, { credentials: 'include' }); checkForError(data); - const maybe = GetSceneResponse.decode(data); + const maybe = S.decodeUnknownEither(GetSceneResponse)(data); - if (isLeft(maybe)) { - throw new Error(`GET /scene/:id: API response did not match schema: ${PathReporter.report(maybe).join("\n")}`); + if (Either.isLeft(maybe)) { + throw new Error(`GET /scene/:id: API response did not match schema: ${formatError(maybe.left)}`); } return maybe.right; @@ -558,21 +547,21 @@ export async function getScene(fetcher: $Fetch, scene_id: string): Promise; +export type CreateSceneResponseT = S.Schema.To; export async function createScene(fetcher: $Fetch, handle: string, req: SceneCreationInfoT): Promise { return fetcher(`/handle/${encodeURIComponent(handle)}/scene`, { method: 'POST', body: req }).then((data) => { checkForError(data); - const maybe = CreateSceneResponse.decode(data); + const maybe = S.decodeUnknownEither(CreateSceneResponse)(data); - if (isLeft(maybe)) { - throw new Error(`POST /handle/:handle/scene: API response did not match schema: ${PathReporter.report(maybe).join("\n")}`); + if (Either.isLeft(maybe)) { + throw new Error(`POST /handle/:handle/scene: API response did not match schema: ${formatError(maybe.left)}`); } return maybe.right; @@ -582,21 +571,21 @@ export async function createScene(fetcher: $Fetch, handle: string, req: SceneCre // Endpoint: GET /scene/:id/permissions -export const ScenePermissionsResponse = t.type({ - id: t.string, - edit: t.boolean, +export const ScenePermissionsResponse = S.struct({ + id: S.string, + edit: S.boolean, }); -export type ScenePermissionsResponseT = t.TypeOf; +export type ScenePermissionsResponseT = S.Schema.To; export async function scenePermissions(fetcher: $Fetch, id: string): Promise { try { const data = await fetcher(`/scene/${encodeURIComponent(id)}/permissions`); checkForError(data); - const maybe = ScenePermissionsResponse.decode(data); + const maybe = S.decodeUnknownEither(ScenePermissionsResponse)(data); - if (isLeft(maybe)) { - throw new Error(`GET /scene/:id/permissions: API response did not match schema: ${PathReporter.report(maybe).join("\n")}`); + if (Either.isLeft(maybe)) { + throw new Error(`GET /scene/:id/permissions: API response did not match schema: ${formatError(maybe.left)}`); } return maybe.right; @@ -613,19 +602,19 @@ export async function scenePermissions(fetcher: $Fetch, id: string): Promise; +export type SceneUpdateRequestT = S.Schema.To; export async function updateScene(fetcher: $Fetch, id: string, req: SceneUpdateRequestT): Promise { const path = `/scene/${encodeURIComponent(id)}`; @@ -637,11 +626,11 @@ export async function updateScene(fetcher: $Fetch, id: string, req: SceneUpdateR // Endpoint: GET /handle/:handle/timeline?page=$number -export const TimelineResponse = t.type({ - results: t.array(GetSceneResponse), +export const TimelineResponse = S.struct({ + results: S.array(GetSceneResponse), }); -export type TimelineResponseT = t.TypeOf; +export type TimelineResponseT = S.Schema.To; export async function getHandleTimeline( fetcher: $Fetch, @@ -650,10 +639,10 @@ export async function getHandleTimeline( ): Promise { const data = await fetcher(`/handle/${encodeURIComponent(handle)}/timeline`, { query: { page: page_num }, credentials: 'include' }); checkForError(data); - const maybe = TimelineResponse.decode(data); + const maybe = S.decodeUnknownEither(TimelineResponse)(data); - if (isLeft(maybe)) { - throw new Error(`GET /handle/:handle/timeline: API response did not match schema: ${PathReporter.report(maybe).join("\n")}`); + if (Either.isLeft(maybe)) { + throw new Error(`GET /handle/:handle/timeline: API response did not match schema: ${formatError(maybe.left)}`); } return maybe.right; @@ -664,10 +653,10 @@ export async function getHandleTimeline( export async function getHomeTimeline(fetcher: $Fetch, page_num: number): Promise { const data = await fetcher(`/scenes/home-timeline`, { query: { page: page_num } }); checkForError(data); - const maybe = TimelineResponse.decode(data); + const maybe = S.decodeUnknownEither(TimelineResponse)(data); - if (isLeft(maybe)) { - throw new Error(`GET /scenes/home-timeline: API response did not match schema: ${PathReporter.report(maybe).join("\n")}`); + if (Either.isLeft(maybe)) { + throw new Error(`GET /scenes/home-timeline: API response did not match schema: ${formatError(maybe.left)}`); } return maybe.right; @@ -676,29 +665,29 @@ export async function getHomeTimeline(fetcher: $Fetch, page_num: number): Promis export async function getNearbyTimeline(fetcher: $Fetch, sceneID: string): Promise { const data = await fetcher(`/scene/${sceneID}/nearby-global`, { query: { size: 30 } }); checkForError(data); - const maybe = TimelineResponse.decode(data); + const maybe = S.decodeUnknownEither(TimelineResponse)(data); - if (isLeft(maybe)) { - throw new Error(`GET /tessellations/nearby-feed: API response did not match schema: ${PathReporter.report(maybe).join("\n")}`); + if (Either.isLeft(maybe)) { + throw new Error(`GET /tessellations/nearby-feed: API response did not match schema: ${formatError(maybe.left)}`); } return maybe.right; } -export const SceneInteractionResponse = t.type({ - id: t.string, - success: t.boolean +export const SceneInteractionResponse = S.struct({ + id: S.string, + success: S.boolean }); -export type SceneInteractionResponseT = t.TypeOf; +export type SceneInteractionResponseT = S.Schema.To; export async function addImpression(fetcher: $Fetch, id: string): Promise { return fetcher(`/scene/${id}/impressions`, { method: 'POST', credentials: 'include', cache: 'no-store' }).then((data) => { checkForError(data); - const maybe = SceneInteractionResponse.decode(data); + const maybe = S.decodeUnknownEither(SceneInteractionResponse)(data); - if (isLeft(maybe)) { - throw new Error(`POST /scenes/impressions: API response did not match schema: ${PathReporter.report(maybe).join("\n")}`); + if (Either.isLeft(maybe)) { + throw new Error(`POST /scenes/impressions: API response did not match schema: ${formatError(maybe.left)}`); } return maybe.right.success; @@ -708,10 +697,10 @@ export async function addImpression(fetcher: $Fetch, id: string): Promise { return fetcher(`/scene/${id}/likes`, { method: 'POST', credentials: 'include', cache: 'no-store' }).then((data) => { checkForError(data); - const maybe = SceneInteractionResponse.decode(data); + const maybe = S.decodeUnknownEither(SceneInteractionResponse)(data); - if (isLeft(maybe)) { - throw new Error(`POST /scenes/likes: API response did not match schema: ${PathReporter.report(maybe).join("\n")}`); + if (Either.isLeft(maybe)) { + throw new Error(`POST /scenes/likes: API response did not match schema: ${formatError(maybe.left)}`); } return maybe.right.success; @@ -721,10 +710,10 @@ export async function addLike(fetcher: $Fetch, id: string): Promise { export async function removeLike(fetcher: $Fetch, id: string): Promise { return fetcher(`/scene/${id}/likes`, { method: 'DELETE', credentials: 'include', cache: 'no-store' }).then((data) => { checkForError(data); - const maybe = SceneInteractionResponse.decode(data); + const maybe = S.decodeUnknownEither(SceneInteractionResponse)(data); - if (isLeft(maybe)) { - throw new Error(`POST /scenes/likes: API response did not match schema: ${PathReporter.report(maybe).join("\n")}`); + if (Either.isLeft(maybe)) { + throw new Error(`POST /scenes/likes: API response did not match schema: ${formatError(maybe.left)}`); } return maybe.right.success; @@ -734,10 +723,10 @@ export async function removeLike(fetcher: $Fetch, id: string): Promise export async function addShare(fetcher: $Fetch, id: string, type: string): Promise { return fetcher(`/scene/${id}/shares/${type}`, { method: 'POST', credentials: 'include', cache: 'no-store' }).then((data) => { checkForError(data); - const maybe = SceneInteractionResponse.decode(data); + const maybe = S.decodeUnknownEither(SceneInteractionResponse)(data); - if (isLeft(maybe)) { - throw new Error(`POST /scenes/shares: API response did not match schema: ${PathReporter.report(maybe).join("\n")}`); + if (Either.isLeft(maybe)) { + throw new Error(`POST /scenes/shares: API response did not match schema: ${formatError(maybe.left)}`); } return maybe.right.success; @@ -756,10 +745,10 @@ export async function initializeSession(fetcher: $Fetch): Promise { export async function getTessellationCell(fetcher: $Fetch, tessellationName: string, raRad: number, decRad: number): Promise { const data = await fetcher(`/tessellations/${tessellationName}/cell`, { query: { ra: raRad, dec: decRad } }); checkForError(data); - const maybe = TessellationCell.decode(data); + const maybe = S.decodeUnknownEither(TessellationCell)(data); - if (isLeft(maybe)) { - throw new Error(`GET /tessellations/:name/cell: API response did not match schema ${PathReporter.report(maybe).join("\n")}`); + if (Either.isLeft(maybe)) { + throw new Error(`GET /tessellations/:name/cell: API response did not match schema ${formatError(maybe.left)}`); } return maybe.right; diff --git a/utils/types.ts b/utils/types.ts index 8d8e2af..abfd1d9 100644 --- a/utils/types.ts +++ b/utils/types.ts @@ -1,127 +1,111 @@ // Copyright 2023 the .NET Foundation -import * as t from "io-ts"; +import * as S from "@effect/schema/Schema"; import { Color } from "@wwtelescope/engine"; -export const PlaceDetails = t.type({ - ra_rad: t.number, - dec_rad: t.number, - roll_rad: t.union([t.number, t.undefined]), - roi_height_deg: t.number, - roi_aspect_ratio: t.number, +export const PlaceDetails = S.struct({ + ra_rad: S.number, + dec_rad: S.number, + roll_rad: S.union(S.number, S.undefined), + roi_height_deg: S.number, + roi_aspect_ratio: S.number, }); -export type PlaceDetailsT = t.TypeOf; - -export const ImageWwt = t.type({ - base_degrees_per_tile: t.number, - bottoms_up: t.boolean, - center_x: t.number, - center_y: t.number, - file_type: t.string, - offset_x: t.number, - offset_y: t.number, - projection: t.string, - quad_tree_map: t.string, - rotation: t.number, - tile_levels: t.number, - width_factor: t.number, - thumbnail_url: t.string, +export type PlaceDetailsT = S.Schema.To; + +export const ImageWwt = S.struct({ + base_degrees_per_tile: S.number, + bottoms_up: S.boolean, + center_x: S.number, + center_y: S.number, + file_type: S.string, + offset_x: S.number, + offset_y: S.number, + projection: S.string, + quad_tree_map: S.string, + rotation: S.number, + tile_levels: S.number, + width_factor: S.number, + thumbnail_url: S.string, }); -export type ImageWwtT = t.TypeOf; +export type ImageWwtT = S.Schema.To; -export const ImageStorage = t.type({ - legacy_url_template: t.union([t.string, t.undefined]), +export const ImageStorage = S.struct({ + legacy_url_template: S.union(S.string, S.undefined), }); -export type ImageStorageT = t.TypeOf; +export type ImageStorageT = S.Schema.To; -export const ImagePermissions = t.intersection([ - t.type({ - copyright: t.string, - license: t.string, - }), - t.partial({ - credits: t.string, - }) -]); - -export type ImagePermissionsT = t.TypeOf; - -export const ImageDisplayInfo = t.intersection([ - t.type({ - id: t.string, - wwt: ImageWwt, - storage: ImageStorage, - permissions: ImagePermissions, - }), - t.partial({ - alt_text: t.string, - }) -]); +export const ImagePermissions = S.struct({ + copyright: S.string, + license: S.string, + credits: S.optional(S.string, { exact: true }), +}); -export type ImageDisplayInfoT = t.TypeOf; +export type ImagePermissionsT = S.Schema.To; -export const SceneImageLayer = t.type({ - image_id: t.string, - opacity: t.number, +export const ImageDisplayInfo = S.struct({ + id: S.string, + wwt: ImageWwt, + storage: ImageStorage, + permissions: ImagePermissions, + alt_text: S.optional(S.string, { exact: true }), }); -export type SceneImageLayerT = t.TypeOf; +export type ImageDisplayInfoT = S.Schema.To; -export const SceneImageLayerHydrated = t.type({ +export const SceneImageLayer = S.struct({ + image_id: S.string, + opacity: S.number, +}); + +export type SceneImageLayerT = S.Schema.To; + +export const SceneImageLayerHydrated = S.struct({ image: ImageDisplayInfo, - opacity: t.number, + opacity: S.number, }); -export type SceneImageLayerHydratedT = t.TypeOf; +export type SceneImageLayerHydratedT = S.Schema.To; -export const SceneContent = t.type({ - image_layers: t.array(SceneImageLayer), +export const SceneContent = S.struct({ + image_layers: S.array(SceneImageLayer), }); -export type SceneContentT = t.TypeOf; +export type SceneContentT = S.Schema.To; -export const SceneContentHydrated = t.intersection([ - t.partial({ - background: ImageDisplayInfo, - }), - t.type({ - image_layers: t.union([t.array(SceneImageLayerHydrated), t.undefined]), - }) -]); +export const SceneContentHydrated = S.struct({ + image_layers: S.union(S.array(SceneImageLayerHydrated), S.undefined), + background: S.optional(ImageDisplayInfo, { exact: true }) +}); -export type SceneContentHydratedT = t.TypeOf; +export type SceneContentHydratedT = S.Schema.To; -export const SceneDisplayInfo = t.type({ - id: t.string, +export const SceneDisplayInfo = S.struct({ + id: S.string, place: PlaceDetails, content: SceneContentHydrated, }); -export type SceneDisplayInfoT = t.TypeOf; +export type SceneDisplayInfoT = S.Schema.To; -export const SceneCreationInfo = t.intersection([ - t.partial({ - outgoing_url: t.string, - }), - t.type({ - place: PlaceDetails, - content: SceneContent, - text: t.string, - }) -]); - -export type SceneCreationInfoT = t.TypeOf; - -export const ScenePreviews = t.partial({ - video: t.string, - thumbnail: t.string, +export const SceneCreationInfo = S.struct({ + place: PlaceDetails, + content: SceneContent, + text: S.string, + outgoing_url: S.optional(S.string, { exact: true }), +}); + +export type SceneCreationInfoT = S.Schema.To; + +export const ScenePreviews = S.struct({ + video: S.optional(S.string, { exact: true }), + thumbnail: S.optional(S.string, { exact: true }), }); -export type ScenePreviewsT = t.TypeOf; +export type ScenePreviewsT = S.Schema.To; export interface SkymapSceneInfo { id: string; @@ -133,16 +117,16 @@ export interface SkymapSceneInfo { adjacent: boolean; } -export const TessellationCell = t.type({ - neighbors: t.array(t.string), - location: t.type({ - ra: t.number, - dec: t.number, +export const TessellationCell = S.struct({ + neighbors: S.array(S.string), + location: S.struct({ + ra: S.number, + dec: S.number, }), - scene_id: t.string + scene_id: S.string }); -export type TessellationCellT = t.TypeOf; +export type TessellationCellT = S.Schema.To; // Older types, potentially to be removed: diff --git a/yarn.lock b/yarn.lock index bea65e8..686ddf8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -466,6 +466,16 @@ __metadata: languageName: node linkType: hard +"@effect/schema@npm:^0.62": + version: 0.62.6 + resolution: "@effect/schema@npm:0.62.6" + peerDependencies: + effect: ^2.3.5 + fast-check: ^3.13.2 + checksum: 2f047646b00c30eb0c700b19264b96d62b35cc89d37c1ef8850999fd4988a517eef9e2630f85e3a3f8d29567a27cda268e7d46632ee5fb043dafdfec6c9f8046 + languageName: node + linkType: hard + "@emotion/hash@npm:~0.8.0": version: 0.8.0 resolution: "@emotion/hash@npm:0.8.0" @@ -2599,6 +2609,7 @@ __metadata: dependencies: "@css-render/vue3-ssr": ^0.15.12 "@dargmuesli/nuxt-cookie-control": ^5.9.2 + "@effect/schema": ^0.62 "@nuxt/webpack-builder": ^3.4.3 "@nuxtjs/google-analytics": ^2.4.0 "@pinia/nuxt": ^0.5.1 @@ -2617,13 +2628,12 @@ __metadata: "@wwtelescope/engine-types": ^0.6.4 axe-core: ^4.7.1 css-loader: ^6 + effect: ^2.3 ellipsize: ^0.1 esbuild-loader: ^3 escape-html: ^1.0.3 femtotween: ^2.0.3 file-loader: ^6.2 - fp-ts: ^2.13 - io-ts: ^2.2 keycloak-js: ~21.0.0 less: ^4.1.3 less-loader: ^11.1.0 @@ -4082,6 +4092,13 @@ __metadata: languageName: node linkType: hard +"effect@npm:^2.3": + version: 2.3.5 + resolution: "effect@npm:2.3.5" + checksum: a1aea6dfcbc6c9cad559b9005b29efd1722611fc32c474f76c8fdaa08e0808a29abe702e8a872161f49e2bdc0b8e87cdd9d807787e26d7ce7ae079276991bf7d + languageName: node + linkType: hard + "electron-to-chromium@npm:^1.4.648": version: 1.4.667 resolution: "electron-to-chromium@npm:1.4.667" @@ -4700,13 +4717,6 @@ __metadata: languageName: node linkType: hard -"fp-ts@npm:^2.13": - version: 2.16.2 - resolution: "fp-ts@npm:2.16.2" - checksum: 5c2e3f096ba4be5646bda38a43e3e8ca3bd7693fefb26e6c4d4c451efbc9a227f7f7cad562b087cb134d4e41f4ae615602f0b17890e9e40a9e83f2f8822da666 - languageName: node - linkType: hard - "fraction.js@npm:^4.3.7": version: 4.3.7 resolution: "fraction.js@npm:4.3.7" @@ -5327,15 +5337,6 @@ __metadata: languageName: node linkType: hard -"io-ts@npm:^2.2": - version: 2.2.21 - resolution: "io-ts@npm:2.2.21" - peerDependencies: - fp-ts: ^2.5.0 - checksum: c6ae5237e313f7428c874fa5667b3656adaa5ec29f7f34194ad8ea8894b525c89322a5b74ca560e7cd66f8334b0b48cae6c4dc517c662de72da86110140646d4 - languageName: node - linkType: hard - "ioredis@npm:^5.3.2": version: 5.3.2 resolution: "ioredis@npm:5.3.2"