Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor #11

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest]
rust-version: [stable, beta, nightly]
rust-version: [nightly]
checks:
- advisories
- bans licenses sources
Expand Down Expand Up @@ -46,7 +46,7 @@ jobs:
strategy:
matrix:
os: [windows-latest, ubuntu-latest, macos-latest]
rust-version: [stable, beta, nightly]
rust-version: [nightly]
name: Build with ${{ matrix.rust-version }} on ${{ matrix.os }}
runs-on: ${{ matrix.os }}

Expand Down Expand Up @@ -88,7 +88,7 @@ jobs:
test:
strategy:
matrix:
rust-version: [stable, beta, nightly]
rust-version: [nightly]
os: [windows-latest, ubuntu-latest, macos-latest]
name: Test with rust ${{ matrix.rust-version }} on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
Expand Down
23 changes: 20 additions & 3 deletions client/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "minkan-client"
version = "0.1.0"
edition = "2018"
edition = "2021"
authors = [
"Erik Tesar <[email protected]>"
]
Expand All @@ -22,10 +22,27 @@ serde = { version = "1.0.130", features = ["derive"]}
url = "2.2.2"
async-stream = "0.3.2"
futures = "0.3"
directories = "4.0.1"
downcast-rs = "1.2.0"
typetag = "0.1.7"
sequoia-openpgp = { version = "1.5.0", default-features = false, features = [
"crypto-rust",
"allow-experimental-crypto",
"allow-variable-time-crypto",
"compression"
]}
uuid = { version = "0.8.2", features = ["serde"] }
bytes = { version = "1.1.0", features = ["serde"] }
graphql_client = { version = "0.10.0", features = ["reqwest"]}
# I'd really like to use ciborium since serde_cbor is no longer maintained,
# but it seems like they aren't maintaing ciborium either.
serde_cbor = "0.11.2"

[dev-dependencies]
# used to test async code
tokio-test = "0.4.2"

[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
sqlx = { version = "0.5.9", features = ["sqlite", "runtime-tokio-native-tls"]}
sqlx = { version = "0.5.9", features = ["sqlite", "runtime-tokio-native-tls", "offline"]}

[target.'cfg(target_arch = "wasm32")'.dependencies]
js-sys = "0.3.55"
Expand Down
61 changes: 61 additions & 0 deletions client/sqlx-data.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
{
"db": "SQLite",
"09909c6a0d31e8548b6709b2900416735b5651c4930659bb334fdd28a5b57d55": {
"query": "\n SELECT api_endpoint AS endpoint, nickname FROM servers\n WHERE api_endpoint = $1\n ",
"describe": {
"columns": [
{
"name": "endpoint",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "nickname",
"ordinal": 1,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
true
]
}
},
"7c41905b00b9562519a884cca46a02f9ddb7327586cc72009e5c766c88d67616": {
"query": "\n SELECT api_endpoint AS endpoint, nickname FROM servers\n ",
"describe": {
"columns": [
{
"name": "endpoint",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "nickname",
"ordinal": 1,
"type_info": "Text"
}
],
"parameters": {
"Right": 0
},
"nullable": [
false,
true
]
}
},
"f460c9e58df7e0ba86a6fc16042f59c63597d389cd293394f4e13087c37cea78": {
"query": "\n INSERT INTO servers(api_endpoint, nickname)\n VALUES ($1, $2)\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
}
}
}
24 changes: 24 additions & 0 deletions client/src/actor.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//! Keeps the different types of actors that can appear

use std::fmt::Debug;

use downcast_rs::{impl_downcast, Downcast};
use sequoia_openpgp::Cert;

mod user;

use crate::{seal::Sealed, Node};
#[doc(inline)]
pub use user::User;

// GraphQL tags interfaces (traits in rust) with `__typename`
#[typetag::serde(tag = "__typename")]
/// Everything that can take actions implements the [`Actor`] trait.
pub trait Actor: Sealed + Downcast + Node + Debug {
/// The openpgp certificate of an [`Actor`] from `sequoia-openpgp`
fn certificate(&self) -> &Cert;
/// The name of an [`Actor`] used to identify them
fn name(&self) -> &str;
}
// actor types support downcasting
impl_downcast!(Actor);
39 changes: 39 additions & 0 deletions client/src/actor/user.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
use crate::{seal::Sealed, Node};

use super::Actor;
use sequoia_openpgp::Cert;
use serde::{Deserialize, Serialize};
use uuid::Uuid;

#[derive(Debug, Deserialize, Serialize)]
#[non_exhaustive]
/// Represents a real person using the application
pub struct User {
id: Uuid,
name: String,
#[serde(
rename = "certificate",
serialize_with = "crate::serialize_cert",
deserialize_with = "crate::deserialize_cert"
)]
cert: Cert,
}

