Compare commits
7 commits
56dfa16b84
...
8b9fb53ca5
Author | SHA1 | Date | |
---|---|---|---|
8b9fb53ca5 | |||
61a3c10377 | |||
474b4cafab | |||
78136ac622 | |||
1e8b7a1105 | |||
49a843f853 | |||
2b783805ea |
16 changed files with 228 additions and 216 deletions
9
.forgejo/workflows/test.yml
Normal file
9
.forgejo/workflows/test.yml
Normal file
|
@ -0,0 +1,9 @@
|
|||
on: [push]
|
||||
jobs:
|
||||
test:
|
||||
name: cargo test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- run: cargo test --all-features
|
65
Cargo.lock
generated
65
Cargo.lock
generated
|
@ -248,10 +248,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.0"
|
||||
name = "cfg_aliases"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "80c21025abd42669a92efc996ef13cfb2c5c627858421ea58d5c3b331a6c134f"
|
||||
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "949626d00e063efc93b6dca932419ceb5432f99769911c0b995f7e884c778813"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
|
@ -259,9 +265,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.0"
|
||||
version = "4.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "458bf1f341769dfcf849846f65dffdf9146daa56bcd2a47cb4e1de9915567c99"
|
||||
checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
|
@ -271,9 +277,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.0"
|
||||
version = "4.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "307bc0538d5f0f83b8248db3087aa92fe504e4691294d0c96c0eabc33f47ba47"
|
||||
checksum = "90239a040c80f5e14809ca132ddc4176ab33d5e17e49691793296e3fcb34d72f"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
|
@ -511,15 +517,15 @@ checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
|
|||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.4.1"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.3.5"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0c62115964e08cb8039170eb33c1d0e2388a256930279edca206fff675f82c3"
|
||||
checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7"
|
||||
|
||||
[[package]]
|
||||
name = "home"
|
||||
|
@ -786,12 +792,13 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.27.1"
|
||||
version = "0.28.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053"
|
||||
checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
]
|
||||
|
||||
|
@ -817,9 +824,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.32.2"
|
||||
version = "0.32.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441"
|
||||
checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
@ -932,9 +939,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.11.24"
|
||||
version = "0.11.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6920094eb85afde5e4a138be3f2de8bbdf28000f0029e72c45025a56b042251"
|
||||
checksum = "78bf93c4af7a8bb7d879d51cebe797356ff10ae8516ace542b5182d9dcac10b2"
|
||||
dependencies = [
|
||||
"async-compression",
|
||||
"base64",
|
||||
|
@ -1067,18 +1074,18 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.196"
|
||||
version = "1.0.197"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32"
|
||||
checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.196"
|
||||
version = "1.0.197"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67"
|
||||
checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -1087,9 +1094,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.113"
|
||||
version = "1.0.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79"
|
||||
checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"ryu",
|
||||
|
@ -1213,9 +1220,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.10.0"
|
||||
version = "3.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a365e8cd18e44762ef95d87f284f4b5cd04107fec2ff3052bd6a3e6069669e67"
|
||||
checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"fastrand",
|
||||
|
@ -1225,18 +1232,18 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.57"
|
||||
version = "1.0.58"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b"
|
||||
checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.57"
|
||||
version = "1.0.58"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81"
|
||||
checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
14
Cargo.toml
14
Cargo.toml
|
@ -16,15 +16,15 @@ tokio = { version = "1.36.0", features = ["full"], optional = true }
|
|||
# dashmap = { version = "5.5.3", features = ["inline"]}
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = { version = "0.3.18", features = ["fmt"] }
|
||||
serde = { version = "1.0.196", features = ["derive"] }
|
||||
serde_json = "1.0.113"
|
||||
thiserror = "1.0.57"
|
||||
clap = { version = "4.5.0", features = ["derive"]}
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
serde_json = "1.0.114"
|
||||
thiserror = "1.0.58"
|
||||
clap = { version = "4.5.3", features = ["derive"]}
|
||||
axum = { version = "0.7.4", optional = true }
|
||||
reqwest = { version = "0.11.24", optional = true, default-features = false, features = ["rustls-tls", "blocking", "json", "gzip", "brotli", "deflate"] }
|
||||
tempfile = { version = "3.10.0", optional = true }
|
||||
reqwest = { version = "0.11.26", optional = true, default-features = false, features = ["rustls-tls", "blocking", "json", "gzip", "brotli", "deflate"] }
|
||||
tempfile = { version = "3.10.1", optional = true }
|
||||
which = { version = "6.0.0", optional = true }
|
||||
nix = { version = "0.27.1", optional = true, default-features = false, features = ["signal"] }
|
||||
nix = { version = "0.28.0", optional = true, default-features = false, features = ["signal"] }
|
||||
urlencoding = { version = "2.1.3"}
|
||||
|
||||
[profile.release] # 💛 @Ryze@equestria.social
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
//! Commands to run from the command line other than launching a server instance
|
||||
pub mod editor;
|
||||
pub mod fetch;
|
||||
pub mod init;
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
//! Command for editing the record associated with a provided query string
|
||||
//! in the system-defined editor
|
||||
|
||||
use crate::args_parser::SaveSettings;
|
||||
use crate::editor::open_in_editor::open_resource_in_editor;
|
||||
use crate::schema::lookup_handler::LookupHandler;
|
||||
|
@ -5,7 +8,12 @@ use crate::schema::resource::Resource;
|
|||
use std::path::PathBuf;
|
||||
use tracing::{error, info};
|
||||
|
||||
pub fn editor(database_path: PathBuf, save_settings: SaveSettings, resource_lookup: String, create_new_resource: bool) {
|
||||
pub fn editor(
|
||||
database_path: PathBuf,
|
||||
save_settings: SaveSettings,
|
||||
resource_lookup: String,
|
||||
create_new_resource: bool,
|
||||
) {
|
||||
let resources = LookupHandler::load(&database_path).unwrap();
|
||||
|
||||
let (index, resource) = match resources.lookup_with_index(resource_lookup.as_str()) {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
use crate::args_parser::SaveSettings;
|
||||
use crate::editor::finger_remote::finger_many_fedi;
|
||||
use crate::schema::resource_list::ResourceList;
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::path::PathBuf;
|
||||
use tracing::{debug, error, info, instrument, trace, warn};
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
//! Utilities for running a WebFinger query against a remote target
|
||||
|
||||
use crate::editor::finger_remote::FediFingerError::UrlBuildFailed;
|
||||
use crate::schema::resource::Resource;
|
||||
use reqwest::blocking::Client;
|
||||
|
@ -6,7 +8,7 @@ use std::fmt::Display;
|
|||
use thiserror::Error;
|
||||
use tracing::{debug, error, instrument};
|
||||
|
||||
/// Error type returned by `finger_url`
|
||||
/// Error type returned by [`finger_url`]
|
||||
#[derive(Debug, Error)]
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
pub enum FingerError {
|
||||
|
@ -18,7 +20,7 @@ pub enum FingerError {
|
|||
ParseFailed(reqwest::Error),
|
||||
}
|
||||
|
||||
/// Run a WebFinger request at the provided URL and parse it as a `Resource`
|
||||
/// Run a WebFinger request at the provided URL and parse it as a [`Resource`]
|
||||
#[instrument(skip(client))]
|
||||
pub fn finger_url(client: &Client, url: Url) -> Result<Resource, FingerError> {
|
||||
use FingerError::*;
|
||||
|
@ -31,7 +33,7 @@ pub fn finger_url(client: &Client, url: Url) -> Result<Resource, FingerError> {
|
|||
Ok(response)
|
||||
}
|
||||
|
||||
/// Error type returned by `finger_fedi`
|
||||
/// Error type returned by [`finger_fedi`]
|
||||
#[derive(Debug, Error)]
|
||||
pub enum FediFingerError {
|
||||
#[error("Couldn't find the middle @ symbol")]
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
//! Utilities for editing records in a database stored on disk
|
||||
pub mod commands;
|
||||
pub mod finger_remote;
|
||||
pub mod open_in_editor;
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
//! Utilities for opening a string in the system-defined editor
|
||||
|
||||
use crate::schema::resource::Resource;
|
||||
use std::fmt::{Debug, Display};
|
||||
use std::io::{Read, Seek, SeekFrom, Write};
|
||||
|
@ -6,7 +8,7 @@ use tempfile::NamedTempFile;
|
|||
use thiserror::Error;
|
||||
use tracing::{debug, instrument, trace, warn};
|
||||
|
||||
/// Error type returned by `spawn_editor`
|
||||
/// Error type returned by [`spawn_editor`]
|
||||
#[derive(Debug, Error)]
|
||||
pub enum EditorSpawnError {
|
||||
#[error("failed to parse out absolute path of editor: {0}")]
|
||||
|
@ -27,7 +29,8 @@ pub enum EditorSpawnError {
|
|||
BufferReadbackFailed(std::io::Error),
|
||||
}
|
||||
|
||||
/// Spawn the system editor to edit the provided `buffer`. Returns the edited buffer or an error if something goes wrong
|
||||
/// Spawn the system editor to edit the provided `buffer`.
|
||||
/// Returns the edited buffer or an error if something goes wrong
|
||||
#[instrument(skip_all)]
|
||||
pub fn spawn_editor(buffer: impl AsRef<str> + Display) -> Result<String, EditorSpawnError> {
|
||||
use EditorSpawnError::*;
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
//! Utility functions for attempting to reload a running server instance of the application
|
||||
|
||||
use crate::args_parser::ServerReloadOptions;
|
||||
use nix::sys::signal::{kill, Signal};
|
||||
use nix::unistd::Pid;
|
||||
|
@ -7,8 +9,9 @@ use std::str::FromStr;
|
|||
use thiserror::Error;
|
||||
use tracing::{error, info};
|
||||
|
||||
/// Error type returned by [`try_reload_server`] and [`load_pid`]
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ServerReloadError {
|
||||
enum ServerReloadError {
|
||||
#[error("Couldn't find PID file at {0}")]
|
||||
PIDFileNotFound(String),
|
||||
#[error("Failed to open PID file: {0}")]
|
||||
|
@ -21,6 +24,7 @@ pub enum ServerReloadError {
|
|||
FailedToSendSignal(#[from] nix::errno::Errno),
|
||||
}
|
||||
|
||||
/// Tries to load and parse the PID file at the provided path
|
||||
fn load_pid(pid_file_path: &Path) -> Result<Pid, ServerReloadError> {
|
||||
use ServerReloadError::*;
|
||||
if !pid_file_path.exists() {
|
||||
|
@ -34,12 +38,14 @@ fn load_pid(pid_file_path: &Path) -> Result<Pid, ServerReloadError> {
|
|||
Ok(Pid::from_raw(pid))
|
||||
}
|
||||
|
||||
/// Tries to send a reload command to the server instance
|
||||
fn try_reload_server(pid_file_path: &Path) -> Result<(), ServerReloadError> {
|
||||
let pid = load_pid(pid_file_path)?;
|
||||
kill(pid, Signal::SIGHUP)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempts to reload an already running server instance of the application, logging an error if it was unsuccessful.
|
||||
pub fn reload(pid_file_path: PathBuf, options: ServerReloadOptions) {
|
||||
if options.reload_server {
|
||||
info!("Attempting to reload server...");
|
||||
|
|
22
src/main.rs
22
src/main.rs
|
@ -1,16 +1,16 @@
|
|||
use crate::args_parser::Command;
|
||||
use crate::editor::commands::editor::editor;
|
||||
use crate::editor::commands::init::init;
|
||||
use crate::editor::commands::list::list;
|
||||
use crate::editor::try_reload_server::reload;
|
||||
use clap::Parser;
|
||||
use editor::commands::fetch::fetch;
|
||||
use editor::commands::query::query;
|
||||
use tracing::error;
|
||||
use tracing_subscriber::util::SubscriberInitExt;
|
||||
|
||||
mod args_parser;
|
||||
#[cfg(feature = "editor")]
|
||||
mod editor;
|
||||
#[cfg(feature = "editor")]
|
||||
use editor::{
|
||||
commands::{editor::editor, fetch::fetch, init::init, list::list, query::query},
|
||||
try_reload_server::reload,
|
||||
};
|
||||
mod schema;
|
||||
#[cfg(feature = "server")]
|
||||
mod server;
|
||||
|
@ -30,6 +30,7 @@ fn main() {
|
|||
..
|
||||
} = args;
|
||||
match run_mode {
|
||||
#[allow(unused_variables)] // For when the server feature is compiled out
|
||||
Command::Serve(params) => {
|
||||
#[cfg(not(feature = "server"))]
|
||||
{
|
||||
|
@ -38,6 +39,7 @@ fn main() {
|
|||
#[cfg(feature = "server")]
|
||||
server::init(data_paths, params);
|
||||
}
|
||||
#[cfg(feature = "editor")]
|
||||
Command::Fetch {
|
||||
save,
|
||||
handles,
|
||||
|
@ -54,6 +56,7 @@ fn main() {
|
|||
);
|
||||
reload(data_paths.pid_file_path, server_reload);
|
||||
}
|
||||
#[cfg(feature = "editor")]
|
||||
Command::Query {
|
||||
save,
|
||||
resource,
|
||||
|
@ -64,6 +67,7 @@ fn main() {
|
|||
|
||||
reload(data_paths.pid_file_path, server_reload);
|
||||
}
|
||||
#[cfg(feature = "editor")]
|
||||
Command::Editor {
|
||||
save,
|
||||
resource,
|
||||
|
@ -74,11 +78,17 @@ fn main() {
|
|||
|
||||
reload(data_paths.pid_file_path, server_reload);
|
||||
}
|
||||
#[cfg(feature = "editor")]
|
||||
Command::Init { force } => {
|
||||
init(data_paths.database_path, force);
|
||||
}
|
||||
#[cfg(feature = "editor")]
|
||||
Command::List {} => {
|
||||
list(data_paths.database_path);
|
||||
}
|
||||
#[allow(unreachable_patterns)] // This is a catch-all if the editor feature is disabled
|
||||
_ => {
|
||||
error!("The requested command is not supported by this build. Please rebuild with the \"editor\" feature enabled")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
//! Struct for storing WebFinger resources and running queries against them
|
||||
|
||||
use crate::schema::resource::Resource;
|
||||
use crate::schema::resource_list::{ResourceList, ResourceLoadError};
|
||||
use std::collections::HashMap;
|
||||
|
@ -25,25 +27,31 @@ pub enum DataLoadError {
|
|||
}
|
||||
|
||||
impl LookupHandler {
|
||||
/// Load and prepare a new LookupHandler from the file at `path`
|
||||
/// Load and prepare a new [`LookupHandler`] from the file at `path`
|
||||
#[instrument(level = "debug", skip(path))]
|
||||
pub fn load(path: impl AsRef<Path> + Debug) -> Result<Self, DataLoadError> {
|
||||
Self::build_from_resource_list(ResourceList::load(path)?)
|
||||
}
|
||||
|
||||
/// Load and prepare a new [`LookupHandler`] from the provided reader
|
||||
#[cfg(test)]
|
||||
fn load_from_reader(reader: impl std::io::Read) -> Result<Self, DataLoadError> {
|
||||
Self::build_from_resource_list(ResourceList::load_from_reader(reader)?)
|
||||
}
|
||||
|
||||
/// Build a new [`LookupHandler`] from the provided [`ResourceList`]
|
||||
fn build_from_resource_list(resources: ResourceList) -> Result<Self, DataLoadError> {
|
||||
let mut lookup = HashMap::new();
|
||||
debug!("Building lookup map...");
|
||||
|
||||
let mut lookup = HashMap::new();
|
||||
for (index, resource) in resources.0.iter().enumerate() {
|
||||
for lookup_to_add in resource.keys() {
|
||||
let lookup_to_add = lookup_to_add.to_lowercase();
|
||||
|
||||
debug!("Adding {lookup_to_add} for {}", resource.subject);
|
||||
if let Some(duplicate) = lookup.insert(lookup_to_add.clone(), index) {
|
||||
let duplicate = lookup.insert(lookup_to_add.clone(), index);
|
||||
|
||||
if let Some(duplicate) = duplicate {
|
||||
return Err(DataLoadError::DuplicateLookupFound {
|
||||
duplicated: lookup_to_add,
|
||||
subjects: [
|
||||
|
@ -54,24 +62,33 @@ impl LookupHandler {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!("Aggregated {} lookup strings", lookup.len());
|
||||
Ok(LookupHandler { resources, lookup })
|
||||
}
|
||||
|
||||
/// Run the provided `resource_query` on the index and return a borrow of the returned resource
|
||||
#[instrument(level = "debug")]
|
||||
pub fn lookup(&self, resource_query: &str) -> Option<&Resource> {
|
||||
self.lookup_with_index(resource_query)
|
||||
.map(|(_index, resource)| resource)
|
||||
}
|
||||
|
||||
/// Run the provided `resource_query` on the index and
|
||||
/// return a borrow of the returned resource and its index in the database
|
||||
pub fn lookup_with_index(&self, resource_query: &str) -> Option<(usize, &Resource)> {
|
||||
let resource_query = resource_query.to_lowercase();
|
||||
|
||||
let resource_index = *self.lookup.get(resource_query.as_str())?;
|
||||
|
||||
let found_resource = &self.resources.0[resource_index];
|
||||
|
||||
debug!("Lookup for {resource_query} returned {found_resource:?}");
|
||||
Some((resource_index, found_resource))
|
||||
}
|
||||
|
||||
/// Convert the [`LookupHandler`] into its internal [`ResourceList`]
|
||||
#[allow(dead_code)]
|
||||
pub fn into_inner(self) -> ResourceList {
|
||||
self.resources
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
//! "Schema" of the internal representation of WebFinger data
|
||||
|
||||
pub mod lookup_handler;
|
||||
pub mod resource;
|
||||
pub mod resource_list;
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
//! A single WebFinger resource and associated utility functions
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::{Debug, Display, Formatter};
|
||||
|
@ -6,11 +8,15 @@ use tracing::debug;
|
|||
/// A single WebFinger resource
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Resource {
|
||||
/// The subject of this resource
|
||||
pub subject: String,
|
||||
/// Known aliases of the resource to also respond to
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub aliases: Option<Vec<String>>,
|
||||
/// List of properties associated with the resource
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub properties: Option<HashMap<String, Option<String>>>,
|
||||
/// Links associated with the resource
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub links: Option<Vec<Link>>,
|
||||
}
|
||||
|
@ -23,6 +29,7 @@ impl Display for Resource {
|
|||
}
|
||||
|
||||
impl Resource {
|
||||
/// Creates a new completely blank WebFinger resource with the provided subject
|
||||
pub fn new(subject: String) -> Self {
|
||||
Self {
|
||||
subject,
|
||||
|
@ -33,7 +40,7 @@ impl Resource {
|
|||
}
|
||||
|
||||
/// Returns the aliases of the given record. If the `aliases` field is
|
||||
/// entirely missing, returns &[].
|
||||
/// entirely missing, returns an empty array.
|
||||
pub fn keys(&self) -> impl Iterator<Item = &String> {
|
||||
let aliases = if let Some(aliases) = &self.aliases {
|
||||
aliases.as_slice()
|
||||
|
@ -44,13 +51,18 @@ impl Resource {
|
|||
aliases.iter().chain(std::iter::once(&self.subject))
|
||||
}
|
||||
|
||||
/// Replaces the current `subject` field of the WebFinger resource
|
||||
/// with a new value and pushes the old value into the aliases field
|
||||
/// if it is not already present there
|
||||
pub fn add_new_primary_subject(&mut self, mut subject: String) {
|
||||
if subject == self.subject {
|
||||
debug!("New and old subjects match, skipping...");
|
||||
return;
|
||||
}
|
||||
|
||||
debug!("Swapping new and old subject");
|
||||
std::mem::swap(&mut subject, &mut self.subject);
|
||||
|
||||
debug!("Pushing current subject into aliases");
|
||||
if let Some(ref mut aliases) = self.aliases {
|
||||
if !aliases.contains(&subject) {
|
||||
|
@ -68,27 +80,45 @@ impl Resource {
|
|||
/// and instead replaced with `Some(Default::default())`. Useful to provide an easier to
|
||||
/// extend version of the data for manual editing.
|
||||
pub fn as_completely_serializable(&self) -> Self {
|
||||
let clone_hashmap_with_option_value_as_complete = |props: &HashMap<String, Option<String>>| {
|
||||
HashMap::from_iter(props.iter().map(|(key, value)| {
|
||||
(key.clone(), Some(value.clone().unwrap_or_default()))
|
||||
}))
|
||||
};
|
||||
let clone_hashmap_with_option_value_as_complete =
|
||||
|props: &HashMap<String, Option<String>>| {
|
||||
HashMap::from_iter(
|
||||
props
|
||||
.iter()
|
||||
.map(|(key, value)| (key.clone(), Some(value.clone().unwrap_or_default()))),
|
||||
)
|
||||
};
|
||||
|
||||
Self {
|
||||
subject: self.subject.clone(),
|
||||
aliases: Some(self.aliases.clone().unwrap_or_default()),
|
||||
properties: Some(self.properties.as_ref().map(clone_hashmap_with_option_value_as_complete).unwrap_or_default()),
|
||||
links: Some(self.links.as_ref().map(|links| {
|
||||
Vec::from_iter(links.iter().map(|link| {
|
||||
Link {
|
||||
rel: link.rel.clone(),
|
||||
media_type: Some(link.media_type.clone().unwrap_or_default()),
|
||||
href: Some(link.href.clone().unwrap_or_default()),
|
||||
titles: Some(link.titles.clone().unwrap_or_default()),
|
||||
properties: Some(link.properties.as_ref().map(clone_hashmap_with_option_value_as_complete).unwrap_or_default()),
|
||||
}
|
||||
}))
|
||||
}).unwrap_or_default()),
|
||||
properties: Some(
|
||||
self.properties
|
||||
.as_ref()
|
||||
.map(clone_hashmap_with_option_value_as_complete)
|
||||
.unwrap_or_default(),
|
||||
),
|
||||
links: Some(
|
||||
self.links
|
||||
.as_ref()
|
||||
.map(|links| {
|
||||
Vec::from_iter(links.iter().map(|link| {
|
||||
Link {
|
||||
rel: link.rel.clone(),
|
||||
media_type: Some(link.media_type.clone().unwrap_or_default()),
|
||||
href: Some(link.href.clone().unwrap_or_default()),
|
||||
titles: Some(link.titles.clone().unwrap_or_default()),
|
||||
properties: Some(
|
||||
link.properties
|
||||
.as_ref()
|
||||
.map(clone_hashmap_with_option_value_as_complete)
|
||||
.unwrap_or_default(),
|
||||
),
|
||||
}
|
||||
}))
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -99,32 +129,50 @@ impl Resource {
|
|||
Self {
|
||||
subject: self.subject,
|
||||
aliases: self.aliases.filter(|data| !data.is_empty()),
|
||||
properties: self.properties.filter(|data| !data.is_empty()).map(|mut data| {
|
||||
for value in data.values_mut() {
|
||||
if let Some(ref mut string) = value {
|
||||
if string.is_empty() {
|
||||
*value = None;
|
||||
properties: self
|
||||
.properties
|
||||
.filter(|data| !data.is_empty())
|
||||
.map(|mut data| {
|
||||
for value in data.values_mut() {
|
||||
if let Some(ref mut string) = value {
|
||||
if string.is_empty() {
|
||||
*value = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
data
|
||||
}),
|
||||
links: self.links.filter(|links| !links.is_empty()).map(|mut links| {
|
||||
links.retain(|link| {
|
||||
// Empty `rel` is invalid, but short-circuiting here would delete records
|
||||
// that are only partially edited. Better to store invalid data than to delete
|
||||
// users' work.
|
||||
let mut is_default = link.rel.is_empty();
|
||||
is_default &= link.media_type.as_ref().filter(|media_type| !media_type.is_empty()).is_none();
|
||||
is_default &= link.href.as_ref().filter(|href| !href.is_empty()).is_none();
|
||||
is_default &= link.titles.as_ref().filter(|titles| !titles.is_empty()).is_none();
|
||||
is_default &= link.properties.as_ref().filter(|titles| !titles.is_empty()).is_none();
|
||||
data
|
||||
}),
|
||||
links: self
|
||||
.links
|
||||
.filter(|links| !links.is_empty())
|
||||
.map(|mut links| {
|
||||
links.retain(|link| {
|
||||
// Empty `rel` is invalid, but short-circuiting here would delete records
|
||||
// that are only partially edited. Better to store invalid data than to delete
|
||||
// users' work.
|
||||
let mut is_default = link.rel.is_empty();
|
||||
is_default &= link
|
||||
.media_type
|
||||
.as_ref()
|
||||
.filter(|media_type| !media_type.is_empty())
|
||||
.is_none();
|
||||
is_default &= link.href.as_ref().filter(|href| !href.is_empty()).is_none();
|
||||
is_default &= link
|
||||
.titles
|
||||
.as_ref()
|
||||
.filter(|titles| !titles.is_empty())
|
||||
.is_none();
|
||||
is_default &= link
|
||||
.properties
|
||||
.as_ref()
|
||||
.filter(|titles| !titles.is_empty())
|
||||
.is_none();
|
||||
|
||||
is_default
|
||||
});
|
||||
is_default
|
||||
});
|
||||
|
||||
links
|
||||
})
|
||||
links
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -147,14 +195,13 @@ pub struct Link {
|
|||
/// Functions to generate data for testing functions that manipulate `Resource` structs
|
||||
pub mod test_data {
|
||||
use crate::schema::resource::Resource;
|
||||
|
||||
/// A [`Resource`] with only the `subject` field set
|
||||
pub fn barebones_user() -> Resource {
|
||||
Resource {
|
||||
subject: "acct:user@domain.tld".to_string(),
|
||||
aliases: None,
|
||||
properties: None,
|
||||
links: None,
|
||||
}
|
||||
Resource::new("acct:user@domain.tld".to_string())
|
||||
}
|
||||
|
||||
/// A default [`Resource`] with a single alias
|
||||
pub fn user_with_single_alias() -> Resource {
|
||||
Resource {
|
||||
subject: "acct:user@domain.tld".to_string(),
|
||||
|
@ -164,6 +211,7 @@ pub mod test_data {
|
|||
}
|
||||
}
|
||||
|
||||
/// A default [`Resource`] with a single alias that matches the subject
|
||||
pub fn user_with_matching_subject_and_alias() -> Resource {
|
||||
Resource {
|
||||
subject: "acct:user@domain.tld".to_string(),
|
||||
|
@ -239,7 +287,7 @@ mod tests {
|
|||
for data in [
|
||||
test_data::barebones_user(),
|
||||
test_data::user_with_matching_subject_and_alias(),
|
||||
test_data::user_with_single_alias()
|
||||
test_data::user_with_single_alias(),
|
||||
] {
|
||||
assert_eq!(data, data.as_completely_serializable().compress());
|
||||
}
|
||||
|
|
|
@ -1,112 +0,0 @@
|
|||
use crate::schema::resource::{Link, Resource};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Same as `Resource`, but doesn't have any of the skip_serializing_if fields and options set so the structure is more friendly to editing through the `editor` command
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ResourceComplete {
|
||||
subject: String,
|
||||
aliases: Vec<String>,
|
||||
properties: HashMap<String, Option<String>>,
|
||||
links: Vec<LinkComplete>,
|
||||
}
|
||||
|
||||
impl ResourceComplete {
|
||||
pub fn from_resource(value: Resource) -> Self {
|
||||
let Resource {
|
||||
subject,
|
||||
aliases,
|
||||
properties,
|
||||
links,
|
||||
} = value;
|
||||
Self {
|
||||
subject,
|
||||
aliases: aliases.unwrap_or_default(),
|
||||
properties: properties.unwrap_or_default(),
|
||||
links: links
|
||||
.map(|vec| vec.into_iter().map(LinkComplete::from_link).collect())
|
||||
.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_resource(self) -> Resource {
|
||||
let ResourceComplete {
|
||||
subject,
|
||||
aliases,
|
||||
properties,
|
||||
links,
|
||||
} = self;
|
||||
Resource {
|
||||
subject,
|
||||
aliases: if aliases.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(aliases)
|
||||
},
|
||||
properties: if properties.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(properties)
|
||||
},
|
||||
links: if links.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(links.into_iter().map(LinkComplete::into_link).collect())
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Same as `Link`, but doesn't have any of the skip_serializing_if fields set so the structure is more friendly to editing through the `editor` command
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct LinkComplete {
|
||||
rel: String,
|
||||
media_type: Option<String>,
|
||||
href: Option<String>,
|
||||
titles: HashMap<String, String>,
|
||||
properties: HashMap<String, Option<String>>,
|
||||
}
|
||||
|
||||
impl LinkComplete {
|
||||
fn from_link(value: Link) -> Self {
|
||||
let Link {
|
||||
rel,
|
||||
media_type,
|
||||
href,
|
||||
titles,
|
||||
properties,
|
||||
} = value;
|
||||
Self {
|
||||
rel,
|
||||
media_type,
|
||||
href,
|
||||
titles: titles.unwrap_or_default(),
|
||||
properties: properties.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn into_link(self) -> Link {
|
||||
let LinkComplete {
|
||||
rel,
|
||||
media_type,
|
||||
href,
|
||||
titles,
|
||||
properties,
|
||||
} = self;
|
||||
Link {
|
||||
rel,
|
||||
media_type,
|
||||
href,
|
||||
titles: if titles.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(titles)
|
||||
},
|
||||
properties: if properties.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(properties)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
//! An array of [`Resource`]s with a builtin way to serialise and deserialise to file and/or stream
|
||||
|
||||
use crate::args_parser::CollisionHandling;
|
||||
use crate::schema::resource::Resource;
|
||||
use std::collections::HashSet;
|
||||
|
@ -6,9 +8,11 @@ use std::path::Path;
|
|||
use thiserror::Error;
|
||||
use tracing::{debug, info, instrument, trace, warn};
|
||||
|
||||
/// A dynamic array of [`Resource`]s
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ResourceList(pub Vec<Resource>);
|
||||
|
||||
/// Error type returned by [`ResourceList::load`] and [`ResourceList::load_from_reader`]
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ResourceLoadError {
|
||||
#[error("failed to open the resource database: {0}")]
|
||||
|
@ -17,7 +21,9 @@ pub enum ResourceLoadError {
|
|||
FileParse(#[from] serde_json::Error),
|
||||
}
|
||||
|
||||
/// Error type returned by [`ResourceList::save`] and [`ResourceList::save_to_writer`]
|
||||
#[derive(Debug, Error)]
|
||||
#[allow(unused)]
|
||||
pub enum ResourceSaveError {
|
||||
#[error("Failed to open the resource database for writing: {0}")]
|
||||
FileOpen(std::io::Error),
|
||||
|
@ -28,7 +34,7 @@ pub enum ResourceSaveError {
|
|||
}
|
||||
|
||||
impl ResourceList {
|
||||
/// Loads the `Resource`s from the given `path`
|
||||
/// Loads the [`Resource`]s from the given `path`
|
||||
#[instrument(level = "debug")]
|
||||
pub fn load(path: impl AsRef<Path> + Debug) -> Result<Self, ResourceLoadError> {
|
||||
info!("Loading data from {path:?}...");
|
||||
|
@ -36,7 +42,7 @@ impl ResourceList {
|
|||
Self::load_from_reader(file)
|
||||
}
|
||||
|
||||
/// Loads the `Resource`s from the given reader
|
||||
/// Loads the [`Resource`]s from the given reader
|
||||
pub fn load_from_reader(reader: impl std::io::Read) -> Result<Self, ResourceLoadError> {
|
||||
let reader = std::io::BufReader::new(reader);
|
||||
debug!("Parsing as JSON...");
|
||||
|
@ -46,8 +52,10 @@ impl ResourceList {
|
|||
Ok(Self(resources))
|
||||
}
|
||||
|
||||
/// Save the [`Resource`]s to the given `path`, backing up the file if it is already present
|
||||
/// to `path.bak`
|
||||
#[instrument(level = "debug", skip(path, self))]
|
||||
pub fn save(&self, path: impl AsRef<Path> + Debug) -> Result<(), ResourceSaveError> {
|
||||
pub fn save(&self, path: impl AsRef<Path>) -> Result<(), ResourceSaveError> {
|
||||
info!("Creating backup before writing...");
|
||||
let path = path.as_ref();
|
||||
std::fs::rename(path, path.with_extension("bak"))
|
||||
|
@ -57,6 +65,7 @@ impl ResourceList {
|
|||
self.save_to_writer(file)
|
||||
}
|
||||
|
||||
/// Save the [`Resource`]s into the given `writer`
|
||||
pub fn save_to_writer(&self, writer: impl std::io::Write) -> Result<(), ResourceSaveError> {
|
||||
trace!("{self:?}");
|
||||
let writer = std::io::BufWriter::new(writer);
|
||||
|
@ -64,7 +73,8 @@ impl ResourceList {
|
|||
Ok(serde_json::to_writer(writer, &self.0)?)
|
||||
}
|
||||
|
||||
/// Merges `new_records` into the `ResourceList` with lookup collisions being handled as defined in `collision_handling`
|
||||
/// Merges `new_records` into the [`ResourceList`] with lookup collisions
|
||||
/// being handled as defined in `collision_handling`
|
||||
#[instrument(level = "debug", skip(self, new_records))]
|
||||
pub fn merge_records(
|
||||
&mut self,
|
||||
|
@ -74,6 +84,7 @@ impl ResourceList {
|
|||
debug!("Building hashset of already taken queries...");
|
||||
let unique_check: HashSet<String> =
|
||||
HashSet::from_iter(self.0.iter().flat_map(Resource::keys).cloned());
|
||||
|
||||
for record in new_records {
|
||||
let record_keys = HashSet::from_iter(record.keys().cloned());
|
||||
let collisions = unique_check
|
||||
|
|
Loading…
Reference in a new issue