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: better representation of widget configuration #380

Merged
merged 2 commits into from
Feb 1, 2025
Merged
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
28 changes: 14 additions & 14 deletions crates/deskulpt-core/src/commands/bundle_widget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ use tauri::{command, AppHandle, Runtime};

use super::error::{cmderr, CmdResult};
use crate::bundler::WidgetBundlerBuilder;
use crate::config::WidgetConfig;
use crate::path::PathExt;
use crate::states::StatesExtWidgetCollection;
use crate::states::StatesExtWidgetConfigMap;

/// Bundle a widget.
///
/// ### Errors
///
/// - Widget ID does not exist in the collection.
/// - Widget ID does not exist in the configuration map.
/// - Widget has a configuration error.
/// - Error bundling the widget.
#[command]
Expand All @@ -21,24 +22,23 @@ pub async fn bundle_widget<R: Runtime>(
apis_blob_url: String,
) -> CmdResult<String> {
let widgets_dir = app_handle.widgets_dir();
let widget_dir = widgets_dir.join(&id);

let mut bundler = app_handle.with_widget_collection(|collection| {
collection
let mut bundler = app_handle.with_widget_config_map(|config_map| {
match config_map
.get(&id)
.ok_or_else(|| cmderr!("Widget (id={}) does not exist in the collection", id))?
.as_ref()
.map(|config| {
.ok_or_else(|| cmderr!("Widget (id={}) does not exist", id))?
{
WidgetConfig::Valid { dir, entry, .. } => {
let builder = WidgetBundlerBuilder::new(
widget_dir.to_path_buf(),
config.entry(),
widgets_dir.join(dir),
entry.clone(),
base_url,
apis_blob_url,
);
builder.build()
})
// Propagate the configuration error message
.map_err(|e| cmderr!(e.to_string()))
Ok(builder.build())
},
WidgetConfig::Invalid { error, .. } => Err(cmderr!(error.clone())),
}
})?;

let code = bundler
Expand Down
38 changes: 15 additions & 23 deletions crates/deskulpt-core/src/commands/rescan_widgets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,25 @@ use std::fs::read_dir;
use tauri::{command, AppHandle, Runtime};

use super::error::{cmdbail, CmdResult};
use crate::config::{WidgetCollection, WidgetConfig};
use crate::config::WidgetConfig;
use crate::path::PathExt;
use crate::states::StatesExtWidgetCollection;
use crate::states::StatesExtWidgetConfigMap;

/// Rescan the widgets directory and update the widget collection.
/// Rescan the widgets directory and update the widget configuration map.
///
/// This will update the widget collection state and return the updated
/// collection as well.
/// This will update the widget configuration map state and return the updated
/// configuration map as well.
///
/// ### Errors
///
/// - Error traversing the widgets directory.
/// - Error inferring widget ID from the directory entry.
#[command]
pub async fn rescan_widgets<R: Runtime>(app_handle: AppHandle<R>) -> CmdResult<WidgetCollection> {
pub async fn rescan_widgets<R: Runtime>(
app_handle: AppHandle<R>,
) -> CmdResult<HashMap<String, WidgetConfig>> {
let widgets_dir = app_handle.widgets_dir();
let mut new_widget_collection = HashMap::new();
let mut new_config_map = HashMap::new();

let entries = read_dir(widgets_dir)?;
for entry in entries {
Expand All @@ -36,23 +38,13 @@ pub async fn rescan_widgets<R: Runtime>(app_handle: AppHandle<R>) -> CmdResult<W
None => cmdbail!("Invalid widget directory: '{}'", path.display()),
};

// Load the widget configuration
match WidgetConfig::load(&path) {
Ok(Some(widget_config)) => {
new_widget_collection.insert(id, Ok(widget_config));
},
Ok(None) => {},
Err(e) => {
// Configuration errors are recorded as error messages and do
// not fail the command
new_widget_collection.insert(id, Err(e.to_string()));
},
};
if let Some(widget_config) = WidgetConfig::load(&path) {
new_config_map.insert(id, widget_config);
}
}

// Update the widget collection state
app_handle.with_widget_collection_mut(|collection| {
collection.clone_from(&new_widget_collection);
app_handle.with_widget_config_map_mut(|config_map| {
config_map.clone_from(&new_config_map);
});
Ok(new_widget_collection)
Ok(new_config_map)
}
142 changes: 91 additions & 51 deletions crates/deskulpt-core/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Configuration of Deskulpt widgets.

use std::collections::{HashMap, HashSet};
use std::collections::HashMap;
use std::fs::File;
use std::io::BufReader;
use std::path::Path;
Expand All @@ -24,73 +24,113 @@ struct PackageJson {
dependencies: HashMap<String, String>,
}

/// Full configuration of a Deskulpt widget.
#[derive(Clone, Serialize, PartialEq, Debug)]
pub struct WidgetConfig {
/// Name of the widget.
name: String,
/// Entry file of the widget, relative to the widget directory.
entry: String,
/// Whether the widget is ignored.
ignore: bool,
/// The dependency mapping from package names to versions.
dependencies: HashMap<String, String>,
/// Macro for implementing [`DeskulptConf::load`] and [`PackageJson::load`].
///
/// The first argument is the type to implement the method on, and the second
/// argument is the path to the target file within the widget directory.
macro_rules! impl_load {
($type:ty, $path:expr) => {
impl $type {
/// Load `
#[doc = $path]
/// ` from a directory.
///
/// This method returns `Ok(None)` if the target file does not exist
/// and `Err` if there is failure to read or parse the file.
fn load(dir: &Path) -> Result<Option<Self>> {
let path = dir.join($path);
if !path.exists() {
return Ok(None);
}

let file = File::open(path)?;
let reader = BufReader::new(file);
let config = serde_json::from_reader(reader)?;
Ok(Some(config))
}
}
};
}

/// The widget collection.
///
/// This is a mapping from widget IDs to either widget configurations if valid
/// or configuration error messages otherwise.
pub type WidgetCollection = HashMap<String, Result<WidgetConfig, String>>;
impl_load!(DeskulptConf, "deskulpt.conf.json");
impl_load!(PackageJson, "package.json");

/// Full configuration of a Deskulpt widget.
#[derive(Serialize, Clone)]
#[serde(tag = "type", content = "content", rename_all = "SCREAMING_SNAKE_CASE")]
pub enum WidgetConfig {
/// Valid widget configuration.
#[serde(rename_all = "camelCase")]
Valid {
/// The directory name of the widget.
dir: String,
/// Display name of the widget.
name: String,
/// Entry file of the widget source code.
entry: String,
/// External dependencies of the widget as in `package.json`.
dependencies: HashMap<String, String>,
},
/// Invalid widget configuration.
#[serde(rename_all = "camelCase")]
Invalid {
/// The directory name of the widget.
dir: String,
/// Error message.
error: String,
},
}

impl WidgetConfig {
/// Read widget configuratoin from a directory.
pub fn load<P: AsRef<Path>>(dir: P) -> Result<Option<Self>> {
/// Read widget configuration from a directory.
///
/// This returns `None` if the directory is not considered a widget
/// directory, or if the widget is explicitly marked as ignored.
pub fn load<P: AsRef<Path>>(dir: P) -> Option<Self> {
let dir = dir.as_ref();
debug_assert!(dir.is_absolute() && dir.is_dir());
let dir_name = dir.file_name()?.to_string_lossy();

let deskulpt_conf_path = dir.join("deskulpt.conf.json");
if !deskulpt_conf_path.exists() {
return Ok(None);
}
let deskulpt_conf_file =
File::open(&deskulpt_conf_path).context("Failed to open deskulpt.conf.json")?;
let deskulpt_conf_reader = BufReader::new(deskulpt_conf_file);
let deskulpt_conf: DeskulptConf = serde_json::from_reader(deskulpt_conf_reader)
.context("Failed to parse deskulpt.conf.json")?;
let deskulpt_conf =
match DeskulptConf::load(dir).context("Failed to load deskulpt.conf.json") {
Ok(Some(deskulpt_conf)) => deskulpt_conf,
Ok(None) => return None,
Err(e) => {
return Some(WidgetConfig::Invalid {
dir: dir_name.to_string(),
error: e.to_string(),
})
},
};

// Ignore widgets that are explcitly marked as such
if deskulpt_conf.ignore {
return Ok(None);
return None;
}

let package_json_path = dir.join("package.json");
let dependencies = if package_json_path.exists() {
let package_json_file =
File::open(&package_json_path).context("Failed to open package.json")?;
let reader = BufReader::new(package_json_file);
let package_json: PackageJson =
serde_json::from_reader(reader).context("Failed to parse package.json")?;
package_json.dependencies
} else {
Default::default()
let dependencies = match PackageJson::load(dir).context("Failed to load package.json") {
Ok(Some(package_json)) => package_json.dependencies,
Ok(None) => Default::default(),
Err(e) => {
return Some(WidgetConfig::Invalid {
dir: dir_name.to_string(),
error: e.to_string(),
})
},
};

Ok(Some(WidgetConfig {
Some(WidgetConfig::Valid {
dir: dir_name.to_string(),
name: deskulpt_conf.name,
entry: deskulpt_conf.entry,
ignore: deskulpt_conf.ignore,
dependencies,
}))
}

/// Get the entry file of the widget.
pub fn entry(&self) -> String {
self.entry.to_string()
})
}

/// Get the set of external dependencies of the widget.
pub fn external_deps(&self) -> HashSet<String> {
self.dependencies.keys().cloned().collect()
/// Get the directory of the widget inside the widgets directory.
pub fn dir(&self) -> &str {
match self {
WidgetConfig::Valid { dir, .. } => dir,
WidgetConfig::Invalid { dir, .. } => dir,
}
}
}
2 changes: 1 addition & 1 deletion crates/deskulpt-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@ mod window;
pub use events::EventsExt;
pub use path::PathExt;
pub use settings::Settings;
pub use states::{StatesExtCanvasClickThrough, StatesExtWidgetCollection};
pub use states::{StatesExtCanvasClickThrough, StatesExtWidgetConfigMap};
pub use tray::TrayExt;
pub use window::{on_window_event, WindowExt};
4 changes: 2 additions & 2 deletions crates/deskulpt-core/src/states/mod.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
//! Deskulpt runtime state management.

mod canvas_click_through;
mod widget_collection;
mod widget_config_map;

#[doc(hidden)]
pub use canvas_click_through::StatesExtCanvasClickThrough;
#[doc(hidden)]
pub use widget_collection::StatesExtWidgetCollection;
pub use widget_config_map::StatesExtWidgetConfigMap;
48 changes: 0 additions & 48 deletions crates/deskulpt-core/src/states/widget_collection.rs

This file was deleted.

Loading