From cbfb32e991bf4b5f864028de25bb13c3beddef3a Mon Sep 17 00:00:00 2001 From: LynithDev <61880709+LynithDev@users.noreply.github.com> Date: Mon, 9 Jun 2025 18:41:39 +0200 Subject: [PATCH] chore: add proper error serialization --- apps/oneclient/desktop/package.json | 3 +- apps/oneclient/frontend/src/bindings.gen.ts | 47 ++--- .../frontend/src/components/MyContext.tsx | 27 +++ apps/oneclient/frontend/src/routeTree.gen.ts | 67 ++++++- apps/oneclient/frontend/src/routes/index.tsx | 35 +++- .../frontend/src/routes/test/app.tsx | 21 ++ .../frontend/src/routes/test/route.tsx | 15 ++ packages/core/src/api/cluster/mod.rs | 5 +- packages/core/src/api/game/launch.rs | 2 +- packages/core/src/api/java/mod.rs | 8 +- packages/core/src/api/packages/mod.rs | 9 +- packages/core/src/error.rs | 39 ++-- packages/core/src/store/credentials.rs | 20 +- packages/core/src/store/dirs.rs | 5 +- packages/core/src/store/discord.rs | 5 +- packages/core/src/store/ingress.rs | 4 +- packages/core/src/store/metadata.rs | 10 +- packages/core/src/store/processes.rs | 4 +- packages/core/src/utils/crypto.rs | 4 +- packages/core/src/utils/io.rs | 12 +- packages/macro/src/error.rs | 179 ++++++++++++++++++ packages/macro/src/lib.rs | 26 +-- packages/macro/src/tauri.rs | 14 -- packages/utils/src/io.rs | 2 + test.rs | 0 25 files changed, 425 insertions(+), 138 deletions(-) create mode 100644 apps/oneclient/frontend/src/components/MyContext.tsx create mode 100644 apps/oneclient/frontend/src/routes/test/app.tsx create mode 100644 apps/oneclient/frontend/src/routes/test/route.tsx create mode 100644 packages/macro/src/error.rs delete mode 100644 packages/macro/src/tauri.rs create mode 100644 test.rs diff --git a/apps/oneclient/desktop/package.json b/apps/oneclient/desktop/package.json index bb196947..59d4687e 100644 --- a/apps/oneclient/desktop/package.json +++ b/apps/oneclient/desktop/package.json @@ -16,7 +16,8 @@ "build": "tauri build", "dev": "tauri dev", "tauri": "tauri", - "tauri-ext": "pnpm --filter @onelauncher/scripts -- tauri" + "tauri-ext": "pnpm --filter @onelauncher/scripts -- tauri", + "cargo": "cargo" }, "dependencies": { "@oneclient/frontend": "workspace:*" diff --git a/apps/oneclient/frontend/src/bindings.gen.ts b/apps/oneclient/frontend/src/bindings.gen.ts index ad95f510..273d8182 100644 --- a/apps/oneclient/frontend/src/bindings.gen.ts +++ b/apps/oneclient/frontend/src/bindings.gen.ts @@ -5,58 +5,45 @@ import { createTauRPCProxy as createProxy } from 'taurpc'; type TAURI_CHANNEL = (response: T) => void; -export type ClusterError = { InvalidVersion: string } | 'MissingJavaVersion' | 'ClusterDownloading' | 'ClusterAlreadyRunning'; +export type ClusterError = { type: 'InvalidVersion'; data: string } | { type: 'MissingJavaVersion'; data: string } | { type: 'ClusterDownloading'; data: string } | { type: 'ClusterAlreadyRunning'; data: string }; export type ClusterStage = 'notready' | 'downloading' | 'repairing' | 'ready'; export interface CreateCluster { name: string; mc_version: string; mc_loader: GameLoader; mc_loader_version: string | null; icon: Icon | null } -export type CryptoError = { InvalidHash: { algorithm: HashAlgorithm; expected: string; actual: string } } | 'InvalidAlgorithm'; +export type CryptoError = { type: 'InvalidHash'; data: string } | { type: 'InvalidAlgorithm'; data: string }; -export type DaoError = 'NotFound' | 'AlreadyExists' | { InvalidValue: [string, string] }; +export type DaoError = { type: 'NotFound'; data: string } | { type: 'AlreadyExists'; data: string } | { type: 'InvalidValue'; data: string }; -export type DirectoryError = 'BaseDir'; +export interface DirectoryError { type: 'BaseDir'; data: string } -export type DiscordError = 'MissingClientId' | 'ConnectError'; +export type DiscordError = { type: 'MissingClientId'; data: string } | { type: 'ConnectError'; data: string }; export type GameLoader = 'vanilla' | 'forge' | 'neoforge' | 'quilt' | 'fabric' | 'legacyfabric'; -export type HashAlgorithm = 'Sha1' | 'Sha256' | 'Md5'; - -/** - * A wrapper around generic and unhelpful [`std::io::Error`] messages. - */ -export type IOError = { InvalidAbsolutePath: string } | { IOErrorWrapper: { path: string } } | 'IOError' | 'DeserializeError' | 'AsyncZipError'; +export type IOError = { type: 'InvalidAbsolutePath'; data: string } | { type: 'IOErrorWrapper'; data: string } | { type: 'IOError'; data: string } | { type: 'DeserializeError'; data: string } | { type: 'AsyncZipError'; data: string }; export type Icon = string; -export type IncompatiblePackageType = 'McVersion' | 'Loader'; +export type IncompatiblePackageType = { type: 'McVersion'; data: string } | { type: 'Loader'; data: string }; -export type IngressError = 'NotFound'; +export interface IngressError { type: 'NotFound'; data: string } export interface IngressPayload { id: string; message: string; ingress_type: IngressType; percent: number | null; total: number } export type IngressType = { Download: { file_name: string } } | 'JavaPrepare' | 'JavaCheck' | 'JavaLocate' | 'MinecraftDownload' | { PrepareCluster: { cluster_name: string } }; -export type JavaError = { ParseVersion: [string] } | 'Execute' | 'MissingJava'; +export type JavaError = { type: 'ParseVersion'; data: string } | { type: 'Execute'; data: string } | { type: 'MissingJava'; data: string }; -export type LauncherError = { type: 'DirError'; data: DirectoryError } | { type: 'IOError'; data: IOError } | { type: 'IngressError'; data: IngressError } | { type: 'JavaError'; data: JavaError } | { type: 'CryptoError'; data: CryptoError } | { type: 'DiscordError'; data: DiscordError } | { type: 'MetadataError'; data: MetadataError } | { type: 'ClusterError'; data: ClusterError } | { type: 'MinecraftAuthError'; data: MinecraftAuthError } | { type: 'ProcessError'; data: ProcessError } | { type: 'PackageError'; data: PackageError } | { type: 'DaoError'; data: DaoError } | { type: 'SerdeError' } | { type: 'AnyhowError' } | { type: 'DbError' } | { type: 'ReqwestError' } | { type: 'InterpulseError' } | { type: 'RegexError' } | { type: 'SemaphoreError' } | { type: 'UrlError' } | { type: 'TauriError' }; +export type LauncherError = { type: 'DirError'; data: DirectoryError } | { type: 'IOError'; data: IOError } | { type: 'IngressError'; data: IngressError } | { type: 'JavaError'; data: JavaError } | { type: 'CryptoError'; data: CryptoError } | { type: 'DiscordError'; data: DiscordError } | { type: 'MetadataError'; data: MetadataError } | { type: 'ClusterError'; data: ClusterError } | { type: 'MinecraftAuthError'; data: MinecraftAuthError } | { type: 'ProcessError'; data: ProcessError } | { type: 'PackageError'; data: PackageError } | { type: 'DaoError'; data: DaoError } | { type: 'SerdeError'; data: string } | { type: 'AnyhowError'; data: string } | { type: 'DbError'; data: string } | { type: 'ReqwestError'; data: string } | { type: 'InterpulseError'; data: string } | { type: 'RegexError'; data: string } | { type: 'SemaphoreError'; data: string } | { type: 'UrlError'; data: string } | { type: 'TauriError'; data: string }; export type MessageLevel = 'Info' | 'Warn' | 'Error'; export interface MessagePayload { level: MessageLevel; message: string } -export type MetadataError = 'FetchError' | { NotModdedManifest: GameLoader } | { NotVanillaManifest: GameLoader } | 'ParseError' | 'NoMatchingLoader' | 'NoMatchingVersion'; +export type MetadataError = { type: 'FetchError'; data: string } | { type: 'NotModdedManifest'; data: string } | { type: 'NotVanillaManifest'; data: string } | { type: 'ParseError'; data: string } | { type: 'NoMatchingLoader'; data: string } | { type: 'NoMatchingVersion'; data: string }; -/** - * Wrapper around all `Error`s that can occur during the Microsoft authentication process. - */ -export type MinecraftAuthError = 'PublicKeyReading' | 'PKCS8Error' | { SerializeError: { step: MinecraftAuthStep } } | { DeserializeError: { step: MinecraftAuthStep; raw: string } } | { RequestError: { step: MinecraftAuthStep } } | { SigningError: { step: MinecraftAuthStep } } | 'HashError' | 'SessionIdError'; - -/** - * An ordered list of all MSA authentication steps. - */ -export type MinecraftAuthStep = 'DeviceToken' | 'SisuAuthenicate' | 'OAuthToken' | 'RefreshOAuthToken' | 'SisuAuthorize' | 'XstsAuthorize' | 'MinecraftToken' | 'MinecraftEntitlements' | 'MinecraftProfile'; +export type MinecraftAuthError = { type: 'PublicKeyReading'; data: string } | { type: 'PKCS8Error'; data: string } | { type: 'SerializeError'; data: string } | { type: 'DeserializeError'; data: string } | { type: 'RequestError'; data: string } | { type: 'SigningError'; data: string } | { type: 'HashError'; data: string } | { type: 'SessionIdError'; data: string }; /** * A structure of all needed Minecraft credentials for logging in and account management. @@ -87,11 +74,11 @@ export interface MinecraftCredentials { export interface Model { id: bigint; folder_name: string; stage: ClusterStage; created_at: string; group_id: bigint | null; name: string; mc_version: string; mc_loader: GameLoader; mc_loader_version: string | null; last_played: string | null; overall_played: bigint | null; icon_url: Icon | null; setting_profile_name: string | null; linked_pack_id: string | null; linked_pack_version: string | null } -export type PackageError = 'NoPrimaryFile' | 'IsModPack' | { Incompatible: IncompatiblePackageType }; +export type PackageError = { type: 'NoPrimaryFile'; data: string } | { type: 'IsModPack'; data: string } | { type: 'Incompatible'; data: IncompatiblePackageType }; export interface Process { pid: number; started_at: string; cluster_id: bigint; post_hook: string | null; account_id: string } -export type ProcessError = { HookUnsuccessful: number } | 'NoPid'; +export type ProcessError = { type: 'HookUnsuccessful'; data: string } | { type: 'NoPid'; data: string }; export type ProcessPayload = { type: 'Starting'; command: string } | { type: 'Started'; process: Process } | { type: 'Stopped'; pid: number; exit_code: number } | { type: 'Output'; pid: number; output: string }; @@ -101,11 +88,11 @@ export interface SettingProfileModel { name: string; java_id: bigint | null; res export interface SettingsOsExtra { enable_gamemode: boolean | null } -const ARGS_MAP = { oneclient: '{"open_dev_tools":[],"return_error":[]}', core: '{"getClusters":[],"createCluster":["options"],"getGlobalProfile":[],"getClusterById":["id"],"openMsaLogin":[],"getUsers":[],"getDefaultUser":["fallback"],"setDefaultUser":["uuid"],"getUser":["uuid"],"getProfileOrDefault":["name"],"removeUser":["uuid"],"removeCluster":["id"],"launchCluster":["id","uuid"]}', events: '{"message":["event"],"process":["event"],"ingress":["event"]}' }; +const ARGS_MAP = { events: '{"message":["event"],"ingress":["event"],"process":["event"]}', core: '{"getUser":["uuid"],"getDefaultUser":["fallback"],"createCluster":["options"],"getClusters":[],"launchCluster":["id","uuid"],"setDefaultUser":["uuid"],"getGlobalProfile":[],"getProfileOrDefault":["name"],"getUsers":[],"openMsaLogin":[],"removeUser":["uuid"],"removeCluster":["id"],"getClusterById":["id"]}', oneclient: '{"return_error":[],"open_dev_tools":[]}' }; export interface Router { - events: { ingress: (event: IngressPayload) => Promise; message: (event: MessagePayload) => Promise; process: (event: ProcessPayload) => Promise }; - core: { getClusters: () => Promise>; getClusterById: (id: bigint) => Promise; removeCluster: (id: bigint) => Promise; createCluster: (options: CreateCluster) => Promise; launchCluster: (id: bigint, uuid: string | null) => Promise; getProfileOrDefault: (name: string | null) => Promise; getGlobalProfile: () => Promise; getUsers: () => Promise>; getUser: (uuid: string) => Promise; removeUser: (uuid: string) => Promise; getDefaultUser: (fallback: boolean | null) => Promise; setDefaultUser: (uuid: string | null) => Promise; openMsaLogin: () => Promise }; oneclient: { return_error: () => Promise; open_dev_tools: () => Promise }; + core: { getClusters: () => Promise>; getClusterById: (id: bigint) => Promise; removeCluster: (id: bigint) => Promise; createCluster: (options: CreateCluster) => Promise; launchCluster: (id: bigint, uuid: string | null) => Promise; getProfileOrDefault: (name: string | null) => Promise; getGlobalProfile: () => Promise; getUsers: () => Promise>; getUser: (uuid: string) => Promise; removeUser: (uuid: string) => Promise; getDefaultUser: (fallback: boolean | null) => Promise; setDefaultUser: (uuid: string | null) => Promise; openMsaLogin: () => Promise }; + events: { ingress: (event: IngressPayload) => Promise; message: (event: MessagePayload) => Promise; process: (event: ProcessPayload) => Promise }; } export type { InferCommandOutput }; diff --git a/apps/oneclient/frontend/src/components/MyContext.tsx b/apps/oneclient/frontend/src/components/MyContext.tsx new file mode 100644 index 00000000..b3b4269b --- /dev/null +++ b/apps/oneclient/frontend/src/components/MyContext.tsx @@ -0,0 +1,27 @@ +import type { Context } from 'react'; +import { createContext, useContext } from 'react'; + +const myContext = createContext(null) as Context< + { + text: string; + } | null +>; + +export function MyContextProvider({ children, value }: { + children: React.ReactNode; + value: { + text: string; + } | null; +}) { + // eslint-disable-next-line react/no-context-provider -- ok + return {children}; +} + +export default function useMyContext() { + const context = useContext(myContext); + + if (!context) + throw new Error('useMyContext must be used within a MyContextProvider'); + + return context; +} diff --git a/apps/oneclient/frontend/src/routeTree.gen.ts b/apps/oneclient/frontend/src/routeTree.gen.ts index 5e682936..9a5d0a35 100644 --- a/apps/oneclient/frontend/src/routeTree.gen.ts +++ b/apps/oneclient/frontend/src/routeTree.gen.ts @@ -11,16 +11,30 @@ // Import Routes import { Route as rootRoute } from './routes/__root' +import { Route as TestRouteImport } from './routes/test/route' import { Route as IndexImport } from './routes/index' +import { Route as TestAppImport } from './routes/test/app' // Create/Update Routes +const TestRouteRoute = TestRouteImport.update({ + id: '/test', + path: '/test', + getParentRoute: () => rootRoute, +} as any) + const IndexRoute = IndexImport.update({ id: '/', path: '/', getParentRoute: () => rootRoute, } as any) +const TestAppRoute = TestAppImport.update({ + id: '/app', + path: '/app', + getParentRoute: () => TestRouteRoute, +} as any) + // Populate the FileRoutesByPath interface declare module '@tanstack/react-router' { @@ -32,39 +46,73 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof IndexImport parentRoute: typeof rootRoute } + '/test': { + id: '/test' + path: '/test' + fullPath: '/test' + preLoaderRoute: typeof TestRouteImport + parentRoute: typeof rootRoute + } + '/test/app': { + id: '/test/app' + path: '/app' + fullPath: '/test/app' + preLoaderRoute: typeof TestAppImport + parentRoute: typeof TestRouteImport + } } } // Create and export the route tree +interface TestRouteRouteChildren { + TestAppRoute: typeof TestAppRoute +} + +const TestRouteRouteChildren: TestRouteRouteChildren = { + TestAppRoute: TestAppRoute, +} + +const TestRouteRouteWithChildren = TestRouteRoute._addFileChildren( + TestRouteRouteChildren, +) + export interface FileRoutesByFullPath { '/': typeof IndexRoute + '/test': typeof TestRouteRouteWithChildren + '/test/app': typeof TestAppRoute } export interface FileRoutesByTo { '/': typeof IndexRoute + '/test': typeof TestRouteRouteWithChildren + '/test/app': typeof TestAppRoute } export interface FileRoutesById { __root__: typeof rootRoute '/': typeof IndexRoute + '/test': typeof TestRouteRouteWithChildren + '/test/app': typeof TestAppRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/' + fullPaths: '/' | '/test' | '/test/app' fileRoutesByTo: FileRoutesByTo - to: '/' - id: '__root__' | '/' + to: '/' | '/test' | '/test/app' + id: '__root__' | '/' | '/test' | '/test/app' fileRoutesById: FileRoutesById } export interface RootRouteChildren { IndexRoute: typeof IndexRoute + TestRouteRoute: typeof TestRouteRouteWithChildren } const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, + TestRouteRoute: TestRouteRouteWithChildren, } export const routeTree = rootRoute @@ -77,11 +125,22 @@ export const routeTree = rootRoute "__root__": { "filePath": "__root.tsx", "children": [ - "/" + "/", + "/test" ] }, "/": { "filePath": "index.tsx" + }, + "/test": { + "filePath": "test/route.tsx", + "children": [ + "/test/app" + ] + }, + "/test/app": { + "filePath": "test/app.tsx", + "parent": "/test" } } } diff --git a/apps/oneclient/frontend/src/routes/index.tsx b/apps/oneclient/frontend/src/routes/index.tsx index 7f4a4afa..eb19c339 100644 --- a/apps/oneclient/frontend/src/routes/index.tsx +++ b/apps/oneclient/frontend/src/routes/index.tsx @@ -1,3 +1,4 @@ +import type { LauncherError } from '@/bindings.gen'; import { bindings } from '@/main'; import { Button } from '@onelauncher/common/components'; import { createFileRoute } from '@tanstack/react-router'; @@ -16,15 +17,33 @@ function App() { // mc_loader_version: '11.15.1.2318', // }); - await bindings.core.launchCluster(2 as unknown as bigint, null); + try { + // await bindings.core.launchCluster(9 as unknown as bigint, null); + await bindings.core.removeCluster(9 as unknown as bigint); - const unlisten = await bindings.events.process.on((e) => { - if (e.type === 'Output') { console.log(e.output); } - else if (e.type === 'Stopped') { - console.log(e.exit_code); - unlisten(); - } - }); + const unlisten = await bindings.events.process.on((e) => { + if (e.type === 'Output') { console.log(e.output); } + else if (e.type === 'Stopped') { + console.log(e.exit_code); + unlisten(); + } + }); + } + catch (err: unknown) { + let error = err as LauncherError; + // console.log(error); + // if (error.type === 'DaoError') + // if (error.data.type === 'NotFound') + // console.log(error.data.data); + if (error.type === 'TauriError') + console.error(error.data); + else if (error.type === 'IOError') + console.error(error.data.data); + else if (error.type === 'DaoError') + console.error(error.data); + + console.log(error); + } }; return ( diff --git a/apps/oneclient/frontend/src/routes/test/app.tsx b/apps/oneclient/frontend/src/routes/test/app.tsx new file mode 100644 index 00000000..9b3e6e67 --- /dev/null +++ b/apps/oneclient/frontend/src/routes/test/app.tsx @@ -0,0 +1,21 @@ +import useMyContext from '@/components/MyContext'; +import { createFileRoute } from '@tanstack/react-router'; + +export const Route = createFileRoute('/test/app')({ + component: RouteComponent, +}); + +function RouteComponent() { + const myContext = useMyContext(); + + return ( +
+
Hello "/test/app"!
+
+ Value from MyContext: + {' '} + {myContext.text} +
+
+ ); +} diff --git a/apps/oneclient/frontend/src/routes/test/route.tsx b/apps/oneclient/frontend/src/routes/test/route.tsx new file mode 100644 index 00000000..58647854 --- /dev/null +++ b/apps/oneclient/frontend/src/routes/test/route.tsx @@ -0,0 +1,15 @@ +import { MyContextProvider } from '@/components/MyContext'; +import { createFileRoute, Outlet } from '@tanstack/react-router'; + +export const Route = createFileRoute('/test')({ + component: RouteComponent, +}); + +function RouteComponent() { + return ( + +
Hello "/test"!
+ +
+ ); +} diff --git a/packages/core/src/api/cluster/mod.rs b/packages/core/src/api/cluster/mod.rs index fa9704d3..f565e69f 100644 --- a/packages/core/src/api/cluster/mod.rs +++ b/packages/core/src/api/cluster/mod.rs @@ -6,7 +6,6 @@ use onelauncher_entity::{clusters, java_versions}; use onelauncher_entity::icon::Icon; use onelauncher_entity::loader::GameLoader; use sea_orm::ActiveValue::Set; -use serde::Serialize; use tokio::process::Command; use crate::api::game::arguments; @@ -25,8 +24,8 @@ pub use sync::*; use super::ingress::init_ingress; -#[onelauncher_macro::specta] -#[derive(Debug, thiserror::Error, Serialize)] +#[onelauncher_macro::error] +#[derive(Debug, thiserror::Error)] pub enum ClusterError { #[error("version '{0}' was not found")] InvalidVersion(String), diff --git a/packages/core/src/api/game/launch.rs b/packages/core/src/api/game/launch.rs index 9771d4e6..007c080b 100644 --- a/packages/core/src/api/game/launch.rs +++ b/packages/core/src/api/game/launch.rs @@ -119,7 +119,7 @@ pub async fn launch_minecraft( arguments::java_arguments( args.get(&interpulse::api::minecraft::ArgumentType::Jvm) .map(Vec::as_slice), - &dirs.natives_dir().join(&version_name), + &dirs.natives_dir().join(&version.id), &dirs.libraries_dir(), &arguments::classpaths( &dirs.libraries_dir(), diff --git a/packages/core/src/api/java/mod.rs b/packages/core/src/api/java/mod.rs index 50362ea7..6b1051c1 100644 --- a/packages/core/src/api/java/mod.rs +++ b/packages/core/src/api/java/mod.rs @@ -12,13 +12,13 @@ use crate::utils::io; pub mod dao; -#[onelauncher_macro::specta] -#[derive(Debug, thiserror::Error, Serialize)] +#[onelauncher_macro::error] +#[derive(Debug, thiserror::Error)] pub enum JavaError { #[error("failed to parse version '{0}'")] - ParseVersion(String, #[source] #[serde(skip)] std::num::ParseIntError), + ParseVersion(String, #[source] #[skip] std::num::ParseIntError), #[error("failed to execute java command")] - Execute(#[from] #[serde(skip)] std::io::Error), + Execute(#[from] #[skip] std::io::Error), #[error("no java installations found")] MissingJava, } diff --git a/packages/core/src/api/packages/mod.rs b/packages/core/src/api/packages/mod.rs index 00c6e5e1..b8c384c0 100644 --- a/packages/core/src/api/packages/mod.rs +++ b/packages/core/src/api/packages/mod.rs @@ -3,7 +3,6 @@ use onelauncher_entity::icon::Icon; use onelauncher_entity::package::PackageType; use onelauncher_entity::{clusters, packages}; use reqwest::Method; -use serde::Serialize; use crate::error::{LauncherError, LauncherResult}; use crate::store::Dirs; @@ -19,8 +18,8 @@ pub mod provider; #[cfg(test)] mod tests; -#[onelauncher_macro::specta] -#[derive(Debug, thiserror::Error, Serialize)] +#[onelauncher_macro::error] +#[derive(Debug, thiserror::Error)] pub enum PackageError { #[error("no file marked for download")] NoPrimaryFile, @@ -30,8 +29,8 @@ pub enum PackageError { Incompatible(#[from] IncompatiblePackageType), } -#[onelauncher_macro::specta] -#[derive(Debug, thiserror::Error, Serialize)] +#[onelauncher_macro::error] +#[derive(Debug, thiserror::Error)] pub enum IncompatiblePackageType { #[error("package is not compatible with the current version")] McVersion, diff --git a/packages/core/src/error.rs b/packages/core/src/error.rs index d24049f4..a2760b19 100644 --- a/packages/core/src/error.rs +++ b/packages/core/src/error.rs @@ -1,8 +1,5 @@ -use serde::Serialize; - -#[onelauncher_macro::specta] -#[derive(thiserror::Error, Debug, Serialize)] -#[serde(tag = "type", content = "data")] +#[onelauncher_macro::error] +#[derive(Debug, thiserror::Error)] pub enum LauncherError { #[error(transparent)] DirError(#[from] crate::store::DirectoryError), @@ -30,41 +27,31 @@ pub enum LauncherError { DaoError(#[from] DaoError), #[error("json error: {0}")] - SerdeError(#[from] #[serde(skip)] serde_json::Error), + SerdeError(#[from] #[skip] serde_json::Error), #[error(transparent)] - AnyhowError(#[from] #[serde(skip)] anyhow::Error), + AnyhowError(#[from] #[skip] anyhow::Error), #[error("database error: {0}")] - DbError(#[from] #[serde(skip)] sea_orm::DbErr), + DbError(#[from] #[skip] sea_orm::DbErr), #[error("http error: {0}")] - ReqwestError(#[from] #[serde(skip)] reqwest::Error), + ReqwestError(#[from] #[skip] reqwest::Error), #[error("meta error: {0}")] - InterpulseError(#[from] #[serde(skip)] interpulse::Error), + InterpulseError(#[from] #[skip] interpulse::Error), #[error(transparent)] - RegexError(#[from] #[serde(skip)] regex::Error), + RegexError(#[from] #[skip] regex::Error), #[error("couldn't acquire semaphore: {0}")] - SemaphoreError(#[from] #[serde(skip)] tokio::sync::AcquireError), + SemaphoreError(#[from] #[skip] tokio::sync::AcquireError), #[error(transparent)] - UrlError(#[from] #[serde(skip)] url::ParseError), + UrlError(#[from] #[skip] url::ParseError), #[cfg(feature = "tauri")] #[error(transparent)] - TauriError(#[from] #[serde(skip)] tauri::Error), + TauriError(#[from] #[skip] tauri::Error), } -// impl serde::Serialize for LauncherError { -// fn serialize(&self, serializer: S) -> Result -// where -// S: serde::Serializer, -// { -// // Serialize the error as a string -// serializer.serialize_str(&self.to_string()) -// } -// } - pub type LauncherResult = Result; -#[onelauncher_macro::specta] -#[derive(thiserror::Error, Debug, Serialize)] +#[onelauncher_macro::error] +#[derive(Debug, thiserror::Error)] pub enum DaoError { #[error("entity was not found")] NotFound, diff --git a/packages/core/src/store/credentials.rs b/packages/core/src/store/credentials.rs index c32d7b73..822c7349 100644 --- a/packages/core/src/store/credentials.rs +++ b/packages/core/src/store/credentials.rs @@ -1010,19 +1010,23 @@ pub enum MinecraftAuthStep { } /// Wrapper around all `Error`s that can occur during the Microsoft authentication process. -#[onelauncher_macro::specta] -#[derive(thiserror::Error, Debug, Serialize)] +#[onelauncher_macro::error] +#[derive(Debug, thiserror::Error)] pub enum MinecraftAuthError { #[error("failed to read public key during key generation")] PublicKeyReading, #[error("failed to serialize private key using PKCS8: {0}")] - PKCS8Error(#[from] #[serde(skip)] p256::pkcs8::Error), + PKCS8Error( + #[from] + #[skip] + p256::pkcs8::Error + ), #[error("failed to serialize JSON during MSA step {step:?}: {source}")] SerializeError { step: MinecraftAuthStep, #[source] - #[serde(skip)] + #[skip] source: serde_json::Error, }, #[error( @@ -1033,24 +1037,24 @@ pub enum MinecraftAuthError { raw: String, #[source] - #[serde(skip)] + #[skip] source: serde_json::Error, - #[serde(skip)] + #[skip] status_code: reqwest::StatusCode, }, #[error("failed to request using HTTP during MSA step {step:?}: {source}")] RequestError { step: MinecraftAuthStep, #[source] - #[serde(skip)] + #[skip] source: reqwest::Error, }, #[error("failed to create signed buffer during MSA step {step:?}: {source}")] SigningError { step: MinecraftAuthStep, #[source] - #[serde(skip)] + #[skip] source: std::io::Error, }, #[error("failed to read user hash")] diff --git a/packages/core/src/store/dirs.rs b/packages/core/src/store/dirs.rs index 7300149f..009705cc 100644 --- a/packages/core/src/store/dirs.rs +++ b/packages/core/src/store/dirs.rs @@ -1,7 +1,6 @@ use std::path::PathBuf; use onelauncher_entity::package::{PackageType, Provider}; -use serde::Serialize; use tokio::sync::OnceCell; use crate::{utils, LauncherResult}; @@ -16,8 +15,8 @@ pub struct Dirs { base_dir: PathBuf, } -#[onelauncher_macro::specta] -#[derive(Debug, thiserror::Error, Serialize)] +#[onelauncher_macro::error] +#[derive(Debug, thiserror::Error)] pub enum DirectoryError { #[error("Failed to get the base directory for the launcher.")] BaseDir, diff --git a/packages/core/src/store/discord.rs b/packages/core/src/store/discord.rs index 4c3134ce..d8e58b15 100644 --- a/packages/core/src/store/discord.rs +++ b/packages/core/src/store/discord.rs @@ -1,7 +1,6 @@ use std::sync::{atomic::AtomicBool, Arc}; use discord_rich_presence::{activity::{Activity, Assets, Button, Timestamps}, DiscordIpc, DiscordIpcClient}; -use serde::Serialize; use tokio::sync::RwLock; use crate::error::LauncherResult; @@ -107,8 +106,8 @@ impl DiscordRPC { } } -#[onelauncher_macro::specta] -#[derive(Debug, thiserror::Error, Serialize)] +#[onelauncher_macro::error] +#[derive(Debug, thiserror::Error)] pub enum DiscordError { #[error("Discord client ID is missing")] MissingClientId, diff --git a/packages/core/src/store/ingress.rs b/packages/core/src/store/ingress.rs index 77562965..10184951 100644 --- a/packages/core/src/store/ingress.rs +++ b/packages/core/src/store/ingress.rs @@ -15,8 +15,8 @@ pub struct IngressProcessor { ingress_feeds: RwLock>, } -#[onelauncher_macro::specta] -#[derive(Debug, thiserror::Error, Serialize)] +#[onelauncher_macro::error] +#[derive(Debug, thiserror::Error)] pub enum IngressError { #[error("ingress not found")] NotFound, diff --git a/packages/core/src/store/metadata.rs b/packages/core/src/store/metadata.rs index 228539d9..6de36ecd 100644 --- a/packages/core/src/store/metadata.rs +++ b/packages/core/src/store/metadata.rs @@ -31,8 +31,8 @@ struct MetadataInner { // legacyfabric: Option, } -#[onelauncher_macro::specta] -#[derive(Debug, thiserror::Error, Serialize)] +#[onelauncher_macro::error] +#[derive(Debug, thiserror::Error)] pub enum MetadataError { #[error("failed to fetch metadata")] FetchError, @@ -41,7 +41,11 @@ pub enum MetadataError { #[error("loader {0} does not use a vanilla manifest")] NotVanillaManifest(GameLoader), #[error("failed to parse metadata: {0}")] - ParseError(#[from] #[serde(skip)] serde_json::Error), + ParseError( + #[from] + #[skip] + serde_json::Error + ), #[error("no matching loader found")] NoMatchingLoader, #[error("no matching version found")] diff --git a/packages/core/src/store/processes.rs b/packages/core/src/store/processes.rs index c111771f..a3790d2c 100644 --- a/packages/core/src/store/processes.rs +++ b/packages/core/src/store/processes.rs @@ -24,8 +24,8 @@ use crate::utils::io::IOError; use super::State; use super::credentials::MinecraftCredentials; -#[onelauncher_macro::specta] -#[derive(Debug, thiserror::Error, Serialize)] +#[onelauncher_macro::error] +#[derive(Debug, thiserror::Error)] pub enum ProcessError { #[error("launch hook exited with non-zero code {0}")] HookUnsuccessful(i32), diff --git a/packages/core/src/utils/crypto.rs b/packages/core/src/utils/crypto.rs index 1daa5134..5687c906 100644 --- a/packages/core/src/utils/crypto.rs +++ b/packages/core/src/utils/crypto.rs @@ -8,8 +8,8 @@ use tokio::io::AsyncReadExt; use super::io::IOError; -#[onelauncher_macro::specta] -#[derive(Debug, thiserror::Error, Serialize)] +#[onelauncher_macro::error] +#[derive(Debug, thiserror::Error)] pub enum CryptoError { #[error("expected {algorithm} hash {expected}, but got {actual}")] InvalidHash { diff --git a/packages/core/src/utils/io.rs b/packages/core/src/utils/io.rs index cb376e2d..34702c61 100644 --- a/packages/core/src/utils/io.rs +++ b/packages/core/src/utils/io.rs @@ -13,8 +13,8 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio_util::compat::TokioAsyncWriteCompatExt; /// A wrapper around generic and unhelpful [`std::io::Error`] messages. -#[onelauncher_macro::specta] -#[derive(Debug, thiserror::Error, Serialize)] +#[onelauncher_macro::error] +#[derive(Debug, thiserror::Error)] pub enum IOError { #[error("invalid absolute path '{0}'")] InvalidAbsolutePath(PathBuf), @@ -22,16 +22,16 @@ pub enum IOError { #[error("error acessing path: {source}, path: {path}")] IOErrorWrapper { #[source] - #[serde(skip)] + #[skip] source: std::io::Error, path: String, }, #[error(transparent)] - IOError(#[from] #[serde(skip)] std::io::Error), + IOError(#[from] #[skip] std::io::Error), #[error("json deserialization error: {0}")] - DeserializeError(#[from] #[serde(skip)] serde_json::Error), + DeserializeError(#[from] #[skip] serde_json::Error), #[error(transparent)] - AsyncZipError(#[from] #[serde(skip)] async_zip::error::ZipError), + AsyncZipError(#[from] #[skip] async_zip::error::ZipError), } impl> From<(P, std::io::Error)> for IOError { diff --git a/packages/macro/src/error.rs b/packages/macro/src/error.rs new file mode 100644 index 00000000..0d98cbd3 --- /dev/null +++ b/packages/macro/src/error.rs @@ -0,0 +1,179 @@ +use proc_macro::TokenStream; +use quote::{format_ident, quote}; +use syn::{parse_macro_input, Data, DeriveInput, Fields}; + +pub fn error_attr(_attr: TokenStream, item: TokenStream) -> TokenStream { + let input = parse_macro_input!(item as DeriveInput); + + let Data::Enum(data_enum) = &input.data else { + return syn::Error::new_spanned(&input.ident, "Only enums are supported") + .to_compile_error() + .into(); + }; + + let ident = &input.ident; + let mut serialize_arms = Vec::new(); + + for variant in &data_enum.variants { + let v_ident = &variant.ident; + match &variant.fields { + Fields::Unit => { + serialize_arms.push(quote! { + Self::#v_ident => { + let mut s = serializer.serialize_struct(stringify!(#ident), 2)?; + s.serialize_field("type", stringify!(#v_ident))?; + s.serialize_field("data", &self.to_string())?; + s.end() + } + }); + } + Fields::Unnamed(fields_unnamed) => { + if fields_unnamed.unnamed.len() == 1 { + let field = &fields_unnamed.unnamed[0]; + let mut has_from = false; + let mut has_skip = false; + + for attr in &field.attrs { + if attr.path().is_ident("from") { + has_from = true; + } else if attr.path().is_ident("skip") { + has_skip = true; + } + } + + if has_from && !has_skip { + serialize_arms.push(quote! { + Self::#v_ident(inner) => { + let mut s = serializer.serialize_struct(stringify!(#ident), 2)?; + s.serialize_field("type", stringify!(#v_ident))?; + s.serialize_field("data", inner)?; + s.end() + } + }); + } else { + serialize_arms.push(quote! { + Self::#v_ident(_) => { + let mut s = serializer.serialize_struct(stringify!(#ident), 2)?; + s.serialize_field("type", stringify!(#v_ident))?; + s.serialize_field("data", &self.to_string())?; + s.end() + } + }); + } + } else { + serialize_arms.push(quote! { + Self::#v_ident(..) => { + let mut s = serializer.serialize_struct(stringify!(#ident), 2)?; + s.serialize_field("type", stringify!(#v_ident))?; + s.serialize_field("data", &self.to_string())?; + s.end() + } + }); + } + } + Fields::Named(_) => { + serialize_arms.push(quote! { + Self::#v_ident { .. } => { + let mut s = serializer.serialize_struct(stringify!(#ident), 2)?; + s.serialize_field("type", stringify!(#v_ident))?; + s.serialize_field("data", &self.to_string())?; + s.end() + } + }); + } + } + } + + let serialize_impl = quote! { + impl serde::Serialize for #ident { + fn serialize(&self, serializer: S) -> Result + where + S: serde::ser::Serializer, + { + use serde::ser::SerializeStruct; + match self { + #(#serialize_arms),* + } + } + } + }; + + let output = quote! { + #[cfg_attr(feature = "specta", derive(onelauncher_macro::SerializedError))] + #input + + #serialize_impl + }; + + output.into() +} + +pub fn error_derive(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + + let enum_name = &input.ident; + let specta_name = format_ident!("{}SpectaType", enum_name); + let mut variants = vec![]; + + let Data::Enum(data_enum) = &input.data else { + return syn::Error::new_spanned(enum_name, "Only enums are supported") + .to_compile_error() + .into(); + }; + + for variant in &data_enum.variants { + let name = &variant.ident; + let ty = match &variant.fields { + Fields::Unit => quote! { String }, + + Fields::Unnamed(fields) => { + // Handle newtype-style or tuple-like enums + if fields.unnamed.len() == 1 { + let field = &fields.unnamed[0]; + let field_ty = &field.ty; + + let has_from = field + .attrs + .iter() + .any(|a| a.path().is_ident("from")); + + let has_skip = field + .attrs + .iter() + .any(|a| a.path().is_ident("skip")); + + if has_from && !has_skip { + quote! { + #field_ty + } + } else { + quote! { String } + } + } else { + quote! { String } + } + } + + Fields::Named(_) => { + quote! { String } // Simplification: treat all struct-like variants as String + } + }; + + variants.push(quote! { + #name(#ty) + }); + } + + let enum_name_str = enum_name.to_string(); + let output = quote! { + #[derive(Debug, specta::Type, serde::Serialize)] + #[specta(remote = #enum_name)] + #[specta(tag = "type", content = "data")] + #[specta(rename = #enum_name_str)] + pub enum #specta_name { + #(#variants,)* + } + }; + + output.into() +} \ No newline at end of file diff --git a/packages/macro/src/lib.rs b/packages/macro/src/lib.rs index 584d54b0..89cd0997 100644 --- a/packages/macro/src/lib.rs +++ b/packages/macro/src/lib.rs @@ -2,7 +2,7 @@ mod specta; #[cfg(feature = "tauri")] -mod tauri; +mod error; mod pin; @@ -14,13 +14,13 @@ mod pin; /// #[cfg_attr(feature = "specta", derive(specta::Type))] /// ``` /// -/// ## `with_event` argument -/// Add `#[specta(with_event)]` to add the `tauri_specta::Event` derive. +/// ## `rename` attribute +/// Add `rename = ''` attribute to rename the type. /// /// #### Expands to: /// ``` /// #[cfg_attr(feature = "specta", derive(specta::Type))] -/// #[cfg_attr(feature = "tauri", derive(tauri_specta::Event))] +/// #[cfg_attr(feature = "specta", specta(rename = #rename))] /// ``` #[proc_macro_attribute] #[cfg(feature = "specta")] @@ -28,16 +28,16 @@ pub fn specta(attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> p specta::specta(attr, item) } -/// This macro is used to derive the `tauri::command` as well as the `specta::specta` attribute for the annotated item. -/// #### Expands to: -/// ``` -/// #[specta::specta] -/// #[tauri::command] -/// ``` +/// This macro is used to implement the `serde::Serialize` trait for the annotated item. +/// It only works for enums and tries to serialize them so that taurpc can use them properly. +#[proc_macro_derive(SerializedError, attributes(from, skip))] +pub fn serialized_error_derive(item: proc_macro::TokenStream) -> proc_macro::TokenStream { + error::error_derive(item) +} + #[proc_macro_attribute] -#[cfg(feature = "tauri")] -pub fn command(attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream { - tauri::tauri_command(attr, item) +pub fn error(attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream { + error::error_attr(attr, item) } /// This macro Box::Pin a function diff --git a/packages/macro/src/tauri.rs b/packages/macro/src/tauri.rs deleted file mode 100644 index 3600a859..00000000 --- a/packages/macro/src/tauri.rs +++ /dev/null @@ -1,14 +0,0 @@ -use quote::quote; -use syn::{parse_macro_input, Item}; - -pub fn tauri_command(_attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream { - let input = parse_macro_input!(item as Item); - - let expanded = quote! { - #[specta::specta] - #[tauri::command] - #input - }; - - expanded.into() -} \ No newline at end of file diff --git a/packages/utils/src/io.rs b/packages/utils/src/io.rs index 68a2b610..401cfcf0 100644 --- a/packages/utils/src/io.rs +++ b/packages/utils/src/io.rs @@ -10,6 +10,7 @@ use tokio::task::spawn_blocking; /// A wrapper around generic and unhelpful [`std::io::Error`] messages. #[derive(Debug, thiserror::Error)] +#[serde(tag = "type", content = "data")] pub enum IOError { /// A wrapped [`std::io::Error`] along with the path involved in the error. #[error("error acessing path: {source}, path: {path}")] @@ -36,6 +37,7 @@ impl> From<(P, std::io::Error)> for IOError { } #[derive(Debug, thiserror::Error)] +#[serde(tag = "type", content = "data")] #[error("received a non UTF-8 path: ", .0.to_string_lossy())] pub struct NonUtf8PathError(pub Box); diff --git a/test.rs b/test.rs new file mode 100644 index 00000000..e69de29b