impl Sealed for User {}

impl Node for User {
fn id(&self) -> &Uuid {
&self.id
}
}

#[typetag::serde]
impl Actor for User {
fn certificate(&self) -> &Cert {
&self.cert
}

fn name(&self) -> &str {
&self.name
}
}
55 changes: 43 additions & 12 deletions client/src/application.rs
Original file line number Diff line number Diff line change
@@ -1,24 +1,55 @@
use directories::ProjectDirs;

use crate::{database::Database, Result};
use crate::{database::DatabaseError, Result};

#[non_exhaustive]
#[derive(Debug)]
#[derive(Debug, Clone)]
/// The base application
///
/// It keeps track where to store the data for different server instances
/// It keeps track where to store the application data
pub struct Application {
// the database, the application will use
database: Database,
#[cfg(not(target_arch = "wasm32"))]
/// On native builds, we'll use a sqlite database for better performance
pool: sqlx::SqlitePool,
}

impl Application {
/// Creates a new [`Application`] instance. If ``path`` is set,
/// it will try to use that as the project dir.
#[cfg(not(target_arch = "wasm32"))]
/// Returns the underlying database pool
pub(crate) fn pool(&self) -> &sqlx::SqlitePool {
&self.pool
}
}
impl Application {
/// Creates a new [`Application`] instance. If `uri` is set,
/// it will try to use that uri for the SQLite database driver.
/// Returns [`crate::Error::Other`] if it can't initalize a database.
///
/// # Note
/// On wasm targets, ``path`` will be ignored
pub async fn new(_path: impl Into<ProjectDirs>) -> Result<Self> {
todo!()
/// On wasm targets, `uri` will be ignored.
///
/// # Example
///
/// ```
/// # use minkan_client::Application;
/// # tokio_test::block_on(async {
/// let app = Application::new("sqlite::memory:").await.unwrap();
/// # })
pub async fn new(uri: impl AsRef<str>) -> Result<Self> {
#[cfg(not(target_arch = "wasm32"))]
let pool = {
let pool = sqlx::SqlitePool::connect(uri.as_ref())
.await
.map_err(|e| DatabaseError::OpenError(e.to_string()))?;

sqlx::migrate!("./migrations/")
.run(&pool)
.await
.map_err(|e| DatabaseError::MigrationError(e.to_string()))?;
pool
};
#[cfg(target_arch = "wasm32")]
{
todo!("database backend for wasm is not ready yet")
}
Ok(Self { pool })
}
}
56 changes: 56 additions & 0 deletions client/src/challenge.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//! Proof the ownership of a key to a [`Server`]
use bytes::Bytes;
use graphql_client::{GraphQLQuery, QueryBody};

use crate::{server::Server, util::perform_query, Result};
use serde::{Deserialize, Serialize};

#[derive(Debug, PartialEq, Eq, Deserialize, Serialize, Clone)]
#[non_exhaustive]
/// A random challenge used by the [`crate::server::Server`] to ensure that the
/// [`crate::actor::Actor`] has control over the primary key of a [`sequoia_openpgp::Cert`]
pub struct Challenge {
/// The actual challenge hex string
challenge: Bytes,
}

impl Challenge {
/// Request a [`Challenge`] from a [`Server`]
///
/// # Example
///
/// Note: This example is not tested because it needs a running backend server
/// ```ignore
/// # use url::Url;
/// # use minkan_client::server::Server;
/// # use minkan_client::Application;
/// # use minkan_client::challenge::Challenge;
/// # tokio_test::block_on( async {
/// let api_endpoint = Url::parse("http://localhost:8000/graphql").unwrap();
///
/// // the server we request the challange from
/// let server = Server::new(api_endpoint, None).await.unwrap();
///
/// let challenge = Challenge::request(&server).await.unwrap();
///
/// // and you can compare them (but they should never be the same anyway)
/// assert!(challenge == challenge);
///
/// # })
/// ```
pub async fn request(server: &Server) -> Result<Self> {
perform_query::<Self>(Self::build_query(()), server).await
}
}

impl GraphQLQuery for Challenge {
type Variables = ();
type ResponseData = Self;
fn build_query(variables: Self::Variables) -> QueryBody<Self::Variables> {
QueryBody {
variables,
query: include_str!("../../other/graphql/queries/get_challenge.graphql"),
operation_name: "getChallenge",
}
}
}
74 changes: 57 additions & 17 deletions client/src/database.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,61 @@
//! Data storage and different database backends
use async_trait::async_trait;
use futures::Stream;
use thiserror::Error;

#[cfg(target_arch = "wasm")]
pub mod indexed_db;
#[cfg(not(target_arch = "wasm"))]
pub mod sqlite;
/// A database that will be used to store application data
use crate::{seal::Sealed, Result};

#[cfg(target_arch = "wasm32")]
pub(crate) mod indexed_db;
#[cfg(not(target_arch = "wasm32"))]
pub(crate) mod sqlite;

#[async_trait]
/// A trait for all objects that can be inserted into the database
///
/// This struct is used to abstract different database backends
// Usually, you probably want to use a trait for this but since async traits
// and especially streams in async traits are really not a real thing and we
// don't expose this struct, it's okay to just use different implementations
// on different backends (see [`self::indexed_db`] and [`self::sqlite`])
#[derive(Debug)]
pub struct Database {
#[cfg(not(target_arch = "wasm32"))]
db: sqlx::SqlitePool,
#[cfg(target_arch = "wasm32")]
// guess thats the right thing?
db: web_sys::IdbOpenDbRequest,
/// # Note
/// This trait is sealed to allow future extension
pub trait Insert: Sealed {
/// The parent of this struct. Same as the `Parent` in the [`Get`] trait
type Parent;
/// Inserts the instance of [`Self`] into the database.
async fn insert(&self, p: &Self::Parent) -> Result<()>;
}

#[async_trait]
/// A trait for objects that are stored locally and can be retrieved
///
/// # Note
/// This trait is sealed to allow future extension
pub trait Get: Sealed + Sized {
/// [`crate::Application`] is the root parent. For example, a
/// [`crate::server::Server`] depends on an [`crate::Application`]
type Parent;
/// The type that is used to identify an object. This could be an [`u32`]
/// or something else.
type Identifier;
/// Workaround for streams in traits
type Stream<'a>: Stream<Item = Result<Self>> + 'a;
/// Returns a [`Stream`] of all items
fn get_all(app: &Self::Parent) -> Self::Stream<'_>;
/// Returns a single item
async fn get(i: &Self::Identifier, p: &Self::Parent) -> Result<Option<Self>>;
}

#[derive(Debug, Error)]
/// Errors that happen during database operations
pub enum DatabaseError {
#[error("failed to open the database: {0}")]
/// Used if we can't open the database. This could be the case because the
/// user denied access to indexedDB in a webapp or because sqlx failed to
/// open a connection to the sqlite url
OpenError(String),
#[error("error during database migration: {0}")]
/// Used if a database migration fails.
/// At an application level, there's nothing you can do except using another
/// database for application startup
MigrationError(String),
#[error("database error")]
/// Errors that are not handled in any special way
Other,
}
Loading