Skip to content

Commit

Permalink
feat(glowsquid): 🥅 an unholy error handling solution
Browse files Browse the repository at this point in the history
At least it's 100% typesafe.

Signed-off-by: Suyashtnt <[email protected]>
  • Loading branch information
Suyashtnt committed Jun 21, 2024
1 parent 6dec1ef commit c341088
Show file tree
Hide file tree
Showing 12 changed files with 338 additions and 352 deletions.
439 changes: 146 additions & 293 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ resolver = "2"
[workspace.dependencies]
serde_json = "1.0.114"
serde = { version = "1.0.197", features = ["derive"] }
oauth2 = "4.4.2"
specta = "=2.0.0-rc.12"

reqwest = { version = "0.12.4", default-features = false, features = [
"rustls-tls",
Expand All @@ -15,7 +17,5 @@ reqwest = { version = "0.12.4", default-features = false, features = [

error-stack = { version = "0.4.1", features = ["spantrace", "serde"] }

oauth2 = "4.4.2"

[profile.release]
lto = true
101 changes: 101 additions & 0 deletions apps/glowsquid-frontend/src/lib/bindings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@

// This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually.

/** user-defined commands **/

export const commands = {
async greet(name: string) : Promise<string> {
return await TAURI_INVOKE("greet", { name });
},
async addAccount() : Promise<Result<MinecraftProfile, Error<AuthError>>> {
try {
return { status: "ok", data: await TAURI_INVOKE("add_account") };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
}
}

/** user-defined events **/



/** user-defined statics **/



/** user-defined types **/

export type AuthError = "MsToken" | "MinecraftToken" | "MinecraftProfile"
export type Cape = { id: string; state: UsageState; url: string; alias: string }
/**
* A custom error type that wraps around a error-stack error, converting it from Report<T> to Error<T>
*
* Also known as the most unholy error type known to crabkind
*/
export type Error<T> = { error: T; report: JsonValue }
export type JsonValue = null | boolean | number | string | JsonValue[] | { [key in string]: JsonValue }
export type MinecraftProfile = { id: string; name: string; skins: Skin[]; capes: Cape[] }
export type Skin = { id: string; state: UsageState; url: string; variant: SkinVariant }
export type SkinVariant = "CLASSIC" | "SLIM"
export type UsageState = "ACTIVE" | "INACTIVE"

/** tauri-specta globals **/

import { invoke as TAURI_INVOKE } from "@tauri-apps/api/core";
import * as TAURI_API_EVENT from "@tauri-apps/api/event";
import { type WebviewWindow as __WebviewWindow__ } from "@tauri-apps/api/webviewWindow";

type __EventObj__<T> = {
listen: (
cb: TAURI_API_EVENT.EventCallback<T>
) => ReturnType<typeof TAURI_API_EVENT.listen<T>>;
once: (
cb: TAURI_API_EVENT.EventCallback<T>
) => ReturnType<typeof TAURI_API_EVENT.once<T>>;
emit: T extends null
? (payload?: T) => ReturnType<typeof TAURI_API_EVENT.emit>
: (payload: T) => ReturnType<typeof TAURI_API_EVENT.emit>;
};

export type Result<T, E> =
| { status: "ok"; data: T }
| { status: "error"; error: E };

function __makeEvents__<T extends Record<string, any>>(
mappings: Record<keyof T, string>
) {
return new Proxy(
{} as unknown as {
[K in keyof T]: __EventObj__<T[K]> & {
(handle: __WebviewWindow__): __EventObj__<T[K]>;
};
},
{
get: (_, event) => {
const name = mappings[event as keyof T];

return new Proxy((() => {}) as any, {
apply: (_, __, [window]: [__WebviewWindow__]) => ({
listen: (arg: any) => window.listen(name, arg),
once: (arg: any) => window.once(name, arg),
emit: (arg: any) => window.emit(name, arg),
}),
get: (_, command: keyof __EventObj__<any>) => {
switch (command) {
case "listen":
return (arg: any) => TAURI_API_EVENT.listen(name, arg);
case "once":
return (arg: any) => TAURI_API_EVENT.once(name, arg);
case "emit":
return (arg: any) => TAURI_API_EVENT.emit(name, arg);
}
},
});
},
}
);
}


3 changes: 2 additions & 1 deletion apps/glowsquid-frontend/src/routes/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script lang="ts">
import { invoke } from '@tauri-apps/api/core'
import { commands } from "$lib/bindings"
import { Input, Message, Button } from '@repo/ui'
let name = $state('')
Expand All @@ -9,7 +10,7 @@
e.preventDefault()
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
greetMsg = await invoke('greet', { name })
greetMsg = await commands.greet(name)
}
</script>

Expand Down
13 changes: 7 additions & 6 deletions apps/glowsquid-frontend/src/routes/accountDropdown.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,18 @@
import DownArrow from '~icons/material-symbols/keyboard-arrow-down'
import Settings from '~icons/material-symbols/settings-outline'
import Plus from '~icons/material-symbols/add'
import { invoke } from '@tauri-apps/api/core'
import { commands } from '$lib/bindings'
const addAccount = (e: Event) => {
console.log('Add account')
invoke('add_account')
commands.addAccount()
.then((response) => {
console.log(`Success`, response)
})
.catch((error) => {
console.error(`Error`, error)
if (response.status === 'ok') {
console.log('Account added', response.data)
} else {
console.error('Failed to add account', response.error.error, response.error.report)
}
})
}
Expand Down
5 changes: 4 additions & 1 deletion apps/glowsquid/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ edition = "2021"
tauri-build = { version = "2.0.0-beta", features = [] }

[dependencies]
tauri = { version = "2.0.0-beta", features = [] }
tauri = { version = "=2.0.0-beta.22", features = [] }
copper = { path = "../../libs/copper" }

serde.workspace = true
Expand All @@ -30,6 +30,9 @@ once_cell = "1.19.0"
axum = "0.7.5"
tokio = "1.38.0"
open = "5.1.4"
specta = { version = "=2.0.0-rc.12", features = ["serde", "serde_json", "typescript"] }
tauri-specta = { version = "=2.0.0-rc.11", features = ["javascript", "typescript"] }


[features]
# This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!!
Expand Down
38 changes: 11 additions & 27 deletions apps/glowsquid/src/auth.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
use std::{
error::Error,
fmt,
future::IntoFuture,
net::{IpAddr, Ipv4Addr, SocketAddr},
sync::Arc,
};
use std::{error::Error, fmt, future::IntoFuture, sync::Arc};

use axum::{extract, response::IntoResponse, routing, Router};
use copper::client::auth::{MicrosoftAuthenticator, MinecraftProfile, MinecraftToken, OauthCode};
use oauth2::{CsrfToken, PkceCodeVerifier};
use once_cell::sync::OnceCell;
use serde::Serialize;
use specta::Type;
use tauri::{async_runtime, AppHandle, Manager, State};
use tokio::net::TcpListener;
use tokio_rusqlite::Connection;

use error_stack::{Result, ResultExt};
use error_stack::ResultExt;

// Glowsquids details. If you're making your own app, please do not use these.
// These are just for ease of use/packaging.
Expand Down Expand Up @@ -47,19 +43,14 @@ impl AuthState {
}
}

// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
#[tauri::command]
#[specta::specta]
pub async fn add_account(
state: State<'_, AuthState>,
app: AppHandle,
) -> Result<MinecraftProfile, AuthError> {
) -> Result<MinecraftProfile, crate::error::Error<AuthError>> {
// setup the server
let listener = TcpListener::bind((
"127.0.0.1",
get_available_port().expect("Failed to get available port"),
))
.await
.unwrap();
let listener = TcpListener::bind(("127.0.0.1", 0)).await.unwrap();
let port = listener.local_addr().unwrap().port();

let redirect_uri = format!("http://localhost:{}/code", port);
Expand Down Expand Up @@ -102,9 +93,10 @@ pub async fn add_account(
.get_minecraft_profile(&minecraft_token)
.await
.change_context(AuthError::MinecraftProfile)
.map_err(Into::into)
}

#[derive(Debug)]
#[derive(Debug, Clone, Copy, Type, Serialize)]
pub enum AuthError {
MsToken,
MinecraftToken,
Expand All @@ -128,13 +120,13 @@ struct AuthServerState {
oauth: MicrosoftAuthenticator,
csrf_token: CsrfToken,
pkce_verifier: Arc<PkceCodeVerifier>,
send_token: async_runtime::Sender<Result<MinecraftToken, AuthError>>,
send_token: async_runtime::Sender<error_stack::Result<MinecraftToken, AuthError>>,
}

async fn get_code(
extract::Query(code): extract::Query<OauthCode>,
extract::State(state): extract::State<AuthServerState>,
) -> std::result::Result<&'static str, impl IntoResponse> {
) -> Result<&'static str, impl IntoResponse> {
let ms_token = match state
.oauth
.get_ms_access_token(code, state.csrf_token, &state.pkce_verifier)
Expand Down Expand Up @@ -171,11 +163,3 @@ async fn get_code(

Ok("Authenticated! You can now close this window and go back to Glowsquid.")
}

fn get_available_port() -> Option<u16> {
(8000..9000).find(|port| port_is_available(*port))
}

fn port_is_available(port: u16) -> bool {
std::net::TcpListener::bind(("127.0.0.1", port)).is_ok()
}
40 changes: 40 additions & 0 deletions apps/glowsquid/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
use std::{
error,
fmt::{self, Debug},
};

use serde::Serialize;
use serde_json::Value;
use specta::Type;

/// A custom error type that wraps around a error-stack error, converting it from Report<T> to Error<T>
///
/// Also known as the most unholy error type known to crabkind
#[derive(Debug, Serialize, Type)]
pub struct Error<T> {
error: T,
report: Value,
#[serde(skip)]
error_stack: error_stack::Report<T>,
}

impl<T: error::Error> error::Error for Error<T> {}

impl<T> fmt::Display for Error<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
std::fmt::Display::fmt(&self.error_stack, f)
}
}

// copy instead of clone to enforce cheapness
impl<T: Send + Sync + error::Error + Copy + 'static> From<error_stack::Report<T>> for Error<T> {
fn from(error_stack: error_stack::Report<T>) -> Self {
let error = *error_stack.current_context();
Self {
error,
// cursed way to do this without cloning
report: serde_json::from_str(&serde_json::to_string(&error_stack).unwrap()).unwrap(),
error_stack,
}
}
}
14 changes: 13 additions & 1 deletion apps/glowsquid/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,36 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

mod auth;
mod error;

use auth::*;
use tauri::{async_runtime::block_on, Manager};

// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
#[tauri::command]
#[specta::specta]
fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)
}

fn main() {
let invoke_handler = {
let builder = tauri_specta::ts::builder()
.commands(tauri_specta::collect_commands![greet, add_account]);

#[cfg(debug_assertions)] // <- Only export on non-release builds
let builder = builder.path("../glowsquid-frontend/src/lib/bindings.ts");

builder.build().unwrap()
};

tauri::Builder::default()
.setup(|app| {
app.manage(block_on(AuthState::new(app.app_handle())));

Ok(())
})
.invoke_handler(tauri::generate_handler![greet, add_account])
.invoke_handler(invoke_handler)
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
1 change: 1 addition & 0 deletions libs/copper/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ serde_json.workspace = true
error-stack.workspace = true
oauth2.workspace = true
reqwest.workspace = true
specta.workspace = true

chrono = { version = "0.4.35", default-features = false, features = ["std"] }
derive_builder = { version = "0.20.0", features = ["clippy"] }
Expand Down
12 changes: 0 additions & 12 deletions libs/copper/src/auth/structs.rs

This file was deleted.

Loading

0 comments on commit c341088

Please sign in to comment.