diff --git a/src/editor/commands/editor.rs b/src/editor/commands/editor.rs index 93603bc..4bc2d22 100644 --- a/src/editor/commands/editor.rs +++ b/src/editor/commands/editor.rs @@ -5,20 +5,23 @@ use crate::schema::resource::Resource; use std::path::PathBuf; use tracing::{error, info}; -pub fn editor(database_path: PathBuf, save_settings: SaveSettings, resource: String, create: 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.as_str()) { + + let (index, resource) = match resources.lookup_with_index(resource_lookup.as_str()) { None => { - if create { - (None, Resource::new(resource)) + if create_new_resource { + (None, Resource::new(resource_lookup)) } else { - error!("Couldn't find a resource for the query \"{resource}\""); + error!("Couldn't find a resource for the query \"{resource_lookup}\""); return; } } Some((index, resource)) => (Some(index), resource.clone()), }; - let resource = open_resource_in_editor(resource).unwrap(); + + let resource = open_resource_in_editor(&resource).unwrap(); + if save_settings.save { let mut resources = resources.into_inner(); if let Some(index) = index { diff --git a/src/editor/open_in_editor.rs b/src/editor/open_in_editor.rs index fd97868..4f7599e 100644 --- a/src/editor/open_in_editor.rs +++ b/src/editor/open_in_editor.rs @@ -1,5 +1,4 @@ use crate::schema::resource::Resource; -use crate::schema::resource_complete_serialisation::ResourceComplete; use std::fmt::{Debug, Display}; use std::io::{Read, Seek, SeekFrom, Write}; use std::process::Command; @@ -71,21 +70,22 @@ pub fn spawn_editor(buffer: impl AsRef + Display) -> Result Result { +#[instrument(skip_all)] +pub fn open_resource_in_editor(resource: &Resource) -> Result { use ResourceEditingError::*; - let resource = ResourceComplete::from_resource(resource); + + let resource = resource.as_completely_serializable(); let printed = serde_json::to_string_pretty(&resource).map_err(PrettyPrintFailed)?; let edited = spawn_editor(printed)?; - let parsed: ResourceComplete = serde_json::from_str(edited.as_str()).map_err(ParseFailed)?; - Ok(parsed.into_resource()) + let parsed: Resource = serde_json::from_str(edited.as_str()).map_err(ParseFailed)?; + Ok(parsed.compress()) } diff --git a/src/schema/mod.rs b/src/schema/mod.rs index c87de93..485aae9 100644 --- a/src/schema/mod.rs +++ b/src/schema/mod.rs @@ -1,4 +1,3 @@ pub mod lookup_handler; pub mod resource; -pub mod resource_complete_serialisation; pub mod resource_list; diff --git a/src/schema/resource.rs b/src/schema/resource.rs index 6957dda..ae2e179 100644 --- a/src/schema/resource.rs +++ b/src/schema/resource.rs @@ -63,6 +63,70 @@ impl Resource { self.aliases = Some(vec![subject]) } } + + /// Returns a clone of the underlying data with all instances of `Option::None` removed + /// 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>| { + 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()), + } + } + + /// Replaces all instances of `Some(Default::default())` with `None` + /// + /// Useful to reduce the size of the serialized representation. + pub fn compress(self) -> Self { + 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; + } + } + } + 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 + }); + + links + }) + } + } } /// A link contained within a WebFinger resource @@ -169,4 +233,15 @@ mod tests { check.subject = "new_subject".to_string(); assert_eq!(data, check) } + + #[test] + fn test_compression_decompression_round_trip() { + for data in [ + test_data::barebones_user(), + test_data::user_with_matching_subject_and_alias(), + test_data::user_with_single_alias() + ] { + assert_eq!(data, data.as_completely_serializable().compress()); + } + } }