use crate::{ApiEntityModel, EntityType, FatcatApiClient}; use anyhow::{anyhow, Context, Result}; use lazy_static::lazy_static; use regex::Regex; use std::fmt; use std::str::FromStr; #[derive(Debug, PartialEq, Clone)] pub enum ReleaseLookupKey { DOI, PMCID, PMID, Arxiv, Hdl, // TODO: the others } impl fmt::Display for ReleaseLookupKey { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::DOI => write!(f, "doi"), Self::PMCID => write!(f, "pmcid"), Self::PMID => write!(f, "pmid"), Self::Arxiv => write!(f, "arxiv"), Self::Hdl => write!(f, "hdl"), } } } #[derive(Debug, PartialEq, Clone)] pub enum ContainerLookupKey { ISSNL, ISSNE, ISSNP, ISSN, } impl fmt::Display for ContainerLookupKey { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::ISSNL => write!(f, "issnl"), Self::ISSNE => write!(f, "issne"), Self::ISSNP => write!(f, "issnp"), Self::ISSN => write!(f, "issn"), } } } #[derive(Debug, PartialEq, Clone)] pub enum CreatorLookupKey { Orcid, } impl fmt::Display for CreatorLookupKey { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Orcid => write!(f, "orcid"), } } } #[derive(Debug, PartialEq, Clone)] pub enum FileLookupKey { SHA1, SHA256, MD5, } impl fmt::Display for FileLookupKey { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::SHA1 => write!(f, "sha1"), Self::SHA256 => write!(f, "sha256"), Self::MD5 => write!(f, "md5"), } } } #[derive(Debug, PartialEq, Clone)] pub enum Specifier { Release(String), ReleaseLookup(ReleaseLookupKey, String), Work(String), Container(String), ContainerLookup(ContainerLookupKey, String), Creator(String), CreatorLookup(CreatorLookupKey, String), File(String), FileLookup(FileLookupKey, String), FileSet(String), WebCapture(String), Editgroup(String), Editor(String), EditorUsername(String), Changelog(i64), } impl Specifier { pub fn from_ident(entity_type: EntityType, ident: String) -> Specifier { match entity_type { EntityType::Release => Specifier::Release(ident), EntityType::Work => Specifier::Work(ident), EntityType::Container => Specifier::Container(ident), EntityType::Creator => Specifier::Creator(ident), EntityType::File => Specifier::File(ident), EntityType::FileSet => Specifier::FileSet(ident), EntityType::WebCapture => Specifier::WebCapture(ident), } } /// If this Specifier is a lookup, call the API to do the lookup and return the resulting /// specific entity specifier (eg, with an FCID). If already specific, just pass through. pub fn into_entity_specifier(self, api_client: &mut FatcatApiClient) -> Result { use Specifier::*; match self { Release(_) | Work(_) | Creator(_) | Container(_) | File(_) | FileSet(_) | WebCapture(_) | Editgroup(_) | Editor(_) | Changelog(_) => Ok(self), ReleaseLookup(_, _) => Ok(self.get_from_api(api_client, None, None)?.specifier()), ContainerLookup(_, _) => Ok(self.get_from_api(api_client, None, None)?.specifier()), CreatorLookup(_, _) => Ok(self.get_from_api(api_client, None, None)?.specifier()), FileLookup(_, _) => Ok(self.get_from_api(api_client, None, None)?.specifier()), EditorUsername(_) => Ok(self.get_from_api(api_client, None, None)?.specifier()), } } pub fn get_from_api( &self, api_client: &mut FatcatApiClient, expand: Option, hide: Option, ) -> Result> { use Specifier::*; let ret: Result> = match self { Release(fcid) => match api_client.rt.block_on(api_client.api.get_release( fcid.to_string(), expand, hide, ))? { fatcat_openapi::GetReleaseResponse::FoundEntity(model) => Ok(Box::new(model)), fatcat_openapi::GetReleaseResponse::BadRequest(err) => { Err(anyhow!("Bad Request ({}): {}", err.error, err.message)) } fatcat_openapi::GetReleaseResponse::NotFound(err) => { Err(anyhow!("Not Found: {}", err.message)) } resp => Err(anyhow!("{:?}", resp)) .with_context(|| format!("API GET failed: {:?}", self)), }, ReleaseLookup(ext_id, key) => { use ReleaseLookupKey::*; let (doi, pmcid, pmid, arxiv, hdl) = ( if let DOI = ext_id { Some(key.to_string()) } else { None }, if let PMCID = ext_id { Some(key.to_string()) } else { None }, if let PMID = ext_id { Some(key.to_string()) } else { None }, if let Arxiv = ext_id { Some(key.to_string()) } else { None }, if let Hdl = ext_id { Some(key.to_string()) } else { None }, ); // doi, wikidata, isbn13, pmid, pmcid, core, arxiv, jstor, ark, mag, hdl let result = api_client.rt.block_on(api_client.api.lookup_release( doi, None, None, pmid, pmcid, None, arxiv, None, None, None, None, None, None, hdl, expand, hide, ))?; match result { fatcat_openapi::LookupReleaseResponse::FoundEntity(model) => { Ok(Box::new(model)) } fatcat_openapi::LookupReleaseResponse::BadRequest(err) => { Err(anyhow!("Bad Request ({}): {}", err.error, err.message)) } fatcat_openapi::LookupReleaseResponse::NotFound(err) => { Err(anyhow!("Not Found: {}", err.message)) } resp => Err(anyhow!("{:?}", resp)) .with_context(|| format!("API GET failed: {:?}", self)), } } Work(fcid) => match api_client.rt.block_on(api_client.api.get_work( fcid.to_string(), expand, hide, ))? { fatcat_openapi::GetWorkResponse::FoundEntity(model) => Ok(Box::new(model)), fatcat_openapi::GetWorkResponse::BadRequest(err) => { Err(anyhow!("Bad Request ({}): {}", err.error, err.message)) } fatcat_openapi::GetWorkResponse::NotFound(err) => { Err(anyhow!("Not Found: {}", err.message)) } resp => Err(anyhow!("{:?}", resp)) .with_context(|| format!("API GET failed: {:?}", self)), }, Container(fcid) => match api_client.rt.block_on(api_client.api.get_container( fcid.to_string(), expand, hide, ))? { fatcat_openapi::GetContainerResponse::FoundEntity(model) => Ok(Box::new(model)), fatcat_openapi::GetContainerResponse::BadRequest(err) => { Err(anyhow!("Bad Request ({}): {}", err.error, err.message)) } fatcat_openapi::GetContainerResponse::NotFound(err) => { Err(anyhow!("Not Found: {}", err.message)) } resp => Err(anyhow!("{:?}", resp)) .with_context(|| format!("API GET failed: {:?}", self)), }, ContainerLookup(ext_id, key) => { use ContainerLookupKey::*; let (issnl, issnp, issne, issn) = ( if let ISSNL = ext_id { Some(key.to_string()) } else { None }, if let ISSNE = ext_id { Some(key.to_string()) } else { None }, if let ISSNP = ext_id { Some(key.to_string()) } else { None }, if let ISSN = ext_id { Some(key.to_string()) } else { None }, ); let result = api_client.rt.block_on( api_client .api .lookup_container(issnl, issne, issnp, issn, None, expand, hide), )?; match result { fatcat_openapi::LookupContainerResponse::FoundEntity(model) => { Ok(Box::new(model)) } fatcat_openapi::LookupContainerResponse::BadRequest(err) => { Err(anyhow!("Bad Request ({}): {}", err.error, err.message)) } fatcat_openapi::LookupContainerResponse::NotFound(err) => { Err(anyhow!("Not Found: {}", err.message)) } resp => Err(anyhow!("{:?}", resp)) .with_context(|| format!("API GET failed: {:?}", self)), } } Creator(fcid) => match api_client.rt.block_on(api_client.api.get_creator( fcid.to_string(), expand, hide, ))? { fatcat_openapi::GetCreatorResponse::FoundEntity(model) => Ok(Box::new(model)), fatcat_openapi::GetCreatorResponse::BadRequest(err) => { Err(anyhow!("Bad Request ({}): {}", err.error, err.message)) } fatcat_openapi::GetCreatorResponse::NotFound(err) => { Err(anyhow!("Not Found: {}", err.message)) } resp => Err(anyhow!("{:?}", resp)) .with_context(|| format!("API GET failed: {:?}", self)), }, CreatorLookup(ext_id, key) => { let result = api_client.rt.block_on(match ext_id { CreatorLookupKey::Orcid => { api_client .api .lookup_creator(Some(key.to_string()), None, expand, hide) } })?; match result { fatcat_openapi::LookupCreatorResponse::FoundEntity(model) => { Ok(Box::new(model)) } fatcat_openapi::LookupCreatorResponse::BadRequest(err) => { Err(anyhow!("Bad Request ({}): {}", err.error, err.message)) } fatcat_openapi::LookupCreatorResponse::NotFound(err) => { Err(anyhow!("Not Found: {}", err.message)) } resp => Err(anyhow!("{:?}", resp)) .with_context(|| format!("API GET failed: {:?}", self)), } } File(fcid) => match api_client.rt.block_on(api_client.api.get_file( fcid.to_string(), expand, hide, ))? { fatcat_openapi::GetFileResponse::FoundEntity(model) => Ok(Box::new(model)), fatcat_openapi::GetFileResponse::BadRequest(err) => { Err(anyhow!("Bad Request ({}): {}", err.error, err.message)) } fatcat_openapi::GetFileResponse::NotFound(err) => { Err(anyhow!("Not Found: {}", err.message)) } resp => Err(anyhow!("{:?}", resp)) .with_context(|| format!("API GET failed: {:?}", self)), }, FileLookup(hash, key) => { use FileLookupKey::*; let (sha1, sha256, md5) = ( if let SHA1 = hash { Some(key.to_string()) } else { None }, if let SHA256 = hash { Some(key.to_string()) } else { None }, if let MD5 = hash { Some(key.to_string()) } else { None }, ); let result = api_client .rt .block_on(api_client.api.lookup_file(md5, sha1, sha256, expand, hide))?; match result { fatcat_openapi::LookupFileResponse::FoundEntity(model) => Ok(Box::new(model)), fatcat_openapi::LookupFileResponse::BadRequest(err) => { Err(anyhow!("Bad Request ({}): {}", err.error, err.message)) } fatcat_openapi::LookupFileResponse::NotFound(err) => { Err(anyhow!("Not Found: {}", err.message)) } resp => Err(anyhow!("{:?}", resp)) .with_context(|| format!("API GET failed: {:?}", self)), } } FileSet(fcid) => match api_client.rt.block_on(api_client.api.get_fileset( fcid.to_string(), expand, hide, ))? { fatcat_openapi::GetFilesetResponse::FoundEntity(model) => Ok(Box::new(model)), fatcat_openapi::GetFilesetResponse::BadRequest(err) => { Err(anyhow!("Bad Request ({}): {}", err.error, err.message)) } fatcat_openapi::GetFilesetResponse::NotFound(err) => { Err(anyhow!("Not Found: {}", err.message)) } resp => Err(anyhow!("{:?}", resp)) .with_context(|| format!("API GET failed: {:?}", self)), }, WebCapture(fcid) => match api_client.rt.block_on(api_client.api.get_webcapture( fcid.to_string(), expand, hide, ))? { fatcat_openapi::GetWebcaptureResponse::FoundEntity(model) => Ok(Box::new(model)), fatcat_openapi::GetWebcaptureResponse::BadRequest(err) => { Err(anyhow!("Bad Request ({}): {}", err.error, err.message)) } fatcat_openapi::GetWebcaptureResponse::NotFound(err) => { Err(anyhow!("Not Found: {}", err.message)) } resp => Err(anyhow!("{:?}", resp)) .with_context(|| format!("API GET failed: {:?}", self)), }, Editgroup(fcid) => match api_client .rt .block_on(api_client.api.get_editgroup(fcid.to_string()))? { fatcat_openapi::GetEditgroupResponse::Found(model) => Ok(Box::new(model)), fatcat_openapi::GetEditgroupResponse::BadRequest(err) => { Err(anyhow!("Bad Request ({}): {}", err.error, err.message)) } fatcat_openapi::GetEditgroupResponse::NotFound(err) => { Err(anyhow!("Not Found: {}", err.message)) } resp => Err(anyhow!("{:?}", resp)) .with_context(|| format!("API GET failed: {:?}", self)), }, Editor(fcid) => match api_client .rt .block_on(api_client.api.get_editor(fcid.to_string()))? { fatcat_openapi::GetEditorResponse::Found(model) => Ok(Box::new(model)), fatcat_openapi::GetEditorResponse::BadRequest(err) => { Err(anyhow!("Bad Request ({}): {}", err.error, err.message)) } fatcat_openapi::GetEditorResponse::NotFound(err) => { Err(anyhow!("Not Found: {}", err.message)) } resp => Err(anyhow!("{:?}", resp)) .with_context(|| format!("API GET failed: {:?}", self)), }, Changelog(index) => match api_client .rt .block_on(api_client.api.get_changelog_entry(*index))? { fatcat_openapi::GetChangelogEntryResponse::FoundChangelogEntry(model) => { Ok(Box::new(model)) } fatcat_openapi::GetChangelogEntryResponse::BadRequest(err) => { Err(anyhow!("Bad Request ({}): {}", err.error, err.message)) } fatcat_openapi::GetChangelogEntryResponse::NotFound(err) => { Err(anyhow!("Not Found: {}", err.message)) } resp => Err(anyhow!("{:?}", resp)) .with_context(|| format!("API GET failed: {:?}", self)), }, EditorUsername(username) => match api_client .rt .block_on(api_client.api.lookup_editor(Some(username.to_string())))? { fatcat_openapi::LookupEditorResponse::Found(model) => Ok(Box::new(model)), fatcat_openapi::LookupEditorResponse::BadRequest(err) => { Err(anyhow!("Bad Request ({}): {}", err.error, err.message)) } fatcat_openapi::LookupEditorResponse::NotFound(err) => { Err(anyhow!("Not Found: {}", err.message)) } resp => Err(anyhow!("{:?}", resp)) .with_context(|| format!("API GET failed: {:?}", self)), }, }; match ret { Ok(_) => ret, Err(_) => ret.with_context(|| format!("Failed to GET {:?}", self)), } } pub fn get_history( &self, api_client: &mut FatcatApiClient, limit: Option, ) -> Result> { let limit: Option = limit.map(|v| v as i64); use Specifier::*; let ret: Result> = match self { Release(fcid) => match api_client .rt .block_on(api_client.api.get_release_history(fcid.to_string(), limit))? { fatcat_openapi::GetReleaseHistoryResponse::FoundEntityHistory(entries) => { Ok(entries) } fatcat_openapi::GetReleaseHistoryResponse::NotFound(err) => { Err(anyhow!("Not Found: {}", err.message)) } resp => Err(anyhow!("{:?}", resp)) .with_context(|| format!("API GET failed: {:?}", self)), }, Work(fcid) => match api_client .rt .block_on(api_client.api.get_work_history(fcid.to_string(), limit))? { fatcat_openapi::GetWorkHistoryResponse::FoundEntityHistory(entries) => Ok(entries), fatcat_openapi::GetWorkHistoryResponse::NotFound(err) => { Err(anyhow!("Not Found: {}", err.message)) } resp => Err(anyhow!("{:?}", resp)) .with_context(|| format!("API GET failed: {:?}", self)), }, Container(fcid) => match api_client.rt.block_on( api_client .api .get_container_history(fcid.to_string(), limit), )? { fatcat_openapi::GetContainerHistoryResponse::FoundEntityHistory(entries) => { Ok(entries) } fatcat_openapi::GetContainerHistoryResponse::NotFound(err) => { Err(anyhow!("Not Found: {}", err.message)) } resp => Err(anyhow!("{:?}", resp)) .with_context(|| format!("API GET failed: {:?}", self)), }, Creator(fcid) => match api_client .rt .block_on(api_client.api.get_creator_history(fcid.to_string(), limit))? { fatcat_openapi::GetCreatorHistoryResponse::FoundEntityHistory(entries) => { Ok(entries) } fatcat_openapi::GetCreatorHistoryResponse::NotFound(err) => { Err(anyhow!("Not Found: {}", err.message)) } resp => Err(anyhow!("{:?}", resp)) .with_context(|| format!("API GET failed: {:?}", self)), }, File(fcid) => match api_client .rt .block_on(api_client.api.get_file_history(fcid.to_string(), limit))? { fatcat_openapi::GetFileHistoryResponse::FoundEntityHistory(entries) => Ok(entries), fatcat_openapi::GetFileHistoryResponse::NotFound(err) => { Err(anyhow!("Not Found: {}", err.message)) } resp => Err(anyhow!("{:?}", resp)) .with_context(|| format!("API GET failed: {:?}", self)), }, FileSet(fcid) => match api_client .rt .block_on(api_client.api.get_fileset_history(fcid.to_string(), limit))? { fatcat_openapi::GetFilesetHistoryResponse::FoundEntityHistory(entries) => { Ok(entries) } fatcat_openapi::GetFilesetHistoryResponse::NotFound(err) => { Err(anyhow!("Not Found: {}", err.message)) } resp => Err(anyhow!("{:?}", resp)) .with_context(|| format!("API GET failed: {:?}", self)), }, WebCapture(fcid) => match api_client.rt.block_on( api_client .api .get_webcapture_history(fcid.to_string(), limit), )? { fatcat_openapi::GetWebcaptureHistoryResponse::FoundEntityHistory(entries) => { Ok(entries) } fatcat_openapi::GetWebcaptureHistoryResponse::NotFound(err) => { Err(anyhow!("Not Found: {}", err.message)) } resp => Err(anyhow!("{:?}", resp)) .with_context(|| format!("API GET failed: {:?}", self)), }, _ => Err(anyhow!("Don't know how to look up history for: {:?}", self)), }; match ret { Ok(_) => ret, Err(_) => ret.with_context(|| format!("Failed to GET history: {:?}", self)), } } } impl fmt::Display for Specifier { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Release(fcid) => write!(f, "release_{}", fcid), Self::ReleaseLookup(prefix, val) => write!(f, "{}:{}", prefix, val), Self::Work(fcid) => write!(f, "work_{}", fcid), Self::Container(fcid) => write!(f, "container_{}", fcid), Self::ContainerLookup(prefix, val) => write!(f, "{}:{}", prefix, val), Self::Creator(fcid) => write!(f, "creator_{}", fcid), Self::CreatorLookup(prefix, val) => write!(f, "{}:{}", prefix, val), Self::File(fcid) => write!(f, "file_{}", fcid), Self::FileLookup(prefix, val) => write!(f, "{}:{}", prefix, val), Self::FileSet(fcid) => write!(f, "fileset_{}", fcid), Self::WebCapture(fcid) => write!(f, "webcapture_{}", fcid), Self::Editgroup(fcid) => write!(f, "editgroup_{}", fcid), Self::Editor(fcid) => write!(f, "editor_{}", fcid), Self::EditorUsername(username) => write!(f, "username:{}", username), Self::Changelog(index) => write!(f, "changelog_{}", index), } } } impl FromStr for Specifier { type Err = anyhow::Error; fn from_str(s: &str) -> Result { // first try simple entity prefixes lazy_static! { static ref SPEC_ENTITY_RE: Regex = Regex::new(r"^(release|work|creator|container|file|fileset|webcapture|editgroup|editor)_([2-7a-z]{26})$").unwrap(); } if let Some(caps) = SPEC_ENTITY_RE.captures(s) { return match (&caps[1], &caps[2]) { ("release", fcid) => Ok(Specifier::Release(fcid.to_string())), ("work", fcid) => Ok(Specifier::Work(fcid.to_string())), ("container", fcid) => Ok(Specifier::Container(fcid.to_string())), ("creator", fcid) => Ok(Specifier::Creator(fcid.to_string())), ("file", fcid) => Ok(Specifier::File(fcid.to_string())), ("fileset", fcid) => Ok(Specifier::FileSet(fcid.to_string())), ("webcapture", fcid) => Ok(Specifier::WebCapture(fcid.to_string())), ("editgroup", fcid) => Ok(Specifier::Editgroup(fcid.to_string())), ("editor", fcid) => Ok(Specifier::Editor(fcid.to_string())), _ => Err(anyhow!("unexpected fatcat FCID type: {}", &caps[1])), }; } // then try lookup prefixes lazy_static! { static ref SPEC_LOOKUP_RE: Regex = Regex::new(r"^(doi|pmcid|pmid|arxiv|hdl|issnl|issne|issnp|issn|orcid|sha1|sha256|md5|username|u):(\S+)$") .unwrap(); } if let Some(caps) = SPEC_LOOKUP_RE.captures(s) { return match (&caps[1], &caps[2]) { ("doi", key) => Ok(Specifier::ReleaseLookup( ReleaseLookupKey::DOI, key.to_string(), )), ("pmcid", key) => Ok(Specifier::ReleaseLookup( ReleaseLookupKey::PMCID, key.to_string(), )), ("pmid", key) => Ok(Specifier::ReleaseLookup( ReleaseLookupKey::PMID, key.to_string(), )), ("arxiv", key) => Ok(Specifier::ReleaseLookup( ReleaseLookupKey::Arxiv, key.to_string(), )), ("hdl", key) => Ok(Specifier::ReleaseLookup( ReleaseLookupKey::Hdl, key.to_string(), )), ("issnl", key) => Ok(Specifier::ContainerLookup( ContainerLookupKey::ISSNL, key.to_string(), )), ("issne", key) => Ok(Specifier::ContainerLookup( ContainerLookupKey::ISSNE, key.to_string(), )), ("issnp", key) => Ok(Specifier::ContainerLookup( ContainerLookupKey::ISSNP, key.to_string(), )), ("issn", key) => Ok(Specifier::ContainerLookup( ContainerLookupKey::ISSN, key.to_string(), )), ("orcid", key) => Ok(Specifier::CreatorLookup( CreatorLookupKey::Orcid, key.to_string(), )), ("sha1", key) => Ok(Specifier::FileLookup(FileLookupKey::SHA1, key.to_string())), ("sha256", key) => Ok(Specifier::FileLookup( FileLookupKey::SHA256, key.to_string(), )), ("md5", key) => Ok(Specifier::FileLookup(FileLookupKey::MD5, key.to_string())), ("username", key) | ("u", key) => Ok(Specifier::EditorUsername(key.to_string())), _ => Err(anyhow!("unexpected entity lookup type: {}", &caps[1])), }; } // lastly, changelog entity lookup lazy_static! { static ref SPEC_CHANGELOG_RE: Regex = Regex::new(r"^changelog_(\d+)$").unwrap(); }; if let Some(caps) = SPEC_CHANGELOG_RE.captures(s) { return Ok(Specifier::Changelog(caps[1].parse::()?)); } Err(anyhow!( "expecting a specifier: entity identifier or key/value lookup: {}", s )) } } pub struct EditgroupSpecifier(String); impl EditgroupSpecifier { pub fn into_string(self) -> String { self.0 } } impl FromStr for EditgroupSpecifier { type Err = anyhow::Error; fn from_str(s: &str) -> Result { lazy_static! { static ref SPEC_ENTITY_RE: Regex = Regex::new(r"^(editgroup_)?([2-7a-z]{26})$").unwrap(); } if let Some(caps) = SPEC_ENTITY_RE.captures(s) { return Ok(EditgroupSpecifier(caps[2].to_string())); } Err(anyhow!("expecting an editgroup identifier, got: {}", s)) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_specifier_from_str() -> () { assert!(Specifier::from_str("release_asdf").is_err()); assert_eq!( Specifier::from_str("creator_iimvc523xbhqlav6j3sbthuehu").unwrap(), Specifier::Creator("iimvc523xbhqlav6j3sbthuehu".to_string()) ); assert_eq!( Specifier::from_str("username:big-bot").unwrap(), Specifier::EditorUsername("big-bot".to_string()) ); assert_eq!( Specifier::from_str("u:big-bot").unwrap(), Specifier::EditorUsername("big-bot".to_string()) ); assert_eq!( Specifier::from_str("doi:10.1234/a!s.df+-d").unwrap(), Specifier::ReleaseLookup(ReleaseLookupKey::DOI, "10.1234/a!s.df+-d".to_string()) ); assert!(Specifier::from_str("doi:").is_err()); assert_eq!( Specifier::from_str("changelog_1234").unwrap(), Specifier::Changelog(1234) ); assert!(Specifier::from_str("changelog_12E4").is_err()); } #[test] fn test_editgroup_from_str() -> () { assert!(EditgroupSpecifier::from_str("release_asdf").is_err()); assert_eq!( EditgroupSpecifier::from_str("editgroup_iimvc523xbhqlav6j3sbthuehu") .unwrap() .0, "iimvc523xbhqlav6j3sbthuehu".to_string() ); assert_eq!( EditgroupSpecifier::from_str("iimvc523xbhqlav6j3sbthuehu") .unwrap() .0, "iimvc523xbhqlav6j3sbthuehu".to_string() ); } }